Commit 5d2dea03230e4a3fad84f797b6a7fb1b29780f28

Authored by 杨鑫
2 parents 3b969879 c2bac760

Merge branch 'main' of http://39.98.150.180/wangming/Food-Labeling-Management-Platform

Showing 14 changed files with 252 additions and 350 deletions
美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/Reports/ReportsPrintLogListItemDto.cs
... ... @@ -27,7 +27,7 @@ public class ReportsPrintLogListItemDto
27 27 /// <summary>模板展示(尺寸 + 模板名)</summary>
28 28 public string TemplateText { get; set; } = "无";
29 29  
30   - /// <summary>打印时间展示(<c>MM/dd/yy HH:mm</c>)</summary>
  30 + /// <summary>打印时间展示(<c>MM/dd/yy</c>,即 MM/DD/YY)</summary>
31 31 public string PrintedAt { get; set; } = "无";
32 32  
33 33 /// <summary>打印人姓名</summary>
... ...
美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/IServices/IReportsAppService.cs
... ... @@ -13,7 +13,7 @@ public interface IReportsAppService : IApplicationService
13 13 {
14 14 /// <summary>
15 15 /// Print Log 分页列表;角色 <c>admin</c> 可查全部,否则仅当前用户打印记录。
16   - /// 出参 <c>printedAt</c>、<c>expiryDateText</c> 统一为美式短日期 <c>MM/dd/yy HH:mm</c>(精确到分钟)
  16 + /// 出参 <c>printedAt</c>、<c>expiryDateText</c> 日期部分统一为美式短日期 <c>MM/dd/yy</c>(MM/DD/YY);含时分时 <c>MM/dd/yy HH:mm</c>
17 17 /// </summary>
18 18 Task<PagedResultWithPageDto<ReportsPrintLogListItemDto>> GetPrintLogListAsync(ReportsPrintLogGetListInputVo input);
19 19  
... ...
美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application/Helpers/LabelEntityPartnerScopeHelper.cs
... ... @@ -138,7 +138,7 @@ public static class LabelEntityPartnerScopeHelper
138 138 }
139 139  
140 140 /// <summary>
141   - /// 保存 Company 适用范围(关联表 + <c>AppliedPartnerType</c> 列);未执行迁移脚本时 no-op。
  141 + /// 保存 Company 适用范围(关联表与 <c>AppliedPartnerType</c> 列独立探测与落库);未执行迁移脚本时 no-op。
142 142 /// </summary>
143 143 public static async Task SavePartnerScopeAsync(
144 144 ISqlSugarClient db,
... ... @@ -154,25 +154,54 @@ public static class LabelEntityPartnerScopeHelper
154 154 return;
155 155 }
156 156  
  157 + var needsPartnerRows = NeedsPartnerRowPersistence(scope);
157 158 var schema = await GetSchemaStatusAsync(db);
158   - if (!HasPartnerScopeForKind(schema, kind))
  159 + var hasTable = HasPartnerTableForKind(schema, kind);
  160 + var hasColumn = HasPartnerColumnForKind(schema, kind);
  161 +
  162 + if (!hasTable && !hasColumn)
159 163 {
160   - return;
  164 + if (needsPartnerRows)
  165 + {
  166 + schema = await RefreshSchemaCacheAsync(db);
  167 + hasTable = HasPartnerTableForKind(schema, kind);
  168 + hasColumn = HasPartnerColumnForKind(schema, kind);
  169 + }
  170 + else
  171 + {
  172 + return;
  173 + }
  174 + }
  175 + else if (needsPartnerRows && !hasTable)
  176 + {
  177 + schema = await RefreshSchemaCacheAsync(db);
  178 + hasTable = HasPartnerTableForKind(schema, kind);
  179 + hasColumn = HasPartnerColumnForKind(schema, kind);
161 180 }
162 181  
163   - await DeletePartnerScopeRowsAsync(db, kind, entityId);
  182 + if (needsPartnerRows && !hasTable)
  183 + {
  184 + throw new UserFriendlyException(
  185 + "Company 适用范围关联表尚未就绪,请联系管理员执行数据库迁移(fl_label_entity_partner_scope.sql)后重试");
  186 + }
164 187  
165   - if (string.Equals(scope.AppliedPartnerType, ScopeSpecified, StringComparison.OrdinalIgnoreCase)
166   - && scope.PartnerIds.Count > 0)
  188 + if (hasTable)
167 189 {
168   - await InsertPartnerRowsAsync(db, guidGenerator, kind, entityId, scope.PartnerIds, currentUserId, now);
  190 + await DeletePartnerScopeRowsCoreAsync(db, kind, entityId);
  191 + if (needsPartnerRows)
  192 + {
  193 + await InsertPartnerRowsAsync(db, guidGenerator, kind, entityId, scope.PartnerIds, currentUserId, now);
  194 + }
169 195 }
170 196  
171   - await SetAppliedPartnerTypeAsync(db, kind, entityId, scope.AppliedPartnerType);
  197 + if (hasColumn)
  198 + {
  199 + await SetAppliedPartnerTypeAsync(db, kind, entityId, scope.AppliedPartnerType);
  200 + }
172 201 }
173 202  
174 203 /// <summary>
175   - /// 删除实体下 Company 关联行;未迁移 partner 表时跳过
  204 + /// 删除实体下 Company 关联行;未迁移 partner 表时跳过(缓存过期时会刷新再试)
