# demo-shop **Repository Path**: javaeur/demo-shop ## Basic Information - **Project Name**: demo-shop - **Description**: 基于thinkphp6.1开发的商城,,有详细的开发教程,可供学习 - **Primary Language**: PHP - **License**: Apache-2.0 - **Default Branch**: master - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 0 - **Forks**: 10 - **Created**: 2023-09-19 - **Last Updated**: 2024-06-15 ## Categories & Tags **Categories**: Uncategorized **Tags**: None ## README # 0.简介 大家好!我是一名九零后程序员,搬砖了好几年,前些年用`PHP`开发,后面因为开发需要就转`java`了。为了不让自己幸苦自学的`php`丢掉,趁一些空余时间,写一些教程,分享一下自己的工作经验。 这个教程打算以一个商城的项目为例,我会教大家一步一步完成功能的开发。这个教程会很讲得很细致,主要涉及到架构的设计、开发过程中的规范、常用的技术知识以及最重要的就是如何避坑。可以说是把自己的干货无私的奉献给大家,如果对你有用,请点个赞~ > 如果教程中有些代码写得不好,欢迎指正,大家一起学习学习~ > > 教程会持续更新~ # 1.架构搭建 ## 1.1环境搭建 | | 环境 | 版本 | | ------ | ----- | ----- | | 语言 | PHP | 7.3.4 | | 数据库 | mysql | 5.7 | | 缓存 | redis | 3.0.5 | 这里推荐使用`phpstudy`集成环境 ## 1.2框架选型 到目前为止,国内使用最多的`PHP`框架当属`Thinkphp`和`Laravel`,这两款框架很相似,文档也是比较齐全。作为一个`PHP`程序员至少要掌握其中一个,当然最好都掌握。 本教程的项目使用了`Thinkphp6.1`的版本,是当前最新版本。 官方文档链接:https://www.kancloud.cn/manual/thinkphp6_0/1037479 **安装** ```shell composer create-project topthink/think demo-shop ``` 默认是单应用模式的,这里我们开发的项目是多应用,因此还需执行以下命令 ```shell composer require topthink/think-multi-app ``` > 本项目有后台模块、提供给web端的API模块,因此就需要用到多应用模型、这样有利于我们后期项目的管理和维护 **目录结构** 把框架下载下来后,我们认识一下里面的一些目录,下面是官网给出的目录结构,实际上下载下来的目录比较简洁 ``` www WEB部署目录(或者子目录) ├─app 应用目录 │ ├─app_name 应用目录 │ │ ├─common.php 函数文件 │ │ ├─controller 控制器目录 │ │ ├─model 模型目录 │ │ ├─view 视图目录 │ │ ├─config 配置目录 │ │ ├─route 路由目录 │ │ └─ ... 更多类库目录 │ │ │ ├─common.php 公共函数文件 │ └─event.php 事件定义文件 │ ├─config 全局配置目录 │ ├─app.php 应用配置 │ ├─cache.php 缓存配置 │ ├─console.php 控制台配置 │ ├─cookie.php Cookie配置 │ ├─database.php 数据库配置 │ ├─filesystem.php 文件磁盘配置 │ ├─lang.php 多语言配置 │ ├─log.php 日志配置 │ ├─middleware.php 中间件配置 │ ├─route.php URL和路由配置 │ ├─session.php Session配置 │ ├─trace.php Trace配置 │ └─view.php 视图配置 │ ├─public WEB目录(对外访问目录) │ ├─index.php 入口文件 │ ├─router.php 快速测试文件 │ └─.htaccess 用于apache的重写 │ ├─extend 扩展类库目录 ├─runtime 应用的运行时目录(可写,可定制) ├─vendor Composer类库目录 ├─.example.env 环境变量示例文件 ├─composer.json composer 定义文件 ├─LICENSE.txt 授权说明文件 ├─README.md README 文件 ├─think 命令行入口文件 ``` ![](demo-shop-img/1.1.jpg) 稍微留意一下框起来的部分,后面会用到 **URL重写** 重写url主要是为了隐藏入口`index.php`,我们可以发现很多网站的地址并没有带上入口文件,比如说tp的官网 https://www.kancloud.cn/manual/thinkphp6_0/1037479 而不是 https://www.kancloud.cn/index.php/manual/thinkphp6_0/1037479 官网文档里面也提到了,它也给出了重写的规则,但是最后会发现有点问题,因此做了一下调整 ``` Options +FollowSymlinks -Multiviews RewriteEngine On RewriteCond %{REQUEST_FILENAME} !-d RewriteCond %{REQUEST_FILENAME} !-f #####RewriteRule ^(.*)$ index.php/$1 [QSA,PT,L] 这是官网的 RewriteRule ^(.*)$ index.php [L,E=PATH_INFO:$1] ``` 注意上面是基于Apache的规则,我们在根目录(入口文件所在的目录),新建一个` .htaccess `文件,把规则写进里面去即可 # 2.后台管理员模块 我们将后台管理员及权限管理作为这个教程的开端,这一部分的内容非常重要,首先是功能上我们实现了后台的权限控制,然后就是我们在实现功能的同时也对整个框架进行了一些常用逻辑的封装,其中涉及到了以下几个知识: - 接口的代码结构 - 基础控制器 - 返回值封装 - 表单验证器 - 异常接管 - 路由 - 中间件 - redis缓存 - Jwt(token) - 模型层封装 - 登录用户信息的封装 - 分页查询的封装 ## 2.1接口的代码结构 正常来说开发一个接口,并不会把所有的代码都写在控制器,这样的话代码会非常臃肿,并且复用性差,因此如何设计代码结构,成为你程序简洁、高效的关键。 本教程的接口的代码结构主要分为三层:控制层、业务层、模型层 **控制层**:接口名的定义,主要负责参数的过滤及结果的返回 **业务层**:实现功能的具体核心代码 **模型层**:负责与数据库交互 每个应用都拥有独立的控制层,这个很好理解,因为你总不可能两个不同的应用使用同一个接口。 每个应用也可以拥有独立的业务层,因为不同应用可能业务代码不一样,但不管是哪个应用都离不开对数据库的增删改查,因此我们这里封装一个核心的业务层,只提供增删改查的逻辑,所有应用都可以直接调用它,做到最大程度的复用性 模型层我们可以把它比作数据表,每个应用都可以使用同一个模型 或许你还不理解,不过没关系,有个印象就行,后面具体开发按照这个结构来,你就会明白了 ## 2.2基础控制器 在`app`目录下我们会发现框架生成的`BaseController.php`基础控制器,我们可以把它当作最底层的控制器。前面我们说过,我们这个项目是一个多应用项目,因此这里为每个应用都建一个基础控制器,让其继承`BaseController.php`,这样每个应用即可使用底层控制器提供的方法,又可以定制开发一些当前应用共用的方法。 在`app`下新建一个`admin`目录,作为后台应用。接着新建`controller-->AdminController.php`,内容如下 ```php 200, 'msg'=>'操作成功','data'=>$data]); json(['code'=>500, 'msg'=>'服务器异常~']); ``` 不知道你们有没有发现,假如说查询列表数据,每次返回成功,你都要写`'code'=>200, 'msg'=>'操作成功'`,这样很繁琐,因此这里我们封装一下。 打开`app`目录下的全局函数文件`common.php`,这里面新增的函数可以全局调用 ```php /** * 返回值-成功 * @param string $code 错误码 * @param string $msg 提示信息 */ function success($data = []){ return json(['code'=>200, 'msg'=>'操作成功','data'=>$data]); } /** * 返回值-失败 * @param string $code 错误码 * @param string $msg 提示信息 */ function failure($code=201, $msg='操作失败'){ return json(['code'=>$code, 'msg'=> $msg]); } /** * 返回值-异常 * @param string $code 错误码 * @param string $msg 提示信息 */ function error(){ return json(['code'=>500, 'msg'=>'服务器异常~']); } ``` 这三个方法一般在控制层使用,输出最后结果给前端 ```php //返回列表数据 return success($data); //新增一条数据成功 return success(); //新增一条数据失败 return failure(); //新增一条数据,名称不能为空 return failure(2002,'名称不能为空'); //异常只返回500 return error(); ``` ## 2.4表单验证器 新增或修改数据时,前端会传一些数据给后端接口,比如新增一条管理员数据,那此时就必须验证账号和密码这两个字段,这两个字段不能为空,同时密码还需要满足一定的条件,此时框架提供的验证器就派上用场了。 打开底层控制器`BaseController.php`,我们会发现框架已经给我们创建了一个验证码方法,非常的好用 ```php /** * 验证数据 * @access protected * @param array $data 数据 * @param string|array $validate 验证器名或者验证规则数组 * @param array $message 提示信息 * @param bool $batch 是否批量验证 * @return array|string|true * @throws ValidateException */ protected function validate(array $data, $validate, array $message = [], bool $batch = false){ //内容省略 } ``` 接下来看看调用示例 ```php $data = $this->request->post(); //验证规则 $validate = [ 'account' => 'require', 'password' => 'require|regex:/^[a-zA-Z0-9_]{8,20}$/', ]; //提示信息 $message = [ 'account.require' => '账号不能为空!', 'password.require' => '密码不能为空!', 'password.regex' => '密码长度8~20位,包含字母数字下划线!', ]; $this->validate($data, $validate, $message); ``` 如果输入的账号为空,运行这段代码后你会发现抛出以下异常 ![](demo-shop-img/2.6.jpg) ## 2.5异常接管 上一节讲验证码的时候,抛出了一个账号不能为空的异常,很显然这种提示方式对于前端很不友好,前端无法获取到相应的信息,因此我们要想办法捕抓到这个异常,将提示信息`json`的格式返回给前端 打开`app/ExceptionHandle.php`,里面有一个这样的函数`render($request, Throwable $e)`,我们在里面新加参数验证码错误的代码 ```php public function render($request, Throwable $e): Response { // 参数验证错误 if ($e instanceof ValidateException) { return failure(1004,$e->getError()); } // 其他错误交给系统处理 return parent::render($request, $e); } ``` 这时如果参数验证码错误的话,就会被捕抓到,同时返回`json`格式的数据给前端,如下 ```json { "code": 1004, "msg": "账号不能为空!" } ``` 同样的,这里还可以捕获其它异常,例如路由异常 比如说路由设置了get方式提交,如果你的提交方式是post,这时新增如下代码即可 ```php // 请求异常 if ($e instanceof RouteNotFoundException) { return failure(404,$e->getMessage()); } ``` > 捕抓异常,输出给前端,并不会记录日志,如果交由系统处理,则会记录相应的日志信息,日志一般存储在runtime目录里面 ## 2.6自定义抛出异常 前面我们提到,控制层最后会返回结果给前端,但很多时候可能执行不到这一步,因为在业务层的时候有可能因为不满足某些条件,而需要被迫中断程序,直接返回结果给前端,例如下面的登录例子 控制层 ```php //登录 public function login(){ $data = $this->request->post(); /*省略了参数验证*/ return success(SysUserService::login($data)); } ``` 业务层 ```php public static function login($data){ $user = SysUserModel::getByAccount($data['account']); if($user['status'] == 0){ //账号被禁用,这里要中断程序,直接抛出一个提示信息给前端 } } ``` 这里我们无法使用`return failure()`,因此我们需要使用另外一种方式 还是打开全局函数文件`common.php`,新增一个方法 ```php /** * 抛出异常 * @param string $code 错误码 * @param string $msg 提示信息 */ function serviceException($code=500, $msg ='服务器异常~'){ // 使用think自带异常类抛出异常 throw new \think\Exception($msg, $code); } ``` 这个函数就是主动抛出异常,同时我们需要捕抓到这个异常。上一节我们讲了异常接管,因此我们需要`ExceptionHandle.php`文件中新增如下代码 ```php public function render($request, Throwable $e): Response { // 参数验证错误 if ($e instanceof ValidateException) { return failure(1004,$e->getError()); } // 请求异常 if ($e instanceof RouteNotFoundException) { return failure(404,$e->getMessage()); } // 捕获主动抛出的异常-----新加的 if ($e instanceof Exception) { return failure($e->getCode(),$e->getMessage()); } // 其他错误交给系统处理 return parent::render($request, $e); } ``` ## 2.7开启Redis缓存 框架默认的缓存是基于本地文件,这里我们只需配置一下,把它改成基于`redis` 打开全局的`config/cache.php`,新增以下内容 ```php 'redis' => [ // 驱动方式 'type' => 'redis', //服务地址 'host' => env('cache.host'), //端口 'port' => env('cache.port'), //密码 'password' => env('cache.password'), //节点 'select' => env('cache.select'), // 缓存前缀 'prefix' => env('cache.prefix'), // 缓存有效期 0表示永久缓存 'expire' => env('cache.expire') ], ``` 打开`.env`,新增以下内容 ``` [CACHE] DRIVER = redis HOST = 127.0.0.1 PORT = 6379 PASSWORD = SELECT = 0 PREFIX = EXPIRE = 0 ``` 一般本地的`redis`配置信息都默认是这样,如果发现你的连接不上,就看看密码是否正确 框架给我们提供了助手函数` cache()`,看看它的使用方式 ```php //设置缓存,且缓存时间是永久,因为配置EXPIRE = 0,标识永久 cache("user_id",1); //缓存10秒 cache("user_id",1,10); //获取缓存 cache("user_id"); ``` ## 2.8路由配置 为什么要使用路由?个人认为主要是有下面两个好处: 1. 访问地址写到一个地方,方便管理 2. 使用路由中间件,可以提前过滤接口 打开`config/route.php`,把以下的配置项设置为true,开启强制路由 ```php // 是否强制使用路由 'url_route_must' => true ``` 另外我们在`admin`应用下新建`route/app.php`,每个应用可设置单独的路由,例如 ```php 建表语句和字段说明查看最后的附录 最后我们在项目中新增数据库的配置,在最外层目录新建一个`.env`,参考其给出的示例`.example.env`,具体内容如下: ``` #数据库配置配置 [DATABASE] TYPE = mysql HOSTNAME = 127.0.0.1 DATABASE = demo-shop USERNAME = root PASSWORD = 123456 HOSTPORT = 3306 CHARSET = utf8 DEBUG = true ``` ## 2.11前后端分离验证码 `thinkphp`提供的验证码插件是基于`session`,对于前后端分离的项目,并不能直接使用,但是我们可以间接使用它,而不用重新写一个插件或者是网上找一个别人写的,毕竟别人写的东西不一定没毛病。 首先我们安装一下官网提供的验证码插件 ```shell composer require topthink/think-captcha ``` **生成验证码接口** 在`admin/controller`新建`SysUser.php`,内容如下 ```php getData()); $key = session('captcha.key'); cache(config('redisKey.ADMIN_LOGIN_VERIFY_').$uniqid,$key); return ['uniqid'=>$uniqid,'image'=>$base64_image]; } } ``` 正常情况下,你只需这样`return Captcha::create()`,就能在网页上看到验证码,但前面说了,这是前后端分离的项目,因此返回给前端需要两个东西:用于设置缓存的唯一标识和base64加密的图片。 接下来解释一下代码,为什么是这么写的 我看了一下源码,打开`Captcha.php`发现`Captcha::create()`返回的是一个`Response`对象 ```php public function create(string $config = null, bool $api = false): Response ``` `Response`里面有个`getData()`就是获取内容的方法,因此`base64_encode($rs->getData())`就是获取验证码图片 另外我们从源码中的`generate()`,这个是生成验证码的方法,其中有这样的代码 ```php //$key这里的$key是验证码字符 $hash = password_hash($key, PASSWORD_BCRYPT, ['cost' => 10]); $this->session->set('captcha', [ 'key' => $hash, ]); ``` 把验证码字符进行加密,存储到session,因此这里使用`session('captcha.key')`获取到这个key值,然后我们把它存储到缓存中,而这个key是使用`uniqid()`方法生成的唯一标识 **验证输入的验证码是否正确** 还是先看看验证的代码 ```php $data = $this->request->post(); $key = cache(ADMIN_LOGIN_VERIFY_.$data['uniqid']); if($key && password_verify(mb_strtolower($data['code'], 'UTF-8'), $key)){ cache(ADMIN_LOGIN_VERIFY_.$uniqid,null); }else{ return failure(config('error.er17')['code'],config('error.er17')['msg']); } ``` 首先前端需要提交`uniqid`和`code`这两个字段过来,第一步判断`cache(ADMIN_LOGIN_VERIFY_.$data['uniqid'])`是否存在,如果存在,就判断输入进来的验证码是否正确,这里用到`password_verify()`,很多人可能有疑问,为什么这么写? 答案还是在源码里面,还是打开`Captcha.php`,其中有个验证验证码是否正确的方法 ```php /** * 验证验证码是否正确 * @access public * @param string $code 用户验证码 * @return bool 用户验证码是否正确 */ public function check(string $code): bool { if (!$this->session->has('captcha')) { return false; } //就是这三行代码 $key = $this->session->get('captcha.key'); $code = mb_strtolower($code, 'UTF-8'); $res = password_verify($code, $key); if ($res) { $this->session->delete('captcha'); } return $res; } ``` ```php //源码 $key = $this->session->get('captcha.key'); //我们的代码 $key = cache(ADMIN_LOGIN_VERIFY_.$data['uniqid']); //源码 $code = mb_strtolower($code, 'UTF-8'); $res = password_verify($code, $key); //我们的代码 password_verify(mb_strtolower($data['code'], 'UTF-8'), $key) ``` 其实就是照搬过来,一个前后端分离的验证码就这么简单的实现了~ ## 2.12Jwt(token) 前后端分离项目,一般来说服务端的代码和前端代码都是放在不同服务器上,有不同的域名,因此前端无法携带与后端服务器对应的cookie,这也就是无法使用session的原因,因此一般情况下都使用`jwt`来保存登录的验证状态 接下来我们说说如何生成`jwt` 文档地址:https://github.com/firebase/php-jwt **安装** ```shell composer require firebase/php-jwt ``` **示例** ```php //用户信息 $user = ['id'=>1,'account'=>'admin']; //一天后过期 $user['exp'] = time() + 24*3600; //生成token,config('app.jwt_code_pc')加密字符串 $jwt = JWT::encode($user,config('app.jwt_code_pc'),"HS256"); ``` 最后生成一窜以下的字符串 ``` eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpZCI6Miwicm9vdCI6MSwiYWNjb3VudCI6ImFkbWluIiwicGFzc3dvcmQiOiIqKioxMWIyYWFiMzcyMWU5NDAyZTJjMWQ1YmNkYmI0ZDI1ZiIsInJlbWFyayI6IiIsInJvbGVzIjpudWxsLCJsb2dpbl90aW1lIjoiMjAyMy0wNi0wMiAyMjozOToxOSIsImxvZ2luX2lwIjoiMTI3LjAuMC4xIiwic3RhdHVzIjoxLCJjcmVhdG9yIjowLCJ1cGRhdG9yIjowLCJjcmVhdGVfdGltZSI6IjIwMjMtMDUtMjUgMTU6MTg6MzYiLCJ1cGRhdGVfdGltZSI6IjIwMjMtMDYtMDIgMjI6Mzk6MTkiLCJkZWxldGVfdGltZSI6bnVsbCwiZXhwIjoxNjg1ODAzMTU5fQ.Nmay9ZTW3LTvRMJLRC1cz0wtvQS014f68epLPrH7IAo ``` 下次用户访问后台接口的时候,把`jwt`通过header头传给后端接口,后端接口会对其进行解析,如果能够被解析,则说明`jwt有效` ```php //解析jwt JWT::decode(jwt,new Key(config('app.jwt_code_pc'),'HS256')); ``` 最后得到用户信息 ```php $user = ['id'=>1,'account'=>'admin']; ``` **注:** > 无论是生成`jwt`还是解析`jwt`,其中有个非常重要的参数:加密的密钥,这个密钥一旦泄露,那么别人就看可以轻易的解析和伪造`jwt`进行登录 ## 2.13模型层的封装 `thinkphp`的模型,我的理解就是跟数据库打交道,我看了文档确实是比较方便,因此整个项目都会用模型。一个模型对应一张表,无论是pc端、app端、后台的接口都需要操作模型,因此把模型全部放在公共应用commom。 新建`commom/model/BaseModel.php`,内容如下 ```php login_time = date('Y-m-d H:i:s',time()); $user->login_ip = getClientIp(); $user->save(); //生成token $token = $user->toArray(); //一天后过期 $token['exp'] = time() + 24*3600; //注意这里的第一个参数,在config/app.php里面配置jwt_code_admin $jwt = JWT::encode($token,config('app.jwt_code_admin'),"HS256"); return [ 'token' => $jwt, ]; } ``` 我们看看这段代码的逻辑,根据账号去查找数据表中查找是否存在该账号,如果存在则验证密码是否正确,满足以上两个条件后,还要判断该账号是否处于`启用`状态。以上条件都满足后,更新登录时间和ip,然后生成`jwt`返回给前端,作为登录的凭证。 接下来了解一下`comparePassword()`这个密码比较函数,打开`app/common.php`,这里新加了两个函数 ```php /** * 密码加密 * @param string $pw 要加密的原始密码 * @param string $authCode 加密字符串 * @return string */ function createPassword($pw, $authCode = '') { if (empty($authCode)) { $authCode = config('app.authcode'); } $result = "***" . md5(md5($authCode . $pw)); return $result; } /** * 密码比较方法,所有涉及密码比较的地方都用这个方法 * @param string $password 要比较的密码 * @param string $passwordInDb 数据库保存的已经加密过的密码 * @return boolean 密码相同,返回true */ function comparePassword($password, $passwordInDb) { return createPassword($password) == $passwordInDb; } ``` 看注释应该看得懂了 看完主要的业务核心代码后,我们看看登录的控制器写的是什么代码 打开`SysUser.php` ```php //登录 public function login(){ $data = $this->request->post(); //验证规则 $validate = [ 'account' => 'require', 'password' => 'require', 'code' => 'require', 'uniqid' => 'require', ]; //提示信息 $message = [ 'account.require' => '账号不能为空!', 'password.require' => '密码不能为空!', 'code.require' => '验证码不能为空!', 'uniqid.require' => 'uniqid不能为空!', ]; $this->validate($data, $validate, $message); $key = cache(ADMIN_LOGIN_VERIFY_.$data['uniqid']); if($key && password_verify(mb_strtolower($data['code'], 'UTF-8'), $key)){ cache(ADMIN_LOGIN_VERIFY_.$data['uniqid'],null); }else{ return failure(config('error.er17')['code'],config('error.er17')['msg']); } return success(SysUserService::login($data)); } ``` 这里主要是对参数进行验证,这里也包含了输入验证码的验证,验证通过后,再调用业务核心代码。如果你看不懂这个验证码的验证,你可以阅读`2.9前后端分离验证码`这一章节,里面讲得很明白。 最后配置一下路由,打开`app.php` ```php //获取验证码 Route::get('login/verify','sysUser/verify'); //管理员登录 Route::post('login','sysUser/login'); ``` **疑问:这里不用判断请求是post请求吗?我看有些是这么写的`if(!$this->request->isPost()){//抛出异常}`?** > 如果说每个方法都这么写,非常的繁琐,因此为了解决这个问题,我们开启了强制路由,路由配置那章有提到。同时我们也对路由异常进行捕抓,异常接管那章有提到。一旦我们请求方式不对,或者写错了,都会抛出异常 ## 2.15用户验证中间件 用户想访问后台,那一定是要登录的,后端的接口是如何知道用户是否登录了呢? 上节讲到用户登录成功后,会返回一串token给前端,这个token就是一个登录凭证。当我们调用后台的接口时一定要带上这个凭证,否则你是无法访问。 一般情况下我们都是通过header头来携带token信息,拿到这窜token后,我们在那里解析它呢?? 接下来路由中间件要出场了 在`admin`应用下新建`middleware/Auth.php`,内容如下 ```php header('token'); $device = $request->header('device'); if($device == 'admin' && $token){ JWT::decode($token,new Key(config('app.jwt_code_admin'),'HS256')); }else{ return failure(config('error.er14')['code'],config('error.er14')['msg']); } }catch (\Exception $e) { return failure(config('error.er14')['code'],config('error.er14')['msg']); } return $next($request); } } ``` 这里面有两个地方要注意的,前端通过header头,同时把token和device传递过来,这里之所以要把$device传过来主要时标识这窜token是哪个应用的token。同时解析token的时候,一定要带上`app.php`文件中配置的加密窜 ```php //jwt加密字符串 'jwt_code_admin' => 'admin-demo-shop' ``` 不同的应用配置不一样的加密串进行区分。 只有解析成功了才会往下走,否则会提示如下信息 ```json { "code": "2000", "msg": "token非法!" } ``` 接下来把中间件注册到路由中,打开`admin/route/app.php`,在后面添加如下 ```php //路由分组,sysuser组里面的请求都要登录后才能访问 Route::group('sysuser', function(){ })->middleware(app\admin\middleware\Auth::class); ``` 之后凡是要登录验证的接口都要注册在`sysuser`这个组里面 ## 2.16用户信息封装 前面讲`jwt`的时候提到过,把用户信息加密生成一串`token`,用户登录后台成功后,访问后台接口需要带上这一串`token`,中间件会解析这个`token`,如果能被解析则说明这窜`token`有效,并且解析出来的内容就是当初加密的用户信息。 接下来的问题就是,怎么保存这个用户信息,使其在任何地方都可以被调用? 这里我们打开上一节实现的中间件`Auth.php`,需要改动一下 ```php $userInfo = JWT::decode($token,new Key(config('app.jwt_code_admin'),'HS256')); $request->user = ['id'=>$userInfo->id,'account'=>$userInfo->account]; ``` 这里使用$request这个全局对象,设置`sysuser`变量保存登录信息,这里只保存了id和account,一般情况下id是用到最多的,比如说创建者、更新者保存的都是id,再比如我们修改个人信息的时候,也需要根据id去更新。 为了方便我们获取到id或者是用户信息,最好在写两个全局函数,打开`common.php` ```php /** * 获取登录用户ID */ function getUserId(){ return isset(request()->user['id']) ? request()->user['id'] : 0; } /** * 获取登录用户 */ function getUserInfo(){ return isset(request()->user) ? request()->user : []; } ``` 这样的话我们就可以在任何地方很方便的使用它 ## 2.17添加/编辑管理员 前面2.1讲的时候有提到过,对于增删改查,我们需要封装一个公共的业务层,接下来看看如何实现 在`commom`目录新建`service`目录,在新建`SysUserService.php`,注意命名方式不要跟`admin/service/AsysUserService.php`一样 ```php /** * 新增数据 * @param array $data 新增的数据 * @return int */ public static function save($data){ if(SysUserModel::getByAccount($data['account'])){ //如果存在,就抛出提示 serviceException(config('error.er8')['code'],config('error.er8')['msg']); } $sysUser = new SysUserModel; //加密 $data['password'] = createPassword($data['password']); $sysUser->save($data); return $sysUser->id; } /** * 更新数据 * @param array $data 更新的数据 * @return int */ public static function update($data){ $sysUser = SysUserModel::find($data['id']); if(!$sysUser){ serviceException(config('error.er15')['code'],config('error.er15')['msg']); } //如果提交过来的账号跟数据库记录的账号不一样,说明当前提交过来的账号是修改过后的账号,因此这里需要验证码修改过的账号是否在数据库里面已经存在 if(isset($data['account']) && $data['account'] !== $sysUser->account){ if(SysUserModel::getByAccount($data['account'])){ //如果存在 serviceException(config('error.er8')['code'],config('error.er8')['msg']); } } //加密 if(isset($data['password'])){ $data['password'] = createPassword($data['password']); } $sysUser->save($data); return $sysUser->id; } ``` 新增的代码比较简单,就不讲了。我们来看看更新的逻辑 这里面有个账号的判断,如果你有修改账号,则要判断你修改的账号在数据库中是否存在,如果你没有修改则不进行验证码。 密码的判断是这样的,如果你没有提交修改密码的信息,则视为此次更新不涉及到密码。 **疑问:我们可以看到无论是新增、编辑都没有看到是如何保存创建人和更新人的字段,难道就不更新了吗?** 这里的话类似于创建时间和更新时间的自动填充,我把它封装到模型里面了,打开`BaseModel.php`,新增如下方法 ```php //插入前 public static function onBeforeInsert($data){ return $data['creator'] = getUserId(); } //更新前 public static function onBeforeUpdate($data){ return $data['updator'] = getUserId(); } ``` 利用模型事件,完成字段的填充,这样我们就不用每个方法都写这两行代码了。 > 作为一个程序员要具备懒人思想,想办法把重复的东西给封装起来 接下来就是控制器的代码了 打开`SysUser.php`,新增如下内容 ```php //新增 public function add(){ $data = $this->request->post(); //验证规则 $validate = [ 'account' => 'require', 'password' => 'require|regex:/^[a-zA-Z0-9_]{8,20}$/', ]; //提示信息 $message = [ 'account.require' => '账号不能为空!', 'password.require' => '密码不能为空!', 'password.regex' => '密码长度8~20位,包含字母数字下划线!', ]; $this->validate($data, $validate, $message); if(SysUserService::save($data)){ return success(); }else{ return failure(); } } //更新 public function edit(){ $data = $this->request->post(); //验证规则 $validate = [ 'id' => 'require', 'account' => 'require', ]; //提示信息 $message = [ 'id.require' => 'ID不能为空!', 'account.require' => '账号不能为空!', ]; if(!empty($data['password'])){ //如果密码不为空时,说明是修改密码,因此要进行密码校验 $validate['password'] = 'require|regex:/^[a-zA-Z0-9_]{8,20}$/'; $message['password.require'] = '密码不能为空!'; $message['password.regex'] = '密码长度8~20位,包含字母数字下划线!'; }else{ //销毁变量 unset($data['password']); } $this->validate($data, $validate, $message); if(SysUserService::update($data)){ return success(); }else{ return failure(); } } ``` 还是一样的逻辑,参数验证和结果返回,后面的功能就不再重复讲解,只贴出代码。 新增路由`app.php` ```php //路由分组 Route::group('sysuser', function(){ //新增管理员 Route::post('add','sysUser/add'); //编辑管理员 Route::post('edit','sysUser/edit'); })->middleware(app\admin\middleware\Auth::class); ``` ## 2.18分页查询 获取列表数据,一般都会设置搜索条件,就拿管理员列表为例子,根据账号搜索,这个功能是很常见的,一般来说我们会在业务核心层这样写 ```php if($request->get('account')){ $where = $query->where('account', $value); } ``` 当然这样写并没有问题,但你会发现如果条件很多,这个函数体的代码量就会很多,再说了这些搜索条件我们需要在任何地方都可以复用,为了解决这个问题,`thinkphp`的模型给我们提供了很便利的方法:**搜索器** 我们只需要在`SysUserModel.php`新增如下方法 ```php //根据账号搜索 public function searchAccountAttr($query, $value, $data) { $query->where('account', $value); } //根据状态搜索 public function searchStatusAttr($query, $value, $data) { $query->where('status', $value); } ``` 这些搜索只针对当前的模型,简单的讲就是根据表的字段去查询数据,你可以随便定义搜索条件,无论哪个应用都可以共用这些搜索条件 我们调用的时候可以这样调用 ```php SysUserModel::withSearch(array_keys($param),$param)->select(); ``` `withSearch()`第一个参数是搜索的字段,是一个一维数组,例如['account','status'] 第二个参数是搜索字段对应的条件,也是一个一维数组,例如['account'=>'admin'] 接下来实现业务核心层的代码,打开`SysUserService.php` ```php /** * 列表 * @param array $param 请求参数 * @return array */ public static function page($param){ //分页设置 $page = !isset($param['page']) ? 1 : $param['page']; $pageSize = !isset($param['page_size']) || $param['page_size'] > 100 ? 10 : $param['page_size']; //提取搜索体检 $keyArr = array_keys($param); $query = SysUserModel::withSearch(array_keys($param),$param); $data['total'] = $query->count(); $data['list'] = []; if($data['total'] > 0){ $data['list'] = $query->withoutField('password')->order('id','desc')->limit(($page-1)*$pageSize,$pageSize)->select()->toArray(); foreach ($data['list'] as $k => $v) { $data['list'][$k]['status_text'] = SysUserModel::$status[$v['status']]; } } return $data; } ``` 这里有个关键的地方,我们先查总数,如果总数为0,那就没有必要查列表了,直接返回。另外的话这个状态是个整型,因此我们需要转换成中文,因此我在`SysUserModel.php`定义了如下的数组 ```php public static $status = [0=>'禁用',1=>'正常']; ``` **疑问:不知道大家有没有发现,分页查询代码的逻辑都一样,我们可不可以封装一下,下次就可以不用写那么多?** 这都是懒人思想在作怪,为了偷懒,咱们当然可以把公共的东西抽离出来,咱们的基础模型`BaseModel.php`又派上用场了,我们只需要在这里封装一个分页查询的方法,整个项目都可以使用它 ```php //分页查询 public static function page($param, $other = []){ //分页默认值 $page = !isset($param['page']) ? 1 : $param['page']; $pageSize = !isset($param['page_size']) || $param['page_size'] > 100 ? 10 : $param['page_size']; //设置排序 if(isset($other['order']) && $other['order']){ $arr = explode(',',$other['order']); $orderField = $arr[0]; $orderValue = $arr[1]; }else{ $orderField = "id"; $orderValue = "desc"; } $keyArr = array_keys($param); $query = self::withSearch(array_keys($param),$param); $data['total'] = $query->count(); $data['list'] = []; if($data['total'] > 0){ //设置排除字段 if(isset($other['withoutField']) && $other['withoutField']){ $query = $query->withoutField($other['withoutField']); } //设置查询的字段 if(isset($other['field']) && $other['field']){ $query = $query->field($other['field']); } $data['list'] = $query->order($orderField,$orderValue)->limit(($page-1)*$pageSize,$pageSize)->select()->toArray(); } return $data; } ``` 再回头看看我们的业务核心层代码 ```php /** * 列表 * @param array $param 请求参数 * @return array */ public static function page($param){ $data = SysUserModel::page($param,['withoutField'=>'password']); foreach ($data['list'] as $k => $v) { $data['list'][$k]['status_text'] = SysUserModel::$status[$v['status']]; } return $data; } ``` 看到没有,就是这么简单 再看看我们控制器`SysUser.php`写的代码 ```php //列表 public function page(){ $param = $this->request->get(); return success(SysUserService::list($param)); } ``` ## 2.19删除管理员 这里的删除是并不是真实的删除,而是指逻辑删除(软删除),一般情况下删除数据都是根据id进行删除,另外删除有可能删除一条,也有可能批量删除,因此从实际业务出发,这里又对它进行封装 打开`BaseModel.php`,新增以下内容 ```php /** * 删除 * @param string $ids 需要删除数据的id */ public static function deleteById($ids){ if(!empty($ids)){ foreach(explode(',',$ids) as $id){ $model = self::find($id); $model->delete_time = date('Y-m-d H:i:s',time()); $model->save(); } } } ``` 这段代码满足批量删除,接下来业务核心层及控制器层调用就非常简单了 `SysUserService.php` ```php /** * 删除 * @param string $ids 需要删除数据的id */ public static function destroy($ids){ if(!empty($ids)){ SysUserModel::deleteById($ids); } } ``` `SysUser.php` ```php //删除 public function delete(){ $ids = $this->request->get("ids"); if(empty($ids)){ return failure(config('error.er5')['code'],"请选择要删除的数据"); } SysUserService::destroy($ids); return success(); } ``` ## 2.20其他 **修改个人密码** 打开`SysUser.php`,新增以下内容 ```php //修改个人密码 public function updatePwd(){ $data = $this->request->post(); //验证规则 $validate = [ 'password' => 'require|regex:/^[a-zA-Z0-9_]{8,20}$/', 'repassword' => 'require' ]; //提示信息 $message = [ 'password.require' => '密码不能为空!', 'password.regex' => '密码长度8~20位,包含字母数字下划线!', 'repassword.require' => '确认密码不能为空!', ]; $this->validate($data, $validate, $message); //两次输入的密码不一致 if($data['password'] !== $data['repassword']){ return failure(config('error.er6')['code'],config('error.er6')['msg']); } if(SysUserService::update($data)){ return success(); }else{ return failure(); } } ``` 修改密码其实也是修改编辑管理员信息,只不过这个管理员是自己而已 **启用/禁用** ```php //启用、禁用 public function updateStatus(){ $data = $this->request->post(); //验证规则 $validate = [ 'id' => 'require', 'status' => 'require' ]; //提示信息 $message = [ 'id.require' => '请选择要禁用的数据!', 'status.require' => '状态不能为空!', ]; $result = $this->validate($data, $validate, $message); if($result !== true){ return failure(config('error.er5')['code'],$result->getError()); } $data['updator'] = $this->user->id; if(SysUserService::update($data)){ return success(); }else{ return failure(); } } ``` 看看路由的配置 ```php //获取验证码 Route::get('login/verify','sysUser/verify'); //管理员登录 Route::post('login','sysUser/login'); //路由分组 Route::group('sysuser', function(){ //新增管理员 Route::post('add','sysUser/add'); //编辑管理员 Route::post('edit','sysUser/edit'); //管理员列表 Route::get('list','sysUser/list'); //启用、禁用 Route::post('updateStatus','sysUser/updateStatus'); //删除 Route::get('delete','sysUser/delete'); //修改个人密码 Route::post('updatePwd','sysUser/updatePwd'); })->middleware(app\admin\middleware\Auth::class); ``` **注:** > 无论是修改密码还是对账号进行启用、禁用,其本质是都是根据id修改数据,因此这里的业务逻辑直接调用common应用里面的service层,当然这里你也可以把业务写在admin下的service层,因为管理员模块一般都是amdin应用特有的 ## 2.21角色管理 角色管理主要分为两部分: - 增删改查 - 授权,该角色拥有什么权限 **增删改查** 这一部分的内容很简单,并没有什么特殊操作,我们在业务核心层新增四个方法:增、删、改、查 在`common/service`下新建`SysRoleService.php`,新增如下内容 ```php id; } /** * 更新 * @param array $data 更新的数据 * @return int */ public static function update($data){ $role = SysRoleModel::find($data['id']); if(!$role){ serviceException(config('error.er15')['code'],config('error.er15')['msg']); } $role->save($data); return $role->id; } /** * 删除 * @param string $ids 需要删除数据的id */ public static function destroy($ids){ if(!empty($ids)){ SysRoleModel::deleteById($ids); } } /** * 分页 * @param array $param 搜索条件 * @return array */ public static function page($param){ $data = SysRoleModel::page($param); return $data; } } ``` 再新建控制器`SysRole.php` ```php request->post(); //验证规则 $validate = [ 'role_name' => 'require', ]; //提示信息 $message = [ 'role_name.require' => '名称不能为空!', ]; $this->validate($data, $validate, $message); if(SysRoleService::save($data)){ return success(); }else{ return failure(); } } //更新 public function edit(){ $data = $this->request->post(); //验证规则 $validate = [ 'role_name' => 'require', ]; //提示信息 $message = [ 'role_name.require' => '名称不能为空!', ]; $this->validate($data, $validate, $message); if(SysRoleService::update($data)){ return success(); }else{ return failure(); } } //列表 public function page(){ $param = $this->request->get(); return success(SysRoleService::page($param)); } //删除 public function delete(){ $ids = $this->request->get("ids"); if(empty($ids)){ return failure(config('error.er5')['code'],"请选择要删除的数据"); } SysRoleService::destroy($ids); return success(); } } ``` 对应的路由配置`app.php` ```php //路由分组 Route::group('sysuser', function(){ //新增管理员 Route::post('add','sysUser/add'); //编辑管理员 Route::post('edit','sysUser/edit'); //管理员列表 Route::get('page','sysUser/page'); //启用、禁用 Route::post('updateStatus','sysUser/updateStatus'); //删除 Route::get('delete','sysUser/delete'); //修改个人密码 Route::post('updatePwd','sysUser/updatePwd'); //新增角色 Route::post('role/add','sysRole/add'); //编辑角色 Route::post('role/edit','sysRole/edit'); //角色列表 Route::get('role/page','sysRole/page'); //删除角色 Route::get('role/delete','sysRole/delete'); })->middleware(app\admin\middleware\Auth::class); ``` 这里就没有新建一个role组,这里分组的思路按照模块来,角色属于管理员模块,因此就把路由建在一个分组下 **授权** 不同的角色拥有不同的权限,比如说商品管理员拥有管理商品的权限,订单管理员拥有订单管理的权限,老板拥有所有权限 那如何授权呢? 我们拉到最后,看看`管理员角色表`,你发现有这样的一个字段`auth_ids`,它就是存储权限的id,因此实现起来也比较简单,其实就是更新这条信息而已 打开`SysRole.php`,新增以下内容 ```php //授权 public function hasAuth(){ $data = $this->request->post(); //验证规则 $validate = [ 'id' => 'require', 'auth_ids' => 'require', ]; //提示信息 $message = [ 'id.require' => 'ID不能为空!', 'auth_ids.require' => '请选择相应权限!', ]; $this->validate($data, $validate, $message); if(SysRoleService::update($data)){ return success(); }else{ return failure(); } } ``` auth_ids内容是用逗号分开的权限ID ## 2.22菜单管理 新建模型`SysMenuModel.php` ```php id; } /** * 更新 * @param array $data 更新的数据 * @return int */ public static function update($data){ $result = SysMenuModel::find($data['id']); if(!$result){ failure(config('error.er15')['code'],config('error.er15')['msg']); } $result->save($data); return $result->id; } /** * 删除 * @param string $ids 需要删除数据的id */ public static function destroy($ids){ if(!empty($ids)){ SysMenuModel::deleteById($ids); } } /** * 列表 * @return array */ public static function list(){ $list = SysMenuModel::select()->toArray(); $list = treeMenu($list); return $list; } } ``` 这里面的列表查询有点特别,首先我们要一次性查出所有菜单,然后递归输出其层级结构,我们看看这个递归函数 `common.php` ```php /** * 递归输出菜单 * @param array $data 菜单列表 * @param array $parentId 父级菜单ID */ function treeMenu($data, $parentId=0){ if(empty($data)) { return []; } $list = []; foreach ($data as $k => $v) { if($parentId == $v['parent_id']){ $v['children'] = treeMenu($data,$v['id']); $list[] = $v; } } return $list; } ``` 最后输出的内容是这样的 ```json Array ( [0] => Array ( [id] => 1 [parent_id] => 0 [name] => 管理员管理 [icon] => [route] => [is_show] => 1 [children] => Array ( [0] => Array ( [id] => 2 [parent_id] => 1 [name] => 管理员列表 [icon] => [route] => sysuser/list [is_show] => 1 [children] => Array ( ) ) [1] => Array ( [id] => 3 [parent_id] => 1 [name] => 角色列表 [icon] => [route] => sysrole/list [is_show] => 1 [children] => Array ( ) ) ) ) [1] => Array ( [id] => 4 [parent_id] => 0 [name] => 系统管理 [icon] => [route] => [is_show] => 1 [children] => Array ( [0] => Array ( [id] => 5 [parent_id] => 4 [name] => 菜单管理 [icon] => [route] => menu/list [is_show] => 1 [children] => Array ( ) ) ) ) ) ``` 控制器`SysMenu.php` ```php request->post(); //验证规则 $validate = [ 'name' => 'require' ]; //提示信息 $message = [ 'name.require' => '名称不能为空!' ]; $this->validate($data, $validate, $message); if(SysMenuService::save($data)){ return success(); }else{ return failure(); } } //更新 public function edit(){ $data = $this->request->post(); //验证规则 $validate = [ 'id' => 'require', 'name' => 'require' ]; //提示信息 $message = [ 'id.require' => 'ID不能为空!', 'name.require' => '名称不能为空!' ]; $this->validate($data, $validate, $message); if(SysMenuService::update($data)){ return success(); }else{ return failure(); } } //列表 public function list(){ return success(SysMenuService::list()); } //删除 public function delete(){ $ids = $this->request->get("ids"); if(empty($ids)){ return failure(config('error.er5')['code'],"请选择要删除的数据"); } SysMenuService::destroy($ids); return success(); } } ``` ## 2.23权限控制 ### 2.23.1左侧菜单 当用户登录成功后,不同角色应该看到不一样的菜单,如图 ![](demo-shop-img/后台菜单.jpg) 那接下来看看如何实现 打开`SysUserService.php`,新增以下内容 ```php /** * 详情 * @param string $id 用户ID */ public static function info($id){ $user = SysUserModel::withoutField('password')->find($id)->toArray(); $user['menu'] = []; if($user['root'] == 1){ //如果是超级管理员,直接返回所有菜单 $user['menu'] = SysMenuService::list(); }else{ if($user['roles']){ //根据角色ID获取角色拥有的菜单ID $authIds = SysRoleService::getAuths($user['roles']); //根据id获取菜单详细信息,例如名称 $user['menu'] = SysMenuService::listById($authIds); } } return $user; } ``` 这个函数主要是获取用户详细信息,其中包含了用户可以访问到菜单栏目。这里面有个判断就是如果`root=1`表示是超级管理员(一般是开发者账号的归属角色),否则就是获取该账号下角色拥有的权限 **注:** > `$user['roles']`这里存储的是用逗号分割的角色ID,其实就是数据表字段 我们看看`SysRoleService.php`里面的`getAuths()`方法 ```php /** * 返回角色的权限ID * @param string $ids 角色ID * @return array */ public static function getAuths($ids){ $result = SysRoleModel::where('id','in',$ids)->select()->toArray(); if(count($result) == 0){ return []; } $atuhStr = ''; foreach ($result as $k => $v) { if($v['auth_ids']){ $atuhStr = $atuhStr.$v['auth_ids'].','; } } if(empty($atuhStr)){ return []; } return array_unique(explode(',',substr($atuhStr,0,-1))); } ``` 每个角色保存的权限id是用逗号分隔,如果多个角色,就把所有`auth_ids`串联起来,然后再分割、去重,最后得到当前用户的权限ID,最后根据ID去获取列表信息 这边的控制器`SysUser.php`比较简单 ```php //登录用户信息 public function loginUserInfo(){ return success(SysUserService::info(getUserId())); } ``` 这个方法方法一般是在登录成功里面调用的 ### 2.23.2授权判断 上面一节我们实现了界面的权限控制,但细想一下,假如说系统设置下还有一个`会员注册设置`的菜单,虽然说它不会显示在页面左侧,但如果直接访问的话,它还是可以被访问到,因为到目前为止,我们只要求用户登录了就能访问后台,并没有对其拥有的权限进行授权设置。 那怎么时间权限校验呢?下面是大概思路 1. 新增菜单的时候,我们需要添加一个权限标识,也就是保存到菜单表的`perms`字段的值,这个值一般就是访问当前菜单的控制器名+方法名 2. 就是上一节说的,获取当前登录用户拥有的菜单权限 3. 获取当前访问的控制器名+方法名,看看这个组合是否存在于当前用户拥有的菜单权限中 有了思路之后呢,我们打开登录校验的中间件`Auth.php`,在方法里面增加如下的代码 ```php public function handle($request, \Closure $next) { try{ $token = $request->header('token'); $device = $request->header('device'); if($device == 'admin' && $token){ //解析token得到用户信息 $userInfo = JWT::decode($token,new Key(config('app.jwt_code_admin'),'HS256')); //把部分用户信息保存到全局reqeust对象中 $request->user = ['id'=>$userInfo->id,'account'=>$userInfo->account]; //判断是否有权限访问 if(!$this->checkPermission($request,$userInfo)){ return failure(config('error.er19')['code'],config('error.er19')['msg']); } }else{ return failure(config('error.er14')['code'],config('error.er14')['msg']); } }catch (\Exception $e) { return failure(config('error.er14')['code'],config('error.er14')['msg']); } return $next($request); } private function checkPermission($request,$userInfo){ if($userInfo->root == 1){ //超管直接返回true return true; } if(!$userInfo->roles){ return false; } if(cache(ADMIN_PERMS_.$userInfo->id)){ $perms = cache(ADMIN_PERMS_.$userInfo->id); }else{ //根据角色获取菜单ID $authIds = SysRoleService::getAuths($userInfo->roles); //根据菜单ID获取权限标识,返回的是一个一维数组 $perms = SysMenuService::getPerms($authIds); //保存到缓存中,避免每次查询数据库,注意这里的键值使用了用户ID进行区分 cache(ADMIN_PERMS_.$userInfo->id,$perms); } if(count($perms) == 0) return false; //通过request对象获取当前访问的控制器名和方法名,也就是权限标识 $current = $request->controller().'/'.$request->action(); //判断标识是否存在于当前用户中 if(!in_array($current,$perms)){ return false; } return true; } ``` **注:** > 建议此功能放到最后开发,或者是开发好注释掉,这样方便开发 # 3.后台会员模块 后台会员的数据来源一般都是用户在网站进行注册而得到的,因此在讲后台管理模块时,会穿插讲一下会员的注册 ## 3.1功能简介 下面规划的一些基础功能,前期从简单的开始,后面再来丰富它的功能 | 功能 | 描述 | | -------- | --------------------------- | | 会员注册 | 手机号注册、手机验证码 | | 会员信息 | 查询、更新、启用/禁用、删除 | | 会员等级 | 新增、查询、更新、删除 | ## 3.2手机验证码 目前用户注册比较主流的都是使用手机号了,因此我们先来实现一下发生手机验证码的功能 ### 3.2.1免费的短信服务 手机短信平台有很多,例如百度的、腾讯的、阿里的,无论哪个平台都有相应的`sdk`,这里我们使用阿里云的短信服务,上面提供了免费服务供我们学习测试打开如下 地址:https://dysms.console.aliyun.com/quickstart,然后获取相对应的签名和模板 ![](demo-shop-img/手机验证码01.jpg) ![](demo-shop-img/手机验证码02.jpg) 这里边是示例代码,有几个比较重要的参数,`accessKeyId`,`accessKeySecret`,`signName`,`templateCode` 其中`signName`,`templateCode`示例代码有 至于`accessKeyId`,`accessKeySecretd`的获取,从下面入口进入即可 ![](demo-shop-img/手机验证码03.jpg) 做完这些准备工作后,下面开始写代码了 ### 3.2.2策略模式 这里我们学习一种`php`的设计模式-策略模式,来封装发送验证码的核心代码,我们先了解一下它的概念 **策略模式**又叫做政策模式,用于如何组织和调用算法的,是属于行为型模式的一种。 策略模式需要三个角色构成: - `Context` 封装角色:也叫做上下文角色,起承上启下封装作用,屏蔽高层模块对策略、算法的直接访问,封装可能存在的变化 - `Strategy` 抽象策略角色:通常为接口,指定规则 - `ConcreteStrategy` 具体策略角色:实现抽象策略中的操作,该类含有具体的算法 **优点** 算法可以通过参数自由切换, 方便扩展,增加策略只需要实现接口就行了 。 **第一步:Strategy 抽象策略角色** 它就是定义一个基础接口,这样做的目的就是规范代码,不管你用哪个服务商的代码,但都必须实现这个基础接口 在`commom`应用里面新建一个lib目录(第三方组件)--->`msg`目录(手机发送信息组件)--->`Message.php` ```php namespace app\common\lib\msg; interface Message { /** * 发送验证码 * @param string $mobile 手机号 * @param string $code 验证码 * @return bool */ public function send($mobile,$code); } ``` 这里定义了一个`send()`方法,后续接入进来的服务商代码都要继承这个接口,实现`send()`方法 **第二步:ConcreteStrategy 具体策略角色 ** 以阿里云的服务商为例,我们把代码复制过来 ```php class AliYunMessage implements Message { //实现接口 public function send($mobile, $code){ //读取配置文件的配置 $config = new Config([ "accessKeyId" => config('aliyun.accessKeyId'), "accessKeySecret" => config('aliyun.accessKeySecret') ]); $config->endpoint = config('aliyun.dysmsapi'); $client = new Dysmsapi($config); $sendSmsRequest = new SendSmsRequest([ "phoneNumbers" => $mobile, "signName" => config('aliyun.signName'), "templateCode" => config('aliyun.templateCode'), "templateParam" => json_encode(['code'=>$code]) ]); $runtime = new RuntimeOptions([]); try { // 复制代码运行请自行打印 API 的返回值 $rs = $client->sendSmsWithOptions($sendSmsRequest, $runtime); Log::write("获取手机验证码:".json_encode($rs)); return true; } catch (Exception $error) { if (!($error instanceof TeaError)) { $error = new TeaError([], $error->getMessage(), $error->getCode(), $error); } Log::write('获取手机验证码异常:'.$error->message); return false; } } } ``` 代码中第一步就是读取配置文件的一些关键信息,这里需要`common`下新建`config`目录新建`aliyun.php`,内容如下 ```php '', // AccessKey Secret 'accessKeySecret' => '', /***短信配置****/ // 签名 'signName' => '阿里云短信测试', // 短信模板CODE 'templateCode' => 'SMS_154950909', // 访问的域名 'dysmsapi' => 'dysmsapi.aliyuncs.com' ]; ``` **第三步:Context 封装角色** ```php namespace app\common\lib\msg; class MessageContext { private $message; public function __construct(Message $msg) { $this->message = $msg; } public function sendMessage($mobile,$code) { return $this->message->send($mobile,$code); } } ``` 这个类里面的构造函数的参数实际就是策略类,然后再调用其里面的`send()`方法 最后看看调用示例 ```java $msgCtx = new MessageContext(new AliYunMessage); $msgCtx->sendMessage($mobile,$code); ``` 有些人可能会觉得,如果直接在`AliYunMessage`类定义一个静态方法,然后直接调用是不是更加方便? 显然,确实是更加方便。 这里我们使用这种策略模式,其目的就是多学习一下php的设计模式,它本身的代码也不复杂,适合在项目中使用。另外它的一个优点就是调用方并不是直接调用算法,而是通过上下文角色进行调用,这样我们就不需要关注算法本身,如果我们要加策略,直接新增类就行。 **注:** > 把手机验证码发送的核心逻辑封装在`common`应用里面,主要还是因为它是一个公用的功能,任务其它应用如果使用到手机验证码都可以调用它 ## 3.3用户注册 ### 3.3.1获取手机验证码 用户注册肯定是web端应用,因此在获取验证码的之前,我们先建一个`pc`目录作为web端应用,在`pc`目录下新建如下目录 ``` ├─app 应用目录 │ ├─pc 应用目录 │ │ ├─controller 控制器目录 │ │ ├─config 配置目录 │ │ ├─route 路由目录 │ │ ├─service 业务核心目录 │ │ ├─middleware 中间件目录 │ │ └─ ... 更多类库目录 ``` 类似于`admin`应用,我们也为`pc`应用新建一个`PcController`的基础控制器,内容跟`admin`应用差不多,大家可以下载源码看 接下来在`common`应用下的`service`目录,新建`UserService.php`用户核心业务层 ```php /** * 获取手机验证码 * @param string $mobile 手机号 * @param string $limit 位数 * @return bool */ public static function getMobileCode($mobile, $limit = 4){ if($limit == 4){ $code = rand(0000,9999); }else{ $code = rand(000000,999999); } //如果手机验证码还没有过期,则提示不要重复发送 if(cache(config('cachekey.mobile_code').$mobile)){ serviceException(config('error.er3')['code'],config('error.er3')['msg']); } $msgCtx = new MessageContext(new AliYunMessage); if($msgCtx->sendMessage($mobile,$code)){ cache(config('cachekey.mobile_code').$mobile,$code,120); return true; }else{ return false; } } ``` 该函数实现了手机发送4位或6位手机验证码,这里调用我们前面封装好的函数,然后保存到缓存中,注意这里的key值,一定要把手机号作为key的一部分,这样才能唯一,同时缓存时间设置位120秒 ### 3.3.2注册 在common/model新建`UserModel.php` ```php save($data); cache(config('cachekey.mobile_code').$data['mobile'],null); return $user->id; } ``` 首先判断账号和手机号是否被注册,然后对密码进行加密存储,最后新增到数表中,并删除验证码缓存。 这里的密码加密函数,是自定义在`app`目录下的`common.php` ```php /** * 密码加密 * @param string $pw 要加密的原始密码 * @param string $authCode 加密字符串 * @return string */ function createPassword($pw, $authCode = '') { if (empty($authCode)) { $authCode = config('app.authcode'); } $result = "***" . md5(md5($authCode . $pw)); return $result; } /** * 密码比较方法,所有涉及密码比较的地方都用这个方法 * @param string $password 要比较的密码 * @param string $passwordInDb 数据库保存的已经加密过的密码 * @return boolean 密码相同,返回true */ function comparePassword($password, $passwordInDb) { return createPassword($password) == $passwordInDb; } ``` 一个是加密函数,一个是比较函数 最后就是控制器`User.php`代码 ```php //用户注册 public function register(){ $data = $this->request->post(); $data['account'] = trim($data['account']); //验证规则 $validate = [ 'account' => 'require', 'password' => 'require|regex:/^[a-zA-Z0-9_]{8,20}$/', 'repassword' => 'require', 'mobile' => 'require|regex:/^1[3-9]\d{9}$/', 'code' => 'require' ]; //提示信息 $message = [ 'account.require' => '账号不能为空!', 'password.require' => '密码不能为空!', 'password.regex' => '密码长度8~20位,包含字母数字下划线!', 'repassword.require' => '确认密码不能为空!', 'mobile.require' => '手机号不能为空!', 'mobile.regex' => '手机号格式不正确!', 'code.require' => '验证码不能为空!', ]; $this->validate($data, $validate, $message); //验证码不一致 if($data['code'] != config('cachekey.mobile_code').$data['mobile']){ return failure(config('error.er7')['code'],config('error.er7')['msg']); } //验证两次输入的密码是否一致 if($data['password'] != $data['repassword']){ return failure(config('error.er6')['code'],config('error.er6')['msg']); } if(PuserService::register($data)){ return success(); }else{ return failure(); } } ``` 没有啥逻辑可言,就是验证数据 最后就是路由 ```php //用户注册 Route::post('user/register','user/register'); ``` ## 3.4用户列表 这一章回到我们的`admin`应用,管理员可以查看会员的信息,包括对其进行禁用/启用,首先我们实现它的列表接口 打开`UserService.php`,新增如下内容 ```php /** * 用户列表 * @param array $param 请求参数 * @return array */ public static function page($param){ $data = UserModel::page($param,['withoutField'=>'password']); foreach ($data['list'] as $k => $v) { $data['list'][$k]['status_text'] = UserModel::$status[$v['status']]; } return $data; } ``` 就是这么方便,我们直接调用`BaseModel`里面封装的`page()`方法即可,获取到数据后,我们需要对一些字段进行转换,例如代码中的状态,需要把数字转换成中文,因此在`UserModel`模型定义了如下的静态数组 ```php public static $status = [0=>'禁用',1=>'正常']; ``` 控制器代码更简单了,在`admin/controller`应用下新建`User.php` ```php //分页列表 public function page(){ return $this->success(UserService::page($this->request->get())); } ``` **注:** > 只要前期做好了代码的封装,后续开发会非常的方便,包括后面要做的会员等级列表也是一样 ## 3.5启用/禁用 可能因为某些原因,我们需要对违规的会员账号进行禁用操作,目前实现这一功能主要是更改会员表的`status`字段即可 打开`common/service/UserService.php`, ```php /** * 更新数据 * @param array $data 更新数据 * @return bool */ public static function update($data){ $user = UserModel::find($data['id']); if(!$user){ serviceException(config('error.er15')['code'],config('error.er15')['msg']); } if($user->save($data)){ return true; }else{ return false; } } ``` 启用、禁用本质上是修改数据,因此我们在`common`应用下封装一个更新用户信息的方法,后面会员修改个人信息的时候也会用到 再看看控制器`User.php`代码 ```php //启用、禁用 public function updateStatus(){ $data = $this->request->post(); //验证规则 $validate = [ 'id' => 'require', 'status' => 'require' ]; //提示信息 $message = [ 'id.require' => '请选择要禁用的数据!', 'status.require' => '状态不能为空!', ]; $this->validate($data, $validate, $message); if(UserService::update($data)){ return success(); }else{ return failure(); } } ``` ## 3.6会员等级 会员等级的管理并没有什么复杂的逻辑,只是简单的对数据的增删改查,后面如果遇到跟它类似的模块都按照这个模块的方式去开发 新建`common/model/UserLevelModel.php` ```php save($data); //清空缓存 cache(config('redisKey.USER_LEVEL_LIST'),null); return $userLevel->id; } /** * 更新等级数据 * @param array $data 更新的数据 * @return int */ public static function update($data){ $user = UserLevelModel::find($data['id']); if(!$user){ serviceException(config('error.er15')['code'],config('error.er15')['msg']); } if($user->save($data)){ //清空缓存 cache(config('redisKey.USER_LEVEL_LIST'),null); return true; }else{ return false; } } /** * 删除 * @param string $ids 需要删除数据的id */ public static function destroy($ids){ if(!empty($ids)){ //清空缓存 cache(config('redisKey.USER_LEVEL_LIST'),null); UserLevelModel::destroy(explode(',',$ids)); } } /** * 会员等级列表 * @return array */ public static function list(){ if(cache(config('redisKey.USER_LEVEL_LIST'))){ return cache(config('redisKey.USER_LEVEL_LIST')); } $list = UserLevelModel::order('growth_value','asc')->select(); if(!empty($list)){ //永久缓存 cache(config('redisKey.USER_LEVEL_LIST'),$list,0); } return $list; } ``` 会员等级不会有很多条数据,因此这里不做分页,同时我们给列表数据加上缓存。 控制器`UserLevel.php`代码 ```php request->get(); return $this->success(UserLevelService::list($param)); } //新增 public function add(){ $data = $this->request->post(); //验证规则 $validate = [ 'level_name' => 'require', 'growth_value' => 'require', ]; //提示信息 $message = [ 'level_name.require' => '等级名称不能为空!', 'growth_value.require' => '成长值不能为空!', ]; $this->validate($data, $validate, $message); if(UserLevelService::save($data)){ return success(); }else{ return failure(); } } //更新 public function edit(){ $data = $this->request->post(); //验证规则 $validate = [ 'id' => 'require', 'level_name' => 'require', 'growth_value' => 'require', ]; //提示信息 $message = [ 'id.require' => '请选择要更新的数据!', 'level_name.require' => '等级名称不能为空!', 'growth_value.require' => '成长值不能为空!', ]; $this->validate($data, $validate, $message); if(UserLevelService::update($data)){ return success(); }else{ return failure(); } } //删除 public function delete(){ $ids = $this->request->get("ids"); if(empty($ids)){ return failure(config('error.er5')['code'],"请选择要删除的数据"); } UserLevelService::destroy($ids); return success(); } } ``` ## 3.7完善会员列表 之前我们查询用户列表的时候,返回的用户等级level字段是一个整型,它存储的是`ds_user_level`这个表的自增ID,因此我们需要在查询用户列表的时候把level_name给查出来。 打开`UserLevelService.php`,新增一个方法 ```php /** * 根据ID获取名称 * @param string $id 等级ID * @return string */ public static function getLevelNameById($id){ $list = self::list(); $levelName = ''; foreach($list as $v){ if($v['id'] == $id){ $levelName = $v['level_name']; break; } } return $levelName; } ``` 这段代码先调用`list()`方法,获取所有用户等级,然后再根据传进来的ID去获取相对于的等级名称 接下来打开`UserService.php`,找到之前写好的`page($param)`方法,在循环体里面新增如下一行代码即可 ```php foreach ($data['list'] as $k => $v) { $data['list'][$k]['status_text'] = UserModel::$status[$v['status']]; //新加的 $data['list'][$k]['level_name'] = UserLevelService::getLevelNameById($v['level']); } ``` 这里我并没有使用连表查询,其实开发过程中尽量不要连表查,特别是表数据量大,连的表比较多的时候更加不要连表。像上面这种情况是可以连表的,因为等级表的数据量就是那么几条,连表也无所谓。 另外还有一个地方,上诉代码中在循环体中调用了`getLevelNameById()`这个方法,需要注意的是如果这个方法里面每次都要访问数据库,就不建议这样去做,因为这会增加数据库的压力,千万别怀疑这个东西,当你的循环体有好几个都是这么写,系统越来越庞大,数据量越来越多,性能肯定会下降的,因此我们从一开始就要杜绝这种情况的发生。 这里我之所有那么写,是因为`getLevelNameById($id)`这个方法的实现理论上是不用查数据库的,即使要查也是最多查一次,因为用户等级全部数据都是存储在redis缓存中。 相信大家也知道为啥我不这样实现这个方法 ```php public static function getLevelNameById($id){ $level = UserLevelModel::find($id); return $level->level_name; } ``` **注:** > 上面这种实现逻辑主要是针对表数据比较少的情况,如果说用户等级的数据也几十万条,甚至更多,那这里就不能这样实现,因为你不太可能一次性查出所有数据 ## 3.8会员注册默认等级 现在回到注册那一块,注册的时候我们需要给一个默认的会员等级,一般都是最普通的等级。 打开`pc/service/PuserService.php`,找到之前写的`register($data)`方法,新增如下代码 ```php public static function save($data){ //验证该账号是否存在 if(UserModel::getByAccount($data['account'])){ failure(config('error.er8')['code'],config('error.er8')['msg']); } //验证手机号是否存在 if(UserModel::getByMobile($data['mobile'])){ failure(config('error.er9')['code'],config('error.er9')['msg']); } //默认会员等级----这是新增的代码 $level = UserLevelService::list(); $data['level'] = empty($level) ? 0 : $level[0]['id']; $data['password'] = createPassword($data['password']); $user = new UserModel; $user->save($data); return $user->id; } ``` 这里调用`UserLevelService::list()`获取会员等级列表,这里的会员等级列表获取是根据`成长值`字段排序输出的,排在第一个的肯定是最开始的一个等级。 # 4.后台商品模块 商品模块可以说是最复杂最核心的一个模块了,首先我们要搞清楚需要实现什么功能,然后我们再去设计数据库。 商品这个模块,我打算拿京东商城的作为参考,可以说是比较复杂的了,只要把这个搞懂了,以后遇到其它的商城项目,对你来说,应该就没啥难度了 ## 4.1谈谈SPU和SKU 如果谈到商城项目,特别是目前主流的商城网站,商品SPU和SKU 当属最常用的术语了,我们需要根据它去设计数据的存储方式。 **SPU**是指产品标准化信息单元(Standard Product Unit),是描述一类产品的属性、特征、规格等信息的统一标准 。例如牛仔裤它就是一个spu,一般来说包含了尺寸、颜色、适用季节、适用人群、风格等等属性,你可以把它看作是这一类东西的集合 **SKU**是指库存量单位(Stock Keeping Unit),是指一种特定型号、颜色、尺寸等属性的产品,每个SKU都有一个唯一的编码。你可以把它理解为具体的一个产品 电商平台上,一个SPU可能对应多个SKU,每个SKU代表着不同的产品属性组合,例如不同颜色或尺寸的牛仔裤。通过使用SPU和SKU来管理商品信息,电商平台可以更好地组织和展示商品,方便消费者进行比较和选择。 下面截取了某商城的网页 ![](demo-shop-img/4.1spu.jpg) 这里展示的牛仔裤就是一个spu,它具备了尺码、颜色、适用季节、裤型等属性,当面我们点击其中一个spu来到详情页 ![](demo-shop-img/4.1sku.jpg) 这里展示的是这个spu集合里面具体的某个产品,也就是sku,这里面sku也有相关的属性,例如颜色、尺码 ## 4.2设计数据表 # 附录 ## 1.建表语句 ### **1.1会员表** ```sql CREATE TABLE `ds_user` ( `id` int(11) UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '自增ID', `user_sn` char(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT '' COMMENT '会员码', `account` varchar(30) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT '' COMMENT '账号', `password` varchar(80) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT '' COMMENT '密码', `nickname` varchar(30) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT '' COMMENT '昵称', `avatar` varchar(200) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT '' COMMENT '头像', `mobile` varchar(11) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT '' COMMENT '手机', `level` tinyint(4) NULL DEFAULT 0 COMMENT '等级', `sex` tinyint(1) NULL DEFAULT 0 COMMENT '性别:0-未知;1-男;2-女', `birthday` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT '' COMMENT '生日', `user_integral` int(11) NULL DEFAULT 0 COMMENT '积分', `user_growth` int(11) NULL DEFAULT 0 COMMENT '成长值', `login_time` datetime(0) NULL DEFAULT NULL COMMENT '最后登录时间', `login_ip` varchar(30) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT '' COMMENT '最后登录IP', `status` tinyint(4) NULL DEFAULT 1 COMMENT '状态:0-禁用 1-启用', `is_first_login` tinyint(1) NULL DEFAULT 0 COMMENT '第一次登录:0-未登录 1-已经登录过', `creator` int(11) NULL DEFAULT 0 COMMENT '创建人', `updator` int(11) NULL DEFAULT 0 COMMENT '更新人', `create_time` datetime(0) NULL DEFAULT NULL COMMENT '创建时间', `update_time` datetime(0) NULL DEFAULT NULL COMMENT '更新时间', `delete_time` datetime(1) NULL DEFAULT NULL COMMENT '删除时间,默认为空', PRIMARY KEY (`id`) USING BTREE, INDEX `idx_account`(`account`) USING BTREE, INDEX `idx_mobile`(`mobile`) USING BTREE ) ENGINE = InnoDB AUTO_INCREMENT = 1 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '用户表' ROW_FORMAT = Dynamic; ``` | 字段 | 类型 | 备注 | | -------------- | ---------------- | --------------------------------- | | id | int(11) unsigned | 主键(PRIMARY) | | user_sn | char(20) | 会员码 | | account | varchar(30) | 账号 | | password | varchar(80) | 密码 | | nickname | varchar(30) | 昵称 | | avatar | varchar(200) | 头像 | | mobile | varchar(11) | 手机 | | level | tinyint(4) | 等级 | | sex | tinyint(1) | 性别:0-未知;1-男;2-女 | | birthday | varchar(20) | 生日 | | user_integral | int(11) | 积分 | | user_growth | int(11) | 成长值 | | login_time | datetime | 最后登录时间 | | login_ip | varchar(30) | 最后登录IP | | status | tinyint(4) | 状态:0-禁用 1-启用 | | is_first_login | tinyint(1) | 第一次登录:0-未登录 1-已经登录过 | | creator | int(11) | 创建人 | | updator | int(11) | 更新人 | | create_time | datetime | 创建时间 | | update_time | datetime | 更新时间 | | delete_time | datetime(1) | 删除时间,默认为空 | ### 1.2会员积分明细表 ```mysql CREATE TABLE `ds_user_integral_log` ( `id` int(10) UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '自增ID', `type` tinyint(4) NULL DEFAULT 0 COMMENT '类型:1-签到,2-下单', `user_id` int(11) NULL DEFAULT 0 COMMENT '用户ID', `integral` int(11) NULL DEFAULT 0 COMMENT '积分', `creator` int(11) NULL DEFAULT 0 COMMENT '创建者ID', `updator` int(1) NULL DEFAULT 0 COMMENT '更新者ID', `create_time` datetime(0) NULL DEFAULT NULL COMMENT '创建时间', `update_time` datetime(0) NULL DEFAULT NULL COMMENT '更新时间', `delete_time` datetime(0) NULL DEFAULT NULL COMMENT '删除时间', PRIMARY KEY (`id`) USING BTREE, INDEX `idx_user_id`(`user_id`) USING BTREE ) ENGINE = InnoDB AUTO_INCREMENT = 1 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '用户积分明细表' ROW_FORMAT = Dynamic; ``` | 字段 | 类型 | 备注 | | ----------- | ---------------- | -------------------- | | id | int(10) unsigned | 主键(PRIMARY) | | type | tinyint(4) | 类型:1-签到,2-下单 | | user_id | int(11) | 用户ID | | integral | int(11) | 积分 | | creator | int(11) | 创建者ID | | updator | int(1) | 更新者ID | | create_time | datetime | 创建时间 | | update_time | datetime | 更新时间 | | delete_time | datetime | 删除时间 | ### 1.3会员等级表 ```mysql CREATE TABLE `ds_user_level` ( `id` int UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '自增ID', `level_name` varchar(30) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT '' COMMENT '等级名称', `growth_value` int(11) NULL DEFAULT 0 COMMENT '成长值', `remark` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT '' COMMENT '等级备注', `image` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT '' COMMENT '等级图标', `privilege` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT '' COMMENT '等级权益', `discount` decimal(11, 1) NULL COMMENT '等级折扣', `creator` int(11) NULL DEFAULT 0 COMMENT '创建者', `updator` int(11) NULL DEFAULT 0 COMMENT '更新者', `create_time` datetime NULL COMMENT '创建时间', `update_time` datetime NULL COMMENT '更新时间', `delete_time` datetime NULL COMMENT '删除时间', PRIMARY KEY (`id`) ) COMMENT = '会员等级表'; ``` | 字段 | 类型 | 备注 | | ------------ | ---------------- | --------------- | | id | int(10) unsigned | 主键(PRIMARY) | | level_name | varchar(30) | 等级名称 | | growth_value | int(11) | 成长值 | | remark | varchar(255) | 等级备注 | | image | varchar(255) | 等级图标 | | privilege | varchar(255) | 等级权益 | | discount | decimal(11,1) | 等级折扣 | | creator | int(11) | 创建者 | | updator | int(11) | 更新者 | | create_time | datetime | 创建时间 | | update_time | datetime | 更新时间 | | delete_time | datetime | 删除时间 | ### 1.4管理员表 ```mysql CREATE TABLE `ds_sys_user` ( `id` int(10) UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '自增ID', `root` tinyint(1) NULL DEFAULT 0 COMMENT '是否是超级管理:0-否 1-是', `account` varchar(30) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT '' COMMENT '账号', `password` varchar(80) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT '' COMMENT '密码', `remark` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT '' COMMENT '备注', `roles` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '角色,多个用逗号分开', `login_time` datetime(0) NULL DEFAULT NULL COMMENT '最后登录时间', `login_ip` varchar(30) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT '' COMMENT '最后登录ip', `status` tinyint(1) NULL DEFAULT 1 COMMENT '状态:0-禁用,1-启用', `creator` int(11) NULL DEFAULT 0 COMMENT '创建者', `updator` int(11) NULL DEFAULT NULL COMMENT '更新者', `create_time` datetime(0) NULL DEFAULT NULL COMMENT '创建时间', `update_time` datetime(0) NULL DEFAULT NULL COMMENT '更新时间', `delete_time` datetime(0) NULL DEFAULT NULL COMMENT '删除时间', PRIMARY KEY (`id`) USING BTREE ) ENGINE = InnoDB AUTO_INCREMENT = 1 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '管理员表' ROW_FORMAT = Dynamic; ``` | 字段 | 类型 | 备注 | | ----------- | ---------------- | ------------------------- | | id | int(10) unsigned | 主键(PRIMARY) | | root | tinyint(1) | 是否是超级管理:0-否 1-是 | | account | varchar(30) | 账号 | | password | varchar(80) | 密码 | | remark | varchar(255) | 备注 | | roles | varchar(255) | 角色,多个用逗号分开 | | login_time | datetime | 最后登录时间 | | login_ip | varchar(30) | 最后登录ip | | status | tinyint(1) | 状态:0-禁用,1-启用 | | creator | int(11) | 创建者 | | updator | int(11) | 更新者 | | create_time | datetime | 创建时间 | | update_time | datetime | 更新时间 | | delete_time | datetime | 删除时间 | ### 1.5管理员角色表 ```mysql DROP TABLE IF EXISTS `ds_sys_role`; CREATE TABLE `ds_sys_role` ( `id` int(10) UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '自增ID', `role_name` varchar(30) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT '' COMMENT '角色名称', ` remark` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT '' COMMENT '描述', `auth_ids` text CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL COMMENT '权限', `creator` int(11) NULL DEFAULT 0 COMMENT '创建者', `updator` int(11) NULL DEFAULT 0 COMMENT '更新者', `create_time` datetime(0) NULL DEFAULT NULL COMMENT '创建时间', `update_time` datetime(0) NULL DEFAULT NULL COMMENT '更新时间', `delete_time` datetime(0) NULL DEFAULT NULL COMMENT '删除时间', PRIMARY KEY (`id`) USING BTREE ) ENGINE = InnoDB AUTO_INCREMENT = 1 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '管理员角色表' ROW_FORMAT = Dynamic; ``` | 字段 | 类型 | 备注 | | ----------- | ---------------- | --------------- | | id | int(10) unsigned | 主键(PRIMARY) | | role_name | varchar(30) | 角色名称 | | remark | varchar(255) | 描述 | | auth_ids | text | 权限 | | creator | int(11) | 创建者 | | updator | int(11) | 更新者 | | create_time | datetime | 创建时间 | | update_time | datetime | 更新时间 | | delete_time | datetime | 删除时间 | ### 1.6后台菜单表 ```mysql CREATE TABLE `ds_sys_menu` ( `id` int(10) UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '自增ID', `parent_id` int(11) NULL DEFAULT 0 COMMENT '上一级ID', `type` tinyint(1) NULL DEFAULT 1 COMMENT '菜单类型:1-目录,2-菜单,3-按钮', `name` varchar(30) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT '' COMMENT '菜单名称', `icon` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT '' COMMENT '图标', `route` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT '' COMMENT '前端路由', `component` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT '' COMMENT '组件', `perms` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT '' COMMENT '权限标识', `method` varchar(30) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT '' COMMENT '请求方式', `creator` int(11) NULL DEFAULT 0 COMMENT '创建者', `updator` int(11) NULL DEFAULT 0 COMMENT '更新者', `create_time` datetime(0) NULL DEFAULT NULL COMMENT '创建时间', `update_time` datetime(0) NULL DEFAULT NULL COMMENT '更新时间', `delete_time` datetime(0) NULL DEFAULT NULL COMMENT '删除时间', PRIMARY KEY (`id`) USING BTREE ) ENGINE = InnoDB AUTO_INCREMENT = 1 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '菜单表' ROW_FORMAT = Dynamic; ``` | 字段 | 类型 | 备注 | | ----------- | ---------------- | -------------------------------- | | id | int(10) unsigned | 主键(PRIMARY) | | parent_id | int(11) | 上一级ID | | type | tinyint(1) | 菜单类型:1-目录,2-菜单,3-按钮 | | name | varchar(30) | 菜单名称 | | icon | varchar(255) | 图标 | | route | varchar(50) | 前端路由 | | component | varchar(50) | 组件 | | perms | varchar(50) | 权限标识 | | method | varchar(30) | 请求方式 | | creator | int(11) | 创建者 | | updator | int(11) | 更新者 | | create_time | datetime | 创建时间 | | update_time | datetime | 更新时间 | | delete_time | datetime | 删除时间 |