diff --git a/backend/internal/database/test_helper.go b/backend/internal/database/test_helper.go index b1c7a2a..8a64e7d 100644 --- a/backend/internal/database/test_helper.go +++ b/backend/internal/database/test_helper.go @@ -43,6 +43,8 @@ func MigrateListingLifecycleTestSchema(db *gorm.DB) error { &model.RentalListing{}, &model.ListingUpload{}, &model.ListingStatusEvent{}, + &model.DistributionSite{}, + &model.DistributionSiteAccount{}, ) } diff --git a/backend/internal/model/listing.go b/backend/internal/model/listing.go index 53cf73c..386c42f 100644 --- a/backend/internal/model/listing.go +++ b/backend/internal/model/listing.go @@ -40,11 +40,12 @@ type RentalListing struct { ReviewStatus string `gorm:"size:32;not null;default:'none'" json:"review_status"` HandoffMode string `gorm:"size:16;not null;default:'owner';index" json:"handoff_mode"` SettlementMode string `gorm:"size:32;not null;default:'owner_wallet'" json:"settlement_mode"` - ManagedAdminID *uint64 `gorm:"index" json:"managed_admin_id"` - ReviewReason string `gorm:"size:255;not null;default:''" json:"review_reason"` - PublishedAt *time.Time `json:"published_at"` - CreatedAt time.Time `json:"created_at"` - UpdatedAt time.Time `json:"updated_at"` + ManagedAdminID *uint64 `gorm:"index" json:"managed_admin_id"` + DistributionSiteID *uint64 `gorm:"column:distribution_site_id;index" json:"distribution_site_id,omitempty"` + ReviewReason string `gorm:"size:255;not null;default:''" json:"review_reason"` + PublishedAt *time.Time `json:"published_at"` + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` } func (RentalListing) TableName() string { diff --git a/backend/internal/modules/distribution/lifecycle.go b/backend/internal/modules/distribution/lifecycle.go index 8702844..c041aad 100644 --- a/backend/internal/modules/distribution/lifecycle.go +++ b/backend/internal/modules/distribution/lifecycle.go @@ -24,28 +24,66 @@ func distributionTablesPresent(tx *gorm.DB) bool { return distributionTablePresent } -// ResolveOrderSourceTx 校验站点、账号展示关系和生效规则,供正常订单创建事务写入快照引用。 -// 第一期仅启用 two_party_distributor_receive 模式,其他模式规则不会命中(见设计文档实施顺序)。 +// ResolveOrderSourceTx 校验收号方站点与售出方站点、账号展示关系和生效规则,供正常订单创建事务写入快照引用。 func ResolveOrderSourceTx(tx *gorm.DB, siteCode string, listingID, accountID uint64, at time.Time) (*model.DistributionSite, *model.DistributionRuleVersion, error) { - if siteCode == "" { + var listing model.RentalListing + var listingSiteID *uint64 + if listingID > 0 { + if err := tx.Select("id, distribution_site_id").First(&listing, listingID).Error; err == nil { + listingSiteID = listing.DistributionSiteID + } + } + + // 1. 两方均无分销站:纯主站交易 + if siteCode == "" && listingSiteID == nil { return nil, nil, nil } - var site model.DistributionSite - if err := tx.Where("site_code = ? AND status = 'active' AND display_account_enabled = ?", siteCode, true).First(&site).Error; err != nil { + + // 2. 模式一:分销网收号、主站卖出 (ModeTwoPartyDistributorReceive) + if siteCode == "" && listingSiteID != nil { + var site model.DistributionSite + if err := tx.Where("id = ? AND status = 'active'", *listingSiteID).First(&site).Error; err != nil { + return nil, nil, ErrSiteNotFound + } + var rule model.DistributionRuleVersion + query := tx.Where("mode = ? AND status = 'active' AND effective_from <= ? AND (effective_to IS NULL OR effective_to > ?)", ModeTwoPartyDistributorReceive, at, at). + Where("site_id = ? OR site_id IS NULL", site.ID). + Order("CASE WHEN site_id IS NULL THEN 1 ELSE 0 END, effective_from DESC, id DESC") + if err := query.First(&rule).Error; err != nil { + return nil, nil, ErrRuleNotFound + } + return &site, &rule, nil + } + + // 3. 买家在分销站下单:校验售出站点状态与账号展示关系 + var sellSite model.DistributionSite + if err := tx.Where("site_code = ? AND status = 'active' AND display_account_enabled = ?", siteCode, true).First(&sellSite).Error; err != nil { return nil, nil, ErrSiteNotFound } var relation model.DistributionSiteAccount - if err := tx.Where("site_id = ? AND listing_id = ? AND account_id = ? AND display_status = 'enabled'", site.ID, listingID, accountID).First(&relation).Error; err != nil { + if err := tx.Where("site_id = ? AND listing_id = ? AND account_id = ? AND display_status = 'enabled'", sellSite.ID, listingID, accountID).First(&relation).Error; err != nil { return nil, nil, ErrAccountDisplayNotFound } + + // 4. 解析生效规则:优先匹配有效规则版本 + targetMode := ModeTwoPartyDistributorReceive + if listingSiteID != nil && *listingSiteID != sellSite.ID { + targetMode = ModeThreeParty + } var rule model.DistributionRuleVersion - query := tx.Where("mode = ? AND status = 'active' AND effective_from <= ? AND (effective_to IS NULL OR effective_to > ?)", ModeTwoPartyDistributorReceive, at, at). - Where("site_id = ? OR site_id IS NULL", site.ID). - Order("CASE WHEN site_id IS NULL THEN 1 ELSE 0 END, effective_from DESC, id DESC") + query := tx.Where("status = 'active' AND effective_from <= ? AND (effective_to IS NULL OR effective_to > ?)", at, at). + Where("site_id = ? OR site_id IS NULL", sellSite.ID) + if targetMode == ModeThreeParty { + query = query.Where("mode = ?", ModeThreeParty) + } else { + query = query.Where("mode IN (?, ?)", ModeTwoPartyDistributorReceive, ModeTwoPartyPlatformReceive). + Order("CASE WHEN mode = '" + ModeTwoPartyDistributorReceive + "' THEN 0 ELSE 1 END") + } + query = query.Order("CASE WHEN site_id IS NULL THEN 1 ELSE 0 END, effective_from DESC, id DESC") if err := query.First(&rule).Error; err != nil { return nil, nil, ErrRuleNotFound } - return &site, &rule, nil + return &sellSite, &rule, nil } // calculateForRule 按规则快照计算手续费、服务费和分成基数。 @@ -99,6 +137,12 @@ func SnapshotPaidOrderTx(tx *gorm.DB, orderID uint64, grossPaidCent int64) error partyID := *order.DistributionSiteID if partyType == "platform" { partyID = 0 + } else if split.PartyRole == "distributor_a" { + // 三方模式下 distributor_a 为商品收号站 + var listing model.RentalListing + if err := tx.Select("distribution_site_id").First(&listing, order.ListingID).Error; err == nil && listing.DistributionSiteID != nil { + partyID = *listing.DistributionSiteID + } } parties = append(parties, model.DistributionOrderParty{OrderID: order.ID, SiteID: *order.DistributionSiteID, PartyRole: split.PartyRole, PartyType: partyType, PartyID: partyID, Ratio: split.Ratio, IncomeAmountCent: split.AmountCent, ProfitAmountCent: split.AmountCent}) } diff --git a/backend/internal/modules/listing/dto.go b/backend/internal/modules/listing/dto.go index 9de4c08..6a46c07 100644 --- a/backend/internal/modules/listing/dto.go +++ b/backend/internal/modules/listing/dto.go @@ -38,6 +38,9 @@ type ListingDTO struct { ManagedAdminID *uint64 `json:"managed_admin_id,omitempty"` ReviewReason string `json:"review_reason"` PublishedAt *time.Time `json:"published_at"` + DistributionSiteID *uint64 `json:"distribution_site_id,omitempty"` + DistributionSiteCode string `json:"distribution_site_code,omitempty"` + DistributionSiteName string `json:"distribution_site_name,omitempty"` ListingGroupConversationID uint64 `json:"listing_group_conversation_id,omitempty"` CreatedAt time.Time `json:"created_at"` UpdatedAt time.Time `json:"updated_at"` diff --git a/backend/internal/modules/listing/handler_user.go b/backend/internal/modules/listing/handler_user.go index b236cce..86d5d8a 100644 --- a/backend/internal/modules/listing/handler_user.go +++ b/backend/internal/modules/listing/handler_user.go @@ -18,7 +18,8 @@ func (h *Handler) Create(c *gin.Context) { response.BadRequest(c, listingBindErrorMessage(err)) return } - item, err := h.service.Create(c.Request.Context(), ownerID, req) + siteCode := strings.TrimSpace(c.GetHeader("X-Distribution-Site-Code")) + item, err := h.service.Create(c.Request.Context(), ownerID, req, siteCode) if err != nil { writeListingError(c, err) return diff --git a/backend/internal/modules/listing/mutation.go b/backend/internal/modules/listing/mutation.go index 5c07519..68c62cf 100644 --- a/backend/internal/modules/listing/mutation.go +++ b/backend/internal/modules/listing/mutation.go @@ -15,7 +15,7 @@ import ( "gorm.io/gorm/clause" ) -func (r *Repository) Create(ctx context.Context, ownerID uint64, req CreateRequest, reviewRequired bool, publishCooldown time.Duration) (*ListingDTO, error) { +func (r *Repository) Create(ctx context.Context, ownerID uint64, req CreateRequest, reviewRequired bool, publishCooldown time.Duration, siteCode ...string) (*ListingDTO, error) { var dto *ListingDTO err := r.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error { now := time.Now() @@ -51,20 +51,44 @@ func (r *Repository) Create(ctx context.Context, ownerID uint64, req CreateReque if err := tx.Create(&account).Error; err != nil { return err } + + var site model.DistributionSite + var siteID *uint64 + if len(siteCode) > 0 && strings.TrimSpace(siteCode[0]) != "" { + trimmedSiteCode := strings.TrimSpace(siteCode[0]) + if err := tx.Where("site_code = ? AND status = 'active'", trimmedSiteCode).First(&site).Error; err == nil { + siteID = &site.ID + } + } + priceCent := normalizedListingPriceCent(req) listing := model.RentalListing{ - ListingNo: listingNo, - AccountID: account.ID, - OwnerID: ownerID, - PriceCent: priceCent, - DepositAmountCent: req.DepositAmountCent, - Status: listingStatus, - ReviewStatus: reviewStatus, - PublishedAt: publishedAt, + ListingNo: listingNo, + AccountID: account.ID, + OwnerID: ownerID, + PriceCent: priceCent, + DepositAmountCent: req.DepositAmountCent, + Status: listingStatus, + ReviewStatus: reviewStatus, + PublishedAt: publishedAt, + DistributionSiteID: siteID, } if err := tx.Create(&listing).Error; err != nil { return err } + + if siteID != nil { + dsa := model.DistributionSiteAccount{ + SiteID: *siteID, + AccountID: account.ID, + ListingID: listing.ID, + DisplayStatus: "enabled", + } + if err := tx.Create(&dsa).Error; err != nil { + return err + } + } + if err := listingstatus.AppendTransition(tx, &listing, "", listingstatus.SourceSeller, listingstatus.ActorUser, ownerID, "发布上架"); err != nil { return err } @@ -80,6 +104,12 @@ func (r *Repository) Create(ctx context.Context, ownerID uint64, req CreateReque } dto = toDTO(account, listing) + if siteID != nil { + dto.DistributionSiteID = siteID + dto.DistributionSiteCode = site.SiteCode + dto.DistributionSiteName = site.SiteName + dto.SourceChannel = fmt.Sprintf("分销站 - %s (%s)", site.SiteName, site.SiteCode) + } dto.ListingGroupConversationID = conversationID return nil }) diff --git a/backend/internal/modules/listing/presenter.go b/backend/internal/modules/listing/presenter.go index f6bab7b..157943e 100644 --- a/backend/internal/modules/listing/presenter.go +++ b/backend/internal/modules/listing/presenter.go @@ -18,8 +18,10 @@ type listingRow struct { Title string OwnerPhone string OwnerNickname string - SourceChannel string `gorm:"column:source_channel"` - IsExternalUpload bool `gorm:"column:is_external_upload"` + SourceChannel string `gorm:"column:source_channel"` + IsExternalUpload bool `gorm:"column:is_external_upload"` + DistributionSiteCode string `gorm:"column:distribution_site_code"` + DistributionSiteName string `gorm:"column:distribution_site_name"` Description string GameName string ServerRegion string @@ -189,11 +191,14 @@ func (row listingRow) toDTO() ListingDTO { ReviewStatus: reviewStatus, HandoffMode: row.HandoffMode, SettlementMode: row.SettlementMode, - ManagedAdminID: row.ManagedAdminID, - ReviewReason: reviewReason, - PublishedAt: row.PublishedAt, - CreatedAt: row.CreatedAt, - UpdatedAt: row.UpdatedAt, + ManagedAdminID: row.ManagedAdminID, + DistributionSiteID: row.DistributionSiteID, + DistributionSiteCode: row.DistributionSiteCode, + DistributionSiteName: row.DistributionSiteName, + ReviewReason: reviewReason, + PublishedAt: row.PublishedAt, + CreatedAt: row.CreatedAt, + UpdatedAt: row.UpdatedAt, } } @@ -202,33 +207,34 @@ func toDTO(account model.GameAccount, listing model.RentalListing) *ListingDTO { screenshotURLS := cleanScreenshotURLs(decodeScreenshots(account.ScreenshotURLS)) reviewStatus, reviewReason := normalizedReviewState(listing.Status, listing.ReviewStatus, listing.ReviewReason) return &ListingDTO{ - ID: listing.ID, - ListingNo: listing.ListingNo, - AccountID: account.ID, - OwnerID: listing.OwnerID, - Title: account.Title, - Description: account.Description, - GameName: account.GameName, - ServerRegion: account.ServerRegion, - LoginPlatform: account.LoginPlatform, - RankLevel: account.RankLevel, - HafCoinAmount: account.HafCoinAmount, - AssetSummary: assetSummary, - ScreenshotURLS: screenshotURLS, - CoverURL: publicCoverURL(listing.ID, screenshotURLS, listing.Status, listing.ReviewStatus), - PriceCent: listing.PriceCent, - DepositAmountCent: listing.DepositAmountCent, - IsAccelerated: isAcceleratedSale(assetSummary), - InTransaction: listing.InTransaction, - Status: listing.Status, - ReviewStatus: reviewStatus, - HandoffMode: listing.HandoffMode, - SettlementMode: listing.SettlementMode, - ManagedAdminID: listing.ManagedAdminID, - ReviewReason: reviewReason, - PublishedAt: listing.PublishedAt, - CreatedAt: listing.CreatedAt, - UpdatedAt: listing.UpdatedAt, + ID: listing.ID, + ListingNo: listing.ListingNo, + AccountID: account.ID, + OwnerID: listing.OwnerID, + Title: account.Title, + Description: account.Description, + GameName: account.GameName, + ServerRegion: account.ServerRegion, + LoginPlatform: account.LoginPlatform, + RankLevel: account.RankLevel, + HafCoinAmount: account.HafCoinAmount, + AssetSummary: assetSummary, + ScreenshotURLS: screenshotURLS, + CoverURL: publicCoverURL(listing.ID, screenshotURLS, listing.Status, listing.ReviewStatus), + PriceCent: listing.PriceCent, + DepositAmountCent: listing.DepositAmountCent, + IsAccelerated: isAcceleratedSale(assetSummary), + InTransaction: listing.InTransaction, + Status: listing.Status, + ReviewStatus: reviewStatus, + HandoffMode: listing.HandoffMode, + SettlementMode: listing.SettlementMode, + ManagedAdminID: listing.ManagedAdminID, + DistributionSiteID: listing.DistributionSiteID, + ReviewReason: reviewReason, + PublishedAt: listing.PublishedAt, + CreatedAt: listing.CreatedAt, + UpdatedAt: listing.UpdatedAt, } } diff --git a/backend/internal/modules/listing/query.go b/backend/internal/modules/listing/query.go index d656227..89c0836 100644 --- a/backend/internal/modules/listing/query.go +++ b/backend/internal/modules/listing/query.go @@ -220,13 +220,17 @@ func (r *Repository) baseQuery(ctx context.Context) *gorm.DB { Select(`l.*, a.title, a.description, a.game_name, a.server_region, a.login_platform, a.rank_level, a.haf_coin_amount, a.asset_summary, a.screenshot_urls, COALESCE(u.phone, '') AS owner_phone, COALESCE(u.nickname, '') AS owner_nickname, CASE WHEN lu.id IS NULL THEN 0 ELSE 1 END AS is_external_upload, + COALESCE(src_site.site_code, '') AS distribution_site_code, + COALESCE(src_site.site_name, '') AS distribution_site_name, CASE + WHEN l.distribution_site_id IS NOT NULL AND src_site.id IS NOT NULL THEN CONCAT('分销站 - ', src_site.site_name, ' (', src_site.site_code, ')') WHEN lu.id IS NULL THEN ? WHEN COALESCE(lu.source_channel, '') = '' THEN ? ELSE lu.source_channel END AS source_channel`, sourceChannelWebsite, sourceChannelExternalUnknown). Joins("JOIN game_accounts AS a ON a.id = l.account_id"). Joins("LEFT JOIN users AS u ON u.id = l.owner_id"). + Joins("LEFT JOIN distribution_sites AS src_site ON src_site.id = l.distribution_site_id"). // 逐商品按 listing_id 索引定位最后一次导入,避免每次列表查询都对全量导入记录分组。 Joins(`LEFT JOIN listing_uploads AS lu ON lu.id = ( SELECT MAX(lu_latest.id) FROM listing_uploads AS lu_latest WHERE lu_latest.listing_id = l.id diff --git a/backend/internal/modules/listing/service_mutation.go b/backend/internal/modules/listing/service_mutation.go index a7ed059..db0aa70 100644 --- a/backend/internal/modules/listing/service_mutation.go +++ b/backend/internal/modules/listing/service_mutation.go @@ -4,7 +4,7 @@ import ( "context" ) -func (s *Service) Create(ctx context.Context, ownerID uint64, req CreateRequest) (*ListingDTO, error) { +func (s *Service) Create(ctx context.Context, ownerID uint64, req CreateRequest, siteCode ...string) (*ListingDTO, error) { if s.repo == nil { return nil, ErrDependencyUnavailable } @@ -29,7 +29,11 @@ func (s *Service) Create(ctx context.Context, ownerID uint64, req CreateRequest) if err != nil { return nil, err } - return s.repo.Create(ctx, ownerID, req, reviewRequired, cooldown) + effectiveSiteCode := "" + if len(siteCode) > 0 { + effectiveSiteCode = siteCode[0] + } + return s.repo.Create(ctx, ownerID, req, reviewRequired, cooldown, effectiveSiteCode) } func (s *Service) Update(ctx context.Context, ownerID uint64, id uint64, req UpdateRequest) (*ListingDTO, error) { diff --git a/backend/internal/modules/listing/service_test.go b/backend/internal/modules/listing/service_test.go index 7f7fd49..21c2ac0 100644 --- a/backend/internal/modules/listing/service_test.go +++ b/backend/internal/modules/listing/service_test.go @@ -820,3 +820,71 @@ func TestUpdateScreenshots(t *testing.T) { t.Fatalf("unexpected coin screenshots: %#v", coinList) } } + +func TestCreateListingWithDistributionSiteCode(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: "18800000001", Nickname: "分销卖家", RealnameStatus: "verified", Status: "active"} + db.Create(&user) + + site := model.DistributionSite{ + SiteCode: "dist_test_1", + SiteName: "测试分销商1", + Domain: "d1.com", + Status: "active", + DisplayAccountEnabled: true, + } + db.Create(&site) + + req := CreateRequest{ + Title: "分销上架商品", + ServerRegion: "QQ", + LoginPlatform: "PC", + PriceCent: 8800, + } + + dto, err := repo.Create(context.Background(), user.ID, req, true, 0, "dist_test_1") + if err != nil { + t.Fatalf("repo.Create failed: %v", err) + } + + if dto.DistributionSiteID == nil || *dto.DistributionSiteID != site.ID { + t.Fatalf("expected DistributionSiteID=%d, got %v", site.ID, dto.DistributionSiteID) + } + if dto.DistributionSiteCode != "dist_test_1" { + t.Fatalf("expected dist_test_1, got %s", dto.DistributionSiteCode) + } + if dto.DistributionSiteName != "测试分销商1" { + t.Fatalf("expected 测试分销商1, got %s", dto.DistributionSiteName) + } + expectedSource := "分销站 - 测试分销商1 (dist_test_1)" + if dto.SourceChannel != expectedSource { + t.Fatalf("expected %s, got %s", expectedSource, dto.SourceChannel) + } + + // 验证 distribution_site_accounts 自动创建并启用 + var dsa model.DistributionSiteAccount + if err := db.Where("site_id = ? AND listing_id = ?", site.ID, dto.ID).First(&dsa).Error; err != nil { + t.Fatalf("failed to find distribution_site_account: %v", err) + } + if dsa.DisplayStatus != "enabled" { + t.Fatalf("expected enabled, got %s", dsa.DisplayStatus) + } + + // 验证审核工作台列表展示分销站来源 + pending, err := repo.ListPendingReview(context.Background()) + if err != nil { + t.Fatalf("ListPendingReview failed: %v", err) + } + if len(pending) != 1 { + t.Fatalf("expected 1 pending item, got len=%d", len(pending)) + } + if pending[0].SourceChannel != expectedSource { + t.Fatalf("expected pending SourceChannel=%s, got %s", expectedSource, pending[0].SourceChannel) + } +} + diff --git a/backend/internal/modules/order/lifecycle.go b/backend/internal/modules/order/lifecycle.go index 23b7430..f27a88d 100644 --- a/backend/internal/modules/order/lifecycle.go +++ b/backend/internal/modules/order/lifecycle.go @@ -3,6 +3,8 @@ package order import ( "context" "encoding/json" + "errors" + "fmt" "time" "hfb_sys/backend/internal/model" @@ -146,13 +148,22 @@ func (r *Repository) Create(ctx context.Context, renterID uint64, req CreateRequ func orderAccountSourceSnapshot(tx *gorm.DB, listingID uint64) (string, string, error) { var upload model.ListingUpload err := tx.Where("listing_id = ?", listingID).Order("id DESC").First(&upload).Error - if err == gorm.ErrRecordNotFound { - return "internal", "", nil + if err == nil { + return "external", upload.SourceChannel, nil } - if err != nil { + if !errors.Is(err, gorm.ErrRecordNotFound) { return "", "", err } - return "external", upload.SourceChannel, nil + + var listing model.RentalListing + if err := tx.Select("distribution_site_id").First(&listing, listingID).Error; err == nil && listing.DistributionSiteID != nil { + var site model.DistributionSite + if err := tx.Select("site_code, site_name").First(&site, *listing.DistributionSiteID).Error; err == nil { + return "distribution", fmt.Sprintf("分销站 - %s (%s)", site.SiteName, site.SiteCode), nil + } + } + + return "internal", "", nil } func listingHandoffMode(listing model.RentalListing) string { diff --git a/backend/migrations/000072_distribution_listing_attribution.sql b/backend/migrations/000072_distribution_listing_attribution.sql new file mode 100644 index 0000000..38128df --- /dev/null +++ b/backend/migrations/000072_distribution_listing_attribution.sql @@ -0,0 +1,46 @@ +-- +goose Up + +-- 1. rental_listings 表新增 distribution_site_id 并添加索引 +SET @sql = IF( + (SELECT COUNT(*) FROM information_schema.COLUMNS WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'rental_listings' AND COLUMN_NAME = 'distribution_site_id') = 0, + 'ALTER TABLE rental_listings ADD COLUMN distribution_site_id BIGINT UNSIGNED NULL AFTER managed_admin_id', + 'SELECT 1' +); +PREPARE stmt FROM @sql; +EXECUTE stmt; +DEALLOCATE PREPARE stmt; + +SET @sql = IF( + (SELECT COUNT(*) FROM information_schema.STATISTICS WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'rental_listings' AND INDEX_NAME = 'idx_rental_listings_distribution_site') = 0, + 'ALTER TABLE rental_listings ADD KEY idx_rental_listings_distribution_site (distribution_site_id, status)', + 'SELECT 1' +); +PREPARE stmt FROM @sql; +EXECUTE stmt; +DEALLOCATE PREPARE stmt; + +-- 2. 回填历史分销账号的归属站点 +UPDATE rental_listings l +JOIN distribution_site_accounts dsa ON dsa.listing_id = l.id +SET l.distribution_site_id = dsa.site_id +WHERE l.distribution_site_id IS NULL; + +-- +goose Down + +SET @sql = IF( + (SELECT COUNT(*) FROM information_schema.STATISTICS WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'rental_listings' AND INDEX_NAME = 'idx_rental_listings_distribution_site') > 0, + 'ALTER TABLE rental_listings DROP KEY idx_rental_listings_distribution_site', + 'SELECT 1' +); +PREPARE stmt FROM @sql; +EXECUTE stmt; +DEALLOCATE PREPARE stmt; + +SET @sql = IF( + (SELECT COUNT(*) FROM information_schema.COLUMNS WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'rental_listings' AND COLUMN_NAME = 'distribution_site_id') > 0, + 'ALTER TABLE rental_listings DROP COLUMN distribution_site_id', + 'SELECT 1' +); +PREPARE stmt FROM @sql; +EXECUTE stmt; +DEALLOCATE PREPARE stmt; diff --git a/frontend/src/features/admin/views/AdminListingReviewView.vue b/frontend/src/features/admin/views/AdminListingReviewView.vue index 5c5590b..0f17e0b 100644 --- a/frontend/src/features/admin/views/AdminListingReviewView.vue +++ b/frontend/src/features/admin/views/AdminListingReviewView.vue @@ -61,7 +61,7 @@ import { } from '@/shared/utils/listingDisplay' import { formatDateTime } from '@/shared/utils/time' -type RiskLevel = 'danger' | 'warning' | 'info' +type RiskLevel = 'danger' | 'warning' | 'info' | 'success' interface RiskItem { label: string @@ -598,6 +598,10 @@ function sourceChannel(row: Listing) { return isExternalUpload(row) ? '未填写' : '站内发布' } +function isDistributionListing(row: Listing) { + return Boolean(row.distribution_site_id || sourceChannel(row).startsWith('分销站')) +} + function ownerOnlineText(row: Listing) { const text = row.asset_summary?.online_time_text if (typeof text === 'string' && text.trim()) return text @@ -674,6 +678,8 @@ function riskItems(row: Listing): RiskItem[] { level: 'info', type: 'general', }) + } else if (isDistributionListing(row)) { + items.push({ label: `来源渠道:${sourceChannel(row)}`, level: 'success', type: 'general' }) } else { items.push({ label: `来源渠道:${sourceChannel(row)}`, level: 'info', type: 'general' }) } @@ -712,6 +718,7 @@ function riskItems(row: Listing): RiskItem[] { function riskTagType(level: RiskLevel) { if (level === 'danger') return 'danger' if (level === 'warning') return 'warning' + if (level === 'success') return 'success' return 'info' } @@ -1114,9 +1121,11 @@ async function handleScreenshotSaved(payload: { oldUrl: string; newUrl: string } 押 ¥{{ formatCent(item.deposit_amount_cent) }}
- {{ - sourceChannel(item) - }} + {{ sourceChannel(item) }} {{ uploaderName(item) }} diff --git a/frontend/src/features/listings/api/listings.ts b/frontend/src/features/listings/api/listings.ts index b0e4596..23f5e9d 100644 --- a/frontend/src/features/listings/api/listings.ts +++ b/frontend/src/features/listings/api/listings.ts @@ -30,6 +30,9 @@ export interface Listing { review_status: ListingReviewStatus review_reason: string published_at?: string + distribution_site_id?: number + distribution_site_code?: string + distribution_site_name?: string listing_group_conversation_id?: number created_at: string updated_at: string