Initial commit: ThinkPHP refactor (tp)
Made-with: Cursor
This commit is contained in:
@@ -0,0 +1,22 @@
|
||||
<?php
|
||||
declare (strict_types = 1);
|
||||
|
||||
namespace app\api;
|
||||
|
||||
use think\Service;
|
||||
|
||||
/**
|
||||
* API 应用服务
|
||||
*/
|
||||
class AppService extends Service
|
||||
{
|
||||
public function register()
|
||||
{
|
||||
// 注册服务
|
||||
}
|
||||
|
||||
public function boot()
|
||||
{
|
||||
// 启动服务
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,137 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace app\api\common;
|
||||
|
||||
/**
|
||||
* JWT 工具类
|
||||
* 用于生成和验证 JWT Token
|
||||
*/
|
||||
class Jwt
|
||||
{
|
||||
/**
|
||||
* 生成 Token
|
||||
* @param array $payload 载荷数据
|
||||
* @return string
|
||||
*/
|
||||
public static function encode(array $payload): string
|
||||
{
|
||||
$config = config('jwt');
|
||||
|
||||
// 添加标准声明
|
||||
$payload['iat'] = time();
|
||||
$payload['iss'] = $config['issuer'] ?? 'dyai-api';
|
||||
$payload['exp'] = time() + ($config['expire'] ?? 604800);
|
||||
|
||||
// 编码
|
||||
$header = self::base64UrlEncode(json_encode(['typ' => 'JWT', 'alg' => 'HS256']));
|
||||
$body = self::base64UrlEncode(json_encode($payload));
|
||||
|
||||
// 签名
|
||||
$signature = self::signature("$header.$body", $config['secret'] ?? 'default_secret');
|
||||
|
||||
return "$header.$body.$signature";
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析 Token
|
||||
* @param string $token
|
||||
* @return array|null
|
||||
*/
|
||||
public static function decode(string $token): ?array
|
||||
{
|
||||
$parts = explode('.', $token);
|
||||
if (count($parts) !== 3) {
|
||||
return null;
|
||||
}
|
||||
|
||||
[$header, $body, $signature] = $parts;
|
||||
|
||||
// 验证签名
|
||||
$config = config('jwt');
|
||||
$expectedSignature = self::signature("$header.$body", $config['secret'] ?? 'default_secret');
|
||||
|
||||
if (!hash_equals($expectedSignature, $signature)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// 解码载荷
|
||||
$payload = json_decode(self::base64UrlDecode($body), true);
|
||||
if (!$payload) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// 验证过期时间
|
||||
if (isset($payload['exp']) && $payload['exp'] < time()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return $payload;
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成刷新 Token
|
||||
* @param int $userid
|
||||
* @return string
|
||||
*/
|
||||
public static function refreshToken(int $userid): string
|
||||
{
|
||||
$config = config('jwt');
|
||||
|
||||
$payload = [
|
||||
'userid' => $userid,
|
||||
'type' => 'refresh',
|
||||
'iat' => time(),
|
||||
'exp' => time() + ($config['refresh_expire'] ?? 2592000),
|
||||
];
|
||||
|
||||
return self::encode($payload);
|
||||
}
|
||||
|
||||
/**
|
||||
* 从请求头获取 Token
|
||||
* @return string|null
|
||||
*/
|
||||
public static function getTokenFromRequest(): ?string
|
||||
{
|
||||
$request = request();
|
||||
|
||||
// 从 Authorization 头获取
|
||||
$authorization = $request->header('Authorization', '');
|
||||
if (preg_match('/Bearer\s+(.+)/', $authorization, $matches)) {
|
||||
return $matches[1];
|
||||
}
|
||||
|
||||
// 从参数获取
|
||||
$token = $request->param('token');
|
||||
if ($token) {
|
||||
return $token;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* URL安全的 Base64 编码
|
||||
*/
|
||||
private static function base64UrlEncode(string $data): string
|
||||
{
|
||||
return rtrim(strtr(base64_encode($data), '+/', '-_'), '=');
|
||||
}
|
||||
|
||||
/**
|
||||
* URL安全的 Base64 解码
|
||||
*/
|
||||
private static function base64UrlDecode(string $data): string
|
||||
{
|
||||
return base64_decode(strtr($data, '-_', '+/'));
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成签名
|
||||
*/
|
||||
private static function signature(string $data, string $secret): string
|
||||
{
|
||||
return self::base64UrlEncode(hash_hmac('sha256', $data, $secret, true));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
<?php
|
||||
declare (strict_types = 1);
|
||||
|
||||
namespace app\api\common;
|
||||
|
||||
/**
|
||||
* 统一响应类
|
||||
*/
|
||||
class Response
|
||||
{
|
||||
/**
|
||||
* 成功响应
|
||||
* @param mixed $data 返回数据
|
||||
* @param string $message 提示信息
|
||||
* @param int $code 状态码
|
||||
* @return \think\response\Json
|
||||
*/
|
||||
public static function success($data = [], string $message = 'success', int $code = 200)
|
||||
{
|
||||
return json([
|
||||
'code' => $code,
|
||||
'msg' => $message,
|
||||
'data' => $data,
|
||||
'time' => time(),
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* 失败响应
|
||||
* @param string $message 提示信息
|
||||
* @param int $code 状态码
|
||||
* @param mixed $data 返回数据
|
||||
* @return \think\response\Json
|
||||
*/
|
||||
public static function error(string $message = 'error', int $code = 400, $data = [])
|
||||
{
|
||||
return json([
|
||||
'code' => $code,
|
||||
'msg' => $message,
|
||||
'data' => $data,
|
||||
'time' => time(),
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* 分页数据响应
|
||||
* @param mixed $list 数据列表
|
||||
* @param int $total 总数
|
||||
* @param int $page 当前页
|
||||
* @param int $pageSize 每页数量
|
||||
* @param string $message 提示信息
|
||||
* @return \think\response\Json
|
||||
*/
|
||||
public static function paginate($list, int $total, int $page, int $pageSize, string $message = 'success')
|
||||
{
|
||||
return json([
|
||||
'code' => 200,
|
||||
'msg' => $message,
|
||||
'data' => [
|
||||
'list' => $list,
|
||||
'total' => $total,
|
||||
'page' => $page,
|
||||
'page_size' => $pageSize,
|
||||
],
|
||||
'time' => time(),
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,115 @@
|
||||
<?php
|
||||
declare (strict_types = 1);
|
||||
|
||||
namespace app\api\controller;
|
||||
|
||||
use think\App;
|
||||
use think\exception\ValidateException;
|
||||
use think\Validate;
|
||||
|
||||
/**
|
||||
* API 基础控制器
|
||||
*/
|
||||
abstract class BaseController
|
||||
{
|
||||
/**
|
||||
* Request实例
|
||||
* @var \think\Request
|
||||
*/
|
||||
protected $request;
|
||||
|
||||
/**
|
||||
* 应用实例
|
||||
* @var \think\App
|
||||
*/
|
||||
protected $app;
|
||||
|
||||
/**
|
||||
* 构造方法
|
||||
* @access public
|
||||
* @param App $app 应用对象
|
||||
*/
|
||||
public function __construct(App $app)
|
||||
{
|
||||
$this->app = $app;
|
||||
$this->request = $this->app->request;
|
||||
|
||||
// 控制器初始化
|
||||
$this->initialize();
|
||||
}
|
||||
|
||||
// 初始化
|
||||
protected function initialize()
|
||||
{}
|
||||
|
||||
/**
|
||||
* 成功响应
|
||||
* @param mixed $data 返回数据
|
||||
* @param string $message 提示信息
|
||||
* @param int $code 状态码
|
||||
* @return \think\response\Json
|
||||
*/
|
||||
protected function success($data = [], string $message = 'success', int $code = 200)
|
||||
{
|
||||
return json([
|
||||
'code' => $code,
|
||||
'msg' => $message,
|
||||
'data' => $data,
|
||||
'time' => time(),
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* 失败响应
|
||||
* @param string $message 提示信息
|
||||
* @param int $code 状态码
|
||||
* @param mixed $data 返回数据
|
||||
* @return \think\response\Json
|
||||
*/
|
||||
protected function error(string $message = 'error', int $code = 400, $data = [])
|
||||
{
|
||||
return json([
|
||||
'code' => $code,
|
||||
'msg' => $message,
|
||||
'data' => $data,
|
||||
'time' => time(),
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证数据
|
||||
* @access protected
|
||||
* @param array $data 数据
|
||||
* @param string|array $validate 验证器名或者验证规则数组
|
||||
* @param array $message 提示信息
|
||||
* @param bool $batch 是否批量验证
|
||||
* @return array|string|true
|
||||
* @throws ValidateException
|
||||
*/
|
||||
protected function validate(array $data, $validate, array $message = [], bool $batch = false)
|
||||
{
|
||||
if (is_array($validate)) {
|
||||
$v = new Validate();
|
||||
$v->rule($validate);
|
||||
} else {
|
||||
if (strpos($validate, '.')) {
|
||||
// 支持场景
|
||||
[$validate, $scene] = explode('.', $validate);
|
||||
}
|
||||
$class = false !== strpos($validate, '\\') ? $validate : $this->app->parseClass('validate', $validate);
|
||||
$v = new $class();
|
||||
if (!empty($scene)) {
|
||||
$v->scene($scene);
|
||||
}
|
||||
}
|
||||
|
||||
$v->message($message);
|
||||
|
||||
// 是否批量验证
|
||||
if ($batch || $this->request->isBatchValidate()) {
|
||||
$v->batch(true);
|
||||
}
|
||||
|
||||
return $v->failException(true)->check($data);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
<?php
|
||||
declare (strict_types = 1);
|
||||
|
||||
namespace app\api\controller;
|
||||
|
||||
/**
|
||||
* API 示例控制器
|
||||
*/
|
||||
class Index extends BaseController
|
||||
{
|
||||
/**
|
||||
* 首页接口
|
||||
* @return \think\response\Json
|
||||
*/
|
||||
public function index()
|
||||
{
|
||||
$data = [
|
||||
'name' => 'ThinkPHP API',
|
||||
'version' => app()->version(),
|
||||
'message' => 'Welcome to ThinkPHP API Application',
|
||||
];
|
||||
|
||||
return $this->success($data);
|
||||
}
|
||||
|
||||
/**
|
||||
* 健康检查接口
|
||||
* @return \think\response\Json
|
||||
*/
|
||||
public function health()
|
||||
{
|
||||
return $this->success([
|
||||
'status' => 'ok',
|
||||
'timestamp' => date('Y-m-d H:i:s'),
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,81 @@
|
||||
<?php
|
||||
declare (strict_types = 1);
|
||||
|
||||
namespace app\api\controller;
|
||||
|
||||
/**
|
||||
* 用户控制器示例
|
||||
*/
|
||||
class User extends BaseController
|
||||
{
|
||||
/**
|
||||
* 用户登录
|
||||
* @return \think\response\Json
|
||||
*/
|
||||
public function login()
|
||||
{
|
||||
$data = $this->request->post();
|
||||
|
||||
// 验证数据
|
||||
$this->validate($data, [
|
||||
'username' => 'require',
|
||||
'password' => 'require',
|
||||
], [
|
||||
'username.require' => '用户名不能为空',
|
||||
'password.require' => '密码不能为空',
|
||||
]);
|
||||
|
||||
// TODO: 实际的登录逻辑
|
||||
return $this->success([
|
||||
'token' => 'example_token_' . md5($data['username']),
|
||||
'username' => $data['username'],
|
||||
], '登录成功');
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取用户信息
|
||||
* @return \think\response\Json
|
||||
*/
|
||||
public function info()
|
||||
{
|
||||
// TODO: 从 token 或 session 中获取用户信息
|
||||
$userInfo = [
|
||||
'id' => 1,
|
||||
'username' => 'demo_user',
|
||||
'nickname' => '演示用户',
|
||||
'avatar' => '',
|
||||
'email' => 'demo@example.com',
|
||||
'created_at' => date('Y-m-d H:i:s'),
|
||||
];
|
||||
|
||||
return $this->success($userInfo);
|
||||
}
|
||||
|
||||
/**
|
||||
* 用户注册
|
||||
* @return \think\response\Json
|
||||
*/
|
||||
public function register()
|
||||
{
|
||||
$data = $this->request->post();
|
||||
|
||||
// 验证数据
|
||||
$this->validate($data, [
|
||||
'username' => 'require|length:3,20',
|
||||
'password' => 'require|length:6,20',
|
||||
'email' => 'require|email',
|
||||
], [
|
||||
'username.require' => '用户名不能为空',
|
||||
'username.length' => '用户名长度3-20位',
|
||||
'password.require' => '密码不能为空',
|
||||
'password.length' => '密码长度6-20位',
|
||||
'email.require' => '邮箱不能为空',
|
||||
'email.email' => '邮箱格式不正确',
|
||||
]);
|
||||
|
||||
// TODO: 实际的注册逻辑
|
||||
return $this->success([
|
||||
'user_id' => rand(1000, 9999),
|
||||
], '注册成功');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,193 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace app\api\controller;
|
||||
|
||||
use app\api\common\Jwt;
|
||||
use app\api\common\Response;
|
||||
use app\api\controller\BaseController;
|
||||
use app\api\service\AuthService;
|
||||
use think\exception\ValidateException;
|
||||
|
||||
/**
|
||||
* 认证控制器 (v1版本)
|
||||
* 处理用户登录、注册、Token 刷新等
|
||||
*/
|
||||
class Auth extends BaseController
|
||||
{
|
||||
/**
|
||||
* @var AuthService
|
||||
*/
|
||||
protected AuthService $authService;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
parent::__construct();
|
||||
$this->authService = new AuthService();
|
||||
}
|
||||
|
||||
/**
|
||||
* 用户登录
|
||||
* POST /api/v1/auth/login
|
||||
* @return \think\response\Json
|
||||
*/
|
||||
public function login()
|
||||
{
|
||||
try {
|
||||
$data = $this->request->post();
|
||||
|
||||
// 验证参数
|
||||
validate($data, [
|
||||
'username' => 'require',
|
||||
'password' => 'require',
|
||||
], [
|
||||
'username.require' => '用户名不能为空',
|
||||
'password.require' => '密码不能为空',
|
||||
]);
|
||||
|
||||
$result = $this->authService->login(
|
||||
$data['username'],
|
||||
$data['password']
|
||||
);
|
||||
|
||||
return Response::success($result, '登录成功');
|
||||
} catch (ValidateException $e) {
|
||||
return Response::error($e->getMessage(), 400);
|
||||
} catch (\Exception $e) {
|
||||
return Response::error($e->getMessage(), $e->getCode() ?: 500);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 用户注册
|
||||
* POST /api/v1/auth/register
|
||||
* @return \think\response\Json
|
||||
*/
|
||||
public function register()
|
||||
{
|
||||
try {
|
||||
$data = $this->request->post();
|
||||
|
||||
// 验证参数
|
||||
validate($data, [
|
||||
'username' => 'require|length:3,20|alphaNum',
|
||||
'password' => 'require|length:6,20',
|
||||
'email' => 'email',
|
||||
], [
|
||||
'username.require' => '用户名不能为空',
|
||||
'username.length' => '用户名长度3-20位',
|
||||
'username.alphaNum' => '用户名只能包含字母和数字',
|
||||
'password.require' => '密码不能为空',
|
||||
'password.length' => '密码长度6-20位',
|
||||
'email.email' => '邮箱格式不正确',
|
||||
]);
|
||||
|
||||
$result = $this->authService->register(
|
||||
$data['username'],
|
||||
$data['password'],
|
||||
$data['email'] ?? null,
|
||||
$data['formtypeid'] ?? null
|
||||
);
|
||||
|
||||
return Response::success($result, '注册成功');
|
||||
} catch (ValidateException $e) {
|
||||
return Response::error($e->getMessage(), 400);
|
||||
} catch (\Exception $e) {
|
||||
return Response::error($e->getMessage(), $e->getCode() ?: 500);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 刷新 Token
|
||||
* POST /api/v1/auth/refresh
|
||||
* @return \think\response\Json
|
||||
*/
|
||||
public function refresh()
|
||||
{
|
||||
try {
|
||||
$data = $this->request->post();
|
||||
|
||||
if (empty($data['refresh_token'])) {
|
||||
return Response::error('刷新令牌不能为空', 400);
|
||||
}
|
||||
|
||||
$result = $this->authService->refreshToken($data['refresh_token']);
|
||||
|
||||
return Response::success($result, '刷新成功');
|
||||
} catch (\Exception $e) {
|
||||
return Response::error($e->getMessage(), $e->getCode() ?: 500);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取当前用户信息
|
||||
* GET /api/v1/auth/me
|
||||
* @return \think\response\Json
|
||||
*/
|
||||
public function me()
|
||||
{
|
||||
try {
|
||||
$payload = $this->request->payload ?? null;
|
||||
if (!$payload) {
|
||||
return Response::error('未登录', 401);
|
||||
}
|
||||
|
||||
$result = $this->authService->getUserInfo($payload['userid']);
|
||||
|
||||
return Response::success($result);
|
||||
} catch (\Exception $e) {
|
||||
return Response::error($e->getMessage(), $e->getCode() ?: 500);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 退出登录
|
||||
* POST /api/v1/auth/logout
|
||||
* @return \think\response\Json
|
||||
*/
|
||||
public function logout()
|
||||
{
|
||||
// JWT 无状态,退出只需客户端删除 Token
|
||||
// 如果需要服务端失效,可以将 Token 加入黑名单(需要 Redis 支持)
|
||||
return Response::success([], '退出成功');
|
||||
}
|
||||
|
||||
/**
|
||||
* 修改密码
|
||||
* POST /api/v1/auth/password
|
||||
* @return \think\response\Json
|
||||
*/
|
||||
public function password()
|
||||
{
|
||||
try {
|
||||
$payload = $this->request->payload ?? null;
|
||||
if (!$payload) {
|
||||
return Response::error('未登录', 401);
|
||||
}
|
||||
|
||||
$data = $this->request->post();
|
||||
|
||||
validate($data, [
|
||||
'old_password' => 'require',
|
||||
'new_password' => 'require|length:6,20|confirm:confirm_password',
|
||||
], [
|
||||
'old_password.require' => '原密码不能为空',
|
||||
'new_password.require' => '新密码不能为空',
|
||||
'new_password.length' => '新密码长度6-20位',
|
||||
'new_password.confirm' => '两次密码输入不一致',
|
||||
]);
|
||||
|
||||
$this->authService->changePassword(
|
||||
$payload['userid'],
|
||||
$data['old_password'],
|
||||
$data['new_password']
|
||||
);
|
||||
|
||||
return Response::success([], '密码修改成功');
|
||||
} catch (ValidateException $e) {
|
||||
return Response::error($e->getMessage(), 400);
|
||||
} catch (\Exception $e) {
|
||||
return Response::error($e->getMessage(), $e->getCode() ?: 500);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
<?php
|
||||
declare (strict_types = 1);
|
||||
|
||||
// API 应用中间件配置
|
||||
|
||||
return [
|
||||
// 全局中间件
|
||||
\app\api\middleware\CrossDomain::class,
|
||||
];
|
||||
@@ -0,0 +1,42 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace app\api\middleware;
|
||||
|
||||
use app\api\common\Jwt;
|
||||
use app\api\common\Response;
|
||||
|
||||
/**
|
||||
* JWT 认证中间件
|
||||
*/
|
||||
class Auth
|
||||
{
|
||||
/**
|
||||
* 处理请求
|
||||
* @param \think\Request $request
|
||||
* @param \Closure $next
|
||||
* @return mixed
|
||||
*/
|
||||
public function handle($request, \Closure $next)
|
||||
{
|
||||
// 获取 Token
|
||||
$token = Jwt::getTokenFromRequest();
|
||||
|
||||
if (!$token) {
|
||||
return Response::error('未提供认证令牌', 401);
|
||||
}
|
||||
|
||||
// 验证 Token
|
||||
$payload = Jwt::decode($token);
|
||||
|
||||
if (!$payload) {
|
||||
return Response::error('令牌无效或已过期', 401);
|
||||
}
|
||||
|
||||
// 将用户信息注入请求
|
||||
$request->payload = $payload;
|
||||
$request->userid = $payload['userid'] ?? null;
|
||||
|
||||
return $next($request);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
<?php
|
||||
declare (strict_types = 1);
|
||||
|
||||
namespace app\api\middleware;
|
||||
|
||||
use think\Response;
|
||||
|
||||
/**
|
||||
* API 跨域中间件
|
||||
*/
|
||||
class CrossDomain
|
||||
{
|
||||
/**
|
||||
* 处理请求
|
||||
* @param \think\Request $request
|
||||
* @param \Closure $next
|
||||
* @return mixed
|
||||
*/
|
||||
public function handle($request, \Closure $next)
|
||||
{
|
||||
// OPTIONS 请求直接返回
|
||||
if ($request->method() == 'OPTIONS') {
|
||||
return Response::create('', 'html', 204)
|
||||
->header([
|
||||
'Access-Control-Allow-Origin' => '*',
|
||||
'Access-Control-Allow-Methods' => 'GET, POST, PUT, DELETE, OPTIONS',
|
||||
'Access-Control-Allow-Headers' => 'Origin, Content-Type, Accept, Authorization, X-Request-With, token',
|
||||
'Access-Control-Allow-Credentials' => 'true',
|
||||
]);
|
||||
}
|
||||
|
||||
$response = $next($request);
|
||||
|
||||
// 设置跨域响应头
|
||||
$response->header([
|
||||
'Access-Control-Allow-Origin' => '*',
|
||||
'Access-Control-Allow-Methods' => 'GET, POST, PUT, DELETE, OPTIONS',
|
||||
'Access-Control-Allow-Headers' => 'Origin, Content-Type, Accept, Authorization, X-Request-With, token',
|
||||
'Access-Control-Allow-Credentials' => 'true',
|
||||
]);
|
||||
|
||||
return $response;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,157 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace app\api\model;
|
||||
|
||||
use think\Model;
|
||||
|
||||
/**
|
||||
* 用户模型 (对应原系统 member 表)
|
||||
* 数据库连接: dbmember (member库)
|
||||
*/
|
||||
class Member extends Model
|
||||
{
|
||||
// 设置连接名 (对应 config/database.php 中的 connections)
|
||||
protected $connection = 'dbmember';
|
||||
|
||||
// 表名
|
||||
protected $name = 'member';
|
||||
|
||||
// 主键
|
||||
protected $pk = 'userid';
|
||||
|
||||
// 自动时间戳
|
||||
protected $autoWriteTimestamp = false;
|
||||
|
||||
// 隐藏字段
|
||||
protected $hidden = ['password'];
|
||||
|
||||
/**
|
||||
* 根据用户名查找用户
|
||||
* @param string $username
|
||||
* @return Member|null
|
||||
*/
|
||||
public static function findByUsername(string $username): ?Member
|
||||
{
|
||||
return self::where('username', $username)->find();
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据用户ID查找用户
|
||||
* @param int $userid
|
||||
* @return Member|null
|
||||
*/
|
||||
public static function findByUserid(int $userid): ?Member
|
||||
{
|
||||
return self::where('userid', $userid)->find();
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证密码
|
||||
* 支持两种密码格式:
|
||||
* 1. 新格式: bcrypt hash (60字符, 以 $2y$ 开头)
|
||||
* 2. 旧格式: 双重MD5 (32字符)
|
||||
*
|
||||
* @param string $password 明文密码
|
||||
* @return bool
|
||||
*/
|
||||
public function verifyPassword(string $password): bool
|
||||
{
|
||||
$hash = $this->password;
|
||||
|
||||
// 新格式: bcrypt
|
||||
if (strlen($hash) === 60 && strpos($hash, '$2y$') === 0) {
|
||||
return password_verify($password, $hash);
|
||||
}
|
||||
|
||||
// 旧格式: 双重MD5 (兼容原系统)
|
||||
$legacyHash = md5(md5($password));
|
||||
return $legacyHash === $hash;
|
||||
}
|
||||
|
||||
/**
|
||||
* 升级密码为 bcrypt 格式
|
||||
* @param string $password 明文密码
|
||||
* @return bool
|
||||
*/
|
||||
public function upgradePassword(string $password): bool
|
||||
{
|
||||
$this->password = password_hash($password, PASSWORD_DEFAULT);
|
||||
return $this->save();
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查用户是否被禁用
|
||||
* @return bool
|
||||
*/
|
||||
public function isDisabled(): bool
|
||||
{
|
||||
return $this->disabled == 1;
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查账号是否过期
|
||||
* @return bool
|
||||
*/
|
||||
public function isExpired(): bool
|
||||
{
|
||||
if (empty($this->endtime)) {
|
||||
return false;
|
||||
}
|
||||
return $this->endtime < time();
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取用户套餐信息
|
||||
* @return array|null
|
||||
*/
|
||||
public function getProductInfo(): ?array
|
||||
{
|
||||
// 从主库获取套餐信息
|
||||
$product = \think\facade\Db::connect('dbbiz')
|
||||
->name('product_list')
|
||||
->where('v_type', $this->v_type)
|
||||
->find();
|
||||
|
||||
return $product;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取代理商信息
|
||||
* @return array|null
|
||||
*/
|
||||
public function getAgentInfo(): ?array
|
||||
{
|
||||
if (empty($this->formtypeid)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return self::where('userid', $this->formtypeid)->find();
|
||||
}
|
||||
|
||||
/**
|
||||
* 记录登录日志
|
||||
* @param bool $success
|
||||
* @param string $loginType
|
||||
* @return void
|
||||
*/
|
||||
public function logLogin(bool $success, string $loginType = 'password'): void
|
||||
{
|
||||
try {
|
||||
\think\facade\Db::connect('dbmember')
|
||||
->name('member_login_log')
|
||||
->insert([
|
||||
'userid' => $this->userid,
|
||||
'ip' => request()->ip(),
|
||||
'time' => time(),
|
||||
'succeed' => $success ? 1 : 0,
|
||||
'diqu' => '',
|
||||
'login_type' => $loginType,
|
||||
'adminid' => 0,
|
||||
'v_type' => $this->v_type ?? 0,
|
||||
]);
|
||||
} catch (\Exception $e) {
|
||||
// 日志记录失败不影响登录
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
<?php
|
||||
declare (strict_types = 1);
|
||||
|
||||
namespace app\api\model;
|
||||
|
||||
use think\Model;
|
||||
|
||||
/**
|
||||
* 用户模型
|
||||
*/
|
||||
class User extends Model
|
||||
{
|
||||
// 表名
|
||||
protected $name = 'user';
|
||||
|
||||
// 自动写入时间戳
|
||||
protected $autoWriteTimestamp = true;
|
||||
|
||||
// 类型转换
|
||||
protected $type = [
|
||||
'id' => 'integer',
|
||||
'status' => 'integer',
|
||||
];
|
||||
|
||||
// 隐藏字段
|
||||
protected $hidden = ['password', 'delete_time'];
|
||||
|
||||
/**
|
||||
* 密码加密
|
||||
* @param string $value
|
||||
* @return string
|
||||
*/
|
||||
public function setPasswordAttr(string $value): string
|
||||
{
|
||||
return password_hash($value, PASSWORD_DEFAULT);
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证密码
|
||||
* @param string $password 明文密码
|
||||
* @param string $hash 加密后的密码
|
||||
* @return bool
|
||||
*/
|
||||
public static function verifyPassword(string $password, string $hash): bool
|
||||
{
|
||||
return password_verify($password, $hash);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
use think\facade\Route;
|
||||
use app\api\controller\Index;
|
||||
use app\api\controller\User;
|
||||
use app\api\controller\V1Auth;
|
||||
|
||||
/**
|
||||
* API 应用路由
|
||||
*/
|
||||
|
||||
// ==================== v1 版本接口 ====================
|
||||
|
||||
// 健康检查 (公开)
|
||||
Route::get('v1/health', [Index::class, 'health']);
|
||||
|
||||
// 认证接口 (公开)
|
||||
Route::post('v1/auth/login', [V1Auth::class, 'login']);
|
||||
Route::post('v1/auth/register', [V1Auth::class, 'register']);
|
||||
Route::post('v1/auth/refresh', [V1Auth::class, 'refresh']);
|
||||
|
||||
// 认证接口 (需登录)
|
||||
Route::group('v1/auth', function () {
|
||||
Route::get('me', [V1Auth::class, 'me']);
|
||||
Route::post('logout', [V1Auth::class, 'logout']);
|
||||
Route::post('password', [V1Auth::class, 'password']);
|
||||
})->middleware(\app\api\middleware\Auth::class);
|
||||
|
||||
// ==================== 兼容旧版路由 ====================
|
||||
|
||||
Route::get('index', [Index::class, 'index']);
|
||||
Route::get('health', [Index::class, 'health']);
|
||||
Route::post('user/login', [V1Auth::class, 'login']);
|
||||
Route::post('user/register', [V1Auth::class, 'register']);
|
||||
Route::get('user/info', [V1Auth::class, 'me'])->middleware(\app\api\middleware\Auth::class);
|
||||
@@ -0,0 +1,201 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace app\api\service;
|
||||
|
||||
use app\api\common\Jwt;
|
||||
use app\api\model\Member;
|
||||
|
||||
/**
|
||||
* 认证服务
|
||||
* 处理用户登录、注册、Token 管理等
|
||||
*/
|
||||
class AuthService
|
||||
{
|
||||
/**
|
||||
* 用户登录
|
||||
* @param string $username 用户名
|
||||
* @param string $password 密码
|
||||
* @return array
|
||||
* @throws \Exception
|
||||
*/
|
||||
public function login(string $username, string $password): array
|
||||
{
|
||||
// 查找用户
|
||||
$member = Member::findByUsername($username);
|
||||
if (!$member) {
|
||||
throw new \Exception('用户名或密码错误', 4001);
|
||||
}
|
||||
|
||||
// 检查是否被禁用
|
||||
if ($member->isDisabled()) {
|
||||
$member->logLogin(false, 'password');
|
||||
throw new \Exception('账号已被禁用', 4002);
|
||||
}
|
||||
|
||||
// 验证密码
|
||||
if (!$member->verifyPassword($password)) {
|
||||
$member->logLogin(false, 'password');
|
||||
throw new \Exception('用户名或密码错误', 4001);
|
||||
}
|
||||
|
||||
// 检查是否过期
|
||||
if ($member->isExpired()) {
|
||||
$member->logLogin(false, 'password');
|
||||
throw new \Exception('账号已过期,请联系客服续费', 4003);
|
||||
}
|
||||
|
||||
// 密码升级:旧MD5格式自动升级为bcrypt
|
||||
if (strlen($member->password) === 32) {
|
||||
$member->upgradePassword($password);
|
||||
}
|
||||
|
||||
// 记录登录日志
|
||||
$member->logLogin(true, 'password');
|
||||
|
||||
// 生成 Token
|
||||
$token = Jwt::encode([
|
||||
'userid' => $member->userid,
|
||||
'username' => $member->username,
|
||||
'v_type' => $member->v_type,
|
||||
]);
|
||||
|
||||
$refreshToken = Jwt::refreshToken($member->userid);
|
||||
|
||||
// 返回用户信息
|
||||
return [
|
||||
'token' => $token,
|
||||
'refresh_token' => $refreshToken,
|
||||
'expires_in' => config('jwt.expire', 604800),
|
||||
'user' => [
|
||||
'userid' => $member->userid,
|
||||
'username' => $member->username,
|
||||
'v_type' => $member->v_type,
|
||||
'endtime' => $member->endtime,
|
||||
'formtypeid' => $member->formtypeid,
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* 用户注册
|
||||
* @param string $username 用户名
|
||||
* @param string $password 密码
|
||||
* @param string|null $email 邮箱
|
||||
* @param int|null $formtypeid 代理商ID
|
||||
* @return array
|
||||
* @throws \Exception
|
||||
*/
|
||||
public function register(string $username, string $password, ?string $email = null, ?int $formtypeid = null): array
|
||||
{
|
||||
// 检查用户名是否已存在
|
||||
$exists = Member::findByUsername($username);
|
||||
if ($exists) {
|
||||
throw new \Exception('用户名已存在', 4004);
|
||||
}
|
||||
|
||||
// 创建用户
|
||||
$member = new Member();
|
||||
$member->username = $username;
|
||||
$member->password = password_hash($password, PASSWORD_DEFAULT);
|
||||
$member->email = $email;
|
||||
$member->formtypeid = $formtypeid ?? 0;
|
||||
$member->v_type = 0; // 默认套餐
|
||||
$member->disabled = 0;
|
||||
$member->endtime = 0;
|
||||
$member->regtime = time();
|
||||
$member->regip = request()->ip();
|
||||
|
||||
if (!$member->save()) {
|
||||
throw new \Exception('注册失败,请稍后重试', 5001);
|
||||
}
|
||||
|
||||
// 自动登录
|
||||
return $this->login($username, $password);
|
||||
}
|
||||
|
||||
/**
|
||||
* 刷新 Token
|
||||
* @param string $refreshToken
|
||||
* @return array
|
||||
* @throws \Exception
|
||||
*/
|
||||
public function refreshToken(string $refreshToken): array
|
||||
{
|
||||
$payload = Jwt::decode($refreshToken);
|
||||
if (!$payload || ($payload['type'] ?? '') !== 'refresh') {
|
||||
throw new \Exception('无效的刷新令牌', 4005);
|
||||
}
|
||||
|
||||
$member = Member::findByUserid($payload['userid']);
|
||||
if (!$member || $member->isDisabled()) {
|
||||
throw new \Exception('用户不存在或已被禁用', 4002);
|
||||
}
|
||||
|
||||
// 生成新 Token
|
||||
$token = Jwt::encode([
|
||||
'userid' => $member->userid,
|
||||
'username' => $member->username,
|
||||
'v_type' => $member->v_type,
|
||||
]);
|
||||
|
||||
return [
|
||||
'token' => $token,
|
||||
'expires_in' => config('jwt.expire', 604800),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取用户信息
|
||||
* @param int $userid
|
||||
* @return array
|
||||
* @throws \Exception
|
||||
*/
|
||||
public function getUserInfo(int $userid): array
|
||||
{
|
||||
$member = Member::findByUserid($userid);
|
||||
if (!$member) {
|
||||
throw new \Exception('用户不存在', 4006);
|
||||
}
|
||||
|
||||
// 获取套餐信息
|
||||
$productInfo = $member->getProductInfo();
|
||||
|
||||
return [
|
||||
'userid' => $member->userid,
|
||||
'username' => $member->username,
|
||||
'v_type' => $member->v_type,
|
||||
'endtime' => $member->endtime,
|
||||
'formtypeid' => $member->formtypeid,
|
||||
'disabled' => $member->disabled,
|
||||
'product' => $productInfo ? [
|
||||
'v_type' => $productInfo['v_type'] ?? null,
|
||||
'video_num' => $productInfo['video_num'] ?? 0,
|
||||
'account_num' => $productInfo['account_num'] ?? 0,
|
||||
] : null,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* 修改密码
|
||||
* @param int $userid
|
||||
* @param string $oldPassword
|
||||
* @param string $newPassword
|
||||
* @return bool
|
||||
* @throws \Exception
|
||||
*/
|
||||
public function changePassword(int $userid, string $oldPassword, string $newPassword): bool
|
||||
{
|
||||
$member = Member::findByUserid($userid);
|
||||
if (!$member) {
|
||||
throw new \Exception('用户不存在', 4006);
|
||||
}
|
||||
|
||||
if (!$member->verifyPassword($oldPassword)) {
|
||||
throw new \Exception('原密码错误', 4007);
|
||||
}
|
||||
|
||||
$member->password = password_hash($newPassword, PASSWORD_DEFAULT);
|
||||
return $member->save();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
<?php
|
||||
declare (strict_types = 1);
|
||||
|
||||
namespace app\api\validate;
|
||||
|
||||
use think\Validate;
|
||||
|
||||
/**
|
||||
* 用户验证器
|
||||
*/
|
||||
class User extends Validate
|
||||
{
|
||||
/**
|
||||
* 验证规则
|
||||
*/
|
||||
protected $rule = [
|
||||
'username' => 'require|length:3,20|chsDash',
|
||||
'password' => 'require|length:6,20',
|
||||
'email' => 'require|email',
|
||||
'phone' => 'mobile',
|
||||
'nickname' => 'length:2,20',
|
||||
];
|
||||
|
||||
/**
|
||||
* 验证提示信息
|
||||
*/
|
||||
protected $message = [
|
||||
'username.require' => '用户名不能为空',
|
||||
'username.length' => '用户名长度3-20位',
|
||||
'username.chsDash' => '用户名只能是汉字、字母、数字和下划线_及破折号-',
|
||||
'password.require' => '密码不能为空',
|
||||
'password.length' => '密码长度6-20位',
|
||||
'email.require' => '邮箱不能为空',
|
||||
'email.email' => '邮箱格式不正确',
|
||||
'phone.mobile' => '手机号格式不正确',
|
||||
'nickname.length' => '昵称长度2-20位',
|
||||
];
|
||||
|
||||
/**
|
||||
* 验证场景
|
||||
*/
|
||||
protected $scene = [
|
||||
'login' => ['username', 'password'],
|
||||
'register' => ['username', 'password', 'email'],
|
||||
'update' => ['email', 'phone', 'nickname'],
|
||||
];
|
||||
}
|
||||
Reference in New Issue
Block a user