222 lines
7.6 KiB
Go
222 lines
7.6 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,
|
|
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,
|
|
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)")
|
|
|
|
union := db.Raw("? UNION ALL ?", platformManaged, withdrawal)
|
|
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
|
|
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,
|
|
}
|
|
}
|