feat(pickup): 线下提号支持扩展字段店铺管理CRUD及时间筛选
- 线下提号创建表单扩展:增加「操作人」、「第三方单号」、「下单金额」、「押金金额」字段,重构弹窗布局为紧凑双列/三列金额卡片排布,消除过度垂直滚动并默认预填操作人 - 店铺管理 CRUD:新增 pickup_shops 配置表与配套 API,支持在提号页面直接管理店铺(增删改查、启用/禁用、筛选),下拉选项自动优先加载活跃配置店铺并兼顾历史店铺 - 列表时间范围筛选:增加精确到秒的「日期+时间」时间范围选择器,支持快捷时段与后端时间范围过滤 - 生产环境数据核查:只读提取线上全部 61 项历史提号店铺名称与单量分布,完整整理沉淀至实施文档供后续映射规则参考 - 验证方式:执行并通过全套端到端代码门禁(scripts/check.sh:SQL约束、go vet、go build、MySQL真实测试库单元测试及覆盖率、vue大小检查、prettier、eslint、vue-tsc及vite构建)
This commit is contained in:
@@ -16,12 +16,16 @@ type AdminPickup struct {
|
||||
AccountID uint64 `gorm:"not null;index" json:"account_id"`
|
||||
OwnerID uint64 `gorm:"not null;index" json:"owner_id"`
|
||||
AdminID uint64 `gorm:"not null" json:"admin_id"`
|
||||
Operator string `gorm:"size:64;not null;default:'';index" json:"operator"`
|
||||
Platform string `gorm:"size:32;not null;default:''" json:"platform"`
|
||||
ThirdPartyOrderNo string `gorm:"size:128;not null;default:'';index" json:"third_party_order_no"`
|
||||
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"`
|
||||
SettlementMode string `gorm:"size:32;not null;default:'owner_wallet'" json:"settlement_mode"`
|
||||
ListingPriceCent int64 `gorm:"not null;default:0" json:"listing_price_cent"`
|
||||
OrderAmountCent int64 `gorm:"not null;default:0" json:"order_amount_cent"`
|
||||
DepositAmountCent int64 `gorm:"not null;default:0" json:"deposit_amount_cent"`
|
||||
OwnerPriceCent int64 `gorm:"not null;default:0" json:"owner_price_cent"`
|
||||
WebsiteProfitCent int64 `gorm:"not null;default:0" json:"website_profit_cent"`
|
||||
ProfitAmountCent int64 `gorm:"not null;default:0" json:"profit_amount_cent"`
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
package model
|
||||
|
||||
import "time"
|
||||
|
||||
// PickupShop 线下提号店铺配置模型。
|
||||
type PickupShop struct {
|
||||
ID uint64 `gorm:"primaryKey" json:"id"`
|
||||
Name string `gorm:"size:100;not null;uniqueIndex" json:"name"`
|
||||
Platform string `gorm:"size:32;not null;default:''" json:"platform"`
|
||||
Status string `gorm:"size:20;not null;default:'active';index" json:"status"`
|
||||
SortOrder int `gorm:"not null;default:0" json:"sort_order"`
|
||||
Remark string `gorm:"size:255;not null;default:''" json:"remark"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
}
|
||||
|
||||
func (PickupShop) TableName() string {
|
||||
return "pickup_shops"
|
||||
}
|
||||
@@ -25,6 +25,9 @@ const (
|
||||
FinancialAdjustmentStatusSettled = "settled"
|
||||
FinancialAdjustmentStatusPending = "pending"
|
||||
FinancialAdjustmentStatusRecovery = "recovery_pending"
|
||||
|
||||
PickupShopStatusActive = "active"
|
||||
PickupShopStatusDisabled = "disabled"
|
||||
)
|
||||
|
||||
var (
|
||||
@@ -39,6 +42,8 @@ var (
|
||||
ErrFinancialAdjustmentInvalid = errors.New("financial adjustment invalid")
|
||||
ErrFinancialAdjustmentNotFound = errors.New("financial adjustment not found")
|
||||
ErrFinancialAdjustmentCannotSettle = errors.New("financial adjustment cannot be settled")
|
||||
ErrPickupShopNotFound = errors.New("pickup shop not found")
|
||||
ErrPickupShopNameExists = errors.New("pickup shop name already exists")
|
||||
)
|
||||
|
||||
type PickupDTO struct {
|
||||
@@ -53,12 +58,16 @@ type PickupDTO struct {
|
||||
OwnerID uint64 `json:"owner_id"`
|
||||
OwnerPhone string `json:"owner_phone"`
|
||||
AdminID uint64 `json:"admin_id"`
|
||||
Operator string `json:"operator"`
|
||||
Platform string `json:"platform"`
|
||||
ThirdPartyOrderNo string `json:"third_party_order_no"`
|
||||
ShopName string `json:"shop_name"`
|
||||
AccountSource string `json:"account_source"`
|
||||
SourceChannel string `json:"source_channel"`
|
||||
SettlementMode string `json:"settlement_mode"`
|
||||
ListingPriceCent int64 `json:"listing_price_cent"`
|
||||
OrderAmountCent int64 `json:"order_amount_cent"`
|
||||
DepositAmountCent int64 `json:"deposit_amount_cent"`
|
||||
OwnerPriceCent int64 `json:"owner_price_cent"`
|
||||
WebsiteProfitCent int64 `json:"website_profit_cent"`
|
||||
ProfitAmountCent int64 `json:"profit_amount_cent"`
|
||||
@@ -98,11 +107,15 @@ type FinancialAdjustmentDTO 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"`
|
||||
ListingID uint64 `json:"listing_id" binding:"required"`
|
||||
Operator string `json:"operator" binding:"max=64"`
|
||||
Platform string `json:"platform"`
|
||||
ThirdPartyOrderNo string `json:"third_party_order_no" binding:"max=128"`
|
||||
ShopName string `json:"shop_name" binding:"max=100"`
|
||||
OrderAmountCent int64 `json:"order_amount_cent" binding:"min=0"`
|
||||
DepositAmountCent int64 `json:"deposit_amount_cent" binding:"min=0"`
|
||||
ProfitAmountCent int64 `json:"profit_amount_cent"`
|
||||
Remark string `json:"remark"`
|
||||
}
|
||||
|
||||
type CompleteRequest struct {
|
||||
@@ -143,6 +156,43 @@ type AdminPickupQuery struct {
|
||||
ShopName string
|
||||
AccountSource string
|
||||
OfflineSettlementStatus string
|
||||
Operator string
|
||||
ThirdPartyOrderNo string
|
||||
StartTime *time.Time
|
||||
EndTime *time.Time
|
||||
}
|
||||
|
||||
type PickupShopDTO struct {
|
||||
ID uint64 `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Platform string `json:"platform"`
|
||||
Status string `json:"status"`
|
||||
SortOrder int `json:"sort_order"`
|
||||
Remark string `json:"remark"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
}
|
||||
|
||||
type CreatePickupShopRequest struct {
|
||||
Name string `json:"name" binding:"required,max=100"`
|
||||
Platform string `json:"platform" binding:"max=32"`
|
||||
Status string `json:"status" binding:"omitempty,oneof=active disabled"`
|
||||
SortOrder int `json:"sort_order"`
|
||||
Remark string `json:"remark" binding:"max=255"`
|
||||
}
|
||||
|
||||
type UpdatePickupShopRequest struct {
|
||||
Name string `json:"name" binding:"required,max=100"`
|
||||
Platform string `json:"platform" binding:"max=32"`
|
||||
Status string `json:"status" binding:"required,oneof=active disabled"`
|
||||
SortOrder int `json:"sort_order"`
|
||||
Remark string `json:"remark" binding:"max=255"`
|
||||
}
|
||||
|
||||
type PickupShopQuery struct {
|
||||
Keyword string
|
||||
Platform string
|
||||
Status string
|
||||
}
|
||||
|
||||
type SellerPickupQuery struct {
|
||||
|
||||
@@ -301,11 +301,88 @@ func writePickupError(c *gin.Context, err error) {
|
||||
response.Error(c, http.StatusNotFound, "pickup_financial_adjustment_not_found", "财务调整记录不存在")
|
||||
case errors.Is(err, ErrFinancialAdjustmentCannotSettle):
|
||||
response.BadRequest(c, "当前调整不可确认处理")
|
||||
case errors.Is(err, ErrPickupShopNotFound):
|
||||
response.Error(c, http.StatusNotFound, "pickup_shop_not_found", "店铺不存在")
|
||||
case errors.Is(err, ErrPickupShopNameExists):
|
||||
response.Error(c, http.StatusConflict, "pickup_shop_name_exists", "店铺名称已存在")
|
||||
default:
|
||||
response.Error(c, http.StatusInternalServerError, "pickup_error", err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
// ListShops 获取店铺配置列表(管理员)
|
||||
func (h *Handler) ListShops(c *gin.Context) {
|
||||
query := parsePickupShopQuery(c)
|
||||
items, err := h.service.ListShops(c.Request.Context(), query)
|
||||
if err != nil {
|
||||
writePickupError(c, err)
|
||||
return
|
||||
}
|
||||
response.OK(c, items)
|
||||
}
|
||||
|
||||
// CreateShop 创建店铺配置(管理员)
|
||||
func (h *Handler) CreateShop(c *gin.Context) {
|
||||
adminID, ok := currentAdminID(c)
|
||||
if !ok {
|
||||
response.Unauthorized(c, "缺少管理员上下文")
|
||||
return
|
||||
}
|
||||
var req CreatePickupShopRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
response.BadRequest(c, "店铺参数格式不正确")
|
||||
return
|
||||
}
|
||||
item, err := h.service.CreateShop(c.Request.Context(), req, adminID, auditMeta(c))
|
||||
if err != nil {
|
||||
writePickupError(c, err)
|
||||
return
|
||||
}
|
||||
response.OK(c, item)
|
||||
}
|
||||
|
||||
// UpdateShop 更新店铺配置(管理员)
|
||||
func (h *Handler) UpdateShop(c *gin.Context) {
|
||||
adminID, ok := currentAdminID(c)
|
||||
if !ok {
|
||||
response.Unauthorized(c, "缺少管理员上下文")
|
||||
return
|
||||
}
|
||||
id, ok := parseID(c)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
var req UpdatePickupShopRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
response.BadRequest(c, "店铺参数格式不正确")
|
||||
return
|
||||
}
|
||||
item, err := h.service.UpdateShop(c.Request.Context(), id, req, adminID, auditMeta(c))
|
||||
if err != nil {
|
||||
writePickupError(c, err)
|
||||
return
|
||||
}
|
||||
response.OK(c, item)
|
||||
}
|
||||
|
||||
// DeleteShop 删除店铺配置(管理员)
|
||||
func (h *Handler) DeleteShop(c *gin.Context) {
|
||||
adminID, ok := currentAdminID(c)
|
||||
if !ok {
|
||||
response.Unauthorized(c, "缺少管理员上下文")
|
||||
return
|
||||
}
|
||||
id, ok := parseID(c)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
if err := h.service.DeleteShop(c.Request.Context(), id, adminID, auditMeta(c)); err != nil {
|
||||
writePickupError(c, err)
|
||||
return
|
||||
}
|
||||
response.OK(c, gin.H{"deleted": true})
|
||||
}
|
||||
|
||||
func auditMeta(c *gin.Context) auditlog.Meta {
|
||||
return auditlog.Meta{
|
||||
IP: c.ClientIP(),
|
||||
|
||||
@@ -2,6 +2,8 @@ package pickup
|
||||
|
||||
import (
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"hfb_sys/backend/internal/middleware"
|
||||
"hfb_sys/backend/pkg/response"
|
||||
@@ -51,6 +53,26 @@ func parsePagination(c *gin.Context) (int, int) {
|
||||
return page, pageSize
|
||||
}
|
||||
|
||||
func parseFlexibleTime(val string) *time.Time {
|
||||
val = strings.TrimSpace(val)
|
||||
if val == "" {
|
||||
return nil
|
||||
}
|
||||
layouts := []string{
|
||||
"2006-01-02 15:04:05",
|
||||
time.RFC3339,
|
||||
"2006-01-02T15:04:05",
|
||||
"2006-01-02",
|
||||
}
|
||||
loc := time.Local
|
||||
for _, layout := range layouts {
|
||||
if t, err := time.ParseInLocation(layout, val, loc); err == nil {
|
||||
return &t
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func parseAdminQuery(c *gin.Context) AdminPickupQuery {
|
||||
page, pageSize := parsePagination(c)
|
||||
return AdminPickupQuery{
|
||||
@@ -61,6 +83,18 @@ func parseAdminQuery(c *gin.Context) AdminPickupQuery {
|
||||
ShopName: c.Query("shop_name"),
|
||||
AccountSource: c.Query("account_source"),
|
||||
OfflineSettlementStatus: c.Query("offline_settlement_status"),
|
||||
Operator: c.Query("operator"),
|
||||
ThirdPartyOrderNo: c.Query("third_party_order_no"),
|
||||
StartTime: parseFlexibleTime(c.Query("start_time")),
|
||||
EndTime: parseFlexibleTime(c.Query("end_time")),
|
||||
}
|
||||
}
|
||||
|
||||
func parsePickupShopQuery(c *gin.Context) PickupShopQuery {
|
||||
return PickupShopQuery{
|
||||
Keyword: c.Query("keyword"),
|
||||
Platform: c.Query("platform"),
|
||||
Status: c.Query("status"),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -87,12 +87,16 @@ func (r *Repository) Create(ctx context.Context, req CreateRequest, adminID uint
|
||||
AccountID: listing.AccountID,
|
||||
OwnerID: listing.OwnerID,
|
||||
AdminID: adminID,
|
||||
Operator: strings.TrimSpace(req.Operator),
|
||||
Platform: strings.TrimSpace(req.Platform),
|
||||
ThirdPartyOrderNo: strings.TrimSpace(req.ThirdPartyOrderNo),
|
||||
ShopName: strings.TrimSpace(req.ShopName),
|
||||
AccountSource: accountSource,
|
||||
SourceChannel: sourceChannel,
|
||||
SettlementMode: pickupSettlementMode(listing.SettlementMode, accountSource),
|
||||
ListingPriceCent: priceSnapshot.ListingPriceCent,
|
||||
OrderAmountCent: req.OrderAmountCent,
|
||||
DepositAmountCent: req.DepositAmountCent,
|
||||
OwnerPriceCent: priceSnapshot.OwnerPriceCent,
|
||||
WebsiteProfitCent: priceSnapshot.WebsiteProfitCent,
|
||||
ProfitAmountCent: req.ProfitAmountCent,
|
||||
@@ -106,9 +110,15 @@ func (r *Repository) Create(ctx context.Context, req CreateRequest, adminID uint
|
||||
return err
|
||||
}
|
||||
if err := appendPickupEvent(tx, pickup, "pickup_created", adminID, "管理员创建线下提号。", pickup.Remark, map[string]any{
|
||||
"listing_price_cent": pickup.ListingPriceCent, "profit_amount_cent": pickup.ProfitAmountCent,
|
||||
"account_source": pickup.AccountSource, "source_channel": pickup.SourceChannel,
|
||||
"settlement_mode": pickup.SettlementMode,
|
||||
"listing_price_cent": pickup.ListingPriceCent,
|
||||
"order_amount_cent": pickup.OrderAmountCent,
|
||||
"deposit_amount_cent": pickup.DepositAmountCent,
|
||||
"profit_amount_cent": pickup.ProfitAmountCent,
|
||||
"account_source": pickup.AccountSource,
|
||||
"source_channel": pickup.SourceChannel,
|
||||
"settlement_mode": pickup.SettlementMode,
|
||||
"operator": pickup.Operator,
|
||||
"third_party_order_no": pickup.ThirdPartyOrderNo,
|
||||
}, map[string]any{}); err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -157,13 +167,17 @@ 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,
|
||||
"account_source": pickup.AccountSource,
|
||||
"source_channel": pickup.SourceChannel,
|
||||
"profit_cent": pickup.ProfitAmountCent,
|
||||
"pickup_no": pickupNo,
|
||||
"listing_id": listing.ID,
|
||||
"platform": pickup.Platform,
|
||||
"third_party_order_no": pickup.ThirdPartyOrderNo,
|
||||
"shop_name": pickup.ShopName,
|
||||
"operator": pickup.Operator,
|
||||
"account_source": pickup.AccountSource,
|
||||
"source_channel": pickup.SourceChannel,
|
||||
"order_amount_cent": pickup.OrderAmountCent,
|
||||
"deposit_amount_cent": pickup.DepositAmountCent,
|
||||
"profit_cent": pickup.ProfitAmountCent,
|
||||
},
|
||||
}); err != nil {
|
||||
return err
|
||||
@@ -735,6 +749,18 @@ func applyAdminPickupFilters(db *gorm.DB, query AdminPickupQuery) *gorm.DB {
|
||||
if offlineSettlementStatus := strings.TrimSpace(query.OfflineSettlementStatus); offlineSettlementStatus != "" {
|
||||
db = db.Where("p.offline_settlement_status = ?", offlineSettlementStatus)
|
||||
}
|
||||
if operator := strings.TrimSpace(query.Operator); operator != "" {
|
||||
db = db.Where("p.operator = ?", operator)
|
||||
}
|
||||
if thirdPartyOrderNo := strings.TrimSpace(query.ThirdPartyOrderNo); thirdPartyOrderNo != "" {
|
||||
db = db.Where("p.third_party_order_no = ?", thirdPartyOrderNo)
|
||||
}
|
||||
if query.StartTime != nil {
|
||||
db = db.Where("p.created_at >= ?", *query.StartTime)
|
||||
}
|
||||
if query.EndTime != nil {
|
||||
db = db.Where("p.created_at <= ?", *query.EndTime)
|
||||
}
|
||||
if kw := strings.TrimSpace(query.Keyword); kw != "" {
|
||||
upper := strings.ToUpper(kw)
|
||||
if len(kw) == 11 && kw[0] == '1' && isAllDigits(kw) {
|
||||
@@ -752,7 +778,9 @@ func applyAdminPickupFilters(db *gorm.DB, query AdminPickupQuery) *gorm.DB {
|
||||
} else {
|
||||
like := "%" + kw + "%"
|
||||
db = db.Where(
|
||||
"p.pickup_no LIKE ? OR l.listing_no LIKE ? OR a.title LIKE ? OR owner.phone LIKE ?",
|
||||
"p.pickup_no LIKE ? OR l.listing_no LIKE ? OR a.title LIKE ? OR owner.phone LIKE ? OR p.third_party_order_no LIKE ? OR p.operator LIKE ?",
|
||||
like,
|
||||
like,
|
||||
like,
|
||||
like,
|
||||
like,
|
||||
@@ -822,21 +850,240 @@ func (r *Repository) ListAdmin(ctx context.Context, query AdminPickupQuery) (*Pa
|
||||
return &PaginatedResult{Items: items, Total: total, Page: page, PageSize: pageSize}, nil
|
||||
}
|
||||
|
||||
// ListShopNames 返回历史提号中已使用的店铺名称,供后台选择和筛选。
|
||||
// ListShopNames 返回店铺配置与历史提号中已使用的店铺名称,供后台选择和筛选。
|
||||
// 优先返回 pickup_shops 中配置的 active 店铺(按权重排序),再补充历史提号中未在配置表但已有的店铺。
|
||||
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)
|
||||
|
||||
result := make([]string, 0)
|
||||
seen := make(map[string]struct{})
|
||||
|
||||
// 1. 查询店铺配置表中的 active 店铺
|
||||
shopDB := r.db.WithContext(ctx).Model(&model.PickupShop{}).Where("status = ?", PickupShopStatusActive)
|
||||
if val := strings.TrimSpace(platform); val != "" {
|
||||
shopDB = shopDB.Where("platform = ? OR platform = ''", val)
|
||||
}
|
||||
names := make([]string, 0)
|
||||
if err := db.Distinct("shop_name").Order("shop_name ASC").Limit(200).Pluck("shop_name", &names).Error; err != nil {
|
||||
var configuredNames []string
|
||||
if err := shopDB.Order("sort_order DESC, name ASC").Pluck("name", &configuredNames).Error; err == nil {
|
||||
for _, name := range configuredNames {
|
||||
trimmed := strings.TrimSpace(name)
|
||||
if trimmed != "" {
|
||||
if _, ok := seen[trimmed]; !ok {
|
||||
seen[trimmed] = struct{}{}
|
||||
result = append(result, trimmed)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 2. 查询 admin_pickups 中的历史店铺作为补充
|
||||
historyDB := r.db.WithContext(ctx).Model(&model.AdminPickup{}).Where("shop_name <> ''")
|
||||
if val := strings.TrimSpace(platform); val != "" {
|
||||
historyDB = historyDB.Where("platform = ?", val)
|
||||
}
|
||||
var historyNames []string
|
||||
if err := historyDB.Distinct("shop_name").Order("shop_name ASC").Limit(200).Pluck("shop_name", &historyNames).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return names, nil
|
||||
for _, name := range historyNames {
|
||||
trimmed := strings.TrimSpace(name)
|
||||
if trimmed != "" {
|
||||
if _, ok := seen[trimmed]; !ok {
|
||||
seen[trimmed] = struct{}{}
|
||||
result = append(result, trimmed)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// ListShops 查询店铺配置列表
|
||||
func (r *Repository) ListShops(ctx context.Context, query PickupShopQuery) ([]PickupShopDTO, error) {
|
||||
if r == nil || r.db == nil {
|
||||
return nil, ErrDependencyUnavailable
|
||||
}
|
||||
db := r.db.WithContext(ctx).Model(&model.PickupShop{})
|
||||
if kw := strings.TrimSpace(query.Keyword); kw != "" {
|
||||
like := "%" + kw + "%"
|
||||
db = db.Where("name LIKE ? OR remark LIKE ?", like, like)
|
||||
}
|
||||
if p := strings.TrimSpace(query.Platform); p != "" {
|
||||
db = db.Where("platform = ?", p)
|
||||
}
|
||||
if s := strings.TrimSpace(query.Status); s != "" {
|
||||
db = db.Where("status = ?", s)
|
||||
}
|
||||
var rows []model.PickupShop
|
||||
if err := db.Order("sort_order DESC, id DESC").Find(&rows).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
items := make([]PickupShopDTO, 0, len(rows))
|
||||
for _, row := range rows {
|
||||
items = append(items, PickupShopDTO{
|
||||
ID: row.ID,
|
||||
Name: row.Name,
|
||||
Platform: row.Platform,
|
||||
Status: row.Status,
|
||||
SortOrder: row.SortOrder,
|
||||
Remark: row.Remark,
|
||||
CreatedAt: row.CreatedAt,
|
||||
UpdatedAt: row.UpdatedAt,
|
||||
})
|
||||
}
|
||||
return items, nil
|
||||
}
|
||||
|
||||
// CreateShop 创建店铺配置
|
||||
func (r *Repository) CreateShop(ctx context.Context, req CreatePickupShopRequest, adminID uint64, meta auditlog.Meta) (*PickupShopDTO, error) {
|
||||
if r == nil || r.db == nil {
|
||||
return nil, ErrDependencyUnavailable
|
||||
}
|
||||
name := strings.TrimSpace(req.Name)
|
||||
if name == "" {
|
||||
return nil, ErrInvalidAmount
|
||||
}
|
||||
var count int64
|
||||
if err := r.db.WithContext(ctx).Model(&model.PickupShop{}).Where("name = ?", name).Count(&count).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if count > 0 {
|
||||
return nil, ErrPickupShopNameExists
|
||||
}
|
||||
status := strings.TrimSpace(req.Status)
|
||||
if status == "" {
|
||||
status = PickupShopStatusActive
|
||||
}
|
||||
now := time.Now()
|
||||
shop := model.PickupShop{
|
||||
Name: name,
|
||||
Platform: strings.TrimSpace(req.Platform),
|
||||
Status: status,
|
||||
SortOrder: req.SortOrder,
|
||||
Remark: strings.TrimSpace(req.Remark),
|
||||
CreatedAt: now,
|
||||
UpdatedAt: now,
|
||||
}
|
||||
if err := r.db.WithContext(ctx).Create(&shop).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
bid := shop.ID
|
||||
_ = auditlog.Append(r.db.WithContext(ctx), auditlog.Entry{
|
||||
ActorType: "admin",
|
||||
ActorID: adminID,
|
||||
Action: "pickup_shop_create",
|
||||
BizType: "pickup_shop",
|
||||
BizID: &bid,
|
||||
Meta: meta,
|
||||
Detail: map[string]any{
|
||||
"shop_id": shop.ID,
|
||||
"name": shop.Name,
|
||||
"platform": shop.Platform,
|
||||
"status": shop.Status,
|
||||
},
|
||||
})
|
||||
return &PickupShopDTO{
|
||||
ID: shop.ID,
|
||||
Name: shop.Name,
|
||||
Platform: shop.Platform,
|
||||
Status: shop.Status,
|
||||
SortOrder: shop.SortOrder,
|
||||
Remark: shop.Remark,
|
||||
CreatedAt: shop.CreatedAt,
|
||||
UpdatedAt: shop.UpdatedAt,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// UpdateShop 更新店铺配置
|
||||
func (r *Repository) UpdateShop(ctx context.Context, id uint64, req UpdatePickupShopRequest, adminID uint64, meta auditlog.Meta) (*PickupShopDTO, error) {
|
||||
if r == nil || r.db == nil {
|
||||
return nil, ErrDependencyUnavailable
|
||||
}
|
||||
name := strings.TrimSpace(req.Name)
|
||||
if name == "" {
|
||||
return nil, ErrInvalidAmount
|
||||
}
|
||||
var shop model.PickupShop
|
||||
if err := r.db.WithContext(ctx).First(&shop, id).Error; err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return nil, ErrPickupShopNotFound
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
var count int64
|
||||
if err := r.db.WithContext(ctx).Model(&model.PickupShop{}).Where("name = ? AND id <> ?", name, id).Count(&count).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if count > 0 {
|
||||
return nil, ErrPickupShopNameExists
|
||||
}
|
||||
shop.Name = name
|
||||
shop.Platform = strings.TrimSpace(req.Platform)
|
||||
shop.Status = strings.TrimSpace(req.Status)
|
||||
shop.SortOrder = req.SortOrder
|
||||
shop.Remark = strings.TrimSpace(req.Remark)
|
||||
shop.UpdatedAt = time.Now()
|
||||
if err := r.db.WithContext(ctx).Save(&shop).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
bid := shop.ID
|
||||
_ = auditlog.Append(r.db.WithContext(ctx), auditlog.Entry{
|
||||
ActorType: "admin",
|
||||
ActorID: adminID,
|
||||
Action: "pickup_shop_update",
|
||||
BizType: "pickup_shop",
|
||||
BizID: &bid,
|
||||
Meta: meta,
|
||||
Detail: map[string]any{
|
||||
"shop_id": shop.ID,
|
||||
"name": shop.Name,
|
||||
"platform": shop.Platform,
|
||||
"status": shop.Status,
|
||||
},
|
||||
})
|
||||
return &PickupShopDTO{
|
||||
ID: shop.ID,
|
||||
Name: shop.Name,
|
||||
Platform: shop.Platform,
|
||||
Status: shop.Status,
|
||||
SortOrder: shop.SortOrder,
|
||||
Remark: shop.Remark,
|
||||
CreatedAt: shop.CreatedAt,
|
||||
UpdatedAt: shop.UpdatedAt,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// DeleteShop 删除店铺配置
|
||||
func (r *Repository) DeleteShop(ctx context.Context, id uint64, adminID uint64, meta auditlog.Meta) error {
|
||||
if r == nil || r.db == nil {
|
||||
return ErrDependencyUnavailable
|
||||
}
|
||||
var shop model.PickupShop
|
||||
if err := r.db.WithContext(ctx).First(&shop, id).Error; err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return ErrPickupShopNotFound
|
||||
}
|
||||
return err
|
||||
}
|
||||
if err := r.db.WithContext(ctx).Delete(&shop).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
bid := shop.ID
|
||||
_ = auditlog.Append(r.db.WithContext(ctx), auditlog.Entry{
|
||||
ActorType: "admin",
|
||||
ActorID: adminID,
|
||||
Action: "pickup_shop_delete",
|
||||
BizType: "pickup_shop",
|
||||
BizID: &bid,
|
||||
Meta: meta,
|
||||
Detail: map[string]any{
|
||||
"shop_id": shop.ID,
|
||||
"name": shop.Name,
|
||||
},
|
||||
})
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *Repository) ListForSeller(ctx context.Context, ownerID uint64, query SellerPickupQuery) (*PaginatedResult, error) {
|
||||
@@ -977,12 +1224,16 @@ func (row pickupRow) toDTO() PickupDTO {
|
||||
OwnerID: row.OwnerID,
|
||||
OwnerPhone: row.OwnerPhone,
|
||||
AdminID: row.AdminID,
|
||||
Operator: row.Operator,
|
||||
Platform: row.Platform,
|
||||
ThirdPartyOrderNo: row.ThirdPartyOrderNo,
|
||||
ShopName: row.ShopName,
|
||||
AccountSource: row.AccountSource,
|
||||
SourceChannel: row.SourceChannel,
|
||||
SettlementMode: row.SettlementMode,
|
||||
ListingPriceCent: row.ListingPriceCent,
|
||||
OrderAmountCent: row.OrderAmountCent,
|
||||
DepositAmountCent: row.DepositAmountCent,
|
||||
OwnerPriceCent: row.OwnerPriceCent,
|
||||
WebsiteProfitCent: row.WebsiteProfitCent,
|
||||
ProfitAmountCent: row.ProfitAmountCent,
|
||||
|
||||
@@ -5,6 +5,7 @@ import (
|
||||
"errors"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"hfb_sys/backend/internal/auditlog"
|
||||
"hfb_sys/backend/internal/database"
|
||||
@@ -621,6 +622,7 @@ func newPickupTestRepo(t *testing.T) (*Repository, *gorm.DB) {
|
||||
&model.ListingUpload{},
|
||||
&model.AdminPickup{},
|
||||
&model.AdminPickupFinancialAdjustment{},
|
||||
&model.PickupShop{},
|
||||
&model.WalletAccount{},
|
||||
&model.WalletLedger{},
|
||||
&model.AuditLog{},
|
||||
@@ -761,3 +763,158 @@ func TestApplyAdminPickupFiltersSmartRouting(t *testing.T) {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestRepositoryCreateExtraFieldsAndQueryTimeRange(t *testing.T) {
|
||||
repo, db := newPickupTestRepo(t)
|
||||
listing := seedAvailableListing(t, db, "LST-TEST-EXTRA-01", "测试商品带扩展字段", false)
|
||||
|
||||
created, err := repo.Create(t.Context(), CreateRequest{
|
||||
ListingID: listing.ID,
|
||||
Operator: "运营小张",
|
||||
Platform: "淘宝",
|
||||
ThirdPartyOrderNo: "TB-20261009-8888",
|
||||
ShopName: "测试大锤专营",
|
||||
OrderAmountCent: 12800,
|
||||
DepositAmountCent: 5000,
|
||||
ProfitAmountCent: 3000,
|
||||
Remark: "测试提号备注",
|
||||
}, 1, auditlog.Meta{})
|
||||
if err != nil {
|
||||
t.Fatalf("Create() error = %v", err)
|
||||
}
|
||||
|
||||
if created.Operator != "运营小张" {
|
||||
t.Fatalf("Operator = %q, want %q", created.Operator, "运营小张")
|
||||
}
|
||||
if created.ThirdPartyOrderNo != "TB-20261009-8888" {
|
||||
t.Fatalf("ThirdPartyOrderNo = %q, want %q", created.ThirdPartyOrderNo, "TB-20261009-8888")
|
||||
}
|
||||
assertInt64(t, "OrderAmountCent", created.OrderAmountCent, 12800)
|
||||
assertInt64(t, "DepositAmountCent", created.DepositAmountCent, 5000)
|
||||
|
||||
detail, err := repo.FindByID(t.Context(), created.ID)
|
||||
if err != nil {
|
||||
t.Fatalf("FindByID() error = %v", err)
|
||||
}
|
||||
if detail.Operator != "运营小张" || detail.ThirdPartyOrderNo != "TB-20261009-8888" {
|
||||
t.Fatalf("FindByID fields mismatch: %+v", detail)
|
||||
}
|
||||
|
||||
// 测试时间范围筛选
|
||||
now := time.Now()
|
||||
before1Hour := now.Add(-1 * time.Hour)
|
||||
after1Hour := now.Add(1 * time.Hour)
|
||||
before2Hour := now.Add(-2 * time.Hour)
|
||||
|
||||
// 命中的时间范围
|
||||
hitRes, err := repo.ListAdmin(t.Context(), AdminPickupQuery{
|
||||
StartTime: &before1Hour,
|
||||
EndTime: &after1Hour,
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("ListAdmin() with time range error = %v", err)
|
||||
}
|
||||
if hitRes.Total < 1 {
|
||||
t.Fatalf("ListAdmin() in-range total = %d, want >= 1", hitRes.Total)
|
||||
}
|
||||
|
||||
// 未命中的时间范围(过去)
|
||||
missRes, err := repo.ListAdmin(t.Context(), AdminPickupQuery{
|
||||
StartTime: &before2Hour,
|
||||
EndTime: &before1Hour,
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("ListAdmin() with miss range error = %v", err)
|
||||
}
|
||||
if missRes.Total != 0 {
|
||||
t.Fatalf("ListAdmin() out-of-range total = %d, want 0", missRes.Total)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRepositoryPickupShopCRUDAndOptions(t *testing.T) {
|
||||
repo, _ := newPickupTestRepo(t)
|
||||
|
||||
// 1. 创建店铺
|
||||
shop1, err := repo.CreateShop(t.Context(), CreatePickupShopRequest{
|
||||
Name: "旗舰一店",
|
||||
Platform: "淘宝",
|
||||
Status: "active",
|
||||
SortOrder: 10,
|
||||
Remark: "测试店铺一",
|
||||
}, 1, auditlog.Meta{})
|
||||
if err != nil {
|
||||
t.Fatalf("CreateShop() error = %v", err)
|
||||
}
|
||||
if shop1.ID == 0 || shop1.Name != "旗舰一店" {
|
||||
t.Fatalf("CreateShop() result = %+v", shop1)
|
||||
}
|
||||
|
||||
// 2. 重名校验
|
||||
_, err = repo.CreateShop(t.Context(), CreatePickupShopRequest{
|
||||
Name: "旗舰一店",
|
||||
}, 1, auditlog.Meta{})
|
||||
if !errors.Is(err, ErrPickupShopNameExists) {
|
||||
t.Fatalf("CreateShop() duplicate error = %v, want ErrPickupShopNameExists", err)
|
||||
}
|
||||
|
||||
// 3. 创建第二个店铺
|
||||
shop2, err := repo.CreateShop(t.Context(), CreatePickupShopRequest{
|
||||
Name: "拼多多二店",
|
||||
Platform: "拼多多",
|
||||
Status: "active",
|
||||
SortOrder: 20,
|
||||
Remark: "测试店铺二",
|
||||
}, 1, auditlog.Meta{})
|
||||
if err != nil {
|
||||
t.Fatalf("CreateShop(2) error = %v", err)
|
||||
}
|
||||
|
||||
// 4. 查询店铺列表
|
||||
shops, err := repo.ListShops(t.Context(), PickupShopQuery{})
|
||||
if err != nil {
|
||||
t.Fatalf("ListShops() error = %v", err)
|
||||
}
|
||||
if len(shops) != 2 {
|
||||
t.Fatalf("ListShops() length = %d, want 2", len(shops))
|
||||
}
|
||||
// 权重 20 的应该排在前面
|
||||
if shops[0].Name != "拼多多二店" {
|
||||
t.Fatalf("ListShops() first = %s, want 拼多多二店", shops[0].Name)
|
||||
}
|
||||
|
||||
// 5. 更新店铺
|
||||
updated, err := repo.UpdateShop(t.Context(), shop1.ID, UpdatePickupShopRequest{
|
||||
Name: "旗舰一店改名",
|
||||
Platform: "淘宝",
|
||||
Status: "disabled",
|
||||
SortOrder: 5,
|
||||
Remark: "修改备注",
|
||||
}, 1, auditlog.Meta{})
|
||||
if err != nil {
|
||||
t.Fatalf("UpdateShop() error = %v", err)
|
||||
}
|
||||
if updated.Name != "旗舰一店改名" || updated.Status != "disabled" {
|
||||
t.Fatalf("UpdateShop() result = %+v", updated)
|
||||
}
|
||||
|
||||
// 6. ListShopNames 选项列表(仅返回 active 店铺)
|
||||
options, err := repo.ListShopNames(t.Context(), "")
|
||||
if err != nil {
|
||||
t.Fatalf("ListShopNames() error = %v", err)
|
||||
}
|
||||
if len(options) != 1 || options[0] != "拼多多二店" {
|
||||
t.Fatalf("ListShopNames() options = %v, want [拼多多二店]", options)
|
||||
}
|
||||
|
||||
// 7. 删除店铺
|
||||
if err := repo.DeleteShop(t.Context(), shop2.ID, 1, auditlog.Meta{}); err != nil {
|
||||
t.Fatalf("DeleteShop() error = %v", err)
|
||||
}
|
||||
shopsAfterDelete, err := repo.ListShops(t.Context(), PickupShopQuery{Status: "active"})
|
||||
if err != nil {
|
||||
t.Fatalf("ListShops() after delete error = %v", err)
|
||||
}
|
||||
if len(shopsAfterDelete) != 0 {
|
||||
t.Fatalf("active shops after delete length = %d, want 0", len(shopsAfterDelete))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -148,3 +148,31 @@ func (s *Service) ListAvailableListings(ctx context.Context, query AvailableList
|
||||
}
|
||||
return s.repo.ListAvailableListings(ctx, query)
|
||||
}
|
||||
|
||||
func (s *Service) ListShops(ctx context.Context, query PickupShopQuery) ([]PickupShopDTO, error) {
|
||||
if s.repo == nil {
|
||||
return nil, ErrDependencyUnavailable
|
||||
}
|
||||
return s.repo.ListShops(ctx, query)
|
||||
}
|
||||
|
||||
func (s *Service) CreateShop(ctx context.Context, req CreatePickupShopRequest, adminID uint64, meta auditlog.Meta) (*PickupShopDTO, error) {
|
||||
if s.repo == nil {
|
||||
return nil, ErrDependencyUnavailable
|
||||
}
|
||||
return s.repo.CreateShop(ctx, req, adminID, meta)
|
||||
}
|
||||
|
||||
func (s *Service) UpdateShop(ctx context.Context, id uint64, req UpdatePickupShopRequest, adminID uint64, meta auditlog.Meta) (*PickupShopDTO, error) {
|
||||
if s.repo == nil {
|
||||
return nil, ErrDependencyUnavailable
|
||||
}
|
||||
return s.repo.UpdateShop(ctx, id, req, adminID, meta)
|
||||
}
|
||||
|
||||
func (s *Service) DeleteShop(ctx context.Context, id uint64, adminID uint64, meta auditlog.Meta) error {
|
||||
if s.repo == nil {
|
||||
return ErrDependencyUnavailable
|
||||
}
|
||||
return s.repo.DeleteShop(ctx, id, adminID, meta)
|
||||
}
|
||||
|
||||
@@ -759,6 +759,10 @@ 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/shops", requirePerm("order:pickup"), pickupHandler.ListShops)
|
||||
adminRoutes.POST("/pickups/shops", requirePerm("order:pickup"), pickupHandler.CreateShop)
|
||||
adminRoutes.PUT("/pickups/shops/:id", requirePerm("order:pickup"), pickupHandler.UpdateShop)
|
||||
adminRoutes.DELETE("/pickups/shops/:id", requirePerm("order:pickup"), pickupHandler.DeleteShop)
|
||||
adminRoutes.GET("/pickups/available-listings", requirePerm("order:pickup"), pickupHandler.AvailableListings)
|
||||
adminRoutes.GET("/pickups/:id/financial-adjustments", requirePerm("order:pickup"), pickupHandler.ListFinancialAdjustments)
|
||||
adminRoutes.GET("/pickups/:id/process-events", requirePerm("order:pickup"), pickupHandler.ProcessEvents)
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
-- +goose Up
|
||||
|
||||
ALTER TABLE admin_pickups
|
||||
ADD COLUMN operator VARCHAR(64) NOT NULL DEFAULT '' COMMENT '操作人' AFTER admin_id,
|
||||
ADD COLUMN third_party_order_no VARCHAR(128) NOT NULL DEFAULT '' COMMENT '第三方订单号' AFTER platform,
|
||||
ADD COLUMN order_amount_cent BIGINT NOT NULL DEFAULT 0 COMMENT '下单金额(分)' AFTER listing_price_cent,
|
||||
ADD COLUMN deposit_amount_cent BIGINT NOT NULL DEFAULT 0 COMMENT '押金金额(分)' AFTER order_amount_cent,
|
||||
ADD KEY idx_admin_pickups_third_party_order_no (third_party_order_no),
|
||||
ADD KEY idx_admin_pickups_operator (operator);
|
||||
|
||||
CREATE TABLE pickup_shops (
|
||||
id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
|
||||
name VARCHAR(100) NOT NULL COMMENT '店铺名称',
|
||||
platform VARCHAR(32) NOT NULL DEFAULT '' COMMENT '所属平台(如淘宝/拼多多/闲鱼/微信等)',
|
||||
status VARCHAR(20) NOT NULL DEFAULT 'active' COMMENT '状态: active启用/disabled禁用',
|
||||
sort_order INT NOT NULL DEFAULT 0 COMMENT '排序权重',
|
||||
remark VARCHAR(255) NOT NULL DEFAULT '' COMMENT '备注说明',
|
||||
created_at DATETIME NOT NULL,
|
||||
updated_at DATETIME NOT NULL,
|
||||
PRIMARY KEY (id),
|
||||
UNIQUE KEY uk_pickup_shop_name (name),
|
||||
KEY idx_pickup_shop_status (status)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='线下提号店铺配置表';
|
||||
|
||||
-- +goose Down
|
||||
|
||||
DROP TABLE IF EXISTS pickup_shops;
|
||||
|
||||
ALTER TABLE admin_pickups
|
||||
DROP KEY idx_admin_pickups_third_party_order_no,
|
||||
DROP KEY idx_admin_pickups_operator,
|
||||
DROP COLUMN operator,
|
||||
DROP COLUMN third_party_order_no,
|
||||
DROP COLUMN order_amount_cent,
|
||||
DROP COLUMN deposit_amount_cent;
|
||||
@@ -0,0 +1,153 @@
|
||||
# 线下提号字段扩展与店铺管理实施方案
|
||||
|
||||
## 一、进度与证据分类登记
|
||||
|
||||
### 1. 已知(附证据与确信度)
|
||||
- **远程生产环境只读统计(确信度 100%)**:
|
||||
通过 SSH 连接生产服务器(aliyun, 42.121.217.194),从线上 MySQL 执行只读聚合查询:
|
||||
`SELECT DISTINCT shop_name, COUNT(*) as cnt FROM admin_pickups GROUP BY shop_name ORDER BY cnt DESC;`
|
||||
获取到了全部 61 种历史店铺名称及统计分布(详见后文清单)。其中高频店铺包括:糖浆(1049)、王大锤游戏(846)、王大锤数娱(604)、淘宝王大锤网游专营店(494)等,亦包含部分历史空值和脏数据。
|
||||
- **当前提号表结构(确信度 100%)**:
|
||||
`admin_pickups` 现有字段包含 `admin_id`, `platform`, `shop_name`, `listing_price_cent`, `profit_amount_cent` 等,缺少 `operator`, `third_party_order_no`, `order_amount_cent`, `deposit_amount_cent`。
|
||||
- **当前店铺来源逻辑(确信度 100%)**:
|
||||
现有 `GET /api/v1/admin/pickups/shop-options` 直接对 `admin_pickups` 表进行 `Distinct("shop_name")` 查询,没有独立的店铺配置表。
|
||||
- **当前筛选条件(确信度 100%)**:
|
||||
现有列表仅支持关键词、状态、店铺、账号来源、线下结算状态筛选,缺少精确到“日期+时间”的时间范围筛选。
|
||||
- **SQL 防线与测试规范(确信度 100%)**:
|
||||
`scripts/check-sql-guard.sh` 对 migration 追加、Goose Up/Down 规范以及单号唯一索引直达有严格断言。
|
||||
|
||||
### 2. 未知与假设(附当前假设与确信度)
|
||||
- **店铺历史脏数据映射(确信度 100%)**:
|
||||
用户表示后续会提供具体的店铺映射规则;当前阶段先完成标准店铺表设计及 CRUD 管理功能,前端与接口在选择店铺时兼容已录入店铺和历史店铺。
|
||||
- **操作人录入习惯(确信度 95%)**:
|
||||
创建提号时,操作人字段默认自动预填当前登录管理员的用户名/昵称,同时支持管理员自由修改输入为客服名或经办人姓名。
|
||||
|
||||
---
|
||||
|
||||
## 二、生产环境历史店铺名称只读清单(共 61 项)
|
||||
|
||||
| 序号 | 店铺名称 | 历史提号单量 | 备注/分类特征 |
|
||||
| :--- | :--- | :--- | :--- |
|
||||
| 1 | 糖浆 | 1049 | 主力店铺 |
|
||||
| 2 | 王大锤游戏 | 846 | 主力店铺 |
|
||||
| 3 | 王大锤数娱 | 604 | 主力店铺 |
|
||||
| 4 | 淘宝王大锤网游专营店 | 494 | 淘宝渠道 |
|
||||
| 5 | 咸鱼大锤商行小白 | 170 | 闲鱼渠道 |
|
||||
| 6 | 拼多多王大锤数娱 | 152 | 拼多多渠道 |
|
||||
| 7 | 淘宝王大锤网游 | 149 | 淘宝渠道 |
|
||||
| 8 | 糖浆游戏 拼多多 | 143 | 拼多多渠道 |
|
||||
| 9 | (空字符串) | 137 | 未填店铺 |
|
||||
| 10 | 咸鱼羊小胖 | 114 | 闲鱼渠道 |
|
||||
| 11 | 咸鱼滑翔伞 | 110 | 闲鱼渠道 |
|
||||
| 12 | 咸鱼大锤商行 | 68 | 闲鱼渠道 |
|
||||
| 13 | 王大锤游戏专营 | 36 | 主力店铺变体 |
|
||||
| 14 | 咸鱼西红柿不错 | 35 | 闲鱼渠道 |
|
||||
| 15 | 微信 | 33 | 微信私域 |
|
||||
| 16 | 拼多多游戏 | 32 | 拼多多渠道 |
|
||||
| 17 | 王大锤网游专营店 | 30 | 淘宝/天猫变体 |
|
||||
| 18 | 淘宝艾优游戏 | 30 | 淘宝渠道 |
|
||||
| 19 | 游戏 | 29 | 简写 |
|
||||
| 20 | 淘宝三角洲行动租号商行 | 27 | 淘宝渠道 |
|
||||
| 21 | 闲鱼羊小胖 | 23 | 闲鱼渠道 |
|
||||
| 22 | 富友 | 18 | 渠道/平台 |
|
||||
| 23 | 滑翔伞 | 17 | 简写 |
|
||||
| 24 | 宜收宝 | 15 | 渠道/平台 |
|
||||
| 25 | 淘宝大锤商行 | 15 | 淘宝渠道 |
|
||||
| 26 | 西红柿不错 | 14 | 简写 |
|
||||
| 27 | 三角洲行动租号商行 | 11 | 简写 |
|
||||
| 28 | 大锤商行 | 9 | 简写 |
|
||||
| 29 | 羊小胖 | 8 | 简写 |
|
||||
| 30 | 数 | 7 | 简写脏数据 |
|
||||
| 31 | 三角洲行动商行 | 6 | 简写 |
|
||||
| 32 | 三角洲行动 | 6 | 简写 |
|
||||
| 33 | 淘宝356166138小边 | 5 | 淘宝单店 |
|
||||
| 34 | 艾优 | 4 | 简写 |
|
||||
| 35 | 溟科 | 4 | 机构/分销 |
|
||||
| 36 | 覃公子未付 | 4 | 临时备注型店铺名 |
|
||||
| 37 | 11.25 7 | 3 | 日期数字型脏数据 |
|
||||
| 38 | 艾优游戏 | 3 | 简写 |
|
||||
| 39 | 小边 | 3 | 客服/分销 |
|
||||
| 40 | 数娱 | 3 | 简写 |
|
||||
| 41 | 淘宝王大锤商行 | 3 | 淘宝渠道 |
|
||||
| 42 | 富友+宜收宝 | 2 | 复合渠道 |
|
||||
| 43 | 淘宝易磊 | 2 | 淘宝单店 |
|
||||
| 44 | 半野潮汐俱乐部 | 2 | 专营店 |
|
||||
| 45 | 10.17W免押500 | 2 | 备注型脏数据 |
|
||||
| 46 | 7.20 拼多多东 | 2 | 日期拼多多型 |
|
||||
| 47 | 超帅 | 1 | 别名 |
|
||||
| 48 | 0526🐠 | 1 | 特殊符号 |
|
||||
| 49 | 西红柿 | 1 | 简写 |
|
||||
| 50 | 行动 | 1 | 简写 |
|
||||
| 51 | 淘宝980493223 | 1 | 淘宝店号 |
|
||||
| 52 | .7.22 | 1 | 日期型 |
|
||||
| 53 | 网游 | 1 | 简写 |
|
||||
| 54 | Lost | 1 | 别名 |
|
||||
| 55 | 七友 | 1 | 别名 |
|
||||
| 56 | 大锤 | 1 | 简写 |
|
||||
| 57 | 王大锤商行 | 1 | 简写 |
|
||||
| 58 | 爱优游戏 | 1 | 同音别名 |
|
||||
| 59 | 小白 | 1 | 客服名 |
|
||||
| 60 | 深南群主正常拿号 | 1 | 渠道拿号说明 |
|
||||
| 61 | 撞车 / 正在输入 | 各 1 | 临时测试/异常字符 |
|
||||
|
||||
---
|
||||
|
||||
## 三、方案设计与改造范围
|
||||
|
||||
### 1. 数据库升级 (`000079_admin_pickup_extra_fields_and_shops.sql`)
|
||||
1. 对 `admin_pickups` 表增加 4 个字段及必要索引:
|
||||
- `operator VARCHAR(64) NOT NULL DEFAULT '' COMMENT '操作人(如客服/运营人员名字)'`
|
||||
- `third_party_order_no VARCHAR(128) NOT NULL DEFAULT '' COMMENT '第三方订单号'`
|
||||
- `order_amount_cent BIGINT NOT NULL DEFAULT 0 COMMENT '下单金额(分)'`
|
||||
- `deposit_amount_cent BIGINT NOT NULL DEFAULT 0 COMMENT '押金金额(分)'`
|
||||
- 索引:`idx_admin_pickups_third_party_order_no`, `idx_admin_pickups_operator`
|
||||
2. 新增店铺配置表 `pickup_shops`:
|
||||
- `id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY`
|
||||
- `name VARCHAR(100) NOT NULL UNIQUE COMMENT '店铺名称'`
|
||||
- `platform VARCHAR(32) NOT NULL DEFAULT '' COMMENT '所属平台'`
|
||||
- `status VARCHAR(20) NOT NULL DEFAULT 'active' COMMENT '状态: active/disabled'`
|
||||
- `sort_order INT NOT NULL DEFAULT 0 COMMENT '排序权重'`
|
||||
- `remark VARCHAR(255) NOT NULL DEFAULT '' COMMENT '备注'`
|
||||
- `created_at DATETIME NOT NULL, updated_at DATETIME NOT NULL`
|
||||
- 索引:`uk_pickup_shop_name`, `idx_pickup_shop_status`
|
||||
|
||||
### 2. 后端核心代码改造
|
||||
- `backend/internal/model/pickup.go`:结构体添加新增字段。
|
||||
- `backend/internal/model/pickup_shop.go`:创建店铺模型。
|
||||
- `backend/internal/modules/pickup/dto.go`:
|
||||
- `PickupDTO` 扩充 `Operator`, `ThirdPartyOrderNo`, `OrderAmountCent`, `DepositAmountCent`;
|
||||
- `CreateRequest` 扩充对应 4 个字段;
|
||||
- `AdminPickupQuery` 增加 `StartTime`, `EndTime` 时间范围筛选;
|
||||
- 增加 `PickupShopDTO`, `CreatePickupShopRequest`, `UpdatePickupShopRequest`, `PickupShopQuery` 等 DTO。
|
||||
- `backend/internal/modules/pickup/repository.go`:
|
||||
- `Create` 写入新字段,并在事件与审计日志中留痕;
|
||||
- `applyAdminPickupFilters` 增加 `p.created_at >= ?` 与 `p.created_at <= ?` 时间筛选;关键词搜索补充支持第三方单号与操作人;
|
||||
- 增加店铺管理 CRUD:`ListShops`, `CreateShop`, `UpdateShop`, `DeleteShop`;
|
||||
- `ListShopNames`:优先读取 `pickup_shops` 活跃店铺,并合并 `admin_pickups` 历史店铺去重返回。
|
||||
- `backend/internal/modules/pickup/handler.go` & `handler_parse.go`:
|
||||
- 解析 query 参数中的 `start_time` 与 `end_time`(支持多种格式);
|
||||
- 实现店铺增删改查处理函数。
|
||||
- `backend/internal/router/router.go`:注册店铺相关路由。
|
||||
|
||||
### 3. 前端界面重构与布局优化
|
||||
- **创建线下提号弹窗布局优化**:
|
||||
- 第一行(双列):左侧「账号来源」,右侧「操作人」(默认当前登录用户,可手动修改)。
|
||||
- 第二行(全宽):「选择账号」(带远程搜索、来源确认与价格快照)。
|
||||
- 第三行(双列):左侧「第三方订单号」,右侧「交易渠道」。
|
||||
- 第四行(全宽):「店铺名称」(下拉选择已有店铺,右侧提供店铺管理快捷入口)。
|
||||
- 第五行(三列金额区块):
|
||||
- 「下单金额(元)」
|
||||
- 「押金金额(元)」
|
||||
- 「线下利润(元)」
|
||||
- 第六行(全宽):「备注」。
|
||||
- 底部操作按钮及说明提示。
|
||||
- **店铺管理功能**:
|
||||
- 线下提号页面顶部增加「店铺管理」入口;
|
||||
- 弹出全功能店铺管理对话框,支持店铺列表、新增、编辑、启用/禁用、删除;
|
||||
- 店铺变更后自动同步刷新外层提号的店铺下拉选择框。
|
||||
- **时间筛选(日期+时间)**:
|
||||
- 筛选栏加入 `el-date-picker`(`type="datetimerange"`),格式精确到秒;
|
||||
- 支持快捷查询与一键重置。
|
||||
- **提号列表与详情页字段展示**:
|
||||
- 列表中增加操作人、第三方单号、下单金额、押金金额的展示;
|
||||
- 详情页概览卡片同步展示新增字段。
|
||||
@@ -19,12 +19,16 @@ export interface AdminPickup {
|
||||
owner_id: number
|
||||
owner_phone: string
|
||||
admin_id: number
|
||||
operator: string
|
||||
platform: string
|
||||
third_party_order_no: string
|
||||
shop_name: string
|
||||
account_source: PickupAccountSource
|
||||
source_channel: string
|
||||
settlement_mode: PickupSettlementMode
|
||||
listing_price_cent: number
|
||||
order_amount_cent: number
|
||||
deposit_amount_cent: number
|
||||
owner_price_cent: number
|
||||
website_profit_cent: number
|
||||
profit_amount_cent: number
|
||||
@@ -108,8 +112,12 @@ export interface AvailableListing {
|
||||
|
||||
export interface AdminPickupCreateRequest {
|
||||
listing_id: number
|
||||
operator?: string
|
||||
platform?: string
|
||||
third_party_order_no?: string
|
||||
shop_name?: string
|
||||
order_amount_cent?: number
|
||||
deposit_amount_cent?: number
|
||||
profit_amount_cent?: number
|
||||
remark?: string
|
||||
}
|
||||
@@ -147,6 +155,43 @@ export interface AdminPickupListQuery {
|
||||
shop_name?: string
|
||||
account_source?: PickupAccountSource | ''
|
||||
offline_settlement_status?: PickupOfflineSettlementStatus | ''
|
||||
operator?: string
|
||||
third_party_order_no?: string
|
||||
start_time?: string
|
||||
end_time?: string
|
||||
}
|
||||
|
||||
export interface PickupShop {
|
||||
id: number
|
||||
name: string
|
||||
platform: string
|
||||
status: 'active' | 'disabled'
|
||||
sort_order: number
|
||||
remark: string
|
||||
created_at: string
|
||||
updated_at: string
|
||||
}
|
||||
|
||||
export interface PickupShopCreateRequest {
|
||||
name: string
|
||||
platform?: string
|
||||
status?: 'active' | 'disabled'
|
||||
sort_order?: number
|
||||
remark?: string
|
||||
}
|
||||
|
||||
export interface PickupShopUpdateRequest {
|
||||
name: string
|
||||
platform?: string
|
||||
status: 'active' | 'disabled'
|
||||
sort_order?: number
|
||||
remark?: string
|
||||
}
|
||||
|
||||
export interface PickupShopQuery {
|
||||
keyword?: string
|
||||
platform?: string
|
||||
status?: string
|
||||
}
|
||||
|
||||
function cleanParams(query: object) {
|
||||
@@ -270,3 +315,27 @@ export async function cancelAdminPickup(id: number, reason: string) {
|
||||
)
|
||||
return data.data
|
||||
}
|
||||
|
||||
export async function fetchPickupShops(query: PickupShopQuery = {}) {
|
||||
const { data } = await apiClient.get<ApiResponse<PickupShop[]>>('/admin/pickups/shops', {
|
||||
params: cleanParams(query),
|
||||
})
|
||||
return Array.isArray(data.data) ? data.data : []
|
||||
}
|
||||
|
||||
export async function createPickupShop(req: PickupShopCreateRequest) {
|
||||
const { data } = await apiClient.post<ApiResponse<PickupShop>>('/admin/pickups/shops', req)
|
||||
return data.data
|
||||
}
|
||||
|
||||
export async function updatePickupShop(id: number, req: PickupShopUpdateRequest) {
|
||||
const { data } = await apiClient.put<ApiResponse<PickupShop>>(`/admin/pickups/shops/${id}`, req)
|
||||
return data.data
|
||||
}
|
||||
|
||||
export async function deletePickupShop(id: number) {
|
||||
const { data } = await apiClient.delete<ApiResponse<{ deleted: boolean }>>(
|
||||
`/admin/pickups/shops/${id}`
|
||||
)
|
||||
return data.data
|
||||
}
|
||||
|
||||
@@ -0,0 +1,401 @@
|
||||
<script setup lang="ts">
|
||||
import { onMounted, reactive, ref } from 'vue'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import { Delete, Edit, Plus, Refresh, Search } from '@element-plus/icons-vue'
|
||||
import {
|
||||
createPickupShop,
|
||||
deletePickupShop,
|
||||
fetchPickupShops,
|
||||
updatePickupShop,
|
||||
type PickupShop,
|
||||
type PickupShopCreateRequest,
|
||||
type PickupShopUpdateRequest,
|
||||
} from '@/features/admin/api/adminPickup'
|
||||
import { formatDateTime } from '@/shared/utils/time'
|
||||
|
||||
const props = defineProps<{
|
||||
modelValue: boolean
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: 'update:modelValue', value: boolean): void
|
||||
(e: 'updated'): void
|
||||
}>()
|
||||
|
||||
const loading = ref(false)
|
||||
const shopList = ref<PickupShop[]>([])
|
||||
const filters = reactive({
|
||||
keyword: '',
|
||||
platform: '',
|
||||
status: '',
|
||||
})
|
||||
|
||||
// 新建/编辑店铺弹窗状态
|
||||
const formDialogVisible = ref(false)
|
||||
const formSaving = ref(false)
|
||||
const editingId = ref<number | null>(null)
|
||||
const shopForm = reactive({
|
||||
name: '',
|
||||
platform: '',
|
||||
sort_order: 0,
|
||||
status: 'active' as 'active' | 'disabled',
|
||||
remark: '',
|
||||
})
|
||||
|
||||
const platformOptions = ['淘宝', '拼多多', '闲鱼', '微信', 'QQ', '抖音', '快手']
|
||||
|
||||
function handleClose() {
|
||||
emit('update:modelValue', false)
|
||||
}
|
||||
|
||||
async function loadShops() {
|
||||
loading.value = true
|
||||
try {
|
||||
shopList.value = await fetchPickupShops({
|
||||
keyword: filters.keyword.trim(),
|
||||
platform: filters.platform,
|
||||
status: filters.status,
|
||||
})
|
||||
} catch (err: unknown) {
|
||||
ElMessage.error(getErrorMessage(err) || '加载店铺列表失败')
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function handleSearch() {
|
||||
loadShops()
|
||||
}
|
||||
|
||||
function resetFilters() {
|
||||
filters.keyword = ''
|
||||
filters.platform = ''
|
||||
filters.status = ''
|
||||
loadShops()
|
||||
}
|
||||
|
||||
function openCreateForm() {
|
||||
editingId.value = null
|
||||
shopForm.name = ''
|
||||
shopForm.platform = ''
|
||||
shopForm.sort_order = 0
|
||||
shopForm.status = 'active'
|
||||
shopForm.remark = ''
|
||||
formDialogVisible.value = true
|
||||
}
|
||||
|
||||
function openEditForm(row: PickupShop) {
|
||||
editingId.value = row.id
|
||||
shopForm.name = row.name
|
||||
shopForm.platform = row.platform
|
||||
shopForm.sort_order = row.sort_order
|
||||
shopForm.status = row.status
|
||||
shopForm.remark = row.remark
|
||||
formDialogVisible.value = true
|
||||
}
|
||||
|
||||
async function handleSaveShop() {
|
||||
if (!shopForm.name.trim()) {
|
||||
ElMessage.warning('请输入店铺名称')
|
||||
return
|
||||
}
|
||||
formSaving.value = true
|
||||
try {
|
||||
if (editingId.value) {
|
||||
const payload: PickupShopUpdateRequest = {
|
||||
name: shopForm.name.trim(),
|
||||
platform: shopForm.platform.trim(),
|
||||
sort_order: Number(shopForm.sort_order) || 0,
|
||||
status: shopForm.status,
|
||||
remark: shopForm.remark.trim(),
|
||||
}
|
||||
await updatePickupShop(editingId.value, payload)
|
||||
ElMessage.success('店铺更新成功')
|
||||
} else {
|
||||
const payload: PickupShopCreateRequest = {
|
||||
name: shopForm.name.trim(),
|
||||
platform: shopForm.platform.trim(),
|
||||
sort_order: Number(shopForm.sort_order) || 0,
|
||||
status: shopForm.status,
|
||||
remark: shopForm.remark.trim(),
|
||||
}
|
||||
await createPickupShop(payload)
|
||||
ElMessage.success('店铺创建成功')
|
||||
}
|
||||
formDialogVisible.value = false
|
||||
await loadShops()
|
||||
emit('updated')
|
||||
} catch (err: unknown) {
|
||||
ElMessage.error(getErrorMessage(err) || '保存店铺失败')
|
||||
} finally {
|
||||
formSaving.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function handleToggleStatus(row: PickupShop) {
|
||||
const nextStatus = row.status === 'active' ? 'disabled' : 'active'
|
||||
const actionText = nextStatus === 'active' ? '启用' : '禁用'
|
||||
try {
|
||||
await updatePickupShop(row.id, {
|
||||
name: row.name,
|
||||
platform: row.platform,
|
||||
sort_order: row.sort_order,
|
||||
status: nextStatus,
|
||||
remark: row.remark,
|
||||
})
|
||||
ElMessage.success(`已${actionText}店铺「${row.name}」`)
|
||||
row.status = nextStatus
|
||||
emit('updated')
|
||||
} catch (err: unknown) {
|
||||
ElMessage.error(getErrorMessage(err) || `${actionText}失败`)
|
||||
}
|
||||
}
|
||||
|
||||
async function handleDeleteShop(row: PickupShop) {
|
||||
try {
|
||||
await ElMessageBox.confirm(
|
||||
`确定要删除店铺「${row.name}」吗?历史提号中的记录不会被删除。`,
|
||||
'确认删除店铺',
|
||||
{
|
||||
confirmButtonText: '确定删除',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning',
|
||||
}
|
||||
)
|
||||
loading.value = true
|
||||
try {
|
||||
await deletePickupShop(row.id)
|
||||
ElMessage.success('店铺已删除')
|
||||
await loadShops()
|
||||
emit('updated')
|
||||
} catch (err: unknown) {
|
||||
ElMessage.error(getErrorMessage(err) || '删除店铺失败')
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
} catch {
|
||||
/* 用户放弃 */
|
||||
}
|
||||
}
|
||||
|
||||
function getErrorMessage(e: unknown): string {
|
||||
if (e && typeof e === 'object') {
|
||||
const response = (e as { response?: { data?: { message?: string } } }).response
|
||||
if (response?.data?.message) return response.data.message
|
||||
const message = (e as { message?: string }).message
|
||||
if (message) return message
|
||||
}
|
||||
return ''
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
loadShops()
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<el-dialog
|
||||
:model-value="props.modelValue"
|
||||
title="店铺管理"
|
||||
width="min(920px, calc(100vw - 32px))"
|
||||
:before-close="handleClose"
|
||||
append-to-body
|
||||
>
|
||||
<div class="shop-mgr-toolbar">
|
||||
<el-form inline @submit.prevent>
|
||||
<el-form-item label="店铺名称">
|
||||
<el-input
|
||||
v-model="filters.keyword"
|
||||
placeholder="搜索名称或备注"
|
||||
clearable
|
||||
style="width: 170px"
|
||||
@keyup.enter="handleSearch"
|
||||
@clear="handleSearch"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="平台">
|
||||
<el-select
|
||||
v-model="filters.platform"
|
||||
placeholder="全部平台"
|
||||
clearable
|
||||
style="width: 120px"
|
||||
@change="handleSearch"
|
||||
>
|
||||
<el-option v-for="item in platformOptions" :key="item" :label="item" :value="item" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="状态">
|
||||
<el-select
|
||||
v-model="filters.status"
|
||||
placeholder="全部状态"
|
||||
clearable
|
||||
style="width: 110px"
|
||||
@change="handleSearch"
|
||||
>
|
||||
<el-option label="启用" value="active" />
|
||||
<el-option label="禁用" value="disabled" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button type="primary" :icon="Search" @click="handleSearch">查询</el-button>
|
||||
<el-button @click="resetFilters">重置</el-button>
|
||||
<el-button :icon="Refresh" @click="loadShops">刷新</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<div class="toolbar-right">
|
||||
<el-button type="success" :icon="Plus" @click="openCreateForm">新增店铺</el-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<el-table v-loading="loading" :data="shopList" border stripe max-height="460">
|
||||
<el-table-column prop="name" label="店铺名称" min-width="150">
|
||||
<template #default="{ row }">
|
||||
<strong style="color: var(--el-text-color-primary)">{{ row.name }}</strong>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="platform" label="平台/渠道" width="120">
|
||||
<template #default="{ row }">
|
||||
<el-tag v-if="row.platform" size="small" effect="plain">{{ row.platform }}</el-tag>
|
||||
<span v-else class="text-muted">-</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="sort_order" label="排序权重" width="100" align="center" />
|
||||
<el-table-column prop="status" label="状态" width="100" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="row.status === 'active' ? 'success' : 'info'" size="small">
|
||||
{{ row.status === 'active' ? '启用中' : '已禁用' }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="remark" label="备注" min-width="160" show-overflow-tooltip>
|
||||
<template #default="{ row }">{{ row.remark || '-' }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="updated_at" label="更新时间" width="160">
|
||||
<template #default="{ row }">{{ formatDateTime(row.updated_at) }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" width="190" fixed="right" align="center">
|
||||
<template #default="{ row }">
|
||||
<div class="shop-actions">
|
||||
<el-button size="small" :icon="Edit" @click="openEditForm(row)">编辑</el-button>
|
||||
<el-button
|
||||
size="small"
|
||||
:type="row.status === 'active' ? 'warning' : 'success'"
|
||||
plain
|
||||
@click="handleToggleStatus(row)"
|
||||
>
|
||||
{{ row.status === 'active' ? '禁用' : '启用' }}
|
||||
</el-button>
|
||||
<el-button
|
||||
size="small"
|
||||
type="danger"
|
||||
plain
|
||||
:icon="Delete"
|
||||
@click="handleDeleteShop(row)"
|
||||
>
|
||||
删除
|
||||
</el-button>
|
||||
</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
|
||||
<!-- 新建/编辑店铺弹窗 -->
|
||||
<el-dialog
|
||||
v-model="formDialogVisible"
|
||||
:title="editingId ? '编辑店铺' : '新增店铺'"
|
||||
width="460px"
|
||||
append-to-body
|
||||
>
|
||||
<el-form label-width="90px">
|
||||
<el-form-item label="店铺名称" required>
|
||||
<el-input
|
||||
v-model="shopForm.name"
|
||||
placeholder="例如:糖浆、淘宝王大锤专营店"
|
||||
maxlength="100"
|
||||
show-word-limit
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="所属平台">
|
||||
<el-select
|
||||
v-model="shopForm.platform"
|
||||
filterable
|
||||
allow-create
|
||||
default-first-option
|
||||
clearable
|
||||
placeholder="选择或输入平台(如 淘宝/拼多多)"
|
||||
style="width: 100%"
|
||||
>
|
||||
<el-option v-for="item in platformOptions" :key="item" :label="item" :value="item" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="排序权重">
|
||||
<el-input-number
|
||||
v-model="shopForm.sort_order"
|
||||
:min="0"
|
||||
:max="9999"
|
||||
controls-position="right"
|
||||
style="width: 100%"
|
||||
/>
|
||||
<div class="form-hint">权重越大的店铺在下拉选择时排在越前面</div>
|
||||
</el-form-item>
|
||||
<el-form-item label="状态">
|
||||
<el-radio-group v-model="shopForm.status">
|
||||
<el-radio value="active">启用</el-radio>
|
||||
<el-radio value="disabled">禁用</el-radio>
|
||||
</el-radio-group>
|
||||
</el-form-item>
|
||||
<el-form-item label="备注说明">
|
||||
<el-input
|
||||
v-model="shopForm.remark"
|
||||
type="textarea"
|
||||
:rows="2"
|
||||
placeholder="填写店铺说明或客服负责人(可选)"
|
||||
maxlength="255"
|
||||
show-word-limit
|
||||
/>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<el-button @click="formDialogVisible = false">取消</el-button>
|
||||
<el-button type="primary" :loading="formSaving" @click="handleSaveShop">保存</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
|
||||
<template #footer>
|
||||
<el-button @click="handleClose">关闭</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.shop-mgr-toolbar {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: flex-start;
|
||||
flex-wrap: wrap;
|
||||
gap: 12px;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.toolbar-right {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.shop-actions {
|
||||
display: flex;
|
||||
gap: 6px;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.text-muted {
|
||||
color: var(--el-text-color-secondary);
|
||||
}
|
||||
|
||||
.form-hint {
|
||||
font-size: 12px;
|
||||
color: var(--el-text-color-secondary);
|
||||
line-height: 1.4;
|
||||
margin-top: 4px;
|
||||
}
|
||||
</style>
|
||||
@@ -97,6 +97,12 @@ const moneySplitRows = computed(() => {
|
||||
{ label: '线下利润', amountCent: effectiveProfitCent(row), tone: 'profit' },
|
||||
{ label: '结算给号主', amountCent: effectiveSettleCent(row), tone: '' },
|
||||
]
|
||||
if (row.order_amount_cent > 0) {
|
||||
rows.push({ label: '下单金额', amountCent: row.order_amount_cent, tone: '' })
|
||||
}
|
||||
if (row.deposit_amount_cent > 0) {
|
||||
rows.push({ label: '押金金额', amountCent: row.deposit_amount_cent, tone: '' })
|
||||
}
|
||||
if (offlineTotalCent !== null) {
|
||||
rows.push({ label: '线下成交合计', amountCent: offlineTotalCent, tone: 'total' })
|
||||
}
|
||||
@@ -454,10 +460,36 @@ function readSnapshotResources(summary: Record<string, unknown> | null): Snapsho
|
||||
<dt>交易渠道</dt>
|
||||
<dd>{{ pickup.platform || '-' }}</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt>第三方单号</dt>
|
||||
<dd>{{ pickup.third_party_order_no || '-' }}</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt>店铺名称</dt>
|
||||
<dd>{{ pickup.shop_name || '-' }}</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt>操作人</dt>
|
||||
<dd>{{ pickup.operator || '-' }}</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt>下单金额</dt>
|
||||
<dd>
|
||||
{{
|
||||
pickup.order_amount_cent > 0 ? formatCentWithSymbol(pickup.order_amount_cent) : '-'
|
||||
}}
|
||||
</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt>押金金额</dt>
|
||||
<dd>
|
||||
{{
|
||||
pickup.deposit_amount_cent > 0
|
||||
? formatCentWithSymbol(pickup.deposit_amount_cent)
|
||||
: '-'
|
||||
}}
|
||||
</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt>管理员ID</dt>
|
||||
<dd>{{ pickup.admin_id }}</dd>
|
||||
|
||||
@@ -118,3 +118,27 @@
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.amount-group-card {
|
||||
background: var(--el-fill-color-light, #f8fafc);
|
||||
border: 1px solid var(--el-border-color-lighter, #e2e8f0);
|
||||
border-radius: 8px;
|
||||
padding: 12px 14px 6px;
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
.amount-group-title {
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
color: #334155;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.amount-item :deep(.el-form-item__label) {
|
||||
font-size: 12px;
|
||||
padding-right: 4px;
|
||||
}
|
||||
|
||||
.create-pickup-form :deep(.el-form-item) {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, onMounted, reactive, ref } from 'vue'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import { EditPen, Plus, Refresh, Search } from '@element-plus/icons-vue'
|
||||
import { EditPen, Plus, Refresh, Search, Setting, Shop } from '@element-plus/icons-vue'
|
||||
import {
|
||||
cancelAdminPickup,
|
||||
confirmAdminPickupOfflineSettlement,
|
||||
@@ -18,14 +18,20 @@ import {
|
||||
type PickupAccountSource,
|
||||
type PickupOfflineSettlementStatus,
|
||||
} from '@/features/admin/api/adminPickup'
|
||||
import PickupShopDialog from '@/features/admin/components/PickupShopDialog.vue'
|
||||
import { useAdminSessionStore } from '@/stores/adminSession'
|
||||
import { centToYuan, formatCentWithSymbol, yuanToCent } from '@/shared/utils/money'
|
||||
import { formatDateTime } from '@/shared/utils/time'
|
||||
import { pickupStatusLabel } from '@/shared/utils/statusLabels'
|
||||
import { adminPath } from '@/shared/utils/adminPath'
|
||||
|
||||
const adminSession = useAdminSessionStore()
|
||||
const currentAdminName = computed(() => adminSession.nickname || adminSession.username || '')
|
||||
|
||||
const loading = ref(false)
|
||||
const items = ref<AdminPickup[]>([])
|
||||
const total = ref(0)
|
||||
const filterTimeRange = ref<[string, string] | null>(null)
|
||||
const filters = reactive({
|
||||
page: 1,
|
||||
page_size: 20,
|
||||
@@ -34,12 +40,15 @@ const filters = reactive({
|
||||
shop_name: '',
|
||||
account_source: '' as PickupAccountSource | '',
|
||||
offline_settlement_status: '' as PickupOfflineSettlementStatus | '',
|
||||
start_time: '',
|
||||
end_time: '',
|
||||
})
|
||||
|
||||
const createDialogVisible = ref(false)
|
||||
const completeDialogVisible = ref(false)
|
||||
const profitDialogVisible = ref(false)
|
||||
const financialAdjustmentDialogVisible = ref(false)
|
||||
const shopMgrVisible = ref(false)
|
||||
const activePickupId = ref(0)
|
||||
const activePickup = ref<AdminPickup | null>(null)
|
||||
const profitSaving = ref(false)
|
||||
@@ -47,8 +56,12 @@ const financialAdjustmentSaving = ref(false)
|
||||
|
||||
const createForm = reactive({
|
||||
listing_id: null as number | null,
|
||||
operator: '',
|
||||
platform: '',
|
||||
third_party_order_no: '',
|
||||
shop_name: '',
|
||||
order_amount: 0,
|
||||
deposit_amount: 0,
|
||||
profit_amount: 0,
|
||||
remark: '',
|
||||
})
|
||||
@@ -103,12 +116,26 @@ function handleSearch() {
|
||||
loadList()
|
||||
}
|
||||
|
||||
function handleTimeRangeChange(val: [string, string] | null) {
|
||||
if (val && val.length === 2) {
|
||||
filters.start_time = val[0]
|
||||
filters.end_time = val[1]
|
||||
} else {
|
||||
filters.start_time = ''
|
||||
filters.end_time = ''
|
||||
}
|
||||
handleSearch()
|
||||
}
|
||||
|
||||
function resetFilters() {
|
||||
filters.keyword = ''
|
||||
filters.status = ''
|
||||
filters.shop_name = ''
|
||||
filters.account_source = ''
|
||||
filters.offline_settlement_status = ''
|
||||
filters.start_time = ''
|
||||
filters.end_time = ''
|
||||
filterTimeRange.value = null
|
||||
filters.page = 1
|
||||
loadList()
|
||||
}
|
||||
@@ -124,8 +151,12 @@ function handleSizeChange(s: number) {
|
||||
|
||||
function openCreateDialog() {
|
||||
createForm.listing_id = null
|
||||
createForm.operator = currentAdminName.value || ''
|
||||
createForm.platform = ''
|
||||
createForm.third_party_order_no = ''
|
||||
createForm.shop_name = ''
|
||||
createForm.order_amount = 0
|
||||
createForm.deposit_amount = 0
|
||||
createForm.profit_amount = 0
|
||||
createForm.remark = ''
|
||||
listingOptions.value = []
|
||||
@@ -176,6 +207,14 @@ async function handleCreate() {
|
||||
ElMessage.warning('请选择要提号的账号')
|
||||
return
|
||||
}
|
||||
if (createForm.order_amount < 0) {
|
||||
ElMessage.warning('下单金额不能为负数')
|
||||
return
|
||||
}
|
||||
if (createForm.deposit_amount < 0) {
|
||||
ElMessage.warning('押金金额不能为负数')
|
||||
return
|
||||
}
|
||||
if (createForm.profit_amount < 0) {
|
||||
ElMessage.warning('线下利润不能为负数')
|
||||
return
|
||||
@@ -184,10 +223,14 @@ async function handleCreate() {
|
||||
try {
|
||||
await createAdminPickup({
|
||||
listing_id: createForm.listing_id,
|
||||
platform: createForm.platform,
|
||||
shop_name: createForm.shop_name,
|
||||
operator: createForm.operator.trim(),
|
||||
platform: createForm.platform.trim(),
|
||||
third_party_order_no: createForm.third_party_order_no.trim(),
|
||||
shop_name: createForm.shop_name.trim(),
|
||||
order_amount_cent: yuanToCent(createForm.order_amount),
|
||||
deposit_amount_cent: yuanToCent(createForm.deposit_amount),
|
||||
profit_amount_cent: yuanToCent(createForm.profit_amount),
|
||||
remark: createForm.remark,
|
||||
remark: createForm.remark.trim(),
|
||||
})
|
||||
ElMessage.success('提号订单已创建')
|
||||
createDialogVisible.value = false
|
||||
@@ -449,6 +492,7 @@ function listingOptionLabel(item: AvailableListing) {
|
||||
<p>管理员线下提号,站内上传结算到钱包,平台代管通过线下结算确认打款。</p>
|
||||
</div>
|
||||
<div class="toolbar-actions">
|
||||
<el-button :icon="Shop" @click="shopMgrVisible = true">店铺管理</el-button>
|
||||
<el-button type="primary" :icon="Plus" @click="openCreateDialog">创建提号</el-button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -458,7 +502,7 @@ function listingOptionLabel(item: AvailableListing) {
|
||||
<el-form-item label="关键词" class="pickup-keyword-filter">
|
||||
<el-input
|
||||
v-model="filters.keyword"
|
||||
placeholder="提号编号 / 上架编号 / 账号标题 / 手机号"
|
||||
placeholder="提号编号 / 上架编号 / 账号标题 / 手机号 / 第三方单号 / 操作人"
|
||||
clearable
|
||||
@keyup.enter="handleSearch"
|
||||
@clear="handleSearch"
|
||||
@@ -504,6 +548,19 @@ function listingOptionLabel(item: AvailableListing) {
|
||||
<el-option label="已确认" value="settled" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="创建时间">
|
||||
<el-date-picker
|
||||
v-model="filterTimeRange"
|
||||
type="datetimerange"
|
||||
range-separator="至"
|
||||
start-placeholder="开始时间"
|
||||
end-placeholder="结束时间"
|
||||
value-format="YYYY-MM-DD HH:mm:ss"
|
||||
:default-time="[new Date(2000, 1, 1, 0, 0, 0), new Date(2000, 1, 1, 23, 59, 59)]"
|
||||
style="width: 350px"
|
||||
@change="handleTimeRangeChange"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item class="pickup-filter-actions">
|
||||
<el-button type="primary" :icon="Search" @click="handleSearch">查询</el-button>
|
||||
<el-button @click="resetFilters">重置</el-button>
|
||||
@@ -526,6 +583,9 @@ function listingOptionLabel(item: AvailableListing) {
|
||||
<el-table-column label="卖家" width="140">
|
||||
<template #default="{ row }">{{ row.owner_phone }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="operator" label="操作人" width="105">
|
||||
<template #default="{ row }">{{ row.operator || '-' }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="账号来源" width="180">
|
||||
<template #default="{ row }">
|
||||
<div class="source-cell">
|
||||
@@ -539,9 +599,34 @@ function listingOptionLabel(item: AvailableListing) {
|
||||
<el-table-column label="交易渠道" width="110">
|
||||
<template #default="{ row }">{{ row.platform || '-' }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="第三方单号" min-width="150" show-overflow-tooltip>
|
||||
<template #default="{ row }">
|
||||
<span v-if="row.third_party_order_no">{{ row.third_party_order_no }}</span>
|
||||
<span v-else class="text-muted">-</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="店铺名称" min-width="130">
|
||||
<template #default="{ row }">{{ row.shop_name || '-' }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="下单金额" width="115">
|
||||
<template #default="{ row }">
|
||||
<span v-if="row.order_amount_cent > 0">{{
|
||||
formatCentWithSymbol(row.order_amount_cent)
|
||||
}}</span>
|
||||
<span v-else class="text-muted">-</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="押金金额" width="115">
|
||||
<template #default="{ row }">
|
||||
<span v-if="row.deposit_amount_cent > 0">{{
|
||||
formatCentWithSymbol(row.deposit_amount_cent)
|
||||
}}</span>
|
||||
<span v-else class="text-muted">-</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="线下利润" width="120">
|
||||
<template #default="{ row }">{{ formatCentWithSymbol(effectiveProfitCent(row)) }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="结算金额" width="130">
|
||||
<template #default="{ row }">
|
||||
<span v-if="row.status === 'completed'">{{
|
||||
@@ -550,9 +635,6 @@ function listingOptionLabel(item: AvailableListing) {
|
||||
<span v-else class="text-muted">待结算</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="线下利润" width="120">
|
||||
<template #default="{ row }">{{ formatCentWithSymbol(effectiveProfitCent(row)) }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="状态" width="100">
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="statusType(row.status)" effect="light">
|
||||
@@ -642,20 +724,32 @@ function listingOptionLabel(item: AvailableListing) {
|
||||
<el-dialog
|
||||
v-model="createDialogVisible"
|
||||
title="创建线下提号"
|
||||
width="min(640px, calc(100vw - 32px))"
|
||||
width="min(720px, calc(100vw - 32px))"
|
||||
>
|
||||
<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 label-width="95px" class="create-pickup-form">
|
||||
<!-- 第 1 行:账号来源 + 操作人(双列网格) -->
|
||||
<el-row :gutter="16">
|
||||
<el-col :xs="24" :sm="14">
|
||||
<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-col>
|
||||
<el-col :xs="24" :sm="10">
|
||||
<el-form-item label="操作人">
|
||||
<el-input v-model="createForm.operator" placeholder="经办人/客服姓名" clearable />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
|
||||
<!-- 第 2 行:选择账号(全宽) -->
|
||||
<el-form-item label="选择账号" required>
|
||||
<el-select
|
||||
v-model="createForm.listing_id"
|
||||
@@ -721,44 +815,105 @@ function listingOptionLabel(item: AvailableListing) {
|
||||
<span>售卖 {{ ratioText(selectedListing.buyer_ratio) }}</span>
|
||||
</div>
|
||||
</el-form-item>
|
||||
<el-form-item label="交易渠道">
|
||||
<el-input v-model="createForm.platform" placeholder="如 微信 / QQ / 闲鱼" />
|
||||
</el-form-item>
|
||||
|
||||
<!-- 第 3 行:第三方订单号 + 交易渠道(双列网格) -->
|
||||
<el-row :gutter="16">
|
||||
<el-col :xs="24" :sm="12">
|
||||
<el-form-item label="第三方单号">
|
||||
<el-input
|
||||
v-model="createForm.third_party_order_no"
|
||||
placeholder="淘宝/拼多多/外部订单号"
|
||||
clearable
|
||||
/>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :xs="24" :sm="12">
|
||||
<el-form-item label="交易渠道">
|
||||
<el-input
|
||||
v-model="createForm.platform"
|
||||
placeholder="如 淘宝 / 微信 / QQ / 闲鱼"
|
||||
clearable
|
||||
/>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
|
||||
<!-- 第 4 行:店铺名称(全宽,带快捷管理) -->
|
||||
<el-form-item label="店铺名称">
|
||||
<el-select
|
||||
v-model="createForm.shop_name"
|
||||
filterable
|
||||
allow-create
|
||||
default-first-option
|
||||
clearable
|
||||
:loading="shopOptionsLoading"
|
||||
placeholder="选择历史店铺或输入新店铺"
|
||||
style="width: 100%"
|
||||
>
|
||||
<el-option v-for="name in shopOptions" :key="name" :label="name" :value="name" />
|
||||
</el-select>
|
||||
<div class="form-hint">首次使用的店铺可直接输入名称创建</div>
|
||||
<div style="display: flex; gap: 8px; width: 100%">
|
||||
<el-select
|
||||
v-model="createForm.shop_name"
|
||||
filterable
|
||||
allow-create
|
||||
default-first-option
|
||||
clearable
|
||||
:loading="shopOptionsLoading"
|
||||
placeholder="选择配置店铺或输入新店铺"
|
||||
style="flex: 1"
|
||||
>
|
||||
<el-option v-for="name in shopOptions" :key="name" :label="name" :value="name" />
|
||||
</el-select>
|
||||
<el-button :icon="Setting" plain @click="shopMgrVisible = true">店铺管理</el-button>
|
||||
</div>
|
||||
<div class="form-hint">首次使用的店铺可直接输入名称创建,也可通过「店铺管理」预设</div>
|
||||
</el-form-item>
|
||||
<el-form-item label="线下利润(元)">
|
||||
<el-input-number
|
||||
v-model="createForm.profit_amount"
|
||||
:min="0"
|
||||
:step="1"
|
||||
:precision="2"
|
||||
controls-position="right"
|
||||
style="width: 100%"
|
||||
/>
|
||||
<div class="form-hint">电商平台实际成交利润,财务统计按该金额汇总</div>
|
||||
</el-form-item>
|
||||
<el-form-item label="备注">
|
||||
|
||||
<!-- 第 5 行:金额信息(三列并排卡片) -->
|
||||
<div class="amount-group-card">
|
||||
<div class="amount-group-title">金额核算</div>
|
||||
<el-row :gutter="16">
|
||||
<el-col :xs="24" :sm="8">
|
||||
<el-form-item label="下单金额(元)" class="amount-item">
|
||||
<el-input-number
|
||||
v-model="createForm.order_amount"
|
||||
:min="0"
|
||||
:step="1"
|
||||
:precision="2"
|
||||
controls-position="right"
|
||||
style="width: 100%"
|
||||
/>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :xs="24" :sm="8">
|
||||
<el-form-item label="押金金额(元)" class="amount-item">
|
||||
<el-input-number
|
||||
v-model="createForm.deposit_amount"
|
||||
:min="0"
|
||||
:step="1"
|
||||
:precision="2"
|
||||
controls-position="right"
|
||||
style="width: 100%"
|
||||
/>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :xs="24" :sm="8">
|
||||
<el-form-item label="线下利润(元)" class="amount-item">
|
||||
<el-input-number
|
||||
v-model="createForm.profit_amount"
|
||||
:min="0"
|
||||
:step="1"
|
||||
:precision="2"
|
||||
controls-position="right"
|
||||
style="width: 100%"
|
||||
/>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
<div class="form-hint" style="margin-top: -6px; margin-bottom: 4px">
|
||||
电商平台实际成交利润,财务统计按该金额汇总
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 第 6 行:备注说明(全宽) -->
|
||||
<el-form-item label="备注" style="margin-top: 14px">
|
||||
<el-input
|
||||
v-model="createForm.remark"
|
||||
type="textarea"
|
||||
:rows="3"
|
||||
:rows="2"
|
||||
placeholder="线下交易说明(可选)"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-alert type="info" :closable="false">
|
||||
<el-alert type="info" :closable="false" show-icon>
|
||||
创建后账号将被锁定为"提号中",卖家收到通知;不会立即结算,需手动完成结算。
|
||||
</el-alert>
|
||||
</el-form>
|
||||
@@ -768,6 +923,9 @@ function listingOptionLabel(item: AvailableListing) {
|
||||
</template>
|
||||
</el-dialog>
|
||||
|
||||
<!-- 店铺管理弹窗 -->
|
||||
<PickupShopDialog v-model="shopMgrVisible" @updated="loadShopOptions" />
|
||||
|
||||
<!-- 完成提号 -->
|
||||
<el-dialog v-model="completeDialogVisible" title="完成提号" width="480px">
|
||||
<el-form label-width="110px">
|
||||
|
||||
Reference in New Issue
Block a user