[转载]使用Windbg知道程序运行时的命令行参数.

mikel阅读(1076)

[转载]使用Windbg知道程序运行时的命令行参数. – 嗷嗷 – 博客园.

如何才能一个程序运行时的command line arguments是什么?其实这个信息被记录在了进程的process environment block中,我们可以用Windbg很方便的找出这个信息。

比如说,我在cmd上打开一个notepad, 如下

clip_image001

当notepad运行起来以后,使用windbg attach到notepad, 然后使用命令!peb打印出process environment block. 要注意symbol path要正确。

然后就能看到下面这些信息了。

clip_image002

我们能知道CommandLine了,也能知道DllPath和Environment了.

那么有时候这个程序运行的太快了,还来不及attach就跑完了怎么办?

没关系,我们可以设置notepad.exe一运行就被windbg断下来。这要用到windbg下的另外一个非常有用的工具了,gflags.exe. 按如下图设置notepad.exe

clip_image003

其效果是在注册表中添加了下面一些信息:

clip_image004

现在只要notepad以运行,首先就会被windbg断下来了。

总结,其实也不一定使用Windbg,使用VS的Debugger也是可以的,原理一样,就不详述了。

另外,使用Process Monitor来得到程序运行参数更方便

最后嘛,惯例是要配图的。不过最近手上没有好图,下次吧。

[转载]FLEX+Webservice 大附件上传、断点续传实现

mikel阅读(1173)

转载FLEX+Webservice 大附件上传、断点续传实现 – 大侠酷裤马路 – 博客园.

最近项目需要,面临大附件上传的功能,具体研究如下:
实现思路
大附件上传,如何流畅不占用内存,还要支持断点续传,当第一次看到这 些需求的时候还是有所顾虑,传统ASP.NET中利用fileupload可以实现上传,但是webconfig中文件大小受限制,即使设置大小了也将面 临超时的问题。对于上述情况,WINFORM应该能够很好的解决断点续传大文件,当应用到WEB应用中的时候就很难如此轻松了,因此富客户端思想是很好的 选择,决定采用FLEX实现客户端,Webservice实现客户端。
由于ASP.NET默认支持4M大小文件上传,一次需要将需要上传的文件进行分割,客户端分块上传,服务端分块追加。
具体实现
1)、客户端实现

客户端界面设计

<s:Group width=”100%” height=”100%”>
<s:Button id=”btnBrower” x=”390″ y=”25″ label=”浏览…” width=”60″ click=”btnBrower_clickHandler(event)”></s:Button>
<s:TextInput id=”edFile” x=”21″ y=”24″ width=”361″ enabled=”false”/>
<s:Button id=”btnUpload” x=”458″ y=”25″ label=”开始上传” width=”71″ click=”btnUpload_clickHandler(event)”/>
<mx:Canvas width=”508″ height=”25″ backgroundColor=”0Xf1f1f1″ x=”21″ y=”53″ borderStyle=”solid” borderColor=”0Xbbbbbb”>
<mx:Label text=”” fontWeight=”bold” id=”tip_txt” x=”5″ y=”4″/>
</mx:Canvas>
<mx:Canvas id=”totalProcess” borderStyle=”solid” x=”22″ width=”507″ y=”82″ height=”13″ borderColor=”0X124fc0″ backgroundColor=”0xffffff”>
<mx:Canvas backgroundColor=”0X124fc0″ backgroundAlpha=”0.5″ id=”processBar_Total” width=”0″ height=”23″/>
</mx:Canvas>

</s:Group>

定义Webservice方法,对应服务端,各自有自己的返回事件

<s:WebService id=service wsdl=../Service.asmx?wsdl useProxy=false>
<s:operation name=WriteFile result=onResult(event) fault=onFault(event)>
</s:operation>
<s:operation name=CheckFile result=onCheckResult(event) fault=onCheckFault(event)>
</s:operation>
<s:operation name=CopyFile result=onCopyResult(event) fault=onCopyFault(event)>
</s:operation>
</s:WebService>

浏览需要上传的文件

//页面加载完毕,进行相关事件注册
protected function application1_creationCompleteHandler(event:FlexEvent):void
{
// 注册调用js函数
ExternalInterface.addCallback(getFileInfo,GetFileInfo);

//初始化文件浏览事件
file.addEventListener(Event.SELECT,onSelect);//选择文件事件
file.addEventListener(Event.COMPLETE,onComplete);//文件加载完毕
file.addEventListener(Event.OPEN,onOpen);
btnUpload.enabled
=false;
}

//浏览文件
private function onSelect(evt:Event):void
{
this.tip_txt.text=“”;
edFile.text
=file.name;
//浏览完成,开始加载
file.load();
}
//加载文件完毕
private function onComplete(evt:Event):void
{
this.tip_txt.text=加载完毕;
btnUpload.enabled
=true;
}

private function onOpen(evt:Event):void
{
this.tip_txt.text=正在加载…;
}

校验服务端文件是否存在,不存在则创建,存在则判断是否需要断点续传

//校验文件函数
private function CheckFile():void
{
//调用webservice方法,传递文件名进行校验
service.CheckFile.send(file.name);
}
//webervice,校验成功
private function onCheckResult(event:ResultEvent):void
{
//获取返回值
var retArray:Array=new Array();
retArray
=event.result.toString().split(,);
var retMsg:String=retArray[0].toString();//返回文件存在与否消息
var retNum:int=int(retArray[1].toString());//返回文件大小
tip_txt.text=retMsg;

//判断是否存在文件
if(retNum != 0)
{
//若文件已经存在,判断文件是否已经上传完毕
if(retNum == file.data.length)
tip_txt.text
=文件已上传完毕;
else
{
tip_txt.text
=准备断点续传;

//断点续传需要重新计算块数,剩余大小
var Leave:int= file.data.lengthretNum;

//判断剩余情况
if(Leave>blocksize)
{
//剩余部分分块
var BlockNum2:Number=(Leave / blocksize);
BlockNum
=int(BlockNum2);
BlockNumles
=int(BlockNum2);
reBlock
=Leave % blocksize;
tip_txt.text
=正在处理…;
//调用上传函数
uploadFile(retNum,blocksize);
}
else
{
//直接从返回值大小开始,传递剩余部分
BlockNumles=1;
uploadFile(retNum,reBlock);
}
}
}
else
{
//若文件不存在,则创建,从0开始
var BlockNum1:Number=(file.data.length / blocksize);
BlockNum
=int(BlockNum1);
BlockNumles
=int(BlockNum1);
reBlock
=file.data.length % blocksize;

tip_txt.text=正在处理…;
uploadFile(retNum,blocksize);
}
}

private function onCheckFault(event:FaultEvent):void
{
Alert.show(event.toString());
}

开始上传

//上传附件函数
private function uploadFile(begin:int,end:int):void
{
//判断文件大小
if(file.data.length>blocksize)
{
//读取部分文件,分块上传
fileUpload.writeBytes(file.data,begin,end);
service.WriteFile.send(file.name,fileUpload);
}
else
{
//直接上传
service.WriteFile.send(file.name,fileUpload);
}
}

