package adminfinance import ( "context" "errors" "sync" ordermodule "hfb_sys/backend/internal/modules/order" "hfb_sys/backend/internal/timeutil" ) func (r *Repository) Dashboard(ctx context.Context, query DashboardQuery) (*DashboardDTO, error) { if cached := r.loadDashboardCache(ctx, query); cached != nil { return cached, nil } key := r.dashboardCacheKey(query) val, err, _ := r.sfGroup.Do(key, func() (any, error) { // 在 singleflight 内部二次检查缓存,避免排队等待的并发请求重复执行大聚合 if cached := r.loadDashboardCache(ctx, query); cached != nil { return cached, nil } return r.calculateDashboard(ctx, query) }) if err != nil { return nil, err } res, ok := val.(*DashboardDTO) if !ok { return nil, errors.New("invalid dashboard result type") } return res, nil } func (r *Repository) calculateDashboard(ctx context.Context, query DashboardQuery) (*DashboardDTO, error) { // 各板块的读取互不依赖。并行执行能避免单个请求串行等待二十余条统计 SQL, // 首次打开或缓存失效时仍可尽快返回;每个查询继续复用 GORM 的连接池。 var ( daily dashboardDailyResult pickup *PickupSummaryDTO disbursement *DisbursementSummaryDTO ) var wg sync.WaitGroup errCh := make(chan error, 3) run := func(fn func() error) { wg.Add(1) go func() { defer wg.Done() if err := fn(); err != nil { errCh <- err } }() } run(func() error { var err error daily, err = r.dailyItems(ctx, query) return err }) run(func() error { var err error pickup, err = r.pickupSummary(ctx, query) return err }) run(func() error { var err error disbursement, err = r.disbursementSummary(ctx, query) return err }) wg.Wait() close(errCh) for err := range errCh { if err != nil { return nil, err } } summary := summaryWithPickup(daily.Summary, pickup) result := &DashboardDTO{ Summary: summary, DailyItems: daily.Items, PickupSummary: *pickup, MohongSummary: daily.Mohong, DisbursementSummary: *disbursement, OperatingExpenseSummary: daily.OperatingExpense, GeneratedAt: timeutil.ShanghaiNow(), } r.storeDashboardCache(ctx, query, result) return result, nil } func (r *Repository) operatingExpenseSummary(ctx context.Context, query DashboardQuery) (*OperatingExpenseSummaryDTO, error) { var summary OperatingExpenseSummaryDTO err := r.db.WithContext(ctx).Table("operating_expenses"). Select("COALESCE(SUM(amount_cent), 0) AS amount_cent, COUNT(id) AS count"). Where("status = ?", "paid"). Where("occurred_at >= ? AND occurred_at <= ?", query.StartDate, query.EndDate). Scan(&summary).Error if err != nil { return nil, err } return &summary, nil } // pickupSummary 线下提号统计,独立查询 admin_pickups 表,不混入正常订单口径。 func (r *Repository) pickupSummary(ctx context.Context, query DashboardQuery) (*PickupSummaryDTO, error) { db := r.db.WithContext(ctx) var row struct { SettledAmountCent int64 ProfitAmountCent int64 CompletedCount int64 } if err := db.Table("admin_pickups USE INDEX (idx_admin_pickups_dashboard_completed)"). Select(`COALESCE(SUM(settle_amount_cent), 0) AS settled_amount_cent, COALESCE(SUM(profit_amount_cent), 0) AS profit_amount_cent, COUNT(id) AS completed_count`). Where("status = ?", "completed"). Where("completed_at >= ? AND completed_at <= ?", query.StartDate, query.EndDate). Scan(&row).Error; err != nil { return nil, err } var adjustments struct { SettledAmountCent int64 ProfitAmountCent int64 } if err := db.Table("admin_pickup_financial_adjustments AS fa"). Select(`COALESCE(SUM(fa.settle_delta_cent), 0) AS settled_amount_cent, COALESCE(SUM(fa.profit_delta_cent), 0) AS profit_amount_cent`). Joins("JOIN admin_pickups AS p ON p.id = fa.pickup_id AND p.status = ?", "completed"). Where("fa.created_at >= ? AND fa.created_at <= ?", query.StartDate, query.EndDate). Scan(&adjustments).Error; err != nil { return nil, err } var inProgress int64 if err := db.Table("admin_pickups").Where("status = ?", "picking_up").Count(&inProgress).Error; err != nil { return nil, err } var pending struct { AmountCent int64 Count int64 } if err := db.Table("admin_pickups USE INDEX (idx_admin_pickups_dashboard_offline_settlement)"). Select(`COALESCE(SUM(settle_amount_cent), 0) AS amount_cent, COUNT(id) AS count`). Where("settlement_mode = ? AND offline_settlement_status = ? AND settle_amount_cent > 0", "platform_managed", "pending"). Scan(&pending).Error; err != nil { return nil, err } var pendingAdjustments struct { AmountCent int64 Count int64 } if err := db.Table("admin_pickup_financial_adjustments"). Select(`COALESCE(SUM(settle_delta_cent), 0) AS amount_cent, COUNT(id) AS count`). Where("settlement_mode = ? AND status = ? AND settle_delta_cent > 0", "platform_managed", "pending"). Scan(&pendingAdjustments).Error; err != nil { return nil, err } return &PickupSummaryDTO{ SettledAmountCent: row.SettledAmountCent + adjustments.SettledAmountCent, ProfitAmountCent: row.ProfitAmountCent + adjustments.ProfitAmountCent, CompletedCount: row.CompletedCount, InProgressCount: inProgress, PendingAmountCent: pending.AmountCent + pendingAdjustments.AmountCent, PendingCount: pending.Count + pendingAdjustments.Count, }, nil } // mohongSummary 撞车商城按支付订单统计交易流水,不计入平台利润。 func (r *Repository) mohongSummary(ctx context.Context, query DashboardQuery) (*MohongSummaryDTO, error) { db := r.db.WithContext(ctx) var row struct { FlowAmountCent int64 RefundAmountCent int64 PaidOrderCount int64 } if err := db.Table("payment_orders"). Select(`COALESCE(SUM(CASE WHEN biz_type = 'mohong_pay' AND status = 'paid' THEN amount_cent ELSE 0 END), 0) AS flow_amount_cent, COALESCE(SUM(CASE WHEN biz_type = 'mohong_refund' AND status = 'refunded' THEN amount_cent ELSE 0 END), 0) AS refund_amount_cent, COALESCE(SUM(CASE WHEN biz_type = 'mohong_pay' AND status = 'paid' THEN 1 ELSE 0 END), 0) AS paid_order_count`). Where("created_at >= ? AND created_at <= ?", query.StartDate, query.EndDate). Scan(&row).Error; err != nil { return nil, err } return &MohongSummaryDTO{ FlowAmountCent: row.FlowAmountCent, RefundAmountCent: row.RefundAmountCent, NetAmountCent: row.FlowAmountCent - row.RefundAmountCent, PaidOrderCount: row.PaidOrderCount, }, nil } type dashboardDailyResult struct { Items []FinanceDailyDTO Summary FinanceSummaryDTO Mohong MohongSummaryDTO OperatingExpense OperatingExpenseSummaryDTO } func (r *Repository) dailyItems(ctx context.Context, query DashboardQuery) (dashboardDailyResult, error) { db := r.db.WithContext(ctx) var ( payments []dailyPaymentRow settlements []dailySettlementRow estimates []dailyEstimatedRow ) var wg sync.WaitGroup errCh := make(chan error, 3) run := func(fn func() error) { wg.Add(1) go func() { defer wg.Done() if err := fn(); err != nil { errCh <- err } }() } run(func() error { var err error payments, err = r.dailyPayments(ctx, query) return err }) run(func() error { var err error settlements, err = r.dailySettlements(ctx, query) return err }) run(func() error { var err error estimates, err = r.dailyEstimates(ctx, query) return err }) wg.Wait() close(errCh) for err := range errCh { if err != nil { return dashboardDailyResult{}, err } } paidOrders, err := r.dailyPaidOrders(ctx, query) if err != nil { return dashboardDailyResult{}, err } pickupProfits := make([]dailyPickupProfitRow, 0) if err := db.Table("admin_pickups USE INDEX (idx_admin_pickups_dashboard_completed)"). Select(`DATE_FORMAT(completed_at, '%Y-%m-%d') AS date, COALESCE(SUM(profit_amount_cent), 0) AS profit_amount_cent, COUNT(id) AS completed_count`). Where("status = ?", "completed"). Where("completed_at >= ? AND completed_at <= ?", query.StartDate, query.EndDate). Group("DATE_FORMAT(completed_at, '%Y-%m-%d')"). Scan(&pickupProfits).Error; err != nil { return dashboardDailyResult{}, err } pickupAdjustments := make([]dailyPickupAdjustmentRow, 0) if err := db.Table("admin_pickup_financial_adjustments AS fa"). Select(`DATE_FORMAT(fa.created_at, '%Y-%m-%d') AS date, COALESCE(SUM(fa.profit_delta_cent), 0) AS profit_amount_cent`). Joins("JOIN admin_pickups AS p ON p.id = fa.pickup_id AND p.status = ?", "completed"). Where("fa.created_at >= ? AND fa.created_at <= ?", query.StartDate, query.EndDate). Group("DATE_FORMAT(fa.created_at, '%Y-%m-%d')"). Scan(&pickupAdjustments).Error; err != nil { return dashboardDailyResult{}, err } // 取消提号按取消日统计,与「已付款订单数」中的提号创建口径区分开。 pickupCancelled, err := r.dailyPickupCancelled(ctx, query) if err != nil { return dashboardDailyResult{}, err } // 每日预计收入:尚未结算订单的下单预估平台手续费,按下单日期归集,口径与 summary 一致。 disbursements, err := r.dailyDisbursements(ctx, query) if err != nil { return dashboardDailyResult{}, err } operatingExpenses, err := r.dailyOperatingExpenses(ctx, query) if err != nil { return dashboardDailyResult{}, err } itemsByDate := make(map[string]FinanceDailyDTO) for day := dayStart(query.StartDate); !day.After(query.EndDate); day = day.AddDate(0, 0, 1) { date := day.Format("2006-01-02") itemsByDate[date] = FinanceDailyDTO{Date: date} } for _, row := range payments { date := dailyDateKey(row.Date) item := itemsByDate[date] item.Date = date item.TotalFlowAmountCent = row.TotalFlowAmountCent item.TotalRefundAmountCent = row.TotalRefundAmountCent item.PendingRefundAmountCent = row.PendingRefundAmountCent item.ChannelNetAmountCent = row.TotalFlowAmountCent - row.TotalRefundAmountCent item.SuccessfulPayCount = row.SuccessfulPayCount item.SuccessfulRefundCount = row.FullRefundCount + row.PartialRefundCount item.FullRefundCount = row.FullRefundCount item.PartialRefundCount = row.PartialRefundCount item.PendingRefundCount = row.PendingRefundCount item.NormalFullRefundCount = row.NormalFullRefundCount itemsByDate[date] = item } for _, row := range paidOrders { date := dailyDateKey(row.Date) item := itemsByDate[date] item.Date = date item.PaidOrderCount = row.PaidOrderCount itemsByDate[date] = item } for _, row := range settlements { date := dailyDateKey(row.Date) item := itemsByDate[date] item.Date = date item.PlatformIncomeAmountCent = row.PlatformIncomeAmountCent item.OwnerShouldIncomeAmountCent = row.OwnerShouldIncomeAmountCent item.OwnerWalletIncomeAmountCent = row.OwnerWalletIncomeAmountCent item.OwnerEffectiveSettlementAmountCent = row.OwnerEffectiveSettlementAmountCent item.OfflineSettlementAmountCent = row.OfflineSettlementAmountCent item.OfflineSettlementPendingAmountCent = row.OfflineSettlementPendingAmountCent item.SettlementDiffAmountCent = row.OwnerShouldIncomeAmountCent - row.OwnerEffectiveSettlementAmountCent item.SettledOrderCount = row.SettledOrderCount item.SalesCount = row.SettledOrderCount item.OfflineSettlementPendingCount = row.OfflineSettlementPendingCount itemsByDate[date] = item } for _, row := range pickupProfits { date := dailyDateKey(row.Date) item := itemsByDate[date] item.Date = date item.PlatformIncomeAmountCent += row.ProfitAmountCent item.PickupCompletedCount = row.CompletedCount item.SalesCount += row.CompletedCount itemsByDate[date] = item } for _, row := range pickupAdjustments { date := dailyDateKey(row.Date) item := itemsByDate[date] item.Date = date item.PlatformIncomeAmountCent += row.ProfitAmountCent itemsByDate[date] = item } for _, row := range pickupCancelled { date := dailyDateKey(row.Date) item := itemsByDate[date] item.Date = date item.PickupCancelledCount = row.Count itemsByDate[date] = item } for _, row := range estimates { date := dailyDateKey(row.Date) item := itemsByDate[date] item.Date = date item.EstimatedIncomeAmountCent = row.EstimatedIncomeAmountCent itemsByDate[date] = item } for _, row := range disbursements { date := dailyDateKey(row.Date) item := itemsByDate[date] item.Date = date item.OfflineSettlementPaidAmountCent = row.OfflineSettlementPaidAmountCent item.OfflineSettlementPaidCount = row.OfflineSettlementPaidCount item.WithdrawalPaidAmountCent = row.WithdrawalPaidAmountCent item.WithdrawalPaidCount = row.WithdrawalPaidCount item.ManualPaidAmountCent = row.ManualPaidAmountCent item.ManualPaidCount = row.ManualPaidCount item.DisbursementPaidAmountCent = row.OfflineSettlementPaidAmountCent + row.WithdrawalPaidAmountCent + row.ManualPaidAmountCent item.DisbursementPaidCount = row.OfflineSettlementPaidCount + row.WithdrawalPaidCount + row.ManualPaidCount itemsByDate[date] = item } for _, row := range operatingExpenses { date := dailyDateKey(row.Date) item := itemsByDate[date] item.Date = date item.OperatingExpenseAmountCent = row.AmountCent item.OperatingExpenseCount = row.Count itemsByDate[date] = item } items := make([]FinanceDailyDTO, 0, len(itemsByDate)) for day := dayStart(query.StartDate); !day.After(query.EndDate); day = day.AddDate(0, 0, 1) { items = append(items, itemsByDate[day.Format("2006-01-02")]) } return dashboardDailyResult{ Items: items, Summary: summaryFromDailyRows(payments, settlements, estimates), Mohong: mohongSummaryFromDailyRows(payments), OperatingExpense: operatingExpenseSummaryFromDailyRows(operatingExpenses), }, nil } func (r *Repository) dailyEstimates(ctx context.Context, query DashboardQuery) ([]dailyEstimatedRow, error) { rows := make([]dailyEstimatedRow, 0) err := r.db.WithContext(ctx).Table("rental_orders USE INDEX (idx_rental_orders_dashboard_estimates)"). Select(`DATE_FORMAT(created_at, '%Y-%m-%d') AS date, COALESCE(SUM(platform_fee_cent), 0) AS estimated_income_amount_cent, COUNT(id) AS pending_settle_order_count`). Where("settlement_status NOT IN ?", settledSettlementStatuses()). Where("status NOT IN ?", nonBillableOrderStatuses()). Where("created_at >= ? AND created_at <= ?", query.StartDate, query.EndDate). Group("DATE_FORMAT(created_at, '%Y-%m-%d')"). Scan(&rows).Error return rows, err } 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 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' THEN COALESCE(ro.offline_settlement_amount_cent, 0) ELSE COALESCE(w.owner_wallet_income_amount_cent, 0) END), 0) AS owner_effective_settlement_amount_cent, COALESCE(SUM(CASE WHEN ro.settlement_mode = 'platform_managed' THEN COALESCE(ro.offline_settlement_amount_cent, 0) ELSE 0 END), 0) AS offline_settlement_amount_cent, COALESCE(SUM(CASE WHEN ro.settlement_mode = 'platform_managed' AND COALESCE(NULLIF(ro.offline_settlement_status, ''), 'none') = 'pending' THEN COALESCE(ro.offline_settlement_amount_cent, 0) ELSE 0 END), 0) AS offline_settlement_pending_amount_cent, COALESCE(SUM(CASE WHEN ro.settlement_mode = 'platform_managed' AND COALESCE(NULLIF(ro.offline_settlement_status, ''), 'none') = 'pending' AND COALESCE(ro.offline_settlement_amount_cent, 0) > 0 THEN 1 ELSE 0 END), 0) AS offline_settlement_pending_count, COALESCE(SUM(CASE WHEN ro.refund_status = 'refunding' OR ABS(COALESCE(oc.owner_income_amount_cent, 0) - CASE WHEN ro.settlement_mode = 'platform_managed' THEN COALESCE(ro.offline_settlement_amount_cent, 0) ELSE COALESCE(w.owner_wallet_income_amount_cent, 0) END) >= ? OR EXISTS ( SELECT 1 FROM payment_orders AS exception_payment WHERE exception_payment.order_id = ro.id AND exception_payment.biz_type IN ? AND exception_payment.status IN ? AND exception_payment.amount_cent > 0 ) THEN 1 ELSE 0 END), 0) AS financial_exception_count, COUNT(ro.id) AS settled_order_count`, settlementDiffThresholdCent, refundBizTypes(), []string{"refunding", "failed"}). // 统计口径:只算"正常结账确认"的订单。仲裁结案的结账单终态是 settled // (由 dispute 模块写入),不纳入本指标,此处保持既有口径不变。 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')"). Scan(&rows).Error return rows, err } func summaryFromDailyRows(payments []dailyPaymentRow, settlements []dailySettlementRow, estimates []dailyEstimatedRow) FinanceSummaryDTO { var summary FinanceSummaryDTO for _, row := range payments { summary.TotalFlowAmountCent += row.TotalFlowAmountCent summary.TotalRefundAmountCent += row.TotalRefundAmountCent summary.PendingRefundAmountCent += row.PendingRefundAmountCent summary.SuccessfulPayCount += row.SuccessfulPayCount summary.FullRefundCount += row.FullRefundCount summary.PartialRefundCount += row.PartialRefundCount summary.PendingRefundCount += row.PendingRefundCount } 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 summary.OfflineSettlementAmountCent += row.OfflineSettlementAmountCent summary.OfflineSettlementPendingAmountCent += row.OfflineSettlementPendingAmountCent summary.OfflineSettlementPendingCount += row.OfflineSettlementPendingCount summary.FinancialExceptionCount += row.FinancialExceptionCount summary.SettledOrderCount += row.SettledOrderCount } for _, row := range estimates { summary.EstimatedIncomeAmountCent += row.EstimatedIncomeAmountCent summary.PendingSettleOrderCount += row.PendingSettleOrderCount } summary.ChannelNetAmountCent = summary.TotalFlowAmountCent - summary.TotalRefundAmountCent summary.SuccessfulRefundCount = summary.FullRefundCount + summary.PartialRefundCount summary.PlatformIncomeAmountCent = summary.NormalOrderProfitAmountCent summary.SettlementDiffAmountCent = summary.OwnerShouldIncomeAmountCent - summary.OwnerEffectiveSettlementAmountCent return summary } func summaryWithPickup(summary FinanceSummaryDTO, pickup *PickupSummaryDTO) FinanceSummaryDTO { if pickup == nil { return summary } summary.PickupProfitAmountCent = pickup.ProfitAmountCent summary.PickupCompletedCount = pickup.CompletedCount summary.PlatformIncomeAmountCent = summary.NormalOrderProfitAmountCent + pickup.ProfitAmountCent summary.OfflineSettlementPendingAmountCent += pickup.PendingAmountCent summary.OfflineSettlementPendingCount += pickup.PendingCount return summary } func mohongSummaryFromDailyRows(rows []dailyPaymentRow) MohongSummaryDTO { var summary MohongSummaryDTO for _, row := range rows { summary.FlowAmountCent += row.MohongFlowAmountCent summary.RefundAmountCent += row.MohongRefundAmountCent summary.PaidOrderCount += row.MohongPaidOrderCount } summary.NetAmountCent = summary.FlowAmountCent - summary.RefundAmountCent return summary } func operatingExpenseSummaryFromDailyRows(rows []dailyOperatingExpenseRow) OperatingExpenseSummaryDTO { var summary OperatingExpenseSummaryDTO for _, row := range rows { summary.AmountCent += row.AmountCent summary.Count += row.Count } return summary } // dailyPayments 按支付单创建日统计收付款流水与退款笔数, // normal_full_refund_count 只统计正常(租赁)订单的全额退款,排除撞车商城。 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 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, COALESCE(SUM(CASE WHEN po.biz_type IN ? AND po.status = 'refunding' THEN po.amount_cent ELSE 0 END), 0) AS pending_refund_amount_cent, COALESCE(SUM(CASE WHEN po.biz_type IN ? AND po.status = 'paid' THEN 1 ELSE 0 END), 0) AS successful_pay_count, COALESCE(SUM(CASE WHEN po.biz_type IN ? AND po.status = 'refunded' AND po.amount_cent >= COALESCE(orig.amount_cent, 0) THEN 1 ELSE 0 END), 0) AS full_refund_count, COALESCE(SUM(CASE WHEN po.biz_type IN ? AND po.status = 'refunded' AND po.amount_cent < COALESCE(orig.amount_cent, 0) THEN 1 ELSE 0 END), 0) AS partial_refund_count, COALESCE(SUM(CASE WHEN po.biz_type IN ? AND po.status = 'refunding' THEN 1 ELSE 0 END), 0) AS pending_refund_count, COALESCE(SUM(CASE WHEN po.biz_type IN ? AND po.status = 'refunded' AND po.amount_cent >= COALESCE(orig.amount_cent, 0) THEN 1 ELSE 0 END), 0) AS normal_full_refund_count, COALESCE(SUM(CASE WHEN po.biz_type = 'mohong_pay' AND po.status = 'paid' THEN po.amount_cent ELSE 0 END), 0) AS mohong_flow_amount_cent, COALESCE(SUM(CASE WHEN po.biz_type = 'mohong_refund' AND po.status = 'refunded' THEN po.amount_cent ELSE 0 END), 0) AS mohong_refund_amount_cent, COALESCE(SUM(CASE WHEN po.biz_type = 'mohong_pay' AND po.status = 'paid' THEN 1 ELSE 0 END), 0) AS mohong_paid_order_count`, payBizTypes(), refundBizTypes(), refundBizTypes(), payBizTypes(), refundBizTypes(), refundBizTypes(), refundBizTypes(), normalRefundBizTypes()). Joins("LEFT JOIN (?) AS orig ON orig.order_id = po.order_id", paymentOriginalAmountForRefundsInRangeSubquery(db, query)). Where("po.created_at >= ? AND po.created_at <= ?", query.StartDate, query.EndDate). Group("DATE_FORMAT(po.created_at, '%Y-%m-%d')"). Scan(&rows).Error return rows, err } // dailyPickupCancelled 按取消日统计线下提号取消笔数。 func (r *Repository) dailyPickupCancelled(ctx context.Context, query DashboardQuery) ([]dailyPickupCancelledRow, error) { rows := make([]dailyPickupCancelledRow, 0) err := r.db.WithContext(ctx).Table("admin_pickups"). Select(`DATE_FORMAT(cancelled_at, '%Y-%m-%d') AS date, COUNT(id) AS count`). Where("status = ?", "cancelled"). Where("cancelled_at >= ? AND cancelled_at <= ?", query.StartDate, query.EndDate). Group("DATE_FORMAT(cancelled_at, '%Y-%m-%d')"). Scan(&rows).Error return rows, err } // dailyPaidOrders 按实际付款日统计普通租赁订单,并将已完成的线下提号按完成日计入成交。 func (r *Repository) dailyPaidOrders(ctx context.Context, query DashboardQuery) ([]dailyPaidOrderRow, error) { items := make([]dailyPaidOrderRow, 0) err := r.db.WithContext(ctx).Table(`( SELECT DATE_FORMAT(paid_at, '%Y-%m-%d') AS date, COUNT(DISTINCT order_id) AS paid_order_count FROM payment_orders USE INDEX (idx_payment_orders_biz_status_paid_at_order) WHERE biz_type = 'order_pay' AND status = 'paid' AND paid_at >= ? AND paid_at <= ? GROUP BY DATE_FORMAT(paid_at, '%Y-%m-%d') UNION ALL SELECT DATE_FORMAT(completed_at, '%Y-%m-%d') AS date, COUNT(id) AS paid_order_count FROM admin_pickups USE INDEX (idx_admin_pickups_dashboard_completed) WHERE status = 'completed' AND completed_at >= ? AND completed_at <= ? GROUP BY DATE_FORMAT(completed_at, '%Y-%m-%d') ) AS daily_paid_orders`, query.StartDate, query.EndDate, query.StartDate, query.EndDate). Select("date, SUM(paid_order_count) AS paid_order_count"). Group("date"). Scan(&items).Error return items, err } type dailyPaymentRow struct { Date string TotalFlowAmountCent int64 TotalRefundAmountCent int64 PendingRefundAmountCent int64 SuccessfulPayCount int64 FullRefundCount int64 PartialRefundCount int64 PendingRefundCount int64 NormalFullRefundCount int64 MohongFlowAmountCent int64 MohongRefundAmountCent int64 MohongPaidOrderCount int64 } type dailyPickupCancelledRow struct { Date string Count int64 } type dailyPaidOrderRow struct { Date string PaidOrderCount int64 } 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 OfflineSettlementAmountCent int64 OfflineSettlementPendingAmountCent int64 OfflineSettlementPendingCount int64 FinancialExceptionCount int64 SettledOrderCount int64 } type dailyPickupProfitRow struct { Date string ProfitAmountCent int64 CompletedCount int64 } // dailyPickupAdjustmentRow 调整按调整创建日进入当日利润,避免回写历史完成日报。 type dailyPickupAdjustmentRow struct { Date string ProfitAmountCent int64 } type dailyEstimatedRow struct { Date string EstimatedIncomeAmountCent int64 PendingSettleOrderCount int64 }