wenmo8 发布的文章

在我们Winform开发中,往往需要涉及到附件的统一管理,因此我倾向于把它们独立出来作为一个附件管理模块,这样各个模块都可以使用这个附件管理模块,更好的实现模块重用的目的。在涉及附件管理的场景中,一个数据记录可能对应多个附件组场景,每个附件组则涉及附件多个文件,往往这些附件可能放置的目录会有所不同,导致如果对文件备份管理不是很方便。本篇随笔主要基于这个考虑,对附件文件进行一个集中归档的处理,以便把散落在各个地方的附件集中放置在我们制定的归档目录中,而且并不影响已有附件的关联显示。

1、附件的界面管理

我们在做数据记录存储的时候,一个记录可能需要存储多个场景下的附件信息,这些维护查看附件的界面可以做成相对比较统一的控件方式进行展示,这样我们使用起来就比较省事一些。

样在项目中集成(如数据编辑窗口),直接拖动这个控件到界面中,运行就可以看到下面的效果了。

在实际的项目中,如客户信息,病人信息等记录,可能对应多个附件记录,那么我们把它们分组进行管理即可。

附件管理在很多信息化管理系统中很普遍使用,例如我在病人管理系统界面里面,可以在一个界面里面分门别类管理很多影像学的图片资料,通过查看附件,可以看到其中一些图片附件的缩略图,需要进一步查看,可以双击图片即可实现预览效果。

附件管理的上传界面如下所示,可以从本地选择多个文件一并上传,每个文件上传成功后立即显示状态。

以上就是一些附件管理和使用的界面,这些界面有助于我们针对性的对特定的附件进行查看、维护管理。

2、附件的归档管理

附件的处理,我们通常是把它上传到指定的目录下面,这个目录可以是配置好的特定目录,不过随着程序的运行,这个目录的附件会越来越多,更加难以管理,对于一些病历图片、视频信息这些比较重要的资料,我们往往是需要进行归档备份起来,以便在某个数据灾难后进行恢复,把损失降到最少。

归档文件还有一个作用,就是我们可以以资源管理器的方式同一打开一个目录,然后在里面寻找或者查看该记录相关的所有附件信息,这种比在某个界面附件分组中寻找指定的附件,提供更广范围的指引。

上图就是在实际项目中,需要直接打开某个特定目录的文件,然后查找这个目录下的文件或者打开查看详细信息。这里可能会问,单机版可以直接打开本地目录,如果是混合框架,数据和文件集中在云端服务器的,你如何打开对应的目录文件呢?

这里其实我们普通的Winform程序是可以直接打开本地文件,如果是使用Web API或者WCF通讯的混合框架模式,则是通过文件路径的方式,从服务器端检索对应的文件列表信息,然后进行呈现的,因此就需要在附件管理模块提供对应的接口来获取文件信息了。

下面我们来介绍一下,归档处理的实现,以及通过文件路径获取对应文件列表的处理。

由于病历附件这里我做成了一个用户控件的方式,方便各个模块重用的,因此归档的方式我做了一个功能按钮,并通过事件的方式给外部实现逻辑。

我们通过定义事件的方式来定义这个处理,然后外部可以实现这个事件即可。

按钮的事件处理,就是调用这个我们自定义的事件处理即可。

        private void btnArchive_Click(objectsender, EventArgs e)
{
if (OnArchived != null)
{
string tips = "您确认需要进行图片归档处理吗?";if (MessageDxUtil.ShowYesNoAndTips(tips) ==DialogResult.Yes)
{
//执行归档处理 OnArchived(this.info);
}
}
}

而编辑的窗体,我们显示数据的时候,初始化这个事件的处理即可。

        /// <summary>
        ///数据显示的函数/// </summary>
        public override voidDisplayData()
{
InitDictItem();
//数据字典加载(公用) BindPatient();//绑定显示病人资料//绑定数据并显示 attchmentControl.BindData(PatientInfo.ID);
attchmentControl.OnArchived
+=AttchmentControl_OnArchived;
attchmentControl.Dock
=DockStyle.Fill;this.pageAttachment.Controls.Clear();this.pageAttachment.Controls.Add(attchmentControl);

BindData();
}

而对于处理归档的事件,主要就是我们确定好归档的目录(部分信息),然后交给服务器端处理进行归档即可。这里确定目录的信息,有患者姓名和诊断部分信息组成。

而服务端的最终处理逻辑,也就是根据对应附件的ID,然后找出对应的文件,移动原来文件到新的目录里面,并更新数据库记录中的附件路径信息即可。

....

为了实现对文件路径的文件检索和下载指定文件,我们需要扩展一下原来附件管理通过ID获取和下载文件的方式,原来的是从数据库获取文件路径,并打开下载文件;而扩展后可以通过路径方式获取文件信息。

最近一直在思考如何对Web API的其接口数据进行独立开发的问题,随着Web API的越来越广泛应用,很多开发也要求前端后端分离,例如统一的Web API接口后,Winform团队、Web前端团队、微信小程序或者APP团队大家可以同步开发,在最初约定一些接口的输入JSON数据和输出JSON数据,但是随着项目的进度开展,这些数据结构一直有所变化,那么我们模拟的JSON数据格式也需要协同变化,但是很不幸既然大家忙着开发,接口协调的事情肯定优先级没那么高,即使每次记得协调接口数据,也不一定能够完全一致。

