新增订单待支付与支付确认后端流程

This commit is contained in:
yml
2026-05-24 16:29:47 +08:00
parent 5e422bfa40
commit c4cfe6dd79
15 changed files with 368 additions and 66 deletions
+21
View File
@@ -152,6 +152,23 @@ func (h *Handler) Cancel(c *gin.Context) {
response.OK(c, gin.H{"cancelled": true})
}
func (h *Handler) Pay(c *gin.Context) {
userID, ok := currentUserID(c)
if !ok {
response.Unauthorized(c, "缺少用户上下文")
return
}
id, ok := parseID(c)
if !ok {
return
}
if err := h.service.Pay(userID, id); err != nil {
writeOrderError(c, err)
return
}
response.OK(c, gin.H{"paid": true})
}
func (h *Handler) SubmitHandoff(c *gin.Context) {
userID, ok := currentUserID(c)
if !ok {
@@ -287,6 +304,10 @@ func writeOrderError(c *gin.Context, err error) {
response.Error(c, http.StatusConflict, "listing_unavailable", "该账号暂不可租")
case errors.Is(err, ErrCannotRentOwnListing):
response.BadRequest(c, "不能租用自己发布的账号")
case errors.Is(err, ErrInsufficientBalance):
response.Error(c, http.StatusConflict, "insufficient_balance", "钱包余额不足,请先充值")
case errors.Is(err, ErrOrderCannotPay):
response.Error(c, http.StatusConflict, "order_cannot_pay", "当前订单不能支付")
case errors.Is(err, ErrOrderCannotCancel):
response.Error(c, http.StatusConflict, "order_cannot_cancel", "当前订单不能取消")
case errors.Is(err, ErrOrderCannotHandoff):
+172 -62
View File
@@ -21,6 +21,8 @@ type Repository struct {
db *gorm.DB
}
const defaultPendingPaymentTimeoutMinutes = 15
func NewRepository(db *gorm.DB) *Repository {
return &Repository{db: db}
}
@@ -32,7 +34,7 @@ func (r *Repository) Create(renterID uint64, req CreateRequest) (*OrderDTO, erro
if err := tx.Clauses(clause.Locking{Strength: "UPDATE"}).First(&listing, req.ListingID).Error; err != nil {
return err
}
if listing.Status != "published" || listing.ReviewStatus != "approved" {
if listing.Status != "published" || listing.ReviewStatus != "approved" || listing.InTransaction {
return ErrListingUnavailable
}
if listing.OwnerID == renterID {
@@ -51,72 +53,42 @@ func (r *Repository) Create(renterID uint64, req CreateRequest) (*OrderDTO, erro
if err != nil {
return err
}
now := time.Now()
rentHours := internalOrderHours
rentEnd := now.Add(time.Duration(rentHours) * time.Hour)
order := model.RentalOrder{
OrderNo: orderNo,
ListingID: listing.ID,
AccountID: listing.AccountID,
OwnerID: listing.OwnerID,
RenterID: renterID,
RentStartAt: &now,
RentEndAt: &rentEnd,
RentHours: rentHours,
RentAmount: listing.PriceHourly * float64(rentHours),
DepositAmount: listing.DepositAmount,
PlatformFee: 0,
AccountSnapshot: snapshot,
Status: "pending_handoff",
HandoffStatus: "pending_owner",
Status: "pending_payment",
HandoffStatus: "none",
SettlementStatus: "unsettled",
}
if err := tx.Create(&order).Error; err != nil {
return err
}
orderID := order.ID
if err := wallet.AppendEntries(tx,
wallet.Entry{
UserID: renterID,
OrderID: &orderID,
Direction: "in",
Amount: order.RentAmount + order.DepositAmount,
BalanceType: "frozen",
BizType: "order_lock",
BizNo: order.OrderNo,
Remark: "开发态模拟冻结订单金额和押金",
},
); err != nil {
return err
}
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: "账号已锁定,等待号主提交交接说明。",
Content: "订单已创建,请在有效时间内完成支付。",
BizType: "order",
BizID: &orderID,
},
); err != nil {
return err
}
listing.Status = "rented"
account.Status = "rented"
listing.InTransaction = true
if err := tx.Save(&listing).Error; err != nil {
return err
}
if err := tx.Save(&account).Error; err != nil {
return err
}
createdID = order.ID
return nil
})
@@ -126,6 +98,98 @@ func (r *Repository) Create(renterID uint64, req CreateRequest) (*OrderDTO, erro
return r.FindForUser(renterID, createdID)
}
func (r *Repository) Pay(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"}).
Where("id = ? AND renter_id = ?", orderID, userID).
First(&order).Error; err != nil {
return err
}
if order.Status != "pending_payment" {
return ErrOrderCannotPay
}
timeoutMinutes := pendingPaymentTimeoutMinutes(tx)
if order.CreatedAt.Before(time.Now().Add(-time.Duration(timeoutMinutes) * time.Minute)) {
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
}
total := order.RentAmount + order.DepositAmount
if err := wallet.AppendEntries(tx,
wallet.Entry{
UserID: order.RenterID,
OrderID: &order.ID,
Direction: "out",
Amount: total,
BalanceType: "available",
BizType: "order_pay",
BizNo: order.OrderNo,
Remark: "订单支付扣减可用余额",
},
wallet.Entry{
UserID: order.RenterID,
OrderID: &order.ID,
Direction: "in",
Amount: total,
BalanceType: "frozen",
BizType: "order_lock",
BizNo: order.OrderNo,
Remark: "订单支付冻结租金和押金",
},
); err != nil {
if errors.Is(err, wallet.ErrInsufficientBalance) {
return ErrInsufficientBalance
}
return err
}
order.Status = "pending_handoff"
order.HandoffStatus = "pending_owner"
listing.Status = "rented"
account.Status = "rented"
orderID := order.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
})
}
func (r *Repository) Cancel(userID uint64, orderID uint64) error {
return r.db.Transaction(func(tx *gorm.DB) error {
var order model.RentalOrder
@@ -134,7 +198,7 @@ func (r *Repository) Cancel(userID uint64, orderID uint64) error {
First(&order).Error; err != nil {
return err
}
if order.Status != "pending_handoff" {
if order.Status != "pending_payment" && order.Status != "pending_handoff" {
return ErrOrderCannotCancel
}
var listing model.RentalListing
@@ -146,29 +210,43 @@ func (r *Repository) Cancel(userID uint64, orderID uint64) error {
return err
}
beforeStatus := order.Status
order.Status = "cancelled"
order.HandoffStatus = "cancelled"
orderID := order.ID
if err := wallet.AppendEntries(tx,
wallet.Entry{
UserID: order.RenterID,
OrderID: &orderID,
Direction: "out",
Amount: order.RentAmount + order.DepositAmount,
BalanceType: "frozen",
BizType: "order_cancel",
BizNo: order.OrderNo,
Remark: "取消订单释放模拟冻结金额",
},
); err != nil {
return err
if beforeStatus == "pending_handoff" {
total := order.RentAmount + order.DepositAmount
if err := wallet.AppendEntries(tx,
wallet.Entry{
UserID: order.RenterID,
OrderID: &orderID,
Direction: "out",
Amount: total,
BalanceType: "frozen",
BizType: "order_cancel",
BizNo: order.OrderNo,
Remark: "取消订单释放冻结金额",
},
wallet.Entry{
UserID: order.RenterID,
OrderID: &orderID,
Direction: "in",
Amount: total,
BalanceType: "available",
BizType: "order_cancel_refund",
BizNo: order.OrderNo,
Remark: "取消订单退回可用余额",
},
); err != nil {
return err
}
}
if err := notification.Append(tx,
notification.Entry{
UserID: order.OwnerID,
Type: "order",
Title: "订单已取消",
Content: "租客已取消待交接订单,账号已重新释放。",
Content: "租客已取消订单,账号已重新释放。",
BizType: "order",
BizID: &orderID,
},
@@ -176,7 +254,7 @@ func (r *Repository) Cancel(userID uint64, orderID uint64) error {
UserID: order.RenterID,
Type: "order",
Title: "订单取消成功",
Content: "待交接订单已取消,模拟冻结金额已释放。",
Content: "订单已取消,相关金额已释放。",
BizType: "order",
BizID: &orderID,
},
@@ -184,6 +262,7 @@ func (r *Repository) Cancel(userID uint64, orderID uint64) error {
return err
}
listing.Status = "published"
listing.InTransaction = false
account.Status = "published"
if err := tx.Save(&order).Error; err != nil {
return err
@@ -433,6 +512,7 @@ func (r *Repository) ConfirmReturn(userID uint64, orderID uint64) error {
return err
}
listing.Status = "published"
listing.InTransaction = false
account.Status = "published"
if err := tx.Save(&order).Error; err != nil {
return err
@@ -521,18 +601,34 @@ func (r *Repository) AdminClose(adminID uint64, orderID uint64, req AdminActionR
order.SettlementStatus = "closed"
order.SettledAt = &now
listing.Status = "offline"
listing.InTransaction = false
account.Status = "offline"
if err := wallet.AppendEntries(tx, wallet.Entry{
UserID: order.RenterID,
OrderID: &order.ID,
Direction: "out",
Amount: order.RentAmount + order.DepositAmount,
BalanceType: "frozen",
BizType: "admin_order_close",
BizNo: order.OrderNo,
Remark: "后台关闭订单释放模拟冻结金额",
}); err != nil {
return err
if beforeOrderStatus != "pending_payment" {
total := order.RentAmount + order.DepositAmount
if err := wallet.AppendEntries(tx,
wallet.Entry{
UserID: order.RenterID,
OrderID: &order.ID,
Direction: "out",
Amount: total,
BalanceType: "frozen",
BizType: "admin_order_close",
BizNo: order.OrderNo,
Remark: "后台关闭订单释放冻结金额",
},
wallet.Entry{
UserID: order.RenterID,
OrderID: &order.ID,
Direction: "in",
Amount: total,
BalanceType: "available",
BizType: "admin_order_close_refund",
BizNo: order.OrderNo,
Remark: "后台关闭订单退回可用余额",
},
); err != nil {
return err
}
}
if err := notification.Append(tx,
notification.Entry{
@@ -599,6 +695,7 @@ func (r *Repository) AdminMarkAbnormal(adminID uint64, orderID uint64, req Admin
order.Status = "abnormal"
order.HandoffStatus = "admin_abnormal"
listing.Status = "abnormal"
listing.InTransaction = false
account.Status = "abnormal"
if err := notification.Append(tx,
notification.Entry{
@@ -687,6 +784,19 @@ func (r *Repository) findHandoffRecord(id uint64) (*HandoffRecordDTO, error) {
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 (r *Repository) baseQuery() *gorm.DB {
return r.db.Table("rental_orders AS o").
Select("o.*, a.title, a.server_region, a.login_platform").
+12
View File
@@ -7,6 +7,8 @@ var (
ErrInvalidRentHours = errors.New("invalid rent hours")
ErrListingUnavailable = errors.New("listing unavailable")
ErrCannotRentOwnListing = errors.New("cannot rent own listing")
ErrInsufficientBalance = errors.New("insufficient balance")
ErrOrderCannotPay = errors.New("order cannot pay")
ErrOrderCannotCancel = errors.New("order cannot cancel")
ErrOrderCannotHandoff = errors.New("order cannot handoff")
ErrOrderCannotReceive = errors.New("order cannot receive")
@@ -42,6 +44,16 @@ func (s *Service) Cancel(userID uint64, orderID uint64) error {
return s.repo.Cancel(userID, orderID)
}
func (s *Service) Pay(userID uint64, orderID uint64) error {
if s.repo == nil {
return ErrDependencyUnavailable
}
if orderID == 0 {
return ErrOrderCannotPay
}
return s.repo.Pay(userID, orderID)
}
func (s *Service) SubmitHandoff(userID uint64, orderID uint64, req SubmitHandoffRequest) (*HandoffRecordDTO, error) {
if s.repo == nil {
return nil, ErrDependencyUnavailable