Commit dc39baae10e931c4b15bf334b1cfcaa493800bd4

Authored by 李曜臣
1 parent c2e3194d

后台端:管理员查看日志优化,产品与门店绑定优化;

app:分类优化
标签模块接口对接说明.md
... ... @@ -582,7 +582,7 @@ ADD UNIQUE KEY `uk_fl_ltpd_template_product_label_type` (`TemplateId`, `ProductI
582 582  
583 583 方法:`POST /api/app/product`
584 584  
585   -入参(Body:`ProductCreateInputVo`)
  585 +入参(Body:`ProductCreateInputVo`)示例 1(自填编码)
586 586 ```json
587 587 {
588 588 "productCode": "PRD_TEST_001",
... ... @@ -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 616 | `productName` | string | 是 | 产品名称 |
605 617 | `categoryId` | string \| null | 否 | 产品分类 Id(`fl_product_category.id`) |
606 618 | `productImageUrl` | string \| null | 否 | 主图 URL |
... ... @@ -608,8 +620,8 @@ ADD UNIQUE KEY `uk_fl_ltpd_template_product_label_type` (`TemplateId`, `ProductI
608 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 625 - 若传入 **`locationIds`** 且含非空项:每个 Id 须为合法 Guid,且对应门店存在于 **`Location`** 主数据且未删除;否则返回友好错误(如「门店Id格式不正确」「门店不存在」)
614 626  
615 627 ### 6.4 编辑产品
... ... @@ -620,6 +632,9 @@ ADD UNIQUE KEY `uk_fl_ltpd_template_product_label_type` (`TemplateId`, `ProductI
620 632 - Path:`id` 为当前产品Id(`fl_product.Id`)
621 633 - Body:字段同新增(`ProductUpdateInputVo`,继承 `ProductCreateInputVo`)
622 634  
  635 +**`productCode` 行为:**
  636 +- 不传、`null` 或空串:**保留**原产品的 `productCode`;若原数据异常为空,则按新增规则自动生成唯一编码。
  637 +
623 638 **`locationIds` 行为(与新增不同,请注意):**
624 639 - **请求体中省略 `locationIds` 属性**:不修改 **`fl_location_product`**(仅更新 `fl_product` 主表字段;兼容原「先 PUT 产品再调 §7 同步门店」的调用方式)。
625 640 - **请求体中包含 `locationIds` 属性**(含空数组 `[]`):对该产品的门店关联做 **整表替换**——先删除本产品下全部 **`fl_location_product`** 行,再按列表逐条插入;`[]` 表示解除该产品与所有门店的关联。
... ... @@ -734,7 +749,8 @@ ADD UNIQUE KEY `uk_fl_ltpd_template_product_label_type` (`TemplateId`, `ProductI
734 749 - **门店内商品**:`fl_location_product`(先有 `locationId` 下的 `productId` 集合)。
735 750 - **参与连接的表**:`fl_label_product`(标签-产品)、`fl_label`(`LocationId` 须为当前门店且未删除、启用)、`fl_product`、`fl_label_category`、`fl_label_type`、`fl_label_template`。
736 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 755 #### 出参(`List<UsAppLabelCategoryTreeNodeDto>`)
740 756  
... ... @@ -762,14 +778,17 @@ ADD UNIQUE KEY `uk_fl_ltpd_template_product_label_type` (`TemplateId`, `ProductI
762 778 | `buttonAppearance` | string | **JSON 格式字符串**(与库中一致;空时默认 `"TEXT"`) |
763 779 | `availabilityType` | string | `ALL` / `SPECIFIED`(树已按当前门店过滤,仍返回供客户端展示) |
764 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 788 | `productId` | string | `fl_product.Id` |
  789 +| `templateId` | string | 当前卡片对应 **`fl_label.TemplateId`**;与 `productId` 组合唯一标识一张卡 |
  790 +| `templateCode` | string \| null | 当前卡片所用模板编码 |
  791 +| `templateLabelSizeText` | string \| null | 当前卡片模板尺寸文案(与四级中该模板尺寸一致) |
773 792 | `productName` | string | 产品名称 |
774 793 | `productCode` | string | 产品编码 |
775 794 | `productImageUrl` | string \| null | 主图 |
... ...
美国版/Food Labeling Management App UniApp/src/pages/labels/labels.vue
... ... @@ -155,7 +155,7 @@
155 155 <view class="food-grid">
156 156 <view
157 157 v-for="product in pCat.products"
158   - :key="product.productId"
  158 + :key="productCardKey(product)"
159 159 class="food-card"
160 160 @click="handleProductClick(product, pCat.name)"
161 161 >
... ... @@ -468,8 +468,16 @@ function productPhotoSrc(p: UsAppLabelingProductNodeDto): string {
468 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 478 function primaryLabelSizeText(p: UsAppLabelingProductNodeDto): string {
  479 + const direct = (p.templateLabelSizeText ?? '').trim()
  480 + if (direct) return direct
473 481 const types = p.labelTypes || []
474 482 if (types.length === 0) return '—'
475 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 39 }))
40 40 return {
41 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 47 productName: String(x?.productName ?? x?.ProductName ?? ''),
43 48 productCode: String(x?.productCode ?? x?.ProductCode ?? ''),
44 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 11  
12 12 export interface UsAppLabelingProductNodeDto {
13 13 productId: string
  14 + /** 与 productId 组合唯一标识一张卡(多模板拆卡) */
  15 + templateId?: string | null
  16 + templateCode?: string | null
  17 + /** 当前卡片模板尺寸,与接口 templateLabelSizeText 对齐 */
  18 + templateLabelSizeText?: string | null
14 19 productName: string
15 20 productCode: string
16 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 4  
5 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 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 1 namespace FoodLabeling.Application.Contracts.Dtos.UsAppLabeling;
2 2  
3 3 /// <summary>
4   -/// 第三级:产品
  4 +/// 第三级:产品卡片(同一产品 Id 若存在多套标签模板,按 <c>TemplateId</c> 拆成多条,便于端上多卡展示)
5 5 /// </summary>
6 6 public class UsAppLabelingProductNodeDto
7 7 {
8 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 19 public string ProductName { get; set; } = string.Empty;
11 20  
12 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 24 /// 新增产品
25 25 /// </summary>
26 26 /// <remarks>
  27 + /// <see cref="ProductCreateInputVo.ProductCode"/> 可选;为空时后端生成唯一编码(如 PRD_ + Guid)。
27 28 /// 若 <see cref="ProductCreateInputVo.LocationIds"/> 有值,将在同一事务内批量写入 fl_location_product(一门店一条)。
28 29 /// </remarks>
29 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 5 namespace FoodLabeling.Application.Contracts.IServices;
6 6  
7 7 /// <summary>
8   -/// App Labeling:四级列表(标签分类 → 产品分类 → 产品 → 标签种类)
  8 +/// App Labeling:四级列表(标签分类 → 产品分类 → 产品卡片「按模板拆分」→ 标签种类)
9 9 /// </summary>
10 10 public interface IUsAppLabelingAppService : IApplicationService
11 11 {
... ...
美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application/Helpers/ReportsRoleHelper.cs
1 1 using Volo.Abp.Users;
  2 +using Yi.Framework.Rbac.Domain.Shared.Consts;
2 3  
3 4 namespace FoodLabeling.Application.Helpers;
4 5  
5 6 /// <summary>
6   -/// Reports 模块角色判断(与 JWT / CurrentUser.Roles 中的角色码一致)
  7 +/// Reports 模块角色判断(与 JWT 中角色声明一致)
7 8 /// </summary>
8 9 public static class ReportsRoleHelper
9 10 {
10 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 18 /// </summary>
13 19 public static bool IsAdminRole(ICurrentUser currentUser)
14 20 {
15   - if (currentUser.Roles is null)
  21 + if (currentUser.Id is null)
16 22 {
17 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 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 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 163 [UnitOfWork]
164 164 public async Task<ProductGetOutputDto> CreateAsync(ProductCreateInputVo input)
165 165 {
166   - var code = input.ProductCode?.Trim();
167 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 187 var entity = new FlProductDbEntity
... ... @@ -215,18 +222,27 @@ public class ProductAppService : ApplicationService, IProductAppService
215 222 throw new UserFriendlyException("产品不存在");
216 223 }
217 224  
218   - var code = input.ProductCode?.Trim();
219 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 248 entity.ProductCode = code;
... ... @@ -267,6 +283,25 @@ public class ProductAppService : ApplicationService, IProductAppService
267 283 }
268 284  
269 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 305 /// 去重、校验门店 Id 格式与存在性。
271 306 /// </summary>
272 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 52 /// <remarks>
53 53 /// L1 标签分类 fl_label_category(含 buttonAppearance;COLOR/IMAGE 展示值在 categoryPhotoUrl);仅对当前门店可用:ALL 或 SPECIFIED 且在 fl_label_category_location;
54 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 56 /// L2 仅包含对当前门店可用的类别:AvailabilityType=ALL,或 SPECIFIED 且在 fl_product_category_location 存在该门店记录;
57 57 /// 未归类或分类行未关联到 fl_product_category 时仍归入「无」节点。
58 58 /// </remarks>
... ... @@ -102,7 +102,8 @@ public class UsAppLabelingAppService : ApplicationService, IUsAppLabelingAppServ
102 102 LabelTypeId = t.Id,
103 103 TypeName = t.TypeName,
104 104 TypeOrderNum = t.OrderNum,
105   - LabelCode = l.LabelCode,
  105 + LabelCode = l.LabelCode ?? string.Empty,
  106 + TemplateId = tpl.Id,
106 107 TemplateCode = tpl.TemplateCode,
107 108 TemplateWidth = tpl.Width,
108 109 TemplateHeight = tpl.Height,
... ... @@ -175,7 +176,10 @@ public class UsAppLabelingAppService : ApplicationService, IUsAppLabelingAppServ
175 176  
176 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 183 var appearance = string.IsNullOrWhiteSpace(g2.Key.ButtonAppearance)
180 184 ? "TEXT"
181 185 : g2.Key.ButtonAppearance.Trim();
... ... @@ -208,10 +212,17 @@ public class UsAppLabelingAppService : ApplicationService, IUsAppLabelingAppServ
208 212 var subtitle = string.IsNullOrWhiteSpace(first.ProductCode?.Trim())
209 213 ? "无"
210 214 : first.ProductCode!.Trim();
  215 + var templateLabelSizeText = FormatLabelSize(
  216 + first.TemplateWidth,
  217 + first.TemplateHeight,
  218 + first.TemplateUnit);
211 219  
212 220 l2.Products.Add(new UsAppLabelingProductNodeDto
213 221 {
214 222 ProductId = first.ProductId,
  223 + TemplateId = first.TemplateId,
  224 + TemplateCode = first.TemplateCode,
  225 + TemplateLabelSizeText = templateLabelSizeText,
215 226 ProductName = first.ProductName ?? string.Empty,
216 227 ProductCode = first.ProductCode ?? string.Empty,
217 228 ProductImageUrl = first.ProductImageUrl,
... ... @@ -957,6 +968,8 @@ public class UsAppLabelingAppService : ApplicationService, IUsAppLabelingAppServ
957 968  
958 969 public string LabelCode { get; set; } = string.Empty;
959 970  
  971 + public string TemplateId { get; set; } = string.Empty;
  972 +
960 973 public string? TemplateCode { get; set; }
961 974  
962 975 public decimal TemplateWidth { get; set; }
... ...
项目相关文档/报表Reports接口对接说明.md
... ... @@ -8,8 +8,8 @@
8 8  
9 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 13 - **非 `admin`**:所有列表与统计仅包含 **`CreatedBy == 当前用户 Id`** 的打印任务。
14 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 582  
583 583 方法:`POST /api/app/product`
584 584  
585   -入参(Body:`ProductCreateInputVo`)
  585 +入参(Body:`ProductCreateInputVo`)示例 1(自填编码)
586 586 ```json
587 587 {
588 588 "productCode": "PRD_TEST_001",
... ... @@ -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 616 | `productName` | string | 是 | 产品名称 |
605 617 | `categoryId` | string \| null | 否 | 产品分类 Id(`fl_product_category.id`) |
606 618 | `productImageUrl` | string \| null | 否 | 主图 URL |
... ... @@ -608,8 +620,8 @@ ADD UNIQUE KEY `uk_fl_ltpd_template_product_label_type` (`TemplateId`, `ProductI
608 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 625 - 若传入 **`locationIds`** 且含非空项:每个 Id 须为合法 Guid,且对应门店存在于 **`Location`** 主数据且未删除;否则返回友好错误(如「门店Id格式不正确」「门店不存在」)
614 626  
615 627 ### 6.4 编辑产品
... ... @@ -620,6 +632,9 @@ ADD UNIQUE KEY `uk_fl_ltpd_template_product_label_type` (`TemplateId`, `ProductI
620 632 - Path:`id` 为当前产品Id(`fl_product.Id`)
621 633 - Body:字段同新增(`ProductUpdateInputVo`,继承 `ProductCreateInputVo`)
622 634  
  635 +**`productCode` 行为:**
  636 +- 不传、`null` 或空串:**保留**原产品的 `productCode`;若原数据异常为空,则按新增规则自动生成唯一编码。
  637 +
623 638 **`locationIds` 行为(与新增不同,请注意):**
624 639 - **请求体中省略 `locationIds` 属性**:不修改 **`fl_location_product`**(仅更新 `fl_product` 主表字段;兼容原「先 PUT 产品再调 §7 同步门店」的调用方式)。
625 640 - **请求体中包含 `locationIds` 属性**(含空数组 `[]`):对该产品的门店关联做 **整表替换**——先删除本产品下全部 **`fl_location_product`** 行,再按列表逐条插入;`[]` 表示解除该产品与所有门店的关联。
... ... @@ -734,7 +749,8 @@ ADD UNIQUE KEY `uk_fl_ltpd_template_product_label_type` (`TemplateId`, `ProductI
734 749 - **门店内商品**:`fl_location_product`(先有 `locationId` 下的 `productId` 集合)。
735 750 - **参与连接的表**:`fl_label_product`(标签-产品)、`fl_label`(`LocationId` 须为当前门店且未删除、启用)、`fl_product`、`fl_label_category`、`fl_label_type`、`fl_label_template`。
736 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 755 #### 出参(`List<UsAppLabelCategoryTreeNodeDto>`)
740 756  
... ... @@ -762,14 +778,17 @@ ADD UNIQUE KEY `uk_fl_ltpd_template_product_label_type` (`TemplateId`, `ProductI
762 778 | `buttonAppearance` | string | **JSON 格式字符串**(与库中一致;空时默认 `"TEXT"`) |
763 779 | `availabilityType` | string | `ALL` / `SPECIFIED`(树已按当前门店过滤,仍返回供客户端展示) |
764 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 788 | `productId` | string | `fl_product.Id` |
  789 +| `templateId` | string | 当前卡片对应 **`fl_label.TemplateId`**;与 `productId` 组合唯一标识一张卡 |
  790 +| `templateCode` | string \| null | 当前卡片所用模板编码 |
  791 +| `templateLabelSizeText` | string \| null | 当前卡片模板尺寸文案(与四级中该模板尺寸一致) |
773 792 | `productName` | string | 产品名称 |
774 793 | `productCode` | string | 产品编码 |
775 794 | `productImageUrl` | string \| null | 主图 |
... ...