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:
@@ -11,7 +11,7 @@ declare(strict_types=1);
|
||||
|
||||
namespace app\Api\Controller\V1;
|
||||
|
||||
use ywxapp\controller\ApiController;
|
||||
use ywxapp\controller\ApiBase;
|
||||
use ywxapp\service\RemoteService;
|
||||
use think\Request;
|
||||
|
||||
@@ -26,7 +26,7 @@ use think\Request;
|
||||
*
|
||||
* 中心站领域数据(appmarket_* 表)已全部收敛到 addon/appmall,核心不再直接读写。
|
||||
*/
|
||||
class Addon extends ApiController
|
||||
class Addon extends ApiBase
|
||||
{
|
||||
// notify/payResult 由支付平台回调/跳转,无需登录;info 公开
|
||||
protected $noNeedLogin = ['info', 'notify', 'payResult'];
|
||||
|
||||
@@ -11,10 +11,10 @@ declare (strict_types = 1);
|
||||
namespace app\Api\Controller\V1;
|
||||
|
||||
use think\facade\Validate;
|
||||
use ywxapp\controller\ApiController;
|
||||
use ywxapp\controller\ApiBase;
|
||||
use addon\articles\model\Article as ArticleModel;
|
||||
|
||||
class Article extends ApiController
|
||||
class Article extends ApiBase
|
||||
{
|
||||
protected $noNeedLogin = ['*'];
|
||||
protected $needRight = ['*'];
|
||||
|
||||
@@ -0,0 +1,132 @@
|
||||
<?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\facade\Filesystem;
|
||||
use ywxapp\controller\ApiBase;
|
||||
use ywxapp\model\SystemConfig;
|
||||
|
||||
/**
|
||||
* 通用基础接口(App 启动所需,聊天 / 商城等应用共用)
|
||||
*
|
||||
* 路由前缀:/api/v1/common/<action>
|
||||
* 鉴权:config / init 公开;upload 需登录(防滥用)。
|
||||
*
|
||||
* @package app\api\controller\v1
|
||||
*/
|
||||
class Common extends ApiBase
|
||||
{
|
||||
/**
|
||||
* 免登录(公开)接口白名单。
|
||||
*
|
||||
* @var array<int, string>
|
||||
*/
|
||||
protected $noNeedLogin = ['config', 'init'];
|
||||
|
||||
/**
|
||||
* App 启动初始化数据(公开)。
|
||||
*
|
||||
* 返回站点基础信息、可用的注册方式开关、接口版本等,供客户端冷启动时读取。
|
||||
*
|
||||
* @return \think\Response JSON 响应,携带 site / register_methods / version
|
||||
*
|
||||
* @route GET /api/v1/common/init
|
||||
*/
|
||||
public function init()
|
||||
{
|
||||
$config = $this->siteConfig();
|
||||
$data = [
|
||||
'site' => $config,
|
||||
'register_methods' => [
|
||||
'password' => 1,
|
||||
'sms' => 1,
|
||||
'email' => 1,
|
||||
],
|
||||
'version' => '1.0.0',
|
||||
];
|
||||
return $this->apiSuccess($data);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取站点配置(公开)。
|
||||
*
|
||||
* 返回站点名称、Logo、备案号等基础配置。
|
||||
*
|
||||
* @return \think\Response JSON 响应,携带站点配置数组
|
||||
*
|
||||
* @route GET /api/v1/common/config
|
||||
*/
|
||||
public function config()
|
||||
{
|
||||
return $this->apiSuccess($this->siteConfig());
|
||||
}
|
||||
|
||||
/**
|
||||
* 文件上传(需登录)。
|
||||
*
|
||||
* 接收 multipart/form-data 中的 file 字段,保存到本地存储并返回可访问 URL。
|
||||
*
|
||||
* @param \think\file\UploadedFile $file 上传的文件(form-data: file)
|
||||
*
|
||||
* @return \think\Response JSON 响应,成功携带 url / path
|
||||
*
|
||||
* @throws \Throwable 当文件存储失败时
|
||||
*
|
||||
* @route POST /api/v1/common/upload
|
||||
*/
|
||||
public function upload()
|
||||
{
|
||||
$file = $this->request->file('file');
|
||||
if (!$file) {
|
||||
return $this->apiError('请选择上传文件');
|
||||
}
|
||||
try {
|
||||
$path = Filesystem::disk('local')->putFile('uploads', $file);
|
||||
$url = Filesystem::disk('local')->url($path);
|
||||
return $this->apiSuccess([
|
||||
'url' => $url,
|
||||
'path' => $path,
|
||||
], '上传成功');
|
||||
} catch (\Throwable $e) {
|
||||
return $this->apiError('上传失败:' . $e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 读取站点配置。
|
||||
*
|
||||
* 优先从 SystemConfig 表读取,表不存在或字段缺失时回退到默认值。
|
||||
*
|
||||
* @return array<string, mixed> 站点配置数组(name / logo / icp)
|
||||
*/
|
||||
protected function siteConfig(): array
|
||||
{
|
||||
$defaults = [
|
||||
'name' => 'YwxApp',
|
||||
'logo' => '',
|
||||
'icp' => '',
|
||||
];
|
||||
try {
|
||||
$rows = SystemConfig::column('value', 'name');
|
||||
if ($rows) {
|
||||
foreach ($defaults as $k => $v) {
|
||||
if (isset($rows[$k])) {
|
||||
$defaults[$k] = $rows[$k];
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (\Throwable $e) {
|
||||
// 表不存在时用默认值
|
||||
}
|
||||
return $defaults;
|
||||
}
|
||||
}
|
||||
@@ -10,30 +10,41 @@
|
||||
declare (strict_types = 1);
|
||||
namespace app\api\controller\v1;
|
||||
|
||||
use ywxapp\controller\ApiController;
|
||||
use ywxapp\controller\ApiBase;
|
||||
use think\facade\Event;
|
||||
use ywxapp\model\MemberUser as UserModel;
|
||||
use ywxapp\library\Result;
|
||||
use ywxapp\library\Email as EmailLib;
|
||||
|
||||
/**
|
||||
* 邮箱验证码接口.
|
||||
* 邮箱验证码接口(公开,无需登录)
|
||||
*
|
||||
* 路由前缀:/api/v1/ems/<action>
|
||||
* 业务事件(event)由调用方约定,如 register / resetpwd / changepwd / changeemail 等。
|
||||
*
|
||||
* @package app\api\controller\v1
|
||||
*/
|
||||
class Ems extends ApiController
|
||||
class Ems extends ApiBase
|
||||
{
|
||||
protected $noNeedLogin = '*';
|
||||
protected $needRight = '*';
|
||||
|
||||
|
||||
public function initialize()
|
||||
{
|
||||
}
|
||||
/**
|
||||
* 免登录(公开)接口白名单。
|
||||
*
|
||||
* @var array<int, string>
|
||||
*/
|
||||
protected $noNeedLogin = ['*'];
|
||||
|
||||
/**
|
||||
* 发送验证码
|
||||
* 发送邮箱验证码(公开)。
|
||||
*
|
||||
* @param string $email 邮箱
|
||||
* @param string $event 事件名称
|
||||
* 触发 email_send 事件并调用 Email 服务下发验证码;
|
||||
* 按 event 校验账号是否已注册 / 占用 / 未注册。
|
||||
*
|
||||
* @param string $email 邮箱(必填)
|
||||
* @param string $event 事件名称(可选,默认 register)
|
||||
*
|
||||
* @return \think\Response JSON 响应,成功提示
|
||||
*
|
||||
* @route POST /api/v1/ems/send
|
||||
*/
|
||||
public function send()
|
||||
{
|
||||
@@ -43,24 +54,31 @@ class Ems extends ApiController
|
||||
Event::trigger('email_send', ['email'=>$email, 'event'=>'register'], true);
|
||||
$userinfo = UserModel::getByEmail($email);
|
||||
if ($event == 'register' && $userinfo)
|
||||
Result::instance()->error(('已被注册'));
|
||||
$this->apiError('已被注册');
|
||||
elseif (in_array($event, ['changeemail']) && $userinfo)
|
||||
Result::instance()->error(('已被占用'));
|
||||
$this->apiError('已被占用');
|
||||
elseif (in_array($event, ['changepwd', 'resetpwd']) && !$userinfo)
|
||||
Result::instance()->error(('未注册'));
|
||||
$this->apiError('未注册');
|
||||
$ret = \ywxapp\library\Email::instance()->sendEmail($email, null, $event);
|
||||
if (!$ret)
|
||||
Result::instance()->error(('发送失败'));
|
||||
$this->apiError('发送失败');
|
||||
|
||||
Result::instance()->success(('发送成功'));
|
||||
$this->apiSuccess([], '发送成功');
|
||||
}
|
||||
|
||||
/**
|
||||
* 检测验证码
|
||||
* 校验邮箱验证码(公开)。
|
||||
*
|
||||
* @param string $email 邮箱
|
||||
* @param string $event 事件名称
|
||||
* @param string $captcha 验证码
|
||||
* 校验邮箱格式、事件名称与验证码格式,并依 event 校验账号注册状态,
|
||||
* 最后调用 EmailLib::check 比对验证码。
|
||||
*
|
||||
* @param string $email 邮箱(必填)
|
||||
* @param string $event 事件名称(可选,默认 register)
|
||||
* @param string $captcha 验证码(必填)
|
||||
*
|
||||
* @return \think\Response JSON 响应,成功提示
|
||||
*
|
||||
* @route POST /api/v1/ems/check
|
||||
*/
|
||||
public function check()
|
||||
{
|
||||
@@ -74,20 +92,20 @@ class Ems extends ApiController
|
||||
'code' => 'Num',
|
||||
]);
|
||||
if (!$validate->check(['email' => $email, 'event' => $event, 'code' => $captcha]))
|
||||
Result::instance()->error($validate->getError());
|
||||
$this->apiError($validate->getError());
|
||||
|
||||
$userinfo = UserModel::where('email', $email)->find();
|
||||
if ($event == 'register' && $userinfo)
|
||||
$this->result->error(('已被注册'));
|
||||
$this->apiError('已被注册');
|
||||
elseif (in_array($event, ['changeemail']) && $userinfo)
|
||||
Result::instance()->error(('已被占用'));
|
||||
$this->apiError('已被占用');
|
||||
elseif (in_array($event, ['changepwd', 'resetpwd']) && !$userinfo)
|
||||
Result::instance()->error(('未注册'));
|
||||
$this->apiError('未注册');
|
||||
|
||||
$ret = EmailLib::instance()->check($email, $captcha, $event);
|
||||
if (!$ret)
|
||||
Result::instance()->error(('验证码不正确'));
|
||||
$this->apiError('验证码不正确');
|
||||
|
||||
Result::instance()->success(data: ('验证码正确'));
|
||||
$this->apiSuccess([], '验证码正确');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
// +----------------------------------------------------------------------
|
||||
declare(strict_types=1);
|
||||
namespace app\api\controller\v1;
|
||||
use ywxapp\controller\ApiController;
|
||||
use ywxapp\controller\ApiBase;
|
||||
|
||||
use think\Response;
|
||||
|
||||
@@ -17,7 +17,7 @@ use think\Response;
|
||||
*
|
||||
* @author ywxapp <admin@ywxapp.cn>
|
||||
*/
|
||||
class Example extends ApiController
|
||||
class Example extends ApiBase
|
||||
{
|
||||
|
||||
public function index(): Response
|
||||
|
||||
@@ -8,14 +8,14 @@
|
||||
// +----------------------------------------------------------------------
|
||||
declare (strict_types = 1);
|
||||
namespace app\Api\Controller\V1;
|
||||
use ywxapp\controller\ApiController;
|
||||
use ywxapp\controller\ApiBase;
|
||||
use ywxapp\library\Result;
|
||||
/**
|
||||
* Index 类
|
||||
*
|
||||
* @author ywxapp <admin@ywxapp.cn>
|
||||
*/
|
||||
class Index extends ApiController
|
||||
class Index extends ApiBase
|
||||
{
|
||||
|
||||
|
||||
|
||||
@@ -8,42 +8,70 @@
|
||||
// +----------------------------------------------------------------------
|
||||
declare (strict_types = 1);
|
||||
|
||||
namespace app\Api\Controller\V1;
|
||||
namespace app\api\controller\v1;
|
||||
|
||||
use think\facade\Event;
|
||||
use think\exception\ValidateException;
|
||||
use ywxapp\controller\ApiController;
|
||||
use ywxapp\controller\ApiBase;
|
||||
use ywxapp\library\Sms as Smslib;
|
||||
use ywxapp\model\Sms as SmsModel;
|
||||
use ywxapp\model\CommonSms as SmsModel;
|
||||
use ywxapp\model\MemberUser as UserModel;
|
||||
use ywxapp\utils\Random;
|
||||
|
||||
/**
|
||||
* Sms 类
|
||||
* 短信验证码接口(公开,无需登录)
|
||||
*
|
||||
* @author ywxapp <admin@ywxapp.cn>
|
||||
* 路由前缀:/api/v1/sms/<action>
|
||||
* 业务事件(event)由调用方约定,如 register / resetpwd / changepwd / mobilelogin 等。
|
||||
*
|
||||
* @package app\api\controller\v1
|
||||
*/
|
||||
class Sms extends ApiController
|
||||
class Sms extends ApiBase
|
||||
{
|
||||
|
||||
/**
|
||||
* 免登录(公开)接口白名单。
|
||||
*
|
||||
* @var array<int, string>
|
||||
*/
|
||||
protected $noNeedLogin = ['*'];
|
||||
|
||||
|
||||
/**
|
||||
* 接口存活探测。
|
||||
*
|
||||
* @return \think\Response JSON 响应,演示用
|
||||
*
|
||||
* @route GET /api/v1/sms/index
|
||||
*/
|
||||
public function index()
|
||||
{
|
||||
return json(['message' => 'SMS API']);
|
||||
}
|
||||
|
||||
/**
|
||||
* 发送验证码
|
||||
* 发送短信验证码(公开)。
|
||||
*
|
||||
* @ApiMethod (POST)
|
||||
* @ApiParams (name="mobile", type="string", required=true, description="手机号")
|
||||
* @ApiParams (name="event", type="string", required=true, description="事件名称")
|
||||
* @ApiParams (name="type", type="string", required=false, description="验证类型,auto为自动验证,system为系统验证码")
|
||||
* @ApiParams (name="source_id", type="string", required=false, description="来源ID")
|
||||
* 校验手机号与事件后,受发送频率(同号 60 秒、同 IP 每小时 5 条)限制;
|
||||
* 按 event 校验账号是否已注册 / 占用 / 未注册,最后触发 SmsSend 事件下发短信。
|
||||
*
|
||||
* @param string $mobile 手机号(必填)
|
||||
* @param string $event 事件名称(必填,小写字母,默认 register)
|
||||
* @param string $type 验证类型(可选,auto 自动 / system 系统验证码)
|
||||
* @param string $source_id 来源 ID(可选)
|
||||
*
|
||||
* @return \think\Response JSON 响应,成功提示
|
||||
*
|
||||
* @throws ValidateException 当参数校验失败时
|
||||
*
|
||||
* @route POST /api/v1/sms/send
|
||||
*/
|
||||
public function send()
|
||||
{
|
||||
{
|
||||
$cfg = config('smsbao');
|
||||
|
||||
dump($cfg);die;
|
||||
$mobile = $this->request->post("mobile");
|
||||
$event = $this->request->post("event", 'register');
|
||||
$event = $this->request->param("event", 'register');
|
||||
$type = $this->request->post("type", 'auto');
|
||||
$source_id = $this->request->post("source_id", '');
|
||||
try {
|
||||
@@ -62,46 +90,52 @@ class Sms extends ApiController
|
||||
]);
|
||||
$last = Smslib::get($mobile, $event);
|
||||
if ($last && time() - (int) $last['create_at'] < 60) {
|
||||
$this->result->error('发送频繁');
|
||||
$this->apiError('发送频繁');
|
||||
}
|
||||
$ipSendTotal = SmsModel::where(['ip' => $this->request->ip()])->whereTime('create_at', '-1 hours')->count();
|
||||
if ($ipSendTotal >= 5) {
|
||||
$this->result->error('发送频繁');
|
||||
$this->apiError('发送频繁');
|
||||
}
|
||||
if ($event) {
|
||||
$userinfo = UserModel::getByMobile($mobile);
|
||||
if ($event == 'register' && $userinfo) {
|
||||
//已被注册
|
||||
$this->result->error('已被注册');
|
||||
$this->apiError('已被注册');
|
||||
} elseif (in_array($event, ['changemobile']) && $userinfo) {
|
||||
//被占用
|
||||
$this->result->error('已被占用');
|
||||
$this->apiError('已被占用');
|
||||
} elseif (in_array($event, ['changepwd', 'resetpwd']) && ! $userinfo) {
|
||||
//未注册
|
||||
$this->result->error('未注册');
|
||||
$this->apiError('未注册');
|
||||
}
|
||||
}
|
||||
if (! Event::hasListener('SmsSend')) {
|
||||
$this->result->error('请在后台插件管理安装短信验证插件');
|
||||
$this->apiError('请在后台插件管理安装短信验证插件');
|
||||
}
|
||||
$ret = Smslib::send($mobile, null, $event);
|
||||
if ($ret) {
|
||||
$this->result->success('发送成功');
|
||||
$this->apiSuccess([], '发送成功');
|
||||
} else {
|
||||
$this->result->error('发送失败,请检查短信配置是否正确');
|
||||
$this->apiError('发送失败,请检查短信配置是否正确');
|
||||
}
|
||||
} catch (ValidateException $e) {
|
||||
$this->result->error($e->getError());
|
||||
$this->apiError($e->getError());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 检测验证码
|
||||
* 校验短信验证码(公开)。
|
||||
*
|
||||
* @ApiMethod (POST)
|
||||
* @ApiParams (name="mobile", type="string", required=true, description="手机号")
|
||||
* @ApiParams (name="event", type="string", required=true, description="事件名称")
|
||||
* @ApiParams (name="captcha", type="string", required=true, description="验证码")
|
||||
* 校验手机号、事件名称、验证码格式,并依 event 校验账号注册状态,
|
||||
* 最后调用 SmsLib::check 比对验证码(默认有效期 5 分钟)。
|
||||
*
|
||||
* @param string $mobile 手机号(必填)
|
||||
* @param string $event 事件名称(必填,默认 register)
|
||||
* @param string $captcha 验证码(必填)
|
||||
*
|
||||
* @return \think\Response JSON 响应,成功提示
|
||||
*
|
||||
* @route POST /api/v1/sms/check
|
||||
*/
|
||||
public function check()
|
||||
{
|
||||
@@ -109,33 +143,33 @@ class Sms extends ApiController
|
||||
$event = $this->request->post("event", 'register');
|
||||
$captcha = $this->request->post("captcha");
|
||||
if (! $mobile || ! \think\Validate::regex($mobile, "^1\d{10}$")) {
|
||||
$this->result->error('手机号不正确');
|
||||
$this->apiError('手机号不正确');
|
||||
}
|
||||
if (! preg_match("/^[a-z0-9_\-]{3,30}\$/i", $event)) {
|
||||
$this->result->error('事件名称错误');
|
||||
$this->apiError('事件名称错误');
|
||||
}
|
||||
if (! preg_match("/^[a-z0-9]{4,6}\$/i", $captcha)) {
|
||||
$this->result->error('验证码格式错误');
|
||||
$this->apiError('验证码格式错误');
|
||||
}
|
||||
|
||||
if ($event) {
|
||||
$userinfo = UserModel::getByMobile($mobile);
|
||||
if ($event == 'register' && $userinfo) {
|
||||
//已被注册
|
||||
$this->result->error('已被注册');
|
||||
$this->apiError('已被注册');
|
||||
} elseif (in_array($event, ['changemobile']) && $userinfo) {
|
||||
//被占用
|
||||
$this->result->error('已被占用');
|
||||
$this->apiError('已被占用');
|
||||
} elseif (in_array($event, ['changepwd', 'resetpwd']) && ! $userinfo) {
|
||||
//未注册
|
||||
$this->result->error('未注册');
|
||||
$this->apiError('未注册');
|
||||
}
|
||||
}
|
||||
$ret = Smslib::check($mobile, $captcha, $event);
|
||||
if ($ret) {
|
||||
$this->result->success('成功');
|
||||
$this->apiSuccess([], '成功');
|
||||
} else {
|
||||
$this->result->error('验证码不正确');
|
||||
$this->apiError('验证码不正确');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+354
-162
@@ -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 则需先通过短信验证码校验。
|
||||
* 成功后自动签发 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 {
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,7 +7,7 @@ use think\facade\Db;
|
||||
use think\db\exception\DbException;
|
||||
use think\exception\ValidateException;
|
||||
use ywxapp\controller\BackendBase;
|
||||
use ywxapp\model\Ad as AdModel;
|
||||
use ywxapp\model\CommonAd as AdModel;
|
||||
use app\backend\validate\Ad as AdValidate;
|
||||
|
||||
/**
|
||||
|
||||
@@ -48,7 +48,7 @@ class Card extends BackendBase
|
||||
$uids = array_filter(array_column($items, 'uid'));
|
||||
$users = [];
|
||||
if (!empty($uids)) {
|
||||
$users = Db::name('member')->whereIn('uid', array_unique($uids))
|
||||
$users = Db::name('member_user')->whereIn('uid', array_unique($uids))
|
||||
->column('nickname,username', 'uid');
|
||||
}
|
||||
foreach ($items as &$row) {
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
* @Author: YwxApp <ywx@ywxapp.cn>
|
||||
* @Date: 2026-04-29 02:32:33
|
||||
* @LastEditors: YwxApp <ywx@ywxapp.cn>
|
||||
* @LastEditTime: 2026-08-03 13:32:15
|
||||
* @LastEditTime: 2026-08-19 22:05:03
|
||||
* @Description:
|
||||
* @FilePath: \ywxapp_dev\app\backend\controller\Configure.php
|
||||
* @CustomString: Copyright (c) 2026 YwxApp
|
||||
@@ -14,7 +14,7 @@ namespace app\backend\controller;
|
||||
|
||||
use think\facade\View;
|
||||
use think\Request;
|
||||
use ywxapp\model\Configure as ConfigureModel;
|
||||
use ywxapp\model\CommonConfigure as ConfigureModel;
|
||||
use think\facade\Db;
|
||||
use think\facade\Cache;
|
||||
use ywxapp\controller\BackendBase;
|
||||
|
||||
@@ -64,10 +64,10 @@ class Console extends BackendBase
|
||||
public function hotsearch()
|
||||
{
|
||||
$admin = Db::name('backend_admin')->count();
|
||||
$user = Db::name('member')->count();
|
||||
$user = Db::name('member_user')->count();
|
||||
$article = Db::name('articles_article')->count();
|
||||
$links = Db::name('links')->count();
|
||||
$addon = Db::name('addon')->count();
|
||||
$links = Db::name('common_links')->count();
|
||||
$addon = Db::name('common_addon')->count();
|
||||
$data = [
|
||||
['keywords' => '管理员', 'frequency' => $admin, 'userNums' => $admin],
|
||||
['keywords' => '会员', 'frequency' => $user, 'userNums' => $user],
|
||||
|
||||
@@ -1,14 +1,21 @@
|
||||
<?php
|
||||
/*
|
||||
* @Author: YwxApp <ywx@ywxapp.cn>
|
||||
* @Date: 2026-08-07 09:44:40
|
||||
* @LastEditors: YwxApp <ywx@ywxapp.cn>
|
||||
* @LastEditTime: 2026-08-19 22:06:38
|
||||
* @Description:
|
||||
* @FilePath: \ywxapp_dev\app\backend\controller\Help.php
|
||||
* @CustomString: Copyright (c) 2026 YwxApp
|
||||
*/
|
||||
declare(strict_types=1);
|
||||
namespace app\backend\controller;
|
||||
|
||||
use think\Request;
|
||||
|
||||
use think\facade\Db;
|
||||
use think\db\exception\DbException;
|
||||
use think\exception\ValidateException;
|
||||
use ywxapp\controller\BackendBase;
|
||||
use ywxapp\model\BaseModel;
|
||||
use ywxapp\model\Help as HelpModel;
|
||||
use ywxapp\controller\BackendBase;
|
||||
use ywxapp\model\CommonHelp as HelpModel;
|
||||
use app\backend\validate\Help as HelpValidate;
|
||||
|
||||
/**
|
||||
@@ -22,7 +29,7 @@ class Help extends BackendBase
|
||||
protected function initialize()
|
||||
{
|
||||
// 运行时自愈 help 表结构(category / view_count 等扩展列)
|
||||
\ywxapp\model\Help::ensureSchema();
|
||||
\ywxapp\model\CommonHelp::ensureSchema();
|
||||
$this->model = new HelpModel();
|
||||
}
|
||||
|
||||
|
||||
@@ -17,7 +17,7 @@ use think\facade\Db;
|
||||
use think\db\exception\DbException;
|
||||
use think\exception\ValidateException;
|
||||
use ywxapp\controller\BackendBase;
|
||||
use ywxapp\model\Links as LinksModel;
|
||||
use ywxapp\model\CommonLinks as LinksModel;
|
||||
use app\backend\validate\Links as LinksValidate;
|
||||
|
||||
/**
|
||||
|
||||
@@ -7,7 +7,7 @@ use think\facade\Db;
|
||||
use think\db\exception\DbException;
|
||||
use think\exception\ValidateException;
|
||||
use ywxapp\controller\BackendBase;
|
||||
use ywxapp\model\Medal as MedalModel;
|
||||
use ywxapp\model\CommonMedal as MedalModel;
|
||||
use app\backend\validate\Medal as MedalValidate;
|
||||
|
||||
/**
|
||||
@@ -173,7 +173,7 @@ class Medal extends BackendBase
|
||||
$this->result->error('请选择勋章');
|
||||
}
|
||||
// 校验用户存在
|
||||
$user = Db::name('member')->where('uid', $uid)->find();
|
||||
$user = Db::name('member_user')->where('uid', $uid)->find();
|
||||
if (! $user) {
|
||||
$this->result->error('用户不存在');
|
||||
}
|
||||
|
||||
@@ -10,9 +10,10 @@ declare (strict_types = 1);
|
||||
|
||||
namespace app\backend\controller;
|
||||
|
||||
use think\facade\Cache;
|
||||
use think\facade\Request;
|
||||
use think\facade\View;
|
||||
use app\backend\model\NavMenu;
|
||||
use ywxapp\model\CommonNavMenu as NavMenu;
|
||||
use ywxapp\controller\BackendBase;
|
||||
|
||||
/**
|
||||
@@ -30,12 +31,56 @@ class Navbar extends BackendBase
|
||||
public function index()
|
||||
{
|
||||
if (Request::isAjax()) {
|
||||
$list = NavMenu::getAdminTree();
|
||||
return json(['code' => 0, 'msg' => 'ok', 'data' => $list, 'count' => count($list)]);
|
||||
$title = trim((string) Request::param('title', ''));
|
||||
$rows = NavMenu::getAdminTree($title);
|
||||
// 拍平成 treeTable isSimpleData 所需的扁平 pid 列表(treeTable 用 pid 自动建树)
|
||||
$flat = [];
|
||||
foreach ($rows as $p) {
|
||||
$p['pid'] = (int) $p['parent_id'];
|
||||
$flat[] = $p;
|
||||
foreach (($p['child'] ?? []) as $c) {
|
||||
$c['pid'] = (int) $c['parent_id'];
|
||||
$flat[] = $c;
|
||||
}
|
||||
}
|
||||
foreach ($flat as &$row) {
|
||||
unset($row['child']);
|
||||
}
|
||||
return json(['code' => 0, 'msg' => 'ok', 'data' => $flat, 'count' => count($flat)]);
|
||||
}
|
||||
// 已启用插件列表:导航「所属应用」可选插件(插件页面同样支持绑定域名)
|
||||
View::assign('plugins', $this->enabledPlugins());
|
||||
return View::fetch();
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取已启用插件名列表.
|
||||
*/
|
||||
private function enabledPlugins(): array
|
||||
{
|
||||
$raw = Cache::get('addon_loaded_config');
|
||||
if (! is_array($raw) || empty($raw)) {
|
||||
$addonDir = root_path() . 'addon' . DIRECTORY_SEPARATOR;
|
||||
$raw = [];
|
||||
if (is_dir($addonDir)) {
|
||||
foreach (array_diff(scandir($addonDir), ['.', '..']) as $dir) {
|
||||
$info = @include $addonDir . $dir . DIRECTORY_SEPARATOR . 'info.php';
|
||||
if (is_array($info)) {
|
||||
$raw[$dir] = $info;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
$plugins = [];
|
||||
foreach ($raw as $info) {
|
||||
if (! empty($info['state']) && ! empty($info['name'])) {
|
||||
$plugins[] = (string) $info['name'];
|
||||
}
|
||||
}
|
||||
sort($plugins);
|
||||
return $plugins;
|
||||
}
|
||||
|
||||
/**
|
||||
* 添加/编辑页(页面式,复用 edit.html).
|
||||
*/
|
||||
@@ -47,6 +92,7 @@ class Navbar extends BackendBase
|
||||
'parent_id' => (int) Request::post('parent_id', 0),
|
||||
'title' => trim((string) Request::post('title', '')),
|
||||
'url' => trim((string) Request::post('url', '')),
|
||||
'app' => trim((string) Request::post('app', '')),
|
||||
'icon' => trim((string) Request::post('icon', '')),
|
||||
'sort' => (int) Request::post('sort', 0),
|
||||
'status' => (int) Request::post('status', 1),
|
||||
@@ -86,6 +132,7 @@ class Navbar extends BackendBase
|
||||
'parent_id' => (int) Request::post('parent_id', 0),
|
||||
'title' => trim((string) Request::post('title', '')),
|
||||
'url' => trim((string) Request::post('url', '')),
|
||||
'app' => trim((string) Request::post('app', '')),
|
||||
'icon' => trim((string) Request::post('icon', '')),
|
||||
'sort' => (int) Request::post('sort', 0),
|
||||
'status' => (int) Request::post('status', 1),
|
||||
@@ -114,19 +161,25 @@ class Navbar extends BackendBase
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除(支持单 id;有子项时拒绝,避免孤儿).
|
||||
* 删除(支持单 id 或批量 ids 逗号分隔;有子项时拒绝,避免孤儿).
|
||||
*/
|
||||
public function delete($id = 0)
|
||||
{
|
||||
$id = $id ?: (int) Request::post('id', 0);
|
||||
if (! $id) {
|
||||
$ids = Request::post('ids', '');
|
||||
if ($ids !== '' && $ids !== null) {
|
||||
$ids = array_values(array_unique(array_filter(array_map('intval', explode(',', (string) $ids)))));
|
||||
} else {
|
||||
$id = $id ?: (int) Request::post('id', 0);
|
||||
$ids = $id ? [$id] : [];
|
||||
}
|
||||
if (empty($ids)) {
|
||||
return json(['code' => 1, 'msg' => '请选择要删除的项']);
|
||||
}
|
||||
$hasChild = NavMenu::where('parent_id', $id)->where('delete_at', 0)->count();
|
||||
$hasChild = NavMenu::where('parent_id', 'in', $ids)->where('delete_at', 0)->count();
|
||||
if ($hasChild) {
|
||||
return json(['code' => 1, 'msg' => '请先删除该菜单下的子项']);
|
||||
return json(['code' => 1, 'msg' => '请先删除所选菜单下的子项']);
|
||||
}
|
||||
NavMenu::destroy($id);
|
||||
NavMenu::destroy($ids);
|
||||
return json(['code' => 0, 'msg' => '已删除']);
|
||||
}
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@ use think\db\exception\DbException;
|
||||
use think\exception\ValidateException;
|
||||
use ywxapp\controller\BackendBase;
|
||||
use ywxapp\model\Notice as NoticeModel;
|
||||
use app\backend\validate\Notice as NoticeValidate;
|
||||
use app\backend\validate\CommonNotice as NoticeValidate;
|
||||
|
||||
/**
|
||||
* 站点公告管理
|
||||
|
||||
@@ -7,7 +7,7 @@ use think\facade\Db;
|
||||
use think\db\exception\DbException;
|
||||
use think\exception\ValidateException;
|
||||
use ywxapp\controller\BackendBase;
|
||||
use ywxapp\model\Prop as PropModel;
|
||||
use ywxapp\model\CommonProp as PropModel;
|
||||
use app\backend\validate\Prop as PropValidate;
|
||||
|
||||
/**
|
||||
|
||||
@@ -7,7 +7,7 @@ use think\facade\Db;
|
||||
use think\db\exception\DbException;
|
||||
use think\exception\ValidateException;
|
||||
use ywxapp\controller\BackendBase;
|
||||
use ywxapp\model\Score as ScoreModel;
|
||||
use ywxapp\model\MebmberScore as ScoreModel;
|
||||
use app\backend\validate\Score as ScoreValidate;
|
||||
|
||||
/**
|
||||
|
||||
@@ -7,7 +7,7 @@ use think\facade\Db;
|
||||
use think\db\exception\DbException;
|
||||
use think\exception\ValidateException;
|
||||
use ywxapp\controller\BackendBase;
|
||||
use ywxapp\model\Shop as ShopModel;
|
||||
use ywxapp\model\CommonShop as ShopModel;
|
||||
use app\backend\validate\Shop as ShopValidate;
|
||||
|
||||
/**
|
||||
|
||||
@@ -7,7 +7,7 @@ use think\facade\Db;
|
||||
use think\db\exception\DbException;
|
||||
use think\exception\ValidateException;
|
||||
use ywxapp\controller\BackendBase;
|
||||
use ywxapp\model\Sms as SmsModel;
|
||||
use ywxapp\model\CommonSms as SmsModel;
|
||||
use app\backend\validate\Sms as SmsValidate;
|
||||
|
||||
/**
|
||||
|
||||
@@ -40,7 +40,7 @@ class Spider extends BackendBase
|
||||
$url = $this->request->param('url', '');
|
||||
$date = $this->request->param('date', ''); // YYYY-MM-DD
|
||||
|
||||
$query = Db::name('spider_log');
|
||||
$query = Db::name('common_spider_log');
|
||||
if ($spider !== '') {
|
||||
$query->where('spider', $spider);
|
||||
}
|
||||
@@ -88,11 +88,11 @@ class Spider extends BackendBase
|
||||
$trend = ['dates' => [], 'series' => []];
|
||||
|
||||
try {
|
||||
$rows = Db::name('spider_stat')
|
||||
$rows = Db::name('common_spider_stat')
|
||||
->where('stat_date', '>=', $d30)
|
||||
->select()
|
||||
->toArray();
|
||||
$totalRows = Db::name('spider_stat')
|
||||
$totalRows = Db::name('common_spider_stat')
|
||||
->field('spider, SUM(`count`) AS total')
|
||||
->group('spider')
|
||||
->select()
|
||||
@@ -163,7 +163,7 @@ class Spider extends BackendBase
|
||||
$days = $this->request->param('days/d', 30);
|
||||
$days = max(1, min(365, $days));
|
||||
try {
|
||||
$count = Db::name('spider_log')
|
||||
$count = Db::name('common_spider_log')
|
||||
->where('create_at', '<', time() - $days * 86400)
|
||||
->delete();
|
||||
$this->result->success('', "已清理 {$count} 条 {$days} 天前的明细日志");
|
||||
|
||||
@@ -7,8 +7,8 @@ use think\facade\Db;
|
||||
use think\db\exception\DbException;
|
||||
use think\exception\ValidateException;
|
||||
use ywxapp\controller\BackendBase;
|
||||
use ywxapp\model\Task as TaskModel;
|
||||
use ywxapp\model\Prop as PropModel;
|
||||
use ywxapp\model\CommonTask as TaskModel;
|
||||
use ywxapp\model\CommonProp as PropModel;
|
||||
use app\backend\validate\Task as TaskValidate;
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,114 +0,0 @@
|
||||
<?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\backend\model;
|
||||
|
||||
use think\model\concern\SoftDelete;
|
||||
use ywxapp\model\BaseModel;
|
||||
|
||||
/**
|
||||
* 前台主导航菜单模型(支持两级下拉).
|
||||
*/
|
||||
class NavMenu extends BaseModel
|
||||
{
|
||||
use SoftDelete;
|
||||
|
||||
protected $deleteTime = 'delete_at';
|
||||
|
||||
protected function getOptions(): array
|
||||
{
|
||||
return [
|
||||
'strict' => true,
|
||||
'name' => 'common_nav_menu',
|
||||
'autoWriteTimestamp' => 'int',
|
||||
'createTime' => 'create_at',
|
||||
'updateTime' => 'update_at',
|
||||
'defaultSoftDelete' => 0,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* 运行时自愈:确保 common_nav_menu 主表存在(install.sql 为事实源).
|
||||
*/
|
||||
public static function ensureSchema(): void
|
||||
{
|
||||
BaseModel::ensureTableFromInstall(BaseModel::currentPrefix(), 'common_nav_menu');
|
||||
}
|
||||
|
||||
protected function initialize()
|
||||
{
|
||||
parent::initialize();
|
||||
self::ensureSchema();
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取启用的导航树(两级:父 + 子)。
|
||||
* @return array [{id,title,url,icon,child:[...]}]
|
||||
*/
|
||||
public static function getNavTree(): array
|
||||
{
|
||||
$list = self::where('status', 1)
|
||||
->where('delete_at', 0)
|
||||
->order('sort', 'asc')
|
||||
->order('id', 'asc')
|
||||
->field('id,parent_id,title,url,icon,sort')
|
||||
->select()
|
||||
->toArray();
|
||||
|
||||
$parents = [];
|
||||
$childrenMap = [];
|
||||
foreach ($list as $item) {
|
||||
$item['ctrl'] = strtolower(strtok($item['url'], '/') ?: '');
|
||||
if ((int) $item['parent_id'] === 0) {
|
||||
$item['child'] = [];
|
||||
$parents[] = $item;
|
||||
} else {
|
||||
$childrenMap[$item['parent_id']][] = $item;
|
||||
}
|
||||
}
|
||||
foreach ($parents as &$p) {
|
||||
if (isset($childrenMap[$p['id']])) {
|
||||
$p['child'] = $childrenMap[$p['id']];
|
||||
}
|
||||
}
|
||||
return $parents;
|
||||
}
|
||||
|
||||
/**
|
||||
* 后台列表:返回两级扁平树(含隐藏项).
|
||||
*/
|
||||
public static function getAdminTree(): array
|
||||
{
|
||||
$list = self::where('delete_at', 0)
|
||||
->order('sort', 'asc')
|
||||
->order('id', 'asc')
|
||||
->field('id,parent_id,title,url,icon,sort,status')
|
||||
->select()
|
||||
->toArray();
|
||||
|
||||
$parents = [];
|
||||
$childrenMap = [];
|
||||
foreach ($list as $item) {
|
||||
$item['ctrl'] = strtolower(strtok($item['url'], '/') ?: '');
|
||||
if ((int) $item['parent_id'] === 0) {
|
||||
$item['child'] = [];
|
||||
$parents[] = $item;
|
||||
} else {
|
||||
$childrenMap[$item['parent_id']][] = $item;
|
||||
}
|
||||
}
|
||||
foreach ($parents as &$p) {
|
||||
if (isset($childrenMap[$p['id']])) {
|
||||
$p['child'] = $childrenMap[$p['id']];
|
||||
}
|
||||
}
|
||||
return $parents;
|
||||
}
|
||||
}
|
||||
@@ -121,6 +121,11 @@
|
||||
$.post(window.location.href, data.field, function (r) {
|
||||
if (r.code === 0) {
|
||||
layer.msg(r.message || '保存成功', { icon: 1 });
|
||||
// 保存成功后自动跳回插件列表(兼容 layui admin 的 tab 场景),
|
||||
// 避免停留在配置 tab 需要手动关闭重开才能看到列表。
|
||||
setTimeout(function () {
|
||||
window.location.href = '{:url("addon/index")}';
|
||||
}, 800);
|
||||
} else {
|
||||
layer.msg(r.message || '保存失败', { icon: 2 });
|
||||
}
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
<button class="layui-btn layui-btn-normal" lay-submit lay-filter="data-search-btn">
|
||||
<i class="layui-icon layui-icon-search"></i>
|
||||
</button>
|
||||
<button class="layui-btn layui-btn-primary" id="btn-reset">
|
||||
<button type="button" class="layui-btn layui-btn-primary" id="btn-reset">
|
||||
<i class="layui-icon layui-icon-refresh"></i>
|
||||
</button>
|
||||
</div>
|
||||
@@ -27,15 +27,16 @@
|
||||
<!-- 顶部工具条 -->
|
||||
<script type="text/html" id="tableBar">
|
||||
<div class="layui-btn-group">
|
||||
<a class="layui-btn layui-btn-sm layui-btn-primary" title="新建菜单" lay-event="dataCreate" data-perm="nav:add"> <i class="layui-icon layui-icon-add-1"></i> </a>
|
||||
<a class="layui-btn layui-btn-sm layui-btn-primary" title="新建菜单" lay-event="dataCreate" data-perm="nav:add"> <i class="layui-icon layui-icon-add-1"></i> 新建 </a>
|
||||
<a class="layui-btn layui-btn-sm layui-btn-primary" title="批量删除" lay-event="dataDelete" data-perm="nav:delete"> <i class="layui-icon layui-icon-delete"></i> 批量删除 </a>
|
||||
</div>
|
||||
</script>
|
||||
|
||||
<!-- 操作列模板 -->
|
||||
<script type="text/html" id="dataBar">
|
||||
<a class="layui-btn layui-btn-xs" lay-event="update" data-perm="nav:edit">编辑</a>
|
||||
{{# if(d.parent_id == 0) { }}
|
||||
<a class="layui-btn layui-btn-xs" lay-event="addChild" data-perm="nav:add">加子项</a>
|
||||
{{# if(d.pid == 0) { }}
|
||||
<a class="layui-btn layui-btn-xs" lay-event="create" data-perm="nav:add">加子项</a>
|
||||
{{# } }}
|
||||
<a class="layui-btn layui-btn-danger layui-btn-xs" lay-event="delete" data-perm="nav:delete">删除</a>
|
||||
</script>
|
||||
@@ -45,15 +46,6 @@
|
||||
<input type="checkbox" name="status" value="{{d.id}}" lay-skin="switch" lay-text="显示|隐藏" lay-filter="statusSwitch" {{ d.status == 1 ? 'checked' : '' }}>
|
||||
</script>
|
||||
|
||||
<!-- 父级标识 -->
|
||||
<script type="text/html" id="parentTpl">
|
||||
{{# if(d.parent_id == 0) { }}
|
||||
<span class="layui-badge layui-bg-blue">一级</span>
|
||||
{{# } else { }}
|
||||
<span class="layui-badge-rim">二级</span>
|
||||
{{# } }}
|
||||
</script>
|
||||
|
||||
<!-- 添加/编辑表单(抽屉) -->
|
||||
<script type="text/html" id="dataFormTpl">
|
||||
<form class="layui-form" style="padding: 20px 20px 0;">
|
||||
@@ -82,6 +74,20 @@
|
||||
<div class="layui-form-mid layui-word-aux">留空表示仅作为含有子项的父菜单(下拉)。</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="layui-form-item">
|
||||
<label class="layui-form-label">所属应用</label>
|
||||
<div class="layui-input-block">
|
||||
<select name="app" lay-filter="appSel">
|
||||
<option value="">当前应用(frontend 前台)</option>
|
||||
{{# layui.each(d.plugins || [], function(idx, pname){ }}
|
||||
<option value="{{pname}}" {{ d.app==pname ? 'selected' : '' }}>{{pname}}(插件)</option>
|
||||
{{# }); }}
|
||||
<option value="member" {{ d.app=='member' ? 'selected' : '' }}>member(会员中心)</option>
|
||||
<option value="backend" {{ d.app=='backend' ? 'selected' : '' }}>backend(后台)</option>
|
||||
</select>
|
||||
<div class="layui-form-mid layui-word-aux">选插件则跨应用生成域名链接(如 haonav/index/index);留空=当前应用。</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="layui-form-item">
|
||||
<label class="layui-form-label">图标</label>
|
||||
<div class="layui-input-block">
|
||||
@@ -101,14 +107,10 @@
|
||||
<input type="radio" name="status" value="0" title="隐藏" {{ d.status==0 ? 'checked' : '' }}>
|
||||
</div>
|
||||
</div>
|
||||
<div class="layui-form-item layui-hide">
|
||||
<button class="layui-btn" lay-submit lay-filter="wxapp-form-submit" id="wxapp-form-submit">提交</button>
|
||||
</div>
|
||||
</form>
|
||||
</script>
|
||||
|
||||
<script type="text/javascript">
|
||||
console.log('nav index');
|
||||
window.navPlugins = {:json_encode($plugins)};
|
||||
layui.use('navbar', function () { });
|
||||
console.log(layui.nav);
|
||||
</script>
|
||||
</script>
|
||||
|
||||
@@ -132,319 +132,9 @@
|
||||
</div>
|
||||
</script>
|
||||
<script>
|
||||
layui.use(
|
||||
["table", "form", "layer", "jquery", "tree", "laytpl"],
|
||||
function () {
|
||||
var table = layui.table,
|
||||
form = layui.form,
|
||||
layer = layui.layer,
|
||||
$ = layui.jquery,
|
||||
tree = layui.tree,
|
||||
laytpl = layui.laytpl;
|
||||
|
||||
layui.use('group');
|
||||
|
||||
function api(url, data, type) {
|
||||
return $.ajax({
|
||||
url: url,
|
||||
type: type || "POST",
|
||||
data: data || {},
|
||||
dataType: "json",
|
||||
});
|
||||
}
|
||||
|
||||
function load() {
|
||||
api("index").then(
|
||||
function (res) {
|
||||
if (res.code !== 0) {
|
||||
layer.msg(res.message || "加载失败", { icon: 2 });
|
||||
return;
|
||||
}
|
||||
renderTable(res.data || []);
|
||||
},
|
||||
function () {
|
||||
layer.msg("请求失败", { icon: 2 });
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
function renderTable(list) {
|
||||
table.render({
|
||||
elem: "#dataTable",
|
||||
data: list,
|
||||
page: false,
|
||||
cols: [
|
||||
[
|
||||
{ type: "checkbox", width: 40 },
|
||||
{ field: "id", title: "ID", width: 60 },
|
||||
{ field: "name", title: "编码", width: 140 },
|
||||
{ field: "title", title: "角色名称", minWidth: 140 },
|
||||
{ field: "description", title: "描述", minWidth: 160 },
|
||||
{
|
||||
field: "status",
|
||||
title: "状态",
|
||||
width: 90,
|
||||
templet: "#statusTpl",
|
||||
},
|
||||
{
|
||||
title: "操作",
|
||||
width: 200,
|
||||
templet: "#dataBarTpl",
|
||||
fixed: "right",
|
||||
},
|
||||
],
|
||||
],
|
||||
});
|
||||
}
|
||||
|
||||
// 新建 / 编辑角色
|
||||
function openForm(row) {
|
||||
laytpl($("#dataFormTpl").html()).render(row || {}, function (html) {
|
||||
layer.open({
|
||||
type: 1,
|
||||
title: row ? "编辑角色" : "新建角色",
|
||||
area: ["480px", "auto"],
|
||||
content: html,
|
||||
btn: ["保存", "取消"],
|
||||
success: function () {
|
||||
form.render("radio");
|
||||
},
|
||||
yes: function (index, layero) {
|
||||
var params = $(layero).find("#wxapp-form").serialize();
|
||||
if (row) {
|
||||
params += "&_method=PUT";
|
||||
}
|
||||
api(row ? "update" : "save", params, "POST").then(
|
||||
function (res) {
|
||||
if (res.code !== 0) {
|
||||
layer.msg(res.message || "保存失败", { icon: 2 });
|
||||
return;
|
||||
}
|
||||
layer.msg(res.message || "已保存", { icon: 1 });
|
||||
layer.close(index);
|
||||
load();
|
||||
},
|
||||
function () {
|
||||
layer.msg("保存失败", { icon: 2 });
|
||||
},
|
||||
);
|
||||
},
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// 权限分配(树形勾选 + 保存),后端 Role::permission 提供树与持久化
|
||||
function openPermission(row) {
|
||||
laytpl($("#permissionFormTpl").html()).render(row, function (html) {
|
||||
layer.open({
|
||||
type: 1,
|
||||
title: "权限分配",
|
||||
area: ["360px", "80%"],
|
||||
content: html,
|
||||
btn: ["保存", "取消"],
|
||||
success: function (layero, index) {
|
||||
api("permission?id=" + row.id, null, "GET").then(
|
||||
function (res) {
|
||||
if (res.code !== 0) {
|
||||
layer.msg(res.message || "加载失败", { icon: 2 });
|
||||
return;
|
||||
}
|
||||
tree.render({
|
||||
elem: "#permissionTree",
|
||||
data: convertTree(res.data || []),
|
||||
showCheckbox: true,
|
||||
id: "permTree",
|
||||
});
|
||||
// 超级管理员恒为 *,禁用保存
|
||||
if (res.message && res.message.indexOf("超级管理员") >= 0) {
|
||||
$(layero)
|
||||
.find(".layui-layer-btn0")
|
||||
.addClass("layui-btn-disabled");
|
||||
layer.msg("超级管理员拥有全部权限(*),无需分配", {
|
||||
icon: 0,
|
||||
});
|
||||
}
|
||||
},
|
||||
function () {
|
||||
layer.msg("权限树加载失败", { icon: 2 });
|
||||
},
|
||||
);
|
||||
},
|
||||
yes: function (index) {
|
||||
var ids = collectIds(tree.getChecked("permTree"), []);
|
||||
api(
|
||||
"permission?id=" + row.id,
|
||||
{ permissions: ids, _method: "PUT" },
|
||||
"POST",
|
||||
).then(
|
||||
function (res) {
|
||||
if (res.code !== 0) {
|
||||
layer.msg(res.message || "保存失败", { icon: 2 });
|
||||
return;
|
||||
}
|
||||
layer.msg(res.message || "权限已保存", { icon: 1 });
|
||||
layer.close(index);
|
||||
},
|
||||
function () {
|
||||
layer.msg("保存失败", { icon: 2 });
|
||||
},
|
||||
);
|
||||
},
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// Role::permission 返回树节点用 name 展示,转换为 layui tree 的 title 字段
|
||||
function convertTree(nodes) {
|
||||
return (nodes || []).map(function (n) {
|
||||
return {
|
||||
title: n.name,
|
||||
id: n.id,
|
||||
checked: !!n.checked,
|
||||
spread: !!n.spread,
|
||||
children: convertTree(n.children),
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
// 递归收集勾选节点(含父子)的 id,前端去重
|
||||
function collectIds(nodes, acc) {
|
||||
(nodes || []).forEach(function (n) {
|
||||
if (n.id && acc.indexOf(n.id) < 0) {
|
||||
acc.push(n.id);
|
||||
}
|
||||
if (n.children) {
|
||||
collectIds(n.children, acc);
|
||||
}
|
||||
});
|
||||
return acc;
|
||||
}
|
||||
|
||||
// 回收站
|
||||
function openRecycle() {
|
||||
api("recyclebin").then(
|
||||
function (res) {
|
||||
if (res.code !== 0) {
|
||||
layer.msg(res.message || "加载失败", { icon: 2 });
|
||||
return;
|
||||
}
|
||||
var rows = (res.data || [])
|
||||
.map(function (r) {
|
||||
return (
|
||||
"<tr><td>" +
|
||||
r.id +
|
||||
"</td><td>" +
|
||||
(r.title || "") +
|
||||
"</td>" +
|
||||
'<td><button type="button" class="layui-btn layui-btn-xs" onclick="restoreRole(' +
|
||||
r.id +
|
||||
')">恢复</button></td></tr>'
|
||||
);
|
||||
})
|
||||
.join("");
|
||||
if (!rows) {
|
||||
rows =
|
||||
'<tr><td colspan="3" style="color:#999;text-align:center;">回收站为空</td></tr>';
|
||||
}
|
||||
var html =
|
||||
'<div style="padding:15px;"><table class="layui-table"><thead><tr>' +
|
||||
"<th>ID</th><th>名称</th><th>操作</th></tr></thead><tbody>" +
|
||||
rows +
|
||||
"</tbody></table></div>";
|
||||
layer.open({
|
||||
type: 1,
|
||||
title: "角色回收站",
|
||||
area: ["420px", "70%"],
|
||||
content: html,
|
||||
});
|
||||
},
|
||||
function () {
|
||||
layer.msg("加载失败", { icon: 2 });
|
||||
},
|
||||
);
|
||||
}
|
||||
window.restoreRole = function (id) {
|
||||
api("restore", { ids: id, _method: "PUT" }, "POST").then(
|
||||
function (res) {
|
||||
if (res.code !== 0) {
|
||||
layer.msg(res.message || "恢复失败", { icon: 2 });
|
||||
return;
|
||||
}
|
||||
layer.msg(res.message || "已恢复", { icon: 1 });
|
||||
load();
|
||||
},
|
||||
function () {
|
||||
layer.msg("恢复失败", { icon: 2 });
|
||||
},
|
||||
);
|
||||
};
|
||||
|
||||
// 行内操作
|
||||
table.on("tool(dataTable)", function (obj) {
|
||||
var row = obj.data;
|
||||
if (obj.event === "update") {
|
||||
openForm(row);
|
||||
} else if (obj.event === "permission") {
|
||||
openPermission(row);
|
||||
} else if (obj.event === "delete") {
|
||||
layer.confirm(
|
||||
"确认删除角色「" + (row.title || row.name) + "」?",
|
||||
function () {
|
||||
api("delete", { ids: row.id, _method: "DELETE" }, "POST").then(
|
||||
function (res) {
|
||||
if (res.code !== 0) {
|
||||
layer.msg(res.message || "删除失败", { icon: 2 });
|
||||
return;
|
||||
}
|
||||
layer.msg(res.message || "已删除", { icon: 1 });
|
||||
load();
|
||||
},
|
||||
function () {
|
||||
layer.msg("删除失败", { icon: 2 });
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
// 顶部工具条
|
||||
table.on("toolbar(dataTable)", function (obj) {
|
||||
if (obj.event === "dataCreate") {
|
||||
openForm(null);
|
||||
} else if (obj.event === "dataDelete") {
|
||||
var check = table.checkStatus("dataTable").data;
|
||||
if (!check.length) {
|
||||
layer.msg("请先选择角色", { icon: 2 });
|
||||
return;
|
||||
}
|
||||
var ids = check
|
||||
.map(function (r) {
|
||||
return r.id;
|
||||
})
|
||||
.join(",");
|
||||
layer.confirm(
|
||||
"确认删除选中的 " + check.length + " 个角色?",
|
||||
function () {
|
||||
api("delete", { ids: ids, _method: "DELETE" }, "POST").then(
|
||||
function (res) {
|
||||
if (res.code !== 0) {
|
||||
layer.msg(res.message || "删除失败", { icon: 2 });
|
||||
return;
|
||||
}
|
||||
layer.msg(res.message || "已删除", { icon: 1 });
|
||||
load();
|
||||
},
|
||||
function () {
|
||||
layer.msg("删除失败", { icon: 2 });
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
} else if (obj.event === "dataRecybin") {
|
||||
openRecycle();
|
||||
}
|
||||
});
|
||||
|
||||
load();
|
||||
},
|
||||
);
|
||||
</script>
|
||||
|
||||
@@ -4,7 +4,7 @@ namespace app\frontend\controller;
|
||||
|
||||
use think\facade\Db;
|
||||
use ywxapp\controller\FrontendBase;
|
||||
use ywxapp\model\Help as HelpModel;
|
||||
use ywxapp\model\CommonHelp as HelpModel;
|
||||
|
||||
/**
|
||||
* 站点帮助中心(前台)
|
||||
@@ -53,7 +53,7 @@ class Help extends FrontendBase
|
||||
$this->error('帮助内容不存在或已下架');
|
||||
}
|
||||
// 浏览量自增
|
||||
Db::name('help')->where('id', $id)->inc('view_count')->update();
|
||||
Db::name('common_help')->where('id', $id)->inc('view_count')->update();
|
||||
|
||||
$this->assign('info', $info);
|
||||
return $this->fetch('read');
|
||||
|
||||
@@ -13,7 +13,7 @@ namespace app\frontend\controller;
|
||||
use think\facade\Db;
|
||||
use think\facade\View;
|
||||
use ywxapp\controller\FrontendBase;
|
||||
use ywxapp\model\Notice as NoticeModel;
|
||||
use ywxapp\model\CommonNotice as NoticeModel;
|
||||
|
||||
/**
|
||||
* 前台站点公告(方案 B:列表 + 详情)
|
||||
@@ -62,7 +62,7 @@ class Notice extends FrontendBase
|
||||
$this->redirect('notice/index');
|
||||
}
|
||||
// 浏览量自增(模型 view_count 为 readonly,用 Db 原生 inc 绕过)
|
||||
Db::name('notice')->where('id', $id)->inc('view_count')->update();
|
||||
Db::name('common_notice')->where('id', $id)->inc('view_count')->update();
|
||||
$info->view_count = ($info->view_count ?? 0) + 1;
|
||||
$typeList = NoticeModel::typeList();
|
||||
$info->type_text = $typeList[$info['type']] ?? '公告';
|
||||
|
||||
@@ -36,7 +36,7 @@
|
||||
<div class="layui-container header-container">
|
||||
<div class="header-inner">
|
||||
<div class="logo-box">
|
||||
<a href="{:url('index/index')}" class=" ">
|
||||
<a href="{:url('index/index', [], true, true)}" class=" ">
|
||||
<span class="logo-text">{$siteConf.sitename|default='YwxApp'}</span>
|
||||
</a>
|
||||
</div>
|
||||
@@ -46,14 +46,14 @@
|
||||
<ul class="nav-menu" id="navMenu">
|
||||
{volist name="navMenus" id="m"}
|
||||
<li class="nav-item {if $m.ctrl == $site.controller}active{/if}{if !empty($m.child)} has-child{/if}">
|
||||
<a href="{if strpos($m.url,'http')===0}{$m.url}{else/}{:url($m.url)}{/if}">
|
||||
<a href="{if strpos($m.url,'http')===0}{$m.url}{elseif !empty($m.app)}{:url($m.app.'/'.$m.url, [], true, true)}{else/}{:url($m.url, [], true, true)}{/if}">
|
||||
{if !empty($m.icon)}<i class="layui-icon {$m.icon}"></i> {/if}{$m.title}
|
||||
</a>
|
||||
{if !empty($m.child)}
|
||||
<ul class="sub-menu">
|
||||
{volist name="m.child" id="c"}
|
||||
<li class="sub-item {if $c.ctrl == $site.controller}active{/if}">
|
||||
<a href="{if strpos($c.url,'http')===0}{$c.url}{else/}{:url($c.url)}{/if}">
|
||||
<a href="{if strpos($c.url,'http')===0}{$c.url}{elseif !empty($c.app)}{:url($c.app.'/'.$c.url, [], true, true)}{else/}{:url($c.url, [], true, true)}{/if}">
|
||||
{if !empty($c.icon)}<i class="layui-icon {$c.icon}"></i> {/if}{$c.title}
|
||||
</a>
|
||||
</li>
|
||||
|
||||
@@ -10,7 +10,7 @@ declare(strict_types=1);
|
||||
|
||||
namespace app\member\controller;
|
||||
|
||||
use ywxapp\model\Medal as MedalModel;
|
||||
use ywxapp\model\CommonMedal as MedalModel;
|
||||
use ywxapp\controller\MemberBase;
|
||||
|
||||
/**
|
||||
|
||||
@@ -13,7 +13,7 @@ namespace app\member\controller;
|
||||
use think\Request;
|
||||
use ywxapp\service\FileStorageService;
|
||||
use ywxapp\model\MemberProfile;
|
||||
use ywxapp\model\Medal;
|
||||
use ywxapp\model\CommonMedal;
|
||||
use app\member\validate\Profile as ProfileValidate;
|
||||
|
||||
use ywxapp\controller\MemberBase;
|
||||
@@ -52,7 +52,7 @@ class Profile extends MemberBase
|
||||
$user = $this->auth->model;
|
||||
$this->view->assign('info', $user);
|
||||
$this->view->assign('profile', $user->profile ?? new MemberProfile());
|
||||
$this->view->assign('medals', Medal::getUserMedals((int) $user->uid));
|
||||
$this->view->assign('medals', CommonMedal::getUserMedals((int) $user->uid));
|
||||
|
||||
return $this->view->fetch();
|
||||
}
|
||||
|
||||
@@ -17,7 +17,7 @@ use think\facade\Event;
|
||||
use think\Request;
|
||||
use ywxapp\controller\MemberBase;
|
||||
use ywxapp\library\Sms;
|
||||
use ywxapp\model\Sms as SmsModel;
|
||||
use ywxapp\model\CommonSms as SmsModel;
|
||||
use ywxapp\model\MemberUser as UserModel;
|
||||
use ywxapp\model\MemberProfile;
|
||||
use ywxapp\model\MemberGroupAccess;
|
||||
|
||||
@@ -95,13 +95,13 @@ class Task extends MemberBase
|
||||
public function myPropList()
|
||||
{
|
||||
$uid = (int) ($this->auth->model->uid ?? 0);
|
||||
\ywxapp\model\Task::ensureSchema(); // 确保 user_prop 关联表已就绪
|
||||
\ywxapp\model\CommonTask::ensureSchema(); // 确保 user_prop 关联表已就绪
|
||||
if ($uid <= 0) {
|
||||
$this->result->error('请先登录');
|
||||
}
|
||||
$list = Db::name('member_prop')
|
||||
->alias('up')
|
||||
->join('prop p', 'p.id = up.prop_id', 'left')
|
||||
->join('common_prop p', 'p.id = up.prop_id', 'left')
|
||||
->where('up.uid', $uid)
|
||||
->field('up.id, up.prop_id, up.num, up.create_at, p.title, p.icon')
|
||||
->order('up.create_at', 'desc')
|
||||
|
||||
@@ -51,7 +51,7 @@ class UserMemberActivate
|
||||
$level = (int)($plan['level'] ?? 0);
|
||||
$gid = (int)($plan['gid'] ?? 0);
|
||||
|
||||
$user = Db::name('member')->where('uid', $uid)->find();
|
||||
$user = Db::name('member_user')->where('uid', $uid)->find();
|
||||
if (empty($user)) {
|
||||
Log::warning('[UserMemberActivate] 用户不存在 uid=' . $uid);
|
||||
return;
|
||||
|
||||
Reference in New Issue
Block a user