# 6-30 代码优化 本文档说明 **2026-06-30** 对美国版接口的两项优化: 1. **Company Admin** 在其绑定 Company 下新建 Region 时,自动将新 Region 绑定到当前账号(写入 `userlocation`)。 2. **Product / Label Type** 分页列表增加 **`locationName`**(绑定门店名称)出参。 3. **App Preview / Print Log / Label Report**:`labelId` 统一为 `yyyyMMdd-n`;Print Log 与 Label Report 按门店展示**全部用户**打印记录。 --- ## 一、需求背景 | 项目 | 说明 | |------|------| | 角色 | **Company Admin**(库内 `RoleCode = CompanyAdmin`,`RoleName = Company Admin`) | | 场景 | 在 Account Management → Region 页签,于**已绑定 Company** 下新增 Region | | 期望 | 新建 Region 后,当前账号应**默认绑定**该 Region,无需再手动维护成员门店范围 | | 关联接口 | `GET/POST /api/app/group` | ### 为何需要写 `userlocation` 系统成员与数据范围以 **`userlocation`(用户 ↔ 门店)** 为主键路径: - **Region 列表**(`GET /api/app/group`):Company Admin 已按 `fl_group.PartnerId` 展开可见该公司下全部 Region(见 `6-18代码优化.md` 第十五节)。 - **门店列表 / auth-scope / 打印范围** 等:仍按 `userlocation` 反推 `location.Partner + location.GroupName` 判断 Region 可见性。 因此仅写入 `fl_group` 不足以让新 Region 在「按门店绑定推导」的链路中立即可用;需在新建 Region 时**追加**该 Region 下门店的 `userlocation` 绑定。 --- ## 二、行为说明 ### 1. 触发条件(同时满足) | # | 条件 | |---|------| | 1 | 当前用户角色为 **Company Admin**(`TeamMemberRoleHelper.IsCompanyAdminUserAsync`) | | 2 | 非平台管理员(`admin` 不自动改 `userlocation`) | | 3 | 新建 Region 的 `partnerId` 属于当前账号已绑定 Company(由 `userlocation` 反推 `fl_partner.Id`) | | 4 | 接口已通过 `EnsurePartnerIdAllowedForRegionMutationAsync` 权限校验 | ### 2. 绑定规则 | 步骤 | 动作 | |------|------| | 1 | `POST /api/app/group` 成功插入 `fl_group` | | 2 | 按 `partnerId` 查 `fl_partner.PartnerName`,与新建 `groupName` 匹配 `location` 表:`Partner = PartnerName` 且 `GroupName = groupName` | | 3 | 将匹配到的门店 Id **追加**写入 `userlocation`(**不删除**已有绑定;已绑门店幂等跳过) | ### 3. 空 Region(尚无门店) 新建 Region 时若该公司下尚无同名 `GroupName` 的门店,则本次**不会**新增 `userlocation` 行(无门店可绑)。 **补充**:Company Admin 随后通过 `POST /api/app/location` 在同一 Company / Region 下新建首家门店时,后端会**自动**将该门店追加绑定到当前账号(与 Region 新建逻辑共用 `LocationScopeBindingHelper`)。 ### 4. 不受影响 | 角色 / 场景 | 说明 | |-------------|------| | 平台 `admin` | 不写入 `userlocation` | | Regional Admin / Location Manager 等 | 不自动扩绑;仍按 Team Member 维护范围 | | `PUT /api/app/group` 编辑 Region | 不触发自动绑定 | | 在Snapshot 已有绑定 | 只追加缺失门店,不覆盖、不删除 | --- ## 三、接口对接 ### 1. Region 分页列表 | 项目 | 内容 | |------|------| | 方法 | `GET` | | 路径 | `/api/app/group` | **Query 参数** | 参数 | 类型 | 必填 | 说明 | |------|------|------|------| | `SkipCount` | int | 是 | 页码,**从 1 起** | | `MaxResultCount` | int | 是 | 每页条数 | | `Sorting` | string | 否 | 如 `CreationTime desc` | | `Keyword` | string | 否 | Region 名 / Company 名模糊 | | `PartnerId` | string | 否 | 按 Company(`fl_partner.Id`)筛选 | | `State` | bool | 否 | 启用状态 | **Company Admin 数据范围** - 返回其绑定 Company(`userlocation` → `fl_partner.Id`)下的**全部** Region。 - 新建 Region 后**无需重新登录**即可在列表中看到(列表本身不依赖新 Region 是否已写 `userlocation`)。 **curl 示例** ```bash curl -s -G "http://localhost:19001/api/app/group" \ -H "Authorization: " \ --data-urlencode "SkipCount=1" \ --data-urlencode "MaxResultCount=10" \ --data-urlencode "Sorting=CreationTime desc" ``` --- ### 2. 新增 Region | 项目 | 内容 | |------|------| | 方法 | `POST` | | 路径 | `/api/app/group` | | Content-Type | `application/json` | **Body** | 字段 | 类型 | 必填 | 说明 | |------|------|------|------| | `groupName` | string | 是 | Region 名称,对应 `fl_group.GroupName` / `location.GroupName` | | `partnerId` | string | 是 | 所属 Company(`fl_partner.Id`) | | `state` | bool | 否 | 默认 `true` | **Body 示例** ```json { "groupName": "Subway Guangzhou", "partnerId": "3a218xxx-company-id", "state": true } ``` **成功响应**:`GroupGetOutputDto`(含 `id`、`groupName`、`partnerId`、`partnerName`、`state`、`creationTime`)。 **Company Admin 副作用(2026-06-30)** - 若 `partnerId` 为当前账号已绑定 Company,且存在 `location.Partner + location.GroupName` 与新建 Region 一致的门店,则这些门店写入 `userlocation`。 - 前端**无需**额外调用 Team Member 接口。 **curl 示例** ```bash curl -s -X POST "http://localhost:19001/api/app/group" \ -H "Authorization: " \ -H "Content-Type: application/json" \ -d "{\"groupName\":\"Subway Guangzhou\",\"partnerId\":\"\",\"state\":true}" ``` --- ### 3. 验证清单 | # | 步骤 | 期望 | |---|------|------| | 1 | Company Admin 登录,确认 `userlocation` 已有该公司下至少 1 家门店 | 有绑定 Company | | 2 | `POST /api/app/group` 新建 Region(`partnerId` = 该公司) | 200,返回新 `id` | | 3 | `GET /api/app/group?SkipCount=1&MaxResultCount=10&Sorting=CreationTime desc` | 列表含新 Region | | 4 | 若该公司下已有同名 `GroupName` 的门店 | 查库 `userlocation` 新增对应 `LocationId` | | 5 | `GET /api/app/auth-scope/regions?partnerId=...` | 下拉含新 Region | | 6 | 空 Region 下 `POST /api/app/location` 创建首家门店 | 当前账号 `userlocation` 自动追加该门店 | **查库 SQL** ```sql -- 新建 Region 后:当前用户是否绑定该 Region 下门店 SELECT ul.* FROM userlocation ul INNER JOIN location loc ON loc.Id = ul.LocationId AND loc.IsDeleted = 0 INNER JOIN fl_partner p ON p.PartnerName = loc.Partner AND p.IsDeleted = 0 WHERE ul.IsDeleted = 0 AND ul.UserId = '' AND p.Id = '' AND loc.GroupName = ''; ``` --- ## 四、代码改动 | 文件 | 改动 | |------|------| | `Helpers/LocationScopeBindingHelper.cs` | 新增 `TryAutoBindCompanyAdminToNewRegionAsync`、`TryAutoBindCompanyAdminToNewLocationAsync`、`AppendUserLocationBindingsAsync` | | `Services/GroupAppService.cs` | `CreateAsync` 插入 `fl_group` 后调用 Region 自动绑定 | | `Services/LocationAppService.cs` | `CreateAsync` 插入门店后 Company Admin 自动追加 `userlocation`(空 Region 补门店) | | `IServices/IGroupAppService.cs` | 补充 `CreateAsync` XML 说明 | --- ## 六、Product / Label Type 列表增加 `locationName` ### 需求 | 接口 | 说明 | |------|------| | `GET /api/app/product` | 列表每行返回产品绑定的**门店名称** | | `GET /api/app/label-type` | 列表每行返回类型绑定的**门店名称** | 与 `/api/app/label` 列表的 `locationName` 字段命名一致,前端可直接展示 Location 列,无需再单独拉 `product-location` 或全量门店目录反查。 --- ### 1. Product 分页列表 | 项目 | 内容 | |------|------| | 方法 | `GET` | | 路径 | `/api/app/product` | **Query 示例** ``` SkipCount=1&MaxResultCount=10 ``` **新增出参字段(`items[]`)** | 字段 | 类型 | 说明 | |------|------|------| | `locationName` | string | 绑定门店名称,英文逗号拼接(`location.LocationName`,无名称时回退 `LocationCode`);**无绑定**时为 `无` | | `locationIds` | string[] | 绑定门店 Id(`fl_location_product` → `location.Id`) | **数据来源** ``` fl_product └── fl_location_product (ProductId + LocationId) └── location (LocationName / LocationCode) ``` **响应片段示例** ```json { "totalCount": 2, "items": [ { "id": "prod_001", "productName": "Chicken Wrap", "categoryName": "Sandwich", "noOfLabels": 3, "locationIds": ["3a220c4f-976e-9dcc-186b-ad2b90a9a4ef"], "locationName": "Subway Beijing Store 01" } ] } ``` **curl** ```bash curl -s -G "http://localhost:19001/api/app/product" \ -H "Authorization: " \ --data-urlencode "SkipCount=1" \ --data-urlencode "MaxResultCount=10" ``` --- ### 2. Label Type 分页列表 | 项目 | 内容 | |------|------| | 方法 | `GET` | | 路径 | `/api/app/label-type` | **Query 示例** ``` SkipCount=1&MaxResultCount=10 ``` **新增出参字段(`items[]`)** | 字段 | 类型 | 说明 | |------|------|------| | `locationName` | string | 与既有 `location` **同值**;`availabilityType=ALL` 时为 `All Locations`;`SPECIFIED` 且无绑定时为 `无` | | `location` | string | 保留兼容;内容与 `locationName` 一致 | | `locationIds` | string[] | 已有字段;`fl_label_type_location` 绑定的门店 Id | **数据来源** ``` fl_label_type └── fl_label_type_location (LabelTypeId + LocationId) └── location (LocationName / LocationCode) ``` **展示规则** | `availabilityType` | `locationName` | |--------------------|----------------| | `ALL` | `All Locations` | | `SPECIFIED` + 有绑定 | 门店名去重后英文逗号拼接 | | `SPECIFIED` + 无绑定 | `无` | **响应片段示例** ```json { "totalCount": 1, "items": [ { "id": "lt_price", "typeName": "Price Label", "availabilityType": "SPECIFIED", "locationIds": ["3a220c4f-976e-9dcc-186b-ad2b90a9a4ef"], "location": "Subway Beijing Store 01", "locationName": "Subway Beijing Store 01", "region": "Subway Beijing", "noOfLabels": 12 } ] } ``` **curl** ```bash curl -s -G "http://localhost:19001/api/app/label-type" \ -H "Authorization: " \ --data-urlencode "SkipCount=1" \ --data-urlencode "MaxResultCount=10" ``` --- ### 3. 代码改动 | 文件 | 改动 | |------|------| | `Dtos/Product/ProductGetListOutputDto.cs` | 新增 `LocationName`、`LocationIds` | | `Services/ProductAppService.cs` | `GetListAsync` 批量读 `fl_location_product` 并汇总门店名;Excel 导出复用 `BuildProductLocationDisplayMapsAsync` | | `Dtos/LabelType/LabelTypeGetListOutputDto.cs` | 新增 `LocationName` | | `Services/LabelTypeAppService.cs` | 列表映射 `LocationName = Location` | --- ## 八、App Preview / Print Log / Label Report 修复 ### 问题 | # | 现象 | 根因 | |---|------|------| | 1 | `preview` 出参 `labelId` 为 GUID | 误返回 `fl_label.Id`,未用当日序号 | | 2 | `get-print-log-list` 的 `labelId` 格式不对 | 误返回 `fl_label_print_task.LabelId`(标签主键) | | 3 | Print Log 仅当前用户记录 | `restrictToCreator=true`(非 Partner 角色) | | 4 | Label Report 统计仅当前用户 | 同上 | ### 业务规则(统一) **Label ID** = 某门店某自然日内打印任务递增序号:`{yyyyMMdd}-{n}`(如 `20260707-1`、`20260707-2`)。 | 范围 | 统计维度 | |------|----------| | 同一 `locationId` | 同一自然日(`PrintedAt ?? CreationTime`) | | 排序 | 时间升序,再按任务 `Id` 升序 | | 预览 | 不落库,返回「下一序号」= 已有任务数 + 1 | **Print Log / Label Report 数据范围**:已绑定该门店的账号 → 查看该门店**全部用户**的打印任务(不按 `CreatedBy` 过滤)。 ### 1. Preview | 项目 | 内容 | |------|------| | 方法 | `POST` | | 路径 | `/api/app/us-app-labeling/preview` | **出参 `labelId`**:`yyyyMMdd-n`(下一序号);`printLabelDisplayId`:模板含 Label ID 控件时同步写入画布。 ### 2. Print Log | 项目 | 内容 | |------|------| | 方法 | `POST` | | 路径 | `/api/app/us-app-labeling/get-print-log-list` | **入参** | 字段 | 说明 | |------|------| | `locationId` | 必填 | | `printDateDay` | 可选,`yyyy-MM-dd`,按自然日筛选 | | `skipCount` / `maxResultCount` | 分页(SkipCount 从 1 起) | **出参 `items[]`** | 字段 | 说明 | |------|------| | `labelId` | 门店当日序号 `yyyyMMdd-n` | | `labelEntityId` | `fl_label.Id`(内部关联) | | `labelCode` | `fl_label.LabelCode`(重打/预览入参) | | `operatorName` | 实际打印人 | ### 3. Label Report | 项目 | 内容 | |------|------| | 方法 | `POST` | | 路径 | `/api/app/us-app-labeling/get-label-report` | 统计该 `locationId` 下**全部用户**在 `startDate`~`endDate` 内的打印量(与 Print Log 同范围)。 ### 代码改动 | 文件 | 改动 | |------|------| | `UsAppLabelingAppService.cs` | Preview `labelId`;Print Log 序号 + `printDateDay` + 全店范围;Report 全店范围;Reprint 同店可重打 | | `UsAppPrintLogScopeHelper.cs` | 注释更新 | | `PrintLogGetListInputVo.cs` / `IUsAppLabelingAppService.cs` | 接口说明 | --- ## 九、关联文档 - Company Admin Region 列表范围:`项目相关文档/6-18代码优化.md`(第十五节) - Team Member Company Admin 范围规则:`项目相关文档/6-18代码优化.md`(第十二节) - group 列表 Token 范围(历史):`项目相关文档/5-17接口优化.md`(group Region 列表与 PDF 导出权限) - Product 门店绑定(历史):`项目相关文档/5-17接口优化.md`(product 列表筛选) - App Label ID 规则(历史):`项目相关文档/6-11代码优化.md`、`6-2代码优化.md`