feat(rock): 绑定错误可直接改买家 UID,提货号继续留在原订单
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -110,6 +110,38 @@ func (h *RockHandler) ListTokens(c *gin.Context) {
|
||||
})
|
||||
}
|
||||
|
||||
type correctUIDReq struct {
|
||||
ExpectedUID string `json:"expected_uid"`
|
||||
}
|
||||
|
||||
// CorrectTokenUID 修改绑定错误订单上买家填写的 UID,不释放提货号。
|
||||
func (h *RockHandler) CorrectTokenUID(c *gin.Context) {
|
||||
idUint, err := strconv.ParseUint(c.Param("id"), 10, 64)
|
||||
if err != nil {
|
||||
response.BadRequest(c, "无效的 Token ID")
|
||||
return
|
||||
}
|
||||
var req correctUIDReq
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
response.BadRequest(c, "参数解析失败")
|
||||
return
|
||||
}
|
||||
token, err := h.rockSvc.CorrectMismatchExpectedUID(c.Request.Context(), uint(idUint), req.ExpectedUID)
|
||||
if err != nil {
|
||||
response.BadRequest(c, err.Error())
|
||||
return
|
||||
}
|
||||
message := "已更新买家 UID,提货号仍属于原订单"
|
||||
if token.Status == "bound" {
|
||||
message = "已改成扫码 UID,绑定阻断已解除,提货号仍属于原订单"
|
||||
}
|
||||
response.OK(c, gin.H{
|
||||
"message": message,
|
||||
"status": token.Status,
|
||||
"token": token,
|
||||
})
|
||||
}
|
||||
|
||||
// ResetToken 客服人工重置 mismatch 状态的 Token
|
||||
func (h *RockHandler) ResetToken(c *gin.Context) {
|
||||
idUint, err := strconv.ParseUint(c.Param("id"), 10, 64)
|
||||
|
||||
@@ -315,6 +315,7 @@ func Setup(h *Handlers) *gin.Engine {
|
||||
admin.GET("/platform/rock/stats", h.Rock.GetStats)
|
||||
admin.POST("/platform/rock/purchase", h.Rock.PurchaseTokens)
|
||||
admin.GET("/platform/rock/tokens", h.Rock.ListTokens)
|
||||
admin.POST("/platform/rock/tokens/:id/expected-uid", h.Rock.CorrectTokenUID)
|
||||
admin.POST("/platform/rock/tokens/:id/reset", h.Rock.ResetToken)
|
||||
admin.POST("/platform/rock/tokens/:id/release", h.Rock.ReleaseToken)
|
||||
}
|
||||
|
||||
@@ -706,7 +706,7 @@ func TestRockProvider_SchemeB_UIDSelfHealing(t *testing.T) {
|
||||
if strings.Contains(r.URL.Path, "/api/redeem/") {
|
||||
_ = json.NewEncoder(w).Encode(map[string]interface{}{
|
||||
"id": 1001,
|
||||
"bind_url": "https://igame.qq.com/activity/test?code=redeem_scheme_b",
|
||||
"bind_url": "https://igame.qq.com/activity/test?code=redeem_scheme_b",
|
||||
"redeem_code": "redeem_scheme_b",
|
||||
})
|
||||
return
|
||||
@@ -805,3 +805,60 @@ func TestRockProvider_SchemeB_UIDSelfHealing(t *testing.T) {
|
||||
t.Fatalf("整个流程该订单必须严格只占用 1 个 Token,实际占用: %d", count)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRockService_CorrectMismatchUIDKeepsAssignment(t *testing.T) {
|
||||
db := newRockTestDB(t)
|
||||
now := time.Now()
|
||||
token := model.RockToken{
|
||||
UpstreamTokenID: "179",
|
||||
Token: "token-179",
|
||||
RedeemCode: "redeem-179",
|
||||
Color: model.RockColorBlack,
|
||||
Status: model.RockStatusMismatch,
|
||||
AssignedOrderNo: "FO-KEEP-179",
|
||||
ExpectedUID: "3780457044",
|
||||
ActualRoleName: "德州火箭头",
|
||||
ActualRoleID: "3780451044",
|
||||
ActualChannel: "安卓-微信",
|
||||
MismatchAt: &now,
|
||||
MismatchReason: "预期UID(3780457044) 与 扫码UID(德州火箭头/3780451044) 不一致",
|
||||
}
|
||||
if err := db.Create(&token).Error; err != nil {
|
||||
t.Fatalf("create token: %v", err)
|
||||
}
|
||||
order := model.FulfillmentOrder{
|
||||
OrderNo: "FO-KEEP-179",
|
||||
MerchantID: 2,
|
||||
ProductSKU: model.RockSKUBlackTshirt,
|
||||
OrderStatus: "paid",
|
||||
ResultData: `{"expected_uid":"3780457044","provider":"rock"}`,
|
||||
}
|
||||
if err := db.Create(&order).Error; err != nil {
|
||||
t.Fatalf("create order: %v", err)
|
||||
}
|
||||
|
||||
rockSvc := NewRockService(db, NewRockClient("https://example.com"))
|
||||
kept, err := rockSvc.CorrectMismatchExpectedUID(context.Background(), token.ID, "1111111111")
|
||||
if err != nil {
|
||||
t.Fatalf("update unmatched uid: %v", err)
|
||||
}
|
||||
if kept.Status != model.RockStatusMismatch || kept.AssignedOrderNo != "FO-KEEP-179" || kept.ExpectedUID != "1111111111" {
|
||||
t.Fatalf("未对上扫码 UID 时应保持占用和阻断,实际 status=%s order=%s uid=%s", kept.Status, kept.AssignedOrderNo, kept.ExpectedUID)
|
||||
}
|
||||
|
||||
bound, err := rockSvc.CorrectMismatchExpectedUID(context.Background(), token.ID, "3780451044")
|
||||
if err != nil {
|
||||
t.Fatalf("correct to scanned uid: %v", err)
|
||||
}
|
||||
if bound.Status != model.RockStatusBound || bound.AssignedOrderNo != "FO-KEEP-179" || bound.ExpectedUID != "3780451044" {
|
||||
t.Fatalf("改成扫码 UID 后应解除阻断并留在原订单,实际 status=%s order=%s uid=%s", bound.Status, bound.AssignedOrderNo, bound.ExpectedUID)
|
||||
}
|
||||
|
||||
var savedOrder model.FulfillmentOrder
|
||||
if err := db.Where("order_no = ?", "FO-KEEP-179").First(&savedOrder).Error; err != nil {
|
||||
t.Fatalf("reload order: %v", err)
|
||||
}
|
||||
if !strings.Contains(savedOrder.ResultData, "3780451044") {
|
||||
t.Fatalf("订单期望 UID 应同步为扫码 UID,实际: %s", savedOrder.ResultData)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -800,6 +800,91 @@ func (s *RockService) ListTokens(ctx context.Context, page, pageSize int, status
|
||||
return tokens, total, nil
|
||||
}
|
||||
|
||||
// CorrectMismatchExpectedUID 客服修改买家填写的 UID,提货号继续留在原订单上。
|
||||
// 改成当前扫码角色的 UID 时解除阻断并记为已绑定;改成其他 UID 只更新期望值,仍保持阻断。
|
||||
func (s *RockService) CorrectMismatchExpectedUID(ctx context.Context, tokenID uint, expectedUID string) (*model.RockToken, error) {
|
||||
trimmedUID := strings.TrimSpace(expectedUID)
|
||||
if trimmedUID == "" || !isDigits(trimmedUID) || len(trimmedUID) > 15 {
|
||||
return nil, errors.New("请填写 15 位以内的纯数字 UID")
|
||||
}
|
||||
|
||||
var token model.RockToken
|
||||
err := s.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
|
||||
if err := tx.Clauses(clause.Locking{Strength: "UPDATE"}).First(&token, tokenID).Error; err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return errors.New("提货号不存在")
|
||||
}
|
||||
return err
|
||||
}
|
||||
if token.Status != model.RockStatusMismatch {
|
||||
return errors.New("仅绑定错误的提货号可以修改买家 UID")
|
||||
}
|
||||
if strings.TrimSpace(token.AssignedOrderNo) == "" {
|
||||
return errors.New("该提货号没有关联订单,不能修改 UID")
|
||||
}
|
||||
|
||||
now := time.Now()
|
||||
actualUID := strings.TrimSpace(token.ActualRoleID)
|
||||
updates := map[string]interface{}{
|
||||
"expected_uid": trimmedUID,
|
||||
"updated_at": now,
|
||||
}
|
||||
if actualUID != "" && trimmedUID == actualUID {
|
||||
updates["status"] = model.RockStatusBound
|
||||
updates["mismatch_reason"] = ""
|
||||
updates["mismatch_at"] = nil
|
||||
updates["bound_at"] = now
|
||||
token.Status = model.RockStatusBound
|
||||
token.MismatchReason = ""
|
||||
token.MismatchAt = nil
|
||||
token.BoundAt = &now
|
||||
} else if actualUID != "" {
|
||||
updates["mismatch_reason"] = fmt.Sprintf("预期UID(%s) 与 扫码UID(%s/%s) 不一致", trimmedUID, token.ActualRoleName, actualUID)
|
||||
token.MismatchReason = updates["mismatch_reason"].(string)
|
||||
}
|
||||
if err := tx.Model(&token).Updates(updates).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
token.ExpectedUID = trimmedUID
|
||||
|
||||
var order model.FulfillmentOrder
|
||||
if err := tx.Where("order_no = ?", token.AssignedOrderNo).First(&order).Error; err == nil {
|
||||
data := resultDataMap(order.ResultData)
|
||||
data["expected_uid"] = trimmedUID
|
||||
raw, marshalErr := json.Marshal(data)
|
||||
if marshalErr != nil {
|
||||
return marshalErr
|
||||
}
|
||||
if err := tx.Model(&order).Update("result_data", string(raw)).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return writeAudit(tx, nil, nil, nil, "rock_token.correct_expected_uid", "rock_token", fmt.Sprintf("%d", token.ID), map[string]interface{}{
|
||||
"assigned_order_no": token.AssignedOrderNo,
|
||||
"expected_uid": trimmedUID,
|
||||
"actual_role_id": actualUID,
|
||||
"status": token.Status,
|
||||
})
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &token, nil
|
||||
}
|
||||
|
||||
func isDigits(value string) bool {
|
||||
if value == "" {
|
||||
return false
|
||||
}
|
||||
for _, r := range value {
|
||||
if r < '0' || r > '9' {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// ResetMismatchToken 客服人工重置解冻绑定错误的 Token
|
||||
func (s *RockService) ResetMismatchToken(ctx context.Context, tokenID uint) error {
|
||||
var token model.RockToken
|
||||
|
||||
@@ -326,6 +326,8 @@ export const rockApi = {
|
||||
purchaseTokens: (count = 50) => request.post('/platform/rock/purchase', { count }).then((r) => r.data.data),
|
||||
listTokens: (params?: { page?: number; page_size?: number; status?: string; search?: string }) =>
|
||||
request.get('/platform/rock/tokens', { params }).then((r) => r.data.data as { list: RockTokenItem[]; total: number; page: number; page_size: number }),
|
||||
correctExpectedUid: (id: number, expectedUid: string) =>
|
||||
request.post(`/platform/rock/tokens/${id}/expected-uid`, { expected_uid: expectedUid }).then((r) => r.data.data as { message?: string; status?: string }),
|
||||
resetToken: (id: number) => request.post(`/platform/rock/tokens/${id}/reset`).then((r) => r.data.data),
|
||||
releaseToken: (id: number) => request.post(`/platform/rock/tokens/${id}/release`).then((r) => r.data.data),
|
||||
}
|
||||
|
||||
@@ -69,6 +69,8 @@ export default function PlatformRock() {
|
||||
const [loadingTokens, setLoadingTokens] = useState(false)
|
||||
|
||||
const [form] = Form.useForm()
|
||||
const [uidEdit, setUidEdit] = useState<{ id: number; value: string; scanned: string; roleName: string } | null>(null)
|
||||
const [savingUid, setSavingUid] = useState(false)
|
||||
|
||||
const loadData = useCallback(async () => {
|
||||
setLoadingStats(true)
|
||||
@@ -206,6 +208,29 @@ export default function PlatformRock() {
|
||||
}
|
||||
}
|
||||
|
||||
const handleCorrectUid = async () => {
|
||||
if (!uidEdit) {
|
||||
return
|
||||
}
|
||||
const nextUid = uidEdit.value.trim()
|
||||
if (!/^\d{1,15}$/.test(nextUid)) {
|
||||
message.warning('请填写 15 位以内的纯数字 UID')
|
||||
return
|
||||
}
|
||||
setSavingUid(true)
|
||||
try {
|
||||
const res = await rockApi.correctExpectedUid(uidEdit.id, nextUid)
|
||||
message.success(res.message || '已更新买家 UID')
|
||||
setUidEdit(null)
|
||||
loadData()
|
||||
loadTokens()
|
||||
} catch (e) {
|
||||
message.error('修改 UID 失败: ' + (e instanceof Error ? e.message : '未知错误'))
|
||||
} finally {
|
||||
setSavingUid(false)
|
||||
}
|
||||
}
|
||||
|
||||
// 人工重置解冻 Mismatch Token
|
||||
const handleResetToken = async (id: number) => {
|
||||
try {
|
||||
@@ -542,22 +567,38 @@ export default function PlatformRock() {
|
||||
{
|
||||
title: '操作',
|
||||
key: 'action',
|
||||
width: 140,
|
||||
width: 168,
|
||||
fixed: 'right',
|
||||
render: (_, record) => {
|
||||
if (record.status === 'mismatch') {
|
||||
return (
|
||||
<Popconfirm
|
||||
title="确定解冻重置该 Token?"
|
||||
description="重置后将清除原订单绑定与错误标记,重新释放为可用通用库存。"
|
||||
okText="确认解冻"
|
||||
cancelText="取消"
|
||||
onConfirm={() => handleResetToken(record.id)}
|
||||
>
|
||||
<Button size="small" type="primary" danger ghost icon={<UndoOutlined />}>
|
||||
解冻重置
|
||||
<Space direction="vertical" size={4}>
|
||||
<Button
|
||||
size="small"
|
||||
type="primary"
|
||||
onClick={() =>
|
||||
setUidEdit({
|
||||
id: record.id,
|
||||
value: record.actual_role_id || record.expected_uid || '',
|
||||
scanned: record.actual_role_id || '',
|
||||
roleName: record.actual_role_name || '',
|
||||
})
|
||||
}
|
||||
>
|
||||
修改 UID
|
||||
</Button>
|
||||
</Popconfirm>
|
||||
<Popconfirm
|
||||
title="确定解冻重置该 Token?"
|
||||
description="重置后将清除原订单绑定与错误标记,重新释放为可用通用库存。已扫码的链接放回库存后,其他订单领走就会废掉。"
|
||||
okText="确认解冻"
|
||||
cancelText="取消"
|
||||
onConfirm={() => handleResetToken(record.id)}
|
||||
>
|
||||
<Button size="small" danger ghost icon={<UndoOutlined />}>
|
||||
解冻重置
|
||||
</Button>
|
||||
</Popconfirm>
|
||||
</Space>
|
||||
)
|
||||
}
|
||||
if (record.status === 'using' && !record.bound_at && !record.actual_role_id) {
|
||||
@@ -1150,6 +1191,38 @@ export default function PlatformRock() {
|
||||
</div>
|
||||
</div>
|
||||
</Modal>
|
||||
<Modal
|
||||
title="修改买家填写的 UID"
|
||||
open={Boolean(uidEdit)}
|
||||
okText="保存"
|
||||
cancelText="取消"
|
||||
confirmLoading={savingUid}
|
||||
onOk={() => void handleCorrectUid()}
|
||||
onCancel={() => setUidEdit(null)}
|
||||
destroyOnClose
|
||||
>
|
||||
<p style={{ marginTop: 0, color: '#64748b' }}>
|
||||
提货号继续留在原订单上,不会放回库存。改成扫码 UID 后,绑定阻断会解除。
|
||||
</p>
|
||||
{uidEdit?.scanned ? (
|
||||
<p>
|
||||
扫码角色:{uidEdit.roleName ? `「${uidEdit.roleName}」` : ''} {uidEdit.scanned}
|
||||
</p>
|
||||
) : null}
|
||||
<Input
|
||||
value={uidEdit?.value || ''}
|
||||
maxLength={15}
|
||||
placeholder="买家游戏 UID"
|
||||
onChange={(event) =>
|
||||
setUidEdit((current) =>
|
||||
current
|
||||
? { ...current, value: event.target.value.replace(/\D/g, '').slice(0, 15) }
|
||||
: current,
|
||||
)
|
||||
}
|
||||
onPressEnter={() => void handleCorrectUid()}
|
||||
/>
|
||||
</Modal>
|
||||
{/* 发货结果图片高清预览弹窗 */}
|
||||
<Modal
|
||||
title={
|
||||
|
||||
Reference in New Issue
Block a user