Files
alist-serve-resource/docs/DATABASE_SCHEMA.md
T
hoelee 4693c9d684 feat(W4): daily full reconciliation — user/role drift repair + ReconcileLogs audit
- w4-body.js: ONE-DIRECTIONAL sync (NocoDB status = SOT, never reverse);
  drift matrix: missing user/role (provision w/ username lookup to avoid
  duplicates), dangling refs (adopt/recreate), disabled<->status, canonical
  base_path '/' + permission 16384, scopes vs calculateEffectiveGrants,
  username drift detect+log_only; verify-after-repair on every change;
  password never pushed on repair (方案 C); dry-run via ?dryRun=true
- gen_w4.js: daily 03:00 cron + webhook alist-w4-reconcile; API-import-safe
  (UUID node ids, no pinData, webhookId)
- ReconcileLogs table mktsb7le6fqr2u1 (add_reconcilelogs_table.py, idempotent,
  v2 API); FK column nc_jitu___Customers_id for bt-link writes (v2 ignores
  link column itself)
- test_w4_reconcile.js: 34 assertions / 12 mock worlds, ALL PASSED
- live-verified against production: dry-run (3 drifts + 1 error found),
  real repair (3 repaired, verify all passed, 4 log rows), idempotent rerun
- NocoDB 2026.08.x pitfalls documented (v1 col endpoints gone, row delete
  via body [{Id}]); n8n API import/activation pitfalls (activeVersionId)
- docs: W4-DESIGN.md new; DATABASE_SCHEMA 8 tables; N8N_WORKFLOWS, SETUP,
  PROJECT_STATE updated
2026-08-23 09:34:11 +08:00

