Compare commits

...

414 Commits

Author SHA1 Message Date
萝卜
69e649db87 补齐批次详情页刷新回链测试 2026-03-20 12:55:16 +08:00
萝卜
aaa4ba7d44 补齐批次详情页下钻回链测试 2026-03-20 12:34:34 +08:00
萝卜
a92de6402d 补齐订阅详情页订单回链测试 2026-03-20 12:01:35 +08:00
萝卜
6f94d23f35 补齐套餐详情页治理入口安全回链测试 2026-03-20 11:59:01 +08:00
萝卜
21c86f4b14 补齐平台订单详情页同步订阅回链测试 2026-03-20 11:57:20 +08:00
萝卜
81a9488201 补齐平台订单详情页关联订阅回链测试 2026-03-20 11:31:45 +08:00
萝卜
f9c62afaba 补齐平台订单详情页线索回链测试 2026-03-20 10:59:15 +08:00
萝卜
8e21ab6e46 补齐平台订单详情页查找订阅回链测试 2026-03-20 10:55:47 +08:00
萝卜
7ce509d7f6 补齐平台订单详情页治理入口安全回链测试 2026-03-20 10:49:39 +08:00
萝卜
a0ab815f2e 补齐平台订单详情页商家套餐回链测试 2026-03-20 10:47:07 +08:00
萝卜
959b1d8463 补齐平台订单详情页订阅安全回链测试 2026-03-20 10:45:23 +08:00
萝卜
e0113b2eb6 补齐订阅详情页续费入口安全回链测试 2026-03-20 10:43:21 +08:00
萝卜
95ef295396 补齐订阅详情页平台订单治理回链测试 2026-03-20 10:39:56 +08:00
萝卜
1ecd99f5b4 补齐套餐详情页订阅治理回链测试 2026-03-20 10:37:18 +08:00
萝卜
7c0e3011ed 补齐套餐详情页治理入口回链测试 2026-03-20 10:35:19 +08:00
萝卜
347603c259 补齐订阅详情页续费入口回链测试 2026-03-20 10:13:16 +08:00
萝卜
fafe12a8fa 补齐订阅详情页治理入口回链测试 2026-03-20 10:11:31 +08:00
萝卜
9d68f22214 补齐批次详情页抽样复核空态动作测试 2026-03-20 10:09:26 +08:00
萝卜
c5da67ae10 补齐批次详情页抽样复核末页回链测试 2026-03-20 10:07:26 +08:00
萝卜
cb88f8fc7d 补齐批次详情页抽样复核回到最新护栏 2026-03-20 10:03:50 +08:00
萝卜
15b0ecf094 补齐平台订单详情页治理入口回链测试 2026-03-20 10:01:06 +08:00
萝卜
9cf2e7d8e4 补齐批次详情页访客与空态边界测试 2026-03-20 09:58:37 +08:00
萝卜
9b46699ed7 补齐平台订单详情页商家套餐回链测试 2026-03-20 09:13:56 +08:00
萝卜
294c5f681b 补齐平台订单详情页订阅回链测试 2026-03-20 09:12:17 +08:00
萝卜
24a31e8b96 补齐平台订单详情页线索优先级测试 2026-03-20 09:05:57 +08:00
萝卜
13d0cc9ada 补齐平台订单详情页线索来源安全测试 2026-03-20 09:01:39 +08:00
萝卜
e0931ff55c 补齐平台订单详情页线索提示护栏测试 2026-03-20 08:57:45 +08:00
萝卜
db4d74be7a 补齐平台订单详情页返回列表兜底测试 2026-03-20 08:55:39 +08:00
萝卜
d4653e9527 补齐订阅详情页返回列表兜底测试 2026-03-20 08:53:36 +08:00
萝卜
aab3871814 补齐套餐详情页顶部动作回链测试 2026-03-20 08:51:30 +08:00
萝卜
f7250c485e 补齐套餐详情页返回链接安全护栏测试 2026-03-20 08:49:36 +08:00
萝卜
26d284d3e4 补齐平台批次详情页边界护栏测试 2026-03-20 08:39:08 +08:00
萝卜
d85a3d383a 收口平台订单已付无回执治理提示兼容测试 2026-03-20 08:37:14 +08:00
萝卜
766c9e53bb 收口平台订单页导航与工具区文案兼容测试 2026-03-20 08:07:42 +08:00
萝卜
bd8f62736c 修正平台订单工具区尾部结构与列表锚点 2026-03-20 07:51:54 +08:00
萝卜
7bd40a5527 补齐套餐详情页与订阅无回执治理入口测试 2026-03-20 07:45:29 +08:00
萝卜
50f73d2222 补强摘要导航说明组合语义护栏 2026-03-20 02:31:20 +08:00
萝卜
0a232b96a7 补强摘要导航说明前缀语义标签 2026-03-20 02:13:13 +08:00
萝卜
dd92ab0e6e 补强摘要导航回跳顺序更新护栏 2026-03-20 02:07:24 +08:00
萝卜
21ba7f575a 同步摘要导航完整回跳集合护栏 2026-03-20 01:55:34 +08:00
萝卜
b80c0224cf 补充摘要导航回列表入口 2026-03-20 01:31:24 +08:00
萝卜
c66d299f68 补强摘要导航完整组件更新护栏 2026-03-20 00:47:31 +08:00
萝卜
82b17d913d 补充摘要导航筛选条件入口 2026-03-20 00:31:16 +08:00
萝卜
0f53c6851d 补强摘要导航完整组件护栏 2026-03-20 00:07:19 +08:00
萝卜
d4665138fb 补强摘要导航回跳顺序护栏 2026-03-19 23:59:26 +08:00
萝卜
41458ebd65 补强摘要导航完整回跳集合护栏 2026-03-19 23:51:13 +08:00
萝卜
8b33d5d3bb 补充摘要导航回顶部入口 2026-03-19 23:43:43 +08:00
萝卜
cc2b853e5e 补强摘要导航说明归属护栏 2026-03-19 23:31:21 +08:00
萝卜
7a5be08786 同步摘要导航说明测试口径 2026-03-19 23:19:19 +08:00
萝卜
016080d662 补充摘要导航工具区入口 2026-03-19 23:11:23 +08:00
萝卜
4d456eff7e 修正摘要导航区结构并回归验证 2026-03-19 22:59:24 +08:00
萝卜
ba0ac89132 补强摘要导航完整回跳集合护栏 2026-03-19 22:39:15 +08:00
萝卜
5198b7d6f5 补充摘要导航回摘要区入口 2026-03-19 22:31:24 +08:00
萝卜
e14fcad1ce 补强摘要导航回跳链护栏 2026-03-19 22:15:14 +08:00
萝卜
246b9303c0 补充摘要导航回工具区入口 2026-03-19 22:07:18 +08:00
萝卜
1b4d0addf0 补强摘要导航说明标题护栏 2026-03-19 21:55:27 +08:00
萝卜
462ef5c950 补强摘要导航回筛选归属护栏 2026-03-19 21:27:22 +08:00
萝卜
278f3bfe70 补充摘要导航回筛选入口 2026-03-19 21:07:16 +08:00
萝卜
34d3d53198 补强摘要导航说明归属护栏 2026-03-19 20:51:55 +08:00
萝卜
ce07034886 补强摘要导航说明提示文案 2026-03-19 20:39:20 +08:00
萝卜
3c2b2c80f9 补强摘要导航说明前缀文案 2026-03-19 20:31:21 +08:00
萝卜
fb7012a1e5 补强摘要导航说明语义标签 2026-03-19 20:19:15 +08:00
萝卜
de312919a6 补充摘要导航使用说明 2026-03-19 20:03:08 +08:00
萝卜
0ddef1f6b6 补强摘要导航前缀语义标签 2026-03-19 19:51:12 +08:00
萝卜
ced346c4a1 补强摘要导航按钮提示文案 2026-03-19 19:39:54 +08:00
萝卜
d7804dd755 补充摘要区快速导航前缀文案 2026-03-19 19:31:19 +08:00
萝卜
8de2a97ee5 补强摘要区导航归属护栏 2026-03-19 19:15:10 +08:00
萝卜
d8816869b3 补强摘要区快速导航语义标签 2026-03-19 19:07:09 +08:00
萝卜
b3b9d3dc3c 补充平台订单摘要区快速导航 2026-03-19 18:47:57 +08:00
萝卜
0783548ff4 补齐页内导航完整层护栏测试 2026-03-19 18:39:28 +08:00
萝卜
fb8ce54915 补强页内导航顶层共存护栏 2026-03-19 18:23:04 +08:00
萝卜
4ccf9c28af 补强页内导航说明完整组件护栏 2026-03-19 18:15:17 +08:00
萝卜
85bba5596c 补强页内导航主跳转集合护栏 2026-03-19 18:07:12 +08:00
萝卜
52a9cd130c 补强页内导航完整集合护栏 2026-03-19 17:55:10 +08:00
萝卜
3e58e3e584 收紧页内导航回列表提示语气 2026-03-19 17:39:03 +08:00
萝卜
a264ec5fc9 统一页内导航回顶部提示语气 2026-03-19 17:31:08 +08:00
萝卜
58c2965c7d 收紧页内导航回顶部提示文案 2026-03-19 17:19:06 +08:00
萝卜
3b67065d76 收紧页内导航回列表提示文案 2026-03-19 17:11:06 +08:00
萝卜
4c2f837ddd 收紧页内导航回工具区提示文案 2026-03-19 17:03:06 +08:00
萝卜
224682b144 收紧回摘要入口提示文案 2026-03-19 16:55:05 +08:00
萝卜
1a5c3f0fe9 补强页内导航说明提示文案 2026-03-19 16:47:07 +08:00
萝卜
d8b7887b0f 补强工具锚点组件五要素护栏 2026-03-19 16:35:25 +08:00
萝卜
35e33b8c0e 补强摘要文本链接六要素护栏 2026-03-19 16:27:05 +08:00
萝卜
e6308d2b8d 补强摘要文本链接 title 能力 2026-03-19 16:19:05 +08:00
萝卜
f4f4be3268 补充已付无回执摘要卡直达补回执入口 2026-03-19 16:07:05 +08:00
萝卜
57526577b3 收口页内导航完整组件测试文案 2026-03-19 15:59:59 +08:00
萝卜
73f35d7e31 补强页内导航前缀语义标签 2026-03-19 15:39:05 +08:00
萝卜
fc2c39c5f8 补强工具区说明回导航顺序护栏 2026-03-19 15:31:10 +08:00
萝卜
a2b7e0fc01 补强工具区说明回导航归属护栏 2026-03-19 15:23:02 +08:00
萝卜
6a7a6dac33 补充工具区说明回页内导航入口 2026-03-19 15:15:02 +08:00
萝卜
2ffdc47d08 补强页内导航回顶部提示文案 2026-03-19 15:06:58 +08:00
萝卜
4a54a3e623 补强页内导航快捷筛选提示文案 2026-03-19 14:49:17 +08:00
萝卜
bea9e30d2b 补强页内导航列表入口提示文案 2026-03-19 14:33:57 +08:00
萝卜
9761aabefa 补强页内导航工具入口提示文案 2026-03-19 14:30:55 +08:00
萝卜
fd56ef9123 补强页内导航摘要入口提示文案 2026-03-19 14:23:33 +08:00
萝卜
fe300864dc 补强页内导航筛选入口提示文案 2026-03-19 14:16:57 +08:00
萝卜
0dace50ed3 补强页内导航回顶部顺序护栏 2026-03-19 14:12:55 +08:00
萝卜
6c52a2c3c4 补充页内导航回顶部入口 2026-03-19 14:08:59 +08:00
萝卜
e2c99fb7f5 补强页内导航说明文字顺序护栏 2026-03-19 14:02:57 +08:00
萝卜
24100f1ddd 补强页内导航说明用途提示 2026-03-19 13:54:58 +08:00
萝卜
98a951a41a 补强页内导航完整回跳集合护栏 2026-03-19 13:39:24 +08:00
萝卜
de23a245d6 补强页内回看入口语义标签护栏 2026-03-19 13:31:53 +08:00
萝卜
155e71404e 补强页内导航回看路径顺序护栏 2026-03-19 13:28:55 +08:00
萝卜
23832fe47b 补充页内导航回摘要概览入口 2026-03-19 13:23:01 +08:00
萝卜
0bc82dcf41 补强页内导航回跳目标完整护栏 2026-03-19 13:18:54 +08:00
萝卜
e044d2b25d 补充页内导航回订单列表入口 2026-03-19 13:10:57 +08:00
萝卜
e8744adbdb 补强页内导航回跳顺序护栏 2026-03-19 13:03:20 +08:00
萝卜
1be189f2a9 补强页内导航回跳集合护栏 2026-03-19 12:55:50 +08:00
萝卜
54293147d7 补充页内导航回快捷筛选入口 2026-03-19 12:50:46 +08:00
萝卜
13d836fe57 补强页内导航双回跳结构护栏 2026-03-19 12:45:20 +08:00
萝卜
0bd676992f 补充页内导航回工具区入口 2026-03-19 12:38:58 +08:00
萝卜
3d57b402f7 补强页内导航回顶部归属护栏 2026-03-19 12:32:54 +08:00
萝卜
24fb62d544 补充页内导航回顶部入口 2026-03-19 12:24:53 +08:00
萝卜
3ffd7855fd 补强页内导航说明归属护栏 2026-03-19 12:17:46 +08:00
萝卜
a5537a17ce 补强页内导航说明语义标签 2026-03-19 12:12:53 +08:00
萝卜
595237a9ac 补强平台订单页内导航说明顺序护栏 2026-03-19 12:06:55 +08:00
萝卜
dbc639defe 补充平台订单页内导航使用说明 2026-03-19 12:00:54 +08:00
萝卜
f53128c4f5 补充平台订单页内导航前缀文案 2026-03-19 11:53:53 +08:00
萝卜
566970347f 补强平台订单页内导航语义锚点 2026-03-19 11:50:52 +08:00
萝卜
633e50e303 补充平台订单页内快速跳转导航 2026-03-19 11:30:47 +08:00
萝卜
0da7be44b7 补强已付无回执摘要卡双向导航护栏 2026-03-19 11:21:51 +08:00
萝卜
6cb00c5e47 补强已付无回执摘要卡回看链接护栏 2026-03-19 11:17:49 +08:00
萝卜
8c6342e5d3 补强已付无回执摘要卡完整结构护栏 2026-03-19 11:13:52 +08:00
萝卜
68a3acee7e 补强已付无回执摘要卡语义锚点 2026-03-19 11:09:14 +08:00
萝卜
5234f5d7e4 补强已付无回执摘要说明语义标签 2026-03-19 11:00:55 +08:00
萝卜
7bd33d0c85 修复平台订单快捷筛选重复锚点冲突 2026-03-19 10:55:30 +08:00
萝卜
4f046a1820 补强已付无回执摘要说明顺序护栏 2026-03-19 10:53:44 +08:00
萝卜
f2b74e55e6 补强已付无回执摘要说明归属护栏 2026-03-19 10:45:42 +08:00
萝卜
7f953b4d44 补充已付无回执摘要卡优先级说明 2026-03-19 10:41:51 +08:00
萝卜
e5241f97ba 补强工具区导航双回跳护栏 2026-03-19 10:37:48 +08:00
萝卜
b7cb4aa910 补充工具区导航回摘要入口 2026-03-19 10:31:57 +08:00
萝卜
fc98efa65b 补强工具区导航前缀顺序护栏 2026-03-19 10:27:14 +08:00
萝卜
33cfd7f9c5 补充工具区快速导航前缀文案 2026-03-19 10:18:50 +08:00
萝卜
0888be7672 补充工具区回摘要概览入口 2026-03-19 10:07:10 +08:00
萝卜
abebf57104 补强工具区导航说明组合语义护栏 2026-03-19 09:59:35 +08:00
萝卜
e037795602 补强工具区快速导航说明提示文案 2026-03-19 09:51:41 +08:00
萝卜
65cf173f3d 补强工具区回筛选入口提示文案 2026-03-19 09:48:45 +08:00
萝卜
be40c7540a 补强工具区回筛选说明顺序护栏 2026-03-19 09:41:34 +08:00
萝卜
8bdb70ed07 补强工具区回筛选动作语义标签 2026-03-19 09:38:44 +08:00
萝卜
1946620025 补强工具区导航起始顺序护栏 2026-03-19 09:31:29 +08:00
萝卜
ee2164f0ef 补强工具区导航起点为筛选条件 2026-03-19 09:23:39 +08:00
萝卜
c8fff97f23 补充工具区快速导航筛选条件入口 2026-03-19 09:15:59 +08:00
萝卜
8e5cbdab03 收紧工具区导航完整集测试写法 2026-03-19 09:11:11 +08:00
萝卜
0bab0a329c 补强工具区导航说明顺序护栏 2026-03-19 09:00:43 +08:00
萝卜
9f0ee9fd51 补强工具区快速导航说明语义标签 2026-03-19 08:53:28 +08:00
萝卜
0f84eea12f 补强工具区回筛选入口归属护栏 2026-03-19 08:46:51 +08:00
萝卜
49a56bb6cc 补充工具区快速导航回筛选入口 2026-03-19 08:39:48 +08:00
萝卜
cd8f3c5350 补强工具区导航说明归属护栏 2026-03-19 08:37:02 +08:00
萝卜
caf0cf1420 补充工具区快速导航使用说明 2026-03-19 08:26:34 +08:00
萝卜
44e8bfb178 补强工具区导航头尾闭环护栏 2026-03-19 08:21:27 +08:00
萝卜
fc68964221 补强工具区导航尾部护栏 2026-03-19 08:17:53 +08:00
萝卜
ef237ed2af 补强工具区导航起点护栏 2026-03-19 08:13:29 +08:00
萝卜
d0d6fa0f34 补强工具区顶层导览起始链护栏 2026-03-19 08:10:48 +08:00
萝卜
b93efc9b44 补强工具区导航前置顺序护栏 2026-03-19 07:59:30 +08:00
萝卜
655e7be0ac 补强工具区快速导航顺序护栏 2026-03-19 07:55:55 +08:00
萝卜
dd259da74a 补强工具区导览与导航共存护栏 2026-03-19 07:53:02 +08:00
萝卜
7af59e4d17 补充工具区说明中的快速导航引导 2026-03-19 07:37:38 +08:00
萝卜
3b1ad55ec1 补强工具区快速导航动作语义标签 2026-03-19 07:34:42 +08:00
萝卜
7afaff0641 补强工具区快速导航顺序护栏 2026-03-19 07:23:47 +08:00
萝卜
bde243e03e 补强工具区快速导航归属护栏 2026-03-19 07:19:23 +08:00
萝卜
39337edb1c 补强工具区快速导航结构锚点 2026-03-19 07:09:56 +08:00
萝卜
6c74d37323 补强工具区快速导航语义标签 2026-03-19 07:05:24 +08:00
萝卜
26a3786e82 补充平台订单工具区快速定位导航 2026-03-19 07:02:47 +08:00
萝卜
0ee0379c74 补强平台订单工具区前中段工作流护栏 2026-03-19 06:56:03 +08:00
萝卜
49cbaf7532 补强平台订单工具区前半段顺序护栏 2026-03-19 06:52:41 +08:00
萝卜
6fbb123b56 补强平台订单工具区中后段顺序护栏 2026-03-19 06:46:41 +08:00
萝卜
06e38e79d5 补强平台订单工具区中段工作流护栏 2026-03-19 06:39:21 +08:00
萝卜
22dc9ea3f0 补强平台订单工具区尾部顺序护栏 2026-03-19 06:33:23 +08:00
萝卜
e78a68c38e 补强平台订单工具区导览顺序护栏 2026-03-19 06:30:35 +08:00
萝卜
6a827412d1 补强平台订单工具区整体面板护栏 2026-03-19 06:23:20 +08:00
萝卜
4681556db6 补强平台订单工具区说明顺序护栏 2026-03-19 06:20:50 +08:00
萝卜
09ef2f9754 补强平台订单工具区说明归属护栏 2026-03-19 06:13:16 +08:00
萝卜
c181dbf878 补强平台订单工具区说明语义标签 2026-03-19 06:10:38 +08:00
萝卜
a0f7b4eede 补充平台订单工具区总说明 2026-03-19 06:03:36 +08:00
萝卜
916107e68f 补强平台订单工具区网格归属护栏 2026-03-19 05:56:44 +08:00
萝卜
90c4d2517a 补强平台订单工具区工作流顺序护栏 2026-03-19 05:49:42 +08:00
萝卜
61cb61da6a 补齐平台订单工具分组语义护栏测试 2026-03-19 05:45:53 +08:00
萝卜
164b6e8fee 补强工具锚点组件四要素兼容护栏 2026-03-19 05:39:19 +08:00
萝卜
919f3e298f 补强已付无回执主动作结构锚点 2026-03-19 05:35:26 +08:00
萝卜
1377edb2c0 补强工具锚点组件样式语义兼容护栏 2026-03-19 05:31:10 +08:00
萝卜
a8b8883f90 补强摘要链接组件双属性兼容护栏 2026-03-19 05:27:18 +08:00
萝卜
59954d31c1 补强平台订单动作组件文案兼容护栏 2026-03-19 05:23:12 +08:00
萝卜
ec7795325b 补强平台订单动作组件链接兼容护栏 2026-03-19 05:21:07 +08:00
萝卜
21824a0207 补强平台订单动作组件样式兼容护栏 2026-03-19 05:17:14 +08:00
萝卜
527c59215f 补强平台订单动作组件语义能力护栏 2026-03-19 05:13:23 +08:00
萝卜
6720a8a467 补强已付无回执动作语义标签支持 2026-03-19 05:10:39 +08:00
萝卜
c02c641b3e 补强已付无回执提示文字顺序护栏 2026-03-19 05:05:13 +08:00
萝卜
464f25ad6e 补强已付无回执提示动作顺序护栏 2026-03-19 05:02:33 +08:00
萝卜
4d0a1d0a02 补强已付无回执提示文字层级护栏 2026-03-19 04:55:47 +08:00
萝卜
e6d4adfe47 补强已付无回执提示双向从属护栏 2026-03-19 04:51:43 +08:00
萝卜
fa937fc2d3 补强已付无回执说明从属护栏 2026-03-19 04:43:40 +08:00
萝卜
90aec15333 补强已付无回执说明语义标签 2026-03-19 04:39:50 +08:00
萝卜
ea7c16fe9f 补强已付无回执说明归属护栏 2026-03-19 04:35:36 +08:00
萝卜
cb1f3309cc 补充已付无回执提示优先级说明 2026-03-19 04:31:34 +08:00
萝卜
3ca5a66945 补强已付无回执提示组合语义护栏 2026-03-19 04:25:41 +08:00
萝卜
59488d8232 补强已付无回执提示语义标签 2026-03-19 04:21:12 +08:00
萝卜
aeb0e26976 补强回执筛选说明容器归属护栏 2026-03-19 04:17:19 +08:00
萝卜
5bcb13b41e 收紧回执筛选说明链接样式测试写法 2026-03-19 04:14:49 +08:00
萝卜
bebd91db68 补强回执筛选说明入口语义标签 2026-03-19 04:07:32 +08:00
萝卜
0a94f00064 补强平台订单快捷筛选分组语义标签 2026-03-19 04:03:23 +08:00
萝卜
a0eb9bdfb5 补强平台订单主链组完整护栏 2026-03-19 04:00:34 +08:00
萝卜
ed69d91b53 补强平台订单异常批量组完整护栏 2026-03-19 03:53:40 +08:00
萝卜
f16e3bb5de 补强平台订单收付组完整护栏 2026-03-19 03:49:19 +08:00
萝卜
7d1ebf69e8 补强平台订单收付组退款状态护栏 2026-03-19 03:47:04 +08:00
萝卜
2f6630c318 补强平台订单异常批量分组结构护栏 2026-03-19 03:45:06 +08:00
萝卜
857d2be3a6 补强平台订单收付治理分组结构护栏 2026-03-19 03:43:10 +08:00
萝卜
c80c743013 补强平台订单主链分组结构护栏 2026-03-19 03:41:17 +08:00
萝卜
2365afd741 补充平台订单快捷筛选分组锚点 2026-03-19 03:39:10 +08:00
萝卜
85540ea4a0 补强回执筛选说明业务上下文护栏 2026-03-19 03:37:08 +08:00
萝卜
47f1ee9301 补强回执筛选说明已付无回执自一致护栏 2026-03-19 03:35:10 +08:00
萝卜
3f59a36eb9 补强回执筛选说明收紧已付无回执护栏 2026-03-19 03:33:15 +08:00
萝卜
ab77ed274f 补强回执筛选说明链接回跳护栏 2026-03-19 03:31:11 +08:00
萝卜
a8663c48ab 补强回执筛选说明链接分页护栏 2026-03-19 03:29:15 +08:00
萝卜
d7bb750eef 补强回执筛选说明与快捷入口一致性护栏 2026-03-19 03:23:42 +08:00
萝卜
25908799d2 补充回执筛选说明直达已付无回执入口 2026-03-19 03:19:48 +08:00
萝卜
861e94db4a 补强已付无回执提示标题层级护栏 2026-03-19 03:15:21 +08:00
萝卜
6822e1cf0a 补强已付无回执提示组件层级护栏 2026-03-19 03:13:09 +08:00
萝卜
cd3c54c62d 补强已付无回执治理组件完整护栏 2026-03-19 03:11:04 +08:00
萝卜
a310289872 补强已付无回执提示正文护栏 2026-03-19 03:09:01 +08:00
萝卜
9a9735fcf1 补强已付无回执提示标题动作护栏 2026-03-19 03:07:01 +08:00
萝卜
f837acb630 补强已付无回执提示动作结构护栏 2026-03-19 03:01:41 +08:00
萝卜
37a85c7890 补强已付无回执工具动作可测试锚点 2026-03-19 02:57:35 +08:00
萝卜
337a9ffd27 补强平台订单已付无回执工具提示锚点 2026-03-19 02:55:04 +08:00
萝卜
f040c0d4c3 补强平台订单回执筛选提示可测试锚点 2026-03-19 02:51:07 +08:00
萝卜
cc03b77f65 补强平台订单收款回执治理主链护栏 2026-03-19 02:49:17 +08:00
萝卜
5431503917 修正平台订单收入风险三节点护栏断言方式 2026-03-19 02:45:50 +08:00
萝卜
7d847801bb 补强平台订单失败治理快捷筛选护栏 2026-03-19 02:41:02 +08:00
萝卜
87de9e80ad 补强平台订单收付异常治理衔接护栏 2026-03-19 02:38:55 +08:00
萝卜
ed1a74c39b 补强平台订单同步治理主链护栏 2026-03-19 02:37:02 +08:00
萝卜
d12b3d986b 补强平台订单收费主链快捷筛选护栏 2026-03-19 02:34:59 +08:00
萝卜
084bbbfd8f 补强平台订单24h批量运营三节点护栏 2026-03-19 02:32:59 +08:00
萝卜
901de1de2e 补强平台订单24h批量运营闭环护栏 2026-03-19 02:31:03 +08:00
萝卜
2449140118 补强平台订单24h批量治理成组护栏 2026-03-19 02:28:56 +08:00
萝卜
1fccdfe41c 补强平台订单批量运营快捷筛选成组护栏 2026-03-19 02:26:55 +08:00
萝卜
3c7d3fd66b 补强平台订单退款状态快捷筛选成组护栏 2026-03-19 02:24:59 +08:00
萝卜
f3925f287b 补强平台订单状态快捷筛选成组护栏 2026-03-19 02:23:24 +08:00
萝卜
892c089103 补强平台订单同步快捷筛选成组护栏 2026-03-19 02:21:01 +08:00
萝卜
04b878ed03 补强平台订单BMPA快捷筛选成组护栏 2026-03-19 02:18:50 +08:00
萝卜
c0fd0cdc0b 补强平台订单治理异常快捷筛选成组护栏 2026-03-19 02:16:51 +08:00
萝卜
50765f7a67 补强平台订单退款快捷筛选成组护栏 2026-03-19 02:15:26 +08:00
萝卜
7a1ac998f1 补强平台订单回执快捷筛选成组护栏 2026-03-19 02:13:13 +08:00
萝卜
b18bc016f6 补强平台订单快捷筛选上下文负向护栏 2026-03-19 02:09:39 +08:00
萝卜
95daa179fa 补强平台订单快捷筛选负向护栏 2026-03-19 02:05:34 +08:00
萝卜
3e210c6140 补强平台订单回执筛选结构护栏测试 2026-03-19 01:59:58 +08:00
萝卜
0612224c86 统一仪表盘扫描行已付无回执显示口径 2026-03-19 01:55:49 +08:00
萝卜
ef5f2fe8eb 同步仪表盘最近订单修复测试注释语义 2026-03-19 01:51:38 +08:00
萝卜
69fe88eef5 同步仪表盘最近订单修复测试方法命名 2026-03-19 01:47:34 +08:00
萝卜
17a39664af 同步仪表盘扫描行测试方法命名 2026-03-19 01:45:16 +08:00
萝卜
67e17ee7ae 同步仪表盘最近订单列表测试方法命名 2026-03-19 01:41:31 +08:00
萝卜
1db9c55eca 同步仪表盘最近订单提示测试方法命名 2026-03-19 01:37:41 +08:00
萝卜
c52b213c8f 同步仪表盘扫描行已付无回执测试数据命名 2026-03-19 01:35:26 +08:00
萝卜
6db52ca1fb 同步仪表盘最近订单提示测试数据命名 2026-03-19 01:33:05 +08:00
萝卜
fecfb60c50 同步仪表盘最近订单已付无回执修复测试数据命名 2026-03-19 01:29:31 +08:00
萝卜
d906224eab 同步仪表盘最近订单已付无回执测试数据命名 2026-03-19 01:26:21 +08:00
萝卜
649c3f61ed 同步仪表盘扫描行已付无回执测试语义 2026-03-19 01:20:23 +08:00
萝卜
1e9a193a32 同步平台订单补回执入口测试口径 2026-03-19 01:13:31 +08:00
萝卜
04f3fc2ef4 同步批量同步按钮无回执广义命名语义 2026-03-19 01:07:57 +08:00
萝卜
5b26c27eb8 同步批量仅生效按钮无回执广义测试口径 2026-03-19 01:03:38 +08:00
萝卜
f537080852 同步批量同步按钮无回执广义测试口径 2026-03-19 00:59:48 +08:00
萝卜
2b454a0624 同步导出回执广义测试语义 2026-03-19 00:55:30 +08:00
萝卜
a3e79a8685 补充批量动作回执广义输入注释 2026-03-19 00:53:27 +08:00
萝卜
2af97c6e90 同步批量仅生效回执广义测试语义 2026-03-19 00:51:22 +08:00
萝卜
dd03499b8f 同步回执广义筛选控制器注释口径 2026-03-19 00:49:18 +08:00
萝卜
39208840d7 补强平台订单回执筛选提示位置护栏测试 2026-03-19 00:41:39 +08:00
萝卜
b78b4f0ef2 补充平台订单回执筛选治理提示 2026-03-19 00:37:38 +08:00
萝卜
bab1da0339 同步批量同步无回执广义测试语义 2026-03-19 00:35:28 +08:00
萝卜
dd27d9de26 同步批量仅生效无回执广义测试语义 2026-03-19 00:32:42 +08:00
萝卜
085d3324fd 补强批量仅生效无回执广义提示护栏 2026-03-19 00:23:51 +08:00
萝卜
d5eab40a4b 澄清批量治理无回执广义筛选提示口径 2026-03-19 00:20:38 +08:00
萝卜
6fe8cc916e 同步已付无回执工具区测试注释语义 2026-03-19 00:15:12 +08:00
萝卜
1297619895 同步平台订单已付无回执列表测试口径 2026-03-19 00:11:34 +08:00
萝卜
b875c1ee57 同步已付无回执工具区测试数据命名 2026-03-19 00:09:13 +08:00
萝卜
9fb74bb453 统一平台订单已付无回执提示标题口径 2026-03-19 00:05:40 +08:00
萝卜
75b14447cd 同步仪表盘已付无回执测试注释口径 2026-03-19 00:01:41 +08:00
萝卜
2637d6cef9 补强仪表盘已付无回执治理说明总护栏测试 2026-03-18 23:59:26 +08:00
萝卜
de1be8a01c 同步仪表盘已付无回执快捷区测试口径 2026-03-18 23:55:30 +08:00
萝卜
b68dfd8fdf 统一平台订单已付无回执治理提示文案 2026-03-18 23:51:40 +08:00
萝卜
051a9348ae 统一仪表盘最近订单已付无回执提示文案 2026-03-18 23:49:19 +08:00
萝卜
480cff6e74 同步仪表盘最近订单已付无回执测试口径 2026-03-18 23:45:54 +08:00
萝卜
423f6ba1a5 统一仪表盘最近订单已付无回执前缀文案 2026-03-18 23:39:51 +08:00
萝卜
4226bf641e 同步平台订单快捷筛选已付无回执测试口径 2026-03-18 23:37:13 +08:00
萝卜
b7109b80bd 统一平台订单已付无回执行内告警文案 2026-03-18 23:33:51 +08:00
萝卜
d00609e4e4 澄清平台订单回执筛选广义无回执文案 2026-03-18 23:30:25 +08:00
萝卜
47cae2f3a4 同步仪表盘治理说明已付无回执测试口径 2026-03-18 23:29:40 +08:00
萝卜
0462e0d041 移除平台订单宽口径无回执快捷筛选 2026-03-18 23:22:21 +08:00
萝卜
2ebee7f1ad 统一仪表盘已付无回执帮助说明口径 2026-03-18 23:13:49 +08:00
萝卜
6cb503e09a 统一仪表盘已付无回执可见文案口径 2026-03-18 23:07:42 +08:00
萝卜
23e802ea96 统一总台仪表盘已付无回执辅助文案口径 2026-03-18 23:02:07 +08:00
萝卜
7f8fd95ba2 同步平台订单摘要卡已付无回执总护栏口径 2026-03-18 22:57:48 +08:00
萝卜
cc85bf2912 补强平台订单已付无回执摘要卡上下文护栏测试 2026-03-18 22:55:28 +08:00
萝卜
619f14c252 平台订单摘要卡收紧已付无回执治理口径 2026-03-18 22:52:51 +08:00
萝卜
8b4cac8795 补强订阅详情已付无回执链接回跳护栏测试 2026-03-18 22:31:56 +08:00
萝卜
2d8e89deaf 订阅详情已付无回执链接收紧到收费治理口径 2026-03-18 22:27:48 +08:00
萝卜
24f3bd7e29 补强订阅列表已付无回执快捷筛选口径护栏测试 2026-03-18 22:24:33 +08:00
萝卜
b87a95c29f 订阅列表补充已付无回执快捷筛选 2026-03-18 22:18:26 +08:00
萝卜
463d02cf51 补强套餐页已付无回执入口回跳护栏测试 2026-03-18 22:13:04 +08:00
萝卜
10c2d29f7a 补强站点管理已付无回执入口回跳护栏测试 2026-03-18 22:10:28 +08:00
萝卜
7324e3de33 补强线索页已付无回执入口回跳护栏测试 2026-03-18 22:05:05 +08:00
萝卜
9a3ab559fd 线索页补充已付无回执治理入口 2026-03-18 22:03:28 +08:00
萝卜
aa583e2b72 站点管理补充已付无回执治理入口 2026-03-18 21:59:39 +08:00
萝卜
807b3ae37a 套餐页补充已付无回执治理入口 2026-03-18 21:57:37 +08:00
萝卜
5429bc1f10 补强订阅详情已付无回执入口回跳护栏测试 2026-03-18 21:53:53 +08:00
萝卜
11ee5323ef 订阅详情补充已付无回执治理入口 2026-03-18 21:47:58 +08:00
萝卜
b7d34da73a 补充已付无回执治理入口上下文护栏测试 2026-03-18 21:44:24 +08:00
萝卜
ce2d37428c 补强平台订单治理快捷入口已付无回执护栏测试 2026-03-18 21:39:32 +08:00
萝卜
1750dd17eb 总台平台订单治理入口补充已付无回执快捷链接 2026-03-18 21:36:51 +08:00
萝卜
d953094ecf 测试: 升级清理失败标记阻断提示语义护栏 2026-03-18 20:24:32 +08:00
萝卜
0c3c5abc14 测试: 升级批量生效阻断提示语义护栏 2026-03-18 20:05:48 +08:00
萝卜
4d1b19b295 测试: 升级批量同步阻断提示语义护栏 2026-03-18 20:03:37 +08:00
萝卜
7ba1e2efa8 测试: 升级BMPA阻断提示入口语义护栏 2026-03-18 20:00:22 +08:00
萝卜
c472e57a57 重构: 抽取治理提示锚点按钮组件 2026-03-18 19:53:40 +08:00
萝卜
ba731ac21b 重构: 收口可同步治理提示链接组件 2026-03-18 19:51:17 +08:00
萝卜
bc84f72300 重构: 收口回执缺失提示链接组件 2026-03-18 19:49:29 +08:00
萝卜
79d14c0f41 重构: 收口BMPA治理提示链接组件 2026-03-18 19:45:41 +08:00
萝卜
370a9a58e5 重构: 收口工具治理提示链接组件 2026-03-18 19:41:58 +08:00
萝卜
5d377d16c3 重构: 收口失败原因治理文案链接组件 2026-03-18 19:38:20 +08:00
萝卜
f19f9a38eb 重构: 收口治理摘要单值链接组件 2026-03-18 19:33:17 +08:00
萝卜
b8cd602225 重构: 收口SOP治理文案链接组件 2026-03-18 19:31:22 +08:00
萝卜
5d7dadcb0a 重构: 收口回执摘要单值链接组件 2026-03-18 19:29:25 +08:00
萝卜
524a6c4ddb 重构: 抽取摘要区辅助文案链接组件 2026-03-18 19:27:27 +08:00
萝卜
3417843885 重构: 收口退款摘要卡双值链接组件 2026-03-18 19:25:28 +08:00
萝卜
fc696828d7 重构: 收口BMPA摘要卡双值链接组件 2026-03-18 19:23:08 +08:00
萝卜
49476695f1 重构: 抽取摘要区双值指标链接组件 2026-03-18 19:21:31 +08:00
萝卜
d5807d3623 重构: 抽取摘要区单值指标链接组件 2026-03-18 19:18:46 +08:00
萝卜
842d124c3a 测试: 补齐摘要区总数链接护栏 2026-03-18 19:13:25 +08:00
萝卜
201635229d 测试: 补齐摘要区退款状态链接护栏 2026-03-18 19:11:22 +08:00
萝卜
7b0a63c468 测试: 补齐摘要区退款回执辅助链接护栏 2026-03-18 19:08:19 +08:00
萝卜
7614ada1b6 平台订单摘要状态链接补齐 data-role 与 back 口径护栏 2026-03-18 18:41:25 +08:00
萝卜
eeecfb763e 平台订单摘要批量链接补齐 data-role 与 back 口径护栏 2026-03-18 18:35:24 +08:00
萝卜
e3bec58774 平台订单摘要治理链接补齐 data-role 与 back 口径护栏 2026-03-18 18:29:22 +08:00
萝卜
04e823ec0a 平台订单列表回执退款摘要链接补齐 data-role 与 back 护栏 2026-03-18 18:27:57 +08:00
萝卜
08b61b266c 平台订单列表摘要链接补齐 data-role 与 back 口径护栏 2026-03-18 17:05:16 +08:00
萝卜
afd8302c79 平台订单列表 SOP 链接补齐 data-role 与 back 口径护栏 2026-03-18 17:02:56 +08:00
萝卜
e6f37c9f8f 修正全部快捷筛选 back 转义断言为链接级护栏 2026-03-18 16:53:41 +08:00
萝卜
b3a3ad4b38 补齐部分退款/已退款快捷筛选保留上下文且清理工具开关护栏测试 2026-03-18 16:50:54 +08:00
萝卜
e290baa415 补齐近24小时批量快捷筛选保留上下文且清理工具开关护栏测试 2026-03-18 16:48:47 +08:00
萝卜
47669f1cf8 补齐回执/退款状态快捷筛选保留上下文且清理工具开关护栏测试 2026-03-18 16:46:51 +08:00
萝卜
ee975dc19e 补齐待支付/待生效快捷筛选保留上下文且清理工具开关护栏测试 2026-03-18 16:44:54 +08:00
萝卜
86c0cf532a 补齐 BMPA 成功/失败快捷筛选保留上下文且清理互斥开关护栏测试 2026-03-18 16:42:57 +08:00
萝卜
d308e27189 补齐续费缺订阅快捷筛选保留上下文且清理工具开关护栏测试 2026-03-18 16:39:22 +08:00
萝卜
6171041557 补齐核心治理快捷筛选保留上下文且清理工具开关护栏测试 2026-03-18 16:37:03 +08:00
萝卜
ab9e1411a7 补齐已付无回执快捷筛选保留上下文且清理工具开关护栏测试 2026-03-18 16:34:41 +08:00
萝卜
c5ed4613ad 补齐对账/退款不一致快捷筛选保留上下文且清理工具开关护栏测试 2026-03-18 16:32:48 +08:00
萝卜
6d1353647b 补齐平台订单快捷筛选全部保留 back 且清空筛选护栏测试 2026-03-18 16:26:18 +08:00
萝卜
3d1ebb7a1a 补强快捷筛选不继承工具开关护栏(续费缺订阅) 2026-03-18 16:17:29 +08:00
萝卜
6705875a4c 平台订单快捷筛选全部补齐 data-role 护栏 2026-03-18 16:14:58 +08:00
萝卜
fa6d709962 平台订单快捷筛选近24小时批量集合补齐 data-role 与口径护栏 2026-03-18 16:13:06 +08:00
萝卜
548f947c30 平台订单快捷筛选部分退款/已退款补齐 data-role 与口径护栏 2026-03-18 16:10:47 +08:00
萝卜
fc22ae24b1 平台订单快捷筛选回执/退款状态补齐 data-role 与口径护栏 2026-03-18 16:08:54 +08:00
萝卜
01e9173ae4 平台订单快捷筛选待支付/待生效补齐 data-role 与口径护栏 2026-03-18 16:07:02 +08:00
萝卜
d136f8abe4 平台订单快捷筛选续费缺订阅补齐 data-role 护栏 2026-03-18 16:04:56 +08:00
萝卜
5a5ee2319f 平台订单快捷筛选补齐 BMPA 成功/失败 data-role 与口径护栏 2026-03-18 16:02:50 +08:00
萝卜
13d87748c6 平台订单快捷筛选补齐核心治理入口 data-role 与口径护栏 2026-03-18 16:00:43 +08:00
萝卜
351a60859e 平台订单快捷筛选已付无回执补齐 data-role 护栏 2026-03-18 15:58:49 +08:00
萝卜
147610941c 平台订单快捷筛选补齐对账/退款不一致 data-role 护栏 2026-03-18 15:56:56 +08:00
萝卜
7215b388d6 补齐仪表盘收费工作台入口 back 参数护栏测试 2026-03-18 15:54:38 +08:00
萝卜
6188919445 补齐仪表盘可BMPA处理按钮 data-role 与口径护栏测试 2026-03-18 15:52:35 +08:00
萝卜
0c224db94c 补齐仪表盘可同步/同步失败按钮 data-role 与口径护栏测试 2026-03-18 15:50:54 +08:00
萝卜
7282951796 补齐仪表盘对账/退款不一致按钮 data-role 护栏测试 2026-03-18 15:48:57 +08:00
萝卜
cac5a0f654 补强快捷筛选不继承工具开关护栏(对账/退款不一致) 2026-03-18 15:43:53 +08:00
萝卜
3dec3db75b 补齐对账不一致导出容差配置护栏测试 2026-03-18 15:41:07 +08:00
萝卜
f9bea18ffd 补齐对账不一致筛选容差配置护栏测试 2026-03-18 15:35:21 +08:00
萝卜
888f824206 测试: 仪表盘已付无回执统计不应包含未付 2026-03-18 15:23:30 +08:00
萝卜
b3cb109816 测试: 仪表盘已付无回执按钮data-role护栏 2026-03-18 15:15:08 +08:00
萝卜
f640f75635 界面: 仪表盘无回执入口更名为已付无回执 2026-03-18 15:09:06 +08:00
萝卜
ae42844eb6 测试: 退款不一致筛选口径护栏 2026-03-18 15:03:51 +08:00
萝卜
ac470036fc 界面: 仪表盘已付无回执入口口径注释对齐 2026-03-18 14:57:15 +08:00
萝卜
b79e5bc112 测试: 已付无回执筛选应可正确生效 2026-03-18 14:54:11 +08:00
萝卜
a666c72622 界面: 平台订单快捷筛选新增已付无回执入口 2026-03-18 14:45:10 +08:00
萝卜
7cc3e24846 测试: 进入BMPA失败集合链接清理互斥开关更强场景 2026-03-18 14:42:39 +08:00
萝卜
a1f95e1529 测试: 仪表盘迷你图复用表格链接选择器护栏 2026-03-18 14:39:06 +08:00
萝卜
9d79179c8d 测试: 仪表盘套餐占比迷你图依赖套餐链接护栏 2026-03-18 14:36:49 +08:00
萝卜
ddf5c42d79 测试: BackUrl sanitizeForLinks 安全护栏 2026-03-18 14:27:02 +08:00
萝卜
e06fa9bd9a 界面: 订阅治理入口保留日期范围上下文 2026-03-18 14:25:04 +08:00
萝卜
c2efb2c21f 界面: 进入BMPA失败集合链接清理互斥开关 2026-03-18 14:22:35 +08:00
萝卜
050cd07d18 界面: 进入同步失败集合链接清理互斥开关 2026-03-18 14:21:05 +08:00
萝卜
d1a7ad3369 测试: BackUrl currentPathQuickFilter 口径护栏 2026-03-18 14:15:14 +08:00
萝卜
24e4aaf119 测试: 仪表盘排行迷你图依赖站点链接护栏 2026-03-18 14:09:23 +08:00
萝卜
62e045134f 测试: 订阅治理入口同步失败链接清理BMPA开关护栏 2026-03-18 14:03:20 +08:00
萝卜
54698c10c2 界面: 订阅锁定同步失败入口清理BMPA冲突开关 2026-03-18 14:00:58 +08:00
萝卜
933aabae3b 测试: 仪表盘趋势迷你图依赖日期链接护栏 2026-03-18 13:58:08 +08:00
萝卜
506f5c17ba 测试: 快捷筛选不继承工具型开关护栏 2026-03-18 13:53:14 +08:00
萝卜
7a369d7653 测试: 治理提示区跨集合链接清理冲突开关护栏 2026-03-18 13:51:06 +08:00
萝卜
6ef2403627 界面: 治理提示区跨集合跳转清理冲突开关 2026-03-18 13:41:18 +08:00
萝卜
eed2324c8c 界面: BMPA失败原因提示文案对齐可BMPA处理入口 2026-03-18 13:34:58 +08:00
萝卜
b5e0e58679 界面: 清理同步失败被阻断时跳转链接清理bmpa_failed_only 2026-03-18 13:23:13 +08:00
萝卜
4f7458f187 界面: 清理BMPA失败被阻断时跳转链接清理fail_only 2026-03-18 13:17:13 +08:00
萝卜
4969c49c7a 界面: BMPA失败提示区跳转可处理时清理bmpa_failed_only 2026-03-18 13:11:13 +08:00
萝卜
7ca8f6ec68 界面: 同步失败提示区跳转可同步时清理fail_only 2026-03-18 13:05:28 +08:00
萝卜
5df16e6634 测试: BMPA摘要卡链接不应继承fail_only上下文 2026-03-18 12:55:38 +08:00
萝卜
b0e31eb7f5 ui: BMPA摘要卡链接清理fail_only避免上下文残留 2026-03-18 12:51:28 +08:00
萝卜
c5674d78f0 ui: 可同步订单摘要卡链接清理fail_only避免残留 2026-03-18 12:45:42 +08:00
萝卜
85ecaf5481 test: 同步失败原因重试链接应清除fail_only(从fail_only入口模拟) 2026-03-18 12:39:57 +08:00
萝卜
79903fa639 ui: 同步失败原因重试链接清理fail_only避免口径冲突 2026-03-18 12:33:57 +08:00
萝卜
d91970aed2 test: 行内BMPA失败原因链接默认进入失败集合 2026-03-18 12:31:03 +08:00
萝卜
e17ae3ada3 test: 更新BMPA失败原因TOP链接口径(默认失败集合/重试清理failed) 2026-03-18 12:28:56 +08:00
萝卜
4884faa94f ui: BMPA重试链接清理failed筛选避免口径冲突 2026-03-18 12:22:48 +08:00
萝卜
3704fa928b ui: BMPA原因TOP链接默认落到失败集合 2026-03-18 12:19:31 +08:00
萝卜
6973e5af21 feat: platform order index sync failed reason link prefer batch run 2026-03-18 12:17:45 +08:00
萝卜
83332f265d ops: add oneclick deploy bundle builder 2026-03-18 12:04:09 +08:00
萝卜
8ea5646be5 ops: ensure data repo remote matches configured ssh 2026-03-18 11:51:16 +08:00
萝卜
ee2e75b057 ops: add encrypted db snapshot publish/import scripts 2026-03-18 11:39:50 +08:00
萝卜
8076d5c229 chore: deprecate gitee_push and forward to git_push 2026-03-18 11:19:44 +08:00
萝卜
a74699202d chore: add generic git_push script for origin 2026-03-18 11:15:43 +08:00
萝卜
21e555a628 ui: index receipt/refund count links go to panels 2026-03-18 10:41:56 +08:00
萝卜
9e4a5415ec ui: bmpa error keyword link should imply failed scope 2026-03-18 10:33:24 +08:00
萝卜
bfcf19349b test: BAS spot-check reset keep-back 2026-03-18 10:22:19 +08:00
萝卜
3571c1f405 Platform batch show: guard spot-check reset link keep-back 2026-03-18 10:09:37 +08:00
萝卜
291cf0c8d0 Platform batch show: add spot-check reset link 2026-03-18 10:04:17 +08:00
萝卜
7ac001c1b8 Platform batch show: guard BAS spot-check back includes spot_after_id 2026-03-18 09:53:24 +08:00
萝卜
2423ccf671 Platform batch show: guard spot-check back includes spot_after_id 2026-03-18 09:49:26 +08:00
萝卜
ce12de6593 Platform batch show: add BAS spot-check next link keep-back guardrail 2026-03-18 09:47:15 +08:00
萝卜
0d83d3fef1 Platform batch show: add spot-check next link keep-back guardrail 2026-03-18 09:41:34 +08:00
萝卜
f717316109 Platform batch show: add spot-check back link guardrail test 2026-03-18 09:35:24 +08:00
380 changed files with 16828 additions and 259 deletions

