统一金额分制重构

This commit is contained in:
yml
2026-06-09 19:04:11 +08:00
parent 87673288ef
commit 5cd3ead4bd
69 changed files with 886 additions and 1277 deletions
+39 -39
View File
@@ -9,45 +9,45 @@ import (
)
type ListingDTO struct {
ID uint64 `json:"id"`
ListingNo string `json:"listing_no"`
AccountID uint64 `json:"account_id"`
OwnerID uint64 `json:"owner_id"`
OwnerPhone string `json:"owner_phone,omitempty"`
OwnerNickname string `json:"owner_nickname,omitempty"`
Title string `json:"title"`
Description string `json:"description"`
GameName string `json:"game_name"`
ServerRegion string `json:"server_region"`
LoginPlatform string `json:"login_platform"`
RankLevel string `json:"rank_level"`
HafCoinAmount int64 `json:"haf_coin_amount"`
AssetSummary map[string]any `json:"asset_summary,omitempty"`
ScreenshotURLS []string `json:"screenshot_urls"`
CoverURL string `json:"cover_url"`
PriceCent int64 `json:"price_cent"`
DepositAmountCent int64 `json:"deposit_amount_cent"`
IsAccelerated bool `json:"is_accelerated_sale"`
InTransaction bool `json:"in_transaction"`
Status string `json:"status"`
ReviewStatus string `json:"review_status"`
ReviewReason string `json:"review_reason"`
PublishedAt *time.Time `json:"published_at"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
ID uint64 `json:"id"`
ListingNo string `json:"listing_no"`
AccountID uint64 `json:"account_id"`
OwnerID uint64 `json:"owner_id"`
OwnerPhone string `json:"owner_phone,omitempty"`
OwnerNickname string `json:"owner_nickname,omitempty"`
Title string `json:"title"`
Description string `json:"description"`
GameName string `json:"game_name"`
ServerRegion string `json:"server_region"`
LoginPlatform string `json:"login_platform"`
RankLevel string `json:"rank_level"`
HafCoinAmount int64 `json:"haf_coin_amount"`
AssetSummary map[string]any `json:"asset_summary,omitempty"`
ScreenshotURLS []string `json:"screenshot_urls"`
CoverURL string `json:"cover_url"`
PriceCent int64 `json:"price_cent"`
DepositAmountCent int64 `json:"deposit_amount_cent"`
IsAccelerated bool `json:"is_accelerated_sale"`
InTransaction bool `json:"in_transaction"`
Status string `json:"status"`
ReviewStatus string `json:"review_status"`
ReviewReason string `json:"review_reason"`
PublishedAt *time.Time `json:"published_at"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
}
type CreateRequest struct {
Title string `json:"title" binding:"required"`
Description string `json:"description"`
ServerRegion string `json:"server_region" binding:"required"`
LoginPlatform string `json:"login_platform"`
RankLevel string `json:"rank_level"`
HafCoinAmount int64 `json:"haf_coin_amount"`
AssetSummary map[string]any `json:"asset_summary"`
ScreenshotURLS []string `json:"screenshot_urls"`
Price float64 `json:"price"`
DepositAmount float64 `json:"deposit_amount"`
Title string `json:"title" binding:"required"`
Description string `json:"description"`
ServerRegion string `json:"server_region" binding:"required"`
LoginPlatform string `json:"login_platform"`
RankLevel string `json:"rank_level"`
HafCoinAmount int64 `json:"haf_coin_amount"`
AssetSummary map[string]any `json:"asset_summary"`
ScreenshotURLS []string `json:"screenshot_urls"`
PriceCent int64 `json:"price_cent"`
DepositAmountCent int64 `json:"deposit_amount_cent"`
AgreedVirtualAssetSale bool `json:"agreed_virtual_asset_sale"`
AgreedSellerAgreement bool `json:"agreed_seller_agreement"`
@@ -60,9 +60,9 @@ type ReviewRequest struct {
}
type AdminPriceAdjustRequest struct {
BuyerRatio float64 `json:"buyer_ratio"`
BuyerTotalPrice float64 `json:"buyer_total_price"`
Reason string `json:"reason"`
BuyerRatio float64 `json:"buyer_ratio"`
BuyerTotalPriceCent int64 `json:"buyer_total_price_cent"`
Reason string `json:"reason"`
}
type AdminListQuery struct {
+30 -29
View File
@@ -79,16 +79,13 @@ func (r *Repository) Create(ownerID uint64, req CreateRequest, reviewRequired bo
if err := tx.Create(&account).Error; err != nil {
return err
}
price := normalizedListingPrice(req)
depositAmount := roundMoney(req.DepositAmount)
priceCent := normalizedListingPriceCent(req)
listing := model.RentalListing{
ListingNo: listingNo,
AccountID: account.ID,
OwnerID: ownerID,
Price: price,
PriceCent: int64(math.Round(price * 100)),
DepositAmount: depositAmount,
DepositAmountCent: int64(math.Round(depositAmount * 100)),
PriceCent: priceCent,
DepositAmountCent: req.DepositAmountCent,
Status: listingStatus,
ReviewStatus: reviewStatus,
PublishedAt: publishedAt,
@@ -149,16 +146,13 @@ func (r *Repository) CreateFromExternalUpload(upload externalUploadCreate, req C
if err := tx.Create(&account).Error; err != nil {
return err
}
price := normalizedListingPrice(req)
depositAmount := roundMoney(req.DepositAmount)
priceCent := normalizedListingPriceCent(req)
listing := model.RentalListing{
ListingNo: listingNo,
AccountID: account.ID,
OwnerID: owner.ID,
Price: price,
PriceCent: int64(math.Round(price * 100)),
DepositAmount: depositAmount,
DepositAmountCent: int64(math.Round(depositAmount * 100)),
PriceCent: priceCent,
DepositAmountCent: req.DepositAmountCent,
Status: "draft",
ReviewStatus: "pending",
}
@@ -216,9 +210,8 @@ func (r *Repository) Update(ownerID uint64, listingID uint64, req UpdateRequest,
}
account.ScreenshotURLS = screenshots
price := normalizedListingPrice(req)
listing.Price = price
listing.DepositAmount = roundMoney(req.DepositAmount)
listing.PriceCent = normalizedListingPriceCent(req)
listing.DepositAmountCent = req.DepositAmountCent
listingStatus, reviewStatus, publishedAt := initialPublishState(reviewRequired)
listing.Status = listingStatus
listing.ReviewStatus = reviewStatus
@@ -521,7 +514,7 @@ func (r *Repository) AdjustReviewPrice(adminID uint64, listingID uint64, req Adm
}
sellerTotalPrice := readSummaryNumber(breakdown["seller_total_price"])
if sellerTotalPrice <= 0 {
sellerTotalPrice = math.Max(0, listing.Price-consumablePrice)
sellerTotalPrice = math.Max(0, centToYuan(listing.PriceCent)-consumablePrice)
}
sellerCoinBasePrice := readSummaryNumber(breakdown["seller_coin_base_price"])
if sellerCoinBasePrice <= 0 {
@@ -536,13 +529,13 @@ func (r *Repository) AdjustReviewPrice(adminID uint64, listingID uint64, req Adm
return ErrInvalidPrice
}
beforePrice := listing.Price
beforePriceCent := listing.PriceCent
beforeRatio := readSummaryNumber(breakdown["buyer_ratio"])
if beforeRatio <= 0 && listing.Price > consumablePrice {
beforeRatio = roundRatio(coinWan / (listing.Price - consumablePrice))
if beforeRatio <= 0 && centToYuan(listing.PriceCent) > consumablePrice {
beforeRatio = roundRatio(coinWan / (centToYuan(listing.PriceCent) - consumablePrice))
}
listing.Price = buyerTotalPrice
listing.PriceCent = yuanToCent(buyerTotalPrice)
summary["publish_ratio"] = buyerRatio
breakdown["seller_coin_base_price"] = roundMoney(sellerCoinBasePrice)
breakdown["seller_total_price"] = roundMoney(sellerTotalPrice)
@@ -572,8 +565,8 @@ func (r *Repository) AdjustReviewPrice(adminID uint64, listingID uint64, req Adm
"listing_id": listing.ID,
"account_id": account.ID,
"owner_id": listing.OwnerID,
"before_price": beforePrice,
"after_price": listing.Price,
"before_price_cent": beforePriceCent,
"after_price_cent": listing.PriceCent,
"before_buyer_ratio": beforeRatio,
"after_buyer_ratio": buyerRatio,
"platform_markup": breakdown["platform_markup_amount"],
@@ -783,9 +776,9 @@ func canListPublicWithSQL(query PublicListQuery) bool {
func applyPublicSQLSort(db *gorm.DB, sortKey string) *gorm.DB {
switch sortKey {
case "priceAsc":
return db.Order("l.price ASC, l.published_at DESC, l.id DESC")
return db.Order("l.price_cent ASC, l.published_at DESC, l.id DESC")
case "priceDesc":
return db.Order("l.price DESC, l.published_at DESC, l.id DESC")
return db.Order("l.price_cent DESC, l.published_at DESC, l.id DESC")
case "coinDesc":
return db.Order("a.haf_coin_amount DESC, l.published_at DESC, l.id DESC")
default:
@@ -1384,16 +1377,16 @@ func rowsToDTO(rows []listingRow) []ListingDTO {
return items
}
func normalizedListingPrice(req CreateRequest) float64 {
func normalizedListingPriceCent(req CreateRequest) int64 {
if req.AssetSummary != nil {
if breakdown, ok := req.AssetSummary["price_breakdown"].(map[string]any); ok {
buyerPrice := readSummaryNumber(breakdown["buyer_total_price"])
if buyerPrice > 0 {
return roundMoney(buyerPrice)
return yuanToCent(buyerPrice)
}
}
}
return roundMoney(req.Price)
return req.PriceCent
}
func publicListings(items []ListingDTO) []ListingDTO {
@@ -1620,8 +1613,8 @@ func ensurePriceBreakdown(summary map[string]any) map[string]any {
}
func calculateAdminAdjustedPrice(req AdminPriceAdjustRequest, coinWan float64, consumablePrice float64) (float64, float64, float64) {
if req.BuyerTotalPrice > 0 {
buyerTotalPrice := roundMoney(req.BuyerTotalPrice)
if req.BuyerTotalPriceCent > 0 {
buyerTotalPrice := roundMoney(centToYuan(req.BuyerTotalPriceCent))
buyerCoinBasePrice := roundMoney(buyerTotalPrice - consumablePrice)
if buyerCoinBasePrice <= 0 || coinWan <= 0 {
return 0, 0, 0
@@ -1643,6 +1636,14 @@ func roundRatio(value float64) float64 {
return math.Round(value*10) / 10
}
func yuanToCent(value float64) int64 {
return int64(math.Round(roundMoney(value) * 100))
}
func centToYuan(value int64) float64 {
return float64(value) / 100
}
func cleanScreenshotURLs(urls []string) []string {
cleaned := make([]string, 0, len(urls))
seen := make(map[string]struct{}, len(urls))
+14 -14
View File
@@ -252,7 +252,7 @@ func (s *Service) AdjustReviewPrice(adminID uint64, id uint64, req AdminPriceAdj
if s.repo == nil {
return nil, ErrDependencyUnavailable
}
if req.BuyerRatio <= 0 && req.BuyerTotalPrice <= 0 {
if req.BuyerRatio <= 0 && req.BuyerTotalPriceCent <= 0 {
return nil, ErrInvalidPrice
}
return s.repo.AdjustReviewPrice(adminID, id, req, meta)
@@ -328,13 +328,13 @@ func validateRequest(req CreateRequest, rules publishRules) error {
if strings.TrimSpace(req.ServerRegion) == "" {
return ErrMissingServerRegion
}
if normalizedListingPrice(req) <= 0 {
if normalizedListingPriceCent(req) <= 0 {
return ErrInvalidPrice
}
if req.DepositAmount < 0 {
if req.DepositAmountCent < 0 {
return ErrInvalidDeposit
}
if consumables := consumableValue(req.AssetSummary); consumables > 0 && req.DepositAmount <= consumables {
if consumables := consumableValue(req.AssetSummary); consumables > 0 && req.DepositAmountCent <= yuanToCent(consumables) {
return ErrDepositTooLow
}
if req.HafCoinAmount < 0 {
@@ -525,16 +525,16 @@ func externalAccountToCreateRequest(uploaderName string, uploadTime int64, item
},
}
return CreateRequest{
Title: externalUploadTitle(item, insurance, hafCoinM),
Description: "开放接口自动上传,等待后台审核。",
ServerRegion: serverRegionFromLoginMethod(item.LoginMethod),
LoginPlatform: strings.TrimSpace(item.LoginMethod),
RankLevel: strings.TrimSpace(item.Rank),
HafCoinAmount: int64(math.Round(hafCoinM * 1000000)),
AssetSummary: assetSummary,
ScreenshotURLS: []string{defaultUploadScreenshot},
Price: price,
DepositAmount: item.Deposit,
Title: externalUploadTitle(item, insurance, hafCoinM),
Description: "开放接口自动上传,等待后台审核。",
ServerRegion: serverRegionFromLoginMethod(item.LoginMethod),
LoginPlatform: strings.TrimSpace(item.LoginMethod),
RankLevel: strings.TrimSpace(item.Rank),
HafCoinAmount: int64(math.Round(hafCoinM * 1000000)),
AssetSummary: assetSummary,
ScreenshotURLS: []string{defaultUploadScreenshot},
PriceCent: yuanToCent(price),
DepositAmountCent: yuanToCent(item.Deposit),
}
}
@@ -27,7 +27,7 @@ func TestCalculateAdminAdjustedPriceByRatio(t *testing.T) {
}
func TestCalculateAdminAdjustedPriceByTotalPrice(t *testing.T) {
base, total, ratio := calculateAdminAdjustedPrice(AdminPriceAdjustRequest{BuyerTotalPrice: 70}, 1000, 20)
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)
}
@@ -35,12 +35,12 @@ func TestCalculateAdminAdjustedPriceByTotalPrice(t *testing.T) {
func TestValidateRequestRequiresDepositAboveConsumables(t *testing.T) {
req := CreateRequest{
Title: "测试账号",
ServerRegion: "烽火地带",
Price: 100,
DepositAmount: 2,
HafCoinAmount: 1000000,
ScreenshotURLS: []string{"https://example.com/a.png"},
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元/个"},
@@ -52,7 +52,7 @@ func TestValidateRequestRequiresDepositAboveConsumables(t *testing.T) {
t.Fatalf("expected ErrDepositTooLow, got %v", err)
}
req.DepositAmount = 3
req.DepositAmountCent = 300
if err := validateRequest(req, publishRules{}); err != nil {
t.Fatalf("expected valid request, got %v", err)
}
@@ -159,8 +159,8 @@ func TestExternalAccountToCreateRequestMapsUploadFields(t *testing.T) {
if req.HafCoinAmount != 197100000 {
t.Fatalf("expected haf coin amount 197100000, got %d", req.HafCoinAmount)
}
if req.Price != 458 || req.DepositAmount != 400 {
t.Fatalf("unexpected price/deposit %.2f/%.2f", req.Price, req.DepositAmount)
if req.PriceCent != 45800 || 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"])