aps-agent/docs/architecture/database.md

87 lines
5.3 KiB
Markdown
Raw Permalink 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.

# 数据库层(业务数据 + 共享审批存储)
> M-A 落地:主数据不再写死在代码里,康尼只是数据库里的一个「项目」。
> 代码:`server/db/`(models / database / sync)、`server/state/packs.py`、`server/importers/`。
## 定位
- **SQLite(`server/data/master.db`,`APS_DB_PATH` 可覆盖)**:主数据的持久源——项目、
主数据宽表(资源/物料/BOM/工序/路线/订单/柔性表)、行业工艺路线模板。
- **world.json**:排产运行态(版本/工单/冲突/检查点/审计)仍留在 `WorldStore`;
引擎零改动,读的还是 world dict。
- **knowledge.json + embeddings.json**:RAG 知识库(见 [rag.md](./rag.md))。
- **共享审批库(显式启用)**:`APS_APPROVAL_BACKEND=database` 时复用
`APS_DATABASE_URL` 指向的 MySQL;审批请求、一次性凭据和决策事件由数据库事务协调,
不再依赖单机文件锁。SQLite 只允许测试显式开启,不能作为跨主机共享部署。
## 表结构(`server/db/models.py`,SQLAlchemy 2.0)
| 表 | 作用 |
| --- | --- |
| `projects` | 项目/租户(demo、kangni…),`is_active` 标记当前投影项目 |
| `master_records` | 通用宽表:`(project_id, table_key, seq)` + `code/name` 索引列 + `payload` JSON 全量;`table_key` 覆盖 `factories/materials/flexOrders/routingSteps/...`(清单见 `sync.MASTER_LIST_KEYS`) |
| `project_settings` | dict 型主数据(`factoryCalendar/changeoverMatrix/...`,见 `MASTER_DICT_KEYS`) |
| `routing_templates` / `routing_template_steps` | 行业工艺路线模板库(M-E,含推荐工时区间/设备类型/知识资产关联) |
| `aps_approval_requests` | 审批请求当前状态;`revision` 条件更新保证批准/驳回只有一个终态 |
| `aps_approval_grants` | P3 一次性执行凭据;只持久化 token SHA-256,按状态与 `revision` 全局一次消费 |
| `aps_approval_events` | append-only 审批事件投影;应用路径只追加、不更新历史事件 |
宽表设计的取舍:world 的主数据形态仍在快速演进,`payload` 存整行 JSON、
`code/name/seq` 提列做索引,避免每加一个字段就迁 schema;引擎兼容性优先。
## 共享审批存储
- 默认 `APS_APPROVAL_BACKEND=file`,继续使用 `APS_APPROVAL_PATH` 或
`APS_HOME/data/approvals.json`,适合桌面和单机部署。
- `APS_APPROVAL_BACKEND=database` 必须显式设置 MySQL `APS_DATABASE_URL`;同时设置
`APS_APPROVAL_PATH`、缺少 URL、使用非 MySQL 方言或连接失败均直接启动失败,不回退文件。
- `APS_APPROVAL_DATABASE_ALLOW_SQLITE=1` 仅供测试本地合同和 CAS 竞态;SQLite 不提供本项目
所要求的跨主机共享部署保证。
- 最终批准以一个事务完成请求 CAS、grant 插入和 `APPROVED` 事件追加;grant 消费用
`ACTIVE + revision + expiresAt` 条件更新保证只有一个连接成功。
- 当前 `get_engine()` 会执行 `Base.metadata.create_all()` 供本地兼容。共享环境仍必须先执行
`alembic upgrade head`(revision `20260731_03`)再部署应用;`create_all()` 不是生产迁移机制,
不记录 Alembic revision,也不能替代受控迁移。
从文件后端切换时必须先停止所有 Gateway 写入,等待或迁移活动审批/执行凭据,再一次性切换
全部节点;禁止 file/database 混跑和双写。本切片未提供自动 JSON 导入器。
## 投影与同步(`server/db/sync.py` + `state/store.py`)
```
启动: WorldStore.__init__ → _sync_with_db()
DB 有主数据 → db_to_world() 覆盖 world 主数据键(DB 为准)
DB 空 world 有 → world_to_db() 一次性迁移(保留现场已导入数据)
写入: store.save() → _sync_master_to_db()(master_fingerprint 脏检查,变了才写)
```
- `set_active_project(code)` 切换活跃项目;`db_to_world` 只投影该项目数据。
- `APS_DB_DISABLED=1` 完全绕过 DB(测试/降级);DB 异常不阻断 world 主流程。
## 数据包(`server/state/packs.py`)
- `export_pack(world, name)` → `server/data/packs/<name>.json`(记录 `baseDate`)。
- `load_pack(name)` 重放时把所有日期按 `baseDate→今天` 平移(演示数据永不过期)。
- `seed_world()`:`APS_SEED_PACK` 指定则重放数据包,否则 `build_demo_world()` 程序化种子。
- `scripts/generate_demo_pack.py` 重新生成 `demo.json`。
## 导入器配置化(`server/importers/`)
- `excel_importer.import_site_excel(profile_name, path)`:按 profile 解析现场 Excel → 入库 → 投影。
- `profiles/kangni.json`:工厂名、区域、工时推断规则(部装 45/装配 30…)、换型/接刀默认、
班组、日历——原 `kangni_intake.py` 的硬编码全部搬进配置;解析函数吃 profile 参数。
- 新客户 = 新增一个 profile JSON,不改代码。
## API
| 端点 | 用途 |
| --- | --- |
| `GET /api/projects` | 项目清单 + 活跃项目(P0) |
| `GET /api/packs` | 可重放数据包清单(P0) |
## 测试
`tests/golden/test_db_projection.py`(入库→投影→PoolEngine 可排、多项目隔离、数据包日期平移)、
`tests/golden/test_approval_database_store.py`(SQLite 本地数据库合同、CAS 竞态、token 摘要和配置失败关闭;不代表 MySQL 实机验收)、
`test_excel_importer.py`(profile 等价性 + 覆写生效 + stdTimeSource 标记)。