2023年2月

在我的开发框架系列中,底层数据库的配置处理都是差不多的,框架整体支持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);

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

在后台业务管理系统中使用Autofac实现微信接口的处理,我们只需要把相关使用到的DLL放到BIN目录里面即可,通过IOC控制反转方式实现对接口的调用。在实现在业务系统里面,我们本身程序可能已经依赖了很多相关的DLL类库,这种可以降低对相关DLL的强依赖,而以一种松耦合的方式使用我们所需要的微信接口。

在之前随笔《
C#开发微信门户及应用(42)--使用Autofac实现微信接口处理的控制反转处理
》里面介绍了Autofac组件实现IOC控制反转的一些实现细节,本篇随笔介绍在业务管理系统中,我们需要调用微信接口推送一些业务消息,如待办实现、通知信息等等。使用Autofac组件的控制反转方式,使得我们只需要关注接口的处理即可,不需要过于关注实现的具体细节。

1、功能处理的接口和实现项目

如我们创建一个项目,用于处理对微信或者第三方组件的一些封装处理,通过创建处理功能的接口,以及对应的实现,如下所示。

如对微信的处理,我们需要调用微信框架里面相关的接口封装项目,首先定义一个消息推送的接口

namespaceWHC.Common.Handler
{
/// <summary> ///企业微信消息推送接口/// </summary> public interfaceICorpMessage
{
/// <summary> ///企业微信的APPID/// </summary> string CorpId { get; set; }/// <summary> ///企业微信的APPSecret/// </summary> string CorpSecret { get; set; }/// <summary> ///企业微信应用的ID/// </summary> string AgentId { get; set; }/// <summary> ///发送消息内容(如任务通知)/// </summary> /// <param name="touser">发送给的用户企业微信ID</param> /// <param name="title">消息标题</param> /// <param name="description">消息正文(512字节内)</param> /// <param name="url">跳转URL</param> /// <returns></returns> CommonResult SendMessageTextCard(string touser, string title, string description, stringurl);
}

我们定义了几个属性,就是希望接口能够少传一些参数,而且可以在多个实现函数里面通用的。

具体的发送消息实现类如下所示。

namespaceWHC.Common.Handler
{
/// <summary> ///企业微信消息推送实现/// </summary> public classCorpMessage : ICorpMessage
{
/// <summary> ///企业微信的APPID/// </summary> public string CorpId { get; set; }/// <summary> ///企业微信的APPSecret/// </summary> public string CorpSecret { get; set; }/// <summary> ///企业微信应用的ID/// </summary> public string AgentId { get; set; }/// <summary> ///发送消息内容(如任务通知)/// </summary> /// <param name="touser">发送给的用户企业微信ID</param> /// <param name="title">消息标题</param> /// <param name="description">消息正文(512字节内)</param> /// <param name="url">跳转URL</param> /// <returns></returns> public CommonResult SendMessageTextCard(string touser, string title, string description, stringurl)
{
CommonResult result
= newCommonResult();
ICorpBasicApi baseBLL
= newCorpBasicApi();string token =baseBLL.GetAccessToken(CorpId, CorpSecret);if (!string.IsNullOrEmpty(token))
{
ICorpMessageApi bll
= newCorpMessageApi();
CorpSendTextCard msg
= newCorpSendTextCard(title, description, url);
msg.agentid
=AgentId;
msg.touser
=touser;

result
=bll.SendMessage(token, msg);
}
else{
result.ErrorMessage
= "无法获取Token信息";
}
returnresult;
}
}

这里最终调用的是微信框架里面的项目模块,如下代码所示

ICorpBasicApi baseBLL = new CorpBasicApi();

这里我们通过调用接口发送TextCard 文本卡片信息的。

2、在后台业务管理系统对推送信息接口的调用

首先为了使用IOC的控制反转处理,我们项目需要引用Autofac和Autofac.Configuration

然后把需要的配置信息卸载Autofac.Config文件里面,如下文件内容所示,红框里面的就是我们这里使用到的消息发送接口。

但我们完成了配置文件,并把配置文件放在项目根目录下,就可以通过IOC接口控制反转的方式,获得对应的接口实现了,如下代码所示

 //获取对应的企业微信消息推送接口
 var handler = AutoFactory.Instatnce.Container.Resolve<ICorpMessage>();

有了这个接口,我们就可以在实际项目中调用这个接口进行处理企业微信的消息推送了。

例如我们在Web的MVC控制器端实现一个处理函数,如下所示。

        /// <summary>
        ///批量处理多个任务下发企业微信/// </summary>
        /// <param name="billNo">多个billno组成的列表</param>
        /// <returns></returns>
        public ActionResult SendTask(stringbillNoList)
{
CommonResult result
= newCommonResult();try{if (!string.IsNullOrEmpty(billNoList))
{
//获取对应的企业微信消息推送接口 var handler = AutoFactory.Instatnce.Container.Resolve<ICorpMessage>();if (handler != null)
{
//把逗号分隔的字符串转换为列表 List<string> list = billNoList.ToDelimitedList<string>(",");foreach (string billNo inlist)
{
//获取盘点主表信息 AssetCheckInfo info = BLLFactory<AssetCheck>.Instance.FindByBillNo(billNo);if (info != null)
{
//获取盘点明细~信息~ var detailList = BLLFactory<AssetCheckDetail>.Instance.FindByBillNo(billNo);//读取配置信息 AppConfig config = newAppConfig();
handler.CorpId
= config.AppConfigGet("CorpId");
handler.CorpSecret
= config.AppConfigGet("CorpSecret");
handler.AgentId
= config.AppConfigGet("AgentId");//构建推送的消息体内容 string touser =info.CorpUserId;string title = "您有一个盘点任务待处理";
StringBuilder sb
= newStringBuilder();
sb.AppendFormat(
"盘点单号:{0}\r\n", info.BillNo);//sb.AppendFormat("盘点公司:{0}\r\n", info.Company_ID);//sb.AppendFormat("盘点部门:{0}\r\n", info.Dept_ID); sb.AppendFormat("盘点数量:{0}\r\n", info.CheckQty);
sb.AppendFormat(
"指定盘点人:{0}\r\n", info.CorpUserId);
sb.AppendFormat(
"申请日期:{0}\r\n", info.ApplyDate.ToString("yyyy-MM-dd"));string description =sb.ToString();string url = "http://www.iqidi.com";//调用企业微信消息接口推送消息 result =handler.SendMessageTextCard(touser, title, description, url);if(result.Success)
{
//更新盘点表状态 Hashtable ht = newHashtable();
ht.Add(
"TaskStatus", 1);//下发 1 BLLFactory<AssetCheck>.Instance.UpdateFields(ht, info.ID);
}
}
}
}
}
else{
result.ErrorMessage
= "单号为空";
}
}
catch(Exception ex)
{
LogHelper.Error(ex);
result.ErrorMessage
=ex.Message;
}
returnToJsonContent(result);
}

而在前端的界面里面,我们可以通过定义一个JS函数来发起任务消息的推动处理。

                $("#add").modal("hide");//构造参数发送给后台
                var postData ={
billNoList: billnos,
}
url
= '/AssetCheck/SendTask';
$.post(url, postData,
function(json) {var data =$.parseJSON(json);if(data.Success) {//可增加其他处理 //保存成功 1.关闭弹出层,2.刷新表格数据 showTips("下发盘点任务给微信成功");
$(
"#checkUser").modal("hide");
RefreshAsset();
}
else{
showError(
"下发盘点任务给微信失败:" + data.ErrorMessage, 3000);
}
}).error(
function() {
showTips(
"您未被授权使用该功能,请联系管理员进行处理。");
});

最终在我们完成盘点任务创建的时候,通知信息推送到了企业微信客户端和手机端的企业微信界面上。

当然其他客户端如果处理这种对IOC的接口调用,一样的原理,如Winform客户端,或者是其他.net的项目里面,我们都可以通过IOC实现对接口实现的控制反转,进一步解放强依赖的关系,实现松耦合的接口管理。

我们在部署的时候,把使用到的对应DLL复制过去对应的BIN目录下就可以运行起来了,在我们获取对应的接口的时候,相关的DLL会进行关联处理的。

企业号升级到企业微信后,发送应用消息的接口也变化了不少,除了原来的文本、图片、文件、语音、视频、图文消息等消息外,增加了文本卡片、markdown消息、小程序通知消息等内容,不过它们都可以共用一个接口进行发送,只是它们的对象不太一样,本篇随笔主要介绍整个企业微信应用消息处理这部分内容,包括不同消息的实体关系和接口发送的实现等内容。

1、企业微信消息对象的定义

在早期还是微信企业号的时候,我对发送企业号消息的接口也已经实现,参考《
C#开发微信门户及应用(19)-微信企业号的消息发送(文本、图片、文件、语音、视频、图文消息等)
》,这次对企业号升级到企业微信接口,对所有接口进行了梳理和测试。

我们先看看企业微信对应用消息接口的介绍(
https://work.weixin.qq.com/api/doc#90000/90135/90236

根据消息的类型,我们增加了一些额外的对象实体类,修改后的关系图如下所示。

我们来看看新增的文本卡片、markdown消息、小程序通知消息等内容的对象定义代码。

所有消息的基类信息CorpSendBase基类对象代码如下

    /// <summary>
    ///企业号发送消息的基础消息内容/// </summary>
    public classCorpSendBase
{
/// <summary> ///成员ID列表(消息接收者,多个接收者用‘|’分隔,最多支持1000个)。特殊情况:指定为@all,则向关注该企业应用的全部成员发送/// </summary> public string touser { get; set; }/// <summary> ///部门ID列表,多个接收者用‘|’分隔,最多支持100个。当touser为@all时忽略本参数/// </summary> public string toparty { get; set; }/// <summary> ///标签ID列表,多个接收者用‘|’分隔,最多支持100个。当touser为@all时忽略本参数/// </summary> public string totag { get; set; }/// <summary> ///消息类型/// </summary> public string msgtype { get; set; }/// <summary> ///企业应用的id,整型。可在应用的设置页面查看/// </summary> public string agentid { get; set; }/// <summary> ///表示是否是保密消息,0表示否,1表示是,默认0/// </summary> [JsonProperty(NullValueHandling =NullValueHandling.Ignore)]public string safe { get; set; }

}

其他的文本卡片、markdown消息、小程序通知消息等内容的对象继承它,增加自己对象的消息即可。

如文本卡片的类及其子类的代码如下所示。

    /// <summary>
    ///文本卡片消息/// </summary>
    public classCorpSendTextCard : CorpSendBase
{
publicCorpSendTextCard()
{
this.msgtype =CorpMsgType.textcard;this.textcard = newCorpSendTextCardEntity();
}
public CorpSendTextCard(string title, string description, string url, string btntxt = "详情")
{
this.msgtype =CorpMsgType.textcard;this.textcard = newCorpSendTextCardEntity(title, description, url, btntxt);
}
/// <summary> ///消息内容/// </summary> public CorpSendTextCardEntity textcard { get; set; }
}
public classCorpSendTextCardEntity
{
/// <summary> ///标题,不超过128个字节,超过会自动截断/// </summary> public string title { get; set; }/// <summary> ///描述,不超过512个字节,超过会自动截断/// </summary> public string description { get; set; }/// <summary> ///点击后跳转的链接。/// </summary> public string url { get; set; }/// <summary> ///按钮文字。 默认为“详情”, 不超过4个文字,超过自动截断。/// </summary> public string btntxt { get; set; }publicCorpSendTextCardEntity()
{ }
public CorpSendTextCardEntity(string title, string description, string url, string btntxt = "详情")
{
this.title =title;this.description =description;this.url =url;this.btntxt =btntxt;
}
}

效果借用官方的效果图,如下所示

markdown消息对象如下所示

   /// <summary>
    ///markdown消息///目前仅支持markdown语法的子集///微工作台(原企业号)不支持展示markdown消息/// </summary>
    public classCorpSendMarkdown : CorpSendBase
{
publicCorpSendMarkdown()
{
this.msgtype =CorpMsgType.markdown;this.markdown = newCorpSendMarkdownEntity();
}
public CorpSendMarkdown(stringcontent)
{
this.msgtype =CorpMsgType.markdown;this.markdown = newCorpSendMarkdownEntity(content);
}
/// <summary> ///消息内容/// </summary> public CorpSendMarkdownEntity markdown { get; set; }
}

效果如下所示

小程序通知消息

    /// <summary>
    ///小程序通知消息/// </summary>
    public classCorpSendMiniProgram : CorpSendBase
{
publicCorpSendMiniProgram()
{
this.msgtype =CorpMsgType.miniprogram_notice;this.textcard = newCorpSendMiniProgramEntity();
}
/// <summary> ///消息内容/// </summary> public CorpSendMiniProgramEntity textcard { get; set; }
}

小程序的消息界面效果如下

有了这些消息的定义,我们就可以统一使用接口进行发送了。

2、发送企业微信信息

定义一个消息发送的接口,接口函数的参数,包括accesstoken和消息对象的基类,如下所示。

    /// <summary>
    ///企业微信消息管理接口定义/// </summary>
    public interfaceICorpMessageApi
{
/// <summary> ///发送消息。///消息型应用支持文本、图片、语音、视频、文件、图文等消息类型。主页型应用只支持文本消息类型,且文本长度不超过20个字。///需要管理员对应用有使用权限,对收件人touser、toparty、totag有查看权限,否则本次调用失败。/// </summary> /// <param name="accessToken"></param> /// <returns></returns> CommonResult SendMessage(stringaccessToken, CorpSendBase data);
}

实现接口的代码如下所示

        /// <summary>
        ///发送消息。///消息型应用支持文本、图片、语音、视频、文件、图文等消息类型。主页型应用只支持文本消息类型,且文本长度不超过20个字。///需要管理员对应用有使用权限,对收件人touser、toparty、totag有查看权限,否则本次调用失败。/// </summary>
        /// <param name="accessToken"></param>
        /// <returns></returns>
        public CommonResult SendMessage(stringaccessToken, CorpSendBase data)
{
CommonResult result
= newCommonResult();var url = string.Format("https://qyapi.weixin.qq.com/cgi-bin/message/send?access_token={0}", accessToken);var postData =data.ToJson();//数据不用加密发送 CorpSendResult sendResult = WeJsonHelper<CorpSendResult>.ConvertJson(url, postData);if (sendResult != null)
{
result.Success
= (sendResult.errcode == 0);if (!result.Success)
{
result.ErrorMessage
= string.Format("invaliduser:{0},invalidparty:{1},invalidtag:{2}",
sendResult.invaliduser, sendResult.invalidparty, sendResult.invalidtag);
}
}
returnresult;
}

定义好相应的发送对象后,我们就可以进行统一的消息发送操作,包括文本、图片、文件、语音等等类型的消息,注意有些消息是需要上传到服务器上,然后在根据mediaId进行发送出去的。

程序测试接口发送的调用代码如下所示:

文本卡片发送代码

        /// <summary>
        ///文本卡片消息发送/// </summary>
        private void btnSendTextCard_Click(objectsender, EventArgs e)
{
ICorpMessageApi bll
= newCorpMessageApi();
CorpSendTextCard msg
= new CorpSendTextCard("中秋节礼品领取", "今年中秋节公司有豪礼相送", "http://www.iqidi.com", "更多详情");
msg.agentid
=agentid;
msg.touser
= "wuhuacong";

CommonResult result
=bll.SendMessage(token, msg);if (result != null)
{
Console.WriteLine(
"发送TextCard消息:{0} {1} {2}", fileMediaId, (result.Success ? "成功" : "失败"), result.ErrorMessage);
}
}

Markdown的发送代码如下所示。

        /// <summary>
        ///MarkDown消息发送/// </summary>
        private void btnSendMarkDown_Click(objectsender, EventArgs e)
{
ICorpMessageApi bll
= newCorpMessageApi();string content = @"您的会议室已经预定,稍后会同步到`邮箱`
>**事项详情**
>事 项:<font color='info'>开会</font>
>组织者:@miglioguan
>参与者:@miglioguan、@kunliu、@jamdeezhou、@kanexiong、@kisonwang
>
>会议室:< font color ='info'>广州TIT 1楼 301</font>
>日期:< font color ='warning'>2018年5月18日</font>
>时间:< font color ='comment'>上午9:00-11:00</font>
>
> 请准时参加会议。
>
> 如需修改会议信息,请点击:[修改会议信息] (https://work.weixin.qq.com)
";
CorpSendMarkdown msg
= newCorpSendMarkdown(content);
msg.agentid
=agentid;
msg.touser
= "wuhuacong";

CommonResult result
=bll.SendMessage(token, msg);if (result != null)
{
Console.WriteLine(
"发送Markdown消息:{0} {1} {2}", fileMediaId, (result.Success ? "成功" : "失败"), result.ErrorMessage);
}
}

在开发业务管理系统的时候,往往涉及到资产信息及编码的打印处理,如我们需要对资产信息、条形码、二维码一起打印,以便贴在具体资产信息上面,方便微信公众号、企业微信进行业务处理,那么编码的打印就很有必要了,本篇随笔介绍实际生产环境中的资产编码打印处理。

打印操作,可以使用多种方式处理,如使用PrintThis的JS库打印,或者使用用户界面比较好的LODOP插件来进行打印操作,本篇随笔介绍基于LODOP插件来对资产编码信息的打印操作;另外打印给的时候,如果不恰当的设置,估计打印信息部分打印,那这种情况也是需要避免的,需要人为的增加分页的处理,以便整个编码信息打印正确出来。

1、单个编码信息的打印

打个记录我们可以在表格的后面按钮增加一个处理操作,如下界面所示。

然后触发一个弹出层进行展示即可,如果用户需要打印,则在右下角进行打印。

打印界面如下所示。

以上就是几个操作的界面效果,实际处理代码是如何的呢,我们如何调用LODOP进行页面打印的呢?

定义一个弹出层用来展示资产编码信息,如下所示。

<!--------------------------显示二维码的弹出层---------------------------->
<divid="qrcode"class="modal fade"tabindex="-1"role="dialog"aria-labelledby="myModalLabel"aria-hidden="true">
    <divclass="modal-dialog">
        <divclass="modal-content">
            <divclass="modal-header bg-primary">
                <buttontype="button"class="close"data-dismiss="modal"aria-hidden="true"></button>
                <h4class="modal-title">
                    <iclass="icon-book-open"></i>
                    <spanstyle="font-weight:bold">查看二维码信息</span>
                </h4>
            </div>
            <divclass="modal-body"id="showCodeDiv">
                <tableborder='0'style="width:180px"id="qrcodePrint">
                    <tr>
                        <td>
                            <tableborder='0'style="width:180px;page-break-after:always">
                                <tr>
                                    <td>
                                        <divstyle="padding-left:5px">资产名称:<labelid="lblName"></label></div>
                                        <divstyle="padding-left:5px">管理部门:<labelid="lblChargeDept"></label></div>
                                        <divstyle="padding-left:5px">使用部门:<labelid="lblCurrDept"></label></div>
                                        <divstyle="padding-left:5px">使用人:<labelid="lblUsePerson"></label></div>
                                        <divstyle="padding-left:5px">存放地点:<labelid="lblKeepAddr"></label></div>
                                        <imgalt=""id="img_barcode" />
                                    </td>
                                    <td>
                                        <divid="img_qrcode"style="padding:5px"></div>
                                    </td>
                                </tr>
                            </table>
                        </td>
                    </tr>
                </table>

            </div>
            <divclass="modal-footer bg-info">
                <buttontype="button"class="btn btn-primary"data-dismiss="modal"onclick="PrintQrcode()">打印文档</button>
                <buttontype="button"class="btn default"data-dismiss="modal">关闭</button>
            </div>
        </div>
    </div>
</div>

这里的二维码我使用了qrcodejs插件,条形码使用JsBarcode插件

引入相关的JS文件就可以使用了

<scriptsrc="~/Content/JQueryTools/qrcodejs/qrcode.min.js"></script>
<scriptsrc="~/Content/JQueryTools/JsBarcode/dist/JsBarcode.all.min.js"></script>

通过Ajax在后端获取对象数据,然后绑定信息界面上来,如果是二维码,条形码,则根据对应的接口JS代码进行处理。

        //显示二维码
        functionShowCodeByIds(ID) {if (ID == "") {
showTips(
"请选择一条记录");return;
}
//发送请求 $.getJSON("/Asset/FindByID?r=" + Math.random() + "&id=" + ID, function(info) {
$(
"#lblCode").text(info.Code);
$(
"#lblName").text(info.Name);
$(
"#lblCurrDept").text(info.CurrDept);
$(
"#lblChargeDept").text(info.ChargeDept);
$(
"#lblUsePerson").text(info.UsePerson);
$(
"#lblKeepAddr").text(info.KeepAddr);var code =info.Code;
$(
"#img_qrcode").empty();var qrcode = new QRCode(document.getElementById("img_qrcode"), {
text: code,
width:
100,
height:
100,
colorDark:
"#000000",
colorLight:
"#ffffff",
correctLevel: QRCode.CorrectLevel.H
});
JsBarcode(
"#img_barcode", code, {
format:
"CODE128",
displayValue:
true,
fontSize:
20,
lineColor:
"#0cc",
height:
20,
width:
1.5});
});

$(
"#qrcode").modal("show");
}

打印的代码很简单,主要就是构建HTML打印数据后,调用对象打印即可,代码如下所示。

        //打印数据构建
        var LODOP; //声明为全局变量
        functionCreatePrintData(html) {
LODOP
=getLodop();

LODOP.PRINT_INIT(
""); var strBodyStyle = "<style><!--table { border:1;background-color: #CBCBCC } td {background-color:#FFFFFE;border: 1; }";
strBodyStyle
+= " th { background-color:#F1F1F3;padding-left:5px;border:1}--></style>";var strFormHtml = strBodyStyle + "<body>" + html + "</body>";
LODOP.ADD_PRINT_HTM(
10, 10, 710, "100%", strFormHtml);
LODOP.PREVIEW();
}
//资产编码的打印 functionPrintQrcode() {
CreatePrintData($(
"#qrcodePrint").html());
LODOP.SET_SHOW_MODE(
"PREVIEW_IN_BROWSE", 1);
LODOP.PREVIEW();
}

2、批量记录打印

单个记录打印没问题,有时候,需要多个记录一起打印,这样可以提高效率。

原本想不提供界面,直接生成HTML进行输出打印,不过这个方式处理的话,条形码工作正常但二维码倒是没有出来,只好在界面展示后,在通过按钮进行打印,这样就可以解决这个问题。

先编写一个批量打印的弹出层,用在承载界面内容。

<!--------------------------显示批量打印记录的弹出层---------------------------->
<divid="codeDiv"class="modal fade"tabindex="-1"role="dialog"aria-labelledby="myModalLabel"aria-hidden="true">
    <divclass="modal-dialog">
        <divclass="modal-content">
            <divclass="modal-header bg-primary">
                <buttontype="button"class="close"data-dismiss="modal"aria-hidden="true"></button>
                <h4class="modal-title">
                    <iclass="icon-book-open"></i>
                    <spanstyle="font-weight:bold">查看二维码信息</span>
                </h4>
            </div>
            <divclass="modal-body">
                <divclass="row"id="hiddenDiv">
                </div>
            </div>
            <divclass="modal-footer bg-info">
                <buttontype="button"class="btn btn-primary"data-dismiss="modal"onclick="AssetPrintOK()">打印文档</button>
                <buttontype="button"class="btn default"data-dismiss="modal">关闭</button>
            </div>
        </div>
    </div>
</div>

增加一个按钮的处理入口代码。

<buttontype="button"onclick="AssetPrint()"class="btn btn-circle btn-sm red">
    <iclass="fa fa-plus"></i>资产编码打印</button>

然后编写一个函数,用来选定记录,并动态赋值HTML代码

        functionAssetPrint() {var rows = $table.bootstrapTable('getSelections');if (rows.length == 0) {
showTips(
"请选择你要打印编码的记录");return;
}
var div = $("#hiddenDiv");
div.empty();
div.append(
"<div class='row'>");for (var i = 0; i < rows.length; i++) {
AppendCodeItem(rows[i].Code, rows[i].Name, rows[i].CurrDept, rows[i].ChargeDept, rows[i].UsePerson, rows[i].KeepAddr);
if (i > 0 && i % 3 == 0) {
div.append(
"<p style='page-break-after:always'>&nbsp;</p>");//分页打印 }
}
div.append(
"</div>");

$(
"#codeDiv").modal("show");
}

其中主要的逻辑封装在AppendCodeItem函数里面,这个函数就是批量根据数据进行处理的。

        //动态追加HTML内容在对应的层上,并初始化条形码和二维码的值
        functionAppendCodeItem(Code, Name, CurrDept, ChargeDept, UsePerson, KeepAddr) {var div = $("#hiddenDiv");

div.append(
"<div class='col-md-6'>");
div.append(
"<div class='form-group' style='padding-left:5px'>");
div.append(
"<label class='col-md-12'>资产名称:" + Name + "</label><br/>");
div.append(
"<label class='col-md-12'>管理部门:" + CurrDept + "</label><br/>");
div.append(
"<label class='col-md-12'>使用部门:" + ChargeDept + "</label><br/>");
div.append(
"<label class='col-md-12'>使用人:" + UsePerson + "</label><br/>");
div.append(
"<label class='col-md-12'>存放地点:" + KeepAddr + "</label><br/>");
div.append(
"</div>");//form-group div.append("</div>");//md-6 //加入一个dom,然后绑定二维码 div.append("<div class='col-md-6' style='padding-left;' id='qr_" + Code + "'></div>");//加入一个dom,然后绑定条形码 div.append("<div class='col-md-12'>")
div.append(
"<img id='bar" + Code + "'/>");
div.append(
"</div>");

div.append(
"<hr/>");var barname = "#bar" +Code;
JsBarcode(barname, Code, {
format:
"CODE128",
displayValue:
true,
fontSize:
20,
lineColor:
"#0cc",
height:
20,
width:
1.5});var qrname = "qr_" +Code;//Jquery.qrcode代码 //$("#" + qrname).qrcode({ width: 64, height: 64, text: Code }); //QRCodejs的语法 $("#" +qrname).empty();var qrcode = newQRCode(document.getElementById(qrname), {
text: Code,
width:
80,
height:
80,
colorDark:
"#000000",
colorLight:
"#ffffff",
correctLevel: QRCode.CorrectLevel.H
});
}

批量弹出层界面如下所示。

打印效果如下所示

这里本来想和单个界面设计一样的,不过布局好像不知道是受到什么原因的影响,多个记录的排布总是不理想,因此只好把条形码、二维码挨着放置了。

3、打印的分页处理

上面的代码我们做了打印的分页处理,如果不这样处理,则会导致一个标签信息可能不完整,如下效果所示。

这样如果要裁剪标签的话,这个就会作废了,需要重新打印,但是我们可以通过分页打印进行处理这个问题。

分页打印不复杂,只需要在对应的地方增加一个标记,标注为打印分页部分即可,如下所示。

<pstyle='page-break-after:always'>&nbsp;</p>

那么我们在实际测试,大概几个标签就会排满的时候,增加这个分页的标记就可以了,如下所示。

for (var i = 0; i < rows.length; i++) {
AppendCodeItem(rows[i].Code, rows[i].Name, rows[i].CurrDept, rows[i].ChargeDept, rows[i].UsePerson, rows[i].KeepAddr);
if (i > 0 && i % 3 == 0) {
div.append(
"<p style='page-break-after:always'>&nbsp;</p>");//分页打印 }
}