Files
hfb_sys/backend/internal/modules/listing/service_test.go
T
yml2213 369b0647bb 合并 main 分支并融合订单列表最小化与 SQL 治理
- Auth/权限中间件取 main 演进版(错误细分、token 来源),保留 dev 的用户 cookie 与登出撤销语义
- 后台订单列表:融合 main 的首页免 COUNT 优化与关键词索引路由,保留 dev 的最小化 AdminOrderListItemDTO(结账明细仍由详情接口返回,同步调整对应集成测试)
- 保留 dev 的 token_version 冻结/解冻递增与 main 的状态变化判断及通知
- 前端:保留 dev 登出接口与 main 的 fetchMe silent 参数、SSE 登录跳转;后台订单表按 dev 最小化 DTO 移除号主到手列
2026-09-05 11:43:13 +08:00

749 lines
24 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"
"strings"
"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")
}
if _, ok := item.AssetSummary["screenshot_groups"]; ok {
t.Fatal("expected screenshot_groups hidden from public listing")
}
}
func TestPublicListingDTOsDoNotSerializeSensitiveFields(t *testing.T) {
item := ListingDTO{
ID: 1,
ListingNo: "SP000001",
AccountID: 2,
OwnerID: 3,
OwnerPhone: "13800001234",
OwnerNickname: "号主",
SourceChannel: "外部渠道",
IsExternalUpload: true,
Title: "测试账号",
Description: "公开描述",
ScreenshotURLS: []string{"/api/listings/1/screenshots/0"},
Status: "published",
ReviewStatus: "approved",
HandoffMode: "platform",
SettlementMode: "platform_managed",
ManagedAdminID: uint64Pointer(4),
ReviewReason: "内部审核备注",
ListingGroupConversationID: 5,
AssetSummary: map[string]any{
"season_insurance": "3*3",
"contact_phone": "13900005678",
"remark": "首页不应携带",
},
}
listRaw, err := json.Marshal(PublicListResult{Items: publicListingListItems([]ListingDTO{item})})
if err != nil {
t.Fatalf("marshal public list error = %v", err)
}
detailRaw, err := json.Marshal(item.toPublicDetail())
if err != nil {
t.Fatalf("marshal public detail error = %v", err)
}
for _, field := range []string{
"account_id",
"owner_id",
"owner_phone",
"owner_nickname",
"source_channel",
"is_external_upload",
"status",
"review_status",
"handoff_mode",
"settlement_mode",
"managed_admin_id",
"review_reason",
"listing_group_conversation_id",
} {
if strings.Contains(string(listRaw), field) || strings.Contains(string(detailRaw), field) {
t.Fatalf("public response contains sensitive field %q", field)
}
}
if strings.Contains(string(listRaw), "description") || strings.Contains(string(listRaw), "screenshot_urls") {
t.Fatalf("public list contains detail-only fields: %s", listRaw)
}
if strings.Contains(string(listRaw), "contact_phone") || strings.Contains(string(listRaw), "首页不应携带") {
t.Fatalf("public list contains non-display asset fields: %s", listRaw)
}
if strings.Contains(string(detailRaw), "contact_phone") {
t.Fatalf("public detail contains non-public asset field: %s", detailRaw)
}
}
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
}