From 923d50c0ca7766029554147e4bd5a93d476c5a26 Mon Sep 17 00:00:00 2001 From: jokerxue <2509699647@qq.com> Date: Tue, 19 May 2026 15:43:09 +0800 Subject: [PATCH] 更新bug --- 5-18接口优化.md | 1293 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 泰额版/Food Labeling Management Platform/src/components/ui/select.tsx | 2 +- 美国版/Food Labeling Management App UniApp/src/App.vue | 16 +++++++++++++++- 美国版/Food Labeling Management App UniApp/src/locales/en.ts | 2 ++ 美国版/Food Labeling Management App UniApp/src/locales/zh.ts | 2 ++ 美国版/Food Labeling Management App UniApp/src/pages/labels/labels.vue | 212 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-------------------------------------------------------------- 美国版/Food Labeling Management App UniApp/src/pages/labels/preview.vue | 11 ++++++++++- 美国版/Food Labeling Management App UniApp/src/pages/login/login.vue | 38 ++++++++++++++++++++++++++++++++++++++ 美国版/Food Labeling Management App UniApp/src/services/locationSupport.ts | 27 ++++++++++++++++++--------- 美国版/Food Labeling Management App UniApp/src/services/usAppLabeling.ts | 57 +++++++++++++++++++++++++++++++++++++++++++++++++++------ 美国版/Food Labeling Management App UniApp/src/types/usAppLabeling.ts | 10 ++++++++++ 美国版/Food Labeling Management App UniApp/src/utils/barcodeFormat.ts | 79 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 美国版/Food Labeling Management App UniApp/src/utils/labelPreview/normalizePreviewTemplate.ts | 73 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 美国版/Food Labeling Management App UniApp/src/utils/offlineSyncManager.ts | 35 ++++++++++++++++++++++++++++++++--- 美国版/Food Labeling Management App UniApp/src/utils/print/protocols/escPosBuilder.ts | 15 ++------------- 美国版/Food Labeling Management App UniApp/src/utils/print/systemTemplateAdapter.ts | 31 ++++++++++++++++++++++++++++--- 美国版/Food Labeling Management App UniApp/src/utils/print/tscLabelBuilder.ts | 15 ++------------- 美国版/Food Labeling Management App UniApp/src/utils/sqliteSync.ts | 177 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-- 美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/Product/ProductCreateInputVo.cs | 8 ++++++++ 美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/Product/ProductGetListOutputDto.cs | 8 ++++++++ 美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/Product/ProductGetOutputDto.cs | 8 ++++++++ 美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/UsAppLabeling/UsAppLabelingProductNodeDto.cs | 7 +++++++ 美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application/Services/DbModels/FlProductDbEntity.cs | 12 ++++++++++++ 美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application/Services/ProductAppService.cs | 20 ++++++++++++++++++++ 美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application/Services/UsAppLabelingAppService.cs | 15 +++++++++++++++ 美国版/Food Labeling Management Platform/src/components/dashboard/Dashboard.tsx | 82 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++- 美国版/Food Labeling Management Platform/src/components/labels/LabelCategoriesView.tsx | 33 +++++++++++++++++++++++---------- 美国版/Food Labeling Management Platform/src/components/labels/LabelTemplateDataEntryView.tsx | 26 +++++++++++++++++++++++++- 美国版/Food Labeling Management Platform/src/components/labels/LabelTemplateEditor/LabelCanvas.tsx | 45 +++++++++++++++++++++++++++++++++++---------- 美国版/Food Labeling Management Platform/src/components/labels/LabelTemplateEditor/PropertiesPanel.tsx | 66 +++++++++++++++++++++++++++++++++++++++++++++++++++++------------- 美国版/Food Labeling Management Platform/src/components/labels/LabelTypesView.tsx | 185 ++++++++++++++++++++++++++++++++++++++++++++++++++++++----------------------------------------------------------------------------------------------------------------------------------- 美国版/Food Labeling Management Platform/src/components/labels/LabelsList.tsx | 57 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++- 美国版/Food Labeling Management Platform/src/components/people/PeopleView.tsx | 406 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++---------------------------------------------------------------------------------------- 美国版/Food Labeling Management Platform/src/components/products/ProductsView.tsx | 320 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-------------------- 美国版/Food Labeling Management Platform/src/components/ui/image-url-upload.tsx | 33 ++++++++++++++++++++++++++++----- 美国版/Food Labeling Management Platform/src/components/ui/searchable-multi-select.tsx | 5 +++-- 美国版/Food Labeling Management Platform/src/components/ui/searchable-select.tsx | 5 +++-- 美国版/Food Labeling Management Platform/src/components/ui/select.tsx | 2 +- 美国版/Food Labeling Management Platform/src/lib/barcodeFormat.ts | 105 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 美国版/Food Labeling Management Platform/src/lib/formDialogLayout.ts | 35 +++++++++++++++++++++++++++++++++++ 美国版/Food Labeling Management Platform/src/lib/productCodeValueTemplate.ts | 39 +++++++++++++++++++++++++++++++++++++++ 美国版/Food Labeling Management Platform/src/main.tsx | 1 + 美国版/Food Labeling Management Platform/src/services/productService.ts | 68 ++++++++++++++++++++++++++++++++++++-------------------------------- 美国版/Food Labeling Management Platform/src/services/reportsService.ts | 54 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 美国版/Food Labeling Management Platform/src/services/teamMemberService.ts | 32 +++++++++++++++++++++++++++++++- 美国版/Food Labeling Management Platform/src/styles/form-dialog.css | 30 ++++++++++++++++++++++++++++++ 美国版/Food Labeling Management Platform/src/types/labelTemplate.ts | 12 +++++++++++- 美国版/Food Labeling Management Platform/src/types/labelType.ts | 4 ++-- 美国版/Food Labeling Management Platform/src/types/product.ts | 15 +++++++++++++++ 美国版/Food Labeling Management Platform/src/types/reports.ts | 25 +++++++++++++++++++++++++ 美国版/Food Labeling Management Platform/src/types/teamMember.ts | 9 ++++++++- 51 files changed, 3431 insertions(+), 436 deletions(-) create mode 100644 5-18接口优化.md create mode 100644 美国版/Food Labeling Management App UniApp/src/utils/barcodeFormat.ts create mode 100644 美国版/Food Labeling Management Platform/src/lib/barcodeFormat.ts create mode 100644 美国版/Food Labeling Management Platform/src/lib/formDialogLayout.ts create mode 100644 美国版/Food Labeling Management Platform/src/lib/productCodeValueTemplate.ts create mode 100644 美国版/Food Labeling Management Platform/src/styles/form-dialog.css diff --git a/5-18接口优化.md b/5-18接口优化.md new file mode 100644 index 0000000..200afa6 --- /dev/null +++ b/5-18接口优化.md @@ -0,0 +1,1293 @@ +# 5-18 接口优化 + +本文档说明 **2026-05-18** 对美国版接口修复与约定,包括: + +1. **`/api/app/rbac-role`**:`accessPermissions` 读写(见下文 rbac-role 章节)。 +2. **`/api/app/team-member`**:成员 **平台端无法登录**(已修复);列表支持 **Company / Region / Location** 筛选(见 [team-member-list](#team-member-列表-companyregionlocation-筛选))。 +3. **`/api/app/label-type`**:列表 **No. of Labels / Region / Location**;新增/编辑 **Region·Location 多选**(见 [label-type](#label-type-标签类型))。 +4. **`/api/app/label-multiple-option`**:列表 **Region / Location** 筛选与出参;新增/编辑 **Region·Location 多选**(见 [label-multiple-option](#label-multiple-option-多选项))。 +5. **`/api/app/reports/print-log-list`**:Print Log **Label ID** 当日门店序号 + **Expiry Date** 从打印快照解析(见 [reports-print-log](#reports-print-log-打印日志))。 +6. **`/api/app/reports/label-report`**:按 Token **门店绑定**统计(管理员全量;见 [reports-label-report](#reports-label-report-标签报表))。 +7. **`/api/app/us-app-auth/location-detail/{locationId}`**:出参增加 **经营时间** `operatingHours`(见 [us-app-auth-location-detail](#us-app-auth-location-detail-门店详情))。 +8. **App `POST /api/app/us-app-labeling/get-print-log-list`**、**`get-label-report`**:管理员 / Partner 角色可查看**当前门店全部**打印记录与统计(见 [us-app-print-log](#us-app-print-log-app-打印日志与报表))。 +9. **`GET /api/app/us-app-labeling/labeling-tree`**:修复产品分类 `SPECIFIED` 未配门店关联导致全店空树(见 [us-app-labeling-tree](#us-app-labeling-tree-四级列表))。 +10. **`GET /api/app/location`**:门店列表按 Token **Region** 数据范围过滤(见 [location-list](#location-门店列表))。 +11. **`GET /api/app/reports/template-print-stat-list`**:按模板统计打印标签数量(见 [reports-template-print-stat](#reports-template-print-stat-模板打印统计))。 + +--- + +## reports-print-log 打印日志 + +**应用服务**:`ReportsAppService` +**接口**:`GET /api/app/reports/print-log-list`(及同源导出 `export-print-log-pdf` / `export-print-log-excel`) +**辅助类**:`ReportsPrintLogDailyLabelIdHelper`、`ReportsPrintLogExpiryHelper` + +### Label ID 变更说明 + +| 项 | 说明 | +|----|------| +| **Label ID 含义** | 由 **`fl_label.LabelCode`**(如 `079`)改为 **门店当日打印序号** | +| **展示格式** | `{yyyyMMdd}-{n}`,例如 `20260513-1`、`20260513-2` | +| **排序规则** | 按 **`LocationId`(门店)** + **自然日**(`PrintedAt`,无则 `CreationTime`)分组;组内按打印时间 **升序**,同秒按任务 `Id` 升序,`n` 从 **1** 递增 | +| **跨页一致** | 分页列表与导出使用同一套序号(按门店全日任务计算,非仅当前页内排序) | +| **API 字段名** | 仍为 **`labelCode`**(兼容前端列绑定),内容为当日序号,**不是**标签主数据编码 | + +### 列表出参(节选) + +| 字段 | 说明 | +|------|------| +| taskId | 打印任务 Id(`fl_label_print_task.Id`,重打用) | +| **labelCode** | **Label ID 列**:`20260515-1` 这种当日门店序号 | +| productName / categoryName / templateText | 不变 | +| printedAt | 打印时间 | +| locationText / locationId | 门店 | +| **expiryDateText** | **Expiry Date 列**:从 `PrintInputJson` 解析的保质期展示文案;无则「无」 | + +**示例** + +```json +{ + "taskId": "task-guid-001", + "labelCode": "20260515-3", + "productName": "Tuna & Bacon Sub", + "printedAt": "2026-05-15T15:00:40", + "locationText": "UNCC store (LOC001)", + "expiryDateText": "05/17" +} +``` + +### Expiry Date(到期时间)变更说明 + +| 项 | 说明 | +|----|------| +| **数据来源** | `fl_label_print_task.PrintInputJson`(App 打印接口落库,多为**整份模板快照** JSON,含 `elements[]`) | +| **解析方式** | `ReportsPrintLogExpiryHelper.ExtractExpiryText`:先读根级 `expiryDate` / `expiry` / `expirationDate` 等;若无,在 `elements` 中匹配 **duration date / expiry** 类元素,取 `config.text`(或 `config.format`) | +| **匹配规则** | 与 Web `isDateTimeDataEntryField` 对齐:`elementName` / `inputKey` 含 `durationdate`、`expirydate` 等;`typeAdd` 含 `duration date`;排除 `currentdate`、`currenttime`、`prepped` 等制备日期字段 | +| **展示格式** | **与标签出纸一致**(如 `05/17`、`2026-05-17`),不做二次换算 | +| **适用范围** | 分页列表、`export-print-log-pdf`、`export-print-log-excel` 共用同一解析逻辑 | + +**库内样例(节选)** + +`PrintInputJson.elements` 中 `elementName: "durationdate1"` → `config.text: "05/17"`,接口应返回 `expiryDateText: "05/17"`(此前仅查根级字段,列恒为「无」)。 + +### Label ID 计算逻辑(与代码一致) + +同一门店、同一天内: + +1. 查询 `fl_label_print_task` 中 `LocationId` 相同且 `DATE(COALESCE(PrintedAt, CreationTime))` 相同的全部任务; +2. 按打印时间升序编号 `1, 2, 3…`; +3. 格式化为 `yyyyMMdd-n`。 + +> **注意**:序号统计范围为该门店**当日全部打印任务**(不限于当前列表筛选关键字),以保证同一天内序号全局唯一、连续。列表/导出的日期、门店筛选只决定**哪些行展示**,不改变已展示行的序号。 + +### 库内核对 SQL + +```sql +SELECT + t.Id AS task_id, + t.LocationId, + DATE(COALESCE(t.PrintedAt, t.CreationTime)) AS print_day, + ROW_NUMBER() OVER ( + PARTITION BY t.LocationId, DATE(COALESCE(t.PrintedAt, t.CreationTime)) + ORDER BY COALESCE(t.PrintedAt, t.CreationTime), t.Id + ) AS daily_seq, + CONCAT( + DATE_FORMAT(COALESCE(t.PrintedAt, t.CreationTime), '%Y%m%d'), + '-', + ROW_NUMBER() OVER ( + PARTITION BY t.LocationId, DATE(COALESCE(t.PrintedAt, t.CreationTime)) + ORDER BY COALESCE(t.PrintedAt, t.CreationTime), t.Id + ) + ) AS label_id_display +FROM fl_label_print_task t +WHERE t.LocationId = :locationId + AND COALESCE(t.PrintedAt, t.CreationTime) >= :dayStart + AND COALESCE(t.PrintedAt, t.CreationTime) < :dayEnd +ORDER BY print_day, daily_seq; +``` + +### 联调注意 + +| 现象 | 可能原因 | +|------|----------| +| 仍显示 `079` 等 | 后端未部署;或前端绑错字段(应绑 `labelCode`) | +| 同一天序号不连续 | 存在无 `LocationId` 的任务,显示「无」 | +| 与 App 端打印记录不一致 | App 接口为另一套(`us-app-labeling` 打印历史),本规则仅 **Reports Print Log** | + +--- + +## location 门店列表 + +**应用服务**:`LocationAppService` +**接口**:`GET /api/app/location`(分页;同源 Excel 导出 `export-locations-excel` 使用相同筛选) +**辅助类**:`LocationRegionScopeHelper`、`ReportsRoleHelper` + +### 数据范围(按 Token) + +| 角色 | 可见门店 | +|------|----------| +| **管理员** | **全部**未删除门店(`ReportsRoleHelper.IsAdminRole`) | +| **非管理员** | 仅 **`userlocation` 绑定门店所属 Region** 下的全部门店 | + +**Region 判定**:与 `fl_group` / `group` 列表一致——取绑定门店的 `location.Partner`(公司名称)+ `location.GroupName`(Region 名称)去重后,列表返回 **同一 Partner + GroupName** 的所有 `location` 行(不限于本人绑定的那几家店)。 + +未绑定门店、或绑定门店缺少 `Partner`/`GroupName` 时,列表为空。 + +### 请求示例 + +```http +GET /api/app/location?SkipCount=1&MaxResultCount=10 +Authorization: Bearer {token} +``` + +可选 Query:`Keyword`、`Partner`、`GroupName`、`State`、`Sorting`(在数据范围之上再收窄)。 + +### 与 group 列表的关系 + +| 接口 | 非管理员范围 | +|------|----------------| +| `GET /api/app/group` | 可见的 **fl_group** 记录 | +| `GET /api/app/location` | 上述 Region 对应的 **location** 门店 | + +### 库内核对(非管理员) + +```sql +-- 当前用户绑定门店所属的 Region(Partner + GroupName) +SELECT DISTINCT loc.Partner, loc.GroupName +FROM userlocation ul +INNER JOIN location loc ON ul.LocationId = loc.Id AND loc.IsDeleted = 0 +WHERE ul.IsDeleted = 0 AND ul.UserId = :currentUserId + AND loc.Partner IS NOT NULL AND loc.Partner != '' + AND loc.GroupName IS NOT NULL AND loc.GroupName != ''; + +-- 列表应返回的同 Region 全部门店 +SELECT loc.* +FROM location loc +WHERE loc.IsDeleted = 0 + AND (loc.Partner, loc.GroupName) IN ( + SELECT DISTINCT l2.Partner, l2.GroupName + FROM userlocation ul + INNER JOIN location l2 ON ul.LocationId = l2.Id AND l2.IsDeleted = 0 + WHERE ul.IsDeleted = 0 AND ul.UserId = :currentUserId + ); +``` + +### 联调注意 + +| 现象 | 处理 | +|------|------| +| 非管理员列表为空 | 检查 `userlocation` 是否有绑定;门店 `Partner`、`GroupName` 是否已填 | +| 只能看到部分 Region | 正常:仅能看到绑定门店所在 Region;换绑门店可扩大范围 | +| 入参 `GroupName` 越权筛选 | 仅能筛 **已有权限范围内** 的数据,不会扩大范围 | + +--- + +## us-app-labeling-tree 四级列表 + +**应用服务**:`UsAppLabelingAppService.GetLabelingTreeAsync` +**接口**:`GET /api/app/us-app-labeling/labeling-tree?locationId={guid}` + +### 问题与修复(2026-05-18) + +| 现象 | 原因 | 处理 | +|------|------|------| +| App 各门店 Labeling 页 **No products found** | 产品已写入 `fl_location_product`,但 `fl_product_category.AvailabilityType=SPECIFIED` 且 **未**在 `fl_product_category_location` 配置当前门店时,旧逻辑在 Join 条件中整行过滤 | **已修复**:四级树的产品范围仅由 **`fl_location_product` + `fl_label.LocationId`** 决定;产品分类、标签分类仅校验未删除且启用,不再用 SPECIFIED 子查询拦截 | +| 某门店仍为空 | 该门店 **无** `fl_label`(`LocationId` 匹配且 `State=1`) | 属数据:需在 Web **Labels** 为该门店创建/复制标签,或把已有标签的 `LocationId` 指到该门店 | + +### 数据范围(与代码一致) + +1. **门店产品**:`fl_location_product.LocationId = locationId` +2. **门店标签**:`fl_label.LocationId = locationId` 且未删除、启用 +3. **关联**:`fl_label_product` 连接标签与产品 +4. **登录**:须绑定该门店(`userlocation`),否则返回业务错误 + +### 请求示例 + +```http +GET /api/app/us-app-labeling/labeling-tree?locationId=3a212211-3b01-d66f-a804-125c0cee3bf0 +Authorization: Bearer {token} +``` + +### 出参 + +`UsAppLabelCategoryTreeNodeDto[]`(L1 标签分类 → L2 产品分类 → L3 产品+模板卡片 → L4 标签类型)。 + +### 库内核对 SQL + +```sql +-- 门店是否有可展示产品(绑定产品数) +SELECT COUNT(*) FROM fl_location_product WHERE LocationId = :locationId; + +-- 门店是否有可用标签(决定树是否非空) +SELECT COUNT(*) FROM fl_label +WHERE LocationId = :locationId AND IsDeleted = 0 AND State = 1; + +-- 修复后应能查到的树行数(与接口一致) +SELECT COUNT(*) AS tree_rows +FROM fl_label_product lp +INNER JOIN fl_label l ON lp.LabelId = l.Id +INNER JOIN fl_product p ON lp.ProductId = p.Id +INNER JOIN fl_label_category c ON l.LabelCategoryId = c.Id +INNER JOIN fl_label_type t ON l.LabelTypeId = t.Id +INNER JOIN fl_label_template tpl ON l.TemplateId = tpl.Id +LEFT JOIN fl_product_category pc ON p.CategoryId = pc.Id +WHERE l.LocationId = :locationId + AND p.Id IN (SELECT ProductId FROM fl_location_product WHERE LocationId = :locationId) + AND l.IsDeleted = 0 AND l.State = 1 + AND p.IsDeleted = 0 AND p.State = 1 + AND c.IsDeleted = 0 AND c.State = 1 + AND t.IsDeleted = 0 AND t.State = 1 + AND tpl.IsDeleted = 0 + AND (pc.Id IS NULL OR (pc.IsDeleted = 0 AND pc.State = 1)); +``` + +--- + +## us-app-print-log App 打印日志与报表 + +**应用服务**:`UsAppLabelingAppService` +**辅助类**:`UsAppPrintLogScopeHelper`、`ReportsRoleHelper` + +### 涉及接口 + +| 接口 | 说明 | +|------|------| +| `POST /api/app/us-app-labeling/get-print-log-list` | 打印日志分页 | +| `POST /api/app/us-app-labeling/get-label-report` | 当前门店 Label Report 统计(出参与 Web `reports/label-report` 同结构) | +| `POST /api/app/us-app-labeling/reprint` | 重打;权限与日志查看一致 | + +### 查看权限(按 Token + 当前门店) + +须已登录,且 `userlocation` **绑定** `locationId`。 + +| 角色 | 打印日志 / Report 数据范围 | +|------|---------------------------| +| **管理员** | 当前门店 **全部** 用户的打印任务(`ReportsRoleHelper.IsAdminRole`:用户名为 `admin`、角色码 `admin`、权限 `*:*:*` 等) | +| **Partner** | 同上;判定为 `UserRole` → `Role` 的 **`RoleCode` 或 `RoleName` 含 `partner`**(忽略大小写,如 Partner Admin) | +| **其它**(如 Staff、Store Manager) | 仅 **`CreatedBy == 当前用户 Id`** | + +### get-print-log-list + +**请求示例** + +```http +POST /api/app/us-app-labeling/get-print-log-list +Authorization: Bearer {token} +Content-Type: application/json + +{ + "locationId": "3a21220f-db37-3e32-7390-d55f64cd62a8", + "skipCount": 1, + "maxResultCount": 20 +} +``` + +**出参要点** + +| 字段 | 说明 | +|------|------| +| `items[].operatorName` | 实际打印人姓名(全店可见时为对应 `CreatedBy` 用户,非固定当前登录人) | +| 其它 | 与既有 `PrintLogItemDto` 一致 | + +### get-label-report + +**请求示例** + +```json +{ + "locationId": "3a21220f-db37-3e32-7390-d55f64cd62a8", + "startDate": "2026-04-07", + "endDate": "2026-05-18", + "keyword": "" +} +``` + +**出参**:`ReportsLabelReportOutputDto`(`summary`、`labelsByCategory`、`printVolumeTrend`、`mostUsedProducts`)。 + +### 与 Web Reports 的差异 + +| 模块 | 范围 | +|------|------| +| **App** `get-print-log-list` / `get-label-report` | 单门店 + 上表角色规则 | +| **Web** `reports/print-log-list` | 非管理员仍仅本人;见 reports-print-log 章节 | +| **Web** `reports/label-report` | 非管理员为绑定门店全量;见 reports-label-report 章节 | + +### 联调注意 + +| 现象 | 处理 | +|------|------| +| Partner 仍只看本人 | 确认角色 `RoleCode`/`RoleName` 含 `partner`,或是否为管理员 | +| 列表为空 | 确认 `locationId` 与绑定门店一致;Staff 仅能看到自己打印的记录 | + +--- + +## us-app-auth-location-detail 门店详情 + +**应用服务**:`UsAppAuthAppService` +**接口**:`GET /api/app/us-app-auth/location-detail/{locationId}` +**鉴权**:Bearer Token;仅可查当前用户在 **`userlocation`** 中绑定的门店。 + +### 变更说明 + +| 项 | 说明 | +|----|------| +| **新增出参** | **`operatingHours`**(经营时间) | +| **数据来源** | `location.OperatingHours`(`varchar(512)` 自由文本) | +| **空值展示** | 未维护或空白时返回 **`无`**(与 `locationName`、`storePhone` 等字段一致) | +| **维护入口** | Web 管理端 `POST/PUT /api/app/location` 的 `operatingHours` 字段 | + +### 数据库(若列不存在须先执行) + +脚本:`美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/scripts/fl_location_add_operating_hours_column.sql` + +```sql +ALTER TABLE `location` + ADD COLUMN `OperatingHours` varchar(512) DEFAULT NULL COMMENT '经营时间(自由文本)' AFTER `Longitude`; +``` + +### 请求示例 + +```http +GET /api/app/us-app-auth/location-detail/3a21220f-db37-3e32-7390-d55f64cd62a8 +Authorization: Bearer {token} +``` + +### 响应体(UsAppLocationDetailOutputDto) + +| 字段(JSON) | 类型 | 说明 | +|--------------|------|------| +| `locationId` | string | 门店主键 Guid | +| `locationName` | string | 门店名称 | +| `fullAddress` | string | 街道/城市/州/邮编拼接 | +| `storePhone` | string | `location.Phone` | +| **`operatingHours`** | string | **经营时间**;示例:`Mon–Fri 9:00 AM – 6:00 PM`;空为 `无` | +| `managerName` | string | 本店绑定用户中角色含 `manager` 者姓名 | +| `managerPhone` | string | 同上用户电话 | + +### 响应示例 + +```json +{ + "locationId": "3a21220f-db37-3e32-7390-d55f64cd62a8", + "locationName": "Central Park Store", + "fullAddress": "123 Main St, New York, NY 10001", + "storePhone": "(212) 555-0100", + "operatingHours": "Mon–Fri 9:00 AM – 6:00 PM", + "managerName": "Jane Doe", + "managerPhone": "+1 (555) 123-4567" +} +``` + +### 联调注意 + +| 现象 | 处理 | +|------|------| +| `operatingHours` 恒为 `无` | 1)确认已执行 DDL;2)在 Web 门店编辑保存 `operatingHours`;3)重启 API 使实体映射生效 | +| 403 / 业务异常 | 当前 Token 用户未在 `userlocation` 绑定该 `locationId` | + +--- + +## reports-template-print-stat 模板打印统计 + +**应用服务**:`ReportsAppService` +**接口**:`GET /api/app/reports/template-print-stat-list` +**辅助类**:`ReportsLocationScopeHelper`、`ReportsRoleHelper` + +### 功能 + +按 **`fl_label_template`** 汇总 **`fl_label_print_task`** 行数,返回 **模板名称 + 打印标签数量** 分页列表(默认按 `printedCount` 降序)。 + +### 数据范围(与 label-report 一致) + +| 角色 | 统计范围 | +|------|----------| +| **管理员** | 全部门店打印任务(可按 Company/Region/Location 入参收窄) | +| **非管理员** | 仅 **`userlocation` 绑定门店**内全部打印任务(**不按** `CreatedBy` 过滤) | + +### 入参 + +| 参数 | 说明 | +|------|------| +| `SkipCount` / `MaxResultCount` | 分页(`SkipCount` 为 **1-based 页码**,第一页传 `1`) | +| `StartDate` / `EndDate` | 统计区间(含起止日;未传默认近 30 天至今天) | +| `PartnerId` | Company(`fl_partner.Id`) | +| `GroupId` | Region(`fl_group.Id`) | +| `LocationId` | 门店(`location.Id`) | +| `Keyword` | **模板名称**模糊匹配(`fl_label_template.TemplateName`) | +| `Sorting` | 可选 `PrintedCount asc`;默认 **`PrintedCount desc`** | + +### 请求示例 + +```http +GET /api/app/reports/template-print-stat-list?SkipCount=1&MaxResultCount=20&StartDate=2026-04-07&EndDate=2026-05-18 +Authorization: Bearer {token} +``` + +### 出参(`items[]`) + +| 字段 | 类型 | 说明 | +|------|------|------| +| `templateId` | string? | `fl_label_template.Id` | +| `templateName` | string | 模板名称;缺失时 **「无」** | +| `printedCount` | int | 该模板下打印任务条数 | + +**分页包装**:`pageIndex`、`pageSize`、`totalCount`、`totalPages`、`items`(与其它列表一致)。 + +### 响应示例 + +```json +{ + "pageIndex": 1, + "pageSize": 20, + "totalCount": 3, + "totalPages": 1, + "items": [ + { "templateId": "tpl-001", "templateName": "2x3 Price Label", "printedCount": 128 }, + { "templateId": "tpl-002", "templateName": "Deli Scale Label", "printedCount": 45 } + ] +} +``` + +### 库内核对 + +```sql +SELECT t.TemplateId, + tpl.TemplateName, + COUNT(*) AS printed_count +FROM fl_label_print_task t +LEFT JOIN fl_label_template tpl ON t.TemplateId = tpl.Id +INNER JOIN location loc ON t.LocationId = CAST(loc.Id AS CHAR) AND loc.IsDeleted = 0 +WHERE COALESCE(t.PrintedAt, t.CreationTime) >= :start + AND COALESCE(t.PrintedAt, t.CreationTime) < :endExcl + AND t.LocationId IN (:allowedLocationIds) -- 非管理员:userlocation 绑定门店 +GROUP BY t.TemplateId, tpl.TemplateName +ORDER BY printed_count DESC; +``` + +### 联调注意 + +| 现象 | 处理 | +|------|------| +| 列表为空 | 检查日期区间、门店绑定、该区间是否有打印任务 | +| 与 **print-log-list** 数量不一致 | print-log 非管理员仅本人任务;本接口与 **label-report** 同范围 | +| 模板已删除仍有统计 | 任务仍保留 `TemplateId`;名称来自 Left Join,无名称时显示「无」 | + +--- + +## reports-label-report 标签报表 + +**应用服务**:`ReportsAppService` +**接口**:`GET /api/app/reports/label-report`(及同源 `export-label-report-pdf`) +**辅助类**:`ReportsLocationScopeHelper`、`ReportsRoleHelper` + +### 数据范围(按 Token) + +| 角色 | 统计范围 | 说明 | +|------|----------|------| +| **管理员** | 全部门店打印任务 | 识别方式与 Print Log 一致:`admin` 角色 / 用户名为 `admin` / 权限 `*:*:*` | +| **非管理员** | 仅 **`userlocation` 绑定门店** | `UserLocation.UserId = 当前用户 Id` 的 `LocationId`;统计该门店下**全部**打印任务,**不按** `CreatedBy` 过滤 | + +### 入参筛选(与 Print Log 一致) + +| 参数 | 说明 | +|------|------| +| `StartDate` / `EndDate` | 统计区间(含起日、含止日;未传时默认近 30 天至今天) | +| `PartnerId` | Company(`fl_partner.Id`) | +| `GroupId` | Region(`fl_group.Id`) | +| `LocationId` | 门店(`location.Id`) | +| `Keyword` | 产品名 / 标签分类 / 产品分类模糊匹配 | + +**筛选叠加规则** + +- **管理员**:未传 Company/Region/Location → 不限制门店;传入后与对应门店集合取交集。 +- **非管理员**:始终在绑定门店集合内统计;若再传 `PartnerId` / `GroupId` / `LocationId`,与绑定门店 **取交集**(传了未绑定门店 → 空数据)。 +- **无绑定门店**:返回空统计(各指标为 0 / 空列表)。 + +### 示例 + +```http +GET /api/app/reports/label-report?StartDate=2026-04-07&EndDate=2026-05-18 +Authorization: Bearer {token} +``` + +### 出参结构(不变) + +| 块 | 字段 | +|----|------| +| `summary` | `totalLabelsPrinted`、`totalLabelsPrintedPrevPeriod`、`mostPrintedCategoryName`、`topProductName`、`avgDailyPrints` 等 | +| `labelsByCategory` | 按标签分类汇总 | +| `printVolumeTrend` | 近 7 日(在查询区间内)每日打印量 | +| `mostUsedProducts` | Top 20 产品 | + +### 与 Print Log 的差异 + +| 模块 | 非管理员范围 | +|------|----------------| +| **label-report** | 绑定门店内**所有**打印记录 | +| **print-log-list** | 仍仅 **`CreatedBy = 当前用户`**(见 `报表Reports接口对接说明.md`) | + +### 库内核对(非管理员) + +```sql +-- 当前用户绑定的门店 +SELECT ul.LocationId +FROM userlocation ul +WHERE ul.IsDeleted = 0 AND ul.UserId = :currentUserId; + +-- 绑定门店在区间内的打印量(应与接口 summary.totalLabelsPrinted 一致) +SELECT COUNT(*) AS cnt +FROM fl_label_print_task t +WHERE t.LocationId IN (:boundLocationIds) + AND COALESCE(t.PrintedAt, t.CreationTime) >= :start + AND COALESCE(t.PrintedAt, t.CreationTime) < :endExclusive; +``` + +--- + +## label-multiple-option 多选项 + +**应用服务**:`LabelMultipleOptionAppService` + +**命名约定**:UI **Region** = 入参 **`regionIds` / `groupIds`**、列表筛选 **`groupId`**(均为 `fl_group.Id`);UI **Location** = 入参 **`locationIds`**、列表筛选 **`locationId`**(`location.Id`)。存储表 **`fl_label_multiple_option_location`**(与 label-type / label-category 一致)。 + +### 数据库变更(列表筛选前须执行) + +脚本:`美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/scripts/fl_label_multiple_option_scope.sql` + +```sql +ALTER TABLE `fl_label_multiple_option` + ADD COLUMN `AvailabilityType` varchar(20) NOT NULL DEFAULT 'ALL' COMMENT '门店可用范围:ALL/SPECIFIED' AFTER `State`; + +CREATE TABLE IF NOT EXISTS `fl_label_multiple_option_location` ( + `Id` varchar(36) NOT NULL, + `MultipleOptionId` varchar(36) NOT NULL, + `LocationId` varchar(36) NOT NULL, + `CreationTime` datetime NOT NULL, + `CreatorId` varchar(36) DEFAULT NULL, + PRIMARY KEY (`Id`), + KEY `idx_fl_lmol_option` (`MultipleOptionId`), + KEY `idx_fl_lmol_location` (`LocationId`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='标签多选项适用门店'; +``` + +> 执行 DDL 后,历史数据默认 **`AvailabilityType = ALL`**,任意 Region/门店筛选下仍可见。 + +### 新增 `POST /api/app/label-multiple-option`、编辑 `PUT /api/app/label-multiple-option/{id}` + +| 字段 | 类型 | 必填 | 说明 | +|------|------|------|------| +| optionCode | string | 是 | 多选项编码 | +| optionName | string | 是 | 多选项名称 | +| optionValuesJson | string | 否 | 选项值 JSON 字符串 | +| state | bool | 否 | 默认 `true` | +| orderNum | int | 否 | 排序 | +| availabilityType | string | 否 | `ALL` / `SPECIFIED`;传 region/location 数组时自动 **`SPECIFIED`** | +| **regionIds** | string[] | 否 | **Region 多选**(`fl_group.Id`) | +| **groupIds** | string[] | 否 | 与 `regionIds` 合并去重 | +| **locationIds** | string[] | 否 | **Location 多选**(`location.Id`) | + +**合并规则**:每个 `regionIds` 展开为该 Region 下全部门店,再与 `locationIds` **取并集** → 写入 `fl_label_multiple_option_location`;`SPECIFIED` 时至少 **1** 个有效门店。 + +**请求示例** + +```json +POST /api/app/label-multiple-option +{ + "optionCode": "OPT_ALLERGENS", + "optionName": "Allergens", + "optionValuesJson": "[\"Peanuts\",\"Dairy\",\"Gluten\"]", + "state": true, + "orderNum": 1, + "availabilityType": "SPECIFIED", + "regionIds": ["fl_group_id_east"], + "locationIds": ["11111111-1111-1111-1111-111111111111"] +} +``` + +**详情 `GET /api/app/label-multiple-option/{id}` 出参(范围字段)** + +| 字段 | 说明 | +|------|------| +| availabilityType | `ALL` / `SPECIFIED` | +| regionIds | Region Id(`SPECIFIED` 时由门店反推) | +| groupIds | 与 `regionIds` 相同 | +| locationIds | 已绑定门店 Id | + +**编辑**:Body 与新增相同;会 **先删后插** 重建 `fl_label_multiple_option_location`。 + +--- + +### 列表 `GET /api/app/label-multiple-option` + +示例:`?SkipCount=1&MaxResultCount=10` + +### 列表 Query 筛选 + +| 字段 | 类型 | 必填 | 说明 | +|------|------|------|------| +| skipCount | int | 是 | 跳过条数(第 1 页常为 `1`) | +| maxResultCount | int | 是 | 每页条数 | +| sorting | string | 否 | 排序 | +| keyword | string | 否 | 匹配 `optionCode`、`optionName` | +| state | bool | 否 | 启用状态 | +| **groupId** | string | 否 | **Region** 筛选(`fl_group.Id`) | +| **locationId** | string | 否 | **Location** 筛选;**优先于** `groupId` | + +**筛选语义**(与 label-category / label-type 一致): + +- 未传 `groupId`、`locationId`:返回全部未删除多选项。 +- 传入筛选:返回 **`availabilityType = ALL`** 或 **`fl_label_multiple_option_location` 命中该门店** 的记录。 + +**请求示例** + +```bash +GET /api/app/label-multiple-option?SkipCount=1&MaxResultCount=10 +Authorization: {token} +``` + +```bash +GET /api/app/label-multiple-option?SkipCount=1&MaxResultCount=10&groupId=你的fl_group主键 +Authorization: {token} +``` + +```bash +GET /api/app/label-multiple-option?SkipCount=1&MaxResultCount=10&locationId=11111111-1111-1111-1111-111111111111 +Authorization: {token} +``` + +### 列表出参 `items[]` + +| 字段 | 类型 | 说明 | +|------|------|------| +| id | string | 多选项主键 | +| optionCode / optionName | string | 编码、名称 | +| optionValuesJson | string | 选项值 JSON | +| state / orderNum | bool / int | 状态、排序 | +| availabilityType | string | `ALL` / `SPECIFIED` | +| **region** | string | 列表列 **Region**:`ALL` → `All Regions`;`SPECIFIED` → 绑定门店 `GroupName` 拼接 | +| **location** | string | 列表列 **Location**:`ALL` → `All Locations`;`SPECIFIED` → 门店名拼接 | +| regionIds | string[] | Region Id(`SPECIFIED` 时由门店反推) | +| locationIds | string[] | 门店 Id | +| lastEdited | datetime | 最近编辑时间 | + +**响应示例** + +```json +{ + "id": "opt_allergens_001", + "optionCode": "OPT_ALLERGENS", + "optionName": "Allergens", + "optionValuesJson": "[\"Peanuts\",\"Dairy\"]", + "state": true, + "availabilityType": "SPECIFIED", + "orderNum": 1, + "region": "East Region", + "location": "UNCC store", + "regionIds": ["fl_group_id_east"], + "locationIds": ["11111111-1111-1111-1111-111111111111"], + "lastEdited": "2026-05-18T12:00:00" +} +``` + +### 库内核对 SQL + +```sql +SELECT + o.Id, + o.OptionName, + o.AvailabilityType, + ol.LocationId, + loc.GroupName AS region_name, + COALESCE(NULLIF(TRIM(loc.LocationName), ''), loc.LocationCode) AS location_name +FROM fl_label_multiple_option o +LEFT JOIN fl_label_multiple_option_location ol ON ol.MultipleOptionId = o.Id +LEFT JOIN location loc ON loc.Id = ol.LocationId AND loc.IsDeleted = 0 +WHERE o.IsDeleted = 0 +ORDER BY o.OrderNum DESC; +``` + +### 联调注意 + +| 现象 | 可能原因 | +|------|----------| +| region / location 为 `All Regions` / `All Locations` | `availabilityType = ALL`(历史数据默认) | +| region / location 为「无」 | `SPECIFIED` 但未写入 `fl_label_multiple_option_location` | +| 筛 Region 后列表为空 | 非 `ALL` 且关联表未覆盖该门店 | +| 接口报错列不存在 | 未执行 `fl_label_multiple_option_scope.sql` | +| 保存报「至少需要匹配到一个有效门店」 | Region 下无门店且 `locationIds` 为空 | + +--- + +## label-type 标签类型 + +**应用服务**:`LabelTypeAppService` +**命名约定**:UI **Region** = **`regionIds` / `groupIds`**(`fl_group.Id`);UI **Location** = **`locationIds`**(`location.Id`)。存储表 **`fl_label_type_location`**(与 label-category 一致)。 + +### 数据库变更(新增/编辑 Region·Location 前须执行) + +脚本:`美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/scripts/fl_label_type_scope.sql` + +```sql +ALTER TABLE `fl_label_type` + ADD COLUMN `AvailabilityType` varchar(20) NOT NULL DEFAULT 'ALL' COMMENT '门店可用范围:ALL/SPECIFIED' AFTER `State`; + +CREATE TABLE IF NOT EXISTS `fl_label_type_location` ( + `Id` varchar(36) NOT NULL, + `LabelTypeId` varchar(36) NOT NULL, + `LocationId` varchar(36) NOT NULL, + `CreationTime` datetime NOT NULL, + `CreatorId` varchar(36) DEFAULT NULL, + PRIMARY KEY (`Id`), + KEY `idx_fl_ltl_type` (`LabelTypeId`), + KEY `idx_fl_ltl_location` (`LocationId`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='标签类型适用门店'; +``` + +| 表/字段 | 说明 | +|---------|------| +| `fl_label_type.AvailabilityType` | `ALL` = 全部门店;`SPECIFIED` = 仅关联表内门店 | +| `fl_label_type_location` | `LabelTypeId` + `LocationId` 多选落库 | + +--- + +### 新增 `POST /api/app/label-type`、编辑 `PUT /api/app/label-type/{id}` + +| 字段 | 类型 | 必填 | 说明 | +|------|------|------|------| +| typeCode | string | 是 | 类型编码 | +| typeName | string | 是 | 类型名称 | +| state | bool | 否 | 默认 `true` | +| orderNum | int | 否 | 排序 | +| availabilityType | string | 否 | `ALL` / `SPECIFIED`;传了 region/location 数组时自动 **`SPECIFIED`** | +| **regionIds** | string[] | 否 | **Region 多选**(`fl_group.Id`);推荐字段名 | +| **groupIds** | string[] | 否 | 与 `regionIds` 合并去重(兼容) | +| **locationIds** | string[] | 否 | **Location 多选**(`location.Id`) | + +**合并规则**:每个 `regionIds` 展开为该 Region 下全部门店,再与 `locationIds` **取并集** → 写入 `fl_label_type_location`;`SPECIFIED` 时至少 **1** 个有效门店,否则报错:`指定适用区域或门店时,至少需要匹配到一个有效门店`。 + +**请求示例** + +```json +POST /api/app/label-type +{ + "typeCode": "PRICE", + "typeName": "Price Label", + "state": true, + "orderNum": 10, + "availabilityType": "SPECIFIED", + "regionIds": ["fl_group_id_east"], + "locationIds": ["11111111-1111-1111-1111-111111111111"] +} +``` + +**详情 `GET /api/app/label-type/{id}` 出参(范围字段)** + +| 字段 | 说明 | +|------|------| +| availabilityType | `ALL` / `SPECIFIED` | +| regionIds | Region Id 数组(`SPECIFIED` 时由门店反推) | +| groupIds | 与 `regionIds` 相同 | +| locationIds | 已绑定门店 Id | + +**编辑**:Body 字段与新增相同;会 **先删后插** 重建 `fl_label_type_location`。 + +--- + +### 列表 `GET /api/app/label-type` + +示例:`?SkipCount=1&MaxResultCount=10` + +### 列表 UI 列与 API 字段对照 + +| 列表列(UI) | API 字段(camelCase) | 类型 | 说明 | +|--------------|----------------------|------|------| +| **No. of Labels** | **noOfLabels** | long | 该 Label Type 下 **未删除** 标签条数(`fl_label.LabelTypeId = fl_label_type.Id` 且 `IsDeleted = 0`) | +| **Region** | **region** | string | `ALL` → `All Regions`;`SPECIFIED` → 绑定门店 `GroupName` 拼接;未配置为 **`无`** | +| **Location** | **location** | string | `ALL` → `All Locations`;`SPECIFIED` → 门店名拼接 | +| (辅助) | regionIds | string[] | Region 主键(`fl_group.Id`,由门店反推) | +| (辅助) | locationIds | string[] | 门店主键(`location.Id`) | +| Last Edited | lastEdited | datetime | 类型与下属标签最近编辑时间的较大值 | + +### No. of Labels 是否需要库字段? + +**不需要** `NoOfLabels` 物理列,由 `fl_label` 实时 `COUNT`。 + +| 表 | 作用 | +|----|------| +| `fl_label_type` | 类型主数据 + **`AvailabilityType`** | +| `fl_label_type_location` | 新增/编辑时 Region·Location 多选落库 | +| `fl_label` | 标签实例;**`noOfLabels`** 统计来源 | +| `location` | 门店;Region 展示用 **`GroupName`** | + +**统计逻辑(与代码一致)**: + +```sql +-- 某类型下标签数(No. of Labels) +SELECT COUNT(*) AS no_of_labels +FROM fl_label +WHERE IsDeleted = 0 + AND LabelTypeId = :labelTypeId; +``` + +列表接口在分页查出类型后,对当前页 `Id` 批量 `GROUP BY LabelTypeId` 计数,避免 N+1;**标签增删改后无需改类型表**,下次列表即反映最新数量。 + +### 变更说明 + +| 项 | 说明 | +|----|------| +| **列表** `GET /api/app/label-type` | 出参增加 **`noOfLabels`**、**`region`**、**`location`**、**`regionIds`**、**`locationIds`**、**`lastEdited`** | +| **列表筛选** | Query 可选 **`groupId`**(Region)、**`locationId`**(Location);`locationId` 优先于 `groupId` | +| **统计一致性** | 传 Region/门店筛选时,**`noOfLabels`**、**`region`/`location`** 仅统计筛选范围内标签 | +| **筛选语义** | 传 `groupId`/`locationId` 时:返回 **`availabilityType=ALL`** 或 **关联门店命中** 的类型(与 label-category 一致) | +| **列表 region/location** | 来自 **配置**(`AvailabilityType` + `fl_label_type_location`),非仅标签反推 | + +**命名约定**:UI **Region** = Query **`groupId`**(`fl_group.Id`);UI **Location** = Query **`locationId`**(`location.Id`)。 + +### 列表 Query + +| 字段 | 类型 | 必填 | 说明 | +|------|------|------|------| +| skipCount | int | 是 | 跳过条数(项目约定:第 1 页常为 `1`) | +| maxResultCount | int | 是 | 每页条数 | +| sorting | string | 否 | 排序,如 `orderNum desc` | +| keyword | string | 否 | 匹配 `typeCode`、`typeName` | +| state | bool | 否 | 启用状态 | +| **groupId** | string | 否 | **Region** 筛选(`fl_group.Id`) | +| **locationId** | string | 否 | **Location** 筛选(`location.Id`);**优先于** `groupId` | + +**请求示例(默认列表,含三列出参)** + +```bash +GET /api/app/label-type?SkipCount=1&MaxResultCount=10 +Authorization: {token} +``` + +**按 Region / Location 筛选** + +```bash +GET /api/app/label-type?SkipCount=1&MaxResultCount=10&groupId=你的fl_group主键 +Authorization: {token} +``` + +```bash +GET /api/app/label-type?SkipCount=1&MaxResultCount=10&locationId=11111111-1111-1111-1111-111111111111 +Authorization: {token} +``` + +### 列表出参 `items[]`(节选) + +| 字段 | 类型 | 说明 | +|------|------|------| +| id | string | 类型主键(`fl_label_type.Id`) | +| typeCode / typeName | string | 编码、名称 | +| state / orderNum | bool / int | 状态、排序 | +| availabilityType | string | `ALL` / `SPECIFIED` | +| **noOfLabels** | long | 列表列 **No. of Labels** | +| **region** | string | 列表列 **Region** | +| **location** | string | 列表列 **Location** | +| regionIds | string[] | Region Id(`fl_group.Id`) | +| locationIds | string[] | 门店 Id | +| lastEdited | datetime | 最近编辑时间 | + +**响应 `items[]` 示例** + +```json +{ + "id": "type_price_001", + "typeCode": "PRICE", + "typeName": "Price Label", + "state": true, + "availabilityType": "SPECIFIED", + "orderNum": 10, + "region": "East Region", + "location": "UNCC store, Central Park Store", + "regionIds": ["fl_group_id_east"], + "locationIds": ["11111111-1111-1111-1111-111111111111"], + "noOfLabels": 8, + "lastEdited": "2026-05-18T10:00:00" +} +``` + +### 库内核对 SQL(只读,无需执行 DDL) + +**1)确认 `fl_label_type` 无 NoOfLabels 列(不应加列)** + +```sql +SELECT COLUMN_NAME +FROM information_schema.COLUMNS +WHERE TABLE_SCHEMA = DATABASE() + AND TABLE_NAME = 'fl_label_type' +ORDER BY ORDINAL_POSITION; +``` + +**2)按类型统计标签数(与接口 `noOfLabels` 一致)** + +```sql +SELECT + t.Id, + t.TypeCode, + t.TypeName, + COUNT(l.Id) AS no_of_labels +FROM fl_label_type t +LEFT JOIN fl_label l + ON l.LabelTypeId = t.Id AND l.IsDeleted = 0 +WHERE t.IsDeleted = 0 +GROUP BY t.Id, t.TypeCode, t.TypeName +ORDER BY t.OrderNum DESC, t.CreationTime DESC; +``` + +**3)某类型已配置的 Region / Location(与列表 region/location 一致)** + +```sql +SELECT + t.Id, + t.TypeName, + t.AvailabilityType, + tl.LocationId, + loc.GroupName AS region_name, + COALESCE(NULLIF(TRIM(loc.LocationName), ''), loc.LocationCode) AS location_name +FROM fl_label_type t +LEFT JOIN fl_label_type_location tl ON tl.LabelTypeId = t.Id +LEFT JOIN location loc ON loc.Id = tl.LocationId AND loc.IsDeleted = 0 +WHERE t.IsDeleted = 0 + AND t.Id = :labelTypeId; +``` + +### 联调注意(label-type) + +| 现象 | 可能原因 | +|------|----------| +| **noOfLabels 为 0** | 该类型下尚无标签,或标签均已逻辑删除 | +| region / location 为「无」 | `SPECIFIED` 但未绑定门店,或门店无 `GroupName`/名称 | +| 保存报「至少需要匹配到一个有效门店」 | Region 下无门店且 `locationIds` 为空 | +| 筛 Region 后列表变空 | 非 `ALL` 且 `fl_label_type_location` 未覆盖该门店 | +| 未执行 DDL | 先跑 `fl_label_type_scope.sql` | +| noOfLabels 与标签列表不一致 | 对比时 label 列表需传与 label-type 相同的 `groupId`/`locationId` | +| 前端列空白 | 绑定 **`noOfLabels`**、**`region`**、**`location`**(勿用 `creationTime` 代替 `lastEdited`) | + +> 与 **label-category**(见 `5-17接口优化.md`)区别:分类有 `availabilityType=ALL`;**标签类型**无 ALL,三列均由 **`fl_label`** 实时汇总。 + +> **不提供** `ALTER TABLE fl_label_type ADD NoOfLabels ...`:冗余字段易与 `fl_label` 不一致,且每次标签变更都要维护计数。 + +--- + +## team-member 列表 Company·Region·Location 筛选 + +**应用服务**:`TeamMemberAppService` +**接口**:`GET /api/app/team-member`(分页;PDF 导出 `export-team-members-pdf` 使用相同 Query 筛选) +**辅助类**:`LocationScopeBindingHelper.ResolveFilteredLocationIdsForListAsync` + +### Query 参数(在原有 Keyword / RoleId / State 基础上) + +| UI | Query 参数 | 说明 | +|----|------------|------| +| Company | `partnerId` | `fl_partner.Id` | +| Region | `groupId` | `fl_group.Id` | +| Location | `locationId` | `location.Id`(Guid 字符串) | + +**筛选优先级**(与 product 列表一致):传 `locationId` 时仅按该门店;否则传 `groupId` 按 Region 下全部门店;否则传 `partnerId` 按 Company 下全部门店;均未传则不按组织范围过滤。 + +**命中规则**:返回在 **`userlocation`** 中至少绑定一家「落在上述门店集合内」的成员;列表行内 `assignedLocations` 在传入范围参数时仅展示该范围内的绑定门店。 + +### 请求示例 + +```http +GET /api/app/team-member?SkipCount=1&MaxResultCount=10&partnerId={fl_partner.Id} +Authorization: Bearer {token} + +GET /api/app/team-member?SkipCount=1&MaxResultCount=10&groupId={fl_group.Id} + +GET /api/app/team-member?SkipCount=1&MaxResultCount=10&locationId={location.Id} +``` + +可与 `Keyword`、`RoleId`、`State`、`Sorting` 组合使用。 + +### 库内核对 + +```sql +-- 某 Region 下应出现在列表中的成员(userId 去重) +SELECT DISTINCT ul.UserId +FROM userlocation ul +INNER JOIN location loc ON ul.LocationId = CAST(loc.Id AS CHAR) AND loc.IsDeleted = 0 +WHERE ul.IsDeleted = 0 + AND loc.Partner = :partnerName + AND loc.GroupName = :groupName; +``` + +### 联调注意 + +| 现象 | 处理 | +|------|------| +| 传了 `groupId` 仍无数据 | 确认成员 `userlocation` 是否绑定该区域下门店 | +| `partnerId` / `groupId` 无效 | 返回空列表(非 404) | +| 与 5-17 新增字段关系 | 列表 **筛选** 用 Query;新增/编辑 Body 仍用 `partnerId` / `regionIds` / `locationIds`(见 `5-17接口优化.md`) | + +--- + +## team-member 新增用户无法平台登录 + +### 问题现象 + +通过 **`POST /api/app/team-member`** 新增用户(如 `email=123@qq.com`、`userName=1234`)后,在平台登录页用 **邮箱 + 创建时密码** 登录失败(`Sign-in failed: incorrect email or password`)。 + +### 根因(两处) + +**1)新增时双重 `BuildPassword`(已修复)** + +- `new UserAggregateRoot(userName, password, …)` 构造函数内已 `BuildPassword()` 一次; +- 若再调用 `user.BuildPassword()`,会对 **已哈希值再哈希**,明文登录必失败。 + +**2)改密时 SqlSugar `IsOwnsOne` 只更新了 Salt、未更新 Password(本次主因)** + +- `PUT /api/app/team-member/{id}` 传入新明文 → `BuildPassword()` 会生成 **新 Salt + 新 Password 哈希**; +- `UpdateAsync` 整表更新时 **`Password` 列可能未写入**,库中仍保留旧哈希(例如与 admin 相同的 `ANg9hGZC…`),但 **Salt 已是新值**; +- 校验 `SHA2Encode(明文, Salt)` 与库中 `Password` 永远对不上(查库可见 `123@qq.com` 与 `admin` 的 `Password` 相同、`Salt` 不同)。 + +### 代码修复 + +- 新增:与 `UserDataSeed` 一致,`EncryPassword = new EncryPasswordValueObject(明文)` 后 **只调用一次** `BuildPassword()`。 +- 改密 / 重置密码:改密后调用 `UserPasswordHelper.EnsurePasswordColumnsPersistedAsync`,**显式 SET `Password` 与 `Salt`**(`Yi.Framework.Rbac.Domain/Helpers/UserPasswordHelper.cs`)。 +- 登录校验统一走 `UserPasswordHelper.VerifyPlainPassword` / `UserAggregateRoot.JudgePassword`。 + +### 平台登录约定 + +| 项目 | 说明 | +|------|------| +| 接口 | `POST /api/app/account/login`(`AccountService.PostLoginAsync`) | +| 入参 | `userName` 填 **邮箱**(平台 UI 的 Email 框);`password` 为明文 | +| 匹配 | `User.Email`(忽略大小写)优先;否则 `User.UserName` 与邮箱相同也可 | +| 状态 | `State = true` 且未删除才可登录 | + +**登录示例** + +```json +POST /api/app/account/login +{ + "userName": "123@qq.com", + "password": "创建时设置的明文密码" +} +``` + +### 已受影响账号的处理 + +部署修复后,**已受影响账号**(如 `123@qq.com`,库内 `Password`/`Salt` 不一致)必须 **再保存一次密码**: + +```http +PUT /api/app/team-member/{id} +Content-Type: application/json + +{ + "fullName": "显示名", + "userName": "1234", + "email": "123@qq.com", + "password": "新的明文密码", + "phone": 1234567890, + "roleId": "角色Guid", + "locationIds": ["门店Id"], + "state": true +} +``` + +保存后用 **邮箱 + 新密码** 登录。 + +**库内核对** + +```sql +SELECT Id, UserName, Email, State, Password, Salt +FROM User +WHERE Email = '123@qq.com' AND IsDeleted = 0; +-- 正常:同一用户的 Password 须与 SHA512(salt+明文) 一致;勿出现与 admin 相同 Password、不同 Salt。 +``` + +### 新增成员入参提醒 + +| 字段 | 说明 | +|------|------| +| userName | 登录名(可与邮箱不同);平台登录仍建议用 **email** 字段 | +| email | 平台登录主键(须为合法邮箱格式) | +| password | 明文,至少 6 位(`UserManager` 校验) | +| roleId | 须绑定角色,否则登录后可能无菜单 | +| locationIds / partnerId / groupIds | 门店范围(见 5-17 team-member 章节) | + +--- + +## rbac-role accessPermissions + +以下为 **`/api/app/rbac-role`** 相关说明(原 5-18 内容)。 + +--- + +## 问题与根因 + +| 现象 | 根因 | +|------|------| +| `POST`/`PUT` 传了 `accessPermissions` 后仍无菜单绑定 | 库表 **`Menu.PermissionCode` 全为空**,按 Code 解析不到 `MenuId`;或入参 `menuIds: []` 抢先清空绑定 | +| `GET /api/app/rbac-role/{id}` 的 `accessPermissions` 为 `""` | 无 **`RoleMenu`** 记录,或已绑定菜单的 **`PermissionCode` 为空** | +| `menuIds` 详情为空(历史问题) | 曾错误查询 `rolemenu` 字符串表;已改为查询 **`RoleMenu`**(Guid) | + +**数据链路(正确)** + +```text +accessPermissions(入参/出参字符串) + ↔ Menu.PermissionCode(权限码,可为空时按 Router 推导 menu.xxx) + ↔ RoleMenu(RoleId + MenuId) + ↔ Role +``` + +--- + +## 变更摘要 + +| 项 | 说明 | +|----|------| +| **绑定写入** | `accessPermissions` 解析为 `MenuId` 后 **覆盖写入 `RoleMenu`**;解析不到任何菜单时 **返回明确错误** | +| **出参汇总** | `GET` 列表/详情从 **`RoleMenu` + `Menu.PermissionCode`** 汇总;`PermissionCode` 为空时按 **`Router`** 推导(与回填 SQL 一致) | +| **详情 menuIds** | 从 **`RoleMenu`** 读取,不再查错误的 `rolemenu` 映射 | +| **RoleMenu 主键** | 插入时生成 **`Id`**(`RoleMenu` 表必填) | +| **数据库脚本** | `scripts/menu_backfill_permission_code.sql` 批量回填 `PermissionCode` | + +--- + +## 公共约定 + +- **宿主**:`Yi.Abp.Web`;路由前缀 `api/app`。 +- **应用服务**:`RbacRoleAppService`(模块 `food-labeling-us`)。 +- **鉴权**:`Authorization: {token}`。 + +--- + +## accessPermissions 规则 + +### 出参(只读) + +- 查询 **`RoleMenu`** 中该角色绑定的菜单,取 **`Menu.PermissionCode`**(非空优先)。 +- 若 `PermissionCode` 为空,按 **`Router`** 推导:`/labels` → `menu.labels`。 +- 去重后按字母序用 **`, `** 拼接;无绑定为 `""`。 + +### 入参(写入) + +| 场景 | menuIds | accessPermissions | 行为 | +|------|---------|-------------------|------| +| 新增/编辑 | 非空数组 | 任意 | **以 menuIds 为准**(覆盖 `RoleMenu`) | +| 新增/编辑 | 不传 / null | 非空字符串 | 按 PermissionCode 解析菜单并 **覆盖绑定** | +| 新增/编辑 | 不传 / null | `""` | **清空** `RoleMenu` | +| 新增/编辑 | `[]` | 不传 | **清空** `RoleMenu` | +| 编辑 | 不传 | 不传 / null | **不修改** 已有菜单绑定 | + +- 解析 **忽略大小写**;支持英文逗号、分号分隔。 +- 若传了非空 `accessPermissions` 但 **0 条菜单匹配**,接口返回业务错误(提示检查 `PermissionCode` 或执行回填脚本)。 + +--- + +## 1 角色详情 + +| 项目 | 说明 | +|------|------| +| HTTP | `GET` | +| 路径 | `/api/app/rbac-role/{id}` | +| 示例 | `/api/app/rbac-role/3a1f077b-3665-63f2-5fea-0fd7e7044b88` | + +**响应 `data` 字段(节选)** + +| 字段 | 说明 | +|------|------| +| menuIds | 已绑定菜单 Guid 字符串数组(来自 `RoleMenu`) | +| accessPermissions | 已绑定菜单权限码汇总,如 `menu.account-management, menu.labels` | + +--- + +## 2 新增角色 + +| 项目 | 说明 | +|------|------| +| HTTP | `POST` | +| 路径 | `/api/app/rbac-role` | + +**请求示例(accessPermissions)** + +```json +{ + "roleName": "Partner Admin", + "roleCode": "admin", + "state": true, + "accessPermissions": "menu.labels, menu.label-categories, menu.account-management" +} +``` + +> 须先保证 `Menu` 表存在对应 **`PermissionCode`**(或执行回填脚本后使用 `menu.{router-segment}` 形式)。 + +--- + +## 3 编辑角色 + +| 项目 | 说明 | +|------|------| +| HTTP | `PUT` | +| 路径 | `/api/app/rbac-role/{id}` | +| Body | 与新增相同(`RbacRoleUpdateInputVo`) | + +--- + +## 数据库准备(必做) + +执行(按环境选择库): + +`美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/scripts/menu_backfill_permission_code.sql` + +回填后示例: + +| MenuName | Router | PermissionCode | +|----------|--------|----------------| +| Labels | /labels | menu.labels | +| Account Management | /account-management | menu.account-management | + +**验证 SQL** + +```sql +SELECT r.RoleName, m.MenuName, m.PermissionCode +FROM Role r +INNER JOIN RoleMenu rm ON rm.RoleId = r.Id +INNER JOIN Menu m ON m.Id = rm.MenuId AND m.IsDeleted = 0 +WHERE r.Id = '3a1f077b-3665-63f2-5fea-0fd7e7044b88'; +``` + +--- + +## 关联接口 + +| 接口 | 说明 | +|------|------| +| `POST /api/app/rbac-role-menu/set` | 仅维护 `RoleMenu`(`menuIds` 覆盖式),不直接写 `accessPermissions` 列 | +| `GET /api/app/rbac-role-menu/menu-ids/{roleId}` | 查询已绑定菜单 Id | + +保存菜单权限后,再调 **`GET /api/app/rbac-role/{id}`** 应能看到非空 **`accessPermissions`**(在 `PermissionCode` 已配置前提下)。 + +--- + +## 联调注意 + +| 现象 | 处理 | +|------|------| +| accessPermissions 仍为空 | 1)查 `RoleMenu` 是否有记录;2)查 `Menu.PermissionCode` 是否已回填 | +| 保存报未匹配到菜单 | `accessPermissions` 与库中 Code 不一致;先执行回填脚本或改用 `menuIds` | +| 同时传 `menuIds: []` 与 accessPermissions | **menuIds 优先**,空数组会清空绑定,accessPermissions 被忽略 | + +--- + +## 修订记录 + +| 日期 | 说明 | +|------|------| +| 2026-05-18 | 修复 rbac-role accessPermissions 读写;RoleMenu 绑定;Menu.PermissionCode 回填脚本 | +| 2026-05-18 | 修复 team-member 登录:去掉双重 BuildPassword;改密显式落库 Password+Salt;已建账号需 PUT 重置密码 | +| 2026-05-18 | label-type 列表 noOfLabels/region/location;新增编辑 regionIds+locationIds 多选;DDL `fl_label_type_scope.sql` | +| 2026-05-18 | label-multiple-option 列表筛选与出参;新增/编辑 regionIds+locationIds 多选;DDL `fl_label_multiple_option_scope.sql` | +| 2026-05-18 | reports print-log-list:Label ID 改为门店当日序号 yyyyMMdd-n(labelCode 字段) | +| 2026-05-18 | reports print-log-list:Expiry Date 从 PrintInputJson 模板快照 elements 解析(expiryDateText) | +| 2026-05-18 | reports label-report:管理员全量;非管理员按 userlocation 绑定门店统计(不按 CreatedBy) | +| 2026-05-18 | us-app-auth location-detail:出参 operatingHours 读 location.OperatingHours | +| 2026-05-18 | us-app get-print-log-list / get-label-report:管理员与 Partner 可看当前门店全部打印;reprint 同权 | +| 2026-05-18 | us-app labeling-tree:产品已绑门店时不再因产品分类 SPECIFIED 无 location 关联而空树 | +| 2026-05-18 | location 列表:管理员全部门店;非管理员仅绑定门店所属 Region(Partner+GroupName) | +| 2026-05-18 | team-member 列表:Query 增加 partnerId / groupId / locationId(Company·Region·Location)筛选 | +| 2026-05-18 | reports template-print-stat-list:按模板汇总打印标签数量(templateName + printedCount) | diff --git a/泰额版/Food Labeling Management Platform/src/components/ui/select.tsx b/泰额版/Food Labeling Management Platform/src/components/ui/select.tsx index bd1333f..a658d03 100755 --- a/泰额版/Food Labeling Management Platform/src/components/ui/select.tsx +++ b/泰额版/Food Labeling Management Platform/src/components/ui/select.tsx @@ -41,7 +41,7 @@ function SelectTrigger({ data-slot="select-trigger" data-size={size} className={cn( - "border-input data-[placeholder]:text-muted-foreground [&_svg:not([class*='text-'])]:text-muted-foreground focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive dark:bg-input/30 dark:hover:bg-input/50 flex w-full items-center justify-between gap-2 rounded-md border bg-input-background px-3 py-2 text-sm whitespace-nowrap transition-[color,box-shadow] outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50 data-[size=default]:h-9 data-[size=sm]:h-8 *:data-[slot=select-value]:line-clamp-1 *:data-[slot=select-value]:flex *:data-[slot=select-value]:items-center *:data-[slot=select-value]:gap-2 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4", + "border-input data-[placeholder]:text-muted-foreground [&_svg:not([class*='text-'])]:text-muted-foreground focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive dark:bg-input/30 dark:hover:bg-input/50 flex w-full items-center justify-between gap-2 overflow-hidden rounded-md border bg-input-background px-3 py-2 text-sm transition-[color,box-shadow] outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50 data-[size=default]:h-9 data-[size=sm]:h-8 *:data-[slot=select-value]:min-w-0 *:data-[slot=select-value]:flex-1 *:data-[slot=select-value]:truncate [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4", className, )} {...props} diff --git a/美国版/Food Labeling Management App UniApp/src/App.vue b/美国版/Food Labeling Management App UniApp/src/App.vue index c9b1e25..99d3ed3 100644 --- a/美国版/Food Labeling Management App UniApp/src/App.vue +++ b/美国版/Food Labeling Management App UniApp/src/App.vue @@ -1,8 +1,22 @@