View File

@@ -162,6 +162,64 @@ class PlanController extends Controller
]);
}
public function show(Request $request, Plan $plan): View
{
$this->ensurePlatformAdmin($request);
$plan->loadCount(['subscriptions', 'platformOrders']);
$summaryStats = [
'subscriptions_count' => (int) SiteSubscription::query()->where('plan_id', $plan->id)->count(),
'activated_subscriptions_count' => (int) SiteSubscription::query()->where('plan_id', $plan->id)->where('status', 'activated')->count(),
'expiring_7d_subscriptions_count' => (int) SiteSubscription::query()
->where('plan_id', $plan->id)
->whereNotNull('ends_at')
->whereBetween('ends_at', [now(), now()->copy()->addDays(7)])
->count(),
'platform_orders_count' => (int) PlatformOrder::query()->where('plan_id', $plan->id)->count(),
'paid_orders_count' => (int) PlatformOrder::query()->where('plan_id', $plan->id)->where('payment_status', 'paid')->count(),
'paid_amount_total' => (float) (PlatformOrder::query()->where('plan_id', $plan->id)->where('payment_status', 'paid')->sum('paid_amount') ?? 0),
'paid_no_receipt_orders_count' => (int) PlatformOrder::query()
->where('plan_id', $plan->id)
->where('payment_status', 'paid')
->whereRaw("JSON_EXTRACT(meta, '$.payment_summary.total_amount') IS NULL")
->whereRaw("JSON_EXTRACT(meta, '$.payment_receipts[0].amount') IS NULL")
->count(),
'sync_failed_orders_count' => (int) PlatformOrder::query()
->where('plan_id', $plan->id)
->whereRaw("JSON_EXTRACT(meta, '$.subscription_activation_error.message') IS NOT NULL")
->count(),
'renewal_missing_subscription_orders_count' => (int) PlatformOrder::query()
->where('plan_id', $plan->id)
->where('order_type', 'renewal')
->whereNull('site_subscription_id')
->count(),
];
$recentOrders = PlatformOrder::query()
->with(['merchant', 'siteSubscription'])
->where('plan_id', $plan->id)
->orderByDesc('id')
->limit(10)
->get();
$recentSubscriptions = SiteSubscription::query()
->with('merchant')
->where('plan_id', $plan->id)
->orderByDesc('id')
->limit(10)
->get();
return view('admin.plans.show', [
'plan' => $plan,
'summaryStats' => $summaryStats,
'recentOrders' => $recentOrders,
'recentSubscriptions' => $recentSubscriptions,
'statusLabels' => $this->statusLabels(),
'billingCycleLabels' => $this->billingCycleLabels(),
]);
}
public function store(Request $request): RedirectResponse
{
$this->ensurePlatformAdmin($request);

View File

@@ -284,7 +284,7 @@ class PlatformOrderController extends Controller
'batch_mark_activated_24h' => (string) $request->query('batch_mark_activated_24h', ''),
// 只看“对账不一致”的订单粗版meta.payment_summary.total_amount 与 paid_amount 不一致
'reconcile_mismatch' => (string) $request->query('reconcile_mismatch', ''),
// 支付回执筛选has有回执/none无回执
// 支付回执筛选has有回执/none无回执(广义)
'receipt_status' => trim((string) $request->query('receipt_status', '')),
// 退款轨迹筛选has有退款/none无退款
'refund_status' => trim((string) $request->query('refund_status', '')),
@@ -1367,7 +1367,7 @@ class PlatformOrderController extends Controller
'batch_mark_activated_24h' => (string) $request->query('batch_mark_activated_24h', ''),
// 只看“对账不一致”的订单粗版meta.payment_summary.total_amount 与 paid_amount 不一致
'reconcile_mismatch' => (string) $request->query('reconcile_mismatch', ''),
// 支付回执筛选has有回执/none无回执
// 支付回执筛选has有回执/none无回执(广义)
'receipt_status' => trim((string) $request->query('receipt_status', '')),
// 退款轨迹筛选has有退款/none无退款
'refund_status' => trim((string) $request->query('refund_status', '')),
@@ -1560,6 +1560,7 @@ class PlatformOrderController extends Controller
'batch_mark_paid_and_activate_24h' => (string) $request->input('batch_mark_paid_and_activate_24h', ''),
'batch_mark_activated_24h' => (string) $request->input('batch_mark_activated_24h', ''),
'reconcile_mismatch' => (string) $request->input('reconcile_mismatch', ''),
// receipt_status批量工具输入口径与列表筛选保持一致none 表示“无回执(广义)”
'receipt_status' => trim((string) $request->input('receipt_status', '')),
'refund_status' => trim((string) $request->input('refund_status', '')),
'refund_inconsistent' => (string) $request->input('refund_inconsistent', ''),
@@ -1605,12 +1606,12 @@ class PlatformOrderController extends Controller
return redirect()->back()->with('warning', '当前筛选为「有退款」订单集合。为避免带退款订单直接同步订阅,请先完成退款治理(核对退款回执/修正状态)后再批量同步订阅。');
}
// 防误操作(回执治理优先):当用户显式筛选「无回执」时,禁止直接批量同步
// 防误操作(回执治理优先):当用户显式筛选「无回执(广义)」时,禁止直接批量同步
// 原因:已支付/已生效但无回执证据的订单属于收费闭环缺口,应先补齐回执留痕(可治理、可对账)再同步订阅。
if ($scope === 'filtered'
&& ($filters['syncable_only'] ?? '') === '1'
&& ((string) ($filters['receipt_status'] ?? '') === 'none')) {
return redirect()->back()->with('warning', '当前筛选为「无回执」订单集合。为保证收费闭环可治理,请先补齐支付回执留痕后再批量同步订阅。');
return redirect()->back()->with('warning', '当前筛选为「无回执(广义)」订单集合。为保证收费闭环可治理,请先补齐支付回执留痕后再批量同步订阅。');
}
// 防误操作(口径一致):当用户显式传入了 status/payment_status 时,要求口径至少锁定「已支付+已生效」
@@ -1754,6 +1755,7 @@ class PlatformOrderController extends Controller
'batch_mark_paid_and_activate_24h' => (string) $request->input('batch_mark_paid_and_activate_24h', ''),
'batch_mark_activated_24h' => (string) $request->input('batch_mark_activated_24h', ''),
'reconcile_mismatch' => (string) $request->input('reconcile_mismatch', ''),
// receipt_status批量工具输入口径与列表筛选保持一致none 表示“无回执(广义)”
'receipt_status' => trim((string) $request->input('receipt_status', '')),
'refund_status' => trim((string) $request->input('refund_status', '')),
'refund_inconsistent' => (string) $request->input('refund_inconsistent', ''),
@@ -1906,6 +1908,7 @@ class PlatformOrderController extends Controller
'batch_mark_paid_and_activate_24h' => (string) $request->input('batch_mark_paid_and_activate_24h', ''),
'batch_mark_activated_24h' => (string) $request->input('batch_mark_activated_24h', ''),
'reconcile_mismatch' => (string) $request->input('reconcile_mismatch', ''),
// receipt_status批量工具输入口径与列表筛选保持一致none 表示“无回执(广义)”
'receipt_status' => trim((string) $request->input('receipt_status', '')),
'refund_status' => trim((string) $request->input('refund_status', '')),
'refund_inconsistent' => (string) $request->input('refund_inconsistent', ''),
@@ -1937,9 +1940,9 @@ class PlatformOrderController extends Controller
return redirect()->back()->with('warning', '当前已勾选「只看可同步」:该集合语义为“已生效(activated)+未同步”,与本动作处理的“待处理(pending)”互斥。请先取消只看可同步后再执行。');
}
// 治理优先:当用户显式筛选「无回执」时,不允许直接批量生效
// 治理优先:当用户显式筛选「无回执(广义)」时,不允许直接批量生效
if ((string) ($filters['receipt_status'] ?? '') === 'none') {
return redirect()->back()->with('warning', '当前筛选为「无回执」订单集合。为保证收费闭环可治理,请先补齐支付回执留痕后再执行批量仅标记为已生效。');
return redirect()->back()->with('warning', '当前筛选为「无回执(广义)」订单集合。为保证收费闭环可治理,请先补齐支付回执留痕后再执行批量仅标记为已生效。');
}
// 治理优先:当用户显式筛选「有退款」时,不允许直接批量生效
@@ -2165,6 +2168,7 @@ class PlatformOrderController extends Controller
'batch_mark_paid_and_activate_24h' => (string) $request->input('batch_mark_paid_and_activate_24h', ''),
'batch_mark_activated_24h' => (string) $request->input('batch_mark_activated_24h', ''),
'reconcile_mismatch' => (string) $request->input('reconcile_mismatch', ''),
// receipt_status批量工具输入口径与列表筛选保持一致none 表示“无回执(广义)”
'receipt_status' => trim((string) $request->input('receipt_status', '')),
'refund_status' => trim((string) $request->input('refund_status', '')),
'refund_inconsistent' => (string) $request->input('refund_inconsistent', ''),
@@ -2260,6 +2264,7 @@ class PlatformOrderController extends Controller
'batch_mark_paid_and_activate_24h' => (string) $request->input('batch_mark_paid_and_activate_24h', ''),
'batch_mark_activated_24h' => (string) $request->input('batch_mark_activated_24h', ''),
'reconcile_mismatch' => (string) $request->input('reconcile_mismatch', ''),
// receipt_status批量工具输入口径与列表筛选保持一致none 表示“无回执(广义)”
'receipt_status' => trim((string) $request->input('receipt_status', '')),
'refund_status' => trim((string) $request->input('refund_status', '')),
'refund_inconsistent' => (string) $request->input('refund_inconsistent', ''),
@@ -2534,7 +2539,7 @@ class PlatformOrderController extends Controller
->when(($filters['receipt_status'] ?? '') !== '', function (Builder $builder) use ($filters) {
// 支付回执筛选:
// - has有回执payment_summary.total_amount 存在 或 payment_receipts[0].amount 存在)
// - none无回执两者都不存在
// - none无回执广义payment_summary / payment_receipts 两者都不存在)
$status = (string) ($filters['receipt_status'] ?? '');
if ($status === 'has') {

View File

@@ -72,6 +72,8 @@
'bmpa_failed' => \App\Support\BackUrl::withBack('/admin/platform-orders?bmpa_failed_only=1', $selfWithoutBack),
'bmpa_success' => \App\Support\BackUrl::withBack('/admin/platform-orders?bmpa_success_only=1', $selfWithoutBack),
'paid_no_receipt' => \App\Support\BackUrl::withBack('/admin/platform-orders?payment_status=paid&receipt_status=none', $selfWithoutBack),
// 与平台订单列表快捷筛选“已付无回执”口径一致
'paid_no_receipt_strict' => \App\Support\BackUrl::withBack('/admin/platform-orders?payment_status=paid&receipt_status=none', $selfWithoutBack),
'reconcile_mismatch' => \App\Support\BackUrl::withBack('/admin/platform-orders?reconcile_mismatch=1', $selfWithoutBack),
'refund_inconsistent' => \App\Support\BackUrl::withBack('/admin/platform-orders?refund_inconsistent=1', $selfWithoutBack),
];
@@ -309,7 +311,7 @@
<a class="btn btn-secondary btn-sm" data-role="dashboard-po-quicklink-bmpa-processable" href="{!! $platformOrdersQuickLinks['unpaid_pending'] !!}">可BMPA处理{{ (int) ($stats['platform_orders_unpaid_pending'] ?? 0) }}</a>
<a class="btn btn-secondary btn-sm" data-role="dashboard-po-quicklink-syncable" href="{!! $platformOrdersQuickLinks['syncable_only'] !!}">可同步({{ (int) ($stats['platform_orders_syncable'] ?? 0) }}</a>
<a class="btn btn-secondary btn-sm" data-role="dashboard-po-quicklink-sync-failed" href="{!! $platformOrdersQuickLinks['sync_failed'] !!}">同步失败({{ (int) ($stats['platform_orders_sync_failed'] ?? 0) }}</a>
<a class="btn btn-secondary btn-sm" data-role="dashboard-po-quicklink-no-receipt" href="{!! $platformOrdersQuickLinks['paid_no_receipt'] !!}">无回执({{ (int) ($stats['platform_orders_paid_no_receipt'] ?? 0) }}</a>
<a class="btn btn-secondary btn-sm" data-role="dashboard-po-quicklink-paid-no-receipt" href="{!! $platformOrdersQuickLinks['paid_no_receipt'] !!}">已付无回执({{ (int) ($stats['platform_orders_paid_no_receipt'] ?? 0) }}</a>
<a class="btn btn-secondary btn-sm" data-role="dashboard-po-quicklink-reconcile-mismatch" href="{!! $platformOrdersQuickLinks['reconcile_mismatch'] !!}">对账不一致({{ (int) ($stats['platform_orders_reconcile_mismatch'] ?? 0) }}</a>
<a class="btn btn-secondary btn-sm" data-role="dashboard-po-quicklink-refund-inconsistent" href="{!! $platformOrdersQuickLinks['refund_inconsistent'] !!}">退款不一致({{ (int) ($stats['platform_orders_refund_inconsistent'] ?? 0) }}</a>
</div>
@@ -404,7 +406,7 @@
'items' => [
'同步失败=meta.subscription_activation_error.message 存在',
'BMPA失败=meta.batch_mark_paid_and_activate_error.message 存在',
'无回执=已支付但缺 payment_receipts',
'已付无回执=已支付但缺 payment_receipts',
'对账不一致=回执汇总金额与 paid_amount 不一致',
'退款不一致=退款汇总与退款状态不一致',
'续费缺订阅=renewal 但 site_subscription_id 为空',
@@ -440,8 +442,8 @@
'rowRole' => 'dashboard-po-no-receipt-row',
'barRole' => 'dashboard-po-no-receipt-bar',
'href' => $platformOrdersQuickLinks['paid_no_receipt'],
'ariaLabel' => '进入无回执订单集合',
'label' => '无回执',
'ariaLabel' => '进入已付无回执订单集合',
'label' => '已付无回执',
'pct' => $poNoReceiptPct,
'title' => $poNoReceipt . ' / ' . $poTotal . '' . $poNoReceiptPct . '%',
'value' => $poNoReceiptPct . '%' . $poNoReceipt . '',
@@ -755,8 +757,8 @@
'rowRole' => 'ops-risk-no-receipt-row',
'barRole' => 'ops-risk-no-receipt-bar',
'href' => $platformOrdersQuickLinks['paid_no_receipt'],
'ariaLabel' => '进入无回执订单集合',
'label' => '无回执',
'ariaLabel' => '进入已付无回执订单集合',
'label' => '已付无回执',
'pct' => $pctRiskNoReceipt,
'title' => $riskNoReceipt . ' / ' . $poTotalForOps . '' . $pctRiskNoReceipt . '%',
'value' => $pctRiskNoReceipt . '%' . $riskNoReceipt . '',
@@ -998,7 +1000,7 @@
// partially_refunded 属于“已支付但发生退款”的治理集合:扫描行应展示退款轨迹(避免显示 "-" 造成误判)。
$isPartiallyRefunded = ($paymentStatus === 'partially_refunded');
$receiptStatusText = $isPaid ? ($hasReceiptEvidence ? '有' : '') : '-';
$receiptStatusText = $isPaid ? ($hasReceiptEvidence ? '有' : '已付无回执') : '-';
$reconcileStatusText = ($isPaid && $hasReceiptEvidence)
? ($po->isReconcileMismatch() ? '不一致' : '一致')
: '-';
@@ -1133,8 +1135,8 @@
<td>
{{ $po->payment_status }}
@if((string) $po->payment_status === 'paid' && ! $hasReceiptEvidence)
<div class="muted text-danger muted-xs row-warn" data-role="recent-order-no-receipt-hint" title="已付 ¥{{ number_format((float) $po->paid_amount, 2) }}|无回执证据">
<span class="row-warn-prefix">无回执</span>
<div class="muted text-danger muted-xs row-warn" data-role="recent-order-no-receipt-hint" title="已付 ¥{{ number_format((float) $po->paid_amount, 2) }}已付无回执证据">
<span class="row-warn-prefix">已付无回执</span>
<span class="muted"></span>
<a class="link" href="{!! $fixReceiptUrl !!}">去补回执</a>
<span class="muted"></span>

View File

@@ -98,6 +98,7 @@
<a class="muted" href="{!! $makeSubscriptionsUrl((int) $merchant->id) !!}">订阅</a>
<a class="muted" href="{!! $makePlatformOrdersUrl((int) $merchant->id) !!}">平台订单</a>
<a class="muted" href="{!! $makePlatformOrdersUrl((int) $merchant->id, ['renewal_missing_subscription' => '1']) !!}">续费缺订阅</a>
<a class="muted" href="{!! $makePlatformOrdersUrl((int) $merchant->id, ['payment_status' => 'paid', 'receipt_status' => 'none']) !!}">已付无回执</a>
</div>
<div class="muted muted-xs">当前阶段请使用该站点管理员账号登录</div>
</td>

View File

@@ -257,6 +257,7 @@
</td>
<td>
@php
$showPlanUrl = \App\Support\BackUrl::withBack('/admin/plans/' . $plan->id, $selfWithoutBack);
$editPlanUrl = \App\Support\BackUrl::withBack('/admin/plans/' . $plan->id . '/edit', $selfWithoutBack);
$createOrderUrl = \App\Support\BackUrl::withBack('/admin/platform-orders/create?' . \Illuminate\Support\Arr::query([
'plan_id' => $plan->id,
@@ -268,8 +269,14 @@
'plan_id' => $plan->id,
'renewal_missing_subscription' => '1',
]);
$paidNoReceiptUrl = $makePlatformOrderUrl([
'plan_id' => $plan->id,
'payment_status' => 'paid',
'receipt_status' => 'none',
]);
@endphp
<div class="actions gap-10">
<a href="{!! $showPlanUrl !!}" class="btn btn-secondary btn-sm">查看详情</a>
<a href="{!! $editPlanUrl !!}" class="btn btn-secondary btn-sm">编辑</a>
@if((string) ($plan->status ?? '') === 'active')
<a href="{!! $createOrderUrl !!}" class="btn btn-sm">创建订单</a>
@@ -277,6 +284,7 @@
<span class="muted muted-xs">未启用:不建议下单</span>
@endif
<a href="{!! $renewalMissingSubscriptionUrl !!}" class="btn btn-secondary btn-sm">续费缺订阅</a>
<a href="{!! $paidNoReceiptUrl !!}" class="btn btn-secondary btn-sm">已付无回执</a>
</div>
<form method="post" action="/admin/plans/{{ $plan->id }}/set-status" class="mt-6 actions gap-10" data-action="disable-on-submit">

View File

@@ -0,0 +1,215 @@
@extends('admin.layouts.app')
@section('title', '套餐详情')
@section('page_title', '套餐详情')
@section('content')
@php
$planShowSelf = \App\Support\BackUrl::selfWithoutBack();
$incomingBack = (string) request()->query('back', '');
$safeBackForLinks = \App\Support\BackUrl::sanitizeForLinks($incomingBack);
$makeSubscriptionUrl = function (array $query) use ($planShowSelf) {
return \App\Support\BackUrl::withBack('/admin/site-subscriptions?' . \Illuminate\Support\Arr::query($query), $planShowSelf);
};
$makePlatformOrderUrl = function (array $query) use ($planShowSelf) {
return \App\Support\BackUrl::withBack('/admin/platform-orders?' . \Illuminate\Support\Arr::query($query), $planShowSelf);
};
$editPlanUrl = \App\Support\BackUrl::withBack('/admin/plans/' . $plan->id . '/edit', $planShowSelf);
$createOrderUrl = \App\Support\BackUrl::withBack('/admin/platform-orders/create?' . \Illuminate\Support\Arr::query([
'plan_id' => $plan->id,
'order_type' => 'new_purchase',
]), $planShowSelf);
@endphp
<div class="page-header mb-20" data-page="admin.plans.show">
<div class="page-header-main">
<div>
<div class="page-header-title">套餐详情</div>
<div class="page-header-subtitle">这里用于总台运营查看套餐主数据、关联订阅、关联平台订单,以及当前需要优先治理的收费链路问题。</div>
</div>
<div class="page-header-actions">
@if($safeBackForLinks !== '')
<a href="{!! $safeBackForLinks !!}" class="btn btn-secondary btn-sm">返回上一页(保留上下文)</a>
@else
<a href="/admin/plans" class="btn btn-secondary btn-sm">返回套餐列表</a>
@endif
<a href="{!! $editPlanUrl !!}" class="btn btn-secondary btn-sm">编辑套餐</a>
@if((string) ($plan->status ?? '') === 'active')
<a href="{!! $createOrderUrl !!}" class="btn btn-sm">创建订单</a>
@endif
</div>
</div>
<div class="page-header-meta">
<div>套餐名称:<strong>{{ $plan->name }}</strong></div>
<div>编码:{{ $plan->code }}</div>
<div>状态:{{ $statusLabels[$plan->status] ?? $plan->status }}{{ $plan->status }}</div>
<div>计费周期:{{ $billingCycleLabels[$plan->billing_cycle] ?? $plan->billing_cycle }}</div>
</div>
</div>
<div class="grid-4 mb-20">
<div class="card">
<h3>关联订阅总量</h3>
<div class="num-md">
<a class="link" href="{!! $makeSubscriptionUrl(['plan_id' => $plan->id]) !!}">{{ $summaryStats['subscriptions_count'] ?? 0 }}</a>
</div>
</div>
<div class="card">
<h3>已生效订阅</h3>
<div class="num-md">
<a class="link" href="{!! $makeSubscriptionUrl(['plan_id' => $plan->id, 'status' => 'activated']) !!}">{{ $summaryStats['activated_subscriptions_count'] ?? 0 }}</a>
</div>
</div>
<div class="card">
<h3>7天内到期订阅</h3>
<div class="num-md">
<a class="link" href="{!! $makeSubscriptionUrl(['plan_id' => $plan->id, 'expiry' => 'expiring_7d']) !!}">{{ $summaryStats['expiring_7d_subscriptions_count'] ?? 0 }}</a>
</div>
</div>
<div class="card">
<h3>关联平台订单</h3>
<div class="num-md">
<a class="link" href="{!! $makePlatformOrderUrl(['plan_id' => $plan->id]) !!}">{{ $summaryStats['platform_orders_count'] ?? 0 }}</a>
</div>
</div>
<div class="card">
<h3>已支付订单</h3>
<div class="num-md">
<a class="link" href="{!! $makePlatformOrderUrl(['plan_id' => $plan->id, 'payment_status' => 'paid']) !!}">{{ $summaryStats['paid_orders_count'] ?? 0 }}</a>
</div>
<div class="muted muted-xs mt-6">已付总额:¥{{ number_format((float) ($summaryStats['paid_amount_total'] ?? 0), 2) }}</div>
</div>
<div class="card">
<h3>已付无回执</h3>
<div class="num-md">
<a class="link" href="{!! $makePlatformOrderUrl(['plan_id' => $plan->id, 'payment_status' => 'paid', 'receipt_status' => 'none']) !!}">{{ $summaryStats['paid_no_receipt_orders_count'] ?? 0 }}</a>
</div>
</div>
<div class="card">
<h3>同步失败订单</h3>
<div class="num-md">
<a class="link" href="{!! $makePlatformOrderUrl(['plan_id' => $plan->id, 'sync_status' => 'failed']) !!}">{{ $summaryStats['sync_failed_orders_count'] ?? 0 }}</a>
</div>
</div>
<div class="card">
<h3>续费缺订阅</h3>
<div class="num-md">
<a class="link" href="{!! $makePlatformOrderUrl(['plan_id' => $plan->id, 'renewal_missing_subscription' => '1']) !!}">{{ $summaryStats['renewal_missing_subscription_orders_count'] ?? 0 }}</a>
</div>
</div>
</div>
<div class="card mb-20">
<h3>套餐信息</h3>
<table>
<tbody>
<tr><th class="w-160">ID</th><td>{{ $plan->id }}</td></tr>
<tr><th>套餐名称</th><td>{{ $plan->name }}</td></tr>
<tr><th>编码</th><td>{{ $plan->code }}</td></tr>
<tr><th>状态</th><td>{{ $statusLabels[$plan->status] ?? $plan->status }} <span class="muted">({{ $plan->status }})</span></td></tr>
<tr><th>计费周期</th><td>{{ $billingCycleLabels[$plan->billing_cycle] ?? $plan->billing_cycle }}</td></tr>
<tr><th>售价 / 划线价</th><td>¥{{ number_format((float) $plan->price, 2) }} / ¥{{ number_format((float) $plan->list_price, 2) }}</td></tr>
<tr><th>发布时间</th><td>{{ optional($plan->published_at)->format('Y-m-d H:i:s') ?: '-' }}</td></tr>
<tr><th>描述</th><td>{{ $plan->description ?: '暂无说明' }}</td></tr>
</tbody>
</table>
</div>
<div class="card mb-20">
<h3>治理入口</h3>
<div class="actions gap-10">
<a class="btn btn-secondary btn-sm" href="{!! $makeSubscriptionUrl(['plan_id' => $plan->id]) !!}">查看关联订阅</a>
<a class="btn btn-secondary btn-sm" href="{!! $makeSubscriptionUrl(['plan_id' => $plan->id, 'expiry' => 'expiring_7d']) !!}">查看 7 天内到期订阅</a>
<a class="btn btn-secondary btn-sm" href="{!! $makePlatformOrderUrl(['plan_id' => $plan->id, 'payment_status' => 'paid', 'receipt_status' => 'none']) !!}">查看已付无回执订单</a>
<a class="btn btn-secondary btn-sm" href="{!! $makePlatformOrderUrl(['plan_id' => $plan->id, 'renewal_missing_subscription' => '1']) !!}">查看续费缺订阅订单</a>
</div>
</div>
<div class="card mb-20 list-card">
<div class="list-card-header">
<div>
<h3 class="list-card-title">最近平台订单</h3>
<p class="muted muted-xs list-card-subtitle">展示最近 10 条,便于从套餐维度快速下钻订单治理。</p>
</div>
<a class="btn btn-secondary btn-sm" href="{!! $makePlatformOrderUrl(['plan_id' => $plan->id]) !!}">查看全部订单</a>
</div>
<div class="list-card-body">
<table class="list-card-table">
<thead>
<tr>
<th>订单号</th>
<th>站点</th>
<th>订单类型</th>
<th>订单状态</th>
<th>支付状态</th>
<th>应付 / 已付</th>
<th>关联订阅</th>
</tr>
</thead>
<tbody>
@forelse($recentOrders as $order)
<tr>
<td>{{ $order->order_no }}</td>
<td>{{ $order->merchant?->name ?? '未关联站点' }}</td>
<td>{{ $order->orderTypeLabel() }}</td>
<td>{{ $order->status }}</td>
<td>{{ $order->payment_status }}</td>
<td>¥{{ number_format((float) $order->payable_amount, 2) }} / ¥{{ number_format((float) $order->paid_amount, 2) }}</td>
<td>{{ $order->siteSubscription?->subscription_no ?? '-' }}</td>
</tr>
@empty
<tr>
<td colspan="7" class="muted table-empty">暂无平台订单</td>
</tr>
@endforelse
</tbody>
</table>
</div>
</div>
<div class="card list-card">
<div class="list-card-header">
<div>
<h3 class="list-card-title">最近订阅</h3>
<p class="muted muted-xs list-card-subtitle">展示最近 10 条,便于从套餐维度快速下钻订阅治理。</p>
</div>
<a class="btn btn-secondary btn-sm" href="{!! $makeSubscriptionUrl(['plan_id' => $plan->id]) !!}">查看全部订阅</a>
</div>
<div class="list-card-body">
<table class="list-card-table">
<thead>
<tr>
<th>订阅号</th>
<th>站点</th>
<th>状态</th>
<th>金额</th>
<th>开始时间</th>
<th>到期时间</th>
</tr>
</thead>
<tbody>
@forelse($recentSubscriptions as $subscription)
<tr>
<td>{{ $subscription->subscription_no }}</td>
<td>{{ $subscription->merchant?->name ?? '未关联站点' }}</td>
<td>{{ $subscription->status }}</td>
<td>¥{{ number_format((float) $subscription->amount, 2) }}</td>
<td>{{ optional($subscription->starts_at)->format('Y-m-d H:i:s') ?: '-' }}</td>
<td>{{ optional($subscription->ends_at)->format('Y-m-d H:i:s') ?: '-' }}</td>
</tr>
@empty
<tr>
<td colspan="6" class="muted table-empty">暂无订阅记录</td>
</tr>
@endforelse
</tbody>
</table>
</div>
</div>
@endsection

View File

@@ -43,7 +43,7 @@
</div>
<div class="actions gap-10">
<a class="btn btn-secondary btn-sm" href="{{ $backToListUrl }}">返回上一页</a>
@if($runId !== '')
@if(($error ?? '') === '' && $runId !== '')
<button type="button" class="btn btn-secondary btn-sm" data-action="copy-run-id" data-run-id="{{ $runId }}">复制 run_id</button>
@endif
<a class="btn btn-secondary btn-sm" href="/admin/platform-batches/show?type={{ $type }}&run_id={{ urlencode($runId) }}&back={{ urlencode($selfWithoutBack) }}">刷新</a>
@@ -140,10 +140,39 @@
$spotPickNextUrl = \App\Support\BackUrl::withBack($spotPickNextUrl, $safeBackForLinks);
@endphp
<a class="link muted-xs" data-role="batch-spot-check-next" href="{{ $spotPickNextUrl }}">换一单</a>
@php
$spotAfterId = (int) request()->query('spot_after_id', 0);
@endphp
@if($spotAfterId > 0)
@php
$spotPickResetUrl = '/admin/platform-batches/show?' . \Illuminate\Support\Arr::query([
'type' => $type,
'run_id' => $runId,
]);
$spotPickResetUrl = \App\Support\BackUrl::withBack($spotPickResetUrl, $safeBackForLinks);
@endphp
<span class="muted muted-xs"></span>
<a class="link muted-xs" data-role="batch-spot-check-reset" href="{{ $spotPickResetUrl }}">回到最新</a>
@endif
@endif
</div>
@else
<div class="muted">暂无可抽样订单(可能暂无成功单,或 last_result 尚未补齐)。</div>
@php
$spotAfterId = (int) request()->query('spot_after_id', 0);
@endphp
@if($spotAfterId > 0)
@php
$spotPickResetUrl = '/admin/platform-batches/show?' . \Illuminate\Support\Arr::query([
'type' => $type,
'run_id' => $runId,
]);
$spotPickResetUrl = \App\Support\BackUrl::withBack($spotPickResetUrl, $safeBackForLinks);
@endphp
<div class="mt-8">
<a class="link muted-xs" data-role="batch-spot-check-reset" href="{{ $spotPickResetUrl }}">回到最新</a>
</div>
@endif
@endif
</div>

View File

@@ -217,9 +217,17 @@
'renewal_missing_subscription' => '1',
'back' => $selfWithoutBack,
]);
$viewPaidNoReceiptOrdersUrl = '/admin/platform-orders?' . \Illuminate\Support\Arr::query([
'lead_id' => $l->id,
'payment_status' => 'paid',
'receipt_status' => 'none',
'back' => $selfWithoutBack,
]);
@endphp
<a class="btn-secondary btn-sm" href="{!! $viewOrdersUrl !!}">查看订单</a>
<a class="btn-secondary btn-sm" href="{!! $viewRenewalMissingSubOrdersUrl !!}">续费缺订阅</a>
<a class="btn-secondary btn-sm" href="{!! $viewPaidNoReceiptOrdersUrl !!}">已付无回执</a>
</div>
</td>
</tr>

View File

@@ -0,0 +1 @@
<a data-role="{{ $role ?? '' }}" class="{{ $class ?? 'link' }}" href="{!! $href ?? '#' !!}">{{ $label ?? '' }}</a>

View File

@@ -0,0 +1,13 @@
@include('admin.platform_orders._summary_metric_link', [
'role' => $leftRole ?? '',
'href' => $leftHref ?? '#',
'label' => $leftLabel ?? '',
'class' => $leftClass ?? 'link',
])
<span class="{{ $separatorClass ?? 'muted' }}"> {{ $separator ?? '/' }} </span>
@include('admin.platform_orders._summary_metric_link', [
'role' => $rightRole ?? '',
'href' => $rightHref ?? '#',
'label' => $rightLabel ?? '',
'class' => $rightClass ?? 'link',
])

View File

@@ -0,0 +1 @@
<a data-role="{{ $role ?? '' }}" @if(!empty($ariaLabel ?? '')) aria-label="{{ $ariaLabel }}" @endif @if(!empty($title ?? '')) title="{{ $title }}" @endif class="{{ $class ?? 'link' }}" href="{!! $href ?? '#' !!}">{{ $label ?? '' }}</a>

View File

@@ -0,0 +1 @@
<a @if(!empty($role ?? '')) data-role="{{ $role }}" @endif @if(!empty($ariaLabel ?? '')) aria-label="{{ $ariaLabel }}" @endif class="{{ $class ?? 'btn btn-secondary btn-sm' }}" href="{{ $href ?? '#' }}">{{ $label ?? '' }}</a>

View File

@@ -98,11 +98,21 @@
$renewCreateUrl = '/admin/platform-orders/create?' . \Illuminate\Support\Arr::query($renewCreateQuery);
}
@endphp
<div class="page-header mb-20" data-page="admin.platform_orders.index">
<div class="page-header mb-20" id="po-page-top" data-page="admin.platform_orders.index">
<div class="page-header-main">
<div>
<div class="page-header-title">平台订单</div>
<div class="page-header-subtitle">这里是总台视角的平台收费主链骨架页,当前阶段先承接套餐订购 / 续费 / 生效跟踪。本页先提供可访问列表、基础筛选与摘要卡,后续再补详情、导出、支付记录与退款轨迹。<span class="muted">(建议运营日常优先使用:待支付 / 待生效 / 可同步 / 同步失败 / 续费缺订阅 等治理集合)</span></div>
<div class="actions gap-10 mt-10" data-role="po-page-jump-links" aria-label="平台订单页内导航">
<span class="muted muted-xs" data-role="po-page-jump-prefix" aria-label="平台订单页内导航前缀">页内导航:</span>
<a class="btn btn-secondary btn-sm" data-role="po-page-jump-quick-filters" aria-label="定位到快捷筛选" title="回到快捷筛选查看常用治理集合" href="#po-quick-filters">快捷筛选</a>
<a class="btn btn-secondary btn-sm" data-role="po-page-jump-filters" aria-label="定位到筛选条件" title="回到筛选区调整范围" href="#filters">筛选条件</a>
<a class="btn btn-secondary btn-sm" data-role="po-page-jump-summary" aria-label="定位到摘要概览" title="回到摘要卡查看当前盘面" href="#po-summary-cards">摘要概览</a>
<a class="btn btn-secondary btn-sm" data-role="po-page-jump-tools" aria-label="定位到工具区" title="回到工具区执行导出或批量动作" href="#po-tools-grid">工具区</a>
<a class="btn btn-secondary btn-sm" data-role="po-page-jump-list" aria-label="定位到订单列表" title="回到订单列表查看当前明细" href="#po-list-card">订单列表</a>
<a class="btn btn-secondary btn-sm" data-role="po-page-jump-top" aria-label="回到页面顶部" title="回到平台订单页顶部" href="#po-page-top">回顶部</a>
</div>
<div class="muted muted-xs mt-6" data-role="po-page-jump-note" aria-label="平台订单页内导航说明" title="页内导航说明与回跳入口区">仅用于页内快速跳转,不会改变当前筛选状态。<span class="sr-only">仅用于页内快速跳转,不会改变当前筛选状态;</span>可从这里快速回到关键工作区:如需重新查看页头概述,可<a class="link" data-role="po-page-jump-back-to-top" aria-label="回到页面顶部" title="回到顶部查看页头概述" href="#po-page-top">回到顶部</a>;如需继续调整集合,可<a class="link" data-role="po-page-jump-back-to-quick-filters" aria-label="回到快捷筛选" title="回到平台订单快捷筛选" href="#po-quick-filters">回到快捷筛选</a>;如需重新查看盘面,可<a class="link" data-role="po-page-jump-back-to-summary" aria-label="回到摘要概览" title="回到摘要卡查看当前盘面" href="#po-summary-cards">回到摘要概览</a>;如需继续执行动作,可<a class="link" data-role="po-page-jump-back-to-tools" aria-label="回到工具区" title="回到工具区继续执行动作" href="#po-tools-grid">回到工具区</a>;如需核对明细,可<a class="link" data-role="po-page-jump-back-to-list" aria-label="回到订单列表" title="回到订单列表查看明细" href="#po-list-card">回到订单列表</a></div>
</div>
<div class="page-header-actions">
@@ -154,6 +164,11 @@
'status' => 'pending',
'sync_status' => 'unsynced',
]);
$leadPaidNoReceiptUrl = $buildLeadGovernUrl([
'payment_status' => 'paid',
'receipt_status' => 'none',
]);
@endphp
@php
$leadBmpaUrl = $buildLeadGovernUrl([
@@ -179,6 +194,7 @@
<div class="mt-6 actions gap-10">
<a class="btn btn-secondary btn-sm" href="{!! $leadUnpaidUrl !!}">查看待支付(该线索)</a>
<a class="btn btn-secondary btn-sm" href="{!! $leadPaidPendingUrl !!}">查看待生效(该线索)</a>
<a class="btn btn-secondary btn-sm" href="{!! $leadPaidNoReceiptUrl !!}">查看已付无回执(该线索)</a>
<a class="btn btn-secondary btn-sm" href="{!! $leadBmpaUrl !!}">查看可BMPA处理该线索</a>
<a class="btn btn-secondary btn-sm" href="{!! $leadSyncableUrl !!}">查看可同步(该线索)</a>
<a class="btn btn-secondary btn-sm" href="{!! $leadSyncFailedUrl !!}">查看同步失败(该线索)</a>
@@ -243,7 +259,8 @@
// 订阅治理快捷入口保留订阅上下文site_subscription_id + 其它业务上下文),但不继承 syncable_only/fail_only/page 等工具型开关。
$buildSubGovernUrl = function (array $overrides) use ($safeBackForLinks) {
return \App\Support\BackUrl::currentPathQuickFilter(
['merchant_id', 'plan_id', 'site_subscription_id', 'keyword', 'lead_id'],
// 订阅锁定治理入口除订阅ID外也保留时间范围便于运营在“近7天/某段时间窗口”内治理)
['merchant_id', 'plan_id', 'site_subscription_id', 'keyword', 'lead_id', 'created_from', 'created_to'],
$overrides,
$safeBackForLinks
);
@@ -258,6 +275,9 @@
$subSyncFailedUrl = $buildSubGovernUrl([
'sync_status' => 'failed',
'syncable_only' => null,
// 从 BMPA 失败上下文切回同步失败集合:清理冲突开关
'bmpa_failed_only' => null,
'bmpa_error_keyword' => null,
]);
$subUnpaidUrl = $buildSubGovernUrl([
@@ -277,6 +297,14 @@
'fail_only' => null,
]);
$subPaidNoReceiptUrl = $buildSubGovernUrl([
'payment_status' => 'paid',
'receipt_status' => 'none',
'syncable_only' => null,
'sync_status' => null,
'fail_only' => null,
]);
$subRenewalMissingSubscriptionUrl = $buildSubGovernUrl([
'renewal_missing_subscription' => '1',
]);
@@ -286,13 +314,14 @@
<a class="btn btn-secondary btn-sm" href="{!! $subSyncFailedUrl !!}">查看同步失败(该订阅)</a>
<a class="btn btn-secondary btn-sm" href="{!! $subUnpaidUrl !!}">查看待支付(该订阅)</a>
<a class="btn btn-secondary btn-sm" href="{!! $subPaidPendingUrl !!}">查看待生效(该订阅)</a>
<a class="btn btn-secondary btn-sm" href="{!! $subPaidNoReceiptUrl !!}">查看已付无回执(该订阅)</a>
<a class="btn btn-secondary btn-sm" href="{!! $subRenewalMissingSubscriptionUrl !!}">查看续费缺订阅(该订阅)</a>
</div>
</div>
</div>
@endif
<div class="card mb-20">
<div class="card mb-20" id="po-quick-filters">
<div class="actions-spread">
<div>
<h3>快捷筛选</h3>
@@ -324,33 +353,33 @@
$allUrl = \App\Support\BackUrl::withBack('/admin/platform-orders', $safeBackForLinks);
@endphp
<div class="inline-links">
<a href="{!! $allUrl !!}" class="muted">全部</a>
<a href="{!! $buildQuickFilterUrl(['payment_status' => 'unpaid']) !!}" class="muted">待支付</a>
<a href="{!! $buildQuickFilterUrl(['bmpa_processable_only' => '1']) !!}" class="muted">可BMPA处理</a>
<a href="{!! $buildQuickFilterUrl(['payment_status' => 'paid', 'status' => 'pending', 'sync_status' => 'unsynced']) !!}" class="muted">待生效</a>
<a href="{!! $buildQuickFilterUrl(['syncable_only' => '1', 'sync_status' => 'unsynced']) !!}" class="muted">可同步订阅</a>
<a href="{!! $buildQuickFilterUrl(['sync_status' => 'failed']) !!}" class="muted">同步失败</a>
<a href="{!! $buildQuickFilterUrl(['bmpa_failed_only' => '1']) !!}" class="muted">BMPA失败</a>
<a href="{!! $buildQuickFilterUrl(['bmpa_success_only' => '1']) !!}" class="muted">BMPA成功</a>
<a href="{!! $buildQuickFilterUrl(['renewal_missing_subscription' => '1']) !!}" class="muted">续费缺订阅</a>
<div class="inline-links" data-role="po-quickfilter-primary-group" aria-label="平台订单主链快捷筛选组">
<a data-role="po-quickfilter-all" href="{!! $allUrl !!}" class="muted">全部</a>
<a data-role="po-quickfilter-unpaid" href="{!! $buildQuickFilterUrl(['payment_status' => 'unpaid']) !!}" class="muted">待支付</a>
<a data-role="po-quickfilter-bmpa-processable" href="{!! $buildQuickFilterUrl(['bmpa_processable_only' => '1']) !!}" class="muted">可BMPA处理</a>
<a data-role="po-quickfilter-pending-effective" href="{!! $buildQuickFilterUrl(['payment_status' => 'paid', 'status' => 'pending', 'sync_status' => 'unsynced']) !!}" class="muted">待生效</a>
<a data-role="po-quickfilter-syncable" href="{!! $buildQuickFilterUrl(['syncable_only' => '1', 'sync_status' => 'unsynced']) !!}" class="muted">可同步订阅</a>
<a data-role="po-quickfilter-sync-failed" href="{!! $buildQuickFilterUrl(['sync_status' => 'failed']) !!}" class="muted">同步失败</a>
<a data-role="po-quickfilter-bmpa-failed" href="{!! $buildQuickFilterUrl(['bmpa_failed_only' => '1']) !!}" class="muted">BMPA失败</a>
<a data-role="po-quickfilter-bmpa-success" href="{!! $buildQuickFilterUrl(['bmpa_success_only' => '1']) !!}" class="muted">BMPA成功</a>
<a data-role="po-quickfilter-renewal-missing-sub" href="{!! $buildQuickFilterUrl(['renewal_missing_subscription' => '1']) !!}" class="muted">续费缺订阅</a>
</div>
<div class="inline-links mt-6">
<a href="{!! $buildQuickFilterUrl(['receipt_status' => 'has']) !!}" class="muted">有回执</a>
<a href="{!! $buildQuickFilterUrl(['receipt_status' => 'none']) !!}" class="muted">无回执</a>
<a href="{!! $buildQuickFilterUrl(['refund_status' => 'has']) !!}" class="muted">有退款</a>
<a href="{!! $buildQuickFilterUrl(['refund_status' => 'none']) !!}" class="muted">无退款</a>
<a href="{!! $buildQuickFilterUrl(['payment_status' => 'partially_refunded']) !!}" class="muted">部分退款</a>
<a href="{!! $buildQuickFilterUrl(['payment_status' => 'refunded']) !!}" class="muted">已退款</a>
<div class="inline-links mt-6" data-role="po-quickfilter-receipt-refund-group" aria-label="平台订单收付治理快捷筛选组">
<a data-role="po-quickfilter-receipt-has" href="{!! $buildQuickFilterUrl(['receipt_status' => 'has']) !!}" class="muted">有回执</a>
<a data-role="po-quickfilter-paid-no-receipt" href="{!! $buildQuickFilterUrl(['payment_status' => 'paid', 'receipt_status' => 'none']) !!}" class="muted">已付无回执</a>
<a data-role="po-quickfilter-refund-has" href="{!! $buildQuickFilterUrl(['refund_status' => 'has']) !!}" class="muted">有退款</a>
<a data-role="po-quickfilter-refund-none" href="{!! $buildQuickFilterUrl(['refund_status' => 'none']) !!}" class="muted">无退款</a>
<a data-role="po-quickfilter-partially-refunded" href="{!! $buildQuickFilterUrl(['payment_status' => 'partially_refunded']) !!}" class="muted">部分退款</a>
<a data-role="po-quickfilter-refunded" href="{!! $buildQuickFilterUrl(['payment_status' => 'refunded']) !!}" class="muted">已退款</a>
</div>
<div class="inline-links mt-6">
<a href="{!! $buildQuickFilterUrl(['reconcile_mismatch' => '1']) !!}" class="muted">对账不一致</a>
<a href="{!! $buildQuickFilterUrl(['refund_inconsistent' => '1']) !!}" class="muted">退款不一致</a>
<a href="{!! $buildQuickFilterUrl(['batch_synced_24h' => '1']) !!}" class="muted">近24小时批量同步</a>
<a href="{!! $buildQuickFilterUrl(['batch_mark_paid_and_activate_24h' => '1']) !!}" class="muted">近24小时批量BMPA</a>
<a href="{!! $buildQuickFilterUrl(['batch_mark_activated_24h' => '1']) !!}" class="muted">近24小时批量生效</a>
<div class="inline-links mt-6" data-role="po-quickfilter-governance-batch-group" aria-label="平台订单异常与批量治理快捷筛选组">
<a data-role="po-quickfilter-reconcile-mismatch" href="{!! $buildQuickFilterUrl(['reconcile_mismatch' => '1']) !!}" class="muted">对账不一致</a>
<a data-role="po-quickfilter-refund-inconsistent" href="{!! $buildQuickFilterUrl(['refund_inconsistent' => '1']) !!}" class="muted">退款不一致</a>
<a data-role="po-quickfilter-batch-synced-24h" href="{!! $buildQuickFilterUrl(['batch_synced_24h' => '1']) !!}" class="muted">近24小时批量同步</a>
<a data-role="po-quickfilter-batch-bmpa-24h" href="{!! $buildQuickFilterUrl(['batch_mark_paid_and_activate_24h' => '1']) !!}" class="muted">近24小时批量BMPA</a>
<a data-role="po-quickfilter-batch-activated-24h" href="{!! $buildQuickFilterUrl(['batch_mark_activated_24h' => '1']) !!}" class="muted">近24小时批量生效</a>
</div>
</div>
@@ -396,11 +425,14 @@
<option value="synced" @selected(($filters['sync_status'] ?? '') === 'synced')>已同步</option>
<option value="failed" @selected(($filters['sync_status'] ?? '') === 'failed')>同步失败</option>
</select>
<select name="receipt_status">
<option value="">全部回执状态</option>
<option value="has" @selected(($filters['receipt_status'] ?? '') === 'has')>有回执</option>
<option value="none" @selected(($filters['receipt_status'] ?? '') === 'none')>回执</option>
</select>
<div>
<select name="receipt_status">
<option value="">全部回执状态</option>
<option value="has" @selected(($filters['receipt_status'] ?? '') === 'has')>回执</option>
<option value="none" @selected(($filters['receipt_status'] ?? '') === 'none')>无回执(广义)</option>
</select>
<div class="muted muted-xs mt-6" data-role="po-receipt-status-broad-none-hint">无回执(广义)会包含未支付订单;收费闭环治理请优先使用<a class="link" data-role="po-receipt-status-broad-none-go-paid-no-receipt" aria-label="切换到已付无回执快捷筛选" href="{!! $buildQuickFilterUrl(['payment_status' => 'paid', 'receipt_status' => 'none']) !!}">已付无回执</a>快捷筛选。<span class="sr-only">无回执(广义)会包含未支付订单;收费闭环治理请优先使用“已付无回执”快捷筛选。</span></div>
</div>
<select name="refund_status">
<option value="">全部退款状态</option>
<option value="has" @selected(($filters['refund_status'] ?? '') === 'has')>有退款</option>
@@ -633,53 +665,103 @@
</div>
@endif
<div class="grid-3 mb-20">
<div class="actions gap-10 mb-10" data-role="po-summary-jump-links" aria-label="平台订单摘要快捷导航">
<span class="muted muted-xs" data-role="po-summary-jump-prefix" aria-label="平台订单摘要导航前缀">摘要导航:</span>
<a class="btn btn-secondary btn-sm" data-role="po-summary-jump-filters" aria-label="定位到筛选条件" title="回到筛选区调整范围" href="#filters">筛选条件</a>
<span class="muted muted-xs" data-role="po-summary-jump-note" aria-label="平台订单摘要导航说明" title="摘要导航仅用于快速定位摘要卡,不会改变筛选状态"><span data-role="po-summary-jump-note-prefix" aria-label="平台订单摘要导航说明前缀">说明:</span>仅用于摘要区快速定位,不会改变当前筛选状态;如需重新查看页头概述,可<a class="link" data-role="po-summary-jump-back-to-top" aria-label="回到页面顶部" title="回到顶部查看页头概述" href="#po-page-top">回到顶部</a>;如需重新查看摘要整体,可先<a class="link" data-role="po-summary-jump-back-to-summary-cards" aria-label="回到摘要区" title="回到摘要区查看全部摘要卡" href="#po-summary-cards">回到摘要区</a>;如需重新调整范围,可先<a class="link" data-role="po-summary-jump-back-to-filters" aria-label="回到筛选条件" title="回到筛选区调整范围" href="#filters">回到筛选条件</a>;如需继续执行动作,可<a class="link" data-role="po-summary-jump-back-to-tools" aria-label="回到工具区" title="回到工具区执行导出或批量动作" href="#po-tools-grid">回到工具区</a>;如需核对明细,可<a class="link" data-role="po-summary-jump-back-to-list" aria-label="回到订单列表" title="回到订单列表查看明细" href="#po-list-card">回到订单列表</a></span>
<span class="muted muted-xs mb-10" data-role="po-summary-jump-links-note">摘要导航只负责定位到关键摘要卡,不会触发治理动作。</span>
<a class="btn btn-secondary btn-sm" data-role="po-summary-jump-paid-no-receipt" aria-label="定位到已付无回执摘要卡" title="回到已付无回执摘要卡" href="#po-summary-card-paid-no-receipt">已付无回执</a>
<a class="btn btn-secondary btn-sm" data-role="po-summary-jump-reconcile-mismatch" aria-label="定位到对账不一致摘要卡" title="回到对账不一致摘要卡" href="#po-summary-card-reconcile-mismatch">对账不一致</a>
<a class="btn btn-secondary btn-sm" data-role="po-summary-jump-syncable" aria-label="定位到可同步摘要卡" title="回到可同步摘要卡" href="#po-summary-card-syncable">可同步</a>
<a class="btn btn-secondary btn-sm" data-role="po-summary-jump-renewal-missing-sub" aria-label="定位到续费缺订阅摘要卡" title="回到续费缺订阅摘要卡" href="#po-summary-card-renewal-missing-sub">续费缺订阅</a>
<a class="btn btn-secondary btn-sm" data-role="po-summary-jump-tools" aria-label="定位到工具区" title="前往工具区执行导出或批量动作" href="#po-tools-grid">工具区</a>
</div>
<div class="grid-3 mb-20" id="po-summary-cards" data-role="po-summary-cards">
<div class="card">
<h3>平台订单总数</h3>
<div class="metric-number">{{ $summaryStats['total_orders'] ?? 0 }}</div>
<div class="metric-number">
@include('admin.platform_orders._summary_metric_link', [
'role' => 'po-summary-link-total-orders',
'href' => $safeFullUrlWithQuery(['page' => null]),
'label' => $summaryStats['total_orders'] ?? 0,
])
</div>
</div>
<div class="card">
<h3>已支付 / 已生效</h3>
<div class="metric-number">
<a class="link" href="{!! $safeFullUrlWithQuery(['payment_status' => 'paid', 'page' => null]) !!}">{{ $summaryStats['paid_orders'] ?? 0 }}</a>
/
<a class="link" href="{!! $safeFullUrlWithQuery(['status' => 'activated', 'page' => null]) !!}">{{ $summaryStats['activated_orders'] ?? 0 }}</a>
@include('admin.platform_orders._summary_metric_pair', [
'leftRole' => 'po-summary-link-paid-orders',
'leftHref' => $safeFullUrlWithQuery(['payment_status' => 'paid', 'page' => null]),
'leftLabel' => $summaryStats['paid_orders'] ?? 0,
'rightRole' => 'po-summary-link-activated-orders',
'rightHref' => $safeFullUrlWithQuery(['status' => 'activated', 'page' => null]),
'rightLabel' => $summaryStats['activated_orders'] ?? 0,
'separator' => '/',
'separatorClass' => '',
])
</div>
</div>
<div class="card">
<h3>已同步 / 未同步</h3>
<div class="metric-number">
<a class="link" href="{!! $safeFullUrlWithQuery(['sync_status' => 'synced', 'page' => null]) !!}">{{ $summaryStats['synced_orders'] ?? 0 }}</a>
/
<a class="link" href="{!! $safeFullUrlWithQuery(['sync_status' => 'unsynced', 'page' => null]) !!}">{{ $summaryStats['unsynced_orders'] ?? 0 }}</a>
@include('admin.platform_orders._summary_metric_pair', [
'leftRole' => 'po-summary-link-synced-orders',
'leftHref' => $safeFullUrlWithQuery(['sync_status' => 'synced', 'page' => null]),
'leftLabel' => $summaryStats['synced_orders'] ?? 0,
'rightRole' => 'po-summary-link-unsynced-orders',
'rightHref' => $safeFullUrlWithQuery(['sync_status' => 'unsynced', 'page' => null]),
'rightLabel' => $summaryStats['unsynced_orders'] ?? 0,
'separator' => '/',
'separatorClass' => '',
])
</div>
</div>
<div class="card">
<h3>同步失败数</h3>
<div class="metric-number">
<a class="link" href="{!! $safeFullUrlWithQuery(['sync_status' => 'failed', 'page' => null]) !!}">{{ $summaryStats['failed_sync_orders'] ?? 0 }}</a>
@include('admin.platform_orders._summary_metric_link', [
'role' => 'po-summary-link-sync-failed-orders',
'href' => $safeFullUrlWithQuery(['sync_status' => 'failed', 'page' => null]),
'label' => $summaryStats['failed_sync_orders'] ?? 0,
])
</div>
</div>
<div class="card">
<h3>BMPA 成功 / 失败</h3>
<div class="metric-number">
<a class="link" href="{!! $safeFullUrlWithQuery(['bmpa_success_only' => '1', 'page' => null]) !!}">{{ $summaryStats['bmpa_success_orders'] ?? 0 }}</a>
<span class="muted"> / </span>
<a class="link" href="{!! $safeFullUrlWithQuery(['bmpa_failed_only' => '1', 'page' => null]) !!}">{{ $summaryStats['bmpa_failed_orders'] ?? 0 }}</a>
@include('admin.platform_orders._summary_metric_pair', [
'leftRole' => 'po-summary-link-bmpa-success-orders',
'leftHref' => $safeFullUrlWithQuery(['bmpa_success_only' => '1', 'fail_only' => null, 'page' => null]),
'leftLabel' => $summaryStats['bmpa_success_orders'] ?? 0,
'rightRole' => 'po-summary-link-bmpa-failed-orders',
'rightHref' => $safeFullUrlWithQuery(['bmpa_failed_only' => '1', 'fail_only' => null, 'page' => null]),
'rightLabel' => $summaryStats['bmpa_failed_orders'] ?? 0,
'separator' => '/',
'separatorClass' => 'muted',
])
</div>
<div class="muted muted-xs">成功口径:存在 run_id 且无 error.message失败口径meta 失败标记</div>
</div>
<div class="card">
<div class="card" id="po-summary-card-syncable">
<h3>可同步订单</h3>
<div class="metric-number">
<a class="link" href="{!! $safeFullUrlWithQuery(['syncable_only' => '1', 'sync_status' => 'unsynced', 'page' => null]) !!}">{{ $summaryStats['syncable_orders'] ?? 0 }}</a>
@include('admin.platform_orders._summary_metric_link', [
'role' => 'po-summary-link-syncable-orders',
'href' => $safeFullUrlWithQuery(['syncable_only' => '1', 'sync_status' => 'unsynced', 'fail_only' => null, 'page' => null]),
'label' => $summaryStats['syncable_orders'] ?? 0,
])
</div>
<div class="muted muted-xs">已支付 + 已生效 + 未同步(续费单需已绑定订阅)</div>
</div>
<div class="card">
<div class="card" id="po-summary-card-renewal-missing-sub">
<h3>续费缺订阅</h3>
<div class="metric-number">
<a class="link" href="{!! $safeFullUrlWithQuery(['renewal_missing_subscription' => '1', 'page' => null]) !!}">{{ $summaryStats['renewal_missing_subscription_orders'] ?? 0 }}</a>
@include('admin.platform_orders._summary_metric_link', [
'role' => 'po-summary-link-renewal-missing-sub-orders',
'href' => $safeFullUrlWithQuery(['renewal_missing_subscription' => '1', 'page' => null]),
'label' => $summaryStats['renewal_missing_subscription_orders'] ?? 0,
])
</div>
<div class="muted muted-xs">renewal + site_subscription_id 为空(需治理:去订单详情补订阅/核对来源)</div>
</div>
@@ -687,30 +769,49 @@
<div class="card">
<h3>近24小时批量同步</h3>
<div class="metric-number">
<a class="link" href="{!! $safeFullUrlWithQuery(['batch_synced_24h' => '1', 'page' => null]) !!}">{{ $summaryStats['batch_synced_24h_orders'] ?? 0 }}</a>
@include('admin.platform_orders._summary_metric_link', [
'role' => 'po-summary-link-batch-synced-24h-orders',
'href' => $safeFullUrlWithQuery(['batch_synced_24h' => '1', 'page' => null]),
'label' => $summaryStats['batch_synced_24h_orders'] ?? 0,
])
</div>
<div class="muted muted-xs">基于 meta.batch_activation.at</div>
</div>
<div class="card">
<h3>近24小时批量BMPA</h3>
<div class="metric-number">
<a class="link" href="{!! $safeFullUrlWithQuery(['batch_mark_paid_and_activate_24h' => '1', 'page' => null]) !!}">{{ $summaryStats['batch_mark_paid_and_activate_24h_orders'] ?? 0 }}</a>
@include('admin.platform_orders._summary_metric_link', [
'role' => 'po-summary-link-batch-bmpa-24h-orders',
'href' => $safeFullUrlWithQuery(['batch_mark_paid_and_activate_24h' => '1', 'page' => null]),
'label' => $summaryStats['batch_mark_paid_and_activate_24h_orders'] ?? 0,
])
</div>
<div class="muted muted-xs">基于 meta.batch_mark_paid_and_activate.at</div>
</div>
<div class="card">
<h3>近24小时批量生效</h3>
<div class="metric-number">
<a class="link" href="{!! $safeFullUrlWithQuery(['batch_mark_activated_24h' => '1', 'page' => null]) !!}">{{ $summaryStats['batch_mark_activated_24h_orders'] ?? 0 }}</a>
@include('admin.platform_orders._summary_metric_link', [
'role' => 'po-summary-link-batch-activated-24h-orders',
'href' => $safeFullUrlWithQuery(['batch_mark_activated_24h' => '1', 'page' => null]),
'label' => $summaryStats['batch_mark_activated_24h_orders'] ?? 0,
])
</div>
<div class="muted muted-xs">基于 meta.batch_mark_activated.at</div>
</div>
<div class="card">
<h3>部分退款 / 已退款</h3>
<div class="metric-number">
<a class="link" href="{!! $safeFullUrlWithQuery(['payment_status' => 'partially_refunded', 'page' => null]) !!}">{{ $summaryStats['partially_refunded_orders'] ?? 0 }}</a>
/
<a class="link" href="{!! $safeFullUrlWithQuery(['payment_status' => 'refunded', 'page' => null]) !!}">{{ $summaryStats['refunded_orders'] ?? 0 }}</a>
@include('admin.platform_orders._summary_metric_pair', [
'leftRole' => 'po-summary-link-partially-refunded-orders',
'leftHref' => $safeFullUrlWithQuery(['payment_status' => 'partially_refunded', 'page' => null]),
'leftLabel' => $summaryStats['partially_refunded_orders'] ?? 0,
'rightRole' => 'po-summary-link-refunded-orders',
'rightHref' => $safeFullUrlWithQuery(['payment_status' => 'refunded', 'page' => null]),
'rightLabel' => $summaryStats['refunded_orders'] ?? 0,
'separator' => '/',
'separatorClass' => '',
])
</div>
</div>
<div class="card">
@@ -718,37 +819,73 @@
<div class="metric-number">¥{{ number_format((float) ($summaryStats['total_refunded_amount'] ?? 0), 2) }}</div>
<div class="muted muted-xs">基于 meta.refund_summary.total_amount缺省回退汇总</div>
<div class="muted muted-xs">
<a class="link" href="{!! $safeFullUrlWithQuery(['refund_status' => 'has', 'page' => null]) !!}">查看有退款订单</a>
@include('admin.platform_orders._summary_text_link', [
'role' => 'po-summary-link-view-refund-orders',
'href' => $safeFullUrlWithQuery(['refund_status' => 'has', 'page' => null]),
'label' => '查看有退款订单',
])
</div>
</div>
<div class="card">
<h3>有退款订单 / 无退款订单</h3>
<div class="metric-number">
<a class="link" href="{!! $safeFullUrlWithQuery(['refund_status' => 'has', 'page' => null]) !!}">{{ $summaryStats['refund_orders'] ?? 0 }}</a>
/
<a class="link" href="{!! $safeFullUrlWithQuery(['refund_status' => 'none', 'page' => null]) !!}">{{ $summaryStats['no_refund_orders'] ?? 0 }}</a>
@include('admin.platform_orders._summary_metric_pair', [
'leftRole' => 'po-summary-link-refund-orders',
'leftHref' => $safeFullUrlWithQuery(['refund_status' => 'has', 'page' => null]),
'leftLabel' => $summaryStats['refund_orders'] ?? 0,
'rightRole' => 'po-summary-link-no-refund-orders',
'rightHref' => $safeFullUrlWithQuery(['refund_status' => 'none', 'page' => null]),
'rightLabel' => $summaryStats['no_refund_orders'] ?? 0,
'separator' => '/',
'separatorClass' => '',
])
</div>
<div class="muted muted-xs">口径refund_summary.total_amount 存在或 refund_receipts 有记录</div>
</div>
<div class="card">
<h3>有回执订单 / 回执总额</h3>
<div class="metric-number">
<a class="link" href="{!! $safeFullUrlWithQuery(['receipt_status' => 'has', 'page' => null]) !!}">{{ $summaryStats['receipt_orders'] ?? 0 }}</a>
@include('admin.platform_orders._summary_metric_link', [
'role' => 'po-summary-link-receipt-orders',
'href' => $safeFullUrlWithQuery(['receipt_status' => 'has', 'page' => null]),
'label' => $summaryStats['receipt_orders'] ?? 0,
])
/ ¥{{ number_format((float) ($summaryStats['total_receipt_amount'] ?? 0), 2) }}
</div>
<div class="muted muted-xs">有回执口径payment_summary.total_amount 存在或 payment_receipts 有记录</div>
<div class="muted muted-xs">
<a class="link" href="{!! $safeFullUrlWithQuery(['receipt_status' => 'none', 'page' => null]) !!}">查看无回执订单</a>
@include('admin.platform_orders._summary_text_link', [
'role' => 'po-summary-link-view-no-receipt-orders',
'href' => $safeFullUrlWithQuery(['payment_status' => 'paid', 'receipt_status' => 'none', 'page' => null]),
'label' => '查看已付无回执订单',
])
</div>
</div>
<div class="card">
<h3>无回执订单</h3>
<div class="card" data-role="po-summary-card-paid-no-receipt" aria-label="已付无回执摘要卡">
<h3>已付无回执订单</h3>
<div class="metric-number">
<a class="link" href="{!! $safeFullUrlWithQuery(['receipt_status' => 'none', 'page' => null]) !!}">{{ $summaryStats['no_receipt_orders'] ?? 0 }}</a>
@include('admin.platform_orders._summary_metric_link', [
'role' => 'po-summary-link-no-receipt-orders',
'href' => $safeFullUrlWithQuery(['payment_status' => 'paid', 'receipt_status' => 'none', 'page' => null]),
'label' => $summaryStats['no_receipt_orders'] ?? 0,
])
</div>
<div class="muted muted-xs"> payment_summary 且无 payment_receipts</div>
<div class="muted muted-xs">已支付且 payment_summary / payment_receipts,优先进入补回执治理集合</div>
<div class="muted governance-block-footnote" data-role="po-summary-paid-no-receipt-footnote" aria-label="已付无回执摘要优先级说明">收费闭环优先:建议先处理该集合,再推进同步与批量动作。</div>
<div class="muted muted-xs">
<a class="link" href="{!! $safeFullUrlWithQuery(['receipt_status' => 'has', 'page' => null]) !!}">查看有回执订单</a>
@include('admin.platform_orders._summary_text_link', [
'role' => 'po-summary-link-go-add-payment-receipt',
'href' => '#add-payment-receipt',
'label' => '去补回执',
'ariaLabel' => '直达补回执面板',
'title' => '直达补回执面板',
])
<span class="muted"></span>
@include('admin.platform_orders._summary_text_link', [
'role' => 'po-summary-link-view-receipt-orders',
'href' => $safeFullUrlWithQuery(['receipt_status' => 'has', 'page' => null]),
'label' => '查看有回执订单',
])
</div>
</div>
<div class="card">
@@ -756,22 +893,38 @@
<div class="muted muted-xs">
建议按以下顺序治理当前筛选集合:
<ol class="muted-muted-xs list-indent">
<li>先处理 <a class="link" href="{!! $safeFullUrlWithQuery(['reconcile_mismatch' => '1', 'page' => null]) !!}">对账不一致</a>:优先补齐支付回执,并核对回执渠道/金额,确保回执总额与已付金额一致。</li>
<li>再处理 <a class="link" href="{!! $safeFullUrlWithQuery(['refund_inconsistent' => '1', 'page' => null]) !!}">退款不一致</a>:优先补齐退款回执/核对退款轨迹/修正退款状态(带审计)。</li>
<li>最后处理 <a class="link" href="{!! $safeFullUrlWithQuery(['syncable_only' => '1', 'sync_status' => 'unsynced', 'page' => null]) !!}">可同步</a>:确认无对账/退款异常、无同步失败原因后,再批量同步订阅。</li>
<li>先处理 @include('admin.platform_orders._summary_text_link', [
'role' => 'po-sop-link-reconcile-mismatch',
'href' => $safeFullUrlWithQuery(['reconcile_mismatch' => '1', 'page' => null]),
'label' => '对账不一致',
]):优先补齐支付回执,并核对回执渠道/金额,确保回执总额与已付金额一致。</li>
<li>再处理 @include('admin.platform_orders._summary_text_link', [
'role' => 'po-sop-link-refund-inconsistent',
'href' => $safeFullUrlWithQuery(['refund_inconsistent' => '1', 'page' => null]),
'label' => '退款不一致',
]):优先补齐退款回执/核对退款轨迹/修正退款状态(带审计)。</li>
<li>最后处理 @include('admin.platform_orders._summary_text_link', [
'role' => 'po-sop-link-syncable',
'href' => $safeFullUrlWithQuery(['syncable_only' => '1', 'sync_status' => 'unsynced', 'page' => null]),
'label' => '可同步',
]):确认无对账/退款异常、无同步失败原因后,再批量同步订阅。</li>
</ol>
</div>
<div class="muted governance-block-footnote">说明:本页“批量同步/批量生效/清理失败标记”等工具动作会透传当前筛选条件;建议先缩小到明确集合再操作。</div>
<div class="muted governance-block-footnote">提示:如果你是从其它页面(例如订阅详情/套餐页)通过 back 进入本页,建议优先用上方的「返回上一页」入口回到来源页,再继续操作。</div>
</div>
<div class="card">
<div class="card" id="po-summary-card-reconcile-mismatch">
<h3>对账差额</h3>
@php $delta = (float) ($summaryStats['reconciliation_delta'] ?? 0); @endphp
<div class="metric-number">¥{{ number_format($delta, 2) }}</div>
<div class="muted muted-xs">{{ $summaryStats['reconciliation_delta_note'] ?? '回执总额 - 订单已付总额' }}(当前筛选范围)</div>
<div class="muted muted-xs">对账不一致订单:
<a class="link" href="{!! $safeFullUrlWithQuery(['reconcile_mismatch' => '1', 'page' => null]) !!}">{{ $summaryStats['reconcile_mismatch_orders'] ?? 0 }}</a>
@include('admin.platform_orders._summary_metric_link', [
'role' => 'po-summary-link-reconcile-mismatch-orders',
'href' => $safeFullUrlWithQuery(['reconcile_mismatch' => '1', 'page' => null]),
'label' => $summaryStats['reconcile_mismatch_orders'] ?? 0,
])
</div>
@php $tol = (float) config('saasshop.amounts.tolerance', 0.01); @endphp
<div class="muted muted-xs">当前容差:¥{{ number_format($tol, 2) }}</div>
@@ -782,7 +935,11 @@
<div class="card">
<h3>退款不一致订单</h3>
<div class="metric-number">
<a class="link" href="{!! $safeFullUrlWithQuery(['refund_inconsistent' => '1', 'page' => null]) !!}">{{ $summaryStats['refund_inconsistent_orders'] ?? 0 }}</a>
@include('admin.platform_orders._summary_metric_link', [
'role' => 'po-summary-link-refund-inconsistent-orders',
'href' => $safeFullUrlWithQuery(['refund_inconsistent' => '1', 'page' => null]),
'label' => $summaryStats['refund_inconsistent_orders'] ?? 0,
])
</div>
@php $refundTol = (float) config('saasshop.amounts.tolerance', 0.01); @endphp
<div class="muted muted-xs">口径:状态=refunded 但退款总额 + 容差 < 已付;或状态!=refunded 且退款总额 >= 已付 + 容差</div>
@@ -811,14 +968,23 @@
<span class="muted"></span>
<span class="muted">原因过长,请复制到筛选框</span>
<span class="muted"></span>
<a class="link" href="{!! $safeFullUrlWithQuery(['sync_status' => 'failed', 'page' => null]) !!}">进入同步失败集合</a>
@include('admin.platform_orders._summary_text_link', [
'href' => $safeFullUrlWithQuery(['sync_status' => 'failed', 'page' => null]),
'label' => '进入同步失败集合',
])
@else
<a class="link" title="{{ $reason }}" href="{!! $safeFullUrlWithQuery(['sync_error_keyword' => $reason, 'sync_status' => 'failed', 'page' => null]) !!}">{{ $reasonText }}</a>
<span class="muted">{{ $count }}</span>
<span class="muted"></span>
<a class="link" href="{!! $safeFullUrlWithQuery(['sync_error_keyword' => $reason, 'sync_status' => 'unsynced', 'syncable_only' => '1', 'page' => null]) !!}">切到可同步重试</a>
@include('admin.platform_orders._summary_text_link', [
'href' => $safeFullUrlWithQuery(['sync_error_keyword' => $reason, 'sync_status' => 'unsynced', 'syncable_only' => '1', 'fail_only' => null, 'page' => null]),
'label' => '切到可同步重试',
])
<span class="muted"></span>
<a class="link" href="{!! $safeFullUrlWithQuery(['sync_error_keyword' => $reason, 'sync_status' => 'failed', 'page' => null]) !!}">进入失败集合</a>
@include('admin.platform_orders._summary_text_link', [
'href' => $safeFullUrlWithQuery(['sync_error_keyword' => $reason, 'sync_status' => 'failed', 'page' => null]),
'label' => '进入失败集合',
])
@endif
@else
<span class="muted">(空原因)</span>
@@ -855,14 +1021,23 @@
<span class="muted"></span>
<span class="muted">原因过长,请复制到筛选框</span>
<span class="muted"></span>
<a class="link" href="{!! $safeFullUrlWithQuery(['bmpa_failed_only' => '1', 'bmpa_error_keyword' => null, 'page' => null]) !!}">进入失败集合</a>
@include('admin.platform_orders._summary_text_link', [
'href' => $safeFullUrlWithQuery(['bmpa_failed_only' => '1', 'bmpa_error_keyword' => null, 'page' => null]),
'label' => '进入失败集合',
])
@else
<a class="link" title="{{ $reason }}" href="{!! $safeFullUrlWithQuery(['bmpa_error_keyword' => $reason, 'page' => null]) !!}">{{ $reasonText }}</a>
<a class="link" title="{{ $reason }}" href="{!! $safeFullUrlWithQuery(['bmpa_failed_only' => '1', 'bmpa_error_keyword' => $reason, 'page' => null]) !!}">{{ $reasonText }}</a>
<span class="muted">{{ $count }}</span>
<span class="muted"></span>
<a class="link" href="{!! $safeFullUrlWithQuery(['bmpa_error_keyword' => $reason, 'bmpa_processable_only' => '1', 'page' => null]) !!}">切到可处理集合重试</a>
@include('admin.platform_orders._summary_text_link', [
'href' => $safeFullUrlWithQuery(['bmpa_error_keyword' => $reason, 'bmpa_processable_only' => '1', 'bmpa_failed_only' => null, 'page' => null]),
'label' => '切到可处理集合重试',
])
<span class="muted"></span>
<a class="link" href="{!! $safeFullUrlWithQuery(['bmpa_failed_only' => '1', 'bmpa_error_keyword' => $reason, 'page' => null]) !!}">进入失败集合</a>
@include('admin.platform_orders._summary_text_link', [
'href' => $safeFullUrlWithQuery(['bmpa_failed_only' => '1', 'bmpa_error_keyword' => $reason, 'page' => null]),
'label' => '进入失败集合',
])
@endif
@else
<span class="muted">(空原因)</span>
@@ -874,11 +1049,11 @@
@else
<div class="muted">暂无失败原因聚合数据</div>
@endif
<div class="muted muted-xs mt-6">提示:建议先点原因进入失败集合,完成回执/退款治理后,再切到 pending+unpaid 集合重试。</div>
<div class="muted muted-xs mt-6">提示:建议先点原因进入失败集合,完成回执/退款治理后,再切到「可BMPA处理」集合重试。</div>
</div>
</div>
<div class="card mb-20">
<div class="card mb-20" id="po-tools-section">
<h3>工具</h3>
<div class="muted mb-10">清除仅影响订单 meta 中的失败标记,不改变订单/订阅状态。</div>
@@ -899,17 +1074,28 @@
<div class="muted governance-block-body">
当前筛选包含
@if($hasReconcileMismatchFilter)
<a class="link" href="{!! $safeFullUrlWithQuery(['reconcile_mismatch' => '1', 'page' => null]) !!}">对账不一致</a>
@include('admin.platform_orders._summary_text_link', [
'href' => $safeFullUrlWithQuery(['reconcile_mismatch' => '1', 'page' => null]),
'label' => '对账不一致',
])
@endif
@if($hasReconcileMismatchFilter && $hasRefundInconsistentFilter)
<span class="muted"></span>
@endif
@if($hasRefundInconsistentFilter)
<a class="link" href="{!! $safeFullUrlWithQuery(['refund_inconsistent' => '1', 'page' => null]) !!}">退款不一致</a>
@include('admin.platform_orders._summary_text_link', [
'href' => $safeFullUrlWithQuery(['refund_inconsistent' => '1', 'page' => null]),
'label' => '退款不一致',
])
@endif
。建议先完成金额/状态治理(补回执/核对退款/修正状态)后,再执行批量同步订阅等工具动作。
<div class="mt-6 actions gap-10">
<a class="btn btn-secondary btn-sm" href="#batch-activate-subscriptions">定位到批量同步订阅工具</a>
@include('admin.platform_orders._tool_anchor_button', [
'role' => 'po-tools-paid-no-receipt-focus-batch-activate',
'href' => '#batch-activate-subscriptions',
'label' => '定位到批量同步订阅工具',
'ariaLabel' => '定位到批量同步订阅工具',
])
</div>
</div>
</div>
@@ -920,7 +1106,11 @@
<div class="muted governance-block-body">
注意:当前同时勾选了「只看可同步」—— 这类订单会被批量同步订阅命中。若仍存在对账/退款异常,建议先进入治理集合处理完毕,再回到可同步集合执行批量同步。
<div class="mt-6 actions gap-10">
<a class="btn btn-secondary btn-sm" href="{!! $safeFullUrlWithQuery(['syncable_only' => null, 'page' => null]) !!}">先去治理(取消只看可同步)</a>
@include('admin.platform_orders._summary_text_link', [
'class' => 'btn btn-secondary btn-sm',
'href' => $safeFullUrlWithQuery(['syncable_only' => null, 'page' => null]),
'label' => '先去治理(取消只看可同步)',
])
</div>
</div>
</div>
@@ -928,17 +1118,37 @@
@endif
@if((($filters['receipt_status'] ?? '') === 'none') && $hasSyncableOnlyFilter)
<div class="card governance-block mb-10">
<div class="muted text-danger governance-block-title"><strong>回执缺失提示</strong></div>
<div class="card governance-block mb-10" data-role="po-tools-paid-no-receipt-hint">
<div data-role="po-tools-paid-no-receipt-hint" aria-label="已付无回执治理提示">
<div class="muted text-danger governance-block-title"><strong>已付无回执提示</strong></div>
<div class="muted governance-block-body">
当前集合为「无回执」且已勾选「只看可同步」。为保证收费闭环可治理,建议先补齐支付回执留痕,再执行批量同步订阅。
当前集合为「已付无回执」且已勾选「只看可同步」。为保证收费闭环可治理,建议先补齐支付回执留痕,再执行批量同步订阅。
<div class="mt-6 actions gap-10">
<a class="btn btn-secondary btn-sm" href="#batch-activate-subscriptions">定位到批量同步订阅工具</a>
@include('admin.platform_orders._tool_anchor_button', [
'role' => 'po-tools-paid-no-receipt-focus-batch-activate',
'href' => '#batch-activate-subscriptions',
'label' => '定位到批量同步订阅工具',
'ariaLabel' => '定位到批量同步订阅工具',
])
</div>
<div class="mt-6 actions gap-10">
<a class="btn btn-secondary btn-sm" href="{!! $safeFullUrlWithQuery(['receipt_status' => 'has', 'page' => null]) !!}">切到有回执集合</a>
<a class="btn btn-secondary btn-sm" href="{!! $safeFullUrlWithQuery(['syncable_only' => null, 'page' => null]) !!}">取消只看可同步(先治理)</a>
@include('admin.platform_orders._summary_text_link', [
'role' => 'po-tools-paid-no-receipt-go-receipt-has',
'class' => 'btn btn-secondary btn-sm',
'href' => $safeFullUrlWithQuery(['receipt_status' => 'has', 'page' => null]),
'label' => '切到有回执集合',
'ariaLabel' => '切到有回执集合',
])
@include('admin.platform_orders._summary_text_link', [
'role' => 'po-tools-paid-no-receipt-clear-syncable',
'class' => 'btn btn-secondary btn-sm',
'href' => $safeFullUrlWithQuery(['syncable_only' => null, 'page' => null]),
'label' => '取消只看可同步(先治理)',
'ariaLabel' => '取消只看可同步(先治理)',
])
</div>
<div class="muted governance-block-footnote" data-role="po-tools-paid-no-receipt-footnote" aria-label="已付无回执治理优先级说明">收费闭环优先:先补回执,再推进同步。</div>
</div>
</div>
</div>
@endif
@@ -949,11 +1159,34 @@
<div class="muted governance-block-body">
当前筛选包含「同步失败/失败原因」范围。建议先治理失败原因(修复数据或重试同步),再执行批量同步订阅等工具动作。
<div class="mt-6 actions gap-10">
<a class="btn btn-secondary btn-sm" href="#batch-activate-subscriptions">定位到批量同步订阅工具</a>
@include('admin.platform_orders._tool_anchor_button', [
'role' => 'po-tools-paid-no-receipt-focus-batch-activate',
'href' => '#batch-activate-subscriptions',
'label' => '定位到批量同步订阅工具',
'ariaLabel' => '定位到批量同步订阅工具',
])
</div>
<div class="mt-6 actions gap-10">
<a class="btn btn-secondary btn-sm" href="{!! $safeFullUrlWithQuery(['sync_status' => 'failed', 'page' => null]) !!}">进入同步失败集合</a>
<a class="btn btn-secondary btn-sm" href="{!! $safeFullUrlWithQuery(['syncable_only' => '1', 'sync_status' => 'unsynced', 'page' => null]) !!}">切到只看可同步(用于批量重试同步)</a>
@include('admin.platform_orders._summary_text_link', [
'class' => 'btn btn-secondary btn-sm',
'href' => $safeFullUrlWithQuery([
'sync_status' => 'failed',
// 进入失败集合:不应残留“只看可同步”等互斥开关
'syncable_only' => null,
'synced_only' => null,
'fail_only' => null,
// 从 BMPA 失败上下文切回同步失败集合时,需清理冲突开关
'bmpa_failed_only' => null,
'bmpa_error_keyword' => null,
'page' => null,
]),
'label' => '进入同步失败集合',
])
@include('admin.platform_orders._summary_text_link', [
'class' => 'btn btn-secondary btn-sm',
'href' => $safeFullUrlWithQuery(['syncable_only' => '1', 'sync_status' => 'unsynced', 'fail_only' => null, 'page' => null]),
'label' => '切到只看可同步(用于批量重试同步)',
])
</div>
</div>
</div>
@@ -965,8 +1198,26 @@
<div class="muted governance-block-body">
当前筛选包含「批量标记支付并生效失败/失败原因」范围。建议先补齐回执/核对退款/修正状态后再切到「可BMPA处理」集合重试批量标记支付。
<div class="mt-6 actions gap-10">
<a class="btn btn-secondary btn-sm" href="{!! $safeFullUrlWithQuery(['bmpa_failed_only' => '1', 'page' => null]) !!}">进入批量标记支付失败集合</a>
<a class="btn btn-secondary btn-sm" href="{!! $safeFullUrlWithQuery(['bmpa_processable_only' => '1', 'page' => null]) !!}">切到可BMPA处理用于重试</a>
@include('admin.platform_orders._summary_text_link', [
'class' => 'btn btn-secondary btn-sm',
'href' => $safeFullUrlWithQuery([
'bmpa_failed_only' => '1',
// 进入失败集合:不应残留“只看可同步”等互斥开关
'syncable_only' => null,
'synced_only' => null,
// 从同步失败上下文切到 BMPA 失败集合时,需清理冲突开关
'fail_only' => null,
'sync_status' => null,
'sync_error_keyword' => null,
'page' => null,
]),
'label' => '进入批量标记支付失败集合',
])
@include('admin.platform_orders._summary_text_link', [
'class' => 'btn btn-secondary btn-sm',
'href' => $safeFullUrlWithQuery(['bmpa_processable_only' => '1', 'bmpa_failed_only' => null, 'page' => null]),
'label' => '切到可BMPA处理用于重试',
])
</div>
</div>
</div>
@@ -977,8 +1228,21 @@
$toolGuards = $toolGuards ?? \App\Support\PlatformOrderToolsGuard::forIndex((array) ($filters ?? []));
@endphp
<div class="tool-grid">
<div class="tool-group focus-box">
<div class="tool-grid" id="po-tools-grid" data-role="po-tools-grid" aria-label="平台订单工具区网格">
<div class="muted muted-xs mb-10" data-role="po-tools-grid-intro" aria-label="平台订单工具区说明">当前工具区承载导出、批量治理与失败标记清理,请先缩小筛选范围再执行动作。<span class="sr-only">当前工具区承载导出、批量治理与失败标记清理,请先缩小筛选范围再执行动作;也可使用上方</span>也可使用上方<span class="sr-only">也可使用上方快速定位导航直达目标工具组。</span><a class="link" data-role="po-tools-grid-intro-go-page-jumps" aria-label="回到页内导航" title="回到页内导航查看各区块跳转入口" href="#po-page-jump-links">快速定位导航</a>直达目标工具组。</div>
<div class="actions gap-10 mb-10" data-role="po-tools-grid-jump-links" aria-label="平台订单工具区快速定位导航">
<span class="muted muted-xs" data-role="po-tools-grid-jump-links-prefix">快速定位:</span>
<a class="btn btn-secondary btn-sm" data-role="po-tools-jump-summary" aria-label="定位到订单摘要概览" href="#po-summary-cards">摘要概览</a>
<a class="btn btn-secondary btn-sm" data-role="po-tools-jump-filters" aria-label="定位到筛选条件" href="#filters">筛选条件</a>
<a class="btn btn-secondary btn-sm" data-role="po-tools-jump-export" aria-label="定位到导出工具组" href="#po-tools-export">导出</a>
<a class="btn btn-secondary btn-sm" data-role="po-tools-jump-batch-activate" aria-label="定位到批量同步订阅工具组" href="#batch-activate-subscriptions">批量同步</a>
<span class="muted muted-xs" data-role="po-tools-grid-jump-links-note" aria-label="平台订单工具区快速导航说明" title="快速导航仅负责定位工具组,不会直接执行批量动作">快速定位只负责跳转到工具组,不会直接执行动作。<span class="sr-only">快速定位只负责跳转到工具组,不会直接执行动作;如需先调整范围,可先</span>如需先调整范围,可先<a class="link" data-role="po-tools-grid-jump-links-go-filters" aria-label="回到筛选条件" title="先回筛选再执行工具动作" href="#filters">回到筛选条件</a>,或<a class="link" data-role="po-tools-grid-jump-links-go-summary" aria-label="回到摘要概览" title="先回摘要概览查看当前盘面" href="#po-summary-cards">回到摘要概览</a></span>
<a class="btn btn-secondary btn-sm" data-role="po-tools-jump-batch-bmpa" aria-label="定位到批量标记支付并生效工具组" href="#po-tools-batch-bmpa">批量BMPA</a>
<a class="btn btn-secondary btn-sm" data-role="po-tools-jump-batch-mark-activated" aria-label="定位到批量仅标记为已生效工具组" href="#po-tools-batch-mark-activated">批量仅生效</a>
<a class="btn btn-secondary btn-sm" data-role="po-tools-jump-clear-sync-errors" aria-label="定位到清理同步失败标记工具组" href="#po-tools-clear-sync-errors">清理同步失败</a>
<a class="btn btn-secondary btn-sm" data-role="po-tools-jump-clear-bmpa-errors" aria-label="定位到清理批量BMPA失败标记工具组" href="#po-tools-clear-bmpa-errors">清理BMPA失败</a>
</div>
<div class="tool-group focus-box" id="po-tools-export" data-role="po-tools-group-export" aria-label="平台订单导出工具组">
<div class="tool-group-title">导出</div>
<form method="get" action="/admin/platform-orders/export" class="mb-0">
<input type="hidden" name="download" value="1">
@@ -995,7 +1259,7 @@
</form>
</div>
<div class="tool-group focus-box" id="batch-activate-subscriptions">
<div class="tool-group focus-box" id="batch-activate-subscriptions" data-role="po-tools-group-batch-activate-subscriptions" aria-label="平台订单批量同步订阅工具组">
<div class="tool-group-title">批量同步订阅</div>
@php
$batchActivateBlocked = (bool) ($toolGuards['batch_activate_subscriptions']['blocked'] ?? false);
@@ -1051,7 +1315,7 @@
</form>
</div>
<div class="tool-group focus-box">
<div class="tool-group focus-box" id="po-tools-batch-bmpa" data-role="po-tools-group-batch-bmpa" aria-label="平台订单批量标记支付并生效工具组">
<div class="tool-group-title">批量标记支付并生效BMPA</div>
@php
$batchBmpaBlocked = (bool) ($toolGuards['batch_bmpa']['blocked'] ?? false);
@@ -1106,7 +1370,7 @@
</form>
</div>
<div class="tool-group focus-box">
<div class="tool-group focus-box" id="po-tools-batch-mark-activated" data-role="po-tools-group-batch-mark-activated" aria-label="平台订单批量仅标记为已生效工具组">
<div class="tool-group-title">批量仅标记为已生效</div>
@php
$batchMarkActivatedBlocked = (bool) ($toolGuards['batch_mark_activated']['blocked'] ?? false);
@@ -1164,7 +1428,7 @@
</form>
</div>
<div class="tool-group focus-box">
<div class="tool-group focus-box" id="po-tools-clear-sync-errors" data-role="po-tools-group-clear-sync-errors" aria-label="平台订单清理同步失败标记工具组">
<div class="tool-group-title">清理失败标记:同步订阅</div>
@php
$clearSyncBlocked = (bool) ($toolGuards['clear_sync_errors']['blocked'] ?? false);
@@ -1181,6 +1445,8 @@
// 提效:清理同步失败标记前必须先锁定失败集合;被阻断时给一键跳转入口。
$goSyncFailedUrl = $buildQuickFilterUrl([
'sync_status' => 'failed',
// 从 BMPA 失败上下文被阻断时,跳转到同步失败集合应清理 BMPA 开关,避免口径冲突导致空结果。
'bmpa_failed_only' => null,
]);
@endphp
@include('admin.components.tool_blocked_hint', [
@@ -1210,7 +1476,7 @@
</form>
</div>
<div class="tool-group focus-box">
<div class="tool-group focus-box" id="po-tools-clear-bmpa-errors" data-role="po-tools-group-clear-bmpa-errors" aria-label="平台订单清理批量BMPA失败标记工具组">
<div class="tool-group-title">清理失败标记:批量 BMPA</div>
@php
$clearBmpaBlocked = (bool) ($toolGuards['clear_bmpa_errors']['blocked'] ?? false);
@@ -1227,6 +1493,8 @@
// 提效:清理 BMPA 失败标记前必须先锁定 BMPA 失败集合;被阻断时给一键跳转入口。
$goBmpaFailedUrl = $buildQuickFilterUrl([
'bmpa_failed_only' => '1',
// 从其它治理上下文被阻断时,跳转到 BMPA 失败集合应清理同步失败开关,避免口径冲突导致空结果。
'fail_only' => null,
]);
@endphp
@include('admin.components.tool_blocked_hint', [
@@ -1256,11 +1524,9 @@
</form>
</div>
</div>
</div>
<div class="card list-card">
<div class="list-card-header">
<div class="list-card-header" id="po-list-card">
<div>
<h3 class="list-card-title">平台订单列表</h3>
</div>
@@ -1367,7 +1633,7 @@
@endphp
@if($order->payment_status === 'paid' && ! $hasReceiptEvidenceRow)
<div class="muted text-danger muted-xs row-warn">
<span class="row-warn-prefix">无回执</span>
<span class="row-warn-prefix">已付无回执</span>
<span class="muted"></span>
<a class="link" href="{!! $noReceiptFixUrlRow !!}">去补回执</a>
</div>
@@ -1477,14 +1743,58 @@
<span class="muted">-</span>
@else
@if($syncErrMsg !== '')
@php
// 同步失败:默认落到“可执行集合”(优先同批次)
$basRunIdRow = (string) (data_get($order->meta, 'batch_activation.last_result.run_id') ?? '');
if ($basRunIdRow === '') {
$basRunIdRow = (string) (data_get($order->meta, 'batch_activation.run_id') ?? '');
}
$syncGoFailedUrl = $buildQuickFilterUrl([
'sync_status' => 'failed',
'page' => null,
]);
$syncGoBatchFailedUrl = '';
$syncGoBatchReasonUrl = '';
if ($basRunIdRow !== '') {
$syncGoBatchFailedUrl = $buildQuickFilterUrl([
'batch_activation_run_id' => $basRunIdRow,
'sync_status' => 'failed',
'page' => null,
]);
if (! $syncErrTooLong) {
$syncGoBatchReasonUrl = $buildQuickFilterUrl([
'batch_activation_run_id' => $basRunIdRow,
'sync_status' => 'failed',
'sync_error_keyword' => $syncErrMsg,
'page' => null,
]);
}
}
$syncGoReasonUrl = $buildQuickFilterUrl([
'sync_error_keyword' => $syncErrMsg,
'sync_status' => 'failed',
'page' => null,
]);
$syncErrorLinkUrl = $syncGoBatchReasonUrl !== '' ? $syncGoBatchReasonUrl : $syncGoReasonUrl;
@endphp
<div>
<span class="muted muted-xs">同步:</span>
@if($syncErrTooLong)
<span class="muted text-danger" title="{{ $syncErrMsg }}">{{ mb_substr($syncErrMsg, 0, $SYNC_FAILED_REASON_TRUNCATE_LEN) }}</span>
<div class="muted text-danger muted-xs">原因过长,请复制到筛选框</div>
<a class="link" href="{!! $safeFullUrlWithQuery(['sync_status' => 'failed', 'page' => null]) !!}">进入同步失败集合</a>
@if($syncGoBatchFailedUrl !== '')
<div class="muted muted-xs">治理:<a class="link" href="{!! $syncGoBatchFailedUrl !!}">进入本批次同步失败集合</a></div>
@else
<div class="muted muted-xs">治理:<a class="link" href="{!! $syncGoFailedUrl !!}">进入同步失败集合</a></div>
@endif
@else
<a class="link text-danger" href="{!! $safeFullUrlWithQuery(['sync_error_keyword' => $syncErrMsg, 'sync_status' => 'failed', 'page' => null]) !!}">{{ mb_substr($syncErrMsg, 0, $SYNC_FAILED_REASON_TRUNCATE_LEN) }}</a>
<a class="link text-danger" href="{!! $syncErrorLinkUrl !!}">{{ mb_substr($syncErrMsg, 0, $SYNC_FAILED_REASON_TRUNCATE_LEN) }}</a>
@endif
</div>
@endif
@@ -1529,7 +1839,7 @@
<div class="muted muted-xs">治理:<a class="link" href="{!! $bmpaGoFailedUrl !!}">进入 BMPA 失败集合</a></div>
@endif
@else
<a class="link text-danger" href="{!! $safeFullUrlWithQuery(['bmpa_error_keyword' => $bmpaErrMsg, 'page' => null]) !!}">{{ mb_substr($bmpaErrMsg, 0, $SYNC_FAILED_REASON_TRUNCATE_LEN) }}</a>
<a class="link text-danger" href="{!! $safeFullUrlWithQuery(['bmpa_failed_only' => '1', 'bmpa_error_keyword' => $bmpaErrMsg, 'page' => null]) !!}">{{ mb_substr($bmpaErrMsg, 0, $SYNC_FAILED_REASON_TRUNCATE_LEN) }}</a>
@if($bmpaGoBatchReasonUrl !== '')
<div class="muted muted-xs">治理:<a class="link" href="{!! $bmpaGoBatchReasonUrl !!}">本批次按原因筛选</a></div>
@@ -1765,7 +2075,8 @@
@endphp
@if($receiptCount > 0)
@php
$receiptCountShowUrl = \App\Support\BackUrl::withBackAndFragment('/admin/platform-orders/' . $order->id, $selfWithoutBack, 'add-payment-receipt');
// spot-check有回执时优先直达“回执列表区”#payment-receipts而非“新增回执表单”。
$receiptCountShowUrl = \App\Support\BackUrl::withBackAndFragment('/admin/platform-orders/' . $order->id, $selfWithoutBack, 'payment-receipts');
@endphp
<a href="{!! $receiptCountShowUrl !!}" class="muted">{{ $receiptCount }}</a>
@else
@@ -1780,7 +2091,8 @@
@endphp
@if($refundCount > 0)
@php
$refundCountShowUrl = \App\Support\BackUrl::withBackAndFragment('/admin/platform-orders/' . $order->id, $selfWithoutBack, 'add-refund-receipt');
// spot-check有退款记录时优先直达“退款记录区”#refund-receipts而非“新增退款表单”。
$refundCountShowUrl = \App\Support\BackUrl::withBackAndFragment('/admin/platform-orders/' . $order->id, $selfWithoutBack, 'refund-receipts');
@endphp
<a href="{!! $refundCountShowUrl !!}" class="muted">{{ $refundCount }}</a>
@else

View File

@@ -90,6 +90,8 @@
<a href="{!! $buildQuickFilterUrl(['status' => null, 'expiry' => 'expired']) !!}" class="muted">已过期</a>
<span class="muted"></span>
<a href="{!! $buildQuickFilterUrl(['status' => null, 'expiry' => 'expiring_7d']) !!}" class="muted">7天内到期</a>
<span class="muted"></span>
<a href="{!! $buildQuickFilterUrl(['payment_status' => 'paid', 'receipt_status' => 'none']) !!}" class="muted">已付无回执</a>
</div>
</div>

View File

@@ -150,6 +150,7 @@
<div class="mt-6 actions gap-10">
{{-- 去重降噪:下方摘要卡/表格头部已提供“全部订单/可同步”等跳转入口,这里仅保留治理相关入口与下单入口 --}}
<a class="btn btn-secondary btn-sm" href="{!! $makePlatformOrderUrl(['merchant_id' => $subscription->merchant_id, 'plan_id' => $subscription->plan_id, 'renewal_missing_subscription' => '1']) !!}">查看续费缺订阅订单(同站点/同套餐)</a>
<a class="btn btn-secondary btn-sm" href="{!! $makePlatformOrderUrl(['merchant_id' => $subscription->merchant_id, 'plan_id' => $subscription->plan_id, 'payment_status' => 'paid', 'receipt_status' => 'none']) !!}">查看已付无回执订单(同站点/同套餐)</a>
@php
$createRenewalOrderUrl = '/admin/platform-orders/create?' . \Illuminate\Support\Arr::query([
'merchant_id' => $subscription->merchant_id,
@@ -260,9 +261,13 @@
</div>
<div class="muted muted-xs">点击订单数可跳转:该订阅下「有回执」订单</div>
<div class="muted muted-xs">
无回执订单:
无回执订单(广义)
<a class="link" href="{!! $makePlatformOrderUrl(['site_subscription_id' => $subscription->id, 'receipt_status' => 'none']) !!}">{{ $summaryStats['no_receipt_orders'] ?? 0 }}</a>
</div>
<div class="muted muted-xs">
已付无回执订单:
<a class="link" href="{!! $makePlatformOrderUrl(['site_subscription_id' => $subscription->id, 'payment_status' => 'paid', 'receipt_status' => 'none']) !!}">{{ $summaryStats['no_receipt_orders'] ?? 0 }}</a>
</div>
</div>
<div class="card">

View File

@@ -149,6 +149,7 @@ Route::prefix('admin')->group(function () {
Route::post('/plans', [PlanController::class, 'store']);
// 注意:必须放在 /plans/{plan} 之前,避免被参数路由吞掉导致 404
Route::post('/plans/seed-defaults', [PlanController::class, 'seedDefaults']);
Route::get('/plans/{plan}', [PlanController::class, 'show']);
Route::get('/plans/{plan}/edit', [PlanController::class, 'edit']);
Route::post('/plans/{plan}', [PlanController::class, 'update']);
Route::post('/plans/{plan}/set-status', [PlanController::class, 'setStatus']);

172
scripts/build_oneclick_bundle.sh Executable file
View File

@@ -0,0 +1,172 @@
#!/usr/bin/env bash
set -euo pipefail
# 构建“一键部署包”(代码 + vendor + 加密数据库快照)
# 目标:下载压缩包 → 解压 → 运行 install.sh → 即可恢复到最新数据并可访问。
#
# 依赖git / tar / gzip
# 可选composer仅当需要现装 vendor
#
# 用法:
# bash scripts/build_oneclick_bundle.sh
# 输出:
# /app/working/dist/saasshop_bundle_<timestamp>.tar.gz
# /app/working/dist/saasshop_bundle_latest.tar.gz
REPO_DIR=$(cd "$(dirname "$0")/.." && pwd)
cd "$REPO_DIR"
DIST_DIR="/app/working/dist"
mkdir -p "$DIST_DIR"
DATA_REPO_SSH=""
if [[ -f /app/working.secret/saasshop_data_repo_ssh ]]; then
DATA_REPO_SSH=$(cat /app/working.secret/saasshop_data_repo_ssh)
fi
if [[ "$DATA_REPO_SSH" == "" ]]; then
echo "缺少数据仓地址:/app/working.secret/saasshop_data_repo_ssh"
exit 31
fi
STAMP=$(date +%Y%m%d_%H%M%S)
BUNDLE_ROOT=$(mktemp -d)
trap 'rm -rf "$BUNDLE_ROOT" || true' EXIT
APP_DIR="$BUNDLE_ROOT/saasshop"
mkdir -p "$APP_DIR"
# 1) 打包代码(不带 .git
echo "[bundle] copying app files ..."
# 使用 tar 管道复制(比 cp -a 更容易排除)
tar \
--exclude=".git" \
--exclude="node_modules" \
--exclude="storage/logs" \
--exclude="storage/framework/cache" \
--exclude="storage/framework/sessions" \
--exclude="storage/framework/views" \
--exclude="storage/app/private" \
--exclude=".env" \
-cf - . | (cd "$APP_DIR" && tar -xf -)
# 2) 确保 vendor 存在(无构建链/傻瓜部署,优先直接随包携带)
if [[ ! -d "$APP_DIR/vendor" ]]; then
echo "[bundle] vendor 不存在,尝试 composer install --no-dev ..."
if command -v composer >/dev/null 2>&1; then
(cd "$APP_DIR" && composer install --no-dev --prefer-dist --no-interaction)
else
echo "[bundle] composer 不存在且 vendor 缺失,无法构建傻瓜包"
exit 32
fi
fi
# 3) 拉取数据仓最新快照
echo "[bundle] fetching latest encrypted snapshot from data repo ..."
DATA_TMP="$BUNDLE_ROOT/data_repo"
git clone "$DATA_REPO_SSH" "$DATA_TMP" >/dev/null
if [[ ! -f "$DATA_TMP/snapshots/latest.sql.gz.enc" ]]; then
echo "数据仓缺少 snapshots/latest.sql.gz.enc"
exit 33
fi
mkdir -p "$APP_DIR/bundle/snapshots"
cp -f "$DATA_TMP/snapshots/latest.sql.gz.enc" "$APP_DIR/bundle/snapshots/latest.sql.gz.enc"
if [[ -f "$DATA_TMP/snapshots/manifest.json" ]]; then
cp -f "$DATA_TMP/snapshots/manifest.json" "$APP_DIR/bundle/snapshots/manifest.json"
fi
# 4) 写入 install.sh真正一键入口
cat > "$APP_DIR/install.sh" <<'EOF'
#!/usr/bin/env bash
set -euo pipefail
# SaaSShop 一键部署脚本(随 bundle 一起发放)
# 用法:
# export SAASSHOP_DB_SNAPSHOT_KEY='解密密钥'
# bash install.sh
#
# 你也可以先准备好 .env推荐脚本会优先读取项目根目录 .env。
APP_DIR=$(cd "$(dirname "$0")" && pwd)
cd "$APP_DIR"
if [[ ! -f .env ]]; then
if [[ -f .env.example ]]; then
cp .env.example .env
echo "[install] 已生成 .env来自 .env.example请按需修改 DB_* / APP_URL 等配置。"
else
echo "[install] 缺少 .env 与 .env.example无法继续"
exit 41
fi
fi
# 读取 .env仅用于 DB 连接)
set -a
# shellcheck disable=SC1091
source .env
set +a
DB_HOST=${DB_HOST:-127.0.0.1}
DB_PORT=${DB_PORT:-3306}
DB_DATABASE=${DB_DATABASE:-appdb}
DB_USERNAME=${DB_USERNAME:-appuser}
DB_PASSWORD=${DB_PASSWORD:-}
if [[ "${SAASSHOP_DB_SNAPSHOT_KEY:-}" == "" ]]; then
echo "[install] 缺少 SAASSHOP_DB_SNAPSHOT_KEY无法解密并恢复完整数据。"
echo "[install] 请先export SAASSHOP_DB_SNAPSHOT_KEY='...密钥...'"
exit 42
fi
# 权限准备
mkdir -p storage bootstrap/cache
chmod -R ug+rwX storage bootstrap/cache || true
# APP_KEY
if ! grep -q '^APP_KEY=' .env || [[ "${APP_KEY:-}" == "" ]]; then
echo "[install] 生成 APP_KEY ..."
php artisan key:generate --force
fi
echo "[install] 导入最新数据库快照 ..."
ENC_FILE="bundle/snapshots/latest.sql.gz.enc"
if [[ ! -f "$ENC_FILE" ]]; then
echo "[install] 缺少快照文件:$ENC_FILE"
exit 43
fi
TMP_DIR=$(mktemp -d)
trap 'rm -rf "$TMP_DIR" || true' EXIT
SQL_GZ="$TMP_DIR/latest.sql.gz"
openssl enc -d -aes-256-cbc -pbkdf2 \
-pass env:SAASSHOP_DB_SNAPSHOT_KEY \
-in "$ENC_FILE" -out "$SQL_GZ"
gzip -dc "$SQL_GZ" | mysql \
--host="$DB_HOST" --port="$DB_PORT" \
--user="$DB_USERNAME" --password="$DB_PASSWORD"
echo "[install] 清理缓存 ..."
php artisan optimize:clear
echo "[done] 部署完成。接下来:配置 nginx 指向 public/,即可访问。"
EOF
chmod +x "$APP_DIR/install.sh"
# 5) 打包
OUT_FILE="$DIST_DIR/saasshop_bundle_${STAMP}.tar.gz"
LATEST_FILE="$DIST_DIR/saasshop_bundle_latest.tar.gz"
echo "[bundle] creating tar.gz ..."
(
cd "$BUNDLE_ROOT"
tar -czf "$OUT_FILE" saasshop
)
cp -f "$OUT_FILE" "$LATEST_FILE"
echo "[bundle] output: $OUT_FILE"
echo "[bundle] latest: $LATEST_FILE"

101
scripts/db_snapshot_import.sh Executable file
View File

@@ -0,0 +1,101 @@
#!/usr/bin/env bash
set -euo pipefail
# 从“私有数据仓库”Gitea拉取最新加密快照并导入到本地数据库。
#
# 用法:
# export SAASSHOP_DB_SNAPSHOT_KEY='你的强密码'
# export SAASSHOP_DATA_REPO_SSH='git@git.xxx:owner/saasshop-data(.wiki).git'
# bash scripts/db_snapshot_import.sh
REPO_DIR=$(cd "$(dirname "$0")/.." && pwd)
cd "$REPO_DIR"
DATA_REPO_SSH=${SAASSHOP_DATA_REPO_SSH:-""}
if [[ "$DATA_REPO_SSH" == "" && -f /app/working.secret/saasshop_data_repo_ssh ]]; then
DATA_REPO_SSH=$(cat /app/working.secret/saasshop_data_repo_ssh)
fi
if [[ "$DATA_REPO_SSH" == "" ]]; then
echo "缺少数据仓库地址:"
echo "- 请设置环境变量 SAASSHOP_DATA_REPO_SSH"
echo "- 或创建文件 /app/working.secret/saasshop_data_repo_ssh内容为 ssh 地址)"
exit 21
fi
if [[ "${SAASSHOP_DB_SNAPSHOT_KEY:-}" == "" ]]; then
echo "缺少解密密钥:请先 export SAASSHOP_DB_SNAPSHOT_KEY='...'"
exit 22
fi
ENV_FILE="$REPO_DIR/.env"
if [[ ! -f "$ENV_FILE" && -f /app/working.secret/laravel.env.snapshot ]]; then
ENV_FILE="/app/working.secret/laravel.env.snapshot"
fi
if [[ ! -f "$ENV_FILE" ]]; then
echo "找不到 .env 或 /app/working.secret/laravel.env.snapshot无法确定 DB 配置"
exit 23
fi
# shellcheck disable=SC1090
set -a
source "$ENV_FILE"
set +a
DB_HOST=${DB_HOST:-127.0.0.1}
DB_PORT=${DB_PORT:-3306}
DB_DATABASE=${DB_DATABASE:-}
DB_USERNAME=${DB_USERNAME:-}
DB_PASSWORD=${DB_PASSWORD:-}
if [[ "$DB_DATABASE" == "" || "$DB_USERNAME" == "" ]]; then
echo "DB 配置不完整DB_DATABASE/DB_USERNAME 不能为空"
exit 24
fi
# 数据仓工作区:固定目录,但每次都会强制将 remote 指向当前 DATA_REPO_SSH避免曾经 clone 过其它仓(如 .wiki.git导致拉错。
WORK_DIR="/tmp/saasshop-data-repo"
if [[ -d "$WORK_DIR/.git" ]]; then
echo "[data-repo] updating existing clone: $WORK_DIR"
git -C "$WORK_DIR" remote set-url origin "$DATA_REPO_SSH"
git -C "$WORK_DIR" fetch origin
git -C "$WORK_DIR" checkout main || git -C "$WORK_DIR" checkout -b main
git -C "$WORK_DIR" pull --rebase origin main || true
else
rm -rf "$WORK_DIR" || true
echo "[data-repo] cloning: $DATA_REPO_SSH -> $WORK_DIR"
git clone "$DATA_REPO_SSH" "$WORK_DIR"
git -C "$WORK_DIR" checkout main || git -C "$WORK_DIR" checkout -b main
fi
ENC_FILE="$WORK_DIR/snapshots/latest.sql.gz.enc"
MANIFEST="$WORK_DIR/snapshots/manifest.json"
if [[ ! -f "$ENC_FILE" ]]; then
echo "数据仓未找到快照:$ENC_FILE"
exit 25
fi
TMP_DIR=$(mktemp -d)
trap 'rm -rf "$TMP_DIR" || true' EXIT
SQL_GZ="$TMP_DIR/latest.sql.gz"
echo "[snapshot] decrypting ..."
openssl enc -d -aes-256-cbc -pbkdf2 \
-pass env:SAASSHOP_DB_SNAPSHOT_KEY \
-in "$ENC_FILE" -out "$SQL_GZ"
echo "[snapshot] importing into mysql ($DB_DATABASE) ..."
# 注意:快照里使用了 --databases会包含 CREATE DATABASE/USE
# 这里直接交给 mysql 执行。
gzip -dc "$SQL_GZ" | mysql \
--host="$DB_HOST" --port="$DB_PORT" \
--user="$DB_USERNAME" --password="$DB_PASSWORD"
echo "[done] imported snapshot"
if [[ -f "$MANIFEST" ]]; then
echo "[info] manifest:"
cat "$MANIFEST"
fi

153
scripts/db_snapshot_publish.sh Executable file
View File

@@ -0,0 +1,153 @@
#!/usr/bin/env bash
set -euo pipefail
# 将当前数据库导出并加密后推送到“私有数据仓库”Gitea
# - 不把明文 SQL 放入 git
# - 数据仓库建议单独授权/独立权限
#
# 依赖mysqldump / mysql / openssl / gzip / git
#
# 用法:
# 1) 设置密钥(不要写入仓库):
# export SAASSHOP_DB_SNAPSHOT_KEY='你的强密码'
# 2) 可选设置数据仓库地址(若不设则从 /app/working.secret/saasshop_data_repo_ssh 读取):
# export SAASSHOP_DATA_REPO_SSH='git@git.xxx:owner/saasshop-data(.wiki).git'
# 3) 执行:
# bash scripts/db_snapshot_publish.sh
REPO_DIR=$(cd "$(dirname "$0")/.." && pwd)
cd "$REPO_DIR"
DATA_REPO_SSH=${SAASSHOP_DATA_REPO_SSH:-""}
if [[ "$DATA_REPO_SSH" == "" && -f /app/working.secret/saasshop_data_repo_ssh ]]; then
DATA_REPO_SSH=$(cat /app/working.secret/saasshop_data_repo_ssh)
fi
if [[ "$DATA_REPO_SSH" == "" ]]; then
echo "缺少数据仓库地址:"
echo "- 请设置环境变量 SAASSHOP_DATA_REPO_SSH"
echo "- 或创建文件 /app/working.secret/saasshop_data_repo_ssh内容为 ssh 地址)"
exit 11
fi
if [[ "${SAASSHOP_DB_SNAPSHOT_KEY:-}" == "" ]]; then
echo "缺少加密密钥:请先 export SAASSHOP_DB_SNAPSHOT_KEY='...'(不要写入仓库)"
exit 12
fi
# 读取 DB 连接(优先 .env其次 /app/working.secret/laravel.env.snapshot
ENV_FILE="$REPO_DIR/.env"
if [[ ! -f "$ENV_FILE" && -f /app/working.secret/laravel.env.snapshot ]]; then
ENV_FILE="/app/working.secret/laravel.env.snapshot"
fi
if [[ ! -f "$ENV_FILE" ]]; then
echo "找不到 .env 或 /app/working.secret/laravel.env.snapshot无法确定 DB 配置"
exit 13
fi
# shellcheck disable=SC1090
set -a
source "$ENV_FILE"
set +a
DB_HOST=${DB_HOST:-127.0.0.1}
DB_PORT=${DB_PORT:-3306}
DB_DATABASE=${DB_DATABASE:-}
DB_USERNAME=${DB_USERNAME:-}
DB_PASSWORD=${DB_PASSWORD:-}
if [[ "$DB_DATABASE" == "" || "$DB_USERNAME" == "" ]]; then
echo "DB 配置不完整DB_DATABASE/DB_USERNAME 不能为空"
exit 14
fi
STAMP=$(date +%Y%m%d_%H%M%S)
OUT_DIR=$(mktemp -d)
SQL_GZ="$OUT_DIR/${DB_DATABASE}_${STAMP}.sql.gz"
ENC_FILE="$OUT_DIR/${DB_DATABASE}_${STAMP}.sql.gz.enc"
MANIFEST="$OUT_DIR/manifest.json"
cleanup() {
rm -rf "$OUT_DIR" || true
}
trap cleanup EXIT
echo "[snapshot] dumping database '$DB_DATABASE' ..."
# 导出策略:单库结构+数据;保留 routines/triggers/events避免锁表
# 注意:--databases 会在 SQL 中带 CREATE DATABASE/USE
mysqldump \
--host="$DB_HOST" --port="$DB_PORT" \
--user="$DB_USERNAME" --password="$DB_PASSWORD" \
--databases "$DB_DATABASE" \
--single-transaction --quick --skip-lock-tables \
--routines --triggers --events \
--hex-blob \
--default-character-set=utf8mb4 \
| gzip -9 > "$SQL_GZ"
echo "[snapshot] encrypting ..."
openssl enc -aes-256-cbc -pbkdf2 -salt \
-pass env:SAASSHOP_DB_SNAPSHOT_KEY \
-in "$SQL_GZ" -out "$ENC_FILE"
SHA256=$(sha256sum "$ENC_FILE" | awk '{print $1}')
SIZE=$(wc -c < "$ENC_FILE" | tr -d ' ')
CODE_HEAD=$(git rev-parse --short HEAD)
BRANCH=$(git rev-parse --abbrev-ref HEAD)
cat > "$MANIFEST" <<EOF
{
"generated_at": "$(date -Iseconds)",
"db": {
"host": "${DB_HOST}",
"port": ${DB_PORT},
"database": "${DB_DATABASE}"
},
"code": {
"repo": "saasshop",
"branch": "${BRANCH}",
"commit": "${CODE_HEAD}"
},
"snapshot": {
"filename": "latest.sql.gz.enc",
"source_filename": "$(basename "$ENC_FILE")",
"sha256": "${SHA256}",
"size": ${SIZE}
}
}
EOF
# 准备数据仓工作区
# 数据仓工作区:固定目录,但每次都会强制将 remote 指向当前 DATA_REPO_SSH避免曾经 clone 过其它仓(如 .wiki.git导致推错。
WORK_DIR="/tmp/saasshop-data-repo"
if [[ -d "$WORK_DIR/.git" ]]; then
echo "[data-repo] updating existing clone: $WORK_DIR"
git -C "$WORK_DIR" remote set-url origin "$DATA_REPO_SSH"
git -C "$WORK_DIR" fetch origin
git -C "$WORK_DIR" checkout main || git -C "$WORK_DIR" checkout -b main
git -C "$WORK_DIR" pull --rebase origin main || true
else
rm -rf "$WORK_DIR" || true
echo "[data-repo] cloning: $DATA_REPO_SSH -> $WORK_DIR"
git clone "$DATA_REPO_SSH" "$WORK_DIR"
git -C "$WORK_DIR" checkout main || git -C "$WORK_DIR" checkout -b main
fi
mkdir -p "$WORK_DIR/snapshots"
cp -f "$ENC_FILE" "$WORK_DIR/snapshots/latest.sql.gz.enc"
cp -f "$MANIFEST" "$WORK_DIR/snapshots/manifest.json"
# 同时保留一份带时间戳的历史(便于回滚/比对)
cp -f "$ENC_FILE" "$WORK_DIR/snapshots/${DB_DATABASE}_${STAMP}.sql.gz.enc"
git -C "$WORK_DIR" add snapshots
if git -C "$WORK_DIR" diff --cached --quiet; then
echo "[data-repo] no changes, skip commit"
else
git -C "$WORK_DIR" commit -m "snapshot: ${DB_DATABASE} ${STAMP} (code ${CODE_HEAD})"
git -C "$WORK_DIR" push origin main
fi
echo "[done] snapshot published to data repo"

16
scripts/git_push.sh Executable file
View File

@@ -0,0 +1,16 @@
#!/usr/bin/env bash
set -euo pipefail
# 通用安全推送脚本(当前仓库 origin 已迁移到 Gitea走 SSH key
# 用法bash scripts/git_push.sh
# 行为:仅推送当前分支到 origin并设置 upstream。
REPO_DIR=$(cd "$(dirname "$0")/.." && pwd)
cd "$REPO_DIR"
echo "Pushing to origin ..."
branch=$(git rev-parse --abbrev-ref HEAD)
git push -u origin "$branch"
echo "Push done."

View File

@@ -1,50 +1,15 @@
#!/usr/bin/env bash
set -euo pipefail
# 安全推送到 Gitee凭证从 /app/working.secret 读取,不写入仓库
# 用法bash scripts/gitee_push.sh
# 已弃用:历史上用于推送到 Gitee
# 现已迁移到自建 Giteaorigin 指向 Gitea走 SSH key
# 为避免旧流程误用,这里直接转发到通用脚本。
# 用法bash scripts/gitee_push.sh兼容旧命令
REPO_DIR=$(cd "$(dirname "$0")/.." && pwd)
cd "$REPO_DIR"
SCRIPT_DIR=$(cd "$(dirname "$0")" && pwd)
USER_FILE="/app/working.secret/gitee_user"
TOKEN_FILE="/app/working.secret/gitee_token"
echo "[DEPRECATED] scripts/gitee_push.sh 已弃用:当前请推送到 Giteaorigin"
echo "[DEPRECATED] 正在转发执行bash scripts/git_push.sh"
if [[ ! -f "$USER_FILE" || ! -f "$TOKEN_FILE" ]]; then
echo "缺少凭证文件:"
echo "- $USER_FILE(内容:你的 Gitee 用户名)"
echo "- $TOKEN_FILE(内容:你的 Gitee 私人令牌)"
echo "请你在服务器上手动创建这两个文件(不要提交到 git。"
exit 10
fi
bash "$SCRIPT_DIR/git_push.sh"
ASKPASS=$(mktemp)
chmod 700 "$ASKPASS"
cat > "$ASKPASS" <<'EOF'
#!/usr/bin/env sh
prompt="$1"
if echo "$prompt" | grep -qi "username"; then
cat /app/working.secret/gitee_user
exit 0
fi
if echo "$prompt" | grep -qi "password"; then
cat /app/working.secret/gitee_token
exit 0
fi
exit 0
EOF
chmod 700 "$ASKPASS"
# 禁止交互式提示,强制走 askpass
export GIT_TERMINAL_PROMPT=0
export GIT_ASKPASS="$ASKPASS"
echo "Pushing to origin ..."
# 只推送当前分支
branch=$(git rev-parse --abbrev-ref HEAD)
git push -u origin "$branch"
rm -f "$ASKPASS"
echo "Push done."

View File

@@ -0,0 +1,36 @@
<?php
namespace Tests\Feature;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Tests\TestCase;
class AdminDashboardBillingWorkbenchBmpaProcessableButtonShouldHaveDataRoleAndQueryTest extends TestCase
{
use RefreshDatabase;
protected function loginAsPlatformAdmin(): void
{
$this->seed();
$this->post('/admin/login', [
'email' => 'platform.admin@demo.local',
'password' => 'Platform@123456',
])->assertRedirect('/admin');
}
public function test_dashboard_billing_workbench_bmpa_processable_button_should_have_data_role_and_query(): void
{
$this->loginAsPlatformAdmin();
$res = $this->get('/admin');
$res->assertOk();
$html = (string) $res->getContent();
$this->assertStringContainsString('data-role="dashboard-po-quicklink-bmpa-processable"', $html);
// 口径可BMPA处理入口应指向 bmpa_processable_only=1 集合避免只靠“unpaid_pending”等内部别名导致语义漂移
$this->assertStringContainsString('bmpa_processable_only=1', $html);
}
}

View File

@@ -0,0 +1,53 @@
<?php
namespace Tests\Feature;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Tests\TestCase;
class AdminDashboardBillingWorkbenchButtonsShouldIncludeBackQueryTest extends TestCase
{
use RefreshDatabase;
protected function loginAsPlatformAdmin(): void
{
$this->seed();
$this->post('/admin/login', [
'email' => 'platform.admin@demo.local',
'password' => 'Platform@123456',
])->assertRedirect('/admin');
}
public function test_dashboard_billing_workbench_buttons_should_include_back_query(): void
{
$this->loginAsPlatformAdmin();
$res = $this->get('/admin');
$res->assertOk();
$html = (string) $res->getContent();
$roles = [
'dashboard-po-quicklink-bmpa-processable',
'dashboard-po-quicklink-syncable',
'dashboard-po-quicklink-sync-failed',
'dashboard-po-quicklink-paid-no-receipt',
'dashboard-po-quicklink-reconcile-mismatch',
'dashboard-po-quicklink-refund-inconsistent',
];
foreach ($roles as $role) {
$this->assertMatchesRegularExpression('/<a[^>]*data-role="' . preg_quote($role, '/') . '"[^>]*href="([^"]+)"/u', $html);
preg_match('/<a[^>]*data-role="' . preg_quote($role, '/') . '"[^>]*href="([^"]+)"/u', $html, $m);
$href = html_entity_decode($m[1] ?? '');
$query = parse_url($href, PHP_URL_QUERY) ?: '';
parse_str($query, $q);
$this->assertSame('/admin', (string) ($q['back'] ?? ''));
}
// 避免 Blade 把 URL escape 成 &amp;back=(会导致 back 被错误编码/叠加)
$res->assertDontSee('&amp;back=', false);
}
}

View File

@@ -0,0 +1,34 @@
<?php
namespace Tests\Feature;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Tests\TestCase;
class AdminDashboardBillingWorkbenchPaidNoReceiptButtonShouldHaveDataRoleTest extends TestCase
{
use RefreshDatabase;
protected function loginAsPlatformAdmin(): void
{
$this->seed();
$this->post('/admin/login', [
'email' => 'platform.admin@demo.local',
'password' => 'Platform@123456',
])->assertRedirect('/admin');
}
public function test_dashboard_paid_no_receipt_button_should_have_data_role(): void
{
$this->loginAsPlatformAdmin();
$res = $this->get('/admin');
$res->assertOk();
$res->assertSee('data-role="dashboard-po-quicklink-paid-no-receipt"', false);
$res->assertSee('已付无回执', false);
$res->assertSee('payment_status=paid', false);
$res->assertSee('receipt_status=none', false);
}
}

View File

@@ -33,7 +33,7 @@ class AdminDashboardBillingWorkbenchQuickLinksShouldBeGovernanceOrientedTest ext
$this->assertStringContainsString('可BMPA处理', $html);
$this->assertStringContainsString('可同步', $html);
$this->assertStringContainsString('同步失败', $html);
$this->assertStringContainsString('无回执', $html);
$this->assertStringContainsString('已付无回执', $html);
$this->assertStringContainsString('对账不一致', $html);
$this->assertStringContainsString('退款不一致', $html);

View File

@@ -0,0 +1,34 @@
<?php
namespace Tests\Feature;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Tests\TestCase;
class AdminDashboardBillingWorkbenchReconcileMismatchAndRefundInconsistentButtonsShouldHaveDataRoleTest extends TestCase
{
use RefreshDatabase;
protected function loginAsPlatformAdmin(): void
{
$this->seed();
$this->post('/admin/login', [
'email' => 'platform.admin@demo.local',
'password' => 'Platform@123456',
])->assertRedirect('/admin');
}
public function test_dashboard_billing_workbench_reconcile_mismatch_and_refund_inconsistent_buttons_should_have_data_role(): void
{
$this->loginAsPlatformAdmin();
$res = $this->get('/admin');
$res->assertOk();
$html = (string) $res->getContent();
$this->assertStringContainsString('data-role="dashboard-po-quicklink-reconcile-mismatch"', $html);
$this->assertStringContainsString('data-role="dashboard-po-quicklink-refund-inconsistent"', $html);
}
}

View File

@@ -73,7 +73,7 @@ class AdminDashboardBillingWorkbenchShouldIncludeBmpaFailedAndNoReceiptQuickLink
// 计数应渲染到按钮文案中
$res->assertSee('BMPA失败1');
$res->assertSee('无回执1');
$res->assertSee('已付无回执1');
// 链接应携带 back并且不应出现 &amp;back=(避免回跳断链)
$res->assertSee('bmpa_failed_only=1', false);

View File

@@ -0,0 +1,38 @@
<?php
namespace Tests\Feature;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Tests\TestCase;
class AdminDashboardBillingWorkbenchSyncableAndSyncFailedButtonsShouldHaveDataRoleTest extends TestCase
{
use RefreshDatabase;
protected function loginAsPlatformAdmin(): void
{
$this->seed();
$this->post('/admin/login', [
'email' => 'platform.admin@demo.local',
'password' => 'Platform@123456',
])->assertRedirect('/admin');
}
public function test_dashboard_billing_workbench_syncable_and_sync_failed_buttons_should_have_data_role(): void
{
$this->loginAsPlatformAdmin();
$res = $this->get('/admin');
$res->assertOk();
$html = (string) $res->getContent();
$this->assertStringContainsString('data-role="dashboard-po-quicklink-syncable"', $html);
$this->assertStringContainsString('data-role="dashboard-po-quicklink-sync-failed"', $html);
// 基础语义也钉一下,防止误改 query
$this->assertStringContainsString('syncable_only=1', $html);
$this->assertStringContainsString('sync_status=failed', $html);
}
}

View File

@@ -0,0 +1,65 @@
<?php
namespace Tests\Feature;
use App\Models\Admin;
use App\Models\Merchant;
use App\Models\PlatformOrder;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Support\Facades\Cache;
use Tests\TestCase;
class AdminDashboardMerchantRevenueRank7dMiniChartShouldHaveLinkSourceInTableTest extends TestCase
{
use RefreshDatabase;
protected function loginAsPlatformAdmin(): void
{
$this->seed();
$this->post('/admin/login', [
'email' => 'platform.admin@demo.local',
'password' => 'Platform@123456',
])->assertRedirect('/admin');
}
public function test_rank_mini_chart_should_have_link_source_in_table(): void
{
Cache::flush();
$this->loginAsPlatformAdmin();
$merchant = Merchant::query()->firstOrFail();
$platformAdminId = (int) Admin::query()->where('email', 'platform.admin@demo.local')->value('id');
PlatformOrder::query()->create([
'merchant_id' => $merchant->id,
'plan_id' => null,
'site_subscription_id' => null,
'created_by_admin_id' => $platformAdminId ?: null,
'order_no' => 'PO_DASH_RANK_CHART_LINK_0001',
'order_type' => 'new_purchase',
'status' => 'pending',
'payment_status' => 'paid',
'payable_amount' => 10,
'paid_amount' => 10,
'placed_at' => now(),
'meta' => [],
]);
Cache::flush();
$res = $this->get('/admin');
$res->assertOk();
// 迷你排行JS 渐进增强)复用表格中站点链接口径,因此这里做“表格链接存在”护栏。
$res->assertSee('data-role="merchant-revenue-rank-7d"', false);
$res->assertSee('data-role="merchant-revenue-rank-7d-chart"', false);
// 表格中站点名称应为可点击链接,且带 merchant_id 与近7天范围。
$res->assertSee($merchant->name, false);
$res->assertSee('merchant_id=' . (int) $merchant->id, false);
$res->assertSee('created_from=', false);
$res->assertSee('created_to=', false);
}
}

View File

@@ -36,7 +36,7 @@ class AdminDashboardMiniBarRowsShouldLinkToGovernanceScopesTest extends TestCase
$this->assertStringContainsString('href="/admin/platform-orders?payment_status=paid&status=pending&sync_status=unsynced&back=%2Fadmin"', $html);
$this->assertStringContainsString('href="/admin/platform-orders?syncable_only=1&sync_status=unsynced&back=%2Fadmin"', $html);
// 治理:同步失败 / BMPA失败 / 无回执 / 续费缺订阅 / 对账不一致 / 退款不一致
// 治理:同步失败 / BMPA失败 / 已付无回执 / 续费缺订阅 / 对账不一致 / 退款不一致
$this->assertStringContainsString('href="/admin/platform-orders?sync_status=failed&back=%2Fadmin"', $html);
$this->assertStringContainsString('href="/admin/platform-orders?bmpa_failed_only=1&back=%2Fadmin"', $html);
$this->assertStringContainsString('href="/admin/platform-orders?payment_status=paid&receipt_status=none&back=%2Fadmin"', $html);

View File

@@ -0,0 +1,33 @@
<?php
namespace Tests\Feature;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Tests\TestCase;
class AdminDashboardPaidNoReceiptAriaLabelShouldUsePaidNoReceiptPhraseTest extends TestCase
{
use RefreshDatabase;
protected function loginAsPlatformAdmin(): void
{
$this->seed();
$this->post('/admin/login', [
'email' => 'platform.admin@demo.local',
'password' => 'Platform@123456',
])->assertRedirect('/admin');
}
public function test_dashboard_paid_no_receipt_aria_label_should_use_paid_no_receipt_phrase(): void
{
$this->loginAsPlatformAdmin();
$res = $this->get('/admin');
$res->assertOk();
$html = (string) $res->getContent();
$this->assertStringContainsString('aria-label="进入已付无回执订单集合"', $html);
$this->assertStringNotContainsString('aria-label="进入无回执订单集合"', $html);
}
}

View File

@@ -0,0 +1,75 @@
<?php
namespace Tests\Feature;
use App\Models\Merchant;
use App\Models\PlatformOrder;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Support\Facades\Cache;
use Tests\TestCase;
class AdminDashboardPaidNoReceiptCountShouldExcludeUnpaidOrdersTest extends TestCase
{
use RefreshDatabase;
protected function loginAsPlatformAdmin(): void
{
$this->seed();
$this->post('/admin/login', [
'email' => 'platform.admin@demo.local',
'password' => 'Platform@123456',
])->assertRedirect('/admin');
}
public function test_dashboard_paid_no_receipt_count_should_exclude_unpaid_orders(): void
{
Cache::flush();
$this->loginAsPlatformAdmin();
// 清理 seed 中的订单数据,避免 seed 口径变化影响该用例
PlatformOrder::query()->delete();
$merchantId = (int) Merchant::query()->value('id');
// A已付 + 无回执(应计入)
PlatformOrder::query()->create([
'merchant_id' => $merchantId,
'plan_id' => null,
'site_subscription_id' => null,
'created_by_admin_id' => null,
'order_no' => 'PO_DASH_PAID_NO_RECEIPT_CNT_0001',
'order_type' => 'new_purchase',
'status' => 'pending',
'payment_status' => 'paid',
'payable_amount' => 9,
'paid_amount' => 9,
'meta' => [],
]);
// B未付 + 无回执(不应计入)
PlatformOrder::query()->create([
'merchant_id' => $merchantId,
'plan_id' => null,
'site_subscription_id' => null,
'created_by_admin_id' => null,
'order_no' => 'PO_DASH_PAID_NO_RECEIPT_CNT_0002',
'order_type' => 'new_purchase',
'status' => 'pending',
'payment_status' => 'unpaid',
'payable_amount' => 9,
'paid_amount' => 0,
'meta' => [],
]);
Cache::flush();
$res = $this->get('/admin');
$res->assertOk();
// 只统计已付无回执
$res->assertSee('已付无回执1', false);
$res->assertDontSee('已付无回执2', false);
}
}

View File

@@ -0,0 +1,34 @@
<?php
namespace Tests\Feature;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Tests\TestCase;
class AdminDashboardPaidNoReceiptGovernanceHelpShouldRemainConsistentTest extends TestCase
{
use RefreshDatabase;
protected function loginAsPlatformAdmin(): void
{
$this->seed();
$this->post('/admin/login', [
'email' => 'platform.admin@demo.local',
'password' => 'Platform@123456',
])->assertRedirect('/admin');
}
public function test_dashboard_paid_no_receipt_governance_help_should_remain_consistent(): void
{
$this->loginAsPlatformAdmin();
$res = $this->get('/admin');
$res->assertOk();
$html = (string) $res->getContent();
$this->assertStringContainsString('已付无回执=已支付但缺 payment_receipts', $html);
$this->assertStringContainsString('已付无回执', $html);
$this->assertStringContainsString('data-role="dashboard-po-quicklink-paid-no-receipt"', $html);
}
}

View File

@@ -0,0 +1,32 @@
<?php
namespace Tests\Feature;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Tests\TestCase;
class AdminDashboardPaidNoReceiptHelpTextShouldUsePaidNoReceiptPhraseTest extends TestCase
{
use RefreshDatabase;
protected function loginAsPlatformAdmin(): void
{
$this->seed();
$this->post('/admin/login', [
'email' => 'platform.admin@demo.local',
'password' => 'Platform@123456',
])->assertRedirect('/admin');
}
public function test_dashboard_paid_no_receipt_help_text_should_use_paid_no_receipt_phrase(): void
{
$this->loginAsPlatformAdmin();
$res = $this->get('/admin');
$res->assertOk();
$html = (string) $res->getContent();
$this->assertStringContainsString('已付无回执=已支付但缺 payment_receipts', $html);
}
}

View File

@@ -0,0 +1,33 @@
<?php
namespace Tests\Feature;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Tests\TestCase;
class AdminDashboardPaidNoReceiptVisibleLabelShouldUsePaidNoReceiptPhraseTest extends TestCase
{
use RefreshDatabase;
protected function loginAsPlatformAdmin(): void
{
$this->seed();
$this->post('/admin/login', [
'email' => 'platform.admin@demo.local',
'password' => 'Platform@123456',
])->assertRedirect('/admin');
}
public function test_dashboard_paid_no_receipt_visible_label_should_use_paid_no_receipt_phrase(): void
{
$this->loginAsPlatformAdmin();
$res = $this->get('/admin');
$res->assertOk();
$html = (string) $res->getContent();
$this->assertMatchesRegularExpression('/data-role="dashboard-po-no-receipt-row"[\s\S]*?已付无回执/u', $html);
$this->assertMatchesRegularExpression('/data-role="ops-risk-no-receipt-row"[\s\S]*?已付无回执/u', $html);
}
}

View File

@@ -0,0 +1,88 @@
<?php
namespace Tests\Feature;
use App\Models\Admin;
use App\Models\Merchant;
use App\Models\Plan;
use App\Models\PlatformOrder;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Support\Facades\Cache;
use Tests\TestCase;
class AdminDashboardPlanOrderShareMiniChartShouldHaveLinkSourceInTableTest extends TestCase
{
use RefreshDatabase;
protected function loginAsPlatformAdmin(): void
{
$this->seed();
$this->post('/admin/login', [
'email' => 'platform.admin@demo.local',
'password' => 'Platform@123456',
])->assertRedirect('/admin');
}
public function test_plan_order_share_mini_chart_should_have_link_source_in_table(): void
{
Cache::flush();
$this->loginAsPlatformAdmin();
$merchant = Merchant::query()->firstOrFail();
$platformAdminId = (int) Admin::query()->where('email', 'platform.admin@demo.local')->value('id');
$plan = Plan::query()->create([
'code' => 'dash_plan_share_link_source_plan',
'name' => '仪表盘套餐占比链接来源测试套餐',
'billing_cycle' => 'monthly',
'price' => 10,
'list_price' => 10,
'status' => 'active',
'sort' => 10,
'published_at' => now(),
]);
// 构造足够多订单,确保该套餐进入 Top5避免 seed 数据干扰 Top5 排序)
$ids = [];
for ($i = 1; $i <= 10; $i++) {
$po = PlatformOrder::query()->create([
'merchant_id' => $merchant->id,
'plan_id' => $plan->id,
'site_subscription_id' => null,
'created_by_admin_id' => $platformAdminId ?: null,
'order_no' => 'PO_DASH_PLAN_SHARE_LINK_' . str_pad((string) $i, 4, '0', STR_PAD_LEFT),
'order_type' => 'new_purchase',
'status' => 'pending',
'payment_status' => 'paid',
'payable_amount' => 10,
'paid_amount' => 10,
'placed_at' => now(),
'meta' => [],
]);
$ids[] = (int) $po->id;
}
// 确保命中近7天 created_at 口径
PlatformOrder::query()->whereIn('id', $ids)->update([
'created_at' => now()->startOfDay(),
'updated_at' => now()->startOfDay(),
]);
Cache::flush();
$res = $this->get('/admin');
$res->assertOk();
// 套餐占比迷你图JS 渐进增强)复用表格中套餐链接口径,因此这里做“表格链接存在”护栏。
$res->assertSee('data-role="plan-order-share-top5"', false);
$res->assertSee('data-role="plan-order-share-top5-chart"', false);
// 表格中套餐名称应为可点击链接,且带 plan_id 与近7天范围。
$res->assertSee($plan->name, false);
$res->assertSee('plan_id=' . (int) $plan->id, false);
$res->assertSee('created_from=', false);
$res->assertSee('created_to=', false);
}
}

View File

@@ -30,7 +30,7 @@ class AdminDashboardPlatformOrderGovernanceBarsShouldExplainMetricsTest extends
$res->assertSee('口径说明');
$res->assertSee('同步失败=meta.subscription_activation_error.message');
$res->assertSee('BMPA失败=meta.batch_mark_paid_and_activate_error.message');
$res->assertSee('无回执=已支付但缺 payment_receipts');
$res->assertSee('已付无回执=已支付但缺 payment_receipts');
$res->assertSee('对账不一致=回执汇总金额与 paid_amount 不一致');
$res->assertSee('退款不一致=退款汇总与退款状态不一致');
$res->assertSee('续费缺订阅=renewal 但 site_subscription_id 为空');

View File

@@ -0,0 +1,71 @@
<?php
namespace Tests\Feature;
use App\Models\Admin;
use App\Models\Merchant;
use App\Models\PlatformOrder;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Support\Facades\Cache;
use Tests\TestCase;
class AdminDashboardPlatformOrderTrend7dMiniChartBarsShouldHaveLinkWhenDateLinkPresentTest extends TestCase
{
use RefreshDatabase;
protected function loginAsPlatformAdmin(): void
{
$this->seed();
$this->post('/admin/login', [
'email' => 'platform.admin@demo.local',
'password' => 'Platform@123456',
])->assertRedirect('/admin');
}
public function test_trend_mini_chart_should_reuse_table_date_links(): void
{
Cache::flush();
$this->loginAsPlatformAdmin();
$merchantId = (int) Merchant::query()->value('id');
$platformAdminId = (int) Admin::query()->where('email', 'platform.admin@demo.local')->value('id');
$d3 = now()->subDays(3)->format('Y-m-d');
$po = PlatformOrder::query()->create([
'merchant_id' => $merchantId,
'plan_id' => null,
'site_subscription_id' => null,
'created_by_admin_id' => $platformAdminId ?: null,
'order_no' => 'PO_TREND_CHART_LINK_D3',
'order_type' => 'new_purchase',
'status' => 'pending',
'payment_status' => 'paid',
'payable_amount' => 10,
'paid_amount' => 10,
]);
PlatformOrder::query()->where('id', $po->id)->update([
'created_at' => now()->subDays(3)->startOfDay(),
'updated_at' => now()->subDays(3)->startOfDay(),
]);
Cache::flush();
$res = $this->get('/admin');
$res->assertOk();
// 迷你图表的链接不是 SSR 输出(由 JS 渐进增强渲染),
// 但它依赖下方表格中的日期链接口径,因此这里至少做“表格日期链接存在”护栏。
// 若未来有人把表格链接删掉/改成非 <a>,迷你图表将失去跳转能力。
$res->assertSee('data-role="platform-order-trend-7d"', false);
$res->assertSee('data-role="platform-order-trend-7d-chart"', false);
// 日期列应为可点击链接
$res->assertSee('>' . $d3 . '</a>', false);
$res->assertSee('created_from=' . $d3, false);
$res->assertSee('created_to=' . $d3, false);
}
}

View File

@@ -22,7 +22,7 @@ class AdminDashboardRecentPlatformOrdersNoReceiptHintShouldIncludePaidAmountTool
])->assertRedirect('/admin');
}
public function test_dashboard_recent_platform_orders_no_receipt_hint_should_include_paid_amount_tooltip(): void
public function test_dashboard_recent_platform_orders_paid_no_receipt_hint_should_include_paid_amount_tooltip(): void
{
$this->loginAsPlatformAdmin();
@@ -34,7 +34,7 @@ class AdminDashboardRecentPlatformOrdersNoReceiptHintShouldIncludePaidAmountTool
'plan_id' => null,
'site_subscription_id' => null,
'created_by_admin_id' => $platformAdminId ?: null,
'order_no' => 'PO_DASH_NO_RECEIPT_TIP_0001',
'order_no' => 'PO_DASH_PAID_NO_RECEIPT_TIP_0001',
'order_type' => 'new_purchase',
'status' => 'pending',
'payment_status' => 'paid',
@@ -47,8 +47,8 @@ class AdminDashboardRecentPlatformOrdersNoReceiptHintShouldIncludePaidAmountTool
$res = $this->get('/admin');
$res->assertOk();
// 无回执提示应包含已付金额 tooltip帮助运营快速判断风险金额级别
// 已付无回执提示应包含已付金额 tooltip帮助运营快速判断风险金额级别
$res->assertSee('data-role="recent-order-no-receipt-hint"', false);
$res->assertSee('title="已付 ¥9.00|无回执证据"', false);
$res->assertSee('title="已付 ¥9.00已付无回执证据"', false);
}
}

View File

@@ -23,7 +23,7 @@ class AdminDashboardRecentPlatformOrdersNoReceiptShouldIncludeListLinkTest exten
])->assertRedirect('/admin');
}
public function test_dashboard_recent_platform_orders_no_receipt_should_include_list_link(): void
public function test_dashboard_recent_platform_orders_paid_no_receipt_should_include_list_link(): void
{
$this->loginAsPlatformAdmin();
@@ -35,7 +35,7 @@ class AdminDashboardRecentPlatformOrdersNoReceiptShouldIncludeListLinkTest exten
'plan_id' => null,
'site_subscription_id' => null,
'created_by_admin_id' => $platformAdminId ?: null,
'order_no' => 'PO_DASH_NO_RECEIPT_LIST_0001',
'order_no' => 'PO_DASH_PAID_NO_RECEIPT_LIST_0001',
'order_type' => 'new_purchase',
'status' => 'pending',
'payment_status' => 'paid',
@@ -47,8 +47,8 @@ class AdminDashboardRecentPlatformOrdersNoReceiptShouldIncludeListLinkTest exten
$res = $this->get('/admin');
$res->assertOk();
$res->assertSee('PO_DASH_NO_RECEIPT_LIST_0001');
$res->assertSee('无回执', false);
$res->assertSee('PO_DASH_PAID_NO_RECEIPT_LIST_0001');
$res->assertSee('已付无回执', false);
$listUrl = '/admin/platform-orders?' . Arr::query([
'payment_status' => 'paid',

View File

@@ -23,20 +23,20 @@ class AdminDashboardRecentPlatformOrdersPaidButNoReceiptShouldShowFixLinkTest ex
])->assertRedirect('/admin');
}
public function test_dashboard_recent_platform_orders_paid_but_no_receipt_should_show_fix_link(): void
public function test_dashboard_recent_platform_orders_paid_no_receipt_should_show_fix_link(): void
{
$this->loginAsPlatformAdmin();
$merchantId = (int) Merchant::query()->value('id');
$platformAdminId = (int) Admin::query()->where('email', 'platform.admin@demo.local')->value('id');
// 确保“最近平台订单”卡里有一条已支付但无回执证据的订单
// 确保“最近平台订单”卡里有一条已付无回执订单
$order = PlatformOrder::query()->create([
'merchant_id' => $merchantId,
'plan_id' => null,
'site_subscription_id' => null,
'created_by_admin_id' => $platformAdminId ?: null,
'order_no' => 'PO_DASH_NO_RECEIPT_0001',
'order_no' => 'PO_DASH_PAID_NO_RECEIPT_0001',
'order_type' => 'new_purchase',
'status' => 'pending',
'payment_status' => 'paid',
@@ -49,9 +49,9 @@ class AdminDashboardRecentPlatformOrdersPaidButNoReceiptShouldShowFixLinkTest ex
$res = $this->get('/admin');
$res->assertOk();
// 仪表盘应展示“无回执”提示,并给出“去补回执”入口(锚点 add-payment-receipt
$res->assertSee('PO_DASH_NO_RECEIPT_0001');
$res->assertSee('无回执', false);
// 仪表盘应展示“已付无回执”提示,并给出“去补回执”入口(锚点 add-payment-receipt
$res->assertSee('PO_DASH_PAID_NO_RECEIPT_0001');
$res->assertSee('已付无回执', false);
$res->assertSee('去补回执', false);
$fixUrl = '/admin/platform-orders/' . $order->id . '?' . Arr::query([

View File

@@ -0,0 +1,64 @@
<?php
namespace Tests\Feature;
use App\Models\Merchant;
use App\Models\Plan;
use App\Models\PlatformOrder;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Tests\TestCase;
class AdminDashboardRecentPlatformOrdersPaidNoReceiptPrefixShouldUsePaidNoReceiptPhraseTest extends TestCase
{
use RefreshDatabase;
protected function loginAsPlatformAdmin(): void
{
$this->seed();
$this->post('/admin/login', [
'email' => 'platform.admin@demo.local',
'password' => 'Platform@123456',
])->assertRedirect('/admin');
}
public function test_recent_platform_orders_paid_no_receipt_prefix_should_use_paid_no_receipt_phrase(): void
{
$this->loginAsPlatformAdmin();
$merchant = Merchant::query()->firstOrFail();
$plan = Plan::query()->create([
'code' => 'dashboard_recent_paid_no_receipt_prefix_plan',
'name' => '仪表盘最近订单已付无回执前缀测试套餐',
'billing_cycle' => 'monthly',
'price' => 10,
'list_price' => 10,
'status' => 'active',
'sort' => 10,
'published_at' => now(),
]);
PlatformOrder::query()->create([
'merchant_id' => $merchant->id,
'plan_id' => $plan->id,
'order_no' => 'PO_DASHBOARD_PNR_PREFIX_0001',
'order_type' => 'new_purchase',
'status' => 'activated',
'payment_status' => 'paid',
'plan_name' => $plan->name,
'billing_cycle' => $plan->billing_cycle,
'period_months' => 1,
'quantity' => 1,
'payable_amount' => 10,
'paid_amount' => 10,
'placed_at' => now(),
'paid_at' => now(),
'activated_at' => now(),
'meta' => [],
]);
$res = $this->get('/admin');
$res->assertOk();
$res->assertSee('<span class="row-warn-prefix">已付无回执</span>', false);
}
}

View File

@@ -23,7 +23,7 @@ class AdminDashboardRecentPlatformOrdersScanlineNoReceiptShouldLinkToAddReceiptP
])->assertRedirect('/admin');
}
public function test_dashboard_recent_platform_orders_scanline_no_receipt_should_link_to_add_receipt_panel(): void
public function test_dashboard_recent_platform_orders_scanline_paid_no_receipt_should_link_to_add_receipt_panel(): void
{
$this->loginAsPlatformAdmin();
@@ -31,8 +31,8 @@ class AdminDashboardRecentPlatformOrdersScanlineNoReceiptShouldLinkToAddReceiptP
$platformAdminId = (int) Admin::query()->where('email', 'platform.admin@demo.local')->value('id');
$plan = Plan::query()->create([
'code' => 'dash_recent_order_scanline_no_receipt_plan',
'name' => '仪表盘扫描行无回执直达测试套餐',
'code' => 'dash_recent_order_scanline_paid_no_receipt_plan',
'name' => '仪表盘扫描行已付无回执直达测试套餐',
'billing_cycle' => 'monthly',
'price' => 10,
'list_price' => 10,
@@ -46,7 +46,7 @@ class AdminDashboardRecentPlatformOrdersScanlineNoReceiptShouldLinkToAddReceiptP
'plan_id' => $plan->id,
'site_subscription_id' => null,
'created_by_admin_id' => $platformAdminId ?: null,
'order_no' => 'PO_DASH_SCANLINE_NO_RECEIPT_0001',
'order_no' => 'PO_DASH_SCANLINE_PAID_NO_RECEIPT_0001',
'order_type' => 'new_purchase',
'status' => 'pending',
'payment_status' => 'paid',
@@ -54,18 +54,18 @@ class AdminDashboardRecentPlatformOrdersScanlineNoReceiptShouldLinkToAddReceiptP
'paid_amount' => 10,
'placed_at' => now(),
'meta' => [
// 明确无回执证据:不提供 payment_summary / payment_receipts
// 明确已付无回执证据:不提供 payment_summary / payment_receipts
],
]);
$res = $this->get('/admin');
$res->assertOk();
$res->assertSee('PO_DASH_SCANLINE_NO_RECEIPT_0001');
$res->assertSee('PO_DASH_SCANLINE_PAID_NO_RECEIPT_0001');
// 回执: 应可直达补回执面板(#add-payment-receipt
// 回执:已付无回执 应可直达补回执面板(#add-payment-receipt
$res->assertSee('回执:', false);
$res->assertSee('#add-payment-receipt', false);
$res->assertSee('', false);
$res->assertSee('已付无回执', false);
}
}

View File

@@ -0,0 +1,25 @@
<?php
namespace Tests\Feature;
use Tests\TestCase;
class AdminJsDashboardMiniChartsShouldReuseTableLinksSelectorsTest extends TestCase
{
public function test_admin_js_dashboard_mini_charts_should_reuse_table_links_selectors(): void
{
$js = (string) file_get_contents(public_path('js/admin.js'));
// 趋势:复用日期表格链接
$this->assertStringContainsString('[data-role="platform-order-trend-7d"] a.link', $js);
// 排行:复用站点表格链接(按 merchant_id 提取)
$this->assertStringContainsString('[data-role="merchant-revenue-rank-7d"] a.link[href*="merchant_id="]', $js);
// 占比:复用套餐表格链接(按 plan_id 提取)
$this->assertStringContainsString('[data-role="plan-order-share-top5"] a.link[href*="plan_id="]', $js);
// 渐进增强:可点击则 <a>
$this->assertStringContainsString("document.createElement(href ? 'a' : 'div')", $js);
}
}

View File

@@ -0,0 +1,46 @@
<?php
namespace Tests\Feature;
use App\Models\Merchant;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Tests\TestCase;
class AdminMerchantIndexGovernanceLinksShouldIncludePaidNoReceiptTest extends TestCase
{
use RefreshDatabase;
protected function loginAsPlatformAdmin(): void
{
$this->seed();
$this->post('/admin/login', [
'email' => 'platform.admin@demo.local',
'password' => 'Platform@123456',
])->assertRedirect('/admin');
}
public function test_merchant_index_governance_links_should_include_paid_no_receipt(): void
{
$this->loginAsPlatformAdmin();
$merchant = Merchant::query()->create([
'name' => '站点治理已付无回执测试站点',
'slug' => 'merchant-paid-no-receipt-link',
'plan' => 'pro',
'status' => 'active',
'contact_name' => '张三',
'contact_phone' => '13800138000',
'contact_email' => 'merchant-paid-no-receipt@example.com',
]);
$res = $this->get('/admin/merchants');
$res->assertOk();
$html = (string) $res->getContent();
$this->assertStringContainsString('已付无回执', $html);
$expectedBack = urlencode('/admin/merchants');
$this->assertStringContainsString('/admin/platform-orders?merchant_id=' . $merchant->id . '&payment_status=paid&receipt_status=none&back=' . $expectedBack, $html);
}
}

View File

@@ -0,0 +1,82 @@
<?php
namespace Tests\Feature;
use App\Models\Merchant;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Support\Arr;
use Tests\TestCase;
class AdminMerchantIndexPaidNoReceiptGovernanceLinkShouldUseMerchantIndexSelfBackTest extends TestCase
{
use RefreshDatabase;
protected function loginAsPlatformAdmin(): void
{
$this->seed();
$this->post('/admin/login', [
'email' => 'platform.admin@demo.local',
'password' => 'Platform@123456',
])->assertRedirect('/admin');
}
public function test_paid_no_receipt_governance_link_should_use_merchant_index_self_as_back(): void
{
$this->loginAsPlatformAdmin();
$merchant = Merchant::query()->create([
'name' => '站点已付无回执回跳测试站点',
'slug' => 'merchant-paid-no-receipt-back',
'plan' => 'pro',
'status' => 'active',
'contact_name' => '李四',
'contact_phone' => '13800138001',
'contact_email' => 'merchant-paid-no-receipt-back@example.com',
]);
$res = $this->get('/admin/merchants?back=' . urlencode('/admin/platform-orders?payment_status=paid'));
$res->assertOk();
$html = (string) $res->getContent();
preg_match_all('/href="([^"]+)"/u', $html, $matches);
$hrefs = array_map('html_entity_decode', $matches[1] ?? []);
$targetHref = null;
foreach ($hrefs as $candidate) {
if (! str_contains($candidate, '/admin/platform-orders?')) {
continue;
}
$parts = parse_url($candidate);
parse_str($parts['query'] ?? '', $q);
if ((string) ($q['merchant_id'] ?? '') !== (string) $merchant->id) {
continue;
}
if ((string) ($q['payment_status'] ?? '') !== 'paid') {
continue;
}
if ((string) ($q['receipt_status'] ?? '') !== 'none') {
continue;
}
$targetHref = $candidate;
break;
}
$this->assertNotNull($targetHref, '未找到目标站点的“已付无回执”入口');
$expectedUrl = '/admin/platform-orders?' . Arr::query([
'merchant_id' => $merchant->id,
'payment_status' => 'paid',
'receipt_status' => 'none',
'back' => '/admin/merchants',
]);
$this->assertSame($expectedUrl, $targetHref);
$this->assertStringNotContainsString(urlencode('/admin/platform-orders?payment_status=paid'), $targetHref);
}
}

View File

@@ -0,0 +1,83 @@
<?php
namespace Tests\Feature;
use App\Models\Plan;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Support\Arr;
use Tests\TestCase;
class AdminPlanIndexPaidNoReceiptGovernanceLinkShouldUsePlanIndexSelfBackTest extends TestCase
{
use RefreshDatabase;
protected function loginAsPlatformAdmin(): void
{
$this->seed();
$this->post('/admin/login', [
'email' => 'platform.admin@demo.local',
'password' => 'Platform@123456',
])->assertRedirect('/admin');
}
public function test_paid_no_receipt_governance_link_should_use_plan_index_self_as_back(): void
{
$this->loginAsPlatformAdmin();
$plan = Plan::query()->create([
'code' => 'plan_index_paid_no_receipt_back',
'name' => '套餐页已付无回执回跳测试套餐',
'billing_cycle' => 'monthly',
'price' => 88,
'list_price' => 88,
'status' => 'active',
'sort' => 10,
'published_at' => now(),
]);
$res = $this->get('/admin/plans?back=' . urlencode('/admin/platform-orders?payment_status=paid'));
$res->assertOk();
$html = (string) $res->getContent();
preg_match_all('/href="([^"]+)"/u', $html, $matches);
$hrefs = array_map('html_entity_decode', $matches[1] ?? []);
$targetHref = null;
foreach ($hrefs as $candidate) {
if (! str_contains($candidate, '/admin/platform-orders?')) {
continue;
}
$parts = parse_url($candidate);
parse_str($parts['query'] ?? '', $q);
if ((string) ($q['plan_id'] ?? '') !== (string) $plan->id) {
continue;
}
if ((string) ($q['payment_status'] ?? '') !== 'paid') {
continue;
}
if ((string) ($q['receipt_status'] ?? '') !== 'none') {
continue;
}
$targetHref = $candidate;
break;
}
$this->assertNotNull($targetHref, '未找到套餐页目标“已付无回执”入口');
$expectedUrl = '/admin/platform-orders?' . Arr::query([
'plan_id' => $plan->id,
'payment_status' => 'paid',
'receipt_status' => 'none',
'back' => '/admin/plans',
]);
$this->assertSame($expectedUrl, $targetHref);
$this->assertStringNotContainsString(urlencode('/admin/platform-orders?payment_status=paid'), $targetHref);
}
}

View File

@@ -0,0 +1,47 @@
<?php
namespace Tests\Feature;
use App\Models\Plan;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Tests\TestCase;
class AdminPlanIndexPaidNoReceiptGovernanceLinkTest extends TestCase
{
use RefreshDatabase;
protected function loginAsPlatformAdmin(): void
{
$this->seed();
$this->post('/admin/login', [
'email' => 'platform.admin@demo.local',
'password' => 'Platform@123456',
])->assertRedirect('/admin');
}
public function test_plan_index_should_render_paid_no_receipt_governance_link(): void
{
$this->loginAsPlatformAdmin();
$plan = Plan::query()->create([
'code' => 'plan_index_paid_no_receipt_link',
'name' => '套餐页已付无回执治理入口测试套餐',
'billing_cycle' => 'monthly',
'price' => 99,
'list_price' => 99,
'status' => 'active',
'sort' => 10,
'published_at' => now(),
]);
$res = $this->get('/admin/plans');
$res->assertOk();
$html = (string) $res->getContent();
$this->assertStringContainsString('已付无回执', $html);
$expectedBack = urlencode('/admin/plans');
$this->assertStringContainsString('/admin/platform-orders?plan_id=' . $plan->id . '&payment_status=paid&receipt_status=none&back=' . $expectedBack, $html);
}
}

View File

@@ -0,0 +1,422 @@
<?php
namespace Tests\Feature;
use App\Models\Merchant;
use App\Models\Plan;
use App\Models\PlatformOrder;
use App\Models\SiteSubscription;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Support\Arr;
use Tests\TestCase;
class AdminPlanShowTest extends TestCase
{
use RefreshDatabase;
protected function loginAsPlatformAdmin(): void
{
$this->seed();
$this->post('/admin/login', [
'email' => 'platform.admin@demo.local',
'password' => 'Platform@123456',
])->assertRedirect('/admin');
}
public function test_platform_admin_can_open_plan_show_page(): void
{
$this->loginAsPlatformAdmin();
$merchant = Merchant::query()->firstOrFail();
$plan = Plan::query()->create([
'code' => 'plan_show_test',
'name' => '套餐详情测试套餐',
'billing_cycle' => 'monthly',
'price' => 99,
'list_price' => 199,
'status' => 'active',
'sort' => 10,
'description' => '用于验证套餐详情页',
'published_at' => now(),
]);
$subscription = SiteSubscription::query()->create([
'merchant_id' => $merchant->id,
'plan_id' => $plan->id,
'status' => 'activated',
'source' => 'manual',
'subscription_no' => 'SUB_PLAN_SHOW_0001',
'plan_name' => $plan->name,
'billing_cycle' => $plan->billing_cycle,
'period_months' => 1,
'amount' => 99,
'starts_at' => now()->subDay(),
'ends_at' => now()->addDays(5),
'activated_at' => now()->subDay(),
]);
PlatformOrder::query()->create([
'merchant_id' => $merchant->id,
'plan_id' => $plan->id,
'site_subscription_id' => $subscription->id,
'order_no' => 'PO_PLAN_SHOW_0001',
'order_type' => 'renewal',
'status' => 'activated',
'payment_status' => 'paid',
'plan_name' => $plan->name,
'billing_cycle' => $plan->billing_cycle,
'period_months' => 1,
'quantity' => 1,
'list_amount' => 99,
'discount_amount' => 0,
'payable_amount' => 99,
'paid_amount' => 99,
'placed_at' => now()->subHour(),
'paid_at' => now()->subMinutes(50),
'activated_at' => now()->subMinutes(40),
'meta' => [],
]);
$res = $this->get('/admin/plans/' . $plan->id);
$res->assertOk()
->assertSee('套餐详情')
->assertSee('套餐详情测试套餐')
->assertSee('查看已付无回执订单')
->assertSee('查看续费缺订阅订单');
}
public function test_guest_cannot_open_plan_show_page(): void
{
$plan = Plan::query()->create([
'code' => 'plan_show_guest_test',
'name' => '游客不可见套餐',
'billing_cycle' => 'monthly',
'price' => 10,
'list_price' => 10,
'status' => 'active',
'sort' => 10,
]);
$this->get('/admin/plans/' . $plan->id)->assertRedirect('/admin/login');
}
public function test_plan_show_links_to_subscriptions_and_orders_should_contain_back_to_plan_show(): void
{
$this->loginAsPlatformAdmin();
$plan = Plan::query()->create([
'code' => 'plan_show_back_test',
'name' => '套餐详情 back 测试套餐',
'billing_cycle' => 'monthly',
'price' => 88,
'list_price' => 108,
'status' => 'active',
'sort' => 10,
'published_at' => now(),
]);
$res = $this->get('/admin/plans/' . $plan->id);
$res->assertOk();
$back = '/admin/plans/' . $plan->id;
$expectedSubscriptionUrl = '/admin/site-subscriptions?' . Arr::query([
'plan_id' => $plan->id,
'back' => $back,
]);
$expectedOrderUrl = '/admin/platform-orders?' . Arr::query([
'plan_id' => $plan->id,
'back' => $back,
]);
$expectedPaidNoReceiptUrl = '/admin/platform-orders?' . Arr::query([
'plan_id' => $plan->id,
'payment_status' => 'paid',
'receipt_status' => 'none',
'back' => $back,
]);
$expectedRenewalMissingUrl = '/admin/platform-orders?' . Arr::query([
'plan_id' => $plan->id,
'renewal_missing_subscription' => '1',
'back' => $back,
]);
$res->assertSee($expectedSubscriptionUrl, false);
$res->assertSee($expectedOrderUrl, false);
$res->assertSee($expectedPaidNoReceiptUrl, false);
$res->assertSee($expectedRenewalMissingUrl, false);
}
public function test_plan_index_show_link_should_carry_back_to_index_self_without_back(): void
{
$this->loginAsPlatformAdmin();
$plan = Plan::query()->create([
'code' => 'plan_index_show_link_test',
'name' => '套餐列表详情入口测试套餐',
'billing_cycle' => 'monthly',
'price' => 18,
'list_price' => 18,
'status' => 'active',
'sort' => 10,
]);
$res = $this->get('/admin/plans?status=active&back=' . urlencode('/admin/platform-orders'));
$res->assertOk();
$expectedBack = '/admin/plans?' . Arr::query([
'status' => 'active',
]);
$expectedShowUrl = '/admin/plans/' . $plan->id . '?' . Arr::query([
'back' => $expectedBack,
]);
$res->assertSee($expectedShowUrl, false);
$res->assertSee('查看详情');
}
public function test_plan_show_should_drop_unsafe_back_and_not_render_return_to_previous_link(): void
{
$this->loginAsPlatformAdmin();
$plan = Plan::query()->create([
'code' => 'plan_show_unsafe_back_test',
'name' => '套餐详情 unsafe back 测试套餐',
'billing_cycle' => 'monthly',
'price' => 28,
'list_price' => 38,
'status' => 'active',
'sort' => 10,
]);
$unsafeBack = '/admin/plans?status=active&back=/admin/platform-orders';
$res = $this->get('/admin/plans/' . $plan->id . '?back=' . urlencode($unsafeBack));
$res->assertOk();
$res->assertDontSee('返回上一页(保留上下文)');
$res->assertSee('/admin/plans', false);
$res->assertDontSee('back=' . $unsafeBack, false);
}
public function test_plan_show_should_render_safe_back_but_governance_links_should_still_use_plan_show_self_back(): void
{
$this->loginAsPlatformAdmin();
$plan = Plan::query()->create([
'code' => 'plan_show_safe_back_test',
'name' => '套餐详情 safe back 测试套餐',
'billing_cycle' => 'monthly',
'price' => 58,
'list_price' => 68,
'status' => 'active',
'sort' => 10,
]);
$safeBack = '/admin/plans?' . Arr::query([
'status' => 'active',
'keyword' => '治理',
]);
$res = $this->get('/admin/plans/' . $plan->id . '?back=' . urlencode($safeBack));
$res->assertOk();
$res->assertSee('href="' . $safeBack . '"', false);
$res->assertSee('返回上一页(保留上下文)');
$planShowSelf = '/admin/plans/' . $plan->id;
$expectedOrdersUrl = '/admin/platform-orders?' . Arr::query([
'plan_id' => $plan->id,
'back' => $planShowSelf,
]);
$expectedPaidNoReceiptUrl = '/admin/platform-orders?' . Arr::query([
'plan_id' => $plan->id,
'payment_status' => 'paid',
'receipt_status' => 'none',
'back' => $planShowSelf,
]);
$expectedRenewalMissingUrl = '/admin/platform-orders?' . Arr::query([
'plan_id' => $plan->id,
'renewal_missing_subscription' => '1',
'back' => $planShowSelf,
]);
$res->assertSee($expectedOrdersUrl, false);
$res->assertSee($expectedPaidNoReceiptUrl, false);
$res->assertSee($expectedRenewalMissingUrl, false);
$res->assertDontSee('back=' . $safeBack, false);
$res->assertDontSee('back%3D', false);
}
public function test_plan_show_header_actions_should_use_plan_show_self_back_even_when_safe_back_is_present(): void
{
$this->loginAsPlatformAdmin();
$plan = Plan::query()->create([
'code' => 'plan_show_header_cta_back_test',
'name' => '套餐详情顶部动作回链测试套餐',
'billing_cycle' => 'monthly',
'price' => 66,
'list_price' => 88,
'status' => 'active',
'sort' => 10,
]);
$safeBack = '/admin/plans?' . Arr::query([
'status' => 'active',
'billing_cycle' => 'monthly',
]);
$res = $this->get('/admin/plans/' . $plan->id . '?back=' . urlencode($safeBack));
$res->assertOk();
$planShowSelf = '/admin/plans/' . $plan->id;
$expectedEditUrl = '/admin/plans/' . $plan->id . '/edit?' . Arr::query([
'back' => $planShowSelf,
]);
$expectedCreateOrderUrl = '/admin/platform-orders/create?' . Arr::query([
'plan_id' => $plan->id,
'order_type' => 'new_purchase',
'back' => $planShowSelf,
]);
$res->assertSee($expectedEditUrl, false);
$res->assertSee($expectedCreateOrderUrl, false);
$res->assertDontSee('back=' . $safeBack, false);
}
public function test_plan_show_platform_order_governance_links_should_use_self_back_when_outer_back_is_unsafe(): void
{
$this->loginAsPlatformAdmin();
$plan = Plan::query()->create([
'code' => 'plan_show_governance_unsafe_back_test',
'name' => '套餐详情治理入口 unsafe back 测试套餐',
'billing_cycle' => 'monthly',
'price' => 76,
'list_price' => 96,
'status' => 'active',
'sort' => 10,
]);
$unsafeBack = '/admin/plans?status=active&back=/admin/platform-orders';
$res = $this->get('/admin/plans/' . $plan->id . '?back=' . urlencode($unsafeBack));
$res->assertOk();
$planShowSelf = '/admin/plans/' . $plan->id;
$expectedOrdersUrl = '/admin/platform-orders?' . Arr::query([
'plan_id' => $plan->id,
'back' => $planShowSelf,
]);
$expectedPaidNoReceiptUrl = '/admin/platform-orders?' . Arr::query([
'plan_id' => $plan->id,
'payment_status' => 'paid',
'receipt_status' => 'none',
'back' => $planShowSelf,
]);
$expectedRenewalMissingUrl = '/admin/platform-orders?' . Arr::query([
'plan_id' => $plan->id,
'renewal_missing_subscription' => '1',
'back' => $planShowSelf,
]);
$res->assertSee($expectedOrdersUrl, false);
$res->assertSee($expectedPaidNoReceiptUrl, false);
$res->assertSee($expectedRenewalMissingUrl, false);
$res->assertDontSee('back=' . $unsafeBack, false);
$res->assertDontSee('back%3D', false);
}
public function test_plan_show_subscription_governance_links_should_use_self_back_when_outer_back_is_unsafe(): void
{
$this->loginAsPlatformAdmin();
$plan = Plan::query()->create([
'code' => 'plan_show_subscription_governance_unsafe_back_test',
'name' => '套餐详情订阅治理入口 unsafe back 测试套餐',
'billing_cycle' => 'monthly',
'price' => 86,
'list_price' => 106,
'status' => 'active',
'sort' => 10,
]);
$unsafeBack = '/admin/plans?status=active&back=/admin/site-subscriptions';
$res = $this->get('/admin/plans/' . $plan->id . '?back=' . urlencode($unsafeBack));
$res->assertOk();
$planShowSelf = '/admin/plans/' . $plan->id;
$expectedSubscriptionsUrl = '/admin/site-subscriptions?' . Arr::query([
'plan_id' => $plan->id,
'back' => $planShowSelf,
]);
$expectedActivatedSubscriptionsUrl = '/admin/site-subscriptions?' . Arr::query([
'plan_id' => $plan->id,
'status' => 'activated',
'back' => $planShowSelf,
]);
$expectedExpiringSubscriptionsUrl = '/admin/site-subscriptions?' . Arr::query([
'plan_id' => $plan->id,
'expiry' => 'expiring_7d',
'back' => $planShowSelf,
]);
$res->assertSee($expectedSubscriptionsUrl, false);
$res->assertSee($expectedActivatedSubscriptionsUrl, false);
$res->assertSee($expectedExpiringSubscriptionsUrl, false);
$res->assertDontSee('back=' . $unsafeBack, false);
$res->assertDontSee('back%3D', false);
}
public function test_plan_show_should_render_safe_back_but_subscription_governance_links_should_still_use_plan_show_self_back(): void
{
$this->loginAsPlatformAdmin();
$plan = Plan::query()->create([
'code' => 'plan_show_subscription_governance_safe_back_test',
'name' => '套餐详情订阅治理入口 safe back 测试套餐',
'billing_cycle' => 'monthly',
'price' => 96,
'list_price' => 126,
'status' => 'active',
'sort' => 10,
]);
$safeBack = '/admin/plans?' . Arr::query([
'status' => 'active',
'keyword' => '订阅治理',
]);
$res = $this->get('/admin/plans/' . $plan->id . '?back=' . urlencode($safeBack));
$res->assertOk();
$res->assertSee('href="' . $safeBack . '"', false);
$res->assertSee('返回上一页(保留上下文)');
$planShowSelf = '/admin/plans/' . $plan->id;
$expectedSubscriptionsUrl = '/admin/site-subscriptions?' . Arr::query([
'plan_id' => $plan->id,
'back' => $planShowSelf,
]);
$expectedExpiringSubscriptionsUrl = '/admin/site-subscriptions?' . Arr::query([
'plan_id' => $plan->id,
'expiry' => 'expiring_7d',
'back' => $planShowSelf,
]);
$res->assertSee($expectedSubscriptionsUrl, false);
$res->assertSee($expectedExpiringSubscriptionsUrl, false);
$res->assertDontSee('back=' . $safeBack, false);
}
}

View File

@@ -0,0 +1,106 @@
<?php
namespace Tests\Feature;
use App\Models\Merchant;
use App\Models\Plan;
use App\Models\PlatformOrder;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Tests\TestCase;
class AdminPlatformBatchShowPageFallbackCountsShouldRenderWithoutLastResultTest extends TestCase
{
use RefreshDatabase;
protected function loginAsPlatformAdmin(): void
{
$this->seed();
$this->post('/admin/login', [
'email' => 'platform.admin@demo.local',
'password' => 'Platform@123456',
])->assertRedirect('/admin');
}
public function test_batch_show_page_should_render_fallback_counts_without_last_result(): void
{
$this->loginAsPlatformAdmin();
$merchant = Merchant::query()->firstOrFail();
$plan = Plan::query()->create([
'code' => 'plan_batch_show_fallback_counts_0001',
'name' => '批次页 fallback 粗统计测试套餐',
'billing_cycle' => 'monthly',
'price' => 10,
'list_price' => 10,
'status' => 'active',
'sort' => 10,
'published_at' => now(),
]);
$runId = 'BAS_FALLBACK_COUNTS_0001';
PlatformOrder::query()->create([
'merchant_id' => $merchant->id,
'plan_id' => $plan->id,
'order_no' => 'PO_BATCH_FALLBACK_A',
'order_type' => 'new_purchase',
'status' => 'activated',
'payment_status' => 'paid',
'plan_name' => $plan->name,
'billing_cycle' => $plan->billing_cycle,
'period_months' => 1,
'quantity' => 1,
'payable_amount' => 10,
'paid_amount' => 10,
'placed_at' => now()->subMinutes(10),
'paid_at' => now()->subMinutes(9),
'activated_at' => now()->subMinutes(8),
'meta' => [
'batch_activation' => [
'run_id' => $runId,
],
'subscription_activation_error' => [
'message' => '模拟失败:同步异常',
'at' => now()->toDateTimeString(),
'admin_id' => 1,
],
],
]);
PlatformOrder::query()->create([
'merchant_id' => $merchant->id,
'plan_id' => $plan->id,
'order_no' => 'PO_BATCH_FALLBACK_B',
'order_type' => 'new_purchase',
'status' => 'activated',
'payment_status' => 'paid',
'plan_name' => $plan->name,
'billing_cycle' => $plan->billing_cycle,
'period_months' => 1,
'quantity' => 1,
'payable_amount' => 10,
'paid_amount' => 10,
'placed_at' => now()->subMinutes(7),
'paid_at' => now()->subMinutes(6),
'activated_at' => now()->subMinutes(5),
'meta' => [
'batch_activation' => [
'run_id' => $runId,
],
],
]);
$html = $this->get('/admin/platform-batches/show?type=bas&run_id=' . $runId)
->assertOk()
->getContent();
$this->assertStringContainsString('本批次尚未生成 last_result 汇总', $html);
$this->assertStringContainsString('data-role="batch-matched-link"', $html);
$this->assertStringContainsString('>2<', $html);
$this->assertStringContainsString('data-role="batch-failed-count-link"', $html);
$this->assertStringContainsString('>1<', $html);
$this->assertStringContainsString('失败占比:', $html);
$this->assertStringContainsString('50%', $html);
}
}

View File

@@ -0,0 +1,19 @@
<?php
namespace Tests\Feature;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Tests\TestCase;
class AdminPlatformBatchShowPageGuestCannotAccessTest extends TestCase
{
use RefreshDatabase;
public function test_guest_cannot_access_platform_batch_show_page(): void
{
$this->seed();
$this->get('/admin/platform-batches/show?type=bas&run_id=BAS_GUEST_0001')
->assertRedirect('/admin/login');
}
}

View File

@@ -0,0 +1,55 @@
<?php
namespace Tests\Feature;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Tests\TestCase;
class AdminPlatformBatchShowPageInvalidTypeAndEmptyStateTest extends TestCase
{
use RefreshDatabase;
protected function loginAsPlatformAdmin(): void
{
$this->seed();
$this->post('/admin/login', [
'email' => 'platform.admin@demo.local',
'password' => 'Platform@123456',
])->assertRedirect('/admin');
}
public function test_platform_batch_show_page_should_render_error_when_type_is_invalid(): void
{
$this->loginAsPlatformAdmin();
$html = $this->get('/admin/platform-batches/show?type=unknown&run_id=RUN_INVALID_TYPE_0001')
->assertOk()
->getContent();
$this->assertStringContainsString('参数不完整:请提供 typebas/bmpa与 run_id。', $html);
$this->assertStringContainsString('href="/admin/platform-orders"', $html);
$this->assertStringNotContainsString('data-action="copy-run-id"', $html);
$this->assertStringNotContainsString('data-role="batch-spot-check-link"', $html);
}
public function test_platform_batch_show_page_should_render_empty_batch_zero_state_when_run_id_has_no_orders(): void
{
$this->loginAsPlatformAdmin();
$html = $this->get('/admin/platform-batches/show?type=bas&run_id=BAS_EMPTY_0001')
->assertOk()
->getContent();
$this->assertStringContainsString('BAS批量同步订阅 批次详情', $html);
$this->assertStringContainsString('run_id<strong>BAS_EMPTY_0001</strong>', $html);
$this->assertStringContainsString('本批次尚未生成 last_result 汇总', $html);
$this->assertStringContainsString('data-role="batch-matched-link"', $html);
$this->assertStringContainsString('>0</a>', $html);
$this->assertStringContainsString('暂无可抽样订单(可能暂无成功单,或 last_result 尚未补齐)。', $html);
$this->assertStringContainsString('Top 失败原因', $html);
$this->assertStringContainsString('暂无last_result 未写入时不做原因聚合,以免在页面侧引入重查询)。', $html);
$this->assertStringContainsString('batch_activation_run_id=BAS_EMPTY_0001', $html);
$this->assertStringNotContainsString('data-role="batch-failed-ratio-link"', $html);
}
}

View File

@@ -0,0 +1,100 @@
<?php
namespace Tests\Feature;
use App\Models\Merchant;
use App\Models\Plan;
use App\Models\PlatformOrder;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Support\Arr;
use Tests\TestCase;
class AdminPlatformBatchShowPageRefreshLinkShouldUseBatchShowSelfBackWhenOuterBackSafeTest extends TestCase
{
use RefreshDatabase;
protected function loginAsPlatformAdmin(): void
{
$this->seed();
$this->post('/admin/login', [
'email' => 'platform.admin@demo.local',
'password' => 'Platform@123456',
])->assertRedirect('/admin');
}
public function test_refresh_link_should_use_batch_show_self_back_when_outer_back_safe(): void
{
$this->loginAsPlatformAdmin();
$merchant = Merchant::query()->firstOrFail();
$plan = Plan::query()->create([
'code' => 'batch_show_refresh_safe_back_plan',
'name' => '批次详情刷新回链测试套餐',
'billing_cycle' => 'monthly',
'price' => 10,
'list_price' => 10,
'status' => 'active',
'sort' => 10,
'published_at' => now(),
]);
$runId = 'BAS_REFRESH_SAFE_BACK_0001';
PlatformOrder::query()->create([
'merchant_id' => $merchant->id,
'plan_id' => $plan->id,
'order_no' => 'PO_BATCH_REFRESH_SAFE_BACK_0001',
'order_type' => 'new_purchase',
'status' => 'activated',
'payment_status' => 'paid',
'plan_name' => $plan->name,
'billing_cycle' => $plan->billing_cycle,
'period_months' => 1,
'quantity' => 1,
'payable_amount' => 10,
'paid_amount' => 10,
'placed_at' => now()->subMinutes(10),
'paid_at' => now()->subMinutes(9),
'activated_at' => now()->subMinutes(8),
'meta' => [
'batch_activation' => [
'run_id' => $runId,
'last_result' => [
'run_id' => $runId,
'success' => 1,
'failed' => 0,
'matched' => 1,
'processed' => 1,
'top_reasons' => [],
'at' => now()->toDateTimeString(),
],
],
],
]);
$safeBack = '/admin/platform-orders?' . Arr::query([
'status' => 'pending',
'keyword' => '批次刷新',
]);
$res = $this->get('/admin/platform-batches/show?type=bas&run_id=' . $runId . '&back=' . urlencode($safeBack));
$res->assertOk();
$batchShowSelf = '/admin/platform-batches/show?' . Arr::query([
'type' => 'bas',
'run_id' => $runId,
]);
$expectedRefreshUrl = '/admin/platform-batches/show?' . Arr::query([
'type' => 'bas',
'run_id' => $runId,
'back' => $batchShowSelf,
]);
$res->assertSee(str_replace('&', '&amp;', $safeBack), false);
$res->assertSee('返回上一页');
$res->assertSee($expectedRefreshUrl, false);
$res->assertDontSee('run_id=' . $runId . '&back=' . $safeBack, false);
$res->assertDontSee('back%3D', false);
}
}

View File

@@ -0,0 +1,35 @@
<?php
namespace Tests\Feature;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Tests\TestCase;
class AdminPlatformBatchShowPageShouldRenderErrorWhenParamsMissingTest extends TestCase
{
use RefreshDatabase;
protected function loginAsPlatformAdmin(): void
{
$this->seed();
$this->post('/admin/login', [
'email' => 'platform.admin@demo.local',
'password' => 'Platform@123456',
])->assertRedirect('/admin');
}
public function test_batch_show_page_should_render_error_when_type_or_run_id_missing(): void
{
$this->loginAsPlatformAdmin();
$html = $this->get('/admin/platform-batches/show?type=bas')
->assertOk()
->getContent();
$this->assertStringContainsString('参数不完整:请提供 typebas/bmpa与 run_id。', $html);
$this->assertStringContainsString('href="/admin/platform-orders"', $html);
$this->assertStringNotContainsString('data-action="copy-run-id"', $html);
$this->assertStringNotContainsString('data-role="batch-spot-check-link"', $html);
}
}

View File

@@ -0,0 +1,92 @@
<?php
namespace Tests\Feature;
use App\Models\Merchant;
use App\Models\Plan;
use App\Models\PlatformOrder;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Tests\TestCase;
class AdminPlatformBatchShowPageSpotCheckEmptyStateShouldNotRenderActionButtonsTest extends TestCase
{
use RefreshDatabase;
protected function loginAsPlatformAdmin(): void
{
$this->seed();
$this->post('/admin/login', [
'email' => 'platform.admin@demo.local',
'password' => 'Platform@123456',
])->assertRedirect('/admin');
}
public function test_batch_show_page_spot_check_empty_state_should_not_render_action_buttons_when_no_success_sample(): void
{
$this->loginAsPlatformAdmin();
$merchant = Merchant::query()->firstOrFail();
$plan = Plan::query()->create([
'code' => 'plan_batch_spot_empty_actions_0001',
'name' => '批次页抽样复核空态动作护栏测试套餐',
'billing_cycle' => 'monthly',
'price' => 10,
'list_price' => 10,
'status' => 'active',
'sort' => 10,
'published_at' => now(),
]);
$runId = 'BMPA_SPOT_EMPTY_ACTIONS_0001';
PlatformOrder::query()->create([
'merchant_id' => $merchant->id,
'plan_id' => $plan->id,
'order_no' => 'PO_BMPA_SPOT_EMPTY_ACTIONS_0001',
'order_type' => 'new_purchase',
'status' => 'pending',
'payment_status' => 'paid',
'plan_name' => $plan->name,
'billing_cycle' => $plan->billing_cycle,
'period_months' => 1,
'quantity' => 1,
'payable_amount' => 10,
'paid_amount' => 10,
'placed_at' => now()->subMinutes(10),
'paid_at' => now()->subMinutes(9),
'meta' => [
'batch_mark_paid_and_activate' => [
'run_id' => $runId,
'last_result' => [
'run_id' => $runId,
'success' => 0,
'failed' => 1,
'matched' => 1,
'processed' => 1,
'top_reasons' => [
['reason' => '模拟失败:无成功样本', 'count' => 1],
],
'at' => now()->toDateTimeString(),
],
],
'batch_mark_paid_and_activate_error' => [
'message' => '模拟失败:无成功样本',
'at' => now()->toDateTimeString(),
],
],
]);
$html = $this->get('/admin/platform-batches/show?type=bmpa&run_id=' . $runId)
->assertOk()
->getContent();
$this->assertStringContainsString('data-role="platform-batch-spot-check"', $html);
$this->assertStringContainsString('暂无可抽样订单(可能暂无成功单,或 last_result 尚未补齐)。', $html);
$this->assertStringNotContainsString('data-role="batch-spot-check-link"', $html);
$this->assertStringNotContainsString('data-role="copy-spot-check-link"', $html);
$this->assertStringNotContainsString('data-role="copy-spot-check-order-id"', $html);
$this->assertStringNotContainsString('data-role="copy-spot-check-run-id"', $html);
$this->assertStringNotContainsString('data-role="batch-spot-check-next"', $html);
}
}

View File

@@ -0,0 +1,85 @@
<?php
namespace Tests\Feature;
use App\Models\Merchant;
use App\Models\Plan;
use App\Models\PlatformOrder;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Tests\TestCase;
class AdminPlatformBatchShowPageSpotCheckExhaustedResetLinkShouldKeepBackTest extends TestCase
{
use RefreshDatabase;
protected function loginAsPlatformAdmin(): void
{
$this->seed();
$this->post('/admin/login', [
'email' => 'platform.admin@demo.local',
'password' => 'Platform@123456',
])->assertRedirect('/admin');
}
public function test_batch_show_page_spot_check_exhausted_reset_link_should_keep_incoming_back(): void
{
$this->loginAsPlatformAdmin();
$merchant = Merchant::query()->firstOrFail();
$plan = Plan::query()->create([
'code' => 'plan_batch_spot_exhausted_keep_back_0001',
'name' => '批次页抽样复核翻到底仍保留 back 测试套餐',
'billing_cycle' => 'monthly',
'price' => 10,
'list_price' => 10,
'status' => 'active',
'sort' => 10,
'published_at' => now(),
]);
$runId = 'BMPA_SPOT_EXHAUSTED_KEEP_BACK_0001';
$order = PlatformOrder::query()->create([
'merchant_id' => $merchant->id,
'plan_id' => $plan->id,
'order_no' => 'PO_BMPA_SPOT_EXHAUSTED_KEEP_BACK_0001',
'order_type' => 'new_purchase',
'status' => 'activated',
'payment_status' => 'paid',
'plan_name' => $plan->name,
'billing_cycle' => $plan->billing_cycle,
'period_months' => 1,
'quantity' => 1,
'payable_amount' => 10,
'paid_amount' => 10,
'placed_at' => now()->subMinutes(10),
'paid_at' => now()->subMinutes(9),
'activated_at' => now()->subMinutes(8),
'meta' => [
'batch_mark_paid_and_activate' => [
'run_id' => $runId,
'last_result' => [
'run_id' => $runId,
'success' => 1,
'failed' => 0,
'matched' => 1,
'processed' => 1,
'top_reasons' => [],
'at' => now()->toDateTimeString(),
],
],
],
]);
$incomingBack = '/admin/platform-orders?batch_bmpa_run_id=' . $runId;
$html = $this->get('/admin/platform-batches/show?type=bmpa&run_id=' . $runId . '&spot_after_id=' . $order->id . '&back=' . urlencode($incomingBack))
->assertOk()
->getContent();
$this->assertStringContainsString('data-role="batch-spot-check-reset"', $html);
$this->assertStringContainsString('back=' . urlencode($incomingBack), $html);
$this->assertStringNotContainsString('data-role="batch-spot-check-next"', $html);
}
}

View File

@@ -0,0 +1,88 @@
<?php
namespace Tests\Feature;
use App\Models\Merchant;
use App\Models\Plan;
use App\Models\PlatformOrder;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Tests\TestCase;
class AdminPlatformBatchShowPageSpotCheckExhaustedShouldStillRenderResetLinkTest extends TestCase
{
use RefreshDatabase;
protected function loginAsPlatformAdmin(): void
{
$this->seed();
$this->post('/admin/login', [
'email' => 'platform.admin@demo.local',
'password' => 'Platform@123456',
])->assertRedirect('/admin');
}
public function test_batch_show_page_should_still_render_reset_link_when_spot_check_is_exhausted(): void
{
$this->loginAsPlatformAdmin();
$merchant = Merchant::query()->firstOrFail();
$plan = Plan::query()->create([
'code' => 'plan_batch_spot_check_exhausted',
'name' => '批次抽样复核翻到底测试套餐',
'billing_cycle' => 'monthly',
'price' => 10,
'list_price' => 10,
'status' => 'active',
'sort' => 10,
'published_at' => now(),
]);
$runId = 'BMPA_SPOT_EXHAUSTED_0001';
$order = PlatformOrder::query()->create([
'merchant_id' => $merchant->id,
'plan_id' => $plan->id,
'order_no' => 'PO_SPOT_EXHAUSTED_0001',
'order_type' => 'new_purchase',
'status' => 'activated',
'payment_status' => 'paid',
'plan_name' => $plan->name,
'billing_cycle' => $plan->billing_cycle,
'period_months' => 1,
'quantity' => 1,
'payable_amount' => 10,
'paid_amount' => 10,
'placed_at' => now()->subMinutes(10),
'paid_at' => now()->subMinutes(9),
'activated_at' => now()->subMinutes(8),
'meta' => [
'batch_mark_paid_and_activate' => [
'at' => now()->toDateTimeString(),
'admin_id' => 1,
'scope' => 'filtered',
'mode' => 'queue',
'run_id' => $runId,
'last_result' => [
'run_id' => $runId,
'success' => 1,
'failed' => 0,
'matched' => 1,
'processed' => 1,
'top_reasons' => [],
'at' => now()->toDateTimeString(),
],
],
],
]);
$html = $this->get('/admin/platform-batches/show?type=bmpa&run_id=' . $runId . '&spot_after_id=' . $order->id)
->assertOk()
->getContent();
$this->assertStringContainsString('暂无可抽样订单(可能暂无成功单,或 last_result 尚未补齐)。', $html);
$this->assertStringContainsString('data-role="batch-spot-check-reset"', $html);
$this->assertStringContainsString('href="/admin/platform-batches/show?type=bmpa&amp;run_id=' . $runId . '"', $html);
$this->assertStringNotContainsString('data-role="batch-spot-check-next"', $html);
}
}

View File

@@ -0,0 +1,127 @@
<?php
namespace Tests\Feature;
use App\Models\Merchant;
use App\Models\Plan;
use App\Models\PlatformOrder;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Tests\TestCase;
class AdminPlatformBatchShowPageSpotCheckLinkBackShouldIncludeSpotAfterIdWhenPresentForBasTest extends TestCase
{
use RefreshDatabase;
protected function loginAsPlatformAdmin(): void
{
$this->seed();
$this->post('/admin/login', [
'email' => 'platform.admin@demo.local',
'password' => 'Platform@123456',
])->assertRedirect('/admin');
}
public function test_batch_show_page_spot_check_link_back_should_include_spot_after_id_when_present_for_bas(): void
{
$this->loginAsPlatformAdmin();
$merchant = Merchant::query()->firstOrFail();
$plan = Plan::query()->create([
'code' => 'plan_bas_spot_back_after_id_0001',
'name' => 'BAS 批次页抽样复核 back 包含 spot_after_id 护栏测试套餐',
'billing_cycle' => 'monthly',
'price' => 10,
'list_price' => 10,
'status' => 'active',
'sort' => 10,
'published_at' => now(),
]);
$runId = 'BAS_SPOT_BACK_AFTER_ID_0001';
$orderA = PlatformOrder::query()->create([
'merchant_id' => $merchant->id,
'plan_id' => $plan->id,
'order_no' => 'PO_BAS_SPOT_BACK_AFTER_ID_A',
'order_type' => 'new_purchase',
'status' => 'activated',
'payment_status' => 'paid',
'plan_name' => $plan->name,
'billing_cycle' => $plan->billing_cycle,
'period_months' => 1,
'quantity' => 1,
'payable_amount' => 10,
'paid_amount' => 10,
'placed_at' => now()->subMinutes(11),
'paid_at' => now()->subMinutes(10),
'activated_at' => now()->subMinutes(9),
'meta' => [
'batch_activation' => [
'run_id' => $runId,
'last_result' => [
'run_id' => $runId,
'success' => 2,
'failed' => 0,
'matched' => 2,
'processed' => 2,
'top_reasons' => [],
'at' => now()->toDateTimeString(),
],
],
'subscription_activation' => [
'subscription_id' => 201,
'synced_at' => now()->toDateTimeString(),
],
],
]);
$orderB = PlatformOrder::query()->create([
'merchant_id' => $merchant->id,
'plan_id' => $plan->id,
'order_no' => 'PO_BAS_SPOT_BACK_AFTER_ID_B',
'order_type' => 'new_purchase',
'status' => 'activated',
'payment_status' => 'paid',
'plan_name' => $plan->name,
'billing_cycle' => $plan->billing_cycle,
'period_months' => 1,
'quantity' => 1,
'payable_amount' => 10,
'paid_amount' => 10,
'placed_at' => now()->subMinutes(8),
'paid_at' => now()->subMinutes(7),
'activated_at' => now()->subMinutes(6),
'meta' => [
'batch_activation' => [
'run_id' => $runId,
'last_result' => [
'run_id' => $runId,
'success' => 2,
'failed' => 0,
'matched' => 2,
'processed' => 2,
'top_reasons' => [],
'at' => now()->toDateTimeString(),
],
],
'subscription_activation' => [
'subscription_id' => 202,
'synced_at' => now()->toDateTimeString(),
],
],
]);
// spot_after_id=orderB 将抽样切换到更早的 orderA
$html = $this->get('/admin/platform-batches/show?type=bas&run_id=' . $runId . '&spot_after_id=' . $orderB->id)
->assertOk()
->getContent();
$expectedBack = '/admin/platform-batches/show?type=bas&run_id=' . $runId . '&spot_after_id=' . $orderB->id;
$this->assertStringContainsString('data-role="batch-spot-check-link"', $html);
$this->assertStringContainsString('/admin/platform-orders/' . $orderA->id, $html);
$this->assertStringContainsString('#subscription-sync', $html);
$this->assertStringContainsString('back=' . urlencode($expectedBack), $html);
}
}

View File

@@ -0,0 +1,118 @@
<?php
namespace Tests\Feature;
use App\Models\Merchant;
use App\Models\Plan;
use App\Models\PlatformOrder;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Tests\TestCase;
class AdminPlatformBatchShowPageSpotCheckLinkBackShouldIncludeSpotAfterIdWhenPresentTest extends TestCase
{
use RefreshDatabase;
protected function loginAsPlatformAdmin(): void
{
$this->seed();
$this->post('/admin/login', [
'email' => 'platform.admin@demo.local',
'password' => 'Platform@123456',
])->assertRedirect('/admin');
}
public function test_batch_show_page_spot_check_link_back_should_include_spot_after_id_when_present(): void
{
$this->loginAsPlatformAdmin();
$merchant = Merchant::query()->firstOrFail();
$plan = Plan::query()->create([
'code' => 'plan_batch_spot_back_after_id_0001',
'name' => '批次页抽样复核 back 包含 spot_after_id 护栏测试套餐',
'billing_cycle' => 'monthly',
'price' => 10,
'list_price' => 10,
'status' => 'active',
'sort' => 10,
'published_at' => now(),
]);
$runId = 'BMPA_SPOT_BACK_AFTER_ID_0001';
$orderA = PlatformOrder::query()->create([
'merchant_id' => $merchant->id,
'plan_id' => $plan->id,
'order_no' => 'PO_BMPA_SPOT_BACK_AFTER_ID_A',
'order_type' => 'new_purchase',
'status' => 'activated',
'payment_status' => 'paid',
'plan_name' => $plan->name,
'billing_cycle' => $plan->billing_cycle,
'period_months' => 1,
'quantity' => 1,
'payable_amount' => 10,
'paid_amount' => 10,
'placed_at' => now()->subMinutes(11),
'paid_at' => now()->subMinutes(10),
'activated_at' => now()->subMinutes(9),
'meta' => [
'batch_mark_paid_and_activate' => [
'run_id' => $runId,
'last_result' => [
'run_id' => $runId,
'success' => 2,
'failed' => 0,
'matched' => 2,
'processed' => 2,
'top_reasons' => [],
'at' => now()->toDateTimeString(),
],
],
],
]);
$orderB = PlatformOrder::query()->create([
'merchant_id' => $merchant->id,
'plan_id' => $plan->id,
'order_no' => 'PO_BMPA_SPOT_BACK_AFTER_ID_B',
'order_type' => 'new_purchase',
'status' => 'activated',
'payment_status' => 'paid',
'plan_name' => $plan->name,
'billing_cycle' => $plan->billing_cycle,
'period_months' => 1,
'quantity' => 1,
'payable_amount' => 10,
'paid_amount' => 10,
'placed_at' => now()->subMinutes(8),
'paid_at' => now()->subMinutes(7),
'activated_at' => now()->subMinutes(6),
'meta' => [
'batch_mark_paid_and_activate' => [
'run_id' => $runId,
'last_result' => [
'run_id' => $runId,
'success' => 2,
'failed' => 0,
'matched' => 2,
'processed' => 2,
'top_reasons' => [],
'at' => now()->toDateTimeString(),
],
],
],
]);
// spot_after_id=orderB 将抽样切换到更早的 orderA
$html = $this->get('/admin/platform-batches/show?type=bmpa&run_id=' . $runId . '&spot_after_id=' . $orderB->id)
->assertOk()
->getContent();
$expectedBack = '/admin/platform-batches/show?type=bmpa&run_id=' . $runId . '&spot_after_id=' . $orderB->id;
$this->assertStringContainsString('data-role="batch-spot-check-link"', $html);
$this->assertStringContainsString('/admin/platform-orders/' . $orderA->id, $html);
$this->assertStringContainsString('back=' . urlencode($expectedBack), $html);
}
}

View File

@@ -0,0 +1,84 @@
<?php
namespace Tests\Feature;
use App\Models\Merchant;
use App\Models\Plan;
use App\Models\PlatformOrder;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Tests\TestCase;
class AdminPlatformBatchShowPageSpotCheckLinkBackShouldReturnToBatchPageTest extends TestCase
{
use RefreshDatabase;
protected function loginAsPlatformAdmin(): void
{
$this->seed();
$this->post('/admin/login', [
'email' => 'platform.admin@demo.local',
'password' => 'Platform@123456',
])->assertRedirect('/admin');
}
public function test_batch_show_page_spot_check_link_should_have_back_to_current_batch_page(): void
{
$this->loginAsPlatformAdmin();
$merchant = Merchant::query()->firstOrFail();
$plan = Plan::query()->create([
'code' => 'plan_batch_spot_back_0001',
'name' => '批次页抽样复核 back 回批次页护栏测试套餐',
'billing_cycle' => 'monthly',
'price' => 10,
'list_price' => 10,
'status' => 'active',
'sort' => 10,
'published_at' => now(),
]);
$runId = 'BMPA_SPOT_BACK_0001';
PlatformOrder::query()->create([
'merchant_id' => $merchant->id,
'plan_id' => $plan->id,
'order_no' => 'PO_BMPA_SPOT_BACK_0001',
'order_type' => 'new_purchase',
'status' => 'activated',
'payment_status' => 'paid',
'plan_name' => $plan->name,
'billing_cycle' => $plan->billing_cycle,
'period_months' => 1,
'quantity' => 1,
'payable_amount' => 10,
'paid_amount' => 10,
'placed_at' => now()->subMinutes(10),
'paid_at' => now()->subMinutes(9),
'activated_at' => now()->subMinutes(8),
'meta' => [
'batch_mark_paid_and_activate' => [
'run_id' => $runId,
'last_result' => [
'run_id' => $runId,
'success' => 1,
'failed' => 0,
'matched' => 1,
'processed' => 1,
'top_reasons' => [],
'at' => now()->toDateTimeString(),
],
],
],
]);
$html = $this->get('/admin/platform-batches/show?type=bmpa&run_id=' . $runId)
->assertOk()
->getContent();
$expectedBack = '/admin/platform-batches/show?type=bmpa&run_id=' . $runId;
$this->assertStringContainsString('data-role="batch-spot-check-link"', $html);
$this->assertStringContainsString('back=' . urlencode($expectedBack), $html);
}
}

View File

@@ -0,0 +1,124 @@
<?php
namespace Tests\Feature;
use App\Models\Merchant;
use App\Models\Plan;
use App\Models\PlatformOrder;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Tests\TestCase;
class AdminPlatformBatchShowPageSpotCheckNextLinkShouldKeepBackForBasTest extends TestCase
{
use RefreshDatabase;
protected function loginAsPlatformAdmin(): void
{
$this->seed();
$this->post('/admin/login', [
'email' => 'platform.admin@demo.local',
'password' => 'Platform@123456',
])->assertRedirect('/admin');
}
public function test_batch_show_page_spot_check_next_link_should_keep_incoming_back_for_bas(): void
{
$this->loginAsPlatformAdmin();
$merchant = Merchant::query()->firstOrFail();
$plan = Plan::query()->create([
'code' => 'plan_bas_spot_next_keep_back_0001',
'name' => 'BAS 批次页换一单入口保留 back 护栏测试套餐',
'billing_cycle' => 'monthly',
'price' => 10,
'list_price' => 10,
'status' => 'active',
'sort' => 10,
'published_at' => now(),
]);
$runId = 'BAS_SPOT_NEXT_KEEP_BACK_0001';
PlatformOrder::query()->create([
'merchant_id' => $merchant->id,
'plan_id' => $plan->id,
'order_no' => 'PO_BAS_SPOT_NEXT_KEEP_BACK_A',
'order_type' => 'new_purchase',
'status' => 'activated',
'payment_status' => 'paid',
'plan_name' => $plan->name,
'billing_cycle' => $plan->billing_cycle,
'period_months' => 1,
'quantity' => 1,
'payable_amount' => 10,
'paid_amount' => 10,
'placed_at' => now()->subMinutes(11),
'paid_at' => now()->subMinutes(10),
'activated_at' => now()->subMinutes(9),
'meta' => [
'batch_activation' => [
'run_id' => $runId,
'last_result' => [
'run_id' => $runId,
'success' => 2,
'failed' => 0,
'matched' => 2,
'processed' => 2,
'top_reasons' => [],
'at' => now()->toDateTimeString(),
],
],
'subscription_activation' => [
'subscription_id' => 101,
'synced_at' => now()->toDateTimeString(),
],
],
]);
PlatformOrder::query()->create([
'merchant_id' => $merchant->id,
'plan_id' => $plan->id,
'order_no' => 'PO_BAS_SPOT_NEXT_KEEP_BACK_B',
'order_type' => 'new_purchase',
'status' => 'activated',
'payment_status' => 'paid',
'plan_name' => $plan->name,
'billing_cycle' => $plan->billing_cycle,
'period_months' => 1,
'quantity' => 1,
'payable_amount' => 10,
'paid_amount' => 10,
'placed_at' => now()->subMinutes(8),
'paid_at' => now()->subMinutes(7),
'activated_at' => now()->subMinutes(6),
'meta' => [
'batch_activation' => [
'run_id' => $runId,
'last_result' => [
'run_id' => $runId,
'success' => 2,
'failed' => 0,
'matched' => 2,
'processed' => 2,
'top_reasons' => [],
'at' => now()->toDateTimeString(),
],
],
'subscription_activation' => [
'subscription_id' => 102,
'synced_at' => now()->toDateTimeString(),
],
],
]);
$incomingBack = '/admin/platform-orders?batch_activation_run_id=' . $runId;
$html = $this->get('/admin/platform-batches/show?type=bas&run_id=' . $runId . '&back=' . urlencode($incomingBack))
->assertOk()
->getContent();
$this->assertStringContainsString('data-role="batch-spot-check-next"', $html);
$this->assertStringContainsString('back=' . urlencode($incomingBack), $html);
}
}

View File

@@ -0,0 +1,116 @@
<?php
namespace Tests\Feature;
use App\Models\Merchant;
use App\Models\Plan;
use App\Models\PlatformOrder;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Tests\TestCase;
class AdminPlatformBatchShowPageSpotCheckNextLinkShouldKeepBackTest extends TestCase
{
use RefreshDatabase;
protected function loginAsPlatformAdmin(): void
{
$this->seed();
$this->post('/admin/login', [
'email' => 'platform.admin@demo.local',
'password' => 'Platform@123456',
])->assertRedirect('/admin');
}
public function test_batch_show_page_spot_check_next_link_should_keep_incoming_back(): void
{
$this->loginAsPlatformAdmin();
$merchant = Merchant::query()->firstOrFail();
$plan = Plan::query()->create([
'code' => 'plan_batch_spot_next_keep_back_0001',
'name' => '批次页换一单入口保留 back 护栏测试套餐',
'billing_cycle' => 'monthly',
'price' => 10,
'list_price' => 10,
'status' => 'active',
'sort' => 10,
'published_at' => now(),
]);
$runId = 'BMPA_SPOT_NEXT_KEEP_BACK_0001';
PlatformOrder::query()->create([
'merchant_id' => $merchant->id,
'plan_id' => $plan->id,
'order_no' => 'PO_BMPA_SPOT_NEXT_KEEP_BACK_A',
'order_type' => 'new_purchase',
'status' => 'activated',
'payment_status' => 'paid',
'plan_name' => $plan->name,
'billing_cycle' => $plan->billing_cycle,
'period_months' => 1,
'quantity' => 1,
'payable_amount' => 10,
'paid_amount' => 10,
'placed_at' => now()->subMinutes(11),
'paid_at' => now()->subMinutes(10),
'activated_at' => now()->subMinutes(9),
'meta' => [
'batch_mark_paid_and_activate' => [
'run_id' => $runId,
'last_result' => [
'run_id' => $runId,
'success' => 2,
'failed' => 0,
'matched' => 2,
'processed' => 2,
'top_reasons' => [],
'at' => now()->toDateTimeString(),
],
],
],
]);
PlatformOrder::query()->create([
'merchant_id' => $merchant->id,
'plan_id' => $plan->id,
'order_no' => 'PO_BMPA_SPOT_NEXT_KEEP_BACK_B',
'order_type' => 'new_purchase',
'status' => 'activated',
'payment_status' => 'paid',
'plan_name' => $plan->name,
'billing_cycle' => $plan->billing_cycle,
'period_months' => 1,
'quantity' => 1,
'payable_amount' => 10,
'paid_amount' => 10,
'placed_at' => now()->subMinutes(8),
'paid_at' => now()->subMinutes(7),
'activated_at' => now()->subMinutes(6),
'meta' => [
'batch_mark_paid_and_activate' => [
'run_id' => $runId,
'last_result' => [
'run_id' => $runId,
'success' => 2,
'failed' => 0,
'matched' => 2,
'processed' => 2,
'top_reasons' => [],
'at' => now()->toDateTimeString(),
],
],
],
]);
$incomingBack = '/admin/platform-orders?batch_bmpa_run_id=' . $runId;
$html = $this->get('/admin/platform-batches/show?type=bmpa&run_id=' . $runId . '&back=' . urlencode($incomingBack))
->assertOk()
->getContent();
$this->assertStringContainsString('data-role="batch-spot-check-next"', $html);
$this->assertStringContainsString('back=' . urlencode($incomingBack), $html);
}
}

View File

@@ -0,0 +1,127 @@
<?php
namespace Tests\Feature;
use App\Models\Merchant;
use App\Models\Plan;
use App\Models\PlatformOrder;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Tests\TestCase;
class AdminPlatformBatchShowPageSpotCheckResetLinkShouldKeepBackForBasTest extends TestCase
{
use RefreshDatabase;
protected function loginAsPlatformAdmin(): void
{
$this->seed();
$this->post('/admin/login', [
'email' => 'platform.admin@demo.local',
'password' => 'Platform@123456',
])->assertRedirect('/admin');
}
public function test_batch_show_page_spot_check_reset_link_should_keep_incoming_back_for_bas(): void
{
$this->loginAsPlatformAdmin();
$merchant = Merchant::query()->firstOrFail();
$plan = Plan::query()->create([
'code' => 'plan_bas_spot_reset_keep_back_0001',
'name' => 'BAS 批次页回到最新入口保留 back 护栏测试套餐',
'billing_cycle' => 'monthly',
'price' => 10,
'list_price' => 10,
'status' => 'active',
'sort' => 10,
'published_at' => now(),
]);
$runId = 'BAS_SPOT_RESET_KEEP_BACK_0001';
PlatformOrder::query()->create([
'merchant_id' => $merchant->id,
'plan_id' => $plan->id,
'order_no' => 'PO_BAS_SPOT_RESET_KEEP_BACK_A',
'order_type' => 'new_purchase',
'status' => 'activated',
'payment_status' => 'paid',
'plan_name' => $plan->name,
'billing_cycle' => $plan->billing_cycle,
'period_months' => 1,
'quantity' => 1,
'payable_amount' => 10,
'paid_amount' => 10,
'placed_at' => now()->subMinutes(11),
'paid_at' => now()->subMinutes(10),
'activated_at' => now()->subMinutes(9),
'meta' => [
'batch_activation' => [
'run_id' => $runId,
'last_result' => [
'run_id' => $runId,
'success' => 2,
'failed' => 0,
'matched' => 2,
'processed' => 2,
'top_reasons' => [],
'at' => now()->toDateTimeString(),
],
],
'subscription_activation' => [
'subscription_id' => 301,
'synced_at' => now()->toDateTimeString(),
],
],
]);
$orderB = PlatformOrder::query()->create([
'merchant_id' => $merchant->id,
'plan_id' => $plan->id,
'order_no' => 'PO_BAS_SPOT_RESET_KEEP_BACK_B',
'order_type' => 'new_purchase',
'status' => 'activated',
'payment_status' => 'paid',
'plan_name' => $plan->name,
'billing_cycle' => $plan->billing_cycle,
'period_months' => 1,
'quantity' => 1,
'payable_amount' => 10,
'paid_amount' => 10,
'placed_at' => now()->subMinutes(8),
'paid_at' => now()->subMinutes(7),
'activated_at' => now()->subMinutes(6),
'meta' => [
'batch_activation' => [
'run_id' => $runId,
'last_result' => [
'run_id' => $runId,
'success' => 2,
'failed' => 0,
'matched' => 2,
'processed' => 2,
'top_reasons' => [],
'at' => now()->toDateTimeString(),
],
],
'subscription_activation' => [
'subscription_id' => 302,
'synced_at' => now()->toDateTimeString(),
],
],
]);
$incomingBack = '/admin/platform-orders?batch_activation_run_id=' . $runId;
$html = $this->get('/admin/platform-batches/show?type=bas&run_id=' . $runId . '&spot_after_id=' . $orderB->id . '&back=' . urlencode($incomingBack))
->assertOk()
->getContent();
$this->assertStringContainsString('data-role="batch-spot-check-reset"', $html);
$this->assertStringContainsString('back=' . urlencode($incomingBack), $html);
// reset 链接应回到不带 spot_after_id 的 batch page
$this->assertStringNotContainsString('data-role="batch-spot-check-reset" href="/admin/platform-batches/show?type=bas&amp;run_id=' . $runId . '&amp;spot_after_id=', $html);
}
}

View File

@@ -0,0 +1,116 @@
<?php
namespace Tests\Feature;
use App\Models\Merchant;
use App\Models\Plan;
use App\Models\PlatformOrder;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Tests\TestCase;
class AdminPlatformBatchShowPageSpotCheckResetLinkShouldKeepBackTest extends TestCase
{
use RefreshDatabase;
protected function loginAsPlatformAdmin(): void
{
$this->seed();
$this->post('/admin/login', [
'email' => 'platform.admin@demo.local',
'password' => 'Platform@123456',
])->assertRedirect('/admin');
}
public function test_batch_show_page_spot_check_reset_link_should_keep_incoming_back(): void
{
$this->loginAsPlatformAdmin();
$merchant = Merchant::query()->firstOrFail();
$plan = Plan::query()->create([
'code' => 'plan_batch_spot_reset_keep_back_0001',
'name' => '批次页回到最新入口保留 back 护栏测试套餐',
'billing_cycle' => 'monthly',
'price' => 10,
'list_price' => 10,
'status' => 'active',
'sort' => 10,
'published_at' => now(),
]);
$runId = 'BMPA_SPOT_RESET_KEEP_BACK_0001';
PlatformOrder::query()->create([
'merchant_id' => $merchant->id,
'plan_id' => $plan->id,
'order_no' => 'PO_BMPA_SPOT_RESET_KEEP_BACK_A',
'order_type' => 'new_purchase',
'status' => 'activated',
'payment_status' => 'paid',
'plan_name' => $plan->name,
'billing_cycle' => $plan->billing_cycle,
'period_months' => 1,
'quantity' => 1,
'payable_amount' => 10,
'paid_amount' => 10,
'placed_at' => now()->subMinutes(11),
'paid_at' => now()->subMinutes(10),
'activated_at' => now()->subMinutes(9),
'meta' => [
'batch_mark_paid_and_activate' => [
'run_id' => $runId,
'last_result' => [
'run_id' => $runId,
'success' => 2,
'failed' => 0,
'matched' => 2,
'processed' => 2,
'top_reasons' => [],
'at' => now()->toDateTimeString(),
],
],
],
]);
$orderB = PlatformOrder::query()->create([
'merchant_id' => $merchant->id,
'plan_id' => $plan->id,
'order_no' => 'PO_BMPA_SPOT_RESET_KEEP_BACK_B',
'order_type' => 'new_purchase',
'status' => 'activated',
'payment_status' => 'paid',
'plan_name' => $plan->name,
'billing_cycle' => $plan->billing_cycle,
'period_months' => 1,
'quantity' => 1,
'payable_amount' => 10,
'paid_amount' => 10,
'placed_at' => now()->subMinutes(8),
'paid_at' => now()->subMinutes(7),
'activated_at' => now()->subMinutes(6),
'meta' => [
'batch_mark_paid_and_activate' => [
'run_id' => $runId,
'last_result' => [
'run_id' => $runId,
'success' => 2,
'failed' => 0,
'matched' => 2,
'processed' => 2,
'top_reasons' => [],
'at' => now()->toDateTimeString(),
],
],
],
]);
$incomingBack = '/admin/platform-orders?batch_bmpa_run_id=' . $runId;
$html = $this->get('/admin/platform-batches/show?type=bmpa&run_id=' . $runId . '&spot_after_id=' . $orderB->id . '&back=' . urlencode($incomingBack))
->assertOk()
->getContent();
$this->assertStringContainsString('data-role="batch-spot-check-reset"', $html);
$this->assertStringContainsString('back=' . urlencode($incomingBack), $html);
}
}

View File

@@ -0,0 +1,118 @@
<?php
namespace Tests\Feature;
use App\Models\Merchant;
use App\Models\Plan;
use App\Models\PlatformOrder;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Tests\TestCase;
class AdminPlatformBatchShowPageSpotCheckResetLinkShouldRenderWhenSpotAfterIdPresentTest extends TestCase
{
use RefreshDatabase;
protected function loginAsPlatformAdmin(): void
{
$this->seed();
$this->post('/admin/login', [
'email' => 'platform.admin@demo.local',
'password' => 'Platform@123456',
])->assertRedirect('/admin');
}
public function test_batch_show_page_should_render_spot_check_reset_link_when_spot_after_id_present(): void
{
$this->loginAsPlatformAdmin();
$merchant = Merchant::query()->firstOrFail();
$plan = Plan::query()->create([
'code' => 'plan_batch_spot_reset_0001',
'name' => '批次页抽样复核回到最新入口渲染测试套餐',
'billing_cycle' => 'monthly',
'price' => 10,
'list_price' => 10,
'status' => 'active',
'sort' => 10,
'published_at' => now(),
]);
$runId = 'BMPA_SPOT_RESET_0001';
PlatformOrder::query()->create([
'merchant_id' => $merchant->id,
'plan_id' => $plan->id,
'order_no' => 'PO_BMPA_SPOT_RESET_A',
'order_type' => 'new_purchase',
'status' => 'activated',
'payment_status' => 'paid',
'plan_name' => $plan->name,
'billing_cycle' => $plan->billing_cycle,
'period_months' => 1,
'quantity' => 1,
'payable_amount' => 10,
'paid_amount' => 10,
'placed_at' => now()->subMinutes(11),
'paid_at' => now()->subMinutes(10),
'activated_at' => now()->subMinutes(9),
'meta' => [
'batch_mark_paid_and_activate' => [
'run_id' => $runId,
'last_result' => [
'run_id' => $runId,
'success' => 2,
'failed' => 0,
'matched' => 2,
'processed' => 2,
'top_reasons' => [],
'at' => now()->toDateTimeString(),
],
],
],
]);
$orderB = PlatformOrder::query()->create([
'merchant_id' => $merchant->id,
'plan_id' => $plan->id,
'order_no' => 'PO_BMPA_SPOT_RESET_B',
'order_type' => 'new_purchase',
'status' => 'activated',
'payment_status' => 'paid',
'plan_name' => $plan->name,
'billing_cycle' => $plan->billing_cycle,
'period_months' => 1,
'quantity' => 1,
'payable_amount' => 10,
'paid_amount' => 10,
'placed_at' => now()->subMinutes(8),
'paid_at' => now()->subMinutes(7),
'activated_at' => now()->subMinutes(6),
'meta' => [
'batch_mark_paid_and_activate' => [
'run_id' => $runId,
'last_result' => [
'run_id' => $runId,
'success' => 2,
'failed' => 0,
'matched' => 2,
'processed' => 2,
'top_reasons' => [],
'at' => now()->toDateTimeString(),
],
],
],
]);
$html = $this->get('/admin/platform-batches/show?type=bmpa&run_id=' . $runId . '&spot_after_id=' . $orderB->id)
->assertOk()
->getContent();
$this->assertStringContainsString('data-role="batch-spot-check-reset"', $html);
$this->assertStringContainsString('type=bmpa', $html);
$this->assertStringContainsString('run_id=' . $runId, $html);
$this->assertStringContainsString('data-role="batch-spot-check-reset"', $html);
$this->assertStringContainsString('href="/admin/platform-batches/show?type=bmpa&amp;run_id=' . $runId . '"', $html);
$this->assertStringNotContainsString('data-role="batch-spot-check-reset" href="/admin/platform-batches/show?type=bmpa&amp;run_id=' . $runId . '&amp;spot_after_id=', $html);
}
}

View File

@@ -0,0 +1,127 @@
<?php
namespace Tests\Feature;
use App\Models\Merchant;
use App\Models\Plan;
use App\Models\PlatformOrder;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Tests\TestCase;
class AdminPlatformBatchShowPageTopReasonTooLongShouldNotRenderGovernanceLinkTest extends TestCase
{
use RefreshDatabase;
protected function loginAsPlatformAdmin(): void
{
$this->seed();
$this->post('/admin/login', [
'email' => 'platform.admin@demo.local',
'password' => 'Platform@123456',
])->assertRedirect('/admin');
}
public function test_batch_show_page_should_not_render_top_reason_governance_link_when_reason_too_long(): void
{
$this->loginAsPlatformAdmin();
$merchant = Merchant::query()->firstOrFail();
$plan = Plan::query()->create([
'code' => 'plan_batch_show_long_reason_0001',
'name' => '批次页长原因护栏测试套餐',
'billing_cycle' => 'monthly',
'price' => 10,
'list_price' => 10,
'status' => 'active',
'sort' => 10,
'published_at' => now(),
]);
$longReasonBas = str_repeat('B', 260);
$longReasonBmpa = str_repeat('P', 260);
PlatformOrder::query()->create([
'merchant_id' => $merchant->id,
'plan_id' => $plan->id,
'order_no' => 'PO_BATCH_SHOW_LONG_REASON_BAS_0001',
'order_type' => 'new_purchase',
'status' => 'activated',
'payment_status' => 'paid',
'plan_name' => $plan->name,
'billing_cycle' => $plan->billing_cycle,
'period_months' => 1,
'quantity' => 1,
'payable_amount' => 10,
'paid_amount' => 10,
'placed_at' => now()->subMinutes(10),
'paid_at' => now()->subMinutes(9),
'activated_at' => now()->subMinutes(8),
'meta' => [
'batch_activation' => [
'run_id' => 'BAS_LONG_REASON_0001',
'last_result' => [
'run_id' => 'BAS_LONG_REASON_0001',
'success' => 1,
'failed' => 1,
'matched' => 2,
'processed' => 2,
'top_reasons' => [
['reason' => $longReasonBas, 'count' => 1],
],
'at' => now()->toDateTimeString(),
],
],
],
]);
PlatformOrder::query()->create([
'merchant_id' => $merchant->id,
'plan_id' => $plan->id,
'order_no' => 'PO_BATCH_SHOW_LONG_REASON_BMPA_0001',
'order_type' => 'new_purchase',
'status' => 'activated',
'payment_status' => 'paid',
'plan_name' => $plan->name,
'billing_cycle' => $plan->billing_cycle,
'period_months' => 1,
'quantity' => 1,
'payable_amount' => 10,
'paid_amount' => 10,
'placed_at' => now()->subMinutes(7),
'paid_at' => now()->subMinutes(6),
'activated_at' => now()->subMinutes(5),
'meta' => [
'batch_mark_paid_and_activate' => [
'run_id' => 'BMPA_LONG_REASON_0001',
'last_result' => [
'run_id' => 'BMPA_LONG_REASON_0001',
'success' => 1,
'failed' => 1,
'matched' => 2,
'processed' => 2,
'top_reasons' => [
['reason' => $longReasonBmpa, 'count' => 1],
],
'at' => now()->toDateTimeString(),
],
],
],
]);
$basHtml = $this->get('/admin/platform-batches/show?type=bas&run_id=BAS_LONG_REASON_0001')
->assertOk()
->getContent();
$bmpaHtml = $this->get('/admin/platform-batches/show?type=bmpa&run_id=BMPA_LONG_REASON_0001')
->assertOk()
->getContent();
$this->assertStringNotContainsString('sync_error_keyword=', $basHtml);
$this->assertStringContainsString('原因过长,请复制到列表页筛选框', $basHtml);
$this->assertStringNotContainsString('data-role="batch-top-reason-link"', $basHtml);
$this->assertStringNotContainsString('bmpa_error_keyword=', $bmpaHtml);
$this->assertStringContainsString('原因过长,请复制到列表页筛选框', $bmpaHtml);
$this->assertStringNotContainsString('data-role="batch-top-reason-link"', $bmpaHtml);
}
}

View File

@@ -0,0 +1,88 @@
<?php
namespace Tests\Feature;
use App\Models\Merchant;
use App\Models\Plan;
use App\Models\PlatformOrder;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Tests\TestCase;
class AdminPlatformBatchShowPageUnsafeBackShouldBeDroppedTest extends TestCase
{
use RefreshDatabase;
protected function loginAsPlatformAdmin(): void
{
$this->seed();
$this->post('/admin/login', [
'email' => 'platform.admin@demo.local',
'password' => 'Platform@123456',
])->assertRedirect('/admin');
}
public function test_batch_show_page_should_drop_unsafe_back_from_return_and_governance_links(): void
{
$this->loginAsPlatformAdmin();
$merchant = Merchant::query()->firstOrFail();
$plan = Plan::query()->create([
'code' => 'plan_batch_show_unsafe_back_0001',
'name' => '批次页 unsafe back 护栏测试套餐',
'billing_cycle' => 'monthly',
'price' => 10,
'list_price' => 10,
'status' => 'active',
'sort' => 10,
'published_at' => now(),
]);
$runId = 'BAS_UNSAFE_BACK_0001';
PlatformOrder::query()->create([
'merchant_id' => $merchant->id,
'plan_id' => $plan->id,
'order_no' => 'PO_BATCH_SHOW_UNSAFE_BACK_0001',
'order_type' => 'new_purchase',
'status' => 'activated',
'payment_status' => 'paid',
'plan_name' => $plan->name,
'billing_cycle' => $plan->billing_cycle,
'period_months' => 1,
'quantity' => 1,
'payable_amount' => 10,
'paid_amount' => 10,
'placed_at' => now()->subMinutes(10),
'paid_at' => now()->subMinutes(9),
'activated_at' => now()->subMinutes(8),
'meta' => [
'batch_activation' => [
'run_id' => $runId,
'last_result' => [
'run_id' => $runId,
'success' => 1,
'failed' => 1,
'matched' => 2,
'processed' => 2,
'top_reasons' => [
['reason' => '模拟失败:订阅同步异常', 'count' => 1],
],
'at' => now()->toDateTimeString(),
],
],
],
]);
$unsafeBack = '/admin/platform-orders?x=1%26back%3D%2Fadmin';
$html = $this->get('/admin/platform-batches/show?type=bas&run_id=' . $runId . '&back=' . urlencode($unsafeBack))
->assertOk()
->getContent();
$this->assertStringContainsString('href="/admin/platform-orders"', $html);
$this->assertStringNotContainsString('back=' . urlencode($unsafeBack), $html);
$this->assertStringContainsString('batch_activation_run_id=' . $runId, $html);
$this->assertStringNotContainsString('batch_activation_run_id=' . $runId . '&amp;back=', $html);
}
}

View File

@@ -0,0 +1,68 @@
<?php
namespace Tests\Feature;
use App\Models\Plan;
use App\Models\PlatformLead;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Support\Arr;
use Tests\TestCase;
class AdminPlatformLeadIndexPaidNoReceiptGovernanceLinkShouldUseLeadIndexSelfBackTest extends TestCase
{
use RefreshDatabase;
protected function loginAsPlatformAdmin(): void
{
$this->seed();
$this->post('/admin/login', [
'email' => 'platform.admin@demo.local',
'password' => 'Platform@123456',
])->assertRedirect('/admin');
}
public function test_paid_no_receipt_governance_link_should_use_lead_index_self_as_back(): void
{
$this->loginAsPlatformAdmin();
$plan = Plan::query()->create([
'code' => 'lead_index_paid_no_receipt_back_plan',
'name' => '线索页已付无回执入口 back 口径测试套餐',
'billing_cycle' => 'monthly',
'price' => 99,
'list_price' => 99,
'status' => 'active',
'sort' => 10,
'published_at' => now(),
]);
$lead = PlatformLead::query()->create([
'name' => '线索已付无回执回跳测试',
'mobile' => '13900000009',
'email' => 'lead-paid-no-receipt-back@example.com',
'company' => '线索治理回跳测试公司',
'plan_id' => $plan->id,
'source' => 'web',
'status' => 'new',
]);
$res = $this->get('/admin/platform-leads?back=' . urlencode('/admin/platform-orders?payment_status=paid'));
$res->assertOk();
$html = (string) $res->getContent();
$matched = preg_match('/<a[^>]+href="([^"]+)"[^>]*>\s*已付无回执\s*<\/a>/u', $html, $m);
$this->assertSame(1, $matched, '未找到线索页“已付无回执”入口');
$href = html_entity_decode($m[1] ?? '');
$expectedUrl = '/admin/platform-orders?' . Arr::query([
'lead_id' => $lead->id,
'payment_status' => 'paid',
'receipt_status' => 'none',
'back' => '/admin/platform-leads',
]);
$this->assertSame($expectedUrl, $href);
$this->assertStringNotContainsString(urlencode('/admin/platform-orders?payment_status=paid'), $href);
}
}

View File

@@ -0,0 +1,56 @@
<?php
namespace Tests\Feature;
use App\Models\Plan;
use App\Models\PlatformLead;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Tests\TestCase;
class AdminPlatformLeadIndexShouldIncludePaidNoReceiptGovernanceLinkTest extends TestCase
{
use RefreshDatabase;
protected function loginAsPlatformAdmin(): void
{
$this->seed();
$this->post('/admin/login', [
'email' => 'platform.admin@demo.local',
'password' => 'Platform@123456',
])->assertRedirect('/admin');
}
public function test_platform_lead_index_should_include_paid_no_receipt_governance_link(): void
{
$this->loginAsPlatformAdmin();
$plan = Plan::query()->create([
'code' => 'lead_index_paid_no_receipt_plan',
'name' => '线索页已付无回执治理入口测试套餐',
'billing_cycle' => 'monthly',
'price' => 99,
'list_price' => 99,
'status' => 'active',
'sort' => 10,
'published_at' => now(),
]);
$lead = PlatformLead::query()->create([
'name' => '线索已付无回执测试',
'mobile' => '13900000001',
'email' => 'lead-paid-no-receipt@example.com',
'company' => '线索治理测试公司',
'plan_id' => $plan->id,
'source' => 'web',
'status' => 'new',
]);
$res = $this->get('/admin/platform-leads');
$res->assertOk();
$html = (string) $res->getContent();
$this->assertStringContainsString('已付无回执', $html);
$this->assertStringContainsString('/admin/platform-orders?lead_id=' . $lead->id . '&payment_status=paid&receipt_status=none&back=' . urlencode('/admin/platform-leads'), $html);
}
}

View File

@@ -0,0 +1,20 @@
<?php
namespace Tests\Feature;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Tests\TestCase;
class AdminPlatformOrderActionPartialsShouldKeepClassSupportTest extends TestCase
{
use RefreshDatabase;
public function test_action_partials_should_keep_class_support_after_aria_label_extension(): void
{
$summaryPartial = file_get_contents(resource_path('views/admin/platform_orders/_summary_text_link.blade.php'));
$toolPartial = file_get_contents(resource_path('views/admin/platform_orders/_tool_anchor_button.blade.php'));
$this->assertStringContainsString('class="{{ $class ??', $summaryPartial);
$this->assertStringContainsString('class="{{ $class ??', $toolPartial);
}
}

View File

@@ -0,0 +1,20 @@
<?php
namespace Tests\Feature;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Tests\TestCase;
class AdminPlatformOrderActionPartialsShouldKeepHrefSupportTest extends TestCase
{
use RefreshDatabase;
public function test_action_partials_should_keep_href_support_after_aria_label_extension(): void
{
$summaryPartial = file_get_contents(resource_path('views/admin/platform_orders/_summary_text_link.blade.php'));
$toolPartial = file_get_contents(resource_path('views/admin/platform_orders/_tool_anchor_button.blade.php'));
$this->assertStringContainsString('href="{!! $href ??', $summaryPartial);
$this->assertStringContainsString('href="{{ $href ??', $toolPartial);
}
}

View File

@@ -0,0 +1,20 @@
<?php
namespace Tests\Feature;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Tests\TestCase;
class AdminPlatformOrderActionPartialsShouldKeepLabelSupportTest extends TestCase
{
use RefreshDatabase;
public function test_action_partials_should_keep_label_support_after_aria_label_extension(): void
{
$summaryPartial = file_get_contents(resource_path('views/admin/platform_orders/_summary_text_link.blade.php'));
$toolPartial = file_get_contents(resource_path('views/admin/platform_orders/_tool_anchor_button.blade.php'));
$this->assertStringContainsString('{{ $label ??', $summaryPartial);
$this->assertStringContainsString('{{ $label ??', $toolPartial);
}
}

View File

@@ -0,0 +1,23 @@
<?php
namespace Tests\Feature;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Tests\TestCase;
class AdminPlatformOrderActionPartialsShouldSupportAriaLabelTest extends TestCase
{
use RefreshDatabase;
public function test_action_partials_should_support_aria_label_attributes(): void
{
$summaryPartial = file_get_contents(resource_path('views/admin/platform_orders/_summary_text_link.blade.php'));
$toolPartial = file_get_contents(resource_path('views/admin/platform_orders/_tool_anchor_button.blade.php'));
$this->assertStringContainsString('aria-label', $summaryPartial);
$this->assertStringContainsString('$ariaLabel', $summaryPartial);
$this->assertStringContainsString('aria-label', $toolPartial);
$this->assertStringContainsString('$ariaLabel', $toolPartial);
}
}

View File

@@ -63,7 +63,7 @@ class AdminPlatformOrderBatchActivateSubscriptionsReceiptStatusFilterFieldsTest
],
]);
// B可同步 + 无回执(用于验证 receipt_status=has 不会误命中)
// B可同步 + 无回执(广义)(用于验证 receipt_status=has 不会误命中)
$orderNoReceipt = PlatformOrder::query()->create([
'merchant_id' => $merchant->id,
'plan_id' => $plan->id,

View File

@@ -38,7 +38,7 @@ class AdminPlatformOrderBatchMarkActivatedFilterFieldsTest extends TestCase
'published_at' => now(),
]);
// A无回执 + 对账不一致(回执总额 0但 paid_amount=10=> reconcile_mismatch=1 命中
// A无回执(广义) + 对账不一致(回执总额 0但 paid_amount=10=> reconcile_mismatch=1 命中
$a = PlatformOrder::query()->create([
'merchant_id' => $merchant->id,
'plan_id' => $plan->id,
@@ -81,7 +81,7 @@ class AdminPlatformOrderBatchMarkActivatedFilterFieldsTest extends TestCase
],
]);
// C有退款refund_summary=1+ 无回执 + 对账不一致回执总额0但 paid_amount=10
// C有退款refund_summary=1+ 无回执(广义) + 对账不一致回执总额0但 paid_amount=10
// 如果不加 refund_status=none 的筛选,会被误推进;这里用测试保证“退款筛选口径”能影响批量动作
$c = PlatformOrder::query()->create([
'merchant_id' => $merchant->id,
@@ -106,7 +106,7 @@ class AdminPlatformOrderBatchMarkActivatedFilterFieldsTest extends TestCase
],
]);
// 治理口径升级:当筛选命中「无回执/对账不一致」等治理集合时,不允许直接批量仅标记为已生效。
// 治理口径升级:当筛选命中「无回执(广义)/对账不一致」等治理集合时,不允许直接批量仅标记为已生效。
// 这里用测试锁定:应直接被阻断,并给出 warning订单状态不应发生变化。
$this->post('/admin/platform-orders/batch-mark-activated', [
'scope' => 'filtered',
@@ -121,7 +121,7 @@ class AdminPlatformOrderBatchMarkActivatedFilterFieldsTest extends TestCase
->assertRedirect()
->assertSessionHas('warning', function ($msg) {
$msg = (string) $msg;
return str_contains($msg, '无回执') || str_contains($msg, '回执');
return str_contains($msg, '无回执(广义)') || str_contains($msg, '回执');
});
$a->refresh();

View File

@@ -38,7 +38,7 @@ class AdminPlatformOrderBatchMarkActivatedReceiptStatusFilterFieldsTest extends
'published_at' => now(),
]);
// 待处理订单:已支付 + pending并且无回执
// 待处理订单:已支付 + pending并且无回执(广义)
$order = PlatformOrder::query()->create([
'merchant_id' => $merchant->id,
'plan_id' => $plan->id,

View File

@@ -34,5 +34,6 @@ class AdminPlatformOrderBatchMarkActivatedShouldBlockWhenReceiptStatusNoneTest e
$res->assertRedirect();
$res->assertSessionHas('warning');
$this->assertStringContainsString('无回执(广义)', (string) session('warning'));
}
}

View File

@@ -0,0 +1,35 @@
<?php
namespace Tests\Feature;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Tests\TestCase;
class AdminPlatformOrderBatchSyncWarningsShouldClarifyBroadReceiptNoneScopeTest extends TestCase
{
use RefreshDatabase;
protected function loginAsPlatformAdmin(): void
{
$this->seed();
$this->post('/admin/login', [
'email' => 'platform.admin@demo.local',
'password' => 'Platform@123456',
])->assertRedirect('/admin');
}
public function test_batch_activate_subscriptions_warning_should_clarify_broad_receipt_none_scope(): void
{
$this->loginAsPlatformAdmin();
$syncRes = $this->post('/admin/platform-orders/batch-activate-subscriptions', [
'scope' => 'filtered',
'receipt_status' => 'none',
'syncable_only' => '1',
]);
$syncRes->assertRedirect();
$syncRes->assertSessionHas('warning');
$this->assertStringContainsString('无回执(广义)', (string) session('warning'));
}
}

View File

@@ -69,10 +69,13 @@ class AdminPlatformOrderBmpaFailedReasonStatsLinksTest extends TestCase
// 卡片标题
$res->assertSee('批量标记支付并生效失败原因 TOP5');
// 点击原因 => 自动带上 bmpa_error_keyword
$res->assertSee('/admin/platform-orders?bmpa_error_keyword=' . urlencode($reason), false);
// 点击原因:默认落到失败集合(语义收敛:原因 => 失败集合)
$res->assertSee('/admin/platform-orders?bmpa_failed_only=1&bmpa_error_keyword=' . urlencode($reason), false);
// 一键切到可处理集合重试 => 透传 pending+unpaid
$res->assertSee('bmpa_error_keyword=' . urlencode($reason) . '&status=pending&payment_status=unpaid', false);
// 一键切到可处理集合重试
// - 需携带 bmpa_processable_only=1
// - 且必须清掉 bmpa_failed_only避免 failed_only 与 processable_only 叠加冲突
$res->assertSee('bmpa_error_keyword=' . urlencode($reason) . '&bmpa_processable_only=1', false);
$res->assertDontSee('bmpa_failed_only=1&bmpa_error_keyword=' . urlencode($reason) . '&bmpa_processable_only=1', false);
}
}

View File

@@ -61,11 +61,16 @@ class AdminPlatformOrderBmpaFailedSummaryCardTest extends TestCase
],
]);
$res = $this->get('/admin/platform-orders');
// 模拟从同步失败上下文进入fail_only=1摘要卡链接不应残留 fail_only
$res = $this->get('/admin/platform-orders?fail_only=1');
$res->assertOk();
$res->assertSee('BMPA 成功 / 失败');
$res->assertSee('/admin/platform-orders?bmpa_failed_only=1', false);
$res->assertSee('/admin/platform-orders?bmpa_success_only=1', false);
// 不应出现携带 fail_only=1 的 BMPA 摘要卡链接(避免口径冲突/空结果)
$res->assertDontSee('/admin/platform-orders?fail_only=1&bmpa_failed_only=1', false);
$res->assertDontSee('/admin/platform-orders?fail_only=1&bmpa_success_only=1', false);
}
}

