github https://github.com/likangjun/official
前台演示 http://demo.likangjun.com
后台演示 http://admin.likangjun.com
前台
后台
这里文档和sdk都比较详细,基本上换掉sdk的证书文件夹和配置文件就可以使用,但是还是有不少坑
首先要在公众平台上设置测试授权目录,还有测试白名单,这个就不细说了
重点说下实际中可能会遇到的问题
是因为https证书校验失败
解决方法WxPay.Api.php
537行1
2
3
4
5
6
7
8
9
10
11
12curl_setopt($ch,CURLOPT_SSL_VERIFYPEER,TRUE);
curl_setopt($ch,CURLOPT_SSL_VERIFYHOST,2);//严格校验
//改为
if (stripos($url, "https://") !== FALSE) {
curl_setopt($ch, CURLOPT_SSLVERSION, CURL_SSLVERSION_TLSv1);
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, FALSE);
curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, FALSE);
} else {
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, TRUE);
curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, 2);//严格校验
}
是因为连接超时
我们可以修在下单的时候延长超时时间,sdk的下单的接口默认是6秒
可以给定30秒的超时时间
1 | \WxPayApi::unifiedOrder($input, 30); |
WxPay.Api.php
411行1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23/**
*
* 支付结果通用通知
* @param function $callback
* 直接回调函数使用方法: notify(you_function);
* 回调类成员函数方法:notify(array($this, you_function));
* $callback 原型为:function function_name($data){}
*/
public static function notify($callback, &$msg)
{
//获取通知的数据
$xml = $GLOBALS['HTTP_RAW_POST_DATA'];
//如果返回成功则验证签名
try {
$result = WxPayResults::Init($xml);
} catch (WxPayException $e){
$msg = $e->errorMessage();
return false;
}
return call_user_func($callback, $result);
}
如果php.ini
不是always_populate_raw_post_data = On的话1
$xml = $GLOBALS['HTTP_RAW_POST_DATA'];
会接受不到数据
这时候如果不改配置文件的话
可以改为1
$xml = file_get_contents('php://input');
每一条聊天记录都直接存放mysql,来一条insert一条,量不大还好说,基本都能应付。但是如果量特别大,一天几百万条,上千万条,这个insert操作就会执行几百万次,会不会响应不过来?用户体验会不会降低?
类似的案例还有
一个帖子,用户每访问一次就要set visitor = visitor+1
,每天访问上百万次,就要update上百万次,,这个聊天记录表又是访问请求最高的,会不会锁死?
其实这些都可以优化
Redis读的速度是110000次/s,写的速度是81000次/s 。
第一个案例里面可以用Redis做消息中转站,用一个list存放我们的聊天记录。
利用Redis的性能,我们可以把这些请求都写到缓存。然后后台启用一个crontab定时任务,每分钟执行,把队列里的数据拿出来,存到MySQL,甚至可以每10秒,5秒做一次操作
这里给大家一个简单的demo
1 | //插入消息队列 |
利用linux的crontab定时任务异步的去调用actionPop
因为crontab的最快频率是1分钟调用一次,如果实时性要求很高,比如2秒
可以写个脚本
1 | step=2 #间隔 |
然后创建crontab任务
每分钟去执行这个脚本
1 | * * * * * /web/chat.sh |
这样就实现了每2秒钟去掉我们的pop-insert接口
利用Apache的ab做测试
1000次请求100个并发结果:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17erver Software: nginx/1.8.1
Server Hostname: 127.0.0.1
Server Port: 80
Document Path: /kxds/web/index.php?r=redis/list/test1
Document Length: 0 bytes
Concurrency Level: 100
Time taken for tests: 9.936 seconds
Complete requests: 1000
Failed requests: 0
Total transferred: 357000 bytes
HTML transferred: 0 bytes
Requests per second: 100.65 [#/sec] (mean)
Time per request: 993.563 [ms] (mean)
Time per request: 9.936 [ms] (mean, across all concurrent requests)
Transfer rate: 35.09 [Kbytes/sec] received
可以看到结果接近10秒钟
而直接接受消息同步存入mysql的测试结果是13.288 seconds
这个响应时间相差还是比较大的
不过这里注意的是,异步更新的内容,属于“丢了其实关系也不大”的数据,如果是非常核心的数据,异步更新要注意数据丢失的危险
第二个案例类似的还有很多
这个数据,其实实时性需要不是那么高,不是每个请求都必须立即处理
这里可以用一个Redis的hash来存放,帖子id对应访问量,每次请求的时候把帖子id对应的值incr增1
同样异步的定时的去从hash里读取数据,update数据库
这样就做到了请求合并和异步更新,可能每次去更新的时候都有几十次上百次增量,这样就合并吊了几十次上百次请求
]]>比较简单,直接上demo吧
1 |
|
MySQL:关系型数据库
NoSQL:非关系型的数据库,数据之间无关系,无需事先为要存储的数据建立字段,随时可以存储自定义的数据格式,有非常高的读写性能,尤其在大数据量下,表现非常优秀。
Redis 就是一个 Key-Value 存储系统。支持的数据类型包括string(字符串)、hash(哈希)、list(链表)、set(集合)和zset(有序集合)等等。
官方的bench-mark数据:
测试完成了50个并发执行100000个请求。
设置和获取的值是一个256字节字符串。
结果:读的速度是110000次/s,写的速度是81000次/s 。
Redis运行在内存中但是可以持久化到磁盘
Redis提供了多种不同级别的持久化方式:一种是RDB,另一种是AOF.
RDB 持久化可以在指定的时间间隔内生成数据集的时间点快照。当redis需要做持久化时,redis会fork一个子进程;子进程读出数据并将数据写到磁盘上一个临时RDB文件中;当子进程完成写临时文件后,将原来的RDB替换掉,这样的好处就是可以copy-on-write,缺点就是在redis异常死掉时, 最近的数据会丢失
AOF redis每执行一个修改数据的命令,都会把它添加到aof文件中,当redis重启时,将会读取AOF文件进行“重放”以恢复到redis关闭前的最后时刻。
Redis 可以同时使用 AOF 持久化和 RDB 持久化。 在这种情况下, 当 Redis 重启时, 它会优先使用 AOF 文件来还原数据集, 因为 AOF 文件保存的数据集通常比 RDB 文件所保存的数据集更完整
1)绝大部分请求是纯粹的内存操作(非常快速)
2)采用单线程,避免了不必要的线程切换开销
3)非阻塞IO
redis中文网:http://www.redis.net.cn/
php-redis文档:http://www.cnblogs.com/weafer/archive/2011/09/21/2184059.html
string(字符串)
1 | 127.0.0.1:6379> set lkj likangjun |
1.数据缓存
2.原子计数器(原子性,可用于抢购,秒杀 )
hash(哈希)
Redis 中每个 hash 可以存储 (2^32-1) 键值对(40多亿)1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25127.0.0.1:6379> hmset user_info_7 id 7 name likangjun mobile 18672792276
OK
127.0.0.1:6379> hget user_info_7 id
"7"
127.0.0.1:6379> HGETALL user_info_7
1) "id"
2) "7"
3) "name"
4) "likangjun"
5) "mobile"
6) "18672792276"
127.0.0.1:6379> HKEYS user_info_7
1) "id"
2) "name"
3) "mobile"
127.0.0.1:6379> HVALS user_info_7
1) "7"
2) "likangjun"
3) "18672792276"
127.0.0.1:6379> HEXISTS user_info_7 times
(integer) 0
127.0.0.1:6379> HSETNX user_info_7 times 1
(integer) 1
127.0.0.1:6379> HINCRBY user_info_7 times 1
(integer) 2
1.用户信息
list(列表)
Redis列表是简单的字符串列表,按照插入顺序排序。你可以添加一个元素导列表的头部(左边)或者尾部(右边)
一个列表最多可以包含 (2^32-1) 个元素(每个列表超过40亿个元素)。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50127.0.0.1:6379> rpush list1 a
(integer) 1
127.0.0.1:6379> rpush list1 b
(integer) 2
127.0.0.1:6379> rpush list1 c
(integer) 3
127.0.0.1:6379> lrange list1 0 -1
1) "a"
2) "b"
3) "c"
127.0.0.1:6379> rpop list1
"c"
127.0.0.1:6379> lpop list1
"a"
127.0.0.1:6379> lrange list1 0 -1
1) "b"
127.0.0.1:6379> rpush list2 a
(integer) 1
127.0.0.1:6379> rpush list2 b
(integer) 2
127.0.0.1:6379> rpush list2 c
(integer) 3
127.0.0.1:6379> rpoplpush list2 list1
"c"
127.0.0.1:6379> lrange list1 0 -1
1) "c"
2) "b"
127.0.0.1:6379> lrange list2 0 -1
1) "a"
2) "b"
127.0.0.1:6379> linsert list2 before b d
(integer) 3
127.0.0.1:6379> lrange list2 0 -1
1) "a"
2) "d"
3) "b"
127.0.0.1:6379> lpush list3 a
(integer) 1
127.0.0.1:6379> lpush list3 b
(integer) 2
127.0.0.1:6379> lpush list3 c
(integer) 3
127.0.0.1:6379> lrange list3 0 -1
1) "c"
2) "b"
3) "a"
127.0.0.1:6379> lpop list3
"c"
127.0.0.1:6379> rpop list3
"a"
1.消息队列
set(集合)
Redis的Set是string类型的无序集合。集合成员是唯一的,这就意味着集合中不能出现重复的数据。
Redis 中 集合是通过哈希表实现的,所以添加,删除,查找的复杂度都是O(1)。
集合中最大的成员数为(2^32-1) (每个集合可存储40多亿个成员)。
1 | 127.0.0.1:6379> sadd lkj-code nginx |
1.共同好友
2.利用唯一性,可以统计访问网站的所有独立 IP
3.好友推荐的时候,根据 tag 求交集,大于多少个就可以推荐
zset(有序集合)
Redis 有序集合和集合一样也是string类型元素的集合,且不允许重复的成员。
不同的是每个元素都会关联一个double类型的分数。redis正是通过分数来为集合中的成员进行从小到大的排序。
有序集合的成员是唯一的,但分数(score)却可以重复。
集合是通过哈希表实现的,所以添加,删除,查找的复杂度都是O(1)。 集合中最大的成员数为 (2^32-1) (每个集合可存储40多亿个成员)。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60127.0.0.1:6379> zadd rank1 10 php
(integer) 1
127.0.0.1:6379> zadd rank1 2 ios
(integer) 1
127.0.0.1:6379> zadd rank1 8 java
(integer) 1
127.0.0.1:6379> ZINCRBY rank1 1 ios
"3"
127.0.0.1:6379> ZRANGE rank1 0 -1 withscores
1) "ios"
2) "3"
3) "java"
4) "8"
5) "php"
6) "10"
127.0.0.1:6379> zrevrange rank1 0 -1 withscores
1) "php"
2) "10"
3) "java"
4) "8"
5) "ios"
6) "3"
127.0.0.1:6379> zrevrange rank1 0 -1
1) "php"
2) "java"
3) "ios"
127.0.0.1:6379> zscore rank1 php
"10"
127.0.0.1:6379> zrevrank rank1 php
(integer) 0
127.0.0.1:6379> zadd rank2 99 php
(integer) 1
127.0.0.1:6379> zadd rank2 10 ios
(integer) 1
127.0.0.1:6379> zadd rank2 50 java
(integer) 1
127.0.0.1:6379> zrevrange rank1 0 -1 withscores
1) "php"
2) "10"
3) "java"
4) "8"
5) "ios"
6) "3"
127.0.0.1:6379> zrevrange rank2 0 -1 withscores
1) "php"
2) "99"
3) "java"
4) "50"
5) "ios"
6) "10"
#计算给定的一个或多个有序集的并集
127.0.0.1:6379> ZUNIONSTORE rank 2 rank1 rank2 WEIGHTS 2 1
(integer) 3
127.0.0.1:6379> zrevrange rank 0 -1 withscores
1) "php"
2) "119"
3) "java"
4) "66"
5) "ios"
6) "16"
1.带有权重的元素,比如投票排行榜
事务
Redis 事务可以一次执行多个命令, 并且带有以下两个重要的保证:
事务是一个单独的隔离操作:事务中的所有命令都会序列化、按顺序地执行。事务在执行的过程中,不会被其他客户端发送来的命令请求所打断。
事务是一个原子操作:事务中的命令要么全部被执行,要么全部都不执行。
一个事务从开始到执行会经历以下三个阶段:
开始事务。
命令入队。
执行事务。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22127.0.0.1:6379> MULTI
OK
127.0.0.1:6379> set book likangjun
QUEUED
127.0.0.1:6379> get book
QUEUED
127.0.0.1:6379> ZINCRBY rank 1 php
QUEUED
127.0.0.1:6379> zrevrange rank1 0 -1 withscores
QUEUED
127.0.0.1:6379> EXEC
1) OK
2) "likangjun"
3) "120"
4) 1) "php"
2) "11"
3) "java"
4) "8"
5) "ios"
6) "3"
7) "html"
8) "1"
发布订阅
Redis 发布订阅(pub/sub)是一种消息通信模式:发送者(pub)发送消息,订阅者(sub)接收消息。
HyperLogLog
Redis HyperLogLog 是用来做基数统计的算法,HyperLogLog 的优点是,在输入元素的数量或者体积非常非常大时,计算基数所需的空间总是固定的、并且是很小的。
在 Redis 里面,每个 HyperLogLog 键只需要花费 12 KB 内存,就可以计算接近 2^64个不同元素的基数。这和计算基数时,元素越多耗费内存就越多的集合形成鲜明对比。
但是,因为 HyperLogLog 只会根据输入元素来计算基数,而不会储存输入元素本身,所以 HyperLogLog 不能像集合那样,返回输入的各个元素。
1 | 127.0.0.1:6379> PFADD code "redis" |
主从同步可以防止主机坏掉导致网站不能正常运作,这种方法即把从机设置成主机即可
Redis主从同步设置很简单,设置到Slave服务器后,Slave自动和Master建立连接
第一阶段
1.Slave服务器主动连接到Master服务器
2.Slave服务器发送SYCN命令到Master服务器请求同步
3.Master服务器备份数据到rdb文件
4.Master服务器把rdb文件传输到Slave服务器
5.Slave服务器清空数据库数据,把rdb文件数据导入到数据库
第二阶段
Master服务器把用户所有更改数据的操作,通过命令转发给所有的Slave服务器,Slave服务器只需要执行Master服务器发送过来的命令就可以达到同步的效果
Redis远远不只是缓存这么简单,缓存只是其中的一个小部分
Redis更多的在于其丰富的数据结构和灵活性,可以使很多功能实现起来更加简单有效,极大的提高效率和性能
Redis数据库在以下的这几种情况下比较适用:
1、数据模型比较简单;
2、需要灵活性更强的IT系统;
3、对数据库性能要求较高;
4、不需要高度的数据一致性;
5、对于给定key,比较容易映射复杂值的环境。
注:O(1)指的是常数时间运行,比如操作对象为一个链表,对其有一个算法,O(1)时间指的是,无论链表大或者小,所耗费的时间都是一样的;
O(n)指的是某算法的运行时间与输入规模成正比,即,若输入规模为T,花费时间为N,则输入规模2T时花费时间为2N
Copy-on-Write简称COW,基本的原理是:
当我要修改数据块A的内容的时候,我先把A读出来,写到B块里,如果写的过程掉电了,原来A的内容还在,如果是还写到原来的位置上,那么写入的数据究竟写了多少就不确定了,会不会破坏原来的数据也不好说。
1表级锁
:直接锁定整张表,在你锁定期间,其它进程无法对该表进行写操作。如果你是写锁,则其它进程则读也不允许
2行级锁
:仅对指定的记录进行加锁,这样其它进程还是可以对同一个表中的其它记录进行操作。
3页面锁
:表级锁速度快,但冲突多,行级冲突少,但速度慢。所以取了折衷的页级,一次锁定相邻的一组记录。
场景:现在最后只有1件库存,同时有100个或1000个请求,如何能保证只有一个用户能够抢到最后这件商品呢
1 | public function actionTest() |
这里update()
执行的sql语句是1
update goods set number = number-1 where id = '1';
用ab做压力测试1
/ab -n1000 -c100 "http://127.0.0.1/xxxxxxxx/test"
100
个用户1000
个请求
测试完看数据库,发现number变成了-3
,订单表里面出现了4
个订单
出现这个结果是在我们意料之中的,并发越大,订单数也越多,超卖的也越多
同样的方法,我们只改一下sql
再做测试
将update()
的sql语句改为1
update goods set number = number-1 where id = '1' and number > '0';
再用ab测试
甚至1000个用户10000个请求测试,依然如此
分析下发现,当条件加上number > '0'
后,假如有3个并发,同时执行这条sql
第一个执行的时候,mysql的行级锁会将此条记录加锁,当这个执行完以后,number已被更新为0
此时锁打开,第二个第三个执行的时候,where条件number > '0'
无法满足,所以后面的请求将无法再去更新number和下单
而之前不加这个number > '0'
这个条件的话,有锁和没锁是无所谓的,都会执行,并且完成update
手机网站支付的文档和demo给的已经特别的详细了, 这里我就说一下实际开发中值得注意的问题吧
"app_pay" => "Y"
在浏览器调用的时候可以唤起支付宝客户端,如果没有安装支付宝,则继续进行手机网站支付
return_url
如果你想带其他参数也是可以的文档上是这么写的
设置页面跳转同步通知页面(return_url)的路径时,不要在页面文件的后面再加上自定义参数。例如:
错误的写法:http://www.alipay.com/alipay/return_url.php?xx=11
正确的写法:http://www.alipay.com/alipay/return_url.php
文档这么写是因为return_url
在验证的时候会通过$_GET
去生成签名,这个签名会和通过notify_id
获取到的结果做比对,所以这里$_GET
里面不能有别的参数
比如你的return_url 为 http://xxxxxx/alipay/return_url.php?uuid=12345
那么在你的return_url.php就得改成1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23$get_temp = $_GET;
unset($_GET['uuid']);
//这个时候的$_GET是不带我们附加的参数的
//$alipay_config 配置文件参数
$alipayNotify = new \AlipayNotify($alipay_config);
$verify_result = $alipayNotify->verifyReturn();
$_GET = $get_temp;
//这个时候就可以开始我们的逻辑了
$uuid = $_GET['uuid'];
if ($verify_result) {//验证成功
//交易状态
$trade_status = $_GET['trade_status'];
if ($trade_status == 'TRADE_FINISHED' || $trade_status == 'TRADE_SUCCESS') {
//交易成功
} else {
echo "trade_status=" . $_GET['trade_status'];
}
} else {
die("验证失败");
}
1 | $data = array( |
输出结果1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19一级栏目一
├─ 二级栏目一
│ ├─ 三级栏目一
│ └─ 三级栏目二
└─ 二级栏目二
├─ 三级栏目三
└─ 三级栏目四
└─ 四级栏目一
一级栏目二
└─ 二级栏目三
还能以select形式展现
1 | class Tree |
得出数组用foreach循环即可输出树形结构1
2
3
4
5
6
7
8$tree = new Tree ();
$tree->tree($data);
$result = $tree->getArray();
foreach ($data as $value) {
echo $value['name'];
echo '<br/>';
echo '<br/>';
}
这里给出option模板1
2
3
4$html = '<select name="tree">';
$str = "<option value=%s %s>%s%s</option>";
$html .= $tree->get_tree(0, $str, 2) . '</select>';
echo $html;
即可输出select1
get_tree($myid, $str, $sid = 0, $adds = '')
这里是$sid
参数是给出slected的option的id
利用应用一的数组无限极分类和还原以及应用二的文件夹目录的遍历
可以树状结构输出文件夹目录1
2
3
4
5
6
7
8
9
10
11
12$tree = new Tree (); // new 之前请记得包含tree文件!
//文件夹遍历
$data = $tree->read_all_dir('/web/mall/Application/lib', false);
//转换成[['id','pid','name']]的二维数组
$data = $tree->arrshift($data);
$tree->tree($data);
$data = $tree->getArray();
foreach ($data as $value) {
echo $value['name'];
echo '<br/>';
echo '<br/>';
}
可以得到我们想要的结果1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45Action
├─ Admin
│ ├─ GoodAction.class.php
│ ├─ IndexAction.class.php
│ ├─ LoginAction.class.php
│ ├─ MenuAction.class.php
│ ├─ OrderAction.class.php
│ ├─ PublicAction.class.php
│ ├─ UserAction.class.php
│ ├─ UtilAction.class.php
│ ├─ WechatAction.class.php
│ └─ WeixinAction.class.php
├─ Api
│ └─ ApiAction.class.php
└─ App
├─ IndexAction.class.php
└─ Sms.class.php
log.txt
Model
├─ Admin
│ └─ OrderModel.class.php
└─ App
└─ OrderModel.class.php
虽然递归函数的效率和性能欠佳,但是对我们的逻辑训练还是有很大帮助的,我们可以写出一个方法并不断的优化和简单化,来提高我们的逻辑
]]>以下面这个目录为例的
1 |
|
运行dirList('/web/mall/Application/lib', false)
打印数组1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55array(3) {
["Action"]=>
array(3) {
["Admin"]=>
array(10) {
[0]=>
string(20) "GoodAction.class.php"
[1]=>
string(21) "IndexAction.class.php"
[2]=>
string(21) "LoginAction.class.php"
[3]=>
string(20) "MenuAction.class.php"
[4]=>
string(21) "OrderAction.class.php"
[5]=>
string(22) "PublicAction.class.php"
[6]=>
string(20) "UserAction.class.php"
[7]=>
string(20) "UtilAction.class.php"
[8]=>
string(22) "WechatAction.class.php"
[9]=>
string(22) "WeixinAction.class.php"
}
["Api"]=>
array(1) {
[0]=>
string(19) "ApiAction.class.php"
}
["App"]=>
array(2) {
[0]=>
string(21) "IndexAction.class.php"
[1]=>
string(13) "Sms.class.php"
}
}
[0]=>
string(7) "log.txt"
["Model"]=>
array(2) {
["Admin"]=>
array(1) {
[0]=>
string(20) "OrderModel.class.php"
}
["App"]=>
array(1) {
[0]=>
string(20) "OrderModel.class.php"
}
}
}
可以很清晰的看出一层一层的结构
]]>1 | $data = array( |
调用以后输出,可以看到结果1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107array(2) {
[0]=>
array(4) {
["id"]=>
string(1) "1"
["pid"]=>
int(0)
["name"]=>
string(15) "一级栏目一"
["children"]=>
array(2) {
[0]=>
array(4) {
["id"]=>
string(1) "2"
["pid"]=>
int(1)
["name"]=>
string(15) "二级栏目一"
["children"]=>
array(2) {
[0]=>
array(3) {
["id"]=>
string(1) "4"
["pid"]=>
int(2)
["name"]=>
string(15) "三级栏目一"
}
[1]=>
array(3) {
["id"]=>
string(1) "5"
["pid"]=>
int(2)
["name"]=>
string(15) "三级栏目二"
}
}
}
[1]=>
array(4) {
["id"]=>
string(1) "3"
["pid"]=>
int(1)
["name"]=>
string(15) "二级栏目二"
["children"]=>
array(2) {
[0]=>
array(3) {
["id"]=>
string(1) "6"
["pid"]=>
int(3)
["name"]=>
string(15) "三级栏目三"
}
[1]=>
array(4) {
["id"]=>
string(1) "7"
["pid"]=>
int(3)
["name"]=>
string(15) "三级栏目四"
["children"]=>
array(1) {
[0]=>
array(3) {
["id"]=>
string(2) "10"
["pid"]=>
int(7)
["name"]=>
string(15) "四级栏目一"
}
}
}
}
}
}
}
[1]=>
array(4) {
["id"]=>
string(1) "8"
["pid"]=>
int(0)
["name"]=>
string(15) "一级栏目二"
["children"]=>
array(1) {
[0]=>
array(3) {
["id"]=>
string(1) "9"
["pid"]=>
int(8)
["name"]=>
string(15) "二级栏目三"
}
}
}
}
可以看到最外层的数组只有我们的一级栏目,然后子栏目都嵌套在其中
运行arrRecove()
方法,即可把数组还原到二维数组
甚至可以用一种更简单粗暴的分级1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34//无限极分类
function assortArray($data, $pid = 0)
{
$r = array();
foreach ($list as $id => $item) {
if ($item['pid'] == $pid) {
if ($t = assortArray($list, $item['id'])) {
$r[$item['name']] = $t;
} else {
$r[] = $item['name'];
}
}
}
return $r;
}
//还原
function arrshift($array, $pid = 0)
{
static $r = [];
static $index = 1;
if (is_array($array) && count($array) > 0) {
foreach ($array as $k => $v) {
$r[] = array(
'id' => $index,
'pid' => $pid,
'name' => is_array($v) ? $k : $v
);
$index++;
arrshift($v, $index - 1);
}
}
return $r;
}
运行分级方法可以看到1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27array(2) {
["一级栏目一"]=>
array(2) {
["二级栏目一"]=>
array(2) {
[0]=>
string(15) "三级栏目一"
[1]=>
string(15) "三级栏目二"
}
["二级栏目二"]=>
array(2) {
[0]=>
string(15) "三级栏目三"
["三级栏目四"]=>
array(1) {
[0]=>
string(15) "四级栏目一"
}
}
}
["一级栏目二"]=>
array(1) {
[0]=>
string(15) "二级栏目三"
}
}
运行arrRecove()
方法,同样可以把数组还原到二维数组
不同的分级方式,还原的方法也就不一样
]]>/web/
.
表示当前目录..
表示当前目录的上一级目录。./
表示当前目录下的某个文件或文件夹,../
表示当前目录上一级目录的文件或文件夹
在index.html
中引用css
当前目录(b)下的css文件夹1
<link rel="stylesheet" href='css/main.css'/>
1 | <link rel="stylesheet" href='./css/main.css'/> |
再或者是
当前目录的上级目录(demo)下的b目录下的css目录1
<link rel="stylesheet" href='../b/css/main.css'/>
使用图片1
2<img src="test2.jpg">
<img src="../a/test1.jpg">
类似php中也是
在index.php中1
2
3require_once "c/user.php";
//或者
require_once "./c/user.php";
在user.php中1
require_once "../index.php";
getcwd() :在哪个文件里调用此文件的目录
__DIR__ : 当前内容写在哪个文件的目录
__FILE__ :当前内容写在哪个文件的目录+文件名
我们经常会在项目中require或者include引用某个php文件
以yii基础版框架随便举个例子
运行UserController的actionListlocalhost/demo/web/index.php?r=admin/user/list
在UserController中1
2
3
4
5
6
7
8echo __DIR__;
// 当前内容写在哪个文件(UserController.php)的目录admin
// /web/demo/controllers/admin
echo __FILE__;
// /web/demo/controllers/admin/UserController.php
echo getcwd();
// 在入口文件index.php中调用的此文件,所以是index.php的目录
// /web/demo/web
然后我们在UserController.php中require_once submit.php
我们可能会跟上面的例子那样,以当前文件为基准去写相对路径1
require_once "../../alipay/lib/submit.php"
然后我们运行,却发现报错了Failed opening required '../../alipay/lib/submit.php
“UserController.php所在目录(admin)的上级目录(controllers)的上级目录(demo)下面的alipay下面的lib”
“对啊,../../没错啊”
仔细思考后 我们的url localhost/demo/web/index.php?r=admin/user/list
我们这里的相对路径是以index.php为基准的
所以这里我们应该这么写1
require_once "../alipay/lib/submit.php"
运行发现成功
其实这里也可以这么写相对路径1
2
3
4
5
6
7// index.php文件所在目录(web)的上级目录(demo)下的alipay
require_once getcwd()."/../alipay/lib/submit.php"
// 或者
// 当前文件目录(admin)的上级目录(controllers)的上级目录(demo)下的alipay
require_once __DIR__."/../../alipay/lib/submit.php"
如果我们在入口文件(index.php
)定义一个全局的变量1
define('BASE_PATH', str_replace('\\', '/', realpath(dirname(__FILE__) . '/../')));
不管在哪里1
echo BASE_PATH
都会输出我们的根目录/web/demo
那么刚才的可以写成绝对路径1
require_once BASE_PATH."/alipay/lib/submit.php"
这样代码迁移起来也更方便
]]>常用的开发主要分4块
1.access_token的获取和存放
:获取access_token的接口是有调用频率的限制的,我们不能每次需要的时候都去调用接口,我们可以将其写入文件,或者写入缓存。access_token的有效期目前为2个小时,每次拿的时候先将我们存放的时间和当前时间作对比,超过7200秒才去请求接口,否则直接拿文件或缓存里面存放的access_token
2.JS-SDK
:这块主要是应用于微信的网页开发,可以直接使用微信提供的包括扫描二维码,上传图片,分享,定位等很多接口。具体可见JS-SDK文档和官方demo。所有需要使用JS-SDK的页面必须先注入配置信息,获取配置信息的接口,需要一个jsapi_ticket,和access_token一样,有效期也是2个小时,这里我们要做的也是存放jsapi_ticket和拿取配置信息注意
:这里需要到微信公众平台里面设置我们的JS接口安全域名,否则是用不了的
3.网页授权
:这里主要是网页授权获取用户信息,然后插入或者更新的user表
注意
:这里需要到微信公众平台里面设置我们网页授权域名
4.服务器配置
:这里主要是作为一个回调,可以监听到用户的事件,比如关注,取关,click事件,接受用户发送的信息,这样我们可以跟各种事件或者信息来进行我们的逻辑
1.access_token的获取和存放
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
class Token
{
/**
* 微信配置信息
* @var array
*/
private function createAccessToken()
{
//微信配置
$appid = Yii::$app->params['WeChat']['AppID'];
$corpsecret = Yii::$app->params['WeChat']['AppSecret'];
$url = "https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential&appid=$appid&secret=$corpsecret";
$accessTokenJSON = file_get_contents($url);
$accessTokenArr = json_decode($accessTokenJSON, true);
$accessTokenArr['timestamp'] = time();
$write = $this->createFile(json_encode($accessTokenArr));
if (isset($accessTokenArr['access_token']) && $write) {
return $accessTokenArr['access_token'];
} else {
die($accessTokenJSON);
}
}
/**
* 读取accessToken
* @return string
*/
public function getAccessToken()
{
$currentTimestamp = time(); // 当前时间戳
$accessTokenJSON = file_get_contents(__DIR__ . '/../wechat/params/token.json');
$accessTokenArr = json_decode($accessTokenJSON, true);
$timestamp = $currentTimestamp - $accessTokenArr['timestamp'];
if ($timestamp < 7200) {// token 有效期2个小时
return $accessTokenArr['access_token'];
} else {//请求接口
return $this->createAccessToken();
}
}
/**
* 创建临时文件
* @param $content
* @return boolean 创建文件是否成功
*/
public function createFile($content)
{
$filename = __DIR__ . '/../wechat/params/token.json';
$file = fopen($filename, "w") or die("Unable to open file!");
$txt = $content;
$count = strlen($content);
if ($count === fwrite($file, $txt)) {// 根据写入内容长度判断文件是否写入,
fclose($file);
return true;
} else {
fclose($file);
return false;
}
}
}
2.JS-SDK
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87class Jssdk
{
private $appId;
private $appSecret;
public function __construct()
{
$this->appId = Yii::$app->params['WeChat']['AppID'];
$this->appSecret = Yii::$app->params['WeChat']['AppSecret'];
}
public function getSignPackage()
{
$jsapiTicket = $this->getJsApiTicket();
// 注意 URL 一定要动态获取,不能 hardcode.
$protocol = (!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off' || $_SERVER['SERVER_PORT'] == 443) ? "https://" : "http://";
$url = "$protocol$_SERVER[HTTP_HOST]$_SERVER[REQUEST_URI]";
$timestamp = time();
$nonceStr = $this->createNonceStr();
// 这里参数的顺序要按照 key 值 ASCII 码升序排序
$string = "jsapi_ticket=$jsapiTicket&noncestr=$nonceStr×tamp=$timestamp&url=$url";
$signature = sha1($string);
$signPackage = array(
"appId" => $this->appId,
"nonceStr" => $nonceStr,
"timestamp" => $timestamp,
"url" => $url,
"signature" => $signature,
"rawString" => $string
);
return $signPackage;
}
private function createNonceStr($length = 16)
{
$chars = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";
$str = "";
for ($i = 0; $i < $length; $i++) {
$str .= substr($chars, mt_rand(0, strlen($chars) - 1), 1);
}
return $str;
}
private function getJsApiTicket()
{
// jsapi_ticket 应该全局存储与更新,以下代码以写入到文件中做示例
$data = json_decode($this->get_ticket_file(), true);
if ($data['expire_time'] < time()) {
$tokenClass = new Token();
$accessToken = $tokenClass->getAccessToken();
$url = "https://api.weixin.qq.com/cgi-bin/ticket/getticket?type=jsapi&access_token=$accessToken";
$resJson = file_get_contents($url);
$res = json_decode($resJson, true);
if (isset($res['ticket'])) {
$ticket = $res['ticket'];
$data['expire_time'] = time() + 7200;
$data['jsapi_ticket'] = $ticket;
$this->set_php_file(json_encode($data));
} else {
die($resJson);
}
} else {
$ticket = $data['jsapi_ticket'];
}
return $ticket;
}
private function get_ticket_file()
{
return file_get_contents(__DIR__ . "/../wechat/params/jsapi_ticket.json");
}
private function set_php_file($content)
{
$filename = __DIR__ . "/../wechat/params/jsapi_ticket.json";
$fp = fopen($filename, "w");
fwrite($fp, $content);
fclose($fp);
}
}
通过getSignPackage()获取配置信息后就可以在网页部分调用了1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20<script src="http://res.wx.qq.com/open/js/jweixin-1.0.0.js"></script>
<script>
wx.config({
debug: true,
appId: '<?php echo $signPackage["appId"];?>',
timestamp: echo $signPackage["timestamp"];,
nonceStr: 'echo $signPackage["nonceStr"];',
signature: 'echo $signPackage["signature"];',
jsApiList: [
// 所有要调用的 API 都要加到这个列表中
]
});
wx.ready(function () {
// 在这里调用 API
'checkJsApi',
'onMenuShareTimeline',
'onMenuShareAppMessage',
//......
});
</script>
3.网页授权
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
public function oauth()
{
if (!isset($_SESSION['user'])) {
if (isset($_GET['code'])) {//获取到授权code
$code = $_GET['code'];
$appid = Yii::$app->params['WeChat']['AppID'];
$secret = Yii::$app->params['WeChat']['AppSecret'];
//获取access_token和openid
$url = "https://api.weixin.qq.com/sns/oauth2/access_token?appid=$appid&secret=$secret&code=$code&grant_type=authorization_code";
$getTokenJson = file_get_contents($url);
$getTokenArray = json_decode($getTokenJson, true);
$openId = $getTokenArray['openid'];
$accessToken = $getTokenArray['access_token'];
//查询本地数据库判断用户是否存在
$user = UserService::getUserByOpenId($openId);
//请求微信接口获取用户信息
$url = "https://api.weixin.qq.com/sns/userinfo?access_token=$accessToken&openid=$openId&lang=zh_CN";
$getTokenUser = file_get_contents($url);
$getTokenUser = json_decode($getTokenUser, true);
//包括openid,nickname,sex,province,city,country,headimgurl
$data = array(
"subscribe" => 1,
"name" => $getTokenUser["nickname"],
"openid" => $getTokenUser["openid"],
"headimgurl" => $getTokenUser["headimgurl"],
"sex" => $getTokenUser["sex"],
);
//用户存在则更新最新信息
if ($user) {
$user_id = $user['id'];
UserService::udateUser($data, $user_id);
} else {
$user_id = UserService::addUser($data);
}
$_SESSION['user'] = UserService::getUserByUserId($user_id);
} else {//请求oauth2授权
$callback = urlencode("http://" . $_SERVER['HTTP_HOST'] . strip_tags($_SERVER['REQUEST_URI']));
$state = '';
$url = "https://open.weixin.qq.com/connect/oauth2/authorize?appid=$appid&redirect_uri=$callback&response_type=code&scope=snsapi_userinfo&state=$state#wechat_redirect";
header("Location: $url");
exit;
}
}
return $_SESSION['user'];
}
4.服务器配置
这里可以用到微信公众平台PHP-SDK中的wechat.class.php
库
1 | class Weixin |
这里可以用的2张表菜单表
id | type | name | key | url | pid | order |
---|---|---|---|---|---|---|
1 | view | 菜单一 | http://m.baidu.com | 0 | 1 | |
2 | view | 菜单二 | key | http://m.baidu.com | 0 | 2 |
3 | click | 菜单三 | 0 | 3 | ||
4 | view | 子菜单 | http://m.baidu.com | 3 | 1 |
关键字回复表
id | type | title | description | url | image | key |
---|---|---|---|---|---|---|
1 | news | 菜单三的回复 | 描述 | http://m.baidu.com | 1.jpg | key |
2 | news | 欢迎关注 | 描述 | http://m.baidu.com | 2.jpg | subscribe |
3 | text | 测试 | http://m.baidu.com | 测试 |
可以看到菜单3是click按钮,点击菜单3(key)会回复一个图文消息
关注事件也会回复一个图文消息
给公众号发测试
会回复一条文字消息
大致就这些了,如有错误欢迎指正
具体可参考微信开发者文档
]]>这里实现的是一个底图和一个二维码合成并加上水印的功能,其中二维码可以自定义缩放比例
先是生成一个底图画布,然后生成相关大小的二维码图片的临时文件,然后将两张图合成,最后在画布上写入文件
1 |
|
相关的小项目可以看githu和demo更加的直观
demo用get自定义二维码内容和水印内容,不妨点击去试试~
github的地址是:https://github.com/likangjun/image
demo: http://demo.likangjun.com/image/index.php?qrcode=http://likangjun.com&water=likangjun.com
这里是用MySQL
和txt
文件来保存的聊天记录,然后用js去不断的请求聊天内容展现到页面上而达到了即时的效果,这里也可以用redis
这种nosql
。之前还在网上看到过一个PHP+Swoole
扩展的即时聊天项目,地址是https://github.com/matyhtf/php-webim ,也值得去学习学习
先看下文件结构1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39chat
├─ ajax.php
├─ chat.php
├─ conn.inc
├─ css
│ ├─ main.css
│ └─ style.css
├─ get.php
├─ image
│ ├─ bg.jpg
│ ├─ close.png
│ ├─ logo.jpg
│ └─ user.jpg
├─ index.php
├─ jquery-1.6.min.js
├─ log
│ └─ 20161026.txt
├─ login.php
├─ logout.php
└─ README.md
log
是聊天记录存放目录
conn
是用来链接数据库的
index
,login
,logout
是登录,退出这些页面
ajax
是用于接受信息并储存在数据库或文件
最主要的还是chat.php
和get.php
chat
是聊天室页面,用ajax
不断的请求get.php
获得聊天内容并展示get.php
里把第一次请求时的聊天条数和第一次以后的聊天条数作对比,然后用死循环去不断查询数据库或者读本地文件获取聊天信息返回给chat.php
,从而形成了即时聊天
这个只是一个简单粗暴的方法,性能和体验上还达不到要求,后续也将会去学习研究更好的解决方案
]]>然后在hexo根部目录下_config.yml
里配置自己的多说账号1
2# Comments
duoshuo_shortname: liakngjun
记得: 后面有一个空格
最后在主题文件themes\landscape\layout\_partial\article.ejs
找到1
2
3
4
5
6
7<% if (!index && post.comments && config.disqus_shortname){ %>
<section id="comments">
<div id="disqus_thread">
<noscript>Please enable JavaScript to view the <a href="//disqus.com/?ref_noscript">comments powered by Disqus.</a></noscript>
</div>
</section>
<% } %>
改为1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20<% if (!index && post.comments && config.duoshuo_shortname){ %>
<section id="comments">
<!-- 多说评论框 start -->
<div class="ds-thread" data-thread-key="<%= post.layout %>-<%= post.slug %>" data-title="<%= post.title %>" data-url="<%= page.permalink %>"></div>
<!-- 多说评论框 end -->
<!-- 多说公共JS代码 start (一个网页只需插入一次) -->
<script type="text/javascript">
var duoshuoQuery = {short_name:'<%= config.duoshuo_shortname %>'};
(function() {
var ds = document.createElement('script');
ds.type = 'text/javascript';ds.async = true;
ds.src = (document.location.protocol == 'https:' ? 'https:' : 'http:') + '//static.duoshuo.com/embed.js';
ds.charset = 'UTF-8';
(document.getElementsByTagName('head')[0]
|| document.getElementsByTagName('body')[0]).appendChild(ds);
})();
</script>
<!-- 多说公共JS代码 end -->
</section>
<% } %>
也可以看到,我们可以写博客的时候通过comments来控制这篇是否开启评论
]]>又是一个Hello World
上次写博客的时候还是WordPress
现在在jayxhj的推荐下用Hexo + github 搭建了这个静态博客
后续会做一些博客迁移
并将继续与大家交流,并分享一些有趣的技术