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 并集;按下列顺序取单一规则展开:
locationIds含ALL→ 按partnerId/partnerIds展开该公司全部门店- 有具体
locationIds,且regionIds为空或为ALL→ 只绑这些门店(Region=ALL 下再收窄到指定门店;避免regionIds:["ALL"]盖掉单店) regionIds含ALL(且无具体门店)→ 按 Company 展开该公司全部 Region 下门店regionIds有具体 Guid → 按这些 Region 展开门店落库(忽略同传的locationIds,保证多区域能落库回显)- 仅有具体
locationIds→ 只绑定指定门店 - 仅有
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):
{
"partnerIds": ["3a22a48c-9758-9a26-e4ee-390b0f042835"],
"regionIds": ["ALL"],
"groupIds": ["ALL"],
"locationIds": ["ALL"]
}
请求 / 响应示例
1. Locations 全选(ALL)
请求
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(覆盖全部门店时):
"assignedLocations": [
{ "locationName": "All Location" }
]
2. 仅指定门店 Guid(对比)
请求
"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)
请求
"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 为准展开,保证多区域能落库并回显。
请求
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
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
curl -X POST "http://localhost:19001/api/app/team-member" \
-H "Authorization: <data.token>" \
-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\": \"<ROLE_GUID>\",
\"partnerId\": \"3a222876-7c49-6f01-c232-2d21dde8f27e\",
\"locationIds\": [\"ALL\"],
\"state\": true
}"
更新 — Region ALL
curl -X PUT "http://localhost:19001/api/app/team-member/<USER_GUID>" \
-H "Authorization: <data.token>" \
-H "Content-Type: application/json" \
-d "{
\"fullName\": \"All Region User\",
\"userName\": \"all.region.user@example.com\",
\"roleId\": \"<ROLE_GUID>\",
\"partnerId\": \"3a222876-7c49-6f01-c232-2d21dde8f27e\",
\"regionIds\": [\"ALL\"],
\"state\": true
}"
查询列表验证 Assigned Locations
curl -X GET "http://localhost:19001/api/app/team-member?SkipCount=0&MaxResultCount=20" \
-H "Authorization: <data.token>"
检查 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 个门店)
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)
- 保存后查库:
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');
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"]仍归档为ALLPOST/PUT /api/app/label-template— 新增与编辑均支持appliedRegionType/appliedLocation: SPECIFIED+regionIds/groupIds/locationIds/appliedLocationIds: ["ALL"],Region/Location 维度归档ALL;Partner 仍可SPECIFIED+ 多companyIdsPOST/PUT /api/app/label-type— 新增与编辑均支持(与 multiple-option 相同):availabilityType: SPECIFIED+regionIds/groupIds/locationIds: ["ALL"]→AvailabilityType=ALLPOST/PUT /api/app/label-category— 新增与编辑均支持(同上):availabilityType: SPECIFIED+regionIds/groupIds/locationIds: ["ALL"]→AvailabilityType=ALLPOST/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=ALLPOST/PUT /api/app/product— 新增与编辑均支持:availabilityType: SPECIFIED+partnerId: "ALL"或groupIds/locationIds: ["ALL"]→AvailabilityType=ALL,清空fl_location_product快照
label-multiple-option 编辑传 ALL 示例
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 示例
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 |
验证
dotnet build module/food-labeling-us/FoodLabeling.Application/FoodLabeling.Application.csproj
保存后:主表 Applied*Type / AvailabilityType 为 ALL,关联表无快照行;GET 详情仍回显 ["ALL"]。