From dc39baae10e931c4b15bf334b1cfcaa493800bd4 Mon Sep 17 00:00:00 2001
From: 李曜臣
Date: Sat, 9 May 2026 19:32:31 +0800
Subject: [PATCH] 后台端:管理员查看日志优化,产品与门店绑定优化; app:分类优化
---
标签模块接口对接说明.md | 35 +++++++++++++++++++++++++++--------
美国版/Food Labeling Management App UniApp/src/pages/labels/labels.vue | 10 +++++++++-
美国版/Food Labeling Management App UniApp/src/services/usAppLabeling.ts | 5 +++++
美国版/Food Labeling Management App UniApp/src/types/usAppLabeling.ts | 5 +++++
美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/Product/ProductCreateInputVo.cs | 5 ++++-
美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/UsAppLabeling/UsAppLabelingProductNodeDto.cs | 11 ++++++++++-
美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/IServices/IProductAppService.cs | 1 +
美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/IServices/IUsAppLabelingAppService.cs | 2 +-
美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application/Helpers/ReportsRoleHelper.cs | 49 +++++++++++++++++++++++++++++++++++++++++++------
美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application/Services/ProductAppService.cs | 63 +++++++++++++++++++++++++++++++++++++++++++++++++--------------
美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application/Services/UsAppLabelingAppService.cs | 19 ++++++++++++++++---
项目相关文档/报表Reports接口对接说明.md | 4 ++--
项目相关文档/标签模块接口对接说明.md | 35 +++++++++++++++++++++++++++--------
13 files changed, 199 insertions(+), 45 deletions(-)
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 | 主图 |
--
libgit2 0.21.4