实现结账流程:替换旧归还逻辑为完整结账-修正-争议流程
- 新增 order_checkouts 表,支持结账明细(消耗/押金扣除/退回/号主入账) - 租客发起结账 → 号主确认 → 已完成(正常路径) - 号主修改结账 → 租客确认修正 → 已完成(修正路径) - 结账阶段任一方发起争议 → 后台仲裁 → 已完成/已关闭/异常(争议路径) - 争议模块适配结账争议类型,仲裁结果新增 mark_abnormal - 超时任务适配新的 pending_checkout_confirm 状态 - 前端结账明细面板、发起结账/确认/修正/拒绝表单全部实现 - 移除旧的 SubmitReturn/ConfirmReturn 接口
This commit is contained in:
@@ -253,16 +253,16 @@ func (j *Job) handleReturnOverdue(ctx context.Context, now time.Time, cfg thresh
|
||||
notification.Entry{
|
||||
UserID: order.RenterID,
|
||||
Type: "timeout",
|
||||
Title: "订单已逾期未归还",
|
||||
Content: "订单已超过预计截止时间,请尽快提交归还说明,避免进入申诉处理。",
|
||||
Title: "订单已逾期未结账",
|
||||
Content: "订单已超过预计截止时间,请尽快发起结账,避免进入申诉处理。",
|
||||
BizType: "order",
|
||||
BizID: &orderID,
|
||||
},
|
||||
notification.Entry{
|
||||
UserID: order.OwnerID,
|
||||
Type: "timeout",
|
||||
Title: "租客逾期未归还",
|
||||
Content: "租客未在预计截止后及时归还,你可以发起申诉或等待客服处理。",
|
||||
Title: "租客逾期未结账",
|
||||
Content: "租客未在预计截止后及时发起结账,你可以发起申诉或等待客服处理。",
|
||||
BizType: "order",
|
||||
BizID: &orderID,
|
||||
},
|
||||
@@ -281,7 +281,7 @@ func (j *Job) handleReturnOverdue(ctx context.Context, now time.Time, cfg thresh
|
||||
func (j *Job) handleOwnerReturnConfirmTimeout(ctx context.Context, now time.Time, cfg thresholds) (int, error) {
|
||||
var rows []model.RentalOrder
|
||||
err := j.db.WithContext(ctx).
|
||||
Where("status = ? AND handoff_status = ? AND updated_at <= ?", "pending_return_confirm", "pending_owner_return_confirm", now.Add(-time.Duration(cfg.OwnerReturnConfirmTimeoutMinutes)*time.Minute)).
|
||||
Where("status = ? AND handoff_status = ? AND updated_at <= ?", "pending_checkout_confirm", "pending_owner_checkout", now.Add(-time.Duration(cfg.OwnerReturnConfirmTimeoutMinutes)*time.Minute)).
|
||||
Order("id ASC").
|
||||
Limit(100).
|
||||
Find(&rows).Error
|
||||
@@ -290,28 +290,28 @@ func (j *Job) handleOwnerReturnConfirmTimeout(ctx context.Context, now time.Time
|
||||
}
|
||||
count := 0
|
||||
for _, row := range rows {
|
||||
if err := j.updateOrder(ctx, row.ID, "order.timeout.owner_return_confirm", func(tx *gorm.DB, order *model.RentalOrder) (string, error) {
|
||||
if order.Status != "pending_return_confirm" || order.HandoffStatus != "pending_owner_return_confirm" {
|
||||
if err := j.updateOrder(ctx, row.ID, "order.timeout.owner_checkout_confirm", func(tx *gorm.DB, order *model.RentalOrder) (string, error) {
|
||||
if order.Status != "pending_checkout_confirm" || order.HandoffStatus != "pending_owner_checkout" {
|
||||
return "", nil
|
||||
}
|
||||
before := snapshot(order)
|
||||
order.Status = "abnormal"
|
||||
order.HandoffStatus = "owner_return_confirm_timeout"
|
||||
order.HandoffStatus = "owner_checkout_confirm_timeout"
|
||||
orderID := order.ID
|
||||
if err := notification.Append(tx,
|
||||
notification.Entry{
|
||||
UserID: order.RenterID,
|
||||
Type: "timeout",
|
||||
Title: "号主确认归还超时",
|
||||
Content: "号主未在规定时间内确认归还,订单已进入客服复核状态。",
|
||||
Title: "号主确认结账超时",
|
||||
Content: "号主未在规定时间内确认结账,订单已进入客服复核状态。",
|
||||
BizType: "order",
|
||||
BizID: &orderID,
|
||||
},
|
||||
notification.Entry{
|
||||
UserID: order.OwnerID,
|
||||
Type: "timeout",
|
||||
Title: "确认归还已超时",
|
||||
Content: "你未在规定时间内确认归还,订单已进入客服复核状态。",
|
||||
Title: "确认结账已超时",
|
||||
Content: "你未在规定时间内确认结账,订单已进入客服复核状态。",
|
||||
BizType: "order",
|
||||
BizID: &orderID,
|
||||
},
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
package model
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"gorm.io/datatypes"
|
||||
)
|
||||
|
||||
type OrderCheckout struct {
|
||||
ID uint64 `gorm:"primaryKey" json:"id"`
|
||||
OrderID uint64 `gorm:"not null;index" json:"order_id"`
|
||||
InitiatedBy uint64 `gorm:"not null" json:"initiated_by"`
|
||||
Status string `gorm:"size:32;not null;default:'submitted'" json:"status"`
|
||||
RentAmount float64 `gorm:"type:decimal(12,2);not null;default:0" json:"rent_amount"`
|
||||
DepositAmount float64 `gorm:"type:decimal(12,2);not null;default:0" json:"deposit_amount"`
|
||||
ConsumableAmount float64 `gorm:"type:decimal(12,2);not null;default:0" json:"consumable_amount"`
|
||||
CoinConsumedM float64 `gorm:"type:decimal(12,2);not null;default:0" json:"coin_consumed_m"`
|
||||
OtherAmount float64 `gorm:"type:decimal(12,2);not null;default:0" json:"other_amount"`
|
||||
DepositDeductAmount float64 `gorm:"type:decimal(12,2);not null;default:0" json:"deposit_deduct_amount"`
|
||||
RenterRefundAmount float64 `gorm:"type:decimal(12,2);not null;default:0" json:"renter_refund_amount"`
|
||||
OwnerIncomeAmount float64 `gorm:"type:decimal(12,2);not null;default:0" json:"owner_income_amount"`
|
||||
Content string `json:"content"`
|
||||
EvidenceURLS datatypes.JSON `json:"evidence_urls"`
|
||||
OwnerAdjustmentReason string `json:"owner_adjustment_reason"`
|
||||
OwnerAdjustedAt *time.Time `json:"owner_adjusted_at"`
|
||||
RenterConfirmedAt *time.Time `json:"renter_confirmed_at"`
|
||||
RenterRejectedAt *time.Time `json:"renter_rejected_at"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
}
|
||||
|
||||
func (OrderCheckout) TableName() string {
|
||||
return "order_checkouts"
|
||||
}
|
||||
@@ -58,7 +58,7 @@ func (r *Repository) Summary() (*DashboardDTO, error) {
|
||||
if err := r.db.Model(&model.RentalOrder{}).Where("status = ? AND handoff_status IN ?", "pending_handoff", []string{"pending_owner", "pending_renter_confirm"}).Count(&pending.PendingHandoffs).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := r.db.Model(&model.RentalOrder{}).Where("status = ?", "pending_return_confirm").Count(&pending.PendingReturnConfirms).Error; err != nil {
|
||||
if err := r.db.Model(&model.RentalOrder{}).Where("status IN ?", []string{"pending_checkout_confirm", "pending_checkout_accept"}).Count(&pending.PendingReturnConfirms).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
recentOrders, err := r.recentOrders()
|
||||
|
||||
@@ -35,6 +35,7 @@ func (r *Repository) Create(userID uint64, orderID uint64, req CreateRequest) (*
|
||||
if order.Status == "completed" || order.Status == "cancelled" || order.Status == "closed" {
|
||||
return ErrInvalidDispute
|
||||
}
|
||||
isCheckoutDispute := order.Status == "pending_checkout_confirm" || order.Status == "pending_checkout_accept"
|
||||
var count int64
|
||||
if err := tx.Model(&model.Dispute{}).
|
||||
Where("order_id = ? AND status IN ?", order.ID, []string{"open", "processing"}).
|
||||
@@ -57,7 +58,7 @@ func (r *Repository) Create(userID uint64, orderID uint64, req CreateRequest) (*
|
||||
OrderID: order.ID,
|
||||
InitiatorID: userID,
|
||||
TargetUserID: targetID,
|
||||
Type: req.Type,
|
||||
Type: disputeType(req.Type, isCheckoutDispute),
|
||||
Status: "open",
|
||||
Description: req.Description,
|
||||
EvidenceURLS: evidence,
|
||||
@@ -65,17 +66,42 @@ func (r *Repository) Create(userID uint64, orderID uint64, req CreateRequest) (*
|
||||
if err := tx.Create(&row).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
order.Status = "disputing"
|
||||
if isCheckoutDispute {
|
||||
now := time.Now()
|
||||
order.Status = "checkout_disputing"
|
||||
order.HandoffStatus = "checkout_disputed"
|
||||
order.SettlementStatus = "disputed"
|
||||
updates := map[string]any{
|
||||
"status": "disputed",
|
||||
"updated_at": now,
|
||||
}
|
||||
if userID == order.RenterID {
|
||||
updates["renter_rejected_at"] = now
|
||||
}
|
||||
if err := tx.Model(&model.OrderCheckout{}).
|
||||
Where("order_id = ? AND status IN ?", order.ID, []string{"submitted", "countered"}).
|
||||
Updates(updates).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
order.Status = "disputing"
|
||||
}
|
||||
if err := tx.Save(&order).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
disputeID := row.ID
|
||||
title := "订单进入申诉"
|
||||
content := "对方已发起申诉,请等待客服仲裁或补充沟通记录。"
|
||||
if isCheckoutDispute {
|
||||
title = "订单进入结账争议"
|
||||
content = "对方已发起结账争议,请等待客服仲裁或补充结账证据。"
|
||||
}
|
||||
if err := notification.Append(tx,
|
||||
notification.Entry{
|
||||
UserID: targetID,
|
||||
Type: "dispute",
|
||||
Title: "订单进入申诉",
|
||||
Content: "对方已发起申诉,请等待客服仲裁或补充沟通记录。",
|
||||
Title: title,
|
||||
Content: content,
|
||||
BizType: "dispute",
|
||||
BizID: &disputeID,
|
||||
},
|
||||
@@ -177,6 +203,9 @@ func (r *Repository) Arbitrate(adminID uint64, id uint64, req ArbitrateRequest,
|
||||
if req.Result == "order_close" {
|
||||
listing.Status = "offline"
|
||||
account.Status = "offline"
|
||||
} else if req.Result == "mark_abnormal" {
|
||||
listing.Status = "abnormal"
|
||||
account.Status = "abnormal"
|
||||
} else {
|
||||
listing.Status = "published"
|
||||
account.Status = "published"
|
||||
@@ -335,6 +364,8 @@ func buildArbitrationSettlement(order model.RentalOrder, req ArbitrateRequest) (
|
||||
addRenterRefund(order.DepositAmount-deductAmount, "仲裁退回剩余押金给租客")
|
||||
case "order_close":
|
||||
// Only release frozen funds. No available-balance settlement happens in development mode.
|
||||
case "mark_abnormal":
|
||||
// 标记异常只释放冻结账务,后续由客服继续线下复核。
|
||||
default:
|
||||
return settlement, ErrInvalidDispute
|
||||
}
|
||||
@@ -395,11 +426,20 @@ func arbitrateOrderStatus(result string) string {
|
||||
switch result {
|
||||
case "full_refund", "partial_refund", "order_close":
|
||||
return "closed"
|
||||
case "mark_abnormal":
|
||||
return "abnormal"
|
||||
default:
|
||||
return "completed"
|
||||
}
|
||||
}
|
||||
|
||||
func disputeType(input string, isCheckoutDispute bool) string {
|
||||
if isCheckoutDispute {
|
||||
return "checkout_dispute"
|
||||
}
|
||||
return input
|
||||
}
|
||||
|
||||
func appendAuditLog(tx *gorm.DB, actorID uint64, action string, bizType string, bizID uint64, meta AuditMeta, detail map[string]any) error {
|
||||
raw, err := json.Marshal(detail)
|
||||
if err != nil {
|
||||
|
||||
@@ -27,6 +27,7 @@ type OrderDTO struct {
|
||||
Status string `json:"status"`
|
||||
HandoffStatus string `json:"handoff_status"`
|
||||
SettlementStatus string `json:"settlement_status"`
|
||||
Checkout *CheckoutDTO `json:"checkout,omitempty"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
}
|
||||
@@ -39,8 +40,21 @@ type SubmitHandoffRequest struct {
|
||||
Content string `json:"content" binding:"required"`
|
||||
}
|
||||
|
||||
type SubmitReturnRequest struct {
|
||||
Content string `json:"content" binding:"required"`
|
||||
type SubmitCheckoutRequest struct {
|
||||
Content string `json:"content" binding:"required"`
|
||||
ConsumableAmount float64 `json:"consumable_amount"`
|
||||
CoinConsumedM float64 `json:"coin_consumed_m"`
|
||||
OtherAmount float64 `json:"other_amount"`
|
||||
EvidenceURLS []string `json:"evidence_urls"`
|
||||
}
|
||||
|
||||
type CounterCheckoutRequest struct {
|
||||
ConsumableAmount float64 `json:"consumable_amount"`
|
||||
CoinConsumedM float64 `json:"coin_consumed_m"`
|
||||
OtherAmount float64 `json:"other_amount"`
|
||||
DepositDeductAmount float64 `json:"deposit_deduct_amount"`
|
||||
Reason string `json:"reason" binding:"required"`
|
||||
EvidenceURLS []string `json:"evidence_urls"`
|
||||
}
|
||||
|
||||
type AdminActionRequest struct {
|
||||
@@ -63,3 +77,26 @@ type HandoffRecordDTO struct {
|
||||
ConfirmedByOwnerAt *time.Time `json:"confirmed_by_owner_at"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
}
|
||||
|
||||
type CheckoutDTO struct {
|
||||
ID uint64 `json:"id"`
|
||||
OrderID uint64 `json:"order_id"`
|
||||
InitiatedBy uint64 `json:"initiated_by"`
|
||||
Status string `json:"status"`
|
||||
RentAmount float64 `json:"rent_amount"`
|
||||
DepositAmount float64 `json:"deposit_amount"`
|
||||
ConsumableAmount float64 `json:"consumable_amount"`
|
||||
CoinConsumedM float64 `json:"coin_consumed_m"`
|
||||
OtherAmount float64 `json:"other_amount"`
|
||||
DepositDeductAmount float64 `json:"deposit_deduct_amount"`
|
||||
RenterRefundAmount float64 `json:"renter_refund_amount"`
|
||||
OwnerIncomeAmount float64 `json:"owner_income_amount"`
|
||||
Content string `json:"content"`
|
||||
EvidenceURLS []string `json:"evidence_urls"`
|
||||
OwnerAdjustmentReason string `json:"owner_adjustment_reason"`
|
||||
OwnerAdjustedAt *time.Time `json:"owner_adjusted_at"`
|
||||
RenterConfirmedAt *time.Time `json:"renter_confirmed_at"`
|
||||
RenterRejectedAt *time.Time `json:"renter_rejected_at"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
}
|
||||
|
||||
@@ -210,7 +210,7 @@ func (h *Handler) HandoffRecords(c *gin.Context) {
|
||||
response.OK(c, gin.H{"items": items})
|
||||
}
|
||||
|
||||
func (h *Handler) SubmitReturn(c *gin.Context) {
|
||||
func (h *Handler) SubmitCheckout(c *gin.Context) {
|
||||
userID, ok := currentUserID(c)
|
||||
if !ok {
|
||||
response.Unauthorized(c, "缺少用户上下文")
|
||||
@@ -220,12 +220,12 @@ func (h *Handler) SubmitReturn(c *gin.Context) {
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
var req SubmitReturnRequest
|
||||
var req SubmitCheckoutRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
response.BadRequest(c, "归还说明不能为空")
|
||||
response.BadRequest(c, "结账说明不能为空")
|
||||
return
|
||||
}
|
||||
record, err := h.service.SubmitReturn(userID, id, req)
|
||||
record, err := h.service.SubmitCheckout(userID, id, req)
|
||||
if err != nil {
|
||||
writeOrderError(c, err)
|
||||
return
|
||||
@@ -233,7 +233,7 @@ func (h *Handler) SubmitReturn(c *gin.Context) {
|
||||
response.Created(c, record)
|
||||
}
|
||||
|
||||
func (h *Handler) ConfirmReturn(c *gin.Context) {
|
||||
func (h *Handler) ConfirmCheckout(c *gin.Context) {
|
||||
userID, ok := currentUserID(c)
|
||||
if !ok {
|
||||
response.Unauthorized(c, "缺少用户上下文")
|
||||
@@ -243,7 +243,47 @@ func (h *Handler) ConfirmReturn(c *gin.Context) {
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
if err := h.service.ConfirmReturn(userID, id); err != nil {
|
||||
if err := h.service.ConfirmCheckout(userID, id); err != nil {
|
||||
writeOrderError(c, err)
|
||||
return
|
||||
}
|
||||
response.OK(c, gin.H{"completed": true})
|
||||
}
|
||||
|
||||
func (h *Handler) CounterCheckout(c *gin.Context) {
|
||||
userID, ok := currentUserID(c)
|
||||
if !ok {
|
||||
response.Unauthorized(c, "缺少用户上下文")
|
||||
return
|
||||
}
|
||||
id, ok := parseID(c)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
var req CounterCheckoutRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
response.BadRequest(c, "结账修正原因不能为空")
|
||||
return
|
||||
}
|
||||
checkout, err := h.service.CounterCheckout(userID, id, req)
|
||||
if err != nil {
|
||||
writeOrderError(c, err)
|
||||
return
|
||||
}
|
||||
response.OK(c, checkout)
|
||||
}
|
||||
|
||||
func (h *Handler) AcceptCheckout(c *gin.Context) {
|
||||
userID, ok := currentUserID(c)
|
||||
if !ok {
|
||||
response.Unauthorized(c, "缺少用户上下文")
|
||||
return
|
||||
}
|
||||
id, ok := parseID(c)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
if err := h.service.AcceptCheckout(userID, id); err != nil {
|
||||
writeOrderError(c, err)
|
||||
return
|
||||
}
|
||||
@@ -293,10 +333,16 @@ func writeOrderError(c *gin.Context, err error) {
|
||||
response.Error(c, http.StatusConflict, "order_cannot_handoff", "当前订单不能交接")
|
||||
case errors.Is(err, ErrOrderCannotReceive):
|
||||
response.Error(c, http.StatusConflict, "order_cannot_receive", "当前订单不能确认收号")
|
||||
case errors.Is(err, ErrOrderCannotReturn):
|
||||
response.Error(c, http.StatusConflict, "order_cannot_return", "当前订单不能归还")
|
||||
case errors.Is(err, ErrOrderCannotComplete):
|
||||
response.Error(c, http.StatusConflict, "order_cannot_complete", "当前订单不能完成")
|
||||
case errors.Is(err, ErrCheckoutCannotSubmit):
|
||||
response.Error(c, http.StatusConflict, "checkout_cannot_submit", "当前订单不能发起结账")
|
||||
case errors.Is(err, ErrCheckoutCannotConfirm):
|
||||
response.Error(c, http.StatusConflict, "checkout_cannot_confirm", "当前结账不能确认")
|
||||
case errors.Is(err, ErrCheckoutCannotCounter):
|
||||
response.Error(c, http.StatusConflict, "checkout_cannot_counter", "当前结账不能修改")
|
||||
case errors.Is(err, ErrInvalidCheckoutAmount):
|
||||
response.BadRequest(c, "结账金额不符合规则")
|
||||
case errors.Is(err, ErrPermissionDenied):
|
||||
response.Error(c, http.StatusForbidden, "permission_denied", "无权操作该订单")
|
||||
case IsNotFound(err):
|
||||
|
||||
@@ -5,6 +5,7 @@ import (
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"math"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
@@ -296,7 +297,7 @@ func (r *Repository) HandoffRecords(userID uint64, orderID uint64) ([]HandoffRec
|
||||
return items, nil
|
||||
}
|
||||
|
||||
func (r *Repository) SubmitReturn(userID uint64, orderID uint64, req SubmitReturnRequest) (*HandoffRecordDTO, error) {
|
||||
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
|
||||
@@ -307,28 +308,43 @@ func (r *Repository) SubmitReturn(userID uint64, orderID uint64, req SubmitRetur
|
||||
return ErrPermissionDenied
|
||||
}
|
||||
if (order.Status != "renting" && order.Status != "overdue") || (order.HandoffStatus != "received" && order.HandoffStatus != "return_overdue") {
|
||||
return ErrOrderCannotReturn
|
||||
return ErrCheckoutCannotSubmit
|
||||
}
|
||||
hasOpenCheckout, err := hasOpenCheckout(tx, order.ID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if hasOpenCheckout {
|
||||
return ErrCheckoutCannotSubmit
|
||||
}
|
||||
now := time.Now()
|
||||
record := model.HandoffRecord{
|
||||
OrderID: order.ID,
|
||||
FromUserID: order.RenterID,
|
||||
ToUserID: order.OwnerID,
|
||||
Type: "renter_return",
|
||||
Type: "renter_checkout",
|
||||
Content: req.Content,
|
||||
}
|
||||
if err := tx.Create(&record).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
order.Status = "pending_return_confirm"
|
||||
order.HandoffStatus = "pending_owner_return_confirm"
|
||||
checkout, err := buildCheckout(order, order.RenterID, "submitted", req.Content, req.EvidenceURLS, req.ConsumableAmount, req.CoinConsumedM, req.OtherAmount, 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"
|
||||
order.RentEndAt = &now
|
||||
orderID := order.ID
|
||||
if err := notification.Append(tx, notification.Entry{
|
||||
UserID: order.OwnerID,
|
||||
Type: "return",
|
||||
Title: "租客已提交归还",
|
||||
Content: "请检查账号状态,确认无误后完成订单。",
|
||||
Type: "checkout",
|
||||
Title: "租客已发起结账",
|
||||
Content: "请检查账号状态和消耗明细,确认无误后完成结算。",
|
||||
BizType: "order",
|
||||
BizID: &orderID,
|
||||
}); err != nil {
|
||||
@@ -346,7 +362,7 @@ func (r *Repository) SubmitReturn(userID uint64, orderID uint64, req SubmitRetur
|
||||
return r.findHandoffRecord(recordID)
|
||||
}
|
||||
|
||||
func (r *Repository) ConfirmReturn(userID uint64, orderID uint64) error {
|
||||
func (r *Repository) ConfirmCheckout(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 {
|
||||
@@ -355,92 +371,120 @@ func (r *Repository) ConfirmReturn(userID uint64, orderID uint64) error {
|
||||
if order.OwnerID != userID {
|
||||
return ErrPermissionDenied
|
||||
}
|
||||
if order.Status != "pending_return_confirm" || order.HandoffStatus != "pending_owner_return_confirm" {
|
||||
return ErrOrderCannotComplete
|
||||
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_return").
|
||||
Where("order_id = ? AND type = ?", order.ID, "renter_checkout").
|
||||
Update("confirmed_by_owner_at", now).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
var listing model.RentalListing
|
||||
if err := tx.Clauses(clause.Locking{Strength: "UPDATE"}).First(&listing, order.ListingID).Error; err != nil {
|
||||
checkout.Status = "accepted"
|
||||
checkout.OwnerAdjustedAt = &now
|
||||
return r.finalizeCheckout(tx, &order, &checkout, "号主已确认租客结账,订单完成。")
|
||||
})
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
var account model.GameAccount
|
||||
if err := tx.Clauses(clause.Locking{Strength: "UPDATE"}).First(&account, order.AccountID).Error; err != nil {
|
||||
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
|
||||
}
|
||||
order.Status = "completed"
|
||||
order.HandoffStatus = "returned"
|
||||
order.SettlementStatus = "settled"
|
||||
order.SettledAt = &now
|
||||
order.OwnerSettledAt = &now
|
||||
next, err := buildCheckout(order, checkout.InitiatedBy, "countered", checkout.Content, req.EvidenceURLS, req.ConsumableAmount, req.CoinConsumedM, req.OtherAmount, req.DepositDeductAmount, true)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
now := time.Now()
|
||||
checkout.Status = "countered"
|
||||
checkout.ConsumableAmount = next.ConsumableAmount
|
||||
checkout.CoinConsumedM = next.CoinConsumedM
|
||||
checkout.OtherAmount = next.OtherAmount
|
||||
checkout.DepositDeductAmount = next.DepositDeductAmount
|
||||
checkout.RenterRefundAmount = next.RenterRefundAmount
|
||||
checkout.OwnerIncomeAmount = next.OwnerIncomeAmount
|
||||
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 := wallet.AppendEntries(tx,
|
||||
wallet.Entry{
|
||||
UserID: order.RenterID,
|
||||
OrderID: &orderID,
|
||||
Direction: "out",
|
||||
Amount: order.RentAmount + order.DepositAmount,
|
||||
BalanceType: "frozen",
|
||||
BizType: "order_settle",
|
||||
BizNo: order.OrderNo,
|
||||
Remark: "订单完成释放模拟冻结金额",
|
||||
},
|
||||
wallet.Entry{
|
||||
UserID: order.OwnerID,
|
||||
OrderID: &orderID,
|
||||
Direction: "in",
|
||||
Amount: order.RentAmount,
|
||||
BalanceType: "available",
|
||||
BizType: "owner_income",
|
||||
BizNo: order.OrderNo,
|
||||
Remark: "订单完成模拟结算订单金额",
|
||||
},
|
||||
wallet.Entry{
|
||||
UserID: order.RenterID,
|
||||
OrderID: &orderID,
|
||||
Direction: "in",
|
||||
Amount: order.DepositAmount,
|
||||
BalanceType: "available",
|
||||
BizType: "deposit_release",
|
||||
BizNo: order.OrderNo,
|
||||
Remark: "订单完成模拟退回押金",
|
||||
},
|
||||
); err != nil {
|
||||
if err := notification.Append(tx, notification.Entry{
|
||||
UserID: order.RenterID,
|
||||
Type: "checkout",
|
||||
Title: "号主已修改结账金额",
|
||||
Content: "请核对号主修正的消耗和结算金额。同意后订单完成;不同意可发起争议。",
|
||||
BizType: "order",
|
||||
BizID: &orderID,
|
||||
}); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := notification.Append(tx,
|
||||
notification.Entry{
|
||||
UserID: order.RenterID,
|
||||
Type: "settlement",
|
||||
Title: "订单已完成",
|
||||
Content: "号主已确认归还,模拟押金已退回。",
|
||||
BizType: "order",
|
||||
BizID: &orderID,
|
||||
},
|
||||
notification.Entry{
|
||||
UserID: order.OwnerID,
|
||||
Type: "settlement",
|
||||
Title: "订单已完成",
|
||||
Content: "订单已完成,模拟订单金额已入账。",
|
||||
BizType: "order",
|
||||
BizID: &orderID,
|
||||
},
|
||||
); err != nil {
|
||||
return err
|
||||
}
|
||||
listing.Status = "published"
|
||||
account.Status = "published"
|
||||
if err := tx.Save(&order).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
if err := tx.Save(&listing).Error; err != nil {
|
||||
if err := tx.Save(&checkout).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
return tx.Save(&account).Error
|
||||
checkoutID = checkout.ID
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
checkout, err := r.findCheckout(checkoutID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
dto := toCheckoutDTO(*checkout)
|
||||
return &dto, nil
|
||||
}
|
||||
|
||||
func (r *Repository) AcceptCheckout(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_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
|
||||
return r.finalizeCheckout(tx, &order, &checkout, "租客已确认修正结账,订单完成。")
|
||||
})
|
||||
}
|
||||
|
||||
@@ -482,6 +526,7 @@ func (r *Repository) FindAdmin(orderID uint64) (*OrderDTO, error) {
|
||||
return nil, err
|
||||
}
|
||||
dto := row.toDTO()
|
||||
dto.Checkout = r.latestCheckoutDTO(orderID)
|
||||
return &dto, nil
|
||||
}
|
||||
|
||||
@@ -655,9 +700,175 @@ func (r *Repository) FindForUser(userID uint64, orderID uint64) (*OrderDTO, erro
|
||||
return nil, err
|
||||
}
|
||||
dto := row.toDTO()
|
||||
dto.Checkout = r.latestCheckoutDTO(orderID)
|
||||
return &dto, nil
|
||||
}
|
||||
|
||||
func (r *Repository) finalizeCheckout(tx *gorm.DB, order *model.RentalOrder, checkout *model.OrderCheckout, renterContent string) error {
|
||||
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
|
||||
}
|
||||
now := time.Now()
|
||||
order.Status = "completed"
|
||||
order.HandoffStatus = "returned"
|
||||
order.SettlementStatus = "settled"
|
||||
order.SettledAt = &now
|
||||
order.OwnerSettledAt = &now
|
||||
listing.Status = "published"
|
||||
account.Status = "published"
|
||||
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_settle",
|
||||
BizNo: order.OrderNo,
|
||||
Remark: "订单结账释放模拟冻结金额",
|
||||
},
|
||||
wallet.Entry{
|
||||
UserID: order.OwnerID,
|
||||
OrderID: &orderID,
|
||||
Direction: "in",
|
||||
Amount: checkout.OwnerIncomeAmount,
|
||||
BalanceType: "available",
|
||||
BizType: "owner_income",
|
||||
BizNo: order.OrderNo,
|
||||
Remark: "订单结账收入",
|
||||
},
|
||||
wallet.Entry{
|
||||
UserID: order.RenterID,
|
||||
OrderID: &orderID,
|
||||
Direction: "in",
|
||||
Amount: checkout.RenterRefundAmount,
|
||||
BalanceType: "available",
|
||||
BizType: "deposit_release",
|
||||
BizNo: order.OrderNo,
|
||||
Remark: "订单结账退回押金",
|
||||
},
|
||||
); err != nil {
|
||||
return err
|
||||
}
|
||||
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 err
|
||||
}
|
||||
if err := tx.Save(order).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
if err := tx.Save(checkout).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
if err := tx.Save(&listing).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
return tx.Save(&account).Error
|
||||
}
|
||||
|
||||
func buildCheckout(order model.RentalOrder, initiatedBy uint64, status string, content string, evidenceURLS []string, consumableAmount float64, coinConsumedM float64, otherAmount float64, explicitDeduct float64, useExplicitDeduct bool) (model.OrderCheckout, error) {
|
||||
if consumableAmount < 0 || coinConsumedM < 0 || otherAmount < 0 || explicitDeduct < 0 {
|
||||
return model.OrderCheckout{}, ErrInvalidCheckoutAmount
|
||||
}
|
||||
deductAmount := consumableAmount + otherAmount
|
||||
if useExplicitDeduct {
|
||||
deductAmount = explicitDeduct
|
||||
}
|
||||
if deductAmount > order.DepositAmount {
|
||||
return model.OrderCheckout{}, ErrInvalidCheckoutAmount
|
||||
}
|
||||
renterRefund := order.DepositAmount - deductAmount
|
||||
evidence, err := marshalStringList(evidenceURLS)
|
||||
if err != nil {
|
||||
return model.OrderCheckout{}, err
|
||||
}
|
||||
return model.OrderCheckout{
|
||||
OrderID: order.ID,
|
||||
InitiatedBy: initiatedBy,
|
||||
Status: status,
|
||||
RentAmount: order.RentAmount,
|
||||
DepositAmount: order.DepositAmount,
|
||||
ConsumableAmount: roundMoney(consumableAmount),
|
||||
CoinConsumedM: roundMoney(coinConsumedM),
|
||||
OtherAmount: roundMoney(otherAmount),
|
||||
DepositDeductAmount: roundMoney(deductAmount),
|
||||
RenterRefundAmount: roundMoney(renterRefund),
|
||||
OwnerIncomeAmount: roundMoney(order.RentAmount + deductAmount),
|
||||
Content: content,
|
||||
EvidenceURLS: evidence,
|
||||
}, nil
|
||||
}
|
||||
|
||||
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 roundMoney(value float64) float64 {
|
||||
return math.Round(value*100) / 100
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
func (r *Repository) latestCheckoutDTO(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 := toCheckoutDTO(checkout)
|
||||
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) 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 {
|
||||
@@ -751,6 +962,31 @@ func toHandoffDTO(record model.HandoffRecord) HandoffRecordDTO {
|
||||
}
|
||||
}
|
||||
|
||||
func toCheckoutDTO(checkout model.OrderCheckout) CheckoutDTO {
|
||||
return CheckoutDTO{
|
||||
ID: checkout.ID,
|
||||
OrderID: checkout.OrderID,
|
||||
InitiatedBy: checkout.InitiatedBy,
|
||||
Status: checkout.Status,
|
||||
RentAmount: checkout.RentAmount,
|
||||
DepositAmount: checkout.DepositAmount,
|
||||
ConsumableAmount: checkout.ConsumableAmount,
|
||||
CoinConsumedM: checkout.CoinConsumedM,
|
||||
OtherAmount: checkout.OtherAmount,
|
||||
DepositDeductAmount: checkout.DepositDeductAmount,
|
||||
RenterRefundAmount: checkout.RenterRefundAmount,
|
||||
OwnerIncomeAmount: checkout.OwnerIncomeAmount,
|
||||
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 makeAccountSnapshot(account model.GameAccount) (datatypes.JSON, error) {
|
||||
payload := map[string]any{
|
||||
"account_id": account.ID,
|
||||
|
||||
@@ -10,8 +10,11 @@ var (
|
||||
ErrOrderCannotCancel = errors.New("order cannot cancel")
|
||||
ErrOrderCannotHandoff = errors.New("order cannot handoff")
|
||||
ErrOrderCannotReceive = errors.New("order cannot receive")
|
||||
ErrOrderCannotReturn = errors.New("order cannot return")
|
||||
ErrOrderCannotComplete = errors.New("order cannot complete")
|
||||
ErrCheckoutCannotSubmit = errors.New("checkout cannot submit")
|
||||
ErrCheckoutCannotConfirm = errors.New("checkout cannot confirm")
|
||||
ErrCheckoutCannotCounter = errors.New("checkout cannot counter")
|
||||
ErrInvalidCheckoutAmount = errors.New("invalid checkout amount")
|
||||
ErrPermissionDenied = errors.New("permission denied")
|
||||
)
|
||||
|
||||
@@ -66,21 +69,38 @@ func (s *Service) HandoffRecords(userID uint64, orderID uint64) ([]HandoffRecord
|
||||
return s.repo.HandoffRecords(userID, orderID)
|
||||
}
|
||||
|
||||
func (s *Service) SubmitReturn(userID uint64, orderID uint64, req SubmitReturnRequest) (*HandoffRecordDTO, error) {
|
||||
func (s *Service) SubmitCheckout(userID uint64, orderID uint64, req SubmitCheckoutRequest) (*HandoffRecordDTO, error) {
|
||||
if s.repo == nil {
|
||||
return nil, ErrDependencyUnavailable
|
||||
}
|
||||
if orderID == 0 || req.Content == "" {
|
||||
return nil, ErrOrderCannotReturn
|
||||
return nil, ErrCheckoutCannotSubmit
|
||||
}
|
||||
return s.repo.SubmitReturn(userID, orderID, req)
|
||||
return s.repo.SubmitCheckout(userID, orderID, req)
|
||||
}
|
||||
|
||||
func (s *Service) ConfirmReturn(userID uint64, orderID uint64) error {
|
||||
func (s *Service) ConfirmCheckout(userID uint64, orderID uint64) error {
|
||||
if s.repo == nil {
|
||||
return ErrDependencyUnavailable
|
||||
}
|
||||
return s.repo.ConfirmReturn(userID, orderID)
|
||||
return s.repo.ConfirmCheckout(userID, orderID)
|
||||
}
|
||||
|
||||
func (s *Service) CounterCheckout(userID uint64, orderID uint64, req CounterCheckoutRequest) (*CheckoutDTO, error) {
|
||||
if s.repo == nil {
|
||||
return nil, ErrDependencyUnavailable
|
||||
}
|
||||
if orderID == 0 || req.Reason == "" {
|
||||
return nil, ErrCheckoutCannotCounter
|
||||
}
|
||||
return s.repo.CounterCheckout(userID, orderID, req)
|
||||
}
|
||||
|
||||
func (s *Service) AcceptCheckout(userID uint64, orderID uint64) error {
|
||||
if s.repo == nil {
|
||||
return ErrDependencyUnavailable
|
||||
}
|
||||
return s.repo.AcceptCheckout(userID, orderID)
|
||||
}
|
||||
|
||||
func (s *Service) ListForUser(userID uint64) ([]OrderDTO, error) {
|
||||
|
||||
@@ -30,8 +30,8 @@ type defaultConfig struct {
|
||||
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.return_overdue_grace_minutes", Value: "10", Description: "预计截止后归还宽限分钟数"},
|
||||
{Key: "handoff.owner_return_confirm_timeout_minutes", Value: "120", Description: "号主待确认结账超时分钟数"},
|
||||
{Key: "order.return_overdue_grace_minutes", Value: "10", Description: "预计截止后结账宽限分钟数"},
|
||||
{Key: "deposit.min_amount", Value: "50", Description: "发布租号最低押金"},
|
||||
{Key: "listing.review_required", Value: "false", Description: "发布账号是否需要后台人工审核"},
|
||||
{Key: "risk.sms_limit_per_phone_hour", Value: "5", Description: "单手机号每小时短信验证码次数"},
|
||||
|
||||
@@ -170,8 +170,10 @@ func New(cfg config.Config, deps Dependencies, logger *zap.Logger) *gin.Engine {
|
||||
orderRoutes.POST("/:id/handoff", orderHandler.SubmitHandoff)
|
||||
orderRoutes.GET("/:id/handoff-records", orderHandler.HandoffRecords)
|
||||
orderRoutes.POST("/:id/confirm-receive", orderHandler.ConfirmReceive)
|
||||
orderRoutes.POST("/:id/return", orderHandler.SubmitReturn)
|
||||
orderRoutes.POST("/:id/confirm-return", orderHandler.ConfirmReturn)
|
||||
orderRoutes.POST("/:id/checkout", orderHandler.SubmitCheckout)
|
||||
orderRoutes.POST("/:id/checkout/confirm", orderHandler.ConfirmCheckout)
|
||||
orderRoutes.POST("/:id/checkout/counter", orderHandler.CounterCheckout)
|
||||
orderRoutes.POST("/:id/checkout/accept", orderHandler.AcceptCheckout)
|
||||
orderRoutes.POST("/:id/dispute", disputeHandler.Create)
|
||||
}
|
||||
|
||||
|
||||
@@ -109,6 +109,31 @@ CREATE TABLE handoff_records (
|
||||
KEY idx_handoff_records_order_id (order_id)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
|
||||
CREATE TABLE order_checkouts (
|
||||
id BIGINT UNSIGNED PRIMARY KEY AUTO_INCREMENT,
|
||||
order_id BIGINT UNSIGNED NOT NULL,
|
||||
initiated_by BIGINT UNSIGNED NOT NULL,
|
||||
status VARCHAR(32) NOT NULL DEFAULT 'submitted',
|
||||
rent_amount DECIMAL(12,2) NOT NULL DEFAULT 0.00,
|
||||
deposit_amount DECIMAL(12,2) NOT NULL DEFAULT 0.00,
|
||||
consumable_amount DECIMAL(12,2) NOT NULL DEFAULT 0.00,
|
||||
coin_consumed_m DECIMAL(12,2) NOT NULL DEFAULT 0.00,
|
||||
other_amount DECIMAL(12,2) NOT NULL DEFAULT 0.00,
|
||||
deposit_deduct_amount DECIMAL(12,2) NOT NULL DEFAULT 0.00,
|
||||
renter_refund_amount DECIMAL(12,2) NOT NULL DEFAULT 0.00,
|
||||
owner_income_amount DECIMAL(12,2) NOT NULL DEFAULT 0.00,
|
||||
content TEXT NULL,
|
||||
evidence_urls JSON NULL,
|
||||
owner_adjustment_reason TEXT NULL,
|
||||
owner_adjusted_at DATETIME NULL,
|
||||
renter_confirmed_at DATETIME NULL,
|
||||
renter_rejected_at DATETIME NULL,
|
||||
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
KEY idx_order_checkouts_order_id (order_id),
|
||||
KEY idx_order_checkouts_status (status)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
|
||||
CREATE TABLE wallet_accounts (
|
||||
id BIGINT UNSIGNED PRIMARY KEY AUTO_INCREMENT,
|
||||
user_id BIGINT UNSIGNED NOT NULL,
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
CREATE TABLE IF NOT EXISTS order_checkouts (
|
||||
id BIGINT UNSIGNED PRIMARY KEY AUTO_INCREMENT,
|
||||
order_id BIGINT UNSIGNED NOT NULL,
|
||||
initiated_by BIGINT UNSIGNED NOT NULL,
|
||||
status VARCHAR(32) NOT NULL DEFAULT 'submitted',
|
||||
rent_amount DECIMAL(12,2) NOT NULL DEFAULT 0.00,
|
||||
deposit_amount DECIMAL(12,2) NOT NULL DEFAULT 0.00,
|
||||
consumable_amount DECIMAL(12,2) NOT NULL DEFAULT 0.00,
|
||||
coin_consumed_m DECIMAL(12,2) NOT NULL DEFAULT 0.00,
|
||||
other_amount DECIMAL(12,2) NOT NULL DEFAULT 0.00,
|
||||
deposit_deduct_amount DECIMAL(12,2) NOT NULL DEFAULT 0.00,
|
||||
renter_refund_amount DECIMAL(12,2) NOT NULL DEFAULT 0.00,
|
||||
owner_income_amount DECIMAL(12,2) NOT NULL DEFAULT 0.00,
|
||||
content TEXT NULL,
|
||||
evidence_urls JSON NULL,
|
||||
owner_adjustment_reason TEXT NULL,
|
||||
owner_adjusted_at DATETIME NULL,
|
||||
renter_confirmed_at DATETIME NULL,
|
||||
renter_rejected_at DATETIME NULL,
|
||||
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
KEY idx_order_checkouts_order_id (order_id),
|
||||
KEY idx_order_checkouts_status (status)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
Reference in New Issue
Block a user