author : ShopEx开发团队
since : 2009-12-03 $Rev: 195 $
1. 前言和导读
本手册对应版本shopex 4.8.5
本手册部分内容由shopex系统源代码直接生成,因此会持续保持更新
- html版本: http://docs.dev.shopex.cn/shopex-b2c/
- pdf版本: http://docs.dev.shopex.cn/shopex-b2c/shopex-book.pdf
相关文档
本手册适合谁来阅读:
- 网店店主: 查看包括怎样获取shopex。如何选择主机。怎样安装和调优你的网店系统。
- 开发者: 希望扩展shopex功能的朋友,本手册将为你展示多种扩展系统的方法,你将发现shopex已经为你很多准备工作。手册后面的附录里包含了shopex系统的数据对象和数据库的定义细节。
2. 安装和使用
ShopExV4.8网店系统是一套基于网上快速建店的标准化B2C电子商务系统。系统集成了最基本最普通最常用的电子商务运作流程及使用功能,可以满足正常的开店需求。
2.1. 安装shopex
2.1.1. 如何选择主机
shopex提供了一个服务器性能检测探针文件,位于de>install/svinfo.phpde>。你可以单独上传这个文件到被测的服务器环境上。这个探针所探测的数据和你在de>系统安装时de>或者后台看到的de>服务器状态de> 是完全一样的。 下面就是个典型的服务器检测报告:
$Rev: 195 $ ShopEx 服务器测评 ================================================================服务器基本信息================================================================ 操作系统 WINNT 服务器软件 Apache/2.0.59 (Win32) DAV/2 SVN/1.4.0 PHP/5.2.3 php运行方式 apache2handler ================================================================php基本信息================================================================ php版本 5.2.3 程序最多允许使用内存量 memory_limit 128M POST最大字节数 post_max_size 8M 允许最大上传文件 upload_max_filesize 2M 程序最长运行时间 max_execution_time 30 被禁用的函数 disable_functions 无 ================================================================基本需求================================================================ PHP4以上 5.2.3 zend.ze1_compatibility_mode 关闭 Off 支持文件锁(flock) 支持 php可以解析xml文件 支持 MySQL函数库可用 5.0.37 数据库Mysql 3.2.23以上 127.0.0.1:3306ZEND Optimizer2.5.7以上 未安装 DNS配置完成,本机上能通过域名访问网络 成功 ================================================================推荐配置================================================================ unix/linux 主机 WINNT php 版本5.2.0以上 5.2.3 MySQL版本 4.1.2 以上 5.0.27 GD支持 freetype,gif,jpg,png,bmp Zlib支持 支持 Json支持 支持 mbstring支持 支持 fsockopen支持 支持 iconv支持 支持 register_globals关闭 已关闭 allow_url_include关闭 (php5.2.0以上) 已关闭 高速缓存模块(apc,memcached) Memcached
小技巧:
- 你可以在ie浏览器里直接ctrl+a,ctrl+c复制出来,不需要担心格式问题
- 这是个独立的文件,同时也会检测php环境和zend opt。
2.2. 初始化配置系统
2.3. 系统调优
2.3.1. url rewrite
- 如果开启rewrite,过去的网址将依然有效,用户可以同时通过新老两种url访问你的系统。同时我们会通过发送301头信息更新搜索引擎的记录为新地址
- 该转换规则存在于plugins/functions/urlmap.php文件中。 其他系统转换过来的用户可以通过修改该文件,使得旧地址依然有效
- 新版本url地址映射关系是由plugins/functions/actmapper.php文件所定义, 熟悉php的用户可以自己设计独特的url方式
Apache的rewrite规则
RewriteEngine on # 设置RewriteBase的值为你的商店目录地址 RewriteBase / RewriteCond %{REQUEST_FILENAME} !-f RewriteCond %{REQUEST_FILENAME} !-d RewriteRule ^(.*)$ index.php?$1 [L]
rewrite启用方法,就是把root.htaccess改名成.htaccess。更改.htaccess文件中rewritebase为你url的前缀如果感觉麻烦,直接在后台 设置->基本设置->启用伪静态。由程序自动完成上面两个工作。
IIS下的配置方法
iis下需要isapi_rewrite 第三版,老版本的rewrite不支持RewriteCond语法。下载地址 http://www.helicontech.com/download-isapi_rewrite3.htm 下载那个ISAPI_Rewrite Lite for Windows NT4/2000/XP/2003 (Freeware!)即可,免费软件! 安装好后,规则和apache类似。直接复制到httpd.conf文件中即可,有的系统也可能叫httpd.ini。
# 设置RewriteBase的值为你的商店目录地址 RewriteBase / RewriteCond %{REQUEST_FILENAME} !-f RewriteCond %{REQUEST_FILENAME} !-d RewriteRule ^(.*)$ index.php?$1 [L]
管理工具-> internet信息服务-> 网站 点右键. -> 属性 ISAPI筛选器看到下面那个ISAPI_Rewrite3了吧。 转到 rewrite的选项卡 可以看到里面的rewrite规则
设置mime类型,启用javascript加速
Nginx 请修改PATH为你的安装路径
location /PATH/ { if (!-e $request_filename) { rewrite ^/PATH/(.*)$ /PATH/index.php?$1 last; } }
webserver众多,我们仅列出具有代表性的几款
2.3.2. 搜索引擎优化(SEO)
2.3.3. 服务器配置
2.4. 操作技巧
2.4.1. 使用快捷键
快捷键 | 功能 |
---|---|
alt+1 | 商品 |
alt+2 | 订单 |
alt+3 | 会员 |
alt+4 | 营销推广 |
alt+5 | 站点管理 |
alt+6 | 统计报表 |
alt+5 | 站点管理 |
esc | 关闭对话框 / 关闭列表详细 |
R | 刷新主区域 |
U | 切换左侧导航 |
H | 打开使用向导 |
/ | 定位到搜索框 |
按下 Ctrl 点击鼠标拖动列表页 | 将航向滚动列表到拖动方向 |
2.4.2. 使用条码扫描器
2.5. 业务成长之后...
当你需要丰富的权限分配,工作流,来规范你的业务流程的时候。可以选用shopex 提供的订单处理中心系统。
当你需要扩展网店系统功能来支撑精细化的销售运作的时候。你可以寻找一些有技术实例的团队来解决这个问题。当然你也可以组织自己的技术部门,把本手册第三章后面的部分扔给他们就好了。shopex内建的插件体系足够让他们满足你的要求。
2.6. 升级方法
现在的升级逻辑很简单
先跑php 然后调用diff
先跑php 是让数据库里的东西可以跟着变过来
调用数据库diff 去处理 php没搞完的杂事
3. 扩展shopex
扩展有两种办法,一种是通过增加插件。另一种是采用我们称之为 de>"第三方开发方案"de>的方式。
- shopex对插件的设计目标是语法简单,并且可以很方便的将成果发布,传递出去。
- 第三方开发方案的特点是,无所不能,功能强大。但实现稍微复杂。
除了一些基本的模板改动之外,其他都要求开发者要基本的懂得PHP语法,并且有一些最基础的面向对象编程的知识。函数不熟可以去查手册,大多数时候,只要会改的技能就可以了。
3.1. 插件体系
shopex系统支持13种插件,其中促销方式可能在会后面的版本有大的改动。
插件名 | 类型 | 标示名 | 默认路径 |
---|---|---|---|
用户登录插件 | - | passport | plugins/passport |
图片存储方式插件 | - | storager | plugins/storager |
支付方式插件 | - | payment | plugins/payment |
促销方式差将 | - | pmtScheme | plugins/pmtScheme |
网店机器人动作插件 | - | action | plugins/action |
数据导入导出插件 | - | dataio | plugins/dataio |
用户消息插件 | - | messenger | plugins/messenger |
单独页面布局插件 | - | layout | plugins/layout |
地区数据插件 | - | location | plugins/location |
网页挂件 | - | widgets | plugins/widgets |
商品插件 | - | schema | plugins/schema |
前台功能插件 | - | shop | - |
后台功能插件 | - | admin | - |
从插件的文件组织方式上可以分成de>目录型de>和de>文件型de>两大类插件。
- 文件型插件:每个单独的php文件就是一个插件,文件名和其中的类的名称要符合一下规则:
- 文件名: <标示名>.插件名.php
- 类名: <标示名>_插件名 例如: 支付插件taobao的文件名是:de>payment.taobao.phpde>, 对应的文件内容是:
<?php class payment_taobao { var $name = ... //插件名称 var $version = ... //版本号 ... } ?>
可以被放置在除目录型插件里面的任何plugins下的任何位置。到后台刷新插件数据库时,就会被发现。
- 目录型插件: 因为有些自己的资源,所以被设计成独立的一个目录,其中必须放置一个主插件php文件,文件的命名规则和类的内容都和de>文件型插件de>相同。 一个目录下只能放置一个主文件。其余文件都作为这个主文件的资源存在。 可以被放置在除目录型插件里面的任何plugins下的任何位置。
在4.8.5版本之后,不同类型插件不需要放在各自类型的目录里。只要放在plugins下面就都可以被扫描到。这是为下一步app机制作的准备。
3.1.1. 用户登录插件(passport)
文件型
可以集合msn passport, google account, openid等统一登录系统
<?phpclass passport_phpwind extends modelFactory { var $passport_name = "PhpWind论坛V6.3.2"; //整合论坛名称 var $passport_memo = "描述"; //整合论坛描述内容 var $_config = null; //初始化配置变量 var $forward = 0; //初始化跳转链接变量 var $name = "phpwind"; //用户登陆插件名称 /** * setConfig,发送时调用 * 必有方法,config参数为getOptions取得的所有项的配置结果 */ function setConfig($config) { $this->_config = $config; } /** * verifylogin,用户登陆验证 * 可选方法,login参数为用户名,passwd参数为用户密码 */ function verifylogin($login,$passwd){ } /** *decode,解码 *可选方法 */ function decode($responseData){ } /** * getoptions * 必有方法,取得的所有项的配置结果 * 返回一个配置结果的数组 */ function getoptions(){ return $options; } /** * login,用户登陆 * 必有方法,userId参数为用户Id号,rurl参数为返回的跳转链接 */ function login($userId, $rurl) { } /** * regist,用户注册 * 必有方法,userId参数为用户Id号,rurl参数为返回的跳转链接 */ function regist($userId,$rurl) { } /** * logout,用户退出 * 必有方法,userId参数为用户Id号,rurl参数为返回的跳转链接 */ function logout($userId,$rurl) { } /** * _StrCode,编码/解码 * 可选方法,string参数为需要进行处理的字符串, * action参数表示对数据进行ENCODE编码还是DECODE解码 */ function _StrCode($string,$action='ENCODE'){ } /** * ClientUserAction * 必有方法,action参数为作为客户端操作动作, * userdb参数为数据库名,forward参数为跳转链接 */ function ClientUserAction($action,$userdb,$forward=''){ } /** * StrCode,编码/解码 * 可选方法,string参数为需要进行处理的字符串, * action参数表示对数据进行ENCODE编码还是DECODE解码 */ function StrCode($string,$action='ENCODE'){ } /** * ServerClient * 必有方法,action参数为作为服务器端操作动作 */ function ServerClient($action){ } /** * getPlugCookie,取得插件Cookie * 可选方法,action参数为作为服务器端操作动作 */ function getPlugCookie(){ } /** * setPlugCookie,设置插件Cookie * 可选方法,val参数为设置插件Cookie的值 */ function setPlugCookie($val){ }}?>
3.1.2. 图片存储方式插件(storager)
文件型
如果你去查看shopex系统里存放商品图片的数据表sdb_gimages,会发现图片存放是一个竖线分割的字符串, 规则如下:
网址|资源定位|存储方式
比如:
images/goods/01/02.jpg|goods/01/02.jpg|fs_storager
为什么要这样设计?
我们把这个字符串叫storager格式,他的好处是可以方便的使用多种存储后端,shopex可以把图片存在ftp上,存在flickr里,存在amazon的s3里,或者存在memcache里。 在此分段解释一下各部分的使用方式:
- 第一个段是用来在前台访问的:就是url,可以是绝对地址,也可以是相对地址。
- 第二段是存储代码用来定位资源的:比如是flickr里的图片id, ftp服务器里的相对资源
- 第三段是图片存储器的类型表示, 代码会定位到de>plugins/function/<存储类型>.phpde>
规则很简单,只取第一个竖线前面的部分,我们通过一个smarty插件做storager格式和url的转换:
<img src="$storager_str|storager" alt="" />
如果是远程图片,或者是从其他系统导入的数据,就不会有竖线。storager过滤器就把整条数据都当第一段来处理,以此保证这个结构的兼容性。 第二段和第三段是用来管理资源的当对这个图片进行删除,修改等操作时,系统会找到由第三段定义的存储插件,然后用插件通过第二段进行定位进而对资源进行操作。下面贴一个使用nginx + Tokyo Tyrant进行存储的方案: nginx的NginxHttpMemcachedModule有个很牛X的特性,可以使用url作为memcache的key进行资源访问。这使得建造分布式的图片存储集群变得简单,同时,性能也有大规模提升。
使用内存做存储有两个缺点,一是有容量限制,二是不能永久存储。因此我们使用 Tokyo Tyrant 做存储方案。Tokyo Tyrant是Tokyo Cabinet的网络接口,可以使用memcached一样的协议。同memcache相比,Tokyo Cabinet可以将资源存放在硬盘中,可以实现互备,可以作为永久存储方案。
实施分两部分,首先配置软件端,我们增加一个shopex的存储插件 – tt_storager 在其中实现向tt服务器的存储部分,很简单:把它当memcache用就可以了。
function save($file,&$url,$type,$addons){ $id = $this->_get_ident($file,$type,$addons,$url,$path); if($path && $this->memcache->set($path,file_get_contents($file))){ $this->memcache->delete($file,10); return $id; }else{ return false; }}
接着配置nginx,让他可以对那个访问的url到tt上取资源:
#这里可以配置n组tt_serverupstream tt_server1{ #配置一个tt服务器组 server 192.168.2.35:11211; server 192.168.2.36:11211; #双主互备} server { server_name image.example.com; listen *:80; location / { set $memcached_key $uri; memcached_pass tt_server1; }}
接下来,搭建一组这样的nginx服务器,再配合lvs,nginx集群+tt集群,一个高性能,高可用互备的图片集群就建立起来了。
3.1.3. 支付方式插件(payment)
文件型
<?phprequire('paymentPlugin.php');class pay_xxxx extends paymentPlugin{ var $name = 'xxx'; //支付方式名称 //支付方式logo名称, //logo图片保存在\plugins\payment\images下, //命名规则比如:logourl.gif var $logo = 'logourl'; var $version = 20050615; //支付方式版本 var $charset = 'gb2312'; //编码设置 //订单提交正式交易地址 var $submitUrl= 'https://www.czwin.com/Payment.aspx'; //提交按钮的图片地址 var $submitButton = '提交按钮url'; //支持支付的货币类型,定义的数组中货币类 //型键值根据支付网关给出的技术文档来决定 var $supportCurrency = array("CNY"=>"RMB", "USD"=>"02"); //支持支付的地区 var $supportArea = array('AREA_CNY','AREA_USD'); var $desc = ''; //支付方式描述 var $intro = ''; //支付方式介绍 var $M_Language = "1"; //语言选择,表示商家使用的页面语言 //在添加支付方式时,下拉列表的排列顺序(越小越靠前显示) var $orderby = 29; var $head_charset = "gb2312"; //编码设置 //支持真实的外币交易 var $cur_trading = true; /** * toSubmit,向支付网关发出支付请求 * 必有方法,payment参数为需要提交的订单详细信息 */ function toSubmit($payment){ return $return; } /** * callback,支付网关向系统发出支付应答 * 必有方法,返回四种状态: * 1.PAY_ERROR:签名认证失败; * 2.PAY_SUCCESS:支付成功; * 3.PAY_FAILED:支付失败; * 4.PAY_PROGRESS:交易处理中; */ function callback($in,&$paymentId,&$money,&$message,&$tradeno){ return PAY_ERROR; } /** * getfields * 必有方法,返回所需要配置的信息数组 */ function getfields(){ return array( 'member_id'=>array( 'label'=>'客户号', //配置信息标签 'type'=>'string' //配置信息类型 ), 'PrivateKey'=>array( 'label'=>'私钥', 'type'=>'string' ) ); } /** * applyForm,支付方式申请的表单 * 可选方法,agentfield参数为申请表单需要提交的信息 * 返回一个支付方式申请的表单 */ function applyForm($agentfield){ return $tmp_form; }}?>
3.1.4. 网店机器人动作插件(actions)
文件型
<?phpclass action_order{ var $name = '订单触发器可用动作'; //动作名称 var $action_for = 'trading/order'; //绑定的特殊模块 /** * actions,机器人执行动作 * 必有方法,返回机器人执行动作数组 */ function actions(){ return array( 'addPoint'=>array( 'label'=>'增加积分', //执行动作标签名称 //执行动作的内容,以及该内容的类型 'args'=>array('点数'=>array('type'=>'number'))), 'delPoint'=>array('label'=>'扣除积分' ,'args'=>array('点数'=>array('type'=>'number'))), 'addCoupon'=>array('label'=>'送订单优惠券' ,'args'=>array('优惠券'=>array( 'required'=>true, //引用对象的特殊模块 'type'=>'object:trading/coupon', 'filter'=>'cpns_type=1&ifvalid=1') )), ); } /** * addPoint,根据actions方法返回的动作命名的函数 * 必有方法,data参数为订单信息,point参数为变化的积分 */ function addPoint($data,$point){ ... } /** * delPoint,根据actions方法返回的动作命名的函数 * 必有方法,data参数为订单信息,point参数为变化的积分 */ function delPoint($data,$point){ ... } /** * addCoupon,根据actions方法返回的动作命名的函数 * 必有方法,data参数为订单信息,coupon参数为优惠券信息 */ function addCoupon($data, $coupon){ ... }}
de>actionsde>返回的数组,在网店后台工具箱->网店机器人->订单->添加规则中,“执行动作”的下拉列表(见下图示),下拉列表的选择项就是返回数组的'label'标签名称。后面的输入框是根据返回数组的键args来判断,假如'type'类型是 number,则显示文本框,假如'type'类型是引用特殊模块,则根据特殊模块来显示内容。
3.1.5. 数据导入导出插件(dataio)
文件型
<?phpclass io_txt{ var $name = 'txt-制表符分隔的文本文件'; //数据导入导出插件名称 /** * export_begin * 必有方法,keys参数为导出文件类型的键名数组, * type参数为导出文件类型,count参数为导出数据数量 */ function export_begin($keys,$type,$count){ //文件下载方法,其中第一个参数为文件名 download($type.'-'.date('Ymd').'('.$count.').txt'); //以制表符为间隔来输出每一个键名 echo implode("\t",$keys)."\r\n"; flush(); //刷新输出缓冲 } /** * export_rows * 必有方法,rows参数为需要导出所有行 */ function export_rows($rows){ foreach($rows as $row){ foreach($row as $k=>$v){ $row[$k] = str_replace("\n",'\n',$v); } //以制表符为间隔来输入每一行的每一列,并且最后换行 echo implode("\t",$row)."\r\n"; } flush(); } /** * export_finish * 必有方法,导出结束 */ function export_finish(){ }}?>
目前数据导入方法暂时不公布。
3.1.6. 用户消息插件(messenger)
目录型
<?phpclass messenger_email{ var $name = '电子邮件'; //名称 var $iconclass="sysiconBtn email"; //操作区图标 var $name_show = '发邮件'; //列表页操作区名称 var $version='$ver$'; //版本 var $updateUrl=''; //新版本检查地址 var $isHtml = true; //是否html消息 var $hasTitle = true; //是否有标题 var $maxtime = 300; //发送超时时间 ,单位:秒 var $maxbodylength =300; //最多字符 var $allowMultiTarget=false; //是否允许多目标 var $targetSplit = ','; //目标分隔符 var $dataname='email'; //数据库名 var $debug = false; /** * ready,准备发送时触发 * 可选方法,config参数为getOptions取得的所有项的配置结果 */ function ready($config){ ... } /** * finish,结束发送时触发 * 可选方法,config参数为getOptions取得的所有项的配置结果 */ function finish($config){ ... } /** * send,发送时调用 * 必有方法,to参数为发送对象,subject参数为消息主题, * body参数为消息内容,config参数为getOptions取得的所有项的配置结果 */ function send($to, $subject, $body, $config){ ... } /** * getOptions,取得配置信息 * 必有方法,返回配置信息的数组 */ function getOptions(){ ... }}?>
在网店后台,会员->邮件短信设置->电子邮件下的配置,就可以根据用户消息插件中de>getOptionsde>定义的配置信息来配置发送方式、发送地址等。
3.1.7. 单独页面布局插件(layout)
目录型
├─3-columns //三列 │ layout_3-columns.php //布局配置文件 │ preview.png //布局缩略图预览 │ layout.html //布局模板 │ ├─2-columns-left //两列,左为主 │ preview.png │ layout_2-columns-left.php │ layout.html │ ├─2-columns-right //两列,右为主 │ preview.png │ layout_2-columns-right.php │ layout.html │ └─1-column //单列 layout.html preview.png layout_1-column.php
layout.html
比如:在2-columns-left目录下的布局模板,布局:两列,左为主。
<div style="float:left;width:180px; overflow:hidden;"> <{widgets}></div> <div style="margin-left:190px; overflow:hidden;"> <{widgets}></div>
layout_2_columns_left.php
比如:在2-columns-left目录下的布局配置文件。
<?phpclass layout_2_columns_left{ var $name='两列,左为主'; //布局插件名称 var $slotsNum = 2; //布局列数}?>
在网店后台,页面管理->站点栏目->添加顶级栏目->选择单独页面->选择布局->设置布局(如下图示),就可以根据自定义的单独页面布局插件,来选择布局。
3.1.8. 地区数据插件(location)
目录型
<?phpclass local_difang{ var $name = '某地方'; //地区插件名称 var $desc = '地区描述'; //地区插件描述 var $maxdepth = 3; //地区最大深度为3层 /** * install * 必有方法,安装时调用,将area.txt文件内地区信息插入数据库表中。 */ function install(){ .... }}
3.1.9. 网页挂件(widgets)
3.1.9.1. 一步步做个widgets 基础篇
首先建立一个目录plugins/widgets/helloword 这个就是新的挂件目录,系统的每个widgets都是一个目录
里面放两个文件:
widgets.php:
<?php$setting['author']='我'; //挂件作者$setting['name']='Helloword'; //挂件名称$setting['version']='0.0.1'; //挂件版本$setting['catalog']='我的原创'; //挂件目录$setting['description'] = '这是我自己的helloword板块'; //挂件描述?>
default.html:
<h1>HelloWorld</h1><br />
这时目录看起来这个样子:
我们到后台可视化模板编辑里面添加板块。
保存,前台就能看到了
添加一个输入框
下面我们来改进这个板块:在后台输入一串文字,在前台显示出来。
增加一个配置文件_config.html 这个文件名是定死的,只要存在这个文件,那么就会把这个文件内容放到后台的版块编辑里面
_config.html
输入我的文字: <input type="text" value="<{$setting.string_1}>" name="string_1" />
注意:这个输入框的value在这里设置的是<{$setting. string_1}> , 这样在版块编辑的时候,才会作为把你上次填的文字放在输入框里我们在这里做的每个input/select元素,都会作为$setting这个变量的一部分
编辑的时候就是这个样子:
下面把输入的字符串在前台显示出来:修改default.html
<h1>Hello:<{$setting.string_1}></h1>
这时前台的样子:
加入控制程序
再在这个目录下添加一个 widgets_helloword.php,这里的文件和php函数,要用widget_ + 目录的名字(helloword)命名
widget_helloword.php
<?phpfunction widget_helloword(&$setting,&$system){ return '<b style="color:red">'.$setting['string_1'].'</b>';}?>
下面修改default.html,让他输出通过php修改过的文字。只要把<{$setting.string_1}> 改成<{$data}>即可。
default.html
<h1>Hello:<{$data}></h1>
写到这里发现写错字了,helloword... 只好继续"你好单词"下去...
前台表现:
3.1.9.2. 一步步做个widgets 实战篇
很多人做挂件的动机一般都是看到一个比较好效果或者是对官方提供的功能进行更深入的修改。比如看到了一个很好的效果,想把他用到自己的网店上,同时也可以在后台维护,下面的一个效果挂件,就是出于这样的一个动机来制作的。
原始的效果:http://www.stoutlabs.com/demos/slider_class/ 感觉这个带有数字控制的滑动的效果还是不错,想把它改成的ShopEx的挂件,只要店主在后台设置了图片的路径和这个图片上的链接就可以实现这个广告效果。
下面的图帮你来温习一下挂件的制作基础。
首先要想清楚,挂件里面有些什么东西是需要店主设置的: 1、图片的路径,这个当然需要店主自己改了,否则就不能自己添加广告了。 2、点击图片所链接的地址,这个也是必须的。 3、每一个图片停留的时间,是让浏览者看3秒还是看5秒,这个也是需要店主自定义的 4、图片滑动的时间,就是从一张图片向另外一张图片切换的时候所需要的时间。 5、数字的颜色和背景,因为是带有数字的,所以就有当前的图片的数字和非当前的图片数字,这两个地方的颜色都需要用户可以设定的。
然后看看,上面看到的那个效果的HTML和js是不是支持上面所理想中设定,当然,这个地方需要你了解一些js了。看了一下这个效果整个的js和HTML结构,ok,没有问题,都是可以支持的。把这些定下来以后,就可以去制作_config.html了。
制作_config.htm文件的时候,可以直接从一个类似的官方挂件中复制一个过来,这样有些现在的html和js都是可以使用。比如从exchangeeffect这个挂件中把_config文件复制过来。现在还是对这个文件动手了。在制作_config.htm的时候,也不需要考虑太多,凡是需要店主定义的地方,统统写成 <{$setting.XXXXX}>的格式。
制作好_config.htm以后,在后台插入这个挂件的时候,就可以看到大概的样子了。
然后接下来制作default.html,这个就是这个挂件的主区域。按部就班的把js和html调整好,保证在静态页面的情况下(就是没有嵌入到ShopEx系统中),可以正常使用。
上面的步骤全部做好以后,就可以使用这个挂件了,简单的预览如下:
3.1.10. 商品插件(schema)
目录型
─custom │ schema.custom.php │ icon-48x48.png │ └─view init.html
商品可以定义商品的发货函数可以实现点卡自动充值,机票的代理自动化等等甚至向工厂自动传送用户的下单信息。
schema.custom.php
<?phpclass schema_custom{ var $name='自定义商品类型'; //商品插件名称 var $version='11689$'; //商品插件版本号 var $use_brand = true; //本类型商品是否有品牌 var $use_params = false; //本类型商品是否有参数表 var $use_props = true; //本类型商品是否有属性 var $use_minfo = false; //本类型商品是否有用户购买时的必填信息 /** * init,自定义商品类型初始化 * 可选方法 */ function init(&$post){ if(!isset($post['is_physical']))$post['is_physical'] = true; return true; } }?>
3.1.11. 前台功能插件(shop)
<?phpclass shop_helloworld{ var $name = 'hello'; function index(){ echo 'hello'; } }
3.1.12. 后台功能插件(admin)
<?phpclass admin_helloworld{ var $name = 'hello'; var $workground = 'tools'; function index(){ echo 'hello'; } }
3.2. 软件功能包(app)
即将出现在485正式版
我们提供了一个很干净的机制让开发者扩展网店的功能,实现以下特性:
- 建立自己的数据库表
- 创建自己的控制器
- 在前后台增加栏目
- 用自己的控制器替换系统默认的(自定义业务流程)
- 添加事件侦听器,使得系统事件时调用自己的代码
可以看到,这基本相当提供给大家一个简单的php框架了。下面说下技术细节。
每个程序包占一个目录。 我们推荐将所有的包都放置在de>plugins/appde>文件夹下。下面用个例子, 阐述下功能包的各个控制点。假设这个文件夹的名字为:demo, 包里面必须有一个 app.demo.php。并且里面必须含有一个 app_demo的类。该类需要继承shopex_appdef这个基类,例如:
plugins/app/demo/de>app.demo.phpde>
<?phpclass app_demo extends shopex_appdef{ var $ver = 0.8; var $name='样例程序'; var $website = 'http://www.shopex.cn'; var $author = 'dev@shopex.cn'; //可选函数 //定义接管系统哪些流程,由自身的哪个类/方法去执行 //本例表示,启用后前台将把所有访问购物车的控制器请求重定向 //到本软件包内 democtl对象的cartidx方法里。 function ctl_mapper(){ return array( 'shop:cart:index' => 'demo_ctl:cartidx', ); } //可选函数 //侦听系统哪些事件 //此处可用关键字any表示所有事件 //本例表示: //侦听订单新建事件 -> 调用event_handle类的order_new方法执行 //侦听会员新建事件 -> 调用event_handle类的member_create方法执行 function listener(){ return array( 'trading/order:create' => 'demo_event_handler:order_new', 'member/account:register' => 'demo_event_handler:member_create', 'any'=>'demo_event_handler:any', ); } //可选函数, 返回需要建表的信息 //本例是建立两个表, 系统会自动加前缀 sdb_<ident> function dbtables(){ $tables['table_2'] = array ( 'columns' => array ( 'controller' => array ( 'type' => 'varchar(100)', 'required' => true, 'pkey' => true, 'editable' => false, ), 'plugin' => array ( 'type' =>'varchar(100)', 'required' => true, 'editable' => false, ), ), ); $tables['table_1'] = array ( 'columns' => array ( 'controller' => array ( 'type' => 'varchar(100)', 'required' => true, 'pkey' => true, 'editable' => false, ), 'plugin' => array ( 'type' =>'varchar(100)', 'required' => true, 'editable' => false, ), ), ); return $tables; } //我承认这是个非常邪恶的设计... //但你要承认它可以让你无所不能 function output_modifiers(){ return array( 'admin:goods/product:index'=>'demo_modifiers:product_edit' ); } //重载安装时的方法... //同样可重载的还有: // uninstall -> 卸载 // enable -> 程序启动 // disable -> 程序关闭 function install(){ //别忘了调用父类的install return parent::install(); } }
为了防止命名冲突,请使用自己包的名字作为类的前缀。
plugins/app/demo/de>demo_event_handler.phpde>
<?phpclass demo_event_handler{ //订单新建时本方法将被自动执行 //此处event被赋值为 order:new function order_new($event_type,$order_data){ ... } //会员新建时本方法将被自动执行 //此处event被赋值为 order:new function member_create($event_type,$member_data){ ... } //任何事件都将调用此函数 function any($event_type,$event_data){ ... }}
建立一个类来重定义购物车页面
plugins/app/demo/de>demo_ctl.phpde>
<?phprequire('app_page.php');class demo_ctl extends app_page{ function cartidx(){ ... //输出模板:软件包文件夹里的cart.html $this->system; //可以调用系统入口 $this->db; //可以直接使用数据库 //载入包里的类... require(dirname(__FILE__).'/demo_my_model_layer.php'); $obj = new demo_my_model_layer; $this->output('view/cart.html'); } }
建立一个类来重定义购物车页面用关键字de><{$_BASE_PATH_}>de>可以定位到插件文件夹的url。
plugins/app/demo/de>view/cart.htmlde>
<p>例如输出<br />plugins/app/demo/images/cart.png</p> <img src="<{$_BASE_PATH_}>/images/cart.png" />
过滤器,将商品列表页面的“新建商品”按钮上的文字换掉。
plugins/app/demo/de>demo_modifiers.phpde>
<?phpclass demo_modifiers{ function product_edit( &$content ){ return str_replace('添加商品','别看我是只羊!',$content); }}
在第一次运行时会进行初始化。本例初始化时出现的信息是:
欢迎来到样例程序初始化向导
即将初始化 样例程序 0.8 到你的网店系统中。
注意!本应用程序将:
- 向数据库里安装2个表: sdb_demo_table_2 , sdb_demo_table_1 。
- 接管你的查看购物车流程。
- 监督订单新建事件。
- 监督会员注册事件。
- 监督所有事件。
是否继续安装流程?
3.3. 使用二次开发接口
定制可以根据客户的需求对网站进行相应功能的添加修改或者删除,同时定制也存在一定的弊端。ShopExV4.8以前版本的定制是在原来的程序上修改的所以定制过的网站就不能使用该版本后发布的相关补丁。 ShopExV4.8版本采用MVC开发模式,二次开发解决了定制在原程序上进行修改导致程序不能升级的问题,使新的程序模块可以很好的融合到ShopExV4.8系统中同时也可以继承原有程序的所有功能。
本着不与原程序冲突的原则,需要新建一个目录去存放二次开发所用的程序,这就要求在ShopExV4.8的配置文件中定义一个存放二次开发程序目录的常量。同时为了使二次开发程序能够兼容原程序的所有功能也要求要包含原来的控制器文件或模型层文件,通过类继承和函数重载的方式实现原有功能的保留、修改和新功能的开发,当然如果该功能完全与原有功能没有联系则只需继承控制器文件或模型层文件的基类。
- 配置config.php文件:
define(‘CUSTOM_CORE_DIR’,’自定义文件路径’)
自定义文件路径建议和core同级
- 后台菜单新增规则(customSchema.php): 格式参照原有后台菜单文件的书写格式但数组名必须为$cusmenu 新增菜单项 此处格式参照adminSchema.php即可,数组名称注意应为$cusmenu 在已有菜单项中添加根据菜单出现的位置添加不同的参数 如在“统计报表”下新增二级菜单“测试二次开发”
$cusmenu['analytics']=array( 'items'=>array( array( 'type'=>'group', 'label'=>'测试二次开发', 'position'=>'after|begin|end|before', 'reference'=>'访问统计', 'items'=>array( array( 'type'=>'menu', 'label'=>'测试二次开发1', 'link'=>'index.php?ctl=vip/vote&act=index' ), array( 'type'=>'menu','label'=>'测试二次开发2', 'link'=>'index.php?ctl=vip/vote&act=index' ) ) ) ) );
position值及说明:
- after:在某个菜单项的后面,此时reference必须为一个同级已存在的 菜单项。如上述例子中为“访问统计”则“测试二次开发”出现在”访问 统计”的下面
- before:同I,只是位置在前
- begin:在同级目录的最前面,refernce可以为空
- end:在同级目录的最后面,refernce可以为空
- 在三级菜单中插入某菜单项同上述二级菜单的操作
- 控制器文件: 控制器文件必须以cct命名
- 继承原有控制器文件,前提是原控制器文件必须存在
cct_原控制器名称 extends ctl_原控制名称{ function 新增函数名(){ //新增功能函数 } function 原有函数名(){ //函数重载 }}
- 新建控制器后台控制器文件
cct_control extends adminPage{ function __construct(){ //自定义操作 } //自定义函数}
或者
cct_control extends objectPage{ function __construct(){ //自定义操作 } //自定义函数}
前台控制器文件
cct_control extends shopPage{ function __construct(){ //自定义操作 } //自定义函数}
- 模型层文件: 模型层文件必须以cmd开头命名,继承原有模型层文件,前提是原模型层文件必须存在
cmd_原模型层名称 extends mdl_模型层名称{ function 新增函数(){ //新增函数 } function 原函数名(){ //重载函数 }}
新建模型层文件
cmd_model extends shopObject{ function __construct(){ //自定义操作 } //自定义函数 ...}
或
cmd_model extends modlFactory{ function __construct(){ //自定义操作 } //自定义函数 ...}
3.3.1. 案例A: 更改友情链接页面显示个数
前台友情链接页面默认每页显示的友情链接为10个,大多数店主会觉得这个数字太少导致页面太空,所以想修改此处的数字为30或者更多。要实现这个修改,首先设置好二次开发目录。 然后在二次开发目录下新建目录。 shop/controller
然后在此目录下建立文件cct.link.php继承原始的ctl_link类,之后重载控制器函数。
内容为:
<?phpclass cct_link extends ctl_link{ function showList($page=1){ $sitemapts=$this->system->loadModel('content/sitemap'); $title=$sitemapts->getTitleByAction('link:showList'); $title=$title['title']?$title['title']:'友情链接'; $this->path[]=array('title'=>$title); $this->title = $title; $pageLimit = 100; $oLink=$this->system->loadModel('content/frendlink'); $result=$oLink->getList('*', '', ($page-1)*$pageLimit,$pageLimit,$linkCount); $this->pagedata['pager'] = array( 'current'=>$page, 'total'=>ceil($linkCount/$pageLimit), 'link'=>$this->system->mkUrl('link', 'showList',array(($tmp = time()))), 'token'=>$tmp); if($page > $this->pagedata['pager']['total']){ trigger_error('查询数为空',E_USER_NOTICE); } $this->pagedata['data'] = $result; $this->output(); }}
将其中第九行 $pageLimit = 100; 改为您要的数字就行了。
3.4. 自定义核心流程页面模板
相比大家都感觉48只能修改框架而感到束手束脚了吧很多东西只有修改de>core/shop/viewde>下的html文件才可以实现,而这些都是核心的东西。模板又无法控制~~
其实有办法的:可以在模板目录下 建一个view文件夹。 然后按照de>core/shop/viewde>同样的路径,建立新的html文件。系统输出的时候就会优先调用模板里自定义的核心业务html部分了
例如:修改会员后台的样式需要修改de>core/shop/view/member/main.htmlde>才能实现。
现在自己做了一个模板叫myth,模板目录在de>themes/mythde>。就可以建立一个,把原来的de>core/shop/view/member/main.htmlde>文件复制de>themes/myth/view/member/main.htmlde>到想要自定义的细节样式,通过修改新的de>themes/myth/view/member/main.htmlde>文件即可实现。
现在,你可以随心所欲的控制样式了。
注意! 这样的话,模板就不能跟随升级了,因为里面有自定义的核心业务所需的模板。很可能你修改过的member/main.html里面的一些变量在新版本里已经失效。于是,这需要你手动修改member/main.html文件来跟随系统升级。因此我们建议,你可以自用,但尽量不要使用自定义的核心业务区域html去做通用模板。否则,需要你亲自升级。当然,你也可以为这个服务提高模板价格....
自定义导入导出介质,支付方式,配送方式,促销规则,促销页面模板等等扩展接口…
4. 系统探秘
4.1. 系统结构
以下文档均以最新的shopex 4.8.5 为准。
程序为mvc 3层结构。模型-视图-控制器。
- 模型是前后台公用。完成业务逻辑,所有的数据库操作,文件等资源调用,都由此层实现。
- 控制器则是前后台独立。负责业务模型的调用,拼接,变量的转换等等。
- 视图层采用类似smarty的实现。
在任何时候都有一个全局变量$system指向唯一的一个内核类:core/kernel.php
结构图
公共类图
公共函数库位于/core/func_ext.php,它在内核加载时首先被加载,对全系统有效。其中包含若干php5函数的php4模拟版本:
- file_put_contents
- json_encode
- json_decode
- ftp_chmod
- array_diff_key
- http_build_query
这是一个结构图这里是前台的控制器,视图文件这里是后台的控制器,视图文件这里是后台的smarty插件文件这里是前后台公用的smarty插件文件
我们的逻辑是这样的。
控制器负责所有的变量的转换,包括对外界的变量转换。像胶水一样粘合各个业务model,并在其中的数据交互过程中做一些数据转换过程。最终将变量抛到模板层。模板层负责编译成html输出,这里我们创建了大量的smarty插件来减少开发者的重复工作量。
model层负责所有的资源操作,包括数据库和文件系统以及读取网络等等。
业务模型层下面是schema,这些是对数据库表结构的最底层的描述。
我们将从底向上的介绍这些层次
4.1.1. 数据库结构定义文件
在485之前,我们使用powerdesigner来管理这些表结构,但是在一些项目的版本管理过程中,有时需要维护多个版本。powerdesigner的储存文件为一个单一的巨大的xml格式pdm文件。这在进行版本的合并分支时几乎无法处理。而在485里,我们使用php去描述表结构有了以下几个优势:
- 数据库定义具有版本管理特性
- 各分支开发互不干扰
- 协同更改时,可以进行合并操作
- 针对单表的版本还原与操作日志
- 容易做自动化的表结构对比
- 更快速的getInsertSQL/getUpdateSQL: 不需要select * from xxx where 0=1
- 插件可以有自己的表定义文件
- 为model::getColumns自动化做准备
- 按照用途来定义字段类型,不会出现同一种数据类型而字段定义不一致的情况
- 简单的外键实现,方便的统计表的依赖关系,使得外键字段定义完全一致
- 更好的mysql版本兼容:type = MyISAM DEFAULT CHARACTER SET语法
- 很方便的实现gbk/utf8多版本
现在这些数据库定义文件放置在 core/schemas 下面,每个表一个php文件, 样例:
<?php$db['table_name'] = array( 'columns'=>array( 'col_1'=>array('type'=>'int', 'pkey'=>true,'comment'=>'id字段'), //主键 'col_p1'=>array('type'=>'int', 'pkey'=>true,'comment'=>'主键2'), //联合主键 'col_33'=>array('type'=>'mediumint unsigned'), //当然也支持通常的type 'col_2'=>array('type'=>'email', 'comment'=>'id字段'), //email = varchar(255) 'col_3'=>array('type'=>'money', 'notnull'=>true,'default'=>3), //money = decimal(20,3), 非空字段,默认为3.000 'col_4'=>array('type'=>'html', 'comment'=>'id字段'), //html = text 'col_4'=>array('type'=>'bool', 'comment'=>'id字段'), //bool = enum('true','false') 'table2_c1'=>array('type'=>'table_2:c1', 'comment'=>'表table_2的c1字段'), //类型和table_2的c1字段相同, 当外键处理,自动加索引 ), 'index'=>array( '...',//再定义 ) 'engine'=>'heap'//可不填,默认为myisam 'option'=>''//其他扩展语法);
好了,现在一切的表结构都可以被系统精确控制了。利用这个东西,我们实现了485里的数据表结构比较功能,并且以这个为基础建立了新的升级机制。并且在未来,这将是app体系的一个基石,
关于app体系,我将在后来的章节讲解,这将是一个神奇的东西。
4.1.2. 网店对象
在model里面,有一些特别特别的类是基于shopObject这个类的。 shopObject用来表示一些有共性和意义的对象,比如商品,订单,会员。有一些抽象的共用方法。
每个shopObject都有一个绑定的表,并且标明了哪些字段是主键。因此我们可以做一些批量的东西。后台的订单列表,商品列表,会员列表等等。我们给他取名为finder。在此向osx致以敬意。
函数名 | 参数 | 返回值 | 说明 |
---|---|---|---|
is_highlight($row) | 行数据 | bool | 用来判断是否将该行高亮 |
因此如果你想修改这些默认行为,比如在列上加些自定义的字段,而这个字段可能完全不是数据库里,而存在于其他某外部系统中。那么很简单,你只需要重载对应的方法即可。
- 你可以去思考将这个重载放在哪一层。如果希望所有调用这个数据的地方都变化,那么就重载model。
- 如果只是希望后台的列表增加一个列,就重载他的控制器即可
shopObject还有一个特性,就是支持事件广播。他有一个特殊的方法:shopObject::fireEvent($eventName) 用来广播事件。其他对象采用观察者模式listen到感兴趣的对象上。
当订单新建时,系统会调用相应的de>$orderModel->firevent('create')de>方法,这时所有关注order-create的函数就全都被纷纷调用,有的负责发邮件通知用户,有的负责记录的数据库,有的可能是插件写的:将这些事件广播道某esb系统里。
这时一条网店系统的数字神经,当订单新建,或者会员登录的时候。我们可以通过外挂的脚本做一些触发事件。这也是485网店机器人的基础。
4.1.3. 业务模型(model)
模型是前后台公用。完成业务逻辑,所有的数据库操作,文件等资源调用,都由此层实现
模块调用方法: de>$systme->loadModel(path/to/a/model);de> 载入过程如图所示:
4.2. 运行过程
ShopEx前后台调度都分别通过各自目录下一个名为index.php的入口文件进行,入口文件接受控制器、方法等的输入参数,调度给对应的控制器方法执行并输出结果。 index.php会调用kernel。 Kernel基础类提供模型对象加载,插件入口,错误处理,设置管理,输入输出,第三方类库加载等基础服务。
前后台及安装入口程序在初始化时,都会初始化一个对应的内核类,这个类在前台是shopCore.php,后台时是adminCore.php。这两个文件都是继承de>core/kernel.phpde>文件。该内核类首先会形成一个名为system的指向自身的全局对象,然后根据传入参数调用对应的控制器方法运行。控制器层与模型层都会产生对该全局system对象的调用来使用基础服务。
并且控制器和模型层在自身被实例化之后都可以使用de>$this->systemde>来引用内核对象。
也就是说,构造函数中只能通过$GLOBALS['system']来访问。
4.2.1. 前台流程
对于前台来说,会有缓存的介入。
为方便rewrite,采用形如文件名的形式做为请求句柄,请求句柄包含了控制器名,控制器方法,类型及传入参数信息请求句柄语法:
CTL[-ARG…][-METHOD].<html|json|xml>
其中CTL为控制器. ARG部分为传入参数组, 可省略. METHOD为控制器方法,可省略,默认为index 为方便更改请求句柄的编码方式,请求句柄的编码与解析采取读取system.seo.parselink中定义的外挂函数(见插件小节外挂函数部分)处理,默认函数为actmapper:
前台控制器位de>/core/shop/controllerde>于前台的入口文件为de>index.phpde>,该页面初始化shopCore,初始化时运行父类kernel的构造方法设定system的指向自身的全局对象,最后构造函数调用de>shopCore->run()de>,流程如下:
说明入口程序 /index.php,判断de>config.phpde>是否存在,不存在则转至安装目录重新设定超级全局变量$_COOKIE为de>$_COOKIE[COOKIE_PFIX]de>的值;de>COOKIE_PFIXde>用以区分同一目录下安装的不同ShopEX实例。对传入URL进行处理,返回数组$this->request
array( //cache是否公用,用于用户登录或转换货币后界面 'cache'=>$cache, //请求句柄XXXX.html 'query'=>$query?$query:'index.html', //链接基地址(带?m/) 'base_url'=>$url_prefix.($domain?$domain.'/':''), 'url_prefix'=>$url_prefix, //链接基地址 'domain'=>$domain?$domain.'/':'', // m/或空 'member'=>$memberInfo, //用户信息数组 'cur'=>$cur, //货币 'lang'=>$lang //语言);
注:在会员登录后,链接将变为 de>index.php?/m/XXXX.htmlde>
判断传回的de>request['cache']de>:
if($request['cache']=='public' && !(defined('WITHOUT_CACHE') && WITHOUT_CACHE )){ //对公共cache进行处理}else($request['cache']=='private') //对私有cache进行处理}else{ 直接调用动态程序(流程5)}
输出缓存或重新生成和输出
调用请求句柄解析方法进行处理 if 请求句柄 = ‘index.html’ 返回array('controller'=>'index','method'=>'home','args'=>array(),'type'=>'html') else 读取system.seo.parselink中定义的外挂函数(见插件小节外挂函数部分)解析处理: 默认函数为actmapper->parse(处理逻辑见下节)
4.2.2. 后台流程
后台入口文件为de>/shopadmin/index.phpde>文件后台的文件链接形如
http://host/shopadmin/index.php?#ctl=<控制器>&act=<方法>[&p[0]...]
后台控制器位de>/core/admin/controllerde>于后台的入口文件为de>/shopadmin/index.phpde>,该页面初始化de>adminCorede>,初始化时运行父类kernel的构造方法设定system的指向自身的全局对象,最后构造函数调用de>adminCore->run()de>,流程如下:
4.3. 配置信息的存储
网店的配置信息分为两种。
- 安装数据:存放在de>config/config.phpde>里 通常存放软件安装配置,即当系统无法正常运行时也可以通过ftp进行修改的参数。安装配置文件在系统安装时依据de>config.sample.phpde>模板文件自动生成。 秘诀: 你也可以通过修改这个文件来覆盖系统中的一些常量。de>config.sample.phpde>内容如下:
<?php/** * 网店配置模板 * * 版本 $Id: config.sample.php 37215 2009-12-03 09:28:28Z flaboy $ * 配置参数讨论专贴 http://www.shopex.cn/bbs/thread-61957-1-1.html */ // ** 数据库配置 ** //define('DB_USER', 'usernamehere'); # 数据库用户名define('DB_PASSWORD', 'yourpasswordhere'); # 数据库密码define('DB_NAME', 'putyourdbnamehere'); # 数据库名 # 数据库服务器 -- 99% 的情况下您不需要修改此参数define('DB_HOST', 'localhost');//define('DB_PCONNECT',1); #是否启用数据库持续连接? define ('STORE_KEY', ''); #密钥define('DB_PREFIX', 'sdb_');define ('LANG', ''); define ('WITHOUT_CACHE',false); #启用触发器日志: home/logs/trigger.php//define ('TRIGGER_LOG',true);//define ('DISABLE_TRIGGER',true); #禁用触发器 /* 以下为调优参数 */define('DB_CHARSET', 'utf8');define('DB_COLLATE', '');define('DEBUG_JS',false);define('BASE_DIR', realpath(dirname(__FILE__).'/../'));define('CORE_DIR', BASE_DIR.'/core'); //安全模式启用后将禁用插件define('SAFE_MODE',false); #您可以更改这个目录的位置来获得更高的安全性define('HOME_DIR', BASE_DIR.'/home'); define('PLUGIN_DIR', BASE_DIR.'/plugins');define('THEME_DIR', BASE_DIR.'/themes');define('MEDIA_DIR', BASE_DIR.'/images');define('PUBLIC_DIR', BASE_DIR.'/public'); #同一主机共享文件define('CERT_DIR', BASE_DIR.'/cert');define('DEFAULT_LOCAL','mainland');define('SECACHE_SIZE','15M'); #缓存大小,最大不能超过1G//define('TEMPLATE_MODE','database');define("MAIL_LOG",false);define('DEFAULT_INDEX','');define('SERVER_TIMEZONE',8); #服务器时区//define('APP_ROOT_PHP','index.php'); #iis 5 @ini_set('memory_limit','32M');define('WITHOUT_GZIP',false); #前台禁ip//define('BLACKLIST','10.0.0.0/24 192.168.0.1/24'); #数据库集群.//define('DB_SLAVE_NAME',DB_NAME);//define('DB_SLAVE_USER',DB_USER);//define('DB_SLAVE_PASSWORD',DB_PASSWORD);//define('DB_SLAVE_HOST',DB_HOST); #支持泛解的时候才可以用这个, 仅支持fs_storager/* * define('HOST_MIRRORS', * 'http://img0.example.com, * http://img2.example.com, * http://img2.example.com'); */ #使用ftp存放图片文件//define('WITH_STORAGER','ftp_storager'); #确定服务器支持htaccess文件时,可以打开下面两个参数获得加速。//define ('GZIP_CSS',true);//define ('GZIP_JS',true); #可以选择缓存方式apc 或者 memcached//define('CACHE_METHOD','cacheApc');//======================================//define('CACHE_METHOD','memcached');//======================================#使用单个文件存放,稳定,但无法控制文件大小//define('CACHE_METHOD','cachedir'); /* 日志 *///define('LOG_LEVEL',E_ERROR); #按日期分目录,每个ip一个日志文件。扩展名是php防止下载。//define('LOG_FILE',HOME_DIR.'/logs/{date}/{ip}.php'); #log文件头部放上exit()保证无法下载。//define('LOG_HEAD_TEXT','<'.'?php exit()?'.'>'); //define('LOG_FORMAT',"{gmt}\t{request}\t{code}"); //======================================//define('WITH_MEMCACHE',true);//define('MEMCACHED_HOST','192.168.0.230');//define('MEMCACHED_PORT','11211');//====================================== #禁止运行安装//define('DISABLE_SYS_CALL',1); #使用数据库存放改动过的模板//define('THEME_STORAGE','db');
运行时的默认值存放在core/include/defined.php中。
- 业务数据:存放在系统的配置数据库里的 存放在数据库中,必须在系统可以正常运行时才能修改,大多是网店经营性质的配置。这个机制有点像windows的注册表,或者firefox地址栏里输入: about:config看到的东西。 ShopEx通过参数表的方式存放业务环节中的常用参数。参数表采用文件与数据库双重定义方式。默认值位于文件/core/include/setting.php,实际设定值通过sdb_settings表进行存储。对于每一个参数,存在一个存取用的key,对应它的有类型、值、说明。类型分为:
define('SET_T_STR',0); //textdefine('SET_T_INT',1); //numberdefine('SET_T_ENUM',2); //selectdefine('SET_T_BOOL',3); //booldefine('SET_T_TXT',4); //textareadefine('SET_T_FILE',5); //file
系统方法 $system->getConf() 负责取值,$system->setConf()保存值。系统在取值时,首先在数据库中查找对应的key,如果未找到,则在默认值文件中进行查找。
4.4. 模板系统
smarty风格的模板引擎。语法参照smarty的文档:http://www.smarty.net/manual/en/
前后台的公用的模板插件路径为:
core/include/smartyplugins
后台的专有模板插件路径为:
core/admin/smartyplugin
特别之处:
在4.8.5之后,我们重新编写了模板编译引擎。沿用一直以来的smarty语法。我们做了以下增强:
- 重写的正则表达式,编译时匹配效率更高
- 更严谨的字符串变量里的变量,避免语法模糊造成错误。
- 支持变量预绑定,我们把在很长时间内不变的变量,可以通过语法通知模板系统。使其直接输出目标值,而不是php代码。
- if等条件语句,会自动判断条件,如果其中的条件不包括可变变量(非预绑定的变量),则预先做条件判断。这几乎减少了一倍的目标代码量。
- 支持编译型的modifier插件
- 重新设计的简单实用的errorhandler
- include语法支持文件名后面加井号"#",来为同一模板生成不同的编译缓存
- 大量目标php代码的优化
- 更改缓存文件名为md5hash格式,使其在将来有希望归并到secache体系。
这个优化的原则就是把更多的插件改为编译时插件,然后通过精心设计的缓存控制系统去自动更新模板缓存.
这是一个完全重写的引擎。我们把这个类smarty的模板引擎取名为tramsy。实际检测,同样的代码,我们的tramsy生成的代码运行效率提高了50%以上。而如果用变量预绑定的方式传入变量,则可以使目标代码减少50%,运行效率提高80%。
我们会在将来的版本中更多的利用变量预绑定机制来提高性能。
4.5. 缓存机制
缓存系统的设计,直接决定了系统的运行效能。系统的无数bug,也可能由缓存而生。我们在此一起抽丝剥茧的看一下shopex的缓存设计方案。
shopex有3层缓存结构,按顺序触发:
层次 | 缓存名称 | 存放位置 |
---|---|---|
1 | 基于http协议的浏览器缓存 | 用户浏览器本地 |
2 | 前台全页缓存 | home/cache/cachedata.php |
3 | 模板缓存 | home/cache/front_tmpl |
4.5.1. 基于http协议的浏览器缓存
浏览器都很聪明,他们懒的把同样的数据从网上下载两次。
如果用户访问过这个网站,那么我们会在系统的header头里放一个Etag标签。这个Etag数据是和页面的内容相关的,如果页面内容变了,Etag也会不同。
第一次访问一个shopex页面,服务器的返回情况, 使用抓包工具可以看到http头如下所示。
HTTP/1.1 200 OKDate: Tue, 24 Nov 2009 09:14:27 GMTServer: Apache/2.0.59 (Win32) DAV/2 SVN/1.4.0 PHP/5.2.3X-Powered-By: PHP/5.2.3Cache-Control: privateExpires: Mon, 26 Jul 1997 05:00:00 GMTContent-Language: utf-8, zh-CNEtag: 4be41fcf3eb37261873e631daf4d3226
其中的Expires设置为1997年是为了让浏览器每次都来判断etag是否过期,因为我们是电子商务站点,一些促销规则,商品上下架对时间比较敏感。
最后一行就是Etag数据:de>4be41fcf3eb37261873e631daf4d3226de>,当用户第二次访问这个页面的时候(访问,或者按F5刷新),浏览器会把Etag里的数据放在请求的http头中:
GET /shop/current/src/ HTTP/1.1Host: localhostIf-None-Match: 4be41fcf3eb37261873e631daf4d3226...
最后一行:浏览器会把上次的etag值放在If-None-Match里面。如果网店程序端检测到:
- 存在这个页面的全页缓存
- 该全页缓存的页面数据未过期
- 全页缓存的Etag值和浏览器传过来的If-None-Match得相同
则发送给浏览器一个304的头信息,浏览器就将自己本地缓存的页面数据直接显示出来,避免了网络传输。
HTTP/1.1 304 Not ModifiedDate: Tue, 24 Nov 2009 09:16:36 GMTServer: Apache/2.0.59 (Win32) DAV/2 SVN/1.4.0 PHP/5.2.3Connection: closeEtag: 4be41fcf3eb37261873e631daf4d3226Expires: Mon, 26 Jul 1997 05:00:00 GMTCache-Control: private
此时,网络上传输的数据只有以上两次,并没有实际的内容传输,所以响应会非常快。 我们看一下详细过程。
- 一些比较高级的爬虫,比如google,baidu的,也会响应etag值,因此他们会主动的提交if-None-Match。从而减少了传输量,也提高了被搜索引擎索引的概率。
- 推荐一个很不错http sniffer: fiddler2<http://www.fiddlertool.com/>
- 如果你怀疑这个层次出问题了,de>ctrl+f5de>可以强制忽略etag,强迫重新获取最新的数据。
- 在shopex后台也应用了这个基于etag的缓存机制
- 这个一整套的http缓存机制是写在RFC2616文档中,感兴趣可以去查阅: http://tools.ietf.org/html/rfc2616
4.5.2. 前台全页缓存-控制器
现在聚焦到上图中"生成页面内容..."这个环节中,此时系统并不是为每次生成页面内容的请求都重新运行一次,而是将页面的结果整体缓存下来,如果该页面所包含的数据都没有更新,则是直接输出被缓存下来的内容。
这里比较复杂的是缓存的自动更新,下面来解释这个过程。
shopex在数据库的操作底层设置了一个触发动作,当有任何表的更新,插入,删除操作时,将当时的时间和被操作的表明记录在一个特殊的表里: de>sdb_cachemgrde>。例如某时刻该表的数据如下:
表名 | 最后时间 |
---|---|
sdb_goods | 2009-11-21 16:20:26 |
sdb_goods_cat | 2009-11-20 05:25:21 |
sdb_setting | 2009-11-23 19:40:13 |
sdb_member | 2009-11-23 08:36:23 |
sdb_order | 2009-11-22 20:46:42 |
sdb_payments | 2009-11-24 05:20:27 |
... | ... |
实际上,真实系统里表的时间都是unix时间戳格式,那个数字的意思是1970-01-01 00:00:00之后的秒数。在这里,为了方便大家理解,我们用实际时间表示。
在处理前台请求时,数据库底层会记录在这个过程中用到的数据表,包括页面挂件,流程插件,自动机器人,模块,以及各种子流程。同时会记录用到的表的最后修改时间,存到最终生成的缓存条目头部。
假设过了5分钟,又有人访问同样的网址,系统发现存在针对这个url的缓存。此时会读出这个缓存条目用到的表名列表: de>sdb_goods,sdb_goods_cat,sdb_settingde> 并到de>sdb_cachemgrde>中检查这些表当前的状态,如果发现有以上表里有任何一个在上次缓存保存之后被修改过了,则宣布该缓存作废,重新生成页面。这个过程是在de>core/include/cachemgrde>这个类管理的。
4.5.3. 前台全页缓存-存储器
de>cachemgrde>决定了哪些内容被缓存,缓存多久,如何被更新,怎样判别缓存过期。缓存存储器仅仅是负责缓存如何存放,如何读取。当缓存过多时如何被自动删除,删除哪一些。
shopex系统默认的缓存存储器是de>secachede>。可以被替换为apc或者memcache。secache的特点如下:
- 使用lru算法自动清理过期内容
- 可以安全用于多进程并发
- 最大支持1G缓存文件,性能不受文件大小影响
- 使用hash定位,读取迅速
de>secachede>已经被我们开源: http://code.google.com/p/secache/ 。版权采用的是最开放的MIT Licence,大家可以随意的用在自己的系统里。
用法样例
require('../secache/secache.php');$cache = new secache;$cache->workat('cachedata'); $key = md5('test'); //必须自己做hash,前4位是16进制0-f,最长32位。$value = '值数据'; //必须是字符串 $cache->store($key,$value); if($cache->fetch($key,$return)){ echo '<li>'.$key.'=>'.$return.'</li>';}else{ echo '<li>Data get failed! <b>'.$key.'</b></li>';}
基于性能考虑,几点约束:
- 键需要自己做hash处理,最长32位.
- 值必须是字符串。如果要存对象,请自己serialize
下面详细说明secache的实现原理
secache是一个采用拉链法的hash结构。整个文件分成2个主要部分。
- 结构信息区
- 保留空间 20字节,放置一个de><?php exit()?>de>防止文件被下载
- 保留空间 20字节,放置文件的版本信息,最大容量
- 20字节保留区域
- 结构信息 16种slab结构,每个需要24字节,占用空间:24*16=384字节
- 索引信息区
- 索引计数器数据 40-44
- 数据最大空间指针 44-48
- 空闲链表入口地址 48-52
- hash索引入口地址 (16^4)*4 = 256k
- 数据区(混杂)
- 索引链表
- 数据链表
整个结构如图所示
系统将要存储的数据最多可分为16中大小,然后分别创建数据空间。避免一条数据被拆分到多个数据块中的复杂度。我们一共预设了13组slab结构,预设的数据大小和预分配的条目为:
大小 | 条数 | 占用空间 |
---|---|---|
512 | 10 | 5k |
3k | 10 | 30k |
8k | 10 | 80k |
20k | 4 | 80k |
30k | 2 | 60k |
50k | 2 | 100k |
80k | 2 | 160k |
96k | 2 | 192k |
128k | 2 | 256k |
224k | 2 | 448k |
256k | 2 | 512k |
512k | 1 | 512k |
1M | 1 | 1M |
因此secache要求数据空间最小为10M。当超过这个值时,secache会根据存放数据的频率动态增加各个slab的数据块数量,直到增长到配置好的最大数据量为止。当增长到极限时,各slab的数据块分布将不再变动。如果想重建结构,需要删除缓存文件,重新生成。当无法申请到新的空间之后,将采用lru的策略删除相对不活跃的缓存条目,为新的缓存请求腾出数据空间。
每个slab结构占用24个字节,组成为:
类型 | 大小 | 空闲指针 | lru头部指针 | lru尾部指针 | 缓存命中 | 未命中 |
占用空间 | 4bytes | 4bytes | 4bytes | 4bytes | 4bytes | 4bytes |
再看一下hash索引表的结构。所有进入secache的数据,key的前4位必须是16进制数据,即de>0000de>-de>ffffde>, 共65536种前缀, 每个前缀的索引占用4字节的入口, 所以索引入口区占用空间为65536*4=256k空间。
可以看到,secache的地址数据都是4字节,即最大支持256^4=4G的寻址空间,但是由于PHP在32位系统下的大数上限约为二十亿。所以保险起见,我们强制secache最大仅支持1G空间。
数据区的数据有两种,一种是实际数据,一种是索引节点数据。实际数据空间占用情况依照slab不同而不同,索引节点的长度是定长:56Bytes。
索引节点结构:
类型 | 下个节点 | 上个节点 | 数据入口指针 | 实际长度 | lru右节点 | lru左节点 | 键名 |
占用空间 | 4bytes | 4bytes | 4bytes | 4bytes | 4bytes | 4bytes | 32bytes |
4.5.4. 模板缓存
模板缓存比较简单,就是smarty的原则,每个html文件会被编译成一个php文件,每次执行该php文件之前去判断原始文件的最后修改时间。
5. shopex对外数据接口
外部系统和shopex系统的交互主要有两个应用场景:
- 一种是外部系统需要调用shopex,比如通知shopex创建订单,向shopex里面增加一个商品。更新一下库存量,修改某订单状态等等。(图中实线部分)
- 另一种是外部系统需要被shopex调用,例如当用人下订单时,需要通知财务系统有应收款项。 某些商品库存不足时,需要shopex对外发出警告。当有人通过网店注册时,需要同步到crm系统等等(图中虚线部分)。
以消息体系为根本,每一个shopex内发生的事件,均对外发出事件广播。关心该消息的外部系统,接到消息后后,可以通过API去获取详细的数据。
获取数据的方法有两种:
- 采用预设的api去获取
- 利用dav接口获取,数据的结构是标准数据格式 (推荐使用)
这种做法也被称为观察者模式。并且我们要秉着"小消息,大运算"的原则。就是广播出的事件消息本身,数据量会尽可能的小。以便于在各种载体上广播,详细的变动数据,状态结果。各系统要通过统一的接口去获取。
shopex内部是通过消息体系驱动的。具体在代码实现上,就是de>shopObjectde>这个类,他的所有子类通过de>fireEventde>方法产生事件。fireEvent会调用de>model:system/triggerde>来将事件广播出去,并调用注册在这个事件上的系统内行为。
6. 附录
6.1. setting
条目 | 用途 | 备注 |
---|---|---|
coupon.mc.use_times | 优惠券可用次数 | - |
security.guest.enabled | 是否支持非会员购物 | - |
site.version | version的最后修改时间 | - |
site.dateFormat | 商店日期格式 | - |
site.timeFormat | 商店日期时间格式 | - |
site.coupon_order_limit | 每张订单可用优惠券数量 | - |
site.decimal_digit | 订单金额取整位数 | - |
site.decimal_type | 订单金额取整方式 | - |
site.delivery_time | 默认备货时间 | - |
site.show_mark_price | 前台是否显示市场价 | - |
site.login_valide | 会员登录需输入验证码 | - |
site.login_type | 顾客登录方式 | - |
site.register_valide | 会员注册需输入验证码 | - |
site.buy.target | 顾客点击商品购买按钮后 | - |
site.market_price | 商品页是否显示市场价 | - |
site.market_rate | 请输入比例值或增额值 | - |
site.save_price | 商品页是否显示节省金额 | - |
site.retail_member_price_display | 零售会员价显示设定 | - |
site.wholesale_member_price_display | 批发会员价显示设定 | - |
site.meta_desc | META_DESCRIPTION | - |
site.meta_key_words | META_KEYWORDS | - |
site.order_storage | 库存扣除方式 | - |
site.offline_pay | 支持线下支付方式 | - |
site.shopex_certify | ShopEx Store 认证显示 | - |
site.tax_ratio | 税率 | - |
site.trigger_tax | 是否设置含税价格 | - |
site.copyright | 版权信息 | - |
site.logo | 商店Logo | - |
site.certtext | 备案号 | - |
site.cert | 备案证书 | - |
site.thumbnail_pic_height | 缩略图高度 | - |
site.thumbnail_pic_width | 缩略图宽度 | - |
site.homepage_title | TITLE(首页标题) | - |
site.homepage_meta_key_words | META_KEYWORDS | - |
site.homepage_meta_desc | META_DESCRIPTION | - |
site.goods_title | TITLE(首页标题) | - |
site.goods_meta_key_words | META_KEYWORDS | - |
site.goods_meta_desc | META_DESCRIPTION | - |
site.list_title | TITLE(首页标题) | - |
site.list_meta_key_words | META_KEYWORDS | - |
site.list_meta_desc | META_DESCRIPTION | - |
site.brand_index_title | TITLE(首页标题) | - |
site.brand_index_meta_key_words | META_KEYWORDS | - |
site.brand_index_meta_desc | META_DESCRIPTION | - |
site.brand_list_title | TITLE(首页标题) | - |
site.brand_list_meta_key_words | META_KEYWORDS | - |
site.brand_list_meta_desc | META_DESCRIPTION | - |
site.article_list_title | TITLE(首页标题) | - |
site.article_list_meta_key_words | META_KEYWORDS | - |
site.article_list_meta_desc | META_DESCRIPTION | - |
site.article_title | TITLE(首页标题) | - |
site.article_meta_key_words | META_KEYWORDS | - |
site.article_meta_desc | META_DESCRIPTION | - |
system.admin_verycode | 管理员后台登陆启用验证码 | - |
store.address | 联系地址 | - |
store.company_name | 网站所有人 | - |
store.contact | 联系人 | - |
store.email | 电子邮件 | - |
store.mobile_phone | 手机 | - |
store.shop_url | 商店网址 | - |
store.telephone | 固定电话 | - |
store.zip_code | 邮政编码 | - |
system.money.operation.decimals | 前台价格精确到 | - |
system.money.decimals | 订单金额显示位数 | - |
system.money.operation.carryset | 价格进位方式 | - |
system.category.showgoods | 商品分类列表页显示设置 | - |
system.product.alert.num | 商品库存报警数量 | - |
system.shopname | 商店名称 | - |
system.seo.emuStatic | 商店页面启用伪静态URL | - |
system.seo.noindex_catalog | 通知搜索引擎不索引目录页 | - |
system.ui.webslice | 支持ie8的webslice特性 | - |
system.timezone.default | 用户默认时区 | - |
site.index_title | TITLE(首页标题) | - |
site.title_format | 网站标题格式 | - |
site.stripHtml | 是否压缩html | - |
site.url.base | 主站访问地址 | - |
site.url.themeres | 模板资源访问地址 | - |
site.url.widgetsres | 版块资源访问地址 | - |
goods.rate_nums | 相关商品最大数量 | - |
gallery.default_view | 商品列表默认展示方式 | - |
system.fast_delivery_as_progress | 后台手工发货为"已发货" | - |
system.auto_delivery | 用户到款则自动发货 | - |
system.auto_delivery_physical | 用户到款自动发货时,实体商品如何处理(auto:发货为ready,no:不发货,yes:发货为progress) | - |
system.auto_use_advance | 自动使用预存款 | - |
search.show.range | 搜索是否显示价格区间 | - |
order.flow.payed | 订单付款流程 | - |
order.flow.consign | 订单发货流程 | - |
order.flow.refund | 订单退款流程 | - |
order.flow.reship | 订单退货流程 | - |
certificate.id | ShopEx证书编号 | - |
certificate.token | ShopEx证书密钥 | - |
certificate.str | ShopEx证书身份说明 | - |
certificate.formal | ShopEx证书身份 | - |
certificate.kft.cid | 客服通公司id | - |
certificate.kft.style | 客服通风格号 | - |
certificate.kft.action | 客服通动作 | - |
certificate.kft.enable | 客服通开关 | - |
certificate.channel.url | 渠道url | - |
certificate.channel.name | 渠道商名 | - |
certificate.channel.status | 渠道状态 | - |
certificate.channel.service | 渠道服务类型 | - |
certificate.distribute | 是否开通分销模块 | - |
messenger.sms.config | 短信sms签名 | - |
shopex.wss.username | 合作统计用户名 | - |
shopex.wss.password | 合作统计密码 | - |
shopex.wss.enable | 合作统计开关 | - |
shopex.wss.show | 合作统计前台开关 | - |
shopex.wss.js | 合作统计js | - |
system.area_depth | 地区级数 | - |
comment.index.listnum | 商品首页显示评论条数 | - |
comment.list.listnum | 评论列表页显示评论条数 | - |
comment.switch.ask | 商品询问开关 | - |
comment.switch.discuss | 商品评论开关 | - |
comment.switch.buy | 商品经验评论开关 | - |
comment.display.ask | 商品评论(询问),回复显示 | - |
comment.display.discuss | 商品评论(评论),回复显示 | - |
comment.display.buy | 商品评论(经验),回复显示 | - |
comment.power.ask | 商品评论(询问),发布权限 | - |
comment.power.discuss | 商品评论(评论),发布权限 | - |
comment.power.buy | 商品评论(经验),发布权限 | - |
comment.null_notice.ask | 没有咨询记录,提示文字 | - |
comment.null_notice.discuss | 商品评论(经验),发布权限 | - |
comment.null_notice.buy | 商品评论(经验),发布权限 | - |
comment.submit_display_notice.ask | 没有咨询记录,提示文字 | - |
comment.submit_hidden_notice.ask | 商品评论(经验),发布权限 | - |
comment.submit_display_notice.discuss | 商品评论(经验),发布权限 | - |
comment.submit_hidden_notice.discuss | 没有咨询记录,提示文字 | - |
comment.submit_display_notice.buy | 商品评论(经验),发布权限 | - |
comment.submit_hidden_notice.buy | 商品评论(经验),发布权限 | - |
selllog.display.switch | 是否显示销售记录 | - |
selllog.display.limit | 低于多少条不显示销售记录 | - |
selllog.display.listnum | 显示条数 | - |
goodsbn.display.switch | 是否启用商品编号 | - |
storeplace.display.switch | 是否使用商品货位 | - |
gallery.display.listnum | 搜索列表显示条数 | - |
gallery.display.grid.colnum | 搜索橱窗页显示行数 | - |
plugin.passport.config.current_use | 当前使用的passport | - |
system.message.open | 商店留言发布 | - |
site.refer_timeout | 推荐链接过期时间(天) | - |
site.is_open_return_product | 是否开启退货功能 | - |
spec.image.height | 规格图片宽度 | - |
spec.image.width | 规格图片高度 | - |
spec.default.pic | 规格默认图片 | - |
system.editortype | HTML编辑器设置 | - |
system.upload.limit | 前台图片大小限定 | - |
system.store.time | 库存预占触发时间 | - |
system.guide | 向导设置 | - |
goodsprop.display.switch | 是否启用商品属性链接 | - |
store.site_owner | 商店所有人 | - |
store.mobile | 手机 | - |
store.qq | - | |
store.wangwang | 旺旺 | - |