//webservice相关事件函数,上传成功
private function onResult(event:ResultEvent):void
{
//每次上传成功返回值,作为下次传递的开始位置
var begin:int=int(event.result.toString());
BlockNumles
-=1;//递减
BlockNumadd+=1;//递增
//清空历史数据
fileUpload.clear();

//进度条
onProgress(begin,file.data.length);
//判断剩余块多少,进行不同情况的上传
if(BlockNumles>0)
{
uploadFile(begin,blocksize);
}

if(BlockNumles==0)
{
uploadFile(begin,file.data.length
begin);
tip_txt.text
=上传完毕!;
tip_txt.text
=开始扫描文件…;
service.CopyFile.send(file.name);
}
}
//上传失败
private function onFault(event:FaultEvent):void
{
Alert.show(
event.toString());
}

进度监视

//上传进度条
private function onProgress(Loaded:int,Total:int):void
{
processBar_Total.width
=(Loaded/Total)*506;
tip_txt.text
=已上传: + Loaded+/+Total;
if(Loaded==Total)
tip_txt.text
=已上传完毕;
}

上传完毕处理文件,完毕之后需要对文件进行类似处理,在这里是对文件进行重命名。具体在客户端可以体现出来

//复制文件
private function onCopyResult(event:ResultEvent):void
{
tip_txt.text
=扫描完成;
//文件上传结束,调用js函数
var f:String = showButton;
var m:String
= ExternalInterface.call(f);
trace(m);
}

private function onCopyFault(event:FaultEvent):void
{
Alert.show(
event.toString());
}

2)、服务端实现
对应客户端三个方法实现,分别是校验、上传、上传完毕
校验文件,并返回值

#region 校验文件是否存在
[WebMethod]
public string CheckFile(string FileName)
{
string FileSavePath = Server.MapPath(File/) + FileName;
if (!IsExistFile(FileSavePath))
return 文件不存在,0;
else
{
string FileSize = GetFileSize(FileSavePath).ToString();
return 文件已存在, + FileSize;
}
}
#endregion

开始写文件,没有则创建,有则追加

#region 写文件
[WebMethod]
public string WriteFile(string FileName, byte[] filestrem)
{
string FileSavePath = Server.MapPath(File/) + FileName + .temp;
if (!IsExistFile(FileSavePath))
{
FileStream fs
= new FileStream(FileSavePath, FileMode.Create);
//获得字节数组
byte[] data = filestrem;
//开始写入
fs.Write(data, 0, data.Length);
//清空缓冲区、关闭流
fs.Flush();
fs.Close();
}
else
{
//追加文件
using (System.IO.FileStream f = new System.IO.FileStream(FileSavePath, System.IO.FileMode.Append, FileAccess.Write))
{
byte[] b = filestrem;
f.Write(b,
0, b.Length);
}
}

return GetFileSize(FileSavePath).ToString();
}
#endregion

上传完毕

#region
[WebMethod]
public string CopyFile(string FileName)
{
string FileSavePath = Server.MapPath(File/) + FileName+.temp;
string FileConvertPath = Server.MapPath(ConvertFile/) + FileName;

//如果目标中存在同名文件,则删除
if (IsExistFile(FileConvertPath))
{
DeleteFile(FileConvertPath);
}
//将文件复制到指定目录
File.Copy(FileSavePath, FileConvertPath);

//删除原始临时文件
DeleteFile(FileSavePath);

string Path = FileConvertPath;
return Path;
}
#endregion

存在问题
客户端要把文件读取完毕之后,才开始分段上传,如果文件过大,内存玩儿不转那么浏览器将会死掉。需要继续改进,请大家拍 砖!!源代码全部奉上,了解flex的可以看看flex部分源码,不了解的可以直接在项目中使用,已经在.NET项目中配置完毕,可直接运行看到效果。本 例子仅为beta1.0版本,还在继续修改当中。

效果图:

源码:断点续传.rar

[转载]漂亮的后台WebUi框架(有源码下载)

mikel阅读(934)

[转载]漂亮的后台WebUi框架(有源码下载) – 孟晨 – 博客园.

今天分享下以前收藏的几个漂亮的后台WebUi框架,之前听人说到“一个项目最重要的是前台,后台谁看啊”,这话说的,是,领导是只看前台界面,但系统 管理员呢?不能不说前台不重要,前台是很重要,但后台怎么能不重要呢,大部分数据要靠后台来输入,操作。如果一个好的后台界面,漂亮的,易用的页面对系统 来说也很重要,如果你是系统管理员看到一个很草的后台,也会不爽吧,今天就给大家分享几套不错的后台框架,有源码下载。


打包下载,可用的模板

[转载]Android定位功能(一)

mikel阅读(860)

[转载]Android定位功能(一) – 熊猫82 – 博客园.

废话不多说,直接开始说说与实现Android定位有关的API吧。

这些API都在Android.location包下,一共有三个接口和八个类。它们配合使用即可实现定位功能。

三个接口:

GpsStatus.Listener: 这是一个当GPS状态发生改变时,用来接收通知的接口。

GpsStatus.NmeaListener: 这是一个用来从GPS里接收Nmea-0183(为海用电子设备制定的标准格式)信息的接口。

LocationListener: 位置监听器,用于接收当位置信息发生改变时从LocationManager接收通知的接口。

八个类:

Address: 描述地址的类,比如:北京天安门

Criteria: 用于描述Location Provider标准的类,标准包括位置精度水平,电量消耗水平,是否获取海拔、方位信息,是否允许接收付费服务。

GeoCoder: 用于处理地理位置的编码。

GpsSatellite: 和GpsStatus联合使用,用于描述当前GPS卫星的状态。

GpsStatus: 和GpsStatus.Listener联合使用,用于描述当前GPS卫星的状态。

Location: 用于描述位置信息。

LocationManager: 通过此类获取和调用系统位置服务

LocationProvider: 用于描述Location Provider的抽象超类,一个LocationProvider应该能够周期性的报告当前设备的位置信息。

这里通过一个代码示例,演示一下如何实现定位。

首先,在AndroidManifest.xml清单文件里需要加入ACCESS_FINE_LOCATION权限

<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION"></uses-permission>

