# 使用说明 清算完成后,核对发行、销售、TRS 的订单和日常报表,把结论写入 `db_post_clear_check`,并发到飞书专用群。 这是一次性命令行进程,没有页面。启动后做完一轮就退出。进程不读 leopard 的 `.env`、源码或本目录里的文档。链路口径和节点公式见 `doc/业务链路核对设计.md`,本文件只讲怎么跑、怎么看结果。 程序在 `code/post_clear_check/`。下面的命令都从该目录执行。要求 Python `>=3.11.8`。 ## 准备 两台连接可以是同一台 MySQL,账号必须分开。 | 连接 | 账号权限 | 用途 | |---|---|---| | 业务库 | 只对 `db_issue`、`db_issue_clear`、`db_sales`、`db_trs` 有 `SELECT` | 读批次、订单、流水、报表 | | 自有库 | 只对 `db_post_clear_check` 有读写 | 写运行记录和结论 | 业务连接用 `mysql-connector-python` 的 C 扩展,并固定打开 MySQL 协议压缩:业务库常在异地,TRS 明细报表一次要传几 MB,压缩后取数快约 4 倍(本机连 UAT 实测从约 43 秒降到约 10 秒)。C 扩展随安装包自带;不可用时进程直接报错,不退回不压缩的实现。自有库连接仍用 `asyncmy`。结论行按块批量写入自有库。 自有库由人执行建库脚本,进程启动时不建表: ```bash mysql --default-character-set=utf8mb4 -h <自有库主机> -u <管理员> -p \ < code/post_clear_check/sql/schema.sql ``` 脚本只创建 `db_post_clear_check` 和三张表:`check_run`、`check_result`、`check_line`。 脚本是重建脚本:三张表已存在时先 `DROP TABLE IF EXISTS` 再创建,已有的轮次、卡片和结论行全部清空,可以重复执行。清空后下次启动会把当天重新核一遍并重新发卡,群里会看到同一天的卡再出现一次。 要保留历史核对记录时不要跑整份脚本,改由人执行一次下面的语句,只给 `check_result` 加链路列。旧轮次的行取默认空串;它们的规则版本与新代码不同,不会再被命中,也不会被重发。 ```sql ALTER TABLE check_result ADD COLUMN chain_name VARCHAR(32) NOT NULL DEFAULT '' COMMENT '业务链路代码,见业务链路核对设计 4.1;unassigned 为未归链路' AFTER card_kind, ADD CONSTRAINT chk_check_result_chain_name CHECK ( chain_name COLLATE utf8mb4_bin IN ( '', 'buy_execution', 'sell_execution', 'cash_change', 'cash_settlement', 'account_asset', 'note_holding', 'topup', 'extension', 'trade_execution', 'user_asset', 'check_gate', 'unassigned' ) ); ``` 回滚代码到上一版时,`chain_name` 列可以保留:旧代码不读它,插入时取默认空串,CHECK 允许空串。要连列一起撤,人工执行一次 `ALTER TABLE check_result DROP CHECK chk_check_result_chain_name, DROP COLUMN chain_name;`。 ## 配置 配置文件放在工具目录之外。工具目录是同时有 `pyproject.toml` 和 `sql/schema.sql` 的那一层,即 `code/post_clear_check/`。文件放进这一层、权限不是 `0600`、或没设置路径,进程退出且退出码为 1。不要把这份文件放进仓库。 ```bash install -m 0600 /dev/null "$HOME/.config/post-clear-check.env" ``` 文件里的键都带前缀 `POST_CLEAR_CHECK_`: ```bash POST_CLEAR_CHECK_BUSINESS_HOST=127.0.0.1 POST_CLEAR_CHECK_BUSINESS_PORT=3306 POST_CLEAR_CHECK_BUSINESS_USER=业务只读账号 POST_CLEAR_CHECK_BUSINESS_PASSWORD=口令 POST_CLEAR_CHECK_OWN_HOST=127.0.0.1 POST_CLEAR_CHECK_OWN_PORT=3306 POST_CLEAR_CHECK_OWN_USER=自有库账号 POST_CLEAR_CHECK_OWN_PASSWORD=口令 POST_CLEAR_CHECK_FEISHU_WEBHOOK=https://open.feishu.cn/open-apis/bot/v2/hook/xxx POST_CLEAR_CHECK_FEISHU_SIGN_SECRET= # 只核这个营业日。留空或不写这一行,只核上海时区的今天。格式 YYYY-MM-DD。 POST_CLEAR_CHECK_BUSINESS_DATE= # 推送飞书的结论类型。留空或不写,正确、错误、可疑都推,日志里带「推送类型未配置,三类都推」。 # 多选用逗号、顿号或空格分开,例如只推错误:错误 # 也可以写成:错误、可疑 POST_CLEAR_CHECK_FEISHU_PUSH= # 旧归属卡按链路限制本次推送张数。四个方向卡不受这个上限。 # 大于该链路现有张数、填 0 或留空,则这条链路的旧卡全部推送。 POST_CLEAR_CHECK_FEISHU_PUSH_LIMIT= ``` 进程不循环。两次启动隔多久由 cron 决定,本工具不提供轮询间隔配置。 `POST_CLEAR_CHECK_BUSINESS_DATE` 可省略。省略或留空时,只核启动时刻上海时区的今天,不扫描其他营业日。要核历史某一天,写成 `2026-10-17` 这种日期。 `FEISHU_SIGN_SECRET` 可留空。机器人开了签名校验时填密钥。`FEISHU_WEBHOOK` 留空时结论仍落库,发送状态记为 `failed`。 `FEISHU_PUSH` 留空时三类都推。只写 `错误` 时,正确和可疑的旧归属卡、门禁快照仍写入 `check_result`,状态保持 `pending`,不调用飞书。四个方向卡不看这个过滤,正确的方向也会发。之后把类型加回配置,下次启动会补发这些 `pending` 卡。日志里「飞书卡片已发送」「飞书发送未成功」「飞书卡片未能发送」在未配置推送类型时会带「推送类型未配置,三类都推」。这表示没有按结论类型过滤,不是没配 Webhook。配了类型之后,被跳过的旧卡打「不在推送配置内」。 `FEISHU_PUSH_LIMIT` 是旧归属卡按链路计数的张数上限,和上面的类型过滤叠在一起。四个方向卡不受这个上限,否则会少发一整个方向。旧归属卡仍按主键从前往后排,每条链路只发前 N 张。N 大于该链路现有的待发张数、填 0 或留空时,这条链路的待发明细全部发送。配了 N 时,被抽中的旧明细卡正文第一行写「抽样明细第 n 条」。超出的卡保持 `pending`。 路径本身用环境变量指出,不要写进配置文件里: ```bash export POST_CLEAR_CHECK_CONFIG="$HOME/.config/post-clear-check.env" ``` 日志不打印口令、Webhook、签名密钥和连接串。 ## 启动 ```bash cd code/post_clear_check python3 -m venv .venv . .venv/bin/activate pip install -e . # 默认源下载慢时可换镜像,例如:pip install -e . -i https://mirrors.aliyun.com/pypi/simple/ export POST_CLEAR_CHECK_CONFIG="$HOME/.config/post-clear-check.env" python -m post_clear_check ``` 安装后也可以直接执行 `post-clear-check`。退出码 `0` 表示这一轮走完。退出码 `1` 表示配置打不开、业务连接不是只读,或启动失败。日志写到标准错误。 常见日志: | 日志 | 含义 | |---|---| | `本轮只核对营业日 YYYY-MM-DD` | 这一轮只读这一个营业日。没配置时就是上海时区的今天 | | `没有开跑,本轮结束` | 该营业日发行和销售还没有同一市场、同一交易日的完成批次,也没有提前出现的日切报表 | | `轮次已存在,不重新计算` | 这一轮已经落过库,只重发还没送到的卡片 | | `本轮核对已结束` | 本进程该做的都做完了 | | `业务连接不是只读会话,退出` | `@@transaction_read_only` 不是 1 | | `飞书卡片已发送,…` | 这一张卡已送到。未配置推送类型时,这句后面带「推送类型未配置,三类都推」,表示正确、错误、可疑都发 | | `飞书卡片未能发送:…` | 这一张卡改为 `failed`,下次启动再发 | | `飞书卡片不推送,…不在推送配置内` | 这张卡的结论不在 `FEISHU_PUSH` 里,状态仍是 `pending` | | `飞书卡片超出推送条数,…` | 这条链路本次已发满 `FEISHU_PUSH_LIMIT` 张。这张没发,状态仍是 `pending`,下次启动再从该链路剩余列表前面取 | | `有对冲,TRS 批次未完成,发行和销售本轮不核对` | 这一天先等 TRS。核对日早于今天时另落一张错误卡 | | `开始<步骤>` / `<步骤>完成,用时 N 秒,…` | 每一步的开始和结束。步骤依次是检查三端清算批次、从业务库取数、核对订单和三端报表、写入自有库、发送飞书卡片。完成句后面带查询条数、方向张数、结论行数或发送结果 | | `仍在<步骤>,已用 N 秒,…` | 不在终端里运行时(cron、重定向到文件),长步骤每 10 秒打一条,带进度和当前在读哪张表,表示程序没有卡住 | | `<步骤>中断,已用 N 秒` | 这一步抛错退出,原因看紧接着的错误日志 | 在终端里运行时不打「仍在…」,改为在标准错误最下面保留一行会刷新的状态:总数未知的步骤(检查批次、取数)显示循环的「...」、已用秒数和「第 N 条查询,读 某库.某表」;总数已知的步骤(核对、写库、发卡)显示进度条、完成数和百分比。日志写出时状态行会先擦掉再画回,不会和日志混在一行。 ## 一轮里做什么 1. 业务连接执行 `SET SESSION TRANSACTION READ ONLY`,再读 `@@transaction_read_only`。不是 1 就退出。之后这条连接只接受 `SELECT`、`SHOW`、`EXPLAIN`。 2. 只读配置营业日(没配置则是上海时区的今天)的三端 `clearing_batch`。同一个市场、同一个交易日,发行和销售都是 `completed`,并且 `end_time` 有值,才开跑。同一市场同一交易日有多条完成批次时,取 `id` 最大的那条。`applied_revision` 只存在于发行批次;销售和 TRS 没有这一列,核对记录里记 0。TRS 批次也没有 `market`,本期按 `a_share` 与发行、销售对齐。 3. 发行库该交易日存在未删除、订单号以 `ORD` 开头的对冲时,TRS 也必须是 `completed`。TRS 还没完成就跳过这一天,并打上面那句日志。核对的营业日早于上海时区的今天时,再落一张清算批次错误卡,`biz_key` 为交易日。当天等待不落这张卡。没有对冲则不产生 TRS 结论行,`trs_batch_id` 写 0,`trs_end_time` 写 `1000-01-01 00:00:00`。 4. 日切报表已经有行,对应交易日却还没有完成批次,单独落一张核对门禁卡,结论为 `error`。代入式后面写明这是门禁快照。报表表名变了会在同一轮次追加卡。对应的真实轮次落下之后,还没发出的快照卡改为 `sent` 且不再补发,库里的结论行仍是当时的 `error`。 5. 开跑后只读取数,再算订单规则和发行、销售、TRS 报表。没有发生的业务不写结论行,也不发卡片。取数范围: - 发行、销售日报按营业日整张读。 - 主订单读「交易日等于本轮交易日」的(核对人口),再补读当日日报行引用的往日主订单(参照订单,例如 T+1 在本营业日交收的买单)。参照订单只用来挂链路、取方向、子单执行值和交收台账,不跑订单执行规则,也不要求必须有 OTC003 行。 - 资金账户取订单上的账户与当日日报行上的资金账户的并集。T 日流水读已发布 `kind=cleared`,没有候选时不退回实时流水。营业日晚于 T 的延后流水另读实时表。 - OTC004 的已卖出合计按报表票据号补读该票据已完成或已结算的订单,交易日不晚于本轮交易日;这些订单不进入订单规则。票据和报表用的订单行只读已发布数据集的 `cleared/0`。往日卖单不在这份归档里时不退回实时表。买入执行的成交名义本金、订单成交均价读 `input/0`,并与实时源表这两列对照;卖出执行另外把卖出股数、卖出面值、卖出增额、实现分红也读 `input/0`。两条的结算口径都读 `work/3`,`work/4` 与 `cleared/0` 应与它一致。报表对 `cleared` 子单,不用回写 after_val。销售端 OTC003、OTC007、OTC008 的承销费用等于已发布销售订单的 `exec_underwrite_fee_settle`,发行子单没有这一列。实时表上的结算口径列、买入股数和结算汇率不作为公式输入。 - 对冲和 TRS 执行根只读核对人口的主订单,TRS 报表的用户范围与改动前一致;参照订单的对冲不算本批对冲。这些用户本交易日的全部委托(含算法子单,ext_ref 为空)和这些委托的成交回报也读入。有本批 `kind=cleared` 时,TRS003、TRS004 和步骤节点改读 cleared 的委托、成交,不退回实时委托当 T 日结果。报表列名与委托表、成交表不同的按列注释对应,不改报表列名。成交金额等于该委托成交回报金额之和(算法母单、拆单主单累加执行子单),不是成交数量乘成交均价。TRS004 没有成交金额列,不核这一列。TRS006 按流水主键读本批 `clearing_snapshot` 里 `fund_ledger` 与 `clearing_fund_ledger` 的 payload,不读实时 `fund_ledger`。汇率调整另用这份快照里的 `clearing_fund_ledger`,按 T 日 cleared 成交现金减封存的原始买卖凭证。封存汇率只核第 5 步和第 8 步用的是本批 `clr_fx_user`,不计算应该等于多少。成交数量为 0 的委托保留下单汇率,不与封存汇率比较。成交数量大于 0 的委托和全部成交才要求等于封存汇率。TRS009 的出入金读 `db_trs.fund_in_out_record`,按报表 `ref` 对流水 `source_id`(入金/出金单主键),兼容流水主键与 `source_no`。TRS007 利息收取读同营业日 `fund_interest_collect`,按区间与结算币种对齐。T 日债务用本批 `interest_input`,T+1 已收取的不从债务里拿掉。`cleared` 不得含 `trade_date` 晚于 T 的成交,快照不得含营业日晚于 T 的非交易。OTC004 的票据列只用已发布 `cleared/0`,不退回实时表;最新价用本交易日 `cfg_daily_close_price` 收盘价。底层资产浮动盈亏按收盘价 × 数量 × 账面汇率 − 期初价格 × 数量 × 初始汇率 + 分红计算,结算金额为交易金额乘汇率。对冲仓位只读已发布 `db_issue_clear.clr_hedge_position` 的 `cleared/0`,没有已发布数据集不读实时持仓。TRS005 持仓数量读本批已清算数据集的 `clr_user_position.position_qty`,持仓成本金额对 `book_cost_amount`。TRS001 可用金额、冻结金额对 `clr_fund_account` 的 `available`、`frozen`,持仓市值对 `clr_user_position.market_value` 按用户和结算币种合计。TRS014 持仓市值用同一合计,不乘清算价。TRS016 持仓成本金额对 `cost_amount` 合计;有非零持仓成本而 `finance_rate` 为空时记错误(与区间导出缺少当月有效月度融资利率拒绝导出一致)。TRS010 现金结余等于本行承前现金、出入金额、实际盈亏、已付利息之和。TRS013 当日应付利息等于(融资金额 + 授信借贷 + 信用借贷)× 年化 / 100 / 365,年化按百分数(3.2542 表示 3.2542%),未收取且计息日为本营业日的应付利息必须有报表行。买入和卖出的累计发生金额、结算汇率先用 input 对冲的两币种发生金额;本批 clearing_trs_result 有该对冲记录时改用其发生金额的绝对值再反推汇率,不用 TRS 导出的汇率原值。销售 input 里营业日早于 T、步骤号为 3 的买卖待交收是上一批尚未交收的来源,计入历史未交收和到期交收,不算当日原始流水。卖出比例在没有执行名义本金时不出结论,日志记一行。票据当前名义本金为 0 是全部卖出,比例用初始名义本金(一笔卖完为 100%,多笔卖完按各笔执行名义本金去除),面值用初始面值;当前名义本金缺失才是错误。 - OTC010 只要求金额非 0 的已交收流水出现在报表里;金额为 0 的展期费冻结、解冻不要求。 - OTC001 的钱包比对和现金可用读 T 日已发布候选派生出的钱包(`kind=cleared`,当日买卖先置 0,再算现金余额、冻结和可用)。科目都是正数;报表买入待交收展示为负值,比较和代入公式时取相反数。没有已发布候选或缺本币种行时这几条记可疑。账户资产、资金变动、资金交收的步骤节点另读销售 `kind=input` 与 `kind=cleared`:第 3、4 步用 input 重算后与 cleared 比,不读 `pre_clearing`。input 里已经有该订单 posted 的解冻凭证时,不再把交易冻结扣第二次。重放 input 流水按主键 id 升序,不按查询返回的幂等哈希顺序。发行和销售日报只读 `status=active`,同一营业日的 superseded、deprecated 旧行不核对。等值港币资产净值按现金可用、冻结、票据估值各自换算后量化到分再相加(与销售报表一致);整笔资产净值推不出唯一方向时改用三项总和推断。乘、除量化到分后结果相同时,方向视为已确定。汇率为 1 的同币种订单结算汇率与名义本金换算记正确,不再因乘除同值记可疑。钱包只重算本批 `input` 与 `cleared` 的 T 日结果,不读实时钱包。发行交收台账读 `work/4`,`cleared/0` 应与它一致,不读实时台账。日终 `daycut_fund_eod` 只在核对门禁上与这份候选比,不一致才记错误。 - 同一 IN 列表超过 500 个值时分几条语句读。 - OTC007、OTC008 行按票据号挂到订单上,只核业务类型(`buy_note`、`sell_note`)与主订单方向一致、交易日相同的行。同一票据另一张订单的行不算进来;哪张已读主单都对不上的行按「有行但无法对应主订单编号」记可疑。 6. 结论行在调用核对函数的地方挂到业务链路(见下表),再按产生来源收成最多四张方向卡写入自有库,发给飞书。顺序是订单链路、发行报表、销售报表、TRS 报表。没有结论行的方向不发。一张卡上有多条时,总结论取最严重的一档:`error` 高于 `suspect`,`suspect` 高于 `correct`。方向卡主题是错误时,正确行仍写在正文里。`check_run` 上的三个计数是方向卡张数,不是结论行数。门禁没开跑时仍只落清算批次卡,不发四张空卡。 方向卡的 `card_kind` 是 `dimension`,`chain_name` 是 `order_chain`、`issue_report`、`sales_report`、`trs_report`,`biz_key` 是营业日。结论行上的内部链路代码不改。没挂链或归属键为空的行仍是未归错误,放进产生它的那个方向。发行门禁写进发行报表的批次门禁,销售、TRS 同理。订单卡的摘要只引用三端门禁有没有差异。链路的口径和节点以 `doc/业务链路核对设计.md` 为准。 | 链路 | 结论行上的 `chain_name` | 结论行归属 | 现有结论行 | |---|---|---|---| | 买入执行 | `buy_execution` | 主订单 | 买入方向的订单规则、对冲执行根、两端 OTC003 | | 卖出执行 | `sell_execution` | 主订单 | 卖出方向(含强平、到期)的订单规则、对冲执行根、两端 OTC003 | | 资金变动 | `cash_change` | 资金账户 | 销售 OTC002;`input` 原始流水、T 日重算流水、延后流水 | | 资金交收 | `cash_settlement` | 资金账户 | 两端 OTC007、OTC008,销售 OTC010;发行 `work/4` 与 `cleared/0`;销售历史未交收与到期交收 | | 账户资产 | `account_asset` | 资金账户 | 销售 OTC001;期初钱包、T 日重算钱包、候选范围 | | 票据存续 | `note_holding` | 资金账户 | 两端 OTC004、OTC005;发行 `work/2` 期初、`cleared` 与 `work/4`、当前投影、昨基线;销售 `note_view` | | 增额 | `topup` | 资金账户 | 发行 `input` 增额主键集合(仅 `top_up`);`work/3` 增额累计含同日 `sell_release` | | 展期 | `extension` | 资金账户 | 发行 `input` 展期主键集合,以及 `work/3` 的展期后到期日 | | 委托执行 | `trade_execution` | 主订单,对不上时用户账号 | TRS002、TRS003、TRS004、TRS005、TRS008;封存成交、T 日委托、持仓行、候选成交 | | 用户资产 | `user_asset` | 用户账号 | TRS001、TRS006、TRS007、TRS009、TRS010、TRS012、TRS013、TRS014、TRS016;封存资金、封存汇率、T 日债务、汇率调整、资金账户、候选资金 | | 核对门禁 | `check_gate` | 开跑前仍是清算批次卡;开跑后并进对应报表卡 | 报表早于批次、TRS 未完成、业务表缺失(1146)、发行基线与上游版本、发行发布唯一、销售锁定的发行版本、销售发布成功、TRS 日期推进、TRS 版本切换、日终快照与候选不一致。批次 completed 是开跑条件,不单独成卡 | | 未归链路 | `unassigned` | 并进产生它的方向卡 | 挂不上链路、主订单方向取不到、取数缺口、链路要求的归属键为空的行,结论一律为错误 | 票据存续、资金交收的报表行只有资金账户列。这两条链路的结论行取报表行的 `fund_account_no`;没有报表行时,票据存续、增额、展期取子票据或展期任务上的 `fund_account_ref`,其余取主单上的销售侧资金账户;都没有就进未归链路。资金变动、资金交收的行对得上主订单时,正文仍写主订单编号,归属仍是资金账户。 委托执行里,TRS002、TRS003、TRS004 只在对应委托或算法母单的成交数量大于 0 时要求有行。已撤、废单、未成交不进这三张报表。对冲带回委托号本身不构成缺行。卡片上的主订单编号是发行单号,用来把结论归到这张主订单。持仓的数量、成本和第 6 步的可用、冻结、估值在 cleared 的同一行上核对,不拆成两份快照。资金账户只读 cleared 的第 5 步列,第 6 步不替换这张表。 OTC009、OTC012 是实时导出,没有落库结果,`report_topup_payment`、`report_extension_fee` 两张表没有写入方。增额、展期不比对这两张表,取数也不读它们。增额核的是本批 `input` 里归属本营业日的 `top_up` 主键,以及这些发生额记入 `work/3` 后的增额;展期核的是已通过且审批归属本营业日的任务主键,以及 `work/3` 的到期日。卖出释放和展期费流水不进这两条链路。 飞书按订单链路、发行报表、销售报表、TRS 报表的顺序发。标题形如 `订单链路 可疑`、`发行报表 正确`。失败重发在末尾加「重发」。标题不写系统和清算步骤。颜色按这张卡的总结论:有错误为红,否则有可疑为橙,否则为蓝。 订单链路正文是摘要、每单链路、盘中公式、清算对照、关联报表、错误清单、可疑清单、无法展开。摘要里的笔数按主单计。每单链路写两段:执行事实是否与实时源表一致;需要 TRS 执行根的对冲,成交股数是否对上,并说明自动单按 ext_ref、补录按委托号或母单号。不列表,也不把未核对的票据和已撤单写进去。同一字段的正确行收成一块,结论写「正确(n/n)」。清算对照除订单自己的已发布执行列、工作快照外,再写交收台账、销售资金流水、TRS 发生额与汇率、清算留痕;没有对应结论行就写「本轮未核」,不编造金额。关联报表按发行/销售的 OTC003、OTC004、OTC007/OTC008 展开期望、实据和依据。三端门禁在摘要里各一行。 三张报表正文是批次门禁、摘要、分报表、步骤核对、错误清单、可疑清单、无法展开。没有门禁行时不写批次门禁。摘要只按 OTC、TRS 编号计张数,并写目录是否到齐。票据号、子订单交收推演、增额、钱包、原始流水是步骤,写在「步骤核对」,标题用步骤名,不叫报表。同一报表、同一依据的正确列收成一块。错误和可疑逐条写节点、期望、实据、依据、结论;没有报表编号的写步骤和对象。方向卡是错误主题时,正确列仍留在分报表。OTC009、OTC012 不进目录。TRS011、TRS015 不进目录。 章节之间空一行。一张报表或一个字段内部用单个换行。期望的多个字段用逗号连成一句。同一份原型只写一次,字段接在文件名后面。空清单写成「错误清单:无」。方向卡不按 3500 字删报表或公式段。请求体超过约 28KB 时按结论行拆成多张,每张带同一个记录编号和「第几张,共几张」,整卡仍是一个 `notify_status`。公式里的制表符会去掉,`<` `>` 会改成全角。发送失败时日志带上飞书返回的 `msg`。 已有库要先放宽两道 CHECK,再启动。不要重跑 `schema.sql`,那会清空已有轮次。没执行时新的方向卡插不进去,这一轮会整段回滚。 ```sql ALTER TABLE check_result DROP CHECK chk_check_result_card_kind; ALTER TABLE check_result ADD CONSTRAINT chk_check_result_card_kind CHECK ( card_kind COLLATE utf8mb4_bin IN ( 'dimension', 'master_order', 'fund_account', 'trs_user', 'clearing_batch', 'chain_summary' ) ); ALTER TABLE check_result DROP CHECK chk_check_result_chain_name; ALTER TABLE check_result ADD CONSTRAINT chk_check_result_chain_name CHECK ( chain_name COLLATE utf8mb4_bin IN ( '', 'order_chain', 'issue_report', 'sales_report', 'trs_report', 'buy_execution', 'sell_execution', 'cash_change', 'cash_settlement', 'account_asset', 'note_holding', 'topup', 'extension', 'trade_execution', 'user_asset', 'check_gate', 'unassigned' ) ); ``` 同一轮次由唯一键识别:市场、交易日、三端批次 id、三端 `end_time`、规则版本。规则版本是订单和三端报表规则源文件、列清单 `columns.py`、取数 `reader.py`、销售资金步骤 `fund_chain.py`、链路登记表 `chain.py` 的 sha256。卡片幂等键在卡片种类后面加方向代码。同一营业日的四个方向是四张卡、四个幂等键。这些文件有一个字节变化,就新开一轮,旧轮次保留。第一次换成这个版本时,已经核对过的营业日会再跑一遍并发出四张方向卡。已经 `sent` 的旧归属卡不撤回。命中已有轮次时不重新取数、不新插结论行。`notify_status` 为 `pending` 或 `failed` 的卡片会重发:四个方向按固定顺序,不看推送类型和条数;旧归属卡和门禁快照仍按推送配置过滤。成功改为 `sent`。从 `failed` 再发时,标题末尾加「重发」。发行的 `applied_revision` 会记在 `check_run` 上,销售和 TRS 记 0。修订号不单独构成新的一轮;三端 `end_time` 或批次 id 没变、规则文件也没变时,再次启动仍走重发。 `check_run.create_time` 和 `check_result` 的时间用上海时区的墙钟,不跟容器的 UTC。 业务表不存在(MySQL 1146)时,在对应报表方向卡上记一条错误,公式是「业务表应存在」。开跑前的门禁快照仍单独落清算批次卡。挂不上链路、主订单方向取不到、链路要求的归属键为空的结论行并进产生它的方向卡,结论为错误,公式以「结论行没有归属」开头,原因、原结论和原依据写在代入式后面。原因写「取数缺口:报表引用的主订单不在已读取范围」的,是报表行引用的主订单补读后仍没有主单,属于核对工具的取数问题,先查取数范围,不要当成业务数据错误;「主订单方向取不到」才是读到了主单但方向不是买卖。未知列(1054)仍然让进程失败退出。 正文超过列宽时保留前缀,并以「…已截断」结尾。数值仍在 `calc_value`、`check_value`。 要对着真实数据看结果,当天的发行批次和销售批次都得已经是 `completed`。批次还在跑时,正常日志就是「没有开跑,本轮结束」。 ## 看结果 飞书群里看卡片。库里用下面三句对上同一轮,查询带 `--default-character-set=utf8mb4`。 ```sql SELECT id, market, trade_date, business_date, issue_batch_id, sales_batch_id, trs_batch_id, correct_count, error_count, suspect_count, create_time FROM db_post_clear_check.check_run ORDER BY id DESC; SELECT id, run_id, chain_name, card_kind, biz_key, verdict, notify_status FROM db_post_clear_check.check_result WHERE run_id = <上一句的 id> ORDER BY id; SELECT formula_text, calc_text, calc_value, check_text, check_value, basis, verdict FROM db_post_clear_check.check_line WHERE result_id = <卡片 id> ORDER BY id; ``` `calc_text`、`calc_value` 是按公式算出来的,原来的说明接在 `calc_text` 的代入式后面。`check_text`、`check_value` 是库里已经落下的那一列。`basis` 是三种依据之一的定位。缺输入或整行不存在时,对应的值为 `NULL`,说明写在文字列里。百分比在 `calc_value` 里按百分数存,`12.12` 表示 12.12%。 `verdict` 只存英文:`correct`、`error`、`suspect`。`notify_status` 为 `pending`、`sent`、`failed`。`chain_name` 存链路英文代码,卡片上显示中文名。 ## 不连库走一遍 单测用假连接把「门禁、取数、规则、落库、发送、失败重发」走完,不连接 MySQL,也不发飞书。 ```bash cd code/post_clear_check python3 -m venv .venv . .venv/bin/activate pip install -e ".[test]" pytest ``` 编排行为在 `tests/test_orchestrate.py`:同一轮次第二次进入不新增 `check_run`;发送失败后再跑,不复制 `check_line`。