修复预计 截止时间

This commit is contained in:
yml
2026-06-17 23:10:38 +08:00
parent f8156aec44
commit 0add23dedb
8 changed files with 162 additions and 3 deletions
+1
View File
@@ -23,6 +23,7 @@ type OrderDTO struct {
LoginPlatform string `json:"login_platform"`
RentedAt *time.Time `json:"rented_at"`
EstimatedDurationHours int `json:"estimated_duration_hours"`
EstimatedEndAt *time.Time `json:"estimated_end_at,omitempty"`
PriceRole string `json:"price_role,omitempty"`
DisplayAmountCent int64 `json:"display_amount_cent"`
RentAmountCent *int64 `json:"rent_amount_cent,omitempty"`
+2 -3
View File
@@ -40,7 +40,7 @@ func (r *Repository) Create(ctx context.Context, renterID uint64, req CreateRequ
if err != nil {
return err
}
rentHours := internalOrderHours
rentHours := estimateOrderDurationHours(snapshot)
pricing := buildOrderPricing(listing, account)
depositOriginalAmountCent := listing.DepositAmountCent
paidDepositAmountCent, waivedDepositAmountCent, err := r.depositAmountsForOrder(tx, renterID, depositOriginalAmountCent)
@@ -339,8 +339,7 @@ func (r *Repository) ConfirmReceive(ctx context.Context, userID uint64, orderID
}
order.Status = orderStatusRenting
order.HandoffStatus = handoffStatusReceived
durationHours := orderDurationHours(order)
order.EstimatedDurationHours = durationHours
order.EstimatedDurationHours = estimateOrderDurationHours(order.AccountSnapshot)
order.RentedAt = &now
orderID := order.ID
if err := notification.Append(tx, notification.Entry{
@@ -11,6 +11,7 @@ import (
func (row orderRow) toAdminDTO() OrderDTO {
rentedAt := row.RentedAt
durationHours := orderDurationHours(row.RentalOrder)
estimatedEndAt := orderEstimatedEndAt(row.RentalOrder)
rentAmountCent := row.RentAmountCent
ownerRentAmountCent := row.OwnerRentAmountCent
platformFeeCent := row.PlatformFeeCent
@@ -29,6 +30,7 @@ func (row orderRow) toAdminDTO() OrderDTO {
LoginPlatform: row.LoginPlatform,
RentedAt: rentedAt,
EstimatedDurationHours: durationHours,
EstimatedEndAt: estimatedEndAt,
PriceRole: "admin",
DisplayAmountCent: row.RentAmountCent,
RentAmountCent: &rentAmountCent,
+51
View File
@@ -4,6 +4,7 @@ import (
"encoding/json"
"math"
"strconv"
"time"
"hfb_sys/backend/internal/model"
"hfb_sys/backend/pkg/money"
@@ -35,6 +36,38 @@ func orderDurationHours(order model.RentalOrder) int {
return internalOrderHours
}
func orderEstimatedEndAt(order model.RentalOrder) *time.Time {
if order.RentedAt == nil {
return nil
}
durationHours := orderDurationHours(order)
if durationHours <= 0 {
return nil
}
endAt := order.RentedAt.Add(time.Duration(durationHours) * time.Hour)
return &endAt
}
func estimateOrderDurationHours(snapshot datatypes.JSON) int {
hafCoinM := readOrderSnapshotCoinM(snapshot)
if hafCoinM <= 0 {
return internalOrderHours
}
dailyLossM := readOrderSnapshotAssetNumber(snapshot, "daily_loss_m")
if dailyLossM <= 0 {
return internalOrderHours
}
days := hafCoinM / dailyLossM
if days <= 0 {
return internalOrderHours
}
hours := int(math.Round(days * 24))
if hours <= 0 {
return internalOrderHours
}
return hours
}
func buildOrderPricing(listing model.RentalListing, account model.GameAccount) orderPricing {
rentAmountCent := listing.PriceCent
ownerRentAmountCent := int64(math.Round(readSnapshotPrice(account.AssetSummary, "seller_total_price") * 100))
@@ -203,6 +236,24 @@ func readOrderSnapshotCoinM(raw datatypes.JSON) float64 {
return roundQuantity(readJSONNumber(snapshot["haf_coin_amount"]) / 1000000)
}
func readOrderSnapshotAssetNumber(raw datatypes.JSON, key string) float64 {
if len(raw) == 0 {
return 0
}
var snapshot map[string]any
if err := json.Unmarshal(raw, &snapshot); err != nil {
return 0
}
assetSummary, ok := snapshot["asset_summary"].(map[string]any)
if !ok {
return readJSONNumber(snapshot[key])
}
if value, ok := assetSummary[key]; ok {
return readJSONNumber(value)
}
return readJSONNumber(snapshot[key])
}
// roundMoney 使用统一的角精度(0.1元)
func roundMoney(value float64) float64 {
return money.Round(value)
@@ -2,9 +2,11 @@ package order
import (
"testing"
"time"
"hfb_sys/backend/internal/model"
"gorm.io/datatypes"
"gorm.io/driver/sqlite"
"gorm.io/gorm"
"gorm.io/gorm/logger"
@@ -378,6 +380,107 @@ func TestOrderDurationHoursUsesDefault(t *testing.T) {
}
}
func TestEstimateOrderDurationHoursFromSnapshot(t *testing.T) {
snapshot := datatypes.JSON([]byte(`{
"haf_coin_amount": 165000000,
"asset_summary": {
"daily_loss_m": 10
}
}`))
hours := estimateOrderDurationHours(snapshot)
if hours != 396 {
t.Fatalf("hours = %d, want 396", hours)
}
}
func TestEstimateOrderDurationHoursUsesDefaultWithoutDailyLoss(t *testing.T) {
snapshot := datatypes.JSON([]byte(`{
"haf_coin_amount": 165000000,
"asset_summary": {}
}`))
hours := estimateOrderDurationHours(snapshot)
if hours != internalOrderHours {
t.Fatalf("hours = %d, want %d", hours, internalOrderHours)
}
}
func TestConfirmReceiveRecalculatesEstimatedDuration(t *testing.T) {
db := setupOrderTestDB(t)
repo := NewRepository(db)
owner := model.User{Phone: "13800002001"}
renter := model.User{Phone: "13900002001"}
if err := db.Create(&owner).Error; err != nil {
t.Fatalf("create owner failed: %v", err)
}
if err := db.Create(&renter).Error; err != nil {
t.Fatalf("create renter failed: %v", err)
}
account := model.GameAccount{
OwnerID: owner.ID,
Status: "rented",
ServerRegion: "国服",
LoginPlatform: "steam",
Title: "测试账号",
HafCoinAmount: 165000000,
AssetSummary: datatypes.JSON([]byte(`{"daily_loss_m":10}`)),
}
if err := db.Create(&account).Error; err != nil {
t.Fatalf("create account failed: %v", err)
}
listing := model.RentalListing{
ListingNo: "LST-DUR-001",
OwnerID: owner.ID,
AccountID: account.ID,
Status: "published",
ReviewStatus: "approved",
InTransaction: true,
PriceCent: 1000,
}
if err := db.Create(&listing).Error; err != nil {
t.Fatalf("create listing failed: %v", err)
}
snapshot, err := makeAccountSnapshot(account, listing)
if err != nil {
t.Fatalf("make snapshot failed: %v", err)
}
order := model.RentalOrder{
OrderNo: "ORD-DUR-001",
ListingID: listing.ID,
AccountID: account.ID,
OwnerID: owner.ID,
RenterID: renter.ID,
EstimatedDurationHours: internalOrderHours,
AccountSnapshot: snapshot,
Status: orderStatusPendingHandoff,
HandoffStatus: handoffStatusPendingRenterConfirm,
}
if err := db.Create(&order).Error; err != nil {
t.Fatalf("create order failed: %v", err)
}
if err := repo.ConfirmReceive(t.Context(), renter.ID, order.ID); err != nil {
t.Fatalf("ConfirmReceive() error = %v", err)
}
var saved model.RentalOrder
if err := db.First(&saved, order.ID).Error; err != nil {
t.Fatalf("load order failed: %v", err)
}
if saved.EstimatedDurationHours != 396 {
t.Fatalf("EstimatedDurationHours = %d, want 396", saved.EstimatedDurationHours)
}
if saved.RentedAt == nil || saved.RentedAt.After(time.Now()) {
t.Fatalf("RentedAt not set correctly: %#v", saved.RentedAt)
}
}
// TestCancelClosesPendingPaymentOrder 验证租客取消待支付订单时,未完成支付流水会同步关闭。
func TestCancelClosesPendingPaymentOrder(t *testing.T) {
db := setupOrderTestDB(t)
@@ -109,6 +109,7 @@ function orderRentedAt() {
function orderEstimatedEndAt() {
if (!order.value) return undefined
if (order.value.estimated_end_at) return order.value.estimated_end_at
const rentedAt = orderRentedAt()
const durationHours = Number(order.value.estimated_duration_hours || 0)
if (!rentedAt || durationHours <= 0) return undefined
@@ -18,6 +18,7 @@ export interface Order {
login_platform: string
rented_at?: string
estimated_duration_hours: number
estimated_end_at?: string
price_role?: 'renter' | 'owner' | 'admin' | string
display_amount_cent: number
rent_amount_cent?: number
@@ -211,6 +211,7 @@ export function getSnapshotHafCoinM(order: Order | null) {
}
export function orderEstimatedEndAt(order: Order | null) {
if (order?.estimated_end_at) return order.estimated_end_at
const rentedAt = order?.rented_at
const durationHours = Number(order?.estimated_duration_hours || 0)
if (!rentedAt || durationHours <= 0) return undefined