[转载]业务逻辑层的封装实践

mikel阅读(1132)

[转载]业务逻辑层的封装实践 – 无待 – 博客园.

在系统开发中,通常都会采用经典的三层或者四层架构。其中数据模型层通过ORM工具来生成模型代码,实现了数据库操作的CRUD方法,上层的业务层 进行简单的封装,供界面层调用。但由于模型层是与数据库中的单个表对应,而很多数据模型之间是有关联和上下级关系的,如果仅仅对业务层做简单封装,作为传 值和分层之用,则很可能在开发和维护中出现以下问题:

1. 上层界面在增加和修改数据时,需要维护数据之间的关联和上下级关系;

2.上层界面调用删除等操作时,需要处理级联删除相关数据;

3.上层界面在操作某个数据的下级菜单时,通常要重新获取,增加了数据库访问次数;

4.上层界面在根据用户选择的数据来控制操作菜单时,需要重新加载权限信息,进行复杂的权限判断;

5.通常的数据修改操作都需要记录日志,则界面层有大量的日志记录代码;

6.在进行排序和移出(移进)操作时,需要大量代码来实现。

以上问题是我在C/S结构的应用程序开发中亲身经历过的,不知是否具有普遍性。于是,希望业务层能封装上述问题相关的“附加”信息,希望达到以下效果:

1.能够在移出(移进)时能自动判断两个数据之间是否支持该操作;

2.在常用的排序中业务对象能自我排序;

3.在用户选中某个数据时,能识别出用户权限,从而控制界面菜单;

4.将日志的操作封装成统一接口,并在数据修改发生时业务层自动记录,上层操作不知道日志记录;

5.将系统中的各类数据抽象成统一的资源接口。

按照以上的需求,对业务层进行了点儿封装,基本上达到了上述的几个需求,同时其它开发者在调用过程中感觉到很顺手和舒服,代码量也减少很多,便于系统的升级和维护,上层调用代码示例如下:

//将当前选中数据按名称排序
IDataResource resource = resPad.GetSelectedResource();
if (resource == null)
return;
resource.SortByName();
resPad.RefreshNode(resource);

//权限判断
IDataResource resource = resPad.GetSelectedResource();
if (resource != null && !resource.ReadOnly)
{
resource.Update();
}

//数据移出
smDatasetDirectory dir = treeRes as smDatasetDirectory;
foreach (ListViewItem item in this.listViewUngroup.SelectedItems)
{
IDataResource res = item.Tag as IDataResource;
dir.MoveOut(res);
}

//删除被选中数据资源及其下级
IDataResource resource = resPad.GetSelectedResource();
resource.Delete();

//添加数据
smLayer layerLogic = new smLayer("layer1");
mapLogic.AddResource(layerLogic);

示意类图如下:

主要类图:

设计要点:

1.所有业务对象都是数据资源(甚至包括用户和角色等),统一实现顶级接口;

2.顶级的虚基类实现大部分通用功能(比如日志记录和排序等),各业务对象只关心自身的特别业务;

3.业务对象除了包含模型数据,应该是自我描述的,标识自身数据资源的类型,具有权限信息;

4.业务对象自身知道能够与哪些对象发生关系,比如是否能移到某个资源下;

5.业务对象能够知道自己的上级资源是哪个,下级资源是哪些;

6.当业务对象被执行某种数据库操作时,应该能自动记录下相关操作日志,不必由上层调用来关心日志。

以上是我的一点儿设计实践,欢迎大家拍砖!

代码下载:BusinessLogicCommon_src.rar

[转载]ubuntu下调试android手机,并进入手机shell终端

mikel阅读(1087)

[转载]ubuntu下调试android手机,并进入手机shell终端 – 黑暗伯爵 – 博客园.

既然玩Android开发,就必须也得深入到系统,模拟器慢得要死,就上真机了。

windows下一切都是那么小白,换到linux下就得自己动手。

查了相关资料 ,终于知道怎么弄了。

我的机型是 moto mb525 (Defy)

首先准备好一切环境

adb,官网有,开发肯定离不开这个Android sdk

手机设置为调试模式

能看到这篇文章,你的手机肯定是已经root过了。

1、首先查看手机的usb信息

hang@CAPF:~$ lsusb
Bus
002 Device 004: ID 18c3:6255
Bus
002 Device 002: ID 8087:0020 Intel Corp. Integrated Rate Matching Hub
Bus
002 Device 001: ID 1d6b:0002 Linux Foundation 2.0 root hub
Bus
001 Device 005: ID 22b8:41db Motorola PCS Motorola Droid (USB Debug)
Bus
001 Device 004: ID 04d9:a06b Holtek Semiconductor, Inc.
Bus
001 Device 003: ID 058f:b002 Alcor Micro Corp.
Bus
001 Device 002: ID 8087:0020 Intel Corp. Integrated Rate Matching Hub

Bus 001 Device 001: ID 1d6b:0002 Linux Foundation 2.0 root hub

lsusb命令是列出当前所有得usb设备,我的defy就是

Bus 001 Device 005: ID 22b8:41db Motorola PCS Motorola Droid (USB Debug)

设备id是 22b8 ,设备信息描述是

Motorola PCS Motorola Droid (USB Debug)hang@CAPF:/etc/udev$ cd /etc/udev/rules.d

2、添加udev规则

udev就是一个动态硬件管理服务

hang@CAPF:/etc/udev/rules.d$ cd /etc/udev/rules.d/

hang@CAPF:/etc/udev/rules.d$ sudo vi 50Androidusb.rules

编辑规则文件并保存

SUBSYSTEM==usb, SYSFS(Motorola PCS Motorola Droid (USB Debug))==22b8,MODE=0666

其中,sysfs括号内是自己android手机的实际描述信息,==后面的是id号,mode是读取模式,0666是所有人可以访问,以上的信息都是lsusb查处来的。

3、设置规则文件权限并重启udev

hang@CAPF:/etc/udev/rules.d$ sudo chmod a+rx /etc/udev/rules.d/50androidusb.rules

hang@CAPF:/etc/udev/rules.d$ sudo /etc/init.d/udev restart

会看到udev相关的提示信息

4、设置adb

进入sdk得platform-tools目录

hang@CAPF:/opt/androidsdklinux_x86/platformtools$ sudo ./adb killserver
hang
@CAPF:/opt/androidsdklinux_x86/platformtools$ sudo ./adb devices
* daemon not running. starting it now on port 5037 *
* daemon started successfully *
List of devices attached

