diff --git a/标签模块接口对接说明.md b/标签模块接口对接说明.md index b002b2b..5bcfb92 100644 --- a/标签模块接口对接说明.md +++ b/标签模块接口对接说明.md @@ -582,7 +582,7 @@ ADD UNIQUE KEY `uk_fl_ltpd_template_product_label_type` (`TemplateId`, `ProductI 方法:`POST /api/app/product` -入参(Body:`ProductCreateInputVo`): +入参(Body:`ProductCreateInputVo`)示例 1(自填编码): ```json { "productCode": "PRD_TEST_001", @@ -597,10 +597,22 @@ ADD UNIQUE KEY `uk_fl_ltpd_template_product_label_type` (`TemplateId`, `ProductI } ``` +示例 2(不传或 `null` 产品编码时,由后端生成唯一值,形如 `PRD_` + 32 位十六进制): +```json +{ + "productCode": null, + "productName": "Chicken", + "categoryId": "a2696b9e-2277-11f1-b4c6-00163e0c7c4f", + "productImageUrl": null, + "state": true, + "locationIds": [] +} +``` + 字段说明: | 字段 | 类型 | 必填 | 说明 | |------|------|------|------| -| `productCode` | string | 是 | 产品编码 | +| `productCode` | string \| null | **否** | **可选。** 有非空值时按该编码落库,且须全局唯一(未删除数据);不传、`null` 或空串时由后端 **自动生成**唯一 `productCode`。 | | `productName` | string | 是 | 产品名称 | | `categoryId` | string \| null | 否 | 产品分类 Id(`fl_product_category.id`) | | `productImageUrl` | string \| null | 否 | 主图 URL | @@ -608,8 +620,8 @@ ADD UNIQUE KEY `uk_fl_ltpd_template_product_label_type` (`TemplateId`, `ProductI | **`locationIds`** | `string[]` \| **省略** | 否 | **可选。** 有该字段时:在同一事务内按列表批量写入 **`fl_location_product`**(**每个门店 Id 一行**,即「一产品一门店一条关联」)。**请求体中省略该字段**时:本接口不写门店关联,仍可通过 **§7 Product-Location** 维护。传空数组 `[]` 表示新建产品后不绑定任何门店。 | 校验: -- `productCode` / `productName` 不能为空 -- `productCode` 不能与未删除的数据重复 +- `productName` 不能为空 +- 若请求中 **`productCode` 有非空值**:不能与**其它**未删除产品的 `productCode` 重复 - 若传入 **`locationIds`** 且含非空项:每个 Id 须为合法 Guid,且对应门店存在于 **`Location`** 主数据且未删除;否则返回友好错误(如「门店Id格式不正确」「门店不存在」) ### 6.4 编辑产品 @@ -620,6 +632,9 @@ ADD UNIQUE KEY `uk_fl_ltpd_template_product_label_type` (`TemplateId`, `ProductI - Path:`id` 为当前产品Id(`fl_product.Id`) - Body:字段同新增(`ProductUpdateInputVo`,继承 `ProductCreateInputVo`) +**`productCode` 行为:** +- 不传、`null` 或空串:**保留**原产品的 `productCode`;若原数据异常为空,则按新增规则自动生成唯一编码。 + **`locationIds` 行为(与新增不同,请注意):** - **请求体中省略 `locationIds` 属性**:不修改 **`fl_location_product`**(仅更新 `fl_product` 主表字段;兼容原「先 PUT 产品再调 §7 同步门店」的调用方式)。 - **请求体中包含 `locationIds` 属性**(含空数组 `[]`):对该产品的门店关联做 **整表替换**——先删除本产品下全部 **`fl_location_product`** 行,再按列表逐条插入;`[]` 表示解除该产品与所有门店的关联。 @@ -734,7 +749,8 @@ ADD UNIQUE KEY `uk_fl_ltpd_template_product_label_type` (`TemplateId`, `ProductI - **门店内商品**:`fl_location_product`(先有 `locationId` 下的 `productId` 集合)。 - **参与连接的表**:`fl_label_product`(标签-产品)、`fl_label`(`LocationId` 须为当前门店且未删除、启用)、`fl_product`、`fl_label_category`、`fl_label_type`、`fl_label_template`。 - **第二级「产品分类」**:来自 `fl_product.CategoryName`,trim 后为空则归并为显示名 **`无`**。 -- **第四级去重**:同一产品在同一标签分类、同一门店下,多条 `fl_label` 若 **`labelCode` 相同**,只保留一条用于列表(预览/打印仍用返回的 `labelCode` 等业务字段)。 +- **第三级拆卡**:同一 `productId` 若同时存在多套 **`fl_label.TemplateId`**(不同标签模板),在 **`products`** 中拆成 **多条 L3**(`productId` 可相同,以 **`templateId`** 区分);`itemCount` 为卡片条数。 +- **第四级去重**:在同一 L3 卡片(同一产品、同一模板)下,多条 `fl_label` 若 **`labelCode` 相同**,只保留一条用于列表(预览/打印仍用返回的 `labelCode` 等业务字段)。 #### 出参(`List`) @@ -762,14 +778,17 @@ ADD UNIQUE KEY `uk_fl_ltpd_template_product_label_type` (`TemplateId`, `ProductI | `buttonAppearance` | string | **JSON 格式字符串**(与库中一致;空时默认 `"TEXT"`) | | `availabilityType` | string | `ALL` / `SPECIFIED`(树已按当前门店过滤,仍返回供客户端展示) | | `orderNum` | number | 排序 | -| `itemCount` | number | 该分类下 **产品个数**(去重后的产品数) | -| `products` | array | 第三级产品列表(见下表) | +| `itemCount` | number | 该分类下 **L3 产品卡片条数**(同一产品多模板会多张卡) | +| `products` | array | 第三级产品卡片列表(见下表) | -**L3 `UsAppLabelingProductNodeDto`(产品)** +**L3 `UsAppLabelingProductNodeDto`(产品卡片,按模板拆分)** | 字段 | 类型 | 说明 | |------|------|------| | `productId` | string | `fl_product.Id` | +| `templateId` | string | 当前卡片对应 **`fl_label.TemplateId`**;与 `productId` 组合唯一标识一张卡 | +| `templateCode` | string \| null | 当前卡片所用模板编码 | +| `templateLabelSizeText` | string \| null | 当前卡片模板尺寸文案(与四级中该模板尺寸一致) | | `productName` | string | 产品名称 | | `productCode` | string | 产品编码 | | `productImageUrl` | string \| null | 主图 | diff --git a/美国版/Food Labeling Management App UniApp/src/pages/labels/labels.vue b/美国版/Food Labeling Management App UniApp/src/pages/labels/labels.vue index e9e4369..4d867de 100644 --- a/美国版/Food Labeling Management App UniApp/src/pages/labels/labels.vue +++ b/美国版/Food Labeling Management App UniApp/src/pages/labels/labels.vue @@ -155,7 +155,7 @@ @@ -468,8 +468,16 @@ function productPhotoSrc(p: UsAppLabelingProductNodeDto): string { return resolveMediaUrlForApp(p.productImageUrl) } +/** 同一 productId 多模板拆卡时保证列表 :key 唯一 */ +function productCardKey(p: UsAppLabelingProductNodeDto): string { + const tid = (p.templateId ?? '').trim() + return tid ? `${p.productId}|${tid}` : p.productId +} + /** 无商品图时由标签类型尺寸文案拼接展示(接口无单独预览图字段) */ function primaryLabelSizeText(p: UsAppLabelingProductNodeDto): string { + const direct = (p.templateLabelSizeText ?? '').trim() + if (direct) return direct const types = p.labelTypes || [] if (types.length === 0) return '—' const texts = types.map((t) => (t.labelSizeText || '').trim()).filter(Boolean) diff --git a/美国版/Food Labeling Management App UniApp/src/services/usAppLabeling.ts b/美国版/Food Labeling Management App UniApp/src/services/usAppLabeling.ts index 57ed6be..329dab7 100644 --- a/美国版/Food Labeling Management App UniApp/src/services/usAppLabeling.ts +++ b/美国版/Food Labeling Management App UniApp/src/services/usAppLabeling.ts @@ -39,6 +39,11 @@ function normalizeLabelingTreePayload(raw: unknown): UsAppLabelCategoryTreeNodeD })) return { productId: String(x?.productId ?? x?.ProductId ?? ''), + templateId: (x?.templateId ?? x?.TemplateId ?? null) as string | null, + templateCode: (x?.templateCode ?? x?.TemplateCode ?? null) as string | null, + templateLabelSizeText: (x?.templateLabelSizeText ?? x?.TemplateLabelSizeText ?? null) as + | string + | null, productName: String(x?.productName ?? x?.ProductName ?? ''), productCode: String(x?.productCode ?? x?.ProductCode ?? ''), productImageUrl: (x?.productImageUrl ?? x?.ProductImageUrl ?? null) as string | null, diff --git a/美国版/Food Labeling Management App UniApp/src/types/usAppLabeling.ts b/美国版/Food Labeling Management App UniApp/src/types/usAppLabeling.ts index 69268b1..1e272ff 100644 --- a/美国版/Food Labeling Management App UniApp/src/types/usAppLabeling.ts +++ b/美国版/Food Labeling Management App UniApp/src/types/usAppLabeling.ts @@ -11,6 +11,11 @@ export interface UsAppLabelTypeNodeDto { export interface UsAppLabelingProductNodeDto { productId: string + /** 与 productId 组合唯一标识一张卡(多模板拆卡) */ + templateId?: string | null + templateCode?: string | null + /** 当前卡片模板尺寸,与接口 templateLabelSizeText 对齐 */ + templateLabelSizeText?: string | null productName: string productCode: string productImageUrl: string | null diff --git a/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/Product/ProductCreateInputVo.cs b/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/Product/ProductCreateInputVo.cs index 3ed7135..fb33b83 100644 --- a/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/Product/ProductCreateInputVo.cs +++ b/美国版/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; public class ProductCreateInputVo { - public string ProductCode { get; set; } = string.Empty; + /// + /// 可选。不传或空则创建时由后端生成唯一编码(如 PRD_xxxxxxxx)。 + /// + public string? ProductCode { get; set; } public string ProductName { get; set; } = string.Empty; diff --git a/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/UsAppLabeling/UsAppLabelingProductNodeDto.cs b/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/UsAppLabeling/UsAppLabelingProductNodeDto.cs index 9e31138..a737684 100644 --- a/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/UsAppLabeling/UsAppLabelingProductNodeDto.cs +++ b/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/UsAppLabeling/UsAppLabelingProductNodeDto.cs @@ -1,12 +1,21 @@ namespace FoodLabeling.Application.Contracts.Dtos.UsAppLabeling; /// -/// 第三级:产品 +/// 第三级:产品卡片(同一产品 Id 若存在多套标签模板,按 TemplateId 拆成多条,便于端上多卡展示) /// public class UsAppLabelingProductNodeDto { public string ProductId { get; set; } = string.Empty; + /// 当前卡片对应 fl_label.TemplateId;与 ProductId 共同唯一标识一张卡 + public string TemplateId { get; set; } = string.Empty; + + /// 当前卡片所用模板编码(与四级节点一致) + public string? TemplateCode { get; set; } + + /// 当前卡片模板尺寸文案(如 6.00x12.00cm) + public string? TemplateLabelSizeText { get; set; } + public string ProductName { get; set; } = string.Empty; public string ProductCode { get; set; } = string.Empty; diff --git a/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/IServices/IProductAppService.cs b/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/IServices/IProductAppService.cs index 4193329..d126552 100644 --- a/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/IServices/IProductAppService.cs +++ b/美国版/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 /// 新增产品 /// /// + /// 可选;为空时后端生成唯一编码(如 PRD_ + Guid)。 /// 若 有值,将在同一事务内批量写入 fl_location_product(一门店一条)。 /// Task CreateAsync(ProductCreateInputVo input); diff --git a/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/IServices/IUsAppLabelingAppService.cs b/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/IServices/IUsAppLabelingAppService.cs index 781adcd..c97133f 100644 --- a/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/IServices/IUsAppLabelingAppService.cs +++ b/美国版/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; namespace FoodLabeling.Application.Contracts.IServices; /// -/// App Labeling:四级列表(标签分类 → 产品分类 → 产品 → 标签种类) +/// App Labeling:四级列表(标签分类 → 产品分类 → 产品卡片「按模板拆分」→ 标签种类) /// public interface IUsAppLabelingAppService : IApplicationService { diff --git a/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application/Helpers/ReportsRoleHelper.cs b/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application/Helpers/ReportsRoleHelper.cs index 01a205a..823d22f 100644 --- a/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application/Helpers/ReportsRoleHelper.cs +++ b/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application/Helpers/ReportsRoleHelper.cs @@ -1,31 +1,68 @@ using Volo.Abp.Users; +using Yi.Framework.Rbac.Domain.Shared.Consts; namespace FoodLabeling.Application.Helpers; /// -/// Reports 模块角色判断(与 JWT / CurrentUser.Roles 中的角色码一致) +/// Reports 模块角色判断(与 JWT 中角色声明一致) /// public static class ReportsRoleHelper { /// - /// 是否为管理员:任一角色码等于 admin(忽略大小写)则视为可查看全部打印数据。 + /// 是否为「可查看全部用户打印数据」的管理员: + /// + /// 标准 中含角色码 admin(普通账号绑定 RoleCode=admin 时走此路径); + /// 内置超管:用户名 admin 时 JWT 使用自定义 claim Roles,不写多条 role,需单独识别; + /// 超管权限 claim Permission*:*:* 时视为管理员。 + /// /// public static bool IsAdminRole(ICurrentUser currentUser) { - if (currentUser.Roles is null) + if (currentUser.Id is null) { return false; } - foreach (var r in currentUser.Roles) + var userName = currentUser.UserName?.Trim(); + if (!string.IsNullOrWhiteSpace(userName) && + string.Equals(userName, UserConst.Admin, StringComparison.OrdinalIgnoreCase)) { - if (!string.IsNullOrWhiteSpace(r) && - string.Equals(r.Trim(), "admin", StringComparison.OrdinalIgnoreCase)) + return true; + } + + foreach (var c in currentUser.FindClaims(TokenTypeConst.Permission)) + { + if (!string.IsNullOrWhiteSpace(c.Value) && + string.Equals(c.Value.Trim(), UserConst.AdminPermissionCode, StringComparison.Ordinal)) { return true; } } + var rolesClaim = currentUser.FindClaims(TokenTypeConst.Roles).Select(x => x.Value).FirstOrDefault(); + if (!string.IsNullOrWhiteSpace(rolesClaim)) + { + foreach (var part in rolesClaim.Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries)) + { + if (string.Equals(part, UserConst.AdminRolesCode, StringComparison.OrdinalIgnoreCase)) + { + return true; + } + } + } + + if (currentUser.Roles is not null) + { + foreach (var r in currentUser.Roles) + { + if (!string.IsNullOrWhiteSpace(r) && + string.Equals(r.Trim(), UserConst.AdminRolesCode, StringComparison.OrdinalIgnoreCase)) + { + return true; + } + } + } + return false; } } diff --git a/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application/Services/ProductAppService.cs b/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application/Services/ProductAppService.cs index 6b04d91..dbe0058 100644 --- a/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application/Services/ProductAppService.cs +++ b/美国版/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 [UnitOfWork] public async Task CreateAsync(ProductCreateInputVo input) { - var code = input.ProductCode?.Trim(); var name = input.ProductName?.Trim(); - if (string.IsNullOrWhiteSpace(code) || string.IsNullOrWhiteSpace(name)) + if (string.IsNullOrWhiteSpace(name)) { - throw new UserFriendlyException("产品编码和名称不能为空"); + throw new UserFriendlyException("产品名称不能为空"); } - var duplicated = await _dbContext.SqlSugarClient.Queryable() - .AnyAsync(x => !x.IsDeleted && (x.ProductCode == code)); - if (duplicated) + var code = input.ProductCode?.Trim(); + if (string.IsNullOrWhiteSpace(code)) + { + code = await GenerateUniqueProductCodeAsync(); + } + else { - throw new UserFriendlyException("产品编码已存在"); + var duplicated = await _dbContext.SqlSugarClient.Queryable() + .AnyAsync(x => !x.IsDeleted && x.ProductCode == code); + if (duplicated) + { + throw new UserFriendlyException("产品编码已存在"); + } } var entity = new FlProductDbEntity @@ -215,18 +222,27 @@ public class ProductAppService : ApplicationService, IProductAppService throw new UserFriendlyException("产品不存在"); } - var code = input.ProductCode?.Trim(); var name = input.ProductName?.Trim(); - if (string.IsNullOrWhiteSpace(code) || string.IsNullOrWhiteSpace(name)) + if (string.IsNullOrWhiteSpace(name)) { - throw new UserFriendlyException("产品编码和名称不能为空"); + throw new UserFriendlyException("产品名称不能为空"); } - var duplicated = await _dbContext.SqlSugarClient.Queryable() - .AnyAsync(x => !x.IsDeleted && x.Id != productId && x.ProductCode == code); - if (duplicated) + var codeInput = input.ProductCode?.Trim(); + var code = string.IsNullOrWhiteSpace(codeInput) ? entity.ProductCode : codeInput; + if (string.IsNullOrWhiteSpace(code)) { - throw new UserFriendlyException("产品编码已存在"); + code = await GenerateUniqueProductCodeAsync(); + } + + if (code != entity.ProductCode) + { + var duplicated = await _dbContext.SqlSugarClient.Queryable() + .AnyAsync(x => !x.IsDeleted && x.Id != productId && x.ProductCode == code); + if (duplicated) + { + throw new UserFriendlyException("产品编码已存在"); + } } entity.ProductCode = code; @@ -267,6 +283,25 @@ public class ProductAppService : ApplicationService, IProductAppService } /// + /// 生成未删除数据中不重复的 PRD_ 前缀产品编码。 + /// + private async Task GenerateUniqueProductCodeAsync() + { + for (var i = 0; i < 8; i++) + { + var code = $"PRD_{_guidGenerator.Create():N}"; + var exists = await _dbContext.SqlSugarClient.Queryable() + .AnyAsync(x => !x.IsDeleted && x.ProductCode == code); + if (!exists) + { + return code; + } + } + + throw new UserFriendlyException("无法生成唯一产品编码,请稍后重试或手动填写产品编码"); + } + + /// /// 去重、校验门店 Id 格式与存在性。 /// private async Task> NormalizeAndValidateLocationIdsAsync(IEnumerable rawIds) diff --git a/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application/Services/UsAppLabelingAppService.cs b/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application/Services/UsAppLabelingAppService.cs index 03a6822..65a6bad 100644 --- a/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application/Services/UsAppLabelingAppService.cs +++ b/美国版/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 /// /// L1 标签分类 fl_label_category(含 buttonAppearance;COLOR/IMAGE 展示值在 categoryPhotoUrl);仅对当前门店可用:ALL 或 SPECIFIED 且在 fl_label_category_location; /// L2 产品分类 fl_product.CategoryId join fl_product_category(同上,展示值在 categoryPhotoUrl); - /// L3 产品;L4 与该门店、该标签分类、该产品关联的标签实例(fl_label + fl_label_type)。 + /// L3 产品卡片:按「产品 + 标签模板」拆分(同一 productId、不同 fl_label.TemplateId 为多张卡);L4 为该卡下与门店、标签分类、该产品、该模板关联的标签实例(fl_label + fl_label_type)。 /// L2 仅包含对当前门店可用的类别:AvailabilityType=ALL,或 SPECIFIED 且在 fl_product_category_location 存在该门店记录; /// 未归类或分类行未关联到 fl_product_category 时仍归入「无」节点。 /// @@ -102,7 +102,8 @@ public class UsAppLabelingAppService : ApplicationService, IUsAppLabelingAppServ LabelTypeId = t.Id, TypeName = t.TypeName, TypeOrderNum = t.OrderNum, - LabelCode = l.LabelCode, + LabelCode = l.LabelCode ?? string.Empty, + TemplateId = tpl.Id, TemplateCode = tpl.TemplateCode, TemplateWidth = tpl.Width, TemplateHeight = tpl.Height, @@ -175,7 +176,10 @@ public class UsAppLabelingAppService : ApplicationService, IUsAppLabelingAppServ foreach (var g2 in byL2) { - var productsGrouped = g2.GroupBy(x => x.ProductId).OrderBy(pg => pg.First().ProductName); + var productsGrouped = g2 + .GroupBy(x => new { x.ProductId, x.TemplateId }) + .OrderBy(pg => pg.First().ProductName) + .ThenBy(pg => pg.Key.TemplateId); var appearance = string.IsNullOrWhiteSpace(g2.Key.ButtonAppearance) ? "TEXT" : g2.Key.ButtonAppearance.Trim(); @@ -208,10 +212,17 @@ public class UsAppLabelingAppService : ApplicationService, IUsAppLabelingAppServ var subtitle = string.IsNullOrWhiteSpace(first.ProductCode?.Trim()) ? "无" : first.ProductCode!.Trim(); + var templateLabelSizeText = FormatLabelSize( + first.TemplateWidth, + first.TemplateHeight, + first.TemplateUnit); l2.Products.Add(new UsAppLabelingProductNodeDto { ProductId = first.ProductId, + TemplateId = first.TemplateId, + TemplateCode = first.TemplateCode, + TemplateLabelSizeText = templateLabelSizeText, ProductName = first.ProductName ?? string.Empty, ProductCode = first.ProductCode ?? string.Empty, ProductImageUrl = first.ProductImageUrl, @@ -957,6 +968,8 @@ public class UsAppLabelingAppService : ApplicationService, IUsAppLabelingAppServ public string LabelCode { get; set; } = string.Empty; + public string TemplateId { get; set; } = string.Empty; + public string? TemplateCode { get; set; } public decimal TemplateWidth { get; set; } diff --git a/项目相关文档/报表Reports接口对接说明.md b/项目相关文档/报表Reports接口对接说明.md index 7e55269..71cf647 100644 --- a/项目相关文档/报表Reports接口对接说明.md +++ b/项目相关文档/报表Reports接口对接说明.md @@ -8,8 +8,8 @@ ## 0. 角色与数据范围(必读) -- 判断依据:`CurrentUser.Roles` 中是否存在**忽略大小写**等于 **`admin`** 的角色码(与 JWT 中角色码一致,参见 `AuthSessionAppService` / `ReportsRoleHelper`)。 -- **`admin`**:**不按** `CreatedBy` 过滤,可查看/统计全部 `fl_label_print_task`(仍受 Partner/Group/Location/日期/关键字筛选)。 +- 判断依据(`ReportsRoleHelper.IsAdminRole`,满足其一即可):① `UserName` 为内置 **`admin`**;② JWT 自定义 claim **`Roles`** 中含 **`admin`**(内置超管走 `AccountManager.UserInfoToClaim`,不写标准 `role` claim);③ 任一 **`Permission`** claim 为 **`\*:\*:\*`**;④ 标准 **`CurrentUser.Roles`** 中含忽略大小写的 **`admin`**(普通账号绑定 `RoleCode=admin` 时走此路径)。 +- **管理员(上述任一)**:**不按** `CreatedBy` 过滤,可查看/统计全部 `fl_label_print_task`(仍受 Partner/Group/Location/日期/关键字筛选)。 - **非 `admin`**:所有列表与统计仅包含 **`CreatedBy == 当前用户 Id`** 的打印任务。 - **重打**:非 admin 仅能重打本人任务;**`admin` 可重打任意用户任务**,但仍须 `locationId` 与历史任务一致(与 App 重打规则一致)。 diff --git a/项目相关文档/标签模块接口对接说明.md b/项目相关文档/标签模块接口对接说明.md index 6cb7c8d..869e45d 100644 --- a/项目相关文档/标签模块接口对接说明.md +++ b/项目相关文档/标签模块接口对接说明.md @@ -582,7 +582,7 @@ ADD UNIQUE KEY `uk_fl_ltpd_template_product_label_type` (`TemplateId`, `ProductI 方法:`POST /api/app/product` -入参(Body:`ProductCreateInputVo`): +入参(Body:`ProductCreateInputVo`)示例 1(自填编码): ```json { "productCode": "PRD_TEST_001", @@ -597,10 +597,22 @@ ADD UNIQUE KEY `uk_fl_ltpd_template_product_label_type` (`TemplateId`, `ProductI } ``` +示例 2(不传或 `null` 产品编码时,由后端生成唯一值,形如 `PRD_` + 32 位十六进制): +```json +{ + "productCode": null, + "productName": "Chicken", + "categoryId": "a2696b9e-2277-11f1-b4c6-00163e0c7c4f", + "productImageUrl": null, + "state": true, + "locationIds": [] +} +``` + 字段说明: | 字段 | 类型 | 必填 | 说明 | |------|------|------|------| -| `productCode` | string | 是 | 产品编码 | +| `productCode` | string \| null | **否** | **可选。** 有非空值时按该编码落库,且须全局唯一(未删除数据);不传、`null` 或空串时由后端 **自动生成**唯一 `productCode`。 | | `productName` | string | 是 | 产品名称 | | `categoryId` | string \| null | 否 | 产品分类 Id(`fl_product_category.id`) | | `productImageUrl` | string \| null | 否 | 主图 URL | @@ -608,8 +620,8 @@ ADD UNIQUE KEY `uk_fl_ltpd_template_product_label_type` (`TemplateId`, `ProductI | **`locationIds`** | `string[]` \| **省略** | 否 | **可选。** 有该字段时:在同一事务内按列表批量写入 **`fl_location_product`**(**每个门店 Id 一行**,即「一产品一门店一条关联」)。**请求体中省略该字段**时:本接口不写门店关联,仍可通过 **§7 Product-Location** 维护。传空数组 `[]` 表示新建产品后不绑定任何门店。 | 校验: -- `productCode` / `productName` 不能为空 -- `productCode` 不能与未删除的数据重复 +- `productName` 不能为空 +- 若请求中 **`productCode` 有非空值**:不能与**其它**未删除产品的 `productCode` 重复 - 若传入 **`locationIds`** 且含非空项:每个 Id 须为合法 Guid,且对应门店存在于 **`Location`** 主数据且未删除;否则返回友好错误(如「门店Id格式不正确」「门店不存在」) ### 6.4 编辑产品 @@ -620,6 +632,9 @@ ADD UNIQUE KEY `uk_fl_ltpd_template_product_label_type` (`TemplateId`, `ProductI - Path:`id` 为当前产品Id(`fl_product.Id`) - Body:字段同新增(`ProductUpdateInputVo`,继承 `ProductCreateInputVo`) +**`productCode` 行为:** +- 不传、`null` 或空串:**保留**原产品的 `productCode`;若原数据异常为空,则按新增规则自动生成唯一编码。 + **`locationIds` 行为(与新增不同,请注意):** - **请求体中省略 `locationIds` 属性**:不修改 **`fl_location_product`**(仅更新 `fl_product` 主表字段;兼容原「先 PUT 产品再调 §7 同步门店」的调用方式)。 - **请求体中包含 `locationIds` 属性**(含空数组 `[]`):对该产品的门店关联做 **整表替换**——先删除本产品下全部 **`fl_location_product`** 行,再按列表逐条插入;`[]` 表示解除该产品与所有门店的关联。 @@ -734,7 +749,8 @@ ADD UNIQUE KEY `uk_fl_ltpd_template_product_label_type` (`TemplateId`, `ProductI - **门店内商品**:`fl_location_product`(先有 `locationId` 下的 `productId` 集合)。 - **参与连接的表**:`fl_label_product`(标签-产品)、`fl_label`(`LocationId` 须为当前门店且未删除、启用)、`fl_product`、`fl_label_category`、`fl_label_type`、`fl_label_template`。 - **第二级「产品分类」**:来自 `fl_product.CategoryName`,trim 后为空则归并为显示名 **`无`**。 -- **第四级去重**:同一产品在同一标签分类、同一门店下,多条 `fl_label` 若 **`labelCode` 相同**,只保留一条用于列表(预览/打印仍用返回的 `labelCode` 等业务字段)。 +- **第三级拆卡**:同一 `productId` 若同时存在多套 **`fl_label.TemplateId`**(不同标签模板),在 **`products`** 中拆成 **多条 L3**(`productId` 可相同,以 **`templateId`** 区分);`itemCount` 为卡片条数。 +- **第四级去重**:在同一 L3 卡片(同一产品、同一模板)下,多条 `fl_label` 若 **`labelCode` 相同**,只保留一条用于列表(预览/打印仍用返回的 `labelCode` 等业务字段)。 #### 出参(`List`) @@ -762,14 +778,17 @@ ADD UNIQUE KEY `uk_fl_ltpd_template_product_label_type` (`TemplateId`, `ProductI | `buttonAppearance` | string | **JSON 格式字符串**(与库中一致;空时默认 `"TEXT"`) | | `availabilityType` | string | `ALL` / `SPECIFIED`(树已按当前门店过滤,仍返回供客户端展示) | | `orderNum` | number | 排序 | -| `itemCount` | number | 该分类下 **产品个数**(去重后的产品数) | -| `products` | array | 第三级产品列表(见下表) | +| `itemCount` | number | 该分类下 **L3 产品卡片条数**(同一产品多模板会多张卡) | +| `products` | array | 第三级产品卡片列表(见下表) | -**L3 `UsAppLabelingProductNodeDto`(产品)** +**L3 `UsAppLabelingProductNodeDto`(产品卡片,按模板拆分)** | 字段 | 类型 | 说明 | |------|------|------| | `productId` | string | `fl_product.Id` | +| `templateId` | string | 当前卡片对应 **`fl_label.TemplateId`**;与 `productId` 组合唯一标识一张卡 | +| `templateCode` | string \| null | 当前卡片所用模板编码 | +| `templateLabelSizeText` | string \| null | 当前卡片模板尺寸文案(与四级中该模板尺寸一致) | | `productName` | string | 产品名称 | | `productCode` | string | 产品编码 | | `productImageUrl` | string \| null | 主图 |