5-17接口优化.md 52.6 KB

5-17 接口优化

本文档说明 2026-05-17 对美国版对外接口的优化,包括:

  1. /api/app/rbac-roleaccessPermissionsmenuIdsorderNum 等(见 rbac-role 角色管理)。
  2. /api/app/partner/api/app/group:列表与 export-pdf 按登录 Token 数据范围过滤(见 partner / group 章节)。
  3. /api/app/location:门店新增 / 编辑 / 列表增加 operatingHours(经营时间),见 location 经营时间
  4. /api/app/product:产品列表 / 导出增加 公司 / 组织 / 门店 筛选,见 product 列表筛选
  5. /api/app/product-category:列表出参增加 regionlocation 展示字段,见 product-category 列表
  6. /api/app/team-member:成员新增 / 编辑 / 详情 / 列表增加 CompanyRegion 绑定字段,见 team-member 成员
  7. /api/app/label:标签列表 Region 筛选;新增/编辑 Company、Region 绑定,见 label
  8. /api/app/label-category:见 label-category

单条角色菜单的独立维护仍可使用 /api/app/rbac-role-menu(见 关联接口)。


目录

章节 内容
变更摘要 本次优化点一览
公共约定 基址、鉴权、响应包装
数据模型 入参 / 出参字段
accessPermissions 规则 读写语义(重要)
1 角色分页列表 GET /api/app/rbac-role
2 角色详情 GET /api/app/rbac-role/{id}
3 新增角色 POST /api/app/rbac-role
4 编辑角色 PUT /api/app/rbac-role/{id}
5 删除角色 DELETE /api/app/rbac-role
与 /api/app/role 的差异 平台内置 role 接口对比
partner 合作伙伴列表权限 GET /api/app/partner 按 Token 过滤
partner 新增与编辑地址字段 POST / PUT 增加 street 等
group Region PDF 导出权限 GET /api/app/group/export-pdf
location 经营时间 operatingHours 字段
product 列表筛选 partnerId / groupId / locationId
product-category 列表 region / location 出参
team-member 成员 Company / Region 绑定
label 列表筛选 + 新增/编辑 Company·Region
label-category Region/Location 绑定与筛选
附录 curl 示例 登录与调用示例

rbac-role 角色管理

变更摘要

说明
accessPermissions 出参 列表、详情、新增返回、编辑返回均包含 accessPermissions:由该角色已绑定菜单的 PermissionCode 去重后,按字母序用 ,(英文逗号+空格)拼接。
accessPermissions 入参 新增 / 编辑 body 可传 accessPermissions(英文逗号分隔的 PermissionCode),用于绑定角色菜单;逻辑见 accessPermissions 规则
menuIds 入参 新增 / 编辑 body 可传 menuIds(菜单 Guid 数组);与 accessPermissions 同时传时 menuIds 为准
orderNum 类型为 int?:新增不传或 null 时默认为 0;编辑不传或 null保留原排序号
事务 新增、编辑在写入角色后会同步角色-菜单绑定(若入参指定了 menuIdsaccessPermissions),与删除角色同属可回滚单元。

公共约定

  • 宿主:美国版后端 Yi.Abp.Web;本地示例:http://localhost:19001
  • 路由前缀api/app
  • Swagger 分组:「食品标签-美国版接口」。
  • 应用服务RbacRoleAppService(模块 food-labeling-us)。
  • 鉴权:与其它业务接口相同,请求头:Authorization: {登录返回的 data.token}(含 Bearer 前缀)。
  • Content-Type:JSON 接口使用 application/json;字段名一般为 camelCase(如 accessPermissionsmenuIds)。

统一响应包装(与其它 api/app 接口一致)示例:

{
  "statusCode": 200,
  "succeeded": true,
  "data": { },
  "errors": null,
  "extras": null,
  "timestamp": 1710000000000
}

失败时 succeededfalseerrorserror 中为业务提示(如「角色名称或编码已存在」)。


数据模型

查询入参 RbacRoleGetListInputVo

字段 类型 必填 说明
skipCount int 跳过条数(分页;与项目其它列表约定一致)
maxResultCount int 每页条数
sorting string 排序,如 orderNum desc
roleName string 角色名称,模糊
roleCode string 角色编码,模糊
state bool 启用状态

新增 / 编辑入参 RbacRoleCreateInputVo(编辑 RbacRoleUpdateInputVo 与其相同)

字段 类型 必填 说明
roleName string 角色名称
roleCode string 角色编码,唯一(与名称不可与未删角色重复)
remark string 备注
dataScope int 数据范围,默认 0(与内置 RBAC DataScope 枚举一致)
state bool 是否启用,默认 true
orderNum int? 排序号;见 变更摘要
menuIds Guid[] 要绑定的菜单 Id;见 accessPermissions 规则
accessPermissions string PermissionCode 列表(英文逗号分隔);见 accessPermissions 规则

列表项 / 详情基础 RbacRoleGetListOutputDto

字段 类型 说明
id Guid 角色主键
roleName string 角色名称
roleCode string 角色编码
remark string? 备注
dataScope int 数据范围
state bool 是否启用
orderNum int 排序号
accessPermissions string 已绑定菜单的 PermissionCode 汇总;无绑定时为空字符串 ""

详情扩展 RbacRoleGetOutputDto

继承列表字段,并增加:

字段 类型 说明
menuIds string[] 已分配菜单 Id(字符串形式的 Guid)

分页列表包装 PagedResultWithPageDto<T>

字段 类型 说明
pageIndex int 当前页码(从 1 开始)
pageSize int 每页条数
totalCount int 总记录数
totalPages int 总页数
items T[] 当前页数据

accessPermissions 规则

出参(只读汇总)

  • 来源:表 RoleMenu + Menu,取每条绑定菜单的 PermissionCode(非空)。
  • 格式:去重后按字母序拼接,例如:"product.view, product.edit, report.print"
  • GET /api/app/role 列表/详情中的 accessPermissions 含义一致,便于 People / Account 页与平台角色展示对齐。

