feat(rock): 发货记录表格新增时间与备注列并支持上报失效联动
- 做了什么: 1. 数据库新增 013_rock_tokens_remark.sql 迁移,rock_tokens 表补充 remark 字段并在 ListTokens 中支持备注模糊检索; 2. 后端扩展 UpdateTokenRemark、MarkTokenAbnormal 接口,同时优化 ResetMismatchToken 支持 mismatch 与 abnormal 状态的解冻重置; 3. 前端 RockTokenTable 新增「流转时间」列(入库/绑错/发货/绑定/提货)与「备注」列(TokenRemarkCell 浮窗快捷预设选择与自定义输入); 4. 快捷选择「已上报」或点击「上报失效」自动将链接标记为 abnormal 作废,从可用库存池中剔除,防止被新订单领走; 5. 优化操作列与顶部筛选栏:mismatch 增加「上报失效」与警示重置;abnormal 状态明确显示已失效并提供强制恢复;筛选栏新增「已上报失效」Tab。 - 为什么: 解决绑定错误上报 0868 上游后无法记录备注、且原「解冻重置」容易误将已作废卡密放回可用库存池导致后续订单报错的问题。 - 验证方式: 1. 后端单元测试 TestRockService_TokenRemarkAndAbnormal 与 TestRock 测试集通过; 2. make fmt、go vet 通过; 3. 前端 oxlint 检查通过 (0 errors, 0 warnings),npm run build 生产构建通过。
This commit is contained in:
@@ -0,0 +1,5 @@
|
||||
-- ============================================================================
|
||||
-- 013_rock_tokens_remark.sql: 为 rock_tokens 表新增 remark 备注字段
|
||||
-- ============================================================================
|
||||
|
||||
ALTER TABLE rock_tokens ADD COLUMN IF NOT EXISTS remark TEXT NOT NULL DEFAULT '';
|
||||
@@ -169,3 +169,55 @@ func (h *RockHandler) ReleaseToken(c *gin.Context) {
|
||||
}
|
||||
response.OK(c, gin.H{"message": "已释放回可用库存,该提货链接可以再次分配"})
|
||||
}
|
||||
|
||||
type updateRemarkReq struct {
|
||||
Remark string `json:"remark"`
|
||||
Status string `json:"status"` // 可选:比如 abnormal
|
||||
}
|
||||
|
||||
// UpdateRemark 更新 Token 备注并可联动更新状态
|
||||
func (h *RockHandler) UpdateRemark(c *gin.Context) {
|
||||
idUint, err := strconv.ParseUint(c.Param("id"), 10, 64)
|
||||
if err != nil {
|
||||
response.BadRequest(c, "无效的 Token ID")
|
||||
return
|
||||
}
|
||||
var req updateRemarkReq
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
response.BadRequest(c, "参数解析失败")
|
||||
return
|
||||
}
|
||||
token, err := h.rockSvc.UpdateTokenRemark(c.Request.Context(), uint(idUint), req.Remark, req.Status)
|
||||
if err != nil {
|
||||
response.BadRequest(c, err.Error())
|
||||
return
|
||||
}
|
||||
response.OK(c, gin.H{
|
||||
"message": "备注更新成功",
|
||||
"token": token,
|
||||
})
|
||||
}
|
||||
|
||||
type markAbnormalReq struct {
|
||||
Remark string `json:"remark"`
|
||||
}
|
||||
|
||||
// MarkAbnormal 快捷标记为上报失效(作废,不可再用)
|
||||
func (h *RockHandler) MarkAbnormal(c *gin.Context) {
|
||||
idUint, err := strconv.ParseUint(c.Param("id"), 10, 64)
|
||||
if err != nil {
|
||||
response.BadRequest(c, "无效的 Token ID")
|
||||
return
|
||||
}
|
||||
var req markAbnormalReq
|
||||
_ = c.ShouldBindJSON(&req)
|
||||
token, err := h.rockSvc.MarkTokenAbnormal(c.Request.Context(), uint(idUint), req.Remark)
|
||||
if err != nil {
|
||||
response.BadRequest(c, err.Error())
|
||||
return
|
||||
}
|
||||
response.OK(c, gin.H{
|
||||
"message": "已标记为上报失效,该提货链接已作废",
|
||||
"token": token,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -77,6 +77,7 @@ type RockToken struct {
|
||||
TargetCurrencyQuantity int `gorm:"not null;default:0" json:"target_currency_quantity"`
|
||||
MismatchAt *time.Time `gorm:"index" json:"mismatch_at"`
|
||||
MismatchReason string `gorm:"size:255" json:"mismatch_reason"`
|
||||
Remark string `gorm:"type:text;not null;default:''" json:"remark"`
|
||||
ClaimedAt *time.Time `json:"claimed_at"`
|
||||
BoundAt *time.Time `json:"bound_at"`
|
||||
RedeemedAt *time.Time `json:"redeemed_at"`
|
||||
|
||||
@@ -279,6 +279,8 @@ func Setup(h *Handlers) *gin.Engine {
|
||||
platform.POST("/platform/rock/tokens/:id/expected-uid", h.Rock.CorrectTokenUID)
|
||||
platform.POST("/platform/rock/tokens/:id/reset", h.Rock.ResetToken)
|
||||
platform.POST("/platform/rock/tokens/:id/release", h.Rock.ReleaseToken)
|
||||
platform.POST("/platform/rock/tokens/:id/remark", h.Rock.UpdateRemark)
|
||||
platform.POST("/platform/rock/tokens/:id/abnormal", h.Rock.MarkAbnormal)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -996,3 +996,71 @@ func TestRockProvider_CheckBindRefreshesSameTokenLink(t *testing.T) {
|
||||
t.Fatalf("其他订单不能拿到这个提货号的二维码: %s", checkOther.BindURL)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRockService_TokenRemarkAndAbnormal(t *testing.T) {
|
||||
db := testdb.New(t)
|
||||
client := NewRockClient("https://0666.pro")
|
||||
rockSvc := NewRockService(db, client)
|
||||
|
||||
now := time.Now()
|
||||
token := model.RockToken{
|
||||
UpstreamTokenID: "TEST_REMARK_1",
|
||||
Token: "jwt-remark-1",
|
||||
RedeemCode: "CODE_REMARK_1",
|
||||
Status: model.RockStatusMismatch,
|
||||
AssignedOrderNo: "ORDER_REMARK_1",
|
||||
ExpectedUID: "123456",
|
||||
ActualRoleID: "654321",
|
||||
ActualRoleName: "路人甲",
|
||||
MismatchAt: &now,
|
||||
MismatchReason: "预期UID不符",
|
||||
}
|
||||
if err := db.Create(&token).Error; err != nil {
|
||||
t.Fatalf("create test token failed: %v", err)
|
||||
}
|
||||
|
||||
// 1. 测试单纯更新备注
|
||||
updated, err := rockSvc.UpdateTokenRemark(context.Background(), token.ID, "买家联系客服核对中", "")
|
||||
if err != nil {
|
||||
t.Fatalf("update remark failed: %v", err)
|
||||
}
|
||||
if updated.Remark != "买家联系客服核对中" || updated.Status != model.RockStatusMismatch {
|
||||
t.Fatalf("更新备注不符: remark=%s status=%s", updated.Remark, updated.Status)
|
||||
}
|
||||
|
||||
// 2. 测试快捷标记为已上报失效 (MarkTokenAbnormal)
|
||||
abnormalToken, err := rockSvc.MarkTokenAbnormal(context.Background(), token.ID, "已上报0868客服 (卡密绑定错误废弃)")
|
||||
if err != nil {
|
||||
t.Fatalf("mark abnormal failed: %v", err)
|
||||
}
|
||||
if abnormalToken.Status != model.RockStatusAbnormal {
|
||||
t.Fatalf("标记异常后状态应为 abnormal,实际: %s", abnormalToken.Status)
|
||||
}
|
||||
if abnormalToken.Remark != "已上报0868客服 (卡密绑定错误废弃)" {
|
||||
t.Fatalf("标记异常后备注不符: %s", abnormalToken.Remark)
|
||||
}
|
||||
|
||||
// 3. 搜索备注测试
|
||||
list, total, err := rockSvc.ListTokens(context.Background(), 1, 10, "", "卡密绑定错误废弃")
|
||||
if err != nil {
|
||||
t.Fatalf("list by remark search error: %v", err)
|
||||
}
|
||||
if total != 1 || len(list) != 1 || list[0].ID != token.ID {
|
||||
t.Fatalf("通过备注搜索未检索到目标 token, total=%d", total)
|
||||
}
|
||||
|
||||
// 4. 从 abnormal 状态解冻重置回 available
|
||||
if err := rockSvc.ResetMismatchToken(context.Background(), token.ID); err != nil {
|
||||
t.Fatalf("reset abnormal token error: %v", err)
|
||||
}
|
||||
var resetToken model.RockToken
|
||||
if err := db.First(&resetToken, token.ID).Error; err != nil {
|
||||
t.Fatalf("find reset token error: %v", err)
|
||||
}
|
||||
if resetToken.Status != model.RockStatusAvailable {
|
||||
t.Fatalf("重置后应为 available,实际: %s", resetToken.Status)
|
||||
}
|
||||
if resetToken.AssignedOrderNo != "" || resetToken.ExpectedUID != "" {
|
||||
t.Fatalf("重置后应清空订单关联, assigned_order_no=%s", resetToken.AssignedOrderNo)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -98,20 +98,20 @@ func isDigits(value string) bool {
|
||||
return true
|
||||
}
|
||||
|
||||
// ResetMismatchToken 客服人工重置解冻绑定错误的 Token
|
||||
// ResetMismatchToken 客服人工重置解冻绑定错误或作废的 Token
|
||||
func (s *RockService) ResetMismatchToken(ctx context.Context, tokenID uint) error {
|
||||
var token model.RockToken
|
||||
if err := s.db.WithContext(ctx).First(&token, tokenID).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if token.Status != model.RockStatusMismatch {
|
||||
return fmt.Errorf("仅 mismatch 状态的 Token 允许人工解冻重置")
|
||||
if token.Status != model.RockStatusMismatch && token.Status != model.RockStatusAbnormal {
|
||||
return fmt.Errorf("仅 mismatch 或 abnormal 状态的 Token 允许人工解冻重置")
|
||||
}
|
||||
|
||||
s.refreshCache.Delete(token.UpstreamTokenID)
|
||||
|
||||
// 重置回 available,清空订单关联
|
||||
// 重置回 available,清空订单关联与异常标记
|
||||
return s.db.WithContext(ctx).Model(&token).Updates(map[string]interface{}{
|
||||
"status": model.RockStatusAvailable,
|
||||
"assigned_order_no": "",
|
||||
@@ -125,3 +125,51 @@ func (s *RockService) ResetMismatchToken(ctx context.Context, tokenID uint) erro
|
||||
"updated_at": time.Now(),
|
||||
}).Error
|
||||
}
|
||||
|
||||
// UpdateTokenRemark 更新 Token 备注,并支持联动流转状态(例如上报后置为 abnormal 失效)
|
||||
func (s *RockService) UpdateTokenRemark(ctx context.Context, tokenID uint, remark string, nextStatus string) (*model.RockToken, error) {
|
||||
var token model.RockToken
|
||||
if err := s.db.WithContext(ctx).First(&token, tokenID).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
updates := map[string]interface{}{
|
||||
"remark": strings.TrimSpace(remark),
|
||||
"updated_at": time.Now(),
|
||||
}
|
||||
|
||||
trimmedStatus := strings.TrimSpace(nextStatus)
|
||||
if trimmedStatus != "" {
|
||||
switch trimmedStatus {
|
||||
case model.RockStatusAbnormal, model.RockStatusAvailable, model.RockStatusMismatch:
|
||||
updates["status"] = trimmedStatus
|
||||
token.Status = trimmedStatus
|
||||
if trimmedStatus == model.RockStatusAbnormal {
|
||||
s.refreshCache.Delete(token.UpstreamTokenID)
|
||||
}
|
||||
default:
|
||||
return nil, fmt.Errorf("不支持的目标状态: %s", trimmedStatus)
|
||||
}
|
||||
}
|
||||
|
||||
if err := s.db.WithContext(ctx).Model(&token).Updates(updates).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
token.Remark = updates["remark"].(string)
|
||||
|
||||
_ = writeAudit(s.db.WithContext(ctx), nil, nil, nil, "rock_token.update_remark", "rock_token", fmt.Sprintf("%d", token.ID), map[string]interface{}{
|
||||
"remark": token.Remark,
|
||||
"status": token.Status,
|
||||
})
|
||||
|
||||
return &token, nil
|
||||
}
|
||||
|
||||
// MarkTokenAbnormal 快捷标记为上报失效(算作废,不可再用)
|
||||
func (s *RockService) MarkTokenAbnormal(ctx context.Context, tokenID uint, reason string) (*model.RockToken, error) {
|
||||
remark := strings.TrimSpace(reason)
|
||||
if remark == "" {
|
||||
remark = "已上报上游 (失效作废)"
|
||||
}
|
||||
return s.UpdateTokenRemark(ctx, tokenID, remark, model.RockStatusAbnormal)
|
||||
}
|
||||
|
||||
@@ -127,8 +127,8 @@ func (s *RockService) ListTokens(ctx context.Context, page, pageSize int, status
|
||||
}
|
||||
if strings.TrimSpace(search) != "" {
|
||||
term := "%" + strings.TrimSpace(search) + "%"
|
||||
db = db.Where("CAST(id AS TEXT) = ? OR upstream_token_id = ? OR upstream_token_id LIKE ? OR assigned_order_no LIKE ? OR expected_uid LIKE ? OR actual_role_name LIKE ? OR actual_role_id LIKE ? OR redeem_code LIKE ? OR assigned_order_no IN (SELECT order_no FROM fulfillment_orders WHERE client_order_no LIKE ?)",
|
||||
strings.TrimSpace(search), strings.TrimSpace(search), term, term, term, term, term, term, term)
|
||||
db = db.Where("CAST(id AS TEXT) = ? OR upstream_token_id = ? OR upstream_token_id LIKE ? OR assigned_order_no LIKE ? OR expected_uid LIKE ? OR actual_role_name LIKE ? OR actual_role_id LIKE ? OR redeem_code LIKE ? OR remark LIKE ? OR assigned_order_no IN (SELECT order_no FROM fulfillment_orders WHERE client_order_no LIKE ?)",
|
||||
strings.TrimSpace(search), strings.TrimSpace(search), term, term, term, term, term, term, term, term)
|
||||
}
|
||||
|
||||
var total int64
|
||||
|
||||
Reference in New Issue
Block a user