修复预计 截止时间
This commit is contained in:
@@ -23,6 +23,7 @@ type OrderDTO struct {
|
|||||||
LoginPlatform string `json:"login_platform"`
|
LoginPlatform string `json:"login_platform"`
|
||||||
RentedAt *time.Time `json:"rented_at"`
|
RentedAt *time.Time `json:"rented_at"`
|
||||||
EstimatedDurationHours int `json:"estimated_duration_hours"`
|
EstimatedDurationHours int `json:"estimated_duration_hours"`
|
||||||
|
EstimatedEndAt *time.Time `json:"estimated_end_at,omitempty"`
|
||||||
PriceRole string `json:"price_role,omitempty"`
|
PriceRole string `json:"price_role,omitempty"`
|
||||||
DisplayAmountCent int64 `json:"display_amount_cent"`
|
DisplayAmountCent int64 `json:"display_amount_cent"`
|
||||||
RentAmountCent *int64 `json:"rent_amount_cent,omitempty"`
|
RentAmountCent *int64 `json:"rent_amount_cent,omitempty"`
|
||||||
|
|||||||
@@ -40,7 +40,7 @@ func (r *Repository) Create(ctx context.Context, renterID uint64, req CreateRequ
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
rentHours := internalOrderHours
|
rentHours := estimateOrderDurationHours(snapshot)
|
||||||
pricing := buildOrderPricing(listing, account)
|
pricing := buildOrderPricing(listing, account)
|
||||||
depositOriginalAmountCent := listing.DepositAmountCent
|
depositOriginalAmountCent := listing.DepositAmountCent
|
||||||
paidDepositAmountCent, waivedDepositAmountCent, err := r.depositAmountsForOrder(tx, renterID, depositOriginalAmountCent)
|
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.Status = orderStatusRenting
|
||||||
order.HandoffStatus = handoffStatusReceived
|
order.HandoffStatus = handoffStatusReceived
|
||||||
durationHours := orderDurationHours(order)
|
order.EstimatedDurationHours = estimateOrderDurationHours(order.AccountSnapshot)
|
||||||
order.EstimatedDurationHours = durationHours
|
|
||||||
order.RentedAt = &now
|
order.RentedAt = &now
|
||||||
orderID := order.ID
|
orderID := order.ID
|
||||||
if err := notification.Append(tx, notification.Entry{
|
if err := notification.Append(tx, notification.Entry{
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import (
|
|||||||
func (row orderRow) toAdminDTO() OrderDTO {
|
func (row orderRow) toAdminDTO() OrderDTO {
|
||||||
rentedAt := row.RentedAt
|
rentedAt := row.RentedAt
|
||||||
durationHours := orderDurationHours(row.RentalOrder)
|
durationHours := orderDurationHours(row.RentalOrder)
|
||||||
|
estimatedEndAt := orderEstimatedEndAt(row.RentalOrder)
|
||||||
rentAmountCent := row.RentAmountCent
|
rentAmountCent := row.RentAmountCent
|
||||||
ownerRentAmountCent := row.OwnerRentAmountCent
|
ownerRentAmountCent := row.OwnerRentAmountCent
|
||||||
platformFeeCent := row.PlatformFeeCent
|
platformFeeCent := row.PlatformFeeCent
|
||||||
@@ -29,6 +30,7 @@ func (row orderRow) toAdminDTO() OrderDTO {
|
|||||||
LoginPlatform: row.LoginPlatform,
|
LoginPlatform: row.LoginPlatform,
|
||||||
RentedAt: rentedAt,
|
RentedAt: rentedAt,
|
||||||
EstimatedDurationHours: durationHours,
|
EstimatedDurationHours: durationHours,
|
||||||
|
EstimatedEndAt: estimatedEndAt,
|
||||||
PriceRole: "admin",
|
PriceRole: "admin",
|
||||||
DisplayAmountCent: row.RentAmountCent,
|
DisplayAmountCent: row.RentAmountCent,
|
||||||
RentAmountCent: &rentAmountCent,
|
RentAmountCent: &rentAmountCent,
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import (
|
|||||||
"encoding/json"
|
"encoding/json"
|
||||||
"math"
|
"math"
|
||||||
"strconv"
|
"strconv"
|
||||||
|
"time"
|
||||||
|
|
||||||
"hfb_sys/backend/internal/model"
|
"hfb_sys/backend/internal/model"
|
||||||
"hfb_sys/backend/pkg/money"
|
"hfb_sys/backend/pkg/money"
|
||||||
@@ -35,6 +36,38 @@ func orderDurationHours(order model.RentalOrder) int {
|
|||||||
return internalOrderHours
|
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 {
|
func buildOrderPricing(listing model.RentalListing, account model.GameAccount) orderPricing {
|
||||||
rentAmountCent := listing.PriceCent
|
rentAmountCent := listing.PriceCent
|
||||||
ownerRentAmountCent := int64(math.Round(readSnapshotPrice(account.AssetSummary, "seller_total_price") * 100))
|
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)
|
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元)
|
// roundMoney 使用统一的角精度(0.1元)
|
||||||
func roundMoney(value float64) float64 {
|
func roundMoney(value float64) float64 {
|
||||||
return money.Round(value)
|
return money.Round(value)
|
||||||
|
|||||||
@@ -2,9 +2,11 @@ package order
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"testing"
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
"hfb_sys/backend/internal/model"
|
"hfb_sys/backend/internal/model"
|
||||||
|
|
||||||
|
"gorm.io/datatypes"
|
||||||
"gorm.io/driver/sqlite"
|
"gorm.io/driver/sqlite"
|
||||||
"gorm.io/gorm"
|
"gorm.io/gorm"
|
||||||
"gorm.io/gorm/logger"
|
"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 验证租客取消待支付订单时,未完成支付流水会同步关闭。
|
// TestCancelClosesPendingPaymentOrder 验证租客取消待支付订单时,未完成支付流水会同步关闭。
|
||||||
func TestCancelClosesPendingPaymentOrder(t *testing.T) {
|
func TestCancelClosesPendingPaymentOrder(t *testing.T) {
|
||||||
db := setupOrderTestDB(t)
|
db := setupOrderTestDB(t)
|
||||||
|
|||||||
@@ -109,6 +109,7 @@ function orderRentedAt() {
|
|||||||
|
|
||||||
function orderEstimatedEndAt() {
|
function orderEstimatedEndAt() {
|
||||||
if (!order.value) return undefined
|
if (!order.value) return undefined
|
||||||
|
if (order.value.estimated_end_at) return order.value.estimated_end_at
|
||||||
const rentedAt = orderRentedAt()
|
const rentedAt = orderRentedAt()
|
||||||
const durationHours = Number(order.value.estimated_duration_hours || 0)
|
const durationHours = Number(order.value.estimated_duration_hours || 0)
|
||||||
if (!rentedAt || durationHours <= 0) return undefined
|
if (!rentedAt || durationHours <= 0) return undefined
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ export interface Order {
|
|||||||
login_platform: string
|
login_platform: string
|
||||||
rented_at?: string
|
rented_at?: string
|
||||||
estimated_duration_hours: number
|
estimated_duration_hours: number
|
||||||
|
estimated_end_at?: string
|
||||||
price_role?: 'renter' | 'owner' | 'admin' | string
|
price_role?: 'renter' | 'owner' | 'admin' | string
|
||||||
display_amount_cent: number
|
display_amount_cent: number
|
||||||
rent_amount_cent?: number
|
rent_amount_cent?: number
|
||||||
|
|||||||
@@ -211,6 +211,7 @@ export function getSnapshotHafCoinM(order: Order | null) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function orderEstimatedEndAt(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 rentedAt = order?.rented_at
|
||||||
const durationHours = Number(order?.estimated_duration_hours || 0)
|
const durationHours = Number(order?.estimated_duration_hours || 0)
|
||||||
if (!rentedAt || durationHours <= 0) return undefined
|
if (!rentedAt || durationHours <= 0) return undefined
|
||||||
|
|||||||
Reference in New Issue
Block a user