2023年2月

.Net Reactor 是一款比较不错的混淆工具,比VS自带的那个好用很多,一直以来也陪伴着我们的成长,虽然没有完美的混淆工具,不过也算还是不错的,至少能在一定程度上对DLL进行一定的保护处理。

不过最近客户反映我们在混合框架删除操作的时候,没有如期的实现删除操作,由于混合框架是基于Web API / WCF这样的分布式开发方式,因此和普通跟踪的方式有所不同,针对Web API的使用是比较广泛的在云端实现数据集中管理的一种方式,相对普通的调试来说,难度增加了一些,需要在服务端(本篇主要是Web API操作)进行调试,以及在客户端界面进行联合调试处理。

本篇随笔主要介绍如何在碰到基于分布式处理数据的接口的时候,对错误问题的分析和逐步缩小范围的方式进行排查,最终解决问题的分析处理过程。

1、定位问题的发生

在我们出现问题的时候,往往需要定位在那个部分出现了错误,首先我们在客户端和服务端都需要进行跟踪调试,首先我们需要在Web  API 层跟踪对应的控制器操作是否获得对应要删除记录的ID值。

我们前面功能测试的时候,发现所有删除操作都出现了无法删除的问题,因此很可能是没有传递ID值,或者转换过程中出现了问题。

我们服务器端的删除操作接口如下所示。

        /// <summary>
        ///根据指定对象的ID,从数据库中删除指定对象(用于整型主键)/// </summary>
        /// <param name="key">指定对象的ID</param>
        /// <returns>执行成功返回<c>true</c>,否则为<c>false</c></returns>
[HttpPost]public virtual CommonResult Delete(KeyInfo keyInfo, string token, string signature, string timestamp, string nonce, stringappid)
{
//检查用户是否有权限,否则抛出MyDenyAccessException异常 base.CheckAuthorized(AuthorizeKey.DeleteKey, token, signature, timestamp, nonce, appid);

CommonResult result
= newCommonResult();try{if (keyInfo != null)
{
result.Success
=baseBLL.Delete(keyInfo.id);
}
}
catch(Exception ex)
{
LogTextHelper.Error(ex);
//错误记录 result.ErrorMessage =ex.Message;
}
returnresult;
}

其中的KeyInfo类是我们定义的一个实体类,定义代码如下所示。

    /// <summary>
    ///用于删除的ID对象/// </summary>
[Serializable]public classKeyInfo
{
/// <summary> ///ID主键/// </summary>public object id { get; set; }
}

我们在调试Web API控制器的时候,无法获得KeyInfo参数的值,如下界面所示。

那么可能KeyInfo无法被反序列化,又或者是KeyInfo没有传递过来,我们跟踪对应的接口,方向本来应该在客户端POST提交的ID信息,无法提交过来。

这个是针对客户端提交操作的抓包处理,本来想用Fiddler来抓取的,不过Fiddler好像无法直接抓取localhost的请求包体,通过代理设置没有处理成功,改用以前用的很顺手的 HttpAnalyzer,直接运行就可以抓取了,非常方便。

通过上面的操作,我们发现数据没有提交到控制器里面,因此排除Web API控制器反序列化对象的时候丢掉值的可能,
而是客户端根本没有提交数据过来

2、定位具体的值丢失位置

那么我们回到对删除操作的统一处理方法里面看看,有Delete和Delete2操作类似,分别对应不同类型处理。

我们发现这里的处理,是直接把ID传递过来构建一个匿名对象,然后序列化为JSON字符串提交给Web API控制器处理的。在界面上主要就是通过统一调用进行删除的,传递ID给对应接口进行处理的。

以权限系统模块,用户删除操作为例,它的界面端处理代码如下所示。

以上代码我增加了一行用来记录是否获得ID的内容的,通过日志记录,可以看到有ID传递给接口处理了。

这样看到,问题出现在接口的处理里面,也就是可能由于我对DLL采用了混淆操作,导致的匿名类解析出现了问题了。

我们首先重写一下具体类的删除接口操作,跟踪一下问题。

为了有效验证我们的问题出现在这里,我们对比勾选和取消了红色勾选,编译后的代码进行测试。

对比处理结果,我们可以看到混淆前后,接口获得的数据不同,因此可以知道是混淆导致匿名类处理出现了问题。

于是,我们对所有相关的DLL,取消对应的这个混淆选项,运行可以得到正确的结果。

以上就是我们对这个.Net Reactor混淆导致匿名类处理出现的问题处理分析,其中主要涉及到了客户端localhost地址的本地抓包处理,采用了比较方便易用的HttpAnalyzer来分析是否数据提交有问题,还是数据解析出现问题,定位问题的边界,然后逐步对界面和接口部分进行分析。

由于对DLL混淆导致的错误问题,一般来说不易推断,所以尽可能多的列出可能影响的因素,逐一测试解决,慢慢缩小范围即可获得解决问题的办法。

前阵子一直期待.net core3.0正式版本的出来,以为这个版本出来,Winform程序又迎来一次新生了,不过9.23日出来的马上下载更新VS,创建新的.net core Winform项目,发现并没有Winform窗体设计器。而微软目前则是通过插件的方式,让我们单独下载Winform设计器,这个设计器还是预览版本,很多功能还是没有实现的,只能算是一个简单的雏形,本博客案例介绍基于.net core3.0创建一个普通的WInform程序,让大家了解下基于.net core3.0创建的程序的大概模样。

1、开发环境的准备

要做基于.net core3.0的WInform开发,需要首先更新你的Visual Studio到16.3,这个版本是整合.net core3.0的,因此也是能够开发.net core Winform程序的基础。

