feat:流水错误修改

This commit is contained in:
dzc committed 2026-10-22 19:22:16 +08:00
1 parent 3ae6fdbfb4
commit c0285edd4c
29 files changed
+2631 -1111

No files matched your search

@@ -1,5 +1,7 @@
# 部门交易统计优化验证记录(2026-10-22)
> **历史记录:下文验证的是旧订单统计口径的性能优化,已由本次钱包账本统计替代。** 下文的 106 项回归证明与旧口径保持一致,不能作为商户、部门报表已经统一到 `wallet_transaction` 的证据。当前支付与退款均按流水发生时间、历史部门、原始金额和流水条数汇总;生产差额须使用 [只读对账脚本](20261022_wallet_ledger_reconciliation.sql) 按实际租户和权限另行核实。本次未执行生产对账。
## 范围与数据库
目标接口:`POST /hard/hard-total/page-list-dept`,同时优化共用统计逻辑的同步/异步部门导出。用户反馈线上超过 30 秒未返回。线上为 PolarDB MySQL 8 兼容版;本次实际连接的是用户提供的本机 MySQL 8.0.46 测试库,未连接或修改生产数据库。
@@ -0,0 +1,382 @@
-- 钱包账本核对草稿(2026-10-22);只含 SELECT 和会话变量 SET,不建表、不改数据。
-- 本文件未连接任何数据库、未执行生产核对,不能将下列查询当作已有线上结果。
-- 适用 MySQL 8 / PolarDB MySQL 8。三个库须在同一实例且核对账号有 SELECT 权限。
-- 使用前只填写本租户编号。NULL 默认不会匹配任何业务数据,避免不知租户时查全库。
-- 核对的是用户给出的“全权限、未额外筛选商户/商户类型/钱包类型”场景;
-- 若实际账号有行级权限或商户类型限制,应先将相同限制添加到新旧两侧,不能直接拿全租户结果与页面比较。
-- 日期使用中国业务时间;请确保会话中 DATETIME/TIMESTAMP 的解释与应用一致,本脚本不更改时区。
SET @recon_tenant_id = NULL;
SET @recon_from = CAST('2026-09-01 00:00:00' AS DATETIME);
SET @recon_to = CAST('2026-10-01 00:00:00' AS DATETIME);
SET @recon_excluded_dept_1 = 1875013738914156546;
SET @recon_excluded_dept_2 = 1910218102049226753;
SET @recon_excluded_dept_3 = 1910218128624336898;
SET @recon_excluded_dept_4 = 1942046316044144642;
-- 用户报告的原页面结果,单位分;用于比较是否复现,绝不代表本次已验证。
SET @recon_reported_merchant_cents = 429542126;
SET @recon_reported_department_cents = 429394976;
SET @recon_reported_merchant_count = 64361;
SET @recon_reported_department_count = 64133;
SELECT @recon_tenant_id AS selected_tenant,
@recon_from AS inclusive_start, @recon_to AS exclusive_end,
CASE WHEN @recon_tenant_id IS NULL THEN '请先填写租户编号,后续空结果不是零流水结论' ELSE '已指定租户' END AS setup_status,
@recon_reported_merchant_cents - @recon_reported_department_cents AS reported_delta_cents,
@recon_reported_merchant_count - @recon_reported_department_count AS reported_delta_count;
-- 1. 新口径原始账本基准:PAYMENT=4 价格取反;REFUND=5 保留原始符号;零金额也计次。
-- 部门排除保留 dept_id=NULL,这些交易必须进入“未归属部门/非订单未归属”桶。
WITH ledger_base AS (
SELECT ledger.*,
CASE WHEN ledger.biz_id REGEXP '^[0-9]{1,19}$'
THEN CAST(ledger.biz_id AS UNSIGNED) ELSE NULL END AS numeric_biz_id
FROM pjcloudx_wallet.wallet_transaction ledger
WHERE @recon_tenant_id IS NOT NULL
AND ledger.tenant_id = @recon_tenant_id
AND ledger.del_flag = '0'
AND ledger.biz_type IN ('4', '5')
AND ledger.create_time >= @recon_from
AND ledger.create_time < @recon_to
AND (ledger.dept_id IS NULL OR ledger.dept_id NOT IN (@recon_excluded_dept_1, @recon_excluded_dept_2, @recon_excluded_dept_3, @recon_excluded_dept_4))
)
SELECT COALESCE(-SUM(CASE WHEN biz_type = '4' THEN price ELSE 0 END), 0) AS payment_cents,
COUNT(CASE WHEN biz_type = '4' THEN 1 END) AS payment_count,
COALESCE(SUM(CASE WHEN biz_type = '5' THEN price ELSE 0 END), 0) AS refund_cents,
COUNT(CASE WHEN biz_type = '5' THEN 1 END) AS refund_count,
COALESCE(-SUM(price), 0) AS net_cents,
COUNT(CASE WHEN biz_type = '4' THEN 1 END)
- COUNT(CASE WHEN biz_type = '5' THEN 1 END) AS net_count
FROM ledger_base;
-- 2. 同一批账本按真实商户归属汇总,金额和次数仍完全来自账本。
-- 订单/售后只补商户关系,不限制其软删除、状态、支付金额及支付/创建时间。
-- 精确业务编号 + 同租户 + 同钱包,未匹配项显示“非订单/未归属”;不能漏掉这个桶。
WITH ledger_base AS (
SELECT ledger.*,
CASE WHEN ledger.biz_id REGEXP '^[0-9]{1,19}$'
THEN CAST(ledger.biz_id AS UNSIGNED) ELSE NULL END AS numeric_biz_id
FROM pjcloudx_wallet.wallet_transaction ledger
WHERE @recon_tenant_id IS NOT NULL
AND ledger.tenant_id = @recon_tenant_id
AND ledger.del_flag = '0'
AND ledger.biz_type IN ('4', '5')
AND ledger.create_time >= @recon_from
AND ledger.create_time < @recon_to
AND (ledger.dept_id IS NULL OR ledger.dept_id NOT IN (@recon_excluded_dept_1, @recon_excluded_dept_2, @recon_excluded_dept_3, @recon_excluded_dept_4))
),
merchant_ledger AS (
SELECT ledger.*, ledger_order.merchant_id
FROM ledger_base ledger
LEFT JOIN pjcloudx_hardware.hard_after_sale ledger_refund
ON ledger.biz_type = '5'
AND ledger_refund.tenant_id = ledger.tenant_id
AND ledger_refund.id = ledger.numeric_biz_id
AND CAST(ledger.biz_id AS BINARY) = CAST(CONCAT('', ledger_refund.id) AS BINARY)
LEFT JOIN pjcloudx_hardware.hard_order ledger_order
ON ledger_order.tenant_id = ledger.tenant_id
AND ledger_order.wallet_id = ledger.wallet_id
AND ledger_order.id = CASE WHEN ledger.biz_type = '4' THEN ledger.numeric_biz_id
WHEN ledger.biz_type = '5' THEN ledger_refund.order_id ELSE NULL END
AND (ledger.biz_type = '5'
OR CAST(ledger.biz_id AS BINARY) = CAST(CONCAT('', ledger_order.id) AS BINARY))
)
SELECT merchant_id,
CASE WHEN merchant_id IS NULL THEN '非订单/未归属' ELSE CONCAT('商户(', merchant_id, ')') END AS merchant_bucket,
COALESCE(-SUM(CASE WHEN biz_type = '4' THEN price ELSE 0 END), 0) AS payment_cents,
COUNT(CASE WHEN biz_type = '4' THEN 1 END) AS payment_count,
COALESCE(SUM(CASE WHEN biz_type = '5' THEN price ELSE 0 END), 0) AS refund_cents,
COUNT(CASE WHEN biz_type = '5' THEN 1 END) AS refund_count,
COALESCE(-SUM(price), 0) AS net_cents,
COUNT(CASE WHEN biz_type = '4' THEN 1 END)
- COUNT(CASE WHEN biz_type = '5' THEN 1 END) AS net_count
FROM merchant_ledger
GROUP BY merchant_id
ORDER BY merchant_id;
-- 3. 同一批账本按流水历史部门汇总;各组相加应等于第1组、第2组结果。
-- 不与当前部门列表做 INNER JOIN,物理缺失部门及 NULL 部门仍保留。
WITH ledger_base AS (
SELECT ledger.*,
CASE WHEN ledger.biz_id REGEXP '^[0-9]{1,19}$'
THEN CAST(ledger.biz_id AS UNSIGNED) ELSE NULL END AS numeric_biz_id
FROM pjcloudx_wallet.wallet_transaction ledger
WHERE @recon_tenant_id IS NOT NULL
AND ledger.tenant_id = @recon_tenant_id
AND ledger.del_flag = '0'
AND ledger.biz_type IN ('4', '5')
AND ledger.create_time >= @recon_from
AND ledger.create_time < @recon_to
AND (ledger.dept_id IS NULL OR ledger.dept_id NOT IN (@recon_excluded_dept_1, @recon_excluded_dept_2, @recon_excluded_dept_3, @recon_excluded_dept_4))
)
SELECT dept_id,
CASE WHEN dept_id IS NULL THEN '未归属部门' ELSE CONCAT('历史部门(', dept_id, ')') END AS department_bucket,
COALESCE(-SUM(CASE WHEN biz_type = '4' THEN price ELSE 0 END), 0) AS payment_cents,
COUNT(CASE WHEN biz_type = '4' THEN 1 END) AS payment_count,
COALESCE(SUM(CASE WHEN biz_type = '5' THEN price ELSE 0 END), 0) AS refund_cents,
COUNT(CASE WHEN biz_type = '5' THEN 1 END) AS refund_count,
COALESCE(-SUM(price), 0) AS net_cents,
COUNT(CASE WHEN biz_type = '4' THEN 1 END)
- COUNT(CASE WHEN biz_type = '5' THEN 1 END) AS net_count
FROM ledger_base
GROUP BY dept_id
ORDER BY dept_id;
-- 4. 旧两个支付链路的完整集合差异(仅解释旧“流水金额/次数”,不把退款混入支付差额)。
-- 旧商户:订单 pay_time BETWEEN(含右端)、订单 user_dept_id、merchant_id非空、pay_price/每单一次。
-- 旧部门:订单 create_time BETWEEN(含右端)、最新有效 PAYMENT 历史部门(不限流水时间/金额符号),
-- 无快照则回退订单部门;最后仅输出 UPMS 存在的部门。UPMS 软删除部门仍存在,不能过滤del_flag。
-- 该查询故意保留旧 BETWEEN;第1至3组的新账本使用半开区间,两者并非相同口径。
-- 本段模拟现有源码的全权限行为;若部署代码版本不同,先比对源码,不可硬解释为生产差额。
WITH order_basis AS (
SELECT a.id, a.tenant_id, a.wallet_id, a.merchant_id, a.user_dept_id,
a.pay_price, a.pay_time, a.create_time,
COALESCE((
SELECT history.dept_id
FROM pjcloudx_wallet.wallet_transaction history
WHERE history.tenant_id = a.tenant_id
AND history.biz_id = CONCAT('', a.id)
AND CAST(history.biz_id AS BINARY) = CAST(CONCAT('', a.id) AS BINARY)
AND history.wallet_id = a.wallet_id
AND history.biz_type = '4'
AND CAST(history.biz_type AS BINARY) = CAST('4' AS BINARY)
AND history.del_flag = '0'
AND history.dept_id IS NOT NULL
ORDER BY history.id DESC LIMIT 1
), a.user_dept_id) AS historical_dept_id
FROM pjcloudx_hardware.hard_order a
WHERE @recon_tenant_id IS NOT NULL
AND a.tenant_id = @recon_tenant_id AND a.del_flag = '0'
AND a.order_status IN ('5', '20', '10')
AND ((a.pay_time BETWEEN @recon_from AND @recon_to)
OR (a.create_time BETWEEN @recon_from AND @recon_to))
),
old_flags AS (
SELECT a.*,
CASE WHEN a.pay_time BETWEEN @recon_from AND @recon_to THEN 1 ELSE 0 END AS pay_time_inside,
CASE WHEN a.create_time BETWEEN @recon_from AND @recon_to THEN 1 ELSE 0 END AS create_time_inside,
CASE WHEN a.user_dept_id NOT IN (@recon_excluded_dept_1, @recon_excluded_dept_2, @recon_excluded_dept_3, @recon_excluded_dept_4) THEN 1 ELSE 0 END AS order_department_inside,
CASE WHEN a.historical_dept_id NOT IN (@recon_excluded_dept_1, @recon_excluded_dept_2, @recon_excluded_dept_3, @recon_excluded_dept_4) THEN 1 ELSE 0 END AS historical_department_inside,
CASE WHEN EXISTS (
SELECT 1 FROM pjcloudx.sys_dept department
WHERE department.tenant_id = a.tenant_id AND department.dept_id = a.historical_dept_id
) THEN 1 ELSE 0 END AS historical_department_exists,
CASE WHEN a.merchant_id IS NOT NULL
AND a.pay_time BETWEEN @recon_from AND @recon_to
AND a.user_dept_id NOT IN (@recon_excluded_dept_1, @recon_excluded_dept_2, @recon_excluded_dept_3, @recon_excluded_dept_4)
THEN 1 ELSE 0 END AS old_merchant_match,
CASE WHEN a.create_time BETWEEN @recon_from AND @recon_to
AND a.historical_dept_id NOT IN (@recon_excluded_dept_1, @recon_excluded_dept_2, @recon_excluded_dept_3, @recon_excluded_dept_4)
AND EXISTS (
SELECT 1 FROM pjcloudx.sys_dept department
WHERE department.tenant_id = a.tenant_id AND department.dept_id = a.historical_dept_id
)
THEN 1 ELSE 0 END AS old_department_match
FROM order_basis a
)
SELECT COALESCE(SUM(CASE WHEN old_merchant_match = 1 THEN pay_price ELSE 0 END), 0) AS old_merchant_cents,
SUM(old_merchant_match) AS old_merchant_count,
COALESCE(SUM(CASE WHEN old_department_match = 1 THEN pay_price ELSE 0 END), 0) AS old_department_cents,
SUM(old_department_match) AS old_department_count,
COALESCE(SUM(COALESCE(pay_price, 0) * (old_merchant_match - old_department_match)), 0) AS reconstructed_delta_cents,
COALESCE(SUM(old_merchant_match - old_department_match), 0) AS reconstructed_delta_count,
@recon_reported_merchant_cents - @recon_reported_department_cents AS reported_delta_cents,
@recon_reported_merchant_count - @recon_reported_department_count AS reported_delta_count,
CASE WHEN @recon_tenant_id IS NOT NULL
AND COALESCE(SUM(COALESCE(pay_price, 0) * (old_merchant_match - old_department_match)), 0)
= @recon_reported_merchant_cents - @recon_reported_department_cents
AND COALESCE(SUM(old_merchant_match - old_department_match), 0)
= @recon_reported_merchant_count - @recon_reported_department_count
THEN '金额及次数差额同时复现,仍需下组逐笔核对' ELSE '未同时复现,检查租户/权限/部署版本/元数据变化' END AS comparison_status
FROM old_flags;
-- 5. 逐笔列出旧链路集合差:正向仅进商户,负向仅进部门。
-- signed_difference_cents/count 相加应等于第4组重建差额;同时查看各时间、部门、缺失元数据列。
WITH order_basis AS (
SELECT a.id, a.tenant_id, a.wallet_id, a.merchant_id, a.user_dept_id,
a.pay_price, a.pay_time, a.create_time,
COALESCE((
SELECT history.dept_id
FROM pjcloudx_wallet.wallet_transaction history
WHERE history.tenant_id = a.tenant_id
AND history.biz_id = CONCAT('', a.id)
AND CAST(history.biz_id AS BINARY) = CAST(CONCAT('', a.id) AS BINARY)
AND history.wallet_id = a.wallet_id
AND history.biz_type = '4'
AND CAST(history.biz_type AS BINARY) = CAST('4' AS BINARY)
AND history.del_flag = '0'
AND history.dept_id IS NOT NULL
ORDER BY history.id DESC LIMIT 1
), a.user_dept_id) AS historical_dept_id
FROM pjcloudx_hardware.hard_order a
WHERE @recon_tenant_id IS NOT NULL
AND a.tenant_id = @recon_tenant_id AND a.del_flag = '0'
AND a.order_status IN ('5', '20', '10')
AND ((a.pay_time BETWEEN @recon_from AND @recon_to)
OR (a.create_time BETWEEN @recon_from AND @recon_to))
),
old_flags AS (
SELECT a.*,
CASE WHEN a.pay_time BETWEEN @recon_from AND @recon_to THEN 1 ELSE 0 END AS pay_time_inside,
CASE WHEN a.create_time BETWEEN @recon_from AND @recon_to THEN 1 ELSE 0 END AS create_time_inside,
CASE WHEN a.user_dept_id NOT IN (@recon_excluded_dept_1, @recon_excluded_dept_2, @recon_excluded_dept_3, @recon_excluded_dept_4) THEN 1 ELSE 0 END AS order_department_inside,
CASE WHEN a.historical_dept_id NOT IN (@recon_excluded_dept_1, @recon_excluded_dept_2, @recon_excluded_dept_3, @recon_excluded_dept_4) THEN 1 ELSE 0 END AS historical_department_inside,
CASE WHEN EXISTS (
SELECT 1 FROM pjcloudx.sys_dept department
WHERE department.tenant_id = a.tenant_id AND department.dept_id = a.historical_dept_id
) THEN 1 ELSE 0 END AS historical_department_exists,
CASE WHEN a.merchant_id IS NOT NULL
AND a.pay_time BETWEEN @recon_from AND @recon_to
AND a.user_dept_id NOT IN (@recon_excluded_dept_1, @recon_excluded_dept_2, @recon_excluded_dept_3, @recon_excluded_dept_4)
THEN 1 ELSE 0 END AS old_merchant_match,
CASE WHEN a.create_time BETWEEN @recon_from AND @recon_to
AND a.historical_dept_id NOT IN (@recon_excluded_dept_1, @recon_excluded_dept_2, @recon_excluded_dept_3, @recon_excluded_dept_4)
AND EXISTS (
SELECT 1 FROM pjcloudx.sys_dept department
WHERE department.tenant_id = a.tenant_id AND department.dept_id = a.historical_dept_id
)
THEN 1 ELSE 0 END AS old_department_match
FROM order_basis a
)
SELECT id AS order_id, merchant_id, wallet_id,
pay_time AS old_merchant_time, create_time AS old_department_time,
user_dept_id AS old_merchant_department, historical_dept_id AS old_department_department,
pay_price AS order_payment_cents,
old_merchant_match, old_department_match,
CASE WHEN old_merchant_match = 1 THEN '仅旧商户链路' ELSE '仅旧部门链路' END AS difference_direction,
COALESCE(pay_price, 0) * (old_merchant_match - old_department_match) AS signed_difference_cents,
old_merchant_match - old_department_match AS signed_difference_count,
pay_time_inside, create_time_inside, order_department_inside, historical_department_inside,
historical_department_exists
FROM old_flags
WHERE old_merchant_match <> old_department_match
ORDER BY difference_direction, id;
-- 6. 单独隔离“订单支付时间 vs 订单创建时间”差集:其他条件固定为旧商户的订单部门、真实商户及有效订单。
-- 这只是时间因素,不与第7组机械相加,时间和部门因素可能交叠。
WITH order_basis AS (
SELECT a.id, a.tenant_id, a.wallet_id, a.merchant_id, a.user_dept_id,
a.pay_price, a.pay_time, a.create_time,
COALESCE((
SELECT history.dept_id
FROM pjcloudx_wallet.wallet_transaction history
WHERE history.tenant_id = a.tenant_id
AND history.biz_id = CONCAT('', a.id)
AND CAST(history.biz_id AS BINARY) = CAST(CONCAT('', a.id) AS BINARY)
AND history.wallet_id = a.wallet_id
AND history.biz_type = '4'
AND CAST(history.biz_type AS BINARY) = CAST('4' AS BINARY)
AND history.del_flag = '0'
AND history.dept_id IS NOT NULL
ORDER BY history.id DESC LIMIT 1
), a.user_dept_id) AS historical_dept_id
FROM pjcloudx_hardware.hard_order a
WHERE @recon_tenant_id IS NOT NULL
AND a.tenant_id = @recon_tenant_id AND a.del_flag = '0'
AND a.order_status IN ('5', '20', '10')
AND ((a.pay_time BETWEEN @recon_from AND @recon_to)
OR (a.create_time BETWEEN @recon_from AND @recon_to))
),
old_flags AS (
SELECT a.*,
CASE WHEN a.pay_time BETWEEN @recon_from AND @recon_to THEN 1 ELSE 0 END AS pay_time_inside,
CASE WHEN a.create_time BETWEEN @recon_from AND @recon_to THEN 1 ELSE 0 END AS create_time_inside,
CASE WHEN a.user_dept_id NOT IN (@recon_excluded_dept_1, @recon_excluded_dept_2, @recon_excluded_dept_3, @recon_excluded_dept_4) THEN 1 ELSE 0 END AS order_department_inside,
CASE WHEN a.historical_dept_id NOT IN (@recon_excluded_dept_1, @recon_excluded_dept_2, @recon_excluded_dept_3, @recon_excluded_dept_4) THEN 1 ELSE 0 END AS historical_department_inside,
CASE WHEN EXISTS (
SELECT 1 FROM pjcloudx.sys_dept department
WHERE department.tenant_id = a.tenant_id AND department.dept_id = a.historical_dept_id
) THEN 1 ELSE 0 END AS historical_department_exists,
CASE WHEN a.merchant_id IS NOT NULL
AND a.pay_time BETWEEN @recon_from AND @recon_to
AND a.user_dept_id NOT IN (@recon_excluded_dept_1, @recon_excluded_dept_2, @recon_excluded_dept_3, @recon_excluded_dept_4)
THEN 1 ELSE 0 END AS old_merchant_match,
CASE WHEN a.create_time BETWEEN @recon_from AND @recon_to
AND a.historical_dept_id NOT IN (@recon_excluded_dept_1, @recon_excluded_dept_2, @recon_excluded_dept_3, @recon_excluded_dept_4)
AND EXISTS (
SELECT 1 FROM pjcloudx.sys_dept department
WHERE department.tenant_id = a.tenant_id AND department.dept_id = a.historical_dept_id
)
THEN 1 ELSE 0 END AS old_department_match
FROM order_basis a
)
SELECT CASE WHEN pay_time_inside = 1 THEN '仅支付时间命中' ELSE '仅创建时间命中' END AS time_difference,
COUNT(*) AS order_count,
COALESCE(SUM(pay_price), 0) AS order_payment_cents,
COALESCE(SUM(COALESCE(pay_price, 0) * (pay_time_inside - create_time_inside)), 0) AS signed_time_delta_cents,
SUM(pay_time_inside - create_time_inside) AS signed_time_delta_count
FROM old_flags
WHERE merchant_id IS NOT NULL AND order_department_inside = 1
AND pay_time_inside <> create_time_inside
GROUP BY pay_time_inside, create_time_inside
ORDER BY pay_time_inside DESC;
-- 7. 单独隔离“订单部门快照 vs 最新钱包支付历史部门”差集:统一使用订单支付时间、有效订单及真实商户。
-- 对照的是历史快照归属,不以当前用户部门代替;部门物理缺失的影响另看第5组 exists 字段。
-- 本组两方向均可能存在,不能因为总差额为正就只查“测试部门迁出”。
WITH order_basis AS (
SELECT a.id, a.tenant_id, a.wallet_id, a.merchant_id, a.user_dept_id,
a.pay_price, a.pay_time, a.create_time,
COALESCE((
SELECT history.dept_id
FROM pjcloudx_wallet.wallet_transaction history
WHERE history.tenant_id = a.tenant_id
AND history.biz_id = CONCAT('', a.id)
AND CAST(history.biz_id AS BINARY) = CAST(CONCAT('', a.id) AS BINARY)
AND history.wallet_id = a.wallet_id
AND history.biz_type = '4'
AND CAST(history.biz_type AS BINARY) = CAST('4' AS BINARY)
AND history.del_flag = '0'
AND history.dept_id IS NOT NULL
ORDER BY history.id DESC LIMIT 1
), a.user_dept_id) AS historical_dept_id
FROM pjcloudx_hardware.hard_order a
WHERE @recon_tenant_id IS NOT NULL
AND a.tenant_id = @recon_tenant_id AND a.del_flag = '0'
AND a.order_status IN ('5', '20', '10')
AND ((a.pay_time BETWEEN @recon_from AND @recon_to)
OR (a.create_time BETWEEN @recon_from AND @recon_to))
),
old_flags AS (
SELECT a.*,
CASE WHEN a.pay_time BETWEEN @recon_from AND @recon_to THEN 1 ELSE 0 END AS pay_time_inside,
CASE WHEN a.create_time BETWEEN @recon_from AND @recon_to THEN 1 ELSE 0 END AS create_time_inside,
CASE WHEN a.user_dept_id NOT IN (@recon_excluded_dept_1, @recon_excluded_dept_2, @recon_excluded_dept_3, @recon_excluded_dept_4) THEN 1 ELSE 0 END AS order_department_inside,
CASE WHEN a.historical_dept_id NOT IN (@recon_excluded_dept_1, @recon_excluded_dept_2, @recon_excluded_dept_3, @recon_excluded_dept_4) THEN 1 ELSE 0 END AS historical_department_inside,
CASE WHEN EXISTS (
SELECT 1 FROM pjcloudx.sys_dept department
WHERE department.tenant_id = a.tenant_id AND department.dept_id = a.historical_dept_id
) THEN 1 ELSE 0 END AS historical_department_exists,
CASE WHEN a.merchant_id IS NOT NULL
AND a.pay_time BETWEEN @recon_from AND @recon_to
AND a.user_dept_id NOT IN (@recon_excluded_dept_1, @recon_excluded_dept_2, @recon_excluded_dept_3, @recon_excluded_dept_4)
THEN 1 ELSE 0 END AS old_merchant_match,
CASE WHEN a.create_time BETWEEN @recon_from AND @recon_to
AND a.historical_dept_id NOT IN (@recon_excluded_dept_1, @recon_excluded_dept_2, @recon_excluded_dept_3, @recon_excluded_dept_4)
AND EXISTS (
SELECT 1 FROM pjcloudx.sys_dept department
WHERE department.tenant_id = a.tenant_id AND department.dept_id = a.historical_dept_id
)
THEN 1 ELSE 0 END AS old_department_match
FROM order_basis a
)
SELECT id AS order_id, pay_time, wallet_id, merchant_id,
user_dept_id AS order_department, historical_dept_id AS wallet_snapshot_department,
order_department_inside, historical_department_inside,
COALESCE(pay_price, 0) * (order_department_inside - historical_department_inside) AS signed_department_delta_cents,
order_department_inside - historical_department_inside AS signed_department_delta_count
FROM old_flags
WHERE merchant_id IS NOT NULL AND pay_time_inside = 1
AND order_department_inside <> historical_department_inside
ORDER BY signed_department_delta_count DESC, id;
-- 8. 结束时刻探针:旧 BETWEEN 会包含 10月1日00:00:00,新账本半开区间排除它。
-- 这里只展示边界交易,不把结果预设为任何金额或次数。
SELECT ledger.id AS transaction_id, ledger.biz_type, ledger.price AS signed_cents,
ledger.create_time, ledger.dept_id, ledger.wallet_id, ledger.biz_id
FROM pjcloudx_wallet.wallet_transaction ledger
WHERE @recon_tenant_id IS NOT NULL
AND ledger.tenant_id = @recon_tenant_id AND ledger.del_flag = '0'
AND ledger.biz_type IN ('4', '5') AND ledger.create_time = @recon_to
AND (ledger.dept_id IS NULL OR ledger.dept_id NOT IN (@recon_excluded_dept_1, @recon_excluded_dept_2, @recon_excluded_dept_3, @recon_excluded_dept_4))
ORDER BY ledger.id;
@@ -0,0 +1,43 @@
# 钱包账本交易统计验证记录(2026-10-22)
## 结果与口径
商户、部门和商户月份报表的支付、退款均改为 `wallet_transaction` 聚合:PAYMENT=4 的金额取 `-SUM(price)`,REFUND=5 保留原符号,每条流水分别计次,净额和净次数分别相减。所有报表使用流水 `create_time >= start AND create_time < end`,部门包含/排除使用流水 `dept_id`。
订单、售后只用于商户归属及原有数据权限,不再根据订单金额、状态、创建或支付时间决定账本收支。零元但无流水的订单不计入,实际零金额流水与冲正记录保留。全部 REFUND 都保留,非订单退款及缺失关联归入空商户 ID 的“非订单/未归属”行;该行仅全局权限、未限定商户范围时可见。普通历史商户资料缺失、部门物理缺失或空部门不会导致账本漏数。
部门的批量充值/扣减、单次充值/扣减和退还食堂仍保留原计算规则,改在同一账本查询中聚合。其他钱包接口、数据库结构与生产数据均未修改。
## 接口和导出
- 日商户和部门接口仍返回原列表结构;月份接口仍分页,支付与退款先合并聚合后分页,包含只有退款的月份。
- 现有字段名、金额分单位、租户和商户权限保留。权限在分组前执行;客户端指定商户类型必须与服务端类型授权取交集。
- 页面、同步/异步汇总导出共享查询。自然日分片使用相邻半开边界,支付和退款四个原始汇总字段一起累计;抽成在完整合并后计算。
- 月导出从账本月份分组产生商户集合,包含未归属和退款单边分组;一次固定查询/权限上下文,分页读取,金额全程以 BigDecimal 分值累计后转元。
- 单商户明细导出改为逐条钱包流水,包含原始金额、支付/退款金额与关联业务编号。保留五万笔上限,拒绝空或非正商户 ID,每批 200 笔按流水主键游标读取,完整临时文件生成后响应。
- 三个 Panel report 共用商户统计服务,原 createTimeList 映射为账本时间,保留其商户类型和商户筛选。
- 前端日期标签、半开边界、逐流水计次说明同步更新;未归属行禁止结账和单商户导出;月表明确“本页合计”。
## 实际验证
`mvn -o -pl pjcloud-hardware/pjcloud-hardware-biz -am -DskipTests -Dfile.encoding=UTF-8 compile` 实际通过。
联合 Maven test 在 testCompile 阶段被原有 `src/test/java/hard/com/bsd/pjcloud/hard/WechatPayTest.java:39` 的未定义 `log` 阻塞,未修改该无关测试。使用该模块 Maven 已解析的依赖类路径、当前 Maven 编译的主代码,在临时目录以 javac 编译本次定向测试,并通过 JUnit Platform 1.10.5 执行:
- 12 个测试类,共发现 88 项,实际执行 **87 项全部通过,0 失败、0 中止**。
- 1 项原订单统计百万行压测因 `hard.order.statistics.performance` 未开启而跳过,不计为通过。
- 其中新账本 SQL 13 项执行真实 XML、租户和分页插件,验证商户合计、部门合计与独立原始账本基准一致;覆盖半开边界、多流水、正负冲正、零金额、非订单退款、精确大整数关联、软删/缺失资料、钱包类型、权限和跨租户。
- Service 与导出回归覆盖元数据分批查询及失败、半开自然日分片、空商户分组跨片合并、月导出跨 200 分组边界、真实 XLSX 全部金额/次数列和合计、重复导出不污染原统计,以及明细导出跨批及失败不响应残缺文件。
- 现有订单统计 SQL、权限、Service 和 Controller 定向回归同时通过;旧订单查询的通过不作为新账本口径正确性依据。
后端运行日志:`/var/folders/ng/w0f04t956sb_29sq0_pbw63c0000gn/T/pj-wallet-ledger-regression-gyqchw17/test.log`。该路径为临时验证文件,不是发布依赖。
管理端相关 Node 测试 **15/15 通过**,目标 ESLint 检查与四个 Vue SFC 模板编译通过,两仓库变更的 whitespace 检查通过。
## 生产核对边界
SQL 执行正确性使用 H2 的 MySQL 兼容模式;另验证未经替换的 MySQL 源 SQL 能被生产使用的 SQL 解析器解析。未连接生产 MySQL/PolarDB,未执行生产查询计划或新查询的大范围压测,不承诺生产时延。
线上用户报告的旧金额差 147150 分、次数差 228 尚未逐笔归因。使用 [只读对账脚本](20261022_wallet_ledger_reconciliation.sql),指定租户后核对原始账本、新商户/部门分组、旧订单时间/部门快照双向差集。脚本默认租户 NULL,空结果不能解释为零流水。
上线前应以实际 Hardware 数据库账号确认跨库 SELECT 权限,并在真实 PolarDB 上检查时间区间扫描、订单/售后主键关联与耗时;本次不自动执行索引或数据变更。新报表不再调用 Wallet 部门聚合 RPC,无需先发布 Wallet 服务。前端和 Hardware 应配套发布以保持时间标签和实际语义一致。本次尚未发布上线。