From 66af387b4e2838615f3a15830cad1f8796e369e4 Mon Sep 17 00:00:00 2001 From: yml2213 Date: Mon, 27 Jul 2026 11:13:31 +0800 Subject: [PATCH] =?UTF-8?q?=E5=8C=BA=E5=88=86=E7=BA=BF=E4=B8=8B=E6=8F=90?= =?UTF-8?q?=E5=8F=B7=E8=B4=A6=E5=8F=B7=E6=9D=A5=E6=BA=90?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/internal/model/pickup.go | 2 + backend/internal/modules/pickup/dto.go | 30 ++- backend/internal/modules/pickup/handler.go | 8 +- .../internal/modules/pickup/handler_parse.go | 11 +- backend/internal/modules/pickup/repository.go | 73 +++++++- .../modules/pickup/repository_test.go | 136 ++++++++++++++ backend/internal/modules/pickup/service.go | 4 +- .../000044_admin_pickup_account_source.sql | 33 ++++ .../src/features/admin/api/adminPickup.ts | 20 +- .../admin/views/AdminPickupDetailView.vue | 28 +++ .../features/admin/views/AdminPickupView.vue | 174 ++++++++++++++++-- 11 files changed, 483 insertions(+), 36 deletions(-) create mode 100644 backend/migrations/000044_admin_pickup_account_source.sql diff --git a/backend/internal/model/pickup.go b/backend/internal/model/pickup.go index 60a55a8..1ae2d58 100644 --- a/backend/internal/model/pickup.go +++ b/backend/internal/model/pickup.go @@ -18,6 +18,8 @@ type AdminPickup struct { 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"` + AccountSource string `gorm:"size:32;not null;default:'internal';index" json:"account_source"` + SourceChannel string `gorm:"size:32;not null;default:''" json:"source_channel"` 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 7e46be1..6afa71b 100644 --- a/backend/internal/modules/pickup/dto.go +++ b/backend/internal/modules/pickup/dto.go @@ -12,6 +12,11 @@ const ( StatusPickingUp = "picking_up" StatusCompleted = "completed" StatusCancelled = "cancelled" + + AccountSourceInternal = "internal" + AccountSourceExternalPlatformManaged = "external_platform_managed" + ListingSourceExternal = "external" + ListingSourceInternal = "internal" ) var ( @@ -38,6 +43,8 @@ type PickupDTO struct { AdminID uint64 `json:"admin_id"` Platform string `json:"platform"` ShopName string `json:"shop_name"` + AccountSource string `json:"account_source"` + SourceChannel string `json:"source_channel"` ListingPriceCent int64 `json:"listing_price_cent"` OwnerPriceCent int64 `json:"owner_price_cent"` WebsiteProfitCent int64 `json:"website_profit_cent"` @@ -78,11 +85,12 @@ type CancelRequest struct { } type AdminPickupQuery struct { - Page int - PageSize int - Keyword string - Status string - ShopName string + Page int + PageSize int + Keyword string + Status string + ShopName string + AccountSource string } type SellerPickupQuery struct { @@ -108,6 +116,18 @@ type AvailableListingDTO struct { WebsiteProfitCent int64 `json:"website_profit_cent"` SellerRatio float64 `json:"seller_ratio"` BuyerRatio float64 `json:"buyer_ratio"` + AccountSource string `json:"account_source"` + IsExternalUpload bool `json:"is_external_upload"` + SourceChannel string `json:"source_channel"` + HandoffMode string `json:"handoff_mode"` + SettlementMode string `json:"settlement_mode"` +} + +type AvailableListingQuery struct { + Page int + PageSize int + Keyword string + SourceType string } type PaginatedResult struct { diff --git a/backend/internal/modules/pickup/handler.go b/backend/internal/modules/pickup/handler.go index 404eec6..aa6bcd5 100644 --- a/backend/internal/modules/pickup/handler.go +++ b/backend/internal/modules/pickup/handler.go @@ -148,9 +148,13 @@ func (h *Handler) Detail(c *gin.Context) { // AvailableListings 可提号的 listing 搜索(管理员) func (h *Handler) AvailableListings(c *gin.Context) { - keyword := strings.TrimSpace(c.Query("keyword")) page, pageSize := parsePagination(c) - result, err := h.service.ListAvailableListings(c.Request.Context(), keyword, page, pageSize) + result, err := h.service.ListAvailableListings(c.Request.Context(), AvailableListingQuery{ + Keyword: strings.TrimSpace(c.Query("keyword")), + SourceType: strings.TrimSpace(c.Query("source_type")), + Page: page, + PageSize: pageSize, + }) if err != nil { writePickupError(c, err) return diff --git a/backend/internal/modules/pickup/handler_parse.go b/backend/internal/modules/pickup/handler_parse.go index 007c87d..cab57e2 100644 --- a/backend/internal/modules/pickup/handler_parse.go +++ b/backend/internal/modules/pickup/handler_parse.go @@ -54,11 +54,12 @@ func parsePagination(c *gin.Context) (int, int) { func parseAdminQuery(c *gin.Context) AdminPickupQuery { page, pageSize := parsePagination(c) return AdminPickupQuery{ - Page: page, - PageSize: pageSize, - Keyword: c.Query("keyword"), - Status: c.Query("status"), - ShopName: c.Query("shop_name"), + Page: page, + PageSize: pageSize, + Keyword: c.Query("keyword"), + Status: c.Query("status"), + ShopName: c.Query("shop_name"), + AccountSource: c.Query("account_source"), } } diff --git a/backend/internal/modules/pickup/repository.go b/backend/internal/modules/pickup/repository.go index b3ae547..b960ce0 100644 --- a/backend/internal/modules/pickup/repository.go +++ b/backend/internal/modules/pickup/repository.go @@ -67,6 +67,10 @@ func (r *Repository) Create(ctx context.Context, req CreateRequest, adminID uint if err := tx.Clauses(clause.Locking{Strength: "UPDATE"}).First(&account, listing.AccountID).Error; err != nil { return err } + accountSource, sourceChannel, err := resolvePickupAccountSource(tx, listing.ID) + if err != nil { + return err + } priceSnapshot := buildPickupPriceSnapshot(listing, account) accountSnapshot, err := makePickupAccountSnapshot(account, listing) if err != nil { @@ -85,6 +89,8 @@ func (r *Repository) Create(ctx context.Context, req CreateRequest, adminID uint AdminID: adminID, Platform: strings.TrimSpace(req.Platform), ShopName: strings.TrimSpace(req.ShopName), + AccountSource: accountSource, + SourceChannel: sourceChannel, ListingPriceCent: priceSnapshot.ListingPriceCent, OwnerPriceCent: priceSnapshot.OwnerPriceCent, WebsiteProfitCent: priceSnapshot.WebsiteProfitCent, @@ -143,11 +149,13 @@ func (r *Repository) Create(ctx context.Context, req CreateRequest, adminID uint BizID: &bid, Meta: meta, Detail: map[string]any{ - "pickup_no": pickupNo, - "listing_id": listing.ID, - "platform": pickup.Platform, - "shop_name": pickup.ShopName, - "profit_cent": pickup.ProfitAmountCent, + "pickup_no": pickupNo, + "listing_id": listing.ID, + "platform": pickup.Platform, + "shop_name": pickup.ShopName, + "account_source": pickup.AccountSource, + "source_channel": pickup.SourceChannel, + "profit_cent": pickup.ProfitAmountCent, }, }); err != nil { return err @@ -422,6 +430,9 @@ func (r *Repository) ListAdmin(ctx context.Context, query AdminPickupQuery) (*Pa if shopName := strings.TrimSpace(query.ShopName); shopName != "" { db = db.Where("p.shop_name = ?", shopName) } + if accountSource := strings.TrimSpace(query.AccountSource); accountSource != "" { + db = db.Where("p.account_source = ?", accountSource) + } 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) @@ -476,6 +487,7 @@ func (r *Repository) paginatePickups(db *gorm.DB, page, pageSize int, sellerView item.ProfitAmountCent = 0 item.BuyerRatio = 0 item.ShopName = "" + item.SourceChannel = "" } items = append(items, item) } @@ -483,25 +495,41 @@ func (r *Repository) paginatePickups(db *gorm.DB, page, pageSize int, sellerView } // ListAvailableListings 可提号的 listing:已发布、已审核、未在交易中。 -func (r *Repository) ListAvailableListings(ctx context.Context, keyword string, page, pageSize int) (*AvailableListingsResult, error) { +func (r *Repository) ListAvailableListings(ctx context.Context, query AvailableListingQuery) (*AvailableListingsResult, error) { if r == nil || r.db == nil { return nil, ErrDependencyUnavailable } + latestUploads := r.db.WithContext(ctx).Table("listing_uploads"). + Select("listing_id, MAX(id) AS upload_id"). + Where("listing_id IS NOT NULL"). + Group("listing_id") db := r.db.WithContext(ctx).Table("rental_listings AS l"). - Select("l.id, l.listing_no, l.account_id, l.owner_id, l.price_cent AS listing_price_cent, a.title AS account_title, a.server_region, a.login_platform, a.haf_coin_amount, a.asset_summary, owner.phone AS owner_phone"). + Select(`l.id, l.listing_no, l.account_id, l.owner_id, l.price_cent AS listing_price_cent, + a.title AS account_title, a.server_region, a.login_platform, a.haf_coin_amount, a.asset_summary, + owner.phone AS owner_phone, l.handoff_mode, l.settlement_mode, + CASE WHEN lu.id IS NULL THEN 0 ELSE 1 END AS is_external_upload, + COALESCE(lu.source_channel, '') AS source_channel`). Joins("JOIN game_accounts AS a ON a.id = l.account_id"). Joins("JOIN users AS owner ON owner.id = l.owner_id"). + Joins("LEFT JOIN (?) AS latest_upload ON latest_upload.listing_id = l.id", latestUploads). + Joins("LEFT JOIN listing_uploads AS lu ON lu.id = latest_upload.upload_id"). Where("l.status = ? AND l.review_status = ? AND l.in_transaction = ?", "published", "approved", false) - if kw := strings.TrimSpace(keyword); kw != "" { + if kw := strings.TrimSpace(query.Keyword); kw != "" { like := "%" + kw + "%" db = db.Where("l.listing_no LIKE ? OR a.title LIKE ?", like, like) } + switch strings.TrimSpace(query.SourceType) { + case ListingSourceExternal: + db = db.Where("lu.id IS NOT NULL") + case ListingSourceInternal: + db = db.Where("lu.id IS NULL") + } var total int64 if err := db.Count(&total).Error; err != nil { return nil, err } - page, pageSize = normalizePaging(page, pageSize) + page, pageSize := normalizePaging(query.Page, query.PageSize) var rows []availableListingRow if err := db.Order("l.id DESC").Offset((page - 1) * pageSize).Limit(pageSize).Scan(&rows).Error; err != nil { return nil, err @@ -545,6 +573,8 @@ func (row pickupRow) toDTO() PickupDTO { AdminID: row.AdminID, Platform: row.Platform, ShopName: row.ShopName, + AccountSource: row.AccountSource, + SourceChannel: row.SourceChannel, ListingPriceCent: row.ListingPriceCent, OwnerPriceCent: row.OwnerPriceCent, WebsiteProfitCent: row.WebsiteProfitCent, @@ -574,6 +604,10 @@ type availableListingRow struct { ListingPriceCent int64 HafCoinAmount int64 AssetSummary datatypes.JSON + IsExternalUpload bool + SourceChannel string + HandoffMode string + SettlementMode string } func (row availableListingRow) toDTO() AvailableListingDTO { @@ -581,6 +615,10 @@ func (row availableListingRow) toDTO() AvailableListingDTO { model.RentalListing{PriceCent: row.ListingPriceCent}, model.GameAccount{HafCoinAmount: row.HafCoinAmount, AssetSummary: row.AssetSummary}, ) + accountSource := AccountSourceInternal + if row.IsExternalUpload { + accountSource = AccountSourceExternalPlatformManaged + } return AvailableListingDTO{ ID: row.ID, ListingNo: row.ListingNo, @@ -598,9 +636,26 @@ func (row availableListingRow) toDTO() AvailableListingDTO { WebsiteProfitCent: snapshot.WebsiteProfitCent, SellerRatio: snapshot.SellerRatio, BuyerRatio: snapshot.BuyerRatio, + AccountSource: accountSource, + IsExternalUpload: row.IsExternalUpload, + SourceChannel: row.SourceChannel, + HandoffMode: row.HandoffMode, + SettlementMode: row.SettlementMode, } } +func resolvePickupAccountSource(tx *gorm.DB, listingID uint64) (string, string, error) { + var upload model.ListingUpload + err := tx.Where("listing_id = ?", listingID).Order("id DESC").First(&upload).Error + if errors.Is(err, gorm.ErrRecordNotFound) { + return AccountSourceInternal, "", nil + } + if err != nil { + return "", "", err + } + return AccountSourceExternalPlatformManaged, strings.TrimSpace(upload.SourceChannel), nil +} + func makePickupAccountSnapshot(account model.GameAccount, listing model.RentalListing) (datatypes.JSON, error) { payload := map[string]any{ "listing_id": listing.ID, diff --git a/backend/internal/modules/pickup/repository_test.go b/backend/internal/modules/pickup/repository_test.go index 189c437..7de3389 100644 --- a/backend/internal/modules/pickup/repository_test.go +++ b/backend/internal/modules/pickup/repository_test.go @@ -181,6 +181,103 @@ func TestRepositoryListShopNamesReturnsDistinctPlatformOptions(t *testing.T) { } } +func TestRepositoryListAvailableListingsFiltersAccountSource(t *testing.T) { + repo, db := newPickupTestRepo(t) + internal := seedAvailableListing(t, db, "LST-SOURCE-INTERNAL", "站内账号", false) + external := seedAvailableListing(t, db, "LST-SOURCE-EXTERNAL", "外部账号", true) + if err := db.Create(&model.ListingUpload{ + UploaderName: "外部号主", + SourceChannel: "淘宝", + ListingID: &external.ID, + Status: "listing_created", + }).Error; err != nil { + t.Fatalf("创建外部上传记录失败: %v", err) + } + + all, err := repo.ListAvailableListings(t.Context(), AvailableListingQuery{Page: 1, PageSize: 20}) + if err != nil { + t.Fatalf("ListAvailableListings() error = %v", err) + } + if all.Total != 2 || len(all.Items) != 2 { + t.Fatalf("全部来源结果 = total %d, items %d, want 2", all.Total, len(all.Items)) + } + + externalResult, err := repo.ListAvailableListings(t.Context(), AvailableListingQuery{ + SourceType: ListingSourceExternal, + Page: 1, + PageSize: 20, + }) + if err != nil { + t.Fatalf("筛选外部上传 error = %v", err) + } + if externalResult.Total != 1 || len(externalResult.Items) != 1 { + t.Fatalf("外部上传结果 = total %d, items %d, want 1", externalResult.Total, len(externalResult.Items)) + } + externalItem := externalResult.Items[0] + if externalItem.ID != external.ID || !externalItem.IsExternalUpload { + t.Fatalf("外部上传账号 = id %d, external %t, want id %d", externalItem.ID, externalItem.IsExternalUpload, external.ID) + } + if externalItem.AccountSource != AccountSourceExternalPlatformManaged || externalItem.SourceChannel != "淘宝" { + t.Fatalf("外部上传来源 = %q/%q", externalItem.AccountSource, externalItem.SourceChannel) + } + if externalItem.HandoffMode != "platform" || externalItem.SettlementMode != "platform_managed" { + t.Fatalf("外部上传代管模式 = %q/%q", externalItem.HandoffMode, externalItem.SettlementMode) + } + + internalResult, err := repo.ListAvailableListings(t.Context(), AvailableListingQuery{ + SourceType: ListingSourceInternal, + Page: 1, + PageSize: 20, + }) + if err != nil { + t.Fatalf("筛选站内上传 error = %v", err) + } + if internalResult.Total != 1 || len(internalResult.Items) != 1 { + t.Fatalf("站内上传结果 = total %d, items %d, want 1", internalResult.Total, len(internalResult.Items)) + } + internalItem := internalResult.Items[0] + if internalItem.ID != internal.ID || internalItem.IsExternalUpload || internalItem.AccountSource != AccountSourceInternal { + t.Fatalf("站内上传账号 = id %d, external %t, source %q", internalItem.ID, internalItem.IsExternalUpload, internalItem.AccountSource) + } +} + +func TestRepositoryCreateSnapshotsExternalAccountSource(t *testing.T) { + repo, db := newPickupTestRepo(t) + listing := seedAvailableListing(t, db, "LST-SOURCE-CREATE", "待提号外部账号", true) + upload := model.ListingUpload{ + UploaderName: "外部号主", + SourceChannel: "闲鱼", + ListingID: &listing.ID, + Status: "listing_created", + } + if err := db.Create(&upload).Error; err != nil { + t.Fatalf("创建外部上传记录失败: %v", err) + } + + item, err := repo.Create(t.Context(), CreateRequest{ + ListingID: listing.ID, + Platform: "微信", + ProfitAmountCent: 1200, + }, 99, auditlog.Meta{RequestID: "req-source"}) + if err != nil { + t.Fatalf("Create() error = %v", err) + } + if item.AccountSource != AccountSourceExternalPlatformManaged || item.SourceChannel != "闲鱼" { + t.Fatalf("创建来源快照 = %q/%q", item.AccountSource, item.SourceChannel) + } + + if err := db.Delete(&upload).Error; err != nil { + t.Fatalf("删除上传关联失败: %v", err) + } + stored, err := repo.FindByID(t.Context(), item.ID) + if err != nil { + t.Fatalf("FindByID() error = %v", err) + } + if stored.AccountSource != AccountSourceExternalPlatformManaged || stored.SourceChannel != "闲鱼" { + t.Fatalf("关联删除后的来源快照 = %q/%q", stored.AccountSource, stored.SourceChannel) + } +} + func assertInt64(t *testing.T, name string, got, want int64) { t.Helper() if got != want { @@ -208,14 +305,53 @@ func newPickupTestRepo(t *testing.T) (*Repository, *gorm.DB) { &model.User{}, &model.GameAccount{}, &model.RentalListing{}, + &model.ListingUpload{}, &model.AdminPickup{}, &model.AuditLog{}, + &model.Notification{}, + &model.ListingStatusEvent{}, ); err != nil { t.Fatalf("迁移测试表失败: %v", err) } return NewRepository(db), db } +func seedAvailableListing(t *testing.T, db *gorm.DB, listingNo, title string, external bool) model.RentalListing { + t.Helper() + owner := model.User{Phone: "188" + listingNo} + if err := db.Create(&owner).Error; err != nil { + t.Fatalf("创建候选号主失败: %v", err) + } + account := model.GameAccount{ + OwnerID: owner.ID, + ServerRegion: "测试区", + LoginPlatform: "微信", + Title: title, + Status: "published", + } + if err := db.Create(&account).Error; err != nil { + t.Fatalf("创建候选账号失败: %v", err) + } + listing := model.RentalListing{ + ListingNo: listingNo, + AccountID: account.ID, + OwnerID: owner.ID, + PriceCent: 10000, + Status: "published", + ReviewStatus: "approved", + HandoffMode: "owner", + SettlementMode: "owner_wallet", + } + if external { + listing.HandoffMode = "platform" + listing.SettlementMode = "platform_managed" + } + if err := db.Create(&listing).Error; err != nil { + t.Fatalf("创建候选上架记录失败: %v", err) + } + return listing +} + func seedPickup(t *testing.T, db *gorm.DB, status string) model.AdminPickup { t.Helper() owner := model.User{Phone: "18800000000"} diff --git a/backend/internal/modules/pickup/service.go b/backend/internal/modules/pickup/service.go index 72bb868..4d1fd11 100644 --- a/backend/internal/modules/pickup/service.go +++ b/backend/internal/modules/pickup/service.go @@ -91,9 +91,9 @@ func (s *Service) ListForSeller(ctx context.Context, ownerID uint64, query Selle return s.repo.ListForSeller(ctx, ownerID, query) } -func (s *Service) ListAvailableListings(ctx context.Context, keyword string, page, pageSize int) (*AvailableListingsResult, error) { +func (s *Service) ListAvailableListings(ctx context.Context, query AvailableListingQuery) (*AvailableListingsResult, error) { if s.repo == nil { return nil, ErrDependencyUnavailable } - return s.repo.ListAvailableListings(ctx, keyword, page, pageSize) + return s.repo.ListAvailableListings(ctx, query) } diff --git a/backend/migrations/000044_admin_pickup_account_source.sql b/backend/migrations/000044_admin_pickup_account_source.sql new file mode 100644 index 0000000..af83726 --- /dev/null +++ b/backend/migrations/000044_admin_pickup_account_source.sql @@ -0,0 +1,33 @@ +-- +goose Up + +ALTER TABLE admin_pickups + ADD COLUMN account_source VARCHAR(32) NOT NULL DEFAULT 'internal' COMMENT '账号来源快照: internal站内上传/external_platform_managed外部上传平台代管' AFTER shop_name, + ADD COLUMN source_channel VARCHAR(32) NOT NULL DEFAULT '' COMMENT '外部上传来源渠道快照' AFTER account_source, + ADD KEY idx_admin_pickups_account_source (account_source, created_at); + +UPDATE admin_pickups AS p +LEFT JOIN ( + SELECT uploads.listing_id, uploads.source_channel + FROM listing_uploads AS uploads + INNER JOIN ( + SELECT listing_id, MAX(id) AS upload_id + FROM listing_uploads + WHERE listing_id IS NOT NULL + GROUP BY listing_id + ) AS latest ON latest.upload_id = uploads.id +) AS lu ON lu.listing_id = p.listing_id +SET p.account_source = CASE + WHEN lu.listing_id IS NULL THEN 'internal' + ELSE 'external_platform_managed' + END, + p.source_channel = CASE + WHEN lu.listing_id IS NULL THEN '' + ELSE COALESCE(lu.source_channel, '') + END; + +-- +goose Down + +ALTER TABLE admin_pickups + DROP KEY idx_admin_pickups_account_source, + DROP COLUMN source_channel, + DROP COLUMN account_source; diff --git a/frontend/src/features/admin/api/adminPickup.ts b/frontend/src/features/admin/api/adminPickup.ts index 18f0f7d..8287b2d 100644 --- a/frontend/src/features/admin/api/adminPickup.ts +++ b/frontend/src/features/admin/api/adminPickup.ts @@ -1,6 +1,9 @@ import { apiClient } from '@/shared/api/client' import type { ApiResponse, PaginatedResult } from '@/shared/types/types' +export type PickupAccountSource = 'internal' | 'external_platform_managed' +export type AvailableListingSourceType = '' | 'external' | 'internal' + export interface AdminPickup { id: number pickup_no: string @@ -15,6 +18,8 @@ export interface AdminPickup { admin_id: number platform: string shop_name: string + account_source: PickupAccountSource + source_channel: string listing_price_cent: number owner_price_cent: number website_profit_cent: number @@ -48,6 +53,11 @@ export interface AvailableListing { website_profit_cent: number seller_ratio: number buyer_ratio: number + account_source: PickupAccountSource + is_external_upload: boolean + source_channel: string + handoff_mode: string + settlement_mode: string } export interface AdminPickupCreateRequest { @@ -75,6 +85,7 @@ export interface AdminPickupListQuery { keyword?: string status?: string shop_name?: string + account_source?: PickupAccountSource | '' } function cleanParams(query: object) { @@ -109,10 +120,15 @@ export async function fetchAdminPickupShopOptions(platform = '') { return Array.isArray(data.data) ? data.data : [] } -export async function fetchAvailableListings(keyword: string, page = 1, page_size = 20) { +export async function fetchAvailableListings( + keyword = '', + page = 1, + page_size = 20, + source_type: AvailableListingSourceType = '' +) { const { data } = await apiClient.get>>( '/admin/pickups/available-listings', - { params: cleanParams({ keyword, page, page_size }) } + { params: cleanParams({ keyword, page, page_size, source_type }) } ) const result = data.data return { diff --git a/frontend/src/features/admin/views/AdminPickupDetailView.vue b/frontend/src/features/admin/views/AdminPickupDetailView.vue index f4a784d..0fc4744 100644 --- a/frontend/src/features/admin/views/AdminPickupDetailView.vue +++ b/frontend/src/features/admin/views/AdminPickupDetailView.vue @@ -149,6 +149,14 @@ function statusType(status: string) { return '' } +function pickupSourceLabel(source: AdminPickup['account_source']) { + return source === 'external_platform_managed' ? '外部上传 · 平台代管' : '站内上传' +} + +function pickupSourceTagType(source: AdminPickup['account_source']) { + return source === 'external_platform_managed' ? 'primary' : 'info' +} + function ratioText(value: number) { const ratio = Number(value || 0) if (ratio <= 0) return '-' @@ -296,6 +304,19 @@ function readSnapshotResources(summary: Record | null): Snapsho
号主
{{ pickup.owner_phone || pickup.owner_id }}
+
+
账号来源
+
+ + {{ pickupSourceLabel(pickup.account_source) }} + + {{ pickup.source_channel }} +
+
交易渠道
{{ pickup.platform || '-' }}
@@ -518,6 +539,13 @@ function readSnapshotResources(summary: Record | null): Snapsho white-space: pre-wrap; } +.source-detail { + display: flex; + flex-wrap: wrap; + align-items: center; + gap: 6px 8px; +} + .money-list { display: grid; gap: 8px; diff --git a/frontend/src/features/admin/views/AdminPickupView.vue b/frontend/src/features/admin/views/AdminPickupView.vue index 3dcdf93..a851f1d 100644 --- a/frontend/src/features/admin/views/AdminPickupView.vue +++ b/frontend/src/features/admin/views/AdminPickupView.vue @@ -12,6 +12,8 @@ import { updateAdminPickupProfit, type AdminPickup, type AvailableListing, + type AvailableListingSourceType, + type PickupAccountSource, } from '@/features/admin/api/adminPickup' import { centToYuan, formatCentWithSymbol, yuanToCent } from '@/shared/utils/money' import { formatDateTime } from '@/shared/utils/time' @@ -21,7 +23,14 @@ 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: '', shop_name: '' }) +const filters = reactive({ + page: 1, + page_size: 20, + keyword: '', + status: '', + shop_name: '', + account_source: '' as PickupAccountSource | '', +}) const createDialogVisible = ref(false) const completeDialogVisible = ref(false) @@ -41,6 +50,9 @@ const profitForm = reactive({ profit_amount: 0, reason: '' }) const listingOptions = ref([]) const listingLoading = ref(false) +const listingKeyword = ref('') +const listingSourceType = ref('') +let listingRequestID = 0 const shopOptions = ref([]) const shopOptionsLoading = ref(false) const selectedListing = computed( @@ -99,7 +111,10 @@ function openCreateDialog() { createForm.profit_amount = 0 createForm.remark = '' listingOptions.value = [] + listingKeyword.value = '' + listingSourceType.value = '' createDialogVisible.value = true + void searchListings('') } async function loadShopOptions() { @@ -112,19 +127,32 @@ async function loadShopOptions() { } async function searchListings(keyword: string) { - if (!keyword) { - listingOptions.value = [] - return - } + listingKeyword.value = keyword.trim() + const requestID = ++listingRequestID listingLoading.value = true try { - const result = await fetchAvailableListings(keyword) - listingOptions.value = result.items + const result = await fetchAvailableListings( + listingKeyword.value, + 1, + 20, + listingSourceType.value + ) + if (requestID === listingRequestID) { + listingOptions.value = result.items + } } finally { - listingLoading.value = false + if (requestID === listingRequestID) { + listingLoading.value = false + } } } +function handleListingSourceChange() { + createForm.listing_id = null + listingOptions.value = [] + void searchListings(listingKeyword.value) +} + async function handleCreate() { if (!createForm.listing_id) { ElMessage.warning('请选择要提号的账号') @@ -269,6 +297,27 @@ function normalizeCent(value: number | undefined | null) { function listingOwnerTotalCent(item: AvailableListing) { return normalizeCent(item.owner_total_price_cent || item.owner_price_cent) } + +function isExternalSource(source: PickupAccountSource) { + return source === 'external_platform_managed' +} + +function pickupSourceLabel(source: PickupAccountSource) { + return isExternalSource(source) ? '外部上传 · 平台代管' : '站内上传' +} + +function pickupSourceTagType(source: PickupAccountSource) { + return isExternalSource(source) ? 'primary' : 'info' +} + +function listingSourceText(item: AvailableListing) { + const label = pickupSourceLabel(item.account_source) + return item.source_channel ? `${label} · ${item.source_channel}` : label +} + +function listingOptionLabel(item: AvailableListing) { + return `${listingSourceText(item)} · ${item.listing_no} · ${item.account_title} · ${formatCentWithSymbol(listingOwnerTotalCent(item))} / ${formatCentWithSymbol(item.listing_price_cent)}` +}