Commit ecb291fd1960e19566da9b022f461e5ba4d5b45d

Authored by 李曜臣
1 parent 87313aec

门店支持优化;标签,产品组件优化

Showing 21 changed files with 300 additions and 216 deletions
本次新增与优化接口汇总(1).md
@@ -39,9 +39,9 @@ @@ -39,9 +39,9 @@
39 39
40 - `fl_product_category`(主表)关键字段: 40 - `fl_product_category`(主表)关键字段:
41 - `DisplayText`:按钮展示文案(为空可回退 `CategoryName`) 41 - `DisplayText`:按钮展示文案(为空可回退 `CategoryName`)
42 - - `ButtonAppearance`:`TEXT/COLOR/IMAGE`  
43 - - `ButtonTextColor` / `ButtonBgColor` / `ButtonImageUrl`  
44 - - `ButtonStyleJson`:样式扩展 JSON(可选) 42 + - `ButtonAppearance`:**JSON 格式字符串**(如 `["TEXT","COLOR"]`);兼容历史单行 `TEXT`/`COLOR`/`IMAGE`(保存时规范为 JSON 数组)
  43 + - `CategoryPhotoUrl`:**JSON 格式字符串**(展示数据由前端解析);非 JSON 纯文本入库时由后端包成 JSON 字符串
  44 + - **已删除列**(若库上仍存在需迁移):`ButtonTextColor`、`ButtonBgColor`、`ButtonImageUrl`、`ButtonStyleJson`
45 - `AvailabilityType`:`ALL/SPECIFIED`(门店可用范围) 45 - `AvailabilityType`:`ALL/SPECIFIED`(门店可用范围)
46 - `fl_product_category_location`(关联表): 46 - `fl_product_category_location`(关联表):
47 - `(CategoryId, LocationId)` 唯一约束;用于 `AvailabilityType=SPECIFIED` 指定门店 47 - `(CategoryId, LocationId)` 唯一约束;用于 `AvailabilityType=SPECIFIED` 指定门店
@@ -56,7 +56,7 @@ @@ -56,7 +56,7 @@
56 - **路径**:`/api/app/product-category` 56 - **路径**:`/api/app/product-category`
57 - **列表行新增返回**: 57 - **列表行新增返回**:
58 - `displayText` 58 - `displayText`
59 - - `buttonAppearance` 59 + - `buttonAppearance`、`categoryPhotoUrl`(均为字符串,内容多为 JSON)
60 - `availabilityType` 60 - `availabilityType`
61 61
62 #### 2.2.2 详情 62 #### 2.2.2 详情
@@ -65,7 +65,7 @@ @@ -65,7 +65,7 @@
65 - **路径**:`/api/app/product-category/{id}` 65 - **路径**:`/api/app/product-category/{id}`
66 - **新增返回字段**: 66 - **新增返回字段**:
67 - `displayText` 67 - `displayText`
68 - - `buttonAppearance/buttonTextColor/buttonBgColor/buttonImageUrl/buttonStyleJson` 68 + - `buttonAppearance`、`categoryPhotoUrl`
69 - `availabilityType` 69 - `availabilityType`
70 - `locationIds`(当 `availabilityType=SPECIFIED` 返回门店 Id 列表,否则为空数组) 70 - `locationIds`(当 `availabilityType=SPECIFIED` 返回门店 Id 列表,否则为空数组)
71 71
@@ -75,7 +75,7 @@ @@ -75,7 +75,7 @@
75 - **路径**:`/api/app/product-category` 75 - **路径**:`/api/app/product-category`
76 - **新增入参字段**: 76 - **新增入参字段**:
77 - `displayText` 77 - `displayText`
78 - - `buttonAppearance/buttonTextColor/buttonBgColor/buttonImageUrl/buttonStyleJson` 78 + - `buttonAppearance`、`categoryPhotoUrl`(JSON 字符串约定,详见 `项目相关文档/产品模块Categories接口对接说明.md`)
79 - `availabilityType` 79 - `availabilityType`
80 - `locationIds`(当 `availabilityType=SPECIFIED` 必填且至少 1 个) 80 - `locationIds`(当 `availabilityType=SPECIFIED` 必填且至少 1 个)
81 81
@@ -95,9 +95,7 @@ @@ -95,9 +95,7 @@
95 95
96 - `availabilityType` 仅允许 `ALL/SPECIFIED` 96 - `availabilityType` 仅允许 `ALL/SPECIFIED`
97 - `SPECIFIED` 时 `locationIds` 至少 1 个 97 - `SPECIFIED` 时 `locationIds` 至少 1 个
98 -- `buttonAppearance` 仅允许 `TEXT/COLOR/IMAGE`  
99 - - `IMAGE` 时必须有 `buttonImageUrl`  
100 - - `COLOR` 时必须有 `buttonBgColor` 98 +- `buttonAppearance`:须为 **合法 JSON**,或为兼容的 **`TEXT`/`COLOR`/`IMAGE` 单行**;`categoryPhotoUrl` 非 JSON 纯文本时后端会序列化为 JSON 字符串存储
101 99
102 --- 100 ---
103 101
@@ -109,9 +107,9 @@ @@ -109,9 +107,9 @@
109 107
110 - `fl_label_category`(主表)关键字段: 108 - `fl_label_category`(主表)关键字段:
111 - `DisplayText`:按钮展示文案(为空可回退 `CategoryName`) 109 - `DisplayText`:按钮展示文案(为空可回退 `CategoryName`)
112 - - `ButtonAppearance`:`TEXT/COLOR/IMAGE`  
113 - - `ButtonTextColor` / `ButtonBgColor` / `ButtonImageUrl`  
114 - - `ButtonStyleJson`:样式扩展 JSON(可选) 110 + - `ButtonAppearance`:**JSON 格式字符串**(如 `["TEXT","COLOR"]`);兼容历史单行 `TEXT`/`COLOR`/`IMAGE`
  111 + - `CategoryPhotoUrl`:**JSON 格式字符串**;非 JSON 纯文本入库时由后端包成 JSON 字符串
  112 + - **已删除列**(若库上仍存在需迁移):`ButtonTextColor`、`ButtonBgColor`、`ButtonImageUrl`、`ButtonStyleJson`
115 - `AvailabilityType`:`ALL/SPECIFIED`(门店可用范围) 113 - `AvailabilityType`:`ALL/SPECIFIED`(门店可用范围)
116 - `fl_label_category_location`(关联表): 114 - `fl_label_category_location`(关联表):
117 - `(CategoryId, LocationId)` 唯一约束;用于 `AvailabilityType=SPECIFIED` 指定门店(`LocationId` 对应 `location` 表主键) 115 - `(CategoryId, LocationId)` 唯一约束;用于 `AvailabilityType=SPECIFIED` 指定门店(`LocationId` 对应 `location` 表主键)
@@ -126,7 +124,7 @@ @@ -126,7 +124,7 @@
126 - **路径**:`/api/app/label-category` 124 - **路径**:`/api/app/label-category`
127 - **列表行新增返回**: 125 - **列表行新增返回**:
128 - `displayText` 126 - `displayText`
129 - - `buttonAppearance` 127 + - `buttonAppearance`、`categoryPhotoUrl`(均为字符串,内容多为 JSON)
130 - `availabilityType` 128 - `availabilityType`
131 129
132 #### 3.2.2 详情 130 #### 3.2.2 详情
@@ -135,7 +133,7 @@ @@ -135,7 +133,7 @@
135 - **路径**:`/api/app/label-category/{id}` 133 - **路径**:`/api/app/label-category/{id}`
136 - **新增返回字段**: 134 - **新增返回字段**:
137 - `displayText` 135 - `displayText`
138 - - `buttonAppearance/buttonTextColor/buttonBgColor/buttonImageUrl/buttonStyleJson` 136 + - `buttonAppearance`、`categoryPhotoUrl`
139 - `availabilityType` 137 - `availabilityType`
140 - `locationIds`(当 `availabilityType=SPECIFIED` 返回门店 Id 列表,否则为空数组) 138 - `locationIds`(当 `availabilityType=SPECIFIED` 返回门店 Id 列表,否则为空数组)
141 139
@@ -145,7 +143,7 @@ @@ -145,7 +143,7 @@
145 - **路径**:`/api/app/label-category` 143 - **路径**:`/api/app/label-category`
146 - **新增入参字段**: 144 - **新增入参字段**:
147 - `displayText` 145 - `displayText`
148 - - `buttonAppearance/buttonTextColor/buttonBgColor/buttonImageUrl/buttonStyleJson` 146 + - `buttonAppearance`、`categoryPhotoUrl`(JSON 字符串约定,详见 `项目相关文档/标签模块接口对接说明.md`)
149 - `availabilityType` 147 - `availabilityType`
150 - `locationIds`(当 `availabilityType=SPECIFIED` 必填且至少 1 个) 148 - `locationIds`(当 `availabilityType=SPECIFIED` 必填且至少 1 个)
151 149
@@ -165,7 +163,5 @@ @@ -165,7 +163,5 @@
165 163
166 - `availabilityType` 仅允许 `ALL/SPECIFIED` 164 - `availabilityType` 仅允许 `ALL/SPECIFIED`
167 - `SPECIFIED` 时 `locationIds` 至少 1 个 165 - `SPECIFIED` 时 `locationIds` 至少 1 个
168 -- `buttonAppearance` 仅允许 `TEXT/COLOR/IMAGE`  
169 - - `IMAGE` 时必须有 `buttonImageUrl`  
170 - - `COLOR` 时必须有 `buttonBgColor` 166 +- `buttonAppearance`:须为 **合法 JSON**,或为兼容的 **`TEXT`/`COLOR`/`IMAGE` 单行**;`categoryPhotoUrl` 非 JSON 纯文本时后端会序列化为 JSON 字符串存储
171 167
标签模块接口对接说明.md
@@ -51,6 +51,12 @@ Swagger 地址: @@ -51,6 +51,12 @@ Swagger 地址:
51 } 51 }
52 ``` 52 ```
53 53
  54 +### 1.1.1 字段约定:`buttonAppearance` 与 `categoryPhotoUrl`(JSON 字符串)
  55 +
  56 +- **`buttonAppearance`**:库中存 **JSON 文本**(如 `["TEXT","COLOR"]`、仅图片 `["IMAGE"]` 等);兼容历史单行 `TEXT`/`COLOR`/`IMAGE`(保存时规范为 `["TEXT"]` 等)。未传或空白时后端默认 `["TEXT"]`。非法值(非 JSON 且非上述三者)会返回友好错误。
  57 +- **`categoryPhotoUrl`**:同样为 **JSON 文本**(如 `["Prep","#10B981"]`);若传**非 JSON** 的纯文本(色值、`/picture/...` 等),后端会序列化为合法 JSON 字符串再存储。列表/详情/App 树**原样返回**字符串,由客户端解析。
  58 +- 其它常用字段:`displayText`、`availabilityType`(`ALL`/`SPECIFIED`)、`locationIds`(指定门店时必填),与产品类别接口语义一致(见 `项目相关文档/产品模块Categories接口对接说明.md`)。
  59 +
