2023年2月

在我们开发微信应用的时候,包括公众号、企业微信、小程序等,都需要基于一定的框架基础上开发,才能事半功倍,一般情况下,我们需要处理好微信菜单和微信事件的对接,以及后台数据管理系统和权限管理的有效整合,或者是基于业务流程的相关流转等,本篇随笔介绍如何基于微信开发框架进行业务开发的处理过程。

1、微信账号配置

在我们开发任何微信基础应用之前,都需要先录入微信账号的相关信息,包括appId,AppSecret等基础信息,这个是用来获取token,和微信服务器进行交互的基础,我们的微信框架提供了多种类型和多个账号的管理。

其中在新增或者编辑界面中录入我们公众号或者企业微信的信息,如果涉及微信支付的,录入微信支付相关秘钥和证书位置。

如果是企业微信,我们选择账号类型为企业微信,然后根据提示录入相关企业微信信息即可。

其中主要是CorpID和CorpSecret和企业的AgentId信息,以及加密token和秘钥,保持和微信后台信息一致即可。

我们微信开发框架,提供了标准的微信账号信息填写界面,录入对应的信息,完成微信后台的对接,即可开启开发微信应用之旅了。

账号登录公众号后台或者企业微信后台,我们根据和账号配置一致对应的信息,完成系统对接即可,以下是微信公众号的配置对接界面。

企业微信的对接类似,不在赘述。

2、微信菜单配置

为了开发微信应用,我们还是需要准备好微信应用的菜单,菜单是我们所有前端微信功能的入口,我们一般需要先在微信开发框架 基础上进行菜单的录入维护,然后在提交到微信服务器上,实现应用菜单的提交。

在微信开发框架上维护菜单,可以根据需要禁用、启用某个菜单,然后再通过微信SDK接口提交更新到服务器进行菜单的更新,我们可以对多个账号的微信菜单进行维护。

1)医疗设备维修的微信应用菜单

例如我在开发一个医疗设备维修的微信应用的时候,菜单定义可能是这样子。

这样我们提交菜单后,在公众号上就可以对相关的功能进行操作了。

相关的菜单对应功能如下所示。

2)药店处方管理的微信应用菜单

我们再来看看,另一个微信公众号应用的菜单管理

提交到服务器后,在应用入口的菜单如下所示。

3)企业微信应用菜单配置

由于微信开发框架也支持企业微信接入,因此我们也可以同时开发企业微信应用的对接,首先也是一样,定义好对应的企业微信菜单。

以下是我们开发的一个资产管理的企业微信应用,配置了相关的菜单如下所示。

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

3、机构、角色、用户的权限管理

微信应用,除了提供相应的菜单功能外,一般应用前端的H5页面或者后台页面都可能需要结合机构、角色、用户和其权限信息进行系统权限的判断和数据的处理。

例如我们针对医疗设备微信的应用,在微信后台系统里面定义了相关的机构、用户、角色等信息。

组织机构如下所示。

用户角色如下所示:

系统根据角色对菜单进行不同的显示设置。

而我们如果应用不同,在系统后台调整这些机构、角色、用户信息即可,如下面则是药店处方定义的机构信息。

用户角色定义如下所示。

根据不同的业务需要,定义自己的组织机构层次,角色列表和人员信息即可,有了这些信息,我们可以同时对用户系统的操作功能和H5前端的权限进行控制管理。

4、H5页面的开发

开发微信公众号或者企业应用,很多时候工作的时间需要花费在H5页面的开发上,H5页面的规范我们可以参考微信的Weui(公众号) 或者WeUI for work (企业微信),以及借用一些漂亮的图表,以及现成的一些H5案例界面。WeUI还可以参考另一款 基于JQuery的 jQuery WeUI, 提供更多的案例和插件。

WeUI :
https://github.com/Tencent/weui

WeUI for work:
WeUI for Work (企业微信版)

jQuery WeUI:
http://jqweui.com/

WeUI+ 演示
https://weui.shanliwawa.top/

常规情况下,我们可以根据上面的一些案例,构建我们的一些输入或者显示元素,如日期输入、选择输入、字典绑定、图片展示、文件上传等等。

一般来说,H5页面开发,需要准备一些漂亮一点的图标,可以让程序增色不少,图片可以在网站下载或者淘宝购买一些设计图标,根据相应场景进行使用即可。以下是我自己收集的一些图表,在需要的时候,找一些匹配的使用即可。

H5页面如果有一些标准的案例来参考,做起来可能更加得心应手,如下是我自己收集的一些H5案例,根据实际项目的需要,在找一些对应的页面来模仿修改即可。



根据一些界面的样式,我们设计了一些类似的界面效果。

医疗设备维修的界面设计效果。

医药处方审核界面设计效果。



对于图片预览以及一些特殊功能,我们使用JSSDK实现即可,而表单的数据提交,主要就是POST、GET的JS代码处理,这样可以实现动态的数据绑定和处理,特别是在查询分页展示的时候,使用ajax的处理,可以提高体验效果。

