| .scratch/order-payment-display/H2Probe.java | ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史 | |
| .scratch/order-payment-display/README.md | ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史 | |
| .scratch/order-payment-display/issues/bug-1-merchant-dashboard-total.md | ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史 | |
| .scratch/order-payment-display/issues/bug-2-verification-code-image.md | ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史 |
.scratch/order-payment-display/H2Probe.java
New file @@ -0,0 +1,26 @@ import java.sql.*; /** 一次性只读探针:确认 H2(MySQL 模式) 支持哪些函数,决定方言测试需要补哪些别名。 */ public class H2Probe { public static void main(String[] args) throws Exception { try (Connection c = DriverManager.getConnection("jdbc:h2:mem:probe;MODE=MySQL;DATABASE_TO_LOWER=TRUE", "sa", ""); Statement s = c.createStatement()) { probe(s, "if(1 = 1, 'a', 'b')"); probe(s, "json_extract('{\"a\":1}', '$.a')"); probe(s, "json_unquote('\"a\"')"); probe(s, "lower(replace(cast('\"TRUE\"' as char), '\"', '')) in ('true')"); probe(s, "greatest(3, 0)"); probe(s, "coalesce(null, 1)"); probe(s, "date_sub(now(), interval 20 second)"); } } private static void probe(Statement s, String expression) { try (ResultSet r = s.executeQuery("select " + expression)) { r.next(); System.out.println("OK " + expression + " => " + r.getObject(1)); } catch (SQLException e) { System.out.println("FAIL " + expression + " => " + e.getMessage().split("\n")[0]); } } } .scratch/order-payment-display/README.md
New file @@ -0,0 +1,26 @@ # 商户端/人才端订单支付流程两处显示问题(2026-09-17) 本次会话处理用户报的两个问题,结论、证据与遗留都写在 `issues/`: | 问题 | 结论 | 状态 | | --- | --- | --- | | 商户端首页「交易总额」把未核销、退款中的订单也算进去(像缓存) | 首页卡片 SQL 漏了核销条件;退款中的订单靠「必然未核销」被连带挡掉,不是缓存 | 已修 + 回归测试(`issues/bug-1-merchant-dashboard-total.md`) | | 人才端待使用页核销码图片真机不显示 | 该页动态图片缺 `@error` 兜底(P1-006 同款漏项),已补;图片取不回来的原因还差一份真机抓包 | 兜底已修,根因待证据(`issues/bug-2-verification-code-image.md`) | ## 回归怎么跑的 - `talent-card-server/scripts/run-server-tests.ps1 -Test MerchantSettlementStatisticsServiceTests` —— 新增的首页统计口径测试,先看它红(`expected: 1000L but was: 7000L`)再改 SQL,改后绿。 - 同一条链路的 8 个测试类(商户工作台、手动消费、核销、退款收敛、核销撤销、订单资金边界)一起跑: 只有 `MerchantWorkServiceTests.filtersOrdersByCategoryStatusAndDateWithoutExposingFullMobile` 失败,即 `docs/agents/testing.md` 记的已知基线失败,与本次改动无关。 - 人才端小程序:`E:\HBuilderX\cli.exe launch mp-weixin --project rencaika --compile true` 编译通过。 - 开发库只读复核:按新口径算商户 631 的交易总额由 ¥0.13 变 ¥0.12(那 ¥0.01 是订单 515, 已支付未核销),商户 632 不变(它的三笔退款单已收敛成 `order_status = 2`)。 ## 目录 - `issues/bug-1-merchant-dashboard-total.md`、`issues/bug-2-verification-code-image.md`:现象、根因、证据、遗留。 - `H2Probe.java`:一次性探针,用来确认 H2 2.4.240(MODE=MySQL)没有 `json_extract`、`if()`、`date_sub`, 说明首页统计的回归测试为什么只能写在 MySQL 测试库上。运行: `java -cp <h2.jar> .scratch/order-payment-display/H2Probe.java` .scratch/order-payment-display/issues/bug-1-merchant-dashboard-total.md
New file @@ -0,0 +1,83 @@ # 商户端首页「交易总额」把未核销、退款中的订单也算进去 状态:主缺陷已修(交易总额口径 + 回归测试);另有 3 项遗留待决策,见文末。 ## 现象 1. 人才端下单一笔订单后,商户端首页「交易总额」立刻多出这一笔的金额。 2. 人才端退款后,商户端再打开首页,那笔金额还在,看起来像缓存。 3. 用户口径:交易总额只应统计**核销过**的订单,未核销的不算。 ## 根因 `StatisticsQuerySql.selectDashboardCards` 的 `transactionAmountCents` 只过滤了 `order_status = 1`(已支付),**没有核销条件**: ```sql select sum(o.pay_amount) from benefit_order o where o.deleted = 0 and o.order_status = 1 and o.merchant_id = #{merchantId} ``` - 现象 1 是这条 SQL 的直接结果:已支付未核销的订单也被求和。 - 现象 2 是它的推论,不是缓存:退款受理成功只把订单置为「退款中」 (`OrderFundBoundaryService.markRefundInProgress` → `refund_status = 1`), `order_status` 仍是 1,通道最终结果要等对账任务(最长 10 分钟)或下一次读订单才收敛。 这段时间里订单仍满足 `order_status = 1`,所以一直计入。 读路径收敛只加在人才端读订单/列表与商户端订单列表(`HuifuRefundService.confirmPendingRefunds` / `confirmPendingRefundsOfMerchant`),**商户首页统计的读路径没有收敛**。 因此「清数据 + 重新编译 + 重新登录之后才消失」是时间上的巧合:那几分钟里对账任务已经把这笔退款收敛成 `order_status = 2`,金额随之回落。客户端没有缓存嫌疑(见「排除项」)。 ## 证据 - 开发库 `benefit_order` 实测(2026-09-17):商户 632 的 522/524/525 三笔都是 `order_status = 2, refund_status = 2`,退款从受理到收敛耗时 21 分钟 / 2 分钟 / 1 分钟 (`order_refund.refund_time` 与订单 `created_at` 相差),期间它们都满足 `order_status = 1`。 同商户 526 为 `order_status = 1, verify_status = 1`(真核销),515 为 `order_status = 1, verify_status = 0`(已支付未核销,就是被错算进来的那一类)。 - 同一模块的结算法口径本来就是「已支付且已核销」:`selectPeriodSummaries`、 `AdminWorkbenchStatisticsMapper`、`MerchantSettlementStatisticsController` 的接口描述都用 `order_status = 1 and verify_status = 1`;只有首页卡片这条漏了核销条件。 - 回归测试(新增 `MerchantSettlementStatisticsServiceTests`,真 MySQL + 真 SQL)在修前是红的: `expected: 1000L but was: 7000L` —— 三笔订单(已核销 10 元、未核销 20 元、退款中 40 元)被全额计入。 ## 已改 - `StatisticsQuerySql.selectDashboardCards`:`transactionAmountCents` 增加 `and o.verify_status = 1`。 未核销的订单不再计入,退款中的订单因为必然未核销而被同时挡住,首页在退款受理的那一刻就回落, 不再依赖通道结果收敛。 - `MerchantSettlementStatisticsService.dashboardCards`:`statisticsBasis` 文案由「已支付订单实付金额」 改为「已支付且已核销订单的实付金额」。 - 新增 `talent-card-server/src/test/java/com/talentcard/server/service/MerchantSettlementStatisticsServiceTests.java` 锁住这个口径。 ## 排除项(不是缓存) - 商户端小程序首页每次 `onShow` 都发 `GET /api/merchant/dashboard/summary`,`services/storage.uts` 只存会话与进件草稿,没有缓存统计结果。 - 服务端没有 `@Cacheable`/CacheManager,`selectDashboardCards` 是实时聚合。 - `pages/index/index.uvue` 只在请求失败且已有旧数据时保留旧值(并显示「点击重新加载」小字), 不是持续展示旧值的来源。 ## 遗留(需决策,本轮未动) 1. **补贴总额是同一类缺陷**:`subsidyAmountCents` 的订单部分同样只有 `order_status = 1`, 所以「未核销就算、退款后不回落」对它一样成立。只改交易总额会让两个卡片口径不一致 (一个要求核销、一个不要求)。`GovernmentSubsidySql` 的类注释写的是「工作台按已支付订单计入, 不要求核销」,所以这是有意的还是漏的,需要产品和结算口径确认。 2. **`DashboardSummaryService.summary()` 是同一个错 SQL 的死副本**:`MerchantWorkController` 注入了它 但 `/dashboard/summary` 实际调的是 `MerchantSettlementStatisticsService.dashboardCards`,全仓没有调用方。 谁把它接上去,这个 bug 就会回来。建议删掉或同样加 `verify_status = 1`。 3. **商户首页统计的读路径不做退款收敛**:本轮靠口径修好了症状;如果以后口径又依赖 `order_status`, 缺收敛会让首页比人才端晚最多 10 分钟。见 `HuifuRefundService.confirmPendingRefunds*` 的注释, 那里已写明「读订单/列表之前收敛」的设计意图。 ## 另一件值得记的事:H2 缝跑不了这条 SQL `MerchantSettlementStatisticsMapperTests` 用 H2 内存库,而 `selectDashboardCards` 与 `selectPeriodSummaries` 都内联 `GovernmentSubsidySql.AMOUNT_CENTS`,其中用到 MySQL 的 `json_extract` 与 `if()`。H2 2.4.240(MODE=MySQL)两者都没有,`if()` 更是语法错误、无法用 `create alias` 绕开(探针见 `../H2Probe.java`),所以该测试类在 HEAD 上就红 (`docs/agents/testing.md` 的已知基线失败之一,且不能在这个缝上修好)。 首页统计的回归测试因此写在 MySQL 测试库上,而不是 H2。 .scratch/order-payment-display/issues/bug-2-verification-code-image.md
New file @@ -0,0 +1,64 @@ # 人才端待使用页核销码图片真机不显示 状态:已修一项确定的代码缺陷(失败无兜底)。 **决定(2026-09-17,用户):图片取不回来的原因不再单独排期**——后台域名与服务器随后会切换到 HTTPS 合法域名, `<image>` 的真机取图限制随之消失,届时本问题迎刃而解。下面保留两个候选以便届时复核。 ## 现象 微信开发者工具的手机端真机调试下:人才端下单支付后,进「使用记录」→ 待核销记录 → 订单详情, 核销二维码位置一片空白,没有任何提示,也没有重试入口。 ## 已确定的代码缺陷(已修) `rencaika/pages/pending-use/pending-use.uvue` 的核销码 `<image>` 没有 `@error` 兜底: ```html <image v-if="codeImage != ''" class="code-image" :src="codeImage" mode="aspectFit"></image> <text v-else ...>{{codeError ? '核销码加载失败,点击重试' : '正在生成核销码'}}</text> ``` 图片加载失败时 `codeImage` 仍是非空字符串,`v-if` 依旧成立,`v-else` 的失败文案永远显示不出来 —— 于是页面只剩空白。这正是本项目自己记录过的缺陷类:`rencaika/docs/7.22遗留问题列表.md` 的 **P1-006「远程图片没有加载失败兜底 / 图片失败时会直接留白 / 以 @error 回退」**,当时覆盖了权益专题、 商户列表、商户详情、商品详情,后续 `talent-code.uvue`(人才码)与 `free-benefit.uvue`(免费券码) 也都按此做了 `@error` 回退,**待使用页被漏掉**。 已改(与上述两页同样的做法,`E:\HBuilderX\cli.exe launch mp-weixin --compile true` 编译通过): - 核销码图片加 `@error="handleCodeImageError"`:收回 `codeImage`、置 `codeError`, 让失败文案与「点击重新生成」重试入口显出来(重试入口同时也挂到占位文案上)。 - 同页商品封面图同样加 `@error`,回退到本地占位图(P1-006 对商品图的要求)。 ## 图片取不回来的原因:两个候选,需要一个真机证据 同一个 URL 在模拟器里能显示,说明服务端没问题(两边请求的是同一台后台、同一个 `/api/app/pending-use-orders/verification-codes/{token}/image`)。变量只在真机侧,候选: 1. **http 图片在真机被微信拦掉(最可能)**。开发环境后台是 `http://192.168.110.26:8080` (`rencaika/config/development-server.uts`,本机当前 IP 确认就是 .26),而微信从 2021 年起 在真机上屏蔽 http 图片;开发者工具的「不校验合法域名」对 `wx.request` 有效,对 `<image>` 取图不生效。本项目的发布清单里本来就挂着这件事: `rencaika_merchant/docs/项目接口对接说明.md`「微信真机发布前必须替换为 HTTPS 合法请求域名」。 同页商品封面是 `/uploads/...jpg`(同一台 http 主机),按这个解释它也应该显示不出来。 2. **图片请求 404**:核销码 token 有效期 60 秒、前端每 54 秒重新签发且**签发时会把上一个 token 置为失效** (`PendingUsePortalService.issueCode` 的 `update ... set status = 2 where status = 0`)。 如果某次取图比签发晚超过 54 秒(弱网、渲染层重试),拿到的就是「核销码已失效」。 要定死是哪一个,只需要一份真机证据: - 真机调试下打开该页,看**图片请求 `/api/app/pending-use-orders/verification-codes/*/image` 的状态码**: 4xx(尤其 404)就是候选 2;请求根本没发出去、或报域名/协议类错误就是候选 1; - 或者一张真机截图 + 微信开发者工具「真机调试」控制台里该请求的那一行。 候选 1 的两种修法(都不改口径,可任选): - 按发布清单把后台换成 HTTPS 合法域名 —— 一次性解决全 App 的远程图片(含商品封面、专题图); - 开发期临时绕过:把二维码字节用 `uni.request(..., responseType: 'arraybuffer')` 取回来 (和业务请求同一条通道,真机可用),转 base64 交给 `<image src="data:image/png;base64,...">`。 代价是人才码、免费券码三处都要同步改,且需要一次真机验证。 ## 不能复现的原因 真机取图失败发生在微信客户端/设备侧,仓库里没有可跑的等价环境:模拟器走的是开发者工具的取图通道, 不受同一限制,所以这个缺陷无法用本地循环复现,只能靠上面那份真机证据定位。