0%

所有的时光都是被辜负被浪费后,才能从记忆里将某一段拎出来,拍拍上面沉积的灰尘,感叹它是最好的时光。

代码片段一

1
2
3
4
5
for (var i = 0 ; i < 5; i++) {
setTimeout(function () {
console.log(i);
}, 1000);
}

结果:

在控制台下, 一秒后输出5个5, 没有时间间隔;

分析:

setTimeout的执行异步的, for 循环的执行是很快的, 当setTimeout开始执行的时候,for循环已经执行完毕, 此时的i变为5(for循环使用var申明的变量是全局的), 因为所有的都是延迟1S执行, 所以最后一起输出;

代码片段二

1
2
3
4
5
for (var i = 0 ; i < 5; i++) {
setTimeout(function () {
console.log(i);
}, i * 1000);
}

结果:

在控制台下, 每隔一秒输出一个5;

分析:

为什么是5, 上一个代码片段已经分析, 每一秒输出一个的原因就是for循环过程中调用setTimeout的时候,调用5次时间非常短可忽略不计, 调用依次给的延迟秒数是0s,1s,2s,3s,4s.所以我们再最后看到他执行的时候, 是每隔一秒出现一个;

代码片段三

1
2
3
4
5
6
7
for (var i = 0 ; i < 5; i++) {
(function () {
setTimeout(function () {
console.log(i);
}, i*1000);
})();
}

结果:

在控制台下, 每隔一秒输出一个5, 和代码片段二的结果一样;

分析:
虽然加了闭包, 但是并没有影响结果

代码片段四

1
2
3
4
5
6
7
for (var i = 0 ; i < 5; i++) {
(function (i) {
setTimeout(function () {
console.log(i);
}, i*1000);
})(i);
}

结果:

在控制台下,每隔一秒依次输出0,1,2,3,4;

分析:
此时setTimeout里面的参数是通过闭包传递的, 并没有受到全局i的影响

代码片段五

1
2
3
4
5
6
7
for (let i = 0 ; i < 5; i++) {
(function () {
setTimeout(function () {
console.log(i);
}, i*1000);
})();
}

结果:

在控制台下,每隔一秒依次输出0,1,2,3,4;

分析:

使用let申明的变量是块级作用域, 当前的i只在本轮循环有效,所以每一次循环的i其实都是一个新的变量;

更多关于let var 的区别, 请参考我的另外一篇文字: var,let,const的区别

在硝烟中想起冰棒汽水的味道 和那些无所事事一整个夏天的年少

什么是 RESTful

RESTful 是一种软件设计风格,由 Roy Fielding 在他的 论文 中提出,全称为 Representational State Transfer,直译为表现层状态转移,或许可以解释为用 URL 定位资源,用 HTTP 动词描述操作,不用太纠结于定义,接下来我们会详细讨论。

RESTful 风格的接口,目前来看,实现的最好的就是 Github API,经常被效仿。接下来我们通过分析 Github API 来引出我们的 API 设计原则。

为什么选择 RESTful

我认为一套接口应该尽量满足以下几个原则:

  • 安全可靠,高效,易扩展。
  • 简单明了,可读性强,没有歧义。
  • API 风格统一,调用规则,传入参数和返回数据有统一的标准。

我们当然可以根据自己的经验,或者参考知名公司的接口总结设计出一套满足要求的接口,但是每个人对接口的理解不同,设计出来的接口也会有所不同,接口的命名,请求参数的格式,响应的结果,错误响应的错误码,等等很多地方都会有不一样的实现。当你去寻求一种设计理念来帮助我们设计出满足要求的接口,一定会发现 RESTful。
RESTful 的设计理念基于 HTTP 协议,因为 Roy Fielding 就是 HTTP 协议(1.0版和1.1版)的主要设计者。它是一种设计风格,没有规定我们一定如何实现,但是为我们提供了很好的设计理念。风格的统一,使得我们不需要过多的解释,就能让使用者明白该如何使用,同时也会有很多现成的工具来帮助我们实现 RESTful 风格的接口。

RESTful 设计原则

1. HTTPS

HTTPS 为接口的安全提供了保障,可以有效防止通信被窃听和篡改。而且现在部署 HTTPS 的成本也越来越低,你可以通过 cerbot 等工具,方便快速的制作免费的安全证书,所以生产环境,请务必使用 HTTPS。

另外注意,非 HTTPS 的 API 调用,不要重定向到 HTTPS,而要直接返回调用错误以禁止不安全的调用。

