Commit a4baaa73a80394707b9437478b205c4ed87de901

Authored by 李曜臣
1 parent ca4ab0f7

模板与产品关联实现

Showing 11 changed files with 324 additions and 14 deletions
美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/LabelTemplate/LabelTemplateCreateInputVo.cs
... ... @@ -57,5 +57,11 @@ public class LabelTemplateCreateInputVo
57 57 /// </summary>
58 58 [JsonPropertyName("appliedLocationIds")]
59 59 public List<string> AppliedLocationIds { get; set; } = new();
  60 +
  61 + /// <summary>
  62 + /// 模板与产品/标签类型绑定默认值
  63 + /// </summary>
  64 + [JsonPropertyName("templateProductDefaults")]
  65 + public List<LabelTemplateProductDefaultDto>? TemplateProductDefaults { get; set; }
60 66 }
61 67  
... ...
美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/LabelTemplate/LabelTemplateElementDto.cs
... ... @@ -13,6 +13,9 @@ public class LabelTemplateElementDto
13 13 [JsonPropertyName("type")]
14 14 public string ElementType { get; set; } = string.Empty;
15 15  
  16 + [JsonPropertyName("elementName")]
  17 + public string ElementName { get; set; } = string.Empty;
  18 +
16 19 [JsonPropertyName("x")]
17 20 public decimal PosX { get; set; }
18 21  
... ...
美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/LabelTemplate/LabelTemplateGetOutputDto.cs
... ... @@ -29,5 +29,7 @@ public class LabelTemplateGetOutputDto
29 29 public List<LabelTemplateElementDto> Elements { get; set; } = new();
30 30  
31 31 public List<string> AppliedLocationIds { get; set; } = new();
  32 +
  33 + public List<LabelTemplateProductDefaultDto> TemplateProductDefaults { get; set; } = new();
32 34 }
33 35  
... ...
美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/LabelTemplate/LabelTemplateProductDefaultDto.cs 0 → 100644
  1 +using System.Text.Json.Serialization;
  2 +
  3 +namespace FoodLabeling.Application.Contracts.Dtos.LabelTemplate;
  4 +
  5 +/// <summary>
  6 +/// 模板与产品/标签类型绑定后的默认值行
  7 +/// </summary>
  8 +public class LabelTemplateProductDefaultDto
  9 +{
  10 + [JsonPropertyName("productId")]
  11 + public string ProductId { get; set; } = string.Empty;
  12 +
  13 + [JsonPropertyName("labelTypeId")]
  14 + public string LabelTypeId { get; set; } = string.Empty;
  15 +
  16 + /// <summary>
  17 + /// 默认值JSON(建议结构:{ "el-xxx": "默认值" })
  18 + /// </summary>
  19 + [JsonPropertyName("defaultValues")]
  20 + public object? DefaultValues { get; set; }
  21 +
  22 + [JsonPropertyName("orderNum")]
  23 + public int OrderNum { get; set; }
  24 +}
  25 +
... ...
美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/UsAppLabeling/UsAppLabelPreviewDto.cs
  1 +using System;
  2 +using System.Collections.Generic;
