- 订单列表使用独立最小 DTO 并分页,号主待办提供独立接口与统计 - 用户 token 增加版本控制,冻结/改密/退出即时撤销会话 - 移除 URL token 传参,SSE 与接口统一使用 HttpOnly Cookie - 私有文件按上传归属与业务关联授权,收款凭证转私有访问并校验归属 - 公开商品接口返回最小字段,隐藏号主身份与内部状态 - 每日清理超过 30 天未关联业务的上传归属,上传归属失败时补偿删除对象
724 lines
23 KiB
Go
724 lines
23 KiB
Go
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"])
|
||
}
|
||
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 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
|
||
}
|