6-30 代码优化
本文档说明 2026-06-30 对美国版接口的两项优化:
- Company Admin 在其绑定 Company 下新建 Region 时,自动将新 Region 绑定到当前账号(写入
userlocation)。 - Product / Label Type 分页列表增加
locationName(绑定门店名称)出参。 - App Preview / Print Log / Label Report:
labelId统一为yyyyMMdd-n;Print Log 与 Label Report 按门店展示全部用户打印记录。
一、需求背景
| 项目 | 说明 |
|---|---|
| 角色 | Company Admin(库内 RoleCode = CompanyAdmin,RoleName = Company Admin) |
| 场景 | 在 Account Management → Region 页签,于已绑定 Company 下新增 Region |
| 期望 | 新建 Region 后,当前账号应默认绑定该 Region,无需再手动维护成员门店范围 |
| 关联接口 | GET/POST /api/app/group |
为何需要写 userlocation
系统成员与数据范围以 userlocation(用户 ↔ 门店) 为主键路径:
- Region 列表(
GET /api/app/group):Company Admin 已按fl_group.PartnerId展开可见该公司下全部 Region(见6-18代码优化.md第十五节)。 - 门店列表 / auth-scope / 打印范围 等:仍按
userlocation反推location.Partner + location.GroupName判断 Region 可见性。
因此仅写入 fl_group 不足以让新 Region 在「按门店绑定推导」的链路中立即可用;需在新建 Region 时追加该 Region 下门店的 userlocation 绑定。
二、行为说明
1. 触发条件(同时满足)
| # | 条件 |
|---|---|
| 1 | 当前用户角色为 Company Admin(TeamMemberRoleHelper.IsCompanyAdminUserAsync) |
| 2 | 非平台管理员(admin 不自动改 userlocation) |
| 3 | 新建 Region 的 partnerId 属于当前账号已绑定 Company(由 userlocation 反推 fl_partner.Id) |
| 4 | 接口已通过 EnsurePartnerIdAllowedForRegionMutationAsync 权限校验 |
2. 绑定规则
| 步骤 | 动作 |
|---|---|
| 1 | POST /api/app/group 成功插入 fl_group |
| 2 | 按 partnerId 查 fl_partner.PartnerName,与新建 groupName 匹配 location 表:Partner = PartnerName 且 GroupName = groupName |
| 3 | 将匹配到的门店 Id 追加写入 userlocation(不删除已有绑定;已绑门店幂等跳过) |
3. 空 Region(尚无门店)
新建 Region 时若该公司下尚无同名 GroupName 的门店,则本次不会新增 userlocation 行(无门店可绑)。
补充:Company Admin 随后通过 POST /api/app/location 在同一 Company / Region 下新建首家门店时,后端会自动将该门店追加绑定到当前账号(与 Region 新建逻辑共用 LocationScopeBindingHelper)。
4. 不受影响
| 角色 / 场景 | 说明 |
|---|---|
平台 admin |
不写入 userlocation |
| Regional Admin / Location Manager 等 | 不自动扩绑;仍按 Team Member 维护范围 |
PUT /api/app/group 编辑 Region |
不触发自动绑定 |
| 在Snapshot 已有绑定 | 只追加缺失门店,不覆盖、不删除 |
三、接口对接
1. Region 分页列表
| 项目 | 内容 |
|---|---|
| 方法 | GET |
| 路径 | /api/app/group |
Query 参数
| 参数 | 类型 | 必填 | 说明 |
|---|---|---|---|
SkipCount |
int | 是 | 页码,从 1 起 |
MaxResultCount |
int | 是 | 每页条数 |
Sorting |
string | 否 | 如 CreationTime desc |
Keyword |
string | 否 | Region 名 / Company 名模糊 |
PartnerId |
string | 否 | 按 Company(fl_partner.Id)筛选 |
State |
bool | 否 | 启用状态 |
Company Admin 数据范围
- 返回其绑定 Company(
userlocation→fl_partner.Id)下的全部 Region。 - 新建 Region 后无需重新登录即可在列表中看到(列表本身不依赖新 Region 是否已写
userlocation)。
curl 示例
curl -s -G "http://localhost:19001/api/app/group" \
-H "Authorization: <token>" \
--data-urlencode "SkipCount=1" \
--data-urlencode "MaxResultCount=10" \
--data-urlencode "Sorting=CreationTime desc"
2. 新增 Region
| 项目 | 内容 |
|---|---|
| 方法 | POST |
| 路径 | /api/app/group |
| Content-Type | application/json |
Body
| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
groupName |
string | 是 | Region 名称,对应 fl_group.GroupName / location.GroupName |
partnerId |
string | 是 | 所属 Company(fl_partner.Id) |
state |
bool | 否 | 默认 true |
Body 示例
{
"groupName": "Subway Guangzhou",
"partnerId": "3a218xxx-company-id",
"state": true
}
成功响应:GroupGetOutputDto(含 id、groupName、partnerId、partnerName、state、creationTime)。
Company Admin 副作用(2026-06-30)
- 若
partnerId为当前账号已绑定 Company,且存在location.Partner + location.GroupName与新建 Region 一致的门店,则这些门店写入userlocation。 - 前端无需额外调用 Team Member 接口。
curl 示例
curl -s -X POST "http://localhost:19001/api/app/group" \
-H "Authorization: <token>" \
-H "Content-Type: application/json" \
-d "{\"groupName\":\"Subway Guangzhou\",\"partnerId\":\"<fl_partner.Id>\",\"state\":true}"
3. 验证清单
| # | 步骤 | 期望 |
|---|---|---|
| 1 | Company Admin 登录,确认 userlocation 已有该公司下至少 1 家门店 |
有绑定 Company |
| 2 | POST /api/app/group 新建 Region(partnerId = 该公司) |
200,返回新 id |
| 3 | GET /api/app/group?SkipCount=1&MaxResultCount=10&Sorting=CreationTime desc |
列表含新 Region |
| 4 | 若该公司下已有同名 GroupName 的门店 |
查库 userlocation 新增对应 LocationId |
| 5 | GET /api/app/auth-scope/regions?partnerId=... |
下拉含新 Region |
| 6 | 空 Region 下 POST /api/app/location 创建首家门店 |
当前账号 userlocation 自动追加该门店 |
查库 SQL
-- 新建 Region 后:当前用户是否绑定该 Region 下门店
SELECT ul.*
FROM userlocation ul
INNER JOIN location loc ON loc.Id = ul.LocationId AND loc.IsDeleted = 0
INNER JOIN fl_partner p ON p.PartnerName = loc.Partner AND p.IsDeleted = 0
WHERE ul.IsDeleted = 0
AND ul.UserId = '<UserId>'
AND p.Id = '<partnerId>'
AND loc.GroupName = '<groupName>';
四、代码改动
| 文件 | 改动 |
|---|---|
Helpers/LocationScopeBindingHelper.cs |
新增 TryAutoBindCompanyAdminToNewRegionAsync、TryAutoBindCompanyAdminToNewLocationAsync、AppendUserLocationBindingsAsync |
Services/GroupAppService.cs |
CreateAsync 插入 fl_group 后调用 Region 自动绑定 |
Services/LocationAppService.cs |
CreateAsync 插入门店后 Company Admin 自动追加 userlocation(空 Region 补门店) |
IServices/IGroupAppService.cs |
补充 CreateAsync XML 说明 |
六、Product / Label Type 列表增加 locationName
需求
| 接口 | 说明 |
|---|---|
GET /api/app/product |
列表每行返回产品绑定的门店名称 |
GET /api/app/label-type |
列表每行返回类型绑定的门店名称 |
与 /api/app/label 列表的 locationName 字段命名一致,前端可直接展示 Location 列,无需再单独拉 product-location 或全量门店目录反查。
1. Product 分页列表
| 项目 | 内容 |
|---|---|
| 方法 | GET |
| 路径 | /api/app/product |
Query 示例
SkipCount=1&MaxResultCount=10
新增出参字段(items[])
| 字段 | 类型 | 说明 |
|---|---|---|
locationName |
string | 绑定门店名称,英文逗号拼接(location.LocationName,无名称时回退 LocationCode);无绑定时为 无 |
locationIds |
string[] | 绑定门店 Id(fl_location_product → location.Id) |
数据来源
fl_product
└── fl_location_product (ProductId + LocationId)
└── location (LocationName / LocationCode)
响应片段示例
{
"totalCount": 2,
"items": [
{
"id": "prod_001",
"productName": "Chicken Wrap",
"categoryName": "Sandwich",
"noOfLabels": 3,
"locationIds": ["3a220c4f-976e-9dcc-186b-ad2b90a9a4ef"],
"locationName": "Subway Beijing Store 01"
}
]
}
curl
curl -s -G "http://localhost:19001/api/app/product" \
-H "Authorization: <token>" \
--data-urlencode "SkipCount=1" \
--data-urlencode "MaxResultCount=10"
2. Label Type 分页列表
| 项目 | 内容 |
|---|---|
| 方法 | GET |
| 路径 | /api/app/label-type |
Query 示例
SkipCount=1&MaxResultCount=10
新增出参字段(items[])
| 字段 | 类型 | 说明 |
|---|---|---|
locationName |
string | 与既有 location 同值;availabilityType=ALL 时为 All Locations;SPECIFIED 且无绑定时为 无 |
location |
string | 保留兼容;内容与 locationName 一致 |
locationIds |
string[] | 已有字段;fl_label_type_location 绑定的门店 Id |
数据来源
fl_label_type
└── fl_label_type_location (LabelTypeId + LocationId)
└── location (LocationName / LocationCode)
展示规则
availabilityType |
locationName |
|---|---|
ALL |
All Locations |
SPECIFIED + 有绑定 |
门店名去重后英文逗号拼接 |
SPECIFIED + 无绑定 |
无 |
响应片段示例
{
"totalCount": 1,
"items": [
{
"id": "lt_price",
"typeName": "Price Label",
"availabilityType": "SPECIFIED",
"locationIds": ["3a220c4f-976e-9dcc-186b-ad2b90a9a4ef"],
"location": "Subway Beijing Store 01",
"locationName": "Subway Beijing Store 01",
"region": "Subway Beijing",
"noOfLabels": 12
}
]
}
curl
curl -s -G "http://localhost:19001/api/app/label-type" \
-H "Authorization: <token>" \
--data-urlencode "SkipCount=1" \
--data-urlencode "MaxResultCount=10"
3. 代码改动
| 文件 | 改动 |
|---|---|
Dtos/Product/ProductGetListOutputDto.cs |
新增 LocationName、LocationIds |
Services/ProductAppService.cs |
GetListAsync 批量读 fl_location_product 并汇总门店名;Excel 导出复用 BuildProductLocationDisplayMapsAsync |
Dtos/LabelType/LabelTypeGetListOutputDto.cs |
新增 LocationName |
Services/LabelTypeAppService.cs |
列表映射 LocationName = Location |
八、App Preview / Print Log / Label Report 修复
问题
| # | 现象 | 根因 |
|---|---|---|
| 1 | preview 出参 labelId 为 GUID |
误返回 fl_label.Id,未用当日序号 |
| 2 | get-print-log-list 的 labelId 格式不对 |
误返回 fl_label_print_task.LabelId(标签主键) |
| 3 | Print Log 仅当前用户记录 | restrictToCreator=true(非 Partner 角色) |
| 4 | Label Report 统计仅当前用户 | 同上 |
业务规则(统一)
Label ID = 某门店某自然日内打印任务递增序号:{yyyyMMdd}-{n}(如 20260707-1、20260707-2)。
| 范围 | 统计维度 |
|---|---|
同一 locationId |
同一自然日(PrintedAt ?? CreationTime) |
| 排序 | 时间升序,再按任务 Id 升序 |
| 预览 | 不落库,返回「下一序号」= 已有任务数 + 1 |
Print Log / Label Report 数据范围:已绑定该门店的账号 → 查看该门店全部用户的打印任务(不按 CreatedBy 过滤)。
1. Preview
| 项目 | 内容 |
|---|---|
| 方法 | POST |
| 路径 | /api/app/us-app-labeling/preview |
出参 labelId:yyyyMMdd-n(下一序号);printLabelDisplayId:模板含 Label ID 控件时同步写入画布。
2. Print Log
| 项目 | 内容 |
|---|---|
| 方法 | POST |
| 路径 | /api/app/us-app-labeling/get-print-log-list |
入参
| 字段 | 说明 |
|---|---|
locationId |
必填 |
printDateDay |
可选,yyyy-MM-dd,按自然日筛选 |
skipCount / maxResultCount |
分页(SkipCount 从 1 起) |
出参 items[]
| 字段 | 说明 |
|---|---|
labelId |
门店当日序号 yyyyMMdd-n |
labelEntityId |
fl_label.Id(内部关联) |
labelCode |
fl_label.LabelCode(重打/预览入参) |
operatorName |
实际打印人 |
3. Label Report
| 项目 | 内容 |
|---|---|
| 方法 | POST |
| 路径 | /api/app/us-app-labeling/get-label-report |
统计该 locationId 下全部用户在 startDate~endDate 内的打印量(与 Print Log 同范围)。
代码改动
| 文件 | 改动 |
|---|---|
UsAppLabelingAppService.cs |
Preview labelId;Print Log 序号 + printDateDay + 全店范围;Report 全店范围;Reprint 同店可重打 |
UsAppPrintLogScopeHelper.cs |
注释更新 |
PrintLogGetListInputVo.cs / IUsAppLabelingAppService.cs |
接口说明 |
九、关联文档
- Company Admin Region 列表范围:
项目相关文档/6-18代码优化.md(第十五节) - Team Member Company Admin 范围规则:
项目相关文档/6-18代码优化.md(第十二节) - group 列表 Token 范围(历史):
项目相关文档/5-17接口优化.md(group Region 列表与 PDF 导出权限) - Product 门店绑定(历史):
项目相关文档/5-17接口优化.md(product 列表筛选) - App Label ID 规则(历史):
项目相关文档/6-11代码优化.md、6-2代码优化.md