其次是下载
winforms-designer
插件,这个是支持对Winform窗体的设计器,让我们可以通过拖动控件的方式进行界面的设计开发。

.NET Core Windows Forms 可视化设计器在将来一定是未来的Visual Studio 2019更新的一部分,但目前来说,想要可视化设计器,需要一个预发布的Visual Studio扩展。

完成这两个步骤,其他开发就和我们普通创建VS项目一样的。

创建项目后,我们可以打开对应的Winform窗体,并可以在工具箱里面看到一些Winform界面控件,好的是控件的大概和以前差不多,不好的事情是少了很多常规Winform控件,这个也是目前WInform 设计器处于开发预览版的原因所在吧。

2、创建一个WInform程序

为了创建一个简单测试的WInform程序,我们可以往里面添加一些WInform的界面控件,不过使用过程中,发现很多界面所需元素没有提供界面控件的支持,包括工具栏、属性里面都还不完善,如ImageList对象和Image对象的属性支持等,我们只能通过代码的方式进行使用。

我创建一个简单的WInform界面,拖动了一些常规的控件,但是一些控件需要使用图片的,如ListView、PictureBox等这些,需要通过代码设置(无法通过属性加入的方式指定图片)

最后界面展示效果如下所示。

窗体源码如下所示。

   public partial classForm1 : Form
{
publicForm1()
{
InitializeComponent();
}
private void button1_Click(objectsender, EventArgs e)
{
MessageBox.Show(
"你好,这是一个.net core的Winform程序", "提示信息",
MessageBoxButtons.OK, MessageBoxIcon.Information
|MessageBoxIcon.Asterisk);
}
private ImageList imageList = newImageList();private void Form1_Load(objectsender, EventArgs e)
{
var image = Image.FromFile(Path.Combine(Application.StartupPath, "SplashScreen.png"));if(image != null)
{
this.pictureBox1.Image =image;
}

imageList.Images.Clear();
var iconPath = Path.Combine(Application.StartupPath, "icons");var fileNames = Directory.GetFiles(iconPath, "*.ico");foreach(string file infileNames)
{
imageList.Images.Add(file, Image.FromFile(file));
}
this.treeView1.ImageList =imageList;foreach(TreeNode node in this.treeView1.Nodes)
{
SetNodeImage(node);
}
this.button1.Image = imageList.Images[2];
}
private voidSetNodeImage(TreeNode node)
{
foreach (TreeNode subNode innode.Nodes)
{
subNode.ImageIndex
=subNode.Level;
subNode.SelectedImageIndex
=subNode.Level;
SetNodeImage(subNode);
}
}

从中我们可以看到,.net core下的WInform程序,它的窗体元素或者相关对象,没有发生不一致命名的情况,用起来还是非常方便一致的,不过就是对应很多界面的功能,目前只能通过后台代码的方式进行补充,才能实现一个比较完整的效果,和.net Framework框架下已经完善的非常好的Winform开发,真的是差距不是一点半点,看来.net core winform开发的路还是很漫长,需要在工具层面更多的支持才行。

界面方案里面,我们看到命名空间也比以前少了很多了了。主要还是基于.net core 提供的WInform包。

我们再来看看程序目录下的文件如下所示。

由于目前我们还没有考虑第三方的.net core 层面的类库,因此这里没有使用第三方的DLL,以后整合的话,第三方相关的引用也是一个非常头大的问题,如果大多数常用的类库都有基于.net standard 的类库支持,那倒是好,否则可能会面临两难的抉择,不过.net core的Winform开发我觉得还是很值得期待的,毕竟引入一个整体的.net core开发路线,对企业或者个人来说,都是一个非常不错的开发场景。

在我们开发某个系统的时候,客户总会提出一些特定的报表需求,固定的报表格式符合他们的业务处理需要,也贴合他们的工作场景,因此我们尽可能做出符合他们实际需要的报表,这样我们的系统会得到更好的认同感。本篇随笔介绍如何基于FastReport报表工具,生成报表PDF文档展示医院处方笺的内容。

之前在随笔《
在Winform开发中使用FastReport创建报表
》介绍过FastReport这个强大的报表工具,虽然介绍了各种报表的处理代码,不过主要的案例还是官方的案例,本篇随笔介绍基于某个医院的处方笺的格式报表的处理。

FastReport.Net是一款适用于Windows Forms, ASP.NET和MVC框架的功能齐全的报表分析解决方案。FastReport.Net以C#语言编写而成并只包含可托管的代码。它与.NET Framework 2.0以及更高版本兼容。支持在报表中添加文本、图像、线条、形状、语句、条形码、矩阵、表格、RTF、选择框等,列表报表、分组报表、主从报表、多列报表,子报表都可以实现处理。通可以为终端用户提供一个报表设计器,让用户可以方便的修改现有报表和创建自定义报表。

1、定义报表模板

和其他常规的报表工具一样,FastReport.Net报表工具也需要定义好报表模板文件,然后再基于这个报表模板进行内容的呈现,报表模板一般定义标题、报表页眉、明细内容、页脚等信息。

我们来看看大概的需求效果,这个是处方笺的常规格式。

我大概需要弄个类似格式的处方笺的报表,其中处方药需要动态生成,以及患者信息、医生审核签字的地方需要动态生成,当然,二维码,条码等内容也需要一并根据信息动态生成出来,由于我主要想通过PDF展示,因此使用报表工具生成PDF文档,已经预览或者下载即可。

我们先来看看最终设计好的报表模板,在FastReport设计器里面的效果如下所示。

其中,标题部分,主要在页眉,需要展示处方列表的在数据区展示,页脚放置一些联系信息等,这样就构建了一个完整的报表模板。

创建一个报表模板,我们先要定义报表页面格式,报表报表的宽度,高度是自定义的还是标准的,还要设置它的页边距等信息,如下所示。

页边距设置如下所示。

由于这个报表包含了主表信息,和明细表的信息,我们主表动态信息,可以通过参数的绑定方式绑定,明细表则通过绑定DataTable的方式动态处理即可。

采用参数绑定,我们需要在报表设计器里面定义好我们需要的参数,如下所示。

我们一般预先定义好相关的参数,然后绑定在模板里面,并设置好内容的对其格式即可。

如报表页面里面,我们放置了一个表格,定义好表格的行列和宽度后,双击表格单元格,就可以设置表格单元格的文本内容为对应的参数了,如下界面所示。

为了展示每项的序号,我们也需要使用到系统变量,如我们需要展示下面的内容。

那么需要定义好每项的序号,和数据字段名称。

对于动态展示的明细列表部分,我们需要定义一个数据源的方式,从而可以让报表模板绑定对应的字段名称。

我根据数据表的信息,生成一个用于绑定明细列表的数据源,如下所示。

这样我们在代码绑定的时候,只需要指定Detail的名称和对应的字段名称即可,有了这些定义,我们可以在报表设计中使用字段绑定了。

在数据区拖入对应的字段定义,并调整文本大小和对其,就可以设计出明细的部分字段绑定了。

对于二维码和条码,我们可以从报表工具栏里面拖入对应的控件,并设置对应的绑定参数和显示内容即可(这些也可以通过参数,运行的时候进行动态绑定)。

最后设计好的报表如开始介绍那样,是一个完整的报表模板了。

预览的时候,我们可以看到内容绑定的地方都是空白,因为我们没有绑定数据源的原因,不过整个报表的格式已经出来了,大概就是我们需要的结果。

2、生成报表PDF内容

通过上面报表模板的设计,我们基本的前期工作就准备好了,需要的就是根据实际业务的需要,动态呈现数据了。

在绑定数据并生成PDF格式报表的时候,我们需要先构建一个报表对象,如下代码所示。

//生成PDF报表文档到具体文件
Report report = newReport();
report.Load(reportPath);

由于数据我们是动态构建的,因此我们需要准备参数数据源和字段数据源两个部分,参数我们用字典来承载,字段数据,我们用DataTable来承载,如下所示。

//定义参数和数据格式
var dict = new Dictionary<string, object>();var dt = DataTableHelper.CreateTable("ProductName,Quantity|int,Unit,Specification,HowTo,Frequency");

然后我们根据系统需要填入动态的数据,如下代码所示。

//准备数据
dict.Add("Name", info.PatientName);
dict.Add(
"Gender", info.Gender);var age =info.BirthDate.GetAge(); dict.Add("Age", age);
dict.Add(
"Telephone", info.Telephone);
dict.Add(
"CreateTime", info.CreateTime);var checkDoctor = BLLFactory<User>.Instance.GetFullNameByOpenID(info.CheckDoctor);
dict.Add(
"CheckDoctor", !string.IsNullOrEmpty(checkDoctor) ? checkDoctor : "未知");var CheckPharmacist = BLLFactory<User>.Instance.GetFullNameByOpenID(info.CheckPharmacist);
dict.Add(
"CheckPharmacist", !string.IsNullOrEmpty(CheckPharmacist) ? CheckPharmacist : "未知");var SendUser = BLLFactory<User>.Instance.GetFullNameByOpenID(info.SendUser);
dict.Add(
"SendUser", !string.IsNullOrEmpty(SendUser) ? SendUser : "未知");var qrcode = string.Format("{0}/h5/PrescriptionDetail?id={1}", ConfigData.WebsiteDomain, info.ID);
dict.Add(
"QrCode", qrcode);
dict.Add(
"BarCode", info.PrescriptionNo);if(detailList != null)
{
foreach(var item indetailList)
{
var dr =dt.NewRow();
dr[
"ProductName"] =item.ProductName;
dr[
"Quantity"] =item.Quantity;
dr[
"Unit"] =item.Unit;
dr[
"Specification"] = "";
dr[
"HowTo"] =item.HowTo;
dr[
"Frequency"] =item.Frequency;
dt.Rows.Add(dr);
}
}

最后根据上面的数据,绑定并生成PDF报表即可,如下代码所示。

//刷新数据源
report.RegisterData(dt, "Detail");foreach (string key indict.Keys)
{
report.SetParameterValue(key, dict[key]);
}
//运行报表 report.Prepare();//导出PDF报表 PDFExport export = newPDFExport();
report.Export(export, realPath);
report.Dispose();

由于这个功能我们是在微信公众号里面集成的一个报表呈现,因此我们可以通过PDF预览的方式,或者直接打开PDF文档。、

如果采用PDF在线预览方式,可以参考我随笔《
实现在线预览PDF的几种解决方案
》介绍的那样,最终采用PDFJS的在线预览方案,不管在微信端,还是Web端都是比较不错的效果。

如果采用PDFJS预览方式,那么JS代码如下所示。

    var baseUrl = "@ViewBag.WebsiteDomain/Content/JQueryTools/pdfjs/web/viewer.html";var url = baseUrl + "?file=" + filePath;//实际地址
    location.href = url;

如果是直接打开PDF,我们我们就直接传递给浏览器一个PDF文件路径即可

location.href = filePath

在微信端预览的效果如下所示。

使用FastReport报表,总体来说,工作量主要是在设计报表模板这里,通过代码实现数据绑定的工作反而非常简单,只需要指定对应的参数和字段数据表即可,而报表的设计是一项精细的工作,我们需要根据实际情况,反复调整格式和呈现的效果才能做到尽善尽美,不过整体来说FastReport提供了非常强大的报表设计和处理过程,使得我们可以在设计一些复杂报表的时候,可以更加高效。

3、采用其他报表设计-锐浪报表设计展现

在选项使用FastReport报表呈现的时候,我也试过锐浪报表的处理方式,锐浪报表的整体呈现效果也是非常不错的,这里顺便介绍一下锐浪报表的设计、运行时绑定数据源等的步骤代码,以供参考。

首先我们需要定义好一个报表的模板信息,和FastReport报表模板一样,也是类似的定义方式,报表模板设计如下所示。

上面我们可以看到,它也是有参数绑定和字段绑定两种方式。

实现数据绑定的代码如下所示。

//生成PDF报表文档到具体文件
GridExportHelper helper = newGridExportHelper(reportPath);var json = FileUtil.FileToString(Path.Combine(System.AppDomain.CurrentDomain.BaseDirectory, "Report/Pres.json"), Encoding.UTF8);bool success =helper.ExportPdf(json, realPath, HttpContext);if(success)
{
result
= Content(exportPdfPath);//返回Web相对路径 }
helper.Dispose();
//销毁对象

其中ExportPdf接收一个JSON字符串,实现代码如下所示。

        /// <summary>
        ///导出PDF/// </summary>
        /// <typeparam name="T">列表对象类型</typeparam>
        /// <param name="list">列表对象</param>
        /// <param name="filePath">存储路径</param>
        /// <param name="context"></param>
        /// <returns></returns>
        public bool ExportPdf(string json, stringfilePath, HttpContextBase context)
{
//从对应文件中载入报表模板数据 Report.LoadFromFile(this.ReportPath);//加载JSON对象 Report.LoadDataFromXML(json);

IGRExportOption ExportOption
=Report.PrepareExport(GRExportType.gretPDF);var exportPdf =Report.ExportToBinaryObject();
Report.UnprepareExport();
var succeeded =exportPdf.SaveToFile(filePath);returnsucceeded;
}

最后呈现的大概效果如下所示。

在做微信公众号或者企业微信开发业务应用的时候,我们常常会涉及到图片预览、上传等的处理,往往业务需求不止一张图片,因此相对来说,需要考虑的全面一些,用户还需要对图片进行预览和相应的处理,在开始的时候我使用JSSDK方式,使用微信的SDK接口进行图片的上传、预览操作,后来发现通过URL.createObjectURL选定本地图片预览、上传也是非常方便的,本篇随笔针对同一个多图片的业务需求,使用JSSDK和URL.createObjectURL两种方式进行图片预览、上传、删除等常规的处理。

1、使用JSSDK对图片的处理

在一个公众号页面-问诊界面里面,我们需要让用户上传相关的图片,包括症状图片、处方图片等,每个列表可以上传多张图片,如下界面所示。

这里使用了SDK进行图片的上传处理,参考Weui的上传样式,选择本地几张图片,可以看到缩略图展示在图框里面,但是图片还没有上传,我们在保存问诊信息的时候,才启动图片文件的上传处理。

如果图片是在编辑界面中,我们需要考虑对现有图片进行删除的处理,删除前确认即可。

单击删除图标的按钮,提示用户进行图片删除确认即可。

以上就是我们几个图片处理的场景,我们来看看如何实现的。

我们以症状图片为例,它的界面HTML部分的代码如下所示。

<divclass="weui-cells__title">症状图片</div>
<divclass="weui-cells weui-cells_form">
    <divclass="weui-cell">
        <divclass="weui-cell__bd">
            <divclass="weui-uploader">
                <!--编辑的时候,放置已有图片进行预览-->
                <ulclass="weui-weui-uploader__files"style="list-style-type: none"id="imgSickPreview"></ul>
                <divclass="weui-uploader__bd">
                    <!--放置选择的图片进行预览-->
                    <ulclass="weui-weui-uploader__files"style="list-style-type: none"id="imgSick"></ul>
                    <divclass="weui-uploader__input-box">
                        <!--图片上传的图标处理-->
                        <spanid="uploaderSick"class="weui-uploader__input"></span>
                    </div>
                </div>
            </div>
        </div>
    </div>
</div>

为了使用微信JSSDK来实现上传、预览图片的功能,我们需要定义好对应的JS接口,如下代码所示。

    <scriptlanguage="javascript">
        varappid= '@ViewBag.appid';varnoncestr= '@ViewBag.noncestr';varsignature= '@ViewBag.signature';vartimestamp= '@ViewBag.timestamp';

wx.config({
debug:
false,
appId: appid,
//必填,公众号的唯一标识 timestamp: timestamp,//必填,生成签名的时间戳 nonceStr: noncestr,//必填,生成签名的随机串 signature: signature,//必填,签名,见附录1 jsApiList: ['checkJsApi','chooseImage','previewImage','uploadImage','downloadImage','getLocalImgData']
});

......
</script>

在上传图片之前,我们需要通过JSSDK的方式选择图片,这里用到了chooseImage的接口,大概所需的代码如下所示。

        //上传图片集合[用微信上传的时候,记录微信mediaId集合]
        var images ={
localSickId: [],
//病情 localPresId: [],//处方 serverSickId: [],
serverPresId: []
};
//图片选择 $("#uploaderSick").click(function() {
chooseImage(
"imgSick", "sick");
});
$(
"#uploaderPres").click(function() {
chooseImage(
"imgPres", "pres");
});
//选择图片显示 functionchooseImage(ctrlName, type) {//清空集合 if (type == "sick") {
images.localSickId
=[];
}
else{
images.localPresId
=[];
}

