package adminfinance import ( "context" "encoding/json" "strings" "time" "gorm.io/gorm" ) func (r *Repository) Disbursements(ctx context.Context, query DisbursementQuery) (*DisbursementListDTO, error) { var summary DisbursementListSummaryDTO summaryDB := r.applyDisbursementFilters(r.disbursementListBaseQuery(ctx), query) if err := summaryDB.Select(`COUNT(*) AS record_count, COALESCE(SUM(CASE WHEN status = 'paid' THEN actual_amount_cent ELSE 0 END), 0) AS paid_amount_cent, COALESCE(SUM(CASE WHEN status = 'paid' THEN 1 ELSE 0 END), 0) AS paid_count, COALESCE(SUM(CASE WHEN status = 'payment_pending' THEN actual_amount_cent ELSE 0 END), 0) AS payment_pending_amount_cent, COALESCE(SUM(CASE WHEN status = 'payment_pending' THEN 1 ELSE 0 END), 0) AS payment_pending_count, COALESCE(SUM(CASE WHEN status = 'review_pending' THEN actual_amount_cent ELSE 0 END), 0) AS review_pending_amount_cent, COALESCE(SUM(CASE WHEN status = 'review_pending' THEN 1 ELSE 0 END), 0) AS review_pending_count, COALESCE(SUM(CASE WHEN source_type = 'withdrawal' AND status = 'paid' THEN fee_cent ELSE 0 END), 0) AS withdrawal_fee_amount_cent`). Scan(&summary).Error; err != nil { return nil, err } rows := make([]disbursementItemRow, 0, query.PageSize) orderColumn := "business_created_at" if query.DateType == "paid" { orderColumn = "paid_at" } offset := (query.Page - 1) * query.PageSize rowsDB := r.applyDisbursementFilters(r.disbursementListBaseQuery(ctx), query) if err := rowsDB.Order(orderColumn + " DESC"). Order("source_id DESC"). Offset(offset). Limit(query.PageSize). Scan(&rows).Error; err != nil { return nil, err } items := make([]DisbursementItemDTO, 0, len(rows)) for _, row := range rows { items = append(items, row.toDTO()) } return &DisbursementListDTO{ Items: items, Total: summary.RecordCount, Page: query.Page, PageSize: query.PageSize, Summary: summary, }, nil } func (r *Repository) disbursementListBaseQuery(ctx context.Context) *gorm.DB { db := r.db.WithContext(ctx) platformManaged := db.Table("rental_orders AS ro"). Select(`'platform_managed' AS source_type, ro.id AS source_id, ro.order_no AS business_no, ro.id AS order_id, 0 AS withdrawal_id, ro.owner_id AS user_id, '外部卖家' AS payee_name, '' AS payee_phone, COALESCE(lu.uploader_name, '') AS uploader_name, COALESCE(lu.source_channel, '') AS source_channel, '' AS account_type, '' AS account_name, '' AS account_no, '' AS bank_name, ro.offline_settlement_amount_cent AS amount_cent, 0 AS fee_cent, ro.offline_settlement_amount_cent AS actual_amount_cent, CASE WHEN ro.offline_settlement_status = 'settled' THEN 'paid' ELSE 'payment_pending' END AS status, ro.offline_settlement_status AS raw_status, ro.settled_at AS business_created_at, ro.offline_settled_at AS paid_at, ro.offline_settled_by AS operator_id, COALESCE(NULLIF(operator.nickname, ''), operator.username, '') AS operator_name, ro.offline_settlement_remark AS remark, '' AS category, '' AS voucher_url, NULL AS voided_at, NULL AS voided_by, '' AS voided_by_name, '' AS void_reason, lu.parsed_payload AS source_payload`). Joins(`LEFT JOIN listing_uploads AS lu ON lu.id = ( SELECT MAX(lu2.id) FROM listing_uploads AS lu2 WHERE lu2.listing_id = ro.listing_id )`). Joins("LEFT JOIN admin_users AS operator ON operator.id = ro.offline_settled_by"). Where("ro.settlement_mode = ?", "platform_managed"). Where("ro.offline_settlement_status IN ?", []string{"pending", "settled"}). Where("ro.offline_settlement_amount_cent > 0") withdrawal := db.Table("withdrawal_requests AS wr"). Select(`'withdrawal' AS source_type, wr.id AS source_id, wr.withdraw_no AS business_no, 0 AS order_id, wr.id AS withdrawal_id, wr.user_id, COALESCE(NULLIF(u.nickname, ''), NULLIF(wr.account_name, ''), '') AS payee_name, COALESCE(u.phone, '') AS payee_phone, '' AS uploader_name, '' AS source_channel, wr.account_type, wr.account_name, wr.account_no, wr.bank_name, wr.amount_cent, wr.fee_cent, wr.actual_amount_cent, CASE WHEN wr.status = 'pending' THEN 'review_pending' WHEN wr.status = 'processing' THEN 'payment_pending' WHEN wr.status = 'completed' THEN 'paid' ELSE wr.status END AS status, wr.status AS raw_status, wr.created_at AS business_created_at, wr.paid_at, COALESCE(wr.paid_by, wr.reviewed_by) AS operator_id, COALESCE(NULLIF(operator.nickname, ''), operator.username, '') AS operator_name, CASE WHEN wr.payment_remark <> '' THEN wr.payment_remark ELSE wr.review_remark END AS remark, '' AS category, '' AS voucher_url, NULL AS voided_at, NULL AS voided_by, '' AS voided_by_name, '' AS void_reason, NULL AS source_payload`). Joins("LEFT JOIN users AS u ON u.id = wr.user_id"). Joins("LEFT JOIN admin_users AS operator ON operator.id = COALESCE(wr.paid_by, wr.reviewed_by)") manualOffline := db.Table("manual_disbursements AS md"). Select(`'manual_offline' AS source_type, md.id AS source_id, md.disbursement_no AS business_no, 0 AS order_id, 0 AS withdrawal_id, 0 AS user_id, md.payee_name, '' AS payee_phone, '' AS uploader_name, '' AS source_channel, '' AS account_type, '' AS account_name, '' AS account_no, '' AS bank_name, md.amount_cent, 0 AS fee_cent, md.amount_cent AS actual_amount_cent, md.status, md.status AS raw_status, md.created_at AS business_created_at, md.paid_at, md.created_by AS operator_id, COALESCE(NULLIF(creator.nickname, ''), creator.username, '') AS operator_name, md.remark, md.category, md.voucher_url, md.voided_at, md.voided_by, COALESCE(NULLIF(voider.nickname, ''), voider.username, '') AS voided_by_name, md.void_reason, NULL AS source_payload`). Joins("LEFT JOIN admin_users AS creator ON creator.id = md.created_by"). Joins("LEFT JOIN admin_users AS voider ON voider.id = md.voided_by") union := db.Raw("? UNION ALL ? UNION ALL ?", platformManaged, withdrawal, manualOffline) return db.Table("(?) AS d", union) } func (r *Repository) applyDisbursementFilters(db *gorm.DB, query DisbursementQuery) *gorm.DB { if query.SourceType != "" { db = db.Where("d.source_type = ?", query.SourceType) } if query.Status != "" { db = db.Where("d.status = ?", query.Status) } if query.BusinessNo != "" { db = db.Where("d.business_no LIKE ?", "%"+query.BusinessNo+"%") } if query.UserID > 0 { db = db.Where("d.user_id = ?", query.UserID) } if query.Keyword != "" { like := "%" + query.Keyword + "%" db = db.Where(`(d.business_no LIKE ? OR d.payee_name LIKE ? OR d.payee_phone LIKE ? OR d.uploader_name LIKE ? OR d.account_name LIKE ? OR d.account_no LIKE ?)`, like, like, like, like, like, like) } if !query.StartDate.IsZero() && !query.EndDate.IsZero() { if query.DateType == "paid" { db = db.Where("d.paid_at >= ? AND d.paid_at <= ?", query.StartDate, query.EndDate) } else { db = db.Where("d.business_created_at >= ? AND d.business_created_at <= ?", query.StartDate, query.EndDate) } } return db } type disbursementItemRow struct { SourceType string SourceID uint64 BusinessNo string OrderID uint64 WithdrawalID uint64 UserID uint64 PayeeName string PayeePhone string UploaderName string SourceChannel string AccountType string AccountName string AccountNo string BankName string AmountCent int64 FeeCent int64 ActualAmountCent int64 Status string RawStatus string BusinessCreatedAt *time.Time PaidAt *time.Time OperatorID *uint64 OperatorName string Remark string Category string VoucherURL string VoidedAt *time.Time VoidedBy *uint64 VoidedByName string VoidReason string SourcePayload []byte } func (r disbursementItemRow) toDTO() DisbursementItemDTO { phone := strings.TrimSpace(r.PayeePhone) if phone == "" && r.SourceType == "platform_managed" && len(r.SourcePayload) > 0 { var payload struct { ContactPhone string `json:"contactPhone"` } if json.Unmarshal(r.SourcePayload, &payload) == nil { phone = strings.TrimSpace(payload.ContactPhone) } } return DisbursementItemDTO{ SourceType: r.SourceType, SourceID: r.SourceID, BusinessNo: r.BusinessNo, OrderID: r.OrderID, WithdrawalID: r.WithdrawalID, UserID: r.UserID, PayeeName: r.PayeeName, PayeePhone: phone, UploaderName: r.UploaderName, SourceChannel: r.SourceChannel, AccountType: r.AccountType, AccountName: r.AccountName, AccountNo: r.AccountNo, BankName: r.BankName, AmountCent: r.AmountCent, FeeCent: r.FeeCent, ActualAmountCent: r.ActualAmountCent, Status: r.Status, RawStatus: r.RawStatus, BusinessCreatedAt: r.BusinessCreatedAt, PaidAt: r.PaidAt, OperatorID: r.OperatorID, OperatorName: r.OperatorName, Remark: r.Remark, Category: r.Category, VoucherURL: r.VoucherURL, VoidedAt: r.VoidedAt, VoidedBy: r.VoidedBy, VoidedByName: r.VoidedByName, VoidReason: r.VoidReason, } }