最近,物联网的概念比较热门,一大批厂商抢着占领物联网的高低,包括有华为物联网、阿里云物联网、腾讯物联网、AWS物联网等等,无法一一列举,一般物联网包含设备侧开发、平台侧开发、应用侧开发,三个部分构成了线上线下的完整连接,和我们常规的微信应用、钉钉应用等不同,物联网的终端是由各种各样的设备组合而成,这些设备通过各种不同的协议(如CoAP,LWM2M、MQTT)连接到IOT的平台,而且这些设备是低能耗的设备,可以实时的发送数据上来,也可以接受来自IOT平台下发的各种操作指令。本篇随笔主要介绍基于华为物联网IOT的应用开发,实现对.net SDK的封装,方便后期进行应用集成使用。

1、物联网的相关介绍

物联网其实有点类似我们以前做的一些行业设备的接入,不过它相对比较通用化一些,可以连接各种各样的类型设备,而且更加安全、低能耗等,我们以前接入很多设备,可能需要走TCP/UDP协议,然后在后台服务器有一个对这些设备管理的一个Socket服务器,不过和物联网对比,这些都被彻底改造过了,以便适应更多的 应用场景,更简化的开发,以及支持更强大的功能吧。

物联网目前可以针对一些传感器采集一些特定的参数,如光感、温度、湿度、压力、电压、电流等常规的信息,也可以扩展实现语音、图像、视频等方面的采集处理,如经典的智慧路灯应用场景。

下面是其中的一个应用的架构设计,主要就是针对这些设备管理,物联网还提供了很多完善的应用API接口,使得我们可以更加简化对设备的管理(不用架设Socket服务),更加方便的通过API获取相应的信息,节省更多的维护成本。

物联网平台支持海量设备连接上云,设备与云端可以实现稳定可靠地双向通信。

  • 提供设备端SDK、驱动、软件包等帮助不同设备、网关轻松接入物联网云。
  • 提供2G/ 3G /4G、NB-IoT、LoRa、WiFi等不同网络设备接入方案,解决企业异构网络设备接入管理痛点。
  • 提供MQTT、CoAP、HTTP/S等多种协议的设备端SDK,既满足长连接的实时性需求,也满足短连接的低功耗需求。
  • 开源多种平台设备端代码,提供跨平台移植指导,赋能企业基于多种平台做设备接入。

一般的物联网平台,都会包括产品管理、设备管理、设备组管理、规则引擎管理、消息推送和消息订阅、任务管理、设备升级等等,不同的物联网云平台有所不同。

物联网的几个相关的协议:

MQTT(Message Queue Telemetry Transport)

MQTT是一个物联网传输协议,被设计用于轻量级的发布/订阅式消息传输,旨在为低带宽和不稳定的网络环境中的物联网设备提供可靠的网络服务。

MQTTS指MQTT+SSL/TLS,在MQTTS中使用SSL/TLS协议进行加密传输。

CoAP(Constrained Application Protocol)

受约束的应用协议(CoAP)是一种软件协议,旨在使非常简单的电子设备能够在互联网上进行交互式通信。

CoAPS指CoAP over DTLS,在CoAPS中使用DTLS协议进行加密传输。

LWM2M(lightweight Machine to Machine)

LWM2M是由OMA(Open Mobile Alliance)定义的物联网协议,主要使用在资源受限(包括存储、功耗等)的NB-IoT终端

2、应用侧开发接口

应用侧的开发接口一般云平台都会提供不同平台的SDK,如阿里云开源提供Java SDK/C# SDK等;而华为则提供了Java、PHP等SDK,没有包含.net 的SDK。

阿里物联网云的应用侧API接口包括:

华为物联网云的应用侧API接口包括:

本篇主要介绍最基础的物联网SDK的包装,以方便后续的应用开发集成。本篇随笔也主要是基于华为应用侧API的封装,使用C#语言实现对.net SDK的全部封装处理。

针对上面的接口分类,我们定义不同的接口类来处理它们。

基本上所有API访问都先需要通过鉴权接口获取访问的token,鉴权接口定义如下所示。

而且华为的API接口,需要使用X509证书处理的,我们可以通过在官网下载对应的X509证书进行集成测试API。

为了实现对API进行的.net SDK封装,我们定义一些系统常见变量,方便在接口中使用。

根据鉴权返回的结果,我们定义一个对应的实体类来存储这些属性信息,如下所示。

    /// <summary>
    ///鉴权结果/// </summary>
    public classAuthenticationResult : BaseJsonResult
{
/// <summary> ///申请权限范围,即accessToken所能访问物联网平台资源的范围,参数值固定为default。/// </summary> public string scope { get; set; }/// <summary> ///accessToken的类型,参数值固定为Bearer 。/// </summary> public string tokenType { get; set; }/// <summary> ///accessToken的有效时间,参数值固定为3600秒/// </summary> public int expiresIn { get; set; }/// <summary> ///鉴权参数,访问物联网平台API接口的凭证。/// </summary> public string accessToken { get; set; }/// <summary> ///鉴权参数,有效时间为1个月,用于“刷新Token”接口。///当accessToken即将过期时,可通过“刷新Token”接口来获取新的accessToken。/// </summary> public string refreshToken { get; set; }
}

