# 2026-07-27 代码优化 ## product-category 新增 Company 适用范围(companyIds) ### 背景 产品分类(Product Category)与标签分类(Label Category)对齐,新增/编辑/详情/列表支持 **Company 多选** 传参与 **ALL 哨兵** 回显。 ### 涉及接口 | 方法 | 路径 | |------|------| | GET | `/api/app/product-category/{id}` | | GET | `/api/app/product-category`(列表 `items[]` 同步) | | POST | `/api/app/product-category` | | PUT | `/api/app/product-category/{id}` | 示例详情:`GET /api/app/product-category/3a22b3d6-897d-d89a-e263-b30761fe48be` ### 新增入参字段(POST / PUT) | 字段 | 类型 | 说明 | |------|------|------| | `appliedPartnerType` | string | 适用 Company:`ALL` / `SPECIFIED` | | `partnerIds` | string[] | Company Id(`fl_partner.Id`);与 `companyIds` 合并去重 | | `companyIds` | string[] | 与 `partnerIds` 相同(推荐前端使用本字段) | 原有 `availabilityType`、`regionIds` / `groupIds`、`locationIds` 规则不变,仍支持 `["ALL"]` 哨兵。 ### 新增出参字段(GET 详情 / 列表) | 字段 | 类型 | 说明 | |------|------|------| | `appliedPartnerType` | string | `ALL` / `SPECIFIED` | | `company` | string | 展示文案:`All Companies` 或公司名称逗号拼接 | | `partnerIds` | string[] | Company Id 数组 | | `companyIds` | string[] | 与 `partnerIds` 相同 | ### ALL 哨兵约定(与 Team Member / Label Category 一致) | 入参 | 保存行为 | |------|----------| | `companyIds: ["ALL"]` 或 `appliedPartnerType: "ALL"` | 主表 `AppliedPartnerType=ALL`,不写 `fl_product_category_partner` 快照 | | `appliedPartnerType: "SPECIFIED"` + 具体 Guid | 写入 `fl_product_category_partner` 每 Company 一行 | | `appliedPartnerType: "SPECIFIED"` + `companyIds: ["ALL"]` | **识别为全选**,归档 `AppliedPartnerType=ALL` | | 勾选当前全部 Company(传全量 Guid) | 归档 `ALL`,编辑回显 `companyIds: ["ALL"]` | | 出参折叠 | 条件 | |----------|------| | `companyIds: ["ALL"]` | `AppliedPartnerType=ALL`,或绑定覆盖系统全部 Company | | `regionIds` / `locationIds: ["ALL"]` | 见下方「范围内 ALL」规则 | **说明**:Company 为 `ALL` 时 Region/Location 可为 `ALL`;Company 为 `SPECIFIED` 且 Region/Location 全选时,`companyIds` 仍保留具体 Company Guid(与 Label Category 一致)。 ### 范围内 ALL(2026-07-27 修复) 编辑/新增若已指定具体 `companyIds`/`partnerIds` 或具体 `groupIds`/`regionIds`,再传 `locationIds: ["ALL"]`(或仅 Region=`ALL`): | 入参 | 落库 | 回显 | |------|------|------| | 具体 Company + 具体 Region + `locationIds:["ALL"]` | `AvailabilityType=SPECIFIED`,展开该 Region(且属该 Company)下门店快照 | Region 仍为具体 Id;Location 可折叠为 `["ALL"]` | | 具体 Company + `locationIds:["ALL"]`(无具体 Region) | `SPECIFIED`,展开该公司全部门店 | Location/Region 按覆盖全集折叠 | | 无具体 Company 且无具体 Region + Location/Region ALL | 仍为全局 `AvailabilityType=ALL`(不写门店快照) | Region/Location 均为 `["ALL"]` | **修复前问题**:具体 Company + Region + `locationIds:["ALL"]` 被误归档为全局 `AvailabilityType=ALL`,列表 Region/Location 都显示 All。 ### 具体 locationIds 被 Region 并集冲掉(2026-07-27 再修) 前端编辑时常同时传 `groupIds`/`regionIds`(当前 Region)+ `locationIds`(用户勾选的门店)。旧逻辑 `MergeToLocationIdsAsync(Region ∪ Location)` 会把「只选 1 个门店」扩成整 Region,回显再折成 `locationIds:["ALL"]`。 | 入参 | 修复后落库 | |------|------------| | 具体 `locationIds: ["{loc}"]`(可同传 Region) | **只绑该门店**,`AvailabilityType=SPECIFIED`,回显仍为该 Guid | | 仅具体 Region、无具体 Location | 仍按 Region 展开门店 | | 具体 Location 覆盖 Company 下全部门店 | 仍可归档为全局 `AvailabilityType=ALL` | ### `locationIds:["ALL"]` 回显未折成 ALL(2026-07-27 再修) 落库仍为 `AvailabilityType=SPECIFIED` + 区内门店快照(正确);但回显用 `ResolveAllLocationIdsAsync(Company, Region)` 时旧实现是 **Company∪Region 并集**,用「公司全部门店」去比,导致区内全选无法折叠为 `locationIds:["ALL"]`,列表 Location 也不显示 All。 **修复**:同时传 Company + Region 时改为**交集**。再编:具体 Company + Region + `locationIds:["ALL"]` → 回显 `locationIds:["ALL"]`,`regionIds` 仍为具体 Region。 ### label-category / label-type / label-multiple-option 范围内 ALL(2026-07-27) 与 product-category 对齐:具体 Company + Region + `locationIds:["ALL"]` 不再误归档全局 `AvailabilityType=ALL`,改为展开区内门店快照(`SPECIFIED`),回显 `locationIds` 可折回 `["ALL"]`。 共用:`LocationScopeBindingHelper.ExpandScopedAllLocationsForSaveAsync`。 ### team-member 范围内 ALL(2026-07-27) | 入参 | 旧行为 | 新行为 | |------|--------|--------| | 具体 Company + 具体 Region + `locationIds/locations:["ALL"]` | 忽略 Region,展开**整公司**门店 | 展开**该 Region**(且属该公司)下门店 | | 回显 | 非整公司时 `locationIds` 无法折成 ALL | 区内全选时 `locationIds:["ALL"]`,`regionIds` 仍为具体 Region | ### Region=ALL + 单个 locationId(2026-07-27 再修) 入参示例:`regionIds/groupIds:["ALL"]` + `locationIds:["{单店Guid}"]` | 旧问题 | 修复 | |--------|------| | 无 Region 维度字段,Region=ALL 无法落库;回显只能从门店反推具体 Region | 新增 `AppliedRegionType`(对齐 Label) | | 单店落库后 Region 回显不成 ALL | `AppliedRegionType=ALL` + `AvailabilityType=SPECIFIED` + 门店快照 | **DDL**:执行 `美国版/.../scripts/fl_entity_applied_region_type.sql`(给 label-category / product-category / label-type / label-multiple-option 加列)。 落库后回显示例: ```json { "appliedPartnerType": "SPECIFIED", "availabilityType": "SPECIFIED", "regionIds": ["ALL"], "groupIds": ["ALL"], "locationIds": ["3a22a490-6bcc-ee4f-2163-44f3e7b82906"] } ``` ### 请求示例(新增 / 编辑) ```json { "categoryName": "Prep", "appliedPartnerType": "SPECIFIED", "companyIds": ["{partnerGuid1}", "{partnerGuid2}"], "availabilityType": "SPECIFIED", "regionIds": ["ALL"], "groupIds": ["ALL"], "locationIds": ["ALL"], "state": true, "orderNum": 1 } ``` 全选 Company + 全选门店: ```json { "categoryName": "Prep", "appliedPartnerType": "SPECIFIED", "companyIds": ["ALL"], "availabilityType": "SPECIFIED", "regionIds": ["ALL"], "locationIds": ["ALL"] } ``` **落库**:`AppliedPartnerType=ALL`,`AvailabilityType=ALL`;不写 partner/location 关联快照。 ### 响应示例(详情回显) ```json { "id": "3a22b3d6-897d-d89a-e263-b30761fe48be", "categoryName": "Prep", "appliedPartnerType": "ALL", "company": "All Companies", "companyIds": ["ALL"], "partnerIds": ["ALL"], "availabilityType": "ALL", "regionIds": ["ALL"], "groupIds": ["ALL"], "locationIds": ["ALL"] } ``` ### 数据库迁移(部署前执行) 脚本路径: `美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/scripts/fl_product_category_partner_scope.sql` 内容概要: - `fl_product_category` 增加 `AppliedPartnerType`(默认 `ALL`) - 新建 `fl_product_category_partner`(`CategoryId` + `PartnerId`) 未执行迁移时:`AppliedPartnerType=ALL` 可正常保存;`SPECIFIED` + 具体 `companyIds` 保存会提示执行迁移。 ### 改动文件 | 文件 | 变更 | |------|------| | `scripts/fl_product_category_partner_scope.sql` | **新增** 迁移脚本 | | `DbModels/FlProductCategoryDbEntity.cs` | `AppliedPartnerType` | | `DbModels/FlProductCategoryPartnerDbEntity.cs` | **新增** | | `Helpers/LabelEntityPartnerScopeHelper.cs` | `ProductCategory` 种类 + 列表筛选 | | `Dtos/ProductCategory/*` | 入参/出参 `companyIds` 等 | | `Services/ProductCategoryAppService.cs` | Create/Update/Get/List 集成 Company 范围 | ### 联调注意 1. **先执行 SQL 迁移**,再重启美国版 API。 2. 编辑弹窗回显以 `companyIds` 为准(与 `partnerIds` 等价)。 3. 列表 `company` 为展示字段;勾选状态以 `companyIds` 数组为准。 --- ## 列表筛选:ALL 绑定命中任意具体 Id(2026-07-27 续) ### 约定 | 实体绑定 | Query 传入 | 应命中 | |----------|------------|--------| | Company = `ALL`(`appliedPartnerType=ALL` / 无 partner 关联行) | 任意 `partnerId` | ✅ | | Region = `ALL`(`availabilityType`/`appliedRegionType=ALL` / 无 region 关联行) | 任意 `groupId` | ✅ | | Location = `ALL`(`availabilityType`/`appliedLocationType=ALL`) | 任意 `locationId` | ✅ | | 维度 = `SPECIFIED` | 对应具体 Id | 仅关联命中 | ### 涉及列表接口 - `GET /api/app/label-type` - `GET /api/app/label-multiple-option` - `GET /api/app/label-template` - `GET /api/app/label-category` - `GET /api/app/label`(原已支持 `AppliedRegionType=ALL`) - `GET /api/app/product-category` - `GET /api/app/product`(原已支持 `AvailabilityType=ALL`) - `GET /api/app/product-location`(按 `locationId` 时并入 `AvailabilityType=ALL` 产品) ### 实现要点 | 文件 | 变更 | |------|------| | `LabelEntityPartnerScopeHelper` | Partner 筛选:`AppliedPartnerType=ALL` **或** SPECIFIED 关联命中 | | `LabelEntityListScopeHelper` | Location 筛选:`AvailabilityType=ALL` **或** SPECIFIED 门店命中(去掉「须 SPECIFIED Company」限制) | | `LabelTemplateScopeHelper.ApplyTemplateScopeFilterAsync` | Company/Region 无关联行视为 ALL;Location 保留 `AppliedLocationType=ALL` | | `ProductLocationAppService` | 按门店查时并入 `AvailabilityType=ALL` 产品 | ### 联调示例 ``` GET /api/app/label-type?partnerId={任意公司Guid}&SkipCount=0&MaxResultCount=50 ``` 绑定 `appliedPartnerType=ALL` 的类型应出现在结果中。 ``` GET /api/app/product-location?locationId={任意门店Guid}&MaxResultCount=2000 ``` `AvailabilityType=ALL` 的产品应与该门店 SPECIFIED 关联产品一并返回。 --- ## label-multiple-option 仅 PartnerId 查不到第二家公司(2026-07-27 续) ### 原因 列表在传 `PartnerId` 时会: 1. 按 Company 关联表过滤(正确) 2. **再**把该公司下门店展开,与 `AvailabilityType` / 门店关联做 AND 多公司绑定时,若第二家公司暂无门店、或门店未写入 location 关联表,第 2 步会把已命中的记录误杀。 ### 修复 - **仅传 `PartnerId`**(未传 `GroupId` / `LocationId`):只按 Company 维度过滤,跳过门店 Availability AND - 传了 `GroupId` / `LocationId`:仍按 Region/Location 过滤 - 门店范围为 empty 时:保留 `AvailabilityType=ALL`,不再 `Where false` 清空 涉及:`label-multiple-option` / `label-type` / `label-category` / `product-category` / `label-template` / `label` / `product` / `product-location` ### 各接口要点 | 接口 | 修复 | |------|------| | label-type / label-category / product-category / label-multiple-option | 仅 `PartnerId` 跳过门店 AND | | label-template | 仅 `PartnerId`:**只返回** `fl_label_template_partner` 含该公司的模板;不再把「无关联行」当成 ALL(避免查出其他公司/`AppliedPartnerType=ALL` 的无关数据) | | label | 门店筛选补 `AppliedRegionType=ALL`;无门店时保留 ALL | | product | 仅 `PartnerId` 且公司无门店时保留 `AvailabilityType=ALL` | | product-location | 支持 `PartnerId`;按公司/门店查时并入 `AvailabilityType=ALL` | --- ## product 新增/编辑 `companyIds`(单选)(2026-07-27 续) ### 接口 | 方法 | 路径 | |------|------| | POST | `/api/app/product` | | PUT | `/api/app/product/{id}` | | GET | `/api/app/product/{id}`(编辑回显) | ### 入参 | 字段 | 类型 | 说明 | |------|------|------| | `companyIds` | string[] | **仅单选具体 Guid**;**不支持 ALL**;传多个 Guid 或含 ALL 报错 | | `partnerId` | string | 兼容旧字段;与 `companyIds` 同时传须一致;**不支持 ALL** | ### 出参(详情) | 字段 | 说明 | |------|------| | `companyIds` | 单选回显:最多 1 个具体 Guid;全部门店(AvailabilityType=ALL)时为空 | | `partnerId` | 与 `companyIds[0]` 对齐;无 Company 时为 null | ### 示例 ```json { "productName": "Milk", "companyIds": ["3a22a4f2-1ec6-c7a1-b1a9-f82e71e9754c"], "availabilityType": "SPECIFIED", "groupIds": ["ALL"], "locationIds": ["ALL"] } ``` 传 `companyIds: ["ALL"]` 或 `partnerId: "ALL"` 将返回错误:`产品适用 Company 不支持 ALL,请传单个具体 Company Id`。 ### product `partnerId` 兼容数组 + 范围内 ALL(2026-07-27) | 问题 | 处理 | |------|------| | 前端传 `"partnerId":["{guid}"]` 导致 JSON 反序列化失败 | `PartnerId` 增加转换器,兼容字符串或数组(取首项) | | 增加 `regionIds` | 与 `groupIds` 合并解析 | | 具体 Company/Region + `locationIds:["ALL"]` | 同 product-category:展开为 SPECIFIED 快照,回显可折回 `["ALL"]` |