feat: 新增店铺联级查询接口
All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 7m3s

- 新增 GET /api/admin/shops/cascade 接口,支持按店铺名模糊查询和上级ID过滤
- Store 层新增 ListForCascade(支持数据权限过滤)和 GetParentIDsWithChildren 方法
- Service 层新增 ListCascade,批量判断 has_children 避免 N+1 查询
- 返回格式:[{id, shop_name, has_children}]

💘 Generated with Crush

Assisted-by: Claude Sonnet 4.6 via Crush <crush@charm.land>
This commit is contained in:
2026-04-09 11:43:37 +08:00
parent 384a54164b
commit 81ba84cf05
6 changed files with 201 additions and 0 deletions

View File

@@ -6134,6 +6134,19 @@ components:
- card_no
- ssid
type: object
DtoShopCascadeItem:
properties:
has_children:
description: 是否有下级店铺
type: boolean
id:
description: 店铺ID
minimum: 0
type: integer
shop_name:
description: 店铺名称
type: string
type: object
DtoShopCommissionRecordItem:
properties:
amount:
@@ -20811,6 +20824,82 @@ paths:
summary: 代理商提现记录
tags:
- 代理商佣金管理
/api/admin/shops/cascade:
get:
parameters:
- description: 店铺名称(模糊查询)
in: query
name: shop_name
schema:
description: 店铺名称(模糊查询)
maxLength: 100
type: string
- description: 上级店铺ID不传则查询顶级店铺
in: query
name: parent_id
schema:
description: 上级店铺ID不传则查询顶级店铺
minimum: 1
nullable: true
type: integer
responses:
"200":
content:
application/json:
schema:
properties:
code:
description: 响应码
example: 0
type: integer
data:
items:
$ref: '#/components/schemas/DtoShopCascadeItem'
type: array
msg:
description: 响应消息
example: success
type: string
timestamp:
description: 时间戳
format: date-time
type: string
required:
- code
- msg
- data
- timestamp
type: object
description: 成功
"400":
content:
application/json:
schema:
$ref: '#/components/schemas/ErrorResponse'
description: 请求参数错误
"401":
content:
application/json:
schema:
$ref: '#/components/schemas/ErrorResponse'
description: 未认证或认证已过期
"403":
content:
application/json:
schema:
$ref: '#/components/schemas/ErrorResponse'
description: 无权访问
"500":
content:
application/json:
schema:
$ref: '#/components/schemas/ErrorResponse'
description: 服务器内部错误
security:
- BearerAuth: []
summary: 店铺联级查询
tags:
- 店铺管理
/api/admin/shops/commission-summary:
get:
parameters:

View File

@@ -78,3 +78,19 @@ func (h *ShopHandler) Delete(c *fiber.Ctx) error {
return response.Success(c, nil)
}
// Cascade 店铺联级查询
// GET /api/admin/shops/cascade
func (h *ShopHandler) Cascade(c *fiber.Ctx) error {
var req dto.ShopCascadeRequest
if err := c.QueryParser(&req); err != nil {
return errors.New(errors.CodeInvalidParam, "请求参数解析失败")
}
items, err := h.service.ListCascade(c.UserContext(), &req)
if err != nil {
return err
}
return response.Success(c, items)
}

View File

@@ -69,3 +69,16 @@ type UpdateShopParams struct {
IDReq
UpdateShopRequest
}
// ShopCascadeRequest 店铺联级查询请求
type ShopCascadeRequest struct {
ShopName string `json:"shop_name" query:"shop_name" validate:"omitempty,max=100" maxLength:"100" description:"店铺名称(模糊查询)"`
ParentID *uint `json:"parent_id" query:"parent_id" validate:"omitempty,min=1" minimum:"1" description:"上级店铺ID不传则查询顶级店铺"`
}
// ShopCascadeItem 联级查询结果项
type ShopCascadeItem struct {
ID uint `json:"id" description:"店铺ID"`
ShopName string `json:"shop_name" description:"店铺名称"`
HasChildren bool `json:"has_children" description:"是否有下级店铺"`
}

