交付出件箱

← 返回出件箱

视宜生眼科(单机构)结算补录 —— 执行说明书

dev2 · 2026-09-06 · worktree feature/backfill-shiyisheng-offline(自 origin/test)
本次全部在云机本地假数据库真跑验证,未碰生产写操作;生产部分只做了 sudo db-prod 只读 SELECT 印证。
与 8-21 那次 4 家门店批量补录(backfill_legacy_offline_stores.py)不同:这次是单机构、UPDATE 现有草稿档案,不 INSERT 新档案。


一、脚本与凭证

文件 路径 sha256
补录脚本 settlement/backend/scripts/backfill_shiyisheng_offline_store.py(worktree 内) 275822f220fb4e0b0a2ea1fe51d535d3ba9cfced9b407ee9696ab9dcc7b10d75
付款凭证 /home/dev2/inbox-vouchers/凭证归档/视宜生眼科-付款凭证-2380-20260902.jpg 4eae4f002503f9f1b0e47cd4212e5f44eb86f3d2c4a09758cc3b8b8113d7578c

⚠️ 交付包内容一变,这两个 sha 必须同步改并重发(8-21 那次踩过的坑:脚本改了、说明书里的 sha 没同步改,双重校验形同虚设)。

脚本行数 444 行;已跑 python3 -m py_compile 通过语法检查。


二、这次跟 8-21 批量脚本的关键差异

  1. UPDATE,不 INSERT:目标是现存草稿档案 org_signup_profiles.org_id=10000011(待提交资料 / core_org_id 空),脚本原地把它改成 已开通,绝不再插一行新档案。
  2. 单机构,参数全部写死为常量(TARGET_ORG_ID / CORE_ORG_ID / 两个渠道 UUID / 金额 / 日期 全在文件顶部)。
  3. service_end_at 是长官给的显式日期 2027-09-03 08:13 Asia/Shanghai(对齐核心侧席位到期),不是 paid_at + timedelta 算出来的。SERVICE_DAYS=366 只是文档/展示常量,不参与该字段计算。
  4. 复用 8-21 脚本的四个函数(_money / _paid_at / _next_no / _split_settlement_rate)直接 from scripts.backfill_legacy_offline_stores import ...,不复制一份逻辑;_upload_voucher 因为 org_id 语义不同(这里用已存在的 10000011,不分配新的结算签约编号)单独重写,key 规则原样照抄(store-payment-vouchers/{org_id}/{uuid}-{文件名})。
  5. 不碰 payment_orders(微信小程序在线支付待支付单):该机构从未走过、也不会走微信支付链路,脚本不 import 也不查询这张表。

三、生产只读印证(sudo db-prod 逐条 SELECT,原样贴)

$ sudo db-prod "SELECT org_id, channel_id, status, core_org_id, license_company_name, store_display_name, address, contact_name, contact_phone FROM org_signup_profiles WHERE org_id = 10000011"
  org_id  |              channel_id              |   status   | core_org_id | license_company_name | store_display_name | address | contact_name | contact_phone
----------+--------------------------------------+------------+-------------+----------------------+--------------------+---------+--------------+---------------
 10000011 | e02b3643-0473-49ff-9899-46d44e919d6d | 待提交资料 |             |                      | 视宜生眼科         |         | 张恒         | 13330961935
(1 row)

$ sudo db-prod "SELECT id, name, is_active FROM organizations WHERE id = 22"
 id |            name            | is_active
----+----------------------------+-----------
 22 | 四川视宜生眼科诊所有限公司 | t
(1 row)

$ sudo db-prod "SELECT id, name, rate_percent, channel_level, parent_channel_id, settlement_mode, status FROM channels WHERE id::text LIKE 'e52ac807%' OR id = 'e02b3643-0473-49ff-9899-46d44e919d6d'"
                  id                  |            name            | rate_percent | channel_level |          parent_channel_id           | settlement_mode | status
--------------------------------------+----------------------------+--------------+---------------+--------------------------------------+-----------------+--------
 e02b3643-0473-49ff-9899-46d44e919d6d | 京东方健康-姜怀            |        30.00 |             2 | e52ac807-6436-4f6c-aff1-eb2d3261ca24 | parent_offline  | active
 e52ac807-6436-4f6c-aff1-eb2d3261ca24 | 北京京东方健康科技有限公司 |        50.00 |             1 |                                       | platform_direct | active
