feat(distribution): 重构分销后台为SaaS商户工作台并全面商业化优化

- 商品货架 (Accounts.tsx): 支持期望售价/固定金额/百分比多模式实时联动加价与批量调价,高亮100%独享溢价
- 订单中心 (Orders.tsx): 透传账号游戏租客信息,新增收益对账抽屉透明拆解底价、手续费、分成与自主溢价
- 结算与规则 (Settlements.tsx & Rules.tsx): 纠正角色倒错,商户视角展现周期对账单与分成合作政策
- 店铺装修 (SiteSettings.tsx): 废除手写JSON,提供横幅/公告/页脚可视化列表编辑器与前台实时模拟器
- 数据大盘 (Dashboard.tsx): 对齐最新订单字段,大字展示分销商净收益
- 后端数据层 (dto.go, repository.go, handler.go): 聚合计算分销商订单收益,支持商品与租客搜索
This commit is contained in:
yml2213
2026-09-09 14:44:02 +08:00
parent aa466051ac
commit f6ad7c34ee
12 changed files with 2257 additions and 465 deletions
+49 -27
View File
@@ -91,36 +91,58 @@ type UpdateRuleStatusRequest struct {
}
type DistributionOrderDTO struct {
OrderID uint64 `json:"order_id"`
OrderNo string `json:"order_no"`
SiteID uint64 `json:"site_id"`
ListingID uint64 `json:"listing_id"`
AccountID uint64 `json:"account_id"`
OwnerID uint64 `json:"owner_id"`
RenterID uint64 `json:"renter_id"`
Status string `json:"status"`
HandoffStatus string `json:"handoff_status"`
RefundStatus string `json:"refund_status"`
SettlementStatus string `json:"settlement_status"`
DistributionMode string `json:"distribution_mode"`
GrossPaidAmountCent int64 `json:"gross_paid_amount_cent"`
RefundAmountCent int64 `json:"refund_amount_cent"`
NetPaidAmountCent int64 `json:"net_paid_amount_cent"`
PaymentFeeCent int64 `json:"payment_fee_cent"`
ServiceFeeCent int64 `json:"service_fee_cent"`
MarkupAmountCent int64 `json:"markup_amount_cent"`
TaxAmountCent int64 `json:"tax_amount_cent"`
SplitBaseAmountCent int64 `json:"split_base_amount_cent"`
RoundingAdjustmentCent int64 `json:"rounding_adjustment_cent"`
CreatedAt time.Time `json:"created_at"`
OrderID uint64 `json:"order_id"`
OrderNo string `json:"order_no"`
SiteID uint64 `json:"site_id"`
ListingID uint64 `json:"listing_id"`
AccountID uint64 `json:"account_id"`
OwnerID uint64 `json:"owner_id"`
RenterID uint64 `json:"renter_id"`
Status string `json:"status"`
HandoffStatus string `json:"handoff_status"`
RefundStatus string `json:"refund_status"`
SettlementStatus string `json:"settlement_status"`
DistributionMode string `json:"distribution_mode"`
GrossPaidAmountCent int64 `json:"gross_paid_amount_cent"`
RefundAmountCent int64 `json:"refund_amount_cent"`
NetPaidAmountCent int64 `json:"net_paid_amount_cent"`
PaymentFeeCent int64 `json:"payment_fee_cent"`
ServiceFeeCent int64 `json:"service_fee_cent"`
MarkupAmountCent int64 `json:"markup_amount_cent"`
TaxAmountCent int64 `json:"tax_amount_cent"`
SplitBaseAmountCent int64 `json:"split_base_amount_cent"`
RoundingAdjustmentCent int64 `json:"rounding_adjustment_cent"`
CreatedAt time.Time `json:"created_at"`
// 关联业务与分销商收益透传字段
ListingNo string `json:"listing_no,omitempty"`
Title string `json:"title,omitempty"`
GameName string `json:"game_name,omitempty"`
ServerRegion string `json:"server_region,omitempty"`
LoginPlatform string `json:"login_platform,omitempty"`
RentDurationHours int `json:"rent_duration_hours,omitempty"`
RentedAt *time.Time `json:"rented_at,omitempty"`
RenterPhone string `json:"renter_phone,omitempty"`
RenterNickname string `json:"renter_nickname,omitempty"`
OwnerRentAmountCent int64 `json:"owner_rent_amount_cent,omitempty"`
BasePriceCent int64 `json:"base_price_cent,omitempty"`
ShelfMarkupCent int64 `json:"shelf_markup_cent,omitempty"`
DepositAmountCent int64 `json:"deposit_amount_cent,omitempty"`
// 当前分销商在该订单中的收益拆解
DistributorEarningCent int64 `json:"distributor_earning_cent"`
DistributorRole string `json:"distributor_role,omitempty"`
DistributorMarkupCent int64 `json:"distributor_markup_cent"`
DistributorSplitCent int64 `json:"distributor_split_cent"`
}
type DistributionOrderListQuery struct {
SiteID uint64
Kind string
Status string
Page int
Size int
SiteID uint64
Kind string
Status string
Keyword string
Page int
Size int
}
type DistributionOrderPage struct {
@@ -143,7 +143,14 @@ func (h *Handler) ListOrders(c *gin.Context) {
}
page, _ := strconv.Atoi(c.DefaultQuery("page", "1"))
size, _ := strconv.Atoi(c.DefaultQuery("page_size", "20"))
result, err := h.service.ListOrders(c.Request.Context(), DistributionOrderListQuery{SiteID: scoped, Kind: c.Query("kind"), Status: c.Query("status"), Page: page, Size: size})
result, err := h.service.ListOrders(c.Request.Context(), DistributionOrderListQuery{
SiteID: scoped,
Kind: c.Query("kind"),
Status: c.Query("status"),
Keyword: c.Query("keyword"),
Page: page,
Size: size,
})
if err != nil {
writeError(c, err)
return
@@ -417,13 +417,22 @@ func (r *Repository) ListOrders(ctx context.Context, query DistributionOrderList
if size > 100 {
size = 100
}
base := r.db.WithContext(ctx).Table("rental_orders ro").Joins("JOIN distribution_order_financials f ON f.order_id = ro.id")
base := r.db.WithContext(ctx).Table("rental_orders ro").
Joins("JOIN distribution_order_financials f ON f.order_id = ro.id").
Joins("LEFT JOIN rental_listings rl ON rl.id = ro.listing_id").
Joins("LEFT JOIN game_accounts ga ON ga.id = ro.account_id").
Joins("LEFT JOIN users u ON u.id = ro.renter_id")
if query.SiteID > 0 {
base = base.Where("ro.distribution_site_id = ?", query.SiteID)
}
if query.Status != "" {
base = base.Where("ro.status = ?", query.Status)
}
if strings.TrimSpace(query.Keyword) != "" {
kw := "%" + strings.TrimSpace(query.Keyword) + "%"
base = base.Where("(ro.order_no LIKE ? OR rl.listing_no LIKE ? OR ga.title LIKE ? OR u.phone LIKE ?)", kw, kw, kw, kw)
}
switch strings.TrimSpace(query.Kind) {
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'))")
@@ -438,16 +447,99 @@ func (r *Repository) ListOrders(ctx context.Context, query DistributionOrderList
return nil, err
}
items := make([]DistributionOrderDTO, 0)
selectSQL := "ro.id AS order_id, ro.order_no, ro.distribution_site_id AS site_id, ro.listing_id, ro.account_id, ro.owner_id, ro.renter_id, ro.status, ro.handoff_status, ro.refund_status, ro.settlement_status, ro.distribution_mode, f.gross_paid_amount_cent, f.refund_amount_cent, f.net_paid_amount_cent, f.payment_fee_cent, f.service_fee_cent, f.markup_amount_cent, f.tax_amount_cent, f.split_base_amount_cent, f.rounding_adjustment_cent, ro.created_at"
selectSQL := `
ro.id AS order_id, ro.order_no, ro.distribution_site_id AS site_id, ro.listing_id, ro.account_id, ro.owner_id, ro.renter_id,
ro.status, ro.handoff_status, ro.refund_status, ro.settlement_status, ro.distribution_mode,
f.gross_paid_amount_cent, f.refund_amount_cent, f.net_paid_amount_cent, f.payment_fee_cent, f.service_fee_cent,
f.markup_amount_cent, f.tax_amount_cent, f.split_base_amount_cent, f.rounding_adjustment_cent, ro.created_at,
COALESCE(rl.listing_no, '') AS listing_no,
COALESCE(ga.title, '') AS title,
COALESCE(ga.game_name, '') AS game_name,
COALESCE(ga.server_region, '') AS server_region,
COALESCE(ga.login_platform, '') AS login_platform,
ro.estimated_duration_hours AS rent_duration_hours,
ro.rented_at AS rented_at,
COALESCE(u.phone, '') AS renter_phone,
COALESCE(u.nickname, '') AS renter_nickname,
COALESCE(ro.owner_rent_amount_cent, 0) AS owner_rent_amount_cent,
COALESCE(ro.base_price_cent, 0) AS base_price_cent,
COALESCE(ro.shelf_markup_cent, 0) AS shelf_markup_cent,
COALESCE(ro.deposit_amount_cent, 0) AS deposit_amount_cent
`
if err := base.Select(selectSQL).Order("ro.id DESC").Offset((page - 1) * size).Limit(size).Scan(&items).Error; err != nil {
return nil, err
}
if len(items) > 0 {
orderIDs := make([]uint64, len(items))
for i, it := range items {
orderIDs[i] = it.OrderID
}
var parties []model.DistributionOrderParty
pq := r.db.WithContext(ctx).Where("order_id IN ?", orderIDs)
if query.SiteID > 0 {
pq = pq.Where("site_id = ?", query.SiteID)
}
if err := pq.Find(&parties).Error; err == nil {
partiesByOrder := make(map[uint64][]model.DistributionOrderParty)
for _, p := range parties {
partiesByOrder[p.OrderID] = append(partiesByOrder[p.OrderID], p)
}
for i := range items {
plist := partiesByOrder[items[i].OrderID]
var totalEarning int64
var markupCent int64
var splitCent int64
var roles []string
for _, p := range plist {
totalEarning += p.IncomeAmountCent
if p.PartyRole == "distributor_sell" || p.PartyRole == "distributor_b" {
roles = append(roles, "sell")
markupCent += items[i].ShelfMarkupCent
splitCent += (p.IncomeAmountCent - items[i].ShelfMarkupCent)
} else if p.PartyRole == "distributor_receive" || p.PartyRole == "distributor_a" {
roles = append(roles, "receive")
splitCent += p.IncomeAmountCent
}
}
items[i].DistributorEarningCent = totalEarning
items[i].DistributorMarkupCent = markupCent
items[i].DistributorSplitCent = splitCent
items[i].DistributorRole = strings.Join(roles, ",")
}
}
}
return &DistributionOrderPage{Items: items, Total: total, Page: page, PageSize: size}, nil
}
func (r *Repository) FindOrder(ctx context.Context, adminID, id uint64) (*DistributionOrderDTO, error) {
var item DistributionOrderDTO
err := r.db.WithContext(ctx).Table("rental_orders ro").Joins("JOIN distribution_order_financials f ON f.order_id = ro.id").Select("ro.id AS order_id, ro.order_no, ro.distribution_site_id AS site_id, ro.listing_id, ro.account_id, ro.owner_id, ro.renter_id, ro.status, ro.handoff_status, ro.refund_status, ro.settlement_status, ro.distribution_mode, f.gross_paid_amount_cent, f.refund_amount_cent, f.net_paid_amount_cent, f.payment_fee_cent, f.service_fee_cent, f.markup_amount_cent, f.tax_amount_cent, f.split_base_amount_cent, f.rounding_adjustment_cent, ro.created_at").Where("ro.id = ?", id).Scan(&item).Error
selectSQL := `
ro.id AS order_id, ro.order_no, ro.distribution_site_id AS site_id, ro.listing_id, ro.account_id, ro.owner_id, ro.renter_id,
ro.status, ro.handoff_status, ro.refund_status, ro.settlement_status, ro.distribution_mode,
f.gross_paid_amount_cent, f.refund_amount_cent, f.net_paid_amount_cent, f.payment_fee_cent, f.service_fee_cent,
f.markup_amount_cent, f.tax_amount_cent, f.split_base_amount_cent, f.rounding_adjustment_cent, ro.created_at,
COALESCE(rl.listing_no, '') AS listing_no,
COALESCE(ga.title, '') AS title,
COALESCE(ga.game_name, '') AS game_name,
COALESCE(ga.server_region, '') AS server_region,
COALESCE(ga.login_platform, '') AS login_platform,
ro.estimated_duration_hours AS rent_duration_hours,
ro.rented_at AS rented_at,
COALESCE(u.phone, '') AS renter_phone,
COALESCE(u.nickname, '') AS renter_nickname,
COALESCE(ro.owner_rent_amount_cent, 0) AS owner_rent_amount_cent,
COALESCE(ro.base_price_cent, 0) AS base_price_cent,
COALESCE(ro.shelf_markup_cent, 0) AS shelf_markup_cent,
COALESCE(ro.deposit_amount_cent, 0) AS deposit_amount_cent
`
err := r.db.WithContext(ctx).Table("rental_orders ro").
Joins("JOIN distribution_order_financials f ON f.order_id = ro.id").
Joins("LEFT JOIN rental_listings rl ON rl.id = ro.listing_id").
Joins("LEFT JOIN game_accounts ga ON ga.id = ro.account_id").
Joins("LEFT JOIN users u ON u.id = ro.renter_id").
Select(selectSQL).Where("ro.id = ?", id).Scan(&item).Error
if err != nil {
return nil, err
}
@@ -457,6 +549,28 @@ func (r *Repository) FindOrder(ctx context.Context, adminID, id uint64) (*Distri
if err := r.ensureSiteAccess(ctx, adminID, item.SiteID); err != nil {
return nil, err
}
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 {
var totalEarning int64
var markupCent int64
var splitCent int64
var roles []string
for _, p := range parties {
totalEarning += p.IncomeAmountCent
if p.PartyRole == "distributor_sell" || p.PartyRole == "distributor_b" {
roles = append(roles, "sell")
markupCent += item.ShelfMarkupCent
splitCent += (p.IncomeAmountCent - item.ShelfMarkupCent)
} else if p.PartyRole == "distributor_receive" || p.PartyRole == "distributor_a" {
roles = append(roles, "receive")
splitCent += p.IncomeAmountCent
}
}
item.DistributorEarningCent = totalEarning
item.DistributorMarkupCent = markupCent
item.DistributorSplitCent = splitCent
item.DistributorRole = strings.Join(roles, ",")
}
return &item, nil
}