176 205 /// </summary>
177 206 public static async Task DeletePartnerScopeRowsAsync(
178 207 ISqlSugarClient db,
... ... @@ -185,29 +214,19 @@ public static class LabelEntityPartnerScopeHelper
185 214 }
186 215  
187 216 var schema = await GetSchemaStatusAsync(db);
188   - if (!HasPartnerTableForKind(schema, kind))
  217 + var hasTable = HasPartnerTableForKind(schema, kind);
  218 + if (!hasTable)
189 219 {
190   - return;
  220 + schema = await RefreshSchemaCacheAsync(db);
  221 + hasTable = HasPartnerTableForKind(schema, kind);
191 222 }
192 223  
193   - switch (kind)
  224 + if (!hasTable)
194 225 {
195   - case LabelEntityPartnerKind.Type:
196   - await db.Deleteable<FlLabelTypePartnerDbEntity>()
197   - .Where(x => x.LabelTypeId == entityId)
198   - .ExecuteCommandAsync();
199   - break;
200   - case LabelEntityPartnerKind.Category:
201   - await db.Deleteable<FlLabelCategoryPartnerDbEntity>()
202   - .Where(x => x.CategoryId == entityId)
203   - .ExecuteCommandAsync();
204   - break;
205   - case LabelEntityPartnerKind.MultipleOption:
206   - await db.Deleteable<FlLabelMultipleOptionPartnerDbEntity>()
207   - .Where(x => x.MultipleOptionId == entityId)
208   - .ExecuteCommandAsync();
209   - break;
  226 + return;
210 227 }
  228 +
  229 + await DeletePartnerScopeRowsCoreAsync(db, kind, entityId);
211 230 }
212 231  
213 232 /// <summary>
... ... @@ -234,9 +253,10 @@ public static class LabelEntityPartnerScopeHelper
234 253 }
235 254  
236 255 var schema = await GetSchemaStatusAsync(db);
237   - var hasScope = HasPartnerScopeForKind(schema, kind);
  256 + var hasTable = HasPartnerTableForKind(schema, kind);
  257 + var hasColumn = HasPartnerColumnForKind(schema, kind);
238 258  
239   - var partnerLinks = hasScope
  259 + var partnerLinks = hasTable
240 260 ? await LoadPartnerLinksAsync(db, kind, ids)
241 261 : new List<(string EntityId, string PartnerId)>();
242 262  
... ... @@ -249,7 +269,7 @@ public static class LabelEntityPartnerScopeHelper
249 269 partnerLinks.Where(x => x.EntityId == entityId).Select(x => x.PartnerId).ToList());
250 270  
251 271 var partnerType = pIds.Count > 0 ? ScopeSpecified : ScopeAll;
252   - if (hasScope)
  272 + if (hasColumn)
253 273 {
254 274 var storedType = await GetAppliedPartnerTypeAsync(db, kind, entityId);
255 275 if (!string.IsNullOrWhiteSpace(storedType))
... ... @@ -261,6 +281,10 @@ public static class LabelEntityPartnerScopeHelper
261 281 partnerType = ScopeSpecified;
262 282 }
263 283 }
  284 + else if (pIds.Count > 0)
  285 + {
  286 + partnerType = ScopeSpecified;
  287 + }
264 288  
265 289 var companyDisplay = string.Equals(partnerType, ScopeAll, StringComparison.OrdinalIgnoreCase)
266 290 ? AllCompaniesDisplay
... ... @@ -456,6 +480,41 @@ public static class LabelEntityPartnerScopeHelper
456 480 /// <summary>单元测试或切换库后调用。</summary>
457 481 public static void ResetSchemaCacheForTests() => _cachedSchema = null;
458 482  
  483 + private static bool NeedsPartnerRowPersistence(LabelEntityPartnerScopeSaveResult scope) =>
  484 + string.Equals(scope.AppliedPartnerType, ScopeSpecified, StringComparison.OrdinalIgnoreCase)
  485 + && scope.PartnerIds.Count > 0;
  486 +
  487 + private static async Task<LabelEntityPartnerScopeSchemaStatus> RefreshSchemaCacheAsync(ISqlSugarClient db)
  488 + {
  489 + _cachedSchema = null;
  490 + return await GetSchemaStatusAsync(db);
  491 + }
  492 +
  493 + private static async Task DeletePartnerScopeRowsCoreAsync(
  494 + ISqlSugarClient db,
  495 + LabelEntityPartnerKind kind,
  496 + string entityId)
  497 + {
  498 + switch (kind)
  499 + {
  500 + case LabelEntityPartnerKind.Type:
  501 + await db.Deleteable<FlLabelTypePartnerDbEntity>()
  502 + .Where(x => x.LabelTypeId == entityId)
  503 + .ExecuteCommandAsync();
  504 + break;
  505 + case LabelEntityPartnerKind.Category:
  506 + await db.Deleteable<FlLabelCategoryPartnerDbEntity>()
  507 + .Where(x => x.CategoryId == entityId)
  508 + .ExecuteCommandAsync();
  509 + break;
  510 + case LabelEntityPartnerKind.MultipleOption:
  511 + await db.Deleteable<FlLabelMultipleOptionPartnerDbEntity>()
  512 + .Where(x => x.MultipleOptionId == entityId)
  513 + .ExecuteCommandAsync();
  514 + break;
  515 + }
  516 + }
  517 +