(2 rows)

$ sudo db-prod "SELECT count(*) FROM store_service_orders WHERE org_id = 10000011"
 count
-------
     0
(1 row)

$ sudo db-prod "SELECT count(*) FROM payment_vouchers WHERE org_id = 10000011"
 count
-------
     0
(1 row)

$ sudo db-prod "SELECT org_id, status, core_org_id FROM org_signup_profiles WHERE core_org_id = 22"
 org_id | status | core_org_id
--------+--------+-------------
(0 rows)

$ sudo db-prod "SELECT order_no FROM store_service_orders WHERE order_no LIKE 'SVC20260902%' ORDER BY order_no"
(0 rows)

$ sudo db-prod "SELECT payment_no FROM payment_vouchers WHERE payment_no LIKE 'PAY20260902%' ORDER BY payment_no"
(0 rows)

结论:档案行现状、机构、渠道费率、单号空位(生产上 SVC20260902001/PAY20260902001 都是空位,跟脚本里 dry-run 预告一致)、幂等前置条件(无重复 core_org_id=22)全部对得上脚本里写死的常量,未发现需要改脚本的偏差。


四、本地假数据 e2e 真打(云机本地 PG doctor_ai_brain@localhost:5432,全测试数据)

4.1 种子数据

按上面第三节的生产 SELECT 结果,在本地库按同一批 id 插入最小假数据:channels 两条(一级 rate 50 / 二级 rate 30,父子关系一致)、organizations id=22、org_signup_profiles org_id=10000011 草稿行;凭证文件复制到 /tmp/backfill-test-vouchers/。

4.2 dry-run(贴关键输出)

$ BACKFILL_VOUCHER_DIR=/tmp/backfill-test-vouchers python3 -m scripts.backfill_shiyisheng_offline_store --dry-run
【dry-run · 不写库】 视宜生眼科(org_id=10000011):
  档案更新(org_signup_profiles · UPDATE org_id=10000011)
    core_org_id = 22
    ...
    service_end_at = 2027-09-03 08:13:00+08:00
  订单(store_service_orders · INSERT)
    order_no = SVC20260902113   ← 本地库有 112 条无关历史测试数据占用该前缀,脚本自动避让到 113(生产是空位,会落 001)
    ...
  凭证(payment_vouchers · INSERT)
    payment_no = PAY20260902001
    ...
  收益快照(revenue_snapshots · INSERT)
    结算渠道(一级) 北京京东方健康科技有限公司(20.00%) = 476.00
    来源渠道(二级) 京东方健康-姜怀(30.00%) = 714.00
    period_key = 2026-09-H1

(dry-run 已回滚,库里没有任何改动)

⚠️ order_no 落在 113 是因为本地测试库恰好有 112 条无关的历史测试数据占用了 SVC20260902 前缀(第三节生产 SELECT 已确认生产该前缀是空位)——这正好实测验证了 _next_no 的避让逻辑真的在工作,不是巧合对上、也不是 bug。

4.3 commit(贴关键输出 + 5 张表核对)

$ BACKFILL_VOUCHER_DIR=/tmp/backfill-test-vouchers python3 -m scripts.backfill_shiyisheng_offline_store --commit
【真写入】 视宜生眼科(org_id=10000011):
  ...(同上)
✅ 已提交

commit 后 SQL 核对五张表(原样贴):

--- org_signup_profiles ---
{'org_id': 10000011, 'core_org_id': 22, 'license_company_name': '四川视宜生眼科诊所有限公司',
 'store_display_name': '视宜生眼科', 'status': '已开通',
 'submitted_at': 2026-09-02 04:00 UTC(=12:00 上海), 'reviewed_at': 同上, 'activated_at': 同上,
 'activated_seat_count': 1, 'service_start_at': 同上,
 'service_end_at': 2027-09-03 00:13 UTC(=08:13 上海)}

--- store_service_orders ---
{'order_no': 'SVC20260902113', 'org_id': 10000011, 'source_channel_id': e02b3643...,
 'order_type': '首次开通', 'seat_count': 1, 'service_days': 366, 'amount_due': 2380.00,
 'payment_method': '线下转账', 'status': '已完成', 'contract_status': '待补合同',
 'created_terminal': 'backfill'}

