14afbc16
李曜臣
2026-6-22
|
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
|
# 6-18 代码优化
本文档说明 **2026-06-18** 对 **`GET /api/app/label-template`** 列表接口的第二轮修复。
6-17 已完成 scope 库结构兼容(未迁移 `fl_label_template_partner` / `fl_label_template_region` 时仍可查询),但测试环境 Web **Label Templates** 页仍返回 **500**,Network 中:
`GET /api/app/label-template?SkipCount=1&MaxResultCount=10`
测试环境:`http://flus-test.3ffoodsafety.com`
---
## 一、现象
- 页面提示:**Failed to load label templates. Request failed.**
- 接口 HTTP **500**,响应体 `errors` 为空,无具体异常文案。
- 同页 `partner` / `group` / `location` 下拉接口可正常加载。
> `SkipCount=1` 表示**第 1 页**(页码从 1 起),不是报错原因。详见 `Helpers/PagedQueryConvention.cs`。
---
## 二、根因
服务端日志已明确报错:
```text
Unknown column 'AppliedPartnerType' in 'field list'
```
对应 SQL:
```sql
SELECT ... `AppliedLocationType`,`AppliedPartnerType`,`AppliedRegionType`, ...
FROM `fl_label_template`
WHERE NOT ( `IsDeleted`=1 )
ORDER BY IFNULL(`LastModificationTime`,`CreationTime`) DESC
LIMIT 0,10
```
### 2.1 主因:ORM 实体仍映射不存在列
6-17 曾在 `FlLabelTemplateDbEntity` 上对 `AppliedPartnerType` / `AppliedRegionType` 标记 `[SugarColumn(IsIgnore = true)]`,但当前 SqlSugar 运行时**仍会**把这两列拼进 `SELECT`(与 `fl_label.AppliedRegionType` 的 `IsIgnore` 行为不一致),导致未执行 `fl_label_template_scope.sql` 的库直接 500。
**MCP 查库(测试库)**:
- `fl_label_template` **无** `AppliedPartnerType` / `AppliedRegionType` 列
- **无** `fl_label_template_partner` / `fl_label_template_region` 表
- **有** `AppliedLocationType` 与 `fl_label_template_location`
### 2.2 次因:列表 SQL 其它隐患(一并修复)
| # | 问题 | 后果 |
|---|------|------|
| 1 | 默认排序曾用 `LastModificationTime ?? CreationTime` | SqlSugar 翻译失败 → 500 |
| 2 | `Sorting` 直接 `OrderBy(input.Sorting)` 拼 SQL | 非法字段 / 注入风险 |
| 3 | 空 `templateIds` 仍 `Contains` 查询 | 可能生成 `IN ()` |
| 4 | 无效 partner/group 筛选未安全降级 | 未捕获异常 |
---
## 三、修复说明
### 1. 从 ORM 实体移除不存在列 + 强制列投影(核心)
| 改动 | 说明 |
|------|------|
| `FlLabelTemplateDbEntity` | **删除** `AppliedPartnerType` / `AppliedRegionType` 属性 |
| **`LabelTemplateQueryHelper.ProjectListColumns`** | 列表/详情/重复校验等只读查询 **显式 Select** 真实列,SQL 不再出现 scope 列 |
| `LabelTemplateScopeSchemaHelper` | 探测列/表;已迁移时用 raw SQL 写入 scope 列 |
| `LabelTemplateAppService` | `GetListAsync` 在排序后调用 `ProjectListColumns` |
> **重要**:若 Yi-SQL 日志仍出现 `AppliedPartnerType`,说明进程加载的是**旧 DLL**(`FoodLabeling.Application` 未重新编译或未重启)。请先 `dotnet build` 通过后再**完全停止并重启** `Yi.Abp.Web`。
### 2. 安全排序
新增 `ApplyLabelTemplateListSorting`:
- 默认:`ORDER BY IFNULL(LastModificationTime, CreationTime) DESC, TemplateCode ASC`
- `Sorting` 白名单字段 + asc/desc
### 3. 列表查询健壮性
| 改动 | 说明 |
|------|------|
| `input ??= new()`、`pageSize` 默认 10 | 入参/分页兜底 |
| `templateIds.Count > 0` 再查 elements/items | 避免空 `IN ()` |
| `ResolveFilteredLocationIdsForListAsync` | 无效 partner/group 返回空列表 |
| `DeleteAsync` | 仅当 scope 关联表存在时才删除 partner/region 行 |
### 4. 与 6-17 的关系
6-17 用 `IsIgnore` 试图跳过列映射,**实测无效**;6-18 改为**实体不含列 + raw SQL 按需写入**,与 `fl_label.AppliedRegionType` 处理方式一致。
---
## 四、接口说明
| 项目 | 内容 |
|------|------|
| 方法 | `GET` |
| 路径 | `/api/app/label-template` |
| 鉴权 | Bearer Token(`Authorization: {data.token}`,`data.token` 已含 `Bearer ` 前缀) |
### 入参(Query)
| 参数 | 类型 | 必填 | 说明 |
|------|------|------|------|
| `SkipCount` | int | 否 | **页码,从 1 起**;第一页传 `1` |
| `MaxResultCount` | int | 否 | 每页条数;`<=0` 时后端按 **10** 处理 |
| `Keyword` | string | 否 | 模板名称 / 编码模糊搜索 |
| `PartnerId` | string | 否 | 按 Company(`fl_partner.Id`)筛选 |
| `GroupId` | string | 否 | 按 Region(`fl_group.Id`)筛选 |
| `LocationId` | string | 否 | 按门店(`location.Id`)筛选;**优先于** Partner/Region |
| `LabelType` | string | 否 | 如 `PRICE` / `NUTRITION` |
| `State` | bool | 否 | 启用状态 |
| `Sorting` | string | 否 | 白名单:`TemplateName asc/desc`、`TemplateCode asc/desc`、`CreationTime asc/desc`、`LastModificationTime asc/desc`;其它值忽略并走默认排序 |
筛选解析顺序:**LocationId → GroupId → PartnerId**;均未传则不按门店范围收窄(仍返回全部未删除模板,除非前端传了无效 Id 则返回空列表)。
### 出参(`PagedResultWithPageDto<LabelTemplateGetListOutputDto>`)
| 字段 | 说明 |
|------|------|
| `pageIndex` | 当前页码 |
| `pageSize` | 每页条数 |
| `totalCount` | 总条数 |
| `totalPages` | 总页数 |
| `items[]` | 模板列表 |
**`items[]` 主要字段**
| 字段 | 说明 |
|------|------|
| `id` / `templateCode` | 模板编码(前端主键) |
| `templateName` | 模板名称 |
| `company` / `region` / `location` | 适用范围展示;未迁移 scope 库时 Company/Region 为 `All Companies` / `All Regions` |
| `partnerIds` / `regionIds` / `locationIds` | 对应 Id 数组 |
| `items` / `itemNames` | 模板内控件名称 |
| `contentsCount` | 控件数量 |
| `sizeText` | 如 `2x2inch` |
| `lastEdited` | 最近编辑时间(`LastModificationTime ?? CreationTime`) |
---
## 五、请求示例
### 登录获取 Token
```bash
curl -X POST "http://flus-test.3ffoodsafety.com/api/oauth/Login" \
-H "Content-Type: application/x-www-form-urlencoded" \
-d "userName=admin&password=123456"
```
### 列表(第一页,默认排序)
```bash
curl -G "http://flus-test.3ffoodsafety.com/api/app/label-template" \
-H "Authorization: Bearer {token}" \
--data-urlencode "SkipCount=1" \
--data-urlencode "MaxResultCount=10"
```
### 带 Company 筛选
```bash
curl -G "http://flus-test.3ffoodsafety.com/api/app/label-template" \
-H "Authorization: Bearer {token}" \
--data-urlencode "SkipCount=1" \
--data-urlencode "MaxResultCount=10" \
--data-urlencode "PartnerId={fl_partner.Id}"
```
### 指定排序
```bash
curl -G "http://flus-test.3ffoodsafety.com/api/app/label-template" \
-H "Authorization: Bearer {token}" \
--data-urlencode "SkipCount=1" \
--data-urlencode "MaxResultCount=10" \
--data-urlencode "Sorting=TemplateName asc"
```
### 响应片段(示例)
```json
{
"pageIndex": 1,
"pageSize": 10,
"totalCount": 3,
"totalPages": 1,
"items": [
{
"id": "tpl_n0b5h9_mpyssitm",
"templateCode": "tpl_n0b5h9_mpyssitm",
"templateName": "Retail Label w/Price Copy",
"company": "All Companies",
"region": "All Regions",
"location": "Ordos Airport, Store B",
"items": "Label Name, Price, Barcode",
"contentsCount": 5,
"sizeText": "2x2inch",
"lastEdited": "2026-06-04T08:30:00"
}
]
}
```
---
## 六、验证步骤
0. **重新编译并重启**(必做):
```bash
cd "美国版/Food Labeling Management Code/Yi.Abp.Net8/src/Yi.Abp.Web"
dotnet build
```
停止正在运行的 Web 进程后重新启动;确认 Yi-SQL 中 **不再出现** `AppliedPartnerType`。
1. **部署**包含 6-17 + 6-18 的后端并重启。
2. 调用 `GET /api/app/label-template?SkipCount=1&MaxResultCount=10` → 应 **200**,`totalCount >= 0`。
3. Web **Label Templates** 列表可加载,不再出现红色 **Failed to load label templates**。
4. 传无效 `PartnerId` / `GroupId` → **200** 且 `items=[]`(非 500)。
5. 不传 `Sorting` → 按最近编辑时间降序;传 `Sorting=TemplateName asc` → 按名称升序。
6. 分页:第 2 页 `SkipCount=2`,`totalCount` 与 UI 一致。
### SQL 抽查(修复后 ORM 应生成的列)
```sql
SELECT Id, TemplateCode, TemplateName, AppliedLocationType,
IFNULL(LastModificationTime, CreationTime) AS LastEdited
FROM fl_label_template
WHERE IsDeleted = 0
ORDER BY IFNULL(LastModificationTime, CreationTime) DESC, TemplateCode ASC
LIMIT 10;
```
**不应再出现** `AppliedPartnerType` / `AppliedRegionType`。
### 检查 scope 是否已迁移
```sql
SELECT COUNT(*) AS scope_table_cnt
FROM information_schema.TABLES
WHERE TABLE_SCHEMA = DATABASE()
AND TABLE_NAME IN ('fl_label_template_partner', 'fl_label_template_region');
```
`scope_table_cnt = 0` 时行为与 6-17 一致:仅 Location 维度落库与展示。
---
## 七、涉及代码
| 文件 | 说明 |
|------|------|
| `Helpers/LabelTemplateQueryHelper.cs` | **新增** `ProjectListColumns` 强制 SQL 列白名单 |
| `Services/DbModels/FlLabelTemplateDbEntity.cs` | **移除** Partner/Region 列属性 |
| `Helpers/LabelTemplateScopeSchemaHelper.cs` | 列/表探测 + `SetAppliedScopeTypesAsync` raw SQL |
| `Services/LabelTemplateAppService.cs` | 安全排序、Create/Update 写 scope 列、列表健壮性 |
| `Helpers/LabelTemplateScopeHelper.cs` | 未迁移库 scope 过滤/展示(6-17) |
| `Helpers/LocationScopeBindingHelper.cs` | `ResolveFilteredLocationIdsForListAsync` |
| `Helpers/LabelTemplateListItemsHelper.cs` | Items 列 |
| `Dtos/LabelTemplate/LabelTemplateGetListInputVo.cs` | `PartnerId` 筛选 |
---
## 八、数据库迁移(可选)
需完整 Company / Region 三维 scope 时,执行:
`美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/scripts/fl_label_template_scope.sql`
未执行前:**列表可正常返回**(6-17 + 6-18),但无法持久化 Company/Region 多选明细。
---
## 九、App 标签预览 `POST /api/app/us-app-labeling/preview`
### 现象
App 标签预览页调用 preview 失败(500 或 400),常与 **label-template 列表** 同源:加载模板头时 ORM 仍 SELECT 不存在的 `AppliedPartnerType` / `AppliedRegionType`。
另有两项逻辑缺陷(与 `6-11` 文档不一致):
| # | 问题 | 后果 |
|---|------|------|
| 1 | `LabelAppService.PreviewAsync` 全表查询 `FlLabelTemplateDbEntity` | 未迁移 scope 列时 **Unknown column** → 500 |
| 2 | `UsAppLabelingAppService.PreviewAsync` 未向 `_labelAppService.PreviewAsync` 传 **`locationId`** | 模板含 Company 自动生成元素时报 **「预览/打印需要 locationId 以填充 Company 信息」** |
| 3 | 出参 **`labelId`** 仍返回 `fl_label.Id`(GUID) | 与 Print Log 当日序号 `yyyyMMdd-n` 不一致 |
### 修复说明
| 改动 | 说明 |
|------|------|
| `LabelAppService.PreviewAsync` | 模板头查询改用 `LabelTemplateQueryHelper.ProjectListColumns` |
| `UsAppLabelingAppService.PreviewAsync` | 传入 `LocationId`;`labelId` 改用 `ReportsPrintLogDailyLabelIdHelper.ResolveNextDailyLabelIdAsync` |
| `UsAppLabelingAppService.PrintAsync` | 解析模板时同步传入 `LocationId`(打印与预览 Company 填充一致) |
| `DashboardAppService` | 模板统计 Count 同样走 `ProjectListColumns` |
### 接口说明
| 项目 | 内容 |
|------|------|
| 方法 | `POST` |
| 路径 | `/api/app/us-app-labeling/preview` |
| 鉴权 | App Bearer Token |
#### 入参(Body:`UsAppLabelPreviewInputVo`)
| 字段 | 类型 | 必填 | 说明 |
|------|------|------|------|
| `locationId` | string | 是 | 当前门店 Id(`location.Id`) |
| `labelCode` | string | 是 | 标签编码(`fl_label.LabelCode`) |
| `productId` | string | 否 | 预览产品 Id;不传则取标签绑定第一个产品 |
| `baseTime` | DateTime | 否 | 日期/时间控件基准;也用于计算当日 `labelId` 序号;未传为服务器当前时间 |
| `printInputJson` | object | 否 | `PRINT_INPUT` 元素用户输入 |
#### 出参(`UsAppLabelPreviewDto`)
| 字段 | 说明 |
|------|------|
| **`labelId`** | 门店当日**下一个**打印序号 `yyyyMMdd-n`(预览不落库) |
| `locationId` / `labelCode` | 回传入参 |
| `template` | 已解析 AUTO_DB / PRINT_INPUT 的模板结构(含 Company 自动填充) |
| `labelLastEdited` | 标签最近编辑时间 |
| 其它 | `typeName`、`productName`、`templateProductDefaultValues` 等 |
#### `labelId` 规则
与 `6-11`、`get-print-log-list` 一致:`{baseTime 日期 yyyyMMdd}-{当日已有打印任务数 + 1}`。
#### Company 自动元素
模板含 Company 自动生成控件时,后端根据 **`locationId`** 查 `fl_partner` 填充 `config.text`(详见 `6-11` 第二节)。
### 请求示例
```bash
curl -X POST "http://flus-test.3ffoodsafety.com/api/app/us-app-labeling/preview" \
-H "Authorization: Bearer {token}" \
-H "Content-Type: application/json" \
-d '{
"locationId": "550e8400-e29b-41d4-a716-446655440000",
"labelCode": "LBL0001",
"productId": "PROD001",
"baseTime": "2026-06-18T09:00:00"
}'
```
### 响应片段(示例)
```json
{
"labelId": "20260618-3",
"locationId": "550e8400-e29b-41d4-a716-446655440000",
"labelCode": "LBL0001",
"labelLastEdited": "2026-06-01T08:30:09",
"template": {
"id": "tpl_xxx",
"width": 2,
"height": 2,
"unit": "inch",
"elements": []
}
}
```
### 验证步骤
1. 停服 → `dotnet build` → 重启(同 label-template 一节)。
2. App 进入标签预览页,Network 中 preview 应 **200**。
3. 响应 `labelId` 为 `yyyyMMdd-n`,非 GUID。
4. 模板含 Company 元素时,`template.elements` 中对应 `config.text` 为门店所属公司名。
5. 当日已有 N 条打印任务时,preview 返回 `-{N+1}`。
### 涉及代码(preview)
| 文件 | 说明 |
|------|------|
| `Services/UsAppLabelingAppService.cs` | `PreviewAsync` / `PrintAsync` 传 `LocationId`、当日 `labelId` |
| `Services/LabelAppService.cs` | `PreviewAsync` 模板头 `ProjectListColumns` |
| `Helpers/LabelTemplateQueryHelper.cs` | 列投影 |
| `Helpers/ReportsPrintLogDailyLabelIdHelper.cs` | `ResolveNextDailyLabelIdAsync` |
| `Helpers/PartnerCompanyDisplayHelper.cs` | Company 自动填充 |
---
## 十、App 打印日志 `POST /api/app/us-app-labeling/get-print-log-list`
### 现象
Postman 传 `printDate: "2026-06-16"` 返回 `totalCount: 0`,误以为接口异常。
### 根因(查库核对)
门店 `3a218397-8dda-a378-e024-ef89bcef8d24` 在测试库中:
| 自然日(`DATE(IFNULL(PrintedAt, CreationTime))`) | 记录数 |
|---------------------------------------------------|--------|
| `2026-06-01` | **7** |
| `2026-05-31` | **3** |
| `2026-06-16` | **0** |
**接口按入参日期筛选时,该日无数据则返回空列表,行为正确。** 文档/示例误用 `2026-06-16`,应改用 **`2026-06-01`** 验证。
另:`PrintedAt` 入库多为 `null`,筛选实际走 **`CreationTime`**。
### 本轮修复
| 改动 | 说明 |
|------|------|
| 日期条件 | 改用 MySQL `DATE(IFNULL(t.PrintedAt, t.CreationTime)) = 'yyyy-MM-dd'`,避免 DateTime 区间比较时区偏差 |
| `printDateDay` | 新增字符串入参(`yyyy-MM-dd`),优先于 `printDate`,避免 JSON 仅日期 UTC 歧义 |
| 未传日期 | **`printDate` 与 `printDateDay` 均未传时不按日过滤**(返回该门店全部打印记录,兼容 App 未传参) |
| `labelId` | 列表出参改为当日序号 `yyyyMMdd-n`;`labelEntityId` 为 `fl_label.Id` |
### 请求示例(有数据的日期)
```bash
curl -X POST "http://192.168.1.4:19001/api/app/us-app-labeling/get-print-log-list" \
-H "Authorization: Bearer {token}" \
-H "Content-Type: application/json" \
-d '{
"locationId": "3a218397-8dda-a378-e024-ef89bcef8d24",
"skipCount": 1,
"maxResultCount": 20,
"printDateDay": "2026-06-01"
}'
```
或使用 `"printDate": "2026-06-01"`(等价)。
### 权限说明
非 admin / 非 Partner 角色时,仅返回 **`CreatedBy = 当前用户`** 的记录;若 Token 用户不是打印人,即使日期正确也会空列表。
### 部署
修改在 `FoodLabeling.Application`,需 **停服 → dotnet build → 重启** 后 Postman 才生效。
### 涉及代码
| 文件 | 说明 |
|------|------|
| `Helpers/ReportsPrintLogDailyLabelIdHelper.cs` | `ResolvePrintLogFilterCalendarDay`、`ApplyPrintTaskCalendarDayFilter` |
| `Dtos/UsAppLabeling/PrintLogGetListInputVo.cs` | `PrintDateDay` |
| `Services/UsAppLabelingAppService.cs` | `GetPrintLogListAsync` |
---
## 十一、角色编辑 `PUT /api/app/rbac-role/{id}` — accessPermissions
### 现象
编辑角色传 `"accessPermissions": "[\"manage_labels\"]"` 后权限未生效或菜单绑定被清空。
### 根因
前端/Web 约定 `accessPermissions` 为 **JSON 数组字符串**(见 `rbacRoleService.ts` 的 `JSON.stringify(codes)`),后端 `ParseAccessPermissionCodes` 原先仅按逗号拆分,整段 `["manage_labels"]` 被当成一个非法 token,`NormalizeAccessPermissionCodes` 过滤后为空 → **RoleMenu 被清空**。
### 修复
`RbacAccessPermissionHelper.ParseAccessPermissionCodes`:若字符串以 `[` 开头,先 `JsonSerializer.Deserialize<List<string>>`,再回退逗号分隔。
### 合法 accessPermissions 值
| 编码 | 说明 |
|------|------|
| `manage_labels` | 标签相关菜单(/labeling、/labels 等) |
| `manage_people` | 账号管理 |
| `edit_settings` | 菜单/多选项设置 |
| `view_reports` | 报表 |
| `manage_products` / `approve_batches` | 当前无独立菜单,不阻断其它权限 |
### 请求示例
```json
{
"roleName": "Staff",
"roleCode": "Staff",
"remark": null,
"dataScope": 0,
"state": true,
"orderNum": 0,
"accessPermissions": "[\"manage_labels\"]"
}
```
亦可传 `"accessPermissionCodes": ["manage_labels"]`(与 `accessPermissions` 合并去重)。
### 涉及代码
| 文件 | 说明 |
|------|------|
| `Helpers/RbacAccessPermissionHelper.cs` | JSON 数组解析 |
| `Helpers/RoleAccessPermissionMenuMapping.cs` | UI 权限码 → Menu.Router |
| `Services/RbacRoleAppService.cs` | `UpdateAsync` / `ApplyRoleMenuBindingsAsync` |
---
## 十二、Team Member — Company Admin 范围绑定
### 规则
角色为 **Company Admin**(库内常为 **Partner Admin**,或 RoleCode 含 `partner`)时:
| 字段 | 是否必填 | 说明 |
|------|----------|------|
| `partnerId` / `partnerIds` | **是** | 指定适用 Company |
| `regionIds` | **否** | 未传时后端自动绑定该公司下**全部 Region**(出参展示) |
| `locationIds` / `assignedLocations` | **否** | 未传时后端自动绑定该公司下**全部门店** 并写入 `userlocation` |
其它角色仍须 Company / Region / 门店至少一项能解析出门店。
### 请求示例(Create / Update)
```json
{
"fullName": "Jane Doe",
"userName": "jane.doe",
"password": "123456",
"email": "jane@example.com",
"roleId": "{Partner Admin 角色 Guid}",
"partnerId": "{fl_partner.Id}",
"state": true
}
```
无需传 `regionIds`、`locationIds`。
### 列表出参
`GET /api/app/team-member` 对 Company Admin 成员:
- `roleName` 统一为 **Company Admin**(库内 Partner Admin 也会转换)
- `regionIds` 展示该公司下**全部 Region**
- `assignedLocations` 展示该公司下**全部门店**(无需前端再选手动 Region/Locations)
### 编辑弹窗(Web)
选 **Company Admin** 时隐藏 Region / Locations 字段,仅保留 **Company**;保存时只传 `partnerId`,后端自动展开绑定。
### 涉及代码
| 文件 | 说明 |
|------|------|
| `Helpers/TeamMemberRoleHelper.cs` | Company Admin 角色识别 |
| `Helpers/LocationScopeBindingHelper.cs` | `ResolveGroupIdsFromPartnerIdsAsync` |
| `Services/TeamMemberAppService.cs` | 保存/列表/详情 scope 逻辑 |
| `PeopleView.tsx` | 前端 Company Admin 放宽 Region/Locations 必填 |
---
## 十三、Team Member 批量导入 / PDF 导出 — Region 列
### 模板列(下载 `download-team-member-import-template`)
| 列 | 必填 | 说明 |
|----|------|------|
| Name / Email / Role Name | 是 | 与单条创建一致 |
| **Region** | **否** | 可留空;填 `fl_group.Id` 或 Region 名称(GroupName),多个用 `;` 分隔 |
| **Assigned Location Ids** | **否*** | 与 Region **至少填一项**;见下 |
\* Company Admin 在 Web 端可只选 Company;Excel 批量导入仍须 Region 或门店至少一项(或后续扩展 Company 列)。
### Assigned Location Ids 填什么?
**优先填 `location.LocationCode`(门店编码)**,例如 `LOC001`、`33333`。
也支持:
- **`location.Id`(Guid 字符串)**
- **`编码 - 门店名`** 格式(取 ` -` 前一段作为编码或 Guid)
模板示例 `LOC001;LOC002` 即为 **LocationCode**,不是 Guid。
### 涉及代码
| 文件 | 说明 |
|------|------|
| `Helpers/TeamMemberBatchExcelHelper.cs` | Region 列解析、`BuildImportTemplateWorkbook` |
| `Services/TeamMemberAppService.cs` | 导入解析 Region/Location、PDF 增 Region 列 |
---
## 十四、Team Member 列表只返回 1 条(应 3 条)
### 现象
`GET /api/app/team-member?SkipCount=1&MaxResultCount=10` 在 Subway China 下应返回 **123 / Sandi / Tom** 共 3 人,实际 `totalCount=1`。
### 根因
1. **Guid 字符串大小写不一致**:`userlocation.UserId` / `LocationId` 与 `User.Id.ToString()` 比较时用字符串 `Contains`,大小写不同会漏关联,导致成员 scope 命中失败、`assignedLocations` 为空。
2. **前端未传 `PartnerId`**:筛选 Company 时只靠客户端二次过滤,且 `locationIds` 为空时被误过滤。
3. **列表出参缺 `locationIds`**:前端无法正确做门店 scope 匹配。
### 修复
| 改动 | 说明 |
|------|------|
| `TeamMemberListScopeHelper` | 统一 `NormalizeScopeKey` / `UserKey`,用 **Guid** 比较 |
| `BuildFilteredUserQueryAsync` | 按 Guid 解析 scope 与 userlocation |
| `MapUsersToOutputAsync` | assignedMap 用规范化 userKey;出参增加 **`locationIds`** |
| 前端 `getTeamMembers` | 传 **`PartnerId` / `GroupId`** |
| `memberMatchesLocationScope` | 支持 **partnerIds** 匹配 + locationId 小写归一 |
### 验证(测试库 Subway China)
| 用户 | 应出现在 Subway 范围列表 |
|------|--------------------------|
| 123 / Sandi / Tom | 是 |
| Nancy Lang(GongCha) | 否 |
| admin | 仅平台管理员无筛选时可见 |
---
## 十五、Region(Group)列表 Company Admin 只返回 1 条
### 现象
`GET /api/app/group?SkipCount=1&MaxResultCount=10&Sorting=CreationTime+desc` 以 **Company Admin(Subway China / 用户 123)** 登录时,`totalCount=1`,仅 **Subway Beijing**;测试库 Subway China 下应有 **Beijing / Chendu / Shanghai** 共 3 条。
### 根因
`PartnerScopeHelper.ResolveGroupScopeAsync` 对非管理员按 **userlocation 绑定的门店** 推导 Region:只匹配 `location.Partner + location.GroupName` 对应的 `fl_group`。用户 123 仅绑定了 Beijing 门店,因此 scope 只有 **Subway Beijing** 一条,未展开到公司下全部 Region。
|