wx.chooseImage({
count:
3, //默认9 sizeType: ['original', 'compressed'], //可以指定是原图还是压缩图,默认二者都有 sourceType: ['album', 'camera'], //可以指定来源是相册还是相机,默认二者都有 success: function(res) {var ctrl = $("#" +ctrlName);
ctrl.html(
"");//清空图片显示 //localIds = res.localIds; // 返回选定照片的本地ID列表,localId可以作为img标签的src属性显示图片 if (type == "sick") {
images.localSickId
=res.localIds;
}
else{
images.localPresId
=res.localIds;
}
//动态增加img标识 $.each(res.localIds, function(index, item) {
ctrl.append(
"<img class='weui-uploader__file' src='" + item + "' />");
});
}
});
}

选择图片后,就是将图片的缩略图动态的增加在指定图片框里面。然后在保存数据的时候,使用JSSDK提交图片到微信服务器,我们服务器后台再从微信服务器获取图片(通过媒体id)。

这里我们定义了两类的图片,方便区分,分别是症状图片和处方图片,因此需要定义两个类别的变量,分别存储本地和服务器返回的id集合。

我们在进行表单提交的时候,需要确认一些必填项,然后在检查是否有文件需要上传,如果有则执行上传处理后提交表单,大概的处理代码如下所示。

        //上传数据
        $("#btnOK").click(function() {var PatientName = $("#PatientName").val();if (PatientName == '' || PatientName ==undefined) {
$.toast(
'患者姓名不能为空', "forbidden");return;
}
var ProblemDetail = $("#ProblemDetail").val();if (ProblemDetail == '' || ProblemDetail ==undefined) {
$.toast(
'详细描述不能为空', "forbidden");return;
}
//上传图片 if (images.localSickId.length > 0 || images.localPresId.length > 0) {
uploadImage(submitCallback);
//通过就提交数据 } else{
submitCallback();
}
});

