月度归档:2014年11月

JavaScript循环一个对象的方法

JavaScript循环一个数组很常见:

var arr = ["aa","bb","cc"];
for(var i=0;i<arr.length;i++){
    alert(arr[i]);
}

纯JavaScript代码写得还是不多,所以当遇到要需要一个对象时,还真不知道如何写。实际过程是,在PHP中返回JSON数据:

$arr = array("USD"=>120,"GBP"=>200);
echo json_encode($arr);
//输出
{"USD":120,"GBP":200}

首先,PHP的关联数组会输出一个JS对象(JS中只有整型下标数组),我要循环输出这个JS对象:

var jso = {"USD":120,"GBP":200};

for(var o in jso){
    alert(jso[o]);
}

可以看到,这个循环也可用于循环数组。在for in中,第一个变量就是对象属性名或者是数组的下标,然后通过对象名或者数组的下标获取对应的值。

稍作总结,是为备忘。

Zend Framework 1.X 使用Zend_Auth组件实现登录

先来一段示例代码:

    	if ($this->getRequest()->isPost()){
        	if ($formLogin->isValid($_POST)){
        		$data = $formLogin->getValues();
        		
        		// 取得默认的数据库适配器
        		$db = Zend_Db_Table::getDefaultAdapter();
        		// 实例化一个auth适配器
        		$authAdapter = new Zend_Auth_Adapter_DbTable($db, 'core_users', 'username', 'password');
        		// 设置认证用户名和密码
        		$authAdapter->setIdentity($data['username']);
        		$authAdapter->setCredential(md5($data['password']));
        		// 实现authenticate方法
        		$result = $authAdapter->authenticate();
        		if ($result->isValid()){
        			// 获得getInstance实例
        			$auth = Zend_Auth::getInstance();
        			// 存储用户信息
        			$storage = $auth->getStorage();
        			$storage->write($authAdapter->getResultRowObject(
        				array('id', 'username', 'role')
        			));
        			$id = $auth->getIdentity()->id;// 获取用户id
        			// 记录登录时间
        			$modelUser = new Kh_Model_User();
        			$loginTime = $modelUser->loginTime($id);
        			
        			return $this->_redirect('/user/account/id/'.$id);
        		}
        		else{
        			$this->view->loginMessage = "对不起,你的用户名或密码不符。";
        		}
        	}
        }

Zend_Auth_Adapter_DbTable设置一个认证适配器,所谓认证适配器说得直接点就是存放了所有用户与用户证书(密码)的地方,要认证一个用户和对应的证书是否合法,就要使用这个适配器来帮助完成。认证适配器需要实现Zend_Auth_Adapter_Interface接口,它只有一个方法:authenticate(),一般来说,认证适配器可以是很宽松的,Zend_Auth组件提供了Zend_Auth_Adapter_DbTable、Zend_Auth_Adapter_Digest、Zend_Auth_Adapter_Http、Zend_Auth_Adapter_Ldap、Zend_Auth_Adapter_OpenId。使用Zend_Auth_Adapter_DbTable意思是把认证信息放入一张数据表中,这个还是非常经典的引用场景。

以使用Zend_Auth_Adapter_DbTable为例子,它的构造函数有四个参数,首先是数据库适配器,因为它需要连接数据库,然后是数据库中的哪个数据表,然后是数据表对应的用户名和证书(密码)字段。

实例化这个适配器之后,还需要设置要比对的用户名(用户认证标识)和证书(密码):

$authAdapter->setIdentity($data['username']);
$authAdapter->setCredential(md5($data['password']));

然后就是调用authenticate()方法,这个方法完成链接数据库查询比对数据,这个方法返回一个Zend_Auth_Result实例,如果因为某些原因认证查询不能执行,authenticate()应该抛出一个由Zend_Auth_Adapter_Exception产生的异常。 Zend_Auth_Result实际只有4个可用方法:

$result->getCode();       //错误代码
$result->getIdentity();   //尝试认证的用户名
$result->getMessages();   //错误信息
$result->isValid();       //是否通过认证

其中getCode()可能为如下值:

Zend_Auth_Result::SUCCESS
Zend_Auth_Result::FAILURE
Zend_Auth_Result::FAILURE_IDENTITY_NOT_FOUND
Zend_Auth_Result::FAILURE_IDENTITY_AMBIGUOUS
Zend_Auth_Result::FAILURE_CREDENTIAL_INVALID
Zend_Auth_Result::FAILURE_UNCATEGORIZED

对应成功,失败,用户名不存在失败,用户名模糊失败(???),密码错误失败,为分类失败。

如果认证成功,需要持久化存储认证结果,这个一般都是通过SESSION实现的。Zend_Auth组件也是如此。Zend_Auth组件默认使用Zend_Auth_Storage_Session类来实现持久化存储,它内部使用Zend_Session,真实的过程是它使用Zend_Session_Namespace来注册一个’Zend_Auth’名空间,然后存储到这个名空间中。

Zend_Auth组件可以通过Zend_Auth::setStorage()来设置一个自定义的持久化存储类实例(这个类必须实现Zend_Auth_Storage_Interface接口),比如:

$auth = Zend_Auth::getInstance();

$auth->setStorage(new Zend_Auth_Storage_Session('someNamespace'));

这样,它就会使用你指定的实例,并且改变了要注册的名空间(一般都不需要这么干)。默认只要通过调用getStorage()就会默认取得一个持久化存储对象(就是new Zend_Auth_Storage_Session(‘Zend_Auth’)),然后只要调用它的write()方法就可以包数据写入到Zend_Auth这个会话名空间中。

以上代码调用了适配器的getResultRowObject()方法,传递了一个数组,意思就是要从已经获取认证的那行数据中,获取指定的数据,它返回一个stdClass对象,把这个对象写入到会话中。

Linux中SVN服务器搭建

———————————–
命令行svn工具备忘:

#Checkout一个项目
svn checkout svn://192.168.1.168/test ./html --username username --password password

以上test为版本库,./html为本地对应的目录(会提示是否保存密码)。

#Update一个目录或文件
svn update 目录或文件

如果没有指定目录或文件,表示update当前工作目录。
———————————–

安装:(SVN服务器端可以独立存在对外提供服务,不需要所谓的Apache)

yum install subversion

rpm -ql subversion-1.6.11-10.el6_5.x86_64
/etc/bash_completion.d/subversion
/etc/rc.d/init.d/svnserve
/usr/bin/svn
/usr/bin/svnadmin
/usr/bin/svndumpfilter
/usr/bin/svnlook
/usr/bin/svnserve
/usr/bin/svnsync
/usr/bin/svnversion

subversion-1.6.11-10.el6_5.x86_64包含了服务端(svnserve)和客户端(svn),/etc/rc.d/init.d/svnserve是服务启动脚本,在CentOS下安装之后就注册到服务了,不过不是自动启动的:

chkconfig --list | grep svn
svnserve       	0:off	1:off	2:off	3:off	4:off	5:off	6:off

可以设置它自动启动:

chkconfig --level 345 svnserve on

然后启动服务:

service svnserve start

##查看
netstat -nalp | grep svn
tcp        0      0 0.0.0.0:3690                0.0.0.0:*                   LISTEN      1166/svnserve 

它监听3690端口。不过,如果就这样启动服务,SVN实际是无法工作的,通过:

ps -ef | grep svn
/usr/bin/svnserve --daemon --pid-file=/var/run/svnserve.pid

看到,这个启动脚本实际是帮你去执行/usr/bin/svnserve而已,而它默认并没有指定版本库路径。打开/etc/rc.d/init.d/svnserve看一下:

if [ -f /etc/sysconfig/svnserve ]; then
        . /etc/sysconfig/svnserve
fi

exec=/usr/bin/svnserve
prog=svnserve
pidfile=${PIDFILE-/var/run/svnserve.pid}
lockfile=${LOCKFILE-/var/lock/subsys/svnserve}
args="--daemon --pid-file=${pidfile} $OPTIONS"
[ -e /etc/sysconfig/$prog ] && . /etc/sysconfig/$prog

lockfile=/var/lock/subsys/$prog

start() {
    [ -x $exec ] || exit 5
    [ -f $config ] || exit 6
    echo -n $"Starting $prog: "
    daemon --pidfile=${pidfile} $exec $args
    retval=$?
    echo
    if [ $retval -eq 0 ]; then
        touch $lockfile || retval=4
    fi
    return $retval
}

不难看出,args是指定参数,附加参数通过$OPTIONS指定,在它之前的一段脚本让其可以取到外部变量,编辑/etc/sysconfig/svnserve(不存在则建立):

OPTIONS="-r /www/svn"

这样svnserve启动时就会添加这个参数,实际如果觉得麻烦可以直接编辑就好了。或者不使用这个脚本,直接手动启动:

/usr/bin/svnserve -d -r /www/svn

这样设置后应该是如下结果:(添加了-r /www/svn)

ps -ef | grep svn
/usr/bin/svnserve --daemon --pid-file=/var/run/svnserve.pid -r /www/svn

注意:svnserve应该以root身份运行,否则的话就要保证运行的用户对指定的版本库路径具有可读可写权限。

接下来就是建立版本库:

#cd /www/svn
#mkdir vfeelit
#svnadmin create vfeelit
#cd vfeelit/
#ls
conf  db  format  hooks  locks  README.txt
#cd conf
#ls 
authz  passwd  svnserve.conf

##编辑这三个文件
#vi svnserve.conf
[general]
anon-access = none
auth-access = write
password-db = passwd
authz-db = authz
realm = SVN Vfeelit

#vi passwd
[users]
vfeelit = sha830923

#vi authz
[groups]
admin = vfeelit
[vfeelit:/]
@admin = rw

##最后把svnserve启动
service svnserve start

文件svnserve.conf分别设置未认证用户无权访问,认证用户可写,认证密码文件为同目录下的passwd文件,权限配置为同目录下的authz文件;文件passwd设置用户名和密码;文件authz是权限配置,groups段配置用户组,左边是组名,右边是用户,[vfeelit:/]这个写法是指vfeelit这个版本库的根目录,下面的@admin = rw表示admin用户组有读写权限。