459 518 private static void ValidatePartnerType(string type)
460 519 {
461 520 if (type != ScopeAll && type != ScopeSpecified)
... ... @@ -514,9 +573,6 @@ public static class LabelEntityPartnerScopeHelper
514 573 return _cachedSchema;
515 574 }
516 575  
517   - private static bool HasPartnerScopeForKind(LabelEntityPartnerScopeSchemaStatus schema, LabelEntityPartnerKind kind) =>
518   - HasPartnerTableForKind(schema, kind) && HasPartnerColumnForKind(schema, kind);
519   -
520 576 private static bool HasPartnerTableForKind(LabelEntityPartnerScopeSchemaStatus schema, LabelEntityPartnerKind kind) =>
521 577 kind switch
522 578 {
... ...
美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application/Helpers/LabelTemplateScopeHelper.cs
... ... @@ -150,13 +150,29 @@ public static class LabelTemplateScopeHelper
150 150 string? currentUserId,
151 151 DateTime now)
152 152 {
153   - var hasExtendedScope = await LabelTemplateScopeSchemaHelper.HasPartnerRegionScopeTablesAsync(db);
  153 + var needsPartnerRows = string.Equals(scope.AppliedPartnerType, ScopeSpecified, StringComparison.OrdinalIgnoreCase)
  154 + && scope.PartnerIds.Count > 0;
  155 + var needsRegionRows = string.Equals(scope.AppliedRegionType, ScopeSpecified, StringComparison.OrdinalIgnoreCase)
  156 + && scope.RegionIds.Count > 0;
154 157  
155   - if (hasExtendedScope)
  158 + var hasPartnerTable = await LabelTemplateScopeSchemaHelper.EnsurePartnerScopeTableAsync(db, needsPartnerRows);
  159 + var hasRegionTable = await LabelTemplateScopeSchemaHelper.EnsureRegionScopeTableAsync(db, needsRegionRows);
  160 +
  161 + if (needsPartnerRows && !hasPartnerTable)
  162 + {
  163 + throw new UserFriendlyException(
  164 + "Company 适用范围关联表尚未就绪,请联系管理员执行数据库迁移(fl_label_template_scope.sql)后重试");
  165 + }
  166 +
  167 + if (hasPartnerTable)
156 168 {
157 169 await db.Deleteable<FlLabelTemplatePartnerDbEntity>()
158 170 .Where(x => x.TemplateId == templateId)
159 171 .ExecuteCommandAsync();
  172 + }
  173 +
  174 + if (hasRegionTable)
  175 + {
160 176 await db.Deleteable<FlLabelTemplateRegionDbEntity>()
161 177 .Where(x => x.TemplateId == templateId)
162 178 .ExecuteCommandAsync();
... ... @@ -166,9 +182,7 @@ public static class LabelTemplateScopeHelper
166 182 .Where(x => x.TemplateId == templateId)
167 183 .ExecuteCommandAsync();
168 184  
169   - if (hasExtendedScope
170   - && string.Equals(scope.AppliedPartnerType, ScopeSpecified, StringComparison.OrdinalIgnoreCase)
171   - && scope.PartnerIds.Count > 0)
  185 + if (hasPartnerTable && needsPartnerRows)
172 186 {
173 187 var rows = scope.PartnerIds.Select(pid => new FlLabelTemplatePartnerDbEntity
174 188 {
... ... @@ -181,9 +195,7 @@ public static class LabelTemplateScopeHelper
181 195 await db.Insertable(rows).ExecuteCommandAsync();
182 196 }
183 197  
184   - if (hasExtendedScope
185   - && string.Equals(scope.AppliedRegionType, ScopeSpecified, StringComparison.OrdinalIgnoreCase)
186   - && scope.RegionIds.Count > 0)
  198 + if (hasRegionTable && needsRegionRows)
187 199 {
188 200 var rows = scope.RegionIds.Select(gid => new FlLabelTemplateRegionDbEntity
189 201 {
... ...
美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application/Helpers/LabelTemplateScopeSchemaHelper.cs
... ... @@ -116,6 +116,13 @@ public static class LabelTemplateScopeSchemaHelper
116 116 /// <summary>单元测试或切换库后调用。</summary>
117 117 public static void ResetCacheForTests() => _cached = null;
118 118  
  119 + /// <summary>清除缓存并重新探测 schema(应用启动早于迁移脚本执行后调用)。</summary>
  120 + public static async Task<LabelTemplateScopeSchemaStatus> RefreshStatusAsync(ISqlSugarClient db)
  121 + {
  122 + _cached = null;
  123 + return await GetStatusAsync(db);
  124 + }
  125 +
119 126 /// <summary>
120 127 /// 是否已创建 <c>fl_label_template_partner</c> 与 <c>fl_label_template_region</c>。
121 128 /// </summary>
... ... @@ -126,6 +133,46 @@ public static class LabelTemplateScopeSchemaHelper
126 133 }
127 134  
128 135 /// <summary>
  136 + /// 是否已创建 <c>fl_label_template_partner</c>;缓存未命中时刷新后再探测。
  137 + /// </summary>
  138 + public static async Task<bool> EnsurePartnerScopeTableAsync(ISqlSugarClient db, bool needsPartnerRows)
  139 + {
  140 + var status = await GetStatusAsync(db);
  141 + if (status.HasPartnerScopeTable)
  142 + {
  143 + return true;
  144 + }
  145 +
  146 + if (!needsPartnerRows)
  147 + {
  148 + return false;
  149 + }
  150 +
  151 + status = await RefreshStatusAsync(db);
  152 + return status.HasPartnerScopeTable;
  153 + }
  154 +
  155 + /// <summary>
  156 + /// 是否已创建 <c>fl_label_template_region</c>;缓存未命中时刷新后再探测。
  157 + /// </summary>
  158 + public static async Task<bool> EnsureRegionScopeTableAsync(ISqlSugarClient db, bool needsRegionRows)
  159 + {
  160 + var status = await GetStatusAsync(db);
  161 + if (status.HasRegionScopeTable)
  162 + {
  163 + return true;
  164 + }
  165 +
  166 + if (!needsRegionRows)
  167 + {
  168 + return false;
  169 + }
  170 +
  171 + status = await RefreshStatusAsync(db);
  172 + return status.HasRegionScopeTable;
  173 + }
  174 +
  175 + /// <summary>
129 176 /// 已迁移 scope 列时,写入 <c>AppliedPartnerType</c> / <c>AppliedRegionType</c>(未迁移则 no-op)。
130 177 /// </summary>
131 178 public static async Task SetAppliedScopeTypesAsync(
... ...
美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application/Helpers/LocationScopeBindingHelper.cs
... ... @@ -336,6 +336,34 @@ public static class LocationScopeBindingHelper
336 336 }
337 337  
338 338 /// <summary>
  339 + /// 标签类型/分类/多选项门店范围落库:仅按 Region/Location 合并,不因 Company(partner)展开并集。
  340 + /// </summary>
  341 + public static async Task<List<string>> ResolveEntityLocationIdsForSaveAsync(
  342 + ISqlSugarClient db,
  343 + bool locationSpecified,
  344 + IReadOnlyList<string>? regionIds,
  345 + IReadOnlyList<string>? locationIds)
  346 + {
  347 + if (!locationSpecified)
  348 + {
  349 + return new List<string>();
  350 + }
  351 +
  352 + var merged = await MergeToLocationIdsAsync(
  353 + db,
  354 + (IReadOnlyList<string>?)null,
  355 + regionIds,
  356 + locationIds);
  357 + if (merged.Count == 0)
  358 + {
  359 + throw new UserFriendlyException("指定适用区域或门店时,至少需要匹配到一个有效门店");
  360 + }
  361 +
  362 + await ValidateLocationIdsExistAsync(db, merged);
  363 + return merged;
  364 + }
  365 +
  366 + /// <summary>
339 367 /// 根据已绑定门店反推适用的 Company Id(<c>fl_partner.Id</c>)。
340 368 /// </summary>
341 369 public static async Task<List<string>> ResolvePartnerIdsFromLocationIdsAsync(
... ...
美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application/Helpers/ReportsDateTimeDisplayHelper.cs
1 1 using System.Globalization;
2 2  
3   -
4   -
5 3 namespace FoodLabeling.Application.Helpers;
6 4  
7   -
8   -
9 5 /// <summary>
10   -
11   -/// Reports Print Log 日期时间展示:统一为 <c>MM/dd/yy HH:mm</c>(精确到分钟)。
12   -
  6 +/// Reports Print Log 日期展示:统一为美式短日期 <c>MM/dd/yy</c>(MM/DD/YY)。
13 7 /// </summary>
14   -
15 8 public static class ReportsDateTimeDisplayHelper
16   -
17 9 {
  10 + /// <summary>列表日期列统一格式(MM/DD/YY)。</summary>
  11 + public const string DateDisplayFormat = "MM/dd/yy";
18 12  
19   - public const string DisplayFormat = "MM/dd/yy HH:mm";
20   -
  13 + /// <summary>含时分时在同日期格式后追加 HH:mm。</summary>
  14 + public const string DateTimeDisplayFormat = "MM/dd/yy HH:mm";
21 15  
  16 + /// <summary>兼容旧引用。</summary>
  17 + public const string DisplayFormat = DateTimeDisplayFormat;
22 18  
23 19 private static readonly string[] ExpiryParseFormats =
24   -
25 20 {
26   -
27   - DisplayFormat,
28   -
  21 + DateTimeDisplayFormat,
  22 + DateDisplayFormat,
29 23 "MM/dd/yy",
30   -
31 24 "MM/dd",
32   -
33 25 "yyyy-MM-dd H:mm",
34   -
35 26 "yyyy-MM-dd",
36   -
37 27 "yyyy/MM/dd HH:mm",
38   -
39 28 "yyyy/MM/dd",
40   -
41 29 "MM/dd/yyyy hh:mm tt",
42   -
43 30 "MM/dd/yyyy h:mm tt",
44   -
45 31 "MM/dd/yyyy HH:mm",
46   -
47 32 "MM/dd/yyyy",
48   -
49 33 "M/d/yyyy hh:mm tt",
50   -
51 34 "M/d/yyyy h:mm tt",
52   -
53 35 "M/d/yyyy",
54   -
55 36 "dd/MM/yyyy HH:mm",
56   -
57 37 "dd/MM/yyyy",
58   -
59 38 "d/M/yyyy HH:mm",
60   -
61 39 "d/M/yyyy",
62   -
63 40 };
64 41  
65   -
66   -
67   - /// <summary>打印时间展示(Printed at 列)</summary>
68   -
  42 + /// <summary>打印时间展示(Printed at 列):统一 MM/DD/YY。</summary>
69 43 public static string FormatPrintedAt(DateTime printedAt)
70   -
71 44 {
72   -
73 45 if (printedAt == DateTime.MinValue)
74   -
75 46 {
76   -
77 47 return "无";
78   -
79 48 }
80 49  
81   -
82   -
83   - return printedAt.ToString(DisplayFormat, CultureInfo.InvariantCulture);
84   -
  50 + return printedAt.ToString(DateDisplayFormat, CultureInfo.InvariantCulture);
85 51 }
86 52  
87   -
88   -
89   - /// <summary>保质期展示(Expiration 列):解析后统一为 <see cref="DisplayFormat"/>。</summary>
90   -
  53 + /// <summary>保质期展示(Expiration 列):解析后统一为 <see cref="DateDisplayFormat"/> 或含时分时 <see cref="DateTimeDisplayFormat"/>。</summary>
91 54 public static string FormatExpiryDisplay(string? raw, DateTime? anchor = null)
92   -
93 55 {
94   -
95 56 if (string.IsNullOrWhiteSpace(raw) || string.Equals(raw.Trim(), "无", StringComparison.Ordinal))
96   -
97 57 {
98   -
99 58 return "无";
100   -
101 59 }
102 60  
103   -
104   -
105 61 var s = raw.Trim();
106   -
107 62 if (TryParseToDateTime(s, out var dt))
108   -
109 63 {
110   -
111   - return dt.ToString(DisplayFormat, CultureInfo.InvariantCulture);
112   -
  64 + return FormatExpiryDateTime(dt);
113 65 }
114 66  
115   -
116   -
117   - if (anchor.HasValue && TryParsePartialDateTime(s, anchor.Value, out dt))
118   -
  67 + var reference = anchor ?? DateTime.Now;
  68 + if (TryParsePartialDateTime(s, reference, out dt))
119 69 {
120   -
121   - return dt.ToString(DisplayFormat, CultureInfo.InvariantCulture);
122   -
  70 + return FormatExpiryDateTime(dt);
123 71 }
124 72  
125   -
126   -
127 73 return s;
128   -
129 74 }
130 75  
131   -
132   -
133 76 /// <summary>合并日期片段(如 07/09)与时间片段(如 11:56),缺少年份时用 anchor 补全。</summary>
134   -
135 77 public static string MergeDateAndTimeParts(string datePart, string timePart, DateTime anchor)
136   -
137 78 {
138   -
139 79 var date = datePart.Trim();
140   -
141 80 var time = timePart.Trim();
142   -
143 81 if (string.IsNullOrWhiteSpace(date))
144   -
145 82 {
146   -
147 83 return string.IsNullOrWhiteSpace(time) ? "无" : time;
148   -
149 84 }
150 85  
151   -
152   -
153 86 if (!TryParsePartialDateTime(date, anchor, out var dt) && !TryParseToDateTime(date, out dt))
154   -
155 87 {
156   -
157 88 return string.IsNullOrWhiteSpace(time) ? date : $"{date} {time}";
158   -
159 89 }
160 90  
161   -
162   -
163 91 if (!string.IsNullOrWhiteSpace(time))
164   -
165 92 {
166   -
167 93 var timeMatch = System.Text.RegularExpressions.Regex.Match(
168   -
169 94 time,
170   -
171 95 @"^(?<h>\d{1,2}):(?<m>\d{2})",
172   -
173 96 System.Text.RegularExpressions.RegexOptions.CultureInvariant);
174   -
175 97 if (timeMatch.Success &&
176   -
177 98 int.TryParse(timeMatch.Groups["h"].Value, out var h) &&
178   -
179 99 int.TryParse(timeMatch.Groups["m"].Value, out var m))
180   -
181 100 {
182   -
183 101 dt = new DateTime(dt.Year, dt.Month, dt.Day, h, m, 0, dt.Kind);
184   -
185 102 }
186   -
187 103 }
188 104  
189   -
190   -
191   - return dt.ToString(DisplayFormat, CultureInfo.InvariantCulture);
192   -
  105 + return FormatExpiryDateTime(dt);
193 106 }
194 107  
195   -
196   -
197 108 private static bool TryParsePartialDateTime(string s, DateTime anchor, out DateTime dt)
198   -
199 109 {
200   -
201 110 dt = default;
202   -
203 111 var text = s.Trim();
204   -
205 112 var timeMatch = System.Text.RegularExpressions.Regex.Match(
206   -
207 113 text,
208   -
209 114 @"^(?<date>\d{1,2}/\d{1,2}(?:/\d{2,4})?)\s+(?<time>\d{1,2}:\d{2})$",
210   -
211 115 System.Text.RegularExpressions.RegexOptions.CultureInvariant);
212   -
213 116 if (timeMatch.Success)
214   -
215 117 {
216   -
217 118 text = timeMatch.Groups["date"].Value;
218   -
219 119 if (TryParsePartialDateOnly(text, anchor, out dt))
220   -
221 120 {
222   -
223 121 var timePart = timeMatch.Groups["time"].Value;
224   -
225 122 var parts = timePart.Split(':');
226   -
227 123 if (parts.Length == 2 &&
228   -
229 124 int.TryParse(parts[0], out var h) &&
230   -
231 125 int.TryParse(parts[1], out var m))
232   -
233 126 {
234   -
235 127 dt = new DateTime(dt.Year, dt.Month, dt.Day, h, m, 0, dt.Kind);
236   -
237 128 return true;
238   -
239 129 }
240   -
241 130 }
242   -
243 131 }
244 132  
245   -
246   -
247 133 return TryParsePartialDateOnly(text, anchor, out dt);
248   -
249 134 }
250 135  
251   -
252   -
253 136 private static bool TryParsePartialDateOnly(string s, DateTime anchor, out DateTime dt)
254   -
255 137 {
256   -
257 138 dt = default;
258   -
259 139 var parts = s.Split('/');
260   -
261 140 if (parts.Length is < 2 or > 3)
262   -
263 141 {
264   -
265 142 return false;
266   -
267 143 }
268 144  
269   -
270   -
271 145 if (!int.TryParse(parts[0], out var month) || !int.TryParse(parts[1], out var day))
272   -
273 146 {
274   -
275 147 return false;
276   -
277 148 }
278 149  
279   -
280   -
281 150 var year = anchor.Year;
282   -
283 151 if (parts.Length == 3)
284   -
285 152 {
286   -
287 153 if (!int.TryParse(parts[2], out var y))
288   -
289 154 {
290   -
291 155 return false;
292   -
293 156 }
294 157  
295   -
296   -
297 158 year = parts[2].Length == 2 ? 2000 + y : y;
298   -
299 159 }
300 160  
301   -
302   -
303 161 try
304   -
305 162 {
306   -
307 163 dt = new DateTime(year, month, day, 0, 0, 0, anchor.Kind);
308   -
309 164 return true;
310   -
311 165 }
312   -
313 166 catch
314   -
315 167 {
316   -
317 168 return false;
318   -
319 169 }
320   -
321 170 }
322 171  
323   -
324   -
325 172 private static bool TryParseToDateTime(string s, out DateTime dt)
326   -
327 173 {
328   -
329 174 if (DateTime.TryParse(
330   -
331 175 s,
332   -
333 176 CultureInfo.InvariantCulture,
334   -
335 177 DateTimeStyles.AllowWhiteSpaces | DateTimeStyles.AssumeLocal,
336   -
337 178 out dt))
338   -
339 179 {
340   -
341 180 return true;
342   -
343 181 }
344 182  
345   -
346   -
347 183 if (DateTime.TryParse(
348   -
349 184 s,
350   -
351 185 CultureInfo.GetCultureInfo("en-US"),
352   -
353 186 DateTimeStyles.AllowWhiteSpaces | DateTimeStyles.AssumeLocal,
354   -
355 187 out dt))
356   -
357 188 {
358   -
359 189 return true;
360   -
361 190 }
362 191  
363   -
364   -
365 192 foreach (var fmt in ExpiryParseFormats)
366   -
367 193 {
368   -
369 194 if (DateTime.TryParseExact(
370   -
371 195 s,
372   -
373 196 fmt,
374   -
375 197 CultureInfo.InvariantCulture,
376   -
377 198 DateTimeStyles.AllowWhiteSpaces | DateTimeStyles.AssumeLocal,
378   -
379 199 out dt))
380   -
381 200 {
382   -
383 201 return true;
384   -
385 202 }
386 203  
387   -
388   -
389 204 if (DateTime.TryParseExact(
390   -
391 205 s,
392   -
393 206 fmt,
394   -
395 207 CultureInfo.GetCultureInfo("en-US"),
396   -
397 208 DateTimeStyles.AllowWhiteSpaces | DateTimeStyles.AssumeLocal,
398   -
399 209 out dt))
400   -
401 210 {
402   -
403 211 return true;
404   -
405 212 }
406   -
407 213 }
408 214  
409   -
410   -
411 215 return false;
412 216 }
413 217  
... ... @@ -419,7 +223,12 @@ public static class ReportsDateTimeDisplayHelper
419 223 return "无";
420 224 }
421 225  
422   - return dt.ToString(DisplayFormat, CultureInfo.InvariantCulture);
  226 + if (dt.Hour == 0 && dt.Minute == 0 && dt.Second == 0)
  227 + {
  228 + return dt.ToString(DateDisplayFormat, CultureInfo.InvariantCulture);
  229 + }
  230 +
  231 + return dt.ToString(DateTimeDisplayFormat, CultureInfo.InvariantCulture);
423 232 }
424 233  
425 234 /// <summary>解析到期字面量(含 anchor 补全年份/时间)。</summary>
... ... @@ -433,5 +242,3 @@ public static class ReportsDateTimeDisplayHelper
433 242 return TryParsePartialDateTime(s, anchor, out dt);
434 243 }
435 244 }
436   -
437   -
... ...
美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application/Helpers/ReportsPrintLogExpiryHelper.cs
... ... @@ -747,8 +747,8 @@ public static class ReportsPrintLogExpiryHelper
747 747 text = raw.Trim();
748 748 if (TryParseOffsetPayload(text, out var amount, out var unit))
749 749 {
750   - text = ApplyOffset(anchor, amount, unit)
751   - .ToString(ReportsDateTimeDisplayHelper.DisplayFormat, CultureInfo.InvariantCulture);
  750 + text = ReportsDateTimeDisplayHelper.FormatExpiryDateTime(
  751 + ApplyOffset(anchor, amount, unit));
752 752 return true;
753 753 }
754 754  
... ... @@ -777,8 +777,8 @@ public static class ReportsPrintLogExpiryHelper
777 777  
778 778 if (v.ValueKind == JsonValueKind.Number && v.TryGetInt64(out var unix))
779 779 {
780   - text = DateTimeOffset.FromUnixTimeSeconds(unix).LocalDateTime
781   - .ToString(ReportsDateTimeDisplayHelper.DisplayFormat, CultureInfo.InvariantCulture);
  780 + text = ReportsDateTimeDisplayHelper.FormatExpiryDateTime(
  781 + DateTimeOffset.FromUnixTimeSeconds(unix).LocalDateTime);
782 782 return true;
783 783 }
784 784  
... ...
美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application/Services/DbModels/FlLabelCategoryDbEntity.cs
... ... @@ -47,5 +47,10 @@ public class FlLabelCategoryDbEntity
47 47 /// 门店可用范围:ALL / SPECIFIED
48 48 /// </summary>
49 49 public string AvailabilityType { get; set; } = "ALL";
  50 +
  51 + /// <summary>
  52 + /// 适用 Company 范围:ALL / SPECIFIED
  53 + /// </summary>
  54 + public string AppliedPartnerType { get; set; } = "ALL";
50 55 }
51 56  
... ...
美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application/Services/DbModels/FlLabelMultipleOptionDbEntity.cs
... ... @@ -34,5 +34,10 @@ public class FlLabelMultipleOptionDbEntity
34 34 /// 门店可用范围:ALL / SPECIFIED
35 35 /// </summary>
36 36 public string AvailabilityType { get; set; } = "ALL";
  37 +
  38 + /// <summary>
  39 + /// 适用 Company 范围:ALL / SPECIFIED
  40 + /// </summary>
  41 + public string AppliedPartnerType { get; set; } = "ALL";
37 42 }
38 43  
... ...
美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application/Services/DbModels/FlLabelTypeDbEntity.cs
... ... @@ -32,5 +32,10 @@ public class FlLabelTypeDbEntity
32 32 /// 门店可用范围:ALL / SPECIFIED
33 33 /// </summary>
34 34 public string AvailabilityType { get; set; } = "ALL";
  35 +
  36 + /// <summary>
  37 + /// 适用 Company 范围:ALL / SPECIFIED
  38 + /// </summary>
  39 + public string AppliedPartnerType { get; set; } = "ALL";
35 40 }
36 41  
... ...
美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application/Services/LabelCategoryAppService.cs
... ... @@ -176,6 +176,7 @@ public class LabelCategoryAppService : ApplicationService, ILabelCategoryAppServ
176 176 State = input.State,
177 177 ButtonAppearance = appearance,
178 178 AvailabilityType = availabilityType,
  179 + AppliedPartnerType = partnerScope.AppliedPartnerType,
179 180 OrderNum = input.OrderNum
180 181 };
181 182  
... ... @@ -219,6 +220,7 @@ public class LabelCategoryAppService : ApplicationService, ILabelCategoryAppServ
219 220 entity.State = input.State;
220 221 entity.ButtonAppearance = appearance;
221 222 entity.AvailabilityType = availabilityType;
  223 + entity.AppliedPartnerType = partnerScope.AppliedPartnerType;