入参(写入时绑定菜单)

场景 menuIds accessPermissions 行为
新增 传数组 不传 menuIds 绑定菜单(仅保留未删除且存在的菜单)
新增 不传 传非空字符串 按 PermissionCode 解析菜单并绑定
新增 不传 "" 不绑定任何菜单
新增 传数组 同时传 以 menuIds 为准,忽略 accessPermissions
编辑 传数组 任意 覆盖式更新为该 menuIds 集合
编辑 不传 传非空字符串 按 PermissionCode 覆盖绑定
编辑 不传 "" 清空该角色全部菜单绑定
编辑 不传 不传 / null 不修改已有菜单绑定,仅更新角色基础字段

说明:

  • accessPermissions 中的 Code 必须在 Menu.PermissionCode 中存在且菜单未删除,否则该 Code 被忽略(不会报错,但不会绑定对应菜单)。
  • 绑定方式为 覆盖式:每次按入参指定的菜单集合替换原 RoleMenu 关系(与 rbac-role-menu/set 一致)。

1 角色分页列表

项目 说明
HTTP GET
路径 /api/app/rbac-role
Query skipCountmaxResultCountsortingroleNameroleCodestate
默认排序 未传 sorting 时按 orderNum 降序

响应 dataPagedResultWithPageDto<RbacRoleGetListOutputDto>,每项含 accessPermissions


2 角色详情

项目 说明
HTTP GET
路径 /api/app/rbac-role/{id}
路径参数 id:角色 Guid

响应 dataRbacRoleGetOutputDto(含 menuIdsaccessPermissions)。


3 新增角色

项目 说明
HTTP POST
路径 /api/app/rbac-role
Body RbacRoleCreateInputVo

请求示例(仅基础字段)

{
  "roleName": "Store Manager",
  "roleCode": "store_manager",
  "remark": "门店管理员",
  "dataScope": 0,
  "state": true,
  "orderNum": 10
}

请求示例(同时用 menuIds 绑定权限)

{
  "roleName": "Store Manager",
  "roleCode": "store_manager",
  "state": true,
  "menuIds": [
    "33333333-3333-3333-3333-333333333301",
    "33333333-3333-3333-3333-333333333302"
  ]
}

请求示例(用 accessPermissions 绑定权限)

{
  "roleName": "Viewer",
  "roleCode": "viewer",
  "state": true,
  "accessPermissions": "product.view, report.print"
}

响应 dataRbacRoleGetOutputDto(含写入后的 menuIdsaccessPermissions)。

常见错误

提示 原因
角色名称不能为空 roleName 为空
角色编码不能为空 roleCode 为空
角色名称或编码已存在 与未删除角色重复

4 编辑角色

项目 说明
HTTP PUT
路径 /api/app/rbac-role/{id}
路径参数 id:角色 Guid
Body RbacRoleUpdateInputVo(字段同新增)

请求示例(只改名称,不动菜单)

{
  "roleName": "Store Manager (Updated)",
  "roleCode": "store_manager",
  "state": true
}

请求示例(清空全部菜单权限)

{
  "roleName": "Store Manager",
  "roleCode": "store_manager",
  "state": true,
  "accessPermissions": ""
}

响应 dataRbacRoleGetOutputDto


5 删除角色

项目 说明
HTTP DELETE
路径 /api/app/rbac-role
Body Guid[],要删除的角色 Id 列表(可批量)

删除时会同步清理该角色的 RoleMenuRoleDeptUserRole 关联,角色表为 软删除


与 /api/app/role 的差异

对比项 /api/app/rbac-role(美国版对外) /api/app/role(内置 RBAC)
用途 People / 美国版 Web 角色管理 平台权限体系内置角色
accessPermissions 出参 有(本次优化)
新增/编辑传 menuIds 支持 RoleCreateInputVo 支持 menuIds
新增/编辑传 accessPermissions 支持(按 Code 解析) 入参无此字段,仅出参汇总
详情 menuIds string[] 结构以 Swagger 为准
orderNum 可选 int?,编辑可省略保留原值 以内置 DTO 为准

前端 People → Roles 页应优先调用 /api/app/rbac-role;若仍调用 /api/app/role,字段命名需单独对齐。


关联接口

接口 说明
POST /api/app/rbac-role-menu/set 单独为角色设置菜单(body:roleId + menuIds),覆盖式
GET /api/app/rbac-role-menu/menu-ids?roleId={guid} 查询角色已绑定的 menuId 列表
DELETE /api/app/rbac-role-menu 按 roleId + menuIds 移除部分绑定

若在新增/编辑角色时已传 menuIdsaccessPermissions,一般无需再调 set;仅在「只改菜单、不改角色字段」时使用 rbac-role-menu


附录 curl 示例

以下 BASETOKEN 请替换为实际环境。

BASE=http://localhost:19001
TOKEN="Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."

登录(获取 Token)

curl -s -X POST "%BASE%/api/app/account/login" \
  -H "Content-Type: application/json" \
  -d "{\"userName\":\"admin@example.com\",\"password\":\"your_password\"}"

从返回的 data.token 填入 TOKEN

分页列表

curl -s -G "%BASE%/api/app/rbac-role" \
  -H "Authorization: %TOKEN%" \
  --data-urlencode "skipCount=0" \
  --data-urlencode "maxResultCount=20" \
  --data-urlencode "sorting=orderNum desc"

详情

curl -s "%BASE%/api/app/rbac-role/{roleId}" \
  -H "Authorization: %TOKEN%"

新增(含 accessPermissions)

curl -s -X POST "%BASE%/api/app/rbac-role" \
  -H "Authorization: %TOKEN%" \
  -H "Content-Type: application/json" \
  -d "{\"roleName\":\"Test Role\",\"roleCode\":\"test_role\",\"state\":true,\"accessPermissions\":\"product.view\"}"

编辑(不传权限字段,保留原菜单)