2. 域名

应当尽可能的将 API 与其主域名区分开,可以使用专用的域名,访问我们的 API,例如:

1
https://api.vxndy.com

或者可以放在主域名下,例如:

1
https://www.vxndy.com/api

3. 版本控制

随着业务的发展,需求的不断变化,API 的迭代是必然的,很可能当前版本正在使用,而我们就得开发甚至上线一个不兼容的新版本,为了让旧用户可以正常使用,为了保证开发的顺利进行,我们需要控制好 API 的版本。

通常情况下,有两种做法:

  • 将版本号直接加入 URL 中
    1
    2
    https://api.vxndy.com/v1
    https://api.vxndy.com/v2
  • 使用 HTTP 请求头的 Accept 字段进行区分
    1
    2
    3
    https://api.larabbs.com/
    Accept: application/prs.vxndy.v1+json
    Accept: application/prs.vxndy.v2+json
    Github Api 虽然默认使用了第一种方法,但是其实是推荐并实现了第二种方法的,我们同样也尽量使用第二种方式。

4. 用 URL 定位资源

在 RESTful 的架构中,所有的一切都表示资源,每一个 URL 都代表着一种资源,资源应当是一个名词,而且大部分情况下是名词的复数,尽量不要在 URL 中出现动词。
先来看看 github 的 例子:

1
2
3
4
5
6
7
GET /issues                                      列出所有的 issue
GET /orgs/:org/issues 列出某个项目的 issue
GET /repos/:owner/:repo/issues/:number 获取某个项目的某个 issue
POST /repos/:owner/:repo/issues 为某个项目创建 issue
PATCH /repos/:owner/:repo/issues/:number 修改某个 issue
PUT /repos/:owner/:repo/issues/:number/lock 锁住某个 issue
DELETE /repos/:owner/:repo/issues/:number/lock 接收某个 issue

例子中冒号开始的代表变量,例如 /repos/summerblue/larabbs/issues

在 github 的实现中,我们可以总结出:

  • 资源的设计可以嵌套,表明资源与资源之间的关系。
  • 大部分情况下我们访问的是某个资源集合,想得到单个资源可以通过资源的 id 或number 等唯一标识获取。
  • 某些情况下,资源会是单数形式,例如某个项目某个 issue 的锁,每个 issue 只会有一把锁,所以它是单数。

错误的例子

1
2
3
4
POST https://api.larabbs.com/createTopic
GET https://api.larabbs.com/topic/show/1
POST https://api.larabbs.com/topics/1/comments/create
POST https://api.larabbs.com/topics/1/comments/100/delete

正确的例子

1
2
3
4
POST https://api.larabbs.com/topics
GET https://api.larabbs.com/topics/1
POST https://api.larabbs.com/topics/1/comments
DELETE https://api.larabbs.com/topics/1/comments/100

5. 用 HTTP 动词描述操作

HTTP 设计了很多动词,来表示不同的操作,RESTful 很好的利用的这一点,我们需要正确的使用 HTTP 动词,来表明我们要如何操作资源。
先来解释一个概念,幂等性,指一次和多次请求某一个资源应该具有同样的副作用,也就是一次访问与多次访问,对这个资源带来的变化是相同的。

常用的动词及幂等性

1
2
3
4
5
6
动词	描述	                              是否幂等
GET 获取资源,单个或多个 是
POST 创建资源 否
PUT 更新资源,客户端提供完整的资源数据 是
PATCH 更新资源,客户端提供部分的资源数据 否
DELETE 删除资源 是

为什么 PUT 是幂等的而 PATCH 是非幂等的,因为 PUT 是根据客户端提供了完整的资源数据,客户端提交什么就替换为什么,而 PATCH 有可能是根据客户端提供的参数,动态的计算出某个值,例如每次请求后资源的某个参数减1,所以多次调用,资源会有不同的变化。

另外需要注意的是,GET 请求是安全的,不允许通过 GET 请求改变(更新或创建)资源,但是真实使用中,为了方便统计类的数据,会有一些例外情况,例如帖子详情,记录访问次数,每调用一次,访问次数 +1;

6. 资源过滤

我们需要提供合理的参数供客户端过滤资源,例如

1
2
3
?state=closed: 不同状态的资源
?page=2&per_page=100:访问第几页数据,每页多少条。
?sortby=name&order=asc:指定返回结果按照哪个属性排序,以及排序顺序。

7. 正确使用状态码

