---
title: 项目编码约束
doc_type: coding-rulebook
domain: project
keywords:
  - 编码约束
  - 单一契约
  - fail fast
  - 运行时兼容
summary: 定义处理契约漂移、历史字段和结构性错误时必须遵守的编码硬规则。
when_to_read:
  - 要改字段契约、数据映射或错误处理逻辑
  - 需要判断某种兼容写法是否允许
read_next:
  - ./README.md
  - ../features/notice-data-model-migration.md
---
# 项目编码约束

这份文档定义本项目的代码设计硬规则。

它只解决一个问题：

当代码面对“契约漂移、字段改名、数据缺失、历史遗留格式”时，应该怎么处理。

如果你想先看全局边界，而不是只看编码层的硬规则，先读 [项目约束总表](./README.md)。

本项目采用的默认立场是：

- 禁止用“兜底”“兼容”“静默回退”掩盖问题
- 采用 fail fast，尽快暴露真实问题
- 问题一旦暴露，优先修正数据源、契约或边界，而不是继续堆胶水代码

## 适用范围

这些规则对下面几层同时生效：

- `src-tauri/src/*`
- `src/shared/*`
- `src/renderer/src/*`
- `src-tauri/sql/*`

## 核心原则

### 1. 单一语义只能有一个正式字段

同一个业务概念，只允许保留一个正式字段名和一个正式语义。

例如：

- 公告分类查询参数如果查的是中文分类名，就应该叫 `categoryName`
- 如果字段表示“最终打开的原文链接”，就应该明确叫 `noticeLink`
- 如果字段表示“数据库里原样落库的链接”，就应该单独命名，不允许和“最终展示链接”混用

禁止这种设计：

- 同时保留 `categoryCode` 和 `categoryName` 但实际都拿来查中文分类
- 同时接受 `noticeLink`、`notice_link`、`sourceUrl`、`source_url`，靠运行时猜测哪个是真值
- 前端和后端各自对同一语义做一套解释

### 2. 不允许新增运行时兼容层

新改动默认不允许引入下面这些模式：

- camelCase / snake_case 双字段并行读取
- 新旧接口字段并行兜底
- “如果 A 没有就读 B，B 没有就读 C”的结构性回退
- “先兼容一下，后面再收口”的半迁移状态

如果确实存在历史数据迁移需求，只能在这两层处理：

- 数据迁移脚本
- 单次 schema / 数据修复逻辑

运行时消费层只能读取新契约。

### 2.1 原始 API 表只保存原始值

凡是“原始 API 快照 / 原始来源元数据”表，默认遵守下面 3 条：

- 表命名统一使用 `api_<provider>_<domain>_*`
- 表字段允许用规范化列名，但字段值必须保持 API 原样返回
- 业务派生值只能在服务层或读模型层生成，禁止反写回原始表

例如：

- CNInfo 返回 `adjunctUrl` 是相对路径，就原样存入 `api_cninfo_notice_raw.source_url`
- 如果业务需要“正式打开的公告详情页链接”，应通过公共派生方法生成 `noticeLink`
- 不允许把派生后的详情页链接覆盖写回 `api_cninfo_notice_raw.source_url`

一句话：

`api_*` 表负责可重现的原始事实，业务字段负责可消费的正式语义，两者不能混用。

### 2.2 SQLite 表必须使用固定分类前缀

所有新增 SQLite 表都必须使用固定分类前缀命名，统一格式为：

`<category>_<domain>_<entity>[_qualifier]`

当前正式前缀如下：

| 前缀 | 正式语义 | 例子 |
| --- | --- | --- |
| `api_` | 第三方 provider 原始快照、官方码表、原始返回值 | `api_cninfo_notice_raw` |
| `art_` | 原文、报告、快照等制品 | `art_artifact` |
| `biz_` | 产品正式消费的业务事实和业务工作流实体 | `biz_notice_fact` |
| `llm_` | 大模型调用和审计控制面 | `llm_request_log` |
| `mkt_` | 市场底座、行情、指标和派生市场事实 | `mkt_stock_basic` |
| `sync_` | 同步状态、导入批次和同步元数据 | `sync_status` |
| `sys_` | 系统配置、用户、审计和通用控制面 | `sys_config` |
| `tsk_` | 后台任务运行轨迹和任务事件 | `tsk_run` |
| `tmp_` | 一次性迁移、导入、测试期临时表 | `tmp_cninfo_notice_raw_import` |

硬规则：

- 新表必须命中上述前缀之一。
- 禁止继续新增 `notice_*`、`stock_*`、`tushare_*` 这类不带固定分类语义的表名。
- legacy 表名如果暂时不能迁移，必须先登记到 `config/constraints.json` 的 legacy 例外列表。
- 运行时代码、迁移代码和测试里的临时表也不能随意发明前缀；临时表统一用 `tmp_`。

一句话：

`表名第一段不是装饰，它是数据库职责分类。`

## 3. fail fast 的具体要求

### 契约层

共享契约、bridge 返回值、IPC 输入输出必须尽早校验。

要求：

- 必填字段缺失时直接报错
- 字段类型不对时直接报错
- 语义错位时直接报错
- 不允许自动改写成看起来“还能跑”的值

推荐做法：

