Files
YwxAppThink/app/api/controller/v1/User.php
T
ywxapp 1d49e6f5ee feat: 公共表更名 common/member 前缀 + 插件命令自动加载 + 版本 1.1.1
- 18 张公共表更名(addon/attachment/configure/links/spider_log/spider_stat/sms/notice/ad/task/prop/medal/help/card -> common_*,member_wallets->member_wallet,score_rule/score_log -> member_*,addon_config->common_addonconf),模型全部对齐新表名,Db 直引用清零
- Attachment 模型补  表名绑定,修复富文本上传查 wxapp_attachment 1146 隐患
- install.sql + 迁移 SQL:backend_admin/backend_role delete_at 默认 0,修复软删除(NULL != 0)误过滤导致后台菜单为空
- AppService::boot() 支持插件 info.php 声明 commands 自动注册插件命令(psr-4 自动加载,坏类名自动跳过)
- 各插件(haonav/mqttbroker/wxchat/articles/blog/forum 等)字段与配置同步调整
- 框架版本 1.1.0 -> 1.1.1
2026-08-20 20:19:15 +08:00

399 lines
14 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<?php
// +----------------------------------------------------------------------
// | YwxApp [ WE CAN DO IT JUST THINK ]
// +----------------------------------------------------------------------
// | Copyright (c) 2026-2036 http://ywxapp.cn All rights reserved.
// +----------------------------------------------------------------------
// | Author: ywxapp<admin@ywxapp.cn>
// +----------------------------------------------------------------------
declare(strict_types=1);
namespace app\api\controller\v1;
use think\exception\ValidateException;
use ywxapp\controller\ApiBase;
use ywxapp\service\JwtService;
use ywxapp\library\Sms as SmsLib;
use ywxapp\library\Email as EmailLib;
use ywxapp\model\MemberUser as UserModel;
/**
* 会员账户接口(标准基础能力,为聊天 / 商城等应用铺路)
*
* 路由前缀:/api/v1/user/<action>
* 鉴权:除 $noNeedLogin 声明的公开接口外,其余接口均需 Bearer Authorization 登录态(由父类统一拦截)。
*
* @package app\api\controller\v1
*/
class User extends ApiBase
{
/**
* 免登录(公开)接口白名单。
*
* @var array<int, string>
*/
protected $noNeedLogin = [
'register',
'login',
'loginBySms',
'refresh',
'resetPwdBySms',
'resetPwdByEmail',
];
/**
* 账号密码注册。
*
* 普通注册仅需 account + password;若传入 mobile 则需先通过短信验证码校验。
* 成功后自动签发 JWTaccess_token / refresh_token 由响应自动携带)。
*
* @param string $account 登录账号(必填,唯一)
* @param string $password 登录密码(必填,至少 6 位)
* @param string $mobile 手机号(可选,注册时必需短信验证码)
* @param string $captcha 短信验证码(mobile 传入时必填)
* @param string $nickname 昵称(可选,默认同 account)
*
* @return \think\Response JSON 响应,成功携带会员信息与 token
*
* @route POST /api/v1/user/register
*/
public function register()
{
$account = trim((string) $this->request->post('account', ''));
$password = (string) $this->request->post('password', '');
$mobile = trim((string) $this->request->post('mobile', ''));
$captcha = trim((string) $this->request->post('captcha', ''));
$nickname = trim((string) $this->request->post('nickname', ''));
if ($account === '' || $password === '') {
return $this->apiError('账号和密码不能为空');
}
if (strlen($password) < 6) {
return $this->apiError('密码至少 6 位');
}
if (UserModel::getByAccount($account)) {
return $this->apiError('该账号已被注册');
}
// 手机号注册需校验短信验证码
if ($mobile !== '') {
if ($captcha === '' || ! SmsLib::check($mobile, $captcha, 'register')) {
return $this->apiError('短信验证码不正确');
}
if (UserModel::getByMobile($mobile)) {
return $this->apiError('该手机号已被注册');
}
}
$user = new UserModel();
$user->account = $account;
$user->password = $password; // 触发 setPasswordAttr 自动 bcrypt
$user->nickname = $nickname ?: $account;
if ($mobile !== '') {
$user->mobile = $mobile;
}
$user->status = 1;
$user->save();
return $this->issueToken($user, '注册成功');
}
/**
* 账号密码登录。
*
* 校验账号存在、状态正常、未被锁定,并通过 checkPassword 验证密码;
* 成功后记录登录信息并签发 JWTaccess_token / refresh_token 由响应自动携带)。
*
* @param string $account 登录账号(必填)
* @param string $password 登录密码(必填)
*
* @return \think\Response JSON 响应,成功携带会员信息与 token
*
* @route POST /api/v1/user/login
*/
public function login()
{
$account = trim((string) $this->request->post('account', ''));
$password = (string) $this->request->post('password', '');
$user = UserModel::getByAccount($account);
if (!$user) {
return $this->apiError('账号不存在');
}
if ($user->status != 1) {
return $this->apiError('账号已被禁用');
}
if ($user->isLocked()) {
return $this->apiError('账号已锁定,请稍后再试');
}
if (!$user->checkPassword($password)) {
$user->recordLoginFail($this->request->ip());
return $this->apiError('密码错误');
}
$user->recordLogin($this->request->ip());
return $this->issueToken($user, '登录成功');
}
/**
* 短信验证码登录 / 一键注册。
*
* 校验 mobile + captcha 通过后,若该手机号已注册则直接登录,
* 否则自动创建账号并登录。成功后签发 JWT。
*
* @param string $mobile 手机号(必填)
* @param string $captcha 短信验证码(必填)
*
* @return \think\Response JSON 响应,成功携带会员信息与 token
*
* @route POST /api/v1/user/loginBySms
*/
public function loginBySms()
{
$mobile = trim((string) $this->request->post('mobile', ''));
$captcha = trim((string) $this->request->post('captcha', ''));
if ($mobile === '' || ! SmsLib::check($mobile, $captcha, 'mobilelogin')) {
return $this->apiError('短信验证码不正确');
}
$user = UserModel::getByMobile($mobile);
if (!$user) {
// 该手机号未注册 → 自动注册
$user = new UserModel();
$user->account = 'u' . $mobile;
$user->password = mt_rand(100000, 999999); // 随机初始密码(仅短信登录,密码登录不可用)
$user->mobile = $mobile;
$user->nickname = '用户' . substr($mobile, -4);
$user->status = 1;
$user->save();
} elseif ($user->status != 1) {
return $this->apiError('账号已被禁用');
}
$user->recordLogin($this->request->ip());
return $this->issueToken($user, '登录成功');
}
/**
* 使用 refresh_token 刷新 access_token。
*
* 优先读取请求体中的 refresh_token,缺失时回退到 Header / Cookie。
* 调用 JwtService::refreshAccessToken 校验并签发新的 access_token。
*
* @param string $refresh_token 刷新令牌(请求体 / Header / Cookie
*
* @return \think\Response JSON 响应,成功携带新的 token 对;失败返回 401
*
* @throws \Throwable 当 refresh_token 非法或过期时
*
* @route POST /api/v1/user/refresh
*/
public function refresh()
{
$refreshToken = trim((string) $this->request->post('refresh_token', ''));
if ($refreshToken === '') {
// 兼容从 Cookie / Header 读取
$refreshToken = $this->request->header('refresh_token', '') ?: $this->request->cookie('refresh_token', '');
}
if ($refreshToken === '') {
return $this->apiError('缺少 refresh_token', 401, null, 401);
}
try {
$data = JwtService::instance()->refreshAccessToken($refreshToken);
return $this->apiSuccess($data, '刷新成功');
} catch (\Throwable $e) {
return $this->apiError($e->getMessage(), 401, null, 401);
}
}
/**
* 退出登录。
*
* 系统采用无状态 JWT,服务端不维护会话;客户端收到成功响应后自行丢弃本地 token 即可。
*
* @return \think\Response JSON 响应,成功提示
*
* @route POST /api/v1/user/logout
*/
public function logout()
{
return $this->apiSuccess([], '已登出');
}
/**
* 获取当前登录会员资料(需登录)。
*
* 返回会员模型数组(password 等敏感字段已由模型 hidden 自动隐藏)。
*
* @return \think\Response JSON 响应,携带会员信息数组
*
* @route GET /api/v1/user/profile
*/
public function profile()
{
$user = $this->auth->model;
return $this->apiSuccess($user->toArray());
}
/**
* 修改当前会员资料(需登录)。
*
* 仅更新传入的非空字段(nickname / avatar / email),
* 邮箱需通过格式校验且未被其他会员占用。
*
* @param string $nickname 昵称(可选)
* @param string $avatar 头像地址(可选)
* @param string $email 邮箱(可选,唯一)
*
* @return \think\Response JSON 响应,携带更新后的会员信息
*
* @route POST /api/v1/user/updateProfile
*/
public function updateProfile()
{
$user = $this->auth->model;
$nickname = trim((string) $this->request->post('nickname', ''));
$avatar = trim((string) $this->request->post('avatar', ''));
$email = trim((string) $this->request->post('email', ''));
if ($nickname !== '') {
$user->nickname = $nickname;
}
if ($avatar !== '') {
$user->avatar = $avatar;
}
if ($email !== '') {
if (!filter_var($email, FILTER_VALIDATE_EMAIL)) {
return $this->apiError('邮箱格式不正确');
}
$exists = UserModel::getByEmail($email);
if ($exists && $exists->uid != $user->uid) {
return $this->apiError('该邮箱已被占用');
}
$user->email = $email;
}
$user->save();
return $this->apiSuccess($user->toArray(), '保存成功');
}
/**
* 修改密码(需登录)。
*
* 先校验原密码正确,再校验新密码长度(至少 6 位),通过后通过 resetPassword 更新。
*
* @param string $oldpassword 原密码(必填)
* @param string $newpassword 新密码(必填,至少 6 位)
*
* @return \think\Response JSON 响应,成功提示
*
* @route POST /api/v1/user/changePwd
*/
public function changePwd()
{
$old = (string) $this->request->post('oldpassword', '');
$new = (string) $this->request->post('newpassword', '');
$user = $this->auth->model;
if (!$user->checkPassword($old)) {
return $this->apiError('原密码错误');
}
if (strlen($new) < 6) {
return $this->apiError('新密码至少 6 位');
}
$user->resetPassword($new);
return $this->apiSuccess([], '密码修改成功');
}
/**
* 短信验证码找回密码(公开)。
*
* 校验 mobile + captcha(事件 resetpwd)通过后,若该手机号已注册则重置其密码。
*
* @param string $mobile 手机号(必填)
* @param string $captcha 短信验证码(必填)
* @param string $newpassword 新密码(必填,至少 6 位)
*
* @return \think\Response JSON 响应,成功提示
*
* @route POST /api/v1/user/resetPwdBySms
*/
public function resetPwdBySms()
{
$mobile = trim((string) $this->request->post('mobile', ''));
$captcha = trim((string) $this->request->post('captcha', ''));
$new = (string) $this->request->post('newpassword', '');
if ($new === '' || strlen($new) < 6) {
return $this->apiError('新密码至少 6 位');
}
if (! SmsLib::check($mobile, $captcha, 'resetpwd')) {
return $this->apiError('短信验证码不正确');
}
$user = UserModel::getByMobile($mobile);
if (!$user) {
return $this->apiError('该手机号未注册');
}
$user->resetPassword($new);
return $this->apiSuccess([], '密码重置成功');
}
/**
* 邮箱验证码找回密码(公开)。
*
* 校验 email + captcha(事件 resetpwd)通过后,若该邮箱已注册则重置其密码。
*
* @param string $email 邮箱(必填)
* @param string $captcha 邮箱验证码(必填)
* @param string $newpassword 新密码(必填,至少 6 位)
*
* @return \think\Response JSON 响应,成功提示
*
* @route POST /api/v1/user/resetPwdByEmail
*/
public function resetPwdByEmail()
{
$email = trim((string) $this->request->post('email', ''));
$captcha = trim((string) $this->request->post('captcha', ''));
$new = (string) $this->request->post('newpassword', '');
if ($new === '' || strlen($new) < 6) {
return $this->apiError('新密码至少 6 位');
}
if (! EmailLib::instance()->check($email, $captcha, 'resetpwd')) {
return $this->apiError('邮箱验证码不正确');
}
$user = UserModel::getByEmail($email);
if (!$user) {
return $this->apiError('该邮箱未注册');
}
$user->resetPassword($new);
return $this->apiSuccess([], '密码重置成功');
}
/**
* 签发 JWT 并通过标准响应返回会员信息。
*
* 调用 JwtService::createToken 生成 access_token / refresh_token(由 Result 自动随响应输出)。
*
* @param UserModel $user 会员模型
* @param string $msg 成功提示语
*
* @return \think\Response JSON 响应,携带会员信息与 token
*/
protected function issueToken(UserModel $user, string $msg = 'success')
{
JwtService::instance()->createToken([
'uid' => $user->uid,
'account' => $user->account,
]);
$data = $user->toArray();
return $this->apiSuccess($data, $msg);
}
}