其次,实现代码如下:

 package com.test;
 import java.io.IOException;
 import java.util.List;

 import android.app.Activity;
 import android.location.Address;
 import android.location.Criteria;
 import android.location.Geocoder;
 import android.location.Location;
 import android.location.LocationListener;
 import android.location.LocationManager;
 import android.os.Bundle;
 import android.util.Log;
 import android.widget.Toast;

 public class MainActivity extends Activity {
     @Override
     public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.main);

        //获取到LocationManager对象
        LocationManager locationManager = (LocationManager) getSystemService(LOCATION_SERVICE);
        //创建一个Criteria对象
        Criteria criteria = new Criteria();
        //设置粗略精确度
        criteria.setAccuracy(Criteria.ACCURACY_COARSE);
        //设置是否需要返回海拔信息
        criteria.setAltitudeRequired(false);
        //设置是否需要返回方位信息
        criteria.setBearingRequired(false);
        //设置是否允许付费服务
        criteria.setCostAllowed(true);
        //设置电量消耗等级
        criteria.setPowerRequirement(Criteria.POWER_HIGH);
        //设置是否需要返回速度信息
        criteria.setSpeedRequired(false);

        //根据设置的Criteria对象,获取最符合此标准的provider对象
        String currentProvider = locationManager.getBestProvider(criteria, true);
        Log.d("Location", "currentProvider: " + currentProvider);
        //根据当前provider对象获取最后一次位置信息
        Location currentLocation = locationManager.getLastKnownLocation(currentProvider);
        //如果位置信息为null,则请求更新位置信息
        if(currentLocation == null){
            locationManager.requestLocationUpdates(currentProvider, 0, 0, locationListener);
        }
        //直到获得最后一次位置信息为止,如果未获得最后一次位置信息,则显示默认经纬度
        //每隔10秒获取一次位置信息
        while(true){
            currentLocation = locationManager.getLastKnownLocation(currentProvider);
            if(currentLocation != null){
                Log.d("Location", "Latitude: " + currentLocation.getLatitude());
                Log.d("Location", "location: " + currentLocation.getLongitude());
                break;
            }else{
                Log.d("Location", "Latitude: " + 0);
                Log.d("Location", "location: " + 0);
            }
            try {
                Thread.sleep(10000);
            } catch (InterruptedException e) {
                 Log.e("Location", e.getMessage());
            }
        }

        //解析地址并显示
        Geocoder geoCoder = new Geocoder(this);
        try {
            int latitude = (int) currentLocation.getLatitude();
            int longitude = (int) currentLocation.getLongitude();
            List

<address> list = geoCoder.getFromLocation(latitude, longitude, 2);
            for(int i=0; i  75                Address address = list.get(i);
                Toast.makeText(MainActivity.this, address.getCountryName() + address.getAdminArea() + address.getFeatureName(), Toast.LENGTH_LONG).show();
            }
        } catch (IOException e) {
            Toast.makeText(MainActivity.this,e.getMessage(), Toast.LENGTH_LONG).show();
        }

     }

     //创建位置监听器
     private LocationListener locationListener = new LocationListener(){
         //位置发生改变时调用
         @Override
         public void onLocationChanged(Location location) {
             Log.d("Location", "onLocationChanged");
             Log.d("Location", "onLocationChanged Latitude" + location.getLatitude());
                  Log.d("Location", "onLocationChanged location" + location.getLongitude());
         }

         //provider失效时调用
         @Override
         public void onProviderDisabled(String provider) {
             Log.d("Location", "onProviderDisabled");
         }

         //provider启用时调用
         @Override
         public void onProviderEnabled(String provider) {
             Log.d("Location", "onProviderEnabled");
        }

         //状态改变时调用
         @Override
         public void onStatusChanged(String provider, int status, Bundle extras) {
             Log.d("Location", "onStatusChanged");
         }
     };
 }

由于代码里的Criteria对象对位置精度要求并不高,所以一般会返回“network”作为provider,而基于network的定位 往往会存在一定的位置偏差,这对于需要精确定位的应用程序来说,显然不合要求。这时,需要则需要用到基于GPS的定位方法了。具体详情,请看后续博文 Android定位功能(二)。

[转载]FlexPaper+SwfTools实现的在线文档功能

mikel阅读(1166)

[转载]FlexPaper+SwfTools实现的在线文档功能 – penbox – 博客园.

最近一个项目需要实现一个在线浏览文档的功能。准备使用FlexPaper配合Pdf2Swf实现。

主要需求在于:

➔ 文档页数很多,少则几百页,多则上千页。
➔ 相应的文档大小也在50MB以上。

根据需求,将该功能拆分成了三部分:

上传:支持大文件,断点续传。
文件转换服务:在后台进行Pdf转Swf。
在线浏览:分页加载。

其中文件上传部分是我同事实现的,这里主要讲下后台服务和在线浏览部分。

文件转换服务

大体思路是:
后台服务定时扫描文件夹PdfPath,如果发现Pdf文件,则调用Pdf2Swf.exe,将PDF逐页转换成Swf文件,存入SwfPath中的与文件名称对应的文件夹里。

这里使用了SwfTools套件中的Pdf2Swf工具:下载

主要代码:

//PDF转换成SWF
private void ConvertPDFtoSWF(string pdfFile)
{
using (Process p = new Process())
{
SystemLog.CurrentLogger.Debug(string.Format("正在处理 {0} ...", pdfFile));

string pdf2swfExe = "pdf2swf.exe";
string savePath = GetSavePathFromName(pdfFile);

string cmd = pdf2swfExe;
string args = "  -t \"" + pdfFile + "\"  -o \"" + savePath + pdfFile.Split('\\').Last().Replace(".pdf", "")
+ "%.swf\" -s drawonlyshapes -s flashversion=9";

p.StartInfo.FileName = cmd;
p.StartInfo.Arguments = args;
p.StartInfo.UseShellExecute = false;

//此类提供的标准output流只有2k,不要重定向
p.StartInfo.RedirectStandardOutput = false;

p.StartInfo.CreateNoWindow = true;
p.Start();
p.PriorityClass = ProcessPriorityClass.High;
p.WaitForExit();

SystemLog.CurrentLogger.Debug(string.Format("{0} 处理完成。", pdfFile));

if (AppConfiguration.DeleteConvertedPdf)
{
//转换完成后删除Pdf
File.Delete(pdfFile);
SystemLog.CurrentLogger.Debug(string.Format("{0} 已删除。", pdfFile));
}
else
{
//重命名Pdf
File.Move(pdfFile, pdfFile + ".bak");
SystemLog.CurrentLogger.Debug(string.Format("{0} 已重命名。", pdfFile));
}
}
}
gentleface.com free icon set 写服务的时候遇到一个奇怪的Bug,发布为服务的时候,测试用的Pdf文件每次转换到37页的时候就卡住了,但是用命令行进行调试的时候却一切OK。
Google后发现,原来是Process的output流问题:output流只有2KB,而Pdf2Swf会产生大量输出,2KB很快就耗尽了。

之前在测试的时候,发现转换过程中会出现文字丢失的现象。这里使用了-s drawonlyshapes 这个参数,将Pdf全部作为图片转换的。这样虽然保证了兼容性,但是牺牲了空间。作为图片生成的Swf比文本格式的Swf要大不少,不知道大家有没有什么 好的解决方法。

在线浏览

FlexPaper支持分页加载,采用{filename[*,padding],total pages}这种语法即可。

主要代码:

<a id="viewerPlaceHolder" style="width: 800px; height: 600px; display: block;"></a>