这里主要的图片上传处理,就是 uploadImage 函数的处理了,而submitCallback这是定义一个函数上传表单数据的。

由于微信JSSDK上传图片,是一个个上传的,我们需要把它们串联起来,一并上传。uploadImage 里面定义了一个内部函数,依次进行图片的上传。

我们通过序号来标识两类图片,图片上传成功后,我们把图片媒体的id(JSSDK返回的)记录下来,统一提交给对应数据库记录,在后台进行图片文件的提取即可。

        //上传图片
        functionuploadImage(callback) {var localIds = images.localSickId.concat(images.localPresId);//合并数组
            var i = 0, length =localIds.length;//$.toast(length);
images.serverSickId=[];
images.serverPresId
=[];//定义一个子级函数,方便递归调用 functionupload(callback) {
wx.uploadImage({
localId: localIds[i],
success:
function(res) {
i
++;//成功后加入不同的集合 if (i <=images.localSickId.length) {
images.serverSickId.push(res.serverId);
//第一部分 } else{
images.serverPresId.push(res.serverId);
//第二部分 }if (i <length) {
upload(callback);
}
else if (callback !=undefined) {
callback();
//回调函数 }
},
fail:
function(res) {
alert(JSON.stringify(res));
}
});
};

upload(callback);
}

其中我们的定义的callback函数,是用来最后上传完成后,执行表单的记录存储的,表单包含各种输入和图片的ID信息,如下是详细的表单保存操作代码。

        //在上传图片后触发的回调函数
        functionsubmitCallback() {var druglist = [];//构造集合对象
            for (var key initemDict) {//Drug_ID,DrugName,How,Freq
druglist.push({'Drug_ID': key, "DrugName": itemDict[key], 'How': howDict[key],'Freq': freqDict[key], 'Quantity': quantityDict[key]
});
}
var url = "/H5/DrugInquirySave?openid=@ViewBag.openid";var postData ={
PatientName: $(
"#PatientName").val(),
Gender: $(
"#Gender").val(),
BirthDate: $(
"#BirthDate").val(),
Telephone: $(
"#Telephone").val(),
ProblemDetail: $(
"#ProblemDetail").val(),
Creator: $(
"#Creator").val(),
ProblemItems: $(
"input[name='ProblemItems']:checked").val(),
@
if (ViewBag.Info != null) {<text>ID:'@ViewBag.Info.ID',</text> }

SickAttachGUID: $(
"#SickAttachGUID").val(),
PresAttachGUID: $(
"#PresAttachGUID").val(),
ServerSickId: JSON.stringify(images.serverSickId),
ServerPresId: JSON.stringify(images.serverPresId),
DrugList: JSON.stringify(druglist)
};

$.post(url, postData,
function(json) {//转义JSON为对象 var data =$.parseJSON(json);if(data.Success) {
$.toast(
"处方已提交审核中,稍后请到处方查询查看。");//WeixinJSBridge.call('closeWindow');//关闭窗口 location.href = "/h5/Prescription";//跳转到处方页面 }else{
$.toast(
"保存失败:" + data.ErrorMessage, "forbidden");
}
})
;
};