然后根据鉴权定义,我们实现对这个接口的封装处理。

    /// <summary>
    ///IOT鉴权接口实现/// </summary>
    public classAuthenticationApi : IAuthenticationApi
{
/// <summary> ///用户鉴权///应用服务器首次访问物联网平台的开放API时,需调用此接口完成接入认证;///应用服务器在物联网平台的认证过期后,需调用此接口重新进行认证,才能继续访问物联网平台的开放API。/// </summary> publicAuthenticationResult Authentication()
{
string postData = string.Format("appId={0}&secret={1}", Constants.AppId, Constants.Secret);var url = Constants.AppBaseUrl + "/iocm/app/sec/v1.1.0/login";//名称大小写不能错 HttpHelper helper= newHttpHelper();
helper.ContentType
= "application/x-www-form-urlencoded";
helper.ClientCertificates
= new X509CertificateCollection() { newX509Certificate2(Constants.CertFilePath, Constants.CertPassword) };string content = helper.GetHtml(url, postData, true);var result = JsonConvert.DeserializeObject<AuthenticationResult>(content);returnresult;
}

对于Token的刷新操作,封装也是类似操作

        /// <summary>
        ///刷新token。///应用服务器通过鉴权接口获取到的accessToken是有有效时间的,在accessToken快过期时,///应用服务器通过调用此接口,获取新的accessToken。/// </summary>
        /// <param name="refreshToken">
        ///刷新token,用来获取一个新的accessToken。refreshToken在调用鉴权接口或刷新token接口时获得。/// </param>
        /// <returns></returns>
        public AuthenticationResult RefreshToken(stringrefreshToken)
{
string postData = new{
appId
=Constants.AppId,
secret
=Constants.Secret,
refreshToken
=refreshToken
}.ToJson();
var url = Constants.AppBaseUrl + "/iocm/app/sec/v1.1.0/refreshToken";//名称大小写不能错 var result = WeJsonHelper<AuthenticationResult>.ConvertJson(url, postData);returnresult;
}

上面有些是为了方便操作,定义了一些公用类库,以减少代码的重复。

一般除了鉴权外的所有的API,它们处理方式都类似的,都是以 application/json 格式进行交互,使用POST、PUT、GET、Delete操作方式实现对数据的处理。

而且它们都需要有固定的请求头信息,我们以设备注册为例进行介绍。

一般我们可以通过一个函数封装,对接口的头部请求信息进行设置,如下所示。

        /// <summary>
        ///通用获取头部信息/// </summary>
        /// <param name="accessToken">接口访问口令</param>
        /// <returns></returns>
        protected NameValueCollection GetHeader(stringaccessToken)
{
var header = newNameValueCollection();
header.Add(Constants.HEADER_APP_KEY, Constants.AppId);
header.Add(Constants.HEADER_APP_AUTH,
string.Format("Bearer {0}", accessToken));returnheader;
}

然后在定义请求的JSON和返回的JSON对应的实体类对象,封装对应的API接口即可。

例如,我们注册设备的接口封装如下所示。

    /// <summary>
    ///设备管理接口实现/// </summary>
    public classDeviceApi : BaseCommon, IDeviceApi
{
/// <summary> ///注册设备(验证码方式)。///在设备接入物联网平台前,应用服务器需要调用此接口在物联网平台注册设备,并设置设备的唯一标识(如IMEI)。///在设备接入物联网平台时携带设备唯一标识,完成设备的接入认证。/// </summary> /// <param name="accessToken"></param> /// <param name="info"></param> /// <returns></returns> public RegDeviceResult RegisterDevice(stringaccessToken, RegDeviceJson info)
{
var header =GetHeader(accessToken);string postData =info.ToJson();var url = Constants.AppBaseUrl + "/iocm/app/reg/v1.1.0/deviceCredentials";//名称大小写不能错 url += string.Format("?appId={0}", Constants.AppId);var result = WeJsonHelper<RegDeviceResult>.ConvertJson(url, postData, header);returnresult;
}
......

这里请求的信息是 RegDeviceJson , 返回信息的类是RegDeviceResult 。我们依照API定义,实现对应的处理即可。

为了方便处理,我们可以把这些对应设备的实体类定义在一个文件里面,如DeviceJson.cs里面,如下所示,这样非常方便我们管理。

其他业务范畴的Api也是如此封装,不在一一赘述。

我们测试的时候,可以建立一个单独的Winform项目进行接口功能的测试,也可以自己编写单元测试代码进行测试,根据自己熟练程度选择吧。

例如鉴权接口测试代码如下所示,我们可以看看输出进行判断是否正常工作。

        private void btnLogin_Click(objectsender, EventArgs e)
{
var result =basicApi.Authentication();
Console.WriteLine(result
!= null ? "accessToken:" + result.ToJson() : "获取结果出错");if (result != null)
{
var refreshResult =basicApi.RefreshToken(result.refreshToken);

Console.WriteLine(refreshResult
!= null ? "accessToken:" + refreshResult.ToJson() : "获取结果出错");this.accessToken = refreshResult.accessToken;//记录待用 }
}

设备注册的功能测试如下所示。

        private void btnRegDevice_Click(objectsender, EventArgs e)
{
if (string.IsNullOrEmpty(accessToken))
{
MessageUtil.ShowTips(
"请先鉴权获取AccessToken");return;
}
var deviceApi = newDeviceApi();var regDeviceInfo = newRegDeviceJson()
{
endUserId
= "64bf5869-b271-4007-8db8-fab185e19c10",
nodeId
= "64bf5869-b271-4007-8db8-fab185e19c10",
psk
= "12345678",
timeout
= 0,
verifyCode
= "",
deviceInfo
= newDeviceJson()
{
deviceType
= "Smoke",
manufacturerId
= "49ac78c99f3e453598c155870efe8bfc",
manufacturerName
= "iqidi",//与manufacturerId、manufacturerName、deviceType、model和protocolType系列参数二选一//productId = "5d9bf49b6a018f02d04cae28", model = "1001",
name
= "NBSimulator",
imsi
= "fafafasfasf",
mac
= "testetst",
isSecurity
= true.ToString().ToUpper(),
protocolType
= "LWM2M",
}
};
var regResult =deviceApi.RegisterDevice(accessToken, regDeviceInfo);
Console.WriteLine(regResult
!= null ? regResult.ToJson() : "no regResult");
}

另外对于事件的通知,我们一般是在应用端被动的进行相应的处理,因此需要对它们的消息进行转换和处理。

针对以上的消息通知,我们需要定义对应的消息类型,然后进行判断处理。

我们另起一个项目,然后定义对应给的事件接收处理,如下所示是一个统一的入口处理。

有了一个总入口,我们把对应通知的信息转换为对应的实体后,就可以进行记录或者响应的处理了。

在后面我们接着开发应用功能的时候,这些对应的接口API就可以集成整合在我们的系统中了。

以上就是对于华为物联网IOT平台应用侧的API封装处理的思路, 供大家参考交流。

SignalR是一个.NET Core/.NET Framework的实时通讯的框架,一般应用在ASP.NET上,当然也可以应用在Winform上实现服务端和客户端的消息通讯,本篇随笔主要基于SignalR的构建一个基于Winform的服务端和客户端的通讯处理案例,介绍其中的处理过程。

1、SignalR基础知识

SignalR是一个.NET Core/.NET Framework的开源实时框架. SignalR的可使用Web Socket, Server Sent Events 和 Long Polling作为底层传输方式。

SignalR基于这三种技术构建, 抽象于它们之上, 它让你更好的关注业务问题而不是底层传输技术问题。

SignalR将整个信息的交换封装起来,客户端和服务器都是使用JSON来沟通的,在服务端声明的所有Hub信息,都会生成JavaScript输出到客户端,.NET则依赖Proxy来生成代理对象,而Proxy的内部则是将JSON转换成对象。

RPC

RPC (Remote Procedure Call). 它的优点就是可以像调用本地方法一样调用远程服务.

SignalR采用RPC范式来进行客户端与服务器端之间的通信.

SignalR利用底层传输来让服务器可以调用客户端的方法, 反之亦然, 这些方法可以带参数, 参数也可以是复杂对象, SignalR负责序列化和反序列化.

Hub

Hub是SignalR的一个组件, 它运行在ASP.NET Core应用里. 所以它是服务器端的一个类.

Hub使用RPC接受从客户端发来的消息, 也能把消息发送给客户端. 所以它就是一个通信用的Hub.

在ASP.NET Core里, 自己创建的Hub类需要继承于基类Hub。在Hub类里面, 我们就可以调用所有客户端上的方法了. 同样客户端也可以调用Hub类里的方法.

SignalR可以将参数序列化和反序列化. 这些参数被序列化的格式叫做Hub 协议, 所以Hub协议就是一种用来序列化和反序列化的格式.

Hub协议的默认协议是JSON, 还支持另外一个协议是MessagePack。MessagePack是二进制格式的, 它比JSON更紧凑, 而且处理起来更简单快速, 因为它是二进制的.

此外, SignalR也可以扩展使用其它协议。

2、基于SignalR构建的Winform服务端和客户端案例

服务单界面效果如下所示,主要功能为启动服务、停止服务,广播消息和查看连接客户端信息。

客户端主要就是实时获取在线用户列表,以及发送、应答消息,消息可以群发,也可以针对特定的客户端进行消息一对一发送。

客户端1:

客户端2:

构建的项目工程,包括服务端、客户端和两个之间的通讯对象类,如下所示。

服务端引用

客户端引用

服务端启动代码,想要定义一个Startup类,用来承载SignalR的入口处理。

[assembly: OwinStartup(typeof(SignalRServer.Startup))]namespaceSignalRServer
{
public classStartup
{
public voidConfiguration(IAppBuilder app)
{
var config = newHubConfiguration();
config.EnableDetailedErrors
= true;//设置可以跨域访问 app.UseCors(Microsoft.Owin.Cors.CorsOptions.AllowAll);//映射到默认的管理 app.MapSignalR(config);
}
}
}

我们前面介绍过,服务端使用Winform程序来处理它的启动,停止的,如下所示。

因此界面上通过按钮事件进行启动,启动服务的代码如下所示。

        private void btnStart_Click(objectsender, EventArgs e)
{
this.btnStart.Enabled = false;
WriteToInfo(
"正在连接中....");

Task.Run(()
=>{
ServerStart();
});
}

这里通过启动另外一个线程的处理,通过WebApp.Start启动入口类,并传入配置好的端口连接地址。

        /// <summary>
        ///开启服务/// </summary>
        private voidServerStart()
{
try{//开启服务 signalR = WebApp.Start<Startup>(serverUrl);

InitControlState(
true);
}
catch(Exception ex)
{
//服务失败时的处理 WriteToInfo("服务开启失败,原因:" +ex.Message);
InitControlState(
false);return;
}

WriteToInfo(
"服务开启成功 :" +serverUrl);
}

连接地址我们配置在xml文件里面,其中的 serverUrl 就是指向下面的键url, 配置的url如下所示:

<?xml version="1.0" encoding="utf-8"?>
<configuration>
    <startup> 
        <supportedRuntimeversion="v4.0"sku=".NETFramework,Version=v4.6.2"/>
    </startup>
  <appSettings>
    <addkey="url"value="http://localhost:17284"/>
  </appSettings>

停止服务代码如下所示,通过一个异步操作停止服务。

        /// <summary>
        ///停止服务/// </summary>
        /// <returns></returns>
        private asyncTask StopServer()
{
if (signalR != null)
{
//向客户端广播消息 hubContext = GlobalHost.ConnectionManager.GetHubContext<SignalRHub>();await hubContext.Clients.All.SendClose("服务端已关闭");//释放对象 signalR.Dispose();
signalR
= null;

WriteToInfo(
"服务端已关闭");
}
}

服务端对SignalR客户端的管理是通过一个继承于Hub的类SignalRHub进行管理,这个就是整个SignalR的核心了,它主要有几个函数需要重写,如OnConnected、OnDisconnected、OnReconnected、以及一个通用的消息发送AddMessage函数。

客户端有接入的时候,我们会通过参数获取连接客户端的信息,并统一广播当前客户的状态信息,如下所示是服务端对于接入客户端的管理代码。

        /// <summary>
        ///在连接上时/// </summary>
        public overrideTask OnConnected()
{
var client = JsonConvert.DeserializeObject<ClientModel>(Context.QueryString.Get("Param"));if (client != null)
{
client.ConnId
=Context.ConnectionId;//将客户端连接加入列表 if (!Portal.gc.ClientList.Exists(e => e.ConnId ==client.ConnId))
{
Portal.gc.ClientList.Add(client);
}
Groups.Add(client.ConnId,
"Client");//向服务端写入一些数据 Portal.gc.MainForm.WriteToInfo("客户端连接ID:" +Context.ConnectionId);
Portal.gc.MainForm.WriteToInfo(
string.Format("客户端 【{0}】接入: {1} , IP地址: {2} \n 客户端总数: {3}", client.Name, Context.ConnectionId, client.IPAddress, Portal.gc.ClientList.Count));//先所有连接客户端广播连接客户状态 var imcp = newStateMessage()
{
Client
=client,
MsgType
=MsgType.State,
FromConnId
=client.ConnId,
Success
= true};var jsonStr =JsonConvert.SerializeObject(imcp);
Clients.Group(
"Client", new string[0]).addMessage(jsonStr);return base.OnConnected();

}
return Task.FromResult(0);
}

客户端的接入,需要对相应的HubConnection事件进行处理,并初始化相关信息,如下代码所示。

        /// <summary>
        ///初始化服务连接/// </summary>
        private voidInitHub()
{
。。。。。。
//连接的时候传递参数Param var param = new Dictionary<string, string>{
{
"Param", JsonConvert.SerializeObject(client) }
};
//创建连接对象,并实现相关事件 Connection = newHubConnection(serverUrl, param);。。。。。。//实现相关事件 Connection.Closed +=HubConnection_Closed;
Connection.Received
+=HubConnection_Received;
Connection.Reconnected
+=HubConnection_Succeed;
Connection.TransportConnectTimeout
= new TimeSpan(3000);//绑定一个集线器 hubProxy = Connection.CreateHubProxy("SignalRHub");
AddProtocal();
}
        private asyncTask StartConnect()
{
try{//开始连接 awaitConnection.Start();await hubProxy.Invoke<CommonResult>("CheckLogin", this.txtUser.Text);

HubConnection_Succeed();
//处理连接后的初始化 。。。。。。
}
catch(Exception ex)
{
Console.WriteLine(ex.StackTrace);
this.richTextBox.AppendText("服务器连接失败:" +ex.Message);

InitControlStatus(
false);return;
}
}

客户端根据收到的不同协议信息,进行不同的事件处理,如下代码所示。

        /// <summary>
        ///对各种协议的事件进行处理/// </summary>
        private voidAddProtocal()
{
//接收实时信息 hubProxy.On<string>("AddMessage", DealMessage);//连接上触发connected处理 hubProxy.On("logined", () => this.Invoke((Action)(() =>{this.Text = string.Format("当前用户:{0}", this.txtUser.Text);
richTextBox.AppendText(
string.Format("以名称【" + this.txtUser.Text + "】连接成功!" +Environment.NewLine));
InitControlStatus(
true);
}))
);
//服务端拒绝的处理 hubProxy.On("rejected", () => this.Invoke((Action)(() =>{
richTextBox.AppendText(
string.Format("无法使用名称【" + this.txtUser.Text + "】进行连接!" +Environment.NewLine));
InitControlStatus(
false);
CloseHub();
}))
);
//客户端收到服务关闭消息 hubProxy.On("SendClose", () =>{
CloseHub();
});
}

