6-1 代码优化
本文档说明 2026-06-01 对美国版 auth-session/my-menus 中 lastUpdated 字段的语义与实现变更。
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、/logout、forgot-password、/captcha、/my-menus、get-label-report |
否(登录、退出、验证码、纯查询报表等) |
缓存键:FoodLabeling:SystemEditStamp(SystemEditStampCacheHelper)。
验证步骤(建议)
- 调用
my-menus,记录lastUpdated为 T0。 - 调用任意会改数据的写接口(如新增/修改产品、用户、菜单、选店
auth-scope/select-location等),确认 2xx。 - 再次调用
my-menus,lastUpdated应为 T1,且 T1 > T0(秒级即可)。 - 仅调用
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 内):locationId → groupId → partnerId;均未传时,管理员不限制组织范围,非管理员限定为其可见公司下全部门店。
命中规则:成员须在 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}
验证步骤(建议)
- 使用 系统管理员 Token 调用列表 → 应返回全库成员(或受 Query 筛选)。
- 使用 仅绑定 A 公司门店 的 Partner/公司管理员 Token → 仅返回在 A 公司门店有
userlocation绑定的成员;不应出现仅绑定 B 公司的成员。 - 非管理员传 其它公司 的
partnerId→ 空列表(非 403)。 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 |
修复方式
- 后端
ProductAppService:增加显式路径,并 同时接受 GET 与 POST(兼容未刷新前端):[HttpGet("product/download-product-import-template")][HttpPost("product/download-product-import-template")]
- 前端
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"
验证步骤
- 浏览器或 Postman 使用 GET + Token → 应下载 xlsx(非 405)。
- 仍用 POST(无 Body)→ 部署新后端后亦应成功(兼容)。
- 若返回业务错误「模板文件不存在」→ 检查服务器
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 Users、Locations、People 与业务实际不符;公司维度账号可能看到全库用户数/门店数,且系统 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 Today、Weekly Print Volume、Recent Labels)与 Products 亦按同一门店范围过滤;Active Templates、By 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 条打印记录 |
各指标对象含 value、previousValue、changeValue、changeRate(周对比基准:最近 7 天起始日前创建的存量等,见实现)。
验证步骤(建议)
- 系统管理员 Token:Locations / People 为全平台(People 不含
admin账号);与库内location、userlocation汇总大致一致。 - 仅绑定 A 公司门店 的账号:三项指标仅反映 A 公司范围;不应等于全库总数。
- 确认
admin用户不在 People / Active Users 中(即使其有userlocation绑定)。 - 无
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-type、label-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 筛选;出参含 region、regionIds、appliedRegionType |
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 | 否 | ALL 或 SPECIFIED。传了非空 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 等 |
与其它筛选组合 |
验证步骤(建议)
- 执行
fl_label_region_scope.sql后部署后端。 - POST 新增:仅
regionIds单选 → 查fl_label_region一行、AppliedRegionType=SPECIFIED。 - POST
appliedRegionType=ALL→fl_label_region无行、AppliedRegionType=ALL。 - PUT 编辑:改为多选
regionIds→ 关联表行数与 Id 一致(先删后插)。 - GET 列表/详情:出参
region、regionIds与库一致。 - 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 须传 regionIds 或 locationId |
列表无 region 列 |
确认已执行 DDL 并部署含 LabelRegionScopeHelper 的版本 |
| 编辑后 Region 未变 | 检查 Body 是否带 regionIds;Web 创建/编辑已随 Region 选择器提交 |