支持提号完成后财务调整
This commit is contained in:
@@ -0,0 +1,23 @@
|
|||||||
|
package model
|
||||||
|
|
||||||
|
import "time"
|
||||||
|
|
||||||
|
// AdminPickupFinancialAdjustment 保存已完成提号的财务调整,原始结算快照不可修改。
|
||||||
|
type AdminPickupFinancialAdjustment struct {
|
||||||
|
ID uint64 `gorm:"primaryKey" json:"id"`
|
||||||
|
PickupID uint64 `gorm:"not null;index" json:"pickup_id"`
|
||||||
|
ProfitDeltaCent int64 `gorm:"not null;default:0" json:"profit_delta_cent"`
|
||||||
|
SettleDeltaCent int64 `gorm:"not null;default:0" json:"settle_delta_cent"`
|
||||||
|
SettlementMode string `gorm:"size:32;not null" json:"settlement_mode"`
|
||||||
|
Status string `gorm:"size:24;not null;index" json:"status"`
|
||||||
|
Reason string `gorm:"size:255;not null" json:"reason"`
|
||||||
|
CreatedBy uint64 `gorm:"not null" json:"created_by"`
|
||||||
|
SettledBy *uint64 `json:"settled_by,omitempty"`
|
||||||
|
SettledAt *time.Time `json:"settled_at,omitempty"`
|
||||||
|
SettlementRemark string `gorm:"size:255;not null;default:''" json:"settlement_remark"`
|
||||||
|
CreatedAt time.Time `json:"created_at"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (AdminPickupFinancialAdjustment) TableName() string {
|
||||||
|
return "admin_pickup_financial_adjustments"
|
||||||
|
}
|
||||||
@@ -52,13 +52,25 @@ func (r *Repository) pickupSummary(ctx context.Context, query DashboardQuery) (*
|
|||||||
Scan(&row).Error; err != nil {
|
Scan(&row).Error; err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
var adjustments struct {
|
||||||
|
SettledAmountCent int64
|
||||||
|
ProfitAmountCent int64
|
||||||
|
}
|
||||||
|
if err := db.Table("admin_pickup_financial_adjustments AS fa").
|
||||||
|
Select(`COALESCE(SUM(fa.settle_delta_cent), 0) AS settled_amount_cent,
|
||||||
|
COALESCE(SUM(fa.profit_delta_cent), 0) AS profit_amount_cent`).
|
||||||
|
Joins("JOIN admin_pickups AS p ON p.id = fa.pickup_id AND p.status = ?", "completed").
|
||||||
|
Where("fa.created_at >= ? AND fa.created_at <= ?", query.StartDate, query.EndDate).
|
||||||
|
Scan(&adjustments).Error; err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
var inProgress int64
|
var inProgress int64
|
||||||
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
|
||||||
}
|
}
|
||||||
return &PickupSummaryDTO{
|
return &PickupSummaryDTO{
|
||||||
SettledAmountCent: row.SettledAmountCent,
|
SettledAmountCent: row.SettledAmountCent + adjustments.SettledAmountCent,
|
||||||
ProfitAmountCent: row.ProfitAmountCent,
|
ProfitAmountCent: row.ProfitAmountCent + adjustments.ProfitAmountCent,
|
||||||
CompletedCount: row.CompletedCount,
|
CompletedCount: row.CompletedCount,
|
||||||
InProgressCount: inProgress,
|
InProgressCount: inProgress,
|
||||||
}, nil
|
}, nil
|
||||||
@@ -257,6 +269,16 @@ func (r *Repository) dailyItems(ctx context.Context, query DashboardQuery) ([]Fi
|
|||||||
Scan(&pickupProfits).Error; err != nil {
|
Scan(&pickupProfits).Error; err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
pickupAdjustments := make([]dailyPickupAdjustmentRow, 0)
|
||||||
|
if err := db.Table("admin_pickup_financial_adjustments AS fa").
|
||||||
|
Select(`DATE(fa.created_at) AS date,
|
||||||
|
COALESCE(SUM(fa.profit_delta_cent), 0) AS profit_amount_cent`).
|
||||||
|
Joins("JOIN admin_pickups AS p ON p.id = fa.pickup_id AND p.status = ?", "completed").
|
||||||
|
Where("fa.created_at >= ? AND fa.created_at <= ?", query.StartDate, query.EndDate).
|
||||||
|
Group("DATE(fa.created_at)").
|
||||||
|
Scan(&pickupAdjustments).Error; err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
// 取消提号按取消日统计,与「已付款订单数」中的提号创建口径区分开。
|
// 取消提号按取消日统计,与「已付款订单数」中的提号创建口径区分开。
|
||||||
pickupCancelled, err := r.dailyPickupCancelled(ctx, query)
|
pickupCancelled, err := r.dailyPickupCancelled(ctx, query)
|
||||||
@@ -335,6 +357,13 @@ func (r *Repository) dailyItems(ctx context.Context, query DashboardQuery) ([]Fi
|
|||||||
item.SalesCount += row.CompletedCount
|
item.SalesCount += row.CompletedCount
|
||||||
itemsByDate[date] = item
|
itemsByDate[date] = item
|
||||||
}
|
}
|
||||||
|
for _, row := range pickupAdjustments {
|
||||||
|
date := dailyDateKey(row.Date)
|
||||||
|
item := itemsByDate[date]
|
||||||
|
item.Date = date
|
||||||
|
item.PlatformIncomeAmountCent += row.ProfitAmountCent
|
||||||
|
itemsByDate[date] = item
|
||||||
|
}
|
||||||
for _, row := range pickupCancelled {
|
for _, row := range pickupCancelled {
|
||||||
date := dailyDateKey(row.Date)
|
date := dailyDateKey(row.Date)
|
||||||
item := itemsByDate[date]
|
item := itemsByDate[date]
|
||||||
@@ -496,6 +525,12 @@ type dailyPickupProfitRow struct {
|
|||||||
CompletedCount int64
|
CompletedCount int64
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// dailyPickupAdjustmentRow 调整按调整创建日进入当日利润,避免回写历史完成日报。
|
||||||
|
type dailyPickupAdjustmentRow struct {
|
||||||
|
Date string
|
||||||
|
ProfitAmountCent int64
|
||||||
|
}
|
||||||
|
|
||||||
type dailyEstimatedRow struct {
|
type dailyEstimatedRow struct {
|
||||||
Date string
|
Date string
|
||||||
EstimatedIncomeAmountCent int64
|
EstimatedIncomeAmountCent int64
|
||||||
|
|||||||
@@ -183,3 +183,60 @@ func TestDailyPickupCancelledByCancelledAt(t *testing.T) {
|
|||||||
t.Fatalf("8月2日取消提号数 = %d, want 2", result[0].Count)
|
t.Fatalf("8月2日取消提号数 = %d, want 2", result[0].Count)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestPickupSummaryKeepsOriginalAndAdjustmentOnTheirOwnDates(t *testing.T) {
|
||||||
|
db, err := gorm.Open(sqlite.Open(":memory:"), &gorm.Config{Logger: logger.Default.LogMode(logger.Silent)})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("打开测试数据库失败: %v", err)
|
||||||
|
}
|
||||||
|
if err := db.Exec(`CREATE TABLE admin_pickups (
|
||||||
|
id INTEGER PRIMARY KEY,
|
||||||
|
status TEXT NOT NULL,
|
||||||
|
settle_amount_cent INTEGER NOT NULL,
|
||||||
|
profit_amount_cent INTEGER NOT NULL,
|
||||||
|
completed_at DATETIME NULL
|
||||||
|
)`).Error; err != nil {
|
||||||
|
t.Fatalf("创建提号表失败: %v", err)
|
||||||
|
}
|
||||||
|
if err := db.Exec(`CREATE TABLE admin_pickup_financial_adjustments (
|
||||||
|
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
|
||||||
|
)`).Error; err != nil {
|
||||||
|
t.Fatalf("创建提号调整表失败: %v", err)
|
||||||
|
}
|
||||||
|
loc := timeutil.ShanghaiLocation()
|
||||||
|
completedAt := time.Date(2026, 8, 2, 10, 0, 0, 0, loc)
|
||||||
|
adjustedAt := time.Date(2026, 8, 4, 10, 0, 0, 0, loc)
|
||||||
|
if err := db.Exec(`INSERT INTO admin_pickups (id, status, settle_amount_cent, profit_amount_cent, completed_at)
|
||||||
|
VALUES (1, 'completed', 12000, 1000, ?)`, completedAt).Error; err != nil {
|
||||||
|
t.Fatalf("写入提号失败: %v", err)
|
||||||
|
}
|
||||||
|
if err := db.Exec(`INSERT INTO admin_pickup_financial_adjustments (id, pickup_id, settle_delta_cent, profit_delta_cent, created_at)
|
||||||
|
VALUES (1, 1, 1500, 500, ?)`, adjustedAt).Error; err != nil {
|
||||||
|
t.Fatalf("写入调整失败: %v", err)
|
||||||
|
}
|
||||||
|
repo := NewRepository(db)
|
||||||
|
dayQuery := func(day int) DashboardQuery {
|
||||||
|
return DashboardQuery{
|
||||||
|
StartDate: time.Date(2026, 8, day, 0, 0, 0, 0, loc),
|
||||||
|
EndDate: time.Date(2026, 8, day, 23, 59, 59, 0, loc),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
original, err := repo.pickupSummary(t.Context(), dayQuery(2))
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("查询完成日提号汇总失败: %v", err)
|
||||||
|
}
|
||||||
|
if original.SettledAmountCent != 12000 || original.ProfitAmountCent != 1000 || original.CompletedCount != 1 {
|
||||||
|
t.Fatalf("完成日汇总 = %+v", original)
|
||||||
|
}
|
||||||
|
adjusted, err := repo.pickupSummary(t.Context(), dayQuery(4))
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("查询调整日提号汇总失败: %v", err)
|
||||||
|
}
|
||||||
|
if adjusted.SettledAmountCent != 1500 || adjusted.ProfitAmountCent != 500 || adjusted.CompletedCount != 0 {
|
||||||
|
t.Fatalf("调整日汇总 = %+v", adjusted)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -18,6 +18,31 @@ func (r *Repository) disbursementSummary(ctx context.Context, query DashboardQue
|
|||||||
Scan(&offlinePaid).Error; err != nil {
|
Scan(&offlinePaid).Error; err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
var pickupPaid 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 = ?", "platform_managed").
|
||||||
|
Where("offline_settlement_status = ?", "settled").
|
||||||
|
Where("offline_settled_at >= ? AND offline_settled_at <= ?", query.StartDate, query.EndDate).
|
||||||
|
Scan(&pickupPaid).Error; err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
var pickupAdjustmentPaid 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 = ?", "platform_managed").
|
||||||
|
Where("status = ?", "settled").
|
||||||
|
Where("settle_delta_cent > 0").
|
||||||
|
Where("settled_at >= ? AND settled_at <= ?", query.StartDate, query.EndDate).
|
||||||
|
Scan(&pickupAdjustmentPaid).Error; err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
var offlinePending struct {
|
var offlinePending struct {
|
||||||
AmountCent int64
|
AmountCent int64
|
||||||
@@ -31,6 +56,30 @@ func (r *Repository) disbursementSummary(ctx context.Context, query DashboardQue
|
|||||||
Scan(&offlinePending).Error; err != nil {
|
Scan(&offlinePending).Error; err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
var pickupPending 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 = ?", "platform_managed").
|
||||||
|
Where("offline_settlement_status = ?", "pending").
|
||||||
|
Where("settle_amount_cent > 0").
|
||||||
|
Scan(&pickupPending).Error; err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
var pickupAdjustmentPending 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 = ?", "platform_managed").
|
||||||
|
Where("status = ?", "pending").
|
||||||
|
Where("settle_delta_cent > 0").
|
||||||
|
Scan(&pickupAdjustmentPending).Error; err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
var withdrawalPaid struct {
|
var withdrawalPaid struct {
|
||||||
AmountCent int64
|
AmountCent int64
|
||||||
@@ -84,12 +133,12 @@ func (r *Repository) disbursementSummary(ctx context.Context, query DashboardQue
|
|||||||
}
|
}
|
||||||
|
|
||||||
return &DisbursementSummaryDTO{
|
return &DisbursementSummaryDTO{
|
||||||
PaidAmountCent: offlinePaid.AmountCent + withdrawalPaid.ActualAmountCent + manualPaid.AmountCent,
|
PaidAmountCent: offlinePaid.AmountCent + pickupPaid.AmountCent + pickupAdjustmentPaid.AmountCent + withdrawalPaid.ActualAmountCent + manualPaid.AmountCent,
|
||||||
PaidCount: offlinePaid.Count + withdrawalPaid.Count + manualPaid.Count,
|
PaidCount: offlinePaid.Count + pickupPaid.Count + pickupAdjustmentPaid.Count + withdrawalPaid.Count + manualPaid.Count,
|
||||||
PendingPaymentAmountCent: offlinePending.AmountCent + withdrawalPending.ActualAmountCent,
|
PendingPaymentAmountCent: offlinePending.AmountCent + pickupPending.AmountCent + pickupAdjustmentPending.AmountCent + withdrawalPending.ActualAmountCent,
|
||||||
PendingPaymentCount: offlinePending.Count + withdrawalPending.Count,
|
PendingPaymentCount: offlinePending.Count + pickupPending.Count + pickupAdjustmentPending.Count + withdrawalPending.Count,
|
||||||
OfflineSettlementPaidAmountCent: offlinePaid.AmountCent,
|
OfflineSettlementPaidAmountCent: offlinePaid.AmountCent + pickupPaid.AmountCent + pickupAdjustmentPaid.AmountCent,
|
||||||
OfflineSettlementPaidCount: offlinePaid.Count,
|
OfflineSettlementPaidCount: offlinePaid.Count + pickupPaid.Count + pickupAdjustmentPaid.Count,
|
||||||
OfflineSettlementPendingAmountCent: offlinePending.AmountCent,
|
OfflineSettlementPendingAmountCent: offlinePending.AmountCent,
|
||||||
OfflineSettlementPendingCount: offlinePending.Count,
|
OfflineSettlementPendingCount: offlinePending.Count,
|
||||||
WithdrawalAmountCent: withdrawalPaid.AmountCent,
|
WithdrawalAmountCent: withdrawalPaid.AmountCent,
|
||||||
@@ -119,6 +168,33 @@ func (r *Repository) dailyDisbursements(ctx context.Context, query DashboardQuer
|
|||||||
Scan(&offlineRows).Error; err != nil {
|
Scan(&offlineRows).Error; err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
pickupOfflineRows := make([]dailyDisbursementSourceRow, 0)
|
||||||
|
if err := db.Table("admin_pickups").
|
||||||
|
Select(`DATE(offline_settled_at) AS date,
|
||||||
|
COALESCE(SUM(settle_amount_cent), 0) AS amount_cent,
|
||||||
|
COUNT(id) AS count`).
|
||||||
|
Where("settlement_mode = ?", "platform_managed").
|
||||||
|
Where("offline_settlement_status = ?", "settled").
|
||||||
|
Where("offline_settled_at >= ? AND offline_settled_at <= ?", query.StartDate, query.EndDate).
|
||||||
|
Group("DATE(offline_settled_at)").
|
||||||
|
Scan(&pickupOfflineRows).Error; err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
pickupAdjustmentRows := make([]dailyDisbursementSourceRow, 0)
|
||||||
|
if err := db.Table("admin_pickup_financial_adjustments").
|
||||||
|
Select(`DATE(settled_at) AS date,
|
||||||
|
COALESCE(SUM(settle_delta_cent), 0) AS amount_cent,
|
||||||
|
COUNT(id) AS count`).
|
||||||
|
Where("settlement_mode = ?", "platform_managed").
|
||||||
|
Where("status = ?", "settled").
|
||||||
|
Where("settle_delta_cent > 0").
|
||||||
|
Where("settled_at >= ? AND settled_at <= ?", query.StartDate, query.EndDate).
|
||||||
|
Group("DATE(settled_at)").
|
||||||
|
Scan(&pickupAdjustmentRows).Error; err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
offlineRows = append(offlineRows, pickupOfflineRows...)
|
||||||
|
offlineRows = append(offlineRows, pickupAdjustmentRows...)
|
||||||
|
|
||||||
withdrawalRows := make([]dailyDisbursementSourceRow, 0)
|
withdrawalRows := make([]dailyDisbursementSourceRow, 0)
|
||||||
if err := db.Table("withdrawal_requests").
|
if err := db.Table("withdrawal_requests").
|
||||||
@@ -149,8 +225,8 @@ func (r *Repository) dailyDisbursements(ctx context.Context, query DashboardQuer
|
|||||||
date := dailyDateKey(row.Date)
|
date := dailyDateKey(row.Date)
|
||||||
item := rowsByDate[date]
|
item := rowsByDate[date]
|
||||||
item.Date = date
|
item.Date = date
|
||||||
item.OfflineSettlementPaidAmountCent = row.AmountCent
|
item.OfflineSettlementPaidAmountCent += row.AmountCent
|
||||||
item.OfflineSettlementPaidCount = row.Count
|
item.OfflineSettlementPaidCount += row.Count
|
||||||
rowsByDate[date] = item
|
rowsByDate[date] = item
|
||||||
}
|
}
|
||||||
for _, row := range withdrawalRows {
|
for _, row := range withdrawalRows {
|
||||||
|
|||||||
@@ -43,6 +43,24 @@ func TestDisbursementStatisticsUseActualPaymentTimeAndAmount(t *testing.T) {
|
|||||||
)`).Error; err != nil {
|
)`).Error; err != nil {
|
||||||
t.Fatalf("创建其他线下出款表失败: %v", err)
|
t.Fatalf("创建其他线下出款表失败: %v", err)
|
||||||
}
|
}
|
||||||
|
if err := db.Exec(`CREATE TABLE admin_pickups (
|
||||||
|
id INTEGER PRIMARY KEY,
|
||||||
|
settlement_mode TEXT NOT NULL,
|
||||||
|
offline_settlement_status TEXT NOT NULL,
|
||||||
|
settle_amount_cent INTEGER NOT NULL,
|
||||||
|
offline_settled_at DATETIME NULL
|
||||||
|
)`).Error; err != nil {
|
||||||
|
t.Fatalf("创建提号表失败: %v", err)
|
||||||
|
}
|
||||||
|
if err := db.Exec(`CREATE TABLE admin_pickup_financial_adjustments (
|
||||||
|
id INTEGER PRIMARY KEY,
|
||||||
|
settlement_mode TEXT NOT NULL,
|
||||||
|
status TEXT NOT NULL,
|
||||||
|
settle_delta_cent INTEGER NOT NULL,
|
||||||
|
settled_at DATETIME NULL
|
||||||
|
)`).Error; err != nil {
|
||||||
|
t.Fatalf("创建提号调整表失败: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
loc := timeutil.ShanghaiLocation()
|
loc := timeutil.ShanghaiLocation()
|
||||||
inRangeOffline := time.Date(2026, 7, 1, 10, 0, 0, 0, loc)
|
inRangeOffline := time.Date(2026, 7, 1, 10, 0, 0, 0, loc)
|
||||||
@@ -63,6 +81,27 @@ func TestDisbursementStatisticsUseActualPaymentTimeAndAmount(t *testing.T) {
|
|||||||
t.Fatalf("写入订单失败: %v", err)
|
t.Fatalf("写入订单失败: %v", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
for _, args := range [][]any{
|
||||||
|
{1, "platform_managed", "settled", 4000, inRangeOffline},
|
||||||
|
{2, "platform_managed", "pending", 2500, nil},
|
||||||
|
} {
|
||||||
|
if err := db.Exec(`INSERT INTO admin_pickups
|
||||||
|
(id, settlement_mode, offline_settlement_status, settle_amount_cent, offline_settled_at)
|
||||||
|
VALUES (?, ?, ?, ?, ?)`, args...).Error; err != nil {
|
||||||
|
t.Fatalf("写入提号失败: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for _, args := range [][]any{
|
||||||
|
{1, "platform_managed", "settled", 500, inRangeManual},
|
||||||
|
{2, "platform_managed", "pending", 300, nil},
|
||||||
|
{3, "platform_managed", "recovery_pending", -200, nil},
|
||||||
|
} {
|
||||||
|
if err := db.Exec(`INSERT INTO admin_pickup_financial_adjustments
|
||||||
|
(id, settlement_mode, status, settle_delta_cent, settled_at)
|
||||||
|
VALUES (?, ?, ?, ?, ?)`, args...).Error; err != nil {
|
||||||
|
t.Fatalf("写入提号调整失败: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
for _, args := range [][]any{
|
for _, args := range [][]any{
|
||||||
{1, "paid", 2500, inRangeManual},
|
{1, "paid", 2500, inRangeManual},
|
||||||
{2, "paid", 1000, outOfRange},
|
{2, "paid", 1000, outOfRange},
|
||||||
@@ -96,11 +135,11 @@ func TestDisbursementStatisticsUseActualPaymentTimeAndAmount(t *testing.T) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("disbursementSummary() error = %v", err)
|
t.Fatalf("disbursementSummary() error = %v", err)
|
||||||
}
|
}
|
||||||
if summary.PaidAmountCent != 24400 || summary.PaidCount != 3 {
|
if summary.PaidAmountCent != 28900 || summary.PaidCount != 5 {
|
||||||
t.Fatalf("实际出款 = %d/%d, want 24400/3", summary.PaidAmountCent, summary.PaidCount)
|
t.Fatalf("实际出款 = %d/%d, want 28900/5", summary.PaidAmountCent, summary.PaidCount)
|
||||||
}
|
}
|
||||||
if summary.OfflineSettlementPaidAmountCent != 12000 || summary.OfflineSettlementPaidCount != 1 {
|
if summary.OfflineSettlementPaidAmountCent != 16500 || summary.OfflineSettlementPaidCount != 3 {
|
||||||
t.Fatalf("代管打款 = %d/%d, want 12000/1", summary.OfflineSettlementPaidAmountCent, summary.OfflineSettlementPaidCount)
|
t.Fatalf("代管打款 = %d/%d, want 16500/3", summary.OfflineSettlementPaidAmountCent, summary.OfflineSettlementPaidCount)
|
||||||
}
|
}
|
||||||
if summary.WithdrawalAmountCent != 10000 || summary.WithdrawalFeeAmountCent != 100 || summary.WithdrawalPaidAmountCent != 9900 {
|
if summary.WithdrawalAmountCent != 10000 || summary.WithdrawalFeeAmountCent != 100 || summary.WithdrawalPaidAmountCent != 9900 {
|
||||||
t.Fatalf("提现申请/手续费/实付 = %d/%d/%d, want 10000/100/9900", summary.WithdrawalAmountCent, summary.WithdrawalFeeAmountCent, summary.WithdrawalPaidAmountCent)
|
t.Fatalf("提现申请/手续费/实付 = %d/%d/%d, want 10000/100/9900", summary.WithdrawalAmountCent, summary.WithdrawalFeeAmountCent, summary.WithdrawalPaidAmountCent)
|
||||||
@@ -108,8 +147,8 @@ func TestDisbursementStatisticsUseActualPaymentTimeAndAmount(t *testing.T) {
|
|||||||
if summary.ManualPaidAmountCent != 2500 || summary.ManualPaidCount != 1 {
|
if summary.ManualPaidAmountCent != 2500 || summary.ManualPaidCount != 1 {
|
||||||
t.Fatalf("其他线下出款 = %d/%d, want 2500/1", summary.ManualPaidAmountCent, summary.ManualPaidCount)
|
t.Fatalf("其他线下出款 = %d/%d, want 2500/1", summary.ManualPaidAmountCent, summary.ManualPaidCount)
|
||||||
}
|
}
|
||||||
if summary.PendingPaymentAmountCent != 12800 || summary.PendingPaymentCount != 2 {
|
if summary.PendingPaymentAmountCent != 15600 || summary.PendingPaymentCount != 4 {
|
||||||
t.Fatalf("待出款 = %d/%d, want 12800/2", summary.PendingPaymentAmountCent, summary.PendingPaymentCount)
|
t.Fatalf("待出款 = %d/%d, want 15600/4", summary.PendingPaymentAmountCent, summary.PendingPaymentCount)
|
||||||
}
|
}
|
||||||
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)
|
||||||
@@ -122,13 +161,13 @@ func TestDisbursementStatisticsUseActualPaymentTimeAndAmount(t *testing.T) {
|
|||||||
if len(daily) != 3 {
|
if len(daily) != 3 {
|
||||||
t.Fatalf("每日出款行数 = %d, want 3", len(daily))
|
t.Fatalf("每日出款行数 = %d, want 3", len(daily))
|
||||||
}
|
}
|
||||||
if daily[0].Date != "2026-07-01" || daily[0].OfflineSettlementPaidAmountCent != 12000 {
|
if daily[0].Date != "2026-07-01" || daily[0].OfflineSettlementPaidAmountCent != 16000 {
|
||||||
t.Fatalf("第一日代管出款 = %+v", daily[0])
|
t.Fatalf("第一日代管出款 = %+v", daily[0])
|
||||||
}
|
}
|
||||||
if daily[1].Date != "2026-07-02" || daily[1].WithdrawalPaidAmountCent != 9900 {
|
if daily[1].Date != "2026-07-02" || daily[1].WithdrawalPaidAmountCent != 9900 {
|
||||||
t.Fatalf("第二日提现出款 = %+v", daily[1])
|
t.Fatalf("第二日提现出款 = %+v", daily[1])
|
||||||
}
|
}
|
||||||
if daily[2].Date != "2026-07-03" || daily[2].ManualPaidAmountCent != 2500 {
|
if daily[2].Date != "2026-07-03" || daily[2].ManualPaidAmountCent != 2500 || daily[2].OfflineSettlementPaidAmountCent != 500 {
|
||||||
t.Fatalf("第三日其他线下出款 = %+v", daily[2])
|
t.Fatalf("第三日其他线下出款 = %+v", daily[2])
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -22,54 +22,79 @@ const (
|
|||||||
OfflineSettlementStatusNone = "none"
|
OfflineSettlementStatusNone = "none"
|
||||||
OfflineSettlementStatusPending = "pending"
|
OfflineSettlementStatusPending = "pending"
|
||||||
OfflineSettlementStatusSettled = "settled"
|
OfflineSettlementStatusSettled = "settled"
|
||||||
|
FinancialAdjustmentStatusSettled = "settled"
|
||||||
|
FinancialAdjustmentStatusPending = "pending"
|
||||||
|
FinancialAdjustmentStatusRecovery = "recovery_pending"
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
ErrDependencyUnavailable = errors.New("dependency unavailable")
|
ErrDependencyUnavailable = errors.New("dependency unavailable")
|
||||||
ErrListingUnavailable = errors.New("listing unavailable")
|
ErrListingUnavailable = errors.New("listing unavailable")
|
||||||
ErrPickupNotFound = errors.New("pickup not found")
|
ErrPickupNotFound = errors.New("pickup not found")
|
||||||
ErrPickupNotPickingUp = errors.New("pickup not in picking_up status")
|
ErrPickupNotPickingUp = errors.New("pickup not in picking_up status")
|
||||||
ErrInvalidAmount = errors.New("invalid amount")
|
ErrInvalidAmount = errors.New("invalid amount")
|
||||||
ErrInvalidProfit = errors.New("invalid profit amount")
|
ErrInvalidProfit = errors.New("invalid profit amount")
|
||||||
ErrDuplicatePickup = errors.New("duplicate pickup in progress")
|
ErrDuplicatePickup = errors.New("duplicate pickup in progress")
|
||||||
ErrOfflineSettlementCannotMark = errors.New("pickup offline settlement cannot be marked")
|
ErrOfflineSettlementCannotMark = errors.New("pickup offline settlement cannot be marked")
|
||||||
|
ErrFinancialAdjustmentInvalid = errors.New("financial adjustment invalid")
|
||||||
|
ErrFinancialAdjustmentNotFound = errors.New("financial adjustment not found")
|
||||||
|
ErrFinancialAdjustmentCannotSettle = errors.New("financial adjustment cannot be settled")
|
||||||
)
|
)
|
||||||
|
|
||||||
type PickupDTO struct {
|
type PickupDTO struct {
|
||||||
ID uint64 `json:"id"`
|
ID uint64 `json:"id"`
|
||||||
PickupNo string `json:"pickup_no"`
|
PickupNo string `json:"pickup_no"`
|
||||||
ListingID uint64 `json:"listing_id"`
|
ListingID uint64 `json:"listing_id"`
|
||||||
ListingNo string `json:"listing_no"`
|
ListingNo string `json:"listing_no"`
|
||||||
AccountID uint64 `json:"account_id"`
|
AccountID uint64 `json:"account_id"`
|
||||||
AccountTitle string `json:"account_title"`
|
AccountTitle string `json:"account_title"`
|
||||||
ServerRegion string `json:"server_region"`
|
ServerRegion string `json:"server_region"`
|
||||||
LoginPlatform string `json:"login_platform"`
|
LoginPlatform string `json:"login_platform"`
|
||||||
OwnerID uint64 `json:"owner_id"`
|
OwnerID uint64 `json:"owner_id"`
|
||||||
OwnerPhone string `json:"owner_phone"`
|
OwnerPhone string `json:"owner_phone"`
|
||||||
AdminID uint64 `json:"admin_id"`
|
AdminID uint64 `json:"admin_id"`
|
||||||
Platform string `json:"platform"`
|
Platform string `json:"platform"`
|
||||||
ShopName string `json:"shop_name"`
|
ShopName string `json:"shop_name"`
|
||||||
AccountSource string `json:"account_source"`
|
AccountSource string `json:"account_source"`
|
||||||
SourceChannel string `json:"source_channel"`
|
SourceChannel string `json:"source_channel"`
|
||||||
SettlementMode string `json:"settlement_mode"`
|
SettlementMode string `json:"settlement_mode"`
|
||||||
ListingPriceCent int64 `json:"listing_price_cent"`
|
ListingPriceCent int64 `json:"listing_price_cent"`
|
||||||
OwnerPriceCent int64 `json:"owner_price_cent"`
|
OwnerPriceCent int64 `json:"owner_price_cent"`
|
||||||
WebsiteProfitCent int64 `json:"website_profit_cent"`
|
WebsiteProfitCent int64 `json:"website_profit_cent"`
|
||||||
ProfitAmountCent int64 `json:"profit_amount_cent"`
|
ProfitAmountCent int64 `json:"profit_amount_cent"`
|
||||||
SellerRatio float64 `json:"seller_ratio"`
|
ProfitAdjustmentCent int64 `json:"profit_adjustment_cent"`
|
||||||
BuyerRatio float64 `json:"buyer_ratio"`
|
EffectiveProfitAmountCent int64 `json:"effective_profit_amount_cent"`
|
||||||
AccountSnapshot datatypes.JSON `json:"account_snapshot,omitempty"`
|
SellerRatio float64 `json:"seller_ratio"`
|
||||||
SettleAmountCent int64 `json:"settle_amount_cent"`
|
BuyerRatio float64 `json:"buyer_ratio"`
|
||||||
Status string `json:"status"`
|
AccountSnapshot datatypes.JSON `json:"account_snapshot,omitempty"`
|
||||||
OfflineSettlementStatus string `json:"offline_settlement_status"`
|
SettleAmountCent int64 `json:"settle_amount_cent"`
|
||||||
OfflineSettlementRemark string `json:"offline_settlement_remark"`
|
SettleAdjustmentCent int64 `json:"settle_adjustment_cent"`
|
||||||
OfflineSettledBy *uint64 `json:"offline_settled_by,omitempty"`
|
EffectiveSettleAmountCent int64 `json:"effective_settle_amount_cent"`
|
||||||
OfflineSettledAt *time.Time `json:"offline_settled_at,omitempty"`
|
Status string `json:"status"`
|
||||||
Remark string `json:"remark"`
|
OfflineSettlementStatus string `json:"offline_settlement_status"`
|
||||||
CompleteRemark string `json:"complete_remark"`
|
OfflineSettlementRemark string `json:"offline_settlement_remark"`
|
||||||
CreatedAt time.Time `json:"created_at"`
|
OfflineSettledBy *uint64 `json:"offline_settled_by,omitempty"`
|
||||||
CompletedAt *time.Time `json:"completed_at,omitempty"`
|
OfflineSettledAt *time.Time `json:"offline_settled_at,omitempty"`
|
||||||
CancelledAt *time.Time `json:"cancelled_at,omitempty"`
|
Remark string `json:"remark"`
|
||||||
|
CompleteRemark string `json:"complete_remark"`
|
||||||
|
CreatedAt time.Time `json:"created_at"`
|
||||||
|
CompletedAt *time.Time `json:"completed_at,omitempty"`
|
||||||
|
CancelledAt *time.Time `json:"cancelled_at,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type FinancialAdjustmentDTO struct {
|
||||||
|
ID uint64 `json:"id"`
|
||||||
|
PickupID uint64 `json:"pickup_id"`
|
||||||
|
ProfitDeltaCent int64 `json:"profit_delta_cent"`
|
||||||
|
SettleDeltaCent int64 `json:"settle_delta_cent"`
|
||||||
|
SettlementMode string `json:"settlement_mode"`
|
||||||
|
Status string `json:"status"`
|
||||||
|
Reason string `json:"reason"`
|
||||||
|
CreatedBy uint64 `json:"created_by"`
|
||||||
|
SettledBy *uint64 `json:"settled_by,omitempty"`
|
||||||
|
SettledAt *time.Time `json:"settled_at,omitempty"`
|
||||||
|
SettlementRemark string `json:"settlement_remark"`
|
||||||
|
CreatedAt time.Time `json:"created_at"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type CreateRequest struct {
|
type CreateRequest struct {
|
||||||
@@ -95,6 +120,17 @@ type UpdateProfitRequest struct {
|
|||||||
Reason string `json:"reason"`
|
Reason string `json:"reason"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// FinancialAdjustmentRequest 按目标金额创建完成后的差额流水,至少提供一个金额。
|
||||||
|
type FinancialAdjustmentRequest struct {
|
||||||
|
ProfitAmountCent *int64 `json:"profit_amount_cent"`
|
||||||
|
SettleAmountCent *int64 `json:"settle_amount_cent"`
|
||||||
|
Reason string `json:"reason" binding:"required,max=255"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type FinancialAdjustmentSettlementRequest struct {
|
||||||
|
Remark string `json:"remark" binding:"max=255"`
|
||||||
|
}
|
||||||
|
|
||||||
type CancelRequest struct {
|
type CancelRequest struct {
|
||||||
Reason string `json:"reason" binding:"required"`
|
Reason string `json:"reason" binding:"required"`
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -112,6 +112,67 @@ func (h *Handler) UpdateProfit(c *gin.Context) {
|
|||||||
response.OK(c, item)
|
response.OK(c, item)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// CreateFinancialAdjustment 为已完成提号创建利润或打款调整。
|
||||||
|
func (h *Handler) CreateFinancialAdjustment(c *gin.Context) {
|
||||||
|
adminID, ok := currentAdminID(c)
|
||||||
|
if !ok {
|
||||||
|
response.Unauthorized(c, "缺少管理员上下文")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
id, ok := parseID(c)
|
||||||
|
if !ok {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
var req FinancialAdjustmentRequest
|
||||||
|
if err := c.ShouldBindJSON(&req); err != nil {
|
||||||
|
response.BadRequest(c, "调整金额或原因不正确")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
item, err := h.service.CreateFinancialAdjustment(c.Request.Context(), id, req, adminID, auditMeta(c))
|
||||||
|
if err != nil {
|
||||||
|
writePickupError(c, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
response.OK(c, item)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ListFinancialAdjustments 返回提号完成后的财务调整历史。
|
||||||
|
func (h *Handler) ListFinancialAdjustments(c *gin.Context) {
|
||||||
|
id, ok := parseID(c)
|
||||||
|
if !ok {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
items, err := h.service.ListFinancialAdjustments(c.Request.Context(), id)
|
||||||
|
if err != nil {
|
||||||
|
writePickupError(c, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
response.OK(c, items)
|
||||||
|
}
|
||||||
|
|
||||||
|
// SettleFinancialAdjustment 确认代管提号的补款或追回调整已线下处理。
|
||||||
|
func (h *Handler) SettleFinancialAdjustment(c *gin.Context) {
|
||||||
|
adminID, ok := currentAdminID(c)
|
||||||
|
if !ok {
|
||||||
|
response.Unauthorized(c, "缺少管理员上下文")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
id, ok := parseID(c)
|
||||||
|
if !ok {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
var req FinancialAdjustmentSettlementRequest
|
||||||
|
if err := c.ShouldBindJSON(&req); err != nil {
|
||||||
|
response.BadRequest(c, "确认备注格式不正确")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if err := h.service.SettleFinancialAdjustment(c.Request.Context(), id, req, adminID, auditMeta(c)); err != nil {
|
||||||
|
writePickupError(c, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
response.OK(c, gin.H{"settled": true})
|
||||||
|
}
|
||||||
|
|
||||||
// Cancel 取消提号(管理员)
|
// Cancel 取消提号(管理员)
|
||||||
func (h *Handler) Cancel(c *gin.Context) {
|
func (h *Handler) Cancel(c *gin.Context) {
|
||||||
adminID, ok := currentAdminID(c)
|
adminID, ok := currentAdminID(c)
|
||||||
@@ -221,6 +282,12 @@ func writePickupError(c *gin.Context, err error) {
|
|||||||
response.BadRequest(c, "利润金额不正确")
|
response.BadRequest(c, "利润金额不正确")
|
||||||
case errors.Is(err, ErrOfflineSettlementCannotMark):
|
case errors.Is(err, ErrOfflineSettlementCannotMark):
|
||||||
response.BadRequest(c, "当前提号不可确认线下结算")
|
response.BadRequest(c, "当前提号不可确认线下结算")
|
||||||
|
case errors.Is(err, ErrFinancialAdjustmentInvalid):
|
||||||
|
response.BadRequest(c, "调整金额、状态或原因不正确")
|
||||||
|
case errors.Is(err, ErrFinancialAdjustmentNotFound):
|
||||||
|
response.Error(c, http.StatusNotFound, "pickup_financial_adjustment_not_found", "财务调整记录不存在")
|
||||||
|
case errors.Is(err, ErrFinancialAdjustmentCannotSettle):
|
||||||
|
response.BadRequest(c, "当前调整不可确认处理")
|
||||||
default:
|
default:
|
||||||
response.Error(c, http.StatusInternalServerError, "pickup_error", err.Error())
|
response.Error(c, http.StatusInternalServerError, "pickup_error", err.Error())
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -316,6 +316,14 @@ func (r *Repository) MarkOfflineSettlement(ctx context.Context, pickupID uint64,
|
|||||||
pickup.OfflineSettlementStatus != OfflineSettlementStatusPending || pickup.SettleAmountCent <= 0 {
|
pickup.OfflineSettlementStatus != OfflineSettlementStatusPending || pickup.SettleAmountCent <= 0 {
|
||||||
return ErrOfflineSettlementCannotMark
|
return ErrOfflineSettlementCannotMark
|
||||||
}
|
}
|
||||||
|
pendingTotals, err := pendingPickupFinancialAdjustmentTotals(tx, pickup.ID)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
effectiveSettleAmountCent := pickup.SettleAmountCent + pendingTotals.SettleAdjustmentCent
|
||||||
|
if effectiveSettleAmountCent < 0 {
|
||||||
|
return ErrFinancialAdjustmentInvalid
|
||||||
|
}
|
||||||
|
|
||||||
now := time.Now()
|
now := time.Now()
|
||||||
pickup.OfflineSettlementStatus = OfflineSettlementStatusSettled
|
pickup.OfflineSettlementStatus = OfflineSettlementStatusSettled
|
||||||
@@ -325,6 +333,18 @@ func (r *Repository) MarkOfflineSettlement(ctx context.Context, pickupID uint64,
|
|||||||
if err := tx.Save(&pickup).Error; err != nil {
|
if err := tx.Save(&pickup).Error; err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
if pendingTotals.Count > 0 {
|
||||||
|
if err := tx.Model(&model.AdminPickupFinancialAdjustment{}).
|
||||||
|
Where("pickup_id = ? AND status IN ?", pickup.ID, []string{FinancialAdjustmentStatusPending, FinancialAdjustmentStatusRecovery}).
|
||||||
|
Updates(map[string]any{
|
||||||
|
"status": FinancialAdjustmentStatusSettled,
|
||||||
|
"settled_by": adminID,
|
||||||
|
"settled_at": now,
|
||||||
|
"settlement_remark": pickup.OfflineSettlementRemark,
|
||||||
|
}).Error; err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
bid := pickup.ID
|
bid := pickup.ID
|
||||||
return auditlog.Append(tx, auditlog.Entry{
|
return auditlog.Append(tx, auditlog.Entry{
|
||||||
@@ -336,7 +356,8 @@ func (r *Repository) MarkOfflineSettlement(ctx context.Context, pickupID uint64,
|
|||||||
Meta: meta,
|
Meta: meta,
|
||||||
Detail: map[string]any{
|
Detail: map[string]any{
|
||||||
"pickup_no": pickup.PickupNo,
|
"pickup_no": pickup.PickupNo,
|
||||||
"settle_amount_cent": pickup.SettleAmountCent,
|
"settle_amount_cent": effectiveSettleAmountCent,
|
||||||
|
"settled_adjustment_count": pendingTotals.Count,
|
||||||
"before_offline_settlement_status": OfflineSettlementStatusPending,
|
"before_offline_settlement_status": OfflineSettlementStatusPending,
|
||||||
"after_offline_settlement_status": pickup.OfflineSettlementStatus,
|
"after_offline_settlement_status": pickup.OfflineSettlementStatus,
|
||||||
"remark": pickup.OfflineSettlementRemark,
|
"remark": pickup.OfflineSettlementRemark,
|
||||||
@@ -394,6 +415,170 @@ func (r *Repository) UpdateProfit(ctx context.Context, pickupID uint64, req Upda
|
|||||||
return r.FindByID(ctx, pickupID)
|
return r.FindByID(ctx, pickupID)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// CreateFinancialAdjustment 为已完成提号追加财务差额,不改写完成时的原始金额。
|
||||||
|
func (r *Repository) CreateFinancialAdjustment(ctx context.Context, pickupID uint64, req FinancialAdjustmentRequest, adminID uint64, meta auditlog.Meta) (*PickupDTO, error) {
|
||||||
|
if r == nil || r.db == nil {
|
||||||
|
return nil, ErrDependencyUnavailable
|
||||||
|
}
|
||||||
|
err := r.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
|
||||||
|
var pickup model.AdminPickup
|
||||||
|
if err := tx.Clauses(clause.Locking{Strength: "UPDATE"}).First(&pickup, pickupID).Error; err != nil {
|
||||||
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||||
|
return ErrPickupNotFound
|
||||||
|
}
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if pickup.Status != StatusCompleted {
|
||||||
|
return ErrFinancialAdjustmentInvalid
|
||||||
|
}
|
||||||
|
|
||||||
|
totals, err := pickupFinancialAdjustmentTotals(tx, pickup.ID)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
profitDeltaCent := int64(0)
|
||||||
|
if req.ProfitAmountCent != nil {
|
||||||
|
profitDeltaCent = *req.ProfitAmountCent - (pickup.ProfitAmountCent + totals.ProfitAdjustmentCent)
|
||||||
|
}
|
||||||
|
settleDeltaCent := int64(0)
|
||||||
|
if req.SettleAmountCent != nil {
|
||||||
|
settleDeltaCent = *req.SettleAmountCent - (pickup.SettleAmountCent + totals.SettleAdjustmentCent)
|
||||||
|
}
|
||||||
|
if profitDeltaCent == 0 && settleDeltaCent == 0 {
|
||||||
|
return ErrFinancialAdjustmentInvalid
|
||||||
|
}
|
||||||
|
|
||||||
|
status := FinancialAdjustmentStatusSettled
|
||||||
|
if settleDeltaCent != 0 {
|
||||||
|
if pickupRequiresOfflineSettlement(pickup) && pickup.OfflineSettlementStatus == OfflineSettlementStatusPending {
|
||||||
|
// 原结算尚未付款时,差额随同一次线下打款处理。
|
||||||
|
status = FinancialAdjustmentStatusPending
|
||||||
|
} else if settleDeltaCent < 0 {
|
||||||
|
status = FinancialAdjustmentStatusRecovery
|
||||||
|
} else if pickupRequiresOfflineSettlement(pickup) {
|
||||||
|
status = FinancialAdjustmentStatusPending
|
||||||
|
}
|
||||||
|
}
|
||||||
|
adjustment := model.AdminPickupFinancialAdjustment{
|
||||||
|
PickupID: pickup.ID,
|
||||||
|
ProfitDeltaCent: profitDeltaCent,
|
||||||
|
SettleDeltaCent: settleDeltaCent,
|
||||||
|
SettlementMode: pickup.SettlementMode,
|
||||||
|
Status: status,
|
||||||
|
Reason: strings.TrimSpace(req.Reason),
|
||||||
|
CreatedBy: adminID,
|
||||||
|
}
|
||||||
|
if err := tx.Create(&adjustment).Error; err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// 站内钱包的补款在创建调整时立即到账;扣款统一转待追回,避免余额被扣成负数。
|
||||||
|
if pickup.SettlementMode == SettlementModeOwnerWallet && settleDeltaCent > 0 {
|
||||||
|
if err := wallet.AppendEntries(tx, wallet.Entry{
|
||||||
|
UserID: pickup.OwnerID,
|
||||||
|
Direction: "in",
|
||||||
|
AmountCent: settleDeltaCent,
|
||||||
|
BalanceType: "available",
|
||||||
|
BizType: "admin_pickup_adjustment",
|
||||||
|
BizNo: fmt.Sprintf("%s-A%d", pickup.PickupNo, adjustment.ID),
|
||||||
|
Remark: fmt.Sprintf("管理员提号结算补款:%s", pickup.PickupNo),
|
||||||
|
}); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bid := adjustment.ID
|
||||||
|
return auditlog.Append(tx, auditlog.Entry{
|
||||||
|
ActorType: "admin",
|
||||||
|
ActorID: adminID,
|
||||||
|
Action: "pickup_financial_adjustment_create",
|
||||||
|
BizType: "admin_pickup_financial_adjustment",
|
||||||
|
BizID: &bid,
|
||||||
|
Meta: meta,
|
||||||
|
Detail: map[string]any{
|
||||||
|
"pickup_id": pickup.ID,
|
||||||
|
"pickup_no": pickup.PickupNo,
|
||||||
|
"profit_delta_cent": profitDeltaCent,
|
||||||
|
"settle_delta_cent": settleDeltaCent,
|
||||||
|
"settlement_mode": pickup.SettlementMode,
|
||||||
|
"adjustment_status": status,
|
||||||
|
"reason": adjustment.Reason,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return r.FindByID(ctx, pickupID)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ListFinancialAdjustments 返回提号的不可变财务调整流水。
|
||||||
|
func (r *Repository) ListFinancialAdjustments(ctx context.Context, pickupID uint64) ([]FinancialAdjustmentDTO, error) {
|
||||||
|
if r == nil || r.db == nil {
|
||||||
|
return nil, ErrDependencyUnavailable
|
||||||
|
}
|
||||||
|
var count int64
|
||||||
|
if err := r.db.WithContext(ctx).Model(&model.AdminPickup{}).Where("id = ?", pickupID).Count(&count).Error; err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if count == 0 {
|
||||||
|
return nil, ErrPickupNotFound
|
||||||
|
}
|
||||||
|
items := make([]FinancialAdjustmentDTO, 0)
|
||||||
|
if err := r.db.WithContext(ctx).Model(&model.AdminPickupFinancialAdjustment{}).
|
||||||
|
Where("pickup_id = ?", pickupID).
|
||||||
|
Order("id DESC").
|
||||||
|
Find(&items).Error; err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return items, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// SettleFinancialAdjustment 确认平台代管的补款或追回调整已在线下处理。
|
||||||
|
func (r *Repository) SettleFinancialAdjustment(ctx context.Context, adjustmentID uint64, req FinancialAdjustmentSettlementRequest, adminID uint64, meta auditlog.Meta) error {
|
||||||
|
if r == nil || r.db == nil {
|
||||||
|
return ErrDependencyUnavailable
|
||||||
|
}
|
||||||
|
return r.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
|
||||||
|
var adjustment model.AdminPickupFinancialAdjustment
|
||||||
|
if err := tx.Clauses(clause.Locking{Strength: "UPDATE"}).First(&adjustment, adjustmentID).Error; err != nil {
|
||||||
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||||
|
return ErrFinancialAdjustmentNotFound
|
||||||
|
}
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if adjustment.Status != FinancialAdjustmentStatusPending && adjustment.Status != FinancialAdjustmentStatusRecovery {
|
||||||
|
return ErrFinancialAdjustmentCannotSettle
|
||||||
|
}
|
||||||
|
beforeStatus := adjustment.Status
|
||||||
|
now := time.Now()
|
||||||
|
adjustment.Status = FinancialAdjustmentStatusSettled
|
||||||
|
adjustment.SettledBy = &adminID
|
||||||
|
adjustment.SettledAt = &now
|
||||||
|
adjustment.SettlementRemark = strings.TrimSpace(req.Remark)
|
||||||
|
if err := tx.Save(&adjustment).Error; err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
bid := adjustment.ID
|
||||||
|
return auditlog.Append(tx, auditlog.Entry{
|
||||||
|
ActorType: "admin",
|
||||||
|
ActorID: adminID,
|
||||||
|
Action: "pickup_financial_adjustment_settle",
|
||||||
|
BizType: "admin_pickup_financial_adjustment",
|
||||||
|
BizID: &bid,
|
||||||
|
Meta: meta,
|
||||||
|
Detail: map[string]any{
|
||||||
|
"pickup_id": adjustment.PickupID,
|
||||||
|
"profit_delta_cent": adjustment.ProfitDeltaCent,
|
||||||
|
"settle_delta_cent": adjustment.SettleDeltaCent,
|
||||||
|
"before_status": beforeStatus,
|
||||||
|
"after_status": adjustment.Status,
|
||||||
|
"settlement_remark": adjustment.SettlementRemark,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
// Cancel 取消提号:恢复 listing 为 published + 解锁 in_transaction,账号恢复可租。
|
// Cancel 取消提号:恢复 listing 为 published + 解锁 in_transaction,账号恢复可租。
|
||||||
func (r *Repository) Cancel(ctx context.Context, pickupID uint64, reason string, adminID uint64, meta auditlog.Meta) error {
|
func (r *Repository) Cancel(ctx context.Context, pickupID uint64, reason string, adminID uint64, meta auditlog.Meta) error {
|
||||||
if r == nil || r.db == nil {
|
if r == nil || r.db == nil {
|
||||||
@@ -560,6 +745,8 @@ func (r *Repository) paginatePickups(db *gorm.DB, page, pageSize int, sellerView
|
|||||||
if sellerView {
|
if sellerView {
|
||||||
item.WebsiteProfitCent = 0
|
item.WebsiteProfitCent = 0
|
||||||
item.ProfitAmountCent = 0
|
item.ProfitAmountCent = 0
|
||||||
|
item.ProfitAdjustmentCent = 0
|
||||||
|
item.EffectiveProfitAmountCent = 0
|
||||||
item.BuyerRatio = 0
|
item.BuyerRatio = 0
|
||||||
item.ShopName = ""
|
item.ShopName = ""
|
||||||
item.SourceChannel = ""
|
item.SourceChannel = ""
|
||||||
@@ -618,7 +805,9 @@ func (r *Repository) ListAvailableListings(ctx context.Context, query AvailableL
|
|||||||
|
|
||||||
func (r *Repository) baseQuery(ctx context.Context) *gorm.DB {
|
func (r *Repository) baseQuery(ctx context.Context) *gorm.DB {
|
||||||
return r.db.WithContext(ctx).Table("admin_pickups AS p").
|
return r.db.WithContext(ctx).Table("admin_pickups AS p").
|
||||||
Select("p.*, l.listing_no, a.title AS account_title, a.server_region, a.login_platform, owner.phone AS owner_phone").
|
Select(`p.*, l.listing_no, a.title AS account_title, a.server_region, a.login_platform, owner.phone AS owner_phone,
|
||||||
|
COALESCE((SELECT SUM(fa.profit_delta_cent) FROM admin_pickup_financial_adjustments AS fa WHERE fa.pickup_id = p.id), 0) AS profit_adjustment_cent,
|
||||||
|
COALESCE((SELECT SUM(fa.settle_delta_cent) FROM admin_pickup_financial_adjustments AS fa WHERE fa.pickup_id = p.id), 0) AS settle_adjustment_cent`).
|
||||||
Joins("JOIN rental_listings AS l ON l.id = p.listing_id").
|
Joins("JOIN rental_listings AS l ON l.id = p.listing_id").
|
||||||
Joins("JOIN game_accounts AS a ON a.id = p.account_id").
|
Joins("JOIN game_accounts AS a ON a.id = p.account_id").
|
||||||
Joins("JOIN users AS owner ON owner.id = p.owner_id")
|
Joins("JOIN users AS owner ON owner.id = p.owner_id")
|
||||||
@@ -626,52 +815,87 @@ func (r *Repository) baseQuery(ctx context.Context) *gorm.DB {
|
|||||||
|
|
||||||
type pickupRow struct {
|
type pickupRow struct {
|
||||||
model.AdminPickup
|
model.AdminPickup
|
||||||
ListingNo string
|
ListingNo string
|
||||||
AccountTitle string
|
AccountTitle string
|
||||||
ServerRegion string
|
ServerRegion string
|
||||||
LoginPlatform string
|
LoginPlatform string
|
||||||
OwnerPhone string
|
OwnerPhone string
|
||||||
|
ProfitAdjustmentCent int64
|
||||||
|
SettleAdjustmentCent int64
|
||||||
}
|
}
|
||||||
|
|
||||||
func (row pickupRow) toDTO() PickupDTO {
|
func (row pickupRow) toDTO() PickupDTO {
|
||||||
return PickupDTO{
|
return PickupDTO{
|
||||||
ID: row.ID,
|
ID: row.ID,
|
||||||
PickupNo: row.PickupNo,
|
PickupNo: row.PickupNo,
|
||||||
ListingID: row.ListingID,
|
ListingID: row.ListingID,
|
||||||
ListingNo: row.ListingNo,
|
ListingNo: row.ListingNo,
|
||||||
AccountID: row.AccountID,
|
AccountID: row.AccountID,
|
||||||
AccountTitle: row.AccountTitle,
|
AccountTitle: row.AccountTitle,
|
||||||
ServerRegion: row.ServerRegion,
|
ServerRegion: row.ServerRegion,
|
||||||
LoginPlatform: row.LoginPlatform,
|
LoginPlatform: row.LoginPlatform,
|
||||||
OwnerID: row.OwnerID,
|
OwnerID: row.OwnerID,
|
||||||
OwnerPhone: row.OwnerPhone,
|
OwnerPhone: row.OwnerPhone,
|
||||||
AdminID: row.AdminID,
|
AdminID: row.AdminID,
|
||||||
Platform: row.Platform,
|
Platform: row.Platform,
|
||||||
ShopName: row.ShopName,
|
ShopName: row.ShopName,
|
||||||
AccountSource: row.AccountSource,
|
AccountSource: row.AccountSource,
|
||||||
SourceChannel: row.SourceChannel,
|
SourceChannel: row.SourceChannel,
|
||||||
SettlementMode: row.SettlementMode,
|
SettlementMode: row.SettlementMode,
|
||||||
ListingPriceCent: row.ListingPriceCent,
|
ListingPriceCent: row.ListingPriceCent,
|
||||||
OwnerPriceCent: row.OwnerPriceCent,
|
OwnerPriceCent: row.OwnerPriceCent,
|
||||||
WebsiteProfitCent: row.WebsiteProfitCent,
|
WebsiteProfitCent: row.WebsiteProfitCent,
|
||||||
ProfitAmountCent: row.ProfitAmountCent,
|
ProfitAmountCent: row.ProfitAmountCent,
|
||||||
SellerRatio: row.SellerRatio,
|
ProfitAdjustmentCent: row.ProfitAdjustmentCent,
|
||||||
BuyerRatio: row.BuyerRatio,
|
EffectiveProfitAmountCent: row.ProfitAmountCent + row.ProfitAdjustmentCent,
|
||||||
AccountSnapshot: row.AccountSnapshot,
|
SellerRatio: row.SellerRatio,
|
||||||
SettleAmountCent: row.SettleAmountCent,
|
BuyerRatio: row.BuyerRatio,
|
||||||
Status: row.Status,
|
AccountSnapshot: row.AccountSnapshot,
|
||||||
OfflineSettlementStatus: row.OfflineSettlementStatus,
|
SettleAmountCent: row.SettleAmountCent,
|
||||||
OfflineSettlementRemark: row.OfflineSettlementRemark,
|
SettleAdjustmentCent: row.SettleAdjustmentCent,
|
||||||
OfflineSettledBy: row.OfflineSettledBy,
|
EffectiveSettleAmountCent: row.SettleAmountCent + row.SettleAdjustmentCent,
|
||||||
OfflineSettledAt: row.OfflineSettledAt,
|
Status: row.Status,
|
||||||
Remark: row.Remark,
|
OfflineSettlementStatus: row.OfflineSettlementStatus,
|
||||||
CompleteRemark: row.CompleteRemark,
|
OfflineSettlementRemark: row.OfflineSettlementRemark,
|
||||||
CreatedAt: row.CreatedAt,
|
OfflineSettledBy: row.OfflineSettledBy,
|
||||||
CompletedAt: row.CompletedAt,
|
OfflineSettledAt: row.OfflineSettledAt,
|
||||||
CancelledAt: row.CancelledAt,
|
Remark: row.Remark,
|
||||||
|
CompleteRemark: row.CompleteRemark,
|
||||||
|
CreatedAt: row.CreatedAt,
|
||||||
|
CompletedAt: row.CompletedAt,
|
||||||
|
CancelledAt: row.CancelledAt,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type pickupAdjustmentTotals struct {
|
||||||
|
ProfitAdjustmentCent int64
|
||||||
|
SettleAdjustmentCent int64
|
||||||
|
}
|
||||||
|
|
||||||
|
type pendingPickupAdjustmentTotals struct {
|
||||||
|
SettleAdjustmentCent int64
|
||||||
|
Count int64
|
||||||
|
}
|
||||||
|
|
||||||
|
func pickupFinancialAdjustmentTotals(db *gorm.DB, pickupID uint64) (pickupAdjustmentTotals, error) {
|
||||||
|
var totals pickupAdjustmentTotals
|
||||||
|
err := db.Model(&model.AdminPickupFinancialAdjustment{}).
|
||||||
|
Select(`COALESCE(SUM(profit_delta_cent), 0) AS profit_adjustment_cent,
|
||||||
|
COALESCE(SUM(settle_delta_cent), 0) AS settle_adjustment_cent`).
|
||||||
|
Where("pickup_id = ?", pickupID).
|
||||||
|
Scan(&totals).Error
|
||||||
|
return totals, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func pendingPickupFinancialAdjustmentTotals(db *gorm.DB, pickupID uint64) (pendingPickupAdjustmentTotals, error) {
|
||||||
|
var totals pendingPickupAdjustmentTotals
|
||||||
|
err := db.Model(&model.AdminPickupFinancialAdjustment{}).
|
||||||
|
Select(`COALESCE(SUM(settle_delta_cent), 0) AS settle_adjustment_cent, COUNT(id) AS count`).
|
||||||
|
Where("pickup_id = ? AND status IN ?", pickupID, []string{FinancialAdjustmentStatusPending, FinancialAdjustmentStatusRecovery}).
|
||||||
|
Scan(&totals).Error
|
||||||
|
return totals, err
|
||||||
|
}
|
||||||
|
|
||||||
func pickupSettlementMode(listingMode, accountSource string) string {
|
func pickupSettlementMode(listingMode, accountSource string) string {
|
||||||
if listingMode == SettlementModePlatformManaged || accountSource == AccountSourceExternalPlatformManaged {
|
if listingMode == SettlementModePlatformManaged || accountSource == AccountSourceExternalPlatformManaged {
|
||||||
return SettlementModePlatformManaged
|
return SettlementModePlatformManaged
|
||||||
|
|||||||
@@ -380,6 +380,161 @@ func TestRepositoryCompletePlatformManagedPickupUsesOfflineSettlement(t *testing
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestRepositoryFinancialAdjustmentCreditsInternalPickupWallet(t *testing.T) {
|
||||||
|
repo, db := newPickupTestRepo(t)
|
||||||
|
listing := seedAvailableListing(t, db, "LST-ADJUST-INTERNAL", "站内补款账号", false)
|
||||||
|
pickup, err := repo.Create(t.Context(), CreateRequest{ListingID: listing.ID, ProfitAmountCent: 1000}, 99, auditlog.Meta{})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Create() error = %v", err)
|
||||||
|
}
|
||||||
|
if _, err = repo.Complete(t.Context(), pickup.ID, CompleteRequest{SettleAmountCent: 12000}, 99, auditlog.Meta{}); err != nil {
|
||||||
|
t.Fatalf("Complete() error = %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
updated, err := repo.CreateFinancialAdjustment(t.Context(), pickup.ID, FinancialAdjustmentRequest{
|
||||||
|
ProfitAmountCent: ptrInt64(1500),
|
||||||
|
SettleAmountCent: ptrInt64(13500),
|
||||||
|
Reason: "补录实际成交金额",
|
||||||
|
}, 100, auditlog.Meta{})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("CreateFinancialAdjustment() error = %v", err)
|
||||||
|
}
|
||||||
|
assertInt64(t, "原始利润不变", updated.ProfitAmountCent, 1000)
|
||||||
|
assertInt64(t, "有效利润", updated.EffectiveProfitAmountCent, 1500)
|
||||||
|
assertInt64(t, "原始结算额不变", updated.SettleAmountCent, 12000)
|
||||||
|
assertInt64(t, "有效结算额", updated.EffectiveSettleAmountCent, 13500)
|
||||||
|
|
||||||
|
var adjustment model.AdminPickupFinancialAdjustment
|
||||||
|
if err := db.Where("pickup_id = ?", pickup.ID).First(&adjustment).Error; err != nil {
|
||||||
|
t.Fatalf("查询调整流水失败: %v", err)
|
||||||
|
}
|
||||||
|
if adjustment.Status != FinancialAdjustmentStatusSettled || adjustment.ProfitDeltaCent != 500 || adjustment.SettleDeltaCent != 1500 {
|
||||||
|
t.Fatalf("调整流水 = %+v", adjustment)
|
||||||
|
}
|
||||||
|
var ledger model.WalletLedger
|
||||||
|
if err := db.Where("biz_type = ?", "admin_pickup_adjustment").First(&ledger).Error; err != nil {
|
||||||
|
t.Fatalf("站内补款应生成钱包流水: %v", err)
|
||||||
|
}
|
||||||
|
assertInt64(t, "站内补款钱包入账", ledger.AmountCent, 1500)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRepositoryFinancialAdjustmentUsesRecoveryForInternalReduction(t *testing.T) {
|
||||||
|
repo, db := newPickupTestRepo(t)
|
||||||
|
listing := seedAvailableListing(t, db, "LST-ADJUST-RECOVERY", "站内追回账号", false)
|
||||||
|
pickup, err := repo.Create(t.Context(), CreateRequest{ListingID: listing.ID}, 99, auditlog.Meta{})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Create() error = %v", err)
|
||||||
|
}
|
||||||
|
if _, err = repo.Complete(t.Context(), pickup.ID, CompleteRequest{SettleAmountCent: 12000}, 99, auditlog.Meta{}); err != nil {
|
||||||
|
t.Fatalf("Complete() error = %v", err)
|
||||||
|
}
|
||||||
|
updated, err := repo.CreateFinancialAdjustment(t.Context(), pickup.ID, FinancialAdjustmentRequest{
|
||||||
|
SettleAmountCent: ptrInt64(10000),
|
||||||
|
Reason: "实际应结算金额减少",
|
||||||
|
}, 100, auditlog.Meta{})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("CreateFinancialAdjustment() error = %v", err)
|
||||||
|
}
|
||||||
|
assertInt64(t, "追回后的有效结算额", updated.EffectiveSettleAmountCent, 10000)
|
||||||
|
var adjustment model.AdminPickupFinancialAdjustment
|
||||||
|
if err := db.Where("pickup_id = ?", pickup.ID).First(&adjustment).Error; err != nil {
|
||||||
|
t.Fatalf("查询追回调整失败: %v", err)
|
||||||
|
}
|
||||||
|
if adjustment.Status != FinancialAdjustmentStatusRecovery || adjustment.SettleDeltaCent != -2000 {
|
||||||
|
t.Fatalf("追回调整 = %+v", adjustment)
|
||||||
|
}
|
||||||
|
var ledgerCount int64
|
||||||
|
if err := db.Model(&model.WalletLedger{}).Where("biz_type = ?", "admin_pickup_adjustment").Count(&ledgerCount).Error; err != nil {
|
||||||
|
t.Fatalf("统计补款流水失败: %v", err)
|
||||||
|
}
|
||||||
|
if ledgerCount != 0 {
|
||||||
|
t.Fatalf("待追回不能直接扣钱包, got %d 条流水", ledgerCount)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRepositoryFinancialAdjustmentSettlesPlatformManagedPickupSeparately(t *testing.T) {
|
||||||
|
repo, db := newPickupTestRepo(t)
|
||||||
|
listing := seedAvailableListing(t, db, "LST-ADJUST-PLATFORM", "代管调整账号", true)
|
||||||
|
if err := db.Create(&model.ListingUpload{UploaderName: "外部号主", ListingID: &listing.ID, Status: "listing_created"}).Error; err != nil {
|
||||||
|
t.Fatalf("创建外部上传记录失败: %v", err)
|
||||||
|
}
|
||||||
|
pickup, err := repo.Create(t.Context(), CreateRequest{ListingID: listing.ID}, 99, auditlog.Meta{})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Create() error = %v", err)
|
||||||
|
}
|
||||||
|
if _, err = repo.Complete(t.Context(), pickup.ID, CompleteRequest{SettleAmountCent: 12000}, 99, auditlog.Meta{}); err != nil {
|
||||||
|
t.Fatalf("Complete() error = %v", err)
|
||||||
|
}
|
||||||
|
if _, err = repo.MarkOfflineSettlement(t.Context(), pickup.ID, OfflineSettlementRequest{}, 100, auditlog.Meta{}); err != nil {
|
||||||
|
t.Fatalf("MarkOfflineSettlement() error = %v", err)
|
||||||
|
}
|
||||||
|
updated, err := repo.CreateFinancialAdjustment(t.Context(), pickup.ID, FinancialAdjustmentRequest{
|
||||||
|
ProfitAmountCent: ptrInt64(2000),
|
||||||
|
SettleAmountCent: ptrInt64(13500),
|
||||||
|
Reason: "补录代管打款",
|
||||||
|
}, 101, auditlog.Meta{})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("CreateFinancialAdjustment() error = %v", err)
|
||||||
|
}
|
||||||
|
if updated.OfflineSettlementStatus != OfflineSettlementStatusSettled {
|
||||||
|
t.Fatalf("原线下结算状态不应被重置: %q", updated.OfflineSettlementStatus)
|
||||||
|
}
|
||||||
|
adjustments, err := repo.ListFinancialAdjustments(t.Context(), pickup.ID)
|
||||||
|
if err != nil || len(adjustments) != 1 {
|
||||||
|
t.Fatalf("ListFinancialAdjustments() = %+v, %v", adjustments, err)
|
||||||
|
}
|
||||||
|
if adjustments[0].Status != FinancialAdjustmentStatusPending {
|
||||||
|
t.Fatalf("代管补款状态 = %q, want pending", adjustments[0].Status)
|
||||||
|
}
|
||||||
|
if err := repo.SettleFinancialAdjustment(t.Context(), adjustments[0].ID, FinancialAdjustmentSettlementRequest{Remark: "已补款"}, 102, auditlog.Meta{}); err != nil {
|
||||||
|
t.Fatalf("SettleFinancialAdjustment() error = %v", err)
|
||||||
|
}
|
||||||
|
adjustments, err = repo.ListFinancialAdjustments(t.Context(), pickup.ID)
|
||||||
|
if err != nil || adjustments[0].Status != FinancialAdjustmentStatusSettled || adjustments[0].SettledBy == nil || *adjustments[0].SettledBy != 102 {
|
||||||
|
t.Fatalf("确认后的调整流水 = %+v, %v", adjustments, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRepositoryOfflineSettlementProcessesPendingPickupAdjustments(t *testing.T) {
|
||||||
|
repo, db := newPickupTestRepo(t)
|
||||||
|
listing := seedAvailableListing(t, db, "LST-ADJUST-WITH-SETTLEMENT", "代管合并结算账号", true)
|
||||||
|
if err := db.Create(&model.ListingUpload{UploaderName: "外部号主", ListingID: &listing.ID, Status: "listing_created"}).Error; err != nil {
|
||||||
|
t.Fatalf("创建外部上传记录失败: %v", err)
|
||||||
|
}
|
||||||
|
pickup, err := repo.Create(t.Context(), CreateRequest{ListingID: listing.ID}, 99, auditlog.Meta{})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Create() error = %v", err)
|
||||||
|
}
|
||||||
|
if _, err = repo.Complete(t.Context(), pickup.ID, CompleteRequest{SettleAmountCent: 12000}, 99, auditlog.Meta{}); err != nil {
|
||||||
|
t.Fatalf("Complete() error = %v", err)
|
||||||
|
}
|
||||||
|
if _, err = repo.CreateFinancialAdjustment(t.Context(), pickup.ID, FinancialAdjustmentRequest{
|
||||||
|
SettleAmountCent: ptrInt64(10000),
|
||||||
|
Reason: "付款前修正应付金额",
|
||||||
|
}, 100, auditlog.Meta{}); err != nil {
|
||||||
|
t.Fatalf("CreateFinancialAdjustment() error = %v", err)
|
||||||
|
}
|
||||||
|
adjustments, err := repo.ListFinancialAdjustments(t.Context(), pickup.ID)
|
||||||
|
if err != nil || len(adjustments) != 1 || adjustments[0].Status != FinancialAdjustmentStatusPending {
|
||||||
|
t.Fatalf("结算前调整流水 = %+v, %v", adjustments, err)
|
||||||
|
}
|
||||||
|
settled, err := repo.MarkOfflineSettlement(t.Context(), pickup.ID, OfflineSettlementRequest{Remark: "按修正后金额打款"}, 101, auditlog.Meta{})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("MarkOfflineSettlement() error = %v", err)
|
||||||
|
}
|
||||||
|
if settled.OfflineSettlementStatus != OfflineSettlementStatusSettled || settled.EffectiveSettleAmountCent != 10000 {
|
||||||
|
t.Fatalf("合并线下结算结果 = %+v", settled)
|
||||||
|
}
|
||||||
|
adjustments, err = repo.ListFinancialAdjustments(t.Context(), pickup.ID)
|
||||||
|
if err != nil || adjustments[0].Status != FinancialAdjustmentStatusSettled || adjustments[0].SettledBy == nil || *adjustments[0].SettledBy != 101 {
|
||||||
|
t.Fatalf("合并结算后的调整流水 = %+v, %v", adjustments, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func ptrInt64(value int64) *int64 {
|
||||||
|
return &value
|
||||||
|
}
|
||||||
|
|
||||||
func assertInt64(t *testing.T, name string, got, want int64) {
|
func assertInt64(t *testing.T, name string, got, want int64) {
|
||||||
t.Helper()
|
t.Helper()
|
||||||
if got != want {
|
if got != want {
|
||||||
@@ -409,6 +564,7 @@ func newPickupTestRepo(t *testing.T) (*Repository, *gorm.DB) {
|
|||||||
&model.RentalListing{},
|
&model.RentalListing{},
|
||||||
&model.ListingUpload{},
|
&model.ListingUpload{},
|
||||||
&model.AdminPickup{},
|
&model.AdminPickup{},
|
||||||
|
&model.AdminPickupFinancialAdjustment{},
|
||||||
&model.WalletAccount{},
|
&model.WalletAccount{},
|
||||||
&model.WalletLedger{},
|
&model.WalletLedger{},
|
||||||
&model.AuditLog{},
|
&model.AuditLog{},
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ package pickup
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"strings"
|
||||||
|
|
||||||
"hfb_sys/backend/internal/auditlog"
|
"hfb_sys/backend/internal/auditlog"
|
||||||
)
|
)
|
||||||
@@ -63,6 +64,39 @@ func (s *Service) UpdateProfit(ctx context.Context, pickupID uint64, req UpdateP
|
|||||||
return s.repo.UpdateProfit(ctx, pickupID, req, adminID, meta)
|
return s.repo.UpdateProfit(ctx, pickupID, req, adminID, meta)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *Service) CreateFinancialAdjustment(ctx context.Context, pickupID uint64, req FinancialAdjustmentRequest, adminID uint64, meta auditlog.Meta) (*PickupDTO, error) {
|
||||||
|
if s.repo == nil {
|
||||||
|
return nil, ErrDependencyUnavailable
|
||||||
|
}
|
||||||
|
if pickupID == 0 || (req.ProfitAmountCent == nil && req.SettleAmountCent == nil) || strings.TrimSpace(req.Reason) == "" {
|
||||||
|
return nil, ErrFinancialAdjustmentInvalid
|
||||||
|
}
|
||||||
|
if (req.ProfitAmountCent != nil && *req.ProfitAmountCent < 0) || (req.SettleAmountCent != nil && *req.SettleAmountCent < 0) {
|
||||||
|
return nil, ErrFinancialAdjustmentInvalid
|
||||||
|
}
|
||||||
|
return s.repo.CreateFinancialAdjustment(ctx, pickupID, req, adminID, meta)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Service) ListFinancialAdjustments(ctx context.Context, pickupID uint64) ([]FinancialAdjustmentDTO, error) {
|
||||||
|
if s.repo == nil {
|
||||||
|
return nil, ErrDependencyUnavailable
|
||||||
|
}
|
||||||
|
if pickupID == 0 {
|
||||||
|
return nil, ErrPickupNotFound
|
||||||
|
}
|
||||||
|
return s.repo.ListFinancialAdjustments(ctx, pickupID)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Service) SettleFinancialAdjustment(ctx context.Context, adjustmentID uint64, req FinancialAdjustmentSettlementRequest, adminID uint64, meta auditlog.Meta) error {
|
||||||
|
if s.repo == nil {
|
||||||
|
return ErrDependencyUnavailable
|
||||||
|
}
|
||||||
|
if adjustmentID == 0 {
|
||||||
|
return ErrFinancialAdjustmentNotFound
|
||||||
|
}
|
||||||
|
return s.repo.SettleFinancialAdjustment(ctx, adjustmentID, req, adminID, meta)
|
||||||
|
}
|
||||||
|
|
||||||
func (s *Service) Cancel(ctx context.Context, pickupID uint64, reason string, adminID uint64, meta auditlog.Meta) error {
|
func (s *Service) Cancel(ctx context.Context, pickupID uint64, reason string, adminID uint64, meta auditlog.Meta) error {
|
||||||
if s.repo == nil {
|
if s.repo == nil {
|
||||||
return ErrDependencyUnavailable
|
return ErrDependencyUnavailable
|
||||||
|
|||||||
@@ -617,11 +617,14 @@ func New(cfg config.Config, deps Dependencies, logger *zap.Logger) *gin.Engine {
|
|||||||
adminRoutes.GET("/pickups", requirePerm("order:pickup"), pickupHandler.List)
|
adminRoutes.GET("/pickups", requirePerm("order:pickup"), pickupHandler.List)
|
||||||
adminRoutes.GET("/pickups/shop-options", requirePerm("order:pickup"), pickupHandler.ShopOptions)
|
adminRoutes.GET("/pickups/shop-options", requirePerm("order:pickup"), pickupHandler.ShopOptions)
|
||||||
adminRoutes.GET("/pickups/available-listings", requirePerm("order:pickup"), pickupHandler.AvailableListings)
|
adminRoutes.GET("/pickups/available-listings", requirePerm("order:pickup"), pickupHandler.AvailableListings)
|
||||||
|
adminRoutes.GET("/pickups/:id/financial-adjustments", requirePerm("order:pickup"), pickupHandler.ListFinancialAdjustments)
|
||||||
|
adminRoutes.POST("/pickups/:id/financial-adjustments", requirePerm("order:pickup"), pickupHandler.CreateFinancialAdjustment)
|
||||||
adminRoutes.GET("/pickups/:id", requirePerm("order:pickup"), pickupHandler.Detail)
|
adminRoutes.GET("/pickups/:id", requirePerm("order:pickup"), pickupHandler.Detail)
|
||||||
adminRoutes.POST("/pickups/:id/complete", requirePerm("order:pickup"), pickupHandler.Complete)
|
adminRoutes.POST("/pickups/:id/complete", requirePerm("order:pickup"), pickupHandler.Complete)
|
||||||
adminRoutes.POST("/pickups/:id/offline-settlement", requirePerm("order:pickup"), pickupHandler.MarkOfflineSettlement)
|
adminRoutes.POST("/pickups/:id/offline-settlement", requirePerm("order:pickup"), pickupHandler.MarkOfflineSettlement)
|
||||||
adminRoutes.PUT("/pickups/:id/profit", requirePerm("order:pickup"), pickupHandler.UpdateProfit)
|
adminRoutes.PUT("/pickups/:id/profit", requirePerm("order:pickup"), pickupHandler.UpdateProfit)
|
||||||
adminRoutes.POST("/pickups/:id/cancel", requirePerm("order:pickup"), pickupHandler.Cancel)
|
adminRoutes.POST("/pickups/:id/cancel", requirePerm("order:pickup"), pickupHandler.Cancel)
|
||||||
|
adminRoutes.POST("/pickup-financial-adjustments/:id/settle", requirePerm("order:pickup"), pickupHandler.SettleFinancialAdjustment)
|
||||||
|
|
||||||
adminRoutes.GET("/listings", requirePerm("listing:view"), listingHandler.ListAdmin)
|
adminRoutes.GET("/listings", requirePerm("listing:view"), listingHandler.ListAdmin)
|
||||||
adminRoutes.GET("/listings/pending", requirePerm("listing:approve"), listingHandler.ListPendingReview)
|
adminRoutes.GET("/listings/pending", requirePerm("listing:approve"), listingHandler.ListPendingReview)
|
||||||
|
|||||||
@@ -0,0 +1,23 @@
|
|||||||
|
-- +goose Up
|
||||||
|
|
||||||
|
CREATE TABLE admin_pickup_financial_adjustments (
|
||||||
|
id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
|
||||||
|
pickup_id BIGINT UNSIGNED NOT NULL COMMENT '关联提号ID',
|
||||||
|
profit_delta_cent BIGINT NOT NULL DEFAULT 0 COMMENT '利润调整差额(分,可正负)',
|
||||||
|
settle_delta_cent BIGINT NOT NULL DEFAULT 0 COMMENT '打款调整差额(分,可正负)',
|
||||||
|
settlement_mode VARCHAR(32) NOT NULL COMMENT '调整时结算模式快照',
|
||||||
|
status VARCHAR(24) NOT NULL COMMENT '打款调整状态: settled已处理/pending待付款/recovery_pending待追回',
|
||||||
|
reason VARCHAR(255) NOT NULL COMMENT '调整原因',
|
||||||
|
created_by BIGINT UNSIGNED NOT NULL COMMENT '创建管理员ID',
|
||||||
|
settled_by BIGINT UNSIGNED NULL COMMENT '确认付款或追回的管理员ID',
|
||||||
|
settled_at DATETIME NULL COMMENT '确认付款或追回时间',
|
||||||
|
settlement_remark VARCHAR(255) NOT NULL DEFAULT '' COMMENT '确认备注',
|
||||||
|
created_at DATETIME NOT NULL,
|
||||||
|
PRIMARY KEY (id),
|
||||||
|
KEY idx_pickup_financial_adjustment_pickup (pickup_id, created_at),
|
||||||
|
KEY idx_pickup_financial_adjustment_status (status, created_at)
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='管理员提号完成后的财务调整流水';
|
||||||
|
|
||||||
|
-- +goose Down
|
||||||
|
|
||||||
|
DROP TABLE IF EXISTS admin_pickup_financial_adjustments;
|
||||||
@@ -4,6 +4,7 @@ import type { ApiResponse, PaginatedResult } from '@/shared/types/types'
|
|||||||
export type PickupAccountSource = 'internal' | 'external_platform_managed'
|
export type PickupAccountSource = 'internal' | 'external_platform_managed'
|
||||||
export type PickupSettlementMode = 'owner_wallet' | 'platform_managed'
|
export type PickupSettlementMode = 'owner_wallet' | 'platform_managed'
|
||||||
export type PickupOfflineSettlementStatus = 'none' | 'pending' | 'settled'
|
export type PickupOfflineSettlementStatus = 'none' | 'pending' | 'settled'
|
||||||
|
export type PickupFinancialAdjustmentStatus = 'settled' | 'pending' | 'recovery_pending'
|
||||||
export type AvailableListingSourceType = '' | 'external' | 'internal'
|
export type AvailableListingSourceType = '' | 'external' | 'internal'
|
||||||
|
|
||||||
export interface AdminPickup {
|
export interface AdminPickup {
|
||||||
@@ -27,10 +28,14 @@ export interface AdminPickup {
|
|||||||
owner_price_cent: number
|
owner_price_cent: number
|
||||||
website_profit_cent: number
|
website_profit_cent: number
|
||||||
profit_amount_cent: number
|
profit_amount_cent: number
|
||||||
|
profit_adjustment_cent: number
|
||||||
|
effective_profit_amount_cent: number
|
||||||
seller_ratio: number
|
seller_ratio: number
|
||||||
buyer_ratio: number
|
buyer_ratio: number
|
||||||
account_snapshot?: Record<string, unknown>
|
account_snapshot?: Record<string, unknown>
|
||||||
settle_amount_cent: number
|
settle_amount_cent: number
|
||||||
|
settle_adjustment_cent: number
|
||||||
|
effective_settle_amount_cent: number
|
||||||
status: string
|
status: string
|
||||||
offline_settlement_status: PickupOfflineSettlementStatus
|
offline_settlement_status: PickupOfflineSettlementStatus
|
||||||
offline_settlement_remark: string
|
offline_settlement_remark: string
|
||||||
@@ -43,6 +48,21 @@ export interface AdminPickup {
|
|||||||
cancelled_at?: string
|
cancelled_at?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface AdminPickupFinancialAdjustment {
|
||||||
|
id: number
|
||||||
|
pickup_id: number
|
||||||
|
profit_delta_cent: number
|
||||||
|
settle_delta_cent: number
|
||||||
|
settlement_mode: PickupSettlementMode
|
||||||
|
status: PickupFinancialAdjustmentStatus
|
||||||
|
reason: string
|
||||||
|
created_by: number
|
||||||
|
settled_by?: number
|
||||||
|
settled_at?: string
|
||||||
|
settlement_remark: string
|
||||||
|
created_at: string
|
||||||
|
}
|
||||||
|
|
||||||
export interface AvailableListing {
|
export interface AvailableListing {
|
||||||
id: number
|
id: number
|
||||||
listing_no: string
|
listing_no: string
|
||||||
@@ -90,6 +110,16 @@ export interface AdminPickupOfflineSettlementRequest {
|
|||||||
remark?: string
|
remark?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface AdminPickupFinancialAdjustmentRequest {
|
||||||
|
profit_amount_cent?: number
|
||||||
|
settle_amount_cent?: number
|
||||||
|
reason: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AdminPickupFinancialAdjustmentSettlementRequest {
|
||||||
|
remark?: string
|
||||||
|
}
|
||||||
|
|
||||||
export interface AdminPickupListQuery {
|
export interface AdminPickupListQuery {
|
||||||
page?: number
|
page?: number
|
||||||
page_size?: number
|
page_size?: number
|
||||||
@@ -177,6 +207,35 @@ export async function updateAdminPickupProfit(id: number, req: AdminPickupProfit
|
|||||||
return data.data
|
return data.data
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function createAdminPickupFinancialAdjustment(
|
||||||
|
id: number,
|
||||||
|
req: AdminPickupFinancialAdjustmentRequest
|
||||||
|
) {
|
||||||
|
const { data } = await apiClient.post<ApiResponse<AdminPickup>>(
|
||||||
|
`/admin/pickups/${id}/financial-adjustments`,
|
||||||
|
req
|
||||||
|
)
|
||||||
|
return data.data
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function fetchAdminPickupFinancialAdjustments(id: string | number) {
|
||||||
|
const { data } = await apiClient.get<ApiResponse<AdminPickupFinancialAdjustment[]>>(
|
||||||
|
`/admin/pickups/${id}/financial-adjustments`
|
||||||
|
)
|
||||||
|
return Array.isArray(data.data) ? data.data : []
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function settleAdminPickupFinancialAdjustment(
|
||||||
|
id: number,
|
||||||
|
req: AdminPickupFinancialAdjustmentSettlementRequest = {}
|
||||||
|
) {
|
||||||
|
const { data } = await apiClient.post<ApiResponse<{ settled: boolean }>>(
|
||||||
|
`/admin/pickup-financial-adjustments/${id}/settle`,
|
||||||
|
req
|
||||||
|
)
|
||||||
|
return data.data
|
||||||
|
}
|
||||||
|
|
||||||
export async function cancelAdminPickup(id: number, reason: string) {
|
export async function cancelAdminPickup(id: number, reason: string) {
|
||||||
const { data } = await apiClient.post<ApiResponse<{ cancelled: boolean }>>(
|
const { data } = await apiClient.post<ApiResponse<{ cancelled: boolean }>>(
|
||||||
`/admin/pickups/${id}/cancel`,
|
`/admin/pickups/${id}/cancel`,
|
||||||
|
|||||||
@@ -1,13 +1,17 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { computed, onMounted, reactive, ref } from 'vue'
|
import { computed, onMounted, reactive, ref } from 'vue'
|
||||||
import { EditPen } from '@element-plus/icons-vue'
|
import { EditPen } from '@element-plus/icons-vue'
|
||||||
import { ElMessage } from 'element-plus'
|
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||||
import { useRoute } from 'vue-router'
|
import { useRoute } from 'vue-router'
|
||||||
|
|
||||||
import {
|
import {
|
||||||
fetchAdminPickup,
|
fetchAdminPickup,
|
||||||
|
fetchAdminPickupFinancialAdjustments,
|
||||||
|
createAdminPickupFinancialAdjustment,
|
||||||
|
settleAdminPickupFinancialAdjustment,
|
||||||
updateAdminPickupProfit,
|
updateAdminPickupProfit,
|
||||||
type AdminPickup,
|
type AdminPickup,
|
||||||
|
type AdminPickupFinancialAdjustment,
|
||||||
} from '@/features/admin/api/adminPickup'
|
} from '@/features/admin/api/adminPickup'
|
||||||
import { quantity, readNumber, readUnitPrice } from '@/features/orders/composables/useOrderSnapshot'
|
import { quantity, readNumber, readUnitPrice } from '@/features/orders/composables/useOrderSnapshot'
|
||||||
import { adminPath } from '@/shared/utils/adminPath'
|
import { adminPath } from '@/shared/utils/adminPath'
|
||||||
@@ -33,14 +37,19 @@ interface SnapshotResource {
|
|||||||
const route = useRoute()
|
const route = useRoute()
|
||||||
const loading = ref(false)
|
const loading = ref(false)
|
||||||
const pickup = ref<AdminPickup | null>(null)
|
const pickup = ref<AdminPickup | null>(null)
|
||||||
|
const financialAdjustments = ref<AdminPickupFinancialAdjustment[]>([])
|
||||||
const profitDialogVisible = ref(false)
|
const profitDialogVisible = ref(false)
|
||||||
|
const financialAdjustmentDialogVisible = ref(false)
|
||||||
const profitSaving = ref(false)
|
const profitSaving = ref(false)
|
||||||
|
const financialAdjustmentSaving = ref(false)
|
||||||
const profitForm = reactive({ profit_amount: 0, reason: '' })
|
const profitForm = reactive({ profit_amount: 0, reason: '' })
|
||||||
|
const financialAdjustmentForm = reactive({ profit_amount: 0, settle_amount: 0, reason: '' })
|
||||||
|
|
||||||
const listingCode = computed(() =>
|
const listingCode = computed(() =>
|
||||||
pickup.value ? formatListingNo(pickup.value.listing_no, pickup.value.listing_id) : '-'
|
pickup.value ? formatListingNo(pickup.value.listing_no, pickup.value.listing_id) : '-'
|
||||||
)
|
)
|
||||||
const canEditProfit = computed(() => pickup.value?.status === 'picking_up')
|
const canEditProfit = computed(() => pickup.value?.status === 'picking_up')
|
||||||
|
const canCreateFinancialAdjustment = computed(() => pickup.value?.status === 'completed')
|
||||||
const snapshot = computed(() => normalizeRecord(pickup.value?.account_snapshot))
|
const snapshot = computed(() => normalizeRecord(pickup.value?.account_snapshot))
|
||||||
const assetSummary = computed(() => normalizeRecord(snapshot.value?.asset_summary))
|
const assetSummary = computed(() => normalizeRecord(snapshot.value?.asset_summary))
|
||||||
const priceBreakdown = computed(() => normalizeRecord(assetSummary.value?.price_breakdown))
|
const priceBreakdown = computed(() => normalizeRecord(assetSummary.value?.price_breakdown))
|
||||||
@@ -74,15 +83,15 @@ const moneySplitRows = computed(() => {
|
|||||||
const ownerLossPriceCent =
|
const ownerLossPriceCent =
|
||||||
readBreakdownCent('consumable_price') ?? fallbackOwnerLossCent(ownerCoinBasePriceCent)
|
readBreakdownCent('consumable_price') ?? fallbackOwnerLossCent(ownerCoinBasePriceCent)
|
||||||
const offlineTotalCent =
|
const offlineTotalCent =
|
||||||
row.settle_amount_cent > 0 ? row.settle_amount_cent + row.profit_amount_cent : null
|
effectiveSettleCent(row) > 0 ? effectiveSettleCent(row) + effectiveProfitCent(row) : null
|
||||||
const rows = [
|
const rows = [
|
||||||
{ label: '网站售价', amountCent: row.listing_price_cent, tone: '' },
|
{ label: '网站售价', amountCent: row.listing_price_cent, tone: '' },
|
||||||
{ label: '号主纯币价格', amountCent: ownerCoinBasePriceCent, tone: '' },
|
{ label: '号主纯币价格', amountCent: ownerCoinBasePriceCent, tone: '' },
|
||||||
{ label: '号主损耗', amountCent: ownerLossPriceCent, tone: '' },
|
{ label: '号主损耗', amountCent: ownerLossPriceCent, tone: '' },
|
||||||
{ label: '号主价合计', amountCent: row.owner_price_cent, tone: 'subtotal' },
|
{ label: '号主价合计', amountCent: row.owner_price_cent, tone: 'subtotal' },
|
||||||
{ label: '网站加价', amountCent: row.website_profit_cent, tone: '' },
|
{ label: '网站加价', amountCent: row.website_profit_cent, tone: '' },
|
||||||
{ label: '线下利润', amountCent: row.profit_amount_cent, tone: 'profit' },
|
{ label: '线下利润', amountCent: effectiveProfitCent(row), tone: 'profit' },
|
||||||
{ label: '结算给号主', amountCent: row.settle_amount_cent, tone: '' },
|
{ label: '结算给号主', amountCent: effectiveSettleCent(row), tone: '' },
|
||||||
]
|
]
|
||||||
if (offlineTotalCent !== null) {
|
if (offlineTotalCent !== null) {
|
||||||
rows.push({ label: '线下成交合计', amountCent: offlineTotalCent, tone: 'total' })
|
rows.push({ label: '线下成交合计', amountCent: offlineTotalCent, tone: 'total' })
|
||||||
@@ -108,12 +117,70 @@ onMounted(loadPickup)
|
|||||||
async function loadPickup() {
|
async function loadPickup() {
|
||||||
loading.value = true
|
loading.value = true
|
||||||
try {
|
try {
|
||||||
pickup.value = await fetchAdminPickup(String(route.params.id))
|
const [item, adjustments] = await Promise.all([
|
||||||
|
fetchAdminPickup(String(route.params.id)),
|
||||||
|
fetchAdminPickupFinancialAdjustments(String(route.params.id)),
|
||||||
|
])
|
||||||
|
pickup.value = item
|
||||||
|
financialAdjustments.value = adjustments
|
||||||
} finally {
|
} finally {
|
||||||
loading.value = false
|
loading.value = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function openFinancialAdjustmentDialog() {
|
||||||
|
if (!pickup.value) return
|
||||||
|
financialAdjustmentForm.profit_amount = centToYuan(effectiveProfitCent(pickup.value))
|
||||||
|
financialAdjustmentForm.settle_amount = centToYuan(effectiveSettleCent(pickup.value))
|
||||||
|
financialAdjustmentForm.reason = ''
|
||||||
|
financialAdjustmentDialogVisible.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleFinancialAdjustment() {
|
||||||
|
if (!pickup.value) return
|
||||||
|
if (!financialAdjustmentForm.reason.trim()) {
|
||||||
|
ElMessage.warning('请输入调整原因')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (financialAdjustmentForm.profit_amount < 0 || financialAdjustmentForm.settle_amount < 0) {
|
||||||
|
ElMessage.warning('金额不能小于 0')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
financialAdjustmentSaving.value = true
|
||||||
|
try {
|
||||||
|
pickup.value = await createAdminPickupFinancialAdjustment(pickup.value.id, {
|
||||||
|
profit_amount_cent: yuanToCent(financialAdjustmentForm.profit_amount),
|
||||||
|
settle_amount_cent: yuanToCent(financialAdjustmentForm.settle_amount),
|
||||||
|
reason: financialAdjustmentForm.reason.trim(),
|
||||||
|
})
|
||||||
|
financialAdjustments.value = await fetchAdminPickupFinancialAdjustments(pickup.value.id)
|
||||||
|
financialAdjustmentDialogVisible.value = false
|
||||||
|
ElMessage.success('财务调整已创建')
|
||||||
|
} catch (e: unknown) {
|
||||||
|
ElMessage.error(errorMessage(e) || '调整失败')
|
||||||
|
} finally {
|
||||||
|
financialAdjustmentSaving.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleSettleFinancialAdjustment(item: AdminPickupFinancialAdjustment) {
|
||||||
|
const action = item.status === 'recovery_pending' ? '已追回' : '已线下打款'
|
||||||
|
try {
|
||||||
|
const { value } = await ElMessageBox.prompt(`确认该笔调整${action}?`, '确认调整处理', {
|
||||||
|
confirmButtonText: '确认',
|
||||||
|
cancelButtonText: '取消',
|
||||||
|
inputType: 'textarea',
|
||||||
|
inputPlaceholder: '处理备注(可选)',
|
||||||
|
inputValidator: input => input.length <= 255 || '备注不能超过 255 个字符',
|
||||||
|
})
|
||||||
|
await settleAdminPickupFinancialAdjustment(item.id, { remark: value })
|
||||||
|
financialAdjustments.value = await fetchAdminPickupFinancialAdjustments(item.pickup_id)
|
||||||
|
ElMessage.success('调整已确认')
|
||||||
|
} catch {
|
||||||
|
// 用户取消确认。
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function openProfitDialog() {
|
function openProfitDialog() {
|
||||||
if (!pickup.value) return
|
if (!pickup.value) return
|
||||||
profitForm.profit_amount = centToYuan(pickup.value.profit_amount_cent)
|
profitForm.profit_amount = centToYuan(pickup.value.profit_amount_cent)
|
||||||
@@ -168,6 +235,34 @@ function offlineSettlementLabel(row: AdminPickup) {
|
|||||||
return '待完成提号'
|
return '待完成提号'
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function effectiveProfitCent(row: AdminPickup) {
|
||||||
|
return Number.isFinite(row.effective_profit_amount_cent)
|
||||||
|
? row.effective_profit_amount_cent
|
||||||
|
: row.profit_amount_cent
|
||||||
|
}
|
||||||
|
|
||||||
|
function effectiveSettleCent(row: AdminPickup) {
|
||||||
|
return Number.isFinite(row.effective_settle_amount_cent)
|
||||||
|
? row.effective_settle_amount_cent
|
||||||
|
: row.settle_amount_cent
|
||||||
|
}
|
||||||
|
|
||||||
|
function financialAdjustmentStatusLabel(status: AdminPickupFinancialAdjustment['status']) {
|
||||||
|
if (status === 'pending') return '待线下处理'
|
||||||
|
if (status === 'recovery_pending') return '待追回'
|
||||||
|
return '已处理'
|
||||||
|
}
|
||||||
|
|
||||||
|
function financialAdjustmentStatusType(status: AdminPickupFinancialAdjustment['status']) {
|
||||||
|
if (status === 'pending' || status === 'recovery_pending') return 'warning'
|
||||||
|
return 'success'
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatSignedCent(value: number) {
|
||||||
|
const amount = formatCentWithSymbol(Math.abs(value))
|
||||||
|
return value > 0 ? `+${amount}` : value < 0 ? `-${amount}` : amount
|
||||||
|
}
|
||||||
|
|
||||||
function ratioText(value: number) {
|
function ratioText(value: number) {
|
||||||
const ratio = Number(value || 0)
|
const ratio = Number(value || 0)
|
||||||
if (ratio <= 0) return '-'
|
if (ratio <= 0) return '-'
|
||||||
@@ -261,6 +356,15 @@ function readSnapshotResources(summary: Record<string, unknown> | null): Snapsho
|
|||||||
>
|
>
|
||||||
修改利润
|
修改利润
|
||||||
</el-button>
|
</el-button>
|
||||||
|
<el-button
|
||||||
|
v-if="canCreateFinancialAdjustment"
|
||||||
|
type="primary"
|
||||||
|
plain
|
||||||
|
:icon="EditPen"
|
||||||
|
@click="openFinancialAdjustmentDialog"
|
||||||
|
>
|
||||||
|
财务调整
|
||||||
|
</el-button>
|
||||||
<RouterLink :to="adminPath('pickup')">
|
<RouterLink :to="adminPath('pickup')">
|
||||||
<el-button>返回列表</el-button>
|
<el-button>返回列表</el-button>
|
||||||
</RouterLink>
|
</RouterLink>
|
||||||
@@ -275,12 +379,12 @@ function readSnapshotResources(summary: Record<string, unknown> | null): Snapsho
|
|||||||
</div>
|
</div>
|
||||||
<div class="metric-card">
|
<div class="metric-card">
|
||||||
<span>线下利润</span>
|
<span>线下利润</span>
|
||||||
<strong>{{ formatCentWithSymbol(pickup.profit_amount_cent) }}</strong>
|
<strong>{{ formatCentWithSymbol(effectiveProfitCent(pickup)) }}</strong>
|
||||||
<small>财务统计使用该金额</small>
|
<small>财务统计使用该金额</small>
|
||||||
</div>
|
</div>
|
||||||
<div class="metric-card">
|
<div class="metric-card">
|
||||||
<span>结算金额</span>
|
<span>结算金额</span>
|
||||||
<strong>{{ formatCentWithSymbol(pickup.settle_amount_cent) }}</strong>
|
<strong>{{ formatCentWithSymbol(effectiveSettleCent(pickup)) }}</strong>
|
||||||
<small>{{ pickup.completed_at ? formatDateTime(pickup.completed_at) : '待结算' }}</small>
|
<small>{{ pickup.completed_at ? formatDateTime(pickup.completed_at) : '待结算' }}</small>
|
||||||
</div>
|
</div>
|
||||||
<div class="metric-card">
|
<div class="metric-card">
|
||||||
@@ -363,6 +467,52 @@ function readSnapshotResources(summary: Record<string, unknown> | null): Snapsho
|
|||||||
</dl>
|
</dl>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
|
<section
|
||||||
|
v-if="financialAdjustments.length"
|
||||||
|
class="dashboard-panel detail-panel pickup-wide-panel"
|
||||||
|
>
|
||||||
|
<div class="panel-heading">
|
||||||
|
<h2>财务调整记录</h2>
|
||||||
|
<span class="panel-subtitle">原始完成金额不变,调整按创建日计入财务</span>
|
||||||
|
</div>
|
||||||
|
<el-table :data="financialAdjustments" size="small" class="adjustment-table">
|
||||||
|
<el-table-column label="利润调整" width="130">
|
||||||
|
<template #default="{ row }">{{ formatSignedCent(row.profit_delta_cent) }}</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column label="打款调整" width="130">
|
||||||
|
<template #default="{ row }">{{ formatSignedCent(row.settle_delta_cent) }}</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column label="处理状态" width="130">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<el-tag :type="financialAdjustmentStatusType(row.status)" effect="light" size="small">
|
||||||
|
{{ financialAdjustmentStatusLabel(row.status) }}
|
||||||
|
</el-tag>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column prop="reason" label="调整原因" min-width="180" />
|
||||||
|
<el-table-column label="创建时间" width="170">
|
||||||
|
<template #default="{ row }">{{ formatDateTime(row.created_at) }}</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column label="操作" width="120" fixed="right">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<el-button
|
||||||
|
v-if="
|
||||||
|
row.status === 'recovery_pending' ||
|
||||||
|
(row.status === 'pending' && pickup?.offline_settlement_status !== 'pending')
|
||||||
|
"
|
||||||
|
size="small"
|
||||||
|
type="primary"
|
||||||
|
plain
|
||||||
|
@click="handleSettleFinancialAdjustment(row)"
|
||||||
|
>
|
||||||
|
{{ row.status === 'recovery_pending' ? '确认追回' : '确认打款' }}
|
||||||
|
</el-button>
|
||||||
|
<span v-else class="text-muted">{{ formatDateTime(row.settled_at, '-') }}</span>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
</el-table>
|
||||||
|
</section>
|
||||||
|
|
||||||
<section class="dashboard-panel detail-panel">
|
<section class="dashboard-panel detail-panel">
|
||||||
<div class="panel-heading">
|
<div class="panel-heading">
|
||||||
<h2>资金拆分</h2>
|
<h2>资金拆分</h2>
|
||||||
@@ -475,6 +625,58 @@ function readSnapshotResources(summary: Record<string, unknown> | null): Snapsho
|
|||||||
</el-button>
|
</el-button>
|
||||||
</template>
|
</template>
|
||||||
</el-dialog>
|
</el-dialog>
|
||||||
|
|
||||||
|
<el-dialog v-model="financialAdjustmentDialogVisible" title="完成后财务调整" width="500px">
|
||||||
|
<el-form label-width="130px">
|
||||||
|
<el-form-item label="目标利润(元)">
|
||||||
|
<el-input-number
|
||||||
|
v-model="financialAdjustmentForm.profit_amount"
|
||||||
|
:min="0"
|
||||||
|
:step="1"
|
||||||
|
:precision="2"
|
||||||
|
controls-position="right"
|
||||||
|
style="width: 100%"
|
||||||
|
/>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="目标打款金额(元)">
|
||||||
|
<el-input-number
|
||||||
|
v-model="financialAdjustmentForm.settle_amount"
|
||||||
|
:min="0"
|
||||||
|
:step="1"
|
||||||
|
:precision="2"
|
||||||
|
controls-position="right"
|
||||||
|
style="width: 100%"
|
||||||
|
/>
|
||||||
|
<div class="form-hint">
|
||||||
|
{{
|
||||||
|
pickup?.settlement_mode === 'platform_managed'
|
||||||
|
? '会生成待线下补款或待追回记录'
|
||||||
|
: '增加金额立即补入钱包,减少金额生成待追回记录'
|
||||||
|
}}
|
||||||
|
</div>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="调整原因" required>
|
||||||
|
<el-input
|
||||||
|
v-model="financialAdjustmentForm.reason"
|
||||||
|
type="textarea"
|
||||||
|
:rows="3"
|
||||||
|
maxlength="255"
|
||||||
|
show-word-limit
|
||||||
|
placeholder="请输入调整原因"
|
||||||
|
/>
|
||||||
|
</el-form-item>
|
||||||
|
</el-form>
|
||||||
|
<template #footer>
|
||||||
|
<el-button @click="financialAdjustmentDialogVisible = false">取消</el-button>
|
||||||
|
<el-button
|
||||||
|
type="primary"
|
||||||
|
:loading="financialAdjustmentSaving"
|
||||||
|
@click="handleFinancialAdjustment"
|
||||||
|
>
|
||||||
|
确认调整
|
||||||
|
</el-button>
|
||||||
|
</template>
|
||||||
|
</el-dialog>
|
||||||
</section>
|
</section>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -498,6 +700,17 @@ function readSnapshotResources(summary: Record<string, unknown> | null): Snapsho
|
|||||||
word-break: break-word;
|
word-break: break-word;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.form-hint {
|
||||||
|
margin-top: 4px;
|
||||||
|
color: #64748b;
|
||||||
|
font-size: 12px;
|
||||||
|
line-height: 1.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.adjustment-table {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
.pickup-detail-grid {
|
.pickup-detail-grid {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: minmax(0, 1.1fr) minmax(360px, 0.9fr);
|
grid-template-columns: minmax(0, 1.1fr) minmax(360px, 0.9fr);
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import {
|
|||||||
cancelAdminPickup,
|
cancelAdminPickup,
|
||||||
confirmAdminPickupOfflineSettlement,
|
confirmAdminPickupOfflineSettlement,
|
||||||
completeAdminPickup,
|
completeAdminPickup,
|
||||||
|
createAdminPickupFinancialAdjustment,
|
||||||
createAdminPickup,
|
createAdminPickup,
|
||||||
fetchAdminPickups,
|
fetchAdminPickups,
|
||||||
fetchAdminPickupShopOptions,
|
fetchAdminPickupShopOptions,
|
||||||
@@ -36,9 +37,11 @@ const filters = reactive({
|
|||||||
const createDialogVisible = ref(false)
|
const createDialogVisible = ref(false)
|
||||||
const completeDialogVisible = ref(false)
|
const completeDialogVisible = ref(false)
|
||||||
const profitDialogVisible = ref(false)
|
const profitDialogVisible = ref(false)
|
||||||
|
const financialAdjustmentDialogVisible = ref(false)
|
||||||
const activePickupId = ref(0)
|
const activePickupId = ref(0)
|
||||||
const activePickup = ref<AdminPickup | null>(null)
|
const activePickup = ref<AdminPickup | null>(null)
|
||||||
const profitSaving = ref(false)
|
const profitSaving = ref(false)
|
||||||
|
const financialAdjustmentSaving = ref(false)
|
||||||
|
|
||||||
const createForm = reactive({
|
const createForm = reactive({
|
||||||
listing_id: null as number | null,
|
listing_id: null as number | null,
|
||||||
@@ -49,6 +52,7 @@ const createForm = reactive({
|
|||||||
})
|
})
|
||||||
const completeForm = reactive({ settle_amount: 0, profit_amount: 0, complete_remark: '' })
|
const completeForm = reactive({ settle_amount: 0, profit_amount: 0, complete_remark: '' })
|
||||||
const profitForm = reactive({ profit_amount: 0, reason: '' })
|
const profitForm = reactive({ profit_amount: 0, reason: '' })
|
||||||
|
const financialAdjustmentForm = reactive({ profit_amount: 0, settle_amount: 0, reason: '' })
|
||||||
|
|
||||||
const listingOptions = ref<AvailableListing[]>([])
|
const listingOptions = ref<AvailableListing[]>([])
|
||||||
const listingLoading = ref(false)
|
const listingLoading = ref(false)
|
||||||
@@ -196,7 +200,7 @@ function openCompleteDialog(row: AdminPickup) {
|
|||||||
async function handleConfirmOfflineSettlement(row: AdminPickup) {
|
async function handleConfirmOfflineSettlement(row: AdminPickup) {
|
||||||
try {
|
try {
|
||||||
const { value } = await ElMessageBox.prompt(
|
const { value } = await ElMessageBox.prompt(
|
||||||
`确认已向卖家线下支付 ${formatCentWithSymbol(row.settle_amount_cent)}?`,
|
`确认已向卖家线下支付 ${formatCentWithSymbol(effectiveSettleCent(row))}?`,
|
||||||
'确认线下结算',
|
'确认线下结算',
|
||||||
{
|
{
|
||||||
confirmButtonText: '确认已打款',
|
confirmButtonText: '确认已打款',
|
||||||
@@ -228,6 +232,45 @@ function openProfitDialog(row: AdminPickup) {
|
|||||||
profitDialogVisible.value = true
|
profitDialogVisible.value = true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function openFinancialAdjustmentDialog(row: AdminPickup) {
|
||||||
|
activePickupId.value = row.id
|
||||||
|
activePickup.value = row
|
||||||
|
financialAdjustmentForm.profit_amount = centToYuan(effectiveProfitCent(row))
|
||||||
|
financialAdjustmentForm.settle_amount = centToYuan(effectiveSettleCent(row))
|
||||||
|
financialAdjustmentForm.reason = ''
|
||||||
|
financialAdjustmentDialogVisible.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleFinancialAdjustment() {
|
||||||
|
if (!financialAdjustmentForm.reason.trim()) {
|
||||||
|
ElMessage.warning('请输入调整原因')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (financialAdjustmentForm.profit_amount < 0 || financialAdjustmentForm.settle_amount < 0) {
|
||||||
|
ElMessage.warning('金额不能小于 0')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
financialAdjustmentSaving.value = true
|
||||||
|
try {
|
||||||
|
await createAdminPickupFinancialAdjustment(activePickupId.value, {
|
||||||
|
profit_amount_cent: yuanToCent(financialAdjustmentForm.profit_amount),
|
||||||
|
settle_amount_cent: yuanToCent(financialAdjustmentForm.settle_amount),
|
||||||
|
reason: financialAdjustmentForm.reason.trim(),
|
||||||
|
})
|
||||||
|
ElMessage.success(
|
||||||
|
activePickup.value?.settlement_mode === 'platform_managed'
|
||||||
|
? '财务调整已创建,请确认线下补款或追回'
|
||||||
|
: '财务调整已创建'
|
||||||
|
)
|
||||||
|
financialAdjustmentDialogVisible.value = false
|
||||||
|
loadList()
|
||||||
|
} catch (e: unknown) {
|
||||||
|
ElMessage.error(errorMessage(e) || '调整失败')
|
||||||
|
} finally {
|
||||||
|
financialAdjustmentSaving.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async function handleComplete() {
|
async function handleComplete() {
|
||||||
if (completeForm.settle_amount <= 0) {
|
if (completeForm.settle_amount <= 0) {
|
||||||
ElMessage.warning('请输入结算金额')
|
ElMessage.warning('请输入结算金额')
|
||||||
@@ -329,6 +372,18 @@ function normalizeCent(value: number | undefined | null) {
|
|||||||
return Math.max(Math.round(cent), 0)
|
return Math.max(Math.round(cent), 0)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function effectiveProfitCent(row: AdminPickup) {
|
||||||
|
return Number.isFinite(row.effective_profit_amount_cent)
|
||||||
|
? row.effective_profit_amount_cent
|
||||||
|
: row.profit_amount_cent
|
||||||
|
}
|
||||||
|
|
||||||
|
function effectiveSettleCent(row: AdminPickup) {
|
||||||
|
return Number.isFinite(row.effective_settle_amount_cent)
|
||||||
|
? row.effective_settle_amount_cent
|
||||||
|
: row.settle_amount_cent
|
||||||
|
}
|
||||||
|
|
||||||
function listingOwnerTotalCent(item: AvailableListing) {
|
function listingOwnerTotalCent(item: AvailableListing) {
|
||||||
return normalizeCent(item.owner_total_price_cent || item.owner_price_cent)
|
return normalizeCent(item.owner_total_price_cent || item.owner_price_cent)
|
||||||
}
|
}
|
||||||
@@ -454,13 +509,13 @@ function listingOptionLabel(item: AvailableListing) {
|
|||||||
<el-table-column label="结算金额" width="130">
|
<el-table-column label="结算金额" width="130">
|
||||||
<template #default="{ row }">
|
<template #default="{ row }">
|
||||||
<span v-if="row.status === 'completed'">{{
|
<span v-if="row.status === 'completed'">{{
|
||||||
formatCentWithSymbol(row.settle_amount_cent)
|
formatCentWithSymbol(effectiveSettleCent(row))
|
||||||
}}</span>
|
}}</span>
|
||||||
<span v-else class="text-muted">待结算</span>
|
<span v-else class="text-muted">待结算</span>
|
||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
<el-table-column label="线下利润" width="120">
|
<el-table-column label="线下利润" width="120">
|
||||||
<template #default="{ row }">{{ formatCentWithSymbol(row.profit_amount_cent) }}</template>
|
<template #default="{ row }">{{ formatCentWithSymbol(effectiveProfitCent(row)) }}</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
<el-table-column label="状态" width="100">
|
<el-table-column label="状态" width="100">
|
||||||
<template #default="{ row }">
|
<template #default="{ row }">
|
||||||
@@ -512,6 +567,15 @@ function listingOptionLabel(item: AvailableListing) {
|
|||||||
>
|
>
|
||||||
确认线下结算
|
确认线下结算
|
||||||
</el-button>
|
</el-button>
|
||||||
|
<el-button
|
||||||
|
v-if="row.status === 'completed'"
|
||||||
|
size="small"
|
||||||
|
plain
|
||||||
|
:icon="EditPen"
|
||||||
|
@click="openFinancialAdjustmentDialog(row)"
|
||||||
|
>
|
||||||
|
财务调整
|
||||||
|
</el-button>
|
||||||
<el-button
|
<el-button
|
||||||
v-if="row.status === 'picking_up'"
|
v-if="row.status === 'picking_up'"
|
||||||
type="danger"
|
type="danger"
|
||||||
@@ -747,6 +811,58 @@ function listingOptionLabel(item: AvailableListing) {
|
|||||||
</el-button>
|
</el-button>
|
||||||
</template>
|
</template>
|
||||||
</el-dialog>
|
</el-dialog>
|
||||||
|
|
||||||
|
<el-dialog v-model="financialAdjustmentDialogVisible" title="完成后财务调整" width="500px">
|
||||||
|
<el-form label-width="120px">
|
||||||
|
<el-form-item label="目标利润(元)">
|
||||||
|
<el-input-number
|
||||||
|
v-model="financialAdjustmentForm.profit_amount"
|
||||||
|
:min="0"
|
||||||
|
:step="1"
|
||||||
|
:precision="2"
|
||||||
|
controls-position="right"
|
||||||
|
style="width: 100%"
|
||||||
|
/>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="目标打款金额(元)">
|
||||||
|
<el-input-number
|
||||||
|
v-model="financialAdjustmentForm.settle_amount"
|
||||||
|
:min="0"
|
||||||
|
:step="1"
|
||||||
|
:precision="2"
|
||||||
|
controls-position="right"
|
||||||
|
style="width: 100%"
|
||||||
|
/>
|
||||||
|
<div class="form-hint">
|
||||||
|
{{
|
||||||
|
activePickup?.settlement_mode === 'platform_managed'
|
||||||
|
? '代管账号会生成待线下补款或待追回记录'
|
||||||
|
: '增加金额会立即补入号主钱包,减少金额会生成待追回记录'
|
||||||
|
}}
|
||||||
|
</div>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="调整原因" required>
|
||||||
|
<el-input
|
||||||
|
v-model="financialAdjustmentForm.reason"
|
||||||
|
type="textarea"
|
||||||
|
:rows="3"
|
||||||
|
maxlength="255"
|
||||||
|
show-word-limit
|
||||||
|
placeholder="请输入调整原因"
|
||||||
|
/>
|
||||||
|
</el-form-item>
|
||||||
|
</el-form>
|
||||||
|
<template #footer>
|
||||||
|
<el-button @click="financialAdjustmentDialogVisible = false">取消</el-button>
|
||||||
|
<el-button
|
||||||
|
type="primary"
|
||||||
|
:loading="financialAdjustmentSaving"
|
||||||
|
@click="handleFinancialAdjustment"
|
||||||
|
>
|
||||||
|
确认调整
|
||||||
|
</el-button>
|
||||||
|
</template>
|
||||||
|
</el-dialog>
|
||||||
</section>
|
</section>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user