Some checks failed
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Failing after 1h43m42s
- 迁移 000221:新增 tb_business_user_group、tb_business_user_group_member、tb_shop_business_owner_import_task,成员一账号一行由部分唯一索引保证,店铺所属组按当前负责人实时推导,不回填历史分组。 - 用户组 CRUD、成员改组/清空归属、店铺批量交接(原子失败不部分写入)。 - 店铺负责人 CSV 导入任务:逐行独立事务、逐行明细、任务级与行级失败分离。 - 读侧推导与筛选:未分组、业务线、停用组可筛出并带停用标记。 - 补齐操作审计动作与资源、openapi 清单、发布门禁巡检表清单。 - 归档 add-shop-salesperson-groups 变更并同步 openspec/specs/business-user-group,补齐 AUG26-003 验证证据链。
159 lines
8.8 KiB
Go
159 lines
8.8 KiB
Go
package routes
|
||
|
||
import (
|
||
"github.com/gofiber/fiber/v2"
|
||
|
||
"github.com/break/junhong_cmp_fiber/internal/handler/admin"
|
||
"github.com/break/junhong_cmp_fiber/internal/model/dto"
|
||
"github.com/break/junhong_cmp_fiber/pkg/constants"
|
||
"github.com/break/junhong_cmp_fiber/pkg/openapi"
|
||
)
|
||
|
||
// shopOwnerImportDoc 说明店铺负责人导入 CSV 的模板要求。
|
||
// 模板由前端提供,后端只描述固定列序、取值与编码要求,不提供模板下载端点与模板资源。
|
||
const shopOwnerImportDoc = `仅超级管理员和平台账号可操作,代理与企业账号返回 403。
|
||
|
||
### 完整导入流程
|
||
|
||
1. **获取上传 URL**:调用 ` + "`POST /api/admin/storage/upload-url`" + `,purpose 传 ` + "`shop_import`" + `
|
||
2. **上传 CSV**:使用返回的预签名 URL 上传文件到对象存储
|
||
3. **调用本接口**:使用返回的 ` + "`file_key`" + ` 创建导入任务(必须以 ` + "`shop-imports/`" + ` 开头且扩展名为 ` + "`.csv`" + `)
|
||
|
||
### 模板列序(首行表头必须完全一致)
|
||
|
||
` + "`店铺编码`" + `、` + "`操作类型`" + `、` + "`业务员登录账号`" + `、` + "`备注`" + `
|
||
|
||
- 店铺编码:以店铺编号唯一定位店铺;不存在或已删除时该行失败
|
||
- 操作类型:取值仅 ` + "`换绑`" + ` 与 ` + "`清空`" + `;换绑必须填写业务员登录账号,清空不得填写
|
||
- 业务员登录账号:以登录账号唯一定位业务员;必须是启用平台用户
|
||
- 备注:可选,填写时写入该行审计
|
||
|
||
### 编码要求
|
||
|
||
文件编码为 UTF-8,可带 BOM;非 UTF-8 时按 GBK 尝试解码,仍失败时按任务级失败并给出明确原因。
|
||
|
||
### 执行语义
|
||
|
||
每行独立校验与执行:有效行成功更新,失败行保留原值且不影响其他已成功行。结果返回行号(自数据首行起计,表头不计入)、成功或失败状态与失败原因。表头或编码不符为任务级失败,不产生行明细。不设行数硬上限。`
|
||
|
||
// registerBusinessUserGroupRoutes 注册业务用户组、成员维护与店铺负责人批量交接路由。
|
||
// 入口只读认证上下文判断身份,代理与企业一律 403,既有单店行为不变。
|
||
func registerBusinessUserGroupRoutes(router fiber.Router, handler *admin.BusinessUserGroupHandler, doc *openapi.Generator, basePath string) {
|
||
groups := router.Group("/business-user-groups")
|
||
groupPath := basePath + "/business-user-groups"
|
||
|
||
Register(groups, doc, groupPath, "POST", "", handler.Create, RouteSpec{
|
||
Summary: "创建业务用户组",
|
||
Description: "仅超级管理员和平台账号可操作。稳定编码创建时必填、未删除组内唯一且创建后不可修改;用户组不设上级、层级与组管理员,不改变角色权限与数据范围。",
|
||
Tags: []string{"业务用户组"},
|
||
Input: new(dto.CreateBusinessUserGroupRequest),
|
||
Output: new(dto.BusinessUserGroupResponse),
|
||
Auth: true,
|
||
})
|
||
|
||
Register(groups, doc, groupPath, "GET", "", handler.List, RouteSpec{
|
||
Summary: "查询业务用户组列表",
|
||
Description: "仅超级管理员和平台账号可操作。分页默认第 1 页、每页 20 条,最大 100;返回组字段与业务线,供维护与筛选下拉使用。",
|
||
Tags: []string{"业务用户组"},
|
||
Input: new(dto.BusinessUserGroupListRequest),
|
||
Output: new(dto.BusinessUserGroupPageResult),
|
||
Auth: true,
|
||
})
|
||
|
||
// 批量清空归属使用静态路径 /members,必须先于动态 /:id 注册,否则会被 :id 吞掉。
|
||
Register(groups, doc, groupPath, "DELETE", "/members", handler.ClearMembers, RouteSpec{
|
||
Summary: "批量清空平台用户业务用户组归属",
|
||
Description: "仅超级管理员和平台账号可操作。与批量设置使用同一校验:所有账号必须是启用平台用户,任一账号无效则全量回滚。清空后账号回到未分组。",
|
||
Tags: []string{"业务用户组"},
|
||
Input: new(dto.ClearBusinessUserGroupMembersParams),
|
||
Output: new(dto.BusinessUserGroupMembersResult),
|
||
Auth: true,
|
||
})
|
||
|
||
Register(groups, doc, groupPath, "GET", "/:id", handler.Detail, RouteSpec{
|
||
Summary: "查询业务用户组详情",
|
||
Description: "仅超级管理员和平台账号可操作。返回组字段含业务线,供维护与筛选下拉使用;不存在或已删除返回资源不存在。",
|
||
Tags: []string{"业务用户组"},
|
||
Input: new(dto.IDReq),
|
||
Output: new(dto.BusinessUserGroupResponse),
|
||
Auth: true,
|
||
})
|
||
|
||
Register(groups, doc, groupPath, "PUT", "/:id", handler.Update, RouteSpec{
|
||
Summary: "更新业务用户组",
|
||
Description: "仅超级管理员和平台账号可操作。允许更新名称、业务线、排序、启停与备注;稳定编码永不允许修改;不存在或已删除返回资源不存在。",
|
||
Tags: []string{"业务用户组"},
|
||
Input: new(dto.BusinessUserGroupUpdateParams),
|
||
Output: new(dto.BusinessUserGroupResponse),
|
||
Auth: true,
|
||
})
|
||
|
||
Register(groups, doc, groupPath, "DELETE", "/:id", handler.Delete, RouteSpec{
|
||
Summary: "删除业务用户组",
|
||
Description: "仅超级管理员和平台账号可操作,必须提交二次确认。仅无成员的用户组可删除;存在成员时返回“用户组仍有成员,只能停用或先移走成员”,不做物理删除。",
|
||
Tags: []string{"业务用户组"},
|
||
Input: new(dto.IDReq),
|
||
Body: new(dto.BusinessUserGroupDeleteRequest),
|
||
Output: nil,
|
||
Auth: true,
|
||
})
|
||
|
||
Register(groups, doc, groupPath, "PUT", "/:id/members", handler.SetMembers, RouteSpec{
|
||
Summary: "批量设置平台用户业务用户组归属",
|
||
Description: "仅超级管理员和平台账号可操作。请求为账号ID数组,所有账号必须是启用平台用户且目标组启用;事务内直接替换每个账号原归属,任一账号无效则全量回滚。停用组不得作为目标,已有成员关系保留并显示已停用。",
|
||
Tags: []string{"业务用户组"},
|
||
Input: new(dto.BusinessUserGroupMemberRequest),
|
||
Body: new(dto.SetBusinessUserGroupMembersRequest),
|
||
Output: new(dto.BusinessUserGroupMembersResult),
|
||
Auth: true,
|
||
})
|
||
}
|
||
|
||
// registerShopBusinessOwnerBatchRoute 注册勾选店铺批量设置或清空负责人路由。
|
||
func registerShopBusinessOwnerBatchRoute(router fiber.Router, handler *admin.BusinessUserGroupHandler, doc *openapi.Generator, basePath string) {
|
||
shops := router.Group("/shops")
|
||
groupPath := basePath + "/shops"
|
||
|
||
Register(shops, doc, groupPath, "PUT", "/business-owner/batch", handler.BatchUpdateShopBusinessOwner, RouteSpec{
|
||
Summary: "批量设置或清空店铺负责人",
|
||
Description: "仅超级管理员和平台账号可操作。提交前校验全部目标店铺均存在、未删除且可管理,并校验目标业务员为启用平台用户;任一项失败时整批不修改,失败文案不区分无权、不存在与已删除。business_owner_account_id 缺失时请求非法,显式 null 表示清空负责人。成功时在同一事务内统一更新并为每家店铺记录负责人前后值、操作者、时间与入口审计,同时保留批次汇总结果。",
|
||
Tags: []string{"店铺管理"},
|
||
Input: new(dto.BatchUpdateShopBusinessOwnerRequest),
|
||
Output: new(dto.BatchUpdateShopBusinessOwnerResult),
|
||
Auth: true,
|
||
})
|
||
}
|
||
|
||
// registerShopBusinessOwnerImportRoutes 注册店铺负责人 CSV 导入任务路由。
|
||
func registerShopBusinessOwnerImportRoutes(router fiber.Router, handler *admin.ShopBusinessOwnerImportHandler, doc *openapi.Generator, basePath string) {
|
||
shops := router.Group("/shops")
|
||
groupPath := basePath + "/shops"
|
||
|
||
Register(shops, doc, groupPath, "POST", "/business-owner-imports", handler.Create, RouteSpec{
|
||
Summary: "创建店铺负责人 CSV 导入任务",
|
||
Description: shopOwnerImportDoc,
|
||
Tags: []string{"店铺负责人导入"},
|
||
Input: new(dto.CreateShopBusinessOwnerImportRequest),
|
||
Output: new(dto.ShopBusinessOwnerImportTaskResponse),
|
||
Auth: true,
|
||
})
|
||
|
||
Register(shops, doc, groupPath, "GET", "/business-owner-imports", handler.List, RouteSpec{
|
||
Summary: "查询店铺负责人导入任务列表",
|
||
Description: constants.ShopOwnerImportAccessDescription,
|
||
Tags: []string{"店铺负责人导入"},
|
||
Input: new(dto.ListShopBusinessOwnerImportRequest),
|
||
Output: new(dto.ShopBusinessOwnerImportTaskPageResult),
|
||
Auth: true,
|
||
})
|
||
|
||
Register(shops, doc, groupPath, "GET", "/business-owner-imports/:id", handler.Detail, RouteSpec{
|
||
Summary: "查询店铺负责人导入任务详情",
|
||
Description: constants.ShopOwnerImportAccessDescription + "返回成功数、失败数与逐行失败原因,并单独返回任务级错误原因。",
|
||
Tags: []string{"店铺负责人导入"},
|
||
Input: new(dto.GetShopBusinessOwnerImportRequest),
|
||
Output: new(dto.ShopBusinessOwnerImportTaskDetailResponse),
|
||
Auth: true,
|
||
})
|
||
}
|