例如我们对收到的文本信息,如一对一的发送消息或者广播消息,统一进行展示处理。

        /// <summary>
        ///处理文本消息/// </summary>
        /// <param name="data"></param>
        /// <param name="basemsg"></param>
        private void DealText(stringdata, BaseMessage basemsg)
{
//JSON转换为文本消息 var msg = JsonConvert.DeserializeObject<TextMessage>(data);var ownerClient = ClientList.FirstOrDefault(f => f.ConnId ==basemsg.FromConnId);var ownerName = ownerClient == null ? "系统广播": ownerClient.Name;this.Invoke(new Action(() =>{
richTextBox.AppendText(
string.Format("{0} - {1}:\n {2}" +Environment.NewLine, DateTime.Now, ownerName, msg.Message));
richTextBox.ScrollToCaret();
}));
}

客户端对消息的处理界面

而客户端发送消息,则是统一通过调用Hub的AddMessage方法进行发送即可,如下代码所示。

        private void BtnSendMessage_Click(objectsender, EventArgs e)
{
if (txtMessage.Text.Length == 0)return;var message = newTextMessage() {
MsgType
=MsgType.Text,
FromConnId
=client.ConnId,
ToConnId
= this.toId,
Message
=txtMessage.Text,
Success
= true};

hubProxy.Invoke(
"AddMessage", JsonConvert.SerializeObject(message));
txtMessage.Text
= string.Empty;
txtMessage.Focus();
}