在Web API端也就是后端没有完全做好的情况下,我们不可能给出所有接口的输入输出JSON格式,那么我们是否可以考虑采用折中模拟数据接口的方式来提供,并且能够提供比较易读、易维护的处理方式呢?答案是肯定有的,结合RAP2和Mock.js两者的特点,可以较为方便的模拟出我们需要接口的URL、输入参数、输出参数等信息。

RAP2是一个Web接口管理工具,开源免费,接口自动化,MOCK数据自动生成,自动化测试,企业级管理。

Mock.js的出现解决了前端离开后端就没法请求数据的问题,但也出现了新的问题,就是在切换生产环境的时候,模拟的数据可能和接口又发生不同步的情况。

RAP2是一个新的解决方案,将前端后端拉倒一个团队仓库中,共享一个仓库,无论是URL地址,还是请求需要的参数,在团队仓库中双方都可以管理,并且可以记录团队成员修改了哪些接口,整合到生产环境的时候,可以比较完美的切换,同步性较好!

相关网站资源:

1、RAP2创建接口

打开网站
http://rap2.taobao.org/
,注册一个用户账号,并创建自己的模块和接口,如下所示

默认有一个示例接口指引我们做相关的参数或者响应内容

对每个参数,都可以使用Mock规则进行生成值,如下界面所示

除了示例,我们也可以创建自己的模块和接口,如下所示我们创建一个用户管理的模块,以及两个接口。

获取用户的接口定义如下所示。

这个URL地址是我们逻辑上的相对地址,实际在使用的时候,我们加上一个基地址就可以了,在RAP2 它也是一个相对地址,测试接口的地址也是加上一个基础地址,这样我们实际开发的时候,把这部分基础地址替换为生产环境的地址即可顺利切换过去了。

这个接口实际返回的JSON数据格式就是我们约定好的JSON格式了,如果我们有变化,大家参考这个中间的平台就可以了,沟通时比较方便,也可以对比那些字段参数变化调整了,这样前后端就有了一个参考的中间平台,一致性较好。

2、接口参数定义和Mock变量使用

那么这些JSON数据是如何来的,我们其实是填写了一些变量,让它自动生成的数据的,这个也是Mock.js的功劳吧。RAP2集成了MockJS的很多变量使用,参考着使用就可以构建很多不一样的数据,非常方便。

下面列出我的一些输入、输入的变量内容。

输入的参数,定义如下:

响应的输出参数如下定义。

获得的输出自动生成的JSON数据如下所示。

这个就是我们使用Mock变量的好处,自动根据规则填充,上面我使用了各种变量来定义数据,如果你需要了解Mock的变量,可以结合Mock的变量来测试即可。具体参考下面说明。

http://mockjs.com/examples.html

上面链接列出了Mock的两项定义:数据模板定义和数据占位符定义,可以根据情况用不同的定义。我更侧向于使用后者,方便快捷。

RAP2给前后端开发者一种更好更透明的协同开发方式,一个负责维护接口、一个负责参考修改调用,大家相互配合,其乐融融。
关于RAP2的功能好像还有很多,不过我们使用它最主要的目的是解决模拟API接口和协同性的问题,这个大原则是它的主要目的,基于RAP2和Mock.js的结合,我们可以解放频繁沟通而得不到接口一致性问题的问题,从而实现更好从调试环境到生产环境的切换。

甚至更好的方案是我们 在Web API接口开发的初期,先花点功夫来定义好所有的接口输入输出,整理一个大家认为比较好的接口规范后,再开始Web API的编码,这样肯定后期开发是事半功倍,而且前端也可以很快对接。

自从微信企业号升级到企业微信后,一些API接口和处理方式有所变化,企业微信支持更多、更丰富的接口,本篇在改造企业微信过程中对相关内容的更新进行记录,希望大家对企业微信的各个方面有所了解。本篇基于对企业微信菜单的管理进行介绍,包括介绍企业微信菜单涉及的几个重要参数,以及后台统一处理公众号和企业微信菜单的处理。

1、菜单管理接口

关于企业微信的前身企业号菜单的管理,在之前随笔有所介绍《
C#开发微信门户及应用(20)-微信企业号的菜单管理
》,基本上企业微信的接口没有太大变化,不过原先的企业号仅仅支持Click和View两种菜单类型,企业微信则拓展了更多的类型支持,详细的企业微信菜单接口,可以参考官方链接地址:
https://work.weixin.qq.com/api/doc#90000/90135/90231
,菜单支持类型如下介绍所示。

自定义菜单接口可实现多种类型按钮,如下:

