Compare commits
20 Commits
e8058d1240
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4fd81ffbf2 | ||
|
|
cf39ea2b0c | ||
|
|
560f40ae8a | ||
|
|
838c0afc0b | ||
|
|
0666b8683e | ||
|
|
428b6292ea | ||
|
|
cd7feec38a | ||
|
|
01a4580faf | ||
|
|
649af715e2 | ||
|
|
3571cdf2ee | ||
|
|
099ed03be7 | ||
|
|
f62ed97e2b | ||
|
|
760902f53c | ||
|
|
998771bc11 | ||
|
|
576ba9b2fe | ||
|
|
b7b312e69a | ||
|
|
865be73d06 | ||
|
|
ed4b22a564 | ||
|
|
2da43cf027 | ||
|
|
200c86c924 |
2
backend
2
backend
Submodule backend updated: 60608fe199...405000ded5
@@ -3,15 +3,12 @@ services:
|
||||
build:
|
||||
context: ./backend
|
||||
dockerfile: Dockerfile
|
||||
container_name: memberapi-backend
|
||||
container_name: memberapi_ose_tw
|
||||
restart: unless-stopped
|
||||
env_file:
|
||||
- ./backend/.env
|
||||
ports:
|
||||
- "127.0.0.1:8000:8000"
|
||||
depends_on:
|
||||
redis:
|
||||
condition: service_healthy
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "wget -qO- http://127.0.0.1:8000/healthz >/dev/null || exit 1"]
|
||||
interval: 30s
|
||||
@@ -19,26 +16,14 @@ services:
|
||||
retries: 5
|
||||
start_period: 20s
|
||||
networks:
|
||||
- member-net
|
||||
|
||||
redis:
|
||||
image: redis:7-alpine
|
||||
container_name: memberapi-redis
|
||||
restart: unless-stopped
|
||||
command: ["redis-server", "--appendonly", "yes"]
|
||||
volumes:
|
||||
- redis-data:/data
|
||||
healthcheck:
|
||||
test: ["CMD", "redis-cli", "ping"]
|
||||
interval: 10s
|
||||
timeout: 3s
|
||||
retries: 5
|
||||
networks:
|
||||
- member-net
|
||||
- postgres
|
||||
- ose-cache
|
||||
- nginx
|
||||
|
||||
networks:
|
||||
member-net:
|
||||
driver: bridge
|
||||
|
||||
volumes:
|
||||
redis-data:
|
||||
postgres:
|
||||
external: true
|
||||
ose-cache:
|
||||
external: true
|
||||
nginx:
|
||||
external: true
|
||||
|
||||
@@ -34,6 +34,7 @@
|
||||
## 4) roles
|
||||
- `id` UUID PK default `gen_random_uuid()`
|
||||
- `role_key` TEXT NOT NULL UNIQUE
|
||||
- `role_code` TEXT NOT NULL(語意代碼,建議格式:`<system>:<module>:<action>`,例如 `mkt:marketing_card:edit`)
|
||||
- `system_id` UUID NOT NULL FK -> `systems(id)` ON DELETE CASCADE
|
||||
- `name` TEXT NOT NULL
|
||||
- `description` TEXT
|
||||
@@ -41,6 +42,7 @@
|
||||
- `created_at` TIMESTAMPTZ NOT NULL default `now()`
|
||||
- `updated_at` TIMESTAMPTZ NOT NULL default `now()`
|
||||
- UNIQUE(`system_id`, `name`)
|
||||
- UNIQUE(`system_id`, `role_code`)
|
||||
|
||||
## 5) site_roles
|
||||
- `id` UUID PK default `gen_random_uuid()`
|
||||
|
||||
@@ -36,6 +36,7 @@
|
||||
"system_key": "SY20260402X0001",
|
||||
"system_name": "Marketing",
|
||||
"role_key": "RL20260402X0002",
|
||||
"role_code": "mkt:marketing_card:edit",
|
||||
"role_name": "campaign_edit"
|
||||
}
|
||||
]
|
||||
@@ -45,3 +46,4 @@
|
||||
## 注意事項
|
||||
- 不提供 user direct role 寫入 API。
|
||||
- User 最終角色由 `user_sites` + `site_roles` 推導。
|
||||
- `role_key` 是唯一識別鍵;業務語意解析請使用 `role_code`。
|
||||
|
||||
@@ -8,13 +8,13 @@ psql "$DATABASE_URL" -f scripts/init_schema.sql
|
||||
- DB schema 檔案:[backend/scripts/init_schema.sql](../backend/scripts/init_schema.sql)
|
||||
|
||||
## 2) 啟動後端
|
||||
先準備 `.env`:
|
||||
本地開發使用 `.env.development`:
|
||||
```bash
|
||||
cd backend
|
||||
cp .env.example .env
|
||||
# edit .env.development directly
|
||||
```
|
||||
|
||||
本機開發與 VPS 一律使用 `backend/.env`(不再分 `.env.development`)。
|
||||
本機開發固定使用 `backend/.env.development`。
|
||||
|
||||
再啟動:
|
||||
```bash
|
||||
@@ -30,9 +30,11 @@ cd frontend
|
||||
npm install
|
||||
npm run dev
|
||||
```
|
||||
- 本地開發固定使用 `frontend/.env.development`。
|
||||
- production build 讀取 `frontend/.env.production`。
|
||||
- 專案路徑:[frontend](../frontend)
|
||||
|
||||
## 4) 必要環境變數([backend/.env](../backend/.env))
|
||||
## 4) 必要環境變數([backend/.env.development](../backend/.env.development))
|
||||
- `KEYCLOAK_BASE_URL`
|
||||
- `KEYCLOAK_REALM`
|
||||
- `KEYCLOAK_CLIENT_ID`
|
||||
|
||||
@@ -11,7 +11,7 @@ git submodule update --init --recursive
|
||||
## 2) 後端部署(Docker)
|
||||
```bash
|
||||
cd /opt/member-platform/backend
|
||||
cp .env.example .env
|
||||
cp .env.production .env
|
||||
```
|
||||
編輯 `.env`(DB、Keycloak、Realm Roles、Cache)。
|
||||
|
||||
@@ -63,9 +63,8 @@ docker compose down
|
||||
## 3) 前端部署(Nginx)
|
||||
```bash
|
||||
cd /opt/member-platform/frontend
|
||||
cp .env.example .env
|
||||
```
|
||||
設定:
|
||||
production build 會自動讀取 `.env.production`,請先確認設定:
|
||||
```env
|
||||
VITE_API_BASE_URL=https://memberapi.ose.tw
|
||||
```
|
||||
|
||||
81
docs/directus/directus_key_autogen.sql
Normal file
81
docs/directus/directus_key_autogen.sql
Normal file
@@ -0,0 +1,81 @@
|
||||
-- Directus key auto-generation triggers
|
||||
-- Target tables: companies, sites, users, roles
|
||||
-- Key format: PREFIX + yyyymmdd + 'X' + 4 digits
|
||||
-- Example: CP20260404X1234
|
||||
|
||||
BEGIN;
|
||||
|
||||
CREATE OR REPLACE FUNCTION public.directus_autogen_entity_key()
|
||||
RETURNS trigger
|
||||
LANGUAGE plpgsql
|
||||
AS $$
|
||||
DECLARE
|
||||
v_column_name text := TG_ARGV[0];
|
||||
v_prefix text := TG_ARGV[1];
|
||||
v_current_value text;
|
||||
v_candidate text;
|
||||
v_exists boolean;
|
||||
v_attempt int;
|
||||
v_day text;
|
||||
v_suffix text;
|
||||
BEGIN
|
||||
v_current_value := to_jsonb(NEW) ->> v_column_name;
|
||||
IF v_current_value IS NOT NULL AND btrim(v_current_value) <> '' THEN
|
||||
RETURN NEW;
|
||||
END IF;
|
||||
|
||||
v_day := to_char(clock_timestamp(), 'YYYYMMDD');
|
||||
|
||||
FOR v_attempt IN 0..9999 LOOP
|
||||
v_suffix := lpad((((extract(epoch FROM clock_timestamp()) * 1000)::bigint + v_attempt) % 10000)::text, 4, '0');
|
||||
v_candidate := v_prefix || v_day || 'X' || v_suffix;
|
||||
|
||||
EXECUTE format(
|
||||
'SELECT EXISTS (SELECT 1 FROM %I.%I WHERE %I = $1)',
|
||||
TG_TABLE_SCHEMA,
|
||||
TG_TABLE_NAME,
|
||||
v_column_name
|
||||
) INTO v_exists USING v_candidate;
|
||||
|
||||
IF NOT v_exists THEN
|
||||
NEW := jsonb_populate_record(NEW, jsonb_build_object(v_column_name, v_candidate));
|
||||
RETURN NEW;
|
||||
END IF;
|
||||
END LOOP;
|
||||
|
||||
RAISE EXCEPTION 'Failed to generate unique key for %.% (column=%)', TG_TABLE_SCHEMA, TG_TABLE_NAME, v_column_name;
|
||||
END;
|
||||
$$;
|
||||
|
||||
DROP TRIGGER IF EXISTS trg_companies_key_autogen ON public.companies;
|
||||
CREATE TRIGGER trg_companies_key_autogen
|
||||
BEFORE INSERT ON public.companies
|
||||
FOR EACH ROW
|
||||
EXECUTE FUNCTION public.directus_autogen_entity_key('key', 'CP');
|
||||
|
||||
DROP TRIGGER IF EXISTS trg_sites_key_autogen ON public.sites;
|
||||
CREATE TRIGGER trg_sites_key_autogen
|
||||
BEFORE INSERT ON public.sites
|
||||
FOR EACH ROW
|
||||
EXECUTE FUNCTION public.directus_autogen_entity_key('key', 'ST');
|
||||
|
||||
DROP TRIGGER IF EXISTS trg_users_key_autogen ON public.users;
|
||||
CREATE TRIGGER trg_users_key_autogen
|
||||
BEFORE INSERT ON public.users
|
||||
FOR EACH ROW
|
||||
EXECUTE FUNCTION public.directus_autogen_entity_key('key', 'UE');
|
||||
|
||||
DROP TRIGGER IF EXISTS trg_roles_key_autogen ON public.roles;
|
||||
CREATE TRIGGER trg_roles_key_autogen
|
||||
BEFORE INSERT ON public.roles
|
||||
FOR EACH ROW
|
||||
EXECUTE FUNCTION public.directus_autogen_entity_key('key', 'RL');
|
||||
|
||||
-- cleanup old trigger names to avoid duplicates from previous scripts
|
||||
DROP TRIGGER IF EXISTS trg_companies_company_key_autogen ON public.companies;
|
||||
DROP TRIGGER IF EXISTS trg_sites_site_key_autogen ON public.sites;
|
||||
DROP TRIGGER IF EXISTS trg_users_user_key_autogen ON public.users;
|
||||
DROP TRIGGER IF EXISTS trg_systems_system_key_autogen ON public.systems;
|
||||
DROP TRIGGER IF EXISTS trg_roles_role_key_autogen ON public.roles;
|
||||
|
||||
COMMIT;
|
||||
3657
docs/directus/member-schema.directus.json
Normal file
3657
docs/directus/member-schema.directus.json
Normal file
File diff suppressed because it is too large
Load Diff
3656
docs/directus/member-schema.directus.uuid.json
Normal file
3656
docs/directus/member-schema.directus.uuid.json
Normal file
File diff suppressed because it is too large
Load Diff
3702
docs/directus/member-schema.directus.uuid.keys.json
Normal file
3702
docs/directus/member-schema.directus.uuid.keys.json
Normal file
File diff suppressed because it is too large
Load Diff
182
docs/directus/member-schema.snapshot.json
Normal file
182
docs/directus/member-schema.snapshot.json
Normal file
@@ -0,0 +1,182 @@
|
||||
{
|
||||
"version": 1,
|
||||
"directus": "11.0.0",
|
||||
"vendor": "postgres",
|
||||
"collections": [
|
||||
{ "collection": "users", "meta": null, "schema": { "name": "users" } },
|
||||
{ "collection": "companies", "meta": null, "schema": { "name": "companies" } },
|
||||
{ "collection": "sites", "meta": null, "schema": { "name": "sites" } },
|
||||
{ "collection": "systems", "meta": null, "schema": { "name": "systems" } },
|
||||
{ "collection": "roles", "meta": null, "schema": { "name": "roles" } },
|
||||
{ "collection": "site_roles", "meta": null, "schema": { "name": "site_roles" } },
|
||||
{ "collection": "user_sites", "meta": null, "schema": { "name": "user_sites" } },
|
||||
{ "collection": "auth_sync_state", "meta": null, "schema": { "name": "auth_sync_state" } },
|
||||
{ "collection": "api_clients", "meta": null, "schema": { "name": "api_clients" } }
|
||||
],
|
||||
"fields": [
|
||||
{ "collection": "users", "field": "id", "type": "uuid", "meta": null, "schema": { "is_nullable": false, "is_primary_key": true, "has_auto_increment": false, "default_value": "gen_random_uuid()" } },
|
||||
{ "collection": "users", "field": "user_sub", "type": "string", "meta": null, "schema": { "is_nullable": false, "is_unique": true } },
|
||||
{ "collection": "users", "field": "provider_user_id", "type": "string", "meta": null, "schema": { "is_nullable": true, "is_unique": true } },
|
||||
{ "collection": "users", "field": "username", "type": "string", "meta": null, "schema": { "is_nullable": true, "is_unique": true } },
|
||||
{ "collection": "users", "field": "email", "type": "string", "meta": null, "schema": { "is_nullable": true, "is_unique": true } },
|
||||
{ "collection": "users", "field": "display_name", "type": "string", "meta": null, "schema": { "is_nullable": true } },
|
||||
{ "collection": "users", "field": "status", "type": "string", "meta": null, "schema": { "is_nullable": false, "default_value": "active" } },
|
||||
{ "collection": "users", "field": "is_active", "type": "boolean", "meta": null, "schema": { "is_nullable": false, "default_value": true } },
|
||||
{ "collection": "users", "field": "created_at", "type": "timestamp", "meta": null, "schema": { "is_nullable": false, "default_value": "now()" } },
|
||||
{ "collection": "users", "field": "updated_at", "type": "timestamp", "meta": null, "schema": { "is_nullable": false, "default_value": "now()" } },
|
||||
|
||||
{ "collection": "companies", "field": "id", "type": "uuid", "meta": null, "schema": { "is_nullable": false, "is_primary_key": true, "default_value": "gen_random_uuid()" } },
|
||||
{ "collection": "companies", "field": "company_key", "type": "string", "meta": null, "schema": { "is_nullable": false, "is_unique": true } },
|
||||
{ "collection": "companies", "field": "name", "type": "string", "meta": null, "schema": { "is_nullable": false } },
|
||||
{ "collection": "companies", "field": "provider_group_id", "type": "string", "meta": null, "schema": { "is_nullable": true } },
|
||||
{ "collection": "companies", "field": "status", "type": "string", "meta": null, "schema": { "is_nullable": false, "default_value": "active" } },
|
||||
{ "collection": "companies", "field": "created_at", "type": "timestamp", "meta": null, "schema": { "is_nullable": false, "default_value": "now()" } },
|
||||
{ "collection": "companies", "field": "updated_at", "type": "timestamp", "meta": null, "schema": { "is_nullable": false, "default_value": "now()" } },
|
||||
|
||||
{ "collection": "sites", "field": "id", "type": "uuid", "meta": null, "schema": { "is_nullable": false, "is_primary_key": true, "default_value": "gen_random_uuid()" } },
|
||||
{ "collection": "sites", "field": "site_key", "type": "string", "meta": null, "schema": { "is_nullable": false, "is_unique": true } },
|
||||
{ "collection": "sites", "field": "company_id", "type": "uuid", "meta": null, "schema": { "is_nullable": false, "foreign_key_table": "companies", "foreign_key_column": "id" } },
|
||||
{ "collection": "sites", "field": "display_name", "type": "string", "meta": null, "schema": { "is_nullable": false } },
|
||||
{ "collection": "sites", "field": "domain", "type": "string", "meta": null, "schema": { "is_nullable": true } },
|
||||
{ "collection": "sites", "field": "provider_group_id", "type": "string", "meta": null, "schema": { "is_nullable": true } },
|
||||
{ "collection": "sites", "field": "status", "type": "string", "meta": null, "schema": { "is_nullable": false, "default_value": "active" } },
|
||||
{ "collection": "sites", "field": "created_at", "type": "timestamp", "meta": null, "schema": { "is_nullable": false, "default_value": "now()" } },
|
||||
{ "collection": "sites", "field": "updated_at", "type": "timestamp", "meta": null, "schema": { "is_nullable": false, "default_value": "now()" } },
|
||||
|
||||
{ "collection": "systems", "field": "id", "type": "uuid", "meta": null, "schema": { "is_nullable": false, "is_primary_key": true, "default_value": "gen_random_uuid()" } },
|
||||
{ "collection": "systems", "field": "system_key", "type": "string", "meta": null, "schema": { "is_nullable": false, "is_unique": true } },
|
||||
{ "collection": "systems", "field": "name", "type": "string", "meta": null, "schema": { "is_nullable": false } },
|
||||
{ "collection": "systems", "field": "status", "type": "string", "meta": null, "schema": { "is_nullable": false, "default_value": "active" } },
|
||||
{ "collection": "systems", "field": "created_at", "type": "timestamp", "meta": null, "schema": { "is_nullable": false, "default_value": "now()" } },
|
||||
{ "collection": "systems", "field": "updated_at", "type": "timestamp", "meta": null, "schema": { "is_nullable": false, "default_value": "now()" } },
|
||||
|
||||
{ "collection": "roles", "field": "id", "type": "uuid", "meta": null, "schema": { "is_nullable": false, "is_primary_key": true, "default_value": "gen_random_uuid()" } },
|
||||
{ "collection": "roles", "field": "role_key", "type": "string", "meta": null, "schema": { "is_nullable": false, "is_unique": true } },
|
||||
{ "collection": "roles", "field": "role_code", "type": "string", "meta": null, "schema": { "is_nullable": false } },
|
||||
{ "collection": "roles", "field": "system_id", "type": "uuid", "meta": null, "schema": { "is_nullable": false, "foreign_key_table": "systems", "foreign_key_column": "id" } },
|
||||
{ "collection": "roles", "field": "name", "type": "string", "meta": null, "schema": { "is_nullable": false } },
|
||||
{ "collection": "roles", "field": "description", "type": "text", "meta": null, "schema": { "is_nullable": true } },
|
||||
{ "collection": "roles", "field": "status", "type": "string", "meta": null, "schema": { "is_nullable": false, "default_value": "active" } },
|
||||
{ "collection": "roles", "field": "created_at", "type": "timestamp", "meta": null, "schema": { "is_nullable": false, "default_value": "now()" } },
|
||||
{ "collection": "roles", "field": "updated_at", "type": "timestamp", "meta": null, "schema": { "is_nullable": false, "default_value": "now()" } },
|
||||
|
||||
{ "collection": "site_roles", "field": "id", "type": "uuid", "meta": null, "schema": { "is_nullable": false, "is_primary_key": true, "default_value": "gen_random_uuid()" } },
|
||||
{ "collection": "site_roles", "field": "site_id", "type": "uuid", "meta": null, "schema": { "is_nullable": false, "foreign_key_table": "sites", "foreign_key_column": "id" } },
|
||||
{ "collection": "site_roles", "field": "role_id", "type": "uuid", "meta": null, "schema": { "is_nullable": false, "foreign_key_table": "roles", "foreign_key_column": "id" } },
|
||||
{ "collection": "site_roles", "field": "created_at", "type": "timestamp", "meta": null, "schema": { "is_nullable": false, "default_value": "now()" } },
|
||||
|
||||
{ "collection": "user_sites", "field": "id", "type": "uuid", "meta": null, "schema": { "is_nullable": false, "is_primary_key": true, "default_value": "gen_random_uuid()" } },
|
||||
{ "collection": "user_sites", "field": "user_id", "type": "uuid", "meta": null, "schema": { "is_nullable": false, "foreign_key_table": "users", "foreign_key_column": "id" } },
|
||||
{ "collection": "user_sites", "field": "site_id", "type": "uuid", "meta": null, "schema": { "is_nullable": false, "foreign_key_table": "sites", "foreign_key_column": "id" } },
|
||||
{ "collection": "user_sites", "field": "created_at", "type": "timestamp", "meta": null, "schema": { "is_nullable": false, "default_value": "now()" } },
|
||||
{ "collection": "user_sites", "field": "updated_at", "type": "timestamp", "meta": null, "schema": { "is_nullable": false, "default_value": "now()" } },
|
||||
|
||||
{ "collection": "auth_sync_state", "field": "id", "type": "uuid", "meta": null, "schema": { "is_nullable": false, "is_primary_key": true, "default_value": "gen_random_uuid()" } },
|
||||
{ "collection": "auth_sync_state", "field": "entity_type", "type": "string", "meta": null, "schema": { "is_nullable": false } },
|
||||
{ "collection": "auth_sync_state", "field": "entity_id", "type": "uuid", "meta": null, "schema": { "is_nullable": false } },
|
||||
{ "collection": "auth_sync_state", "field": "last_synced_at", "type": "timestamp", "meta": null, "schema": { "is_nullable": true } },
|
||||
{ "collection": "auth_sync_state", "field": "source_version", "type": "string", "meta": null, "schema": { "is_nullable": true } },
|
||||
{ "collection": "auth_sync_state", "field": "last_error", "type": "text", "meta": null, "schema": { "is_nullable": true } },
|
||||
{ "collection": "auth_sync_state", "field": "updated_at", "type": "timestamp", "meta": null, "schema": { "is_nullable": false, "default_value": "now()" } },
|
||||
|
||||
{ "collection": "api_clients", "field": "id", "type": "uuid", "meta": null, "schema": { "is_nullable": false, "is_primary_key": true, "default_value": "gen_random_uuid()" } },
|
||||
{ "collection": "api_clients", "field": "client_key", "type": "string", "meta": null, "schema": { "is_nullable": false, "is_unique": true } },
|
||||
{ "collection": "api_clients", "field": "name", "type": "string", "meta": null, "schema": { "is_nullable": false } },
|
||||
{ "collection": "api_clients", "field": "status", "type": "string", "meta": null, "schema": { "is_nullable": false, "default_value": "active" } },
|
||||
{ "collection": "api_clients", "field": "api_key_hash", "type": "text", "meta": null, "schema": { "is_nullable": false } },
|
||||
{ "collection": "api_clients", "field": "allowed_origins", "type": "json", "meta": null, "schema": { "is_nullable": false } },
|
||||
{ "collection": "api_clients", "field": "allowed_ips", "type": "json", "meta": null, "schema": { "is_nullable": false } },
|
||||
{ "collection": "api_clients", "field": "allowed_paths", "type": "json", "meta": null, "schema": { "is_nullable": false } },
|
||||
{ "collection": "api_clients", "field": "rate_limit_per_min", "type": "integer", "meta": null, "schema": { "is_nullable": true } },
|
||||
{ "collection": "api_clients", "field": "expires_at", "type": "timestamp", "meta": null, "schema": { "is_nullable": true } },
|
||||
{ "collection": "api_clients", "field": "last_used_at", "type": "timestamp", "meta": null, "schema": { "is_nullable": true } },
|
||||
{ "collection": "api_clients", "field": "created_at", "type": "timestamp", "meta": null, "schema": { "is_nullable": false, "default_value": "now()" } },
|
||||
{ "collection": "api_clients", "field": "updated_at", "type": "timestamp", "meta": null, "schema": { "is_nullable": false, "default_value": "now()" } }
|
||||
],
|
||||
"relations": [
|
||||
{
|
||||
"collection": "sites",
|
||||
"field": "company_id",
|
||||
"related_collection": "companies",
|
||||
"schema": {
|
||||
"table": "sites",
|
||||
"column": "company_id",
|
||||
"foreign_key_table": "companies",
|
||||
"foreign_key_column": "id",
|
||||
"on_update": "NO ACTION",
|
||||
"on_delete": "CASCADE"
|
||||
},
|
||||
"meta": null
|
||||
},
|
||||
{
|
||||
"collection": "roles",
|
||||
"field": "system_id",
|
||||
"related_collection": "systems",
|
||||
"schema": {
|
||||
"table": "roles",
|
||||
"column": "system_id",
|
||||
"foreign_key_table": "systems",
|
||||
"foreign_key_column": "id",
|
||||
"on_update": "NO ACTION",
|
||||
"on_delete": "CASCADE"
|
||||
},
|
||||
"meta": null
|
||||
},
|
||||
{
|
||||
"collection": "site_roles",
|
||||
"field": "site_id",
|
||||
"related_collection": "sites",
|
||||
"schema": {
|
||||
"table": "site_roles",
|
||||
"column": "site_id",
|
||||
"foreign_key_table": "sites",
|
||||
"foreign_key_column": "id",
|
||||
"on_update": "NO ACTION",
|
||||
"on_delete": "CASCADE"
|
||||
},
|
||||
"meta": null
|
||||
},
|
||||
{
|
||||
"collection": "site_roles",
|
||||
"field": "role_id",
|
||||
"related_collection": "roles",
|
||||
"schema": {
|
||||
"table": "site_roles",
|
||||
"column": "role_id",
|
||||
"foreign_key_table": "roles",
|
||||
"foreign_key_column": "id",
|
||||
"on_update": "NO ACTION",
|
||||
"on_delete": "CASCADE"
|
||||
},
|
||||
"meta": null
|
||||
},
|
||||
{
|
||||
"collection": "user_sites",
|
||||
"field": "user_id",
|
||||
"related_collection": "users",
|
||||
"schema": {
|
||||
"table": "user_sites",
|
||||
"column": "user_id",
|
||||
"foreign_key_table": "users",
|
||||
"foreign_key_column": "id",
|
||||
"on_update": "NO ACTION",
|
||||
"on_delete": "CASCADE"
|
||||
},
|
||||
"meta": null
|
||||
},
|
||||
{
|
||||
"collection": "user_sites",
|
||||
"field": "site_id",
|
||||
"related_collection": "sites",
|
||||
"schema": {
|
||||
"table": "user_sites",
|
||||
"column": "site_id",
|
||||
"foreign_key_table": "sites",
|
||||
"foreign_key_column": "id",
|
||||
"on_update": "NO ACTION",
|
||||
"on_delete": "CASCADE"
|
||||
},
|
||||
"meta": null
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -35,6 +35,17 @@
|
||||
- 後端子模組:[backend](../backend)(submodule: `../member-backend`)
|
||||
- 前端子模組:[frontend](../frontend)(submodule: `../member-frontend`)
|
||||
|
||||
## 開發工作目錄(防呆)
|
||||
- 只在 `member-platform` 內開發與提交。
|
||||
- 後端只改 `member-platform/backend`。
|
||||
- 前端只改 `member-platform/frontend`。
|
||||
- 根目錄外的 `member-backend` / `member-frontend` 若有另一份 clone,視為非主要工作副本,避免混用。
|
||||
|
||||
## 提交順序(固定)
|
||||
1. 先在 `member-platform/backend` 或 `member-platform/frontend` 各自 commit / push。
|
||||
2. 再回 `member-platform` 根目錄,提交子模組版本指標變更(submodule pointer)並 push。
|
||||
3. 部署端只需要更新 `member-platform`,再執行 `git submodule update --init --recursive`。
|
||||
|
||||
## 文件邊界
|
||||
- 本輪只保留可開發、可交辦、可驗收文件。
|
||||
- 最終規格白皮書延後到專案完成後再產出。
|
||||
|
||||
2
frontend
2
frontend
Submodule frontend updated: 563199083d...ed63eaffc6
44
scripts/push-backend.sh
Executable file
44
scripts/push-backend.sh
Executable file
@@ -0,0 +1,44 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
if [ $# -lt 1 ]; then
|
||||
echo "用法: scripts/push-backend.sh \"後端 commit 訊息\" [根目錄 commit 訊息]"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
SUB_MSG="$1"
|
||||
ROOT_MSG="${2:-chore: bump backend submodule}"
|
||||
|
||||
ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)"
|
||||
SUB_DIR="$ROOT_DIR/backend"
|
||||
|
||||
current_branch="$(git -C "$SUB_DIR" branch --show-current)"
|
||||
if [ -z "$current_branch" ]; then
|
||||
echo "backend 目前是 detached HEAD,請先執行: cd backend && git checkout master"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [ "$current_branch" != "master" ]; then
|
||||
echo "backend 目前分支是 $current_branch,建議先切到 master 再執行。"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [ -n "$(git -C "$SUB_DIR" status --porcelain)" ]; then
|
||||
git -C "$SUB_DIR" add -A
|
||||
git -C "$SUB_DIR" commit -m "$SUB_MSG"
|
||||
else
|
||||
echo "backend 無本地變更,略過 backend commit。"
|
||||
fi
|
||||
|
||||
git -C "$SUB_DIR" push origin master
|
||||
|
||||
git -C "$ROOT_DIR" add backend
|
||||
if ! git -C "$ROOT_DIR" diff --cached --quiet; then
|
||||
git -C "$ROOT_DIR" commit -m "$ROOT_MSG"
|
||||
else
|
||||
echo "根目錄無 submodule pointer 變更,略過根目錄 commit。"
|
||||
fi
|
||||
|
||||
git -C "$ROOT_DIR" push origin master
|
||||
|
||||
echo "完成: backend 與根目錄已 push。"
|
||||
44
scripts/push-frontend.sh
Executable file
44
scripts/push-frontend.sh
Executable file
@@ -0,0 +1,44 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
if [ $# -lt 1 ]; then
|
||||
echo "用法: scripts/push-frontend.sh \"前端 commit 訊息\" [根目錄 commit 訊息]"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
SUB_MSG="$1"
|
||||
ROOT_MSG="${2:-chore: bump frontend submodule}"
|
||||
|
||||
ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)"
|
||||
SUB_DIR="$ROOT_DIR/frontend"
|
||||
|
||||
current_branch="$(git -C "$SUB_DIR" branch --show-current)"
|
||||
if [ -z "$current_branch" ]; then
|
||||
echo "frontend 目前是 detached HEAD,請先執行: cd frontend && git checkout master"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [ "$current_branch" != "master" ]; then
|
||||
echo "frontend 目前分支是 $current_branch,建議先切到 master 再執行。"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [ -n "$(git -C "$SUB_DIR" status --porcelain)" ]; then
|
||||
git -C "$SUB_DIR" add -A
|
||||
git -C "$SUB_DIR" commit -m "$SUB_MSG"
|
||||
else
|
||||
echo "frontend 無本地變更,略過 frontend commit。"
|
||||
fi
|
||||
|
||||
git -C "$SUB_DIR" push origin master
|
||||
|
||||
git -C "$ROOT_DIR" add frontend
|
||||
if ! git -C "$ROOT_DIR" diff --cached --quiet; then
|
||||
git -C "$ROOT_DIR" commit -m "$ROOT_MSG"
|
||||
else
|
||||
echo "根目錄無 submodule pointer 變更,略過根目錄 commit。"
|
||||
fi
|
||||
|
||||
git -C "$ROOT_DIR" push origin master
|
||||
|
||||
echo "完成: frontend 與根目錄已 push。"
|
||||
Reference in New Issue
Block a user