View File

@@ -87,7 +87,7 @@ class AdminPlatformOrderExportReceiptStatusFilterTest extends TestCase
],
]);
// 无回执
// 无回执(广义)
PlatformOrder::query()->create([
'merchant_id' => $merchant->id,
'plan_id' => $plan->id,

View File

@@ -0,0 +1,101 @@
<?php
namespace Tests\Feature;
use App\Models\Merchant;
use App\Models\Plan;
use App\Models\PlatformOrder;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Tests\TestCase;
class AdminPlatformOrderExportReconcileMismatchToleranceConfigTest extends TestCase
{
use RefreshDatabase;
protected function loginAsPlatformAdmin(): void
{
$this->seed();
$this->post('/admin/login', [
'email' => 'platform.admin@demo.local',
'password' => 'Platform@123456',
])->assertRedirect('/admin');
}
public function test_platform_orders_export_reconcile_mismatch_filter_respects_configured_tolerance(): void
{
$this->loginAsPlatformAdmin();
config(['saasshop.amounts.tolerance' => 0.05]);
$merchant = Merchant::query()->firstOrFail();
$plan = Plan::query()->create([
'code' => 'export_reconcile_mismatch_tol_cfg_test',
'name' => '导出对账不一致容差测试套餐',
'billing_cycle' => 'monthly',
'price' => 10,
'list_price' => 10,
'status' => 'active',
'sort' => 10,
'published_at' => now(),
]);
// A差额 0.04(在 tol=0.05 内)=> 不应命中
PlatformOrder::query()->create([
'merchant_id' => $merchant->id,
'plan_id' => $plan->id,
'order_no' => 'PO_EXP_RECON_TOL_A',
'order_type' => 'new_purchase',
'status' => 'activated',
'payment_status' => 'paid',
'plan_name' => $plan->name,
'billing_cycle' => $plan->billing_cycle,
'period_months' => 1,
'quantity' => 1,
'payable_amount' => 10,
'paid_amount' => 10,
'placed_at' => now(),
'paid_at' => now(),
'activated_at' => now(),
'meta' => [
'payment_summary' => [
'count' => 1,
'total_amount' => 9.96,
],
],
]);
// B差额 0.06(超出 tol=0.05=> 应命中
PlatformOrder::query()->create([
'merchant_id' => $merchant->id,
'plan_id' => $plan->id,
'order_no' => 'PO_EXP_RECON_TOL_B',
'order_type' => 'new_purchase',
'status' => 'activated',
'payment_status' => 'paid',
'plan_name' => $plan->name,
'billing_cycle' => $plan->billing_cycle,
'period_months' => 1,
'quantity' => 1,
'payable_amount' => 10,
'paid_amount' => 10,
'placed_at' => now(),
'paid_at' => now(),
'activated_at' => now(),
'meta' => [
'payment_summary' => [
'count' => 1,
'total_amount' => 9.94,
],
],
]);
$res = $this->get('/admin/platform-orders/export?download=1&reconcile_mismatch=1');
$res->assertOk();
$content = $res->streamedContent();
$this->assertStringNotContainsString('PO_EXP_RECON_TOL_A', $content);
$this->assertStringContainsString('PO_EXP_RECON_TOL_B', $content);
}
}

View File

@@ -0,0 +1,46 @@
<?php
namespace Tests\Feature;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Support\Arr;
use Tests\TestCase;
class AdminPlatformOrderFiltersReceiptStatusHintLinkShouldDropPageTest extends TestCase
{
use RefreshDatabase;
protected function loginAsPlatformAdmin(): void
{
$this->seed();
$this->post('/admin/login', [
'email' => 'platform.admin@demo.local',
'password' => 'Platform@123456',
])->assertRedirect('/admin');
}
public function test_receipt_status_hint_link_should_drop_page_query(): void
{
$this->loginAsPlatformAdmin();
$res = $this->get('/admin/platform-orders?' . Arr::query([
'merchant_id' => 2,
'plan_id' => 3,
'keyword' => 'alpha',
'back' => '/admin/plans',
'page' => 5,
]));
$res->assertOk();
$html = (string) $res->getContent();
preg_match('/data-role="po-receipt-status-broad-none-go-paid-no-receipt"[^>]*href="([^"]+)"/u', $html, $m);
$href = html_entity_decode($m[1] ?? '');
$query = parse_url($href, PHP_URL_QUERY) ?: '';
parse_str($query, $q);
$this->assertArrayNotHasKey('page', $q);
$this->assertSame('paid', (string) ($q['payment_status'] ?? ''));
$this->assertSame('none', (string) ($q['receipt_status'] ?? ''));
}
}

View File

@@ -0,0 +1,33 @@
<?php
namespace Tests\Feature;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Tests\TestCase;
class AdminPlatformOrderFiltersReceiptStatusHintLinkShouldHaveAriaLabelTest extends TestCase
{
use RefreshDatabase;
protected function loginAsPlatformAdmin(): void
{
$this->seed();
$this->post('/admin/login', [
'email' => 'platform.admin@demo.local',
'password' => 'Platform@123456',
])->assertRedirect('/admin');
}
public function test_platform_orders_receipt_status_hint_link_should_have_aria_label(): void
{
$this->loginAsPlatformAdmin();
$res = $this->get('/admin/platform-orders');
$res->assertOk();
$html = (string) $res->getContent();
$this->assertStringContainsString('data-role="po-receipt-status-broad-none-go-paid-no-receipt"', $html);
$this->assertStringContainsString('aria-label="切换到已付无回执快捷筛选"', $html);
}
}

View File

@@ -0,0 +1,43 @@
<?php
namespace Tests\Feature;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Support\Arr;
use Tests\TestCase;
class AdminPlatformOrderFiltersReceiptStatusHintLinkShouldKeepBackTest extends TestCase
{
use RefreshDatabase;
protected function loginAsPlatformAdmin(): void
{
$this->seed();
$this->post('/admin/login', [
'email' => 'platform.admin@demo.local',
'password' => 'Platform@123456',
])->assertRedirect('/admin');
}
public function test_receipt_status_hint_link_should_keep_back_query(): void
{
$this->loginAsPlatformAdmin();
$res = $this->get('/admin/platform-orders?' . Arr::query([
'merchant_id' => 2,
'plan_id' => 3,
'keyword' => 'alpha',
'back' => '/admin/plans',
]));
$res->assertOk();
$html = (string) $res->getContent();
preg_match('/data-role="po-receipt-status-broad-none-go-paid-no-receipt"[^>]*href="([^"]+)"/u', $html, $m);
$href = html_entity_decode($m[1] ?? '');
$query = parse_url($href, PHP_URL_QUERY) ?: '';
parse_str($query, $q);
$this->assertSame('/admin/plans', (string) ($q['back'] ?? ''));
}
}

View File

@@ -0,0 +1,47 @@
<?php
namespace Tests\Feature;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Support\Arr;
use Tests\TestCase;
class AdminPlatformOrderFiltersReceiptStatusHintLinkShouldKeepBusinessContextTest extends TestCase
{
use RefreshDatabase;
protected function loginAsPlatformAdmin(): void
{
$this->seed();
$this->post('/admin/login', [
'email' => 'platform.admin@demo.local',
'password' => 'Platform@123456',
])->assertRedirect('/admin');
}
public function test_receipt_status_hint_link_should_keep_business_context(): void
{
$this->loginAsPlatformAdmin();
$res = $this->get('/admin/platform-orders?' . Arr::query([
'merchant_id' => 2,
'plan_id' => 3,
'keyword' => 'alpha',
'receipt_status' => 'none',
'back' => '/admin/plans',
]));
$res->assertOk();
$html = (string) $res->getContent();
preg_match('/data-role="po-receipt-status-broad-none-go-paid-no-receipt"[^>]*href="([^"]+)"/u', $html, $m);
$href = html_entity_decode($m[1] ?? '');
$query = parse_url($href, PHP_URL_QUERY) ?: '';
parse_str($query, $q);
$this->assertSame('2', (string) ($q['merchant_id'] ?? ''));
$this->assertSame('3', (string) ($q['plan_id'] ?? ''));
$this->assertSame('alpha', (string) ($q['keyword'] ?? ''));
$this->assertSame('/admin/plans', (string) ($q['back'] ?? ''));
}
}

View File

@@ -0,0 +1,36 @@
<?php
namespace Tests\Feature;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Tests\TestCase;
class AdminPlatformOrderFiltersReceiptStatusHintLinkShouldKeepLinkClassTest extends TestCase
{
use RefreshDatabase;
protected function loginAsPlatformAdmin(): void
{
$this->seed();
$this->post('/admin/login', [
'email' => 'platform.admin@demo.local',
'password' => 'Platform@123456',
])->assertRedirect('/admin');
}
public function test_platform_orders_receipt_status_hint_link_should_keep_link_class(): void
{
$this->loginAsPlatformAdmin();
$res = $this->get('/admin/platform-orders');
$res->assertOk();
$html = (string) $res->getContent();
preg_match('/<a[^>]*data-role="po-receipt-status-broad-none-go-paid-no-receipt"[^>]*>/u', $html, $m);
$anchor = $m[0] ?? '';
$this->assertNotSame('', $anchor, '未找到回执筛选说明中的已付无回执直达入口');
$this->assertStringContainsString('class="link"', $anchor);
}
}

View File

@@ -0,0 +1,54 @@
<?php
namespace Tests\Feature;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Support\Arr;
use Tests\TestCase;
class AdminPlatformOrderFiltersReceiptStatusHintLinkShouldMatchPaidNoReceiptQuickFilterTest extends TestCase
{
use RefreshDatabase;
protected function loginAsPlatformAdmin(): void
{
$this->seed();
$this->post('/admin/login', [
'email' => 'platform.admin@demo.local',
'password' => 'Platform@123456',
])->assertRedirect('/admin');
}
public function test_receipt_status_hint_link_should_match_paid_no_receipt_quick_filter_scope(): void
{
$this->loginAsPlatformAdmin();
$res = $this->get('/admin/platform-orders?' . Arr::query([
'merchant_id' => 2,
'plan_id' => 3,
'keyword' => 'alpha',
'back' => '/admin/plans',
]));
$res->assertOk();
$html = (string) $res->getContent();
preg_match('/data-role="po-receipt-status-broad-none-go-paid-no-receipt"[^>]*href="([^"]+)"/u', $html, $m1);
preg_match('/data-role="po-quickfilter-paid-no-receipt"[^>]*href="([^"]+)"/u', $html, $m2);
$this->assertNotEmpty($m1[1] ?? '');
$this->assertNotEmpty($m2[1] ?? '');
$hintHref = html_entity_decode($m1[1]);
$quickHref = html_entity_decode($m2[1]);
$hintQuery = parse_url($hintHref, PHP_URL_QUERY) ?: '';
$quickQuery = parse_url($quickHref, PHP_URL_QUERY) ?: '';
parse_str($hintQuery, $hint);
parse_str($quickQuery, $quick);
$this->assertSame($quick, $hint);
}
}

View File

@@ -0,0 +1,32 @@
<?php
namespace Tests\Feature;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Tests\TestCase;
class AdminPlatformOrderFiltersReceiptStatusHintLinkShouldStayInsideHintBlockTest extends TestCase
{
use RefreshDatabase;
protected function loginAsPlatformAdmin(): void
{
$this->seed();
$this->post('/admin/login', [
'email' => 'platform.admin@demo.local',
'password' => 'Platform@123456',
])->assertRedirect('/admin');
}
public function test_receipt_status_hint_link_should_stay_inside_hint_block(): void
{
$this->loginAsPlatformAdmin();
$res = $this->get('/admin/platform-orders');
$res->assertOk();
$html = (string) $res->getContent();
$this->assertMatchesRegularExpression('/data-role="po-receipt-status-broad-none-hint"[\s\S]*?data-role="po-receipt-status-broad-none-go-paid-no-receipt"/u', $html);
}
}

View File

@@ -0,0 +1,48 @@
<?php
namespace Tests\Feature;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Support\Arr;
use Tests\TestCase;
class AdminPlatformOrderFiltersReceiptStatusHintLinkShouldStayOnPaidNoReceiptScopeTest extends TestCase
{
use RefreshDatabase;
protected function loginAsPlatformAdmin(): void
{
$this->seed();
$this->post('/admin/login', [
'email' => 'platform.admin@demo.local',
'password' => 'Platform@123456',
])->assertRedirect('/admin');
}
public function test_receipt_status_hint_link_should_stay_on_paid_no_receipt_scope_when_already_there(): void
{
$this->loginAsPlatformAdmin();
$res = $this->get('/admin/platform-orders?' . Arr::query([
'payment_status' => 'paid',
'receipt_status' => 'none',
'merchant_id' => 2,
'plan_id' => 3,
'back' => '/admin/plans',
]));
$res->assertOk();
$html = (string) $res->getContent();
preg_match('/data-role="po-receipt-status-broad-none-go-paid-no-receipt"[^>]*href="([^"]+)"/u', $html, $m);
$href = html_entity_decode($m[1] ?? '');
$query = parse_url($href, PHP_URL_QUERY) ?: '';
parse_str($query, $q);
$this->assertSame('paid', (string) ($q['payment_status'] ?? ''));
$this->assertSame('none', (string) ($q['receipt_status'] ?? ''));
$this->assertSame('2', (string) ($q['merchant_id'] ?? ''));
$this->assertSame('3', (string) ($q['plan_id'] ?? ''));
$this->assertSame('/admin/plans', (string) ($q['back'] ?? ''));
}
}

View File

@@ -0,0 +1,47 @@
<?php
namespace Tests\Feature;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Support\Arr;
use Tests\TestCase;
class AdminPlatformOrderFiltersReceiptStatusHintLinkShouldTightenBroadNoneToPaidNoReceiptTest extends TestCase
{
use RefreshDatabase;
protected function loginAsPlatformAdmin(): void
{
$this->seed();
$this->post('/admin/login', [
'email' => 'platform.admin@demo.local',
'password' => 'Platform@123456',
])->assertRedirect('/admin');
}
public function test_receipt_status_hint_link_should_tighten_broad_none_to_paid_no_receipt(): void
{
$this->loginAsPlatformAdmin();
$res = $this->get('/admin/platform-orders?' . Arr::query([
'receipt_status' => 'none',
'merchant_id' => 2,
'plan_id' => 3,
'back' => '/admin/plans',
]));
$res->assertOk();
$html = (string) $res->getContent();
preg_match('/data-role="po-receipt-status-broad-none-go-paid-no-receipt"[^>]*href="([^"]+)"/u', $html, $m);
$href = html_entity_decode($m[1] ?? '');
$query = parse_url($href, PHP_URL_QUERY) ?: '';
parse_str($query, $q);
$this->assertSame('paid', (string) ($q['payment_status'] ?? ''));
$this->assertSame('none', (string) ($q['receipt_status'] ?? ''));
$this->assertSame('2', (string) ($q['merchant_id'] ?? ''));
$this->assertSame('3', (string) ($q['plan_id'] ?? ''));
$this->assertSame('/admin/plans', (string) ($q['back'] ?? ''));
}
}

View File

@@ -0,0 +1,32 @@
<?php
namespace Tests\Feature;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Tests\TestCase;
class AdminPlatformOrderFiltersReceiptStatusHintShouldAppearNearReceiptSelectTest extends TestCase
{
use RefreshDatabase;
protected function loginAsPlatformAdmin(): void
{
$this->seed();
$this->post('/admin/login', [
'email' => 'platform.admin@demo.local',
'password' => 'Platform@123456',
])->assertRedirect('/admin');
}
public function test_platform_orders_receipt_status_hint_should_appear_near_receipt_select(): void
{
$this->loginAsPlatformAdmin();
$res = $this->get('/admin/platform-orders');
$res->assertOk();
$html = (string) $res->getContent();
$this->assertMatchesRegularExpression('/<select name="receipt_status">[\s\S]*?<\/select>[\s\S]*?无回执(广义)会包含未支付订单;收费闭环治理请优先使用“已付无回执”快捷筛选。/u', $html);
}
}

View File

@@ -0,0 +1,32 @@
<?php
namespace Tests\Feature;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Tests\TestCase;
class AdminPlatformOrderFiltersReceiptStatusHintShouldCoexistWithReceiptOptionsTest extends TestCase
{
use RefreshDatabase;
protected function loginAsPlatformAdmin(): void
{
$this->seed();
$this->post('/admin/login', [
'email' => 'platform.admin@demo.local',
'password' => 'Platform@123456',
])->assertRedirect('/admin');
}
public function test_platform_orders_receipt_status_hint_should_coexist_with_receipt_options(): void
{
$this->loginAsPlatformAdmin();
$res = $this->get('/admin/platform-orders');
$res->assertOk();
$html = (string) $res->getContent();
$this->assertMatchesRegularExpression('/全部回执状态[\s\S]*?有回执[\s\S]*?无回执(广义)[\s\S]*?已付无回执/u', $html);
}
}

View File

@@ -0,0 +1,33 @@
<?php
namespace Tests\Feature;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Tests\TestCase;
class AdminPlatformOrderFiltersReceiptStatusHintShouldHaveDataRoleTest extends TestCase
{
use RefreshDatabase;
protected function loginAsPlatformAdmin(): void
{
$this->seed();
$this->post('/admin/login', [
'email' => 'platform.admin@demo.local',
'password' => 'Platform@123456',
])->assertRedirect('/admin');
}
public function test_platform_orders_receipt_status_hint_should_have_data_role(): void
{
$this->loginAsPlatformAdmin();
$res = $this->get('/admin/platform-orders');
$res->assertOk();
$html = (string) $res->getContent();
$this->assertStringContainsString('data-role="po-receipt-status-broad-none-hint"', $html);
$this->assertStringContainsString('无回执(广义)会包含未支付订单;收费闭环治理请优先使用“已付无回执”快捷筛选。', $html);
}
}

View File

@@ -0,0 +1,51 @@
<?php
namespace Tests\Feature;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Support\Arr;
use Tests\TestCase;
class AdminPlatformOrderFiltersReceiptStatusHintShouldLinkToPaidNoReceiptQuickFilterTest extends TestCase
{
use RefreshDatabase;
protected function loginAsPlatformAdmin(): void
{
$this->seed();
$this->post('/admin/login', [
'email' => 'platform.admin@demo.local',
'password' => 'Platform@123456',
])->assertRedirect('/admin');
}
public function test_platform_orders_receipt_status_hint_should_link_to_paid_no_receipt_quick_filter(): void
{
$this->loginAsPlatformAdmin();
$res = $this->get('/admin/platform-orders?' . Arr::query([
'merchant_id' => 2,
'plan_id' => 3,
'keyword' => 'alpha',
'back' => '/admin/plans',
]));
$res->assertOk();
$html = (string) $res->getContent();
$this->assertStringContainsString('data-role="po-receipt-status-broad-none-go-paid-no-receipt"', $html);
$this->assertStringContainsString('已付无回执</a>快捷筛选', $html);
preg_match('/data-role="po-receipt-status-broad-none-go-paid-no-receipt"[^>]*href="([^"]+)"/u', $html, $m);
$href = html_entity_decode($m[1] ?? '');
$parts = parse_url($href);
parse_str($parts['query'] ?? '', $q);
$this->assertSame('2', (string) ($q['merchant_id'] ?? ''));
$this->assertSame('3', (string) ($q['plan_id'] ?? ''));
$this->assertSame('alpha', (string) ($q['keyword'] ?? ''));
$this->assertSame('paid', (string) ($q['payment_status'] ?? ''));
$this->assertSame('none', (string) ($q['receipt_status'] ?? ''));
$this->assertSame('/admin/plans', (string) ($q['back'] ?? ''));
}
}

View File

@@ -0,0 +1,32 @@
<?php
namespace Tests\Feature;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Tests\TestCase;
class AdminPlatformOrderFiltersReceiptStatusOptionShouldClarifyBroadNoneTest extends TestCase
{
use RefreshDatabase;
protected function loginAsPlatformAdmin(): void
{
$this->seed();
$this->post('/admin/login', [
'email' => 'platform.admin@demo.local',
'password' => 'Platform@123456',
])->assertRedirect('/admin');
}
public function test_platform_orders_filters_receipt_status_option_should_clarify_broad_none_scope(): void
{
$this->loginAsPlatformAdmin();
$res = $this->get('/admin/platform-orders');
$res->assertOk();
$html = (string) $res->getContent();
$this->assertStringContainsString('无回执(广义)', $html);
}
}

View File

@@ -0,0 +1,32 @@
<?php
namespace Tests\Feature;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Tests\TestCase;
class AdminPlatformOrderFiltersReceiptStatusShouldExplainBroadNoneAndPaidNoReceiptPriorityTest extends TestCase
{
use RefreshDatabase;
protected function loginAsPlatformAdmin(): void
{
$this->seed();
$this->post('/admin/login', [
'email' => 'platform.admin@demo.local',
'password' => 'Platform@123456',
])->assertRedirect('/admin');
}
public function test_platform_orders_filters_should_explain_broad_none_and_paid_no_receipt_priority(): void
{
$this->loginAsPlatformAdmin();
$res = $this->get('/admin/platform-orders');
$res->assertOk();
$html = (string) $res->getContent();
$this->assertStringContainsString('无回执(广义)会包含未支付订单;收费闭环治理请优先使用“已付无回执”快捷筛选。', $html);
}
}

View File

@@ -23,15 +23,27 @@ class AdminPlatformOrderIndexBatchActivateBlockedHintShouldIncludeGoSyncableLink
{
$this->loginAsPlatformAdmin();
// 构造一个会触发 batch_activate_subscriptions 被阻断的筛选:未勾选 syncable_only。
$res = $this->get('/admin/platform-orders');
// 构造一个会触发 batch_activate_subscriptions 被阻断的复杂筛选;
// blocked hint 应给出“切到可同步订阅集合”入口,并清理冲突开关。
$res = $this->get('/admin/platform-orders?sync_status=failed&bmpa_failed_only=1&synced_only=1&page=2&back=/admin');
$res->assertOk();
$html = (string) $res->getContent();
$this->assertStringContainsString('data-role="batch-activate-subscriptions-blocked-hint"', $html);
$this->assertStringContainsString('切到可同步订阅集合', $html);
$this->assertStringContainsString('syncable_only=1', $html);
$this->assertStringContainsString('sync_status=unsynced', $html);
$this->assertMatchesRegularExpression('/href="([^"]+)"[^>]*>切到可同步订阅集合<\/a>/', $html);
preg_match('/href="([^"]+)"[^>]*>切到可同步订阅集合<\/a>/', $html, $m);
$href = html_entity_decode((string) ($m[1] ?? ''));
$query = parse_url($href, PHP_URL_QUERY) ?: '';
parse_str($query, $q);
$this->assertSame('1', (string) ($q['syncable_only'] ?? ''));
$this->assertSame('unsynced', (string) ($q['sync_status'] ?? ''));
$this->assertSame('/admin', (string) ($q['back'] ?? ''));
$this->assertArrayNotHasKey('page', $q);
$this->assertArrayNotHasKey('bmpa_failed_only', $q);
$this->assertArrayNotHasKey('synced_only', $q);
}
}

View File

@@ -81,7 +81,7 @@ class AdminPlatformOrderIndexBatchActivateButtonShouldDisableWhenNotSyncableOnly
$merchant = Merchant::query()->firstOrFail();
$plan = Plan::query()->create([
'code' => 'po_index_batch_sync_btn_disable_receipt_none_plan',
'name' => '批量同步按钮禁用无回执测试套餐',
'name' => '批量同步按钮禁用无回执(广义)测试套餐',
'billing_cycle' => 'monthly',
'price' => 10,
'list_price' => 10,
@@ -106,7 +106,7 @@ class AdminPlatformOrderIndexBatchActivateButtonShouldDisableWhenNotSyncableOnly
'placed_at' => now(),
'paid_at' => now(),
'activated_at' => now(),
// meta 为空 => 无回执
// meta 为空 => 无回执(广义)
'meta' => [],
]);
@@ -118,7 +118,7 @@ class AdminPlatformOrderIndexBatchActivateButtonShouldDisableWhenNotSyncableOnly
$this->assertStringContainsString('批量同步订阅(当前筛选范围)', $html);
$this->assertStringContainsString('disabled', $html);
$this->assertStringContainsString('无回执', $html);
$this->assertStringContainsString('无回执(广义)', $html);
$this->assertStringContainsString('data-role="batch-activate-subscriptions-blocked-hint"', $html);
}
}

