Files
hfb_sys/backend/internal/modules/order/admin_actions.go
T

333 lines
11 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
})
}
type adminResetTarget struct {
OrderStatus string
HandoffStatus string
Label string
OwnerTitle string
OwnerContent string
RenterTitle string
RenterContent string
AuditAction string
RefreshStageTime bool
}
// AdminResetHandoff 将超时卡住的订单恢复到对应待办阶段,并刷新阶段计时,
// 让客服可以给责任方再次处理机会,避免订单卡死在异常状态。
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
}
target, ok := resetTargetForOrder(order)
if !ok {
return ErrOrderCannotResetHandoff
}
beforeOrderStatus := order.Status
beforeHandoffStatus := order.HandoffStatus
now := time.Now()
order.Status = target.OrderStatus
order.HandoffStatus = target.HandoffStatus
if target.RefreshStageTime {
order.HandoffStartedAt = &now
}
if err := notification.Append(tx,
notification.Entry{
UserID: order.OwnerID,
Type: "order_admin",
Title: target.OwnerTitle,
Content: target.OwnerContent + "原因:" + req.Reason,
BizType: "order",
BizID: &order.ID,
},
notification.Entry{
UserID: order.RenterID,
Type: "order_admin",
Title: target.RenterTitle,
Content: target.RenterContent + "原因:" + req.Reason,
BizType: "order",
BizID: &order.ID,
},
); err != nil {
return err
}
if err := appendAuditLog(tx, adminID, target.AuditAction, "order", order.ID, meta, map[string]any{
"order_id": order.ID,
"order_no": order.OrderNo,
"reason": req.Reason,
"before_order_status": beforeOrderStatus,
"after_order_status": order.Status,
"before_handoff_status": beforeHandoffStatus,
"after_handoff_status": order.HandoffStatus,
}); err != nil {
return err
}
return tx.Save(&order).Error
})
}
func resetTargetForOrder(order model.RentalOrder) (adminResetTarget, bool) {
if order.Status == orderStatusPendingHandoff && order.HandoffStatus == handoffStatusOwnerTimeout {
return adminResetTarget{
OrderStatus: orderStatusPendingHandoff,
HandoffStatus: handoffStatusPendingOwner,
Label: "重置交接",
OwnerTitle: "请重新提交交接说明",
OwnerContent: "客服已重置交接状态,请尽快提交交接说明,避免再次超时。",
RenterTitle: "订单交接已重置",
RenterContent: "客服已重置交接状态,正在等待号主重新提交交接说明。",
AuditAction: "order.reset_handoff",
RefreshStageTime: true,
}, true
}
if order.Status == orderStatusAbnormal && order.HandoffStatus == handoffStatusRenterConfirmTimeout {
return adminResetTarget{
OrderStatus: orderStatusPendingHandoff,
HandoffStatus: handoffStatusPendingRenterConfirm,
Label: "重置确认收号",
OwnerTitle: "订单确认收号已重置",
OwnerContent: "客服已重置确认收号状态,正在等待租客重新确认收号。",
RenterTitle: "请重新确认收号",
RenterContent: "客服已重置确认收号状态,请尽快检查账号并确认收号,避免再次超时。",
AuditAction: "order.reset_renter_confirm",
RefreshStageTime: true,
}, true
}
if order.Status == orderStatusAbnormal &&
(order.HandoffStatus == handoffStatusOwnerCheckoutConfirmTimeout ||
order.HandoffStatus == handoffStatusOwnerReturnConfirmTimeout) {
return adminResetTarget{
OrderStatus: orderStatusPendingCheckoutConfirm,
HandoffStatus: handoffStatusPendingOwnerCheckout,
Label: "重置结账确认",
OwnerTitle: "请重新确认结账",
OwnerContent: "客服已重置结账确认状态,请尽快核对账号和结账金额,避免再次超时。",
RenterTitle: "结账确认已重置",
RenterContent: "客服已重置结账确认状态,正在等待号主重新确认结账。",
AuditAction: "order.reset_checkout_confirm",
RefreshStageTime: true,
}, true
}
return adminResetTarget{}, false
}
// 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
}