后台会员的数据来源一般都是用户在网站进行注册而得到的,因此在讲后台管理模块时,会穿插讲一下会员的注册
源码地址:https://gitee.com/myha/demo-shop
下面规划的一些基础功能,前期从简单的开始,后面再来丰富它的功能
功能 | 描述 |
---|---|
会员注册 | 手机号注册、手机验证码 |
会员信息 | 查询、更新、启用/禁用、删除 |
会员等级 | 新增、查询、更新、删除 |
目前用户注册比较主流的都是使用手机号了,因此我们先来实现一下发生手机验证码的功能
手机短信平台有很多,例如百度的、腾讯的、阿里的,无论哪个平台都有相应的sdk
,这里我们使用阿里云的短信服务,上面提供了免费服务供我们学习测试打开如下
地址:https://dysms.console.aliyun.com/quickstart,然后获取相对应的签名和模板
这里边是示例代码,有几个比较重要的参数,accessKeyId
,accessKeySecret
,signName
,templateCode
其中signName
,templateCode
示例代码有
至于accessKeyId
,accessKeySecretd
的获取,从下面入口进入即可
做完这些准备工作后,下面开始写代码了
这里我们学习一种php
的设计模式-策略模式,来封装发送验证码的核心代码,我们先了解一下它的概念
策略模式又叫做政策模式,用于如何组织和调用算法的,是属于行为型模式的一种。 策略模式需要三个角色构成:
Context
封装角色:也叫做上下文角色,起承上启下封装作用,屏蔽高层模块对策略、算法的直接访问,封装可能存在的变化Strategy
抽象策略角色:通常为接口,指定规则ConcreteStrategy
具体策略角色:实现抽象策略中的操作,该类含有具体的算法优点
算法可以通过参数自由切换, 方便扩展,增加策略只需要实现接口就行了 。
第一步:Strategy 抽象策略角色
它就是定义一个基础接口,这样做的目的就是规范代码,不管你用哪个服务商的代码,但都必须实现这个基础接口
在commom
应用里面新建一个lib目录(第三方组件)—>msg
目录(手机发送信息组件)—>Message.php
namespace app\common\lib\msg;
interface Message
{
/**
* 发送验证码
* @param string $mobile 手机号
* @param string $code 验证码
* @return bool
*/
public function send($mobile,$code);
}
这里定义了一个send()
方法,后续接入进来的服务商代码都要继承这个接口,实现send()
方法
**第二步:ConcreteStrategy 具体策略角色 **
以阿里云的服务商为例,我们把代码复制过来
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
,内容如下
// +----------------------------------------------------------------------
// | 阿里云相关配置
// +----------------------------------------------------------------------
return [
//AccessKey ID
'accessKeyId' => '',
// AccessKey Secret
'accessKeySecret' => '',
/***短信配置****/
// 签名
'signName' => '阿里云短信测试',
// 短信模板CODE
'templateCode' => 'SMS_154950909',
// 访问的域名
'dysmsapi' => 'dysmsapi.aliyuncs.com'
];
第三步:Context 封装角色
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()
方法
最后看看调用示例
$msgCtx = new MessageContext(new AliYunMessage);
$msgCtx->sendMessage($mobile,$code);
有些人可能会觉得,如果直接在AliYunMessage
类定义一个静态方法,然后直接调用是不是更加方便?
显然,确实是更加方便。
这里我们使用这种策略模式,其目的就是多学习一下php的设计模式,它本身的代码也不复杂,适合在项目中使用。另外它的一个优点就是调用方并不是直接调用算法,而是通过上下文角色进行调用,这样我们就不需要关注算法本身,如果我们要加策略,直接新增类就行。
注:
把手机验证码发送的核心逻辑封装在
common
应用里面,主要还是因为它是一个公用的功能,任务其它应用如果使用到手机验证码都可以调用它
用户注册肯定是web端应用,因此在获取验证码的之前,我们先建一个pc
目录作为web端应用,在pc
目录下新建如下目录
├─app 应用目录
│ ├─pc 应用目录
│ │ ├─controller 控制器目录
│ │ ├─config 配置目录
│ │ ├─route 路由目录
│ │ ├─service 业务核心目录
│ │ ├─middleware 中间件目录
│ │ └─ ... 更多类库目录
类似于admin
应用,我们也为pc
应用新建一个PcController
的基础控制器,内容跟admin
应用差不多,大家可以下载源码看
接下来在common
应用下的service
目录,新建UserService.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秒
在common/model新建UserModel.php
// +----------------------------------------------------------------------
// | 用户模型
// +----------------------------------------------------------------------
// | Author: myh
// +----------------------------------------------------------------------
namespace app\common\model;
use think\Model;
class UserModel extends BaseModel
{
protected $table = 'ds_user';
}
在pc
应用下的service
目录新建PuserService.php
/**
* 用户注册
* @param array $data 新增的数据
* @return int
*/
public static function register($data){
//验证该账号是否存在
if(UserModel::getByAccount($data['account'])){
serviceException(config('error.er8')['code'],config('error.er8')['msg']);
}
//验证手机号是否存在
if(UserModel::getByMobile($data['mobile'])){
serviceException(config('error.er9')['code'],config('error.er9')['msg']);
}
$data['password'] = createPassword($data['password']);
$user = new UserModel;
$user->save($data);
cache(config('cachekey.mobile_code').$data['mobile'],null);
return $user->id;
}
首先判断账号和手机号是否被注册,然后对密码进行加密存储,最后新增到数表中,并删除验证码缓存。
这里的密码加密函数,是自定义在app
目录下的common.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
代码
//用户注册
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();
}
}
没有啥逻辑可言,就是验证数据
最后就是路由
//用户注册
Route::post('user/register','user/register');
这一章回到我们的admin
应用,管理员可以查看会员的信息,包括对其进行禁用/启用,首先我们实现它的列表接口
打开UserService.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
模型定义了如下的静态数组
public static $status = [0=>'禁用',1=>'正常'];
控制器代码更简单了,在admin/controller
应用下新建User.php
//分页列表
public function page(){
return $this->success(UserService::page($this->request->get()));
}
注:
只要前期做好了代码的封装,后续开发会非常的方便,包括后面要做的会员等级列表也是一样
可能因为某些原因,我们需要对违规的会员账号进行禁用操作,目前实现这一功能主要是更改会员表的status
字段即可
打开common/service/UserService.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
代码
//启用、禁用
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();
}
}
会员等级的管理并没有什么复杂的逻辑,只是简单的对数据的增删改查,后面如果遇到跟它类似的模块都按照这个模块的方式去开发
新建common/model/UserLevelModel.php
// +----------------------------------------------------------------------
// | 用户等级模型
// +----------------------------------------------------------------------
// | Author: myh
// +----------------------------------------------------------------------
namespace app\common\model;
class UserLevelModel extends BaseModel
{
protected $table = 'ds_user_level';
}
新建common/model/UserLevelService.php
/**
* 新增等级数据
* @param array $data 新增的数据
* @return int
*/
public static function save($data){
$userLevel = new UserLevelModel;
$userLevel->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
代码
// +----------------------------------------------------------------------
// | 用户等级模块
// +----------------------------------------------------------------------
// | Author: myh
// +----------------------------------------------------------------------
namespace app\admin\controller;
use app\common\service\UserLevelService;
class UserLevel extends AdminController
{
//列表
public function list(){
$param = $this->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();
}
}
之前我们查询用户列表的时候,返回的用户等级level字段是一个整型,它存储的是ds_user_level
这个表的自增ID,因此我们需要在查询用户列表的时候把level_name给查出来。
打开UserLevelService.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)
方法,在循环体里面新增如下一行代码即可
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缓存中。
相信大家也知道为啥我不这样实现这个方法
public static function getLevelNameById($id){
$level = UserLevelModel::find($id);
return $level->level_name;
}
注:
上面这种实现逻辑主要是针对表数据比较少的情况,如果说用户等级的数据也几十万条,甚至更多,那这里就不能这样实现,因为你不太可能一次性查出所有数据
现在回到注册那一块,注册的时候我们需要给一个默认的会员等级,一般都是最普通的等级。
打开pc/service/PuserService.php
,找到之前写的register($data)
方法,新增如下代码
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()
获取会员等级列表,这里的会员等级列表获取是根据成长值
字段排序输出的,排在第一个的肯定是最开始的一个等级。
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) | 删除时间,默认为空 |
字段 | 类型 | 备注 |
---|---|---|
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 | 删除时间 |
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 | 删除时间 |