新增订单待支付与支付确认后端流程
This commit is contained in:
@@ -22,6 +22,7 @@ type Job struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type thresholds struct {
|
type thresholds struct {
|
||||||
|
PendingPaymentTimeoutMinutes int
|
||||||
OwnerSubmitTimeoutMinutes int
|
OwnerSubmitTimeoutMinutes int
|
||||||
RenterConfirmTimeoutMinutes int
|
RenterConfirmTimeoutMinutes int
|
||||||
ReturnOverdueGraceMinutes int
|
ReturnOverdueGraceMinutes int
|
||||||
@@ -66,6 +67,7 @@ func (j *Job) run(ctx context.Context) {
|
|||||||
}
|
}
|
||||||
now := time.Now()
|
now := time.Now()
|
||||||
handlers := []func(context.Context, time.Time, thresholds) (int, error){
|
handlers := []func(context.Context, time.Time, thresholds) (int, error){
|
||||||
|
j.handlePendingPaymentTimeout,
|
||||||
j.handleOwnerSubmitTimeout,
|
j.handleOwnerSubmitTimeout,
|
||||||
j.handleRenterConfirmTimeout,
|
j.handleRenterConfirmTimeout,
|
||||||
j.handleReturnOverdue,
|
j.handleReturnOverdue,
|
||||||
@@ -87,6 +89,7 @@ func (j *Job) run(ctx context.Context) {
|
|||||||
|
|
||||||
func (j *Job) loadThresholds(ctx context.Context) (thresholds, error) {
|
func (j *Job) loadThresholds(ctx context.Context) (thresholds, error) {
|
||||||
cfg := thresholds{
|
cfg := thresholds{
|
||||||
|
PendingPaymentTimeoutMinutes: 15,
|
||||||
OwnerSubmitTimeoutMinutes: 30,
|
OwnerSubmitTimeoutMinutes: 30,
|
||||||
RenterConfirmTimeoutMinutes: 30,
|
RenterConfirmTimeoutMinutes: 30,
|
||||||
ReturnOverdueGraceMinutes: 10,
|
ReturnOverdueGraceMinutes: 10,
|
||||||
@@ -97,6 +100,7 @@ func (j *Job) loadThresholds(ctx context.Context) (thresholds, error) {
|
|||||||
Where("`key` IN ?", []string{
|
Where("`key` IN ?", []string{
|
||||||
"handoff.owner_submit_timeout_minutes",
|
"handoff.owner_submit_timeout_minutes",
|
||||||
"handoff.renter_confirm_timeout_minutes",
|
"handoff.renter_confirm_timeout_minutes",
|
||||||
|
"order.pending_payment_timeout_minutes",
|
||||||
"order.return_overdue_grace_minutes",
|
"order.return_overdue_grace_minutes",
|
||||||
"handoff.owner_return_confirm_timeout_minutes",
|
"handoff.owner_return_confirm_timeout_minutes",
|
||||||
}).
|
}).
|
||||||
@@ -114,6 +118,8 @@ func (j *Job) loadThresholds(ctx context.Context) (thresholds, error) {
|
|||||||
cfg.OwnerSubmitTimeoutMinutes = value
|
cfg.OwnerSubmitTimeoutMinutes = value
|
||||||
case "handoff.renter_confirm_timeout_minutes":
|
case "handoff.renter_confirm_timeout_minutes":
|
||||||
cfg.RenterConfirmTimeoutMinutes = value
|
cfg.RenterConfirmTimeoutMinutes = value
|
||||||
|
case "order.pending_payment_timeout_minutes":
|
||||||
|
cfg.PendingPaymentTimeoutMinutes = value
|
||||||
case "order.return_overdue_grace_minutes":
|
case "order.return_overdue_grace_minutes":
|
||||||
cfg.ReturnOverdueGraceMinutes = value
|
cfg.ReturnOverdueGraceMinutes = value
|
||||||
case "handoff.owner_return_confirm_timeout_minutes":
|
case "handoff.owner_return_confirm_timeout_minutes":
|
||||||
@@ -123,6 +129,65 @@ func (j *Job) loadThresholds(ctx context.Context) (thresholds, error) {
|
|||||||
return cfg, nil
|
return cfg, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (j *Job) handlePendingPaymentTimeout(ctx context.Context, now time.Time, cfg thresholds) (int, error) {
|
||||||
|
var rows []model.RentalOrder
|
||||||
|
err := j.db.WithContext(ctx).
|
||||||
|
Where("status = ? AND created_at <= ?", "pending_payment", now.Add(-time.Duration(cfg.PendingPaymentTimeoutMinutes)*time.Minute)).
|
||||||
|
Order("id ASC").
|
||||||
|
Limit(100).
|
||||||
|
Find(&rows).Error
|
||||||
|
if err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
count := 0
|
||||||
|
for _, row := range rows {
|
||||||
|
if err := j.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
|
||||||
|
var order model.RentalOrder
|
||||||
|
if err := tx.Clauses(clause.Locking{Strength: "UPDATE"}).First(&order, row.ID).Error; err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if order.Status != "pending_payment" {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
before := snapshot(&order)
|
||||||
|
var listing model.RentalListing
|
||||||
|
if err := tx.Clauses(clause.Locking{Strength: "UPDATE"}).First(&listing, order.ListingID).Error; err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
order.Status = "cancelled"
|
||||||
|
order.HandoffStatus = "cancelled"
|
||||||
|
listing.InTransaction = false
|
||||||
|
orderID := order.ID
|
||||||
|
if err := notification.Append(tx, notification.Entry{
|
||||||
|
UserID: order.RenterID,
|
||||||
|
Type: "timeout",
|
||||||
|
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 appendAuditLog(tx, "order.timeout.pending_payment", order.ID, map[string]any{
|
||||||
|
"order_id": order.ID,
|
||||||
|
"order_no": order.OrderNo,
|
||||||
|
"before": before,
|
||||||
|
"after": snapshot(&order),
|
||||||
|
})
|
||||||
|
}); err != nil {
|
||||||
|
return count, err
|
||||||
|
}
|
||||||
|
count++
|
||||||
|
}
|
||||||
|
return count, nil
|
||||||
|
}
|
||||||
|
|
||||||
func (j *Job) handleOwnerSubmitTimeout(ctx context.Context, now time.Time, cfg thresholds) (int, error) {
|
func (j *Job) handleOwnerSubmitTimeout(ctx context.Context, now time.Time, cfg thresholds) (int, error) {
|
||||||
var rows []model.RentalOrder
|
var rows []model.RentalOrder
|
||||||
err := j.db.WithContext(ctx).
|
err := j.db.WithContext(ctx).
|
||||||
|
|||||||
@@ -36,6 +36,7 @@ type RentalListing struct {
|
|||||||
PriceDaily float64 `gorm:"type:decimal(12,2);not null;default:0" json:"price_daily"`
|
PriceDaily float64 `gorm:"type:decimal(12,2);not null;default:0" json:"price_daily"`
|
||||||
PriceWeekly float64 `gorm:"type:decimal(12,2);not null;default:0" json:"price_weekly"`
|
PriceWeekly float64 `gorm:"type:decimal(12,2);not null;default:0" json:"price_weekly"`
|
||||||
DepositAmount float64 `gorm:"type:decimal(12,2);not null;default:0" json:"deposit_amount"`
|
DepositAmount float64 `gorm:"type:decimal(12,2);not null;default:0" json:"deposit_amount"`
|
||||||
|
InTransaction bool `gorm:"not null;default:false" json:"in_transaction"`
|
||||||
Status string `gorm:"size:32;not null;default:'draft'" json:"status"`
|
Status string `gorm:"size:32;not null;default:'draft'" json:"status"`
|
||||||
ReviewStatus string `gorm:"size:32;not null;default:'none'" json:"review_status"`
|
ReviewStatus string `gorm:"size:32;not null;default:'none'" json:"review_status"`
|
||||||
ReviewReason string `gorm:"size:255;not null;default:''" json:"review_reason"`
|
ReviewReason string `gorm:"size:255;not null;default:''" json:"review_reason"`
|
||||||
|
|||||||
@@ -361,7 +361,7 @@ func (r *Repository) Offline(ownerID uint64, listingID uint64) error {
|
|||||||
func (r *Repository) ListPublic() ([]ListingDTO, error) {
|
func (r *Repository) ListPublic() ([]ListingDTO, error) {
|
||||||
var rows []listingRow
|
var rows []listingRow
|
||||||
err := r.baseQuery().
|
err := r.baseQuery().
|
||||||
Where("l.status = ? AND l.review_status = ?", "published", "approved").
|
Where("l.status = ? AND l.review_status = ? AND l.in_transaction = ?", "published", "approved", false).
|
||||||
Order("l.published_at DESC, l.id DESC").
|
Order("l.published_at DESC, l.id DESC").
|
||||||
Limit(100).
|
Limit(100).
|
||||||
Scan(&rows).Error
|
Scan(&rows).Error
|
||||||
@@ -384,7 +384,7 @@ func (r *Repository) ListMine(ownerID uint64) ([]ListingDTO, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (r *Repository) FindPublic(id uint64) (*ListingDTO, error) {
|
func (r *Repository) FindPublic(id uint64) (*ListingDTO, error) {
|
||||||
dto, err := r.findDTO("l.id = ? AND l.status = ? AND l.review_status = ?", id, "published", "approved")
|
dto, err := r.findDTO("l.id = ? AND l.status = ? AND l.review_status = ? AND l.in_transaction = ?", id, "published", "approved", false)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -400,7 +400,7 @@ func (r *Repository) FindPublicScreenshotKey(id uint64, index int) (string, erro
|
|||||||
if index < 0 {
|
if index < 0 {
|
||||||
return "", gorm.ErrRecordNotFound
|
return "", gorm.ErrRecordNotFound
|
||||||
}
|
}
|
||||||
dto, err := r.findDTO("l.id = ? AND l.status = ? AND l.review_status = ?", id, "published", "approved")
|
dto, err := r.findDTO("l.id = ? AND l.status = ? AND l.review_status = ? AND l.in_transaction = ?", id, "published", "approved", false)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -152,6 +152,23 @@ func (h *Handler) Cancel(c *gin.Context) {
|
|||||||
response.OK(c, gin.H{"cancelled": true})
|
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) {
|
func (h *Handler) SubmitHandoff(c *gin.Context) {
|
||||||
userID, ok := currentUserID(c)
|
userID, ok := currentUserID(c)
|
||||||
if !ok {
|
if !ok {
|
||||||
@@ -287,6 +304,10 @@ func writeOrderError(c *gin.Context, err error) {
|
|||||||
response.Error(c, http.StatusConflict, "listing_unavailable", "该账号暂不可租")
|
response.Error(c, http.StatusConflict, "listing_unavailable", "该账号暂不可租")
|
||||||
case errors.Is(err, ErrCannotRentOwnListing):
|
case errors.Is(err, ErrCannotRentOwnListing):
|
||||||
response.BadRequest(c, "不能租用自己发布的账号")
|
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):
|
case errors.Is(err, ErrOrderCannotCancel):
|
||||||
response.Error(c, http.StatusConflict, "order_cannot_cancel", "当前订单不能取消")
|
response.Error(c, http.StatusConflict, "order_cannot_cancel", "当前订单不能取消")
|
||||||
case errors.Is(err, ErrOrderCannotHandoff):
|
case errors.Is(err, ErrOrderCannotHandoff):
|
||||||
|
|||||||
@@ -21,6 +21,8 @@ type Repository struct {
|
|||||||
db *gorm.DB
|
db *gorm.DB
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const defaultPendingPaymentTimeoutMinutes = 15
|
||||||
|
|
||||||
func NewRepository(db *gorm.DB) *Repository {
|
func NewRepository(db *gorm.DB) *Repository {
|
||||||
return &Repository{db: db}
|
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 {
|
if err := tx.Clauses(clause.Locking{Strength: "UPDATE"}).First(&listing, req.ListingID).Error; err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
if listing.Status != "published" || listing.ReviewStatus != "approved" {
|
if listing.Status != "published" || listing.ReviewStatus != "approved" || listing.InTransaction {
|
||||||
return ErrListingUnavailable
|
return ErrListingUnavailable
|
||||||
}
|
}
|
||||||
if listing.OwnerID == renterID {
|
if listing.OwnerID == renterID {
|
||||||
@@ -51,72 +53,42 @@ func (r *Repository) Create(renterID uint64, req CreateRequest) (*OrderDTO, erro
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
now := time.Now()
|
|
||||||
rentHours := internalOrderHours
|
rentHours := internalOrderHours
|
||||||
rentEnd := now.Add(time.Duration(rentHours) * time.Hour)
|
|
||||||
order := model.RentalOrder{
|
order := model.RentalOrder{
|
||||||
OrderNo: orderNo,
|
OrderNo: orderNo,
|
||||||
ListingID: listing.ID,
|
ListingID: listing.ID,
|
||||||
AccountID: listing.AccountID,
|
AccountID: listing.AccountID,
|
||||||
OwnerID: listing.OwnerID,
|
OwnerID: listing.OwnerID,
|
||||||
RenterID: renterID,
|
RenterID: renterID,
|
||||||
RentStartAt: &now,
|
|
||||||
RentEndAt: &rentEnd,
|
|
||||||
RentHours: rentHours,
|
RentHours: rentHours,
|
||||||
RentAmount: listing.PriceHourly * float64(rentHours),
|
RentAmount: listing.PriceHourly * float64(rentHours),
|
||||||
DepositAmount: listing.DepositAmount,
|
DepositAmount: listing.DepositAmount,
|
||||||
PlatformFee: 0,
|
PlatformFee: 0,
|
||||||
AccountSnapshot: snapshot,
|
AccountSnapshot: snapshot,
|
||||||
Status: "pending_handoff",
|
Status: "pending_payment",
|
||||||
HandoffStatus: "pending_owner",
|
HandoffStatus: "none",
|
||||||
SettlementStatus: "unsettled",
|
SettlementStatus: "unsettled",
|
||||||
}
|
}
|
||||||
if err := tx.Create(&order).Error; err != nil {
|
if err := tx.Create(&order).Error; err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
orderID := order.ID
|
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,
|
if err := notification.Append(tx,
|
||||||
notification.Entry{
|
|
||||||
UserID: order.OwnerID,
|
|
||||||
Type: "order",
|
|
||||||
Title: "收到新的租号订单",
|
|
||||||
Content: "租客已下单,请尽快提交交接说明。",
|
|
||||||
BizType: "order",
|
|
||||||
BizID: &orderID,
|
|
||||||
},
|
|
||||||
notification.Entry{
|
notification.Entry{
|
||||||
UserID: order.RenterID,
|
UserID: order.RenterID,
|
||||||
Type: "order",
|
Type: "order",
|
||||||
Title: "订单已创建",
|
Title: "订单已创建",
|
||||||
Content: "账号已锁定,等待号主提交交接说明。",
|
Content: "订单已创建,请在有效时间内完成支付。",
|
||||||
BizType: "order",
|
BizType: "order",
|
||||||
BizID: &orderID,
|
BizID: &orderID,
|
||||||
},
|
},
|
||||||
); err != nil {
|
); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
listing.Status = "rented"
|
listing.InTransaction = true
|
||||||
account.Status = "rented"
|
|
||||||
if err := tx.Save(&listing).Error; err != nil {
|
if err := tx.Save(&listing).Error; err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
if err := tx.Save(&account).Error; err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
createdID = order.ID
|
createdID = order.ID
|
||||||
return nil
|
return nil
|
||||||
})
|
})
|
||||||
@@ -126,6 +98,98 @@ func (r *Repository) Create(renterID uint64, req CreateRequest) (*OrderDTO, erro
|
|||||||
return r.FindForUser(renterID, createdID)
|
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 {
|
func (r *Repository) Cancel(userID uint64, orderID uint64) error {
|
||||||
return r.db.Transaction(func(tx *gorm.DB) error {
|
return r.db.Transaction(func(tx *gorm.DB) error {
|
||||||
var order model.RentalOrder
|
var order model.RentalOrder
|
||||||
@@ -134,7 +198,7 @@ func (r *Repository) Cancel(userID uint64, orderID uint64) error {
|
|||||||
First(&order).Error; err != nil {
|
First(&order).Error; err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
if order.Status != "pending_handoff" {
|
if order.Status != "pending_payment" && order.Status != "pending_handoff" {
|
||||||
return ErrOrderCannotCancel
|
return ErrOrderCannotCancel
|
||||||
}
|
}
|
||||||
var listing model.RentalListing
|
var listing model.RentalListing
|
||||||
@@ -146,29 +210,43 @@ func (r *Repository) Cancel(userID uint64, orderID uint64) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
beforeStatus := order.Status
|
||||||
order.Status = "cancelled"
|
order.Status = "cancelled"
|
||||||
order.HandoffStatus = "cancelled"
|
order.HandoffStatus = "cancelled"
|
||||||
orderID := order.ID
|
orderID := order.ID
|
||||||
|
if beforeStatus == "pending_handoff" {
|
||||||
|
total := order.RentAmount + order.DepositAmount
|
||||||
if err := wallet.AppendEntries(tx,
|
if err := wallet.AppendEntries(tx,
|
||||||
wallet.Entry{
|
wallet.Entry{
|
||||||
UserID: order.RenterID,
|
UserID: order.RenterID,
|
||||||
OrderID: &orderID,
|
OrderID: &orderID,
|
||||||
Direction: "out",
|
Direction: "out",
|
||||||
Amount: order.RentAmount + order.DepositAmount,
|
Amount: total,
|
||||||
BalanceType: "frozen",
|
BalanceType: "frozen",
|
||||||
BizType: "order_cancel",
|
BizType: "order_cancel",
|
||||||
BizNo: order.OrderNo,
|
BizNo: order.OrderNo,
|
||||||
Remark: "取消订单释放模拟冻结金额",
|
Remark: "取消订单释放冻结金额",
|
||||||
|
},
|
||||||
|
wallet.Entry{
|
||||||
|
UserID: order.RenterID,
|
||||||
|
OrderID: &orderID,
|
||||||
|
Direction: "in",
|
||||||
|
Amount: total,
|
||||||
|
BalanceType: "available",
|
||||||
|
BizType: "order_cancel_refund",
|
||||||
|
BizNo: order.OrderNo,
|
||||||
|
Remark: "取消订单退回可用余额",
|
||||||
},
|
},
|
||||||
); err != nil {
|
); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
}
|
||||||
if err := notification.Append(tx,
|
if err := notification.Append(tx,
|
||||||
notification.Entry{
|
notification.Entry{
|
||||||
UserID: order.OwnerID,
|
UserID: order.OwnerID,
|
||||||
Type: "order",
|
Type: "order",
|
||||||
Title: "订单已取消",
|
Title: "订单已取消",
|
||||||
Content: "租客已取消待交接订单,账号已重新释放。",
|
Content: "租客已取消订单,账号已重新释放。",
|
||||||
BizType: "order",
|
BizType: "order",
|
||||||
BizID: &orderID,
|
BizID: &orderID,
|
||||||
},
|
},
|
||||||
@@ -176,7 +254,7 @@ func (r *Repository) Cancel(userID uint64, orderID uint64) error {
|
|||||||
UserID: order.RenterID,
|
UserID: order.RenterID,
|
||||||
Type: "order",
|
Type: "order",
|
||||||
Title: "订单取消成功",
|
Title: "订单取消成功",
|
||||||
Content: "待交接订单已取消,模拟冻结金额已释放。",
|
Content: "订单已取消,相关金额已释放。",
|
||||||
BizType: "order",
|
BizType: "order",
|
||||||
BizID: &orderID,
|
BizID: &orderID,
|
||||||
},
|
},
|
||||||
@@ -184,6 +262,7 @@ func (r *Repository) Cancel(userID uint64, orderID uint64) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
listing.Status = "published"
|
listing.Status = "published"
|
||||||
|
listing.InTransaction = false
|
||||||
account.Status = "published"
|
account.Status = "published"
|
||||||
if err := tx.Save(&order).Error; err != nil {
|
if err := tx.Save(&order).Error; err != nil {
|
||||||
return err
|
return err
|
||||||
@@ -433,6 +512,7 @@ func (r *Repository) ConfirmReturn(userID uint64, orderID uint64) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
listing.Status = "published"
|
listing.Status = "published"
|
||||||
|
listing.InTransaction = false
|
||||||
account.Status = "published"
|
account.Status = "published"
|
||||||
if err := tx.Save(&order).Error; err != nil {
|
if err := tx.Save(&order).Error; err != nil {
|
||||||
return err
|
return err
|
||||||
@@ -521,19 +601,35 @@ func (r *Repository) AdminClose(adminID uint64, orderID uint64, req AdminActionR
|
|||||||
order.SettlementStatus = "closed"
|
order.SettlementStatus = "closed"
|
||||||
order.SettledAt = &now
|
order.SettledAt = &now
|
||||||
listing.Status = "offline"
|
listing.Status = "offline"
|
||||||
|
listing.InTransaction = false
|
||||||
account.Status = "offline"
|
account.Status = "offline"
|
||||||
if err := wallet.AppendEntries(tx, wallet.Entry{
|
if beforeOrderStatus != "pending_payment" {
|
||||||
|
total := order.RentAmount + order.DepositAmount
|
||||||
|
if err := wallet.AppendEntries(tx,
|
||||||
|
wallet.Entry{
|
||||||
UserID: order.RenterID,
|
UserID: order.RenterID,
|
||||||
OrderID: &order.ID,
|
OrderID: &order.ID,
|
||||||
Direction: "out",
|
Direction: "out",
|
||||||
Amount: order.RentAmount + order.DepositAmount,
|
Amount: total,
|
||||||
BalanceType: "frozen",
|
BalanceType: "frozen",
|
||||||
BizType: "admin_order_close",
|
BizType: "admin_order_close",
|
||||||
BizNo: order.OrderNo,
|
BizNo: order.OrderNo,
|
||||||
Remark: "后台关闭订单释放模拟冻结金额",
|
Remark: "后台关闭订单释放冻结金额",
|
||||||
}); err != nil {
|
},
|
||||||
|
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
|
return err
|
||||||
}
|
}
|
||||||
|
}
|
||||||
if err := notification.Append(tx,
|
if err := notification.Append(tx,
|
||||||
notification.Entry{
|
notification.Entry{
|
||||||
UserID: order.RenterID,
|
UserID: order.RenterID,
|
||||||
@@ -599,6 +695,7 @@ func (r *Repository) AdminMarkAbnormal(adminID uint64, orderID uint64, req Admin
|
|||||||
order.Status = "abnormal"
|
order.Status = "abnormal"
|
||||||
order.HandoffStatus = "admin_abnormal"
|
order.HandoffStatus = "admin_abnormal"
|
||||||
listing.Status = "abnormal"
|
listing.Status = "abnormal"
|
||||||
|
listing.InTransaction = false
|
||||||
account.Status = "abnormal"
|
account.Status = "abnormal"
|
||||||
if err := notification.Append(tx,
|
if err := notification.Append(tx,
|
||||||
notification.Entry{
|
notification.Entry{
|
||||||
@@ -687,6 +784,19 @@ func (r *Repository) findHandoffRecord(id uint64) (*HandoffRecordDTO, error) {
|
|||||||
return &dto, nil
|
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 {
|
func (r *Repository) baseQuery() *gorm.DB {
|
||||||
return r.db.Table("rental_orders AS o").
|
return r.db.Table("rental_orders AS o").
|
||||||
Select("o.*, a.title, a.server_region, a.login_platform").
|
Select("o.*, a.title, a.server_region, a.login_platform").
|
||||||
|
|||||||
@@ -7,6 +7,8 @@ var (
|
|||||||
ErrInvalidRentHours = errors.New("invalid rent hours")
|
ErrInvalidRentHours = errors.New("invalid rent hours")
|
||||||
ErrListingUnavailable = errors.New("listing unavailable")
|
ErrListingUnavailable = errors.New("listing unavailable")
|
||||||
ErrCannotRentOwnListing = errors.New("cannot rent own listing")
|
ErrCannotRentOwnListing = errors.New("cannot rent own listing")
|
||||||
|
ErrInsufficientBalance = errors.New("insufficient balance")
|
||||||
|
ErrOrderCannotPay = errors.New("order cannot pay")
|
||||||
ErrOrderCannotCancel = errors.New("order cannot cancel")
|
ErrOrderCannotCancel = errors.New("order cannot cancel")
|
||||||
ErrOrderCannotHandoff = errors.New("order cannot handoff")
|
ErrOrderCannotHandoff = errors.New("order cannot handoff")
|
||||||
ErrOrderCannotReceive = errors.New("order cannot receive")
|
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)
|
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) {
|
func (s *Service) SubmitHandoff(userID uint64, orderID uint64, req SubmitHandoffRequest) (*HandoffRecordDTO, error) {
|
||||||
if s.repo == nil {
|
if s.repo == nil {
|
||||||
return nil, ErrDependencyUnavailable
|
return nil, ErrDependencyUnavailable
|
||||||
|
|||||||
@@ -31,6 +31,7 @@ var defaultConfigs = []defaultConfig{
|
|||||||
{Key: "handoff.owner_submit_timeout_minutes", Value: "30", Description: "号主待交接超时分钟数"},
|
{Key: "handoff.owner_submit_timeout_minutes", Value: "30", Description: "号主待交接超时分钟数"},
|
||||||
{Key: "handoff.renter_confirm_timeout_minutes", Value: "30", Description: "租客待确认收号超时分钟数"},
|
{Key: "handoff.renter_confirm_timeout_minutes", Value: "30", Description: "租客待确认收号超时分钟数"},
|
||||||
{Key: "handoff.owner_return_confirm_timeout_minutes", Value: "120", Description: "号主待确认归还超时分钟数"},
|
{Key: "handoff.owner_return_confirm_timeout_minutes", Value: "120", Description: "号主待确认归还超时分钟数"},
|
||||||
|
{Key: "order.pending_payment_timeout_minutes", Value: "15", Description: "订单待支付超时取消分钟数"},
|
||||||
{Key: "order.return_overdue_grace_minutes", Value: "10", Description: "预计截止后归还宽限分钟数"},
|
{Key: "order.return_overdue_grace_minutes", Value: "10", Description: "预计截止后归还宽限分钟数"},
|
||||||
{Key: "deposit.min_amount", Value: "50", Description: "发布租号最低押金"},
|
{Key: "deposit.min_amount", Value: "50", Description: "发布租号最低押金"},
|
||||||
{Key: "listing.review_required", Value: "false", Description: "发布账号是否需要后台人工审核"},
|
{Key: "listing.review_required", Value: "false", Description: "发布账号是否需要后台人工审核"},
|
||||||
|
|||||||
@@ -9,6 +9,10 @@ type AccountDTO struct {
|
|||||||
Status string `json:"status"`
|
Status string `json:"status"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type RechargeRequest struct {
|
||||||
|
Amount float64 `json:"amount" binding:"required"`
|
||||||
|
}
|
||||||
|
|
||||||
type LedgerDTO struct {
|
type LedgerDTO struct {
|
||||||
ID uint64 `json:"id"`
|
ID uint64 `json:"id"`
|
||||||
LedgerNo string `json:"ledger_no"`
|
LedgerNo string `json:"ledger_no"`
|
||||||
|
|||||||
@@ -46,6 +46,25 @@ func (h *Handler) Ledger(c *gin.Context) {
|
|||||||
response.OK(c, gin.H{"items": items})
|
response.OK(c, gin.H{"items": items})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (h *Handler) Recharge(c *gin.Context) {
|
||||||
|
userID, ok := currentUserID(c)
|
||||||
|
if !ok {
|
||||||
|
response.Unauthorized(c, "缺少用户上下文")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
var req RechargeRequest
|
||||||
|
if err := c.ShouldBindJSON(&req); err != nil {
|
||||||
|
response.BadRequest(c, "充值金额不正确")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
account, err := h.service.Recharge(userID, req)
|
||||||
|
if err != nil {
|
||||||
|
writeWalletError(c, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
response.OK(c, account)
|
||||||
|
}
|
||||||
|
|
||||||
func (h *Handler) AdminLedger(c *gin.Context) {
|
func (h *Handler) AdminLedger(c *gin.Context) {
|
||||||
query, ok := parseAdminLedgerQuery(c)
|
query, ok := parseAdminLedgerQuery(c)
|
||||||
if !ok {
|
if !ok {
|
||||||
@@ -102,6 +121,10 @@ func writeWalletError(c *gin.Context, err error) {
|
|||||||
switch {
|
switch {
|
||||||
case errors.Is(err, ErrDependencyUnavailable):
|
case errors.Is(err, ErrDependencyUnavailable):
|
||||||
response.ServiceUnavailable(c, "数据库未连接")
|
response.ServiceUnavailable(c, "数据库未连接")
|
||||||
|
case errors.Is(err, ErrInvalidAmount):
|
||||||
|
response.BadRequest(c, "充值金额不正确")
|
||||||
|
case errors.Is(err, ErrInsufficientBalance):
|
||||||
|
response.Error(c, 409, "insufficient_balance", "钱包余额不足")
|
||||||
default:
|
default:
|
||||||
response.ServiceUnavailable(c, "钱包服务暂时不可用")
|
response.ServiceUnavailable(c, "钱包服务暂时不可用")
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -57,6 +57,24 @@ func (r *Repository) Ledger(userID uint64) ([]LedgerDTO, error) {
|
|||||||
return items, nil
|
return items, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (r *Repository) Recharge(userID uint64, amount float64) (*AccountDTO, error) {
|
||||||
|
err := r.db.Transaction(func(tx *gorm.DB) error {
|
||||||
|
return AppendEntries(tx, Entry{
|
||||||
|
UserID: userID,
|
||||||
|
Direction: "in",
|
||||||
|
Amount: amount,
|
||||||
|
BalanceType: "available",
|
||||||
|
BizType: "dev_recharge",
|
||||||
|
BizNo: "DEV",
|
||||||
|
Remark: "开发环境充值",
|
||||||
|
})
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return r.Account(userID)
|
||||||
|
}
|
||||||
|
|
||||||
func (r *Repository) AdminLedger(query AdminLedgerQuery) ([]AdminLedgerDTO, error) {
|
func (r *Repository) AdminLedger(query AdminLedgerQuery) ([]AdminLedgerDTO, error) {
|
||||||
limit := query.Limit
|
limit := query.Limit
|
||||||
if limit <= 0 || limit > 500 {
|
if limit <= 0 || limit > 500 {
|
||||||
@@ -146,6 +164,9 @@ func applyEntry(account *model.WalletAccount, entry Entry) (float64, error) {
|
|||||||
if entry.Direction == "in" {
|
if entry.Direction == "in" {
|
||||||
account.AvailableBalance += entry.Amount
|
account.AvailableBalance += entry.Amount
|
||||||
} else {
|
} else {
|
||||||
|
if account.AvailableBalance < entry.Amount {
|
||||||
|
return 0, ErrInsufficientBalance
|
||||||
|
}
|
||||||
account.AvailableBalance -= entry.Amount
|
account.AvailableBalance -= entry.Amount
|
||||||
}
|
}
|
||||||
return account.AvailableBalance, nil
|
return account.AvailableBalance, nil
|
||||||
@@ -153,6 +174,9 @@ func applyEntry(account *model.WalletAccount, entry Entry) (float64, error) {
|
|||||||
if entry.Direction == "in" {
|
if entry.Direction == "in" {
|
||||||
account.FrozenBalance += entry.Amount
|
account.FrozenBalance += entry.Amount
|
||||||
} else {
|
} else {
|
||||||
|
if account.FrozenBalance < entry.Amount {
|
||||||
|
return 0, ErrInsufficientBalance
|
||||||
|
}
|
||||||
account.FrozenBalance -= entry.Amount
|
account.FrozenBalance -= entry.Amount
|
||||||
}
|
}
|
||||||
return account.FrozenBalance, nil
|
return account.FrozenBalance, nil
|
||||||
|
|||||||
@@ -2,7 +2,11 @@ package wallet
|
|||||||
|
|
||||||
import "errors"
|
import "errors"
|
||||||
|
|
||||||
var ErrDependencyUnavailable = errors.New("dependency unavailable")
|
var (
|
||||||
|
ErrDependencyUnavailable = errors.New("dependency unavailable")
|
||||||
|
ErrInvalidAmount = errors.New("invalid amount")
|
||||||
|
ErrInsufficientBalance = errors.New("insufficient balance")
|
||||||
|
)
|
||||||
|
|
||||||
type Service struct {
|
type Service struct {
|
||||||
repo *Repository
|
repo *Repository
|
||||||
@@ -26,6 +30,16 @@ func (s *Service) Ledger(userID uint64) ([]LedgerDTO, error) {
|
|||||||
return s.repo.Ledger(userID)
|
return s.repo.Ledger(userID)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *Service) Recharge(userID uint64, req RechargeRequest) (*AccountDTO, error) {
|
||||||
|
if s.repo == nil {
|
||||||
|
return nil, ErrDependencyUnavailable
|
||||||
|
}
|
||||||
|
if req.Amount <= 0 {
|
||||||
|
return nil, ErrInvalidAmount
|
||||||
|
}
|
||||||
|
return s.repo.Recharge(userID, req.Amount)
|
||||||
|
}
|
||||||
|
|
||||||
func (s *Service) AdminLedger(query AdminLedgerQuery) ([]AdminLedgerDTO, error) {
|
func (s *Service) AdminLedger(query AdminLedgerQuery) ([]AdminLedgerDTO, error) {
|
||||||
if s.repo == nil {
|
if s.repo == nil {
|
||||||
return nil, ErrDependencyUnavailable
|
return nil, ErrDependencyUnavailable
|
||||||
|
|||||||
@@ -166,6 +166,7 @@ func New(cfg config.Config, deps Dependencies, logger *zap.Logger) *gin.Engine {
|
|||||||
orderRoutes.POST("", orderHandler.Create)
|
orderRoutes.POST("", orderHandler.Create)
|
||||||
orderRoutes.GET("", orderHandler.List)
|
orderRoutes.GET("", orderHandler.List)
|
||||||
orderRoutes.GET("/:id", orderHandler.Detail)
|
orderRoutes.GET("/:id", orderHandler.Detail)
|
||||||
|
orderRoutes.POST("/:id/pay", orderHandler.Pay)
|
||||||
orderRoutes.POST("/:id/cancel", orderHandler.Cancel)
|
orderRoutes.POST("/:id/cancel", orderHandler.Cancel)
|
||||||
orderRoutes.POST("/:id/handoff", orderHandler.SubmitHandoff)
|
orderRoutes.POST("/:id/handoff", orderHandler.SubmitHandoff)
|
||||||
orderRoutes.GET("/:id/handoff-records", orderHandler.HandoffRecords)
|
orderRoutes.GET("/:id/handoff-records", orderHandler.HandoffRecords)
|
||||||
@@ -185,6 +186,7 @@ func New(cfg config.Config, deps Dependencies, logger *zap.Logger) *gin.Engine {
|
|||||||
{
|
{
|
||||||
walletRoutes.GET("/balance", walletHandler.Balance)
|
walletRoutes.GET("/balance", walletHandler.Balance)
|
||||||
walletRoutes.GET("/ledger", walletHandler.Ledger)
|
walletRoutes.GET("/ledger", walletHandler.Ledger)
|
||||||
|
walletRoutes.POST("/recharge", walletHandler.Recharge)
|
||||||
}
|
}
|
||||||
|
|
||||||
fileRoutes := api.Group("/files", requireAuth)
|
fileRoutes := api.Group("/files", requireAuth)
|
||||||
|
|||||||
@@ -57,6 +57,7 @@ CREATE TABLE rental_listings (
|
|||||||
price_daily DECIMAL(12,2) NOT NULL DEFAULT 0.00,
|
price_daily DECIMAL(12,2) NOT NULL DEFAULT 0.00,
|
||||||
price_weekly DECIMAL(12,2) NOT NULL DEFAULT 0.00,
|
price_weekly DECIMAL(12,2) NOT NULL DEFAULT 0.00,
|
||||||
deposit_amount DECIMAL(12,2) NOT NULL DEFAULT 0.00,
|
deposit_amount DECIMAL(12,2) NOT NULL DEFAULT 0.00,
|
||||||
|
in_transaction TINYINT(1) NOT NULL DEFAULT 0,
|
||||||
status VARCHAR(32) NOT NULL DEFAULT 'draft',
|
status VARCHAR(32) NOT NULL DEFAULT 'draft',
|
||||||
review_status VARCHAR(32) NOT NULL DEFAULT 'none',
|
review_status VARCHAR(32) NOT NULL DEFAULT 'none',
|
||||||
review_reason VARCHAR(255) NOT NULL DEFAULT '',
|
review_reason VARCHAR(255) NOT NULL DEFAULT '',
|
||||||
|
|||||||
@@ -0,0 +1,20 @@
|
|||||||
|
SET @listing_in_transaction_exists := (
|
||||||
|
SELECT COUNT(*)
|
||||||
|
FROM information_schema.columns
|
||||||
|
WHERE table_schema = DATABASE()
|
||||||
|
AND table_name = 'rental_listings'
|
||||||
|
AND column_name = 'in_transaction'
|
||||||
|
);
|
||||||
|
|
||||||
|
SET @add_listing_in_transaction_sql := IF(
|
||||||
|
@listing_in_transaction_exists = 0,
|
||||||
|
'ALTER TABLE rental_listings ADD COLUMN in_transaction TINYINT(1) NOT NULL DEFAULT 0 AFTER deposit_amount',
|
||||||
|
'SELECT 1'
|
||||||
|
);
|
||||||
|
PREPARE add_listing_in_transaction_stmt FROM @add_listing_in_transaction_sql;
|
||||||
|
EXECUTE add_listing_in_transaction_stmt;
|
||||||
|
DEALLOCATE PREPARE add_listing_in_transaction_stmt;
|
||||||
|
|
||||||
|
INSERT INTO system_configs (`key`, `value`, description)
|
||||||
|
VALUES ('order.pending_payment_timeout_minutes', '15', '订单待支付超时取消分钟数')
|
||||||
|
ON DUPLICATE KEY UPDATE `key` = VALUES(`key`);
|
||||||
@@ -70,6 +70,10 @@ init_database() {
|
|||||||
log "数据库结构已存在,跳过迁移"
|
log "数据库结构已存在,跳过迁移"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
if [[ -f "${ROOT_DIR}/backend/migrations/000002_order_payment_stepwise.sql" ]]; then
|
||||||
|
log "应用订单支付增量迁移..."
|
||||||
|
docker exec -i hfb-mysql mysql -uhfb -psecret hfb_sys < "${ROOT_DIR}/backend/migrations/000002_order_payment_stepwise.sql"
|
||||||
|
fi
|
||||||
}
|
}
|
||||||
|
|
||||||
load_env_file() {
|
load_env_file() {
|
||||||
|
|||||||
Reference in New Issue
Block a user