<script type="text/javascript">// <!&#91;CDATA&#91;
    var fp = new FlexPaperViewer(
    'FlexPaperViewer',
     'viewerPlaceHolder', { config: {
         SwfFile: 'SwfFolder/<%=Folder %>/{<%=Folder %>&#91;*,0&#93;.swf,<%=PageNum %>}',
         localeChain: "zh_CN", //中文
         Scale: 1,
         ZoomTransition: 'easeOut',
         ZoomTime: 0.5,
         ZoomInterval: 0.2,
         FitPageOnLoad: false,
         FitWidthOnLoad: false,
         PrintEnabled: true,
         FullScreenAsMaxWindow: false,
         ProgressiveLoading: false,
         MinZoomSize: 0.2,
         MaxZoomSize: 5,
         SearchMatchAll: false,
         InitViewMode: 'Portrait',

         ViewModeToolsVisible: true,
         ZoomToolsVisible: true,
         NavToolsVisible: true,
         CursorToolsVisible: true,
         SearchToolsVisible: true
     }
     });
// &#93;&#93;></script>
warning FlexPaper无法载入中文文件名。如果右上角的圈圈一直在转,注意是不是文件名称的问题。

参考文章

c# System.Diagnostics.Process 调用外部程序时WaitForExit锁死问题分析及解决方案

FlexPaper+SWFTools 实现仿百度文库及一些小问题

Pdf2Swf命令行参数

解决FlexPaper分页分段加载问题

源码

分页加载:FlexPaper.zip

文件转换服务:PDFtoSWFService.zip

[转载]ASP.NET MVC3实战系列(三):MVC3中使用依赖注入(IOC)

mikel阅读(818)

[转载]ASP.NET MVC3实战系列(三):MVC3中使用依赖注入(IOC) – 王德水 – 博客园.

ASP.NET MVC3实战系列(二):面向接口编程,提高系统可测试性。 中我们说之前的程序虽然可以单元测试。

第一就是我们暴露了HomeController的repository的属性。

第二我们在类内部new了一个对象,假如我们这次是从数据库中得到Lovers,下次想从文件或者Web Service里去数据时,我们必须修改Controller里的代码。

在这一节里我们引入一个依赖注入的工具Ninject.

image

点击安装

image

修改HomeController为如下代码

image

这样我们就不需要暴露属性了,修改对应的单元测试为如下图所示。测试顺利通过

image

但是,当我们运行程序时出错了,因为MVC在实例化Controller时出错,因为没有无参构造函数参数,而我们确要实例化带参的构造函数。

image

我们需要自己实例化Controller,所以我们需要建立一个ControllerFactory并让应用程序使用我们自己的这个ControllerFactory

image

然后在Global.asax.cs里指定使用这个ControllerFactory

image

按F5运行程序又运行正常了

image

至此,这个应用程序已经易于测试了,而且面向了接口编程。易于扩展。

最后,打个小广告:西安分公司急聘如下人员,有意者直接联系我,左边侧栏有我的联系方式,博客园招聘频道有详细描述 http://job.cnblogs.com/offer/13800/

基本要求:
4年以上C#开发经验, .Net基础扎实,熟练使用.Net3.5新特性。
精通ASP.NET Web开发。
熟练使用WCF.
熟练使用ORM,LINQ TO SQL /Entity Framework或者NHibernate.
熟练使用JavaScript, JQuery.
熟悉Web标准,熟悉HTML&CSS.
熟悉SQL Server, 熟练掌握T-SQL,存储过程。
英语读写熟练,可以看懂英文需求,可以和客户流畅的用英语文字沟通(MSN/Skype)
良好的编码习惯。

熟悉下面任意一项优先:
熟悉敏捷开发者优先
英语听说熟练者优先
熟悉ASP.NET MVC者优先。
熟悉WPF者优先
熟悉Silverlight优先
有系统架构经验者优先,有单元测试或者TDD经验者优先。

作者: 王德水
出处:http://www.cnblogs.com/cnblogsfans
版权:本文版权归作者和博客园共有,转载需经作者同意。

[转载]ASP.NET MVC3实战系列(二):面向接口编程,提高系统可测试性。

mikel阅读(895)

[转载]ASP.NET MVC3实战系列(二):面向接口编程,提高系统可测试性。 – 王德水 – 博客园.

ASP.NET MVC 使用MVC的架构,其架构本身就使应用程序更易于测试,但这并不意味着可以随便写出易于测试的程序。我们都知道单元测试在系统开发有着很重要的作用。

我们来写这样的一个程序,系统获取某个坏男人的情人信息,然后发送给他老婆。

1. 建一个Lover的ASP.NET MVC3项目

image

我们需要1个实体类,存储男人,情人和老婆的信息。

image

然后我们需要一个LoverRepository来获取某个人的情人,这里就想成从数据库取数据。我们这里先返回固定的数据

image

建一个HomeController, 代码如下

image

建立一个Index视图

image

建立一个Send 视图

image

F5运行

image

image

2. 重构这个小程序。

我们可以看到如果我们想测试HomeController里Send的这个Action,如果LoverRepository没有开发完或者出错,我们将无法测试

image

image

可以看到单元测试出错了。

image

那么我们如何来隔离LoveRepository呢?我们都知道面向接口编程可以提高系统的可测试性。

打开LoveRepository.cs, 右键重构为接口:

image

image

image

image

为了容易测试,我们把这个接口属性设为公有。

image

这时我们再自己可以实现一个FakeRepository继承ILoverRepository这个接口,这样就测试通过了。

image

但是这样有几个问题:

第一就是我们暴露了HomeController的repository的属性。

第二我们在类内部new了一个对象,假如我们这次是从数据库中得到Lovers,下次想从文件或者Web Service里去数据时,我们必须修改Controller里的代码。

第三我们需要自己写一个Fake类。

要想解决这些问题,下一节我们将讨论ASP.NET MVC3里如何使用IOC来解决对象的依赖问题。

最后,打个小广告:西安分公司急聘如下人员,有意者直接联系我,左边侧栏有我的联系方式,博客园招聘频道有详细描述 http://job.cnblogs.com/offer/13800/

基本要求:
4年以上C#开发经验, .Net基础扎实,熟练使用.Net3.5新特性。
精通ASP.NET Web开发。
熟练使用WCF.
熟练使用ORM,LINQ TO SQL /Entity Framework或者NHibernate.
熟练使用JavaScript, JQuery.
熟悉Web标准,熟悉HTML&CSS.
熟悉SQL Server, 熟练掌握T-SQL,存储过程。
英语读写熟练,可以看懂英文需求,可以和客户流畅的用英语文字沟通(MSN/Skype)
良好的编码习惯。

熟悉下面任意一项优先:
熟悉敏捷开发者优先
英语听说熟练者优先
熟悉ASP.NET MVC者优先。
熟悉WPF者优先
熟悉Silverlight优先
有系统架构经验者优先,有单元测试或者TDD经验者优先。

作者: 王德水
出处:http://www.cnblogs.com/cnblogsfans
版权:本文版权归作者和博客园共有,转载需经作者同意。

[转载]34个漂亮的应用程序后台管理系统界面(系列二)