- 在前端 API 门面层做显式 contract parse
- 在 Rust 命令入口或服务边界做显式参数校验
- 错误信息里写清楚缺了哪个字段、哪层返回了错误结构

### 展示层

界面层不能用沉默的 `-` 或空字符串掩盖结构性错误。

允许：

- 数据确实为空时显示 `-`
- 数据存在但依赖缺失时显示 `分类映射缺失`、`原文链接缺失` 这类显式问题状态

禁止：

- 因为分类映射没建好，就回退显示原始数字分类码
- 因为字段缺了，就悄悄显示空白，让问题看起来像“没有数据”

### 服务层

服务层必须把“解析”“选择”“回退”分清楚。

允许：

- 在服务层一次性产出一个明确的“最终展示字段”
- 同时保留一个单独命名的“原始字段”用于排障
- 基于原始字段按固定规则派生业务字段，但不污染原始表

禁止：

- 把多种来源值交给页面层临时拼接
- 让页面层自己决定哪个字段才是真值

## 4. 精准修复优先级

当问题出现时，按下面顺序修：

1. 修数据源或生产者
2. 修共享契约
3. 修服务边界
4. 修消费方
5. 最后才考虑一次性数据回填

禁止反过来做：

- 先在页面层做兼容
- 再在 API 门面层做兼容
- 最后把真正的契约问题留到以后

## 5. 逻辑准确性校验是完成定义的一部分

这条规则对所有业务逻辑改动、边界改动和共享契约改动生效。

### 必须校验什么

任何一次改动，都必须覆盖下面 3 类中的所有适用项：

1. 契约校验
2. 核心逻辑校验
3. 关键链路校验

具体要求：

- 契约校验：
  - 校验输入输出结构、字段语义、命名一致性
  - 适用于 contract parse、shared type、IPC 输入输出、mapper、adapter
- 核心逻辑校验：
  - 校验主分支、边界条件、失败路径、关键计算
  - 适用于筛选、映射、状态推进、聚合、策略判断、格式选择
- 关键链路校验：
  - 至少验证一个真实关键链路的调用编排或状态传递
  - 适用于 `view/composable/store -> api/client -> bridge/shared contract -> command/service` 这类跨层调用

### 怎么执行

- 本地统一入口：`npm run verify:logic`
- 当前默认顺序：
  1. `npm run check:constraints`
  2. `npm run typecheck`
  3. `npm run test`

### 不允许的做法

- 只看页面能不能点通，不验证业务判断是否正确
- 只做 happy path，不验证边界条件和失败路径
- 用“只是重构”作为跳过验证的理由
- 提交代码但不运行 `npm run verify:logic`

一句话：

`没有完成逻辑准确性校验的代码设计，不算完成。`

## 6. 允许与禁止

| 场景 | 允许 | 禁止 |
| --- | --- | --- |
| 字段改名 | 同步改 producer、contract、consumer | 在 consumer 侧双读新旧字段 |
| 旧数据修复 | migration / backfill 一次性修复 | 运行时长期兼容旧格式 |
| 多来源值 | 服务层产出单一明确字段 | UI 自己在多个候选字段里猜 |
| 缺失依赖 | 明确报错或显式问题态 | 静默显示为空或回退旧值 |
| 契约校验 | 入口处校验并报错 | 默认值吞掉问题 |
| 逻辑验证 | 运行 `npm run verify:logic` 并补适用测试 | 不验证关键分支或关键链路 |

## 7. 公告原文链路的直接约束

这条规则对“公告原文”功能立即生效：

- 页面查询中文分类名时，参数必须命名为 `categoryName`
- 页面展示分类时，只展示 `categoryName`
- 若存在原始分类码但映射表未命中，页面应明确显示 `分类映射缺失`
- `api_cninfo_notice_raw.source_url` 必须保存 CNInfo API 原始返回值，不得写入业务拼接后的详情页地址
- 业务正式打开链接必须由 Rust 服务层按 `announcementId + stockCode + plate` 规则派生为 `noticeLink`
- 前端不得再接受 `category_name`、`notice_link`、`source_url` 这类历史字段作为运行时兼容输入
- 若 IPC 返回了旧字段，必须视为契约错误并直接暴露

## 8. 代码评审清单

评审任何新改动时，至少检查这 7 条：

1. 有没有同一语义的双字段并存
2. 有没有把结构性错误变成默认值
3. 有没有把历史兼容压到页面层
4. 有没有把服务层应做的选择丢给 UI
5. 有没有用模糊命名掩盖真实语义
6. 有没有跳过逻辑准确性校验
7. 有没有新增“以后再收口”的半迁移逻辑

## 9. 违反规则时怎么处理

如果发现违规代码，不要继续在违规实现上加补丁。

处理顺序：

1. 停止继续叠加兼容逻辑
2. 说明真实契约应该是什么
3. 删掉兼容分支
4. 把错误改成显式暴露
5. 补上测试，防止旧模式回流

## 结论

这个项目以后默认不是“尽量跑起来”，而是“尽快暴露真实问题，然后做精确修复”。

一句话总结：

`禁止运行时兜底兼容，强制单一契约，使用 fail fast 暴露问题，并把逻辑准确性校验作为完成定义的一部分。`