curl -s -X PUT "%BASE%/api/app/rbac-role/{roleId}" \
  -H "Authorization: %TOKEN%" \
  -H "Content-Type: application/json" \
  -d "{\"roleName\":\"Test Role Updated\",\"roleCode\":\"test_role\",\"state\":true}"

删除

curl -s -X DELETE "%BASE%/api/app/rbac-role" \
  -H "Authorization: %TOKEN%" \
  -H "Content-Type: application/json" \
  -d "[\"{roleId}\"]"


partner 合作伙伴列表权限

应用服务PartnerAppServicefl_partner,Account Management「Company」页签)。

变更说明

说明
影响接口 GET /api/app/partner(分页列表)、GET /api/app/partner/export-pdf(PDF 导出,筛选与列表一致)
管理员 可查看 全部 合作伙伴(公司)
其它角色 仅可查看与当前登录用户在 userlocation已绑定门店 相关联的公司
实现类 PartnerScopeHelper(与 Reports 模块 ReportsRoleHelper.IsAdminRole 判定一致)

详情、单条新增/编辑/删除 不做 本次范围限制(仍按原逻辑);若需详情也校验归属,可另行扩展。

管理员判定(与 Reports 一致)

满足 任一 条件视为管理员,不做 公司范围过滤:

条件 说明
用户名 JWT 中用户名为 admin(内置超管)
角色码 Roles claim 或 ICurrentUser.Roles 中含 admin
权限码 Permission claim 为 *:*:*

非管理员数据范围算法

  1. 取当前用户 Id(CurrentUser.Id)→ 查 userlocationIsDeleted = 0)得到 locationId 列表。
  2. 若无绑定门店 → 列表 / 导出 0 条
  3. location 表上述门店的 Partner 字段(存的是 公司名称,与新建门店时下拉所选 partnerName 一致)。
  4. fl_partner 中匹配:PartnerName 等于上述值 Id 等于上述值(兼容历史若 location.Partner 误存 Id)。
  5. 列表 / 导出仅返回匹配到的 fl_partner.Id 记录。
User ──userlocation──► Location ──Partner(公司名)──► fl_partner

1 合作伙伴分页列表

项目 说明
HTTP GET
路径 /api/app/partner
Query 示例 SkipCount=1&MaxResultCount=10&Sorting=CreationTime desc
其它筛选 keyword(名称/邮箱/电话模糊)、state(启用状态)

说明

  • SkipCount 在本项目中为 页码(从 1 起),非 SQL offset;与 PagedQueryConvention 一致。
  • Sorting 支持:CreationTime asc/descPartnerName asc/descState asc/desc;未识别时默认 CreationTime desc
  • Keyword / State 在数据范围过滤之后生效(先按权限收窄公司集合,再筛选)。

响应 dataPagedResultWithPageDto<PartnerGetListOutputDto>idpartnerNamecontactEmailphoneNumberstatecreationTime)。

2 PDF 导出(同范围)

项目 说明
HTTP GET
路径 /api/app/partner/export-pdf
Query 示例 Sorting=CreationTime desc(另可传 keywordstate
分页 忽略 SkipCount / MaxResultCount,导出符合筛选的全量公司
上限 5000 条,超出返回业务错误
数据范围 与列表相同:BuildPartnerListQueryAsyncPartnerScopeHelper

规则:管理员导出全部公司;非管理员仅导出其绑定门店所属公司(算法见上)。同一 Token、同一 Query 筛选下,导出行集 = 列表全量结果。

curl 示例(partner 导出 PDF)

curl -s -G "%BASE%/api/app/partner/export-pdf" \
  -H "Authorization: %TOKEN%" \
  --data-urlencode "Sorting=CreationTime desc" \
  -o companies.pdf

curl 示例(partner 列表)

curl -s -G "%BASE%/api/app/partner" \
  -H "Authorization: %TOKEN%" \
  --data-urlencode "SkipCount=1" \
  --data-urlencode "MaxResultCount=10" \
  --data-urlencode "Sorting=CreationTime desc"

group Region 列表与 PDF 导出权限

应用服务GroupAppServicefl_group,Account Management「Region」页签)。

变更说明

说明
影响接口 GET /api/app/group(分页列表)、GET /api/app/group/export-pdf(PDF 导出)
管理员 可查看 / 导出 全部 Region(组织)
其它角色 仅可查看 / 导出与 userlocation 绑定门店 对应的 Region(见下方算法)
实现 BuildGroupJoinedQueryAsync + PartnerScopeHelper.ResolveGroupScopeAsync / ApplyGroupScope

partner 列表 的区别:partner 按「绑定门店所属 公司」过滤;group 按「绑定门店上的 公司 + 组织名」精确匹配 fl_group不会列出该公司下其它未绑定门店的 Region。

管理员判定

partner 章节 相同(ReportsRoleHelper.IsAdminRole)。

非管理员数据范围算法

  1. 取当前用户 Id → 查 userlocation 得到绑定的 locationId
  2. 若无绑定门店 → 列表 / 导出 0 条
  3. 读取这些门店的 location.Partner(公司名称,与门店保存时一致)与 location.GroupName(Region 名称)。
  4. Partner 解析为 fl_partner.Id(按 PartnerNameId 匹配,与 partner 列表相同)。
  5. fl_group 中查找同时满足:
    • fl_group.PartnerId = 上一步的公司 Id
    • fl_group.GroupName = 该门店的 GroupName(trim 后全等)
  6. 列表 / 导出仅包含上述匹配到的 fl_group.Id
User ──userlocation──► Location(Partner + GroupName) ──匹配──► fl_group(PartnerId + GroupName)

注意:若门店未填 GroupNamePartner,该门店不会贡献任何可见 Region。

1 组织分页列表

项目 说明
HTTP GET
路径 /api/app/group
Query 示例 SkipCount=1&MaxResultCount=10&Sorting=CreationTime desc
其它筛选 keywordpartnerIdstate

