118 lines
4.2 KiB
Go
Executable File
118 lines
4.2 KiB
Go
Executable File
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
|
||
}
|