Commit dc39baae10e931c4b15bf334b1cfcaa493800bd4
1 parent
c2e3194d
后台端:管理员查看日志优化,产品与门店绑定优化;
app:分类优化
Showing
13 changed files
with
199 additions
and
45 deletions
标签模块接口对接说明.md
| @@ -582,7 +582,7 @@ ADD UNIQUE KEY `uk_fl_ltpd_template_product_label_type` (`TemplateId`, `ProductI | @@ -582,7 +582,7 @@ ADD UNIQUE KEY `uk_fl_ltpd_template_product_label_type` (`TemplateId`, `ProductI | ||
| 582 | 582 | ||
| 583 | 方法:`POST /api/app/product` | 583 | 方法:`POST /api/app/product` |
| 584 | 584 | ||
| 585 | -入参(Body:`ProductCreateInputVo`): | 585 | +入参(Body:`ProductCreateInputVo`)示例 1(自填编码): |
| 586 | ```json | 586 | ```json |
| 587 | { | 587 | { |
| 588 | "productCode": "PRD_TEST_001", | 588 | "productCode": "PRD_TEST_001", |
| @@ -597,10 +597,22 @@ ADD UNIQUE KEY `uk_fl_ltpd_template_product_label_type` (`TemplateId`, `ProductI | @@ -597,10 +597,22 @@ ADD UNIQUE KEY `uk_fl_ltpd_template_product_label_type` (`TemplateId`, `ProductI | ||
| 597 | } | 597 | } |
| 598 | ``` | 598 | ``` |
| 599 | 599 | ||
| 600 | +示例 2(不传或 `null` 产品编码时,由后端生成唯一值,形如 `PRD_` + 32 位十六进制): | ||
| 601 | +```json | ||
| 602 | +{ | ||
| 603 | + "productCode": null, | ||
| 604 | + "productName": "Chicken", | ||
| 605 | + "categoryId": "a2696b9e-2277-11f1-b4c6-00163e0c7c4f", | ||
| 606 | + "productImageUrl": null, | ||
| 607 | + "state": true, | ||
| 608 | + "locationIds": [] | ||
| 609 | +} | ||
| 610 | +``` | ||
| 611 | + | ||
| 600 | 字段说明: | 612 | 字段说明: |
| 601 | | 字段 | 类型 | 必填 | 说明 | | 613 | | 字段 | 类型 | 必填 | 说明 | |
| 602 | |------|------|------|------| | 614 | |------|------|------|------| |
| 603 | -| `productCode` | string | 是 | 产品编码 | | 615 | +| `productCode` | string \| null | **否** | **可选。** 有非空值时按该编码落库,且须全局唯一(未删除数据);不传、`null` 或空串时由后端 **自动生成**唯一 `productCode`。 | |
| 604 | | `productName` | string | 是 | 产品名称 | | 616 | | `productName` | string | 是 | 产品名称 | |
| 605 | | `categoryId` | string \| null | 否 | 产品分类 Id(`fl_product_category.id`) | | 617 | | `categoryId` | string \| null | 否 | 产品分类 Id(`fl_product_category.id`) | |
| 606 | | `productImageUrl` | string \| null | 否 | 主图 URL | | 618 | | `productImageUrl` | string \| null | 否 | 主图 URL | |
| @@ -608,8 +620,8 @@ ADD UNIQUE KEY `uk_fl_ltpd_template_product_label_type` (`TemplateId`, `ProductI | @@ -608,8 +620,8 @@ ADD UNIQUE KEY `uk_fl_ltpd_template_product_label_type` (`TemplateId`, `ProductI | ||
| 608 | | **`locationIds`** | `string[]` \| **省略** | 否 | **可选。** 有该字段时:在同一事务内按列表批量写入 **`fl_location_product`**(**每个门店 Id 一行**,即「一产品一门店一条关联」)。**请求体中省略该字段**时:本接口不写门店关联,仍可通过 **§7 Product-Location** 维护。传空数组 `[]` 表示新建产品后不绑定任何门店。 | | 620 | | **`locationIds`** | `string[]` \| **省略** | 否 | **可选。** 有该字段时:在同一事务内按列表批量写入 **`fl_location_product`**(**每个门店 Id 一行**,即「一产品一门店一条关联」)。**请求体中省略该字段**时:本接口不写门店关联,仍可通过 **§7 Product-Location** 维护。传空数组 `[]` 表示新建产品后不绑定任何门店。 | |
| 609 | 621 | ||
| 610 | 校验: | 622 | 校验: |
| 611 | -- `productCode` / `productName` 不能为空 | ||
| 612 | -- `productCode` 不能与未删除的数据重复 | 623 | +- `productName` 不能为空 |
| 624 | +- 若请求中 **`productCode` 有非空值**:不能与**其它**未删除产品的 `productCode` 重复 | ||
| 613 | - 若传入 **`locationIds`** 且含非空项:每个 Id 须为合法 Guid,且对应门店存在于 **`Location`** 主数据且未删除;否则返回友好错误(如「门店Id格式不正确」「门店不存在」) | 625 | - 若传入 **`locationIds`** 且含非空项:每个 Id 须为合法 Guid,且对应门店存在于 **`Location`** 主数据且未删除;否则返回友好错误(如「门店Id格式不正确」「门店不存在」) |
| 614 | 626 | ||
| 615 | ### 6.4 编辑产品 | 627 | ### 6.4 编辑产品 |
| @@ -620,6 +632,9 @@ ADD UNIQUE KEY `uk_fl_ltpd_template_product_label_type` (`TemplateId`, `ProductI | @@ -620,6 +632,9 @@ ADD UNIQUE KEY `uk_fl_ltpd_template_product_label_type` (`TemplateId`, `ProductI | ||
| 620 | - Path:`id` 为当前产品Id(`fl_product.Id`) | 632 | - Path:`id` 为当前产品Id(`fl_product.Id`) |
| 621 | - Body:字段同新增(`ProductUpdateInputVo`,继承 `ProductCreateInputVo`) | 633 | - Body:字段同新增(`ProductUpdateInputVo`,继承 `ProductCreateInputVo`) |
| 622 | 634 | ||
| 635 | +**`productCode` 行为:** | ||
| 636 | +- 不传、`null` 或空串:**保留**原产品的 `productCode`;若原数据异常为空,则按新增规则自动生成唯一编码。 | ||
| 637 | + | ||
| 623 | **`locationIds` 行为(与新增不同,请注意):** | 638 | **`locationIds` 行为(与新增不同,请注意):** |
| 624 | - **请求体中省略 `locationIds` 属性**:不修改 **`fl_location_product`**(仅更新 `fl_product` 主表字段;兼容原「先 PUT 产品再调 §7 同步门店」的调用方式)。 | 639 | - **请求体中省略 `locationIds` 属性**:不修改 **`fl_location_product`**(仅更新 `fl_product` 主表字段;兼容原「先 PUT 产品再调 §7 同步门店」的调用方式)。 |
| 625 | - **请求体中包含 `locationIds` 属性**(含空数组 `[]`):对该产品的门店关联做 **整表替换**——先删除本产品下全部 **`fl_location_product`** 行,再按列表逐条插入;`[]` 表示解除该产品与所有门店的关联。 | 640 | - **请求体中包含 `locationIds` 属性**(含空数组 `[]`):对该产品的门店关联做 **整表替换**——先删除本产品下全部 **`fl_location_product`** 行,再按列表逐条插入;`[]` 表示解除该产品与所有门店的关联。 |
| @@ -734,7 +749,8 @@ ADD UNIQUE KEY `uk_fl_ltpd_template_product_label_type` (`TemplateId`, `ProductI | @@ -734,7 +749,8 @@ ADD UNIQUE KEY `uk_fl_ltpd_template_product_label_type` (`TemplateId`, `ProductI | ||
| 734 | - **门店内商品**:`fl_location_product`(先有 `locationId` 下的 `productId` 集合)。 | 749 | - **门店内商品**:`fl_location_product`(先有 `locationId` 下的 `productId` 集合)。 |
| 735 | - **参与连接的表**:`fl_label_product`(标签-产品)、`fl_label`(`LocationId` 须为当前门店且未删除、启用)、`fl_product`、`fl_label_category`、`fl_label_type`、`fl_label_template`。 | 750 | - **参与连接的表**:`fl_label_product`(标签-产品)、`fl_label`(`LocationId` 须为当前门店且未删除、启用)、`fl_product`、`fl_label_category`、`fl_label_type`、`fl_label_template`。 |
| 736 | - **第二级「产品分类」**:来自 `fl_product.CategoryName`,trim 后为空则归并为显示名 **`无`**。 | 751 | - **第二级「产品分类」**:来自 `fl_product.CategoryName`,trim 后为空则归并为显示名 **`无`**。 |
| 737 | -- **第四级去重**:同一产品在同一标签分类、同一门店下,多条 `fl_label` 若 **`labelCode` 相同**,只保留一条用于列表(预览/打印仍用返回的 `labelCode` 等业务字段)。 | 752 | +- **第三级拆卡**:同一 `productId` 若同时存在多套 **`fl_label.TemplateId`**(不同标签模板),在 **`products`** 中拆成 **多条 L3**(`productId` 可相同,以 **`templateId`** 区分);`itemCount` 为卡片条数。 |
| 753 | +- **第四级去重**:在同一 L3 卡片(同一产品、同一模板)下,多条 `fl_label` 若 **`labelCode` 相同**,只保留一条用于列表(预览/打印仍用返回的 `labelCode` 等业务字段)。 | ||
| 738 | 754 | ||
| 739 | #### 出参(`List<UsAppLabelCategoryTreeNodeDto>`) | 755 | #### 出参(`List<UsAppLabelCategoryTreeNodeDto>`) |
| 740 | 756 | ||
| @@ -762,14 +778,17 @@ ADD UNIQUE KEY `uk_fl_ltpd_template_product_label_type` (`TemplateId`, `ProductI | @@ -762,14 +778,17 @@ ADD UNIQUE KEY `uk_fl_ltpd_template_product_label_type` (`TemplateId`, `ProductI | ||
| 762 | | `buttonAppearance` | string | **JSON 格式字符串**(与库中一致;空时默认 `"TEXT"`) | | 778 | | `buttonAppearance` | string | **JSON 格式字符串**(与库中一致;空时默认 `"TEXT"`) | |
| 763 | | `availabilityType` | string | `ALL` / `SPECIFIED`(树已按当前门店过滤,仍返回供客户端展示) | | 779 | | `availabilityType` | string | `ALL` / `SPECIFIED`(树已按当前门店过滤,仍返回供客户端展示) | |
| 764 | | `orderNum` | number | 排序 | | 780 | | `orderNum` | number | 排序 | |
| 765 | -| `itemCount` | number | 该分类下 **产品个数**(去重后的产品数) | | ||
| 766 | -| `products` | array | 第三级产品列表(见下表) | | 781 | +| `itemCount` | number | 该分类下 **L3 产品卡片条数**(同一产品多模板会多张卡) | |
| 782 | +| `products` | array | 第三级产品卡片列表(见下表) | | ||
| 767 | 783 | ||
| 768 | -**L3 `UsAppLabelingProductNodeDto`(产品)** | 784 | +**L3 `UsAppLabelingProductNodeDto`(产品卡片,按模板拆分)** |
| 769 | 785 | ||
| 770 | | 字段 | 类型 | 说明 | | 786 | | 字段 | 类型 | 说明 | |
| 771 | |------|------|------| | 787 | |------|------|------| |
| 772 | | `productId` | string | `fl_product.Id` | | 788 | | `productId` | string | `fl_product.Id` | |
| 789 | +| `templateId` | string | 当前卡片对应 **`fl_label.TemplateId`**;与 `productId` 组合唯一标识一张卡 | | ||
| 790 | +| `templateCode` | string \| null | 当前卡片所用模板编码 | | ||
| 791 | +| `templateLabelSizeText` | string \| null | 当前卡片模板尺寸文案(与四级中该模板尺寸一致) | | ||
| 773 | | `productName` | string | 产品名称 | | 792 | | `productName` | string | 产品名称 | |
| 774 | | `productCode` | string | 产品编码 | | 793 | | `productCode` | string | 产品编码 | |
| 775 | | `productImageUrl` | string \| null | 主图 | | 794 | | `productImageUrl` | string \| null | 主图 | |
美国版/Food Labeling Management App UniApp/src/pages/labels/labels.vue
| @@ -155,7 +155,7 @@ | @@ -155,7 +155,7 @@ | ||
| 155 | <view class="food-grid"> | 155 | <view class="food-grid"> |
| 156 | <view | 156 | <view |
| 157 | v-for="product in pCat.products" | 157 | v-for="product in pCat.products" |
| 158 | - :key="product.productId" | 158 | + :key="productCardKey(product)" |
| 159 | class="food-card" | 159 | class="food-card" |
| 160 | @click="handleProductClick(product, pCat.name)" | 160 | @click="handleProductClick(product, pCat.name)" |
| 161 | > | 161 | > |
| @@ -468,8 +468,16 @@ function productPhotoSrc(p: UsAppLabelingProductNodeDto): string { | @@ -468,8 +468,16 @@ function productPhotoSrc(p: UsAppLabelingProductNodeDto): string { | ||
| 468 | return resolveMediaUrlForApp(p.productImageUrl) | 468 | return resolveMediaUrlForApp(p.productImageUrl) |
| 469 | } | 469 | } |
| 470 | 470 | ||
| 471 | +/** 同一 productId 多模板拆卡时保证列表 :key 唯一 */ | ||
| 472 | +function productCardKey(p: UsAppLabelingProductNodeDto): string { | ||
| 473 | + const tid = (p.templateId ?? '').trim() | ||
| 474 | + return tid ? `${p.productId}|${tid}` : p.productId | ||
| 475 | +} | ||
| 476 | + | ||
| 471 | /** 无商品图时由标签类型尺寸文案拼接展示(接口无单独预览图字段) */ | 477 | /** 无商品图时由标签类型尺寸文案拼接展示(接口无单独预览图字段) */ |
| 472 | function primaryLabelSizeText(p: UsAppLabelingProductNodeDto): string { | 478 | function primaryLabelSizeText(p: UsAppLabelingProductNodeDto): string { |
| 479 | + const direct = (p.templateLabelSizeText ?? '').trim() | ||
| 480 | + if (direct) return direct | ||
| 473 | const types = p.labelTypes || [] | 481 | const types = p.labelTypes || [] |
| 474 | if (types.length === 0) return '—' | 482 | if (types.length === 0) return '—' |
| 475 | const texts = types.map((t) => (t.labelSizeText || '').trim()).filter(Boolean) | 483 | const texts = types.map((t) => (t.labelSizeText || '').trim()).filter(Boolean) |
美国版/Food Labeling Management App UniApp/src/services/usAppLabeling.ts
| @@ -39,6 +39,11 @@ function normalizeLabelingTreePayload(raw: unknown): UsAppLabelCategoryTreeNodeD | @@ -39,6 +39,11 @@ function normalizeLabelingTreePayload(raw: unknown): UsAppLabelCategoryTreeNodeD | ||
| 39 | })) | 39 | })) |
| 40 | return { | 40 | return { |
| 41 | productId: String(x?.productId ?? x?.ProductId ?? ''), | 41 | productId: String(x?.productId ?? x?.ProductId ?? ''), |
| 42 | + templateId: (x?.templateId ?? x?.TemplateId ?? null) as string | null, | ||
| 43 | + templateCode: (x?.templateCode ?? x?.TemplateCode ?? null) as string | null, | ||
| 44 | + templateLabelSizeText: (x?.templateLabelSizeText ?? x?.TemplateLabelSizeText ?? null) as | ||
| 45 | + | string | ||
| 46 | + | null, | ||
| 42 | productName: String(x?.productName ?? x?.ProductName ?? ''), | 47 | productName: String(x?.productName ?? x?.ProductName ?? ''), |
| 43 | productCode: String(x?.productCode ?? x?.ProductCode ?? ''), | 48 | productCode: String(x?.productCode ?? x?.ProductCode ?? ''), |
| 44 | productImageUrl: (x?.productImageUrl ?? x?.ProductImageUrl ?? null) as string | null, | 49 | productImageUrl: (x?.productImageUrl ?? x?.ProductImageUrl ?? null) as string | null, |
美国版/Food Labeling Management App UniApp/src/types/usAppLabeling.ts
| @@ -11,6 +11,11 @@ export interface UsAppLabelTypeNodeDto { | @@ -11,6 +11,11 @@ export interface UsAppLabelTypeNodeDto { | ||
| 11 | 11 | ||
| 12 | export interface UsAppLabelingProductNodeDto { | 12 | export interface UsAppLabelingProductNodeDto { |
| 13 | productId: string | 13 | productId: string |
| 14 | + /** 与 productId 组合唯一标识一张卡(多模板拆卡) */ | ||
| 15 | + templateId?: string | null | ||
| 16 | + templateCode?: string | null | ||
| 17 | + /** 当前卡片模板尺寸,与接口 templateLabelSizeText 对齐 */ | ||
| 18 | + templateLabelSizeText?: string | null | ||
| 14 | productName: string | 19 | productName: string |
| 15 | productCode: string | 20 | productCode: string |
| 16 | productImageUrl: string | null | 21 | productImageUrl: string | null |
美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/Product/ProductCreateInputVo.cs
| @@ -4,7 +4,10 @@ namespace FoodLabeling.Application.Contracts.Dtos.Product; | @@ -4,7 +4,10 @@ namespace FoodLabeling.Application.Contracts.Dtos.Product; | ||
| 4 | 4 | ||
| 5 | public class ProductCreateInputVo | 5 | public class ProductCreateInputVo |
| 6 | { | 6 | { |
| 7 | - public string ProductCode { get; set; } = string.Empty; | 7 | + /// <summary> |
| 8 | + /// 可选。不传或空则创建时由后端生成唯一编码(如 PRD_xxxxxxxx)。 | ||
| 9 | + /// </summary> | ||
| 10 | + public string? ProductCode { get; set; } | ||
| 8 | 11 | ||
| 9 | public string ProductName { get; set; } = string.Empty; | 12 | public string ProductName { get; set; } = string.Empty; |
| 10 | 13 |
美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/UsAppLabeling/UsAppLabelingProductNodeDto.cs
| 1 | namespace FoodLabeling.Application.Contracts.Dtos.UsAppLabeling; | 1 | namespace FoodLabeling.Application.Contracts.Dtos.UsAppLabeling; |
| 2 | 2 | ||
| 3 | /// <summary> | 3 | /// <summary> |
| 4 | -/// 第三级:产品 | 4 | +/// 第三级:产品卡片(同一产品 Id 若存在多套标签模板,按 <c>TemplateId</c> 拆成多条,便于端上多卡展示) |
| 5 | /// </summary> | 5 | /// </summary> |
| 6 | public class UsAppLabelingProductNodeDto | 6 | public class UsAppLabelingProductNodeDto |
| 7 | { | 7 | { |
| 8 | public string ProductId { get; set; } = string.Empty; | 8 | public string ProductId { get; set; } = string.Empty; |
| 9 | 9 | ||
| 10 | + /// <summary>当前卡片对应 <c>fl_label.TemplateId</c>;与 <c>ProductId</c> 共同唯一标识一张卡</summary> | ||
| 11 | + public string TemplateId { get; set; } = string.Empty; | ||
| 12 | + | ||
| 13 | + /// <summary>当前卡片所用模板编码(与四级节点一致)</summary> | ||
| 14 | + public string? TemplateCode { get; set; } | ||
| 15 | + | ||
| 16 | + /// <summary>当前卡片模板尺寸文案(如 6.00x12.00cm)</summary> | ||
| 17 | + public string? TemplateLabelSizeText { get; set; } | ||
| 18 | + | ||
| 10 | public string ProductName { get; set; } = string.Empty; | 19 | public string ProductName { get; set; } = string.Empty; |
| 11 | 20 | ||
| 12 | public string ProductCode { get; set; } = string.Empty; | 21 | public string ProductCode { get; set; } = string.Empty; |
美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/IServices/IProductAppService.cs
| @@ -24,6 +24,7 @@ public interface IProductAppService : IApplicationService | @@ -24,6 +24,7 @@ public interface IProductAppService : IApplicationService | ||
| 24 | /// 新增产品 | 24 | /// 新增产品 |
| 25 | /// </summary> | 25 | /// </summary> |
| 26 | /// <remarks> | 26 | /// <remarks> |
| 27 | + /// <see cref="ProductCreateInputVo.ProductCode"/> 可选;为空时后端生成唯一编码(如 PRD_ + Guid)。 | ||
| 27 | /// 若 <see cref="ProductCreateInputVo.LocationIds"/> 有值,将在同一事务内批量写入 fl_location_product(一门店一条)。 | 28 | /// 若 <see cref="ProductCreateInputVo.LocationIds"/> 有值,将在同一事务内批量写入 fl_location_product(一门店一条)。 |
| 28 | /// </remarks> | 29 | /// </remarks> |
| 29 | Task<ProductGetOutputDto> CreateAsync(ProductCreateInputVo input); | 30 | Task<ProductGetOutputDto> CreateAsync(ProductCreateInputVo input); |
美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/IServices/IUsAppLabelingAppService.cs
| @@ -5,7 +5,7 @@ using Volo.Abp.Application.Services; | @@ -5,7 +5,7 @@ using Volo.Abp.Application.Services; | ||
| 5 | namespace FoodLabeling.Application.Contracts.IServices; | 5 | namespace FoodLabeling.Application.Contracts.IServices; |
| 6 | 6 | ||
| 7 | /// <summary> | 7 | /// <summary> |
| 8 | -/// App Labeling:四级列表(标签分类 → 产品分类 → 产品 → 标签种类) | 8 | +/// App Labeling:四级列表(标签分类 → 产品分类 → 产品卡片「按模板拆分」→ 标签种类) |
| 9 | /// </summary> | 9 | /// </summary> |
| 10 | public interface IUsAppLabelingAppService : IApplicationService | 10 | public interface IUsAppLabelingAppService : IApplicationService |
| 11 | { | 11 | { |
美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application/Helpers/ReportsRoleHelper.cs
| 1 | using Volo.Abp.Users; | 1 | using Volo.Abp.Users; |
| 2 | +using Yi.Framework.Rbac.Domain.Shared.Consts; | ||
| 2 | 3 | ||
| 3 | namespace FoodLabeling.Application.Helpers; | 4 | namespace FoodLabeling.Application.Helpers; |
| 4 | 5 | ||
| 5 | /// <summary> | 6 | /// <summary> |
| 6 | -/// Reports 模块角色判断(与 JWT / CurrentUser.Roles 中的角色码一致) | 7 | +/// Reports 模块角色判断(与 JWT 中角色声明一致) |
| 7 | /// </summary> | 8 | /// </summary> |
| 8 | public static class ReportsRoleHelper | 9 | public static class ReportsRoleHelper |
| 9 | { | 10 | { |
| 10 | /// <summary> | 11 | /// <summary> |
| 11 | - /// 是否为管理员:任一角色码等于 <c>admin</c>(忽略大小写)则视为可查看全部打印数据。 | 12 | + /// 是否为「可查看全部用户打印数据」的管理员: |
| 13 | + /// <list type="bullet"> | ||
| 14 | + /// <item>标准 <see cref="ICurrentUser.Roles"/> 中含角色码 <c>admin</c>(普通账号绑定 RoleCode=admin 时走此路径);</item> | ||
| 15 | + /// <item>内置超管:用户名 <c>admin</c> 时 JWT 使用自定义 claim <c>Roles</c>,不写多条 <c>role</c>,需单独识别;</item> | ||
| 16 | + /// <item>超管权限 claim <c>Permission</c> 为 <c>*:*:*</c> 时视为管理员。</item> | ||
| 17 | + /// </list> | ||
| 12 | /// </summary> | 18 | /// </summary> |
| 13 | public static bool IsAdminRole(ICurrentUser currentUser) | 19 | public static bool IsAdminRole(ICurrentUser currentUser) |
| 14 | { | 20 | { |
| 15 | - if (currentUser.Roles is null) | 21 | + if (currentUser.Id is null) |
| 16 | { | 22 | { |
| 17 | return false; | 23 | return false; |
| 18 | } | 24 | } |
| 19 | 25 | ||
| 20 | - foreach (var r in currentUser.Roles) | 26 | + var userName = currentUser.UserName?.Trim(); |
| 27 | + if (!string.IsNullOrWhiteSpace(userName) && | ||
| 28 | + string.Equals(userName, UserConst.Admin, StringComparison.OrdinalIgnoreCase)) | ||
| 21 | { | 29 | { |
| 22 | - if (!string.IsNullOrWhiteSpace(r) && | ||
| 23 | - string.Equals(r.Trim(), "admin", StringComparison.OrdinalIgnoreCase)) | 30 | + return true; |
| 31 | + } | ||
| 32 | + | ||
| 33 | + foreach (var c in currentUser.FindClaims(TokenTypeConst.Permission)) | ||
| 34 | + { | ||
| 35 | + if (!string.IsNullOrWhiteSpace(c.Value) && | ||
| 36 | + string.Equals(c.Value.Trim(), UserConst.AdminPermissionCode, StringComparison.Ordinal)) | ||
| 24 | { | 37 | { |
| 25 | return true; | 38 | return true; |
| 26 | } | 39 | } |
| 27 | } | 40 | } |
| 28 | 41 | ||
| 42 | + var rolesClaim = currentUser.FindClaims(TokenTypeConst.Roles).Select(x => x.Value).FirstOrDefault(); | ||
| 43 | + if (!string.IsNullOrWhiteSpace(rolesClaim)) | ||
| 44 | + { | ||
| 45 | + foreach (var part in rolesClaim.Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries)) | ||
| 46 | + { | ||
| 47 | + if (string.Equals(part, UserConst.AdminRolesCode, StringComparison.OrdinalIgnoreCase)) | ||
| 48 | + { | ||
| 49 | + return true; | ||
| 50 | + } | ||
| 51 | + } | ||
| 52 | + } | ||
| 53 | + | ||
| 54 | + if (currentUser.Roles is not null) | ||
| 55 | + { | ||
| 56 | + foreach (var r in currentUser.Roles) | ||
| 57 | + { | ||
| 58 | + if (!string.IsNullOrWhiteSpace(r) && | ||
| 59 | + string.Equals(r.Trim(), UserConst.AdminRolesCode, StringComparison.OrdinalIgnoreCase)) | ||
| 60 | + { | ||
| 61 | + return true; | ||
| 62 | + } | ||
| 63 | + } | ||
| 64 | + } | ||
| 65 | + | ||
| 29 | return false; | 66 | return false; |
| 30 | } | 67 | } |
| 31 | } | 68 | } |
美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application/Services/ProductAppService.cs
| @@ -163,18 +163,25 @@ public class ProductAppService : ApplicationService, IProductAppService | @@ -163,18 +163,25 @@ public class ProductAppService : ApplicationService, IProductAppService | ||
| 163 | [UnitOfWork] | 163 | [UnitOfWork] |
| 164 | public async Task<ProductGetOutputDto> CreateAsync(ProductCreateInputVo input) | 164 | public async Task<ProductGetOutputDto> CreateAsync(ProductCreateInputVo input) |
| 165 | { | 165 | { |
| 166 | - var code = input.ProductCode?.Trim(); | ||
| 167 | var name = input.ProductName?.Trim(); | 166 | var name = input.ProductName?.Trim(); |
| 168 | - if (string.IsNullOrWhiteSpace(code) || string.IsNullOrWhiteSpace(name)) | 167 | + if (string.IsNullOrWhiteSpace(name)) |
| 169 | { | 168 | { |
| 170 | - throw new UserFriendlyException("产品编码和名称不能为空"); | 169 | + throw new UserFriendlyException("产品名称不能为空"); |
| 171 | } | 170 | } |
| 172 | 171 | ||
| 173 | - var duplicated = await _dbContext.SqlSugarClient.Queryable<FlProductDbEntity>() | ||
| 174 | - .AnyAsync(x => !x.IsDeleted && (x.ProductCode == code)); | ||
| 175 | - if (duplicated) | 172 | + var code = input.ProductCode?.Trim(); |
| 173 | + if (string.IsNullOrWhiteSpace(code)) | ||
| 174 | + { | ||
| 175 | + code = await GenerateUniqueProductCodeAsync(); | ||
| 176 | + } | ||
| 177 | + else | ||
| 176 | { | 178 | { |
| 177 | - throw new UserFriendlyException("产品编码已存在"); | 179 | + var duplicated = await _dbContext.SqlSugarClient.Queryable<FlProductDbEntity>() |
| 180 | + .AnyAsync(x => !x.IsDeleted && x.ProductCode == code); | ||
| 181 | + if (duplicated) | ||
| 182 | + { | ||
| 183 | + throw new UserFriendlyException("产品编码已存在"); | ||
| 184 | + } | ||
| 178 | } | 185 | } |
| 179 | 186 | ||
| 180 | var entity = new FlProductDbEntity | 187 | var entity = new FlProductDbEntity |
| @@ -215,18 +222,27 @@ public class ProductAppService : ApplicationService, IProductAppService | @@ -215,18 +222,27 @@ public class ProductAppService : ApplicationService, IProductAppService | ||
| 215 | throw new UserFriendlyException("产品不存在"); | 222 | throw new UserFriendlyException("产品不存在"); |
| 216 | } | 223 | } |
| 217 | 224 | ||
| 218 | - var code = input.ProductCode?.Trim(); | ||
| 219 | var name = input.ProductName?.Trim(); | 225 | var name = input.ProductName?.Trim(); |
| 220 | - if (string.IsNullOrWhiteSpace(code) || string.IsNullOrWhiteSpace(name)) | 226 | + if (string.IsNullOrWhiteSpace(name)) |
| 221 | { | 227 | { |
| 222 | - throw new UserFriendlyException("产品编码和名称不能为空"); | 228 | + throw new UserFriendlyException("产品名称不能为空"); |
| 223 | } | 229 | } |
| 224 | 230 | ||
| 225 | - var duplicated = await _dbContext.SqlSugarClient.Queryable<FlProductDbEntity>() | ||
| 226 | - .AnyAsync(x => !x.IsDeleted && x.Id != productId && x.ProductCode == code); | ||
| 227 | - if (duplicated) | 231 | + var codeInput = input.ProductCode?.Trim(); |
| 232 | + var code = string.IsNullOrWhiteSpace(codeInput) ? entity.ProductCode : codeInput; | ||
| 233 | + if (string.IsNullOrWhiteSpace(code)) | ||
| 228 | { | 234 | { |
| 229 | - throw new UserFriendlyException("产品编码已存在"); | 235 | + code = await GenerateUniqueProductCodeAsync(); |
| 236 | + } | ||
| 237 | + | ||
| 238 | + if (code != entity.ProductCode) | ||
| 239 | + { | ||
| 240 | + var duplicated = await _dbContext.SqlSugarClient.Queryable<FlProductDbEntity>() | ||
| 241 | + .AnyAsync(x => !x.IsDeleted && x.Id != productId && x.ProductCode == code); | ||
| 242 | + if (duplicated) | ||
| 243 | + { | ||
| 244 | + throw new UserFriendlyException("产品编码已存在"); | ||
| 245 | + } | ||
| 230 | } | 246 | } |
| 231 | 247 | ||
| 232 | entity.ProductCode = code; | 248 | entity.ProductCode = code; |
| @@ -267,6 +283,25 @@ public class ProductAppService : ApplicationService, IProductAppService | @@ -267,6 +283,25 @@ public class ProductAppService : ApplicationService, IProductAppService | ||
| 267 | } | 283 | } |
| 268 | 284 | ||
| 269 | /// <summary> | 285 | /// <summary> |
| 286 | + /// 生成未删除数据中不重复的 <c>PRD_</c> 前缀产品编码。 | ||
| 287 | + /// </summary> | ||
| 288 | + private async Task<string> GenerateUniqueProductCodeAsync() | ||
| 289 | + { | ||
| 290 | + for (var i = 0; i < 8; i++) | ||
| 291 | + { | ||
| 292 | + var code = $"PRD_{_guidGenerator.Create():N}"; | ||
| 293 | + var exists = await _dbContext.SqlSugarClient.Queryable<FlProductDbEntity>() | ||
| 294 | + .AnyAsync(x => !x.IsDeleted && x.ProductCode == code); | ||
| 295 | + if (!exists) | ||
| 296 | + { | ||
| 297 | + return code; | ||
| 298 | + } | ||
| 299 | + } | ||
| 300 | + | ||
| 301 | + throw new UserFriendlyException("无法生成唯一产品编码,请稍后重试或手动填写产品编码"); | ||
| 302 | + } | ||
| 303 | + | ||
| 304 | + /// <summary> | ||
| 270 | /// 去重、校验门店 Id 格式与存在性。 | 305 | /// 去重、校验门店 Id 格式与存在性。 |
| 271 | /// </summary> | 306 | /// </summary> |
| 272 | private async Task<List<string>> NormalizeAndValidateLocationIdsAsync(IEnumerable<string> rawIds) | 307 | private async Task<List<string>> NormalizeAndValidateLocationIdsAsync(IEnumerable<string> rawIds) |
美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application/Services/UsAppLabelingAppService.cs
| @@ -52,7 +52,7 @@ public class UsAppLabelingAppService : ApplicationService, IUsAppLabelingAppServ | @@ -52,7 +52,7 @@ public class UsAppLabelingAppService : ApplicationService, IUsAppLabelingAppServ | ||
| 52 | /// <remarks> | 52 | /// <remarks> |
| 53 | /// L1 标签分类 fl_label_category(含 buttonAppearance;COLOR/IMAGE 展示值在 categoryPhotoUrl);仅对当前门店可用:ALL 或 SPECIFIED 且在 fl_label_category_location; | 53 | /// L1 标签分类 fl_label_category(含 buttonAppearance;COLOR/IMAGE 展示值在 categoryPhotoUrl);仅对当前门店可用:ALL 或 SPECIFIED 且在 fl_label_category_location; |
| 54 | /// L2 产品分类 fl_product.CategoryId join fl_product_category(同上,展示值在 categoryPhotoUrl); | 54 | /// L2 产品分类 fl_product.CategoryId join fl_product_category(同上,展示值在 categoryPhotoUrl); |
| 55 | - /// L3 产品;L4 与该门店、该标签分类、该产品关联的标签实例(fl_label + fl_label_type)。 | 55 | + /// L3 产品卡片:按「产品 + 标签模板」拆分(同一 productId、不同 fl_label.TemplateId 为多张卡);L4 为该卡下与门店、标签分类、该产品、该模板关联的标签实例(fl_label + fl_label_type)。 |
| 56 | /// L2 仅包含对当前门店可用的类别:AvailabilityType=ALL,或 SPECIFIED 且在 fl_product_category_location 存在该门店记录; | 56 | /// L2 仅包含对当前门店可用的类别:AvailabilityType=ALL,或 SPECIFIED 且在 fl_product_category_location 存在该门店记录; |
| 57 | /// 未归类或分类行未关联到 fl_product_category 时仍归入「无」节点。 | 57 | /// 未归类或分类行未关联到 fl_product_category 时仍归入「无」节点。 |
| 58 | /// </remarks> | 58 | /// </remarks> |
| @@ -102,7 +102,8 @@ public class UsAppLabelingAppService : ApplicationService, IUsAppLabelingAppServ | @@ -102,7 +102,8 @@ public class UsAppLabelingAppService : ApplicationService, IUsAppLabelingAppServ | ||
| 102 | LabelTypeId = t.Id, | 102 | LabelTypeId = t.Id, |
| 103 | TypeName = t.TypeName, | 103 | TypeName = t.TypeName, |
| 104 | TypeOrderNum = t.OrderNum, | 104 | TypeOrderNum = t.OrderNum, |
| 105 | - LabelCode = l.LabelCode, | 105 | + LabelCode = l.LabelCode ?? string.Empty, |
| 106 | + TemplateId = tpl.Id, | ||
| 106 | TemplateCode = tpl.TemplateCode, | 107 | TemplateCode = tpl.TemplateCode, |
| 107 | TemplateWidth = tpl.Width, | 108 | TemplateWidth = tpl.Width, |
| 108 | TemplateHeight = tpl.Height, | 109 | TemplateHeight = tpl.Height, |
| @@ -175,7 +176,10 @@ public class UsAppLabelingAppService : ApplicationService, IUsAppLabelingAppServ | @@ -175,7 +176,10 @@ public class UsAppLabelingAppService : ApplicationService, IUsAppLabelingAppServ | ||
| 175 | 176 | ||
| 176 | foreach (var g2 in byL2) | 177 | foreach (var g2 in byL2) |
| 177 | { | 178 | { |
| 178 | - var productsGrouped = g2.GroupBy(x => x.ProductId).OrderBy(pg => pg.First().ProductName); | 179 | + var productsGrouped = g2 |
| 180 | + .GroupBy(x => new { x.ProductId, x.TemplateId }) | ||
| 181 | + .OrderBy(pg => pg.First().ProductName) | ||
| 182 | + .ThenBy(pg => pg.Key.TemplateId); | ||
| 179 | var appearance = string.IsNullOrWhiteSpace(g2.Key.ButtonAppearance) | 183 | var appearance = string.IsNullOrWhiteSpace(g2.Key.ButtonAppearance) |
| 180 | ? "TEXT" | 184 | ? "TEXT" |
| 181 | : g2.Key.ButtonAppearance.Trim(); | 185 | : g2.Key.ButtonAppearance.Trim(); |
| @@ -208,10 +212,17 @@ public class UsAppLabelingAppService : ApplicationService, IUsAppLabelingAppServ | @@ -208,10 +212,17 @@ public class UsAppLabelingAppService : ApplicationService, IUsAppLabelingAppServ | ||
| 208 | var subtitle = string.IsNullOrWhiteSpace(first.ProductCode?.Trim()) | 212 | var subtitle = string.IsNullOrWhiteSpace(first.ProductCode?.Trim()) |
| 209 | ? "无" | 213 | ? "无" |
| 210 | : first.ProductCode!.Trim(); | 214 | : first.ProductCode!.Trim(); |
| 215 | + var templateLabelSizeText = FormatLabelSize( | ||
| 216 | + first.TemplateWidth, | ||
| 217 | + first.TemplateHeight, | ||
| 218 | + first.TemplateUnit); | ||
| 211 | 219 | ||
| 212 | l2.Products.Add(new UsAppLabelingProductNodeDto | 220 | l2.Products.Add(new UsAppLabelingProductNodeDto |
| 213 | { | 221 | { |
| 214 | ProductId = first.ProductId, | 222 | ProductId = first.ProductId, |
| 223 | + TemplateId = first.TemplateId, | ||
| 224 | + TemplateCode = first.TemplateCode, | ||
| 225 | + TemplateLabelSizeText = templateLabelSizeText, | ||
| 215 | ProductName = first.ProductName ?? string.Empty, | 226 | ProductName = first.ProductName ?? string.Empty, |
| 216 | ProductCode = first.ProductCode ?? string.Empty, | 227 | ProductCode = first.ProductCode ?? string.Empty, |
| 217 | ProductImageUrl = first.ProductImageUrl, | 228 | ProductImageUrl = first.ProductImageUrl, |
| @@ -957,6 +968,8 @@ public class UsAppLabelingAppService : ApplicationService, IUsAppLabelingAppServ | @@ -957,6 +968,8 @@ public class UsAppLabelingAppService : ApplicationService, IUsAppLabelingAppServ | ||
| 957 | 968 | ||
| 958 | public string LabelCode { get; set; } = string.Empty; | 969 | public string LabelCode { get; set; } = string.Empty; |
| 959 | 970 | ||
| 971 | + public string TemplateId { get; set; } = string.Empty; | ||
| 972 | + | ||
| 960 | public string? TemplateCode { get; set; } | 973 | public string? TemplateCode { get; set; } |
| 961 | 974 | ||
| 962 | public decimal TemplateWidth { get; set; } | 975 | public decimal TemplateWidth { get; set; } |
项目相关文档/报表Reports接口对接说明.md
| @@ -8,8 +8,8 @@ | @@ -8,8 +8,8 @@ | ||
| 8 | 8 | ||
| 9 | ## 0. 角色与数据范围(必读) | 9 | ## 0. 角色与数据范围(必读) |
| 10 | 10 | ||
| 11 | -- 判断依据:`CurrentUser.Roles` 中是否存在**忽略大小写**等于 **`admin`** 的角色码(与 JWT 中角色码一致,参见 `AuthSessionAppService` / `ReportsRoleHelper`)。 | ||
| 12 | -- **`admin`**:**不按** `CreatedBy` 过滤,可查看/统计全部 `fl_label_print_task`(仍受 Partner/Group/Location/日期/关键字筛选)。 | 11 | +- 判断依据(`ReportsRoleHelper.IsAdminRole`,满足其一即可):① `UserName` 为内置 **`admin`**;② JWT 自定义 claim **`Roles`** 中含 **`admin`**(内置超管走 `AccountManager.UserInfoToClaim`,不写标准 `role` claim);③ 任一 **`Permission`** claim 为 **`\*:\*:\*`**;④ 标准 **`CurrentUser.Roles`** 中含忽略大小写的 **`admin`**(普通账号绑定 `RoleCode=admin` 时走此路径)。 |
| 12 | +- **管理员(上述任一)**:**不按** `CreatedBy` 过滤,可查看/统计全部 `fl_label_print_task`(仍受 Partner/Group/Location/日期/关键字筛选)。 | ||
| 13 | - **非 `admin`**:所有列表与统计仅包含 **`CreatedBy == 当前用户 Id`** 的打印任务。 | 13 | - **非 `admin`**:所有列表与统计仅包含 **`CreatedBy == 当前用户 Id`** 的打印任务。 |
| 14 | - **重打**:非 admin 仅能重打本人任务;**`admin` 可重打任意用户任务**,但仍须 `locationId` 与历史任务一致(与 App 重打规则一致)。 | 14 | - **重打**:非 admin 仅能重打本人任务;**`admin` 可重打任意用户任务**,但仍须 `locationId` 与历史任务一致(与 App 重打规则一致)。 |
| 15 | 15 |
项目相关文档/标签模块接口对接说明.md
| @@ -582,7 +582,7 @@ ADD UNIQUE KEY `uk_fl_ltpd_template_product_label_type` (`TemplateId`, `ProductI | @@ -582,7 +582,7 @@ ADD UNIQUE KEY `uk_fl_ltpd_template_product_label_type` (`TemplateId`, `ProductI | ||
| 582 | 582 | ||
| 583 | 方法:`POST /api/app/product` | 583 | 方法:`POST /api/app/product` |
| 584 | 584 | ||
| 585 | -入参(Body:`ProductCreateInputVo`): | 585 | +入参(Body:`ProductCreateInputVo`)示例 1(自填编码): |
| 586 | ```json | 586 | ```json |
| 587 | { | 587 | { |
| 588 | "productCode": "PRD_TEST_001", | 588 | "productCode": "PRD_TEST_001", |
| @@ -597,10 +597,22 @@ ADD UNIQUE KEY `uk_fl_ltpd_template_product_label_type` (`TemplateId`, `ProductI | @@ -597,10 +597,22 @@ ADD UNIQUE KEY `uk_fl_ltpd_template_product_label_type` (`TemplateId`, `ProductI | ||
| 597 | } | 597 | } |
| 598 | ``` | 598 | ``` |
| 599 | 599 | ||
| 600 | +示例 2(不传或 `null` 产品编码时,由后端生成唯一值,形如 `PRD_` + 32 位十六进制): | ||
| 601 | +```json | ||
| 602 | +{ | ||
| 603 | + "productCode": null, | ||
| 604 | + "productName": "Chicken", | ||
| 605 | + "categoryId": "a2696b9e-2277-11f1-b4c6-00163e0c7c4f", | ||
| 606 | + "productImageUrl": null, | ||
| 607 | + "state": true, | ||
| 608 | + "locationIds": [] | ||
| 609 | +} | ||
| 610 | +``` | ||
| 611 | + | ||
| 600 | 字段说明: | 612 | 字段说明: |
| 601 | | 字段 | 类型 | 必填 | 说明 | | 613 | | 字段 | 类型 | 必填 | 说明 | |
| 602 | |------|------|------|------| | 614 | |------|------|------|------| |
| 603 | -| `productCode` | string | 是 | 产品编码 | | 615 | +| `productCode` | string \| null | **否** | **可选。** 有非空值时按该编码落库,且须全局唯一(未删除数据);不传、`null` 或空串时由后端 **自动生成**唯一 `productCode`。 | |
| 604 | | `productName` | string | 是 | 产品名称 | | 616 | | `productName` | string | 是 | 产品名称 | |
| 605 | | `categoryId` | string \| null | 否 | 产品分类 Id(`fl_product_category.id`) | | 617 | | `categoryId` | string \| null | 否 | 产品分类 Id(`fl_product_category.id`) | |
| 606 | | `productImageUrl` | string \| null | 否 | 主图 URL | | 618 | | `productImageUrl` | string \| null | 否 | 主图 URL | |
| @@ -608,8 +620,8 @@ ADD UNIQUE KEY `uk_fl_ltpd_template_product_label_type` (`TemplateId`, `ProductI | @@ -608,8 +620,8 @@ ADD UNIQUE KEY `uk_fl_ltpd_template_product_label_type` (`TemplateId`, `ProductI | ||
| 608 | | **`locationIds`** | `string[]` \| **省略** | 否 | **可选。** 有该字段时:在同一事务内按列表批量写入 **`fl_location_product`**(**每个门店 Id 一行**,即「一产品一门店一条关联」)。**请求体中省略该字段**时:本接口不写门店关联,仍可通过 **§7 Product-Location** 维护。传空数组 `[]` 表示新建产品后不绑定任何门店。 | | 620 | | **`locationIds`** | `string[]` \| **省略** | 否 | **可选。** 有该字段时:在同一事务内按列表批量写入 **`fl_location_product`**(**每个门店 Id 一行**,即「一产品一门店一条关联」)。**请求体中省略该字段**时:本接口不写门店关联,仍可通过 **§7 Product-Location** 维护。传空数组 `[]` 表示新建产品后不绑定任何门店。 | |
| 609 | 621 | ||
| 610 | 校验: | 622 | 校验: |
| 611 | -- `productCode` / `productName` 不能为空 | ||
| 612 | -- `productCode` 不能与未删除的数据重复 | 623 | +- `productName` 不能为空 |
| 624 | +- 若请求中 **`productCode` 有非空值**:不能与**其它**未删除产品的 `productCode` 重复 | ||
| 613 | - 若传入 **`locationIds`** 且含非空项:每个 Id 须为合法 Guid,且对应门店存在于 **`Location`** 主数据且未删除;否则返回友好错误(如「门店Id格式不正确」「门店不存在」) | 625 | - 若传入 **`locationIds`** 且含非空项:每个 Id 须为合法 Guid,且对应门店存在于 **`Location`** 主数据且未删除;否则返回友好错误(如「门店Id格式不正确」「门店不存在」) |
| 614 | 626 | ||
| 615 | ### 6.4 编辑产品 | 627 | ### 6.4 编辑产品 |
| @@ -620,6 +632,9 @@ ADD UNIQUE KEY `uk_fl_ltpd_template_product_label_type` (`TemplateId`, `ProductI | @@ -620,6 +632,9 @@ ADD UNIQUE KEY `uk_fl_ltpd_template_product_label_type` (`TemplateId`, `ProductI | ||
| 620 | - Path:`id` 为当前产品Id(`fl_product.Id`) | 632 | - Path:`id` 为当前产品Id(`fl_product.Id`) |
| 621 | - Body:字段同新增(`ProductUpdateInputVo`,继承 `ProductCreateInputVo`) | 633 | - Body:字段同新增(`ProductUpdateInputVo`,继承 `ProductCreateInputVo`) |
| 622 | 634 | ||
| 635 | +**`productCode` 行为:** | ||
| 636 | +- 不传、`null` 或空串:**保留**原产品的 `productCode`;若原数据异常为空,则按新增规则自动生成唯一编码。 | ||
| 637 | + | ||
| 623 | **`locationIds` 行为(与新增不同,请注意):** | 638 | **`locationIds` 行为(与新增不同,请注意):** |
| 624 | - **请求体中省略 `locationIds` 属性**:不修改 **`fl_location_product`**(仅更新 `fl_product` 主表字段;兼容原「先 PUT 产品再调 §7 同步门店」的调用方式)。 | 639 | - **请求体中省略 `locationIds` 属性**:不修改 **`fl_location_product`**(仅更新 `fl_product` 主表字段;兼容原「先 PUT 产品再调 §7 同步门店」的调用方式)。 |
| 625 | - **请求体中包含 `locationIds` 属性**(含空数组 `[]`):对该产品的门店关联做 **整表替换**——先删除本产品下全部 **`fl_location_product`** 行,再按列表逐条插入;`[]` 表示解除该产品与所有门店的关联。 | 640 | - **请求体中包含 `locationIds` 属性**(含空数组 `[]`):对该产品的门店关联做 **整表替换**——先删除本产品下全部 **`fl_location_product`** 行,再按列表逐条插入;`[]` 表示解除该产品与所有门店的关联。 |
| @@ -734,7 +749,8 @@ ADD UNIQUE KEY `uk_fl_ltpd_template_product_label_type` (`TemplateId`, `ProductI | @@ -734,7 +749,8 @@ ADD UNIQUE KEY `uk_fl_ltpd_template_product_label_type` (`TemplateId`, `ProductI | ||
| 734 | - **门店内商品**:`fl_location_product`(先有 `locationId` 下的 `productId` 集合)。 | 749 | - **门店内商品**:`fl_location_product`(先有 `locationId` 下的 `productId` 集合)。 |
| 735 | - **参与连接的表**:`fl_label_product`(标签-产品)、`fl_label`(`LocationId` 须为当前门店且未删除、启用)、`fl_product`、`fl_label_category`、`fl_label_type`、`fl_label_template`。 | 750 | - **参与连接的表**:`fl_label_product`(标签-产品)、`fl_label`(`LocationId` 须为当前门店且未删除、启用)、`fl_product`、`fl_label_category`、`fl_label_type`、`fl_label_template`。 |
| 736 | - **第二级「产品分类」**:来自 `fl_product.CategoryName`,trim 后为空则归并为显示名 **`无`**。 | 751 | - **第二级「产品分类」**:来自 `fl_product.CategoryName`,trim 后为空则归并为显示名 **`无`**。 |
| 737 | -- **第四级去重**:同一产品在同一标签分类、同一门店下,多条 `fl_label` 若 **`labelCode` 相同**,只保留一条用于列表(预览/打印仍用返回的 `labelCode` 等业务字段)。 | 752 | +- **第三级拆卡**:同一 `productId` 若同时存在多套 **`fl_label.TemplateId`**(不同标签模板),在 **`products`** 中拆成 **多条 L3**(`productId` 可相同,以 **`templateId`** 区分);`itemCount` 为卡片条数。 |
| 753 | +- **第四级去重**:在同一 L3 卡片(同一产品、同一模板)下,多条 `fl_label` 若 **`labelCode` 相同**,只保留一条用于列表(预览/打印仍用返回的 `labelCode` 等业务字段)。 | ||
| 738 | 754 | ||
| 739 | #### 出参(`List<UsAppLabelCategoryTreeNodeDto>`) | 755 | #### 出参(`List<UsAppLabelCategoryTreeNodeDto>`) |
| 740 | 756 | ||
| @@ -762,14 +778,17 @@ ADD UNIQUE KEY `uk_fl_ltpd_template_product_label_type` (`TemplateId`, `ProductI | @@ -762,14 +778,17 @@ ADD UNIQUE KEY `uk_fl_ltpd_template_product_label_type` (`TemplateId`, `ProductI | ||
| 762 | | `buttonAppearance` | string | **JSON 格式字符串**(与库中一致;空时默认 `"TEXT"`) | | 778 | | `buttonAppearance` | string | **JSON 格式字符串**(与库中一致;空时默认 `"TEXT"`) | |
| 763 | | `availabilityType` | string | `ALL` / `SPECIFIED`(树已按当前门店过滤,仍返回供客户端展示) | | 779 | | `availabilityType` | string | `ALL` / `SPECIFIED`(树已按当前门店过滤,仍返回供客户端展示) | |
| 764 | | `orderNum` | number | 排序 | | 780 | | `orderNum` | number | 排序 | |
| 765 | -| `itemCount` | number | 该分类下 **产品个数**(去重后的产品数) | | ||
| 766 | -| `products` | array | 第三级产品列表(见下表) | | 781 | +| `itemCount` | number | 该分类下 **L3 产品卡片条数**(同一产品多模板会多张卡) | |
| 782 | +| `products` | array | 第三级产品卡片列表(见下表) | | ||
| 767 | 783 | ||
| 768 | -**L3 `UsAppLabelingProductNodeDto`(产品)** | 784 | +**L3 `UsAppLabelingProductNodeDto`(产品卡片,按模板拆分)** |
| 769 | 785 | ||
| 770 | | 字段 | 类型 | 说明 | | 786 | | 字段 | 类型 | 说明 | |
| 771 | |------|------|------| | 787 | |------|------|------| |
| 772 | | `productId` | string | `fl_product.Id` | | 788 | | `productId` | string | `fl_product.Id` | |
| 789 | +| `templateId` | string | 当前卡片对应 **`fl_label.TemplateId`**;与 `productId` 组合唯一标识一张卡 | | ||
| 790 | +| `templateCode` | string \| null | 当前卡片所用模板编码 | | ||
| 791 | +| `templateLabelSizeText` | string \| null | 当前卡片模板尺寸文案(与四级中该模板尺寸一致) | | ||
| 773 | | `productName` | string | 产品名称 | | 792 | | `productName` | string | 产品名称 | |
| 774 | | `productCode` | string | 产品编码 | | 793 | | `productCode` | string | 产品编码 | |
| 775 | | `productImageUrl` | string \| null | 主图 | | 794 | | `productImageUrl` | string \| null | 主图 | |