222 224 entity.OrderNum = input.OrderNum;
223 225 entity.LastModificationTime = DateTime.Now;
224 226 entity.LastModifierId = CurrentUser?.Id?.ToString();
... ... @@ -305,37 +307,14 @@ public class LabelCategoryAppService : ApplicationService, ILabelCategoryAppServ
305 307 throw new UserFriendlyException("门店可用范围不合法(ALL/SPECIFIED)");
306 308 }
307 309  
308   - var partnerSpecified = string.Equals(
309   - partnerScope.AppliedPartnerType,
310   - LabelEntityPartnerScopeHelper.ScopeSpecified,
311   - StringComparison.OrdinalIgnoreCase);
312 310 var locationSpecified = string.Equals(availabilityType, "SPECIFIED", StringComparison.OrdinalIgnoreCase);
  311 + var savedLocationIds = await LocationScopeBindingHelper.ResolveEntityLocationIdsForSaveAsync(
  312 + _dbContext.SqlSugarClient,
  313 + locationSpecified,
  314 + regionIds,
  315 + explicitLocationIds);
313 316  
314   - if (partnerSpecified || locationSpecified)
315   - {
316   - var merged = await LocationScopeBindingHelper.MergeToLocationIdsAsync(
317   - _dbContext.SqlSugarClient,
318   - partnerSpecified ? partnerScope.PartnerIds : null,
319   - locationSpecified ? regionIds : null,
320   - locationSpecified ? explicitLocationIds : null);
321   - if (merged.Count == 0)
322   - {
323   - throw new UserFriendlyException("指定适用范围后至少需要匹配到一个有效门店");
324   - }
325   -
326   - if (locationSpecified)
327   - {
328   - await LocationScopeBindingHelper.ValidateLocationIdsExistAsync(_dbContext.SqlSugarClient, merged);
329   - return (partnerScope, "SPECIFIED", merged);
330   - }
331   - }
332   -
333   - if (string.Equals(availabilityType, "ALL", StringComparison.OrdinalIgnoreCase))
334   - {
335   - return (partnerScope, "ALL", new List<string>());
336   - }
337   -
338   - throw new UserFriendlyException("指定适用区域或门店时,至少需要匹配到一个有效门店");
  317 + return (partnerScope, locationSpecified ? "SPECIFIED" : "ALL", savedLocationIds);
