5-18接口优化.md 49.6 KB

5-18 接口优化

本文档说明 2026-05-18 对美国版接口修复与约定,包括:

  1. /api/app/rbac-roleaccessPermissions 读写(见下文 rbac-role 章节)。
  2. /api/app/team-member:成员 平台端无法登录(已修复);列表支持 Company / Region / Location 筛选(见 team-member-list)。
  3. /api/app/label-type:列表 No. of Labels / Region / Location;新增/编辑 Region·Location 多选(见 label-type)。
  4. /api/app/label-multiple-option:列表 Region / Location 筛选与出参;新增/编辑 Region·Location 多选(见 label-multiple-option)。
  5. /api/app/reports/print-log-list:Print Log Label ID 当日门店序号 + Expiry Date 从打印快照解析(见 reports-print-log)。
  6. /api/app/reports/label-report:按 Token 门店绑定统计(管理员全量;见 reports-label-report)。
  7. /api/app/us-app-auth/location-detail/{locationId}:出参增加 经营时间 operatingHours(见 us-app-auth-location-detail)。
  8. App POST /api/app/us-app-labeling/get-print-log-listget-label-report:管理员 / Partner 角色可查看当前门店全部打印记录与统计(见 us-app-print-log)。
  9. GET /api/app/us-app-labeling/labeling-tree:修复产品分类 SPECIFIED 未配门店关联导致全店空树(见 us-app-labeling-tree)。
  10. GET /api/app/location:门店列表按 Token Region 数据范围过滤(见 location-list)。
  11. GET /api/app/reports/template-print-stat-list:按模板统计打印标签数量(见 reports-template-print-stat)。

reports-print-log 打印日志

应用服务ReportsAppService
接口GET /api/app/reports/print-log-list(及同源导出 export-print-log-pdf / export-print-log-excel
辅助类ReportsPrintLogDailyLabelIdHelperReportsPrintLogExpiryHelper

Label ID 变更说明

说明
Label ID 含义 fl_label.LabelCode(如 079)改为 门店当日打印序号
展示格式 {yyyyMMdd}-{n},例如 20260513-120260513-2
排序规则 LocationId(门店) + 自然日PrintedAt,无则 CreationTime)分组;组内按打印时间 升序,同秒按任务 Id 升序,n1 递增
跨页一致 分页列表与导出使用同一套序号(按门店全日任务计算,非仅当前页内排序)
API 字段名 仍为 labelCode(兼容前端列绑定),内容为当日序号,不是标签主数据编码

列表出参(节选)

字段 说明
taskId 打印任务 Id(fl_label_print_task.Id,重打用)
labelCode Label ID 列20260515-1 这种当日门店序号
productName / categoryName / templateText 不变
printedAt 打印时间
locationText / locationId 门店
expiryDateText Expiry Date 列:从 PrintInputJson 解析的保质期展示文案;无则「无」

示例

{
  "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 / inputKeydurationdateexpirydate 等;typeAddduration date;排除 currentdatecurrenttimeprepped 等制备日期字段
展示格式 与标签出纸一致(如 05/172026-05-17),不做二次换算
适用范围 分页列表、export-print-log-pdfexport-print-log-excel 共用同一解析逻辑

库内样例(节选)

PrintInputJson.elementselementName: "durationdate1"config.text: "05/17",接口应返回 expiryDateText: "05/17"(此前仅查根级字段,列恒为「无」)。

Label ID 计算逻辑(与代码一致)

同一门店、同一天内:

  1. 查询 fl_label_print_taskLocationId 相同且 DATE(COALESCE(PrintedAt, CreationTime)) 相同的全部任务;
  2. 按打印时间升序编号 1, 2, 3…
  3. 格式化为 yyyyMMdd-n

注意:序号统计范围为该门店当日全部打印任务(不限于当前列表筛选关键字),以保证同一天内序号全局唯一、连续。列表/导出的日期、门店筛选只决定哪些行展示,不改变已展示行的序号。

库内核对 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 使用相同筛选)
辅助类LocationRegionScopeHelperReportsRoleHelper

数据范围(按 Token)

