feat(pickup): 增加第三方单号补录功能并优化列表提号备注展示
- 后端支持补录/修改第三方单号接口 PUT /api/v1/admin/pickups/:id/third-party-order-no,记录流转事件与审计日志 - 前端线下提号主列表在第三方单号列后展示提号备注列 - 前端列表操作列及提号详情页增加补录/修改第三方单号操作入口与弹窗 - 通过 scripts/check.sh 全量门禁校验
This commit is contained in:
@@ -44,6 +44,8 @@ var (
|
||||
ErrFinancialAdjustmentCannotSettle = errors.New("financial adjustment cannot be settled")
|
||||
ErrPickupShopNotFound = errors.New("pickup shop not found")
|
||||
ErrPickupShopNameExists = errors.New("pickup shop name already exists")
|
||||
ErrPickupCancelled = errors.New("pickup is cancelled")
|
||||
ErrInvalidThirdPartyOrderNo = errors.New("invalid third party order no")
|
||||
)
|
||||
|
||||
type PickupDTO struct {
|
||||
@@ -134,6 +136,10 @@ type UpdateProfitRequest struct {
|
||||
Reason string `json:"reason"`
|
||||
}
|
||||
|
||||
type UpdateThirdPartyOrderNoRequest struct {
|
||||
ThirdPartyOrderNo string `json:"third_party_order_no" binding:"required,max=128"`
|
||||
}
|
||||
|
||||
// FinancialAdjustmentRequest 按目标金额创建完成后的差额流水,至少提供一个金额。
|
||||
type FinancialAdjustmentRequest struct {
|
||||
ProfitAmountCent *int64 `json:"profit_amount_cent"`
|
||||
|
||||
@@ -112,6 +112,30 @@ func (h *Handler) UpdateProfit(c *gin.Context) {
|
||||
response.OK(c, item)
|
||||
}
|
||||
|
||||
// UpdateThirdPartyOrderNo 补录或修改第三方单号(管理员)
|
||||
func (h *Handler) UpdateThirdPartyOrderNo(c *gin.Context) {
|
||||
adminID, ok := currentAdminID(c)
|
||||
if !ok {
|
||||
response.Unauthorized(c, "缺少管理员上下文")
|
||||
return
|
||||
}
|
||||
id, ok := parseID(c)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
var req UpdateThirdPartyOrderNoRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
response.BadRequest(c, "第三方单号不能为空且长度不能超过128位")
|
||||
return
|
||||
}
|
||||
item, err := h.service.UpdateThirdPartyOrderNo(c.Request.Context(), id, req, adminID, auditMeta(c))
|
||||
if err != nil {
|
||||
writePickupError(c, err)
|
||||
return
|
||||
}
|
||||
response.OK(c, item)
|
||||
}
|
||||
|
||||
// CreateFinancialAdjustment 为已完成提号创建利润或打款调整。
|
||||
func (h *Handler) CreateFinancialAdjustment(c *gin.Context) {
|
||||
adminID, ok := currentAdminID(c)
|
||||
@@ -305,6 +329,10 @@ func writePickupError(c *gin.Context, err error) {
|
||||
response.Error(c, http.StatusNotFound, "pickup_shop_not_found", "店铺不存在")
|
||||
case errors.Is(err, ErrPickupShopNameExists):
|
||||
response.Error(c, http.StatusConflict, "pickup_shop_name_exists", "店铺名称已存在")
|
||||
case errors.Is(err, ErrPickupCancelled):
|
||||
response.BadRequest(c, "提号订单已取消,不能补录单号")
|
||||
case errors.Is(err, ErrInvalidThirdPartyOrderNo):
|
||||
response.BadRequest(c, "第三方单号不能为空且长度不能超过128位")
|
||||
default:
|
||||
response.Error(c, http.StatusInternalServerError, "pickup_error", err.Error())
|
||||
}
|
||||
|
||||
@@ -465,6 +465,61 @@ func (r *Repository) UpdateProfit(ctx context.Context, pickupID uint64, req Upda
|
||||
return r.FindByID(ctx, pickupID)
|
||||
}
|
||||
|
||||
// UpdateThirdPartyOrderNo 补录或修改提号单第三方单号。
|
||||
func (r *Repository) UpdateThirdPartyOrderNo(ctx context.Context, pickupID uint64, req UpdateThirdPartyOrderNoRequest, adminID uint64, meta auditlog.Meta) (*PickupDTO, error) {
|
||||
if r == nil || r.db == nil {
|
||||
return nil, ErrDependencyUnavailable
|
||||
}
|
||||
newThirdPartyOrderNo := strings.TrimSpace(req.ThirdPartyOrderNo)
|
||||
if newThirdPartyOrderNo == "" || len([]rune(newThirdPartyOrderNo)) > 128 {
|
||||
return nil, ErrInvalidThirdPartyOrderNo
|
||||
}
|
||||
err := r.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
|
||||
var pickup model.AdminPickup
|
||||
if err := tx.Clauses(clause.Locking{Strength: "UPDATE"}).First(&pickup, pickupID).Error; err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return ErrPickupNotFound
|
||||
}
|
||||
return err
|
||||
}
|
||||
if pickup.Status == StatusCancelled {
|
||||
return ErrPickupCancelled
|
||||
}
|
||||
before := pickupState(pickup)
|
||||
|
||||
oldThirdPartyOrderNo := pickup.ThirdPartyOrderNo
|
||||
pickup.ThirdPartyOrderNo = newThirdPartyOrderNo
|
||||
if err := tx.Model(&pickup).Update("third_party_order_no", pickup.ThirdPartyOrderNo).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
if err := appendPickupEvent(tx, pickup, "pickup_third_party_order_no_updated", adminID, "补录/修改第三方单号。", "", map[string]any{
|
||||
"old_third_party_order_no": oldThirdPartyOrderNo,
|
||||
"third_party_order_no": pickup.ThirdPartyOrderNo,
|
||||
}, before); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
bid := pickup.ID
|
||||
return auditlog.Append(tx, auditlog.Entry{
|
||||
ActorType: "admin",
|
||||
ActorID: adminID,
|
||||
Action: "pickup_third_party_order_no_update",
|
||||
BizType: "admin_pickup",
|
||||
BizID: &bid,
|
||||
Meta: meta,
|
||||
Detail: map[string]any{
|
||||
"pickup_no": pickup.PickupNo,
|
||||
"old_third_party_order_no": oldThirdPartyOrderNo,
|
||||
"new_third_party_order_no": pickup.ThirdPartyOrderNo,
|
||||
},
|
||||
})
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return r.FindByID(ctx, pickupID)
|
||||
}
|
||||
|
||||
// CreateFinancialAdjustment 为已完成提号追加财务差额,不改写完成时的原始金额。
|
||||
func (r *Repository) CreateFinancialAdjustment(ctx context.Context, pickupID uint64, req FinancialAdjustmentRequest, adminID uint64, meta auditlog.Meta) (*PickupDTO, error) {
|
||||
if r == nil || r.db == nil {
|
||||
|
||||
@@ -1019,3 +1019,61 @@ func TestRepositoryHistoricalShopStatsAndMapping(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestRepositoryUpdateThirdPartyOrderNo(t *testing.T) {
|
||||
t.Run("成功补录第三方单号并记录审计", func(t *testing.T) {
|
||||
repo, db := newPickupTestRepo(t)
|
||||
pickup := seedPickup(t, db, StatusPickingUp)
|
||||
|
||||
item, err := repo.UpdateThirdPartyOrderNo(t.Context(), pickup.ID, UpdateThirdPartyOrderNoRequest{
|
||||
ThirdPartyOrderNo: "TB-20261009-8888",
|
||||
}, 99, auditlog.Meta{RequestID: "req-third-party-order-no"})
|
||||
if err != nil {
|
||||
t.Fatalf("UpdateThirdPartyOrderNo() error = %v", err)
|
||||
}
|
||||
if item.ThirdPartyOrderNo != "TB-20261009-8888" {
|
||||
t.Fatalf("item.ThirdPartyOrderNo = %q, want TB-20261009-8888", item.ThirdPartyOrderNo)
|
||||
}
|
||||
|
||||
var stored model.AdminPickup
|
||||
if err := db.First(&stored, pickup.ID).Error; err != nil {
|
||||
t.Fatalf("查询提号失败: %v", err)
|
||||
}
|
||||
if stored.ThirdPartyOrderNo != "TB-20261009-8888" {
|
||||
t.Fatalf("stored.ThirdPartyOrderNo = %q, want TB-20261009-8888", stored.ThirdPartyOrderNo)
|
||||
}
|
||||
|
||||
// 验证审计日志
|
||||
var audit model.AuditLog
|
||||
if err := db.Where("action = ?", "pickup_third_party_order_no_update").First(&audit).Error; err != nil {
|
||||
t.Fatalf("查询审计日志失败: %v", err)
|
||||
}
|
||||
var detail map[string]any
|
||||
if err := json.Unmarshal(audit.Detail, &detail); err != nil {
|
||||
t.Fatalf("解析审计日志失败: %v", err)
|
||||
}
|
||||
if detail["new_third_party_order_no"] != "TB-20261009-8888" {
|
||||
t.Fatalf("detail[new_third_party_order_no] = %v, want TB-20261009-8888", detail["new_third_party_order_no"])
|
||||
}
|
||||
|
||||
// 空单号拒绝
|
||||
_, err = repo.UpdateThirdPartyOrderNo(t.Context(), pickup.ID, UpdateThirdPartyOrderNoRequest{
|
||||
ThirdPartyOrderNo: " ",
|
||||
}, 99, auditlog.Meta{})
|
||||
if !errors.Is(err, ErrInvalidThirdPartyOrderNo) {
|
||||
t.Fatalf("UpdateThirdPartyOrderNo() with blank error = %v, want ErrInvalidThirdPartyOrderNo", err)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("已取消提号单拒绝补录", func(t *testing.T) {
|
||||
repo, db := newPickupTestRepo(t)
|
||||
cancelledPickup := seedPickup(t, db, StatusCancelled)
|
||||
|
||||
_, err := repo.UpdateThirdPartyOrderNo(t.Context(), cancelledPickup.ID, UpdateThirdPartyOrderNoRequest{
|
||||
ThirdPartyOrderNo: "TB-CANCELLED-999",
|
||||
}, 99, auditlog.Meta{})
|
||||
if !errors.Is(err, ErrPickupCancelled) {
|
||||
t.Fatalf("UpdateThirdPartyOrderNo() on cancelled error = %v, want ErrPickupCancelled", err)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -64,6 +64,19 @@ func (s *Service) UpdateProfit(ctx context.Context, pickupID uint64, req UpdateP
|
||||
return s.repo.UpdateProfit(ctx, pickupID, req, adminID, meta)
|
||||
}
|
||||
|
||||
func (s *Service) UpdateThirdPartyOrderNo(ctx context.Context, pickupID uint64, req UpdateThirdPartyOrderNoRequest, adminID uint64, meta auditlog.Meta) (*PickupDTO, error) {
|
||||
if s.repo == nil {
|
||||
return nil, ErrDependencyUnavailable
|
||||
}
|
||||
if pickupID == 0 {
|
||||
return nil, ErrPickupNotFound
|
||||
}
|
||||
if strings.TrimSpace(req.ThirdPartyOrderNo) == "" {
|
||||
return nil, ErrInvalidThirdPartyOrderNo
|
||||
}
|
||||
return s.repo.UpdateThirdPartyOrderNo(ctx, pickupID, req, adminID, meta)
|
||||
}
|
||||
|
||||
func (s *Service) CreateFinancialAdjustment(ctx context.Context, pickupID uint64, req FinancialAdjustmentRequest, adminID uint64, meta auditlog.Meta) (*PickupDTO, error) {
|
||||
if s.repo == nil {
|
||||
return nil, ErrDependencyUnavailable
|
||||
|
||||
Reference in New Issue
Block a user