存客宝应用接口初始化
This commit is contained in:
122
application/common/Fork.php
Normal file
122
application/common/Fork.php
Normal file
@@ -0,0 +1,122 @@
|
||||
<?php
|
||||
|
||||
namespace app\common;
|
||||
|
||||
class Fork {
|
||||
|
||||
/**
|
||||
* 执行多条任务并等待所有任务执行完成后结束
|
||||
*
|
||||
* @param array $taskFuncs
|
||||
*/
|
||||
static public function wait(array $taskFuncs) {
|
||||
foreach ($taskFuncs as $i => $taskFunc) {
|
||||
$pid = pcntl_fork();
|
||||
if ($pid == -1) {
|
||||
exit('Could not fork.');
|
||||
} elseif ($pid == 0) {
|
||||
call_user_func_array($taskFunc, [$i]);
|
||||
exit(0);
|
||||
}
|
||||
}
|
||||
foreach ($taskFuncs as $taskFunc) {
|
||||
pcntl_wait($status);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 并发执行多条任务
|
||||
*
|
||||
* @param array $taskFuncs
|
||||
*/
|
||||
static public function invoke(array $taskFuncs) {
|
||||
$taskRuns = [];
|
||||
$signal = FALSE;
|
||||
if (function_exists('pcntl_async_signals')) {
|
||||
pcntl_async_signals(TRUE);
|
||||
}
|
||||
while (TRUE) {
|
||||
foreach ($taskFuncs as $i => $taskFunc) {
|
||||
if (!in_array($i, $taskRuns)) {
|
||||
$pid = pcntl_fork();
|
||||
if ($pid == -1) {
|
||||
exit('Could not fork.');
|
||||
} elseif ($pid > 0) {
|
||||
$taskRuns[$pid] = $i;
|
||||
if (!$signal) {
|
||||
$signal = TRUE;
|
||||
static::signalHandler();
|
||||
}
|
||||
} else {
|
||||
call_user_func_array($taskFunc, [$i]);
|
||||
exit(0);
|
||||
}
|
||||
}
|
||||
}
|
||||
if ($pid = pcntl_wait($status, WNOHANG)) {
|
||||
unset($taskRuns[$pid]);
|
||||
} else {
|
||||
sleep(1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 按指定数量并发执行相同任务
|
||||
*
|
||||
* @param $count
|
||||
* @param $taskFunc
|
||||
* @param int $destroy
|
||||
*/
|
||||
static public function execute($count, $taskFunc, $destroy = 100) {
|
||||
$runSum = 0;
|
||||
$runNum = 0;
|
||||
$signal = FALSE;
|
||||
if (function_exists('pcntl_async_signals')) {
|
||||
pcntl_async_signals(TRUE);
|
||||
}
|
||||
while (TRUE) {
|
||||
if ($runNum >= $count) {
|
||||
if (pcntl_wait($status, WNOHANG) > 0) {
|
||||
$runNum --;
|
||||
} else {
|
||||
sleep(1);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
$pid = pcntl_fork();
|
||||
if ($pid == -1) {
|
||||
exit('Could not fork.');
|
||||
} elseif ($pid > 0) {
|
||||
$runSum ++;
|
||||
$runNum ++;
|
||||
if (!$signal) {
|
||||
$signal = TRUE;
|
||||
static::signalHandler();
|
||||
}
|
||||
} else {
|
||||
for ($i = 0; $i < $destroy; $i ++) {
|
||||
call_user_func_array($taskFunc, [$runSum % $count]);
|
||||
}
|
||||
|
||||
exit;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置信息处理器
|
||||
*
|
||||
*/
|
||||
static public function signalHandler() {
|
||||
$handler = function () {
|
||||
posix_kill(0, SIGKILL);
|
||||
exit(0);
|
||||
};
|
||||
pcntl_signal(SIGINT, $handler);
|
||||
pcntl_signal(SIGTERM, $handler);
|
||||
pcntl_signal(SIGUSR1, $handler);
|
||||
pcntl_signal(SIGUSR2, $handler);
|
||||
}
|
||||
}
|
||||
90
application/common/Logger.php
Normal file
90
application/common/Logger.php
Normal file
@@ -0,0 +1,90 @@
|
||||
<?php
|
||||
|
||||
namespace app\common;
|
||||
|
||||
class Logger {
|
||||
|
||||
const INFO = 'INFO';
|
||||
const WARN = 'WARN';
|
||||
const ERROR = 'ERROR';
|
||||
|
||||
static protected $loggers = [];
|
||||
|
||||
/**
|
||||
* 获取日志对象
|
||||
*
|
||||
* @param $name
|
||||
* @return Logger
|
||||
*/
|
||||
static public function _($name) {
|
||||
$lower = strtolower($name);
|
||||
if (!isset(static::$loggers[$lower])) {
|
||||
static::$loggers[$lower] = new Logger($name);
|
||||
}
|
||||
return static::$loggers[$lower];
|
||||
}
|
||||
|
||||
protected $name;
|
||||
protected $print = TRUE;
|
||||
|
||||
/**
|
||||
* Logger constructor.
|
||||
*
|
||||
* @param $name
|
||||
*/
|
||||
protected function __construct($name) {
|
||||
$this->name = $name;
|
||||
}
|
||||
|
||||
/**
|
||||
* 写入信息
|
||||
*
|
||||
* @param $message
|
||||
* @param array $params
|
||||
*/
|
||||
public function info($message, array $params = []) {
|
||||
$this->write(static::INFO, $message, $params);
|
||||
}
|
||||
|
||||
/**
|
||||
* 写入警告
|
||||
*
|
||||
* @param $message
|
||||
* @param array $params
|
||||
*/
|
||||
public function warn($message, array $params = []) {
|
||||
$this->write(static::WARN, $message, $params);
|
||||
}
|
||||
|
||||
/**
|
||||
* 写入错误
|
||||
*
|
||||
* @param $message
|
||||
* @param array $params
|
||||
*/
|
||||
public function error($message, array $params = []) {
|
||||
$this->write(static::ERROR, $message, $params);
|
||||
}
|
||||
|
||||
/**
|
||||
* 写入日志
|
||||
*
|
||||
* @param $type
|
||||
* @param $message
|
||||
* @param array $params
|
||||
*/
|
||||
public function write($type, $message, array $params = []) {
|
||||
foreach ($params as $key => $value) {
|
||||
$message = str_replace('{' . $key . '}', $value, $message);
|
||||
}
|
||||
|
||||
$data = '[' . date('Y-m-d H:i:s') . '][' . $type . '] ' . $message . PHP_EOL;
|
||||
$file = ROOT_PATH . DS . 'runtime' . DS . $this->name . '_' . date('Ymd') . '.txt';
|
||||
|
||||
file_put_contents($file, $data, FILE_APPEND);
|
||||
|
||||
if ($this->print) {
|
||||
echo '[' . date('Y-m-d H:i:s') . '][' . $this->name . '][' . $type . '] ' . $message . PHP_EOL;;
|
||||
}
|
||||
}
|
||||
}
|
||||
BIN
application/common/Server.rar
Normal file
BIN
application/common/Server.rar
Normal file
Binary file not shown.
93
application/common/TaskServer.php
Normal file
93
application/common/TaskServer.php
Normal file
@@ -0,0 +1,93 @@
|
||||
<?php
|
||||
|
||||
namespace app\common;
|
||||
|
||||
use think\Db;
|
||||
use think\facade\Log;
|
||||
use Workerman\Lib\Timer;
|
||||
use think\worker\Server;
|
||||
use WeChatDeviceApi\Adapters\ChuKeBao\Adapter as ChuKeBaoAdapter;
|
||||
|
||||
class TaskServer extends Server
|
||||
{
|
||||
|
||||
const PROCESS_COUNT = 5;
|
||||
|
||||
protected $socket = 'text://0.0.0.0:2980';
|
||||
|
||||
protected $option = [
|
||||
'count' => self::PROCESS_COUNT,
|
||||
'name' => 'ckb_task_server'
|
||||
];
|
||||
|
||||
/**
|
||||
* 当客户端的连接上发生错误时触发
|
||||
* @param $connection
|
||||
* @param $code
|
||||
* @param $msg
|
||||
*/
|
||||
public function onError($connection, $code, $msg)
|
||||
{
|
||||
Log::record("error $code $msg");
|
||||
}
|
||||
|
||||
public function onMessage($connection, $data)
|
||||
{
|
||||
}
|
||||
|
||||
public function onClose($connection)
|
||||
{
|
||||
}
|
||||
|
||||
public function onConnect($connection)
|
||||
{
|
||||
}
|
||||
|
||||
public function onWorkerStart($worker)
|
||||
{
|
||||
|
||||
$current_worker_id = $worker->id;
|
||||
|
||||
$process_count_for_status_0 = self::PROCESS_COUNT - 1;
|
||||
|
||||
$adapter = new ChuKeBaoAdapter();
|
||||
|
||||
|
||||
Log::info('Workerman进程:' . $current_worker_id);
|
||||
|
||||
|
||||
// 在一个进程里处理获客任务新是数据
|
||||
if ($current_worker_id == 4) {
|
||||
Timer::add(60, function () use ($adapter) {
|
||||
$adapter->handleCustomerTaskNewUser();
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
// 在一个进程里处理获客任务添加后的相关逻辑
|
||||
if ($current_worker_id == 3) {
|
||||
Timer::add(60, function () use ($adapter) {
|
||||
$adapter->handleCustomerTaskWithStatusIsCreated();
|
||||
});
|
||||
}
|
||||
|
||||
// 进程处理获客新任务
|
||||
if ($current_worker_id == 2) {
|
||||
Timer::add(1, function () use ($current_worker_id, $process_count_for_status_0, $adapter) {
|
||||
$adapter->handleCustomerTaskWithStatusIsNew($current_worker_id, $process_count_for_status_0);
|
||||
});
|
||||
}
|
||||
|
||||
// 在一个进程里处理自动问候任务
|
||||
if ($current_worker_id == 1) {
|
||||
// 每60秒检查一次自动问候规则
|
||||
Timer::add(60, function () use ($adapter) {
|
||||
//$adapter->handleAutoGreetings();
|
||||
});
|
||||
}
|
||||
|
||||
// 更多其他后台任务
|
||||
// ......
|
||||
|
||||
}
|
||||
}
|
||||
36
application/common/Utils.php
Normal file
36
application/common/Utils.php
Normal file
@@ -0,0 +1,36 @@
|
||||
<?php
|
||||
|
||||
namespace app\common;
|
||||
|
||||
class Utils {
|
||||
|
||||
/**
|
||||
* 获取URL
|
||||
*
|
||||
* @param $url
|
||||
* @return string
|
||||
*/
|
||||
static public function absoluteUrl($url) {
|
||||
if (!empty($_SERVER['HTTP_HOST'])) {
|
||||
return (!empty($_SERVER['HTTPS']) ? 'https' : 'http')
|
||||
. '://' . $_SERVER['HTTP_HOST']
|
||||
. ($url{0} === '/' ? $url : '/' . $url);
|
||||
} else {
|
||||
return config('config.domain') . ($url{0} === '/' ? $url : '/' . $url);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 计算熟知
|
||||
*
|
||||
* @param $total
|
||||
* @param $number1
|
||||
* @param $number2
|
||||
*/
|
||||
static public function allocNumber($total, & $number1, & $number2) {
|
||||
if ($number1 > 0 OR $number2 > 0) {
|
||||
$number1 = $total * ($number1 / ($number1 + $number2));
|
||||
$number2 = $total - $number1;
|
||||
}
|
||||
}
|
||||
}
|
||||
101
application/common/Video.php
Normal file
101
application/common/Video.php
Normal file
@@ -0,0 +1,101 @@
|
||||
<?php
|
||||
|
||||
namespace app\common;
|
||||
|
||||
class Video {
|
||||
|
||||
/**
|
||||
* 图片合成视频
|
||||
*
|
||||
* @param array $images
|
||||
* @return string
|
||||
* @throws \Exception
|
||||
*/
|
||||
static public function compositing(array $images) {
|
||||
if (empty($images)) {
|
||||
throw new \Exception('图片列表不能为空');
|
||||
}
|
||||
|
||||
// 设置视频参数
|
||||
$width = 800;
|
||||
$height = 600; // 4:3 比例
|
||||
$frameRate = 30; // 调整帧率为30fps,使动画更流畅
|
||||
|
||||
// 创建临时文件夹
|
||||
$tempDir = ROOT_PATH . DS . 'runtime' . DS . 'video_temp';
|
||||
if (!is_dir($tempDir)) {
|
||||
mkdir($tempDir, 0777, true);
|
||||
}
|
||||
|
||||
// 生成唯一的输出文件名
|
||||
$outputFile = $tempDir . DS . uniqid() . '.mp4';
|
||||
|
||||
// 准备图片列表文件
|
||||
$listFile = $tempDir . DS . uniqid() . '.txt';
|
||||
$content = '';
|
||||
|
||||
// 生成临时图片序列目录
|
||||
$tempImagesDir = $tempDir . DS . uniqid();
|
||||
if (!is_dir($tempImagesDir)) {
|
||||
mkdir($tempImagesDir, 0777, true);
|
||||
}
|
||||
|
||||
// 处理每张图片,生成临时文件
|
||||
$tempImages = [];
|
||||
foreach ($images as $index => $image) {
|
||||
if (!file_exists($image)) {
|
||||
throw new \Exception('图片不存在:' . $image);
|
||||
}
|
||||
$tempImage = $tempImagesDir . DS . sprintf('%04d.jpg', $index);
|
||||
copy($image, $tempImage);
|
||||
$tempImages[] = $tempImage;
|
||||
}
|
||||
|
||||
// 构建 FFmpeg 命令
|
||||
$filterComplex = [];
|
||||
|
||||
// 处理所有输入流的格式和缩放
|
||||
foreach ($tempImages as $i => $image) {
|
||||
$filterComplex[] = sprintf('[%d:v]format=yuv420p,scale=%d:%d:force_original_aspect_ratio=decrease,pad=%d:%d:(ow-iw)/2:(oh-ih)/2[v%d]',
|
||||
$i, $width, $height, $width, $height, $i);
|
||||
}
|
||||
|
||||
// 构建转场效果链
|
||||
$lastOutput = 'v0';
|
||||
for ($i = 1; $i < count($tempImages); $i++) {
|
||||
$filterComplex[] = sprintf('[%s][v%d]xfade=transition=fade:duration=1:offset=%d[fade%d]',
|
||||
$lastOutput, $i, ($i * 3) - 1, $i);
|
||||
$lastOutput = sprintf('fade%d', $i);
|
||||
}
|
||||
|
||||
$inputFiles = '';
|
||||
foreach ($tempImages as $image) {
|
||||
$inputFiles .= sprintf('-loop 1 -t %d -i "%s" ', count($tempImages) * 3, $image);
|
||||
}
|
||||
|
||||
$duration = count($tempImages) * 3 - (count($tempImages) - 1); // 总时长计算
|
||||
|
||||
$cmd = sprintf(
|
||||
'ffmpeg5 %s -filter_complex "%s" -map "[%s]" -t %d -c:v libx264 -pix_fmt yuv420p -r %d "%s" 2>&1',
|
||||
$inputFiles,
|
||||
implode(';', $filterComplex),
|
||||
$lastOutput,
|
||||
$duration,
|
||||
$frameRate,
|
||||
$outputFile
|
||||
);
|
||||
|
||||
// 执行命令
|
||||
exec($cmd, $output, $returnCode);
|
||||
|
||||
// 清理临时文件
|
||||
array_map('unlink', glob($tempImagesDir . DS . '*'));
|
||||
rmdir($tempImagesDir);
|
||||
|
||||
if ($returnCode !== 0) {
|
||||
throw new \Exception('视频生成失败:' . implode("\n", $output));
|
||||
}
|
||||
|
||||
return $outputFile;
|
||||
}
|
||||
}
|
||||
33
application/common/config/route.php
Normal file
33
application/common/config/route.php
Normal file
@@ -0,0 +1,33 @@
|
||||
<?php
|
||||
// common模块路由配置
|
||||
|
||||
use think\facade\Route;
|
||||
|
||||
// 定义RESTful风格的API路由 - 认证相关
|
||||
Route::group('v1/auth', function () {
|
||||
// 无需认证的接口
|
||||
Route::post('login', 'app\common\controller\PasswordLoginController@index'); // 账号密码登录
|
||||
Route::post('mobile-login', 'app\common\controller\Auth@mobileLogin'); // 手机号验证码登录
|
||||
Route::post('code', 'app\common\controller\Auth@SendCodeController'); // 发送验证码
|
||||
// 需要JWT认证的接口
|
||||
Route::get('info', 'app\common\controller\Auth@info')->middleware(['jwt']); // 获取用户信息
|
||||
Route::post('refresh', 'app\common\controller\Auth@refresh')->middleware(['jwt']); // 刷新令牌
|
||||
});
|
||||
|
||||
// 附件上传相关路由
|
||||
Route::group('v1/', function () {
|
||||
Route::post('attachment/upload', 'app\common\controller\Attachment@upload'); // 上传附件
|
||||
Route::get('attachment/:id', 'app\common\controller\Attachment@info'); // 获取附件信息
|
||||
})->middleware(['jwt']);
|
||||
|
||||
|
||||
|
||||
Route::group('v1/pay', function () {
|
||||
Route::post('', 'app\cunkebao\controller\Pay@createOrder')->middleware(['jwt']);
|
||||
Route::any('notify', 'app\common\controller\PaymentService@notify');
|
||||
});
|
||||
|
||||
|
||||
|
||||
|
||||
Route::get('v1/app/update', 'app\common\controller\Api@uploadApp'); //检测app是否需要更新
|
||||
151
application/common/controller/Api.php
Normal file
151
application/common/controller/Api.php
Normal file
@@ -0,0 +1,151 @@
|
||||
<?php
|
||||
|
||||
namespace app\common\controller;
|
||||
|
||||
use library\ResponseHelper;
|
||||
use think\Controller;
|
||||
use think\Db;
|
||||
use think\facade\Config;
|
||||
use think\facade\Request;
|
||||
use think\facade\Response;
|
||||
|
||||
/**
|
||||
* API基础控制器
|
||||
*/
|
||||
class Api extends Controller
|
||||
{
|
||||
/**
|
||||
* 无需登录的方法,同时也就不需要鉴权了
|
||||
* @var array
|
||||
*/
|
||||
protected $noNeedLogin = [];
|
||||
|
||||
/**
|
||||
* 无需鉴权的方法,但需要登录
|
||||
* @var array
|
||||
*/
|
||||
protected $noNeedRight = [];
|
||||
|
||||
/**
|
||||
* 当前请求方法
|
||||
* @var string
|
||||
*/
|
||||
protected $requestType = '';
|
||||
|
||||
/**
|
||||
* 默认响应输出类型
|
||||
* @var string
|
||||
*/
|
||||
protected $responseType = 'json';
|
||||
|
||||
/**
|
||||
* 构造方法
|
||||
*/
|
||||
public function __construct()
|
||||
{
|
||||
parent::__construct();
|
||||
|
||||
// 请求类型
|
||||
$this->requestType = Request::method();
|
||||
|
||||
// 控制器初始化
|
||||
$this->_initialize();
|
||||
|
||||
// 跨域请求检测
|
||||
$this->_checkCors();
|
||||
}
|
||||
|
||||
/**
|
||||
* 初始化操作
|
||||
*/
|
||||
protected function _initialize()
|
||||
{
|
||||
// 初始化操作
|
||||
}
|
||||
|
||||
/**
|
||||
* 跨域检测
|
||||
* @deprecated 已由全局中间件 AllowCrossDomain 处理,此方法保留用于兼容
|
||||
*/
|
||||
protected function _checkCors()
|
||||
{
|
||||
// 由全局中间件处理跨域,此处不再处理
|
||||
if ($this->requestType === 'OPTIONS') {
|
||||
Response::create()->send();
|
||||
exit;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 操作成功返回的数据
|
||||
* @param string $msg 提示信息
|
||||
* @param mixed $data 返回的数据
|
||||
* @param int $code 错误码,默认为1
|
||||
* @param string $type 输出类型
|
||||
* @param array $header 发送的header信息
|
||||
*/
|
||||
protected function success($msg = '', $data = null, $code = 1, $type = null, array $header = [])
|
||||
{
|
||||
$this->result($msg, $data, $code, $type, $header);
|
||||
}
|
||||
|
||||
/**
|
||||
* 操作失败返回的数据
|
||||
* @param string $msg 提示信息
|
||||
* @param mixed $data 返回的数据
|
||||
* @param int $code 错误码,默认为0
|
||||
* @param string $type 输出类型
|
||||
* @param array $header 发送的header信息
|
||||
*/
|
||||
protected function error($msg = '', $data = null, $code = 0, $type = null, array $header = [])
|
||||
{
|
||||
$this->result($msg, $data, $code, $type, $header);
|
||||
}
|
||||
|
||||
/**
|
||||
* 返回封装后的API数据
|
||||
* @param string $msg 提示信息
|
||||
* @param mixed $data 要返回的数据
|
||||
* @param int $code 错误码,默认为0
|
||||
* @param string $type 输出类型,支持json/xml/jsonp
|
||||
* @param array $header 发送的header信息
|
||||
*/
|
||||
protected function result($msg, $data = null, $code = 0, $type = null, array $header = [])
|
||||
{
|
||||
$result = [
|
||||
'code' => $code,
|
||||
'msg' => $msg,
|
||||
'time' => time(),
|
||||
'data' => $data,
|
||||
];
|
||||
|
||||
// 返回数据格式
|
||||
$type = $type ?: $this->responseType;
|
||||
|
||||
// 发送响应
|
||||
$response = Response::create($result, $type)->header($header);
|
||||
$response->send();
|
||||
exit;
|
||||
}
|
||||
|
||||
|
||||
public function uploadApp()
|
||||
{
|
||||
$type = $this->request->param('type', '');
|
||||
if (empty($type)){
|
||||
return ResponseHelper::error('参数缺失');
|
||||
}
|
||||
|
||||
if (!in_array($type,['ckb','aiStore'])){
|
||||
return ResponseHelper::error('参数错误');
|
||||
}
|
||||
|
||||
$data = Db::name('app_version')
|
||||
->field('version,downloadUrl,updateContent,forceUpdate')
|
||||
->where(['type'=>$type])
|
||||
->order('id DESC')
|
||||
->find();
|
||||
return ResponseHelper::success($data, '获取成功');
|
||||
|
||||
}
|
||||
}
|
||||
144
application/common/controller/Attachment.php
Normal file
144
application/common/controller/Attachment.php
Normal file
@@ -0,0 +1,144 @@
|
||||
<?php
|
||||
namespace app\common\controller;
|
||||
|
||||
use think\Controller;
|
||||
use think\facade\Request;
|
||||
use app\common\model\Attachment as AttachmentModel;
|
||||
use app\common\util\AliyunOSS;
|
||||
|
||||
class Attachment extends Controller
|
||||
{
|
||||
/**
|
||||
* 上传文件
|
||||
* @return \think\response\Json
|
||||
*/
|
||||
public function upload()
|
||||
{
|
||||
try {
|
||||
// 获取上传文件
|
||||
$file = Request::file('file');
|
||||
if (!$file) {
|
||||
return json([
|
||||
'code' => 400,
|
||||
'msg' => '请选择要上传的文件'
|
||||
]);
|
||||
}
|
||||
|
||||
// 验证文件
|
||||
$validate = \think\facade\Validate::rule([
|
||||
'file' => [
|
||||
'fileSize' => 50485760, // 50MB
|
||||
'fileExt' => 'jpg,jpeg,png,gif,doc,docx,pdf,zip,rar,mp4,mp3,csv,xlsx,xls,ppt,pptx,txt',
|
||||
]
|
||||
]);
|
||||
|
||||
if (!$validate->check(['file' => $file])) {
|
||||
return json([
|
||||
'code' => 400,
|
||||
'msg' => $validate->getError()
|
||||
]);
|
||||
}
|
||||
|
||||
// 生成文件hash
|
||||
$hashKey = md5_file($file->getRealPath());
|
||||
|
||||
// 检查文件是否已存在
|
||||
$existFile = AttachmentModel::getByHashKey($hashKey);
|
||||
if ($existFile) {
|
||||
return json([
|
||||
'code' => 200,
|
||||
'msg' => '文件已存在',
|
||||
'data' => [
|
||||
'id' => $existFile['id'],
|
||||
'name' => $existFile['name'],
|
||||
'url' => $existFile['source'],
|
||||
'size' => isset($existFile['size']) ? $existFile['size'] : 0
|
||||
]
|
||||
]);
|
||||
}
|
||||
|
||||
|
||||
|
||||
// 生成OSS对象名称
|
||||
$objectName = AliyunOSS::generateObjectName($file->getInfo('name'));
|
||||
|
||||
// 上传到OSS
|
||||
$result = AliyunOSS::uploadFile($file->getRealPath(), $objectName);
|
||||
|
||||
|
||||
if (!$result['success']) {
|
||||
return json([
|
||||
'code' => 500,
|
||||
'msg' => '文件上传失败:' . $result['error']
|
||||
]);
|
||||
}
|
||||
|
||||
// 保存到数据库
|
||||
$attachmentData = [
|
||||
'name' => Request::param('name') ?: $file->getInfo('name'),
|
||||
'hash_key' => $hashKey,
|
||||
'server' => 'aliyun_oss',
|
||||
'source' => $result['url'],
|
||||
'size' => $result['size'],
|
||||
'suffix' => pathinfo($file->getInfo('name'), PATHINFO_EXTENSION)
|
||||
];
|
||||
|
||||
$attachment = AttachmentModel::addAttachment($attachmentData);
|
||||
|
||||
if (!$attachment) {
|
||||
return json([
|
||||
'code' => 500,
|
||||
'msg' => '保存附件信息失败'
|
||||
]);
|
||||
}
|
||||
|
||||
return json([
|
||||
'code' => 200,
|
||||
'msg' => '上传成功',
|
||||
'data' => [
|
||||
'id' => $attachment->id,
|
||||
'name' => $attachmentData['name'],
|
||||
'url' => $attachmentData['source'],
|
||||
'size' => $attachmentData['size']
|
||||
]
|
||||
]);
|
||||
|
||||
} catch (\Exception $e) {
|
||||
return json([
|
||||
'code' => 500,
|
||||
'msg' => '上传失败:' . $e->getMessage()
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取附件信息
|
||||
* @param int $id 附件ID
|
||||
* @return \think\response\Json
|
||||
*/
|
||||
public function info($id)
|
||||
{
|
||||
try {
|
||||
$attachment = AttachmentModel::find($id);
|
||||
|
||||
if (!$attachment) {
|
||||
return json([
|
||||
'code' => 404,
|
||||
'msg' => '附件不存在'
|
||||
]);
|
||||
}
|
||||
|
||||
return json([
|
||||
'code' => 200,
|
||||
'msg' => '获取成功',
|
||||
'data' => $attachment
|
||||
]);
|
||||
|
||||
} catch (\Exception $e) {
|
||||
return json([
|
||||
'code' => 500,
|
||||
'msg' => '获取失败:' . $e->getMessage()
|
||||
]);
|
||||
}
|
||||
}
|
||||
}
|
||||
157
application/common/controller/Auth.php
Normal file
157
application/common/controller/Auth.php
Normal file
@@ -0,0 +1,157 @@
|
||||
<?php
|
||||
namespace app\common\controller;
|
||||
|
||||
use app\common\service\AuthService;
|
||||
use library\ResponseHelper;
|
||||
use think\Controller;
|
||||
use think\facade\Request;
|
||||
|
||||
/**
|
||||
* 认证控制器
|
||||
* 处理用户登录和身份验证
|
||||
*/
|
||||
class Auth extends Controller
|
||||
{
|
||||
/**
|
||||
* 允许跨域请求的域名
|
||||
* @var string
|
||||
*/
|
||||
protected $allowOrigin = '*';
|
||||
|
||||
/**
|
||||
* 认证服务实例
|
||||
* @var AuthService
|
||||
*/
|
||||
protected $authService;
|
||||
|
||||
/**
|
||||
* 初始化
|
||||
*/
|
||||
public function initialize()
|
||||
{
|
||||
parent::initialize();
|
||||
|
||||
// 由全局中间件处理跨域,此处不再处理
|
||||
|
||||
// 初始化认证服务
|
||||
$this->authService = new AuthService();
|
||||
}
|
||||
|
||||
/**
|
||||
* 用户登录
|
||||
* @return \think\response\Json
|
||||
*/
|
||||
public function login()
|
||||
{
|
||||
// 获取登录参数
|
||||
$params = Request::only(['account', 'password', 'typeId']);
|
||||
|
||||
// 参数验证
|
||||
$validate = validate('common/Auth');
|
||||
if (!$validate->scene('login')->check($params)) {
|
||||
return ResponseHelper::error($validate->getError());
|
||||
}
|
||||
|
||||
try {
|
||||
// 调用登录服务
|
||||
$result = $this->authService->login(
|
||||
$params['account'],
|
||||
$params['password'],
|
||||
$params['typeId'],
|
||||
Request::ip()
|
||||
);
|
||||
|
||||
return ResponseHelper::success($result, '登录成功');
|
||||
} catch (\Exception $e) {
|
||||
return ResponseHelper::error($e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 手机号验证码登录
|
||||
* @return \think\response\Json
|
||||
*/
|
||||
public function mobileLogin()
|
||||
{
|
||||
// 获取登录参数
|
||||
$params = Request::only(['account', 'code', 'typeId']);
|
||||
|
||||
// 参数验证
|
||||
$validate = validate('common/Auth');
|
||||
if (!$validate->scene('mobile_login')->check($params)) {
|
||||
return ResponseHelper::error($validate->getError());
|
||||
}
|
||||
|
||||
try {
|
||||
// 判断验证码是否已加密
|
||||
$isEncrypted = isset($params['is_encrypted']) && $params['is_encrypted'] === true;
|
||||
|
||||
// 调用手机号登录服务
|
||||
$result = $this->authService->mobileLogin(
|
||||
$params['account'],
|
||||
$params['code'],
|
||||
Request::ip(),
|
||||
$isEncrypted
|
||||
);
|
||||
|
||||
return ResponseHelper::success($result, '登录成功');
|
||||
} catch (\Exception $e) {
|
||||
return ResponseHelper::error($e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 发送验证码
|
||||
* @return \think\response\Json
|
||||
*/
|
||||
public function sendCode()
|
||||
{
|
||||
// 获取参数
|
||||
$params = Request::only(['account', 'type']);
|
||||
|
||||
// 参数验证
|
||||
$validate = validate('common/Auth');
|
||||
if (!$validate->scene('send_code')->check($params)) {
|
||||
return ResponseHelper::error($validate->getError());
|
||||
}
|
||||
|
||||
try {
|
||||
// 调用发送验证码服务
|
||||
$result = $this->authService->sendLoginCode(
|
||||
$params['account'],
|
||||
$params['type']
|
||||
);
|
||||
return ResponseHelper::success($result, '验证码发送成功');
|
||||
} catch (\Exception $e) {
|
||||
return ResponseHelper::error($e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取用户信息
|
||||
* @return \think\response\Json
|
||||
*/
|
||||
public function info()
|
||||
{
|
||||
try {
|
||||
$result = $this->authService->getUserInfo(request()->userInfo);
|
||||
return ResponseHelper::success($result);
|
||||
} catch (\Exception $e) {
|
||||
return ResponseHelper::unauthorized($e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 刷新令牌
|
||||
* @return \think\response\Json
|
||||
*/
|
||||
public function refresh()
|
||||
{
|
||||
try {
|
||||
$result = $this->authService->refreshToken(request()->userInfo);
|
||||
return ResponseHelper::success($result, '刷新成功');
|
||||
} catch (\Exception $e) {
|
||||
return ResponseHelper::unauthorized($e->getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
12
application/common/controller/BaseController.php
Normal file
12
application/common/controller/BaseController.php
Normal file
@@ -0,0 +1,12 @@
|
||||
<?php
|
||||
|
||||
namespace app\common\controller;
|
||||
|
||||
use think\Controller;
|
||||
|
||||
/**
|
||||
* 基础控制器
|
||||
*/
|
||||
class BaseController extends Controller
|
||||
{
|
||||
}
|
||||
333
application/common/controller/ExportController.php
Normal file
333
application/common/controller/ExportController.php
Normal file
@@ -0,0 +1,333 @@
|
||||
<?php
|
||||
|
||||
namespace app\common\controller;
|
||||
|
||||
use PHPExcel;
|
||||
use PHPExcel_IOFactory;
|
||||
use PHPExcel_Worksheet_Drawing;
|
||||
use think\Controller;
|
||||
use think\Exception;
|
||||
|
||||
/**
|
||||
* 通用导出控制器,提供 Excel 导出与图片插入能力
|
||||
*/
|
||||
class ExportController extends Controller
|
||||
{
|
||||
/**
|
||||
* @var array<string> 需要在请求结束时清理的临时文件
|
||||
*/
|
||||
protected static $tempFiles = [];
|
||||
|
||||
/**
|
||||
* 导出 Excel(支持指定列插入图片)
|
||||
*
|
||||
* @param string $fileName 输出文件名(可不带扩展名)
|
||||
* @param array $headers 列定义,例如 ['name' => '姓名', 'phone' => '电话']
|
||||
* @param array $rows 数据行,需与 $headers 的 key 对应
|
||||
* @param array $imageColumns 需要渲染为图片的列 key 列表
|
||||
* @param string $sheetName 工作表名称
|
||||
* @param array $options 额外选项:
|
||||
* - imageWidth(图片宽度,默认100)
|
||||
* - imageHeight(图片高度,默认100)
|
||||
* - imageColumnWidth(图片列宽,默认15)
|
||||
* - titleRow(标题行内容,支持多行文本数组)
|
||||
*
|
||||
* @throws Exception
|
||||
*/
|
||||
public static function exportExcelWithImages(
|
||||
$fileName,
|
||||
array $headers,
|
||||
array $rows,
|
||||
array $imageColumns = [],
|
||||
$sheetName = 'Sheet1',
|
||||
array $options = []
|
||||
) {
|
||||
if (empty($headers)) {
|
||||
throw new Exception('导出列定义不能为空');
|
||||
}
|
||||
if (empty($rows)) {
|
||||
throw new Exception('导出数据不能为空');
|
||||
}
|
||||
|
||||
// 抑制 PHPExcel 库中已废弃的大括号语法警告(PHP 7.4+)
|
||||
$oldErrorReporting = error_reporting(E_ALL & ~E_DEPRECATED & ~E_STRICT);
|
||||
|
||||
// 默认选项
|
||||
$imageWidth = isset($options['imageWidth']) ? (int)$options['imageWidth'] : 100;
|
||||
$imageHeight = isset($options['imageHeight']) ? (int)$options['imageHeight'] : 100;
|
||||
$imageColumnWidth = isset($options['imageColumnWidth']) ? (float)$options['imageColumnWidth'] : 15;
|
||||
$rowHeight = isset($options['rowHeight']) ? (int)$options['rowHeight'] : ($imageHeight + 10);
|
||||
|
||||
$excel = new PHPExcel();
|
||||
$sheet = $excel->getActiveSheet();
|
||||
$sheet->setTitle($sheetName);
|
||||
|
||||
$columnKeys = array_keys($headers);
|
||||
$totalColumns = count($columnKeys);
|
||||
$lastColumnLetter = self::columnLetter($totalColumns - 1);
|
||||
|
||||
// 定义特定列的固定宽度(如果未指定则使用默认值)
|
||||
$columnWidths = isset($options['columnWidths']) ? $options['columnWidths'] : [];
|
||||
|
||||
// 检查是否有标题行
|
||||
$titleRow = isset($options['titleRow']) ? $options['titleRow'] : null;
|
||||
$dataStartRow = 1; // 数据开始行(表头行)
|
||||
|
||||
// 如果有标题行,先写入标题行(支持数组或字符串)
|
||||
if (!empty($titleRow)) {
|
||||
$dataStartRow = 2; // 数据从第2行开始(第1行是标题,第2行是表头)
|
||||
|
||||
// 合并标题行单元格(从第一列到最后一列)
|
||||
$titleRange = 'A1:' . $lastColumnLetter . '1';
|
||||
$sheet->mergeCells($titleRange);
|
||||
|
||||
// 构建标题内容(支持多行数组或字符串)
|
||||
$titleContent = '';
|
||||
if (is_array($titleRow)) {
|
||||
$titleContent = implode("\n", $titleRow);
|
||||
} else {
|
||||
$titleContent = (string)$titleRow;
|
||||
}
|
||||
|
||||
// 写入标题
|
||||
$sheet->setCellValue('A1', $titleContent);
|
||||
|
||||
// 设置标题行样式
|
||||
$sheet->getStyle('A1')->applyFromArray([
|
||||
'font' => ['bold' => true, 'size' => 16],
|
||||
'alignment' => [
|
||||
'horizontal' => \PHPExcel_Style_Alignment::HORIZONTAL_CENTER,
|
||||
'vertical' => \PHPExcel_Style_Alignment::VERTICAL_CENTER,
|
||||
'wrap' => true
|
||||
],
|
||||
'fill' => [
|
||||
'type' => \PHPExcel_Style_Fill::FILL_SOLID,
|
||||
'color' => ['rgb' => 'FFF8DC'] // 浅黄色背景
|
||||
],
|
||||
'borders' => [
|
||||
'allborders' => [
|
||||
'style' => \PHPExcel_Style_Border::BORDER_THIN,
|
||||
'color' => ['rgb' => '000000']
|
||||
]
|
||||
]
|
||||
]);
|
||||
$sheet->getRowDimension(1)->setRowHeight(80); // 标题行高度
|
||||
}
|
||||
|
||||
// 写入表头并设置列宽
|
||||
$headerRow = $dataStartRow;
|
||||
foreach ($columnKeys as $index => $key) {
|
||||
$columnLetter = self::columnLetter($index);
|
||||
$sheet->setCellValue($columnLetter . $headerRow, $headers[$key]);
|
||||
|
||||
// 如果是图片列,设置固定列宽
|
||||
if (in_array($key, $imageColumns, true)) {
|
||||
$sheet->getColumnDimension($columnLetter)->setWidth($imageColumnWidth);
|
||||
} elseif (isset($columnWidths[$key])) {
|
||||
// 如果指定了该列的宽度,使用指定宽度
|
||||
$sheet->getColumnDimension($columnLetter)->setWidth($columnWidths[$key]);
|
||||
} else {
|
||||
// 否则自动调整
|
||||
$sheet->getColumnDimension($columnLetter)->setAutoSize(true);
|
||||
}
|
||||
}
|
||||
|
||||
// 设置表头样式
|
||||
$headerRange = 'A' . $headerRow . ':' . $lastColumnLetter . $headerRow;
|
||||
$sheet->getStyle($headerRange)->applyFromArray([
|
||||
'font' => ['bold' => true, 'size' => 11],
|
||||
'alignment' => [
|
||||
'horizontal' => \PHPExcel_Style_Alignment::HORIZONTAL_CENTER,
|
||||
'vertical' => \PHPExcel_Style_Alignment::VERTICAL_CENTER,
|
||||
'wrap' => true
|
||||
],
|
||||
'fill' => [
|
||||
'type' => \PHPExcel_Style_Fill::FILL_SOLID,
|
||||
'color' => ['rgb' => 'FFF8DC']
|
||||
],
|
||||
'borders' => [
|
||||
'allborders' => [
|
||||
'style' => \PHPExcel_Style_Border::BORDER_THIN,
|
||||
'color' => ['rgb' => '000000']
|
||||
]
|
||||
]
|
||||
]);
|
||||
$sheet->getRowDimension($headerRow)->setRowHeight(30); // 增加表头行高以确保文本完整显示
|
||||
|
||||
// 写入数据与图片
|
||||
$dataRowStart = $dataStartRow + 1; // 数据从表头行下一行开始
|
||||
foreach ($rows as $rowIndex => $rowData) {
|
||||
$excelRow = $dataRowStart + $rowIndex; // 数据行
|
||||
$maxRowHeight = $rowHeight; // 记录当前行的最大高度
|
||||
|
||||
foreach ($columnKeys as $colIndex => $key) {
|
||||
$columnLetter = self::columnLetter($colIndex);
|
||||
$cell = $columnLetter . $excelRow;
|
||||
$value = isset($rowData[$key]) ? $rowData[$key] : '';
|
||||
|
||||
if (in_array($key, $imageColumns, true) && !empty($value)) {
|
||||
$imagePath = self::resolveImagePath($value);
|
||||
if ($imagePath) {
|
||||
// 获取图片实际尺寸并等比例缩放
|
||||
$imageSize = @getimagesize($imagePath);
|
||||
if ($imageSize) {
|
||||
$originalWidth = $imageSize[0];
|
||||
$originalHeight = $imageSize[1];
|
||||
|
||||
// 计算等比例缩放后的尺寸
|
||||
$ratio = min($imageWidth / $originalWidth, $imageHeight / $originalHeight);
|
||||
$scaledWidth = $originalWidth * $ratio;
|
||||
$scaledHeight = $originalHeight * $ratio;
|
||||
|
||||
// 确保不超过最大尺寸
|
||||
if ($scaledWidth > $imageWidth) {
|
||||
$scaledWidth = $imageWidth;
|
||||
$scaledHeight = $originalHeight * ($imageWidth / $originalWidth);
|
||||
}
|
||||
if ($scaledHeight > $imageHeight) {
|
||||
$scaledHeight = $imageHeight;
|
||||
$scaledWidth = $originalWidth * ($imageHeight / $originalHeight);
|
||||
}
|
||||
|
||||
$drawing = new PHPExcel_Worksheet_Drawing();
|
||||
$drawing->setPath($imagePath);
|
||||
$drawing->setCoordinates($cell);
|
||||
|
||||
// 居中显示图片(Excel列宽1单位≈7像素,行高1单位≈0.75像素)
|
||||
$cellWidthPx = $imageColumnWidth * 7;
|
||||
$cellHeightPx = $maxRowHeight * 0.75;
|
||||
$offsetX = max(2, ($cellWidthPx - $scaledWidth) / 2);
|
||||
$offsetY = max(2, ($cellHeightPx - $scaledHeight) / 2);
|
||||
|
||||
$drawing->setOffsetX((int)$offsetX);
|
||||
$drawing->setOffsetY((int)$offsetY);
|
||||
$drawing->setWidth((int)$scaledWidth);
|
||||
$drawing->setHeight((int)$scaledHeight);
|
||||
$drawing->setWorksheet($sheet);
|
||||
|
||||
// 更新行高以适应图片(留出一些边距)
|
||||
$neededHeight = (int)($scaledHeight / 0.75) + 10;
|
||||
if ($neededHeight > $maxRowHeight) {
|
||||
$maxRowHeight = $neededHeight;
|
||||
}
|
||||
} else {
|
||||
// 如果无法获取图片尺寸,使用默认尺寸
|
||||
$drawing = new PHPExcel_Worksheet_Drawing();
|
||||
$drawing->setPath($imagePath);
|
||||
$drawing->setCoordinates($cell);
|
||||
$drawing->setOffsetX(5);
|
||||
$drawing->setOffsetY(5);
|
||||
$drawing->setWidth($imageWidth);
|
||||
$drawing->setHeight($imageHeight);
|
||||
$drawing->setWorksheet($sheet);
|
||||
}
|
||||
} else {
|
||||
$sheet->setCellValue($cell, '');
|
||||
}
|
||||
} else {
|
||||
$sheet->setCellValue($cell, $value);
|
||||
// 设置文本对齐和换行
|
||||
$style = $sheet->getStyle($cell);
|
||||
$style->getAlignment()->setVertical(\PHPExcel_Style_Alignment::VERTICAL_CENTER);
|
||||
$style->getAlignment()->setWrapText(true);
|
||||
// 根据列类型设置水平对齐
|
||||
if (in_array($key, ['date', 'postTime'])) {
|
||||
$style->getAlignment()->setHorizontal(\PHPExcel_Style_Alignment::HORIZONTAL_CENTER);
|
||||
} else {
|
||||
$style->getAlignment()->setHorizontal(\PHPExcel_Style_Alignment::HORIZONTAL_LEFT);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 设置行高
|
||||
$sheet->getRowDimension($excelRow)->setRowHeight($maxRowHeight);
|
||||
}
|
||||
|
||||
$safeName = preg_replace('/[^\w\-]/', '_', $fileName ?: 'export_' . date('Ymd_His'));
|
||||
if (stripos($safeName, '.xlsx') === false) {
|
||||
$safeName .= '.xlsx';
|
||||
}
|
||||
|
||||
if (ob_get_length()) {
|
||||
ob_end_clean();
|
||||
}
|
||||
|
||||
header('Content-Type: application/vnd.openxmlformats-officedocument.spreadsheetml.sheet');
|
||||
header('Cache-Control: max-age=0');
|
||||
header('Content-Disposition: attachment;filename="' . $safeName . '"');
|
||||
|
||||
try {
|
||||
$writer = PHPExcel_IOFactory::createWriter($excel, 'Excel2007');
|
||||
$writer->save('php://output');
|
||||
} catch (\Exception $e) {
|
||||
// 恢复错误报告级别
|
||||
error_reporting($oldErrorReporting);
|
||||
throw $e;
|
||||
}
|
||||
|
||||
// 恢复错误报告级别
|
||||
error_reporting($oldErrorReporting);
|
||||
self::cleanupTempFiles();
|
||||
exit;
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据列序号生成 Excel 列字母
|
||||
*
|
||||
* @param int $index
|
||||
* @return string
|
||||
*/
|
||||
protected static function columnLetter($index)
|
||||
{
|
||||
$letters = '';
|
||||
do {
|
||||
$letters = chr($index % 26 + 65) . $letters;
|
||||
$index = intval($index / 26) - 1;
|
||||
} while ($index >= 0);
|
||||
|
||||
return $letters;
|
||||
}
|
||||
|
||||
/**
|
||||
* 将远程或本地图片路径转换为可用的本地文件路径
|
||||
*
|
||||
* @param string $path
|
||||
* @return string|null
|
||||
*/
|
||||
protected static function resolveImagePath($path)
|
||||
{
|
||||
if (empty($path)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (preg_match('/^https?:\/\//i', $path)) {
|
||||
$tempFile = tempnam(sys_get_temp_dir(), 'export_img_');
|
||||
$stream = @file_get_contents($path);
|
||||
if ($stream === false) {
|
||||
return null;
|
||||
}
|
||||
file_put_contents($tempFile, $stream);
|
||||
self::$tempFiles[] = $tempFile;
|
||||
return $tempFile;
|
||||
}
|
||||
|
||||
if (file_exists($path)) {
|
||||
return $path;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 清理所有临时文件
|
||||
*/
|
||||
protected static function cleanupTempFiles()
|
||||
{
|
||||
foreach (self::$tempFiles as $file) {
|
||||
if (file_exists($file)) {
|
||||
@unlink($file);
|
||||
}
|
||||
}
|
||||
self::$tempFiles = [];
|
||||
}
|
||||
}
|
||||
52
application/common/controller/GetOpenid.php
Normal file
52
application/common/controller/GetOpenid.php
Normal file
@@ -0,0 +1,52 @@
|
||||
<?php
|
||||
|
||||
namespace app\common\controller;
|
||||
|
||||
use EasyWeChat\Factory;
|
||||
use think\Controller;
|
||||
use think\facade\Env;
|
||||
class GetOpenid extends Controller
|
||||
{
|
||||
|
||||
protected $app;
|
||||
public function __construct()
|
||||
{
|
||||
parent::__construct();
|
||||
|
||||
// 从环境变量获取配置
|
||||
$config = [
|
||||
'app_id' => Env::get('weChat.appid'),
|
||||
'secret' => Env::get('weChat.secret'),
|
||||
'response_type' => 'array'
|
||||
];
|
||||
$this->app = Factory::officialAccount($config);
|
||||
}
|
||||
|
||||
|
||||
|
||||
public function index()
|
||||
{
|
||||
$app = $this->app;
|
||||
$oauth = $app->oauth;
|
||||
|
||||
// 未登录
|
||||
if (empty($_SESSION['wechat_user'])) {
|
||||
|
||||
$_SESSION['target_url'] = 'user/profile';
|
||||
|
||||
$redirectUrl = $oauth->redirect();
|
||||
|
||||
exit_data($redirectUrl);
|
||||
header("Location: {$redirectUrl}");
|
||||
exit;
|
||||
}
|
||||
|
||||
// 已经登录过
|
||||
$user = $_SESSION['wechat_user'];
|
||||
|
||||
exit_data($user);
|
||||
|
||||
return 'Hello, World!';
|
||||
}
|
||||
|
||||
}
|
||||
157
application/common/controller/PasswordLoginController.php
Normal file
157
application/common/controller/PasswordLoginController.php
Normal file
@@ -0,0 +1,157 @@
|
||||
<?php
|
||||
|
||||
namespace app\common\controller;
|
||||
|
||||
use app\common\model\User as UserModel;
|
||||
use app\common\util\JwtUtil;
|
||||
use Exception;
|
||||
use library\ResponseHelper;
|
||||
use think\Db;
|
||||
use think\Validate;
|
||||
|
||||
/**
|
||||
* 认证控制器
|
||||
* 处理用户登录和身份验证
|
||||
*/
|
||||
class PasswordLoginController extends BaseController
|
||||
{
|
||||
/**
|
||||
* 获取用户基本信息
|
||||
*
|
||||
* @param string $account
|
||||
* @param int $typeId
|
||||
* @return UserModel
|
||||
*/
|
||||
protected function getUserProfileWithAccountAndType(string $account, int $typeId)
|
||||
{
|
||||
$user = UserModel::where(
|
||||
function ($query) use ($account) {
|
||||
$query->where('phone', $account)->whereOr('account', $account);
|
||||
}
|
||||
)
|
||||
->where(
|
||||
function ($query) use ($typeId) {
|
||||
$query->where('status', 1)->where('typeId', $typeId);
|
||||
}
|
||||
)->find();
|
||||
|
||||
if(!empty($user)){
|
||||
return $user;
|
||||
}else{
|
||||
return '';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取用户信息
|
||||
*
|
||||
* @param string $account 账号(手机号)
|
||||
* @param string $password 密码(可能是加密后的)
|
||||
* @param int $typeId 身份信息
|
||||
* @return array|null
|
||||
*/
|
||||
protected function getUser(string $account, string $password, int $typeId): array
|
||||
{
|
||||
$user = $this->getUserProfileWithAccountAndType($account, $typeId);
|
||||
if (!$user) {
|
||||
throw new \Exception('用户不存在或已禁用', 403);
|
||||
}
|
||||
|
||||
$password = md5($password);
|
||||
if ($user->passwordMd5 !== $password) {
|
||||
throw new \Exception('账号或密码错误', 403);
|
||||
}
|
||||
|
||||
return array_merge($user->toArray(), [
|
||||
'lastLoginIp' => $this->request->ip(),
|
||||
'lastLoginTime' => time()
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* 数据验证
|
||||
*
|
||||
* @param array $params
|
||||
* @return $this
|
||||
* @throws \Exception
|
||||
*/
|
||||
protected function dataValidate(array $params): self
|
||||
{
|
||||
$validate = Validate::make([
|
||||
'account' => 'require',
|
||||
'password' => 'require|length:6,64',
|
||||
'typeId' => 'require|in:1,2',
|
||||
], [
|
||||
'account.require' => '账号不能为空',
|
||||
'password.require' => '密码不能为空',
|
||||
'password.length' => '密码长度必须在6-64个字符之间',
|
||||
'typeId.require' => '用户类型不能为空',
|
||||
'typeId.in' => '用户类型错误',
|
||||
]);
|
||||
|
||||
if (!$validate->check($params)) {
|
||||
throw new \Exception($validate->getError(), 400);
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* 用户登录
|
||||
*
|
||||
* @param string $account 账号(手机号)
|
||||
* @param string $password 密码(可能是加密后的)
|
||||
* @param string $typeId 登录IP
|
||||
* @param string $deviceId 本地设备imei
|
||||
* @return array
|
||||
* @throws \Exception
|
||||
*/
|
||||
protected function doLogin(string $account, string $password, int $typeId, string $deviceId): array
|
||||
{
|
||||
// 获取用户信息
|
||||
$member = $this->getUser($account, $password, $typeId);
|
||||
$deviceTotal = Db::name('device')->where(['companyId' => $member['companyId'],'deleteTime' => 0])->count();
|
||||
|
||||
//更新设备imei
|
||||
if ($typeId == 2 && !empty($deviceId)){
|
||||
$deviceUser = Db::name('device_user')->where(['companyId' => $member['companyId'],'userId' => $member['id'],'deleteTime' => 0])->find();
|
||||
if (!empty($deviceUser)){
|
||||
$device = Db::name('device')->where(['companyId' => $member['companyId'],'deleteTime' => 0,'id' => $deviceUser['deviceId']])->find();
|
||||
if (!empty($device) && empty($device['deviceImei'])){
|
||||
Db::table('s2_device')->where(['id' => $device['id']])->update(['deviceImei' => $deviceId,'updateTime' => time()]);
|
||||
Db::name('device')->where(['id' => $device['id']])->update(['deviceImei' => $deviceId,'updateTime' => time()]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// 生成JWT令牌
|
||||
$token = JwtUtil::createToken($member, 86400 * 30);
|
||||
$token_expired = time() + 86400 * 30;
|
||||
return compact('member', 'token', 'token_expired','deviceTotal');
|
||||
}
|
||||
|
||||
/**
|
||||
* 用户登录
|
||||
*
|
||||
* @return \think\response\Json
|
||||
*/
|
||||
public function index()
|
||||
{
|
||||
$params = $this->request->only(['account', 'password', 'typeId','deviceId']);
|
||||
try {
|
||||
$deviceId = isset($params['deviceId']) ? $params['deviceId'] : '';
|
||||
$userData = $this->dataValidate($params)->doLogin(
|
||||
$params['account'],
|
||||
$params['password'],
|
||||
$params['typeId'],
|
||||
$deviceId
|
||||
);
|
||||
|
||||
|
||||
return ResponseHelper::success($userData, '登录成功');
|
||||
} catch (Exception $e) {
|
||||
return ResponseHelper::error($e->getMessage(), $e->getCode());
|
||||
}
|
||||
}
|
||||
}
|
||||
543
application/common/controller/PaymentService.php
Normal file
543
application/common/controller/PaymentService.php
Normal file
@@ -0,0 +1,543 @@
|
||||
<?php
|
||||
|
||||
namespace app\common\controller;
|
||||
|
||||
use app\chukebao\model\TokensCompany;
|
||||
use app\chukebao\model\TokensRecord;
|
||||
use think\Db;
|
||||
use app\common\util\PaymentUtil;
|
||||
use think\facade\Env;
|
||||
use think\facade\Request;
|
||||
use app\common\model\Order;
|
||||
use app\common\model\User;
|
||||
|
||||
/**
|
||||
* 支付服务(内部调用)
|
||||
*/
|
||||
class PaymentService
|
||||
{
|
||||
/**
|
||||
* 统一支付下单接口
|
||||
* 支持扫码付款、微信支付、支付宝支付
|
||||
*
|
||||
* @param array $order
|
||||
* - orderNo: string 商户订单号(必填)
|
||||
* - money: int 金额(分,必填)
|
||||
* - goodsName: string 商品描述(必填)
|
||||
* - service: string 支付服务类型(可选)
|
||||
* - 'wechat' 或 'pay.weixin.jspay': 微信JSAPI支付
|
||||
* - 'alipay' 或 'pay.alipay.jspay': 支付宝JSAPI支付
|
||||
* - 不传或空: 默认扫码付款
|
||||
* - openid: string 微信用户openid(微信JSAPI支付必填)
|
||||
* - buyer_id: string 支付宝用户ID(支付宝JSAPI支付可选)
|
||||
* - notify_url: string 异步通知地址(可选)
|
||||
* @return string JSON格式响应
|
||||
*/
|
||||
public function createOrder(array $order)
|
||||
{
|
||||
// 确定service类型:支持简写形式 wechat/alipay,或完整的 service 值
|
||||
$serviceType = $order['service'] ?? '';
|
||||
|
||||
// 映射简写形式到完整的 service 值
|
||||
if ($serviceType === 'wechat' || $serviceType === 'pay.weixin.jspay') {
|
||||
$service = 'pay.weixin.jspay';
|
||||
} elseif ($serviceType === 'alipay' || $serviceType === 'pay.alipay.jspay') {
|
||||
$service = 'pay.alipay.jspay';
|
||||
} elseif ($serviceType === 'qrCode' || $serviceType === 'unified.trade.native') {
|
||||
$service = 'unified.trade.native';
|
||||
} else {
|
||||
// 默认扫码支付
|
||||
$service = 'unified.trade.native';
|
||||
}
|
||||
|
||||
// 构建基础参数
|
||||
$params = [
|
||||
'service' => $service,
|
||||
'sign_type' => PaymentUtil::SIGN_TYPE_MD5,
|
||||
'mch_id' => Env::get('payment.mchId'),
|
||||
'out_trade_no' => $order['orderNo'],
|
||||
'body' => $order['goodsName'] ?? '',
|
||||
'total_fee' => $order['money'] ?? 0,
|
||||
'mch_create_ip' => Request::ip(),
|
||||
'notify_url' => $order['notify_url'] ?? Env::get('payment.notify_url', '127.0.0.1'),
|
||||
'nonce_str' => PaymentUtil::generateNonceStr(),
|
||||
];
|
||||
|
||||
// 微信JSAPI支付需要openid
|
||||
if ($service == 'pay.weixin.jspay') {
|
||||
// $params['sub_openid'] = 'oB44Yw1T6bfVAZwjj729P-6CUSPE';
|
||||
$params['is_raw'] = 0;
|
||||
$params['mch_app_name'] = '存客宝';
|
||||
$params['mch_app_id'] = 'https://kr-op.quwanzhi.com';
|
||||
}
|
||||
|
||||
// 支付宝JSAPI支付需要buyer_id(可选)
|
||||
if ($service == 'pay.alipay.jspay') {
|
||||
$params['is_raw'] = 0;
|
||||
$params['quit_url'] = $params['notify_url'];
|
||||
$params['buyer_id'] = '';
|
||||
}
|
||||
|
||||
Db::startTrans();
|
||||
try {
|
||||
// 签名
|
||||
$secret = Env::get('payment.key');
|
||||
$params['sign_type'] = 'MD5';
|
||||
$params['sign'] = PaymentUtil::generateSign($params, $secret, 'MD5');
|
||||
|
||||
$url = Env::get('payment.url');
|
||||
if (empty($url)) {
|
||||
throw new \Exception('支付网关地址未配置');
|
||||
}
|
||||
|
||||
// 创建订单
|
||||
Order::create([
|
||||
'mchId' => $params['mch_id'],
|
||||
'companyId' => isset($order['companyId']) ? $order['companyId'] : 0,
|
||||
'userId' => isset($order['userId']) ? $order['userId'] : 0,
|
||||
'orderType' => isset($order['orderType']) ? $order['orderType'] : 1,
|
||||
'status' => 0,
|
||||
'goodsId' => isset($order['goodsId']) ? $order['goodsId'] : 0,
|
||||
'goodsName' => isset($order['goodsName']) ? $order['goodsName'] : '',
|
||||
'money' => isset($order['money']) ? $order['money'] : 0,
|
||||
'goodsSpecs' => isset($order['goodsSpecs']) ? json_encode($order['goodsSpecs'], 256) : json_encode([]),
|
||||
'orderNo' => isset($order['orderNo']) ? $order['orderNo'] : '',
|
||||
'ip' => Request::ip(),
|
||||
'nonceStr' => isset($order['nonceStr']) ? $order['nonceStr'] : '',
|
||||
'createTime' => time(),
|
||||
]);
|
||||
|
||||
// XML POST 请求
|
||||
$xmlBody = $this->arrayToXml($params);
|
||||
$response = $this->postXml($url, $xmlBody);
|
||||
$parsed = $this->parseXmlOrRaw($response);
|
||||
|
||||
|
||||
if ($parsed['status'] == 0 && $parsed['result_code'] == 0) {
|
||||
Db::commit();
|
||||
|
||||
// 根据service类型返回不同的数据格式(仅返回接口文档中的字段)
|
||||
$responseData = null;
|
||||
if ($service == 'unified.trade.native') {
|
||||
// 扫码支付返回二维码URL
|
||||
$responseData = $parsed['code_img_url'] ?? '';
|
||||
} elseif ($service == 'pay.weixin.jspay') {
|
||||
// 微信JSAPI支付返回支付参数(仅返回接口文档中存在的字段)
|
||||
$responseData = [];
|
||||
if (isset($parsed['appid'])) $responseData['appid'] = $parsed['appid'];
|
||||
if (isset($parsed['time_stamp'])) $responseData['time_stamp'] = $parsed['time_stamp'];
|
||||
if (isset($parsed['nonce_str'])) $responseData['nonce_str'] = $parsed['nonce_str'];
|
||||
if (isset($parsed['package'])) $responseData['package'] = $parsed['package'];
|
||||
if (isset($parsed['sign_type'])) $responseData['sign_type'] = $parsed['sign_type'];
|
||||
if (isset($parsed['pay_sign'])) $responseData['pay_sign'] = $parsed['pay_sign'];
|
||||
} elseif ($service == 'pay.alipay.jspay') {
|
||||
// 支付宝JSAPI支付返回订单信息(仅返回接口文档中存在的字段)
|
||||
$responseData = [];
|
||||
if (isset($parsed['order_info'])) $responseData['order_info'] = $parsed['order_info'];
|
||||
if (isset($parsed['order_string'])) $responseData['order_string'] = $parsed['order_string'];
|
||||
}
|
||||
|
||||
return json_encode(['code' => 200, 'msg' => '订单创建成功', 'data' => $responseData]);
|
||||
} else {
|
||||
Db::rollback();
|
||||
return json_encode(['code' => 500, 'msg' => '订单创建失败:' . ($parsed['err_msg'] ?? '未知错误')]);
|
||||
}
|
||||
|
||||
} catch (\Exception $e) {
|
||||
Db::rollback();
|
||||
return json_encode(['code' => 500, 'msg' => '订单创建失败:' . $e->getMessage()]);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* POST 请求(x-www-form-urlencoded)
|
||||
*/
|
||||
protected function httpPost(string $url, array $params, array $headers = [])
|
||||
{
|
||||
if (!function_exists('requestCurl')) {
|
||||
throw new \RuntimeException('requestCurl 未定义');
|
||||
}
|
||||
return requestCurl($url, $params, 'POST', $headers, 'dataBuild');
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析响应
|
||||
*/
|
||||
protected function parseResponse($response)
|
||||
{
|
||||
if ($response === '' || $response === null) {
|
||||
return '';
|
||||
}
|
||||
$decoded = json_decode($response, true);
|
||||
if (json_last_error() === JSON_ERROR_NONE) {
|
||||
return $decoded;
|
||||
}
|
||||
if (strpos($response, '=') !== false && strpos($response, '&') !== false) {
|
||||
$arr = [];
|
||||
foreach (explode('&', $response) as $pair) {
|
||||
if ($pair === '') continue;
|
||||
$kv = explode('=', $pair, 2);
|
||||
$arr[$kv[0]] = $kv[1] ?? '';
|
||||
}
|
||||
return $arr;
|
||||
}
|
||||
return $response;
|
||||
}
|
||||
|
||||
/**
|
||||
* 以 XML 方式 POST(text/xml)
|
||||
*/
|
||||
protected function postXml(string $url, string $xml)
|
||||
{
|
||||
$ch = curl_init();
|
||||
curl_setopt($ch, CURLOPT_URL, $url);
|
||||
curl_setopt($ch, CURLOPT_POST, 1);
|
||||
curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
|
||||
curl_setopt($ch, CURLOPT_TIMEOUT, 30);
|
||||
curl_setopt($ch, CURLOPT_HTTPHEADER, [
|
||||
'Content-Type: text/xml; charset=UTF-8'
|
||||
]);
|
||||
curl_setopt($ch, CURLOPT_POSTFIELDS, $xml);
|
||||
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
|
||||
curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, false);
|
||||
$res = curl_exec($ch);
|
||||
curl_close($ch);
|
||||
return $res;
|
||||
}
|
||||
|
||||
/**
|
||||
* 数组转 XML(按 ASCII 升序,字符串走 CDATA)
|
||||
*/
|
||||
protected function arrayToXml(array $data): string
|
||||
{
|
||||
// 过滤空值
|
||||
$filtered = [];
|
||||
foreach ($data as $k => $v) {
|
||||
if ($v === '' || $v === null) continue;
|
||||
$filtered[$k] = $v;
|
||||
}
|
||||
ksort($filtered, SORT_STRING);
|
||||
|
||||
$xml = '<xml>';
|
||||
foreach ($filtered as $key => $value) {
|
||||
if (is_numeric($value)) {
|
||||
$xml .= "<{$key}>{$value}</{$key}>";
|
||||
} else {
|
||||
$xml .= "<{$key}><![CDATA[{$value}]]></{$key}>";
|
||||
}
|
||||
}
|
||||
$xml .= '</xml>';
|
||||
return $xml;
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析 XML 响应
|
||||
*/
|
||||
protected function parseXmlOrRaw($response)
|
||||
{
|
||||
if (!is_string($response) || $response === '') {
|
||||
return $response;
|
||||
}
|
||||
libxml_use_internal_errors(true);
|
||||
$xml = simplexml_load_string($response, 'SimpleXMLElement', LIBXML_NOCDATA);
|
||||
if ($xml !== false) {
|
||||
$json = json_encode($xml, JSON_UNESCAPED_UNICODE);
|
||||
return json_decode($json, true);
|
||||
}
|
||||
return $response;
|
||||
}
|
||||
|
||||
/**
|
||||
* 支付结果异步通知
|
||||
* - 威富通回调为 XML;需校验签名与业务字段并更新订单
|
||||
* - 支持扫码付款、微信支付、支付宝支付的通知
|
||||
* - 回应:成功返回XML格式SUCCESS,失败返回XML格式FAIL
|
||||
* @return string XML响应
|
||||
*/
|
||||
public function notify()
|
||||
{
|
||||
$rawBody = file_get_contents('php://input');
|
||||
$payload = $this->parseXmlOrRaw($rawBody);
|
||||
if (!is_array($payload) || empty($payload)) {
|
||||
\think\facade\Log::error('支付通知:XML解析错误', ['rawBody' => $rawBody]);
|
||||
return '<?xml version="1.0" encoding="UTF-8"?><xml><return_code><![CDATA[FAIL]]></return_code><return_msg><![CDATA[XML解析错误]]></return_msg></xml>';
|
||||
}
|
||||
|
||||
// 验证签名
|
||||
$secret = Env::get('payment.key');
|
||||
if (!empty($secret) && isset($payload['sign'])) {
|
||||
$signType = $payload['sign_type'] ?? 'MD5';
|
||||
if (!PaymentUtil::verifySign($payload, $secret, $signType)) {
|
||||
\think\facade\Log::error('支付通知:签名验证失败', ['payload' => $payload]);
|
||||
return '<?xml version="1.0" encoding="UTF-8"?><xml><return_code><![CDATA[FAIL]]></return_code><return_msg><![CDATA[签名验证失败]]></return_msg></xml>';
|
||||
}
|
||||
}
|
||||
|
||||
// 检查通信状态
|
||||
if (isset($payload['status']) && $payload['status'] != 0) {
|
||||
$errMsg = $payload['err_msg'] ?? '通信失败';
|
||||
\think\facade\Log::error('支付通知:通信失败', ['payload' => $payload]);
|
||||
return '<?xml version="1.0" encoding="UTF-8"?><xml><return_code><![CDATA[FAIL]]></return_code><return_msg><![CDATA[' . $errMsg . ']]></return_msg></xml>';
|
||||
}
|
||||
|
||||
// 检查业务结果
|
||||
if (isset($payload['result_code']) && $payload['result_code'] != 0) {
|
||||
$errMsg = $payload['err_msg'] ?? '业务处理失败';
|
||||
\think\facade\Log::error('支付通知:业务处理失败', ['payload' => $payload]);
|
||||
return '<?xml version="1.0" encoding="UTF-8"?><xml><return_code><![CDATA[FAIL]]></return_code><return_msg><![CDATA[' . $errMsg . ']]></return_msg></xml>';
|
||||
}
|
||||
|
||||
|
||||
// 业务处理:更新订单
|
||||
Db::startTrans();
|
||||
try {
|
||||
$outTradeNo = $payload['out_trade_no'] ?? '';
|
||||
$pay_result = $payload['pay_result'] ?? 0;
|
||||
$time_end = $payload['time_end'] ?? '';
|
||||
|
||||
if (empty($outTradeNo)) {
|
||||
Db::rollback();
|
||||
\think\facade\Log::error('支付通知:订单号为空', ['payload' => $payload]);
|
||||
return '<?xml version="1.0" encoding="UTF-8"?><xml><return_code><![CDATA[FAIL]]></return_code><return_msg><![CDATA[订单号为空]]></return_msg></xml>';
|
||||
}
|
||||
|
||||
$order = Order::where('orderNo', $outTradeNo)->find();
|
||||
if (!$order) {
|
||||
Db::rollback();
|
||||
\think\facade\Log::error('支付通知:订单不存在', ['out_trade_no' => $outTradeNo]);
|
||||
return '<?xml version="1.0" encoding="UTF-8"?><xml><return_code><![CDATA[FAIL]]></return_code><return_msg><![CDATA[订单不存在]]></return_msg></xml>';
|
||||
}
|
||||
|
||||
// 如果订单已支付,直接返回成功(防止重复处理)
|
||||
if ($order->status == 1) {
|
||||
Db::rollback();
|
||||
return '<?xml version="1.0" encoding="UTF-8"?><xml><return_code><![CDATA[SUCCESS]]></return_code><return_msg><![CDATA[OK]]></return_msg></xml>';
|
||||
}
|
||||
|
||||
if ($pay_result != 0) {
|
||||
$order->payInfo = $payload['pay_info'] ?? '支付失败';
|
||||
$order->status = 3;
|
||||
$order->save();
|
||||
Db::commit();
|
||||
\think\facade\Log::error('支付通知:支付失败', ['orderNo' => $outTradeNo, 'pay_info' => $payload['pay_info'] ?? '']);
|
||||
return '<?xml version="1.0" encoding="UTF-8"?><xml><return_code><![CDATA[FAIL]]></return_code><return_msg><![CDATA[' . ($payload['pay_info'] ?? '支付失败') . ']]></return_msg></xml>';
|
||||
}
|
||||
|
||||
// 根据trade_type判断支付方式
|
||||
$tradeType = $payload['trade_type'] ?? '';
|
||||
if (strpos($tradeType, 'wechat') !== false || strpos($tradeType, 'weixin') !== false) {
|
||||
$order->payType = 1; // 微信支付
|
||||
} elseif (strpos($tradeType, 'alipay') !== false) {
|
||||
$order->payType = 2; // 支付宝支付
|
||||
} else {
|
||||
// 默认根据原有逻辑判断
|
||||
$order->payType = $tradeType == 'pay.wechat.jspay' ? 1 : 2;
|
||||
}
|
||||
|
||||
$order->status = 1;
|
||||
$order->payTime = $this->parsePayTime($time_end);
|
||||
$order->transactionId = $payload['transaction_id'] ?? '';
|
||||
$order->save();
|
||||
//订单处理
|
||||
$this->processOrder($order);
|
||||
Db::commit();
|
||||
|
||||
// 返回成功响应(XML格式)
|
||||
return '<?xml version="1.0" encoding="UTF-8"?><xml><return_code><![CDATA[SUCCESS]]></return_code><return_msg><![CDATA[OK]]></return_msg></xml>';
|
||||
} catch (\Exception $e) {
|
||||
Db::rollback();
|
||||
\think\facade\Log::error('支付通知:处理异常', ['error' => $e->getMessage(), 'trace' => $e->getTraceAsString()]);
|
||||
return '<?xml version="1.0" encoding="UTF-8"?><xml><return_code><![CDATA[FAIL]]></return_code><return_msg><![CDATA[处理异常]]></return_msg></xml>';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析威富通时间(yyyyMMddHHmmss)为时间戳
|
||||
*/
|
||||
protected function parsePayTime(string $timeEnd)
|
||||
{
|
||||
if ($timeEnd === '') {
|
||||
return 0;
|
||||
}
|
||||
// 期望格式:20250102153045
|
||||
if (preg_match('/^\\d{14}$/', $timeEnd) !== 1) {
|
||||
return 0;
|
||||
}
|
||||
$dt = \DateTime::createFromFormat('YmdHis', $timeEnd, new \DateTimeZone('Asia/Shanghai'));
|
||||
return $dt ? $dt->getTimestamp() : 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询订单(威富通 unified.trade.query)
|
||||
* - 入参:商户订单号或平台交易号
|
||||
* - 出参:统一 JSON 格式,包含交易状态与关键信息
|
||||
* @param array $query
|
||||
* - out_trade_no: string 商户订单号(与 transaction_id 二选一)
|
||||
* - transaction_id: string 平台交易号(与 out_trade_no 二选一)
|
||||
* @return \think\response\Json
|
||||
*/
|
||||
public function queryOrder($orderNo = '')
|
||||
{
|
||||
if (empty($orderNo)) {
|
||||
return json(['code' => 422, 'msg' => '订单号缺失']);
|
||||
}
|
||||
|
||||
$params = [
|
||||
'service' => 'unified.trade.query',
|
||||
'mch_id' => Env::get('payment.mchId'),
|
||||
'out_trade_no' => $orderNo ?: null,
|
||||
'nonce_str' => PaymentUtil::generateNonceStr(),
|
||||
'sign_type' => 'MD5',
|
||||
];
|
||||
|
||||
// 过滤空值后签名
|
||||
$secret = Env::get('payment.key');
|
||||
if (empty($secret)) {
|
||||
return json_encode(['code' => 500, 'msg' => '支付密钥未配置']);
|
||||
}
|
||||
|
||||
$filtered = [];
|
||||
foreach ($params as $k => $v) {
|
||||
if ($v === '' || $v === null) continue;
|
||||
$filtered[$k] = $v;
|
||||
}
|
||||
$filtered['sign'] = PaymentUtil::generateSign($filtered, $secret, $filtered['sign_type']);
|
||||
|
||||
$url = Env::get('payment.url');
|
||||
if (empty($url)) {
|
||||
return json_encode(['code' => 500, 'msg' => '支付网关地址未配置']);
|
||||
}
|
||||
|
||||
// 请求网关
|
||||
$xmlBody = $this->arrayToXml($filtered);
|
||||
$response = $this->postXml($url, $xmlBody);
|
||||
$parsed = $this->parseXmlOrRaw($response);
|
||||
|
||||
if (!is_array($parsed)) {
|
||||
return json_encode(['code' => 500, 'msg' => '响应解析失败', 'data' => $response]);
|
||||
}
|
||||
|
||||
if ($parsed['status'] != 0) {
|
||||
return json_encode(['code' => 500, 'msg' => '通信失败']);
|
||||
}
|
||||
|
||||
|
||||
if ($parsed['result_code'] == 0) {
|
||||
$order = Order::where('orderNo', $orderNo)->lock(true)->find();
|
||||
if (empty($order)) {
|
||||
return json_encode(['code' => 500, 'msg' => '订单不存在']);
|
||||
}
|
||||
|
||||
if ($order['status'] == 1) {
|
||||
return json_encode(['code' => 200, 'msg' => '支付成功']);
|
||||
}
|
||||
|
||||
|
||||
$tradeState = $parsed['trade_state'] ?? '';
|
||||
$resp = [
|
||||
'trade_state' => $tradeState,
|
||||
'trade_state_desc' => $parsed['trade_state_desc'] ?? '',
|
||||
'transaction_id' => $parsed['transaction_id'] ?? '',
|
||||
'out_trade_no' => $parsed['out_trade_no'] ?? $orderNo,
|
||||
'total_fee' => isset($parsed['total_fee']) ? (int)$parsed['total_fee'] : null,
|
||||
'time_end' => $parsed['time_end'] ?? '',
|
||||
'buyer_logon_id' => $parsed['buyer_logon_id'] ?? '',
|
||||
'bank_type' => $parsed['bank_type'] ?? '',
|
||||
];
|
||||
|
||||
// 若已支付,同步本地订单
|
||||
if ($tradeState == 'SUCCESS') {
|
||||
Db::startTrans();
|
||||
try {
|
||||
/** @var Order|null $order */
|
||||
$order = Order::where('orderNo', $resp['out_trade_no'])->lock(true)->find();
|
||||
if ($order) {
|
||||
$paidAt = $this->parsePayTime($resp['time_end'] ?? '') ?: time();
|
||||
if ($order['status'] != 1) {
|
||||
$order->save([
|
||||
'status' => 1,
|
||||
'transactionId' => $resp['transaction_id'] ?? '',
|
||||
'payTime' => $paidAt,
|
||||
'updateTime' => time(),
|
||||
]);
|
||||
}
|
||||
}
|
||||
//订单处理
|
||||
$this->processOrder($order);
|
||||
Db::commit();
|
||||
return json_encode(['code' => 200, 'msg' => '支付成功']);
|
||||
} catch (\Exception $e) {
|
||||
Db::rollback();
|
||||
return json_encode(['code' => 500, 'msg' => '付款失败' . $e->getMessage()]);
|
||||
}
|
||||
} else {
|
||||
$order = Order::where('orderNo', $resp['out_trade_no'])->lock(true)->find();
|
||||
if ($order) {
|
||||
$order->status = 3;
|
||||
$order->payInfo = $resp['trade_state_desc'] ?? '';
|
||||
$order->save();
|
||||
}
|
||||
return json_encode(['code' => 500, 'msg' => '支付失败', 'data' => $resp]);
|
||||
}
|
||||
|
||||
} else {
|
||||
return json_encode(['code' => 500, 'msg' => '通信失败']);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public function processOrder($order = [])
|
||||
{
|
||||
if (empty($order)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
switch ($order['orderType']) {
|
||||
case 1:
|
||||
// 处理购买算力
|
||||
// 查询用户信息,判断是否为管理员(需要同时匹配userId和companyId)
|
||||
$user = User::where([
|
||||
'id' => $order->userId,
|
||||
'companyId' => $order->companyId
|
||||
])->find();
|
||||
$isAdmin = (!empty($user) && isset($user->isAdmin) && $user->isAdmin == 1) ? 1 : 0;
|
||||
|
||||
$token = TokensCompany::where(['companyId' => $order->companyId,'userId' => $order->userId])->find();
|
||||
$goodsSpecs = json_decode($order->goodsSpecs, true);
|
||||
if (!empty($token)) {
|
||||
$token->tokens = $token->tokens + $goodsSpecs['tokens'];
|
||||
$token->updateTime = time();
|
||||
$token->save();
|
||||
$newTokens = $token->tokens;
|
||||
} else {
|
||||
$tokensCompany = new TokensCompany();
|
||||
$tokensCompany->userId = $order->userId;
|
||||
$tokensCompany->companyId = $order->companyId;
|
||||
$tokensCompany->tokens = $goodsSpecs['tokens'];
|
||||
$tokensCompany->isAdmin = $isAdmin;
|
||||
$tokensCompany->createTime = time();
|
||||
$tokensCompany->updateTime = time();
|
||||
$tokensCompany->save();
|
||||
$newTokens = $tokensCompany->tokens;
|
||||
}
|
||||
//添加记录
|
||||
$record = new TokensRecord();
|
||||
$record->companyId = $order->companyId;
|
||||
$record->userId = $order->userId;
|
||||
$record->type = 1;
|
||||
$record->form = 5;
|
||||
$record->wechatAccountId = 0;
|
||||
$record->friendIdOrGroupId = 0;
|
||||
$record->remarks = '购买算力【' . $goodsSpecs['name'] . '】';
|
||||
$record->tokens = $goodsSpecs['tokens'];
|
||||
$record->balanceTokens = $newTokens;
|
||||
$record->createTime = time();
|
||||
$record->save();
|
||||
break;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
}
|
||||
39
application/common/controller/SendCodeController.php
Normal file
39
application/common/controller/SendCodeController.php
Normal file
@@ -0,0 +1,39 @@
|
||||
<?php
|
||||
|
||||
namespace app\common\controller;
|
||||
|
||||
use library\ResponseHelper;
|
||||
use think\facade\Request;
|
||||
|
||||
/**
|
||||
* 认证控制器
|
||||
* 处理用户登录和身份验证
|
||||
*/
|
||||
class SendCodeController extends BaseController
|
||||
{
|
||||
/**
|
||||
* 发送验证码
|
||||
* @return \think\response\Json
|
||||
*/
|
||||
public function index()
|
||||
{
|
||||
$params = $this->request->only(['account', 'type']);
|
||||
|
||||
// 参数验证
|
||||
$validate = validate('common/Auth');
|
||||
if (!$validate->scene('send_code')->check($params)) {
|
||||
return ResponseHelper::error($validate->getError());
|
||||
}
|
||||
|
||||
try {
|
||||
// 调用发送验证码服务
|
||||
$result = $this->authService->sendLoginCode(
|
||||
$params['account'],
|
||||
$params['type']
|
||||
);
|
||||
return ResponseHelper::success($result, '验证码发送成功');
|
||||
} catch (\Exception $e) {
|
||||
return ResponseHelper::error($e->getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
59
application/common/middleware/AllowCrossDomain.php
Normal file
59
application/common/middleware/AllowCrossDomain.php
Normal file
@@ -0,0 +1,59 @@
|
||||
<?php
|
||||
namespace app\common\middleware;
|
||||
|
||||
/**
|
||||
* 跨域请求中间件
|
||||
*/
|
||||
class AllowCrossDomain
|
||||
{
|
||||
/**
|
||||
* 处理跨域请求
|
||||
* @param \think\Request $request
|
||||
* @param \Closure $next
|
||||
* @return mixed
|
||||
*/
|
||||
public function handle($request, \Closure $next)
|
||||
{
|
||||
// // 获取当前请求的域名
|
||||
// $origin = $request->header('origin');
|
||||
//
|
||||
// // 当请求使用 credentials 模式时,不能使用通配符
|
||||
// // 必须指定具体的域名或提取请求中的 Origin
|
||||
// $allowOrigin = '*';
|
||||
// if ($origin) {
|
||||
// // 如果需要限制特定域名,可以在这里判断
|
||||
// // 以下是允许的域名列表,如果请求来自这些域名之一,则允许跨域
|
||||
// $allowDomains = [ /* */ ];
|
||||
//
|
||||
// // 如果请求来源在允许列表中,直接使用该源
|
||||
// if (in_array($origin, $allowDomains)) {
|
||||
// $allowOrigin = $origin;
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// // 设置允许的请求头信息
|
||||
// $allowHeaders = [
|
||||
// 'Authorization', 'Content-Type', 'If-Match', 'If-Modified-Since',
|
||||
// 'If-None-Match', 'If-Unmodified-Since', 'X-Requested-With',
|
||||
// 'X-Token', 'X-Api-Token', 'Accept', 'Origin'
|
||||
// ];
|
||||
//
|
||||
// $response = $next($request);
|
||||
//
|
||||
// // 添加跨域响应头
|
||||
// $response->header([
|
||||
// 'Access-Control-Allow-Origin' => $allowOrigin,
|
||||
// 'Access-Control-Allow-Headers' => implode(', ', $allowHeaders),
|
||||
// 'Access-Control-Allow-Methods' => 'GET, POST, PUT, DELETE, OPTIONS',
|
||||
// 'Access-Control-Allow-Credentials' => 'true',
|
||||
// 'Access-Control-Max-Age' => '86400',
|
||||
// ]);
|
||||
//
|
||||
// // 对于预检请求,直接返回成功响应
|
||||
// if ($request->method(true) == 'OPTIONS') {
|
||||
// return response()->code(200);
|
||||
// }
|
||||
//
|
||||
// return $response;
|
||||
}
|
||||
}
|
||||
49
application/common/middleware/jwt.php
Normal file
49
application/common/middleware/jwt.php
Normal file
@@ -0,0 +1,49 @@
|
||||
<?php
|
||||
namespace app\common\middleware;
|
||||
|
||||
use app\common\util\JwtUtil;
|
||||
use think\facade\Log;
|
||||
|
||||
/**
|
||||
* JWT认证中间件
|
||||
*/
|
||||
class jwt
|
||||
{
|
||||
/**
|
||||
* 处理请求
|
||||
* @param \think\Request $request
|
||||
* @param \Closure $next
|
||||
* @return mixed
|
||||
*/
|
||||
public function handle($request, \Closure $next)
|
||||
{
|
||||
// 获取Token
|
||||
$token = JwtUtil::getRequestToken();
|
||||
|
||||
// 验证Token
|
||||
if (!$token) {
|
||||
return json([
|
||||
'code' => 401,
|
||||
'msg' => '未授权访问,缺少有效的身份凭证',
|
||||
'data' => null
|
||||
])->header(['Content-Type' => 'application/json; charset=utf-8']);
|
||||
}
|
||||
|
||||
$payload = JwtUtil::verifyToken($token);
|
||||
if (!$payload) {
|
||||
return json([
|
||||
'code' => 401,
|
||||
'msg' => '授权已过期或无效',
|
||||
'data' => null
|
||||
])->header(['Content-Type' => 'application/json; charset=utf-8']);
|
||||
}
|
||||
|
||||
// 将用户信息附加到请求中
|
||||
$request->userInfo = $payload;
|
||||
|
||||
// 写入日志
|
||||
Log::info('JWT认证通过', ['user_id' => $payload['id'] ?? 0, 'username' => $payload['username'] ?? '']);
|
||||
|
||||
return $next($request);
|
||||
}
|
||||
}
|
||||
31
application/common/model/Administrator.php
Normal file
31
application/common/model/Administrator.php
Normal file
@@ -0,0 +1,31 @@
|
||||
<?php
|
||||
|
||||
namespace app\common\model;
|
||||
|
||||
use think\Model;
|
||||
use think\model\concern\SoftDelete;
|
||||
|
||||
/**
|
||||
* 超级管理员模型类
|
||||
*/
|
||||
class Administrator extends Model
|
||||
{
|
||||
use SoftDelete;
|
||||
|
||||
const MASTER_ID = 1;
|
||||
|
||||
// 设置数据表名
|
||||
protected $name = 'administrators';
|
||||
|
||||
// 自动写入时间戳
|
||||
protected $autoWriteTimestamp = true;
|
||||
protected $createTime = 'createTime';
|
||||
protected $updateTime = 'updateTime';
|
||||
protected $deleteTime = 'deleteTime';
|
||||
protected $defaultSoftDelete = 0;
|
||||
|
||||
// 隐藏字段
|
||||
protected $hidden = [
|
||||
'password'
|
||||
];
|
||||
}
|
||||
23
application/common/model/AdministratorPermissions.php
Normal file
23
application/common/model/AdministratorPermissions.php
Normal file
@@ -0,0 +1,23 @@
|
||||
<?php
|
||||
namespace app\common\model;
|
||||
|
||||
use think\Model;
|
||||
use think\model\concern\SoftDelete;
|
||||
|
||||
/**
|
||||
* 超级管理员权限配置模型类
|
||||
*/
|
||||
class AdministratorPermissions extends Model
|
||||
{
|
||||
use SoftDelete;
|
||||
|
||||
// 设置数据表名
|
||||
protected $name = 'administrator_permissions';
|
||||
|
||||
// 自动写入时间戳
|
||||
protected $autoWriteTimestamp = true;
|
||||
protected $createTime = 'createTime';
|
||||
protected $updateTime = 'updateTime';
|
||||
protected $deleteTime = 'deleteTime';
|
||||
protected $defaultSoftDelete = 0;
|
||||
}
|
||||
23
application/common/model/Attachment.php
Normal file
23
application/common/model/Attachment.php
Normal file
@@ -0,0 +1,23 @@
|
||||
<?php
|
||||
|
||||
namespace app\common\model;
|
||||
|
||||
use think\Model;
|
||||
|
||||
class Attachment extends Model
|
||||
{
|
||||
// 设置表名
|
||||
protected $name = 'attachments';
|
||||
|
||||
// 自动写入时间戳
|
||||
protected $autoWriteTimestamp = true;
|
||||
protected $createTime = 'createTime';
|
||||
protected $updateTime = 'updateTime';
|
||||
protected $defaultSoftDelete = 0;
|
||||
|
||||
|
||||
public static function addAttachment($attachmentData)
|
||||
{
|
||||
return self::create($attachmentData);
|
||||
}
|
||||
}
|
||||
23
application/common/model/Company.php
Normal file
23
application/common/model/Company.php
Normal file
@@ -0,0 +1,23 @@
|
||||
<?php
|
||||
namespace app\common\model;
|
||||
|
||||
use think\Model;
|
||||
use think\model\concern\SoftDelete;
|
||||
|
||||
/**
|
||||
* 项目模型
|
||||
*/
|
||||
class Company extends Model
|
||||
{
|
||||
use SoftDelete;
|
||||
|
||||
// 设置数据表名
|
||||
protected $name = 'company';
|
||||
|
||||
// 自动写入时间戳
|
||||
protected $autoWriteTimestamp = true;
|
||||
protected $createTime = 'createTime';
|
||||
protected $updateTime = 'updateTime';
|
||||
protected $deleteTime = 'deleteTime';
|
||||
protected $defaultSoftDelete = 0;
|
||||
}
|
||||
24
application/common/model/Device.php
Normal file
24
application/common/model/Device.php
Normal file
@@ -0,0 +1,24 @@
|
||||
<?php
|
||||
|
||||
namespace app\common\model;
|
||||
|
||||
use think\Model;
|
||||
use think\model\concern\SoftDelete;
|
||||
|
||||
/**
|
||||
* 设备模型类
|
||||
*/
|
||||
class Device extends Model
|
||||
{
|
||||
use SoftDelete;
|
||||
|
||||
// 设置表名
|
||||
protected $name = 'device';
|
||||
|
||||
// 自动写入时间戳
|
||||
protected $autoWriteTimestamp = true;
|
||||
protected $createTime = 'createTime';
|
||||
protected $updateTime = 'updateTime';
|
||||
protected $deleteTime = 'deleteTime';
|
||||
protected $defaultSoftDelete = 0;
|
||||
}
|
||||
34
application/common/model/DeviceHandleLog.php
Normal file
34
application/common/model/DeviceHandleLog.php
Normal file
@@ -0,0 +1,34 @@
|
||||
<?php
|
||||
|
||||
namespace app\common\model;
|
||||
|
||||
use think\Model;
|
||||
|
||||
/**
|
||||
* 设备操作日志模型类
|
||||
*/
|
||||
class DeviceHandleLog extends Model
|
||||
{
|
||||
// 设置表名
|
||||
protected $name = 'device_handle_log';
|
||||
|
||||
// 自动写入时间戳
|
||||
protected $autoWriteTimestamp = true;
|
||||
protected $createTime = 'createTime';
|
||||
protected $updateTime = 'updateTime';
|
||||
|
||||
/**
|
||||
* 添加设备操作日志
|
||||
*
|
||||
* @param array $data 日志数据
|
||||
* @return int 新增日志ID
|
||||
*/
|
||||
public static function addLog(array $data): int
|
||||
{
|
||||
$log = new self();
|
||||
|
||||
$log->allowField(true)->save($data);
|
||||
|
||||
return $log->id;
|
||||
}
|
||||
}
|
||||
24
application/common/model/DeviceTaskconf.php
Normal file
24
application/common/model/DeviceTaskconf.php
Normal file
@@ -0,0 +1,24 @@
|
||||
<?php
|
||||
|
||||
namespace app\common\model;
|
||||
|
||||
use think\Model;
|
||||
use think\model\concern\SoftDelete;
|
||||
|
||||
/**
|
||||
* 设备任务配置模型类
|
||||
*/
|
||||
class DeviceTaskconf extends Model
|
||||
{
|
||||
use SoftDelete;
|
||||
|
||||
// 设置表名
|
||||
protected $name = 'device_taskconf';
|
||||
|
||||
// 自动写入时间戳
|
||||
protected $autoWriteTimestamp = true;
|
||||
protected $createTime = 'createTime';
|
||||
protected $updateTime = 'updateTime';
|
||||
protected $deleteTime = 'deleteTime';
|
||||
protected $defaultSoftDelete = 0;
|
||||
}
|
||||
28
application/common/model/DeviceUser.php
Normal file
28
application/common/model/DeviceUser.php
Normal file
@@ -0,0 +1,28 @@
|
||||
<?php
|
||||
|
||||
namespace app\common\model;
|
||||
|
||||
use think\Model;
|
||||
use think\model\concern\SoftDelete;
|
||||
|
||||
/**
|
||||
* 设备用户关联模型
|
||||
* 用于管理设备与操盘手(用户)的关联关系
|
||||
*/
|
||||
class DeviceUser extends Model
|
||||
{
|
||||
use SoftDelete;
|
||||
|
||||
/**
|
||||
* 数据表名
|
||||
* @var string
|
||||
*/
|
||||
protected $name = 'device_user';
|
||||
|
||||
// 自动写入时间戳
|
||||
protected $autoWriteTimestamp = true;
|
||||
protected $createTime = 'createTime';
|
||||
protected $updateTime = 'updateTime';
|
||||
protected $deleteTime = 'deleteTime';
|
||||
protected $defaultSoftDelete = 0;
|
||||
}
|
||||
37
application/common/model/DeviceWechatLogin.php
Normal file
37
application/common/model/DeviceWechatLogin.php
Normal file
@@ -0,0 +1,37 @@
|
||||
<?php
|
||||
|
||||
namespace app\common\model;
|
||||
|
||||
use think\Model;
|
||||
|
||||
/**
|
||||
* 微信好友模型类
|
||||
*/
|
||||
class DeviceWechatLogin extends Model
|
||||
{
|
||||
const ALIVE_WECHAT_ACTIVE = 1; // 微信在线
|
||||
const ALIVE_WECHAT_DIED = 0; // 微信离线
|
||||
|
||||
// 登录日志最新登录 alive = 1,旧数据全部设置0
|
||||
protected $name = 'device_wechat_login';
|
||||
|
||||
// 自动写入时间戳
|
||||
protected $autoWriteTimestamp = true;
|
||||
protected $createTime = 'createTime';
|
||||
protected $updateTime = 'updateTime';
|
||||
|
||||
/**
|
||||
* 获取设备最新登录记录的id
|
||||
*
|
||||
* @param array $deviceIds
|
||||
* @return array
|
||||
*/
|
||||
public static function getDevicesLatestLogin(array $deviceIds): array
|
||||
{
|
||||
return static::fieldRaw('max(id) as lastedId,deviceId')
|
||||
->whereIn('deviceId', $deviceIds)
|
||||
->group('deviceId')
|
||||
->select()
|
||||
->toArray();
|
||||
}
|
||||
}
|
||||
17
application/common/model/Menu.php
Normal file
17
application/common/model/Menu.php
Normal file
@@ -0,0 +1,17 @@
|
||||
<?php
|
||||
|
||||
namespace app\common\model;
|
||||
|
||||
use think\Model;
|
||||
|
||||
/**
|
||||
* 菜单模型类
|
||||
*/
|
||||
class Menu extends Model
|
||||
{
|
||||
const STATUS_ACTIVE = 1;
|
||||
const TOP_LEVEL = 0;
|
||||
|
||||
// 设置数据表名
|
||||
protected $name = 'menus';
|
||||
}
|
||||
14
application/common/model/Order.php
Normal file
14
application/common/model/Order.php
Normal file
@@ -0,0 +1,14 @@
|
||||
<?php
|
||||
|
||||
namespace app\common\model;
|
||||
|
||||
use think\Model;
|
||||
|
||||
|
||||
class Order extends Model
|
||||
{
|
||||
|
||||
// 设置数据表名
|
||||
protected $name = 'order';
|
||||
|
||||
}
|
||||
26
application/common/model/PlanScene.php
Normal file
26
application/common/model/PlanScene.php
Normal file
@@ -0,0 +1,26 @@
|
||||
<?php
|
||||
|
||||
namespace app\common\model;
|
||||
|
||||
use think\Model;
|
||||
use think\model\concern\SoftDelete;
|
||||
|
||||
/**
|
||||
* 获客场景模型类
|
||||
*/
|
||||
class PlanScene extends Model
|
||||
{
|
||||
use SoftDelete;
|
||||
|
||||
const STATUS_ACTIVE = 1; // 活动状态
|
||||
|
||||
// 设置表名
|
||||
protected $name = 'plan_scene';
|
||||
|
||||
// 自动写入时间戳
|
||||
protected $autoWriteTimestamp = true;
|
||||
protected $createTime = 'createTime';
|
||||
protected $updateTime = 'updateTime';
|
||||
protected $deleteTime = 'deleteTime';
|
||||
protected $defaultSoftDelete = 0;
|
||||
}
|
||||
19
application/common/model/TrafficPool.php
Normal file
19
application/common/model/TrafficPool.php
Normal file
@@ -0,0 +1,19 @@
|
||||
<?php
|
||||
|
||||
namespace app\common\model;
|
||||
|
||||
use think\Model;
|
||||
|
||||
/**
|
||||
* 流量池模型类
|
||||
*/
|
||||
class TrafficPool extends Model
|
||||
{
|
||||
// 设置数据表名
|
||||
protected $name = 'traffic_pool';
|
||||
|
||||
// 自动写入时间戳
|
||||
protected $autoWriteTimestamp = true;
|
||||
protected $createTime = 'createTime';
|
||||
protected $updateTime = 'updateTime';
|
||||
}
|
||||
27
application/common/model/TrafficSource.php
Normal file
27
application/common/model/TrafficSource.php
Normal file
@@ -0,0 +1,27 @@
|
||||
<?php
|
||||
|
||||
namespace app\common\model;
|
||||
|
||||
use think\Model;
|
||||
|
||||
/**
|
||||
* 流量池模型类
|
||||
*/
|
||||
class TrafficSource extends Model
|
||||
{
|
||||
const STATUS_PENDING = 1; // 待处理
|
||||
const STATUS_WORKING = 2; // 处理中
|
||||
const STATUS_PASSED = 3; // 已通过
|
||||
const STATUS_REFUSED = 4; // 已拒绝
|
||||
const STATUS_EXPIRED = 5; // 已过期
|
||||
const STATUS_CANCELED = 6; // 已取消
|
||||
|
||||
|
||||
// 设置数据表名
|
||||
protected $name = 'traffic_source';
|
||||
|
||||
// 自动写入时间戳
|
||||
protected $autoWriteTimestamp = true;
|
||||
protected $createTime = 'createTime';
|
||||
protected $updateTime = 'updateTime';
|
||||
}
|
||||
17
application/common/model/TrafficSourcePackage.php
Normal file
17
application/common/model/TrafficSourcePackage.php
Normal file
@@ -0,0 +1,17 @@
|
||||
<?php
|
||||
|
||||
namespace app\common\model;
|
||||
|
||||
use think\Model;
|
||||
|
||||
/**
|
||||
* 流量池模型类
|
||||
*/
|
||||
class TrafficSourcePackage extends Model
|
||||
{
|
||||
|
||||
// 设置数据表名
|
||||
protected $name = 'traffic_source_package';
|
||||
|
||||
|
||||
}
|
||||
17
application/common/model/TrafficSourcePackageItem.php
Normal file
17
application/common/model/TrafficSourcePackageItem.php
Normal file
@@ -0,0 +1,17 @@
|
||||
<?php
|
||||
|
||||
namespace app\common\model;
|
||||
|
||||
use think\Model;
|
||||
|
||||
/**
|
||||
* 流量池模型类
|
||||
*/
|
||||
class TrafficSourcePackageItem extends Model
|
||||
{
|
||||
|
||||
// 设置数据表名
|
||||
protected $name = 'traffic_source_package_item';
|
||||
|
||||
|
||||
}
|
||||
38
application/common/model/User.php
Normal file
38
application/common/model/User.php
Normal file
@@ -0,0 +1,38 @@
|
||||
<?php
|
||||
|
||||
namespace app\common\model;
|
||||
|
||||
use think\Model;
|
||||
use think\model\concern\SoftDelete;
|
||||
|
||||
class User extends Model
|
||||
{
|
||||
use SoftDelete;
|
||||
|
||||
const ADMIN_STP = 1; // 操盘手账号
|
||||
const ADMIN_OTP = 0;
|
||||
const NOT_USER = -1; // 非登录用户用于任务操作的(S2系统专属)
|
||||
const MASTER_USER = 1; // 操盘手
|
||||
const CUSTOMER_USER = 2; // 门店接待
|
||||
const STATUS_STOP = 0; // 禁用状态
|
||||
const STATUS_ACTIVE = 1; // 活动状态
|
||||
|
||||
/**
|
||||
* 数据表名
|
||||
* @var string
|
||||
*/
|
||||
protected $name = 'users';
|
||||
|
||||
// 自动写入时间戳
|
||||
protected $autoWriteTimestamp = true;
|
||||
protected $createTime = 'createTime';
|
||||
protected $updateTime = 'updateTime';
|
||||
protected $deleteTime = 'deleteTime';
|
||||
protected $defaultSoftDelete = 0;
|
||||
|
||||
/**
|
||||
* 隐藏属性
|
||||
* @var array
|
||||
*/
|
||||
protected $hidden = ['passwordMd5', 'deleteTime'];
|
||||
}
|
||||
19
application/common/model/WechatAccount.php
Normal file
19
application/common/model/WechatAccount.php
Normal file
@@ -0,0 +1,19 @@
|
||||
<?php
|
||||
|
||||
namespace app\common\model;
|
||||
|
||||
use think\Model;
|
||||
|
||||
/**
|
||||
* 微信账号模型类
|
||||
*/
|
||||
class WechatAccount extends Model
|
||||
{
|
||||
// 设置表名
|
||||
protected $name = 'wechat_account';
|
||||
|
||||
// 自动写入时间戳
|
||||
protected $autoWriteTimestamp = true;
|
||||
protected $createTime = 'createTime';
|
||||
protected $updateTime = 'updateTime';
|
||||
}
|
||||
44
application/common/model/WechatAccountScore.php
Normal file
44
application/common/model/WechatAccountScore.php
Normal file
@@ -0,0 +1,44 @@
|
||||
<?php
|
||||
|
||||
namespace app\common\model;
|
||||
|
||||
use think\Model;
|
||||
|
||||
/**
|
||||
* 微信账号评分记录模型类
|
||||
*/
|
||||
class WechatAccountScore extends Model
|
||||
{
|
||||
// 设置表名
|
||||
protected $name = 'wechat_account_score';
|
||||
protected $table = 's2_wechat_account_score';
|
||||
|
||||
// 自动写入时间戳
|
||||
protected $autoWriteTimestamp = false;
|
||||
|
||||
// 定义字段类型
|
||||
protected $type = [
|
||||
'accountId' => 'integer',
|
||||
'baseScore' => 'integer',
|
||||
'baseScoreCalculated' => 'integer',
|
||||
'baseInfoScore' => 'integer',
|
||||
'friendCountScore' => 'integer',
|
||||
'friendCount' => 'integer',
|
||||
'dynamicScore' => 'integer',
|
||||
'frequentCount' => 'integer',
|
||||
'frequentPenalty' => 'integer',
|
||||
'consecutiveNoFrequentDays' => 'integer',
|
||||
'noFrequentBonus' => 'integer',
|
||||
'banPenalty' => 'integer',
|
||||
'healthScore' => 'integer',
|
||||
'maxAddFriendPerDay' => 'integer',
|
||||
'isModifiedAlias' => 'integer',
|
||||
'isBanned' => 'integer',
|
||||
'lastFrequentTime' => 'integer',
|
||||
'lastNoFrequentTime' => 'integer',
|
||||
'baseScoreCalcTime' => 'integer',
|
||||
'createTime' => 'integer',
|
||||
'updateTime' => 'integer',
|
||||
];
|
||||
}
|
||||
|
||||
21
application/common/model/WechatCustomer.php
Normal file
21
application/common/model/WechatCustomer.php
Normal file
@@ -0,0 +1,21 @@
|
||||
<?php
|
||||
|
||||
namespace app\common\model;
|
||||
|
||||
use think\Model;
|
||||
|
||||
/**
|
||||
* 微信客服信息模型类
|
||||
*/
|
||||
class WechatCustomer extends Model
|
||||
{
|
||||
// 设置表名
|
||||
protected $name = 'wechat_customer';
|
||||
|
||||
// 自动进行 json_encode/json_decode
|
||||
protected $json = ['basic', 'weight', 'activity', 'friendShip'];
|
||||
|
||||
// 自动写入时间戳
|
||||
protected $autoWriteTimestamp = true;
|
||||
protected $updateTime = 'updateTime';
|
||||
}
|
||||
24
application/common/model/WechatFriendShip.php
Normal file
24
application/common/model/WechatFriendShip.php
Normal file
@@ -0,0 +1,24 @@
|
||||
<?php
|
||||
|
||||
namespace app\common\model;
|
||||
|
||||
use think\Model;
|
||||
use think\model\concern\SoftDelete;
|
||||
|
||||
/**
|
||||
* 微信好友模型类
|
||||
*/
|
||||
class WechatFriendShip extends Model
|
||||
{
|
||||
use SoftDelete;
|
||||
|
||||
// 设置表名
|
||||
protected $name = 'wechat_friendship';
|
||||
|
||||
// 自动写入时间戳
|
||||
protected $autoWriteTimestamp = true;
|
||||
protected $createTime = 'createTime';
|
||||
protected $updateTime = 'updateTime';
|
||||
protected $deleteTime = 'deleteTime';
|
||||
protected $defaultSoftDelete = 0;
|
||||
}
|
||||
17
application/common/model/WechatRestricts.php
Normal file
17
application/common/model/WechatRestricts.php
Normal file
@@ -0,0 +1,17 @@
|
||||
<?php
|
||||
|
||||
namespace app\common\model;
|
||||
|
||||
use think\Model;
|
||||
|
||||
/**
|
||||
* 微信风险受限记录
|
||||
*/
|
||||
class WechatRestricts extends Model
|
||||
{
|
||||
const LEVEL_WARNING = 2;
|
||||
const LEVEL_ERROR = 3;
|
||||
|
||||
// 设置数据表名
|
||||
protected $name = 'wechat_restricts';
|
||||
}
|
||||
20
application/common/model/WechatTag.php
Normal file
20
application/common/model/WechatTag.php
Normal file
@@ -0,0 +1,20 @@
|
||||
<?php
|
||||
|
||||
namespace app\common\model;
|
||||
|
||||
use think\Model;
|
||||
|
||||
/**
|
||||
* 微信好友模型类
|
||||
*/
|
||||
class WechatTag extends Model
|
||||
{
|
||||
// 设置表名
|
||||
protected $name = 'ck_wechat_tag';
|
||||
|
||||
// 自动写入时间戳
|
||||
protected $autoWriteTimestamp = true;
|
||||
protected $createTime = 'createTime';
|
||||
protected $updateTime = 'updateTime';
|
||||
protected $defaultSoftDelete = 0;
|
||||
}
|
||||
255
application/common/service/AuthService.php
Normal file
255
application/common/service/AuthService.php
Normal file
@@ -0,0 +1,255 @@
|
||||
<?php
|
||||
|
||||
namespace app\common\service;
|
||||
|
||||
use app\common\model\User as UserModel;
|
||||
use app\common\util\JwtUtil;
|
||||
use think\facade\Cache;
|
||||
use think\facade\Env;
|
||||
use think\facade\Log;
|
||||
|
||||
class AuthService
|
||||
{
|
||||
const TOKEN_EXPIRE = 86400 * 365;
|
||||
|
||||
protected $smsService;
|
||||
|
||||
/**
|
||||
* 获取用户基本信息
|
||||
*
|
||||
* @param string $account
|
||||
* @param int $typeId
|
||||
* @return UserModel
|
||||
*/
|
||||
protected function getUserProfileWithAccountAndType(string $account, int $typeId): UserModel
|
||||
{
|
||||
$user = UserModel::where(function ($query) use ($account) {
|
||||
$query->where('phone', $account)->whereOr('account', $account);
|
||||
})
|
||||
->where(function ($query) use ($typeId) {
|
||||
$query->where('status', 1)->where('typeId', $typeId);
|
||||
})->find();
|
||||
|
||||
return $user;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取用户信息
|
||||
*
|
||||
* @param string $account 账号(手机号)
|
||||
* @param string $password 密码(可能是加密后的)
|
||||
* @param int $typeId 身份信息
|
||||
* @return array|null
|
||||
*/
|
||||
protected function getUser(string $account, string $password, int $typeId): array
|
||||
{
|
||||
$user = $this->getUserProfileWithAccountAndType($account, $typeId);
|
||||
|
||||
if (!$user) {
|
||||
throw new \Exception('用户不存在或已禁用', 403);
|
||||
}
|
||||
|
||||
if ($user->passwordMd5 !== md5($password)) {
|
||||
throw new \Exception('账号或密码错误', 403);
|
||||
}
|
||||
|
||||
return $user->toArray();
|
||||
}
|
||||
|
||||
/**
|
||||
* 构造函数
|
||||
*/
|
||||
public function __construct()
|
||||
{
|
||||
$this->smsService = new SmsService();
|
||||
}
|
||||
|
||||
/**
|
||||
* 用户登录
|
||||
*
|
||||
* @param string $account 账号(手机号)
|
||||
* @param string $password 密码(可能是加密后的)
|
||||
* @param string $ip 登录IP
|
||||
* @return array
|
||||
* @throws \Exception
|
||||
*/
|
||||
public function login(string $account, string $password, int $typeId, string $ip)
|
||||
{
|
||||
// 获取用户信息
|
||||
$member = $this->getUser($account, $password, $typeId);
|
||||
|
||||
// 生成JWT令牌
|
||||
$token = JwtUtil::createToken($user, self::TOKEN_EXPIRE);
|
||||
$token_expired = time() + self::TOKEN_EXPIRE;
|
||||
|
||||
return compact('member', 'token', 'token_expired');
|
||||
}
|
||||
|
||||
/**
|
||||
* 手机号验证码登录
|
||||
*
|
||||
* @param string $account 手机号
|
||||
* @param string $code 验证码(可能是加密后的)
|
||||
* @param string $ip 登录IP
|
||||
* @param bool $isEncrypted 验证码是否已加密
|
||||
* @return array
|
||||
* @throws \Exception
|
||||
*/
|
||||
public function mobileLogin($account, $code, $ip, $isEncrypted = false)
|
||||
{
|
||||
// 验证验证码
|
||||
if (!$this->smsService->verifyCode($account, $code, 'login', $isEncrypted)) {
|
||||
Log::info('验证码验证失败', ['account' => $account, 'ip' => $ip, 'is_encrypted' => $isEncrypted]);
|
||||
throw new \Exception('验证码错误或已过期', 404);
|
||||
}
|
||||
|
||||
// 获取用户信息
|
||||
$user = User::getUserByMobile($account);
|
||||
if (empty($user)) {
|
||||
Log::info('用户不存在', ['account' => $account, 'ip' => $ip]);
|
||||
throw new \Exception('用户不存在', 404);
|
||||
}
|
||||
|
||||
// 生成JWT令牌
|
||||
$token = JwtUtil::createToken($user, self::TOKEN_EXPIRE);
|
||||
$expireTime = time() + self::TOKEN_EXPIRE;
|
||||
|
||||
// 记录登录成功
|
||||
Log::info('手机号登录成功', ['account' => $account, 'ip' => $ip]);
|
||||
|
||||
return [
|
||||
'token' => $token,
|
||||
'token_expired' => $expireTime,
|
||||
'member' => $user
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* 发送登录验证码
|
||||
*
|
||||
* @param string $account 手机号
|
||||
* @param string $type 验证码类型
|
||||
* @return array
|
||||
* @throws \Exception
|
||||
*/
|
||||
public function sendLoginCode($account, $type)
|
||||
{
|
||||
return $this->smsService->sendCode($account, $type);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取用户信息
|
||||
*
|
||||
* @param array $userInfo JWT中的用户信息
|
||||
* @return array
|
||||
* @throws \Exception
|
||||
*/
|
||||
public function getUserInfo($userInfo)
|
||||
{
|
||||
if (empty($userInfo)) {
|
||||
throw new \Exception('获取用户信息失败');
|
||||
}
|
||||
|
||||
// 移除不需要返回的字段
|
||||
unset($userInfo['exp']);
|
||||
unset($userInfo['iat']);
|
||||
|
||||
return $userInfo;
|
||||
}
|
||||
|
||||
/**
|
||||
* 刷新令牌
|
||||
*
|
||||
* @param array $userInfo JWT中的用户信息
|
||||
* @return array
|
||||
* @throws \Exception
|
||||
*/
|
||||
public function refreshToken($userInfo)
|
||||
{
|
||||
if (empty($userInfo)) {
|
||||
throw new \Exception('刷新令牌失败');
|
||||
}
|
||||
|
||||
// 移除过期时间信息
|
||||
unset($userInfo['exp']);
|
||||
unset($userInfo['iat']);
|
||||
|
||||
// 生成新令牌
|
||||
$token = JwtUtil::createToken($userInfo, self::TOKEN_EXPIRE);
|
||||
$expireTime = time() + self::TOKEN_EXPIRE;
|
||||
|
||||
return [
|
||||
'token' => $token,
|
||||
'token_expired' => $expireTime
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取系统授权信息,使用缓存存储10分钟
|
||||
*
|
||||
* @param bool $useCache 是否使用缓存
|
||||
* @return string
|
||||
*/
|
||||
public static function getSystemAuthorization($useCache = true)
|
||||
{
|
||||
// 定义缓存键名
|
||||
$cacheKey = 'system_authorization_token';
|
||||
|
||||
// 尝试从缓存获取授权信息
|
||||
$authorization = Cache::get($cacheKey);
|
||||
//$authorization = '';
|
||||
// 如果缓存中没有或已过期,则重新获取
|
||||
if (empty($authorization) || !$useCache) {
|
||||
try {
|
||||
// 从环境变量中获取API用户名和密码
|
||||
$username = Env::get('api.username', '');
|
||||
$password = Env::get('api.password', '');
|
||||
|
||||
if (empty($username) || empty($password)) {
|
||||
Log::error('缺少API用户名或密码配置');
|
||||
return '';
|
||||
}
|
||||
|
||||
// 构建登录参数
|
||||
$params = [
|
||||
'grant_type' => 'password',
|
||||
'username' => $username,
|
||||
'password' => $password
|
||||
];
|
||||
|
||||
// 获取API基础URL
|
||||
$baseUrl = Env::get('api.wechat_url', '');
|
||||
if (empty($baseUrl)) {
|
||||
Log::error('缺少API基础URL配置');
|
||||
return '';
|
||||
}
|
||||
|
||||
// 调用登录接口获取token
|
||||
// 设置请求头
|
||||
$headerData = ['client:system'];
|
||||
$header = setHeader($headerData, '', 'plain');
|
||||
$result = requestCurl($baseUrl . 'token', $params, 'POST', $header);
|
||||
$result_array = handleApiResponse($result);
|
||||
|
||||
if (isset($result_array['access_token']) && !empty($result_array['access_token'])) {
|
||||
$authorization = $result_array['access_token'];
|
||||
|
||||
// 存入缓存,有效期10分钟(600秒)
|
||||
Cache::set($cacheKey, $authorization, 600);
|
||||
Cache::set('system_refresh_token', $result_array['refresh_token'], 600);
|
||||
|
||||
Log::info('已重新获取系统授权信息并缓存');
|
||||
return $authorization;
|
||||
} else {
|
||||
Log::error('获取系统授权信息失败:' . ($response['message'] ?? '未知错误'));
|
||||
return '';
|
||||
}
|
||||
} catch (\Exception $e) {
|
||||
Log::error('获取系统授权信息异常:' . $e->getMessage());
|
||||
return '';
|
||||
}
|
||||
}
|
||||
|
||||
return $authorization;
|
||||
}
|
||||
}
|
||||
82
application/common/service/ClassTableService.php
Normal file
82
application/common/service/ClassTableService.php
Normal file
@@ -0,0 +1,82 @@
|
||||
<?php
|
||||
namespace app\common\service;
|
||||
|
||||
use library\ClassTable;
|
||||
use think\Container;
|
||||
|
||||
class ClassTableService
|
||||
{
|
||||
protected $app;
|
||||
protected $classTable;
|
||||
|
||||
public function __construct(Container $app)
|
||||
{
|
||||
$this->app = $app;
|
||||
$this->classTable = ClassTable::getSelfInstance();
|
||||
}
|
||||
|
||||
/**
|
||||
* 绑定实例或类到容器
|
||||
* @param string|array $alias
|
||||
* @param mixed $instance
|
||||
* @param string|null $tag
|
||||
*/
|
||||
public function bind($alias, $instance = null, string $tag = null)
|
||||
{
|
||||
$this->classTable->bind($alias, $instance, $tag);
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取实例
|
||||
* @param string|object $class
|
||||
* @param array $parameters
|
||||
* @return object
|
||||
*/
|
||||
public function getInstance($class, ...$parameters)
|
||||
{
|
||||
return $this->classTable->getInstance($class, ...$parameters);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取共享实例
|
||||
* @param string $alias
|
||||
* @param array $parameters
|
||||
* @return object|null
|
||||
*/
|
||||
public function getShared($alias, array $parameters = [])
|
||||
{
|
||||
return $this->classTable->getShared($alias, $parameters);
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据标签获取类
|
||||
* @param string $tag
|
||||
* @return array|null
|
||||
*/
|
||||
public function getClassByTag(string $tag)
|
||||
{
|
||||
return $this->classTable->getClassByTag($tag);
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查别名是否存在
|
||||
* @param string $alias
|
||||
* @return bool
|
||||
*/
|
||||
public function has(string $alias)
|
||||
{
|
||||
return $this->classTable->has($alias);
|
||||
}
|
||||
|
||||
/**
|
||||
* 复制实例
|
||||
* @param mixed $class
|
||||
* @param string|null $name
|
||||
* @return object
|
||||
*/
|
||||
public function copy($class, string $name = null)
|
||||
{
|
||||
return $this->classTable->copy($class, $name);
|
||||
}
|
||||
}
|
||||
203
application/common/service/SmsService.php
Normal file
203
application/common/service/SmsService.php
Normal file
@@ -0,0 +1,203 @@
|
||||
<?php
|
||||
|
||||
namespace app\common\service;
|
||||
|
||||
use think\facade\Cache;
|
||||
use think\facade\Log;
|
||||
|
||||
/**
|
||||
* 短信服务类
|
||||
*/
|
||||
class SmsService
|
||||
{
|
||||
/**
|
||||
* 验证码有效期(秒)
|
||||
*/
|
||||
const CODE_EXPIRE = 300;
|
||||
|
||||
/**
|
||||
* 验证码长度
|
||||
*/
|
||||
const CODE_LENGTH = 4;
|
||||
|
||||
/**
|
||||
* 发送验证码
|
||||
* @param string $mobile 手机号
|
||||
* @param string $type 验证码类型 (login, register)
|
||||
* @return array
|
||||
* @throws \Exception
|
||||
*/
|
||||
public function sendCode($mobile, $type)
|
||||
{
|
||||
// 检查发送频率限制
|
||||
$this->checkSendLimit($mobile, $type);
|
||||
|
||||
// 生成验证码
|
||||
$code = $this->generateCode();
|
||||
|
||||
// 缓存验证码
|
||||
$this->saveCode($mobile, $code, $type);
|
||||
|
||||
// 发送验证码(实际项目中对接短信平台)
|
||||
$this->doSend($mobile, $code, $type);
|
||||
|
||||
// 记录日志
|
||||
Log::info('发送验证码', [
|
||||
'mobile' => $mobile,
|
||||
'type' => $type,
|
||||
'code' => $code
|
||||
]);
|
||||
|
||||
return [
|
||||
'mobile' => $mobile,
|
||||
'expire' => self::CODE_EXPIRE,
|
||||
// 测试环境返回验证码,生产环境不应返回
|
||||
'code' => $code
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证验证码
|
||||
* @param string $mobile 手机号
|
||||
* @param string $code 验证码(可能是加密后的)
|
||||
* @param string $type 验证码类型
|
||||
* @param bool $isEncrypted 验证码是否已加密
|
||||
* @return bool
|
||||
*/
|
||||
public function verifyCode($mobile, $code, $type, $isEncrypted = false)
|
||||
{
|
||||
$cacheKey = $this->getCodeCacheKey($mobile, $type);
|
||||
$cacheCode = Cache::get($cacheKey);
|
||||
|
||||
if (!$cacheCode) {
|
||||
Log::info('验证码不存在或已过期', [
|
||||
'mobile' => $mobile,
|
||||
'type' => $type
|
||||
]);
|
||||
return false;
|
||||
}
|
||||
|
||||
// 验证码是否匹配
|
||||
$isValid = false;
|
||||
|
||||
if ($isEncrypted) {
|
||||
// 前端已加密,需要对缓存中的验证码进行相同的加密处理
|
||||
$encryptedCacheCode = $this->encryptCode($cacheCode);
|
||||
$isValid = hash_equals($encryptedCacheCode, $code);
|
||||
|
||||
// 记录日志
|
||||
Log::info('加密验证码验证', [
|
||||
'mobile' => $mobile,
|
||||
'cache_code' => $cacheCode,
|
||||
'encrypted_cache_code' => $encryptedCacheCode,
|
||||
'input_code' => $code,
|
||||
'is_valid' => $isValid
|
||||
]);
|
||||
} else {
|
||||
// 未加密,直接比较
|
||||
$isValid = ($cacheCode === $code);
|
||||
|
||||
// 记录日志
|
||||
Log::info('明文验证码验证', [
|
||||
'mobile' => $mobile,
|
||||
'cache_code' => $cacheCode,
|
||||
'input_code' => $code,
|
||||
'is_valid' => $isValid
|
||||
]);
|
||||
}
|
||||
|
||||
// 验证成功后删除缓存
|
||||
if ($isValid) {
|
||||
Cache::rm($cacheKey);
|
||||
}
|
||||
|
||||
return $isValid;
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查发送频率限制
|
||||
* @param string $mobile 手机号
|
||||
* @param string $type 验证码类型
|
||||
* @throws \Exception
|
||||
*/
|
||||
protected function checkSendLimit($mobile, $type)
|
||||
{
|
||||
$cacheKey = $this->getCodeCacheKey($mobile, $type);
|
||||
|
||||
// 检查是否存在未过期的验证码
|
||||
if (Cache::has($cacheKey)) {
|
||||
throw new \Exception('验证码已发送,请稍后再试');
|
||||
}
|
||||
|
||||
// 检查当日发送次数限制
|
||||
$limitKey = "sms_limit:{$mobile}:" . date('Ymd');
|
||||
$sendCount = Cache::get($limitKey, 0);
|
||||
|
||||
if ($sendCount >= 10) {
|
||||
throw new \Exception('今日发送次数已达上限');
|
||||
}
|
||||
|
||||
// 更新发送次数
|
||||
Cache::set($limitKey, $sendCount + 1, 86400);
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成随机验证码
|
||||
* @return string
|
||||
*/
|
||||
protected function generateCode()
|
||||
{
|
||||
// 生成4位数字验证码
|
||||
return sprintf("%0" . self::CODE_LENGTH . "d", mt_rand(0, pow(10, self::CODE_LENGTH) - 1));
|
||||
}
|
||||
|
||||
/**
|
||||
* 保存验证码到缓存
|
||||
* @param string $mobile 手机号
|
||||
* @param string $code 验证码
|
||||
* @param string $type 验证码类型
|
||||
*/
|
||||
protected function saveCode($mobile, $code, $type)
|
||||
{
|
||||
$cacheKey = $this->getCodeCacheKey($mobile, $type);
|
||||
Cache::set($cacheKey, $code, self::CODE_EXPIRE);
|
||||
}
|
||||
|
||||
/**
|
||||
* 执行发送验证码
|
||||
* @param string $mobile 手机号
|
||||
* @param string $code 验证码
|
||||
* @param string $type 验证码类型
|
||||
* @return bool
|
||||
*/
|
||||
protected function doSend($mobile, $code, $type)
|
||||
{
|
||||
// 实际项目中对接短信平台API
|
||||
// 这里仅做模拟,返回成功
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取验证码缓存键名
|
||||
* @param string $mobile 手机号
|
||||
* @param string $type 验证码类型
|
||||
* @return string
|
||||
*/
|
||||
protected function getCodeCacheKey($mobile, $type)
|
||||
{
|
||||
return "sms_code:{$mobile}:{$type}";
|
||||
}
|
||||
|
||||
/**
|
||||
* 加密验证码
|
||||
* 使用与前端相同的加密算法
|
||||
* @param string $code 原始验证码
|
||||
* @return string 加密后的验证码
|
||||
*/
|
||||
protected function encryptCode($code)
|
||||
{
|
||||
// 使用与前端相同的加密算法
|
||||
$salt = 'yishi_salt_2024'; // 与前端相同的盐值
|
||||
return hash('sha256', $code . $salt);
|
||||
}
|
||||
}
|
||||
1506
application/common/service/WechatAccountHealthScoreService.php
Normal file
1506
application/common/service/WechatAccountHealthScoreService.php
Normal file
File diff suppressed because it is too large
Load Diff
126
application/common/socket/Events.php
Normal file
126
application/common/socket/Events.php
Normal file
@@ -0,0 +1,126 @@
|
||||
<?php
|
||||
|
||||
namespace app\common\socket;
|
||||
|
||||
use app\common\Logger;
|
||||
use app\common\model\DeviceModel;
|
||||
use \GatewayWorker\Lib\Gateway;
|
||||
use Workerman\Worker;
|
||||
|
||||
/**
|
||||
* Worker 命令行服务类
|
||||
*/
|
||||
class Events {
|
||||
|
||||
const LOGGER = 'WS';
|
||||
|
||||
/**
|
||||
* onConnect 事件回调
|
||||
* 当客户端连接上gateway进程时(TCP三次握手完毕时)触发
|
||||
*
|
||||
* @access public
|
||||
* @param int $client_id
|
||||
* @return void
|
||||
*/
|
||||
public static function onConnect($client_id) {
|
||||
//echo '---------';
|
||||
}
|
||||
|
||||
/**
|
||||
* onWebSocketConnect 事件回调
|
||||
* 当客户端连接上gateway完成websocket握手时触发
|
||||
*
|
||||
* @param integer $client_id 断开连接的客户端client_id
|
||||
* @param mixed $data
|
||||
* @return void
|
||||
*/
|
||||
public static function onWebSocketConnect($clientId, $data) {
|
||||
try {
|
||||
// 清除原会话数据
|
||||
Gateway::setSession($clientId, []);
|
||||
|
||||
// 设置会话数据
|
||||
//Gateway::setSession($clientId, $client);
|
||||
|
||||
Logger::_(static::LOGGER)->info('连接成功: {id}', [
|
||||
'id' => $clientId,
|
||||
]);
|
||||
} catch (\Exception $ex) {
|
||||
Gateway::closeClient($clientId);
|
||||
|
||||
Logger::_(static::LOGGER)->info('连接错误: {id}|{error}', [
|
||||
'id' => $clientId,
|
||||
'error' => $ex->getMessage() . '<' . $ex->getCode() . '>',
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* onMessage 事件回调
|
||||
* 当客户端发来数据(Gateway进程收到数据)后触发
|
||||
*
|
||||
* @access public
|
||||
* @param int $client_id
|
||||
* @param mixed $data
|
||||
* @return void
|
||||
*/
|
||||
public static function onMessage($clientId, $data) {
|
||||
$json = @json_decode($data, TRUE);
|
||||
if (!empty($json)
|
||||
AND !empty($json['type'])
|
||||
AND is_array($json['data'])) {
|
||||
$session = Gateway::getSession($clientId);
|
||||
if (empty($session)) {
|
||||
throw new \Exception('获取SESSION失败: ' . $clientId);
|
||||
}
|
||||
|
||||
} else {
|
||||
Gateway::closeClient($clientId);
|
||||
|
||||
Logger::_(static::LOGGER)->error('消息错误: {id}|{data}', [
|
||||
'id' => $clientId,
|
||||
'data' => $data,
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* onClose 事件回调 当用户断开连接时触发的方法
|
||||
*
|
||||
* @param integer $clientId 断开连接的客户端client_id
|
||||
* @return void
|
||||
*/
|
||||
public static function onClose($clientId) {
|
||||
Gateway::setSession($clientId, []);
|
||||
Logger::_(static::LOGGER)->info('连接关闭: {id}', [
|
||||
'id' => $clientId,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* onWorkerStart 事件回调
|
||||
* 当businessWorker进程启动时触发。每个进程生命周期内都只会触发一次
|
||||
*
|
||||
* @access public
|
||||
* @param \Workerman\Worker $businessWorker
|
||||
* @return void
|
||||
*/
|
||||
public static function onWorkerStart(Worker $businessWorker) {
|
||||
Logger::_(static::LOGGER)->info('进程启动: {id}', [
|
||||
'id' => $businessWorker->id,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* onWorkerStop 事件回调
|
||||
* 当businessWorker进程退出时触发。每个进程生命周期内都只会触发一次。
|
||||
*
|
||||
* @param \Workerman\Worker $businessWorker
|
||||
* @return void
|
||||
*/
|
||||
public static function onWorkerStop(Worker $businessWorker) {
|
||||
Logger::_(static::LOGGER)->info('进程关闭: {id}', [
|
||||
'id' => $businessWorker->id,
|
||||
]);
|
||||
}
|
||||
}
|
||||
79
application/common/util/AliyunOSS.php
Normal file
79
application/common/util/AliyunOSS.php
Normal file
@@ -0,0 +1,79 @@
|
||||
<?php
|
||||
namespace app\common\util;
|
||||
|
||||
use OSS\OssClient;
|
||||
use OSS\Core\OssException;
|
||||
use think\facade\Env;
|
||||
|
||||
class AliyunOSS
|
||||
{
|
||||
// OSS配置信息
|
||||
const ACCESS_KEY_ID = 'LTAIxvJUmlt2gLiY';
|
||||
const ACCESS_KEY_SECRET = '0WUo8r6BT4I8ZVUQxflmD8rLHrFNHO';
|
||||
const ENDPOINT = 'oss-cn-shenzhen.aliyuncs.com';
|
||||
const BUCKET = 'karuosiyujzk';
|
||||
const ossUrl = 'https://res.quwanzhi.com';
|
||||
|
||||
/**
|
||||
* 获取OSS客户端实例
|
||||
* @return OssClient
|
||||
* @throws OssException
|
||||
*/
|
||||
public static function getClient()
|
||||
{
|
||||
try {
|
||||
return new OssClient(
|
||||
self::ACCESS_KEY_ID,
|
||||
self::ACCESS_KEY_SECRET,
|
||||
self::ENDPOINT
|
||||
);
|
||||
} catch (OssException $e) {
|
||||
throw new OssException('创建OSS客户端失败:' . $e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 上传文件到OSS
|
||||
* @param string $filePath 本地文件路径
|
||||
* @param string $objectName OSS对象名称
|
||||
* @return array
|
||||
* @throws OssException
|
||||
*/
|
||||
public static function uploadFile($filePath, $objectName)
|
||||
{
|
||||
try {
|
||||
$client = self::getClient();
|
||||
|
||||
// 上传文件
|
||||
$result = $client->uploadFile(self::BUCKET, $objectName, $filePath);
|
||||
|
||||
// 获取文件访问URL
|
||||
$url = !empty($result['oss-request-url']) ? $result['oss-request-url'] : $client->signUrl(self::BUCKET, $objectName, 3600);
|
||||
|
||||
return [
|
||||
'success' => true,
|
||||
'url' => $url,
|
||||
'object_name' => $objectName,
|
||||
'size' => filesize($filePath),
|
||||
'mime_type' => mime_content_type($filePath)
|
||||
];
|
||||
} catch (OssException $e) {
|
||||
return [
|
||||
'success' => false,
|
||||
'error' => $e->getMessage()
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成OSS对象名称
|
||||
* @param string $originalName 原始文件名
|
||||
* @return string
|
||||
*/
|
||||
public static function generateObjectName($originalName)
|
||||
{
|
||||
$ext = pathinfo($originalName, PATHINFO_EXTENSION);
|
||||
$name = md5(uniqid(mt_rand(), true));
|
||||
return date('Y/m/d/') . $name . '.' . $ext;
|
||||
}
|
||||
}
|
||||
65
application/common/util/AliyunSMS.php
Normal file
65
application/common/util/AliyunSMS.php
Normal file
@@ -0,0 +1,65 @@
|
||||
<?php
|
||||
|
||||
namespace app\common\util;
|
||||
|
||||
use Darabonba\OpenApi\Models\Config;
|
||||
use AlibabaCloud\SDK\Dysmsapi\V20170525\Dysmsapi;
|
||||
use AlibabaCloud\SDK\Dysmsapi\V20170525\Models\SendSmsRequest;
|
||||
use AlibabaCloud\Tea\Utils\Utils\RuntimeOptions;
|
||||
|
||||
class AliyunSMS {
|
||||
|
||||
const ACCESS_KEY_ID = 'LTAI5tFjVRYAFmo6fayvv2Te';
|
||||
const ACCESS_KEY_SECRET = 'mdc7KETPGVon8PiA2kfM47rAOpJne8';
|
||||
const SIGN_NAME = '广州宏科网络';
|
||||
|
||||
const TC_VCODE = 'SMS_464445466';
|
||||
|
||||
static public function createClient() {
|
||||
$config = new Config([
|
||||
// AccessKey ID
|
||||
'accessKeyId' => static::ACCESS_KEY_ID,
|
||||
// AccessKey Secret
|
||||
'accessKeySecret' => static::ACCESS_KEY_SECRET
|
||||
]);
|
||||
|
||||
// 访问的域名
|
||||
$config->endpoint = 'dysmsapi.aliyuncs.com';
|
||||
|
||||
return new Dysmsapi($config);
|
||||
}
|
||||
|
||||
/**
|
||||
* 发送验证码
|
||||
*
|
||||
* @param $phoneNumbers
|
||||
* @param $templateCode
|
||||
* @param array $templateParam
|
||||
* @return bool
|
||||
*/
|
||||
static public function send($phoneNumbers, $templateCode, array $templateParam = array()) {
|
||||
$client = static::createClient();
|
||||
$sendSmsRequest = new SendSmsRequest([
|
||||
'phoneNumbers' => $phoneNumbers,
|
||||
'signName' => static::SIGN_NAME,
|
||||
'templateCode' => $templateCode,
|
||||
'templateParam' => json_encode($templateParam)
|
||||
]);
|
||||
$runtime = new RuntimeOptions([]);
|
||||
$logFile = ROOT_PATH . DS . 'aliyun-sms.txt';
|
||||
try {
|
||||
$data = $client->sendSmsWithOptions($sendSmsRequest, $runtime);
|
||||
if ($data->body->code === 'OK') {
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
$logData = print_r($data, TRUE);
|
||||
} catch (\Exception $ex) {
|
||||
$logData = print_r($ex, TRUE);
|
||||
}
|
||||
|
||||
file_put_contents($logFile, '[' . date('Y-m-d H:i:s') . ']' . PHP_EOL . $logData . PHP_EOL . PHP_EOL, FILE_APPEND);
|
||||
|
||||
return FALSE;
|
||||
}
|
||||
}
|
||||
137
application/common/util/JwtUtil.php
Normal file
137
application/common/util/JwtUtil.php
Normal file
@@ -0,0 +1,137 @@
|
||||
<?php
|
||||
namespace app\common\util;
|
||||
|
||||
use think\facade\Config;
|
||||
use think\facade\Request;
|
||||
|
||||
/**
|
||||
* JWT工具类
|
||||
* 用于生成和验证JWT令牌
|
||||
*/
|
||||
class JwtUtil
|
||||
{
|
||||
/**
|
||||
* 密钥
|
||||
* @var string
|
||||
*/
|
||||
protected static $secret = 'YiShi@2023#JWT';
|
||||
|
||||
/**
|
||||
* 头部
|
||||
* @var array
|
||||
*/
|
||||
protected static $header = [
|
||||
'alg' => 'HS256', // 加密算法
|
||||
'typ' => 'JWT' // 类型
|
||||
];
|
||||
|
||||
/**
|
||||
* 创建JWT令牌
|
||||
* @param array $payload 载荷信息
|
||||
* @param int $expire 过期时间(秒),默认2小时
|
||||
* @return string
|
||||
*/
|
||||
public static function createToken($payload, $expire = 7200)
|
||||
{
|
||||
$header = self::base64UrlEncode(json_encode(self::$header, JSON_UNESCAPED_UNICODE));
|
||||
|
||||
// 附加过期时间
|
||||
$payload['exp'] = time() + $expire;
|
||||
$payload['iat'] = time(); // 签发时间
|
||||
|
||||
unset($payload['passwordMd5']);
|
||||
|
||||
$payload = self::base64UrlEncode(json_encode($payload, JSON_UNESCAPED_UNICODE));
|
||||
$signature = self::signature($header . '.' . $payload, self::$secret);
|
||||
|
||||
return $header . '.' . $payload . '.' . $signature;
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证令牌
|
||||
* @param string $token 令牌
|
||||
* @return array|bool 验证通过返回载荷信息,失败返回false
|
||||
*/
|
||||
public static function verifyToken($token)
|
||||
{
|
||||
if (empty($token)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$tokenArray = explode('.', $token);
|
||||
if (count($tokenArray) != 3) {
|
||||
return false;
|
||||
}
|
||||
|
||||
list($header, $payload, $signature) = $tokenArray;
|
||||
|
||||
// 验证签名
|
||||
if (self::signature($header . '.' . $payload, self::$secret) !== $signature) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// 解码载荷
|
||||
$payload = json_decode(self::base64UrlDecode($payload), true);
|
||||
|
||||
// 验证是否过期
|
||||
if (isset($payload['exp']) && $payload['exp'] < time()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return $payload;
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成签名
|
||||
* @param string $input 输入
|
||||
* @param string $key 密钥
|
||||
* @return string
|
||||
*/
|
||||
private static function signature($input, $key)
|
||||
{
|
||||
return self::base64UrlEncode(hash_hmac('sha256', $input, $key, true));
|
||||
}
|
||||
|
||||
/**
|
||||
* URL安全的Base64编码
|
||||
* @param string $input
|
||||
* @return string
|
||||
*/
|
||||
private static function base64UrlEncode($input)
|
||||
{
|
||||
return str_replace(['+', '/', '='], ['-', '_', ''], base64_encode($input));
|
||||
}
|
||||
|
||||
/**
|
||||
* URL安全的Base64解码
|
||||
* @param string $input
|
||||
* @return string
|
||||
*/
|
||||
private static function base64UrlDecode($input)
|
||||
{
|
||||
$remainder = strlen($input) % 4;
|
||||
if ($remainder) {
|
||||
$input .= str_repeat('=', 4 - $remainder);
|
||||
}
|
||||
return base64_decode(str_replace(['-', '_'], ['+', '/'], $input));
|
||||
}
|
||||
|
||||
/**
|
||||
* 从请求头中获取Token
|
||||
* @return string|null
|
||||
*/
|
||||
public static function getRequestToken()
|
||||
{
|
||||
$authorization = Request::header('Authorization');
|
||||
if (!$authorization) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// 检查Bearer前缀
|
||||
if (strpos($authorization, 'Bearer ') !== 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return substr($authorization, 7);
|
||||
}
|
||||
}
|
||||
255
application/common/util/PaymentUtil.php
Normal file
255
application/common/util/PaymentUtil.php
Normal file
@@ -0,0 +1,255 @@
|
||||
<?php
|
||||
|
||||
namespace app\common\util;
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* 支付工具类
|
||||
* 用于处理第三方支付相关功能
|
||||
* 仅限内部调用
|
||||
*/
|
||||
class PaymentUtil
|
||||
{
|
||||
/**
|
||||
* 签名算法类型
|
||||
*/
|
||||
const SIGN_TYPE_MD5 = 'MD5';
|
||||
const SIGN_TYPE_RSA_1_256 = 'RSA_1_256';
|
||||
const SIGN_TYPE_RSA_1_1 = 'RSA_1_1';
|
||||
|
||||
/**
|
||||
* 生成支付签名
|
||||
*
|
||||
* @param array $params 待签名参数
|
||||
* @param string $secretKey 签名密钥
|
||||
* @param string $signType 签名类型 MD5/RSA_1_256/RSA_1_1
|
||||
* @return string 签名结果
|
||||
*/
|
||||
public static function generateSign(array $params, string $secretKey, string $signType = self::SIGN_TYPE_MD5): string
|
||||
{
|
||||
// 1. 移除sign字段
|
||||
unset($params['sign']);
|
||||
|
||||
// 2. 过滤空值
|
||||
$params = array_filter($params, function($value) {
|
||||
return $value !== '' && $value !== null;
|
||||
});
|
||||
|
||||
// 3. 按字段名ASCII码从小到大排序
|
||||
ksort($params);
|
||||
|
||||
// 4. 拼接成QueryString格式
|
||||
$queryString = self::buildQueryString($params);
|
||||
|
||||
// 5. 根据签名类型生成签名
|
||||
switch (strtoupper($signType)) {
|
||||
case self::SIGN_TYPE_MD5:
|
||||
return self::generateMd5Sign($queryString, $secretKey);
|
||||
case self::SIGN_TYPE_RSA_1_256:
|
||||
return self::generateRsa256Sign($queryString, $secretKey);
|
||||
case self::SIGN_TYPE_RSA_1_1:
|
||||
return self::generateRsa1Sign($queryString, $secretKey);
|
||||
default:
|
||||
throw new \InvalidArgumentException('不支持的签名类型: ' . $signType);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证支付签名
|
||||
*
|
||||
* @param array $params 待验证参数(包含sign字段)
|
||||
* @param string $secretKey 签名密钥
|
||||
* @param string $signType 签名类型
|
||||
* @return bool 验证结果
|
||||
*/
|
||||
public static function verifySign(array $params, string $secretKey, string $signType = self::SIGN_TYPE_MD5): bool
|
||||
{
|
||||
if (!isset($params['sign'])) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$receivedSign = $params['sign'];
|
||||
$generatedSign = self::generateSign($params, $secretKey, $signType);
|
||||
|
||||
return $receivedSign === $generatedSign;
|
||||
}
|
||||
|
||||
/**
|
||||
* 构建QueryString
|
||||
*
|
||||
* @param array $params 参数数组
|
||||
* @return string QueryString
|
||||
*/
|
||||
private static function buildQueryString(array $params): string
|
||||
{
|
||||
$pairs = [];
|
||||
foreach ($params as $key => $value) {
|
||||
$pairs[] = $key . '=' . $value;
|
||||
}
|
||||
return implode('&', $pairs);
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成MD5签名
|
||||
*
|
||||
* @param string $queryString 待签名字符串
|
||||
* @param string $secretKey 密钥
|
||||
* @return string MD5签名
|
||||
*/
|
||||
private static function generateMd5Sign(string $queryString, string $secretKey): string
|
||||
{
|
||||
$signString = $queryString . '&key=' . $secretKey;
|
||||
return strtoupper(md5($signString));
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成RSA256签名
|
||||
*
|
||||
* @param string $queryString 待签名字符串
|
||||
* @param string $privateKey 私钥
|
||||
* @return string RSA256签名
|
||||
*/
|
||||
private static function generateRsa256Sign(string $queryString, string $privateKey): string
|
||||
{
|
||||
$privateKey = self::formatPrivateKey($privateKey);
|
||||
$key = openssl_pkey_get_private($privateKey);
|
||||
if (!$key) {
|
||||
throw new \Exception('RSA私钥格式错误');
|
||||
}
|
||||
|
||||
$signature = '';
|
||||
$result = openssl_sign($queryString, $signature, $key, OPENSSL_ALGO_SHA256);
|
||||
openssl_pkey_free($key);
|
||||
|
||||
if (!$result) {
|
||||
throw new \Exception('RSA256签名失败');
|
||||
}
|
||||
|
||||
return base64_encode($signature);
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成RSA1签名
|
||||
*
|
||||
* @param string $queryString 待签名字符串
|
||||
* @param string $privateKey 私钥
|
||||
* @return string RSA1签名
|
||||
*/
|
||||
private static function generateRsa1Sign(string $queryString, string $privateKey): string
|
||||
{
|
||||
$privateKey = self::formatPrivateKey($privateKey);
|
||||
$key = openssl_pkey_get_private($privateKey);
|
||||
if (!$key) {
|
||||
throw new \Exception('RSA私钥格式错误');
|
||||
}
|
||||
|
||||
$signature = '';
|
||||
$result = openssl_sign($queryString, $signature, $key, OPENSSL_ALGO_SHA1);
|
||||
openssl_pkey_free($key);
|
||||
|
||||
if (!$result) {
|
||||
throw new \Exception('RSA1签名失败');
|
||||
}
|
||||
|
||||
return base64_encode($signature);
|
||||
}
|
||||
|
||||
/**
|
||||
* 格式化私钥
|
||||
*
|
||||
* @param string $privateKey 原始私钥
|
||||
* @return string 格式化后的私钥
|
||||
*/
|
||||
private static function formatPrivateKey(string $privateKey): string
|
||||
{
|
||||
$privateKey = str_replace(['-----BEGIN PRIVATE KEY-----', '-----END PRIVATE KEY-----', "\n", "\r"], '', $privateKey);
|
||||
$privateKey = chunk_split($privateKey, 64, "\n");
|
||||
return "-----BEGIN PRIVATE KEY-----\n" . $privateKey . "-----END PRIVATE KEY-----";
|
||||
}
|
||||
|
||||
/**
|
||||
* 格式化公钥
|
||||
*
|
||||
* @param string $publicKey 原始公钥
|
||||
* @return string 格式化后的公钥
|
||||
*/
|
||||
private static function formatPublicKey(string $publicKey): string
|
||||
{
|
||||
$publicKey = str_replace(['-----BEGIN PUBLIC KEY-----', '-----END PUBLIC KEY-----', "\n", "\r"], '', $publicKey);
|
||||
$publicKey = chunk_split($publicKey, 64, "\n");
|
||||
return "-----BEGIN PUBLIC KEY-----\n" . $publicKey . "-----END PUBLIC KEY-----";
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证RSA签名
|
||||
*
|
||||
* @param string $queryString 原始字符串
|
||||
* @param string $signature 签名
|
||||
* @param string $publicKey 公钥
|
||||
* @param string $signType 签名类型
|
||||
* @return bool 验证结果
|
||||
*/
|
||||
public static function verifyRsaSign(string $queryString, string $signature, string $publicKey, string $signType = self::SIGN_TYPE_RSA_1_256): bool
|
||||
{
|
||||
$publicKey = self::formatPublicKey($publicKey);
|
||||
$key = openssl_pkey_get_public($publicKey);
|
||||
if (!$key) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$algorithm = $signType === self::SIGN_TYPE_RSA_1_1 ? OPENSSL_ALGO_SHA1 : OPENSSL_ALGO_SHA256;
|
||||
$result = openssl_verify($queryString, base64_decode($signature), $key, $algorithm);
|
||||
openssl_pkey_free($key);
|
||||
|
||||
return $result === 1;
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成随机字符串
|
||||
*
|
||||
* @param int $length 长度
|
||||
* @return string 随机字符串
|
||||
*/
|
||||
public static function generateNonceStr(int $length = 32): string
|
||||
{
|
||||
$chars = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789';
|
||||
$str = '';
|
||||
for ($i = 0; $i < $length; $i++) {
|
||||
$str .= $chars[mt_rand(0, strlen($chars) - 1)];
|
||||
}
|
||||
return $str;
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成时间戳
|
||||
*
|
||||
* @return int 时间戳
|
||||
*/
|
||||
public static function generateTimestamp(): int
|
||||
{
|
||||
return time();
|
||||
}
|
||||
|
||||
/**
|
||||
* 格式化金额(分转元)
|
||||
*
|
||||
* @param int $amount 金额(分)
|
||||
* @return string 格式化后的金额(元)
|
||||
*/
|
||||
public static function formatAmount(int $amount): string
|
||||
{
|
||||
return number_format($amount / 100, 2, '.', '');
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析金额(元转分)
|
||||
*
|
||||
* @param string $amount 金额(元)
|
||||
* @return int 金额(分)
|
||||
*/
|
||||
public static function parseAmount(string $amount): int
|
||||
{
|
||||
return (int) round(floatval($amount) * 100);
|
||||
}
|
||||
}
|
||||
135
application/common/util/Signer.php
Normal file
135
application/common/util/Signer.php
Normal file
@@ -0,0 +1,135 @@
|
||||
<?php
|
||||
|
||||
namespace app\common\util;
|
||||
|
||||
/**
|
||||
* 第三方支付签名工具(仅内部调用)
|
||||
* 规则:
|
||||
* 1. 除 sign 外的所有非空参数,按字段名 ASCII 升序,使用 QueryString 形式拼接(key1=value1&key2=value2)
|
||||
* 2. 参与签名的字段名与值均为原始值,不做 URL Encode
|
||||
* 3. 支持算法:MD5(默认)/ RSA_1_256 / RSA_1_1
|
||||
*/
|
||||
class Signer
|
||||
{
|
||||
/**
|
||||
* 生成签名
|
||||
*
|
||||
* @param array $params 参与签名的参数(会自动剔除 sign 及空值)
|
||||
* @param string $algorithm 签名算法:md5 | RSA_1_256 | RSA_1_1
|
||||
* @param array $options 额外选项:
|
||||
* - secret: string MD5 签名时可选的密钥,若提供则会在原串末尾以 &key=SECRET 追加
|
||||
* - private_key: string RSA 签名所需私钥(PEM 字符串,支持带头尾)
|
||||
* - passphrase: string 可选,RSA 私钥口令
|
||||
* @return string 返回签名串(MD5 为32位小写;RSA为base64编码)
|
||||
* @throws \InvalidArgumentException
|
||||
*/
|
||||
public static function sign(array $params, $algorithm = 'md5', array $options = [])
|
||||
{
|
||||
$signString = self::buildSignString($params);
|
||||
|
||||
$algo = strtolower($algorithm);
|
||||
switch ($algo) {
|
||||
case 'md5':
|
||||
return self::signMd5($signString, isset($options['secret']) ? (string)$options['secret'] : null);
|
||||
case 'rsa_1_256':
|
||||
return self::signRsa($signString, $options, 'sha256');
|
||||
case 'rsa_1_1':
|
||||
return self::signRsa($signString, $options, 'sha1');
|
||||
default:
|
||||
throw new \InvalidArgumentException('Unsupported algorithm: ' . $algorithm);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 构建签名原始串
|
||||
* - 剔除 sign 字段
|
||||
* - 过滤空值(null、'')
|
||||
* - 按键名 ASCII 升序
|
||||
* - 使用原始值拼接为 key1=value1&key2=value2
|
||||
*
|
||||
* @param array $params
|
||||
* @return string
|
||||
*/
|
||||
public static function buildSignString(array $params)
|
||||
{
|
||||
$filtered = [];
|
||||
foreach ($params as $key => $value) {
|
||||
if ($key === 'sign') {
|
||||
continue;
|
||||
}
|
||||
if ($value === '' || $value === null) {
|
||||
continue;
|
||||
}
|
||||
$filtered[$key] = $value;
|
||||
}
|
||||
|
||||
ksort($filtered, SORT_STRING);
|
||||
|
||||
$pairs = [];
|
||||
foreach ($filtered as $key => $value) {
|
||||
// 原始值拼接,不做 urlencode
|
||||
$pairs[] = $key . '=' . (is_bool($value) ? ($value ? '1' : '0') : (string)$value);
|
||||
}
|
||||
|
||||
return implode('&', $pairs);
|
||||
}
|
||||
|
||||
/**
|
||||
* MD5 签名
|
||||
* - 若提供 secret,则原串末尾追加 &key=SECRET
|
||||
* - 返回 32 位小写
|
||||
*
|
||||
* @param string $signString
|
||||
* @param string|null $secret
|
||||
* @return string
|
||||
*/
|
||||
protected static function signMd5($signString, $secret = null)
|
||||
{
|
||||
if ($secret !== null && $secret !== '') {
|
||||
$signString .= '&key=' . $secret;
|
||||
}
|
||||
return strtolower(md5($signString));
|
||||
}
|
||||
|
||||
/**
|
||||
* RSA 签名
|
||||
*
|
||||
* @param string $signString
|
||||
* @param array $options 必填:private_key,可选:passphrase
|
||||
* @param string $hashAlgo sha256|sha1
|
||||
* @return string base64 签名
|
||||
* @throws \InvalidArgumentException
|
||||
*/
|
||||
protected static function signRsa($signString, array $options, $hashAlgo = 'sha256')
|
||||
{
|
||||
if (empty($options['private_key'])) {
|
||||
throw new \InvalidArgumentException('RSA signing requires private_key.');
|
||||
}
|
||||
|
||||
$privateKey = $options['private_key'];
|
||||
$passphrase = isset($options['passphrase']) ? (string)$options['passphrase'] : '';
|
||||
|
||||
// 兼容无头尾私钥,自动包裹为 PEM
|
||||
if (strpos($privateKey, 'BEGIN') === false) {
|
||||
$privateKey = "-----BEGIN PRIVATE KEY-----\n" . trim(chunk_split(str_replace(["\r", "\n"], '', $privateKey), 64, "\n")) . "\n-----END PRIVATE KEY-----";
|
||||
}
|
||||
|
||||
$pkeyId = openssl_pkey_get_private($privateKey, $passphrase);
|
||||
if ($pkeyId === false) {
|
||||
throw new \InvalidArgumentException('Invalid RSA private key or passphrase.');
|
||||
}
|
||||
|
||||
$signature = '';
|
||||
$algoConst = $hashAlgo === 'sha1' ? OPENSSL_ALGO_SHA1 : OPENSSL_ALGO_SHA256;
|
||||
$ok = openssl_sign($signString, $signature, $pkeyId, $algoConst);
|
||||
openssl_free_key($pkeyId);
|
||||
|
||||
if (!$ok) {
|
||||
throw new \InvalidArgumentException('OpenSSL sign failed.');
|
||||
}
|
||||
|
||||
return base64_encode($signature);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
46
application/common/validate/Auth.php
Normal file
46
application/common/validate/Auth.php
Normal file
@@ -0,0 +1,46 @@
|
||||
<?php
|
||||
namespace app\common\validate;
|
||||
|
||||
use think\Validate;
|
||||
|
||||
/**
|
||||
* 认证相关验证器
|
||||
*/
|
||||
class Auth extends Validate
|
||||
{
|
||||
/**
|
||||
* 验证规则
|
||||
* @var array
|
||||
*/
|
||||
protected $rule = [
|
||||
'account' => 'require',
|
||||
'password' => 'require|length:6,64',
|
||||
'code' => 'require|length:4,6',
|
||||
'typeId' => 'require|in:1,2',
|
||||
];
|
||||
|
||||
/**
|
||||
* 错误信息
|
||||
* @var array
|
||||
*/
|
||||
protected $message = [
|
||||
'account.require' => '账号不能为空',
|
||||
'password.require' => '密码不能为空',
|
||||
'password.length' => '密码长度必须在6-64个字符之间',
|
||||
'code.require' => '验证码不能为空',
|
||||
'code.length' => '验证码长度必须在4-6个字符之间',
|
||||
'typeId.require' => '用户类型不能为空',
|
||||
'typeId.in' => '用户类型错误',
|
||||
];
|
||||
|
||||
/**
|
||||
* 验证场景
|
||||
* @var array
|
||||
*/
|
||||
protected $scene = [
|
||||
'login' => ['account', 'password', 'typeId'],
|
||||
'mobile_login' => ['account', 'code', 'typeId'],
|
||||
'refresh' => [],
|
||||
'send_code' => ['account', 'type'],
|
||||
];
|
||||
}
|
||||
93
application/common/view/tpl/dispatch_jump.tpl
Normal file
93
application/common/view/tpl/dispatch_jump.tpl
Normal file
@@ -0,0 +1,93 @@
|
||||
{__NOLAYOUT__}<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>跳转提示</title>
|
||||
<style type="text/css">
|
||||
body {
|
||||
background-color: #fff;
|
||||
font-family: "Microsoft YaHei", "Helvetica Neue", Helvetica, Arial, sans-serif;
|
||||
font-size: 14px;
|
||||
line-height: 20px;
|
||||
color: #333333;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
.system-message {
|
||||
padding: 24px 48px;
|
||||
margin: 100px auto;
|
||||
max-width: 600px;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 4px;
|
||||
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
.system-message h1 {
|
||||
font-size: 36px;
|
||||
line-height: 40px;
|
||||
margin-bottom: 12px;
|
||||
font-weight: 200;
|
||||
text-align: center;
|
||||
}
|
||||
.system-message .success, .system-message .error {
|
||||
line-height: 1.8em;
|
||||
font-size: 16px;
|
||||
text-align: center;
|
||||
}
|
||||
.system-message .detail {
|
||||
font-size: 12px;
|
||||
line-height: 20px;
|
||||
margin-top: 12px;
|
||||
text-align: center;
|
||||
}
|
||||
.system-message .jump {
|
||||
text-align: center;
|
||||
padding-top: 10px;
|
||||
}
|
||||
.system-message .success {
|
||||
color: #27ae60;
|
||||
}
|
||||
.system-message .error {
|
||||
color: #e74c3c;
|
||||
}
|
||||
.system-message .jump a {
|
||||
color: #333;
|
||||
text-decoration: none;
|
||||
}
|
||||
.system-message .jump a:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="system-message">
|
||||
<?php switch ($code) {?>
|
||||
<?php case 1:?>
|
||||
<h1>:)</h1>
|
||||
<p class="success"><?php echo(strip_tags($msg));?></p>
|
||||
<?php break;?>
|
||||
<?php case 0:?>
|
||||
<h1>:(</h1>
|
||||
<p class="error"><?php echo(strip_tags($msg));?></p>
|
||||
<?php break;?>
|
||||
<?php } ?>
|
||||
<p class="detail"></p>
|
||||
<p class="jump">
|
||||
页面自动 <a id="href" href="<?php echo($url);?>">跳转</a> 等待时间: <b id="wait"><?php echo($wait);?></b>
|
||||
</p>
|
||||
</div>
|
||||
<script type="text/javascript">
|
||||
(function(){
|
||||
var wait = document.getElementById('wait'),
|
||||
href = document.getElementById('href').href;
|
||||
var interval = setInterval(function(){
|
||||
var time = --wait.innerHTML;
|
||||
if(time <= 0) {
|
||||
location.href = href;
|
||||
clearInterval(interval);
|
||||
}
|
||||
}, 1000);
|
||||
})();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
Reference in New Issue
Block a user