Skip to content

Latest commit

 

History

History
114 lines (67 loc) · 16.6 KB

File metadata and controls

114 lines (67 loc) · 16.6 KB

系统架构

目标形态

目标系统采用 C++17 模块化单体:一个 warehouse_server 进程、一个 MySQL 8 数据库和清晰的进程内模块边界。阶段 1 已建立模块目标、组合根、typed 遗留用例、cpp-httplib/nlohmann-json 适配器、MySQL RAII/迁移基础,src/main.cpp 只进入启动组合根。下述完整业务模块仍须按阶段 2–10 实现,不能因平台骨架已经落地而视为可用。

依赖方向固定为:

bootstrap -> api -> application -> domain

bootstrap -> infrastructure -> application ports

领域层不依赖 HTTP、MySQL、ClamAV、汇率供应商、文件格式或外部总账。基础设施适配器可以依赖应用层定义的端口和领域值类型,但不同适配器不得通过彼此的具体类耦合。

目录与构建目标

  • src/domain/:标识、金额、数量、SKU、仓库树、库存批次、申请状态、审批策略、权限范围、会计分录等业务模型和纯规则,对应 warehouse_domain
  • src/application/:命令、查询、应用服务、端口接口和工作单元编排,对应 warehouse_application,只依赖 warehouse_domain
  • src/api/:基于 cpp-httplibnlohmann/json 的 HTTP 路由、认证上下文、请求 DTO、格式校验、错误映射和响应 DTO,对应 warehouse_api,依赖 warehouse_application;第三方协议类型不得出现在应用或领域头文件中。
  • src/infrastructure/mysql/libmysql RAII 封装、有界连接池、参数化语句、事务工作单元、仓储、投影、迁移器和事务性发件箱,对应 warehouse_mysql;模块外不得出现 MYSQL*MYSQL_STMT*
  • src/infrastructure/security/:Argon2id、OpenSSL 3 RFC 6238 HMAC-SHA1 TOTP、libsodium XChaCha20-Poly1305 密钥加密、会话和恢复码实现;现有 warehouse_password_hash 迁入该边界但保持独立测试。
  • src/infrastructure/attachments/:MySQL 二进制附件、格式识别及 ClamAV 适配器。
  • src/infrastructure/exchange/:可替换汇率提供方、双免密源和本地缓存。
  • src/infrastructure/integrations/:总账及上下级仓库账册导出适配器、通知投递器。
  • src/bootstrap/:配置、依赖组装、后台任务和进程生命周期;src/main.cpp 最终只调用此处的组合根。
  • migrations/:按编号排序的版本化 SQL,不再由运行时代码拼接 CREATE TABLE
  • frontend/:Vue 3、Vite 和 TypeScript 应用,使用 Vue Router、Pinia、Vitest 和 Playwright;构建产物由 HTTP 适配层或受控静态服务部署。

CMake 为上述稳定边界建立库目标,并通过链接方向阻止领域层反向依赖基础设施。单元测试按模块链接最小目标;涉及 MySQL 事务、迁移、并发预留、哈希链和发件箱的行为使用独立集成测试目标。

业务模块

  • identity_access:单一身份账户、显式权限、职责范围、暂停、临时代管、TOTP 和高权限账户治理。
  • catalog:SKU、品类、计量单位、数量精度、新货提案和归档生命周期。
  • warehousing:仓库组织树、货位树、管理员与员工范围及组织完整性。
  • stock_requests:公共申请头、类型明细、审批、复核、撤回、取消、预留和分批执行。
  • inventory:批次、先进先出分配、库存流水、余额投影和重建。
  • transfers:库内移位、跨仓双边审批、在途库存、收货和差异解决。
  • accounting:原币与人民币分录、固定模板、会计表决、库存估值和账册导出事件。
  • documents:附件版本、安全扫描、隔离及业务关联。
  • audit_notifications:追加式哈希链审计、提醒、警告和事务性发件箱。

模块可共享稳定的值类型和事务端口,但不得直接修改其他模块的数据表。跨模块状态变化由应用服务调用明确领域操作,并在同一个应用工作单元内完成。

事务与一致性