字段值 功能名称 说明
click 点击推事件 成员点击click类型按钮后,企业微信服务器会通过消息接口推送消息类型为event 的结构给开发者(参考消息接口指南),并且带上按钮中开发者填写的key值,开发者可以通过自定义的key值与成员进行交互;
view 跳转URL 成员点击view类型按钮后,企业微信客户端将会打开开发者在按钮中填写的网页URL,可与网页授权获取成员基本信息接口结合,获得成员基本信息。
scancode_push 扫码推事件 成员点击按钮后,企业微信客户端将调起扫一扫工具,完成扫码操作后显示扫描结果(如果是URL,将进入URL),且会将扫码的结果传给开发者,开发者可用于下发消息。
scancode_waitmsg 扫码推事件 且弹出“消息接收中”提示框 成员点击按钮后,企业微信客户端将调起扫一扫工具,完成扫码操作后,将扫码的结果传给开发者,同时收起扫一扫工具,然后弹出“消息接收中”提示框,随后可能会收到开发者下发的消息。
pic_sysphoto 弹出系统拍照发图 弹出系统拍照发图 成员点击按钮后,企业微信客户端将调起系统相机,完成拍照操作后,会将拍摄的相片发送给开发者,并推送事件给开发者,同时收起系统相机,随后可能会收到开发者下发的消息。
pic_photo_or_album 弹出拍照或者相册发图 成员点击按钮后,企业微信客户端将弹出选择器供成员选择“拍照”或者“从手机相册选择”。成员选择后即走其他两种流程。
pic_weixin 弹出企业微信相册发图器 成员点击按钮后,企业微信客户端将调起企业微信相册,完成选择操作后,将选择的相片发送给开发者的服务器,并推送事件给开发者,同时收起相册,随后可能会收到开发者下发的消息。
location_select 弹出地理位置选择器 成员点击按钮后,企业微信客户端将调起地理位置选择工具,完成选择操作后,将选择的地理位置发送给开发者的服务器,同时收起位置选择工具,随后可能会收到开发者下发的消息。

目前菜单的接口也依旧是原先的几个接口,包括获取菜单、创建菜单、删除菜单三个接口。

由于菜单模块处理,在企业微信、公众号、小程序等应用上,他们的JSON数据结构信息是完全一样的,不同的是提交各自的接口地址而已,因此菜单模块的实体类信息是公用的,定义的实体类信息如下所示。

    /// <summary>
    ///菜单基本信息(公众号、企业微信公用)/// </summary>
    public classMenuJson : BaseJsonResult
{
/// <summary> ///菜单的名字。不能为空,主菜单不能超过16字节,子菜单不能超过40字节。/// </summary> public string name { get; set; }/// <summary> ///按钮类型(click,view,scancode_push,scancode_waitmsg,pic_sysphoto,pic_photo_or_album,pic_weixin,location_select)/// </summary> [JsonProperty(NullValueHandling =NullValueHandling.Ignore)]public string type { get; set; }/// <summary> ///菜单KEY值,用于消息接口推送,不超过128字节/// </summary> [JsonProperty(NullValueHandling =NullValueHandling.Ignore)]public string key { get; set; }/// <summary> ///网页链接,成员点击菜单可打开链接,不超过1024字节。为了提高安全性,建议使用https的url/// </summary> [JsonProperty(NullValueHandling =NullValueHandling.Ignore)]public string url { get; set; }/// <summary> ///media_id类型和view_limited类型必须,调用新增永久素材接口返回的合法media_id/// </summary> [JsonProperty(NullValueHandling =NullValueHandling.Ignore)]public string media_id { get; set; }/// <summary> ///子按钮数组,按钮个数应为2~5个/// </summary> [JsonProperty(NullValueHandling =NullValueHandling.Ignore)]public List<MenuJson> sub_button { get; set; }/// <summary> ///默认构造函数/// </summary> publicMenuJson() { }/// <summary> ///参数化构造函数,用于构造子菜单/// </summary> /// <param name="name">按钮名称</param> /// <param name="sub_button">子菜单集合</param> public MenuJson(string name, IEnumerable<MenuJson>sub_button)
{
this.name =name;this.sub_button = new List<MenuJson>();this.sub_button.AddRange(sub_button);
}
/// <summary> ///参数化构造函数/// </summary> /// <param name="name">按钮名称</param> /// <param name="buttonType">菜单按钮类型</param> /// <param name="value">按钮的键值(Click),或者连接URL(View)</param> public MenuJson(string name, ButtonType buttonType, stringvalue)
{
this.name =name;this.type =buttonType.ToString();if (buttonType ==ButtonType.view)
{
this.url =value;
}
else{this.key =value;
}
}
}

例如企业微信创建菜单的接口如下所示。

        /// <summary>
        ///创建菜单/// </summary>
        /// <param name="accessToken">调用接口凭证</param>
        /// <param name="menuJson">菜单对象</param>
        /// <returns></returns>
        public CommonResult CreateMenu(string accessToken, MenuListJson menuJson, stringagentid)
{
var url = string.Format("https://qyapi.weixin.qq.com/cgi-bin/menu/create?access_token={0}&agentid={1}", accessToken, agentid);string postData =menuJson.ToJson();returnHelper.GetExecuteResult(url, postData);
}

2、企业微信菜单处理

企业微信和公众号(或者小程序)菜单创建的接口参数有所不同,企业微信增加了一个AgentId的参数,因为企业微信可以创建多个应用,一个AgentId代表其中一个应用的标识。