--- payment_vouchers ---
{'payment_no': 'PAY20260902001', 'org_id': 10000011, 'payer_name': '四川视宜生眼科诊所有限公司',
 'submitted_amount': 2380.00, 'purchase_seat_count': 1, 'status': '已确认到账',
 'service_order_id': <指向上面订单id>,
 'voucher_file_key': 'store-payment-vouchers/10000011/<uuid>-视宜生眼科-付款凭证-2380-20260902.jpg'}

--- payment_confirmations ---
{'voucher_id': <指向上面凭证id>, 'org_id': 10000011, 'confirmed_amount': 2380.00,
 'confirmed_by': None, 'result': 'confirmed'}

--- revenue_snapshots ---
{'payment_confirmation_id': <指向上面确认id>, 'channel_id': e52ac807...(一级),
 'source_channel_id': e02b3643...(二级), 'org_id': 10000011, 'received_amount': 2380.00,
 'rate_percent_snapshot': 20.00, 'source_rate_percent_snapshot': 30.00,
 'source_channel_amount': 714.00, 'channel_amount': 476.00,
 'period_month': '2026-09', 'period_key': '2026-09-H1', 'settlement_status': 'in_statement',
 'service_order_id': <指向上面订单id>}

linkage ok: True True True True   ← service_order_id / voucher_id / payment_confirmation_id 全部正确串联

物理落盘凭证文件也确认存在:
settlement/backend/data/archive/store-payment-vouchers/10000011/<uuid>-视宜生眼科-付款凭证-2380-20260902.jpg

4.4 幂等验证(第二次 commit)

$ BACKFILL_VOUCHER_DIR=/tmp/backfill-test-vouchers python3 -m scripts.backfill_shiyisheng_offline_store --commit
⏭  已补过,跳过:org_id=10000011
EXIT=0

4.5 清理本地假数据(贴清理后 count=0)

channels 0
organizations 0
org_signup_profiles 0
store_service_orders 0
payment_vouchers 0
payment_confirmations 0
revenue_snapshots 0

(同时删除了本地落盘的凭证文件、临时种子脚本、临时凭证目录,本地库/磁盘均已复原为改动前状态。)


五、单元测试(tests/unit/test_backfill_shiyisheng.py,no-mock 真连本地库)

$ python3 -m pytest tests/unit/test_backfill_shiyisheng.py -v
tests/unit/test_backfill_shiyisheng.py::test_split_settlement_rate_30_50_gives_20_percent_diff PASSED
tests/unit/test_backfill_shiyisheng.py::test_paid_at_is_noon_shanghai_and_service_end_is_explicit_shanghai_time PASSED
tests/unit/test_backfill_shiyisheng.py::test_precheck_rejects_when_core_org_id_already_set PASSED
tests/unit/test_backfill_shiyisheng.py::test_dry_run_does_not_write_any_row PASSED
============================== 4 passed in 0.95s ===============================

四条用例:①分成算式 30/50→差额 20%(714/476)②paid_at 中午上海时区 + service_end_at 显式上海时间点 ③precheck 拒绝 core_org_id 已非空 ④dry-run 真跑本地库确认 5 张表 0 行。每条用例自带 setup/teardown,不留残留。


六、生产执行怎么跑(给 root)

6.1 🔴 这次没有现成的 sudo backfill-prod 通道可直接用

8-21 那条 sudo backfill-prod {status|dry-run|commit|cleanup} 通道是 sha256 锁死了 backfill_legacy_offline_stores.py(4 家批量脚本) 的专用通道(/usr/local/bin/backfill-prod 就是个 SSH 转发,具体校验逻辑在生产主机那一端,我这边看不到、也不该猜)。

这次是新脚本,我判断不出它是否已经/能够被那条通道识别 —— 这件事只能 root 判断,两条路选一条:

无论走哪条,下面的暂存 / 指纹 / dry-run / 批准闸 / commit 步骤都适用:

6.2 暂存与指纹校验

# 1. 脚本 + 凭证放到生产可访问位置后,先双重校验 sha256(本地已算好,见第一节)
sha256sum settlement/backend/scripts/backfill_shiyisheng_offline_store.py
# 期望: 275822f220fb4e0b0a2ea1fe51d535d3ba9cfced9b407ee9696ab9dcc7b10d75

sha256sum "视宜生眼科-付款凭证-2380-20260902.jpg"
# 期望: 4eae4f002503f9f1b0e47cd4212e5f44eb86f3d2c4a09758cc3b8b8113d7578c

