Files
hfb_sys/backend/internal/modules/adminfinance/disbursement_list.go
T

281 lines
9.3 KiB
Go

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,
}
}