- 号主交接超时(owner_timeout)后允许补提交交接说明,避免临时延误导致订单卡死 - 后台新增"重置交接"动作,可将超时订单恢复为待号主交接并刷新计时 - 交接超时改以进入待交接时刻为基准计算,新增 handoff_started_at 字段 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
269 lines
8.9 KiB
Go
269 lines
8.9 KiB
Go
package order
|
|
|
|
import (
|
|
"context"
|
|
"time"
|
|
|
|
"hfb_sys/backend/internal/model"
|
|
"hfb_sys/backend/internal/modules/notification"
|
|
|
|
"gorm.io/gorm"
|
|
"gorm.io/gorm/clause"
|
|
)
|
|
|
|
func (r *Repository) AdminClose(ctx context.Context, adminID uint64, orderID uint64, req AdminActionRequest, meta AuditMeta) error {
|
|
var refund *refundAction
|
|
err := r.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
|
|
assets, err := r.lockOrderAssets(tx, orderID)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
order := assets.Order
|
|
listing := assets.Listing
|
|
account := assets.Account
|
|
if isTerminalStatus(order.Status) {
|
|
return ErrOrderCannotComplete
|
|
}
|
|
beforeOrderStatus := order.Status
|
|
beforeHandoffStatus := order.HandoffStatus
|
|
beforeSettlementStatus := order.SettlementStatus
|
|
beforeListingStatus := listing.Status
|
|
beforeAccountStatus := account.Status
|
|
now := time.Now()
|
|
order.Status = orderStatusClosed
|
|
order.HandoffStatus = handoffStatusAdminClosed
|
|
order.SettlementStatus = settlementStatusClosed
|
|
order.SettledAt = &now
|
|
archiveAssets(listing, account)
|
|
if beforeOrderStatus != orderStatusPendingPayment {
|
|
totalCent := order.RentAmountCent + order.DepositAmountCent
|
|
action, err := r.prepareRefund(order, totalCent, refundBizAdminClose, "客服关闭订单原路退款")
|
|
if err != nil {
|
|
return err
|
|
}
|
|
refund = action
|
|
}
|
|
if err := notification.Append(tx,
|
|
notification.Entry{
|
|
UserID: order.RenterID,
|
|
Type: "order_admin",
|
|
Title: "订单已由客服关闭",
|
|
Content: "客服已关闭订单,退款将原路退回您的支付账户。原因:" + req.Reason,
|
|
BizType: "order",
|
|
BizID: &order.ID,
|
|
},
|
|
notification.Entry{
|
|
UserID: order.OwnerID,
|
|
Type: "order_admin",
|
|
Title: "订单已由客服关闭",
|
|
Content: "客服已关闭订单,关联商品已下架。原因:" + req.Reason,
|
|
BizType: "order",
|
|
BizID: &order.ID,
|
|
},
|
|
); err != nil {
|
|
return err
|
|
}
|
|
if err := appendAuditLog(tx, adminID, "order.admin_close", "order", order.ID, meta, map[string]any{
|
|
"order_id": order.ID,
|
|
"order_no": order.OrderNo,
|
|
"listing_id": order.ListingID,
|
|
"account_id": order.AccountID,
|
|
"reason": req.Reason,
|
|
"before_order_status": beforeOrderStatus,
|
|
"after_order_status": order.Status,
|
|
"before_handoff_status": beforeHandoffStatus,
|
|
"after_handoff_status": order.HandoffStatus,
|
|
"before_settlement_status": beforeSettlementStatus,
|
|
"after_settlement_status": order.SettlementStatus,
|
|
"before_listing_status": beforeListingStatus,
|
|
"after_listing_status": listing.Status,
|
|
"before_account_status": beforeAccountStatus,
|
|
"after_account_status": account.Status,
|
|
}); 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
|
|
})
|
|
if err != nil {
|
|
return err
|
|
}
|
|
r.startRefundBestEffort(ctx, refund)
|
|
return nil
|
|
}
|
|
|
|
func (r *Repository) AdminMarkAbnormal(ctx context.Context, adminID uint64, orderID uint64, req AdminActionRequest, meta AuditMeta) error {
|
|
return r.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
|
|
assets, err := r.lockOrderAssets(tx, orderID)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
order := assets.Order
|
|
listing := assets.Listing
|
|
account := assets.Account
|
|
if isTerminalStatus(order.Status) {
|
|
return ErrOrderCannotComplete
|
|
}
|
|
beforeOrderStatus := order.Status
|
|
beforeHandoffStatus := order.HandoffStatus
|
|
beforeListingStatus := listing.Status
|
|
beforeAccountStatus := account.Status
|
|
order.Status = orderStatusAbnormal
|
|
order.HandoffStatus = handoffStatusAdminAbnormal
|
|
markAssetsAbnormal(listing, account)
|
|
if err := notification.Append(tx,
|
|
notification.Entry{
|
|
UserID: order.RenterID,
|
|
Type: "order_admin",
|
|
Title: "订单已被标记异常",
|
|
Content: "客服已将订单标记为异常,请等待进一步处理。原因:" + req.Reason,
|
|
BizType: "order",
|
|
BizID: &order.ID,
|
|
},
|
|
notification.Entry{
|
|
UserID: order.OwnerID,
|
|
Type: "order_admin",
|
|
Title: "订单已被标记异常",
|
|
Content: "客服已将订单标记为异常,关联商品暂不可出租。原因:" + req.Reason,
|
|
BizType: "order",
|
|
BizID: &order.ID,
|
|
},
|
|
); err != nil {
|
|
return err
|
|
}
|
|
if err := appendAuditLog(tx, adminID, "order.mark_abnormal", "order", order.ID, meta, map[string]any{
|
|
"order_id": order.ID,
|
|
"order_no": order.OrderNo,
|
|
"listing_id": order.ListingID,
|
|
"account_id": order.AccountID,
|
|
"reason": req.Reason,
|
|
"before_order_status": beforeOrderStatus,
|
|
"after_order_status": order.Status,
|
|
"before_handoff_status": beforeHandoffStatus,
|
|
"after_handoff_status": order.HandoffStatus,
|
|
"before_listing_status": beforeListingStatus,
|
|
"after_listing_status": listing.Status,
|
|
"before_account_status": beforeAccountStatus,
|
|
"after_account_status": account.Status,
|
|
}); 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
|
|
})
|
|
}
|
|
|
|
// AdminResetHandoff 将号主交接超时(owner_timeout)的订单重置回待号主交接,并刷新交接计时,
|
|
// 让客服可以给号主再次提交交接说明的机会,避免订单卡死在超时状态。
|
|
func (r *Repository) AdminResetHandoff(ctx context.Context, adminID uint64, orderID uint64, req AdminActionRequest, meta AuditMeta) error {
|
|
return r.db.WithContext(ctx).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.Status != orderStatusPendingHandoff || order.HandoffStatus != handoffStatusOwnerTimeout {
|
|
return ErrOrderCannotResetHandoff
|
|
}
|
|
beforeHandoffStatus := order.HandoffStatus
|
|
now := time.Now()
|
|
order.HandoffStatus = handoffStatusPendingOwner
|
|
order.HandoffStartedAt = &now
|
|
if err := notification.Append(tx,
|
|
notification.Entry{
|
|
UserID: order.OwnerID,
|
|
Type: "order_admin",
|
|
Title: "请重新提交交接说明",
|
|
Content: "客服已重置交接状态,请尽快提交交接说明,避免再次超时。原因:" + req.Reason,
|
|
BizType: "order",
|
|
BizID: &order.ID,
|
|
},
|
|
notification.Entry{
|
|
UserID: order.RenterID,
|
|
Type: "order_admin",
|
|
Title: "订单交接已重置",
|
|
Content: "客服已重置交接状态,正在等待号主重新提交交接说明。原因:" + req.Reason,
|
|
BizType: "order",
|
|
BizID: &order.ID,
|
|
},
|
|
); err != nil {
|
|
return err
|
|
}
|
|
if err := appendAuditLog(tx, adminID, "order.reset_handoff", "order", order.ID, meta, map[string]any{
|
|
"order_id": order.ID,
|
|
"order_no": order.OrderNo,
|
|
"reason": req.Reason,
|
|
"before_handoff_status": beforeHandoffStatus,
|
|
"after_handoff_status": order.HandoffStatus,
|
|
}); err != nil {
|
|
return err
|
|
}
|
|
return tx.Save(&order).Error
|
|
})
|
|
}
|
|
|
|
// AdminRefund 触发后台人工退款,退款由 payment 模块走渠道原路退回。
|
|
func (r *Repository) AdminRefund(ctx context.Context, orderID uint64) (*RefundStatusDTO, error) {
|
|
var order model.RentalOrder
|
|
db := r.db.WithContext(ctx)
|
|
if err := db.First(&order, orderID).Error; err != nil {
|
|
return nil, err
|
|
}
|
|
if order.RefundStatus == refundStatusRefunded {
|
|
return r.buildRefundStatusDTO(&order), nil
|
|
}
|
|
if r.refundStarter == nil {
|
|
return nil, ErrDependencyUnavailable
|
|
}
|
|
totalCent := order.RentAmountCent + order.DepositAmountCent
|
|
if totalCent <= 0 {
|
|
return nil, ErrInvalidCheckoutAmount
|
|
}
|
|
status, err := r.refundStarter.StartRefund(ctx, orderID, totalCent, refundBizAdmin, "后台人工退款")
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
// 重新读取订单,拿到 payment 模块更新后的退款字段。
|
|
if err := db.First(&order, orderID).Error; err != nil {
|
|
return nil, err
|
|
}
|
|
dto := r.buildRefundStatusDTO(&order)
|
|
if status != "" {
|
|
dto.RefundStatus = status
|
|
}
|
|
return dto, nil
|
|
}
|
|
|
|
// AdminRefundStatus 查询订单退款状态。
|
|
func (r *Repository) AdminRefundStatus(ctx context.Context, orderID uint64) (*RefundStatusDTO, error) {
|
|
var order model.RentalOrder
|
|
if err := r.db.WithContext(ctx).First(&order, orderID).Error; err != nil {
|
|
return nil, err
|
|
}
|
|
return r.buildRefundStatusDTO(&order), nil
|
|
}
|
|
|
|
func (r *Repository) buildRefundStatusDTO(order *model.RentalOrder) *RefundStatusDTO {
|
|
return &RefundStatusDTO{
|
|
OrderID: order.ID,
|
|
OrderNo: order.OrderNo,
|
|
RefundStatus: order.RefundStatus,
|
|
RefundAmountCent: order.RefundAmountCent,
|
|
RefundedAt: order.RefundedAt,
|
|
TotalAmountCent: order.RentAmountCent + order.DepositAmountCent,
|
|
}
|
|
}
|
|
|
|
func isTerminalStatus(status string) bool {
|
|
return status == orderStatusCompleted || status == orderStatusCancelled || status == orderStatusClosed
|
|
}
|