Commit ecb291fd1960e19566da9b022f461e5ba4d5b45d

Authored by 李曜臣
1 parent 87313aec

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

Showing 21 changed files with 300 additions and 216 deletions
本次新增与优化接口汇总(1).md
... ... @@ -39,9 +39,9 @@
39 39  
40 40 - `fl_product_category`(主表)关键字段:
41 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 45 - `AvailabilityType`:`ALL/SPECIFIED`(门店可用范围)
46 46 - `fl_product_category_location`(关联表):
47 47 - `(CategoryId, LocationId)` 唯一约束;用于 `AvailabilityType=SPECIFIED` 指定门店
... ... @@ -56,7 +56,7 @@
56 56 - **路径**:`/api/app/product-category`
57 57 - **列表行新增返回**:
58 58 - `displayText`
59   - - `buttonAppearance`
  59 + - `buttonAppearance`、`categoryPhotoUrl`(均为字符串,内容多为 JSON)
60 60 - `availabilityType`
61 61  
62 62 #### 2.2.2 详情
... ... @@ -65,7 +65,7 @@
65 65 - **路径**:`/api/app/product-category/{id}`
66 66 - **新增返回字段**:
67 67 - `displayText`
68   - - `buttonAppearance/buttonTextColor/buttonBgColor/buttonImageUrl/buttonStyleJson`
  68 + - `buttonAppearance`、`categoryPhotoUrl`
69 69 - `availabilityType`
70 70 - `locationIds`(当 `availabilityType=SPECIFIED` 返回门店 Id 列表,否则为空数组)
71 71  
... ... @@ -75,7 +75,7 @@
75 75 - **路径**:`/api/app/product-category`
76 76 - **新增入参字段**:
77 77 - `displayText`
78   - - `buttonAppearance/buttonTextColor/buttonBgColor/buttonImageUrl/buttonStyleJson`
  78 + - `buttonAppearance`、`categoryPhotoUrl`(JSON 字符串约定,详见 `项目相关文档/产品模块Categories接口对接说明.md`)
79 79 - `availabilityType`
80 80 - `locationIds`(当 `availabilityType=SPECIFIED` 必填且至少 1 个)
81 81  
... ... @@ -95,9 +95,7 @@
95 95  
96 96 - `availabilityType` 仅允许 `ALL/SPECIFIED`
97 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 107  
110 108 - `fl_label_category`(主表)关键字段:
111 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 113 - `AvailabilityType`:`ALL/SPECIFIED`(门店可用范围)
116 114 - `fl_label_category_location`(关联表):
117 115 - `(CategoryId, LocationId)` 唯一约束;用于 `AvailabilityType=SPECIFIED` 指定门店(`LocationId` 对应 `location` 表主键)
... ... @@ -126,7 +124,7 @@
126 124 - **路径**:`/api/app/label-category`
127 125 - **列表行新增返回**:
128 126 - `displayText`
129   - - `buttonAppearance`
  127 + - `buttonAppearance`、`categoryPhotoUrl`(均为字符串,内容多为 JSON)
130 128 - `availabilityType`
131 129  
132 130 #### 3.2.2 详情
... ... @@ -135,7 +133,7 @@
135 133 - **路径**:`/api/app/label-category/{id}`
136 134 - **新增返回字段**:
137 135 - `displayText`
138   - - `buttonAppearance/buttonTextColor/buttonBgColor/buttonImageUrl/buttonStyleJson`
  136 + - `buttonAppearance`、`categoryPhotoUrl`
139 137 - `availabilityType`
140 138 - `locationIds`(当 `availabilityType=SPECIFIED` 返回门店 Id 列表,否则为空数组)
141 139  
... ... @@ -145,7 +143,7 @@
145 143 - **路径**:`/api/app/label-category`
146 144 - **新增入参字段**:
147 145 - `displayText`
148   - - `buttonAppearance/buttonTextColor/buttonBgColor/buttonImageUrl/buttonStyleJson`
  146 + - `buttonAppearance`、`categoryPhotoUrl`(JSON 字符串约定,详见 `项目相关文档/标签模块接口对接说明.md`)
