微信小程序后端笔记

微信小程序商城构建全栈应用

  • php+微信小程序全栈应用

软件/素材

  • mac os 10.13.3
  • PhpStorm 2018
  • Postman
  • XAMPP 7.0.2-1
  • ThinkPHP 5.0.7

项目目录结构

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
├─application           应用目录
├─api 公共模块目录(可以更改)
│ │-controller 控制器目录 (版本以及业务)
│ │-model 模型目录 (关联模型处理)
│ │-service 模型服务层(相对复杂的业务处理)
│ └─validate 验证层 (客户端数据验证)
├─extra 自定义公共资源层(tp5自带的)
├─lib 模块目录
│ ├─enum 枚举
│ └─exception 全局异常处理目录

├─command.php 命令行工具配置文件
├─common.php 公共函数文件
├─config.php 公共配置文件
├─route.php 路由配置文件
├─tags.php 应用行为扩展定义文件
└─database.php 数据库配置文件

笔记

第八章

数据表关系分析 (写着写着就绕了)

  1. 数据表之间的关系: 1 对 1 1 对多 多对多
    • 如何判断数据表之间的结构
  2. 首先确立是否是一个多对多的关系
    • 查看表与表之间是否存在双方的外建均能被多个表调用,如果不是那就去除多对多关系
  3. 1 对 1 1 对多
    • 在 thinkphp 中问题不大
    • 如何去分析 1 对多或 1 对 1
    • 1 对 1 的关系中, 两个表直接同时并且单次被执行,就是说一个关联请求中,表 1 一次只可以调用一个表 2 的元素,并且表 2 也只是被调用了一次
    • 1 对多 的关系中, 表 1 通过一个外建,调用了多个表 2 的数据,并且表 2 的数据不能属于多个表 1,这样就是 1 对多的表现了

模型关联(我们确立了 er 关系再来做这么的一个关联)

  1. 模型关联查询

    • 在我们的 model 是作为一个 ORM 模式的模型结构
    • 在这之前我们就已经定义了模型了
    • 我们有两个模型 Banner 与 BannerItem
    • tp5 对我们提供了关联查询的方法 hasMany
    • 定义关联查询

      1
      2
      3
      4
      5
      6
      7
      // 在当前模型 Banner 新建类  类名自定义喜欢什么来什么
      // 函数体要写在 Banner 这个主模型中,BannerItem是被关联模型
      // 调用模型关联时要清晰的知道 外键 以及主建(某程度下是不用写后面两个,不建议)
      public function items () {
      // 关联查询方法hasMany 关联模型 外建 当前模型 banner id主建
      return $this->hasMany('BannerItem','banner_id','id');
      }
    • 调用关联查询

      1
      2
        //  在调用 模型的时候加上 with这么个方法 (括号内填写的就是刚才定义的函数名)
      $banner = BannerModel::with('items')->find($id);
  2. 模型嵌套关联查询

    • 在我们的 查询中 会存在被关联体中还关联着变得关联体,在 tp5 中就形成了嵌套查询
    • 当然 tp5 也给我们提供了方法:belongsTo
    • 嵌套关系 Banner -> BannerItem -> Image (这里就存在了多重的嵌套)
    • 模型 Banner BannerItem Image
    • 是 BannerItem 关联 Image 所以关联函数我们写在 BannerItem 中
    • 定义嵌套查询
    1
    2
    3
    4
      public function img() {
    // 处理方法名其他都是一样的,这里就不多说了
    return $this->belongsTo('Image','img_id','id');
    }
    • 调用查询 (这个比较关键,不过还是很简单的)
    1
    2
    3
    4
    5
    // with 可以是字符串也可以是数组(嵌套关联时就会用数组)
    // 为什么是items.img 而不是 直接img呢,因为是嵌套关系,在模型中可以嵌套这里也是可以的
    // 但是在 嵌套时 是items 关联的 img ,这里就会用.来链接
    // 这个解释比较绕但是,知道方法就是要这样去用的就好啦
    $banner = BannerModel::with(['items','items.img'])->find($id);