1 3 using FoodLabeling.Application.Contracts.Dtos.Label;
2 4  
3 5 namespace FoodLabeling.Application.Contracts.Dtos.UsAppLabeling;
... ... @@ -7,6 +9,8 @@ namespace FoodLabeling.Application.Contracts.Dtos.UsAppLabeling;
7 9 /// </summary>
8 10 public class UsAppLabelPreviewDto
9 11 {
  12 + public string LabelId { get; set; } = string.Empty;
  13 +
10 14 public string LocationId { get; set; } = string.Empty;
11 15  
12 16 public string LabelCode { get; set; } = string.Empty;
... ... @@ -23,6 +27,8 @@ public class UsAppLabelPreviewDto
23 27  
24 28 public string? LabelCategoryName { get; set; }
25 29  
  30 + public DateTime? LabelLastEdited { get; set; }
  31 +
26 32 /// <summary>
27 33 /// 预览图(base64 png,可空;若为空,客户端可用 Template 自行渲染)
28 34 /// </summary>
... ... @@ -32,5 +38,11 @@ public class UsAppLabelPreviewDto
32 38 /// 预览模板结构(与 LabelCanvas/LabelPreviewOnly 结构尽量一致)
33 39 /// </summary>
34 40 public LabelTemplatePreviewDto Template { get; set; } = new();
  41 +
  42 + /// <summary>
  43 + /// 当前预览上下文(模板+产品+标签类型)命中的默认值配置。
  44 + /// 数据来源:fl_label_template_product_default.DefaultValuesJson
  45 + /// </summary>
  46 + public Dictionary<string, object?>? TemplateProductDefaultValues { get; set; }
35 47 }
36 48  
... ...
美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application/Services/DbModels/FlLabelTemplateElementDbEntity.cs
... ... @@ -14,6 +14,9 @@ public class FlLabelTemplateElementDbEntity
14 14  
15 15 public string ElementType { get; set; } = string.Empty;
16 16  
  17 + [SugarColumn(ColumnName = "ElementName")]
  18 + public string ElementName { get; set; } = string.Empty;
  19 +
17 20 public decimal PosX { get; set; }
18 21  
19 22 public decimal PosY { get; set; }
... ...
美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application/Services/DbModels/FlLabelTemplateProductDefaultDbEntity.cs 0 → 100644
  1 +using SqlSugar;
  2 +
  3 +namespace FoodLabeling.Application.Services.DbModels;
  4 +
  5 +[SugarTable("fl_label_template_product_default")]
  6 +public class FlLabelTemplateProductDefaultDbEntity
  7 +{
  8 + [SugarColumn(IsPrimaryKey = true)]
  9 + public string Id { get; set; } = string.Empty;
  10 +
  11 + public string TemplateId { get; set; } = string.Empty;
  12 +
  13 + public string ProductId { get; set; } = string.Empty;
  14 +
  15 + public string LabelTypeId { get; set; } = string.Empty;
  16 +
  17 + /// <summary>
  18 + /// 默认值JSON(字符串保存)
  19 + /// </summary>
  20 + public string? DefaultValuesJson { get; set; }
  21 +
  22 + public int OrderNum { get; set; }
  23 +}
  24 +