339 318 }
340 319  
341 320 private static List<string> NormalizeRegionIds(LabelCategoryCreateInputVo input)
... ...
美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application/Services/LabelMultipleOptionAppService.cs
... ... @@ -148,6 +148,7 @@ public class LabelMultipleOptionAppService : ApplicationService, ILabelMultipleO
148 148 OptionValuesJson = input.OptionValuesJson?.Trim(),
149 149 State = input.State,
150 150 AvailabilityType = availabilityType,
  151 + AppliedPartnerType = partnerScope.AppliedPartnerType,
151 152 OrderNum = input.OrderNum
152 153 };
153 154  
... ... @@ -185,6 +186,7 @@ public class LabelMultipleOptionAppService : ApplicationService, ILabelMultipleO
185 186 entity.OptionValuesJson = input.OptionValuesJson?.Trim();
186 187 entity.State = input.State;
187 188 entity.AvailabilityType = availabilityType;
  189 + entity.AppliedPartnerType = partnerScope.AppliedPartnerType;
188 190 entity.OrderNum = input.OrderNum;
189 191 entity.LastModificationTime = DateTime.Now;
190 192 entity.LastModifierId = CurrentUser?.Id?.ToString();
... ... @@ -252,37 +254,14 @@ public class LabelMultipleOptionAppService : ApplicationService, ILabelMultipleO
252 254 throw new UserFriendlyException("门店可用范围不合法(ALL/SPECIFIED)");
253 255 }
254 256  
255   - var partnerSpecified = string.Equals(
256   - partnerScope.AppliedPartnerType,
257   - LabelEntityPartnerScopeHelper.ScopeSpecified,
258   - StringComparison.OrdinalIgnoreCase);
259 257 var locationSpecified = string.Equals(availabilityType, "SPECIFIED", StringComparison.OrdinalIgnoreCase);
  258 + var savedLocationIds = await LocationScopeBindingHelper.ResolveEntityLocationIdsForSaveAsync(
  259 + _dbContext.SqlSugarClient,
  260 + locationSpecified,
  261 + regionIds,
  262 + explicitLocationIds);
