package adminfinance import ( "math" "time" "hfb_sys/backend/internal/timeutil" "hfb_sys/backend/pkg/money" "gorm.io/gorm" ) type Repository struct { db *gorm.DB } func NewRepository(db *gorm.DB) *Repository { return &Repository{db: db} } func (r *Repository) Dashboard(query DashboardQuery) (*DashboardDTO, error) { dailyItems, err := r.dailyItems(query) if err != nil { return nil, err } summary, err := r.summary(query) if err != nil { return nil, err } return &DashboardDTO{ Summary: *summary, DailyItems: dailyItems, GeneratedAt: timeutil.ShanghaiNow(), }, nil } func (r *Repository) Details(query DetailQuery) (*PaginatedResult, error) { db := r.financeDetailBaseQuery(query) var total int64 countDB := r.db.Table("(?) AS finance_rows", db) if err := countDB.Count(&total).Error; err != nil { return nil, err } offset := (query.Page - 1) * query.PageSize rows := make([]financeDetailRow, 0, query.PageSize) if err := db.Order("ro.id DESC").Offset(offset).Limit(query.PageSize).Scan(&rows).Error; err != nil { return nil, err } items := make([]FinanceDetailDTO, 0, len(rows)) for _, row := range rows { items = append(items, row.toDTO()) } return &PaginatedResult{Items: items, Total: total, Page: query.Page, PageSize: query.PageSize}, nil } func (r *Repository) summary(query DashboardQuery) (*FinanceSummaryDTO, error) { var payment paymentSummaryRow if err := r.db.Table("payment_orders"). Select(`COALESCE(SUM(CASE WHEN biz_type IN ('order_pay', 'wallet_recharge') AND status = 'paid' THEN amount_cent ELSE 0 END), 0) AS total_flow_amount_cent, COALESCE(SUM(CASE WHEN biz_type IN ? AND status = 'refunded' THEN amount_cent ELSE 0 END), 0) AS total_refund_amount_cent, COALESCE(SUM(CASE WHEN biz_type IN ? AND status = 'refunding' THEN amount_cent ELSE 0 END), 0) AS pending_refund_amount_cent, COALESCE(SUM(CASE WHEN biz_type IN ('order_pay', 'wallet_recharge') AND status = 'paid' THEN 1 ELSE 0 END), 0) AS successful_pay_count, COALESCE(SUM(CASE WHEN biz_type IN ? AND status = 'refunded' THEN 1 ELSE 0 END), 0) AS successful_refund_count, COALESCE(SUM(CASE WHEN biz_type IN ? AND status = 'refunding' THEN 1 ELSE 0 END), 0) AS pending_refund_count`, refundBizTypes(), refundBizTypes(), refundBizTypes(), refundBizTypes()). Where("created_at >= ? AND created_at <= ?", query.StartDate, query.EndDate). Scan(&payment).Error; err != nil { return nil, err } var settlement settlementSummaryRow if err := r.db.Table("rental_orders AS ro"). Select(`COALESCE(SUM(oc.platform_fee), 0) AS platform_income_amount, COALESCE(SUM(oc.owner_income_amount), 0) AS owner_should_income_amount, COALESCE(SUM(COALESCE(w.owner_wallet_income_amount, 0)), 0) AS owner_wallet_income_amount, COUNT(ro.id) AS settled_order_count`). Joins("JOIN order_checkouts AS oc ON oc.order_id = ro.id AND oc.status = 'accepted'"). Joins("LEFT JOIN (?) AS w ON w.order_id = ro.id", ownerWalletIncomeSubquery(r.db)). Where("ro.settled_at >= ? AND ro.settled_at <= ?", query.StartDate, query.EndDate). Scan(&settlement).Error; err != nil { return nil, err } var exceptionCount int64 if err := r.db.Table("(?) AS d", r.financeDetailBaseQuery(DetailQuery{ DateType: "settled", StartDate: query.StartDate, EndDate: query.EndDate, })). Where("finance_status <> ?", "normal"). Count(&exceptionCount).Error; err != nil { return nil, err } return &FinanceSummaryDTO{ TotalFlowAmountCent: payment.TotalFlowAmountCent, TotalRefundAmountCent: payment.TotalRefundAmountCent, PendingRefundAmountCent: payment.PendingRefundAmountCent, ChannelNetAmountCent: payment.TotalFlowAmountCent - payment.TotalRefundAmountCent, PlatformIncomeAmount: money.Round(settlement.PlatformIncomeAmount), OwnerShouldIncomeAmount: money.Round(settlement.OwnerShouldIncomeAmount), OwnerWalletIncomeAmount: money.Round(settlement.OwnerWalletIncomeAmount), SettlementDiffAmount: money.Round(settlement.OwnerShouldIncomeAmount - settlement.OwnerWalletIncomeAmount), SuccessfulPayCount: payment.SuccessfulPayCount, SuccessfulRefundCount: payment.SuccessfulRefundCount, PendingRefundCount: payment.PendingRefundCount, SettledOrderCount: settlement.SettledOrderCount, FinancialExceptionCount: exceptionCount, }, nil } func (r *Repository) dailyItems(query DashboardQuery) ([]FinanceDailyDTO, error) { payments := make([]dailyPaymentRow, 0) if err := r.db.Table("payment_orders"). Select(`DATE(created_at) AS date, COALESCE(SUM(CASE WHEN biz_type IN ('order_pay', 'wallet_recharge') AND status = 'paid' THEN amount_cent ELSE 0 END), 0) AS total_flow_amount_cent, COALESCE(SUM(CASE WHEN biz_type IN ? AND status = 'refunded' THEN amount_cent ELSE 0 END), 0) AS total_refund_amount_cent, COALESCE(SUM(CASE WHEN biz_type IN ? AND status = 'refunding' THEN amount_cent ELSE 0 END), 0) AS pending_refund_amount_cent, COALESCE(SUM(CASE WHEN biz_type IN ('order_pay', 'wallet_recharge') AND status = 'paid' THEN 1 ELSE 0 END), 0) AS successful_pay_count, COALESCE(SUM(CASE WHEN biz_type IN ? AND status = 'refunded' THEN 1 ELSE 0 END), 0) AS successful_refund_count, COALESCE(SUM(CASE WHEN biz_type IN ? AND status = 'refunding' THEN 1 ELSE 0 END), 0) AS pending_refund_count`, refundBizTypes(), refundBizTypes(), refundBizTypes(), refundBizTypes()). Where("created_at >= ? AND created_at <= ?", query.StartDate, query.EndDate). Group("DATE(created_at)"). Scan(&payments).Error; err != nil { return nil, err } settlements := make([]dailySettlementRow, 0) if err := r.db.Table("rental_orders AS ro"). Select(`DATE(ro.settled_at) AS date, COALESCE(SUM(oc.platform_fee), 0) AS platform_income_amount, COALESCE(SUM(oc.owner_income_amount), 0) AS owner_should_income_amount, COALESCE(SUM(COALESCE(w.owner_wallet_income_amount, 0)), 0) AS owner_wallet_income_amount, COUNT(ro.id) AS settled_order_count`). Joins("JOIN order_checkouts AS oc ON oc.order_id = ro.id AND oc.status = 'accepted'"). Joins("LEFT JOIN (?) AS w ON w.order_id = ro.id", ownerWalletIncomeSubquery(r.db)). Where("ro.settled_at >= ? AND ro.settled_at <= ?", query.StartDate, query.EndDate). Group("DATE(ro.settled_at)"). Scan(&settlements).Error; err != nil { return nil, 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 { item := itemsByDate[row.Date] item.Date = row.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.SuccessfulRefundCount item.PendingRefundCount = row.PendingRefundCount itemsByDate[row.Date] = item } for _, row := range settlements { item := itemsByDate[row.Date] item.Date = row.Date item.PlatformIncomeAmount = money.Round(row.PlatformIncomeAmount) item.OwnerShouldIncomeAmount = money.Round(row.OwnerShouldIncomeAmount) item.OwnerWalletIncomeAmount = money.Round(row.OwnerWalletIncomeAmount) item.SettlementDiffAmount = money.Round(row.OwnerShouldIncomeAmount - row.OwnerWalletIncomeAmount) item.SettledOrderCount = row.SettledOrderCount itemsByDate[row.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 items, nil } func (r *Repository) financeDetailBaseQuery(query DetailQuery) *gorm.DB { db := r.db.Table("rental_orders AS ro"). Select(`ro.id AS order_id, ro.order_no, ro.status AS order_status, ro.settlement_status, ro.refund_status, ro.renter_id, COALESCE(ru.phone, '') AS renter_phone, COALESCE(ru.nickname, '') AS renter_nickname, ro.owner_id, COALESCE(ou.phone, '') AS owner_phone, COALESCE(ou.nickname, '') AS owner_nickname, ro.rent_amount AS order_rent_amount, ro.deposit_amount AS order_deposit_amount, COALESCE(oc.rent_amount, 0) AS checkout_rent_amount, COALESCE(oc.renter_refund_amount, 0) AS checkout_renter_refund, COALESCE(oc.owner_income_amount, 0) AS checkout_owner_income, COALESCE(oc.platform_fee, 0) AS checkout_platform_fee, COALESCE(w.owner_wallet_income_amount, 0) AS owner_wallet_income_amount, COALESCE(p.paid_amount_cent, 0) AS paid_amount_cent, COALESCE(p.refunded_amount_cent, 0) AS refunded_amount_cent, COALESCE(p.refunding_amount_cent, 0) AS refunding_amount_cent, COALESCE(p.failed_refund_amount_cent, 0) AS failed_refund_amount_cent, COALESCE(p.paid_amount_cent, 0) - COALESCE(p.refunded_amount_cent, 0) AS channel_net_amount_cent, COALESCE(oc.platform_fee, 0) + ((COALESCE(oc.owner_income_amount, 0) - COALESCE(w.owner_wallet_income_amount, 0))) AS platform_net_amount, COALESCE(oc.owner_income_amount, 0) - COALESCE(w.owner_wallet_income_amount, 0) AS settlement_diff_amount, CASE WHEN COALESCE(p.failed_refund_amount_cent, 0) > 0 THEN 'refund_failed' WHEN COALESCE(p.refunding_amount_cent, 0) > 0 OR ro.refund_status = 'refunding' THEN 'refund_pending' WHEN ABS(COALESCE(oc.owner_income_amount, 0) - COALESCE(w.owner_wallet_income_amount, 0)) >= 0.05 THEN 'settlement_diff' ELSE 'normal' END AS finance_status, ro.created_at, ro.settled_at`). Joins("LEFT JOIN users AS ru ON ru.id = ro.renter_id"). Joins("LEFT JOIN users AS ou ON ou.id = ro.owner_id"). Joins("LEFT JOIN (?) AS oc ON oc.order_id = ro.id", acceptedCheckoutSubquery(r.db)). Joins("LEFT JOIN (?) AS p ON p.order_id = ro.id", orderPaymentSubquery(r.db)). Joins("LEFT JOIN (?) AS w ON w.order_id = ro.id", ownerWalletIncomeSubquery(r.db)) if query.OrderNo != "" { db = db.Where("ro.order_no = ?", query.OrderNo) } if query.UserID > 0 { db = db.Where("(ro.renter_id = ? OR ro.owner_id = ?)", query.UserID, query.UserID) } if query.OrderStatus != "" { db = db.Where("ro.status = ?", query.OrderStatus) } if query.SettlementStatus != "" { db = db.Where("ro.settlement_status = ?", query.SettlementStatus) } if !query.StartDate.IsZero() && !query.EndDate.IsZero() { if query.DateType == "created" { db = db.Where("ro.created_at >= ? AND ro.created_at <= ?", query.StartDate, query.EndDate) } else { db = db.Where("ro.settled_at >= ? AND ro.settled_at <= ?", query.StartDate, query.EndDate) } } return db } func acceptedCheckoutSubquery(db *gorm.DB) *gorm.DB { latest := db.Table("order_checkouts").Select("MAX(id) AS id").Where("status = ?", "accepted").Group("order_id") return db.Table("order_checkouts AS oc"). Select("oc.*"). Joins("JOIN (?) AS latest ON latest.id = oc.id", latest) } func orderPaymentSubquery(db *gorm.DB) *gorm.DB { return db.Table("payment_orders"). Select(`order_id, COALESCE(SUM(CASE WHEN biz_type = 'order_pay' AND status = 'paid' THEN amount_cent ELSE 0 END), 0) AS paid_amount_cent, COALESCE(SUM(CASE WHEN biz_type IN ? AND status = 'refunded' THEN amount_cent ELSE 0 END), 0) AS refunded_amount_cent, COALESCE(SUM(CASE WHEN biz_type IN ? AND status = 'refunding' THEN amount_cent ELSE 0 END), 0) AS refunding_amount_cent, COALESCE(SUM(CASE WHEN biz_type IN ? AND status = 'failed' THEN amount_cent ELSE 0 END), 0) AS failed_refund_amount_cent`, refundBizTypes(), refundBizTypes(), refundBizTypes()). Group("order_id") } func ownerWalletIncomeSubquery(db *gorm.DB) *gorm.DB { return db.Table("wallet_ledger"). Select("order_id, COALESCE(SUM(amount), 0) AS owner_wallet_income_amount"). Where("direction = ? AND biz_type IN ? AND order_id IS NOT NULL", "in", []string{"owner_income", "deposit_compensation"}). Group("order_id") } func refundBizTypes() []string { return []string{ "cancel_refund", "admin_close_refund", "admin_refund", "checkout_refund", "deposit_refund", "rent_refund", "arbitration_refund", } } func dayStart(value time.Time) time.Time { loc := timeutil.ShanghaiLocation() local := value.In(loc) return time.Date(local.Year(), local.Month(), local.Day(), 0, 0, 0, 0, loc) } type paymentSummaryRow struct { TotalFlowAmountCent int64 TotalRefundAmountCent int64 PendingRefundAmountCent int64 SuccessfulPayCount int64 SuccessfulRefundCount int64 PendingRefundCount int64 } type settlementSummaryRow struct { PlatformIncomeAmount float64 OwnerShouldIncomeAmount float64 OwnerWalletIncomeAmount float64 SettledOrderCount int64 } type dailyPaymentRow struct { Date string TotalFlowAmountCent int64 TotalRefundAmountCent int64 PendingRefundAmountCent int64 SuccessfulPayCount int64 SuccessfulRefundCount int64 PendingRefundCount int64 } type dailySettlementRow struct { Date string PlatformIncomeAmount float64 OwnerShouldIncomeAmount float64 OwnerWalletIncomeAmount float64 SettledOrderCount int64 } type financeDetailRow struct { OrderID uint64 OrderNo string OrderStatus string SettlementStatus string RefundStatus string RenterID uint64 RenterPhone string RenterNickname string OwnerID uint64 OwnerPhone string OwnerNickname string OrderRentAmount float64 OrderDepositAmount float64 CheckoutRentAmount float64 CheckoutRenterRefund float64 CheckoutOwnerIncome float64 CheckoutPlatformFee float64 OwnerWalletIncomeAmount float64 PaidAmountCent int64 RefundedAmountCent int64 RefundingAmountCent int64 FailedRefundAmountCent int64 ChannelNetAmountCent int64 PlatformNetAmount float64 SettlementDiffAmount float64 FinanceStatus string CreatedAt time.Time SettledAt *time.Time } func (r financeDetailRow) toDTO() FinanceDetailDTO { diff := money.Round(r.SettlementDiffAmount) status := r.FinanceStatus if status == "" { status = "normal" } if math.Abs(diff) < 0.05 && status == "settlement_diff" { status = "normal" } return FinanceDetailDTO{ OrderID: r.OrderID, OrderNo: r.OrderNo, OrderStatus: r.OrderStatus, SettlementStatus: r.SettlementStatus, RefundStatus: r.RefundStatus, RenterID: r.RenterID, RenterPhone: r.RenterPhone, RenterNickname: r.RenterNickname, OwnerID: r.OwnerID, OwnerPhone: r.OwnerPhone, OwnerNickname: r.OwnerNickname, OrderRentAmount: money.Round(r.OrderRentAmount), OrderDepositAmount: money.Round(r.OrderDepositAmount), CheckoutRentAmount: money.Round(r.CheckoutRentAmount), CheckoutRenterRefund: money.Round(r.CheckoutRenterRefund), CheckoutOwnerIncome: money.Round(r.CheckoutOwnerIncome), CheckoutPlatformFee: money.Round(r.CheckoutPlatformFee), OwnerWalletIncomeAmount: money.Round(r.OwnerWalletIncomeAmount), PaidAmountCent: r.PaidAmountCent, RefundedAmountCent: r.RefundedAmountCent, RefundingAmountCent: r.RefundingAmountCent, FailedRefundAmountCent: r.FailedRefundAmountCent, ChannelNetAmountCent: r.ChannelNetAmountCent, PlatformNetAmount: money.Round(r.PlatformNetAmount), SettlementDiffAmount: diff, FinanceStatus: status, CreatedAt: r.CreatedAt, SettledAt: r.SettledAt, } }