实现店铺联系电话精确查询与索引

This commit is contained in:
2026-07-22 15:06:06 +09:00
parent 751cb46079
commit 2fd11daaf0
14 changed files with 904 additions and 64 deletions

View File

@@ -22,8 +22,8 @@ type ShopHandler struct {
}
// NewShopHandler 创建店铺管理处理器。
func NewShopHandler(service *shopService.Service, validate *validator.Validate) *ShopHandler {
return &ShopHandler{service: service, validator: validate}
func NewShopHandler(service *shopService.Service, validator *validator.Validate) *ShopHandler {
return &ShopHandler{service: service, validator: validator}
}
// List 查询店铺列表。
@@ -73,7 +73,7 @@ func (h *ShopHandler) logListValidationFailure(c *fiber.Ctx, err error) {
func normalizeShopListPagination(req *dto.ShopListRequest) {
if req.Page == 0 {
req.Page = 1
req.Page = constants.DefaultPage
}
if req.PageSize == 0 {
req.PageSize = constants.DefaultPageSize

View File

@@ -1,13 +1,14 @@
package dto
type ShopListRequest struct {
Page int `json:"page" query:"page" validate:"omitempty,min=1" minimum:"1" description:"页码"`
PageSize int `json:"page_size" query:"page_size" validate:"omitempty,min=1,max=100" minimum:"1" maximum:"100" description:"每页数量"`
ShopName string `json:"shop_name" query:"shop_name" validate:"omitempty,max=100" maxLength:"100" description:"店铺名称模糊查询"`
ShopCode string `json:"shop_code" query:"shop_code" validate:"omitempty,max=50" maxLength:"50" description:"店铺编号模糊查询"`
ParentID *uint `json:"parent_id" query:"parent_id" validate:"omitempty,min=1" minimum:"1" description:"上级店铺ID"`
Level *int `json:"level" query:"level" validate:"omitempty,min=1,max=7" minimum:"1" maximum:"7" description:"店铺层级 (1-7级)"`
Status *int `json:"status" query:"status" validate:"omitempty,oneof=0 1" description:"状态 (0:禁用, 1:启用)"`
Page int `json:"page" query:"page" validate:"omitempty,min=1" minimum:"1" description:"页码"`
PageSize int `json:"page_size" query:"page_size" validate:"omitempty,min=1,max=100" minimum:"1" maximum:"100" description:"每页数量"`
ShopName string `json:"shop_name" query:"shop_name" validate:"omitempty,max=100" maxLength:"100" description:"店铺名称模糊查询"`
ShopCode string `json:"shop_code" query:"shop_code" validate:"omitempty,max=50" maxLength:"50" description:"店铺编号精确查询"`
ContactPhone string `json:"contact_phone" query:"contact_phone" validate:"omitempty,len=11,numeric,ascii" minLength:"11" maxLength:"11" pattern:"^[0-9]{11}$" description:"联系电话精确查询11位 ASCII 数字;空值不启用筛选;与其他条件按 AND 组合)"`
ParentID *uint `json:"parent_id" query:"parent_id" validate:"omitempty,min=1" minimum:"1" description:"上级店铺ID"`
Level *int `json:"level" query:"level" validate:"omitempty,min=1,max=7" minimum:"1" maximum:"7" description:"店铺层级 (1-7级)"`
Status *int `json:"status" query:"status" validate:"omitempty,oneof=0 1" description:"状态 (0:禁用, 1:启用)"`
}
type CreateShopRequest struct {

View File

@@ -17,7 +17,7 @@ func registerShopRoutes(router fiber.Router, handler *admin.ShopHandler, doc *op
Register(shops, doc, groupPath, "GET", "", coreShopManagement(handler.List), RouteSpec{
Summary: "店铺列表",
Description: "仅超级管理员、平台账号和代理账号可访问,企业账号返回 403。分页默认第 1 页、每页 20 条page 最小为 1page_size 范围为 1 至 100。所有筛选条件在查询前统一校验。",
Description: constants.ShopManagementAccessDescription + constants.ShopListPaginationDescription,
Tags: []string{"店铺管理"},
Input: new(dto.ShopListRequest),
Output: new(dto.ShopPageResult),
@@ -26,7 +26,7 @@ func registerShopRoutes(router fiber.Router, handler *admin.ShopHandler, doc *op
Register(shops, doc, groupPath, "POST", "", coreShopManagement(handler.Create), RouteSpec{
Summary: "创建店铺",
Description: "仅超级管理员、平台账号和代理账号可访问,企业账号返回 403。",
Description: constants.ShopManagementAccessDescription,
Tags: []string{"店铺管理"},
Input: new(dto.CreateShopRequest),
Output: new(dto.ShopResponse),
@@ -35,7 +35,7 @@ func registerShopRoutes(router fiber.Router, handler *admin.ShopHandler, doc *op
Register(shops, doc, groupPath, "PUT", "/:id", coreShopManagement(handler.Update), RouteSpec{
Summary: "更新店铺",
Description: "仅超级管理员、平台账号和代理账号可访问,企业账号返回 403。",
Description: constants.ShopManagementAccessDescription,
Tags: []string{"店铺管理"},
Input: new(dto.UpdateShopParams),
Output: new(dto.ShopResponse),
@@ -44,7 +44,7 @@ func registerShopRoutes(router fiber.Router, handler *admin.ShopHandler, doc *op
Register(shops, doc, groupPath, "DELETE", "/:id", coreShopManagement(handler.Delete), RouteSpec{
Summary: "删除店铺",
Description: "仅超级管理员、平台账号和代理账号可访问,企业账号返回 403。",
Description: constants.ShopManagementAccessDescription,
Tags: []string{"店铺管理"},
Input: new(dto.IDReq),
Output: nil,
@@ -53,7 +53,7 @@ func registerShopRoutes(router fiber.Router, handler *admin.ShopHandler, doc *op
Register(shops, doc, groupPath, "GET", "/cascade", coreShopManagement(handler.Cascade), RouteSpec{
Summary: "店铺联级查询",
Description: "仅超级管理员、平台账号和代理账号可访问,企业账号返回 403。",
Description: constants.ShopManagementAccessDescription,
Tags: []string{"店铺管理"},
Input: new(dto.ShopCascadeRequest),
Output: new([]dto.ShopCascadeItem),
@@ -64,7 +64,7 @@ func registerShopRoutes(router fiber.Router, handler *admin.ShopHandler, doc *op
func coreShopManagement(handler fiber.Handler) fiber.Handler {
return func(c *fiber.Ctx) error {
if middleware.GetUserTypeFromContext(c.UserContext()) == constants.UserTypeEnterprise {
return errors.New(errors.CodeForbidden, "无权限访问店铺管理功能")
return errors.New(errors.CodeForbidden, constants.ShopManagementForbiddenMessage)
}
return handler(c)
}

View File

@@ -2,11 +2,14 @@ package routes
import (
"context"
"fmt"
"io"
"net/http"
"net/url"
"os"
"strconv"
"strings"
"sync/atomic"
"testing"
"time"
@@ -20,7 +23,9 @@ import (
"github.com/break/junhong_cmp_fiber/internal/bootstrap"
"github.com/break/junhong_cmp_fiber/internal/handler/admin"
internalMiddleware "github.com/break/junhong_cmp_fiber/internal/middleware"
"github.com/break/junhong_cmp_fiber/internal/model"
shopService "github.com/break/junhong_cmp_fiber/internal/service/shop"
shopCommissionService "github.com/break/junhong_cmp_fiber/internal/service/shop_commission"
"github.com/break/junhong_cmp_fiber/internal/store/postgres"
"github.com/break/junhong_cmp_fiber/pkg/auth"
"github.com/break/junhong_cmp_fiber/pkg/config"
@@ -50,6 +55,7 @@ type shopTestEnv struct {
tokenManager *auth.TokenManager
}
// TestShopListValidatesAllQueryParameters 验证店铺列表会完整校验所有查询参数。
func TestShopListValidatesAllQueryParameters(t *testing.T) {
env := newShopTestEnv(t)
token := env.newToken(t, auth.TokenInfo{
@@ -100,6 +106,7 @@ func TestShopListValidatesAllQueryParameters(t *testing.T) {
}
}
// TestShopListUsesNormalizedAndExplicitPagination 验证默认分页归一化且显式分页保持不变。
func TestShopListUsesNormalizedAndExplicitPagination(t *testing.T) {
env := newShopTestEnv(t)
token := env.newToken(t, auth.TokenInfo{
@@ -114,7 +121,7 @@ func TestShopListUsesNormalizedAndExplicitPagination(t *testing.T) {
expectedPage int
expectedSize int
}{
{name: "默认分页", expectedPage: 1, expectedSize: constants.DefaultPageSize},
{name: "默认分页", expectedPage: constants.DefaultPage, expectedSize: constants.DefaultPageSize},
{name: "显式分页", query: "?page=2&page_size=7&shop_name=ur60-no-match", expectedPage: 2, expectedSize: 7},
}
@@ -136,6 +143,7 @@ func TestShopListUsesNormalizedAndExplicitPagination(t *testing.T) {
}
}
// TestShopListKeepsAuthenticationContract 验证店铺列表保持既有认证错误契约。
func TestShopListKeepsAuthenticationContract(t *testing.T) {
env := newShopTestEnv(t)
@@ -160,6 +168,7 @@ func TestShopListKeepsAuthenticationContract(t *testing.T) {
}
}
// TestEnterpriseCannotAccessCoreShopManagementRoutes 验证企业账号无法访问五个核心店铺管理入口。
func TestEnterpriseCannotAccessCoreShopManagementRoutes(t *testing.T) {
env := newShopTestEnv(t)
token := env.newToken(t, auth.TokenInfo{
@@ -195,7 +204,7 @@ func TestEnterpriseCannotAccessCoreShopManagementRoutes(t *testing.T) {
if err := sonic.Unmarshal(body, &response); err != nil {
t.Fatalf("解析响应失败:%v", err)
}
if response.Code != errors.CodeForbidden || response.Msg != "无权限访问店铺管理功能" || response.Data != nil {
if response.Code != errors.CodeForbidden || response.Msg != constants.ShopManagementForbiddenMessage || response.Data != nil {
t.Fatalf("企业账号禁止响应不符合契约:%s", body)
}
if !strings.Contains(string(body), "timestamp") {
@@ -205,6 +214,7 @@ func TestEnterpriseCannotAccessCoreShopManagementRoutes(t *testing.T) {
}
}
// TestCoreShopRestrictionDoesNotAffectOtherShopRoutes 验证核心店铺权限限制不影响其他店铺前缀路由。
func TestCoreShopRestrictionDoesNotAffectOtherShopRoutes(t *testing.T) {
env := newShopTestEnv(t)
token := env.newToken(t, auth.TokenInfo{
@@ -214,12 +224,22 @@ func TestCoreShopRestrictionDoesNotAffectOtherShopRoutes(t *testing.T) {
Username: "ur60-enterprise",
})
status, body := env.request(t, http.MethodGet, "/api/admin/shops/1/roles", token)
if status == http.StatusForbidden && strings.Contains(string(body), "无权限访问店铺管理功能") {
t.Fatalf("核心店铺管理拦截误伤店铺角色路由:%s", body)
paths := []string{
"/api/admin/shops/1/roles",
"/api/admin/shops/fund-summary",
"/api/admin/shops/1/withdrawal-requests",
"/api/admin/shops/1/commission-records",
"/api/admin/shops/1/main-wallet/transactions",
}
for _, path := range paths {
status, body := env.request(t, http.MethodGet, path, token)
if status == http.StatusForbidden && strings.Contains(string(body), constants.ShopManagementForbiddenMessage) {
t.Fatalf("核心店铺管理拦截误伤独立路由 %s%s", path, body)
}
}
}
// TestNonEnterpriseAccountsKeepCoreShopListAccess 验证非企业账号保留核心店铺列表访问能力。
func TestNonEnterpriseAccountsKeepCoreShopListAccess(t *testing.T) {
env := newShopTestEnv(t)
testCases := []struct {
@@ -248,6 +268,233 @@ func TestNonEnterpriseAccountsKeepCoreShopListAccess(t *testing.T) {
}
}
// TestShopListFiltersContactPhoneExactly 验证联系电话精确匹配、AND 组合、重复号码、排序及软删除语义。
func TestShopListFiltersContactPhoneExactly(t *testing.T) {
env := newShopTestEnv(t)
phone := "13800000000"
parent := env.createShop(t, "UR60父店", phone, nil, 1, time.Now().Add(-4*time.Minute))
newer := env.createShop(t, "UR60目标新店", phone, &parent.ID, 2, time.Now().Add(-time.Minute))
older := env.createShop(t, "UR60目标旧店", phone, &parent.ID, 2, time.Now().Add(-2*time.Minute))
env.createShop(t, "UR60相似号码", "13800000001", &parent.ID, 2, time.Now())
deleted := env.createShop(t, "UR60软删除店", phone, &parent.ID, 2, time.Now().Add(time.Minute))
if err := env.db.Delete(deleted).Error; err != nil {
t.Fatalf("软删除测试店铺失败:%v", err)
}
token := env.newToken(t, auth.TokenInfo{
UserID: uniqueTestID(),
UserType: constants.UserTypeSuperAdmin,
Username: "ur60-super-admin",
})
status, body := env.request(t, http.MethodGet, "/api/admin/shops?contact_phone="+phone+"&shop_name=UR60目标&parent_id="+strconv.FormatUint(uint64(parent.ID), 10)+"&level=2&status=1", token)
if status != http.StatusOK {
t.Fatalf("联系电话组合查询失败HTTP %d%s", status, body)
}
var response shopTestResponse
if err := sonic.Unmarshal(body, &response); err != nil {
t.Fatalf("解析联系电话查询响应失败:%v", err)
}
if response.Data.Total != 2 || len(response.Data.Items) != 2 {
t.Fatalf("联系电话组合查询应返回两条可见记录:%s", body)
}
if uint(response.Data.Items[0]["id"].(float64)) != newer.ID || uint(response.Data.Items[1]["id"].(float64)) != older.ID {
t.Fatalf("联系电话查询未保持 created_at DESC%s", body)
}
status, body = env.request(t, http.MethodGet, "/api/admin/shops?contact_phone=13800000001&shop_code="+newer.ShopCode, token)
if status != http.StatusOK {
t.Fatalf("联系电话精确性查询失败HTTP %d%s", status, body)
}
if err := sonic.Unmarshal(body, &response); err != nil {
t.Fatalf("解析联系电话精确性响应失败:%v", err)
}
if response.Data.Total != 0 {
t.Fatalf("联系电话必须与其他条件按 AND 精确匹配:%s", body)
}
platformToken := env.newToken(t, auth.TokenInfo{
UserID: uniqueTestID(),
UserType: constants.UserTypePlatform,
Username: "ur60-platform",
})
status, body = env.request(t, http.MethodGet, "/api/admin/shops?contact_phone="+phone+"&shop_name=UR60目标", platformToken)
if status != http.StatusOK {
t.Fatalf("平台账号联系电话查询失败HTTP %d%s", status, body)
}
if err := sonic.Unmarshal(body, &response); err != nil {
t.Fatalf("解析平台账号联系电话响应失败:%v", err)
}
if response.Data.Total != 2 {
t.Fatalf("平台账号联系电话查询结果不符合契约:%s", body)
}
}
// TestShopListContactPhoneKeepsEmptyAndPaginationSemantics 验证空电话、空结果和显式分页契约。
func TestShopListContactPhoneKeepsEmptyAndPaginationSemantics(t *testing.T) {
env := newShopTestEnv(t)
token := env.newToken(t, auth.TokenInfo{UserID: uniqueTestID(), UserType: constants.UserTypeSuperAdmin, Username: "ur60-super-admin"})
status, body := env.request(t, http.MethodGet, "/api/admin/shops?contact_phone=&page=2&page_size=3", token)
if status != http.StatusOK {
t.Fatalf("空联系电话不应改变列表语义HTTP %d%s", status, body)
}
var response shopTestResponse
if err := sonic.Unmarshal(body, &response); err != nil {
t.Fatalf("解析空联系电话响应失败:%v", err)
}
if response.Data.Page != 2 || response.Data.Size != 3 {
t.Fatalf("联系电话筛选不应重置显式分页:%s", body)
}
status, body = env.request(t, http.MethodGet, "/api/admin/shops?contact_phone=19999999999", token)
if status != http.StatusOK {
t.Fatalf("无匹配联系电话应返回成功空分页HTTP %d%s", status, body)
}
if err := sonic.Unmarshal(body, &response); err != nil {
t.Fatalf("解析空结果响应失败:%v", err)
}
if response.Data.Total != 0 || len(response.Data.Items) != 0 {
t.Fatalf("无匹配联系电话响应不符合空分页契约:%s", body)
}
}
// TestShopListRejectsInvalidContactPhoneBeforeQuery 验证非法联系电话在执行店铺查询前被拒绝。
func TestShopListRejectsInvalidContactPhoneBeforeQuery(t *testing.T) {
env := newShopTestEnv(t)
token := env.newToken(t, auth.TokenInfo{UserID: uniqueTestID(), UserType: constants.UserTypeSuperAdmin, Username: "ur60-super-admin"})
var shopQueryCount atomic.Int64
callbackName := fmt.Sprintf("ur60:count_shop_queries:%d", time.Now().UnixNano())
if err := env.db.Callback().Query().Before("gorm:query").Register(callbackName, func(db *gorm.DB) {
if db.Statement != nil && db.Statement.Table == (model.Shop{}).TableName() {
shopQueryCount.Add(1)
}
}); err != nil {
t.Fatalf("注册店铺查询计数回调失败:%v", err)
}
t.Cleanup(func() { _ = env.db.Callback().Query().Remove(callbackName) })
invalidPhones := []string{
"1380000000", "138000000000", "1380000000A", "+8613800000000",
"138-0000-0000", " 13800000000", "",
}
for _, phone := range invalidPhones {
shopQueryCount.Store(0)
status, body := env.request(t, http.MethodGet, "/api/admin/shops?contact_phone="+url.QueryEscape(phone), token)
if status != http.StatusBadRequest || shopQueryCount.Load() != 0 {
t.Fatalf("非法联系电话应在查询前返回 400phone=%q, queries=%d, body=%s", phone, shopQueryCount.Load(), body)
}
}
}
// TestAgentContactPhoneSearchKeepsShopScope 验证代理账号无法通过联系电话越过店铺层级范围。
func TestAgentContactPhoneSearchKeepsShopScope(t *testing.T) {
env := newShopTestEnv(t)
phone := "13700000000"
root := env.createShop(t, "UR60代理根店", phone, nil, 1, time.Now().Add(-3*time.Minute))
child := env.createShop(t, "UR60代理下级", phone, &root.ID, 2, time.Now().Add(-2*time.Minute))
env.createShop(t, "UR60范围外店铺", phone, nil, 1, time.Now().Add(-time.Minute))
token := env.newToken(t, auth.TokenInfo{UserID: uniqueTestID(), UserType: constants.UserTypeAgent, ShopID: root.ID, Username: "ur60-agent"})
status, body := env.request(t, http.MethodGet, "/api/admin/shops?contact_phone="+phone, token)
if status != http.StatusOK {
t.Fatalf("代理联系电话查询失败HTTP %d%s", status, body)
}
var response shopTestResponse
if err := sonic.Unmarshal(body, &response); err != nil {
t.Fatalf("解析代理联系电话响应失败:%v", err)
}
if response.Data.Total != 2 {
t.Fatalf("代理联系电话查询应只返回自身及下级:%s", body)
}
returned := map[uint]bool{}
for _, item := range response.Data.Items {
returned[uint(item["id"].(float64))] = true
}
if !returned[root.ID] || !returned[child.ID] {
t.Fatalf("代理联系电话查询缺少范围内店铺:%s", body)
}
}
// TestShopListDatabaseFailureIsSanitized 验证数据库错误通过统一 500 响应脱敏。
func TestShopListDatabaseFailureIsSanitized(t *testing.T) {
env := newShopTestEnv(t)
token := env.newToken(t, auth.TokenInfo{UserID: uniqueTestID(), UserType: constants.UserTypeSuperAdmin, Username: "ur60-super-admin"})
sqlDB, err := env.db.DB()
if err != nil {
t.Fatalf("获取测试数据库连接失败:%v", err)
}
if err := sqlDB.Close(); err != nil {
t.Fatalf("关闭测试数据库连接失败:%v", err)
}
status, body := env.request(t, http.MethodGet, "/api/admin/shops?contact_phone=13800000000", token)
if status != http.StatusInternalServerError {
t.Fatalf("数据库失败应返回 HTTP 500实际 %d%s", status, body)
}
var response struct {
Code int `json:"code"`
Msg string `json:"msg"`
Data any `json:"data"`
}
if err := sonic.Unmarshal(body, &response); err != nil {
t.Fatalf("解析数据库错误响应失败:%v", err)
}
if response.Code != errors.CodeInternalError || response.Msg != "内部服务器错误" || response.Data != nil {
t.Fatalf("数据库错误响应不符合脱敏契约:%s", body)
}
for _, leaked := range []string{"sql", "postgres", "host", "driver", "database is closed"} {
if strings.Contains(strings.ToLower(string(body)), leaked) {
t.Fatalf("数据库错误响应泄露底层信息 %q%s", leaked, body)
}
}
}
// TestShopContactPhoneIndexDefinition 验证联系电话索引的类型、唯一性和部分条件。
func TestShopContactPhoneIndexDefinition(t *testing.T) {
env := newShopTestEnv(t)
var indexCount int64
if err := env.db.Raw(`
SELECT COUNT(*)
FROM pg_class
WHERE relname = ?
`, "idx_shop_contact_phone").Scan(&indexCount).Error; err != nil {
t.Fatalf("查询联系电话索引数量失败:%v", err)
}
if os.Getenv("UR60_EXPECT_INDEX_ABSENT") == "1" {
if indexCount != 0 {
t.Fatalf("回滚后联系电话索引仍然存在")
}
return
}
if indexCount != 1 {
t.Fatalf("联系电话索引数量异常:%d", indexCount)
}
var index struct {
AccessMethod string `gorm:"column:access_method"`
IsUnique bool `gorm:"column:is_unique"`
Definition string `gorm:"column:definition"`
Predicate string `gorm:"column:predicate"`
}
err := env.db.Raw(`
SELECT am.amname AS access_method,
ix.indisunique AS is_unique,
pg_get_indexdef(ix.indexrelid) AS definition,
pg_get_expr(ix.indpred, ix.indrelid) AS predicate
FROM pg_index ix
JOIN pg_class i ON i.oid = ix.indexrelid
JOIN pg_am am ON am.oid = i.relam
WHERE i.relname = ?
`, "idx_shop_contact_phone").Scan(&index).Error
if err != nil {
t.Fatalf("查询联系电话索引定义失败:%v", err)
}
if index.AccessMethod != "btree" || index.IsUnique || !strings.Contains(index.Definition, "contact_phone") || !strings.Contains(index.Predicate, "deleted_at IS NULL") {
t.Fatalf("联系电话索引定义不符合契约:%+v", index)
}
}
func newShopTestEnv(t *testing.T) *shopTestEnv {
t.Helper()
if os.Getenv("JUNHONG_DATABASE_HOST") == "" || os.Getenv("JUNHONG_REDIS_ADDRESS") == "" {
@@ -286,6 +533,17 @@ func newShopTestEnv(t *testing.T) *shopTestEnv {
shopStore := postgres.NewShopStore(db, redisClient)
service := shopService.New(shopStore, nil, nil, nil, nil, nil)
shopCommission := shopCommissionService.New(
shopStore,
postgres.NewAccountStore(db, redisClient),
postgres.NewAgentWalletStore(db, redisClient),
postgres.NewCommissionWithdrawalRequestStore(db, redisClient),
postgres.NewCommissionWithdrawalSettingStore(db, redisClient),
postgres.NewCommissionRecordStore(db, redisClient),
postgres.NewAgentWalletTransactionStore(db, redisClient),
db,
logger,
)
tokenManager := auth.NewTokenManager(redisClient, cfg.JWT.AccessTokenTTL, cfg.JWT.RefreshTokenTTL)
authMiddleware := pkgMiddleware.Auth(pkgMiddleware.AuthConfig{
TokenValidator: func(token string) (*pkgMiddleware.UserContextInfo, error) {
@@ -310,13 +568,35 @@ func newShopTestEnv(t *testing.T) *shopTestEnv {
ErrorHandler: internalMiddleware.ErrorHandler(logger),
})
RegisterAdminRoutes(app.Group("/api/admin"), &bootstrap.Handlers{
Shop: admin.NewShopHandler(service, validator.New()),
ShopRole: admin.NewShopRoleHandler(service),
Shop: admin.NewShopHandler(service, validator.New()),
ShopRole: admin.NewShopRoleHandler(service),
ShopCommission: admin.NewShopCommissionHandler(shopCommission),
}, &bootstrap.Middlewares{AdminAuth: authMiddleware}, nil, "/api/admin")
return &shopTestEnv{app: app, db: db, redis: redisClient, tokenManager: tokenManager}
}
func (e *shopTestEnv) createShop(t *testing.T, name, phone string, parentID *uint, level int, createdAt time.Time) *model.Shop {
t.Helper()
shop := &model.Shop{
ShopName: name,
ShopCode: fmt.Sprintf("UR60-%d", time.Now().UnixNano()),
ParentID: parentID,
Level: level,
ContactPhone: phone,
Status: constants.ShopStatusEnabled,
}
shop.CreatedAt = createdAt
shop.UpdatedAt = createdAt
shop.Creator = uniqueTestID()
shop.Updater = shop.Creator
if err := e.db.Create(shop).Error; err != nil {
t.Fatalf("创建测试店铺失败:%v", err)
}
t.Cleanup(func() { _ = e.db.Unscoped().Delete(shop).Error })
return shop
}
func (e *shopTestEnv) newToken(t *testing.T, info auth.TokenInfo) string {
t.Helper()
accessToken, refreshToken, err := e.tokenManager.GenerateTokenPair(context.Background(), &info)

View File

@@ -323,6 +323,9 @@ func (s *Service) ListShopResponses(ctx context.Context, req *dto.ShopListReques
if req.ShopCode != "" {
filters["shop_code"] = req.ShopCode
}
if req.ContactPhone != "" {
filters["contact_phone"] = req.ContactPhone
}
if req.ParentID != nil {
filters["parent_id"] = *req.ParentID
}

View File

@@ -114,6 +114,9 @@ func (s *ShopStore) List(ctx context.Context, opts *store.QueryOptions, filters
if shopCode, ok := filters["shop_code"].(string); ok && shopCode != "" {
query = query.Where("shop_code = ?", shopCode)
}
if contactPhone, ok := filters["contact_phone"].(string); ok && contactPhone != "" {
query = query.Where("contact_phone = ?", contactPhone)
}
if parentID, ok := filters["parent_id"].(uint); ok {
query = query.Where("parent_id = ?", parentID)
}