diff --git a/Atomx.Admin/Atomx.Admin.Client/Pages/Settings/AreaEdit.razor b/Atomx.Admin/Atomx.Admin.Client/Pages/Settings/AreaEdit.razor index f8e304f..bdfc446 100644 --- a/Atomx.Admin/Atomx.Admin.Client/Pages/Settings/AreaEdit.razor +++ b/Atomx.Admin/Atomx.Admin.Client/Pages/Settings/AreaEdit.razor @@ -152,7 +152,7 @@ async void LoadData() { pageLoading = true; - var url = $"/api/stateprovince/detail?id={Id}"; + var url = $"/api/area/detail?id={Id}"; var apiResult = await HttpService.Get>(url); if (apiResult.Success) { @@ -185,6 +185,8 @@ void OnCitiesChange(CascaderNode[] selectedNodes) { + Console.WriteLine($"value: {cities} selected: {string.Join(",", selectedNodes.Select(x => x.Value))}"); + model.StateProvinceId = long.Parse(selectedNodes.First().Value.ToString() ?? "0"); model.ParentId = long.Parse(selectedNodes.Last().Value.ToString() ?? "0"); if (model.StateProvinceId == model.ParentId) @@ -198,7 +200,7 @@ if (editform.Validate()) { saving = true; - var url = $"api/stateprovince/save"; + var url = $"api/area/save"; var result = new ApiResult(); result = await HttpService.Post>(url, model); @@ -207,7 +209,7 @@ { saving = false; await ModalService.InfoAsync(new ConfirmOptions() { Title = "提示", Content = "数据提交成功!" }); - Navigation.NavigateTo($"/stateprovince/list/{CountryId}"); + Navigation.NavigateTo($"/area/list/{CountryId}"); } else { diff --git a/Atomx.Admin/Atomx.Admin.Client/Pages/Settings/AreaList.razor b/Atomx.Admin/Atomx.Admin.Client/Pages/Settings/AreaList.razor index c86f74c..3857ef4 100644 --- a/Atomx.Admin/Atomx.Admin.Client/Pages/Settings/AreaList.razor +++ b/Atomx.Admin/Atomx.Admin.Client/Pages/Settings/AreaList.razor @@ -128,7 +128,7 @@ var uri = new Uri(Navigation.Uri); var query = uri.Query; search.Name = query.GetQueryString("Name"); - search.StateProvinceId = query.GetQueryString("StateProvinceId").ToLong(); + search.StateProvinceId = StateProvinceId; search.CountryId = CountryId; } @@ -220,7 +220,7 @@ void HandleEdit(Area model) { - Navigation.NavigateTo($"/area/edit/{model.Id}"); + Navigation.NavigateTo($"/area/edit/{model.CountryId}/{model.StateProvinceId}/{model.Id}"); } } diff --git a/Atomx.Admin/Atomx.Admin/Controllers/AreaController.cs b/Atomx.Admin/Atomx.Admin/Controllers/AreaController.cs index 4bbc693..8eb6dd6 100644 --- a/Atomx.Admin/Atomx.Admin/Controllers/AreaController.cs +++ b/Atomx.Admin/Atomx.Admin/Controllers/AreaController.cs @@ -3,6 +3,7 @@ using Atomx.Admin.Services; using Atomx.Common.Constants; using Atomx.Common.Entities; using Atomx.Common.Models; +using Atomx.Core.Jos; using Atomx.Data; using Atomx.Data.CacheServices; using Atomx.Data.Services; @@ -29,6 +30,7 @@ namespace Atomx.Admin.Controllers private readonly IIdentityService _identityService; private readonly IMapper _mapper; private readonly ICacheService _cacheService; + private readonly IBackgroundJobService _backgroundService; readonly IValidator _validator; readonly IStringLocalizer _localizer; @@ -40,9 +42,12 @@ namespace Atomx.Admin.Controllers /// /// /// - /// /// - public AreaController(ILogger logger, IIdCreatorService idCreator, IIdentityService identityService, DataContext dbContext, IMapper mapper, ICacheService cacheService, IValidator validator, IStringLocalizer localizer) + /// + /// + /// + public AreaController(ILogger logger, IIdCreatorService idCreator, IIdentityService identityService, DataContext dbContext, IMapper mapper, + ICacheService cacheService, IBackgroundJobService backgroundJobService, IValidator validator, IStringLocalizer localizer) { _logger = logger; _idCreator = idCreator; @@ -50,6 +55,7 @@ namespace Atomx.Admin.Controllers _dbContext = dbContext; _mapper = mapper; _cacheService = cacheService; + _backgroundService = backgroundJobService; _validator = validator; _localizer = localizer; } @@ -112,7 +118,7 @@ namespace Atomx.Admin.Controllers countries = _dbContext.Countries.Where(p => countryIds.Contains(p.Id)).ToList(); } - if(search.StateProvinceId > 0) + if (search.StateProvinceId > 0) { var state = _dbContext.StateProvinces.SingleOrDefault(p => p.Id == search.StateProvinceId); if (state != null) @@ -213,6 +219,8 @@ namespace Atomx.Admin.Controllers } if (model.Id > 0) { + + data = _dbContext.Areas.SingleOrDefault(p => p.Id == model.Id); if (data == null) { @@ -221,6 +229,18 @@ namespace Atomx.Admin.Controllers data = _mapper.Map(model, data); + var parent = _dbContext.Categories.Where(p => p.Id == model.ParentId).SingleOrDefault(); + if (parent == null) + { + data.Depth = 0; + data.Path = model.Id.ToString(); + } + else + { + data.Depth = parent.Depth + 1; + data.Path = $"{parent.Path},{data.Id}"; + } + _dbContext.SaveChanges(); } @@ -229,10 +249,25 @@ namespace Atomx.Admin.Controllers data = _mapper.Map(model); data.Id = _idCreator.CreateId(); + var parent = _dbContext.Categories.Where(p => p.Id == data.ParentId).SingleOrDefault(); + if (parent == null) + { + data.Depth = 0; + data.Path = data.Id.ToString(); + } + else + { + data.Depth = parent.Depth + 1; + data.Path = $"{parent.Path},{data.Id}"; + } + _dbContext.Areas.Add(data); _dbContext.SaveChanges(); } + _backgroundService.ResetStateProvinceAndAreaTree(model.CountryId); + + return new JsonResult(new ApiResult().IsSuccess("操作成功")); } diff --git a/Atomx.Admin/Atomx.Admin/Controllers/StateProvinceController.cs b/Atomx.Admin/Atomx.Admin/Controllers/StateProvinceController.cs index 3cc0e63..5bbea35 100644 --- a/Atomx.Admin/Atomx.Admin/Controllers/StateProvinceController.cs +++ b/Atomx.Admin/Atomx.Admin/Controllers/StateProvinceController.cs @@ -3,6 +3,7 @@ using Atomx.Admin.Services; using Atomx.Common.Constants; using Atomx.Common.Entities; using Atomx.Common.Models; +using Atomx.Core.Jos; using Atomx.Data; using Atomx.Data.CacheServices; using Atomx.Data.Services; @@ -26,6 +27,7 @@ namespace Atomx.Admin.Controllers private readonly IIdentityService _identityService; private readonly IMapper _mapper; private readonly ICacheService _cacheService; + private readonly IBackgroundJobService _backgroundService; readonly IValidator _validator; readonly IStringLocalizer _localizer; @@ -39,7 +41,8 @@ namespace Atomx.Admin.Controllers /// /// /// - public StateProvinceController(ILogger logger, IIdCreatorService idCreator, IIdentityService identityService, DataContext dbContext, IMapper mapper, ICacheService cacheService, IValidator validator, IStringLocalizer localizer) + public StateProvinceController(ILogger logger, IIdCreatorService idCreator, IIdentityService identityService, DataContext dbContext, IMapper mapper, + ICacheService cacheService, IBackgroundJobService backgroundJobService, IValidator validator, IStringLocalizer localizer) { _logger = logger; _idCreator = idCreator; @@ -47,6 +50,7 @@ namespace Atomx.Admin.Controllers _dbContext = dbContext; _mapper = mapper; _cacheService = cacheService; + _backgroundService = backgroundJobService; _validator = validator; _localizer = localizer; } @@ -200,7 +204,7 @@ namespace Atomx.Admin.Controllers data = _mapper.Map(model, data); _dbContext.SaveChanges(); - + _backgroundService.ResetStateProvinceAndAreaTree(data.CountryId); } else { diff --git a/Atomx.Common/Entities/Area.cs b/Atomx.Common/Entities/Area.cs index 3a87fcc..1b91328 100644 --- a/Atomx.Common/Entities/Area.cs +++ b/Atomx.Common/Entities/Area.cs @@ -44,6 +44,17 @@ namespace Atomx.Common.Entities [Column(TypeName = "varchar(1)")] public string Initial { get; set; } = string.Empty; + /// + /// 地区深度 + /// + public int Depth { get; set; } + + /// + /// 层级路径 + /// + [Column(TypeName = "varchar(100)")] + public string Path { get; set; } = string.Empty; + /// /// 是否允许配送 /// diff --git a/Atomx.Core/Jos/ResetCacheJob.cs b/Atomx.Core/Jos/ResetCacheJob.cs new file mode 100644 index 0000000..ad30364 --- /dev/null +++ b/Atomx.Core/Jos/ResetCacheJob.cs @@ -0,0 +1,54 @@ +using Atomx.Data; +using Atomx.Data.CacheServices; +using Hangfire; +using Microsoft.Extensions.Logging; + +namespace Atomx.Core.Jos +{ + public class ResetCacheJob + { + readonly ILogger _logger; + readonly DataContext _dbContext; + readonly ICacheService _cacheService; + public ResetCacheJob(ILogger logger, DataContext dataContext, ICacheService cacheService) + { + _logger = logger; + _dbContext = dataContext; + _cacheService = cacheService; + } + + /// + /// 如果任务失败,重试 3 次,超过后删除任务,60 秒内不允许并发执行 + /// + [AutomaticRetry(Attempts = 3, OnAttemptsExceeded = AttemptsExceededAction.Delete)] + [DisableConcurrentExecution(60)] + public async Task ResetStateProvinceAndAreaTree(long countryId) + { + try + { + await _cacheService.GetAreaTree(countryId, true); + } + catch (Exception ex) + { + _logger.LogError(ex, "Error resetting cache for countryId: {CountryId}", countryId); + } + } + + /// + /// 如果任务失败,重试 3 次,超过后删除任务,60 秒内不允许并发执行 + /// + [AutomaticRetry(Attempts = 3, OnAttemptsExceeded = AttemptsExceededAction.Delete)] + [DisableConcurrentExecution(60)] + public async Task ResetCountry() + { + try + { + await _cacheService.GetCountries(true); + } + catch (Exception ex) + { + _logger.LogError(ex, "Error resetting country cache"); + } + } + } +} diff --git a/Atomx.Core/Services/BackgroundJobService.cs b/Atomx.Core/Services/BackgroundJobService.cs index b87c016..896eb11 100644 --- a/Atomx.Core/Services/BackgroundJobService.cs +++ b/Atomx.Core/Services/BackgroundJobService.cs @@ -1,5 +1,7 @@ using Hangfire; using Microsoft.Extensions.Logging; +using System.IO; +using System.Text.RegularExpressions; namespace Atomx.Core.Jos { @@ -24,6 +26,13 @@ namespace Atomx.Core.Jos string SendSMSVerificationCode(string phoneNumber, string code, TimeSpan validDuration); + + /// + /// 更新调整区域树缓存数据,会更新州省缓存 + /// + /// + /// + string ResetStateProvinceAndAreaTree(long countryId); } public partial class BackgroundJobService : IBackgroundJobService @@ -68,5 +77,16 @@ namespace Atomx.Core.Jos return string.Empty; } + + /// + /// 更新调整区域树缓存数据,会更新州省缓存 + /// + /// + /// + public string ResetStateProvinceAndAreaTree(long countryId) + { + var jobId = _backgroundJobClient.Enqueue(job => job.ResetStateProvinceAndAreaTree(countryId)); + return jobId; + } } } diff --git a/Atomx.Data/CacheServices/AreaCacheService.cs b/Atomx.Data/CacheServices/AreaCacheService.cs index 7eae433..a6a2904 100644 --- a/Atomx.Data/CacheServices/AreaCacheService.cs +++ b/Atomx.Data/CacheServices/AreaCacheService.cs @@ -47,21 +47,21 @@ namespace Atomx.Data.CacheServices /// /// /// - Task ResetArea(Area area); + Task UpdateArea(Area area); /// /// 更新调整州省缓存数据 /// /// /// - Task ResetStateProvince(StateProvince stateProvince); + Task UpdateStateProvince(StateProvince stateProvince); /// /// 更新调整国家缓存数据 /// /// /// - Task ResetCountry(Country country); + Task UpdateCountry(Country country); /// /// 获取国家-省份-城市树形数据 @@ -78,8 +78,8 @@ namespace Atomx.Data.CacheServices var cacheData = await GetCacheAsync>($"{CacheKeys.CountryTree}{countryId}"); if (cacheData == null || reloadData) { - var states = await GetStateProvinces(countryId); - var cities = await GetAreas(countryId); + var states = await GetStateProvinces(countryId, true); + var cities = await GetAreas(countryId, true); var tree = new List(); @@ -87,13 +87,24 @@ namespace Atomx.Data.CacheServices { var item = new KeyValueTree { - Label = state.Id.ToString(), - Value = state.Name, - Children = new List() + Label = state.Name, + Value = state.Id.ToString() }; - item.Children = BuildAreaTree(state.Id, cities, new List()); + var citysInState = cities.Where(p => p.StateProvinceId == state.Id).ToList(); + foreach (var city in citysInState) + { + var cityItem = new KeyValueTree + { + Label = state.Name, + Value = state.Id.ToString() + }; + + cityItem.Children = BuildAreaTree(city.Id, cities, new List()); + item.Children.Add(cityItem); + } tree.Add(item); + } await SetCacheAsync($"{CacheKeys.CountryTree}{countryId}", tree); @@ -188,7 +199,7 @@ namespace Atomx.Data.CacheServices return cacheData; } - public async Task ResetArea(Area area) + public async Task UpdateArea(Area area) { var cacheData = await GetAreas(area.CountryId); var data = cacheData.Where(p => p.Id == area.Id).SingleOrDefault(); @@ -207,7 +218,7 @@ namespace Atomx.Data.CacheServices /// /// /// - public async Task ResetStateProvince(StateProvince stateProvince) + public async Task UpdateStateProvince(StateProvince stateProvince) { var cacheData = await GetStateProvinces(stateProvince.CountryId); var data = cacheData.Where(p => p.Id == stateProvince.Id).SingleOrDefault(); @@ -224,7 +235,7 @@ namespace Atomx.Data.CacheServices /// /// /// - public async Task ResetCountry(Country country) + public async Task UpdateCountry(Country country) { var cacheData = await GetCountries(); var data = cacheData.Where(p => p.Id == country.Id).SingleOrDefault(); @@ -243,9 +254,8 @@ namespace Atomx.Data.CacheServices { var item = new KeyValueTree { - Label = area.Id.ToString(), - Value = area.Name, - Children = new List() + Label = area.Name, + Value = area.Id.ToString() }; var childs = areas.Where(p => p.ParentId == area.Id).ToList(); if (childs.Count > 0) diff --git a/Atomx.Data/Migrations/20251224030208_0.1.Designer.cs b/Atomx.Data/Migrations/20260104093702_0.1.Designer.cs similarity index 99% rename from Atomx.Data/Migrations/20251224030208_0.1.Designer.cs rename to Atomx.Data/Migrations/20260104093702_0.1.Designer.cs index 2d5995a..bf15e64 100644 --- a/Atomx.Data/Migrations/20251224030208_0.1.Designer.cs +++ b/Atomx.Data/Migrations/20260104093702_0.1.Designer.cs @@ -12,7 +12,7 @@ using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; namespace Atomx.Data.Migrations { [DbContext(typeof(DataContext))] - [Migration("20251224030208_0.1")] + [Migration("20260104093702_0.1")] partial class _01 { /// @@ -245,6 +245,9 @@ namespace Atomx.Data.Migrations b.Property("CountryId") .HasColumnType("bigint"); + b.Property("Depth") + .HasColumnType("integer"); + b.Property("DisplayOrder") .HasColumnType("integer"); @@ -262,6 +265,10 @@ namespace Atomx.Data.Migrations b.Property("ParentId") .HasColumnType("bigint"); + b.Property("Path") + .IsRequired() + .HasColumnType("varchar(100)"); + b.Property("StateProvinceId") .HasColumnType("bigint"); diff --git a/Atomx.Data/Migrations/20251224030208_0.1.cs b/Atomx.Data/Migrations/20260104093702_0.1.cs similarity index 99% rename from Atomx.Data/Migrations/20251224030208_0.1.cs rename to Atomx.Data/Migrations/20260104093702_0.1.cs index 8a18f0e..c96adc0 100644 --- a/Atomx.Data/Migrations/20251224030208_0.1.cs +++ b/Atomx.Data/Migrations/20260104093702_0.1.cs @@ -106,6 +106,8 @@ namespace Atomx.Data.Migrations ParentId = table.Column(type: "bigint", nullable: false), Name = table.Column(type: "varchar(256)", nullable: false), Initial = table.Column(type: "varchar(1)", nullable: false), + Depth = table.Column(type: "integer", nullable: false), + Path = table.Column(type: "varchar(100)", nullable: false), AllowShipping = table.Column(type: "boolean", nullable: false), Enabled = table.Column(type: "boolean", nullable: false), DisplayOrder = table.Column(type: "integer", nullable: false) diff --git a/Atomx.Data/Migrations/DataContextModelSnapshot.cs b/Atomx.Data/Migrations/DataContextModelSnapshot.cs index 0e4e112..2fbbd1a 100644 --- a/Atomx.Data/Migrations/DataContextModelSnapshot.cs +++ b/Atomx.Data/Migrations/DataContextModelSnapshot.cs @@ -242,6 +242,9 @@ namespace Atomx.Data.Migrations b.Property("CountryId") .HasColumnType("bigint"); + b.Property("Depth") + .HasColumnType("integer"); + b.Property("DisplayOrder") .HasColumnType("integer"); @@ -259,6 +262,10 @@ namespace Atomx.Data.Migrations b.Property("ParentId") .HasColumnType("bigint"); + b.Property("Path") + .IsRequired() + .HasColumnType("varchar(100)"); + b.Property("StateProvinceId") .HasColumnType("bigint");