我们注意到,我们服务端返回的ID集合,我们分别放在了两个字段里面提交到后台处理。

ServerSickId: JSON.stringify(images.serverSickId),
ServerPresId: JSON.stringify(images.serverPresId),

在后台,我们首先需要提取用户提交的基础表单数据,如下是后台定义的函数处理

这些是常规的表单信息,我们提交到微信服务器的图片信息也需要提取出来的,这些图片分两类,每类都包含多个字符串组成的图片ID集合。

后台主要就是根据这些ID,使用微信基础接口,获取临时图片的接口方式,把图片从服务器上下载下来存储到本地服务器上。

其中UploadFile函数就是封装了如何实现图片获取、图片存储的处理逻辑,主要的代码部分逻辑如下所示。

这种方式很好的利用了JSSDK的图片选择、上传的处理,实现了我们所需要的图片预览、选择、上传等一系列操作,也能够满足实际的功能需要。

不过总感觉把图片绕了一圈再回来不太好而已。

2、使用URL.createObjectURL对图片的处理

前面介绍了使用微信JSSDK方式实现图片预览、选择、上传等一系列操作,在上传文件的时候,感觉绕了一圈再回来,一直希望能够直接把文件直接提交到服务器上更好,就像我们一般的Web应用上传附件一样感觉更好一些,后来发现了可以通过URL.createObjectURL进行相关的处理,参考了一些案例,对前面介绍的JSSDK的图片上传方式进行改良,从而实现了把图片附件通过表单的方式直接提交到自己后台服务器上了,下面开始介绍一下这种方式的思路和实现代码。

