aps-agent/docs/architecture/database.md

87 lines
5.3 KiB
Markdown
Raw Permalink Normal View History

# 数据库层(业务数据 + 共享审批存储)
> 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 标记)。