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, }) }