From 43d16ca62e733f63d39e572b5c556cf8ee4dd302 Mon Sep 17 00:00:00 2001 From: jokerxue <2509699647@qq.com> Date: Thu, 2 Apr 2026 15:00:30 +0800 Subject: [PATCH] 打印日志 --- 标签模块接口对接说明(8).md | 961 ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- 标签模块接口对接说明.md | 1089 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 美国版/Food Labeling Management App UniApp/src/pages/labels/preview.vue | 38 +++++++++++++++++++++++++++++++++++--- 美国版/Food Labeling Management App UniApp/src/pages/more/print-log.vue | 250 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++----------------------------- 美国版/Food Labeling Management App UniApp/src/services/usAppLabeling.ts | 66 ++++++++++++++++++++++++++++++++++++++++++++++++++++-------------- 美国版/Food Labeling Management App UniApp/src/types/usAppLabeling.ts | 66 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++----- 美国版/Food Labeling Management App UniApp/src/utils/labelPreview/buildLabelPrintPayload.ts | 10 +++++++++- 美国版/Food Labeling Management App UniApp/src/utils/labelPreview/printInputOptions.ts | 16 ++++++++++++++++ 美国版/Food Labeling Management App UniApp/src/utils/labelPreview/renderLabelPreviewCanvas.ts | 55 ++++++++++++++++++++++++++++++++++++++++++------------- 美国版/Food Labeling Management App UniApp/src/utils/print/bleWriteModeRules.ts | 9 ++++++++- 美国版/Food Labeling Management App UniApp/src/utils/print/manager/printerManager.ts | 58 ++++++++++++++++++++++++++++++++++++---------------------- 美国版/Food Labeling Management App UniApp/src/utils/print/printerConnection.ts | 103 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++----------- 美国版/Food Labeling Management App UniApp/src/utils/print/systemTemplateAdapter.ts | 50 ++++++++++++++++++++++++++++++++++++++++++++++---- 美国版/Food Labeling Management App UniApp/src/utils/print/tscLabelBuilder.ts | 1 + 美国版/Food Labeling Management App UniApp/src/utils/printFromPrintDataList.ts | 536 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 美国版/Food Labeling Management App UniApp/src/utils/printSnapshotStorage.ts | 61 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 美国版/Food Labeling Management App UniApp/src/utils/reprintFromMergedTemplate.ts | 38 ++++++++++++++++++++++++++++++++++++++ 美国版/Food Labeling Management App UniApp/src/utils/resolveMediaUrl.ts | 19 +++++++++++++++++++ 美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/UsAppLabeling/PrintLogDataItemDto.cs | 18 ++++++++++++++++++ 美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/UsAppLabeling/PrintLogGetListInputVo.cs | 16 ++++++++++++++++ 美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/UsAppLabeling/PrintLogItemDto.cs | 44 ++++++++++++++++++++++++++++++++++++++++++++ 美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/UsAppLabeling/UsAppLabelPrintInputVo.cs | 11 ++++++----- 美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/UsAppLabeling/UsAppLabelPrintOutputDto.cs | 11 ++++++++++- 美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/UsAppLabeling/UsAppLabelReprintInputVo.cs | 21 +++++++++++++++++++++ 美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/IServices/IUsAppLabelingAppService.cs | 11 +++++++++++ 美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application/Services/UsAppLabelingAppService.cs | 468 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++--------------- 美国版/Food Labeling Management Platform/build/assets/index-BaZIqfDW.js | 458 -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- 美国版/Food Labeling Management Platform/build/assets/index-rOdDFGrB.js | 458 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 美国版/Food Labeling Management Platform/build/index.html | 2 +- 美国版/Food Labeling Management Platform/src/components/labels/LabelTemplateDataEntryView.tsx | 22 ++++++++++++++++------ 美国版/Food Labeling Management Platform/src/components/products/ProductsView.tsx | 24 ++++++++++++------------ 美国版/Food Labeling Management Platform/src/services/productService.ts | 5 +++-- 美国版/Food Labeling Management Platform/src/types/product.ts | 3 ++- 33 files changed, 3433 insertions(+), 1565 deletions(-) delete mode 100644 标签模块接口对接说明(8).md create mode 100644 标签模块接口对接说明.md create mode 100644 美国版/Food Labeling Management App UniApp/src/utils/printFromPrintDataList.ts create mode 100644 美国版/Food Labeling Management App UniApp/src/utils/printSnapshotStorage.ts create mode 100644 美国版/Food Labeling Management App UniApp/src/utils/reprintFromMergedTemplate.ts create mode 100644 美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/UsAppLabeling/PrintLogDataItemDto.cs create mode 100644 美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/UsAppLabeling/PrintLogGetListInputVo.cs create mode 100644 美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/UsAppLabeling/PrintLogItemDto.cs create mode 100644 美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/UsAppLabeling/UsAppLabelReprintInputVo.cs delete mode 100644 美国版/Food Labeling Management Platform/build/assets/index-BaZIqfDW.js create mode 100644 美国版/Food Labeling Management Platform/build/assets/index-rOdDFGrB.js diff --git a/标签模块接口对接说明(8).md b/标签模块接口对接说明(8).md deleted file mode 100644 index 8823a8f..0000000 --- a/标签模块接口对接说明(8).md +++ /dev/null @@ -1,961 +0,0 @@ -## 概述 - -美国版后端采用 ABP 动态接口(ConventionalControllers),宿主统一前缀为 `api/app`。 -Swagger 地址: - -- `http://localhost:19001/swagger` - -说明: -- 接口最终 URL 以 Swagger 展示为准(可在 Swagger 里搜索 `LabelCategory / LabelType / LabelMultipleOption / LabelTemplate / Label / UsAppLabeling`)。 -- 本模块后端接口以各 AppService 的方法签名自动暴露。 -- 返回分页统一包含 `PageIndex / PageSize / TotalCount / TotalPages / Items`。 - ---- - -## Swagger 中如何找到 - -1. 启动后端宿主(`Yi.Abp.Web`),端口 `19001`。 -2. 打开 `http://localhost:19001/swagger`。 -3. 在接口分组里搜索以下关键词之一: - - `label-category` - - `label-type` - - `label-multiple-option` - - `label-template` - - `label` - - `us-app-labeling`(App 端 Labeling 四级树) - ---- - -## 接口 1:Label Categories(标签分类) - -### 1.1 分页列表 - -方法:`GET /api/app/label-category` - -入参(`LabelCategoryGetListInputVo`,查询参数): - -- `skipCount`(int) -- `maxResultCount`(int) -- `sorting`(string,可选) -- `keyword`(string,可选) -- `state`(boolean,可选) - -示例(查询参数): - -```json -{ - "skipCount": 0, - "maxResultCount": 10, - "keyword": "Prep", - "state": true -} -``` - -### 1.2 详情 - -方法:`GET /api/app/label-category/{id}` - -入参: - -- `id`:分类 Id(字符串) - -### 1.3 新增 - -方法:`POST /api/app/label-category` - -入参(Body:`LabelCategoryCreateInputVo`): - -```json -{ - "categoryCode": "CAT_PREP", - "categoryName": "Prep", - "categoryPhotoUrl": "https://cdn.example.com/cat-prep.png", - "state": true, - "orderNum": 1 -} -``` - -### 1.4 编辑 - -方法:`PUT /api/app/label-category/{id}` - -入参(Body:`LabelCategoryUpdateInputVo`,字段同创建): - -```json -{ - "categoryCode": "CAT_PREP", - "categoryName": "Prep", - "categoryPhotoUrl": null, - "state": true, - "orderNum": 2 -} -``` - -### 1.5 删除(逻辑删除) - -方法:`DELETE /api/app/label-category/{id}` - -入参: - -- `id`:分类 Id(字符串) - -删除校验: -- 若该分类已被 `fl_label` 引用,则抛出友好错误,禁止删除。 - ---- - -## 接口 2:Label Types(标签类型) - -### 2.1 分页列表 - -方法:`GET /api/app/label-type` - -入参(`LabelTypeGetListInputVo`,查询参数): - -```json -{ - "skipCount": 0, - "maxResultCount": 10, - "keyword": "Defrost", - "state": true -} -``` - -### 2.2 详情 - -方法:`GET /api/app/label-type/{id}` - -入参: - -- `id`:类型 Id(字符串) - -### 2.3 新增 - -方法:`POST /api/app/label-type` - -入参(Body:`LabelTypeCreateInputVo`): - -```json -{ - "typeCode": "TYPE_DEFROST", - "typeName": "Defrost", - "state": true, - "orderNum": 1 -} -``` - -### 2.4 编辑 - -方法:`PUT /api/app/label-type/{id}` - -入参(Body:`LabelTypeUpdateInputVo`,字段同创建): - -```json -{ - "typeCode": "TYPE_DEFROST", - "typeName": "Defrost", - "state": true, - "orderNum": 2 -} -``` - -### 2.5 删除(逻辑删除) - -方法:`DELETE /api/app/label-type/{id}` - -删除校验: -- 若该类型已被 `fl_label` 引用,则禁止删除。 - ---- - -## 接口 3:Multiple Options(多选项字典) - -### 3.1 分页列表 - -方法:`GET /api/app/label-multiple-option` - -入参(`LabelMultipleOptionGetListInputVo`,查询参数): - -```json -{ - "skipCount": 0, - "maxResultCount": 10, - "keyword": "Allergens", - "state": true -} -``` - -### 3.2 详情 - -方法:`GET /api/app/label-multiple-option/{id}` - -入参: - -- `id`:多选项 Id(字符串) - -### 3.3 新增 - -方法:`POST /api/app/label-multiple-option` - -入参(Body:`LabelMultipleOptionCreateInputVo`): - -```json -{ - "optionCode": "OPT_ALLERGENS", - "optionName": "Allergens", - "optionValuesJson": ["Peanuts", "Dairy", "Gluten", "Soy"], - "state": true, - "orderNum": 1 -} -``` - -### 3.4 编辑 - -方法:`PUT /api/app/label-multiple-option/{id}` - -入参(Body:`LabelMultipleOptionUpdateInputVo`,字段同创建): - -```json -{ - "optionCode": "OPT_ALLERGENS", - "optionName": "Allergens", - "optionValuesJson": ["Peanuts", "Dairy"], - "state": true, - "orderNum": 2 -} -``` - -### 3.5 删除(逻辑删除) - -方法:`DELETE /api/app/label-multiple-option/{id}` - ---- - -## 接口 4:Label Templates(标签模板) - -说明: -- 模板标识入参 `id` 使用 `fl_label_template.TemplateCode`。 -- 创建/编辑的 Body 字段名对齐你前端 editor JSON(`id/name/appliedLocation/elements/config`)。 -- 模板组件 `elements[]` 的 `elementName` 为必填(前端传值,后端校验为空会报错)。 -- `templateProductDefaults[]` 用于模板内“产品 + 标签类型”绑定默认值(进入模板详情页后的绑定列表)。 -- **新增模板时不处理默认值**;默认值仅在后续“产品关联/编辑模板”阶段维护。 - -### 4.1 分页列表 - -方法:`GET /api/app/label-template` - -入参(`LabelTemplateGetListInputVo`,查询参数): - -```json -{ - "skipCount": 0, - "maxResultCount": 10, - "keyword": "测试模板", - "locationId": "11111111-1111-1111-1111-111111111111", - "labelType": "PRICE", - "state": true -} -``` - -### 4.2 详情 - -方法:`GET /api/app/label-template/{id}` - -入参: - -- `id`:模板编码 `TemplateCode`(字符串) - -### 4.3 新增模板 - -方法:`POST /api/app/label-template` - -入参(Body:`LabelTemplateCreateInputVo`): - -```json -{ - "id": "TPL_TEST_001", - "name": "测试模板-价格签(4x6)", - "labelType": "PRICE", - "unit": "inch", - "width": 4, - "height": 6, - "appliedLocation": "ALL", - "showRuler": true, - "showGrid": true, - "state": true, - "elements": [ - { - "id": "el-fixed-title", - "elementName": "标题文本", - "type": "TEXT_STATIC", - "x": 32, - "y": 24, - "width": 160, - "height": 24, - "rotation": "horizontal", - "border": "none", - "zIndex": 1, - "orderNum": 1, - "valueSourceType": "FIXED", - "isRequiredInput": false, - "config": { - "text": "商品名", - "fontFamily": "Arial", - "fontSize": 14, - "fontWeight": "bold", - "textAlign": "left" - } - } - ], - "appliedLocationIds": [] -} -``` - -说明: -- 当 `appliedLocation=SPECIFIED` 时,`appliedLocationIds` 必须至少选择一个门店。 -- `elements[].elementName` 必填;为空或空白将返回友好错误:`组件名字不能为空`。 -- 新增模板时即使传了 `templateProductDefaults`,后端也不会写入默认值数据。 - -### 4.4 编辑模板 - -方法:`PUT /api/app/label-template/{id}` - -入参: -- Path:`id` 是当前模板编码(TemplateCode) -- Body:字段同新增(`id/name/elements/...`) - -示例(编辑:同样字段,appliedLocation 切到 SPECIFIED): - -```json -{ - "id": "TPL_TEST_001", - "name": "测试模板-价格签(4x6) v2", - "labelType": "PRICE", - "unit": "inch", - "width": 4, - "height": 6, - "appliedLocation": "SPECIFIED", - "showRuler": true, - "showGrid": true, - "state": true, - "elements": [ - { - "id": "el-price", - "elementName": "价格文本", - "type": "TEXT_PRICE", - "x": 40, - "y": 120, - "width": 140, - "height": 28, - "rotation": "horizontal", - "border": "none", - "zIndex": 2, - "orderNum": 2, - "valueSourceType": "PRINT_INPUT", - "inputKey": "price", - "isRequiredInput": true, - "config": { - "text": "", - "fontFamily": "Arial", - "fontSize": 18, - "fontWeight": "bold", - "textAlign": "left" - } - } - ], - "appliedLocationIds": ["11111111-1111-1111-1111-111111111111"], - "templateProductDefaults": [ - { - "productId": "3a20-xxxx", - "labelTypeId": "3a20-yyyy", - "defaultValues": { - "el-fixed-title": "Chicken", - "el-price": "2.00", - "el-desc": "23" - }, - "orderNum": 1 - } - ] -} -``` - -版本: -- `VersionNo` 会在编辑时自动 `+1`。 -- `elements` 会按传入内容全量重建。 -- 查询/预览返回的 `elements[]` 同样会带 `elementName` 字段。 -- `templateProductDefaults` 在编辑接口中**仅当显式传入时**才会重建(同一模板先删后插)。 -- 若编辑时不传 `templateProductDefaults`,后端会保留数据库中原有默认值,不做覆盖。 - -`templateProductDefaults` 结构说明: -- 每一行需传 `productId` 与 `labelTypeId`。 -- `defaultValues` 建议使用 `element.id => 默认文本` 结构;页面展示时可结合模板 `elements[].config.text` 作为列头与初始值。 - -### 4.6 模板与产品默认值关联表(新增) - -用于存储“模板-产品-标签类型”的默认值,推荐执行以下建表 SQL: - -```sql -CREATE TABLE `fl_label_template_product_default` ( - `Id` varchar(36) NOT NULL COMMENT '主键', - `TemplateId` varchar(36) NOT NULL COMMENT '模板Id(关联 fl_label_template.Id)', - `ProductId` varchar(36) NOT NULL COMMENT '产品Id(关联 fl_product.Id)', - `LabelTypeId` varchar(36) NOT NULL COMMENT '标签类型Id(关联 fl_label_type.Id)', - `DefaultValuesJson` text NULL COMMENT '默认值JSON(如 elementId=>默认文本)', - `OrderNum` int NOT NULL DEFAULT 1 COMMENT '排序', - PRIMARY KEY (`Id`), - KEY `idx_fl_ltpd_template` (`TemplateId`), - KEY `idx_fl_ltpd_product` (`ProductId`), - KEY `idx_fl_ltpd_label_type` (`LabelTypeId`), - UNIQUE KEY `uk_fl_ltpd_template_product_label_type` (`TemplateId`, `ProductId`, `LabelTypeId`) -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='标签模板-产品默认值关联表'; -``` - -若表已存在,可执行以下 SQL 增加唯一约束: - -```sql -ALTER TABLE `fl_label_template_product_default` -ADD UNIQUE KEY `uk_fl_ltpd_template_product_label_type` (`TemplateId`, `ProductId`, `LabelTypeId`); -``` - -### 4.5 删除(逻辑删除) - -方法:`DELETE /api/app/label-template/{id}` - -入参: -- `id`:模板编码 `TemplateCode` - -删除校验: -- 若该模板已被 `fl_label` 引用,则禁止删除。 - ---- - -## 接口 5:Labels(按产品展示多个标签) - -说明: -- 列表接口以“标签”为维度分页展示(同一个标签会绑定多个产品)。 -- 列表支持按 `ProductId` 过滤:仅返回“绑定了该产品”的标签。 -- 标签详情/编辑/删除的 `id` 使用 `fl_label.LabelCode`。 - -### 5.1 分页列表(按产品) - -方法:`GET /api/app/label` - -入参(`LabelGetListInputVo`,查询参数): - -```json -{ - "skipCount": 0, - "maxResultCount": 10, - "sorting": "", - "keyword": "早餐", - "locationId": "11111111-1111-1111-1111-111111111111", - "productId": "22222222-2222-2222-2222-222222222222", - "labelCategoryId": "33333333-3333-3333-3333-333333333333", - "labelTypeId": "44444444-4444-4444-4444-444444444444", - "templateCode": "TPL_TEST_001", - "state": true -} -``` - -列表出参要点(`LabelGetListOutputDto`): - -- `products`:同一个标签下绑定的产品名称,用 `,` 分割(例如:`Chicken,Sandwich`) -- 其他字段与之前一致:`labelName/locationName/category/type/template/state/lastEdited...` - -### 5.2 详情 - -方法:`GET /api/app/label/{id}` - -入参: -- `id`:标签编码 `LabelCode` - -返回: -- `productIds`:该标签绑定的产品Id 列表 - -### 5.3 新增标签 - -方法:`POST /api/app/label` - -入参(Body:`LabelCreateInputVo`): - -```json -{ - "labelCode": "LBL_TEST_001", - "labelName": "早餐标签", - "templateCode": "TPL_TEST_001", - "locationId": "11111111-1111-1111-1111-111111111111", - "labelCategoryId": "33333333-3333-3333-3333-333333333333", - "labelTypeId": "44444444-4444-4444-4444-444444444444", - "productIds": ["22222222-2222-2222-2222-222222222222"], - "labelInfoJson": { "note": "测试标签1" }, - "state": true -} -``` - -校验: -- `productIds` 至少 1 个 -- `templateCode/locationId/labelCategoryId/labelTypeId` 不能为空 - -### 5.4 编辑标签 - -方法:`PUT /api/app/label/{id}` - -入参: -- Path:`id` 为当前标签编码 `LabelCode` -- Body:字段同创建(`LabelUpdateInputVo`) - -```json -{ - "labelName": "早餐标签 v2", - "templateCode": "TPL_TEST_001", - "locationId": "11111111-1111-1111-1111-111111111111", - "labelCategoryId": "33333333-3333-3333-3333-333333333333", - "labelTypeId": "44444444-4444-4444-4444-444444444444", - "productIds": ["22222222-2222-2222-2222-222222222222"], - "labelInfoJson": { "note": "测试标签1 v2" }, - "state": true -} -``` - -关联维护: -- `fl_label_product` 会按新 `productIds` 重建。 - -### 5.5 删除标签(逻辑删除) - -方法:`DELETE /api/app/label/{id}` - -入参: -- `id`:标签编码 `LabelCode` - -删除行为: -- 逻辑删除 `fl_label` -- 删除该标签对应的 `fl_label_product` 关联 - ---- -## 接口 6:Products(产品) - -说明: -- 产品表:`fl_product` -- 删除为逻辑删除:`IsDeleted = true` - -### 6.1 分页列表 - -方法:`GET /api/app/product` - -入参(`ProductGetListInputVo`,查询参数): -```json -{ - "skipCount": 0, - "maxResultCount": 10, - "sorting": "", - "keyword": "Chicken", - "state": true -} -``` - -### 6.2 详情 - -方法:`GET /api/app/product/{id}` - -入参: -- `id`:产品Id(`fl_product.Id`) - -### 6.3 新增产品 - -方法:`POST /api/app/product` - -入参(Body:`ProductCreateInputVo`): -```json -{ - "productCode": "PRD_TEST_001", - "productName": "Chicken", - "categoryName": "Meat", - "productImageUrl": "https://example.com/img.png", - "state": true -} -``` - -校验: -- `productCode/productName` 不能为空 -- `productCode` 不能与未删除的数据重复 - -### 6.4 编辑产品 - -方法:`PUT /api/app/product/{id}` - -入参: -- Path:`id` 为当前产品Id(`fl_product.Id`) -- Body:字段同新增(`ProductUpdateInputVo`) - -### 6.5 删除(逻辑删除) - -方法:`DELETE /api/app/product/{id}` - -入参: -- `id`:产品Id - ---- -## 接口 7:Product-Location(门店-产品关联) - -说明: -- 关联表:`fl_location_product` -- 关联按门店进行批量替换: - - `Create`:在门店下新增未存在的 product 关联 - - `Update`:替换该门店下全部关联(先删后建) - - `Delete`:删除该门店下全部关联 - -### 7.1 分页列表 - -方法:`GET /api/app/product-location` - -入参(`ProductLocationGetListInputVo`,查询参数): -```json -{ - "skipCount": 0, - "maxResultCount": 10, - "sorting": "", - "locationId": "11111111-1111-1111-1111-111111111111", - "productId": "22222222-2222-2222-2222-222222222222" -} -``` - -### 7.2 获取门店下全部产品 - -方法:`GET /api/app/product-location/{id}` - -入参: -- `id`:门店Id(`location.Id`,string 表示) - -返回: -- 门店Id + 该门店关联的产品列表 - -### 7.3 新增/建立门店关联 - -方法:`POST /api/app/product-location` - -入参(Body:`ProductLocationCreateInputVo`): -```json -{ - "locationId": "11111111-1111-1111-1111-111111111111", - "productIds": ["22222222-2222-2222-2222-222222222222"] -} -``` - -校验: -- `locationId` 对应门店必须存在 -- `productIds` 必须都存在于 `fl_product` 且未删除 - -### 7.4 编辑/替换门店关联 - -方法:`PUT /api/app/product-location/{id}` - -入参: -- Path:`id` 为门店Id -- Body:`ProductLocationUpdateInputVo` -```json -{ - "productIds": ["22222222-2222-2222-2222-222222222222"] -} -``` - -### 7.5 删除门店关联(按门店删除全部) - -方法:`DELETE /api/app/product-location/{id}` - -入参: -- `id`:门店Id - ---- - -## 接口 8:App Labeling 四级列表(门店打标页) - -**场景**:美国版 UniApp「Labeling」页:左侧 **标签分类(Label Category)** → 主区域按 **产品分类(Product Category)** 折叠分组 → **产品(Product)** 卡片 → 点选后底部弹层展示 **标签种类(Label Type)**。 - -**实现**:`UsAppLabelingAppService.GetLabelingTreeAsync`,约定式 API 控制器名 **`us-app-labeling`**(与 `UsAppAuth` → `us-app-auth` 同规则)。 - -### 8.1 获取四级嵌套树 - -#### HTTP - -- **方法**:`GET` -- **路径**:`/api/app/us-app-labeling/labeling-tree`(若与 Swagger 不一致,**以 Swagger 为准**) -- **鉴权**:需要登录(`Authorization: Bearer ...`);可使用 App 登录或 Web 账号 Token,需能通过 `[Authorize]`。当前用户可选门店列表见 **`/api/app/us-app-auth/my-locations`**(说明见 `美国版App登录接口说明.md`)。 - -#### 入参(Query:`UsAppLabelingTreeInputVo`) - -| 参数名 | 类型 | 必填 | 说明 | -|--------|------|------|------| -| `locationId` | string | 是 | 当前门店 Id(`location.Id`,与 `fl_location_product.LocationId`、`fl_label.LocationId` 一致) | -| `keyword` | string | 否 | 模糊过滤:标签名、产品名、产品分类、**标签分类**名、标签类型名、`labelCode` 等(实现见服务内 `WhereIF`) | -| `labelCategoryId` | string | 否 | 侧边栏只展示某一 **标签分类** 时传入;不传则返回当前门店下出现的全部标签分类节点 | - -#### 数据范围与表关联(便于联调对照) - -- **门店内商品**:`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` 等业务字段)。 - -#### 出参(`List`) - -若宿主对成功结果有统一包装,业务数组一般在 **`data`** 中;下列为 **解包后的数组项** 结构。 - -**L1 `UsAppLabelCategoryTreeNodeDto`(标签分类)** - -| 字段 | 类型 | 说明 | -|------|------|------| -| `id` | string | `fl_label_category.Id` | -| `categoryName` | string | 分类名称 | -| `categoryPhotoUrl` | string \| null | 分类图标/图 | -| `orderNum` | number | 排序 | -| `productCategories` | array | 第二级列表(见下表) | - -**L2 `UsAppProductCategoryNodeDto`(产品分类)** - -| 字段 | 类型 | 说明 | -|------|------|------| -| `categoryId` | string \| null | 产品分类Id;产品未归类或分类不存在时为空 | -| `categoryPhotoUrl` | string \| null | 产品分类图片地址;产品未归类或分类不存在时为空 | -| `name` | string | 产品分类显示名;空源数据为 **`无`** | -| `itemCount` | number | 该分类下 **产品个数**(去重后的产品数) | -| `products` | array | 第三级产品列表(见下表) | - -**L3 `UsAppLabelingProductNodeDto`(产品)** - -| 字段 | 类型 | 说明 | -|------|------|------| -| `productId` | string | `fl_product.Id` | -| `productName` | string | 产品名称 | -| `productCode` | string | 产品编码 | -| `productImageUrl` | string \| null | 主图 | -| `subtitle` | string | 卡片副标题:**有 `productCode` 则显示编码,否则「无」**(与原型「Basic」等独立文案不同,需另行扩展字段时再对齐) | -| `labelTypeCount` | number | 第四级条数,可用于角标「N Types」 | -| `labelTypes` | array | 第四级(见下表) | - -**L4 `UsAppLabelTypeNodeDto`(标签种类 / 可选项)** - -| 字段 | 类型 | 说明 | -|------|------|------| -| `labelTypeId` | string | `fl_label_type.Id` | -| `typeName` | string | 类型名称(如 Defrost) | -| `orderNum` | number | 排序 | -| `labelCode` | string | 业务标签编码,后续预览、打印流程使用 | -| `templateCode` | string \| null | 关联模板编码 | -| `labelSizeText` | string \| null | 尺寸文案;`inch` 常用格式如 `2"x2"` | - -#### 错误与边界 - -- `locationId` 为空:返回友好错误 **「门店Id不能为空」**。 -- 门店下无关联产品:返回 **空数组** `[]`。 -- 有产品但无任何符合条件的标签关联:返回 **空数组** `[]`。 - -#### 请求示例 - -```http -GET /api/app/us-app-labeling/labeling-tree?locationId=11111111-1111-1111-1111-111111111111&labelCategoryId=a2696b9e-2277-11f1-b4c6-00163e0c7c4f&keyword=Chicken HTTP/1.1 -Host: localhost:19001 -Authorization: Bearer eyJhbGciOi... -``` - -**curl**(Token 取自登录响应的 `data.token` 整段,已含 `Bearer ` 前缀时直接放入 Header): - -```bash -curl -X GET "http://localhost:19001/api/app/us-app-labeling/labeling-tree?locationId=11111111-1111-1111-1111-111111111111" \ - -H "Authorization: " -``` - -#### 响应结构示例(解包后) - -```json -[ - { - "id": "cat-prep-id", - "categoryName": "Prep", - "categoryPhotoUrl": "/picture/...", - "orderNum": 1, - "productCategories": [ - { - "categoryId": "pc-meat-id", - "categoryPhotoUrl": "/picture/product-category/20260325123010_xxx.png", - "name": "Meat", - "itemCount": 1, - "products": [ - { - "productId": "prod-chicken-id", - "productName": "Chicken", - "productCode": "CHK-001", - "productImageUrl": "/picture/...", - "subtitle": "CHK-001", - "labelTypeCount": 3, - "labelTypes": [ - { - "labelTypeId": "lt-defrost", - "typeName": "Defrost", - "orderNum": 1, - "labelCode": "LBL_CHICKEN_DEFROST", - "templateCode": "TPL_2X2", - "labelSizeText": "2\"x2\"" - } - ] - } - ] - } - ] - } -] -``` - -> 前端 Axios 若项目约定 **GET 使用 `data` 配置对象** 传参,请仍绑定到与上述 Query 同名的字段(`locationId`、`keyword`、`labelCategoryId`),与 URL Query 等价即可。 - -### 8.2 App 打印预览(elements 渲染结构) - -**场景**:用户选择某个 Product + Label Type 进入「Label Preview」页面,需要把模板预览区域渲染出来。 -后端根据 `labelCode` 读取模板(`fl_label_template` + `fl_label_template_element`),并将 AUTO_DB / PRINT_INPUT 的值渲染回每个 element 的 `config`,前端按 `elements` 自行绘制预览。 - -#### HTTP - -- **方法**:`POST` -- **路径**:`/api/app/us-app-labeling/preview`(若与 Swagger 不一致,**以 Swagger 为准**) -- **鉴权**:需要登录(`Authorization: Bearer ...`) - -#### 入参(Body:`UsAppLabelPreviewInputVo`) - -| 参数名(JSON) | 类型 | 必填 | 说明 | -|---|---|---|---| -| `locationId` | string | 是 | 门店Id(校验 `fl_label.LocationId` 必须一致) | -| `labelCode` | string | 是 | 标签编码(`fl_label.LabelCode`) | -| `productId` | string | 否 | 预览用产品Id;不传则默认取该标签绑定的第一个产品(用于 AUTO_DB 数据填充) | -| `baseTime` | string | 否 | 业务基准时间(用于 DATE/TIME 元素计算;不传则用服务器当前时间) | -| `printInputJson` | object | 否 | 打印输入(用于 PRINT_INPUT 元素),key 建议与模板元素 `inputKey` 对齐 | - -#### 出参(`UsAppLabelPreviewDto`) - -除顶部信息外,核心是 `template`(供前端画布渲染): - -- `template`:`LabelTemplatePreviewDto` - - `width` / `height` / `unit`:模板物理尺寸 - - `elements[]`:元素数组(对齐前端 editor JSON:`id/elementName/type/x/y/width/height/rotation/border/zIndex/orderNum/config`) - -`elements[].config` 内常用字段(示例): - -- 文本类(如 `TEXT_PRODUCT` / `TEXT_STATIC` / `TEXT_PRICE`):`config.text` -- 条码/二维码(`BARCODE` / `QRCODE`):`config.data` -- 日期/时间(`DATE` / `TIME`):`config.format`(后端已计算并写回) - -#### 数据来源说明 - -- 模板头:`fl_label_template` -- 模板元素:`fl_label_template_element`(按 `OrderNum` + `ZIndex` 排序) -- 标签归属:`fl_label`(校验 `labelCode` 存在且 `LocationId == locationId`) - -#### 错误与边界 - -- `locationId` 为空:友好错误 **「门店Id不能为空」**。 -- `labelCode` 为空:友好错误 **「labelCode不能为空」**。 -- 标签不存在:友好错误 **「标签不存在」**。 -- 模板不存在:友好错误 **「模板不存在」**。 -- 标签不属于当前门店:友好错误 **「该标签不属于当前门店」**。 -- 标签未绑定产品且未传 `productId`:友好错误 **「该标签未绑定产品,无法预览」**。 - -#### 请求示例 - -```json -{ - "locationId": "11111111-1111-1111-1111-111111111111", - "labelCode": "LBL_CHICKEN_DEFROST", - "productId": "22222222-2222-2222-2222-222222222222", - "baseTime": "2026-03-26T10:30:00", - "printInputJson": { - "price": "12.99" - } -} -``` - -**curl:** - -```bash -curl -X POST "http://localhost:19001/api/app/us-app-labeling/preview" \ - -H "Authorization: " \ - -H "Content-Type: application/json" \ - -d '{"locationId":"11111111-1111-1111-1111-111111111111","labelCode":"LBL_CHICKEN_DEFROST","productId":"22222222-2222-2222-2222-222222222222","baseTime":"2026-03-26T10:30:00","printInputJson":{"price":"12.99"}}' -``` - ---- - -## 接口 9:App 打印(落库打印任务与明细) - -**场景**:移动端预览确认后点击 **Print**。后端负责把“本次打印”写入数据库,方便追溯/统计/重打。 - -### 9.1 创建打印任务并写入明细 - -#### HTTP - -- **方法**:`POST` -- **路径**:`/api/app/us-app-labeling/print`(若与 Swagger 不一致,**以 Swagger 为准**) -- **鉴权**:需要登录(`Authorization: Bearer ...`) - -#### 入参(Body:`UsAppLabelPrintInputVo`) - -| 参数名(JSON) | 类型 | 必填 | 说明 | -|---|---|---|---| -| `locationId` | string | 是 | 门店Id(校验 `fl_label.LocationId` 必须一致) | -| `labelCode` | string | 是 | 标签编码(`fl_label.LabelCode`) | -| `productId` | string | 否 | 打印用产品Id;不传则默认取该标签绑定的第一个产品(用于模板解析) | -| `printQuantity` | number | 否 | 打印份数;`<=0` 按 1 处理 | -| `baseTime` | string | 否 | 业务基准时间(用于 DATE/TIME 元素计算) | -| `printInputJson` | object | 否 | 打印输入(用于模板 PRINT_INPUT 元素),key 建议与模板元素 `inputKey` 对齐 | -| `printerId` | string | 否 | 打印机Id(可选,用于追踪) | -| `printerMac` | string | 否 | 打印机蓝牙 MAC(可选) | -| `printerAddress` | string | 否 | 打印机地址(可选) | - -#### 数据落库说明 - -- **任务表**:`fl_label_print_task` - - 插入 1 条任务记录(`locationId / labelCode / productId / labelTypeId / templateCode / printQuantity / baseTime / printer...` 等)。 -- **明细表**:`fl_label_print_data` - - 按 `printQuantity` 插入 N 条明细记录(`copyIndex = 1..N`)。 - - `printInputJson`:保存本次打印的原始输入(JSON 字符串)。 - - `renderDataJson`:保存本次解析后的模板预览结构(`LabelTemplatePreviewDto`,包含 resolved 后的 `elements[].config`),供追溯/重打使用。 - -> 模板解析的数据源来自 `fl_label_template` + `fl_label_template_element`,与预览接口一致。 - -#### 出参(`UsAppLabelPrintOutputDto`) - -| 字段 | 类型 | 说明 | -|---|---|---| -| `taskId` | string | 打印任务Id(用于后续查询/重打/统计) | -| `printQuantity` | number | 实际写入的份数 | - -#### 错误与边界 - -- `locationId` 为空:友好错误 **「门店Id不能为空」**。 -- `labelCode` 为空:友好错误 **「labelCode不能为空」**。 -- 标签不存在/不可用:友好错误 **「标签不存在或不可用」**。 -- 标签不属于当前门店:友好错误 **「该标签不属于当前门店」**。 -- 标签未绑定产品且未传 `productId`:友好错误 **「该标签未绑定产品,无法预览」**(模板解析阶段抛出)。 - -#### 请求示例 - -```json -{ - "locationId": "11111111-1111-1111-1111-111111111111", - "labelCode": "LBL_CHICKEN_DEFROST", - "productId": "22222222-2222-2222-2222-222222222222", - "printQuantity": 2, - "baseTime": "2026-03-26T10:30:00", - "printInputJson": { - "price": "12.99" - }, - "printerMac": "AA:BB:CC:DD:EE:FF" -} -``` - -**curl:** - -```bash -curl -X POST "http://localhost:19001/api/app/us-app-labeling/print" \ - -H "Authorization: " \ - -H "Content-Type: application/json" \ - -d '{"locationId":"11111111-1111-1111-1111-111111111111","labelCode":"LBL_CHICKEN_DEFROST","productId":"22222222-2222-2222-2222-222222222222","printQuantity":2,"baseTime":"2026-03-26T10:30:00","printInputJson":{"price":"12.99"}}' -``` - diff --git a/标签模块接口对接说明.md b/标签模块接口对接说明.md new file mode 100644 index 0000000..b8553a7 --- /dev/null +++ b/标签模块接口对接说明.md @@ -0,0 +1,1089 @@ +## 概述 + +美国版后端采用 ABP 动态接口(ConventionalControllers),宿主统一前缀为 `api/app`。 +Swagger 地址: + +- `http://localhost:19001/swagger` + +说明: +- 接口最终 URL 以 Swagger 展示为准(可在 Swagger 里搜索 `LabelCategory / LabelType / LabelMultipleOption / LabelTemplate / Label / UsAppLabeling`)。 +- 本模块后端接口以各 AppService 的方法签名自动暴露。 +- 返回分页统一包含 `PageIndex / PageSize / TotalCount / TotalPages / Items`。 + +--- + +## Swagger 中如何找到 + +1. 启动后端宿主(`Yi.Abp.Web`),端口 `19001`。 +2. 打开 `http://localhost:19001/swagger`。 +3. 在接口分组里搜索以下关键词之一: + - `label-category` + - `label-type` + - `label-multiple-option` + - `label-template` + - `label` + - `us-app-labeling`(App 端 Labeling 四级树) + +--- + +## 接口 1:Label Categories(标签分类) + +### 1.1 分页列表 + +方法:`GET /api/app/label-category` + +入参(`LabelCategoryGetListInputVo`,查询参数): + +- `skipCount`(int) +- `maxResultCount`(int) +- `sorting`(string,可选) +- `keyword`(string,可选) +- `state`(boolean,可选) + +示例(查询参数): + +```json +{ + "skipCount": 0, + "maxResultCount": 10, + "keyword": "Prep", + "state": true +} +``` + +### 1.2 详情 + +方法:`GET /api/app/label-category/{id}` + +入参: + +- `id`:分类 Id(字符串) + +### 1.3 新增 + +方法:`POST /api/app/label-category` + +入参(Body:`LabelCategoryCreateInputVo`): + +```json +{ + "categoryCode": "CAT_PREP", + "categoryName": "Prep", + "categoryPhotoUrl": "https://cdn.example.com/cat-prep.png", + "state": true, + "orderNum": 1 +} +``` + +### 1.4 编辑 + +方法:`PUT /api/app/label-category/{id}` + +入参(Body:`LabelCategoryUpdateInputVo`,字段同创建): + +```json +{ + "categoryCode": "CAT_PREP", + "categoryName": "Prep", + "categoryPhotoUrl": null, + "state": true, + "orderNum": 2 +} +``` + +### 1.5 删除(逻辑删除) + +方法:`DELETE /api/app/label-category/{id}` + +入参: + +- `id`:分类 Id(字符串) + +删除校验: +- 若该分类已被 `fl_label` 引用,则抛出友好错误,禁止删除。 + +--- + +## 接口 2:Label Types(标签类型) + +### 2.1 分页列表 + +方法:`GET /api/app/label-type` + +入参(`LabelTypeGetListInputVo`,查询参数): + +```json +{ + "skipCount": 0, + "maxResultCount": 10, + "keyword": "Defrost", + "state": true +} +``` + +### 2.2 详情 + +方法:`GET /api/app/label-type/{id}` + +入参: + +- `id`:类型 Id(字符串) + +### 2.3 新增 + +方法:`POST /api/app/label-type` + +入参(Body:`LabelTypeCreateInputVo`): + +```json +{ + "typeCode": "TYPE_DEFROST", + "typeName": "Defrost", + "state": true, + "orderNum": 1 +} +``` + +### 2.4 编辑 + +方法:`PUT /api/app/label-type/{id}` + +入参(Body:`LabelTypeUpdateInputVo`,字段同创建): + +```json +{ + "typeCode": "TYPE_DEFROST", + "typeName": "Defrost", + "state": true, + "orderNum": 2 +} +``` + +### 2.5 删除(逻辑删除) + +方法:`DELETE /api/app/label-type/{id}` + +删除校验: +- 若该类型已被 `fl_label` 引用,则禁止删除。 + +--- + +## 接口 3:Multiple Options(多选项字典) + +### 3.1 分页列表 + +方法:`GET /api/app/label-multiple-option` + +入参(`LabelMultipleOptionGetListInputVo`,查询参数): + +```json +{ + "skipCount": 0, + "maxResultCount": 10, + "keyword": "Allergens", + "state": true +} +``` + +### 3.2 详情 + +方法:`GET /api/app/label-multiple-option/{id}` + +入参: + +- `id`:多选项 Id(字符串) + +### 3.3 新增 + +方法:`POST /api/app/label-multiple-option` + +入参(Body:`LabelMultipleOptionCreateInputVo`): + +```json +{ + "optionCode": "OPT_ALLERGENS", + "optionName": "Allergens", + "optionValuesJson": ["Peanuts", "Dairy", "Gluten", "Soy"], + "state": true, + "orderNum": 1 +} +``` + +### 3.4 编辑 + +方法:`PUT /api/app/label-multiple-option/{id}` + +入参(Body:`LabelMultipleOptionUpdateInputVo`,字段同创建): + +```json +{ + "optionCode": "OPT_ALLERGENS", + "optionName": "Allergens", + "optionValuesJson": ["Peanuts", "Dairy"], + "state": true, + "orderNum": 2 +} +``` + +### 3.5 删除(逻辑删除) + +方法:`DELETE /api/app/label-multiple-option/{id}` + +--- + +## 接口 4:Label Templates(标签模板) + +说明: +- 模板标识入参 `id` 使用 `fl_label_template.TemplateCode`。 +- 创建/编辑的 Body 字段名对齐你前端 editor JSON(`id/name/appliedLocation/elements/config`)。 +- 模板组件 `elements[]` 的 `elementName` 为必填(前端传值,后端校验为空会报错)。 +- `templateProductDefaults[]` 用于模板内“产品 + 标签类型”绑定默认值(进入模板详情页后的绑定列表)。 +- **新增模板时不处理默认值**;默认值仅在后续“产品关联/编辑模板”阶段维护。 + +### 4.1 分页列表 + +方法:`GET /api/app/label-template` + +入参(`LabelTemplateGetListInputVo`,查询参数): + +```json +{ + "skipCount": 0, + "maxResultCount": 10, + "keyword": "测试模板", + "locationId": "11111111-1111-1111-1111-111111111111", + "labelType": "PRICE", + "state": true +} +``` + +### 4.2 详情 + +方法:`GET /api/app/label-template/{id}` + +入参: + +- `id`:模板编码 `TemplateCode`(字符串) + +### 4.3 新增模板 + +方法:`POST /api/app/label-template` + +入参(Body:`LabelTemplateCreateInputVo`): + +```json +{ + "id": "TPL_TEST_001", + "name": "测试模板-价格签(4x6)", + "labelType": "PRICE", + "unit": "inch", + "width": 4, + "height": 6, + "appliedLocation": "ALL", + "showRuler": true, + "showGrid": true, + "state": true, + "elements": [ + { + "id": "el-fixed-title", + "elementName": "标题文本", + "type": "TEXT_STATIC", + "x": 32, + "y": 24, + "width": 160, + "height": 24, + "rotation": "horizontal", + "border": "none", + "zIndex": 1, + "orderNum": 1, + "valueSourceType": "FIXED", + "isRequiredInput": false, + "config": { + "text": "商品名", + "fontFamily": "Arial", + "fontSize": 14, + "fontWeight": "bold", + "textAlign": "left" + } + } + ], + "appliedLocationIds": [] +} +``` + +说明: +- 当 `appliedLocation=SPECIFIED` 时,`appliedLocationIds` 必须至少选择一个门店。 +- `elements[].elementName` 必填;为空或空白将返回友好错误:`组件名字不能为空`。 +- 新增模板时即使传了 `templateProductDefaults`,后端也不会写入默认值数据。 + +### 4.4 编辑模板 + +方法:`PUT /api/app/label-template/{id}` + +入参: +- Path:`id` 是当前模板编码(TemplateCode) +- Body:字段同新增(`id/name/elements/...`) + +示例(编辑:同样字段,appliedLocation 切到 SPECIFIED): + +```json +{ + "id": "TPL_TEST_001", + "name": "测试模板-价格签(4x6) v2", + "labelType": "PRICE", + "unit": "inch", + "width": 4, + "height": 6, + "appliedLocation": "SPECIFIED", + "showRuler": true, + "showGrid": true, + "state": true, + "elements": [ + { + "id": "el-price", + "elementName": "价格文本", + "type": "TEXT_PRICE", + "x": 40, + "y": 120, + "width": 140, + "height": 28, + "rotation": "horizontal", + "border": "none", + "zIndex": 2, + "orderNum": 2, + "valueSourceType": "PRINT_INPUT", + "inputKey": "price", + "isRequiredInput": true, + "config": { + "text": "", + "fontFamily": "Arial", + "fontSize": 18, + "fontWeight": "bold", + "textAlign": "left" + } + } + ], + "appliedLocationIds": ["11111111-1111-1111-1111-111111111111"], + "templateProductDefaults": [ + { + "productId": "3a20-xxxx", + "labelTypeId": "3a20-yyyy", + "defaultValues": { + "el-fixed-title": "Chicken", + "el-price": "2.00", + "el-desc": "23" + }, + "orderNum": 1 + } + ] +} +``` + +版本: +- `VersionNo` 会在编辑时自动 `+1`。 +- `elements` 会按传入内容全量重建。 +- 查询/预览返回的 `elements[]` 同样会带 `elementName` 字段。 +- `templateProductDefaults` 在编辑接口中**仅当显式传入时**才会重建(同一模板先删后插)。 +- 若编辑时不传 `templateProductDefaults`,后端会保留数据库中原有默认值,不做覆盖。 + +`templateProductDefaults` 结构说明: +- 每一行需传 `productId` 与 `labelTypeId`。 +- `defaultValues` 建议使用 `element.id => 默认文本` 结构;页面展示时可结合模板 `elements[].config.text` 作为列头与初始值。 + +### 4.6 模板与产品默认值关联表(新增) + +用于存储“模板-产品-标签类型”的默认值,推荐执行以下建表 SQL: + +```sql +CREATE TABLE `fl_label_template_product_default` ( + `Id` varchar(36) NOT NULL COMMENT '主键', + `TemplateId` varchar(36) NOT NULL COMMENT '模板Id(关联 fl_label_template.Id)', + `ProductId` varchar(36) NOT NULL COMMENT '产品Id(关联 fl_product.Id)', + `LabelTypeId` varchar(36) NOT NULL COMMENT '标签类型Id(关联 fl_label_type.Id)', + `DefaultValuesJson` text NULL COMMENT '默认值JSON(如 elementId=>默认文本)', + `OrderNum` int NOT NULL DEFAULT 1 COMMENT '排序', + PRIMARY KEY (`Id`), + KEY `idx_fl_ltpd_template` (`TemplateId`), + KEY `idx_fl_ltpd_product` (`ProductId`), + KEY `idx_fl_ltpd_label_type` (`LabelTypeId`), + UNIQUE KEY `uk_fl_ltpd_template_product_label_type` (`TemplateId`, `ProductId`, `LabelTypeId`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='标签模板-产品默认值关联表'; +``` + +若表已存在,可执行以下 SQL 增加唯一约束: + +```sql +ALTER TABLE `fl_label_template_product_default` +ADD UNIQUE KEY `uk_fl_ltpd_template_product_label_type` (`TemplateId`, `ProductId`, `LabelTypeId`); +``` + +### 4.5 删除(逻辑删除) + +方法:`DELETE /api/app/label-template/{id}` + +入参: +- `id`:模板编码 `TemplateCode` + +删除校验: +- 若该模板已被 `fl_label` 引用,则禁止删除。 + +--- + +## 接口 5:Labels(按产品展示多个标签) + +说明: +- 列表接口以“标签”为维度分页展示(同一个标签会绑定多个产品)。 +- 列表支持按 `ProductId` 过滤:仅返回“绑定了该产品”的标签。 +- 标签详情/编辑/删除的 `id` 使用 `fl_label.LabelCode`。 + +### 5.1 分页列表(按产品) + +方法:`GET /api/app/label` + +入参(`LabelGetListInputVo`,查询参数): + +```json +{ + "skipCount": 0, + "maxResultCount": 10, + "sorting": "", + "keyword": "早餐", + "locationId": "11111111-1111-1111-1111-111111111111", + "productId": "22222222-2222-2222-2222-222222222222", + "labelCategoryId": "33333333-3333-3333-3333-333333333333", + "labelTypeId": "44444444-4444-4444-4444-444444444444", + "templateCode": "TPL_TEST_001", + "state": true +} +``` + +列表出参要点(`LabelGetListOutputDto`): + +- `products`:同一个标签下绑定的产品名称,用 `,` 分割(例如:`Chicken,Sandwich`) +- 其他字段与之前一致:`labelName/locationName/category/type/template/state/lastEdited...` + +### 5.2 详情 + +方法:`GET /api/app/label/{id}` + +入参: +- `id`:标签编码 `LabelCode` + +返回: +- `productIds`:该标签绑定的产品Id 列表 + +### 5.3 新增标签 + +方法:`POST /api/app/label` + +入参(Body:`LabelCreateInputVo`): + +```json +{ + "labelCode": "LBL_TEST_001", + "labelName": "早餐标签", + "templateCode": "TPL_TEST_001", + "locationId": "11111111-1111-1111-1111-111111111111", + "labelCategoryId": "33333333-3333-3333-3333-333333333333", + "labelTypeId": "44444444-4444-4444-4444-444444444444", + "productIds": ["22222222-2222-2222-2222-222222222222"], + "labelInfoJson": { "note": "测试标签1" }, + "state": true +} +``` + +校验: +- `productIds` 至少 1 个 +- `templateCode/locationId/labelCategoryId/labelTypeId` 不能为空 + +### 5.4 编辑标签 + +方法:`PUT /api/app/label/{id}` + +入参: +- Path:`id` 为当前标签编码 `LabelCode` +- Body:字段同创建(`LabelUpdateInputVo`) + +```json +{ + "labelName": "早餐标签 v2", + "templateCode": "TPL_TEST_001", + "locationId": "11111111-1111-1111-1111-111111111111", + "labelCategoryId": "33333333-3333-3333-3333-333333333333", + "labelTypeId": "44444444-4444-4444-4444-444444444444", + "productIds": ["22222222-2222-2222-2222-222222222222"], + "labelInfoJson": { "note": "测试标签1 v2" }, + "state": true +} +``` + +关联维护: +- `fl_label_product` 会按新 `productIds` 重建。 + +### 5.5 删除标签(逻辑删除) + +方法:`DELETE /api/app/label/{id}` + +入参: +- `id`:标签编码 `LabelCode` + +删除行为: +- 逻辑删除 `fl_label` +- 删除该标签对应的 `fl_label_product` 关联 + +--- +## 接口 6:Products(产品) + +说明: +- 产品表:`fl_product` +- 删除为逻辑删除:`IsDeleted = true` + +### 6.1 分页列表 + +方法:`GET /api/app/product` + +入参(`ProductGetListInputVo`,查询参数): +```json +{ + "skipCount": 0, + "maxResultCount": 10, + "sorting": "", + "keyword": "Chicken", + "state": true +} +``` + +### 6.2 详情 + +方法:`GET /api/app/product/{id}` + +入参: +- `id`:产品Id(`fl_product.Id`) + +### 6.3 新增产品 + +方法:`POST /api/app/product` + +入参(Body:`ProductCreateInputVo`): +```json +{ + "productCode": "PRD_TEST_001", + "productName": "Chicken", + "categoryName": "Meat", + "productImageUrl": "https://example.com/img.png", + "state": true +} +``` + +校验: +- `productCode/productName` 不能为空 +- `productCode` 不能与未删除的数据重复 + +### 6.4 编辑产品 + +方法:`PUT /api/app/product/{id}` + +入参: +- Path:`id` 为当前产品Id(`fl_product.Id`) +- Body:字段同新增(`ProductUpdateInputVo`) + +### 6.5 删除(逻辑删除) + +方法:`DELETE /api/app/product/{id}` + +入参: +- `id`:产品Id + +--- +## 接口 7:Product-Location(门店-产品关联) + +说明: +- 关联表:`fl_location_product` +- 关联按门店进行批量替换: + - `Create`:在门店下新增未存在的 product 关联 + - `Update`:替换该门店下全部关联(先删后建) + - `Delete`:删除该门店下全部关联 + +### 7.1 分页列表 + +方法:`GET /api/app/product-location` + +入参(`ProductLocationGetListInputVo`,查询参数): +```json +{ + "skipCount": 0, + "maxResultCount": 10, + "sorting": "", + "locationId": "11111111-1111-1111-1111-111111111111", + "productId": "22222222-2222-2222-2222-222222222222" +} +``` + +### 7.2 获取门店下全部产品 + +方法:`GET /api/app/product-location/{id}` + +入参: +- `id`:门店Id(`location.Id`,string 表示) + +返回: +- 门店Id + 该门店关联的产品列表 + +### 7.3 新增/建立门店关联 + +方法:`POST /api/app/product-location` + +入参(Body:`ProductLocationCreateInputVo`): +```json +{ + "locationId": "11111111-1111-1111-1111-111111111111", + "productIds": ["22222222-2222-2222-2222-222222222222"] +} +``` + +校验: +- `locationId` 对应门店必须存在 +- `productIds` 必须都存在于 `fl_product` 且未删除 + +### 7.4 编辑/替换门店关联 + +方法:`PUT /api/app/product-location/{id}` + +入参: +- Path:`id` 为门店Id +- Body:`ProductLocationUpdateInputVo` +```json +{ + "productIds": ["22222222-2222-2222-2222-222222222222"] +} +``` + +### 7.5 删除门店关联(按门店删除全部) + +方法:`DELETE /api/app/product-location/{id}` + +入参: +- `id`:门店Id + +--- + +## 接口 8:App Labeling 四级列表(门店打标页) + +**场景**:美国版 UniApp「Labeling」页:左侧 **标签分类(Label Category)** → 主区域按 **产品分类(Product Category)** 折叠分组 → **产品(Product)** 卡片 → 点选后底部弹层展示 **标签种类(Label Type)**。 + +**实现**:`UsAppLabelingAppService.GetLabelingTreeAsync`,约定式 API 控制器名 **`us-app-labeling`**(与 `UsAppAuth` → `us-app-auth` 同规则)。 + +### 8.1 获取四级嵌套树 + +#### HTTP + +- **方法**:`GET` +- **路径**:`/api/app/us-app-labeling/labeling-tree`(若与 Swagger 不一致,**以 Swagger 为准**) +- **鉴权**:需要登录(`Authorization: Bearer ...`);可使用 App 登录或 Web 账号 Token,需能通过 `[Authorize]`。当前用户可选门店列表见 **`/api/app/us-app-auth/my-locations`**(说明见 `美国版App登录接口说明.md`)。 + +#### 入参(Query:`UsAppLabelingTreeInputVo`) + +| 参数名 | 类型 | 必填 | 说明 | +|--------|------|------|------| +| `locationId` | string | 是 | 当前门店 Id(`location.Id`,与 `fl_location_product.LocationId`、`fl_label.LocationId` 一致) | +| `keyword` | string | 否 | 模糊过滤:标签名、产品名、产品分类、**标签分类**名、标签类型名、`labelCode` 等(实现见服务内 `WhereIF`) | +| `labelCategoryId` | string | 否 | 侧边栏只展示某一 **标签分类** 时传入;不传则返回当前门店下出现的全部标签分类节点 | + +#### 数据范围与表关联(便于联调对照) + +- **门店内商品**:`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` 等业务字段)。 + +#### 出参(`List`) + +若宿主对成功结果有统一包装,业务数组一般在 **`data`** 中;下列为 **解包后的数组项** 结构。 + +**L1 `UsAppLabelCategoryTreeNodeDto`(标签分类)** + +| 字段 | 类型 | 说明 | +|------|------|------| +| `id` | string | `fl_label_category.Id` | +| `categoryName` | string | 分类名称 | +| `categoryPhotoUrl` | string \| null | 分类图标/图 | +| `orderNum` | number | 排序 | +| `productCategories` | array | 第二级列表(见下表) | + +**L2 `UsAppProductCategoryNodeDto`(产品分类)** + +| 字段 | 类型 | 说明 | +|------|------|------| +| `categoryId` | string \| null | 产品分类Id;产品未归类或分类不存在时为空 | +| `categoryPhotoUrl` | string \| null | 产品分类图片地址;产品未归类或分类不存在时为空 | +| `name` | string | 产品分类显示名;空源数据为 **`无`** | +| `itemCount` | number | 该分类下 **产品个数**(去重后的产品数) | +| `products` | array | 第三级产品列表(见下表) | + +**L3 `UsAppLabelingProductNodeDto`(产品)** + +| 字段 | 类型 | 说明 | +|------|------|------| +| `productId` | string | `fl_product.Id` | +| `productName` | string | 产品名称 | +| `productCode` | string | 产品编码 | +| `productImageUrl` | string \| null | 主图 | +| `subtitle` | string | 卡片副标题:**有 `productCode` 则显示编码,否则「无」**(与原型「Basic」等独立文案不同,需另行扩展字段时再对齐) | +| `labelTypeCount` | number | 第四级条数,可用于角标「N Types」 | +| `labelTypes` | array | 第四级(见下表) | + +**L4 `UsAppLabelTypeNodeDto`(标签种类 / 可选项)** + +| 字段 | 类型 | 说明 | +|------|------|------| +| `labelTypeId` | string | `fl_label_type.Id` | +| `typeName` | string | 类型名称(如 Defrost) | +| `orderNum` | number | 排序 | +| `labelCode` | string | 业务标签编码,后续预览、打印流程使用 | +| `templateCode` | string \| null | 关联模板编码 | +| `labelSizeText` | string \| null | 尺寸文案;`inch` 常用格式如 `2"x2"` | + +#### 错误与边界 + +- `locationId` 为空:返回友好错误 **「门店Id不能为空」**。 +- 门店下无关联产品:返回 **空数组** `[]`。 +- 有产品但无任何符合条件的标签关联:返回 **空数组** `[]`。 + +#### 请求示例 + +```http +GET /api/app/us-app-labeling/labeling-tree?locationId=11111111-1111-1111-1111-111111111111&labelCategoryId=a2696b9e-2277-11f1-b4c6-00163e0c7c4f&keyword=Chicken HTTP/1.1 +Host: localhost:19001 +Authorization: Bearer eyJhbGciOi... +``` + +**curl**(Token 取自登录响应的 `data.token` 整段,已含 `Bearer ` 前缀时直接放入 Header): + +```bash +curl -X GET "http://localhost:19001/api/app/us-app-labeling/labeling-tree?locationId=11111111-1111-1111-1111-111111111111" \ + -H "Authorization: " +``` + +#### 响应结构示例(解包后) + +```json +[ + { + "id": "cat-prep-id", + "categoryName": "Prep", + "categoryPhotoUrl": "/picture/...", + "orderNum": 1, + "productCategories": [ + { + "categoryId": "pc-meat-id", + "categoryPhotoUrl": "/picture/product-category/20260325123010_xxx.png", + "name": "Meat", + "itemCount": 1, + "products": [ + { + "productId": "prod-chicken-id", + "productName": "Chicken", + "productCode": "CHK-001", + "productImageUrl": "/picture/...", + "subtitle": "CHK-001", + "labelTypeCount": 3, + "labelTypes": [ + { + "labelTypeId": "lt-defrost", + "typeName": "Defrost", + "orderNum": 1, + "labelCode": "LBL_CHICKEN_DEFROST", + "templateCode": "TPL_2X2", + "labelSizeText": "2\"x2\"" + } + ] + } + ] + } + ] + } +] +``` + +> 前端 Axios 若项目约定 **GET 使用 `data` 配置对象** 传参,请仍绑定到与上述 Query 同名的字段(`locationId`、`keyword`、`labelCategoryId`),与 URL Query 等价即可。 + +### 8.2 App 打印预览(elements 渲染结构) + +**场景**:用户选择某个 Product + Label Type 进入「Label Preview」页面,需要把模板预览区域渲染出来。 +后端根据 `labelCode` 读取模板(`fl_label_template` + `fl_label_template_element`),并将 AUTO_DB / PRINT_INPUT 的值渲染回每个 element 的 `config`,前端按 `elements` 自行绘制预览。 + +#### HTTP + +- **方法**:`POST` +- **路径**:`/api/app/us-app-labeling/preview`(若与 Swagger 不一致,**以 Swagger 为准**) +- **鉴权**:需要登录(`Authorization: Bearer ...`) + +#### 入参(Body:`UsAppLabelPreviewInputVo`) + +| 参数名(JSON) | 类型 | 必填 | 说明 | +|---|---|---|---| +| `locationId` | string | 是 | 门店Id(校验 `fl_label.LocationId` 必须一致) | +| `labelCode` | string | 是 | 标签编码(`fl_label.LabelCode`) | +| `productId` | string | 否 | 预览用产品Id;不传则默认取该标签绑定的第一个产品(用于 AUTO_DB 数据填充) | +| `baseTime` | string | 否 | 业务基准时间(用于 DATE/TIME 元素计算;不传则用服务器当前时间) | +| `printInputJson` | object | 否 | 打印输入(用于 PRINT_INPUT 元素),key 建议与模板元素 `inputKey` 对齐 | + +#### 出参(`UsAppLabelPreviewDto`) + +除顶部信息外,核心是 `template`(供前端画布渲染): + +- `template`:`LabelTemplatePreviewDto` + - `width` / `height` / `unit`:模板物理尺寸 + - `elements[]`:元素数组(对齐前端 editor JSON:`id/elementName/type/x/y/width/height/rotation/border/zIndex/orderNum/config`) +- `templateProductDefaultValues`:`object | null` + - 来源:`fl_label_template_product_default.DefaultValuesJson` + - 命中条件:当前预览上下文的 `templateId + productId + labelTypeId` + - 建议结构:`{ "elementId": "默认值" }` + - 未命中时返回 `null`(向后兼容) + +`elements[].config` 内常用字段(示例): + +- 文本类(如 `TEXT_PRODUCT` / `TEXT_STATIC` / `TEXT_PRICE`):`config.text` +- 条码/二维码(`BARCODE` / `QRCODE`):`config.data` +- 日期/时间(`DATE` / `TIME`):`config.format`(后端已计算并写回) + +#### 数据来源说明 + +- 模板头:`fl_label_template` +- 模板元素:`fl_label_template_element`(按 `OrderNum` + `ZIndex` 排序) +- 标签归属:`fl_label`(校验 `labelCode` 存在且 `LocationId == locationId`) + +#### 错误与边界 + +- `locationId` 为空:友好错误 **「门店Id不能为空」**。 +- `labelCode` 为空:友好错误 **「labelCode不能为空」**。 +- 标签不存在:友好错误 **「标签不存在」**。 +- 模板不存在:友好错误 **「模板不存在」**。 +- 标签不属于当前门店:友好错误 **「该标签不属于当前门店」**。 +- 标签未绑定产品且未传 `productId`:友好错误 **「该标签未绑定产品,无法预览」**。 + +#### 请求示例 + +```json +{ + "locationId": "11111111-1111-1111-1111-111111111111", + "labelCode": "LBL_CHICKEN_DEFROST", + "productId": "22222222-2222-2222-2222-222222222222", + "baseTime": "2026-03-26T10:30:00", + "printInputJson": { + "price": "12.99" + } +} +``` + +**curl:** + +```bash +curl -X POST "http://localhost:19001/api/app/us-app-labeling/preview" \ + -H "Authorization: " \ + -H "Content-Type: application/json" \ + -d '{"locationId":"11111111-1111-1111-1111-111111111111","labelCode":"LBL_CHICKEN_DEFROST","productId":"22222222-2222-2222-2222-222222222222","baseTime":"2026-03-26T10:30:00","printInputJson":{"price":"12.99"}}' +``` + +--- + +## 接口 9:App 打印(落库打印任务与明细) + +**场景**:移动端预览确认后点击 **Print**。后端负责把“本次打印”写入数据库,方便追溯/统计/重打。 + +### 9.1 创建打印任务并写入明细 + +#### HTTP + +- **方法**:`POST` +- **路径**:`/api/app/us-app-labeling/print`(若与 Swagger 不一致,**以 Swagger 为准**) +- **鉴权**:需要登录(`Authorization: Bearer ...`) + +#### 入参(Body:`UsAppLabelPrintInputVo`) + +| 参数名(JSON) | 类型 | 必填 | 说明 | +|---|---|---|---| +| `locationId` | string | 是 | 门店Id(校验 `fl_label.LocationId` 必须一致) | +| `labelCode` | string | 是 | 标签编码(`fl_label.LabelCode`) | +| `productId` | string | 否 | 打印用产品Id;不传则默认取该标签绑定的第一个产品(用于模板解析) | +| `printQuantity` | number | 否 | 打印份数;`<=0` 按 1 处理 | +| `clientRequestId` | string | 否 | 客户端幂等请求Id;同一个值重复调用会直接返回首次创建的 `batchId/taskIds`,避免重复写库 用法:前端/客户端每次点击 Print 生成一个稳定的 clientRequestId(比如 uuid) | +| `baseTime` | string | 否 | 业务基准时间(用于 DATE/TIME 元素计算) | +| `printInputJson` | object | 否 | 打印输入(用于模板 PRINT_INPUT 元素),key 建议与模板元素 `inputKey` 对齐 | +| `printerId` | string | 否 | 打印机Id(可选,用于追踪) | +| `printerMac` | string | 否 | 打印机蓝牙 MAC(可选) | +| `printerAddress` | string | 否 | 打印机地址(可选) | + +#### 数据落库说明 + +- **任务表**:`fl_label_print_task` + - **一份打印 = 一条任务**:当 `printQuantity = N` 时,后端会插入 **N 条任务记录**(同一次点击 Print 共享一个 `BatchId`,并记录 `CopyIndex=1..N`)。 + - 任务表会保存:本次打印的输入、命中的模板默认值、以及整份 resolved 后的模板快照 JSON,便于追溯/重打。 +- **明细表**:`fl_label_print_data` + - **按组件写快照**:每个任务会按模板 `elements[]` 逐个插入明细记录(`ElementId/ElementName/RenderValue/RenderConfigJson`)。 + - 适用于按组件维度审计/统计/追溯。 + +> 模板解析的数据源来自 `fl_label_template` + `fl_label_template_element`,与预览接口一致。 + +#### 出参(`UsAppLabelPrintOutputDto`) + +| 字段 | 类型 | 说明 | +|---|---|---| +| `taskId` | string | 第 1 份打印任务Id(兼容旧逻辑) | +| `printQuantity` | number | 实际写入的份数 | +| `batchId` | string | 本次点击 Print 的批次Id | +| `taskIds` | string[] | 本次生成的所有任务Id(长度=printQuantity) | + + +#### 错误与边界 + +- `locationId` 为空:友好错误 **「门店Id不能为空」**。 +- `labelCode` 为空:友好错误 **「labelCode不能为空」**。 +- 标签不存在/不可用:友好错误 **「标签不存在或不可用」**。 +- 标签不属于当前门店:友好错误 **「该标签不属于当前门店」**。 +- 标签未绑定产品且未传 `productId`:友好错误 **「该标签未绑定产品,无法预览」**(模板解析阶段抛出)。 + +#### 请求示例 + +```json +{ + "locationId": "11111111-1111-1111-1111-111111111111", + "labelCode": "LBL_CHICKEN_DEFROST", + "productId": "22222222-2222-2222-2222-222222222222", + "printQuantity": 2, + "baseTime": "2026-03-26T10:30:00", + "printInputJson": { + "price": "12.99" + }, + "printerMac": "AA:BB:CC:DD:EE:FF" +} +``` + +**curl:** + +```bash +curl -X POST "http://localhost:19001/api/app/us-app-labeling/print" \ + -H "Authorization: " \ + -H "Content-Type: application/json" \ + -d '{"locationId":"11111111-1111-1111-1111-111111111111","labelCode":"LBL_CHICKEN_DEFROST","productId":"22222222-2222-2222-2222-222222222222","printQuantity":2,"baseTime":"2026-03-26T10:30:00","printInputJson":{"price":"12.99"}}' +``` + +--- + +## 接口 10:App 打印日志(当前登录账号 + 当前门店) + +**场景**:移动端“打印记录/历史”页面。只展示**当前登录账号**在**当前门店**打印的记录,便于追溯/重打。 + +### 10.1 分页获取打印日志 + +#### HTTP + +- **方法**:`POST`(与本模块其它复杂入参接口一致;若与 Swagger 不一致,**以 Swagger 为准**) +- **路径**:`/api/app/us-app-labeling/get-print-log-list` +- **鉴权**:需要登录(`Authorization: Bearer ...`) + +#### 入参(Body:`PrintLogGetListInputVo`) + +> 本项目分页约定:`skipCount` 表示 **页码(从 1 开始)**,不是 0 基 offset。 + +| 参数名(JSON) | 类型 | 必填 | 说明 | +|---|---|---|---| +| `locationId` | string | 是 | 当前门店Id(仅返回该门店记录) | +| `skipCount` | number | 否 | 页码,从 1 开始;默认 1 | +| `maxResultCount` | number | 否 | 每页条数;默认按后端/ABP 默认 | + +#### 过滤条件(后端固定逻辑) + +- `fl_label_print_task.CreatedBy == CurrentUser.Id` +- `fl_label_print_task.LocationId == locationId` +- 按时间倒序:`PrintedAt ?? CreationTime`(越新的越靠前) + +#### 出参(`PagedResultWithPageDto`) + +| 字段 | 类型 | 说明 | +|---|---|---| +| `pageIndex` | number | 当前页码(从 1 开始) | +| `pageSize` | number | 每页条数 | +| `totalCount` | number | 总条数 | +| `totalPages` | number | 总页数 | +| `items` | PrintLogItemDto[] | 列表 | + +`PrintLogItemDto`: + +| 字段 | 类型 | 说明 | +|---|---|---| +| `taskId` | string | 任务Id(fl_label_print_task.Id) | +| `batchId` | string | 批次Id(同一次点击 Print 共享) | +| `copyIndex` | number | 第几份(从 1 开始) | +| `labelId` | string | 标签Id | +| `labelCode` | string | 标签编码(来自 fl_label.LabelCode) | +| `productId` | string | 产品Id | +| `productName` | string | 产品名(来自 fl_product.ProductName;无则 “无”) | +| `typeName` | string | 标签类型名称(来自 fl_label_type.TypeName) | +| `labelSizeText` | string | 模板尺寸(宽高+单位,如 `2.00x2.00inch` / `6.00x4.00cm`) | +| `printDataList` | PrintLogDataItemDto[] | 本次打印内容快照(来自 fl_label_print_data,按 taskId 关联) | +| `printedAt` | string | 打印时间(PrintedAt ?? CreationTime) | +| `operatorName` | string | 操作人姓名(当前登录账号 Name) | +| `locationName` | string | 门店名称 | + +`PrintLogDataItemDto`: + +| 字段 | 类型 | 说明 | +|---|---|---| +| `elementId` | string | 模板组件Id(fl_label_print_data.ElementId) | +| `renderValue` | string | 最终渲染值(fl_label_print_data.RenderValue) | +| `renderConfigJson` | object | 最终渲染配置(fl_label_print_data.RenderConfigJson 反序列化) | + +#### curl + +```bash +curl -X POST "http://localhost:19001/api/app/us-app-labeling/get-print-log-list" \ + -H "Authorization: " \ + -H "Content-Type: application/json" \ + -d '{"locationId":"11111111-1111-1111-1111-111111111111","skipCount":1,"maxResultCount":20}' +``` + +--- + +## 接口 11:App 重新打印(根据任务Id重打) + +**场景**:移动端“打印记录/历史”页面点击 **Reprint**。后端根据历史任务 `taskId` 创建一批新的打印任务与明细。 + +### 11.1 重打 + +#### HTTP + +- **方法**:`POST` +- **路径**:`/api/app/us-app-labeling/reprint`(若与 Swagger 不一致,**以 Swagger 为准**) +- **鉴权**:需要登录(`Authorization: Bearer ...`) + +#### 入参(Body:`UsAppLabelReprintInputVo`) + +| 参数名(JSON) | 类型 | 必填 | 说明 | +|---|---|---|---| +| `locationId` | string | 是 | 当前门店Id(后端校验历史任务必须属于该门店) | +| `taskId` | string | 是 | 历史打印任务Id(`fl_label_print_task.Id`) | +| `printQuantity` | number | 否 | 重新打印份数;`<=0` 按 1 处理;默认 1 | +| `clientRequestId` | string | 否 | 客户端幂等请求Id;同一个值重复调用会直接返回首次创建的 `batchId/taskIds`,避免重复写库 | +| `printerId` | string | 否 | 可选,覆盖历史任务的打印机Id | +| `printerMac` | string | 否 | 可选,覆盖历史任务的打印机MAC | +| `printerAddress` | string | 否 | 可选,覆盖历史任务的打印机地址 | + +#### 权限校验(后端固定逻辑) + +- 历史任务必须满足:`CreatedBy == CurrentUser.Id` +- 且 `LocationId == locationId` + +#### 出参(`UsAppLabelPrintOutputDto`) + +字段与接口 9 一致:`taskId / printQuantity / batchId / taskIds` + +#### curl + +```bash +curl -X POST "http://localhost:19001/api/app/us-app-labeling/reprint" \ + -H "Authorization: " \ + -H "Content-Type: application/json" \ + -d '{"locationId":"11111111-1111-1111-1111-111111111111","taskId":"3a205389-78dd-4750-51ab-720344c9f607","printQuantity":1}' +``` + diff --git a/美国版/Food Labeling Management App UniApp/src/pages/labels/preview.vue b/美国版/Food Labeling Management App UniApp/src/pages/labels/preview.vue index 88411ff..c2fbc93 100644 --- a/美国版/Food Labeling Management App UniApp/src/pages/labels/preview.vue +++ b/美国版/Food Labeling Management App UniApp/src/pages/labels/preview.vue @@ -215,6 +215,7 @@ import { postUsAppLabelPrint, US_APP_LABEL_PRINT_PATH, } from '../../services/usAppLabeling' +import { savePrintTemplateSnapshotForTask } from '../../utils/printSnapshotStorage' import { applyTemplateProductDefaultValuesToTemplate, extractTemplateProductDefaultValuesFromPreviewPayload, @@ -547,6 +548,15 @@ const goBluetoothPage = () => { uni.navigateTo({ url: '/pages/labels/bluetooth' }) } +/** 接口 9 可选 clientRequestId(文档 10 幂等) */ +function createPrintClientRequestId (): string { + const c = typeof globalThis !== 'undefined' ? (globalThis as { crypto?: Crypto }).crypto : undefined + if (c && typeof c.randomUUID === 'function') { + return c.randomUUID() + } + return `print-${Date.now()}-${Math.random().toString(36).slice(2, 12)}` +} + const handlePrint = async () => { if (isPrinting.value || previewLoading.value || !systemTemplate.value) return @@ -660,17 +670,39 @@ const handlePrint = async () => { let printLogRequestBody: ReturnType = null try { const bt = getBluetoothConnection() + /** + * 接口 9 落库必须与本次出纸使用的合并模板完全一致(与 labelPrintJobPayload.template 同源), + * 避免另起 buildPrintPersistTemplateSnapshot(base) 与 computeMergedPreviewTemplate() 细微偏差, + * 导致库内缺用户输入的价签/过敏原/数字/日期,重打与预览不一致。 + */ + const persistTemplateDoc = JSON.parse( + JSON.stringify(labelPrintJobPayload.template) + ) as Record + printLogRequestBody = buildUsAppLabelPrintRequestBody({ locationId: getCurrentStoreId(), labelCode: labelCode.value, productId: productId.value || undefined, printQuantity: printQty.value, - printInputJson, - templateSnapshot: labelPrintJobPayload.template, + mergedTemplate: persistTemplateDoc, + clientRequestId: createPrintClientRequestId(), printerMac: bt?.deviceId || undefined, }) if (printLogRequestBody) { - await postUsAppLabelPrint(printLogRequestBody) + const printRes = await postUsAppLabelPrint(printLogRequestBody) + /** 本机快照:列表接口 renderTemplateJson 常为设计器占位,重打需与当次出纸合并模板一致 */ + try { + const tid = String( + (printRes as { taskId?: string })?.taskId + ?? (printRes as { TaskId?: string })?.TaskId + ?? '', + ).trim() + if (tid) { + savePrintTemplateSnapshotForTask(tid, JSON.stringify(persistTemplateDoc)) + } + } catch { + /* 忽略快照写入失败 */ + } } } catch (syncErr: unknown) { if (!isUsAppSessionExpiredError(syncErr)) { diff --git a/美国版/Food Labeling Management App UniApp/src/pages/more/print-log.vue b/美国版/Food Labeling Management App UniApp/src/pages/more/print-log.vue index 890da41..d319ffa 100644 --- a/美国版/Food Labeling Management App UniApp/src/pages/more/print-log.vue +++ b/美国版/Food Labeling Management App UniApp/src/pages/more/print-log.vue @@ -34,21 +34,32 @@ - + + + Loading… + + + No print records + + - + - {{ row.productName }} - {{ row.labelId }} + {{ row.productName || '无' }} + {{ shortRef(row) }} - {{ row.category }} - {{ row.template }} + {{ tagLabelSize(row) }} + {{ tagTypeName(row) }} @@ -57,15 +68,11 @@ - {{ row.printedBy }} + {{ row.operatorName || '无' }} - {{ row.location }} - - - - Expires {{ row.expiryDate }} + {{ row.locationName || '无' }} @@ -75,6 +82,12 @@ + + Loading more… + + + End of list + @@ -82,20 +95,22 @@ Product - Label ID + Ref + Label size + Type Printed At - Expires Action - {{ row.productName }} - {{ row.labelId }} + {{ row.productName || '无' }} + {{ shortRef(row) }} + {{ tagLabelSize(row) }} + {{ tagTypeName(row) }} {{ row.printedAt }} - {{ row.expiryDate }} @@ -103,6 +118,9 @@ + + Loading more… + @@ -112,20 +130,169 @@ + diff --git a/美国版/Food Labeling Management Platform/src/components/labels/LabelTemplateDataEntryView.tsx b/美国版/Food Labeling Management Platform/src/components/labels/LabelTemplateDataEntryView.tsx index 5e0bdd1..562bfd8 100644 --- a/美国版/Food Labeling Management Platform/src/components/labels/LabelTemplateDataEntryView.tsx +++ b/美国版/Food Labeling Management Platform/src/components/labels/LabelTemplateDataEntryView.tsx @@ -17,7 +17,7 @@ import { getLabelTemplate, updateLabelTemplate } from '../../services/labelTempl import { getProducts } from '../../services/productService'; import { getLabelTypes } from '../../services/labelTypeService'; import { skipCountForPage } from '../../lib/paginationQuery'; -import type { ElementType, LabelElement, LabelTemplateDto, LabelType, Unit } from '../../types/labelTemplate'; +import type { LabelElement, LabelTemplateDto, LabelType, Unit } from '../../types/labelTemplate'; import { appliedLocationToEditor, dataEntryColumnLabel, @@ -44,23 +44,33 @@ function newRowId(): string { } } +/** 模板录入表:图片与二维码(及名称含 qrcode 的控件)用上传组件,预览区固定 100×100 */ +const DATA_ENTRY_IMAGE_BOX = + 'h-[100px] w-[100px] min-h-[100px] min-w-[100px] max-h-[100px] max-w-[100px] shrink-0 aspect-auto'; + +function dataEntryUsesImageUpload(element: LabelElement): boolean { + if (element.type === 'IMAGE' || element.type === 'QRCODE') return true; + const n = (element.elementName ?? '').trim().toLowerCase(); + return n.includes('qrcode'); +} + function DataEntryValueCell({ - elementType, + element, value, onValueChange, }: { - elementType: ElementType; + element: LabelElement; value: string; onValueChange: (next: string) => void; }) { - if (elementType === 'IMAGE') { + if (dataEntryUsesImageUpload(element)) { return ( ); @@ -381,7 +391,7 @@ export function LabelTemplateDataEntryView({ {printFields.map((f) => ( setFieldValue(row.id, f.id, v)} /> diff --git a/美国版/Food Labeling Management Platform/src/components/products/ProductsView.tsx b/美国版/Food Labeling Management Platform/src/components/products/ProductsView.tsx index 17dfac1..a2ad453 100755 --- a/美国版/Food Labeling Management Platform/src/components/products/ProductsView.tsx +++ b/美国版/Food Labeling Management Platform/src/components/products/ProductsView.tsx @@ -238,7 +238,7 @@ export function ProductsView() { list = list.filter((p) => allowed.has(p.id)); } if (categoryFilter !== "all") { - list = list.filter((p) => (p.categoryName ?? "").trim() === categoryFilter); + list = list.filter((p) => (p.categoryId ?? "").trim() === categoryFilter); } const t = list.length; setTotal(t); @@ -350,11 +350,11 @@ export function ProductsView() { [locations], ); - const categoryNameOptions = useMemo( + const categorySelectOptions = useMemo( () => productCategoriesCatalog .map((c) => ({ - value: (c.categoryName ?? c.categoryCode ?? c.id ?? "").trim(), + value: (c.id ?? "").trim(), label: toDisplay(c.categoryName ?? c.categoryCode ?? c.id), })) .filter((o) => o.value), @@ -434,7 +434,7 @@ export function ProductsView() { All Categories - {categoryNameOptions.map((o) => ( + {categorySelectOptions.map((o) => ( {o.label} @@ -861,7 +861,7 @@ export function ProductsView() { }} editing={editingProduct} locationOptions={locationOptions} - categoryOptions={categoryNameOptions} + categoryOptions={categorySelectOptions} locationMap={locationMap} onSaved={() => { refresh(); @@ -925,7 +925,7 @@ function ProductFormDialog({ const [submitting, setSubmitting] = useState(false); const [productCode, setProductCode] = useState(""); const [productName, setProductName] = useState(""); - const [categoryName, setCategoryName] = useState(""); + const [categoryId, setCategoryId] = useState(""); const [productImageUrl, setProductImageUrl] = useState(""); const [state, setState] = useState(true); const [locationId, setLocationId] = useState(""); @@ -935,7 +935,7 @@ function ProductFormDialog({ if (editing) { setProductCode(editing.productCode ?? ""); setProductName(editing.productName ?? ""); - setCategoryName((editing.categoryName ?? "").trim()); + setCategoryId((editing.categoryId ?? "").trim()); setProductImageUrl(editing.productImageUrl ?? ""); setState(editing.state !== false); const lids = locationMap.get(editing.id) ?? []; @@ -943,7 +943,7 @@ function ProductFormDialog({ } else { setProductCode(""); setProductName(""); - setCategoryName(""); + setCategoryId(""); setProductImageUrl(""); setState(true); setLocationId(""); @@ -963,7 +963,7 @@ function ProductFormDialog({ const body: ProductCreateInput = { productCode: productCode.trim(), productName: productName.trim(), - categoryName: categoryName.trim() || null, + categoryId: categoryId.trim() || null, productImageUrl: productImageUrl.trim() || null, state, }; @@ -1021,10 +1021,10 @@ function ProductFormDialog({
- +