[转载]解决方法:An error occurred on the server when processing the URL. Please contact the system administrato-杰诚网络工作室

mikel阅读(22238)

[转载]解决方法:An error occurred on the server when processing the URL. Please contact the system administrato-杰诚网络工作室.

解决方法:An error occurred on the server when processing the URL. Please contact the system administrator

   在WINDOWS7或SERVER2008上安装了IIS7.5,调试ASP程序时出现以下错误:

 

   An error occurred on the server when processing the URL. Please contact the system administrator

 

   解决方法如下:

 

   设置方法一:

 

   以管理员身份运行CMD,将目录定位到%windir%\system32\inetsrv\,然后执行appcmd set config -section:asp -scriptErrorSentToBrowser:true。

   %windir%\system32\inetsrv\appcmd set config -section:asp -scriptErrorSentToBrowser:true

 

   设置方法二:

 

   打开IIS7的asp设置,展开“调试属性”选项,“将错误发送到浏览器”这项默认的是False,改为True,然后点右侧的应用!如图所示:

 

解决方法:An error occurred on the server when processing the URL. Please contact the system administrator - 榕river - 很长的春天夏天

 

   通过以上设置后,再从浏览时打开出错ASP页面时就能看到页面出错的详细信息,方使调试。如果是公开的Web服务器建议不要打开此选项,以防出错信息被他人利用。

[转载]ASP与sql存储过程 - Eric Zhang - 博客园

mikel阅读(971)

[转载]ASP与sql存储过程 – Eric Zhang – 博客园.

ASP与存储过程(Stored Procedures)的文章不少,但是我怀疑作者们是否真正实践过。我在初学时查阅过大量相关资料,发现其中提供的很多方法实际操作起来并不是那么回 事。对于简单的应用,这些资料也许是有帮助的,但仅限于此,因为它们根本就是千篇一律,互相抄袭,稍微复杂点的应用,就全都语焉不详了。
现在,我基本上通过调用存储过程访问SQL Server,以下的文字虽不敢保证绝对正确,但都是实践的总结,希望对大家能有帮助。

存储过程就是作为可执行对象存放在数据库中的一个或多个SQL命令。
定义总是很抽象。存储过程其实就是能完成一定操作的一 组SQL语句,只不过这组语句是放在数据库中的(这里我们只谈SQL Server)。如果我们通过创建存储过程以及在ASP中调用存储过程,就可以避免将SQL语句同ASP代码混杂在一起。这样做的好处至少有三个:
第一、大大提高效率。存储过程本身的执行速度非常快,而且,调用存储过程可以大大减少同数据库的交互次数。
第二、提高安全性。假如将SQL语句混合在ASP代码中,一旦代码失密,同时也就意味着库结构失密。
第三、有利于SQL语句的重用。

在ASP中,一般通过command对象调用存储过程,根据不同情况,本文也介绍其它调用方法。为了方便说明,根据存储过程的输入输出,作以下简单分类:
1. 只返回单一记录集的存储过程
假设有以下存储过程(本文的目的不在于讲述T-SQL语法,所以存储过程只给出代码,不作说明):

/*SP1*/
    CREATE PROCEDURE dbo.getUserList
as
set nocount on
begin
select * from dbo.[userinfo]
end
go

以上存储过程取得userinfo表中的所有记录,返回一个记录集。通过command对象调用该存储过程的ASP代码如下:

‘**通过Command对象调用存储过程**
DIM MyComm,MyRst
Set MyComm = Server.CreateObject(“ADODB.Command”)
MyComm.ActiveConnection = MyConStr          ‘MyConStr是数据库连接字串
MyComm.CommandText      = “getUserList”     ‘指定存储过程名
MyComm.CommandType      = 4                 ‘表明这是一个存储过程
MyComm.Prepared         = true              ‘要求将SQL命令先行编译
Set MyRst = MyComm.Execute
Set MyComm = Nothing

存储过程取得的记录集赋给MyRst,接下来,可以对MyRst进行操作。
在以上代码中,CommandType属性表明请求的类型,取值及说明如下:
-1   表明CommandText参数的类型无法确定
1    表明CommandText是一般的命令类型
2    表明CommandText参数是一个存在的表名称
4    表明CommandText参数是一个存储过程的名称

还可以通过Connection对象或Recordset对象调用存储过程,方法分别如下:

‘**通过Connection对象调用存储过程**
    DIM MyConn,MyRst
Set MyConn = Server.CreateObject(“ADODB.Connection”)
MyConn.open MyConStr                            ‘MyConStr是数据库连接字串
    Set MyRst  = MyConn.Execute(“getUserList”,0,4) ‘最后一个参断含义同CommandType
Set MyConn = Nothing

‘**通过Recordset对象调用存储过程**
DIM MyRst
Set MyRst = Server.CreateObject(“ADODB.Recordset”)
MyRst.open “getUserList”,MyConStr,0,1,4
‘MyConStr是数据库连接字串,最后一个参断含义与CommandType相同
2. 没有输入输出的存储过程
    请看以下存储过程:

/*SP2*/
    CREATE PROCEDURE dbo.delUserAll
as
set nocount on
begin
delete from dbo.[userinfo]
end
go

该存储过程删去userinfo表中的所有记录,没有任何输入及输出,调用方法与上面讲过的基本相同,只是不用取得记录集:

‘**通过Command对象调用存储过程**
DIM MyComm
Set MyComm = Server.CreateObject(“ADODB.Command”)
MyComm.ActiveConnection = MyConStr          ‘MyConStr是数据库连接字串
MyComm.CommandText      = “delUserAll”      ‘指定存储过程名
    MyComm.CommandType      = 4                 ‘表明这是一个存储过程
    MyComm.Prepared         = true              ‘要求将SQL命令先行编译
MyComm.Execute                              ‘此处不必再取得记录集
    Set MyComm = Nothing

当然也可通过Connection对象或Recordset对象调用此类存储过程,不过建立Recordset对象是为了取得记录集,在没有返回记录集的情况下,还是利用Command对象吧。
3. 有返回值的存储过程
在进行类似SP2的操作时,应充分利用SQL Server强大的事务处理功能,以维护数据的一致性。并且,我们可能需要存储过程返回执行情况,为此,将SP2修改如下:

/*SP3*/
    CREATE PROCEDURE dbo.delUserAll
as
set nocount on
begin
BEGIN TRANSACTION
delete from dbo.[userinfo]
IF @@error=0
begin
COMMIT TRANSACTION
return 1
end
ELSE
begin
ROLLBACK TRANSACTION
return 0
end
return
end
go

以上存储过程,在delete顺利执行时,返回1,否则返回0,并进行回滚操作。为了在ASP中取得返回值,需要利用Parameters集合来声明参数:

‘**调用带有返回值的存储过程并取得返回值**
DIM MyComm,MyPara
Set MyComm = Server.CreateObject(“ADODB.Command”)
MyComm.ActiveConnection = MyConStr         ‘MyConStr是数据库连接字串
    MyComm.CommandText      = “delUserAll”      ‘指定存储过程名
    MyComm.CommandType      = 4                 ‘表明这是一个存储过程
MyComm.Prepared         = true              ‘要求将SQL命令先行编译
   ‘声明返回值
    Set Mypara = MyComm.CreateParameter(“RETURN”,2,4)
MyComm.Parameters.Append MyPara
MyComm.Execute
‘取得返回值
    DIM retValue
retValue = MyComm(0)    ‘或retValue = MyComm.Parameters(0)
Set MyComm = Nothing

在MyComm.CreateParameter(“RETURN”,2,4)中,各参数的含义如下:
第一个参数(“RETURE”)为参数名。参数名可以任意设定,但一般应与存储过程中声明的参数名相同。此处是返回值,我习惯上设为”RETURE”;
第二个参数(2),表明该参数的数据类型,具体的类型代码请参阅ADO参考,以下给出常用的类型代码:
adBigInt: 20 ;
adBinary : 128 ;
adBoolean: 11 ;
adChar: 129 ;
adDBTimeStamp: 135 ;
adEmpty: 0 ;
adInteger: 3 ;
adSmallInt: 2 ;
adTinyInt: 16 ;
adVarChar: 200 ;
对于返回值,只能取整形,且-1到-99为保留值;
第三个参数(4),表明参数的性质,此处4表明这是一个返回值。此参数取值的说明如下:
0 : 类型无法确定; 1: 输入参数;2: 输入参数;3:输入或输出参数;4: 返回值

以上给出的ASP代码,应该说是完整的代码,也即最复杂的代码,其实

Set Mypara = MyComm.CreateParameter(“RETURN”,2,4)
MyComm.Parameters.Append MyPara

可以简化为

MyComm.Parameters.Append MyComm.CreateParameter(“RETURN”,2,4)

甚至还可以继续简化,稍后会做说明。
对于带参数的存储过程,只能使用Command对象调用(也有资料说可通过Connection对象或Recordset对象调用,但我没有试成过)。
4. 有输入参数和输出参数的存储过程
返回值其实是一种特殊的输出参数。在大多数情况下,我们用到的是同时有输入及输出参数的存储过程,比如我们想取得用户信息表中,某ID用户的用户名,这时候,有一个输入参数—-用户ID,和一个输出参数—-用户名。实现这一功能的存储过程如下:

/*SP4*/
    CREATE PROCEDURE dbo.getUserName
@UserID int,
@UserName varchar(40) output
as
set nocount on
begin
if @UserID is null return
select @UserName=username
from dbo.[userinfo]
where userid=@UserID
return
end
go

调用该存储过程的ASP代码如下:

‘**调用带有输入输出参数的存储过程**
    DIM MyComm,UserID,UserName
UserID = 1
Set MyComm = Server.CreateObject(“ADODB.Command”)
MyComm.ActiveConnection = MyConStr          ‘MyConStr是数据库连接字串
    MyComm.CommandText      = “getUserName”     ‘指定存储过程名
    MyComm.CommandType      = 4                 ‘表明这是一个存储过程