其中的hubProxy是我们前面连接服务端的时候,构造出的一个代理对象

hubProxy = Connection.CreateHubProxy("SignalRHub");

客户端关闭的时候,我们销毁相关的对象即可。

        private void Form1_FormClosing(objectsender, FormClosingEventArgs e)
{
if (Connection != null)
{
Connection.Stop();
Connection.Dispose();
}
}

以上就是SignalR的服务端和客户端的相互配合,相互通讯过程。

在我们实际开发报表的时候,我们需要按一定的业务规则组织好报表的模板设计,让报表尽可能的贴近实际的需求,在之前的随笔中《
使用FastReport报表工具生成报表PDF文档
》介绍了FastReport生成常规报表的处理,本篇随笔回顾常规报表的处理效果,并介绍基于FastReport生成标签纸打印需要的报表格式。

1、常规报表的处理

我们一般处理报表的时候,大多数情况碰到的是明细报表,或者有主从表这种样式的报表格式,明细报表就是只需要设计一个表头,按列表展示即可,如下所示格式。

或者类似一个基于XtraReport报表的功能界面,如下面图示所示。

这里面涉及的字段,包括字符型、日期型,数值型、枚举类型等,还有统计值、打印时间(参数),因此也算一个比较完整的报表展示了。

或者一些有一条记录构建成的报表信息,如下报表所示,这些也只是设计模板上的差异,在模板里面绑定对应的字段或者参数即可实现。

