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 }