// +---------------------------------------------------------------------- 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/ * 鉴权:除 $noNeedLogin 声明的公开接口外,其余接口均需 Bearer Authorization 登录态(由父类统一拦截)。 * * @package app\api\controller\v1 */ class User extends ApiBase { /** * 免登录(公开)接口白名单。 * * @var array */ protected $noNeedLogin = [ 'register', 'login', 'loginBySms', 'refresh', 'resetPwdBySms', 'resetPwdByEmail', ]; /** * 账号密码注册。 * * 普通注册仅需 account + password;若传入 mobile 则需先通过短信验证码校验。 * 成功后自动签发 JWT(access_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 验证密码; * 成功后记录登录信息并签发 JWT(access_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); } }