还有一种是主表有信息,从表有明细的数据展示方式,这个在随笔《
使用FastReport报表工具生成报表PDF文档
》有介绍过。

2、基于标签信息报表的打印处理

之前在随笔《
在Winform开发中使用Grid++报表
》和《
在Bootstrap开发框架中使用Grid++报表
》中介绍了使用锐浪报表来设计展现标签打印报表的处理,如下效果所示。

本篇随笔介绍基于FastReport报表工具生成标签打印文档的操作。

其实如果细心查找,FastReport也提供了很多Demo案例,其中就有标签的案例介绍,设计效果如下所示。

报表预览效果如下所示。

我们要做的类似,不过我们需要增加二维码、条码上去进行打印而已。

首先我们需要设计一个报表模板,设计格式如下所示。

在其中我们需要加入一个数据源进行绑定和测试预览效果,我们选择SQLServer一个表的数据进行创建数据源,如下所示。

设计报表,我们需要根据实际标签纸张预先设计好报表页面大小,如下所示。

如果需要展示多少列,可以通过页面设置中的列进行指定划分多少列,如下所示。

不过在实际测试的时候,这样的属性设置,报表渲染的时候,是按指定高度,从左列到右列进行依次展现的,如果我们需要按实际渲染高度,那么这里可以设置为1,然后由数据区进行设置列的数量即可。如下所示。

数据区设置列数为实际需要展现的列数,这个根据宽度预览看大概设计多少列合适,让页面高宽尽可能利用好即可。

二维码标签报表格式设计效果大概如下所示。

我们使用数据源的数据预览下效果,效果还是杠杠的。

报表模板弄好了,我们就需要如何生成FastReport报表或者导出PDF了。

我在之前的随笔《
使用FastReport报表工具生成报表PDF文档
》里面介绍了FastReport报表的处理代码,这里做法依旧差不多,绑定数据源即可展示或者导出PDF了。

最主要的代码如下所示。

//生成PDF报表文档到具体文件
Report report = newReport();
report.Load(reportPath);
//定义参数和数据格式 var dict = new Dictionary<string, object>();var dt = DataTableHelper.CreateTable("ID,Name,CurrDept,Code,UsePerson,KeepAddr");if (list != null)
{
foreach (var info inlist)
{
var dr =dt.NewRow();
dr[
"ID"] =info.ID;
dr[
"Name"] =info.Name;
dr[
"CurrDept"] =info.CurrDept;
dr[
"Code"] =info.Code;
dr[
"UsePerson"] =info.UsePerson;
dr[
"KeepAddr"] =info.KeepAddr;
dt.Rows.Add(dr);
}
}
//刷新数据源 foreach (string key indict.Keys)
{
report.SetParameterValue(key, dict[key]);
}
report.RegisterData(dt,
"T_Asset");//运行报表 report.Prepare();

