6-1代码优化.md 20.5 KB

6-1 代码优化

本文档说明 2026-06-01 对美国版 auth-session/my-menuslastUpdated 字段的语义与实现变更。


my-menus 的 lastUpdated(系统编辑更新时间)

背景

前端(AuthProvider)用 GET /api/app/auth-session/my-menus 返回的 lastUpdated 判断菜单/权限缓存是否需要刷新。原先取 User.LastModificationTime,仅在用户资料变更时变化,菜单、角色、业务数据 等任意接口修改后不会更新。

目标行为

  • lastUpdated 表示 系统编辑更新时间
  • 宿主下任意 /api/app写操作POST / PUT / PATCH / DELETE)且 HTTP 2xx 成功 后,刷新全局时间戳(分布式缓存)。
  • my-menus 返回:max(系统编辑时间戳, User.LastModificationTime);若从未有过写操作,则仅回退用户资料时间。

接口

说明
方法 GET
路径 /api/app/auth-session/my-menus
鉴权 Web 登录 Bearer Token
测试环境示例 http://flus-test.3ffoodsafety.com/api/app/auth-session/my-menus

请求示例

GET /api/app/auth-session/my-menus HTTP/1.1
Host: flus-test.3ffoodsafety.com
Authorization: Bearer {access_token}

响应字段(与本次变更相关)

字段 类型 说明
lastUpdated DateTime? 系统编辑全局时间戳;与任意符合条件的写接口成功联动。与用户 LastModificationTime较晚 者。
menus 数组 可见菜单树
permissionCodes 字符串数组 权限码
role / fullName 字符串 角色展示名、全名

响应片段示例

{
  "user": {
    "id": "…",
    "userName": "admin"
  },
  "lastUpdated": "2026-06-01T14:32:10.123",
  "menus": [],
  "permissionCodes": [],
  "role": "Administrator",
  "fullName": "Admin"
}

写操作如何刷新时间戳

实现类:SystemEditStampGlobalFilter(注册于 FoodLabelingApplicationModule)。

条件 是否刷新
路径以 /api/app 开头 是(含 RBAC、美国版 food-labeling-us、同宿主泰额版等)
方法为 POST / PUT / PATCH / DELETE
响应状态码 2xx
路径含 /login/logoutforgot-password/captcha/my-menusget-label-report (登录、退出、验证码、纯查询报表等)

缓存键:FoodLabeling:SystemEditStampSystemEditStampCacheHelper)。

验证步骤(建议)

  1. 调用 my-menus,记录 lastUpdatedT0
  2. 调用任意会改数据的写接口(如新增/修改产品、用户、菜单、选店 auth-scope/select-location 等),确认 2xx
  3. 再次调用 my-menuslastUpdated 应为 T1,且 T1 > T0(秒级即可)。
  4. 仅调用 get-label-report 等排除路径时,lastUpdated 不应 变化。

涉及文件

文件 说明
FoodLabeling.Application/Filters/SystemEditStampGlobalFilter.cs 写成功后 Touch 时间戳
FoodLabeling.Application/Helpers/SystemEditStampCache.cs 缓存读写
FoodLabeling.Application/Services/AuthSessionAppService.cs GetMyMenusAsync 组装 lastUpdated
FoodLabeling.Application.Contracts/.../CurrentUserMenuPermissionsOutputDto.cs DTO 注释
FoodLabeling.Application.Contracts/IServices/IAuthSessionAppService.cs 接口 XML 注释

前端说明

Food Labeling Management Platform/src/components/auth/AuthProvider.tsx 已支持从响应根级或 user 内读取 lastUpdated无需改前端即可使用新语义;建议在本地对比 lastUpdated 与上次缓存值,变化时重新拉取菜单。


team-member 列表按 Token 权限过滤

背景

GET /api/app/team-member 原先仅按 Query 的 partnerId / groupId / locationId 筛选,结合登录 Token 的数据范围,非管理员可能看到其它公司的成员。

目标行为

