diff --git a/NugetTest/rezero.db b/NugetTest/rezero.db new file mode 100644 index 0000000..1d14aaa Binary files /dev/null and b/NugetTest/rezero.db differ diff --git a/ReZero.sln b/ReZero.sln index fc9ce5b..30e3226 100644 --- a/ReZero.sln +++ b/ReZero.sln @@ -17,6 +17,8 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "DemoTest", "DemoTest", "{10 EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DependencyInjectionTest", "DependencyInjectionTest\DependencyInjectionTest.csproj", "{E7355DEA-E652-4C6A-960C-CECF4B4EB673}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "YbTest", "YbTest\YbTest.csproj", "{0E79520C-8BBF-4B4E-9BAB-79E4B913162F}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -43,6 +45,10 @@ Global {E7355DEA-E652-4C6A-960C-CECF4B4EB673}.Debug|Any CPU.Build.0 = Debug|Any CPU {E7355DEA-E652-4C6A-960C-CECF4B4EB673}.Release|Any CPU.ActiveCfg = Release|Any CPU {E7355DEA-E652-4C6A-960C-CECF4B4EB673}.Release|Any CPU.Build.0 = Release|Any CPU + {0E79520C-8BBF-4B4E-9BAB-79E4B913162F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {0E79520C-8BBF-4B4E-9BAB-79E4B913162F}.Debug|Any CPU.Build.0 = Debug|Any CPU + {0E79520C-8BBF-4B4E-9BAB-79E4B913162F}.Release|Any CPU.ActiveCfg = Release|Any CPU + {0E79520C-8BBF-4B4E-9BAB-79E4B913162F}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/YbTest/Models/Insurance/InsuranceRequest.cs b/YbTest/Models/Insurance/InsuranceRequest.cs new file mode 100644 index 0000000..6d49f22 --- /dev/null +++ b/YbTest/Models/Insurance/InsuranceRequest.cs @@ -0,0 +1,110 @@ +using System.ComponentModel.DataAnnotations; + +namespace YbTest.Models.Insurance +{ + public class InsuranceRequest + { + /// + /// 交易类型代码 + /// [必填] 固定4位数字,参考医保业务代码表 + /// 示例:1101(人员信息查询) + /// + [Required, StringLength(4)] + public string Infno { get; set; } + + /// + /// 交易流水号 + /// [必填] 30位复合格式:机构编号(12位)+yyyyMMddHHmmss(14位)+4位序列号 + /// 示例:H44123456202308141023001234 + /// + [Required, StringLength(30)] + public string MsgId { get; set; } + + /// + /// 参保地区行政区划代码 + /// [必填] 6位数字,参考最新医保区划代码 + /// 示例:440305(南山区) + /// + [Required, StringLength(6)] + public string MdtrtareaAdmvs { get; set; } + + /// + /// 参保地行政区划代码 + /// [条件必填] 6位数字,异地就医时必填 + /// 示例:440306(宝安区) + /// + [StringLength(6)] + public string InsuplcAdmdvs { get; set; } + + [Required, StringLength(10)] + public string RecerSysCode { get; set; } + + [StringLength(100)] + public string DevNo { get; set; } + + [StringLength(2000)] + public string DevSafeInfo { get; set; } + + [StringLength(1024)] + public string Cainfo { get; set; } + + [StringLength(10)] + public string Signtype { get; set; } + + [Required, StringLength(6)] + public string Infver { get; set; } + + /// + /// 操作员类型 + /// [必填] 1-医保经办人 2-医院工作人员 3-参保人 + /// + [Required] + public int OpterType { get; set; } + + /// + /// 操作员账号 + /// [必填] 30位医保经办人账号(区域编码+工作人员编号) + /// 示例:4403052021000123 + /// + [Required, StringLength(30)] + public string Opter { get; set; } + + /// + /// 操作员姓名 + /// [必填] 50位中文实名,与医保系统登记信息一致 + /// 示例:张三 + /// + [Required, StringLength(50)] + public string OpterName { get; set; } + + /// + /// 交易时间 + /// [必填] ISO 8601格式:yyyy-MM-ddTHH:mm:ss + /// 示例:2023-08-14T15:30:00 + /// + [Required] + public DateTime InfTime { get; set; } + + /// + /// 定点医疗机构编号 + /// [必填] 12位医保标准编码 + /// 示例:H441234567890 + /// + [Required, StringLength(12)] + public string FixmedinsCode { get; set; } + + /// + /// 定点医疗机构名称 + /// [必填] 20位中文标准名称 + /// 示例:深圳市人民医院 + /// + [Required, StringLength(20)] + public string FixmedinsName { get; set; } + + [StringLength(30)] + public string SignNo { get; set; } + + [Required, StringLength(40000)] + public string Input { get; set; } + } +} \ No newline at end of file diff --git a/YbTest/Models/Insurance/InsuranceResponse.cs b/YbTest/Models/Insurance/InsuranceResponse.cs new file mode 100644 index 0000000..4121f15 --- /dev/null +++ b/YbTest/Models/Insurance/InsuranceResponse.cs @@ -0,0 +1,24 @@ +using System; + +namespace YbTest.Models.Insurance +{ + public class Response + { + public ResponseHeader Header { get; set; } + public string Body { get; set; } + public string Signature { get; set; } + } + + public class ResponseHeader + { + public string Version { get; set; } + public string SenderCode { get; set; } + public string ReceiverCode { get; set; } + public string MsgId { get; set; } + public string InfTime { get; set; } + public string ResultCode { get; set; } + public string ResultMsg { get; set; } + public string SignatureAlgorithm { get; set; } + public string EncryptionAlgorithm { get; set; } + } +} \ No newline at end of file diff --git a/YbTest/Models/MedicalTransactionSystem.cs b/YbTest/Models/MedicalTransactionSystem.cs new file mode 100644 index 0000000..cf15b23 --- /dev/null +++ b/YbTest/Models/MedicalTransactionSystem.cs @@ -0,0 +1,155 @@ +using System.ComponentModel.DataAnnotations.Schema; +using System.ComponentModel.DataAnnotations; + +namespace YbTest.Models +{ + public class MedicalTransactionSystem + { + + /// + /// 医保交易业务对象(入参) + /// + public class MedicalTransaction + { + /// + /// 交易编号(详见接口列表) + /// + [Required] + [StringLength(4)] + public string Infno { get; set; } + + /// + /// 发送方报文ID(主键) + /// 格式:定点医药机构编号(12)+时间(14)+顺序号(4) + /// + [Key] + [StringLength(30)] + [RegularExpression(@"^\d{12}\d{14}\d{4}$", ErrorMessage = "报文ID格式无效")] + public string MsgId { get; set; } + + /// + /// 就医地医保区划 + /// + [Required] + [StringLength(6)] + public string MdtrtareaAdmvs { get; set; } + + /// + /// 参保地医保区划(人员编号存在时必填) + /// + [StringLength(6)] + public string InsuplcAdmdvs { get; set; } + + /// + /// 接收方系统代码 + /// + [Required] + [StringLength(10)] + public string RecerSysCode { get; set; } + + /// + /// 设备编号 + /// + [StringLength(100)] + public string DevNo { get; set; } + + /// + /// 设备安全信息(JSON/XML格式) + /// + [Column(TypeName = "text")] + public string DevSafeInfo { get; set; } + + /// + /// 数字签名信息(安全管理码) + /// + [Column(TypeName = "text")] + public string Cainfo { get; set; } + + /// + /// 签名类型 + /// + [StringLength(10)] + public SignType Signtype { get; set; } + + /// + /// 接口版本号 + /// + [Required] + [StringLength(6)] + public string Infver { get; set; } + + /// + /// 经办人类别 + /// + [Required] + public OperatorType OpterType { get; set; } + + /// + /// 经办人/终端编号 + /// + [Required] + [StringLength(30)] + public string Opter { get; set; } + + /// + /// 经办人姓名/终端名称 + /// + [Required] + [StringLength(50)] + public string OpterName { get; set; } + + /// + /// 交易时间 + /// + [Required] + public DateTime InfTime { get; set; } + + /// + /// 定点医药机构编号 + /// + [Required] + [StringLength(12)] + public string FixmedinsCode { get; set; } + + /// + /// 定点医药机构名称 + /// + [Required] + [StringLength(20)] + public string FixmedinsName { get; set; } + + /// + /// 交易签到流水号(来自9001交易) + /// + [StringLength(30)] + public string SignNo { get; set; } + + /// + /// 交易输入(.NET对象序列化) + /// + [Required] + [Column(TypeName = "text")] + public string Input { get; set; } + } + + /// + /// 签名类型枚举 + /// + public enum SignType + { + SM2, + SM3 + } + + /// + /// 经办人类别枚举 + /// + public enum OperatorType + { + 经办人 = 1, + 自助终端 = 2, + 移动终端 = 3 + } + + } +} diff --git a/YbTest/Program.cs b/YbTest/Program.cs new file mode 100644 index 0000000..017b164 --- /dev/null +++ b/YbTest/Program.cs @@ -0,0 +1,26 @@ + +var builder = WebApplication.CreateBuilder(args); + +// Add services to the container. +builder.Services.AddRazorPages(); + +var app = builder.Build(); + +// Configure the HTTP request pipeline. +if (!app.Environment.IsDevelopment()) +{ + app.UseExceptionHandler("/Error"); + // The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts. + app.UseHsts(); +} + +app.UseHttpsRedirection(); +app.UseStaticFiles(); + +app.UseRouting(); + +app.UseAuthorization(); + +app.MapRazorPages(); + +app.Run(); diff --git a/YbTest/Properties/launchSettings.json b/YbTest/Properties/launchSettings.json new file mode 100644 index 0000000..2c9ea18 --- /dev/null +++ b/YbTest/Properties/launchSettings.json @@ -0,0 +1,38 @@ +{ + "$schema": "http://json.schemastore.org/launchsettings.json", + "iisSettings": { + "windowsAuthentication": false, + "anonymousAuthentication": true, + "iisExpress": { + "applicationUrl": "http://localhost:22272", + "sslPort": 44362 + } + }, + "profiles": { + "http": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": true, + "applicationUrl": "http://localhost:5283", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + }, + "https": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": true, + "applicationUrl": "https://localhost:7083;http://localhost:5283", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + }, + "IIS Express": { + "commandName": "IISExpress", + "launchBrowser": true, + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + } + } +} diff --git a/YbTest/Services/InsuranceBusinessServices/IInsuranceBusinessService.cs b/YbTest/Services/InsuranceBusinessServices/IInsuranceBusinessService.cs new file mode 100644 index 0000000..900965a --- /dev/null +++ b/YbTest/Services/InsuranceBusinessServices/IInsuranceBusinessService.cs @@ -0,0 +1,17 @@ +using YbTest.Models.Insurance; + + +namespace YbTest.Services.InsuranceBusinessServices; + +public interface IInsuranceBusinessService +{ + /// + /// 执行医保业务请求 + /// + Response Execute(InsuranceRequest request); + + /// + /// 业务校验方法 + /// + void ValidateBusinessRule(InsuranceRequest request); +} \ No newline at end of file diff --git a/YbTest/Services/MedicalInsuranceService.cs b/YbTest/Services/MedicalInsuranceService.cs new file mode 100644 index 0000000..5114015 --- /dev/null +++ b/YbTest/Services/MedicalInsuranceService.cs @@ -0,0 +1,210 @@ +using System.Text; +using System.Text.RegularExpressions; +using System.ComponentModel.DataAnnotations; +using YbTest.Models.Insurance; +using YbTest.Services.InsuranceBusinessServices; +using System.Collections.Concurrent; +using System.Security.Cryptography; +using System.Text.Json; + +namespace YbTest.Services +{ + public class MedicalInsuranceService + { + private readonly ConcurrentDictionary _serviceCache = new(); + + public void ValidateRequest(InsuranceRequest request) + { + if (string.IsNullOrEmpty(request.MsgId)) + throw new ValidationException("报文ID不能为空"); + + if (string.IsNullOrEmpty(request.MdtrtareaAdmvs)) + throw new ValidationException("就医地医保区划不能为空"); + + if (string.IsNullOrEmpty(request.RecerSysCode)) + throw new ValidationException("接收方系统代码不能为空"); + + if (string.IsNullOrEmpty(request.Infver)) + throw new ValidationException("接口版本号不能为空"); + if (string.IsNullOrEmpty(request.OpterType.ToString())) + throw new ValidationException("经办人类别不能为空"); + if (string.IsNullOrEmpty(request.Opter)) + throw new ValidationException("经办人不能为空"); + + if (string.IsNullOrEmpty(request.OpterName)) + throw new ValidationException("经办人姓名不能为空"); + + // 假设 request.InfTime 现在是 DateTime 类型,将其转换为字符串进行判断 + if (string.IsNullOrEmpty(request.InfTime.ToString("yyyyMMddHHmmss"))) + throw new ValidationException("交易时间不能为空"); + if (string.IsNullOrEmpty(request.FixmedinsCode)) + throw new ValidationException("定点医药机构编号不能为空"); + + if (string.IsNullOrEmpty(request.FixmedinsName)) + throw new ValidationException("定点医药机构名称不能为空"); + } + + public IInsuranceBusinessService GetServiceByInfoNo(string infoNo) + { + return _serviceCache.GetOrAdd(infoNo, key => key switch + { + // "1101" => new PersonInfoService(), + // "2101" => new RegistrationService(), + // "2201" => new PreSettlementService(), + // _ => throw new BusinessException($"未知的交易编号: {key}") + }); + } + + //入口方法 + public Response ProcessRequest(InsuranceRequest request) + { + ValidateRequest(request); + ValidateParameterFormat(request); + VerifySignature(request); + + var service = GetServiceByInfoNo(request.Infno); + var result = service.Execute(request); + + return new Response + { + Header = GenerateResponseHeader(request), + Body = EncryptData(result), + Signature = GenerateSignature(result) + }; + } + + // 新增参数格式验证 + public static void ValidateParameterFormat(InsuranceRequest request) + { + // 增强msgid格式校验 + if (!Regex.IsMatch(request.MsgId, @"^\d{12}\d{14}\d{4}$")) + throw new ValidationException("报文ID格式必须为12位机构编号+14位时间+4位流水号"); + + // 条件必填验证 + if (!string.IsNullOrEmpty(request.Input) && string.IsNullOrEmpty(request.InsuplcAdmdvs)) + throw new ValidationException("当交易输入包含人员编号时,参保地医保区划为必填项"); + + // 版本号格式校验 + if (!Regex.IsMatch(request.Infver, @"^V\d+\.\d+$")) + throw new ValidationException("接口版本号格式应为VX.X"); + + // 签名类型校验 + if (!string.IsNullOrEmpty(request.Signtype) && !new[] { "SM2", "SM3" }.Contains(request.Signtype)) + throw new ValidationException("签名类型只支持SM2/SM3"); + + // 由于 request.InfTime 是 DateTime 类型,先将其转换为指定格式的字符串再进行正则匹配 + if (!Regex.IsMatch(request.InfTime.ToString("yyyyMMddHHmmss"), @"^\d{14}$")) + throw new ValidationException("交易时间格式必须为yyyyMMddHHmmss"); + + if (request.OpterName?.Length > 50) + throw new ValidationException("经办人姓名不能超过50个字符"); + } + + // 增强版响应头生成 + public static ResponseHeader GenerateResponseHeader(InsuranceRequest request) + { + return new ResponseHeader + { + Version = "1.0", + // 由于 _config 不存在,这里假设添加一个默认值,实际使用时需要根据业务逻辑修改 + SenderCode = "DefaultInstitutionCode", + ReceiverCode = request.FixmedinsCode, + MsgId = Guid.NewGuid().ToString("N").ToUpper(), + InfTime = DateTime.Now.ToString("yyyyMMddHHmmss"), + ResultCode = "1000", + ResultMsg = "处理成功", + SignatureAlgorithm = "HMAC-SHA256", + EncryptionAlgorithm = "AES-256-GCM" + }; + } + + // 完整签名验证流程 + public static void VerifySignature(InsuranceRequest request) + { + var rawData = $"{request.MsgId}{request.InfTime}{request.FixmedinsCode}"; + + + var hmac = new HMACSHA256(Encoding.UTF8.GetBytes("DefaultSignKey")); + var computedSignature = hmac.ComputeHash(Encoding.UTF8.GetBytes(rawData)); + + // 由于 InsuranceRequest 未包含 Signature 的定义,这里假设添加一个默认空字符串处理 + if (!computedSignature.SequenceEqual(Convert.FromBase64String(""))) + // 由于 SecurityException 可能未找到,使用更通用的 InvalidOperationException 替代 + throw new InvalidOperationException("数字签名验证失败"); + } + + // 医保系统通信模块 + public async Task CallInsuranceSystem(string endpoint, object payload) + { + // 由于 _httpClientFactory 不存在,使用新创建的 HttpClient 实例替代 + using var client = new HttpClient(); + // 由于 _config 不存在,暂时使用一个默认的 API Key,实际使用时需根据业务逻辑修改 + client.DefaultRequestHeaders.Add("X-Medicare-API-Key", "DefaultApiKey"); + + var content = new StringContent( + // 引入 System.Text.Json 命名空间以使用 JsonSerializer + + System.Text.Json.JsonSerializer.Serialize(payload), + Encoding.UTF8, + "application/json"); + + var response = await client.PostAsync(endpoint, content); + response.EnsureSuccessStatusCode(); + + return await response.Content.ReadFromJsonAsync(); + } + + public static string EncryptData(object data) + { + var json = JsonSerializer.Serialize(data); + // 由于 AesUtility 不存在,使用 .NET 内置的 AES 加密实现 + + + // 定义一个临时的 AES 加密方法 + static string EncryptWithAes(string plainText, string key) + { + byte[] encrypted; + using var aesAlg = Aes.Create(); + { + aesAlg.Key = Encoding.UTF8.GetBytes(key); + aesAlg.GenerateIV(); + + ICryptoTransform encryptor = aesAlg.CreateEncryptor(aesAlg.Key, aesAlg.IV); + + using (MemoryStream msEncrypt = new MemoryStream()) + { + using (CryptoStream csEncrypt = new CryptoStream(msEncrypt, encryptor, CryptoStreamMode.Write)) + { + using (StreamWriter swEncrypt = new StreamWriter(csEncrypt)) + { + swEncrypt.Write(plainText); + } + encrypted = msEncrypt.ToArray(); + } + } + + // 将 IV 和加密后的数据拼接,以方便后续解密 + byte[] combinedIvCt = new byte[aesAlg.IV.Length + encrypted.Length]; + Array.Copy(aesAlg.IV, 0, combinedIvCt, 0, aesAlg.IV.Length); + Array.Copy(encrypted, 0, combinedIvCt, aesAlg.IV.Length, encrypted.Length); + + return Convert.ToBase64String(combinedIvCt); + } + } + + // 由于 _config 不存在,使用一个默认的加密密钥替代,实际使用时需根据业务逻辑修改 + return EncryptWithAes(json, "DefaultEncryptionKey"); + } + + public static string GenerateSignature(object data) + { + var json = JsonSerializer.Serialize(data); + // 由于 HMACSHA256.HashData 没有单参数重载,需要创建 HMACSHA256 实例来计算哈希值 + using var hmac = new HMACSHA256(); + { + byte[] hashBytes = hmac.ComputeHash(Encoding.UTF8.GetBytes(json)); + return Convert.ToBase64String(hashBytes); + } + } + } +} diff --git a/YbTest/YbTest.csproj b/YbTest/YbTest.csproj new file mode 100644 index 0000000..075d1bc --- /dev/null +++ b/YbTest/YbTest.csproj @@ -0,0 +1,14 @@ + + + + net8.0 + enable + enable + + + + + + + + diff --git a/YbTest/appsettings.Development.json b/YbTest/appsettings.Development.json new file mode 100644 index 0000000..770d3e9 --- /dev/null +++ b/YbTest/appsettings.Development.json @@ -0,0 +1,9 @@ +{ + "DetailedErrors": true, + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + } +} diff --git a/YbTest/appsettings.json b/YbTest/appsettings.json new file mode 100644 index 0000000..10f68b8 --- /dev/null +++ b/YbTest/appsettings.json @@ -0,0 +1,9 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + }, + "AllowedHosts": "*" +}