diff --git a/.env.example b/.env.example new file mode 100644 index 0000000000000000000000000000000000000000..ec34523b416f003159509cf039ba40b3dd7f2329 --- /dev/null +++ b/.env.example @@ -0,0 +1,57 @@ +# Docker Compose settings +TSHOTEL_IMAGE=yjj6731/tshotel-management-system-api:latest +CONTAINER_NAME=tshotel-api +API_HOST_PORT=63001 +APP_CONFIG_DIR=./docker-data/config +APP_KEYS_DIR=./docker-data/keys + +# Application settings +ASPNETCORE_ENVIRONMENT=docker +DefaultDatabase=MariaDB +InitializeDatabase=false + +# Database connection strings (pick one based on DefaultDatabase) +MariaDBConnectStr=Server=your_db_host;Database=tshoteldb;User=tshoteldb;Password=your_password; +MySqlConnectStr= +PgSqlConnectStr= +SqlServerConnectStr= + +# Security +Jwt__Key=replace_with_a_long_random_secret +Jwt__ExpiryMinutes=20 + +# CORS +AllowedOrigins__0=http://localhost:8080 +AllowedOrigins__1=https://www.yourdomain.com + +# Quartz jobs +JobKeys__0=ReservationExpirationCheckJob +JobKeys__1=MailServiceCheckJob +JobKeys__2=RedisServiceCheckJob +ExpirationSettings__NotifyDaysBefore=3 +ExpirationSettings__CheckIntervalMinutes=5 + +# Mail service +Mail__Enabled=false +Mail__Host=smtp.example.com +Mail__UserName=admin@example.com +Mail__Password=replace_with_mail_password +Mail__Port=465 +Mail__EnableSsl=true +Mail__DisplayName=TSHotel + +# Redis +Redis__Enabled=false +Redis__ConnectionString=host:port,password=your_redis_password +Redis__DefaultDatabase=0 + +# Lsky (optional) +Lsky__Enabled=false +Lsky__BaseAddress= +Lsky__Email= +Lsky__Password= +Lsky__UploadApi= +Lsky__GetTokenApi= + +# Version display (optional) +SoftwareVersion=1.0.0 diff --git a/EOM.TSHotelManagement.API/appsettings.Database.json b/EOM.TSHotelManagement.API/appsettings.Database.json index 0c020cb33fa566cef789a22a64918a785a8f5ea0..f4844c299062d2f9cfed4867e12420aadcb53eab 100644 --- a/EOM.TSHotelManagement.API/appsettings.Database.json +++ b/EOM.TSHotelManagement.API/appsettings.Database.json @@ -9,8 +9,8 @@ }, "Redis": { "Enabled": false, - "ConnectionString": "host:port,password=your_redis_password", //host:port,password=your_redis_password + "ConnectionString": "host:port,password=your_redis_password", "DefaultDatabase": 0 }, "InitializeDatabase": false -} \ No newline at end of file +} diff --git a/EOM.TSHotelManagement.Common/Helper/EntityMapper.cs b/EOM.TSHotelManagement.Common/Helper/EntityMapper.cs index 08f9a869aafa8f0113b8f5e83c2ef26f13d737ff..db5d8687c3ce69a1892aeb2c0cb5c4cd9ee4ad40 100644 --- a/EOM.TSHotelManagement.Common/Helper/EntityMapper.cs +++ b/EOM.TSHotelManagement.Common/Helper/EntityMapper.cs @@ -50,6 +50,14 @@ namespace EOM.TSHotelManagement.Common if (sourceValue == null) { + // Ensure optimistic-lock version does not silently fall back to entity defaults. + if (destinationProperty.Name.Equals("RowVersion", StringComparison.OrdinalIgnoreCase) + && destinationProperty.PropertyType == typeof(long)) + { + destinationProperty.SetValue(destination, 0L); + continue; + } + if (destinationProperty.PropertyType.IsValueType && Nullable.GetUnderlyingType(destinationProperty.PropertyType) != null) { @@ -257,4 +265,4 @@ namespace EOM.TSHotelManagement.Common return sourceList?.Select(Map).ToList(); } } -} \ No newline at end of file +} diff --git a/EOM.TSHotelManagement.Contract/Common/Dto/BaseInputDto.cs b/EOM.TSHotelManagement.Contract/Common/Dto/BaseInputDto.cs index 7362bb2c34cf996599bfe0a708cad704bdc96b58..3356d22205873883fea178bd3a3d88cd2586c943 100644 --- a/EOM.TSHotelManagement.Contract/Common/Dto/BaseInputDto.cs +++ b/EOM.TSHotelManagement.Contract/Common/Dto/BaseInputDto.cs @@ -6,5 +6,9 @@ /// 删除标识 /// public int? IsDelete { get; set; } = 0; + /// + /// 行版本(乐观锁) + /// + public long? RowVersion { get; set; } } } diff --git a/EOM.TSHotelManagement.Contract/Common/Dto/BaseOutputDto.cs b/EOM.TSHotelManagement.Contract/Common/Dto/BaseOutputDto.cs index db10398c890df0ce8ee0778d9a77a2eee7ce6299..0d262c4adf647ea436481701169c82e50299a7fd 100644 --- a/EOM.TSHotelManagement.Contract/Common/Dto/BaseOutputDto.cs +++ b/EOM.TSHotelManagement.Contract/Common/Dto/BaseOutputDto.cs @@ -3,5 +3,9 @@ public class BaseOutputDto : BaseAuditDto { public int? IsDelete { get; set; } + /// + /// 行版本(乐观锁) + /// + public long? RowVersion { get; set; } } } diff --git a/EOM.TSHotelManagement.Contract/Common/Dto/BaseResponseFactory.cs b/EOM.TSHotelManagement.Contract/Common/Dto/BaseResponseFactory.cs new file mode 100644 index 0000000000000000000000000000000000000000..52abea6f0cb19a8efde6390b7ec8db8ee4ac61b2 --- /dev/null +++ b/EOM.TSHotelManagement.Contract/Common/Dto/BaseResponseFactory.cs @@ -0,0 +1,16 @@ +using EOM.TSHotelManagement.Common; + +namespace EOM.TSHotelManagement.Contract +{ + public static class BaseResponseFactory + { + public static BaseResponse ConcurrencyConflict() + { + return new BaseResponse( + BusinessStatusCode.Conflict, + LocalizationHelper.GetLocalizedString( + "Data has been modified by another user. Please refresh and retry.", + "数据已被其他用户修改,请刷新后重试。")); + } + } +} diff --git a/EOM.TSHotelManagement.Data/Repository/GenericRepository.cs b/EOM.TSHotelManagement.Data/Repository/GenericRepository.cs index a2599873eb0f31a5951d12412207430227973647..2c06278ee3a88330946c8b0d195b66e302567de9 100644 --- a/EOM.TSHotelManagement.Data/Repository/GenericRepository.cs +++ b/EOM.TSHotelManagement.Data/Repository/GenericRepository.cs @@ -1,6 +1,7 @@ -using EOM.TSHotelManagement.Common; +using EOM.TSHotelManagement.Common; using EOM.TSHotelManagement.Domain; using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.Logging; using SqlSugar; using System.Linq.Expressions; @@ -15,11 +16,14 @@ namespace EOM.TSHotelManagement.Data private readonly JWTHelper _jWTHelper; - public GenericRepository(ISqlSugarClient client, IHttpContextAccessor httpContextAccessor, JWTHelper jWTHelper) : base(client) + private readonly ILogger> _log; + + public GenericRepository(ISqlSugarClient client, IHttpContextAccessor httpContextAccessor, JWTHelper jWTHelper, ILogger> log) : base(client) { base.Context = client; _httpContextAccessor = httpContextAccessor; _jWTHelper = jWTHelper; + _log = log; } private string GetCurrentUser() @@ -48,12 +52,16 @@ namespace EOM.TSHotelManagement.Data baseEntity.DataInsDate = DateTime.Now; if (string.IsNullOrEmpty(baseEntity.DataInsUsr)) baseEntity.DataInsUsr = currentUser; + if (baseEntity.RowVersion <= 0) + baseEntity.RowVersion = 1; } return base.Insert(entity); } public override bool Update(T entity) { + Expression>? rowVersionWhere = null; + if (entity is BaseEntity baseEntity) { var currentUser = GetCurrentUser(); @@ -61,6 +69,14 @@ namespace EOM.TSHotelManagement.Data baseEntity.DataChgDate = DateTime.Now; if (string.IsNullOrEmpty(baseEntity.DataChgUsr)) baseEntity.DataChgUsr = currentUser; + + // 更新接口必须携带行版本,缺失时视为并发校验失败。 + if (baseEntity.RowVersion <= 0) + return false; + + var currentRowVersion = baseEntity.RowVersion; + rowVersionWhere = BuildEqualsLambda(nameof(BaseEntity.RowVersion), currentRowVersion); + baseEntity.RowVersion = currentRowVersion + 1; } var primaryKeys = base.Context.EntityMaintenance.GetEntityInfo().Columns @@ -68,11 +84,17 @@ namespace EOM.TSHotelManagement.Data .Select(it => it.PropertyName) .ToList(); + var updateable = base.Context.Updateable(entity) + .IgnoreColumns(true, false); + + if (rowVersionWhere != null) + { + updateable = updateable.Where(rowVersionWhere); + } + if (primaryKeys.Count <= 1) { - return base.Context.Updateable(entity) - .IgnoreColumns(true, false) - .ExecuteCommand() > 0; + return updateable.ExecuteCommand() > 0; } var idProperty = entity.GetType().GetProperty("Id"); @@ -83,41 +105,25 @@ namespace EOM.TSHotelManagement.Data if (idValue == 0) { var otherPrimaryKeys = primaryKeys.Where(pk => pk != "Id").ToList(); + var primaryKeyWhere = BuildPrimaryKeyWhereExpression(entity, otherPrimaryKeys); - var parameter = Expression.Parameter(typeof(T), "it"); - Expression whereExpression = null; - - foreach (var key in otherPrimaryKeys) - { - var property = Expression.Property(parameter, key); - var value = entity.GetType().GetProperty(key).GetValue(entity); - var constant = Expression.Constant(value); - var equal = Expression.Equal(property, constant); - - whereExpression = whereExpression == null - ? equal - : Expression.AndAlso(whereExpression, equal); - } - - if (whereExpression != null) + if (primaryKeyWhere != null) { - var lambda = Expression.Lambda>(whereExpression, parameter); - - return base.Context.Updateable(entity) - .Where(lambda) - .IgnoreColumns(true, false) - .ExecuteCommand() > 0; + return updateable.Where(primaryKeyWhere).ExecuteCommand() > 0; } } } - return base.Context.Updateable(entity) - .IgnoreColumns(true, false) - .ExecuteCommand() > 0; + return updateable.ExecuteCommand() > 0; } public override bool UpdateRange(List updateObjs) { + if (updateObjs == null || updateObjs.Count == 0) + { + return false; + } + foreach (var entity in updateObjs) { if (entity is BaseEntity baseEntity) @@ -130,6 +136,25 @@ namespace EOM.TSHotelManagement.Data } } + // For BaseEntity types, route through single-entity Update in a transaction + // so optimistic-lock checks are consistently enforced. + if (typeof(BaseEntity).IsAssignableFrom(typeof(T))) + { + var tranResult = base.Context.Ado.UseTran(() => + { + foreach (var entity in updateObjs) + { + if (!Update(entity)) + { + _log.LogWarning("Optimistic concurrency check failed for entity of type {EntityType}. Update aborted.", typeof(T).Name); + throw new InvalidOperationException("Optimistic concurrency check failed."); + } + } + }); + + return tranResult.IsSuccess; + } + return base.Context.Updateable(updateObjs) .IgnoreColumns(ignoreAllNullColumns: true) .ExecuteCommand() > 0; @@ -242,5 +267,61 @@ namespace EOM.TSHotelManagement.Data return totalAffected > 0; } + + private static Expression> BuildEqualsLambda(string propertyName, object propertyValue) + { + var parameter = Expression.Parameter(typeof(T), "it"); + var property = Expression.Property(parameter, propertyName); + + object? normalizedValue = propertyValue; + var targetType = Nullable.GetUnderlyingType(property.Type) ?? property.Type; + if (normalizedValue != null && normalizedValue.GetType() != targetType) + { + normalizedValue = Convert.ChangeType(normalizedValue, targetType); + } + + var constant = Expression.Constant(normalizedValue, property.Type); + var equal = Expression.Equal(property, constant); + return Expression.Lambda>(equal, parameter); + } + + private static Expression>? BuildPrimaryKeyWhereExpression(T entity, List primaryKeys) + { + if (entity == null || primaryKeys == null || primaryKeys.Count == 0) + { + return null; + } + + var parameter = Expression.Parameter(typeof(T), "it"); + Expression? whereExpression = null; + + foreach (var key in primaryKeys) + { + var value = entity.GetType().GetProperty(key)?.GetValue(entity); + if (value == null) + { + continue; + } + + var property = Expression.Property(parameter, key); + object normalizedValue = value; + var targetType = Nullable.GetUnderlyingType(property.Type) ?? property.Type; + if (normalizedValue.GetType() != targetType) + { + normalizedValue = Convert.ChangeType(normalizedValue, targetType); + } + + var constant = Expression.Constant(normalizedValue, property.Type); + var equal = Expression.Equal(property, constant); + + whereExpression = whereExpression == null + ? equal + : Expression.AndAlso(whereExpression, equal); + } + + return whereExpression == null + ? null + : Expression.Lambda>(whereExpression, parameter); + } } } diff --git a/EOM.TSHotelManagement.Domain/BaseEntity.cs b/EOM.TSHotelManagement.Domain/BaseEntity.cs index 221afec415589503cb9d0e988ab5193d0d551651..68954f79cd610901814f60bbe159981e32d5b17b 100644 --- a/EOM.TSHotelManagement.Domain/BaseEntity.cs +++ b/EOM.TSHotelManagement.Domain/BaseEntity.cs @@ -30,6 +30,11 @@ namespace EOM.TSHotelManagement.Domain [SqlSugar.SugarColumn(ColumnName = "datachg_date", IsOnlyIgnoreInsert = true, IsNullable = true)] public DateTime? DataChgDate { get; set; } /// + /// 行版本(乐观锁) + /// + [SqlSugar.SugarColumn(ColumnName = "row_version", IsNullable = false, DefaultValue = "1")] + public long RowVersion { get; set; } = 1; + /// /// Token /// [SqlSugar.SugarColumn(IsIgnore = true)] diff --git a/EOM.TSHotelManagement.Service/Application/NavBar/NavBarService.cs b/EOM.TSHotelManagement.Service/Application/NavBar/NavBarService.cs index fe14d8f58bd2d4cb4fe794044262f51834236866..e3f4b6f02fc87e734d9c4dade560e99b361ec225 100644 --- a/EOM.TSHotelManagement.Service/Application/NavBar/NavBarService.cs +++ b/EOM.TSHotelManagement.Service/Application/NavBar/NavBarService.cs @@ -97,14 +97,11 @@ namespace EOM.TSHotelManagement.Service navBar.NavigationBarImage = input.NavigationBarImage; navBar.NavigationBarEvent = input.NavigationBarEvent; navBar.MarginLeft = input.MarginLeft; + navBar.RowVersion = input.RowVersion ?? 0; var result = navBarRepository.Update(navBar); if (!result) { - return new BaseResponse - { - Code = BusinessStatusCode.InternalServerError, - Message = "更新失败" - }; + return BaseResponseFactory.ConcurrencyConflict(); } return new BaseResponse { diff --git a/EOM.TSHotelManagement.Service/Business/Asset/AssetService.cs b/EOM.TSHotelManagement.Service/Business/Asset/AssetService.cs index 3e211031f3aca27f31cce527761be38613f80de8..636ede335059c7f4a86f3d6572af1827cf1b4bf9 100644 --- a/EOM.TSHotelManagement.Service/Business/Asset/AssetService.cs +++ b/EOM.TSHotelManagement.Service/Business/Asset/AssetService.cs @@ -156,7 +156,7 @@ namespace EOM.TSHotelManagement.Service var result = assetRepository.Update(entity); if (!result) { - return new BaseResponse() { Message = LocalizationHelper.GetLocalizedString("update asset failed.", "资产更新失败"), Code = BusinessStatusCode.InternalServerError }; + return BaseResponseFactory.ConcurrencyConflict(); } } catch (Exception ex) diff --git a/EOM.TSHotelManagement.Service/Business/Customer/CustomerService.cs b/EOM.TSHotelManagement.Service/Business/Customer/CustomerService.cs index 7a7d57c6d342d450af16520d8a19f4661744fe0a..68fc2203d0bc73b3d61d645f70ec01f7cb188821 100644 --- a/EOM.TSHotelManagement.Service/Business/Customer/CustomerService.cs +++ b/EOM.TSHotelManagement.Service/Business/Customer/CustomerService.cs @@ -187,7 +187,7 @@ namespace EOM.TSHotelManagement.Service var result = custoRepository.Update(customer); if (!result) { - return new BaseResponse() { Message = LocalizationHelper.GetLocalizedString("Update Customer Failed", "客户信息更新失败"), Code = BusinessStatusCode.InternalServerError }; + return BaseResponseFactory.ConcurrencyConflict(); } } catch (Exception ex) @@ -263,11 +263,14 @@ namespace EOM.TSHotelManagement.Service { try { - if (!custoRepository.IsAny(a => a.CustomerNumber == updateCustomerInputDto.CustomerNumber)) + var customer = custoRepository.GetFirst(a => a.CustomerNumber == updateCustomerInputDto.CustomerNumber && a.IsDelete != 1); + if (customer == null) { return new BaseResponse() { Message = LocalizationHelper.GetLocalizedString("customer number does not exist.", "客户编号不存在"), Code = BusinessStatusCode.InternalServerError }; } - var result = custoRepository.Update(a => new Customer { CustomerType = updateCustomerInputDto.CustomerType }, a => a.CustomerNumber == updateCustomerInputDto.CustomerNumber); + customer.CustomerType = updateCustomerInputDto.CustomerType; + customer.RowVersion = updateCustomerInputDto.RowVersion ?? 0; + var result = custoRepository.Update(customer); if (result) { @@ -275,7 +278,7 @@ namespace EOM.TSHotelManagement.Service } else { - return new BaseResponse(BusinessStatusCode.InternalServerError, LocalizationHelper.GetLocalizedString("Update Customer Type Failed", "客户类型更新失败")); + return BaseResponseFactory.ConcurrencyConflict(); } } catch (Exception ex) diff --git a/EOM.TSHotelManagement.Service/Business/EnergyManagement/EnergyManagementService.cs b/EOM.TSHotelManagement.Service/Business/EnergyManagement/EnergyManagementService.cs index a3a7c57a4a227b04a0222501f51dd7bda31d9535..b404337c5a850e61db618160e1481d07bc2eef77 100644 --- a/EOM.TSHotelManagement.Service/Business/EnergyManagement/EnergyManagementService.cs +++ b/EOM.TSHotelManagement.Service/Business/EnergyManagement/EnergyManagementService.cs @@ -127,7 +127,7 @@ namespace EOM.TSHotelManagement.Service } else { - return new BaseResponse(BusinessStatusCode.InternalServerError, LocalizationHelper.GetLocalizedString("Update Energy Management Failed", "水电费信息更新失败")); + return BaseResponseFactory.ConcurrencyConflict(); } } catch (Exception ex) diff --git a/EOM.TSHotelManagement.Service/Business/News/NewsService.cs b/EOM.TSHotelManagement.Service/Business/News/NewsService.cs index 4968d003e1d9d84a33344ecf4d394e4349d484bf..18e9a5a96b55a4b8df18d6a59723fac28b761c85 100644 --- a/EOM.TSHotelManagement.Service/Business/News/NewsService.cs +++ b/EOM.TSHotelManagement.Service/Business/News/NewsService.cs @@ -196,16 +196,13 @@ namespace EOM.TSHotelManagement.Service news.NewsLink = updateNewsInputDto.NewsLink; news.NewsDate = updateNewsInputDto.NewsDate; news.NewsImage = updateNewsInputDto.NewsImage; + news.RowVersion = updateNewsInputDto.RowVersion ?? 0; try { var result = _newsRepository.Update(news); if (!result) { - return new BaseResponse - { - Code = BusinessStatusCode.InternalServerError, - Message = "新闻更新失败" - }; + return BaseResponseFactory.ConcurrencyConflict(); } } catch (Exception ex) diff --git a/EOM.TSHotelManagement.Service/Business/PromotionContent/PromotionContentService.cs b/EOM.TSHotelManagement.Service/Business/PromotionContent/PromotionContentService.cs index a59475027d763796f69fe7214d3a0cd675a8a6c2..af380947a2d030a21ba49f1ccf3d4452c113055f 100644 --- a/EOM.TSHotelManagement.Service/Business/PromotionContent/PromotionContentService.cs +++ b/EOM.TSHotelManagement.Service/Business/PromotionContent/PromotionContentService.cs @@ -223,7 +223,11 @@ namespace EOM.TSHotelManagement.Service try { var entity = EntityMapper.Map(updatePromotionContentInputDto); - fontsRepository.Update(entity); + var result = fontsRepository.Update(entity); + if (!result) + { + return BaseResponseFactory.ConcurrencyConflict(); + } } catch (Exception ex) { diff --git a/EOM.TSHotelManagement.Service/Business/Reser/ReserService.cs b/EOM.TSHotelManagement.Service/Business/Reser/ReserService.cs index 8ac20da4457c05d917edad55145e6f1fb1817495..25edadae227ceffaac76231733048cf1fa89851d 100644 --- a/EOM.TSHotelManagement.Service/Business/Reser/ReserService.cs +++ b/EOM.TSHotelManagement.Service/Business/Reser/ReserService.cs @@ -238,7 +238,13 @@ namespace EOM.TSHotelManagement.Service a.RoomStateId = (int)RoomState.Vacant; return a; }).ToList(); - roomRepository.UpdateRange(rooms); + + var roomUpdateResult = roomRepository.UpdateRange(rooms); + if (!roomUpdateResult) + { + return BaseResponseFactory.ConcurrencyConflict(); + } + scope.Complete(); return new BaseResponse(BusinessStatusCode.Success, LocalizationHelper.GetLocalizedString("Delete Reser Success", "预约信息删除成功")); } @@ -321,16 +327,28 @@ namespace EOM.TSHotelManagement.Service // 恢复预约并更新房间状态 var entity = EntityMapper.Map(reser); - reserRepository.Update(entity); + var reserUpdateResult = reserRepository.Update(entity); + if (!reserUpdateResult) + { + return BaseResponseFactory.ConcurrencyConflict(); + } room.RoomStateId = (int)RoomState.Reserved; - roomRepository.Update(room); + var roomUpdateResult = roomRepository.Update(room); + if (!roomUpdateResult) + { + return BaseResponseFactory.ConcurrencyConflict(); + } } else { // 普通更新逻辑 var entity = EntityMapper.Map(reser); - reserRepository.Update(entity); + var reserUpdateResult = reserRepository.Update(entity); + if (!reserUpdateResult) + { + return BaseResponseFactory.ConcurrencyConflict(); + } } scope.Complete(); diff --git a/EOM.TSHotelManagement.Service/Business/Room/RoomService.cs b/EOM.TSHotelManagement.Service/Business/Room/RoomService.cs index b3a2e98f2b9583b0141e88d84eefea7376efa64a..c7c1f4ef4aeef3bf4492fe3c94c2b216a2bdb11b 100644 --- a/EOM.TSHotelManagement.Service/Business/Room/RoomService.cs +++ b/EOM.TSHotelManagement.Service/Business/Room/RoomService.cs @@ -339,7 +339,12 @@ namespace EOM.TSHotelManagement.Service room.LastCheckInTime = r.LastCheckInTime; room.DataChgDate = r.DataChgDate; room.DataChgUsr = r.DataChgUsr; - roomRepository.Update(room); + room.RowVersion = r.RowVersion ?? 0; + var updateResult = roomRepository.Update(room); + if (!updateResult) + { + return BaseResponseFactory.ConcurrencyConflict(); + } } catch (Exception ex) { @@ -362,7 +367,12 @@ namespace EOM.TSHotelManagement.Service room.RoomStateId = r.RoomStateId; room.DataChgDate = r.DataChgDate; room.DataChgUsr = r.DataChgUsr; - roomRepository.Update(room); + room.RowVersion = r.RowVersion ?? 0; + var updateResult = roomRepository.Update(room); + if (!updateResult) + { + return BaseResponseFactory.ConcurrencyConflict(); + } } catch (Exception ex) { @@ -512,7 +522,12 @@ namespace EOM.TSHotelManagement.Service { var room = roomRepository.GetFirst(a => a.RoomNumber == updateRoomInputDto.RoomNumber); room.RoomStateId = updateRoomInputDto.RoomStateId; - roomRepository.Update(room); + room.RowVersion = updateRoomInputDto.RowVersion ?? 0; + var updateResult = roomRepository.Update(room); + if (!updateResult) + { + return BaseResponseFactory.ConcurrencyConflict(); + } } catch (Exception ex) { @@ -562,7 +577,11 @@ namespace EOM.TSHotelManagement.Service return new BaseResponse { Message = LocalizationHelper.GetLocalizedString("This room does not exist.", "房间不存在。"), Code = BusinessStatusCode.InternalServerError }; var entity = EntityMapper.Map(rn); - roomRepository.Update(entity); + var updateResult = roomRepository.Update(entity); + if (!updateResult) + { + return BaseResponseFactory.ConcurrencyConflict(); + } } catch (Exception ex) { @@ -691,14 +710,22 @@ namespace EOM.TSHotelManagement.Service targetRoom.CustomerNumber = originalRoom.CustomerNumber; targetRoom.RoomStateId = (int)RoomState.Occupied; targetRoom.LastCheckInTime = DateOnly.FromDateTime(DateTime.Now); - roomRepository.Update(targetRoom); + var targetRoomUpdateResult = roomRepository.Update(targetRoom); + if (!targetRoomUpdateResult) + { + return BaseResponseFactory.ConcurrencyConflict(); + } //更新原房间状态 originalRoom.CustomerNumber = string.Empty; originalRoom.RoomStateId = (int)RoomState.Dirty; originalRoom.LastCheckInTime = DateOnly.MinValue; originalRoom.LastCheckOutTime = DateOnly.MinValue; - roomRepository.Update(originalRoom); + var originalRoomUpdateResult = roomRepository.Update(originalRoom); + if (!originalRoomUpdateResult) + { + return BaseResponseFactory.ConcurrencyConflict(); + } //转移原房间消费记录 if (originalSpendNumbers.Count > 0) @@ -710,9 +737,14 @@ namespace EOM.TSHotelManagement.Service { spend.SpendNumber = spend.SpendNumber; spend.RoomNumber = transferRoomDto.TargetRoomNumber; + spends.Add(spend); } - spendRepository.UpdateRange(spends); + var spendTransferResult = spendRepository.UpdateRange(spends); + if (!spendTransferResult) + { + return BaseResponseFactory.ConcurrencyConflict(); + } } //添加旧房间消费记录 @@ -765,7 +797,11 @@ namespace EOM.TSHotelManagement.Service room.CustomerNumber = string.Empty; room.LastCheckOutTime = DateOnly.FromDateTime(DateTime.Now); room.RoomStateId = (int)RoomState.Dirty; - roomRepository.Update(room); + var roomUpdateResult = roomRepository.Update(room); + if (!roomUpdateResult) + { + return BaseResponseFactory.ConcurrencyConflict(); + } //添加能源使用记录 var energy = new EnergyManagement @@ -795,7 +831,11 @@ namespace EOM.TSHotelManagement.Service spends.Add(spend); } - spendRepository.UpdateRange(spends); + var settleSpendResult = spendRepository.UpdateRange(spends); + if (!settleSpendResult) + { + return BaseResponseFactory.ConcurrencyConflict(); + } } scope.Complete(); @@ -849,7 +889,7 @@ namespace EOM.TSHotelManagement.Service if (!roomUpdateResult) { - return new BaseResponse { Message = LocalizationHelper.GetLocalizedString("Failed to update room.", "更新房间失败。"), Code = BusinessStatusCode.InternalServerError }; + return BaseResponseFactory.ConcurrencyConflict(); } var reser = reserRepository.GetFirst(a => a.ReservationId == checkinRoomByReservationDto.ReservationId && a.IsDelete != 1); @@ -858,7 +898,7 @@ namespace EOM.TSHotelManagement.Service if (!reserUpdateResult) { - return new BaseResponse { Message = LocalizationHelper.GetLocalizedString("Failed to update reservation.", "更新预约失败。"), Code = BusinessStatusCode.InternalServerError }; + return BaseResponseFactory.ConcurrencyConflict(); } scope.Complete(); diff --git a/EOM.TSHotelManagement.Service/Business/Room/RoomTypeService.cs b/EOM.TSHotelManagement.Service/Business/Room/RoomTypeService.cs index bd80d780e1839ca9c52ff5df1fa60a772ad92768..bf099227d4860cd9ea3f6ffa6bf8c4ccb144d318 100644 --- a/EOM.TSHotelManagement.Service/Business/Room/RoomTypeService.cs +++ b/EOM.TSHotelManagement.Service/Business/Room/RoomTypeService.cs @@ -190,7 +190,7 @@ namespace EOM.TSHotelManagement.Service { try { - roomTypeRepository.Update(new RoomType + var result = roomTypeRepository.Update(new RoomType { RoomTypeId = roomType.RoomTypeId, Id = roomType.Id ?? 0, @@ -199,8 +199,13 @@ namespace EOM.TSHotelManagement.Service RoomDeposit = roomType.RoomDeposit, IsDelete = roomType.IsDelete, DataChgUsr = roomType.DataChgUsr, - DataChgDate = roomType.DataChgDate + DataChgDate = roomType.DataChgDate, + RowVersion = roomType.RowVersion ?? 0 }); + if (!result) + { + return BaseResponseFactory.ConcurrencyConflict(); + } } catch (Exception ex) { diff --git a/EOM.TSHotelManagement.Service/Business/Sellthing/SellService.cs b/EOM.TSHotelManagement.Service/Business/Sellthing/SellService.cs index 69a7b072eb7a02eef947768955762fb939f266a8..dcc5e44450ee00b26d7c011e15432b9c131c7cf2 100644 --- a/EOM.TSHotelManagement.Service/Business/Sellthing/SellService.cs +++ b/EOM.TSHotelManagement.Service/Business/Sellthing/SellService.cs @@ -166,12 +166,21 @@ namespace EOM.TSHotelManagement.Service try { var product = sellThingRepository.GetFirst(a => a.Id == sellThing.Id); + if (product == null) + { + return new BaseResponse(BusinessStatusCode.NotFound, LocalizationHelper.GetLocalizedString("Goods Information Not Found", "商品信息未找到")); + } product.ProductName = sellThing.ProductName; product.ProductPrice = sellThing.ProductPrice; product.Stock = sellThing.Stock; product.Specification = sellThing.Specification; product.IsDelete = sellThing.IsDelete; - sellThingRepository.Update(product); + product.RowVersion = sellThing.RowVersion ?? 0; + var result = sellThingRepository.Update(product); + if (!result) + { + return BaseResponseFactory.ConcurrencyConflict(); + } } catch (Exception ex) { diff --git a/EOM.TSHotelManagement.Service/Business/Spend/SpendService.cs b/EOM.TSHotelManagement.Service/Business/Spend/SpendService.cs index 2c107495565ac5526c76e9c15bf4a87ddeefee8c..1fb1e461ea618bd5f4d50b8197efcc113570d41d 100644 --- a/EOM.TSHotelManagement.Service/Business/Spend/SpendService.cs +++ b/EOM.TSHotelManagement.Service/Business/Spend/SpendService.cs @@ -283,8 +283,17 @@ namespace EOM.TSHotelManagement.Service try { var existingSpend = spendRepository.GetFirst(a => a.SpendNumber == updateSpendInputDto.SpendNumber && a.IsDelete != 1); + if (existingSpend == null) + { + return new BaseResponse(BusinessStatusCode.NotFound, LocalizationHelper.GetLocalizedString("Spend record not found", "消费记录不存在")); + } existingSpend.IsDelete = 1; - spendRepository.Update(existingSpend); + existingSpend.RowVersion = updateSpendInputDto.RowVersion ?? 0; + var updateResult = spendRepository.Update(existingSpend); + if (!updateResult) + { + return BaseResponseFactory.ConcurrencyConflict(); + } } catch (Exception ex) { @@ -342,7 +351,7 @@ namespace EOM.TSHotelManagement.Service var result = spendRepository.Update(existingSpend); if (!result) { - return new BaseResponse() { Message = "更新消费记录失败", Code = BusinessStatusCode.InternalServerError }; + return BaseResponseFactory.ConcurrencyConflict(); } } else @@ -376,7 +385,7 @@ namespace EOM.TSHotelManagement.Service var updateResult = sellThingRepository.Update(product); if (!updateResult) { - return new BaseResponse() { Message = "商品库存更新失败", Code = BusinessStatusCode.InternalServerError }; + return BaseResponseFactory.ConcurrencyConflict(); } var logContent = $"{addCustomerSpendInputDto.WorkerNo} 添加了消费记录: " + @@ -423,13 +432,22 @@ namespace EOM.TSHotelManagement.Service try { var dbSpend = spendRepository.GetFirst(a => a.SpendNumber == spend.SpendNumber && a.IsDelete != 1); + if (dbSpend == null) + { + return new BaseResponse(BusinessStatusCode.NotFound, LocalizationHelper.GetLocalizedString("Spend record not found", "消费记录不存在")); + } dbSpend.SettlementStatus = spend.SettlementStatus; dbSpend.RoomNumber = spend.RoomNumber; dbSpend.CustomerNumber = spend.CustomerNumber; dbSpend.ProductName = spend.ProductName; dbSpend.ConsumptionQuantity = spend.ConsumptionQuantity; dbSpend.ConsumptionAmount = spend.ConsumptionAmount; - spendRepository.Update(dbSpend); + dbSpend.RowVersion = spend.RowVersion ?? 0; + var updateResult = spendRepository.Update(dbSpend); + if (!updateResult) + { + return BaseResponseFactory.ConcurrencyConflict(); + } } catch (Exception ex) { diff --git a/EOM.TSHotelManagement.Service/Employee/Photo/EmployeePhotoService.cs b/EOM.TSHotelManagement.Service/Employee/Photo/EmployeePhotoService.cs index 6eb1df1058e131763b371f3bda4e0eb5ccbfefa0..72b5c2b8c3effe875d773ab0cb1964e761e62726 100644 --- a/EOM.TSHotelManagement.Service/Employee/Photo/EmployeePhotoService.cs +++ b/EOM.TSHotelManagement.Service/Employee/Photo/EmployeePhotoService.cs @@ -130,7 +130,15 @@ namespace EOM.TSHotelManagement.Service { workerPicData = workerPicRepository.GetFirst(a => a.EmployeeId.Equals(createEmployeePhotoInputDto.EmployeeId)); workerPicData.PhotoPath = imageUrl; - workerPicRepository.Update(workerPicData); + var updateResult = workerPicRepository.Update(workerPicData); + if (!updateResult) + { + return new SingleOutputDto + { + Message = LocalizationHelper.GetLocalizedString("Data has been modified by another user. Please refresh and retry.", "数据已被其他用户修改,请刷新后重试。"), + Code = BusinessStatusCode.Conflict + }; + } } return new SingleOutputDto @@ -178,8 +186,17 @@ namespace EOM.TSHotelManagement.Service try { var workerPicData = workerPicRepository.GetFirst(a => a.EmployeeId.Equals(updateEmployeePhotoInputDto.EmployeeId)); + if (workerPicData == null) + { + return new BaseResponse(BusinessStatusCode.NotFound, LocalizationHelper.GetLocalizedString("Photo information not found", "照片信息不存在")); + } workerPicData.PhotoPath = updateEmployeePhotoInputDto.PhotoUrl; - workerPicRepository.Update(workerPicData); + workerPicData.RowVersion = updateEmployeePhotoInputDto.RowVersion ?? 0; + var updateResult = workerPicRepository.Update(workerPicData); + if (!updateResult) + { + return BaseResponseFactory.ConcurrencyConflict(); + } } catch (Exception ex) { diff --git a/EOM.TSHotelManagement.Service/SystemManagement/Base/BaseService.cs b/EOM.TSHotelManagement.Service/SystemManagement/Base/BaseService.cs index f609425bd75ec1ce2a91b173e77941423f943704..3959c66acf42f01aed3f2c1ddb9ae6320c87501e 100644 --- a/EOM.TSHotelManagement.Service/SystemManagement/Base/BaseService.cs +++ b/EOM.TSHotelManagement.Service/SystemManagement/Base/BaseService.cs @@ -360,7 +360,11 @@ namespace EOM.TSHotelManagement.Service try { var position = EntityMapper.Map(updatePositionInputDto); - positionRepository.Update(position); + var result = positionRepository.Update(position); + if (!result) + { + return BaseResponseFactory.ConcurrencyConflict(); + } return new BaseResponse(); } catch (Exception ex) @@ -499,7 +503,11 @@ namespace EOM.TSHotelManagement.Service try { var nation = EntityMapper.Map(updateNationInputDto); - nationRepository.Update(nation); + var result = nationRepository.Update(nation); + if (!result) + { + return BaseResponseFactory.ConcurrencyConflict(); + } return new BaseResponse(); } catch (Exception ex) @@ -635,7 +643,11 @@ namespace EOM.TSHotelManagement.Service try { var entity = EntityMapper.Map(education); - educationRepository.Update(entity); + var result = educationRepository.Update(entity); + if (!result) + { + return BaseResponseFactory.ConcurrencyConflict(); + } return new BaseResponse(); } catch (Exception ex) @@ -804,6 +816,10 @@ namespace EOM.TSHotelManagement.Service { var department = EntityMapper.Map(dept); var result = deptRepository.Update(department); + if (!result) + { + return BaseResponseFactory.ConcurrencyConflict(); + } return new BaseResponse(); } catch (Exception ex) @@ -958,7 +974,11 @@ namespace EOM.TSHotelManagement.Service try { var entity = EntityMapper.Map(custoType); - custoTypeRepository.Update(entity); + var result = custoTypeRepository.Update(entity); + if (!result) + { + return BaseResponseFactory.ConcurrencyConflict(); + } return new BaseResponse(); } catch (Exception ex) @@ -1125,6 +1145,10 @@ namespace EOM.TSHotelManagement.Service { var entity = EntityMapper.Map(portType); var result = passPortTypeRepository.Update(entity); + if (!result) + { + return BaseResponseFactory.ConcurrencyConflict(); + } return new BaseResponse(); } catch (Exception ex) @@ -1268,6 +1292,10 @@ namespace EOM.TSHotelManagement.Service { var entity = EntityMapper.Map(gBType); var result = goodbadTypeRepository.Update(entity); + if (!result) + { + return BaseResponseFactory.ConcurrencyConflict(); + } return new BaseResponse(); } catch (Exception ex) @@ -1410,7 +1438,11 @@ namespace EOM.TSHotelManagement.Service return new BaseResponse { Code = BusinessStatusCode.InternalServerError, Message = LocalizationHelper.GetLocalizedString("appointment notice number does not already.", "公告类型编号不存在") }; } var entity = EntityMapper.Map(updateAppointmentNoticeTypeInputDto); - appointmentNoticeTypeRepository.Update(entity); + var result = appointmentNoticeTypeRepository.Update(entity); + if (!result) + { + return BaseResponseFactory.ConcurrencyConflict(); + } return new BaseResponse { Code = BusinessStatusCode.Success, Message = LocalizationHelper.GetLocalizedString("update appointment notice successful.", "公告类型更新成功") }; } diff --git a/EOM.TSHotelManagement.Service/SystemManagement/Menu/MenuService.cs b/EOM.TSHotelManagement.Service/SystemManagement/Menu/MenuService.cs index b2416e6cb9b58e25956f94df36c60b8129302c5e..8eda14278ccc6656ac3f042a06d4e9ca07aaa1d4 100644 --- a/EOM.TSHotelManagement.Service/SystemManagement/Menu/MenuService.cs +++ b/EOM.TSHotelManagement.Service/SystemManagement/Menu/MenuService.cs @@ -273,7 +273,11 @@ namespace EOM.TSHotelManagement.Service { try { - menuRepository.Update(EntityMapper.Map(menu)); + var result = menuRepository.Update(EntityMapper.Map(menu)); + if (!result) + { + return BaseResponseFactory.ConcurrencyConflict(); + } return new BaseResponse(); } catch (Exception ex) diff --git a/EOM.TSHotelManagement.Service/SystemManagement/Notice/NoticeService.cs b/EOM.TSHotelManagement.Service/SystemManagement/Notice/NoticeService.cs index d71f2b77bc2a6bb943eb442f0780a083343fec05..ce33c798d53535c9cddcd018def2674e8a081933 100644 --- a/EOM.TSHotelManagement.Service/SystemManagement/Notice/NoticeService.cs +++ b/EOM.TSHotelManagement.Service/SystemManagement/Notice/NoticeService.cs @@ -174,6 +174,10 @@ namespace EOM.TSHotelManagement.Service { var entity = EntityMapper.Map(updateAppointmentNoticeInputDto); var result = noticeRepository.Update(entity); + if (!result) + { + return BaseResponseFactory.ConcurrencyConflict(); + } return new BaseResponse(); } catch (Exception ex) diff --git a/EOM.TSHotelManagement.Service/SystemManagement/Role/RoleAppService.cs b/EOM.TSHotelManagement.Service/SystemManagement/Role/RoleAppService.cs index 7d1174852ddeba511548910186d6aacd4bae9577..434900e5036f9c17d3d251c765412977e50d1f24 100644 --- a/EOM.TSHotelManagement.Service/SystemManagement/Role/RoleAppService.cs +++ b/EOM.TSHotelManagement.Service/SystemManagement/Role/RoleAppService.cs @@ -174,7 +174,11 @@ namespace EOM.TSHotelManagement.Service try { var entity = EntityMapper.Map(updateRoleInputDto); - roleRepository.Update(entity); + var result = roleRepository.Update(entity); + if (!result) + { + return BaseResponseFactory.ConcurrencyConflict(); + } return new BaseResponse(); } catch (Exception ex) diff --git a/EOM.TSHotelManagement.Service/SystemManagement/SupervisionStatistics/SupervisionStatisticsService.cs b/EOM.TSHotelManagement.Service/SystemManagement/SupervisionStatistics/SupervisionStatisticsService.cs index 7b545eafd550128eadc2329d7f3051f81c993123..2d79fb2dc8921d9550b894339b0404b21544e4c7 100644 --- a/EOM.TSHotelManagement.Service/SystemManagement/SupervisionStatistics/SupervisionStatisticsService.cs +++ b/EOM.TSHotelManagement.Service/SystemManagement/SupervisionStatistics/SupervisionStatisticsService.cs @@ -120,8 +120,13 @@ namespace EOM.TSHotelManagement.Service supervisionStatistics.SupervisionAdvice = checkInfo.SupervisionAdvice; supervisionStatistics.SupervisionStatistician = checkInfo.SupervisionStatistician; supervisionStatistics.SupervisionProgress = checkInfo.SupervisionProgress; + supervisionStatistics.RowVersion = checkInfo.RowVersion ?? 0; - checkInfoRepository.Update(supervisionStatistics); + var updateResult = checkInfoRepository.Update(supervisionStatistics); + if (!updateResult) + { + return BaseResponseFactory.ConcurrencyConflict(); + } } catch (Exception ex) { diff --git a/EOM.TSHotelManagement.Service/SystemManagement/VipRule/VipRuleAppService.cs b/EOM.TSHotelManagement.Service/SystemManagement/VipRule/VipRuleAppService.cs index e50f0aa6bf7f519b547adec6031a722f11b4c39c..69bfcb20efa850584b1391286d794520d4118170 100644 --- a/EOM.TSHotelManagement.Service/SystemManagement/VipRule/VipRuleAppService.cs +++ b/EOM.TSHotelManagement.Service/SystemManagement/VipRule/VipRuleAppService.cs @@ -186,11 +186,20 @@ namespace EOM.TSHotelManagement.Service try { var dbVipRule = vipRuleRepository.GetFirst(a => a.Id == vipRule.Id); + if (dbVipRule == null) + { + return new BaseResponse(BusinessStatusCode.NotFound, LocalizationHelper.GetLocalizedString("Vip Rule Not Found", "会员规则未找到")); + } dbVipRule.RuleName = vipRule.RuleName; dbVipRule.RuleValue = vipRule.RuleValue; dbVipRule.VipLevelId = vipRule.VipLevelId; dbVipRule.IsDelete = vipRule.IsDelete; - vipRuleRepository.Update(dbVipRule); + dbVipRule.RowVersion = vipRule.RowVersion ?? 0; + var updateResult = vipRuleRepository.Update(dbVipRule); + if (!updateResult) + { + return BaseResponseFactory.ConcurrencyConflict(); + } } catch (Exception ex) { diff --git a/README.en.md b/README.en.md index 1f8b43f874a00d47692807b1ea75497b6784a283..84bc2da09a86b8d67011ab32687d69a7843df847 100644 --- a/README.en.md +++ b/README.en.md @@ -132,32 +132,37 @@ This project utilises the SqlSugar framework to support one-click database and t ### Docker Deployment -The project provides a Dockerfile (alternatively, images can be rapidly built via the `build.ps1` script, provided WSL 2.0 and Hyper-V are enabled locally and Docker Desktop is installed), supporting Docker containerised deployment. The API listens on port 8080 by default. +The project provides a Dockerfile (alternatively, images can be rapidly built via the `build.ps1` script, provided WSL 2.0 and Hyper-V are enabled locally and Docker Desktop is installed), supporting Docker containerised deployment. The API listens on port 8080 by default. +To avoid manually maintaining a very long `docker run` command, this repo now includes `docker-compose.yml` and `.env.example`. + +```bash +# 1) Prepare env file +cp .env.example .env + +# Windows PowerShell: +# Copy-Item .env.example .env + +# 2) Edit .env (database connection, JWT key, mail settings, etc.) + +# 3) Start service +docker compose up -d + +# 4) View logs +docker compose logs -f tshotel-api + +# 5) Stop and remove container +docker compose down +``` + +If you still prefer `docker run`, you can shorten it with `--env-file`: ```bash -# Please modify environment variable parameters according to your actual setup docker run -d \ --name tshotel-api \ - --health-cmd="curl -f http://localhost:8080/health || exit 1" \ - --health-interval=30s \ - --health-timeout=10s \ - --health-retries=3 \ + --env-file .env \ -v /app/config:/app/config \ -v /app/keys:/app/keys \ -p 63001:8080 \ - -e ASPNETCORE_ENVIRONMENT=docker \ - -e DefaultDatabase=MariaDB \ - -e MariaDBConnectStr="Server=your_db_host;Database=tshoteldb;User=tshoteldb;Password=your_password;" \ - -e InitializeDatabase=true \ - -e Jwt__Key=your_super_secret_key \ - -e Jwt__ExpiryMinutes=20 \ - -e Mail__Enabled=true \ - -e Mail__Host=smtp.example.com \ - -e Mail__UserName=admin@example.com \ - -e Mail__Password=your_email_password \ - -e Mail__Port=465 \ - -e AllowedOrigins__0=http://localhost:8080 \ - -e AllowedOrigins__1=https://www.yourdomain.com \ yjj6731/tshotel-management-system-api:latest ``` diff --git a/README.md b/README.md index 0f034ff2e518e5378eef451b5395c6b96b00eb95..3b1322897bdd80859c29c8d8682a1d802ae5989c 100644 --- a/README.md +++ b/README.md @@ -132,32 +132,37 @@ EOM.TSHotelManagement.Web ### Docker 部署 -项目提供了 Dockerfile(亦可通过build.ps1文件快速构建镜像,前提需确保本地启用WSL2.0以及Hyper-V和安装Docker Desktop),支持 Docker 容器化部署。API 默认监听 8080 端口。 +项目提供了 Dockerfile(亦可通过build.ps1文件快速构建镜像,前提需确保本地启用WSL2.0以及Hyper-V和安装Docker Desktop),支持 Docker 容器化部署。API 默认监听 8080 端口。 +为了避免手写超长 `docker run` 命令,仓库已提供 `docker-compose.yml` + `.env.example`。 + +```bash +# 1) 准备环境变量文件 +cp .env.example .env + +# Windows PowerShell 可用: +# Copy-Item .env.example .env + +# 2) 按需修改 .env(数据库连接、JWT 密钥、邮箱等) + +# 3) 启动 +docker compose up -d + +# 4) 查看日志 +docker compose logs -f tshotel-api + +# 5) 停止并移除容器 +docker compose down +``` + +如果你仍想使用 `docker run`,也可以改成 `--env-file` 方式,命令会短很多: ```bash -# 请根据实际情况修改环境变量参数 docker run -d \ --name tshotel-api \ - --health-cmd="curl -f http://localhost:8080/health || exit 1" \ - --health-interval=30s \ - --health-timeout=10s \ - --health-retries=3 \ + --env-file .env \ -v /app/config:/app/config \ -v /app/keys:/app/keys \ -p 63001:8080 \ - -e ASPNETCORE_ENVIRONMENT=docker \ - -e DefaultDatabase=MariaDB \ - -e MariaDBConnectStr="Server=your_db_host;Database=tshoteldb;User=tshoteldb;Password=your_password;" \ - -e InitializeDatabase=true \ - -e Jwt__Key=your_super_secret_key \ - -e Jwt__ExpiryMinutes=20 \ - -e Mail__Enabled=true \ - -e Mail__Host=smtp.example.com \ - -e Mail__UserName=admin@example.com \ - -e Mail__Password=your_email_password \ - -e Mail__Port=465 \ - -e AllowedOrigins__0=http://localhost:8080 \ - -e AllowedOrigins__1=https://www.yourdomain.com \ yjj6731/tshotel-management-system-api:latest ``` diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000000000000000000000000000000000000..004c1b6df6a5a3629530116b8a01cf861c90434a --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,17 @@ +services: + tshotel-api: + image: ${TSHOTEL_IMAGE:-yjj6731/tshotel-management-system-api:latest} + container_name: ${CONTAINER_NAME:-tshotel-api} + restart: unless-stopped + ports: + - "${API_HOST_PORT:-63001}:8080" + volumes: + - "${APP_CONFIG_DIR:-./docker-data/config}:/app/config" + - "${APP_KEYS_DIR:-./docker-data/keys}:/app/keys" + healthcheck: + test: ["CMD", "curl", "-f", "http://localhost:8080/health", "||", "exit", "1"] + interval: 30s + timeout: 10s + retries: 3 + env_file: + - .env diff --git "a/\346\225\260\346\215\256\345\272\223\350\204\232\346\234\254/MariaDB\347\211\210\346\234\254/MDB_patch_add_row_version.sql" "b/\346\225\260\346\215\256\345\272\223\350\204\232\346\234\254/MariaDB\347\211\210\346\234\254/MDB_patch_add_row_version.sql" new file mode 100644 index 0000000000000000000000000000000000000000..71dcc22713264cb3235b38e6c58577f2518813d1 --- /dev/null +++ "b/\346\225\260\346\215\256\345\272\223\350\204\232\346\234\254/MariaDB\347\211\210\346\234\254/MDB_patch_add_row_version.sql" @@ -0,0 +1,26 @@ +SET @schema_name = DATABASE(); +SET SESSION group_concat_max_len = 1024000; + +SELECT GROUP_CONCAT( + CONCAT( + 'ALTER TABLE `', c.table_name, '` ', + 'ADD COLUMN `row_version` BIGINT NOT NULL DEFAULT 1 COMMENT ''Row version (optimistic lock)'';' + ) + SEPARATOR ' ' + ) +INTO @ddl_sql +FROM information_schema.columns c +WHERE c.table_schema = @schema_name + AND c.column_name = 'delete_mk' + AND NOT EXISTS ( + SELECT 1 + FROM information_schema.columns x + WHERE x.table_schema = c.table_schema + AND x.table_name = c.table_name + AND x.column_name = 'row_version' + ); + +SET @ddl_sql = IFNULL(@ddl_sql, 'SELECT 1;'); +PREPARE stmt FROM @ddl_sql; +EXECUTE stmt; +DEALLOCATE PREPARE stmt; diff --git "a/\346\225\260\346\215\256\345\272\223\350\204\232\346\234\254/MySQL\347\211\210\346\234\254/MDB_patch_add_row_version.sql" "b/\346\225\260\346\215\256\345\272\223\350\204\232\346\234\254/MySQL\347\211\210\346\234\254/MDB_patch_add_row_version.sql" new file mode 100644 index 0000000000000000000000000000000000000000..71dcc22713264cb3235b38e6c58577f2518813d1 --- /dev/null +++ "b/\346\225\260\346\215\256\345\272\223\350\204\232\346\234\254/MySQL\347\211\210\346\234\254/MDB_patch_add_row_version.sql" @@ -0,0 +1,26 @@ +SET @schema_name = DATABASE(); +SET SESSION group_concat_max_len = 1024000; + +SELECT GROUP_CONCAT( + CONCAT( + 'ALTER TABLE `', c.table_name, '` ', + 'ADD COLUMN `row_version` BIGINT NOT NULL DEFAULT 1 COMMENT ''Row version (optimistic lock)'';' + ) + SEPARATOR ' ' + ) +INTO @ddl_sql +FROM information_schema.columns c +WHERE c.table_schema = @schema_name + AND c.column_name = 'delete_mk' + AND NOT EXISTS ( + SELECT 1 + FROM information_schema.columns x + WHERE x.table_schema = c.table_schema + AND x.table_name = c.table_name + AND x.column_name = 'row_version' + ); + +SET @ddl_sql = IFNULL(@ddl_sql, 'SELECT 1;'); +PREPARE stmt FROM @ddl_sql; +EXECUTE stmt; +DEALLOCATE PREPARE stmt; diff --git "a/\346\225\260\346\215\256\345\272\223\350\204\232\346\234\254/PostgreSQL\347\211\210\346\234\254/PGDB_patch_add_row_version.sql" "b/\346\225\260\346\215\256\345\272\223\350\204\232\346\234\254/PostgreSQL\347\211\210\346\234\254/PGDB_patch_add_row_version.sql" new file mode 100644 index 0000000000000000000000000000000000000000..e9f2076744bbc62b835e6e63c9d1ac7201e130d9 --- /dev/null +++ "b/\346\225\260\346\215\256\345\272\223\350\204\232\346\234\254/PostgreSQL\347\211\210\346\234\254/PGDB_patch_add_row_version.sql" @@ -0,0 +1,23 @@ +DO +$$ +DECLARE + r record; +BEGIN + FOR r IN + SELECT c.table_name + FROM information_schema.columns c + WHERE c.table_schema = current_schema() + AND c.column_name = 'delete_mk' + AND NOT EXISTS ( + SELECT 1 + FROM information_schema.columns x + WHERE x.table_schema = c.table_schema + AND x.table_name = c.table_name + AND x.column_name = 'row_version' + ) + LOOP + EXECUTE format('ALTER TABLE %I ADD COLUMN row_version BIGINT NOT NULL DEFAULT 1;', r.table_name); + EXECUTE format('COMMENT ON COLUMN %I.row_version IS %L;', r.table_name, 'Row version (optimistic lock)'); + END LOOP; +END +$$;