363 lines
12 KiB
Go
363 lines
12 KiB
Go
package snapshot
|
||
|
||
import (
|
||
"context"
|
||
"encoding/json"
|
||
"errors"
|
||
"fmt"
|
||
"time"
|
||
|
||
"bindbox-game/internal/pkg/pay"
|
||
"bindbox-game/internal/repository/mysql"
|
||
"bindbox-game/internal/repository/mysql/model"
|
||
|
||
"gorm.io/gorm"
|
||
)
|
||
|
||
// RollbackResult 回滚结果
|
||
type RollbackResult struct {
|
||
Success bool `json:"success"`
|
||
RollbackLogID int64 `json:"rollback_log_id"`
|
||
PointsRestored int64 `json:"points_restored"`
|
||
CouponsRestored int `json:"coupons_restored"`
|
||
ItemCardsRestored int `json:"item_cards_restored"`
|
||
InventoryRevoked int `json:"inventory_revoked"`
|
||
RefundAmount int64 `json:"refund_amount"`
|
||
SubsequentOrdersCount int `json:"subsequent_orders_count"`
|
||
SubsequentOrders []string `json:"subsequent_orders"`
|
||
RedemptionsReverted int `json:"redemptions_reverted"`
|
||
ErrorMsg string `json:"error_msg,omitempty"`
|
||
}
|
||
|
||
// RollbackService 回滚服务
|
||
type RollbackService interface {
|
||
// ExecuteRollback 执行时间点完整回滚
|
||
ExecuteRollback(ctx context.Context, orderID int64, operatorID int64, operatorName string, reason string) (*RollbackResult, error)
|
||
}
|
||
|
||
type rollbackService struct {
|
||
db mysql.Repo
|
||
snapshot Service
|
||
}
|
||
|
||
// NewRollbackService 创建回滚服务
|
||
func NewRollbackService(db mysql.Repo, snapshot Service) RollbackService {
|
||
return &rollbackService{
|
||
db: db,
|
||
snapshot: snapshot,
|
||
}
|
||
}
|
||
|
||
// ExecuteRollback 执行时间点完整回滚
|
||
// 回滚到目标订单消费前的状态,撤销该时间点之后的所有变化
|
||
func (s *rollbackService) ExecuteRollback(ctx context.Context, orderID int64, operatorID int64, operatorName string, reason string) (*RollbackResult, error) {
|
||
result := &RollbackResult{}
|
||
|
||
// 1. 查询目标订单
|
||
var order model.Orders
|
||
if err := s.db.GetDbR().WithContext(ctx).Where("id = ?", orderID).First(&order).Error; err != nil {
|
||
return nil, fmt.Errorf("订单不存在: %w", err)
|
||
}
|
||
|
||
// 2. 检查订单状态(只有已支付状态可以回滚)
|
||
if order.Status != 2 {
|
||
return nil, errors.New("订单状态不允许回滚,只有已支付订单可以回滚")
|
||
}
|
||
|
||
// 3. 检查是否已有回滚记录
|
||
var existingRollback model.AuditRollbackLogs
|
||
if err := s.db.GetDbR().WithContext(ctx).Where("order_id = ? AND status = 1", orderID).First(&existingRollback).Error; err == nil {
|
||
return nil, errors.New("该订单已经执行过回滚")
|
||
}
|
||
|
||
// 4. 获取快照
|
||
beforeSnap, afterSnap, err := s.snapshot.GetSnapshotsByOrderID(ctx, orderID)
|
||
if err != nil {
|
||
return nil, fmt.Errorf("获取快照失败: %w", err)
|
||
}
|
||
if beforeSnap == nil {
|
||
return nil, errors.New("未找到消费前快照")
|
||
}
|
||
|
||
// 5. 解析目标状态
|
||
var targetState UserStateSnapshot
|
||
if err := json.Unmarshal([]byte(beforeSnap.SnapshotData), &targetState); err != nil {
|
||
return nil, fmt.Errorf("解析快照失败: %w", err)
|
||
}
|
||
|
||
// 获取快照时间点
|
||
snapshotTime := beforeSnap.CreatedAt
|
||
|
||
// 6. 开始事务执行时间点完整回滚
|
||
var rollbackLogID int64
|
||
changesApplied := map[string]any{}
|
||
var totalRefundAmount int64
|
||
|
||
err = s.db.GetDbW().WithContext(ctx).Transaction(func(tx *gorm.DB) error {
|
||
userID := order.UserID
|
||
|
||
// ============ 步骤1: 查询并回滚该时间点之后的所有订单 ============
|
||
var subsequentOrders []model.Orders
|
||
tx.Where("user_id = ? AND id > ? AND status = 2", userID, orderID).Order("id DESC").Find(&subsequentOrders)
|
||
|
||
subsequentOrderNos := []string{}
|
||
for _, subOrder := range subsequentOrders {
|
||
subsequentOrderNos = append(subsequentOrderNos, subOrder.OrderNo)
|
||
|
||
// 作废后续订单产生的资产
|
||
tx.Model(&model.UserInventory{}).Where("order_id = ? AND status IN (1,3)", subOrder.ID).Update("status", 2)
|
||
|
||
// 恢复后续订单使用的优惠券
|
||
if subOrder.CouponID > 0 {
|
||
tx.Model(&model.UserCoupons{}).Where("id = ?", subOrder.CouponID).Updates(map[string]any{
|
||
"status": 1,
|
||
"used_order_id": 0,
|
||
"used_at": nil,
|
||
})
|
||
}
|
||
|
||
// 恢复后续订单使用的道具卡
|
||
if subOrder.ItemCardID > 0 {
|
||
tx.Model(&model.UserItemCards{}).Where("id = ?", subOrder.ItemCardID).Updates(map[string]any{
|
||
"status": 1,
|
||
"used_draw_log_id": 0,
|
||
"used_activity_id": 0,
|
||
"used_issue_id": 0,
|
||
"used_at": nil,
|
||
})
|
||
}
|
||
|
||
// 取消发货记录
|
||
tx.Model(&model.ShippingRecords{}).Where("order_id = ?", subOrder.ID).Update("status", 5)
|
||
|
||
// 更新订单状态为已回滚
|
||
tx.Model(&subOrder).Update("status", 5)
|
||
|
||
// 累计退款金额
|
||
if subOrder.ActualAmount > 0 {
|
||
totalRefundAmount += subOrder.ActualAmount
|
||
}
|
||
}
|
||
result.SubsequentOrdersCount = len(subsequentOrders)
|
||
result.SubsequentOrders = subsequentOrderNos
|
||
changesApplied["subsequent_orders"] = subsequentOrderNos
|
||
|
||
// ============ 步骤2: 撤销该时间点之后的积分兑换记录 ============
|
||
// 查询资产兑换记录(status=3表示已兑换)
|
||
var redeemedInventories []model.UserInventory
|
||
tx.Where("user_id = ? AND status = 3 AND updated_at > ?", userID, snapshotTime).Find(&redeemedInventories)
|
||
for _, inv := range redeemedInventories {
|
||
// 恢复资产状态为持有
|
||
tx.Model(&inv).Update("status", 1)
|
||
result.RedemptionsReverted++
|
||
}
|
||
changesApplied["redemptions_reverted"] = result.RedemptionsReverted
|
||
|
||
// ============ 步骤3: 撤销该时间点之后的优惠券兑换 ============
|
||
// 查询该时间点之后获取的优惠券(积分兑换的)
|
||
var redeemedCoupons []model.UserCoupons
|
||
tx.Where("user_id = ? AND created_at > ? AND status = 1 AND source LIKE '%redeem%'", userID, snapshotTime).Find(&redeemedCoupons)
|
||
for _, coupon := range redeemedCoupons {
|
||
// 作废兑换的优惠券
|
||
tx.Model(&coupon).Update("status", 3) // 过期/作废
|
||
}
|
||
changesApplied["coupons_voided"] = len(redeemedCoupons)
|
||
|
||
// ============ 步骤4: 撤销该时间点之后的道具卡兑换 ============
|
||
var redeemedCards []model.UserItemCards
|
||
tx.Where("user_id = ? AND created_at > ? AND status = 1 AND source LIKE '%redeem%'", userID, snapshotTime).Find(&redeemedCards)
|
||
for _, card := range redeemedCards {
|
||
// 作废兑换的道具卡
|
||
tx.Model(&card).Update("status", 3)
|
||
}
|
||
changesApplied["item_cards_voided"] = len(redeemedCards)
|
||
|
||
// ============ 步骤5: 恢复目标订单的优惠券 ============
|
||
if order.CouponID > 0 {
|
||
for _, c := range targetState.Coupons {
|
||
if c.UserCouponID == order.CouponID {
|
||
updates := map[string]any{
|
||
"status": c.Status,
|
||
"balance_amount": c.BalanceAmount,
|
||
"used_order_id": 0,
|
||
"used_at": nil,
|
||
}
|
||
if err := tx.Model(&model.UserCoupons{}).Where("id = ?", order.CouponID).Updates(updates).Error; err != nil {
|
||
return fmt.Errorf("恢复优惠券失败: %w", err)
|
||
}
|
||
result.CouponsRestored++
|
||
break
|
||
}
|
||
}
|
||
if result.CouponsRestored == 0 {
|
||
updates := map[string]any{
|
||
"status": 1,
|
||
"used_order_id": 0,
|
||
"used_at": nil,
|
||
}
|
||
if err := tx.Model(&model.UserCoupons{}).Where("id = ?", order.CouponID).Updates(updates).Error; err == nil {
|
||
result.CouponsRestored++
|
||
}
|
||
}
|
||
}
|
||
changesApplied["coupons_restored"] = result.CouponsRestored
|
||
|
||
// ============ 步骤6: 恢复目标订单的道具卡 ============
|
||
if order.ItemCardID > 0 {
|
||
for _, c := range targetState.ItemCards {
|
||
if c.UserItemCardID == order.ItemCardID {
|
||
updates := map[string]any{
|
||
"status": c.Status,
|
||
"used_draw_log_id": 0,
|
||
"used_activity_id": 0,
|
||
"used_issue_id": 0,
|
||
"used_at": nil,
|
||
}
|
||
if err := tx.Model(&model.UserItemCards{}).Where("id = ?", order.ItemCardID).Updates(updates).Error; err != nil {
|
||
return fmt.Errorf("恢复道具卡失败: %w", err)
|
||
}
|
||
result.ItemCardsRestored++
|
||
break
|
||
}
|
||
}
|
||
if result.ItemCardsRestored == 0 {
|
||
updates := map[string]any{
|
||
"status": 1,
|
||
"used_draw_log_id": 0,
|
||
"used_activity_id": 0,
|
||
"used_issue_id": 0,
|
||
"used_at": nil,
|
||
}
|
||
if err := tx.Model(&model.UserItemCards{}).Where("id = ?", order.ItemCardID).Updates(updates).Error; err == nil {
|
||
result.ItemCardsRestored++
|
||
}
|
||
}
|
||
}
|
||
changesApplied["item_cards_restored"] = result.ItemCardsRestored
|
||
|
||
// ============ 步骤7: 作废目标订单产生的资产 ============
|
||
updateRes := tx.Model(&model.UserInventory{}).Where("order_id = ? AND status IN (1,3)", orderID).Update("status", 2)
|
||
if updateRes.Error != nil {
|
||
return fmt.Errorf("作废资产失败: %w", updateRes.Error)
|
||
}
|
||
result.InventoryRevoked = int(updateRes.RowsAffected)
|
||
changesApplied["inventory_revoked"] = result.InventoryRevoked
|
||
|
||
// ============ 步骤8: 取消目标订单发货记录 ============
|
||
tx.Model(&model.ShippingRecords{}).Where("order_id = ?", orderID).Update("status", 5)
|
||
|
||
// ============ 步骤9: 恢复积分到目标状态 ============
|
||
if targetState.Points != nil {
|
||
var currentPoints model.UserPoints
|
||
if err := tx.Where("user_id = ?", userID).First(¤tPoints).Error; err == nil {
|
||
pointsDiff := targetState.Points.Balance - currentPoints.Points
|
||
if pointsDiff != 0 {
|
||
if err := tx.Model(¤tPoints).Update("points", targetState.Points.Balance).Error; err != nil {
|
||
return fmt.Errorf("恢复积分失败: %w", err)
|
||
}
|
||
// 记录积分流水
|
||
ledger := &model.UserPointsLedger{
|
||
UserID: userID,
|
||
Action: "rollback_restore",
|
||
Points: pointsDiff,
|
||
RefTable: "orders",
|
||
RefID: order.OrderNo,
|
||
Remark: fmt.Sprintf("时间点回滚恢复积分(含后续%d单): %s", len(subsequentOrders), reason),
|
||
}
|
||
if err := tx.Create(ledger).Error; err != nil {
|
||
return fmt.Errorf("记录积分流水失败: %w", err)
|
||
}
|
||
result.PointsRestored = pointsDiff
|
||
changesApplied["points_restored"] = pointsDiff
|
||
}
|
||
}
|
||
}
|
||
|
||
// ============ 步骤10: 更新目标订单状态为已回滚 ============
|
||
if err := tx.Model(&order).Update("status", 5).Error; err != nil {
|
||
return fmt.Errorf("更新订单状态失败: %w", err)
|
||
}
|
||
|
||
// 累计目标订单退款金额
|
||
if order.ActualAmount > 0 {
|
||
totalRefundAmount += order.ActualAmount
|
||
}
|
||
|
||
// ============ 步骤11: 记录回滚日志 ============
|
||
changesJSON, _ := json.Marshal(changesApplied)
|
||
var beforeSnapID, afterSnapID int64
|
||
if beforeSnap != nil {
|
||
beforeSnapID = beforeSnap.ID
|
||
}
|
||
if afterSnap != nil {
|
||
afterSnapID = afterSnap.ID
|
||
}
|
||
rollbackLog := &model.AuditRollbackLogs{
|
||
OrderID: orderID,
|
||
OrderNo: order.OrderNo,
|
||
UserID: userID,
|
||
OperatorID: operatorID,
|
||
OperatorName: operatorName,
|
||
RollbackType: 2, // 2=时间点完整回滚
|
||
BeforeSnapshotID: afterSnapID,
|
||
AfterSnapshotID: beforeSnapID,
|
||
ChangesApplied: string(changesJSON),
|
||
Reason: reason,
|
||
Status: 1,
|
||
}
|
||
if err := tx.Create(rollbackLog).Error; err != nil {
|
||
return fmt.Errorf("记录回滚日志失败: %w", err)
|
||
}
|
||
rollbackLogID = rollbackLog.ID
|
||
|
||
return nil
|
||
})
|
||
|
||
if err != nil {
|
||
result.Success = false
|
||
result.ErrorMsg = err.Error()
|
||
return result, err
|
||
}
|
||
|
||
// ============ 步骤12: 调用微信退款(事务外执行) ============
|
||
if totalRefundAmount > 0 {
|
||
// 为每个需要退款的订单发起退款
|
||
s.refundOrders(ctx, order.UserID, orderID, reason, result)
|
||
}
|
||
|
||
result.Success = true
|
||
result.RollbackLogID = rollbackLogID
|
||
|
||
return result, nil
|
||
}
|
||
|
||
// refundOrders 为回滚的订单发起退款
|
||
func (s *rollbackService) refundOrders(ctx context.Context, userID int64, startOrderID int64, reason string, result *RollbackResult) {
|
||
// 查询需要退款的订单(状态已变为5=已回滚)
|
||
var ordersToRefund []model.Orders
|
||
s.db.GetDbR().WithContext(ctx).Where("user_id = ? AND id >= ? AND status = 5 AND actual_amount > 0", userID, startOrderID).Find(&ordersToRefund)
|
||
|
||
wc, err := pay.NewWechatPayClient(ctx)
|
||
if err != nil {
|
||
return
|
||
}
|
||
|
||
for _, refundOrder := range ordersToRefund {
|
||
refundNo := fmt.Sprintf("RB%s-%d", refundOrder.OrderNo, time.Now().Unix())
|
||
_, status, err := wc.RefundOrder(ctx, refundOrder.OrderNo, refundNo, refundOrder.ActualAmount, refundOrder.ActualAmount, fmt.Sprintf("时间点回滚: %s", reason))
|
||
if err == nil {
|
||
refundRecord := &model.PaymentRefunds{
|
||
OrderID: refundOrder.ID,
|
||
OrderNo: refundOrder.OrderNo,
|
||
RefundNo: refundNo,
|
||
Channel: "wechat_jsapi",
|
||
Status: status,
|
||
AmountRefund: refundOrder.ActualAmount,
|
||
Reason: fmt.Sprintf("时间点回滚: %s", reason),
|
||
}
|
||
s.db.GetDbW().WithContext(ctx).Create(refundRecord)
|
||
result.RefundAmount += refundOrder.ActualAmount
|
||
}
|
||
}
|
||
}
|