HTTP 路由只完成格式解析、身份建立和应用命令调用。应用服务开始工作单元,按固定顺序锁定申请、预留、余额与批次,调用领域规则,再通过端口写入业务事实、审计事件和发件箱。任一环节失败时整体回滚;适配器不得自行提交事务。

审批、预留、库存执行、跨仓调拨和会计入账使用 READ COMMITTED 命令事务。任何“读取后决定并写入”的不变量都使用 SELECT ... FOR UPDATE 或等价锁定写入,并按申请、预留、余额、批次的稳定顺序获取锁;普通一致性读取不能替代锁定读取。唯一约束和受影响行数仍须作为提交前的最终并发校验。

定稿报表、账册导出批次和审计校验使用有时限的 REPEATABLE READ, READ ONLY, WITH CONSISTENT SNAPSHOT 事务。普通分页与详情查询保持短事务,不为用户会话维持数据库快照。SKIP LOCKED 只允许发件箱和通知投递器领取队列工作,不得用于库存可用量、FIFO、审批或会计判断。

库存余额属于可重建投影,库存流水、批次分配和分录属于不可变事实。事务性发件箱与分录或通知事实同事务写入,后台投递只更新独立投递状态。幂等键、唯一最终审批约束和数据库行锁共同处理重复请求与并发竞争。

MySQL 适配层

继续使用现有 libmysql,不引入 ORM。warehouse_mysql 提供 MySQL 库生命周期、线程上下文、ConnectionPoolConnectionLeasePreparedStatementTransaction 和工作单元工厂等 RAII 类型;应用服务只通过端口取得工作单元。连接池容量有上限且与 HTTP 工作线程和数据库容量协调,一个工作单元在完整业务事务期间独占一条连接,未显式提交的事务在析构时回滚。

所有业务输入和值使用 MYSQL_STMT 参数绑定,动态排序、字段和其他标识符只能取自代码允许列表。金额、数量和汇率以定点整数或规范十进制字符串绑定并读取,不经过二进制浮点。大附件通过预处理语句的长数据接口写入。版本化迁移使用仓库内可信固定 SQL 文件,并由独立迁移器执行,不复用面向业务输入的语句接口。

数量列使用 DECIMAL(38,6) 并按 SKU 校验 0 至 6 位精度;单位进价和售价使用 DECIMAL(38,4) 且必须大于零,汇率使用 DECIMAL(38,8),原币与人民币总额使用 DECIMAL(38,2),JPY 业务输入额外限制为整数。任何超精度、溢出、零或负数量价格都在进入领域命令前拒绝。

数据库会话统一配置 UTC、严格 SQL 模式并禁用静默自动重连;工作单元按用途显式选择命令事务或定稿读取事务。连接归还池前必须结束结果集和事务,失效连接被丢弃并重建。只有可识别的死锁或锁等待超时可以在应用命令边界使用相同幂等键进行有界重试;语法、约束、连接及未知错误不得盲目重试。

数据库迁移语义

迁移器使用预检、单条原子 DDL、事务化 DML 回填、切换和后续清理的可重入步骤。每一步保存开始、成功或失败事实并在重复启动时检查实际结构;服务只在所有必需步骤明确成功后开放业务流量。MySQL DDL 的隐式提交意味着多条 DDL 不能被描述为可整体事务回滚,失败时系统拒绝启动并保留真实失败点,恢复依赖修复后的可重入执行或已验证备份。

HTTP 与 JSON 边界

阶段 1 已通过 vcpkg 引入 cpp-httplibnlohmann-json,替换在编译路径中的套接字报文解析、正则 JSON 提取和每连接分离线程。HTTP 适配器使用有界线程池;worker 数和精确 CORS 来源可部署配置,请求头、普通 JSON、总体载荷、单个 multipart 文件及读写/空闲超时目前使用代码内固定安全上限。阶段 1 没有附件业务端点,也未承诺拒绝多附件请求;20 MiB 附件持久化、真实类型识别、隔离、安全扫描及其完整协议测试仍属于阶段 4。

阶段 1 的遗留 API 只接受声明的 JSON 字段类型,拒绝语法错误和类型错误,并继续返回冻结的 {error:<中文文案>};其路由、认证头、状态、JSON 字段、精确 CORS 和静态路径由协议测试覆盖。阶段 2 已注册的 /api/v1 子集使用统一 code/message/details/requestId 错误对象;阶段 3 数值精度边界和阶段 4 multipart 业务协议仍未实现,当前不得描述为可用。

