区分线下提号账号来源
This commit is contained in:
@@ -18,6 +18,8 @@ type AdminPickup struct {
|
|||||||
AdminID uint64 `gorm:"not null" json:"admin_id"`
|
AdminID uint64 `gorm:"not null" json:"admin_id"`
|
||||||
Platform string `gorm:"size:32;not null;default:''" json:"platform"`
|
Platform string `gorm:"size:32;not null;default:''" json:"platform"`
|
||||||
ShopName string `gorm:"size:100;not null;default:'';index" json:"shop_name"`
|
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"`
|
ListingPriceCent int64 `gorm:"not null;default:0" json:"listing_price_cent"`
|
||||||
OwnerPriceCent int64 `gorm:"not null;default:0" json:"owner_price_cent"`
|
OwnerPriceCent int64 `gorm:"not null;default:0" json:"owner_price_cent"`
|
||||||
WebsiteProfitCent int64 `gorm:"not null;default:0" json:"website_profit_cent"`
|
WebsiteProfitCent int64 `gorm:"not null;default:0" json:"website_profit_cent"`
|
||||||
|
|||||||
@@ -12,6 +12,11 @@ const (
|
|||||||
StatusPickingUp = "picking_up"
|
StatusPickingUp = "picking_up"
|
||||||
StatusCompleted = "completed"
|
StatusCompleted = "completed"
|
||||||
StatusCancelled = "cancelled"
|
StatusCancelled = "cancelled"
|
||||||
|
|
||||||
|
AccountSourceInternal = "internal"
|
||||||
|
AccountSourceExternalPlatformManaged = "external_platform_managed"
|
||||||
|
ListingSourceExternal = "external"
|
||||||
|
ListingSourceInternal = "internal"
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
@@ -38,6 +43,8 @@ type PickupDTO struct {
|
|||||||
AdminID uint64 `json:"admin_id"`
|
AdminID uint64 `json:"admin_id"`
|
||||||
Platform string `json:"platform"`
|
Platform string `json:"platform"`
|
||||||
ShopName string `json:"shop_name"`
|
ShopName string `json:"shop_name"`
|
||||||
|
AccountSource string `json:"account_source"`
|
||||||
|
SourceChannel string `json:"source_channel"`
|
||||||
ListingPriceCent int64 `json:"listing_price_cent"`
|
ListingPriceCent int64 `json:"listing_price_cent"`
|
||||||
OwnerPriceCent int64 `json:"owner_price_cent"`
|
OwnerPriceCent int64 `json:"owner_price_cent"`
|
||||||
WebsiteProfitCent int64 `json:"website_profit_cent"`
|
WebsiteProfitCent int64 `json:"website_profit_cent"`
|
||||||
@@ -78,11 +85,12 @@ type CancelRequest struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type AdminPickupQuery struct {
|
type AdminPickupQuery struct {
|
||||||
Page int
|
Page int
|
||||||
PageSize int
|
PageSize int
|
||||||
Keyword string
|
Keyword string
|
||||||
Status string
|
Status string
|
||||||
ShopName string
|
ShopName string
|
||||||
|
AccountSource string
|
||||||
}
|
}
|
||||||
|
|
||||||
type SellerPickupQuery struct {
|
type SellerPickupQuery struct {
|
||||||
@@ -108,6 +116,18 @@ type AvailableListingDTO struct {
|
|||||||
WebsiteProfitCent int64 `json:"website_profit_cent"`
|
WebsiteProfitCent int64 `json:"website_profit_cent"`
|
||||||
SellerRatio float64 `json:"seller_ratio"`
|
SellerRatio float64 `json:"seller_ratio"`
|
||||||
BuyerRatio float64 `json:"buyer_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 {
|
type PaginatedResult struct {
|
||||||
|
|||||||
@@ -148,9 +148,13 @@ func (h *Handler) Detail(c *gin.Context) {
|
|||||||
|
|
||||||
// AvailableListings 可提号的 listing 搜索(管理员)
|
// AvailableListings 可提号的 listing 搜索(管理员)
|
||||||
func (h *Handler) AvailableListings(c *gin.Context) {
|
func (h *Handler) AvailableListings(c *gin.Context) {
|
||||||
keyword := strings.TrimSpace(c.Query("keyword"))
|
|
||||||
page, pageSize := parsePagination(c)
|
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 {
|
if err != nil {
|
||||||
writePickupError(c, err)
|
writePickupError(c, err)
|
||||||
return
|
return
|
||||||
|
|||||||
@@ -54,11 +54,12 @@ func parsePagination(c *gin.Context) (int, int) {
|
|||||||
func parseAdminQuery(c *gin.Context) AdminPickupQuery {
|
func parseAdminQuery(c *gin.Context) AdminPickupQuery {
|
||||||
page, pageSize := parsePagination(c)
|
page, pageSize := parsePagination(c)
|
||||||
return AdminPickupQuery{
|
return AdminPickupQuery{
|
||||||
Page: page,
|
Page: page,
|
||||||
PageSize: pageSize,
|
PageSize: pageSize,
|
||||||
Keyword: c.Query("keyword"),
|
Keyword: c.Query("keyword"),
|
||||||
Status: c.Query("status"),
|
Status: c.Query("status"),
|
||||||
ShopName: c.Query("shop_name"),
|
ShopName: c.Query("shop_name"),
|
||||||
|
AccountSource: c.Query("account_source"),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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 {
|
if err := tx.Clauses(clause.Locking{Strength: "UPDATE"}).First(&account, listing.AccountID).Error; err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
accountSource, sourceChannel, err := resolvePickupAccountSource(tx, listing.ID)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
priceSnapshot := buildPickupPriceSnapshot(listing, account)
|
priceSnapshot := buildPickupPriceSnapshot(listing, account)
|
||||||
accountSnapshot, err := makePickupAccountSnapshot(account, listing)
|
accountSnapshot, err := makePickupAccountSnapshot(account, listing)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -85,6 +89,8 @@ func (r *Repository) Create(ctx context.Context, req CreateRequest, adminID uint
|
|||||||
AdminID: adminID,
|
AdminID: adminID,
|
||||||
Platform: strings.TrimSpace(req.Platform),
|
Platform: strings.TrimSpace(req.Platform),
|
||||||
ShopName: strings.TrimSpace(req.ShopName),
|
ShopName: strings.TrimSpace(req.ShopName),
|
||||||
|
AccountSource: accountSource,
|
||||||
|
SourceChannel: sourceChannel,
|
||||||
ListingPriceCent: priceSnapshot.ListingPriceCent,
|
ListingPriceCent: priceSnapshot.ListingPriceCent,
|
||||||
OwnerPriceCent: priceSnapshot.OwnerPriceCent,
|
OwnerPriceCent: priceSnapshot.OwnerPriceCent,
|
||||||
WebsiteProfitCent: priceSnapshot.WebsiteProfitCent,
|
WebsiteProfitCent: priceSnapshot.WebsiteProfitCent,
|
||||||
@@ -143,11 +149,13 @@ func (r *Repository) Create(ctx context.Context, req CreateRequest, adminID uint
|
|||||||
BizID: &bid,
|
BizID: &bid,
|
||||||
Meta: meta,
|
Meta: meta,
|
||||||
Detail: map[string]any{
|
Detail: map[string]any{
|
||||||
"pickup_no": pickupNo,
|
"pickup_no": pickupNo,
|
||||||
"listing_id": listing.ID,
|
"listing_id": listing.ID,
|
||||||
"platform": pickup.Platform,
|
"platform": pickup.Platform,
|
||||||
"shop_name": pickup.ShopName,
|
"shop_name": pickup.ShopName,
|
||||||
"profit_cent": pickup.ProfitAmountCent,
|
"account_source": pickup.AccountSource,
|
||||||
|
"source_channel": pickup.SourceChannel,
|
||||||
|
"profit_cent": pickup.ProfitAmountCent,
|
||||||
},
|
},
|
||||||
}); err != nil {
|
}); err != nil {
|
||||||
return err
|
return err
|
||||||
@@ -422,6 +430,9 @@ func (r *Repository) ListAdmin(ctx context.Context, query AdminPickupQuery) (*Pa
|
|||||||
if shopName := strings.TrimSpace(query.ShopName); shopName != "" {
|
if shopName := strings.TrimSpace(query.ShopName); shopName != "" {
|
||||||
db = db.Where("p.shop_name = ?", 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 != "" {
|
if kw := strings.TrimSpace(query.Keyword); kw != "" {
|
||||||
like := "%" + kw + "%"
|
like := "%" + kw + "%"
|
||||||
db = db.Where("p.pickup_no LIKE ? OR l.listing_no LIKE ? OR a.title LIKE ?", like, like, like)
|
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.ProfitAmountCent = 0
|
||||||
item.BuyerRatio = 0
|
item.BuyerRatio = 0
|
||||||
item.ShopName = ""
|
item.ShopName = ""
|
||||||
|
item.SourceChannel = ""
|
||||||
}
|
}
|
||||||
items = append(items, item)
|
items = append(items, item)
|
||||||
}
|
}
|
||||||
@@ -483,25 +495,41 @@ func (r *Repository) paginatePickups(db *gorm.DB, page, pageSize int, sellerView
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ListAvailableListings 可提号的 listing:已发布、已审核、未在交易中。
|
// 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 {
|
if r == nil || r.db == nil {
|
||||||
return nil, ErrDependencyUnavailable
|
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").
|
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 game_accounts AS a ON a.id = l.account_id").
|
||||||
Joins("JOIN users AS owner ON owner.id = l.owner_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)
|
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 + "%"
|
like := "%" + kw + "%"
|
||||||
db = db.Where("l.listing_no LIKE ? OR a.title LIKE ?", like, like)
|
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
|
var total int64
|
||||||
if err := db.Count(&total).Error; err != nil {
|
if err := db.Count(&total).Error; err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
page, pageSize = normalizePaging(page, pageSize)
|
page, pageSize := normalizePaging(query.Page, query.PageSize)
|
||||||
var rows []availableListingRow
|
var rows []availableListingRow
|
||||||
if err := db.Order("l.id DESC").Offset((page - 1) * pageSize).Limit(pageSize).Scan(&rows).Error; err != nil {
|
if err := db.Order("l.id DESC").Offset((page - 1) * pageSize).Limit(pageSize).Scan(&rows).Error; err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@@ -545,6 +573,8 @@ func (row pickupRow) toDTO() PickupDTO {
|
|||||||
AdminID: row.AdminID,
|
AdminID: row.AdminID,
|
||||||
Platform: row.Platform,
|
Platform: row.Platform,
|
||||||
ShopName: row.ShopName,
|
ShopName: row.ShopName,
|
||||||
|
AccountSource: row.AccountSource,
|
||||||
|
SourceChannel: row.SourceChannel,
|
||||||
ListingPriceCent: row.ListingPriceCent,
|
ListingPriceCent: row.ListingPriceCent,
|
||||||
OwnerPriceCent: row.OwnerPriceCent,
|
OwnerPriceCent: row.OwnerPriceCent,
|
||||||
WebsiteProfitCent: row.WebsiteProfitCent,
|
WebsiteProfitCent: row.WebsiteProfitCent,
|
||||||
@@ -574,6 +604,10 @@ type availableListingRow struct {
|
|||||||
ListingPriceCent int64
|
ListingPriceCent int64
|
||||||
HafCoinAmount int64
|
HafCoinAmount int64
|
||||||
AssetSummary datatypes.JSON
|
AssetSummary datatypes.JSON
|
||||||
|
IsExternalUpload bool
|
||||||
|
SourceChannel string
|
||||||
|
HandoffMode string
|
||||||
|
SettlementMode string
|
||||||
}
|
}
|
||||||
|
|
||||||
func (row availableListingRow) toDTO() AvailableListingDTO {
|
func (row availableListingRow) toDTO() AvailableListingDTO {
|
||||||
@@ -581,6 +615,10 @@ func (row availableListingRow) toDTO() AvailableListingDTO {
|
|||||||
model.RentalListing{PriceCent: row.ListingPriceCent},
|
model.RentalListing{PriceCent: row.ListingPriceCent},
|
||||||
model.GameAccount{HafCoinAmount: row.HafCoinAmount, AssetSummary: row.AssetSummary},
|
model.GameAccount{HafCoinAmount: row.HafCoinAmount, AssetSummary: row.AssetSummary},
|
||||||
)
|
)
|
||||||
|
accountSource := AccountSourceInternal
|
||||||
|
if row.IsExternalUpload {
|
||||||
|
accountSource = AccountSourceExternalPlatformManaged
|
||||||
|
}
|
||||||
return AvailableListingDTO{
|
return AvailableListingDTO{
|
||||||
ID: row.ID,
|
ID: row.ID,
|
||||||
ListingNo: row.ListingNo,
|
ListingNo: row.ListingNo,
|
||||||
@@ -598,9 +636,26 @@ func (row availableListingRow) toDTO() AvailableListingDTO {
|
|||||||
WebsiteProfitCent: snapshot.WebsiteProfitCent,
|
WebsiteProfitCent: snapshot.WebsiteProfitCent,
|
||||||
SellerRatio: snapshot.SellerRatio,
|
SellerRatio: snapshot.SellerRatio,
|
||||||
BuyerRatio: snapshot.BuyerRatio,
|
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) {
|
func makePickupAccountSnapshot(account model.GameAccount, listing model.RentalListing) (datatypes.JSON, error) {
|
||||||
payload := map[string]any{
|
payload := map[string]any{
|
||||||
"listing_id": listing.ID,
|
"listing_id": listing.ID,
|
||||||
|
|||||||
@@ -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) {
|
func assertInt64(t *testing.T, name string, got, want int64) {
|
||||||
t.Helper()
|
t.Helper()
|
||||||
if got != want {
|
if got != want {
|
||||||
@@ -208,14 +305,53 @@ func newPickupTestRepo(t *testing.T) (*Repository, *gorm.DB) {
|
|||||||
&model.User{},
|
&model.User{},
|
||||||
&model.GameAccount{},
|
&model.GameAccount{},
|
||||||
&model.RentalListing{},
|
&model.RentalListing{},
|
||||||
|
&model.ListingUpload{},
|
||||||
&model.AdminPickup{},
|
&model.AdminPickup{},
|
||||||
&model.AuditLog{},
|
&model.AuditLog{},
|
||||||
|
&model.Notification{},
|
||||||
|
&model.ListingStatusEvent{},
|
||||||
); err != nil {
|
); err != nil {
|
||||||
t.Fatalf("迁移测试表失败: %v", err)
|
t.Fatalf("迁移测试表失败: %v", err)
|
||||||
}
|
}
|
||||||
return NewRepository(db), db
|
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 {
|
func seedPickup(t *testing.T, db *gorm.DB, status string) model.AdminPickup {
|
||||||
t.Helper()
|
t.Helper()
|
||||||
owner := model.User{Phone: "18800000000"}
|
owner := model.User{Phone: "18800000000"}
|
||||||
|
|||||||
@@ -91,9 +91,9 @@ func (s *Service) ListForSeller(ctx context.Context, ownerID uint64, query Selle
|
|||||||
return s.repo.ListForSeller(ctx, ownerID, query)
|
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 {
|
if s.repo == nil {
|
||||||
return nil, ErrDependencyUnavailable
|
return nil, ErrDependencyUnavailable
|
||||||
}
|
}
|
||||||
return s.repo.ListAvailableListings(ctx, keyword, page, pageSize)
|
return s.repo.ListAvailableListings(ctx, query)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
@@ -1,6 +1,9 @@
|
|||||||
import { apiClient } from '@/shared/api/client'
|
import { apiClient } from '@/shared/api/client'
|
||||||
import type { ApiResponse, PaginatedResult } from '@/shared/types/types'
|
import type { ApiResponse, PaginatedResult } from '@/shared/types/types'
|
||||||
|
|
||||||
|
export type PickupAccountSource = 'internal' | 'external_platform_managed'
|
||||||
|
export type AvailableListingSourceType = '' | 'external' | 'internal'
|
||||||
|
|
||||||
export interface AdminPickup {
|
export interface AdminPickup {
|
||||||
id: number
|
id: number
|
||||||
pickup_no: string
|
pickup_no: string
|
||||||
@@ -15,6 +18,8 @@ export interface AdminPickup {
|
|||||||
admin_id: number
|
admin_id: number
|
||||||
platform: string
|
platform: string
|
||||||
shop_name: string
|
shop_name: string
|
||||||
|
account_source: PickupAccountSource
|
||||||
|
source_channel: string
|
||||||
listing_price_cent: number
|
listing_price_cent: number
|
||||||
owner_price_cent: number
|
owner_price_cent: number
|
||||||
website_profit_cent: number
|
website_profit_cent: number
|
||||||
@@ -48,6 +53,11 @@ export interface AvailableListing {
|
|||||||
website_profit_cent: number
|
website_profit_cent: number
|
||||||
seller_ratio: number
|
seller_ratio: number
|
||||||
buyer_ratio: number
|
buyer_ratio: number
|
||||||
|
account_source: PickupAccountSource
|
||||||
|
is_external_upload: boolean
|
||||||
|
source_channel: string
|
||||||
|
handoff_mode: string
|
||||||
|
settlement_mode: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface AdminPickupCreateRequest {
|
export interface AdminPickupCreateRequest {
|
||||||
@@ -75,6 +85,7 @@ export interface AdminPickupListQuery {
|
|||||||
keyword?: string
|
keyword?: string
|
||||||
status?: string
|
status?: string
|
||||||
shop_name?: string
|
shop_name?: string
|
||||||
|
account_source?: PickupAccountSource | ''
|
||||||
}
|
}
|
||||||
|
|
||||||
function cleanParams(query: object) {
|
function cleanParams(query: object) {
|
||||||
@@ -109,10 +120,15 @@ export async function fetchAdminPickupShopOptions(platform = '') {
|
|||||||
return Array.isArray(data.data) ? data.data : []
|
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<ApiResponse<PaginatedResult<AvailableListing>>>(
|
const { data } = await apiClient.get<ApiResponse<PaginatedResult<AvailableListing>>>(
|
||||||
'/admin/pickups/available-listings',
|
'/admin/pickups/available-listings',
|
||||||
{ params: cleanParams({ keyword, page, page_size }) }
|
{ params: cleanParams({ keyword, page, page_size, source_type }) }
|
||||||
)
|
)
|
||||||
const result = data.data
|
const result = data.data
|
||||||
return {
|
return {
|
||||||
|
|||||||
@@ -149,6 +149,14 @@ function statusType(status: string) {
|
|||||||
return ''
|
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) {
|
function ratioText(value: number) {
|
||||||
const ratio = Number(value || 0)
|
const ratio = Number(value || 0)
|
||||||
if (ratio <= 0) return '-'
|
if (ratio <= 0) return '-'
|
||||||
@@ -296,6 +304,19 @@ function readSnapshotResources(summary: Record<string, unknown> | null): Snapsho
|
|||||||
<dt>号主</dt>
|
<dt>号主</dt>
|
||||||
<dd>{{ pickup.owner_phone || pickup.owner_id }}</dd>
|
<dd>{{ pickup.owner_phone || pickup.owner_id }}</dd>
|
||||||
</div>
|
</div>
|
||||||
|
<div>
|
||||||
|
<dt>账号来源</dt>
|
||||||
|
<dd class="source-detail">
|
||||||
|
<el-tag
|
||||||
|
:type="pickupSourceTagType(pickup.account_source)"
|
||||||
|
effect="light"
|
||||||
|
size="small"
|
||||||
|
>
|
||||||
|
{{ pickupSourceLabel(pickup.account_source) }}
|
||||||
|
</el-tag>
|
||||||
|
<span v-if="pickup.source_channel">{{ pickup.source_channel }}</span>
|
||||||
|
</dd>
|
||||||
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<dt>交易渠道</dt>
|
<dt>交易渠道</dt>
|
||||||
<dd>{{ pickup.platform || '-' }}</dd>
|
<dd>{{ pickup.platform || '-' }}</dd>
|
||||||
@@ -518,6 +539,13 @@ function readSnapshotResources(summary: Record<string, unknown> | null): Snapsho
|
|||||||
white-space: pre-wrap;
|
white-space: pre-wrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.source-detail {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
align-items: center;
|
||||||
|
gap: 6px 8px;
|
||||||
|
}
|
||||||
|
|
||||||
.money-list {
|
.money-list {
|
||||||
display: grid;
|
display: grid;
|
||||||
gap: 8px;
|
gap: 8px;
|
||||||
|
|||||||
@@ -12,6 +12,8 @@ import {
|
|||||||
updateAdminPickupProfit,
|
updateAdminPickupProfit,
|
||||||
type AdminPickup,
|
type AdminPickup,
|
||||||
type AvailableListing,
|
type AvailableListing,
|
||||||
|
type AvailableListingSourceType,
|
||||||
|
type PickupAccountSource,
|
||||||
} from '@/features/admin/api/adminPickup'
|
} from '@/features/admin/api/adminPickup'
|
||||||
import { centToYuan, formatCentWithSymbol, yuanToCent } from '@/shared/utils/money'
|
import { centToYuan, formatCentWithSymbol, yuanToCent } from '@/shared/utils/money'
|
||||||
import { formatDateTime } from '@/shared/utils/time'
|
import { formatDateTime } from '@/shared/utils/time'
|
||||||
@@ -21,7 +23,14 @@ import { adminPath } from '@/shared/utils/adminPath'
|
|||||||
const loading = ref(false)
|
const loading = ref(false)
|
||||||
const items = ref<AdminPickup[]>([])
|
const items = ref<AdminPickup[]>([])
|
||||||
const total = ref(0)
|
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 createDialogVisible = ref(false)
|
||||||
const completeDialogVisible = ref(false)
|
const completeDialogVisible = ref(false)
|
||||||
@@ -41,6 +50,9 @@ const profitForm = reactive({ profit_amount: 0, reason: '' })
|
|||||||
|
|
||||||
const listingOptions = ref<AvailableListing[]>([])
|
const listingOptions = ref<AvailableListing[]>([])
|
||||||
const listingLoading = ref(false)
|
const listingLoading = ref(false)
|
||||||
|
const listingKeyword = ref('')
|
||||||
|
const listingSourceType = ref<AvailableListingSourceType>('')
|
||||||
|
let listingRequestID = 0
|
||||||
const shopOptions = ref<string[]>([])
|
const shopOptions = ref<string[]>([])
|
||||||
const shopOptionsLoading = ref(false)
|
const shopOptionsLoading = ref(false)
|
||||||
const selectedListing = computed(
|
const selectedListing = computed(
|
||||||
@@ -99,7 +111,10 @@ function openCreateDialog() {
|
|||||||
createForm.profit_amount = 0
|
createForm.profit_amount = 0
|
||||||
createForm.remark = ''
|
createForm.remark = ''
|
||||||
listingOptions.value = []
|
listingOptions.value = []
|
||||||
|
listingKeyword.value = ''
|
||||||
|
listingSourceType.value = ''
|
||||||
createDialogVisible.value = true
|
createDialogVisible.value = true
|
||||||
|
void searchListings('')
|
||||||
}
|
}
|
||||||
|
|
||||||
async function loadShopOptions() {
|
async function loadShopOptions() {
|
||||||
@@ -112,19 +127,32 @@ async function loadShopOptions() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function searchListings(keyword: string) {
|
async function searchListings(keyword: string) {
|
||||||
if (!keyword) {
|
listingKeyword.value = keyword.trim()
|
||||||
listingOptions.value = []
|
const requestID = ++listingRequestID
|
||||||
return
|
|
||||||
}
|
|
||||||
listingLoading.value = true
|
listingLoading.value = true
|
||||||
try {
|
try {
|
||||||
const result = await fetchAvailableListings(keyword)
|
const result = await fetchAvailableListings(
|
||||||
listingOptions.value = result.items
|
listingKeyword.value,
|
||||||
|
1,
|
||||||
|
20,
|
||||||
|
listingSourceType.value
|
||||||
|
)
|
||||||
|
if (requestID === listingRequestID) {
|
||||||
|
listingOptions.value = result.items
|
||||||
|
}
|
||||||
} finally {
|
} 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() {
|
async function handleCreate() {
|
||||||
if (!createForm.listing_id) {
|
if (!createForm.listing_id) {
|
||||||
ElMessage.warning('请选择要提号的账号')
|
ElMessage.warning('请选择要提号的账号')
|
||||||
@@ -269,6 +297,27 @@ function normalizeCent(value: number | undefined | null) {
|
|||||||
function listingOwnerTotalCent(item: AvailableListing) {
|
function listingOwnerTotalCent(item: AvailableListing) {
|
||||||
return normalizeCent(item.owner_total_price_cent || item.owner_price_cent)
|
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)}`
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
@@ -306,6 +355,15 @@ function listingOwnerTotalCent(item: AvailableListing) {
|
|||||||
>
|
>
|
||||||
<el-option v-for="name in shopOptions" :key="name" :label="name" :value="name" />
|
<el-option v-for="name in shopOptions" :key="name" :label="name" :value="name" />
|
||||||
</el-select>
|
</el-select>
|
||||||
|
<el-select
|
||||||
|
v-model="filters.account_source"
|
||||||
|
placeholder="账号来源"
|
||||||
|
clearable
|
||||||
|
style="width: 190px"
|
||||||
|
>
|
||||||
|
<el-option label="外部上传 · 平台代管" value="external_platform_managed" />
|
||||||
|
<el-option label="站内上传" value="internal" />
|
||||||
|
</el-select>
|
||||||
<el-button type="primary" :icon="Search" @click="handleSearch">查询</el-button>
|
<el-button type="primary" :icon="Search" @click="handleSearch">查询</el-button>
|
||||||
<el-button :icon="Refresh" @click="loadList">刷新</el-button>
|
<el-button :icon="Refresh" @click="loadList">刷新</el-button>
|
||||||
</div>
|
</div>
|
||||||
@@ -324,6 +382,16 @@ function listingOwnerTotalCent(item: AvailableListing) {
|
|||||||
<el-table-column label="卖家" width="140">
|
<el-table-column label="卖家" width="140">
|
||||||
<template #default="{ row }">{{ row.owner_phone }}</template>
|
<template #default="{ row }">{{ row.owner_phone }}</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
|
<el-table-column label="账号来源" width="180">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<div class="source-cell">
|
||||||
|
<el-tag :type="pickupSourceTagType(row.account_source)" effect="light" size="small">
|
||||||
|
{{ pickupSourceLabel(row.account_source) }}
|
||||||
|
</el-tag>
|
||||||
|
<small v-if="row.source_channel">{{ row.source_channel }}</small>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
<el-table-column label="交易渠道" width="110">
|
<el-table-column label="交易渠道" width="110">
|
||||||
<template #default="{ row }">{{ row.platform || '-' }}</template>
|
<template #default="{ row }">{{ row.platform || '-' }}</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
@@ -402,8 +470,23 @@ function listingOwnerTotalCent(item: AvailableListing) {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 创建提号 -->
|
<!-- 创建提号 -->
|
||||||
<el-dialog v-model="createDialogVisible" title="创建线下提号" width="520px">
|
<el-dialog
|
||||||
|
v-model="createDialogVisible"
|
||||||
|
title="创建线下提号"
|
||||||
|
width="min(640px, calc(100vw - 32px))"
|
||||||
|
>
|
||||||
<el-form label-width="110px">
|
<el-form label-width="110px">
|
||||||
|
<el-form-item label="账号来源">
|
||||||
|
<el-radio-group
|
||||||
|
v-model="listingSourceType"
|
||||||
|
size="small"
|
||||||
|
@change="handleListingSourceChange"
|
||||||
|
>
|
||||||
|
<el-radio-button value="">全部</el-radio-button>
|
||||||
|
<el-radio-button value="external">外部上传 · 平台代管</el-radio-button>
|
||||||
|
<el-radio-button value="internal">站内上传</el-radio-button>
|
||||||
|
</el-radio-group>
|
||||||
|
</el-form-item>
|
||||||
<el-form-item label="选择账号" required>
|
<el-form-item label="选择账号" required>
|
||||||
<el-select
|
<el-select
|
||||||
v-model="createForm.listing_id"
|
v-model="createForm.listing_id"
|
||||||
@@ -418,12 +501,46 @@ function listingOwnerTotalCent(item: AvailableListing) {
|
|||||||
<el-option
|
<el-option
|
||||||
v-for="item in listingOptions"
|
v-for="item in listingOptions"
|
||||||
:key="item.id"
|
:key="item.id"
|
||||||
:label="`${item.listing_no} · ${item.account_title} · ${formatCentWithSymbol(listingOwnerTotalCent(item))} / ${formatCentWithSymbol(item.listing_price_cent)}`"
|
:label="listingOptionLabel(item)"
|
||||||
:value="item.id"
|
:value="item.id"
|
||||||
/>
|
>
|
||||||
|
<div class="listing-option">
|
||||||
|
<el-tag
|
||||||
|
:type="pickupSourceTagType(item.account_source)"
|
||||||
|
effect="light"
|
||||||
|
size="small"
|
||||||
|
>
|
||||||
|
{{ pickupSourceLabel(item.account_source) }}
|
||||||
|
</el-tag>
|
||||||
|
<span class="listing-option-title"
|
||||||
|
>{{ item.listing_no }} · {{ item.account_title }}</span
|
||||||
|
>
|
||||||
|
<span class="listing-option-price">
|
||||||
|
{{ formatCentWithSymbol(listingOwnerTotalCent(item)) }} /
|
||||||
|
{{ formatCentWithSymbol(item.listing_price_cent) }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</el-option>
|
||||||
</el-select>
|
</el-select>
|
||||||
<div class="form-hint">仅显示已发布、已审核、未在交易中的账号</div>
|
<div class="form-hint">仅显示已发布、已审核、未在交易中的账号</div>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
|
<el-form-item v-if="selectedListing" label="来源确认">
|
||||||
|
<div class="selected-source">
|
||||||
|
<el-tag :type="pickupSourceTagType(selectedListing.account_source)" effect="light">
|
||||||
|
{{ pickupSourceLabel(selectedListing.account_source) }}
|
||||||
|
</el-tag>
|
||||||
|
<span v-if="selectedListing.source_channel">
|
||||||
|
来源渠道:{{ selectedListing.source_channel }}
|
||||||
|
</span>
|
||||||
|
<span v-if="selectedListing.is_external_upload">
|
||||||
|
交接:{{
|
||||||
|
selectedListing.handoff_mode === 'platform'
|
||||||
|
? '平台代管'
|
||||||
|
: selectedListing.handoff_mode
|
||||||
|
}}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</el-form-item>
|
||||||
<el-form-item v-if="selectedListing && selectedPriceSnapshot" label="价格快照">
|
<el-form-item v-if="selectedListing && selectedPriceSnapshot" label="价格快照">
|
||||||
<div class="price-snapshot">
|
<div class="price-snapshot">
|
||||||
<span>纯比价格 {{ formatCentWithSymbol(selectedPriceSnapshot.pureCent) }}</span>
|
<span>纯比价格 {{ formatCentWithSymbol(selectedPriceSnapshot.pureCent) }}</span>
|
||||||
@@ -575,6 +692,41 @@ function listingOwnerTotalCent(item: AvailableListing) {
|
|||||||
font-size: 13px;
|
font-size: 13px;
|
||||||
line-height: 1.6;
|
line-height: 1.6;
|
||||||
}
|
}
|
||||||
|
.source-cell {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: flex-start;
|
||||||
|
gap: 3px;
|
||||||
|
}
|
||||||
|
.source-cell small {
|
||||||
|
color: #64748b;
|
||||||
|
}
|
||||||
|
.listing-option {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
.listing-option-title {
|
||||||
|
min-width: 0;
|
||||||
|
flex: 1;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
.listing-option-price {
|
||||||
|
flex: none;
|
||||||
|
color: #64748b;
|
||||||
|
font-variant-numeric: tabular-nums;
|
||||||
|
}
|
||||||
|
.selected-source {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px 12px;
|
||||||
|
color: #475569;
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
.action-buttons {
|
.action-buttons {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
|
|||||||
Reference in New Issue
Block a user