mikel阅读(880)

[转载]34个漂亮的应用程序后台管理系统界面(系列二) – 梦想天空(山边小溪) – 博客园.

今天这篇文章收集了34个漂亮的应用程序后台管理界面分享给大家。这些界面都是来自themeforest网站,虽然直接下载需要付费的,不过 大部分都提供了在线预览,所以完全能够复制下来,有的提供了预览图,设计师可以根据预览图自己设计。希望这些漂亮的后台管理界面设计案例能帮助到你。(有 登录界面的,点击登录即可进入后台界面)

Flexy Admin


34个漂亮的应用程序后台管理系统界面

Broom Cupboard Admin Skin


34个漂亮的应用程序后台管理系统界面

Fresh CMS


34个漂亮的应用程序后台管理系统界面

Ultimate Admin Panel Solution


34个漂亮的应用程序后台管理系统界面

Titanium


34个漂亮的应用程序后台管理系统界面

Wide Admin


34个漂亮的应用程序后台管理系统界面

AP Admin Panel


34个漂亮的应用程序后台管理系统界面

Ninja Admin


34个漂亮的应用程序后台管理系统界面

Sleek Admin Skin


34个漂亮的应用程序后台管理系统界面

Pro Admin Template


34个漂亮的应用程序后台管理系统界面

您可能还喜欢

[原创]DEDECMS仿站笔记1:自定义联动类别调用

mikel阅读(1140)

1. PrintAutoFieldsAdd 载入自定义表单(用于发布):
文件:inc/inc_archives_function.php
行号:472
函数声明:function PrintAutoFieldsAdd(&$fieldset,$loadtype=’all’)
参数:
$fieldset:自定义模型的字段集合
$loadtype:价值类型有两种:all ,autofield;autofield
功能说明:
发布自定义模型界面调用时,拼装自定义模型的表单。

2. DedeTagParse Dede织梦模板类
文件:include\dedetag.class.php
行号:70
类声明:class DedeTagParse
方法:
 LoadSource($str):载入模板字符串,调用data/tplcache/MD5($str).inc的模板文件,然后调用LoadTemplate()方法解析模板
 LoadTemplate($filename):载入模板文件 然后调用ParseTemplet()解析模板
 ParseTemplet():解析模板