如果需要导出PDF,那么代码增加部分导出处理即可。

//导出PDF报表
PDFExport export = newPDFExport();
report.Export(export, realPath);
report.Dispose();

我们来看看实际在浏览器预览的效果,如下所示。

以上效果结合了pdfJS的在线预览PDF操作,如果需要了解PDF的在线处理,参考下随笔《
实现在线预览PDF的几种解决方案
》。

在前面随笔《
基于华为物联网IOT的应用开发 --- 基于.net 的SDK封装
》介绍过IOT中应用侧SDK的封装,主要就是基于华为IOT的应用侧封装,以便在应用系统中进行调用。应用侧SDK的封装是一切应用开发的基础,不过华为并没有提供对应.net的SDK封装,不过SDK都是基于Web  API 的JSON数据进行交互,因此花了一点时间进行了华为.net 的SDK进行了全面封装,本篇随笔介绍如何基于封装好的IOT 应用侧的SDK进行界面管理的开发,实现对IOT业务数据的管理等。

1、应用侧SDK封装回顾

应用侧的开发接口一般云平台都会提供不同平台的SDK,如阿里云开源提供Java SDK/C# SDK等;而华为则提供了Java、PHP等SDK,没有包含.net 的SDK。华为物联网云的应用侧API接口包括:

基于对应应用侧API接口的定义,我们使用C#进行了对应接口的封装。

包含了一个测试接口项目、一个SDK封装接口项目,以及一个订阅的消息推送接口的解析和处理,例如新增设备、设备信息变化、设备绑定激活等。

对于事件的通知,我们一般是在应用端被动的进行相应的处理,因此需要对它们的消息进行转换和处理。

类似在管理后台订阅这些事件,然后这些事件触发后会推送给应用服务器。

2、接口的测试使用

根据IOT的应用侧API的定义,我们编写一个快速测试SDK工作情况的Winform程序来测试,如获取授权信息,以及查询产品、注册设备、修改设备、获取设备状态和详细等等接口的正常与否,然后再在Web应用中进行整合,这样可以减少返工调试的问题。

按照业务接口的归类测试,我们编写如下程序用来测试对应SDK接口。

在使用SDK接口前,我们需要知道,华为IOT的API接口,基本上全部需要证书的对接的,这个和我们开发微信API有所差异。例如,我们的Winform测试程序,让它带有一个Cert目录下的证书文件,这个证书文件可以在API的Demo里面找到,或者在华为IOT平台上下载。

我们最终用到的是cert/outgoing.CertwithKey.pkcs12这个证书文件。

我们创建一个HTTPRequest对象获取数据的时候,需要指定这个证书,如下所示。

helper.ContentType = "application/json";
helper.ClientCertificates
= new X509CertificateCollection() { new X509Certificate2(Constants.CertFilePath, Constants.CertPassword) };

其他部分就是对API接口封装的调用测试了。

如首先是授权的测试,用来获取AccessToken的,这个是调用其他接口的前提。