角色 可见门店
管理员 全部未删除门店(ReportsRoleHelper.IsAdminRole
非管理员 userlocation 绑定门店所属 Region 下的全部门店

Region 判定:与 fl_group / group 列表一致——取绑定门店的 location.Partner(公司名称)+ location.GroupName(Region 名称)去重后,列表返回 同一 Partner + GroupName 的所有 location 行(不限于本人绑定的那几家店)。

未绑定门店、或绑定门店缺少 Partner/GroupName 时,列表为空。

请求示例

GET /api/app/location?SkipCount=1&MaxResultCount=10
Authorization: Bearer {token}

可选 Query:KeywordPartnerGroupNameStateSorting(在数据范围之上再收窄)。

与 group 列表的关系

接口 非管理员范围
GET /api/app/group 可见的 fl_group 记录
GET /api/app/location 上述 Region 对应的 location 门店

库内核对(非管理员)

-- 当前用户绑定门店所属的 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 是否有绑定;门店 PartnerGroupName 是否已填
只能看到部分 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=SPECIFIEDfl_product_category_location 配置当前门店时,旧逻辑在 Join 条件中整行过滤 已修复:四级树的产品范围仅由 fl_location_product + fl_label.LocationId 决定;产品分类、标签分类仅校验未删除且启用,不再用 SPECIFIED 子查询拦截
某门店仍为空 该门店 fl_labelLocationId 匹配且 State=1 属数据:需在 Web Labels 为该门店创建/复制标签,或把已有标签的 LocationId 指到该门店

数据范围(与代码一致)

  1. 门店产品fl_location_product.LocationId = locationId
  2. 门店标签fl_label.LocationId = locationId 且未删除、启用
  3. 关联fl_label_product 连接标签与产品
  4. 登录:须绑定该门店(userlocation),否则返回业务错误

请求示例

GET /api/app/us-app-labeling/labeling-tree?locationId=3a212211-3b01-d66f-a804-125c0cee3bf0
Authorization: Bearer {token}

出参

UsAppLabelCategoryTreeNodeDto[](L1 标签分类 → L2 产品分类 → L3 产品+模板卡片 → L4 标签类型)。

库内核对 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
辅助类UsAppPrintLogScopeHelperReportsRoleHelper

涉及接口

接口 说明
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 同上;判定为 UserRoleRoleRoleCodeRoleNamepartner(忽略大小写,如 Partner Admin)
其它(如 Staff、Store Manager) CreatedBy == 当前用户 Id

get-print-log-list

请求示例

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

请求示例

{
  "locationId": "3a21220f-db37-3e32-7390-d55f64cd62a8",
  "startDate": "2026-04-07",
  "endDate": "2026-05-18",
  "keyword": ""
}

出参ReportsLabelReportOutputDtosummarylabelsByCategoryprintVolumeTrendmostUsedProducts)。

与 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/RoleNamepartner,或是否为管理员
列表为空 确认 locationId 与绑定门店一致;Staff 仅能看到自己打印的记录

us-app-auth-location-detail 门店详情

应用服务UsAppAuthAppService
接口GET /api/app/us-app-auth/location-detail/{locationId}
鉴权:Bearer Token;仅可查当前用户在 userlocation 中绑定的门店。

变更说明

说明
新增出参 operatingHours(经营时间)
数据来源 location.OperatingHoursvarchar(512) 自由文本)
空值展示 未维护或空白时返回 (与 locationNamestorePhone 等字段一致)
维护入口 Web 管理端 POST/PUT /api/app/locationoperatingHours 字段

数据库(若列不存在须先执行)

脚本:美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/scripts/fl_location_add_operating_hours_column.sql

ALTER TABLE `location`
  ADD COLUMN `OperatingHours` varchar(512) DEFAULT NULL COMMENT '经营时间(自由文本)' AFTER `Longitude`;

请求示例

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 同上用户电话

响应示例

