使用说明.md 30 KB

使用说明

清算完成后,核对发行、销售、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。结论行按块批量写入自有库。

自有库由人执行建库脚本,进程启动时不建表:

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 加链路列。旧轮次的行取默认空串;它们的规则版本与新代码不同,不会再被命中,也不会被重发。

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。不要把这份文件放进仓库。

install -m 0600 /dev/null "$HOME/.config/post-clear-check.env"

文件里的键都带前缀 POST_CLEAR_CHECK_:

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。

路径本身用环境变量指出,不要写进配置文件里:

export POST_CLEAR_CHECK_CONFIG="$HOME/.config/post-clear-check.env"

日志不打印口令、Webhook、签名密钥和连接串。

启动

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 按用户和结算币种合计;本批没有该用户清算持仓行时合计为 0。TRS014 持仓市值用同一合计,不乘清算价。TRS016 持仓成本金额对 book_cost_amount 合计(与 TRS005 账面成本同源,同币种多通道行先汇总);有非零持仓成本而 finance_rate 为空时记错误(与区间导出缺少当月有效月度融资利率拒绝导出一致)。TRS016 当日计息金额 = 持仓成本金额 × 当月融资利率 / 100 / 年天数(CNY=360,HKD=365),利率按百分数。TRS005 同一证券多通道报表行先按证券+交易渠道+结算币种汇总再对清算聚合持仓。TRS009 流水金额按方向赋符号后与报表 Amount 比较(入正出负)。TRS010 现金结余等于本行承前现金、出入金额、实际盈亏、已付利息之和。TRS013 当日应付利息等于(融资金额 + 授信借贷 + 信用借贷)× 年化 / 100 / 365,年化按百分数(3.2542 表示 3.2542%),未收取且计息日为本营业日的应付利息必须有报表行。买入和卖出的累计发生金额、结算汇率先用 input 对冲的两币种发生金额;本批 clearing_trs_result 有该对冲记录时改用其发生金额的绝对值再反推汇率,不用 TRS 导出的汇率原值。销售 input 里营业日早于 T、步骤号为 3 的买卖待交收是上一批尚未交收的来源,计入历史未交收和到期交收,不算当日原始流水。卖出比例在没有执行名义本金时不出结论,日志记一行。票据当前名义本金为 0 是全部卖出,比例用初始名义本金(一笔卖完为 100%,多笔卖完按各笔执行名义本金去除),面值用初始面值;当前名义本金缺失才是错误。全部卖出后票上增额或分红已是 0、订单对应金额却不是 0 时,无卖出前快照,卖出增额、实现分红与因此缺独立增额的总金额均不核(不出结论行);底层盈亏在分红跳过时改用订单已落库的实现分红代入。
    • 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)与主订单方向一致、交易日相同的行。同一票据另一张订单的行不算进来;哪张已读主单都对不上的行按「有行但无法对应主订单编号」记可疑。
    • 主订单买入、卖出执行链路用已发布 cleared/0 的 note_sub_a 核对三个风控距离比例:分别按 (latest_px − warn_price) / latest_px × 100%、(latest_px − danger_price) / latest_px × 100%、(latest_px − force_price) / latest_px × 100% 计算,四舍五入到 2 位后比较。latest_px 为 0 时无法计算。
  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 报表的顺序发。标题形如 订单链路 可疑、发行报表 正确。失败重发在末尾加「重发」。标题不写系统和清算步骤。颜色按这张卡的总结论:有错误为红,否则有可疑为橙,否则为蓝。错误清单里:有期望数值的仍写「期望 x,当前 y」;算不出期望数值的存在性错误,期望写正向门槛或公式,失败原因放在结论里,不写「期望 {缺少…}」。

订单链路正文是摘要、每单链路、盘中公式、清算对照、关联报表、错误清单、可疑清单、无法展开。摘要里的笔数按主单计。每单链路写两段:执行事实是否与实时源表一致;需要 TRS 执行根的对冲,成交股数是否对上,并说明自动单按 ext_ref、补录按委托号或母单号。不列表,也不把未核对的票据和已撤单写进去。同一字段的正确行收成一块,结论写「正确(n/n)」。清算对照除订单自己的已发布执行列、工作快照外,再写交收台账、销售资金流水、TRS 发生额与汇率、清算留痕;没有对应结论行就写「本轮未核」,不编造金额。关联报表按发行/销售的 OTC003、OTC004、OTC007/OTC008 展开期望、实据和依据。三端门禁在摘要里各一行。

三张报表正文是批次门禁、摘要、分报表、步骤核对、错误清单、可疑清单、无法展开。没有门禁行时不写批次门禁。摘要只按 OTC、TRS 编号计张数,并写目录是否到齐。票据号、子订单交收推演、增额、钱包、原始流水是步骤,写在「步骤核对」,标题用步骤名,不叫报表。同一报表、同一依据的正确列收成一块。错误和可疑逐条写节点、期望、实据、依据、结论;没有报表编号的写步骤和对象。方向卡是错误主题时,正确列仍留在分报表。OTC009、OTC012 不进目录。TRS011、TRS015 不进目录。

章节之间空一行。一张报表或一个字段内部用单个换行。期望的多个字段用逗号连成一句。同一份原型只写一次,字段接在文件名后面。空清单写成「错误清单:无」。方向卡不按 3500 字删报表或公式段。请求体超过约 28KB 时按结论行拆成多张,每张带同一个记录编号和「第几张,共几张」,整卡仍是一个 notify_status。公式里的制表符会去掉,业务正文里的 < > 会改成全角。错误块整段用 lark_md 红加粗;可疑块与无法展开同款——正文加粗,结论行用黄色 text_tag(飞书正文没有黄色字体,只用标签醒目)。正确块不加色。发送失败时日志带上飞书返回的 msg。

已有库要先放宽两道 CHECK,再启动。不要重跑 schema.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。

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,也不发飞书。

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。