private void btnLogin_Click(objectsender, EventArgs e)
{
var result =basicApi.Authentication();
Console.WriteLine(result
!= null ? "accessToken:" + result.ToJson() : "获取结果出错");if (result != null)
{
var refreshResult =basicApi.RefreshToken(result.refreshToken);

Console.WriteLine(refreshResult
!= null ? "accessToken:" + refreshResult.ToJson() : "获取结果出错");this.accessToken = refreshResult.accessToken;//记录待用 }
}

华为IOT接口很多没有全部整合在一起,有的在
https://support.huaweicloud.com/api-IoT/iot_06_0003.html
,而有的则在
https://support.huaweicloud.com/api-iothub/iot_06_0008.html
, 就拿授权来说,还有另外一个版本的接口可以获取,两个接口获得的token都可以通用。

var url = Constants.AppBaseUrl + "/iocm/app/sec/v1.1.0/login";
var url = Constants.AppBaseUrl + "/api/v3.0/auth/tokens";

这两个地址都可以获取AccessToken,如设备创建,也是有多个接口,版本不同。

我们针对各个接口的封装,对相关接口进行测试,如基于产品、产品下面的列表等信息,可以在一个接口测试代码里面进行测试,如下所示。

        /// <summary>
        ///产品相关接口测试/// </summary>
        private void btnProduct_Click(objectsender, EventArgs e)
{
if (string.IsNullOrEmpty(accessToken))
{
MessageUtil.ShowTips(
"请先鉴权获取AccessToken");return;
}
try{var productApi = newProductApi();var deviceApi = newDataCollectionApi();//查询产品列表 var queryJson = new QueryProductJson { ownerAppId =Constants.AppId };var result =productApi.QueryProduct(accessToken, queryJson);
Console.WriteLine(result
!= null ? result.ToJson() : "no result");if (result != null && result.products != null)
{
//遍历产品,根据产品ID获取产品信息 foreach (var p inresult.products)
{
var detailResult =productApi.QueryProduct(accessToken, p.productId, Constants.AppId);
Console.WriteLine(detailResult
!= null ? detailResult.ToJson() : "no result");
}
//遍历产品下面的设备信息 foreach (var p inresult.products)
{
var deviceJson = new QueryDeviceJson { ownerAppId = Constants.AppId, productId =p.productId };var deviceResult =deviceApi.BatchQueryDevice2(accessToken, deviceJson);
Console.WriteLine(deviceResult
!= null ? deviceResult.ToJson() : "no result");
}
}
}
catch(Exception ex)
{
MessageUtil.ShowError(ex.Message);
}
}

对于设备,产品、设备组,都属于设备的入口之一,因此设备组也是管理接口设备的一个类别,我们可以根据SDK对设备组接口进行测试,如下代码所示。

        /// <summary>
        ///设备分组测试/// </summary>
        private void btnDeviceGroup_Click(objectsender, EventArgs e)
{
if (string.IsNullOrEmpty(accessToken))
{
MessageUtil.ShowTips(
"请先鉴权获取AccessToken");return;
}
try{//根据自己数据修改 var deviceId = "64bf5869-b271-4007-8db8-fab185e19c10";var groupName = "testGroup";var groupApi = newDeviceGroupApi();var groupJson = newCreateDeviceGroupJson()
{
appId
=Constants.AppId,
name
=groupName,
description
= "测试设备分组",
deviceIds
={ deviceId }
};
//创建设备组 var result =groupApi.CreateDeviceGroup(accessToken, groupJson);
Console.WriteLine(result
!= null ? result.ToJson() : "no result");//查询设备组 var queryJson = newQueryDeviceGroupJson()
{
accessAppId
=Constants.AppId,
name
=groupName
};
var queryResult =groupApi.QueryDeviceGroup(accessToken, queryJson);
Console.WriteLine(queryResult
!= null ? queryResult.ToJson() : "no result");//查询设备组成员 if (queryResult != null && queryResult.list != null)
{
foreach (var group inqueryResult.list)
{
//设备明细 var groupResult =groupApi.GetDeviceGroup(accessToken, group.id, Constants.AppId);
Console.WriteLine(groupResult
!= null ? groupResult.ToJson() : "no result");//设备组成员 var queryMemberJson = newQueryDeviceGroupMemberJson()
{
appId
=Constants.AppId,
devGroupId
=group.id
};
var queryMemberResult =groupApi.QueryDeviceGroupMember(accessToken, queryMemberJson);
Console.WriteLine(queryMemberResult
!= null ? queryMemberResult.ToJson() : "no result");
}
}
}
catch(Exception ex)
{
MessageUtil.ShowError(ex.Message);
}
}

3、IOT业务界面管理开发

当我们把大多数接口都跑一遍,并解决相关问题后,我们就可以根据这些接口进行Web应用的开发了。

为了方便,我们把IOT相关的业务,定义在一个控制器里面,方便管理,以后如果有需要,可以拆分进行管理。

例如,对于数据提供,主要就是分页JSON数据的处理,方便界面控件通过Ajax进行获取数据显示。

例如,对于产品列表,我们构建了一个控制器方法,如下所示。

        /// <summary>
        ///根据条件查询产品列表,并返回对象集合(用于分页数据显示)/// </summary>
        /// <returns>指定对象的集合</returns>
        public virtualActionResult FindProductWithPager()
{
var pagerInfo =GetPagerInfo();var sort =GetSortOrder();var accessToken =GetAccessToken();var queryJson = new QueryProductJson(pagerInfo) { ownerAppId =Constants.AppId };var result =productApi.QueryProduct(accessToken, queryJson);var list = result != null ? result.products : null;var totalCount = result != null ? result.totalCount : 0;//Json格式的要求{total:22,rows:{}} var jsonResult = new { total = totalCount, rows =list };returnToJsonContent(jsonResult);
}

其他,如设备组、设备列表等类似的处理,都是先通过接口获取数据,然后组装为对应的JSON格式提供给视图即可。

有时候,除了列表展示外,我们可能还需要对视图中创建、删除、获取单个明细的接口进行处理,如下所示是设备分组的管理接口。

其他的业务对象也是类似的封装,有了这些接口,我们就可以处理分页、获取详细、编辑、删除等接口的处理了。

在Web应用中定义几个业务菜单,用来管理产品信息、设备组和设备信息。

产品管理界面如下所示。

如果我们要查看产品明细,调用对应接口进行展示即可。

包含设备列表,我们根据产品ID获取对应设备列表返回到界面进行展示即可,如下所示效果。

设备分组如下所示。

而设备列表展示可以通过产品和设备组进行限定查询,我们如下管理这个设备列表的展示的。

同样,设备详细信息,通过对应ID调用SDK接口获取数据,并返回到视图即可展示出来了。设备的相关信息,如下历史数据、历史命令等,我们也可以通过对应接口进行数据获取返回,在界面的Tab控件进行展示即可。

如设备历史命令,可以获取到相关历史命令信息。

以上就是对IOT应用侧API接口的封装和应用界面的管理开发,不过使用过程中,对于IOT的接口还是不够完善,希望华为在这方便能够继续完善和提供良好的开发人员支持,我们也继续关注,以便在后续项目中整合物联网的硬件设备进行使用。