隐藏模型字段 (模型自带)

  1. hidden 方法隐藏字段
1
2
// 数据      方法      字段名
$banner->hidden(['字段名例:id'])
  1. visible 只显示的字段
1
$banner->visible(['字段名例:id','update_time])

模型内部隐藏字段 (自定义模型的内部隐藏,把一些前端不需要的字段隐藏了)

  • hidden 隐藏
  • 直接在 model 定义的模型内添加方法 (以 Banner 为例)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
namespace app\api\model;

use think\Model;

class Banner extends Model
{
// 直接添加 $hidden的数组填入要隐藏的字段即可
// visible 等方法用法一样,那个模型内部的字段要隐藏就在那个模型内部设置
protected $hidden = ['id'];

public function items () {
// 关联模型 外建 当前模型 banner id主建
return $this->hasMany('BannerItem','banner_id','id');
}
public static function getBannerByID($id) {
$banner = self::with(['items','items.img'])->find($id);

return $banner;
}
}

自定义配置

  • /application/extra (extra 自己新建的,凡是放在这里面的配置文件都会被自动加载)
  • 手动配置一个本地的 img 图片路径
  1. 在 extra 下 新建 setting.php
1
2
3
4
return [
// 名称 域名 路径(直接放在public下的images就是这样写就可以了)
'img_prefix' => 'http://zerg.cn/images'
];
  1. 使用自定义变量
    • 因为是在 extra 内部定义的所以会自动调用,那么我们用 config 就可以去掉用到了
      1
      2
            // 配置文件名.变量名
      config('setting.img_prefix');

静态文件存放

  • 静态的外部文件,例如图片啊文本啊等的文件,必须放在 public 这个公共目录下
  • 并不是放在 application 的这个开发目录下,因为 tp5 的架构里面只有 public 这个目录是对外开放的
  • 所以文件都必须是要放在 public 目录下

tp 模型读取器 (数据拼合)

  • 为了获取数据/修改数据,tp5 给出了一个读取器的方法
  • 用来给我们读取数据修改数据用的
  • 那个模型要修改数据就在哪个模型定义
  1. 定义读取器(其实也是一个函数方法)
    • 读取器命名规范 开头 get 必须有 + 读取数据的名称并且开头要大写例 Url + Attr 必须加的(利用驼峰命名法)
    • getUrlAttr (完整的编写,除了中间的那个数据,其他都是必须有的,中间数据名开头必须大写)
    • 传入一个值,名字自定义 (这个传入的数据其实就是我们要获取到要修改的数据)
    • 每一次传入一个数据,有多个输出就会重复的执行读取器
    • 因为在我们的业务逻辑中会调用到当前模型的其他数据,但是第一个参数只是获取到的是当前读取器的数据,并无法读取到其他的数据
    • 所以添加了第二个参数 (这个参数会给我们返回一个这个模型的数据,就是所有的数据)
1
2
3
public function getUrlAttr ($value,$data) {

}
  1. 使用读取器 (做数据的修改然后返回)
1
2
3
4
public function getUrlAttr ($value) {
// 这里我们只是做了一个自定义的 变量和url路径的拼接
return config('setting.img_prefix').$value;
}
  1. 业务逻辑添加
1
2
3
4
5
6
7
8
public function getUrlAttr ($value,$data) {
$finalUrl = $value;
// 判断是否要拼接
if ($data['from'] === 1) {
$finalUrl = config('setting.img_prefix') . $value;
}
return $finalUrl;
}

自定义基类 (面向对象,提取模型读取器)

  • 一开始这样做会觉得好像代码还多了啊,这么不就是做无用功吗,在业务不断增加的时候,后期修改就可以看出来好处了
  • 集中业务逻辑
  • 创建 BaseModel.php 作为模型基类
  • 把让所有的模型都继承这个基类
  1. 把读取器提取到 模型基类 (这样做是一个面向对象的思想)

    • 但是提取了模型基类后我们所有的子模型都会自动的去执行模型
    • 这样可能会造成一些数据的变更和错误,比如说,两个命名一样但是代表的数据不同是就会出现错误
    • 所以我们把它封装为一个自调用的方法
      1
      2
      3
      4
      5
      6
      7
      8
      9
      // BaseModel
      // 读取器
      protected function prefixImgUrl ($value,$data) {
      $finalUrl = $value;
      if ($data['from'] === 1) {
      $finalUrl = config('setting.img_prefix') . $value;
      }
      return $finalUrl;
      }
  2. 子模型调用基类方法

    • Image
      1
      2
      3
      public function getUrlAttr ($value,$data) {
      return $this->prefixImgUrl($value,$data);
      }

定义 api 版本号

  • 在互联网的项目中,我们会对项目版本对升级,以及业务逻辑改变和变更
  • 同时也是需要去兼容旧版本,所以会保留旧版本的 api
  1. 开发开闭原则
    • 代码对拓展开发,对修改封闭
    • 添加功能直接以拓展的方式添加就可以,不需要去改变代码
    • 修改是封闭的,业务变更上升版本
    • 不可以修改原来的版本代码,会破坏了原版本的代码,和影响功能调用的风险
    • 需要修改就要添加新的版本
  2. 多版本
    • 版本的分离,新旧版本不发生冲突
    • 新老版本的兼容问题
    • 给用户缓冲时间,也不能兼容太多的版本,成本太高
    • v1 做 v1 版本层
    • v2 做 v2 版本层

路由 api 动态变更

1
2
3
4
//              动态版本 实现传什么就调用什么版本的api,同时也是要修改版本指向接口
// 传 v1 就是 v1
// 传 v2 就是 v2 动态写入
Route::get('api/:version/banner/:id','api/:version.Banner/getBanner');

一对一关系选择关联方法

  1. belongsTo
    • 在有外建的表内请求就用 belongsTo
  2. hasOne
    • 在没有外建的表亲求就用 hasOne

多对多查询 (belongsToMany)

-

  • 多对多的查询呢 就比一对多和 1 对 1 的查询要多了一个参数
  • 在参数中第二个是放入第三个表也就是中间表
1
2
3
4
public function products () {
// 关联表名 中间表名 关联表id 主建
return $this->belongsToMany('Product','theme_product','product_id','theme_id');
}

开启路由完整匹配模式

  • 开我们开发的过程中难免会有 api 相同当是请求的方式以及传参的不同,但是又需要相同的 api 名称
  • 在我们的 tp5 中,会自动追寻一个半路径的匹配,所以当匹配到了相关的路由时就会停止匹配
  • 但是这样返回的结果肯定不是我们要的,所以就要开启这个完整的路由匹配模式
  • 在 config.php 配置文件中,我们就可以来更改了
1
2
3
//  只有找到这句话改变就可以了   false -> true
// 路由使用完整匹配
'route_complete_match' => true,

合理利用数据冗余

  • 在查询量上来的时候避免数据量大多表查询之间耗时
  • 合理的利用数据冗余来减少联合表的查询减少查询时间
  • 但不要太过多但使用,只是为了减少数据库压力
  • 在数据库中做相关的优化

collection 字符集

  • 我们使用获取到的数据是字符集更方便让我们来修改数据
  1. tp5 修改获取返回数据 (/application/database.php)
1
2
3
// 找到这个吧 arr改为 collection
// 数据集返回类型
'resultset_type' => 'collection',
  1. 使用字符集就可以轻松的临时隐藏字段

    • 当我们在开发的过程中,不是所有业务逻辑都需要隐藏的字段,我们就不可以在关联模型中直接就隐藏字段
    • 我们会使用临时隐藏字段
    • 当然数组我们是不可以直接这样来隐藏的,但是使用字符集的话就可以直接的去使用函数进行数据的隐藏
      1
      2
      // 使用hidden进行隐藏
      $products = $products->hidden(['summary']);
  2. 字符集判空

    • isEmpty 内置函数
1
2
3
4
// 判断空抛出异常
if ($products->isEmpty()) {
throw new ProductException();
}

##第九章

service (建立在 model 上的,用来处理复制的业务)

  • 在我们的 tp5 中,我们的 model 代表的一个很重要的位置
  • 可以写业务逻辑,也访问数据库
  • 但是 service 不可以用来访问数据库,因为上建立在 model 之上的
  • 我们都会把复杂的业务逻辑放在 service 层中

公共应用文件 common.php

  • 编写公共的 http 请求
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
/*
* @param string $url get 请求地址
* @param int $httpCode 返回状态码
* @return mixed
*/
function curl_get ($url,&$httpCode = 0) {
$ch = curl_init();
curl_setopt($ch,CURLOPT_URL,$url);
curl_setopt($ch,CURLOPT_RETURNTRANSFER,1);

// 不做证书校验,部署在linux环境下请改为true
curl_setopt($ch,CURLOPT_SSL_VERIFYPEER,false);
curl_setopt($ch,CURLOPT_CONNECTTIMEOUT,10);
$file_contents = curl_exec($ch);
$httpCode = curl_getinfo($ch,CURLINFO_HTTP_CODE);
curl_close($ch);
return $file_contents;
}

模型插入数据(create)

  • 在 tp5 中如何向数据库插入数据
  • tp5 模型给我们准备了 create 的方法
1
2
3
4
    //  模型名   create方法 数组传入要添加的字段和数据
$user = UserModel::create([
'openid' => $openid
]);

动态传入数值随机生成字符串方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
/*
* 生成随机字符串
*/
function getRandChar ($length) {
$str = null;
$strPol = "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789abcdefghijklmnopqrstuvwxyz";
$max = strlen($strPol) - 1;

for ($i=0;$i < $length; $i++) {
$str .= $strPol[rand(0,$max)];
}

return $str;
}

文件缓存 chache

  • 使用 cache 写入缓存
  • 使用文件存储的方式
  • 缓存的地址在目录文件/runtime/cache 文件内
1
$request = cache($key,$value,$expire_in);

路由分组

  • 由于我们 api 接口的不断增加
  • 在一个分类中会有很多的相同的接口路由
  • 这个时候如果我们业务的变更修改起来就会很麻烦
  • 所以我们是用来路由分组来实现
  • group 方法
  • 第一个是公共的路由部分,第二个是一个闭包(也就是一个 function 的方法)
  • 在里面还是安装路由一样去定义就可以了
  • 也能提高路由的效率
1
2
3
4
5
6
7
8
9
10

//Route::get('api/:version/product/recent','api/:version.Product/getRecent');
//Route::get('api/:version/product/by_category','api/:version.Product/getAllInCategory');
//Route::get('api/:version/product/:id','api/:version.Product/getOne',[],['id'=>'\d+']);

Route::group('api/:version/product', function () {
Route::get('/recent','api/:version.Product/getRecent');
Route::get('/by_category','api/:version.Product/getAllInCategory');
Route::get('/:id','api/:version.Product/getOne',[],['id'=>'\d+']);
});

关联模型下个关联数据排序(tp5 没有的,重点)

  • 使用 模型+query 添加排序
1
2
3
4
5
6
7
8
9
10
// 关联模型 imgs  properties 查询
// 模型的嵌套 imgurl
public static function getProductDetail ($id) {
// 在 with 中 嵌套function
// 在内部添加 query
$product = self::with(['imgs' => function ($query) {
$query->with(['imgUrl'])->order('order','asc');
}])->with(['properties'])->find($id);
return $product;
}

使用 关联模型 添加/更新数据

  • 添加数据的方法有很多,我们来使用一下关联模型的方法
  • 两个的区别在于 修改操作的 关联 不可以用括号
1
2
3
4
// 调用 user 中的 address 关联 使用 save方法添加数据
$user->address()->save($dataArray);
// 调用 user 中的 address 关联 使用 save方法修改数据
$user->address->save($dataArray);

第十章

前置操作

  • 在我们编写 api 业务逻辑的时候,我们会想在调用 api 接口之前,需要满足某些条件
  • 这样才可以去访问我们的接口中的业务逻辑
  • 所以我们要在做一个前置操作,抵挡不满足条件的抛出异常
  1. tp5 中使用前置操作需要基础自带的一个基类 Controller
  2. 定义一个名为 \$beforeActionList 的数组
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
use think\Controller

class Address extends Controller
{
// 定义前置属性
// 第一个字段是 访问api接口前 需要 访问的一个前置方法
// 箭指的 是一个数组
// 数组内部定义一个箭指数据,也可以直接是一个字符串(内部填入api接口函数就可以了)
// 否则向下面这样写
// 多api编写
protected $beforeActionList = [
'first' => ['only' => 'second,third']
];

// 触发api前 执行的前置函数
protected function first () {
echo 'first';
}

// api接口
public function second () {
echo 'second';
}

// api接口
public function third () {
echo 'third';
}
}

重构前置验证操作 (实现面向对象)

  • 提取验证业务逻辑到 service 的基类中
  • 提取前置方法到 BaseController 的基类中
  • 继承基类,执行前置方法
  1. 提取出一个前置的基类 BaseController (继承内置 Controller)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
use app\api\service\Token as TokenService;

// 继承
class BaseController extends Controller
{
// 前置方法
// 验证初级权限作用域,用户和cms都可以访问
protected function checkPrimaryScope () {
// 向Token调用验证方法
TokenService::needPrimaryScope();
}

// 验证权限,只有用户可以访问,cms无法访问
protected function checkExclusiveScope () {
TokenService::needExclusiveScope();
}
}
  1. 提取验证业务逻辑(因为是 token 相关的就归并到 token 的 service 业务层中)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// 重构前置方法,验证权限
// 用户和cms管理员都可以访问的权限
public static function needPrimaryScope () {
// 调用token中的方法获取scope
$scope = self::getCurrentTokenVar('scope');
// 判断是否存在
if ($scope) {
// 判断 scope的权限大小
if ($scope >= ScopeEnum::User) {
return true;
} else {
throw new ForbiddenException();
}
} else {
throw new TokenException();
}
}
  1. 继承 BaseController 基类使用前置方法
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
         // 继承基类
class Address extends BaseController
{
// 调用前置的方法
protected $beforeActionList = [
// 前置验证的方法名 需要前置验证的函数
'checkPrimaryScope' => ['only' => 'createOrUpdateAddress']
];

/*
* @url api/v1/address
*/
public function createOrUpdateAddress () {

}
}

验证器数据自定义子项验证

  • 自定义子项验证,通过自定义的方法调用实现
  • 当我们在验证时,传入的是一个二维数组,就可以使用来验证子项
  • 我们就自定义一个验证的方法,通过基类的验证的调用
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

// 整体验证
protected $rule = [
'products' => 'checkProducts'
];

// 数据子项的验证
protected $singleRule = [
'product_id' => 'require|isPositiveInteger',
'count' => 'require|isPositiveInteger'
];

/*
* 自定义整体验证
*/
protected function checkProducts ($values) {
// 验证是不是数组
if (!is_array($values)) {
throw new ParameterException([
'msg' => '商品参数不正确'
]);
}

// 验证不为空
if (empty($values)) {
throw new ParameterException([
'msg' => '商品列表不能为空'
]);
}

// 循环对每一项进行验证
foreach ($values as $value) {
$this->checkProduct($value);
}

return true;
}

// 基础调用子项验证
protected function checkProduct ($value) {
$validate = new BaseValidate($this->singleRule);
$result = $validate->check($value);
if (!$result) {
throw new ParameterException([
'msg' => '商品参数不正确'
]);
}
}

自动添加时间戳(TP5 内置添加时间戳)

  • 在我们的操作中,我们的数据中会带有数据,tp5 为我们提供了自动添加时间戳
  1. 找到自己要添加的时间戳的模型 我是在 order 添加那我就去 orde 人的模型中
  2. \$autoWriteTimestamp 添加为 true,需要是模型的方式才可以使用的
  3. 创建 修改 删除
  4. 默认为 create_time update_time delete_time
  5. 修改方法名 在模型下修改
1
2
3
4
5
// 自动写入时间戳
protected $autoWriteTimestamp = true;
// 修改字段名
// 内置名称 自定义修改的名称
protected $createTime = 'create_timestamp';

Tp5 事务应用

  • 在我们的应用中可能会出现分步的操作,可能会本地与服务端出现不一致
  • 所以我们使用事务来做处理
  • 在中间出现错误就会把数据回滚保持数据的一致性
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
// 开头加入开始
Db::startTrans();
try {
$orderNo = $this->makeOrderNo();
$order = new \app\api\model\Order();
$order->user_id = $this->uid;
$order->order_no = $orderNo;
$order->total_price = $snap['orderPrice'];
$order->total_count = $snap['totalCount'];
$order->snap_img = $snap['snapImg'];
$order->snap_name = $snap['snapName'];
$order->snap_address = $snap['snapAddress'];
$order->snap_items = json_encode($snap['pStatus']);

$order->save();

$orderID = $order->id;
$create_time = $order->create_time;

foreach ($this->oProducts as &$p) {
$p['order_id'] = $orderID;
}

$orderProduct = new OrderProduct();
$orderProduct->saveAll($this->oProducts);

// 结尾加上结束
Db::commit();

return [
'order_no' => $orderNo,
'order_id' => $orderID,
'create_time' => $create_time
];
} catch (Exception $ex) {
// 异常出现回滚
Db::rollback();
throw $ex;
}

引入没有命名空间的文件与调用(Loader),手动引入微信支付 php

  • 使用 loader 的 import 方法
  • extend/WxPay/WePay.Api.php
1
2
3
4
5
    //         文件开头的第一个  文件路径       // 类的名称
Loader::import('WxPay.WxPay',EXTEND_PATH,'.Api.php');
// 调用
// 调用的时候前面要加反斜杠
$wxOrderData = new \WxPayUnifiedOrder();

TP5 模型实现数据减少 setDec

1
2
                                            // 前面是查询  第一个数是写要改变的字段  第二个是要减少的数量
Product::where('id','=',$singlePStatus['id'])->setDec('stock',$singlePStatus['count']);

数据库锁与事务锁的区别

  • 数据库模型->lock(true)
  • 事务锁 Db
  1. 事务锁是等待整个事务提交才会执行第二次事务,但是数据库模型锁只是单步的锁着了数据库查询语句
  2. 在后面的操作还没有执行时,数据库模型锁已经放开了

外部网址使用

模型分页查询(paginate)

  • 第一个参数是分类数
  • 第二个数是否简洁模式
  • 第三个是数组填入分页数
1
2
3
4
5

public static function getSummaryByUser ($uid,$page=1,$size=15) {
$paginData = self::where('user_id','=',$uid)->order('create_time desc')->paginate($size,true,['page' => $page]);
return $paginData;
}

后记

  • 这是学习微信小程序开发后端PHP时候的笔记,欢迎更多的同行大哥指导交流
  • 欢迎进入我的博客https://yhf7.github.io/
  • 如果有什么侵权的话,请及时添加小编微信以及qq也可以来告诉小编(905477376微信qq通用),谢谢!
-------------本文结束感谢您的阅读-------------
0%