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
This commit is contained in:
ywxapp
2026-08-20 20:19:15 +08:00
parent 303f548664
commit 1d49e6f5ee
395 changed files with 13342 additions and 2012 deletions
+354 -162
View File
@@ -6,201 +6,393 @@
// +----------------------------------------------------------------------
// | Author: ywxapp<admin@ywxapp.cn>
// +----------------------------------------------------------------------
declare (strict_types = 1);
namespace app\Api\Controller\V1;
declare(strict_types=1);
use app\api\validate\Login as LoginValidate;
use think\facade\Event;
use think\facade\Validate;
use ywxapp\controller\ApiController;
use ywxapp\model\MemberUser as UserModel;
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;
class User extends ApiController
/**
* 会员账户接口(标准基础能力,为聊天 / 商城等应用铺路)
*
* 路由前缀:/api/v1/user/<action>
* 鉴权:除 $noNeedLogin 声明的公开接口外,其余接口均需 Bearer Authorization 登录态(由父类统一拦截)。
*
* @package app\api\controller\v1
*/
class User extends ApiBase
{
protected $noNeedLogin = ['*'];
protected $noNeedVerify = ['*'];
public function initialize()
{
$this->model = new \ywxapp\model\Members();
}
public function index()
{
return json(['message' => 'This is version 1 of the API']);
}
/**
* 注册会员.
* @route put /register, method:put
* @param string $username 用户名
* @param string $password 密码
* @param string $email 邮箱
* @param string $mobile 手机号
* @param string $code 验证码
* @return \think\Response
*/
public function register(\think\Request $request)
{
$data = $request->param();
$validate = validate([
'account|账户或手机号' => 'require',
'password' => 'alphaDash',
'code' => 'number|length:4',
'captcha' => 'alphaNum',
]);
if (! $validate->check($data)) {
$this->result->error($validate->getError());
}
$account = $request->param('account');
$password = $request->has('password') ? $request->param('password') : md5("xixingwl");
$code = $request->param('code');
if (Validate::is($account, 'email') && $request->has('code')) {
$ret = \ywxapp\library\Sms::instence()->check($account, $code, 'register');
if (! $ret) {
$this->result->error('Code is incorrect');
}
}
if (Validate::is($account, 'mobile') && $request->has('code')) {
$ret = \ywxapp\library\Sms::instence()->check($account, $code, 'register');
if (! $ret) {
$this->result->error('Code is incorrect');
}
}
$extend = [];
if ($request->param('avatar')) {
$extend['avatar'] = $request->param('avatar');
}
if ($request->param('nickname')) {
$extend['nickname'] = $request->param('nickname');
}
$this->user->create($account, $password, $extend);
$this->result->success(['userinfo' => $this->Member->info]);
}
/**
* Member Login.
* 免登录(公开)接口白名单。
*
* @param string $account 账号
* @param string $password 密码
* @return \think\Response
* @var array<int, string>
*/
public function login(\think\Request $request)
{
protected $noNeedLogin = [
'register',
'login',
'loginBySms',
'refresh',
'resetPwdBySms',
'resetPwdByEmail',
];
$data = $this->request->param();
/**
* 账号密码注册。
*
* 普通注册仅需 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 {
validate(LoginValidate::class)->check($data);
$info = UserModel::where('account', $data['username'])
->whereOr('mobile', $data['username'])
->whereOr('email', $data['username'])
->findOrEmpty();
if ($info->isEmpty()) {
$this->result->error('用户不存在', 4010);
}
// 检查账户状态
if ($info->status == 0) {
$this->result->error('账号已被禁用', 403);
}
// 检查是否被锁定
if ($info->isLocked()) {
$lockTime = strtotime($info->lock_time) + 1800 - time();
$minutes = ceil($lockTime / 60);
$this->result->error("账号被锁定,请 {$minutes} 分钟后重试", 403);
}
$info->resetPassword($data['password']);
// 验证密码
if (! $info->checkPassword($data['password'])) {
$info->recordLoginFail($this->request->ip()); // 记录失败
$this->result->error('密码错误', 4011);
}
Event::trigger('MemberLog', [
'uid' => $info->uid,
'action' => 'login',
'ip' => $this->request->ip(),
'remark' => '用户注册',
]);
$newClaims = [
'uid' => $info->uid,
'account' => $info->account,
];
JwtService::instance()->createToken($newClaims);
$this->result->success($info);
} catch (ValidateException $e) {
$this->result->error($e->getMessage(), 1);
$data = JwtService::instance()->refreshAccessToken($refreshToken);
return $this->apiSuccess($data, '刷新成功');
} catch (\Throwable $e) {
return $this->apiError($e->getMessage(), 401, null, 401);
}
}
/**
* Get user detail
* @route get /detail, method:get
* @param int $uid
* @return \think\Response
* 退出登录。
*
* 系统采用无状态 JWT,服务端不维护会话;客户端收到成功响应后自行丢弃本地 token 即可。
*
* @return \think\Response JSON 响应,成功提示
*
* @route POST /api/v1/user/logout
*/
public function detail(\think\Request $request, int $uid = 0)
public function logout()
{
if (! $uid) {
$this->result->error('Invalid parameters', 404);
}
$this->result->success($this->user->info);
return $this->apiSuccess([], '已登出');
}
/**
* update user detail
* @route put /update, method:get
* @param int $uid
* @return \think\Response
* 获取当前登录会员资料(需登录)。
*
* 返回会员模型数组(password 等敏感字段已由模型 hidden 自动隐藏)。
*
* @return \think\Response JSON 响应,携带会员信息数组
*
* @route GET /api/v1/user/profile
*/
public function update(\think\Request $request, int $uid = 0)
public function profile()
{
if (! $uid) {
$this->result->error('Invalid parameters', 404);
}
$this->result->success($this->user->info);
$user = $this->auth->model;
return $this->apiSuccess($user->toArray());
}
/**
* DELETE user detail
* @route DELETE /update, method:DELETE
* @param int $uid
* @return \think\Response
* 修改当前会员资料(需登录)。
*
* 仅更新传入的非空字段(nickname / avatar / email),
* 邮箱需通过格式校验且未被其他会员占用。
*
* @param string $nickname 昵称(可选)
* @param string $avatar 头像地址(可选)
* @param string $email 邮箱(可选,唯一)
*
* @return \think\Response JSON 响应,携带更新后的会员信息
*
* @route POST /api/v1/user/updateProfile
*/
public function delete(\think\Request $request, int $uid = 0)
public function updateProfile()
{
if (! $uid) {
$this->result->error('Invalid parameters', 404);
$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(), '保存成功');
}
private function getEncryptPassword($password, $salt = '')
/**
* 修改密码(需登录)。
*
* 先校验原密码正确,再校验新密码长度(至少 6 位),通过后通过 resetPassword 更新。
*
* @param string $oldpassword 原密码(必填)
* @param string $newpassword 新密码(必填,至少 6 位)
*
* @return \think\Response JSON 响应,成功提示
*
* @route POST /api/v1/user/changePwd
*/
public function changePwd()
{
return md5(md5($password) . $salt);
$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([], '密码修改成功');
}
public function init()
/**
* 短信验证码找回密码(公开)。
*
* 校验 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()
{
return json(['message' => 'This is version 1 of the API']);
$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);
}
}