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:
@@ -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
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user