54 ### 1.2 详情 60 ### 1.2 详情
55 61
56 方法:`GET /api/app/label-category/{id}` 62 方法:`GET /api/app/label-category/{id}`
@@ -69,7 +75,11 @@ Swagger 地址: @@ -69,7 +75,11 @@ Swagger 地址:
69 { 75 {
70 "categoryCode": "CAT_PREP", 76 "categoryCode": "CAT_PREP",
71 "categoryName": "Prep", 77 "categoryName": "Prep",
72 - "categoryPhotoUrl": "https://cdn.example.com/cat-prep.png", 78 + "displayText": "Prep",
  79 + "buttonAppearance": "[\"TEXT\",\"COLOR\"]",
  80 + "categoryPhotoUrl": "[\"Prep\",\"#10B981\"]",
  81 + "availabilityType": "ALL",
  82 + "locationIds": [],
73 "state": true, 83 "state": true,
74 "orderNum": 1 84 "orderNum": 1
75 } 85 }
@@ -85,7 +95,11 @@ Swagger 地址: @@ -85,7 +95,11 @@ Swagger 地址:
85 { 95 {
86 "categoryCode": "CAT_PREP", 96 "categoryCode": "CAT_PREP",
87 "categoryName": "Prep", 97 "categoryName": "Prep",
88 - "categoryPhotoUrl": null, 98 + "displayText": "Prep",
  99 + "buttonAppearance": "[\"TEXT\",\"COLOR\"]",
  100 + "categoryPhotoUrl": "[\"Prep\",\"#10B981\"]",
  101 + "availabilityType": "ALL",
  102 + "locationIds": [],
89 "state": true, 103 "state": true,
90 "orderNum": 2 104 "orderNum": 2
91 } 105 }
@@ -706,7 +720,8 @@ ADD UNIQUE KEY `uk_fl_ltpd_template_product_label_type` (`TemplateId`, `ProductI @@ -706,7 +720,8 @@ ADD UNIQUE KEY `uk_fl_ltpd_template_product_label_type` (`TemplateId`, `ProductI
706 |------|------|------| 720 |------|------|------|
707 | `id` | string | `fl_label_category.Id` | 721 | `id` | string | `fl_label_category.Id` |
708 | `categoryName` | string | 分类名称 | 722 | `categoryName` | string | 分类名称 |
709 -| `categoryPhotoUrl` | string \| null | 分类图标/图 | 723 +| `categoryPhotoUrl` | string \| null | 分类展示数据,**JSON 格式字符串**(与库中 `CategoryPhotoUrl` 一致,客户端解析) |
  724 +| `buttonAppearance` | string | 按钮外观,**JSON 格式字符串**(与库中 `ButtonAppearance` 一致;空时后端默认 `"TEXT"`) |
710 | `orderNum` | number | 排序 | 725 | `orderNum` | number | 排序 |
711 | `productCategories` | array | 第二级列表(见下表) | 726 | `productCategories` | array | 第二级列表(见下表) |
712 727
@@ -715,8 +730,12 @@ ADD UNIQUE KEY `uk_fl_ltpd_template_product_label_type` (`TemplateId`, `ProductI @@ -715,8 +730,12 @@ ADD UNIQUE KEY `uk_fl_ltpd_template_product_label_type` (`TemplateId`, `ProductI
715 | 字段 | 类型 | 说明 | 730 | 字段 | 类型 | 说明 |
716 |------|------|------| 731 |------|------|------|
717 | `categoryId` | string \| null | 产品分类Id;产品未归类或分类不存在时为空 | 732 | `categoryId` | string \| null | 产品分类Id;产品未归类或分类不存在时为空 |
718 -| `categoryPhotoUrl` | string \| null | 产品分类图片地址;产品未归类或分类不存在时为空 | 733 +| `categoryPhotoUrl` | string \| null | 产品分类展示数据,**JSON 格式字符串**;未归类或分类不存在时为空 |
719 | `name` | string | 产品分类显示名;空源数据为 **`无`** | 734 | `name` | string | 产品分类显示名;空源数据为 **`无`** |
  735 +| `displayText` | string \| null | 按钮展示文案 |
  736 +| `buttonAppearance` | string | **JSON 格式字符串**(与库中一致;空时默认 `"TEXT"`) |
  737 +| `availabilityType` | string | `ALL` / `SPECIFIED`(树已按当前门店过滤,仍返回供客户端展示) |
  738 +| `orderNum` | number | 排序 |
720 | `itemCount` | number | 该分类下 **产品个数**(去重后的产品数) | 739 | `itemCount` | number | 该分类下 **产品个数**(去重后的产品数) |
721 | `products` | array | 第三级产品列表(见下表) | 740 | `products` | array | 第三级产品列表(见下表) |
722 741
@@ -771,13 +790,18 @@ curl -X GET "http://localhost:19001/api/app/us-app-labeling/labeling-tree?locati @@ -771,13 +790,18 @@ curl -X GET "http://localhost:19001/api/app/us-app-labeling/labeling-tree?locati
771 { 790 {
772 "id": "cat-prep-id", 791 "id": "cat-prep-id",
773 "categoryName": "Prep", 792 "categoryName": "Prep",
774 - "categoryPhotoUrl": "/picture/...", 793 + "categoryPhotoUrl": "[\"Prep\",\"#10B981\"]",
  794 + "buttonAppearance": "[\"TEXT\",\"COLOR\"]",
775 "orderNum": 1, 795 "orderNum": 1,
776 "productCategories": [ 796 "productCategories": [
777 { 797 {
778 "categoryId": "pc-meat-id", 798 "categoryId": "pc-meat-id",
779 - "categoryPhotoUrl": "/picture/product-category/20260325123010_xxx.png", 799 + "categoryPhotoUrl": "[\"/picture/product-category/20260325123010_xxx.png\"]",
780 "name": "Meat", 800 "name": "Meat",
  801 + "displayText": "Meat",
  802 + "buttonAppearance": "[\"IMAGE\"]",
  803 + "availabilityType": "ALL",
  804 + "orderNum": 10,
781 "itemCount": 1, 805 "itemCount": 1,
782 "products": [ 806 "products": [
783 { 807 {
美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/LocationSupport/LocationSupportCreateInputVo.cs
@@ -5,8 +5,6 @@ namespace FoodLabeling.Application.Contracts.Dtos.LocationSupport; @@ -5,8 +5,6 @@ namespace FoodLabeling.Application.Contracts.Dtos.LocationSupport;
5 /// </summary> 5 /// </summary>
6 public class LocationSupportCreateInputVo 6 public class LocationSupportCreateInputVo
7 { 7 {
8 - public string LocationId { get; set; } = string.Empty;  
9 -  
10 public string SupportPhone { get; set; } = string.Empty; 8 public string SupportPhone { get; set; } = string.Empty;
11 9
12 public string SupportEmail { get; set; } = string.Empty; 10 public string SupportEmail { get; set; } = string.Empty;
美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/LocationSupport/LocationSupportGetOutputDto.cs
@@ -7,10 +7,6 @@ public class LocationSupportGetOutputDto @@ -7,10 +7,6 @@ public class LocationSupportGetOutputDto
7 { 7 {
8 public string Id { get; set; } = string.Empty; 8 public string Id { get; set; } = string.Empty;
9 9
10 - public string LocationId { get; set; } = string.Empty;  
11 -  
12 - public string? LocationName { get; set; }  
13 -  
14 public string SupportPhone { get; set; } = string.Empty; 10 public string SupportPhone { get; set; } = string.Empty;
15 11
16 public string SupportEmail { get; set; } = string.Empty; 12 public string SupportEmail { get; set; } = string.Empty;
美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/LocationSupport/LocationSupportUpdateInputVo.cs
@@ -5,8 +5,6 @@ namespace FoodLabeling.Application.Contracts.Dtos.LocationSupport; @@ -5,8 +5,6 @@ namespace FoodLabeling.Application.Contracts.Dtos.LocationSupport;
5 /// </summary> 5 /// </summary>
6 public class LocationSupportUpdateInputVo 6 public class LocationSupportUpdateInputVo
7 { 7 {
8 - public string LocationId { get; set; } = string.Empty;  
9 -  
10 public string SupportPhone { get; set; } = string.Empty; 8 public string SupportPhone { get; set; } = string.Empty;
11 9
12 public string SupportEmail { get; set; } = string.Empty; 10 public string SupportEmail { get; set; } = string.Empty;
美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/IServices/ILocationSupportAppService.cs
@@ -4,24 +4,23 @@ using Volo.Abp.Application.Services; @@ -4,24 +4,23 @@ using Volo.Abp.Application.Services;
4 namespace FoodLabeling.Application.Contracts.IServices; 4 namespace FoodLabeling.Application.Contracts.IServices;
5 5
6 /// <summary> 6 /// <summary>
7 -/// 门店 Support 联系方式管理(后台设置 7 +/// 全局 Support 联系方式(全平台共用;Web 可增改查,App 仅可查
8 /// </summary> 8 /// </summary>
9 public interface ILocationSupportAppService : IApplicationService 9 public interface ILocationSupportAppService : IApplicationService
10 { 10 {
11 /// <summary> 11 /// <summary>
12 - /// 按门店查询 Support 联系方式 12 + /// 查询全局 Support 联系方式(已登录即可;App / Web 共用)
13 /// </summary> 13 /// </summary>
14 - /// <param name="locationId">门店Id</param>  
15 - Task<LocationSupportGetOutputDto?> GetByLocationIdAsync(string locationId); 14 + Task<LocationSupportGetOutputDto?> GetSupportAsync();
16 15
17 /// <summary> 16 /// <summary>
18 - /// 新增门店 Support 联系方式(每个门店仅允许一条 17 + /// 新增全局 Support 联系方式(系统仅允许一条;Web 管理端
19 /// </summary> 18 /// </summary>
20 /// <param name="input">联系方式</param> 19 /// <param name="input">联系方式</param>
21 Task<LocationSupportGetOutputDto> CreateAsync(LocationSupportCreateInputVo input); 20 Task<LocationSupportGetOutputDto> CreateAsync(LocationSupportCreateInputVo input);
22 21
23 /// <summary> 22 /// <summary>
24 - /// 编辑门店 Support 联系方式 23 + /// 编辑全局 Support 联系方式(Web 管理端)
25 /// </summary> 24 /// </summary>
26 /// <param name="id">联系方式主键</param> 25 /// <param name="id">联系方式主键</param>
27 /// <param name="input">联系方式</param> 26 /// <param name="input">联系方式</param>
美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/UsAppJwtClaims.cs 0 → 100644
  1 +namespace FoodLabeling.Application.Contracts;
  2 +
  3 +/// <summary>
  4 +/// 美国版 App JWT 自定义声明(用于与 Web 管理端 Token 区分能力)
  5 +/// </summary>
  6 +public static class UsAppJwtClaims
  7 +{
  8 + /// <summary>声明类型:客户端种类</summary>
  9 + public const string ClientKind = "client_kind";
  10 +
  11 + /// <summary>美国版移动端 App</summary>
  12 + public const string ClientKindUsApp = "us-app";
  13 +}
美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application/Helpers/CategoryAppearanceStorageHelper.cs 0 → 100644
  1 +using System.Text.Json;
  2 +using Volo.Abp;
  3 +
  4 +namespace FoodLabeling.Application.Helpers;
  5 +
  6 +/// <summary>
  7 +/// 将标签/产品类别的按钮外观与展示字段按「JSON 字符串」落库;兼容历史单行 TEXT/COLOR/IMAGE。
  8 +/// </summary>
  9 +public static class CategoryAppearanceStorageHelper
  10 +{
  11 + /// <summary>未传按钮外观时的默认 JSON(与前端数组语义一致)。</summary>
  12 + public const string DefaultButtonAppearanceJson = """["TEXT"]""";
  13 +
  14 + /// <summary>
  15 + /// 规范化 <see cref="FoodLabeling.Application.Services.DbModels.FlLabelCategoryDbEntity.ButtonAppearance"/> /
  16 + /// 产品类别同名字段:落库为合法 JSON 文本,不做整串 ToUpper(避免破坏 JSON)。
  17 + /// </summary>
  18 + public static string NormalizeButtonAppearanceForStorage(string? raw)
  19 + {
  20 + if (string.IsNullOrWhiteSpace(raw))
  21 + {
  22 + return DefaultButtonAppearanceJson;
  23 + }
  24 +
  25 + var t = raw.Trim();
  26 + var legacy = t.ToUpperInvariant();
  27 + if (legacy is "TEXT" or "COLOR" or "IMAGE")
  28 + {
  29 + return JsonSerializer.Serialize(new[] { legacy });
  30 + }
  31 +
  32 + try
  33 + {
  34 + using var _ = JsonDocument.Parse(t);
  35 + return t;
  36 + }
  37 + catch (JsonException)
  38 + {
  39 + throw new UserFriendlyException("按钮外观格式不正确,须为合法 JSON(或兼容旧的 TEXT/COLOR/IMAGE)");
  40 + }
  41 + }
  42 +
  43 + /// <summary>
  44 + /// 规范化 <see cref="FoodLabeling.Application.Services.DbModels.FlLabelCategoryDbEntity.CategoryPhotoUrl"/> /
  45 + /// 产品类别同名字段:已是 JSON 则原样落库;否则将整段文本序列化为 JSON 字符串(兼容历史单行色值/URL)。
  46 + /// </summary>
  47 + public static string? NormalizeCategoryPhotoUrlForStorage(string? raw)
  48 + {
  49 + if (string.IsNullOrWhiteSpace(raw))
  50 + {
  51 + return null;
  52 + }
  53 +
  54 + var t = raw.Trim();
  55 + try
  56 + {
  57 + using var _ = JsonDocument.Parse(t);
  58 + return t;
  59 + }
  60 + catch (JsonException)
  61 + {
  62 + return JsonSerializer.Serialize(t);
  63 + }
  64 + }
  65 +}
美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application/Services/DbModels/FlLocationSupportDbEntity.cs
@@ -21,8 +21,6 @@ public class FlLocationSupportDbEntity @@ -21,8 +21,6 @@ public class FlLocationSupportDbEntity
21 21
22 public DateTime? LastModificationTime { get; set; } 22 public DateTime? LastModificationTime { get; set; }
23 23
24 - public string LocationId { get; set; } = string.Empty;  
25 -  
26 public string SupportPhone { get; set; } = string.Empty; 24 public string SupportPhone { get; set; } = string.Empty;
27 25
28 public string SupportEmail { get; set; } = string.Empty; 26 public string SupportEmail { get; set; } = string.Empty;
美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application/Services/LabelCategoryAppService.cs
@@ -125,9 +125,8 @@ public class LabelCategoryAppService : ApplicationService, ILabelCategoryAppServ @@ -125,9 +125,8 @@ public class LabelCategoryAppService : ApplicationService, ILabelCategoryAppServ
125 } 125 }
126 126
127 var displayText = input.DisplayText?.Trim(); 127 var displayText = input.DisplayText?.Trim();
128 - var appearance = (input.ButtonAppearance ?? "TEXT").Trim().ToUpperInvariant(); 128 + var appearance = CategoryAppearanceStorageHelper.NormalizeButtonAppearanceForStorage(input.ButtonAppearance);
129 var availabilityType = (input.AvailabilityType ?? "ALL").Trim().ToUpperInvariant(); 129 var availabilityType = (input.AvailabilityType ?? "ALL").Trim().ToUpperInvariant();
130 - ValidateButtonAppearance(appearance);  
131 var locationIds = NormalizeLocationIds(input.LocationIds); 130 var locationIds = NormalizeLocationIds(input.LocationIds);
132 ValidateAvailabilityTypeAndLocations(availabilityType, locationIds); 131 ValidateAvailabilityTypeAndLocations(availabilityType, locationIds);
133 132
@@ -146,7 +145,7 @@ public class LabelCategoryAppService : ApplicationService, ILabelCategoryAppServ @@ -146,7 +145,7 @@ public class LabelCategoryAppService : ApplicationService, ILabelCategoryAppServ
146 CategoryCode = code, 145 CategoryCode = code,
147 CategoryName = name, 146 CategoryName = name,
148 DisplayText = string.IsNullOrWhiteSpace(displayText) ? null : displayText, 147 DisplayText = string.IsNullOrWhiteSpace(displayText) ? null : displayText,
149 - CategoryPhotoUrl = input.CategoryPhotoUrl?.Trim(), 148 + CategoryPhotoUrl = CategoryAppearanceStorageHelper.NormalizeCategoryPhotoUrlForStorage(input.CategoryPhotoUrl),
150 State = input.State, 149 State = input.State,
151 ButtonAppearance = appearance, 150 ButtonAppearance = appearance,
152 AvailabilityType = availabilityType, 151 AvailabilityType = availabilityType,
@@ -175,9 +174,8 @@ public class LabelCategoryAppService : ApplicationService, ILabelCategoryAppServ @@ -175,9 +174,8 @@ public class LabelCategoryAppService : ApplicationService, ILabelCategoryAppServ
175 } 174 }
176 175
177 var displayText = input.DisplayText?.Trim(); 176 var displayText = input.DisplayText?.Trim();
178 - var appearance = (input.ButtonAppearance ?? "TEXT").Trim().ToUpperInvariant(); 177 + var appearance = CategoryAppearanceStorageHelper.NormalizeButtonAppearanceForStorage(input.ButtonAppearance);
179 var availabilityType = (input.AvailabilityType ?? "ALL").Trim().ToUpperInvariant(); 178 var availabilityType = (input.AvailabilityType ?? "ALL").Trim().ToUpperInvariant();
180 - ValidateButtonAppearance(appearance);  
181 var locationIds = NormalizeLocationIds(input.LocationIds); 179 var locationIds = NormalizeLocationIds(input.LocationIds);
182 ValidateAvailabilityTypeAndLocations(availabilityType, locationIds); 180 ValidateAvailabilityTypeAndLocations(availabilityType, locationIds);
183 181
@@ -191,7 +189,7 @@ public class LabelCategoryAppService : ApplicationService, ILabelCategoryAppServ @@ -191,7 +189,7 @@ public class LabelCategoryAppService : ApplicationService, ILabelCategoryAppServ
191 entity.CategoryCode = code; 189 entity.CategoryCode = code;
192 entity.CategoryName = name; 190 entity.CategoryName = name;
193 entity.DisplayText = string.IsNullOrWhiteSpace(displayText) ? null : displayText; 191 entity.DisplayText = string.IsNullOrWhiteSpace(displayText) ? null : displayText;
194 - entity.CategoryPhotoUrl = input.CategoryPhotoUrl?.Trim(); 192 + entity.CategoryPhotoUrl = CategoryAppearanceStorageHelper.NormalizeCategoryPhotoUrlForStorage(input.CategoryPhotoUrl);
195 entity.State = input.State; 193 entity.State = input.State;
196 entity.ButtonAppearance = appearance; 194 entity.ButtonAppearance = appearance;
197 entity.AvailabilityType = availabilityType; 195 entity.AvailabilityType = availabilityType;
@@ -264,14 +262,6 @@ public class LabelCategoryAppService : ApplicationService, ILabelCategoryAppServ @@ -264,14 +262,6 @@ public class LabelCategoryAppService : ApplicationService, ILabelCategoryAppServ
264 .ToList() ?? new(); 262 .ToList() ?? new();
265 } 263 }
266 264
267 - private static void ValidateButtonAppearance(string appearance)  
268 - {  
269 - if (appearance != "TEXT" && appearance != "COLOR" && appearance != "IMAGE")  
270 - {  
271 - throw new UserFriendlyException("按钮外观不合法(TEXT/COLOR/IMAGE)");  
272 - }  
273 - }  
274 -  
275 private async Task SaveCategoryLocationsAsync( 265 private async Task SaveCategoryLocationsAsync(
276 string categoryId, 266 string categoryId,
277 string availabilityType, 267 string availabilityType,
美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application/Services/LocationSupportAppService.cs
  1 +using FoodLabeling.Application.Contracts;
1 using FoodLabeling.Application.Contracts.Dtos.LocationSupport; 2 using FoodLabeling.Application.Contracts.Dtos.LocationSupport;
2 using FoodLabeling.Application.Contracts.IServices; 3 using FoodLabeling.Application.Contracts.IServices;
3 using FoodLabeling.Application.Services.DbModels; 4 using FoodLabeling.Application.Services.DbModels;
4 -using FoodLabeling.Domain.Entities; 5 +using Microsoft.AspNetCore.Authorization;
5 using Volo.Abp; 6 using Volo.Abp;
6 using Volo.Abp.Application.Services; 7 using Volo.Abp.Application.Services;
7 using Volo.Abp.Guids; 8 using Volo.Abp.Guids;
@@ -11,8 +12,9 @@ using Yi.Framework.SqlSugarCore.Abstractions; @@ -11,8 +12,9 @@ using Yi.Framework.SqlSugarCore.Abstractions;
11 namespace FoodLabeling.Application.Services; 12 namespace FoodLabeling.Application.Services;
12 13
13 /// <summary> 14 /// <summary>
14 -/// 门店 Support 联系方式(后台设置,App 展示 15 +/// 全局 Support 联系方式(全门店共用;Web 可增改查,App JWT 仅可读
15 /// </summary> 16 /// </summary>
  17 +[Authorize]
16 public class LocationSupportAppService : ApplicationService, ILocationSupportAppService 18 public class LocationSupportAppService : ApplicationService, ILocationSupportAppService
17 { 19 {
18 private readonly ISqlSugarDbContext _dbContext; 20 private readonly ISqlSugarDbContext _dbContext;
@@ -25,46 +27,36 @@ public class LocationSupportAppService : ApplicationService, ILocationSupportApp @@ -25,46 +27,36 @@ public class LocationSupportAppService : ApplicationService, ILocationSupportApp
25 } 27 }
26 28
27 /// <inheritdoc /> 29 /// <inheritdoc />
28 - public async Task<LocationSupportGetOutputDto?> GetByLocationIdAsync(string locationId) 30 + public async Task<LocationSupportGetOutputDto?> GetSupportAsync()
29 { 31 {
30 - var lid = locationId?.Trim();  
31 - if (string.IsNullOrWhiteSpace(lid))  
32 - {  
33 - throw new UserFriendlyException("门店Id不能为空");  
34 - }  
35 -  
36 - // 全门店共用一套 Support 联系方式,按任意门店 Id 查询都返回同一条配置。  
37 - var entity = await _dbContext.SqlSugarClient.Queryable<FlLocationSupportDbEntity>()  
38 - .FirstAsync(x => !x.IsDeleted);  
39 - if (entity is null)  
40 - {  
41 - return null;  
42 - }  
43 -  
44 - return await MapOutputAsync(entity); 32 + var rows = await _dbContext.SqlSugarClient.Queryable<FlLocationSupportDbEntity>()
  33 + .Where(x => !x.IsDeleted)
  34 + .ToListAsync();
  35 + var entity = rows.FirstOrDefault();
  36 + return MapOutput(entity);
45 } 37 }
46 38
47 /// <inheritdoc /> 39 /// <inheritdoc />
48 [UnitOfWork] 40 [UnitOfWork]
49 public async Task<LocationSupportGetOutputDto> CreateAsync(LocationSupportCreateInputVo input) 41 public async Task<LocationSupportGetOutputDto> CreateAsync(LocationSupportCreateInputVo input)
50 { 42 {
  43 + EnsureNotUsAppClient();
  44 +
51 if (input is null) 45 if (input is null)
52 { 46 {
53 - throw new UserFriendlyException("入参不能为空"); 47 + throw new UserFriendlyException("Request body is required.");
54 } 48 }
55 49
56 - var locationId = NormalizeLocationId(input.LocationId);  
57 - var phone = NormalizeRequired(input.SupportPhone, "Support 电话不能为空");  
58 - var email = NormalizeRequired(input.SupportEmail, "Support 邮箱不能为空"); 50 + var phone = NormalizeRequired(input.SupportPhone, "Support phone is required.");
  51 + var email = NormalizeRequired(input.SupportEmail, "Support email is required.");
59 EnsureEmailFormat(email); 52 EnsureEmailFormat(email);
60 53
61 - await EnsureLocationExistsAsync(locationId);  
62 -  
63 var existed = await _dbContext.SqlSugarClient.Queryable<FlLocationSupportDbEntity>() 54 var existed = await _dbContext.SqlSugarClient.Queryable<FlLocationSupportDbEntity>()
64 .AnyAsync(x => !x.IsDeleted); 55 .AnyAsync(x => !x.IsDeleted);
65 if (existed) 56 if (existed)
66 { 57 {
67 - throw new UserFriendlyException("已存在全局 Support 联系方式,请使用编辑接口"); 58 + throw new UserFriendlyException(
  59 + "Global support contact already exists. Use update instead.");
68 } 60 }
69 61
70 var now = Clock.Now; 62 var now = Clock.Now;
@@ -76,74 +68,60 @@ public class LocationSupportAppService : ApplicationService, ILocationSupportApp @@ -76,74 +68,60 @@ public class LocationSupportAppService : ApplicationService, ILocationSupportApp
76 CreatorId = CurrentUser?.Id?.ToString(), 68 CreatorId = CurrentUser?.Id?.ToString(),
77 LastModificationTime = now, 69 LastModificationTime = now,
78 LastModifierId = CurrentUser?.Id?.ToString(), 70 LastModifierId = CurrentUser?.Id?.ToString(),
79 - LocationId = locationId,  
80 SupportPhone = phone, 71 SupportPhone = phone,
81 SupportEmail = email 72 SupportEmail = email
82 }; 73 };
83 74
84 await _dbContext.SqlSugarClient.Insertable(entity).ExecuteCommandAsync(); 75 await _dbContext.SqlSugarClient.Insertable(entity).ExecuteCommandAsync();
85 - return (await MapOutputAsync(entity))!; 76 + return MapOutput(entity)!;
86 } 77 }
87 78
88 /// <inheritdoc /> 79 /// <inheritdoc />
89 [UnitOfWork] 80 [UnitOfWork]
90 public async Task<LocationSupportGetOutputDto> UpdateAsync(string id, LocationSupportUpdateInputVo input) 81 public async Task<LocationSupportGetOutputDto> UpdateAsync(string id, LocationSupportUpdateInputVo input)
91 { 82 {
  83 + EnsureNotUsAppClient();
  84 +
92 var supportId = id?.Trim(); 85 var supportId = id?.Trim();
93 if (string.IsNullOrWhiteSpace(supportId)) 86 if (string.IsNullOrWhiteSpace(supportId))
94 { 87 {
95 - throw new UserFriendlyException("联系方式Id不能为空"); 88 + throw new UserFriendlyException("Support record id is required.");
96 } 89 }
97 90
98 if (input is null) 91 if (input is null)
99 { 92 {
100 - throw new UserFriendlyException("入参不能为空"); 93 + throw new UserFriendlyException("Request body is required.");
101 } 94 }
102 95
103 - var entity = await _dbContext.SqlSugarClient.Queryable<FlLocationSupportDbEntity>()  
104 - .FirstAsync(x => !x.IsDeleted && x.Id == supportId); 96 + var rows = await _dbContext.SqlSugarClient.Queryable<FlLocationSupportDbEntity>()
  97 + .Where(x => !x.IsDeleted && x.Id == supportId)
  98 + .ToListAsync();
  99 + var entity = rows.FirstOrDefault();
105 if (entity is null) 100 if (entity is null)
106 { 101 {
107 - throw new UserFriendlyException("联系方式记录不存在"); 102 + throw new UserFriendlyException("Support record not found.");
108 } 103 }
109 104
110 - var locationId = NormalizeLocationId(input.LocationId);  
111 - var phone = NormalizeRequired(input.SupportPhone, "Support 电话不能为空");  
112 - var email = NormalizeRequired(input.SupportEmail, "Support 邮箱不能为空"); 105 + var phone = NormalizeRequired(input.SupportPhone, "Support phone is required.");
  106 + var email = NormalizeRequired(input.SupportEmail, "Support email is required.");
113 EnsureEmailFormat(email); 107 EnsureEmailFormat(email);
114 - await EnsureLocationExistsAsync(locationId);  
115 108
116 - var conflict = await _dbContext.SqlSugarClient.Queryable<FlLocationSupportDbEntity>()  
117 - .AnyAsync(x => !x.IsDeleted && x.Id != supportId);  
118 - if (conflict)  
119 - {  
120 - throw new UserFriendlyException("系统仅允许一条全局 Support 联系方式");  
121 - }  
122 -  
123 - entity.LocationId = locationId;  
124 entity.SupportPhone = phone; 109 entity.SupportPhone = phone;
125 entity.SupportEmail = email; 110 entity.SupportEmail = email;
126 entity.LastModificationTime = Clock.Now; 111 entity.LastModificationTime = Clock.Now;
127 entity.LastModifierId = CurrentUser?.Id?.ToString(); 112 entity.LastModifierId = CurrentUser?.Id?.ToString();
128 113
129 await _dbContext.SqlSugarClient.Updateable(entity).ExecuteCommandAsync(); 114 await _dbContext.SqlSugarClient.Updateable(entity).ExecuteCommandAsync();
130 - return (await MapOutputAsync(entity))!; 115 + return MapOutput(entity)!;
131 } 116 }
132 117
133 - private string NormalizeLocationId(string? locationId) 118 + private void EnsureNotUsAppClient()
134 { 119 {
135 - var lid = locationId?.Trim();  
136 - if (string.IsNullOrWhiteSpace(lid))  
137 - {  
138 - throw new UserFriendlyException("门店Id不能为空");  
139 - }  
140 -  
141 - if (!Guid.TryParse(lid, out _)) 120 + if (CurrentUser.FindClaim(UsAppJwtClaims.ClientKind)?.Value == UsAppJwtClaims.ClientKindUsApp)
142 { 121 {
143 - throw new UserFriendlyException("门店Id格式不正确"); 122 + throw new UserFriendlyException(
  123 + "The mobile app can only view support contacts. Please use the web console to edit.");
144 } 124 }
145 -  
146 - return lid;  
147 } 125 }
148 126
149 private static string NormalizeRequired(string? value, string message) 127 private static string NormalizeRequired(string? value, string message)
@@ -162,48 +140,22 @@ public class LocationSupportAppService : ApplicationService, ILocationSupportApp @@ -162,48 +140,22 @@ public class LocationSupportAppService : ApplicationService, ILocationSupportApp
162 if (!email.Contains("@", StringComparison.Ordinal) || email.StartsWith("@", StringComparison.Ordinal) || 140 if (!email.Contains("@", StringComparison.Ordinal) || email.StartsWith("@", StringComparison.Ordinal) ||
163 email.EndsWith("@", StringComparison.Ordinal)) 141 email.EndsWith("@", StringComparison.Ordinal))
164 { 142 {
165 - throw new UserFriendlyException("Support 邮箱格式不正确"); 143 + throw new UserFriendlyException("Support email format is invalid.");
166 } 144 }
167 } 145 }
168 146
169 - private async Task EnsureLocationExistsAsync(string locationId)  
170 - {  
171 - if (!Guid.TryParse(locationId, out var gid))  
172 - {  
173 - throw new UserFriendlyException("门店Id格式不正确");  
174 - }  
175 -  
176 - var exists = await _dbContext.SqlSugarClient.Queryable<LocationAggregateRoot>()  
177 - .AnyAsync(x => !x.IsDeleted && x.Id == gid);  
178 - if (!exists)  
179 - {  
180 - throw new UserFriendlyException("门店不存在");  
181 - }  
182 - }  
183 -  
184 - private async Task<LocationSupportGetOutputDto?> MapOutputAsync(FlLocationSupportDbEntity? entity) 147 + private static LocationSupportGetOutputDto? MapOutput(FlLocationSupportDbEntity? entity)
185 { 148 {
186 if (entity is null) 149 if (entity is null)
187 { 150 {
188 return null; 151 return null;
189 } 152 }
190 153
191 - string? locationName = null;  
192 - if (Guid.TryParse(entity.LocationId, out var lid))  
193 - {  
194 - var loc = await _dbContext.SqlSugarClient.Queryable<LocationAggregateRoot>()  
195 - .FirstAsync(x => !x.IsDeleted && x.Id == lid);  
196 - locationName = loc?.LocationName?.Trim();  
197 - }  
198 -  
199 return new LocationSupportGetOutputDto 154 return new LocationSupportGetOutputDto
200 { 155 {
201 Id = entity.Id, 156 Id = entity.Id,
202 - LocationId = entity.LocationId,  
203 - LocationName = locationName,  
204 SupportPhone = entity.SupportPhone, 157 SupportPhone = entity.SupportPhone,
205 SupportEmail = entity.SupportEmail 158 SupportEmail = entity.SupportEmail
206 }; 159 };
207 } 160 }
208 } 161 }
209 -  
美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application/Services/ProductCategoryAppService.cs
  1 +using FoodLabeling.Application.Helpers;
1 using FoodLabeling.Application.Contracts.Dtos.Common; 2 using FoodLabeling.Application.Contracts.Dtos.Common;
2 using FoodLabeling.Application.Contracts.Dtos.ProductCategory; 3 using FoodLabeling.Application.Contracts.Dtos.ProductCategory;
3 using FoodLabeling.Application.Contracts.IServices; 4 using FoodLabeling.Application.Contracts.IServices;
@@ -128,9 +129,8 @@ public class ProductCategoryAppService : ApplicationService, IProductCategoryApp @@ -128,9 +129,8 @@ public class ProductCategoryAppService : ApplicationService, IProductCategoryApp
128 } 129 }
129 130
130 var displayText = input.DisplayText?.Trim(); 131 var displayText = input.DisplayText?.Trim();
131 - var appearance = (input.ButtonAppearance ?? "TEXT").Trim().ToUpperInvariant(); 132 + var appearance = CategoryAppearanceStorageHelper.NormalizeButtonAppearanceForStorage(input.ButtonAppearance);
132 var availabilityType = (input.AvailabilityType ?? "ALL").Trim().ToUpperInvariant(); 133 var availabilityType = (input.AvailabilityType ?? "ALL").Trim().ToUpperInvariant();
133 - ValidateButtonAppearance(appearance);  
134 var locationIds = NormalizeLocationIds(input.LocationIds); 134 var locationIds = NormalizeLocationIds(input.LocationIds);
135 ValidateAvailabilityTypeAndLocations(availabilityType, locationIds); 135 ValidateAvailabilityTypeAndLocations(availabilityType, locationIds);
136 136
@@ -155,7 +155,7 @@ public class ProductCategoryAppService : ApplicationService, IProductCategoryApp @@ -155,7 +155,7 @@ public class ProductCategoryAppService : ApplicationService, IProductCategoryApp
155 CategoryCode = code, 155 CategoryCode = code,
156 CategoryName = name, 156 CategoryName = name,
157 DisplayText = string.IsNullOrWhiteSpace(displayText) ? null : displayText, 157 DisplayText = string.IsNullOrWhiteSpace(displayText) ? null : displayText,
158 - CategoryPhotoUrl = input.CategoryPhotoUrl?.Trim(), 158 + CategoryPhotoUrl = CategoryAppearanceStorageHelper.NormalizeCategoryPhotoUrlForStorage(input.CategoryPhotoUrl),
159 ButtonAppearance = appearance, 159 ButtonAppearance = appearance,
160 State = input.State, 160 State = input.State,
161 AvailabilityType = availabilityType, 161 AvailabilityType = availabilityType,
@@ -187,9 +187,8 @@ public class ProductCategoryAppService : ApplicationService, IProductCategoryApp @@ -187,9 +187,8 @@ public class ProductCategoryAppService : ApplicationService, IProductCategoryApp
187 } 187 }
188 188
189 var displayText = input.DisplayText?.Trim(); 189 var displayText = input.DisplayText?.Trim();
190 - var appearance = (input.ButtonAppearance ?? "TEXT").Trim().ToUpperInvariant(); 190 + var appearance = CategoryAppearanceStorageHelper.NormalizeButtonAppearanceForStorage(input.ButtonAppearance);
191 var availabilityType = (input.AvailabilityType ?? "ALL").Trim().ToUpperInvariant(); 191 var availabilityType = (input.AvailabilityType ?? "ALL").Trim().ToUpperInvariant();
192 - ValidateButtonAppearance(appearance);  
193 var locationIds = NormalizeLocationIds(input.LocationIds); 192 var locationIds = NormalizeLocationIds(input.LocationIds);
194 ValidateAvailabilityTypeAndLocations(availabilityType, locationIds); 193 ValidateAvailabilityTypeAndLocations(availabilityType, locationIds);
195 194
@@ -203,7 +202,7 @@ public class ProductCategoryAppService : ApplicationService, IProductCategoryApp @@ -203,7 +202,7 @@ public class ProductCategoryAppService : ApplicationService, IProductCategoryApp
203 entity.CategoryCode = code; 202 entity.CategoryCode = code;
204 entity.CategoryName = name; 203 entity.CategoryName = name;
205 entity.DisplayText = string.IsNullOrWhiteSpace(displayText) ? null : displayText; 204 entity.DisplayText = string.IsNullOrWhiteSpace(displayText) ? null : displayText;
206 - entity.CategoryPhotoUrl = input.CategoryPhotoUrl?.Trim(); 205 + entity.CategoryPhotoUrl = CategoryAppearanceStorageHelper.NormalizeCategoryPhotoUrlForStorage(input.CategoryPhotoUrl);
207 entity.ButtonAppearance = appearance; 206 entity.ButtonAppearance = appearance;
208 entity.State = input.State; 207 entity.State = input.State;
209 entity.AvailabilityType = availabilityType; 208 entity.AvailabilityType = availabilityType;
@@ -280,14 +279,6 @@ public class ProductCategoryAppService : ApplicationService, IProductCategoryApp @@ -280,14 +279,6 @@ public class ProductCategoryAppService : ApplicationService, IProductCategoryApp
280 .ToList() ?? new(); 279 .ToList() ?? new();
281 } 280 }
282 281
283 - private static void ValidateButtonAppearance(string appearance)  
284 - {  
285 - if (appearance != "TEXT" && appearance != "COLOR" && appearance != "IMAGE")  
286 - {  
287 - throw new UserFriendlyException("按钮外观不合法(TEXT/COLOR/IMAGE)");  
288 - }  
289 - }  
290 -  
291 private async Task SaveCategoryLocationsAsync( 282 private async Task SaveCategoryLocationsAsync(
292 string categoryId, 283 string categoryId,
293 string availabilityType, 284 string availabilityType,
美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application/Services/UsAppAuthAppService.cs
@@ -6,6 +6,7 @@ using System.Linq; @@ -6,6 +6,7 @@ using System.Linq;
6 using System.Security.Claims; 6 using System.Security.Claims;
7 using System.Text; 7 using System.Text;
8 using System.Threading.Tasks; 8 using System.Threading.Tasks;
  9 +using FoodLabeling.Application.Contracts;
9 using FoodLabeling.Application.Contracts.Dtos.UsAppAuth; 10 using FoodLabeling.Application.Contracts.Dtos.UsAppAuth;
10 using FoodLabeling.Application.Contracts.IServices; 11 using FoodLabeling.Application.Contracts.IServices;
11 using FoodLabeling.Application.Services.DbModels; 12 using FoodLabeling.Application.Services.DbModels;
@@ -432,7 +433,8 @@ public class UsAppAuthAppService : ApplicationService, IUsAppAuthAppService @@ -432,7 +433,8 @@ public class UsAppAuthAppService : ApplicationService, IUsAppAuthAppService
432 var claims = new List<Claim> 433 var claims = new List<Claim>
433 { 434 {
434 new(AbpClaimTypes.UserId, user.Id.ToString()), 435 new(AbpClaimTypes.UserId, user.Id.ToString()),
435 - new(AbpClaimTypes.UserName, user.UserName) 436 + new(AbpClaimTypes.UserName, user.UserName),
  437 + new(UsAppJwtClaims.ClientKind, UsAppJwtClaims.ClientKindUsApp)
436 }; 438 };
437 439
438 if (!string.IsNullOrWhiteSpace(user.Email)) 440 if (!string.IsNullOrWhiteSpace(user.Email))
美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application/Services/UsAppLabelingAppService.cs
@@ -129,7 +129,7 @@ public class UsAppLabelingAppService : ApplicationService, IUsAppLabelingAppServ @@ -129,7 +129,7 @@ public class UsAppLabelingAppService : ApplicationService, IUsAppLabelingAppServ
129 { 129 {
130 var l1Appearance = string.IsNullOrWhiteSpace(g1.Key.LabelCategoryButtonAppearance) 130 var l1Appearance = string.IsNullOrWhiteSpace(g1.Key.LabelCategoryButtonAppearance)
131 ? "TEXT" 131 ? "TEXT"
132 - : g1.Key.LabelCategoryButtonAppearance.Trim().ToUpperInvariant(); 132 + : g1.Key.LabelCategoryButtonAppearance.Trim();
133 var l1 = new UsAppLabelCategoryTreeNodeDto 133 var l1 = new UsAppLabelCategoryTreeNodeDto
134 { 134 {
135 Id = g1.Key.LabelCategoryId, 135 Id = g1.Key.LabelCategoryId,
@@ -178,7 +178,7 @@ public class UsAppLabelingAppService : ApplicationService, IUsAppLabelingAppServ @@ -178,7 +178,7 @@ public class UsAppLabelingAppService : ApplicationService, IUsAppLabelingAppServ
178 var productsGrouped = g2.GroupBy(x => x.ProductId).OrderBy(pg => pg.First().ProductName); 178 var productsGrouped = g2.GroupBy(x => x.ProductId).OrderBy(pg => pg.First().ProductName);
179 var appearance = string.IsNullOrWhiteSpace(g2.Key.ButtonAppearance) 179 var appearance = string.IsNullOrWhiteSpace(g2.Key.ButtonAppearance)
180 ? "TEXT" 180 ? "TEXT"
181 - : g2.Key.ButtonAppearance.Trim().ToUpperInvariant(); 181 + : g2.Key.ButtonAppearance.Trim();
182 var availability = string.IsNullOrWhiteSpace(g2.Key.AvailabilityType) 182 var availability = string.IsNullOrWhiteSpace(g2.Key.AvailabilityType)
183 ? "ALL" 183 ? "ALL"
184 : g2.Key.AvailabilityType.Trim().ToUpperInvariant(); 184 : g2.Key.AvailabilityType.Trim().ToUpperInvariant();
美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/scripts/fl_location_support_alter_drop_locationid.sql 0 → 100644
  1 +-- 从旧版「按门店」结构迁移为「全局一条」:删除 LocationId 及门店唯一索引
  2 +-- 执行前请确认库中 `fl_location_support` 已存在;若不存在可跳过本脚本,直接使用 fl_location_support_create.sql
  3 +
  4 +-- 若存在按门店的唯一索引则删除(名称与历史脚本一致)
  5 +ALTER TABLE `fl_location_support` DROP INDEX `uk_fl_location_support_locationid`;
  6 +
  7 +-- 删除门店列(若列不存在会报错,需按环境调整)
  8 +ALTER TABLE `fl_location_support` DROP COLUMN `LocationId`;
美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/scripts/fl_location_support_create.sql
1 --- 门店 Support 联系方式(每个门店仅一条) 1 +-- 全局 Support 联系方式(全门店共用一条)
2 CREATE TABLE IF NOT EXISTS `fl_location_support` ( 2 CREATE TABLE IF NOT EXISTS `fl_location_support` (
3 `Id` varchar(50) NOT NULL COMMENT '主键', 3 `Id` varchar(50) NOT NULL COMMENT '主键',
4 `IsDeleted` tinyint(1) NOT NULL DEFAULT 0 COMMENT '逻辑删除', 4 `IsDeleted` tinyint(1) NOT NULL DEFAULT 0 COMMENT '逻辑删除',
@@ -6,11 +6,8 @@ CREATE TABLE IF NOT EXISTS `fl_location_support` ( @@ -6,11 +6,8 @@ CREATE TABLE IF NOT EXISTS `fl_location_support` (
6 `CreatorId` varchar(50) DEFAULT NULL COMMENT '创建人', 6 `CreatorId` varchar(50) DEFAULT NULL COMMENT '创建人',
7 `LastModifierId` varchar(50) DEFAULT NULL COMMENT '最后修改人', 7 `LastModifierId` varchar(50) DEFAULT NULL COMMENT '最后修改人',
8 `LastModificationTime` datetime DEFAULT NULL COMMENT '最后修改时间', 8 `LastModificationTime` datetime DEFAULT NULL COMMENT '最后修改时间',
9 - `LocationId` varchar(50) NOT NULL COMMENT '门店Id(对应location.Id)',  
10 `SupportPhone` varchar(100) NOT NULL COMMENT 'Support 电话', 9 `SupportPhone` varchar(100) NOT NULL COMMENT 'Support 电话',
11 `SupportEmail` varchar(200) NOT NULL COMMENT 'Support 邮箱', 10 `SupportEmail` varchar(200) NOT NULL COMMENT 'Support 邮箱',
12 PRIMARY KEY (`Id`), 11 PRIMARY KEY (`Id`),
13 - UNIQUE KEY `uk_fl_location_support_locationid` (`LocationId`),  
14 KEY `idx_fl_location_support_isdeleted` (`IsDeleted`) 12 KEY `idx_fl_location_support_isdeleted` (`IsDeleted`)
15 -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci COMMENT='门店 Support 联系方式';  
16 - 13 +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci COMMENT='全局 Support 联系方式';
项目相关文档/产品模块Categories接口对接说明.md
@@ -8,7 +8,9 @@ @@ -8,7 +8,9 @@
8 - **接口前缀**:宿主统一前缀为 `/api/app` 8 - **接口前缀**:宿主统一前缀为 `/api/app`
9 - **分类表**:`fl_product_category` 9 - **分类表**:`fl_product_category`
10 - **关联字段**:`fl_product.category_id` → `fl_product_category.id` 10 - **关联字段**:`fl_product.category_id` → `fl_product_category.id`
11 -- **图片字段**:`CategoryPhotoUrl`(前端字段:`categoryPhotoUrl`) 11 +- **外观字段(字符串落库,内容为 JSON 文本)**:
  12 + - `ButtonAppearance`(`buttonAppearance`):如 `["TEXT","COLOR"]`、仅图片 `["IMAGE"]`、或合法 JSON 对象/数组;兼容历史单行 `TEXT`/`COLOR`/`IMAGE`(保存时会规范为 JSON 数组,如 `["TEXT"]`)。
  13 + - `CategoryPhotoUrl`(`categoryPhotoUrl`):与外观配合的**展示数据**,同样为 **JSON 字符串**(如 `["Prep","#10B981"]`、图片 URL 数组等);若传入**非 JSON** 的纯文本(如旧数据中的 `#EC4899` 或 `/picture/...`),后端会序列化为合法 JSON 字符串再存储。列表/详情/App 树**原样返回**库中字符串,由前端解析。
12 14
13 > 说明:本文以 Swagger 为准(本地示例:`http://localhost:19001/swagger`,搜索 `ProductCategory`)。 15 > 说明:本文以 Swagger 为准(本地示例:`http://localhost:19001/swagger`,搜索 `ProductCategory`)。
14 16
@@ -57,7 +59,10 @@ Authorization: Bearer eyJhbGciOi... @@ -57,7 +59,10 @@ Authorization: Bearer eyJhbGciOi...
57 | `id` | string | 主键 | 59 | `id` | string | 主键 |
58 | `categoryCode` | string | 类别编码 | 60 | `categoryCode` | string | 类别编码 |
59 | `categoryName` | string | 类别名称 | 61 | `categoryName` | string | 类别名称 |
60 -| `categoryPhotoUrl` | string \| null | 类别图片 URL(建议用 `/picture/...`) | 62 +| `displayText` | string \| null | 按钮展示文案(空可回退 `categoryName`) |
  63 +| `categoryPhotoUrl` | string \| null | 分类展示数据,**JSON 格式字符串**(含义由前端与 `buttonAppearance` 约定) |
  64 +| `buttonAppearance` | string | 按钮外观,**JSON 格式字符串**(见上文「外观字段」) |
  65 +| `availabilityType` | string | `ALL` / `SPECIFIED`(门店可用范围) |
61 | `state` | boolean | 是否启用 | 66 | `state` | boolean | 是否启用 |
62 | `orderNum` | number | 排序 | 67 | `orderNum` | number | 排序 |
63 | `lastEdited` | string | 最后编辑时间 | 68 | `lastEdited` | string | 最后编辑时间 |
@@ -75,7 +80,10 @@ Authorization: Bearer eyJhbGciOi... @@ -75,7 +80,10 @@ Authorization: Bearer eyJhbGciOi...
75 "id": "a2696b9e-2277-11f1-b4c6-00163e0c7c4f", 80 "id": "a2696b9e-2277-11f1-b4c6-00163e0c7c4f",
76 "categoryCode": "CAT_PREP", 81 "categoryCode": "CAT_PREP",
77 "categoryName": "Prep", 82 "categoryName": "Prep",
78 - "categoryPhotoUrl": "/picture/category/20260325123010_xxx.png", 83 + "displayText": "Prep",
  84 + "categoryPhotoUrl": "[\"Prep\",\"#10B981\"]",
  85 + "buttonAppearance": "[\"TEXT\",\"COLOR\"]",
  86 + "availabilityType": "ALL",
79 "state": true, 87 "state": true,
80 "orderNum": 100, 88 "orderNum": 100,
81 "lastEdited": "2026-03-25 12:30:10" 89 "lastEdited": "2026-03-25 12:30:10"
@@ -108,7 +116,11 @@ Authorization: Bearer eyJhbGciOi... @@ -108,7 +116,11 @@ Authorization: Bearer eyJhbGciOi...
108 "id": "a2696b9e-2277-11f1-b4c6-00163e0c7c4f", 116 "id": "a2696b9e-2277-11f1-b4c6-00163e0c7c4f",
109 "categoryCode": "CAT_PREP", 117 "categoryCode": "CAT_PREP",
110 "categoryName": "Prep", 118 "categoryName": "Prep",
111 - "categoryPhotoUrl": "/picture/category/20260325123010_xxx.png", 119 + "displayText": "Prep",
  120 + "categoryPhotoUrl": "[\"Prep\",\"#10B981\"]",
  121 + "buttonAppearance": "[\"TEXT\",\"COLOR\"]",
  122 + "availabilityType": "ALL",
  123 + "locationIds": [],
112 "state": true, 124 "state": true,
113 "orderNum": 100 125 "orderNum": 100
114 } 126 }
@@ -130,7 +142,11 @@ Authorization: Bearer eyJhbGciOi... @@ -130,7 +142,11 @@ Authorization: Bearer eyJhbGciOi...
130 |------|------|------|------| 142 |------|------|------|------|
131 | `categoryCode` | string | 是 | 类别编码(唯一) | 143 | `categoryCode` | string | 是 | 类别编码(唯一) |
132 | `categoryName` | string | 是 | 类别名称(唯一) | 144 | `categoryName` | string | 是 | 类别名称(唯一) |
133 -| `categoryPhotoUrl` | string \| null | 否 | 图片 URL(建议先上传图片拿到 `/picture/...` 再保存) | 145 +| `displayText` | string \| null | 否 | 按钮展示文案 |
  146 +| `categoryPhotoUrl` | string \| null | 否 | **JSON 字符串**;与 `buttonAppearance` 配合(见概述)。纯路径等非 JSON 文本会被后端包成 JSON 字符串存储。 |
  147 +| `buttonAppearance` | string | 否 | **JSON 字符串**;未传或空白时后端默认 `["TEXT"]`。兼容传 `TEXT`/`COLOR`/`IMAGE` 单行(会规范为 `["TEXT"]` 等)。非法非 JSON 且非上述三者时报错。 |
  148 +| `availabilityType` | string | 否 | `ALL`(默认)或 `SPECIFIED` |
  149 +| `locationIds` | string[] | 条件 | `availabilityType=SPECIFIED` 时必填且至少 1 个门店 Id |
134 | `state` | boolean | 否 | 是否启用(默认 true) | 150 | `state` | boolean | 否 | 是否启用(默认 true) |
135 | `orderNum` | number | 否 | 排序(默认 0) | 151 | `orderNum` | number | 否 | 排序(默认 0) |
136 152
@@ -140,7 +156,11 @@ Authorization: Bearer eyJhbGciOi... @@ -140,7 +156,11 @@ Authorization: Bearer eyJhbGciOi...
140 { 156 {
141 "categoryCode": "CAT_PREP", 157 "categoryCode": "CAT_PREP",
142 "categoryName": "Prep", 158 "categoryName": "Prep",
143 - "categoryPhotoUrl": "/picture/category/20260325123010_xxx.png", 159 + "displayText": "Prep",
  160 + "buttonAppearance": "[\"TEXT\",\"COLOR\"]",
  161 + "categoryPhotoUrl": "[\"Prep\",\"#10B981\"]",
  162 + "availabilityType": "ALL",
  163 + "locationIds": [],
144 "state": true, 164 "state": true,
145 "orderNum": 100 165 "orderNum": 100
146 } 166 }
@@ -162,7 +182,11 @@ Authorization: Bearer eyJhbGciOi... @@ -162,7 +182,11 @@ Authorization: Bearer eyJhbGciOi...
162 { 182 {
163 "categoryCode": "CAT_PREP", 183 "categoryCode": "CAT_PREP",
164 "categoryName": "Prep", 184 "categoryName": "Prep",
165 - "categoryPhotoUrl": "/picture/category/20260325123010_xxx.png", 185 + "displayText": "Prep",
  186 + "buttonAppearance": "[\"TEXT\",\"COLOR\"]",
  187 + "categoryPhotoUrl": "[\"Prep\",\"#10B981\"]",
  188 + "availabilityType": "ALL",
  189 + "locationIds": [],
166 "state": true, 190 "state": true,
167 "orderNum": 100 191 "orderNum": 100
168 } 192 }
@@ -179,7 +203,7 @@ Authorization: Bearer eyJhbGciOi... @@ -179,7 +203,7 @@ Authorization: Bearer eyJhbGciOi...
179 203
180 ### 约束 204 ### 约束
181 205
182 -- 若该类别已被 `fl_label` 引用(`fl_label.LabelCategoryId = id`),删除会失败并返回友好提示:`该类别已被标签引用,无法删除`。 206 +- 若该类别已被 `fl_product` 引用(`fl_product.CategoryId = id`),删除会失败并返回友好提示:`该类别已被产品引用,无法删除`。
183 207
184 ### 请求示例 208 ### 请求示例
185 209
@@ -200,5 +224,5 @@ Authorization: Bearer eyJhbGciOi... @@ -200,5 +224,5 @@ Authorization: Bearer eyJhbGciOi...
200 推荐前端流程: 224 推荐前端流程:
201 225
202 1. 调用上传接口 `POST /api/app/picture/category/upload` 拿到响应 `url` 226 1. 调用上传接口 `POST /api/app/picture/category/upload` 拿到响应 `url`
203 -2. 新增/编辑类别时把 `categoryPhotoUrl` 设为该 `url` 227 +2. 新增/编辑类别时:若采用 **JSON** 存展示数据,将 `url` 写入你方约定的 JSON 结构(例如 `["IMAGE","/picture/..."]`);若仍传**纯路径字符串**,后端会将其序列化为 JSON 字符串再入库(与仅图片场景兼容)。
204 228
项目相关文档/平台端Categories图片上传接口说明.md
@@ -54,7 +54,7 @@ curl -X POST &quot;http://localhost:19001/api/app/picture/category/upload&quot; ^ @@ -54,7 +54,7 @@ curl -X POST &quot;http://localhost:19001/api/app/picture/category/upload&quot; ^
54 54
55 | 字段 | 类型 | 说明 | 55 | 字段 | 类型 | 说明 |
56 |------|------|------| 56 |------|------|------|
57 -| `url` | string | 图片访问的相对路径,可直接保存到 `CategoryPhotoUrl`(例如:`/picture/category/xxx.png`) | 57 +| `url` | string | 图片访问的相对路径;写入分类接口时,若 `categoryPhotoUrl` 采用 **JSON** 存展示数据,请将该 `url` 放入你方约定的 JSON 结构中。若仍传**纯路径字符串**,后端会序列化为 JSON 字符串再入库。 |
58 | `fileName` | string | 服务器保存的文件名 | 58 | `fileName` | string | 服务器保存的文件名 |
59 | `size` | number | 文件大小(字节) | 59 | `size` | number | 文件大小(字节) |
60 60
@@ -96,7 +96,7 @@ curl -X POST &quot;http://localhost:19001/api/app/picture/category/upload&quot; ^ @@ -96,7 +96,7 @@ curl -X POST &quot;http://localhost:19001/api/app/picture/category/upload&quot; ^
96 推荐前端流程: 96 推荐前端流程:
97 97
98 1. 调用本上传接口,拿到返回的 `url` 98 1. 调用本上传接口,拿到返回的 `url`
99 -2. 再调用分类新增/编辑接口,把 `categoryPhotoUrl` 设置为该 `url` 99 +2. 再调用分类新增/编辑接口:按平台与 **`buttonAppearance`(JSON 字符串)** 的约定组装 `categoryPhotoUrl`(JSON);或继续传纯 `url` 由后端自动包成 JSON 字符串。
100 100
101 -> 说明:分类 CRUD 已支持 `CategoryPhotoUrl` 字段;你只需要在页面表单里新增该字段即可 101 +> 说明:详见 `项目相关文档/产品模块Categories接口对接说明.md`、`项目相关文档/标签模块接口对接说明.md` 中「JSON 字符串」约定
102 102
项目相关文档/本次新增与优化接口汇总.md
@@ -42,8 +42,8 @@ @@ -42,8 +42,8 @@
42 42
43 - `fl_product_category`(主表)关键字段: 43 - `fl_product_category`(主表)关键字段:
44 - `DisplayText`:按钮展示文案(为空可回退 `CategoryName`) 44 - `DisplayText`:按钮展示文案(为空可回退 `CategoryName`)
45 - - `ButtonAppearance`:`TEXT/COLOR/IMAGE`(前端按类型解析)  
46 - - `CategoryPhotoUrl`:与 `ButtonAppearance` 配合——`COLOR` 存颜色值、`IMAGE` 存图片 URL;`TEXT` 可空或不用 45 + - `ButtonAppearance`:**JSON 格式字符串**落库(如 `["TEXT","COLOR"]`、`["IMAGE"]`);兼容历史单行 `TEXT`/`COLOR`/`IMAGE`(保存时规范为 JSON 数组)
  46 + - `CategoryPhotoUrl`:**JSON 格式字符串**落库(展示数据由前端解析);非 JSON 纯文本(色值、URL 等)保存时会被后端包成 JSON 字符串
47 - **已删除列**(需在库上执行 `DROP COLUMN`):`ButtonTextColor`、`ButtonBgColor`、`ButtonImageUrl`、`ButtonStyleJson` 47 - **已删除列**(需在库上执行 `DROP COLUMN`):`ButtonTextColor`、`ButtonBgColor`、`ButtonImageUrl`、`ButtonStyleJson`
48 - `AvailabilityType`:`ALL/SPECIFIED`(门店可用范围) 48 - `AvailabilityType`:`ALL/SPECIFIED`(门店可用范围)
49 - `fl_product_category_location`(关联表): 49 - `fl_product_category_location`(关联表):
@@ -69,7 +69,7 @@ @@ -69,7 +69,7 @@
69 - **路径**:`/api/app/product-category/{id}` 69 - **路径**:`/api/app/product-category/{id}`
70 - **新增返回字段**: 70 - **新增返回字段**:
71 - `displayText` 71 - `displayText`
72 - - `buttonAppearance`、`categoryPhotoUrl`(COLOR/IMAGE 的展示数据统一在此字段 72 + - `buttonAppearance`、`categoryPhotoUrl`(**JSON 格式字符串**,展示语义由前端解析
73 - `availabilityType` 73 - `availabilityType`
74 - `locationIds`(当 `availabilityType=SPECIFIED` 返回门店 Id 列表,否则为空数组) 74 - `locationIds`(当 `availabilityType=SPECIFIED` 返回门店 Id 列表,否则为空数组)
75 75
@@ -99,7 +99,7 @@ @@ -99,7 +99,7 @@
99 99
100 - `availabilityType` 仅允许 `ALL/SPECIFIED` 100 - `availabilityType` 仅允许 `ALL/SPECIFIED`
101 - `SPECIFIED` 时 `locationIds` 至少 1 个 101 - `SPECIFIED` 时 `locationIds` 至少 1 个
102 -- `buttonAppearance` 仅允许 `TEXT/COLOR/IMAGE`(接口层校验枚举;`categoryPhotoUrl` 是否必填由业务/前端约定,`COLOR/IMAGE` 时应写入该字段) 102 +- `buttonAppearance`:须为 **合法 JSON**(任意对象/数组),或为兼容的 **`TEXT`/`COLOR`/`IMAGE` 单行**;其它字符串拒绝。`categoryPhotoUrl` 非空且非 JSON 时后端会序列化为 JSON 字符串存储;是否必填由业务/前端约定
103 103
104 --- 104 ---
105 105
@@ -111,8 +111,8 @@ @@ -111,8 +111,8 @@
111 111
112 - `fl_label_category`(主表)关键字段: 112 - `fl_label_category`(主表)关键字段:
113 - `DisplayText`:按钮展示文案(为空可回退 `CategoryName`) 113 - `DisplayText`:按钮展示文案(为空可回退 `CategoryName`)
114 - - `ButtonAppearance`:`TEXT/COLOR/IMAGE`(前端按类型解析)  
115 - - `CategoryPhotoUrl`:与 `ButtonAppearance` 配合——`COLOR` 存颜色值、`IMAGE` 存图片 URL;`TEXT` 可空或不用 114 + - `ButtonAppearance`:**JSON 格式字符串**落库(如 `["TEXT","COLOR"]`、`["IMAGE"]`);兼容历史单行 `TEXT`/`COLOR`/`IMAGE`(保存时规范为 JSON 数组)
  115 + - `CategoryPhotoUrl`:**JSON 格式字符串**落库(展示数据由前端解析);非 JSON 纯文本(色值、URL 等)保存时会被后端包成 JSON 字符串
116 - **已删除列**(需在库上执行 `DROP COLUMN`):`ButtonTextColor`、`ButtonBgColor`、`ButtonImageUrl`、`ButtonStyleJson` 116 - **已删除列**(需在库上执行 `DROP COLUMN`):`ButtonTextColor`、`ButtonBgColor`、`ButtonImageUrl`、`ButtonStyleJson`
117 - `AvailabilityType`:`ALL/SPECIFIED`(门店可用范围) 117 - `AvailabilityType`:`ALL/SPECIFIED`(门店可用范围)
118 - `fl_label_category_location`(关联表): 118 - `fl_label_category_location`(关联表):
@@ -138,7 +138,7 @@ @@ -138,7 +138,7 @@
138 - **路径**:`/api/app/label-category/{id}` 138 - **路径**:`/api/app/label-category/{id}`
139 - **新增返回字段**: 139 - **新增返回字段**:
140 - `displayText` 140 - `displayText`
141 - - `buttonAppearance`、`categoryPhotoUrl`(COLOR/IMAGE 的展示数据统一在此字段 141 + - `buttonAppearance`、`categoryPhotoUrl`(**JSON 格式字符串**,展示语义由前端解析
142 - `availabilityType` 142 - `availabilityType`
143 - `locationIds`(当 `availabilityType=SPECIFIED` 返回门店 Id 列表,否则为空数组) 143 - `locationIds`(当 `availabilityType=SPECIFIED` 返回门店 Id 列表,否则为空数组)
144 144
@@ -168,14 +168,14 @@ @@ -168,14 +168,14 @@
168 168
169 - `availabilityType` 仅允许 `ALL/SPECIFIED` 169 - `availabilityType` 仅允许 `ALL/SPECIFIED`
170 - `SPECIFIED` 时 `locationIds` 至少 1 个 170 - `SPECIFIED` 时 `locationIds` 至少 1 个
171 -- `buttonAppearance` 仅允许 `TEXT/COLOR/IMAGE`(接口层校验枚举;`categoryPhotoUrl` 是否必填由业务/前端约定,`COLOR/IMAGE` 时应写入该字段) 171 +- `buttonAppearance`:须为 **合法 JSON**(任意对象/数组),或为兼容的 **`TEXT`/`COLOR`/`IMAGE` 单行**;其它字符串拒绝。`categoryPhotoUrl` 非空且非 JSON 时后端会序列化为 JSON 字符串存储;是否必填由业务/前端约定
172 172
173 --- 173 ---
174 174
175 ## 4. App 端 `GET /api/app/us-app-labeling/labeling-tree` 175 ## 4. App 端 `GET /api/app/us-app-labeling/labeling-tree`
176 176
177 -- **L1(标签分类)节点**:除原有 `categoryName`、`categoryPhotoUrl`、`orderNum` 等外,**返回 `buttonAppearance`**(缺省或空时后端按 `TEXT` 规范化为大写)。  
178 -- **L2(产品分类)节点**:仅 `buttonAppearance` + `categoryPhotoUrl` 承载外观数据(已不再返回 `buttonTextColor`、`buttonBgColor`、`buttonImageUrl`、`buttonStyleJson`)。 177 +- **L1(标签分类)节点**:返回 `categoryPhotoUrl`、`buttonAppearance`(均为库中字符串,**多为 JSON**,与 CRUD 一致);缺省或空时 `buttonAppearance` 后端默认 **`"TEXT"`**(兼容旧数据,**不再**对整段做 `ToUpperInvariant` 以免破坏 JSON)。
  178 +- **L2(产品分类)节点**:返回 `displayText`、`buttonAppearance`、`categoryPhotoUrl`、`availabilityType`、`orderNum` 等;外观数据由 **`buttonAppearance` + `categoryPhotoUrl`** 承载(已不再返回 `buttonTextColor`、`buttonBgColor`、`buttonImageUrl`、`buttonStyleJson`)。
179 179
180 ### 4.1 数据库迁移(两张主表) 180 ### 4.1 数据库迁移(两张主表)
181 181
项目相关文档/标签模块接口对接说明.md
@@ -51,6 +51,12 @@ Swagger 地址: @@ -51,6 +51,12 @@ Swagger 地址:
51 } 51 }
52 ``` 52 ```
53 53
  54 +### 1.1.1 字段约定:`buttonAppearance` 与 `categoryPhotoUrl`(JSON 字符串)
  55 +
  56 +- **`buttonAppearance`**:库中存 **JSON 文本**(如 `["TEXT","COLOR"]`、仅图片 `["IMAGE"]` 等);兼容历史单行 `TEXT`/`COLOR`/`IMAGE`(保存时规范为 `["TEXT"]` 等)。未传或空白时后端默认 `["TEXT"]`。非法值(非 JSON 且非上述三者)会返回友好错误。
  57 +- **`categoryPhotoUrl`**:同样为 **JSON 文本**(如 `["Prep","#10B981"]`);若传**非 JSON** 的纯文本(色值、`/picture/...` 等),后端会序列化为合法 JSON 字符串再存储。列表/详情/App 树**原样返回**字符串,由客户端解析。
  58 +- 其它常用字段:`displayText`、`availabilityType`(`ALL`/`SPECIFIED`)、`locationIds`(指定门店时必填),与产品类别接口语义一致(见 `项目相关文档/产品模块Categories接口对接说明.md`)。
  59 +
54 ### 1.2 详情 60 ### 1.2 详情
55 61
56 方法:`GET /api/app/label-category/{id}` 62 方法:`GET /api/app/label-category/{id}`
@@ -69,7 +75,11 @@ Swagger 地址: @@ -69,7 +75,11 @@ Swagger 地址:
69 { 75 {
70 "categoryCode": "CAT_PREP", 76 "categoryCode": "CAT_PREP",
71 "categoryName": "Prep", 77 "categoryName": "Prep",
72 - "categoryPhotoUrl": "https://cdn.example.com/cat-prep.png", 78 + "displayText": "Prep",
  79 + "buttonAppearance": "[\"TEXT\",\"COLOR\"]",
  80 + "categoryPhotoUrl": "[\"Prep\",\"#10B981\"]",
  81 + "availabilityType": "ALL",
  82 + "locationIds": [],
73 "state": true, 83 "state": true,
74 "orderNum": 1 84 "orderNum": 1
75 } 85 }
@@ -85,7 +95,11 @@ Swagger 地址: @@ -85,7 +95,11 @@ Swagger 地址:
85 { 95 {
86 "categoryCode": "CAT_PREP", 96 "categoryCode": "CAT_PREP",
87 "categoryName": "Prep", 97 "categoryName": "Prep",
88 - "categoryPhotoUrl": null, 98 + "displayText": "Prep",
  99 + "buttonAppearance": "[\"TEXT\",\"COLOR\"]",
  100 + "categoryPhotoUrl": "[\"Prep\",\"#10B981\"]",
  101 + "availabilityType": "ALL",
  102 + "locationIds": [],
89 "state": true, 103 "state": true,
90 "orderNum": 2 104 "orderNum": 2
91 } 105 }
@@ -706,7 +720,8 @@ ADD UNIQUE KEY `uk_fl_ltpd_template_product_label_type` (`TemplateId`, `ProductI @@ -706,7 +720,8 @@ ADD UNIQUE KEY `uk_fl_ltpd_template_product_label_type` (`TemplateId`, `ProductI
706 |------|------|------| 720 |------|------|------|
707 | `id` | string | `fl_label_category.Id` | 721 | `id` | string | `fl_label_category.Id` |
708 | `categoryName` | string | 分类名称 | 722 | `categoryName` | string | 分类名称 |
709 -| `categoryPhotoUrl` | string \| null | 分类图标/图 | 723 +| `categoryPhotoUrl` | string \| null | 分类展示数据,**JSON 格式字符串**(与库中 `CategoryPhotoUrl` 一致,客户端解析) |
  724 +| `buttonAppearance` | string | 按钮外观,**JSON 格式字符串**(与库中 `ButtonAppearance` 一致;空时后端默认 `"TEXT"`) |
710 | `orderNum` | number | 排序 | 725 | `orderNum` | number | 排序 |
711 | `productCategories` | array | 第二级列表(见下表) | 726 | `productCategories` | array | 第二级列表(见下表) |
712 727
@@ -715,8 +730,12 @@ ADD UNIQUE KEY `uk_fl_ltpd_template_product_label_type` (`TemplateId`, `ProductI @@ -715,8 +730,12 @@ ADD UNIQUE KEY `uk_fl_ltpd_template_product_label_type` (`TemplateId`, `ProductI
715 | 字段 | 类型 | 说明 | 730 | 字段 | 类型 | 说明 |
716 |------|------|------| 731 |------|------|------|
717 | `categoryId` | string \| null | 产品分类Id;产品未归类或分类不存在时为空 | 732 | `categoryId` | string \| null | 产品分类Id;产品未归类或分类不存在时为空 |
718 -| `categoryPhotoUrl` | string \| null | 产品分类图片地址;产品未归类或分类不存在时为空 | 733 +| `categoryPhotoUrl` | string \| null | 产品分类展示数据,**JSON 格式字符串**;未归类或分类不存在时为空 |
719 | `name` | string | 产品分类显示名;空源数据为 **`无`** | 734 | `name` | string | 产品分类显示名;空源数据为 **`无`** |
  735 +| `displayText` | string \| null | 按钮展示文案 |
  736 +| `buttonAppearance` | string | **JSON 格式字符串**(与库中一致;空时默认 `"TEXT"`) |
  737 +| `availabilityType` | string | `ALL` / `SPECIFIED`(树已按当前门店过滤,仍返回供客户端展示) |
  738 +| `orderNum` | number | 排序 |
720 | `itemCount` | number | 该分类下 **产品个数**(去重后的产品数) | 739 | `itemCount` | number | 该分类下 **产品个数**(去重后的产品数) |
721 | `products` | array | 第三级产品列表(见下表) | 740 | `products` | array | 第三级产品列表(见下表) |
722 741
@@ -771,13 +790,18 @@ curl -X GET &quot;http://localhost:19001/api/app/us-app-labeling/labeling-tree?locati @@ -771,13 +790,18 @@ curl -X GET &quot;http://localhost:19001/api/app/us-app-labeling/labeling-tree?locati
771 { 790 {
772 "id": "cat-prep-id", 791 "id": "cat-prep-id",
773 "categoryName": "Prep", 792 "categoryName": "Prep",
774 - "categoryPhotoUrl": "/picture/...", 793 + "categoryPhotoUrl": "[\"Prep\",\"#10B981\"]",
  794 + "buttonAppearance": "[\"TEXT\",\"COLOR\"]",
775 "orderNum": 1, 795 "orderNum": 1,
776 "productCategories": [ 796 "productCategories": [
777 { 797 {
778 "categoryId": "pc-meat-id", 798 "categoryId": "pc-meat-id",
779 - "categoryPhotoUrl": "/picture/product-category/20260325123010_xxx.png", 799 + "categoryPhotoUrl": "[\"/picture/product-category/20260325123010_xxx.png\"]",
780 "name": "Meat", 800 "name": "Meat",
  801 + "displayText": "Meat",
  802 + "buttonAppearance": "[\"IMAGE\"]",
  803 + "availabilityType": "ALL",
  804 + "orderNum": 10,
781 "itemCount": 1, 805 "itemCount": 1,
782 "products": [ 806 "products": [
783 { 807 {
项目相关文档/美国版App登录接口说明.md
@@ -285,54 +285,53 @@ Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9... @@ -285,54 +285,53 @@ Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
285 285
286 --- 286 ---
287 287
288 -## 接口 6:门店 Support 联系方式(App 展示 288 +## 接口 6:全局 Support 联系方式(App 只读 + Web 可读
289 289
290 -用于 App「Support」页面读取联系方式(电话、邮箱)。  
291 -后台由 `LocationSupportAppService` 维护,规则已调整为 **全门店共用一条全局联系方式**。 290 +用于 App「Support」页与 Web 展示**全平台共用**的一条电话与邮箱。
  291 +实现:`LocationSupportAppService`,表 `fl_location_support` **不再包含门店 Id**。
292 292
293 -### HTTP 293 +### HTTP(查询)
294 294
295 - **方法**:`GET` 295 - **方法**:`GET`
296 -- **路径**:`/api/app/location-support/by-location-id?locationId={locationId}`(以 Swagger 中 `LocationSupport` 为准)  
297 -- **鉴权**:需要登录(`Authorization: Bearer {token}`) 296 +- **路径**(约定式 API,以 Swagger 中 `LocationSupport` → `GetSupport` 为准):一般为 **`/api/app/location-support/support`**
  297 +- **鉴权**:需要登录(`Authorization: Bearer {token}`)。**App 登录 Token 与 Web Token 均可调用本接口。**
298 298
299 ### 请求参数 299 ### 请求参数
300 300
301 -| 参数名 | 位置 | 类型 | 必填 | 说明 |  
302 -|--------|------|------|------|------|  
303 -| `locationId` | Query | string | 是 | 门店主键(Guid 字符串)。当前用于入参兼容,返回值按全局联系方式配置 | 301 +无 Query / Body。
304 302
305 ### 响应体(LocationSupportGetOutputDto) 303 ### 响应体(LocationSupportGetOutputDto)
306 304
307 | 字段(JSON) | 类型 | 说明 | 305 | 字段(JSON) | 类型 | 说明 |
308 |--------------|------|------| 306 |--------------|------|------|
309 -| `id` | string | 联系方式主键 |  
310 -| `locationId` | string | 配置记录中的门店主键(仅作兼容字段) |  
311 -| `locationName` | string \| null | 配置记录中的门店名称(仅作兼容字段) | 307 +| `id` | string | 记录主键(Web 编辑 `PUT` 路径中的 `{id}`) |
312 | `supportPhone` | string | Support 电话 | 308 | `supportPhone` | string | Support 电话 |
313 | `supportEmail` | string | Support 邮箱 | 309 | `supportEmail` | string | Support 邮箱 |
314 310
315 -> 若门店尚未配置联系方式,接口返回 `null`。 311 +> 若尚未在后台配置,接口返回 `null`。
316 312
317 ### 响应示例 313 ### 响应示例
318 314
319 ```json 315 ```json
320 { 316 {
321 "id": "3a2f4fda-1a93-4a35-9b98-95dca7bb5d2a", 317 "id": "3a2f4fda-1a93-4a35-9b98-95dca7bb5d2a",
322 - "locationId": "a2696b9e-2277-11f1-b4c6-00163e0c7c4f",  
323 - "locationName": "Downtown Store",  
324 "supportPhone": "1-800-SUPPORT", 318 "supportPhone": "1-800-SUPPORT",
325 "supportEmail": "support@medvantage.com" 319 "supportEmail": "support@medvantage.com"
326 } 320 }
327 ``` 321 ```
328 322
  323 +### App 与 Web 权限说明
  324 +
  325 +- App 登录签发 JWT 时会写入声明 **`client_kind` = `us-app`**(与 Web 管理端 Token 区分)。
  326 +- **App 仅允许调用本节的 `GET`(查询)**;若使用 App Token 调用新增/编辑,将返回业务错误(英文):`The mobile app can only view support contacts. Please use the web console to edit.`
  327 +
329 --- 328 ---
330 329
331 -## 后台维护接口:Location Support(新增/编辑) 330 +## 后台维护接口:Location Support(Web:新增 / 编辑)
332 331
333 -仅后台管理端使用,用于配置 App Support 页面展示内容(全局唯一) 332 +仅 **Web 管理端 Token**(无 `client_kind=us-app`)可调用,用于维护全局 Support 联系方式
334 333
335 -### 接口 A:新增 Support 联系方式(全局 334 +### 接口 A:新增(全局一条
336 335
337 - **方法**:`POST` 336 - **方法**:`POST`
338 - **路径**:`/api/app/location-support` 337 - **路径**:`/api/app/location-support`
@@ -342,36 +341,46 @@ Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9... @@ -342,36 +341,46 @@ Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
342 341
343 ```json 342 ```json
344 { 343 {
345 - "locationId": "a2696b9e-2277-11f1-b4c6-00163e0c7c4f",  
346 "supportPhone": "1-800-SUPPORT", 344 "supportPhone": "1-800-SUPPORT",
347 "supportEmail": "support@medvantage.com" 345 "supportEmail": "support@medvantage.com"
348 } 346 }
349 ``` 347 ```
350 348
351 约束: 349 约束:
352 -- 系统内仅允许存在一条未删除记录;若已存在,再次新增会报错:`已存在全局 Support 联系方式,请使用编辑接口`  
353 350
354 -### 接口 B:编辑 Support 联系方式(全局) 351 +- 系统内仅允许存在一条未删除记录;若已存在,再次新增会报错:`Global support contact already exists. Use update instead.`
  352 +
  353 +### 接口 B:编辑
355 354
356 - **方法**:`PUT` 355 - **方法**:`PUT`
357 - **路径**:`/api/app/location-support/{id}` 356 - **路径**:`/api/app/location-support/{id}`
358 - **Content-Type**:`application/json` 357 - **Content-Type**:`application/json`
359 358
360 -请求体(LocationSupportUpdateInputVo)与新增一致 359 +请求体(LocationSupportUpdateInputVo)
361 360
362 ```json 361 ```json
363 { 362 {
364 - "locationId": "a2696b9e-2277-11f1-b4c6-00163e0c7c4f",  
365 "supportPhone": "1-800-SUPPORT", 363 "supportPhone": "1-800-SUPPORT",
366 "supportEmail": "support@medvantage.com" 364 "supportEmail": "support@medvantage.com"
367 } 365 }
368 ``` 366 ```
369 367
370 -常见错误:  
371 -- `门店Id不能为空` / `门店Id格式不正确`  
372 -- `Support 电话不能为空`  
373 -- `Support 邮箱不能为空` / `Support 邮箱格式不正确`  
374 -- `系统仅允许一条全局 Support 联系方式` 368 +常见错误(英文):
  369 +
  370 +- `The mobile app can only view support contacts. Please use the web console to edit.`
  371 +- `Support phone is required.` / `Support email is required.` / `Support email format is invalid.`
  372 +- `Global support contact already exists. Use update instead.`
  373 +- `Support record not found.` / `Support record id is required.`
  374 +
  375 +### 数据库迁移(删除 `LocationId`)
  376 +
  377 +若线上表仍为旧结构(含 `LocationId`),请在库中执行脚本:
  378 +
  379 +- `美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/scripts/fl_location_support_alter_drop_locationid.sql`
  380 +
  381 +新建库请使用:
  382 +
  383 +- `美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/scripts/fl_location_support_create.sql`
375 384
376 --- 385 ---
377 386