首先我们定义一个预览图片的列表和一个Input的文件控件元素,替代前面的做法,如下所示。

<divclass="weui-cells__title">症状图片</div>
<divclass="weui-cells weui-cells_form">
    <divclass="weui-cell">
        <divclass="weui-cell__bd weui-cell_primary">
            <divclass="weui-uploader">
                <!--预览图片的列表-->
                <ulclass="weui-uploader__files"id="imgSick">
                </ul>
                <divclass="weui-uploader__input-box">
                    <!--上传图片附件控件-->
                    <inputid="uploaderSick"class="weui-uploader__input"type="file"accept="image/*"multiple="">
                </div>
            </div>
        </div>
    </div>
</div>

为了实现选择图片文件的时候,预览图片的列表可以动态变化(动态增加 li 项目),我们需要定义对应的事件来实现这个操作。

        //存放文件图片的集合
        var fileSick = newArray();var filePres = newArray();functioninitImage() {var tmpl = '<li class="weui-uploader__file" style="background-image:url(#url#)"></li>',
$gallery
= $("#gallery"),
$galleryImg
= $("#galleryImg"),

$uploaderSick
= $("#uploaderSick"),
$imgSick
= $("#imgSick"),
$uploaderPres
= $("#uploaderPres"),
$imgPres
= $("#imgPres");//症状图片上传 $uploaderSick.on("change", function(e) {var src, url = window.URL || window.webkitURL ||window.mozURL,
files
=e.target.files;for (var i = 0, len = files.length; i < len; ++i) {var file =files[i];
fileSick.push(file);
//加入集合 if(url) {
src
=url.createObjectURL(file);
}
else{
src
=e.target.result;
}
$imgSick.append($(tmpl.replace(
'#url#', src)));
}
});

..............

我们注意到了,这里没有使用chooseImage的JSSDK接口,而是通过 url.createObjectURL(file) 的方式获取路径,展示在图片列表控件里面。

对于动态增加的图片,我们可以让它支持单击预览的方式,预览其实是把图片放在一个预览层里面。

    var index; //第几张图片
    var category;//那个类别
    var imgid;//图片ID
    //症状图片单击处理
    $imgSick.on("click", "li", function() {
index
= $(this).index();
category
= "sick";
imgid
= $(this).attr("id");
$galleryImg.attr(
"style", this.getAttribute("style"));
$gallery.fadeIn(
100);
});

预览层的DIV是放在主界面上的,主界面是一个放置图片的区域,底部是一个删除按钮,用来我们实现图片删除操作的。

<!--图片预览层-->
<divclass="weui-gallery"id="gallery">
    <spanclass="weui-gallery__img"id="galleryImg"style="width:auto"></span>
    <divclass="weui-gallery__opr">
        <ahref="javascript:"class="weui-gallery__del">
            <iclass="weui-icon-delete weui-icon_gallery-delete"></i>
        </a>
    </div>
</div>

预览层再次单击的时候关闭,执行的JS代码如下所示。

$gallery.on("click", function() {
$gallery.fadeOut(
100);
});

删除图片的时候,我们区分是存在服务器的图片,还是本地临时选择的图片,区别对待。如果服务器图片,需要提示确认删除,如果是本地临时图片,直接移除即可。

//删除图片(根据类别和序号处理)
$(".weui-gallery__del").click(function() {                
console.log(index
+ ',' + category + ',' + imgid);//记录显示 //如果是在服务端的图片,确认后移除 if (imgid != undefined && imgid != '') {
$.confirm(
"您确定要永久删除该图片吗?", "永久删除?", function() {var url = "/H5/DeleteAttachment?openid=@ViewBag.openid";var postData ={
id: imgid.replace(
/img_/, '') //控件id去掉前缀为真正附件ID };

$.post(url, postData,
function(json) {//转义JSON为对象 var data =$.parseJSON(json);if(data.Success) {
$.toptip(
"删除成功!", 'success');//在界面上找到对应控件ID,移除控件 RemoveImg();
}
else{
$.toast(
"操作失败:" + data.ErrorMessage, "forbidden");
}
});
});
}
else{
RemoveImg();
//普通图片快速移除 };
});

其中移除图片显示的JS代码如下所示。

//移除对应类别的图片
functionRemoveImg() {if (category == "sick") {
$imgSick.find(
"li").eq(index).remove();
fileSick.splice(index,
1);
}
else{
$imgPres.find(
"li").eq(index).remove();
filePres.splice(index,
1);
}
};

我们要使用表单上传文件的方式,就需要在JS里面创建一个FormData的对象,用来承载文件内容,如下所示

var formData = new FormData();//构建一个FormData存储复杂对象

如果是常规的表单数据,我们通过键值,把内容填入FormData即可,如下所示。

var formData = new FormData();//构建一个FormData存储复杂对象
formData.append("PatientName", $("#PatientName").val());

如果是图片附件的,我们则需要遍历集合文件,把它们逐一加入对应键值里面,为了区分不同的类别文件,我们使用不同的前缀方式,如下代码所示。

