Compare commits
414 Commits
0cc71b7e66
...
feat/audit
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
69e649db87 | ||
|
|
aaa4ba7d44 | ||
|
|
a92de6402d | ||
|
|
6f94d23f35 | ||
|
|
21c86f4b14 | ||
|
|
81a9488201 | ||
|
|
f9c62afaba | ||
|
|
8e21ab6e46 | ||
|
|
7ce509d7f6 | ||
|
|
a0ab815f2e | ||
|
|
959b1d8463 | ||
|
|
e0113b2eb6 | ||
|
|
95ef295396 | ||
|
|
1ecd99f5b4 | ||
|
|
7c0e3011ed | ||
|
|
347603c259 | ||
|
|
fafe12a8fa | ||
|
|
9d68f22214 | ||
|
|
c5da67ae10 | ||
|
|
cb88f8fc7d | ||
|
|
15b0ecf094 | ||
|
|
9cf2e7d8e4 | ||
|
|
9b46699ed7 | ||
|
|
294c5f681b | ||
|
|
24a31e8b96 | ||
|
|
13d0cc9ada | ||
|
|
e0931ff55c | ||
|
|
db4d74be7a | ||
|
|
d4653e9527 | ||
|
|
aab3871814 | ||
|
|
f7250c485e | ||
|
|
26d284d3e4 | ||
|
|
d85a3d383a | ||
|
|
766c9e53bb | ||
|
|
bd8f62736c | ||
|
|
7bd40a5527 | ||
|
|
50f73d2222 | ||
|
|
0a232b96a7 | ||
|
|
dd92ab0e6e | ||
|
|
21ba7f575a | ||
|
|
b80c0224cf | ||
|
|
c66d299f68 | ||
|
|
82b17d913d | ||
|
|
0f53c6851d | ||
|
|
d4665138fb | ||
|
|
41458ebd65 | ||
|
|
8b33d5d3bb | ||
|
|
cc2b853e5e | ||
|
|
7a5be08786 | ||
|
|
016080d662 | ||
|
|
4d456eff7e | ||
|
|
ba0ac89132 | ||
|
|
5198b7d6f5 | ||
|
|
e14fcad1ce | ||
|
|
246b9303c0 | ||
|
|
1b4d0addf0 | ||
|
|
462ef5c950 | ||
|
|
278f3bfe70 | ||
|
|
34d3d53198 | ||
|
|
ce07034886 | ||
|
|
3c2b2c80f9 | ||
|
|
fb7012a1e5 | ||
|
|
de312919a6 | ||
|
|
0ddef1f6b6 | ||
|
|
ced346c4a1 | ||
|
|
d7804dd755 | ||
|
|
8de2a97ee5 | ||
|
|
d8816869b3 | ||
|
|
b3b9d3dc3c | ||
|
|
0783548ff4 | ||
|
|
fb8ce54915 | ||
|
|
4ccf9c28af | ||
|
|
85bba5596c | ||
|
|
52a9cd130c | ||
|
|
3e58e3e584 | ||
|
|
a264ec5fc9 | ||
|
|
58c2965c7d | ||
|
|
3b67065d76 | ||
|
|
4c2f837ddd | ||
|
|
224682b144 | ||
|
|
1a5c3f0fe9 | ||
|
|
d8b7887b0f | ||
|
|
35e33b8c0e | ||
|
|
e6308d2b8d | ||
|
|
f4f4be3268 | ||
|
|
57526577b3 | ||
|
|
73f35d7e31 | ||
|
|
fc2c39c5f8 | ||
|
|
a2b7e0fc01 | ||
|
|
6a7a6dac33 | ||
|
|
2ffdc47d08 | ||
|
|
4a54a3e623 | ||
|
|
bea9e30d2b | ||
|
|
9761aabefa | ||
|
|
fd56ef9123 | ||
|
|
fe300864dc | ||
|
|
0dace50ed3 | ||
|
|
6c52a2c3c4 | ||
|
|
e2c99fb7f5 | ||
|
|
24100f1ddd | ||
|
|
98a951a41a | ||
|
|
de23a245d6 | ||
|
|
155e71404e | ||
|
|
23832fe47b | ||
|
|
0bc82dcf41 | ||
|
|
e044d2b25d | ||
|
|
e8744adbdb | ||
|
|
1be189f2a9 | ||
|
|
54293147d7 | ||
|
|
13d836fe57 | ||
|
|
0bd676992f | ||
|
|
3d57b402f7 | ||
|
|
24fb62d544 | ||
|
|
3ffd7855fd | ||
|
|
a5537a17ce | ||
|
|
595237a9ac | ||
|
|
dbc639defe | ||
|
|
f53128c4f5 | ||
|
|
566970347f | ||
|
|
633e50e303 | ||
|
|
0da7be44b7 | ||
|
|
6cb00c5e47 | ||
|
|
8c6342e5d3 | ||
|
|
68a3acee7e | ||
|
|
5234f5d7e4 | ||
|
|
7bd33d0c85 | ||
|
|
4f046a1820 | ||
|
|
f2b74e55e6 | ||
|
|
7f953b4d44 | ||
|
|
e5241f97ba | ||
|
|
b7cb4aa910 | ||
|
|
fc98efa65b | ||
|
|
33cfd7f9c5 | ||
|
|
0888be7672 | ||
|
|
abebf57104 | ||
|
|
e037795602 | ||
|
|
65cf173f3d | ||
|
|
be40c7540a | ||
|
|
8bdb70ed07 | ||
|
|
1946620025 | ||
|
|
ee2164f0ef | ||
|
|
c8fff97f23 | ||
|
|
8e5cbdab03 | ||
|
|
0bab0a329c | ||
|
|
9f0ee9fd51 | ||
|
|
0f84eea12f | ||
|
|
49a56bb6cc | ||
|
|
cd8f3c5350 | ||
|
|
caf0cf1420 | ||
|
|
44e8bfb178 | ||
|
|
fc68964221 | ||
|
|
ef237ed2af | ||
|
|
d0d6fa0f34 | ||
|
|
b93efc9b44 | ||
|
|
655e7be0ac | ||
|
|
dd259da74a | ||
|
|
7af59e4d17 | ||
|
|
3b1ad55ec1 | ||
|
|
7afaff0641 | ||
|
|
bde243e03e | ||
|
|
39337edb1c | ||
|
|
6c74d37323 | ||
|
|
26a3786e82 | ||
|
|
0ee0379c74 | ||
|
|
49cbaf7532 | ||
|
|
6fbb123b56 | ||
|
|
06e38e79d5 | ||
|
|
22dc9ea3f0 | ||
|
|
e78a68c38e | ||
|
|
6a827412d1 | ||
|
|
4681556db6 | ||
|
|
09ef2f9754 | ||
|
|
c181dbf878 | ||
|
|
a0f7b4eede | ||
|
|
916107e68f | ||
|
|
90c4d2517a | ||
|
|
61cb61da6a | ||
|
|
164b6e8fee | ||
|
|
919f3e298f | ||
|
|
1377edb2c0 | ||
|
|
a8b8883f90 | ||
|
|
59954d31c1 | ||
|
|
ec7795325b | ||
|
|
21824a0207 | ||
|
|
527c59215f | ||
|
|
6720a8a467 | ||
|
|
c02c641b3e | ||
|
|
464f25ad6e | ||
|
|
4d0a1d0a02 | ||
|
|
e6d4adfe47 | ||
|
|
fa937fc2d3 | ||
|
|
90aec15333 | ||
|
|
ea7c16fe9f | ||
|
|
cb1f3309cc | ||
|
|
3ca5a66945 | ||
|
|
59488d8232 | ||
|
|
aeb0e26976 | ||
|
|
5bcb13b41e | ||
|
|
bebd91db68 | ||
|
|
0a94f00064 | ||
|
|
a0eb9bdfb5 | ||
|
|
ed69d91b53 | ||
|
|
f16e3bb5de | ||
|
|
7d1ebf69e8 | ||
|
|
2f6630c318 | ||
|
|
857d2be3a6 | ||
|
|
c80c743013 | ||
|
|
2365afd741 | ||
|
|
85540ea4a0 | ||
|
|
47f1ee9301 | ||
|
|
3f59a36eb9 | ||
|
|
ab77ed274f | ||
|
|
a8663c48ab | ||
|
|
d7bb750eef | ||
|
|
25908799d2 | ||
|
|
861e94db4a | ||
|
|
6822e1cf0a | ||
|
|
cd3c54c62d | ||
|
|
a310289872 | ||
|
|
9a9735fcf1 | ||
|
|
f837acb630 | ||
|
|
37a85c7890 | ||
|
|
337a9ffd27 | ||
|
|
f040c0d4c3 | ||
|
|
cc03b77f65 | ||
|
|
5431503917 | ||
|
|
7d847801bb | ||
|
|
87de9e80ad | ||
|
|
ed1a74c39b | ||
|
|
d12b3d986b | ||
|
|
084bbbfd8f | ||
|
|
901de1de2e | ||
|
|
2449140118 | ||
|
|
1fccdfe41c | ||
|
|
3c7d3fd66b | ||
|
|
f3925f287b | ||
|
|
892c089103 | ||
|
|
04b878ed03 | ||
|
|
c0fd0cdc0b | ||
|
|
50765f7a67 | ||
|
|
7a1ac998f1 | ||
|
|
b18bc016f6 | ||
|
|
95daa179fa | ||
|
|
3e210c6140 | ||
|
|
0612224c86 | ||
|
|
ef5f2fe8eb | ||
|
|
69fe88eef5 | ||
|
|
17a39664af | ||
|
|
67e17ee7ae | ||
|
|
1db9c55eca | ||
|
|
c52b213c8f | ||
|
|
6db52ca1fb | ||
|
|
fecfb60c50 | ||
|
|
d906224eab | ||
|
|
649c3f61ed | ||
|
|
1e9a193a32 | ||
|
|
04f3fc2ef4 | ||
|
|
5b26c27eb8 | ||
|
|
f537080852 | ||
|
|
2b454a0624 | ||
|
|
a3e79a8685 | ||
|
|
2af97c6e90 | ||
|
|
dd03499b8f | ||
|
|
39208840d7 | ||
|
|
b78b4f0ef2 | ||
|
|
bab1da0339 | ||
|
|
dd27d9de26 | ||
|
|
085d3324fd | ||
|
|
d5eab40a4b | ||
|
|
6fe8cc916e | ||
|
|
1297619895 | ||
|
|
b875c1ee57 | ||
|
|
9fb74bb453 | ||
|
|
75b14447cd | ||
|
|
2637d6cef9 | ||
|
|
de1be8a01c | ||
|
|
b68dfd8fdf | ||
|
|
051a9348ae | ||
|
|
480cff6e74 | ||
|
|
423f6ba1a5 | ||
|
|
4226bf641e | ||
|
|
b7109b80bd | ||
|
|
d00609e4e4 | ||
|
|
47cae2f3a4 | ||
|
|
0462e0d041 | ||
|
|
2ebee7f1ad | ||
|
|
6cb503e09a | ||
|
|
23e802ea96 | ||
|
|
7f8fd95ba2 | ||
|
|
cc85bf2912 | ||
|
|
619f14c252 | ||
|
|
8b4cac8795 | ||
|
|
2d8e89deaf | ||
|
|
24f3bd7e29 | ||
|
|
b87a95c29f | ||
|
|
463d02cf51 | ||
|
|
10c2d29f7a | ||
|
|
7324e3de33 | ||
|
|
9a3ab559fd | ||
|
|
aa583e2b72 | ||
|
|
807b3ae37a | ||
|
|
5429bc1f10 | ||
|
|
11ee5323ef | ||
|
|
b7d34da73a | ||
|
|
ce2d37428c | ||
|
|
1750dd17eb | ||
|
|
d953094ecf | ||
|
|
0c3c5abc14 | ||
|
|
4d1b19b295 | ||
|
|
7ba1e2efa8 | ||
|
|
c472e57a57 | ||
|
|
ba731ac21b | ||
|
|
bc84f72300 | ||
|
|
79d14c0f41 | ||
|
|
370a9a58e5 | ||
|
|
5d377d16c3 | ||
|
|
f19f9a38eb | ||
|
|
b8cd602225 | ||
|
|
5d7dadcb0a | ||
|
|
524a6c4ddb | ||
|
|
3417843885 | ||
|
|
fc696828d7 | ||
|
|
49476695f1 | ||
|
|
d5807d3623 | ||
|
|
842d124c3a | ||
|
|
201635229d | ||
|
|
7b0a63c468 | ||
|
|
7614ada1b6 | ||
|
|
eeecfb763e | ||
|
|
e3bec58774 | ||
|
|
04e823ec0a | ||
|
|
08b61b266c | ||
|
|
afd8302c79 | ||
|
|
e6f37c9f8f | ||
|
|
b3a3ad4b38 | ||
|
|
e290baa415 | ||
|
|
47669f1cf8 | ||
|
|
ee975dc19e | ||
|
|
86c0cf532a | ||
|
|
d308e27189 | ||
|
|
6171041557 | ||
|
|
ab9e1411a7 | ||
|
|
c5ed4613ad | ||
|
|
6d1353647b | ||
|
|
3d1ebb7a1a | ||
|
|
6705875a4c | ||
|
|
fa6d709962 | ||
|
|
548f947c30 | ||
|
|
fc22ae24b1 | ||
|
|
01e9173ae4 | ||
|
|
d136f8abe4 | ||
|
|
5a5ee2319f | ||
|
|
13d87748c6 | ||
|
|
351a60859e | ||
|
|
147610941c | ||
|
|
7215b388d6 | ||
|
|
6188919445 | ||
|
|
0c224db94c | ||
|
|
7282951796 | ||
|
|
cac5a0f654 | ||
|
|
3dec3db75b | ||
|
|
f9bea18ffd | ||
|
|
888f824206 | ||
|
|
b3cb109816 | ||
|
|
f640f75635 | ||
|
|
ae42844eb6 | ||
|
|
ac470036fc | ||
|
|
b79e5bc112 | ||
|
|
a666c72622 | ||
|
|
7cc3e24846 | ||
|
|
a1f95e1529 | ||
|
|
9d79179c8d | ||
|
|
ddf5c42d79 | ||
|
|
e06fa9bd9a | ||
|
|
c2efb2c21f | ||
|
|
050cd07d18 | ||
|
|
d1a7ad3369 | ||
|
|
24e4aaf119 | ||
|
|
62e045134f | ||
|
|
54698c10c2 | ||
|
|
933aabae3b | ||
|
|
506f5c17ba | ||
|
|
7a369d7653 | ||
|
|
6ef2403627 | ||
|
|
eed2324c8c | ||
|
|
b5e0e58679 | ||
|
|
4f7458f187 | ||
|
|
4969c49c7a | ||
|
|
7ca8f6ec68 | ||
|
|
5df16e6634 | ||
|
|
b0e31eb7f5 | ||
|
|
c5674d78f0 | ||
|
|
85ecaf5481 | ||
|
|
79903fa639 | ||
|
|
d91970aed2 | ||
|
|
e17ae3ada3 | ||
|
|
4884faa94f | ||
|
|
3704fa928b | ||
|
|
6973e5af21 | ||
|
|
83332f265d | ||
|
|
8ea5646be5 | ||
|
|
ee2e75b057 | ||
|
|
8076d5c229 | ||
|
|
a74699202d | ||
|
|
21e555a628 | ||
|
|
9e4a5415ec | ||
|
|
bfcf19349b | ||
|
|
3571c1f405 | ||
|
|
291cf0c8d0 | ||
|
|
7ac001c1b8 | ||
|
|
2423ccf671 | ||
|
|
ce12de6593 | ||
|
|
0d83d3fef1 | ||
|
|
f717316109 |
@@ -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);
|
||||
|
||||
@@ -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') {
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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">
|
||||
|
||||
215
resources/views/admin/plans/show.blade.php
Normal file
215
resources/views/admin/plans/show.blade.php
Normal 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
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
<a data-role="{{ $role ?? '' }}" class="{{ $class ?? 'link' }}" href="{!! $href ?? '#' !!}">{{ $label ?? '' }}</a>
|
||||
@@ -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',
|
||||
])
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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
172
scripts/build_oneclick_bundle.sh
Executable 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
101
scripts/db_snapshot_import.sh
Executable 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
153
scripts/db_snapshot_publish.sh
Executable 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
16
scripts/git_push.sh
Executable 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."
|
||||
@@ -1,50 +1,15 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
# 安全推送到 Gitee:凭证从 /app/working.secret 读取,不写入仓库。
|
||||
# 用法:bash scripts/gitee_push.sh
|
||||
# 已弃用:历史上用于推送到 Gitee。
|
||||
# 现已迁移到自建 Gitea(origin 指向 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 已弃用:当前请推送到 Gitea(origin)。"
|
||||
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."
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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 成 &back=(会导致 back 被错误编码/叠加)
|
||||
$res->assertDontSee('&back=', false);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -73,7 +73,7 @@ class AdminDashboardBillingWorkbenchShouldIncludeBmpaFailedAndNoReceiptQuickLink
|
||||
|
||||
// 计数应渲染到按钮文案中
|
||||
$res->assertSee('BMPA失败(1)');
|
||||
$res->assertSee('无回执(1)');
|
||||
$res->assertSee('已付无回执(1)');
|
||||
|
||||
// 链接应携带 back,并且不应出现 &back=(避免回跳断链)
|
||||
$res->assertSee('bmpa_failed_only=1', false);
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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 为空');
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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([
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
422
tests/Feature/AdminPlanShowTest.php
Normal file
422
tests/Feature/AdminPlanShowTest.php
Normal 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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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');
|
||||
}
|
||||
}
|
||||
@@ -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('参数不完整:请提供 type(bas/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);
|
||||
}
|
||||
}
|
||||
@@ -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('&', '&', $safeBack), false);
|
||||
$res->assertSee('返回上一页');
|
||||
$res->assertSee($expectedRefreshUrl, false);
|
||||
$res->assertDontSee('run_id=' . $runId . '&back=' . $safeBack, false);
|
||||
$res->assertDontSee('back%3D', false);
|
||||
}
|
||||
}
|
||||
@@ -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('参数不完整:请提供 type(bas/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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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&run_id=' . $runId . '"', $html);
|
||||
$this->assertStringNotContainsString('data-role="batch-spot-check-next"', $html);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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&run_id=' . $runId . '&spot_after_id=', $html);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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&run_id=' . $runId . '"', $html);
|
||||
$this->assertStringNotContainsString('data-role="batch-spot-check-reset" href="/admin/platform-batches/show?type=bmpa&run_id=' . $runId . '&spot_after_id=', $html);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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 . '&back=', $html);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -38,7 +38,7 @@ class AdminPlatformOrderBatchMarkActivatedReceiptStatusFilterFieldsTest extends
|
||||
'published_at' => now(),
|
||||
]);
|
||||
|
||||
// 待处理订单:已支付 + pending;并且无回执
|
||||
// 待处理订单:已支付 + pending;并且无回执(广义)
|
||||
$order = PlatformOrder::query()->create([
|
||||
'merchant_id' => $merchant->id,
|
||||
'plan_id' => $plan->id,
|
||||
|
||||
@@ -34,5 +34,6 @@ class AdminPlatformOrderBatchMarkActivatedShouldBlockWhenReceiptStatusNoneTest e
|
||||
|
||||
$res->assertRedirect();
|
||||
$res->assertSessionHas('warning');
|
||||
$this->assertStringContainsString('无回执(广义)', (string) session('warning'));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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'));
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -87,7 +87,7 @@ class AdminPlatformOrderExportReceiptStatusFilterTest extends TestCase
|
||||
],
|
||||
]);
|
||||
|
||||
// 无回执
|
||||
// 无回执(广义)
|
||||
PlatformOrder::query()->create([
|
||||
'merchant_id' => $merchant->id,
|
||||
'plan_id' => $plan->id,
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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'] ?? ''));
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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'] ?? ''));
|
||||
}
|
||||
}
|
||||
@@ -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'] ?? ''));
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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'] ?? ''));
|
||||
}
|
||||
}
|
||||
@@ -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'] ?? ''));
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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'] ?? ''));
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
Reference in New Issue
Block a user