要使用API来创建菜单,几个地方的设置参数是比较重要的,如下所示。

分别是应用的AgentId、应用的Secret(相当于AppSecret)、以及创建JS-SDK的可信域名为自己服务器的域名。AppID则在企业微信的入口查看企业ID就是。

而这些我们可以在自己的微信后台账号管理进行配置即可,如下是我们企业微信后台的配置参数管理。

由于企业微信、公众号微信的菜单信息结构一致,那么我们在平台维护菜单信息的时候,也可以做到统一处理了。

如下是创建菜单的事件后台代码。

        /// <summary>
        ///更新微信菜单(公众号、企业微信公用)/// </summary>
        /// <returns></returns>
        public ActionResult UpdateWeChatMenu(stringaccountId)
{
CommonResult result
= newCommonResult();
AccountInfo info
= BLLFactory<Account>.Instance.FindByID(accountId);if (info != null)
{
//获取微信的访问令牌Access_token AccessTokenInfo token = base.GetAccessToken(accountId);//获取微信菜单信息 MenuListJson menuJson =GetWeixinMenu(accountId);if (info.AccountType == "企业微信")
{
//用企业接口提交 ICorpMenuApi menuApi = newCorpMenuApi();
result
=menuApi.CreateMenu(token.AccessToken, menuJson, info.AgentId);
}
else{//用公众号接口提交 IMenuApi menuApi = newMenuApi();
result
=menuApi.CreateMenu(token.AccessToken, menuJson);
}
}
returnToJsonContent(result);
}

提交成功后,我们就可以在企业微信的工作台上看到对应企业微信的菜单了。

在我的开发框架系列中,底层数据库的配置处理都是差不多的,框架整体支持SQLServer、DB2、MySql、ODP.NET(Oracle)、PostgreSQL、SQLite、SqlEx等数据库的,往往客户在使用框架的时候会问,框架支持哪些数据库、各种数据库配置信息、如何实现数据库分库处理、如何同时支持SQLServer和Oracle等数据库支持、如何实现数据库连接字符串加密等问题,本篇随笔逐一进行介绍。

1、框架支持的数据库介绍

框架底层数据库访问采用了微软企业库实现,因此在处理多种数据库访问的时候,能够提供统一的访问处理操作,同时对不同的数据库支持操作也是非常不错的。下图是框架底层数据库的支持情况。

采用了微软企业库Enterprise Library作为我们底层的数据库访问模块后,对于多种数据库的访问操作,就会统一采用这个企业库的数据库访问对象,操作起来非常一致,为了对不同数据库的常规增删改查等一些操作进行进一步的封装,以达到简化代码的目的,因此我们可以为每个不同的数据库定义一个数据访问操作基类,以便实现一些不同数据库差异性的处理,但是它们还是有一个共同的数据访问基类。

采用不同的数据库,我们需要为不同数据库的访问层进行生成处理,如为SQLServer数据的表生成相关的数据访问层DALSQL,里面放置各个表对象的内容,不过由于采用了相关的继承类处理和基于数据库的代码生成,需要调整的代码很少。

这样整合多种数据库支持的底层后,整个数据访问的架构设计如下所示。

2、各种数据库配置信息

对于默认支持的SQLServer数据库,它的连接字符串如下所示。

<?xml version="1.0"?>
<configuration>
  <configSections>
    <sectionname="dataConfiguration"type="Microsoft.Practices.EnterpriseLibrary.Data.Configuration.DatabaseSettings, Microsoft.Practices.EnterpriseLibrary.Data"/>
  </configSections>
  <connectionStrings>
    <!--SQLServer数据库的连接字符串-->
    <addname="sqlserver"providerName="System.Data.SqlClient"connectionString="Persist Security Info=False;Data Source=(local);Initial Catalog=WinFramework;Integrated Security=SSPI"/>
  </connectionStrings>
  <dataConfigurationdefaultDatabase="sqlserver">
  </dataConfiguration>
</configuration>

上面的sqlserver数据库连接信息是采用信任模式配置的,如果我们基于局域网,那么需要配置对应的IP或者sa用户名和密码的方式,配置信息如下所示。

<addname="sqlserver2"providerName="System.Data.SqlClient"connectionString="Data Source=192.168.1.10;Initial Catalog=CRM;Persist Security Info=True;User ID=sa;Password=123456"/>

不过对于一些扩展支持的数据库,我们还需要添加一些映射处理,如对于MySQL的支持,我们需要添加连接字符串:

  <!--MySQL数据库的连接字符串-->
    <addname="mysql"providerName="MySql.Data.MySqlClient"connectionString="Server=localhost;Database=WinFramework;Uid=root;Pwd=123456;"/>

还需要添加ProviderMappings的支持,如下所示的XML。

  <dataConfigurationdefaultDatabase="mysql">
    <providerMappings>
      <adddatabaseType="EntLibContrib.Data.MySql.MySqlDatabase, EntLibContrib.Data.MySql"name="MySql.Data.MySqlClient" />
    </providerMappings>
  </dataConfiguration>

下面我列出所有不同数据库的连接字符串以及映射关系的一个完整版本,供参考。

