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:
yml2213
2026-10-09 12:46:15 +08:00
parent 6b74e07e70
commit 9d9adfbd5e
16 changed files with 1571 additions and 75 deletions
+270 -19
View File
@@ -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,