fix(distribution): 修复分销结账退款分成计算逻辑与利润统计,优化分销订单表格列宽与展示体验
This commit is contained in:
@@ -139,6 +139,7 @@ type DistributionOrderDTO struct {
|
|||||||
type DistributionOrderListQuery struct {
|
type DistributionOrderListQuery struct {
|
||||||
SiteID uint64
|
SiteID uint64
|
||||||
Kind string
|
Kind string
|
||||||
|
Role string
|
||||||
Status string
|
Status string
|
||||||
Keyword string
|
Keyword string
|
||||||
Page int
|
Page int
|
||||||
|
|||||||
@@ -146,6 +146,7 @@ func (h *Handler) ListOrders(c *gin.Context) {
|
|||||||
result, err := h.service.ListOrders(c.Request.Context(), DistributionOrderListQuery{
|
result, err := h.service.ListOrders(c.Request.Context(), DistributionOrderListQuery{
|
||||||
SiteID: scoped,
|
SiteID: scoped,
|
||||||
Kind: c.Query("kind"),
|
Kind: c.Query("kind"),
|
||||||
|
Role: c.Query("role"),
|
||||||
Status: c.Query("status"),
|
Status: c.Query("status"),
|
||||||
Keyword: c.Query("keyword"),
|
Keyword: c.Query("keyword"),
|
||||||
Page: page,
|
Page: page,
|
||||||
|
|||||||
@@ -240,7 +240,7 @@ func SnapshotPaidOrderTx(tx *gorm.DB, orderID uint64, grossPaidCent int64) error
|
|||||||
return tx.Create(&parties).Error
|
return tx.Create(&parties).Error
|
||||||
}
|
}
|
||||||
|
|
||||||
// ApplyRefundTx 按累计成功退款金额重算手续费、分成基数和各方分成,不覆盖订单创建时的规则版本。
|
// ApplyRefundTx 按累计成功退款或结账结算重算手续费、分成基数和各方分成,不覆盖订单创建时的规则版本。
|
||||||
func ApplyRefundTx(tx *gorm.DB, orderID uint64, refundAmountCent int64) error {
|
func ApplyRefundTx(tx *gorm.DB, orderID uint64, refundAmountCent int64) error {
|
||||||
if !distributionTablesPresent(tx) {
|
if !distributionTablesPresent(tx) {
|
||||||
return nil
|
return nil
|
||||||
@@ -255,33 +255,96 @@ func ApplyRefundTx(tx *gorm.DB, orderID uint64, refundAmountCent int64) error {
|
|||||||
if refundAmountCent < 0 {
|
if refundAmountCent < 0 {
|
||||||
return ErrInvalidAmount
|
return ErrInvalidAmount
|
||||||
}
|
}
|
||||||
if refundAmountCent > financial.GrossPaidAmountCent {
|
|
||||||
refundAmountCent = financial.GrossPaidAmountCent
|
|
||||||
}
|
|
||||||
var rule model.DistributionRuleVersion
|
var rule model.DistributionRuleVersion
|
||||||
if err := tx.First(&rule, financial.RuleVersionID).Error; err != nil {
|
if err := tx.First(&rule, financial.RuleVersionID).Error; err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
var order model.RentalOrder
|
var order model.RentalOrder
|
||||||
ownerRent := int64(0)
|
if err := tx.First(&order, orderID).Error; err != nil {
|
||||||
if err := tx.Select("owner_rent_amount_cent").First(&order, orderID).Error; err == nil {
|
return err
|
||||||
ownerRent = order.OwnerRentAmountCent
|
}
|
||||||
|
|
||||||
|
// 判定实际租金退款和号主应得租金:
|
||||||
|
// 1. 如果存在已达成一致的结账单(accepted 或 settled),以结账单中的实际消费租金与号主实际所得为准
|
||||||
|
var checkout model.OrderCheckout
|
||||||
|
hasAcceptedCheckout := false
|
||||||
|
if err := tx.Where("order_id = ? AND status IN ('accepted', 'settled')", orderID).
|
||||||
|
Order("id DESC").First(&checkout).Error; err == nil {
|
||||||
|
hasAcceptedCheckout = true
|
||||||
|
}
|
||||||
|
|
||||||
|
rentRefundCent := refundAmountCent
|
||||||
|
ownerRent := order.OwnerRentAmountCent
|
||||||
|
if ownerRent < 0 {
|
||||||
|
ownerRent = 0
|
||||||
|
}
|
||||||
|
|
||||||
|
if hasAcceptedCheckout {
|
||||||
|
// 结账单中,实际有效租金为 checkout.RentAmountCent
|
||||||
|
// 真实的租金退款为 订单初始租金 - 实际有效租金
|
||||||
|
actualRentRefund := order.RentAmountCent - checkout.RentAmountCent
|
||||||
|
if actualRentRefund < 0 {
|
||||||
|
actualRentRefund = 0
|
||||||
|
}
|
||||||
|
rentRefundCent = actualRentRefund
|
||||||
|
// 号主实际租金所得以结账单的实际租金收入为准
|
||||||
|
ownerRent = checkout.OwnerRentAmountCent
|
||||||
if ownerRent < 0 {
|
if ownerRent < 0 {
|
||||||
ownerRent = 0
|
ownerRent = 0
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
// 无结账单场景(例如交接超时、提前取消退款、维权全额退款):
|
||||||
|
// refundAmountCent 是租客收到的总退款(含押金与租金),押金不计入分销毛收入
|
||||||
|
// 先抵扣押金退款,剩余部分才属于租金退款
|
||||||
|
depositRefund := order.DepositAmountCent
|
||||||
|
if refundAmountCent < depositRefund {
|
||||||
|
depositRefund = refundAmountCent
|
||||||
|
}
|
||||||
|
actualRentRefund := refundAmountCent - depositRefund
|
||||||
|
if actualRentRefund < 0 {
|
||||||
|
actualRentRefund = 0
|
||||||
|
}
|
||||||
|
rentRefundCent = actualRentRefund
|
||||||
|
// 如果租金全额退款,号主应得租金为 0
|
||||||
|
if rentRefundCent >= financial.GrossPaidAmountCent {
|
||||||
|
ownerRent = 0
|
||||||
|
}
|
||||||
}
|
}
|
||||||
result, err := calculateForRule(rule, SettlementInput{GrossPaidAmountCent: financial.GrossPaidAmountCent, RefundAmountCent: refundAmountCent, OwnerRentAmountCent: ownerRent, MarkupAmountCent: financial.MarkupAmountCent})
|
|
||||||
|
if rentRefundCent > financial.GrossPaidAmountCent {
|
||||||
|
rentRefundCent = financial.GrossPaidAmountCent
|
||||||
|
}
|
||||||
|
|
||||||
|
result, err := calculateForRule(rule, SettlementInput{
|
||||||
|
GrossPaidAmountCent: financial.GrossPaidAmountCent,
|
||||||
|
RefundAmountCent: rentRefundCent,
|
||||||
|
OwnerRentAmountCent: ownerRent,
|
||||||
|
MarkupAmountCent: financial.MarkupAmountCent,
|
||||||
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
splits := SplitSettlement(ruleFromVersion(rule), result.SplitBaseAmountCent, financial.MarkupAmountCent)
|
splits := SplitSettlement(ruleFromVersion(rule), result.SplitBaseAmountCent, financial.MarkupAmountCent)
|
||||||
if err := tx.Model(&financial).Updates(map[string]any{"refund_amount_cent": refundAmountCent, "net_paid_amount_cent": result.NetPaidAmountCent, "payment_fee_cent": result.PaymentFeeCent, "service_fee_cent": result.ServiceFeeCent, "split_base_amount_cent": result.SplitBaseAmountCent, "rounding_adjustment_cent": SplitRoundingCent(result.SplitBaseAmountCent, result.ServiceFeeCent, financial.MarkupAmountCent, splits), "settlement_status": "pending"}).Error; err != nil {
|
if err := tx.Model(&financial).Updates(map[string]any{
|
||||||
|
"refund_amount_cent": rentRefundCent,
|
||||||
|
"net_paid_amount_cent": result.NetPaidAmountCent,
|
||||||
|
"payment_fee_cent": result.PaymentFeeCent,
|
||||||
|
"service_fee_cent": result.ServiceFeeCent,
|
||||||
|
"split_base_amount_cent": result.SplitBaseAmountCent,
|
||||||
|
"rounding_adjustment_cent": SplitRoundingCent(result.SplitBaseAmountCent, result.ServiceFeeCent, financial.MarkupAmountCent, splits),
|
||||||
|
"settlement_status": "pending",
|
||||||
|
}).Error; err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
for _, split := range splits {
|
for _, split := range splits {
|
||||||
if err := tx.Model(&model.DistributionOrderParty{}).
|
if err := tx.Model(&model.DistributionOrderParty{}).
|
||||||
Where("order_id = ? AND party_role = ?", orderID, split.PartyRole).
|
Where("order_id = ? AND party_role = ?", orderID, split.PartyRole).
|
||||||
Updates(map[string]any{"income_amount_cent": split.AmountCent, "profit_amount_cent": split.AmountCent}).Error; err != nil {
|
Updates(map[string]any{
|
||||||
|
"income_amount_cent": split.AmountCent,
|
||||||
|
"profit_amount_cent": split.AmountCent,
|
||||||
|
}).Error; err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -399,3 +399,76 @@ func TestSnapshotPaidOrderTxSplitsPercentOfProfit(t *testing.T) {
|
|||||||
t.Fatalf("platform receive = %+v", platform)
|
t.Fatalf("platform receive = %+v", platform)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestApplyRefundTxWithCheckoutAndDeposit(t *testing.T) {
|
||||||
|
db := distributionTestDB(t)
|
||||||
|
if err := db.AutoMigrate(&model.OrderCheckout{}); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
now := time.Now()
|
||||||
|
site := model.DistributionSite{SiteCode: "checkout_site", SiteName: "结账测试站", Status: "active"}
|
||||||
|
if err := db.Create(&site).Error; err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
rule := model.DistributionRuleVersion{
|
||||||
|
SiteID: &site.ID, Mode: ModeTwoPartyDistributorReceive, Version: 1, Status: "active",
|
||||||
|
DistributorReceiveRatio: 35, PlatformSellRatio: 40,
|
||||||
|
ServiceFeeValue: 25, ServiceFeeUnit: ServiceFeeUnitPercent, PaymentFeeBPS: 50,
|
||||||
|
EffectiveFrom: now.Add(-time.Hour),
|
||||||
|
}
|
||||||
|
if err := db.Create(&rule).Error; err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
order := model.RentalOrder{
|
||||||
|
OrderNo: "ORD-CHECKOUT-REFUND", ListingID: 10, AccountID: 20, OwnerID: 30, RenterID: 40, Status: "renting",
|
||||||
|
RentAmountCent: 63130, DepositAmountCent: 15500, OwnerRentAmountCent: 55950,
|
||||||
|
DistributionSiteID: &site.ID, DistributionRuleVersionID: &rule.ID, DistributionMode: ModeTwoPartyDistributorReceive,
|
||||||
|
}
|
||||||
|
if err := db.Create(&order).Error; err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if err := SnapshotPaidOrderTx(db, order.ID, 63130); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
checkout := model.OrderCheckout{
|
||||||
|
OrderID: order.ID,
|
||||||
|
Status: "accepted",
|
||||||
|
RentAmountCent: 53330,
|
||||||
|
OwnerRentAmountCent: 46150,
|
||||||
|
DepositAmountCent: 15500,
|
||||||
|
RenterRefundAmountCent: 25300,
|
||||||
|
OwnerIncomeAmountCent: 46150,
|
||||||
|
}
|
||||||
|
if err := db.Create(&checkout).Error; err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 退款 25300(包含押金全退 15500 + 租金退款 9800)
|
||||||
|
if err := ApplyRefundTx(db, order.ID, 25300); err != nil {
|
||||||
|
t.Fatalf("ApplyRefundTx failed: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
var financial model.DistributionOrderFinancial
|
||||||
|
if err := db.Where("order_id = ?", order.ID).First(&financial).Error; err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if financial.RefundAmountCent != 9800 {
|
||||||
|
t.Fatalf("financial.RefundAmountCent = %d, want 9800 (only rent refund)", financial.RefundAmountCent)
|
||||||
|
}
|
||||||
|
if financial.NetPaidAmountCent != 53330 {
|
||||||
|
t.Fatalf("financial.NetPaidAmountCent = %d, want 53330", financial.NetPaidAmountCent)
|
||||||
|
}
|
||||||
|
if financial.SplitBaseAmountCent != 6913 {
|
||||||
|
t.Fatalf("financial.SplitBaseAmountCent = %d, want 6913", financial.SplitBaseAmountCent)
|
||||||
|
}
|
||||||
|
|
||||||
|
var distributorParty model.DistributionOrderParty
|
||||||
|
if err := db.Where("order_id = ? AND party_role = 'distributor_receive'", order.ID).First(&distributorParty).Error; err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if distributorParty.IncomeAmountCent != 2420 {
|
||||||
|
t.Fatalf("distributorParty.IncomeAmountCent = %d, want 2420", distributorParty.IncomeAmountCent)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -424,7 +424,7 @@ func (r *Repository) ListOrders(ctx context.Context, query DistributionOrderList
|
|||||||
Joins("LEFT JOIN users u ON u.id = ro.renter_id")
|
Joins("LEFT JOIN users u ON u.id = ro.renter_id")
|
||||||
|
|
||||||
if query.SiteID > 0 {
|
if query.SiteID > 0 {
|
||||||
base = base.Where("ro.distribution_site_id = ?", query.SiteID)
|
base = base.Where("(ro.distribution_site_id = ? OR EXISTS (SELECT 1 FROM distribution_order_parties p WHERE p.order_id = ro.id AND (p.site_id = ? OR p.party_id = ?)))", query.SiteID, query.SiteID, query.SiteID)
|
||||||
}
|
}
|
||||||
if query.Status != "" {
|
if query.Status != "" {
|
||||||
base = base.Where("ro.status = ?", query.Status)
|
base = base.Where("ro.status = ?", query.Status)
|
||||||
@@ -435,7 +435,13 @@ func (r *Repository) ListOrders(ctx context.Context, query DistributionOrderList
|
|||||||
}
|
}
|
||||||
switch strings.TrimSpace(query.Kind) {
|
switch strings.TrimSpace(query.Kind) {
|
||||||
case "sold":
|
case "sold":
|
||||||
base = base.Where("EXISTS (SELECT 1 FROM distribution_order_parties p WHERE p.order_id = ro.id AND p.party_role IN ('distributor_sell','distributor_b'))")
|
if query.Role == "sell" {
|
||||||
|
base = base.Where("EXISTS (SELECT 1 FROM distribution_order_parties p WHERE p.order_id = ro.id AND p.party_role IN ('distributor_sell','distributor_b'))")
|
||||||
|
} else if query.Role == "receive" {
|
||||||
|
base = base.Where("EXISTS (SELECT 1 FROM distribution_order_parties p WHERE p.order_id = ro.id AND p.party_role IN ('distributor_receive','distributor_a'))")
|
||||||
|
} else {
|
||||||
|
base = base.Where("EXISTS (SELECT 1 FROM distribution_order_parties p WHERE p.order_id = ro.id AND p.party_role IN ('distributor_sell','distributor_b','distributor_receive','distributor_a'))")
|
||||||
|
}
|
||||||
case "receive":
|
case "receive":
|
||||||
base = base.Where("EXISTS (SELECT 1 FROM distribution_order_parties p WHERE p.order_id = ro.id AND p.party_role IN ('distributor_receive','distributor_a'))")
|
base = base.Where("EXISTS (SELECT 1 FROM distribution_order_parties p WHERE p.order_id = ro.id AND p.party_role IN ('distributor_receive','distributor_a'))")
|
||||||
case "", "all":
|
case "", "all":
|
||||||
@@ -486,7 +492,7 @@ func (r *Repository) ListOrders(ctx context.Context, query DistributionOrderList
|
|||||||
var parties []model.DistributionOrderParty
|
var parties []model.DistributionOrderParty
|
||||||
pq := r.db.WithContext(ctx).Where("order_id IN ?", orderIDs)
|
pq := r.db.WithContext(ctx).Where("order_id IN ?", orderIDs)
|
||||||
if query.SiteID > 0 {
|
if query.SiteID > 0 {
|
||||||
pq = pq.Where("site_id = ?", query.SiteID)
|
pq = pq.Where("(site_id = ? OR party_id = ?)", query.SiteID, query.SiteID)
|
||||||
}
|
}
|
||||||
if err := pq.Find(&parties).Error; err == nil {
|
if err := pq.Find(&parties).Error; err == nil {
|
||||||
partiesByOrder := make(map[uint64][]model.DistributionOrderParty)
|
partiesByOrder := make(map[uint64][]model.DistributionOrderParty)
|
||||||
@@ -500,6 +506,9 @@ func (r *Repository) ListOrders(ctx context.Context, query DistributionOrderList
|
|||||||
var splitCent int64
|
var splitCent int64
|
||||||
var roles []string
|
var roles []string
|
||||||
for _, p := range plist {
|
for _, p := range plist {
|
||||||
|
if p.PartyType != PartyTypeSite {
|
||||||
|
continue
|
||||||
|
}
|
||||||
totalEarning += p.IncomeAmountCent
|
totalEarning += p.IncomeAmountCent
|
||||||
if p.PartyRole == "distributor_sell" || p.PartyRole == "distributor_b" {
|
if p.PartyRole == "distributor_sell" || p.PartyRole == "distributor_b" {
|
||||||
roles = append(roles, "sell")
|
roles = append(roles, "sell")
|
||||||
@@ -566,12 +575,15 @@ func (r *Repository) FindOrder(ctx context.Context, adminID, id uint64) (*Distri
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
var parties []model.DistributionOrderParty
|
var parties []model.DistributionOrderParty
|
||||||
if err := r.db.WithContext(ctx).Where("order_id = ? AND site_id = ?", item.OrderID, item.SiteID).Find(&parties).Error; err == nil {
|
if err := r.db.WithContext(ctx).Where("order_id = ? AND (site_id = ? OR party_id = ?)", item.OrderID, item.SiteID, item.SiteID).Find(&parties).Error; err == nil {
|
||||||
var totalEarning int64
|
var totalEarning int64
|
||||||
var markupCent int64
|
var markupCent int64
|
||||||
var splitCent int64
|
var splitCent int64
|
||||||
var roles []string
|
var roles []string
|
||||||
for _, p := range parties {
|
for _, p := range parties {
|
||||||
|
if p.PartyType != PartyTypeSite {
|
||||||
|
continue
|
||||||
|
}
|
||||||
totalEarning += p.IncomeAmountCent
|
totalEarning += p.IncomeAmountCent
|
||||||
if p.PartyRole == "distributor_sell" || p.PartyRole == "distributor_b" {
|
if p.PartyRole == "distributor_sell" || p.PartyRole == "distributor_b" {
|
||||||
roles = append(roles, "sell")
|
roles = append(roles, "sell")
|
||||||
@@ -2079,12 +2091,12 @@ func (r *Repository) GetDashboardAnalytics(ctx context.Context, adminID, siteID
|
|||||||
|
|
||||||
var todayProfit int64
|
var todayProfit int64
|
||||||
_ = r.db.WithContext(ctx).Model(&model.DistributionOrderParty{}).
|
_ = r.db.WithContext(ctx).Model(&model.DistributionOrderParty{}).
|
||||||
Where("site_id = ? AND created_at >= ?", siteID, todayStr+" 00:00:00").
|
Where("(site_id = ? OR party_id = ?) AND party_type = 'site' AND created_at >= ?", siteID, siteID, todayStr+" 00:00:00").
|
||||||
Select("COALESCE(SUM(profit_amount_cent), 0)").Scan(&todayProfit).Error
|
Select("COALESCE(SUM(profit_amount_cent), 0)").Scan(&todayProfit).Error
|
||||||
|
|
||||||
var monthProfit int64
|
var monthProfit int64
|
||||||
_ = r.db.WithContext(ctx).Model(&model.DistributionOrderParty{}).
|
_ = r.db.WithContext(ctx).Model(&model.DistributionOrderParty{}).
|
||||||
Where("site_id = ? AND created_at >= ?", siteID, monthStr+"-01 00:00:00").
|
Where("(site_id = ? OR party_id = ?) AND party_type = 'site' AND created_at >= ?", siteID, siteID, monthStr+"-01 00:00:00").
|
||||||
Select("COALESCE(SUM(profit_amount_cent), 0)").Scan(&monthProfit).Error
|
Select("COALESCE(SUM(profit_amount_cent), 0)").Scan(&monthProfit).Error
|
||||||
|
|
||||||
var wallet model.WalletAccount
|
var wallet model.WalletAccount
|
||||||
@@ -2101,41 +2113,49 @@ func (r *Repository) GetDashboardAnalytics(ctx context.Context, adminID, siteID
|
|||||||
var tRows []trendRow
|
var tRows []trendRow
|
||||||
startDate := time.Now().AddDate(0, 0, -6).Format("2006-01-02")
|
startDate := time.Now().AddDate(0, 0, -6).Format("2006-01-02")
|
||||||
dateFormatExpr := "DATE_FORMAT(ro.created_at, '%Y-%m-%d')"
|
dateFormatExpr := "DATE_FORMAT(ro.created_at, '%Y-%m-%d')"
|
||||||
|
dateFormatPartyExpr := "DATE_FORMAT(created_at, '%Y-%m-%d')"
|
||||||
if r.db != nil && r.db.Dialector != nil && r.db.Dialector.Name() == "sqlite" {
|
if r.db != nil && r.db.Dialector != nil && r.db.Dialector.Name() == "sqlite" {
|
||||||
dateFormatExpr = "strftime('%Y-%m-%d', ro.created_at)"
|
dateFormatExpr = "strftime('%Y-%m-%d', ro.created_at)"
|
||||||
|
dateFormatPartyExpr = "strftime('%Y-%m-%d', created_at)"
|
||||||
}
|
}
|
||||||
selectTrendSQL := fmt.Sprintf("%s AS date_str, COUNT(ro.id) AS orders, COALESCE(SUM(f.gross_paid_amount_cent), 0) AS gmv", dateFormatExpr)
|
selectTrendSQL := fmt.Sprintf("%s AS date_str, COUNT(ro.id) AS orders, COALESCE(SUM(f.gross_paid_amount_cent), 0) AS gmv", dateFormatExpr)
|
||||||
_ = r.db.WithContext(ctx).Table("rental_orders ro").
|
_ = r.db.WithContext(ctx).Table("rental_orders ro").
|
||||||
Joins("JOIN distribution_order_financials f ON f.order_id = ro.id").
|
Joins("JOIN distribution_order_financials f ON f.order_id = ro.id").
|
||||||
Where("ro.distribution_site_id = ? AND ro.created_at >= ?", siteID, startDate+" 00:00:00").
|
Where("(ro.distribution_site_id = ? OR EXISTS (SELECT 1 FROM distribution_order_parties p WHERE p.order_id = ro.id AND (p.site_id = ? OR p.party_id = ?))) AND ro.created_at >= ?", siteID, siteID, siteID, startDate+" 00:00:00").
|
||||||
Select(selectTrendSQL).
|
Select(selectTrendSQL).
|
||||||
Group(dateFormatExpr).
|
Group(dateFormatExpr).
|
||||||
Order("date_str ASC").
|
Order("date_str ASC").
|
||||||
Scan(&tRows).Error
|
Scan(&tRows).Error
|
||||||
|
|
||||||
|
type profitRow struct {
|
||||||
|
DateStr string `gorm:"column:date_str"`
|
||||||
|
Profit int64 `gorm:"column:profit"`
|
||||||
|
}
|
||||||
|
var pRows []profitRow
|
||||||
|
_ = r.db.WithContext(ctx).Model(&model.DistributionOrderParty{}).
|
||||||
|
Where("(site_id = ? OR party_id = ?) AND party_type = 'site' AND created_at >= ?", siteID, siteID, startDate+" 00:00:00").
|
||||||
|
Select(fmt.Sprintf("%s AS date_str, COALESCE(SUM(profit_amount_cent), 0) AS profit", dateFormatPartyExpr)).
|
||||||
|
Group(dateFormatPartyExpr).
|
||||||
|
Scan(&pRows).Error
|
||||||
|
pRowMap := make(map[string]int64, len(pRows))
|
||||||
|
for _, pr := range pRows {
|
||||||
|
pRowMap[pr.DateStr] = pr.Profit
|
||||||
|
}
|
||||||
|
|
||||||
tRowMap := make(map[string]trendRow, len(tRows))
|
tRowMap := make(map[string]trendRow, len(tRows))
|
||||||
for _, tr := range tRows {
|
for _, tr := range tRows {
|
||||||
tRowMap[tr.DateStr] = tr
|
tRowMap[tr.DateStr] = tr
|
||||||
}
|
}
|
||||||
for i := -6; i <= 0; i++ {
|
for i := -6; i <= 0; i++ {
|
||||||
dStr := time.Now().AddDate(0, 0, i).Format("2006-01-02")
|
dStr := time.Now().AddDate(0, 0, i).Format("2006-01-02")
|
||||||
if tr, ok := tRowMap[dStr]; ok {
|
tr := tRowMap[dStr]
|
||||||
trendDays = append(trendDays, DistributionTrendPoint{
|
trendDays = append(trendDays, DistributionTrendPoint{
|
||||||
Date: dStr,
|
Date: dStr,
|
||||||
ReceivedCount: tr.Received,
|
ReceivedCount: tr.Received,
|
||||||
OrderCount: tr.Orders,
|
OrderCount: tr.Orders,
|
||||||
GMVCent: tr.GMV,
|
GMVCent: tr.GMV,
|
||||||
ProfitCent: tr.Profit,
|
ProfitCent: pRowMap[dStr],
|
||||||
})
|
})
|
||||||
} else {
|
|
||||||
trendDays = append(trendDays, DistributionTrendPoint{
|
|
||||||
Date: dStr,
|
|
||||||
ReceivedCount: 0,
|
|
||||||
OrderCount: 0,
|
|
||||||
GMVCent: 0,
|
|
||||||
ProfitCent: 0,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return &DistributionAnalyticsDTO{
|
return &DistributionAnalyticsDTO{
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
"hfb_sys/backend/internal/model"
|
"hfb_sys/backend/internal/model"
|
||||||
|
"hfb_sys/backend/internal/modules/distribution"
|
||||||
"hfb_sys/backend/internal/modules/notification"
|
"hfb_sys/backend/internal/modules/notification"
|
||||||
"hfb_sys/backend/internal/modules/rentergrowth"
|
"hfb_sys/backend/internal/modules/rentergrowth"
|
||||||
"hfb_sys/backend/internal/modules/wallet"
|
"hfb_sys/backend/internal/modules/wallet"
|
||||||
@@ -55,6 +56,9 @@ func (r *Repository) finalizeCheckout(tx *gorm.DB, order *model.RentalOrder, che
|
|||||||
if err := saveFinalizedCheckout(tx, order, checkout, listing, account); err != nil {
|
if err := saveFinalizedCheckout(tx, order, checkout, listing, account); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
if order.DistributionSiteID != nil && *order.DistributionSiteID > 0 {
|
||||||
|
_ = distribution.ApplyRefundTx(tx, order.ID, 0)
|
||||||
|
}
|
||||||
|
|
||||||
return refund, nil
|
return refund, nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import {
|
|||||||
EyeOutlined,
|
EyeOutlined,
|
||||||
FileSearchOutlined,
|
FileSearchOutlined,
|
||||||
LogoutOutlined,
|
LogoutOutlined,
|
||||||
|
OrderedListOutlined,
|
||||||
SettingOutlined,
|
SettingOutlined,
|
||||||
ShopOutlined,
|
ShopOutlined,
|
||||||
SkinOutlined,
|
SkinOutlined,
|
||||||
@@ -39,8 +40,9 @@ function MainLayout({ member, onLogout }: { member: CurrentMember; onLogout: ()
|
|||||||
...(member.is_superadmin
|
...(member.is_superadmin
|
||||||
? [{ key: '/sites', icon: <ShopOutlined />, label: <Link to="/sites">分销站点管理</Link> }]
|
? [{ key: '/sites', icon: <ShopOutlined />, label: <Link to="/sites">分销站点管理</Link> }]
|
||||||
: []),
|
: []),
|
||||||
{ key: '/receive', icon: <AccountBookOutlined />, label: <Link to="/receive">分销收号</Link> },
|
{ key: '/orders', icon: <OrderedListOutlined />, label: <Link to="/orders">全部分销订单</Link> },
|
||||||
{ key: '/sold', icon: <FileSearchOutlined />, label: <Link to="/sold">售出账号订单</Link> },
|
{ key: '/sold', icon: <FileSearchOutlined />, label: <Link to="/sold">售出账号订单</Link> },
|
||||||
|
{ key: '/receive', icon: <AccountBookOutlined />, label: <Link to="/receive">分销收号管理</Link> },
|
||||||
{ key: '/accounts', icon: <ShopOutlined />, label: <Link to="/accounts">商品货架与加价</Link> },
|
{ key: '/accounts', icon: <ShopOutlined />, label: <Link to="/accounts">商品货架与加价</Link> },
|
||||||
{ key: '/wallet', icon: <WalletOutlined />, label: <Link to="/wallet">资金与提现</Link> },
|
{ key: '/wallet', icon: <WalletOutlined />, label: <Link to="/wallet">资金与提现</Link> },
|
||||||
{
|
{
|
||||||
@@ -59,7 +61,7 @@ function MainLayout({ member, onLogout }: { member: CurrentMember; onLogout: ()
|
|||||||
},
|
},
|
||||||
]
|
]
|
||||||
|
|
||||||
const selectedKey = menuItems.some((item) => item.key === location.pathname)
|
const selectedKey = menuItems.some((item) => item && 'key' in item && item.key === location.pathname)
|
||||||
? location.pathname
|
? location.pathname
|
||||||
: '/'
|
: '/'
|
||||||
|
|
||||||
|
|||||||
@@ -357,15 +357,19 @@ export async function updateRuleStatus(id: number, status: string): Promise<Rule
|
|||||||
|
|
||||||
export async function listOrders(params: {
|
export async function listOrders(params: {
|
||||||
kind?: string
|
kind?: string
|
||||||
|
role?: string
|
||||||
status?: string
|
status?: string
|
||||||
siteID?: number
|
siteID?: number
|
||||||
page?: number
|
page?: number
|
||||||
pageSize?: number
|
pageSize?: number
|
||||||
|
keyword?: string
|
||||||
}): Promise<OrderPage> {
|
}): Promise<OrderPage> {
|
||||||
const resp = await api.get<ApiResponse<OrderPage>>('/admin/distribution/orders', {
|
const resp = await api.get<ApiResponse<OrderPage>>('/admin/distribution/orders', {
|
||||||
params: {
|
params: {
|
||||||
kind: params.kind || undefined,
|
kind: params.kind || undefined,
|
||||||
|
role: params.role || undefined,
|
||||||
status: params.status || undefined,
|
status: params.status || undefined,
|
||||||
|
keyword: params.keyword || undefined,
|
||||||
site_id: params.siteID || undefined,
|
site_id: params.siteID || undefined,
|
||||||
page: params.page || 1,
|
page: params.page || 1,
|
||||||
page_size: params.pageSize || 20,
|
page_size: params.pageSize || 20,
|
||||||
|
|||||||
@@ -72,7 +72,17 @@ const TX_OPTIONS = [
|
|||||||
|
|
||||||
function formatGameName(name?: string) {
|
function formatGameName(name?: string) {
|
||||||
if (!name) return '默认游戏'
|
if (!name) return '默认游戏'
|
||||||
if (name === 'delta_force') return '三角洲行动'
|
const lower = name.toLowerCase().trim()
|
||||||
|
if (
|
||||||
|
lower === 'delta_force' ||
|
||||||
|
lower === 'deltaforce' ||
|
||||||
|
lower === 'delta' ||
|
||||||
|
lower.includes('delta_force') ||
|
||||||
|
lower.includes('delta') ||
|
||||||
|
name.includes('三角洲')
|
||||||
|
) {
|
||||||
|
return '三角洲'
|
||||||
|
}
|
||||||
return name
|
return name
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -882,7 +892,7 @@ export default function Accounts() {
|
|||||||
width={580}
|
width={580}
|
||||||
destroyOnClose
|
destroyOnClose
|
||||||
>
|
>
|
||||||
<Space direction="vertical" style={{ orientation: 'vertical', width: '100%', padding: '8px 0' }} size="middle">
|
<Space direction="vertical" style={{ width: '100%', padding: '8px 0' }} size="middle">
|
||||||
<Alert
|
<Alert
|
||||||
type="success"
|
type="success"
|
||||||
showIcon
|
showIcon
|
||||||
|
|||||||
@@ -58,9 +58,19 @@ const ORDER_STATUS_MAP: Record<string, { label: string; color: string }> = {
|
|||||||
const SETTLEMENT_STATUS_MAP: Record<string, { label: string; color: string }> = {
|
const SETTLEMENT_STATUS_MAP: Record<string, { label: string; color: string }> = {
|
||||||
settled: { label: '已结算入账', color: 'success' },
|
settled: { label: '已结算入账', color: 'success' },
|
||||||
pending: { label: '履约中待结算', color: 'warning' },
|
pending: { label: '履约中待结算', color: 'warning' },
|
||||||
|
unsettled: { label: '未结算', color: 'default' },
|
||||||
|
settling: { label: '结算中', color: 'processing' },
|
||||||
|
disputed: { label: '争议冻结', color: 'magenta' },
|
||||||
failed: { label: '结算异常', color: 'error' },
|
failed: { label: '结算异常', color: 'error' },
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const REFUND_STATUS_MAP: Record<string, { label: string; color: string }> = {
|
||||||
|
none: { label: '未退款', color: 'default' },
|
||||||
|
refunding: { label: '退款中', color: 'orange' },
|
||||||
|
refunded: { label: '已退押金/差额', color: 'blue' },
|
||||||
|
failed: { label: '退款失败', color: 'error' },
|
||||||
|
}
|
||||||
|
|
||||||
const REVIEW_STATUS_LABELS: Record<string, { label: string; color: string }> = {
|
const REVIEW_STATUS_LABELS: Record<string, { label: string; color: string }> = {
|
||||||
pending: { label: '待审核', color: 'orange' },
|
pending: { label: '待审核', color: 'orange' },
|
||||||
approved: { label: '审核通过', color: 'success' },
|
approved: { label: '审核通过', color: 'success' },
|
||||||
@@ -80,6 +90,22 @@ function maskPhone(phone?: string): string {
|
|||||||
return phone.slice(0, 3) + '****' + phone.slice(-4)
|
return phone.slice(0, 3) + '****' + phone.slice(-4)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function formatGameName(gameName?: string): string {
|
||||||
|
if (!gameName) return ''
|
||||||
|
const lower = gameName.toLowerCase().trim()
|
||||||
|
if (
|
||||||
|
lower === 'delta_force' ||
|
||||||
|
lower === 'deltaforce' ||
|
||||||
|
lower === 'delta' ||
|
||||||
|
lower.includes('delta_force') ||
|
||||||
|
lower.includes('delta') ||
|
||||||
|
gameName.includes('三角洲')
|
||||||
|
) {
|
||||||
|
return '三角洲'
|
||||||
|
}
|
||||||
|
return gameName
|
||||||
|
}
|
||||||
|
|
||||||
export default function Orders({ kind }: { kind: 'all' | 'sold' | 'receive' }) {
|
export default function Orders({ kind }: { kind: 'all' | 'sold' | 'receive' }) {
|
||||||
const member = useMember()
|
const member = useMember()
|
||||||
const [sites, setSites] = useState<Site[]>([])
|
const [sites, setSites] = useState<Site[]>([])
|
||||||
@@ -223,14 +249,70 @@ export default function Orders({ kind }: { kind: 'all' | 'sold' | 'receive' }) {
|
|||||||
|
|
||||||
const orderColumns = [
|
const orderColumns = [
|
||||||
{
|
{
|
||||||
title: '订单号 / 时间',
|
title: '订单与商品编号',
|
||||||
width: 190,
|
width: 270,
|
||||||
render: (_: unknown, row: DistributionOrder) => (
|
render: (_: unknown, row: DistributionOrder) => (
|
||||||
<div>
|
<div style={{ whiteSpace: 'nowrap' }}>
|
||||||
<Typography.Text copyable strong style={{ fontSize: 13 }}>
|
<div style={{ display: 'flex', alignItems: 'center', whiteSpace: 'nowrap', marginBottom: 4 }}>
|
||||||
{row.order_no}
|
<Tag
|
||||||
</Typography.Text>
|
color="default"
|
||||||
<div style={{ fontSize: 12, color: '#8c8c8c', marginTop: 2 }}>
|
style={{
|
||||||
|
margin: 0,
|
||||||
|
marginInlineEnd: 6,
|
||||||
|
fontSize: 11,
|
||||||
|
padding: '0 5px',
|
||||||
|
lineHeight: '18px',
|
||||||
|
flexShrink: 0,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
订单
|
||||||
|
</Tag>
|
||||||
|
<Typography.Text
|
||||||
|
copyable={{ text: row.order_no }}
|
||||||
|
strong
|
||||||
|
style={{
|
||||||
|
fontSize: 13,
|
||||||
|
fontFamily: 'monospace',
|
||||||
|
whiteSpace: 'nowrap',
|
||||||
|
display: 'inline-flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{row.order_no}
|
||||||
|
</Typography.Text>
|
||||||
|
</div>
|
||||||
|
{row.listing_no ? (
|
||||||
|
<div style={{ display: 'flex', alignItems: 'center', whiteSpace: 'nowrap', marginBottom: 4 }}>
|
||||||
|
<Tag
|
||||||
|
color="blue"
|
||||||
|
style={{
|
||||||
|
margin: 0,
|
||||||
|
marginInlineEnd: 6,
|
||||||
|
fontSize: 11,
|
||||||
|
padding: '0 5px',
|
||||||
|
lineHeight: '18px',
|
||||||
|
flexShrink: 0,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
商品
|
||||||
|
</Tag>
|
||||||
|
<Typography.Text
|
||||||
|
copyable={{ text: row.listing_no }}
|
||||||
|
style={{
|
||||||
|
fontSize: 13,
|
||||||
|
color: '#1677ff',
|
||||||
|
fontWeight: 600,
|
||||||
|
fontFamily: 'monospace',
|
||||||
|
whiteSpace: 'nowrap',
|
||||||
|
display: 'inline-flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{row.listing_no}
|
||||||
|
</Typography.Text>
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
|
<div style={{ fontSize: 12, color: '#8c8c8c', whiteSpace: 'nowrap', marginTop: 2 }}>
|
||||||
{row.created_at ? new Date(row.created_at).toLocaleString('zh-CN', { hour12: false }) : '-'}
|
{row.created_at ? new Date(row.created_at).toLocaleString('zh-CN', { hour12: false }) : '-'}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -238,14 +320,18 @@ export default function Orders({ kind }: { kind: 'all' | 'sold' | 'receive' }) {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: '商品与游戏',
|
title: '商品与游戏',
|
||||||
minWidth: 220,
|
width: 260,
|
||||||
render: (_: unknown, row: DistributionOrder) => (
|
render: (_: unknown, row: DistributionOrder) => (
|
||||||
<div>
|
<div>
|
||||||
<Typography.Text strong style={{ display: 'block', fontSize: 14 }}>
|
<Typography.Text
|
||||||
{row.title || (row.listing_no ? `商品 #${row.listing_no}` : `订单商品 #${row.order_id}`)}
|
strong
|
||||||
|
style={{ fontSize: 14, lineHeight: '20px', display: 'block' }}
|
||||||
|
ellipsis={{ tooltip: row.title }}
|
||||||
|
>
|
||||||
|
{row.title || `商品 #${row.listing_no || row.order_id}`}
|
||||||
</Typography.Text>
|
</Typography.Text>
|
||||||
<Space size={4} style={{ marginTop: 4 }} wrap>
|
<Space size={4} style={{ marginTop: 4 }} wrap>
|
||||||
{row.game_name && <Tag color="blue">{row.game_name}</Tag>}
|
{row.game_name && <Tag color="blue">{formatGameName(row.game_name)}</Tag>}
|
||||||
{row.server_region && <Tag>{row.server_region}</Tag>}
|
{row.server_region && <Tag>{row.server_region}</Tag>}
|
||||||
{row.login_platform && <Tag color="cyan">{row.login_platform}</Tag>}
|
{row.login_platform && <Tag color="cyan">{row.login_platform}</Tag>}
|
||||||
{row.rent_duration_hours ? <Tag color="purple">租期: {row.rent_duration_hours}h</Tag> : null}
|
{row.rent_duration_hours ? <Tag color="purple">租期: {row.rent_duration_hours}h</Tag> : null}
|
||||||
@@ -255,28 +341,30 @@ export default function Orders({ kind }: { kind: 'all' | 'sold' | 'receive' }) {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: '租客',
|
title: '租客',
|
||||||
width: 140,
|
width: 130,
|
||||||
render: (_: unknown, row: DistributionOrder) => (
|
render: (_: unknown, row: DistributionOrder) => (
|
||||||
<div>
|
<div>
|
||||||
<div style={{ fontWeight: 500 }}>{row.renter_nickname || '平台租客'}</div>
|
<div style={{ fontWeight: 500, whiteSpace: 'nowrap' }}>{row.renter_nickname || '平台租客'}</div>
|
||||||
<Typography.Text type="secondary" style={{ fontSize: 12 }}>
|
<Typography.Text type="secondary" style={{ fontSize: 12, whiteSpace: 'nowrap' }}>
|
||||||
{maskPhone(row.renter_phone)}
|
{maskPhone(row.renter_phone)}
|
||||||
</Typography.Text>
|
</Typography.Text>
|
||||||
</div>
|
</div>
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: '实付租金 / 押金',
|
title: '实付 / 押金',
|
||||||
width: 140,
|
width: 140,
|
||||||
render: (_: unknown, row: DistributionOrder) => {
|
render: (_: unknown, row: DistributionOrder) => {
|
||||||
const netPaid = row.net_paid_amount_cent || row.gross_paid_amount_cent || 0
|
const netPaid = row.net_paid_amount_cent || row.gross_paid_amount_cent || 0
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<Typography.Text strong style={{ color: '#1677ff', fontSize: 15 }}>
|
<Typography.Text strong style={{ color: '#1677ff', fontSize: 15, whiteSpace: 'nowrap', display: 'block' }}>
|
||||||
{formatMoney(netPaid)}
|
{formatMoney(netPaid)}
|
||||||
</Typography.Text>
|
</Typography.Text>
|
||||||
{row.deposit_amount_cent ? (
|
{row.deposit_amount_cent ? (
|
||||||
<div style={{ fontSize: 12, color: '#8c8c8c' }}>押金: {formatMoney(row.deposit_amount_cent)}</div>
|
<div style={{ fontSize: 12, color: '#8c8c8c', whiteSpace: 'nowrap', marginTop: 2 }}>
|
||||||
|
押金: {formatMoney(row.deposit_amount_cent)}
|
||||||
|
</div>
|
||||||
) : null}
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
@@ -284,30 +372,42 @@ export default function Orders({ kind }: { kind: 'all' | 'sold' | 'receive' }) {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: '本站净收益',
|
title: '本站净收益',
|
||||||
width: 180,
|
width: 150,
|
||||||
render: (_: unknown, row: DistributionOrder) => {
|
render: (_: unknown, row: DistributionOrder) => {
|
||||||
const earning = row.distributor_earning_cent ?? 0
|
const earning = row.distributor_earning_cent ?? 0
|
||||||
|
const isSell = row.distributor_role?.includes('sell')
|
||||||
|
const isRecv = row.distributor_role?.includes('receive')
|
||||||
const roleLabel =
|
const roleLabel =
|
||||||
row.distributor_role === 'seller'
|
isSell && isRecv
|
||||||
? '售出方'
|
? '自收自售'
|
||||||
: row.distributor_role === 'receiver'
|
: isSell
|
||||||
? '收号方'
|
? '渠道代销'
|
||||||
: row.distributor_role === 'two_party_distributor'
|
: isRecv
|
||||||
? '分销方'
|
? '号源出租'
|
||||||
: '本站收益'
|
: '本站收益'
|
||||||
|
const tagColor =
|
||||||
|
isSell && isRecv
|
||||||
|
? 'purple'
|
||||||
|
: isSell
|
||||||
|
? 'green'
|
||||||
|
: isRecv
|
||||||
|
? 'blue'
|
||||||
|
: earning > 0
|
||||||
|
? 'green'
|
||||||
|
: 'default'
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<Typography.Text strong style={{ color: '#389e0d', fontSize: 16 }}>
|
<Typography.Text strong style={{ color: '#389e0d', fontSize: 16, whiteSpace: 'nowrap', display: 'block' }}>
|
||||||
+{formatMoney(earning)}
|
+{formatMoney(earning)}
|
||||||
</Typography.Text>
|
</Typography.Text>
|
||||||
<div style={{ marginTop: 2 }}>
|
<div style={{ marginTop: 2 }}>
|
||||||
<Tag color={earning > 0 ? 'green' : 'default'} style={{ fontSize: 11 }}>
|
<Tag color={tagColor} style={{ fontSize: 11, margin: 0 }}>
|
||||||
{roleLabel}
|
{roleLabel}
|
||||||
</Tag>
|
</Tag>
|
||||||
</div>
|
</div>
|
||||||
{row.distributor_markup_cent && row.distributor_markup_cent > 0 ? (
|
{row.distributor_markup_cent && row.distributor_markup_cent > 0 ? (
|
||||||
<div style={{ fontSize: 11, color: '#52c41a' }}>
|
<div style={{ fontSize: 11, color: '#52c41a', whiteSpace: 'nowrap', marginTop: 2 }}>
|
||||||
含自主溢价: +{formatMoney(row.distributor_markup_cent)}
|
含加价: +{formatMoney(row.distributor_markup_cent)}
|
||||||
</div>
|
</div>
|
||||||
) : null}
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
@@ -315,22 +415,11 @@ export default function Orders({ kind }: { kind: 'all' | 'sold' | 'receive' }) {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: '订单履约状态',
|
title: '履约状态',
|
||||||
width: 120,
|
width: 110,
|
||||||
render: (_: unknown, row: DistributionOrder) => {
|
render: (_: unknown, row: DistributionOrder) => {
|
||||||
const conf = ORDER_STATUS_MAP[row.status] || { label: row.status, color: 'default' }
|
const conf = ORDER_STATUS_MAP[row.status] || { label: row.status, color: 'default' }
|
||||||
return (
|
return <Tag color={conf.color}>{conf.label}</Tag>
|
||||||
<div>
|
|
||||||
<Tag color={conf.color}>{conf.label}</Tag>
|
|
||||||
{row.refund_status && row.refund_status !== 'none' && (
|
|
||||||
<div style={{ marginTop: 2 }}>
|
|
||||||
<Tag color="magenta" style={{ fontSize: 11 }}>
|
|
||||||
退款: {row.refund_status}
|
|
||||||
</Tag>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -346,12 +435,14 @@ export default function Orders({ kind }: { kind: 'all' | 'sold' | 'receive' }) {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: '操作',
|
title: '操作',
|
||||||
width: 110,
|
width: 90,
|
||||||
|
fixed: 'right' as const,
|
||||||
render: (_: unknown, row: DistributionOrder) => (
|
render: (_: unknown, row: DistributionOrder) => (
|
||||||
<Button
|
<Button
|
||||||
type="link"
|
type="link"
|
||||||
size="small"
|
size="small"
|
||||||
icon={<FileTextOutlined />}
|
icon={<FileTextOutlined />}
|
||||||
|
style={{ padding: 0 }}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setSelectedOrder(row)
|
setSelectedOrder(row)
|
||||||
setDrawerVisible(true)
|
setDrawerVisible(true)
|
||||||
@@ -474,7 +565,7 @@ export default function Orders({ kind }: { kind: 'all' | 'sold' | 'receive' }) {
|
|||||||
{row.title || '无标题'}
|
{row.title || '无标题'}
|
||||||
</Typography.Text>
|
</Typography.Text>
|
||||||
<Space size={4} style={{ marginTop: 4 }}>
|
<Space size={4} style={{ marginTop: 4 }}>
|
||||||
<Tag color="blue">{row.game_name}</Tag>
|
<Tag color="blue">{formatGameName(row.game_name)}</Tag>
|
||||||
{row.server_region && <Tag>{row.server_region}</Tag>}
|
{row.server_region && <Tag>{row.server_region}</Tag>}
|
||||||
{row.login_platform && <Tag>{row.login_platform}</Tag>}
|
{row.login_platform && <Tag>{row.login_platform}</Tag>}
|
||||||
{row.rank_level && <Tag color="purple">{row.rank_level}</Tag>}
|
{row.rank_level && <Tag color="purple">{row.rank_level}</Tag>}
|
||||||
@@ -604,6 +695,7 @@ export default function Orders({ kind }: { kind: 'all' | 'sold' | 'receive' }) {
|
|||||||
loading={orderLoading}
|
loading={orderLoading}
|
||||||
dataSource={orders}
|
dataSource={orders}
|
||||||
columns={orderColumns}
|
columns={orderColumns}
|
||||||
|
scroll={{ x: 1200 }}
|
||||||
pagination={{
|
pagination={{
|
||||||
current: orderPage,
|
current: orderPage,
|
||||||
pageSize,
|
pageSize,
|
||||||
@@ -657,6 +749,7 @@ export default function Orders({ kind }: { kind: 'all' | 'sold' | 'receive' }) {
|
|||||||
loading={orderLoading}
|
loading={orderLoading}
|
||||||
dataSource={orders}
|
dataSource={orders}
|
||||||
columns={orderColumns}
|
columns={orderColumns}
|
||||||
|
scroll={{ x: 1200 }}
|
||||||
pagination={{
|
pagination={{
|
||||||
current: orderPage,
|
current: orderPage,
|
||||||
pageSize,
|
pageSize,
|
||||||
@@ -741,6 +834,22 @@ export default function Orders({ kind }: { kind: 'all' | 'sold' | 'receive' }) {
|
|||||||
size="small"
|
size="small"
|
||||||
column={2}
|
column={2}
|
||||||
items={[
|
items={[
|
||||||
|
{
|
||||||
|
key: 'order_no',
|
||||||
|
label: '订单编号',
|
||||||
|
children: <Typography.Text copyable>{selectedOrder.order_no}</Typography.Text>,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'listing_no',
|
||||||
|
label: '商品编号',
|
||||||
|
children: selectedOrder.listing_no ? (
|
||||||
|
<Typography.Text copyable={{ text: selectedOrder.listing_no }} strong style={{ color: '#1677ff' }}>
|
||||||
|
{selectedOrder.listing_no}
|
||||||
|
</Typography.Text>
|
||||||
|
) : (
|
||||||
|
'-'
|
||||||
|
),
|
||||||
|
},
|
||||||
{
|
{
|
||||||
key: 'title',
|
key: 'title',
|
||||||
label: '商品名称',
|
label: '商品名称',
|
||||||
@@ -750,7 +859,7 @@ export default function Orders({ kind }: { kind: 'all' | 'sold' | 'receive' }) {
|
|||||||
{
|
{
|
||||||
key: 'game',
|
key: 'game',
|
||||||
label: '游戏 / 区服',
|
label: '游戏 / 区服',
|
||||||
children: `${selectedOrder.game_name || '-'} / ${selectedOrder.server_region || '-'}`,
|
children: `${formatGameName(selectedOrder.game_name) || '-'} / ${selectedOrder.server_region || '-'}`,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: 'platform',
|
key: 'platform',
|
||||||
@@ -781,6 +890,20 @@ export default function Orders({ kind }: { kind: 'all' | 'sold' | 'receive' }) {
|
|||||||
</Tag>
|
</Tag>
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
key: 'refund_status',
|
||||||
|
label: '资金原路退还',
|
||||||
|
children:
|
||||||
|
selectedOrder.refund_status && selectedOrder.refund_status !== 'none' ? (
|
||||||
|
<Tag color={REFUND_STATUS_MAP[selectedOrder.refund_status]?.color || 'default'}>
|
||||||
|
{selectedOrder.status === 'completed' && selectedOrder.refund_status === 'refunded'
|
||||||
|
? '已退还押金/租金差额'
|
||||||
|
: REFUND_STATUS_MAP[selectedOrder.refund_status]?.label || selectedOrder.refund_status}
|
||||||
|
</Tag>
|
||||||
|
) : (
|
||||||
|
'无退款'
|
||||||
|
),
|
||||||
|
},
|
||||||
{
|
{
|
||||||
key: 'created_at',
|
key: 'created_at',
|
||||||
label: '下单时间',
|
label: '下单时间',
|
||||||
@@ -837,10 +960,12 @@ export default function Orders({ kind }: { kind: 'all' | 'sold' | 'receive' }) {
|
|||||||
<div style={{ marginBottom: 8 }}>
|
<div style={{ marginBottom: 8 }}>
|
||||||
<Tag color="blue">
|
<Tag color="blue">
|
||||||
本分销站角色:
|
本分销站角色:
|
||||||
{selectedOrder.distributor_role === 'seller'
|
{selectedOrder.distributor_role?.includes('sell') && selectedOrder.distributor_role?.includes('receive')
|
||||||
? '售出方 (享 40% 提成分润 + 100% 自主加价溢价)'
|
? '自收自售 (独享全部分润与自主溢价)'
|
||||||
: selectedOrder.distributor_role === 'receiver'
|
: selectedOrder.distributor_role?.includes('sell')
|
||||||
? '收号方 (享 35% 提成分润)'
|
? '售出方 / 渠道代销 (享 40% 提成分润 + 100% 自主加价溢价)'
|
||||||
|
: selectedOrder.distributor_role?.includes('receive')
|
||||||
|
? '供号方 / 收号出租 (享 35% 提成分润)'
|
||||||
: '分销合作方'}
|
: '分销合作方'}
|
||||||
</Tag>
|
</Tag>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -348,7 +348,7 @@ export default function SiteSettings() {
|
|||||||
</div>
|
</div>
|
||||||
</Col>
|
</Col>
|
||||||
<Col span={18}>
|
<Col span={18}>
|
||||||
<Space orientation="vertical" style={{ width: '100%' }} size="small">
|
<Space direction="vertical" style={{ width: '100%' }} size="small">
|
||||||
<Input
|
<Input
|
||||||
placeholder="输入图片 URL (https://...)"
|
placeholder="输入图片 URL (https://...)"
|
||||||
value={item.image_url}
|
value={item.image_url}
|
||||||
|
|||||||
@@ -139,6 +139,18 @@ body,
|
|||||||
padding: 24px;
|
padding: 24px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* AntD Table Typography & Layout Tweaks */
|
||||||
|
.ant-table-cell {
|
||||||
|
vertical-align: middle !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ant-typography-copy {
|
||||||
|
display: inline-flex !important;
|
||||||
|
align-items: center !important;
|
||||||
|
vertical-align: middle !important;
|
||||||
|
margin-inline-start: 4px !important;
|
||||||
|
}
|
||||||
|
|
||||||
.stats {
|
.stats {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||||
|
|||||||
Reference in New Issue
Block a user