... ...
美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application/Services/LabelAppService.cs
... ... @@ -695,6 +695,7 @@ public class LabelAppService : ApplicationService, ILabelAppService
695 695 {
696 696 Id = el.ElementKey,
697 697 ElementType = el.ElementType,
  698 + ElementName = el.ElementName,
698 699 PosX = el.PosX,
699 700 PosY = el.PosY,
700 701 Width = el.Width,
... ...
美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application/Services/LabelTemplateAppService.cs
... ... @@ -157,6 +157,7 @@ public class LabelTemplateAppService : ApplicationService, ILabelTemplateAppServ
157 157 {
158 158 Id = e.ElementKey,
159 159 ElementType = e.ElementType,
  160 + ElementName = e.ElementName,
160 161 PosX = e.PosX,
161 162 PosY = e.PosY,
162 163 Width = e.Width,
... ... @@ -180,6 +181,28 @@ public class LabelTemplateAppService : ApplicationService, ILabelTemplateAppServ
180 181 .Select(x => x.LocationId)
181 182 .ToListAsync();
182 183  
  184 + var defaultRows = await _dbContext.SqlSugarClient.Queryable<FlLabelTemplateProductDefaultDbEntity>()
  185 + .Where(x => x.TemplateId == template.Id)
  186 + .OrderBy(x => x.OrderNum)
  187 + .ToListAsync();
  188 +
  189 + var productDefaults = defaultRows.Select(x =>
  190 + {
  191 + object? defaults = null;
  192 + if (!string.IsNullOrWhiteSpace(x.DefaultValuesJson))
  193 + {
  194 + defaults = JsonSerializer.Deserialize<object>(x.DefaultValuesJson);
  195 + }
  196 +
  197 + return new LabelTemplateProductDefaultDto
  198 + {
  199 + ProductId = x.ProductId,
  200 + LabelTypeId = x.LabelTypeId,
  201 + DefaultValues = defaults,
  202 + OrderNum = x.OrderNum
  203 + };
  204 + }).ToList();
  205 +
183 206 return new LabelTemplateGetOutputDto
184 207 {
185 208 Id = template.TemplateCode,
... ... @@ -195,7 +218,8 @@ public class LabelTemplateAppService : ApplicationService, ILabelTemplateAppServ
195 218 VersionNo = template.VersionNo,
196 219 State = template.State,
197 220 Elements = MapElements(),
198   - AppliedLocationIds = appliedLocationIds
  221 + AppliedLocationIds = appliedLocationIds,
  222 + TemplateProductDefaults = productDefaults
199 223 };
200 224 }
201 225  
... ... @@ -246,7 +270,12 @@ public class LabelTemplateAppService : ApplicationService, ILabelTemplateAppServ
246 270  
247 271 await _dbContext.SqlSugarClient.Insertable(entity).ExecuteCommandAsync();
248 272  
249   - await RebuildTemplateElementsAndLocationsAsync(entity.Id, input.Elements, entity.AppliedLocationType, input.AppliedLocationIds);
  273 + await RebuildTemplateElementsLocationsAndDefaultsAsync(
  274 + entity.Id,
  275 + input.Elements,
  276 + entity.AppliedLocationType,
  277 + input.AppliedLocationIds,
  278 + new List<LabelTemplateProductDefaultDto>());
250 279  
251 280 return await GetAsync(code);
252 281 }
... ... @@ -292,7 +321,12 @@ public class LabelTemplateAppService : ApplicationService, ILabelTemplateAppServ
292 321  
293 322 await _dbContext.SqlSugarClient.Updateable(template).ExecuteCommandAsync();
294 323  
295   - await RebuildTemplateElementsAndLocationsAsync(template.Id, input.Elements, template.AppliedLocationType, input.AppliedLocationIds);
  324 + await RebuildTemplateElementsLocationsAndDefaultsAsync(
  325 + template.Id,
  326 + input.Elements,
  327 + template.AppliedLocationType,
  328 + input.AppliedLocationIds,
  329 + input.TemplateProductDefaults);
296 330  
297 331 return await GetAsync(template.TemplateCode);
298 332 }
... ... @@ -327,13 +361,17 @@ public class LabelTemplateAppService : ApplicationService, ILabelTemplateAppServ
327 361 await _dbContext.SqlSugarClient.Deleteable<FlLabelTemplateLocationDbEntity>()
328 362 .Where(x => x.TemplateId == template.Id)
329 363 .ExecuteCommandAsync();
  364 + await _dbContext.SqlSugarClient.Deleteable<FlLabelTemplateProductDefaultDbEntity>()
  365 + .Where(x => x.TemplateId == template.Id)
  366 + .ExecuteCommandAsync();
330 367 }
331 368  
332   - private async Task RebuildTemplateElementsAndLocationsAsync(
  369 + private async Task RebuildTemplateElementsLocationsAndDefaultsAsync(
333 370 string templateDbId,
334 371 List<LabelTemplateElementDto> elements,
335 372 string appliedLocationType,
336   - List<string> appliedLocationIds)
  373 + List<string> appliedLocationIds,
  374 + List<LabelTemplateProductDefaultDto>? templateProductDefaults)