所有新业务接口计划位于 /api/v1。当前 /api/login/api/goods* 只作为冻结的迁移兼容契约存在,在对应新用例、数据迁移和对账获得用户批准前不得删除,也不得承载新能力。普通列表采用稳定键游标,默认 50 条、最大 200 条,响应包含 itemsnextCursor;统一错误对象包含 codemessagedetailsrequestId。审批决定和每次执行使用 Idempotency-Key

审计写路由的传输观察值由 HTTP 适配器建立,不能从 JSON DTO 注入。默认来源 IP 是套接字直接对等地址并忽略转发头;可选 WAREHOUSE_HTTP_TRUSTED_PROXY_CIDRS 仅在直接对等端受信时启用严格的 X-Forwarded-For 逐跳剥离,畸形链回退直接对等端,首版不解释 RFC 7239 Forwarded。User-Agent 仅是客户端自述观察值并按数据库边界规范化。每个已注册审计写请求生成一个服务端 requestId,贯穿响应、应用、审计和重试;详细协议见 ADR 0045。

人员账户激活与双因素信任建立

除受控启动建立的首名经理和监察员外,新建人员账户先进入 PENDING_ACTIVATION。创建或治理批准只建立账户和 current issuer authorization,不生成秘密;初始指定者是创建申请发起人。指定者通过独立领取命令调用无数据库副作用的 ActivationCredentialIssuer,现场生成 activation ID、256 位随机秘密、哈希和 24 小时期限,并在同一调用方工作单元中只持久化哈希。明文只在成功领取响应中显示一次,再由实际签发者通过线下或已核验的独立渠道交付。

累计第 5 次失败立即吊销凭据;消费、吊销或轮换也会使原凭据失效,响应丢失时不恢复旧明文。原指定者失权后,只有活动经理发起、不同自然人监察员批准的 ReassignActivationIssuer 可以转移领取权,批准本身不生成秘密。领取人激活时自行建立长期密码,长期密码不经过创建者界面、日志或审计载荷。账户治理应用层使用 typed variant commands/receipts;持久化和 HTTP 合同按 ADR 0046 分片实现,当前不得把合同冻结描述为生产写路由已完成。

高权限账户只通过密码验证后获得受限密码会话,只能访问账户状态、退出、TOTP 绑定/确认和重新认证资源;完成 TOTP 后轮换为完整会话。管理员和会计的 TOTP 恢复由经理发起、监察员批准;经理由另一名经理发起、监察员批准;监察员由经理发起、另一名监察员批准。发起人、批准人和目标互不相同,可用人员不足时严格阻断,不提供 break-glass 绕过。

浏览器会话与请求防伪

密码验证成功后生成相互独立的 256 位随机不透明会话令牌和初始 CSRF Token;高权限账户在完成 TOTP 前该会话保持受限,浏览器仅通过 __Host-warehouse_session Cookie 携带会话令牌;生产 Cookie 设置 Secure; HttpOnly; SameSite=Strict; Path=/ 且不设置 Domain。MySQL sessions 表只保存会话令牌哈希、初始随机 CSRF 哈希、用户、创建时间、最近活动时间、绝对到期时间、最近 TOTP 重新认证时间、撤销时间及必要请求环境摘要,派生恢复 CSRF 及其哈希不持久化。服务重启不改变有效性或撤销状态。认证令牌不得写入浏览器持久化存储、URL、日志或审计前后摘要。

管理员、经理、监察员和会计会话闲置 15 分钟或创建满 8 小时失效;仓库员工和代理商闲置 30 分钟或创建满 12 小时失效。账户治理、权限恢复、临时代管、仓库结构、重大会计决定和外部账册映射要求最近 10 分钟内重新验证 TOTP。登录、重新认证、身份或密码变化、TOTP 恢复和管理员权限恢复按规则轮换或撤销会话。

