12 changed files with 618 additions and 0 deletions
Binary file not shown.
@ -0,0 +1,110 @@
|
||||
using System.ComponentModel.DataAnnotations; |
||||
|
||||
namespace YbTest.Models.Insurance |
||||
{ |
||||
public class InsuranceRequest |
||||
{ |
||||
/// <summary> |
||||
/// 交易类型代码 |
||||
/// [必填] 固定4位数字,参考医保业务代码表 |
||||
/// 示例:1101(人员信息查询) |
||||
/// </summary> |
||||
[Required, StringLength(4)] |
||||
public string Infno { get; set; } |
||||
|
||||
/// <summary> |
||||
/// 交易流水号 |
||||
/// [必填] 30位复合格式:机构编号(12位)+yyyyMMddHHmmss(14位)+4位序列号 |
||||
/// 示例:H44123456202308141023001234 |
||||
/// </summary> |
||||
[Required, StringLength(30)] |
||||
public string MsgId { get; set; } |
||||
|
||||
/// <summary> |
||||
/// 参保地区行政区划代码 |
||||
/// [必填] 6位数字,参考最新医保区划代码 |
||||
/// 示例:440305(南山区) |
||||
/// </summary> |
||||
[Required, StringLength(6)] |
||||
public string MdtrtareaAdmvs { get; set; } |
||||
|
||||
/// <summary> |
||||
/// 参保地行政区划代码 |
||||
/// [条件必填] 6位数字,异地就医时必填 |
||||
/// 示例:440306(宝安区) |
||||
/// </summary> |
||||
[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; } |
||||
|
||||
/// <summary> |
||||
/// 操作员类型 |
||||
/// [必填] 1-医保经办人 2-医院工作人员 3-参保人 |
||||
/// </summary> |
||||
[Required] |
||||
public int OpterType { get; set; } |
||||
|
||||
/// <summary> |
||||
/// 操作员账号 |
||||
/// [必填] 30位医保经办人账号(区域编码+工作人员编号) |
||||
/// 示例:4403052021000123 |
||||
/// </summary> |
||||
[Required, StringLength(30)] |
||||
public string Opter { get; set; } |
||||
|
||||
/// <summary> |
||||
/// 操作员姓名 |
||||
/// [必填] 50位中文实名,与医保系统登记信息一致 |
||||
/// 示例:张三 |
||||
/// </summary> |
||||
[Required, StringLength(50)] |
||||
public string OpterName { get; set; } |
||||
|
||||
/// <summary> |
||||
/// 交易时间 |
||||
/// [必填] ISO 8601格式:yyyy-MM-ddTHH:mm:ss |
||||
/// 示例:2023-08-14T15:30:00 |
||||
/// </summary> |
||||
[Required] |
||||
public DateTime InfTime { get; set; } |
||||
|
||||
/// <summary> |
||||
/// 定点医疗机构编号 |
||||
/// [必填] 12位医保标准编码 |
||||
/// 示例:H441234567890 |
||||
/// </summary> |
||||
[Required, StringLength(12)] |
||||
public string FixmedinsCode { get; set; } |
||||
|
||||
/// <summary> |
||||
/// 定点医疗机构名称 |
||||
/// [必填] 20位中文标准名称 |
||||
/// 示例:深圳市人民医院 |
||||
/// </summary> |
||||
[Required, StringLength(20)] |
||||
public string FixmedinsName { get; set; } |
||||
|
||||
[StringLength(30)] |
||||
public string SignNo { get; set; } |
||||
|
||||
[Required, StringLength(40000)] |
||||
public string Input { get; set; } |
||||
} |
||||
} |
@ -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; } |
||||
} |
||||
} |
@ -0,0 +1,155 @@
|
||||
using System.ComponentModel.DataAnnotations.Schema; |
||||
using System.ComponentModel.DataAnnotations; |
||||
|
||||
namespace YbTest.Models |
||||
{ |
||||
public class MedicalTransactionSystem |
||||
{ |
||||
|
||||
/// <summary> |
||||
/// 医保交易业务对象(入参) |
||||
/// </summary> |
||||
public class MedicalTransaction |
||||
{ |
||||
/// <summary> |
||||
/// 交易编号(详见接口列表) |
||||
/// </summary> |
||||
[Required] |
||||
[StringLength(4)] |
||||
public string Infno { get; set; } |
||||
|
||||
/// <summary> |
||||
/// 发送方报文ID(主键) |
||||
/// 格式:定点医药机构编号(12)+时间(14)+顺序号(4) |
||||
/// </summary> |
||||
[Key] |
||||
[StringLength(30)] |
||||
[RegularExpression(@"^\d{12}\d{14}\d{4}$", ErrorMessage = "报文ID格式无效")] |
||||
public string MsgId { get; set; } |
||||
|
||||
/// <summary> |
||||
/// 就医地医保区划 |
||||
/// </summary> |
||||
[Required] |
||||
[StringLength(6)] |
||||
public string MdtrtareaAdmvs { get; set; } |
||||
|
||||
/// <summary> |
||||
/// 参保地医保区划(人员编号存在时必填) |
||||
/// </summary> |
||||
[StringLength(6)] |
||||
public string InsuplcAdmdvs { get; set; } |
||||
|
||||
/// <summary> |
||||
/// 接收方系统代码 |
||||
/// </summary> |
||||
[Required] |
||||
[StringLength(10)] |
||||
public string RecerSysCode { get; set; } |
||||
|
||||
/// <summary> |
||||
/// 设备编号 |
||||
/// </summary> |
||||
[StringLength(100)] |
||||
public string DevNo { get; set; } |
||||
|
||||
/// <summary> |
||||
/// 设备安全信息(JSON/XML格式) |
||||
/// </summary> |
||||
[Column(TypeName = "text")] |
||||
public string DevSafeInfo { get; set; } |
||||
|
||||
/// <summary> |
||||
/// 数字签名信息(安全管理码) |
||||
/// </summary> |
||||
[Column(TypeName = "text")] |
||||
public string Cainfo { get; set; } |
||||
|
||||
/// <summary> |
||||
/// 签名类型 |
||||
/// </summary> |
||||
[StringLength(10)] |
||||
public SignType Signtype { get; set; } |
||||
|
||||
/// <summary> |
||||
/// 接口版本号 |
||||
/// </summary> |
||||
[Required] |
||||
[StringLength(6)] |
||||
public string Infver { get; set; } |
||||
|
||||
/// <summary> |
||||
/// 经办人类别 |
||||
/// </summary> |
||||
[Required] |
||||
public OperatorType OpterType { get; set; } |
||||
|
||||
/// <summary> |
||||
/// 经办人/终端编号 |
||||
/// </summary> |
||||
[Required] |
||||
[StringLength(30)] |
||||
public string Opter { get; set; } |
||||
|
||||
/// <summary> |
||||
/// 经办人姓名/终端名称 |
||||
/// </summary> |
||||
[Required] |
||||
[StringLength(50)] |
||||
public string OpterName { get; set; } |
||||
|
||||
/// <summary> |
||||
/// 交易时间 |
||||
/// </summary> |
||||
[Required] |
||||
public DateTime InfTime { get; set; } |
||||
|
||||
/// <summary> |
||||
/// 定点医药机构编号 |
||||
/// </summary> |
||||
[Required] |
||||
[StringLength(12)] |
||||
public string FixmedinsCode { get; set; } |
||||
|
||||
/// <summary> |
||||
/// 定点医药机构名称 |
||||
/// </summary> |
||||
[Required] |
||||
[StringLength(20)] |
||||
public string FixmedinsName { get; set; } |
||||
|
||||
/// <summary> |
||||
/// 交易签到流水号(来自9001交易) |
||||
/// </summary> |
||||
[StringLength(30)] |
||||
public string SignNo { get; set; } |
||||
|
||||
/// <summary> |
||||
/// 交易输入(.NET对象序列化) |
||||
/// </summary> |
||||
[Required] |
||||
[Column(TypeName = "text")] |
||||
public string Input { get; set; } |
||||
} |
||||
|
||||
/// <summary> |
||||
/// 签名类型枚举 |
||||
/// </summary> |
||||
public enum SignType |
||||
{ |
||||
SM2, |
||||
SM3 |
||||
} |
||||
|
||||
/// <summary> |
||||
/// 经办人类别枚举 |
||||
/// </summary> |
||||
public enum OperatorType |
||||
{ |
||||
经办人 = 1, |
||||
自助终端 = 2, |
||||
移动终端 = 3 |
||||
} |
||||
|
||||
} |
||||
} |
@ -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(); |
@ -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" |
||||
} |
||||
} |
||||
} |
||||
} |
@ -0,0 +1,17 @@
|
||||
using YbTest.Models.Insurance; |
||||
|
||||
|
||||
namespace YbTest.Services.InsuranceBusinessServices; |
||||
|
||||
public interface IInsuranceBusinessService |
||||
{ |
||||
/// <summary> |
||||
/// 执行医保业务请求 |
||||
/// </summary> |
||||
Response Execute(InsuranceRequest request); |
||||
|
||||
/// <summary> |
||||
/// 业务校验方法 |
||||
/// </summary> |
||||
void ValidateBusinessRule(InsuranceRequest request); |
||||
} |
@ -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<string, IInsuranceBusinessService> _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<Response> 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<Response>(); |
||||
} |
||||
|
||||
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); |
||||
} |
||||
} |
||||
} |
||||
} |
@ -0,0 +1,14 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk.Web"> |
||||
|
||||
<PropertyGroup> |
||||
<TargetFramework>net8.0</TargetFramework> |
||||
<Nullable>enable</Nullable> |
||||
<ImplicitUsings>enable</ImplicitUsings> |
||||
</PropertyGroup> |
||||
|
||||
<ItemGroup> |
||||
<Folder Include="Controllers\" /> |
||||
<Folder Include="Services\" /> |
||||
</ItemGroup> |
||||
|
||||
</Project> |
@ -0,0 +1,9 @@
|
||||
{ |
||||
"DetailedErrors": true, |
||||
"Logging": { |
||||
"LogLevel": { |
||||
"Default": "Information", |
||||
"Microsoft.AspNetCore": "Warning" |
||||
} |
||||
} |
||||
} |
Loading…
Reference in new issue