# 5-26 代码优化 本文档说明 **2026-05-26** 对美国版接口的变更。 1. **`/api/app/product-category`**:新增/编辑 **`categoryCode` 取消必填**(见 [product-category-categoryCode](#product-category-categorycode-可选))。 2. **`/api/app/label-template`**:新增/编辑/列表/详情支持 **Region、Location 多选数组**;列表 Query 增加 **Region/Location 筛选**(见 [label-template-regionlocation](#label-template-regionlocation-多选))。 3. **`/api/app/rbac-role`**:修复 **`accessPermissions` JSON 数组**(如 `manage_labels`)无法绑定菜单(见 [rbac-role-accesspermissions](#rbac-role-accesspermissions-修复))。 4. **`/api/app/auth-scope`**:管理员(及按数据范围受限账号)登录后 **Company → Region → Location** 级联选店(见 [auth-scope-登录选店](#auth-scope-登录后-company--region--location-级联选店))。 5. **`/api/app/us-app-auth`**:App 管理员 Token 专用 **Company / Region / 门店筛选** 接口(见 [us-app-auth-管理员选店](#us-app-auth-app-管理员级联选店))。 **应用服务**:`ProductCategoryAppService`、`LabelTemplateAppService`、`RbacRoleAppService` **命名约定**(与 5-17 / 5-18 一致):UI **Region** = API **`regionIds` / `groupIds` / `groupId`**(`fl_group.Id`);UI **Location** = **`locationIds` / `locationId`**(`location.Id`)。 --- ## product-category categoryCode 可选 **影响接口** | 方法 | 路径 | |------|------| | POST | `/api/app/product-category` | | PUT | `/api/app/product-category/{id}` | ### 变更说明 | 项 | 变更前 | 变更后 | |----|--------|--------| | **categoryCode** | 必填;空则报「类别编码和名称不能为空」 | **可选**;可不传、传 `null` 或 `""` | | **categoryName** | 必填 | 仍必填 | | **落库** | — | 未填编码时 `CategoryCode` 存 **空字符串** | | **唯一性** | 编码或名称重复即报错 | 有编码:编码 **或** 名称重复报错;**无编码**:仅校验 **名称** 不重复 | ### 入参(节选) | 字段 | 类型 | 必填 | 说明 | |------|------|------|------| | categoryCode | string | **否** | 类别编码 | | categoryName | string | **是** | 类别名称 | | regionIds / groupIds / locationIds | string[] | 否 | Region·Location 范围(规则见 `5-17接口优化.md`) | ### 请求示例(无编码) ```http POST /api/app/product-category Content-Type: application/json Authorization: Bearer {token} ``` ```json { "categoryName": "Beverages", "buttonAppearance": "TEXT", "state": true, "availabilityType": "ALL", "orderNum": 0 } ``` ### 联调注意 | 现象 | 处理 | |------|------| | 仍报「类别编码和名称不能为空」 | 确认已部署含本变更的后端;仅需保证 **categoryName** 非空 | | 无编码时名称重复 | 正常:仅按 **categoryName** 判重 | > Region/Location 多选、列表 `region`/`location` 展示等完整说明见 `5-17接口优化.md` → product-category 章节。 --- ## label-template Region·Location 多选 **应用服务**:`LabelTemplateAppService` **存储表**:`fl_label_template_location`(模板 ↔ 门店,**无新表**) **主表字段**:`fl_label_template.AppliedLocationType` = `ALL` / `SPECIFIED` ### 变更说明 | 项 | 变更前 | 变更后 | |----|--------|--------| | **新增/编辑 Body** | 仅 `appliedLocation` + `appliedLocationIds` | 增加 **`regionIds`**、**`groupIds`**、**`locationIds`**(与 `appliedLocationIds` 合并) | | **列表 Query** | 仅 `locationId` | 增加 **`groupId`**(Region);`locationId` 优先于 `groupId`(与 product-category 一致) | | **列表出参** | 仅 `locationText`(单条展示) | 增加 **`region`**、**`location`** 展示 + **`regionIds`**、**`locationIds`** 数组 | | **详情出参** | `appliedLocationIds` | 同上,并保留 **`appliedLocationIds`**(与 `locationIds` 一致,兼容编辑器) | | **范围解析** | 仅显式门店 Id | Region 展开为门店后与门店 Id **取并集** 落库 | ### 影响接口 | 方法 | 路径 | 说明 | |------|------|------| | GET | `/api/app/label-template?SkipCount=1&MaxResultCount=10` | 列表支持 `groupId`/`locationId` 筛选;`items[]` 增加 `region`、`location`、`regionIds`、`locationIds` | | GET | `/api/app/label-template/{id}` | 详情增加上述字段 | | POST | `/api/app/label-template` | Body 支持 Region/Location 多选 | | PUT | `/api/app/label-template/{id}` | 同新增 | 路径参数 **`id`** 仍为模板编码 **`TemplateCode`**(与编辑器 JSON 的 `id` 一致)。 ### 新增/编辑入参(Body:`LabelTemplateCreateInputVo`) | 字段 | JSON 名 | 类型 | 必填 | 说明 | |------|---------|------|------|------| | TemplateCode | `id` | string | 是 | 模板编码 | | TemplateName | `name` | string | 是 | 模板名称 | | AppliedLocationType | `appliedLocation` | string | 否 | `ALL` / `SPECIFIED`,默认 `ALL` | | RegionIds | `regionIds` | string[] | 否 | Region 多选(`fl_group.Id`) | | GroupIds | `groupIds` | string[] | 否 | 与 `regionIds` 等价,合并去重 | | LocationIds | `locationIds` | string[] | 否 | 门店多选(`location.Id`) | | AppliedLocationIds | `appliedLocationIds` | string[] | 否 | 兼容旧字段,与 `locationIds` 合并 | | Elements | `elements` | array | 否 | 模板组件,全量重建 | | TemplateProductDefaults | `templateProductDefaults` | array | 否 | 仅 **编辑** 时显式传入才重建 | **自动规则** | 入参 | 行为 | |------|------| | `regionIds` / `groupIds` / `locationIds` / `appliedLocationIds` 任一有有效 Id | `appliedLocation` 按 **`SPECIFIED`** 处理 | | 仅传空数组 `[]` 且 `appliedLocation` 为 `ALL` | 不绑定门店(全部门店) | | `appliedLocation: "SPECIFIED"` 且合并后无有效门店 | 报错:`指定适用区域或门店时,至少需要匹配到一个有效门店` | | `appliedLocation` 非法值 | 报错:`适用门店范围不合法(ALL/SPECIFIED)` | **合并规则**:每个 `regionIds` 展开为该 Region 下全部门店,再与 `locationIds`、`appliedLocationIds` **取并集** → 写入 `fl_label_template_location`。 ### 请求示例(Region + 门店多选) ```http POST /api/app/label-template Content-Type: application/json Authorization: Bearer {token} ``` ```json { "id": "TPL_TEST_001", "name": "Price Tag 4x6", "labelType": "PRICE", "unit": "inch", "width": 4, "height": 6, "appliedLocation": "SPECIFIED", "regionIds": [ "fl_group_id_east", "fl_group_id_west" ], "locationIds": [ "11111111-1111-1111-1111-111111111111" ], "showRuler": true, "showGrid": true, "state": true, "elements": [] } ``` ### 请求示例(全部门店,兼容旧版) ```json { "id": "TPL_ALL", "name": "Global Template", "labelType": "PRICE", "unit": "inch", "width": 4, "height": 6, "appliedLocation": "ALL", "appliedLocationIds": [], "elements": [] } ``` ### 列表(`GET /api/app/label-template`) **Query 参数** | 字段 | 类型 | 说明 | |------|------|------| | SkipCount / MaxResultCount | int | 分页(项目约定 SkipCount 从 1 起) | | keyword | string | 模板名称/编码模糊 | | **groupId** | string | **按 Region 筛选**(`fl_group.Id`):命中 `appliedLocation=ALL` 的模板,或在 `fl_label_template_location` 中绑定了该 Region 下任一门门店的模板 | | **locationId** | string | **按门店筛选**(`location.Id`);**优先于 groupId** | | labelType | string | 如 `PRICE` | | state | bool | 启用状态 | | sorting | string | 排序(可选) | **筛选规则**(与 product-category / label-type 相同,内部 `LocationScopeBindingHelper.ResolveScopedLocationIdsAsync`) | 入参 | 行为 | |------|------| | 均未传 `groupId`、`locationId` | 不过滤适用范围 | | 仅 `groupId` | 解析该 Region 下全部门店 Id,再筛模板 | | 仅 `locationId` | 按该门店 Id 筛模板 | | 同时传 | **以 `locationId` 为准**(忽略 `groupId`) | | Region/门店无效或解析结果为空 | 仅返回 **`appliedLocation=ALL`** 的模板 | 命中条件(满足其一即可出现在列表): - `fl_label_template.AppliedLocationType = 'ALL'` - `SPECIFIED` 且 `fl_label_template_location` 中存在 `LocationId ∈` 解析得到的门店集合 **请求示例** ```http GET /api/app/label-template?SkipCount=1&MaxResultCount=10&groupId=fl_group_id_east HTTP/1.1 Authorization: Bearer {token} ``` ```http GET /api/app/label-template?SkipCount=1&MaxResultCount=10&locationId=11111111-1111-1111-1111-111111111111 HTTP/1.1 Authorization: Bearer {token} ``` **命名对照**:UI **Region** → Query **`groupId`**;UI **Location** → Query **`locationId`**。 ### 列表出参 **`items[]` 新增/对齐字段** | 字段 | 类型 | 说明 | |------|------|------| | region | string | 适用 Region 展示文案 | | location | string | 适用门店展示文案 | | regionIds | string[] | Region Id 多选;`ALL` 时为 `[]` | | locationIds | string[] | 门店 Id 多选;`ALL` 时为 `[]` | | locationText | string | **兼容字段**,与 `location` 相同 | 其它字段不变:`id`(= TemplateCode)、`templateName`、`contentsCount`、`sizeText`、`versionNo`、`lastEdited` 等。 **列表响应示例片段** ```json { "pageIndex": 1, "pageSize": 10, "totalCount": 2, "items": [ { "id": "TPL_ALL", "templateCode": "TPL_ALL", "templateName": "Global Template", "labelType": "PRICE", "region": "All Regions", "location": "All Locations", "locationText": "All Locations", "regionIds": [], "locationIds": [], "contentsCount": 5, "sizeText": "4x6inch", "versionNo": 1, "lastEdited": "2026-05-26T10:00:00" }, { "id": "TPL_TEST_001", "templateName": "Price Tag 4x6", "region": "East Region, West Region", "location": "UNCC store, Central Park Store", "locationText": "UNCC store, Central Park Store", "regionIds": ["fl_group_id_east", "fl_group_id_west"], "locationIds": [ "11111111-1111-1111-1111-111111111111", "22222222-2222-2222-2222-222222222222" ], "contentsCount": 3, "sizeText": "4x6inch", "versionNo": 2, "lastEdited": "2026-05-26T11:30:00" } ] } ``` ### 详情出参(`GET /api/app/label-template/{id}`) 在原有 `elements`、`templateProductDefaults`、`appliedLocationType` 等基础上增加: | 字段 | 类型 | 说明 | |------|------|------| | region | string | 展示文案 | | location | string | 展示文案 | | regionIds | string[] | Region Id 多选 | | groupIds | string[] | 与 `regionIds` 相同(兼容) | | locationIds | string[] | 门店 Id 多选 | | appliedLocationIds | string[] | 与 `locationIds` 一致(编辑器回显) | ### 展示规则 | appliedLocation | region | location | regionIds / locationIds | |-----------------|--------|----------|-------------------------| | **ALL** | `All Regions` | `All Locations` | 空数组 `[]` | | **SPECIFIED** | 绑定门店 `location.GroupName` 去重后 `, ` 拼接 | 门店名(优先 `LocationName`,否则 `LocationCode`)拼接 | 由绑定门店反推 / 直接为绑定 Id | | **SPECIFIED** 无绑定 | `无` | `无` | `[]` | `regionIds` 由 `locationIds` 反查 `fl_group` 得到(与 product-category、label-type 一致)。 ### 编辑说明 - `PUT` Body 字段与 `POST` 相同;传 `regionIds` / `locationIds` 会 **全量替换** 模板适用门店(先删 `fl_label_template_location` 再插入)。 - `elements` 仍为全量重建;`templateProductDefaults` 仅当 Body **显式包含** 该字段时才重建,避免普通保存误清空。 - 编辑成功 **`versionNo` +1**。 ### 联调注意 | 现象 | 处理 | |------|------| | 列表无 `regionIds` | 确认已部署含本变更的后端 | | 传 `groupId` 列表仍很多 | 正常:`appliedLocation=ALL` 的模板始终可见 | | 传 `groupId` 列表为空 | 检查 Region 是否存在、其下是否有门店;无效 Region 时仅剩 ALL 模板 | | 传了 Region 仍显示 All Locations | 检查 Region Id 是否有效、是否能在库中展开到门店 | | 仅 `appliedLocationIds` 不传 `locationIds` | 仍支持,与 `locationIds` 合并 | | 前端编辑器仍传 `appliedLocation: "ALL"` | 管理端若需多选,须在 Body 增加 `regionIds` / `locationIds`(见 `labelTemplateService.ts`) | | 指定范围但 0 门店 | 后端报错,需至少 1 个有效门店 | ### 与 product-category / label-type 的关系 逻辑与 **`5-17接口优化.md`** 中 product-category、label-type 的 Region·Location 绑定一致,差异仅为: | 模块 | 范围字段名 | 关联表 | |------|------------|--------| | product-category | `availabilityType` | `fl_product_category_location` | | label-type | `availabilityType` | `fl_label_type_location` | | **label-template** | **`appliedLocation`** | **`fl_label_template_location`** | --- ## rbac-role accessPermissions 修复 **应用服务**:`RbacRoleAppService` **影响接口**:`POST` / `PUT /api/app/rbac-role/{id}`、`GET` 列表/详情回显 ### 问题与根因 | 现象 | 根因 | |------|------| | 保存报 `accessPermissions 未匹配到任何菜单` | 前端提交 **JSON 数组字符串**(如 `["manage_labels",...]`),旧逻辑按逗号拆分,解析结果带 `["` 引号,无法匹配 | | 传 `manage_labels` 等仍无菜单 | 表单权限码为 **UI 编码**(`manage_labels`),菜单侧为 **`menu.labels`**(由 `Menu.Router` 推导);二者未做映射 | | 详情 `accessPermissionCodes` 为空 | 新增/编辑未写入 **`Role.AccessPermissionCodes`**(JSON 列),仅依赖 `RoleMenu` 反查 | ### 变更说明 | 项 | 变更后 | |----|--------| | **入参解析** | `accessPermissions` 支持 **JSON 数组字符串**、逗号分隔、以及 Body 字段 **`accessPermissionCodes`** 数组 | | **菜单绑定** | UI 权限码经 **`RoleAccessPermissionMenuMapping`** 映射到 `Menu.Router`,再写入 **`RoleMenu`** | | **落库** | 同时将勾选的 UI 编码写入 **`Role.AccessPermissionCodes`**(JSON 数组),供 GET 回显 | | **PermissionCode 为空** | 仍可按 **`Router`** 推导 `menu.xxx`(建议执行 `menu_backfill_permission_code.sql`) | ### UI 权限码 → 菜单 Router 映射(当前库) | accessPermissions(UI) | 绑定菜单 Router | |-------------------------|-----------------| | `manage_labels` | `/labeling`、`/labels`、`/label-categories`、`/label-types`、`/label-templates` | | `manage_people` | `/account-management` | | `edit_settings` | `/menu-management`、`/multiple-options` | | `view_reports` | `/reports` | | `manage_products` | (当前 `Menu` 表无 Products 路由,勾选不绑定菜单,**不单独报错**) | | `approve_batches` | (当前无对应菜单路由,同上) | > 至少 **1 个** 权限码能匹配到菜单即保存成功;若 **全部** 均无法匹配(例如只勾 `manage_products` 且库中无对应菜单),仍返回业务错误。 ### 请求示例(与前端一致) ```http PUT /api/app/rbac-role/3a1f077b-3665-63f2-5fea-0fd7e7044b88 Content-Type: application/json Authorization: Bearer {token} ``` ```json { "roleName": "Partner Admin", "roleCode": "admin", "remark": "Admin", "dataScope": 0, "state": true, "orderNum": 999, "accessPermissions": "[\"manage_labels\",\"edit_settings\",\"view_reports\",\"manage_people\",\"manage_products\",\"approve_batches\"]" } ``` 也可使用逗号分隔(旧格式): ```json { "accessPermissions": "manage_labels, view_reports, manage_people" } ``` 或同时传数组字段(与 `accessPermissions` 合并去重): ```json { "accessPermissionCodes": ["manage_labels", "view_reports"] } ``` ### 入参优先级(与 5-18 一致) | menuIds | accessPermissions / accessPermissionCodes | 行为 | |---------|-------------------------------------------|------| | 非空数组 | 任意 | **以 menuIds 为准** | | 不传 | 非空 | 按 UI 权限码映射菜单并覆盖 `RoleMenu` | | 不传 | `""` 或空数组 | 清空 `RoleMenu` 与 `AccessPermissionCodes` | | `[]` | 不传 | 清空绑定 | ### 响应回显 | 字段 | 说明 | |------|------| | `accessPermissionCodes` | 来自 **`Role.AccessPermissionCodes`**,如 `["manage_labels","view_reports"]` | | `accessPermissions` | 已绑定菜单的 **`menu.xxx`** 汇总(逗号拼接,只读展示) | | `menuIds` | 已绑定菜单 Guid 列表(`RoleMenu`) | ### 数据库准备(推荐) ```bash 美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/scripts/menu_backfill_permission_code.sql ``` ### 联调注意 | 现象 | 处理 | |------|------| | 仍报未匹配到菜单 | 确认已部署本修复;检查 `Menu` 是否存在上表 Router | | 只勾 Products/Batches 报错 | 当前库无对应菜单属预期;请同时勾选 Labels/Reports 等 | | 同时传 `menuIds: []` | **menuIds 优先**,会清空绑定并忽略 accessPermissions | > 更完整的 RBAC 说明见 **`5-18接口优化.md`** → rbac-role 章节。 --- ## auth-scope 登录后 Company · Region · Location 级联选店 **应用服务**:`AuthScopeAppService` **适用场景**:Web `POST /api/app/account/login` 或 App `POST /api/app/us-app-auth/login` 取得 Token 后,**管理员**无 `userlocation` 绑定时需先选工作门店;亦支持非管理员在数据范围内级联选择(须已绑定该门店)。 **命名约定**(与 5-17 一致):UI **Company** = **`partnerId`**(`fl_partner.Id`);UI **Region** = **`groupId`**(`fl_group.Id`);UI **Location** = **`locationId`**(`location.Id`,Guid 字符串)。 ### 接口一览 | 步骤 | 方法 | 路径 | 说明 | |------|------|------|------| | 1 | GET | `/api/app/auth-scope/companies` | 可选公司列表 | | 2 | GET | `/api/app/auth-scope/regions?partnerId={partnerId}` | 指定公司下 Region | | 3 | GET | `/api/app/auth-scope/locations?partnerId={partnerId}&groupId={groupId}` | 指定公司+Region 下门店 | | 4 | POST | `/api/app/auth-scope/select-location` | 确认当前工作门店 | | — | GET | `/api/app/auth-scope/current-scope` | 查询已选工作门店(未选返回 `null`) | **鉴权**:均需 `Authorization: Bearer {token}`。 ### 数据范围 | 角色 | Company | Region | Location | |------|---------|--------|----------| | **管理员**(`admin` / 用户名 `admin` / 权限 `*:*:*`) | 全部未删除公司 | 该公司下全部 Region | 该 Region 下全部门店(`location.Partner` + `location.GroupName` 与 `fl_group` 一致) | | **非管理员** | `userlocation` 绑定门店所属公司 | 绑定门店对应 Region | 上述 Region 内且符合 `LocationRegionScopeHelper` 的门店;**选店时**须已绑定该 `locationId` | ### 1)公司列表 ```http GET /api/app/auth-scope/companies HTTP/1.1 Authorization: Bearer {token} ``` **响应**:`AuthScopeCompanyOptionDto[]` ```json [ { "id": "fl_partner_id_1", "partnerName": "Acme Foods", "state": true } ] ``` ### 2)Region 列表 ```http GET /api/app/auth-scope/regions?partnerId=fl_partner_id_1 HTTP/1.1 Authorization: Bearer {token} ``` **响应**:`AuthScopeRegionOptionDto[]` ```json [ { "id": "fl_group_id_east", "groupName": "East Region", "partnerId": "fl_partner_id_1", "state": true } ] ``` ### 3)门店列表 ```http GET /api/app/auth-scope/locations?partnerId=fl_partner_id_1&groupId=fl_group_id_east HTTP/1.1 Authorization: Bearer {token} ``` **响应**:`AuthScopeLocationOptionDto[]`(含 `fullAddress`、`groupName` 等) ### 4)确认选店(与现有 App 逻辑对齐) ```http POST /api/app/auth-scope/select-location HTTP/1.1 Authorization: Bearer {token} Content-Type: application/json ``` ```json { "partnerId": "fl_partner_id_1", "groupId": "fl_group_id_east", "locationId": "a2696b9e-2277-11f1-b4c6-00163e0c7c4f" } ``` **响应**:`AuthScopeSelectLocationOutputDto` | 字段 | 说明 | |------|------| | partnerId / partnerName | 所选公司 | | groupId / groupName | 所选 Region | | location | 与 **`UsAppBoundLocationDto`** 相同(`id`、`locationCode`、`locationName`、`fullAddress`、`state`) | **选店后的服务端行为**(无需改前端即可对接 App): | 能力 | 行为 | |------|------| | **工作范围缓存** | 写入分布式缓存(24h);退出 `POST /api/app/auth-session/logout` 时清除 | | **`GET /api/app/us-app-auth/my-locations`** | 管理员在缓存选店后,列表 **合并** 该门店(与 `userlocation` 并集) | | **`GET .../location-detail/{locationId}`** | 管理员可不依赖 `userlocation` 访问已选门店(`UsAppPrintLogScopeHelper.EnsureUserCanAccessLocationAsync`) | | **App 打印/报表** | 仍传 `locationId`;权限规则不变(见 `5-18接口优化.md`) | ### 5)当前工作范围 ```http GET /api/app/auth-scope/current-scope HTTP/1.1 Authorization: Bearer {token} ``` 未选店时响应体为 **`null`**(HTTP 200)。 ### 联调注意 | 现象 | 处理 | |------|------| | `regions` 为空 | 公司下无 `fl_group` 或当前账号无 Region 数据范围 | | `locations` 为空 | 门店 `Partner` / `GroupName` 未与 `fl_partner`、`fl_group` 对齐 | | 选店报「门店与所选公司/区域不匹配」 | 检查 `location.Partner`、`location.GroupName` | | 非管理员选店报未绑定 | 须在 **Team Member** 中为该账号绑定该门店 | | 选店后 `my-locations` 仍为空 | 确认已调 `select-location` 且 Token 为管理员身份 | > Web 管理端报表等模块仍可按 Query 传 `partnerId` / `groupId` / `locationId` 收窄;本组接口主要解决 **登录后选工作门店** 与 **App 门店列表** 一致性问题。 --- ## us-app-auth App 管理员级联选店 **应用服务**:`UsAppAuthAppService` **适用场景**:App 使用 **`POST /api/app/us-app-auth/login`** 登录后,持 **管理员** 身份(`admin` 角色 / 用户名 `admin` / 权限 `*:*:*`)且 JWT 含 **`client_kind=us-app`**,按 Company → Region 筛选门店。 **与 `auth-scope` 关系**:查询逻辑共用 `AuthScopeQueryHelper`;App 侧路径统一在 **`us-app-auth`** 下,并 **强制 App Token + 管理员**,避免误用 Web Token。 ### 接口一览 | 步骤 | 方法 | 路径 | 说明 | |------|------|------|------| | 0 | POST | `/api/app/us-app-auth/login` | 获取 App Token(须管理员账号) | | 1 | GET | `/api/app/us-app-auth/admin-scope-companies` | 公司列表 → 取 `id` 作 `partnerId` | | 2 | GET | `/api/app/us-app-auth/admin-scope-regions?partnerId={partnerId}` | Region 列表 → 取 `id` 作 `groupId` | | 3 | GET | `/api/app/us-app-auth/admin-scope-locations?partnerId={partnerId}&groupId={groupId}` | **按公司与 Region Id 筛选门店** | | 4 | POST | `/api/app/us-app-auth/select-admin-scope-location` | 确认工作门店 | | — | GET | `/api/app/us-app-auth/my-locations` | 选店后刷新绑定门店(含缓存门店) | **鉴权**:步骤 1–4 须 Header `Authorization: Bearer {App登录返回的token}`。 ### 前置条件 | 项 | 要求 | |----|------| | Token 来源 | 必须来自 **`/api/app/us-app-auth/login`**(非 Web `/api/app/account/login`) | | JWT 声明 | `client_kind` = `us-app` | | 角色 | 平台管理员(`ReportsRoleHelper.IsAdminRole`) | | 违反时 | `请使用 App 登录令牌调用该接口` 或 `仅管理员可使用公司/区域/门店筛选接口` | ### 1)公司列表 ```http GET /api/app/us-app-auth/admin-scope-companies HTTP/1.1 Authorization: Bearer {app_token} ``` **响应**:`AuthScopeCompanyOptionDto[]`(与 auth-scope 相同) ```json [ { "id": "fl_partner_id_1", "partnerName": "Acme Foods", "state": true } ] ``` ### 2)Region 列表 ```http GET /api/app/us-app-auth/admin-scope-regions?partnerId=fl_partner_id_1 HTTP/1.1 Authorization: Bearer {app_token} ``` **响应**:`AuthScopeRegionOptionDto[]` ```json [ { "id": "fl_group_id_east", "groupName": "East Region", "partnerId": "fl_partner_id_1", "state": true } ] ``` ### 3)门店列表(按 partnerId + groupId 筛选) ```http GET /api/app/us-app-auth/admin-scope-locations?partnerId=fl_partner_id_1&groupId=fl_group_id_east HTTP/1.1 Authorization: Bearer {app_token} ``` **Query** | 参数 | 必填 | 说明 | |------|------|------| | partnerId | 是 | 公司 Id(`fl_partner.Id`) | | groupId | 是 | Region Id(`fl_group.Id`) | **响应**:`AuthScopeLocationOptionDto[]` ```json [ { "id": "a2696b9e-2277-11f1-b4c6-00163e0c7c4f", "locationCode": "LOC-1", "locationName": "Downtown Kitchen", "fullAddress": "123 Main St, New York, NY 10001", "state": true, "partnerId": "fl_partner_id_1", "groupId": "fl_group_id_east", "groupName": "East Region" } ] ``` 筛选规则:`location.Partner` 匹配该公司(Id 或名称),且 `location.GroupName` 与所选 `fl_group.GroupName` 一致。 ### 4)确认选店 ```http POST /api/app/us-app-auth/select-admin-scope-location HTTP/1.1 Authorization: Bearer {app_token} Content-Type: application/json ``` ```json { "partnerId": "fl_partner_id_1", "groupId": "fl_group_id_east", "locationId": "a2696b9e-2277-11f1-b4c6-00163e0c7c4f" } ``` **响应**:`AuthScopeSelectLocationOutputDto`(含 `location` 节点,结构同 `UsAppBoundLocationDto`) ### 推荐调用顺序(App) ```text POST /api/app/us-app-auth/login → GET admin-scope-companies → GET admin-scope-regions?partnerId=... → GET admin-scope-locations?partnerId=...&groupId=... → POST select-admin-scope-location → GET my-locations → 后续业务接口传 locationId(打印、报表等,规则不变) ``` ### 联调注意 | 现象 | 处理 | |------|------| | 报「请使用 App 登录令牌」 | 勿用 Web `account/login` 的 Token;须重新 App 登录 | | 报「仅管理员可使用」 | 换管理员账号或绑定 `admin` 角色 | | `locations` 为空 | 核对门店 `Partner`、`GroupName` 与 `fl_partner`、`fl_group` | | 与 auth-scope 重复 | App 端 **优先** 使用本节前缀;Web 端用 `auth-scope` | --- ## 变更记录 | 日期 | 说明 | |------|------| | 2026-05-26 | us-app-auth:App 管理员 `admin-scope-companies/regions/locations`、`select-admin-scope-location` | | 2026-05-26 | auth-scope:登录后 Company/Region/Location 级联选店;选店缓存;`my-locations` / 门店详情与管理员选店对齐 | | 2026-05-26 | product-category:`categoryCode` 新增/编辑改为可选 | | 2026-05-26 | label-template:新增/编辑/列表/详情支持 `regionIds`、`locationIds` 及 `region`、`location` 展示 | | 2026-05-26 | label-template 列表 Query 增加 `groupId`(Region)、`locationId`(门店)筛选 | | 2026-05-26 | rbac-role:支持 accessPermissions JSON 数组 + UI 权限码映射 Menu;落库 AccessPermissionCodes |