Svnserve启动后默认在3690端口监听,一般使用svn://ip/版本库访问即可,但是如果要使用HTTP协议,就需要搭配使用Apache:

yum install mod_dav_svn

rpm -ql mod_dav_svn-1.6.11-10.el6_5.x86_64
/etc/httpd/conf.d/subversion.conf
/usr/lib64/httpd/modules/mod_authz_svn.so
/usr/lib64/httpd/modules/mod_dav_svn.so

查看/etc/httpd/conf.d/subversion.conf(样本文件):

LoadModule dav_svn_module     modules/mod_dav_svn.so
LoadModule authz_svn_module   modules/mod_authz_svn.so

#   # cd /var/www/svn
#   # svnadmin create stuff
#   # chown -R apache.apache stuff
#   # chcon -R -t httpd_sys_content_t stuff

#<Location /repos>
#   DAV svn
#   SVNParentPath /var/www/svn
#
#   # Limit write permission to list of valid users.
#   <LimitExcept GET PROPFIND OPTIONS REPORT>
#      # Require SSL connection for password protection.
#      # SSLRequireSSL
#
#      AuthType Basic
#      AuthName "Authorization Realm"
#      AuthUserFile /path/to/passwdfile
#      Require valid-user
#   </LimitExcept>
#</Location>

通过HTTPD认证用户是不可以使用明文密码的,所以可以进入config目录中运行如下脚本:

#!/usr/bin/perl
# write by huabo, 2009-11-20

use warnings;
use strict;

#open the svn passwd file
open (FILE, "passwd") or die ("Cannot open the passwd file!!!n");

#clear the apache passwd file
open (OUT_FILE, ">webpasswd") or die ("Cannot open the webpasswd file!!!n");
close (OUT_FILE);

