From 1c18dbcb64f63fab73ce7100ad09a8cc6c8a381d Mon Sep 17 00:00:00 2001 From: yml2213 Date: Mon, 2 Nov 2026 19:11:22 +0800 Subject: [PATCH] =?UTF-8?q?perf(finance):=20=E4=BC=98=E5=8C=96=E8=B4=A2?= =?UTF-8?q?=E5=8A=A1=E4=BB=AA=E8=A1=A8=E7=9B=98=E8=81=9A=E5=90=88=E6=9F=A5?= =?UTF-8?q?=E8=AF=A2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../modules/adminfinance/dashboard.go | 6 ++-- .../internal/modules/adminfinance/subquery.go | 4 +-- ...000091_finance_dashboard_query_indexes.sql | 35 +++++++++++++++++++ scripts/check-sql-guard.sh | 9 +++++ 4 files changed, 49 insertions(+), 5 deletions(-) create mode 100644 backend/migrations/000091_finance_dashboard_query_indexes.sql diff --git a/backend/internal/modules/adminfinance/dashboard.go b/backend/internal/modules/adminfinance/dashboard.go index bea5e64..d80876b 100644 --- a/backend/internal/modules/adminfinance/dashboard.go +++ b/backend/internal/modules/adminfinance/dashboard.go @@ -403,7 +403,7 @@ func (r *Repository) dailyEstimates(ctx context.Context, query DashboardQuery) ( func (r *Repository) dailySettlements(ctx context.Context, query DashboardQuery) ([]dailySettlementRow, error) { db := r.db.WithContext(ctx) rows := make([]dailySettlementRow, 0) - err := db.Table("rental_orders AS ro"). + 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(oc.owner_income_amount_cent), 0) AS owner_should_income_amount_cent, @@ -445,7 +445,7 @@ func (r *Repository) dailySettlements(ctx context.Context, query DashboardQuery) COUNT(ro.id) AS settled_order_count`, settlementDiffThresholdCent, refundBizTypes(), []string{"refunding", "failed"}). // 统计口径:只算"正常结账确认"的订单。仲裁结案的结账单终态是 settled // (由 dispute 模块写入),不纳入本指标,此处保持既有口径不变。 - Joins("JOIN order_checkouts AS oc ON oc.order_id = ro.id AND oc.status = ?", ordermodule.CheckoutStatusAccepted). + Joins("JOIN order_checkouts AS oc USE INDEX (idx_order_checkouts_dashboard_accepted) ON oc.order_id = ro.id AND oc.status = ?", ordermodule.CheckoutStatusAccepted). Joins("LEFT JOIN (?) AS w ON w.order_id = ro.id", ownerWalletIncomeForSettledOrdersSubquery(db, query)). Where("ro.settled_at >= ? AND ro.settled_at <= ?", query.StartDate, query.EndDate). Group("DATE_FORMAT(ro.settled_at, '%Y-%m-%d')"). @@ -523,7 +523,7 @@ func operatingExpenseSummaryFromDailyRows(rows []dailyOperatingExpenseRow) Opera func (r *Repository) dailyPayments(ctx context.Context, query DashboardQuery) ([]dailyPaymentRow, error) { rows := make([]dailyPaymentRow, 0) db := r.db.WithContext(ctx) - err := db.Table("payment_orders AS po"). + err := db.Table("payment_orders AS po USE INDEX (idx_payment_orders_dashboard_created)"). Select(`DATE_FORMAT(po.created_at, '%Y-%m-%d') AS date, COALESCE(SUM(CASE WHEN po.biz_type IN ? AND po.status = 'paid' THEN po.amount_cent ELSE 0 END), 0) AS total_flow_amount_cent, COALESCE(SUM(CASE WHEN po.biz_type IN ? AND po.status = 'refunded' THEN po.amount_cent ELSE 0 END), 0) AS total_refund_amount_cent, diff --git a/backend/internal/modules/adminfinance/subquery.go b/backend/internal/modules/adminfinance/subquery.go index 6759d09..f732ffd 100644 --- a/backend/internal/modules/adminfinance/subquery.go +++ b/backend/internal/modules/adminfinance/subquery.go @@ -50,11 +50,11 @@ func ownerWalletIncomeForSettledOrdersSubquery(db *gorm.DB, query DashboardQuery // paymentOriginalAmountForRefundsInRangeSubquery 仅为当前区间内的退款单回查原支付金额。 // 退款统计原先会对全部历史支付单按 order_id 聚合;数据增长后这部分即使只看一天也会很慢。 func paymentOriginalAmountForRefundsInRangeSubquery(db *gorm.DB, query DashboardQuery) *gorm.DB { - refundOrders := db.Table("payment_orders AS refund"). + refundOrders := db.Table("payment_orders AS refund USE INDEX (idx_payment_orders_dashboard_created)"). Select("DISTINCT refund.order_id"). Where("refund.biz_type IN ? AND refund.status = ?", refundBizTypes(), "refunded"). Where("refund.created_at >= ? AND refund.created_at <= ?", query.StartDate, query.EndDate) - return db.Table("payment_orders AS paid"). + return db.Table("payment_orders AS paid USE INDEX (idx_payment_orders_dashboard_order)"). Select("paid.order_id, MAX(paid.amount_cent) AS amount_cent"). Joins("JOIN (?) AS refund_order ON refund_order.order_id = paid.order_id", refundOrders). Where("paid.biz_type IN ? AND paid.status = ?", payBizTypes(), "paid"). diff --git a/backend/migrations/000091_finance_dashboard_query_indexes.sql b/backend/migrations/000091_finance_dashboard_query_indexes.sql new file mode 100644 index 0000000..1577f7b --- /dev/null +++ b/backend/migrations/000091_finance_dashboard_query_indexes.sql @@ -0,0 +1,35 @@ +-- +goose Up +-- 财务仪表盘按时间窗口聚合支付、退款和结算数据。 +-- 支付索引避免退款统计扫描全部历史状态;结算与结账索引避免日报聚合回表。 +ALTER TABLE payment_orders + ADD KEY idx_payment_orders_dashboard_created (created_at, biz_type, status, order_id, amount_cent), + ADD KEY idx_payment_orders_dashboard_order (order_id, biz_type, status, amount_cent); + +ALTER TABLE rental_orders + ADD KEY idx_rental_orders_dashboard_settled ( + settled_at, + id, + settlement_mode, + offline_settlement_status, + offline_settlement_amount_cent, + refund_status + ); + +ALTER TABLE order_checkouts + ADD KEY idx_order_checkouts_dashboard_accepted ( + status, + order_id, + platform_fee_cent, + owner_income_amount_cent + ); + +-- +goose Down +ALTER TABLE order_checkouts + DROP KEY idx_order_checkouts_dashboard_accepted; + +ALTER TABLE rental_orders + DROP KEY idx_rental_orders_dashboard_settled; + +ALTER TABLE payment_orders + DROP KEY idx_payment_orders_dashboard_order, + DROP KEY idx_payment_orders_dashboard_created; diff --git a/scripts/check-sql-guard.sh b/scripts/check-sql-guard.sh index 5874ea9..9a344e8 100755 --- a/scripts/check-sql-guard.sh +++ b/scripts/check-sql-guard.sh @@ -31,6 +31,15 @@ require_pattern "backend/internal/database/mysql.go" "SetMaxOpenConns" "连接 # 当前财务仪表盘的时间范围聚合和索引是高频查询防线。 require_pattern "backend/internal/modules/adminfinance/subquery.go" "settled_at >= ? AND scoped_order.settled_at <= ?" "结算聚合时间范围" require_pattern "backend/internal/modules/adminfinance/dashboard.go" "po.created_at >= ? AND po.created_at <= ?" "支付聚合时间范围" +require_pattern "backend/internal/modules/adminfinance/dashboard.go" "USE INDEX (idx_payment_orders_dashboard_created)" "支付聚合时间窗口索引" +require_pattern "backend/internal/modules/adminfinance/subquery.go" "USE INDEX (idx_payment_orders_dashboard_created)" "退款统计时间窗口索引" +require_pattern "backend/internal/modules/adminfinance/subquery.go" "USE INDEX (idx_payment_orders_dashboard_order)" "退款原金额订单索引" +require_pattern "backend/migrations/000091_finance_dashboard_query_indexes.sql" "idx_payment_orders_dashboard_created" "财务支付聚合时间索引" +require_pattern "backend/migrations/000091_finance_dashboard_query_indexes.sql" "idx_payment_orders_dashboard_order" "财务退款原金额索引" +require_pattern "backend/migrations/000091_finance_dashboard_query_indexes.sql" "idx_rental_orders_dashboard_settled" "财务结算日报覆盖索引" +require_pattern "backend/migrations/000091_finance_dashboard_query_indexes.sql" "idx_order_checkouts_dashboard_accepted" "财务结账行覆盖索引" +require_pattern "backend/internal/modules/adminfinance/dashboard.go" "USE INDEX (idx_rental_orders_dashboard_settled)" "结算日报时间索引" +require_pattern "backend/internal/modules/adminfinance/dashboard.go" "USE INDEX (idx_order_checkouts_dashboard_accepted)" "结算日报结账索引" require_pattern "backend/migrations/000060_finance_dashboard_indexes.sql" "idx_admin_pickups_status_completed_at" "财务完成索引" require_pattern "backend/migrations/000060_finance_dashboard_indexes.sql" "idx_admin_pickups_status_cancelled_at" "财务取消索引"