feat(distribution): 独立分销前台商城多租户体系、货源与订单数据严格隔离

This commit is contained in:
yml2213
2026-09-07 17:18:14 +08:00
parent 550b42eb22
commit ecaf15417b
34 changed files with 2397 additions and 119 deletions
+2
View File
@@ -75,6 +75,7 @@ type OrderDTO struct {
Checkout *CheckoutDTO `json:"checkout,omitempty"`
AdminActions *AdminActionsDTO `json:"admin_actions,omitempty"`
PaymentDeadlineAt *time.Time `json:"payment_deadline_at,omitempty"`
DistributionSiteID *uint64 `json:"distribution_site_id,omitempty"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
}
@@ -95,6 +96,7 @@ type UserOrderListItemDTO struct {
Status string `json:"status"`
HandoffStatus string `json:"handoff_status"`
PaymentDeadlineAt *time.Time `json:"payment_deadline_at,omitempty"`
DistributionSiteID *uint64 `json:"distribution_site_id,omitempty"`
CreatedAt time.Time `json:"created_at"`
}
@@ -1,6 +1,8 @@
package order
import (
"strings"
"hfb_sys/backend/pkg/response"
"github.com/gin-gonic/gin"
@@ -32,7 +34,8 @@ func (h *Handler) List(c *gin.Context) {
return
}
page, pageSize := parsePagination(c)
items, err := h.service.ListForUser(c.Request.Context(), userID, page, pageSize)
siteCode := strings.TrimSpace(firstNonEmpty(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)
return
@@ -69,7 +72,8 @@ func (h *Handler) Detail(c *gin.Context) {
if !ok {
return
}
item, err := h.service.FindForUser(c.Request.Context(), userID, id)
siteCode := strings.TrimSpace(firstNonEmpty(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)
return
@@ -129,6 +129,7 @@ func (row orderRow) toAdminDTO() OrderDTO {
DepositHeldAt: row.DepositHeldAt,
DepositHoldReleasedAt: row.DepositHoldReleasedAt,
AdminActions: adminActionsForOrder(row.RentalOrder),
DistributionSiteID: row.DistributionSiteID,
CreatedAt: row.CreatedAt,
UpdatedAt: row.UpdatedAt,
}
@@ -187,6 +188,7 @@ func (row orderRow) toUserListItem(userID uint64, paymentDeadlineAt *time.Time)
Status: row.Status,
HandoffStatus: row.HandoffStatus,
PaymentDeadlineAt: paymentDeadlineAt,
DistributionSiteID: row.DistributionSiteID,
CreatedAt: row.CreatedAt,
}
}
+30 -8
View File
@@ -11,13 +11,25 @@ import (
"gorm.io/gorm"
)
func (r *Repository) ListForUser(ctx context.Context, userID uint64, page, pageSize int) (*UserPaginatedResult, error) {
func (r *Repository) ListForUser(ctx context.Context, userID uint64, page, pageSize int, siteCode ...string) (*UserPaginatedResult, error) {
page, pageSize = normalizePagination(page, pageSize)
base := r.baseQuery(ctx).Where("o.renter_id = ? OR o.owner_id = ?", userID, userID)
countBase := r.db.WithContext(ctx).Table("rental_orders AS o").Where("o.renter_id = ? OR o.owner_id = ?", userID, userID)
var code string
if len(siteCode) > 0 {
code = strings.TrimSpace(siteCode[0])
}
if code != "" {
base = base.Joins("INNER JOIN distribution_sites AS ds ON ds.id = o.distribution_site_id AND ds.site_code = ?", code)
countBase = countBase.Joins("INNER JOIN distribution_sites AS ds ON ds.id = o.distribution_site_id AND ds.site_code = ?", code)
} else {
base = base.Where("o.distribution_site_id IS NULL OR o.distribution_site_id = 0")
countBase = countBase.Where("o.distribution_site_id IS NULL OR o.distribution_site_id = 0")
}
var total int64
if err := r.db.WithContext(ctx).Table("rental_orders AS o").
Where("o.renter_id = ? OR o.owner_id = ?", userID, userID).
Count(&total).Error; err != nil {
if err := countBase.Count(&total).Error; err != nil {
return nil, err
}
var rows []orderRow
@@ -308,12 +320,22 @@ func (r *Repository) FindLatestAdminByListing(ctx context.Context, listingID uin
return &dto, nil
}
func (r *Repository) FindForUser(ctx context.Context, userID uint64, orderID uint64) (*OrderDTO, error) {
func (r *Repository) FindForUser(ctx context.Context, userID uint64, orderID uint64, siteCode ...string) (*OrderDTO, error) {
var row orderRow
db := r.db.WithContext(ctx)
if err := r.baseQuery(ctx).
Where("o.id = ? AND (o.renter_id = ? OR o.owner_id = ?)", orderID, userID, userID).
First(&row).Error; err != nil {
query := r.baseQuery(ctx).Where("o.id = ? AND (o.renter_id = ? OR o.owner_id = ?)", orderID, userID, userID)
var code string
if len(siteCode) > 0 {
code = strings.TrimSpace(siteCode[0])
}
if code != "" {
query = query.Joins("INNER JOIN distribution_sites AS ds ON ds.id = o.distribution_site_id AND ds.site_code = ?", code)
} else if len(siteCode) > 0 {
query = query.Where("o.distribution_site_id IS NULL OR o.distribution_site_id = 0")
}
if err := query.First(&row).Error; err != nil {
return nil, err
}
dto := row.toDTOForUser(userID)
@@ -1884,3 +1884,91 @@ func TestAdminPlatformCheckoutConfirmRequiresExplicitConfirmationForZeroOwnerInc
t.Fatalf("确认零收入后订单状态 = %s, want %s", completed.Status, orderStatusCompleted)
}
}
func TestListForUserAndFindForUserSiteIsolation(t *testing.T) {
db := setupOrderTestDB(t)
if err := db.AutoMigrate(&model.DistributionSite{}); err != nil {
t.Fatalf("migrate distribution_sites err: %v", err)
}
repo := NewRepository(db)
owner := model.User{Phone: "13800000010"}
db.Create(&owner)
renter := model.User{Phone: "13800000020"}
db.Create(&renter)
account := model.GameAccount{OwnerID: owner.ID, Title: "隔离测试账号", Status: "active"}
db.Create(&account)
listing := model.RentalListing{ListingNo: "L30001", AccountID: account.ID, OwnerID: owner.ID, Status: "published", ReviewStatus: "approved", PriceCent: 1000}
db.Create(&listing)
site := model.DistributionSite{
SiteCode: "site_isolate_1",
SiteName: "隔离分销站",
Domain: "isolate.com",
Status: "active",
DisplayAccountEnabled: true,
}
db.Create(&site)
// Order 1: on main site (distribution_site_id is nil)
mainOrder := model.RentalOrder{
OrderNo: "RO_MAIN_001",
ListingID: listing.ID,
AccountID: account.ID,
OwnerID: owner.ID,
RenterID: renter.ID,
Status: orderStatusCompleted,
}
db.Create(&mainOrder)
// Order 2: on distribution site (distribution_site_id = site.ID)
distOrder := model.RentalOrder{
OrderNo: "RO_DIST_001",
ListingID: listing.ID,
AccountID: account.ID,
OwnerID: owner.ID,
RenterID: renter.ID,
Status: orderStatusCompleted,
DistributionSiteID: &site.ID,
}
db.Create(&distOrder)
ctx := context.Background()
// 1. Query with site_code = "site_isolate_1": should return ONLY distOrder
distList, err := repo.ListForUser(ctx, renter.ID, 1, 20, "site_isolate_1")
if err != nil {
t.Fatalf("ListForUser distribution err: %v", err)
}
if len(distList.Items) != 1 || distList.Items[0].OrderNo != "RO_DIST_001" {
t.Fatalf("expected only RO_DIST_001, got %+v", distList.Items)
}
// 2. Query without siteCode (main site mode): should return ONLY mainOrder
mainList, err := repo.ListForUser(ctx, renter.ID, 1, 20, "")
if err != nil {
t.Fatalf("ListForUser main err: %v", err)
}
if len(mainList.Items) != 1 || mainList.Items[0].OrderNo != "RO_MAIN_001" {
t.Fatalf("expected only RO_MAIN_001, got %+v", mainList.Items)
}
// 3. FindForUser: distOrder found under "site_isolate_1"
dItem, err := repo.FindForUser(ctx, renter.ID, distOrder.ID, "site_isolate_1")
if err != nil || dItem.OrderNo != "RO_DIST_001" {
t.Fatalf("FindForUser distOrder failed: %v", err)
}
// 4. FindForUser: mainOrder CANNOT be accessed under "site_isolate_1"
_, err = repo.FindForUser(ctx, renter.ID, mainOrder.ID, "site_isolate_1")
if err == nil {
t.Fatalf("expected error accessing mainOrder under distribution site, got nil")
}
// 5. FindForUser: distOrder CANNOT be accessed under main site mode ("")
_, err = repo.FindForUser(ctx, renter.ID, distOrder.ID, "")
if err == nil {
t.Fatalf("expected error accessing distOrder under main site mode, got nil")
}
}
+4 -4
View File
@@ -148,11 +148,11 @@ func (s *Service) AcceptCheckout(ctx context.Context, userID uint64, orderID uin
return s.repo.AcceptCheckout(ctx, userID, orderID)
}
func (s *Service) ListForUser(ctx context.Context, userID uint64, page, pageSize int) (*UserPaginatedResult, error) {
func (s *Service) ListForUser(ctx context.Context, userID uint64, page, pageSize int, siteCode ...string) (*UserPaginatedResult, error) {
if s.repo == nil {
return nil, ErrDependencyUnavailable
}
return s.repo.ListForUser(ctx, userID, page, pageSize)
return s.repo.ListForUser(ctx, userID, page, pageSize, siteCode...)
}
func (s *Service) ListSellerHandoffs(ctx context.Context, userID uint64, query SellerHandoffQuery) (*SellerHandoffPaginatedResult, error) {
@@ -377,9 +377,9 @@ func (s *Service) PendingRefundCount(ctx context.Context) (int64, error) {
return s.repo.PendingRefundCount(ctx)
}
func (s *Service) FindForUser(ctx context.Context, userID uint64, orderID uint64) (*OrderDTO, error) {
func (s *Service) FindForUser(ctx context.Context, userID uint64, orderID uint64, siteCode ...string) (*OrderDTO, error) {
if s.repo == nil {
return nil, ErrDependencyUnavailable
}
return s.repo.FindForUser(ctx, userID, orderID)
return s.repo.FindForUser(ctx, userID, orderID, siteCode...)
}