#begin
foreach (<FILE>) {
if($_ =~ m/^[^#].*=/) {
$_ =~ s/=//;
`htpasswd -b webpasswd $_`;
}
}

运行这段代码后会生成webpasswd文件,在配置中使用AuthUserFile指令指到这个文件即可。

关于SVN搭配Apache的使用,需要知道的是,你只要启动Apache并已经加载了配置就能通过HTTP协议访问SVN库,比如版本库的根目录是/www/svn,里面有vfeelit和ifeeline两个版本库,配置上可以集中配置,也可以分开单独配置:

####集中配置
####访问地址
####http://ip/repos/vfeelit 和 http://ip/repos/ifeeline
<Location /repos>
   DAV svn
   #使用SVNParentPath指定版本库根目录
   SVNParentPath /var/www/svn
   <LimitExcept GET PROPFIND OPTIONS REPORT>
      # Require SSL connection for password protection.
      # SSLRequireSSL

      AuthType Basic
      AuthName "Authorization Realm"
      #所有用户密码文件
      AuthUserFile /www/svn/passwd
      #所有版本库权限配置文件
      AuthzSVNAccessFile /www/svn/authz
      Require valid-user
   </LimitExcept>
</Location>

####分别单个配置
####访问地址
####http://ip/vfeelit 和 http://ip/ifeeline
<Location /vfeelit>
   DAV svn
   #这里使用的是SVNPath而不是SVNParentPath来指定版本库具体目录
   SVNPath /var/www/svn/vfeelit
   <LimitExcept GET PROPFIND OPTIONS REPORT>
      # Require SSL connection for password protection.
      # SSLRequireSSL

      AuthType Basic
      AuthName "Authorization Realm"
      AuthUserFile /www/svn/vfeelit/conf/passwd
      AuthzSVNAccessFile /www/svn/vfeelit/conf/authz
      Require valid-user
   </LimitExcept>
</Location>

<Location /ifeeline>
   DAV svn
   #这里使用的是SVNPath而不是SVNParentPath来指定版本库具体目录
   SVNPath /var/www/svn/ifeeline
   <LimitExcept GET PROPFIND OPTIONS REPORT>
      # Require SSL connection for password protection.
      # SSLRequireSSL

      AuthType Basic
      AuthName "Authorization Realm"
      AuthUserFile /www/svn/ifeeline/conf/passwd
      AuthzSVNAccessFile /www/svn/ifeeline/conf/authz
      Require valid-user
   </LimitExcept>
</Location>

单个分别配置的情况跟直接使用SVN的情况类似,不过要把明文密码装换成加密的。至于AuthzSVNAccessFile指令对应的权限配置,跟具体的版本库conf中自动生成的authz一样。

Windows中使用的比较广泛的SVN服务器端软件VirtualSVN使用的就是集中配置,它集成了Apache,简单分析一下它的集中配置:
vitualsvn
添加的用户保存到版本库的根目录的htpasswd文件中:

vfeelit:$apr1$q7m$8Ts2s8IpKs3CylFDV47pv0
ifeeline:$apr1$r14$I34XA.844KXJxvefyNP8P1

添加的组保存到版本库的根目录的groups.conf文件中:(添加组时可以指定组用户)

[groups]
admin=vfeelit
dev=ifeeline
super=vfeelit,ifeeline

这两个东西基本和Linux上的集中配置方式是一样的,唯一不一样的地方是groups.conf文件中没有对应版本库的权限配置文件。那么权限配置放在了什么地方?右击具体的版本库或版本库路径:
svn-alist
这些配置保存到了具体版本库根目录下conf目录中的VisualSVN-SvnAuthz.ini文件中:

[/]
@super=rw
ifeeline=rw
vfeelit=rw

可见,VirsualSVN把authz拆分成了两个文件(groups.conf 和 每个版本库对应的VisualSVN-SvnAuthz.ini)。原理上是一样的。

实际上,安装了VirsualSVN后,也可以不依赖Apache服务而直接启动svnserve进程,这个跟在Linux中的配置是一样的:
VirsualSVN
在命令行中启动:

C:\Program Files\TortoiseSVN\bin>svnserve -d -r E:\Repositories

然后检查是否启动:

C:\Users\Administrator>netstat -an | findstr 3690
  TCP    0.0.0.0:3690           0.0.0.0:0              LISTENING

可见3690端口被监听了。不过使用它的窗口管理工具可能会方便很多。

————————————————————–
客户端工具,安装SVN服务端时客户端工具也安装了,不过是基于命令行工具的。如果在Windows中,可以使用TortoiseSVN客户端工具,这个用得非常广泛。
svn-vfeelit
建立一个文件,右击选择Checkout,输入版本库地址(svn://192.168.1.168/vfeelit),确定后跳出认证窗口:
svn-auth
输入用户名和密码就可以下载版本库。

如何使用这个客户端这里不讨论。在使用时碰到一个问题,当要查看日志时提示无法连接服务器,要求下线,实际是配置出了问题,开始我在svnserve.conf文件中设置anon-access为read,表示无认证的用户可读,可是TortoiseSVN看起来不买这个单,改为none后就可以查看提交日志了。

如果觉得在Linux上配置SVN服务比较麻烦可以去使用VirtualSVN Server,下载链接:https://www.visualsvn.com/downloads/。

PHP集成开发环境 之 Wampserver

前言:公司的电脑是XP系统,2G内存,被监控,在这样的机器下搞开发,真实憋屈的很。我平时习惯使用Zend Studio,虽然在2G内存的XP中也跑得起来,但是东西一多就慢的要死。对于开发环境,我平时是在Windows下安装个虚拟机跑个Linux,再在上面跑LAMP或LNMP,显然2G的内存再跑个虚拟机是吃不消的。所以我必须寻找一个在Windows下的集成开发环境,它必须很方便,扩展齐全,毕竟仅仅是个开发环境而已,我以前用过Zend Server CE Windows版,现在去搜索,MD,好像没有CE版本了,放弃,最终找到了Wampserver,试用一下,基本满意。

官网地址:http://www.wampserver.com/en/,Wampserver 2.5只能允许在Windows 7以上的系统,所以要在XP上允许它你要去找一个旧版本,所有的下载都在http://sourceforge.net/projects/wampserver/files/,比较新的版本还有64位版本。

wampserver

安装后会注册为系统服务:
wampserver-service

Apache、PHP、MySQL在安装目录的bin目录中,里面的配置改变,模块装载等都可以通过桌面右下角的图标进行快速操作。

注意:Wampserver 2.5需要安装微软的C++动态链接库(使用比较新的库,一般电脑可能安装的是低版本的),具体参考安装包中说明。另外,Window中PHP作为Apache模块运行的情况通常是线程安全的,可以运行phpinf()函数来确认:
PHP线程安全
这样在选择PHP的第三方扩展时,就知道应该选择线程安全版本还是非线程安全版本了,比如PHP中的memcache扩展(注意不是memcached,多一个d):
php-memcache-dll
这里资源应该选择TS版本了。

另外有一个需要十分注意的问题就是,命令行运行PHP和通过wamp运行的PHP,应用的php.ini是不一样的,这个不清楚,真实害死人的。

//命令行下
d:\wamp\bin\php\php5.5.12>php.exe -v
PHP 5.5.12 (cli) (built: Apr 30 2014 11:20:58)
Copyright (c) 1997-2014 The PHP Group
Zend Engine v2.5.0, Copyright (c) 1998-2014 Zend Technologies
    with Xdebug v2.2.5, Copyright (c) 2002-2014, by Derick Rethans

d:\wamp\bin\php\php5.5.12>php.exe --ini
Configuration File (php.ini) Path: C:\windows
Loaded Configuration File:         D:\wamp\bin\php\php5.5.12\php.ini
Scan for additional .ini files in: (none)
Additional .ini files parsed:      (none)

命令行下,明确告诉我们搜索到php.ini在D:\wamp\bin\php\php5.5.12\php.ini。如果你手动修改这里的php.ini,然后重启Apache,玛尼,配置没有生效。建立一个脚本,运行phpinfo()函数,通过浏览器访问,发现加载PHP的配置文件是D:\wamp\bin\apache\apache2.4.9\bin\php.ini,我靠,不一样。

PHP作为Apache的模块运行,准确来说命令行运行的PHP解释器和Apache运行的解释器是不一样的,但是大部分情况应用相同的配置文件。很明显,这里是Wamp修改配置路径,大概是拷贝了一份到D:\wamp\bin\apache\apache2.4.9\bin\目录下,并且让PHP运行时读取这份配置。

永久链接:http://blog.ifeeline.com/1464.html

eBay用户授权流程

ebay-developer
往下翻,点击Customize the eBay User Consent Form,找到Manage Your RuNames部分,然后点击Generate Runame,这样就会产生一个所谓的RuName,长成这个样子”vfeelit-vfeelit1d-65eb–tboemfhvb”,可以点击多次产生多个RuName,不过看起来没有什么必要,那么RuName是什么毛呢,先看看针对它的配置吧,在对应字符串右边点击Show Details,将展示如下表单:
eBay RuName
Display Title和Display Description是展示给用户看的标题和描述,这个信息可以在Application Level Settings中设置Show Application Details为enabled或disabled来设置,还可以上传Logo,这些信息是在用户点击同意授权时展示的出来给用户看的。

Token Return Method设置如何获取Token,Authorization Type为授权类型,Accept Redirect URL为成功授权后跳转到的地址,Reject Redirect URL授权被拒绝时跳转到的地址。实际上这里的值只要默认就可以了,关于Accept Redirect URL和Reject Redirect URL如果要设置,必须是HTTPS的链接地址,实际设置这两个变量毫无意义,因为它不会回传任何信息到这两个URL。所以,RuName实际上是一个Application的标识符(别名),它设置了相关的认证类型,Token获取方式等等,比如当用户授权成功后,就需要用这里设置的Token Return Method来获取Token。

对于一个应用,只要设置一个RuName即可。多个RuName也支持。这个步骤完成后应用就支持多用户授权了。

授权过程,步骤如下:
1 调用GetSessionID获取一个SessionID
这个API调用详细参考:http://developer.ebay.com/Devzone/XML/docs/Reference/ebay/GetSessionID.html,这里面如果是发送XML,只要发送RuName即可:

<?xml version="1.0" encoding="utf-8"?>
<GetSessionIDRequest xmlns="urn:ebay:apis:eBLBaseComponents">
  <!-- Call-specific Input Fields -->
  <RuName> string </RuName>
  <!-- Standard Input Fields -->
  <ErrorLanguage> string </ErrorLanguage>
  <MessageID> string </MessageID>
  <Version> string </Version>
  <WarningLevel> WarningLevelCodeType </WarningLevel>
</GetSessionIDRequest>

从返回提取SessionID即可。

2 用获取到的SessionID构建URL
URL格式:https://signin.ebay.com/ws/eBayISAPI.dll?SignIn&RUName=RUName&SessID=SessionID,SessionID需要经过URL-encoded,然后定位到这个URL,这样将打开用户登录表单,用户登录成功后将跳到一个是否同意授权的页面:
eBay 用户登录
登录后跳到:
授权应用
这个的Grant application access后的名称就是设置RuName是指定的Display Title,这就说明RuName可以看做是Application。最下面展示的是应用的信息,这个可以在开发者账户中进行设置。点击I agree跳转到如下页:
ebay_auth_success
这个也是可以设置的。前面已经论述。这里叫你去关闭这个页。TMD,这就完了,然后接下来要发起获取Token的操作,这个过程明显让我们感觉整个流程被中断了。我们观察一下这个返回的URL:https://signin.ebay.com/ws/eBayISAPI.dll?ThirdPartyAuthSucessFailure&isAuthSuccessful=true&ebaytkn=&tknexp=1970-01-01+00%3A00%3A00&username=testuser_vfeelit,问号之前的是可以设置的(需要HTTPS),后面的数据是固定的,isAuthSuccessful参数表明是否成功,username指出了eBay用户名,应该只要设置一下应用的返回地址,根据这些参数,也是可以自动获取Token的(流程不中断,因为获取到了username)

3 获取Token
以上两个步骤完成后,只是说明用户对应用进行了授权,但是授权码应用程序还没有获取到,这个时候调用FetchToken(传递eBay ID),就能返回针对这个账户的Token。

4 保存这个Token和有效时间
Token保存起来后就可以使用这个Token来访问API操作对应账户数据了。

这个过程看起来并没有比oAuth 2先进多少,这个流程过程中的中断让人产生困惑,虽然可以设置Accept Redirect URL,但是它要求是HTTPS的链接。

PHP文件编码遭遇UTF-8的BOM

之前总结过PHP文件编码的UTF-8编码带BOM的问题:http://blog.ifeeline.com/952.html

最近又遭遇了一次。过程如下:
我本机无法链接上实际服务器,也没有发布服务器,要做实际的测试需要到专门机器上去做,代码上传上去后报错,在FTP本地这边直接编辑文件保存然后上传,于是问题出现了,特别是那些返回JSON数据的方法,数据传递到客户端,都是就是无法解析。刚开始是代码哪里出了问题,用尽了所有可用方法,仍然无法找到问题出在哪里,最终恢复之前的版本上传到服务器,暂时缓解压力。

建立一个文件,保存为UTF-8编码:
UTF-8编码

然后用Windows中的文本编辑器打开并编辑后保存:
UTF-8 文本编辑

然后再次打开文件(使用EditPlus)查看编码:
修改文件编码

看到了吧,文件变成了ANSI,TMD,ANSI是啥毛啊,为何会改我的文件编码?我这里是Windows 7下做的实验,实际在XP下,如果文件原来是UTF-8编码,通过文本编辑器编辑保存后编码改为UTF-8 + BOM,注意,它是自动修改的,没有任何提示。

PHP中无法支持带BOM的UTF-8编码的文件,如果包含这种文件,将出现无法预料的问题。此问题是我第二次遇到此问题,真实操蛋。慎用Windows文本编辑器。

Ebay E邮宝API开发

开发者专区(总入口)
http://www.ebay.cn/developer/
国际e邮宝API V3
http://www.ebay.cn/developer/single/epacket.html
国际e邮宝API V4 (eBay亚太物流平台API)
http://www.ebay.cn/developer/single/APAC-SHIPPING.html
说明:V3还可用,以后全面转换成V4(原计划是2014-10-10,后推迟)

1 注册开发者账户
https://developer.ebay.com/devzone/account
在My Account页获取生成一组Key(分开发环境 和 生产环境)

2 注册Sandbox账户
打开www.sandbox.ebay.com,点击Get Started下的eBay Sandbox User Registration链接,会跳转到developer.ebay.com,要求用开发者账户先登录,登录后跳到eBay Sandbox User Registration Tool开始注册账户,用户名统一以TESTUSER_开头,注册类型只有Buyer and Seller,说明既可以是买家,也可以是卖家,也可以再次注册一个账户,分别模拟买家卖家。

也可以登录开发者账户后在tools下点击Sandbox User Registration链接进行Sandbox账户注册。

3 产生User Token
实际就是eBay用户对开发者或APP的授权码,可以在开发账户中的Tools下面点击Get a User Token获取,点击后跳转到key选择页面,输入(或选择环境或Key),然后点击Continue to generate token,然后跳转到授权登录页面,使用之前支持的账户进行登录(表示这个账户授权到开发者账户),输入账户密码登录后跳到一个授权提醒页面,点击I agree,然后弹出结果,点击Save Token。

4 使用对应的授权码访问API(操作授权码对应的店铺)

如果刚开始接触API开发,往往在账户授权这里被卡住,有点难理解。实际上,这里申请的开发者账户可以看做是一个应用程序(至少是代表),现在这个应用程序要获取你账户(eBay账户或eBay账户对应的EUB)里面的信息,这里就涉及到两个问题。第一,数据如何访问,第二,如何授权这个应用程序访问这些个人数据。对第一个问题,自然是通过提供API访问了,但是访问前必须先获取授权,这就是第二个问题。关于授权,业界有成熟的解决方案OAuth 2,它用得非常普遍。但是eBay没有采用OAuth 2,它自己实现了一套授权逻辑(不过跟OAuth 2也有类似地方),过程这里先跳过了,最终结果是eBay卖家输入了它的账户密码点击同意授权,应用程序将接收到一个很长的字符串,美其名曰Token。应用程序在使用API时,这个Token是必须传递的,Token是有有效期的,会不会导致它泄露呢,理论是不会的,因为它作为POST数据的一部分通过HTTPS进行传递。就算泄露了Token,要访问API时还要知道AppKey已经对应的签名等。

以下是一段来自官方的例子:

error_reporting(E_ALL);
$compatabilityLevel = 717;    // eBay API version

$devID = "95a9c0d-1cad-4fda-b74d-b610efbb560";
$appID = "EBTCo63ba-b11-4e96-b0c3-b4dd064239";
$certID = "f2fd3c8-18d4-4419-8fd3-72ae811829f";
$serverUrl = "https://api.sandbox.ebay.com/ws/api.dll";
$userToken = "xxxxxxxxxxxxxx";

$siteID = 0;
//要调用的API
$verb = 'GetTokenStatus';
 
$headers = array (
    'X-EBAY-API-COMPATIBILITY-LEVEL: ' . $compatabilityLevel,
    'X-EBAY-API-DEV-NAME: ' . $devID,
    'X-EBAY-API-APP-NAME: ' . $appID,
    'X-EBAY-API-CERT-NAME: ' . $certID,
             
    //the name of the call we are requesting
    'X-EBAY-API-CALL-NAME: ' . $verb,           
             
    //SiteID must also be set in the Request's XML
    //SiteID = 0  (US) - UK = 3, Canada = 2, Australia = 15, ....
    //SiteID Indicates the eBay site to associate the call with
    'X-EBAY-API-SITEID: ' . $siteID,
);
 
//POST的数据,一个XML字符串
$requestXmlBody = '<?xml version="1.0" encoding="utf-8"?>
<GetTokenStatusRequest xmlns="urn:ebay:apis:eBLBaseComponents">
  <RequesterCredentials>
    <eBayAuthToken>'.$userToken.'</eBayAuthToken>
  </RequesterCredentials>
</GetTokenStatusRequest>';
 
//使用CURL发送数据        
//initialise a CURL session
$connection = curl_init();
//set the server we are using (could be Sandbox or Production server)
curl_setopt($connection, CURLOPT_URL, $serverUrl);
         
//stop CURL from verifying the peer's certificate
curl_setopt($connection, CURLOPT_SSL_VERIFYPEER, 0);
curl_setopt($connection, CURLOPT_SSL_VERIFYHOST, 0);
         
//set the headers using the array of headers
curl_setopt($connection, CURLOPT_HTTPHEADER, $headers);
         
//set method as POST
curl_setopt($connection, CURLOPT_POST, 1);
         
//set the XML body of the request
curl_setopt($connection, CURLOPT_POSTFIELDS, $requestXmlBody);
         
//set it to return the transfer as a string from curl_exec
curl_setopt($connection, CURLOPT_RETURNTRANSFER, 1);
         
//Send the Request
$response = curl_exec($connection);
         
//close the connection
curl_close($connection);
 
header("Content-type: text/xml");
print_r($response);  

返回的XML:

<GetTokenStatusResponse><Timestamp>2014-11-19T15:16:41.219Z</Timestamp><Ack>Success</Ack><Version>893</Version><Build>E893_CORE_API_17097905_R1</Build><TokenStatus><Status>Active</Status><EIASToken>nY+sHZ2PrBmdj6wVnY+sEZ2PrA2dj6wFk4GhDJmKogudj6x9nY+seQ==</EIASToken><ExpirationTime>2016-05-12T15:04:46.000Z</ExpirationTime></TokenStatus></GetTokenStatusResponse>

EBay Api顺利通过沙盒测试,但是我这里的E邮宝V4.0.0沙盒测试一直失败,起初以为是账户等信息不对,换了几次,总是提醒Token无效,但是通过EBay Api测试Token是有效的(如上代码运行结果)。后来我直接到Ebay香港注册了个真实的Ebay账户,然后授权到真实的开发者账户,测试就通过:

$serverUrl="https://api.apacshipping.ebay.com.hk/aspapi/v4/ApacShippingService";

$request=array();
$request["APIDevUserID"]="xxxxx";
$request["APISellerUserToken"]="00000";
$request["APISellerUserID"]="xxxxx";
$request["AppID"]="EBTCo1d7-1e69-4cbf-adbf-7c47209ab";
$request["AppCert"]="45d1d5c-d54c-4381-bd3b-f9b0949479";
$request["MessageID"]="";
$request["Version"]="4.0.0";
$request["Carrier"]="CNPOST";
//$request["Service"]="EPACK";

$client = new SoapClient($serverUrl."?wsdl");
$r = $client->VerifyAPACShippingUser(array("VerifyAPACShippingUserRequest"=>$request));

print_r($r);

///输出
stdClass Object
(
    [VerifyAPACShippingUserResult] => stdClass Object
        (
            [Version] => 4.0.0
            [Ack] => Success
            [Message] => VerifyAPACShippingUser succeeded
            [Timestamp] => 2014-11-20T06:58:05.989-07:00
            [InvocationID] => F4C3C334D173429BA38538986C620B1D
            [CarrierList] => stdClass Object
                (
                    [CarrierGeo] => stdClass Object
                        (
                            [Carrier] => CNPOST
                            [FromCountryCode] => CN
                        )

                )

        )

)

看起来,E邮宝v4.0.0服务还有待完善。

附加信息,E邮宝实际是一个独立的服务,是要注册账户的,你可以使用你的Ebay账户(仅限香港注册的)去注册一个E邮宝账户,这样你的这个EBay账户也就是E邮宝账户,同时你的Ebay账户默认会被添加到管理账户的卖家列表中,你可以继续添加Ebay账户进来,就是一个E邮宝账户对应多个Ebay账户,在调用E邮宝API时,参数APISellerUserID是必填的,它是能定位到E邮宝的中设置的的Ebay账户。

只要通过Ebay的API认证,就能调用E邮宝的API管理物流发货。

使用Zend_Pdf合并多个PDF文件

最近弄了一下E邮宝,相关信息上传之后要打印地址标签,API返回的是PDF文件流。目前情况是,有多个E邮宝账户,里面分别有需要打印的地址标签,现在希望跨账户打印一批地址标签。查阅E邮宝API说明文档得知,同一个账户可以一次打印多个地址标签,但是要把国家分开,意思就是同账号,同国家,可以一次打印。

基于以上的条件,要实现跨账户批量打印好像无解。实际上,解决方案异常简单,就是每次都使用对应的账户通过API只请求一个运单的地址标签,然后把返回的PDF进行合并。

简单科普一下,一个PDF文件是由页面组成的,应给PDF文件至少有一个页。

以下是Zend_Pdf中合并多个PDF文件示例代码:

$pdf1 = new Zend_Pdf("a1.pdf",1,true);
$pdf2 = new Zend_Pdf("a2.pdf",1,true);    	

$pdf = new Zend_Pdf();
for($i=0;$i<count($pdf1->pages);$i++){
    $pdf->pages[] = clone $pdf1->pages[$i];
}
for($i=0;$i<count($pdf2->pages);$i++){
    $pdf->pages[] = clone $pdf2->pages[$i];
}    	

header("Content-type:application/pdf");
echo $pdf->render();

以上代码合并PDF是没有问题的,但是如果Zend_Pdf组件比较旧就要注意了,如果版本比较低,Zend_Pdf_Page不支持clone操作。Zend_Pdf组件是一个高度独立的组件,如果担心升级整个Zend框架导致其它问题,完全可以只升级这个组件。

最近参与开发维护一个ERP项目就是基于Zend Framework的,发现它是比较低的版本,现在我要用新版的Zend_Pdf组件,全局搜索了Zend_Pdf关键字,发现没有任何匹配,于是我放心的做了覆盖了,提交到版本库….

SQL实例系列 之二

有产品表,产品多属性表,订单表,订单详情表。产品多属性表记录产品的多个属性,比如颜色为红色,尺寸为5寸的产品组成一个新的SKU插入产品多属性表,那么这条记录必定就有它的主SKU,所以产品表中通过一个多属性字段记录该产品是否是多属性产品。产品表中有记录该产品最早上线时间。订单详情表中记录了订单具体的产品,每个订单详情中的产品记录有主SKU字段和是否多属性字段和多属性SKU,它通过主SKU和多属性SKU和产品表以及产品多属性表产品联系。每个订单都有创建时间。

现在要查找一批产品从它的最早上线时间开始,顺延一段时间(比如30天)的这一段时间内产生的销售总额(分货币显示)和有销售的产品和涉及到的SKU数以及涉及到的所有订单并且在每个订单后列出这些产品贡献的销售额(分货币显示)。

主要使用如下SQL获取所有符合的记录:

SELECT p.product_id, p.product_name, p.product_sku, p.product_is_more_var, p.product_is_list, if(p.product_is_more_var,pm.product_more_var_sku,p.product_sku) as display_sku, if(p.product_create_time,from_unixtime('%Y%M%D',p.product_create_time),0) as product_create_time, if(p.product_first_list_time,from_unixtime('%Y%M%D',p.product_first_list_time),0) as product_first_list_time,
o.order_number, o.order_currency_code,
od.order_product_qty, od.order_price
FROM 
(product p LEFT JOIN product_more_vars pm ON p.produc_id = pm.product_id) 
JOIN 
(order o JOIN order_detail ON o.order_id = od.order_id) 
ON od.order_product_sku = if(p.product_is_more_var,pm.product_more_var_sku,p.product_sku)
WHERE ((o.order_create_time > product_first_list_time) AND (o.order_create_time < product_first_list_time+30*24*2600))
AND product_id in(......)

可以很容易的在后面添加GROUP BY语句进行各类汇总,如果添加多次GROUP BY语句发送SQL,看起来效率不高(这里是大数据集)。所以最终决定循环一遍这个结果集,然后顺便做各种汇总,这样这里就使用了一个多重嵌套的数组。

关于使用JOIN还是LEFT JOIN,需要紧记,当需要一个集合全部记录时,LEFT JOIN就很合适,以上提到的产品与产品多属性表就必须使用LEFT JOIN来获取完整的产品集合,这个搞法奇葩地方在于,当使用SKU定位产品信息时,你到产品表中可能无法匹配,因为它可能是多属性的子SKU,同样,你到多属性表中定位这个SKU时,可能也无法定位,因为这个SKU的产品是非多属性的,产品表中SKU就是它的实际SKU。

然后把这个汇总数组传递到视图,在视图中调用JS显示详情(比如涉及到的所有订单),如果一页显示不下还要做翻页处理(JS端)。

SQL实例系列 之一

有产品表,产品上架表,站点表,产品表和产品上架表的关系是1对n关系,站点表和产品上架表也是1对n的关系,产品表和站点表通过产品上架表实现了n对n的关系。通俗说就是,产品上架表中记录了每个产品、产品上架时间、是否在上架以及上架到了哪个站点。

现在要找出所有产品的最早上架时间以及是否已经上架(注,产品上架表中记录了同一个产品的多条记录,都有一个上架时间,以及这个产品是否在上架,只要有一个在上架就说明这个产品在上架的),那么从字面理解就是找出同一产品的所有记录,按照时间从小到大排序,取第一条记录的时间即为最早上架时间,然后对同一产品所有记录进行循环,判断是否有产品在上架,一旦碰到真就返回。

这个搞法面对数据表里面的上10万条数据时(超过1万产品),实在让人发毛。虽然从程序上来看,实现非常简单,也很好理解,但是它是最低效的。更加高效的做法应该是充分利用数据库的特征:

SELECT count(is_list) as is_list, create_time 
FROM product_site 
GROUP BY product_id ORDER BY create_time ASC

通过数据表索引的帮助,这条语句非常快速的返回了结果,虽然也有上万条记录,但是这个做法比最原始的做法至少提升10倍。10倍是什么概念,如果原始做法要10秒,那么后面的这个搞法只要1秒。

实际工作中遇到的问题,是为总结记录。