说明

  • SkipCount页码(从 1 起)
  • Sorting 支持:CreationTimeGroupNameStatePartnerName 的 asc/desc。
  • 先按 Token 数据范围收窄,再应用 keyword / partnerId / state

响应 dataPagedResultWithPageDto<GroupGetListOutputDto>

curl 示例(group 列表)

curl -s -G "%BASE%/api/app/group" \
  -H "Authorization: %TOKEN%" \
  --data-urlencode "SkipCount=1" \
  --data-urlencode "MaxResultCount=10" \
  --data-urlencode "Sorting=CreationTime desc"

2 PDF 导出(同范围)

项目 说明
HTTP GET
路径 /api/app/group/export-pdf
Query 示例 Sorting=CreationTime desc(另可传 keywordpartnerIdstate
分页 忽略分页参数,全量导出(上限 5000 条)
数据范围 GET /api/app/group 列表一致(ResolveGroupScopeAsync

curl 示例(group 导出 PDF)

curl -s -G "%BASE%/api/app/group/export-pdf" \
  -H "Authorization: %TOKEN%" \
  --data-urlencode "Sorting=CreationTime desc" \
  -o regions.pdf

联调注意(group)

现象 可能原因
非管理员 Region 列表为空 账号未绑定门店;或门店 Partner / GroupName 为空
少看到 Region 仅显示绑定门店对应的那条 fl_group,同公司其它 Region 不会出现
有门店仍无 Region location.GroupNamefl_group.GroupName 不一致(大小写/空格);或 location.Partnerfl_partner 对不上

partner 新增与编辑地址字段

库表fl_partner 新增列 StreetCityStateCodeCountryZipCode。已有库执行脚本:

美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/scripts/fl_partner_add_address_columns.sql

命名说明(与门店 location 对齐):

业务含义 JSON 字段名 类型 说明
街道 street string? 可空
城市 city string? 可空
州/省代码 stateCode string? NY;口语里的「state(州)」用此字段
国家 country string? 可空
邮编 zipCode string? 可空
是否启用 state boolean Active / Inactive,与地址无关

同一 body 中 state(boolean) 表示启用状态,stateCode(string) 表示美国州缩写,请勿混用。

3 新增合作伙伴

项目 说明
HTTP POST
路径 /api/app/partner
Body PartnerCreateInputVo

请求示例

{
  "partnerName": "Global Foods Inc.",
  "contactEmail": "admin@globalfoods.com",
  "phoneNumber": "+1 (555) 100-2000",
  "street": "123 Main St",
  "city": "New York",
  "stateCode": "NY",
  "country": "USA",
  "zipCode": "10001",
  "state": true
}

响应 dataPartnerGetOutputDto(含上述地址字段及 lastModificationTime)。

4 编辑合作伙伴

项目 说明
HTTP PUT
路径 /api/app/partner/{id}
Body PartnerUpdateInputVo(字段与新增相同)

未传的地址字段会按空字符串处理为 清空null 落库);传则覆盖。

列表 / 详情出参 同步返回:streetcitystateCodecountryzipCodePartnerGetListOutputDto / PartnerGetOutputDto)。

列表 keyword 模糊搜索已包含地址五字段。

curl 示例(新增)

curl -s -X POST "%BASE%/api/app/partner" \
  -H "Authorization: %TOKEN%" \
  -H "Content-Type: application/json" \
  -d "{\"partnerName\":\"Global Foods Inc.\",\"street\":\"123 Main St\",\"city\":\"New York\",\"stateCode\":\"NY\",\"country\":\"USA\",\"zipCode\":\"10001\",\"state\":true}"

联调注意

现象 可能原因
非管理员列表为空 账号在 Team Member 未绑定任何门店;或门店 Partner 字段为空 / 与 fl_partner 名称不一致
管理员仍看不到某公司 检查是否被误判为非管理员(角色码非 admin
绑定门店后仍看不到公司 确认门店保存时 partner 为该公司 名称(与 fl_partner.partnerName 一致)
新增/编辑报列不存在 未执行 fl_partner_add_address_columns.sql

location 门店经营时间 operatingHours

库表location 新增列 OperatingHoursvarchar(512),可空,自由文本)。

脚本路径(在目标库执行一次):

