# 5-17 接口优化 本文档说明 **2026-05-17** 对美国版对外接口的优化,包括: 1. **`/api/app/rbac-role`**:`accessPermissions`、`menuIds`、`orderNum` 等(见 [rbac-role 角色管理](#rbac-role-角色管理))。 2. **`/api/app/partner`**、**`/api/app/group`**:列表与 **export-pdf** 按登录 Token **数据范围**过滤(见 [partner](#partner-合作伙伴列表权限) / [group](#group-region-列表与-pdf-导出权限) 章节)。 3. **`/api/app/location`**:门店新增 / 编辑 / 列表增加 **`operatingHours`**(经营时间),见 [location 经营时间](#location-门店经营时间-operatinghours)。 4. **`/api/app/product`**:产品列表 / 导出增加 **公司 / 组织 / 门店** 筛选,见 [product 列表筛选](#product-产品列表按公司组织门店筛选)。 5. **`/api/app/product-category`**:列表出参增加 **`region`**、**`location`** 展示字段,见 [product-category 列表](#product-category-列表-regionlocation-展示)。 6. **`/api/app/team-member`**:成员新增 / 编辑 / 详情 / 列表增加 **Company**、**Region** 绑定字段,见 [team-member 成员](#team-member-成员-companyregion-绑定)。 7. **`/api/app/label`**:标签列表 **Region** 筛选;新增/编辑 **Company、Region** 绑定,见 [label](#label-标签)。 8. **`/api/app/label-category`**:见 [label-category](#label-category-标签分类-regionlocation)。 单条角色菜单的独立维护仍可使用 **`/api/app/rbac-role-menu`**(见 [关联接口](#关联接口))。 --- ## 目录 | 章节 | 内容 | |------|------| | [变更摘要](#变更摘要) | 本次优化点一览 | | [公共约定](#公共约定) | 基址、鉴权、响应包装 | | [数据模型](#数据模型) | 入参 / 出参字段 | | [accessPermissions 规则](#accesspermissions-规则) | 读写语义(重要) | | [1 角色分页列表](#1-角色分页列表) | `GET /api/app/rbac-role` | | [2 角色详情](#2-角色详情) | `GET /api/app/rbac-role/{id}` | | [3 新增角色](#3-新增角色) | `POST /api/app/rbac-role` | | [4 编辑角色](#4-编辑角色) | `PUT /api/app/rbac-role/{id}` | | [5 删除角色](#5-删除角色) | `DELETE /api/app/rbac-role` | | [与 /api/app/role 的差异](#与-apiapprole-的差异) | 平台内置 role 接口对比 | | [partner 合作伙伴列表权限](#partner-合作伙伴列表权限) | `GET /api/app/partner` 按 Token 过滤 | | [partner 新增与编辑地址字段](#partner-新增与编辑地址字段) | `POST` / `PUT` 增加 street 等 | | [group Region PDF 导出权限](#group-region-列表与-pdf-导出权限) | `GET /api/app/group/export-pdf` | | [location 经营时间](#location-门店经营时间-operatinghours) | `operatingHours` 字段 | | [product 列表筛选](#product-产品列表按公司组织门店筛选) | `partnerId` / `groupId` / `locationId` | | [product-category 列表](#product-category-列表-regionlocation-展示) | `region` / `location` 出参 | | [team-member 成员](#team-member-成员-companyregion-绑定) | Company / Region 绑定 | | [label](#label-标签) | 列表筛选 + 新增/编辑 Company·Region | | [label-category](#label-category-标签分类-regionlocation) | Region/Location 绑定与筛选 | | [附录 curl 示例](#附录-curl-示例) | 登录与调用示例 | --- ## rbac-role 角色管理 ### 变更摘要 | 项 | 说明 | |----|------| | **accessPermissions 出参** | 列表、详情、新增返回、编辑返回均包含 `accessPermissions`:由该角色已绑定菜单的 **`PermissionCode`** 去重后,按字母序用 **`, `**(英文逗号+空格)拼接。 | | **accessPermissions 入参** | 新增 / 编辑 body 可传 `accessPermissions`(英文逗号分隔的 PermissionCode),用于绑定角色菜单;逻辑见 [accessPermissions 规则](#accesspermissions-规则)。 | | **menuIds 入参** | 新增 / 编辑 body 可传 `menuIds`(菜单 Guid 数组);与 `accessPermissions` 同时传时 **以 `menuIds` 为准**。 | | **orderNum** | 类型为 **`int?`**:新增不传或 `null` 时默认为 **0**;编辑不传或 `null` 时 **保留原排序号**。 | | **事务** | 新增、编辑在写入角色后会同步角色-菜单绑定(若入参指定了 `menuIds` 或 `accessPermissions`),与删除角色同属可回滚单元。 | --- ## 公共约定 - **宿主**:美国版后端 `Yi.Abp.Web`;本地示例:`http://localhost:19001`。 - **路由前缀**:`api/app`。 - **Swagger 分组**:「食品标签-美国版接口」。 - **应用服务**:`RbacRoleAppService`(模块 `food-labeling-us`)。 - **鉴权**:与其它业务接口相同,请求头:`Authorization: {登录返回的 data.token}`(含 `Bearer ` 前缀)。 - **Content-Type**:JSON 接口使用 `application/json`;字段名一般为 **camelCase**(如 `accessPermissions`、`menuIds`)。 统一响应包装(与其它 `api/app` 接口一致)示例: ```json { "statusCode": 200, "succeeded": true, "data": { }, "errors": null, "extras": null, "timestamp": 1710000000000 } ``` 失败时 `succeeded` 为 `false`,`errors` 或 `error` 中为业务提示(如「角色名称或编码已存在」)。 --- ## 数据模型 ### 查询入参 `RbacRoleGetListInputVo` | 字段 | 类型 | 必填 | 说明 | |------|------|------|------| | skipCount | int | 是 | 跳过条数(分页;与项目其它列表约定一致) | | maxResultCount | int | 是 | 每页条数 | | sorting | string | 否 | 排序,如 `orderNum desc` | | roleName | string | 否 | 角色名称,模糊 | | roleCode | string | 否 | 角色编码,模糊 | | state | bool | 否 | 启用状态 | ### 新增 / 编辑入参 `RbacRoleCreateInputVo`(编辑 `RbacRoleUpdateInputVo` 与其相同) | 字段 | 类型 | 必填 | 说明 | |------|------|------|------| | roleName | string | 是 | 角色名称 | | roleCode | string | 是 | 角色编码,唯一(与名称不可与未删角色重复) | | remark | string | 否 | 备注 | | dataScope | int | 否 | 数据范围,默认 `0`(与内置 RBAC `DataScope` 枚举一致) | | state | bool | 否 | 是否启用,默认 `true` | | orderNum | int? | 否 | 排序号;见 [变更摘要](#变更摘要) | | menuIds | Guid[] | 否 | 要绑定的菜单 Id;见 [accessPermissions 规则](#accesspermissions-规则) | | accessPermissions | string | 否 | PermissionCode 列表(英文逗号分隔);见 [accessPermissions 规则](#accesspermissions-规则) | ### 列表项 / 详情基础 `RbacRoleGetListOutputDto` | 字段 | 类型 | 说明 | |------|------|------| | id | Guid | 角色主键 | | roleName | string | 角色名称 | | roleCode | string | 角色编码 | | remark | string? | 备注 | | dataScope | int | 数据范围 | | state | bool | 是否启用 | | orderNum | int | 排序号 | | **accessPermissions** | string | 已绑定菜单的 PermissionCode 汇总;无绑定时为空字符串 `""` | ### 详情扩展 `RbacRoleGetOutputDto` 继承列表字段,并增加: | 字段 | 类型 | 说明 | |------|------|------| | menuIds | string[] | 已分配菜单 Id(字符串形式的 Guid) | ### 分页列表包装 `PagedResultWithPageDto` | 字段 | 类型 | 说明 | |------|------|------| | pageIndex | int | 当前页码(从 1 开始) | | pageSize | int | 每页条数 | | totalCount | int | 总记录数 | | totalPages | int | 总页数 | | items | T[] | 当前页数据 | --- ## accessPermissions 规则 ### 出参(只读汇总) - 来源:表 **`RoleMenu`** + **`Menu`**,取每条绑定菜单的 **`PermissionCode`**(非空)。 - 格式:去重后按字母序拼接,例如:`"product.view, product.edit, report.print"`。 - 与 **`GET /api/app/role`** 列表/详情中的 `accessPermissions` 含义一致,便于 People / Account 页与平台角色展示对齐。 ### 入参(写入时绑定菜单) | 场景 | menuIds | accessPermissions | 行为 | |------|---------|-------------------|------| | **新增** | 传数组 | 不传 | 按 `menuIds` 绑定菜单(仅保留未删除且存在的菜单) | | **新增** | 不传 | 传非空字符串 | 按 PermissionCode 解析菜单并绑定 | | **新增** | 不传 | 传 `""` | 不绑定任何菜单 | | **新增** | 传数组 | 同时传 | **以 menuIds 为准**,忽略 accessPermissions | | **编辑** | 传数组 | 任意 | 覆盖式更新为该 `menuIds` 集合 | | **编辑** | 不传 | 传非空字符串 | 按 PermissionCode 覆盖绑定 | | **编辑** | 不传 | 传 `""` | **清空**该角色全部菜单绑定 | | **编辑** | 不传 | 不传 / null | **不修改**已有菜单绑定,仅更新角色基础字段 | 说明: - `accessPermissions` 中的 Code 必须在 **`Menu.PermissionCode`** 中存在且菜单未删除,否则该 Code 被忽略(不会报错,但不会绑定对应菜单)。 - 绑定方式为 **覆盖式**:每次按入参指定的菜单集合替换原 `RoleMenu` 关系(与 `rbac-role-menu/set` 一致)。 --- ## 1 角色分页列表 | 项目 | 说明 | |------|------| | HTTP | `GET` | | 路径 | `/api/app/rbac-role` | | Query | `skipCount`、`maxResultCount`、`sorting`、`roleName`、`roleCode`、`state` | | 默认排序 | 未传 `sorting` 时按 `orderNum` 降序 | **响应 `data`**:`PagedResultWithPageDto`,每项含 **`accessPermissions`**。 --- ## 2 角色详情 | 项目 | 说明 | |------|------| | HTTP | `GET` | | 路径 | `/api/app/rbac-role/{id}` | | 路径参数 | `id`:角色 Guid | **响应 `data`**:`RbacRoleGetOutputDto`(含 `menuIds`、`accessPermissions`)。 --- ## 3 新增角色 | 项目 | 说明 | |------|------| | HTTP | `POST` | | 路径 | `/api/app/rbac-role` | | Body | `RbacRoleCreateInputVo` | **请求示例(仅基础字段)** ```json { "roleName": "Store Manager", "roleCode": "store_manager", "remark": "门店管理员", "dataScope": 0, "state": true, "orderNum": 10 } ``` **请求示例(同时用 menuIds 绑定权限)** ```json { "roleName": "Store Manager", "roleCode": "store_manager", "state": true, "menuIds": [ "33333333-3333-3333-3333-333333333301", "33333333-3333-3333-3333-333333333302" ] } ``` **请求示例(用 accessPermissions 绑定权限)** ```json { "roleName": "Viewer", "roleCode": "viewer", "state": true, "accessPermissions": "product.view, report.print" } ``` **响应 `data`**:`RbacRoleGetOutputDto`(含写入后的 `menuIds`、`accessPermissions`)。 **常见错误** | 提示 | 原因 | |------|------| | 角色名称不能为空 | `roleName` 为空 | | 角色编码不能为空 | `roleCode` 为空 | | 角色名称或编码已存在 | 与未删除角色重复 | --- ## 4 编辑角色 | 项目 | 说明 | |------|------| | HTTP | `PUT` | | 路径 | `/api/app/rbac-role/{id}` | | 路径参数 | `id`:角色 Guid | | Body | `RbacRoleUpdateInputVo`(字段同新增) | **请求示例(只改名称,不动菜单)** ```json { "roleName": "Store Manager (Updated)", "roleCode": "store_manager", "state": true } ``` **请求示例(清空全部菜单权限)** ```json { "roleName": "Store Manager", "roleCode": "store_manager", "state": true, "accessPermissions": "" } ``` **响应 `data`**:`RbacRoleGetOutputDto`。 --- ## 5 删除角色 | 项目 | 说明 | |------|------| | HTTP | `DELETE` | | 路径 | `/api/app/rbac-role` | | Body | `Guid[]`,要删除的角色 Id 列表(可批量) | 删除时会同步清理该角色的 **RoleMenu**、**RoleDept**、**UserRole** 关联,角色表为 **软删除**。 --- ## 与 /api/app/role 的差异 | 对比项 | `/api/app/rbac-role`(美国版对外) | `/api/app/role`(内置 RBAC) | |--------|-----------------------------------|------------------------------| | 用途 | People / 美国版 Web 角色管理 | 平台权限体系内置角色 | | accessPermissions 出参 | 有(本次优化) | 有 | | 新增/编辑传 menuIds | 支持 | `RoleCreateInputVo` 支持 `menuIds` | | 新增/编辑传 accessPermissions | 支持(按 Code 解析) | 入参无此字段,仅出参汇总 | | 详情 menuIds | `string[]` | 结构以 Swagger 为准 | | orderNum 可选 | `int?`,编辑可省略保留原值 | 以内置 DTO 为准 | 前端 **People → Roles** 页应优先调用 **`/api/app/rbac-role`**;若仍调用 `/api/app/role`,字段命名需单独对齐。 --- ## 关联接口 | 接口 | 说明 | |------|------| | `POST /api/app/rbac-role-menu/set` | 单独为角色设置菜单(body:`roleId` + `menuIds`),覆盖式 | | `GET /api/app/rbac-role-menu/menu-ids?roleId={guid}` | 查询角色已绑定的 menuId 列表 | | `DELETE /api/app/rbac-role-menu` | 按 roleId + menuIds 移除部分绑定 | 若在新增/编辑角色时已传 `menuIds` 或 `accessPermissions`,一般无需再调 `set`;仅在「只改菜单、不改角色字段」时使用 `rbac-role-menu`。 --- ## 附录 curl 示例 以下 `BASE`、`TOKEN` 请替换为实际环境。 ```bash BASE=http://localhost:19001 TOKEN="Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..." ``` ### 登录(获取 Token) ```bash curl -s -X POST "%BASE%/api/app/account/login" \ -H "Content-Type: application/json" \ -d "{\"userName\":\"admin@example.com\",\"password\":\"your_password\"}" ``` 从返回的 `data.token` 填入 `TOKEN`。 ### 分页列表 ```bash curl -s -G "%BASE%/api/app/rbac-role" \ -H "Authorization: %TOKEN%" \ --data-urlencode "skipCount=0" \ --data-urlencode "maxResultCount=20" \ --data-urlencode "sorting=orderNum desc" ``` ### 详情 ```bash curl -s "%BASE%/api/app/rbac-role/{roleId}" \ -H "Authorization: %TOKEN%" ``` ### 新增(含 accessPermissions) ```bash curl -s -X POST "%BASE%/api/app/rbac-role" \ -H "Authorization: %TOKEN%" \ -H "Content-Type: application/json" \ -d "{\"roleName\":\"Test Role\",\"roleCode\":\"test_role\",\"state\":true,\"accessPermissions\":\"product.view\"}" ``` ### 编辑(不传权限字段,保留原菜单) ```bash curl -s -X PUT "%BASE%/api/app/rbac-role/{roleId}" \ -H "Authorization: %TOKEN%" \ -H "Content-Type: application/json" \ -d "{\"roleName\":\"Test Role Updated\",\"roleCode\":\"test_role\",\"state\":true}" ``` ### 删除 ```bash curl -s -X DELETE "%BASE%/api/app/rbac-role" \ -H "Authorization: %TOKEN%" \ -H "Content-Type: application/json" \ -d "[\"{roleId}\"]" ``` --- --- ## partner 合作伙伴列表权限 **应用服务**:`PartnerAppService`(`fl_partner`,Account Management「Company」页签)。 ### 变更说明 | 项 | 说明 | |----|------| | **影响接口** | `GET /api/app/partner`(分页列表)、`GET /api/app/partner/export-pdf`(PDF 导出,筛选与列表一致) | | **管理员** | 可查看 **全部** 合作伙伴(公司) | | **其它角色** | 仅可查看与当前登录用户在 **`userlocation`** 中 **已绑定门店** 相关联的公司 | | **实现类** | `PartnerScopeHelper`(与 Reports 模块 `ReportsRoleHelper.IsAdminRole` 判定一致) | 详情、单条新增/编辑/删除 **不做** 本次范围限制(仍按原逻辑);若需详情也校验归属,可另行扩展。 ### 管理员判定(与 Reports 一致) 满足 **任一** 条件视为管理员,**不做** 公司范围过滤: | 条件 | 说明 | |------|------| | 用户名 | JWT 中用户名为 `admin`(内置超管) | | 角色码 | `Roles` claim 或 `ICurrentUser.Roles` 中含 `admin` | | 权限码 | `Permission` claim 为 `*:*:*` | ### 非管理员数据范围算法 1. 取当前用户 Id(`CurrentUser.Id`)→ 查 **`userlocation`**(`IsDeleted = 0`)得到 **`locationId`** 列表。 2. 若无绑定门店 → 列表 / 导出 **0 条**。 3. 查 **`location`** 表上述门店的 **`Partner`** 字段(存的是 **公司名称**,与新建门店时下拉所选 `partnerName` 一致)。 4. 在 **`fl_partner`** 中匹配:`PartnerName` 等于上述值 **或** `Id` 等于上述值(兼容历史若 `location.Partner` 误存 Id)。 5. 列表 / 导出仅返回匹配到的 **`fl_partner.Id`** 记录。 ```text User ──userlocation──► Location ──Partner(公司名)──► fl_partner ``` ### 1 合作伙伴分页列表 | 项目 | 说明 | |------|------| | HTTP | `GET` | | 路径 | `/api/app/partner` | | Query 示例 | `SkipCount=1&MaxResultCount=10&Sorting=CreationTime desc` | | 其它筛选 | `keyword`(名称/邮箱/电话模糊)、`state`(启用状态) | **说明**: - `SkipCount` 在本项目中为 **页码(从 1 起)**,非 SQL offset;与 `PagedQueryConvention` 一致。 - `Sorting` 支持:`CreationTime asc/desc`、`PartnerName asc/desc`、`State asc/desc`;未识别时默认 `CreationTime desc`。 - **Keyword / State 在数据范围过滤之后生效**(先按权限收窄公司集合,再筛选)。 **响应 `data`**:`PagedResultWithPageDto`(`id`、`partnerName`、`contactEmail`、`phoneNumber`、`state`、`creationTime`)。 ### 2 PDF 导出(同范围) | 项目 | 说明 | |------|------| | HTTP | `GET` | | 路径 | `/api/app/partner/export-pdf` | | Query 示例 | `Sorting=CreationTime desc`(另可传 `keyword`、`state`) | | 分页 | **忽略** `SkipCount` / `MaxResultCount`,导出符合筛选的**全量**公司 | | 上限 | 5000 条,超出返回业务错误 | | 数据范围 | 与列表相同:`BuildPartnerListQueryAsync` → `PartnerScopeHelper` | **规则**:管理员导出全部公司;非管理员仅导出其绑定门店所属公司(算法见上)。**同一 Token、同一 Query 筛选**下,导出行集 = 列表全量结果。 #### curl 示例(partner 导出 PDF) ```bash curl -s -G "%BASE%/api/app/partner/export-pdf" \ -H "Authorization: %TOKEN%" \ --data-urlencode "Sorting=CreationTime desc" \ -o companies.pdf ``` ### curl 示例(partner 列表) ```bash curl -s -G "%BASE%/api/app/partner" \ -H "Authorization: %TOKEN%" \ --data-urlencode "SkipCount=1" \ --data-urlencode "MaxResultCount=10" \ --data-urlencode "Sorting=CreationTime desc" ``` --- ## group Region 列表与 PDF 导出权限 **应用服务**:`GroupAppService`(`fl_group`,Account Management「Region」页签)。 ### 变更说明 | 项 | 说明 | |----|------| | **影响接口** | `GET /api/app/group`(分页列表)、`GET /api/app/group/export-pdf`(PDF 导出) | | **管理员** | 可查看 / 导出 **全部** Region(组织) | | **其它角色** | 仅可查看 / 导出与 **`userlocation` 绑定门店** 对应的 Region(见下方算法) | | **实现** | `BuildGroupJoinedQueryAsync` + `PartnerScopeHelper.ResolveGroupScopeAsync` / `ApplyGroupScope` | 与 **partner 列表** 的区别:partner 按「绑定门店所属 **公司**」过滤;group 按「绑定门店上的 **公司 + 组织名**」精确匹配 `fl_group`,**不会**列出该公司下其它未绑定门店的 Region。 ### 管理员判定 与 [partner 章节](#管理员判定与-reports-一致) 相同(`ReportsRoleHelper.IsAdminRole`)。 ### 非管理员数据范围算法 1. 取当前用户 Id → 查 **`userlocation`** 得到绑定的 **`locationId`**。 2. 若无绑定门店 → 列表 / 导出 **0 条**。 3. 读取这些门店的 **`location.Partner`**(公司名称,与门店保存时一致)与 **`location.GroupName`**(Region 名称)。 4. 将 `Partner` 解析为 **`fl_partner.Id`**(按 `PartnerName` 或 `Id` 匹配,与 partner 列表相同)。 5. 在 **`fl_group`** 中查找同时满足: - `fl_group.PartnerId` = 上一步的公司 Id - `fl_group.GroupName` = 该门店的 `GroupName`(trim 后全等) 6. 列表 / 导出仅包含上述匹配到的 **`fl_group.Id`**。 ```text User ──userlocation──► Location(Partner + GroupName) ──匹配──► fl_group(PartnerId + GroupName) ``` **注意**:若门店未填 `GroupName` 或 `Partner`,该门店不会贡献任何可见 Region。 ### 1 组织分页列表 | 项目 | 说明 | |------|------| | HTTP | `GET` | | 路径 | `/api/app/group` | | Query 示例 | `SkipCount=1&MaxResultCount=10&Sorting=CreationTime desc` | | 其它筛选 | `keyword`、`partnerId`、`state` | **说明**: - `SkipCount` 为 **页码(从 1 起)**。 - `Sorting` 支持:`CreationTime`、`GroupName`、`State`、`PartnerName` 的 asc/desc。 - 先按 Token 数据范围收窄,再应用 `keyword` / `partnerId` / `state`。 **响应 `data`**:`PagedResultWithPageDto`。 #### curl 示例(group 列表) ```bash curl -s -G "%BASE%/api/app/group" \ -H "Authorization: %TOKEN%" \ --data-urlencode "SkipCount=1" \ --data-urlencode "MaxResultCount=10" \ --data-urlencode "Sorting=CreationTime desc" ``` ### 2 PDF 导出(同范围) | 项目 | 说明 | |------|------| | HTTP | `GET` | | 路径 | `/api/app/group/export-pdf` | | Query 示例 | `Sorting=CreationTime desc`(另可传 `keyword`、`partnerId`、`state`) | | 分页 | 忽略分页参数,全量导出(上限 5000 条) | | 数据范围 | 与 `GET /api/app/group` 列表一致(`ResolveGroupScopeAsync`) | #### curl 示例(group 导出 PDF) ```bash curl -s -G "%BASE%/api/app/group/export-pdf" \ -H "Authorization: %TOKEN%" \ --data-urlencode "Sorting=CreationTime desc" \ -o regions.pdf ``` ### 联调注意(group) | 现象 | 可能原因 | |------|----------| | 非管理员 Region 列表为空 | 账号未绑定门店;或门店 `Partner` / `GroupName` 为空 | | 少看到 Region | 仅显示绑定门店对应的那条 `fl_group`,同公司其它 Region 不会出现 | | 有门店仍无 Region | `location.GroupName` 与 `fl_group.GroupName` 不一致(大小写/空格);或 `location.Partner` 与 `fl_partner` 对不上 | --- ### partner 新增与编辑地址字段 **库表**:`fl_partner` 新增列 `Street`、`City`、`StateCode`、`Country`、`ZipCode`。已有库执行脚本: `美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/scripts/fl_partner_add_address_columns.sql` **命名说明**(与门店 `location` 对齐): | 业务含义 | JSON 字段名 | 类型 | 说明 | |----------|-------------|------|------| | 街道 | `street` | string? | 可空 | | 城市 | `city` | string? | 可空 | | 州/省代码 | **`stateCode`** | string? | 如 `NY`;口语里的「state(州)」用此字段 | | 国家 | `country` | string? | 可空 | | 邮编 | `zipCode` | string? | 可空 | | 是否启用 | **`state`** | **boolean** | Active / Inactive,与地址无关 | > 同一 body 中 **`state`(boolean)** 表示启用状态,**`stateCode`(string)** 表示美国州缩写,请勿混用。 #### 3 新增合作伙伴 | 项目 | 说明 | |------|------| | HTTP | `POST` | | 路径 | `/api/app/partner` | | Body | `PartnerCreateInputVo` | **请求示例** ```json { "partnerName": "Global Foods Inc.", "contactEmail": "admin@globalfoods.com", "phoneNumber": "+1 (555) 100-2000", "street": "123 Main St", "city": "New York", "stateCode": "NY", "country": "USA", "zipCode": "10001", "state": true } ``` **响应 `data`**:`PartnerGetOutputDto`(含上述地址字段及 `lastModificationTime`)。 #### 4 编辑合作伙伴 | 项目 | 说明 | |------|------| | HTTP | `PUT` | | 路径 | `/api/app/partner/{id}` | | Body | `PartnerUpdateInputVo`(字段与新增相同) | 未传的地址字段会按空字符串处理为 **清空**(`null` 落库);传则覆盖。 **列表 / 详情出参** 同步返回:`street`、`city`、`stateCode`、`country`、`zipCode`(`PartnerGetListOutputDto` / `PartnerGetOutputDto`)。 **列表 keyword** 模糊搜索已包含地址五字段。 #### curl 示例(新增) ```bash curl -s -X POST "%BASE%/api/app/partner" \ -H "Authorization: %TOKEN%" \ -H "Content-Type: application/json" \ -d "{\"partnerName\":\"Global Foods Inc.\",\"street\":\"123 Main St\",\"city\":\"New York\",\"stateCode\":\"NY\",\"country\":\"USA\",\"zipCode\":\"10001\",\"state\":true}" ``` ### 联调注意 | 现象 | 可能原因 | |------|----------| | 非管理员列表为空 | 账号在 **Team Member** 未绑定任何门店;或门店 **`Partner`** 字段为空 / 与 `fl_partner` 名称不一致 | | 管理员仍看不到某公司 | 检查是否被误判为非管理员(角色码非 `admin`) | | 绑定门店后仍看不到公司 | 确认门店保存时 **`partner`** 为该公司 **名称**(与 `fl_partner.partnerName` 一致) | | 新增/编辑报列不存在 | 未执行 `fl_partner_add_address_columns.sql` | --- ## location 门店经营时间 operatingHours **库表**:`location` 新增列 **`OperatingHours`**(`varchar(512)`,可空,自由文本)。 **脚本路径**(在目标库执行一次): `美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/scripts/fl_location_add_operating_hours_column.sql` ```sql ALTER TABLE `location` ADD COLUMN `OperatingHours` varchar(512) DEFAULT NULL COMMENT '经营时间(自由文本)' AFTER `Longitude`; ``` ### 字段说明 | 业务含义 | JSON 字段名 | DB 列名 | 类型 | 说明 | |----------|-------------|---------|------|------| | 经营时间 | **`operatingHours`** | `OperatingHours` | string? | 可空;示例:`Mon–Fri 9:00 AM – 6:00 PM` | ### 影响接口 | 接口 | 说明 | |------|------| | `GET /api/app/location` | 列表 `items[]` 出参含 `operatingHours` | | `POST /api/app/location` | 新增 body 可传 `operatingHours` | | `PUT /api/app/location/{id}` | 编辑 body 可传 `operatingHours`(覆盖;传空字符串可清空) | | `PUT /api/app/location/bulk-update` | 批量编辑继承 `LocationUpdateInputVo`,同行支持 | | App `GET /api/app/us-app-auth/location-detail` | `operatingHours` 读库;为空时展示 **`无`** | **列表 keyword** 模糊搜索已包含 `operatingHours`。 ### 1 门店分页列表 | 项目 | 说明 | |------|------| | HTTP | `GET` | | 路径 | `/api/app/location` | | Query 示例 | `SkipCount=1&MaxResultCount=10` | **响应 `items[]` 新增字段**:`operatingHours`(string?)。 #### curl 示例(列表) ```bash curl -s -G "%BASE%/api/app/location" \ -H "Authorization: %TOKEN%" \ --data-urlencode "SkipCount=1" \ --data-urlencode "MaxResultCount=10" ``` ### 2 新增门店 | 项目 | 说明 | |------|------| | HTTP | `POST` | | 路径 | `/api/app/location` | | Body | `LocationCreateInputVo` | **请求示例** ```json { "partner": "Global Foods Inc.", "groupName": "East Region", "locationCode": "LOC-001", "locationName": "Downtown Store", "street": "123 Main St", "city": "New York", "stateCode": "NY", "country": "USA", "zipCode": "10001", "phone": "+1 (555) 100-2000", "email": "store@example.com", "operatingHours": "Mon–Fri 9:00 AM – 6:00 PM; Sat 10:00 AM – 4:00 PM", "state": true } ``` **响应 `data`**:`LocationGetListOutputDto`(含 `operatingHours`)。 ### 3 编辑门店 | 项目 | 说明 | |------|------| | HTTP | `PUT` | | 路径 | `/api/app/location/{id}` | | Body | `LocationUpdateInputVo`(含 `operatingHours`) | #### curl 示例(编辑经营时间) ```bash curl -s -X PUT "%BASE%/api/app/location/{id}" \ -H "Authorization: %TOKEN%" \ -H "Content-Type: application/json" \ -d "{\"locationName\":\"Downtown Store\",\"operatingHours\":\"Mon–Fri 9:00 AM – 6:00 PM\",\"state\":true}" ``` ### 联调注意(location) | 现象 | 可能原因 | |------|----------| | 新增/编辑报列不存在 | 未执行 `fl_location_add_operating_hours_column.sql` | | App 详情仍显示「无」 | 门店未填写 `operatingHours` 或仅空格 | --- ## product 产品列表按公司/组织/门店筛选 **应用服务**:`ProductAppService`(`fl_product` + `fl_location_product` + `location`)。 ### 变更说明 | 项 | 说明 | |----|------| | **影响接口** | `GET /api/app/product`(分页列表)、`GET /api/app/product/export-products-excel`(Excel 导出) | | **新增 Query** | `partnerId`、`groupId`、`locationId`(均可选) | | **筛选逻辑** | 仅返回在 **`fl_location_product`** 中关联到「匹配门店」的产品;与 Menu Management 页 **All Companies / All Regions / All Locations** 下拉一致 | ### Query 参数 | 字段 | 类型 | 说明 | |------|------|------| | skipCount | int | 页码(从 1 起,与项目其它列表一致) | | maxResultCount | int | 每页条数 | | sorting | string | 可选排序 | | keyword | string | 模糊:ProductCode / ProductName / CategoryName | | state | bool | 启用状态 | | **partnerId** | string | **公司**:`fl_partner.Id`;匹配 `location.Partner` = 该公司名称的门店 | | **groupId** | string | **组织/Region**:`fl_group.Id`;匹配该 Region 下 `Partner` + `GroupName` 的门店 | | **locationId** | string | **门店**:`location.Id`(Guid 字符串);**最优先**,传则忽略 partnerId / groupId | ### 筛选优先级与算法 ```text locationId 有值 → 仅该门店(须存在且未删除) 否则 groupId 有值 → location.Partner + location.GroupName 与 fl_group 对应公司名、组织名一致 否则 partnerId 有值 → location.Partner = fl_partner.PartnerName 否则 → 不按门店收窄(与改前一致) 匹配到的 location Id 集合 → fl_location_product.LocationId → 产品 fl_product.Id ``` | 场景 | 结果 | |------|------| | 三者均未传 | 全部未删除产品(仍受 keyword / state 约束) | | 传了 partnerId 但公司不存在 | **0 条** | | 传了 groupId 但 Region 不存在 | **0 条** | | 传了 locationId 但门店不存在 / 非 Guid | **0 条** | | 产品未绑定任何门店 | 在传了任一公司/组织/门店筛选时 **不会出现** | 与 **Reports** 打印日志列表的 `partnerId` / `groupId` / `locationId` 解析规则一致。 ### 1 产品分页列表 | 项目 | 说明 | |------|------| | HTTP | `GET` | | 路径 | `/api/app/product` | | Query 示例 | `SkipCount=1&MaxResultCount=10` | #### curl 示例(按门店筛选) ```bash curl -s -G "%BASE%/api/app/product" \ -H "Authorization: %TOKEN%" \ --data-urlencode "SkipCount=1" \ --data-urlencode "MaxResultCount=10" \ --data-urlencode "locationId=11111111-1111-1111-1111-111111111111" ``` #### curl 示例(按公司 + 关键字) ```bash curl -s -G "%BASE%/api/app/product" \ -H "Authorization: %TOKEN%" \ --data-urlencode "SkipCount=1" \ --data-urlencode "MaxResultCount=10" \ --data-urlencode "partnerId=你的fl_partner主键" \ --data-urlencode "Keyword=tuna" ``` ### 2 Excel 导出(同筛选) `GET /api/app/product/export-products-excel` 支持与列表相同的 `partnerId`、`groupId`、`locationId`、`keyword`、`state`。 ### 联调注意(product) | 现象 | 可能原因 | |------|----------| | 选了公司后列表为空 | 产品未在 `fl_location_product` 绑定该公司下任一门店 | | Region 筛选无数据 | 门店 `Partner` / `GroupName` 与 `fl_group` 不一致 | | 与前端下拉 Id 对不上 | 公司/组织传 **主键 Id**(`fl_partner.Id`、`fl_group.Id`),门店传 **location.Id** | --- ## product-category / product Region·Location 绑定与筛选 **应用服务**:`ProductCategoryAppService`、`ProductAppService`。 **命名约定**:UI **Region** = API **`groupIds` / `groupId`**(`fl_group.Id`);落库门店冗余字段为 **`location.GroupName`**。UI **Location** = **`locationIds` / `locationId`**(`location.Id`)。 ### 变更说明(product-category) | 项 | 说明 | |----|------| | **列表** `GET /api/app/product-category` | 出参 `region`、`location`;Query 增加 **`groupId`**、**`locationId`** 筛选 | | **新增/编辑** `POST` / `PUT` | Body 增加 **`groupIds`**、**`locationIds`**(与原有 `availabilityType` 配合) | | **详情** `GET /{id}` | 返回 **`groupIds`**、**`locationIds`** | | **存储** | 仍写入 **`fl_product_category_location`**(按合并后的门店 Id,无新表) | ### 变更说明(product) | 项 | 说明 | |----|------| | **新增/编辑** | Body 增加 **`groupIds`**、**`locationIds`**,合并写入 **`fl_location_product`** | | **详情** | 返回 **`groupIds`**(由关联门店反推)、**`locationIds`** | ### 出参字段 | 字段 | 类型 | 说明 | |------|------|------| | region | string | 适用 Region 汇总 | | location | string | 适用门店汇总 | 其它列表字段不变(`categoryCode`、`categoryName`、`availabilityType`、`orderNum` 等)。 ### 展示规则 | availabilityType | region | location | |------------------|--------|----------| | **ALL**(或未配置为 SPECIFIED) | `All Regions` | `All Locations` | | **SPECIFIED** | 绑定门店在 `location.GroupName` 上的**去重**值,按字母序 `, ` 拼接 | | **SPECIFIED** 但未绑定门店 | `无` | `无` | | **SPECIFIED** 已绑定但门店无 GroupName | `无` | 仍有门店名时显示门店名 | 门店名优先 `location.LocationName`,为空则用 `LocationCode`。 数据来源:`fl_product_category_location` → `location`(与详情中的 `locationIds` 一致,列表侧转为可读文案)。 ### 新增/编辑 product-category(Region / Location 多选数组) | 项目 | 说明 | |------|------| | 新增 | `POST /api/app/product-category` | | 编辑 | `PUT /api/app/product-category/{id}` | | Body | `ProductCategoryCreateInputVo` / `ProductCategoryUpdateInputVo` | | 字段 | 类型 | 说明 | |------|------|------| | availabilityType | string | `ALL` / `SPECIFIED`;见下方自动规则 | | **regionIds** | **string[]** | **Region 多选**(`fl_group.Id`);推荐字段名 | | groupIds | string[] | 与 `regionIds` 等价,会合并去重(兼容旧字段名) | | **locationIds** | **string[]** | **门店 多选**(`location.Id`) | **自动规则** | 入参 | 行为 | |------|------| | `regionIds` / `locationIds` 任一有值 | `availabilityType` 按 **`SPECIFIED`** 处理 | | 仅传 `regionIds: []`、`locationIds: []` 且 `availabilityType` 为 `ALL` | 清空指定门店,范围改回 **全部** | | `availabilityType: "ALL"` 且未传上述数组 | 不绑定门店(全部门店可用) | **合并规则**:每个 `regionIds` 展开为该 Region 下全部门店,再与 `locationIds` **取并集** → 写入 `fl_product_category_location`;至少 **1** 个有效门店。 **请求示例(多选 Region + 多选门店)** ```json { "categoryCode": "CAT-01", "categoryName": "Sandwich", "regionIds": [ "fl_group_id_east", "fl_group_id_west" ], "locationIds": [ "11111111-1111-1111-1111-111111111111", "22222222-2222-2222-2222-222222222222" ], "state": true, "orderNum": 10 } ``` **详情 / 列表回显** | 接口 | 多选 Id 字段 | |------|----------------| | `GET /api/app/product-category/{id}` | `regionIds`、`locationIds`(`groupIds` 与 `regionIds` 相同) | | `GET /api/app/product-category` 列表 `items[]` | `regionIds`、`locationIds` + 展示字段 `region`、`location` | ### 1 类别分页列表 | 项目 | 说明 | |------|------| | HTTP | `GET` | | 路径 | `/api/app/product-category` | | Query 示例 | `SkipCount=1&MaxResultCount=10&Sorting=OrderNum desc` | **新增 Query 筛选** | 字段 | 说明 | |------|------| | **groupId** | 按 Region(`fl_group.Id`)筛选;含 `availabilityType=ALL` 的分类 | | **locationId** | 按门店筛选(优先于 `groupId`) | **响应 `items[]` 示例片段** ```json { "id": "...", "categoryName": "Sandwich", "availabilityType": "SPECIFIED", "region": "East Region, West Region", "location": "UNCC store, Central Park Store", "regionIds": ["fl_group_id_east", "fl_group_id_west"], "locationIds": ["11111111-1111-1111-1111-111111111111"], "orderNum": 10 } ``` #### curl 示例(按 Region 筛选) ```bash curl -s -G "%BASE%/api/app/product-category" \ -H "Authorization: %TOKEN%" \ --data-urlencode "SkipCount=1" \ --data-urlencode "MaxResultCount=10" \ --data-urlencode "Sorting=OrderNum desc" \ --data-urlencode "groupId=你的fl_group主键" ``` ### 2 产品新增/编辑(Company + Region + Location) | 项目 | 说明 | |------|------| | 新增 | `POST /api/app/product` | | 编辑 | `PUT /api/app/product/{id}` | | 详情 | `GET /api/app/product/{id}` 返回 `partnerId`、`partnerIds`、`groupIds`、`locationIds` | | 字段 | UI | 类型 | 说明 | |------|-----|------|------| | **partnerId** | **Company** | string? | `fl_partner.Id`;展开该公司下**全部门店**参与合并 | | **groupIds** | **Region** | string[]? | `fl_group.Id`;展开各 Region 下全部门店 | | **locationIds** | **Location** | string[]? | `location.Id`;显式指定门店 | **合并规则**(去重后写入 `fl_location_product`): ```text partnerId → 公司下全部门店 groupIds → 各 Region 下全部门店(Partner + GroupName 匹配 location) locationIds → 指定门店 三者取并集 ``` 传 **`locationIds: []`** 且其它范围字段为空/不传时,可清空门店关联;仅传 `partnerId` 或 `groupIds` 时须至少解析出 1 个有效门店。 **请求示例** ```json { "productName": "Tuna Sub", "categoryId": "分类Id", "partnerId": "fl_partner主键", "groupIds": ["fl_group_id_east"], "locationIds": ["11111111-1111-1111-1111-111111111111"], "state": true } ``` **详情出参示例** ```json { "id": "...", "productName": "Tuna Sub", "partnerId": "fl_partner主键", "partnerIds": ["fl_partner主键"], "groupIds": ["fl_group_id_east"], "locationIds": ["11111111-1111-1111-1111-111111111111"] } ``` > 列表筛选仍用 Query:`partnerId`(Company)、`groupId`(Region)、`locationId`(Location),见 [product 列表筛选](#product-产品列表按公司组织门店筛选)。 ### 联调注意(product-category / product) | 现象 | 可能原因 | |------|----------| | region 为「无」 | SPECIFIED 但门店未填 `GroupName` | | location 为「无」 | SPECIFIED 但未保存绑定 / 关联表无数据 | | 仍为 All Regions | `availabilityType` 不是 `SPECIFIED` | | 保存报「至少需要匹配到一个有效门店」 | `groupIds` 下无门店,且 `locationIds` 为空或无效 | | Region 选了但门店未全带上 | 正常:`groupIds` 会展开该区域下全部门店写入关联表 | --- ## team-member 成员 Company·Region 绑定 **应用服务**:`TeamMemberAppService`。 **命名约定**(与 product / product-category 一致): | UI | API 字段 | 存储 | |----|----------|------| | Company | `partnerId` / `partnerIds` | `fl_partner.Id`;门店通过 `location.Partner`(公司名称)关联 | | Region | `regionIds` / `groupIds` | `fl_group.Id`;门店冗余字段 `location.GroupName` | | Location | `locationIds` | `location.Id` → 写入 **`userlocation`** | ### 变更说明 | 项 | 说明 | |----|------| | **新增** `POST /api/app/team-member` | Body 增加 `partnerId`、`partnerIds`、`regionIds`、`groupIds`;`locationIds` 可与前三者合并 | | **编辑** `PUT /api/app/team-member/{id}` | 同上;批量编辑 `update-team-members-bulk` 的 `items[]` 与单条 PUT 字段一致 | | **详情** `GET /api/app/team-member/{id}` | 返回 `partnerIds`、`regionIds`、`groupIds`(与 `regionIds` 相同)、`locationIds` | | **列表** `GET /api/app/team-member` | `items[]` 增加 `partnerIds`、`regionIds`(由已绑定门店反推) | | **存储** | 仍写入 **`userlocation`**(合并后的门店 Id 列表,无新表) | ### 新增 / 编辑入参(节选) | 字段 | 类型 | 必填 | 说明 | |------|------|------|------| | fullName | string | 是 | 姓名 | | userName | string | 是(新增) | 登录名 | | password | string | 是(新增)/ 可空(编辑) | 密码 | | email | string | 否 | 邮箱 | | phone | long? | 否 | 手机 | | roleId | Guid? | 否 | RBAC 角色 | | **partnerId** | string | 否 | 单个 Company(`fl_partner.Id`) | | **partnerIds** | string[] | 否 | Company 多选;与 `partnerId` 合并 | | **regionIds** | string[] | 否 | Region 多选(`fl_group.Id`) | | **groupIds** | string[] | 否 | 与 `regionIds` 相同,合并去重 | | **locationIds** | string[] | 否 | 显式门店;与 Company/Region 展开结果 **取并集** | | state | bool | 否 | 启用,默认 `true` | **合并规则**:`partnerId`/`partnerIds` → 该公司下全部门店;`regionIds`/`groupIds` → 该区域下全部门店;再并入 `locationIds`。保存前须 **至少 1 个有效门店**,否则返回:`成员必须至少分配一个门店(公司/区域/门店至少选一项)`。 ### 详情 / 列表出参(节选) | 字段 | 类型 | 说明 | |------|------|------| | partnerIds | string[] | 由 `userlocation` 反推的 Company Id | | regionIds | string[] | 由绑定门店反推的 Region Id | | groupIds | string[] | 与 `regionIds` 相同 | | locationIds | string[] | 实际绑定的门店 Id | | assignedLocations | object[] | 门店展示(id、name 等,原有字段) | ### 请求示例(新增) ```http POST /api/app/team-member Content-Type: application/json Authorization: {token} ``` ```json { "fullName": "Jane Doe", "userName": "jane.doe", "password": "ChangeMe123!", "email": "jane@example.com", "roleId": "00000000-0000-0000-0000-000000000001", "partnerId": "fl_partner主键", "groupIds": ["fl_group_id_east"], "locationIds": [], "state": true } ``` ### 列表示例 ```http GET /api/app/team-member?SkipCount=1&MaxResultCount=10 Authorization: {token} ``` ### 联调注意(team-member) | 现象 | 可能原因 | |------|----------| | 保存报「至少分配一个门店」 | Company/Region/门店均未选,或所选范围下无有效门店 | | 详情 `partnerIds` 为空 | 绑定门店的 `location.Partner` 未匹配到 `fl_partner` | | 详情 `regionIds` 为空 | 门店未填 `GroupName` 或无法匹配 `fl_group` | | 批量导入仍只认门店列 | Excel 导入逻辑未改,仍按 `LocationIds` 解析;Web 表单用新字段 | > 批量导入 / PDF 导出说明见 `项目相关文档/批量导入导出接口说明.md`。 --- ## label 标签 **应用服务**:`LabelAppService`。 **命名约定**: | UI | API | 存储 | |----|-----|------| | Company | `partnerId` / `partnerIds` | 解析范围;落库为 **`fl_label.LocationId`**(单门店) | | Region | `regionIds` / `groupIds` / `groupId`(列表筛选) | `fl_group.Id` | | Location | `locationId` / `locationIds` | `location.Id` | 标签表仅保存 **一个** `LocationId`;Company/Region 用于选定或校验门店,与 product 多门店关联不同。 ### 变更说明(列表) | 项 | 说明 | |----|------| | **列表** `GET /api/app/label` | Query 增加 **`groupId`**(Region 筛选);与 **`locationId`** 配合 | ### Query 参数(节选) | 字段 | 类型 | 说明 | |------|------|------| | skipCount | int | 页码(从 1 起) | | maxResultCount | int | 每页条数 | | keyword | string | 模糊:标签名、分类、类型、模板、产品名 | | productId | string | 按关联产品筛选 | | labelCategoryId | string | 标签分类 Id | | labelTypeId | string | 标签类型 Id | | templateCode | string | 模板编码 | | state | bool | 启用状态 | | **groupId** | string | **Region**:`fl_group.Id`;返回 `fl_label.LocationId` 属于该区域下门店的标签 | | **locationId** | string | **门店**:`location.Id`;**优先于** `groupId` | ### 筛选优先级 ```text locationId 有值 → 仅该门店(须存在且未删除) 否则 groupId 有值 → 该区域下全部门店(Partner + GroupName 与 fl_group 一致) 否则 → 不按门店/Region 收窄 ``` | 场景 | 结果 | |------|------| | 均未传 | 与改前一致(仍受 keyword / productId 等约束) | | 传了 groupId 但 Region 不存在 | **0 条** | | 传了 locationId 但门店无效 | **0 条** | | 标签未填 LocationId | 在传了 groupId / locationId 时 **不会出现** | 与 **product-category** 列表的 `groupId` / `locationId` 解析规则一致(`LocationScopeBindingHelper.ResolveScopedLocationIdsAsync`)。 ### 1 标签分页列表 | 项目 | 说明 | |------|------| | HTTP | `GET` | | 路径 | `/api/app/label` | | Query 示例 | `SkipCount=1&MaxResultCount=10` | #### curl 示例(按 Region 筛选) ```bash curl -s -G "%BASE%/api/app/label" \ -H "Authorization: %TOKEN%" \ --data-urlencode "SkipCount=1" \ --data-urlencode "MaxResultCount=10" \ --data-urlencode "groupId=你的fl_group主键" ``` #### curl 示例(按门店筛选,优先于 groupId) ```bash curl -s -G "%BASE%/api/app/label" \ -H "Authorization: %TOKEN%" \ --data-urlencode "SkipCount=1" \ --data-urlencode "MaxResultCount=10" \ --data-urlencode "locationId=11111111-1111-1111-1111-111111111111" ``` ### 变更说明(新增 / 编辑) | 项 | 说明 | |----|------| | **新增** `POST /api/app/label` | Body 增加 **`partnerId`**、**`partnerIds`**、**`regionIds`**、**`groupIds`**、**`locationIds`** | | **编辑** `PUT /api/app/label/{id}` | 同上(`id` = LabelCode) | | **详情** `GET /api/app/label/{id}` | 返回 **`partnerId`**、**`partnerIds`**、**`regionIds`**、**`groupIds`**、**`locationId`** | ### 新增/编辑入参(节选) | 字段 | 类型 | 说明 | |------|------|------| | labelName | string | 标签名称 | | templateCode | string | 模板编码 | | labelCategoryId / labelTypeId | string | 分类、类型 | | productIds | string[] | 关联产品(至少 1 个) | | **partnerId** | string | Company(`fl_partner.Id`) | | **partnerIds** | string[] | Company 多选 | | **regionIds** | string[] | Region 多选(`fl_group.Id`) | | **groupIds** | string[] | 与 `regionIds` 合并 | | **locationId** | string | 所属门店;**优先**直接落库 | | **locationIds** | string[] | 门店候选;未传 `locationId` 且合并后 **唯一** 时自动采用 | **解析规则** ```text 传 locationId → 校验在 Company/Region 展开范围内(有范围时)→ 写入 fl_label.LocationId 未传 locationId → 合并 partnerId/partnerIds + regionIds/groupIds + locationIds → 0 个:报错 → 1 个:自动作为所属门店 → 多个:报错,要求显式传 locationId ``` **请求示例** ```json { "labelName": "Tuna Label", "templateCode": "TMP-01", "partnerId": "fl_partner主键", "groupIds": ["fl_group_id_east"], "locationId": "11111111-1111-1111-1111-111111111111", "labelCategoryId": "分类Id", "labelTypeId": "类型Id", "productIds": ["产品Id"], "state": true } ``` **详情出参示例** ```json { "id": "LBL_xxx", "labelName": "Tuna Label", "locationId": "11111111-1111-1111-1111-111111111111", "partnerId": "fl_partner主键", "partnerIds": ["fl_partner主键"], "regionIds": ["fl_group_id_east"], "groupIds": ["fl_group_id_east"] } ``` ### 联调注意(label) | 现象 | 可能原因 | |------|----------| | 选了 Region 后列表为空 | 标签 `LocationId` 未落在该区域门店,或门店 `Partner`/`GroupName` 与 `fl_group` 不一致 | | 与前端下拉 Id 对不上 | Region 传 **`fl_group.Id`**,门店传 **`location.Id`** | | 保存报「对应多个门店」 | 仅选了 Company/Region 且展开多于 1 家门店,须再选 `locationId` | | 保存报「须指定门店」 | 未传 `locationId` 且 Company/Region 未解析出门店 | --- ## label-category 标签分类 Region·Location **应用服务**:`LabelCategoryAppService`。 **命名约定**:UI **Region** = API **`regionIds` / `groupIds` / `groupId`**(`fl_group.Id`);UI **Location** = **`locationIds` / `locationId`**(`location.Id`)。存储表 **`fl_label_category_location`**(与 product-category 模式一致)。 ### 变更说明 | 项 | 说明 | |----|------| | **新增** `POST /api/app/label-category` | Body 增加 **`regionIds`**、**`groupIds`**、**`locationIds`**(多选数组) | | **编辑** `PUT /api/app/label-category/{id}` | 同上 | | **详情** `GET /{id}` | 返回 **`regionIds`**、**`groupIds`**(与 regionIds 相同)、**`locationIds`** | | **列表** `GET /api/app/label-category` | 出参 **`region`**、**`location`**、**`noOfLabels`**、**`lastEdited`**(与下属标签同步);Query **`groupId`**、**`locationId`** 筛选 | | **存储** | `availabilityType=SPECIFIED` 时写入 **`fl_label_category_location`**(Region 展开为门店后取并集) | ### 新增/编辑入参(节选) | 字段 | 类型 | 说明 | |------|------|------| | availabilityType | string | `ALL` / `SPECIFIED`;传了 regionIds/locationIds 时自动按 **SPECIFIED** | | **regionIds** | string[] | Region 多选(`fl_group.Id`) | | **groupIds** | string[] | 与 `regionIds` 合并去重 | | **locationIds** | string[] | 门店多选(`location.Id`) | **合并规则**:每个 `regionIds` 展开为该 Region 下全部门店,再与 `locationIds` **取并集** → 写入关联表;`SPECIFIED` 时至少 **1** 个有效门店。 **请求示例** ```json { "categoryCode": "LC-01", "categoryName": "Sandwich Labels", "availabilityType": "SPECIFIED", "regionIds": ["fl_group_id_east"], "locationIds": ["11111111-1111-1111-1111-111111111111"], "state": true, "orderNum": 10 } ``` ### 列表 Query 筛选 | 字段 | 说明 | |------|------| | **groupId** | 按 Region 筛选;**含** `availabilityType=ALL` 的分类 | | **locationId** | 按门店筛选;**优先于** `groupId` | ```bash curl -s -G "%BASE%/api/app/label-category" \ -H "Authorization: %TOKEN%" \ --data-urlencode "SkipCount=1" \ --data-urlencode "MaxResultCount=10" \ --data-urlencode "groupId=你的fl_group主键" ``` ### 列表出参(节选) | 字段 | 类型 | 说明 | |------|------|------| | id | string | 分类主键 | | categoryCode / categoryName | string | 编码、名称 | | state / orderNum | bool / int | 状态、排序 | | **region** | string | 列表列 **Region**:`ALL` → `All Regions`;`SPECIFIED` → 绑定门店 `GroupName` 去重 `, ` 拼接 | | **location** | string | 列表列 **Location**:`ALL` → `All Locations`;`SPECIFIED` → 门店名拼接 | | regionIds | string[] | Region Id(`ALL` 时 `[]`) | | locationIds | string[] | 门店 Id(`ALL` 时 `[]`) | | **noOfLabels** | long | 列表列 **No. of Label**:该分类下未删除标签数(`fl_label.LabelCategoryId`) | | creationTime | datetime | 分类创建时间 | | **lastEdited** | datetime | 列表列 **Last Edited**:`max(分类 LastModificationTime/CreationTime, 下属标签最近编辑时间)`;标签增删改会回写分类 `LastModificationTime` | **响应 `items[]` 示例** ```json { "id": "cat_001", "categoryName": "Sandwich Labels", "availabilityType": "SPECIFIED", "region": "East Region", "location": "UNCC store, Central Park Store", "regionIds": ["fl_group_id_east"], "locationIds": ["11111111-1111-1111-1111-111111111111"], "noOfLabels": 12, "creationTime": "2026-05-10T08:00:00", "lastEdited": "2026-05-17T14:30:00", "orderNum": 10 } ``` > 前端 **Last Edited** 列请绑定 **`lastEdited`**,勿用 `creationTime`(仅为兼容保留)。 ### 联调注意(label-category) | 现象 | 可能原因 | |------|----------| | region 为「无」 | SPECIFIED 但门店未填 `GroupName` | | 保存报「至少需要匹配到一个有效门店」 | Region 下无门店且 `locationIds` 为空 | | 列表筛 Region 仍看到 ALL 分类 | 设计如此:`ALL` 对任意 Region/门店均可见 | | Last Edited 不随标签变化 | 未拉最新列表;应读 `lastEdited` 而非 `creationTime` | | noOfLabels 为 0 | 该分类下尚无标签,或标签 `LabelCategoryId` 未指向本分类 | > 逻辑与 [product-category](#product-category--product-regionlocation-绑定与筛选) 对齐,仅表名为 `fl_label_category` / `fl_label_category_location`。 --- ## 修订记录 | 日期 | 说明 | |------|------| | 2026-05-17 | 初版:rbac-role `accessPermissions` / `menuIds` / `orderNum` | | 2026-05-17 | 补充:partner 列表与 export-pdf 按 Token 数据范围过滤 | | 2026-05-17 | 补充:partner 新增/编辑/列表/详情 地址字段 street、city、stateCode、country、zipCode | | 2026-05-17 | 补充:group 列表/export-pdf 按绑定门店 Partner+GroupName 过滤 Region | | 2026-05-17 | 补充:location 新增/编辑/列表 operatingHours(经营时间) | | 2026-05-17 | 补充:product 列表/export 增加 partnerId、groupId、locationId 筛选 | | 2026-05-17 | 补充:product-category 列表出参 region、location 展示适用区域与门店 | | 2026-05-17 | 补充:product-category/product 新增编辑 groupIds+locationIds;分类列表 groupId/locationId 筛选 | | 2026-05-17 | 补充:product 新增/编辑/详情 增加 partnerId(Company)与 groupIds(Region)绑定 | | 2026-05-17 | 补充:product-category 新增/编辑/列表 regionIds、locationIds 多选数组 | | 2026-05-17 | 补充:team-member 新增/编辑/详情/列表 partnerId、partnerIds、regionIds、groupIds 与 locationIds 合并绑定 | | 2026-05-17 | 补充:label 列表 GET 增加 groupId(Region)筛选,locationId 优先 | | 2026-05-17 | 补充:label 新增/编辑/详情 partnerId、partnerIds、regionIds、groupIds 与 locationId 解析绑定 | | 2026-05-17 | 补充:label-category 新增/编辑 regionIds+locationIds 多选;列表 groupId/locationId 筛选与 region/location 展示 | | 2026-05-17 | 补充:label-category 列表 noOfLabels、lastEdited(含下属标签同步)、region/location 列出参说明 |