# 任一不符 → 立即中止,不要往下跑

6.3 dry-run(生产库,只读,随时可跑)

DATABASE_URL=<生产真实DSN> python3 -m scripts.backfill_shiyisheng_offline_store --dry-run

期望输出(跟第四节本地跑出来的结构完全一致,唯二数字不同):
- order_no = SVC20260902001(生产该前缀是空位,见第三节 SELECT)
- payment_no = PAY20260902001
- 分成 714.00(二级 · 京东方健康-姜怀)/ 476.00(一级 · 北京京东方健康科技有限公司)
- period_key = 2026-09-H1

把这份 dry-run 输出发给长官过目,长官点头才能进下一步。

6.4 批准闸 → commit

DATABASE_URL=<生产真实DSN> python3 -m scripts.backfill_shiyisheng_offline_store --commit
# 若长官指定了具体确认操作人(users.id),加:--confirmed-by <该id>

commit 后照第四节 4.3 的 SQL 模板逐张表核对(换成 sudo db-prod 跑,不是本地库了)。

6.5 --confirmed-by 参数说明

payment_confirmations.confirmed_by 默认 NULL(线下补录没有真实操作人,不伪造成某个管理员,与 8-21 那次先例一致)。如果长官要求落一个具体人(例如 ljg_admin),root 先 sudo db-prod "SELECT id FROM users WHERE username='ljg_admin'" 查到 id,再在 commit 命令后加 --confirmed-by <id>。


七、回滚 SQL(万一需要撤销)

-- 执行前务必先用同样 WHERE 跑一遍 SELECT,确认只命中视宜生眼科这一笔

-- ① 收益快照:确认还没被结算单领走(settlement_statement_id IS NULL)才能删
SELECT id, settlement_statement_id FROM revenue_snapshots WHERE org_id = 10000011;
-- 若 settlement_statement_id 非空 → 停手,联系我人工处理,不要贸然删
DELETE FROM revenue_snapshots WHERE org_id = 10000011 AND settlement_statement_id IS NULL;

-- ② 凭证:驳回、不删除(留痕)
UPDATE payment_vouchers
SET status = '已驳回', reject_reason = '补录误操作回滚', rejected_at = now()
WHERE org_id = 10000011;

-- ③ 订单:取消、不删除(留痕)
UPDATE store_service_orders
SET status = '已取消'
WHERE org_id = 10000011 AND created_terminal = 'backfill';

-- ④ 档案:回退到补录前的草稿态
UPDATE org_signup_profiles
SET core_org_id = NULL,
    status = '待提交资料',
    submitted_at = NULL,
    reviewed_at = NULL,
    activated_at = NULL,
    activated_seat_count = NULL,
    service_start_at = NULL,
    service_end_at = NULL
WHERE org_id = 10000011;

🔴 payment_confirmations 不可逆,本回滚脚本不碰它:该表语义是"这笔钱确认到账了"这一事实本身,不因业务态回退而删除或修改。如果最终判定这笔钱确实没收到,怎么处理这条历史记录必须长官单独拍板,不在本脚本范围内(与项目既有纪律"批B/C/D 硬事实:凭证链退役只退一半/confirmation 不可逆"一致)。


八、不碰的东西


九、拿不准、需要长官/root 确认的点

  1. sudo backfill-prod 通道要不要接入这个新脚本——见第六节 6.1,这个决定权在 root,我这边看不到远端脚本内容,不敢替root下结论。
  2. --confirmed-by 要不要填——默认 NULL,长官若要落具体操作人需额外给一个 users.id。
  3. 四类资质(执照/门头/在职视光师/仪器)——跟 8-21 先例一致,本脚本不写,档案已经是"已开通"态,长官后续可在平台后台「机构签约 → 编辑资料」逐项补传。

十、Git 状态

worktree feature/backfill-shiyisheng-offline(自 origin/test)内已提交(未 push):

chore(settlement): 视宜生眼科线下补录脚本(单机构·UPDATE 现有档案)· 长官 9-06 口径

包含:scripts/backfill_shiyisheng_offline_store.py、tests/unit/__init__.py、tests/unit/test_backfill_shiyisheng.py。

云机环境 · 第三节生产数据为 sudo db-prod 只读实查;第四/五节为本地假数据 e2e 真打,非生产验收证据。