149 147 - `availabilityType`
150 148 - `locationIds`(当 `availabilityType=SPECIFIED` 必填且至少 1 个)
151 149  
... ... @@ -165,7 +163,5 @@
165 163  
166 164 - `availabilityType` 仅允许 `ALL/SPECIFIED`
167 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 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 60 ### 1.2 详情
55 61  
56 62 方法:`GET /api/app/label-category/{id}`
... ... @@ -69,7 +75,11 @@ Swagger 地址:
69 75 {
70 76 "categoryCode": "CAT_PREP",
71 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 83 "state": true,
74 84 "orderNum": 1
75 85 }
... ... @@ -85,7 +95,11 @@ Swagger 地址:
85 95 {
86 96 "categoryCode": "CAT_PREP",
87 97 "categoryName": "Prep",
88   - "categoryPhotoUrl": null,
  98 + "displayText": "Prep",
  99 + "buttonAppearance": "[\"TEXT\",\"COLOR\"]",
  100 + "categoryPhotoUrl": "[\"Prep\",\"#10B981\"]",
  101 + "availabilityType": "ALL",
  102 + "locationIds": [],
89 103 "state": true,
90 104 "orderNum": 2
91 105 }
... ... @@ -706,7 +720,8 @@ ADD UNIQUE KEY `uk_fl_ltpd_template_product_label_type` (`TemplateId`, `ProductI
706 720 |------|------|------|
707 721 | `id` | string | `fl_label_category.Id` |
708 722 | `categoryName` | string | 分类名称 |
709   -| `categoryPhotoUrl` | string \| null | 分类图标/图 |
  723 +| `categoryPhotoUrl` | string \| null | 分类展示数据,**JSON 格式字符串**(与库中 `CategoryPhotoUrl` 一致,客户端解析) |
  724 +| `buttonAppearance` | string | 按钮外观,**JSON 格式字符串**(与库中 `ButtonAppearance` 一致;空时后端默认 `"TEXT"`) |
710 725 | `orderNum` | number | 排序 |
711 726 | `productCategories` | array | 第二级列表(见下表) |
712 727  
... ... @@ -715,8 +730,12 @@ ADD UNIQUE KEY `uk_fl_ltpd_template_product_label_type` (`TemplateId`, `ProductI
715 730 | 字段 | 类型 | 说明 |
716 731 |------|------|------|
717 732 | `categoryId` | string \| null | 产品分类Id;产品未归类或分类不存在时为空 |
718   -| `categoryPhotoUrl` | string \| null | 产品分类图片地址;产品未归类或分类不存在时为空 |
  733 +| `categoryPhotoUrl` | string \| null | 产品分类展示数据,**JSON 格式字符串**;未归类或分类不存在时为空 |
719 734 | `name` | string | 产品分类显示名;空源数据为 **`无`** |
  735 +| `displayText` | string \| null | 按钮展示文案 |
  736 +| `buttonAppearance` | string | **JSON 格式字符串**(与库中一致;空时默认 `"TEXT"`) |
  737 +| `availabilityType` | string | `ALL` / `SPECIFIED`(树已按当前门店过滤,仍返回供客户端展示) |
  738 +| `orderNum` | number | 排序 |
720 739 | `itemCount` | number | 该分类下 **产品个数**(去重后的产品数) |
721 740 | `products` | array | 第三级产品列表(见下表) |
722 741  
... ... @@ -771,13 +790,18 @@ curl -X GET "http://localhost:19001/api/app/us-app-labeling/labeling-tree?locati
771 790 {
772 791 "id": "cat-prep-id",
773 792 "categoryName": "Prep",
774   - "categoryPhotoUrl": "/picture/...",
  793 + "categoryPhotoUrl": "[\"Prep\",\"#10B981\"]",
  794 + "buttonAppearance": "[\"TEXT\",\"COLOR\"]",
775 795 "orderNum": 1,
776 796 "productCategories": [
777 797 {
778 798 "categoryId": "pc-meat-id",
779   - "categoryPhotoUrl": "/picture/product-category/20260325123010_xxx.png",
  799 + "categoryPhotoUrl": "[\"/picture/product-category/20260325123010_xxx.png\"]",
780 800 "name": "Meat",
  801 + "displayText": "Meat",
  802 + "buttonAppearance": "[\"IMAGE\"]",
  803 + "availabilityType": "ALL",
  804 + "orderNum": 10,
781 805 "itemCount": 1,
782 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 5 /// </summary>
6 6 public class LocationSupportCreateInputVo
7 7 {
8   - public string LocationId { get; set; } = string.Empty;
9   -
10 8 public string SupportPhone { get; set; } = string.Empty;
11 9  
12 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 7 {
8 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 10 public string SupportPhone { get; set; } = string.Empty;
15 11  
16 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 5 /// </summary>
6 6 public class LocationSupportUpdateInputVo
7 7 {
8   - public string LocationId { get; set; } = string.Empty;
9   -
10 8 public string SupportPhone { get; set; } = string.Empty;
11 9  
12 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 4 namespace FoodLabeling.Application.Contracts.IServices;
5 5  
6 6 /// <summary>
7   -/// 门店 Support 联系方式管理(后台设置
  7 +/// 全局 Support 联系方式(全平台共用;Web 可增改查,App 仅可查
8 8 /// </summary>
9 9 public interface ILocationSupportAppService : IApplicationService
10 10 {
11 11 /// <summary>
12   - /// 按门店查询 Support 联系方式
  12 + /// 查询全局 Support 联系方式(已登录即可;App / Web 共用)
13 13 /// </summary>
14   - /// <param name="locationId">门店Id</param>
15   - Task<LocationSupportGetOutputDto?> GetByLocationIdAsync(string locationId);
  14 + Task<LocationSupportGetOutputDto?> GetSupportAsync();
16 15  
17 16 /// <summary>
18   - /// 新增门店 Support 联系方式(每个门店仅允许一条
  17 + /// 新增全局 Support 联系方式(系统仅允许一条;Web 管理端
19 18 /// </summary>
20 19 /// <param name="input">联系方式</param>
21 20 Task<LocationSupportGetOutputDto> CreateAsync(LocationSupportCreateInputVo input);
22 21  
23 22 /// <summary>
24   - /// 编辑门店 Support 联系方式
  23 + /// 编辑全局 Support 联系方式(Web 管理端)
25 24 /// </summary>
26 25 /// <param name="id">联系方式主键</param>
27 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 21  
22 22 public DateTime? LastModificationTime { get; set; }
23 23  
24   - public string LocationId { get; set; } = string.Empty;
25   -
26 24 public string SupportPhone { get; set; } = string.Empty;
27 25  
28 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 125 }
126 126  
127 127 var displayText = input.DisplayText?.Trim();
128   - var appearance = (input.ButtonAppearance ?? "TEXT").Trim().ToUpperInvariant();
  128 + var appearance = CategoryAppearanceStorageHelper.NormalizeButtonAppearanceForStorage(input.ButtonAppearance);
129 129 var availabilityType = (input.AvailabilityType ?? "ALL").Trim().ToUpperInvariant();
130   - ValidateButtonAppearance(appearance);
131 130 var locationIds = NormalizeLocationIds(input.LocationIds);
132 131 ValidateAvailabilityTypeAndLocations(availabilityType, locationIds);
133 132  
... ... @@ -146,7 +145,7 @@ public class LabelCategoryAppService : ApplicationService, ILabelCategoryAppServ
146 145 CategoryCode = code,
147 146 CategoryName = name,
148 147 DisplayText = string.IsNullOrWhiteSpace(displayText) ? null : displayText,
149   - CategoryPhotoUrl = input.CategoryPhotoUrl?.Trim(),
  148 + CategoryPhotoUrl = CategoryAppearanceStorageHelper.NormalizeCategoryPhotoUrlForStorage(input.CategoryPhotoUrl),
150 149 State = input.State,
151 150 ButtonAppearance = appearance,
152 151 AvailabilityType = availabilityType,
... ... @@ -175,9 +174,8 @@ public class LabelCategoryAppService : ApplicationService, ILabelCategoryAppServ
175 174 }
176 175  
177 176 var displayText = input.DisplayText?.Trim();
178   - var appearance = (input.ButtonAppearance ?? "TEXT").Trim().ToUpperInvariant();
  177 + var appearance = CategoryAppearanceStorageHelper.NormalizeButtonAppearanceForStorage(input.ButtonAppearance);
179 178 var availabilityType = (input.AvailabilityType ?? "ALL").Trim().ToUpperInvariant();
180   - ValidateButtonAppearance(appearance);
181 179 var locationIds = NormalizeLocationIds(input.LocationIds);
182 180 ValidateAvailabilityTypeAndLocations(availabilityType, locationIds);
183 181  
... ... @@ -191,7 +189,7 @@ public class LabelCategoryAppService : ApplicationService, ILabelCategoryAppServ
191 189 entity.CategoryCode = code;
192 190 entity.CategoryName = name;
193 191 entity.DisplayText = string.IsNullOrWhiteSpace(displayText) ? null : displayText;
194   - entity.CategoryPhotoUrl = input.CategoryPhotoUrl?.Trim();
  192 + entity.CategoryPhotoUrl = CategoryAppearanceStorageHelper.NormalizeCategoryPhotoUrlForStorage(input.CategoryPhotoUrl);
195 193 entity.State = input.State;
196 194 entity.ButtonAppearance = appearance;
197 195 entity.AvailabilityType = availabilityType;
... ... @@ -264,14 +262,6 @@ public class LabelCategoryAppService : ApplicationService, ILabelCategoryAppServ
264 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 265 private async Task SaveCategoryLocationsAsync(
276 266 string categoryId,
277 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 2 using FoodLabeling.Application.Contracts.Dtos.LocationSupport;
2 3 using FoodLabeling.Application.Contracts.IServices;
3 4 using FoodLabeling.Application.Services.DbModels;
4   -using FoodLabeling.Domain.Entities;
  5 +using Microsoft.AspNetCore.Authorization;
5 6 using Volo.Abp;
6 7 using Volo.Abp.Application.Services;
7 8 using Volo.Abp.Guids;
... ... @@ -11,8 +12,9 @@ using Yi.Framework.SqlSugarCore.Abstractions;
11 12 namespace FoodLabeling.Application.Services;
12 13  
13 14 /// <summary>
14   -/// 门店 Support 联系方式(后台设置,App 展示
  15 +/// 全局 Support 联系方式(全门店共用;Web 可增改查,App JWT 仅可读
15 16 /// </summary>
  17 +[Authorize]
16 18 public class LocationSupportAppService : ApplicationService, ILocationSupportAppService
17 19 {
18 20 private readonly ISqlSugarDbContext _dbContext;
... ... @@ -25,46 +27,36 @@ public class LocationSupportAppService : ApplicationService, ILocationSupportApp
25 27 }
26 28  
27 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 39 /// <inheritdoc />
48 40 [UnitOfWork]
49 41 public async Task<LocationSupportGetOutputDto> CreateAsync(LocationSupportCreateInputVo input)
50 42 {
  43 + EnsureNotUsAppClient();
  44 +
51 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 52 EnsureEmailFormat(email);
60 53  
61   - await EnsureLocationExistsAsync(locationId);
62   -
63 54 var existed = await _dbContext.SqlSugarClient.Queryable<FlLocationSupportDbEntity>()
64 55 .AnyAsync(x => !x.IsDeleted);
65 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 62 var now = Clock.Now;
... ... @@ -76,74 +68,60 @@ public class LocationSupportAppService : ApplicationService, ILocationSupportApp
76 68 CreatorId = CurrentUser?.Id?.ToString(),
77 69 LastModificationTime = now,
78 70 LastModifierId = CurrentUser?.Id?.ToString(),
79   - LocationId = locationId,
80 71 SupportPhone = phone,
81 72 SupportEmail = email
82 73 };
83 74  
84 75 await _dbContext.SqlSugarClient.Insertable(entity).ExecuteCommandAsync();
85   - return (await MapOutputAsync(entity))!;
  76 + return MapOutput(entity)!;
86 77 }
87 78  
88 79 /// <inheritdoc />
89 80 [UnitOfWork]
90 81 public async Task<LocationSupportGetOutputDto> UpdateAsync(string id, LocationSupportUpdateInputVo input)
91 82 {
  83 + EnsureNotUsAppClient();
  84 +
92 85 var supportId = id?.Trim();
93 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 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 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 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 109 entity.SupportPhone = phone;
125 110 entity.SupportEmail = email;
126 111 entity.LastModificationTime = Clock.Now;
127 112 entity.LastModifierId = CurrentUser?.Id?.ToString();
128 113  
129 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 127 private static string NormalizeRequired(string? value, string message)
... ... @@ -162,48 +140,22 @@ public class LocationSupportAppService : ApplicationService, ILocationSupportApp
162 140 if (!email.Contains("@", StringComparison.Ordinal) || email.StartsWith("@", StringComparison.Ordinal) ||
163 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 149 if (entity is null)
187 150 {
188 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 154 return new LocationSupportGetOutputDto
200 155 {
201 156 Id = entity.Id,
202   - LocationId = entity.LocationId,
203   - LocationName = locationName,
204 157 SupportPhone = entity.SupportPhone,
205 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 2 using FoodLabeling.Application.Contracts.Dtos.Common;
2 3 using FoodLabeling.Application.Contracts.Dtos.ProductCategory;
3 4 using FoodLabeling.Application.Contracts.IServices;
... ... @@ -128,9 +129,8 @@ public class ProductCategoryAppService : ApplicationService, IProductCategoryApp
128 129 }
129 130  
130 131 var displayText = input.DisplayText?.Trim();
131   - var appearance = (input.ButtonAppearance ?? "TEXT").Trim().ToUpperInvariant();
  132 + var appearance = CategoryAppearanceStorageHelper.NormalizeButtonAppearanceForStorage(input.ButtonAppearance);
132 133 var availabilityType = (input.AvailabilityType ?? "ALL").Trim().ToUpperInvariant();
133   - ValidateButtonAppearance(appearance);
134 134 var locationIds = NormalizeLocationIds(input.LocationIds);
135 135 ValidateAvailabilityTypeAndLocations(availabilityType, locationIds);
136 136  
... ... @@ -155,7 +155,7 @@ public class ProductCategoryAppService : ApplicationService, IProductCategoryApp
155 155 CategoryCode = code,
156 156 CategoryName = name,
157 157 DisplayText = string.IsNullOrWhiteSpace(displayText) ? null : displayText,
158   - CategoryPhotoUrl = input.CategoryPhotoUrl?.Trim(),
  158 + CategoryPhotoUrl = CategoryAppearanceStorageHelper.NormalizeCategoryPhotoUrlForStorage(input.CategoryPhotoUrl),
159 159 ButtonAppearance = appearance,
160 160 State = input.State,
161 161 AvailabilityType = availabilityType,
... ... @@ -187,9 +187,8 @@ public class ProductCategoryAppService : ApplicationService, IProductCategoryApp
187 187 }
188 188  
189 189 var displayText = input.DisplayText?.Trim();
190   - var appearance = (input.ButtonAppearance ?? "TEXT").Trim().ToUpperInvariant();
  190 + var appearance = CategoryAppearanceStorageHelper.NormalizeButtonAppearanceForStorage(input.ButtonAppearance);
191 191 var availabilityType = (input.AvailabilityType ?? "ALL").Trim().ToUpperInvariant();
192   - ValidateButtonAppearance(appearance);
193 192 var locationIds = NormalizeLocationIds(input.LocationIds);
194 193 ValidateAvailabilityTypeAndLocations(availabilityType, locationIds);
195 194  
... ... @@ -203,7 +202,7 @@ public class ProductCategoryAppService : ApplicationService, IProductCategoryApp
203 202 entity.CategoryCode = code;
204 203 entity.CategoryName = name;
205 204 entity.DisplayText = string.IsNullOrWhiteSpace(displayText) ? null : displayText;
206   - entity.CategoryPhotoUrl = input.CategoryPhotoUrl?.Trim();
  205 + entity.CategoryPhotoUrl = CategoryAppearanceStorageHelper.NormalizeCategoryPhotoUrlForStorage(input.CategoryPhotoUrl);
207 206 entity.ButtonAppearance = appearance;
208 207 entity.State = input.State;
209 208 entity.AvailabilityType = availabilityType;
... ... @@ -280,14 +279,6 @@ public class ProductCategoryAppService : ApplicationService, IProductCategoryApp
280 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 282 private async Task SaveCategoryLocationsAsync(
292 283 string categoryId,
293 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 using System.Security.Claims;
7 7 using System.Text;
8 8 using System.Threading.Tasks;
  9 +using FoodLabeling.Application.Contracts;
9 10 using FoodLabeling.Application.Contracts.Dtos.UsAppAuth;
10 11 using FoodLabeling.Application.Contracts.IServices;
11 12 using FoodLabeling.Application.Services.DbModels;
... ... @@ -432,7 +433,8 @@ public class UsAppAuthAppService : ApplicationService, IUsAppAuthAppService
432 433 var claims = new List<Claim>
433 434 {
434 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 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 129 {
130 130 var l1Appearance = string.IsNullOrWhiteSpace(g1.Key.LabelCategoryButtonAppearance)
131 131 ? "TEXT"
132   - : g1.Key.LabelCategoryButtonAppearance.Trim().ToUpperInvariant();
  132 + : g1.Key.LabelCategoryButtonAppearance.Trim();
133 133 var l1 = new UsAppLabelCategoryTreeNodeDto
134 134 {
135 135 Id = g1.Key.LabelCategoryId,
... ... @@ -178,7 +178,7 @@ public class UsAppLabelingAppService : ApplicationService, IUsAppLabelingAppServ
178 178 var productsGrouped = g2.GroupBy(x => x.ProductId).OrderBy(pg => pg.First().ProductName);
179 179 var appearance = string.IsNullOrWhiteSpace(g2.Key.ButtonAppearance)
180 180 ? "TEXT"
181   - : g2.Key.ButtonAppearance.Trim().ToUpperInvariant();
  181 + : g2.Key.ButtonAppearance.Trim();
182 182 var availability = string.IsNullOrWhiteSpace(g2.Key.AvailabilityType)
183 183 ? "ALL"
184 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 2 CREATE TABLE IF NOT EXISTS `fl_location_support` (
3 3 `Id` varchar(50) NOT NULL COMMENT '主键',
4 4 `IsDeleted` tinyint(1) NOT NULL DEFAULT 0 COMMENT '逻辑删除',
... ... @@ -6,11 +6,8 @@ CREATE TABLE IF NOT EXISTS `fl_location_support` (
6 6 `CreatorId` varchar(50) DEFAULT NULL COMMENT '创建人',
7 7 `LastModifierId` varchar(50) DEFAULT NULL COMMENT '最后修改人',
8 8 `LastModificationTime` datetime DEFAULT NULL COMMENT '最后修改时间',
9   - `LocationId` varchar(50) NOT NULL COMMENT '门店Id(对应location.Id)',
10 9 `SupportPhone` varchar(100) NOT NULL COMMENT 'Support 电话',
11 10 `SupportEmail` varchar(200) NOT NULL COMMENT 'Support 邮箱',
12 11 PRIMARY KEY (`Id`),
13   - UNIQUE KEY `uk_fl_location_support_locationid` (`LocationId`),
14 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 8 - **接口前缀**:宿主统一前缀为 `/api/app`
9 9 - **分类表**:`fl_product_category`
10 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 15 > 说明:本文以 Swagger 为准(本地示例:`http://localhost:19001/swagger`,搜索 `ProductCategory`)。
14 16  
... ... @@ -57,7 +59,10 @@ Authorization: Bearer eyJhbGciOi...
57 59 | `id` | string | 主键 |
58 60 | `categoryCode` | string | 类别编码 |
59 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 66 | `state` | boolean | 是否启用 |
62 67 | `orderNum` | number | 排序 |
63 68 | `lastEdited` | string | 最后编辑时间 |
... ... @@ -75,7 +80,10 @@ Authorization: Bearer eyJhbGciOi...
75 80 "id": "a2696b9e-2277-11f1-b4c6-00163e0c7c4f",
76 81 "categoryCode": "CAT_PREP",
77 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 87 "state": true,
80 88 "orderNum": 100,
81 89 "lastEdited": "2026-03-25 12:30:10"
... ... @@ -108,7 +116,11 @@ Authorization: Bearer eyJhbGciOi...
108 116 "id": "a2696b9e-2277-11f1-b4c6-00163e0c7c4f",
109 117 "categoryCode": "CAT_PREP",
110 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 124 "state": true,
113 125 "orderNum": 100
114 126 }
... ... @@ -130,7 +142,11 @@ Authorization: Bearer eyJhbGciOi...
130 142 |------|------|------|------|
131 143 | `categoryCode` | string | 是 | 类别编码(唯一) |
132 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 150 | `state` | boolean | 否 | 是否启用(默认 true) |
135 151 | `orderNum` | number | 否 | 排序(默认 0) |
136 152  
... ... @@ -140,7 +156,11 @@ Authorization: Bearer eyJhbGciOi...
140 156 {
141 157 "categoryCode": "CAT_PREP",
142 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 164 "state": true,
145 165 "orderNum": 100
146 166 }
... ... @@ -162,7 +182,11 @@ Authorization: Bearer eyJhbGciOi...
162 182 {
163 183 "categoryCode": "CAT_PREP",
164 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 190 "state": true,
167 191 "orderNum": 100
168 192 }
... ... @@ -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 224 推荐前端流程:
201 225  
202 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 54  
55 55 | 字段 | 类型 | 说明 |
56 56 |------|------|------|
57   -| `url` | string | 图片访问的相对路径,可直接保存到 `CategoryPhotoUrl`(例如:`/picture/category/xxx.png`) |
  57 +| `url` | string | 图片访问的相对路径;写入分类接口时,若 `categoryPhotoUrl` 采用 **JSON** 存展示数据,请将该 `url` 放入你方约定的 JSON 结构中。若仍传**纯路径字符串**,后端会序列化为 JSON 字符串再入库。 |
58 58 | `fileName` | string | 服务器保存的文件名 |
59 59 | `size` | number | 文件大小(字节) |
60 60  
... ... @@ -96,7 +96,7 @@ curl -X POST &quot;http://localhost:19001/api/app/picture/category/upload&quot; ^
96 96 推荐前端流程:
97 97  
98 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 42  
43 43 - `fl_product_category`(主表)关键字段:
44 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 47 - **已删除列**(需在库上执行 `DROP COLUMN`):`ButtonTextColor`、`ButtonBgColor`、`ButtonImageUrl`、`ButtonStyleJson`
48 48 - `AvailabilityType`:`ALL/SPECIFIED`(门店可用范围)
49 49 - `fl_product_category_location`(关联表):
... ... @@ -69,7 +69,7 @@
69 69 - **路径**:`/api/app/product-category/{id}`
70 70 - **新增返回字段**:
71 71 - `displayText`
72   - - `buttonAppearance`、`categoryPhotoUrl`(COLOR/IMAGE 的展示数据统一在此字段
  72 + - `buttonAppearance`、`categoryPhotoUrl`(**JSON 格式字符串**,展示语义由前端解析
73 73 - `availabilityType`
74 74 - `locationIds`(当 `availabilityType=SPECIFIED` 返回门店 Id 列表,否则为空数组)
75 75  
... ... @@ -99,7 +99,7 @@
99 99  
100 100 - `availabilityType` 仅允许 `ALL/SPECIFIED`
101 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 111  
112 112 - `fl_label_category`(主表)关键字段:
113 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 116 - **已删除列**(需在库上执行 `DROP COLUMN`):`ButtonTextColor`、`ButtonBgColor`、`ButtonImageUrl`、`ButtonStyleJson`
117 117 - `AvailabilityType`:`ALL/SPECIFIED`(门店可用范围)
118 118 - `fl_label_category_location`(关联表):
... ... @@ -138,7 +138,7 @@
138 138 - **路径**:`/api/app/label-category/{id}`
139 139 - **新增返回字段**:
140 140 - `displayText`
141   - - `buttonAppearance`、`categoryPhotoUrl`(COLOR/IMAGE 的展示数据统一在此字段
  141 + - `buttonAppearance`、`categoryPhotoUrl`(**JSON 格式字符串**,展示语义由前端解析
142 142 - `availabilityType`
143 143 - `locationIds`(当 `availabilityType=SPECIFIED` 返回门店 Id 列表,否则为空数组)
144 144  
... ... @@ -168,14 +168,14 @@
168 168  
169 169 - `availabilityType` 仅允许 `ALL/SPECIFIED`
170 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 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 180 ### 4.1 数据库迁移(两张主表)
181 181  
... ...
项目相关文档/标签模块接口对接说明.md
... ... @@ -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 60 ### 1.2 详情
55 61  
56 62 方法:`GET /api/app/label-category/{id}`
... ... @@ -69,7 +75,11 @@ Swagger 地址:
69 75 {
70 76 "categoryCode": "CAT_PREP",
71 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 83 "state": true,
74 84 "orderNum": 1
75 85 }
... ... @@ -85,7 +95,11 @@ Swagger 地址:
85 95 {
86 96 "categoryCode": "CAT_PREP",
87 97 "categoryName": "Prep",
88   - "categoryPhotoUrl": null,
  98 + "displayText": "Prep",
  99 + "buttonAppearance": "[\"TEXT\",\"COLOR\"]",
  100 + "categoryPhotoUrl": "[\"Prep\",\"#10B981\"]",
  101 + "availabilityType": "ALL",
  102 + "locationIds": [],
89 103 "state": true,
90 104 "orderNum": 2
91 105 }
... ... @@ -706,7 +720,8 @@ ADD UNIQUE KEY `uk_fl_ltpd_template_product_label_type` (`TemplateId`, `ProductI
706 720 |------|------|------|
707 721 | `id` | string | `fl_label_category.Id` |
708 722 | `categoryName` | string | 分类名称 |
709   -| `categoryPhotoUrl` | string \| null | 分类图标/图 |
  723 +| `categoryPhotoUrl` | string \| null | 分类展示数据,**JSON 格式字符串**(与库中 `CategoryPhotoUrl` 一致,客户端解析) |
  724 +| `buttonAppearance` | string | 按钮外观,**JSON 格式字符串**(与库中 `ButtonAppearance` 一致;空时后端默认 `"TEXT"`) |
710 725 | `orderNum` | number | 排序 |
711 726 | `productCategories` | array | 第二级列表(见下表) |
712 727  
... ... @@ -715,8 +730,12 @@ ADD UNIQUE KEY `uk_fl_ltpd_template_product_label_type` (`TemplateId`, `ProductI
715 730 | 字段 | 类型 | 说明 |
716 731 |------|------|------|
717 732 | `categoryId` | string \| null | 产品分类Id;产品未归类或分类不存在时为空 |
718   -| `categoryPhotoUrl` | string \| null | 产品分类图片地址;产品未归类或分类不存在时为空 |
  733 +| `categoryPhotoUrl` | string \| null | 产品分类展示数据,**JSON 格式字符串**;未归类或分类不存在时为空 |
719 734 | `name` | string | 产品分类显示名;空源数据为 **`无`** |
  735 +| `displayText` | string \| null | 按钮展示文案 |
  736 +| `buttonAppearance` | string | **JSON 格式字符串**(与库中一致;空时默认 `"TEXT"`) |
  737 +| `availabilityType` | string | `ALL` / `SPECIFIED`(树已按当前门店过滤,仍返回供客户端展示) |
  738 +| `orderNum` | number | 排序 |
720 739 | `itemCount` | number | 该分类下 **产品个数**(去重后的产品数) |
721 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 790 {
772 791 "id": "cat-prep-id",
773 792 "categoryName": "Prep",
774   - "categoryPhotoUrl": "/picture/...",
  793 + "categoryPhotoUrl": "[\"Prep\",\"#10B981\"]",
  794 + "buttonAppearance": "[\"TEXT\",\"COLOR\"]",
775 795 "orderNum": 1,
776 796 "productCategories": [
777 797 {
778 798 "categoryId": "pc-meat-id",
779   - "categoryPhotoUrl": "/picture/product-category/20260325123010_xxx.png",
  799 + "categoryPhotoUrl": "[\"/picture/product-category/20260325123010_xxx.png\"]",
780 800 "name": "Meat",
  801 + "displayText": "Meat",
  802 + "buttonAppearance": "[\"IMAGE\"]",
  803 + "availabilityType": "ALL",
  804 + "orderNum": 10,
781 805 "itemCount": 1,
782 806 "products": [
783 807 {
... ...
项目相关文档/美国版App登录接口说明.md
... ... @@ -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 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 303 ### 响应体(LocationSupportGetOutputDto)
306 304  
307 305 | 字段(JSON) | 类型 | 说明 |
308 306 |--------------|------|------|
309   -| `id` | string | 联系方式主键 |
310   -| `locationId` | string | 配置记录中的门店主键(仅作兼容字段) |
311   -| `locationName` | string \| null | 配置记录中的门店名称(仅作兼容字段) |
  307 +| `id` | string | 记录主键(Web 编辑 `PUT` 路径中的 `{id}`) |
312 308 | `supportPhone` | string | Support 电话 |
313 309 | `supportEmail` | string | Support 邮箱 |
314 310  
315   -> 若门店尚未配置联系方式,接口返回 `null`。
  311 +> 若尚未在后台配置,接口返回 `null`。
316 312  
317 313 ### 响应示例
318 314  
319 315 ```json
320 316 {
321 317 "id": "3a2f4fda-1a93-4a35-9b98-95dca7bb5d2a",
322   - "locationId": "a2696b9e-2277-11f1-b4c6-00163e0c7c4f",
323   - "locationName": "Downtown Store",
324 318 "supportPhone": "1-800-SUPPORT",
325 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 336 - **方法**:`POST`
338 337 - **路径**:`/api/app/location-support`
... ... @@ -342,36 +341,46 @@ Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
342 341  
343 342 ```json
344 343 {
345   - "locationId": "a2696b9e-2277-11f1-b4c6-00163e0c7c4f",
346 344 "supportPhone": "1-800-SUPPORT",
347 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 355 - **方法**:`PUT`
357 356 - **路径**:`/api/app/location-support/{id}`
358 357 - **Content-Type**:`application/json`
359 358  
360   -请求体(LocationSupportUpdateInputVo)与新增一致
  359 +请求体(LocationSupportUpdateInputVo)
361 360  
362 361 ```json
363 362 {
364   - "locationId": "a2696b9e-2277-11f1-b4c6-00163e0c7c4f",
365 363 "supportPhone": "1-800-SUPPORT",
366 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  
... ...