182 lines
7.9 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# NocoDB 数据结构(Phase 2b — 重建,8 表最终模型)
> Source of Truth = NocoDB。AList 只做 enforcement,不存业务授权数据。
> 本 schema 按《Hermes 调整指导》最终业务模型重建(2026-08-22)。
> 通过 NocoDB API 自动建表,无需手动点 UI。ReconcileLogs 于 2026-08-23 W4 新增。
- **Base**: `AlistAccess`(id `p3lzkresswyowqg`),独立新 base,与旧 `accessDB` 隔离。
- **MySQL source**: mysql-server 上的 `alistaccess` schema(由 NocoDB 自动管理)。
- **API token**: workspace token(`nc_api_tokens` 表内 `n8n` token),`xc-token` header。
## 表 model_id(n8n v2 API 硬编码用)
| 表 | model_id |
|---|---|
| Customers | `maqb9zgvza2te3k` |
| Products | `molyb3pxv81l75g` |
| Resources | `m6cm7tkxzd8m18c` |
| CustomerProducts | `mvyqbv8ao7u6bct` |
| AccessGrants | `mxdx3yhfcyej3ao` |
| AccessLogs | `ml12zcyzlehgi21` |
| Purchases | `mwp12z9lcohhsuk` |
| ReconcileLogs | `mktsb7le6fqr2u1`(FK 列 `nc_jitu___Customers_id` = `coa6ynwcdlune6p`) |
> ⚠ ID 是本次重建生成的。若再次删库重建,ID 会变,需同步更新 n8n 节点 + 本文档。
## 关系(LinkToAnotherRecord)
| 子表 | 字段 | 方向 | 指向 | 说明 |
|---|---|---|---|---|
| CustomerProducts | `customer` | bt | Customers | |
| CustomerProducts | `product` | bt | Products | |
| Products | `Resources` | **mm** | Resources | M2M link,列 id `c4ko4x8kwi16swg` |
| Resources | `Products` | mm (reverse) | Products | 列 id `cfxi44n9ixkimqc` |
| AccessGrants | `customer` | bt | Customers | |
| AccessGrants | `resource` | bt | Resources | |
| AccessLogs | `customer` | bt | Customers | |
| Purchases | `customer` | bt | Customers | |
| Purchases | `product` | bt | Products | |
| ReconcileLogs | `customer` | bt | Customers | ⚠ 写入时用 FK 列 `nc_jitu___Customers_id`(v2 忽略 link 列本身) |
> M2M 读写方式(已实测):`GET/POST /api/v2/tables/{products}/links/c4ko4x8kwi16swg/records/{productId}`,
> POST body `{"Id": <resourceId>}`。反向用 Resources 的 `cfxi44n9ixkimqc`。
## 表结构
### 1. Customers
| 字段 | 类型 | 说明 |
|---|---|---|
| Id | number (PK) | |
| username | SingleLineText | AList 登录名(唯一) |
| password | SingleLineText | **plaintext**(规格 §2 已确认,勿 hash) |
| display_name | SingleLineText | |
| email | Email | |
| notes | LongText | |
| alist_user_id | Number | AList 用户 ID(n8n 回填) |
| alist_role_id | Number | AList role ID(n8n 回填) |
| status | SingleSelect | pending/active/suspended/expired/deleted(account-level 整户开关,见下) |
| sync_status | SingleSelect | pending/syncing/synced/error/unknown |
| sync_required | Checkbox | 系统维护,管理员勿手改 |
| last_sync | DateTime | |
| last_sync_error | LongText | |
**Customers.status 语义(account-level 整户开关,不替代 CustomerProduct 到期):**
| 值 | 含义 | AList user | scopes |
|---|---|---|---|
| `pending` | 已建但未正式开通 | 若无则**不建**;有则 disabled | empty |
| `active` | 正常使用 | enabled | 按当前有效 CP 重算 |
| `suspended` | 管理员暂时封禁 | disabled | empty |
| `expired` | 整户生命周期结束 | disabled | empty |
| `deleted` | 软删除(保留账号+历史)| disabled | empty |
- 任意 `X → active` 恢复时,重算当前有效 grants(忽略已过期产品),不恢复旧 scopes。
- status 变化**不自动改** CustomerProducts(不批量改 status/expires_at、不删除)。
### 2. Products
| 字段 | 类型 | 说明 |
|---|---|---|
| Id | number (PK) | |
| name | SingleLineText | 如 "Python Basic" |
| description | LongText | |
| default_duration_days | Number | 默认有效期天数 |
| status | SingleSelect | active/inactive |
| Resources | link (mm) | 该产品包含的 Resources |
### 3. Resources
| 字段 | 类型 | 说明 |
|---|---|---|
| Id | number (PK) | |
| name | SingleLineText | 如 "Python 01" |
| alist_path | SingleLineText | 真实 AList path,如 "/Class/Python/01" |
| description | LongText | |
| status | SingleSelect | active/inactive |
| Products | link (mm, reverse) | |
### 4. CustomerProducts(核心新表)
| 字段 | 类型 | 说明 |
|---|---|---|
| Id | number (PK) | |
| customer | link bt → Customers | |
| product | link bt → Products | |
| start_at | Date | |
| expires_at | Date | 实际到期(可 override default_duration) |
| status | SingleSelect | scheduled/active/expired/revoked/suspended |
| sync_status | SingleSelect | pending/syncing/synced/error/unknown |
| sync_required | Checkbox | |
| last_sync | DateTime | |
| last_sync_error | LongText | |
### 5. AccessGrants(最终实际权限,n8n 计算维护)
| 字段 | 类型 | 说明 |
|---|---|---|
| Id | number (PK) | |
| customer | link bt → Customers | |
| resource | link bt → Resources | |
| permission | SingleSelect | read/readwrite |
| start_at | Date | |
| expires_at | Date | effective expiry(多来源取 max) |
| status | SingleSelect | pending/active/revoked/expired |
| source | SingleSelect | product/manual |
| sync_status | SingleSelect | pending/syncing/synced/error/unknown |
| sync_required | Checkbox | |
| last_sync | DateTime | |
| last_sync_error | LongText | |
| revoked_at | DateTime | |
| revoke_reason | LongText | |
### 6. AccessLogs(审计)
| 字段 | 类型 |
|---|---|
| Id | number (PK) |
| customer | link bt → Customers |
| username | SingleLineText |
| resource_path | SingleLineText |
| file_path | SingleLineText |
| action | SingleSelect (login/view/download/preview/logout) |
| ip_address | SingleLineText |
| user_agent | LongText |
| result | SingleSelect (success/denied) |
| bytes | Number |
| timestamp | DateTime |
### 7. ReconcileLogs(W4 对账审计,n8n W4 写入)
| 字段 | 类型 | 说明 |
|---|---|---|
| Id | number (PK) | |
| run_id | SingleLineText | 本次对账 run(`recon-<ISO>`) |
| customer | link bt → Customers | FK 列 `nc_jitu___Customers_id`(v2 写链接必须写 FK 列) |
| drift_type | SingleSelect | user_missing / role_missing / dangling_user_ref / dangling_role_ref / user_disabled_drift / scopes_drift / base_path_drift / permission_drift / role_binding_drift / username_drift / verify_failed / reconcile_error |
| details | LongText | 前后值 JSON |
| action | SingleLineText | 做了什么(provisioned / adopted / fixed / rewritten / disabled / log_only / verify …) |
| result | SingleSelect | repaired / log_only / failed / skipped |
| timestamp | DateTime | |
> 只记 drift / repair / error 行,健康客户不产生行。W4 每日全量或 webhook 触发时写入。
### 8. Purchases(纯记录,不参与权限)
| 字段 | 类型 |
|---|---|
| Id | number (PK) |
| customer | link bt → Customers |
| product | link bt → Products |
| purchase_date | Date |
| amount | Decimal |
| payment_method | SingleLineText |
| order_id | SingleLineText |
| notes | LongText |
## 建表 API recipe(实测 2026-08-22,复用见 nocodb-selfhosted skill)
- 建 base:`POST /api/v2/meta/bases/ {"title"}`
- 建表(内联非 select 列):`POST /api/v2/meta/bases/{baseId}/tables {"title","table_name","type":"table","columns":[{"title","uidt"}]}`
- 可用 uidt:`SingleLineText`,`LongText`,`Number`,`Decimal`,`Date`,`DateTime`,`Email`,`Checkbox`
- SingleSelect 两段式:
1. 建空列 `POST /api/v1/db/meta/tables/{tid}/columns {"title","uidt":"SingleSelect"}`
2. 设选项 **必须同时传 `dtxp`**(逗号枚举串):
`PATCH /api/v1/db/meta/columns/{colid} {"colOptions":{"options":[{"title","order","color":null}]},"dtxp":"'opt1','opt2'"}`
⚠ 只传 `colOptions` 不传 `dtxp` 会静默失败(options 空)。
- Link:`POST /api/v1/db/meta/tables/{childTid}/columns {"title","uidt":"LinkToAnotherRecord","parentId","childId","type":"bt"}`
- M2M:`type":"mm"`,然后走 `/api/v2/tables/{t}/links/{colId}/records/{rowId}` 读写。
- 删记录:`DELETE /api/v2/tables/{tid}/records` body `[{"Id":N},...]`(不是 `/records/{id}`)。