.NET架构小技巧(4)——反射,架构人员法宝II
上一篇博文中,利用属性反射的特点,用两个方法完成了字符转实体,实体转字符的工作,但有些复杂的场景,上面方法就没那么好用了,就需要更复杂的方式来组装处理。
先来看一个接口文档,下面是接口的调用方式
long OltpTransData(unsigned long msgType,unsigned long packageType,
unsigned long packageLength,char *str,LPTSTR com);
I.msgType:业务请求类型;
II.packageType:数据解析格式类型,系统重组数据时使用
III.packageLength:数据串的长度;
IV. str:数据串;调用时,通过数据串传入参数;函数返回时,数据串中包含返回的数据,数据按字符串方式组合,并且在字符串第一位保留一个空格;另外,除了16位日期右补空格,其他的字段均以左空格补位
V. com:数据请求串口
业务请求类型 |
数据解析格式类型 |
数据串最小长度 |
说明 |
1001 |
101 |
126 |
实时验卡(读卡、验卡) |
1002 |
12 |
610 |
实时结算 |
1003 |
7 |
291 |
实时明细数据传输 |
1004 |
9 |
253 |
实时住院登记数据传输 |
1005 |
8 |
309 |
实时医嘱数据传输 |
1006 |
12 |
610 |
实时结算预算 |
1007 |
2 |
1101 |
实时住院首次病程记录传输 |
1009 |
504 |
85 |
医师信息查询 |
1010 |
510 |
331 |
出入院标准传输 |
读卡
序号 |
定义 |
数据原型 |
起始位置 |
数据长度 |
备注 |
数据填充 |
1 |
个人编号 |
CHAR |
1 |
8 |
医保编号,以’E’开头的为异地社保卡,详见说明 |
院端 |
2 |
姓名 |
CHAR |
9 |
20 |
中心 |
|
3 |
身份证号 |
CHAR |
29 |
18 |
18位或15位 |
中心 |
4 |
IC卡号 |
CHAR |
47 |
9 |
院端 |
|
5 |
治疗序号 |
NUM |
56 |
4 |
中心 |
|
6 |
职工就医类别 |
CHAR |
60 |
1 |
A在职、B退休、L事业离休、T特诊、Q企业离休、E退老、N农民工、X未成年居民、O老年居民(老年居民、低收入人员、残疾人)、D低保人员、S 三无人员、U 大学生 |
中心 |
7 |
基本个人帐户余额 |
NUM |
61 |
10 |
中心 |
|
8 |
补助个人帐户余额 |
NUM |
71 |
10 |
现用于公务员单独列帐 |
中心 |
9 |
统筹累计 |
NUM |
81 |
10 |
中心 |
|
10 |
门诊慢病统筹累计 |
NUM |
91 |
10 |
中心 |
|
11 |
月缴费基数 |
NUM |
101 |
10 |
月缴费工资 |
中心 |
12 |
帐户状态 |
CHAR |
111 |
1 |
A正常、B半止付、C全止付、D销户 |
中心 |
13 |
参保类别1 |
CHAR |
112 |
1 |
是否享受高额: 0 不享受高额、1 享受高额、2 医疗保险不可用 |
中心 |
14 |
参保类别2 |
CHAR |
113 |
1 |
是否享受补助(商业补助、公务员补助):0 不享受、1 商业、2 公务员、3事业离休差额拨款人员 |
中心 |
15 |
参保类别3 |
CHAR |
114 |
1 |
0 企保、1 事保、2企业慢病、3事业慢病、4异地就医,详见说明 |
中心 |
16 |
参保类别4 |
CHAR |
115 |
1 |
0或2生育不可用、1或3生育可用 |
中心 |
17 |
参保类别5 |
CHAR |
116 |
1 |
0工伤不可用、1工伤可用 |
中心 |
18 |
住院次数累计 |
NUM |
117 |
4 |
中心 |
|
19 |
家床次数累计 |
NUM |
121 |
4 |
中心 |
查询医师
序号 |
定义 |
数据原型 |
起始位置 |
数据长度 |
备注 |
填写方式 |
1 |
医师编码 |
CHAR |
1 |
8 |
院端 |
|
2 |
医师姓名 |
CHAR |
9 |
20 |
中心 |
|
3 |
身份证号 |
CHAR |
29 |
18 |
中心 |
|
4 |
可出诊医院编号 |
CHAR |
47 |
20 |
详见说明 |
中心 |
5 |
终止日期 |
DATETIME |
67 |
16 |
YYYYMMDDHHMMSS |
中心 |
6 |
有效标志 |
CHAR |
83 |
1 |
‘0’无效,‘1’有效 |
中心 |
这个接口是拼接方式,每个数据项都有固定的长度,有些数据也有固定的格式,比如日期,还有很多类型,都是有固定值固定含义的。这种情况下需要更多的信息来告诉两个转换方法该怎么转换,这里就引出了Attribute——一个专门来给类或属性方法加特征的知识点。
/// <summary>
/// 发送报文类型
/// </summary>
[AttributeUsage(AttributeTargets.Class)]
public class PackageTypeAttribute : Attribute
{
/// <summary>
/// 发关报文实体类属性特性类
/// </summary>
/// <param name="SN">属性的序号,从1开始</param>
/// <param name="Length">属性转成字符串后长度</param>
public PackageTypeAttribute(uint OperationType, uint DataFormaterType, uint MinLength)
{
this.OperationType = OperationType;
this.DataFormaterType = DataFormaterType;
this.MinLength = MinLength;
}
/// <summary>
/// 业务请求类型
/// </summary>
public uint OperationType
{ get; private set; }
/// <summary>
/// 数据解析格式类型
/// </summary>
public uint DataFormaterType
{ get; private set; }
/// <summary>
/// 数据串最小长度
/// </summary>
public uint MinLength
{ get; private set; }
}
/// <summary>
/// 报文中属性的顺序SN和长度Length
/// </summary>
[AttributeUsage(AttributeTargets.Property)]
public class PackageAttribute : Attribute
{
/// <summary>
/// 序号,从1开始
/// </summary>
public int SN
{ get; private set; }
/// <summary>
/// 转成字符串后的长度
/// </summary>
public int Length
{ get; private set; }
/// <summary>
/// 发关报文实体类属性特性类
/// </summary>
/// <param name="SN">属性的序号,从1开始</param>
/// <param name="Length">属性转成字符串后长度</param>
public PackageAttribute(int SN, int Length)
{
this.SN = SN;
this.Length = Length;
}
/// <summary>
/// 是否是时间类型,因为时间类型是左对齐,右补空格
/// </summary>
public bool IsDateTime
{ get; set; }
}
/// <summary>
/// 取枚举的值还是
/// </summary>
[AttributeUsage(AttributeTargets.Enum)]
public class EnumValeuNumberAttribute : Attribute
{
/// <summary>
/// 是否把枚举类型属性的的值数转成Char类型
/// </summary>
public bool IsChar
{ get; set; }
}
定义了三个特性类,PackageTypeAttribute主用来区分不同的交易类型,从实体类上获取不同交易的函数参数;PackageAttribute是在实体类的属性上的,是核心特性类,它标记了属性的序号,和每个属性的长度,和属性是否是DateTime类型;EnumValeuNumberAttribute是用来专门处理枚举类型的属性的。
/// <summary>
/// 医师信息查询
/// </summary>
[PackageType(1009, 504, 85)]
public class DoctorQuery : Entity
{
/// <summary>
/// 医师编码
/// </summary>
[Package(1, 8)]
public virtual String DoctorCode
{
get; set;
}
/// <summary>
/// 医师姓名
/// </summary>
[Package(2, 20)]
public virtual String DoctorName
{ get; set; }
/// <summary>
/// 身份证号
/// </summary>
[Package(3, 18)]
public virtual string PersonID
{ get; set; }
///编号为0002的医院, 下属有编号为0113的定点, 在总院注册登记的医师可以在这样的2家医院出诊, 则“可出诊医院编号”为00020113,若长度不足20位则前补空格。
/// <summary>
/// 可出诊医院编号
/// </summary>
[Package(4, 20)]
public virtual string CanVisitHospitalCode
{ get; set; }
/// <summary>
/// 终止日期
/// </summary>
[Package(5, 16, IsDateTime = true)]
public virtual string TerminationTime
{ get; set; }
/// <summary>
/// 有效标志
/// </summary>
[Package(6, 1)]
public virtual DLYBAvailableMarker DLYBAvailableMarker
{ get; set; }
}
/// <summary>
/// 有效标志
/// </summary>
[EnumValeuNumber]
public enum DLYBAvailableMarker
{
/// <summary>
/// 无效
/// </summary>
nullity = 0,
/// <summary>
/// 有效
/// </summary>
Valid = 1
}
/// <summary>
/// 实时验卡(读卡、验卡)
/// </summary>
[PackageType(1001, 101, 126)]
public class QueryCardEntity : Entity
{
/// <summary>
/// 个人编号
/// </summary>
[Package(1, 8)]
public virtual string PersonNumber
{ get; set; }
/// <summary>
/// 姓名
/// </summary>
[Package(2, 20)]
public virtual string Name
{ get; set; }
/// <summary>
/// 身份证号
/// </summary>
[Package(3, 18)]
public virtual string PersonID
{ get; set; }
/// <summary>
/// IC卡号
/// </summary>
[Package(4, 9)]
public virtual string ICCardNumber
{ get; set; }
long therapyNumber;
/// <summary>
/// 治疗序号
/// </summary>
[Package(5, 4)]
public virtual long TherapyNumber
{
get
{
return therapyNumber;
}
set
{
if (value >= 0 && value <= 9999)
{
therapyNumber = value;
}
else
{
throw new Exception("治疗号在0-9999之间");
}
}
}
/// <summary>
/// 职工就医类别
/// </summary>
[Package(6, 1)]
public virtual string TherapyCategory
{ get; set; }
/// <summary>
/// 基本个人帐户余额
/// </summary>
[Package(7, 10)]
public virtual decimal BasePersonBalance
{ get; set; }
/// <summary>
/// 补助个人帐户余额
/// </summary>
[Package(8, 10)]
public virtual decimal SubsidyPersonBalance
{ get; set; }
/// <summary>
/// 统筹累计
/// </summary>
[Package(9, 10)]
public virtual decimal PlannerTotal
{ get; set; }
/// <summary>
/// 门诊慢病统筹累计///新
/// </summary>
[Package(10, 10)]
public virtual decimal MZSlowDisease
{ get; set; }
/// <summary>
/// 月缴费基数
/// </summary>
[Package(11, 10)]
public virtual decimal BaseFeeByMonth
{ get; set; }
/// <summary>
/// 帐户状态
/// </summary>
[Package(12, 1)]
public virtual string AccoutState
{ get; set; }
/// <summary>
/// 参保类别1
/// </summary>
[Package(13, 1)]
public virtual string InsuranceCategory1
{ get; set; }
/// <summary>
/// 参保类别2
/// </summary>
[Package(14, 1)]
public virtual string InsuranceCategory2
{ get; set; }
/// <summary>
/// 参保类别3
/// </summary>
[Package(15, 1)]
public virtual string InsuranceCategory3
{ get; set; }
/// <summary>
/// 参保类别4
/// </summary>
[Package(16, 1)]
public virtual string InsuranceCategory4
{ get; set; }
/// <summary>
/// 参保类别5
/// </summary>
[Package(17, 1)]
public virtual string InsuranceCategory5
{ get; set; }
/// <summary>
/// 住院次数累计////新
/// </summary>
[Package(18, 4)]
public virtual int ZYAddNumber
{ get; set; }
/// <summary>
/// 家床次数累计////新
/// </summary>
[Package(19, 4)]
public virtual int AddBedNumber
{ get; set; }
}
上面的实体类分别使用了特性类,参照文档就OK。
public static class StringExtension
{
/// <summary>
/// 右边不够长度补空格,汉字算两个空格
/// </summary>
/// <param name="str"></param>
/// <param name="length">设定长度</param>
/// <returns></returns>
public static string ChineseCharacterLeft(this string str, int length)
{
var len = Encoding.Default.GetBytes(str).Length;
if (len < length)
{
for (int i = 0; i < length - len; i++)
{
str = " " + str;
}
}
return str;
}
/// <summary>
/// 右边不够长度补空格,汉字算两个空格
/// </summary>
/// <param name="str"></param>
/// <param name="length">设定长度</param>
/// <returns></returns>
public static string ChineseCharacterRight(this string str, int length)
{
var len = Encoding.Default.GetBytes(str).Length;
if (len < length)
{
for (int i = 0; i < length - len; i++)
{
str += " ";
}
}
return str;
}
/// <summary>
/// 切除字符串
/// </summary>
public static string ChineseCharacterSubstring(this string str, int length, out string remaining)
{
var arr = Encoding.Default.GetBytes(str);
var barr = arr.Take(length).ToArray();
var valuestr = Encoding.Default.GetString(barr);
barr = arr.Skip(length).ToArray();
remaining = Encoding.Default.GetString(barr); ;
return valuestr;
}
}
上面代码是对某些属性的对齐方式作了处理。
/// <summary>
/// 报文类的父类
/// </summary>
public abstract class Entity
{
/// <summary>
/// 组装发送报文格式
/// </summary>
/// <returns></returns>
public override string ToString()
{
var pros = this.GetType().GetProperties();
var sortPro = new SortedList<int, PropertyInfo>();
foreach (var pro in pros)
{
foreach (var att in pro.GetCustomAttributes(false))
{
if (att is PackageAttribute)
{
var packageAtt = att as PackageAttribute;
sortPro.Add(packageAtt.SN, pro);
}
}
}
var content = new StringBuilder();
#region 组合发送字符串
//遍历属性
foreach (var pro in sortPro)
{
//遍历属性上的特性
foreach (var att in pro.Value.GetCustomAttributes(false))
{
//判断是否为自定义的PackageAttribute类型
if (att is PackageAttribute)
{
//转换属性上的特性类
var packageAtt = att as PackageAttribute;
//取拼接时字符长度
var length = packageAtt.Length;
//取属性的值
var proValue = pro.Value.GetValue(this, new Object[0]);
//对decimal作处理
if (pro.Value.PropertyType.Name.ToLower() == "decimal")
{
proValue = Math.Round(Convert.ToDecimal(proValue), 2);
if (Encoding.Default.GetByteCount(proValue.ToString()) > length)
{
proValue = "0";
}
}
//判断字符串长度过长
if (proValue != null && (pro.Value.PropertyType.Name.ToLower() == "string"))
{
if (System.Text.Encoding.Default.GetBytes(proValue.ToString()).Length > length)
{
throw new Exception(string.Format("属性{0}的值{1},长度超过{2}", pro.Value.Name, proValue, length));
}
}
//如果值为非空
if (proValue != null)
{
//日期是右补空格,其他是左补空格
if (!packageAtt.IsDateTime)
{
//这里注册,有些属性是枚举类型,有些属性拼接枚举的值,有些取枚举值对应的枚举数值,这里是从该属性类型上的EnumValeuNumberAttribute特性的IsValue属性来判断的,IsValue为true,就取枚举的值,为false取该值对应的枚举数
if (pro.Value.PropertyType.IsEnum)
{
foreach (var eatt in pro.Value.PropertyType.GetCustomAttributes(false))
{
if (eatt is EnumValeuNumberAttribute)
{
var enumVaNu = eatt as EnumValeuNumberAttribute;
if (enumVaNu.IsChar)
{
var enumNumber = ((char)(int)Enum.Parse(pro.Value.PropertyType, proValue.ToString())).ToString();
content.Append(enumNumber.ChineseCharacterLeft(length));
}
else
{
var enumNumber = ((int)Enum.Parse(pro.Value.PropertyType, proValue.ToString())).ToString();
content.Append(enumNumber.ChineseCharacterLeft(length));
}
}
}
}
else
{
content.Append(proValue.ToString().ChineseCharacterLeft(length));
}
}
else//日期类型右补空格
{
content.Append(proValue.ToString().ChineseCharacterRight(length));
}
}
else
{
content.Append("".ChineseCharacterLeft(length));
}
}
}
}
#endregion
return content.ToString();
}
/// <summary>
/// 把一个字符串转成一个对象
/// </summary>
/// <param name="content"></param>
/// <returns></returns>
public Entity ToEntity(Type entityType,string content)
{
var pros = entityType.GetProperties();
//按照特性类上的SN序号把属性名存入集合proPackageList中
List<PropertyInfo> proPackageList = new List<PropertyInfo>(pros.Length);
//初始化属性集合
for (int i = 0; i < pros.Length; i++)
{
foreach (var att in pros[i].GetCustomAttributes(false))
{
if (att is PackageAttribute)
{
proPackageList.Add(null);
break;
}
}
}
//按属性顺序排列属性
foreach (var pro in pros)
{
foreach (var att in pro.GetCustomAttributes(false))
{
if (att is PackageAttribute)
{
var packageAtt = att as PackageAttribute;
var index = packageAtt.SN - 1;
proPackageList[index] = pro;
}
}
}
//创建实体对象
var constructor = entityType.GetConstructor(new Type[0]);
var entity = constructor.Invoke(new object[0]);
foreach (var pro in proPackageList)
{
//遍历属性上的特性
foreach (var att in pro.GetCustomAttributes(false))
{
//判断是否为自定义的PackageAttribute类型
if (att is PackageAttribute)
{
//转换属性上的特性类
var packageAtt = att as PackageAttribute;
var length = packageAtt.Length;
var valuestr = content.ChineseCharacterSubstring(length, out content).Trim();
if (pro.PropertyType.IsEnum)
{
foreach (var eatt in pro.PropertyType.GetCustomAttributes(false))
{
if (eatt is EnumValeuNumberAttribute)
{
var eat = eatt as EnumValeuNumberAttribute;
if (eat.IsChar)
{
var chr = Convert.ToChar(valuestr);
var value = Convert.ChangeType(Enum.Parse(pro.PropertyType, ((int)chr).ToString()), pro.PropertyType);
pro.SetValue(entity, value, null);
}
else
{
var value = Convert.ChangeType(Enum.Parse(pro.PropertyType, valuestr), pro.PropertyType);
pro.SetValue(entity, value, null);
}
break;
}
}
}
else
{
var value = Convert.ChangeType(valuestr, pro.PropertyType);
pro.SetValue(entity, value, null);
}
}
}
}
return (Entity)entity;
}
}
这两个方法核心里通过反射属性上的特性,取特性中定义的固定值,来生成接口要求的字符串,合理的设计特性,可以使两个转换方法更优雅,更简便,在开发过程中,也需要不断调整理,适配,逐渐完善。
可以用下面的代码完成测试
using System;
namespace ArchitectureDemo04
{
class Program
{
static void Main(string[] args)
{
var backQueryCard = Send(new QueryCardEntity { PersonNumber = "0000001", ICCardNumber = "C00000001" });
var backDoctorQuery = Send(new DoctorQuery { DoctorCode = "0001" });
}
/// <summary>
/// 发送
/// </summary>
/// <param name="entity"></param>
/// <returns></returns>
static Entity Send(Entity entity)
{
try
{
foreach (var att in entity.GetType().GetCustomAttributes(false))
{
if (att is PackageTypeAttribute)
{
var attPackage = att as PackageTypeAttribute;
Console.WriteLine($"入参:");
Console.WriteLine(entity);
Console.WriteLine("模拟函数调用:");
Console.WriteLine($"OltpTransData({attPackage.OperationType},{attPackage.DataFormaterType},{attPackage.MinLength},{entity})");
var backContent = BackOperation(entity);
var backEntity = entity.ToEntity(entity.GetType(),backContent);
return backEntity;
}
}
return null;
}
catch
{
throw;
}
}
/// <summary>
/// 模拟医保中心返回
/// </summary>
/// <param name="entity">参数</param>
/// <returns></returns>
static string BackOperation(Entity entity)
{
switch (entity.GetType().Name)
{
case "QueryCardEntity":
return " 0000001 Jack210213198411113111C00000001 1A 1000.66 0 0 0 1800A00131 0 0";
case "DoctorQuery":
return " 0001 DcotorLi210211198707182233 0002011320201029190850 1";
}
return null;
}
}
}
《asp.net core精要讲解》 https://ke.qq.com/course/265696
《asp.net core 3.0》 https://ke.qq.com/course/437517
《asp.net core项目实战》 https://ke.qq.com/course/291868
《基于.net core微服务》 https://ke.qq.com/course/299524