Compare commits

..

29 Commits

Author SHA1 Message Date
Chris
4fd81ffbf2 chore(directus): rename *_key to key and skip system key autogen 2026-04-04 17:24:54 +08:00
Chris
cf39ea2b0c chore(directus): switch ids to uuid and add key fields only 2026-04-04 16:59:38 +08:00
Chris
560f40ae8a chore(directus): convert ids to uuid and ensure *_key fields 2026-04-04 16:55:09 +08:00
Chris
838c0afc0b chore(directus): add users.user_key to base directus schema 2026-04-04 16:51:23 +08:00
Chris
0666b8683e chore(directus): add import schemas and key auto-generation sql 2026-04-04 16:48:04 +08:00
Chris
428b6292ea chore: bump frontend submodule 2026-04-03 16:02:24 +08:00
Chris
cd7feec38a docs: update env workflow and role_code contracts 2026-04-03 15:50:19 +08:00
Chris
01a4580faf chore(scripts): add helper scripts for submodule push flow 2026-04-03 15:04:32 +08:00
Chris
649af715e2 docs: add workspace guardrails and submodule workflow 2026-04-03 14:51:14 +08:00
Chris
3571cdf2ee docs(env): standardize development and production env workflow 2026-04-03 14:43:40 +08:00
Chris
099ed03be7 chore: bump backend submodule 2026-04-03 06:03:17 +08:00
Chris
f62ed97e2b chore: bump backend submodule for env example 2026-04-03 05:57:13 +08:00
Chris
760902f53c chore: bump frontend submodule to latest 2026-04-03 05:40:12 +08:00
Chris
998771bc11 chore: bump frontend submodule for oidc url guard 2026-04-03 05:27:25 +08:00
Chris
576ba9b2fe chore: bump frontend submodule for dev env 2026-04-03 05:10:26 +08:00
Chris
b7b312e69a fix: remove invalid condition from compose example 2026-04-03 05:04:00 +08:00
Chris
865be73d06 update 2026-04-03 05:02:41 +08:00
Chris
ed4b22a564 chore: bump backend submodule after rebase 2026-04-03 04:59:40 +08:00
Chris
2da43cf027 update 2026-04-03 04:57:16 +08:00
Chris
200c86c924 chore: bump backend submodule for env selection 2026-04-03 04:51:12 +08:00
Chris
e8058d1240 Use single backend env template in deploy docs 2026-04-03 04:33:47 +08:00
Chris
6dabc2eab6 Document single backend .env workflow 2026-04-03 04:31:59 +08:00
Chris
8609d61f82 Use example compose and ignore local deploy env files 2026-04-03 04:26:05 +08:00
Chris
f01a228026 Add docker-compose template for VPS deployment 2026-04-03 04:18:46 +08:00
Chris
a6e5fbbb24 Bump backend submodule to latest ignore rules 2026-04-03 04:01:15 +08:00
Chris
21dc3ea56f Update backend submodule after gitignore hardening 2026-04-03 03:58:07 +08:00
Chris
fdf17ecf85 Update docs and submodule after backend cleanup 2026-04-03 03:55:04 +08:00
Chris
a45aa5a6c7 Add VPS deployment runbook 2026-04-03 03:40:10 +08:00
Chris
c394e9153e Rename integration workspace to member-platform 2026-04-03 03:32:22 +08:00
17 changed files with 11538 additions and 26 deletions

4
.gitignore vendored
View File

@@ -22,3 +22,7 @@ dist/
*.log
npm-debug.log*
yarn-error.log*
# Local deployment files
docker-compose.yml
docker-compose.override.yml

Submodule backend updated: ade60bdbaa...405000ded5

View File

@@ -0,0 +1,29 @@
services:
backend:
build:
context: ./backend
dockerfile: Dockerfile
container_name: memberapi_ose_tw
restart: unless-stopped
env_file:
- ./backend/.env
ports:
- "127.0.0.1:8000:8000"
healthcheck:
test: ["CMD-SHELL", "wget -qO- http://127.0.0.1:8000/healthz >/dev/null || exit 1"]
interval: 30s
timeout: 5s
retries: 5
start_period: 20s
networks:
- postgres
- ose-cache
- nginx
networks:
postgres:
external: true
ose-cache:
external: true
nginx:
external: true

View File

@@ -1,4 +1,4 @@
# member.ose.tw 架構總覽Keycloak 版)
# member-platform 架構總覽Keycloak 版)
## 核心模型
- 業務層:`companies -> sites`

View File

@@ -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()`

View File

@@ -22,8 +22,6 @@
6. `POST /internal/users/upsert-by-sub`
7. `GET /internal/users/{user_sub}/roles`
8. `POST /internal/provider/users/ensure`
9. `POST /internal/idp/users/ensure`(舊路徑相容,不建議新串接使用)
10. `POST /internal/keycloak/users/ensure`(舊路徑相容,不建議新串接使用)
## 角色聚合回應(`GET /internal/users/{user_sub}/roles`
```json
@@ -38,6 +36,7 @@
"system_key": "SY20260402X0001",
"system_name": "Marketing",
"role_key": "RL20260402X0002",
"role_code": "mkt:marketing_card:edit",
"role_name": "campaign_edit"
}
]
@@ -47,3 +46,4 @@
## 注意事項
- 不提供 user direct role 寫入 API。
- User 最終角色由 `user_sites` + `site_roles` 推導。
- `role_key` 是唯一識別鍵;業務語意解析請使用 `role_code`

View File