260 263  
261   - if (partnerSpecified || locationSpecified)
262   - {
263   - var merged = await LocationScopeBindingHelper.MergeToLocationIdsAsync(
264   - _dbContext.SqlSugarClient,
265   - partnerSpecified ? partnerScope.PartnerIds : null,
266   - locationSpecified ? regionIds : null,
267   - locationSpecified ? explicitLocationIds : null);
268   - if (merged.Count == 0)
269   - {
270   - throw new UserFriendlyException("指定适用范围后至少需要匹配到一个有效门店");
271   - }
272   -
273   - if (locationSpecified)
274   - {
275   - await LocationScopeBindingHelper.ValidateLocationIdsExistAsync(_dbContext.SqlSugarClient, merged);
276   - return (partnerScope, "SPECIFIED", merged);
277   - }
278   - }
279   -
280   - if (string.Equals(availabilityType, "ALL", StringComparison.OrdinalIgnoreCase))
281   - {
282   - return (partnerScope, "ALL", new List<string>());
283   - }
284   -
285   - throw new UserFriendlyException("指定适用区域或门店时,至少需要匹配到一个有效门店");
  264 + return (partnerScope, locationSpecified ? "SPECIFIED" : "ALL", savedLocationIds);
286 265 }
287 266  
288 267 private async Task SaveMultipleOptionPartnerScopeAsync(
... ...
美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application/Services/LabelTypeAppService.cs
... ... @@ -155,6 +155,7 @@ public class LabelTypeAppService : ApplicationService, ILabelTypeAppService
155 155 TypeName = name,
156 156 State = input.State,
157 157 AvailabilityType = availabilityType,
  158 + AppliedPartnerType = partnerScope.AppliedPartnerType,
158 159 OrderNum = input.OrderNum
159 160 };
160 161  
... ... @@ -193,6 +194,7 @@ public class LabelTypeAppService : ApplicationService, ILabelTypeAppService
193 194 entity.TypeName = name;
194 195 entity.State = input.State;
195 196 entity.AvailabilityType = availabilityType;
  197 + entity.AppliedPartnerType = partnerScope.AppliedPartnerType;
