使用说明.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 张。买入执行抽 1 张,不会占用卖出执行或资金变动的名额。概况卡不计数。N 大于该链路现有的待发张数、填 0 或留空时,这条链路的待发明细全部发送。配了 N 时,明细卡正文第一行写「抽样明细第 n 条」,n 是这条链路本次抽样的序号,从 1 起。超出的卡保持 pending,下次启动再从该链路剩下的最前面取 N 张。

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

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_no。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 合计。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 旧行不核对。等值港币资产净值在乘、除四舍五入到分后结果相同时,方向视为已确定。钱包只重算本批 input 与 cleared 的 T 日结果,不读实时钱包。发行交收台账读 work/4,cleared/0 应与它一致,不读实时台账。日终 daycut_fund_eod 只在核对门禁上与这份候选比,不一致才记错误。
    • 同一 IN 列表超过 500 个值时分几条语句读。
    • OTC007、OTC008 行按票据号挂到订单上,只核业务类型(buy_note、sell_note)与主订单方向一致、交易日相同的行。同一票据另一张订单的行不算进来;哪张已读主单都对不上的行按「有行但无法对应主订单编号」记可疑。
  6. 结论行在调用核对函数的地方挂到业务链路(见下表),收成卡片写入自有库,再发给飞书。发送时先发每条有明细的链路的概况,再发明细。一张明细卡上有多条时,总结论取最严重的一档:error 高于 suspect,suspect 高于 correct。飞书上显示为错误、可疑、正确。主题为错误的明细卡,正文只写错误清单,摘要写明正确与可疑留在核对明细;同一张卡上的正确行和可疑行仍留在 check_line,不出现在这张飞书卡的正文里。check_run 上的三个计数是明细卡片张数,不是结论行数,也不含概况卡。

一张卡只承载一条链路、一个归属对象,按「链路 + 归属 + 主键」分卡。同一主订单上有几条链路就是几张卡。归属按链路登记的优先顺序取结论行上第一个非空的键,不从正文反推。链路的口径和节点以 doc/业务链路核对设计.md 为准。

链路 chain_name 归属(card_kind) 现有结论行
买入执行 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 增额主键集合,以及记入 work/3 的增额累计
展期 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 的到期日。卖出释放和展期费流水不进这两条链路。

飞书先发链路概况,再发明细。概况一张链路一张,标题形如 链路-买入执行 正确2条,错误1条,可疑0条。这里的「条」是归属个数(一张明细卡计 1),不是结论行数。一张明细卡上又有正确行又有错误行时,只出现在它的总结论那一档。没有的那一档也写 0。失败重发在末尾加「重发」。标题不写系统和清算步骤。卡片颜色仍按这条链路有没有错误、可疑:有错误为红,否则有可疑为橙,否则为蓝。

概况正文按正确、错误、可疑三档写,档与档之间空一行。某一档没有归属时写「无」。有归属时只列名单,不附示例解读:

正确:主订单:ORD1、ORD2、ORD3;资金账户:FA1、FA2

错误:主订单:ORD9

可疑:无

归属名称与明细卡标题相同:主订单、资金账户、用户账号、清算批次。同一归属的主键用顿号分开,不同归属用分号分开,主键按字典序。

超长时先从正确名单末尾删主键,再删可疑,最后删错误。删过的名单写成 主订单:ORD1、ORD2…等共200个。概况不拆成多张,也不看 FEISHU_PUSH。推送配置只过滤后面的明细卡。已经 sent 的概况不因这次版式重发;pending 和 failed 下次启动用新正文发。

概况行写在 check_result,card_kind 为 chain_summary,biz_key 是链路代码,没有结论行。已经 sent 的概况不重发。旧轮次还没有概况行时,下次启动补插后再发,不重算明细、不新插结论行。

已有库要先执行下面两句,再启动。不要重跑 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 (
    'master_order', 'fund_account', 'trs_user', 'clearing_batch', 'chain_summary'
  )
);

明细卡标题一行,顺序是结论、链路名称、归属名称、主键值,形如 错误 买入执行 主订单 ORD001202610210004、错误 未归链路 清算批次 2026-10-21。失败重发在末尾加「重发」。标题不写系统和清算步骤。