3. 模板列表调用内容模型格式数据写法:
{dede:arclist channelid=’18’ addfields=’xuebu,course_author’}

  • [field:course_author/]aaa
  • {/dede:arclist}
    说明:
    Channelid:内容模型管理中的ID号
    AddFields:定义的字段名,需要在内容模型管理中的“列表附加字段”中添加

    4. 自定义联动类别信息的调用方法:
    1)函数编写
    打开 /include/extend.func.php 文件(注:本文件为自定义函数接口文件,若不存在,请自行创建),在最后一行的?>上添加如下代码:
    function Getsysenum($fields,$egroup){
    global $dSQL;
    $row = $dsql->GetOne(“select * from dede_sys_enum where evalue = ‘”.$fields.”‘and egroup=’”.$egroup. “’”);
    if(!is_array($row)){
    return “联动类别不存在“;
    }
    else{
    return $row[‘ename’];
    }
    }
    保存关闭即可!
    2)函数调用
    这个函数的调用非常简单,只需要将“枚举值”传递过来即可,这个缓存组名通常也就是内容模型中的自定义联动菜单字段名:
    function Getsysenum(‘枚举值’,’联动组名’);
    3)调用实例
    我们来看一下调用的实例代码:
    {dede:arclist titlelen=’60’ row=’8′ channelid=’1′ addfields=’nativeplace’}

  • [field:nativeplace function=”Getsysenum(@me,’ nativeplace’)”/]
    [field:title /]
  • {/dede:arclist}
    其中[field:nativeplace function=”Getsysenum(@me,’ nativeplace’)”/] 就是将nativeplace字段进行Getsysenum函数处理。

    5. 联动类别信息存储表dede_sys_enum:
    字段 类型 长度 是否主键 描述
    ID Smallint 6 是
    Ename Char 30 分类名称
    Evalue Smallint 6 分类ID
    Egroup Char 20 分组名称
    Disorder Smallint 5 排列顺序
    Issign Tinyint 1 是否系统1=系统 0=非系统

    6. 扩展函数编写文件/include/extend.func.php:
    通过上面的扩展查询分类的函数可见可以自定义扩展函数在/include/extend.func.php文件中,用于标签中进行调用,
    于是编写了扩展函数查询给定分类组合的,一级分类信息的扩展数:
    Function GetSysEnum($egroup,$level)
    global $dSQL;
    $row = $dsql->GetOne(“select ename from dede_sys_enum where evalue like ‘%”.$level.”‘and egroup=’”.$egroup. “’”);
    if(!is_array($row)){
    return “联动类别不存在“;
    }
    else{
    return $row[‘ename’];

    调用方式:
    Function=’GetSysEnum(‘xuebu’,’0’)’

    [转载]InfoQ: 如何查看我的订单-REST的流程API设计案例

    mikel阅读(1002)

    [转载]InfoQ: 如何查看我的订单-REST的流程API设计案例.

    这是一个关于订单的故事。四个月前,我在某刚刚上市不久的网上书城框框网购买了一包纸尿裤,因为尺寸不对,我选择了退货,由此开始了我糟糕的用户体 验:首先是快递公司取回了纸尿裤却没有还款给我,接下来,在两个月的时间里,我不得一次又一次的向框框的客服投诉,客服很客气,她让我说出我的订单号然后 说需要帮我查一查,两分钟后,她说需要和快递公司联系,稍后再打给我;随后的客服都很客气,但无一例外的,她们都不清楚我订单的处理情况,她们甚至很惊 讶,快递公司还没有给你办吗?终于,我失去了耐心,我说,难道你们框框就只负责卖东西吗?送货外包了你们就不需要负责了吗?究竟是谁在处理我的订单,谁是 责任人你们不知道吗?整个流程的处理状态你们清楚吗?客服支支吾吾半天,说,我们这就和快递公司联系。

    框框的问题出在什么地方?顾客找不到这个订单的直接负责人,只能和客服打电话,而框框自己也失去该订单的状态了,于是只能再打快递公司的客服,这样 问题透过组织结构层层传递下来,执行力可想而知。那为什么顾客找不到订单的直接负责人呢?因为顾客失去对自己订单的可视化了,我不知道我的订单现在处于一 种什么样的状态,正处于哪一个步骤,谁是这个步骤的责任人,网站上没有,自然无从得知。想想我们的迭代和show case,也正是一种对顾客的可视化手段。

    在下面的章节中,我们将一起来应用rest的架构风格逐步搭建一个端到端的流程管理系统,看看如何解决这个问题,这个问题就是:看在上帝的份上,让 我看看我的订单。为什么使用rest?因为在下面的故事中,我们将会有大量的系统集成工作。为什么集成要用rest?因为REST的实质是充分利用 HTTP协议,将其作为一种应用协议,而不仅仅是传输协议,这样在WEB上做集成时能够最大程度上的达成一致,形成大家都能接受的约定,减少集成的工作 量。另外,不可忽视的一点是,我们更多的是做数据的mashup,这很适合rest的应用特点。不能使用web service吗?当然可以使用web service,只是我们这里使用rest而已。

    第一个需求,我想随时随地查看我的订单

    好吧,很自然,这需要我们的程序支持移动设备。

    图 1  昨天我们这样开发程序

    似乎就在昨天,我们还在如图1一般开发程序,我们一边打开firebug进行调试,一边诅咒IE的不得好死,那时我们的关注点集中在前端,集中在如何使各个浏览器的行为和样式保持一致。而服务器端则是经典的MVC框架,直接将渲染好的HTML文档扔回客户端。

    图 2  今天我们这样开发程序

    然而到了今天,一切都发生了变化,我们开发的程序成了图2的样子,随着IE向标准的靠拢,HTML5似乎有一统客户端之势,然而,移动互联网的兴起 让我们编写程序重新变得复杂,昨天我只需要支持浏览器,现在则需要支持各种手机平台上的原生应用。自然,和昨天存在大量的JavaScript和css框 架来抹平不同浏览器之间的差别一样,现在我们也有了PhoneGapTitanium来抹平不同平台之间的差别,尽管目前这些跨平台工具还存在用户体验不理想的问题,但最重要的变化来自两个方面:一是客户端重新变胖;二是服务器端由返回渲染完成的页面退化为返回数据,具体表现就是对客户端暴露出API。

    对移动设备的支持使得我可以在写代码时、吃饭时、睡觉时甚至坐马桶时随时查看我的订单(当然前提是你得有这些设备),而本文后续的架构变化也会围绕 着图2逐渐演进。至于明天HTML5是否会最终代替原生应用,我觉得不会,不仅仅是技术原因(对设备硬件的使用)更重要的是商业原因,替代后的苹果变得和 现在的微软/诺基亚一样尴尬,那么,也许后天?

    好吧,既然是REST的API设计,我们来看看REST的架构风格。RESTful 架构遵从以下几个原则:

    • 请求是客户-服务器 式的,并很自然地使用一种基于拉的交互风格。
    • 请求是无状态的。每个从客户端到服务器端的请求都必须包含理解此请求所需的全部信息,而且不能利用服务器上所存储的上下文。
    • 客户端和服务器都遵从统一的接口。所有的资源都可通过 Web 的普通接口进行访问 —— HTTP 及 HTTP 方法:GET、POST、PUT 和 DELETE。
    • 客户端通过URI与命名的资源进行交互。
    • 将http 状态码作为系统的状态码。

    REST的实质是充分利用HTTP协议,形成大家都能接受的约定。

    看例子,我们以订单列表作为整个应用的调用入口,我们首先会GET:http://api.kuangkuang.com/orders,服务器返回以下的数据:

    1000
    draft
    
    1001
    completed
    
    

    在返回的数据中,我们看到了:

    <link rel="detail" media-type="application/xml" url="http://api.kuangkuang.com/order/1001"/>
    这个链接引导我们查看具体的订单信息,我们GET:http://api.kuangkuang.com/order/1000,服务器返回以下的数据:
    1000
    draft
    88.0
    
    

    这里我们看到了两个链接:

    <link rel="edit" media-type="application/xml" url="http://api.kuangkuang.com/order/1000"/>
    <link rel="delete" media-type="application/xml" url="http://api.kuangkuang.com/order/1000"/>

    它们告诉我们可以对这个处于草拟状态的订单进行修改和删除。我们GET另外一个已完成的订单看看:http://api.kuangkuang.com/order/1001,返回数据:

    <order>
        <link rel="detail" media-type="application/xml" url="http://api.kuangkuang.com/order/1001"/>
        <content>
            <id>1001</id>
            <state>completed</state>
            <cost>66.0</cost>
        </content>
    </order>

    没有更多的链接,这意味着我们只能对该订单进行查看。

    在这些交互中,最重要的是服务器端返回数据本身已包含了对其他资源访问和对现在资源操作的线索。这样的好处在于客户端只需要一个入口地址,其他所有 的操作地址全部由服务器端确定,这使得客户端与服务器端解耦,客户端不必再硬编码入URI,能够各自独立的进化,服务器端负责数据、权限以及交互URI的 确定,客户端重新回归展现数据的单一职责。

    第二个需求,实现一个简单的流程

    在上面的例子里,我们看到了订单的CRUD操作,但这并不是实际生活中的真实情况,整个订单的生命周期如下图所示:

    图 3  订单的完整流程

    在实现这个流程时,我们分为两步:第一步对订单进行资源建模;第二步通过工作流对订单进行流程的生命周期管理。

    图 4  订单的资源模型

    图 5  使用工作流管理订单的生命周期

    工作流的职责在于管理订单的生命周期,在其生命周期的不同阶段,我们会有不同的参与者,对订单不同的操作权限。我们的系统架构演变成下面的样子:

    图 6  使用工作流管理资源模型的生命周期

    看例子,这次将我们视角转移到框框网这边,看看框框如何处理我们已提交的订单。我们使用GET:http://api.kuangkuang.com/orders?status=waiting-review来获取所有需要审核的订单,服务器返回以下的数据:

    <orders>
        <link rel="list" media-type="application/xml" url="http://api.kuangkuang.com/orders"/>
        <order>
            <id>1000</id>
            <state>waiting review</state>
            <link rel="detail" media-type="application/xml" url="http://api.kuangkuang.com/order/1000"/>
        </order>
    </orders>

    我们查看具体的订单信息,我们GET:http://api.kuangkuang.com/order/1000,服务器判断出我们是框框网员工,返回以下的数据:

    <order>
        <link rel="detail" media-type="application/xml" url="http://api.kuangkuang.com/order/1000"/>
        <content>
            <id>1000</id>
            <cost>88.0</cost>
            <state>waiting review</state>
            <squence>
                <activity rel="review" media-type="application/xml" url="http://api.kuangkuang.com/review/order/1000"/>
            </squence>
        </content>
    </order>

    注意到这两行:

    <state>waiting review</state>
    <squence>
        <activity rel="review" media-type="application/xml" url="http://api.kuangkuang.com/review/order/1000"/>
    </squence>

    这段信息是由工作流加入的,它告诉我们当前订单的状态为等待审核以及下一步需要我们来审核。那么,我们PUT http://api.kuangkuang.com/review/order/1000告诉服务器我们审核通过,服务器返回数据:

    <order>
        <link rel="detail" media-type="application/xml" url="http://api.kuangkuang.com/order/1000"/>
        <content>
            <id>1000</id>
            <cost>88.0</cost>
            <state>waiting send</state>
            <squence>
            <activity rel="send" media-type="application/xml" url="http://api.kuangkuang.com/sent/order/1000"/>
            </squence>
        </content>
    </order>

    同样,工作流加入了这两行数据:

    <state>waiting send</state>
    <squence>
        <activity rel="send" media-type="application/xml" url="http://api.kuangkuang.com/sent/order/1000"/>
    </squence>

    告诉我们当前订单状态为等待送货,而下一步需要我们来完成这一步。此时,如果顾客来查看自己的订单会得到什么数据呢?服务器会判断出当前请求的用户是顾客,那么:

    <order>
        <link rel="detail" media-type="application/xml" url="http://api.kuangkuang.com/order/1000"/>
        <content>
            <id>1000</id>
            <cost>88.0</cost>
            <state>waiting send</state>
        </content>
    </order>

    顾客能够看到自己的订单正处于等待送货状态,而不会有下一步的动作。恩,很好,框框网订单处理速度很快,而这正是网购的主要竞争力之一。

    等等,如果订单审核不通过呢,继续看流程:

    图 7  增加一个网关

    作为框框网的员工,我们GET:http://api.kuangkuang.com/order/1000,服务器判断出我们是框框网员工,返回以下的数据:

    <order>
        <link rel="detail" media-type="application/xml" url="http://api.kuangkuang.com/order/1000"/>
        <content>
            <id>1000</id>
            <cost>88.0</cost>
            <state>waiting review</state>
            <squence>
                <xor rel="review" media-type="application/xml" url="http://api.kuangkuang.com/review/order/1000">
                    <choice>pass</choice>
                    <choice>reject</choice>
                </xor>
            </squence>
        </content>
    </order>

    注意到这两行:

    <squence>
        <xor rel="review" media-type="application/xml" url="http://api.kuangkuang.com/review/order/1000">
            <choice>pass</choice>
            <choice>reject</choice>
        </xor>
     </squence>

    系统告诉我们这是一个排他网关,我们需要作出选择,在客户端与服务器端就media-type达成一致的情况下(即客户端能够充分理解服务器端返回 的数据格式,这个数据格式被标准化),我们的代码不需要作出任何的修改,pass和reject作为工作流变量被put到服务器,由工作流引擎进行处理, 同样不会影响到订单的资源建模。

    在这些交互中,最重要的是我们通过工作流实现了REST社区所呼吁的“将超媒体作为应用状态的引擎(hypermedia as the engine of application state)”。更简单地说,URI代表了状态机里的状态迁移。我们通过<squence>、<activity> 和<xor>标签让客户端是通过跟随链接的方式来操作订单状态机的状态转移。

    第三个需求,框框将物流部分外包

    在我们实际的生活中,电商们并不自己送货,他们将这部分工作外包给了物流公司。从成本的角度考虑,外包送货是最合适的选择。实际上,整个订单从提交到最后的完成情况还要稍微复杂一些,如下图所示:

    图 8  订单从提交到完成的整个流程

    从图中我们可以看出,这个流程跨越了两家公司,同时也涉及到了三个系统的集成,这三个系统分别是框框网的前台网站、框框网的后台负责仓储、进出货和 物流的ERP系统以及外包物流公司的ERP系统。三个系统各自有自己的处理流程,整个订单的端到端处理流程由这三个系统的三个流程所共同完成:当我们在框 框网提交订单时,一个消息被发送到框框的后台ERP系统,这个消息触发一个货物的出库流程,当货物打包完毕出库时,一个消息被发送到物流公司的ERP系 统,同时触发物流公司的包裹配送流程,当我们给物流公司的配送员付款完毕时,对我们顾客来说框框的购物流程已经结束,然而整个流程依旧还要继续,配送员回 到公司完款,一个消息被发送回框框的后台ERP,物流公司的包裹配送流程结束,框框网的这个订单这才处理完成。

    在本文的一开始,我们提到了那个糟糕的退货故事,问题就在于当订单交由物流公司进行货物配送时,我们包括框框失去了对配送流程的可视化,物流公司的 处理情况在我们的流程中黑盒了。如何解决这部分的问题呢,有两种处理方法:一是在框框网订单处理流程中加入捕获事件,正如图8中所示的,当框框后台ERP 和物流公司ERP对订单进行处理时,每到一个任务节点就给框框网的订单处理流程发送消息,由此给我们标示出订单的实时状态。

    图 9  通过系统集成传递订单实时处理消息

    现在,让我们来看看自己的订单会得到什么数据呢,GET http://api.kuangkuang.com/order/1000框框网前台网站返回数据:

    <order>
        <link rel="detail" media-type="application/xml" url="http://api.kuangkuang.com/order/1000"/>
        <content>
            <id>1000</id>
            <cost>88.0</cost>
            <state>waiting send</state>
            <history>
                <activity rel="submit" time="2011-6-28 14:00" participant="ronghao"/>
                <activity rel="review" time="2011-6-28 14:30" participant="xinpeng"/>
                <activity rel="delivery package" time="2011-6-28 15:00" participant="haorong"/>
                <activity rel="warehouse" time="2011-6-28 17:00" participant="pengxin"/>
            </history>
        </content>
    </order>

    订单状态为等待物流公司送货,注意到这段数据:

    <history>
        <activity rel="submit" time="2011-6-28 14:00" participant="ronghao"/>
        <activity rel="review" time="2011-6-28 14:30" participant="xinpeng"/>
        <activity rel="delivery package" time="2011-6-28 15:00" participant="haorong"/>
        <activity rel="warehouse" time="2011-6-28 17:00" participant="pengxin"/>
    </history>

    工作流加入了订单处理的历史信息,从这段信息可以看出,我们要明天上午才能收到自己的货物了。

    很不错不是吗,但是现实情况又是怎样呢。我们先来看看当当,当当如是说:订单状态变为“已发货”后,您可以登录“我的订单”,点击订单号进入订单详情页查看快递公司的联系方式,用订单号查询即可。我们再来看看卓越,卓越如是说:宅急送配送的订单:登录宅急送网站或拨打020-82252310-802查询;港中能达配送的订单:登录港中能达网站或拨打020-86443920查询。也就是说物流公司的配送流程状态并没有集成到网站中来,如下图所示:

    图 10  网站与物流公司系统没有集成

    为什么没有集成呢?第一是物流公司的客户往往不止框框一家,第二是框框也不会把鸡蛋放在一家物流公司的篮子里,这些给系统集成带来了难度,我们会突 然发现有大量的系统需要集成,而系统间的集成之间存在太多的集成点,调试以及约定,这些都需要大量的工作和成本,系统们被紧紧的耦合在一起。

    既然第一种实现方式使得我们即时查看我们订单状态成本太大,那我们看看第二种方法:使用一个统一的流程管理系统来管理整个端到端的流程。

    图 11  使用统一的业务流程管理系统管理端到端流程

    业务流程管理系统的职责有两个:一是由其管理起各个系统间的集成工作,这样避免了各个系统间的大量耦合;二是由其跟踪订单状态,完成订单在整个流程中的可视化。

    图 12  框框订单的端到端流程

    我们来看看具体的api调用,当我们在框框网站提交一个ID为1000的订单时,框框网站会发送一个消息到http://api.kuangkuang-bpm.com/process-definition/1,由此触发整个的流程,启动一个新的流程实例。发送的消息:

    <order>
        <link rel="detail" media-type="application/xml" url="http://api.kuangkuang.com/order/1000"/>
        <content>
            <id>1000</id>
            <cost>88.0</cost>
        </content>
    </order>

    业务流程管理系统给我们返回的消息:

    1
    
    1000
    88.0
    
    订单提交
    
    

    返回的消息中给我们指出了该订单所关联的流程实例ID,当前正在执行的任务。流程系统创建流程实例后接下来继续往下执行,它发送一个消息到框框的后 台ERP系统,触发后台ERP系统对订单的处理,同时告诉其访问当前流程实例的URI。现在我们假设流程执行到物流公司的配送任务,我们在框框网站查看订 单的即时状态系统会有哪些动作。第一步我们同样是GET:http://api.kuangkuang.com/order/1000,返回的数据:

    <order>
        <link rel="detail" media-type="application/xml" url="http://api.kuangkuang.com/order/1000"/>
        <link rel="process-instance" media-type="application/xml" url="http://api.kuangkuang-bpm.com/process-instance/1"/>
        <content>
            <id>1000</id>
            <cost>88.0</cost>
        </content>
    </order>

    返回的消息中多了一个访问流程实例的URI,那么我们的客户端程序继续GET:http://api.kuangkuang-bpm.com/process-instance/1,返回的数据:

    1
    
    1000
    88.0
    
    
    
    物流配送
    sub-process
    
    
    
    
    
    

    我们看到当前正在执行的任务是物流配送,这是一个子流程任务,想具体了解这个子流程执行的情况,我们的客户端程序继续GET:http://api.zjs-erp.com/order/2000,啊哈,框框将配送任务外包给了宅急送啊。

    <order>
        <link rel="detail" media-type="application/xml" url="http://api.zjs-erp.com/order/2000"/>
        <link rel="process-instance" media-type="application/xml" url="http://api.kuangkuang-bpm.com/process-instance/1"/>
        <content>
            <id>2000</id>
            <cost>88.0</cost>
            <current-activity>
                <name>配送</name>
            </current-activity>
            <history>
                <activity name="接受包裹配送单" time="2011-6-29 15:40" participant="ronghao"/>
                <activity name="包裹入库" time="2011-6-29 15:45" participant="xinpeng"/>
            </history>
        </content>
    </order>

    好了,有了这三段数据,我们就可以清楚的看到订单所经过的各个环节以及当前的状态。从中可以看到两点:一是我们通过kuangkuang- bpm.com所提供的流程服务将各个系统进行了数据和流程的集成;二是各个被集成的系统需要实现rest的api以供我们的客户端程序进行数据的 mashup。

    故事完了吗?还没有,京东618活动简报:收获订单40多万份,订购金额超2亿,已经发货一个多亿,尚有十几万份订单积压,大约三日左右可以处理完 毕。不足之处:流量多次超过4个G,服务器运行缓慢;图书备货量严重不足。与服务器相比,我更加关心如何及时将这几十万份订单处理完毕以及库存如何应对促 销而产生的水平震荡(图书备货量严重不足,无法预测哪些书籍畅销哪些滞销,由此带来的订单迟迟无货),这显然不是一家物流公司可以完成的,需要多家物流公 司一起消化这些订单,那么,问题来了,当我们mashup数据时,如何对这些物流公司返回的不同的数据格式进行处理?为每家公司实现一个适配器?NO!作 为行业的老大,作为一家一流的企业,我们得制定标准。这时就需要标准化media type了,建立行业标准,企业级rest等于自定义、创造和标准化media type。

    第二个问题,kuangkuang-bpm.com算是云服务否?是的,目前算作框框私有云,对自己和业务伙伴提供流程服务。

    最后一个需求,框框要开放平台

    最后一个需求有些跑题,但是那帮人现在都在搞什么开放平台,框框自然也是要跟风一下的。那么,开放平台都开放了个啥?

    图 13  框框的开放平台

    第一是用户的开放,这个通过网站实现,我们增加上百货、品牌频道,吸引商家入驻,这些入驻的商家能够分享我们的网站用户,同时,品牌的用户也能够被 吸引到我们的网站上。同时,开心、人人、新浪微博、腾讯微博的账号能够直接登录我们网站,同时,我们网站的产品、评价也能分享到这些SNS网站里。

    第二是服务的开放,这个通过流程实现,我们能给商家提供仓储、物流、投诉等一系列的服务。不使用我们的网站,自己有网站?没关系,只要数据格式满足我们定义的media type就行。

    图 14  流程服务的开放

    这算完了吗?不算!小马哥说,要开放就全面开放。我们将仓储环节和物流配送环节也开放出来,允许任何服务提供商使用我们的流程服务。

    图 15  物流环节的开放

    只要系统提供数据格式满足我们定义的media type就可以加入到我们的流程中来,分享我们的客户,为上帝提供服务。我们则从中提成。

    所以,真正的开放是整个流程服务各个环节的开放,不仅仅对流程消费者,也对流程服务提供商,我们在其中起一个协调的作用。由此,我们将 kuangkuang-bpm.com服务开放出来变成一个公有云服务,后来,我们惊讶的发现,kuangkuang-bpm.com比 kuangkuang.com更赚钱,因为它建立起了一个生态链。

    小结

    在上面的故事里,我们一步步看到了框框的系统架构演进,不难看出,架构演进的过程实际上系统不断分解的过程。

    对移动设备的支持,使得我们将页面渲染逻辑从服务器端剥离出来交由客户端完成,服务器只负责提供数据。通过rest的超媒体特性,客户端和服务器端程序能够各自独立演进。

    对订单流程的支持,使得我们采用工作流技术,将资源模型(订单)与其生命周期模型分离,分别交由原有系统和工作流系统管理,这样当某部分需求发生变化(例如增加一个审核不通过步骤),能够隔离变化,容易修改。

    对端到端跨系统流程的支持,使得我们引入一个独立的业务流程管理系统,由此来协调各个系统间的集成工作,避免系统间的大量耦合。同时,我们看到定义和标准化一个大家都能理解的media type是如此的重要。

    对开放平台的支持,使得我们将业务流程管理系统开放出来,作为公有云服务平台,同时流程中的各个环节能够开放出来,作为服务/应用接入和接出的接口,真正做到松耦合,由此看到开放平台的实质是流程服务的开放。

    在同事陈金洲的《架构腐化之谜》中,文章最后提到解决这一问题的方法是:创建应用程序的生态环境,而非单一的项目。深得我心,而我们这里想表达的是,随着系统的演进,我们需要不断进行系统的分解,做到服务的独立演化。

    至于rest,它的作用就在于充分利用HTTP协议,将其作为一种应用协议,而不仅仅是传输协议,这样在WEB上做集成时能够最大程度上的达成一致,形成大家都能接受的约定,减少集成的工作量,对外接口一致,内部独立演化。