feat(distribution): 完善分销站与主站业务数据及权益风控隔离,修复历史商品归属
This commit is contained in:
@@ -103,7 +103,8 @@ func (h *Handler) ListMine(c *gin.Context) {
|
||||
response.Unauthorized(c, "缺少用户上下文")
|
||||
return
|
||||
}
|
||||
items, err := h.service.ListMine(c.Request.Context(), ownerID)
|
||||
siteCode := strings.TrimSpace(c.GetHeader("X-Distribution-Site-Code"))
|
||||
items, err := h.service.ListMine(c.Request.Context(), ownerID, siteCode)
|
||||
if err != nil {
|
||||
writeListingError(c, err)
|
||||
return
|
||||
|
||||
@@ -170,12 +170,18 @@ func (r *Repository) FindAdmin(ctx context.Context, listingID uint64) (*ListingD
|
||||
return r.findDTO(ctx, "l.id = ?", listingID)
|
||||
}
|
||||
|
||||
func (r *Repository) ListMine(ctx context.Context, ownerID uint64) ([]ListingDTO, error) {
|
||||
func (r *Repository) ListMine(ctx context.Context, ownerID uint64, siteCode ...string) ([]ListingDTO, error) {
|
||||
var rows []listingRow
|
||||
err := r.baseQuery(ctx).
|
||||
Where("l.owner_id = ?", ownerID).
|
||||
Order("l.id DESC").
|
||||
Scan(&rows).Error
|
||||
base := r.baseQuery(ctx).Where("l.owner_id = ?", ownerID)
|
||||
if len(siteCode) > 0 {
|
||||
code := strings.TrimSpace(siteCode[0])
|
||||
if code != "" {
|
||||
base = base.Joins("INNER JOIN distribution_sites AS my_ds ON my_ds.id = l.distribution_site_id AND my_ds.site_code = ?", code)
|
||||
} else {
|
||||
base = base.Where("l.distribution_site_id IS NULL OR l.distribution_site_id = 0")
|
||||
}
|
||||
}
|
||||
err := base.Order("l.id DESC").Scan(&rows).Error
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
@@ -11,11 +11,11 @@ func (s *Service) ListPublic(ctx context.Context, query PublicListQuery) (*Publi
|
||||
return s.repo.ListPublic(ctx, query)
|
||||
}
|
||||
|
||||
func (s *Service) ListMine(ctx context.Context, ownerID uint64) ([]ListingDTO, error) {
|
||||
func (s *Service) ListMine(ctx context.Context, ownerID uint64, siteCode ...string) ([]ListingDTO, error) {
|
||||
if s.repo == nil {
|
||||
return nil, ErrDependencyUnavailable
|
||||
}
|
||||
return s.repo.ListMine(ctx, ownerID)
|
||||
return s.repo.ListMine(ctx, ownerID, siteCode...)
|
||||
}
|
||||
|
||||
func (s *Service) FindPublic(ctx context.Context, id uint64) (*ListingDTO, error) {
|
||||
|
||||
@@ -888,3 +888,63 @@ func TestCreateListingWithDistributionSiteCode(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestListMineSiteIsolation(t *testing.T) {
|
||||
db := database.NewTestDB()
|
||||
if err := database.MigrateListingLifecycleTestSchema(db); err != nil {
|
||||
t.Fatalf("migrate err: %v", err)
|
||||
}
|
||||
repo := NewRepository(db, nil)
|
||||
|
||||
user := model.User{Phone: "18800000002", Nickname: "多站卖家", RealnameStatus: "verified", Status: "active"}
|
||||
db.Create(&user)
|
||||
|
||||
site := model.DistributionSite{
|
||||
SiteCode: "dist_x",
|
||||
SiteName: "分销X",
|
||||
Domain: "x.com",
|
||||
Status: "active",
|
||||
DisplayAccountEnabled: true,
|
||||
}
|
||||
db.Create(&site)
|
||||
|
||||
req1 := CreateRequest{Title: "主站商品", ServerRegion: "QQ", LoginPlatform: "PC", PriceCent: 1000}
|
||||
l1, err := repo.Create(context.Background(), user.ID, req1, false, 0)
|
||||
if err != nil {
|
||||
t.Fatalf("Create main failed: %v", err)
|
||||
}
|
||||
|
||||
req2 := CreateRequest{Title: "分销商品", ServerRegion: "QQ", LoginPlatform: "PC", PriceCent: 2000}
|
||||
l2, err := repo.Create(context.Background(), user.ID, req2, false, 0, "dist_x")
|
||||
if err != nil {
|
||||
t.Fatalf("Create dist failed: %v", err)
|
||||
}
|
||||
|
||||
// 1. 分销站查询只返回分销站商品
|
||||
distItems, err := repo.ListMine(context.Background(), user.ID, "dist_x")
|
||||
if err != nil {
|
||||
t.Fatalf("ListMine dist_x err: %v", err)
|
||||
}
|
||||
if len(distItems) != 1 || distItems[0].ID != l2.ID {
|
||||
t.Fatalf("expected only dist listing %d, got %v", l2.ID, distItems)
|
||||
}
|
||||
|
||||
// 2. 主站查询只返回主站商品
|
||||
mainItems, err := repo.ListMine(context.Background(), user.ID, "")
|
||||
if err != nil {
|
||||
t.Fatalf("ListMine main err: %v", err)
|
||||
}
|
||||
if len(mainItems) != 1 || mainItems[0].ID != l1.ID {
|
||||
t.Fatalf("expected only main listing %d, got %v", l1.ID, mainItems)
|
||||
}
|
||||
|
||||
// 3. 不传站点时兼容返回全部
|
||||
allItems, err := repo.ListMine(context.Background(), user.ID)
|
||||
if err != nil {
|
||||
t.Fatalf("ListMine all err: %v", err)
|
||||
}
|
||||
if len(allItems) != 2 {
|
||||
t.Fatalf("expected 2 listings, got %d", len(allItems))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -47,8 +47,10 @@ func (r *Repository) finalizeCheckout(tx *gorm.DB, order *model.RentalOrder, che
|
||||
if err := appendCheckoutCompletedNotifications(tx, order, renterContent); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := rentergrowth.AwardOrderCompleted(tx, order, settlement.PureCoinAmountCent, rentergrowth.SourceOrderCompleted); err != nil {
|
||||
return nil, err
|
||||
if order.DistributionSiteID == nil || *order.DistributionSiteID == 0 {
|
||||
if err := rentergrowth.AwardOrderCompleted(tx, order, settlement.PureCoinAmountCent, rentergrowth.SourceOrderCompleted); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
if err := saveFinalizedCheckout(tx, order, checkout, listing, account); err != nil {
|
||||
return nil, err
|
||||
|
||||
@@ -228,6 +228,7 @@ type SellerHandoffQuery struct {
|
||||
Page int
|
||||
PageSize int
|
||||
Status string
|
||||
SiteCode string
|
||||
}
|
||||
|
||||
// SellerHandoffMetrics 为号主待办页提供跨分页的状态统计。
|
||||
|
||||
@@ -34,7 +34,7 @@ func (h *Handler) List(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
page, pageSize := parsePagination(c)
|
||||
siteCode := strings.TrimSpace(firstNonEmpty(c.Query("distribution_site_code"), c.Query("site_code")))
|
||||
siteCode := strings.TrimSpace(firstNonEmpty(c.GetHeader("X-Distribution-Site-Code"), c.Query("distribution_site_code"), c.Query("site_code")))
|
||||
items, err := h.service.ListForUser(c.Request.Context(), userID, page, pageSize, siteCode)
|
||||
if err != nil {
|
||||
writeOrderError(c, err)
|
||||
@@ -50,10 +50,12 @@ func (h *Handler) ListSellerHandoffs(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
page, pageSize := parsePagination(c)
|
||||
siteCode := strings.TrimSpace(firstNonEmpty(c.GetHeader("X-Distribution-Site-Code"), c.Query("distribution_site_code"), c.Query("site_code")))
|
||||
items, err := h.service.ListSellerHandoffs(c.Request.Context(), userID, SellerHandoffQuery{
|
||||
Page: page,
|
||||
PageSize: pageSize,
|
||||
Status: c.Query("status"),
|
||||
SiteCode: siteCode,
|
||||
})
|
||||
if err != nil {
|
||||
writeOrderError(c, err)
|
||||
@@ -72,7 +74,7 @@ func (h *Handler) Detail(c *gin.Context) {
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
siteCode := strings.TrimSpace(firstNonEmpty(c.Query("distribution_site_code"), c.Query("site_code")))
|
||||
siteCode := strings.TrimSpace(firstNonEmpty(c.GetHeader("X-Distribution-Site-Code"), c.Query("distribution_site_code"), c.Query("site_code")))
|
||||
item, err := h.service.FindForUser(c.Request.Context(), userID, id, siteCode)
|
||||
if err != nil {
|
||||
writeOrderError(c, err)
|
||||
|
||||
@@ -63,9 +63,20 @@ func (r *Repository) Create(ctx context.Context, renterID uint64, req CreateRequ
|
||||
return err
|
||||
}
|
||||
rentOriginalAmountCent := pricing.RentAmountCent
|
||||
growthSnapshot, err := rentergrowth.SnapshotForUser(tx, renterID)
|
||||
if err != nil {
|
||||
return err
|
||||
var growthSnapshot rentergrowth.DiscountSnapshot
|
||||
if distributionSite == nil {
|
||||
snap, err := rentergrowth.SnapshotForUser(tx, renterID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
growthSnapshot = snap
|
||||
} else {
|
||||
// 分销站点订单隔离主站成长等级免押与折扣,防止主站免押坏账与折扣侵蚀分销收益
|
||||
growthSnapshot = rentergrowth.DiscountSnapshot{
|
||||
LevelCode: rentergrowth.DefaultLevelCode,
|
||||
LevelName: rentergrowth.DefaultLevelName,
|
||||
DiscountBps: rentergrowth.DefaultDiscountBps,
|
||||
}
|
||||
}
|
||||
rentDiscountAmountCent := rentergrowth.CalculateDiscountCent(pricing.PureCoinAmountCent, pricing.PureCoinPlatformFeeCent, growthSnapshot.DiscountBps)
|
||||
if rentDiscountAmountCent > 0 {
|
||||
|
||||
@@ -63,6 +63,11 @@ func (r *Repository) ListSellerHandoffs(ctx context.Context, userID uint64, quer
|
||||
if query.Status != "" {
|
||||
base = base.Where("o.status = ?", query.Status)
|
||||
}
|
||||
if query.SiteCode != "" {
|
||||
base = base.Joins("INNER JOIN distribution_sites AS ds ON ds.id = o.distribution_site_id AND ds.site_code = ?", query.SiteCode)
|
||||
} else {
|
||||
base = base.Where("o.distribution_site_id IS NULL OR o.distribution_site_id = 0")
|
||||
}
|
||||
|
||||
var total int64
|
||||
if err := base.Session(&gorm.Session{}).Count(&total).Error; err != nil {
|
||||
@@ -77,7 +82,7 @@ func (r *Repository) ListSellerHandoffs(ctx context.Context, userID uint64, quer
|
||||
for _, row := range rows {
|
||||
items = append(items, row.toUserListItem(userID, nil))
|
||||
}
|
||||
metrics, err := r.sellerHandoffMetrics(ctx, userID)
|
||||
metrics, err := r.sellerHandoffMetrics(ctx, userID, query.SiteCode)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -90,16 +95,21 @@ func (r *Repository) ListSellerHandoffs(ctx context.Context, userID uint64, quer
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (r *Repository) sellerHandoffMetrics(ctx context.Context, userID uint64) (SellerHandoffMetrics, error) {
|
||||
func (r *Repository) sellerHandoffMetrics(ctx context.Context, userID uint64, siteCode ...string) (SellerHandoffMetrics, error) {
|
||||
base := r.db.WithContext(ctx).Table("rental_orders AS o").Where("o.owner_id = ?", userID)
|
||||
if len(siteCode) > 0 && siteCode[0] != "" {
|
||||
base = base.Joins("INNER JOIN distribution_sites AS ds ON ds.id = o.distribution_site_id AND ds.site_code = ?", siteCode[0])
|
||||
} else if len(siteCode) > 0 {
|
||||
base = base.Where("o.distribution_site_id IS NULL OR o.distribution_site_id = 0")
|
||||
}
|
||||
var metrics SellerHandoffMetrics
|
||||
if err := base.Where("o.status = ? AND o.handoff_status = ?", orderStatusPendingHandoff, handoffStatusPendingOwner).Count(&metrics.PendingHandoff).Error; err != nil {
|
||||
if err := base.Session(&gorm.Session{}).Where("o.status = ? AND o.handoff_status = ?", orderStatusPendingHandoff, handoffStatusPendingOwner).Count(&metrics.PendingHandoff).Error; err != nil {
|
||||
return SellerHandoffMetrics{}, err
|
||||
}
|
||||
if err := r.db.WithContext(ctx).Table("rental_orders AS o").Where("o.owner_id = ? AND o.status = ?", userID, orderStatusPendingCheckoutConfirm).Count(&metrics.PendingCheckout).Error; err != nil {
|
||||
if err := base.Session(&gorm.Session{}).Where("o.status = ?", orderStatusPendingCheckoutConfirm).Count(&metrics.PendingCheckout).Error; err != nil {
|
||||
return SellerHandoffMetrics{}, err
|
||||
}
|
||||
if err := r.db.WithContext(ctx).Table("rental_orders AS o").Where("o.owner_id = ? AND o.status IN ?", userID, sellerHandoffAbnormalStatuses()).Count(&metrics.Abnormal).Error; err != nil {
|
||||
if err := base.Session(&gorm.Session{}).Where("o.status IN ?", sellerHandoffAbnormalStatuses()).Count(&metrics.Abnormal).Error; err != nil {
|
||||
return SellerHandoffMetrics{}, err
|
||||
}
|
||||
return metrics, nil
|
||||
|
||||
Reference in New Issue
Block a user