正文在抽样行之后是营业日、交易日、批次号和「涉及」。「涉及」写这条链路经过的系统和清算步骤。核对门禁写「核对工具自身」,并写已经挂上的清算步骤。未归链路只写「核对工具自身」。配了推送条数时,正文第一行是「抽样明细第 n 条」。其后按章写:摘要、错误清单、可疑清单、无法展开、正确、不适用。摘要只数这张正文里的行。错误行、期望值算得出的可疑行、期望值为空的可疑行(无法展开)、公式或说明里带「不出现 / 不适用 / 未触发」的正确行(不适用),各自写成节点、报表、筛选、期望、实据、依据、结论。同一报表、同一依据的其余正确行收成一块,实据写「n 列无差异」。筛选只用来自被核对对象的报表名和归属,没有报表名时写链路名称。飞书 lark_md 把单个换行显示成空格,章与章、六句与六句之间空一行。期望句仍是 期望 X,当前 Y。符合预期。、不符合预期。 或 无法判定。,写在结论里。期望值算不出时写「期望值无法计算」,结论仍是可疑,放在无法展开。说明只保留回写、口径差异,不重复「同行等式」,接在结论里。

买入执行和卖出执行不用上面的清单,改成四章:每单链路、盘中公式、清算对照、关联报表。没有行的章不写。每单链路只写一句结论。另外三章每个字段是字段、期望、实据、依据、结论,五行之间空一行。正确行的结论只写「正确」。

主题为错误时,正文在计长度之前就只含错误行,摘要加「正确与可疑留在核对明细」,不写可疑、无法展开、正确、不适用四章。买入执行和卖出执行同样只含错误行,这句话写在四章前面。其余主题超过 3500 字时,从正确章末尾整块删掉覆盖块,不适用、错误、可疑和无法展开保留;摘要另写「正确另有 k 列未写入本卡」。订单链路则从盘中公式、清算对照、关联报表的末尾删正确字段,写「另有 k 项未写入本卡」。开头四行不删,并附上 check_result.id 作为记录编号。请求体按飞书 30KB 上限按结论行拆成多张,每张按自己的行重新分章,都带开头四行、同一记录编号和「第几张,共几张」。公式里的制表符会去掉,< > 会改成全角,避免卡片内容被拒。发送失败时日志带上飞书返回的 msg。

同一轮次由唯一键识别:市场、交易日、三端批次 id、三端 end_time、规则版本。规则版本是订单和三端报表规则源文件、列清单 columns.py、取数 reader.py、销售资金步骤 fund_chain.py、链路登记表 chain.py 的 sha256。卡片幂等键在卡片种类后面加链路代码,同一主订单的两条链路是两张卡、两个幂等键。这些文件有一个字节变化,就新开一轮,旧轮次保留。第一次换成这个版本时,已经核对过的营业日会再跑一遍并发卡。命中已有轮次时不重新取数、不新插结论行;缺概况卡时只补插概况卡。notify_status 为 pending 或 failed 的卡片会重发:概况卡不看推送配置,排在前面,概况之间按链路登记顺序;明细卡仍要求结论在推送配置内,按主键升序。成功改为 sent。从 failed 再发时,标题末尾加「重发」。发行的 applied_revision 会记在 check_run 上,销售和 TRS 记 0。修订号不单独构成新的一轮;三端 end_time 或批次 id 没变、规则文件也没变时,再次启动仍走重发。

check_run.create_time 和 check_result 的时间用上海时区的墙钟,不跟容器的 UTC。

业务表不存在(MySQL 1146)时落一张核对门禁错误卡,公式是「业务表应存在」。挂不上链路、主订单方向取不到、链路要求的归属键为空的结论行落未归链路卡,biz_key 是营业日,结论为错误,公式以「结论行没有归属」开头,原因、原结论和原依据写在代入式后面。原因写「取数缺口:报表引用的主订单不在已读取范围」的,是报表行引用的主订单补读后仍没有主单,属于核对工具的取数问题,先查取数范围,不要当成业务数据错误;「主订单方向取不到」才是读到了主单但方向不是买卖。未知列(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。