337 375 {
338 376 // elements 重建
339 377 await _dbContext.SqlSugarClient.Deleteable<FlLabelTemplateElementDbEntity>()
... ... @@ -342,9 +380,9 @@ public class LabelTemplateAppService : ApplicationService, ILabelTemplateAppServ
342 380  
343 381 if (elements is not null && elements.Count > 0)
344 382 {
345   - var now = DateTime.Now;
346 383 var rows = elements.Select(e =>
347 384 {
  385 + var elementName = EnsureElementName(e.ElementName);
348 386 object? cfg = e.ConfigJson;
349 387 var configJson = cfg == null ? null : JsonSerializer.Serialize(cfg);
350 388 return new FlLabelTemplateElementDbEntity
... ... @@ -353,6 +391,7 @@ public class LabelTemplateAppService : ApplicationService, ILabelTemplateAppServ
353 391 TemplateId = templateDbId,
354 392 ElementKey = e.Id,
355 393 ElementType = e.ElementType,
  394 + ElementName = elementName,
356 395 PosX = e.PosX,
357 396 PosY = e.PosY,
358 397 Width = e.Width,
... ... @@ -395,6 +434,73 @@ public class LabelTemplateAppService : ApplicationService, ILabelTemplateAppServ
395 434  
396 435 await _dbContext.SqlSugarClient.Insertable(locRows).ExecuteCommandAsync();
397 436 }
  437 +
  438 + // 模板-产品-标签类型默认值:仅在显式传入时重建,避免普通编辑误清空
  439 + if (templateProductDefaults is not null)
  440 + {
  441 + var duplicateCheckSet = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
  442 + foreach (var row in templateProductDefaults)
  443 + {
  444 + var productId = row.ProductId?.Trim();
  445 + var labelTypeId = row.LabelTypeId?.Trim();
  446 + if (string.IsNullOrWhiteSpace(productId) || string.IsNullOrWhiteSpace(labelTypeId))
  447 + {
  448 + continue;
  449 + }
  450 +
  451 + var key = $"{productId}::{labelTypeId}";
  452 + if (!duplicateCheckSet.Add(key))
  453 + {
  454 + throw new UserFriendlyException($"模板默认值绑定重复:产品[{productId}]与标签类型[{labelTypeId}]只能存在一条");
  455 + }
  456 + }
  457 +
  458 + await _dbContext.SqlSugarClient.Deleteable<FlLabelTemplateProductDefaultDbEntity>()
  459 + .Where(x => x.TemplateId == templateDbId)
  460 + .ExecuteCommandAsync();
  461 +
  462 + if (templateProductDefaults.Count > 0)
  463 + {
  464 + var rows = templateProductDefaults.Select((x, idx) =>
  465 + {
  466 + var productId = x.ProductId?.Trim();
  467 + var labelTypeId = x.LabelTypeId?.Trim();
  468 + if (string.IsNullOrWhiteSpace(productId))
  469 + {
  470 + throw new UserFriendlyException("模板默认值绑定中,产品Id不能为空");
  471 + }
  472 +
  473 + if (string.IsNullOrWhiteSpace(labelTypeId))
  474 + {
  475 + throw new UserFriendlyException("模板默认值绑定中,标签类型Id不能为空");
  476 + }
  477 +
  478 + var json = x.DefaultValues is null ? null : JsonSerializer.Serialize(x.DefaultValues);
  479 + return new FlLabelTemplateProductDefaultDbEntity
  480 + {
  481 + Id = _guidGenerator.Create().ToString(),
  482 + TemplateId = templateDbId,
  483 + ProductId = productId,
  484 + LabelTypeId = labelTypeId,
  485 + DefaultValuesJson = json,
  486 + OrderNum = x.OrderNum <= 0 ? idx + 1 : x.OrderNum
  487 + };
  488 + }).ToList();
  489 +
  490 + await _dbContext.SqlSugarClient.Insertable(rows).ExecuteCommandAsync();
  491 + }
  492 + }
  493 + }
  494 +
  495 + private static string EnsureElementName(string? elementName)
  496 + {
  497 + var normalizedName = elementName?.Trim();
  498 + if (string.IsNullOrWhiteSpace(normalizedName))
  499 + {
  500 + throw new UserFriendlyException("组件名字不能为空");
  501 + }
  502 +
  503 + return normalizedName;
398 504 }
399 505  
400 506 private static PagedResultWithPageDto<T> BuildPagedResult<T>(int skipCount, int maxResultCount, int total, List<T> items)
... ...
美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application/Services/UsAppLabelingAppService.cs
... ... @@ -238,8 +238,13 @@ public class UsAppLabelingAppService : ApplicationService, IUsAppLabelingAppServ
238 238 .Where((l, c, t, tpl) => l.LabelCode == labelCode)
239 239 .Select((l, c, t, tpl) => new
240 240 {
  241 + l.Id,
241 242 l.LabelCode,
242 243 l.LocationId,
  244 + l.LabelTypeId,
  245 + l.TemplateId,
  246 + l.LastModificationTime,
  247 + l.CreationTime,
243 248 LabelCategoryName = c.CategoryName,
244 249 TypeName = t.TypeName,
245 250 TemplateCode = tpl.TemplateCode,
... ... @@ -259,21 +264,46 @@ public class UsAppLabelingAppService : ApplicationService, IUsAppLabelingAppServ
259 264 throw new UserFriendlyException("该标签不属于当前门店");
260 265 }
261 266  
  267 + var previewProductId = await ResolvePreviewProductIdAsync(labelRow.Id, input.ProductId);
  268 +
262 269 var template = await _labelAppService.PreviewAsync(new LabelPreviewResolveInputVo
263 270 {
264 271 LabelCode = labelCode,
265   - ProductId = input.ProductId?.Trim(),
  272 + ProductId = previewProductId,
266 273 BaseTime = input.BaseTime,
267 274 PrintInputJson = input.PrintInputJson?.ToDictionary(x => x.Key, x => (object?)x.Value)
268 275 });
269 276  
  277 + Dictionary<string, object?>? templateProductDefaultValues = null;
  278 + if (!string.IsNullOrWhiteSpace(previewProductId))
  279 + {
  280 + var productDefault = await _dbContext.SqlSugarClient.Queryable<FlLabelTemplateProductDefaultDbEntity>()
  281 + .Where(x => x.TemplateId == labelRow.TemplateId)
  282 + .Where(x => x.ProductId == previewProductId)
  283 + .Where(x => x.LabelTypeId == labelRow.LabelTypeId)
  284 + .OrderBy(x => x.OrderNum)
  285 + .FirstAsync();
  286 +
  287 + if (!string.IsNullOrWhiteSpace(productDefault?.DefaultValuesJson))
  288 + {
  289 + try
  290 + {
  291 + templateProductDefaultValues =
  292 + JsonSerializer.Deserialize<Dictionary<string, object?>>(productDefault.DefaultValuesJson!);
  293 + }
  294 + catch
  295 + {
  296 + templateProductDefaultValues = null;
  297 + }
  298 + }
  299 + }
  300 +
270 301 var productName = string.Empty;
271 302 var productCategoryName = "无";
272   - if (!string.IsNullOrWhiteSpace(input.ProductId))
  303 + if (!string.IsNullOrWhiteSpace(previewProductId))
273 304 {
274   - var pid = input.ProductId.Trim();
275 305 var p = await _dbContext.SqlSugarClient.Queryable<FlProductDbEntity>()
276   - .FirstAsync(x => !x.IsDeleted && x.State && x.Id == pid);
  306 + .FirstAsync(x => !x.IsDeleted && x.State && x.Id == previewProductId);
277 307 if (p is not null)
278 308 {
279 309 productName = p.ProductName ?? string.Empty;
... ... @@ -288,6 +318,7 @@ public class UsAppLabelingAppService : ApplicationService, IUsAppLabelingAppServ
288 318  
289 319 return new UsAppLabelPreviewDto
290 320 {
  321 + LabelId = labelRow.Id,
291 322 LocationId = locationId,
292 323 LabelCode = labelCode,
293 324 TemplateCode = labelRow.TemplateCode,
... ... @@ -296,8 +327,10 @@ public class UsAppLabelingAppService : ApplicationService, IUsAppLabelingAppServ
296 327 ProductName = string.IsNullOrWhiteSpace(productName) ? null : productName,
297 328 ProductCategoryName = productCategoryName,
298 329 LabelCategoryName = labelRow.LabelCategoryName,
  330 + LabelLastEdited = labelRow.LastModificationTime ?? labelRow.CreationTime,
299 331 PreviewImageBase64Png = null,
300   - Template = template
  332 + Template = template,
  333 + TemplateProductDefaultValues = templateProductDefaultValues
301 334 };
302 335 }
303 336  
... ... @@ -508,6 +541,20 @@ public class UsAppLabelingAppService : ApplicationService, IUsAppLabelingAppServ
508 541 return string.IsNullOrWhiteSpace(s) ? null : s;
509 542 }
510 543  
  544 + private async Task<string?> ResolvePreviewProductIdAsync(string labelId, string? productId)
  545 + {
  546 + var resolvedProductId = productId?.Trim();
  547 + if (!string.IsNullOrWhiteSpace(resolvedProductId))
  548 + {
  549 + return resolvedProductId;
  550 + }
  551 +
  552 + return await _dbContext.SqlSugarClient.Queryable<FlLabelProductDbEntity>()
  553 + .Where(x => x.LabelId == labelId)
  554 + .Select(x => x.ProductId)
  555 + .FirstAsync();
  556 + }
  557 +
511 558 private static UsAppLabelTypeNodeDto BuildLabelTypeNode(LabelingTreeRow r)
512 559 {
513 560 return new UsAppLabelTypeNodeDto
... ...
项目相关文档/标签模块接口对接说明.md
... ... @@ -236,6 +236,9 @@ Swagger 地址:
236 236 说明:
237 237 - 模板标识入参 `id` 使用 `fl_label_template.TemplateCode`。
238 238 - 创建/编辑的 Body 字段名对齐你前端 editor JSON(`id/name/appliedLocation/elements/config`)。
  239 +- 模板组件 `elements[]` 的 `elementName` 为必填(前端传值,后端校验为空会报错)。
  240 +- `templateProductDefaults[]` 用于模板内“产品 + 标签类型”绑定默认值(进入模板详情页后的绑定列表)。
  241 +- **新增模板时不处理默认值**;默认值仅在后续“产品关联/编辑模板”阶段维护。
239 242  
240 243 ### 4.1 分页列表
241 244  
... ... @@ -283,6 +286,7 @@ Swagger 地址:
283 286 "elements": [
284 287 {
285 288 "id": "el-fixed-title",
  289 + "elementName": "标题文本",
286 290 "type": "TEXT_STATIC",
287 291 "x": 32,
288 292 "y": 24,
... ... @@ -309,6 +313,8 @@ Swagger 地址:
309 313  
310 314 说明:
311 315 - 当 `appliedLocation=SPECIFIED` 时,`appliedLocationIds` 必须至少选择一个门店。
  316 +- `elements[].elementName` 必填;为空或空白将返回友好错误:`组件名字不能为空`。
  317 +- 新增模板时即使传了 `templateProductDefaults`,后端也不会写入默认值数据。
312 318  
313 319 ### 4.4 编辑模板
314 320  
... ... @@ -332,14 +338,84 @@ Swagger 地址:
332 338 "showRuler": true,
333 339 "showGrid": true,
334 340 "state": true,
335   - "elements": [],
336   - "appliedLocationIds": ["11111111-1111-1111-1111-111111111111"]
  341 + "elements": [
  342 + {
  343 + "id": "el-price",
  344 + "elementName": "价格文本",
  345 + "type": "TEXT_PRICE",
  346 + "x": 40,
  347 + "y": 120,
  348 + "width": 140,
  349 + "height": 28,
  350 + "rotation": "horizontal",
  351 + "border": "none",
  352 + "zIndex": 2,
  353 + "orderNum": 2,
  354 + "valueSourceType": "PRINT_INPUT",
  355 + "inputKey": "price",
  356 + "isRequiredInput": true,
  357 + "config": {
  358 + "text": "",
  359 + "fontFamily": "Arial",
  360 + "fontSize": 18,
  361 + "fontWeight": "bold",
  362 + "textAlign": "left"
  363 + }
  364 + }
  365 + ],
  366 + "appliedLocationIds": ["11111111-1111-1111-1111-111111111111"],
  367 + "templateProductDefaults": [
  368 + {
  369 + "productId": "3a20-xxxx",
  370 + "labelTypeId": "3a20-yyyy",
  371 + "defaultValues": {
  372 + "el-fixed-title": "Chicken",
  373 + "el-price": "2.00",
  374 + "el-desc": "23"
  375 + },
  376 + "orderNum": 1
  377 + }
  378 + ]
337 379 }
338 380 ```
339 381  
340 382 版本:
341 383 - `VersionNo` 会在编辑时自动 `+1`。
342 384 - `elements` 会按传入内容全量重建。
  385 +- 查询/预览返回的 `elements[]` 同样会带 `elementName` 字段。
  386 +- `templateProductDefaults` 在编辑接口中**仅当显式传入时**才会重建(同一模板先删后插)。
  387 +- 若编辑时不传 `templateProductDefaults`,后端会保留数据库中原有默认值,不做覆盖。
  388 +
  389 +`templateProductDefaults` 结构说明:
  390 +- 每一行需传 `productId` 与 `labelTypeId`。
  391 +- `defaultValues` 建议使用 `element.id => 默认文本` 结构;页面展示时可结合模板 `elements[].config.text` 作为列头与初始值。
  392 +
  393 +### 4.6 模板与产品默认值关联表(新增)
  394 +
  395 +用于存储“模板-产品-标签类型”的默认值,推荐执行以下建表 SQL:
  396 +
  397 +```sql
  398 +CREATE TABLE `fl_label_template_product_default` (
  399 + `Id` varchar(36) NOT NULL COMMENT '主键',
  400 + `TemplateId` varchar(36) NOT NULL COMMENT '模板Id(关联 fl_label_template.Id)',
  401 + `ProductId` varchar(36) NOT NULL COMMENT '产品Id(关联 fl_product.Id)',
  402 + `LabelTypeId` varchar(36) NOT NULL COMMENT '标签类型Id(关联 fl_label_type.Id)',
  403 + `DefaultValuesJson` text NULL COMMENT '默认值JSON(如 elementId=>默认文本)',
  404 + `OrderNum` int NOT NULL DEFAULT 1 COMMENT '排序',
  405 + PRIMARY KEY (`Id`),
  406 + KEY `idx_fl_ltpd_template` (`TemplateId`),
  407 + KEY `idx_fl_ltpd_product` (`ProductId`),
  408 + KEY `idx_fl_ltpd_label_type` (`LabelTypeId`),
  409 + UNIQUE KEY `uk_fl_ltpd_template_product_label_type` (`TemplateId`, `ProductId`, `LabelTypeId`)
  410 +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='标签模板-产品默认值关联表';
  411 +```
  412 +
  413 +若表已存在,可执行以下 SQL 增加唯一约束:
  414 +
  415 +```sql
  416 +ALTER TABLE `fl_label_template_product_default`
  417 +ADD UNIQUE KEY `uk_fl_ltpd_template_product_label_type` (`TemplateId`, `ProductId`, `LabelTypeId`);
  418 +```
343 419  
344 420 ### 4.5 删除(逻辑删除)
345 421  
... ... @@ -758,7 +834,12 @@ curl -X GET &quot;http://localhost:19001/api/app/us-app-labeling/labeling-tree?locati
758 834  
759 835 - `template`:`LabelTemplatePreviewDto`
760 836 - `width` / `height` / `unit`:模板物理尺寸
761   - - `elements[]`:元素数组(对齐前端 editor JSON:`id/type/x/y/width/height/rotation/border/zIndex/orderNum/config`)
  837 + - `elements[]`:元素数组(对齐前端 editor JSON:`id/elementName/type/x/y/width/height/rotation/border/zIndex/orderNum/config`)
  838 +- `templateProductDefaultValues`:`object | null`
  839 + - 来源:`fl_label_template_product_default.DefaultValuesJson`
  840 + - 命中条件:当前预览上下文的 `templateId + productId + labelTypeId`
  841 + - 建议结构:`{ "elementId": "默认值" }`
  842 + - 未命中时返回 `null`(向后兼容)
762 843  
763 844 `elements[].config` 内常用字段(示例):
764 845  
... ...