# 2026-07-24 Team Member 范围 ALL 哨兵(美国版后端) ## 背景 New User / Edit User 弹窗中 Locations、Region 支持勾选 **ALL**。前端可传 `locationIds: ["ALL"]`(或 `regionIds: ["ALL"]`),后端需按当前 Company(`partnerId` / `partnerIds`)展开并落库到 `userlocation`;列表 **Assigned Locations** 覆盖全部门店时展示 **`All Location`**,Region 覆盖全部时展示 **`All Region`**。 本次**仅改美国版后端**,不改 Web 前端。 --- ## 接口 | 方法 | 路径 | 说明 | |------|------|------| | POST | `/api/app/team-member` | 新增成员 | | PUT | `/api/app/team-member/{id}` | 更新成员 | 批量编辑 `PUT /api/app/team-member/team-members-bulk` 单行字段与单条 PUT 相同,同样支持 ALL。 --- ## ALL 传参约定 ### 哨兵值 - 字符串 **`ALL`**(大小写不敏感:`ALL` / `all` / `All`) - 适用字段:`locationIds`、`regionIds`、`groupIds`(与 `regionIds` 等价)、`locations`(与 `locationIds` 合并解析) ### 与具体 Guid 同传 若数组中同时含 `ALL` 与具体门店/Region Guid,**以 ALL 为准**(视为全选,忽略具体 Id)。 ### 优先级(Team Member 落库) **不再做 partner + region + location 并集**;按下列顺序取**单一**规则展开: 1. **`locationIds` 含 `ALL`** → 按 `partnerId` / `partnerIds` 展开该公司**全部门店** 2. **有具体 `locationIds`,且 `regionIds` 为空或为 `ALL`** → **只绑这些门店**(Region=ALL 下再收窄到指定门店;避免 `regionIds:["ALL"]` 盖掉单店) 3. **`regionIds` 含 `ALL`**(且无具体门店)→ 按 Company 展开该公司**全部 Region** 下门店 4. **`regionIds` 有具体 Guid** → **按这些 Region 展开门店落库**(**忽略同传的 `locationIds`**,保证多区域能落库回显) 5. **仅有具体 `locationIds`** → 只绑定指定门店 6. **仅有 `partnerId` / `partnerIds`**(Company Admin 等)→ 绑定该公司全部门店 | 请求示例 | 落库规则 | |----------|----------| | `partnerId` + `locationIds: ["ALL"]` | 展开该公司**全部**门店写入 `userlocation` | | `partnerId` + `regionIds: ["ALL"]` + `locationIds: ["{loc}"]` | **只绑 `{loc}`**(Region ALL + 具体门店时门店优先) | | `partnerId` + `regionIds: ["ALL"]`(无 location) | 展开该公司**全部 Region** 下门店 | | `partnerId` + `regionIds: ["{groupGuid1}", "{groupGuid2}"]` | 展开两 Region 下门店并集 | | `partnerId` + `regionIds: ["{g1}","{g2}"]` + `locationIds: ["{loc}"]` | **按 Region 展开**(忽略 `{loc}`),`userlocation` 为两 Region 门店并集;Get 回显 `regionIds` 含 2 个 | | `partnerId` + `locationIds: ["{guid1}", "{guid2}"]`(无 region) | 仅绑定指定门店 | | Company Admin 仅传 `partnerId`(无 region/location) | 仍自动绑定该公司全部门店(原逻辑) | **注意**:`locationIds` / `regionIds` 含 ALL 时须传 **`partnerId` 或 `partnerIds`**,否则返回 400。 ### 编辑回显(GET 折叠为 ALL) 库表 `userlocation` **仍存展开后的门店 Guid**。`GET /api/app/team-member/{id}`(及列表同名字段)在判断为「已覆盖该公司全部」时,将 Id 数组折叠为哨兵,与新增传 ALL 对称: | 条件 | 出参 | |------|------| | 绑定门店 = 该公司全部门店 | `locationIds: ["ALL"]`,`assignedLocations` 展示 All Location | | 反推 Region = 该公司全部 Region | `regionIds` / `groupIds: ["ALL"]` | | 未全选 | 仍返回具体 Guid 数组 | 示例(新增时传 `regionIds/locationIds: ["ALL"]` 后,再 GET): ```json { "partnerIds": ["3a22a48c-9758-9a26-e4ee-390b0f042835"], "regionIds": ["ALL"], "groupIds": ["ALL"], "locationIds": ["ALL"] } ``` --- ## 请求 / 响应示例 ### 1. Locations 全选(ALL) **请求** ```json POST /api/app/team-member { "fullName": "Test All Loc", "userName": "test.all.loc@example.com", "password": "Pass123!", "email": "test.all.loc@example.com", "roleId": "ROLE_GUID", "partnerId": "3a222876-7c49-6f01-c232-2d21dde8f27e", "locationIds": ["ALL"], "state": true } ``` **落库**:`userlocation` 写入 Partner `Subway USA` 下全部未删除门店 Id。 **列表/详情 `assignedLocations`**(覆盖全部门店时): ```json "assignedLocations": [ { "locationName": "All Location" } ] ``` ### 2. 仅指定门店 Guid(对比) **请求** ```json "partnerId": "3a222876-7c49-6f01-c232-2d21dde8f27e", "locationIds": [ "3a222878-b4c1-d27b-d437-93da14886350", "3a22287c-d353-da47-869f-0bb5dacced51" ] ``` **列表 `assignedLocations`**:展示具体名称,如 `Subway LAX Store`、`Subway UNCC Store`,**不会**出现 `All Location`。 ### 3. Region 全选(ALL) **请求** ```json "partnerId": "3a222876-7c49-6f01-c232-2d21dde8f27e", "regionIds": ["ALL"] ``` **落库**:该公司下所有 Region 对应门店写入 `userlocation`。 **Region 展示**:绑定覆盖该公司全部门店时,列表 Region 文案为 **`All Region`**(`FoodLabelingDisplayConsts.AllRegion`)。 ### 4. 编辑多 Region + 同传 location(按 Region 展开) **场景**:UI 勾选 2 个 Region,请求体可能仍带 1 个 `locationIds`(历史选中项)。后端**以 Region 为准**展开,保证多区域能落库并回显。 **请求** ```json PUT /api/app/team-member/{id} { "fullName": "张三", "userName": "123", "email": "123@qq.com", "roleId": "3a21836a-cc04-1783-b054-f2ff3c45d4bf", "partnerId": "3a22a48c-9758-9a26-e4ee-390b0f042835", "regionIds": [ "3a22a48d-98be-7d54-7b9d-6062afb176be", "3a22a48d-5356-8754-d839-a6b7aaad02f9" ], "groupIds": [ "3a22a48d-98be-7d54-7b9d-6062afb176be", "3a22a48d-5356-8754-d839-a6b7aaad02f9" ], "locationIds": ["3a22a48f-4114-ad64-11bf-9adb09f999b9"], "state": true } ``` **落库**:`userlocation` 写入两 Region 下门店并集(**不是**仅 `{locationIds}` 那一店)。 **Get 回显**:`regionIds` 含上述 2 个 Region(由 `userlocation` → 门店 → `fl_group` 反推)。 --- ## 列表展示规则 | 条件 | Assigned Locations | Region(列表/PDF) | |------|-------------------|-------------------| | `userlocation` 覆盖该成员 Company 下**全部门店** | `All Location` | 全部 Region 时 `All Region` | | 仅部分门店 | 门店 Code/Name,分号拼接 | 具体 Region 名称 | 实现位置: - 落库展开:`LocationScopeBindingHelper.ResolveTeamMemberLocationIdsForSaveAsync` - 展示折叠:`TeamMemberScopeDisplayHelper.FormatAssignedLocationsForListAsync` / `FormatRegionTextForListAsync` - 全量回显:`TeamMemberAppService.ApplyCompanyAdminDisplayScopeAsync`(绑定覆盖全部门店时展开 Region) --- ## curl 示例 ### 获取 Token ```bash curl -X POST "http://localhost:19001/api/oauth/Login" \ -H "Content-Type: application/x-www-form-urlencoded" \ -d "userName=admin&password=123456" ``` 将响应中 `data.token`(已含 `Bearer ` 前缀)用于后续 `Authorization` 头。 ### 新增 — Locations ALL ```bash curl -X POST "http://localhost:19001/api/app/team-member" \ -H "Authorization: " \ -H "Content-Type: application/json" \ -d "{ \"fullName\": \"All Loc User\", \"userName\": \"all.loc.user@example.com\", \"password\": \"Pass123!\", \"email\": \"all.loc.user@example.com\", \"roleId\": \"\", \"partnerId\": \"3a222876-7c49-6f01-c232-2d21dde8f27e\", \"locationIds\": [\"ALL\"], \"state\": true }" ``` ### 更新 — Region ALL ```bash curl -X PUT "http://localhost:19001/api/app/team-member/" \ -H "Authorization: " \ -H "Content-Type: application/json" \ -d "{ \"fullName\": \"All Region User\", \"userName\": \"all.region.user@example.com\", \"roleId\": \"\", \"partnerId\": \"3a222876-7c49-6f01-c232-2d21dde8f27e\", \"regionIds\": [\"ALL\"], \"state\": true }" ``` ### 查询列表验证 Assigned Locations ```bash curl -X GET "http://localhost:19001/api/app/team-member?SkipCount=0&MaxResultCount=20" \ -H "Authorization: " ``` 检查 `items[].assignedLocations` 与 `items[].locationIds`。 --- ## 改动文件 | 文件 | 变更 | |------|------| | `FoodLabeling.Application/Helpers/LocationScopeBindingHelper.cs` | ALL 哨兵识别;`ResolveTeamMemberLocationIdsForSaveAsync` 展开逻辑 | | `FoodLabeling.Application/Services/TeamMemberAppService.cs` | 合并 `locations`;Create/Update XML 注释;全量绑定展示 | | `FoodLabeling.Application.Contracts/Dtos/TeamMember/TeamMemberCreateInputVo.cs` | 字段注释 + `Locations` | | `FoodLabeling.Application.Contracts/Dtos/TeamMember/TeamMemberUpdateInputVo.cs` | 字段注释 + `Locations` | | `FoodLabeling.Application.Contracts/IServices/ITeamMemberAppService.cs` | Create/Update 接口说明 | --- ## 验证 - `dotnet build` 编译通过 - 可选:MCP `user-antis-foodlabeling-us-mysql` 查 `userlocation` 确认 ALL 请求后行数等于该公司 `location` 未删除门店数 --- ## label-template 多公司 / 多 Region / 多门店传参与落库(美国版) ### 背景 `POST/PUT /api/app/label-template` 在 **Company / Region / Location** 三维均为 `SPECIFIED` 且各传多个 Id 时,若所选 Id 恰好等于「当前上下文全部可选项」,旧逻辑会把维度折叠为 `ALL` 并**清空关联表**,详情再按门店反推 Company,常表现为 **只回显 1 个公司**。 ### 根因 `AllScopeBindingHelper.ResolveDimensionType` 在 `isFullSelection == true` 时一律返回 `ALL`,**未尊重入参显式 `appliedPartnerType` / `appliedRegionType` / `appliedLocation` = `SPECIFIED`**。 ### 修复约定 | 入参 | 落库行为 | |------|----------| | `appliedPartnerType: "SPECIFIED"` + `partnerIds` / `companyIds` 数组 | **始终**写 `fl_label_template_partner`(每个 Id 一行),主表 `AppliedPartnerType = SPECIFIED` | | `appliedRegionType: "SPECIFIED"` + `regionIds` / `groupIds` | **始终**写 `fl_label_template_region` | | `appliedLocation: "SPECIFIED"` + `locationIds` / `appliedLocationIds` | **始终**写 `fl_label_template_location` | | 各维度 `ALL` 或未显式 `SPECIFIED` 且 Id 覆盖当前上下文全集 | 仍归档为 `ALL`,不写该维度关联快照(后续新增主数据动态适用) | **字段别名(合并去重)** - Company:`partnerIds` ≡ `companyIds`(`fl_partner.Id`) - Region:`regionIds` ≡ `groupIds`(`fl_group.Id`) - Location:`locationIds` ≡ `appliedLocationIds`(`location.Id`) ### 请求示例(2 个 Company + 2 个 Region + 2 个门店) ```json PUT /api/app/label-template/tpl_xxx { "appliedPartnerType": "SPECIFIED", "companyIds": ["{partnerId1}", "{partnerId2}"], "partnerIds": ["{partnerId1}", "{partnerId2}"], "appliedRegionType": "SPECIFIED", "regionIds": ["{groupId1}", "{groupId2}"], "groupIds": ["{groupId1}", "{groupId2}"], "appliedLocation": "SPECIFIED", "locationIds": ["{locId1}", "{locId2}"], "appliedLocationIds": ["{locId1}", "{locId2}"] } ``` ### 验证(MCP / curl) 1. 保存后查库: ```sql SELECT AppliedPartnerType, AppliedRegionType, AppliedLocationType FROM fl_label_template WHERE TemplateCode = 'tpl_xxx'; SELECT PartnerId FROM fl_label_template_partner WHERE TemplateId = (SELECT Id FROM fl_label_template WHERE TemplateCode = 'tpl_xxx'); SELECT GroupId FROM fl_label_template_region WHERE TemplateId = (SELECT Id FROM fl_label_template WHERE TemplateCode = 'tpl_xxx'); SELECT LocationId FROM fl_label_template_location WHERE TemplateId = (SELECT Id FROM fl_label_template WHERE TemplateCode = 'tpl_xxx'); ``` 2. `GET /api/app/label-template/tpl_xxx` 出参 `companyIds` / `partnerIds` / `regionIds` / `locationIds` 数组长度应与入参一致。 ### 改动文件 | 文件 | 变更 | |------|------| | `Helpers/AllScopeBindingHelper.cs` | 显式 `SPECIFIED` 时不因全选折叠为 `ALL` | | `Helpers/LabelTemplateScopeHelper.cs` | 保存传参对齐 Label 实体;详情/列表读取主表 scope 类型,避免门店反推覆盖 | | `Helpers/LabelTemplateScopeSchemaHelper.cs` | 批量读取 `AppliedPartnerType` / `AppliedRegionType` | --- ## 七类实体编辑回显 ALL 哨兵(2026-07-24 续) ### 背景 Team Member 已实现「全选回显 `["ALL"]`」规则。本次将同一约定推广至 Product、Product Category、Label、Label Category、Label Type、Label Template、Multiple Options Set 的 **GET 详情**(及列表若返回 scope Id 数组)。 - **落库不变**:仍可存展开 Guid 或 `Applied*Type=ALL` / `AvailabilityType=ALL`;显式 `SPECIFIED` 多 Id 落库逻辑不受影响。 - **回显变更**:不再把 `Type=ALL` 展开为当前可见全量 Guid;改为返回 `["ALL"]` 哨兵,供编辑弹窗勾选 ALL。 - **公共实现**:`ScopeAllEchoHelper.CollapseScopeIdsToAllSentinelAsync`(对齐 `TeamMemberScopeDisplayHelper.CollapseScopeIdsToAllSentinelForEditAsync` + `AllScopeBindingHelper.IsFullIdSelection`)。 ### 折叠规则(与 Team Member 一致) | 条件 | 出参字段 | 值 | |------|----------|-----| | 覆盖全部 Company(系统级) | `partnerIds` / `companyIds` | `["ALL"]` | | 覆盖该公司下全部 Region | `regionIds` / `groupIds` | `["ALL"]` | | 覆盖该公司下全部门店 | `locationIds` / `appliedLocationIds` | `["ALL"]` | | 主表 `Applied*Type=ALL` 或 `AvailabilityType=ALL` | 对应维度 Id 数组 | `["ALL"]`(不展开 Guid) | | 未全选 | 各 Id 数组 | 具体 Guid 列表 | **说明**:Company 维度为 ALL 时 `partnerIds` 为 `["ALL"]`;Company 为 SPECIFIED 且 Region/Location 全选时,`partnerIds` 仍保留具体 Company Guid(与 Team Member 一致)。 ### 字段对照表 | 实体 | 详情/列表接口 | Company | Region | Location | 类型字段 | |------|---------------|---------|--------|----------|----------| | Product | `GET /api/app/product/{id}`、列表 `locationIds` | `partnerIds` / `partnerId` | `groupIds` | `locationIds` | `availabilityType` | | Product Category | `GET /api/app/product-category/{id}`、列表 | — | `regionIds` / `groupIds` | `locationIds` | `availabilityType` | | Label | `GET /api/app/label/{id}`、列表 | `partnerIds` / `partnerId` | `regionIds` / `groupIds` | `locationIds` | `appliedRegionType` | | Label Category | `GET /api/app/label-category/{id}`、列表 | `partnerIds` / `companyIds` | `regionIds` / `groupIds` | `locationIds` | `appliedPartnerType` + `availabilityType` | | Label Type | `GET /api/app/label-type/{id}`、列表 | `partnerIds` / `companyIds` | `regionIds` / `groupIds` | `locationIds` | `appliedPartnerType` + `availabilityType` | | Label Template | `GET /api/app/label-template/{code}`、列表 | `partnerIds` / `companyIds` | `regionIds` / `groupIds` | `locationIds` / `appliedLocationIds` | `appliedPartnerType` / `appliedRegionType` / `appliedLocationType` | | Multiple Options Set | `GET /api/app/label-multiple-option/{id}`、列表 | `partnerIds` / `companyIds` | `regionIds` / `groupIds` | `locationIds` | `appliedPartnerType` + `availabilityType` | 列表 **Region / Location 展示文案**(`All Region` / `All Location` / `All Companies`)保持原逻辑,仅 Id 数组改为哨兵。 ### 改动文件 | 文件 | 变更 | |------|------| | `Helpers/ScopeAllEchoHelper.cs` | **新增** 公共 ALL 回显折叠 | | `Helpers/LabelRegionScopeHelper.cs` | Label Region/Location 展示与 Id 折叠 | | `Helpers/LabelTemplateScopeHelper.cs` | 模板列表/详情 scope Id 折叠 | | `Helpers/LabelEntityPartnerScopeHelper.cs` | 标签实体 Company Id 折叠 | | `Services/ProductAppService.cs` | 产品详情/列表 `locationIds`、`groupIds` | | `Services/ProductCategoryAppService.cs` | 产品分类详情/列表 | | `Services/LabelAppService.cs` | 标签详情/列表 | | `Services/LabelCategoryAppService.cs` | 标签分类详情/列表 | | `Services/LabelTypeAppService.cs` | 标签类型详情/列表;移除 `ExpandAllScopeIdsToDtoAsync` | | `Services/LabelTemplateAppService.cs` | 模板详情;移除全量 Guid 展开 | | `Services/LabelMultipleOptionAppService.cs` | 多选项详情/列表;移除 `ExpandAllScopeIdsToDtoAsync` | --- ## 保存时 Id 数组含 ALL 哨兵(2026-07-24 续) ### 背景 编辑弹窗回显 `regionIds` / `groupIds` / `locationIds: ["ALL"]` 后再次保存时,若后端把字面量 `"ALL"` 当作 Guid 做存在性校验,会报错(如 `门店Id格式不正确`、`存在无效的 Region`),并产生 `fl_group.Id = 'ALL'` 等无效 SQL。 ### 约定(与 Team Member 对齐) | 入参 | 保存行为 | |------|----------| | `locationIds`(或 `appliedLocationIds`)含 `ALL` | **不归档 Guid 校验**;维度 Type 归档为 `ALL`,关联表不写快照 | | `regionIds` / `groupIds` 含 `ALL` 且无具体门店 | 同上,Region/Availability 归档 `ALL` | | `regionIds` 含 `ALL` + 具体 `locationIds` | Region 归档 `ALL`,仅落具体门店快照(Team Member 同语义) | | `applied*Type: "SPECIFIED"` + Id 数组仅 `["ALL"]` | **识别为全选**,Type 归档为 `ALL`(Normalize 阶段转换,非 Guid 校验) | | 显式 `SPECIFIED` + 多个具体 Guid | **不变**:全部落库,编辑仍回显具体 Id | **实现要点** - `AllScopeBindingHelper.Normalize*ScopeAsync` / `ShouldTreat*AsAllAsync`:先识别 `ALL` 哨兵,再决定是否展开或归档 ALL。 - `LocationScopeBindingHelper.MergeToLocationIdsAsync` / `FilterConcreteScopeIds`:合并与校验前剥离 `ALL`,禁止传入 `Validate*ExistAsync`。 - `LabelTemplateScopeHelper.ResolveScopeForSaveAsync`:经 Normalize 后 Region/Location 为 ALL 时跳过 Guid 存在性校验。 ### 涉及接口(示例) - `POST/PUT /api/app/label-multiple-option` — **新增与编辑均支持** `regionIds` / `groupIds` / `locationIds` 传 `["ALL"]`;`availabilityType: SPECIFIED` + Id 数组 `["ALL"]` 仍归档为 `ALL` - `POST/PUT /api/app/label-template` — **新增与编辑均支持** `appliedRegionType` / `appliedLocation: SPECIFIED` + `regionIds` / `groupIds` / `locationIds` / `appliedLocationIds: ["ALL"]`,Region/Location 维度归档 `ALL`;Partner 仍可 `SPECIFIED` + 多 `companyIds` - `POST/PUT /api/app/label-type` — **新增与编辑均支持**(与 multiple-option 相同):`availabilityType: SPECIFIED` + `regionIds`/`groupIds`/`locationIds: ["ALL"]` → `AvailabilityType=ALL` - `POST/PUT /api/app/label-category` — **新增与编辑均支持**(同上):`availabilityType: SPECIFIED` + `regionIds`/`groupIds`/`locationIds: ["ALL"]` → `AvailabilityType=ALL` - `POST/PUT /api/app/label` — **新增与编辑均支持**:`appliedRegionType: SPECIFIED` + `regionIds`/`groupIds`/`locationIds: ["ALL"]` → `AppliedRegionType=ALL`(Create/Update 共用 `LabelRegionScopeHelper.ResolveScopeForSaveAsync`) - `POST/PUT /api/app/product-category` — **新增与编辑均支持**:`availabilityType: SPECIFIED` + `regionIds`/`groupIds`/`locationIds: ["ALL"]` → `AvailabilityType=ALL` - `POST/PUT /api/app/product` — **新增与编辑均支持**:`availabilityType: SPECIFIED` + `partnerId: "ALL"` 或 `groupIds`/`locationIds: ["ALL"]` → `AvailabilityType=ALL`,清空 `fl_location_product` 快照 ### label-multiple-option 编辑传 ALL 示例 ```json PUT /api/app/label-multiple-option/{id} { "optionName": "Size Options", "appliedPartnerType": "SPECIFIED", "companyIds": ["{partnerGuid1}", "{partnerGuid2}"], "availabilityType": "SPECIFIED", "regionIds": ["ALL"], "groupIds": ["ALL"], "locationIds": ["ALL"], "state": true } ``` **落库**:`AvailabilityType = ALL`,`fl_label_multiple_option_location` 无快照行;GET 回显 `regionIds` / `locationIds: ["ALL"]`。 ### label-template 编辑传 ALL 示例 ```json PUT /api/app/label-template/{templateCode} { "appliedPartnerType": "SPECIFIED", "companyIds": ["{partnerGuid1}", "{partnerGuid2}"], "appliedRegionType": "SPECIFIED", "regionIds": ["ALL"], "groupIds": ["ALL"], "appliedLocation": "SPECIFIED", "locationIds": ["ALL"], "appliedLocationIds": ["ALL"] } ``` **落库**:`AppliedPartnerType = SPECIFIED` 且写 `fl_label_template_partner`;`AppliedRegionType` / `AppliedLocationType = ALL`,Region/Location 关联表无快照;GET 回显 Region/Location Id 为 `["ALL"]`。 ### 改动文件 | 文件 | 变更 | |------|------| | `Helpers/AllScopeBindingHelper.cs` | Normalize / ShouldTreat 识别 ALL 哨兵 | | `Helpers/LocationScopeBindingHelper.cs` | Merge / Validate / ResolveEntityLocationIds 过滤 ALL | | `Helpers/LabelRegionScopeHelper.cs` | Label 保存路径 ALL 预处理 | | `Helpers/LabelTemplateScopeHelper.cs` | Create/Update 共用 ResolveScope 开头识别 ALL;ValidateRegion 过滤 ALL | | `Services/LabelMultipleOptionAppService.cs` | `ResolveMultipleOptionScopeForSaveAsync` 开头 ALL 早返回;Create/Update XML | | `Services/LabelTemplateAppService.cs` | 确认 Update 走 SaveTemplateScope + SetAppliedScopeTypes;Create/Update XML | ### 验证 ```bash dotnet build module/food-labeling-us/FoodLabeling.Application/FoodLabeling.Application.csproj ``` 保存后:主表 `Applied*Type` / `AvailabilityType` 为 `ALL`,关联表无快照行;`GET` 详情仍回显 `["ALL"]`。