我思来想去,最后还是觉得先封装一下thinkphp6整体报错机制。要不然后期不好拓展。
一、封装报错机制
1、修改thinkphp6报错文件路径
在根目录下app文件夹下的provider.php文件中,注释thinkphp6自带报错文件类,自定义报错文件配置,代码如下:
<?php
use app\ExceptionHandle;
use app\Request;
// 容器Provider定义文件
return [
'think\Request' => Request::class,
//'think\exception\Handle' => ExceptionHandle::class,
'think\exception\Handle' => '\\Error\\ExecptionHandle'
];
2、在根目录下extend文件夹中创建Error文件夹,然后创建ExecptionHandle.php报错文件类,代码如下:
<?php
/**
* json报错机制
* User: 龙哥·三年风水
* Date: 2024/10/29
* Time: 14:21
*/
namespace Error;
use think\exception\Handle;
use Throwable;
use think\facade\Env;
use think\Response;
class ExecptionHandle extends Handle
{
private $msg = "未知错误";
private $httpCode = 500;
private $errorCode = 50000;
public function render($request, Throwable $e): Response
{
//判断在`.env`里面是否开始了调试,开启了调试就原样返回,关闭了调试就返回自定义的json格式的错误信息
//因我开启了强制路由,所以不管调试模式是否开启,都不需要检测是否开启路由模式
/*if(Env::get("APP_DEBUG") == true){
return parent::render($request,$e);
}*/
$this->msg = $e->getMessage() ?: $this->msg;
//HttpExceptions是继承同级目录下HttpExceptions,代码在下方
if ($e instanceof BaseError) {
$this->httpCode = $e->getStatusCode() ?: $this->httpCode;
}
$this->errorCode = $e->getCode() ?: $this->errorCode;
$result['code'] = $this->errorCode;
$result['message'] = $this->msg;
$result['time'] = time();
return json($result, $this->httpCode);
// 其他错误交给系统处理
return parent::render($request, $e);
}
}
3、在同级目录下接着创建对外报错暴露接口BaseError.php类,代码如下:
<?php
/**
* 总报错机制
* User: 龙哥·三年风水
* Date: 2024/10/29
* Time: 14:56
*/
namespace Error;
use Exception;
class BaseError extends \RuntimeException
{
private $statusCode;
private $headers;
//主要是重构$code前提,$previous和 $headers在后面方便调用
public function __construct(string $message = '',int $code = 50000, int $statusCode = 404, Exception $previous = null, array $headers = [])
{
$this->statusCode = $statusCode;
$this->headers = $headers;
parent::__construct($message, $code, $previous);
}
public function getStatusCode()
{
return $this->statusCode;
}
public function getHeaders()
{
return $this->headers;
}
}
二、总控开发
1、在跟目录下的app文件夹下的controller文件夹中,创建Base.php文件类,代码如下:
<?php
/**
* 总控制
* User: 龙哥 三年风水
* Date: 2024/10/27/0027
* Time: 22:16
*/
namespace app\controller;
use app\BaseController;
use Encipher\Encrypt;
use app\model\common\Token;
use Error\BaseError;
class Base extends BaseController
{
protected $userId = 0;//用户编号,整个系统内部使用
protected $avatar = '';//登录用户的头像
protected $username = '';//登录用户的登录名称
protected $realname = '';//登录用户的真实名称
protected $email = '';//登录用户邮箱
protected $ip = '';//登录用户授权组
protected $departmentId = 0;//登录用户部门编号
protected $gradeId = 0;//登录用户级别编号
protected $isUnserialize = true;//Redis里面有按钮权限
protected $token = '';//当前用户唯一编码
protected $roleId = '';//登录用户的权限编号
protected $key = '';//登录用户权限组标识
protected $butts = [];//操作权限,整个系统内部使用
protected $rules = [];//登录用户的权限组,内部使用
//初始化方法
public function initialize(){
parent::initialize(); // TODO: Change the autogenerated stub
$this->isLoginAuth();//判断是否登录
}
//token验证
private function isLoginAuth(){
$headInfo = $this->request->header();
if(!isset($headInfo['authorization']))throw new BaseError('非法操作!');
if(empty($headInfo['authorization']))throw new BaseError('操作异常!');
$tokenValue = explode('|', Encrypt::encryptRsa($headInfo['authorization']));//解密 --转换token
if(count($tokenValue) != 2)throw new BaseError('登录认证权限错误',50034,200);
$token = sha1(sha1($tokenValue[0]).strtotime($tokenValue[1]));
$res = Token::setToken($token, $headInfo['authorization'], $headInfo['x-real-ip']);
if($res['status'] === false)throw new BaseError($res['info'],50034,200);//赋值错误信息
$this->userId = $res['data']['admin_id'];
$this->avatar = $res['data']['avatar'];
$this->username = $res['data']['username'];
$this->realname = $res['data']['realname'];
$this->email = $res['data']['email'];
$this->ip = $res['data']['ip'];
$this->departmentId = $res['data']['department_id'];
$this->gradeId = $res['data']['grade_id'];
$this->roleId = $res['data']['role_id'];
$this->token = $token;
}
}
三、登录退出开发
1、在根目录下route文件夹中的app.php中添加路由地址。代码如下:
Route::get('login/get_code', 'common.Login/getCode');
Route::post('login/do_login', 'common.Login/doLogin');
Route::post('login/logout', 'common.Login/logout');
2、在根目录下app文件夹下controller文件夹中创建common文件夹,然后在common文件夹中创建Login.php控制文件,代码如下:
a、图片验证码获取
//验证码展示
public function getCode(){
$re = app()->make(\Other\Captcha::class)->create();
$base64 = 'data:image/png;base64,' . base64_encode($re->getData());
return succ($base64);
}
b、登录
//登录
public function doLogin(){
$param = $this->request->param();
$validate = new \app\validate\common\Login;
$result = $validate->check($param);//参数验证
if (!$result) return err($validate->getError());
//验证码的正确性
if (!app()->make(\Other\Captcha::class)->check($param['captcha_code'])) return err('验证码错误');
if (time() + 5 < strtotime($param['login_time'])) return err('非法登录');
$resData = Admin::dataFind(['email' => trim($param['username'])], 'id,password,ip,status', true);
//用户信息的正确性
if (empty($resData) && empty($resData['id'])) return err('用户不存在');
if ($resData['status'] !== 1) return err('该账号已被禁用');
$systemParam = SystemModel::dataFind(['id' => 1],'security_password,platform_token_expira');
if ($resData['password'] !== sha1($param['password'] . $systemParam['security_password'])) return err('账号对应的密码错误');
if(!empty($resData['ip'])){
if($resData['ip'] != $this->request->header('x-real-ip'))return err('禁止访问,不在IP白名单中');
}
//写入Token日志
$data['token_type'] = 1;
$data['menu_name'] = 'CommonLoginDoLogin';
$data['admin_id'] = $resData['id'];
$data['random_number'] = alnum();
$data['create_time'] = date('Y-m-d',strtotime($param['login_time']));
$data['login_time'] = $param['login_time'];
$data['expire_time'] = strtotime($param['login_time']) + $systemParam['platform_token_expira'];
$token = $data['admin_id'] . $data['random_number'];
$data['token'] = sha1(sha1($token) . strtotime($data['login_time']));
TokenModel::save($data,[]);
//加入跨站攻击验证队列
Redis::select(config('cache.stores.redis.token_db'))->setex('token_' . $data['token'],$systemParam['platform_token_expira'],$resData['id']);
Redis::select(config('cache.stores.redis.token_db'))->setex('token_' . $resData['id'],$systemParam['platform_token_expira'],Encrypt::encryptRsa($data['token']));
return succ('登录成功',Encrypt::encryptRsa($token));
}
c、退出
//退出
public function logout(){
$info = $this->request->header();
if(!isset($info['authorization']) || empty($info['authorization'])) return err('参数错误');
$tokenn = Encrypt::decryptRsa($info['authorization']);
$token = explode('|', $tokenn);
$token = sha1(sha1($token[0]) . strtotime($token[1]));
$redisToken = Redis::select(config('cache.stores.redis.token_db'))->get('token_' . $token);
if(!empty($redisToken)){
TokenModel::save(['expire_time' => time()], ['token' => $token]);
Redis::select(config('cache.stores.redis.token_db'))->del('token_' . $redisToken);
Redis::select(config('cache.stores.redis.token_db'))->del('token_' . $token);
}
return succ('退出成功');
}
3、验证码封装
传统的验证码是采用session存储的哈,我这里封装成Redis存储的咯。
a、安装官方提供的依赖
composer require topthink/think-captcha
b、在根目录下extend文件夹中创建Other文件夹,然后创建Captcha.php控制文件,代码如下:
<?php
/**
* 自定义验证码
* User: 龙哥·三年风水
* Date: 2024/10/29
* Time: 15:56
*/
namespace Other;
use Redis\Redis;
use think\Config;
use think\Response;
class Captcha
{
private $im = null; // 验证码图片实例
private $color = null; // 验证码字体颜色
// 配置文件定义
private $config = null;
// 验证码字符集合
protected $codeSet = '234567890abcdefhijkmnpqrstuvwxyzABCDEFGHJKLMNPQRTUVWXY';
// 验证码过期时间(s)
protected $expire = 1800;
// 使用中文验证码
protected $useZh = false;
// 中文验证码字符串
protected $zhSet = '们以我到他会作时要动国产的一是工就年阶义发成部民可出能方进在了不和有大这主中人上为来分生对于学下级地个用同行面说种过命度革而多子后自社加小机也经力线本电高量长党得实家定深法表着水理化争现所二起政三好十战无农使性前等反体合斗路图把结第里正新开论之物从当两些还天资事队批点育重其思与间内去因件日利相由压员气业代全组数果期导平各基或月毛然如应形想制心样干都向变关问比展那它最及外没看治提五解系林者米群头意只明四道马认次文通但条较克又公孔领军流入接席位情运器并飞原油放立题质指建区验活众很教决特此常石强极土少已根共直团统式转别造切九你取西持总料连任志观调七么山程百报更见必真保热委手改管处己将修支识病象几先老光专什六型具示复安带每东增则完风回南广劳轮科北打积车计给节做务被整联步类集号列温装即毫知轴研单色坚据速防史拉世设达尔场织历花受求传口断况采精金界品判参层止边清至万确究书术状厂须离再目海交权且儿青才证低越际八试规斯近注办布门铁需走议县兵固除般引齿千胜细影济白格效置推空配刀叶率述今选养德话查差半敌始片施响收华觉备名红续均药标记难存测士身紧液派准斤角降维板许破述技消底床田势端感往神便贺村构照容非搞亚磨族火段算适讲按值美态黄易彪服早班麦削信排台声该击素张密害侯草何树肥继右属市严径螺检左页抗苏显苦英快称坏移约巴材省黑武培著河帝仅针怎植京助升王眼她抓含苗副杂普谈围食射源例致酸旧却充足短划剂宣环落首尺波承粉践府鱼随考刻靠够满夫失包住促枝局菌杆周护岩师举曲春元超负砂封换太模贫减阳扬江析亩木言球朝医校古呢稻宋听唯输滑站另卫字鼓刚写刘微略范供阿块某功套友限项余倒卷创律雨让骨远帮初皮播优占死毒圈伟季训控激找叫云互跟裂粮粒母练塞钢顶策双留误础吸阻故寸盾晚丝女散焊功株亲院冷彻弹错散商视艺灭版烈零室轻血倍缺厘泵察绝富城冲喷壤简否柱李望盘磁雄似困巩益洲脱投送奴侧润盖挥距触星松送获兴独官混纪依未突架宽冬章湿偏纹吃执阀矿寨责熟稳夺硬价努翻奇甲预职评读背协损棉侵灰虽矛厚罗泥辟告卵箱掌氧恩爱停曾溶营终纲孟钱待尽俄缩沙退陈讨奋械载胞幼哪剥迫旋征槽倒握担仍呀鲜吧卡粗介钻逐弱脚怕盐末阴丰雾冠丙街莱贝辐肠付吉渗瑞惊顿挤秒悬姆烂森糖圣凹陶词迟蚕亿矩康遵牧遭幅园腔订香肉弟屋敏恢忘编印蜂急拿扩伤飞露核缘游振操央伍域甚迅辉异序免纸夜乡久隶缸夹念兰映沟乙吗儒杀汽磷艰晶插埃燃欢铁补咱芽永瓦倾阵碳演威附牙芽永瓦斜灌欧献顺猪洋腐请透司危括脉宜笑若尾束壮暴企菜穗楚汉愈绿拖牛份染既秋遍锻玉夏疗尖殖井费州访吹荣铜沿替滚客召旱悟刺脑措贯藏敢令隙炉壳硫煤迎铸粘探临薄旬善福纵择礼愿伏残雷延烟句纯渐耕跑泽慢栽鲁赤繁境潮横掉锥希池败船假亮谓托伙哲怀割摆贡呈劲财仪沉炼麻罪祖息车穿货销齐鼠抽画饲龙库守筑房歌寒喜哥洗蚀废纳腹乎录镜妇恶脂庄擦险赞钟摇典柄辩竹谷卖乱虚桥奥伯赶垂途额壁网截野遗静谋弄挂课镇妄盛耐援扎虑键归符庆聚绕摩忙舞遇索顾胶羊湖钉仁音迹碎伸灯避泛亡答勇频皇柳哈揭甘诺概宪浓岛袭谁洪谢炮浇斑讯懂灵蛋闭孩释乳巨徒私银伊景坦累匀霉杜乐勒隔弯绩招绍胡呼痛峰零柴簧午跳居尚丁秦稍追梁折耗碱殊岗挖氏刃剧堆赫荷胸衡勤膜篇登驻案刊秧缓凸役剪川雪链渔啦脸户洛孢勃盟买杨宗焦赛旗滤硅炭股坐蒸凝竟陷枪黎救冒暗洞犯筒您宋弧爆谬涂味津臂障褐陆啊健尊豆拔莫抵桑坡缝警挑污冰柬嘴啥饭塑寄赵喊垫丹渡耳刨虎笔稀昆浪萨茶滴浅拥穴覆伦娘吨浸袖珠雌妈紫戏塔锤震岁貌洁剖牢锋疑霸闪埔猛诉刷狠忽灾闹乔唐漏闻沈熔氯荒茎男凡抢像浆旁玻亦忠唱蒙予纷捕锁尤乘乌智淡允叛畜俘摸锈扫毕璃宝芯爷鉴秘净蒋钙肩腾枯抛轨堂拌爸循诱祝励肯酒绳穷塘燥泡袋朗喂铝软渠颗惯贸粪综墙趋彼届墨碍启逆卸航衣孙龄岭骗休借';
// 使用背景图片
protected $useImgBg = false;
// 验证码字体大小(px)
protected $fontSize = 25;
// 是否画混淆曲线
protected $useCurve = true;
// 是否添加杂点
protected $useNoise = true;
// 验证码图片高度
protected $imageH = 0;
// 验证码图片宽度
protected $imageW = 0;
// 验证码位数
protected $length = 5;
// 验证码字体,不设置随机获取
protected $fontttf = '';
// 背景颜色
protected $bg = [243, 251, 254];
//算术验证码
protected $math = false;
/**
* 架构方法 设置参数
* @ access public
* @ param Config $config
* @ param Session $session
*/
public function __construct(Config $config)
{
$this->config = $config;
}
/**
* 配置验证码
* @ param string|null $config
*/
protected function configure(string $config = null): void
{
if (is_null($config)) {
$config = $this->config->get('captcha', []);
} else {
$config = $this->config->get('captcha.' . $config, []);
}
foreach ($config as $key => $val) {
if (property_exists($this, $key)) {
$this->{$key} = $val;
}
}
}
/**
* 创建验证码
* @ return array
* @ throws Exception
*/
protected function generate(): array
{
$bag = '';
if ($this->math) {
$this->useZh = false;
$this->length = 5;
$x = random_int(10, 30);
$y = random_int(1, 9);
$bag = "{$x} + {$y} = ";
$key = $x + $y;
$key .= '';
} else {
if ($this->useZh) {
$characters = preg_split('/(?<!^)(?!$)/u', $this->zhSet);
} else {
$characters = str_split($this->codeSet);
}
for ($i = 0; $i < $this->length; $i++) {
$bag .= $characters[rand(0, count($characters) - 1)];
}
$key = mb_strtolower($bag, 'UTF-8');
}
$hash = password_hash($key, PASSWORD_BCRYPT, ['cost' => 10]);
$generator = [
'value' => $bag,
'key' => $hash,
];
Redis::select(config('cache.stores.redis.other_db'))->setex('captcha_'.$key,$this->expire,serialize($generator));
return $generator;
}
/**
* 验证验证码是否正确
* @ access public
* @ param string $code 用户验证码
* @ return bool 用户验证码是否正确
*/
public function check(string $code): bool
{
$code = mb_strtolower(trim($code), 'UTF-8');
$generator = unserialize(Redis::select(config('cache.stores.redis.other_db'))->get('captcha_'.$code));
if($generator === false)return false;
$key = $generator['key'] ?? '';
$res = password_verify($code, $key);
if($res)Redis::select(config('cache.stores.redis.other_db'))->del('captcha_'.$code);
return $res;
}
/**
* 输出验证码并把验证码的值保存的session中
* @ access public
* @ param null|string $config
* @ param bool $api
* @ return Response
*/
public function create(string $config = null, bool $api = false): Response
{
$this->configure($config);
$generator = $this->generate();
// 图片宽(px)
$this->imageW || $this->imageW = $this->length * $this->fontSize * 1.5 + $this->length * $this->fontSize / 2;
// 图片高(px)
$this->imageH || $this->imageH = $this->fontSize * 2.5;
// 建立一幅 $this->imageW x $this->imageH 的图像
$this->im = imagecreate($this->imageW, $this->imageH);
// 设置背景
imagecolorallocate($this->im, $this->bg[0], $this->bg[1], $this->bg[2]);
// 验证码字体随机颜色
$this->color = imagecolorallocate($this->im, mt_rand(1, 150), mt_rand(1, 150), mt_rand(1, 150));
// 验证码使用随机字体
$ttfPath = dirname(dirname(app()->getThinkPath())) . '/think-captcha' . '/assets/' . ($this->useZh ? 'zhttfs' : 'ttfs') . '/';
if (empty($this->fontttf)) {
$dir = dir($ttfPath);
$ttfs = [];
while (false !== ($file = $dir->read())) {
if ('.' != $file[0] && substr($file, -4) == '.ttf') {
$ttfs[] = $file;
}
}
$dir->close();
$this->fontttf = $ttfs[array_rand($ttfs)];
}
$fontttf = $ttfPath . $this->fontttf;
if ($this->useImgBg) {
$this->background();
}
if ($this->useNoise) {
// 绘杂点
$this->writeNoise();
}
if ($this->useCurve) {
// 绘干扰线
$this->writeCurve();
}
// 绘验证码
$text = $this->useZh ? preg_split('/(?<!^)(?!$)/u', $generator['value']) : str_split($generator['value']); // 验证码
foreach ($text as $index => $char) {
$x = $this->fontSize * ($index + 1) * mt_rand(1.2, 1.6) * ($this->math ? 1 : 1.5);
$y = $this->fontSize + mt_rand(10, 20);
$angle = $this->math ? 0 : mt_rand(-40, 40);
imagettftext($this->im, $this->fontSize, $angle, $x, $y, $this->color, $fontttf, $char);
}
ob_start();
// 输出图像
imagepng($this->im);
$content = ob_get_clean();
imagedestroy($this->im);
return response($content, 200, ['Content-Length' => strlen($content)])->contentType('image/png');
}
/**
* 画一条由两条连在一起构成的随机正弦函数曲线作干扰线(你可以改成更帅的曲线函数)
*
* 高中的数学公式咋都忘了涅,写出来
* 正弦型函数解析式:y=Asin(ωx+φ)+b
* 各常数值对函数图像的影响:
* A:决定峰值(即纵向拉伸压缩的倍数)
* b:表示波形在Y轴的位置关系或纵向移动距离(上加下减)
* φ:决定波形与X轴位置关系或横向移动距离(左加右减)
* ω:决定周期(最小正周期T=2π/∣ω∣)
*
*/
protected function writeCurve(): void
{
$px = $py = 0;
// 曲线前部分
$A = mt_rand(1, $this->imageH / 2); // 振幅
$b = mt_rand(-$this->imageH / 4, $this->imageH / 4); // Y轴方向偏移量
$f = mt_rand(-$this->imageH / 4, $this->imageH / 4); // X轴方向偏移量
$T = mt_rand($this->imageH, $this->imageW * 2); // 周期
$w = (2 * M_PI) / $T;
$px1 = 0; // 曲线横坐标起始位置
$px2 = mt_rand($this->imageW / 2, $this->imageW * 0.8); // 曲线横坐标结束位置
for ($px = $px1; $px <= $px2; $px = $px + 1) {
if (0 != $w) {
$py = $A * sin($w * $px + $f) + $b + $this->imageH / 2; // y = Asin(ωx+φ) + b
$i = (int)($this->fontSize / 5);
while ($i > 0) {
imagesetpixel($this->im, $px + $i, $py + $i, $this->color); // 这里(while)循环画像素点比imagettftext和imagestring用字体大小一次画出(不用这while循环)性能要好很多
$i--;
}
}
}
// 曲线后部分
$A = mt_rand(1, $this->imageH / 2); // 振幅
$f = mt_rand(-$this->imageH / 4, $this->imageH / 4); // X轴方向偏移量
$T = mt_rand($this->imageH, $this->imageW * 2); // 周期
$w = (2 * M_PI) / $T;
$b = $py - $A * sin($w * $px + $f) - $this->imageH / 2;
$px1 = $px2;
$px2 = $this->imageW;
for ($px = $px1; $px <= $px2; $px = $px + 1) {
if (0 != $w) {
$py = $A * sin($w * $px + $f) + $b + $this->imageH / 2; // y = Asin(ωx+φ) + b
$i = (int)($this->fontSize / 5);
while ($i > 0) {
imagesetpixel($this->im, $px + $i, $py + $i, $this->color);
$i--;
}
}
}
}
/**
* 画杂点
* 往图片上写不同颜色的字母或数字
*/
protected function writeNoise(): void
{
$codeSet = '2345678abcdefhijkmnpqrstuvwxyz';
for ($i = 0; $i < 10; $i++) {
//杂点颜色
$noiseColor = imagecolorallocate($this->im, mt_rand(150, 225), mt_rand(150, 225), mt_rand(150, 225));
for ($j = 0; $j < 5; $j++) {
// 绘杂点
imagestring($this->im, 5, mt_rand(-10, $this->imageW), mt_rand(-10, $this->imageH), $codeSet[mt_rand(0, 29)], $noiseColor);
}
}
}
/**
* 绘制背景图片
* 注:如果验证码输出图片比较大,将占用比较多的系统资源
*/
protected function background(): void
{
$path = dirname(dirname(app()->getThinkPath())) . DS . 'think-captcha' . DS . '/assets/bgs/';
$dir = dir($path);
$bgs = [];
while (false !== ($file = $dir->read())) {
if ('.' != $file[0] && substr($file, -4) == '.jpg') {
$bgs[] = $path . $file;
}
}
$dir->close();
$gb = $bgs[array_rand($bgs)];
list($width, $height) = @getimagesize($gb);
// Resample
$bgImage = @imagecreatefromjpeg($gb);
@imagecopyresampled($this->im, $bgImage, 0, 0, 0, 0, $this->imageW, $this->imageH, $width, $height);
@imagedestroy($bgImage);
}
}
c、修改根目录下config文件夹中的captcha.php文件,代码如下:
<?php
// +----------------------------------------------------------------------
// | Captcha配置文件
// +----------------------------------------------------------------------
return [
//验证码位数
'length' => 4,
// 验证码字符集合
'codeSet' => '23456789',
// 验证码过期时间
'expire' => 1800,
// 是否使用中文验证码
'useZh' => false,
// 是否使用算术验证码
'math' => false,
// 是否使用背景图
'useImgBg' => false,
//验证码字符大小
'fontSize' => 16,
// 是否使用混淆曲线
'useCurve' => true,
//是否添加杂点
'useNoise' => true,
// 验证码字体 不设置则随机
'fontttf' => '',
//背景颜色
'bg' => [243, 251, 254],
// 验证码图片高度
'imageH' => 40,
// 验证码图片宽度
'imageW' => 120,
// 验证码图片透明度
'alpha' => 0,
// 是否采用API模式生成
'api' => false,
// 添加额外的验证码设置
// verify => [
// 'length'=>4,
// ...
//],
];
四、修改数据操作Tools工具里面的错误返回
详细代码请查看前一章节文章
《群控系统服务端开发模式-应用开发-业务架构逻辑开发数据操作Tools工具封装》
五、提前说明
到此BaseAPI开发第一阶段已经结束,明天将对api业务逻辑总结及测试。