//加入症状图片
for (var i = 0; i < fileSick.length; i++){
formData.append(
"sick_" +i, fileSick[i]);
};
//加入处方图片 for (var i = 0; i < filePres.length; i++){
formData.append(
"pres_" +i, filePres[i]);
};
//提交表单数据和文件
var url = "/H5/DrugInquirySave2?openid=@ViewBag.openid";
$.ajax({
url: url,
type:
'post',
processData:
false,
contentType:
false,
data: formData,
success:
function(json) {//转义JSON为对象 var data =$.parseJSON(json);if(data.Success) {
$.toast(
"处方已提交审核中,稍后请到处方查询查看。");//WeixinJSBridge.call('closeWindow');//关闭窗口 location.href = "/h5/Prescription";//跳转到处方页面 }else{
$.toast(
"保存失败:" + data.ErrorMessage, "forbidden");
}
}
});

在后台的处理函数 DrugInquirySave2 里面,我们需要把文件按键名提取出来,根据文件键名的不同,放到不同给的集合里面存储起来即可。

如下是DrugInquirySave2 函数里面的部分代码,用来处理收到的表单文件集合。然后我们在把文件写入文件系统即可,这样省却了对JSSDK提交文件,再去微信服务器提取文件方式的麻烦,直接由客户端把文件上传的自己的文件服务器了。

#region 通过文件附件方式获取
var files =Request.Files;if (files != null && files.Count > 0)
{
LogTextHelper.Info(
string.Format("收到文件:{0}", files.Count));//测试 foreach (string key infiles.Keys)
{
LogTextHelper.Info(
string.Format("收到文件key:{0}", key));var fileData =files[key];bool isSickImage = key.ToLower().IndexOf("sick") >= 0;//判断是否为问诊图片分类 if (fileData != null)
{
HttpContext.Request.ContentEncoding
= Encoding.GetEncoding("UTF-8");
HttpContext.Response.ContentEncoding
= Encoding.GetEncoding("UTF-8");
HttpContext.Response.Charset
= "UTF-8";string fileName = Path.GetFileName(fileData.FileName); //原始文件名称 string fileExtension = Path.GetExtension(fileName); //文件扩展名 FileUploadInfo fileInfo= newFileUploadInfo();
fileInfo.FileData
=ReadFileBytes(fileData);if (fileInfo.FileData != null)
{
fileInfo.FileSize
=fileInfo.FileData.Length;
}
//判断图片类别分组 fileInfo.Category = isSickImage ? "问诊图片" : "处方图片";
fileInfo.FileName
=fileName;
fileInfo.FileExtend
=fileExtension;//判断属于那个分组【这里只有两个分组】 fileInfo.AttachmentGUID = isSickImage ?SickAttachGUID : PresAttachGUID;

fileInfo.AddTime
= DateTime.Now;//创建时间 fileInfo.Editor = openid;//记录人 fileInfo.Owner_ID = info.ID;//属于主表记录 result= BLLFactory<FileUpload>.Instance.Upload(fileInfo);if (!result.Success)
{
LogTextHelper.Error(
"上传文件失败:" +result.ErrorMessage);
}
}
}
}
#endregion

编辑现有记录的时候,也可以实现对已有图片的删除操作,临时文件的预览处理和再次上传等操作。

3、两种图片处理方式的总结

本篇随笔是基于公众号上传图片文件的两种方式的处理,分别是使用微信JSSDK和使用URL.createObjectURL上传预览图片的不同处理对比,两种方式都能够满足图片的处理操作。对比处理代码,可能使用后者可能更加简洁一些。而且微信浏览器对URL.createObjectURL的支持也非常不错,可以在微信开发工具和实际环境上都正常使用。

在开发微信应用的时候,我们往往需要确认用户的身份,一般公众号唯一区别用户的身份是openid信息,但是这个信息并不是可以直接获取到,需要通过code进行获取,而code的获取则需要用户进行一个授权的处理才能获得,本篇随笔通过结合Session的方式,自动判断用户状态,如果用户首次访问页面,则以重定向的方式实现用户身份信息的获取并转回原来页面。

1、常规的页面身份获取处理

之前为了在某个页面里面获取用户身份信息,需要把URL进行一个授权的处理URL,如下所示。

通过这样的方式处理,我们可以在页面处理里面,获得code参数,然后根据code参数获取openid。

