统一财务统计与线下提号出款口径
This commit is contained in:
@@ -7,7 +7,7 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
// AdminPickup 管理员线下提号记录。
|
// AdminPickup 管理员线下提号记录。
|
||||||
// 独立于 rental_orders,不进入正常订单状态机与财务统计口径。
|
// 独立于 rental_orders 和正常订单状态机;完成后进入线下提号财务统计。
|
||||||
// 站内上传完成时入卖家钱包;平台代管完成后进入线下结算。
|
// 站内上传完成时入卖家钱包;平台代管完成后进入线下结算。
|
||||||
type AdminPickup struct {
|
type AdminPickup struct {
|
||||||
ID uint64 `gorm:"primaryKey" json:"id"`
|
ID uint64 `gorm:"primaryKey" json:"id"`
|
||||||
|
|||||||
@@ -125,11 +125,33 @@ func (r *Repository) pickupSummary(ctx context.Context, query DashboardQuery) (*
|
|||||||
if err := db.Table("admin_pickups").Where("status = ?", "picking_up").Count(&inProgress).Error; err != nil {
|
if err := db.Table("admin_pickups").Where("status = ?", "picking_up").Count(&inProgress).Error; err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
var pending struct {
|
||||||
|
AmountCent int64
|
||||||
|
Count int64
|
||||||
|
}
|
||||||
|
if err := db.Table("admin_pickups").
|
||||||
|
Select(`COALESCE(SUM(settle_amount_cent), 0) AS amount_cent, COUNT(id) AS count`).
|
||||||
|
Where("settlement_mode = ? AND offline_settlement_status = ? AND settle_amount_cent > 0", "platform_managed", "pending").
|
||||||
|
Scan(&pending).Error; err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
var pendingAdjustments struct {
|
||||||
|
AmountCent int64
|
||||||
|
Count int64
|
||||||
|
}
|
||||||
|
if err := db.Table("admin_pickup_financial_adjustments").
|
||||||
|
Select(`COALESCE(SUM(settle_delta_cent), 0) AS amount_cent, COUNT(id) AS count`).
|
||||||
|
Where("settlement_mode = ? AND status = ? AND settle_delta_cent > 0", "platform_managed", "pending").
|
||||||
|
Scan(&pendingAdjustments).Error; err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
return &PickupSummaryDTO{
|
return &PickupSummaryDTO{
|
||||||
SettledAmountCent: row.SettledAmountCent + adjustments.SettledAmountCent,
|
SettledAmountCent: row.SettledAmountCent + adjustments.SettledAmountCent,
|
||||||
ProfitAmountCent: row.ProfitAmountCent + adjustments.ProfitAmountCent,
|
ProfitAmountCent: row.ProfitAmountCent + adjustments.ProfitAmountCent,
|
||||||
CompletedCount: row.CompletedCount,
|
CompletedCount: row.CompletedCount,
|
||||||
InProgressCount: inProgress,
|
InProgressCount: inProgress,
|
||||||
|
PendingAmountCent: pending.AmountCent + pendingAdjustments.AmountCent,
|
||||||
|
PendingCount: pending.Count + pendingAdjustments.Count,
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -250,7 +272,7 @@ func (r *Repository) summary(ctx context.Context, query DashboardQuery, pickup *
|
|||||||
OwnerWalletIncomeAmountCent: settlement.OwnerWalletIncomeAmountCent,
|
OwnerWalletIncomeAmountCent: settlement.OwnerWalletIncomeAmountCent,
|
||||||
OwnerEffectiveSettlementAmountCent: settlement.OwnerEffectiveSettlementAmountCent,
|
OwnerEffectiveSettlementAmountCent: settlement.OwnerEffectiveSettlementAmountCent,
|
||||||
OfflineSettlementAmountCent: settlement.OfflineSettlementAmountCent,
|
OfflineSettlementAmountCent: settlement.OfflineSettlementAmountCent,
|
||||||
OfflineSettlementPendingAmountCent: settlement.OfflineSettlementPendingAmountCent,
|
OfflineSettlementPendingAmountCent: settlement.OfflineSettlementPendingAmountCent + pickup.PendingAmountCent,
|
||||||
SettlementDiffAmountCent: settlement.OwnerShouldIncomeAmountCent - settlement.OwnerEffectiveSettlementAmountCent,
|
SettlementDiffAmountCent: settlement.OwnerShouldIncomeAmountCent - settlement.OwnerEffectiveSettlementAmountCent,
|
||||||
EstimatedIncomeAmountCent: estimated.EstimatedIncomeAmountCent,
|
EstimatedIncomeAmountCent: estimated.EstimatedIncomeAmountCent,
|
||||||
SuccessfulPayCount: payment.SuccessfulPayCount,
|
SuccessfulPayCount: payment.SuccessfulPayCount,
|
||||||
@@ -260,7 +282,7 @@ func (r *Repository) summary(ctx context.Context, query DashboardQuery, pickup *
|
|||||||
PendingRefundCount: payment.PendingRefundCount,
|
PendingRefundCount: payment.PendingRefundCount,
|
||||||
SettledOrderCount: settlement.SettledOrderCount,
|
SettledOrderCount: settlement.SettledOrderCount,
|
||||||
PendingSettleOrderCount: estimated.PendingSettleOrderCount,
|
PendingSettleOrderCount: estimated.PendingSettleOrderCount,
|
||||||
OfflineSettlementPendingCount: settlement.OfflineSettlementPendingCount,
|
OfflineSettlementPendingCount: settlement.OfflineSettlementPendingCount + pickup.PendingCount,
|
||||||
FinancialExceptionCount: settlement.FinancialExceptionCount,
|
FinancialExceptionCount: settlement.FinancialExceptionCount,
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
@@ -499,7 +521,7 @@ func (r *Repository) dailyPickupCancelled(ctx context.Context, query DashboardQu
|
|||||||
return rows, err
|
return rows, err
|
||||||
}
|
}
|
||||||
|
|
||||||
// dailyPaidOrders 按实际付款日统计普通租赁订单,并将创建的线下提号视为已付款成交。
|
// dailyPaidOrders 按实际付款日统计普通租赁订单,并将已完成的线下提号按完成日计入成交。
|
||||||
func (r *Repository) dailyPaidOrders(ctx context.Context, query DashboardQuery) ([]dailyPaidOrderRow, error) {
|
func (r *Repository) dailyPaidOrders(ctx context.Context, query DashboardQuery) ([]dailyPaidOrderRow, error) {
|
||||||
items := make([]dailyPaidOrderRow, 0)
|
items := make([]dailyPaidOrderRow, 0)
|
||||||
err := r.db.WithContext(ctx).Table(`(
|
err := r.db.WithContext(ctx).Table(`(
|
||||||
@@ -508,10 +530,10 @@ func (r *Repository) dailyPaidOrders(ctx context.Context, query DashboardQuery)
|
|||||||
WHERE biz_type = 'order_pay' AND status = 'paid' AND paid_at >= ? AND paid_at <= ?
|
WHERE biz_type = 'order_pay' AND status = 'paid' AND paid_at >= ? AND paid_at <= ?
|
||||||
GROUP BY DATE(paid_at)
|
GROUP BY DATE(paid_at)
|
||||||
UNION ALL
|
UNION ALL
|
||||||
SELECT DATE(created_at) AS date, COUNT(id) AS paid_order_count
|
SELECT DATE(completed_at) AS date, COUNT(id) AS paid_order_count
|
||||||
FROM admin_pickups
|
FROM admin_pickups
|
||||||
WHERE created_at >= ? AND created_at <= ?
|
WHERE status = 'completed' AND completed_at >= ? AND completed_at <= ?
|
||||||
GROUP BY DATE(created_at)
|
GROUP BY DATE(completed_at)
|
||||||
) AS daily_paid_orders`, query.StartDate, query.EndDate, query.StartDate, query.EndDate).
|
) AS daily_paid_orders`, query.StartDate, query.EndDate, query.StartDate, query.EndDate).
|
||||||
Select("date, SUM(paid_order_count) AS paid_order_count").
|
Select("date, SUM(paid_order_count) AS paid_order_count").
|
||||||
Group("date").
|
Group("date").
|
||||||
|
|||||||
@@ -27,7 +27,9 @@ func TestDailyPaidOrdersUsePaidAtAndIncludeCreatedPickups(t *testing.T) {
|
|||||||
}
|
}
|
||||||
if err := db.Exec(`CREATE TABLE admin_pickups (
|
if err := db.Exec(`CREATE TABLE admin_pickups (
|
||||||
id INTEGER PRIMARY KEY,
|
id INTEGER PRIMARY KEY,
|
||||||
created_at DATETIME NOT NULL
|
status TEXT NOT NULL,
|
||||||
|
created_at DATETIME NOT NULL,
|
||||||
|
completed_at DATETIME NULL
|
||||||
)`).Error; err != nil {
|
)`).Error; err != nil {
|
||||||
t.Fatalf("创建提号表失败: %v", err)
|
t.Fatalf("创建提号表失败: %v", err)
|
||||||
}
|
}
|
||||||
@@ -49,12 +51,12 @@ func TestDailyPaidOrdersUsePaidAtAndIncludeCreatedPickups(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
for _, row := range [][]any{
|
for _, row := range [][]any{
|
||||||
{1, day(2)},
|
{1, "completed", day(2), day(2)},
|
||||||
{2, day(2)},
|
{2, "picking_up", day(2), nil},
|
||||||
{3, day(3)},
|
{3, "completed", day(3), day(3)},
|
||||||
{4, day(1)},
|
{4, "cancelled", day(1), nil},
|
||||||
} {
|
} {
|
||||||
if err := db.Exec(`INSERT INTO admin_pickups (id, created_at) VALUES (?, ?)`, row...).Error; err != nil {
|
if err := db.Exec(`INSERT INTO admin_pickups (id, status, created_at, completed_at) VALUES (?, ?, ?, ?)`, row...).Error; err != nil {
|
||||||
t.Fatalf("写入提号记录失败: %v", err)
|
t.Fatalf("写入提号记录失败: %v", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -75,8 +77,8 @@ func TestDailyPaidOrdersUsePaidAtAndIncludeCreatedPickups(t *testing.T) {
|
|||||||
for _, row := range result {
|
for _, row := range result {
|
||||||
counts[row.Date] = row.PaidOrderCount
|
counts[row.Date] = row.PaidOrderCount
|
||||||
}
|
}
|
||||||
if counts["2026-08-02"] != 3 || counts["2026-08-03"] != 2 {
|
if counts["2026-08-02"] != 2 || counts["2026-08-03"] != 2 {
|
||||||
t.Fatalf("每日已付款订单 = %#v, want 8月2日为3、8月3日为2", counts)
|
t.Fatalf("每日已付款订单 = %#v, want 8月2日为2、8月3日为2", counts)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -194,7 +196,9 @@ func TestPickupSummaryKeepsOriginalAndAdjustmentOnTheirOwnDates(t *testing.T) {
|
|||||||
status TEXT NOT NULL,
|
status TEXT NOT NULL,
|
||||||
settle_amount_cent INTEGER NOT NULL,
|
settle_amount_cent INTEGER NOT NULL,
|
||||||
profit_amount_cent INTEGER NOT NULL,
|
profit_amount_cent INTEGER NOT NULL,
|
||||||
completed_at DATETIME NULL
|
completed_at DATETIME NULL,
|
||||||
|
settlement_mode TEXT NOT NULL DEFAULT 'owner_wallet',
|
||||||
|
offline_settlement_status TEXT NOT NULL DEFAULT 'none'
|
||||||
)`).Error; err != nil {
|
)`).Error; err != nil {
|
||||||
t.Fatalf("创建提号表失败: %v", err)
|
t.Fatalf("创建提号表失败: %v", err)
|
||||||
}
|
}
|
||||||
@@ -203,7 +207,9 @@ func TestPickupSummaryKeepsOriginalAndAdjustmentOnTheirOwnDates(t *testing.T) {
|
|||||||
pickup_id INTEGER NOT NULL,
|
pickup_id INTEGER NOT NULL,
|
||||||
settle_delta_cent INTEGER NOT NULL,
|
settle_delta_cent INTEGER NOT NULL,
|
||||||
profit_delta_cent INTEGER NOT NULL,
|
profit_delta_cent INTEGER NOT NULL,
|
||||||
created_at DATETIME NOT NULL
|
created_at DATETIME NOT NULL,
|
||||||
|
settlement_mode TEXT NOT NULL DEFAULT 'owner_wallet',
|
||||||
|
status TEXT NOT NULL DEFAULT 'settled'
|
||||||
)`).Error; err != nil {
|
)`).Error; err != nil {
|
||||||
t.Fatalf("创建提号调整表失败: %v", err)
|
t.Fatalf("创建提号调整表失败: %v", err)
|
||||||
}
|
}
|
||||||
@@ -267,11 +273,13 @@ func TestSummaryReturnsNormalOrderCountAndScopedExceptionCount(t *testing.T) {
|
|||||||
)`,
|
)`,
|
||||||
`CREATE TABLE admin_pickups (
|
`CREATE TABLE admin_pickups (
|
||||||
id INTEGER PRIMARY KEY, status TEXT NOT NULL, settle_amount_cent INTEGER NOT NULL,
|
id INTEGER PRIMARY KEY, status TEXT NOT NULL, settle_amount_cent INTEGER NOT NULL,
|
||||||
profit_amount_cent INTEGER NOT NULL, completed_at DATETIME NULL
|
profit_amount_cent INTEGER NOT NULL, completed_at DATETIME NULL,
|
||||||
|
settlement_mode TEXT NOT NULL DEFAULT 'owner_wallet', offline_settlement_status TEXT NOT NULL DEFAULT 'none'
|
||||||
)`,
|
)`,
|
||||||
`CREATE TABLE admin_pickup_financial_adjustments (
|
`CREATE TABLE admin_pickup_financial_adjustments (
|
||||||
id INTEGER PRIMARY KEY, pickup_id INTEGER NOT NULL, settle_delta_cent INTEGER NOT NULL,
|
id INTEGER PRIMARY KEY, pickup_id INTEGER NOT NULL, settle_delta_cent INTEGER NOT NULL,
|
||||||
profit_delta_cent INTEGER NOT NULL, created_at DATETIME NOT NULL
|
profit_delta_cent INTEGER NOT NULL, created_at DATETIME NOT NULL,
|
||||||
|
settlement_mode TEXT NOT NULL DEFAULT 'owner_wallet', status TEXT NOT NULL DEFAULT 'settled'
|
||||||
)`,
|
)`,
|
||||||
} {
|
} {
|
||||||
if err := db.Exec(statement).Error; err != nil {
|
if err := db.Exec(statement).Error; err != nil {
|
||||||
|
|||||||
@@ -143,8 +143,8 @@ func (r *Repository) disbursementSummary(ctx context.Context, query DashboardQue
|
|||||||
PendingPaymentCount: offlinePending.Count + pickupPending.Count + pickupAdjustmentPending.Count + withdrawal.PendingCount,
|
PendingPaymentCount: offlinePending.Count + pickupPending.Count + pickupAdjustmentPending.Count + withdrawal.PendingCount,
|
||||||
OfflineSettlementPaidAmountCent: offlinePaid.AmountCent + pickupPaid.AmountCent + pickupAdjustmentPaid.AmountCent,
|
OfflineSettlementPaidAmountCent: offlinePaid.AmountCent + pickupPaid.AmountCent + pickupAdjustmentPaid.AmountCent,
|
||||||
OfflineSettlementPaidCount: offlinePaid.Count + pickupPaid.Count + pickupAdjustmentPaid.Count,
|
OfflineSettlementPaidCount: offlinePaid.Count + pickupPaid.Count + pickupAdjustmentPaid.Count,
|
||||||
OfflineSettlementPendingAmountCent: offlinePending.AmountCent,
|
OfflineSettlementPendingAmountCent: offlinePending.AmountCent + pickupPending.AmountCent + pickupAdjustmentPending.AmountCent,
|
||||||
OfflineSettlementPendingCount: offlinePending.Count,
|
OfflineSettlementPendingCount: offlinePending.Count + pickupPending.Count + pickupAdjustmentPending.Count,
|
||||||
WithdrawalAmountCent: withdrawal.PaidAmountCent,
|
WithdrawalAmountCent: withdrawal.PaidAmountCent,
|
||||||
WithdrawalFeeAmountCent: withdrawal.PaidFeeAmountCent,
|
WithdrawalFeeAmountCent: withdrawal.PaidFeeAmountCent,
|
||||||
WithdrawalPaidAmountCent: withdrawal.PaidActualAmountCent,
|
WithdrawalPaidAmountCent: withdrawal.PaidActualAmountCent,
|
||||||
|
|||||||
@@ -95,6 +95,83 @@ func (r *Repository) disbursementListBaseQuery(ctx context.Context) *gorm.DB {
|
|||||||
Where("ro.offline_settlement_status IN ?", []string{"pending", "settled"}).
|
Where("ro.offline_settlement_status IN ?", []string{"pending", "settled"}).
|
||||||
Where("ro.offline_settlement_amount_cent > 0")
|
Where("ro.offline_settlement_amount_cent > 0")
|
||||||
|
|
||||||
|
pickupManaged := db.Table("admin_pickups AS ap").
|
||||||
|
Select(`'pickup' AS source_type,
|
||||||
|
ap.id AS source_id,
|
||||||
|
ap.pickup_no AS business_no,
|
||||||
|
0 AS order_id,
|
||||||
|
0 AS withdrawal_id,
|
||||||
|
ap.owner_id AS user_id,
|
||||||
|
'外部卖家' AS payee_name,
|
||||||
|
'' AS payee_phone,
|
||||||
|
'' AS uploader_name,
|
||||||
|
ap.source_channel,
|
||||||
|
'' AS account_type,
|
||||||
|
'' AS account_name,
|
||||||
|
'' AS account_no,
|
||||||
|
'' AS bank_name,
|
||||||
|
ap.settle_amount_cent AS amount_cent,
|
||||||
|
0 AS fee_cent,
|
||||||
|
ap.settle_amount_cent AS actual_amount_cent,
|
||||||
|
CASE WHEN ap.offline_settlement_status = 'settled' THEN 'paid' ELSE 'payment_pending' END AS status,
|
||||||
|
ap.offline_settlement_status AS raw_status,
|
||||||
|
ap.completed_at AS business_created_at,
|
||||||
|
ap.offline_settled_at AS paid_at,
|
||||||
|
ap.offline_settled_by AS operator_id,
|
||||||
|
COALESCE(NULLIF(operator.nickname, ''), operator.username, '') AS operator_name,
|
||||||
|
ap.offline_settlement_remark AS remark,
|
||||||
|
'' AS category,
|
||||||
|
'' AS custom_category_name,
|
||||||
|
'' AS voucher_url,
|
||||||
|
NULL AS voided_at,
|
||||||
|
NULL AS voided_by,
|
||||||
|
'' AS voided_by_name,
|
||||||
|
'' AS void_reason,
|
||||||
|
ap.account_snapshot AS source_payload`).
|
||||||
|
Joins("LEFT JOIN admin_users AS operator ON operator.id = ap.offline_settled_by").
|
||||||
|
Where("ap.settlement_mode = ?", "platform_managed").
|
||||||
|
Where("ap.offline_settlement_status IN ?", []string{"pending", "settled"}).
|
||||||
|
Where("ap.settle_amount_cent > 0")
|
||||||
|
|
||||||
|
pickupAdjustments := db.Table("admin_pickup_financial_adjustments AS fa").
|
||||||
|
Select(`'pickup_adjustment' AS source_type,
|
||||||
|
fa.id AS source_id,
|
||||||
|
CAST(fa.id AS CHAR) AS business_no,
|
||||||
|
0 AS order_id,
|
||||||
|
0 AS withdrawal_id,
|
||||||
|
ap.owner_id AS user_id,
|
||||||
|
'外部卖家' AS payee_name,
|
||||||
|
'' AS payee_phone,
|
||||||
|
'' AS uploader_name,
|
||||||
|
ap.source_channel,
|
||||||
|
'' AS account_type,
|
||||||
|
'' AS account_name,
|
||||||
|
'' AS account_no,
|
||||||
|
'' AS bank_name,
|
||||||
|
fa.settle_delta_cent AS amount_cent,
|
||||||
|
0 AS fee_cent,
|
||||||
|
fa.settle_delta_cent AS actual_amount_cent,
|
||||||
|
CASE WHEN fa.status = 'settled' THEN 'paid' ELSE 'payment_pending' END AS status,
|
||||||
|
fa.status AS raw_status,
|
||||||
|
fa.created_at AS business_created_at,
|
||||||
|
fa.settled_at AS paid_at,
|
||||||
|
fa.settled_by AS operator_id,
|
||||||
|
COALESCE(NULLIF(operator.nickname, ''), operator.username, '') AS operator_name,
|
||||||
|
fa.reason AS remark,
|
||||||
|
'' AS category,
|
||||||
|
'' AS custom_category_name,
|
||||||
|
'' AS voucher_url,
|
||||||
|
NULL AS voided_at,
|
||||||
|
NULL AS voided_by,
|
||||||
|
'' AS voided_by_name,
|
||||||
|
'' AS void_reason,
|
||||||
|
ap.account_snapshot AS source_payload`).
|
||||||
|
Joins("JOIN admin_pickups AS ap ON ap.id = fa.pickup_id").
|
||||||
|
Joins("LEFT JOIN admin_users AS operator ON operator.id = fa.settled_by").
|
||||||
|
Where("fa.settlement_mode = ?", "platform_managed").
|
||||||
|
Where("fa.status IN ?", []string{"pending", "settled"}).
|
||||||
|
Where("fa.settle_delta_cent > 0")
|
||||||
|
|
||||||
withdrawal := db.Table("withdrawal_requests AS wr").
|
withdrawal := db.Table("withdrawal_requests AS wr").
|
||||||
Select(`'withdrawal' AS source_type,
|
Select(`'withdrawal' AS source_type,
|
||||||
wr.id AS source_id,
|
wr.id AS source_id,
|
||||||
@@ -172,7 +249,7 @@ func (r *Repository) disbursementListBaseQuery(ctx context.Context) *gorm.DB {
|
|||||||
Joins("LEFT JOIN admin_users AS creator ON creator.id = md.created_by").
|
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")
|
Joins("LEFT JOIN admin_users AS voider ON voider.id = md.voided_by")
|
||||||
|
|
||||||
union := db.Raw("? UNION ALL ? UNION ALL ?", platformManaged, withdrawal, manualOffline)
|
union := db.Raw("? UNION ALL ? UNION ALL ? UNION ALL ? UNION ALL ?", platformManaged, pickupManaged, pickupAdjustments, withdrawal, manualOffline)
|
||||||
return db.Table("(?) AS d", union)
|
return db.Table("(?) AS d", union)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -42,6 +42,17 @@ func TestDisbursementListCombinesPlatformSettlementsAndWithdrawals(t *testing.T)
|
|||||||
status TEXT, created_by INTEGER, voided_by INTEGER, voided_at DATETIME,
|
status TEXT, created_by INTEGER, voided_by INTEGER, voided_at DATETIME,
|
||||||
void_reason TEXT, created_at DATETIME
|
void_reason TEXT, created_at DATETIME
|
||||||
)`,
|
)`,
|
||||||
|
`CREATE TABLE admin_pickups (
|
||||||
|
id INTEGER PRIMARY KEY, pickup_no TEXT, owner_id INTEGER, source_channel TEXT,
|
||||||
|
settlement_mode TEXT, settle_amount_cent INTEGER, offline_settlement_status TEXT,
|
||||||
|
offline_settlement_remark TEXT, offline_settled_by INTEGER, offline_settled_at DATETIME,
|
||||||
|
completed_at DATETIME, account_snapshot BLOB
|
||||||
|
)`,
|
||||||
|
`CREATE TABLE admin_pickup_financial_adjustments (
|
||||||
|
id INTEGER PRIMARY KEY, pickup_id INTEGER, settle_delta_cent INTEGER,
|
||||||
|
settlement_mode TEXT, status TEXT, reason TEXT, settled_by INTEGER,
|
||||||
|
settled_at DATETIME, created_at DATETIME
|
||||||
|
)`,
|
||||||
}
|
}
|
||||||
for _, statement := range statements {
|
for _, statement := range statements {
|
||||||
if err := db.Exec(statement).Error; err != nil {
|
if err := db.Exec(statement).Error; err != nil {
|
||||||
@@ -75,6 +86,17 @@ func TestDisbursementListCombinesPlatformSettlementsAndWithdrawals(t *testing.T)
|
|||||||
at(2), at(3), at(4)).Error; err != nil {
|
at(2), at(3), at(4)).Error; err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
|
if err := db.Exec(`INSERT INTO admin_pickups
|
||||||
|
(id, pickup_no, owner_id, source_channel, settlement_mode, settle_amount_cent, offline_settlement_status,
|
||||||
|
offline_settlement_remark, offline_settled_by, offline_settled_at, completed_at, account_snapshot)
|
||||||
|
VALUES (30, 'P202607010001', 1, '线下录入', 'platform_managed', 4000, 'settled', '提号结算', 7, ?, ?, '{}')`, at(4), at(3)).Error; err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if err := db.Exec(`INSERT INTO admin_pickup_financial_adjustments
|
||||||
|
(id, pickup_id, settle_delta_cent, settlement_mode, status, reason, settled_by, settled_at, created_at)
|
||||||
|
VALUES (31, 30, 500, 'platform_managed', 'settled', '补差价', 7, ?, ?)`, at(5), at(5)).Error; err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
if err := db.Exec(`INSERT INTO withdrawal_requests
|
if err := db.Exec(`INSERT INTO withdrawal_requests
|
||||||
(id, withdraw_no, user_id, account_type, account_name, account_no, bank_name,
|
(id, withdraw_no, user_id, account_type, account_name, account_no, bank_name,
|
||||||
amount_cent, fee_cent, actual_amount_cent, status, created_at, paid_at,
|
amount_cent, fee_cent, actual_amount_cent, status, created_at, paid_at,
|
||||||
@@ -108,11 +130,11 @@ func TestDisbursementListCombinesPlatformSettlementsAndWithdrawals(t *testing.T)
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("Disbursements() error = %v", err)
|
t.Fatalf("Disbursements() error = %v", err)
|
||||||
}
|
}
|
||||||
if result.Total != 7 || len(result.Items) != 7 {
|
if result.Total != 9 || len(result.Items) != 9 {
|
||||||
t.Fatalf("记录数 = %d/%d, want 7/7", result.Total, len(result.Items))
|
t.Fatalf("记录数 = %d/%d, want 9/9", result.Total, len(result.Items))
|
||||||
}
|
}
|
||||||
if result.Summary.PaidAmountCent != 24400 || result.Summary.PaidCount != 3 {
|
if result.Summary.PaidAmountCent != 28900 || result.Summary.PaidCount != 5 {
|
||||||
t.Fatalf("已出款 = %d/%d, want 24400/3", result.Summary.PaidAmountCent, result.Summary.PaidCount)
|
t.Fatalf("已出款 = %d/%d, want 28900/5", result.Summary.PaidAmountCent, result.Summary.PaidCount)
|
||||||
}
|
}
|
||||||
if result.Summary.PaymentPendingAmountCent != 11000 || result.Summary.PaymentPendingCount != 2 {
|
if result.Summary.PaymentPendingAmountCent != 11000 || result.Summary.PaymentPendingCount != 2 {
|
||||||
t.Fatalf("待打款 = %d/%d, want 11000/2", result.Summary.PaymentPendingAmountCent, result.Summary.PaymentPendingCount)
|
t.Fatalf("待打款 = %d/%d, want 11000/2", result.Summary.PaymentPendingAmountCent, result.Summary.PaymentPendingCount)
|
||||||
@@ -134,6 +156,16 @@ func TestDisbursementListCombinesPlatformSettlementsAndWithdrawals(t *testing.T)
|
|||||||
if platformItem.PayeeName != "外部卖家" || platformItem.PayeePhone != "13800000000" || platformItem.UploaderName != "上传客服甲" {
|
if platformItem.PayeeName != "外部卖家" || platformItem.PayeePhone != "13800000000" || platformItem.UploaderName != "上传客服甲" {
|
||||||
t.Fatalf("平台收款人/上传人 = %s/%s/%s", platformItem.PayeeName, platformItem.PayeePhone, platformItem.UploaderName)
|
t.Fatalf("平台收款人/上传人 = %s/%s/%s", platformItem.PayeeName, platformItem.PayeePhone, platformItem.UploaderName)
|
||||||
}
|
}
|
||||||
|
var pickupItem *DisbursementItemDTO
|
||||||
|
for index := range result.Items {
|
||||||
|
if result.Items[index].BusinessNo == "P202607010001" {
|
||||||
|
pickupItem = &result.Items[index]
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if pickupItem == nil || pickupItem.SourceType != "pickup" || pickupItem.ActualAmountCent != 4000 {
|
||||||
|
t.Fatalf("线下提号出款信息不正确: %+v", pickupItem)
|
||||||
|
}
|
||||||
|
|
||||||
var manualItem *DisbursementItemDTO
|
var manualItem *DisbursementItemDTO
|
||||||
for index := range result.Items {
|
for index := range result.Items {
|
||||||
|
|||||||
@@ -150,6 +150,9 @@ func TestDisbursementStatisticsUseActualPaymentTimeAndAmount(t *testing.T) {
|
|||||||
if summary.PendingPaymentAmountCent != 15600 || summary.PendingPaymentCount != 4 {
|
if summary.PendingPaymentAmountCent != 15600 || summary.PendingPaymentCount != 4 {
|
||||||
t.Fatalf("待出款 = %d/%d, want 15600/4", summary.PendingPaymentAmountCent, summary.PendingPaymentCount)
|
t.Fatalf("待出款 = %d/%d, want 15600/4", summary.PendingPaymentAmountCent, summary.PendingPaymentCount)
|
||||||
}
|
}
|
||||||
|
if summary.OfflineSettlementPendingAmountCent != 10800 || summary.OfflineSettlementPendingCount != 3 {
|
||||||
|
t.Fatalf("线下结算待出款 = %d/%d, want 10800/3", summary.OfflineSettlementPendingAmountCent, summary.OfflineSettlementPendingCount)
|
||||||
|
}
|
||||||
if summary.WithdrawalReviewAmountCent != 2000 || summary.WithdrawalReviewCount != 1 {
|
if summary.WithdrawalReviewAmountCent != 2000 || summary.WithdrawalReviewCount != 1 {
|
||||||
t.Fatalf("待审核提现 = %d/%d, want 2000/1", summary.WithdrawalReviewAmountCent, summary.WithdrawalReviewCount)
|
t.Fatalf("待审核提现 = %d/%d, want 2000/1", summary.WithdrawalReviewAmountCent, summary.WithdrawalReviewCount)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -61,6 +61,8 @@ type PickupSummaryDTO struct {
|
|||||||
ProfitAmountCent int64 `json:"profit_amount_cent"` // 区间内已完成提号的线下利润合计
|
ProfitAmountCent int64 `json:"profit_amount_cent"` // 区间内已完成提号的线下利润合计
|
||||||
CompletedCount int64 `json:"completed_count"` // 区间内已完成提号笔数
|
CompletedCount int64 `json:"completed_count"` // 区间内已完成提号笔数
|
||||||
InProgressCount int64 `json:"in_progress_count"` // 当前提号中笔数(不按时间)
|
InProgressCount int64 `json:"in_progress_count"` // 当前提号中笔数(不按时间)
|
||||||
|
PendingAmountCent int64 `json:"pending_amount_cent"` // 当前平台代管提号待结算金额(含正向调整)
|
||||||
|
PendingCount int64 `json:"pending_count"` // 当前平台代管提号待结算笔数(含正向调整)
|
||||||
}
|
}
|
||||||
|
|
||||||
// MohongSummaryDTO 撞车商城的交易流水,单独展示,不计入平台利润。
|
// MohongSummaryDTO 撞车商城的交易流水,单独展示,不计入平台利润。
|
||||||
@@ -239,11 +241,11 @@ type FinanceSummaryDTO struct {
|
|||||||
ChannelNetAmountCent int64 `json:"channel_net_amount_cent"`
|
ChannelNetAmountCent int64 `json:"channel_net_amount_cent"`
|
||||||
// PlatformIncomeAmountCent 为正常订单最终结账利润与已完成线下提号利润之和。
|
// PlatformIncomeAmountCent 为正常订单最终结账利润与已完成线下提号利润之和。
|
||||||
// 保留原 JSON 字段名,避免影响已接入的管理端客户端。
|
// 保留原 JSON 字段名,避免影响已接入的管理端客户端。
|
||||||
PlatformIncomeAmountCent int64 `json:"platform_income_amount_cent"`
|
PlatformIncomeAmountCent int64 `json:"platform_income_amount_cent"`
|
||||||
NormalOrderProfitAmountCent int64 `json:"normal_order_profit_amount_cent"`
|
NormalOrderProfitAmountCent int64 `json:"normal_order_profit_amount_cent"`
|
||||||
PickupProfitAmountCent int64 `json:"pickup_profit_amount_cent"`
|
PickupProfitAmountCent int64 `json:"pickup_profit_amount_cent"`
|
||||||
// PickupCompletedCount 线下提号利润对应的已完成提号笔数。
|
// PickupCompletedCount 线下提号利润对应的已完成提号笔数。
|
||||||
PickupCompletedCount int64 `json:"pickup_completed_count"`
|
PickupCompletedCount int64 `json:"pickup_completed_count"`
|
||||||
OwnerShouldIncomeAmountCent int64 `json:"owner_should_income_amount_cent"`
|
OwnerShouldIncomeAmountCent int64 `json:"owner_should_income_amount_cent"`
|
||||||
OwnerWalletIncomeAmountCent int64 `json:"owner_wallet_income_amount_cent"`
|
OwnerWalletIncomeAmountCent int64 `json:"owner_wallet_income_amount_cent"`
|
||||||
OwnerEffectiveSettlementAmountCent int64 `json:"owner_effective_settlement_amount_cent"`
|
OwnerEffectiveSettlementAmountCent int64 `json:"owner_effective_settlement_amount_cent"`
|
||||||
@@ -286,20 +288,20 @@ type FinanceDailyDTO struct {
|
|||||||
// NormalFullRefundCount 正常(租赁)订单已退全款笔数(不含撞车商城退款)。
|
// NormalFullRefundCount 正常(租赁)订单已退全款笔数(不含撞车商城退款)。
|
||||||
NormalFullRefundCount int64 `json:"normal_full_refund_count"`
|
NormalFullRefundCount int64 `json:"normal_full_refund_count"`
|
||||||
// PickupCancelledCount 线下提号取消笔数(按取消日归集)。
|
// PickupCancelledCount 线下提号取消笔数(按取消日归集)。
|
||||||
PickupCancelledCount int64 `json:"pickup_cancelled_count"`
|
PickupCancelledCount int64 `json:"pickup_cancelled_count"`
|
||||||
SalesCount int64 `json:"sales_count"`
|
SalesCount int64 `json:"sales_count"`
|
||||||
PaidOrderCount int64 `json:"paid_order_count"`
|
PaidOrderCount int64 `json:"paid_order_count"`
|
||||||
OfflineSettlementPendingCount int64 `json:"offline_settlement_pending_count"`
|
OfflineSettlementPendingCount int64 `json:"offline_settlement_pending_count"`
|
||||||
OfflineSettlementPaidAmountCent int64 `json:"offline_settlement_paid_amount_cent"`
|
OfflineSettlementPaidAmountCent int64 `json:"offline_settlement_paid_amount_cent"`
|
||||||
OfflineSettlementPaidCount int64 `json:"offline_settlement_paid_count"`
|
OfflineSettlementPaidCount int64 `json:"offline_settlement_paid_count"`
|
||||||
WithdrawalPaidAmountCent int64 `json:"withdrawal_paid_amount_cent"`
|
WithdrawalPaidAmountCent int64 `json:"withdrawal_paid_amount_cent"`
|
||||||
WithdrawalPaidCount int64 `json:"withdrawal_paid_count"`
|
WithdrawalPaidCount int64 `json:"withdrawal_paid_count"`
|
||||||
ManualPaidAmountCent int64 `json:"manual_paid_amount_cent"`
|
ManualPaidAmountCent int64 `json:"manual_paid_amount_cent"`
|
||||||
ManualPaidCount int64 `json:"manual_paid_count"`
|
ManualPaidCount int64 `json:"manual_paid_count"`
|
||||||
OperatingExpenseAmountCent int64 `json:"operating_expense_amount_cent"`
|
OperatingExpenseAmountCent int64 `json:"operating_expense_amount_cent"`
|
||||||
OperatingExpenseCount int64 `json:"operating_expense_count"`
|
OperatingExpenseCount int64 `json:"operating_expense_count"`
|
||||||
DisbursementPaidAmountCent int64 `json:"disbursement_paid_amount_cent"`
|
DisbursementPaidAmountCent int64 `json:"disbursement_paid_amount_cent"`
|
||||||
DisbursementPaidCount int64 `json:"disbursement_paid_count"`
|
DisbursementPaidCount int64 `json:"disbursement_paid_count"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type PaginatedResult struct {
|
type PaginatedResult struct {
|
||||||
|
|||||||
@@ -225,7 +225,7 @@ func parseDisbursementQuery(c *gin.Context) (DisbursementQuery, bool) {
|
|||||||
StartDate: start,
|
StartDate: start,
|
||||||
EndDate: end,
|
EndDate: end,
|
||||||
}
|
}
|
||||||
if query.SourceType != "" && query.SourceType != "platform_managed" && query.SourceType != "withdrawal" && query.SourceType != "manual_offline" {
|
if query.SourceType != "" && query.SourceType != "platform_managed" && query.SourceType != "pickup" && query.SourceType != "pickup_adjustment" && query.SourceType != "withdrawal" && query.SourceType != "manual_offline" {
|
||||||
response.BadRequest(c, "出款类型不正确")
|
response.BadRequest(c, "出款类型不正确")
|
||||||
return query, false
|
return query, false
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ func orderPaymentSubquery(db *gorm.DB) *gorm.DB {
|
|||||||
func ownerWalletIncomeSubquery(db *gorm.DB) *gorm.DB {
|
func ownerWalletIncomeSubquery(db *gorm.DB) *gorm.DB {
|
||||||
return db.Table("wallet_ledger").
|
return db.Table("wallet_ledger").
|
||||||
Select("order_id, COALESCE(SUM(amount_cent), 0) AS owner_wallet_income_amount_cent").
|
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"}).
|
Where("direction = ? AND biz_type IN ? AND order_id IS NOT NULL", "in", []string{"owner_income", "deposit_compensation", "arbitration_owner_income"}).
|
||||||
Group("order_id")
|
Group("order_id")
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -39,7 +39,7 @@ func ownerWalletIncomeForSettledOrdersSubquery(db *gorm.DB, query DashboardQuery
|
|||||||
Joins(`LEFT JOIN wallet_ledger AS wallet
|
Joins(`LEFT JOIN wallet_ledger AS wallet
|
||||||
ON wallet.order_id = scoped_order.id
|
ON wallet.order_id = scoped_order.id
|
||||||
AND wallet.direction = ?
|
AND wallet.direction = ?
|
||||||
AND wallet.biz_type IN ?`, "in", []string{"owner_income", "deposit_compensation"}).
|
AND wallet.biz_type IN ?`, "in", []string{"owner_income", "deposit_compensation", "arbitration_owner_income"}).
|
||||||
Where("scoped_order.settled_at >= ? AND scoped_order.settled_at <= ?", query.StartDate, query.EndDate).
|
Where("scoped_order.settled_at >= ? AND scoped_order.settled_at <= ?", query.StartDate, query.EndDate).
|
||||||
Group("scoped_order.id")
|
Group("scoped_order.id")
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -126,7 +126,7 @@ export interface FinanceDisbursementListSummary {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export interface FinanceDisbursementItem {
|
export interface FinanceDisbursementItem {
|
||||||
source_type: 'platform_managed' | 'withdrawal' | 'manual_offline'
|
source_type: 'platform_managed' | 'pickup' | 'pickup_adjustment' | 'withdrawal' | 'manual_offline'
|
||||||
source_id: number
|
source_id: number
|
||||||
business_no: string
|
business_no: string
|
||||||
order_id?: number
|
order_id?: number
|
||||||
|
|||||||
@@ -274,6 +274,8 @@ function money(cents: number | undefined | null) {
|
|||||||
function sourceLabel(sourceType: string) {
|
function sourceLabel(sourceType: string) {
|
||||||
const labels: Record<string, string> = {
|
const labels: Record<string, string> = {
|
||||||
platform_managed: '平台代管',
|
platform_managed: '平台代管',
|
||||||
|
pickup: '线下提号',
|
||||||
|
pickup_adjustment: '提号调整',
|
||||||
withdrawal: '用户提现',
|
withdrawal: '用户提现',
|
||||||
manual_offline: '线下出款',
|
manual_offline: '线下出款',
|
||||||
}
|
}
|
||||||
@@ -283,6 +285,8 @@ function sourceLabel(sourceType: string) {
|
|||||||
function sourceType(sourceType: string) {
|
function sourceType(sourceType: string) {
|
||||||
const types: Record<string, 'primary' | 'success' | 'warning' | 'info' | 'danger'> = {
|
const types: Record<string, 'primary' | 'success' | 'warning' | 'info' | 'danger'> = {
|
||||||
platform_managed: 'primary',
|
platform_managed: 'primary',
|
||||||
|
pickup: 'success',
|
||||||
|
pickup_adjustment: 'warning',
|
||||||
withdrawal: 'success',
|
withdrawal: 'success',
|
||||||
manual_offline: 'warning',
|
manual_offline: 'warning',
|
||||||
}
|
}
|
||||||
@@ -312,7 +316,10 @@ function itemStatusLabel(row: FinanceDisbursementItem) {
|
|||||||
}
|
}
|
||||||
return map[row.raw_status] || row.status
|
return map[row.raw_status] || row.status
|
||||||
}
|
}
|
||||||
if (row.source_type === 'platform_managed') {
|
if (row.source_type === 'platform_managed' || row.source_type === 'pickup') {
|
||||||
|
return row.raw_status === 'settled' ? '已打款' : '待打款'
|
||||||
|
}
|
||||||
|
if (row.source_type === 'pickup_adjustment') {
|
||||||
return row.raw_status === 'settled' ? '已打款' : '待打款'
|
return row.raw_status === 'settled' ? '已打款' : '待打款'
|
||||||
}
|
}
|
||||||
return row.status === 'paid' ? '已记账' : '已作废'
|
return row.status === 'paid' ? '已记账' : '已作废'
|
||||||
@@ -472,6 +479,8 @@ function accountTypeLabel(type: string) {
|
|||||||
class="full-control"
|
class="full-control"
|
||||||
>
|
>
|
||||||
<el-option label="平台代管" value="platform_managed" />
|
<el-option label="平台代管" value="platform_managed" />
|
||||||
|
<el-option label="线下提号" value="pickup" />
|
||||||
|
<el-option label="提号调整" value="pickup_adjustment" />
|
||||||
<el-option label="用户提现" value="withdrawal" />
|
<el-option label="用户提现" value="withdrawal" />
|
||||||
<el-option label="其他线下出款" value="manual_offline" />
|
<el-option label="其他线下出款" value="manual_offline" />
|
||||||
</el-select>
|
</el-select>
|
||||||
|
|||||||
Reference in New Issue
Block a user