From 4b8fc76f36ba63109cf5edfd797f277884138976 Mon Sep 17 00:00:00 2001 From: yml2213 Date: Tue, 6 Oct 2026 17:47:02 +0800 Subject: [PATCH] =?UTF-8?q?feat(rock):=20=E7=BB=91=E5=AE=9A=E9=94=99?= =?UTF-8?q?=E8=AF=AF=E5=8F=AF=E7=9B=B4=E6=8E=A5=E6=94=B9=E4=B9=B0=E5=AE=B6?= =?UTF-8?q?=20UID=EF=BC=8C=E6=8F=90=E8=B4=A7=E5=8F=B7=E7=BB=A7=E7=BB=AD?= =?UTF-8?q?=E7=95=99=E5=9C=A8=E5=8E=9F=E8=AE=A2=E5=8D=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Cursor --- backend/internal/handler/rock.go | 32 +++++++ backend/internal/router/router.go | 1 + .../internal/service/provider_rock_test.go | 59 +++++++++++- backend/internal/service/rock_service.go | 85 +++++++++++++++++ frontend/src/api/index.ts | 2 + frontend/src/pages/PlatformRock.tsx | 95 ++++++++++++++++--- 6 files changed, 262 insertions(+), 12 deletions(-) diff --git a/backend/internal/handler/rock.go b/backend/internal/handler/rock.go index 5ba2fe0..ebd489f 100644 --- a/backend/internal/handler/rock.go +++ b/backend/internal/handler/rock.go @@ -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) diff --git a/backend/internal/router/router.go b/backend/internal/router/router.go index 51d7842..3a0fcdd 100644 --- a/backend/internal/router/router.go +++ b/backend/internal/router/router.go @@ -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) } diff --git a/backend/internal/service/provider_rock_test.go b/backend/internal/service/provider_rock_test.go index 57bd466..7e035dc 100644 --- a/backend/internal/service/provider_rock_test.go +++ b/backend/internal/service/provider_rock_test.go @@ -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) + } +} diff --git a/backend/internal/service/rock_service.go b/backend/internal/service/rock_service.go index 402d94f..eb07c41 100644 --- a/backend/internal/service/rock_service.go +++ b/backend/internal/service/rock_service.go @@ -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 diff --git a/frontend/src/api/index.ts b/frontend/src/api/index.ts index 1df179b..66c9105 100644 --- a/frontend/src/api/index.ts +++ b/frontend/src/api/index.ts @@ -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), } diff --git a/frontend/src/pages/PlatformRock.tsx b/frontend/src/pages/PlatformRock.tsx index 5876c75..2db98ef 100644 --- a/frontend/src/pages/PlatformRock.tsx +++ b/frontend/src/pages/PlatformRock.tsx @@ -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 ( - handleResetToken(record.id)} - > - - + handleResetToken(record.id)} + > + + + ) } if (record.status === 'using' && !record.bound_at && !record.actual_role_id) { @@ -1150,6 +1191,38 @@ export default function PlatformRock() { + void handleCorrectUid()} + onCancel={() => setUidEdit(null)} + destroyOnClose + > +

+ 提货号继续留在原订单上,不会放回库存。改成扫码 UID 后,绑定阻断会解除。 +

+ {uidEdit?.scanned ? ( +

+ 扫码角色:{uidEdit.roleName ? `「${uidEdit.roleName}」` : ''} {uidEdit.scanned} +

+ ) : null} + + setUidEdit((current) => + current + ? { ...current, value: event.target.value.replace(/\D/g, '').slice(0, 15) } + : current, + ) + } + onPressEnter={() => void handleCorrectUid()} + /> +
{/* 发货结果图片高清预览弹窗 */}