Blame view

项目相关文档/5-26代码优化.md 26.4 KB
49755ef0   李曜臣   6-12代码优化
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
  # 5-26 代码优化
  
  本文档说明 **2026-05-26** 对美国版接口的变更。
  
  1. **`/api/app/product-category`**:新增/编辑 **`categoryCode` 取消必填**(见 [product-category-categoryCode](#product-category-categorycode-可选))。
  2. **`/api/app/label-template`**:新增/编辑/列表/详情支持 **Region、Location 多选数组**;列表 Query 增加 **Region/Location 筛选**(见 [label-template-regionlocation](#label-template-regionlocation-多选))。
  3. **`/api/app/rbac-role`**:修复 **`accessPermissions` JSON 数组**(如 `manage_labels`)无法绑定菜单(见 [rbac-role-accesspermissions](#rbac-role-accesspermissions-修复))。
  4. **`/api/app/auth-scope`**:管理员(及按数据范围受限账号)登录后 **Company → Region → Location** 级联选店(见 [auth-scope-登录选店](#auth-scope-登录后-company--region--location-级联选店))。
  5. **`/api/app/us-app-auth`**:App 管理员 Token 专用 **Company / Region / 门店筛选** 接口(见 [us-app-auth-管理员选店](#us-app-auth-app-管理员级联选店))。
  
  **应用服务**`ProductCategoryAppService`、`LabelTemplateAppService`、`RbacRoleAppService`  
  **命名约定**(与 5-17 / 5-18 一致):UI **Region** = API **`regionIds` / `groupIds` / `groupId`**`fl_group.Id`);UI **Location** = **`locationIds` / `locationId`**(`location.Id`)。
  
  ---
  
  ## product-category categoryCode 可选
  
  **影响接口**
  
  | 方法 | 路径 |
  |------|------|
  | POST | `/api/app/product-category` |
  | PUT | `/api/app/product-category/{id}` |
  
  ### 变更说明
  
  | 项 | 变更前 | 变更后 |
  |----|--------|--------|
  | **categoryCode** | 必填;空则报「类别编码和名称不能为空」 | **可选**;可不传、传 `null` 或 `""` |
  | **categoryName** | 必填 | 仍必填 |
  | **落库** | — | 未填编码时 `CategoryCode`**空字符串** |
  | **唯一性** | 编码或名称重复即报错 | 有编码:编码 **或** 名称重复报错;**无编码**:仅校验 **名称** 不重复 |
  
  ### 入参(节选)
  
  | 字段 | 类型 | 必填 | 说明 |
  |------|------|------|------|
  | categoryCode | string | **否** | 类别编码 |
  | categoryName | string | **是** | 类别名称 |
  | regionIds / groupIds / locationIds | string[] | 否 | Region·Location 范围(规则见 `5-17接口优化.md`) |
  
  ### 请求示例(无编码)
  
  ```http
  POST /api/app/product-category
  Content-Type: application/json
  Authorization: Bearer {token}
  ```
  
  ```json
  {
    "categoryName": "Beverages",
    "buttonAppearance": "TEXT",
    "state": true,
    "availabilityType": "ALL",
    "orderNum": 0
  }
  ```
  
  ### 联调注意
  
  | 现象 | 处理 |
  |------|------|
  | 仍报「类别编码和名称不能为空」 | 确认已部署含本变更的后端;仅需保证 **categoryName** 非空 |
  | 无编码时名称重复 | 正常:仅按 **categoryName** 判重 |
  
  > Region/Location 多选、列表 `region`/`location` 展示等完整说明见 `5-17接口优化.md` → product-category 章节。
  
  ---
  
  ## label-template Region·Location 多选
  
  **应用服务**`LabelTemplateAppService`  
  **存储表**`fl_label_template_location`(模板 ↔ 门店,**无新表**
  **主表字段**`fl_label_template.AppliedLocationType` = `ALL` / `SPECIFIED`
  
  ### 变更说明
  
  | 项 | 变更前 | 变更后 |
  |----|--------|--------|
  | **新增/编辑 Body** | 仅 `appliedLocation` + `appliedLocationIds` | 增加 **`regionIds`**、**`groupIds`**、**`locationIds`**(与 `appliedLocationIds` 合并) |
  | **列表 Query** | 仅 `locationId` | 增加 **`groupId`**(Region);`locationId` 优先于 `groupId`(与 product-category 一致) |
  | **列表出参** | 仅 `locationText`(单条展示) | 增加 **`region`**、**`location`** 展示 + **`regionIds`**、**`locationIds`** 数组 |
  | **详情出参** | `appliedLocationIds` | 同上,并保留 **`appliedLocationIds`**(与 `locationIds` 一致,兼容编辑器) |
  | **范围解析** | 仅显式门店 Id | Region 展开为门店后与门店 Id **取并集** 落库 |
  
  ### 影响接口
  
  | 方法 | 路径 | 说明 |
  |------|------|------|
  | GET | `/api/app/label-template?SkipCount=1&MaxResultCount=10` | 列表支持 `groupId`/`locationId` 筛选;`items[]` 增加 `region`、`location`、`regionIds`、`locationIds` |
  | GET | `/api/app/label-template/{id}` | 详情增加上述字段 |
  | POST | `/api/app/label-template` | Body 支持 Region/Location 多选 |
  | PUT | `/api/app/label-template/{id}` | 同新增 |
  
  路径参数 **`id`** 仍为模板编码 **`TemplateCode`**(与编辑器 JSON 的 `id` 一致)。
  
  ### 新增/编辑入参(Body:`LabelTemplateCreateInputVo`)
  
  | 字段 | JSON 名 | 类型 | 必填 | 说明 |
  |------|---------|------|------|------|
  | TemplateCode | `id` | string | 是 | 模板编码 |
  | TemplateName | `name` | string | 是 | 模板名称 |
  | AppliedLocationType | `appliedLocation` | string | 否 | `ALL` / `SPECIFIED`,默认 `ALL` |
  | RegionIds | `regionIds` | string[] | 否 | Region 多选(`fl_group.Id`) |
  | GroupIds | `groupIds` | string[] | 否 | 与 `regionIds` 等价,合并去重 |
  | LocationIds | `locationIds` | string[] | 否 | 门店多选(`location.Id`) |
  | AppliedLocationIds | `appliedLocationIds` | string[] | 否 | 兼容旧字段,与 `locationIds` 合并 |
  | Elements | `elements` | array | 否 | 模板组件,全量重建 |
  | TemplateProductDefaults | `templateProductDefaults` | array | 否 | 仅 **编辑** 时显式传入才重建 |
  
  **自动规则**
  
  | 入参 | 行为 |
  |------|------|
  | `regionIds` / `groupIds` / `locationIds` / `appliedLocationIds` 任一有有效 Id | `appliedLocation` 按 **`SPECIFIED`** 处理 |
  | 仅传空数组 `[]` 且 `appliedLocation` 为 `ALL` | 不绑定门店(全部门店) |
  | `appliedLocation: "SPECIFIED"` 且合并后无有效门店 | 报错:`指定适用区域或门店时,至少需要匹配到一个有效门店` |
  | `appliedLocation` 非法值 | 报错:`适用门店范围不合法(ALL/SPECIFIED)` |
  
  **合并规则**:每个 `regionIds` 展开为该 Region 下全部门店,再与 `locationIds`、`appliedLocationIds` **取并集** → 写入 `fl_label_template_location`
  
  ### 请求示例(Region + 门店多选)
  
  ```http
  POST /api/app/label-template
  Content-Type: application/json
  Authorization: Bearer {token}
  ```
  
  ```json
  {
    "id": "TPL_TEST_001",
    "name": "Price Tag 4x6",
    "labelType": "PRICE",
    "unit": "inch",
    "width": 4,
    "height": 6,
    "appliedLocation": "SPECIFIED",
    "regionIds": [
      "fl_group_id_east",
      "fl_group_id_west"
    ],
    "locationIds": [
      "11111111-1111-1111-1111-111111111111"
    ],
    "showRuler": true,
    "showGrid": true,
    "state": true,
    "elements": []
  }
  ```
  
  ### 请求示例(全部门店,兼容旧版)
  
  ```json
  {
    "id": "TPL_ALL",
    "name": "Global Template",
    "labelType": "PRICE",
    "unit": "inch",
    "width": 4,
    "height": 6,
    "appliedLocation": "ALL",
    "appliedLocationIds": [],
    "elements": []
  }
  ```
  
  ### 列表(`GET /api/app/label-template`)
  
  **Query 参数**
  
  | 字段 | 类型 | 说明 |
  |------|------|------|
  | SkipCount / MaxResultCount | int | 分页(项目约定 SkipCount 从 1 起) |
  | keyword | string | 模板名称/编码模糊 |
  | **groupId** | string | **按 Region 筛选**`fl_group.Id`):命中 `appliedLocation=ALL` 的模板,或在 `fl_label_template_location` 中绑定了该 Region 下任一门门店的模板 |
  | **locationId** | string | **按门店筛选**`location.Id`);**优先于 groupId** |
  | labelType | string | 如 `PRICE` |
  | state | bool | 启用状态 |
  | sorting | string | 排序(可选) |
  
  **筛选规则**(与 product-category / label-type 相同,内部 `LocationScopeBindingHelper.ResolveScopedLocationIdsAsync`
  
  | 入参 | 行为 |
  |------|------|
  | 均未传 `groupId`、`locationId` | 不过滤适用范围 |
  | 仅 `groupId` | 解析该 Region 下全部门店 Id,再筛模板 |
  | 仅 `locationId` | 按该门店 Id 筛模板 |
  | 同时传 | **以 `locationId` 为准**(忽略 `groupId`) |
  | Region/门店无效或解析结果为空 | 仅返回 **`appliedLocation=ALL`** 的模板 |
  
  命中条件(满足其一即可出现在列表):
  
  - `fl_label_template.AppliedLocationType = 'ALL'`
  - `SPECIFIED` 且 `fl_label_template_location` 中存在 `LocationId ∈` 解析得到的门店集合
  
  **请求示例**
  
  ```http
  GET /api/app/label-template?SkipCount=1&MaxResultCount=10&groupId=fl_group_id_east HTTP/1.1
  Authorization: Bearer {token}
  ```
  
  ```http
  GET /api/app/label-template?SkipCount=1&MaxResultCount=10&locationId=11111111-1111-1111-1111-111111111111 HTTP/1.1
  Authorization: Bearer {token}
  ```
  
  **命名对照**:UI **Region** → Query **`groupId`**;UI **Location** → Query **`locationId`**
  
  ### 列表出参
  
  **`items[]` 新增/对齐字段**
  
  | 字段 | 类型 | 说明 |
  |------|------|------|
  | region | string | 适用 Region 展示文案 |
  | location | string | 适用门店展示文案 |
  | regionIds | string[] | Region Id 多选;`ALL` 时为 `[]` |
  | locationIds | string[] | 门店 Id 多选;`ALL` 时为 `[]` |
  | locationText | string | **兼容字段**,与 `location` 相同 |
  
  其它字段不变:`id`(= TemplateCode)、`templateName`、`contentsCount`、`sizeText`、`versionNo`、`lastEdited` 等。
  
  **列表响应示例片段**
  
  ```json
  {
    "pageIndex": 1,
    "pageSize": 10,
    "totalCount": 2,
    "items": [
      {
        "id": "TPL_ALL",
        "templateCode": "TPL_ALL",
        "templateName": "Global Template",
        "labelType": "PRICE",
        "region": "All Regions",
        "location": "All Locations",
        "locationText": "All Locations",
        "regionIds": [],
        "locationIds": [],
        "contentsCount": 5,
        "sizeText": "4x6inch",
        "versionNo": 1,
        "lastEdited": "2026-05-26T10:00:00"
      },
      {
        "id": "TPL_TEST_001",
        "templateName": "Price Tag 4x6",
        "region": "East Region, West Region",
        "location": "UNCC store, Central Park Store",
        "locationText": "UNCC store, Central Park Store",
        "regionIds": ["fl_group_id_east", "fl_group_id_west"],
        "locationIds": [
          "11111111-1111-1111-1111-111111111111",
          "22222222-2222-2222-2222-222222222222"
        ],
        "contentsCount": 3,
        "sizeText": "4x6inch",
        "versionNo": 2,
        "lastEdited": "2026-05-26T11:30:00"
      }
    ]
  }
  ```
  
  ### 详情出参(`GET /api/app/label-template/{id}`)
  
  在原有 `elements`、`templateProductDefaults`、`appliedLocationType` 等基础上增加:
  
  | 字段 | 类型 | 说明 |
  |------|------|------|
  | region | string | 展示文案 |
  | location | string | 展示文案 |
  | regionIds | string[] | Region Id 多选 |
  | groupIds | string[] | 与 `regionIds` 相同(兼容) |
  | locationIds | string[] | 门店 Id 多选 |
  | appliedLocationIds | string[] | 与 `locationIds` 一致(编辑器回显) |
  
  ### 展示规则
  
  | appliedLocation | region | location | regionIds / locationIds |
  |-----------------|--------|----------|-------------------------|
  | **ALL** | `All Regions` | `All Locations` | 空数组 `[]` |
  | **SPECIFIED** | 绑定门店 `location.GroupName` 去重后 `, ` 拼接 | 门店名(优先 `LocationName`,否则 `LocationCode`)拼接 | 由绑定门店反推 / 直接为绑定 Id |
  | **SPECIFIED** 无绑定 | `无` | `无` | `[]` |
  
  `regionIds` 由 `locationIds` 反查 `fl_group` 得到(与 product-category、label-type 一致)。
  
  ### 编辑说明
  
  - `PUT` Body 字段与 `POST` 相同;传 `regionIds` / `locationIds` 会 **全量替换** 模板适用门店(先删 `fl_label_template_location` 再插入)。
  - `elements` 仍为全量重建;`templateProductDefaults` 仅当 Body **显式包含** 该字段时才重建,避免普通保存误清空。
  - 编辑成功 **`versionNo` +1**
  
  ### 联调注意
  
  | 现象 | 处理 |
  |------|------|
  | 列表无 `regionIds` | 确认已部署含本变更的后端 |
  | 传 `groupId` 列表仍很多 | 正常:`appliedLocation=ALL` 的模板始终可见 |
  | 传 `groupId` 列表为空 | 检查 Region 是否存在、其下是否有门店;无效 Region 时仅剩 ALL 模板 |
  | 传了 Region 仍显示 All Locations | 检查 Region Id 是否有效、是否能在库中展开到门店 |
  | 仅 `appliedLocationIds` 不传 `locationIds` | 仍支持,与 `locationIds` 合并 |
  | 前端编辑器仍传 `appliedLocation: "ALL"` | 管理端若需多选,须在 Body 增加 `regionIds` / `locationIds`(见 `labelTemplateService.ts`) |
  | 指定范围但 0 门店 | 后端报错,需至少 1 个有效门店 |
  
  ### 与 product-category / label-type 的关系
  
  逻辑与 **`5-17接口优化.md`** 中 product-category、label-type 的 Region·Location 绑定一致,差异仅为:
  
  | 模块 | 范围字段名 | 关联表 |
  |------|------------|--------|
  | product-category | `availabilityType` | `fl_product_category_location` |
  | label-type | `availabilityType` | `fl_label_type_location` |
  | **label-template** | **`appliedLocation`** | **`fl_label_template_location`** |
  
  ---
  
  ## rbac-role accessPermissions 修复
  
  **应用服务**`RbacRoleAppService`  
  **影响接口**`POST` / `PUT /api/app/rbac-role/{id}`、`GET` 列表/详情回显
  
  ### 问题与根因
  
  | 现象 | 根因 |
  |------|------|
  | 保存报 `accessPermissions 未匹配到任何菜单` | 前端提交 **JSON 数组字符串**(如 `["manage_labels",...]`),旧逻辑按逗号拆分,解析结果带 `["` 引号,无法匹配 |
  | 传 `manage_labels` 等仍无菜单 | 表单权限码为 **UI 编码**(`manage_labels`),菜单侧为 **`menu.labels`**(由 `Menu.Router` 推导);二者未做映射 |
  | 详情 `accessPermissionCodes` 为空 | 新增/编辑未写入 **`Role.AccessPermissionCodes`**(JSON 列),仅依赖 `RoleMenu` 反查 |
  
  ### 变更说明
  
  | 项 | 变更后 |
  |----|--------|
  | **入参解析** | `accessPermissions` 支持 **JSON 数组字符串**、逗号分隔、以及 Body 字段 **`accessPermissionCodes`** 数组 |
  | **菜单绑定** | UI 权限码经 **`RoleAccessPermissionMenuMapping`** 映射到 `Menu.Router`,再写入 **`RoleMenu`** |
  | **落库** | 同时将勾选的 UI 编码写入 **`Role.AccessPermissionCodes`**(JSON 数组),供 GET 回显 |
  | **PermissionCode 为空** | 仍可按 **`Router`** 推导 `menu.xxx`(建议执行 `menu_backfill_permission_code.sql`) |
  
  ### UI 权限码 → 菜单 Router 映射(当前库)
  
  | accessPermissions(UI) | 绑定菜单 Router |
  |-------------------------|-----------------|
  | `manage_labels` | `/labeling`、`/labels`、`/label-categories`、`/label-types`、`/label-templates` |
  | `manage_people` | `/account-management` |
  | `edit_settings` | `/menu-management`、`/multiple-options` |
  | `view_reports` | `/reports` |
  | `manage_products` | (当前 `Menu` 表无 Products 路由,勾选不绑定菜单,**不单独报错**) |
  | `approve_batches` | (当前无对应菜单路由,同上) |
  
  > 至少 **1 个** 权限码能匹配到菜单即保存成功;若 **全部** 均无法匹配(例如只勾 `manage_products` 且库中无对应菜单),仍返回业务错误。
  
  ### 请求示例(与前端一致)
  
  ```http
  PUT /api/app/rbac-role/3a1f077b-3665-63f2-5fea-0fd7e7044b88
  Content-Type: application/json
  Authorization: Bearer {token}
  ```
  
  ```json
  {
    "roleName": "Partner Admin",
    "roleCode": "admin",
    "remark": "Admin",
    "dataScope": 0,
    "state": true,
    "orderNum": 999,
    "accessPermissions": "[\"manage_labels\",\"edit_settings\",\"view_reports\",\"manage_people\",\"manage_products\",\"approve_batches\"]"
  }
  ```
  
  也可使用逗号分隔(旧格式):
  
  ```json
  {
    "accessPermissions": "manage_labels, view_reports, manage_people"
  }
  ```
  
  或同时传数组字段(与 `accessPermissions` 合并去重):
  
  ```json
  {
    "accessPermissionCodes": ["manage_labels", "view_reports"]
  }
  ```
  
  ### 入参优先级(与 5-18 一致)
  
  | menuIds | accessPermissions / accessPermissionCodes | 行为 |
  |---------|-------------------------------------------|------|
  | 非空数组 | 任意 | **以 menuIds 为准** |
  | 不传 | 非空 | 按 UI 权限码映射菜单并覆盖 `RoleMenu` |
  | 不传 | `""` 或空数组 | 清空 `RoleMenu` 与 `AccessPermissionCodes` |
  | `[]` | 不传 | 清空绑定 |
  
  ### 响应回显
  
  | 字段 | 说明 |
  |------|------|
  | `accessPermissionCodes` | 来自 **`Role.AccessPermissionCodes`**,如 `["manage_labels","view_reports"]` |
  | `accessPermissions` | 已绑定菜单的 **`menu.xxx`** 汇总(逗号拼接,只读展示) |
  | `menuIds` | 已绑定菜单 Guid 列表(`RoleMenu`) |
  
  ### 数据库准备(推荐)
  
  ```bash
  美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/scripts/menu_backfill_permission_code.sql
  ```
  
  ### 联调注意
  
  | 现象 | 处理 |
  |------|------|
  | 仍报未匹配到菜单 | 确认已部署本修复;检查 `Menu` 是否存在上表 Router |
  | 只勾 Products/Batches 报错 | 当前库无对应菜单属预期;请同时勾选 Labels/Reports 等 |
  | 同时传 `menuIds: []` | **menuIds 优先**,会清空绑定并忽略 accessPermissions |
  
  > 更完整的 RBAC 说明见 **`5-18接口优化.md`** → rbac-role 章节。
  
  ---
  
  ## auth-scope 登录后 Company · Region · Location 级联选店
  
  **应用服务**`AuthScopeAppService`  
  **适用场景**:Web `POST /api/app/account/login` 或 App `POST /api/app/us-app-auth/login` 取得 Token 后,**管理员**无 `userlocation` 绑定时需先选工作门店;亦支持非管理员在数据范围内级联选择(须已绑定该门店)。
  
  **命名约定**(与 5-17 一致):UI **Company** = **`partnerId`**`fl_partner.Id`);UI **Region** = **`groupId`**(`fl_group.Id`);UI **Location** = **`locationId`**(`location.Id`,Guid 字符串)。
  
  ### 接口一览
  
  | 步骤 | 方法 | 路径 | 说明 |
  |------|------|------|------|
  | 1 | GET | `/api/app/auth-scope/companies` | 可选公司列表 |
  | 2 | GET | `/api/app/auth-scope/regions?partnerId={partnerId}` | 指定公司下 Region |
  | 3 | GET | `/api/app/auth-scope/locations?partnerId={partnerId}&groupId={groupId}` | 指定公司+Region 下门店 |
  | 4 | POST | `/api/app/auth-scope/select-location` | 确认当前工作门店 |
  | — | GET | `/api/app/auth-scope/current-scope` | 查询已选工作门店(未选返回 `null`) |
  
  **鉴权**:均需 `Authorization: Bearer {token}`
  
  ### 数据范围
  
  | 角色 | Company | Region | Location |
  |------|---------|--------|----------|
  | **管理员**`admin` / 用户名 `admin` / 权限 `*:*:*`) | 全部未删除公司 | 该公司下全部 Region | 该 Region 下全部门店(`location.Partner` + `location.GroupName` 与 `fl_group` 一致) |
  | **非管理员** | `userlocation` 绑定门店所属公司 | 绑定门店对应 Region | 上述 Region 内且符合 `LocationRegionScopeHelper` 的门店;**选店时**须已绑定该 `locationId` |
  
  ### 1)公司列表
  
  ```http
  GET /api/app/auth-scope/companies HTTP/1.1
  Authorization: Bearer {token}
  ```
  
  **响应**`AuthScopeCompanyOptionDto[]`
  
  ```json
  [
    { "id": "fl_partner_id_1", "partnerName": "Acme Foods", "state": true }
  ]
  ```
  
  ### 2)Region 列表
  
  ```http
  GET /api/app/auth-scope/regions?partnerId=fl_partner_id_1 HTTP/1.1
  Authorization: Bearer {token}
  ```
  
  **响应**`AuthScopeRegionOptionDto[]`
  
  ```json
  [
    { "id": "fl_group_id_east", "groupName": "East Region", "partnerId": "fl_partner_id_1", "state": true }
  ]
  ```
  
  ### 3)门店列表
  
  ```http
  GET /api/app/auth-scope/locations?partnerId=fl_partner_id_1&groupId=fl_group_id_east HTTP/1.1
  Authorization: Bearer {token}
  ```
  
  **响应**`AuthScopeLocationOptionDto[]`(含 `fullAddress`、`groupName` 等)
  
  ### 4)确认选店(与现有 App 逻辑对齐)
  
  ```http
  POST /api/app/auth-scope/select-location HTTP/1.1
  Authorization: Bearer {token}
  Content-Type: application/json
  ```
  
  ```json
  {
    "partnerId": "fl_partner_id_1",
    "groupId": "fl_group_id_east",
    "locationId": "a2696b9e-2277-11f1-b4c6-00163e0c7c4f"
  }
  ```
  
  **响应**`AuthScopeSelectLocationOutputDto`
  
  | 字段 | 说明 |
  |------|------|
  | partnerId / partnerName | 所选公司 |
  | groupId / groupName | 所选 Region |
  | location | 与 **`UsAppBoundLocationDto`** 相同(`id`、`locationCode`、`locationName`、`fullAddress`、`state`) |
  
  **选店后的服务端行为**(无需改前端即可对接 App):
  
  | 能力 | 行为 |
  |------|------|
  | **工作范围缓存** | 写入分布式缓存(24h);退出 `POST /api/app/auth-session/logout` 时清除 |
  | **`GET /api/app/us-app-auth/my-locations`** | 管理员在缓存选店后,列表 **合并** 该门店(与 `userlocation` 并集) |
  | **`GET .../location-detail/{locationId}`** | 管理员可不依赖 `userlocation` 访问已选门店(`UsAppPrintLogScopeHelper.EnsureUserCanAccessLocationAsync`) |
  | **App 打印/报表** | 仍传 `locationId`;权限规则不变(见 `5-18接口优化.md`) |
  
  ### 5)当前工作范围
  
  ```http
  GET /api/app/auth-scope/current-scope HTTP/1.1
  Authorization: Bearer {token}
  ```
  
  未选店时响应体为 **`null`**(HTTP 200)。
  
  ### 联调注意
  
  | 现象 | 处理 |
  |------|------|
  | `regions` 为空 | 公司下无 `fl_group` 或当前账号无 Region 数据范围 |
  | `locations` 为空 | 门店 `Partner` / `GroupName` 未与 `fl_partner`、`fl_group` 对齐 |
  | 选店报「门店与所选公司/区域不匹配」 | 检查 `location.Partner`、`location.GroupName` |
  | 非管理员选店报未绑定 | 须在 **Team Member** 中为该账号绑定该门店 |
  | 选店后 `my-locations` 仍为空 | 确认已调 `select-location` 且 Token 为管理员身份 |
  
  > Web 管理端报表等模块仍可按 Query 传 `partnerId` / `groupId` / `locationId` 收窄;本组接口主要解决 **登录后选工作门店** 与 **App 门店列表** 一致性问题。
  
  ---
  
  ## us-app-auth App 管理员级联选店
  
  **应用服务**`UsAppAuthAppService`  
  **适用场景**:App 使用 **`POST /api/app/us-app-auth/login`** 登录后,持 **管理员** 身份(`admin` 角色 / 用户名 `admin` / 权限 `*:*:*`)且 JWT 含 **`client_kind=us-app`**,按 Company → Region 筛选门店。
  
  **与 `auth-scope` 关系**:查询逻辑共用 `AuthScopeQueryHelper`;App 侧路径统一在 **`us-app-auth`** 下,并 **强制 App Token + 管理员**,避免误用 Web Token。
  
  ### 接口一览
  
  | 步骤 | 方法 | 路径 | 说明 |
  |------|------|------|------|
  | 0 | POST | `/api/app/us-app-auth/login` | 获取 App Token(须管理员账号) |
  | 1 | GET | `/api/app/us-app-auth/admin-scope-companies` | 公司列表 → 取 `id` 作 `partnerId` |
  | 2 | GET | `/api/app/us-app-auth/admin-scope-regions?partnerId={partnerId}` | Region 列表 → 取 `id` 作 `groupId` |
  | 3 | GET | `/api/app/us-app-auth/admin-scope-locations?partnerId={partnerId}&groupId={groupId}` | **按公司与 Region Id 筛选门店** |
  | 4 | POST | `/api/app/us-app-auth/select-admin-scope-location` | 确认工作门店 |
  | — | GET | `/api/app/us-app-auth/my-locations` | 选店后刷新绑定门店(含缓存门店) |
  
  **鉴权**:步骤 1–4 须 Header `Authorization: Bearer {App登录返回的token}`
  
  ### 前置条件
  
  | 项 | 要求 |
  |----|------|
  | Token 来源 | 必须来自 **`/api/app/us-app-auth/login`**(非 Web `/api/app/account/login`) |
  | JWT 声明 | `client_kind` = `us-app` |
  | 角色 | 平台管理员(`ReportsRoleHelper.IsAdminRole`) |
  | 违反时 | `请使用 App 登录令牌调用该接口` 或 `仅管理员可使用公司/区域/门店筛选接口` |
  
  ### 1)公司列表
  
  ```http
  GET /api/app/us-app-auth/admin-scope-companies HTTP/1.1
  Authorization: Bearer {app_token}
  ```
  
  **响应**`AuthScopeCompanyOptionDto[]`(与 auth-scope 相同)
  
  ```json
  [
    { "id": "fl_partner_id_1", "partnerName": "Acme Foods", "state": true }
  ]
  ```
  
  ### 2)Region 列表
  
  ```http
  GET /api/app/us-app-auth/admin-scope-regions?partnerId=fl_partner_id_1 HTTP/1.1
  Authorization: Bearer {app_token}
  ```
  
  **响应**`AuthScopeRegionOptionDto[]`
  
  ```json
  [
    { "id": "fl_group_id_east", "groupName": "East Region", "partnerId": "fl_partner_id_1", "state": true }
  ]
  ```
  
  ### 3)门店列表(按 partnerId + groupId 筛选)
  
  ```http
  GET /api/app/us-app-auth/admin-scope-locations?partnerId=fl_partner_id_1&groupId=fl_group_id_east HTTP/1.1
  Authorization: Bearer {app_token}
  ```
  
  **Query**
  
  | 参数 | 必填 | 说明 |
  |------|------|------|
  | partnerId | 是 | 公司 Id(`fl_partner.Id`) |
  | groupId | 是 | Region Id(`fl_group.Id`) |
  
  **响应**`AuthScopeLocationOptionDto[]`
  
  ```json
  [
    {
      "id": "a2696b9e-2277-11f1-b4c6-00163e0c7c4f",
      "locationCode": "LOC-1",
      "locationName": "Downtown Kitchen",
      "fullAddress": "123 Main St, New York, NY 10001",
      "state": true,
      "partnerId": "fl_partner_id_1",
      "groupId": "fl_group_id_east",
      "groupName": "East Region"
    }
  ]
  ```
  
  筛选规则:`location.Partner` 匹配该公司(Id 或名称),且 `location.GroupName` 与所选 `fl_group.GroupName` 一致。
  
  ### 4)确认选店
  
  ```http
  POST /api/app/us-app-auth/select-admin-scope-location HTTP/1.1
  Authorization: Bearer {app_token}
  Content-Type: application/json
  ```
  
  ```json
  {
    "partnerId": "fl_partner_id_1",
    "groupId": "fl_group_id_east",
    "locationId": "a2696b9e-2277-11f1-b4c6-00163e0c7c4f"
  }
  ```
  
  **响应**`AuthScopeSelectLocationOutputDto`(含 `location` 节点,结构同 `UsAppBoundLocationDto`
  
  ### 推荐调用顺序(App)
  
  ```text
  POST /api/app/us-app-auth/login
    → GET  admin-scope-companies
    → GET  admin-scope-regions?partnerId=...
    → GET  admin-scope-locations?partnerId=...&groupId=...
    → POST select-admin-scope-location
    → GET  my-locations
    → 后续业务接口传 locationId(打印、报表等,规则不变)
  ```
  
  ### 联调注意
  
  | 现象 | 处理 |
  |------|------|
  | 报「请使用 App 登录令牌」 | 勿用 Web `account/login` 的 Token;须重新 App 登录 |
  | 报「仅管理员可使用」 | 换管理员账号或绑定 `admin` 角色 |
  | `locations` 为空 | 核对门店 `Partner`、`GroupName` 与 `fl_partner`、`fl_group` |
  | 与 auth-scope 重复 | App 端 **优先** 使用本节前缀;Web 端用 `auth-scope` |
  
  ---
  
  ## 变更记录
  
  | 日期 | 说明 |
  |------|------|
  | 2026-05-26 | us-app-auth:App 管理员 `admin-scope-companies/regions/locations`、`select-admin-scope-location` |
  | 2026-05-26 | auth-scope:登录后 Company/Region/Location 级联选店;选店缓存;`my-locations` / 门店详情与管理员选店对齐 |
  | 2026-05-26 | product-category:`categoryCode` 新增/编辑改为可选 |
  | 2026-05-26 | label-template:新增/编辑/列表/详情支持 `regionIds`、`locationIds` 及 `region`、`location` 展示 |
  | 2026-05-26 | label-template 列表 Query 增加 `groupId`(Region)、`locationId`(门店)筛选 |
  | 2026-05-26 | rbac-role:支持 accessPermissions JSON 数组 + UI 权限码映射 Menu;落库 AccessPermissionCodes |