拆分订单仓储模块
This commit is contained in:
@@ -0,0 +1,232 @@
|
||||
package order
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"hfb_sys/backend/internal/model"
|
||||
"hfb_sys/backend/internal/modules/notification"
|
||||
|
||||
"gorm.io/gorm"
|
||||
"gorm.io/gorm/clause"
|
||||
)
|
||||
|
||||
func (r *Repository) AdminClose(adminID uint64, orderID uint64, req AdminActionRequest, meta AuditMeta) error {
|
||||
var refund *refundAction
|
||||
err := r.db.Transaction(func(tx *gorm.DB) error {
|
||||
order, listing, account, err := r.findOrderAssetsForAdminUpdate(tx, orderID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if isTerminalStatus(order.Status) {
|
||||
return ErrOrderCannotComplete
|
||||
}
|
||||
beforeOrderStatus := order.Status
|
||||
beforeHandoffStatus := order.HandoffStatus
|
||||
beforeSettlementStatus := order.SettlementStatus
|
||||
beforeListingStatus := listing.Status
|
||||
beforeAccountStatus := account.Status
|
||||
now := time.Now()
|
||||
order.Status = "closed"
|
||||
order.HandoffStatus = "admin_closed"
|
||||
order.SettlementStatus = "closed"
|
||||
order.SettledAt = &now
|
||||
listing.Status = "offline"
|
||||
listing.InTransaction = false
|
||||
account.Status = "offline"
|
||||
if beforeOrderStatus != "pending_payment" {
|
||||
totalCent := order.RentAmountCent + order.DepositAmountCent
|
||||
action, err := r.prepareRefund(order, totalCent, "admin_close_refund", "客服关闭订单原路退款")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
refund = action
|
||||
}
|
||||
if err := notification.Append(tx,
|
||||
notification.Entry{
|
||||
UserID: order.RenterID,
|
||||
Type: "order_admin",
|
||||
Title: "订单已由客服关闭",
|
||||
Content: "客服已关闭订单,退款将原路退回您的支付账户。原因:" + req.Reason,
|
||||
BizType: "order",
|
||||
BizID: &order.ID,
|
||||
},
|
||||
notification.Entry{
|
||||
UserID: order.OwnerID,
|
||||
Type: "order_admin",
|
||||
Title: "订单已由客服关闭",
|
||||
Content: "客服已关闭订单,关联商品已下架。原因:" + req.Reason,
|
||||
BizType: "order",
|
||||
BizID: &order.ID,
|
||||
},
|
||||
); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := appendAuditLog(tx, adminID, "order.admin_close", "order", order.ID, meta, map[string]any{
|
||||
"order_id": order.ID,
|
||||
"order_no": order.OrderNo,
|
||||
"listing_id": order.ListingID,
|
||||
"account_id": order.AccountID,
|
||||
"reason": req.Reason,
|
||||
"before_order_status": beforeOrderStatus,
|
||||
"after_order_status": order.Status,
|
||||
"before_handoff_status": beforeHandoffStatus,
|
||||
"after_handoff_status": order.HandoffStatus,
|
||||
"before_settlement_status": beforeSettlementStatus,
|
||||
"after_settlement_status": order.SettlementStatus,
|
||||
"before_listing_status": beforeListingStatus,
|
||||
"after_listing_status": listing.Status,
|
||||
"before_account_status": beforeAccountStatus,
|
||||
"after_account_status": account.Status,
|
||||
}); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := tx.Save(order).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
if err := tx.Save(listing).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
return tx.Save(account).Error
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
r.startRefundBestEffort(refund)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *Repository) AdminMarkAbnormal(adminID uint64, orderID uint64, req AdminActionRequest, meta AuditMeta) error {
|
||||
return r.db.Transaction(func(tx *gorm.DB) error {
|
||||
order, listing, account, err := r.findOrderAssetsForAdminUpdate(tx, orderID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if isTerminalStatus(order.Status) {
|
||||
return ErrOrderCannotComplete
|
||||
}
|
||||
beforeOrderStatus := order.Status
|
||||
beforeHandoffStatus := order.HandoffStatus
|
||||
beforeListingStatus := listing.Status
|
||||
beforeAccountStatus := account.Status
|
||||
order.Status = "abnormal"
|
||||
order.HandoffStatus = "admin_abnormal"
|
||||
listing.Status = "abnormal"
|
||||
listing.InTransaction = false
|
||||
account.Status = "abnormal"
|
||||
if err := notification.Append(tx,
|
||||
notification.Entry{
|
||||
UserID: order.RenterID,
|
||||
Type: "order_admin",
|
||||
Title: "订单已被标记异常",
|
||||
Content: "客服已将订单标记为异常,请等待进一步处理。原因:" + req.Reason,
|
||||
BizType: "order",
|
||||
BizID: &order.ID,
|
||||
},
|
||||
notification.Entry{
|
||||
UserID: order.OwnerID,
|
||||
Type: "order_admin",
|
||||
Title: "订单已被标记异常",
|
||||
Content: "客服已将订单标记为异常,关联商品暂不可出租。原因:" + req.Reason,
|
||||
BizType: "order",
|
||||
BizID: &order.ID,
|
||||
},
|
||||
); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := appendAuditLog(tx, adminID, "order.mark_abnormal", "order", order.ID, meta, map[string]any{
|
||||
"order_id": order.ID,
|
||||
"order_no": order.OrderNo,
|
||||
"listing_id": order.ListingID,
|
||||
"account_id": order.AccountID,
|
||||
"reason": req.Reason,
|
||||
"before_order_status": beforeOrderStatus,
|
||||
"after_order_status": order.Status,
|
||||
"before_handoff_status": beforeHandoffStatus,
|
||||
"after_handoff_status": order.HandoffStatus,
|
||||
"before_listing_status": beforeListingStatus,
|
||||
"after_listing_status": listing.Status,
|
||||
"before_account_status": beforeAccountStatus,
|
||||
"after_account_status": account.Status,
|
||||
}); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := tx.Save(order).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
if err := tx.Save(listing).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
return tx.Save(account).Error
|
||||
})
|
||||
}
|
||||
|
||||
// AdminRefund 触发后台人工退款,退款由 payment 模块走渠道原路退回。
|
||||
func (r *Repository) AdminRefund(orderID uint64) (*RefundStatusDTO, error) {
|
||||
var order model.RentalOrder
|
||||
if err := r.db.First(&order, orderID).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if order.RefundStatus == "refunded" {
|
||||
return r.buildRefundStatusDTO(&order), nil
|
||||
}
|
||||
if r.refundFunc == nil {
|
||||
return nil, ErrDependencyUnavailable
|
||||
}
|
||||
totalCent := order.RentAmountCent + order.DepositAmountCent
|
||||
if totalCent <= 0 {
|
||||
return nil, ErrInvalidCheckoutAmount
|
||||
}
|
||||
status, err := r.refundFunc(orderID, totalCent, "admin_refund", "后台人工退款")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// 重新读取订单,拿到 payment 模块更新后的退款字段。
|
||||
if err := r.db.First(&order, orderID).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
dto := r.buildRefundStatusDTO(&order)
|
||||
if status != "" {
|
||||
dto.RefundStatus = status
|
||||
}
|
||||
return dto, nil
|
||||
}
|
||||
|
||||
// AdminRefundStatus 查询订单退款状态。
|
||||
func (r *Repository) AdminRefundStatus(orderID uint64) (*RefundStatusDTO, error) {
|
||||
var order model.RentalOrder
|
||||
if err := r.db.First(&order, orderID).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return r.buildRefundStatusDTO(&order), nil
|
||||
}
|
||||
|
||||
func (r *Repository) buildRefundStatusDTO(order *model.RentalOrder) *RefundStatusDTO {
|
||||
return &RefundStatusDTO{
|
||||
OrderID: order.ID,
|
||||
OrderNo: order.OrderNo,
|
||||
RefundStatus: order.RefundStatus,
|
||||
RefundAmountCent: order.RefundAmountCent,
|
||||
RefundedAt: order.RefundedAt,
|
||||
TotalAmountCent: order.RentAmountCent + order.DepositAmountCent,
|
||||
}
|
||||
}
|
||||
|
||||
func (r *Repository) findOrderAssetsForAdminUpdate(tx *gorm.DB, orderID uint64) (*model.RentalOrder, *model.RentalListing, *model.GameAccount, error) {
|
||||
var order model.RentalOrder
|
||||
if err := tx.Clauses(clause.Locking{Strength: "UPDATE"}).First(&order, orderID).Error; err != nil {
|
||||
return nil, nil, nil, err
|
||||
}
|
||||
var listing model.RentalListing
|
||||
if err := tx.Clauses(clause.Locking{Strength: "UPDATE"}).First(&listing, order.ListingID).Error; err != nil {
|
||||
return nil, nil, nil, err
|
||||
}
|
||||
var account model.GameAccount
|
||||
if err := tx.Clauses(clause.Locking{Strength: "UPDATE"}).First(&account, order.AccountID).Error; err != nil {
|
||||
return nil, nil, nil, err
|
||||
}
|
||||
return &order, &listing, &account, nil
|
||||
}
|
||||
|
||||
func isTerminalStatus(status string) bool {
|
||||
return status == "completed" || status == "cancelled" || status == "closed"
|
||||
}
|
||||
@@ -0,0 +1,347 @@
|
||||
package order
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"hfb_sys/backend/internal/model"
|
||||
"hfb_sys/backend/internal/modules/notification"
|
||||
"hfb_sys/backend/internal/modules/wallet"
|
||||
|
||||
"gorm.io/gorm"
|
||||
"gorm.io/gorm/clause"
|
||||
)
|
||||
|
||||
func (r *Repository) SubmitCheckout(userID uint64, orderID uint64, req SubmitCheckoutRequest) (*HandoffRecordDTO, error) {
|
||||
var recordID uint64
|
||||
err := r.db.Transaction(func(tx *gorm.DB) error {
|
||||
var order model.RentalOrder
|
||||
if err := tx.Clauses(clause.Locking{Strength: "UPDATE"}).First(&order, orderID).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
if order.RenterID != userID {
|
||||
return ErrPermissionDenied
|
||||
}
|
||||
if (order.Status != "renting" && order.Status != "overdue") || (order.HandoffStatus != "received" && order.HandoffStatus != "return_overdue") {
|
||||
return ErrCheckoutCannotSubmit
|
||||
}
|
||||
hasOpen, err := hasOpenCheckout(tx, order.ID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if hasOpen {
|
||||
return ErrCheckoutCannotSubmit
|
||||
}
|
||||
record := model.HandoffRecord{
|
||||
OrderID: order.ID,
|
||||
FromUserID: order.RenterID,
|
||||
ToUserID: order.OwnerID,
|
||||
Type: "renter_checkout",
|
||||
Content: req.Content,
|
||||
}
|
||||
if err := tx.Create(&record).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
checkout, err := buildCheckout(order, order.RenterID, "submitted", req.Content, req.EvidenceURLS, req.ConsumableAmountCent, req.CoinConsumedM, req.OtherAmountCent, 0, false)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := tx.Create(&checkout).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
order.Status = "pending_checkout_confirm"
|
||||
order.HandoffStatus = "pending_owner_checkout"
|
||||
order.SettlementStatus = "pending"
|
||||
orderID := order.ID
|
||||
if err := notification.Append(tx, notification.Entry{
|
||||
UserID: order.OwnerID,
|
||||
Type: "checkout",
|
||||
Title: "租客已发起结账",
|
||||
Content: "请检查账号状态和消耗明细,确认无误后完成结算。",
|
||||
BizType: "order",
|
||||
BizID: &orderID,
|
||||
}); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := tx.Save(&order).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
recordID = record.ID
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return r.findHandoffRecord(recordID)
|
||||
}
|
||||
|
||||
func (r *Repository) ConfirmReturn(userID uint64, orderID uint64) error {
|
||||
return r.ConfirmCheckout(userID, orderID)
|
||||
}
|
||||
|
||||
func (r *Repository) ConfirmCheckout(userID uint64, orderID uint64) error {
|
||||
var refund *refundAction
|
||||
err := r.db.Transaction(func(tx *gorm.DB) error {
|
||||
var order model.RentalOrder
|
||||
if err := tx.Clauses(clause.Locking{Strength: "UPDATE"}).First(&order, orderID).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
if order.OwnerID != userID {
|
||||
return ErrPermissionDenied
|
||||
}
|
||||
if order.Status != "pending_checkout_confirm" || order.HandoffStatus != "pending_owner_checkout" {
|
||||
return ErrCheckoutCannotConfirm
|
||||
}
|
||||
var checkout model.OrderCheckout
|
||||
if err := tx.Clauses(clause.Locking{Strength: "UPDATE"}).
|
||||
Where("order_id = ? AND status = ?", order.ID, "submitted").
|
||||
Order("id DESC").
|
||||
First(&checkout).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
now := time.Now()
|
||||
if err := tx.Model(&model.HandoffRecord{}).
|
||||
Where("order_id = ? AND type = ?", order.ID, "renter_checkout").
|
||||
Update("confirmed_by_owner_at", now).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
checkout.Status = "accepted"
|
||||
checkout.OwnerAdjustedAt = &now
|
||||
action, err := r.finalizeCheckout(tx, &order, &checkout, "号主已确认结账,订单完成。")
|
||||
refund = action
|
||||
return err
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
r.startRefundBestEffort(refund)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *Repository) CounterCheckout(userID uint64, orderID uint64, req CounterCheckoutRequest) (*CheckoutDTO, error) {
|
||||
var checkoutID uint64
|
||||
err := r.db.Transaction(func(tx *gorm.DB) error {
|
||||
var order model.RentalOrder
|
||||
if err := tx.Clauses(clause.Locking{Strength: "UPDATE"}).First(&order, orderID).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
if order.OwnerID != userID {
|
||||
return ErrPermissionDenied
|
||||
}
|
||||
if order.Status != "pending_checkout_confirm" || order.HandoffStatus != "pending_owner_checkout" {
|
||||
return ErrCheckoutCannotCounter
|
||||
}
|
||||
var checkout model.OrderCheckout
|
||||
if err := tx.Clauses(clause.Locking{Strength: "UPDATE"}).
|
||||
Where("order_id = ? AND status = ?", order.ID, "submitted").
|
||||
Order("id DESC").
|
||||
First(&checkout).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
next, err := buildCheckout(order, checkout.InitiatedBy, "countered", checkout.Content, req.EvidenceURLS, req.ConsumableAmountCent, req.CoinConsumedM, req.OtherAmountCent, req.DepositDeductAmountCent, true)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
now := time.Now()
|
||||
checkout.Status = "countered"
|
||||
checkout.RentAmountCent = next.RentAmountCent
|
||||
checkout.OwnerRentAmountCent = next.OwnerRentAmountCent
|
||||
checkout.PlatformFeeCent = next.PlatformFeeCent
|
||||
checkout.DepositAmountCent = next.DepositAmountCent
|
||||
checkout.ConsumableAmountCent = next.ConsumableAmountCent
|
||||
checkout.CoinConsumedM = next.CoinConsumedM
|
||||
checkout.OtherAmountCent = next.OtherAmountCent
|
||||
checkout.DepositDeductAmountCent = next.DepositDeductAmountCent
|
||||
checkout.RenterRefundAmountCent = next.RenterRefundAmountCent
|
||||
checkout.OwnerIncomeAmountCent = next.OwnerIncomeAmountCent
|
||||
checkout.OwnerAdjustmentReason = req.Reason
|
||||
checkout.OwnerAdjustedAt = &now
|
||||
checkout.EvidenceURLS = next.EvidenceURLS
|
||||
order.Status = "pending_checkout_accept"
|
||||
order.HandoffStatus = "pending_renter_checkout"
|
||||
order.SettlementStatus = "pending"
|
||||
orderID := order.ID
|
||||
if err := notification.Append(tx, notification.Entry{
|
||||
UserID: order.RenterID,
|
||||
Type: "checkout",
|
||||
Title: "号主已修改结账金额",
|
||||
Content: "请核对号主修正的消耗和结算金额。同意后订单完成;不同意可发起争议。",
|
||||
BizType: "order",
|
||||
BizID: &orderID,
|
||||
}); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := tx.Save(&order).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
if err := tx.Save(&checkout).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
checkoutID = checkout.ID
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
checkout, err := r.findCheckout(checkoutID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
dto := toCheckoutDTOForUser(*checkout, userID, model.RentalOrder{
|
||||
OwnerID: userID,
|
||||
RentAmountCent: checkout.RentAmountCent,
|
||||
OwnerRentAmountCent: checkout.OwnerRentAmountCent,
|
||||
DepositAmountCent: checkout.DepositAmountCent,
|
||||
})
|
||||
return &dto, nil
|
||||
}
|
||||
|
||||
func (r *Repository) AcceptCheckout(userID uint64, orderID uint64) error {
|
||||
var refund *refundAction
|
||||
err := r.db.Transaction(func(tx *gorm.DB) error {
|
||||
var order model.RentalOrder
|
||||
if err := tx.Clauses(clause.Locking{Strength: "UPDATE"}).First(&order, orderID).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
if order.RenterID != userID {
|
||||
return ErrPermissionDenied
|
||||
}
|
||||
if order.Status != "pending_checkout_accept" || order.HandoffStatus != "pending_renter_checkout" {
|
||||
return ErrCheckoutCannotConfirm
|
||||
}
|
||||
var checkout model.OrderCheckout
|
||||
if err := tx.Clauses(clause.Locking{Strength: "UPDATE"}).
|
||||
Where("order_id = ? AND status = ?", order.ID, "countered").
|
||||
Order("id DESC").
|
||||
First(&checkout).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
now := time.Now()
|
||||
checkout.Status = "accepted"
|
||||
checkout.RenterConfirmedAt = &now
|
||||
action, err := r.finalizeCheckout(tx, &order, &checkout, "租客已确认修正结账,订单完成。")
|
||||
refund = action
|
||||
return err
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
r.startRefundBestEffort(refund)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *Repository) finalizeCheckout(tx *gorm.DB, order *model.RentalOrder, checkout *model.OrderCheckout, renterContent string) (*refundAction, error) {
|
||||
var listing model.RentalListing
|
||||
if err := tx.Clauses(clause.Locking{Strength: "UPDATE"}).First(&listing, order.ListingID).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var account model.GameAccount
|
||||
if err := tx.Clauses(clause.Locking{Strength: "UPDATE"}).First(&account, order.AccountID).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
now := time.Now()
|
||||
order.Status = "completed"
|
||||
order.HandoffStatus = "returned"
|
||||
order.SettlementStatus = "settled"
|
||||
order.SettledAt = &now
|
||||
order.OwnerSettledAt = &now
|
||||
archiveListingAfterCheckout(&listing, &account)
|
||||
orderID := order.ID
|
||||
settlement := buildCheckoutSettlement(*order, checkout)
|
||||
|
||||
// 卖家收入进入站内钱包;租客资金不进入站内钱包。
|
||||
var ownerEntries []wallet.Entry
|
||||
if settlement.OwnerRentIncomeCent > 0 {
|
||||
ownerEntries = append(ownerEntries, wallet.Entry{
|
||||
UserID: order.OwnerID,
|
||||
OrderID: &orderID,
|
||||
Direction: "in",
|
||||
AmountCent: settlement.OwnerRentIncomeCent,
|
||||
BalanceType: "available",
|
||||
BizType: "owner_income",
|
||||
BizNo: order.OrderNo,
|
||||
Remark: "订单结账租金收入",
|
||||
})
|
||||
}
|
||||
if settlement.DepositCompensationCent > 0 {
|
||||
ownerEntries = append(ownerEntries, wallet.Entry{
|
||||
UserID: order.OwnerID,
|
||||
OrderID: &orderID,
|
||||
Direction: "in",
|
||||
AmountCent: settlement.DepositCompensationCent,
|
||||
BalanceType: "available",
|
||||
BizType: "deposit_compensation",
|
||||
BizNo: order.OrderNo,
|
||||
Remark: "订单结账押金赔付",
|
||||
})
|
||||
}
|
||||
if len(ownerEntries) > 0 {
|
||||
if err := wallet.AppendEntries(tx, ownerEntries...); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
var refund *refundAction
|
||||
renterRefundTotalCent := settlement.RentRefundCent + settlement.DepositRefundCent
|
||||
if renterRefundTotalCent > 0 {
|
||||
action, err := r.prepareRefund(order, renterRefundTotalCent, "checkout_refund", "结账退款原路退还")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
refund = action
|
||||
}
|
||||
|
||||
checkout.RentAmountCent = settlement.ActualRentAmountCent
|
||||
checkout.OwnerRentAmountCent = settlement.OwnerRentIncomeCent
|
||||
checkout.PlatformFeeCent = settlement.PlatformFeeCent
|
||||
checkout.RenterRefundAmountCent = settlement.RenterRefundCent
|
||||
checkout.OwnerIncomeAmountCent = settlement.OwnerIncomeCent
|
||||
if err := notification.Append(tx,
|
||||
notification.Entry{
|
||||
UserID: order.RenterID,
|
||||
Type: "settlement",
|
||||
Title: "订单已完成",
|
||||
Content: renterContent,
|
||||
BizType: "order",
|
||||
BizID: &orderID,
|
||||
},
|
||||
notification.Entry{
|
||||
UserID: order.OwnerID,
|
||||
Type: "settlement",
|
||||
Title: "订单已完成",
|
||||
Content: "订单已完成,结账金额已入账。",
|
||||
BizType: "order",
|
||||
BizID: &orderID,
|
||||
},
|
||||
); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := tx.Save(order).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := tx.Save(checkout).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := tx.Save(&listing).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := tx.Save(&account).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return refund, nil
|
||||
}
|
||||
|
||||
// 完成后的账号先下架,避免已完成订单对应的账号重新出现在公开首页。
|
||||
func archiveListingAfterCheckout(listing *model.RentalListing, account *model.GameAccount) {
|
||||
listing.Status = "offline"
|
||||
listing.InTransaction = false
|
||||
listing.PublishedAt = nil
|
||||
account.Status = "offline"
|
||||
}
|
||||
|
||||
func hasOpenCheckout(tx *gorm.DB, orderID uint64) (bool, error) {
|
||||
var count int64
|
||||
err := tx.Model(&model.OrderCheckout{}).
|
||||
Where("order_id = ? AND status IN ?", orderID, []string{"submitted", "countered", "accepted", "disputed"}).
|
||||
Count(&count).Error
|
||||
return count > 0, err
|
||||
}
|
||||
@@ -0,0 +1,115 @@
|
||||
package order
|
||||
|
||||
import (
|
||||
"hfb_sys/backend/internal/model"
|
||||
"hfb_sys/backend/internal/modules/notification"
|
||||
|
||||
"gorm.io/gorm"
|
||||
"gorm.io/gorm/clause"
|
||||
)
|
||||
|
||||
func (r *Repository) SubmitHandoff(userID uint64, orderID uint64, req SubmitHandoffRequest) (*HandoffRecordDTO, error) {
|
||||
var recordID uint64
|
||||
err := r.db.Transaction(func(tx *gorm.DB) error {
|
||||
var order model.RentalOrder
|
||||
if err := tx.Clauses(clause.Locking{Strength: "UPDATE"}).First(&order, orderID).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
if order.OwnerID != userID {
|
||||
return ErrPermissionDenied
|
||||
}
|
||||
if order.Status != "pending_handoff" || order.HandoffStatus != "pending_owner" {
|
||||
return ErrOrderCannotHandoff
|
||||
}
|
||||
record := model.HandoffRecord{
|
||||
OrderID: order.ID,
|
||||
FromUserID: order.OwnerID,
|
||||
ToUserID: order.RenterID,
|
||||
Type: "owner_handoff",
|
||||
Content: req.Content,
|
||||
}
|
||||
if err := tx.Create(&record).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
order.HandoffStatus = "pending_renter_confirm"
|
||||
orderID := order.ID
|
||||
if err := notification.Append(tx, notification.Entry{
|
||||
UserID: order.RenterID,
|
||||
Type: "handoff",
|
||||
Title: "号主已提交交接说明",
|
||||
Content: "请查看交接记录,确认账号可正常登录后点击确认收号。",
|
||||
BizType: "order",
|
||||
BizID: &orderID,
|
||||
}); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := tx.Save(&order).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
recordID = record.ID
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return r.findHandoffRecord(recordID)
|
||||
}
|
||||
|
||||
func (r *Repository) HandoffRecords(userID uint64, orderID uint64) ([]HandoffRecordDTO, error) {
|
||||
var order model.RentalOrder
|
||||
if err := r.db.Where("id = ? AND (renter_id = ? OR owner_id = ?)", orderID, userID, userID).First(&order).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var records []model.HandoffRecord
|
||||
if err := r.db.Where("order_id = ?", orderID).Order("id ASC").Find(&records).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
items := make([]HandoffRecordDTO, 0, len(records))
|
||||
for _, record := range records {
|
||||
items = append(items, toHandoffDTO(record))
|
||||
}
|
||||
return items, nil
|
||||
}
|
||||
|
||||
func (r *Repository) SubmitReturn(userID uint64, orderID uint64, req SubmitReturnRequest) (*HandoffRecordDTO, error) {
|
||||
return r.SubmitCheckout(userID, orderID, SubmitCheckoutRequest{Content: req.Content})
|
||||
}
|
||||
|
||||
func (r *Repository) HandoffRecordsAdmin(orderID uint64) ([]HandoffRecordDTO, error) {
|
||||
var order model.RentalOrder
|
||||
if err := r.db.First(&order, orderID).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var records []model.HandoffRecord
|
||||
if err := r.db.Where("order_id = ?", orderID).Order("id ASC").Find(&records).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
items := make([]HandoffRecordDTO, 0, len(records))
|
||||
for _, record := range records {
|
||||
items = append(items, toHandoffDTO(record))
|
||||
}
|
||||
return items, nil
|
||||
}
|
||||
|
||||
func (r *Repository) findHandoffRecord(id uint64) (*HandoffRecordDTO, error) {
|
||||
var record model.HandoffRecord
|
||||
if err := r.db.First(&record, id).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
dto := toHandoffDTO(record)
|
||||
return &dto, nil
|
||||
}
|
||||
|
||||
func toHandoffDTO(record model.HandoffRecord) HandoffRecordDTO {
|
||||
return HandoffRecordDTO{
|
||||
ID: record.ID,
|
||||
OrderID: record.OrderID,
|
||||
FromUserID: record.FromUserID,
|
||||
ToUserID: record.ToUserID,
|
||||
Type: record.Type,
|
||||
Content: record.Content,
|
||||
ConfirmedByRenterAt: record.ConfirmedByRenterAt,
|
||||
ConfirmedByOwnerAt: record.ConfirmedByOwnerAt,
|
||||
CreatedAt: record.CreatedAt,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,316 @@
|
||||
package order
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"hfb_sys/backend/internal/model"
|
||||
"hfb_sys/backend/internal/modules/chat"
|
||||
"hfb_sys/backend/internal/modules/notification"
|
||||
|
||||
"gorm.io/gorm"
|
||||
"gorm.io/gorm/clause"
|
||||
)
|
||||
|
||||
func (r *Repository) Create(renterID uint64, req CreateRequest) (*OrderDTO, error) {
|
||||
var createdID uint64
|
||||
err := r.db.Transaction(func(tx *gorm.DB) error {
|
||||
var listing model.RentalListing
|
||||
if err := tx.Clauses(clause.Locking{Strength: "UPDATE"}).First(&listing, req.ListingID).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
if listing.Status != "published" || listing.ReviewStatus != "approved" || listing.InTransaction {
|
||||
return ErrListingUnavailable
|
||||
}
|
||||
if listing.OwnerID == renterID {
|
||||
return ErrCannotRentOwnListing
|
||||
}
|
||||
|
||||
var account model.GameAccount
|
||||
if err := tx.Clauses(clause.Locking{Strength: "UPDATE"}).First(&account, listing.AccountID).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
snapshot, err := makeAccountSnapshot(account, listing)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
orderNo, err := newOrderNo()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
rentHours := internalOrderHours
|
||||
pricing := buildOrderPricing(listing, account)
|
||||
depositOriginalAmountCent := listing.DepositAmountCent
|
||||
paidDepositAmountCent, waivedDepositAmountCent, err := r.depositAmountsForOrder(tx, renterID, depositOriginalAmountCent)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
order := model.RentalOrder{
|
||||
OrderNo: orderNo,
|
||||
ListingID: listing.ID,
|
||||
AccountID: listing.AccountID,
|
||||
OwnerID: listing.OwnerID,
|
||||
RenterID: renterID,
|
||||
EstimatedDurationHours: rentHours,
|
||||
RentAmountCent: pricing.RentAmountCent,
|
||||
OwnerRentAmountCent: pricing.OwnerRentAmountCent,
|
||||
DepositAmountCent: paidDepositAmountCent,
|
||||
DepositOriginalAmountCent: depositOriginalAmountCent,
|
||||
DepositWaivedAmountCent: waivedDepositAmountCent,
|
||||
PlatformFeeCent: pricing.PlatformFeeCent,
|
||||
AccountSnapshot: snapshot,
|
||||
Status: "pending_payment",
|
||||
HandoffStatus: "none",
|
||||
SettlementStatus: "unsettled",
|
||||
}
|
||||
if err := tx.Create(&order).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
orderID := order.ID
|
||||
if err := notification.Append(tx,
|
||||
notification.Entry{
|
||||
UserID: order.RenterID,
|
||||
Type: "order",
|
||||
Title: "订单已创建",
|
||||
Content: "订单已创建,请在有效时间内完成支付。",
|
||||
BizType: "order",
|
||||
BizID: &orderID,
|
||||
},
|
||||
); err != nil {
|
||||
return err
|
||||
}
|
||||
listing.InTransaction = true
|
||||
if err := tx.Save(&listing).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
createdID = order.ID
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return r.FindForUser(renterID, createdID)
|
||||
}
|
||||
|
||||
func (r *Repository) depositAmountsForOrder(tx *gorm.DB, renterID uint64, originalDepositCent int64) (int64, int64, error) {
|
||||
if originalDepositCent <= 0 {
|
||||
return 0, 0, nil
|
||||
}
|
||||
var user model.User
|
||||
if err := tx.Clauses(clause.Locking{Strength: "UPDATE"}).First(&user, renterID).Error; err != nil {
|
||||
return 0, 0, err
|
||||
}
|
||||
used, err := activeDepositFreeUsed(tx, renterID)
|
||||
if err != nil {
|
||||
return 0, 0, err
|
||||
}
|
||||
paidDeposit, waivedDeposit := calculateDepositWaiver(originalDepositCent, user.DepositFreeQuotaCent, used)
|
||||
return paidDeposit, waivedDeposit, nil
|
||||
}
|
||||
|
||||
func activeDepositFreeUsed(tx *gorm.DB, renterID uint64) (int64, error) {
|
||||
var used int64
|
||||
err := tx.Model(&model.RentalOrder{}).
|
||||
Where("renter_id = ? AND status NOT IN ?",
|
||||
renterID,
|
||||
[]string{"completed", "cancelled", "closed"},
|
||||
).
|
||||
Select("COALESCE(SUM(deposit_waived_amount_cent), 0)").
|
||||
Scan(&used).Error
|
||||
return used, err
|
||||
}
|
||||
|
||||
func calculateDepositWaiver(originalDepositCent int64, quotaCent int64, usedCent int64) (int64, int64) {
|
||||
remaining := maxCent(quotaCent-usedCent, 0)
|
||||
waived := minCent(originalDepositCent, remaining)
|
||||
paid := maxCent(originalDepositCent-waived, 0)
|
||||
return paid, waived
|
||||
}
|
||||
|
||||
// Pay 保留旧接口兼容,但真实付款必须走 payment 模块的渠道支付入口。
|
||||
func (r *Repository) Pay(userID uint64, orderID uint64) error {
|
||||
return ErrChannelPaymentRequired
|
||||
}
|
||||
|
||||
// ConfirmPaidFromChannel 在乐刷确认支付后推进订单状态;租客资金不进入站内钱包。
|
||||
func (r *Repository) ConfirmPaidFromChannel(orderID uint64, providerBizNo string) error {
|
||||
var newConvID uint64
|
||||
err := r.db.Transaction(func(tx *gorm.DB) error {
|
||||
var order model.RentalOrder
|
||||
if err := tx.Clauses(clause.Locking{Strength: "UPDATE"}).First(&order, orderID).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
if order.Status == "pending_handoff" || order.Status == "renting" {
|
||||
return nil
|
||||
}
|
||||
if order.Status != "pending_payment" {
|
||||
return ErrOrderCannotPay
|
||||
}
|
||||
|
||||
var listing model.RentalListing
|
||||
if err := tx.Clauses(clause.Locking{Strength: "UPDATE"}).First(&listing, order.ListingID).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
if listing.Status != "published" || listing.ReviewStatus != "approved" || !listing.InTransaction {
|
||||
return ErrListingUnavailable
|
||||
}
|
||||
var account model.GameAccount
|
||||
if err := tx.Clauses(clause.Locking{Strength: "UPDATE"}).First(&account, order.AccountID).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// 租客已通过外部渠道付款,这里不写租客钱包流水。
|
||||
orderID := order.ID
|
||||
order.Status = "pending_handoff"
|
||||
order.HandoffStatus = "pending_owner"
|
||||
listing.Status = "rented"
|
||||
account.Status = "rented"
|
||||
conv, err := chat.EnsureOrderConversation(tx, order)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
newConvID = conv.ID
|
||||
if err := notification.Append(tx,
|
||||
notification.Entry{
|
||||
UserID: order.OwnerID,
|
||||
Type: "order",
|
||||
Title: "收到新的租号订单",
|
||||
Content: "租客已完成支付,请尽快提交交接说明。",
|
||||
BizType: "order",
|
||||
BizID: &orderID,
|
||||
},
|
||||
notification.Entry{
|
||||
UserID: order.RenterID,
|
||||
Type: "order",
|
||||
Title: "订单支付成功",
|
||||
Content: "支付已完成,等待号主提交交接说明。",
|
||||
BizType: "order",
|
||||
BizID: &orderID,
|
||||
},
|
||||
); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := tx.Save(&order).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
if err := tx.Save(&listing).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
return tx.Save(&account).Error
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if newConvID > 0 && r.chatRepo != nil {
|
||||
r.chatRepo.NotifyNewConversation(newConvID)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *Repository) Cancel(userID uint64, orderID uint64) error {
|
||||
var refund *refundAction
|
||||
err := r.db.Transaction(func(tx *gorm.DB) error {
|
||||
var order model.RentalOrder
|
||||
if err := tx.Clauses(clause.Locking{Strength: "UPDATE"}).
|
||||
Where("id = ? AND renter_id = ?", orderID, userID).
|
||||
First(&order).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
if order.Status != "pending_payment" && order.Status != "pending_handoff" {
|
||||
return ErrOrderCannotCancel
|
||||
}
|
||||
var listing model.RentalListing
|
||||
if err := tx.Clauses(clause.Locking{Strength: "UPDATE"}).First(&listing, order.ListingID).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
var account model.GameAccount
|
||||
if err := tx.Clauses(clause.Locking{Strength: "UPDATE"}).First(&account, order.AccountID).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
beforeStatus := order.Status
|
||||
order.Status = "cancelled"
|
||||
order.HandoffStatus = "cancelled"
|
||||
orderID := order.ID
|
||||
if beforeStatus == "pending_handoff" {
|
||||
totalCent := order.RentAmountCent + order.DepositAmountCent
|
||||
action, err := r.prepareRefund(&order, totalCent, "cancel_refund", "取消订单原路退款")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
refund = action
|
||||
}
|
||||
if err := notification.Append(tx,
|
||||
notification.Entry{
|
||||
UserID: order.OwnerID,
|
||||
Type: "order",
|
||||
Title: "订单已取消",
|
||||
Content: "租客已取消订单,账号已重新释放。",
|
||||
BizType: "order",
|
||||
BizID: &orderID,
|
||||
},
|
||||
notification.Entry{
|
||||
UserID: order.RenterID,
|
||||
Type: "order",
|
||||
Title: "订单取消成功",
|
||||
Content: "订单已取消,退款将原路退回您的支付账户。",
|
||||
BizType: "order",
|
||||
BizID: &orderID,
|
||||
},
|
||||
); err != nil {
|
||||
return err
|
||||
}
|
||||
listing.Status = "published"
|
||||
listing.InTransaction = false
|
||||
account.Status = "published"
|
||||
if err := tx.Save(&order).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
if err := tx.Save(&listing).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
return tx.Save(&account).Error
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
r.startRefundBestEffort(refund)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *Repository) ConfirmReceive(userID uint64, orderID uint64) error {
|
||||
return r.db.Transaction(func(tx *gorm.DB) error {
|
||||
var order model.RentalOrder
|
||||
if err := tx.Clauses(clause.Locking{Strength: "UPDATE"}).First(&order, orderID).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
if order.RenterID != userID {
|
||||
return ErrPermissionDenied
|
||||
}
|
||||
if order.Status != "pending_handoff" || order.HandoffStatus != "pending_renter_confirm" {
|
||||
return ErrOrderCannotReceive
|
||||
}
|
||||
now := time.Now()
|
||||
if err := tx.Model(&model.HandoffRecord{}).
|
||||
Where("order_id = ? AND type = ?", order.ID, "owner_handoff").
|
||||
Update("confirmed_by_renter_at", now).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
order.Status = "renting"
|
||||
order.HandoffStatus = "received"
|
||||
durationHours := orderDurationHours(order)
|
||||
order.EstimatedDurationHours = durationHours
|
||||
order.RentedAt = &now
|
||||
orderID := order.ID
|
||||
if err := notification.Append(tx, notification.Entry{
|
||||
UserID: order.OwnerID,
|
||||
Type: "handoff",
|
||||
Title: "租客已确认收号",
|
||||
Content: "订单已进入使用中。",
|
||||
BizType: "order",
|
||||
BizID: &orderID,
|
||||
}); err != nil {
|
||||
return err
|
||||
}
|
||||
return tx.Save(&order).Error
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,256 @@
|
||||
package order
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
|
||||
"hfb_sys/backend/internal/model"
|
||||
|
||||
"gorm.io/datatypes"
|
||||
)
|
||||
|
||||
func (row orderRow) toAdminDTO() OrderDTO {
|
||||
rentedAt := row.RentedAt
|
||||
durationHours := orderDurationHours(row.RentalOrder)
|
||||
rentAmountCent := row.RentAmountCent
|
||||
ownerRentAmountCent := row.OwnerRentAmountCent
|
||||
platformFeeCent := row.PlatformFeeCent
|
||||
return OrderDTO{
|
||||
ID: row.ID,
|
||||
OrderNo: row.OrderNo,
|
||||
ListingID: row.ListingID,
|
||||
ListingNo: row.ListingNo,
|
||||
AccountID: row.AccountID,
|
||||
OwnerID: row.OwnerID,
|
||||
RenterID: row.RenterID,
|
||||
OwnerPhone: row.OwnerPhone,
|
||||
RenterPhone: row.RenterPhone,
|
||||
Title: row.Title,
|
||||
ServerRegion: row.ServerRegion,
|
||||
LoginPlatform: row.LoginPlatform,
|
||||
RentedAt: rentedAt,
|
||||
EstimatedDurationHours: durationHours,
|
||||
PriceRole: "admin",
|
||||
DisplayAmountCent: row.RentAmountCent,
|
||||
RentAmountCent: &rentAmountCent,
|
||||
OwnerRentAmountCent: &ownerRentAmountCent,
|
||||
DepositAmountCent: row.DepositAmountCent,
|
||||
DepositOriginalAmountCent: effectiveDepositOriginalAmountCent(row.RentalOrder),
|
||||
DepositWaivedAmountCent: row.DepositWaivedAmountCent,
|
||||
PlatformFeeCent: &platformFeeCent,
|
||||
AccountSnapshot: row.AccountSnapshot,
|
||||
Status: row.Status,
|
||||
HandoffStatus: row.HandoffStatus,
|
||||
SettlementStatus: row.SettlementStatus,
|
||||
CreatedAt: row.CreatedAt,
|
||||
UpdatedAt: row.UpdatedAt,
|
||||
}
|
||||
}
|
||||
|
||||
func (row orderRow) toDTOForUser(userID uint64) OrderDTO {
|
||||
dto := row.toAdminDTO()
|
||||
applyOrderPriceView(&dto, row.RentalOrder, userID)
|
||||
return dto
|
||||
}
|
||||
|
||||
func effectiveDepositOriginalAmountCent(order model.RentalOrder) int64 {
|
||||
if order.DepositOriginalAmountCent > 0 {
|
||||
return order.DepositOriginalAmountCent
|
||||
}
|
||||
return order.DepositAmountCent
|
||||
}
|
||||
|
||||
func toCheckoutAdminDTO(checkout model.OrderCheckout) CheckoutDTO {
|
||||
rentAmountCent := checkout.RentAmountCent
|
||||
ownerRentAmountCent := checkout.OwnerRentAmountCent
|
||||
platformFeeCent := checkout.PlatformFeeCent
|
||||
renterRefundAmountCent := checkout.RenterRefundAmountCent
|
||||
ownerIncomeAmountCent := checkout.OwnerIncomeAmountCent
|
||||
return CheckoutDTO{
|
||||
ID: checkout.ID,
|
||||
OrderID: checkout.OrderID,
|
||||
InitiatedBy: checkout.InitiatedBy,
|
||||
Status: checkout.Status,
|
||||
PriceRole: "admin",
|
||||
DisplayAmountCent: rentAmountCent,
|
||||
RentAmountCent: &rentAmountCent,
|
||||
OwnerRentAmountCent: &ownerRentAmountCent,
|
||||
PlatformFeeCent: &platformFeeCent,
|
||||
DepositAmountCent: checkout.DepositAmountCent,
|
||||
ConsumableAmountCent: checkout.ConsumableAmountCent,
|
||||
CoinConsumedM: checkout.CoinConsumedM,
|
||||
OtherAmountCent: checkout.OtherAmountCent,
|
||||
DepositDeductAmountCent: checkout.DepositDeductAmountCent,
|
||||
RenterRefundAmountCent: &renterRefundAmountCent,
|
||||
OwnerIncomeAmountCent: &ownerIncomeAmountCent,
|
||||
Content: checkout.Content,
|
||||
EvidenceURLS: decodeStringList(checkout.EvidenceURLS),
|
||||
OwnerAdjustmentReason: checkout.OwnerAdjustmentReason,
|
||||
OwnerAdjustedAt: checkout.OwnerAdjustedAt,
|
||||
RenterConfirmedAt: checkout.RenterConfirmedAt,
|
||||
RenterRejectedAt: checkout.RenterRejectedAt,
|
||||
CreatedAt: checkout.CreatedAt,
|
||||
UpdatedAt: checkout.UpdatedAt,
|
||||
}
|
||||
}
|
||||
|
||||
func toCheckoutDTOForUser(checkout model.OrderCheckout, userID uint64, order model.RentalOrder) CheckoutDTO {
|
||||
dto := toCheckoutAdminDTO(checkout)
|
||||
applyCheckoutPriceView(&dto, order, userID)
|
||||
return dto
|
||||
}
|
||||
|
||||
func applyOrderPriceView(dto *OrderDTO, order model.RentalOrder, userID uint64) {
|
||||
if dto == nil {
|
||||
return
|
||||
}
|
||||
dto.PlatformFeeCent = nil
|
||||
switch {
|
||||
case userID == order.OwnerID:
|
||||
ownerAmountCent := order.OwnerRentAmountCent
|
||||
if ownerAmountCent <= 0 {
|
||||
ownerAmountCent = order.RentAmountCent
|
||||
}
|
||||
dto.PriceRole = "owner"
|
||||
dto.DisplayAmountCent = ownerAmountCent
|
||||
dto.RentAmountCent = nil
|
||||
dto.OwnerRentAmountCent = &ownerAmountCent
|
||||
sanitizeOrderSnapshot(&dto.AccountSnapshot, "owner")
|
||||
case userID == order.RenterID:
|
||||
rentAmountCent := order.RentAmountCent
|
||||
dto.PriceRole = "renter"
|
||||
dto.DisplayAmountCent = rentAmountCent
|
||||
dto.RentAmountCent = &rentAmountCent
|
||||
dto.OwnerRentAmountCent = nil
|
||||
sanitizeOrderSnapshot(&dto.AccountSnapshot, "renter")
|
||||
default:
|
||||
dto.PriceRole = ""
|
||||
dto.DisplayAmountCent = 0
|
||||
dto.RentAmountCent = nil
|
||||
dto.OwnerRentAmountCent = nil
|
||||
sanitizeOrderSnapshot(&dto.AccountSnapshot, "")
|
||||
}
|
||||
}
|
||||
|
||||
func applyCheckoutPriceView(dto *CheckoutDTO, order model.RentalOrder, userID uint64) {
|
||||
if dto == nil {
|
||||
return
|
||||
}
|
||||
ownerAmountCent := int64(0)
|
||||
if dto.OwnerRentAmountCent != nil {
|
||||
ownerAmountCent = *dto.OwnerRentAmountCent
|
||||
}
|
||||
ownerIncomeAmountCent := int64(0)
|
||||
if dto.OwnerIncomeAmountCent != nil {
|
||||
ownerIncomeAmountCent = *dto.OwnerIncomeAmountCent
|
||||
}
|
||||
rentAmountCent := int64(0)
|
||||
if dto.RentAmountCent != nil {
|
||||
rentAmountCent = *dto.RentAmountCent
|
||||
}
|
||||
renterRefundAmountCent := int64(0)
|
||||
if dto.RenterRefundAmountCent != nil {
|
||||
renterRefundAmountCent = *dto.RenterRefundAmountCent
|
||||
}
|
||||
dto.PlatformFeeCent = nil
|
||||
dto.RenterRefundAmountCent = nil
|
||||
dto.OwnerIncomeAmountCent = nil
|
||||
switch {
|
||||
case userID == order.OwnerID:
|
||||
dto.PriceRole = "owner"
|
||||
dto.DisplayAmountCent = ownerAmountCent
|
||||
dto.RentAmountCent = nil
|
||||
dto.OwnerRentAmountCent = &ownerAmountCent
|
||||
dto.OwnerIncomeAmountCent = &ownerIncomeAmountCent
|
||||
case userID == order.RenterID:
|
||||
dto.PriceRole = "renter"
|
||||
dto.DisplayAmountCent = rentAmountCent
|
||||
dto.RentAmountCent = &rentAmountCent
|
||||
dto.OwnerRentAmountCent = nil
|
||||
dto.RenterRefundAmountCent = &renterRefundAmountCent
|
||||
default:
|
||||
dto.PriceRole = ""
|
||||
dto.DisplayAmountCent = 0
|
||||
dto.RentAmountCent = nil
|
||||
dto.OwnerRentAmountCent = nil
|
||||
}
|
||||
}
|
||||
|
||||
func sanitizeOrderSnapshot(snapshot *datatypes.JSON, role string) {
|
||||
if snapshot == nil || len(*snapshot) == 0 {
|
||||
return
|
||||
}
|
||||
var payload map[string]any
|
||||
if err := json.Unmarshal(*snapshot, &payload); err != nil {
|
||||
return
|
||||
}
|
||||
rawSummary, ok := payload["asset_summary"]
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
var summary map[string]any
|
||||
switch typed := rawSummary.(type) {
|
||||
case map[string]any:
|
||||
summary = typed
|
||||
case string:
|
||||
if err := json.Unmarshal([]byte(typed), &summary); err != nil {
|
||||
return
|
||||
}
|
||||
default:
|
||||
raw, err := json.Marshal(typed)
|
||||
if err != nil || json.Unmarshal(raw, &summary) != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
breakdown, _ := summary["price_breakdown"].(map[string]any)
|
||||
if role == "owner" && breakdown != nil {
|
||||
if sellerRatio := readJSONNumber(breakdown["seller_ratio"]); sellerRatio > 0 {
|
||||
summary["publish_ratio"] = sellerRatio
|
||||
}
|
||||
}
|
||||
delete(summary, "price_breakdown")
|
||||
payload["asset_summary"] = summary
|
||||
raw, err := json.Marshal(payload)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
*snapshot = datatypes.JSON(raw)
|
||||
}
|
||||
|
||||
func marshalStringList(items []string) (datatypes.JSON, error) {
|
||||
if items == nil {
|
||||
items = []string{}
|
||||
}
|
||||
raw, err := json.Marshal(items)
|
||||
return datatypes.JSON(raw), err
|
||||
}
|
||||
|
||||
func decodeStringList(raw datatypes.JSON) []string {
|
||||
if len(raw) == 0 {
|
||||
return []string{}
|
||||
}
|
||||
var items []string
|
||||
if err := json.Unmarshal(raw, &items); err != nil {
|
||||
return []string{}
|
||||
}
|
||||
return items
|
||||
}
|
||||
|
||||
func makeAccountSnapshot(account model.GameAccount, listing model.RentalListing) (datatypes.JSON, error) {
|
||||
payload := map[string]any{
|
||||
"listing_id": listing.ID,
|
||||
"listing_no": listing.ListingNo,
|
||||
"account_id": account.ID,
|
||||
"title": account.Title,
|
||||
"game_name": account.GameName,
|
||||
"server_region": account.ServerRegion,
|
||||
"login_platform": account.LoginPlatform,
|
||||
"rank_level": account.RankLevel,
|
||||
"haf_coin_amount": account.HafCoinAmount,
|
||||
"asset_summary": account.AssetSummary,
|
||||
"season_tags": account.SeasonTags,
|
||||
"screenshot_urls": account.ScreenshotURLS,
|
||||
"snapshot_version": 1,
|
||||
}
|
||||
raw, err := json.Marshal(payload)
|
||||
return datatypes.JSON(raw), err
|
||||
}
|
||||
@@ -0,0 +1,251 @@
|
||||
package order
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"math"
|
||||
"strconv"
|
||||
|
||||
"hfb_sys/backend/internal/model"
|
||||
"hfb_sys/backend/pkg/money"
|
||||
|
||||
"gorm.io/datatypes"
|
||||
)
|
||||
|
||||
type orderPricing struct {
|
||||
RentAmountCent int64
|
||||
OwnerRentAmountCent int64
|
||||
PlatformFeeCent int64
|
||||
}
|
||||
|
||||
type checkoutSettlement struct {
|
||||
OwnerRentIncomeCent int64
|
||||
DepositCompensationCent int64
|
||||
OwnerIncomeCent int64
|
||||
RentRefundCent int64
|
||||
DepositRefundCent int64
|
||||
RenterRefundCent int64
|
||||
PlatformFeeCent int64
|
||||
ActualRentAmountCent int64
|
||||
}
|
||||
|
||||
func orderDurationHours(order model.RentalOrder) int {
|
||||
if order.EstimatedDurationHours > 0 {
|
||||
return order.EstimatedDurationHours
|
||||
}
|
||||
return internalOrderHours
|
||||
}
|
||||
|
||||
func buildOrderPricing(listing model.RentalListing, account model.GameAccount) orderPricing {
|
||||
rentAmountCent := listing.PriceCent
|
||||
ownerRentAmountCent := int64(math.Round(readSnapshotPrice(account.AssetSummary, "seller_total_price") * 100))
|
||||
if ownerRentAmountCent <= 0 || ownerRentAmountCent > rentAmountCent {
|
||||
ownerRentAmountCent = rentAmountCent
|
||||
}
|
||||
platformFeeCent := int64(math.Round(readSnapshotPrice(account.AssetSummary, "platform_markup_amount") * 100))
|
||||
if platformFeeCent <= 0 || ownerRentAmountCent+platformFeeCent != rentAmountCent {
|
||||
platformFeeCent = rentAmountCent - ownerRentAmountCent
|
||||
}
|
||||
if platformFeeCent < 0 {
|
||||
platformFeeCent = 0
|
||||
}
|
||||
return orderPricing{
|
||||
RentAmountCent: rentAmountCent,
|
||||
OwnerRentAmountCent: ownerRentAmountCent,
|
||||
PlatformFeeCent: platformFeeCent,
|
||||
}
|
||||
}
|
||||
|
||||
func readSnapshotPrice(raw datatypes.JSON, key string) float64 {
|
||||
return readOrderSnapshotPrice(raw, key)
|
||||
}
|
||||
|
||||
func readOrderSnapshotPrice(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
|
||||
}
|
||||
if assetSummary, ok := snapshot["asset_summary"].(map[string]any); ok {
|
||||
snapshot = assetSummary
|
||||
}
|
||||
breakdown, ok := snapshot["price_breakdown"].(map[string]any)
|
||||
if !ok {
|
||||
return 0
|
||||
}
|
||||
return readJSONNumber(breakdown[key])
|
||||
}
|
||||
|
||||
func readJSONNumber(value any) float64 {
|
||||
switch typed := value.(type) {
|
||||
case float64:
|
||||
return typed
|
||||
case float32:
|
||||
return float64(typed)
|
||||
case int:
|
||||
return float64(typed)
|
||||
case int64:
|
||||
return float64(typed)
|
||||
case json.Number:
|
||||
number, err := typed.Float64()
|
||||
if err != nil {
|
||||
return 0
|
||||
}
|
||||
return number
|
||||
case string:
|
||||
number, err := strconv.ParseFloat(typed, 64)
|
||||
if err != nil {
|
||||
return 0
|
||||
}
|
||||
return number
|
||||
default:
|
||||
return 0
|
||||
}
|
||||
}
|
||||
|
||||
func buildCheckout(order model.RentalOrder, initiatedBy uint64, status string, content string, evidenceURLS []string, consumableAmountCent int64, coinConsumedM float64, otherAmountCent int64, explicitDeductCent int64, useExplicitDeduct bool) (model.OrderCheckout, error) {
|
||||
if consumableAmountCent < 0 || coinConsumedM < 0 || otherAmountCent < 0 || explicitDeductCent < 0 {
|
||||
return model.OrderCheckout{}, ErrInvalidCheckoutAmount
|
||||
}
|
||||
deductAmountCent := otherAmountCent
|
||||
if useExplicitDeduct {
|
||||
deductAmountCent = explicitDeductCent
|
||||
}
|
||||
if deductAmountCent > order.DepositAmountCent {
|
||||
return model.OrderCheckout{}, ErrInvalidCheckoutAmount
|
||||
}
|
||||
settlement := calculateCheckoutSettlement(order, consumableAmountCent, roundQuantity(coinConsumedM), deductAmountCent)
|
||||
evidence, err := marshalStringList(evidenceURLS)
|
||||
if err != nil {
|
||||
return model.OrderCheckout{}, err
|
||||
}
|
||||
return model.OrderCheckout{
|
||||
OrderID: order.ID,
|
||||
InitiatedBy: initiatedBy,
|
||||
Status: status,
|
||||
RentAmountCent: settlement.ActualRentAmountCent,
|
||||
OwnerRentAmountCent: settlement.OwnerRentIncomeCent,
|
||||
PlatformFeeCent: settlement.PlatformFeeCent,
|
||||
DepositAmountCent: order.DepositAmountCent,
|
||||
ConsumableAmountCent: consumableAmountCent,
|
||||
CoinConsumedM: roundQuantity(coinConsumedM),
|
||||
OtherAmountCent: otherAmountCent,
|
||||
DepositDeductAmountCent: settlement.DepositCompensationCent,
|
||||
RenterRefundAmountCent: settlement.RenterRefundCent,
|
||||
OwnerIncomeAmountCent: settlement.OwnerIncomeCent,
|
||||
Content: content,
|
||||
EvidenceURLS: evidence,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func buildCheckoutSettlement(order model.RentalOrder, checkout *model.OrderCheckout) checkoutSettlement {
|
||||
return calculateCheckoutSettlement(order, checkout.ConsumableAmountCent, checkout.CoinConsumedM, checkout.DepositDeductAmountCent)
|
||||
}
|
||||
|
||||
func calculateCheckoutSettlement(order model.RentalOrder, consumableAmountCent int64, coinConsumedM float64, depositDeductAmountCent int64) checkoutSettlement {
|
||||
orderRentAmountCent := order.RentAmountCent
|
||||
orderOwnerRentAmountCent := order.OwnerRentAmountCent
|
||||
orderDepositAmountCent := order.DepositAmountCent
|
||||
|
||||
buyerCoinBasePriceCent := int64(math.Round(readOrderSnapshotPrice(order.AccountSnapshot, "buyer_coin_base_price") * 100))
|
||||
if buyerCoinBasePriceCent <= 0 || buyerCoinBasePriceCent > orderRentAmountCent {
|
||||
buyerCoinBasePriceCent = orderRentAmountCent
|
||||
}
|
||||
sellerCoinBasePriceCent := int64(math.Round(readOrderSnapshotPrice(order.AccountSnapshot, "seller_coin_base_price") * 100))
|
||||
if sellerCoinBasePriceCent <= 0 || sellerCoinBasePriceCent > orderOwnerRentAmountCent {
|
||||
sellerCoinBasePriceCent = orderOwnerRentAmountCent
|
||||
}
|
||||
prepaidConsumablePriceCent := int64(math.Round(readOrderSnapshotPrice(order.AccountSnapshot, "consumable_price") * 100))
|
||||
if prepaidConsumablePriceCent <= 0 || prepaidConsumablePriceCent > orderRentAmountCent-buyerCoinBasePriceCent {
|
||||
prepaidConsumablePriceCent = maxCent(orderRentAmountCent-buyerCoinBasePriceCent, 0)
|
||||
}
|
||||
prepaidOwnerConsumablePriceCent := maxCent(orderOwnerRentAmountCent-sellerCoinBasePriceCent, 0)
|
||||
totalCoinM := readOrderSnapshotCoinM(order.AccountSnapshot)
|
||||
coinUseRatio := 1.0
|
||||
if totalCoinM > 0 {
|
||||
coinUseRatio = minRatio(maxRatio(roundQuantity(coinConsumedM)/totalCoinM, 0), 1)
|
||||
}
|
||||
usedBuyerCoinPriceCent := int64(math.Round(float64(buyerCoinBasePriceCent) * coinUseRatio))
|
||||
usedOwnerCoinPriceCent := int64(math.Round(float64(sellerCoinBasePriceCent) * coinUseRatio))
|
||||
usedBuyerConsumablePriceCent := minCent(consumableAmountCent, prepaidConsumablePriceCent)
|
||||
consumableUseRatio := 1.0
|
||||
if prepaidConsumablePriceCent > 0 {
|
||||
consumableUseRatio = minRatio(maxRatio(float64(usedBuyerConsumablePriceCent)/float64(prepaidConsumablePriceCent), 0), 1)
|
||||
}
|
||||
usedOwnerConsumablePriceCent := int64(math.Round(float64(prepaidOwnerConsumablePriceCent) * consumableUseRatio))
|
||||
actualRentAmountCent := minCent(usedBuyerCoinPriceCent+usedBuyerConsumablePriceCent, orderRentAmountCent)
|
||||
ownerRentIncomeCent := minCent(usedOwnerCoinPriceCent+usedOwnerConsumablePriceCent, orderOwnerRentAmountCent)
|
||||
depositCompensationCent := minCent(depositDeductAmountCent, orderDepositAmountCent)
|
||||
rentRefundCent := maxCent(orderRentAmountCent-actualRentAmountCent, 0)
|
||||
depositRefundCent := maxCent(orderDepositAmountCent-depositCompensationCent, 0)
|
||||
|
||||
return checkoutSettlement{
|
||||
OwnerRentIncomeCent: ownerRentIncomeCent,
|
||||
DepositCompensationCent: depositCompensationCent,
|
||||
OwnerIncomeCent: ownerRentIncomeCent + depositCompensationCent,
|
||||
RentRefundCent: rentRefundCent,
|
||||
DepositRefundCent: depositRefundCent,
|
||||
RenterRefundCent: rentRefundCent + depositRefundCent,
|
||||
PlatformFeeCent: maxCent(actualRentAmountCent-ownerRentIncomeCent, 0),
|
||||
ActualRentAmountCent: actualRentAmountCent,
|
||||
}
|
||||
}
|
||||
|
||||
func readOrderSnapshotCoinM(raw datatypes.JSON) float64 {
|
||||
if len(raw) == 0 {
|
||||
return 0
|
||||
}
|
||||
var snapshot map[string]any
|
||||
if err := json.Unmarshal(raw, &snapshot); err != nil {
|
||||
return 0
|
||||
}
|
||||
return roundQuantity(readJSONNumber(snapshot["haf_coin_amount"]) / 1000000)
|
||||
}
|
||||
|
||||
// roundMoney 使用统一的角精度(0.1元)
|
||||
func roundMoney(value float64) float64 {
|
||||
return money.Round(value)
|
||||
}
|
||||
|
||||
func roundQuantity(value float64) float64 {
|
||||
return math.Round(value*100) / 100
|
||||
}
|
||||
|
||||
// minMoney 返回较小金额(角精度)
|
||||
func minMoney(a float64, b float64) float64 {
|
||||
return money.Min(a, b)
|
||||
}
|
||||
|
||||
// maxMoney 返回较大金额(角精度)
|
||||
func maxMoney(a float64, b float64) float64 {
|
||||
return money.Max(a, b)
|
||||
}
|
||||
|
||||
func minCent(a int64, b int64) int64 {
|
||||
if a < b {
|
||||
return a
|
||||
}
|
||||
return b
|
||||
}
|
||||
|
||||
func maxCent(a int64, b int64) int64 {
|
||||
if a > b {
|
||||
return a
|
||||
}
|
||||
return b
|
||||
}
|
||||
|
||||
func minRatio(a float64, b float64) float64 {
|
||||
if a < b {
|
||||
return a
|
||||
}
|
||||
return b
|
||||
}
|
||||
|
||||
func maxRatio(a float64, b float64) float64 {
|
||||
if a > b {
|
||||
return a
|
||||
}
|
||||
return b
|
||||
}
|
||||
@@ -0,0 +1,171 @@
|
||||
package order
|
||||
|
||||
import (
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"hfb_sys/backend/internal/model"
|
||||
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
func (r *Repository) ListForUser(userID uint64) ([]OrderDTO, error) {
|
||||
var rows []orderRow
|
||||
err := r.baseQuery().
|
||||
Where("o.renter_id = ? OR o.owner_id = ?", userID, userID).
|
||||
Order("o.id DESC").
|
||||
Scan(&rows).Error
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
items := make([]OrderDTO, 0, len(rows))
|
||||
paymentTimeoutMinutes := pendingPaymentTimeoutMinutes(r.db)
|
||||
for _, row := range rows {
|
||||
dto := row.toDTOForUser(userID)
|
||||
applyPaymentDeadline(&dto, row.RentalOrder, paymentTimeoutMinutes)
|
||||
if shouldAttachCheckout(row.Status) {
|
||||
dto.Checkout = r.latestCheckoutDTOForUser(row.ID, userID, row.RentalOrder)
|
||||
}
|
||||
items = append(items, dto)
|
||||
}
|
||||
return items, nil
|
||||
}
|
||||
|
||||
func (r *Repository) ListAdmin(page, pageSize int) (*PaginatedResult, error) {
|
||||
var total int64
|
||||
if err := r.adminQuery().Count(&total).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
offset := (page - 1) * pageSize
|
||||
var rows []orderRow
|
||||
err := r.adminQuery().
|
||||
Order("o.id DESC").
|
||||
Limit(pageSize).
|
||||
Offset(offset).
|
||||
Scan(&rows).Error
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
items := make([]OrderDTO, 0, len(rows))
|
||||
paymentTimeoutMinutes := pendingPaymentTimeoutMinutes(r.db)
|
||||
for _, row := range rows {
|
||||
dto := row.toAdminDTO()
|
||||
applyPaymentDeadline(&dto, row.RentalOrder, paymentTimeoutMinutes)
|
||||
items = append(items, dto)
|
||||
}
|
||||
|
||||
return &PaginatedResult{
|
||||
Items: items,
|
||||
Total: total,
|
||||
Page: page,
|
||||
PageSize: pageSize,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (r *Repository) FindAdmin(orderID uint64) (*OrderDTO, error) {
|
||||
var row orderRow
|
||||
if err := r.adminQuery().Where("o.id = ?", orderID).First(&row).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
dto := row.toAdminDTO()
|
||||
applyPaymentDeadline(&dto, row.RentalOrder, pendingPaymentTimeoutMinutes(r.db))
|
||||
dto.Checkout = r.latestCheckoutAdminDTO(orderID)
|
||||
return &dto, nil
|
||||
}
|
||||
|
||||
func (r *Repository) FindForUser(userID uint64, orderID uint64) (*OrderDTO, error) {
|
||||
var row orderRow
|
||||
if err := r.baseQuery().
|
||||
Where("o.id = ? AND (o.renter_id = ? OR o.owner_id = ?)", orderID, userID, userID).
|
||||
First(&row).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
dto := row.toDTOForUser(userID)
|
||||
applyPaymentDeadline(&dto, row.RentalOrder, pendingPaymentTimeoutMinutes(r.db))
|
||||
dto.Checkout = r.latestCheckoutDTOForUser(orderID, userID, row.RentalOrder)
|
||||
return &dto, nil
|
||||
}
|
||||
|
||||
func pendingPaymentTimeoutMinutes(tx *gorm.DB) int {
|
||||
var row model.SystemConfig
|
||||
err := tx.Where("`key` = ?", "order.pending_payment_timeout_minutes").First(&row).Error
|
||||
if err != nil {
|
||||
return defaultPendingPaymentTimeoutMinutes
|
||||
}
|
||||
value, err := strconv.Atoi(row.Value)
|
||||
if err != nil || value < 0 {
|
||||
return defaultPendingPaymentTimeoutMinutes
|
||||
}
|
||||
return value
|
||||
}
|
||||
|
||||
func applyPaymentDeadline(dto *OrderDTO, order model.RentalOrder, timeoutMinutes int) {
|
||||
if dto == nil || order.Status != "pending_payment" || timeoutMinutes <= 0 {
|
||||
return
|
||||
}
|
||||
deadline := order.CreatedAt.Add(time.Duration(timeoutMinutes) * time.Minute)
|
||||
dto.PaymentDeadlineAt = &deadline
|
||||
}
|
||||
|
||||
func (r *Repository) latestCheckoutAdminDTO(orderID uint64) *CheckoutDTO {
|
||||
var checkout model.OrderCheckout
|
||||
if err := r.db.Where("order_id = ?", orderID).Order("id DESC").First(&checkout).Error; err != nil {
|
||||
return nil
|
||||
}
|
||||
dto := toCheckoutAdminDTO(checkout)
|
||||
return &dto
|
||||
}
|
||||
|
||||
func shouldAttachCheckout(status string) bool {
|
||||
switch status {
|
||||
case "pending_checkout_confirm", "pending_checkout_accept", "checkout_disputing", "completed":
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
func (r *Repository) latestCheckoutDTOForUser(orderID uint64, userID uint64, order model.RentalOrder) *CheckoutDTO {
|
||||
var checkout model.OrderCheckout
|
||||
if err := r.db.Where("order_id = ?", orderID).Order("id DESC").First(&checkout).Error; err != nil {
|
||||
return nil
|
||||
}
|
||||
dto := toCheckoutDTOForUser(checkout, userID, order)
|
||||
return &dto
|
||||
}
|
||||
|
||||
func (r *Repository) findCheckout(id uint64) (*model.OrderCheckout, error) {
|
||||
var checkout model.OrderCheckout
|
||||
if err := r.db.First(&checkout, id).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &checkout, nil
|
||||
}
|
||||
|
||||
func (r *Repository) baseQuery() *gorm.DB {
|
||||
return r.db.Table("rental_orders AS o").
|
||||
Select("o.*, l.listing_no, a.title, a.server_region, a.login_platform").
|
||||
Joins("JOIN rental_listings AS l ON l.id = o.listing_id").
|
||||
Joins("JOIN game_accounts AS a ON a.id = o.account_id")
|
||||
}
|
||||
|
||||
func (r *Repository) adminQuery() *gorm.DB {
|
||||
return r.db.Table("rental_orders AS o").
|
||||
Select("o.*, l.listing_no, a.title, a.server_region, a.login_platform, owner.phone AS owner_phone, renter.phone AS renter_phone").
|
||||
Joins("JOIN rental_listings AS l ON l.id = o.listing_id").
|
||||
Joins("JOIN game_accounts AS a ON a.id = o.account_id").
|
||||
Joins("JOIN users AS owner ON owner.id = o.owner_id").
|
||||
Joins("JOIN users AS renter ON renter.id = o.renter_id")
|
||||
}
|
||||
|
||||
type orderRow struct {
|
||||
model.RentalOrder
|
||||
Title string
|
||||
ListingNo string
|
||||
ServerRegion string
|
||||
LoginPlatform string
|
||||
OwnerPhone string
|
||||
RenterPhone string
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
package order
|
||||
|
||||
import (
|
||||
"log"
|
||||
|
||||
"hfb_sys/backend/internal/model"
|
||||
)
|
||||
|
||||
func (r *Repository) prepareRefund(order *model.RentalOrder, amountCent int64, bizType string, remark string) (*refundAction, error) {
|
||||
if amountCent <= 0 {
|
||||
return nil, nil
|
||||
}
|
||||
if r.refundFunc == nil {
|
||||
return nil, ErrDependencyUnavailable
|
||||
}
|
||||
order.RefundStatus = "pending"
|
||||
order.RefundAmountCent = amountCent
|
||||
order.RefundedAt = nil
|
||||
return &refundAction{
|
||||
OrderID: order.ID,
|
||||
RefundAmountCent: amountCent,
|
||||
BizType: bizType,
|
||||
Remark: remark,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (r *Repository) startRefundBestEffort(action *refundAction) {
|
||||
if action == nil || r.refundFunc == nil {
|
||||
return
|
||||
}
|
||||
if _, err := r.refundFunc(action.OrderID, action.RefundAmountCent, action.BizType, action.Remark); err != nil {
|
||||
log.Printf("[order] start refund failed order_id=%d biz_type=%s amount_cent=%d err=%v", action.OrderID, action.BizType, action.RefundAmountCent, err)
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,55 @@
|
||||
package order
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"errors"
|
||||
"strconv"
|
||||
|
||||
"gorm.io/gorm"
|
||||
"hfb_sys/backend/internal/auditlog"
|
||||
"hfb_sys/backend/internal/timeutil"
|
||||
)
|
||||
|
||||
func firstNonEmpty(values ...string) string {
|
||||
for _, value := range values {
|
||||
if value != "" {
|
||||
return value
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func newOrderNo() (string, error) {
|
||||
// 生成格式:RO + YYYYMMDDHHMMSS + 3位随机数
|
||||
// 例如:RO20260603123456789
|
||||
now := timeutil.ShanghaiNow()
|
||||
|
||||
// 时间部分:年月日时分秒 (14位)
|
||||
timeStr := now.Format("20060102150405")
|
||||
|
||||
// 随机部分:3位数字 (000-999)
|
||||
buf := make([]byte, 2)
|
||||
if _, err := rand.Read(buf); err != nil {
|
||||
return "", err
|
||||
}
|
||||
// 将2字节转为0-999的数字
|
||||
randomNum := (int(buf[0])<<8 | int(buf[1])) % 1000
|
||||
|
||||
return "RO" + timeStr + strconv.Itoa(1000 + randomNum)[1:], nil
|
||||
}
|
||||
|
||||
func appendAuditLog(tx *gorm.DB, actorID uint64, action string, bizType string, bizID uint64, meta AuditMeta, detail map[string]any) error {
|
||||
return auditlog.Append(tx, auditlog.Entry{
|
||||
ActorType: "admin",
|
||||
ActorID: actorID,
|
||||
Action: action,
|
||||
BizType: bizType,
|
||||
BizID: &bizID,
|
||||
Meta: meta,
|
||||
Detail: detail,
|
||||
})
|
||||
}
|
||||
|
||||
func IsNotFound(err error) bool {
|
||||
return errors.Is(err, gorm.ErrRecordNotFound)
|
||||
}
|
||||
Reference in New Issue
Block a user