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

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
+65
View File
@@ -22,6 +22,7 @@ type Job struct {
}
type thresholds struct {
PendingPaymentTimeoutMinutes int
OwnerSubmitTimeoutMinutes int
RenterConfirmTimeoutMinutes int
ReturnOverdueGraceMinutes int
@@ -66,6 +67,7 @@ func (j *Job) run(ctx context.Context) {
}
now := time.Now()
handlers := []func(context.Context, time.Time, thresholds) (int, error){
j.handlePendingPaymentTimeout,
j.handleOwnerSubmitTimeout,
j.handleRenterConfirmTimeout,
j.handleReturnOverdue,
@@ -87,6 +89,7 @@ func (j *Job) run(ctx context.Context) {
func (j *Job) loadThresholds(ctx context.Context) (thresholds, error) {
cfg := thresholds{
PendingPaymentTimeoutMinutes: 15,
OwnerSubmitTimeoutMinutes: 30,
RenterConfirmTimeoutMinutes: 30,
ReturnOverdueGraceMinutes: 10,
@@ -97,6 +100,7 @@ func (j *Job) loadThresholds(ctx context.Context) (thresholds, error) {
Where("`key` IN ?", []string{
"handoff.owner_submit_timeout_minutes",
"handoff.renter_confirm_timeout_minutes",
"order.pending_payment_timeout_minutes",
"order.return_overdue_grace_minutes",
"handoff.owner_return_confirm_timeout_minutes",
}).
@@ -114,6 +118,8 @@ func (j *Job) loadThresholds(ctx context.Context) (thresholds, error) {
cfg.OwnerSubmitTimeoutMinutes = value
case "handoff.renter_confirm_timeout_minutes":
cfg.RenterConfirmTimeoutMinutes = value
case "order.pending_payment_timeout_minutes":
cfg.PendingPaymentTimeoutMinutes = value
case "order.return_overdue_grace_minutes":
cfg.ReturnOverdueGraceMinutes = value
case "handoff.owner_return_confirm_timeout_minutes":
@@ -123,6 +129,65 @@ func (j *Job) loadThresholds(ctx context.Context) (thresholds, error) {
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) {
var rows []model.RentalOrder
err := j.db.WithContext(ctx).
+1
View File
@@ -36,6 +36,7 @@ type RentalListing struct {
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"`
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"`
ReviewStatus string `gorm:"size:32;not null;default:'none'" json:"review_status"`
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) {
var rows []listingRow
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").
Limit(100).
Scan(&rows).Error
@@ -384,7 +384,7 @@ func (r *Repository) ListMine(ownerID 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 {
return nil, err
}
@@ -400,7 +400,7 @@ func (r *Repository) FindPublicScreenshotKey(id uint64, index int) (string, erro
if index < 0 {
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 {
return "", err
}
+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
@@ -31,6 +31,7 @@ var defaultConfigs = []defaultConfig{
{Key: "handoff.owner_submit_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: "order.pending_payment_timeout_minutes", Value: "15", Description: "订单待支付超时取消分钟数"},
{Key: "order.return_overdue_grace_minutes", Value: "10", Description: "预计截止后归还宽限分钟数"},
{Key: "deposit.min_amount", Value: "50", Description: "发布租号最低押金"},
{Key: "listing.review_required", Value: "false", Description: "发布账号是否需要后台人工审核"},
+4
View File
@@ -9,6 +9,10 @@ type AccountDTO struct {
Status string `json:"status"`
}
type RechargeRequest struct {
Amount float64 `json:"amount" binding:"required"`
}
type LedgerDTO struct {
ID uint64 `json:"id"`
LedgerNo string `json:"ledger_no"`
@@ -46,6 +46,25 @@ func (h *Handler) Ledger(c *gin.Context) {
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) {
query, ok := parseAdminLedgerQuery(c)
if !ok {
@@ -102,6 +121,10 @@ func writeWalletError(c *gin.Context, err error) {
switch {
case errors.Is(err, ErrDependencyUnavailable):
response.ServiceUnavailable(c, "数据库未连接")
case errors.Is(err, ErrInvalidAmount):
response.BadRequest(c, "充值金额不正确")
case errors.Is(err, ErrInsufficientBalance):
response.Error(c, 409, "insufficient_balance", "钱包余额不足")
default:
response.ServiceUnavailable(c, "钱包服务暂时不可用")
}
@@ -57,6 +57,24 @@ func (r *Repository) Ledger(userID uint64) ([]LedgerDTO, error) {
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) {
limit := query.Limit
if limit <= 0 || limit > 500 {
@@ -146,6 +164,9 @@ func applyEntry(account *model.WalletAccount, entry Entry) (float64, error) {
if entry.Direction == "in" {
account.AvailableBalance += entry.Amount
} else {
if account.AvailableBalance < entry.Amount {
return 0, ErrInsufficientBalance
}
account.AvailableBalance -= entry.Amount
}
return account.AvailableBalance, nil
@@ -153,6 +174,9 @@ func applyEntry(account *model.WalletAccount, entry Entry) (float64, error) {
if entry.Direction == "in" {
account.FrozenBalance += entry.Amount
} else {
if account.FrozenBalance < entry.Amount {
return 0, ErrInsufficientBalance
}
account.FrozenBalance -= entry.Amount
}
return account.FrozenBalance, nil
+15 -1
View File
@@ -2,7 +2,11 @@ package wallet
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 {
repo *Repository
@@ -26,6 +30,16 @@ func (s *Service) Ledger(userID uint64) ([]LedgerDTO, error) {
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) {
if s.repo == nil {
return nil, ErrDependencyUnavailable
+2
View File
@@ -166,6 +166,7 @@ func New(cfg config.Config, deps Dependencies, logger *zap.Logger) *gin.Engine {
orderRoutes.POST("", orderHandler.Create)
orderRoutes.GET("", orderHandler.List)
orderRoutes.GET("/:id", orderHandler.Detail)
orderRoutes.POST("/:id/pay", orderHandler.Pay)
orderRoutes.POST("/:id/cancel", orderHandler.Cancel)
orderRoutes.POST("/:id/handoff", orderHandler.SubmitHandoff)
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("/ledger", walletHandler.Ledger)
walletRoutes.POST("/recharge", walletHandler.Recharge)
}
fileRoutes := api.Group("/files", requireAuth)
+1
View File
@@ -57,6 +57,7 @@ CREATE TABLE rental_listings (
price_daily 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,
in_transaction TINYINT(1) NOT NULL DEFAULT 0,
status VARCHAR(32) NOT NULL DEFAULT 'draft',
review_status VARCHAR(32) NOT NULL DEFAULT 'none',
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`);