2026-07-24代码优化.md 20.8 KB

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
  • 适用字段:locationIdsregionIdsgroupIds(与 regionIds 等价)、locations(与 locationIds 合并解析)

与具体 Guid 同传

若数组中同时含 ALL 与具体门店/Region Guid,以 ALL 为准(视为全选,忽略具体 Id)。

优先级(Team Member 落库)

不再做 partner + region + location 并集;按下列顺序取单一规则展开:

  1. locationIdsALL → 按 partnerId / partnerIds 展开该公司全部门店
  2. 有具体 locationIds,且 regionIds 为空或为 ALL只绑这些门店(Region=ALL 下再收窄到指定门店;避免 regionIds:["ALL"] 盖掉单店)
  3. regionIdsALL(且无具体门店)→ 按 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 时须传 partnerIdpartnerIds,否则返回 400。

编辑回显(GET 折叠为 ALL)

库表 userlocation 仍存展开后的门店 GuidGET /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 StoreSubway UNCC Store不会出现 All Location

3. Region 全选(ALL)

请求

"partnerId": "3a222876-7c49-6f01-c232-2d21dde8f27e",
"regionIds": ["ALL"]

落库:该公司下所有 Region 对应门店写入 userlocation

Region 展示:绑定覆盖该公司全部门店时,列表 Region 文案为 All RegionFoodLabelingDisplayConsts.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[].assignedLocationsitems[].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-mysqluserlocation 确认 ALL 请求后行数等于该公司 location 未删除门店数

label-template 多公司 / 多 Region / 多门店传参与落库(美国版)

背景

POST/PUT /api/app/label-templateCompany / Region / Location 三维均为 SPECIFIED 且各传多个 Id 时,若所选 Id 恰好等于「当前上下文全部可选项」,旧逻辑会把维度折叠为 ALL清空关联表,详情再按门店反推 Company,常表现为 只回显 1 个公司

根因

AllScopeBindingHelper.ResolveDimensionTypeisFullSelection == 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:partnerIdscompanyIdsfl_partner.Id
  • Region:regionIdsgroupIdsfl_group.Id
  • Location:locationIdsappliedLocationIdslocation.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)

  1. 保存后查库:
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');
  1. 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=ALLAvailabilityType=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 产品详情/列表 locationIdsgroupIds
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 / groupIdsALL 且无具体门店 同上,Region/Availability 归档 ALL
regionIdsALL + 具体 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 示例

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 = ALLfl_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_partnerAppliedRegionType / 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 / AvailabilityTypeALL,关联表无快照行;GET 详情仍回显 ["ALL"]