HTTP 提供了丰富的状态码供我们使用,正确的使用状态码可以让响应数据更具可读性。

  • 200 OK - 对成功的 GET、PUT、PATCH 或 DELETE 操作进行响应。也可以被用在不创建新资源的 POST 操作上
  • 201 Created - 对创建新资源的 POST 操作进行响应。应该带着指向新资源地址的 Location 头
  • 202 Accepted - 服务器接受了请求,但是还未处理,响应中应该包含相应的指示信息,告诉客户端该去哪里查询关于本次请求的信息
  • 204 No Content - 对不会返回响应体的成功请求进行响应(比如 DELETE 请求)
  • 304 Not Modified - HTTP缓存header生效的时候用
  • 400 Bad Request - 请求异常,比如请求中的body无法解析
  • 401 Unauthorized - 没有进行认证或者认证非法
  • 403 Forbidden - 服务器已经理解请求,但是拒绝执行它
  • 404 Not Found - 请求一个不存在的资源
  • 405 Method Not Allowed - 所请求的 HTTP 方法不允许当前认证用户访问
  • 410 Gone - 表示当前请求的资源不再可用。当调用老版本 API 的时候很有用
  • 415 Unsupported Media Type - 如果请求中的内容类型是错误的
  • 422 Unprocessable Entity - 用来表示校验错误
  • 429 Too Many Requests - 由于请求频次达到上限而被拒绝访问

8. 数据响应格式

考虑到响应数据的可读性及通用性,默认使用 JSON 作为数据响应格式。如果客户端有需求使用其他的响应格式,例如 XML,需要在 Accept 头中指定需要的格式。

1
2
3
https://api.vxndy.com/
Accept: application/prs.vxndy.v1+json
Accept: application/prs.vxndy.v1+xml

对于错误数据,默认使用如下结构:

1
2
3
4
5
'message' => ':message',          // 错误的具体描述
'errors' => ':errors', // 参数的具体错误描述,422 等状态提供
'code' => ':code', // 自定义的异常码
'status_code' => ':status_code', // http状态码
'debug' => ':debug', // debug 信息,非生产环境提供

例如

1
2
3
4
5
6
7
8
9
{
"message": "422 Unprocessable Entity",
"errors": {
"name": [
"姓名 必须为字符串。"
]
},
"status_code": 422
}
1
2
3
4
{
"message": "您无权访问该订单",
"status_code": 403
}

9. 调用频率限制

为了防止服务器被攻击,减少服务器压力,需要对接口进行合适的限流控制,需要在响应头信息中加入合适的信息,告知客户端当前的限流情况

  • X-RateLimit-Limit :100 最大访问次数
  • X-RateLimit-Remaining :93 剩余的访问次数
  • X-RateLimit-Reset :1513784506 到该时间点,访问次数会重置为 X-RateLimit-Limit

10. 编写文档

为了方便用户使用,我们需要提供清晰的文档,尽可能包括以下几点

  • 包括每个接口的请求参数,每个参数的类型限制,是否必填,可选的值等。
  • 响应结果的例子说明,包括响应结果中,每个参数的释义。
  • 对于某一类接口,需要有尽量详细的文字说明,比如针对一些特定场景,接口应该如何调用。

https://developer.github.com/v3/

我从来就没有太阳 所以不怕失去。

什么是AOP?

AOP编程,也叫做面向切面编程,是一种非倾入式编程的方法,采用外部注入的方式来取代嵌入代码。可以实现非常好的模块低耦合。
假设你的框架有一个 Frameworkd::init方法,功能是初始化框架资源。现在有db,template的初始化也需要在这个阶段执行,传统的做法就是只能修改 Frameworkd::init在里面加入 db,template的方法调用。未来如果增加了新的模块,比如cache。那就需要修改Frameworkd::init的代码。这种做法显然是侵入性的。
当然也可以用hook list的方式来实现。在需要外部注入的地方加入一个hook list,遍历执行外部注入的接口。但远没有AOP强大,而且还需要不断加入hook list的遍历点。

示例

假设我们要给程序中的每个方法在他们执行前后都要进行日志输出, 一般我们都会这么写

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<?php
/**
* Test
*/
class Test
{
/**
* 某个方法
*/
public function doSomething()
{
// 初始化 LOG
$logger = new Log();
// 写log
$logger->save('before do something.');
// 程序功能代码
// ...
// 写log
$logger->save('before do something.');
}
}

可是如果今天这个记录 log 的这个动作只是临时的,或是在未来可能会需要再加入不同的动作时 (例如邮件) ,难道我们还要在原有方法的代码里修修改改吗?有没有什麽方式能协助我们动态地把记录的动作插在原有动作之后呢?

