# OIP-Accounting **Repository Path**: orginone/OIP-Accounting ## Basic Information - **Project Name**: OIP-Accounting - **Description**: OIP-Accounting(会计核算子系统) - **Primary Language**: Rust - **License**: MIT - **Default Branch**: master - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 0 - **Forks**: 1 - **Created**: 2026-09-25 - **Last Updated**: 2026-10-12 ## Categories & Tags **Categories**: Uncategorized **Tags**: None ## README # OIP-Accounting(oip-accounting) OIP 元会计核算层(L4)。本服务消费 L3 权利状态机产出的权利事件,实时判断其财务影响并转为核算凭证, 按「业务发生即财务记账、核算即报告」的口径对外提供判断器、规则治理、分录与共识、对账、存证与报表接口。 代码内的定位与分层说明见 `src/lib.rs` 的模块文档。 ## 技术栈 | 项 | 取值 | 依据 | | --- | --- | --- | | 语言 / 版本 | Rust,edition 2024 | `Cargo.toml` | | crate 名 / 版本 | `oip-accounting` 0.1.0 | `Cargo.toml` | | 许可 | MIT | `Cargo.toml`(`license = "MIT"`)与 `LICENSE` | | HTTP 服务 | `axum` 0.7(`json`)、`tower-http` 0.6(`trace`、`limit`) | `Cargo.toml` | | 异步运行时 | `tokio` 1(`full`) | `Cargo.toml` | | 序列化 | `serde`、`serde_json`、`serde_yaml` 0.9、`serde_repr` | `Cargo.toml` | | 错误处理 | `thiserror` 2、`anyhow` 1 | `Cargo.toml` | | 日志 / 追踪 | `tracing` 0.1、`tracing-subscriber` 0.3(`env-filter`、`json`) | `Cargo.toml` | | 时间与标识 | `chrono` 0.4(`serde`)、`uuid` 1(`v4`、`v7`、`serde`、`rng`) | `Cargo.toml` | | 密码学 | `sha2` 0.10、`ed25519-dalek` 2(`rand_core`)、`rand_core` 0.6(`getrandom`)、`base64` 0.22、`aes-gcm` 0.10 | `Cargo.toml` | | 事件通道客户端 | `async-nats` 0.50(JetStream)、`bytes` 1、`futures-util` 0.3 | `Cargo.toml` | | 出站 HTTP 客户端 | `reqwest` 0.12(`json`、`rustls-tls`,`default-features = false`) | `Cargo.toml` | | 测试辅助(dev) | `tower` 0.5(`util`)、`http-body-util` 0.1、`wiremock` 0.6、`tempfile` 3 | `Cargo.toml` | | Release 配置 | `lto = "thin"` | `Cargo.toml` | ## 架构分层 依赖方向单向(`src/lib.rs`): - `domain`:纯业务逻辑——金额与数量(整数最小单位)、维度标签、规则 DSL 与注册表、判断器、三重分录与共识、对账、默克尔审计树、报表。 - `ports`:端口 trait——存储、事件、发件箱、锚定、内容寻址、JWE、价格、L2 只读上下文、DID 公钥解析、LLM 推理。 - `application`:用例——事件摄取、共识推进与冲销、对账、存证、报表物化、规则治理。 - `adapters`:HTTP 接入、进程内内存实现、经 L2 标准 API 的存储、NATS JetStream 通道、本地锚定与内容寻址、 外部 HTTP 接缝(LLM / 公链锚定 / IPFS 网关)、JWE 引擎、接入底座上报、后台任务。 ## 存储边界(§2.3) §2.3 规定 L4 不直连 L2 原始数据集合、只经 L2 标准 API 读写,并列出 L4 在 L2 侧的数据类型。 本仓据此划出一条**显式**边界——`store.mode: oipdata` 时: | 数据 | 位置 | 依据 | | --- | --- | --- | | 核算分录 | L2(`l4_journal_entries`) | §2.3 表「核算分录(热 / 温-合规)」 | | 事件原文 | L2(`l4_accounting_events`) | §2.3 表「事件原文」 | | 报表快照 | L2(`l4_report_snapshots`) | §2.3 表「报表快照」 | | 审计摘要锚定 | 公链(`anchor.mode: chain`) | §2.3 表「审计摘要锚定」 | | 责任锚定 | IPFS(`blob.mode: ipfs`) | §2.3 表「责任锚定」 | | 规则 / 判重 / 人工队列 / DLQ / 发件箱 | **进程内** | §2.3 的表里没有它们:它们是 L4 自有的运行态,不是 L2 的数据资产 | 最后一行是**显式选择而非遗漏**,装配时会打 WARN 逐项列出。它的后果是:多副本部署时 判重与发件箱不共享存储,Exactly-Once 的闸门只在单副本内成立——扩副本的前提是先为这几类 数据提供共享存储。 `oipdata` 模式的存储端口实现与对端契约均已就位(`src/adapters/oipdata/`),但该契约由本仓给出: OIP-Data(L2)尚未提供供 L4 使用的 REST 面(`src/adapters/oipdata/client.rs` 模块头列出对端须实现的 六个端点与统一的 `OperateResult` 信封)。装配不因此失败;在 L2 侧按契约实现之前,`oipdata` 模式的 读写请求会在运行期以「后端不可用 / 存储失败」返回。 Merkle 审计日志与锚定台账**始终落在本地**(`blob.dir` 的同级目录):它们是 L4 自己的审计日志 (叶子顺序 = 本进程的处理顺序),`anchor.mode` 只决定"根锚到哪"。 ## 后台任务 `runtime.enabled` 为 `true` 时启动一个事件消费循环与五个定时作业(`src/adapters/runtime.rs`): | 作业 | 周期配置 | 幂等依据(重入时不产生额外副作用) | | --- | --- | --- | | 事件消费循环 | 常驻 | 事件判重(Exactly-Once 的唯一依据) | | 发件箱中继(§2.3) | `outbox.poll_secs` | 消息 `message_id`:投递成功或放弃后即"了结" | | 共识超时清算(§4.2) | `runtime.consensus_sweep_secs` | 状态机:只清算仍 `PENDING` 且已过截止时刻的共识 | | 日终对账(§4.3) | `runtime.reconcile_secs` | 会计日窗口确定性 + "已有冲销分录即跳过" | | 审计交叉校验(§8.3) | `runtime.cross_verify_secs` | 只读,不写入 | | 报表快照物化(§6.1) | `runtime.snapshot_secs` | 快照一经物化不可改写;重复物化只多出一份同源视图 | 定时作业的失败只记日志、不终止进程——一次短暂的通道不可达不应把核算服务带走。 长周期作业(如 24h 的日终对账)同样能在停机标志置位后立即退出,不会等到下一个周期。 ## 目录结构 ```text OIP-Accounting/ ├── .github/workflows/ │ ├── ci.yml 格式检查 / clippy / 测试 / Release 构建 / 供应链检查 │ └── contract-check.yml 协议面漂移校验 ├── Cargo.toml ├── Cargo.lock ├── LICENSE ├── NOTICE 版权与来源声明 ├── deny.toml cargo-deny 依赖策略(漏洞 / 许可 / 重复版本 / 来源) ├── AGENTS.md 协作规范 ├── README.md / README.en.md 项目总览(中文 / 英文) ├── config.example.yaml 配置模板(复制为 config.yaml 后本地使用) ├── deploy/ │ └── plugin-manifest.json GoInOne 插件清单(唯一事实源,编译期嵌入) ├── rules/ 规则种子 R-001.json … R-017.json ├── src/ │ ├── main.rs 服务入口 │ ├── lib.rs crate 根与分层说明 │ ├── config.rs 配置加载与生产门禁 │ ├── state.rs 组合根(按配置装配适配器) │ ├── clock.rs 时钟抽象(SystemClock / ManualClock) │ ├── error.rs OIP 统一错误模型与错误码 │ ├── response.rs OperateResult 统一响应 │ ├── ids.rs 标识符生成(je_{YYYYMMDD}_{seq}、带前缀 UUID v7) │ ├── metrics.rs Prometheus 文本指标暴露 │ ├── topics.rs 事件主题注册表与订阅白名单 │ ├── topics_gen.rs 由 OIP 规范仓生成的主题表 │ ├── protocol_gen.rs 由 OIP 规范仓生成的协议面规范表 │ ├── domain/ 领域层 │ ├── ports/ 端口层 │ ├── application/ 用例层 │ └── adapters/ 适配器层 │ ├── http/ /oip/v1/* 路由与处理器、插件清单 │ ├── oipdata/ 经 L2 标准 API 的客户端与存储(§2.3) │ ├── remote/ LLM / 公链锚定 / IPFS 网关(外部 HTTP 接缝) │ ├── jwe.rs JWE 引擎(§10.1 的本地等价实现) │ ├── local/ 本地内容寻址 + Merkle 日志 + 锚定台账 │ ├── memory/ 进程内默认实现 │ ├── nats/ JetStream 消费者与发布器 │ └── runtime.rs 事件消费循环与全部定时作业 └── tests/ └── rules_golden.rs 规则黄金用例 ``` `rules/` 下的规则文件由 `src/domain/rules/seed.rs` 以 `include_str!` 编译进二进制,并在启动期自检。 `deploy/plugin-manifest.json` 由 `src/adapters/http/manifest.rs` 编译期嵌入,`GET /manifest` 返回其解析结果, 两者由单测锁定为逐字节一致。文件里的 `endpoint` 是**路径**(部署基址是部署事实,不入版本库); 上报接入底座时由 `src/adapters/goinone.rs` 换成 `public_base_url + 该路径` 的绝对地址——底座据 `endpoint` 拼出站地址转发调用,相对路径拼不出可达地址。 ## 常用命令 ```bash # 格式检查(CI 执行同名命令) cargo fmt --all --check # 静态检查(CI 以 -D warnings 运行) cargo clippy --all-targets -- -D warnings # 全部测试(含 tests/rules_golden.rs) cargo test --all-targets # 供应链检查(依赖漏洞 / 许可 / 重复版本 / 来源,CI 执行同名命令) cargo deny check # Release 构建(CI 以 --locked 运行,锁定 Cargo.lock) cargo build --release --locked # 本地启动(默认读取 ./config.yaml) cargo run # 指定配置文件启动 OIP_ACCOUNTING_CONFIG=/path/to/config.yaml cargo run ``` `Cargo.toml` 未定义 `[features]` 与 `[[bin]]`,因此命令不需要 `--features`,二进制目标即 `src/main.rs`。 仓库中没有 `Makefile` 或 `scripts/`,构建入口只有 Cargo。 无 `config.yaml` 时进程按内置演示配置启动(内存存储 + 内存事件 + `demo_mode`)。 ## 持续集成 `.github/workflows/ci.yml` 在 push(`master` / `main`)与 pull_request 上运行两个作业: - `build-test`(Rust 1.98.1,环境变量 `RUSTFLAGS=-D warnings`):依次执行 `cargo fmt --all --check`、 `cargo clippy --all-targets -- -D warnings`、`cargo test --all-targets`、`cargo build --release --locked`。 - `supply-chain`:经 `EmbarkStudios/cargo-deny-action` 执行 `cargo deny check --all-features`, 判据取自 `deny.toml`。 `.github/workflows/contract-check.yml` 执行协议面漂移校验:规范仓为兄弟目录时直接运行 `python3 ../OIP/tools/gen_protocol.py --check`;规范仓不可用时以 `::error::` 失败(fail-closed), 不静默跳过。`src/topics_gen.rs` 与 `src/protocol_gen.rs` 即由该脚本生成。 ## 配置说明 配置来源为 `config.yaml`,可用环境变量 `OIP_ACCOUNTING_CONFIG` 覆盖路径(`src/config.rs`)。 `config.example.yaml` 是模板,复制为 `config.yaml` 后本地填写;`config.yaml` 不入库。 生产环境必须提供配置文件且 `demo_mode: false`,否则 `enforce_production_gates` 会拒绝启动。 上报配置另有一条自洽门禁 `enforce_goinone_gate`:声明要上报(`goinone.base_url` 非空)却缺 `public_base_url` 时同样拒绝启动——否则底座登记到的 `endpoint` 不可达。 | 配置项 | 含义 | | --- | --- | | `listen` | HTTP 监听地址。本服务只暴露 `/oip/v1/*` 三个前缀,鉴权由 OIP-GoInOne 前门承担。 | | `log_level` | 日志级别默认值:`EnvFilter` 优先取环境变量,取不到时用 `oip_accounting={log_level},axum=info`。 | | `demo_mode` | 演示模式。为 `true` 时允许内存存储与内存事件启动;生产必须为 `false`。 | | `public_base_url` | 本服务对**接入底座可达的绝对基址**(如 `http://oip-accounting:8087`),上报清单的 `endpoint` 由它派生;`goinone.base_url` 为空时可不填。注意它不等于 `listen`(后者常为 `0.0.0.0`,底座不可达)。 | | `fiscal.year_start_month` | 会计年度起始月份(1–12);非 1 月即非常规会计年度。 | | `fiscal.timezone_offset_minutes` | 会计时区相对 UTC 的分钟偏移(东八区为 480);期间归属按此时区派生。 | | `fiscal.late_event_policy` | 期后到达事件策略:`accept_into_period` / `move_to_next_period` / `reject`。 | | `consensus.timeout_secs` | 双签超时(秒),模板取 1800(T+30min)。 | | `consensus.required_signers` | 达成共识所需签名方数量。 | | `consensus.trusted_keys` | 静态信任表:DID → Ed25519 公钥(base64)。默认空表,此时签名无法被验证通过。代码中定义于 `src/config.rs`,模板未列出。 | | `judge.llm_enabled` | 是否启用 LLM 兜底判断器,默认关闭。 | | `judge.confidence_threshold` | 规则命中置信度阈值,低于该值转人工审核队列。 | | `judge.unmatched_policy` | 未命中任何规则时的策略:`human_review` / `record_only`。 | | `store.mode` | 凭证存储:`memory`(演示/测试)或 `oipdata`(经 L2 标准 API)。生产门禁拒绝 `memory`。边界见「存储边界」一节。 | | `store.oipdata_base_url` | `store.mode: oipdata` 时必填(缺省即拒绝启动):写 L4 数据的 L2 基地址。 | | `store.oipdata_token` | 上述 L2 接口的访问令牌(`Authorization: Bearer`)。 | | `events.mode` | 事件消费:`memory`(演示/测试)或 `nats`(生产 JetStream)。 | | `events.nats_uri` | NATS 连接地址。 | | `events.durable_name` | JetStream durable 消费者名。 | | `events.ack_wait_secs` | 投递确认超时(秒)。 | | `events.max_deliver` | 最大重投次数,超限转死信队列。 | | `anchor.mode` | 存证锚定:`local`(进程内 Merkle 链)或 `chain`(公链锚定客户端)。 | | `anchor.chain_endpoint` | `anchor.mode: chain` 的锚定端点;契约见 `src/adapters/remote/anchor.rs` 模块头。 | | `anchor.chain_api_key` | 上述锚定端点的凭据(`Authorization: Bearer`,留空即不发该头)。 | | `blob.mode` | 原始凭证内容寻址:`local`(本地目录)或 `ipfs`(IPFS 网关)。 | | `blob.dir` | `blob.mode: local` 的本地目录,模板取 `./data/blob`;Merkle 日志与锚定台账落在其同级目录。 | | `blob.ipfs_gateway` | `blob.mode: ipfs` 的网关地址;契约见 `src/adapters/remote/blob.rs` 模块头。 | | `llm.base_url` | LLM 兜底判断器的服务地址(OpenAI 兼容 `POST {base_url}/chat/completions`);为空则用本地等价实现。 | | `llm.api_key` | 上述 LLM 服务的凭据(留空即不发 `Authorization` 头)。 | | `llm.model` | 模型名,写入 `judge_log.note` 供审计回溯。 | | `llm.timeout_secs` | LLM 调用超时(秒);超时按"依赖不可用"处理 → 转人工队列。 | | `l2.base_url` | L2(OIP-Data)标准 API 基址,**用于读 VET 上下文**;为空时使用进程内上下文实现(规则因缺值不命中)。 | | `l2.token` | L2 标准 API 的访问令牌。 | | `l2.timeout_secs` | L2 上下文读取超时(秒)。 | | `outbox.poll_secs` | 发件箱中继轮询周期(秒)。 | | `outbox.batch_size` | 单批投递条数。 | | `outbox.max_attempts` | 单条事件最大投递次数。 | | `runtime.consensus_sweep_secs` | 共识超时扫描周期(秒)。 | | `runtime.reconcile_secs` | 日终对账周期(秒)。 | | `runtime.cross_verify_secs` | 审计交叉校验周期(秒)。 | | `runtime.snapshot_secs` | 报表快照物化周期(秒)。 | | `runtime.enabled` | 为 `false` 时不启动任何后台定时任务(测试与单次注入场景)。 | | `audit.signing_key` | 本服务作为存证节点的签名私钥(hex,32 字节 Ed25519 种子)。生产模式必填。 | | `audit.signer_did` | 存证节点 DID,模板取 `did:oip:accounting`。 | | `jwe.key` | 字段级加密的内容加密密钥(§10.1):32 字节的 base64/base64url。**无默认值**,未配置即引擎不可用(`/readyz` 的 `jwe_enabled=false`);长度非法在启动期即拒绝。 | | `jwe.kid` | 密钥标识,写入 JWE 受保护头的 `kid`,供轮换与审计回溯。 | | `goinone.base_url` | 接入底座(OIP-GoInOne)地址:启动时向 `POST {base_url}/oip/v1/plugins/register` 上报插件清单。**为空即不上报**——本服务在未接入底座时仍可独立受理 `/oip/v1/*`。 | | `goinone.auth_token` | 上报使用的内部 Bearer;底座未启用鉴权时留空(留空即不发 `Authorization` 头)。 | | `goinone.timeout_secs` | 上报超时(秒)。上报失败只告警不阻断启动,此项只防"底座无响应把启动拖住"。 | 凭据类字段(`store.oipdata_token`、`anchor.chain_api_key`、`llm.api_key`、`l2.token`、`audit.signing_key`、`jwe.key`、`goinone.auth_token`) 只从配置或环境注入,禁止提交到版本库;这四个 HTTP 客户端的 `Debug` 实现均对凭据做脱敏,不会随日志外泄。 ## HTTP 接口 GoInOne 插件清单(`deploy/plugin-manifest.json`)声明三个路由前缀,本服务不实现鉴权: - `/oip/v1/accounting`:`/manifest` 插件清单,规则目录与治理(版本、状态跃迁、沙箱回放),维度模式与校验,判断器试算与判断日志,事件摄取与查询,审核队列,死信队列。 - `/oip/v1/ledger`:分录查询与冲销、三方共识签名与拒绝、对账、不可变性违规、四重存证与证明、审计追溯与交叉校验、内容寻址读取。 - `/oip/v1/report`:四类报表(碳资产负债表 / 部门损益 / 合规仪表板 / CUBE)、三类账本(序时账 / 总账 / 明细账)、七类使用者投影、报表快照。 - 非前缀路由:`/healthz`、`/readyz`、`/metrics`。 `/metrics` 按设计文档 §12.1 暴露 16 个指标的 Prometheus 文本(含 §12.1 规定的标签维度), 另加本层特有的发件箱投递与判重命中计数(`oip_l4_outbox_published_total`、`oip_l4_outbox_failed_total`、 `oip_l4_dedup_hits_total`); 无界标签取值(如 `dim_value`)有硬上限,超限并入 `__overflow__` 桶且不丢总数。 清单同时声明能力 `accounting.judge`、`accounting.triple_ledger`、`accounting.reconcile`、`accounting.report`、 `accounting.audit`,发布主题 `oip.events.accounting.voucher.created`,以及一组订阅主题(`oip.events.vet.*`、 `oip.events.audit.appended`、`oip.events.compliance.flagged`)。 ## 错误码 错误码定义于 `src/error.rs` 的 `OipErrorCode`(`#[repr(u16)]`,共 19 档),对外经 `OperateResult.code` 返回; 公共码与 `OIP/spec/protocol.json` 逐项一致(单测比对 `protocol_gen::ERROR_CODES`),领域专属码不复用公共码数值。 HTTP 状态映射只经 `OipErrorCode::http_status()`;未知路由由 `router.rs` 的 `fallback` 返回 `4041`。 | 码 | 含义 | 归属 | | --- | --- | --- | | 4001 | 请求参数格式错误 | 公共 | | 4007 | 金额非法(精度越界 / 溢出 / 除零 / 负值) | 领域 | | 4008 | 会计维度非法(缺必选维度 / 枚举外取值 / 类型不符) | 领域 | | 4009 | 规则定义非法(DSL 结构 / 公式 AST / 版本) | 领域 | | 4010 | 事件非法(CloudEvents 形状 / 类型与主题不一致) | 领域 | | 4011 | Proof / 断言验证失败 | 公共 | | 4015 | 签名验证失败(共识签名) | 领域 | | 4031 | 无权执行此操作(布尔盲化) | 公共 | | 4041 | 对象不存在 | 公共 | | 4094 | 状态冲突 | 公共 | | 4095 | 三重分录共识状态冲突 | 领域 | | 4096 | 分录不可变违规 | 领域 | | 4097 | 规则状态机冲突(非法跃迁) | 领域 | | 4098 | 会计期间已结账 | 领域 | | 4291 | 频率限制触发 | 公共 | | 5001 | 内部错误(对外不泄露详情) | 公共 | | 5002 | 存证锚定失败 | 领域 | | 5003 | 存储不可用 | 领域 | | 5031 | 后端服务不可用 / 断路器打开 | 公共 | ## 与 OIP 其他仓库的关系 以下均为工作区内的纯文本路径,不在本仓库内: - OIP-GoInOne:本服务是其业务插件,认证、路由、限流与 VET 权限校验由前门承担,本仓不实现鉴权; 启动时经 `src/adapters/goinone.rs` 向其上报插件清单(`goinone.base_url` 非空时),上报失败不阻断启动。 - OIP-Book:L3 权利事实(VET)的权威源;本层消费其事件,绑定与裁定归其所有。 - OIP-Data:L2 存证与原始数据;按 §2.3 硬约束,本层不直连 L2 原始数据集合,只经 L2 标准 API 读写。 - OIP 规范仓:`src/topics.rs` 与 `src/protocol_gen.rs` 的常量表声明由 OIP/spec/events.json 与 OIP/spec/protocol.json 生成; `contract-check.yml` 调用 OIP/tools/gen_protocol.py 做协议面漂移校验。 - 设计文档:`OIP-Accounting-Design.md`、`OIP-GoInOne-Design.md`(代码注释中按章节引用)。 ## 已知边界 - 经 L2 标准 API 解析 **DID 文档**(`ports::DidKeyResolver` 的实现)不由本仓承担:共识验签的公钥来源 是静态信任表 `consensus.trusted_keys`(`src/state.rs`),表为空时无签名可通过验签。 - §10.1 的落盘 / 传输加密调用点不在本仓:JWE 引擎在 `src/adapters/jwe.rs`(`jwe.key` 未配置时 引擎缺失,`/readyz` 报 `jwe_enabled=false`),写入路径不调用它。 - 进程内的规则 / 判重 / 队列 / DLQ / 发件箱(见「存储边界」)不跨副本共享,多副本部署所需的 共享存储由部署侧提供。 - 本仓不提供独立的部署脚本或容器编排文件;`deploy/` 只包含插件清单 `plugin-manifest.json`。 ## 文档 - 设计:[OIP-Accounting-Design.md](../OIP/docs/OIP-Accounting-Design.md) - 协作规范:[AGENTS.md](AGENTS.md) - 许可:[MIT](LICENSE)