View File

@@ -43,6 +43,14 @@ func registerShopRoutes(router fiber.Router, handler *admin.ShopHandler, doc *op
Output: nil,
Auth: true,
})
Register(shops, doc, groupPath, "GET", "/cascade", handler.Cascade, RouteSpec{
Summary: "店铺联级查询",
Tags: []string{"店铺管理"},
Input: new(dto.ShopCascadeRequest),
Output: new([]dto.ShopCascadeItem),
Auth: true,
})
}
func registerShopRoleRoutes(router fiber.Router, handler *admin.ShopRoleHandler, doc *openapi.Generator, basePath string) {

View File

@@ -350,6 +350,44 @@ func (s *Service) List(ctx context.Context, opts *store.QueryOptions, filters ma
return s.shopStore.List(ctx, opts, filters)
}
// ListCascade 联级查询店铺,用于新增店铺或其他场景选择上级店铺
func (s *Service) ListCascade(ctx context.Context, req *dto.ShopCascadeRequest) ([]*dto.ShopCascadeItem, error) {
shops, err := s.shopStore.ListForCascade(ctx, req.ShopName, req.ParentID)
if err != nil {
return nil, errors.Wrap(errors.CodeInternalError, err, "查询店铺失败")
}
if len(shops) == 0 {
return []*dto.ShopCascadeItem{}, nil
}
ids := make([]uint, 0, len(shops))
for _, shop := range shops {
ids = append(ids, shop.ID)
}
childParentIDs, err := s.shopStore.GetParentIDsWithChildren(ctx, ids)
if err != nil {
return nil, errors.Wrap(errors.CodeInternalError, err, "查询下级店铺失败")
}
hasChildrenSet := make(map[uint]bool, len(childParentIDs))
for _, pid := range childParentIDs {
hasChildrenSet[pid] = true
}
result := make([]*dto.ShopCascadeItem, 0, len(shops))
for _, shop := range shops {
result = append(result, &dto.ShopCascadeItem{
ID: shop.ID,
ShopName: shop.ShopName,
HasChildren: hasChildrenSet[shop.ID],
})
}
return result, nil
}
func (s *Service) Delete(ctx context.Context, id uint) error {
currentUserID := middleware.GetUserIDFromContext(ctx)
if currentUserID == 0 {

View File

@@ -228,3 +228,40 @@ func (s *ShopStore) GetByIDs(ctx context.Context, ids []uint) ([]*model.Shop, er
}
return shops, nil
}
// ListForCascade 联级查询店铺支持按名称模糊查询和按上级ID过滤
func (s *ShopStore) ListForCascade(ctx context.Context, shopName string, parentID *uint) ([]*model.Shop, error) {
query := s.db.WithContext(ctx).Model(&model.Shop{})
query = middleware.ApplyShopIDFilter(ctx, query)
if parentID != nil {
query = query.Where("parent_id = ?", *parentID)
} else {
query = query.Where("parent_id IS NULL")
}
if shopName != "" {
query = query.Where("shop_name LIKE ?", "%"+shopName+"%")
}
var shops []*model.Shop
if err := query.Order("created_at DESC").Find(&shops).Error; err != nil {
return nil, err
}
return shops, nil
}
// GetParentIDsWithChildren 查询指定 ID 列表中哪些店铺有下级返回作为上级的店铺ID集合
func (s *ShopStore) GetParentIDsWithChildren(ctx context.Context, ids []uint) ([]uint, error) {
if len(ids) == 0 {
return []uint{}, nil
}
var parentIDs []uint
if err := s.db.WithContext(ctx).Model(&model.Shop{}).
Distinct("parent_id").
Where("parent_id IN ?", ids).
Pluck("parent_id", &parentIDs).Error; err != nil {
return nil, err
}
return parentIDs, nil
}