string code = Request.QueryString["code"];
var result =baseApi.GetAuthToken(accountInfo.AppID, accountInfo.AppSecret, code);if (result != null && !string.IsNullOrEmpty(result.openid))
{
Session["openid"] = result.openid;//存储在Session }

其中 GetAuthToken 是我们根据微信API进行封装的一个通过code换取网页授权access_token的接口方法。

        /// <summary>
        ///通过code换取网页授权access_token///首先请注意,这里通过code换取的是一个特殊的网页授权access_token,与基础支持中的access_token(该access_token用于调用其他接口)不同。///公众号可通过下述接口来获取网页授权access_token。///如果网页授权的作用域为snsapi_base,则本步骤中获取到网页授权access_token的同时,也获取到了openid,snsapi_base式的网页授权流程即到此为止。/// </summary>
        /// <param name="appId">公众号的唯一标识</param>
        /// <param name="appSecret">公众号的appsecret</param>
        /// <param name="code">code作为换取access_token的票据,每次用户授权带上的code将不一样,code只能使用一次,5分钟未被使用自动过期。</param>
        /// <param name="grantType">填写为authorization_code</param>
        /// <returns></returns>
        public AccessTokenResult GetAuthToken(string appId, string appSecret, string code, string grantType = "authorization_code")
{
var key = code + "_AuthToken";
AccessTokenResult itemResult
= MemoryCacheHelper.GetItem<AccessTokenResult>(key);if (itemResult == null)
{
var url = string.Format("https://api.weixin.qq.com/sns/oauth2/access_token?appid={0}&secret={1}&code={2}&grant_type={3}",
appId, appSecret, code, grantType);
var authToken = WeJsonHelper<AccessTokenResult>.ConvertJson(url);
MemoryCacheHelper.AddItem(key, authToken);
//先加入一个获取的值 }//然后每次从其中取,如果超过时间则启用刷新机制 var access_token = MemoryCacheHelper.GetCacheItem<AccessTokenResult>(key, delegate()
{
returnRefreshAuthToken(appId, itemResult.access_token);
},
new TimeSpan(0, 0, 7000)//7000秒过期 );returnaccess_token;
}

这种方式能够正常获取openid,不过每个菜单这样进行URL处理,并且每次重复这样的逻辑获取openid,肯定不是什么好的办法。

因此考虑一种通用的方式来处理,以减少这些曲折处理过程。

2、通用函数处理,以重定向的方式实现用户身份信息

上面提出了,采用常规处理方式,菜单URL需要先转移,后台重复处理code的转换,非常不方便我们开发业务功能。

其实我们可以把以上获取用户身份的处理放置在一个通用函数里面,这样每次确保获得Openid即可,如果第一次访问页面,那么记录当前页面地址,并重定向到获取code的页面,并解析code为openid即可,然后放在Session里面存储起来,下次直接读取Session获取即可。

我们首先在入口页面里面记录当前页面地址,然后转去判断并获取openid即可。

如果在session里面没有获取到Openid,那么认为是第一访问页面,重新判断是否有code进来,如果没有,先获取code,然后转回到当前的 AuthOpenId 地址入口来。

/// <summary>
///通过重新转向,获取用户code并转换为openid。///用于自动获取当前用户的身份。/// </summary>
/// <returns></returns>
protectedActionResult AuthOpenId()
{
//先判断Session是否存在 var open_id = Session["openid"];if (open_id == null)
{
//如果第一次(没有code),则再次转到授权页面重新获取code string code = Request.QueryString["code"];if (string.IsNullOrEmpty(code))
{
var authUrl = baseApi.GetAuthorizeUrl(accountInfo.AppID, Request.Url.AbsoluteUri, "", OAuthScope.snsapi_base); Response.Redirect(authUrl);return null;
}

如果是已经获取到了code,则根据code进行解析获取openid,如下代码所示。

        else{//如果成功获取code,那么根据code获取openid
            var result =baseApi.GetAuthToken(accountInfo.AppID, accountInfo.AppSecret, code);if (result != null && !string.IsNullOrEmpty(result.openid))
{
//LogHelper.Info("openid:" + result.openid); Session["openid"] = result.openid;//存储在Session }
}

最后如果顺利获得openid,那么返回到最初入口的页面地址(已经存放地址在Session里面了)

    //获取返回的连接
    var backUrl = Session["back_url"];if (backUrl != null)
{
returnRedirect(backUrl.ToString());
}
else{return View("PersonalInfo");//返回个人页面 }

整个函数的完整的代码如下所示。

/// <summary>
///通过重新转向,获取用户code并转换为openid。///用于自动获取当前用户的身份。/// </summary>
/// <returns></returns>
protectedActionResult AuthOpenId()
{
//先判断Session是否存在 var open_id = Session["openid"];if (open_id == null)
{
//如果第一次(没有code),则再次转到授权页面重新获取code string code = Request.QueryString["code"];if (string.IsNullOrEmpty(code))
{
var authUrl = baseApi.GetAuthorizeUrl(accountInfo.AppID, Request.Url.AbsoluteUri, "", OAuthScope.snsapi_base);
Response.Redirect(authUrl);
return null;
}
else{//如果成功获取code,那么根据code获取openid var result =baseApi.GetAuthToken(accountInfo.AppID, accountInfo.AppSecret, code);if (result != null && !string.IsNullOrEmpty(result.openid))
{
Session[
"openid"] = result.openid;//存储在Session }
}
}
//获取返回的连接 var backUrl = Session["back_url"];if (backUrl != null)
{
returnRedirect(backUrl.ToString());
}
else{return View("PersonalInfo");//返回个人页面 }
}

这样我们在菜单里面,就不需要前面所说的转义函数处理了,所有的身份获取都通过标准操作确保获取用户的openid了。

页面的处理也变得相对容易一些,根据用户身份显示不同的视图页面。

/// <summary>
///患者问诊/// </summary>
/// <returns></returns>
publicActionResult DrugInquiry()
{
Session[
"back_url"] =Request.Url.AbsoluteUri;
AuthOpenId();
//自动获取当前用户的身份。 string openid = (string)Session["openid"];if (!string.IsNullOrEmpty(openid))
{
//刷新配置JS的参数 RefreshTicket();
ViewBag.openid
=openid;var userInfo = BLLFactory<User>.Instance.FindByOpenId(openid);if (userInfo != null)
{
//识别用户身份后的处理逻辑 ...............
}
return View("DrugInquiry");
}
else{
ViewBag.Title
= "无法获取用户身份信息";
ViewBag.Message
= "无法获取用户身份信息";
ViewBag.Type
= "error";return View("info");
}
}

以上就是微信开发中使用通用函数处理,以重定向的方式实现用户身份信息的获取并转回原来页面的做法,这个函数给我们减轻了很多繁琐的问题,并且减少了重复复制代码来获取用户身份的弊端,是我们在H5页面里面处理用户身份信息的利器,希望对大家在开发微信公众号或者企业微信,获取用户身份的时候,提供好的参考思路和代码。