所有使用浏览器 Cookie 的状态变更请求必须提交 X-CSRF-Token 并通过 Origin 和 Fetch Metadata 检查;SameSite 只作为纵深防御。写鉴权以常量时间接受两条路径之一:请求值的 SHA-256 与会话行保存的初始随机 CSRF 哈希匹配,或请求值与当前挂载密钥现场派生值匹配。长度无效、密钥缺失或派生失败一律失败关闭。GET、HEAD 和 OPTIONS 不得改变服务端状态。外部总账、汇率或其他服务集成使用独立服务凭据和鉴权适配器,不共享浏览器 Cookie 或 CSRF 机制。

GET /api/v1/sessions/current 使用独立 32 字节版本化挂载密钥和 libsodium keyed BLAKE2b,从域分离串、8 字节大端密钥版本长度、UTF-8 密钥版本和当前原始会话令牌派生 32 字节恢复 CSRF。该读取严格只读:不得触碰活动时间、续期、轮换、撤销、step-up、访问状态或审计。切换当前密钥后旧派生值立即失效、新值立即有效,不接受旧版本重叠;同一 Cookie 会话与仍持有的初始随机 CSRF 保持有效,而会话轮换、撤销或到期会同时淘汰该旧会话的两类 CSRF。

前端固定使用 X-CSRF-Token 自定义头,只在模块或 Pinia 运行时内存保存 CSRF,并在启动或刷新后通过 GET /api/v1/sessions/current 恢复;不得写入 localStoragesessionStorage、Cookie、URL、日志或审计。Vue 3 + Vite + TypeScript 工作台按身份和业务能力拆分路由与 Pinia 状态;Vitest 验证组件及状态逻辑,Playwright 验证六身份端到端场景。界面隐藏只改善体验,任何敏感字段和动作仍由后端授权。

外部集成身份与请求认证

外部总账及上下级仓库账册使用独立的不可交互服务账户,不复用六类人员账户。服务账户没有网页登录、人员密码或 TOTP;数据库只保存账户标识、获批范围、凭据或证书的不可逆指纹、有效期与撤销状态,私钥和共享密钥由可替换密钥存储端口读取首版部署挂载文件,环境变量只保存文件路径。经理发起服务账户创建、授权、轮换或撤销,监察员批准后生效;授权范围必须同时限制仓库子树、品类和允许的拉取、推送接收或确认动作。

所有集成端点强制 HTTPS,账册拉取和接收确认优先使用双向 TLS;服务证书与 HMAC 密钥最长有效 30 天,新旧凭据最多重叠 12 小时。IP 允许列表只能作为可选纵深防御,不能代替凭据。推送适配器以 HMAC-SHA256 对事件 ID、时间戳和原始请求正文签名,接收端只接受服务器时间正负 2 分钟内的时间戳,并按事件唯一标识拒绝重放。签名验证必须在解析业务载荷前完成,并使用恒定时间比较。

审计完整性

audit_events 使用 libsodium SHA-256 和长度前缀规范字节编码形成一条全局链。每次追加在业务工作单元中锁定唯一 audit_chain_head 行,读取前一哈希、计算并插入新事件,再更新链头;业务失败时审计追加一并回滚。应用数据库账户对审计事实只有查询和插入权限,数据库触发器阻止更新和删除。该顺序化链头是已接受的写入开销,不能未经新决策改为分仓或分月多链。

actor 身份与职责范围必须从同一工作单元锁定的授权快照产生,before/after 摘要必须从锁定的 typed 状态与实际写结果产生;公共命令不接受这些字段的自由文本。业务写入、通知、会话撤销等数据库副作用全部完成后才锁定 audit_chain_head;此后只允许审计插入、链头更新和提交,追加失败时整个工作单元回滚。

服务账户只能读取其范围内的账册事件或追加外部接收确认与引用号,不能修改内部库存、流水、分录、映射或审计事实。每次成功使用、鉴权失败、越权拒绝、轮换与撤销都写入审计记录;疑似泄露时可立即撤销而不影响人员会话。版本化外部映射仍由会计重大决定流程控制,不因服务账户获准连接而自动授权。

渐进重构

重构以可运行的纵向切片推进:先建立组合根、配置和 MySQL 工作单元,再迁移认证与只读库存查询,然后迁移申请、审批和执行,最后接入会计、附件、跨仓及外部适配器。旧接口在对应新用例通过测试并完成数据迁移前继续存在;不进行一次性重写,也不在同一提交中同时改变无关行为。