From 6a2652f69d66bfb6e0f0e91c029f2fbe0e5eca1c Mon Sep 17 00:00:00 2001 From: yml Date: Sat, 14 Nov 2026 17:04:24 +0800 Subject: [PATCH] =?UTF-8?q?feat(finance):=20=E5=A2=9E=E5=8A=A0=E4=BF=9D?= =?UTF-8?q?=E9=99=A9=E7=BB=9F=E8=AE=A1=E4=B8=8E=E8=AE=A2=E5=8D=95=E4=BF=9D?= =?UTF-8?q?=E9=99=A9=E6=98=8E=E7=BB=86?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../modules/adminfinance/dashboard.go | 24 ++++++++ .../modules/adminfinance/dashboard_cache.go | 2 +- .../modules/adminfinance/dashboard_test.go | 57 ++++++++++++++++++- backend/internal/modules/adminfinance/dto.go | 7 +++ .../src/features/admin/api/adminFinance.ts | 6 ++ .../__tests__/adminOrderPresentation.spec.ts | 42 ++++++++++++++ .../admin/utils/adminOrderPresentation.ts | 57 +++++++++++++++++++ .../admin/views/AdminFinanceDashboardView.css | 19 +++++++ .../admin/views/AdminFinanceDashboardView.vue | 56 ++++++++++++++++++ .../admin/views/AdminOrderDetailView.vue | 21 ++++++- scripts/prepare-test-db.sh | 4 +- 11 files changed, 288 insertions(+), 7 deletions(-) diff --git a/backend/internal/modules/adminfinance/dashboard.go b/backend/internal/modules/adminfinance/dashboard.go index 7bfb9339..b2f93235 100644 --- a/backend/internal/modules/adminfinance/dashboard.go +++ b/backend/internal/modules/adminfinance/dashboard.go @@ -406,6 +406,18 @@ func (r *Repository) dailySettlements(ctx context.Context, query DashboardQuery) err := db.Table("rental_orders AS ro USE INDEX (idx_rental_orders_dashboard_settled)"). Select(`DATE_FORMAT(ro.settled_at, '%Y-%m-%d') AS date, COALESCE(SUM(oc.platform_fee_cent), 0) AS platform_income_amount_cent, + COALESCE(SUM(CASE WHEN ro.compensation_plan = 'insurance' AND ro.insurance_fee_bps > 0 + THEN 1 ELSE 0 END), 0) AS insurance_order_count, + COALESCE(SUM(CASE WHEN ro.compensation_plan = 'insurance' AND ro.insurance_fee_bps > 0 + THEN COALESCE(oc.insurance_fee_cent, 0) ELSE 0 END), 0) AS insurance_profit_amount_cent, + COALESCE(SUM(CASE WHEN ro.compensation_plan = 'insurance' AND ro.insurance_fee_bps > 0 + AND ro.is_premium = 0 THEN 1 ELSE 0 END), 0) AS normal_insurance_order_count, + COALESCE(SUM(CASE WHEN ro.compensation_plan = 'insurance' AND ro.insurance_fee_bps > 0 + AND ro.is_premium = 0 THEN COALESCE(oc.insurance_fee_cent, 0) ELSE 0 END), 0) AS normal_insurance_profit_amount_cent, + COALESCE(SUM(CASE WHEN ro.compensation_plan = 'insurance' AND ro.insurance_fee_bps > 0 + AND ro.is_premium = 1 THEN 1 ELSE 0 END), 0) AS premium_insurance_order_count, + COALESCE(SUM(CASE WHEN ro.compensation_plan = 'insurance' AND ro.insurance_fee_bps > 0 + AND ro.is_premium = 1 THEN COALESCE(oc.insurance_fee_cent, 0) ELSE 0 END), 0) AS premium_insurance_profit_amount_cent, COALESCE(SUM(oc.owner_income_amount_cent), 0) AS owner_should_income_amount_cent, COALESCE(SUM(COALESCE(w.owner_wallet_income_amount_cent, 0)), 0) AS owner_wallet_income_amount_cent, COALESCE(SUM(CASE WHEN ro.settlement_mode = 'platform_managed' @@ -466,6 +478,12 @@ func summaryFromDailyRows(payments []dailyPaymentRow, settlements []dailySettlem } for _, row := range settlements { summary.NormalOrderProfitAmountCent += row.PlatformIncomeAmountCent + summary.InsuranceOrderCount += row.InsuranceOrderCount + summary.InsuranceProfitAmountCent += row.InsuranceProfitAmountCent + summary.NormalInsuranceOrderCount += row.NormalInsuranceOrderCount + summary.NormalInsuranceProfitAmountCent += row.NormalInsuranceProfitAmountCent + summary.PremiumInsuranceOrderCount += row.PremiumInsuranceOrderCount + summary.PremiumInsuranceProfitAmountCent += row.PremiumInsuranceProfitAmountCent summary.OwnerShouldIncomeAmountCent += row.OwnerShouldIncomeAmountCent summary.OwnerWalletIncomeAmountCent += row.OwnerWalletIncomeAmountCent summary.OwnerEffectiveSettlementAmountCent += row.OwnerEffectiveSettlementAmountCent @@ -604,6 +622,12 @@ type dailyPaidOrderRow struct { type dailySettlementRow struct { Date string PlatformIncomeAmountCent int64 + InsuranceOrderCount int64 + InsuranceProfitAmountCent int64 + NormalInsuranceOrderCount int64 + NormalInsuranceProfitAmountCent int64 + PremiumInsuranceOrderCount int64 + PremiumInsuranceProfitAmountCent int64 OwnerShouldIncomeAmountCent int64 OwnerWalletIncomeAmountCent int64 OwnerEffectiveSettlementAmountCent int64 diff --git a/backend/internal/modules/adminfinance/dashboard_cache.go b/backend/internal/modules/adminfinance/dashboard_cache.go index 5e4d592d..7b2d5e7e 100644 --- a/backend/internal/modules/adminfinance/dashboard_cache.go +++ b/backend/internal/modules/adminfinance/dashboard_cache.go @@ -10,7 +10,7 @@ import ( const dashboardCacheTTL = 5 * time.Minute func (r *Repository) dashboardCacheKey(query DashboardQuery) string { - return fmt.Sprintf("admin-finance:dashboard:v1:%d:%d", query.StartDate.Unix(), query.EndDate.Unix()) + return fmt.Sprintf("admin-finance:dashboard:v2:%d:%d", query.StartDate.Unix(), query.EndDate.Unix()) } func (r *Repository) loadDashboardCache(ctx context.Context, query DashboardQuery) *DashboardDTO { diff --git a/backend/internal/modules/adminfinance/dashboard_test.go b/backend/internal/modules/adminfinance/dashboard_test.go index e165c3a9..d663137b 100644 --- a/backend/internal/modules/adminfinance/dashboard_test.go +++ b/backend/internal/modules/adminfinance/dashboard_test.go @@ -257,8 +257,8 @@ func TestSummaryFromDailyRowsPreservesDashboardTotals(t *testing.T) { {TotalFlowAmountCent: 2500, TotalRefundAmountCent: 400, PendingRefundAmountCent: 70, SuccessfulPayCount: 5, PartialRefundCount: 2, PendingRefundCount: 1}, } settlementRows := []dailySettlementRow{ - {PlatformIncomeAmountCent: 300, OwnerShouldIncomeAmountCent: 900, OwnerWalletIncomeAmountCent: 850, OwnerEffectiveSettlementAmountCent: 850, OfflineSettlementAmountCent: 100, OfflineSettlementPendingAmountCent: 20, OfflineSettlementPendingCount: 1, FinancialExceptionCount: 2, SettledOrderCount: 3}, - {PlatformIncomeAmountCent: 200, OwnerShouldIncomeAmountCent: 600, OwnerWalletIncomeAmountCent: 500, OwnerEffectiveSettlementAmountCent: 550, OfflineSettlementAmountCent: 200, OfflineSettlementPendingAmountCent: 40, OfflineSettlementPendingCount: 2, FinancialExceptionCount: 1, SettledOrderCount: 2}, + {PlatformIncomeAmountCent: 300, InsuranceOrderCount: 2, InsuranceProfitAmountCent: 80, NormalInsuranceOrderCount: 1, NormalInsuranceProfitAmountCent: 50, PremiumInsuranceOrderCount: 1, PremiumInsuranceProfitAmountCent: 30, OwnerShouldIncomeAmountCent: 900, OwnerWalletIncomeAmountCent: 850, OwnerEffectiveSettlementAmountCent: 850, OfflineSettlementAmountCent: 100, OfflineSettlementPendingAmountCent: 20, OfflineSettlementPendingCount: 1, FinancialExceptionCount: 2, SettledOrderCount: 3}, + {PlatformIncomeAmountCent: 200, InsuranceOrderCount: 1, InsuranceProfitAmountCent: 20, NormalInsuranceOrderCount: 1, NormalInsuranceProfitAmountCent: 20, OwnerShouldIncomeAmountCent: 600, OwnerWalletIncomeAmountCent: 500, OwnerEffectiveSettlementAmountCent: 550, OfflineSettlementAmountCent: 200, OfflineSettlementPendingAmountCent: 40, OfflineSettlementPendingCount: 2, FinancialExceptionCount: 1, SettledOrderCount: 2}, } estimateRows := []dailyEstimatedRow{ {EstimatedIncomeAmountCent: 120, PendingSettleOrderCount: 4}, @@ -276,6 +276,11 @@ func TestSummaryFromDailyRowsPreservesDashboardTotals(t *testing.T) { if summary.NormalOrderProfitAmountCent != 500 || summary.PickupProfitAmountCent != 90 || summary.PlatformIncomeAmountCent != 590 || summary.PickupCompletedCount != 2 { t.Fatalf("平台利润汇总错误: %+v", summary) } + if summary.InsuranceOrderCount != 3 || summary.InsuranceProfitAmountCent != 100 || + summary.NormalInsuranceOrderCount != 2 || summary.NormalInsuranceProfitAmountCent != 70 || + summary.PremiumInsuranceOrderCount != 1 || summary.PremiumInsuranceProfitAmountCent != 30 { + t.Fatalf("保险汇总错误: %+v", summary) + } if summary.OwnerShouldIncomeAmountCent != 1500 || summary.OwnerWalletIncomeAmountCent != 1350 || summary.OwnerEffectiveSettlementAmountCent != 1400 || summary.SettlementDiffAmountCent != 100 { t.Fatalf("货主结算汇总错误: %+v", summary) } @@ -287,6 +292,54 @@ func TestSummaryFromDailyRowsPreservesDashboardTotals(t *testing.T) { } } +func TestInsuranceSummaryUsesSettlementDateAndPremiumSnapshot(t *testing.T) { + db := database.NewTestDB() + loc := timeutil.ShanghaiLocation() + settledAt := time.Date(2026, 8, 2, 10, 0, 0, 0, loc) + + for _, row := range [][]any{ + {1, "INS-NORMAL", 1, 1, 1, 1, settledAt, false, "insurance", 800}, + {2, "INS-PREMIUM", 2, 2, 2, 2, settledAt, true, "insurance", 400}, + {3, "NO-INSURANCE", 3, 3, 3, 3, settledAt, false, "normal", 0}, + {4, "OUTSIDE-RANGE", 4, 4, 4, 4, settledAt.AddDate(0, 0, 1), true, "insurance", 400}, + } { + if err := db.Exec(`INSERT INTO rental_orders + (id, order_no, listing_id, account_id, owner_id, renter_id, settled_at, + is_premium, compensation_plan, insurance_fee_bps) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, row...).Error; err != nil { + t.Fatalf("写入订单失败: %v", err) + } + } + for _, row := range [][]any{ + {1, 1, 1, "accepted", 800}, + {2, 2, 2, "accepted", 400}, + {3, 3, 3, "accepted", 0}, + {4, 4, 4, "accepted", 400}, + } { + if err := db.Exec(`INSERT INTO order_checkouts + (id, order_id, initiated_by, status, insurance_fee_cent) + VALUES (?, ?, ?, ?, ?)`, row...).Error; err != nil { + t.Fatalf("写入结账单失败: %v", err) + } + } + + repo := NewRepository(db) + query := DashboardQuery{ + StartDate: time.Date(2026, 8, 2, 0, 0, 0, 0, loc), + EndDate: time.Date(2026, 8, 2, 23, 59, 59, 0, loc), + } + settlements, err := repo.dailySettlements(t.Context(), query) + if err != nil { + t.Fatalf("读取每日结算汇总失败: %v", err) + } + summary := summaryFromDailyRows(nil, settlements, nil) + if summary.InsuranceOrderCount != 2 || summary.InsuranceProfitAmountCent != 1200 || + summary.NormalInsuranceOrderCount != 1 || summary.NormalInsuranceProfitAmountCent != 800 || + summary.PremiumInsuranceOrderCount != 1 || summary.PremiumInsuranceProfitAmountCent != 400 { + t.Fatalf("保险汇总错误: %+v", summary) + } +} + func TestDashboardCacheKeyAndSingleflight(t *testing.T) { repo := NewRepository(nil) q := DashboardQuery{ diff --git a/backend/internal/modules/adminfinance/dto.go b/backend/internal/modules/adminfinance/dto.go index 6f40eb1b..98bd0d7c 100644 --- a/backend/internal/modules/adminfinance/dto.go +++ b/backend/internal/modules/adminfinance/dto.go @@ -246,6 +246,13 @@ type FinanceSummaryDTO struct { PlatformIncomeAmountCent int64 `json:"platform_income_amount_cent"` NormalOrderProfitAmountCent int64 `json:"normal_order_profit_amount_cent"` PickupProfitAmountCent int64 `json:"pickup_profit_amount_cent"` + // 无忧保险统计与平台净利润同口径,按结算日归集。 + InsuranceOrderCount int64 `json:"insurance_order_count"` + InsuranceProfitAmountCent int64 `json:"insurance_profit_amount_cent"` + NormalInsuranceOrderCount int64 `json:"normal_insurance_order_count"` + NormalInsuranceProfitAmountCent int64 `json:"normal_insurance_profit_amount_cent"` + PremiumInsuranceOrderCount int64 `json:"premium_insurance_order_count"` + PremiumInsuranceProfitAmountCent int64 `json:"premium_insurance_profit_amount_cent"` // PickupCompletedCount 线下提号利润对应的已完成提号笔数。 PickupCompletedCount int64 `json:"pickup_completed_count"` OwnerShouldIncomeAmountCent int64 `json:"owner_should_income_amount_cent"` diff --git a/frontend/src/features/admin/api/adminFinance.ts b/frontend/src/features/admin/api/adminFinance.ts index 7f170206..c1e8cd34 100644 --- a/frontend/src/features/admin/api/adminFinance.ts +++ b/frontend/src/features/admin/api/adminFinance.ts @@ -13,6 +13,12 @@ export interface FinanceSummary { platform_income_amount_cent: number normal_order_profit_amount_cent: number pickup_profit_amount_cent: number + insurance_order_count: number + insurance_profit_amount_cent: number + normal_insurance_order_count: number + normal_insurance_profit_amount_cent: number + premium_insurance_order_count: number + premium_insurance_profit_amount_cent: number /** 普通(租赁)订单利润对应的已结算订单数量 */ settled_order_count: number /** 线下提号利润对应的已完成提号笔数 */ diff --git a/frontend/src/features/admin/utils/__tests__/adminOrderPresentation.spec.ts b/frontend/src/features/admin/utils/__tests__/adminOrderPresentation.spec.ts index 82ab90fd..f0240642 100644 --- a/frontend/src/features/admin/utils/__tests__/adminOrderPresentation.spec.ts +++ b/frontend/src/features/admin/utils/__tests__/adminOrderPresentation.spec.ts @@ -2,6 +2,7 @@ import { describe, expect, it } from 'vitest' import { estimateOrderEndAt, + orderPlatformFeeBreakdown, paymentBizTypeLabel, paymentPaidAt, paymentStatusLabel, @@ -39,4 +40,45 @@ describe('admin order presentation', () => { ).toBe('paid') expect(paymentPaidAt({ updated_at: 'updated', created_at: 'created' })).toBe('updated') }) + + it('splits insurance from the final checkout platform income', () => { + expect( + orderPlatformFeeBreakdown({ + status: 'completed', + platform_fee_cent: 3260, + insurance_fee_cent: 1739, + insurance_fee_bps: 800, + checkout: { + status: 'accepted', + platform_fee_cent: 4999, + insurance_fee_cent: 1739, + insurance_fee_bps: 800, + }, + }) + ).toEqual({ + baseFeeCent: 3260, + insuranceFeeCent: 1739, + insuranceFeeBps: 800, + totalFeeCent: 4999, + }) + }) + + it('keeps the order snapshot before settlement and when there is no insurance', () => { + expect( + orderPlatformFeeBreakdown({ + status: 'renting', + platform_fee_cent: 4000, + insurance_fee_cent: 0, + checkout: { + status: 'submitted', + platform_fee_cent: 5000, + }, + }) + ).toEqual({ + baseFeeCent: 4000, + insuranceFeeCent: 0, + insuranceFeeBps: 0, + totalFeeCent: 4000, + }) + }) }) diff --git a/frontend/src/features/admin/utils/adminOrderPresentation.ts b/frontend/src/features/admin/utils/adminOrderPresentation.ts index 1f20c58b..f092a79a 100644 --- a/frontend/src/features/admin/utils/adminOrderPresentation.ts +++ b/frontend/src/features/admin/utils/adminOrderPresentation.ts @@ -1,5 +1,62 @@ import type { AdminPayment } from '@/features/admin/api/adminPayments' +type OrderPlatformFeeSource = { + status?: string + platform_fee_cent?: number + insurance_fee_cent?: number + insurance_fee_bps?: number + checkout?: { + status?: string + platform_fee_cent?: number + insurance_fee_cent?: number + insurance_fee_bps?: number + } | null +} + +export type OrderPlatformFeeBreakdown = { + baseFeeCent: number + insuranceFeeCent: number + insuranceFeeBps: number + totalFeeCent: number +} + +function nonNegativeCent(value: number | undefined) { + const amount = Number(value || 0) + return Number.isFinite(amount) ? Math.max(amount, 0) : 0 +} + +/** + * 订单主表保存下单时的平台基础费用,结账单保存结算后的平台收入合计。 + * 已完成订单优先采用结账单,并从合计中拆出无忧保险,避免客服看到旧快照。 + */ +export function orderPlatformFeeBreakdown( + order: OrderPlatformFeeSource | null | undefined +): OrderPlatformFeeBreakdown { + if (!order) { + return { baseFeeCent: 0, insuranceFeeCent: 0, insuranceFeeBps: 0, totalFeeCent: 0 } + } + + const orderFeeCent = nonNegativeCent(order.platform_fee_cent) + const checkoutFeeCent = nonNegativeCent(order.checkout?.platform_fee_cent) + const totalFeeCent = + order.status === 'completed' ? Math.max(orderFeeCent, checkoutFeeCent) : orderFeeCent + const orderInsuranceFeeCent = nonNegativeCent(order.insurance_fee_cent) + const checkoutInsuranceFeeCent = nonNegativeCent(order.checkout?.insurance_fee_cent) + const insuranceFeeCent = + orderInsuranceFeeCent > 0 ? orderInsuranceFeeCent : checkoutInsuranceFeeCent + const insuranceFeeBps = Math.max( + Number(order.insurance_fee_bps || 0), + insuranceFeeCent > 0 ? Number(order.checkout?.insurance_fee_bps || 0) : 0 + ) + + return { + baseFeeCent: Math.max(totalFeeCent - insuranceFeeCent, 0), + insuranceFeeCent: Math.min(insuranceFeeCent, totalFeeCent), + insuranceFeeBps, + totalFeeCent, + } +} + export function estimateOrderEndAt( estimatedEndAt?: string, rentedAt?: string, diff --git a/frontend/src/features/admin/views/AdminFinanceDashboardView.css b/frontend/src/features/admin/views/AdminFinanceDashboardView.css index b973f556..a3741334 100644 --- a/frontend/src/features/admin/views/AdminFinanceDashboardView.css +++ b/frontend/src/features/admin/views/AdminFinanceDashboardView.css @@ -386,6 +386,11 @@ color: #4f46e5; } +.section-badge.amber { + background: #fffbeb; + color: #d97706; +} + .section-title { margin: 0; font-size: 17px; @@ -457,6 +462,20 @@ flex-wrap: wrap; } +.insurance-type-card .sub-card-label { + display: flex; + align-items: center; + gap: 6px; +} + +.insurance-type-card--normal { + border-left: 3px solid #f59e0b; +} + +.insurance-type-card--premium { + border-left: 3px solid #8b5cf6; +} + .dot-sep { color: #cbd5e1; } diff --git a/frontend/src/features/admin/views/AdminFinanceDashboardView.vue b/frontend/src/features/admin/views/AdminFinanceDashboardView.vue index 488b47b6..5f37c502 100644 --- a/frontend/src/features/admin/views/AdminFinanceDashboardView.vue +++ b/frontend/src/features/admin/views/AdminFinanceDashboardView.vue @@ -4,12 +4,14 @@ import { Coin, CreditCard, DocumentChecked, + Medal, Money, Refresh, Search, ShoppingCart, Timer, TrendCharts, + Umbrella, Van, Wallet, WarningFilled, @@ -370,6 +372,60 @@ function diffType(value: number | undefined | null) { + +
+
+
+
+ +
+
+

无忧保险

+

按结算日统计投保订单数量与保险服务费利润

+
+
+
+ +
+
+
保险利润合计
+
+ {{ moneyCent(dashboard.summary.insurance_profit_amount_cent) }} +
+
+ {{ dashboard.summary.insurance_order_count }} 笔已结算投保订单 +
+
+ +
+
普通无忧保险
+
+ {{ moneyCent(dashboard.summary.normal_insurance_profit_amount_cent) }} +
+
+ {{ dashboard.summary.normal_insurance_order_count }} 笔投保订单 + · + 保险利润 +
+
+ +
+
+ + 极品专区保险 +
+
+ {{ moneyCent(dashboard.summary.premium_insurance_profit_amount_cent) }} +
+
+ {{ dashboard.summary.premium_insurance_order_count }} 笔投保订单 + · + 保险利润 +
+
+
+
+
diff --git a/frontend/src/features/admin/views/AdminOrderDetailView.vue b/frontend/src/features/admin/views/AdminOrderDetailView.vue index ade19cbd..b48bfc78 100644 --- a/frontend/src/features/admin/views/AdminOrderDetailView.vue +++ b/frontend/src/features/admin/views/AdminOrderDetailView.vue @@ -51,7 +51,11 @@ import { formatGameName, formatListingNo } from '@/shared/utils/listingDisplay' import AdminOrderMetrics, { type AdminOrderMetric } from '../components/AdminOrderMetrics.vue' import AdminOrderPaymentRecords from '../components/AdminOrderPaymentRecords.vue' import ProcessTimeline from '../components/ProcessTimeline.vue' -import { estimateOrderEndAt, paymentStatusLabel } from '../utils/adminOrderPresentation' +import { + estimateOrderEndAt, + orderPlatformFeeBreakdown, + paymentStatusLabel, +} from '../utils/adminOrderPresentation' const route = useRoute() const router = useRouter() @@ -362,6 +366,7 @@ const ownerLossPriceCent = computed(() => { return Math.max(Number(order.value.owner_rent_amount_cent) - ownerCoinBasePriceCent.value, 0) }) const hasOwnerRentBreakdown = computed(() => ownerCoinBasePriceCent.value !== null) +const platformFeeBreakdown = computed(() => orderPlatformFeeBreakdown(order.value)) const fundSplitRows = computed(() => { if (!order.value) return [] const rows: FundSplitRow[] = [] @@ -398,8 +403,20 @@ const fundSplitRows = computed(() => { } else { rows.push({ label: '号主租金', amountCent: order.value.owner_rent_amount_cent }) } + const platformFee = platformFeeBreakdown.value + if (platformFee.insuranceFeeCent > 0) { + rows.push( + { label: '平台基础费用', amountCent: platformFee.baseFeeCent }, + { + label: `无忧租号保险服务费(${(platformFee.insuranceFeeBps / 100).toFixed(1)}%)`, + amountCent: platformFee.insuranceFeeCent, + }, + { label: '平台收入合计', amountCent: platformFee.totalFeeCent } + ) + } else { + rows.push({ label: '平台费用', amountCent: platformFee.totalFeeCent }) + } rows.push( - { label: '平台费用', amountCent: order.value.platform_fee_cent }, { label: '实付押金', amountCent: order.value.deposit_amount_cent }, { label: '原始押金', amountCent: order.value.deposit_original_amount_cent }, { label: '免押额度', amountCent: order.value.deposit_waived_amount_cent } diff --git a/scripts/prepare-test-db.sh b/scripts/prepare-test-db.sh index f6dc33e4..0638f73c 100755 --- a/scripts/prepare-test-db.sh +++ b/scripts/prepare-test-db.sh @@ -24,8 +24,8 @@ container="${TEST_MYSQL_CONTAINER:-hfb-mysql}" [[ "${base_database}" =~ ^[A-Za-z0-9_]+$ ]] || { printf '测试库基础名不合法: %s\n' "${base_database}" >&2; exit 2; } -# MySQL 模式串里下划线是通配符,需要转义成字面量。 -pattern="$(printf '%s' "${base_database}" | sed 's/_/\\\\_/g')" +# MySQL 模式串里下划线是通配符,需要转义成字面量;只保留一层反斜杠。 +pattern="$(printf '%s' "${base_database}" | sed 's/_/\\_/g')" run_admin_sql() { if command -v docker >/dev/null 2>&1 && docker ps --format '{{.Names}}' 2>/dev/null | grep -qx "${container}"; then