AOP 就是从这个角度所延伸出来的一种观念,它能协助我们在不侵入原有类别程式码的状况下,动态地为类别方法新增额外的权责;简单来说, AOP 主要的目的就是切入类别原有方法执行之前或之后,并安插我们想要执行的动作。

AOP和装饰者模式

其实一开始我以为 AOP 和 Decorator 模式在 PHP 上的实作方式是差不多的,不过实际上还有是些许的差别。
一般在 Decorator 模式中,具体类别和 Wrapper 类别都会有个共同的祖先,亦即一个抽象类别或介面,因此所产生出来的物件对 Client 程式来说,其抽象型态可以说是一样的。

但是在 AOP in PHP 中,我们必须透过一个代理类别来切入原有的类别方法裡,虽然这个代理类别也能够提供原有类别中的所有方法,但是实际上它却已经失去了与原有类别所拥有的抽象型态了。

用PHP实现AOP

首先先建一个类

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
<?php
/**
* Test class
*
*/
class TestClass
{
/**
* Method 1
*
* @param string $message
*/
public function method1($message)
{
echo "\n", __METHOD__, ":\n", $message, "\n";
}
/**
* Method 2
*
* @return int
*/
public function method2()
{
echo "\n", __METHOD__, ":\n";
return rand(1, 10);
}
/**
* Method 3
*
* @throws Exception
*/
public function method3()
{
echo "\n", __METHOD__, ":\n";
throw new Exception('Test Exception.');
}
}

这个类别提供了三个方法,其中 method1 和 method2 只是简单的显示资料而已,而 method3 则会丢出一个异常。

另外我们需要一个 Log 类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<?php
/**
* Log
*
*/
class Log
{
/**
* log message
*
* @param string $message
*/
public function save($message)
{
echo $message, "\n";
}
}

这个 Log 类别只提供一个 save() 方法,以显示 log 讯息。

现在我们要完成的目标如下:

在 method1 执行前呼叫 Log::save() 。

在 method2 执行后呼叫 Log::save() 。

在 method3 发生异常时呼叫 Log::save() 。

这里我用很简单的方式来做,那就是直接使用一个 Aspect 类:

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
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
<?php
/**
* Aspect
*
*/
class Aspect
{
/**
* Name of target class
*
* @var string
*/
private $_className = null;
/**
* Target object
*
* @var object
*/
private $_target = null;
/**
* Event callback
*
* @var array
*/
private $_eventCallbacks = array();
/**
* Add object
*
* @param object $target
* @return Aspect
*/
public static function addObject($target)
{
return new Aspect($target);
}
/**
* Contructor
*
* @param object $target
*/
public function __construct($target)
{
if (is_object($target)) {
$this->_target = $target;
$this->_className = get_class($this->_target);
}
}
/**
* Register event
*
* @param string $eventName
* @param string $methodName
* @param callback $callback
*/
private function _registerEvent($eventName, $methodName, $callback, $args)
{
if (!isset($this->_eventCallbacks[$methodName])) {
$this->_eventCallbacks[$methodName] = array();
}
if (!is_callable(array($this->_target, $methodName))) {
throw new Exception(get_class($this->_target) . '::' . $methodName . ' is not exists.');
}
if (is_callable($callback)) {
$this->_eventCallbacks[$methodName]($eventName) = array($callback, $args);
} else {
$callbackName = Aspect::getCallbackName($callback);
throw new Exception($callbackName . ' is not callable.');
}
}
/**
* Register 'before' handler
*
* @param string $methodName
* @param callback $callback
*/
public function before($methodName, $callback, $args = array())
{
$this->_registerEvent('before', $methodName, $callback, (array) $args);
}
/**
* Register 'after' handler
*
* @param string $methodName
* @param callback $callback
*/
public function after($methodName, $callback, $args = array())
{
$this->_registerEvent('after', $methodName, $callback, (array) $args);
}
/**
* Register 'on catch exception' handler
*
* @param string $methodName
* @param callback $callback
*/
public function onCatchException($methodName, $callback, $args = array())
{
$this->_registerEvent('onCatchException', $methodName, $callback, (array) $args);
}
/**
* Trigger event
*
* @param string $eventName
*/
private function _trigger($eventName, $methodName, $target)
{
if (isset($this->_eventCallbacks[$methodName]($eventName))) {
list($callback, $args) = $this->_eventCallbacks[$methodName]($eventName);
$args[] = $target;
call_user_func_array($callback, $args);
}
}
/**
* Execute method
*
* @param string $methodName
* @param array $args
* @return mixed
*/
public function __call($methodName, $args)
{
if (is_callable(array($this->_target, $methodName))) {
try {
$this->_trigger('before', $methodName, $this->_target);
$result = call_user_func_array(array($this->_target, $methodName), $args);
$this->_trigger('after', $methodName, $this->_target);
return $result ? $result : null;
} catch (Exception $e) {
$this->_trigger('onCatchException', $methodName, $e);
throw $e;
}
} else {
throw new Exception("Call to undefined method {$this->_className}::$methodName.");
}
}
/**
* Get name of callback
*
* @param callback $callback
* @return string
*/
public static function getCallbackName($callback)
{
$className = '';
$methodName = '';
if (is_array($callback) &amp;&amp; 2 == count($callback)) {
if (is_object($callback[0])) {
$className = get_class($callback[0]);
} else {
$className = (string) $callback[0];
}
$methodName = (string) $callback[1];
} elseif (is_string($callback)) {
$methodName = $callback;
}
return $className . (($className) ? '::' : '') . $methodName;
}
}

