bindbox-game/internal/service/user/cancel_shipping.go

118 lines
4.2 KiB
Go
Executable File
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

package user
import (
"context"
"fmt"
"bindbox-game/internal/repository/mysql/dao"
)
// CancelShipping 取消发货申请
// 支持按单个资产ID取消或按批次号批量取消
// 返回成功取消的记录数
//
// 转赠场景说明A 赠送 B 后shipping_records.user_id 和 user_inventory.user_id
// 均已更新为 B因此 B 可以正常取消A 无法取消shipping_record 属于 B查不到
func (s *service) CancelShipping(ctx context.Context, userID int64, inventoryID int64, batchNo string) (int64, error) {
var cancelledCount int64
err := s.writeDB.Transaction(func(tx *dao.Query) error {
var records []*struct {
ID int64
InventoryID int64
// 记录 shipping_record 中存储的实际 user_id用于后续恢复 inventory
RecordUserID int64
}
// 根据参数查询待取消的发货记录
if batchNo != "" {
// 按批次号查询:仅允许取消属于自己的发货记录
rows, err := tx.ShippingRecords.WithContext(ctx).
Select(tx.ShippingRecords.ID, tx.ShippingRecords.InventoryID, tx.ShippingRecords.UserID).
Where(tx.ShippingRecords.BatchNo.Eq(batchNo)).
Where(tx.ShippingRecords.UserID.Eq(userID)).
Where(tx.ShippingRecords.Status.Eq(1)). // 待发货状态
Find()
if err != nil {
return fmt.Errorf("query shipping records failed: %w", err)
}
for _, r := range rows {
records = append(records, &struct {
ID int64
InventoryID int64
RecordUserID int64
}{ID: r.ID, InventoryID: r.InventoryID, RecordUserID: r.UserID})
}
} else if inventoryID > 0 {
// 按单个资产ID查询先不过滤 user_id取到记录后比对归属
// 避免两次 DB 查询,同时能精确区分"不存在"和"无权限"两种情况
sr, err := tx.ShippingRecords.WithContext(ctx).
Where(tx.ShippingRecords.InventoryID.Eq(inventoryID)).
Where(tx.ShippingRecords.Status.Eq(1)).
First()
if err != nil {
return fmt.Errorf("shipping record not found or already processed")
}
if sr.UserID != userID {
// 转赠场景下 shipping_record.user_id = B受赠方A 无权取消
return fmt.Errorf("no_permission: shipping record belongs to another user")
}
records = append(records, &struct {
ID int64
InventoryID int64
RecordUserID int64
}{ID: sr.ID, InventoryID: sr.InventoryID, RecordUserID: sr.UserID})
}
if len(records) == 0 {
return fmt.Errorf("no pending shipping records found")
}
// 批量处理每条记录
for _, rec := range records {
// 1. 更新发货记录状态为已取消 (status=5)
res, err := tx.ShippingRecords.WithContext(ctx).
Where(tx.ShippingRecords.ID.Eq(rec.ID)).
Where(tx.ShippingRecords.Status.Eq(1)). // 防止并发重复取消
Update(tx.ShippingRecords.Status, 5)
if err != nil {
return fmt.Errorf("update shipping record status failed: %w", err)
}
if res.RowsAffected == 0 {
// 并发场景:记录已被其他请求取消,跳过
continue
}
// 2. 恢复库存状态为可用 (status=1) 并清空 shipping_no
// 关键WHERE user_id 使用 rec.RecordUserID即 shipping_record 中记录的归属人)
// 而不是函数参数 userID保证转赠场景下 inventory.user_id 与条件匹配。
// 在转赠场景中 rec.RecordUserID == B == inventory.user_id两者一致。
remark := fmt.Sprintf("|shipping_cancelled_by_user:%d", userID)
dbResult := tx.UserInventory.WithContext(ctx).UnderlyingDB().Exec(
"UPDATE user_inventory SET status=1, shipping_no='', remark=CONCAT(IFNULL(remark,''), ?) WHERE id=? AND user_id=?",
remark,
rec.InventoryID,
rec.RecordUserID, // 使用 shipping_record 中记录的 user_id而非调用方 userID
)
if dbResult.Error != nil {
return fmt.Errorf("restore inventory status failed: %w", dbResult.Error)
}
if dbResult.RowsAffected == 0 {
// inventory 未能恢复,强制回滚事务,防止数据不一致(物品"丢失"
return fmt.Errorf("restore inventory failed: inventory id=%d user_id=%d not matched or status unexpected",
rec.InventoryID, rec.RecordUserID)
}
cancelledCount++
}
return nil
})
if err != nil {
return 0, err
}
return cancelledCount, nil
}