Files
hfb_sys/backend/internal/modules/listing/service_test.go
T
yml2213 96bc6a4db6 fix(distribution): 货架加价打通前台、下单与卖方分成
分站公开列表和详情展示当前货架加价;下单写入 base_price / shelf_markup / markup_site_id 快照并计入实付。
支付分账改用快照加价,不再误用 PlatformFeeCent;结账不退加价,事后改价不影响历史订单。
2026-09-08 17:07:17 +08:00

965 lines
31 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package listing
import (
"context"
"encoding/json"
"errors"
"testing"
"time"
"hfb_sys/backend/internal/database"
"hfb_sys/backend/internal/model"
)
type stubConfigReader map[string]string
func (s stubConfigReader) FindValue(_ context.Context, key string) (string, error) {
return s[key], nil
}
func TestConsumableValueOnlyCountsChargedResources(t *testing.T) {
value := consumableValue(map[string]any{
"resources": []any{
map[string]any{"mode": "收费", "quantity": float64(5), "price": "0.1元/个"},
map[string]any{"mode": "赠送", "quantity": float64(3), "price": "0.6元/个"},
map[string]any{"mode": "收费", "quantity": float64(2), "price": "1元/2个"},
},
})
if value != 1.5 {
t.Fatalf("expected 1.5, got %.2f", value)
}
}
func TestCalculateAdminAdjustedPriceByRatio(t *testing.T) {
base, total, ratio := calculateAdminAdjustedPrice(AdminPriceAdjustRequest{BuyerRatio: 25}, 1000, 20)
if base != 40 || total != 60 || ratio != 25 {
t.Fatalf("unexpected adjusted price: base=%.2f total=%.2f ratio=%.2f", base, total, ratio)
}
}
func TestCalculateAdminAdjustedPriceByTotalPrice(t *testing.T) {
base, total, ratio := calculateAdminAdjustedPrice(AdminPriceAdjustRequest{BuyerTotalPriceCent: 7000}, 1000, 20)
if base != 50 || total != 70 || ratio != 20 {
t.Fatalf("unexpected adjusted price: base=%.2f total=%.2f ratio=%.2f", base, total, ratio)
}
}
func TestValidateRequestRequiresDepositAboveConsumables(t *testing.T) {
req := CreateRequest{
Title: "测试账号",
ServerRegion: "烽火地带",
PriceCent: 10000,
DepositAmountCent: 200,
HafCoinAmount: 1000000,
ScreenshotURLS: []string{"https://example.com/a.png"},
AssetSummary: map[string]any{
"resources": []any{
map[string]any{"mode": "收费", "quantity": float64(2), "price": "1元/个"},
},
},
}
if err := validateRequest(req, publishRules{}); err != ErrDepositTooLow {
t.Fatalf("expected ErrDepositTooLow, got %v", err)
}
req.DepositAmountCent = 300
if err := validateRequest(req, publishRules{}); err != nil {
t.Fatalf("expected valid request, got %v", err)
}
}
func TestValidateRequiredOnlineTime(t *testing.T) {
req := CreateRequest{
AssetSummary: map[string]any{
"online_time": map[string]any{
"start": "09:00",
"end": "23:00",
},
},
}
if err := validateRequiredOnlineTime(req); err != nil {
t.Fatalf("expected valid online time, got %v", err)
}
}
func TestValidateRequiredOnlineTimeRejectsMissing(t *testing.T) {
req := CreateRequest{AssetSummary: map[string]any{}}
if err := validateRequiredOnlineTime(req); err != ErrMissingOnlineTime {
t.Fatalf("expected ErrMissingOnlineTime, got %v", err)
}
}
func TestValidateRequiredOnlineTimeAllowsCrossDay(t *testing.T) {
req := CreateRequest{
AssetSummary: map[string]any{
"online_time": map[string]any{
"start": "23:00",
"end": "09:00",
},
},
}
if err := validateRequiredOnlineTime(req); err != nil {
t.Fatalf("expected cross-day range to be valid, got %v", err)
}
}
func TestValidateRequiredOnlineTimeRejectsSameStartEnd(t *testing.T) {
req := CreateRequest{
AssetSummary: map[string]any{
"online_time": map[string]any{
"start": "10:00",
"end": "10:00",
},
},
}
if err := validateRequiredOnlineTime(req); err != ErrInvalidOnlineTime {
t.Fatalf("expected ErrInvalidOnlineTime, got %v", err)
}
}
func TestCreateRequiresPublishAgreements(t *testing.T) {
service := NewService(&Repository{}, nil)
_, err := service.Create(t.Context(), 1, CreateRequest{})
if err != ErrAgreementRequired {
t.Fatalf("expected ErrAgreementRequired, got %v", err)
}
}
func TestPublishCooldownUsesConfiguredMinutes(t *testing.T) {
service := NewService(nil, stubConfigReader{publishCooldownMinutesKey: "2"})
cooldown, err := service.publishCooldown(t.Context())
if err != nil {
t.Fatalf("expected no error, got %v", err)
}
if cooldown != 2*time.Minute {
t.Fatalf("expected 2m cooldown, got %s", cooldown)
}
}
func TestPublishCooldownCanBeDisabled(t *testing.T) {
service := NewService(nil, stubConfigReader{publishCooldownMinutesKey: "0"})
cooldown, err := service.publishCooldown(t.Context())
if err != nil {
t.Fatalf("expected no error, got %v", err)
}
if cooldown != 0 {
t.Fatalf("expected disabled cooldown, got %s", cooldown)
}
}
func TestRepositoryCreateRejectsRecentOwnerPublish(t *testing.T) {
db := database.NewTestDB()
if err := database.MigrateListingLifecycleTestSchema(db); err != nil {
t.Fatalf("failed to migrate test db: %v", err)
}
user := model.User{Phone: "18800000000", RealnameStatus: "verified", Status: "active"}
if err := db.Create(&user).Error; err != nil {
t.Fatalf("failed to create user: %v", err)
}
latestCreatedAt := time.Now().Add(-2 * time.Minute)
if err := db.Create(&model.RentalListing{
ListingNo: "202606270001",
AccountID: 1,
OwnerID: user.ID,
Status: "published",
CreatedAt: latestCreatedAt,
ReviewStatus: "approved",
}).Error; err != nil {
t.Fatalf("failed to create latest listing: %v", err)
}
repo := NewRepository(db, nil)
_, err := repo.Create(t.Context(), user.ID, validCreateRequest(), false, 5*time.Minute)
var cooldownErr PublishCooldownError
if !errors.As(err, &cooldownErr) {
t.Fatalf("expected PublishCooldownError, got %v", err)
}
if cooldownErr.Remaining <= 0 {
t.Fatalf("expected positive remaining cooldown, got %s", cooldownErr.Remaining)
}
}
func TestRepositoryUpdateAllowsOfflineListingResubmit(t *testing.T) {
db := database.NewTestDB()
if err := database.MigrateListingLifecycleTestSchema(db); err != nil {
t.Fatalf("failed to migrate test db: %v", err)
}
repo := NewRepository(db, nil)
req := CreateRequest{
Title: "测试账号",
ServerRegion: "QQ",
LoginPlatform: "QQ账号密码",
RankLevel: "黑鹰",
HafCoinAmount: 100000000,
PriceCent: 10000,
DepositAmountCent: 50000,
ScreenshotURLS: []string{"https://example.com/a.png"},
AssetSummary: map[string]any{
"online_time": map[string]any{"start": "09:00", "end": "23:00"},
},
}
created, err := repo.Create(t.Context(), 1, req, false, 0)
if err != nil {
t.Fatalf("failed to create listing: %v", err)
}
if _, err := repo.Offline(t.Context(), 1, created.ID); err != nil {
t.Fatalf("failed to offline listing: %v", err)
}
req.Title = "二次编辑账号"
updated, err := repo.Update(t.Context(), 1, created.ID, req, true)
if err != nil {
t.Fatalf("expected offline listing can be updated, got %v", err)
}
if updated.Title != "二次编辑账号" {
t.Fatalf("expected title updated, got %q", updated.Title)
}
if updated.Status != "draft" || updated.ReviewStatus != "pending" {
t.Fatalf("expected draft/pending after resubmit, got %s/%s", updated.Status, updated.ReviewStatus)
}
}
func TestRepositoryRejectsCompletedListingOwnerMutations(t *testing.T) {
db := database.NewTestDB()
if err := database.MigrateListingLifecycleTestSchema(db); err != nil {
t.Fatalf("failed to migrate test db: %v", err)
}
repo := NewRepository(db, nil)
account := model.GameAccount{
OwnerID: 1,
GameName: "delta_force",
ServerRegion: "QQ",
LoginPlatform: "QQ账号密码",
Title: "已完成账号",
Status: "offline",
}
if err := db.Create(&account).Error; err != nil {
t.Fatalf("failed to create account: %v", err)
}
listing := model.RentalListing{
ListingNo: "202606270099",
AccountID: account.ID,
OwnerID: 1,
Status: "completed",
ReviewStatus: "approved",
PriceCent: 10000,
DepositAmountCent: 50000,
}
if err := db.Create(&listing).Error; err != nil {
t.Fatalf("failed to create listing: %v", err)
}
if _, err := repo.Update(t.Context(), 1, listing.ID, validCreateRequest(), false); err != ErrListingLocked {
t.Fatalf("Update expected ErrListingLocked, got %v", err)
}
if _, err := repo.SubmitReview(t.Context(), 1, listing.ID, false); err != ErrListingLocked {
t.Fatalf("SubmitReview expected ErrListingLocked, got %v", err)
}
if _, err := repo.Offline(t.Context(), 1, listing.ID); err != ErrListingLocked {
t.Fatalf("Offline expected ErrListingLocked, got %v", err)
}
}
func validCreateRequest() CreateRequest {
return CreateRequest{
Title: "测试账号",
ServerRegion: "QQ",
LoginPlatform: "QQ账号密码",
RankLevel: "黑鹰",
HafCoinAmount: 100000000,
PriceCent: 10000,
DepositAmountCent: 50000,
ScreenshotURLS: []string{"https://example.com/a.png"},
AssetSummary: map[string]any{
"online_time": map[string]any{"start": "09:00", "end": "23:00"},
},
}
}
func TestApplySellerListingPriceUsesSellerTotalPrice(t *testing.T) {
item := &ListingDTO{
PriceCent: 23800,
AssetSummary: map[string]any{
"price_breakdown": map[string]any{
"seller_total_price": 200,
},
},
}
applySellerListingPrice(item)
if item.PriceCent != 20000 {
t.Fatalf("expected seller price 20000, got %d", item.PriceCent)
}
}
func TestApplySellerListingPriceKeepsFallbackPrice(t *testing.T) {
item := &ListingDTO{
PriceCent: 23800,
AssetSummary: map[string]any{
"price_breakdown": map[string]any{
"seller_total_price": 0,
},
},
}
applySellerListingPrice(item)
if item.PriceCent != 23800 {
t.Fatalf("expected fallback price 23800, got %d", item.PriceCent)
}
}
func TestApplyPublicListingURLsHidesSourceChannel(t *testing.T) {
item := &ListingDTO{
ID: 1,
Status: "published",
ReviewStatus: "approved",
SourceChannel: "淘宝",
ScreenshotURLS: []string{
"https://example.com/account.png",
},
AssetSummary: map[string]any{
"import_meta": map[string]any{"uploader_name": "客服1"},
"price_breakdown": map[string]any{"buyer_total_price": 100},
"screenshot_groups": map[string]any{"coin": []string{"https://example.com/account.png"}},
},
}
applyPublicListingURLs(item)
if item.SourceChannel != "" {
t.Fatalf("expected public listing source channel hidden, got %q", item.SourceChannel)
}
if _, ok := item.AssetSummary["import_meta"]; ok {
t.Fatal("expected import_meta hidden from public listing")
}
if _, ok := item.AssetSummary["price_breakdown"]; ok {
t.Fatal("expected price_breakdown hidden from public listing")
}
}
func uint64Pointer(value uint64) *uint64 {
return &value
}
func TestParseExternalUploadItemsAcceptsSingleObject(t *testing.T) {
items, err := parseExternalUploadItems(json.RawMessage(validExternalUploadDataJSON()))
if err != nil {
t.Fatalf("expected single upload parsed, got %v", err)
}
if len(items) != 1 || items[0].LoginMethod != "QQ账号密码" {
t.Fatalf("unexpected items: %#v", items)
}
}
func TestParseExternalUploadItemsReportsMissingFields(t *testing.T) {
_, err := parseExternalUploadItems(json.RawMessage(`{"loginMethod":"QQ账号密码","currency":{"hafuCoin":197.1}}`))
if err == nil {
t.Fatal("expected validation error")
}
validationErr, ok := err.(UploadValidationError)
if !ok {
t.Fatalf("expected UploadValidationError, got %T", err)
}
expected := []string{"data.rank", "data.level", "data.currency.recycleRatio", "data.dailyConsumption"}
for _, field := range expected {
if !containsString(validationErr.Missing, field) {
t.Fatalf("expected missing %s in %#v", field, validationErr.Missing)
}
}
}
func TestImportExternalUploadStoresSourceChannel(t *testing.T) {
db := database.NewTestDB()
if err := database.MigrateListingLifecycleTestSchema(db); err != nil {
t.Fatalf("failed to migrate test db: %v", err)
}
if err := db.AutoMigrate(&model.AdminUser{}, &model.ListingUpload{}); err != nil {
t.Fatalf("failed to migrate upload tables: %v", err)
}
admin := model.AdminUser{
Username: "客服1",
PasswordHash: "hash",
Nickname: "客服1",
Status: "active",
}
if err := db.Create(&admin).Error; err != nil {
t.Fatalf("failed to create admin: %v", err)
}
service := NewService(NewRepository(db, nil), nil)
resp, err := service.ImportExternalUpload(t.Context(), ExternalUploadRequest{
UploaderName: "客服1",
SourceChannel: "淘宝",
Data: json.RawMessage(validExternalUploadDataJSON()),
}, ExternalUploadMeta{})
if err != nil {
t.Fatalf("ImportExternalUpload() error = %v", err)
}
if resp.Success != 1 || resp.ListingID == 0 {
t.Fatalf("unexpected response: %#v", resp)
}
var upload model.ListingUpload
if err := db.Where("listing_id = ?", resp.ListingID).First(&upload).Error; err != nil {
t.Fatalf("failed to find listing upload: %v", err)
}
if upload.SourceChannel != "淘宝" {
t.Fatalf("source channel = %q, want 淘宝", upload.SourceChannel)
}
}
func TestListPendingReviewIncludesSourceChannel(t *testing.T) {
db := database.NewTestDB()
if err := database.MigrateListingLifecycleTestSchema(db); err != nil {
t.Fatalf("failed to migrate test db: %v", err)
}
if err := db.AutoMigrate(&model.ListingUpload{}); err != nil {
t.Fatalf("failed to migrate listing uploads: %v", err)
}
user := model.User{Phone: "18800000001", RealnameStatus: "verified", Status: "active"}
if err := db.Create(&user).Error; err != nil {
t.Fatalf("failed to create user: %v", err)
}
account := model.GameAccount{
OwnerID: user.ID,
GameName: "delta_force",
ServerRegion: "QQ",
LoginPlatform: "QQ账号密码",
Title: "待审核账号",
RankLevel: "黑鹰",
Status: "draft",
}
if err := db.Create(&account).Error; err != nil {
t.Fatalf("failed to create account: %v", err)
}
listing := model.RentalListing{
ListingNo: "202607250001",
AccountID: account.ID,
OwnerID: user.ID,
PriceCent: 10000,
DepositAmountCent: 50000,
Status: "draft",
ReviewStatus: "pending",
}
if err := db.Create(&listing).Error; err != nil {
t.Fatalf("failed to create listing: %v", err)
}
listingID := listing.ID
if err := db.Create(&model.ListingUpload{
UploaderName: "客服1",
SourceChannel: "淘宝",
OwnerID: &user.ID,
ListingID: &listingID,
Status: "draft_created",
}).Error; err != nil {
t.Fatalf("failed to create listing upload: %v", err)
}
items, err := NewRepository(db, nil).ListPendingReview(t.Context())
if err != nil {
t.Fatalf("ListPendingReview() error = %v", err)
}
if len(items) != 1 {
t.Fatalf("expected 1 pending listing, got %d", len(items))
}
if items[0].SourceChannel != "淘宝" {
t.Fatalf("source channel = %q, want 淘宝", items[0].SourceChannel)
}
}
func TestExternalAccountToCreateRequestMapsUploadFields(t *testing.T) {
req := externalAccountToCreateRequest("客服1", 1772526103000, ExternalAccountData{
LoginMethod: "QQ账号密码",
Rank: "黑鹰",
Level: 60,
SafeSlots: 9,
SecretKD: 1.6,
DailyLossM: 7,
Deposit: 400,
BanRecord: "有",
CommonRegion: "郑州",
OwnerOnlineTime: "8:00-1:00",
Remark: "有语音封禁\n封禁历史:2026/01/23封禁3天",
Currency: ExternalUploadCurrency{
HafuCoin: 197.1,
RecycleRatio: 43,
RecycleRent: 458,
},
DailyConsumption: ExternalDailyConsumption{Stamina: 7, Weight: 7},
Inventory: ExternalUploadInventory{
AWMBullets: 45,
Level6Helmets: 14,
Level6Armor: 17,
Skins: []string{"信条", "电锯惊魂"},
},
}, defaultSalePriceConfig())
if req.Description != "有语音封禁\n封禁历史:2026/01/23封禁3天" {
t.Fatalf("Description = %q", req.Description)
}
if req.AssetSummary["remark"] != "有语音封禁\n封禁历史:2026/01/23封禁3天" {
t.Fatalf("remark = %#v", req.AssetSummary["remark"])
}
if req.AssetSummary["ban_record"] != "有封禁记录" {
t.Fatalf("ban_record = %#v", req.AssetSummary["ban_record"])
}
if req.ServerRegion != "QQ" {
t.Fatalf("expected QQ server region, got %q", req.ServerRegion)
}
if req.HafCoinAmount != 197100000 {
t.Fatalf("expected haf coin amount 197100000, got %d", req.HafCoinAmount)
}
if req.PriceCent != 59590 || req.DepositAmountCent != 40000 {
t.Fatalf("unexpected price/deposit cent %d/%d", req.PriceCent, req.DepositAmountCent)
}
if req.AssetSummary["season_insurance"] != "3*3" {
t.Fatalf("expected 3*3 insurance, got %#v", req.AssetSummary["season_insurance"])
}
if req.AssetSummary["daily_loss_m"] != float64(7) {
t.Fatalf("expected daily loss 7, got %#v", req.AssetSummary["daily_loss_m"])
}
if req.AssetSummary["stamina_level"] != "7级" || req.AssetSummary["load_level"] != "7级" {
t.Fatalf("unexpected stamina/load: %#v", req.AssetSummary)
}
if req.AssetSummary["online_time_text"] != "08:00 至 01:00" {
t.Fatalf("online_time_text = %#v", req.AssetSummary["online_time_text"])
}
if req.AssetSummary["early_morning_response"] != "否" {
t.Fatalf("early_morning_response = %#v, want 否", req.AssetSummary["early_morning_response"])
}
onlineTime, ok := req.AssetSummary["online_time"].(map[string]any)
if !ok {
t.Fatalf("online_time missing: %#v", req.AssetSummary["online_time"])
}
if onlineTime["start"] != "08:00" || onlineTime["end"] != "01:00" {
t.Fatalf("online_time = %#v", onlineTime)
}
groups := req.AssetSummary["skin_groups"].(map[string][]string)
if len(groups["melee"]) != 2 {
t.Fatalf("expected melee skins mapped, got %#v", groups)
}
if _, ok := groups["imported"]; ok {
t.Fatalf("did not expect imported group, got %#v", groups)
}
if len(req.ScreenshotURLS) != 1 || req.ScreenshotURLS[0] != defaultUploadScreenshot {
t.Fatalf("expected default screenshot, got %#v", req.ScreenshotURLS)
}
breakdown, ok := req.AssetSummary["price_breakdown"].(map[string]any)
if !ok {
t.Fatalf("price_breakdown = %#v", req.AssetSummary["price_breakdown"])
}
if breakdown["seller_coin_base_price"] != 458.0 ||
breakdown["consumable_price"] != 90.5 ||
breakdown["seller_total_price"] != 548.5 ||
breakdown["buyer_coin_base_price"] != 505.4 ||
breakdown["buyer_total_price"] != 595.9 ||
breakdown["buyer_ratio"] != 39.0 ||
breakdown["platform_markup_amount"] != 47.4 ||
breakdown["platform_rule_type"] != "ratio_subtract" {
t.Fatalf("unexpected price breakdown: %#v", breakdown)
}
if req.AssetSummary["publish_ratio"] != 39.0 {
t.Fatalf("publish_ratio = %#v", req.AssetSummary["publish_ratio"])
}
}
func TestServerRegionFromLoginMethod(t *testing.T) {
tests := []struct {
name string
loginMethod string
want string
}{
{name: "QQ账号密码", loginMethod: "QQ账号密码", want: "QQ"},
{name: "微信扫码", loginMethod: "微信扫码", want: "微信"},
{name: "VX扫码", loginMethod: "VX扫码", want: "微信"},
{name: "小写WX扫码", loginMethod: "wx扫码", want: "微信"},
{name: "Steam", loginMethod: "Steam令牌", want: "Steam"},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := serverRegionFromLoginMethod(tt.loginMethod); got != tt.want {
t.Fatalf("serverRegionFromLoginMethod(%q) = %q, want %q", tt.loginMethod, got, tt.want)
}
})
}
}
func TestExternalAccountToCreateRequestSeparatesPureCoinAndExtraItems(t *testing.T) {
req := externalAccountToCreateRequest("客服1", 1772526103000, ExternalAccountData{
LoginMethod: "QQ账号密码",
Rank: "黑鹰",
Level: 60,
SafeSlots: 9,
Deposit: 400,
Currency: ExternalUploadCurrency{
HafuCoin: 282,
RecycleRatio: 42.8,
RecycleRent: 659,
},
DailyConsumption: ExternalDailyConsumption{Stamina: 7, Weight: 7},
Inventory: ExternalUploadInventory{
AWMBullets: 10,
Level6Armor: 2,
},
}, defaultSalePriceConfig())
breakdown, ok := req.AssetSummary["price_breakdown"].(map[string]any)
if !ok {
t.Fatalf("price_breakdown = %#v", req.AssetSummary["price_breakdown"])
}
if breakdown["seller_coin_base_price"] != 659.0 ||
breakdown["consumable_price"] != 11.0 ||
breakdown["seller_total_price"] != 670.0 {
t.Fatalf("回收租金未按纯币价与额外物品拆分:%#v", breakdown)
}
if breakdown["buyer_coin_base_price"] != 717.6 ||
breakdown["buyer_total_price"] != 728.6 ||
breakdown["buyer_ratio"] != 39.3 ||
breakdown["platform_markup_amount"] != 58.6 ||
breakdown["platform_rule_type"] != "ratio_subtract" {
t.Fatalf("外部上传未按比例加价:%#v", breakdown)
}
if req.PriceCent != 72860 {
t.Fatalf("PriceCent = %d, want 72860", req.PriceCent)
}
}
func TestExternalSkinGroupsClassifiesKnownSkins(t *testing.T) {
groups := externalSkinGroups([]string{"暗星", "凌霄成卫", "蛊-不羁人生", "未知皮肤X"})
if got := groups["melee"]; len(got) != 1 || got[0] != "暗星" {
t.Fatalf("melee = %#v", got)
}
if got := groups["operatorRed"]; len(got) != 1 || got[0] != "凌霄成卫" {
t.Fatalf("operatorRed = %#v", got)
}
if got := groups["operatorGold"]; len(got) != 1 || got[0] != "蛊-不羁人生" {
t.Fatalf("operatorGold = %#v", got)
}
if got := groups["other"]; len(got) != 1 || got[0] != "未知皮肤X" {
t.Fatalf("other = %#v", got)
}
if _, ok := groups["imported"]; ok {
t.Fatalf("unexpected imported group: %#v", groups)
}
}
func validExternalUploadDataJSON() string {
return `{
"loginMethod":"QQ账号密码",
"rank":"黑鹰",
"level":60,
"safeSlots":9,
"secretKD":1.6,
"dailyLossM":7,
"deposit":400,
"currency":{"hafuCoin":197.1,"recycleRatio":43,"recycleRent":458},
"dailyConsumption":{"stamina":7,"weight":7}
}`
}
func containsString(values []string, target string) bool {
for _, value := range values {
if value == target {
return true
}
}
return false
}
func TestListPublicWithDistributionSiteCode(t *testing.T) {
db := database.NewTestDB()
if err := database.MigrateListingLifecycleTestSchema(db); err != nil {
t.Fatalf("migrate err: %v", err)
}
if err := db.AutoMigrate(&model.DistributionSite{}, &model.DistributionSiteAccount{}); err != nil {
t.Fatalf("migrate distribution err: %v", err)
}
repo := NewRepository(db, nil)
account1 := model.GameAccount{Title: "号1", GameName: "delta", Status: "active"}
account2 := model.GameAccount{Title: "号2", GameName: "delta", Status: "active"}
db.Create(&account1)
db.Create(&account2)
l1 := model.RentalListing{ListingNo: "L101", AccountID: account1.ID, Status: "published", ReviewStatus: "approved", PriceCent: 1000}
l2 := model.RentalListing{ListingNo: "L102", AccountID: account2.ID, Status: "published", ReviewStatus: "approved", PriceCent: 2000}
db.Create(&l1)
db.Create(&l2)
site := model.DistributionSite{SiteCode: "site_alpha", SiteName: "分销A", Domain: "a.com", Status: "active", DisplayAccountEnabled: true}
db.Create(&site)
dsa := model.DistributionSiteAccount{SiteID: site.ID, AccountID: account1.ID, ListingID: l1.ID, DisplayStatus: "enabled", MarkupAmountCent: 10000}
db.Create(&dsa)
res, err := repo.ListPublic(context.Background(), PublicListQuery{
DistributionSiteCode: "site_alpha",
})
if err != nil {
t.Fatalf("ListPublic err: %v", err)
}
if len(res.Items) != 1 || res.Items[0].ListingNo != "L101" {
t.Fatalf("expected only L101, got %+v", res.Items)
}
if res.Items[0].PriceCent != 11000 {
t.Fatalf("expected public price 11000 with markup, got %d", res.Items[0].PriceCent)
}
detail, err := repo.FindPublic(context.Background(), l1.ID, "site_alpha")
if err != nil {
t.Fatalf("FindPublic err: %v", err)
}
if detail.PriceCent != 11000 {
t.Fatalf("expected detail price 11000 with markup, got %d", detail.PriceCent)
}
mainDetail, err := repo.FindPublic(context.Background(), l1.ID, "")
if err != nil {
t.Fatalf("FindPublic main err: %v", err)
}
if mainDetail.PriceCent != 1000 {
t.Fatalf("main site must keep original price, got %d", mainDetail.PriceCent)
}
allRes, err := repo.ListPublic(context.Background(), PublicListQuery{})
if err != nil {
t.Fatalf("ListPublic err: %v", err)
}
if len(allRes.Items) != 2 {
t.Fatalf("expected 2 listings for main site, got %d", len(allRes.Items))
}
}
func TestApplyPublicScreenshotGroups(t *testing.T) {
rawURLs := []string{
"/api/files/object?key=listing/coin.png",
"/api/files/object?key=listing/skin1.png",
}
publicURLs := []string{
"/api/listings/88/screenshots/0",
"/api/listings/88/screenshots/1",
}
summary := map[string]any{
"screenshot_groups": map[string]any{
"coin": []any{"/api/files/object?key=listing/coin.png"},
"skin": []string{"/api/files/object?key=listing/skin1.png"},
},
}
applyPublicScreenshotGroups(88, summary, rawURLs, publicURLs)
groups := summary["screenshot_groups"].(map[string]any)
coinList := groups["coin"].([]string)
if len(coinList) != 1 || coinList[0] != "/api/listings/88/screenshots/0" {
t.Fatalf("expected /api/listings/88/screenshots/0, got %#v", coinList)
}
skinList := groups["skin"].([]string)
if len(skinList) != 1 || skinList[0] != "/api/listings/88/screenshots/1" {
t.Fatalf("expected /api/listings/88/screenshots/1, got %#v", skinList)
}
}
func TestUpdateScreenshots(t *testing.T) {
db := database.NewTestDB()
if err := database.MigrateListingLifecycleTestSchema(db); err != nil {
t.Fatalf("failed to migrate test db: %v", err)
}
if err := db.AutoMigrate(&model.AuditLog{}); err != nil {
t.Fatalf("failed to migrate audit log: %v", err)
}
repo := NewRepository(db, nil)
account := model.GameAccount{
OwnerID: 1,
GameName: "delta_force",
ServerRegion: "QQ",
LoginPlatform: "QQ账号密码",
Title: "待审核商品",
Status: "draft",
}
if err := db.Create(&account).Error; err != nil {
t.Fatalf("failed to create account: %v", err)
}
listing := model.RentalListing{
ListingNo: "202609070001",
AccountID: account.ID,
OwnerID: 1,
Status: "draft",
ReviewStatus: "pending",
}
if err := db.Create(&listing).Error; err != nil {
t.Fatalf("failed to create listing: %v", err)
}
meta := AuditMeta{IP: "127.0.0.1", UserAgent: "TestAgent", RequestID: "req-1"}
req := AdminUpdateScreenshotsRequest{
ScreenshotURLs: []string{
"/api/files/object?key=edited1.png",
"/api/files/object?key=edited2.png",
},
ScreenshotGroups: map[string]any{
"coin": []string{"/api/files/object?key=edited1.png"},
"gameId": []string{"/api/files/object?key=edited2.png"},
},
}
updated, err := repo.UpdateScreenshots(t.Context(), 999, listing.ID, req, meta)
if err != nil {
t.Fatalf("UpdateScreenshots failed: %v", err)
}
if len(updated.ScreenshotURLS) != 2 || updated.ScreenshotURLS[0] != "/api/files/object?key=edited1.png" {
t.Fatalf("unexpected screenshot_urls: %#v", updated.ScreenshotURLS)
}
groups, ok := updated.AssetSummary["screenshot_groups"].(map[string]any)
if !ok {
t.Fatalf("screenshot_groups not found in AssetSummary: %#v", updated.AssetSummary)
}
coinList, ok := groups["coin"].([]string)
if !ok {
if anyList, isAny := groups["coin"].([]any); isAny {
for _, v := range anyList {
coinList = append(coinList, v.(string))
}
}
}
if len(coinList) != 1 || coinList[0] != "/api/files/object?key=edited1.png" {
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)
}
}
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))
}
}