登录身份 列表可见成员
系统管理员ReportsRoleHelper.IsAdminRole:用户名 admin、角色码 admin、权限 *:*:* 等) 全部 用户(仍可用 Query 按 Company / Region / Location 收窄)
其它账号 仅在 userlocation 至少绑定一家门店,且该门店属于当前账号 绑定门店所属公司fl_partner,见 PartnerScopeHelper)下的成员

GET /api/app/partner 公司列表、auth-scope 级联选店使用的公司范围规则一致。

接口

说明
方法 GET
路径 /api/app/team-member
鉴权 Web 登录 Bearer Token
测试环境示例 http://flus-test.3ffoodsafety.com/api/app/team-member?SkipCount=1&MaxResultCount=10
PDF 导出 GET /api/app/team-member/export-team-members-pdf相同 Token 范围 + Query 筛选)

Query 参数

参数 说明
SkipCount 跳过条数(分页)
MaxResultCount 每页条数
Keyword 姓名 / 用户名 / 邮箱 / 电话模糊搜索
RoleId 角色 Id
State 启用状态
partnerId Company(fl_partner.Id);非管理员须在本人可见公司内
groupId Region(fl_group.Id
locationId 门店 Id;传则优先于 partnerId / groupId
Sorting 排序(可选)

筛选优先级(Query 内):locationIdgroupIdpartnerId;均未传时,管理员不限制组织范围,非管理员限定为其可见公司下全部门店。

命中规则:成员须在 userlocation 中至少绑定一家落在「最终门店集合」内的门店。

请求示例

GET /api/app/team-member?SkipCount=1&MaxResultCount=10 HTTP/1.1
Host: flus-test.3ffoodsafety.com
Authorization: Bearer {access_token}

非管理员带 Company 筛选(须在本人可见公司内):

GET /api/app/team-member?SkipCount=1&MaxResultCount=10&partnerId={fl_partner.Id}
Authorization: Bearer {partner_admin_token}

验证步骤(建议)

  1. 使用 系统管理员 Token 调用列表 → 应返回全库成员(或受 Query 筛选)。
  2. 使用 仅绑定 A 公司门店 的 Partner/公司管理员 Token → 仅返回在 A 公司门店有 userlocation 绑定的成员;不应出现仅绑定 B 公司的成员。
  3. 非管理员传 其它公司partnerId → 空列表(非 403)。
  4. export-team-members-pdf 与列表使用同一套范围,条数应与列表筛选一致。

库内核对(非管理员 · 某公司)

-- 替换 :partnerId 为 fl_partner.Id,:partnerName 为 PartnerName
SELECT DISTINCT u.Id, u.UserName, u.Name
FROM `User` u
INNER JOIN userlocation ul ON ul.UserId = CAST(u.Id AS CHAR) AND ul.IsDeleted = 0
INNER JOIN location loc ON ul.LocationId = CAST(loc.Id AS CHAR) AND loc.IsDeleted = 0
INNER JOIN fl_partner p ON p.IsDeleted = 0 AND (loc.Partner = p.PartnerName OR loc.Partner = p.Id)
WHERE u.IsDeleted = 0
  AND p.Id = :partnerId;

涉及文件

文件 说明
FoodLabeling.Application/Helpers/TeamMemberListScopeHelper.cs Token 范围 + Query 交集
FoodLabeling.Application/Helpers/PartnerScopeHelper.cs 公司可见范围(复用)
FoodLabeling.Application/Services/TeamMemberAppService.cs GetListAsync、PDF 导出
FoodLabeling.Application.Contracts/.../TeamMemberGetListInputVo.cs 入参注释
FoodLabeling.Application.Contracts/IServices/ITeamMemberAppService.cs 接口 XML 注释

联调注意

现象 处理
非管理员列表为空 确认当前账号 userlocation 是否已绑定门店,且门店 Partner 能对应到 fl_partner
成员无门店绑定 不会出现在非管理员列表中(无 userlocation 命中)
与 5-18 文档关系 5-18 描述 Query 筛选字段;本节补充 Token 权限范围,以本节为准

product 下载导入模板 405 修复

问题现象

GET|POST http://flus-test.3ffoodsafety.com/api/app/product/download-product-import-template
Authorization: Bearer {token}

使用 POST 调用时返回 405 Method Not Allowed(后端已限定为 GET;旧版 Web 用 authorizedPostBlobDownload 发 POST)。

根因

说明
后端 DownloadProductImportTemplateAsync 标注 [HttpGet](ABP 对 Download* 也可能误判动词,需显式路由)
前端(修复前) productService.downloadProductImportTemplate 使用 POST 下载 Blob
约定 export-products-excel、Location/批量文档一致:下载模板 / 导出 均为 GET,无 Body

修复方式

  1. 后端 ProductAppService:增加显式路径,并 同时接受 GET 与 POST(兼容未刷新前端):
    • [HttpGet("product/download-product-import-template")]
    • [HttpPost("product/download-product-import-template")]
  2. 前端 productService.ts:改为 authorizedGetBlobDownload(GET)。

接口说明

说明
方法 GET(推荐);POST(兼容,无 Body)
路径 /api/app/product/download-product-import-template
鉴权 Web 登录 Bearer Token
响应 Content-Type: application/vnd.openxmlformats-officedocument.spreadsheetml.sheet;文件名来自配置 FoodLabeling:BatchImport:ProductTemplateFileName(默认 Product-Manager-批量导入模板.xlsx
配置 TemplateDirectory + ProductTemplateFileName(见 批量导入导出接口说明.md §3.1)

请求示例

GET /api/app/product/download-product-import-template HTTP/1.1
Host: flus-test.3ffoodsafety.com
Authorization: Bearer {access_token}
curl -X GET "http://flus-test.3ffoodsafety.com/api/app/product/download-product-import-template" \
  -H "Authorization: Bearer {token}" \
  -o "Product-Manager-template.xlsx"

验证步骤

  1. 浏览器或 Postman 使用 GET + Token → 应下载 xlsx(非 405)。
  2. 仍用 POST(无 Body)→ 部署新后端后亦应成功(兼容)。
  3. 若返回业务错误「模板文件不存在」→ 检查服务器 TemplateDirectory 下是否存在配置的 xlsx。

涉及文件

文件 说明
FoodLabeling.Application/Services/ProductAppService.cs 显式 GET/POST 路由
FoodLabeling.Application.Contracts/IServices/IProductAppService.cs XML 注释
Food Labeling Management Platform/src/services/productService.ts 改为 GET 下载

联调注意

现象 处理
仍 405 确认已部署含本修复的后端;前端须 GET 或等新后端支持 POST
404 路径须为 download-product-import-template,勿当成 GET /product/{id}
与导出区别 全量导出为 GET .../export-products-excel?Keyword=...;模板下载无 Query

dashboard/overview 首页统计口径修复

问题现象

GET /api/app/dashboard/overview 返回的 Active UsersLocationsPeople 与业务实际不符;公司维度账号可能看到全库用户数/门店数,且系统 admin 被计入某公司成员统计。

测试环境示例:http://flus-test.3ffoodsafety.com/api/app/dashboard/overview

目标行为

指标 统计口径
Locations 范围内未删除门店数:管理员=全平台;其它账号=其绑定门店所属公司下全部门店
People 范围内团队成员:在 userlocation 中至少绑定一家范围内门店的用户(User.IsDeleted=false),排除系统 admin
Active Users 同上,且 User.State=true
系统 admin 用户名 admin 或角色码 admin 的账号不计入 People / Active Users(不归属任何 Company)

打印类指标(Labels Printed TodayWeekly Print VolumeRecent Labels)与 Products 亦按同一门店范围过滤;Active TemplatesBy Category 仍为全平台口径(与改前一致)。

接口

说明
方法 GET
路径 /api/app/dashboard/overview
鉴权 Web 登录 Bearer Token
请求参数 无(范围由 Token 解析,与 team-member 列表、partner 列表一致)

请求示例

GET /api/app/dashboard/overview HTTP/1.1
Host: flus-test.3ffoodsafety.com
Authorization: Bearer {access_token}

响应字段(本次相关)

字段 说明
activeUsers 启用状态的范围内团队成员数(不含系统 admin)
locations 范围内门店数
people 范围内团队成员总数(含禁用,不含系统 admin)
labelsPrintedToday 范围内门店当日打印任务数
weeklyPrintVolume 近 7 天范围内打印量
recentLabels 范围内门店最近 10 条打印记录

各指标对象含 valuepreviousValuechangeValuechangeRate(周对比基准:最近 7 天起始日前创建的存量等,见实现)。

验证步骤(建议)

  1. 系统管理员 Token:Locations / People 为全平台(People 不含 admin 账号);与库内 locationuserlocation 汇总大致一致。
  2. 仅绑定 A 公司门店 的账号:三项指标仅反映 A 公司范围;不应等于全库总数。
  3. 确认 admin 用户不在 People / Active Users 中(即使其有 userlocation 绑定)。
  4. userlocation 的普通用户:不计入 People / Active Users。

库内核对(某公司团队成员,不含 admin)

SELECT COUNT(DISTINCT ul.UserId) AS people_count
FROM userlocation ul
INNER JOIN location loc ON ul.LocationId = CAST(loc.Id AS CHAR) AND loc.IsDeleted = 0
INNER JOIN fl_partner p ON p.IsDeleted = 0 AND (loc.Partner = p.PartnerName OR loc.Partner = p.Id)
INNER JOIN `User` u ON CAST(u.Id AS CHAR) = ul.UserId AND u.IsDeleted = 0
WHERE ul.IsDeleted = 0
  AND p.Id = :partnerId
  AND u.UserName <> 'admin'
  AND u.Id NOT IN (
    SELECT ur.UserId FROM UserRole ur
    INNER JOIN Role r ON r.Id = ur.RoleId AND r.IsDeleted = 0 AND r.RoleCode = 'admin'
  );

涉及文件

文件 说明
FoodLabeling.Application/Helpers/DashboardScopeHelper.cs 范围解析、admin 排除、计数
FoodLabeling.Application/Helpers/TeamMemberListScopeHelper.cs 门店范围(复用)
FoodLabeling.Application/Services/DashboardAppService.cs GetOverviewAsync
FoodLabeling.Application.Contracts/IServices/IDashboardAppService.cs XML 注释

联调注意

现象 处理
公司账号统计仍为 0 检查该账号 userlocation 与门店 Partner 能否关联到 fl_partner
admin 仍出现在 People 确认用户名/角色码是否为 admin;部署须含本修复
Dashboard统计接口对接说明.md 该文档为通用结构说明;统计口径以本节为准

标签 /api/app/label 适用 Region(新增/编辑)

背景

标签原先仅通过 locationId 绑定单门店,无法显式表达「适用哪些 Region」。现与 label-typelabel-multiple-option 一致,支持 全选(ALL)单选/多选 Region(SPECIFIED + regionIds,Region 主键为 fl_group.Id

存储

位置 字段 说明
fl_label AppliedRegionType ALL = 全平台 Region;SPECIFIED = 按关联表
fl_label_region LabelId + GroupId 多选 Region 明细(GroupId = fl_group.Id
fl_label LocationId 仍保留:App 打印/树展示兼容;多 Region 且未显式传门店时,取合并门店列表第一家

DDL(测试/生产须先执行)module/food-labeling-us/scripts/fl_label_region_scope.sql

接口一览

方法 路径 说明
GET /api/app/label?SkipCount=&MaxResultCount= 分页列表;支持 GroupId 按 Region 筛选;出参含 regionregionIdsappliedRegionType
GET /api/app/label/{id} 详情(id = LabelCode
POST /api/app/label 新增
PUT /api/app/label/{id} 编辑

测试环境示例列表:http://flus-test.3ffoodsafety.com/api/app/label?SkipCount=1&MaxResultCount=10

请求 Body(新增 / 编辑,JSON)

字段 类型 必填 说明
appliedRegionType string ALLSPECIFIED。传了非空 regionIds/groupIds 时按 SPECIFIED 落库
regionIds string[] 条件 Region Id 数组(fl_group.Id);与 groupIds 合并去重
groupIds string[] regionIds 等价别名
locationId string 条件 门店 Id;SPECIFIED 时用于校验是否在 Region 范围内;ALL 时可单独指定
locationIds string[] 门店候选多选(与 Region 合并解析)
labelName string 标签名称
templateCode string 模板编码
labelCategoryId string 标签类别
labelTypeId string 标签类型
productIds string[] 至少 1 个产品 Id
labelInfoJson object 模板填写数据
state bool 默认 true

三种适用范围示例

1. 全选 Region(ALL)

{
  "labelName": "Demo Label",
  "templateCode": "TPL001",
  "appliedRegionType": "ALL",
  "regionIds": [],
  "labelCategoryId": "...",
  "labelTypeId": "...",
  "productIds": ["..."],
  "locationId": "可选-指定展示/打印默认门店"
}

2. 单选 Region

{
  "appliedRegionType": "SPECIFIED",
  "regionIds": ["1234567890123456789"],
  "locationId": "门店Id-须在Region内",
  "labelName": "...",
  "templateCode": "...",
  "labelCategoryId": "...",
  "labelTypeId": "...",
  "productIds": ["..."]
}

3. 多选 Region

{
  "regionIds": ["regionId1", "regionId2"],
  "groupIds": ["regionId2"],
  "labelName": "...",
  "templateCode": "...",
  "labelCategoryId": "...",
  "labelTypeId": "...",
  "productIds": ["..."]
}

未传 locationId 且合并 Region 对应多家门店时,后端取第一家写入 fl_label.LocationId;App 侧 labeling-tree / 预览打印通过 Region 关联判断是否适用该门店。

响应字段(详情 / 列表相关)

字段 说明
appliedRegionType ALL / SPECIFIED
regionIds Region Id 数组(ALL 时为空)
groupIds regionIds 相同
region 展示名:ALL 时为 All Regions;SPECIFIED 时为 Region 名称逗号拼接
locationId 当前绑定门店

列表筛选

查询参数 说明
GroupId 按 Region(fl_group.Id)过滤:命中 fl_label_region,或 AppliedRegionType=ALL 的标签
LocationId 按门店(原有)
Keyword 与其它筛选组合

验证步骤(建议)

  1. 执行 fl_label_region_scope.sql 后部署后端。
  2. POST 新增:仅 regionIds 单选 → 查 fl_label_region 一行、AppliedRegionType=SPECIFIED
  3. POST appliedRegionType=ALLfl_label_region 无行、AppliedRegionType=ALL
  4. PUT 编辑:改为多选 regionIds → 关联表行数与 Id 一致(先删后插)。
  5. GET 列表/详情:出参 regionregionIds 与库一致。
  6. App:/api/app/us-app-labeling/labeling-tree 仅展示对当前门店 Region 命中的标签(ALL 或关联 Region)。

库内核对 SQL

-- 标签 Region 范围
SELECT l.Id, l.LabelCode, l.AppliedRegionType, l.LocationId
FROM fl_label l
WHERE l.IsDeleted = 0 AND l.LabelCode = :labelCode;

SELECT lr.GroupId, g.GroupName
FROM fl_label_region lr
INNER JOIN fl_group g ON g.Id = lr.GroupId AND g.IsDeleted = 0
WHERE lr.LabelId = :labelId;

涉及文件

文件 说明
scripts/fl_label_region_scope.sql DDL
Helpers/LabelRegionScopeHelper.cs 保存/展示/列表筛选/App 校验
Services/LabelAppService.cs CRUD、列表出参
Services/UsAppLabelingAppService.cs App 树与打印范围
Contracts/Dtos/Label/* 入参/出参
Contracts/IServices/ILabelAppService.cs XML 注释
Web labelService.ts / types/label.ts 前端 API 映射
Web LabelsList.tsx 创建/编辑提交 appliedRegionType + regionIds

联调注意

现象 处理
保存报「请选择适用 Region」 SPECIFIED 须传 regionIdslocationId
列表无 region 确认已执行 DDL 并部署含 LabelRegionScopeHelper 的版本
编辑后 Region 未变 检查 Body 是否带 regionIds;Web 创建/编辑已随 Region 选择器提交