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
+4
View File
@@ -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"`
+19
View File
@@ -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"
}
+55 -5
View File
@@ -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"),
}
}
+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,
@@ -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)
}
+4
View File
@@ -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;