From b4d2d1e2f57e819a7279047cc8fbf24a809780d3 Mon Sep 17 00:00:00 2001
From: yml2213
Date: Fri, 9 Oct 2026 18:45:12 +0800
Subject: [PATCH] =?UTF-8?q?feat(pickup):=20=E5=A2=9E=E5=8A=A0=E7=AC=AC?=
=?UTF-8?q?=E4=B8=89=E6=96=B9=E5=8D=95=E5=8F=B7=E8=A1=A5=E5=BD=95=E5=8A=9F?=
=?UTF-8?q?=E8=83=BD=E5=B9=B6=E4=BC=98=E5=8C=96=E5=88=97=E8=A1=A8=E6=8F=90?=
=?UTF-8?q?=E5=8F=B7=E5=A4=87=E6=B3=A8=E5=B1=95=E7=A4=BA?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
- 后端支持补录/修改第三方单号接口 PUT /api/v1/admin/pickups/:id/third-party-order-no,记录流转事件与审计日志
- 前端线下提号主列表在第三方单号列后展示提号备注列
- 前端列表操作列及提号详情页增加补录/修改第三方单号操作入口与弹窗
- 通过 scripts/check.sh 全量门禁校验
---
backend/internal/modules/pickup/dto.go | 6 ++
backend/internal/modules/pickup/handler.go | 28 ++++++
backend/internal/modules/pickup/repository.go | 55 +++++++++++
.../modules/pickup/repository_test.go | 58 ++++++++++++
backend/internal/modules/pickup/service.go | 13 +++
backend/internal/router/router.go | 1 +
.../src/features/admin/api/adminPickup.ts | 15 +++
.../admin/views/AdminPickupDetailView.vue | 78 ++++++++++++++++
.../features/admin/views/AdminPickupView.vue | 92 ++++++++++++++++++-
9 files changed, 345 insertions(+), 1 deletion(-)
diff --git a/backend/internal/modules/pickup/dto.go b/backend/internal/modules/pickup/dto.go
index ad0672b..86ad29e 100644
--- a/backend/internal/modules/pickup/dto.go
+++ b/backend/internal/modules/pickup/dto.go
@@ -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"`
diff --git a/backend/internal/modules/pickup/handler.go b/backend/internal/modules/pickup/handler.go
index e740921..964f1d2 100644
--- a/backend/internal/modules/pickup/handler.go
+++ b/backend/internal/modules/pickup/handler.go
@@ -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())
}
diff --git a/backend/internal/modules/pickup/repository.go b/backend/internal/modules/pickup/repository.go
index f61144b..0e05b53 100644
--- a/backend/internal/modules/pickup/repository.go
+++ b/backend/internal/modules/pickup/repository.go
@@ -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 {
diff --git a/backend/internal/modules/pickup/repository_test.go b/backend/internal/modules/pickup/repository_test.go
index ba47caf..4a31239 100644
--- a/backend/internal/modules/pickup/repository_test.go
+++ b/backend/internal/modules/pickup/repository_test.go
@@ -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)
+ }
+ })
+}
+
diff --git a/backend/internal/modules/pickup/service.go b/backend/internal/modules/pickup/service.go
index 9dc14f3..eb56674 100644
--- a/backend/internal/modules/pickup/service.go
+++ b/backend/internal/modules/pickup/service.go
@@ -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
diff --git a/backend/internal/router/router.go b/backend/internal/router/router.go
index b775d54..07ed5c8 100644
--- a/backend/internal/router/router.go
+++ b/backend/internal/router/router.go
@@ -774,6 +774,7 @@ func New(cfg config.Config, deps Dependencies, logger *zap.Logger) *gin.Engine {
adminRoutes.POST("/pickups/:id/complete", requirePerm("order:pickup"), pickupHandler.Complete)
adminRoutes.POST("/pickups/:id/offline-settlement", requirePerm("order:pickup"), pickupHandler.MarkOfflineSettlement)
adminRoutes.PUT("/pickups/:id/profit", requirePerm("order:pickup"), pickupHandler.UpdateProfit)
+ adminRoutes.PUT("/pickups/:id/third-party-order-no", requirePerm("order:pickup"), pickupHandler.UpdateThirdPartyOrderNo)
adminRoutes.POST("/pickups/:id/cancel", requirePerm("order:pickup"), pickupHandler.Cancel)
adminRoutes.POST("/pickup-financial-adjustments/:id/settle", requirePerm("order:pickup"), pickupHandler.SettleFinancialAdjustment)
diff --git a/frontend/src/features/admin/api/adminPickup.ts b/frontend/src/features/admin/api/adminPickup.ts
index 3a350d5..48bc70c 100644
--- a/frontend/src/features/admin/api/adminPickup.ts
+++ b/frontend/src/features/admin/api/adminPickup.ts
@@ -134,6 +134,10 @@ export interface AdminPickupProfitUpdateRequest {
reason?: string
}
+export interface AdminPickupThirdPartyOrderNoUpdateRequest {
+ third_party_order_no: string
+}
+
export interface AdminPickupOfflineSettlementRequest {
remark?: string
}
@@ -308,6 +312,17 @@ export async function updateAdminPickupProfit(id: number, req: AdminPickupProfit
return data.data
}
+export async function updateAdminPickupThirdPartyOrderNo(
+ id: number,
+ req: AdminPickupThirdPartyOrderNoUpdateRequest
+) {
+ const { data } = await apiClient.put>(
+ `/admin/pickups/${id}/third-party-order-no`,
+ req
+ )
+ return data.data
+}
+
export async function createAdminPickupFinancialAdjustment(
id: number,
req: AdminPickupFinancialAdjustmentRequest
diff --git a/frontend/src/features/admin/views/AdminPickupDetailView.vue b/frontend/src/features/admin/views/AdminPickupDetailView.vue
index a68cfb0..054f7da 100644
--- a/frontend/src/features/admin/views/AdminPickupDetailView.vue
+++ b/frontend/src/features/admin/views/AdminPickupDetailView.vue
@@ -11,6 +11,7 @@ import {
createAdminPickupFinancialAdjustment,
settleAdminPickupFinancialAdjustment,
updateAdminPickupProfit,
+ updateAdminPickupThirdPartyOrderNo,
type AdminPickup,
type AdminPickupFinancialAdjustment,
type AdminPickupProcessEvent,
@@ -44,16 +45,20 @@ const financialAdjustments = ref([])
const processEvents = ref([])
const profitDialogVisible = ref(false)
const financialAdjustmentDialogVisible = ref(false)
+const thirdPartyOrderNoDialogVisible = ref(false)
const profitSaving = ref(false)
const financialAdjustmentSaving = ref(false)
+const thirdPartyOrderNoSaving = ref(false)
const profitForm = reactive({ profit_amount: 0, reason: '' })
const financialAdjustmentForm = reactive({ profit_amount: 0, settle_amount: 0, reason: '' })
+const thirdPartyOrderNoForm = reactive({ third_party_order_no: '' })
const listingCode = computed(() =>
pickup.value ? formatListingNo(pickup.value.listing_no, pickup.value.listing_id) : '-'
)
const canEditProfit = computed(() => pickup.value?.status === 'picking_up')
const canCreateFinancialAdjustment = computed(() => pickup.value?.status === 'completed')
+const canUpdateThirdPartyOrderNo = computed(() => pickup.value?.status !== 'cancelled')
const snapshot = computed(() => normalizeRecord(pickup.value?.account_snapshot))
const assetSummary = computed(() => normalizeRecord(snapshot.value?.asset_summary))
const priceBreakdown = computed(() => normalizeRecord(assetSummary.value?.price_breakdown))
@@ -221,6 +226,35 @@ async function handleUpdateProfit() {
}
}
+function openThirdPartyOrderNoDialog() {
+ if (!pickup.value) return
+ thirdPartyOrderNoForm.third_party_order_no = pickup.value.third_party_order_no || ''
+ thirdPartyOrderNoDialogVisible.value = true
+}
+
+async function handleSaveThirdPartyOrderNo() {
+ if (!pickup.value) return
+ const orderNo = thirdPartyOrderNoForm.third_party_order_no.trim()
+ if (!orderNo) {
+ ElMessage.warning('请输入第三方单号')
+ return
+ }
+ thirdPartyOrderNoSaving.value = true
+ try {
+ const updated = await updateAdminPickupThirdPartyOrderNo(pickup.value.id, {
+ third_party_order_no: orderNo,
+ })
+ pickup.value = updated
+ processEvents.value = await fetchAdminPickupProcessEvents(pickup.value.id)
+ thirdPartyOrderNoDialogVisible.value = false
+ ElMessage.success('第三方单号已更新')
+ } catch (e: unknown) {
+ ElMessage.error(errorMessage(e) || '保存失败')
+ } finally {
+ thirdPartyOrderNoSaving.value = false
+ }
+}
+
function statusType(status: string) {
if (status === 'picking_up') return 'warning'
if (status === 'completed') return 'success'
@@ -359,6 +393,15 @@ function readSnapshotResources(summary: Record | null): Snapsho