MyComm.Prepared         = true             ‘要求将SQL命令先行编译
    ‘声明参数
    MyComm.Parameters.append MyComm.CreateParameter(“@UserID”,3,1,4,UserID)
MyComm.Parameters.append MyComm.CreateParameter(“@UserName”,200,2,40)
MyComm.Execute
‘取得出参
UserName = MyComm(1)
Set MyComm = Nothing

在以上代码中,可以看到,与声明返回值不同,声明输入参数时需要5个参数,声明输出参数时需要4个参数。声明输入参数时5个参数分别为:参数名、参数数据类型、参数类型、数据长度、参数值。声明输出参数时,没有最后一个参数:参数值。
需要特别注意的是:在声明参数时,顺序一定要与存储过程中定义的顺序相同,而且各参数的数据类型、长度也要与存储过程中定义的相同
如果存储过程有多个参数,ASP代码会显得繁琐,可以使用with命令简化代码:

‘**调用带有输入输出参数的存储过程(简化代码)**
DIM MyComm,UserID,UserName
UserID = 1
Set MyComm = Server.CreateObject(“ADODB.Command”)
with MyComm
.ActiveConnection = MyConStr         ‘MyConStr是数据库连接字串
.CommandText      = “getUserName”     ‘指定存储过程名
       .CommandType      = 4                 ‘表明这是一个存储过程
.Prepared         = true              ‘要求将SQL命令先行编译
.Parameters.append .CreateParameter(“@UserID”,3,1,4,UserID)
.Parameters.append .CreateParameter(“@UserName”,200,2,40)
.Execute
end with
UserName = MyComm(1)
Set MyComm = Nothing

假如我们要取得ID为1到10,10位用户的用户名,是不是要创建10次Command对象呢?不是的。如果需要多次调用同一存储过程,只需改变输入参数,就会得到不同的输出:

‘**多次调用同一存储过程**
DIM MyComm,UserID,UserName
UserName = “”
Set MyComm = Server.CreateObject(“ADODB.Command”)
for UserID = 1 to 10
with MyComm
.ActiveConnection = MyConStr          ‘MyConStr是数据库连接字串
          .CommandText      = “getUserName”     ‘指定存储过程名
          .CommandType      = 4                 ‘表明这是一个存储过程
.Prepared         = true              ‘要求将SQL命令先行编译
if UserID = 1 then
.Parameters.append .CreateParameter(“@UserID”,3,1,4,UserID)
.Parameters.append .CreateParameter(“@UserName”,200,2,40)
.Execute
else
‘重新给入参赋值(此时参数值不发生变化的入参以及出参不必重新声明)
             .Parameters(“@UserID”) = UserID
.Execute
end if
end with
UserName = UserName + MyComm(1) + “,”   ‘也许你喜欢用数组存储
    next
Set MyComm = Nothing

通过以上代码可以看出:重复调用同一存储过程时,只需为值发生改变的输入参数重新赋值即可,这一方法在有多个输入输出参数,且每次调用时只有一个输入参数的值发生变化时,可以大大减少代码量。

5. 同时具有返回值、输入参数、输出参数的存储过程
    前面说过,在调用存储过程时,声明参数的顺序要与存储过程中定义的顺序相同。还有一点要特别注意:如果存储过程同时具有返回值以及输入、输出参数,返回值要最先声明
为了演示这种情况下的调用方法,我们改善一下上面的例子。还是取得ID为1的用户的用户名,但是有可能该用户不存在(该用户已删除,而userid是自增长的字段)。存储过程根据用户存在与否,返回不同的值。此时,存储过程和ASP代码如下:

/*SP5*/
    CREATE PROCEDURE dbo.getUserName
–为了加深对”顺序”的印象,将以下两参数的定义顺序颠倒一下
       @UserName varchar(40) output,
@UserID int
as
set nocount on
begin
if @UserID is null return
select @UserName=username
from dbo.[userinfo]
where userid=@UserID
if @@rowcount>0
return 1
else
return 0
return
end
go

‘**调用同时具有返回值、输入参数、输出参数的存储过程**
DIM MyComm,UserID,UserName
UserID = 1
Set MyComm = Server.CreateObject(“ADODB.Command”)
with MyComm
.ActiveConnection = MyConStr          ‘MyConStr是数据库连接字串
       .CommandText      = “getUserName”     ‘指定存储过程名
.CommandType      = 4                 ‘表明这是一个存储过程
.Prepared         = true              ‘要求将SQL命令先行编译
       ‘返回值要最先被声明
       .Parameters.Append .CreateParameter(“RETURN”,2,4)
‘以下两参数的声明顺序也做相应颠倒
       .Parameters.append .CreateParameter(“@UserName”,200,2,40)
.Parameters.append .CreateParameter(“@UserID”,3,1,4,UserID)
.Execute
end with
if MyComm(0) = 1 then
UserName = MyComm(1)
else
UserName = “该用户不存在”
end if
Set MyComm = Nothing
6. 同时返回参数和记录集的存储过程
有时候,我们需要存储过程同时返回参数和记录集,比如在利用存储过程分页时,要同时返回记录集以及数据总量等参数。以下给出一个进行分页处理的存储过程:

/*SP6*/
    CREATE PROCEDURE dbo.getUserList
@iPageCount int OUTPUT,   –总页数
       @iPage int,               –当前页号
@iPageSize int            –每页记录数
    as
set nocount on
begin
–创建临时表
create table #t (ID int IDENTITY,   –自增字段
                        userid int,
username varchar(40))
–向临时表中写入数据
       insert into #t
select userid,username from dbo.[UserInfo]
order by userid

–取得记录总数
declare @iRecordCount int
set @iRecordCount = @@rowcount

–确定总页数
IF @iRecordCount%@iPageSize=0
SET @iPageCount=CEILING(@iRecordCount/@iPageSize)
ELSE
SET @iPageCount=CEILING(@iRecordCount/@iPageSize)+1

–若请求的页号大于总页数,则显示最后一页
IF @iPage > @iPageCount
SELECT @iPage = @iPageCount

–确定当前页的始末记录
       DECLARE @iStart int    –start record
DECLARE @iEnd int      –end record
SELECT @iStart = (@iPage – 1) * @iPageSize
SELECT @iEnd = @iStart + @iPageSize + 1

–取当前页记录
select * from #t where ID>@iStart and ID<@iEnd

–删除临时表
DROP TABLE #t

–返回记录总数
return @iRecordCount
end
go

在上面的存储过程中,输入当前页号及每页记录数,返回当前页的记录集,总页数及记录总数。为了更具典型性,将记录总数以返回值的形式返回。以下是调用该存储过程的ASP代码(具体的分页操作略去):

‘**调用分页存储过程**
DIM pagenow,pagesize,pagecount,recordcount
DIM MyComm,MyRst
pagenow = Request(“pn”)
‘自定义函数用于验证自然数
if CheckNar(pagenow) = false then pagenow = 1
pagesize = 20
Set MyComm = Server.CreateObject(“ADODB.Command”)
with MyComm
.ActiveConnection = MyConStr         ‘MyConStr是数据库连接字串
.CommandText      = “getUserList”     ‘指定存储过程名
.CommandType      = 4                 ‘表明这是一个存储过程
.Prepared         = true             ‘要求将SQL命令先行编译
‘返回值(记录总量)
.Parameters.Append .CreateParameter(“RETURN”,2,4)
‘出参(总页数)
       .Parameters.Append .CreateParameter(“@iPageCount”,3,2)
‘入参(当前页号)
       .Parameters.append .CreateParameter(“@iPage”,3,1,4,pagenow)
‘入参(每页记录数)
       .Parameters.append .CreateParameter(“@iPageSize”,3,1,4,pagesize)
Set MyRst = .Execute
end with
if MyRst.state = 0 then        ‘未取到数据,MyRst关闭
       recordcount = -1
else
MyRst.close    ‘注意:若要取得参数值,需先关闭记录集对象
recordcount = MyComm(0)
pagecount   = MyComm(1)
if cint(pagenow)>=cint(pagecount) then pagenow=pagecount
end if
Set MyComm = Nothing

‘以下显示记录
    if recordcount = 0 then
Response.Write “无记录”
elseif recordcount > 0 then
MyRst.open
do until MyRst.EOF
……
loop
‘以下显示分页信息
……
else  ‘recordcount=-1
Response.Write “参数错误”
end if

对于以上代码,只有一点需要说明:同时返回记录集和参数时,若要取得参数,需先将记录集关闭,使用记录集时再将其打开
7. 返回多个记录集的存储过程
    本文最先介绍的是返回记录集的存储过程。有时候,需要一个存储过程返回多个记录集,在ASP中,如何同时取得这些记录集呢?为了说明这一问题,在userinfo表中增加两个字段:usertel及usermail,并设定只有登录用户可以查看这两项内容。

/*SP7*/
    CREATE PROCEDURE dbo.getUserInfo
@userid int,
@checklogin bit
as
set nocount on
begin
if @userid is null or @checklogin is null return
select username
from dbo.[usrinfo]
where userid=@userid
–若为登录用户,取usertel及usermail
if @checklogin=1
select usertel,usermail
from dbo.[userinfo]
where userid=@userid
return
end
go

以下是ASP代码:

‘**调用返回多个记录集的存储过程**
DIM checklg,UserID,UserName,UserTel,UserMail
DIM MyComm,MyRst
UserID = 1
‘checklogin()为自定义函数,判断访问者是否登录
    checklg = checklogin()
Set MyComm = Server.CreateObject(“ADODB.Command”)
with MyComm
.ActiveConnection = MyConStr         ‘MyConStr是数据库连接字串
.CommandText      = “getUserInfo”     ‘指定存储过程名
       .CommandType      = 4                 ‘表明这是一个存储过程
.Prepared         = true              ‘要求将SQL命令先行编译
.Parameters.append .CreateParameter(“@userid”,3,1,4,UserID)
.Parameters.append .CreateParameter(“@checklogin”,11,1,1,checklg)
Set MyRst = .Execute
end with
Set MyComm = Nothing