<?xml version="1.0"?>
<configuration>
  <configSections>
    <sectionname="dataConfiguration"type="Microsoft.Practices.EnterpriseLibrary.Data.Configuration.DatabaseSettings, Microsoft.Practices.EnterpriseLibrary.Data"/>
  </configSections>
  <connectionStrings>
    <!--Sqlserver数据库的连接字符串-->
    <addname="sqlserver"providerName="System.Data.SqlClient"connectionString="Persist Security Info=False;Data Source=(local);Initial Catalog=WinFramework;Integrated Security=SSPI"/>

    <!--PostgreSQL数据库的连接字符串-->
    <addname="npgsql"providerName="Npgsql"connectionString="Server=localhost;Port=5432;Database=postgres;User Id=postgres;Password=123456"/>
    <!--MySQL数据库的连接字符串-->
    <addname="mysql"providerName="MySql.Data.MySqlClient"connectionString="Server=localhost;Database=WinFramework;Uid=root;Pwd=root;"/>
    <!--路径符号|DataDirectory|代表当前运行目录-->
    <addname="access"providerName="System.Data.OleDb"connectionString="Provider=Microsoft.Jet.OLEDB.4.0;Data Source=|DataDirectory|\WinFramework.mdb;User ID=Admin;Jet OLEDB:Database Password=;" />
    <!--sqlite数据库字符串,路径符号|DataDirectory|代表当前运行目录-->
    <addname="sqlite"providerName="System.Data.SQLite"connectionString="Data Source=|DataDirectory|\WinFramework.db;Version=3;" />
    <!--Oracle数据库的连接字符串-->
    <addname="oracle"providerName="System.Data.OracleClient"connectionString="Data Source=orcl;User ID=win;Password=win"/>
    <!--达梦数据库的连接字符串-->
    <addname="Dm"providerName="Dm"connectionString="Server=localhost;User ID=SYSDBA;PWD=SYSDBA;Database=WINFRAMEWORK;" />
    <!--IBM DB2数据库的连接字符串-->
    <addname="db2"providerName="IBM.Data.DB2"connectionString="database=whc;uid=whc;pwd=123456"/>
    <!--采用OdpNet方式的Oracle数据库的连接字符串-->
    <addname="oracle2"providerName="Oracle.DataAccess.Client"connectionString="Data Source=orcl;User id=win;Password=win;" />
  </connectionStrings>
  <dataConfigurationdefaultDatabase="sqlserver">
    <providerMappings>
      <adddatabaseType="EntLibContrib.Data.PostgreSql.NpgsqlDatabase, EntLibContrib.Data.PostgreSql"name="Npgsql" />
      <adddatabaseType="EntLibContrib.Data.MySql.MySqlDatabase, EntLibContrib.Data.MySql"name="MySql.Data.MySqlClient" />
      <adddatabaseType="EntLibContrib.Data.SQLite.SQLiteDatabase, EntLibContrib.Data.SqLite"name="System.Data.SQLite" />
      <adddatabaseType="EntLibContrib.Data.Dm.DmDatabase, EntLibContrib.Data.Dm"name="Dm" />
      <adddatabaseType="EntLibContrib.Data.DB2.DB2Database, EntLibContrib.Data.DB2"name="IBM.Data.DB2" />
      <adddatabaseType="EntLibContrib.Data.OdpNet.OracleDatabase, EntLibContrib.Data.OdpNet"name="Oracle.DataAccess.Client" />
    </providerMappings>
  </dataConfiguration>
  <appSettings>
    <!--组件的数据库类型:access、sqlserver、sqlite、oracle等,默认为sqlserver可不写-->
    <addkey="ComponentDbType"value="sqlserver"/>
  </appSettings>
  <startupuseLegacyV2RuntimeActivationPolicy="true">
    <supportedRuntimeversion="v4.0"sku=".NETFramework,Version=v4.0"/>
    <supportedRuntimeversion="v2.0.50727"/>
  </startup>
</configuration>

里面包括了支持的各种数据库的连接字符串的写法。

3、如何实现数据库分库处理、如何同时支持SQLServer和Oracle等数据库支持

由于整个框架的设计抽象了多种数据库的处理模型,因此数据库分库处理实现也是比较方便的,数据库的分库处理和同时支持SQLServer和Oracle等数据库的操作是类似的,他们都是对多个数据库(包括不同各类型)进行访问处理。

之前我在随笔《
Winform开发框架中实现多种数据库类型切换以及分拆数据库的支持
》这里也介绍了具体的实现处理,其实我们使用的同时支持多数据库的操作代码是比较简单的,我们可以在代码里面通过调用BLL层类的接口SetConfitName来指定特定的数据库,如下代码所示。

            //指定业务类的数据库配置
            BLLFactory<Asset>.Instance.SetConfigName("workflow");  
BLLFactory
<StoreAddress>.Instance.SetConfigName("workflow");

有时候,可能BLL对象有可能出现相同的情况,但是需要访问不同库里面的表对象,那么我们可以在使用后恢复默认的配置信息。

            BLLFactory<DictData>.Instance.SetConfigName("workflow");//使用业务库
            Dictionary<string, string> dict = BLLFactory<DictData>.Instance.GetDictByDictType(dictTypeName);