View File

@@ -19,19 +19,31 @@ class AdminPlatformOrderIndexBatchBmpaBlockedHintShouldIncludeGoProcessableLinkT
])->assertRedirect('/admin');
}
public function test_blocked_hint_should_include_link_to_pending_unpaid_set(): void
public function test_blocked_hint_should_include_link_to_bmpa_processable_set(): void
{
$this->loginAsPlatformAdmin();
// 构造一个会触发 batch_bmpa 被阻断的筛选:不给 pending+unpaid。
$res = $this->get('/admin/platform-orders');
// 构造一个会触发 batch_bmpa 被阻断的复杂筛选;
// blocked hint 应给出“切到可BMPA处理集合”入口并清理冲突开关。
$res = $this->get('/admin/platform-orders?sync_status=failed&syncable_only=1&bmpa_failed_only=1&page=2&back=/admin');
$res->assertOk();
$html = (string) $res->getContent();
$this->assertStringContainsString('data-role="batch-bmpa-blocked-hint"', $html);
$this->assertStringContainsString('切到可BMPA处理集合', $html);
$this->assertStringContainsString('status=pending', $html);
$this->assertStringContainsString('payment_status=unpaid', $html);
$this->assertMatchesRegularExpression('/href="([^"]+)"[^>]*>切到可BMPA处理集合<\/a>/', $html);
preg_match('/href="([^"]+)"[^>]*>切到可BMPA处理集合<\/a>/', $html, $m);
$href = html_entity_decode((string) ($m[1] ?? ''));
$query = parse_url($href, PHP_URL_QUERY) ?: '';
parse_str($query, $q);
$this->assertSame('1', (string) ($q['bmpa_processable_only'] ?? ''));
$this->assertSame('/admin', (string) ($q['back'] ?? ''));
$this->assertArrayNotHasKey('page', $q);
$this->assertArrayNotHasKey('bmpa_failed_only', $q);
$this->assertArrayNotHasKey('sync_status', $q);
$this->assertArrayNotHasKey('syncable_only', $q);
}
}

Some files were not shown because too many files have changed in this diff Show More