实现结账流程:替换旧归还逻辑为完整结账-修正-争议流程
- 新增 order_checkouts 表,支持结账明细(消耗/押金扣除/退回/号主入账) - 租客发起结账 → 号主确认 → 已完成(正常路径) - 号主修改结账 → 租客确认修正 → 已完成(修正路径) - 结账阶段任一方发起争议 → 后台仲裁 → 已完成/已关闭/异常(争议路径) - 争议模块适配结账争议类型,仲裁结果新增 mark_abnormal - 超时任务适配新的 pending_checkout_confirm 状态 - 前端结账明细面板、发起结账/确认/修正/拒绝表单全部实现 - 移除旧的 SubmitReturn/ConfirmReturn 接口
This commit is contained in:
@@ -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) {
|
||||
|
||||
Reference in New Issue
Block a user