{
  "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
辅助类ReportsLocationScopeHelperReportsRoleHelper

功能

fl_label_template 汇总 fl_label_print_task 行数,返回 模板名称 + 打印标签数量 分页列表(默认按 printedCount 降序)。

数据范围(与 label-report 一致)

角色 统计范围
管理员 全部门店打印任务(可按 Company/Region/Location 入参收窄)
非管理员 userlocation 绑定门店内全部打印任务(不按 CreatedBy 过滤)

入参

参数 说明
SkipCount / MaxResultCount 分页(SkipCount1-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

请求示例

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 该模板下打印任务条数

分页包装pageIndexpageSizetotalCounttotalPagesitems(与其它列表一致)。

响应示例

{
  "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 }
  ]
}

库内核对

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
辅助类ReportsLocationScopeHelperReportsRoleHelper

数据范围(按 Token)

角色 统计范围 说明
管理员 全部门店打印任务 识别方式与 Print Log 一致:admin 角色 / 用户名为 admin / 权限 *:*:*
非管理员 userlocation 绑定门店 UserLocation.UserId = 当前用户 IdLocationId;统计该门店下全部打印任务,不按 CreatedBy 过滤
参数 说明
StartDate / EndDate 统计区间(含起日、含止日;未传时默认近 30 天至今天)
PartnerId Company(fl_partner.Id
GroupId Region(fl_group.Id
LocationId 门店(location.Id
Keyword 产品名 / 标签分类 / 产品分类模糊匹配

筛选叠加规则

  • 管理员:未传 Company/Region/Location → 不限制门店;传入后与对应门店集合取交集。
  • 非管理员:始终在绑定门店集合内统计;若再传 PartnerId / GroupId / LocationId,与绑定门店 取交集(传了未绑定门店 → 空数据)。
  • 无绑定门店:返回空统计(各指标为 0 / 空列表)。

示例

GET /api/app/reports/label-report?StartDate=2026-04-07&EndDate=2026-05-18
Authorization: Bearer {token}

出参结构(不变)

字段
summary totalLabelsPrintedtotalLabelsPrintedPrevPeriodmostPrintedCategoryNametopProductNameavgDailyPrints
labelsByCategory 按标签分类汇总
printVolumeTrend 近 7 日(在查询区间内)每日打印量
mostUsedProducts Top 20 产品
模块 非管理员范围
label-report 绑定门店内所有打印记录
print-log-list 仍仅 CreatedBy = 当前用户(见 报表Reports接口对接说明.md

库内核对(非管理员)

-- 当前用户绑定的门店
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、列表筛选 locationIdlocation.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

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_locationSPECIFIED 时至少 1 个有效门店。

请求示例

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 匹配 optionCodeoptionName
state bool 启用状态
groupId string Region 筛选(fl_group.Id
locationId string Location 筛选;优先于 groupId

筛选语义(与 label-category / label-type 一致):

  • 未传 groupIdlocationId:返回全部未删除多选项。
  • 传入筛选:返回 availabilityType = ALLfl_label_multiple_option_location 命中该门店 的记录。

请求示例

GET /api/app/label-multiple-option?SkipCount=1&MaxResultCount=10
Authorization: {token}
GET /api/app/label-multiple-option?SkipCount=1&MaxResultCount=10&groupId=你的fl_group主键
Authorization: {token}
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 列表列 RegionALLAll RegionsSPECIFIED → 绑定门店 GroupName 拼接
location string 列表列 LocationALLAll LocationsSPECIFIED → 门店名拼接
regionIds string[] Region Id(SPECIFIED 时由门店反推)
locationIds string[] 门店 Id
lastEdited datetime 最近编辑时间

响应示例

{
  "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

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 / groupIdsfl_group.Id);UI Location = locationIdslocation.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

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_locationSPECIFIED 时至少 1 个有效门店,否则报错:指定适用区域或门店时,至少需要匹配到一个有效门店

请求示例

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.IdIsDeleted = 0
Region region string ALLAll RegionsSPECIFIED → 绑定门店 GroupName 拼接;未配置为
Location location string ALLAll LocationsSPECIFIED → 门店名拼接
(辅助) 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

统计逻辑(与代码一致)

-- 某类型下标签数(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 出参增加 noOfLabelsregionlocationregionIdslocationIdslastEdited
列表筛选 Query 可选 groupId(Region)、locationId(Location);locationId 优先于 groupId
统计一致性 传 Region/门店筛选时,noOfLabelsregion/location 仅统计筛选范围内标签
筛选语义 groupId/locationId 时:返回 availabilityType=ALL关联门店命中 的类型(与 label-category 一致)
列表 region/location 来自 配置AvailabilityType + fl_label_type_location),非仅标签反推

命名约定:UI Region = Query groupIdfl_group.Id);UI Location = Query locationIdlocation.Id)。

列表 Query

字段 类型 必填 说明
skipCount int 跳过条数(项目约定:第 1 页常为 1
maxResultCount int 每页条数
sorting string 排序,如 orderNum desc
keyword string 匹配 typeCodetypeName
state bool 启用状态
groupId string Region 筛选(fl_group.Id
locationId string Location 筛选(location.Id);优先于 groupId

请求示例(默认列表,含三列出参)

GET /api/app/label-type?SkipCount=1&MaxResultCount=10
Authorization: {token}

按 Region / Location 筛选

GET /api/app/label-type?SkipCount=1&MaxResultCount=10&groupId=你的fl_group主键
Authorization: {token}
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[] 示例

{
  "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 列(不应加列)

SELECT COLUMN_NAME
FROM information_schema.COLUMNS
WHERE TABLE_SCHEMA = DATABASE()
  AND TABLE_NAME = 'fl_label_type'
ORDER BY ORDINAL_POSITION;

2)按类型统计标签数(与接口 noOfLabels 一致)

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 一致)

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 后列表变空 ALLfl_label_type_location 未覆盖该门店
未执行 DDL 先跑 fl_label_type_scope.sql
noOfLabels 与标签列表不一致 对比时 label 列表需传与 label-type 相同的 groupId/locationId
前端列空白 绑定 noOfLabelsregionlocation(勿用 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 在传入范围参数时仅展示该范围内的绑定门店。

请求示例

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}

可与 KeywordRoleIdStateSorting 组合使用。

库内核对

-- 某 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.comuserName=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.comadminPassword 相同、Salt 不同)。

代码修复

  • 新增:与 UserDataSeed 一致,EncryPassword = new EncryPasswordValueObject(明文)只调用一次 BuildPassword()
  • 改密 / 重置密码:改密后调用 UserPasswordHelper.EnsurePasswordColumnsPersistedAsync显式 SET PasswordSaltYi.Framework.Rbac.Domain/Helpers/UserPasswordHelper.cs)。
  • 登录校验统一走 UserPasswordHelper.VerifyPlainPassword / UserAggregateRoot.JudgePassword

平台登录约定

项目 说明
接口 POST /api/app/account/loginAccountService.PostLoginAsync
入参 userName邮箱(平台 UI 的 Email 框);password 为明文
匹配 User.Email(忽略大小写)优先;否则 User.UserName 与邮箱相同也可
状态 State = true 且未删除才可登录

登录示例

POST /api/app/account/login
{
  "userName": "123@qq.com",
  "password": "创建时设置的明文密码"
}

已受影响账号的处理

部署修复后,已受影响账号(如 123@qq.com,库内 Password/Salt 不一致)必须 再保存一次密码

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
}

保存后用 邮箱 + 新密码 登录。

库内核对

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)

数据链路(正确)

accessPermissions(入参/出参字符串)
    ↔ Menu.PermissionCode(权限码,可为空时按 Router 推导 menu.xxx)
    ↔ RoleMenu(RoleId + MenuId)
    ↔ Role

变更摘要

说明
绑定写入 accessPermissions 解析为 MenuId覆盖写入 RoleMenu;解析不到任何菜单时 返回明确错误
出参汇总 GET 列表/详情从 RoleMenu + Menu.PermissionCode 汇总;PermissionCode 为空时按 Router 推导(与回填 SQL 一致)
详情 menuIds RoleMenu 读取,不再查错误的 rolemenu 映射
RoleMenu 主键 插入时生成 IdRoleMenu 表必填)
数据库脚本 scripts/menu_backfill_permission_code.sql 批量回填 PermissionCode

公共约定

  • 宿主Yi.Abp.Web;路由前缀 api/app
  • 应用服务RbacRoleAppService(模块 food-labeling-us)。
  • 鉴权Authorization: {token}

accessPermissions 规则

出参(只读)

  • 查询 RoleMenu 中该角色绑定的菜单,取 Menu.PermissionCode(非空优先)。
  • PermissionCode 为空,按 Router 推导:/labelsmenu.labels
  • 去重后按字母序用 , 拼接;无绑定为 ""

入参(写入)

场景 menuIds accessPermissions 行为
新增/编辑 非空数组 任意 以 menuIds 为准(覆盖 RoleMenu
新增/编辑 不传 / null 非空字符串 按 PermissionCode 解析菜单并 覆盖绑定
新增/编辑 不传 / null "" 清空 RoleMenu
新增/编辑 [] 不传 清空 RoleMenu
编辑 不传 不传 / null 不修改 已有菜单绑定
  • 解析 忽略大小写;支持英文逗号、分号分隔。
  • 若传了非空 accessPermissions0 条菜单匹配,接口返回业务错误(提示检查 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)

{
  "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

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 仅维护 RoleMenumenuIds 覆盖式),不直接写 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)