chore(init): 初始化解忧商行项目并完成 Phase 0 基座
This commit is contained in:
@@ -0,0 +1,647 @@
|
||||
package order
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"time"
|
||||
|
||||
"hfb_sys/backend/internal/model"
|
||||
"hfb_sys/backend/internal/modules/distribution"
|
||||
"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 {
|
||||
// 客服常以「支付失败」关闭待支付订单,但渠道结果可能只是还没回来。
|
||||
// 先核对一次,已收款的订单直接推进,避免关单后钱收了却查无记录。
|
||||
if r.pendingPaymentOrderPaidAtChannelForAdmin(ctx, orderID) {
|
||||
return ErrOrderPaidAtChannel
|
||||
}
|
||||
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
|
||||
if err := archiveAssets(tx, listing, account); err != nil {
|
||||
return err
|
||||
}
|
||||
if beforeOrderStatus != orderStatusPendingPayment {
|
||||
totalCent := order.RentAmountCent + order.DepositAmountCent
|
||||
// 押金暂扣:若订单已暂扣,拦截退款中的押金部分挂起不退,其余照常原路退。
|
||||
refundAmountCent := order.ApplyDepositHold(totalCent, order.DepositAmountCent)
|
||||
action, err := r.prepareRefund(order, refundAmountCent, refundBizAdminClose, "客服关闭订单原路退款")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
refund = action
|
||||
} else {
|
||||
// 待支付订单照理没有款项,但渠道钱款迟到时支付单会被记成已支付。
|
||||
// 按「已收款减去已退款」退,漏掉就等于白收用户一笔钱。
|
||||
outstandingCent, err := outstandingPaidOrderPaymentCentTx(tx, order.ID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if outstandingCent > 0 {
|
||||
action, err := r.prepareRefund(order, outstandingCent, 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 order.DistributionSiteID != nil && *order.DistributionSiteID > 0 {
|
||||
if err := distribution.EnsureCompletedReleaseAtTx(tx, order.ID); 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
|
||||
if err := markAssetsAbnormal(tx, listing, account); err != nil {
|
||||
return err
|
||||
}
|
||||
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
|
||||
RecalculateEnd 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 target.RecalculateEnd {
|
||||
durationHours := estimateOrderDurationHours(order.AccountSnapshot)
|
||||
if durationHours > 0 {
|
||||
order.EstimatedDurationHours = durationHours
|
||||
}
|
||||
if order.RentedAt == nil {
|
||||
order.RentedAt = &now
|
||||
}
|
||||
if order.RentedAt != nil {
|
||||
estimatedEndAt := order.RentedAt.Add(time.Duration(order.EstimatedDurationHours) * time.Hour)
|
||||
if !estimatedEndAt.After(now) {
|
||||
order.RentedAt = &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.Status == orderStatusAbnormal) &&
|
||||
(order.HandoffStatus == handoffStatusOwnerTimeout || order.HandoffStatus == handoffStatusPendingOwner) {
|
||||
return adminResetTarget{
|
||||
OrderStatus: orderStatusPendingHandoff,
|
||||
HandoffStatus: handoffStatusPendingOwner,
|
||||
Label: "重置交接",
|
||||
OwnerTitle: "请重新提交交接说明",
|
||||
OwnerContent: "客服已重置交接状态,请尽快提交交接说明,避免再次超时。",
|
||||
RenterTitle: "订单交接已重置",
|
||||
RenterContent: "客服已重置交接状态,正在等待号主重新提交交接说明。",
|
||||
AuditAction: "order.reset_handoff",
|
||||
RefreshStageTime: true,
|
||||
}, true
|
||||
}
|
||||
if (order.Status == orderStatusPendingHandoff || order.Status == orderStatusAbnormal) &&
|
||||
(order.HandoffStatus == handoffStatusRenterConfirmTimeout || order.HandoffStatus == handoffStatusPendingRenterConfirm) {
|
||||
return adminResetTarget{
|
||||
OrderStatus: orderStatusPendingHandoff,
|
||||
HandoffStatus: handoffStatusPendingRenterConfirm,
|
||||
Label: "重置确认收号",
|
||||
OwnerTitle: "订单确认收号已重置",
|
||||
OwnerContent: "客服已重置确认收号状态,正在等待租客重新确认收号。",
|
||||
RenterTitle: "请重新确认收号",
|
||||
RenterContent: "客服已重置确认收号状态,请尽快检查账号并确认收号,避免再次超时。",
|
||||
AuditAction: "order.reset_renter_confirm",
|
||||
RefreshStageTime: true,
|
||||
}, true
|
||||
}
|
||||
if (order.Status == orderStatusPendingCheckoutConfirm || order.Status == orderStatusAbnormal) &&
|
||||
(order.HandoffStatus == handoffStatusOwnerCheckoutConfirmTimeout ||
|
||||
order.HandoffStatus == handoffStatusOwnerReturnConfirmTimeout ||
|
||||
order.HandoffStatus == handoffStatusPendingOwnerCheckout) {
|
||||
return adminResetTarget{
|
||||
OrderStatus: orderStatusPendingCheckoutConfirm,
|
||||
HandoffStatus: handoffStatusPendingOwnerCheckout,
|
||||
Label: "重置结账确认",
|
||||
OwnerTitle: "请重新确认结账",
|
||||
OwnerContent: "客服已重置结账确认状态,请尽快核对账号和结账金额,避免再次超时。",
|
||||
RenterTitle: "结账确认已重置",
|
||||
RenterContent: "客服已重置结账确认状态,正在等待号主重新确认结账。",
|
||||
AuditAction: "order.reset_checkout_confirm",
|
||||
RefreshStageTime: true,
|
||||
}, true
|
||||
}
|
||||
if (order.Status == orderStatusOverdue || order.Status == orderStatusRenting || order.Status == orderStatusAbnormal) &&
|
||||
order.HandoffStatus == handoffStatusReturnOverdue {
|
||||
return adminResetTarget{
|
||||
OrderStatus: orderStatusRenting,
|
||||
HandoffStatus: handoffStatusReceived,
|
||||
Label: "重置归还逾期",
|
||||
OwnerTitle: "订单归还逾期已重置",
|
||||
OwnerContent: "客服已重置归还逾期状态,订单恢复为使用中。",
|
||||
RenterTitle: "请及时发起结账",
|
||||
RenterContent: "客服已重置归还逾期状态,请尽快发起结账。",
|
||||
AuditAction: "order.reset_return_overdue",
|
||||
RecalculateEnd: true,
|
||||
}, true
|
||||
}
|
||||
return adminResetTarget{}, false
|
||||
}
|
||||
|
||||
// AdminRefund 触发后台人工退款,退款由 payment 模块走渠道原路退回。
|
||||
func (r *Repository) AdminRefund(ctx context.Context, orderID uint64) (*RefundStatusDTO, error) {
|
||||
db := r.db.WithContext(ctx)
|
||||
var refund *refundAction
|
||||
err := 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.RefundStatus == refundStatusRefunded {
|
||||
return nil
|
||||
}
|
||||
totalCent := order.RentAmountCent + order.DepositAmountCent
|
||||
if totalCent <= 0 {
|
||||
return ErrInvalidCheckoutAmount
|
||||
}
|
||||
// 押金暂扣:人工退款也必须尊重暂扣状态,避免绕过押金挂起直接全额退款。
|
||||
refundAmountCent := order.ApplyDepositHold(totalCent, order.DepositAmountCent)
|
||||
if refundAmountCent <= 0 {
|
||||
order.RefundStatus = "none"
|
||||
order.RefundAmountCent = 0
|
||||
return tx.Save(&order).Error
|
||||
}
|
||||
if r.refundStarter == nil {
|
||||
return ErrDependencyUnavailable
|
||||
}
|
||||
action, err := r.prepareRefund(&order, refundAmountCent, refundBizAdmin, "后台人工退款")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
refund = action
|
||||
return tx.Save(&order).Error
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
status := ""
|
||||
if refund != nil {
|
||||
var err error
|
||||
status, err = r.refundStarter.StartRefund(ctx, refund.OrderID, refund.RefundAmountCent, refund.BizType, refund.Remark)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
// 重新读取订单,拿到 payment 模块更新后的退款字段。
|
||||
var order model.RentalOrder
|
||||
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,
|
||||
}
|
||||
}
|
||||
|
||||
// AdminApproveRefund 客服审核通过退款申请,触发渠道原路退款。
|
||||
func (r *Repository) AdminApproveRefund(ctx context.Context, orderID uint64) 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 order.RefundStatus != refundStatusPendingReview {
|
||||
return errors.New("订单不处于待审核退款状态")
|
||||
}
|
||||
if order.RefundAmountCent <= 0 {
|
||||
return errors.New("退款金额无效")
|
||||
}
|
||||
// 押金暂扣:若订单已暂扣,拦截退款中的押金部分挂起不退,其余照常原路退。
|
||||
refundAmountCent := order.ApplyDepositHold(order.RefundAmountCent, order.DepositAmountCent)
|
||||
if refundAmountCent <= 0 {
|
||||
// 全部押金被暂扣且无其他可退金额,直接标记为已处理,等待后续手动退押金。
|
||||
order.RefundStatus = "none"
|
||||
order.RefundAmountCent = 0
|
||||
} else {
|
||||
action, err := r.prepareRefund(order, refundAmountCent, refundBizCancel, "取消订单原路退款(客服审核通过)")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
refund = action
|
||||
}
|
||||
if err := releaseAssetsForRentalIfIdle(tx, order, listing, account); 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
|
||||
}
|
||||
|
||||
// AdminRejectRefund 客服驳回退款申请。
|
||||
// action: "restore" 恢复订单至待交接继续流转;"close" 关闭订单并归档商品。
|
||||
func (r *Repository) AdminRejectRefund(ctx context.Context, orderID uint64, action string) 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 order.RefundStatus != refundStatusPendingReview {
|
||||
return errors.New("订单不处于待审核退款状态")
|
||||
}
|
||||
|
||||
order.RefundStatus = "none"
|
||||
order.RefundAmountCent = 0
|
||||
|
||||
switch action {
|
||||
case "restore":
|
||||
if order.Status != orderStatusCancelled {
|
||||
return errors.New("仅已取消的订单可以恢复")
|
||||
}
|
||||
active, err := hasActiveSiblingOrder(tx, order)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if active {
|
||||
return ErrListingUnavailable
|
||||
}
|
||||
order.Status = orderStatusPendingHandoff
|
||||
if order.HandoffStatus == handoffStatusCancelled || order.HandoffStatus == handoffStatusRenterConfirmTimeout {
|
||||
order.HandoffStatus = handoffStatusPendingOwner
|
||||
}
|
||||
order.SettlementStatus = settlementStatusUnsettled
|
||||
order.SettledAt = nil
|
||||
reserveListingForOrder(listing)
|
||||
if err := markAssetsRented(tx, listing, account); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
case "close":
|
||||
now := time.Now()
|
||||
order.Status = orderStatusClosed
|
||||
order.HandoffStatus = handoffStatusAdminClosed
|
||||
order.SettlementStatus = settlementStatusClosed
|
||||
order.SettledAt = &now
|
||||
if err := archiveAssets(tx, listing, account); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
default:
|
||||
return errors.New("无效的驳回操作,可选 restore 或 close")
|
||||
}
|
||||
|
||||
if err := tx.Save(order).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
// 关单同样属于可释放终态,补写释放时间,避免不退款关单的订单永不自动释放;
|
||||
// restore 分支订单未终结,不在此列。
|
||||
if order.Status == orderStatusClosed && order.DistributionSiteID != nil && *order.DistributionSiteID > 0 {
|
||||
if err := distribution.EnsureCompletedReleaseAtTx(tx, order.ID); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if err := tx.Save(listing).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
return tx.Save(account).Error
|
||||
})
|
||||
}
|
||||
|
||||
// AdminSeal 客服封存订单:关闭订单并全额原路退款,将关联商品/账号置为终态 sealed
|
||||
// (号主不可再编辑、提审、上架),同时在同一事务内解散关联群聊。
|
||||
// 适用于号主失联、无法继续交接,需要终止并永久封存该商品的场景。
|
||||
func (r *Repository) AdminSeal(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
|
||||
if err := sealAssets(tx, listing, account); err != nil {
|
||||
return err
|
||||
}
|
||||
if beforeOrderStatus != orderStatusPendingPayment {
|
||||
totalCent := order.RentAmountCent + order.DepositAmountCent
|
||||
// 押金暂扣:若订单已暂扣,拦截退款中的押金部分挂起不退,其余照常原路退。
|
||||
refundAmountCent := order.ApplyDepositHold(totalCent, order.DepositAmountCent)
|
||||
action, err := r.prepareRefund(order, refundAmountCent, refundBizAdminSeal, "客服封存订单原路退款")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
refund = action
|
||||
}
|
||||
if r.chatArchiver != nil {
|
||||
if err := r.chatArchiver.ArchiveListingConversation(tx, listing.ID, req.Reason); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
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_seal", "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 order.DistributionSiteID != nil && *order.DistributionSiteID > 0 {
|
||||
if err := distribution.EnsureCompletedReleaseAtTx(tx, order.ID); 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 isTerminalStatus(status string) bool {
|
||||
return status == orderStatusCompleted || status == orderStatusCancelled || status == orderStatusClosed
|
||||
}
|
||||
Reference in New Issue
Block a user