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