BLLFactory
<DictData>.Instance.SetConfigName(null);//恢复默认

以上代码就是先访问workflow配置的数据库信息,获取字典信息后恢复默认的数据库信息。

上面那种方式是对于同种类型数据库的接口切换,如果不同的数据库类型,如一个是SQLServer,一个是Oracle,那么就可以多指定一个参数即可,如下代码所示。

            //指定使用oracle类型的数据库配置
            BLLFactory<DictData>.Instance.SetConfigName("workflow", "oracle");//使用业务库
            Dictionary<string, string> dict = BLLFactory<DictData>.Instance.GetDictByDictType(dictTypeName);
BLLFactory
<DictData>.Instance.SetConfigName(null);//恢复默认

虽然我们一般使用一个库,但是如果是分库,或者要同时支持多个数据库类型,基本上处理还是很方便的。

如果对于通用类型的数据库处理,我们可以使用公用类库里面的CommonDAL类进行处理。这个类库可以很方便的处理视图、存储过程、或者常规的接口查询操作,不需要和具体的实体类绑定的接口。

它的定义如下所示。

4、数据库字符串加密处理

很多情况下,我们为了部署应用,需要公开数据库连接字符串信息,但是我们又不想让使用者很容易的获取到我们的连接字符串里面的用户名和密码敏感信息,这时候连接字符串加密就是比较必要的了。

处理方式就是我们创建一个工具,使用自己知道的加解密规则来处理连接字符串的加解密处理。

处理的过程大概如下所示。

1)找到app.config文件,打开内容编辑。
2)找到数据库(如SQLServer)连接字符串的connectionString字符串,如下所示。
<add name="sqlserver" providerName="System.Data.SqlClient" connectionString="Persist Security Info=False;Data Source=(local);Initial Catalog=CRM;Integrated Security=SSPI"/>
3)提取里面的connectionString字符串部分,放到上面的加密软件里面进行加密,然后把加密内容替换connectionString字符串,变为如下所示的配置信息。
<add name="sqlserver" providerName="System.Data.SqlClient" connectionString="9Fs/vPhm24CYa0mXCLAMYOJmbBHq/qQAjdbVdbeOhS5L0d8WGhHUR3iIyFZydEV8cPmlPHfDTnwJZMr9xkMAxuNtPKUsIdKTjlWInpf+Vc+UD2gtYIE3FnvL06KcHzX+"/>

4)保存文件,配置加密字符串完成。

这个配置信息在框架的处理的时候有对应的解密处理规则,可以正常解析加密字符串即可。

如果要了解或者修改其中的对应解密处理操作,可以定位扩展公用类库里面数据库访问层,如下所示。

定位到对应的数据库访问类,然后找到下面的对应函数了解即可。

企业微信的支付自从企业号变化为企业微信后,增加了一些支付接口以及对很多接口进行了调整,企业微信的支付处理也是变化了不少,往往有时候碰到不少坑,一步一个脚印趟过来的;企业微信支付是需要结合微信商户后台进行处理,有时候也需要设置好商户平台的相关处理,才能进行发送红包、支付到个人等等支付处理。本篇随笔介绍在企业微信的支付处理中的发送红包的操作相关内容。

1、企业微信的支付接口

我们查看企业微信API的目录,可以看到企业微信支付的相关介绍,如下所示。

1)常见错误处理

企业微信支付,经常见到的错误信息,就是签名错误这个操作,这个很多人出招,解决方法各种各样,其实很多可能是不符合的,这样排查问题起来就很吃力。

这里需要遵循官方的解析进行排查,特别对参数的顺序和数量进行核对,
注意不要增加多一个参数,否则都容易出现签名错误。

我就是在官方需要参数都有了,打印输出的格式也没问题,就是不小心多了一个参数(还是升级前有的一个),导致错误很难排查,弄得很头大。

一般来说发送企业红包,很容易发生签名错误的情况,请检查以下内容
1、企业微信的CorpID/CorpSecret
2、企业微信的支付Secret和商户的API支付秘钥
3、参数不能多也不能少(重要),如很多时候由于版本原因这里不小心多了一个total_num导致签名错误
4、商户平台的证书和密码是否正确

另外,除了这些问题外,重要的问题就是签名的处理了,微信支付除了有一个常规的签名sign参数外,还增加了一个workwx_sign的参数,两者的规则是不同的。

workwx_sign参数在前,使用系统给出的计算方式计算后,然后在计算sign参数,sign参数的计算是包含本身之外的所有参数进行计算,包括了workwx_sign参数。

2)签名参数处理

对于企业微信的签名workwx_sign参数,不要将参数全部参与计算签名,否则会返回微信签名错误!

发红包api固定如下几个字段参与签名:
act_name
mch_billno
mch_id
nonce_str
re_openid
total_amount
wxappid

付款api固定如下几个字段参与签名:
amount
appid
desc
mch_id
nonce_str
openid
partner_trade_no
ww_msg_type

