7083cd6d
李曜臣
优化代码
|
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
|
# 6-23 代码优化
本文档说明 **2026-06-23** 对美国版标签(Label)接口的多门店绑定改造:一个标签可绑定多个门店(`locationIds` 数组),关联表为 `fl_label_location`。
---
## 一、背景与目标
| 项目 | 说明 |
|------|------|
| 原行为 | 标签仅通过 `fl_label.LocationId` 绑定单门店 |
| 新行为 | **`locationIds` 为主字段**,落库 `fl_label_location`;`fl_label.LocationId` 保留为首项兼容列 |
| 兼容 | 未建 `fl_label_location` 时仍读/写 `LocationId`;已建表时列表/筛选/App 打印校验均命中关联表 |
---
## 二、数据库迁移
脚本路径:`美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/scripts/fl_label_location_scope.sql`
| 步骤 | 内容 |
|------|------|
| 1 | 创建 `fl_label_location`(`LabelId` + `LocationId`) |
| 2 | 若关联表为空,将现有 `fl_label.LocationId` 迁移写入关联表 |
**执行前请备份数据库。**
---
## 三、`locationIds` 字段规范
### 入参(POST / PUT)
| 字段 | 类型 | 说明 |
|------|------|------|
| `locationIds` | `string[]` | **主字段**。适用门店 Id(`location.Id`),全量写入 `fl_label_location` |
| `locationId` | `string` | **兼容**。与 `locationIds` 合并;合并后首项写入 `fl_label.LocationId` |
| `appliedRegionType` | `string` | `ALL` / `SPECIFIED` |
| `regionIds` / `groupIds` | `string[]` | Region(`fl_group.Id`),落库 `fl_label_region`;可与 `locationIds` 组合校验 |
**解析规则(`ResolveScopeForSaveAsync`)**
- `appliedRegionType=ALL` 且未传 region:仅校验/保存 `locationIds`(可为空表示全部门店)
- `appliedRegionType=SPECIFIED`:须传 `regionIds` 和/或 `locationIds`;显式 `locationIds` 须在 Region 展开门店范围内
- `locationId` 与 `locationIds` 合并去重,`locationId` 排在数组首位
### 出参(GET 列表 / 详情)
| 字段 | 说明 |
|------|------|
| `locationIds` | 门店 Id 数组(来自 `fl_label_location`,无关联表时回退 `LocationId`) |
| `location` / `locationName` | 门店名称逗号拼接展示 |
| `locationId` | **兼容**:等于 `locationIds[0]`,无门店时为空 |
---
## 四、接口说明
### 1. 标签列表
| 项目 | 内容 |
|------|------|
| 方法 | `GET` |
| 路径 | `/api/app/label` |
| 鉴权 | Bearer Token |
**筛选(Query / data)**
| 参数 | 说明 |
|------|------|
| `SkipCount` | 页码,从 1 起 |
| `MaxResultCount` | 每页条数 |
| `GroupId` | 按 Region 筛选:命中 `fl_label_region`、`fl_label_location` 或 `AppliedRegionType=ALL` |
| `LocationId` | 按门店筛选:命中 `fl_label_location` 或兼容列 `LocationId` |
| `PartnerId` | 按 Company 下门店集合筛选(同 Location 逻辑) |
| `ProductId` / `Keyword` / `LabelCategoryId` / `LabelTypeId` / `TemplateCode` / `State` | 其它原有筛选 |
**出参 `items[]` 新增/变更**
| 字段 | 说明 |
|------|------|
| `locationIds` | 适用门店 Id 数组 |
| `locationName` | 门店展示名(逗号拼接) |
| `region` / `regionIds` / `appliedRegionType` | Region 范围(逻辑不变) |
---
### 2. 标签详情
| 项目 | 内容 |
|------|------|
| 方法 | `GET` |
| 路径 | `/api/app/label/{id}` |
| 路径参数 | `id` = 标签 `LabelCode` |
**出参要点**
| 字段 | 说明 |
|------|------|
| `locationIds` | 适用门店数组 |
| `location` | 门店展示名 |
| `locationId` | 兼容首项 |
| `regionIds` / `groupIds` / `appliedRegionType` | Region 范围 |
---
### 3. 新增标签
| 项目 | 内容 |
|------|------|
| 方法 | `POST` |
| 路径 | `/api/app/label` |
**Body 示例**
```json
{
"labelName": "Price Label A",
"templateCode": "TPL_PRICE_2X2",
"labelCategoryId": "...",
"labelTypeId": "...",
"productIds": ["..."],
"appliedRegionType": "SPECIFIED",
"regionIds": ["group-id-1"],
"locationIds": ["loc-id-1", "loc-id-2"],
"state": true
}
```
**落库**
- `fl_label.LocationId` ← `locationIds[0]`
- `fl_label_location` ← 全量 `locationIds`
- `fl_label_region` ← `regionIds`(SPECIFIED 时)
---
### 4. 编辑标签
| 项目 | 内容 |
|------|------|
| 方法 | `PUT` |
| 路径 | `/api/app/label/{id}` |
| Body | 同 POST;`locationIds` 全量覆盖关联表 |
---
### 5. 删除标签
逻辑删除 `fl_label`,并删除 `fl_label_region`、`fl_label_location`、`fl_label_product` 关联行(表存在时)。
---
## 五、App 打印校验
`UsAppLabelingAppService.PrintAsync` / `PreviewAsync` 使用 `LabelRegionScopeHelper.EnsureLabelAppliesToLocationAsync` / `LabelAppliesToLocationAsync`,支持:
- `AppliedRegionType=ALL` 且配置了 `fl_label_location`:须在关联门店内
- `SPECIFIED`:命中 `fl_label_location`、`LocationId` 或 `fl_label_region`
---
## 六、代码改动摘要
| 文件 | 改动 |
|------|------|
| `LabelRegionScopeHelper.cs` | 多门店解析/保存/列表筛选/展示;`ApplyLabelRegionListFilter` 用 SQL `EXISTS` 查 `fl_label_location` |
| `LabelAppService.cs` | CRUD 读写 `locationIds`;列表筛选走 Helper |
| `LabelCreateInputVo` / `LabelGetOutputDto` / `LabelGetListOutputDto` | 增加 `locationIds` 等字段 |
| `UsAppLabelingAppService.cs` | 打印/预览门店校验对齐多门店 |
| `ILabelAppService.cs` | XML 注释补充 `locationIds` |
---
## 七、向后兼容
| 场景 | 行为 |
|------|------|
| 仅传 `locationId` | 合并为单元素 `locationIds` |
| 未执行 SQL 迁移 | 无 `fl_label_location` 时读写 `fl_label.LocationId` |
| 前端仍读 `locationId` | 详情/列表返回首项,不影响旧页面 |
---
## 八、部署注意
1. 测试/生产库执行 `fl_label_location_scope.sql`
2. `dotnet build` 编译 `FoodLabeling.Application` 后重启 Web 进程
3. 管理端表单改为多选 `locationIds`;旧单选 `locationId` 可逐步下线
---
## 九、请求示例
### 登录获取 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" \
-H "Authorization: Bearer {token}" \
--data-urlencode "SkipCount=1" \
--data-urlencode "MaxResultCount=10"
```
### 按门店筛选
```bash
curl -G "http://flus-test.3ffoodsafety.com/api/app/label" \
-H "Authorization: Bearer {token}" \
--data-urlencode "SkipCount=1" \
--data-urlencode "MaxResultCount=10" \
--data-urlencode "LocationId={location.Id}"
```
### 新增标签(多门店)
```bash
curl -X POST "http://flus-test.3ffoodsafety.com/api/app/label" \
-H "Authorization: Bearer {token}" \
-H "Content-Type: application/json" \
-d '{
"labelName": "Price Label A",
"templateCode": "TPL_PRICE_2X2",
"labelCategoryId": "...",
"labelTypeId": "...",
"productIds": ["..."],
"appliedRegionType": "SPECIFIED",
"regionIds": ["group-id-1"],
"locationIds": ["loc-id-1", "loc-id-2"],
"state": true
}'
```
### 编辑标签(全量覆盖 locationIds)
```bash
curl -X PUT "http://flus-test.3ffoodsafety.com/api/app/label/{labelCode}" \
-H "Authorization: Bearer {token}" \
-H "Content-Type: application/json" \
-d '{
"labelName": "Price Label A",
"templateCode": "TPL_PRICE_2X2",
"labelCategoryId": "...",
"labelTypeId": "...",
"productIds": ["..."],
"locationIds": ["loc-id-1", "loc-id-3"],
"state": true
}'
```
### 详情
```bash
curl -G "http://flus-test.3ffoodsafety.com/api/app/label/{labelCode}" \
-H "Authorization: Bearer {token}"
```
### 响应片段(详情)
```json
{
"id": "LBL0001",
"labelName": "Price Label A",
"locationIds": ["loc-id-1", "loc-id-2"],
"locationId": "loc-id-1",
"location": "Store A, Store B",
"appliedRegionType": "SPECIFIED",
"regionIds": ["group-id-1"],
"region": "West Region"
}
```
---
## 十、标签模板列表 500 修复(`GET /api/app/label-template`)
### 现象
`GET /api/app/label-template?SkipCount=1&MaxResultCount=10` 返回 **500**,日志:
```text
Unknown column 'AppliedPartnerType' in 'field list'
```
对应 SQL 会 SELECT 库中不存在的列:`AppliedPartnerType`、`AppliedRegionType`、`BorderType`。
**MCP 查库(当前测试库)**:`fl_label_template` 仅有 `AppliedLocationType`、`PrintOrientation` 等基础列;**无** scope 列与 `BorderType`;**无** `fl_label_template_partner` / `fl_label_template_region` 表(仅有 `fl_label_template_location`)。
### 根因
`FlLabelTemplateDbEntity` 仍映射 ORM 属性到不存在的列,SqlSugar 生成 `SELECT` 时全部拼入,未执行 `fl_label_template_scope.sql` 的库直接报错。
### 修复说明
| 改动 | 说明 |
|------|------|
| `FlLabelTemplateDbEntity` | **删除** `AppliedPartnerType` / `AppliedRegionType` / `BorderType` 属性 |
| `LabelTemplateQueryHelper.ProjectListColumns` | 列表/详情只读查询 **显式 Select** 真实列 |
| `LabelTemplateQueryHelper.ApplyListSorting` | 默认 `IFNULL(LastModificationTime, CreationTime) DESC`;`Sorting` 白名单 |
| `LabelTemplateScopeSchemaHelper` | 探测 scope / border 列;已迁移时用 raw SQL 读写 |
| `LabelTemplateScopeHelper.SaveTemplateScopeAsync` | 保存关联表后调用 `SetAppliedScopeTypesAsync` |
| `LabelTemplateAppService.GetListAsync` | 排序后 `ProjectListColumns` 再分页 |
> Company/Region 展示与 Id 数组仍由 `fl_label_template_location`(及已迁移时的 partner/region 关联表)推断,**不依赖**主表 scope 列。
### 接口说明
| 项目 | 内容 |
|------|------|
| 方法 | `GET` |
| 路径 | `/api/app/label-template` |
| 鉴权 | Bearer Token |
**入参(Query)**
| 参数 | 说明 |
|------|------|
| `SkipCount` | 页码,从 1 起 |
| `MaxResultCount` | 每页条数 |
| `Keyword` | 模板名称/编码模糊搜索 |
| `PartnerId` / `GroupId` / `LocationId` | 适用范围筛选 |
| `LabelType` / `State` / `Sorting` | 其它筛选与排序(`Sorting` 白名单字段) |
**出参 `items[]` 要点**
| 字段 | 说明 |
|------|------|
| `id` / `templateCode` | 模板编码 |
| `company` / `region` / `location` | 适用范围展示 |
| `partnerIds` / `regionIds` / `locationIds` | 对应 Id 数组 |
| `printOrientation` | 打印方向 `vertical` / `horizontal` |
| `sizeText` / `contentsCount` / `lastEdited` | 尺寸、控件数、最近编辑 |
### 请求示例
```bash
curl -G "http://flus-test.3ffoodsafety.com/api/app/label-template" \
-H "Authorization: Bearer {token}" \
--data-urlencode "SkipCount=1" \
--data-urlencode "MaxResultCount=10"
```
### 部署注意
1. **必须**重新编译 `FoodLabeling.Application` 并**完全重启** Web 进程;若 Yi-SQL 仍出现 `AppliedPartnerType`,说明旧 DLL 未替换。
2. 可选:执行 `scripts/fl_label_template_scope.sql` 启用 Company/Region 主表列与关联表(非列表 500 的必要条件)。
3. 可选:为 `BorderType` 增加列后,`SetBorderTypeAsync` / `GetBorderTypeForTemplateAsync` 会自动读写;未迁移时详情/预览 `border` 默认为 `none`。
---
## 十一、我的菜单 500 修复(`GET /api/app/auth-session/my-menus`)
### 现象
`GET /api/app/auth-session/my-menus` 返回 **500**,日志:
```text
Unknown column 'r.AccessPermissionCodesJson' in 'field list'
```
对应 SQL 错误地 SELECT 了 `Role.AccessPermissionCodesJson`。
### 根因
| 层级 | 名称 |
|------|------|
| C# 属性 | `RoleAggregateRoot.AccessPermissionCodesJson` |
| **数据库列** | **`Role.AccessPermissionCodes`**(`VARCHAR`,JSON 数组字符串) |
`UserRoleAccessPermissionHelper` 直写 SQL 时误用了 C# 属性名作为列名,导致 MySQL 报列不存在。
**MCP 查库**:`Role` 表**有** `AccessPermissionCodes` 列,**无** `AccessPermissionCodesJson` 列。
### 修复说明
| 文件 | 改动 |
|------|------|
| `RoleAccessPermissionSchemaHelper.cs` | **新增**:探测 `Role.AccessPermissionCodes` 列是否存在 |
| `UserRoleAccessPermissionHelper.cs` | SQL 改为读 `AccessPermissionCodes`;列不存在时返回空数组 |
| DTO 注释 | 统一说明库字段名为 `AccessPermissionCodes` |
未执行列迁移的环境(无 `AccessPermissionCodes`)接口仍可返回菜单树,`accessPermissionCodes` 为空数组,不 500。
### 接口说明
| 项目 | 内容 |
|------|------|
| 方法 | `GET` |
| 路径 | `/api/app/auth-session/my-menus` |
| 鉴权 | Bearer Token(须已登录) |
**出参要点(`CurrentUserMenuPermissionsOutputDto`)**
| 字段 | 说明 |
|------|------|
| `user` | 当前用户摘要(id、userName、nick、email 等) |
| `roleCodes` | JWT 中的角色编码列表 |
| `role` | 角色展示名 |
| `menus` | 树形菜单(admin 返回全部未删除菜单;其它用户按 RoleMenu 关联) |
| `permissionCodes` | 可见菜单的 `PermissionCode` 去重列表 |
| `accessPermissionCodes` | 绑定角色 `AccessPermissionCodes` JSON 合并(如 `manage_people`、`manage_labels`) |
| `lastUpdated` | 用户最后修改时间 |
**`accessPermissionCodes` 与菜单的关系**
- 菜单可见性仍由 **RoleMenu + Menu** 决定;
- `accessPermissionCodes` 用于前端/Account Management 等「访问权限」勾选项(Region/Location 管理等能力开关);
- 列未迁移时该数组为空,不影响基础菜单加载。
### 数据库(可选迁移)
若库中尚无 `AccessPermissionCodes` 列,可执行:
`scripts/repair_after_clear_keep_admin_menu_role.sql` 第 1 段,或:
```sql
ALTER TABLE `Role`
ADD COLUMN `AccessPermissionCodes` VARCHAR(2000) NULL
COMMENT '访问权限 JSON 数组,如 ["manage_labels","manage_people"]';
```
### 请求示例
```bash
curl -G "http://flus-test.3ffoodsafety.com/api/app/auth-session/my-menus" \
-H "Authorization: Bearer {token}"
```
### 响应片段
```json
{
"user": { "id": "...", "userName": "admin", "nick": "Admin" },
"roleCodes": ["admin"],
"permissionCodes": ["menu.labels", "menu.reports"],
"accessPermissionCodes": ["manage_labels", "manage_people"],
"menus": [ { "id": "...", "menuName": "Labels", "router": "/labels", "children": [] } ]
}
```
### 部署注意
1. 重新编译并重启 Web 进程;
2. 若仍报 `AccessPermissionCodesJson`,说明旧 DLL 未替换;
3. 无 `AccessPermissionCodes` 列时接口可正常返回菜单,仅 `accessPermissionCodes` 为空。
|