0163C5420200F029    device

大功告成,可以在eclipse下使用真机调试了。

进入android shell的方法:

hang@CAPF:/opt/androidsdklinux_x86/platformtools$ ./adb shell

进去以后,就可以随便折腾了,,最好别乱删乱改

最后附上adb的相关参数,英文–help一样:

adb(Android Debug Bridge)是Android 提供的一个通用的调试工具,借助这个工具,我们可以很好的调试开发的程序,adb.exe在你安装的android的sdk开发包tools目录下

命令 描述
install <path> 安装应用程序到服务器
pull <remote file> <local file> 将远程文件拉出服务器
push <local file> <remote file> 将本地文件推进服务器
shell 在服务器上打开一个壳环境
forward <local port> <remote port> 从一个端口转递流量到另外一个端口(到或者从服务器上)
start-server 启动服务器
kill-server 停止服务器
ppp <tty> <params> 通过USB使用一个ppp连接
devices 列出可用的模拟器
help 列出adb的命令
version 显示adb的版本

adb使用方法:

adb [-d|-e|-s <serialNumber>] <command>

当你发出一个命令,系统启用Android客户端。客户端并不与模拟器实例相关,所以如果双服务器/设备是运行中的,你需要用-d选项 去为应被控制的命令确定目标实例。关于使用这个选项的更多信息,可以查看模拟器/设备实例术语控制命令。
1、安装应用 到模拟器:

你可以使用adb从你的开发电脑上复制一个应用程序,并且将其安装在一个模拟器/设备实例。像这样做,使用install命令。这个 install命令要求你必须指定你所要安装的.apk文件的路径:
adb install <path_to_apk>
为 了获取更多的关于怎样创建一个可以安装在模拟器/设备实例上的.apk文件的信息,可参照Android Asset Packaging Tool (aapt).
要注意的是,如果你正在使用Eclipse IDE并且已经安装过ADT插件,那么就不需要直接使用adb(或者aapt)去安装模拟器/设备上的应用程序。否则,ADT插件代你全权处理应用程序的 打包和安装.
如果要删除某个应用,按下面操作,例如应用为test:
adb shell
cd /data/app
rm test.apk

2、进入设备或模拟器的shell:
adb shell
通过上面的命令,就可以进入设备或模拟器的shell环 境中,在这个Linux Shell中,你可以执行各种Linux 的命令,另外如果只想执行一条shell命令,可以采用以下的方式:
adb shell [command]
如:adb shell dmesg会打印出内核的调试信息。

3、发布端口:
可以设置任意的端口 号,做为主机 向模拟器或设备的请求端口。如:
adb forward tcp:5555 tcp:8000

4、从模拟器/设备中拷入或 拷出文件:
可 以使用adbpull ,push命令将文件复制到一个模拟器/设备实例的数据文件或是从数据文件中复制。install命令只将一个.apk文件复制到一个特定的位置,与其不 同的是,pull和push命令可令你复制任意的目录和文件到一个模拟器/设备实例的任何位置。
从模拟器或者设备中复制文件或目录,使用(如下 命):
adb pull <remote> <local>
将文件或目录复制到模拟器或者设备,使用(如下命 令)
adb push <local> <remote>
在这些命令中,<local> 和<remote>分别指通向自己的发展机(本地)和模拟器/设备实例(远程)上的目标文件/目录的路径
下面是一个例子:
adb push test.txt /data/dat/test/test.txt、

5、查询模拟器/设备实例:
在发布adb命令之前,有必要知道什么样的模拟器/设备实例与adb服务器是相连的。可以通过使用 devices
命令来得到一系列相关联的模拟器/设备:
adb devices

作为回应,adb为每个实例都制定了相应的状态信息:
序列号——由adb创建的一个字符串,这个字符串通过自己的控制端 口<type>-<consolePort>唯一地识别一个模拟器/设备实例。
下面是一个序列号的例子:
emulator-5554
实 例的连接状态有三种状态:
offline — 此实例没有与adb相连接或者无法响应.
device — 此实例正与adb服务器连接。注意这个状态并不能百分之百地表示在运行和操作Android系统,因此这个实例是当系统正在运行的时候与adb连接的。然 而,在系统启动之后,就是一个模拟器/设备状态的正常运行状态了.
每个实例的输出都有如下固定的格式:
[serialNumber] [state]
下面是一个展示devices命令和输出的例子 :
$ adb devices
List of devices attached
emulator-5554  device
emulator-5556  device
emulator-5558  device

如 果当前没有模拟器/设备运行,adb则返回 no device .
6、查看bug报告:
adb bugreport

7、记录无线通讯日志:
一般来说,无线通讯的日志非常多,在运行时没必要去记录,但我们还是可以通过命令,设置记录:
adb shell
logcat -b radio

8、获取设备的ID和序列号:
adb get-product
adb get-serialno

9、 访问数据库SQLite3
adb shell
SQLite3

如要打开已存在数据库:

sqlite3 <路径>/dbname.db

[转载]蛙蛙推荐:用javascript作一个通用向导

mikel阅读(1078)

[转载]蛙蛙推荐:用javascript作一个通用向导 – 蛙蛙王子 – 博客园.

摘要:向导可以让你的网站用户快速上手使用你的web应用,提高网站的吸引力。向导一般分为好几个步骤,每个步骤收集一些数据,并且支持退回功能,所有步骤完成后可以得到每一步的收集结果。这里给大家展示一种比较通用,灵活且简单的向导框架。

1、界面设计

index.html:只提供了一个向导显示位置的占位符

<html>
<head>
<title>礼物推荐向导</title>
<meta http-equiv="content-type" content="text/html; charset=utf-8">
<link rel="stylesheet" type="text/css" href="style.css">
<script src="jquery.js" type="text/javascript"></script>
<script src="wizard.js" type="text/javascript"></script>
</head>
<body>
<div id="wizard"></div>
</body>
</html>

style.css:默认情况下向导里有一个h2呈现的标题,一个ul呈现的主要内容,一个div呈现的按钮条,我们简单设计了一下他们的默认外观,实际应用中大家可以自由的美化它们。