计算企业微信签名的字符串最后拼的secret是企业微信管理端支付应用页面的secret,如下图所示。

示例:请求内容:
act_name XXX
mch_billno 66666611234567890
mch_id 10000098
nonce_str qFKEgfig76DF9912fewmkp
re_openid oxTWIuGaIt6gTKsQRLau2M0yL16E
total_amount 100
wxappid wx123456789

第一步: 对参数按照key=value的格式,并按照参数名ASCII字典序排序如下
stringA=”act_name=XXX&mch_billno=66666611234567890&mch_id=10000098&nonce_str=qFKEgfig76DF9912fewmkp&re_openid=oxTWIuGaIt6gTKsQRLau2M0yL16E&total_amount=100&wxappid=wx123456789
第二步:拼接企业微信支付应用secret(参见企业微信管理端支付应用页面):
stringSignTemp=”stringA&secret=192006250b4c09247ec02edce69f6a2d”
sign=MD5(stringSignTemp).toUpperCase()

2、企业微信发送红包

测试企业微信发送红包和直接支付的接口,响应效果如下所示

在企业微信中,常用到的企业微信的userid,不过发送红包则需要把userid转换为微信的openid进行使用,转换函数根据UserID 换取用户的OpenId 如下。

一般封装一个函数来使用即可。

        private string GetOpenId(stringuserid)
{
//根据UserID 换取用户的OpenId ICorpBasicApi basicAPi = newCorpBasicApi();return basicAPi.ConvertToOpenId(this.token, userid);
}

发送企业红包调用如下代码所示

