package adminfinance import ( "time" "hfb_sys/backend/internal/timeutil" "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_cent), 0) AS platform_income_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, 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, PlatformIncomeAmountCent: settlement.PlatformIncomeAmountCent, OwnerShouldIncomeAmountCent: settlement.OwnerShouldIncomeAmountCent, OwnerWalletIncomeAmountCent: settlement.OwnerWalletIncomeAmountCent, SettlementDiffAmountCent: settlement.OwnerShouldIncomeAmountCent - settlement.OwnerWalletIncomeAmountCent, 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_cent), 0) AS platform_income_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, 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.PlatformIncomeAmountCent = row.PlatformIncomeAmountCent item.OwnerShouldIncomeAmountCent = row.OwnerShouldIncomeAmountCent item.OwnerWalletIncomeAmountCent = row.OwnerWalletIncomeAmountCent item.SettlementDiffAmountCent = row.OwnerShouldIncomeAmountCent - row.OwnerWalletIncomeAmountCent 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_cent AS order_rent_amount_cent, ro.deposit_amount_cent AS order_deposit_amount_cent, COALESCE(oc.rent_amount_cent, 0) AS checkout_rent_amount_cent, COALESCE(oc.renter_refund_amount_cent, 0) AS checkout_renter_refund_cent, COALESCE(oc.owner_income_amount_cent, 0) AS checkout_owner_income_cent, COALESCE(oc.platform_fee_cent, 0) AS checkout_platform_fee_cent, COALESCE(w.owner_wallet_income_amount_cent, 0) AS owner_wallet_income_amount_cent, 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_cent, 0) + ((COALESCE(oc.owner_income_amount_cent, 0) - COALESCE(w.owner_wallet_income_amount_cent, 0))) AS platform_net_amount_cent, COALESCE(oc.owner_income_amount_cent, 0) - COALESCE(w.owner_wallet_income_amount_cent, 0) AS settlement_diff_amount_cent, 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_cent, 0) - COALESCE(w.owner_wallet_income_amount_cent, 0)) >= 5 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_cent), 0) AS owner_wallet_income_amount_cent"). 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 { PlatformIncomeAmountCent int64 OwnerShouldIncomeAmountCent int64 OwnerWalletIncomeAmountCent int64 SettledOrderCount int64 } type dailyPaymentRow struct { Date string TotalFlowAmountCent int64 TotalRefundAmountCent int64 PendingRefundAmountCent int64 SuccessfulPayCount int64 SuccessfulRefundCount int64 PendingRefundCount int64 } type dailySettlementRow struct { Date string PlatformIncomeAmountCent int64 OwnerShouldIncomeAmountCent int64 OwnerWalletIncomeAmountCent int64 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 OrderRentAmountCent int64 OrderDepositAmountCent int64 CheckoutRentAmountCent int64 CheckoutRenterRefundCent int64 CheckoutOwnerIncomeCent int64 CheckoutPlatformFeeCent int64 OwnerWalletIncomeAmountCent int64 PaidAmountCent int64 RefundedAmountCent int64 RefundingAmountCent int64 FailedRefundAmountCent int64 ChannelNetAmountCent int64 PlatformNetAmountCent int64 SettlementDiffAmountCent int64 FinanceStatus string CreatedAt time.Time SettledAt *time.Time } func (r financeDetailRow) toDTO() FinanceDetailDTO { diff := r.SettlementDiffAmountCent status := r.FinanceStatus if status == "" { status = "normal" } if absCent(diff) < 5 && 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, OrderRentAmountCent: r.OrderRentAmountCent, OrderDepositAmountCent: r.OrderDepositAmountCent, CheckoutRentAmountCent: r.CheckoutRentAmountCent, CheckoutRenterRefundCent: r.CheckoutRenterRefundCent, CheckoutOwnerIncomeCent: r.CheckoutOwnerIncomeCent, CheckoutPlatformFeeCent: r.CheckoutPlatformFeeCent, OwnerWalletIncomeAmountCent: r.OwnerWalletIncomeAmountCent, PaidAmountCent: r.PaidAmountCent, RefundedAmountCent: r.RefundedAmountCent, RefundingAmountCent: r.RefundingAmountCent, FailedRefundAmountCent: r.FailedRefundAmountCent, ChannelNetAmountCent: r.ChannelNetAmountCent, PlatformNetAmountCent: r.PlatformNetAmountCent, SettlementDiffAmountCent: diff, FinanceStatus: status, CreatedAt: r.CreatedAt, SettledAt: r.SettledAt, } } func absCent(value int64) int64 { if value < 0 { return -value } return value }