From 6b44ab599db3cde5d6dd489ed8626faa5d8168b3 Mon Sep 17 00:00:00 2001 From: yml2213 Date: Sun, 26 Jul 2026 18:19:12 +0800 Subject: [PATCH] =?UTF-8?q?=E5=A2=9E=E5=8A=A0=E7=BA=BF=E4=B8=8B=E6=8F=90?= =?UTF-8?q?=E5=8F=B7=E5=BA=97=E9=93=BA=E7=AE=A1=E7=90=86?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/internal/model/pickup.go | 1 + backend/internal/modules/pickup/dto.go | 3 + backend/internal/modules/pickup/handler.go | 10 ++++ .../internal/modules/pickup/handler_parse.go | 1 + backend/internal/modules/pickup/repository.go | 24 ++++++++ .../modules/pickup/repository_test.go | 56 +++++++++++++++++++ backend/internal/modules/pickup/service.go | 7 +++ backend/internal/router/router.go | 1 + .../000041_admin_pickup_shop_name.sql | 11 ++++ .../src/features/admin/api/adminPickup.ts | 10 ++++ .../admin/views/AdminPickupDetailView.vue | 4 ++ .../features/admin/views/AdminPickupView.vue | 50 ++++++++++++++++- 12 files changed, 176 insertions(+), 2 deletions(-) create mode 100644 backend/migrations/000041_admin_pickup_shop_name.sql diff --git a/backend/internal/model/pickup.go b/backend/internal/model/pickup.go index 6443afd..60a55a8 100644 --- a/backend/internal/model/pickup.go +++ b/backend/internal/model/pickup.go @@ -17,6 +17,7 @@ type AdminPickup struct { OwnerID uint64 `gorm:"not null;index" json:"owner_id"` AdminID uint64 `gorm:"not null" json:"admin_id"` Platform string `gorm:"size:32;not null;default:''" json:"platform"` + ShopName string `gorm:"size:100;not null;default:'';index" json:"shop_name"` ListingPriceCent int64 `gorm:"not null;default:0" json:"listing_price_cent"` OwnerPriceCent int64 `gorm:"not null;default:0" json:"owner_price_cent"` WebsiteProfitCent int64 `gorm:"not null;default:0" json:"website_profit_cent"` diff --git a/backend/internal/modules/pickup/dto.go b/backend/internal/modules/pickup/dto.go index 34fc97b..7e46be1 100644 --- a/backend/internal/modules/pickup/dto.go +++ b/backend/internal/modules/pickup/dto.go @@ -37,6 +37,7 @@ type PickupDTO struct { OwnerPhone string `json:"owner_phone"` AdminID uint64 `json:"admin_id"` Platform string `json:"platform"` + ShopName string `json:"shop_name"` ListingPriceCent int64 `json:"listing_price_cent"` OwnerPriceCent int64 `json:"owner_price_cent"` WebsiteProfitCent int64 `json:"website_profit_cent"` @@ -56,6 +57,7 @@ type PickupDTO struct { type CreateRequest struct { ListingID uint64 `json:"listing_id" binding:"required"` Platform string `json:"platform"` + ShopName string `json:"shop_name" binding:"max=100"` ProfitAmountCent int64 `json:"profit_amount_cent"` Remark string `json:"remark"` } @@ -80,6 +82,7 @@ type AdminPickupQuery struct { PageSize int Keyword string Status string + ShopName string } type SellerPickupQuery struct { diff --git a/backend/internal/modules/pickup/handler.go b/backend/internal/modules/pickup/handler.go index d3892fe..404eec6 100644 --- a/backend/internal/modules/pickup/handler.go +++ b/backend/internal/modules/pickup/handler.go @@ -122,6 +122,16 @@ func (h *Handler) List(c *gin.Context) { response.OK(c, result) } +// ShopOptions 返回后台提号已使用的店铺名称。 +func (h *Handler) ShopOptions(c *gin.Context) { + items, err := h.service.ListShopNames(c.Request.Context(), c.Query("platform")) + if err != nil { + writePickupError(c, err) + return + } + response.OK(c, items) +} + // Detail 提号订单详情(管理员) func (h *Handler) Detail(c *gin.Context) { id, ok := parseID(c) diff --git a/backend/internal/modules/pickup/handler_parse.go b/backend/internal/modules/pickup/handler_parse.go index a9ed41a..007c87d 100644 --- a/backend/internal/modules/pickup/handler_parse.go +++ b/backend/internal/modules/pickup/handler_parse.go @@ -58,6 +58,7 @@ func parseAdminQuery(c *gin.Context) AdminPickupQuery { PageSize: pageSize, Keyword: c.Query("keyword"), Status: c.Query("status"), + ShopName: c.Query("shop_name"), } } diff --git a/backend/internal/modules/pickup/repository.go b/backend/internal/modules/pickup/repository.go index 9a5ddd9..b3ae547 100644 --- a/backend/internal/modules/pickup/repository.go +++ b/backend/internal/modules/pickup/repository.go @@ -84,6 +84,7 @@ func (r *Repository) Create(ctx context.Context, req CreateRequest, adminID uint OwnerID: listing.OwnerID, AdminID: adminID, Platform: strings.TrimSpace(req.Platform), + ShopName: strings.TrimSpace(req.ShopName), ListingPriceCent: priceSnapshot.ListingPriceCent, OwnerPriceCent: priceSnapshot.OwnerPriceCent, WebsiteProfitCent: priceSnapshot.WebsiteProfitCent, @@ -145,6 +146,7 @@ func (r *Repository) Create(ctx context.Context, req CreateRequest, adminID uint "pickup_no": pickupNo, "listing_id": listing.ID, "platform": pickup.Platform, + "shop_name": pickup.ShopName, "profit_cent": pickup.ProfitAmountCent, }, }); err != nil { @@ -417,6 +419,9 @@ func (r *Repository) ListAdmin(ctx context.Context, query AdminPickupQuery) (*Pa if status := strings.TrimSpace(query.Status); status != "" { db = db.Where("p.status = ?", status) } + if shopName := strings.TrimSpace(query.ShopName); shopName != "" { + db = db.Where("p.shop_name = ?", shopName) + } if kw := strings.TrimSpace(query.Keyword); kw != "" { like := "%" + kw + "%" db = db.Where("p.pickup_no LIKE ? OR l.listing_no LIKE ? OR a.title LIKE ?", like, like, like) @@ -424,6 +429,23 @@ func (r *Repository) ListAdmin(ctx context.Context, query AdminPickupQuery) (*Pa return r.paginatePickups(db, query.Page, query.PageSize, false) } +// ListShopNames 返回历史提号中已使用的店铺名称,供后台选择和筛选。 +func (r *Repository) ListShopNames(ctx context.Context, platform string) ([]string, error) { + if r == nil || r.db == nil { + return nil, ErrDependencyUnavailable + } + db := r.db.WithContext(ctx).Model(&model.AdminPickup{}). + Where("shop_name <> ''") + if value := strings.TrimSpace(platform); value != "" { + db = db.Where("platform = ?", value) + } + names := make([]string, 0) + if err := db.Distinct("shop_name").Order("shop_name ASC").Limit(200).Pluck("shop_name", &names).Error; err != nil { + return nil, err + } + return names, nil +} + func (r *Repository) ListForSeller(ctx context.Context, ownerID uint64, query SellerPickupQuery) (*PaginatedResult, error) { if r == nil || r.db == nil { return nil, ErrDependencyUnavailable @@ -453,6 +475,7 @@ func (r *Repository) paginatePickups(db *gorm.DB, page, pageSize int, sellerView item.WebsiteProfitCent = 0 item.ProfitAmountCent = 0 item.BuyerRatio = 0 + item.ShopName = "" } items = append(items, item) } @@ -521,6 +544,7 @@ func (row pickupRow) toDTO() PickupDTO { OwnerPhone: row.OwnerPhone, AdminID: row.AdminID, Platform: row.Platform, + ShopName: row.ShopName, ListingPriceCent: row.ListingPriceCent, OwnerPriceCent: row.OwnerPriceCent, WebsiteProfitCent: row.WebsiteProfitCent, diff --git a/backend/internal/modules/pickup/repository_test.go b/backend/internal/modules/pickup/repository_test.go index ba1255b..189c437 100644 --- a/backend/internal/modules/pickup/repository_test.go +++ b/backend/internal/modules/pickup/repository_test.go @@ -125,6 +125,62 @@ func TestRepositoryUpdateProfitRejectsCompleted(t *testing.T) { } } +func TestRepositoryListAdminFiltersShopName(t *testing.T) { + repo, db := newPickupTestRepo(t) + first := seedPickup(t, db, StatusPickingUp) + if err := db.Model(&first).Updates(map[string]any{ + "platform": "淘宝", + "shop_name": "大锤一店", + }).Error; err != nil { + t.Fatalf("设置第一条提号店铺失败: %v", err) + } + second := first + second.ID = 0 + second.PickupNo = "PK-PROFIT-002" + second.ShopName = "大锤二店" + if err := db.Create(&second).Error; err != nil { + t.Fatalf("创建第二条提号记录失败: %v", err) + } + + result, err := repo.ListAdmin(t.Context(), AdminPickupQuery{ShopName: " 大锤一店 "}) + if err != nil { + t.Fatalf("ListAdmin() error = %v", err) + } + if result.Total != 1 || len(result.Items) != 1 { + t.Fatalf("店铺筛选结果 = total %d, items %d, want 1", result.Total, len(result.Items)) + } + if result.Items[0].ShopName != "大锤一店" { + t.Fatalf("店铺名称 = %q, want 大锤一店", result.Items[0].ShopName) + } +} + +func TestRepositoryListShopNamesReturnsDistinctPlatformOptions(t *testing.T) { + repo, db := newPickupTestRepo(t) + first := seedPickup(t, db, StatusPickingUp) + if err := db.Model(&first).Updates(map[string]any{ + "platform": "淘宝", + "shop_name": "大锤一店", + }).Error; err != nil { + t.Fatalf("设置第一条提号店铺失败: %v", err) + } + for index, row := range []model.AdminPickup{ + {PickupNo: "PK-SHOP-002", ListingID: first.ListingID, AccountID: first.AccountID, OwnerID: first.OwnerID, AdminID: 1, Platform: "淘宝", ShopName: "大锤一店", Status: StatusPickingUp}, + {PickupNo: "PK-SHOP-003", ListingID: first.ListingID, AccountID: first.AccountID, OwnerID: first.OwnerID, AdminID: 1, Platform: "拼多多", ShopName: "大锤二店", Status: StatusPickingUp}, + } { + if err := db.Create(&row).Error; err != nil { + t.Fatalf("创建第 %d 条店铺选项失败: %v", index+2, err) + } + } + + names, err := repo.ListShopNames(t.Context(), "淘宝") + if err != nil { + t.Fatalf("ListShopNames() error = %v", err) + } + if len(names) != 1 || names[0] != "大锤一店" { + t.Fatalf("淘宝店铺选项 = %v, want [大锤一店]", names) + } +} + func assertInt64(t *testing.T, name string, got, want int64) { t.Helper() if got != want { diff --git a/backend/internal/modules/pickup/service.go b/backend/internal/modules/pickup/service.go index 5aad68e..72bb868 100644 --- a/backend/internal/modules/pickup/service.go +++ b/backend/internal/modules/pickup/service.go @@ -77,6 +77,13 @@ func (s *Service) ListAdmin(ctx context.Context, query AdminPickupQuery) (*Pagin return s.repo.ListAdmin(ctx, query) } +func (s *Service) ListShopNames(ctx context.Context, platform string) ([]string, error) { + if s.repo == nil { + return nil, ErrDependencyUnavailable + } + return s.repo.ListShopNames(ctx, platform) +} + func (s *Service) ListForSeller(ctx context.Context, ownerID uint64, query SellerPickupQuery) (*PaginatedResult, error) { if s.repo == nil { return nil, ErrDependencyUnavailable diff --git a/backend/internal/router/router.go b/backend/internal/router/router.go index 7d4014a..0b13e9c 100644 --- a/backend/internal/router/router.go +++ b/backend/internal/router/router.go @@ -594,6 +594,7 @@ func New(cfg config.Config, deps Dependencies, logger *zap.Logger) *gin.Engine { // 管理员线下提号(独立于正常订单流程) adminRoutes.POST("/pickups", requirePerm("order:pickup"), pickupHandler.Create) adminRoutes.GET("/pickups", requirePerm("order:pickup"), pickupHandler.List) + adminRoutes.GET("/pickups/shop-options", requirePerm("order:pickup"), pickupHandler.ShopOptions) adminRoutes.GET("/pickups/available-listings", requirePerm("order:pickup"), pickupHandler.AvailableListings) adminRoutes.GET("/pickups/:id", requirePerm("order:pickup"), pickupHandler.Detail) adminRoutes.POST("/pickups/:id/complete", requirePerm("order:pickup"), pickupHandler.Complete) diff --git a/backend/migrations/000041_admin_pickup_shop_name.sql b/backend/migrations/000041_admin_pickup_shop_name.sql new file mode 100644 index 0000000..570266b --- /dev/null +++ b/backend/migrations/000041_admin_pickup_shop_name.sql @@ -0,0 +1,11 @@ +-- +goose Up + +ALTER TABLE admin_pickups + ADD COLUMN shop_name VARCHAR(100) NOT NULL DEFAULT '' COMMENT '线下成交店铺名称' AFTER platform, + ADD KEY idx_admin_pickups_shop_name (shop_name); + +-- +goose Down + +ALTER TABLE admin_pickups + DROP KEY idx_admin_pickups_shop_name, + DROP COLUMN shop_name; diff --git a/frontend/src/features/admin/api/adminPickup.ts b/frontend/src/features/admin/api/adminPickup.ts index feb6e5c..18f0f7d 100644 --- a/frontend/src/features/admin/api/adminPickup.ts +++ b/frontend/src/features/admin/api/adminPickup.ts @@ -14,6 +14,7 @@ export interface AdminPickup { owner_phone: string admin_id: number platform: string + shop_name: string listing_price_cent: number owner_price_cent: number website_profit_cent: number @@ -52,6 +53,7 @@ export interface AvailableListing { export interface AdminPickupCreateRequest { listing_id: number platform?: string + shop_name?: string profit_amount_cent?: number remark?: string } @@ -72,6 +74,7 @@ export interface AdminPickupListQuery { page_size?: number keyword?: string status?: string + shop_name?: string } function cleanParams(query: object) { @@ -99,6 +102,13 @@ export async function fetchAdminPickup(id: string | number) { return data.data } +export async function fetchAdminPickupShopOptions(platform = '') { + const { data } = await apiClient.get>('/admin/pickups/shop-options', { + params: cleanParams({ platform }), + }) + return Array.isArray(data.data) ? data.data : [] +} + export async function fetchAvailableListings(keyword: string, page = 1, page_size = 20) { const { data } = await apiClient.get>>( '/admin/pickups/available-listings', diff --git a/frontend/src/features/admin/views/AdminPickupDetailView.vue b/frontend/src/features/admin/views/AdminPickupDetailView.vue index c73d542..f4a784d 100644 --- a/frontend/src/features/admin/views/AdminPickupDetailView.vue +++ b/frontend/src/features/admin/views/AdminPickupDetailView.vue @@ -300,6 +300,10 @@ function readSnapshotResources(summary: Record | null): Snapsho
交易渠道
{{ pickup.platform || '-' }}
+
+
店铺名称
+
{{ pickup.shop_name || '-' }}
+
管理员ID
{{ pickup.admin_id }}
diff --git a/frontend/src/features/admin/views/AdminPickupView.vue b/frontend/src/features/admin/views/AdminPickupView.vue index a0640fd..3dcdf93 100644 --- a/frontend/src/features/admin/views/AdminPickupView.vue +++ b/frontend/src/features/admin/views/AdminPickupView.vue @@ -7,6 +7,7 @@ import { completeAdminPickup, createAdminPickup, fetchAdminPickups, + fetchAdminPickupShopOptions, fetchAvailableListings, updateAdminPickupProfit, type AdminPickup, @@ -20,7 +21,7 @@ import { adminPath } from '@/shared/utils/adminPath' const loading = ref(false) const items = ref([]) const total = ref(0) -const filters = reactive({ page: 1, page_size: 20, keyword: '', status: '' }) +const filters = reactive({ page: 1, page_size: 20, keyword: '', status: '', shop_name: '' }) const createDialogVisible = ref(false) const completeDialogVisible = ref(false) @@ -31,6 +32,7 @@ const profitSaving = ref(false) const createForm = reactive({ listing_id: null as number | null, platform: '', + shop_name: '', profit_amount: 0, remark: '', }) @@ -39,6 +41,8 @@ const profitForm = reactive({ profit_amount: 0, reason: '' }) const listingOptions = ref([]) const listingLoading = ref(false) +const shopOptions = ref([]) +const shopOptionsLoading = ref(false) const selectedListing = computed( () => listingOptions.value.find(item => item.id === createForm.listing_id) || null ) @@ -58,7 +62,10 @@ const selectedPriceSnapshot = computed(() => { return { pureCent, extraCent, totalCent } }) -onMounted(loadList) +onMounted(() => { + loadList() + loadShopOptions() +}) async function loadList() { loading.value = true @@ -88,12 +95,22 @@ function handleSizeChange(s: number) { function openCreateDialog() { createForm.listing_id = null createForm.platform = '' + createForm.shop_name = '' createForm.profit_amount = 0 createForm.remark = '' listingOptions.value = [] createDialogVisible.value = true } +async function loadShopOptions() { + shopOptionsLoading.value = true + try { + shopOptions.value = await fetchAdminPickupShopOptions() + } finally { + shopOptionsLoading.value = false + } +} + async function searchListings(keyword: string) { if (!keyword) { listingOptions.value = [] @@ -122,11 +139,13 @@ async function handleCreate() { await createAdminPickup({ listing_id: createForm.listing_id, platform: createForm.platform, + shop_name: createForm.shop_name, profit_amount_cent: yuanToCent(createForm.profit_amount), remark: createForm.remark, }) ElMessage.success('提号订单已创建') createDialogVisible.value = false + loadShopOptions() loadList() } catch (e: unknown) { ElMessage.error(errorMessage(e) || '创建失败') @@ -278,6 +297,15 @@ function listingOwnerTotalCent(item: AvailableListing) { + + + 查询 刷新
@@ -299,6 +327,9 @@ function listingOwnerTotalCent(item: AvailableListing) { + + +