body{
margin:0;
}
/*向导容器*/
#wizard{
height:400px;
width:600px;
background-color:#999;
}
/*向导的主体内容,用列表展示*/
#wizard ul{
margin:10px;
height:80%;
}
/*横向显示列表内容*/
#wizard li{
display:inline-block;
margin:10px;
cursor:pointer;
}
/*列表的标题*/
#wizard h2{
margin:10px;
}
/*列表的功能条,如返回按钮*/
#wizard .bar{
margin:10px;
clear:both;
}

2、准备每一步骤

向导可以分为每一步骤,每个步骤需要呈现内容,捕捉用户选择,提供标题等功能,我们让每一步都自己负责自己的事情,但要符合我们规定的一些契约。

每一个步骤用一个函数表示,第一个参数data_key是选择本步骤数据的关键字,一般用于上一个步骤的结果决定下一个步骤显示数据的情况,第二个参数result_callback是个回调函数,就是在本步骤获取结果时调用,它用于和向导类进行通信,向导类在得到上一步的结果后存储结果并跳向到下一步。

该函数返回一个二元组,第一个元素是本步骤的标题,第二个元素是本步骤主体部分的UI。

我们的示例是一个礼物推荐系统,共分三步,第一步选择送礼对象,第二步选择关键字,其中第一步的选择结果会影响到第二步显示,第三步选择价格区间,如下就是代码的实现,其中绘制界面和事件捕捉用了JQuery来简化操作。

function step1(data_key, result_callback){
var targets = ['女朋友','男朋友','父亲','妈妈','孩子'];
var warpper = $('<ul></ul>')
$.each(targets, function(k,v){
$('<li>'+v+'</li>').click(function(){result_callback(v)}).appendTo(warpper);
});
return ['第一步:请选择送礼物的对象',warpper];
}
function step2(data_key, result_callback){
var tags = {
'女朋友':['创意','可爱','浪漫','激情','实用','数码',
'自制','毛绒玩具','衣服','包包'],
'男朋友':['男士用品','温馨','实用','数码','创意','衣物'],
'父亲' :['男士用品','健康','植物','衣物'],
'妈妈' :['温馨','健康','创意','护肤品','实用'],
'孩子' :['玩具','学习用品','实用','数码']
};
var warpper = $('<ul></ul>')
$.each(tags[data_key], function(k,v){
$('<li>'+v+'</li>').click(function(){result_callback(v)}).appendTo(warpper);
});
return ['第二步:请选择关键词',warpper];
}
function step3(data_key, result_callback){
var price_level = ['便宜','普通','稍贵','贵重'];
var warpper = $('<ul></ul>')
$.each(price_level, function(k,v){
$('<li>'+v+'</li>').click(function(){result_callback(v)}).appendTo(warpper);
});
return ['第三步:请选择价格区间',warpper];
}

3、向导类的实现

向导类要设置向导所在的DOM元素,要执行的步骤列表,向导完成后执行的回调,向导还应该提供上一步和下一步的方法,所以我们用一个类来表示向导,在构造函数里传入DOM容器,步骤列表和回调函数,用prototype给类增加三个方法。render用来呈现某一步骤的UI,并在本步骤收集结果的回调里推向下一步,如果本步骤是最后一步,则调用向导执行完成的回调函数。

另外两个next和back函数分别是执行上一个步骤和下一个步骤,这两个函数实用index的私有变量来维持整个向导的状态

