diff --git a/.gitignore b/.gitignore
index f8648cd8..f4b69a9b 100644
--- a/.gitignore
+++ b/.gitignore
@@ -112,3 +112,4 @@ UpgradeLog*.XML
.idea
.vs
log
+*.db
diff --git a/Src/Dlhis.Middle/Controllers/OrdersController.cs b/Src/Dlhis.Middle/Controllers/OrdersController.cs
new file mode 100644
index 00000000..ae8a6a45
--- /dev/null
+++ b/Src/Dlhis.Middle/Controllers/OrdersController.cs
@@ -0,0 +1,38 @@
+using Dlhis.Middle.Request;
+using Dlhis.Middle.Response;
+using Dlhis.Middle.Services;
+using Kurisu.AspNetCore.DataAccess.SqlSugar.Aop;
+using Kurisu.AspNetCore.UnifyResultAndValidation;
+using Microsoft.AspNetCore.Authorization;
+using Microsoft.AspNetCore.Mvc;
+
+namespace Dlhis.Middle.Controllers;
+
+///
+/// 订单
+///
+[Authorize]
+[Route("api/[controller]")]
+[ApiController]
+public class OrdersController : ControllerBase
+{
+ private readonly IOrderService _orderService;
+
+ public OrdersController(IOrderService orderService)
+ {
+ _orderService = orderService;
+ }
+
+ ///
+ /// 消费
+ ///
+ ///
+ ///
+ [Authorize(Policy = "consumer")]
+ [HttpPost]
+ [IgnoreTenant]
+ public async Task> AddAsync(OrderAddRequest request)
+ {
+ return await _orderService.AddAsync(request);
+ }
+}
\ No newline at end of file
diff --git a/Src/Dlhis.Middle/Dlhis.Middle.csproj b/Src/Dlhis.Middle/Dlhis.Middle.csproj
new file mode 100644
index 00000000..1b0aef9f
--- /dev/null
+++ b/Src/Dlhis.Middle/Dlhis.Middle.csproj
@@ -0,0 +1,25 @@
+
+
+
+ net6.0
+ disable
+ enable
+ Linux
+ ..\..
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Src/Dlhis.Middle/Dockerfile b/Src/Dlhis.Middle/Dockerfile
new file mode 100644
index 00000000..ff94ef81
--- /dev/null
+++ b/Src/Dlhis.Middle/Dockerfile
@@ -0,0 +1,24 @@
+#See https://aka.ms/customizecontainer to learn how to customize your debug container and how Visual Studio uses this Dockerfile to build your images for faster debugging.
+
+FROM mcr.microsoft.com/dotnet/aspnet:6.0 AS base
+WORKDIR /app
+EXPOSE 80
+
+FROM mcr.microsoft.com/dotnet/sdk:6.0 AS build
+ARG BUILD_CONFIGURATION=Release
+WORKDIR /src
+COPY ["Src/Dlhis.Middle/Dlhis.Middle.csproj", "Src/Dlhis.Middle/"]
+COPY ["Src/Dlhis.Entity/Dlhis.Entity.csproj", "Src/Dlhis.Entity/"]
+RUN dotnet restore "./Src/Dlhis.Middle/./Dlhis.Middle.csproj"
+COPY . .
+WORKDIR "/src/Src/Dlhis.Middle"
+RUN dotnet build "./Dlhis.Middle.csproj" -c $BUILD_CONFIGURATION -o /app/build
+
+FROM build AS publish
+ARG BUILD_CONFIGURATION=Release
+RUN dotnet publish "./Dlhis.Middle.csproj" -c $BUILD_CONFIGURATION -o /app/publish /p:UseAppHost=false
+
+FROM base AS final
+WORKDIR /app
+COPY --from=publish /app/publish .
+ENTRYPOINT ["dotnet", "Dlhis.Middle.dll"]
\ No newline at end of file
diff --git a/Src/Dlhis.Middle/Entities/LocalBalanceRecordEntity.cs b/Src/Dlhis.Middle/Entities/LocalBalanceRecordEntity.cs
new file mode 100644
index 00000000..cbc4ecc5
--- /dev/null
+++ b/Src/Dlhis.Middle/Entities/LocalBalanceRecordEntity.cs
@@ -0,0 +1,9 @@
+using Dlhis.Entity.Canteen.Entities;
+
+namespace Dlhis.Middle.Entities
+{
+ public class LocalBalanceRecordEntity : BalanceRecordEntity
+ {
+ public new int Id { get; set; }
+ }
+}
\ No newline at end of file
diff --git a/Src/Dlhis.Middle/Entities/LocalBusinessPlaceEntity.cs b/Src/Dlhis.Middle/Entities/LocalBusinessPlaceEntity.cs
new file mode 100644
index 00000000..90b9a107
--- /dev/null
+++ b/Src/Dlhis.Middle/Entities/LocalBusinessPlaceEntity.cs
@@ -0,0 +1,9 @@
+using Dlhis.Entity.Basic.Entity;
+
+namespace Dlhis.Middle.Entities
+{
+ public class LocalBusinessPlaceEntity : BusinessPlaceEntity
+ {
+ public new int Id { get; set; }
+ }
+}
\ No newline at end of file
diff --git a/Src/Dlhis.Middle/Entities/LocalBusinessPlaceShiftRelationshipEntity.cs b/Src/Dlhis.Middle/Entities/LocalBusinessPlaceShiftRelationshipEntity.cs
new file mode 100644
index 00000000..bbebf3ea
--- /dev/null
+++ b/Src/Dlhis.Middle/Entities/LocalBusinessPlaceShiftRelationshipEntity.cs
@@ -0,0 +1,9 @@
+using Dlhis.Entity.Basic.Entity;
+
+namespace Dlhis.Middle.Entities
+{
+ public class LocalBusinessPlaceShiftRelationshipEntity : BusinessPlaceShiftRelationshipEntity
+ {
+ public new int Id { get; set; }
+ }
+}
\ No newline at end of file
diff --git a/Src/Dlhis.Middle/Entities/LocalBusinessShiftEntity.cs b/Src/Dlhis.Middle/Entities/LocalBusinessShiftEntity.cs
new file mode 100644
index 00000000..69c61e8d
--- /dev/null
+++ b/Src/Dlhis.Middle/Entities/LocalBusinessShiftEntity.cs
@@ -0,0 +1,9 @@
+using Dlhis.Entity.Basic.Entity;
+
+namespace Dlhis.Middle.Entities
+{
+ public class LocalBusinessShiftEntity : BusinessShiftEntity
+ {
+ public new int Id { get; set; }
+ }
+}
\ No newline at end of file
diff --git a/Src/Dlhis.Middle/Entities/LocalBusinessShiftMealRelationshipEntity.cs b/Src/Dlhis.Middle/Entities/LocalBusinessShiftMealRelationshipEntity.cs
new file mode 100644
index 00000000..eed8f859
--- /dev/null
+++ b/Src/Dlhis.Middle/Entities/LocalBusinessShiftMealRelationshipEntity.cs
@@ -0,0 +1,8 @@
+using Dlhis.Entity.Basic.Entity;
+
+namespace Dlhis.Middle.Entities;
+
+public class LocalBusinessShiftMealRelationshipEntity : BusinessShiftMealRelationshipEntity
+{
+ public new int Id { get; set; }
+}
\ No newline at end of file
diff --git a/Src/Dlhis.Middle/Entities/LocalCanteenSettingEntity.cs b/Src/Dlhis.Middle/Entities/LocalCanteenSettingEntity.cs
new file mode 100644
index 00000000..c1331ed6
--- /dev/null
+++ b/Src/Dlhis.Middle/Entities/LocalCanteenSettingEntity.cs
@@ -0,0 +1,9 @@
+using Dlhis.Entity.Basic.Entity;
+
+namespace Dlhis.Middle.Entities
+{
+ public class LocalCanteenSettingEntity : CanteenSettingEntity
+ {
+ public new int Id { get; set; }
+ }
+}
\ No newline at end of file
diff --git a/Src/Dlhis.Middle/Entities/LocalDepartmentEntity.cs b/Src/Dlhis.Middle/Entities/LocalDepartmentEntity.cs
new file mode 100644
index 00000000..6ccfe180
--- /dev/null
+++ b/Src/Dlhis.Middle/Entities/LocalDepartmentEntity.cs
@@ -0,0 +1,9 @@
+using Dlhis.Entity.Basic.Entity;
+
+namespace Dlhis.Middle.Entities
+{
+ public class LocalDepartmentEntity : DepartmentEntity
+ {
+ public new int Id { get; set; }
+ }
+}
\ No newline at end of file
diff --git a/Src/Dlhis.Middle/Entities/LocalDeviceEntity.cs b/Src/Dlhis.Middle/Entities/LocalDeviceEntity.cs
new file mode 100644
index 00000000..e019ecac
--- /dev/null
+++ b/Src/Dlhis.Middle/Entities/LocalDeviceEntity.cs
@@ -0,0 +1,9 @@
+using Dlhis.Entity.Basic.Entity;
+
+namespace Dlhis.Middle.Entities
+{
+ public class LocalDeviceEntity : DeviceEntity
+ {
+ public new int Id { get; set; }
+ }
+}
\ No newline at end of file
diff --git a/Src/Dlhis.Middle/Entities/LocalDeviceRegistryEntity.cs b/Src/Dlhis.Middle/Entities/LocalDeviceRegistryEntity.cs
new file mode 100644
index 00000000..9328dffd
--- /dev/null
+++ b/Src/Dlhis.Middle/Entities/LocalDeviceRegistryEntity.cs
@@ -0,0 +1,9 @@
+using Dlhis.Entity.Device.Entities;
+
+namespace Dlhis.Middle.Entities
+{
+ public class LocalDeviceRegistryEntity : DeviceRegistryEntity
+ {
+ public new int Id { get; set; }
+ }
+}
\ No newline at end of file
diff --git a/Src/Dlhis.Middle/Entities/LocalGainfoEntity.cs b/Src/Dlhis.Middle/Entities/LocalGainfoEntity.cs
new file mode 100644
index 00000000..74d160de
--- /dev/null
+++ b/Src/Dlhis.Middle/Entities/LocalGainfoEntity.cs
@@ -0,0 +1,9 @@
+using Dlhis.Entity.Basic.Entity;
+
+namespace Dlhis.Middle.Entities
+{
+ public class LocalGainfoEntity : GainfoEntity
+ {
+ public new int Id { get; set; }
+ }
+}
\ No newline at end of file
diff --git a/Src/Dlhis.Middle/Entities/LocalGainfoMealEntity.cs b/Src/Dlhis.Middle/Entities/LocalGainfoMealEntity.cs
new file mode 100644
index 00000000..e21572d2
--- /dev/null
+++ b/Src/Dlhis.Middle/Entities/LocalGainfoMealEntity.cs
@@ -0,0 +1,9 @@
+using Dlhis.Entity.Basic.Entity;
+
+namespace Dlhis.Middle.Entities
+{
+ public class LocalGainfoMealEntity : GainfoMealEntity
+ {
+ public new int Id { get; set; }
+ }
+}
\ No newline at end of file
diff --git a/Src/Dlhis.Middle/Entities/LocalOrderEntity.cs b/Src/Dlhis.Middle/Entities/LocalOrderEntity.cs
new file mode 100644
index 00000000..b61312ee
--- /dev/null
+++ b/Src/Dlhis.Middle/Entities/LocalOrderEntity.cs
@@ -0,0 +1,50 @@
+using Dlhis.Entity.Canteen.Enums;
+using Kurisu.AspNetCore.DataAccess.SqlSugar;
+using SqlSugar;
+
+namespace Dlhis.Middle.Entities;
+
+[SugarTable(TableName = "LocalOrders")]
+public class LocalOrderEntity : SugarBaseEntity
+{
+
+ public new int Id { get; set; }
+
+ ///
+ /// 是否离线数据
+ ///
+ public bool IsOffline { get; set; }
+
+ ///
+ /// 消费机器唯一信息
+ ///
+ public string DeviceUniqueNo { get; set; }
+
+ ///
+ /// 第三方订单号
+ ///
+ public string ThirdNo { get; set; }
+
+ ///
+ /// 档案号/QrCode/学工号
+ ///
+ public string UserUniqueNo { get; set; }
+
+ ///
+ /// 来源
+ ///
+ public BalanceRecordSourceEnumType Source { get; set; }
+
+ ///
+ /// 金额
+ ///
+ public decimal Amount { get; set; }
+
+ ///
+ /// 产生时间
+ ///
+ public DateTime CostDateTime { get; set; }
+
+
+ public bool IsSync { get; set; }
+}
\ No newline at end of file
diff --git a/Src/Dlhis.Middle/Packs/SqlSugarPack.cs b/Src/Dlhis.Middle/Packs/SqlSugarPack.cs
new file mode 100644
index 00000000..19e3ff32
--- /dev/null
+++ b/Src/Dlhis.Middle/Packs/SqlSugarPack.cs
@@ -0,0 +1,150 @@
+using Dlhis.Entity.Basic.Entity;
+using Dlhis.Entity.Canteen.Entities;
+using Dlhis.Entity.Device.Entities;
+using Dlhis.Middle.Entities;
+using Kurisu.AspNetCore.Authentication.Abstractions;
+using Kurisu.AspNetCore.DataAccess.Entity;
+using Kurisu.AspNetCore.DataAccess.SqlSugar.Attributes;
+using Kurisu.AspNetCore.DataAccess.SqlSugar.Extensions;
+using Kurisu.AspNetCore.DataAccess.SqlSugar.Options;
+using Kurisu.AspNetCore.DataAccess.SqlSugar.Services;
+using Kurisu.AspNetCore.Startup;
+using Microsoft.Extensions.DependencyInjection.Extensions;
+using Microsoft.Extensions.Options;
+using SqlSugar;
+
+namespace Dlhis.Middle.Packs;
+
+public class SqlSugarPack : BaseAppPack
+{
+ private const string _infoTemplate = "ExecuteCommand[{ms}] Timeout[{timeout}] \r\n {sql}";
+
+ public override void ConfigureServices(IServiceCollection services)
+ {
+ services.AddSqlSugar();
+ services.Replace(ServiceDescriptor.Scoped(typeof(ISqlSugarClient), sp =>
+ {
+ var options = sp.GetService>().Value;
+ var logger = sp.GetService>();
+ var currentUser = sp.GetService();
+ var sugarOptions = sp.GetService();
+
+ var configs = new List
+ {
+ new()
+ {
+ ConfigId = "db-main",
+
+ ConnectionString = options.DefaultConnectionString,
+ DbType = DbType.Sqlite,
+ InitKeyType = InitKeyType.Attribute,
+ IsAutoCloseConnection = true,
+
+ MoreSettings = new ConnMoreSettings
+ {
+ DisableNvarchar = true,
+ },
+ ConfigureExternalServices = new ConfigureExternalServices
+ {
+ EntityService = (c, p) =>
+ {
+ if (!p.IsPrimarykey && c.PropertyType.IsGenericType && c.PropertyType.GetGenericTypeDefinition() == typeof(Nullable<>)
+ || (!p.IsPrimarykey && p.PropertyInfo.PropertyType == typeof(string)))
+ {
+ p.IsNullable = true;
+ }
+ }
+ }
+ }
+ };
+
+ ISqlSugarClient db = new SqlSugarClient(configs);
+
+ db.Ado.CommandTimeOut = options.Timeout;
+
+ db.Aop.OnLogExecuted = (sql, parameters) =>
+ {
+ var ms = db.Ado.SqlExecutionTime.Milliseconds;
+
+ //警告慢sql
+ if (ms >= options.SlowSqlTime * 1000)
+ logger.LogWarning(message: _infoTemplate, ms, options.Timeout, sql);
+
+ if (options.EnableSqlLog)
+ {
+ sql = UtilMethods.GetSqlString(DbType.MySql, sql, parameters);
+ logger.LogInformation(message: _infoTemplate, ms, options.Timeout, sql);
+ }
+ };
+
+ //软删除
+ db.QueryFilter.AddTableFilter(x => x.IsDeleted == false);
+
+
+ //增删改
+ db.Aop.DataExecuting = (oldValue, model) =>
+ {
+ if (sugarOptions.IgnoreTenant == false //启用租户
+ && currentUser != null //用户信息存在
+ && model.PropertyName == nameof(ITenantId.TenantId) //当前为租户字段
+ && typeof(ITenantId).IsAssignableFrom(model.EntityValue.GetType())) //继承ITenantId
+ {
+ var tenant = currentUser.GetStringTenantId();
+ model.SetValue(tenant);
+ }
+
+ //处理人员和时间字段
+ switch (model.OperationType)
+ {
+ case DataFilterType.InsertByObject:
+ {
+ if (model.IsAnyAttribute())
+ {
+ model.SetValue(DateTime.Now);
+ }
+
+ if (currentUser != null && model.IsAnyAttribute())
+ {
+ if ((int)model.EntityColumnInfo.PropertyInfo.GetValue(model.EntityValue) == 0)
+ {
+ model.SetValue(currentUser.GetStringSubjectId());
+ }
+ }
+
+ break;
+ }
+ case DataFilterType.UpdateByObject:
+ {
+ if (model.IsAnyAttribute())
+ {
+ model.SetValue(DateTime.Now);
+ }
+
+ if (currentUser != null && model.IsAnyAttribute())
+ {
+ if ((int)model.EntityColumnInfo.PropertyInfo.GetValue(model.EntityValue) == 0)
+ {
+ model.SetValue(currentUser.GetStringSubjectId());
+ }
+ }
+
+ break;
+ }
+ }
+ };
+
+ return db;
+ }));
+ }
+
+ public override void Configure(IApplicationBuilder app, IWebHostEnvironment env)
+ {
+ using var scope = app.ApplicationServices.CreateScope();
+ var db = scope.ServiceProvider.GetService();
+
+ db.Client.DbMaintenance.CreateDatabase();
+ db.Client.CodeFirst.InitTables();
+ db.Client.CodeFirst.InitTables();
+ db.Client.CodeFirst.InitTables();
+ }
+}
\ No newline at end of file
diff --git a/Src/Dlhis.Middle/Program.cs b/Src/Dlhis.Middle/Program.cs
new file mode 100644
index 00000000..9d87298e
--- /dev/null
+++ b/Src/Dlhis.Middle/Program.cs
@@ -0,0 +1,64 @@
+using Dlhis.Entity.Basic.Entity;
+using Dlhis.Entity.Device.Entities;
+using Dlhis.Middle.Entities;
+using Dlhis.Middle.Services;
+using Dlhis.Middle.Services.Implements;
+using Kurisu.AspNetCore.Startup;
+using Microsoft.IdentityModel.Logging;
+
+namespace Dlhis.Middle;
+
+class Program
+{
+ static void Main(string[] args)
+ {
+ KurisuHost.Run(args);
+ }
+}
+
+public class Startup : DefaultStartup
+{
+ public Startup(IConfiguration configuration) : base(configuration)
+ {
+ IdentityModelEventSource.ShowPII = true;
+ }
+
+ public override void ConfigureServices(IServiceCollection services)
+ {
+ base.ConfigureServices(services);
+ services.AddHttpClient("face").ConfigurePrimaryHttpMessageHandler(() =>
+ {
+ var handler = new HttpClientHandler
+ {
+ ClientCertificateOptions = ClientCertificateOption.Manual,
+ ServerCertificateCustomValidationCallback = (_, _, _, _) => true
+ };
+
+ return handler;
+ });
+ services.AddHostedService();
+ services.AddMemoryCache();
+ }
+
+ ///
+ public override void Configure(IApplicationBuilder app, IWebHostEnvironment env)
+ {
+ base.Configure(app, env);
+
+ using var scope = app.ApplicationServices.CreateScope();
+ var factory = Configuration.GetSection("Factory").Value;
+
+ var ds = scope.ServiceProvider.GetService();
+ ds.TruncateAsync().Wait();
+ ds.DownloadAsync(factory).Wait();
+ ds.DownloadAsync(factory).Wait();
+ ds.DownloadAsync(factory).Wait();
+ ds.DownloadAsync(factory).Wait();
+ ds.DownloadAsync(factory).Wait();
+ ds.DownloadAsync(factory).Wait();
+ ds.DownloadAsync(factory).Wait();
+
+ ds.DownloadAsync(factory).Wait();
+ ds.DownloadAsync(factory).Wait();
+ }
+}
\ No newline at end of file
diff --git a/Src/Dlhis.Middle/Properties/launchSettings.json b/Src/Dlhis.Middle/Properties/launchSettings.json
new file mode 100644
index 00000000..7f14b6a8
--- /dev/null
+++ b/Src/Dlhis.Middle/Properties/launchSettings.json
@@ -0,0 +1,37 @@
+{
+ "profiles": {
+ "Dlhis.Middle": {
+ "commandName": "Project",
+ "launchUrl": "weatherforecast",
+ "environmentVariables": {
+ "ASPNETCORE_ENVIRONMENT": "Development"
+ },
+ "dotnetRunMessages": true,
+ "applicationUrl": "http://localhost:5027"
+ },
+ "IIS Express": {
+ "commandName": "IISExpress",
+ "launchUrl": "weatherforecast",
+ "environmentVariables": {
+ "ASPNETCORE_ENVIRONMENT": "Development"
+ }
+ },
+ "Docker": {
+ "commandName": "Docker",
+ "launchUrl": "{Scheme}://{ServiceHost}:{ServicePort}/weatherforecast",
+ "environmentVariables": {
+ "ASPNETCORE_URLS": "http://+:80"
+ },
+ "publishAllPorts": true
+ }
+ },
+ "$schema": "https://json.schemastore.org/launchsettings.json",
+ "iisSettings": {
+ "windowsAuthentication": false,
+ "anonymousAuthentication": true,
+ "iisExpress": {
+ "applicationUrl": "http://localhost:1982",
+ "sslPort": 0
+ }
+ }
+}
\ No newline at end of file
diff --git a/Src/Dlhis.Middle/Request/ConsumeRequest.cs b/Src/Dlhis.Middle/Request/ConsumeRequest.cs
new file mode 100644
index 00000000..ffb6ca7b
--- /dev/null
+++ b/Src/Dlhis.Middle/Request/ConsumeRequest.cs
@@ -0,0 +1,44 @@
+using Dlhis.Entity.Canteen.Enums;
+
+namespace Dlhis.Middle.Request;
+
+public class ConsumeRequest
+{
+ ///
+ /// 是否离线数据
+ ///
+ public bool IsOffline { get; set; }
+
+ ///
+ /// 消费机器唯一信息
+ ///
+ public string DeviceUniqueNo { get; set; }
+
+ ///
+ /// 第三方订单号
+ ///
+ public string ThirdNo { get; set; }
+
+ ///
+ /// 档案号/QrCode/学工号
+ ///
+ public string UserUniqueNo { get; set; }
+
+ ///
+ /// 来源
+ ///
+ public BalanceRecordSourceEnumType Source { get; set; }
+
+ ///
+ /// 金额
+ ///
+ public decimal Amount { get; set; }
+
+ ///
+ /// 产生时间
+ ///
+ public DateTime CostDateTime { get; set; }
+
+
+ public string Image { get; set; }
+}
\ No newline at end of file
diff --git a/Src/Dlhis.Middle/Request/OrderAddRequest.cs b/Src/Dlhis.Middle/Request/OrderAddRequest.cs
new file mode 100644
index 00000000..a8cc25b4
--- /dev/null
+++ b/Src/Dlhis.Middle/Request/OrderAddRequest.cs
@@ -0,0 +1,63 @@
+using System.ComponentModel.DataAnnotations;
+using Dlhis.Entity.Canteen.Enums;
+using Kurisu.AspNetCore.Utils.Extensions;
+
+namespace Dlhis.Middle.Request;
+
+public class OrderAddRequest : IValidatableObject
+{
+ ///
+ /// 是否离线数据
+ ///
+ public bool IsOffline { get; set; } = false;
+
+ ///
+ /// 消费机器唯一信息
+ ///
+ public string DeviceUniqueNo { get; set; }
+
+ ///
+ /// 第三方订单号
+ ///
+ public string ThirdNo { get; set; }
+
+ ///
+ /// 档案号/QrCode/学工号
+ ///
+ [Required(ErrorMessage = "人员档案号/QrCode/学工号/userId 不能为空")]
+ public string UserUniqueNo { get; set; }
+
+ ///
+ /// 来源
+ ///
+ [EnumDataType(typeof(BalanceRecordSourceEnumType))]
+ public BalanceRecordSourceEnumType Source { get; set; }
+
+ ///
+ /// 金额
+ ///
+ [Range(0.01, double.MaxValue)]
+ public decimal Amount { get; set; }
+
+ ///
+ /// 产生时间
+ ///
+ public DateTime? CostDateTime { get; set; }
+
+
+ public string Image { get; set; }
+
+
+ public IEnumerable Validate(ValidationContext validationContext)
+ {
+ //处理金额
+ Amount = -Math.Abs(Amount);
+
+ CostDateTime ??= DateTime.Now;
+
+ if (DeviceUniqueNo.IsEmpty())
+ {
+ yield return new ValidationResult("设备号不能为空", new[] { nameof(DeviceUniqueNo) });
+ }
+ }
+}
diff --git a/Src/Dlhis.Middle/Response/CanteenSettingResponse.cs b/Src/Dlhis.Middle/Response/CanteenSettingResponse.cs
new file mode 100644
index 00000000..388d0e4c
--- /dev/null
+++ b/Src/Dlhis.Middle/Response/CanteenSettingResponse.cs
@@ -0,0 +1,44 @@
+using Dlhis.Entity.Canteen.Enums;
+using Kurisu.AspNetCore.Utils.Extensions;
+
+namespace Dlhis.Middle.Response;
+
+public class CanteenSettingResponse
+{
+
+ public long Id { get; set; }
+
+ public CanteenSettingUnqiueEnumType UniqueType { get; set; }
+ public string UniqueTypeDesc => UniqueType.GetDescription();
+
+ ///
+ /// 唯一值
+ ///
+ public string UniqueValue { get; set; }
+
+ ///
+ /// 唯一值说明
+ ///
+ public string UniqueValueDesc { get; set; }
+
+ ///
+ /// 分类
+ ///
+ public string Catalog { get; set; }
+
+ ///
+ /// 参数
+ ///
+ public CanteenSettingItemEnumType Item { get; set; }
+ public string ItemDesc => Item.GetDescription();
+
+ ///
+ /// 值
+ ///
+ public string Value { get; set; }
+
+ ///
+ /// 是否启用
+ ///
+ public bool Enable { get; set; }
+}
diff --git a/Src/Dlhis.Middle/Response/OrderResultResponse.cs b/Src/Dlhis.Middle/Response/OrderResultResponse.cs
new file mode 100644
index 00000000..86c95218
--- /dev/null
+++ b/Src/Dlhis.Middle/Response/OrderResultResponse.cs
@@ -0,0 +1,35 @@
+using Newtonsoft.Json;
+
+namespace Dlhis.Middle.Response;
+
+public class OrderResultResponse
+{
+ ///
+ /// 用户工号
+ ///
+ public string UserCode { get; set; }
+
+ ///
+ /// 用户名称
+ ///
+ public string UserName { get; set; }
+
+ ///
+ /// 部门号
+ ///
+ public string DepartmentNo { get; set; }
+
+ ///
+ /// 部门名称
+ ///
+ public string DepartmentName { get; set; }
+
+
+ [JsonIgnore]
+ public decimal CurrentBalance { get; set; }
+
+ ///
+ /// 余额
+ ///
+ public string Balance => CurrentBalance.ToString("f2");
+}
diff --git a/Src/Dlhis.Middle/Response/TokenResponse.cs b/Src/Dlhis.Middle/Response/TokenResponse.cs
new file mode 100644
index 00000000..1e2d84d0
--- /dev/null
+++ b/Src/Dlhis.Middle/Response/TokenResponse.cs
@@ -0,0 +1,22 @@
+using Newtonsoft.Json;
+
+namespace Dlhis.Middle.Response;
+
+public class TokenResponse
+{
+ [JsonProperty("access_token")]
+ public string AccessToken { get; set; }
+
+ [JsonProperty("token_type")]
+ public string TokenType { get; set; }
+
+ ///
+ ///
+ ///
+ [JsonProperty("scope")]
+ public string Scope { get; set; }
+
+
+ [JsonProperty("expires_in")]
+ public int Expires { get; set; }
+}
diff --git a/Src/Dlhis.Middle/Services/ICanteenSettingService.cs b/Src/Dlhis.Middle/Services/ICanteenSettingService.cs
new file mode 100644
index 00000000..2de8d343
--- /dev/null
+++ b/Src/Dlhis.Middle/Services/ICanteenSettingService.cs
@@ -0,0 +1,18 @@
+using Dlhis.Entity.Canteen.Enums;
+
+namespace Dlhis.Middle.Services;
+
+public interface ICanteenSettingService : IScopeDependency
+{
+ ///
+ /// 获取人员的就餐配置信息
+ ///
+ /// 就餐人员档案号
+ ///
+ ///
+ Task> GetUserSettingAsync(string userNo, string tenantId);
+
+
+ Task GetBusinessSwitchTimeAsync(string tenantId);
+}
+
diff --git a/Src/Dlhis.Middle/Services/IDataDownloadService.cs b/Src/Dlhis.Middle/Services/IDataDownloadService.cs
new file mode 100644
index 00000000..b80ed994
--- /dev/null
+++ b/Src/Dlhis.Middle/Services/IDataDownloadService.cs
@@ -0,0 +1,8 @@
+namespace Dlhis.Middle.Services;
+
+public interface IDataDownloadService : IScopeDependency
+{
+ Task DownloadAsync(string factory) where T : class, new();
+
+ Task TruncateAsync();
+}
\ No newline at end of file
diff --git a/Src/Dlhis.Middle/Services/IHealthCheckService.cs b/Src/Dlhis.Middle/Services/IHealthCheckService.cs
new file mode 100644
index 00000000..e8c05049
--- /dev/null
+++ b/Src/Dlhis.Middle/Services/IHealthCheckService.cs
@@ -0,0 +1,8 @@
+namespace Dlhis.Middle.Services;
+
+public interface IHealthCheckService : ISingletonDependency
+{
+ bool GetIsOffline();
+
+ Task DoHealthCheckAsync();
+}
\ No newline at end of file
diff --git a/Src/Dlhis.Middle/Services/IOrderService.cs b/Src/Dlhis.Middle/Services/IOrderService.cs
new file mode 100644
index 00000000..fb5ae382
--- /dev/null
+++ b/Src/Dlhis.Middle/Services/IOrderService.cs
@@ -0,0 +1,10 @@
+using Dlhis.Middle.Request;
+using Dlhis.Middle.Response;
+using Kurisu.AspNetCore.UnifyResultAndValidation;
+
+namespace Dlhis.Middle.Services;
+
+public interface IOrderService : IScopeDependency
+{
+ Task> AddAsync(OrderAddRequest request);
+}
\ No newline at end of file
diff --git a/Src/Dlhis.Middle/Services/IRemoteOrderService.cs b/Src/Dlhis.Middle/Services/IRemoteOrderService.cs
new file mode 100644
index 00000000..e161d9ad
--- /dev/null
+++ b/Src/Dlhis.Middle/Services/IRemoteOrderService.cs
@@ -0,0 +1,12 @@
+using Dlhis.Middle.Request;
+using Dlhis.Middle.Response;
+using Kurisu.AspNetCore.UnifyResultAndValidation;
+
+namespace Dlhis.Middle.Services;
+
+public interface IRemoteOrderService : IScopeDependency
+{
+ Task> AddAsync(OrderAddRequest request);
+
+ Task AbleRemoteCallAsync();
+}
\ No newline at end of file
diff --git a/Src/Dlhis.Middle/Services/Implements/CanteenSettingService.cs b/Src/Dlhis.Middle/Services/Implements/CanteenSettingService.cs
new file mode 100644
index 00000000..e6a16149
--- /dev/null
+++ b/Src/Dlhis.Middle/Services/Implements/CanteenSettingService.cs
@@ -0,0 +1,104 @@
+using Dlhis.Entity.Basic.Entity;
+using Dlhis.Entity.Canteen.Enums;
+using Kurisu.AspNetCore.DataAccess.SqlSugar.Services;
+using Mapster;
+
+namespace Dlhis.Middle.Services.Implements;
+
+///
+/// 餐饮配置
+///
+public class CanteenSettingService : ICanteenSettingService
+{
+ private readonly IDbContext _context;
+
+ public CanteenSettingService(IDbContext context)
+ {
+ _context = context;
+ }
+
+ public async Task> GetUserSettingAsync(string userNo, string tenantId)
+ {
+ var settings = await _context.Queryable()
+ .Where(x => x.Enable)
+ .Where(x => x.TenantId == tenantId)
+ .Where(x => x.UniqueType == CanteenSettingUnqiueEnumType.Person && x.UniqueValue == userNo || x.UniqueType == CanteenSettingUnqiueEnumType.Tenant)
+ .ToListAsync();
+
+ //从基地 -> 个人,确保下面循环做到覆盖,并且以个人配置为主
+ settings = settings.OrderBy(x => x.UniqueType).ToList();
+
+ var result = new Dictionary();
+ settings.ForEach(x => { result[x.Item] = x.Value; });
+
+ return result;
+ }
+
+
+ public async Task GetBusinessSwitchTimeAsync(string tenantId)
+ {
+ var setting = await _context.Queryable()
+ .Where(x => x.Enable)
+ .Where(x => x.TenantId == tenantId)
+ .Where(x => x.Item == CanteenSettingItemEnumType.BusinessSwitchTime)
+ .FirstAsync();
+
+ try
+ {
+ return TimeOnly.Parse(setting.Value);
+ }
+ catch (Exception)
+ {
+ return null;
+ }
+ }
+
+ //*********************************************** help methods ***************************************************//
+
+
+
+ ///
+ /// 检验参数
+ ///
+ ///
+ ///
+ ///
+ ///
+ private static bool IsValidFormat(string value, CanteenSettingItemEnumType? type = null)
+ {
+ try
+ {
+ //判断0/1
+ if (type == CanteenSettingItemEnumType.AllowSendAtMeal ||
+ type == CanteenSettingItemEnumType.AllowRepeatInOneShift ||
+ type == CanteenSettingItemEnumType.AllowNoBalance ||
+ type == CanteenSettingItemEnumType.AllowCrossTenant)
+ {
+ var intvaule = int.Parse(value);
+ if (intvaule == 0 || intvaule == 1)
+ {
+ return true;
+ }
+ else
+ {
+ return false;
+ }
+ }
+
+ if (type == CanteenSettingItemEnumType.BusinessSwitchTime)
+ {
+ var timeOnly = value.Adapt();
+ return timeOnly.Minute == 0 && timeOnly.Second == 0;
+ }
+
+ //mapster
+ value.Adapt();
+ }
+ catch (Exception)
+ {
+ return false;
+ }
+
+ return true;
+ }
+}
\ No newline at end of file
diff --git a/Src/Dlhis.Middle/Services/Implements/DataDownloadService.cs b/Src/Dlhis.Middle/Services/Implements/DataDownloadService.cs
new file mode 100644
index 00000000..8450d253
--- /dev/null
+++ b/Src/Dlhis.Middle/Services/Implements/DataDownloadService.cs
@@ -0,0 +1,85 @@
+using System.Net.Http.Headers;
+using System.Reflection;
+using System.Text;
+using Dlhis.Middle.Entities;
+using Dlhis.Middle.Response;
+using Kurisu.AspNetCore.DataAccess.SqlSugar.Services;
+using Kurisu.AspNetCore.UnifyResultAndValidation;
+using Kurisu.AspNetCore.Utils.Extensions;
+using Microsoft.AspNetCore.WebUtilities;
+using Microsoft.Extensions.Caching.Memory;
+using Newtonsoft.Json;
+using SqlSugar;
+
+namespace Dlhis.Middle.Services.Implements;
+
+public class DataDownloadService : IDataDownloadService
+{
+ private readonly IHttpClientFactory _httpClientFactory;
+ private readonly IMemoryCache _memoryCache;
+ private readonly IDbContext _dbContext;
+ private readonly string _hostName;
+
+ public DataDownloadService(IHttpClientFactory httpClientFactory, IMemoryCache memoryCache, IConfiguration configuration, IDbContext dbContext)
+ {
+ _httpClientFactory = httpClientFactory;
+ _memoryCache = memoryCache;
+ _dbContext = dbContext;
+ _hostName = configuration.GetSection("HostName").Value;
+ }
+
+ public async Task DownloadAsync(string factory) where T : class, new()
+ {
+ var token = await AuthAsync();
+
+ var httpClient = _httpClientFactory.CreateClient("face");
+ httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", token);
+
+ var requestUrl = QueryHelpers.AddQueryString($"https://{_hostName}/api/common/SysData/download", new Dictionary
+ {
+ ["factory"] = factory,
+ ["tableName"] = typeof(T).GetCustomAttribute()!.TableName
+ });
+
+ var response = await httpClient.GetStringAsync(requestUrl);
+ var pack = response.ToObject>>();
+
+ await _dbContext.InsertAsync(pack.Data);
+ }
+
+ ///
+ public async Task TruncateAsync()
+ {
+ await _dbContext.Client.Deleteable().ExecuteCommandAsync();
+ await _dbContext.Client.Deleteable().ExecuteCommandAsync();
+ await _dbContext.Client.Deleteable().ExecuteCommandAsync();
+ await _dbContext.Client.Deleteable().ExecuteCommandAsync();
+ await _dbContext.Client.Deleteable().ExecuteCommandAsync();
+ await _dbContext.Client.Deleteable().ExecuteCommandAsync();
+ await _dbContext.Client.Deleteable().ExecuteCommandAsync();
+ await _dbContext.Client.Deleteable().ExecuteCommandAsync();
+ await _dbContext.Client.Deleteable().ExecuteCommandAsync();
+ }
+
+ private async Task AuthAsync()
+ {
+ var httpClient = _httpClientFactory.CreateClient("face");
+
+ if (_memoryCache.TryGetValue("token", out string token)) return token;
+
+ var tokenResponse = await httpClient.PostAsync($"https://{_hostName}/api/User/OAuth2/clientgrant", new StringContent(JsonConvert.SerializeObject(new
+ {
+ ClientId = "e509c2aaa98e300c",
+ ClientSecret = "6184d8979fc48105"
+ }), Encoding.UTF8, "application/json"));
+
+ var tokenJson = await tokenResponse.Content.ReadAsStringAsync();
+ var tokenInfo = JsonConvert.DeserializeObject>(tokenJson);
+
+ token = tokenInfo.Data.AccessToken;
+
+ _memoryCache.Set("token", token, TimeSpan.FromSeconds(tokenInfo.Data.Expires));
+
+ return token;
+ }
+}
\ No newline at end of file
diff --git a/Src/Dlhis.Middle/Services/Implements/HealthCheckBackgroundService.cs b/Src/Dlhis.Middle/Services/Implements/HealthCheckBackgroundService.cs
new file mode 100644
index 00000000..bc658f64
--- /dev/null
+++ b/Src/Dlhis.Middle/Services/Implements/HealthCheckBackgroundService.cs
@@ -0,0 +1,19 @@
+namespace Dlhis.Middle.Services.Implements;
+
+public class HealthCheckBackgroundService : BackgroundService
+{
+ private readonly IHealthCheckService _healthCheckService;
+
+ public HealthCheckBackgroundService(IHealthCheckService healthCheckService)
+ {
+ _healthCheckService = healthCheckService;
+ }
+
+ protected override async Task ExecuteAsync(CancellationToken stoppingToken)
+ {
+ while (!stoppingToken.IsCancellationRequested)
+ {
+ await _healthCheckService.DoHealthCheckAsync();
+ }
+ }
+}
\ No newline at end of file
diff --git a/Src/Dlhis.Middle/Services/Implements/HealthcheckService.cs b/Src/Dlhis.Middle/Services/Implements/HealthcheckService.cs
new file mode 100644
index 00000000..117ca886
--- /dev/null
+++ b/Src/Dlhis.Middle/Services/Implements/HealthcheckService.cs
@@ -0,0 +1,65 @@
+using System.Net.Sockets;
+
+namespace Dlhis.Middle.Services.Implements;
+
+public class HealthCheckService : IHealthCheckService
+{
+ private bool _isOffline = true;
+ private readonly string _hostName;
+ private readonly ILogger _logger;
+ private int _successRetry;
+ private int _retry;
+
+ public HealthCheckService(ILogger logger, IConfiguration configuration)
+ {
+ _logger = logger;
+ _hostName = configuration.GetSection("HostName").Value;
+ }
+
+ public bool GetIsOffline()
+ {
+ return _isOffline;
+ }
+
+ public async Task DoHealthCheckAsync()
+ {
+ var tcpClient = new TcpClient();
+ try
+ {
+ _logger.LogInformation("tcp connecting {host} {port}", _hostName, 443);
+ await tcpClient.ConnectAsync(_hostName, 443);
+ _retry++;
+ _logger.LogInformation("retry {retry}", _retry);
+ if (tcpClient.Connected)
+ {
+ _successRetry++;
+ _logger.LogInformation("success connected {count}", _successRetry);
+ if (_successRetry == 4)
+ {
+ _isOffline = false;
+ _successRetry = 0;
+ _retry = 0;
+ _logger.LogInformation("current is {status}", "healthy");
+ }
+ else
+ {
+ _logger.LogInformation("wait next check in {waitTime}", _successRetry * 1000);
+ await Task.Delay(_successRetry * 1000);
+ }
+ }
+ else
+ {
+ _successRetry = 0;
+ _isOffline = true;
+ _logger.LogInformation("current is {status}", "unavailable");
+ await Task.Delay(2000);
+ }
+ }
+ catch (Exception ex)
+ {
+ _logger.LogError("fail cause :{reason}", ex.Message);
+ _isOffline = true;
+ await Task.Delay(5000);
+ }
+ }
+}
\ No newline at end of file
diff --git a/Src/Dlhis.Middle/Services/Implements/OrderService.cs b/Src/Dlhis.Middle/Services/Implements/OrderService.cs
new file mode 100644
index 00000000..ca86c4e2
--- /dev/null
+++ b/Src/Dlhis.Middle/Services/Implements/OrderService.cs
@@ -0,0 +1,320 @@
+using Dlhis.Entity.Basic.Entity;
+using Dlhis.Entity.Canteen.Entities;
+using Dlhis.Entity.Canteen.Enums;
+using Dlhis.Entity.Device.Entities;
+using Kurisu.AspNetCore.Utils;
+using Kurisu.AspNetCore.Utils.Extensions;
+using Mapster;
+using Kurisu.AspNetCore.CustomClass;
+using Kurisu.AspNetCore.DataAccess.SqlSugar.Extensions;
+using Kurisu.AspNetCore.DataAccess.SqlSugar.Services;
+using Kurisu.AspNetCore.UnifyResultAndValidation;
+using Dlhis.Middle.Response;
+using Dlhis.Middle.Request;
+
+namespace Dlhis.Middle.Services.Implements;
+
+public class OrderService : IOrderService
+{
+ private readonly ICanteenSettingService _canteenSettingService;
+ private readonly IRemoteOrderService _remoteOrderService;
+ private readonly string _factory;
+ private readonly IDbContext _dbContext;
+ private Dictionary _settings;
+
+ public OrderService(ICanteenSettingService canteenSettingService,
+ IRemoteOrderService remoteOrderService,
+ IConfiguration configuration,
+ IDbContext dbContext)
+ {
+ _canteenSettingService = canteenSettingService;
+ _remoteOrderService = remoteOrderService;
+ _factory = configuration.GetSection("Factory").Value;
+ _dbContext = dbContext;
+ }
+
+ public async Task> AddAsync(OrderAddRequest request)
+ {
+ if (await _remoteOrderService.AbleRemoteCallAsync())
+ {
+ return await _remoteOrderService.AddAsync(request);
+ }
+
+ //尝试匹配userId
+ _ = long.TryParse(request.UserUniqueNo, out var userId);
+
+ //根据档案号
+ var gainfo = await _dbContext.Queryable()
+ .Where(x => x.Code == request.UserUniqueNo || x.Id == userId || x.CardNo == request.UserUniqueNo)
+ .FirstAsync();
+
+ var message = string.Empty;
+
+ gainfo.ThrowIfNull("无消费权限");
+
+ try
+ {
+ (await _dbContext.Queryable().Where(x => x.UserNo == gainfo.No).AnyAsync())
+ .ThrowIfFalse("余额不足,无法消费");
+
+ await ConsumeAsync(request.Adapt(), gainfo);
+ }
+ catch (Exception ex)
+ {
+ message = ex.Message;
+ }
+
+
+ //获取人员余额
+ var data = await _dbContext.Queryable()
+ .LeftJoin((ga, department) => ga.DepartmentNo == department.No)
+ .LeftJoin((ga, department, mealUser) => ga.No == mealUser.UserNo)
+ .Where(ga => ga.Id == gainfo.Id)
+ .Select((ga, department, mealUser) => new OrderResultResponse
+ {
+ UserCode = ga.Code,
+ UserName = ga.Name,
+ DepartmentNo = ga.DepartmentNo,
+ DepartmentName = department.Name,
+ CurrentBalance = mealUser.Balance
+ })
+ .SingleAsync();
+
+ return new DefaultApiResult
+ {
+ Code = message.IsPresent() ? ApiStateCode.Error : ApiStateCode.Success,
+ Data = data,
+ Msg = message,
+ };
+ }
+
+
+ ///
+ /// 消费
+ ///
+ ///
+ ///
+ ///
+ private async Task ConsumeAsync(ConsumeRequest request, GainfoEntity gainfo)
+ {
+ var registry = await _dbContext.Queryable().Where(x => x.DeviceUniqueNo == request.DeviceUniqueNo).SingleAsync();
+ registry.ThrowIfNull("设备尚未登记");
+
+ var device = await _dbContext.Queryable().Where(x => x.UniqueNo == request.DeviceUniqueNo).SingleAsync();
+ device.ThrowIfNull("设备尚未登记");
+ device.Enable.ThrowIfFalse("设备已被停用");
+
+ var businessPlaceInfo = await _dbContext.Queryable()
+ .LeftJoin((x, sr) => x.Code == sr.PlaceCode)
+ .Where(x => x.Code == registry.BusinessPlaceCode)
+ .Select((x, sr) => new
+ {
+ x.Code,
+ sr.ShiftCode,
+ x.TenantId
+ })
+ .FirstAsync();
+ businessPlaceInfo.ThrowIfNull("设备尚未登记营业点");
+
+ //获取当前消费餐次
+ var shifts = await _dbContext.Queryable().Where(x => x.ShiftCode == businessPlaceInfo.ShiftCode).ToListAsync();
+ shifts.ThrowIfEmpty("营业点餐次尚未设置");
+
+ var shift = shifts.FirstOrDefault(x => new TimePeriod
+ {
+ Start = x.Start,
+ End = x.End
+ }.IsPresent1(request.CostDateTime));
+ shift.ThrowIfNull("非就餐时间,无法消费");
+
+ var costDate = await GetCostDateAsync(shift, request.CostDateTime);
+
+ var balanceRecord = new BalanceRecordEntity
+ {
+ // RequestJson = request.ToJson(),
+ // ThirdNo = request.ThirdNo,
+ Type = BalanceRecordTypeEnumType.Out,
+ ApplicationType = CanteenApplicationTypeEnumType.Device,
+ Name = gainfo.Name,
+ Source = request.Source,
+ UserNo = gainfo.No,
+ UserCode = gainfo.Code,
+ Amount = request.Amount,
+ BusinessPlaceCode = businessPlaceInfo?.Code,
+ BusinessShiftCode = shift?.ShiftCode,
+ ShiftType = shift?.ShiftType,
+ CostDate = costDate.Date,
+ CostId = gainfo.CostId,
+ DepartmentNo = gainfo.DepartmentNo,
+ DeviceUniqueNo = request.DeviceUniqueNo,
+
+ No = _factory + "-" + SnowFlakeHelper.Instance.NextId(),
+ SubsidiesFromMonth = request.CostDateTime.ToString("yyyyMM"),
+ // IsOffline = request.IsOffline,
+ IsCostCrossTenant = await GetSettingAsync(businessPlaceInfo!.TenantId, CanteenSettingItemEnumType.AllowCrossTenant),
+
+ OriginTenantId = gainfo.Factory,
+ Werks = gainfo.Werks,
+
+ //餐别异常处理
+ Status = shift == null
+ ? BalanceRecordStatusEnumType.Doubtful
+ : BalanceRecordStatusEnumType.Valid,
+
+ //OrderStatus = CanteenOrderStatusEnumType.Wait,
+
+ CostDateTime = request.CostDateTime,
+
+ //标识当前消费的基地
+ TenantId = businessPlaceInfo.TenantId,
+ };
+
+ //校验
+ await Verify(balanceRecord);
+
+ var allowNoBalance = await GetSettingAsync(balanceRecord.TenantId, CanteenSettingItemEnumType.AllowNoBalance);
+
+ await CommonHelper.RetryAsync(async () =>
+ {
+ if (!allowNoBalance)
+ {
+ var gainfoMeal = await _dbContext.Queryable().Where(x => x.UserNo == balanceRecord.UserNo).SingleAsync();
+ //金额+-
+ gainfoMeal.Balance += balanceRecord.Amount;
+
+ (gainfoMeal.Balance < 0).ThrowIfTrue("余额不足,无法消费");
+
+ await _dbContext.Updateable()
+ .SetColumns(x => x.Balance == gainfoMeal.Balance)
+ .SetColumns(x => x.Version == x.Version + 1)
+ .SetColumns(x => x.ModifiedTime == DateTime.Now)
+ .Where(x => x.UserNo == balanceRecord.UserNo && x.Version == gainfoMeal.Version) //必须与查询时的version一致
+ .ExecuteCommandThrowVersionAsync();
+ }
+
+ await _dbContext.InsertAsync(balanceRecord);
+ }, 5);
+ }
+
+
+ //******************************************************* help methods ************************************************************/
+
+ ///
+ /// 获取成本日期
+ ///
+ ///
+ ///
+ ///
+ private async Task GetCostDateAsync(BusinessShiftMealRelationshipEntity shift, DateTime costDateTime)
+ {
+ var switchTime = await GetSettingAsync(shift.TenantId, CanteenSettingItemEnumType.BusinessSwitchTime);
+
+ var costDate = costDateTime.Date;
+
+ if (switchTime.HasValue)
+ {
+ var beginOfDay = costDateTime.Date;
+
+ var setTime = beginOfDay.Add(switchTime.Value.ToTimeSpan());
+ if (costDateTime < setTime)
+ costDate = costDateTime.Date.AddDays(-1);
+ }
+
+ return costDate;
+ }
+
+
+ ///
+ /// 验证参数
+ ///
+ ///
+ ///
+ ///
+ private async Task Verify(BalanceRecordEntity order)
+ {
+ var beginOfDay = DateTime.Today;
+ var endOfDay = beginOfDay.AddDays(1).AddSeconds(-1);
+
+ //今天所有消费记录
+ var todayAllBalanceRecords = await _dbContext.Queryable()
+ .Where(x => x.UserNo == order.UserNo)
+ .Where(x => x.Type == BalanceRecordTypeEnumType.Out)
+ .Where(x => x.Status == BalanceRecordStatusEnumType.Valid)
+ .Where(x => x.CreateTime >= beginOfDay && x.CreateTime <= endOfDay)
+ .ToListAsync();
+
+ //超市营业点
+ var supermarketCodes = await _dbContext.Queryable()
+ .Where(x => x.Catalog == CanteenBusinessPlaceCatalogEnumType.Supermarket)
+ .Select(x => x.Code)
+ .ToListAsync();
+
+ //一天消费金额
+ var dayCostMax = await GetSettingAsync(order.TenantId, CanteenSettingItemEnumType.DayCostMax);
+ if (dayCostMax != default)
+ {
+ //must has business place code, use .Value is safe
+ var cost = todayAllBalanceRecords
+ .Where(x => !supermarketCodes.Contains(x.BusinessPlaceCode.Value)) //filter supermarket
+ .Sum(x => Math.Abs(x.Amount));
+ (cost + Math.Abs(order.Amount) > dayCostMax).ThrowIfTrue($"超出当天消费限制{dayCostMax}元,无法消费");
+ }
+
+ //一次消费金额
+ var onceCostMax = await GetSettingAsync(order.TenantId, CanteenSettingItemEnumType.OnceCostMax);
+ if (onceCostMax != default)
+ {
+ (Math.Abs(order.Amount) > onceCostMax).ThrowIfTrue($"单次消费金额不能超出{onceCostMax}元");
+ }
+
+ //超时消费限额
+ var dayMarketCostMax = await GetSettingAsync(order.TenantId, CanteenSettingItemEnumType.DayMarketCostMax);
+ if (dayMarketCostMax != default)
+ {
+ //current businessplace is supermarket
+ if (order.BusinessPlaceCode.HasValue && supermarketCodes.Contains(order.BusinessPlaceCode.Value))
+ {
+ var cost = todayAllBalanceRecords.Where(x => x.BusinessPlaceCode.HasValue)
+ .Where(x => supermarketCodes.Contains(x.BusinessPlaceCode.Value))
+ .Sum(x => Math.Abs(x.Amount));
+
+ (cost + Math.Abs(order.Amount) > dayMarketCostMax).ThrowIfTrue($"超出当天超市消费限制{dayMarketCostMax},无法消费");
+ }
+ }
+
+ //重复餐次消费
+ var allowRepeatInOneShift = await GetSettingAsync(order.TenantId, CanteenSettingItemEnumType.AllowRepeatInOneShift, true);
+ if (!allowRepeatInOneShift)
+ {
+ var costCount = todayAllBalanceRecords.Count(x => x.ShiftType == order.ShiftType);
+ (costCount > 1).ThrowIfTrue($"{order?.ShiftType?.GetDescription()}不允许多次消费");
+ }
+ }
+
+ ///
+ /// 获取参数配置
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ private async Task GetSettingAsync(string tenantId, CanteenSettingItemEnumType item, T defaultValue = default)
+ {
+ _settings ??= await _canteenSettingService.GetUserSettingAsync(string.Empty, tenantId);
+
+ if (_settings.TryGetValue(item, out var v))
+ {
+ try
+ {
+ return int.TryParse(v, out var n) ? n.Adapt() : v.Adapt();
+ }
+ catch (Exception)
+ {
+ return defaultValue;
+ }
+ }
+
+ return defaultValue;
+ }
+}
\ No newline at end of file
diff --git a/Src/Dlhis.Middle/Services/Implements/RemoteOrderService.cs b/Src/Dlhis.Middle/Services/Implements/RemoteOrderService.cs
new file mode 100644
index 00000000..cda2a8b7
--- /dev/null
+++ b/Src/Dlhis.Middle/Services/Implements/RemoteOrderService.cs
@@ -0,0 +1,87 @@
+using System.Net.Http.Headers;
+using System.Text;
+using Dlhis.Middle.Entities;
+using Dlhis.Middle.Request;
+using Dlhis.Middle.Response;
+using Kurisu.AspNetCore.DataAccess.SqlSugar.Services;
+using Kurisu.AspNetCore.UnifyResultAndValidation;
+using Kurisu.AspNetCore.Utils.Extensions;
+using Microsoft.Extensions.Caching.Memory;
+using Newtonsoft.Json;
+
+namespace Dlhis.Middle.Services.Implements;
+
+public class RemoteOrderService : IRemoteOrderService
+{
+ private readonly string _hostName;
+ private readonly IHttpClientFactory _httpClientFactory;
+ private readonly IMemoryCache _memoryCache;
+ private readonly IHealthCheckService _healthCheckService;
+ private readonly IDbContext _dbContext;
+
+ public RemoteOrderService(IHttpClientFactory httpClientFactory,
+ IMemoryCache memoryCache,
+ IConfiguration configuration,
+ IHealthCheckService healthCheckService,
+ IDbContext dbContext)
+ {
+ _httpClientFactory = httpClientFactory;
+ _memoryCache = memoryCache;
+ _hostName = configuration.GetSection("hostName").Value;
+ _healthCheckService = healthCheckService;
+ _dbContext = dbContext;
+ }
+
+
+
+ ///
+ public async Task> AddAsync(OrderAddRequest request)
+ {
+ var token = await AuthAsync();
+
+ var httpClient = _httpClientFactory.CreateClient("face");
+ httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", token);
+
+ var content = new StringContent(request.ToJson(), Encoding.UTF8, "application/json");
+
+ var response = await httpClient.PostAsync($"https://{_hostName}/api/Canteen/Orders/device", content);
+
+ var responseJson = await response.Content.ReadAsStringAsync();
+ return responseJson.ToObject>();
+ }
+
+ ///
+ public async Task AbleRemoteCallAsync()
+ {
+ if (await _dbContext.Queryable().AnyAsync(x => !x.IsSync))
+ {
+ return false;
+ }
+
+ return !_healthCheckService.GetIsOffline();
+ }
+
+
+
+ private async Task AuthAsync()
+ {
+ var httpClient = _httpClientFactory.CreateClient("face");
+
+ if (_memoryCache.TryGetValue("token", out string token)) return token;
+
+ var tokenResponse = await httpClient.PostAsync($"https://{_hostName}/api/User/OAuth2/clientgrant", new StringContent(JsonConvert.SerializeObject(new
+ {
+ ClientId = "e569c2aaa98e300c",
+ ClientSecret = "6184d8979fc48105"
+ }), Encoding.UTF8, "application/json"));
+
+ var tokenJson = await tokenResponse.Content.ReadAsStringAsync();
+ var tokenInfo = JsonConvert.DeserializeObject>(tokenJson);
+
+ token = tokenInfo.Data.AccessToken;
+
+ _memoryCache.Set("token", token, TimeSpan.FromSeconds(tokenInfo.Data.Expires));
+
+ return token;
+ }
+}
\ No newline at end of file
diff --git a/Src/Dlhis.Middle/TimePeriodExtensions.cs b/Src/Dlhis.Middle/TimePeriodExtensions.cs
new file mode 100644
index 00000000..21a93c43
--- /dev/null
+++ b/Src/Dlhis.Middle/TimePeriodExtensions.cs
@@ -0,0 +1,42 @@
+using Kurisu.AspNetCore.CustomClass;
+
+namespace Dlhis.Middle;
+
+
+///
+/// 临时的扩展类 todo: 嵌入框架
+///
+public static class TimePeriodExtensions
+{
+ public static bool IsPresent1(this TimePeriod period, DateTime dateTime)
+ {
+ DateTime start;
+ DateTime end;
+
+ if (period.IsCrossDay())
+ {
+ //period = [22:00:00,03:00:00]
+ var currentStart = dateTime.Date.Add(period.Start.Value.ToTimeSpan());
+ var endOfDay = dateTime.Date.AddDays(1).AddSeconds(-1);
+
+ //assume is left range . then currentStart is 2024-04-16 22:00:00 and endOfDay is 2024-04-16 23:59:59
+ if (dateTime >= currentStart && dateTime <= endOfDay)
+ {
+ start = currentStart; //2024-04-16 22:00:00
+ end = dateTime.Date.AddDays(1).Add(period.End.Value.ToTimeSpan());//2024-04-17 03:00:00
+ }
+ else
+ {
+ start = dateTime.Date.AddDays(-1).Add(period.Start.Value.ToTimeSpan());
+ end = dateTime.Date.Add(period.End.Value.ToTimeSpan());
+ }
+ }
+ else
+ {
+ start = dateTime.Date.Add(period.Start.Value.ToTimeSpan());
+ end = dateTime.Date.Add(period.End.Value.ToTimeSpan());
+ }
+
+ return dateTime >= start && dateTime <= end;
+ }
+}