‘从第一个记录集中取值
UserName = MyRst(0)
‘从第二个记录集中取值
if not MyRst is Nothing then
Set MyRst = MyRst.NextRecordset()
UserTel  = MyRst(0)
UserMail = MyRst(1)
end if
Set MyRst = Nothing

以上代码中,利用Recordset对象的NextRecordset方法,取得了存储过程返回的多个记录集
至此,针对ASP调用存储过程的各种情况,本文已做了较为全面的说明。最后说一下在一个ASP程序中,调用多个存储过程的不同方法。
在一个ASP程序中,调用多个存储过程至少有以下三种方法是可行的:
1. 创建多个Command对象

DIM MyComm
Set MyComm = Server.CreateObject(“ADODB.Command”)
‘调用存储过程一
    ……
Set MyComm = Nothing
Set MyComm = Server.CreateObject(“ADODB.Command”)
‘调用存储过程二
……
Set MyComm = Nothing
……

2. 只创建一个Command对象,结束一次调用时,清除其参数

DIM MyComm
Set MyComm = Server.CreateObject(“ADODB.Command”)
‘调用存储过程一
    …..
‘清除参数(假设有三个参数)
    MyComm.Parameters.delete 2
MyComm.Parameters.delete 1
MyComm.Parameters.delete 0
‘调用存储过程二并清除参数
    ……
Set MyComm = Nothing

此时要注意:清除参数的顺序与参数声明的顺序相反,原因嘛,我也不知道。

3. 利用Parameters数据集合的Refresh方法重置Parameter对象

DIM MyComm
Set MyComm = Server.CreateObject(“ADODB.Command”)
‘调用存储过程一
…..
‘重置Parameters数据集合中包含的所有Parameter对象
    MyComm.Parameters.Refresh
‘调用存储过程二
…..
Set MyComm = Nothing

一般认为,重复创建对象是效率较低的一种方法,但是经测试(测试工具为Microsoft Application Center Test),结果出人意料:
方法2 >= 方法1 >> 方法3
    方法2的运行速度大于等于方法1(最多可高4%左右),这两种方法的运行速度远大于方法3(最多竟高达130%),所以建议在参数多时,采用方法1,在参数较少时,采用方法2。

花了一天的时间,终于把我对于在ASP中调用存储过程的一些粗浅的经验形成了文字。这其中,有些是我只知其果而不明其因的,有些可能是错误的,但是,这些都是经过我亲身实践的。各位看官批判地接受吧。有不同意见,希望一定向我指明,先谢了。

添加评论

单击隐藏此项的评论。

7月27日

MS SQL存储过程编写经验和优化措施

 create   proc   自动保存   as
@字段1   int,
@字段2   char(20),
@条件   char(30)
as
update   table   set   字段1   =   @字段1,字段2   =   @字段2   where   条件   =   @条件
go

调用时
exec   自动保存   @字段1   =   ‘字段1′,@字段2=’字段2’,@条件   =   ‘条件’

 

 

一、适合读者对象:数据库开发程序员,数据库的数据量很多,涉及到对SP(存储过程)的优化的项目开发人员,对数据库有浓厚兴趣的人。

二、介绍:在数据库的开发过程中,经常会遇到复杂的业务逻辑和对数据库的操作,这个时候就会用SP来封装数据库操作。如果项目的SP较多,书写又没 有一定的规范,将会影响以后的系统维护困难和大SP逻辑的难以理解,另外如果数据库的数据量大或者项目对SP的性能要求很,就会遇到优化的问题,否则速度 有可能很慢,经过亲身经验,一个经过优化过的SP要比一个性能差的SP的效率甚至高几百倍。

三、内容:

1、开发人员如果用到其他库的Table或View,务必在当前库中建立View来实现跨库操作,最好不要直接使用“databse.dbo.table_name”,因为sp_depends不能显示出该SP所使用的跨库table或view,不方便校验。

2、开发人员在提交SP前,必须已经使用set showplan on分析过查询计划,做过自身的查询优化检查。

3、高程序运行效率,优化应

3.15不是天天有

mikel阅读(989)

昨天3.15曝光了一大批,整理了互联网相关的曝光,其中免费WiFi成了安全隐患,这倒是和我前几天说到的优酷路由宝有些相关,3.15曝光得是公众场合的WiFi的安全隐患,背后却是隐藏着移动互联网重大的安全隐患:重要信息不加密!

可以说3.15不是天天有,我们的安全意识还是要天天提高为好,要不然自己的各种信息被盗后就只能躲在厕所哭了!特别是现在手机使用频率越来越高,深度绑定支付,手机不再是个通讯工具得时代了,我们很多时候丢了手机,就跟丢了半个世界差不多。更不要说什么支付宝、微信支付之类的了。

说道网络数据安全从过年的微信发红包乌云上就被爆出存在重大安全隐患,当我们的钱在亲朋好友的手机间流转得时候,又有多少信息被截获数据包并分析就不得而知了,所以说,尽管绑定银行卡比较方便网上支付,还是尽快解绑,或者将大额资金的银行卡解除网络支付业务,然后绑定小额银行卡比较稳妥,即使被盗也不至于损失惨重。

说道安全意识,不可避免的就是密码安全这一话题,很多人只用简单的12345就当成密码,难道你不知道现在已经有了共享的密码库吗?尽管号称不可破解的MD5加密很牛叉,不过架不住大家共享的MD5的密码对照库的数据量庞大,你的密码就那么赤裸裸的爆料给了全世界人,你还觉得用简单密码有MD5加密是安全的吗?!建议是大小写字母+数字+再加特殊字符,并且密码长度越长破解起来越麻烦,还有不要一百年不换密码,过段时间就换换吧!安全第一!

至于个人信息,从运营商被爆买卖个人信息看,网上尽量不要贸然提供给对方身份证等重要信息,以免被李鬼冒名顶替。

资金沉淀的力量有多恐怖?

mikel阅读(1545)

昨天群里说道资金沉淀盈利模式,很多时候钱都是积少成多,说道淘宝的开店保证金1000元先给淘宝了,淘宝有好几千万的店铺,那就是好几亿的现金给了淘宝,就说这一天的利息就能赚多少?这就是明显的资金沉淀案例,然后就是过年的微信红包,一晚上就好几亿的现金流入微信支付的账户,一天的利息也是醉了!这些就是明面儿上的可见的资金沉淀案例。

钱不多但是提前支付的人多了成了规模就是一个可怕的利润值了。再说说广告联盟的资金流,百度竞价,是先充值然后再投放关键词竞价广告,这一充值就会产生现金流,净流入的资金就上亿,然后这些资金全部在百度手里面产生利息并进行其他投资,比如基金、投资之类的,这就是为什么各家纷纷推出xx宝之类的理财产品了,完全是因为现金太多了,花不完,然后引入金融产品提高资金的转化率和盈利率,让资金流转起来生钱,同时也是一种吸引资金流入的好渠道,最近小米终于也耐不住了推出小米钱宝,一样是这个道理。

有人会说这些都是BAT那些老大的企业才能运作的,我们这些小人物没法儿运作,那就结合微商谈谈我们这些小人物怎么进行资金沉淀,首先微商为什么都搞加盟模式,那就是将产品变现,回笼资金,然后转嫁风险给加盟商,不管怎么样现金才是王道,然后利用这些现金再生产或者投资钱生钱,最终产品卖得好坏已经不重要了,只要招更多人加盟收取加盟费或者预付货款就ok!不要说2014微商赚的钱很多,究竟多少是靠卖面膜赚的,这些面膜消费力多强不说,就说这加盟费和货款就能够让顶级的微商赚足油水儿了。

其实很多时候我们支付得时候就可以算算商家的现金流的速度多快,比如存话费赠手机,存一年的话费,得一个手机,然后运营商按月返话费给你,其实这一年的现金已经在他的账户上了,如果全国有几个亿的人都选择了这项服务,那么他的账号上的钱的利息足以支付你的月返话费和赠送给你的手机,他还有得赚,积少成多的可怕就在这里。

又来了个更狠的!我也是醉了!

mikel阅读(982)

大早起要不要这么刺激我啊!~~来了个更狠的!我也是醉了!究竟是怎么回事儿,先看图吧!

这是提现图:

qd6

这是2014年就开始注册并签到的

qd

这是截止到2014年底的签到图

qd3

这是至今到2015年的签到提现5元的截图

qd2

果断支付!