@@ -7,28 +7,16 @@ psql "$DATABASE_URL" -f scripts/init_schema.sql
```
- DB schema 檔案:[backend/scripts/init_schema.sql](../backend/scripts/init_schema.sql)
如果你是 macOS 本機沒裝 `psql`,改用:
## 2) 啟動後端
本地開發使用 `.env.development`
```bash
cd backend
./.venv/bin/python - <<'PY'
import psycopg
from pathlib import Path
sql = Path('scripts/migrate_provider_columns.sql').read_text()
with psycopg.connect(
host='127.0.0.1',
port=54321,
dbname='member.ose.tw',
user='member_ose',
password='你的DB密碼'
) as conn:
with conn.cursor() as cur:
cur.execute(sql)
print('provider column migration done')
PY
# edit .env.development directly
```
- 欄位改名 migration[backend/scripts/migrate_provider_columns.sql](../backend/scripts/migrate_provider_columns.sql)
## 2) 啟動後端
本機開發固定使用 `backend/.env.development`
再啟動:
```bash
cd backend
./scripts/start_dev.sh
@@ -42,6 +30,8 @@ cd frontend
npm install
npm run dev
```
- 本地開發固定使用 `frontend/.env.development`
- production build 讀取 `frontend/.env.production`
- 專案路徑:[frontend](../frontend)
## 4) 必要環境變數([backend/.env.development](../backend/.env.development)
@@ -71,7 +61,7 @@ npm run dev
1. `GET http://127.0.0.1:8000/healthz` 應為 200。
2. 前端按「前往 Keycloak 登入」應可成功導轉與回跳。
3. `GET /me` 登入後應有資料。
4. 非 admin 群組帳號打 `/admin/*` 應為 403。
4. 非 admin realm role 帳號打 `/admin/*` 應為 403。
5. `POST /admin/sync/from-provider?force=true` 可手動觸發全量補齊同步。
6. 列表 API 不會自動同步 IdP避免高負載需手動按同步按鈕或呼叫同步 API。

109
docs/VPS_DEPLOY_RUNBOOK.md Normal file
View File

@@ -0,0 +1,109 @@
# VPS Deploy Runbook
## 1) 拉整合層 + 子模組
```bash
cd /opt
git clone --recurse-submodules http://127.0.0.1:8888/member/member-platform.git
cd member-platform
git submodule update --init --recursive
```
## 2) 後端部署Docker
```bash
cd /opt/member-platform/backend
cp .env.production .env
```
編輯 `.env`DB、Keycloak、Realm Roles、Cache
首次建表:
```bash
psql "postgresql://<user>:<pass>@<host>:<port>/<db>" -f scripts/init_schema.sql
```
啟動:
```bash
docker build -t memberapi-backend:latest .
docker rm -f memberapi-backend 2>/dev/null || true
docker run -d \
--name memberapi-backend \
--restart unless-stopped \
-p 127.0.0.1:8000:8000 \
--env-file .env \
memberapi-backend:latest
```
檢查:
```bash
curl http://127.0.0.1:8000/healthz
docker logs -f memberapi-backend
```
### 用 docker compose建議
Compose 檔案:[docker-compose.example.yml](../docker-compose.example.yml)
啟動:
```bash
cd /opt/member-platform
cp docker-compose.example.yml docker-compose.yml
docker compose up -d --build
```
檢查:
```bash
docker compose ps
docker compose logs -f backend
```
停止:
```bash
docker compose down
```
## 3) 前端部署Nginx
```bash
cd /opt/member-platform/frontend
```
production build 會自動讀取 `.env.production`,請先確認設定:
```env
VITE_API_BASE_URL=https://memberapi.ose.tw
```
Build
```bash
npm ci
npm run build
```
Nginx root 指向 `frontend/dist`,並加 SPA rewrite
```nginx
location / {
try_files $uri $uri/ /index.html;
}
```
## 4) 更新流程
```bash
cd /opt/member-platform
git pull
git submodule update --init --recursive --remote
```
後端更新:
```bash
cd backend
docker build -t memberapi-backend:latest .
docker rm -f memberapi-backend
docker run -d --name memberapi-backend --restart unless-stopped -p 127.0.0.1:8000:8000 --env-file .env memberapi-backend:latest
```
前端更新:
```bash
cd ../frontend
npm ci
npm run build
```
## 5) 建議網域
- Frontend: `member.ose.tw`
- API: `memberapi.ose.tw`(反代 `127.0.0.1:8000`

View 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;

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View 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
}
]
}

View File

@@ -1,4 +1,4 @@
# member.ose.tw 文件入口(新架構)
# member-platform 文件入口(新架構)
## 閱讀順序
1. [docs/ARCHITECTURE.md](./ARCHITECTURE.md)
@@ -8,6 +8,7 @@
5. [docs/FRONTEND_HANDOFF.md](./FRONTEND_HANDOFF.md)
6. [docs/INTERNAL_API_HANDOFF.md](./INTERNAL_API_HANDOFF.md)
7. [docs/LOCAL_DEV_RUNBOOK.md](./LOCAL_DEV_RUNBOOK.md)
8. [docs/VPS_DEPLOY_RUNBOOK.md](./VPS_DEPLOY_RUNBOOK.md)
## 交辦順序(建議)
1. 先看 [ARCHITECTURE.md](./ARCHITECTURE.md) 鎖定資料模型與權限模型。
@@ -30,10 +31,21 @@
- DB SQL[backend/scripts/init_schema.sql](../backend/scripts/init_schema.sql)
## Repo 結構(已拆分)
- 整合層(本 repo`member.ose.tw`docs / 部署 / 整合)
- 整合層(本 repo`member-platform`docs / 部署 / 整合)
- 後端子模組:[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`
## 文件邊界
- 本輪只保留可開發、可交辦、可驗收文件。
- 最終規格白皮書延後到專案完成後再產出。

44
scripts/push-backend.sh Executable file
View 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
View 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。"