//构建发送红包的参数信息
SendRedPackJson packJson = newSendRedPackJson()
{
act_name
= "恭喜发财",
client_ip
=NetworkUtil.GetIPAddress(),
remark
= "企业红包",
wishing
= "企业红包",
total_amount
= 100,
total_num
= 1,
re_openid
= openid //发送给用户的OpenID };//调用发送企业红包接口发送 var result = hbApi.SendWorkRedPack(packJson);

函数SendWorkRedPack的实现内容如下所示。

        /// <summary>
        ///发放企业红包。需要商户证书/// </summary>
        /// <param name="json"></param>
        /// <returns></returns>
        publicSendRedPackResult SendWorkRedPack(SendRedPackJson json)
{
CheckAccount();
//检查AccountInfo的对象属性值//加入常规的参数 WxPayData data = newWxPayData();
data.SetValue(
"nonce_str", data.GenerateNonceStr());//随机字符串//商户订单号(每个订单号必须唯一) 组成:mch_id+yyyymmdd+10位一天内不能重复的数字。//接口根据商户订单号支持重入,如出现超时可再调用。 data.SetValue("mch_billno", data.GenerateOutTradeNo(AccountInfo.MchID));
data.SetValue(
"mch_id", AccountInfo.MchID);//商户号 data.SetValue("wxappid", AccountInfo.AppID);//公众账号appid data.SetValue("sender_name", AccountInfo.Name);//红包发送者名称//发送者头像,此id为微信默认的头像(如果想自定义头像,请参见第三部分) data.SetValue("sender_header_media_id", "1G6nrLmr5EC3MMb_-zK1dDdzmd0p7cNliYu9V5w7o8K0");//以企业应用的名义发红包,企业应用id,整型,//可在企业微信管理端应用的设置页面查看。与sender_name互斥,二者只能填一个。//data.SetValue("agentid", "3010046");//企业应用id//发放红包使用场景,红包金额大于200时必传 if (!string.IsNullOrEmpty(json.scene_id))
{
data.SetValue(
"scene_id", json.scene_id);
}

data.SetValue(
"re_openid", json.re_openid);//接受红包的用户.用户在wxappid下的openid。 data.SetValue("total_amount", json.total_amount);//金额 data.SetValue("wishing", json.wishing);//红包祝福语 data.SetValue("act_name", json.act_name);//项目名称 data.SetValue("remark", json.remark);//备注 data.SetValue("workwx_sign", data.MakeWorkWxSign(AccountInfo.CorpPaySecret));//企业微信签名 data.SetValue("sign", data.MakeSign(AccountInfo.PayAPIKey));//发送企业红包,很容易发生签名错误的情况,请检查以下内容//1、企业微信的CorpID/CorpSecret//2、企业微信的支付Secret和商户的API支付秘钥//3、参数不能多也不能少(重要),很多时候由于版本原因这里不小心多了一个total_num导致签名错误//4、商户平台的证书和密码是否正确 var url = "https://api.mch.weixin.qq.com/mmpaymkttransfers/sendworkwxredpack";return Helper.GetPayResultWithCert<SendRedPackResult>(data, url, AccountInfo.CertPath, AccountInfo.CertPassword);
}

其实以上很多参数大家应该都很了解,相对于来说MakeWorkWxSign 和 MakeSign 就是这里的关键处理,而前者正是很多人没有处理好的问题所在。

下面把相关函数贴出来,方便对照了解下吧,其实下面这些函数是放在WxPayData类里面,统一管理处理对应的签名的。

        /// <summary>
        ///拼接用来签名的几个参数,发送红包和付款的签名字段不同/// </summary>
        /// <param name="isRedPack">是否为发送红包操作,或者是付款操作,两者需要签名的字段不同</param>
        /// <returns></returns>
        private string ToWorkWxUrl(boolisRedPack)
{
List
<string> paramRedPack = new List<string>() { "act_name", "mch_billno", "mch_id", "nonce_str", "re_openid", "total_amount", "wxappid"};
List
<string> paramPay = new List<string>() { "amount", "appid", "desc", "mch_id", "nonce_str", "openid", "partner_trade_no", "ww_msg_type"};string buff = "";foreach (KeyValuePair<string, object> pair inValues)
{
if (pair.Value != null && pair.Value.ToString() != "")
{
if(isRedPack)
{
//发送红包的签名字段 if(paramRedPack.Contains(pair.Key))
{

buff
+= pair.Key + "=" + pair.Value + "&";
}
}
else{//付款的签名字段 if(paramPay.Contains(pair.Key))
{
buff
+= pair.Key + "=" + pair.Value + "&";
}
}
}
}
buff
= buff.Trim('&');returnbuff;
}
/// <summary> ///生成企业微信签名/// </summary> /// <param name="corpPaySecret">企业支付的Secret</param> /// <param name="isRedPack">是否为发送红包操作,或者是付款操作,两者需要签名的字段不同</param> /// <returns></returns> public string MakeWorkWxSign(string corpPaySecret, bool isRedPack = true)
{
//转url格式 string str =ToWorkWxUrl(isRedPack);//在string后加入secret str += "&secret=" +corpPaySecret;//MD5加密 var md5 =MD5.Create();var bs =md5.ComputeHash(Encoding.UTF8.GetBytes(str));var sb = newStringBuilder();foreach (byte b inbs)
{
sb.Append(b.ToString(
"x2"));
}
//所有字符转为大写 returnsb.ToString().ToUpper();
}
        /// <summary>
        ///生成签名,详见签名生成算法/// </summary>
        /// <returns>签名, sign字段不参加签名</returns>
        public string MakeSign(stringpayAPIKey)
{
//转url格式 string str =ToUrl();//在string后加入API KEY str += "&key=" +payAPIKey;//MD5加密 var md5 =MD5.Create();var bs =md5.ComputeHash(Encoding.UTF8.GetBytes(str));var sb = newStringBuilder();foreach (byte b inbs)
{
sb.Append(b.ToString(
"x2"));
}
//所有字符转为大写 var result =sb.ToString().ToUpper();returnresult;
}

3、企业微信直接支付的接口

对照这些官方资料,我们可以编写对应的接口API来处理。

        /// <summary>
        ///企业付款(请求需要双向证书)///企业付款业务是基于微信支付商户平台的资金管理能力,为了协助商户方便地实现企业向个人付款,///针对部分有开发能力的商户,提供通过API完成企业付款的功能。 比如目前的保险行业向客户退保、给付、理赔。///企业付款将使用商户的可用余额,需确保可用余额充足。查看可用余额、充值、提现请登录商户平台“资金管理”进行操作。https://pay.weixin.qq.com/ 
        ///注意:与商户微信支付收款资金并非同一账户,需要单独充值。/// </summary>
        /// <param name="json">企业支付数据</param>
        /// <returns></returns>
        publicCorpPayResult CorpPay(CorpPayJson json)
{
CheckAccount();
//检查AccountInfo的对象属性值 WxPayData data= newWxPayData();
data.SetValue(
"mch_appid", AccountInfo.AppID);//公众账号appid, 注意是mch_appid,而非wxappid data.SetValue("mchid", AccountInfo.MchID);//商户号, 注意是mchid而非mch_id data.SetValue("nonce_str", data.GenerateNonceStr());//随机字符串 data.SetValue("spbill_create_ip", NetworkUtil.GetIPAddress());//终端ip data.SetValue("partner_trade_no", data.GenerateOutTradeNo(AccountInfo.MchID));//随机字符串 data.SetValue("device_info", json.device_info);//终端ip data.SetValue("openid", json.openid);
data.SetValue(
"check_name", json.check_name);
data.SetValue(
"re_user_name", json.re_user_name);
data.SetValue(
"amount", json.amount);
data.SetValue(
"desc", json.desc);

data.SetValue(
"sign", data.MakeSign(AccountInfo.PayAPIKey));//最后生成签名 var url = string.Format("https://api.mch.weixin.qq.com/mmpaymkttransfers/promotion/transfers");return Helper.GetPayResultWithCert<CorpPayResult>(data, url, AccountInfo.CertPath, AccountInfo.CertPassword);
}

其中里面的很多参数的处理是和前面支付差不多的,因此不再赘述

调用的处理代码如下所示

            //构建处理信息
            CorpPayJson json = newCorpPayJson()
{
amount
= 100,
check_name
=PayCheckName.FORCE_CHECK.ToString(),
desc
= "测试退款",
openid
=openid,
device_info
= "",
re_user_name
= "伍华聪",
spbill_create_ip
=NetworkUtil.GetIPAddress()
};
//直接付款到员工账号 var result = api.CorpPay(json);

最新的第一条就是直接付款的信息提示。