4BE(G[[CBJLZ4{AKKEYARNJ

好了看到这,不得不让人佩服这位兄弟的坚持和毅力,坚持签到两年!!!不是一般人能够做到的!如果再有人在那嚷嚷网络赚不到钱的话,我会对他说是你自己不行,不要说网络不赚钱!你有这么过人的毅力,签到都能赚钱!就是每天访问网站登陆就会赚到签到金币了!

也有很多人QQ问我有没有赚钱快的方法,我告诉他了手机可以赚钱、做任务赚钱打码赚钱玩游戏赚钱等等项目,他都不理会儿,然后连一句谢谢都没有,过几天又问我什么能赚钱,我问他以前跟他说的都做了吗,他说做了,赚得太慢,然后就不做了。从此这种人我直接拉黑,没有时间和他废话!关键还是在人,人不行真得是再好的项目也不会赚钱,因为他永远不会找自己的原因,而是将经历花费在找借口和理由,还有埋怨别人上了!

[转载]基础总结篇之五:BroadcastReceiver应用详解 - scott's blog - 博客频道 - CSDN.NET

mikel阅读(909)

[转载]基础总结篇之五:BroadcastReceiver应用详解 – scott’s blog – 博客频道 – CSDN.NET.

問渠那得清如許?為有源頭活水來。南宋.朱熹《觀書有感》

据说程序员是最爱学习的群体,IT男都知道,这个行业日新月异,必须不断地学习新知识,不断地为自己注入新鲜的血液,才能使自己跟上技术的步伐。

今天我们来讲一下Android中BroadcastReceiver的相关知识。

BroadcastReceiver也就是“广播接收者”的意思,顾名思义,它就是用来接收来自系统和应用中的广播。

Android系统中,广播体现在方方面面,例如当开机完成后系统会产生一条广播,接收到这条广播就能实现开机启动服务的功能;当网络状态改变时 系统会产生一条广播,接收到这条广播就能及时地做出提示和保存数据等操作;当电池电量改变时,系统会产生一条广播,接收到这条广播就能在电量低时告知用户 及时保存进度,等等。

Android中的广播机制设计的非常出色,很多事情原本需要开发者亲自操作的,现在只需等待广播告知自己就可以了,大大减少了开发的工作量和开发 周期。而作为应用开发者,就需要数练掌握Android系统提供的一个开发利器,那就是BroadcastReceiver。下面我们就对 BroadcastReceiver逐一地分析和演练,了解和掌握它的各种功能和用法。

首先,我们来演示一下创建一个BroadcastReceiver,并让这个BroadcastReceiver能够根据我们的需要来运行。

要创建自己的BroadcastReceiver对象,我们需要继承android.content.BroadcastReceiver,并实现其onReceive方法。下面我们就创建一个名为MyReceiver广播接收者:

  1. package com.scott.receiver;
  2. import android.content.BroadcastReceiver;
  3. import android.content.Context;
  4. import android.content.Intent;
  5. import android.util.Log;
  6. public class MyReceiver extends BroadcastReceiver {
  7.     private static final String TAG = “MyReceiver”;
  8.     @Override
  9.     public void onReceive(Context context, Intent intent) {
  10.         String msg = intent.getStringExtra(“msg”);
  11.         Log.i(TAG, msg);
  12.     }
  13. }

在onReceive方法内,我们可以获取随广播而来的Intent中的数据,这非常重要,就像无线电一样,包含很多有用的信息。

在创建完我们的BroadcastReceiver之后,还不能够使它进入工作状态,我们需要为它注册一个指定的广播地址。没有注册广播地址的 BroadcastReceiver就像一个缺少选台按钮的收音机,虽然功能俱备,但也无法收到电台的信号。下面我们就来介绍一下如何为 BroadcastReceiver注册广播地址。

静态注册

静态注册是在AndroidManifest.xml文件中配置的,我们就来为MyReceiver注册一个广播地址:

  1. <receiver android:name=“.MyReceiver”>
  2.             <intent-filter>
  3.                 <action android:name=“android.intent.action.MY_BROADCAST”/>
  4.                 <category android:name=“android.intent.category.DEFAULT” />
  5.             </intent-filter>
  6.         </receiver>

配置了以上信息之后,只要是android.intent.action.MY_BROADCAST这个地址的广播,MyReceiver都能够接 收的到。注意,这种方式的注册是常驻型的,也就是说当应用关闭后,如果有广播信息传来,MyReceiver也会被系统调用而自动运行。

动态注册

动态注册需要在代码中动态的指定广播地址并注册,通常我们是在Activity或Service注册一个广播,下面我们就来看一下注册的代码:

  1. MyReceiver receiver = new MyReceiver();
  2. IntentFilter filter = new IntentFilter();
  3. filter.addAction(“android.intent.action.MY_BROADCAST”);
  4. registerReceiver(receiver, filter);

注 意,registerReceiver是android.content.ContextWrapper类中的方法,Activity和Service都 继承了ContextWrapper,所以可以直接调用。在实际应用中,我们在Activity或Service中注册了一个 BroadcastReceiver,当这个Activity或Service被销毁时如果没有解除注册,系统会报一个异常,提示我们是否忘记解除注册 了。所以,记得在特定的地方执行解除注册操作:

  1. @Override
  2. protected void onDestroy() {
  3.     super.onDestroy();
  4.     unregisterReceiver(receiver);
  5. }

执行这样行代码就可以解决问题了。注意,这种注册方式与静态注册相反,不是常驻型的,也就是说广播会跟随程序的生命周期。

我们可以根据以上任意一种方法完成注册,当注册完成之后,这个接收者就可以正常工作了。我们可以用以下方式向其发送一条广播:

  1. public void send(View view) {
  2.     Intent intent = new Intent(“android.intent.action.MY_BROADCAST”);
  3.     intent.putExtra(“msg”“hello receiver.”);
  4.     sendBroadcast(intent);
  5. }

注意,sendBroadcast也是android.content.ContextWrapper类中的方法,它可以将一个指定地址和参数信息的Intent对象以广播的形式发送出去。

点击发送按钮,执行send方法,控制台打印如下:

看到这样的打印信息,表明我们的广播已经发出去了,并且被MyReceiver准确无误的接收到了。

上面的例子只是一个接收者来接收广播,如果有多个接收者都注册了相同的广播地址,又会是什么情况呢,能同时接收到同一条广播吗,相互之间会不会有干扰呢?这就涉及到普通广播和有序广播的概念了。

普通广播(Normal Broadcast)

普通广播对于多个接收者来说是完全异步的,通常每个接收者都无需等待即可以接收到广播,接收者相互之间不会有影响。对于这种广播,接收者无法终止广播,即无法阻止其他接收者的接收动作。

为了验证以上论断,我们新建三个BroadcastReceiver,演示一下这个过程,FirstReceiver、SecondReceiver和ThirdReceiver的代码如下:

  1. package com.scott.receiver;
  2. import android.content.BroadcastReceiver;
  3. import android.content.Context;
  4. import android.content.Intent;
  5. import android.util.Log;
  6. public class FirstReceiver extends BroadcastReceiver {
  7.     private static final String TAG = “NormalBroadcast”;
  8.     @Override
  9.     public void onReceive(Context context, Intent intent) {
  10.         String msg = intent.getStringExtra(“msg”);
  11.         Log.i(TAG, “FirstReceiver: “ + msg);
  12.     }
  13. }
  1. public class SecondReceiver extends BroadcastReceiver {
  2.     private static final String TAG = “NormalBroadcast”;
  3.     @Override
  4.     public void onReceive(Context context, Intent intent) {
  5.         String msg = intent.getStringExtra(“msg”);
  6.         Log.i(TAG, “SecondReceiver: “ + msg);
  7.     }
  8. }
  1. public class ThirdReceiver extends BroadcastReceiver {
  2.     private static final String TAG = “NormalBroadcast”;
  3.     @Override
  4.     public void onReceive(Context context, Intent intent) {
  5.         String msg = intent.getStringExtra(“msg”);
  6.         Log.i(TAG, “ThirdReceiver: “ + msg);
  7.     }
  8. }

然后再次点击发送按钮,发送一条广播,控制台打印如下:

看来这三个接收者都接收到这条广播了,我们稍微修改一下三个接收者,在onReceive方法的最后一行添加以下代码,试图终止广播:

  1. abortBroadcast();

再次点击发送按钮,我们会发现,控制台中三个接收者仍然都打印了自己的日志,表明接收者并不能终止广播。

有序广播(Ordered Broadcast)

有序广播比较特殊,它每次只发送到优先级较高的接收者那里,然后由优先级高的接受者再传播到优先级低的接收者那里,优先级高的接收者有能力终止这个广播。

为了演示有序广播的流程,我们修改一下上面三个接收者的代码,如下:

  1. package com.scott.receiver;
  2. import android.content.BroadcastReceiver;
  3. import android.content.Context;
  4. import android.content.Intent;
  5. import android.os.Bundle;
  6. import android.util.Log;
  7. public class FirstReceiver extends BroadcastReceiver {
  8.     private static final String TAG = “OrderedBroadcast”;
  9.     @Override
  10.     public void onReceive(Context context, Intent intent) {
  11.         String msg = intent.getStringExtra(“msg”);
  12.         Log.i(TAG, “FirstReceiver: “ + msg);
  13.         Bundle bundle = new Bundle();
  14.         bundle.putString(“msg”, msg + “@FirstReceiver”);
  15.         setResultExtras(bundle);
  16.     }
  17. }
  1. public class SecondReceiver extends BroadcastReceiver {
  2.     private static final String TAG = “OrderedBroadcast”;
  3.     @Override
  4.     public void onReceive(Context context, Intent intent) {
  5.         String msg = getResultExtras(true).getString(“msg”);
  6.         Log.i(TAG, “SecondReceiver: “ + msg);
  7.         Bundle bundle = new Bundle();
  8.         bundle.putString(“msg”, msg + “@SecondReceiver”);
  9.         setResultExtras(bundle);
  10.     }
  11. }
  1. public class ThirdReceiver extends BroadcastReceiver {
  2.     private static final String TAG = “OrderedBroadcast”;
  3.     @Override
  4.     public void onReceive(Context context, Intent intent) {
  5.         String msg = getResultExtras(true).getString(“msg”);
  6.         Log.i(TAG, “ThirdReceiver: “ + msg);
  7.     }
  8. }

我 们注意到,在FirstReceiver和SecondReceiver中最后都使用了setResultExtras方法将一个Bundle对象设置为 结果集对象,传递到下一个接收者那里,这样以来,优先级低的接收者可以用getResultExtras获取到最新的经过处理的信息集合。

代码改完之后,我们需要为三个接收者注册广播地址,我们修改一下AndroidMainfest.xml文件:

  1. <receiver android:name=“.FirstReceiver”>
  2.     <intent-filter android:priority=“1000”>
  3.         <action android:name=“android.intent.action.MY_BROADCAST”/>
  4.         <category android:name=“android.intent.category.DEFAULT” />
  5.     </intent-filter>
  6. </receiver>
  7. <receiver android:name=“.SecondReceiver”>
  8.     <intent-filter android:priority=“999”>
  9.         <action android:name=“android.intent.action.MY_BROADCAST”/>
  10.         <category android:name=“android.intent.category.DEFAULT” />
  11.     </intent-filter>
  12. </receiver>
  13. <receiver android:name=“.ThirdReceiver”>
  14.     <intent-filter android:priority=“998”>
  15.         <action android:name=“android.intent.action.MY_BROADCAST”/>
  16.         <category android:name=“android.intent.category.DEFAULT” />
  17.     </intent-filter>
  18. </receiver>

我们看到,现在这三个接收者的<intent-filter>多了一个android:priority属性,并且依次减小。这个属性的范围在-1000到1000,数值越大,优先级越高。

现在,我们需要修改一下发送广播的代码,如下:

  1. public void send(View view) {
  2.     Intent intent = new Intent(“android.intent.action.MY_BROADCAST”);
  3.     intent.putExtra(“msg”“hello receiver.”);
  4.     sendOrderedBroadcast(intent, “scott.permission.MY_BROADCAST_PERMISSION”);
  5. }

注 意,使用sendOrderedBroadcast方法发送有序广播时,需要一个权限参数,如果为null则表示不要求接收者声明指定的权限,如果不为 null,则表示接收者若要接收此广播,需声明指定权限。这样做是从安全角度考虑的,例如系统的短信就是有序广播的形式,一个应用可能是具有拦截垃圾短信 的功能,当短信到来时它可以先接受到短信广播,必要时终止广播传递,这样的软件就必须声明接收短信的权限。

所以我们在AndroidMainfest.xml中定义一个权限:

  1. <permission android:protectionLevel=“normal”
  2.             android:name=“scott.permission.MY_BROADCAST_PERMISSION” />

然后声明使用了此权限:

  1. <uses-permission android:name=“scott.permission.MY_BROADCAST_PERMISSION” />

关于这部分如果有不明白的地方可以参考我之前写过的一篇文章:Android声明和使用权限

然后我们点击发送按钮发送一条广播,控制台打印如下:

我们看到接收是按照顺序的,第一个和第二个都在结果集中加入了自己的标记,并且向优先级低的接收者传递下去。

既然是顺序传递,试着终止这种传递,看一看效果如何,我们修改FirstReceiver的代码,在onReceive的最后一行添加以下代码:

  1. abortBroadcast();

然后再次运行程序,控制台打印如下:

此次,只有第一个接收者执行了,其它两个都没能执行,因为广播被第一个接收者终止了。

上面就是BroadcastReceiver的介绍,下面我将会举几个常见的例子加深一下大家对广播的理解和应用:

1.开机启动服务

我们经常会有这样的应用场合,比如消息推送服务,需要实现开机启动的功能。要实现这个功能,我们就可以订阅系统“启动完成”这条广播,接收到这条广 播后我们就可以启动自己的服务了。我们来看一下BootCompleteReceiver和MsgPushService的具体实现:

  1. package com.scott.receiver;
  2. import android.content.BroadcastReceiver;
  3. import android.content.Context;
  4. import android.content.Intent;
  5. import android.util.Log;
  6. public class BootCompleteReceiver extends BroadcastReceiver {
  7.     private static final String TAG = “BootCompleteReceiver”;
  8.     @Override
  9.     public void onReceive(Context context, Intent intent) {
  10.         Intent service = new Intent(context, MsgPushService.class);
  11.         context.startService(service);
  12.         Log.i(TAG, “Boot Complete. Starting MsgPushService…”);
  13.     }
  14. }
  1. package com.scott.receiver;
  2. import android.app.Service;
  3. import android.content.Intent;
  4. import android.os.IBinder;
  5. import android.util.Log;
  6. public class MsgPushService extends Service {
  7.     private static final String TAG = “MsgPushService”;
  8.     @Override
  9.     public void onCreate() {
  10.         super.onCreate();
  11.         Log.i(TAG, “onCreate called.”);
  12.     }
  13.     @Override
  14.     public int onStartCommand(Intent intent, int flags, int startId) {
  15.         Log.i(TAG, “onStartCommand called.”);
  16.         return super.onStartCommand(intent, flags, startId);
  17.     }
  18.     @Override
  19.     public IBinder onBind(Intent arg0) {
  20.         return null;
  21.     }
  22. }

然后我们需要在AndroidManifest.xml中配置相关信息:

  1. <!– 开机广播接受者 –>
  2. <receiver android:name=“.BootCompleteReceiver”>
  3.     <intent-filter>
  4.         <!– 注册开机广播地址–>
  5.         <action android:name=“android.intent.action.BOOT_COMPLETED”/>
  6.         <category android:name=“android.intent.category.DEFAULT” />
  7.     </intent-filter>
  8. </receiver>
  9. <!– 消息推送服务 –>
  10. <service android:name=“.MsgPushService”/>

我们看到BootCompleteReceiver注册了“android.intent.action.BOOT_COMPLETED”这个开机广播地址,从安全角度考虑,系统要求必须声明接收开机启动广播的权限,于是我们再声明使用下面的权限:

  1. <uses-permission android:name=“android.permission.RECEIVE_BOOT_COMPLETED” />

经过上面的几个步骤之后,我们就完成了开机启动的功能,将应用运行在模拟器上,然后重启模拟器,控制台打印如下:

如果我们查看已运行的服务就会发现,MsgPushService已经运行起来了。

2.网络状态变化

在某些场合,比如用户浏览网络信息时,网络突然断开,我们要及时地提醒用户网络已断开。要实现这个功能,我们可以接收网络状态改变这样一条广播,当 由连接状态变为断开状态时,系统就会发送一条广播,我们接收到之后,再通过网络的状态做出相应的操作。下面就来实现一下这个功能:

  1. package com.scott.receiver;
  2. import android.content.BroadcastReceiver;
  3. import android.content.Context;
  4. import android.content.Intent;
  5. import android.net.ConnectivityManager;
  6. import android.net.NetworkInfo;
  7. import android.util.Log;
  8. import android.widget.Toast;
  9. public class NetworkStateReceiver extends BroadcastReceiver {
  10.     private static final String TAG = “NetworkStateReceiver”;
  11.     @Override
  12.     public void onReceive(Context context, Intent intent) {
  13.         Log.i(TAG, “network state changed.”);
  14.         if (!isNetworkAvailable(context)) {
  15.             Toast.makeText(context, “network disconnected!”0).show();
  16.         }
  17.     }
  18.     /**
  19.      * 网络是否可用
  20.      * 
  21.      * @param context
  22.      * @return
  23.      */
  24.     public static boolean isNetworkAvailable(Context context) {
  25.         ConnectivityManager mgr = (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE);
  26.         NetworkInfo[] info = mgr.getAllNetworkInfo();
  27.         if (info != null) {
  28.             for (int i = 0; i < info.length; i++) {
  29.                 if (info[i].getState() == NetworkInfo.State.CONNECTED) {
  30.                     return true;
  31.                 }
  32.             }
  33.         }
  34.         return false;
  35.     }
  36. }

再注册一下这个接收者的信息:

  1. <receiver android:name=“.NetworkStateReceiver”>
  2.     <intent-filter>
  3.         <action android:name=“android.net.conn.CONNECTIVITY_CHANGE”/>
  4.         <category android:name=“android.intent.category.DEFAULT” />
  5.     </intent-filter>
  6. </receiver>

因为在isNetworkAvailable方法中我们使用到了网络状态相关的API,所以需要声明相关的权限才行,下面就是对应的权限声明:

  1. <uses-permission android:name=“android.permission.ACCESS_NETWORK_STATE”/>

我们可以测试一下,比如关闭WiFi,看看有什么效果。

3.电量变化

如果我们阅读软件,可能是全屏阅读,这个时候用户就看不到剩余的电量,我们就可以为他们提供电量的信息。要想做到这一点,我们需要接收一条电量变化的广播,然后获取百分比信息,这听上去挺简单的,我们就来实现以下:

  1. package com.scott.receiver;
  2. import android.content.BroadcastReceiver;
  3. import android.content.Context;
  4. import android.content.Intent;
  5. import android.os.BatteryManager;
  6. import android.util.Log;
  7. public class BatteryChangedReceiver extends BroadcastReceiver {
  8.     private static final String TAG = “BatteryChangedReceiver”;
  9.     @Override
  10.     public void onReceive(Context context, Intent intent) {
  11.         int currLevel = intent.getIntExtra(BatteryManager.EXTRA_LEVEL, 0);  //当前电量
  12.         int total = intent.getIntExtra(BatteryManager.EXTRA_SCALE, 1);      //总电量
  13.         int percent = currLevel * 100 / total;
  14.         Log.i(TAG, “battery: “ + percent + “%”);
  15.     }
  16. }

然后再注册一下广播接地址信息就可以了:

  1. <receiver android:name=“.BatteryChangedReceiver”>
  2.     <intent-filter>
  3.         <action android:name=“android.intent.action.BATTERY_CHANGED”/>
  4.         <category android:name=“android.intent.category.DEFAULT” />
  5.     </intent-filter>
  6. </receiver>

当然,有些时候我们是要立即获取电量的,而不是等电量变化的广播,比如当阅读软件打开时立即显示出电池电量。我们可以按以下方式获取:

  1. Intent batteryIntent = getApplicationContext().registerReceiver(null,
  2.         new IntentFilter(Intent.ACTION_BATTERY_CHANGED));
  3. int currLevel = batteryIntent.getIntExtra(BatteryManager.EXTRA_LEVEL, 0);
  4. int total = batteryIntent.getIntExtra(BatteryManager.EXTRA_SCALE, 1);
  5. int percent = currLevel * 100 / total;
  6. Log.i(“battery”“battery: “ + percent + “%”);

[转载]如何避免开发一款失败的产品_IT新闻_博客园

mikel阅读(1067)

[转载]如何避免开发一款失败的产品_IT新闻_博客园.

  英文原文:How to avoid building products that fail

本文编译自 Medium, 作者 Rian van der Merwe 2005 年到 2009 年间曾就职于 eBay,现在在 Jive Software 担任产品设计主管。在这篇文章中,作者提出打造一款成功的产品,必须在产品开发的始终关注着“用户需求”、“商业需求”以及“技术需求”。

“如果我问人们他们想要什么,他们会说想要一批跑得更快的马。”这句话据说是福特汽车创始人亨利·福特的名言。人们经常引用它来支持那些未经用户测试的所谓的创新。这句话其实价值不大,因为福特可能压根没说过这句话,而且按照这种思维方式经营公司很可能会在市场上惨败。

我们应该认识到,把一个没有经过验证和测试的 idea 拿去执行是一件非常危险的事情。我们在理解某个问题之前,不应该直接跳到解决方案部分。而这也将是本文所要讨论的。

开发一款产品出发点永远是需求。我们不能想当然的认为某个产品会很好,只有真正满足用户的需求并在商业上获得回报的产品才能取得成功。我认为开发产品的过程应该在以下几部分给予更多投入,我们在本文也将详细讨论这 3 方面需求:

  1. 用户需求。我们必须很好地理解市场,理解公司的消费者(包括现有的和潜在的),了解他们的行为和态度。我们在产品目标受众研究方面不应留有死角。
  2. 商业需求。“用户至上”的口号经常掩盖了一个事实,那就是产品存在的意义是为了赚钱。但商业方面的需求也不能成为糟糕设计的借口。
  3. 技术需求。人们常常过于重视更直接的前端和商业需求,而忽视了技术需求。开发人员知道产品的局限,他们知道有哪些问题需要解决,也知道技术方面什么欠缺需要补上。

产品开发中容易犯的最大一个错误就是在完成合理的产品规划前开始执行。所以,我们需要给规划环节足够的重视。首先,我们来谈谈收集用户需求。


用户需求

我们首先要区分清楚两个概念:需求和功能。人们经常错将产品功能等同于用户需求。来看一些家电行业的例子,你就知道为什么我这么说:洗衣机上的 预置模式可能有很多种,但是你常用的是不是只有一两种?用面包机时你需要几种烤面包的方式?这两个例子说明产品的功能并不等同于为用户创造的价值,多并不 意味着好。我们不需要更多的模式来洗衣服,但我们可能需要快洗或者更安静洗衣方式。


当产品设计得过于复杂的时候,我们就得自己想办法解决问题了。(图片来自 Reddit

  Facebook Home 面世后不久,相关评论和使用统计数据就开始出现,John Gruber 说了一句让我印象深刻的话:“它的设计精良,但是没有人想要这个创意。“他的这句话有夸张的成分,但是也说明了如果把功能(首页信息流、朋友充满屏幕、 Chat Heads 功能、app 启动器···)等同于需求(人们为什么会愿意把他们手机的操作系统换成一个 app)的后果。功能和需求之间的差异是非常重要的,有时又很难发现,这时就应该进行用户调研。

收集用户需求的调研主要依靠观察和分析,而不只是收集一堆预先设置好问题的答案。但是探讨优化产品的各种方法之前,我们需要定义一些基本研究内容。

首先,我们需要区分定量研究和定性研究。在定量研究中,数据往往不直接收集自受访者,而是通过调查问卷或网页分析收集。定量分析能帮助你理解发 生了什么情况,或者在多大程度上出现了这种情况。而定性分析数据直接从参与者处收集,通常以访谈或者可用性测试的方式进行。定性分析可以帮助你理解某些特 定的行为会怎样出现,以及为什么会出现。

其次,我们还需要区分市场调研和用户调研。二者都非常重要,但他们的目的不同。市场调研是要了解市场上整体的需求,主要关注品牌价值和市场定位 等问题。态度调查问卷以及焦点小组访谈是市场调研人员通常使用的基本方法,用于搞清楚如何在市场中定位产品。调查问卷和焦点小组访谈在理解市场趋势和需求 的时非常有用,但在产品设计方面用处不大。

另一方面,用户调研的关注点则在于用户如何与你的产品互动,关乎到人们如何使用新技术,以及从他们缺少的,需要的以及感到沮丧的地方我们能了解到什么。在这部分,我们将主要关注用户调研的方法。

那么,基于上述定义,我们来看一些最常用的用户调研方法。大体上分成三类:

1. 探索性调研(Exploratory Research)

当我们的目标是发现用户使用产品最重要(通常是未被满足的)的需求时,探索性调研非常有效的。探索性调研包括情境访谈(也叫做“民族志研究法” 或“实地访问”)、参与式设计会议以及产品概念测试(concept testing)。这么做的目的是发现现有产品在解决用户需求时所出现的不足。新产品或功能的创意常常出自这些会议。

不要搞错,这种方法并不是问人们是不是想要“更快的马”,而是观察人们,发现他们在哪些方面需要比现在做得更好。

举个例子,我们曾对世界各地许多 eBay 卖家做过实地访问。通过走进人们的家中,观察他们如何管理销售,我们发现了一个通过网页分析或问卷调查绝对不可能发现的问题。每个卖家管理店铺的方式都不 同,有些人在显示器周边贴满便利贴,还有些人使用带有复杂公式的 Excel 表格。卖家不得不自己完成一些本该由 eBay 做的事:如何记录销售过程并做出分析得到结论。通过实地走访,我们发现了一些还没有满足的用户需求,并通过多种方式解决了这些问题。而需求是这一切的出发 点。

2. 设计研究(Design Research)

设计研究帮助开发者利用需求分析得出的结论进一步改进产品创意。具体方法包括传统的可用性测试、RITE 测试(rapid iterative testing and evaluation,快速迭代测试与评估),甚至包括眼动记录等定量的方法。这类研究在设计产品,解决用户需求过程中作用十分明显。举个例子,我们可以 先开发一个交互式的原型机,然后把人们带到可用性测试实验室,给他们一些任务让他们在原型机上完成,通过这种方式我们可以在进入代价高昂的开发环节之前发 现一些可用性方面的问题。通过深入的一对一访谈,我们有很多机会深入了解自己是否很好地满足了在探索性调研中发现用户需求。

3. 评估研究(Assessment Research)

评估研究帮助我们验证对产品所做的改变是真正提升了产品,还是只做了无用功。这类研究常常被忽视,但它是产品开发过程中非常重要的一环。我们可 以通过调查问卷和网页分析了解随着时间推进产品的表现如何。这里需要关注的不仅是一些硬指标上的变化,还要看用户态度上的转变。只有将评估研究和设计研究 深入地结合起来,才能更好地理解我们为什么会看到产品发生的变化。比如说,表格分析可以看出人们在哪里放弃填写一份表格。每当我们改进一次表格的可用性, 就需要了解这些改变对表格的完成度有什么影响。没有评估研究,我们就没办法知道产品是否对了方向。


商业需求

互联网行业,我们见过很多充分满足用户需求但没法赚钱、没法持续发展的公司。在过去几年,很多优秀的网络服务关停就是因为缺少收入。比如 Editorially 是一款出色的协同写作和编辑工具,但它的创始人却发现:“即使所有的用户都付费也不够。”

  在 Editorially 之前,照片管理服务 Everpix 也关门大吉了。部分原因就在于他们无力支付云储存费用。虽然 Everpix 平台上有大量付费用户,但仍然入不敷出。创始人后来承认,虽然公司开发出了人们真正喜爱的产品,但是团队在产品上花费得时间过多,没有留出足够的时间去关注公司的发展和产品的推广。

现在很多互联网产品都希望先获得尽可能多的用户,然后再考虑赚钱的事情。但是在我看来,这种并不是做生意的方式。我并不是说一款新产品需要从第一天开始就盈利(当然能做到更好),但是至少你要规划好能够带来稳定收入的业务模式,在做商业计划时明确公司未来的收入来源。

那么,公司应该如何获得收入呢?大多数情况下,我们需要依赖消费者。在“用户需求”的部分,我们讨论过一些调研方法可以帮助你判断用户是否愿意付费,以及愿意支付多少费用。开发产品的过程中,需要联合公司内的业务拓展团队、销售团队、营销团队以及工程团队,做好两方面工作:放弃不良收入,追求优质收入。

放弃不良收入

一位古希腊作家曾说过:“收益总是甜美的,即使它来自与欺骗。”(Profit is sweet, even if it comes from deception.)这句话揭露了我们在金钱面前是多么的脆弱。通过欺骗的手段赚钱有时看起来很诱人,但这种短视行为在长期来看会带来巨大的问题,而且 会让你背负沉重的道德包袱。

在界面设计中,我们把一些欺骗性的技术手段称作“黑暗模式”(Dark Patterns),也就是通过诱导性的界面,让用户做一些正常情况下不会做的事情。在 darkpatterns.org 这个网站上,我们可以看到这样的案例:

  • 会说话的汤姆猫等一些针对儿童的 iOS 应用中会随机弹出一些页面,诱导儿童购买一些内购项目。
  • 登陆 PayPal 时经常会看到全屏广告,只在右上角有一个小小的按钮能关闭广告继续账户操作。
  • Zynga 出品的农场类游戏 FarmVille 在开发时只有一个目标,那就是迫使用户尽可能长时间的照料他们的虚拟土地。
  • Ryanair 把取消购买保险的选项放在一个无关的下拉菜单中,所以很多人根本没有意识到自己买了保险。


Ryanair 网站上如何取消购买保险。(来源:Dark Patterns

  很明显,有一些收入是不道德的,因此也不值得追求。问题在于,这些方法常常是能赚到钱的(至少在短期内)。但是其长期的效应也不容忽视,一旦用 户搞明白发生了什么,他们就会开始抱怨。这些不光彩的手段会直接影响到公司的声誉,同时也会增加客服费用。Ryanair 那样保险销售的阴谋已经成为“黑暗模式”的典型反面教材。

当然,大多数人内心深处并不想通过欺骗的手段挣钱,但是“黑暗模式”可能会潜移默化地侵蚀了我们原本正常的想法,直到彻底改变它们。

对于“黑暗模式”,我们不需要花费太多心思去斗争,只需要提醒自己:小心,不要掉进这个陷阱。每当遇到能增加收入的机会时就问问自己:“如果一 个产品让我这么操作或者让我付费时我会接受吗?”如果答案是否定的,那就放弃这个念头,还会有更好的方法。虽然有时找到合适的盈利模式比较困难,但是牺牲 短期利益来换取用户的长期忠诚才更有价值,你也会过得更加问心无愧。

还有另外一种情况,一条收入线在起初是良性的,但是随着外部环境的变化逐渐变成了一笔不良收入。如果这笔收入已经成为你的一项重要收入来源,那你就需要十分小心谨慎了。

这方面的一个案例就是 eBay 搜索结果中的图片。1995 年 eBay 创办时,存储是非常昂贵的。所以当用户在商品列表中上传图片时收取一定的费用是合理的。10 年过后,到了 2005 年,存储已经变得非常便宜,上传照片要收费这种做法看起来十分荒谬。但是图片上传已经成为 eBay 的一笔可观的收入,要放弃这笔钱,把图片上传免费,着实是一个非常艰难的决定。

我们的用户体验团队和分析团队通过研究发现,在搜索结果中默认显示图片不仅能增加销量,而且对于搜索结果有用性的评分也有积极作用。最终,eBay 决定放弃这笔不良收入,把图片上传免费(最多 8 张),而且后来也没有再改回去过。


眼动追踪数据显示出图片展示对于搜索结果的重要性

  在产品开发过程中如果涉及到一些不良收入时,最好的做法就是进行调研,理解用户的需求和动机,结合 A/B 测试来衡量不良收入对优质收入所带来的影响。

追求优质收入

优质收入可以来自许多不同的渠道。对于消费者来说,只要产品的价值是显而易见的,他们就有付费的意愿。因此,在整个产品管理的过程中,需要首先明确产品的价值,然后再开发产品并开展相关的业务,不能先开发出产品再附加给它价值,用户需求研究永远是产品盈利的第一步。

对于一些已经存在的收入,有一些标准的增长方式,比如拓展到新地区,建立新渠道,延伸到更广阔的市场,为已经现有市场开发新产品等。在 Brandon Schauer 所写的《Adaptive Path》一书,还提出了一种新的收入增长理念,称为 Long Wow。原书中对 Long Wow 的定义如下:

Long Wow 意味着通过一次又一次地满足顾客来获得他们长期的忠诚。Long Wow 不仅仅是衡量忠诚度标准,更是通过以用户体验为核心的方式来培养和创造忠诚度。

Long wow 由以下四个步骤组成:

1. 了解与用户沟通的平台。明确线上和线下与用户接触的不同方式。

2. 满足用户尚未被满足的需求。在用户需求研究的基础上,认清哪些重要的用户需求还没有被你的产品或者任何一款现有产品所满足。

3. 创造并发展一套可重复的流程。将公司现有的优势和新的创意结合起来,不断满足消费者需求,取悦用户。

4. 做好计划,呈现惊艳的用户体验。随着时间的推进,改进你的 idea。在产品整个生命周期中不引入新的、更好的用户体验。

然后,根据情况不断重复这个过程。通过这种方式,你可以衡量产品是否带来了优质的收入,而且能确保为用户持续提供价值,培养更多愿意付费的忠诚客户。


技术需求

在讨论技术需求之前,需要先明确两个概念:“技术资产””技术负债“。 所谓“技术资产”就是产品所依赖的底层技术以及一些日常办公所用的系统(采购、财务、后勤)。相反,“技术负债”指的是限制产品开发的系统和代码(经常以 bug 的形式出现),技术负债如果长期得不到缓解会带来更加严重的问题。Construx 公司的首席软件工程师 Steve McConnell 认为,技术负债主要可以分为两类:

  • 无意的负债(unintentional debt)会出现在错误设计被实施时或者程序员写出了差劲的代码时。这种负债并不是刻意的,当然越少越好。
  • 有意的负债(intentional debt)是指公司明知道某种情况并不理想,但是出于种种原因还是做出了妥协(通常是由于预算或时间限制)。尽管这类技术负债也并不是件好事,但是对任何组织来说,它都是不可避免的,我们需要做的就是将其影响最小化。

对于技术负债来说,我们需要尽可能地减少负面影响,不然就会遇到我们常说的“破窗效应”。

“破窗效应”是犯罪心理学中的术语。用来解释城市中秩序混乱和破坏公物的行为,其含义是:

城市管理中需要保持各种设施处于良好的状态,并随时监控,这样才能阻止进一步的公物破坏甚至升级成更严重的暴力犯罪。

  我们可以把软件比作城市的环境。如果有几扇窗户破了(软件中出现一些糟糕的代码),而破窗又没有尽快修好,那么很有可能会出现更多破碎的窗户 (人们变得不再关心优质代码),继而环境进一步恶化:垃圾到处出现,擅自占用空房的人越来越多(代码标准普遍下降)。不久之后,所有的窗户都会破碎。

如果负债扩大到一定程度,公司最终花费在弥补这些漏洞上精力会比用在创造新价值上的还要多。常见的情况就是遗留的代码库往往需要耗费大量的精力去维护(也就是“还债”),留给开发系统新功能的时间就变少了。——Steve McConnell

在产品开发时需要竭尽全力去避免此类技术负债。如果遇到了,找时间来处理这些欠账的过程会非常艰难,经常看不到任何改变,团队内会有一些人不理 解这么做的原因,很多人懒得去清理代码中的这些垃圾。然而,在开发过程中清理这些技术负债恰恰是一项非常重要的工作,如果做不好很可能会摧毁整个体系。

当然,需要注意的是,技术负债并不一定都是坏事,有时技术负债会催生一些强大的功能。总得来说,新出现的负债是没问题的,但是长期累积起来的旧账就不好了。Henrik Kniberg 在他所写的《Good and Bad Technical Debt》 一文中曾提出一个避免技术负债失控的好方法,那就是引入了债务上限的概念,当你的负债达到一定限额时需要采取措施以避免进一步失控:

当债务达到上限时,我们就宣布进入“负债紧急状态”,停止开发新项目,所有人都将注意力放在清理旧代码中的问题,直到回归到基准线。

  理论上在每个开发周期中你都会遇到技术负债,但是当负债达到上限时,就需要及时调整,以免事态恶化。


权衡三方面需求

收集用户需求、商业需求和技术需求只是产品开发中一部分工作,更重要的是如何处理这些信息,平衡三方面需求。这时我们应该主要考虑以下三个要素:

  • 产品在生命周期中所处的阶段。这是一款全新的产品,还是已经问世一段时间的产品?
  • 用户获取情况。你们在努力吸引用户的阶段,还是用户会自己找上门来使用你们的产品?
  • 公司的财务状况。你们是在想方设法挣钱的阶段,还是已经有了稳定的收入?

这三个要素的组合不同,你关注的重点应该也不一样。如果是一款正在努力获取用户的新产品,那么你就需要十分关注用户需求;如果公司在寻求大规模良性的增长,那你就需要把重点放在盈利上。

  最后,需要强调的是:如果不理解产品的核心用户的需求以及商业上、技术上的需求,那你的产品就是建立在虚无之上的。一款产品可能在一段时间如日 中天,但最终肯定会有新的产品出现。所以不要把你的产品建立在危险的假设之上,开发产品时做到深思熟虑,努力开发出可持续的产品。

[转载]C# 大型电商项目性能优化(一) - Leon.Chen - 博客园

mikel阅读(879)

[转载]C# 大型电商项目性能优化(一) – Leon.Chen – 博客园.

经过几个月的忙碌,我厂最近的电商平台项目终于上线,期间遇到的问题以及解决方案,也可以拿来和大家多做交流了。

我厂的项目大多采用C#.net,使用逐渐发展并流行起来的EF(Entity Framework)框架,并搭配使用丹麦的一款主打CMS, DMS的.net web应用程序sitecore。

本篇为基础篇,侧重于阐述编码规范和一些编码技巧对系统性能的影响。不规范的编码方式,可能对单个方法或模块产生的性能影响是微不足道的,但在大型电商项目中,高并发的场景随处可见,欠妥的编码方式,可能会对整个系统的性能及用户体验,造成很大的影响。

作为电商项目,对性能影响最明显的模块,莫过于下订单及修改库存。在高并发场景下,这些模块的数据库存取的性能要求是非常高的,少许的性能浪费,都可能使系统在使用中的表现差强人意。

首先,我们来阐述一下EF框架下得编码规范。这里我们可以参考msdn关于IQueryable<T>及IEnumerable<T>的介绍:

对于在内存中集合上运行的方法(即扩展 IEnumerable< T> 的那些方法),返回的可枚举对象将捕获传递到方法的参数。在枚举该对象时,将使用查询运算符的逻辑,并返回查询结果。

与之相反,扩展 IQueryable <T> 的方法不会实现任何查询行为,但会生成一个表示要执行的查询的表达式树。查询处理由源 IQueryable<T> 对象处理。

 

IQueryable<T> 会生成一个查询表达式树,并不会将数据直接取出来,但该对象的扩展方法为我们提供了大量的高效辅助功能,例如判断数据是否存在的Any(),查询数据数量 的Count(),统计计数属性的Sum()等,EF会帮助我们生成最优的SQL,以减少数据库存取时的性能损耗。这些方法,我们都可以用比较笨拙的编码 方式进行实现,但其效率会低很多。

另外,编码过程中应尽可能避免使用ToList()方法及GetById()方法,这里依旧可以参考msdn,但笔者可以直白地去描述:采用这两种 方法后,程序会直接将数据从数据库中读取出来,并加载到内存,接下来我们可以直接操作这些实实在在的数据,并使用List<T>所扩展的方 法。然而这种方案会造成以下性能问题:

1.将未经业务处理的表达式树直接用来查询数据,其数据量太大,数据库的存取,磁盘的IO,都需要消耗大量的时间和空间。

2.EF支持的导航属性,会将相关联的对象都查出来,届时庞大的数据又拥有更庞大的分支,让内存和CPU飙升。

为了避免上述问题,EF为我们提供了行之有效的方法:

1.查询初期少用ToList()及GetDtoById()方法。

2.查询过程中,使用Select()和SelectMany()方法,只选取自己所需要的属性或对象。

 

为了看出不规范的编码方式带来的性能损耗,我们来看一段例子比较:

1.以下是只选取自己所需的属性的代码:

var productSKU = unitOfWork.ProductSku.Get(p => p.Id == item.SKUId)
.Select(p => new { p.Id, p.ProductId, p.Product })
.FirstOrDefault();

提交订单耗时的截图:

2.采用GetById()方法:

var productSKU = unitOfWork.ProductSku.GetByID(item.SKUId);

提交订单耗时截图:

笔者电脑老旧,该数据在i5 8G ram的计算机中仅需要200ms的时长,在性能更强的服务器上耗时更短。

从对比中我们可以看到:仅仅只修改了一个方法,程序请求的耗时竟有将近一倍的误差!如果我们的代码中充斥着这种懒惰的不规范写法,在高并发场景下,系统会被拖得很慢,甚至会出现程序报错。

 

接下来,我们说下抛开EF框架的做法,擅长SQL编程的园友,可能会不屑EF的提供的各种方案,直接写sql,采用ADO不是更快吗?的确,直接运行sql会让程序更快,ADO的速度是大家所认可的。EF也支持大家使用直接编写sql的方式:

var productSKU = dbContext.Database.SqlQuery<ProductSku>(sqlStr);

如果sql编程功力深厚,笔者是非常支持这种编程方案的。但EF框架也有其天生的优势:

1.EF框架让更多的初级软件从业者更快地学习和编写程序

2.EF提供的完善的扩展方法,帮助软件从业人员实现各种功能

3.规范编写的EF C#代码,并不会比原生的sql慢太多

 

这让笔者想起C#.net与Java程序员之间的矛盾^_^。笔者因为机缘巧合,也写过一段时间的Java。

二者的设计理念确实有所不同:

1.C#.net让初学者更快地入门,提供了更多的类库和方法,其出色的IDE让编程人员省心省力。且C#.net已经开源。

2.Java则需要编程人员做更多的思考,自己配置环境变量,自己敲命令行,让编程人员在思考的过程中加深对计算机原理、操作系统和软件工程的认识。

毫无疑问:两者都是当代最出色的高级程序语言(PHP,Python,JavaScript等的同行勿喷^_^)与其花时间争论谁才是最好的语言,不如兼而学之。

以上是本次性能优化介绍的基础篇,后续会为大家带来数据库层面的优化经验及体会。

[转载]C#大型电商项目优化(二)——嫌弃EF与抛弃EF - Leon.Chen - 博客园

mikel阅读(930)

[转载]C#大型电商项目优化(二)——嫌弃EF与抛弃EF – Leon.Chen – 博客园.

上一篇博文中讲述了使用EF开发电商项目的代码基础篇,提到EF后,一语激起千层浪。不少园友纷纷表示:EF不适合增长速度飞快的互联网项目,EF只适合企业级应用等等。

也有部分高手提到了分布式,确实,性能优化从数据库出发,初期就加索引,然后垂直拆分,水平拆分,读写分离,甚至是分布式事务,阳春白雪,格局很高。然而笔者希望通过渐进的过程来优化这个项目,我们缩小格局,从细节查看不同方案的优劣。

之前提过,使用EF最主要的原因是项目时间紧迫,EF搭建速度快,熟悉的同事也多,使用方便。这个决策确实帮助我们挺过了初期的难关。在业务量增长的过程中,一些问题也逐渐暴露出来,我们开始针对问题做优化。

问题1:部分请求响应缓慢,影响用户体验。

使用EF做数据的增删改查,一些不规范代码也会拖慢程序效率,笔者在上一篇中已经提过。某些请求中可能包含多次数据查询与更新,如果这些细小的问题 都以低效运行,那这个请求确实会很慢。然而在EF的框架下优化它,也未必能收到明显成效。以更新商品销量为例,我们上方案与代码:

方案1

先查出某一条数据,然后填入新算出的销量,再更新数据库,代码如下:

  //先查出数据,再更新
            var productSKU = unitOfWork.ProductSku.GetByID(dtos[0].Items[0].SKUId);
            productSKU.SalesCount = productSKU.SalesCount + dtos[0].Items[0].Quantity;
            unitOfWork.ProductSku.Update(productSKU);
            unitOfWork.Submit();

其响应时间如图:

方案2

采用IQuryable,之前提到过,这种查询方式不会真的将全部数据加载到内存,代码如下:

复制代码
复制代码
//2采用IQueryable查询更新
            var productSKU1 = unitOfWork.ProductSku.Get(p => p.Id == dtos[0].Items[0].SKUId);
            foreach (var item in productSKU1)
            {
                item.SalesCount = item.SalesCount + dtos[0].Items[0].Quantity;
                unitOfWork.ProductSku.Update(item);
            }
            unitOfWork.Submit();
复制代码
复制代码

其响应时间如图:

方案3

直接使用SQL,简单粗暴,代码如下:

//3直接使用sql更新
            string updateSql = @"update ProductSKU set SalesCount=SalesCount+" + dtos[0].Items[0].Quantity + " where Id='" + dtos[0].Items[0].SKUId.ToString() + "'";
            unitOfWork.ProductSku.ExecuteSqlCommand(updateSql);
            unitOfWork.Submit();

其响应时间如图:

 

我们来分析下这三种方案:

方案1简单易懂,将数据查出来,更新后再塞回数据库,逻辑清晰,代码更清晰。可是将数据取出来,加载到内存,再对内存里的数据进行修改,最后生成SQL送回数据库执行,这套走下来,好像兜了一大圈,其实我们只想更新个销量。

方案2显得好了些,使用了我们在前一篇中讲到的理念,不将数据整个加载到内存,只到用时才加载,通过这种方式更新,其实生成的SQL和直接执行 SQL差不太多了,但是我们可以发现:方案2和方案1时间差不多。在笔者预期中,方案2应该是比方案1好的,至于时间差不多的原因,应该是方案2用了一次 循环,第二次循环时不符合条件,然后跳出循环,造成了些许时间浪费。如果是批量更新销量,方案2必然比方案1优秀很多。

方案3自然是简单粗暴,一条SQL搞定,毫不拖泥带水,其效果也是喜人的,几乎比方案1快1倍!

之前有园友提到索引的问题,其实我厂有专职DBA,不仅为数据量较大的表添加了聚集索引和非聚集索引,还写了定时任务去更新索引。所以索引这块我们不深究。

早起追逐开发效率阶段,我们可能将方案1优化为方案2,然后继续做新功能开发。但是现在我们有更多的选择,我们也有更多的时间去深究用户体验与系统 效率了,那么自然的,我们开始嫌弃EF了。但是开发到这个程度,再去更换框架似乎不合适。而且大道至简,如果能用SQL搞定,那必是极好的。于是乎,我们 开始对关键部分业务代码做重构,替换为原生SQL。这似乎是抛弃EF的开端。

 

问题2:部分数据量很大的表需要分表,EF难以维系

EF是ORM框架,映射数据库对象,然而同一数据库的一张表被拆分为两张以上,EF似乎没法映射了,两张表字段完全一致,难道再写个Model?而 且分表是个动态的过程,也许一年分一次,也许一个月分一次,而且可能是定时任务去执行的,总不能一分表就改代码。自此,我们和EF的矛盾激化了。

园子里有关于数据库拆分的博客,我们所要改的只有数据访问层,最好不要动业务层。而且我们上文也提到:用原生SQL。那么我们就用原生SQL重写数据访问。

这里举个例子,比如我们拆分订单表,按年拆分,通常用户只会查看当年的订单,所以主表的查询次数肯定比其他分出的表要多,如果有用户要查往年的订单,我们再将查询范围扩大。按照这样的理念,我们开始重写数据访问层,并按照以下要点执行:

1.使用原生SQL

2.添加日期参数,如果日期超出主表的范围,则开始连接查询

3.查询中也可以添加其他条件,将参数作为对象填入SQL

4.抽象出一个生成SQL的公共方法,方便大家调用

拼装SQL是一件极其考验基本功的事,这个公共方法是我厂一位大师级的数据专家抽象出来的,大家也可以按照这个思路尝试下,一个简化版是很容易做出来的。

至此,项目中重要的业务功能已经和EF脱离关系了,我们也欣喜地收获了SQL带来的效率。而其他功能模块中,没有高并发场景或并不常用的应用,我们并没有做重构。

尽管本文标题是嫌弃,但如果是企业级应用,需要兼顾开发效率,且没有互联网模式下的业务量激增状况,笔者仍然推荐使用EF。

国外的营销怎么就那么贴心呢?!

mikel阅读(958)

先不说国内的邮件营销被做的多烂,就是动不动脑子的机械群发,也不考察是不是目标客户简直就是浪费带宽和资源,你给一个厨子发送学习理发的推广邮件有意义吗?!看看人家国外的邮件营销已经步入到大数据的分析阶段了,为什么这么说,有图有真相.

QQ截图20150313082720

这是一封域名的抢注的营销邮件,为什么他会发给我,因为我有好几个域名带“wei”,比如weikebao.com weikesou.com,可见人家通过查找所有wei开头的域名的所有者的邮箱,然后发给这些注册过wei开头的人,weiping.net这个域名的确不错,双拼,不过就是.net后缀不符合我的投资原则,要不然我肯定会果断支付抢注的费用,这样一封邮件就产生了一个订单,就这么简单。发对了人,就会产生订单,人家已经走心加数据分析到贴心的地步,最起码我挺感激他的,能够及时通知我weiping.net域名要掉了,可以抢注了,因此不反感,所以会关注下他的邮件内容,然后去他的网站看看。

多么希望国内的邮件营销这么贴心,不再是机械的群发简单粗暴的强奸我的邮箱啦!