196 198 entity.OrderNum = input.OrderNum;
197 199 entity.LastModificationTime = DateTime.Now;
198 200 entity.LastModifierId = CurrentUser?.Id?.ToString();
... ... @@ -267,37 +269,14 @@ public class LabelTypeAppService : ApplicationService, ILabelTypeAppService
267 269 throw new UserFriendlyException("门店可用范围不合法(ALL/SPECIFIED)");
268 270 }
269 271  
270   - var partnerSpecified = string.Equals(
271   - partnerScope.AppliedPartnerType,
272   - LabelEntityPartnerScopeHelper.ScopeSpecified,
273   - StringComparison.OrdinalIgnoreCase);
274 272 var locationSpecified = string.Equals(availabilityType, "SPECIFIED", StringComparison.OrdinalIgnoreCase);
  273 + var savedLocationIds = await LocationScopeBindingHelper.ResolveEntityLocationIdsForSaveAsync(
  274 + _dbContext.SqlSugarClient,
  275 + locationSpecified,
  276 + regionIds,
  277 + explicitLocationIds);
275 278  
276   - if (partnerSpecified || locationSpecified)
277   - {
278   - var merged = await LocationScopeBindingHelper.MergeToLocationIdsAsync(
279   - _dbContext.SqlSugarClient,
280   - partnerSpecified ? partnerScope.PartnerIds : null,
281   - locationSpecified ? regionIds : null,
282   - locationSpecified ? explicitLocationIds : null);
283   - if (merged.Count == 0)
284   - {
285   - throw new UserFriendlyException("指定适用范围后至少需要匹配到一个有效门店");
286   - }
287   -
288   - if (locationSpecified)
289   - {
290   - await LocationScopeBindingHelper.ValidateLocationIdsExistAsync(_dbContext.SqlSugarClient, merged);
291   - return (partnerScope, "SPECIFIED", merged);
292   - }
293   - }
294   -
295   - if (string.Equals(availabilityType, "ALL", StringComparison.OrdinalIgnoreCase))
296   - {
297   - return (partnerScope, "ALL", new List<string>());
298   - }
299   -
300   - throw new UserFriendlyException("指定适用区域或门店时,至少需要匹配到一个有效门店");
  279 + return (partnerScope, locationSpecified ? "SPECIFIED" : "ALL", savedLocationIds);
301 280 }
302 281  
303 282 private async Task SaveTypePartnerScopeAsync(
... ...