function Wizard(container, steps, callback){
this.container = container; //向导容器
this.steps = steps; //向导步骤
this.callback = callback; //向导执行完毕执行的回调
this.collect_data = []; //保存向导每一步骤的结果
this.index = -1; //当前执行在那一步骤
}
//绘制某一步骤
Wizard.prototype.render = function(step, this_result){
var me = this;
//执行该步骤并得到该步骤的UI
var to_append = step(this_result,function(result){
me.collect_data.push(result); //收集本步骤结果
//向导执行完毕时调用回调函数,否则执行下一步
if(me.collect_data.length == me.steps.length)
me.callback(me.collect_data);
else
me.next(result);
});
//绘制本步骤的UI
this.container.empty();
this.container.append(“

“+to_append[0]+”

“);
this.container.append(to_append[1]);
if(this.index > 0){
//后退按钮
this.container.append($(“

“)
.click(function(){me.back()}
));
}
}
//执行下一步
Wizard.prototype.next = function(this_result){
if(this.index >= this.steps.length -1)
return;
var step = this.steps[++this.index];
this.render(step,this_result);
}
//后退到上一步
Wizard.prototype.back = function(){
if(this.index <= 0) return; var step = this.steps[--this.index]; //步骤回到上一步,但上一步的数据需要上上一步的结果来决定 this.collect_data = this.collect_data.slice(0, this.index); this.render(step, this.collect_data[this.index - 1]); } [/js] 4、小结 本向导结构简单,可定制性强,结合了JavaScript的函数式编程特性和面向对象的特性,体现了JavaScript的强大和便利。 其中wizard类里界面绘制的部分和步骤函数里界面绘制的部分还是存在一些耦合,继续重构的话,可以把所有绘制界面的部分再抽象到一起,使界面改动更方便。

[转载]SQL点滴20—T-SQL中的排名函数

mikel阅读(1021)

[转载]SQL点滴20—T-SQL中的排名函数 – nd – 博客园.

提 到排名函数我们首先可能想到的是order by,这个是排序,不是排名,排名需要在前面加个名次序号的,order by是没有这个功能的。还可能会想到identity(1,1),它也给了一个序号,但是不能保证给出的序号是连续升序的。除非能够保证所有的 Insert语句都能够正确成功地完成,并且没有删除操作,实际的使用中大多数的表都不能保证这样。

好在SQL Server中提供了一些排名函数来辅助实现这些功能。排名函数按照需要的顺序对数据进行排名,并提供一个值对数据。下面来了解一下这些排序函数功能。

ROW_NUMBER

ROW_NUMBER函数允许以上升,连续的顺序给每一行数据一个序号,注意ROW_NUMBER()后面一定要跟着over子句。来看语句:

use AdventureWorks
select
ROW_NUMBER() over(order by LastName) as RowNum,
FirstName+' '+ LastName as FullName
from HumanResources.vEmployee
where JobTitle='Production Technician - WC60'

这个语句对符合条件(JobTitle=’Production Technician – WC60’)的LastName按照升序排列,并加上排序的序号,这个序号是连续上升的。结果如下图1是部分结果。

图1

我们可以看到第一个人的LastName是Abercrombie,第二个人的LastName是Adams,以次类推。

PARTITION

如果我们想再细分一下,在一个小的分组范围内排序该怎么办呢?就是说让LastName以‘A’开头的作为第一组,在这个组内进行排序。以‘B’开 头的作为第二组,在这个组内排序。以‘C’开头的作为第三组,在这个组内进行排序,如此等等。这里有一个很简单的实际例子,假如上面这些人都来参加同一场 马拉松比赛,其中有男子组,女子组,男子残疾组,女子残疾组,60岁以上组等等。不管参赛者以第几位触线,名次都以他们的小组为基准。

可以通过PARTITION BY选项来重新排序,给数据分区或者数据区域唯一的递增序号。来看下面的语句:

[注] partition n. 划分,分开;[数] 分割;隔墙;隔离物;vt. [数] 分割;分隔;区分

select
ROW_NUMBER() over(PARTITION by substring(LastName,1,1) order by LastName) as RowNum,
FirstName+' '+ LastName as FullName
from HumanResources.vEmployee
where JobTitle='Production Technician - WC60'

这里模拟上面的情况,首先以Last Name的第一个字母作为分组,然后以第二个字母以后的字母来分组排序。来看看结果,如图2



图2

假设LastName以‘A’开头的是男子组,这个组有共有三个人,Kim Abercrombie是冠军,Jay Adams是亚军,Nancy Anderson是季军。假设LastName以‘B’开头的是女子组,这个组只有一个人Bryan Baker,无论如何她都是冠军。等等如此类推。这样一眼就能看出他们的小组名次了。

这里你可能会觉得使用order by一样可以得到这样类似的结果。如下代码:

select
FirstName+' '+ LastName as FullName
from HumanResources.vEmployee
where JobTitle='Production Technician - WC60'
order by substring(LastName,1,1) ,LastName

这个把order by放在最后,排序放在最后,首先按照LastName的首字母排序,再按照剩整个LastName排序,结果如下图3



图3

结果和上面大致相同,可是少了前面的名次序号。于是我又对她进行了修改,代码如下:

select
ROW_NUMBER() over(order by substring(LastName,1,1),LastName) as RowNum,
FirstName+' '+ LastName as FullName
from HumanResources.vEmployee
where JobTitle='Production Technician - WC60'

这个和上面的类似,在排名函数中使用order by,并且是按照多个字段排序。来看看结果如图4



图4

排 序没有错误,是我们想要的分组排序,但是前面的名次没有分组区分,和图1没有什么差别。可见图3和图4的做法完全是多余,纯属臆造,其实只要order by LastName都能得到正确的排序,只有partition by才是正解。通过上面的例子也可以对排序,排名这二者之间的区别有一个认识,他们虽然有相似之处,但是排名始终会产生一个名次序号,排序只要得到正确的 顺序就好。

RANK

还是拿马拉松比赛来说事,如果有同时撞线的情况发生应该怎么计名次呢?例如A第一个撞线,B和C同时第二个撞线,D第三个撞线,如果我们想把D的名次计为第4名应该怎么处理呢?就是说不计顺序名次,只计人数。这时就可以使用RANK函数了。

[注] rank n. 等级;队列;排;军衔vt. 排列;把…分等vi. 列队;列为

在order by子句中定义的列上,如果返回一行数据与另一行具有相同的值,rank函数将给这些行赋予相同的排名数值。在排名的过程中,保持一个内部计数值,当值有所改变时,排名序号将有一个跳跃。

来看下面的语句:

select
ROW_NUMBER() over(order by Department) as RowNum,
RANK() over(order by Department) as Ranking,
FirstName+' '+ LastName as FullName,
Department
from HumanResources.vEmployeeDepartment
order by RowNum

rank()函数右面也要跟上一个over子句。为了看到效果我们以Department作为排序字段,可以看到RowNum作为升序连续排名,Ranking作为计同排名,当Department的值相同时,Ranking中的值保持不变,当Ranking中的值发生变化时,Ranking列中的值将跳跃到正确的排名数值。来看结果:



图5

从 这个结果中我们可以说这次马拉松赛跑的排名是:Tengiz Kharatishvili,Zainal Arifin,Sean Chai,Karen Berge,Chris Norred并列第1,Michael Sullivan,Sharon Salavaria,Roberto Tamburello,Gail Erickson,Jossef Goldberg并列第6,如此等等。



DENSE_RANK

在上面的例子中,A第一个撞线,B和C同时第二个撞线,D第三个撞线,如果我们想把B和C的名次计位第2名,D的名次计为第3名应该怎么处理呢?就是说考虑并列名次。这里使用DENSE_RANK函数,来看下面的代码。

select
ROW_NUMBER() over(order by Department) as RowNum,
DENSE_RANK() over(order by Department) as Ranking,
FirstName+' '+ LastName as FullName,
Department
from HumanResources.vEmployeeDepartment
order by RowNum

结果如下:



图6

按照这个结果,我们可以说这次马拉松赛跑的排名是:Tengiz KharatishviliZainal ArifinSean ChaiKaren BergeChris Norred并列第1Michael SullivanSharon SalavariaRoberto TamburelloGail EricksonJossef GoldbergTerri Duffy并列第2,等等如此。

NTILE

在开始这个之前,先来一段小插曲。梭罗是铅笔的发明者,不过他没有申请专利。据说他天赋异禀,在父亲的铅笔厂里面打包铅笔的时候,从一堆铅笔里面抓取一把,每次都能精确地抓到一打12支。他在森林中目测两颗树之间的距离,和护林员用卷尺测量的结果相差无几。现在如果我们想从一张表中抓取多比数据,每一笔都是相同的数目,并且标明第几组该怎么办呢?NTILE函数提供了这个功能,他能。来看代码:

select
NTILE(30) over(order by Department) as NTiles,
FirstName+' '+ LastName as FullName,
Department
from HumanResources.vEmployeeDepartment

现在我们要抓取30个组的数据,并保证尽可能的保证每组数目相同。结果如下,



图7

这个视图中共290条数据,290/30=9.7约等于10,所以每组10条数据,如图每一条数据都有一个组号。这个结果要比索罗精确。

[转载]Web三维技术:Flash Builder+away3d平台搭建(含演示视频)

mikel阅读(1427)

[转载]Web三维技术:Flash Builder+away3d平台搭建(含演示视频) – 一点一滴的Beer – 博客园.

Web三维技术:Flash Builder+away3d平台搭建

作者:一点一滴的Beer http://beer.cnblogs.com/

前言:作为NCSLab中实验设备的显示层,需要一个swf作为显示的UI。虽然可以用FlashMX进行简单 的flash设计,但是FlashMX一般是偏向于动画设计,而不是程序设计,所以在进行相关的开发时,支持性不是太好,于是笔者就想到了转用Flex Builder来进行开发。

一、开发语言和开发环境

ActionScript是 Macromedia(现已被Adobe收购)为其Flash产品开发的 ,最初是一种简单的脚本语言,现在最新版本3.0,是一种完全的面向对象的编程语言,功能强大,类库丰富,语法类似JavaScript,多用于 Flash互动性、娱乐性、实用性开发,网页制作和RIA应用程序开发

ActionScript 是一种基于ECMAScript的脚本语言,可用于编写Adobe Flash动画和应用程序。 由于ActionScript和JavaScript都是基于ECMAScript语法的,理论上它们互相可以很流畅地从一种语言翻译到另一种。不过 JavaScript的文档对象模型(DOM)是以浏览器窗口,文档和表单为主的,ActionScript的文档对象模型(DOM)则以SWF格式动画 为主,可包括动画,音频,文字和事件处理。

由于软件和库文件存在版本多样的问题,所以在进行介绍之前进行说明。本文中所演示例子使用的软件版本如下:

Flash Builder:4.5

Flex SDK:4.5.1

Away3D:3.5.0

Flash Player:10 Debug版本或者更高

1.1 Flash Builder集成开发环境

Adobe® Flash® Builder™ 4.5 软件(曾为 Adobe Flex® Builder™)是基于 Eclipse™ 的开发工具,使用 ActionScript® 和开源 Flex 框架快速构建具有表现力的移动、Web 和桌面应用程序。软件可以在Adobe主页上去下载。

安装了Flash Builder4.5之后,Flex SDK4.5也自动被集成到了开发环境中了。然后就可以利用此IDE进行一般的Flex开发了,但是如果要进行3d开发,则还需要引入3d的引擎库。

Flash Builder 4.5下载页面:https://www.adobe.com/cfusion/tdrc/index.cfm?product=flash_builder&loc=zh_cn

1.2 Away3D引擎库

提供了一个3d引擎库。在Flash Builder中建立项目,然后引入此库,可以迅速进行3d的Flash开发。

Away3D引擎属于一个免费开源的项目,而且持续开发和更新中,所有存在版本众多的问题。目前我们主要使用的是稳定版本的3.5版。在 Away3d的主页中可以下载到如下资源:Away3d库、Away3d Examples例程、Away3d Documents参考文档。

Away3d 3.5下载页面:http://away3d.com/download/away3d_3.5.0

1.3 Flash Player播放器

编程的语言ActionScript目前主要分为2.0和3.0两个版本,Flash播放器相当于ActionScript的编译环境,对于不 同版本的ActionScript需要不同的播放器支持,如果使用了低版本的Flash Player则会出现编译无法通过。对于Away3d_3.5库,需要至少Flash Player10的支持。如果希望能够在Flash Builder中对源代码进行调试,则需要到Adobe主页上下载一个Debug版本的Flash Player10。

在Adobe主页上下载Flash Player的时候,除了有发布版和调试版之分,也有IE和非IE之分。如果你使用的是IE或者基于IE内核的浏览器,则下载基于IE的Debug版 Flash Player10,如果是使用Chrome或者Firefox这些非IE内核的浏览器,则下载相应的播放器。

Flash Player 10下载页面:http://www.adobe.com/support/flashplayer/downloads.html

先到网上下载到上述的软件后,就可以遵循下述步骤进行环境搭建:

1.卸载本机上的旧版本的Flash Player,安装Flash Player10。

2.安装Flash Builder4.5到指定目录,例如:

“D:\Program Files\Adobe\Adobe Flash Builder 4.5”

如果成功安装,则Flex Sdk4.5也会被成功安装上,在目录:

“D:\Program Files\Adobe\Adobe Flash Builder 4.5\sdks”

里面包含了不同版本的Flex SDK,今后如果Adobe发布新的SDK,则只需要下载新的SDK然后解压到此目录中即可。

3.解压Away3d的库文件到指定目录。例如:

“D:\FlashBuilder_Libs\Away3d_lib”

在后面进行Away3d开发时,需要引用此路径中的库文件。

完成上述工作后,就可以进行Away3d的Flex开发了。

二、建立ActionScript项目

关于如何建立一般的Flex项目,在此不再赘述,有兴趣的可以到网上找到很多相关例子。本文重点在让Away3d 3.5提供的Examples成功跑起来。

2.1项目建立与设计

打开Flash Builder,然后新建项目,选择”ActionScript Project”,然后输入项目名称点击Finish。在Package Explorer中右击项目选择”Properties”进行相关设置。设置内容包括:

1.设置Flex SDK版本。

一般情况下,在建立项目的时候就已经对SDK进行了指定,但是如果在项目建立完毕后需要修改SDK,可以在此界面下进行修改:ActionScript Build PathàLibrary path

2.设置Away3d库的引用路径。

在ActionScript Build PathàSource path中,可以添加Away3d的引擎库,也就是我们下载下来解压下来的文件夹,添加解压目录src中的内容。

3.设计编译器版本

ActionScript的编译环境就是Flash Player,Away3d 3.5需要10以下的版本的播放器的支持。在Flash Builder4.5中没有必要对此进行设置,因为默认的就是10.2的版本,但是如果今后3d库升级需要更高版本的播放器支持,则需要在此进行相应的设 置。

2.2Away3d Examples项目移植

将从Away3d主页上下载的3.5版本对应的”examples_3_5_0.zip”解压,然后将根目录下的文件复制到新建的ActionScript项目的根目录下进行覆盖。在Flash Builder中对项目的目录进行刷新。然后就可以了。

如果想要运行某文件,例如:”Basic_LoadModel.as”则只需要右击此文件,然后选择”Set as Default Application”,再右击选择Run asà Web Application,然后就可以在浏览器中看到运行的效果图了。

在项目的bin-debug目录下面,会生成对应的swf文件,这个可以独立运行的。

Away3d Examples的演示效果见如下视频:

http://v.youku.com/v_show/id_XMjgzMzI2MDgw.html

2011-7-8

于武汉大学

——————————————————————

Author:一点一滴的Beer

Email /Gtalk:dreamzsm@gmail.com

From:http://www.cnblogs.com/beer

Notes:欢迎转贴,但请在页面中加个链接注明出处

original:http://www.cnblogs.com/beer/archive/2011/05/05/2037449.html

[转载]ASP.NET MVC过滤器中权限过滤器ValidateAntiForgeryToken的用法(Post-Only)

mikel阅读(1219)

[转载]ASP.NET MVC过滤器中权限过滤器ValidateAntiForgeryToken的用法(Post-Only) – 没追求的猪 – 博客园.

用途:防止CSRF(跨网站请求伪造)。

用法:在View->Form表单中:<%:Html.AntiForgeryToken()%>

在Controller->Action动作上:[ValidateAntiForgeryToken]

原理

1、<%:Html.AntiForgeryToken()%>这个方法会生成一个隐藏域:<input name=“__RequestVerificationToken” type=“hidden” value=“7FTM…sdLr1” />并且会将一个以“__RequestVerificationToken“为KEY的COOKIE给控制层。

2、[ValidateAntiForgeryToken],根据传过来的令牌进行对比,如果相同,则允许访问,如果不同则拒绝访问。

关键:ValidateAntiForgeryToken只针对POST请求。

换句话说,[ValidateAntiForgeryToken]必须和[HttpPost]同时加在一个ACTION上才可以正常使用。

这其中的原理我也没想明白,等下次好好把MVC的源代码看看。

不过我这么说是有根据的,我写了一些案例做了测试。

案例

1、在一个ACTION的GET和POST方式分别加了[ValidateAntiForgeryToken]特性

Action:



2、用一个测试页面以POST方式去请求ACTION,结果是成功的。并且,隐藏域的值和COOKIE都是可以拿到的。

测试Post的页面:

3、用一个测试页面以GET方式去请求ACTION,报错。

测试Get的页面:



推荐使用方式:

1、Post-Only:大概思想是,拒绝所有的GET,只允许自己的POST。(安全,但不灵活)

2、GET只做显示,对所有的GET开放;POST做修改,对外界关闭,对自己开放。(灵活,但不够安全)

国外有个人说,其实这个过滤器本身就不安全,他如是说,所有的REQUEST都是可以伪造的。

[转载]SQL 将一个字段内用逗号分隔的内容分成多条记录

mikel阅读(1087)

[转载]SQL 将一个字段内用逗号分隔的内容分成多条记录 – 岩子 – 博客园.

由于业务需求,我们可能会把一串以分割符字符串数据放到一个字段,如我们在客户端处理拆分是很简单的,不过这样做效果不太好,怎么用SQL SERVER 2008 来解决这件事件哪?

方案1

SQL SERVER XML 功能来解决

----&gt; 生成测试数据: @T
DECLARE @T TABLE (id INT,Col1  VARCHAR(50),Col2 VARCHAR(50))
INSERT INTO @T
SELECT 1,'卸货费','运输车队业务, 客服接单业务'
UNION ALL
SELECT 2,'报关费','报关业务、客服接单业务'

----SQL查询如下:

SELECT
T.id,
A.x.value('.','varchar(10)') AS code,
B.x.value('.','varchar(10)') AS code2
FROM (
SELECT *,doc = CONVERT(xml,''+REPLACE(code,',','')+''),
doc2=CONVERT(xml,''+REPLACE(code2,',','')+'')
FROM @T
) AS T
CROSS APPLY doc.nodes('//v') AS A(x)
CROSS APPLY doc2.nodes('//v') AS B(x)

方案2

用SQL SERVER WITH 递归查询

WITH roy
AS ( SELECT Col1 ,
COl2 = CAST(LEFT(Col2, CHARINDEX(',', Col2 + ',') - 1) AS NVARCHAR(100)) ,
Split = CAST(STUFF(COl2 + ',', 1,
CHARINDEX(',', Col2 + ','), '') AS NVARCHAR(100))
FROM Tab
UNION ALL
SELECT Col1 ,
COl2 = CAST(LEFT(Split, CHARINDEX(',', Split) - 1) AS NVARCHAR(100)) ,
Split = CAST(STUFF(Split, 1, CHARINDEX(',', Split), '') AS NVARCHAR(100))
FROM Roy
WHERE split &gt; ''
)
SELECT COl1 ,
COl2
FROM roy
ORDER BY COl1
OPTION ( MAXRECURSION 0 )

[转载]微博应用--PC遥控器 正式开源

mikel阅读(907)

[转载]微博应用–PC遥控器 正式开源 – 小No – 博客园.

首先简单介绍以下PC遥控器是个神马东西。

你只需在被控电脑端安装上 PC遥控器,设置好微博登录账号(最新版同时支持新浪和腾讯微博)。这样无论你在何处何地,只要发个微博,就可以对你的电脑进行关机、重启、关闭显示器、查看屏幕截图等操作!简单方便!非常实用!

PC遥控器目前是靠我业余时间往里面添砖加瓦,但是一个人的力量和精力是有限的,为了让这款微博小应用的功能更加丰富和实用,我决定把PC遥控器开源,希望园里面有朋友能为PC遥控器贡献点代码。

PC遥控器的官方博客:http://suchuanyi.sinaapp.com/

源码是托管在github上面,地址是:https://github.com/terryso/PCRemote

PC遥控器有兴趣,想贡献代码的园友,你只需把你的源码提交到你自己的仓库,然后在github上面通知我去拉就可以了。

PC遥控器的源码里面还包含了一个我自己写的微博SDK,目前支持新浪和腾讯微博的API,只不过目前只包含PC遥控器用到的几个API而已。
不过在这个SDK的基础上新增一个API也是很简单的,对这个SDK有兴趣的园友也可以看看,给些意见。

[转载]百度地图API详解之地图容器

mikel阅读(1270)

[转载]百度地图API详解之地图容器 – jz1108 – 博客园.

百度地图API详解

地图容器就是包含地图区域的那个框框,这个有什么可说的呢?请您往下看。

通常我们会给地图容器一个固定的尺寸:


假设上面的div元素就是我们的地图容器,那么地图显示范围就是500×320:

有时我们希望地图容器随着浏览器变化而变化,那么我们将样式调整为:

html{height:100%}
body{height:100%;margin:0px;padding:0px}
#map{height:100%}

这时地图区域会跟随着浏览器的大小自动变化,另外API也会根据外框尺寸的变化自 动铺设图块以保证视野内地图是完整的。通过监听map的resize事件可得知地图容器在什么时候发生变化了。注意在容器大小发生变化时,API默认保证 地图到左边和上边的距离始终一致,这样做的目的也是保证地图相对于人眼的位置不发生变化(因为用户在修改浏览器窗口尺寸时通常都会调整右下方的点),简单 说就是用户不会感到地图有任何的“跳动”:

我们看到随着浏览器窗口变大,地图逐渐展示出北京市东南区域的图块,而整个地图相对于左上角的位置没有发生变化。还有一点需要注意,这种情况下地图中心点是发生变化的了,之前中心位于天安门附近,之后中心点向东南方向移动了。我们可以通过监听resize事件来验证:

map.addEventListener('resize', function(){
console.log(this.getCenter().lng + ',' + this.getCenter().lat);
});

下面是在firebug中看到的数据,可以看出经度不断变大,纬度不断减小,说明中心点向东南方向移动了。

如果希望不管容器怎样变化地图中心点都保持一致该怎么办呢?首先你需要禁止地图自动适应容器变化,这可通过设置MapOptions的enableAutoResize属性为false或者调用Map的disableAutoResize方法来实现:

var map = new Map('map', {enableAutoResize: false});

此时你会发现容器变化时,地图不再自动铺图,也不会派发resize事件了。此时我们需要自己监听body的resize事件:


resizeMap方法中需要做如下工作:

function resizeMap() {
var center = map.getCenter();
map.checkResize();
map.setCenter(center);
}

我们先获取地图中心点,然后调用Map的checkResize方法通知地图容器发生变 化,此时地图会进行铺图,最后我们设置地图中心点为之前的获取的值。这样不论地图容器如何变化,地图中心点始终保持一致。去哪(qunar.com)酒店 的页面上就有这样一个地图,其提供了大图模式和小图模式切换的按钮,切换后地图中心点保持一致(地址在此)。

还有一种稍复杂的情况,对于以地图应用为主的网站,通常界面布局会是如下样式:

网站上方是搜索区域,左侧是结果展示区域,剩下的区域用来展示地图。网站很可能提 供一个全屏按钮来隐藏上面和左面的区域,为了获得更好的用户体验,我们希望此时地图相对于整个浏览器的位置不会发生变化,仿佛上面和左面的区域是盖在地图 上的。这样空说可能不好理解,我们来看实际的例子:

首先准备完整的HTML页面:


百度地图API详解
<!-- html{height:100%;} body{height:100%;margin:0px;padding:0px;} #top{width:100%;height:79px;border-bottom:1px solid #ccc;background:#eee;} #aside{position:absolute;top:80px;width:159px;border-right:1px solid #bbb;background:#ddd;} #map{margin-left:160px;} -->
<script src="http://api.map.baidu.com/api?v=1.2" type="text/javascript"></script> <script type="text/javascript">// <!&#91;CDATA&#91;
  function checkSize() { var h = document.documentElement.clientHeight; document.getElementById('aside').style.height = h - 80 + "px"; document.getElementById('map').style.height = h - 80 + "px"; } function init() { checkSize(); var map = new BMap.Map('map'); map.centerAndZoom('北京'); window.map = map; }
// &#93;&#93;></script>

页面效果如下:

这时我们修改浏览器窗口大小,地图会自动铺图并且地图相对于左边和上边的距离保持不变,一切效果OK。下面我们添加编写一个全屏函数:

function toFullScreen() {
document.getElementById('top').style.display = 'none';
document.getElementById('aside').style.display = 'none';
var h = document.documentElement.clientHeight;
var mapContainer = document.getElementById('map');
mapContainer.style.height = h + "px";
mapContainer.style.marginLeft = "0";
}

这个函数将两个区域进行隐藏,同时调整地图容器,我们在控制台调用这个方法,会得到这个结果:

和之前的图对比可以发现地图区域向左上方移动了,这样会给用户地图“跳动”的感觉,为了让地图与用户眼睛的位置保持一致,我们需要修改toFullScreen函数:

function toFullScreen() {
map.disableAutoResize();
var h = document.documentElement.clientHeight;
var curPix = map.pointToPixel(map.getCenter());
var newPix = new BMap.Pixel(curPix.x - 80, curPix.y - 40);
var newCenter = map.pixelToPoint(newPix);
document.getElementById('top').style.display = 'none';
document.getElementById('aside').style.display = 'none';
var mapContainer = document.getElementById('map');
mapContainer.style.height = h + "px";
mapContainer.style.marginLeft = "0";
map.checkResize();
map.setCenter(newCenter);
map.enableAutoResize();
}

这里大致的思路是,首先停止地图自动适应容器变化,接着通过坐标转换得到当前中心 点对应的像素坐标curPix,再计算出全屏后中心点的像素坐标newPix,进而转换为经纬度newCenter。下面调用修改容器尺寸并调用 checkResize通知地图容器发生变化,接着再重新设置中心点并恢复自动适应容器变化。效果如下:

此时,全屏过程中地图没有任何的“跳动”,从而提供了较好的用户体验。谷歌地图也使用了类似的效果,当左侧面板收起时,地图区域自动向左侧扩展,而没有向左侧跳动。

最后说明一下,上面的checkResize方法没有考虑全屏幕的情况,因为它不是本文的重点,就不再这里给出具体代码了。

[转载]SQL点滴19—T-SQL中的透视和逆透视

mikel阅读(1074)

[转载]SQL点滴19—T-SQL中的透视和逆透视 – nd – 博客园.

透视

今天抽一点时间来看看透视和逆透视语句,简单的说就是行列转换。假设一个销售表中存放着产品号,产品折扣,产品价格三个列,每一种产品号可能有多种折扣,每一种折扣只对应一个产品价格。下面贴出建表语句和插入数据语句。

create table SalesOrderDetail(
ProductID int unique,
UnitPriceDiscount float,
ProductPrice float
)
insert into SalesOrderDetail values
(711,.00,12),
(711,.00,13),
(711,.02,17),
(711,.02,16),
(711,.05,19),
(711,.05,20),
(711,.10,21),
(711,.10,22),
(711,.15,23),
(711,.15,24),
(747,.00,41),
(747,.00,42),
(747,.02,45),
(747,.02,46),
(776,.20,50),
(776,.20,49),
(776,.35,52),
(776,.35,53)

首先来看一条查询语句

select ProductID,UnitPriceDiscount,SUM(ProductPrice) as SumPrice
from SalesOrderDetail
group by ProductID,UnitPriceDiscount
order by ProductID,UnitPriceDiscount

这条语句查询每一种产品针对每一种折扣的价钱总和,查询结果如下图1

图1

从图中我们可以看出771号产品有4种折扣,747号产品有2种折扣,776号产品有2种折扣。现在如果我们想知道每一种产品折扣,每一种产品的销售总价是多少,如下图2

图2

如图对于折扣0,产品711的总价是25,对以折扣0.02,产品711的总价是33等等不再列举。原来的行是产品号,现在产品号变成了列,原来的折扣变成了现在的第一列。这就是数据透视的效果。下面我们开看看是这个效果是如何用语句实现的。

select * from
(select sod.ProductPrice,sod.ProductID,sod.UnitPriceDiscount from SalesOrderDetail sod) so
pivot
(
sum(so.ProductPrice) for so.ProductID in([711],[747],[776])
) as pt
order by UnitPriceDiscount

首选创建子查询(select sod.ProductPrice,sod.ProductID,sod.UnitPriceDiscount from SalesOrderDetail sod) so ,透视运算符要使用这个子查询中的数据进行聚合运算,此外输出显示也要用到子查询中的列。代码生成一个别名为so的表值表达式。在这个表中使用pivot 在特定的列上进行聚合,这里是对so.ProductPrice进行聚合,聚合针对so.ProductID进行。在这个例子中对三种产品的中的每一种创 建一个列。这个相当于group by,从so表达式中进行数据筛选。不过这里没有选出ProductPrice,仅仅生成每行三个列,每一种产品为一个列的结果集。因此带有povit的 表值表达式生成一个临时的结果集,将这个结果集命名为pt,使用这个结果集生成我们需要的输出。如果想要得到一个更加合适的列名可以修改筛选条件。如下:

select pt.UnitPriceDiscount,[711] as Product711,[747] as Product747,[776] as Product747 from
(select sod.ProductPrice,sod.ProductID,sod.UnitPriceDiscount from SalesOrderDetail sod) so
pivot
(
sum(so.ProductPrice) for so.ProductID in([711],[747],[776])
) as pt
order by UnitPriceDiscount

输出的结果如下图3

图3

逆透视

这次我们首先看语句和查询结果再分析,语句如下:


select ProductID,UnitPriceDiscount,ProductPrice
from
(select UnitPriceDiscount,Product711,Product747,Product776 from #Temp1) as up1
unpivot(ProductPrice for ProductID in(Product711,Product747,Product776)) as up2
order by ProductID

查询结果如下图4:



图4



首先我们来看看逆透视得到了一个什么样的结果。对于每一种产品的每一种折扣查询得 到他们的合计售价,这个和上面图1中的结果是一样的,是的,它和透视之前的结果是相同的。逆透视和透视并不是完全相反。Pivot会执行聚合,把可能存在 的多个行合并输出得到一行。由于已经进行了合并,unpivot无法重新生成原始的表值表达式,unpivot输入中的null值将在输出中消失,尽管在 pivot操作之前输入中可能存在原始的null值。如图5是他们的比较。在图中我们可以看到NULL值下面一个图中没有NULL值,刚好有9行。下图把 他们放在一起比较。



图5

下 面我们来剖析一下上面的语句到底做了些什么。首先是一个表值函数(select UnitPriceDiscount,Product711,Product747,Product776 from #Temp1) as up1,这个表值函数从透视结果,也就是临时表中,然后针对每一个产品号进行逆透视:unpivot(ProductPrice for ProductID in(Product711,Product747,Product776)) as up2,然后从逆透视结果中选择ProductID ,ProductPrice,从表值函数中选择UnitPriceDiscount。

延伸阅读

一个例子还不足以让我们理解这个语句,下面来看看TechNet中的例子。

SELECT DaysToManufacture, AVG(StandardCost) AS AverageCost FROM Production.Product

GROUP BY DaysToManufacture;

这个语句查出Product表中的制造时间和平均成本,得到如下的结果

图6

如图可以看到没有制造时间为3天的产品,这里留下一个伏笔,在透视之后会出现一个NULL值。下面使用透视语句对它进行行列转换,就是使用0,1,2,3来作为列,使用具体的制造成本作为行数据。语句如下

select
'AverageCost' as Cost_Sorted_By_Production_Days,
[0],[1],[3],[4]
from
(select DaysToManufacture,StandardCost from Production.Product) as SourceTable
pivot
(avg(StandardCost) for DaysToManufacture in ([0],[1],[3],[4])) as PivotTable

依旧,首先用一个表值表达式把要透视的列和透视的项选择出来,然后使用透视语句针对每一个项计算平均成本,最后从这个透视结果中选择出结果。
结果如下图7,我们可以看到制造时间为3天的产品没有一个对应的平均成本。

图7

下面这个例子稍微复杂一点。

SELECT VendorID,count(PurchaseOrderID) as PurchaseCunt
FROM Purchasing.PurchaseOrderHeader group by VendorID

这条语句查询得到每个供应商和他对应的交易号的个数,也就是每个供应商成交的交易次数。如图8列举出部分结果

图8

从图中我们可以看到供应商1共成交51比交易,供应商2共成交51笔交易。如果我们想查出这些交易分别是和那些雇员成交的应该怎么写呢?首先我们来看看表中全部的雇员情况。

select distinct(EmployeeID) from Purchasing.PurchaSEOrderHeader

查询结果如图9

图9

如上图我们可以看到共有12个雇员有成交记录。对于这些雇员,如下查询语句

SELECT
VendorID,
[164] AS Emp164,
[198] AS Emp198,
[223] AS Emp223,
[231] AS Emp231,
[233] AS Emp233,
[238] as Emp238,
[241] as Emp241,
[244] as Emp244,
[261] as Emp261,
[264] as Emp264,
[266] as Emp266,
[274] as Emp274
FROM
(SELECT PurchaseOrderID,EmployeeID,VendorID
FROM Purchasing.PurchaseOrderHeader) p
PIVOT
(
COUNT (PurchaseOrderID)
FOR EmployeeID IN
( [164], [198], [223], [231],[233],[238],[241],[244],[261],[264],[266],[274])
) AS pvt
ORDER BY pvt.VendorID;

查询结果如下图10

图10

可以 简单地计算一下1+4+3+5+4+4+4+5+5+4+5+6+2刚好等于51,分开来看就是1号供应商分别和164号雇员成交4比记录,和198号雇员成交3比记录等等。