让分销站能按自身能力下调收号比例扩大利润,同时把主站回收比例作为硬边界,避免分站把比例加得比主站更高。 Co-authored-by: Cursor <cursoragent@cursor.com>
1147 lines
37 KiB
Go
1147 lines
37 KiB
Go
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 := database.AutoMigrateIfMissing(db, &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 := database.AutoMigrateIfMissing(db, &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 := database.AutoMigrateIfMissing(db, &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", HafCoinAmount: 100_000_000}
|
||
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: 10000}
|
||
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", ShelfRatioDelta: 20}
|
||
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)
|
||
}
|
||
// 主站 100 元 (10000 分,100 比例);分站提价减 20 比例 (80 比例) → 售价 125 元 (12500 分)
|
||
if res.Items[0].PriceCent != 12500 {
|
||
t.Fatalf("expected public price 12500 with ratio delta, 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 != 12500 {
|
||
t.Fatalf("expected detail price 12500 with ratio delta, 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 != 10000 {
|
||
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 := database.AutoMigrateIfMissing(db, &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)
|
||
}
|
||
|
||
var stored model.RentalListing
|
||
if err := db.First(&stored, dto.ID).Error; err != nil {
|
||
t.Fatalf("load listing: %v", err)
|
||
}
|
||
if stored.SourceChannel != site.SiteCode {
|
||
t.Fatalf("stored source_channel = %q, want %q", stored.SourceChannel, site.SiteCode)
|
||
}
|
||
|
||
// 验证审核工作台列表展示分销站来源
|
||
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 TestCreateListingInvalidDistributionSiteCode(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: "18800000011", Nickname: "分销卖家", RealnameStatus: "verified", Status: "active"}
|
||
db.Create(&user)
|
||
|
||
inactive := model.DistributionSite{
|
||
SiteCode: "dist_inactive",
|
||
SiteName: "已停用分销站",
|
||
Domain: "inactive.example.com",
|
||
Status: "disabled",
|
||
DisplayAccountEnabled: true,
|
||
}
|
||
db.Create(&inactive)
|
||
|
||
req := CreateRequest{Title: "无效站点商品", ServerRegion: "QQ", LoginPlatform: "PC", PriceCent: 1000}
|
||
|
||
if _, err := repo.Create(context.Background(), user.ID, req, true, 0, "does_not_exist"); !errors.Is(err, ErrDistributionSiteInvalid) {
|
||
t.Fatalf("unknown site err = %v, want ErrDistributionSiteInvalid", err)
|
||
}
|
||
if _, err := repo.Create(context.Background(), user.ID, req, true, 0, "dist_inactive"); !errors.Is(err, ErrDistributionSiteInvalid) {
|
||
t.Fatalf("inactive site err = %v, want ErrDistributionSiteInvalid", err)
|
||
}
|
||
|
||
var count int64
|
||
if err := db.Model(&model.RentalListing{}).Count(&count).Error; err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
if count != 0 {
|
||
t.Fatalf("invalid site must not create listing, got %d", count)
|
||
}
|
||
}
|
||
|
||
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))
|
||
}
|
||
}
|
||
|
||
func TestCreateDistributionListingUsesMainSiteReceiveRatio(t *testing.T) {
|
||
db := database.NewTestDB()
|
||
if err := database.MigrateListingLifecycleTestSchema(db); err != nil {
|
||
t.Fatalf("migrate err: %v", err)
|
||
}
|
||
svc := NewService(NewRepository(db, nil), stubConfigReader{})
|
||
|
||
user := model.User{Phone: "18800000021", Nickname: "分销收号卖家", RealnameStatus: "verified", Status: "active"}
|
||
if err := db.Create(&user).Error; err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
site := model.DistributionSite{
|
||
SiteCode: "dist_ratio_1",
|
||
SiteName: "收号比例站",
|
||
Domain: "ratio.example.com",
|
||
Status: "active",
|
||
DisplayAccountEnabled: true,
|
||
}
|
||
if err := db.Create(&site).Error; err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
|
||
dto, err := svc.Create(context.Background(), user.ID, CreateRequest{
|
||
Title: "分销收号商品",
|
||
ServerRegion: "QQ",
|
||
LoginPlatform: "PC",
|
||
RankLevel: "钻石",
|
||
HafCoinAmount: 100_000_000,
|
||
PriceCent: 100,
|
||
DepositAmountCent: 15000,
|
||
ScreenshotURLS: []string{"https://example.com/ratio.png"},
|
||
AssetSummary: map[string]any{
|
||
"fire_level": 50,
|
||
"season_insurance": "3*3",
|
||
"stamina_level": "7级",
|
||
"load_level": "7级",
|
||
"daily_loss_m": 10,
|
||
"online_time": map[string]any{
|
||
"start": "09:00",
|
||
"end": "23:00",
|
||
},
|
||
"price_breakdown": map[string]any{
|
||
"seller_ratio": 10.0,
|
||
"seller_total_price": 1.0,
|
||
"buyer_total_price": 1.0,
|
||
},
|
||
},
|
||
AgreedVirtualAssetSale: true,
|
||
AgreedSellerAgreement: true,
|
||
}, "dist_ratio_1")
|
||
if err != nil {
|
||
t.Fatalf("Create failed: %v", err)
|
||
}
|
||
|
||
pricing := ReceivePricingFromAssetSummary(mustMarshalSummary(t, dto.AssetSummary))
|
||
if pricing.SellerRatio != 43 {
|
||
t.Fatalf("distribution receive seller_ratio = %v, want 43 from main-site ratio_config", pricing.SellerRatio)
|
||
}
|
||
if pricing.SellerTotalPriceCent != 23260 {
|
||
t.Fatalf("seller_total_price_cent = %d, want 23260", pricing.SellerTotalPriceCent)
|
||
}
|
||
if dto.PriceCent != 26320 {
|
||
t.Fatalf("listing PriceCent = %d, want 26320 from main-site sale price", dto.PriceCent)
|
||
}
|
||
if dto.DistributionSiteCode != "dist_ratio_1" {
|
||
t.Fatalf("DistributionSiteCode = %s, want dist_ratio_1", dto.DistributionSiteCode)
|
||
}
|
||
}
|
||
|
||
func TestCreateDistributionListingAppliesSiteReceiveRatioDelta(t *testing.T) {
|
||
db := database.NewTestDB()
|
||
if err := database.MigrateListingLifecycleTestSchema(db); err != nil {
|
||
t.Fatalf("migrate err: %v", err)
|
||
}
|
||
svc := NewService(NewRepository(db, nil), stubConfigReader{})
|
||
|
||
user := model.User{Phone: "18800000022", Nickname: "分销下调卖家", RealnameStatus: "verified", Status: "active"}
|
||
if err := db.Create(&user).Error; err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
site := model.DistributionSite{
|
||
SiteCode: "dist_ratio_down",
|
||
SiteName: "下调收号站",
|
||
Domain: "ratio-down.example.com",
|
||
Status: "active",
|
||
DisplayAccountEnabled: true,
|
||
ReceiveRatioDelta: 3,
|
||
}
|
||
if err := db.Create(&site).Error; err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
|
||
dto, err := svc.Create(context.Background(), user.ID, CreateRequest{
|
||
Title: "分销下调收号",
|
||
ServerRegion: "QQ",
|
||
LoginPlatform: "PC",
|
||
RankLevel: "钻石",
|
||
HafCoinAmount: 100_000_000,
|
||
PriceCent: 100,
|
||
DepositAmountCent: 15000,
|
||
ScreenshotURLS: []string{"https://example.com/ratio-down.png"},
|
||
AssetSummary: map[string]any{
|
||
"fire_level": 50,
|
||
"season_insurance": "3*3",
|
||
"stamina_level": "7级",
|
||
"load_level": "7级",
|
||
"daily_loss_m": 10,
|
||
"online_time": map[string]any{
|
||
"start": "09:00",
|
||
"end": "23:00",
|
||
},
|
||
"price_breakdown": map[string]any{
|
||
"seller_ratio": 50.0,
|
||
},
|
||
},
|
||
AgreedVirtualAssetSale: true,
|
||
AgreedSellerAgreement: true,
|
||
}, "dist_ratio_down")
|
||
if err != nil {
|
||
t.Fatalf("Create failed: %v", err)
|
||
}
|
||
pricing := ReceivePricingFromAssetSummary(mustMarshalSummary(t, dto.AssetSummary))
|
||
if pricing.SellerRatio != 40 {
|
||
t.Fatalf("seller_ratio = %v, want 40 after site delta 3 below main-site 43", pricing.SellerRatio)
|
||
}
|
||
}
|
||
|
||
func mustMarshalSummary(t *testing.T, summary map[string]any) []byte {
|
||
t.Helper()
|
||
raw, err := json.Marshal(summary)
|
||
if err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
return raw
|
||
}
|