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:
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user