这个类别有点小长,简单说明如下:

我们利用 Aspect::addObject() 方法来指定要被切入的物件; addObject() 方法会回传一个透明的 Aspect 物件。
利用 beforeafteronCatchException 三个方法来指定切入的时机,它们会呼叫 _registerEvent() 方法来注册要执行的回呼函式 (callback)

执行原来被切入物件的方法,这时会触动 Aspect__call()方法,并在指定的切入时机呼叫 _trigger()` 方法来执行我们所切入的回呼函式。

我们利用 Aspect 类别来对 TestClass 物件的三个方法切入 Log::save() :

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
<?php
require_once 'Aspect.php';
require_once 'TestClass.php';
require_once 'Log.php';
$test = Aspect::addObject(new TestClass());
$logger = new Log();
$test->before('method1', array($logger, 'save'), 'Log saved (method1).');
$test->after('method2', array($logger, 'save'), 'Log saved (method2).');
$test->onCatchException('method3', array($logger, 'save'), 'Log saved (method3).');
/* @var $test TestClass */
echo "=======\n";
$test->method1('abc');
echo "=======\n";
echo $test->method2(), "\n";
echo "=======\n";
$test->method3();
echo "=======\n";
/* 執行結果:
=======
Log saved (method1).
TestClass::method1:
abc
=======
TestClass::method2:
Log saved (method2).
8
=======
TestClass::method3:
Log saved (method3).
Exception: Test Exception. in TestClass.php on line 38
*/

http://rango.swoole.com/archives/83
http://jaceju.net/2008-04-14-php-aop/

嗯,对,你说得对。

调用wx.login

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
//app.js
App({
onLaunch: function() {
wx.login({
success: function(res) {
if (res.code) {
//发起网络请求
wx.request({
url: 'https://test.com/onLogin',
data: {
code: res.code
}
})
} else {
console.log('登录失败!' + res.errMsg)
}
}
});
}
})

onLogin接口,php实现

1
2
3
4
5
6
7
$APPID = 'wxf007410...';
$SECRET = '1bb26d74f33910588ee....';
$request = request();
$code = $request->code;
$url = 'https://api.weixin.qq.com/sns/jscode2session?appid='.$APPID.'&secret='.$SECRET.'&js_code='.$code.'&grant_type=authorization_code';
$client = new \GuzzleHttp\Client();
$response = $client->get($url)->getBody()->getContents();

返回数据

1
{"session_key":"VUNQTngHJYeceYRrQpj0cQ==","openid":"o3CTb4kuzH674QgeFsKhyoipfPi4"}  

登录成功后调用获取用户接口

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
success: function (res) {
// 登录成功
console.log(res.data)
wx.getUserInfo({
success: function (res) {
var userInfo = res.userInfo
var nickName = userInfo.nickName
var avatarUrl = userInfo.avatarUrl
var gender = userInfo.gender //性别 0:未知、1:男、2:女
var province = userInfo.province
var city = userInfo.city
var country = userInfo.country

console.log('获取用户信息返回')
console.log(res);
}
})

在登录状态下返回的信息包含敏感数据,将这些敏感数据提交到服务器进行解密

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
console.log('获取用户信息返回');
console.log(res);
wx.request({
method: 'POST',
url: 'https://vxndy.com/user', //仅为示例,并非真实的接口地址
data: {
rawData: res.rawData,
signature: res.signature,
encryptedData : res.encryptedData,
iv : res.iv
},
header: {
'content-type': 'application/json' // 默认值
},
success: function (res) {
console.log('调用解密接口返回')
console.log(res)
}
})

user接口实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
$APPID = 'wxf0074104e....';
$request = request();
$rawData = $request->rawData;
$signature = $request->signature;
$encryptedData = $request->encryptedData;
$iv = $request->iv;

// 解密数据
$path = storage_path('session_key');
info('从文件中获取session_key');
$session_key = file_get_contents($path);

$pc = new \App\Wx\WXBizDataCrypt($APPID, $session_key);
$errCode = $pc->decryptData($encryptedData, $iv, $data );
if ($errCode == 0) {
return response()->json($data);
} else {
info('错误');
info($errCode);
}
return response()->json(['status' => 200]);

返回的数据包括 openId 和 用户其他信息

注意

每次发起登录请求的时候应该先检测是否需要登录

1
2
3
4
5
6
7
8
9
10
wx.checkSession({
success: function(){
//session 未过期,并且在本生命周期一直有效
},
fail: function(){
//登录态过期
wx.login() //重新登录
....
}
})

世人谓我恋长安,其实只恋长安某。
今天在做按照商品的SPU批量采购的时候, 遇到了一个问题, 就是安装SPU`groupBy`后计算的`sum`值仅仅只是每个SPU下的产品数量之和, 这可以满足在每个列表中的正常显示,但是现在需要在页面的顶部显示一个全部的SPU下的产品数量, 因为数据是分页的, 就很难计算了.起初我的代码是这样的:
1
2
3
4
$query = WaitingForPurchase::query()
->selectRaw('id, site_id, ps_type_id, ps_shop_id, ps_product_id, order_product_id, shop_product_id, sum(qty) as qty')
->where(['ps_type_id' => $type_id, 'ps_shop_id' => $shop_id])
->groupBy('shop_product_id')

在GOOGLE了一番以后, 才知道mysql竟然还有这样的用法

1
2
3
4
$query = WaitingForPurchase::query()
->selectRaw('id, site_id, ps_type_id, ps_shop_id, ps_product_id, order_product_id, shop_product_id, sum(qty) as qty')
->where(['ps_type_id' => $type_id, 'ps_shop_id' => $shop_id])
->groupBy(DB::raw('shop_product_id WITH ROLLUP'));

这样得到的结果就是在我们的查询结果中会多出一条数据,用来显示额外的统计信息.
在使用Laravel框架的情况下, 我们还可以这样统计:

1
2
3
4
5
6
7
8
9
10
11
12
$query = WaitingForPurchase::query()
->selectRaw('id, site_id, ps_type_id, ps_shop_id, ps_product_id, order_product_id, shop_product_id, sum(qty) as qty')
->where(['ps_type_id' => $type_id, 'ps_shop_id' => $shop_id]);

WaitingForPurchase::setFilters($query, $conditions);

$sum = $query->sum('qty');

$query->groupBy('shop_product_id')
->orderByDesc('qty');

$items = $query->paginate(20);

...

一般用法

1
2
3
Route::namespace('Admin')->group(function () {
// 在 "App\Http\Controllers\Admin" 命名空间下的控制器
});

进阶用法

如果我们的类不在App\Http\Controllers下, 要使用namespace, 就要修改一些东西
修改文件App\Providers\RouteServiceProvider

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
class RouteServiceProvider extends ServiceProvider
{
/**
* This namespace is applied to your controller routes.
*
* In addition, it is set as the URL generator's root namespace.
*
* @var string
*/
protected $namespace = 'App\Http\Controllers';

protected $apiNamespace = 'App\Http';

......

protected function mapApiRoutes()
{
Route::prefix('api')
->middleware('api')
->namespace($this->apiNamespace)
->group(base_path('routes/api.php'));
}

使用

1
2
3
4
5
6
7
8
Route::namespace('Api')->group(function () {

Route::post('/order/update', 'Order@receive')->name('api.order.receive');
Route::post('/order/delete', 'Order@delete')->name('api.order.delete');

Route::get('/fetch-waybill', 'FetchWaybill@getNotFetchTask')->name('api.fetch-waybill.get');
Route::post('fetch-waybill/callback', 'FetchWaybill@callback')->name('api.fetch-waybill.callback');
});

可我咋觉得你躺在这 我李云龙的半条命 也埋在这儿了……

使用SimpleMDE编辑器

页面上引入css和js文件

1
2
3
4
5
<link rel="stylesheet" href="https://cdn.jsdelivr.net/simplemde/latest/simplemde.min.css">

<textarea name="profile" placeholder="这里请介绍你的专栏内容, 写作计划, 更新进度, 答疑情况等内容"></textarea>

<script src="https://cdn.jsdelivr.net/simplemde/latest/simplemde.min.js"></script>

初始化

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
<script>

var simplemde = new SimpleMDE({
spellChecker: false,
autosave: {
enabled: true,
delay: 5000,
unique_id: "article_content"
},
forceSync: true,
toolbar: [
"bold", "italic", "heading", "|", "quote", "code", "table",
"horizontal-rule", "unordered-list", "ordered-list", "|",
"link", "image", "|", "side-by-side", 'fullscreen', "|",
{
name: "guide",
action: function customFunction(editor) {
var win = window.open('https://github.com/riku/Markdown-Syntax-CN/blob/master/syntax.md', '_blank');
if (win) {
//Browser has allowed it to be opened
win.focus();
} else {
//Browser has blocked it
alert('Please allow popups for this website');
}
},
className: "fa fa-info-circle",
title: "Markdown 语法!"
},
{
name: "publish",
action: function customFunction(editor) {
$('.submit-btn').click();
},
className: "fa fa-paper-plane",
title: "发布文章"
}
],
});

</script>

页面输出解析markdown

安装扩展包

1
composer require erusev/parsedown

使用

1
2
3
$Parsedown = new Parsedown();

echo $Parsedown->text('Hello _Parsedown_!'); # prints: <p>Hello <em>Parsedown</em>!</p>

https://simplemde.com/
https://packagist.org/packages/erusev/parsedown

技术是生产力

简介

由于后期公司业务调整以及业务量增大, 第一版的订单管理系统已经不能很好的满足采购,客服, 仓储,发货等相关人员的需求,于是积极筹备开发第二版.
基于laravel5.5开发,用于所有商城的订单管理,实现了采购,供货,发货, 仓储的完全管理;

功能点

菜单导航

订单列表

订单详情

产品单采

待发货订单

物流停滞核实

退款已申请

立即退款

产品换购

采购供货

运单号抓取

用户信息报错

用户投诉处理

店铺批量采购列表

店铺批量采购

导入采购信息

数据导出中心

站点创建

权限管理

和你们这些少爷不同,我们光是活着就竭尽全力了。

非静态属性

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
<?php

class Foo {

protected $instance = 'Null';

public function setInstance($ins)
{
$this->instance = $ins;
}

public function getInstance()
{
return $this->instance;
}
}

class Bar extends Foo {

}

class Baz extends Foo {

}


$bar = new Bar;
$bar->setInstance('baidu.com');
echo $bar->getInstance(); // baidu.com
echo (new Foo)->getInstance(); // NULL

echo "\n";

$baz = new Baz;
$baz->setInstance('laravel');
echo $baz->getInstance(); // laravel
echo $bar->getInstance(); // baidu.com
echo (new Foo)->getInstance(); // NULL

说明:
非静态属性的继承关系子类会继承父类的属性和方法,子类中的修改并不会影响到父类中属性的改变

静态属性

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
<?php

class Foo {

protected static $instance;

public static function setInstance($ins)
{
static::$instance = $ins;
}

public static function getInstance()
{
return static::$instance;
}
}
class Bar extends Foo {

}

Bar::setInstance('baidu.com');

echo Bar::getInstance(); // baidu.com

echo Foo::getInstance(); // baidu.com

说明:
子类对属性的修改已经影响到了父类, 当然,这是在子类没有设置相同属性的情况下, 此时如果子类设置了相同的静态属性, 则如果对子类中这个属性的修改, 并不会影响到父类

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
<?php

class Foo {

protected static $instance;

public static function setInstance($ins)
{
static::$instance = $ins;
}

public static function getInstance()
{
return static::$instance;
}
}
class Bar extends Foo {
protected static $instance;
}

Bar::setInstance('baidu.com');

echo Bar::getInstance(); // baidu.com

echo Foo::getInstance();

再看一个子类之间相互影响的例子

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
<?php

class Foo {

protected static $instance;

public static function setInstance($ins)
{
static::$instance = $ins;
}

public static function getInstance()
{
return static::$instance;
}
}
class Bar extends Foo {

}

class Baz extends Foo {

}


Bar::setInstance('baidu.com');
echo Bar::getInstance(); // baidu.com
echo Foo::getInstance(); // baidu.com

echo "\n";

Baz::setInstance('laravel');
echo Baz::getInstance(); // laravel
echo Bar::getInstance(); // laravel
echo Foo::getInstance(); // laravel

修改其中一个子类, 父类以及该父类的其他子类都会受到影响

laravel 源码分析

在Laravel 中的 app()这个辅助函数可以获得 一个 Application 对象实例

1
2
3
4
5
6
7
8
function app($abstract = null, array $parameters = [])
{
if (is_null($abstract)) {
return Container::getInstance();
}

return Container::getInstance()->make($abstract, $parameters);
}

进入这个Container类, 发现仅仅是一个静态属性。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class Container implements ArrayAccess, ContainerContract
{
/**
* The current globally available container (if any).
*
* @var static
*/
protected static $instance;

......

public static function getInstance()
{
if (is_null(static::$instance)) {
static::$instance = new static;
}

return static::$instance;
}

然后我们从入口文件看起
index.php

1
$app = require_once __DIR__.'/../bootstrap/app.php';

app.php

1
2
3
$app = new Illuminate\Foundation\Application(
realpath(__DIR__.'/../')
);

Application这个类, 继承了 Container 类, 而Container这个类中有一个静态属性$instance

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
class Application extends Container implements ApplicationContract, HttpKernelInterface
{

......

public function __construct($basePath = null)
{
if ($basePath) {
$this->setBasePath($basePath);
}

$this->registerBaseBindings();

$this->registerBaseServiceProviders();

$this->registerCoreContainerAliases();
}


protected function registerBaseBindings()
{
static::setInstance($this);

$this->instance('app', $this);

$this->instance(Container::class, $this);

$this->instance(PackageManifest::class, new PackageManifest(
new Filesystem, $this->basePath(), $this->getCachedPackagesPath()
));
}

正是是用了静态属性继承这一特性;

http://pilishen.com/posts/Inheritance-with-the-static-elements-in-php-especially-during-late-static-binding

....

自 PHP 5.3.0 起,PHP 增加了一个叫做后期静态绑定的功能,用于在继承范围内引用静态调用的类。
当进行静态方法调用时,该类名即为明确指定的那个(通常在 :: 运算符左侧部分);当进行非静态方法调用时,即为该对象所属的类。
“后期绑定”的意思是说,static:: 不再被解析为定义当前方法所在的类,而是在实际运行时计算的。也可以称之为“静态绑定”,因为它可以用于(但不限于)静态方法的调用。

解释说明

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<?php
class A {
public static function who() {
echo __CLASS__;
}
public static function test() {
self::who();
}
}

class B extends A {
public static function who() {
echo __CLASS__;
}
}

B::test();

以上例程会输出:A, 因为 self 关键词只能取到定义当前方法所在的类, 或者说它只能取到自己所在的那个类, 最终引用的类是A, 而不是B
好吧, 但是如果我们想要运行时最终运行的是B该如何呢?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<?php
class A {
public static function who() {
echo __CLASS__;
}
public static function test() {
static::who(); // 后期静态绑定从这里开始
以上例程会输出:B}

class B extends A {
public static function who() {
echo __CLASS__;
}
}

B::test();

以上例程会输出:B

实际用法

在Laravel框架中

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
<?php

class Model
{
public static function create(array $attributes = [])
{
$model = new static($attributes);

$model->save();

return $model;
}
}

class Task extends Model
{

}

Task::create([
'title'=>'学习laravel',
'author'=>'pilishen'
]);

当执行Task::create()的时候,因为extends了Model,所以就到了Model里的create方法,由于$model = new static($attributes);使用了static关键词,所以此时也就相当于是执行了new Task();,也就是借助static静态绑定,我们在laravel里的自己创建的各个model,就可以共用一系列提前定义好的方法,同时在实际调用的时候,又将结果或过程只作用于自己,从而实现了一个典型的active record design pattern。

其他

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
<?php

class Father
{
public static function getSelf()
{
return new self();
}

public static function getStatic()
{
return new static();
}
}


class Son extends Father {};

echo get_class(Son::getSelf()); // Father

echo get_class(Son::getStatic()); // Son

echo get_class(Father::getSelf()); // Father

echo get_class(Father::getStatic()); // Father

http://pilishen.com/posts/php-late-static-bindings-in-laravel
http://php.net/manual/zh/language.oop5.late-static-bindings.php