美国版/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`;

字段说明

业务含义 JSON 字段名 DB 列名 类型 说明
经营时间 operatingHours OperatingHours string? 可空;示例:Mon–Fri 9:00 AM – 6:00 PM

影响接口

接口 说明
GET /api/app/location 列表 items[] 出参含 operatingHours
POST /api/app/location 新增 body 可传 operatingHours
PUT /api/app/location/{id} 编辑 body 可传 operatingHours(覆盖;传空字符串可清空)
PUT /api/app/location/bulk-update 批量编辑继承 LocationUpdateInputVo,同行支持
App GET /api/app/us-app-auth/location-detail operatingHours 读库;为空时展示

列表 keyword 模糊搜索已包含 operatingHours

1 门店分页列表

项目 说明
HTTP GET
路径 /api/app/location
Query 示例 SkipCount=1&MaxResultCount=10

响应 items[] 新增字段operatingHours(string?)。

curl 示例(列表)

curl -s -G "%BASE%/api/app/location" \
  -H "Authorization: %TOKEN%" \
  --data-urlencode "SkipCount=1" \
  --data-urlencode "MaxResultCount=10"

2 新增门店

项目 说明
HTTP POST
路径 /api/app/location
Body LocationCreateInputVo

请求示例

{
  "partner": "Global Foods Inc.",
  "groupName": "East Region",
  "locationCode": "LOC-001",
  "locationName": "Downtown Store",
  "street": "123 Main St",
  "city": "New York",
  "stateCode": "NY",
  "country": "USA",
  "zipCode": "10001",
  "phone": "+1 (555) 100-2000",
  "email": "store@example.com",
  "operatingHours": "Mon–Fri 9:00 AM – 6:00 PM; Sat 10:00 AM – 4:00 PM",
  "state": true
}

响应 dataLocationGetListOutputDto(含 operatingHours)。

3 编辑门店

项目 说明
HTTP PUT
路径 /api/app/location/{id}
Body LocationUpdateInputVo(含 operatingHours

curl 示例(编辑经营时间)

curl -s -X PUT "%BASE%/api/app/location/{id}" \
  -H "Authorization: %TOKEN%" \
  -H "Content-Type: application/json" \
  -d "{\"locationName\":\"Downtown Store\",\"operatingHours\":\"Mon–Fri 9:00 AM – 6:00 PM\",\"state\":true}"

联调注意(location)

现象 可能原因
新增/编辑报列不存在 未执行 fl_location_add_operating_hours_column.sql
App 详情仍显示「无」 门店未填写 operatingHours 或仅空格

product 产品列表按公司/组织/门店筛选

应用服务ProductAppServicefl_product + fl_location_product + location)。

变更说明

说明
影响接口 GET /api/app/product(分页列表)、GET /api/app/product/export-products-excel(Excel 导出)
新增 Query partnerIdgroupIdlocationId(均可选)
筛选逻辑 仅返回在 fl_location_product 中关联到「匹配门店」的产品;与 Menu Management 页 All Companies / All Regions / All Locations 下拉一致

Query 参数

字段 类型 说明
skipCount int 页码(从 1 起,与项目其它列表一致)
maxResultCount int 每页条数
sorting string 可选排序
keyword string 模糊:ProductCode / ProductName / CategoryName
state bool 启用状态
partnerId string 公司fl_partner.Id;匹配 location.Partner = 该公司名称的门店
groupId string 组织/Regionfl_group.Id;匹配该 Region 下 Partner + GroupName 的门店
locationId string 门店location.Id(Guid 字符串);最优先,传则忽略 partnerId / groupId

筛选优先级与算法

locationId 有值 → 仅该门店(须存在且未删除)
否则 groupId 有值 → location.Partner + location.GroupName 与 fl_group 对应公司名、组织名一致
否则 partnerId 有值 → location.Partner = fl_partner.PartnerName
否则 → 不按门店收窄(与改前一致)

匹配到的 location Id 集合 → fl_location_product.LocationId → 产品 fl_product.Id
场景 结果
三者均未传 全部未删除产品(仍受 keyword / state 约束)
传了 partnerId 但公司不存在 0 条
传了 groupId 但 Region 不存在 0 条
传了 locationId 但门店不存在 / 非 Guid 0 条
产品未绑定任何门店 在传了任一公司/组织/门店筛选时 不会出现

Reports 打印日志列表的 partnerId / groupId / locationId 解析规则一致。

1 产品分页列表

项目 说明
HTTP GET
路径 /api/app/product
Query 示例 SkipCount=1&MaxResultCount=10

curl 示例(按门店筛选)

curl -s -G "%BASE%/api/app/product" \
  -H "Authorization: %TOKEN%" \
  --data-urlencode "SkipCount=1" \
  --data-urlencode "MaxResultCount=10" \
  --data-urlencode "locationId=11111111-1111-1111-1111-111111111111"

curl 示例(按公司 + 关键字)

curl -s -G "%BASE%/api/app/product" \
  -H "Authorization: %TOKEN%" \
  --data-urlencode "SkipCount=1" \
  --data-urlencode "MaxResultCount=10" \
  --data-urlencode "partnerId=你的fl_partner主键" \
  --data-urlencode "Keyword=tuna"

2 Excel 导出(同筛选)

GET /api/app/product/export-products-excel 支持与列表相同的 partnerIdgroupIdlocationIdkeywordstate

联调注意(product)

现象 可能原因
选了公司后列表为空 产品未在 fl_location_product 绑定该公司下任一门店
Region 筛选无数据 门店 Partner / GroupNamefl_group 不一致
与前端下拉 Id 对不上 公司/组织传 主键 Idfl_partner.Idfl_group.Id),门店传 location.Id

product-category / product Region·Location 绑定与筛选

应用服务ProductCategoryAppServiceProductAppService

命名约定:UI Region = API groupIds / groupIdfl_group.Id);落库门店冗余字段为 location.GroupName。UI Location = locationIds / locationIdlocation.Id)。

变更说明(product-category)

说明
列表 GET /api/app/product-category 出参 regionlocation;Query 增加 groupIdlocationId 筛选
新增/编辑 POST / PUT Body 增加 groupIdslocationIds(与原有 availabilityType 配合)
详情 GET /{id} 返回 groupIdslocationIds
存储 仍写入 fl_product_category_location(按合并后的门店 Id,无新表)

变更说明(product)

说明
新增/编辑 Body 增加 groupIdslocationIds,合并写入 fl_location_product
详情 返回 groupIds(由关联门店反推)、locationIds

出参字段

字段 类型 说明
region string 适用 Region 汇总
location string 适用门店汇总

其它列表字段不变(categoryCodecategoryNameavailabilityTypeorderNum 等)。

展示规则

availabilityType region location
ALL(或未配置为 SPECIFIED) All Regions All Locations
SPECIFIED 绑定门店在 location.GroupName 上的去重值,按字母序 , 拼接
SPECIFIED 但未绑定门店
SPECIFIED 已绑定但门店无 GroupName 仍有门店名时显示门店名

门店名优先 location.LocationName,为空则用 LocationCode

数据来源:fl_product_category_locationlocation(与详情中的 locationIds 一致,列表侧转为可读文案)。

新增/编辑 product-category(Region / Location 多选数组)

项目 说明
新增 POST /api/app/product-category
编辑 PUT /api/app/product-category/{id}
Body ProductCategoryCreateInputVo / ProductCategoryUpdateInputVo
字段 类型 说明
availabilityType string ALL / SPECIFIED;见下方自动规则
regionIds string[] Region 多选fl_group.Id);推荐字段名
groupIds string[] regionIds 等价,会合并去重(兼容旧字段名)
locationIds string[] 门店 多选location.Id

自动规则

入参 行为
regionIds / locationIds 任一有值 availabilityTypeSPECIFIED 处理
仅传 regionIds: []locationIds: []availabilityTypeALL 清空指定门店,范围改回 全部
availabilityType: "ALL" 且未传上述数组 不绑定门店(全部门店可用)

合并规则:每个 regionIds 展开为该 Region 下全部门店,再与 locationIds 取并集 → 写入 fl_product_category_location;至少 1 个有效门店。

请求示例(多选 Region + 多选门店)

{
  "categoryCode": "CAT-01",
  "categoryName": "Sandwich",
  "regionIds": [
    "fl_group_id_east",
    "fl_group_id_west"
  ],
  "locationIds": [
    "11111111-1111-1111-1111-111111111111",
    "22222222-2222-2222-2222-222222222222"
  ],
  "state": true,
  "orderNum": 10
}

详情 / 列表回显

接口 多选 Id 字段
GET /api/app/product-category/{id} regionIdslocationIdsgroupIdsregionIds 相同)
GET /api/app/product-category 列表 items[] regionIdslocationIds + 展示字段 regionlocation

1 类别分页列表

项目 说明
HTTP GET
路径 /api/app/product-category
Query 示例 SkipCount=1&MaxResultCount=10&Sorting=OrderNum desc

新增 Query 筛选

字段 说明
groupId 按 Region(fl_group.Id)筛选;含 availabilityType=ALL 的分类
locationId 按门店筛选(优先于 groupId

响应 items[] 示例片段

{
  "id": "...",
  "categoryName": "Sandwich",
  "availabilityType": "SPECIFIED",
  "region": "East Region, West Region",
  "location": "UNCC store, Central Park Store",
  "regionIds": ["fl_group_id_east", "fl_group_id_west"],
  "locationIds": ["11111111-1111-1111-1111-111111111111"],
  "orderNum": 10
}

curl 示例(按 Region 筛选)

curl -s -G "%BASE%/api/app/product-category" \
  -H "Authorization: %TOKEN%" \
  --data-urlencode "SkipCount=1" \
  --data-urlencode "MaxResultCount=10" \
  --data-urlencode "Sorting=OrderNum desc" \
  --data-urlencode "groupId=你的fl_group主键"

2 产品新增/编辑(Company + Region + Location)

项目 说明
新增 POST /api/app/product
编辑 PUT /api/app/product/{id}
详情 GET /api/app/product/{id} 返回 partnerIdpartnerIdsgroupIdslocationIds
字段 UI 类型 说明
partnerId Company string? fl_partner.Id;展开该公司下全部门店参与合并
groupIds Region string[]? fl_group.Id;展开各 Region 下全部门店
locationIds Location string[]? location.Id;显式指定门店

合并规则(去重后写入 fl_location_product):

partnerId → 公司下全部门店
groupIds  → 各 Region 下全部门店(Partner + GroupName 匹配 location)
locationIds → 指定门店
三者取并集

locationIds: [] 且其它范围字段为空/不传时,可清空门店关联;仅传 partnerIdgroupIds 时须至少解析出 1 个有效门店。

请求示例

{
  "productName": "Tuna Sub",
  "categoryId": "分类Id",
  "partnerId": "fl_partner主键",
  "groupIds": ["fl_group_id_east"],
  "locationIds": ["11111111-1111-1111-1111-111111111111"],
  "state": true
}

详情出参示例

{
  "id": "...",
  "productName": "Tuna Sub",
  "partnerId": "fl_partner主键",
  "partnerIds": ["fl_partner主键"],
  "groupIds": ["fl_group_id_east"],
  "locationIds": ["11111111-1111-1111-1111-111111111111"]
}

列表筛选仍用 Query:partnerId(Company)、groupId(Region)、locationId(Location),见 product 列表筛选

联调注意(product-category / product)

现象 可能原因
region 为「无」 SPECIFIED 但门店未填 GroupName
location 为「无」 SPECIFIED 但未保存绑定 / 关联表无数据
仍为 All Regions availabilityType 不是 SPECIFIED
保存报「至少需要匹配到一个有效门店」 groupIds 下无门店,且 locationIds 为空或无效
Region 选了但门店未全带上 正常:groupIds 会展开该区域下全部门店写入关联表

team-member 成员 Company·Region 绑定

应用服务TeamMemberAppService

命名约定(与 product / product-category 一致):

UI API 字段 存储
Company partnerId / partnerIds fl_partner.Id;门店通过 location.Partner(公司名称)关联
Region regionIds / groupIds fl_group.Id;门店冗余字段 location.GroupName
Location locationIds location.Id → 写入 userlocation

变更说明

说明
新增 POST /api/app/team-member Body 增加 partnerIdpartnerIdsregionIdsgroupIdslocationIds 可与前三者合并
编辑 PUT /api/app/team-member/{id} 同上;批量编辑 update-team-members-bulkitems[] 与单条 PUT 字段一致
详情 GET /api/app/team-member/{id} 返回 partnerIdsregionIdsgroupIds(与 regionIds 相同)、locationIds
列表 GET /api/app/team-member items[] 增加 partnerIdsregionIds(由已绑定门店反推)
存储 仍写入 userlocation(合并后的门店 Id 列表,无新表)

新增 / 编辑入参(节选)

字段 类型 必填 说明
fullName string 姓名
userName string 是(新增) 登录名
password string 是(新增)/ 可空(编辑) 密码
email string 邮箱
phone long? 手机
roleId Guid? RBAC 角色
partnerId string 单个 Company(fl_partner.Id
partnerIds string[] Company 多选;与 partnerId 合并
regionIds string[] Region 多选(fl_group.Id
groupIds string[] regionIds 相同,合并去重
locationIds string[] 显式门店;与 Company/Region 展开结果 取并集
state bool 启用,默认 true

合并规则partnerId/partnerIds → 该公司下全部门店;regionIds/groupIds → 该区域下全部门店;再并入 locationIds。保存前须 至少 1 个有效门店,否则返回:成员必须至少分配一个门店(公司/区域/门店至少选一项)

详情 / 列表出参(节选)

字段 类型 说明
partnerIds string[] userlocation 反推的 Company Id
regionIds string[] 由绑定门店反推的 Region Id
groupIds string[] regionIds 相同
locationIds string[] 实际绑定的门店 Id
assignedLocations object[] 门店展示(id、name 等,原有字段)

请求示例(新增)

POST /api/app/team-member
Content-Type: application/json
Authorization: {token}
{
  "fullName": "Jane Doe",
  "userName": "jane.doe",
  "password": "ChangeMe123!",
  "email": "jane@example.com",
  "roleId": "00000000-0000-0000-0000-000000000001",
  "partnerId": "fl_partner主键",
  "groupIds": ["fl_group_id_east"],
  "locationIds": [],
  "state": true
}

列表示例

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

联调注意(team-member)

现象 可能原因
保存报「至少分配一个门店」 Company/Region/门店均未选,或所选范围下无有效门店
详情 partnerIds 为空 绑定门店的 location.Partner 未匹配到 fl_partner
详情 regionIds 为空 门店未填 GroupName 或无法匹配 fl_group
批量导入仍只认门店列 Excel 导入逻辑未改,仍按 LocationIds 解析;Web 表单用新字段

批量导入 / PDF 导出说明见 项目相关文档/批量导入导出接口说明.md


label 标签

应用服务LabelAppService

命名约定

UI API 存储
Company partnerId / partnerIds 解析范围;落库为 fl_label.LocationId(单门店)
Region regionIds / groupIds / groupId(列表筛选) fl_group.Id
Location locationId / locationIds location.Id

标签表仅保存 一个 LocationId;Company/Region 用于选定或校验门店,与 product 多门店关联不同。

变更说明(列表)

说明
列表 GET /api/app/label Query 增加 groupId(Region 筛选);与 locationId 配合

Query 参数(节选)

字段 类型 说明
skipCount int 页码(从 1 起)
maxResultCount int 每页条数
keyword string 模糊:标签名、分类、类型、模板、产品名
productId string 按关联产品筛选
labelCategoryId string 标签分类 Id
labelTypeId string 标签类型 Id
templateCode string 模板编码
state bool 启用状态
groupId string Regionfl_group.Id;返回 fl_label.LocationId 属于该区域下门店的标签
locationId string 门店location.Id优先于 groupId

筛选优先级

locationId 有值 → 仅该门店(须存在且未删除)
否则 groupId 有值 → 该区域下全部门店(Partner + GroupName 与 fl_group 一致)
否则 → 不按门店/Region 收窄
场景 结果
均未传 与改前一致(仍受 keyword / productId 等约束)
传了 groupId 但 Region 不存在 0 条
传了 locationId 但门店无效 0 条
标签未填 LocationId 在传了 groupId / locationId 时 不会出现

product-category 列表的 groupId / locationId 解析规则一致(LocationScopeBindingHelper.ResolveScopedLocationIdsAsync)。

1 标签分页列表

项目 说明
HTTP GET
路径 /api/app/label
Query 示例 SkipCount=1&MaxResultCount=10

curl 示例(按 Region 筛选)

curl -s -G "%BASE%/api/app/label" \
  -H "Authorization: %TOKEN%" \
  --data-urlencode "SkipCount=1" \
  --data-urlencode "MaxResultCount=10" \
  --data-urlencode "groupId=你的fl_group主键"

curl 示例(按门店筛选,优先于 groupId)

curl -s -G "%BASE%/api/app/label" \
  -H "Authorization: %TOKEN%" \
  --data-urlencode "SkipCount=1" \
  --data-urlencode "MaxResultCount=10" \
  --data-urlencode "locationId=11111111-1111-1111-1111-111111111111"

变更说明(新增 / 编辑)

说明
新增 POST /api/app/label Body 增加 partnerIdpartnerIdsregionIdsgroupIdslocationIds
编辑 PUT /api/app/label/{id} 同上(id = LabelCode)
详情 GET /api/app/label/{id} 返回 partnerIdpartnerIdsregionIdsgroupIdslocationId

新增/编辑入参(节选)

字段 类型 说明
labelName string 标签名称
templateCode string 模板编码
labelCategoryId / labelTypeId string 分类、类型
productIds string[] 关联产品(至少 1 个)
partnerId string Company(fl_partner.Id
partnerIds string[] Company 多选
regionIds string[] Region 多选(fl_group.Id
groupIds string[] regionIds 合并
locationId string 所属门店;优先直接落库
locationIds string[] 门店候选;未传 locationId 且合并后 唯一 时自动采用

解析规则

传 locationId → 校验在 Company/Region 展开范围内(有范围时)→ 写入 fl_label.LocationId
未传 locationId → 合并 partnerId/partnerIds + regionIds/groupIds + locationIds
  → 0 个:报错
  → 1 个:自动作为所属门店
  → 多个:报错,要求显式传 locationId

请求示例

{
  "labelName": "Tuna Label",
  "templateCode": "TMP-01",
  "partnerId": "fl_partner主键",
  "groupIds": ["fl_group_id_east"],
  "locationId": "11111111-1111-1111-1111-111111111111",
  "labelCategoryId": "分类Id",
  "labelTypeId": "类型Id",
  "productIds": ["产品Id"],
  "state": true
}

详情出参示例

{
  "id": "LBL_xxx",
  "labelName": "Tuna Label",
  "locationId": "11111111-1111-1111-1111-111111111111",
  "partnerId": "fl_partner主键",
  "partnerIds": ["fl_partner主键"],
  "regionIds": ["fl_group_id_east"],
  "groupIds": ["fl_group_id_east"]
}

联调注意(label)

现象 可能原因
选了 Region 后列表为空 标签 LocationId 未落在该区域门店,或门店 Partner/GroupNamefl_group 不一致
与前端下拉 Id 对不上 Region 传 fl_group.Id,门店传 location.Id
保存报「对应多个门店」 仅选了 Company/Region 且展开多于 1 家门店,须再选 locationId
保存报「须指定门店」 未传 locationId 且 Company/Region 未解析出门店

label-category 标签分类 Region·Location

应用服务LabelCategoryAppService

命名约定:UI Region = API regionIds / groupIds / groupIdfl_group.Id);UI Location = locationIds / locationIdlocation.Id)。存储表 fl_label_category_location(与 product-category 模式一致)。

变更说明

说明
新增 POST /api/app/label-category Body 增加 regionIdsgroupIdslocationIds(多选数组)
编辑 PUT /api/app/label-category/{id} 同上
详情 GET /{id} 返回 regionIdsgroupIds(与 regionIds 相同)、locationIds
列表 GET /api/app/label-category 出参 regionlocationnoOfLabelslastEdited(与下属标签同步);Query groupIdlocationId 筛选
存储 availabilityType=SPECIFIED 时写入 fl_label_category_location(Region 展开为门店后取并集)

新增/编辑入参(节选)

字段 类型 说明
availabilityType string ALL / SPECIFIED;传了 regionIds/locationIds 时自动按 SPECIFIED
regionIds string[] Region 多选(fl_group.Id
groupIds string[] regionIds 合并去重
locationIds string[] 门店多选(location.Id

合并规则:每个 regionIds 展开为该 Region 下全部门店,再与 locationIds 取并集 → 写入关联表;SPECIFIED 时至少 1 个有效门店。

请求示例

{
  "categoryCode": "LC-01",
  "categoryName": "Sandwich Labels",
  "availabilityType": "SPECIFIED",
  "regionIds": ["fl_group_id_east"],
  "locationIds": ["11111111-1111-1111-1111-111111111111"],
  "state": true,
  "orderNum": 10
}

列表 Query 筛选

字段 说明
groupId 按 Region 筛选; availabilityType=ALL 的分类
locationId 按门店筛选;优先于 groupId
curl -s -G "%BASE%/api/app/label-category" \
  -H "Authorization: %TOKEN%" \
  --data-urlencode "SkipCount=1" \
  --data-urlencode "MaxResultCount=10" \
  --data-urlencode "groupId=你的fl_group主键"

列表出参(节选)

字段 类型 说明
id string 分类主键
categoryCode / categoryName string 编码、名称
state / orderNum bool / int 状态、排序
region string 列表列 RegionALLAll RegionsSPECIFIED → 绑定门店 GroupName 去重 , 拼接
location string 列表列 LocationALLAll LocationsSPECIFIED → 门店名拼接
regionIds string[] Region Id(ALL[]
locationIds string[] 门店 Id(ALL[]
noOfLabels long 列表列 No. of Label:该分类下未删除标签数(fl_label.LabelCategoryId
creationTime datetime 分类创建时间
lastEdited datetime 列表列 Last Editedmax(分类 LastModificationTime/CreationTime, 下属标签最近编辑时间);标签增删改会回写分类 LastModificationTime

响应 items[] 示例

{
  "id": "cat_001",
  "categoryName": "Sandwich Labels",
  "availabilityType": "SPECIFIED",
  "region": "East Region",
  "location": "UNCC store, Central Park Store",
  "regionIds": ["fl_group_id_east"],
  "locationIds": ["11111111-1111-1111-1111-111111111111"],
  "noOfLabels": 12,
  "creationTime": "2026-05-10T08:00:00",
  "lastEdited": "2026-05-17T14:30:00",
  "orderNum": 10
}

前端 Last Edited 列请绑定 lastEdited,勿用 creationTime(仅为兼容保留)。

联调注意(label-category)

现象 可能原因
region 为「无」 SPECIFIED 但门店未填 GroupName
保存报「至少需要匹配到一个有效门店」 Region 下无门店且 locationIds 为空
列表筛 Region 仍看到 ALL 分类 设计如此:ALL 对任意 Region/门店均可见
Last Edited 不随标签变化 未拉最新列表;应读 lastEdited 而非 creationTime
noOfLabels 为 0 该分类下尚无标签,或标签 LabelCategoryId 未指向本分类

逻辑与 product-category 对齐,仅表名为 fl_label_category / fl_label_category_location


修订记录

日期 说明
2026-05-17 初版:rbac-role accessPermissions / menuIds / orderNum
2026-05-17 补充:partner 列表与 export-pdf 按 Token 数据范围过滤
2026-05-17 补充:partner 新增/编辑/列表/详情 地址字段 street、city、stateCode、country、zipCode
2026-05-17 补充:group 列表/export-pdf 按绑定门店 Partner+GroupName 过滤 Region
2026-05-17 补充:location 新增/编辑/列表 operatingHours(经营时间)
2026-05-17 补充:product 列表/export 增加 partnerId、groupId、locationId 筛选
2026-05-17 补充:product-category 列表出参 region、location 展示适用区域与门店
2026-05-17 补充:product-category/product 新增编辑 groupIds+locationIds;分类列表 groupId/locationId 筛选
2026-05-17 补充:product 新增/编辑/详情 增加 partnerId(Company)与 groupIds(Region)绑定
2026-05-17 补充:product-category 新增/编辑/列表 regionIds、locationIds 多选数组
2026-05-17 补充:team-member 新增/编辑/详情/列表 partnerId、partnerIds、regionIds、groupIds 与 locationIds 合并绑定
2026-05-17 补充:label 列表 GET 增加 groupId(Region)筛选,locationId 优先
2026-05-17 补充:label 新增/编辑/详情 partnerId、partnerIds、regionIds、groupIds 与 locationId 解析绑定
2026-05-17 补充:label-category 新增/编辑 regionIds+locationIds 多选;列表 groupId/locationId 筛选与 region/location 展示
2026-05-17 补充:label-category 列表 noOfLabels、lastEdited(含下属标签同步)、region/location 列出参说明