chore: 重写初始提交(清空历史,整理后全量提交)
This commit is contained in:
@@ -0,0 +1,20 @@
|
||||
<?php
|
||||
// +----------------------------------------------------------------------
|
||||
// | YwxApp [ WE CAN DO IT JUST THINK ]
|
||||
// +----------------------------------------------------------------------
|
||||
// | Copyright (c) 2026-2036 http://ywxapp.cn All rights reserved.
|
||||
// +----------------------------------------------------------------------
|
||||
// | Author: ywxapp<admin@ywxapp.cn>
|
||||
// +----------------------------------------------------------------------
|
||||
namespace app;
|
||||
|
||||
// 应用请求对象类
|
||||
/**
|
||||
* Request 类
|
||||
*
|
||||
* @author ywxapp <admin@ywxapp.cn>
|
||||
*/
|
||||
class Request extends \think\Request
|
||||
{
|
||||
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
<?php
|
||||
// +----------------------------------------------------------------------
|
||||
// | YwxApp [ WE CAN DO IT JUST THINK ]
|
||||
// +----------------------------------------------------------------------
|
||||
// | Copyright (c) 2026-2036 http://ywxapp.cn All rights reserved.
|
||||
// +----------------------------------------------------------------------
|
||||
// | Author: ywxapp<admin@ywxapp.cn>
|
||||
// +----------------------------------------------------------------------
|
||||
// 这是系统自动生成的公共文件
|
||||
@@ -0,0 +1,30 @@
|
||||
<?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;
|
||||
|
||||
/**
|
||||
* Index 类
|
||||
*
|
||||
* @author ywxapp <admin@ywxapp.cn>
|
||||
*/
|
||||
class Index
|
||||
{
|
||||
|
||||
|
||||
public function index(\think\Request $request)
|
||||
{
|
||||
return json([
|
||||
'name' => 'YwxApp API',
|
||||
'version' => \think\facade\App::version(),
|
||||
'ip' => $request->ip(),
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,430 @@
|
||||
<?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 ywxapp\controller\ApiController;
|
||||
use ywxapp\service\RemoteService;
|
||||
use think\Request;
|
||||
|
||||
/**
|
||||
* 插件商店接口(瘦分发器,不持有市场领域逻辑):
|
||||
*
|
||||
* - 客户机模式(is_market_client()=true,ywxapp.api_url 指向中心站):
|
||||
* 经 RemoteService 代理中心站 /appmall/api/addon/*。
|
||||
* - 中心站模式(api_url 为空/指向本机):
|
||||
* 进程内委托 appmall 插件的 MarketService(订单/授权/收益/支付回调全部在插件侧)。
|
||||
* - 未安装 appmall 插件且非客户机:市场功能不可用,返回明确提示。
|
||||
*
|
||||
* 中心站领域数据(appmarket_* 表)已全部收敛到 addon/appmall,核心不再直接读写。
|
||||
*/
|
||||
class Addon extends ApiController
|
||||
{
|
||||
// notify/payResult 由支付平台回调/跳转,无需登录;info 公开
|
||||
protected $noNeedLogin = ['info', 'notify', 'payResult'];
|
||||
protected $needRight = ['*'];
|
||||
|
||||
/**
|
||||
* 中心站模式下委托的插件市场服务
|
||||
* @var \addon\appmall\service\MarketService|null
|
||||
*/
|
||||
protected $market = null;
|
||||
|
||||
public function initialize()
|
||||
{
|
||||
if (!is_market_client() && class_exists(\addon\appmall\service\MarketService::class)) {
|
||||
$this->market = \addon\appmall\service\MarketService::instance();
|
||||
$this->market->ensureTables();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 本机为中心站但未安装市场插件时的统一出口
|
||||
*/
|
||||
protected function marketUnavailable(): \think\response\Json
|
||||
{
|
||||
return $this->result->error('市场服务不可用:本机未配置中心站地址(ywxapp.api_url),也未安装 appmall 插件', 503);
|
||||
}
|
||||
|
||||
/**
|
||||
* 将插件领域异常转换为 Result 错误响应
|
||||
*/
|
||||
protected function marketError(\Throwable $e): \think\response\Json
|
||||
{
|
||||
$code = (int) $e->getCode();
|
||||
$code = $code >= 400 && $code < 600 ? $code : 500;
|
||||
if ($code === 404) {
|
||||
return $this->result->setStatusCode(404)->error($e->getMessage(), 404);
|
||||
}
|
||||
return $this->result->error($e->getMessage(), $code);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取插件信息(支持列表和详情)
|
||||
*/
|
||||
public function info(Request $request): \think\response\Json
|
||||
{
|
||||
if (is_market_client()) {
|
||||
return $this->remoteInfo($request);
|
||||
}
|
||||
if (!$this->market) {
|
||||
return $this->marketUnavailable();
|
||||
}
|
||||
$id = (int) $request->get('id', 0);
|
||||
if ($id > 0) {
|
||||
try {
|
||||
$data = $this->market->info($id, $this->getLoginUid($request) ?? 0);
|
||||
} catch (\Throwable $e) {
|
||||
return $this->marketError($e);
|
||||
}
|
||||
$data['installed'] = is_dir(ADDON_PATH . ($data['addon']['name'] ?? '') . DIRECTORY_SEPARATOR);
|
||||
return $this->result->success($data, '获取成功');
|
||||
}
|
||||
$r = $this->market->lists(
|
||||
max((int) $request->get('page', 1), 1),
|
||||
(int) $request->get('per_page', 15)
|
||||
);
|
||||
$this->result->setCount($r['total']);
|
||||
return $this->result->success(['data' => $r['items']], '获取成功');
|
||||
}
|
||||
|
||||
/**
|
||||
* 轻量解析请求中的 JWT,返回用户ID(不触发完整登录副作用)
|
||||
*/
|
||||
protected function getLoginUid(Request $request): ?int
|
||||
{
|
||||
$auth = $request->header('authorization');
|
||||
if (!$auth || !str_starts_with($auth, 'Bearer ')) {
|
||||
return null;
|
||||
}
|
||||
try {
|
||||
$token = \ywxapp\service\JwtService::instance()->parseAndValidate(substr($auth, 7));
|
||||
$uid = $token->claims()->get('uid');
|
||||
return $uid ? (int) $uid : null;
|
||||
} catch (\Throwable) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 发起购买:创建/复用待支付订单并拉起支付(或模拟支付直接签发授权)
|
||||
*/
|
||||
public function buy(Request $request)
|
||||
{
|
||||
if (is_market_client()) {
|
||||
return $this->remoteBuy($request);
|
||||
}
|
||||
if (!$this->market) {
|
||||
return $this->marketUnavailable();
|
||||
}
|
||||
$userId = $this->auth->model->uid ?? null;
|
||||
if (empty($userId)) {
|
||||
return $this->result->setStatusCode(401)->error('请先登录后再购买');
|
||||
}
|
||||
try {
|
||||
$r = $this->market->buy(
|
||||
(int) $userId,
|
||||
(int) $request->post('addon_id'),
|
||||
(string) $request->post('method', 'alipay'),
|
||||
(string) $request->root(true)
|
||||
);
|
||||
} catch (\Throwable $e) {
|
||||
return $this->marketError($e);
|
||||
}
|
||||
return $this->result->success($r['data'], $r['message']);
|
||||
}
|
||||
|
||||
/**
|
||||
* 支付结果查询(前端轮询用)
|
||||
*/
|
||||
public function orderStatus(Request $request): \think\response\Json
|
||||
{
|
||||
if (is_market_client()) {
|
||||
return $this->remoteOrderStatus($request);
|
||||
}
|
||||
if (!$this->market) {
|
||||
return $this->marketUnavailable();
|
||||
}
|
||||
$userId = $this->auth->model->uid ?? null;
|
||||
if (empty($userId)) {
|
||||
return $this->result->setStatusCode(401)->error('请先登录');
|
||||
}
|
||||
try {
|
||||
$data = $this->market->orderStatus((int) $userId, (string) $request->get('trade_no'));
|
||||
} catch (\Throwable $e) {
|
||||
return $this->marketError($e);
|
||||
}
|
||||
return $this->result->success($data, '获取成功');
|
||||
}
|
||||
|
||||
/**
|
||||
* 支付完成落地页(支付宝 return_url 跳转,无需登录,仅展示状态)
|
||||
*/
|
||||
public function payResult(Request $request): \think\response\Json
|
||||
{
|
||||
if (is_market_client()) {
|
||||
$r = RemoteService::instance()->payResult($request->get());
|
||||
return $this->result->success($r['data'] ?? ['status' => 0], $r['message'] ?: '获取成功');
|
||||
}
|
||||
if (!$this->market) {
|
||||
return $this->marketUnavailable();
|
||||
}
|
||||
$r = $this->market->payResult((string) $request->get('out_trade_no', ''));
|
||||
return $this->result->success($r['data'], $r['message']);
|
||||
}
|
||||
|
||||
/**
|
||||
* 退款 / 吊销授权(买家或运营)
|
||||
* POST /api/v1/addon/refund {trade_no 或 order_id}
|
||||
*/
|
||||
public function refund(Request $request): \think\response\Json
|
||||
{
|
||||
if (is_market_client()) {
|
||||
return $this->remoteRefund($request);
|
||||
}
|
||||
if (!$this->market) {
|
||||
return $this->marketUnavailable();
|
||||
}
|
||||
$userId = $this->auth->model->uid ?? null;
|
||||
if (empty($userId)) {
|
||||
return $this->result->setStatusCode(401)->error('请先登录');
|
||||
}
|
||||
try {
|
||||
$r = $this->market->refund(
|
||||
(int) $userId,
|
||||
(string) $request->post('trade_no', ''),
|
||||
(int) $request->post('order_id', 0)
|
||||
);
|
||||
} catch (\Throwable $e) {
|
||||
return $this->marketError($e);
|
||||
}
|
||||
return $this->result->success($r['data'], $r['message']);
|
||||
}
|
||||
|
||||
/**
|
||||
* 我的已购插件(买家视角):列出已购付费插件 + 授权状态 + 是否可退款
|
||||
*/
|
||||
public function mine(Request $request): \think\response\Json
|
||||
{
|
||||
if (is_market_client()) {
|
||||
return $this->remoteMine($request);
|
||||
}
|
||||
if (!$this->market) {
|
||||
return $this->marketUnavailable();
|
||||
}
|
||||
$userId = $this->auth->model->uid ?? null;
|
||||
if (empty($userId)) {
|
||||
return $this->result->setStatusCode(401)->error('请先登录');
|
||||
}
|
||||
return $this->result->success($this->market->mine((int) $userId), '获取成功');
|
||||
}
|
||||
|
||||
/**
|
||||
* 支付宝/微信异步回调:验签 -> 校验金额 -> 订单置已付 -> 生成授权
|
||||
*/
|
||||
public function notify()
|
||||
{
|
||||
if (is_market_client()) {
|
||||
return response(RemoteService::instance()->notify(input()));
|
||||
}
|
||||
if (!$this->market) {
|
||||
return response('fail');
|
||||
}
|
||||
return $this->market->notify((string) input('method', 'alipay'));
|
||||
}
|
||||
|
||||
/**
|
||||
* 用户下载已购插件(需登录 + 有效授权 + 次数限制)
|
||||
*/
|
||||
public function download(Request $request)
|
||||
{
|
||||
if (is_market_client()) {
|
||||
return $this->remoteDownload($request);
|
||||
}
|
||||
if (!$this->market) {
|
||||
return $this->marketUnavailable();
|
||||
}
|
||||
$userId = $this->auth->model->uid ?? null;
|
||||
if (empty($userId)) {
|
||||
return $this->result->setStatusCode(401)->error('请先登录');
|
||||
}
|
||||
try {
|
||||
$r = $this->market->download((int) $userId, (int) $request->get('addon_id'), $request);
|
||||
} catch (\Throwable $e) {
|
||||
return $this->marketError($e);
|
||||
}
|
||||
return download($r['file'], $r['filename']);
|
||||
}
|
||||
|
||||
// ============ 客户机模式(ywxapp.api_url 指向其他服务器,见 is_market_client())下的代理实现 ============
|
||||
|
||||
/**
|
||||
* 远程:插件详情 / 列表(代理中心站)
|
||||
*/
|
||||
protected function remoteInfo(Request $request): \think\response\Json
|
||||
{
|
||||
$id = (int) $request->get('id', 0);
|
||||
$uid = $this->getLoginUid($request) ?? 0;
|
||||
if ($id > 0) {
|
||||
$r = RemoteService::instance()->info($id, $uid);
|
||||
if (!$r['success']) {
|
||||
return $this->result->setStatusCode(404)->error($r['message'] ?: '获取失败', 404);
|
||||
}
|
||||
$addon = $r['data']['addon'] ?? [];
|
||||
$installed = is_dir(ADDON_PATH . ($addon['name'] ?? '') . DIRECTORY_SEPARATOR);
|
||||
return $this->result->success([
|
||||
'addon' => $addon,
|
||||
'purchased' => $r['data']['purchased'] ?? false,
|
||||
'installed' => $installed,
|
||||
], '获取成功');
|
||||
}
|
||||
// 透传分页/筛选参数给中心站,由服务端完成分页(避免全量拉回后本地切片)
|
||||
$params = [
|
||||
'keyword' => $request->get('keyword', ''),
|
||||
'category' => $request->get('category', ''),
|
||||
'order' => $request->get('order', ''),
|
||||
'page' => max((int) $request->get('page', 1), 1),
|
||||
'page_size' => min(max((int) $request->get('per_page', 15), 1), 50),
|
||||
];
|
||||
$r = RemoteService::instance()->lists($params);
|
||||
if (!$r['success']) {
|
||||
return $this->result->error($r['message'] ?: '获取失败');
|
||||
}
|
||||
// 优先消费中心站服务端分页(Market::lists 在 page>0 时返回 data.list/data.total);
|
||||
// 兼容历史 data.count 键;老中心站未分页时降级为本地切片。
|
||||
if (isset($r['data']['list']) && (isset($r['data']['total']) || isset($r['data']['count']))) {
|
||||
$items = $r['data']['list'];
|
||||
$total = (int) ($r['data']['total'] ?? $r['data']['count']);
|
||||
} else {
|
||||
$rows = $r['data']['list'] ?? [];
|
||||
$perPage = min(max((int) $request->get('per_page', 15), 1), 50);
|
||||
$page = max((int) $request->get('page', 1), 1);
|
||||
$total = count($rows);
|
||||
$items = array_slice($rows, ($page - 1) * $perPage, $perPage);
|
||||
}
|
||||
$this->result->setCount($total);
|
||||
return $this->result->success(['data' => $items], '获取成功');
|
||||
}
|
||||
|
||||
/**
|
||||
* 远程:发起购买(代理中心站)
|
||||
*/
|
||||
protected function remoteBuy(Request $request): \think\response\Json
|
||||
{
|
||||
$userId = $this->auth->model->uid ?? null;
|
||||
if (empty($userId)) {
|
||||
return $this->result->setStatusCode(401)->error('请先登录后再购买');
|
||||
}
|
||||
$addonId = (int) $request->post('addon_id');
|
||||
$method = $request->post('method', 'alipay');
|
||||
$r = RemoteService::instance()->buy($addonId, $userId, $method);
|
||||
if (!$r['success']) {
|
||||
return $this->result->error($r['message'] ?: '购买失败');
|
||||
}
|
||||
return $this->result->success($r['data'] ?? [], $r['message'] ?: '操作成功');
|
||||
}
|
||||
|
||||
/**
|
||||
* 远程:订单状态(代理中心站)
|
||||
*/
|
||||
protected function remoteOrderStatus(Request $request): \think\response\Json
|
||||
{
|
||||
$tradeNo = $request->get('trade_no');
|
||||
$userId = $this->auth->model->uid ?? null;
|
||||
if (empty($userId)) {
|
||||
return $this->result->setStatusCode(401)->error('请先登录');
|
||||
}
|
||||
$r = RemoteService::instance()->orderStatus($tradeNo, $userId);
|
||||
if (!$r['success']) {
|
||||
return $this->result->error($r['message'] ?: '查询失败');
|
||||
}
|
||||
return $this->result->success($r['data'] ?? [], '获取成功');
|
||||
}
|
||||
|
||||
/**
|
||||
* 远程:下载已购插件(代理中心站 /appmall/api/index,付费插件需已购买)
|
||||
*/
|
||||
protected function remoteDownload(Request $request)
|
||||
{
|
||||
$userId = $this->auth->model->uid ?? null;
|
||||
if (empty($userId)) {
|
||||
return $this->result->setStatusCode(401)->error('请先登录');
|
||||
}
|
||||
$addonId = (int) $request->get('addon_id');
|
||||
$r = RemoteService::instance()->info($addonId, $userId);
|
||||
if (!$r['success']) {
|
||||
return $this->result->error($r['message'] ?: '插件信息获取失败');
|
||||
}
|
||||
$addon = $r['data']['addon'] ?? [];
|
||||
$name = $addon['name'] ?? '';
|
||||
$version = $addon['version'] ?? '';
|
||||
if ($name === '' || $version === '') {
|
||||
return $this->result->error('插件信息不完整', 404);
|
||||
}
|
||||
if (!empty($addon['price']) && (float) $addon['price'] > 0 && empty($r['data']['purchased'])) {
|
||||
return $this->result->error('您尚未购买该插件', 403);
|
||||
}
|
||||
try {
|
||||
$domain = (string) $request->domain();
|
||||
$srcFile = RemoteService::instance()->downloadBinary($name, $version, $userId, $domain);
|
||||
} catch (\Exception $e) {
|
||||
return $this->result->error($e->getMessage(), 404);
|
||||
}
|
||||
$tmpDir = root_path() . 'runtime' . DIRECTORY_SEPARATOR . 'addon_download' . DIRECTORY_SEPARATOR;
|
||||
if (!is_dir($tmpDir)) {
|
||||
@mkdir($tmpDir, 0755, true);
|
||||
}
|
||||
$tmpFile = $tmpDir . $name . '-' . $version . '.zip';
|
||||
if (is_file($srcFile)) {
|
||||
copy($srcFile, $tmpFile);
|
||||
@unlink($srcFile);
|
||||
} else {
|
||||
return $this->result->error('下载失败:未获取到文件', 404);
|
||||
}
|
||||
return new \ywxapp\library\StreamZipResponse($tmpFile, $name . '-' . $version . '.zip');
|
||||
}
|
||||
|
||||
/**
|
||||
* 远程:退款 / 吊销授权(代理中心站)
|
||||
*/
|
||||
protected function remoteRefund(Request $request): \think\response\Json
|
||||
{
|
||||
$userId = $this->auth->model->uid ?? null;
|
||||
if (empty($userId)) {
|
||||
return $this->result->setStatusCode(401)->error('请先登录');
|
||||
}
|
||||
$r = RemoteService::instance()->refund(
|
||||
(string) $request->post('trade_no', ''),
|
||||
(int) $request->post('order_id', 0),
|
||||
$userId
|
||||
);
|
||||
if (!$r['success']) {
|
||||
return $this->result->error($r['message'] ?: '退款失败');
|
||||
}
|
||||
return $this->result->success($r['data'] ?? [], $r['message'] ?: '退款成功');
|
||||
}
|
||||
|
||||
/**
|
||||
* 远程:我的已购插件(代理中心站)
|
||||
*/
|
||||
protected function remoteMine(Request $request): \think\response\Json
|
||||
{
|
||||
$userId = $this->auth->model->uid ?? null;
|
||||
if (empty($userId)) {
|
||||
return $this->result->setStatusCode(401)->error('请先登录');
|
||||
}
|
||||
$r = RemoteService::instance()->my($userId);
|
||||
if (!$r['success']) {
|
||||
return $this->result->error($r['message'] ?: '获取失败');
|
||||
}
|
||||
return $this->result->success($r['data'] ?? ['list' => []], $r['message'] ?: '获取成功');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,120 @@
|
||||
<?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\Validate;
|
||||
use ywxapp\controller\ApiController;
|
||||
use addon\articles\model\Article as ArticleModel;
|
||||
|
||||
class Article extends ApiController
|
||||
{
|
||||
protected $noNeedLogin = ['*'];
|
||||
protected $needRight = ['*'];
|
||||
|
||||
|
||||
public function initialize()
|
||||
{
|
||||
$this->model = new ArticleModel();
|
||||
}
|
||||
|
||||
/**
|
||||
* 文章列表
|
||||
*/
|
||||
public function index()
|
||||
{
|
||||
$page = $this->request->param('page/d', 1);
|
||||
$limit = $this->request->param('limit/d', 15);
|
||||
$list = $this->model->with(['category'])->page($page, $limit)->order('id', 'desc')->select();
|
||||
$this->result->success($list);
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建文章
|
||||
*/
|
||||
public function create(\think\Request $request)
|
||||
{
|
||||
$data = $request->post();
|
||||
if (empty($data['title'])) {
|
||||
$this->result->error('标题不能为空', 404);
|
||||
}
|
||||
$res = $this->model->create($data);
|
||||
if ($res) {
|
||||
event('article_create_after', $res);
|
||||
$this->result->success($res, '创建成功');
|
||||
}
|
||||
$this->result->error('创建失败');
|
||||
}
|
||||
|
||||
/**
|
||||
* 文章详情
|
||||
* @route GET /detail, method:GET
|
||||
* @param int $uid 文章ID
|
||||
*/
|
||||
public function detail(\think\Request $request, int $uid= 0 ) {
|
||||
if (!$uid)
|
||||
$this->result->error('Invalid parameters', 404);
|
||||
$info = $this->model->with(['category', 'tags'])->find($uid);
|
||||
if (! $info) {
|
||||
$this->result->error('文章不存在', 404);
|
||||
}
|
||||
$this->result->success($info);
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新文章
|
||||
* @route PUT /update, method:PUT
|
||||
* @param int $uid 文章ID
|
||||
*/
|
||||
public function update(\think\Request $request, int $uid= 0 ) {
|
||||
if (!$uid)
|
||||
$this->result->error('Invalid parameters', 404);
|
||||
$info = $this->model->find($uid);
|
||||
if (! $info) {
|
||||
$this->result->error('文章不存在', 404);
|
||||
}
|
||||
$data = $request->only(
|
||||
['title', 'cover_image', 'summary', 'content', 'author', 'cid', 'status'],
|
||||
'put'
|
||||
);
|
||||
$info->save($data);
|
||||
event('article_update_after', $info);
|
||||
$this->result->success($info, '更新成功');
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除文章(软删除)
|
||||
* @route DELETE /update, method:DELETE
|
||||
* @param int $uid 文章ID
|
||||
*/
|
||||
public function delete(\think\Request $request, int $uid= 0 ) {
|
||||
if (!$uid)
|
||||
$this->result->error('Invalid parameters', 404);
|
||||
$info = $this->model->find($uid);
|
||||
if (! $info) {
|
||||
$this->result->error('文章不存在', 404);
|
||||
}
|
||||
$info->delete();
|
||||
event('article_delete_after', $info);
|
||||
$this->result->success('', '删除成功');
|
||||
}
|
||||
|
||||
|
||||
private function getEncryptPassword($password, $salt = '')
|
||||
{
|
||||
return md5(md5($password) . $salt);
|
||||
}
|
||||
|
||||
|
||||
public function init()
|
||||
{
|
||||
return json(['message' => 'This is version 1 of the API']);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,93 @@
|
||||
<?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 ywxapp\controller\ApiController;
|
||||
use think\facade\Event;
|
||||
use ywxapp\model\MemberUser as UserModel;
|
||||
use ywxapp\library\Result;
|
||||
use ywxapp\library\Email as EmailLib;
|
||||
|
||||
/**
|
||||
* 邮箱验证码接口.
|
||||
*/
|
||||
class Ems extends ApiController
|
||||
{
|
||||
protected $noNeedLogin = '*';
|
||||
protected $needRight = '*';
|
||||
|
||||
|
||||
public function initialize()
|
||||
{
|
||||
}
|
||||
|
||||
/**
|
||||
* 发送验证码
|
||||
*
|
||||
* @param string $email 邮箱
|
||||
* @param string $event 事件名称
|
||||
*/
|
||||
public function send()
|
||||
{
|
||||
$email = $this->request->request('email');
|
||||
$event = $this->request->request('event');
|
||||
$event = $event ? $event : 'register';
|
||||
Event::trigger('email_send', ['email'=>$email, 'event'=>'register'], true);
|
||||
$userinfo = UserModel::getByEmail($email);
|
||||
if ($event == 'register' && $userinfo)
|
||||
Result::instance()->error(('已被注册'));
|
||||
elseif (in_array($event, ['changeemail']) && $userinfo)
|
||||
Result::instance()->error(('已被占用'));
|
||||
elseif (in_array($event, ['changepwd', 'resetpwd']) && !$userinfo)
|
||||
Result::instance()->error(('未注册'));
|
||||
$ret = \ywxapp\library\Email::instance()->sendEmail($email, null, $event);
|
||||
if (!$ret)
|
||||
Result::instance()->error(('发送失败'));
|
||||
|
||||
Result::instance()->success(('发送成功'));
|
||||
}
|
||||
|
||||
/**
|
||||
* 检测验证码
|
||||
*
|
||||
* @param string $email 邮箱
|
||||
* @param string $event 事件名称
|
||||
* @param string $captcha 验证码
|
||||
*/
|
||||
public function check()
|
||||
{
|
||||
$email = $this->request->request('email');
|
||||
$event = $this->request->request('event', 'register');
|
||||
$captcha = $this->request->request('captcha');
|
||||
|
||||
$validate = validate([
|
||||
'email' => 'email',
|
||||
'event' => 'chsAlphaNum',
|
||||
'code' => 'Num',
|
||||
]);
|
||||
if (!$validate->check(['email' => $email, 'event' => $event, 'code' => $captcha]))
|
||||
Result::instance()->error($validate->getError());
|
||||
|
||||
$userinfo = UserModel::where('email', $email)->find();
|
||||
if ($event == 'register' && $userinfo)
|
||||
$this->result->error(('已被注册'));
|
||||
elseif (in_array($event, ['changeemail']) && $userinfo)
|
||||
Result::instance()->error(('已被占用'));
|
||||
elseif (in_array($event, ['changepwd', 'resetpwd']) && !$userinfo)
|
||||
Result::instance()->error(('未注册'));
|
||||
|
||||
$ret = EmailLib::instance()->check($email, $captcha, $event);
|
||||
if (!$ret)
|
||||
Result::instance()->error(('验证码不正确'));
|
||||
|
||||
Result::instance()->success(data: ('验证码正确'));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
<?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 ywxapp\controller\ApiController;
|
||||
|
||||
use think\Response;
|
||||
|
||||
/**
|
||||
* Example 类
|
||||
*
|
||||
* @author ywxapp <admin@ywxapp.cn>
|
||||
*/
|
||||
class Example extends ApiController
|
||||
{
|
||||
|
||||
public function index(): Response
|
||||
{
|
||||
$user = \ywxapp\model\BackendAdmin::where('id', 1)->findOrEmpty();
|
||||
if (!$user->isEmpty()) {
|
||||
# code...
|
||||
|
||||
$roles = $user->roles;
|
||||
foreach ($roles as $role) {
|
||||
//echo $role->name;
|
||||
// 获取中间表模型
|
||||
// dump($role->pivot);
|
||||
}
|
||||
}
|
||||
return json([
|
||||
'message' => 'This is version 1 of the API',
|
||||
'data' => $user,
|
||||
'role' => $user->roles
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
<?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 ywxapp\controller\ApiController;
|
||||
use ywxapp\library\Result;
|
||||
/**
|
||||
* Index 类
|
||||
*
|
||||
* @author ywxapp <admin@ywxapp.cn>
|
||||
*/
|
||||
class Index extends ApiController
|
||||
{
|
||||
|
||||
|
||||
|
||||
public function init(\think\Request $request)
|
||||
{
|
||||
$result = Result::instance()->success(['name' => 'ThinkPHP', 'version' => '8.0', 'ip' => $request->ip(),'param'=>$request->param()], 'success');
|
||||
return $result;
|
||||
}
|
||||
|
||||
|
||||
public function index(\think\Request $request)
|
||||
{
|
||||
$result = Result::instance()->success(['name' => 'ThinkPHP', 'version' => '8.0', 'ip' => $request->ip(),'param'=>$request->param()], 'success');
|
||||
return $result;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,141 @@
|
||||
<?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\Event;
|
||||
use think\exception\ValidateException;
|
||||
use ywxapp\controller\ApiController;
|
||||
use ywxapp\library\Sms as Smslib;
|
||||
use ywxapp\model\Sms as SmsModel;
|
||||
use ywxapp\model\MemberUser as UserModel;
|
||||
use ywxapp\utils\Random;
|
||||
|
||||
/**
|
||||
* Sms 类
|
||||
*
|
||||
* @author ywxapp <admin@ywxapp.cn>
|
||||
*/
|
||||
class Sms extends ApiController
|
||||
{
|
||||
|
||||
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")
|
||||
*/
|
||||
public function send()
|
||||
{
|
||||
$mobile = $this->request->post("mobile");
|
||||
$event = $this->request->post("event", 'register');
|
||||
$type = $this->request->post("type", 'auto');
|
||||
$source_id = $this->request->post("source_id", '');
|
||||
try {
|
||||
validate([
|
||||
'mobile' => 'require|max:11',
|
||||
'event' => 'require|alpha|lower',
|
||||
])->message([
|
||||
'mobile.require' => '手机号必须',
|
||||
'mobile.max' => '手机号最多不能超过11个字符',
|
||||
'event.require' => '事件名称必须',
|
||||
'event.alpha' => '事件名称必须是字母',
|
||||
'event.lower' => '事件名称必须是小写字母',
|
||||
])->check([
|
||||
'mobile' => $mobile,
|
||||
'event' => $event,
|
||||
]);
|
||||
$last = Smslib::get($mobile, $event);
|
||||
if ($last && time() - (int) $last['create_at'] < 60) {
|
||||
$this->result->error('发送频繁');
|
||||
}
|
||||
$ipSendTotal = SmsModel::where(['ip' => $this->request->ip()])->whereTime('create_at', '-1 hours')->count();
|
||||
if ($ipSendTotal >= 5) {
|
||||
$this->result->error('发送频繁');
|
||||
}
|
||||
if ($event) {
|
||||
$userinfo = UserModel::getByMobile($mobile);
|
||||
if ($event == 'register' && $userinfo) {
|
||||
//已被注册
|
||||
$this->result->error('已被注册');
|
||||
} elseif (in_array($event, ['changemobile']) && $userinfo) {
|
||||
//被占用
|
||||
$this->result->error('已被占用');
|
||||
} elseif (in_array($event, ['changepwd', 'resetpwd']) && ! $userinfo) {
|
||||
//未注册
|
||||
$this->result->error('未注册');
|
||||
}
|
||||
}
|
||||
if (! Event::hasListener('SmsSend')) {
|
||||
$this->result->error('请在后台插件管理安装短信验证插件');
|
||||
}
|
||||
$ret = Smslib::send($mobile, null, $event);
|
||||
if ($ret) {
|
||||
$this->result->success('发送成功');
|
||||
} else {
|
||||
$this->result->error('发送失败,请检查短信配置是否正确');
|
||||
}
|
||||
} catch (ValidateException $e) {
|
||||
$this->result->error($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="验证码")
|
||||
*/
|
||||
public function check()
|
||||
{
|
||||
$mobile = $this->request->post("mobile");
|
||||
$event = $this->request->post("event", 'register');
|
||||
$captcha = $this->request->post("captcha");
|
||||
if (! $mobile || ! \think\Validate::regex($mobile, "^1\d{10}$")) {
|
||||
$this->result->error('手机号不正确');
|
||||
}
|
||||
if (! preg_match("/^[a-z0-9_\-]{3,30}\$/i", $event)) {
|
||||
$this->result->error('事件名称错误');
|
||||
}
|
||||
if (! preg_match("/^[a-z0-9]{4,6}\$/i", $captcha)) {
|
||||
$this->result->error('验证码格式错误');
|
||||
}
|
||||
|
||||
if ($event) {
|
||||
$userinfo = UserModel::getByMobile($mobile);
|
||||
if ($event == 'register' && $userinfo) {
|
||||
//已被注册
|
||||
$this->result->error('已被注册');
|
||||
} elseif (in_array($event, ['changemobile']) && $userinfo) {
|
||||
//被占用
|
||||
$this->result->error('已被占用');
|
||||
} elseif (in_array($event, ['changepwd', 'resetpwd']) && ! $userinfo) {
|
||||
//未注册
|
||||
$this->result->error('未注册');
|
||||
}
|
||||
}
|
||||
$ret = Smslib::check($mobile, $captcha, $event);
|
||||
if ($ret) {
|
||||
$this->result->success('成功');
|
||||
} else {
|
||||
$this->result->error('验证码不正确');
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,206 @@
|
||||
<?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 app\api\validate\Login as LoginValidate;
|
||||
use think\facade\Event;
|
||||
use think\facade\Validate;
|
||||
use ywxapp\controller\ApiController;
|
||||
use ywxapp\model\MemberUser as UserModel;
|
||||
use ywxapp\service\JwtService;
|
||||
|
||||
|
||||
class User extends ApiController
|
||||
{
|
||||
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
|
||||
*/
|
||||
|
||||
public function login(\think\Request $request)
|
||||
{
|
||||
|
||||
$data = $this->request->param();
|
||||
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);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Get user detail
|
||||
* @route get /detail, method:get
|
||||
* @param int $uid
|
||||
* @return \think\Response
|
||||
*/
|
||||
|
||||
public function detail(\think\Request $request, int $uid = 0)
|
||||
{
|
||||
if (! $uid) {
|
||||
$this->result->error('Invalid parameters', 404);
|
||||
}
|
||||
|
||||
$this->result->success($this->user->info);
|
||||
}
|
||||
|
||||
/**
|
||||
* update user detail
|
||||
* @route put /update, method:get
|
||||
* @param int $uid
|
||||
* @return \think\Response
|
||||
*/
|
||||
|
||||
public function update(\think\Request $request, int $uid = 0)
|
||||
{
|
||||
if (! $uid) {
|
||||
$this->result->error('Invalid parameters', 404);
|
||||
}
|
||||
|
||||
$this->result->success($this->user->info);
|
||||
}
|
||||
|
||||
/**
|
||||
* DELETE user detail
|
||||
* @route DELETE /update, method:DELETE
|
||||
* @param int $uid
|
||||
* @return \think\Response
|
||||
*/
|
||||
|
||||
public function delete(\think\Request $request, int $uid = 0)
|
||||
{
|
||||
if (! $uid) {
|
||||
$this->result->error('Invalid parameters', 404);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
private function getEncryptPassword($password, $salt = '')
|
||||
{
|
||||
return md5(md5($password) . $salt);
|
||||
}
|
||||
|
||||
|
||||
public function init()
|
||||
{
|
||||
return json(['message' => 'This is version 1 of the API']);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
<?php
|
||||
// +----------------------------------------------------------------------
|
||||
// | YwxApp [ WE CAN DO IT JUST THINK ]
|
||||
// +----------------------------------------------------------------------
|
||||
// | Copyright (c) 2026-2036 http://ywxapp.cn All rights reserved.
|
||||
// +----------------------------------------------------------------------
|
||||
// | Author: ywxapp<admin@ywxapp.cn>
|
||||
// +----------------------------------------------------------------------
|
||||
// controller/api/v1/Example.php
|
||||
namespace app\api\controller\v2;
|
||||
|
||||
use think\Response;
|
||||
|
||||
/**
|
||||
* Example 类
|
||||
*
|
||||
* @author ywxapp <admin@ywxapp.cn>
|
||||
*/
|
||||
class Example
|
||||
{
|
||||
|
||||
public function index(): Response
|
||||
{
|
||||
return json(['message' => 'This is version 2 of the API']);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
<?php
|
||||
// +----------------------------------------------------------------------
|
||||
// | YwxApp [ WE CAN DO IT JUST THINK ]
|
||||
// +----------------------------------------------------------------------
|
||||
// | Copyright (c) 2026-2036 http://ywxapp.cn All rights reserved.
|
||||
// +----------------------------------------------------------------------
|
||||
// | Author: ywxapp<admin@ywxapp.cn>
|
||||
// +----------------------------------------------------------------------
|
||||
// 这是系统自动生成的event定义文件
|
||||
return [
|
||||
|
||||
];
|
||||
@@ -0,0 +1,10 @@
|
||||
<?php
|
||||
// +----------------------------------------------------------------------
|
||||
// | YwxApp [ WE CAN DO IT JUST THINK ]
|
||||
// +----------------------------------------------------------------------
|
||||
// | Copyright (c) 2026-2036 http://ywxapp.cn All rights reserved.
|
||||
// +----------------------------------------------------------------------
|
||||
// | Author: ywxapp<admin@ywxapp.cn>
|
||||
// +----------------------------------------------------------------------
|
||||
// 这是系统自动生成的middleware定义文件
|
||||
return [];
|
||||
@@ -0,0 +1,28 @@
|
||||
<?php
|
||||
// +----------------------------------------------------------------------
|
||||
// | YwxApp [ WE CAN DO IT JUST THINK ]
|
||||
// +----------------------------------------------------------------------
|
||||
// | Copyright (c) 2026-2036 http://ywxapp.cn All rights reserved.
|
||||
// +----------------------------------------------------------------------
|
||||
// | Author: ywxapp<admin@ywxapp.cn>
|
||||
// +----------------------------------------------------------------------
|
||||
|
||||
use think\facade\Route;
|
||||
|
||||
// 支持批量添加
|
||||
Route::pattern([
|
||||
'name' => '\w+',
|
||||
'id' => '\d+',
|
||||
]);
|
||||
|
||||
// 一级控制器版本化 API(/api/v1/...、/api/v2/...)
|
||||
//
|
||||
// 插件路由(/<插件>/backend、/<插件>/developer、/<插件>/api 等)统一由
|
||||
// 主应用 ywxapp/service/AppService::loadAddonRoutes() 在 boot 阶段注册,
|
||||
// 本文件【不再】引入插件 route/app.php——否则 think-multi-app 会把 api 当作
|
||||
// 应用名截走并叠加前缀,导致 /api/<插件>/* 或 /api/api/* 恒 404。
|
||||
Route::group(':version', function () {
|
||||
Route::rule(':Controller/:action', ':version.:Controller/:action');
|
||||
})->middleware(\think\middleware\AllowCrossDomain::class)->pattern([
|
||||
'version' => 'v\d+', // 匹配 v1/v2/v3...
|
||||
]);
|
||||
@@ -0,0 +1,45 @@
|
||||
<?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\validate;
|
||||
|
||||
use think\Validate;
|
||||
|
||||
/**
|
||||
* Login 类
|
||||
*
|
||||
* @author ywxapp <admin@ywxapp.cn>
|
||||
*/
|
||||
class Login extends Validate
|
||||
{
|
||||
/**
|
||||
* 定义验证规则
|
||||
* 格式:'字段名' => ['规则1','规则2'...]
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $rule = [
|
||||
'username|用户名' => 'require|max:25',
|
||||
'password|密码' => 'require',
|
||||
//'captcha|验证码' => 'require|captcha',
|
||||
];
|
||||
|
||||
/**
|
||||
* 定义错误信息
|
||||
* 格式:'字段名.规则名' => '错误信息'
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $message = [
|
||||
'username.require' => '请输入用户名',
|
||||
'username.max' => '名称最多不能超过25个字符',
|
||||
'password.require' => '密码错误',
|
||||
];
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
<?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\member\validate;
|
||||
|
||||
use think\Validate;
|
||||
|
||||
/**
|
||||
* Register 类
|
||||
*
|
||||
* @author ywxapp <admin@ywxapp.cn>
|
||||
*/
|
||||
class Register extends Validate
|
||||
{
|
||||
/**
|
||||
* 定义验证规则
|
||||
* 格式:'字段名' => ['规则1','规则2'...]
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $rule = [
|
||||
//'username' => 'require|alphaNum|length:4,20|unique:user',
|
||||
'username' => 'require|alphaNum|length:4,20 ',
|
||||
'password' => 'require|length:8,20|confirm',
|
||||
'email' => 'require|email|unique:user',
|
||||
'mobile' => 'require|regex:/^1[3-9]\d{9}$/|unique:user',
|
||||
'captcha' => 'require|captcha'];
|
||||
|
||||
/**
|
||||
* 定义错误信息
|
||||
* 格式:'字段名.规则名' => '错误信息'
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $message = [
|
||||
'username.require' => '用户名不能为空',
|
||||
'username.alphaNum' => '用户名只能包含字母和数字'];
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
<?php
|
||||
/*
|
||||
* @Author: YwxApp <ywx@ywxapp.cn>
|
||||
* @Date: 2026-04-29 02:32:33
|
||||
* @LastEditors: YwxApp <ywx@ywxapp.cn>
|
||||
* @LastEditTime: 2026-07-21 23:27:10
|
||||
* @Description:
|
||||
* @FilePath: \ywxapp_dev\app\backend\validate\Admin.php
|
||||
* @CustomString: Copyright (c) 2026 YwxApp
|
||||
*/
|
||||
|
||||
declare (strict_types = 1);
|
||||
|
||||
|
||||
function var_export_short($expression, $return = false) {
|
||||
$export = var_export($expression, true);
|
||||
$patterns = [
|
||||
"/array \(/" => "[",
|
||||
"/^([ ]*)\)(,?)$/m" => "$1]$2",
|
||||
"/=>[ ]?\n[ ]+\[/" => "=> [",
|
||||
"/([ ]*)(\'[^\']+\') => ([['])/" => "$1$2 => $3",
|
||||
];
|
||||
$export = preg_replace(array_keys($patterns), array_values($patterns), $export);
|
||||
if ($return) return $export;
|
||||
echo $export;
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
<?php
|
||||
/*
|
||||
* @Author: YwxApp <ywx@ywxapp.cn>
|
||||
* @Date: 2026-04-29 02:32:33
|
||||
* @LastEditors: YwxApp <ywx@ywxapp.cn>
|
||||
* @LastEditTime: 2026-07-21 22:12:33
|
||||
* @Description:
|
||||
* @FilePath: \ywxapp_dev\app\backend\config\view.php
|
||||
* @CustomString: Copyright (c) 2026 YwxApp
|
||||
*/
|
||||
|
||||
return [
|
||||
// 模板引擎类型使用Think
|
||||
// 'type' => \ywxapp\utils\ThinkView::class,
|
||||
// 默认模板渲染规则 1 解析为小写+下划线 2 全部转换小写 3 保持操作方法
|
||||
'auto_rule' => 1,
|
||||
// 模板目录名
|
||||
'view_dir_name' => 'view',
|
||||
//'view_path' => app()->getRootPath() . 'view/admin/',
|
||||
// 模板后缀
|
||||
'view_suffix' => 'html',
|
||||
// 模板文件名分隔符
|
||||
'view_depr' => DIRECTORY_SEPARATOR,
|
||||
// 模板引擎普通标签开始标记
|
||||
'tpl_begin' => '{',
|
||||
// 模板引擎普通标签结束标记
|
||||
'tpl_end' => '}',
|
||||
// 标签库标签开始标记
|
||||
'taglib_begin' => '{',
|
||||
// 标签库标签结束标记
|
||||
'taglib_end' => '}',
|
||||
];
|
||||
@@ -0,0 +1,161 @@
|
||||
<?php
|
||||
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\Ad as AdModel;
|
||||
use app\backend\validate\Ad as AdValidate;
|
||||
|
||||
/**
|
||||
* 站点广告管理
|
||||
*/
|
||||
class Ad extends BackendBase
|
||||
{
|
||||
protected $noNeedLogin = [];
|
||||
protected $noNeedVerify = [];
|
||||
|
||||
protected function initialize()
|
||||
{
|
||||
$this->model = new AdModel();
|
||||
AdModel::ensureSchema();
|
||||
}
|
||||
|
||||
public function index()
|
||||
{
|
||||
$page = $this->request->param('page/d', 1);
|
||||
$limit = $this->request->param('limit/d', 15);
|
||||
$title = $this->request->param('title', '');
|
||||
|
||||
if ($this->request->isAjax()) {
|
||||
$query = $this->model->newQuery();
|
||||
if ($title !== '') {
|
||||
$query->where('title', 'like', '%' . $title . '%');
|
||||
}
|
||||
$list = $query->order('sort', 'desc')
|
||||
->paginate(['page' => $page, 'list_rows' => $limit]);
|
||||
$this->result->setCount($list->total())->success($list->items());
|
||||
}
|
||||
$this->assign('positionList', AdModel::positionList());
|
||||
$this->assign('typeList', AdModel::typeList());
|
||||
return $this->fetch();
|
||||
}
|
||||
|
||||
public function save()
|
||||
{
|
||||
if (! $this->request->isPost()) {
|
||||
$this->result->error('请求方式错误', 405);
|
||||
}
|
||||
$params = $this->request->only(['title', 'type', 'position', 'content', 'url', 'image', 'sort', 'status'], 'post');
|
||||
try {
|
||||
validate(AdValidate::class)->check($params);
|
||||
$this->model->create($params);
|
||||
$this->result->success('', '添加成功');
|
||||
} catch (ValidateException $e) {
|
||||
$this->result->error($e->getMessage());
|
||||
} catch (DbException $e) {
|
||||
$this->result->error('添加失败: ' . $e->getMessage(), 2);
|
||||
}
|
||||
}
|
||||
|
||||
public function edit($id = null)
|
||||
{
|
||||
$id = $id ?: $this->request->param('id');
|
||||
$info = $this->model->find($id);
|
||||
if (! $info) {
|
||||
$this->result->error('记录不存在', 404);
|
||||
}
|
||||
$this->result->success(['info' => $info]);
|
||||
}
|
||||
|
||||
public function update()
|
||||
{
|
||||
if (! ($this->request->isAjax() && $this->request->isPut())) {
|
||||
$this->result->error('请求方式错误', 405);
|
||||
}
|
||||
$params = $this->request->param();
|
||||
$id = $params['id'] ?? null;
|
||||
$info = $this->model->find($id);
|
||||
if (! $info) {
|
||||
$this->result->error('记录不存在', 404);
|
||||
}
|
||||
$statusOnly = isset($params['status'])
|
||||
&& count(array_diff(array_keys($params), ['id', 'status'])) === 0;
|
||||
if (! $statusOnly) {
|
||||
try {
|
||||
validate(AdValidate::class)->check($params);
|
||||
} catch (ValidateException $e) {
|
||||
$this->result->error($e->getMessage());
|
||||
}
|
||||
}
|
||||
$info->save($params);
|
||||
$this->result->success($info, '更新成功');
|
||||
}
|
||||
|
||||
public function recyclebin()
|
||||
{
|
||||
$page = $this->request->param('page/d', 1);
|
||||
$limit = $this->request->param('limit/d', 15);
|
||||
if ($this->request->isAjax()) {
|
||||
$list = $this->model->onlyTrashed()
|
||||
->order('sort', 'desc')
|
||||
->paginate(['page' => $page, 'list_rows' => $limit]);
|
||||
$this->result->setCount($list->total())->success($list->items());
|
||||
}
|
||||
return $this->fetch('ad/index');
|
||||
}
|
||||
|
||||
public function delete()
|
||||
{
|
||||
if ($this->request->isAjax() && $this->request->isDelete()) {
|
||||
$ids = $this->request->param('ids', '');
|
||||
$force = $this->request->param('force', false);
|
||||
if (empty($ids)) {
|
||||
$this->result->error('请选择要删除的数据');
|
||||
}
|
||||
$idsArray = array_filter(explode(',', $ids), 'is_numeric');
|
||||
if (empty($idsArray)) {
|
||||
$this->result->error('参数错误');
|
||||
}
|
||||
try {
|
||||
Db::transaction(function () use ($idsArray, $force) {
|
||||
if ($force) {
|
||||
$this->model->onlyTrashed()->whereIn('id', $idsArray)->select()
|
||||
->each(function ($item) { $item->force()->delete(); });
|
||||
} else {
|
||||
$this->model->destroy($idsArray);
|
||||
}
|
||||
});
|
||||
$this->result->success();
|
||||
} catch (\think\exception\HttpResponseException $e) {
|
||||
throw $e;
|
||||
} catch (\Exception $e) {
|
||||
$this->result->error('删除失败: ' . ($e->getMessage() ?: $e->__toString()), 500);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public function restore($ids = '')
|
||||
{
|
||||
if ($this->request->isAjax() && $this->request->isPost()) {
|
||||
$ids = $this->request->param('ids', '');
|
||||
if (empty($ids)) {
|
||||
$this->result->error('请选择要恢复的数据');
|
||||
}
|
||||
$idsArray = array_filter(explode(',', $ids), 'is_numeric');
|
||||
Db::startTrans();
|
||||
try {
|
||||
$this->model->withTrashed()->where('id', 'in', $idsArray)->select()
|
||||
->each(function ($item) { $item->restore(); });
|
||||
Db::commit();
|
||||
$this->result->success('恢复成功');
|
||||
} catch (DbException $e) {
|
||||
Db::rollback();
|
||||
$this->result->error('恢复失败: ' . $e->getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,280 @@
|
||||
<?php
|
||||
/*
|
||||
* @Author: YwxApp <ywx@ywxapp.cn>
|
||||
* @Date: 2026-07-17 17:02:05
|
||||
* @LastEditors: YwxApp <ywx@ywxapp.cn>
|
||||
* @LastEditTime: 2026-08-03 13:33:07
|
||||
* @Description:
|
||||
* @FilePath: \ywxapp_dev\app\backend\controller\AddonMonitor.php
|
||||
* @CustomString: Copyright (c) 2026 YwxApp
|
||||
*/
|
||||
|
||||
declare(strict_types=1);
|
||||
namespace app\backend\controller;
|
||||
|
||||
use think\facade\View;
|
||||
use ywxapp\controller\BackendBase;
|
||||
use ywxapp\service\AddonPerformanceMonitor;
|
||||
use ywxapp\service\AddonHotReload;
|
||||
/**
|
||||
* AddonMonitor 类
|
||||
*
|
||||
* @author ywxapp <admin@ywxapp.cn>
|
||||
*/
|
||||
class AddonMonitor extends BackendBase
|
||||
{
|
||||
protected $noNeedLogin = [];
|
||||
protected $noNeedVerify = [];
|
||||
|
||||
|
||||
public function index()
|
||||
{
|
||||
if ($this->request->isAjax()) {
|
||||
$action = $this->request->param('action', 'overview');
|
||||
|
||||
switch ($action) {
|
||||
case 'overview':
|
||||
return $this->getOverview();
|
||||
case 'performance':
|
||||
return $this->getPerformance();
|
||||
case 'health':
|
||||
return $this->getHealth();
|
||||
case 'reload':
|
||||
return $this->getReloadStats();
|
||||
case 'reload_addon':
|
||||
return $this->reloadAddon();
|
||||
case 'clear_performance':
|
||||
return $this->clearPerformanceData();
|
||||
default:
|
||||
return $this->result->error('未知操作');
|
||||
}
|
||||
}
|
||||
|
||||
return View::fetch('addon_monitor/index');
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取概览数据
|
||||
*/
|
||||
private function getOverview()
|
||||
{
|
||||
$overview = AddonPerformanceMonitor::getAllPerformanceOverview();
|
||||
$problematic = AddonPerformanceMonitor::getProblematicaddon();
|
||||
$reloadStats = AddonHotReload::getReloadStatistics();
|
||||
|
||||
// 统计数据
|
||||
$totaladdon = count($overview);
|
||||
$healthyaddon = 0;
|
||||
$problematicaddon = count($problematic);
|
||||
$totalCalls = 0;
|
||||
$avgExecutionTime = 0;
|
||||
$avgSuccessRate = 0;
|
||||
|
||||
foreach ($overview as $stats) {
|
||||
if ($stats['success_rate'] > 95 && $stats['avg_execution_time'] < 1.0) {
|
||||
$healthyaddon++;
|
||||
}
|
||||
$totalCalls += $stats['total_calls'];
|
||||
$avgExecutionTime += $stats['avg_execution_time'];
|
||||
$avgSuccessRate += $stats['success_rate'];
|
||||
}
|
||||
|
||||
if ($totaladdon > 0) {
|
||||
$avgExecutionTime = $avgExecutionTime / $totaladdon;
|
||||
$avgSuccessRate = $avgSuccessRate / $totaladdon;
|
||||
}
|
||||
|
||||
return $this->result->success([
|
||||
'total_addon' => $totaladdon,
|
||||
'healthy_addon' => $healthyaddon,
|
||||
'problematic_addon' => $problematicaddon,
|
||||
'total_calls' => $totalCalls,
|
||||
'avg_execution_time' => round($avgExecutionTime * 1000, 2),
|
||||
'avg_success_rate' => round($avgSuccessRate, 2),
|
||||
'reload_stats' => $reloadStats
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取性能详情
|
||||
*/
|
||||
private function getPerformance()
|
||||
{
|
||||
$addon = $this->request->param('addon', '');
|
||||
|
||||
if (empty($addon)) {
|
||||
// 返回所有插件性能数据
|
||||
$overview = AddonPerformanceMonitor::getAllPerformanceOverview();
|
||||
return $this->result->success($overview);
|
||||
} else {
|
||||
// 返回指定插件性能数据
|
||||
$stats = AddonPerformanceMonitor::getPerformanceStats($addon);
|
||||
return $this->result->success($stats);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取健康状态
|
||||
*/
|
||||
private function getHealth()
|
||||
{
|
||||
$problematic = AddonPerformanceMonitor::getProblematicaddon();
|
||||
$overview = AddonPerformanceMonitor::getAllPerformanceOverview();
|
||||
|
||||
// 生成健康报告
|
||||
$healthReport = [];
|
||||
foreach ($overview as $addon => $stats) {
|
||||
$isHealthy = true;
|
||||
$issues = [];
|
||||
|
||||
if ($stats['avg_execution_time'] > AddonPerformanceMonitor::PERFORMANCE_THRESHOLD) {
|
||||
$isHealthy = false;
|
||||
$issues[] = '执行时间过长';
|
||||
}
|
||||
|
||||
if ($stats['avg_memory_usage'] > AddonPerformanceMonitor::MEMORY_THRESHOLD) {
|
||||
$isHealthy = false;
|
||||
$issues[] = '内存使用过多';
|
||||
}
|
||||
|
||||
if ($stats['success_rate'] < 90) {
|
||||
$isHealthy = false;
|
||||
$issues[] = '成功率过低';
|
||||
}
|
||||
|
||||
$healthReport[$addon] = [
|
||||
'healthy' => $isHealthy,
|
||||
'issues' => $issues,
|
||||
'stats' => $stats
|
||||
];
|
||||
}
|
||||
|
||||
return $this->result->success([
|
||||
'health_report' => $healthReport,
|
||||
'problematic_addon' => $problematic
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取热重载统计
|
||||
*/
|
||||
private function getReloadStats()
|
||||
{
|
||||
$stats = AddonHotReload::getReloadStatistics();
|
||||
return $this->result->success($stats);
|
||||
}
|
||||
|
||||
/**
|
||||
* 重载插件
|
||||
*/
|
||||
private function reloadAddon()
|
||||
{
|
||||
$addon = $this->request->param('addon', '');
|
||||
$force = $this->request->param('force', false);
|
||||
|
||||
if (empty($addon)) {
|
||||
return $this->result->error('请指定插件名称');
|
||||
}
|
||||
|
||||
if (!config('app.app_debug')) {
|
||||
return $this->result->error('热重载功能仅在开发环境可用');
|
||||
}
|
||||
|
||||
try {
|
||||
$reloaded = AddonHotReload::reloadAddon($addon, $force);
|
||||
|
||||
if ($reloaded) {
|
||||
$status = AddonHotReload::getReloadStatus($addon);
|
||||
return $this->result->success([
|
||||
'reloaded' => true,
|
||||
'status' => $status
|
||||
], '插件重载成功');
|
||||
} else {
|
||||
return $this->result->success([
|
||||
'reloaded' => false
|
||||
], '插件无文件变更,无需重载');
|
||||
}
|
||||
} catch (\Exception $e) {
|
||||
return $this->result->error('插件重载失败:' . $e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 清除性能数据
|
||||
*/
|
||||
private function clearPerformanceData()
|
||||
{
|
||||
$addon = $this->request->param('addon', '');
|
||||
|
||||
try {
|
||||
if (empty($addon)) {
|
||||
AddonPerformanceMonitor::clearPerformanceData();
|
||||
return $this->result->success([], '已清除所有插件性能数据');
|
||||
} else {
|
||||
AddonPerformanceMonitor::clearPerformanceData($addon);
|
||||
return $this->result->success([], "已清除插件 {$addon} 的性能数据");
|
||||
}
|
||||
} catch (\Exception $e) {
|
||||
return $this->result->error('清除性能数据失败:' . $e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 性能图表数据
|
||||
*/
|
||||
public function chart()
|
||||
{
|
||||
$type = $this->request->param('type', 'execution_time');
|
||||
$period = $this->request->param('period', 'day'); // day, week, month
|
||||
|
||||
$overview = AddonPerformanceMonitor::getAllPerformanceOverview();
|
||||
$chartData = [];
|
||||
|
||||
foreach ($overview as $addon => $stats) {
|
||||
if ($stats['total_calls'] === 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
switch ($type) {
|
||||
case 'execution_time':
|
||||
$chartData[] = [
|
||||
'name' => $addon,
|
||||
'value' => round($stats['avg_execution_time'] * 1000, 2)
|
||||
];
|
||||
break;
|
||||
case 'memory_usage':
|
||||
$chartData[] = [
|
||||
'name' => $addon,
|
||||
'value' => round($stats['avg_memory_usage'] / 1024, 2)
|
||||
];
|
||||
break;
|
||||
case 'success_rate':
|
||||
$chartData[] = [
|
||||
'name' => $addon,
|
||||
'value' => round($stats['success_rate'], 2)
|
||||
];
|
||||
break;
|
||||
case 'call_count':
|
||||
$chartData[] = [
|
||||
'name' => $addon,
|
||||
'value' => $stats['total_calls']
|
||||
];
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// 按值排序
|
||||
usort($chartData, function($a, $b) {
|
||||
return $b['value'] - $a['value'];
|
||||
});
|
||||
|
||||
// 只返回前10个
|
||||
$chartData = array_slice($chartData, 0, 10);
|
||||
|
||||
return $this->result->success([
|
||||
'chart_data' => $chartData,
|
||||
'type' => $type,
|
||||
'period' => $period
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,780 @@
|
||||
<?php
|
||||
/*
|
||||
* @Author: YwxApp <ywx@ywxapp.cn>
|
||||
* @Date: 2026-04-29 02:32:33
|
||||
* @LastEditors: YwxApp <ywx@ywxapp.cn>
|
||||
* @LastEditTime: 2026-08-16 15:34:28
|
||||
* @Description:
|
||||
* @FilePath: \ywxapp_dev\app\backend\controller\Addons.php
|
||||
* @CustomString: Copyright (c) 2026 YwxApp
|
||||
*/
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace app\backend\controller;
|
||||
|
||||
use think\exception\HttpException;
|
||||
use think\facade\Config;
|
||||
use think\facade\Request;
|
||||
use think\facade\View;
|
||||
use ywxapp\exception\AddonException;
|
||||
use ywxapp\service\AddonService;
|
||||
use ywxapp\service\AddonDevService;
|
||||
use ywxapp\service\RemoteService;
|
||||
use Exception;
|
||||
use ywxapp\controller\BackendBase;
|
||||
/**
|
||||
* addon 类
|
||||
*
|
||||
* @author ywxapp <admin@ywxapp.cn>
|
||||
*/
|
||||
class Addons extends BackendBase
|
||||
{
|
||||
|
||||
/**
|
||||
* 控制器初始化 initialize
|
||||
* @return void
|
||||
*/
|
||||
public function initialize() {}
|
||||
|
||||
/**
|
||||
* 插件列表
|
||||
*/
|
||||
public function index()
|
||||
{
|
||||
if ($this->request->isAjax()) {
|
||||
$results = scandir(ADDON_PATH);
|
||||
$list = [];
|
||||
foreach ($results as $name) {
|
||||
if ($name === '.' or $name === '..' or is_file(ADDON_PATH . $name)) {
|
||||
continue;
|
||||
}
|
||||
$addonDir = ADDON_PATH . $name . DIRECTORY_SEPARATOR;
|
||||
if (! is_dir($addonDir)) {
|
||||
continue;
|
||||
}
|
||||
$infoFile = $addonDir . 'info.php';
|
||||
if (! is_file($infoFile)) {
|
||||
continue;
|
||||
}
|
||||
$info = include $infoFile;
|
||||
if (! isset($info['name'])) {
|
||||
continue;
|
||||
}
|
||||
// 统一字段,适配前端 layui 表格(列: title/author/version/description/status/id)
|
||||
$info['id'] = $info['name'];
|
||||
$info['description'] = $info['intro'] ?? '';
|
||||
$info['status'] = $info['state'] ?? 0;
|
||||
$info['hasConfig'] = is_file($addonDir . 'config.php');
|
||||
$list[] = $info;
|
||||
}
|
||||
|
||||
// 关键字搜索
|
||||
$keyword = $this->request->param('keyword', '');
|
||||
if ($keyword !== '') {
|
||||
$list = array_values(array_filter($list, function ($it) use ($keyword) {
|
||||
return stripos((string) ($it['title'] ?? ''), $keyword) !== false
|
||||
|| stripos((string) ($it['name'] ?? ''), $keyword) !== false;
|
||||
}));
|
||||
}
|
||||
// 运行状态筛选
|
||||
$status = $this->request->param('status', '');
|
||||
if ($status !== '') {
|
||||
$list = array_values(array_filter($list, function ($it) use ($status) {
|
||||
return (string) ($it['status'] ?? '') === (string) $status;
|
||||
}));
|
||||
}
|
||||
|
||||
// 分页
|
||||
$total = count($list);
|
||||
$page = (int) $this->request->param('page', 1);
|
||||
$limit = (int) $this->request->param('limit', 10);
|
||||
$pageList = array_slice($list, max(0, ($page - 1) * $limit), $limit);
|
||||
|
||||
$this->result->setCount($total)->success($pageList, '获取成功');
|
||||
}
|
||||
return View::fetch('addon/index');
|
||||
}
|
||||
|
||||
/**
|
||||
* 应用市场(Discuz 式远程插件市场,核心后台「插件管理」)
|
||||
*
|
||||
* 服务中心(本机,ywxapp.api_url 为空或指向自己):直接读取本地市场目录(wxapp_appmarket_addon_list),
|
||||
* 无需依赖外部地址,应用商店即刻可见本机托管的插件。
|
||||
* 客户机(ywxapp.api_url 指向其他服务器,见 is_market_client()):经中心站 API(RemoteService::lists)拉取远程市场列表,
|
||||
* 可一键安装到本客户机(downloadInstall)。客户机不安装 market 插件,应用商店功能统一落在本核心控制器。
|
||||
*/
|
||||
public function market()
|
||||
{
|
||||
$apiUrl = Config::get('ywxapp.api_url', '');
|
||||
$isClient = (bool) is_market_client();
|
||||
$keyword = trim((string) $this->request->param('keyword', ''));
|
||||
$category = trim((string) $this->request->param('category', ''));
|
||||
$type = trim((string) $this->request->param('type', ''));
|
||||
$order = trim((string) $this->request->param('order', 'new'));
|
||||
$list = [];
|
||||
$error = '';
|
||||
$categories = [];
|
||||
|
||||
if ($isClient) {
|
||||
// 客户机:经服务中心 API 拉取市场列表
|
||||
if ($apiUrl) {
|
||||
try {
|
||||
$params = [];
|
||||
if ($keyword !== '') {
|
||||
$params['keyword'] = $keyword;
|
||||
}
|
||||
if ($category !== '') {
|
||||
$params['category'] = $category;
|
||||
}
|
||||
if ($order !== '' && $order !== 'new') {
|
||||
$params['order'] = $order;
|
||||
}
|
||||
if ($type !== '') {
|
||||
$params['type'] = $type;
|
||||
}
|
||||
$resp = (new RemoteService())->lists($params);
|
||||
if (!empty($resp['success']) && !empty($resp['data']['list'])) {
|
||||
$list = $resp['data']['list'];
|
||||
} else {
|
||||
$error = $resp['message'] ?? '获取市场列表失败';
|
||||
}
|
||||
} catch (\Throwable $e) {
|
||||
$error = '连接市场失败:' . $e->getMessage();
|
||||
}
|
||||
} else {
|
||||
$error = '未配置市场地址(请在后台配置 ywxapp.api_url)';
|
||||
}
|
||||
} elseif (!class_exists(\addon\appmall\service\MarketService::class)) {
|
||||
// 本机即中心站但未安装 appmall 插件:市场目录不可用
|
||||
$error = '本机未安装 appmall 插件:请安装插件以启用市场目录,或配置 ywxapp.api_url 指向中心站';
|
||||
} else {
|
||||
// 服务中心(本机):委托 appmall 插件读取本地市场目录(中心域逻辑全部在插件侧)
|
||||
try {
|
||||
$r = \addon\appmall\service\MarketService::instance()->catalog($keyword, $category, $order, $type);
|
||||
$list = $r['list'];
|
||||
$categories = $r['categories'];
|
||||
if (empty($list)) {
|
||||
$error = '本机市场目录为空:可导入 docs/*-appmarket_addon_list.sql 测试记录,或在「开发者中心」上传插件';
|
||||
}
|
||||
} catch (\Throwable $e) {
|
||||
$error = '读取本机市场目录失败:' . $e->getMessage();
|
||||
}
|
||||
}
|
||||
|
||||
// 标注本地安装状态与可升级性(addon 看 addon/<name>/info.php;template 看 templates/<name>/template.json)
|
||||
foreach ($list as &$it) {
|
||||
$name = $it['name'] ?? '';
|
||||
$installed = false;
|
||||
$localVersion = '';
|
||||
if (($it['type'] ?? 'addon') === 'template') {
|
||||
$tplJson = root_path() . 'templates' . DIRECTORY_SEPARATOR . $name . DIRECTORY_SEPARATOR . 'template.json';
|
||||
if (is_file($tplJson)) {
|
||||
$tj = (array) json_decode((string) @file_get_contents($tplJson), true);
|
||||
$localVersion = (string) ($tj['version'] ?? '');
|
||||
$installed = true;
|
||||
}
|
||||
} else {
|
||||
$localInfoFile = ADDON_PATH . $name . DIRECTORY_SEPARATOR . 'info.php';
|
||||
if (is_file($localInfoFile)) {
|
||||
$li = include $localInfoFile;
|
||||
$localVersion = $li['version'] ?? '';
|
||||
$installed = true;
|
||||
}
|
||||
}
|
||||
$it['installed'] = $installed;
|
||||
$it['local_version'] = $localVersion;
|
||||
$it['upgradable'] = $installed && $localVersion !== ''
|
||||
&& isset($it['version'])
|
||||
&& version_compare($it['version'], $localVersion, '>');
|
||||
// 预解析 tags 为数组,供前端标签展示(避免模板内嵌 PHP)
|
||||
$rawTags = trim((string) ($it['tags'] ?? ''));
|
||||
$it['tag_list'] = $rawTags === '' ? [] : array_values(array_filter(
|
||||
array_map('trim', explode(',', $rawTags)),
|
||||
function ($t) { return $t !== ''; }
|
||||
));
|
||||
// 元数据键兜底:旧库无 category/rating/screenshots 列或远程列表未返回时,
|
||||
// 避免模板({$it.category} / {$it.rating})触发 Undefined array key 报错。
|
||||
foreach (['category', 'screenshots', 'rating'] as $mk) {
|
||||
if (!isset($it[$mk])) {
|
||||
$it[$mk] = '';
|
||||
}
|
||||
}
|
||||
if (!isset($it['type']) || $it['type'] === '') {
|
||||
$it['type'] = 'addon';
|
||||
}
|
||||
}
|
||||
unset($it);
|
||||
|
||||
// 客户机模式:分类下拉数据源从返回列表中动态归纳
|
||||
if ($isClient && empty($categories) && !empty($list)) {
|
||||
$seen = [];
|
||||
foreach ($list as $it) {
|
||||
$c = (string) ($it['category'] ?? '');
|
||||
if ($c !== '' && !in_array($c, $seen, true)) {
|
||||
$seen[] = $c;
|
||||
}
|
||||
}
|
||||
$categories = $seen;
|
||||
}
|
||||
|
||||
View::assign([
|
||||
'list' => $list,
|
||||
'error' => $error,
|
||||
'is_client' => $isClient,
|
||||
'api_url' => $apiUrl,
|
||||
'keyword' => $keyword,
|
||||
'category' => $category,
|
||||
'type' => $type,
|
||||
'order' => $order,
|
||||
'categories' => $categories,
|
||||
]);
|
||||
return View::fetch('addon/market');
|
||||
}
|
||||
|
||||
/**
|
||||
* 我的插件(核心后台「插件管理」)
|
||||
* 列出本机已安装插件,便于从应用市场跳转后集中管理(配置/升级/启停/卸载见「插件管理」列表)。
|
||||
*/
|
||||
public function my()
|
||||
{
|
||||
$list = [];
|
||||
if (is_dir(ADDON_PATH)) {
|
||||
foreach (scandir(ADDON_PATH) as $name) {
|
||||
if ($name === '.' || $name === '..' || !is_dir(ADDON_PATH . $name)) {
|
||||
continue;
|
||||
}
|
||||
$infoFile = ADDON_PATH . $name . DIRECTORY_SEPARATOR . 'info.php';
|
||||
if (!is_file($infoFile)) {
|
||||
continue;
|
||||
}
|
||||
$info = include $infoFile;
|
||||
if (!isset($info['name'])) {
|
||||
continue;
|
||||
}
|
||||
$info['id'] = $info['name'];
|
||||
$info['description'] = $info['intro'] ?? '';
|
||||
$info['status'] = $info['state'] ?? 0;
|
||||
$info['hasConfig'] = is_file(ADDON_PATH . $name . DIRECTORY_SEPARATOR . 'config.php');
|
||||
$list[] = $info;
|
||||
}
|
||||
}
|
||||
View::assign('list', $list);
|
||||
View::assign('is_client', (bool) is_market_client());
|
||||
return View::fetch('addon/my');
|
||||
}
|
||||
|
||||
/**
|
||||
* 运营退款(后台视角):按插件名退本地最新「已支付」订单,
|
||||
* 吊销授权 + 订单置已退款 + 收益冲正(与会员端 /api/v1/addon/refund 逻辑一致)。
|
||||
* 仅中心站模式(ywxapp.api_url 为空或指向自己)支持;客户机模式订单在中心站、属会员 uid,
|
||||
* 后台无会员信息,需到会员中心申请退款。
|
||||
*/
|
||||
public function refund(Request $request)
|
||||
{
|
||||
if (is_market_client()) {
|
||||
return $this->result->error('远程模式订单存于中心站,请于会员中心申请退款');
|
||||
}
|
||||
if (!class_exists(\addon\appmall\service\MarketService::class)) {
|
||||
return $this->result->error('本机未安装 appmall 插件,无法执行退款');
|
||||
}
|
||||
try {
|
||||
\addon\appmall\service\MarketService::instance()
|
||||
->operatorRefund((string) $request->post('name', ''));
|
||||
} catch (\Throwable $e) {
|
||||
return $this->result->error($e->getMessage());
|
||||
}
|
||||
return $this->result->success([], '退款成功,授权已吊销');
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* 发布到官方公共市场(www.ywxapp.cn)
|
||||
* 打包本机已安装插件并上传到官方市场提交接口,进入「待审核」状态,
|
||||
* 运营审核通过后才在公开市场可见。需先在 .env 配置 APPMARKET_DEV_TOKEN。
|
||||
*/
|
||||
public function submitOfficial()
|
||||
{
|
||||
try {
|
||||
$name = input('name', '');
|
||||
if (!$name || !preg_match('/^[a-zA-Z0-9_]+$/', $name)) {
|
||||
return $this->result->error('插件名称格式不正确');
|
||||
}
|
||||
$token = config('ywxapp.developer_token', '');
|
||||
if (!$token) {
|
||||
return $this->result->error('未配置开发者令牌:请在 .env 设置 DEVELOPER_TOKEN');
|
||||
}
|
||||
$AddonService = AddonService::instance($name);
|
||||
if (!$AddonService->isInstalled()) {
|
||||
return $this->result->error('插件未安装,无法发布');
|
||||
}
|
||||
// 打包(返回本地 zip 路径)
|
||||
$zipFile = $AddonService->package();
|
||||
$infoFile = ADDON_PATH . $name . DIRECTORY_SEPARATOR . 'info.php';
|
||||
$info = is_file($infoFile) ? (array) include $infoFile : [];
|
||||
$meta = [
|
||||
'name' => $info['name'] ?? $name,
|
||||
'title' => $info['title'] ?? $name,
|
||||
'author' => $info['author'] ?? '',
|
||||
'version' => $info['version'] ?? '',
|
||||
'price' => $info['price'] ?? 0,
|
||||
'description' => $info['intro'] ?? ($info['description'] ?? ''),
|
||||
];
|
||||
if (empty($meta['version'])) {
|
||||
return $this->result->error('插件版本号缺失,无法提交');
|
||||
}
|
||||
|
||||
$client = new \GuzzleHttp\Client([
|
||||
'base_uri' => config('ywxapp.api_url'),
|
||||
'timeout' => 60,
|
||||
'verify' => (bool) config('appmall.ssl_verify', true),
|
||||
]);
|
||||
$response = $client->post('/appmall/api/addon/submit', [
|
||||
'multipart' => [
|
||||
['name' => 'token', 'contents' => $token],
|
||||
['name' => 'name', 'contents' => $meta['name']],
|
||||
['name' => 'title', 'contents' => $meta['title']],
|
||||
['name' => 'author', 'contents' => $meta['author']],
|
||||
['name' => 'version', 'contents' => $meta['version']],
|
||||
['name' => 'price', 'contents' => (string) $meta['price']],
|
||||
['name' => 'description', 'contents' => $meta['description']],
|
||||
['name' => 'file', 'contents' => fopen($zipFile, 'r'), 'filename' => basename($zipFile)],
|
||||
],
|
||||
]);
|
||||
$json = json_decode($response->getBody()->getContents(), true);
|
||||
if (empty($json) || (int) ($json['code'] ?? 0) !== 1) {
|
||||
return $this->result->error('官方市场返回:' . ($json['msg'] ?? '未知错误'));
|
||||
}
|
||||
return $this->result->success($json['data'] ?? [], '已提交,等待官方审核');
|
||||
} catch (\GuzzleHttp\Exception\RequestException $e) {
|
||||
$msg = $e->getMessage();
|
||||
if (stripos($msg, 'SSL certificate') !== false || stripos($msg, 'cURL error 60') !== false) {
|
||||
$msg = 'SSL 证书验证失败(cURL error 60):请配置 php.ini 的 curl.cainfo,或在 .env 临时设置 APPMARKET_SSL_VERIFY=false。';
|
||||
}
|
||||
return $this->result->error('提交到官方市场失败:' . $msg);
|
||||
} catch (AddonException $e) {
|
||||
return $this->result->error($e->getMessage());
|
||||
} catch (\Exception $e) {
|
||||
return $this->result->error($e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public function pack()
|
||||
{
|
||||
$addonName = input('name');
|
||||
if (!$addonName) {
|
||||
return $this->result->error('请指定插件名称');
|
||||
}
|
||||
$AddonService = AddonService::instance($addonName);
|
||||
if (!$AddonService->isInstalled()) {
|
||||
return $this->result->error('插件不存在');
|
||||
}
|
||||
try {
|
||||
$zipFile = $AddonService->package();
|
||||
return download($zipFile, $addonName . '-' . $AddonService->getVersion() . '.zip');
|
||||
} catch (\Exception $e) {
|
||||
return $this->result->error($e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 上传安装插件
|
||||
*/
|
||||
public function upload()
|
||||
{
|
||||
if (Request::isAjax()) {
|
||||
Config::set(['default_return_type' => 'json'], 'app');
|
||||
$info = [];
|
||||
$file = $this->request->file('file');
|
||||
try {
|
||||
$uid = $this->request->post("uid");
|
||||
$token = $this->request->post("token");
|
||||
$faversion = $this->request->post("faversion");
|
||||
// 鉴权由 Backend 中间件统一处理;uid/token 仅作为离线安装校验参数透传
|
||||
$extend = [
|
||||
'uid' => $uid,
|
||||
'token' => $token,
|
||||
'faversion' => $faversion,
|
||||
];
|
||||
$info = AddonService::instance()->local($file, $extend);
|
||||
} catch (AddonException $e) {
|
||||
$this->result->error(LANG($e->getMessage(), $e->getCode()));
|
||||
} catch (\Exception $e) {
|
||||
$this->result->error(lang($e->getMessage()));
|
||||
}
|
||||
$this->result->success(['addon' => $info], lang('Offline installed tips'),);
|
||||
}
|
||||
return View::fetch('addon/index');
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* 安装插件
|
||||
*/
|
||||
public function install()
|
||||
{
|
||||
try {
|
||||
$file = Request::file('addon_file');
|
||||
if (!$file) {
|
||||
return $this->result->error('请上传插件文件');
|
||||
}
|
||||
$AddonService = AddonService::instance();
|
||||
$extend = [
|
||||
'install_user' => session('user_id'),
|
||||
'install_ip' => Request::ip(),
|
||||
'install_time' => time()
|
||||
];
|
||||
$info = $AddonService->local($file, $extend);
|
||||
return $this->result->success($info, '插件安装成功');
|
||||
} catch (AddonException $e) {
|
||||
return $this->result->error($e->getMessage());
|
||||
} catch (\Exception $e) {
|
||||
return $this->result->error($e->getMessage());
|
||||
}
|
||||
}
|
||||
/**
|
||||
* 卸载插件
|
||||
*/
|
||||
public function uninstall()
|
||||
{
|
||||
try {
|
||||
$addonName = input('name'); // 从请求参数获取插件名
|
||||
// 验证插件名称
|
||||
if (!$addonName || !preg_match('/^[a-zA-Z0-9_]+$/', $addonName)) {
|
||||
return json(['code' => 0, 'msg' => '插件名称格式不正确']);
|
||||
}
|
||||
$AddonService = AddonService::instance($addonName);
|
||||
$result = $AddonService->uninstall();
|
||||
if ($result) {
|
||||
$this->result->success([], '插件卸载成功');
|
||||
} else {
|
||||
$this->result->error('插件卸载失败');
|
||||
}
|
||||
} catch (AddonException $e) {
|
||||
$this->result->error('插件卸载失败:' . $e->getMessage());
|
||||
} catch (\Exception $e) {
|
||||
$this->result->error('插件卸载失败:' . $e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 启用/禁用插件
|
||||
*/
|
||||
public function toggle()
|
||||
{
|
||||
try {
|
||||
$addonName = input('name');
|
||||
$action = input('action'); // enable or disable
|
||||
$AddonService = AddonService::instance($addonName);
|
||||
if (!$AddonService->isInstalled()) {
|
||||
$this->result->error('插件不存在');
|
||||
}
|
||||
if ($action === 'enable') {
|
||||
$AddonService->enable();
|
||||
$this->result->success([], '插件已启用');
|
||||
} else {
|
||||
$AddonService->disable();
|
||||
$this->result->success([], '插件已禁用');
|
||||
}
|
||||
} catch (\Exception $e) {
|
||||
$this->result->error($e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 打包插件
|
||||
*/
|
||||
public function package()
|
||||
{
|
||||
try {
|
||||
$addonName = input('name');
|
||||
$AddonService = AddonService::instance($addonName);
|
||||
if (!$AddonService->isInstalled()) {
|
||||
return $this->result->error('插件不存在');
|
||||
}
|
||||
$zipFile = $AddonService->package();
|
||||
return download($zipFile, $addonName . '-' . $AddonService->getVersion() . '.zip');
|
||||
} catch (Exception $e) {
|
||||
return $this->result->error($e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 下载并安装
|
||||
*/
|
||||
public function downloadInstall()
|
||||
{
|
||||
try {
|
||||
$addonName = input('name');
|
||||
$version = input('version', '');
|
||||
// 商品类型:addon=插件(默认)/ template=模板(下载后走 TemplateInstaller 落地)
|
||||
$type = strtolower((string) input('type', 'addon'));
|
||||
$AddonService = AddonService::instance($addonName);
|
||||
$extend = [];
|
||||
if ($version) {
|
||||
$extend['version'] = $version;
|
||||
}
|
||||
// 运营安装:透传运营者身份用于审计(不消耗会员下载额度;付费插件按运营特权放行)。
|
||||
// 会员端的真实下载限额由 RemoteService::downloadBinary 携带 uid 触发,本路径不重复计限。
|
||||
$extend['operator_id'] = session('user_id') ?? 0;
|
||||
// 下载
|
||||
$zipFile = $AddonService->download($extend);
|
||||
// 模板商品:不走插件安装流程,交给 TemplateInstaller 还原 templates/<name>/ 与静态资源
|
||||
if ($type === 'template') {
|
||||
$tplInfo = \ywxapp\library\TemplateInstaller::install($zipFile, root_path());
|
||||
@unlink($zipFile);
|
||||
return $this->result->success($tplInfo, '模板下载并安装成功,请到「模板中心」启用');
|
||||
}
|
||||
// 安装(download 返回本地路径,包装为 File 后走离线安装流程)
|
||||
$file = new \think\File($zipFile);
|
||||
$info = $AddonService->local($file, $extend);
|
||||
return $this->result->success($info, '下载并安装成功');
|
||||
} catch (AddonException $e) {
|
||||
return $this->result->error($e->getMessage());
|
||||
} catch (Exception $e) {
|
||||
return $this->result->error($e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 在线升级插件(从市场下载新版本覆盖安装)
|
||||
* 访问:POST /backend/addon/upgrade {name, version?}
|
||||
*/
|
||||
public function upgrade()
|
||||
{
|
||||
try {
|
||||
$addonName = input('name', '');
|
||||
$version = input('version', '');
|
||||
if (!$addonName || !preg_match('/^[a-zA-Z0-9_]+$/', $addonName)) {
|
||||
return $this->result->error('插件名称格式不正确');
|
||||
}
|
||||
$AddonService = AddonService::instance($addonName);
|
||||
if (!$AddonService->isInstalled()) {
|
||||
return $this->result->error('插件未安装,无法升级');
|
||||
}
|
||||
$res = $AddonService->onlineUpgrade($version);
|
||||
return $this->result->success(
|
||||
$res,
|
||||
'升级成功:' . ($res['from'] ?? '') . ' → ' . ($res['to'] ?? '')
|
||||
);
|
||||
} catch (AddonException $e) {
|
||||
return $this->result->error($e->getMessage());
|
||||
} catch (\Exception $e) {
|
||||
return $this->result->error($e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 插件配置(通用后台配置页)
|
||||
* 依据插件 config.php 的字段定义渲染表单,保存写入统一配置表(数据库独立项 + 缓存)。
|
||||
* 访问:/backend/addon/setting?addon=<插件标识>
|
||||
*/
|
||||
public function setting()
|
||||
{
|
||||
$addon = input('addon', '');
|
||||
if (!$addon || !preg_match('/^[a-zA-Z0-9_]+$/', $addon) || !is_dir(ADDON_PATH . $addon)) {
|
||||
return $this->result->error('插件不存在');
|
||||
}
|
||||
$configFile = ADDON_PATH . $addon . DIRECTORY_SEPARATOR . 'config.php';
|
||||
$fields = is_file($configFile) ? (array) include $configFile : [];
|
||||
if ($this->request->isPost()) {
|
||||
$post = input('post.');
|
||||
$data = [];
|
||||
foreach ($fields as $f) {
|
||||
$n = $f['name'] ?? '';
|
||||
if ($n !== '' && array_key_exists($n, $post)) {
|
||||
$data[$n] = $post[$n];
|
||||
}
|
||||
}
|
||||
AddonService::config($addon, $data);
|
||||
AddonService::clearConfigCache($addon);
|
||||
return $this->result->success('保存成功');
|
||||
}
|
||||
$saved = AddonService::config($addon);
|
||||
foreach ($fields as &$f) {
|
||||
if (isset($saved[$f['name']])) {
|
||||
$f['value'] = $saved[$f['name']];
|
||||
}
|
||||
}
|
||||
unset($f);
|
||||
View::assign(['addon' => $addon, 'fields' => $fields]);
|
||||
return View::fetch('addon/setting');
|
||||
}
|
||||
|
||||
// ==================== 开发模式:插件设计器(抄 Discuz!) ====================
|
||||
|
||||
/**
|
||||
* 设计器页面(仅开发模式可访问)
|
||||
*/
|
||||
public function design()
|
||||
{
|
||||
if (!AddonDevService::enabled()) {
|
||||
return $this->result->error('开发模式未开启,请在 .env 设置 ADDON_DEVELOPER=true');
|
||||
}
|
||||
$name = input('addon', '');
|
||||
View::assign('addon', $name);
|
||||
return View::fetch('addon/design');
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建插件骨架
|
||||
*/
|
||||
public function designCreate()
|
||||
{
|
||||
if (!AddonDevService::enabled()) {
|
||||
return $this->result->error('开发模式未开启');
|
||||
}
|
||||
try {
|
||||
$name = input('name', '');
|
||||
if (!$name) {
|
||||
return $this->result->error('请填写插件标识');
|
||||
}
|
||||
$meta = [
|
||||
'title' => input('title', ''),
|
||||
'intro' => input('intro', ''),
|
||||
'author' => input('author', ''),
|
||||
'website' => input('website', ''),
|
||||
'version' => input('version', '1.0.0'),
|
||||
'url' => input('url', ''),
|
||||
'license' => input('license', ''),
|
||||
];
|
||||
$info = AddonDevService::instance($name)->createSkeleton($meta);
|
||||
return $this->result->success($info, '插件骨架创建成功');
|
||||
} catch (\Exception $e) {
|
||||
return $this->result->error($e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 读取插件现有数据(供设计器回显)
|
||||
*/
|
||||
public function designRead()
|
||||
{
|
||||
if (!AddonDevService::enabled()) {
|
||||
return $this->result->error('开发模式未开启');
|
||||
}
|
||||
$name = input('addon', '');
|
||||
if (!$name) {
|
||||
return $this->result->error('缺少插件标识');
|
||||
}
|
||||
try {
|
||||
$data = AddonDevService::instance($name)->readAll();
|
||||
return $this->result->success($data);
|
||||
} catch (\Exception $e) {
|
||||
return $this->result->error($e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 保存插件配置(type: basic/config/menu/hooks/route)
|
||||
*/
|
||||
public function designSave()
|
||||
{
|
||||
if (!AddonDevService::enabled()) {
|
||||
return $this->result->error('开发模式未开启');
|
||||
}
|
||||
$name = input('addon', '');
|
||||
$type = input('type', '');
|
||||
if (!$name) {
|
||||
return $this->result->error('缺少插件标识');
|
||||
}
|
||||
try {
|
||||
$svc = AddonDevService::instance($name);
|
||||
switch ($type) {
|
||||
case 'basic':
|
||||
$svc->saveBasic(input('post.'));
|
||||
break;
|
||||
case 'config':
|
||||
$fields = json_decode(input('fields', '[]'), true) ?: [];
|
||||
$svc->saveConfig($fields);
|
||||
break;
|
||||
case 'menu':
|
||||
$menu = json_decode(input('menu', '[]'), true) ?: [];
|
||||
$svc->saveMenu($menu);
|
||||
break;
|
||||
case 'hooks':
|
||||
$events = json_decode(input('events', '[]'), true) ?: [];
|
||||
$middleware = json_decode(input('middleware', '[]'), true) ?: [];
|
||||
$services = json_decode(input('services', '[]'), true) ?: [];
|
||||
$svc->saveHooks($events, $middleware, $services);
|
||||
break;
|
||||
case 'route':
|
||||
$routes = json_decode(input('routes', '[]'), true) ?: [];
|
||||
$svc->saveRoute($routes);
|
||||
break;
|
||||
default:
|
||||
return $this->result->error('未知保存类型:' . $type);
|
||||
}
|
||||
return $this->result->success([], '保存成功');
|
||||
} catch (\Exception $e) {
|
||||
return $this->result->error($e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成源码文件(gtype: controller/model/event/listener/middleware/service/subscribe/validate/command)
|
||||
*/
|
||||
public function designGenerate()
|
||||
{
|
||||
if (!AddonDevService::enabled()) {
|
||||
return $this->result->error('开发模式未开启');
|
||||
}
|
||||
$name = input('addon', '');
|
||||
$type = input('gtype', '');
|
||||
if (!$name || !$type) {
|
||||
return $this->result->error('缺少参数');
|
||||
}
|
||||
try {
|
||||
$opts = json_decode(input('opts', '[]'), true) ?: [];
|
||||
$file = AddonDevService::instance($name)->generate($type, $opts);
|
||||
$rel = ltrim(str_replace(ADDON_PATH . $name . DIRECTORY_SEPARATOR, '', $file), DIRECTORY_SEPARATOR);
|
||||
return $this->result->success(['file' => $rel], '生成成功:' . $rel);
|
||||
} catch (\Exception $e) {
|
||||
return $this->result->error($e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 开发模式安装(原地建表/注入菜单/启用,免打包,实时生效)
|
||||
*/
|
||||
public function designInstall()
|
||||
{
|
||||
if (!AddonDevService::enabled()) {
|
||||
return $this->result->error('开发模式未开启');
|
||||
}
|
||||
$name = input('addon', '');
|
||||
if (!$name) {
|
||||
return $this->result->error('缺少插件标识');
|
||||
}
|
||||
try {
|
||||
$info = AddonDevService::instance($name)->developInstall();
|
||||
return $this->result->success($info, '开发模式安装成功,菜单已注入并启用');
|
||||
} catch (\Exception $e) {
|
||||
return $this->result->error($e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除插件目录(开发调试用)
|
||||
*/
|
||||
public function designRemove()
|
||||
{
|
||||
if (!AddonDevService::enabled()) {
|
||||
return $this->result->error('开发模式未开启');
|
||||
}
|
||||
$name = input('addon', '');
|
||||
if (!$name) {
|
||||
return $this->result->error('缺少插件标识');
|
||||
}
|
||||
try {
|
||||
AddonDevService::instance($name)->remove();
|
||||
return $this->result->success([], '插件目录已删除');
|
||||
} catch (\Exception $e) {
|
||||
return $this->result->error($e->getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,263 @@
|
||||
<?php
|
||||
/*
|
||||
* @Author: YwxApp <ywx@ywxapp.cn>
|
||||
* @Date: 2026-04-29 02:32:33
|
||||
* @LastEditors: YwxApp <ywx@ywxapp.cn>
|
||||
* @LastEditTime: 2026-07-21 22:26:23
|
||||
* @Description:
|
||||
* @FilePath: \ywxapp_dev\app\backend\controller\Admin.php
|
||||
* @CustomString: Copyright (c) 2026 YwxApp
|
||||
*/
|
||||
|
||||
declare (strict_types = 1);
|
||||
|
||||
namespace app\backend\controller;
|
||||
|
||||
use app\backend\validate\Admin as AdminValidate;
|
||||
use think\db\exception\DbException;
|
||||
use think\exception\ValidateException;
|
||||
use think\facade\Db;
|
||||
use think\facade\View;
|
||||
use think\Request;
|
||||
use ywxapp\model\BackendAdmin as AdminsModel;
|
||||
use ywxapp\model\BackendRole as RoleModel;
|
||||
use ywxapp\controller\BackendBase;
|
||||
|
||||
/**
|
||||
* Backend 类
|
||||
*
|
||||
* @author ywxapp <admin@ywxapp.cn>
|
||||
*/
|
||||
class Admin extends BackendBase
|
||||
{
|
||||
/**
|
||||
* Summary of needLogin
|
||||
* @var array
|
||||
*/
|
||||
protected $noNeedLogin = [];
|
||||
|
||||
/**
|
||||
* Summary of needRight
|
||||
* @var array
|
||||
*/
|
||||
protected $noNeedVerify = [];
|
||||
|
||||
/**
|
||||
* 控制器初始化 initialize
|
||||
* @return void
|
||||
*/
|
||||
protected function initialize()
|
||||
{}
|
||||
|
||||
/**
|
||||
* 获取用户列表
|
||||
*/
|
||||
public function index()
|
||||
{
|
||||
$page = $this->request->param('page/d', 1);
|
||||
$limit = $this->request->param('limit/d', 15);
|
||||
if ($this->request->isAjax()) {
|
||||
$admins = AdminsModel::with(['roles' =>
|
||||
function($query) {
|
||||
$query->field('id,name,status');
|
||||
}])
|
||||
->field('id,account,nickname,email,mobile,status,create_at,update_at')
|
||||
->page($page, $limit)
|
||||
->select();
|
||||
$this->result->success($admins);
|
||||
}
|
||||
View::assign('title', '登录');
|
||||
return View::fetch();
|
||||
}
|
||||
|
||||
/**
|
||||
* 数据创建
|
||||
*
|
||||
* @param Request $request
|
||||
* @return \think\Response
|
||||
*/
|
||||
public function create()
|
||||
{
|
||||
if ($this->request->isAjax()) {
|
||||
$roles = RoleModel::field('id,name,status')->where('status', 1)->select();
|
||||
$this->result->success(['roles' => $roles]);
|
||||
}
|
||||
View::assign('title', '登录');
|
||||
return View::fetch('admin/index');
|
||||
}
|
||||
|
||||
/**
|
||||
* 数据保存
|
||||
*
|
||||
* @param Request $request
|
||||
* @return \think\Response
|
||||
*/
|
||||
public function save()
|
||||
{
|
||||
$params = $this->request->only(['account', 'nickname', 'password', 'confirmpass', 'mobile', 'email', 'role_ids', 'status'], 'post');
|
||||
$roleIds = $this->request->param('role_ids/a', []);
|
||||
try {
|
||||
validate(AdminValidate::class)->check($params);
|
||||
Db::transaction(function () use ($params, $roleIds) {
|
||||
$data = AdminsModel::create($params);
|
||||
$data->roles()->saveAll($roleIds);
|
||||
});
|
||||
$this->result->success();
|
||||
} catch (ValidateException $e) {
|
||||
$this->result->error($e->getMessage());
|
||||
} catch (DbException $e) {
|
||||
$this->result->error($e->getMessage(), 2);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 数据编辑
|
||||
*
|
||||
* @param Request $request
|
||||
* @param int|null $ids
|
||||
* @return \think\Response
|
||||
*/
|
||||
public function edit( $id = null)
|
||||
{
|
||||
if ($this->request->isAjax()) {
|
||||
$data = AdminsModel::with(['roles' =>
|
||||
function($query) {
|
||||
$query->field('id,name,status');
|
||||
}])
|
||||
->field('id,account,nickname,email,mobile,status')
|
||||
->where('id', $id)
|
||||
->find();
|
||||
$roles = RoleModel::field('id,name,status')->where('status', 1)->select();
|
||||
$this->result->success(['info' => $data, 'roles' => $roles]);
|
||||
}
|
||||
View::assign('title', '登录');
|
||||
return View::fetch('admin/index');
|
||||
}
|
||||
|
||||
/**
|
||||
* 数据更新
|
||||
*
|
||||
* @param Request $request
|
||||
* @param int|null $ids
|
||||
* @return \think\Response
|
||||
*/
|
||||
public function update()
|
||||
{
|
||||
$data = $this->request->only(['id', 'account', 'nickname', 'password', 'confirmpass', 'mobile', 'email', 'status','role_ids'], 'put');
|
||||
$roleIds = $this->request->put('role_ids/a', []);
|
||||
$info = AdminsModel::find($data['id']);
|
||||
if (! $info) {
|
||||
$this->result->error('用户不存在',404);
|
||||
}
|
||||
// 超级管理员账号(id=config superAdmin)必须始终保留超级管理员角色,禁止被降权
|
||||
if ((int)$data['id'] === (int)config('ywxapp.superAdmin', 1)
|
||||
&& !in_array((int)config('ywxapp.superAdmin', 1), array_map('intval', $roleIds), true)) {
|
||||
$this->result->error('超级管理员必须保留超级管理员角色', 403);
|
||||
}
|
||||
$info->save($data);
|
||||
$info->roles()->sync($roleIds);
|
||||
$this->result->success($info,"用户更新成功");
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取详情
|
||||
*/
|
||||
public function read()
|
||||
{
|
||||
$info = AdminsModel::with(['roles', 'permissions'])
|
||||
->find($id);
|
||||
if (! $info) {
|
||||
$this->result->error('用户不存在',404);
|
||||
}
|
||||
$this->result->success($info,"用户更新成功");
|
||||
}
|
||||
|
||||
/**
|
||||
* 数据回收站
|
||||
*
|
||||
* @param Request $request
|
||||
* @return \think\Response
|
||||
*/
|
||||
public function recyclebin()
|
||||
{
|
||||
$page = $this->request->param('page', 1);
|
||||
$size = $this->request->param('size', 15);
|
||||
if ($this->request->isAjax()) {
|
||||
$admins = AdminsModel::onlyTrashed()->with(['roles'])
|
||||
->paginate([
|
||||
'page' => $page,
|
||||
'list_rows' => $size,
|
||||
]);
|
||||
$this->result->success($admins->items());
|
||||
}
|
||||
View::assign('title', '回收站');
|
||||
return View::fetch('admin/index');
|
||||
}
|
||||
|
||||
/**
|
||||
* 数据删除
|
||||
*/
|
||||
public function delete()
|
||||
{
|
||||
if ($this->request->isAjax() && $this->request->isDelete()) {
|
||||
$ids = $this->request->param('ids', '');
|
||||
$force = $this->request->param('force', false);
|
||||
if (empty($ids)) {
|
||||
$this->result->error('请选择要删除的数据');
|
||||
}
|
||||
$idsArray = explode(',', $ids);
|
||||
// 禁止删除超级管理员账号
|
||||
if (in_array((int)config('ywxapp.superAdmin', 1), array_map('intval', $idsArray), true)) {
|
||||
$this->result->error('超级管理员账号不可删除', 403);
|
||||
}
|
||||
try {
|
||||
Db::transaction(function () use ($idsArray, $force) {
|
||||
if ($force) {
|
||||
AdminsModel::onlyTrashed()->whereIn('id', $idsArray)->select()->each(function ($item) {
|
||||
$item->roles()->detach();
|
||||
$item->force()->delete();
|
||||
});
|
||||
} else {
|
||||
AdminsModel::destroy($idsArray);
|
||||
}
|
||||
});
|
||||
$this->result->success();
|
||||
} catch (\think\exception\HttpResponseException $e) {
|
||||
throw $e; // 不要 return,不要吞掉!
|
||||
} catch (\Exception $e) {
|
||||
\think\facade\Log::error('批量删除管理员失败', [
|
||||
'exception' => $e->__toString(),
|
||||
'admin_ids' => $idsArray,
|
||||
]);
|
||||
$this->result->error('删除失败: ' . ($e->getMessage() ?: $e->__toString()), 500);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public function restore( $ids = '')
|
||||
{
|
||||
if ($this->request->isAjax() && $this->request->isPost()) {
|
||||
$ids = $this->request->param('ids', '');
|
||||
if (empty($ids)) {
|
||||
$this->result->error('请选择要恢复的数据');
|
||||
}
|
||||
$idsArray = explode(',', $ids);
|
||||
Db::startTrans();
|
||||
try {
|
||||
AdminsModel::withTrashed()
|
||||
->where('id', 'in', $idsArray)
|
||||
->select()
|
||||
->each(function ($item) {
|
||||
$item->restore();
|
||||
});
|
||||
Db::commit();
|
||||
$this->result->success('恢复成功');
|
||||
} catch (DbException $e) {
|
||||
Db::rollback();
|
||||
$this->result->error('恢复失败: ' . $e->getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,117 @@
|
||||
<?php
|
||||
/*
|
||||
* @Author: YwxApp <ywx@ywxapp.cn>
|
||||
* @Date: 2026-04-29 02:32:33
|
||||
* @LastEditors: YwxApp <ywx@ywxapp.cn>
|
||||
* @LastEditTime: 2026-07-21 22:27:10
|
||||
* @Description:
|
||||
* @FilePath: \ywxapp_dev\app\backend\controller\Ajax.php
|
||||
* @CustomString: Copyright (c) 2026 YwxApp
|
||||
*/
|
||||
|
||||
declare (strict_types = 1);
|
||||
|
||||
namespace app\backend\controller;
|
||||
|
||||
use think\facade\Request;
|
||||
use think\facade\Filesystem;
|
||||
|
||||
use think\captcha\facade\Captcha;
|
||||
/**
|
||||
* Ajax 类
|
||||
*
|
||||
* @author ywxapp <admin@ywxapp.cn>
|
||||
*/
|
||||
class Ajax
|
||||
{
|
||||
/**
|
||||
* Summary of needLogin
|
||||
* @var array
|
||||
*/
|
||||
protected $noNeedLogin = ['verify', 'captcha'];
|
||||
|
||||
/**
|
||||
* Summary of needRight
|
||||
* @var array
|
||||
*/
|
||||
protected $noNeedVerify = ['*'];
|
||||
|
||||
/**
|
||||
* 控制器初始化 initialize
|
||||
* @return void
|
||||
*/
|
||||
protected function initialize()
|
||||
{}
|
||||
|
||||
/**
|
||||
* 验证码
|
||||
*/
|
||||
public function verify()
|
||||
{
|
||||
ob_clean();
|
||||
return Captcha::create();
|
||||
}
|
||||
|
||||
|
||||
public function captcha()
|
||||
{
|
||||
ob_clean();
|
||||
return Captcha::create();
|
||||
}
|
||||
|
||||
// 专供 UEditor 上传使用
|
||||
|
||||
public function ueditor()
|
||||
{
|
||||
$action = Request::param('action');
|
||||
|
||||
if ($action == 'config') {
|
||||
// 返回配置文件(JSON)
|
||||
$configStr = file_get_contents(public_path() . 'assets/plugin/ueditor/config.json');
|
||||
$config = json_decode($configStr, true);
|
||||
|
||||
// // 修改上传路径为 ThinkPHP 存储目录
|
||||
// $config['imagePathFormat'] = '/storage/ueditor/images/{yyyy}{mm}{dd}/{filename}_{time}';
|
||||
// $config['scrawlPathFormat'] = '/storage/ueditor/images/{yyyy}{mm}{dd}/{filename}_{time}';
|
||||
// $config['snapscreenPathFormat'] = '/storage/ueditor/images/{yyyy}{mm}{dd}/{filename}_{time}';
|
||||
// $config['catcherPathFormat'] = '/storage/ueditor/images/{yyyy}{mm}{dd}/{filename}_{time}';
|
||||
// $config['videoPathFormat'] = '/storage/ueditor/video/{yyyy}{mm}{dd}/{filename}_{time}';
|
||||
// $config['filePathFormat'] = '/storage/ueditor/files/{yyyy}{mm}{dd}/{filename}_{time}';
|
||||
|
||||
return json($config);
|
||||
}
|
||||
|
||||
// 图片上传处理
|
||||
if ($action == 'image' ||$action == 'uploadimage' || $action == 'uploadscrawl' || $action == 'uploadvideo' || $action == 'uploadfile') {
|
||||
return $this->handleUpload($action);
|
||||
}
|
||||
|
||||
return json(['state' => '请求类型错误']);
|
||||
}
|
||||
|
||||
|
||||
protected function handleUpload($action)
|
||||
{
|
||||
$file = request()->file('file') ?: null;
|
||||
|
||||
if (!$file) {
|
||||
return json(['state' => '没有文件上传']);
|
||||
}
|
||||
|
||||
try {
|
||||
$savename = Filesystem::disk('local')->putFile('ueditor', $file);
|
||||
$url = '/storage/' . str_replace('\\', '/', $savename); // Windows兼容
|
||||
|
||||
return json([
|
||||
'state' => 'SUCCESS',
|
||||
'url' => $url,
|
||||
'title' => basename($url),
|
||||
'original' => $file->getOriginalName(),
|
||||
]);
|
||||
} catch (\Exception $e) {
|
||||
return json(['state' => '上传失败:' . $e->getMessage()]);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -0,0 +1,205 @@
|
||||
<?php
|
||||
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\Card as CardModel;
|
||||
use app\backend\validate\Card as CardValidate;
|
||||
|
||||
/**
|
||||
* 充值卡密管理
|
||||
*/
|
||||
class Card extends BackendBase
|
||||
{
|
||||
protected $noNeedLogin = [];
|
||||
protected $noNeedVerify = [];
|
||||
|
||||
protected function initialize()
|
||||
{
|
||||
// 运行时自愈 card 表结构(uid / batch_no 等扩展列)
|
||||
\ywxapp\model\Card::ensureSchema();
|
||||
$this->model = new CardModel();
|
||||
}
|
||||
|
||||
public function index()
|
||||
{
|
||||
$page = $this->request->param('page/d', 1);
|
||||
$limit = $this->request->param('limit/d', 15);
|
||||
$cardno = $this->request->param('cardno', '');
|
||||
$status = $this->request->param('status', '');
|
||||
|
||||
if ($this->request->isAjax()) {
|
||||
$query = $this->model->newQuery();
|
||||
if ($cardno !== '') {
|
||||
$query->where('cardno', 'like', '%' . $cardno . '%');
|
||||
}
|
||||
if ($status !== '') {
|
||||
$query->where('status', (int)$status);
|
||||
}
|
||||
$list = $query->order('sort', 'desc')
|
||||
->paginate(['page' => $page, 'list_rows' => $limit]);
|
||||
$items = $list->items();
|
||||
// 关联兑换会员昵称
|
||||
$uids = array_filter(array_column($items, 'uid'));
|
||||
$users = [];
|
||||
if (!empty($uids)) {
|
||||
$users = Db::name('member')->whereIn('uid', array_unique($uids))
|
||||
->column('nickname,username', 'uid');
|
||||
}
|
||||
foreach ($items as &$row) {
|
||||
$row['user_name'] = ($row['uid'] > 0 && isset($users[$row['uid']]))
|
||||
? ($users[$row['uid']]['nickname'] ?: $users[$row['uid']]['username'])
|
||||
: '';
|
||||
$row['use_time_text'] = $row['use_time'] > 0 ? date('Y-m-d H:i:s', $row['use_time']) : '';
|
||||
}
|
||||
unset($row);
|
||||
$this->result->setCount($list->total())->success($items);
|
||||
}
|
||||
return $this->fetch();
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量生成卡密
|
||||
*/
|
||||
public function generate()
|
||||
{
|
||||
if (!($this->request->isAjax() && $this->request->isPost())) {
|
||||
$this->result->error('请求方式错误', 405);
|
||||
}
|
||||
$count = (int)$this->request->param('count', 0);
|
||||
$amount = (float)$this->request->param('amount', 0);
|
||||
$prefix = (string)$this->request->param('prefix', '');
|
||||
if ($count < 1 || $count > 200) {
|
||||
$this->result->error('生成数量需在 1-200 之间');
|
||||
}
|
||||
if ($amount <= 0) {
|
||||
$this->result->error('面值必须大于 0');
|
||||
}
|
||||
try {
|
||||
$list = CardModel::generateBatch($count, $amount, $prefix);
|
||||
$this->result->success(['list' => $list], '成功生成 ' . count($list) . ' 张卡密');
|
||||
} catch (\Throwable $e) {
|
||||
$this->result->error('生成失败:' . $e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
public function save()
|
||||
{
|
||||
if (! $this->request->isPost()) {
|
||||
$this->result->error('请求方式错误', 405);
|
||||
}
|
||||
$params = $this->request->only(['cardno', 'password', 'amount', 'status', 'use_time', 'sort'], 'post');
|
||||
try {
|
||||
validate(CardValidate::class)->check($params);
|
||||
$this->model->create($params);
|
||||
$this->result->success('', '添加成功');
|
||||
} catch (ValidateException $e) {
|
||||
$this->result->error($e->getMessage());
|
||||
} catch (DbException $e) {
|
||||
$this->result->error('添加失败: ' . $e->getMessage(), 2);
|
||||
}
|
||||
}
|
||||
|
||||
public function edit($id = null)
|
||||
{
|
||||
$id = $id ?: $this->request->param('id');
|
||||
$info = $this->model->find($id);
|
||||
if (! $info) {
|
||||
$this->result->error('记录不存在', 404);
|
||||
}
|
||||
$this->result->success(['info' => $info]);
|
||||
}
|
||||
|
||||
public function update()
|
||||
{
|
||||
if (! ($this->request->isAjax() && $this->request->isPut())) {
|
||||
$this->result->error('请求方式错误', 405);
|
||||
}
|
||||
$params = $this->request->param();
|
||||
$id = $params['id'] ?? null;
|
||||
$info = $this->model->find($id);
|
||||
if (! $info) {
|
||||
$this->result->error('记录不存在', 404);
|
||||
}
|
||||
$statusOnly = isset($params['status'])
|
||||
&& count(array_diff(array_keys($params), ['id', 'status'])) === 0;
|
||||
if (! $statusOnly) {
|
||||
try {
|
||||
validate(CardValidate::class)->check($params);
|
||||
} catch (ValidateException $e) {
|
||||
$this->result->error($e->getMessage());
|
||||
}
|
||||
}
|
||||
$info->save($params);
|
||||
$this->result->success($info, '更新成功');
|
||||
}
|
||||
|
||||
public function recyclebin()
|
||||
{
|
||||
$page = $this->request->param('page/d', 1);
|
||||
$limit = $this->request->param('limit/d', 15);
|
||||
if ($this->request->isAjax()) {
|
||||
$list = $this->model->onlyTrashed()
|
||||
->order('sort', 'desc')
|
||||
->paginate(['page' => $page, 'list_rows' => $limit]);
|
||||
$this->result->setCount($list->total())->success($list->items());
|
||||
}
|
||||
return $this->fetch('card/index');
|
||||
}
|
||||
|
||||
public function delete()
|
||||
{
|
||||
if ($this->request->isAjax() && $this->request->isDelete()) {
|
||||
$ids = $this->request->param('ids', '');
|
||||
$force = $this->request->param('force', false);
|
||||
if (empty($ids)) {
|
||||
$this->result->error('请选择要删除的数据');
|
||||
}
|
||||
$idsArray = array_filter(explode(',', $ids), 'is_numeric');
|
||||
if (empty($idsArray)) {
|
||||
$this->result->error('参数错误');
|
||||
}
|
||||
try {
|
||||
Db::transaction(function () use ($idsArray, $force) {
|
||||
if ($force) {
|
||||
$this->model->onlyTrashed()->whereIn('id', $idsArray)->select()
|
||||
->each(function ($item) { $item->force()->delete(); });
|
||||
} else {
|
||||
$this->model->destroy($idsArray);
|
||||
}
|
||||
});
|
||||
$this->result->success();
|
||||
} catch (\think\exception\HttpResponseException $e) {
|
||||
throw $e;
|
||||
} catch (\Exception $e) {
|
||||
$this->result->error('删除失败: ' . ($e->getMessage() ?: $e->__toString()), 500);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public function restore($ids = '')
|
||||
{
|
||||
if ($this->request->isAjax() && $this->request->isPost()) {
|
||||
$ids = $this->request->param('ids', '');
|
||||
if (empty($ids)) {
|
||||
$this->result->error('请选择要恢复的数据');
|
||||
}
|
||||
$idsArray = array_filter(explode(',', $ids), 'is_numeric');
|
||||
Db::startTrans();
|
||||
try {
|
||||
$this->model->withTrashed()->where('id', 'in', $idsArray)->select()
|
||||
->each(function ($item) { $item->restore(); });
|
||||
Db::commit();
|
||||
$this->result->success('恢复成功');
|
||||
} catch (DbException $e) {
|
||||
Db::rollback();
|
||||
$this->result->error('恢复失败: ' . $e->getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,123 @@
|
||||
<?php
|
||||
/*
|
||||
* @Author: YwxApp <ywx@ywxapp.cn>
|
||||
* @Date: 2026-04-29 02:32:33
|
||||
* @LastEditors: YwxApp <ywx@ywxapp.cn>
|
||||
* @LastEditTime: 2026-08-03 13:32:15
|
||||
* @Description:
|
||||
* @FilePath: \ywxapp_dev\app\backend\controller\Configure.php
|
||||
* @CustomString: Copyright (c) 2026 YwxApp
|
||||
*/
|
||||
|
||||
declare(strict_types=1);
|
||||
namespace app\backend\controller;
|
||||
|
||||
use think\facade\View;
|
||||
use think\Request;
|
||||
use ywxapp\model\Configure as ConfigureModel;
|
||||
use think\facade\Db;
|
||||
use think\facade\Cache;
|
||||
use ywxapp\controller\BackendBase;
|
||||
|
||||
/**
|
||||
* Configure 类
|
||||
*
|
||||
* @author ywxapp <admin@ywxapp.cn>
|
||||
*/
|
||||
class Configure extends BackendBase
|
||||
{
|
||||
/**
|
||||
* Summary of needLogin
|
||||
* @var array
|
||||
*/
|
||||
protected $noNeedLogin = [];
|
||||
|
||||
/**
|
||||
* Summary of needRight
|
||||
* @var array
|
||||
*/
|
||||
protected $noNeedVerify = [];
|
||||
|
||||
/**
|
||||
* 控制器初始化 initialize
|
||||
* @return void
|
||||
*/
|
||||
protected function initialize()
|
||||
{
|
||||
|
||||
$this->model = new ConfigureModel;
|
||||
}
|
||||
|
||||
/**
|
||||
* 显示资源列表
|
||||
*
|
||||
* @param \think\Request $request
|
||||
* @return \think\Response
|
||||
*/
|
||||
public function index()
|
||||
{
|
||||
|
||||
$page = $this->request->param('page', 1);
|
||||
$size = $this->request->param('size', 15);
|
||||
if ($this->request->isAjax()) {
|
||||
$data = ConfigureModel::order('id')->select();
|
||||
$this->result->success($data);
|
||||
}
|
||||
View::assign('title', '登录');
|
||||
return View::fetch();
|
||||
}
|
||||
|
||||
/**
|
||||
* 保存新建的资源
|
||||
*
|
||||
* @param \think\Request $request
|
||||
* @return \think\Response
|
||||
*/
|
||||
public function update()
|
||||
{
|
||||
if (!$this->request->isPost()) {
|
||||
return $this->result->error('请求方式错误');
|
||||
}
|
||||
$postData = $this->request->post();
|
||||
$allConfigs = ConfigureModel::column('name,type,rule,value', 'name');
|
||||
Db::startTrans();
|
||||
try {
|
||||
foreach ($postData as $name => $value) {
|
||||
if (!isset($allConfigs[$name])) {
|
||||
continue;
|
||||
}
|
||||
$config = $allConfigs[$name];
|
||||
if (!empty($config['rule'])) {
|
||||
$validate = validate([
|
||||
$name => $config['rule']
|
||||
]);
|
||||
//if (!$validate->check([$name => $value])) {
|
||||
// throw new \Exception("配置项 [{$name}] 验证失败:" . $validate->getError());
|
||||
// }
|
||||
}
|
||||
// 特殊处理:复选框数组转字符串
|
||||
if (is_array($value)) {
|
||||
$value = implode(',', $value);
|
||||
}
|
||||
$exists = ConfigureModel::where('name', $name)->find();
|
||||
if ($exists) {
|
||||
$exists->save(['value' => $value ?? ""]);
|
||||
} else {
|
||||
// 如果没有记录,创建新记录
|
||||
ConfigureModel::create([
|
||||
'name' => $name,
|
||||
'value' => $value,
|
||||
'group' => $postData['group'] ?? 'default',
|
||||
'type' => $config['type'] ?? 'string'
|
||||
]);
|
||||
}
|
||||
}
|
||||
// Cache::delete('system_config_all');
|
||||
Db::commit();
|
||||
$this->result->success('配置保存成功');
|
||||
} catch (\Exception $e) {
|
||||
Db::rollback();
|
||||
$this->result->error('保存失败:' . $e->getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,123 @@
|
||||
<?php
|
||||
/*
|
||||
* @Author: YwxApp <ywx@ywxapp.cn>
|
||||
* @Date: 2026-04-29 02:32:33
|
||||
* @LastEditors: YwxApp <ywx@ywxapp.cn>
|
||||
* @LastEditTime: 2026-07-21 22:29:51
|
||||
* @Description:
|
||||
* @FilePath: \ywxapp_dev\app\backend\controller\Console.php
|
||||
* @CustomString: Copyright (c) 2026 YwxApp
|
||||
*/
|
||||
|
||||
declare (strict_types = 1);
|
||||
namespace app\backend\controller;
|
||||
|
||||
use think\Request;
|
||||
use think\facade\View;
|
||||
use think\facade\Db;
|
||||
use ywxapp\controller\BackendBase;
|
||||
|
||||
/**
|
||||
* Console 类
|
||||
*
|
||||
* @author ywxapp <admin@ywxapp.cn>
|
||||
*/
|
||||
class Console extends BackendBase
|
||||
{
|
||||
/**
|
||||
* Summary of needLogin
|
||||
* @var array
|
||||
*/
|
||||
protected $noNeedLogin = [];
|
||||
|
||||
/**
|
||||
* Summary of needRight
|
||||
* @var array
|
||||
*/
|
||||
protected $noNeedVerify = [];
|
||||
|
||||
/**
|
||||
* 控制器初始化 initialize
|
||||
* @return void
|
||||
*/
|
||||
protected function initialize()
|
||||
{}
|
||||
|
||||
/**
|
||||
* 显示资源列表
|
||||
*
|
||||
* @param \think\Request $request
|
||||
* @return \think\Response
|
||||
*/
|
||||
public function index()
|
||||
{
|
||||
View::assign('title', '控制台');
|
||||
return View::fetch();
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 热门统计(真实数据)
|
||||
*
|
||||
* @return \think\Response
|
||||
*/
|
||||
public function hotsearch()
|
||||
{
|
||||
$admin = Db::name('backend_admin')->count();
|
||||
$user = Db::name('member')->count();
|
||||
$article = Db::name('articles_article')->count();
|
||||
$links = Db::name('links')->count();
|
||||
$addon = Db::name('addon')->count();
|
||||
$data = [
|
||||
['keywords' => '管理员', 'frequency' => $admin, 'userNums' => $admin],
|
||||
['keywords' => '会员', 'frequency' => $user, 'userNums' => $user],
|
||||
['keywords' => '文章', 'frequency' => $article, 'userNums' => $article],
|
||||
['keywords' => '友链', 'frequency' => $links, 'userNums' => $links],
|
||||
['keywords' => '插件', 'frequency' => $addon, 'userNums' => $addon],
|
||||
];
|
||||
$this->result->success($data);
|
||||
}
|
||||
|
||||
/**
|
||||
* 热门内容(真实数据:最新文章)
|
||||
*
|
||||
* @return \think\Response
|
||||
*/
|
||||
public function hotTopic()
|
||||
{
|
||||
$list = Db::name('articles_article')
|
||||
->field('id, title, author, cid, create_at')
|
||||
->order('id', 'desc')
|
||||
->limit(10)
|
||||
->select()
|
||||
->toArray();
|
||||
$data = array_map(function ($item) {
|
||||
return [
|
||||
'id' => $item['id'],
|
||||
'title' => $item['title'],
|
||||
'username' => $item['author'] ?? '',
|
||||
'channel' => $item['cid'] ?? '',
|
||||
'href' => '',
|
||||
'crt' => $item['create_at'] ?? 0,
|
||||
];
|
||||
}, $list);
|
||||
$this->result->success($data);
|
||||
}
|
||||
|
||||
/**
|
||||
* 系统进度 / 环境信息(真实数据)
|
||||
*
|
||||
* @return \think\Response
|
||||
*/
|
||||
public function prograss()
|
||||
{
|
||||
$data = [
|
||||
['prograss' => 'PHP 版本', 'time' => PHP_VERSION, 'complete' => '已完成'],
|
||||
['prograss' => 'ThinkPHP', 'time' => \think\facade\App::version(), 'complete' => '已完成'],
|
||||
['prograss' => '安装状态', 'time' => is_file(root_path() . 'install.lock') ? '已安装' : '未安装', 'complete' => '已完成'],
|
||||
['prograss' => '运行环境', 'time' => php_sapi_name(), 'complete' => '进行中'],
|
||||
];
|
||||
$this->result->success($data);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,312 @@
|
||||
<?php
|
||||
/*
|
||||
* @Author: YwxApp <ywx@ywxapp.cn>
|
||||
* @Date: 2026-04-29 02:32:33
|
||||
* @LastEditors: YwxApp <ywx@ywxapp.cn>
|
||||
* @LastEditTime: 2026-07-21 22:31:51
|
||||
* @Description:
|
||||
* @FilePath: \ywxapp_dev\app\backend\controller\Group.php
|
||||
* @CustomString: Copyright (c) 2026 YwxApp
|
||||
*/
|
||||
|
||||
namespace app\backend\controller;
|
||||
|
||||
use think\facade\Db;
|
||||
use think\facade\View;
|
||||
use think\Request;
|
||||
use ywxapp\model\MemberGroup as GroupModel;
|
||||
use ywxapp\model\MemberGroupRule as GroupRuleModel;
|
||||
use ywxapp\model\MemberRule as RuleModel;
|
||||
use ywxapp\controller\BackendBase;
|
||||
|
||||
/**
|
||||
* Group 类
|
||||
*
|
||||
* @author ywxapp <admin@ywxapp.cn>
|
||||
*/
|
||||
class Group extends BackendBase
|
||||
{
|
||||
/**
|
||||
* Summary of needLogin
|
||||
* @var array
|
||||
*/
|
||||
protected $noNeedLogin = [];
|
||||
|
||||
/**
|
||||
* Summary of needRight
|
||||
* @var array
|
||||
*/
|
||||
protected $noNeedVerify = [];
|
||||
|
||||
/**
|
||||
* 控制器初始化 initialize
|
||||
* @return void
|
||||
*/
|
||||
protected function initialize()
|
||||
{}
|
||||
|
||||
/**
|
||||
* 显示资源列表
|
||||
*
|
||||
* @param \think\Request $request
|
||||
* @return \think\Response
|
||||
*/
|
||||
public function index()
|
||||
{
|
||||
$page = $this->request->param('page', 1);
|
||||
$size = $this->request->param('size', 15);
|
||||
if ($this->request->isAjax()) {
|
||||
$data = GroupModel::paginate([
|
||||
'page' => $page,
|
||||
'list_rows' => $size,
|
||||
]);
|
||||
$this->result->success($data->items());
|
||||
}
|
||||
View::assign('title', '登录');
|
||||
return View::fetch();
|
||||
}
|
||||
|
||||
/**
|
||||
* 数据创建
|
||||
*
|
||||
* @param Request $request
|
||||
* @return \think\Response
|
||||
*/
|
||||
public function create()
|
||||
{
|
||||
if ($this->request->isAjax()) {
|
||||
$roles = GroupModel::where('status', 1)->select();
|
||||
$this->result->success(['roles' => $roles]);
|
||||
}
|
||||
View::assign('title', '登录');
|
||||
return View::fetch('group/index');
|
||||
}
|
||||
|
||||
/**
|
||||
* 数据保存
|
||||
*
|
||||
* @param Request $request
|
||||
* @return \think\Response
|
||||
*/
|
||||
public function save()
|
||||
{
|
||||
$params = $this->request->only(['title', 'name', 'status', 'description'], 'post');
|
||||
try {
|
||||
validate(RoleValidate::class)->check($params);
|
||||
Db::transaction(function () use ($params) {
|
||||
$data = GroupModel::create($params);
|
||||
});
|
||||
$this->result->success();
|
||||
} catch (ValidateException $e) {
|
||||
$this->result->error($e->getMessage());
|
||||
} catch (DbException $e) {
|
||||
$this->result->error($e->getMessage(), 2);
|
||||
}
|
||||
}
|
||||
/**
|
||||
* 数据编辑
|
||||
*
|
||||
* @param Request $request
|
||||
* @param int|null $ids
|
||||
* @return \think\Response
|
||||
*/
|
||||
public function edit($id = null)
|
||||
{
|
||||
if ($this->request->isAjax()) {
|
||||
$data = GroupModel::where('id', $id)->find();
|
||||
$this->result->success(['info' => $data]);
|
||||
}
|
||||
View::assign('title', '登录');
|
||||
return View::fetch('group/index');
|
||||
}
|
||||
|
||||
/**
|
||||
* 数据更新
|
||||
*
|
||||
* @param Request $request
|
||||
* @param int|null $ids
|
||||
* @return \think\Response
|
||||
*/
|
||||
public function update()
|
||||
{
|
||||
$params = $this->request->only(['id', 'title', 'name', 'status', 'description'], 'put');
|
||||
$info = GroupModel::find($params['id']);
|
||||
if (! $info) {
|
||||
$this->result->error('角色不存在', 404);
|
||||
}
|
||||
// 禁止编辑超级管理员组
|
||||
if ($params['id'] == config('ywxapp.superAdmin', 1) && $info->name === 'superadmin') {
|
||||
$this->result->error('超级管理员组不可编辑', 403);
|
||||
}
|
||||
$info->save($params);
|
||||
$this->result->success($info, "角色更新成功");
|
||||
}
|
||||
/**
|
||||
* 获取详情
|
||||
*/
|
||||
public function read()
|
||||
{
|
||||
$info = RoleModel::with(['roles', 'permissions'])
|
||||
->find($id);
|
||||
if (! $info) {
|
||||
$this->result->error('角色不存在', 404);
|
||||
}
|
||||
$this->result->success($info, "角色更新成功");
|
||||
}
|
||||
|
||||
/**
|
||||
* 数据回收站
|
||||
*
|
||||
* @param Request $request
|
||||
* @return \think\Response
|
||||
*/
|
||||
public function recyclebin()
|
||||
{
|
||||
$page = $this->request->param('page', 1);
|
||||
$size = $this->request->param('size', 15);
|
||||
if ($this->request->isAjax()) {
|
||||
$roles = GroupModel::onlyTrashed()->with(['permissions'])
|
||||
->paginate([
|
||||
'page' => $page,
|
||||
'list_rows' => $size,
|
||||
]);
|
||||
$this->result->success($roles->items());
|
||||
}
|
||||
View::assign('title', '回收站');
|
||||
return View::fetch('group/index');
|
||||
}
|
||||
|
||||
/**
|
||||
* 数据删除
|
||||
*/
|
||||
public function delete()
|
||||
{
|
||||
if ($this->request->isAjax() && $this->request->isDelete()) {
|
||||
$ids = $this->request->param('ids', '');
|
||||
$force = $this->request->param('force', false);
|
||||
if (empty($ids)) {
|
||||
$this->result->error('请选择要删除的数据');
|
||||
}
|
||||
$idsArray = explode(',', $ids);
|
||||
// 禁止删除超级管理员组
|
||||
if (in_array((int)config('ywxapp.superAdmin', 1), array_map('intval', $idsArray), true)) {
|
||||
$this->result->error('超级管理员组不可删除', 403);
|
||||
}
|
||||
try {
|
||||
Db::transaction(function () use ($idsArray, $force) {
|
||||
if ($force) {
|
||||
GroupModel::onlyTrashed()->whereIn('id', $idsArray)->select()->each(function ($item) {
|
||||
$item->permissions()->detach();
|
||||
$item->force()->delete();
|
||||
});
|
||||
} else {
|
||||
GroupModel::destroy($idsArray);
|
||||
}
|
||||
});
|
||||
$this->result->success();
|
||||
} catch (\think\exception\HttpResponseException $e) {
|
||||
throw $e; // 不要 return,不要吞掉!
|
||||
} catch (\Exception $e) {
|
||||
\think\facade\Log::error('批量删除管理员失败', [
|
||||
'exception' => $e->__toString(),
|
||||
'admin_ids' => $idsArray,
|
||||
]);
|
||||
$this->result->error('删除失败: ' . ($e->getMessage() ?: $e->__toString()), 500);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 数据恢复
|
||||
*/
|
||||
public function restore($ids = '')
|
||||
{
|
||||
if ($this->request->isAjax() && $this->request->isPost()) {
|
||||
$ids = $this->request->param('ids', '');
|
||||
if (empty($ids)) {
|
||||
$this->result->error('请选择要恢复的数据');
|
||||
}
|
||||
$idsArray = explode(',', $ids);
|
||||
Db::startTrans();
|
||||
try {
|
||||
GroupModel::withTrashed()
|
||||
->where('id', 'in', $idsArray)
|
||||
->select()
|
||||
->each(function ($item) {
|
||||
$item->restore();
|
||||
});
|
||||
Db::commit();
|
||||
$this->result->success('恢复成功');
|
||||
} catch (DbException $e) {
|
||||
Db::rollback();
|
||||
$this->result->error('恢复失败: ' . $e->getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 权限分配
|
||||
*/
|
||||
public function permission()
|
||||
{
|
||||
$id = $this->request->param('id', 0);
|
||||
if ($this->request->isGet()) {
|
||||
// 1. 获取所有权限
|
||||
$permissions = RuleModel::where('status', 1)->order('sort', 'asc')->select()->toArray();
|
||||
// 2. 获取角色已拥有的权限ID
|
||||
$ownedPermissions = GroupRuleModel::where('gid', $id)->column('rid');
|
||||
// 超级管理员组:权限恒为 *(全部),前端展示为全部勾选且不可编辑
|
||||
$group = GroupModel::find($id);
|
||||
$isSuper = $group && ($group->name === 'superadmin' || $id == config('ywxapp.superAdmin', 1));
|
||||
if ($isSuper) {
|
||||
$ownedPermissions = array_column($permissions, 'id');
|
||||
}
|
||||
// 3. 构建带选中状态的树
|
||||
$treeData = $this->buildTreeWithChecked($permissions, $ownedPermissions);
|
||||
$this->result->success($treeData, $isSuper ? '超级管理员组拥有全部权限(*)' : '获取成功');
|
||||
}
|
||||
|
||||
if ($this->request->isAjax() && $this->request->isPut()) {
|
||||
// 禁止修改超级管理员组权限
|
||||
$group = GroupModel::find($id);
|
||||
if ($group && ($group->name === 'superadmin' || $id == config('ywxapp.superAdmin', 1))) {
|
||||
$this->result->error('超级管理员组权限不可修改', 403);
|
||||
}
|
||||
$permissionIds = $this->request->param('permissions/a', []);
|
||||
try {
|
||||
Db::startTrans();
|
||||
GroupRuleModel::where('gid', $id)->delete();
|
||||
foreach ($permissionIds as $permissionId) {
|
||||
GroupRuleModel::create([
|
||||
'gid' => $id,
|
||||
'rid' => $permissionId,
|
||||
]);
|
||||
}
|
||||
Db::commit();
|
||||
$this->result->success([], '权限分配成功');
|
||||
} catch (\Exception $e) {
|
||||
Db::rollback();
|
||||
$this->result->error('保存失败:' . $e->getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 构建树选择项
|
||||
*/
|
||||
private function buildTreeWithChecked($items, $checkedIds, $parentId = 0)
|
||||
{
|
||||
$tree = [];
|
||||
foreach ($items as $item) {
|
||||
if ($item['pid'] == $parentId) {
|
||||
$isChecked = in_array($item['id'], $checkedIds);
|
||||
$children = $this->buildTreeWithChecked($items, $checkedIds, $item['id']);
|
||||
$item['spread'] = true;
|
||||
$item['checked'] = $isChecked;
|
||||
$item['children'] = $children;
|
||||
$tree[] = $item;
|
||||
}
|
||||
}
|
||||
return $tree;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,161 @@
|
||||
<?php
|
||||
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 app\backend\validate\Help as HelpValidate;
|
||||
|
||||
/**
|
||||
* 站点帮助管理
|
||||
*/
|
||||
class Help extends BackendBase
|
||||
{
|
||||
protected $noNeedLogin = [];
|
||||
protected $noNeedVerify = [];
|
||||
|
||||
protected function initialize()
|
||||
{
|
||||
// 运行时自愈 help 表结构(category / view_count 等扩展列)
|
||||
\ywxapp\model\Help::ensureSchema();
|
||||
$this->model = new HelpModel();
|
||||
}
|
||||
|
||||
public function index()
|
||||
{
|
||||
$page = $this->request->param('page/d', 1);
|
||||
$limit = $this->request->param('limit/d', 15);
|
||||
$title = $this->request->param('title', '');
|
||||
|
||||
if ($this->request->isAjax()) {
|
||||
$query = $this->model->newQuery();
|
||||
if ($title !== '') {
|
||||
$query->where('title', 'like', '%' . $title . '%');
|
||||
}
|
||||
$list = $query->order('sort', 'desc')
|
||||
->paginate(['page' => $page, 'list_rows' => $limit]);
|
||||
$this->result->setCount($list->total())->success($list->items());
|
||||
}
|
||||
return $this->fetch();
|
||||
}
|
||||
|
||||
public function save()
|
||||
{
|
||||
if (! $this->request->isPost()) {
|
||||
$this->result->error('请求方式错误', 405);
|
||||
}
|
||||
$params = $this->request->only(['title', 'content', 'category', 'sort', 'status'], 'post');
|
||||
try {
|
||||
validate(HelpValidate::class)->check($params);
|
||||
$this->model->create($params);
|
||||
$this->result->success('', '添加成功');
|
||||
} catch (ValidateException $e) {
|
||||
$this->result->error($e->getMessage());
|
||||
} catch (DbException $e) {
|
||||
$this->result->error('添加失败: ' . $e->getMessage(), 2);
|
||||
}
|
||||
}
|
||||
|
||||
public function edit($id = null)
|
||||
{
|
||||
$id = $id ?: $this->request->param('id');
|
||||
$info = $this->model->find($id);
|
||||
if (! $info) {
|
||||
$this->result->error('记录不存在', 404);
|
||||
}
|
||||
$this->result->success(['info' => $info]);
|
||||
}
|
||||
|
||||
public function update()
|
||||
{
|
||||
if (! ($this->request->isAjax() && $this->request->isPut())) {
|
||||
$this->result->error('请求方式错误', 405);
|
||||
}
|
||||
$params = $this->request->param();
|
||||
$id = $params['id'] ?? null;
|
||||
$info = $this->model->find($id);
|
||||
if (! $info) {
|
||||
$this->result->error('记录不存在', 404);
|
||||
}
|
||||
$statusOnly = isset($params['status'])
|
||||
&& count(array_diff(array_keys($params), ['id', 'status'])) === 0;
|
||||
if (! $statusOnly) {
|
||||
try {
|
||||
validate(HelpValidate::class)->check($params);
|
||||
} catch (ValidateException $e) {
|
||||
$this->result->error($e->getMessage());
|
||||
}
|
||||
}
|
||||
$info->save($params);
|
||||
$this->result->success($info, '更新成功');
|
||||
}
|
||||
|
||||
public function recyclebin()
|
||||
{
|
||||
$page = $this->request->param('page/d', 1);
|
||||
$limit = $this->request->param('limit/d', 15);
|
||||
if ($this->request->isAjax()) {
|
||||
$list = $this->model->onlyTrashed()
|
||||
->order('sort', 'desc')
|
||||
->paginate(['page' => $page, 'list_rows' => $limit]);
|
||||
$this->result->setCount($list->total())->success($list->items());
|
||||
}
|
||||
return $this->fetch('help/index');
|
||||
}
|
||||
|
||||
public function delete()
|
||||
{
|
||||
if ($this->request->isAjax() && $this->request->isDelete()) {
|
||||
$ids = $this->request->param('ids', '');
|
||||
$force = $this->request->param('force', false);
|
||||
if (empty($ids)) {
|
||||
$this->result->error('请选择要删除的数据');
|
||||
}
|
||||
$idsArray = array_filter(explode(',', $ids), 'is_numeric');
|
||||
if (empty($idsArray)) {
|
||||
$this->result->error('参数错误');
|
||||
}
|
||||
try {
|
||||
Db::transaction(function () use ($idsArray, $force) {
|
||||
if ($force) {
|
||||
$this->model->onlyTrashed()->whereIn('id', $idsArray)->select()
|
||||
->each(function ($item) { $item->force()->delete(); });
|
||||
} else {
|
||||
$this->model->destroy($idsArray);
|
||||
}
|
||||
});
|
||||
$this->result->success();
|
||||
} catch (\think\exception\HttpResponseException $e) {
|
||||
throw $e;
|
||||
} catch (\Exception $e) {
|
||||
$this->result->error('删除失败: ' . ($e->getMessage() ?: $e->__toString()), 500);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public function restore($ids = '')
|
||||
{
|
||||
if ($this->request->isAjax() && $this->request->isPost()) {
|
||||
$ids = $this->request->param('ids', '');
|
||||
if (empty($ids)) {
|
||||
$this->result->error('请选择要恢复的数据');
|
||||
}
|
||||
$idsArray = array_filter(explode(',', $ids), 'is_numeric');
|
||||
Db::startTrans();
|
||||
try {
|
||||
$this->model->withTrashed()->where('id', 'in', $idsArray)->select()
|
||||
->each(function ($item) { $item->restore(); });
|
||||
Db::commit();
|
||||
$this->result->success('恢复成功');
|
||||
} catch (DbException $e) {
|
||||
Db::rollback();
|
||||
$this->result->error('恢复失败: ' . $e->getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,89 @@
|
||||
<?php
|
||||
/*
|
||||
* @Author: YwxApp <ywx@ywxapp.cn>
|
||||
* @Date: 2026-04-29 02:32:33
|
||||
* @LastEditors: YwxApp <ywx@ywxapp.cn>
|
||||
* @LastEditTime: 2026-08-06 23:06:14
|
||||
* @Description:
|
||||
* @FilePath: \ywxapp_dev\app\backend\controller\Index.php
|
||||
* @CustomString: Copyright (c) 2026 YwxApp
|
||||
*/
|
||||
|
||||
declare (strict_types = 1);
|
||||
namespace app\backend\controller;
|
||||
|
||||
use think\facade\Route;
|
||||
use think\facade\View;
|
||||
use think\Request;
|
||||
use ywxapp\controller\BackendBase;
|
||||
|
||||
/**
|
||||
*
|
||||
* 要是有你在就好了!
|
||||
* 其实我很少和别人聊天,但是我很喜欢你有事就跟我分享感觉,你的快乐我参与,你的烦恼我们一起分担
|
||||
* 如果我不小心惹你生气了,我要怎么做你才能原谅我呢?
|
||||
* 那假如你惹我生气了,我不搭理你,你怎么办
|
||||
* 我知道你对我很好,我也知道我自己有不足,以前我太自我了,没有耐心经验感情,但是面对你我会努力的,因为你很重要,以后有什么不满的都记得告诉我,不要在心里面偷偷扣我分好吗!
|
||||
* 你送我的礼物我很喜欢,从来没有人给我送过这么用心的礼物。
|
||||
* 跟你聊天真的好有意思,不过我现在有点事要去忙,咱们回头聊。
|
||||
* 你已经做得很好了,要是换做是我,我可能比你现在还激动呢
|
||||
* 以前我挺不成熟的,辜负了很多爱我的人,但是我现在成熟了,想要对值得的人更好一点
|
||||
* 我一直觉得自己挺凶的,以为要孤独终老,没想到遇到你这么关心我的人,别人只会挑毛病,你却关心我累不累,你会夸我 会送礼物,遇到你真的是我花光了这辈子的运气
|
||||
*
|
||||
* 后台首页控制器
|
||||
*/
|
||||
class Index extends BackendBase
|
||||
{
|
||||
/**
|
||||
* Summary of needLogin
|
||||
* @var array
|
||||
*/
|
||||
protected $noNeedLogin = ['welcome'];
|
||||
|
||||
/**
|
||||
* Summary of needRight
|
||||
* @var array
|
||||
*/
|
||||
protected $noNeedVerify = ["index"];
|
||||
|
||||
/**
|
||||
* 控制器初始化 initialize
|
||||
* @return void
|
||||
*/
|
||||
protected function initialize()
|
||||
{
|
||||
}
|
||||
|
||||
/**
|
||||
* 显示资源列表
|
||||
*
|
||||
* @param \think\Request $request
|
||||
* @return \think\Response
|
||||
*/
|
||||
public function index()
|
||||
{
|
||||
view::assign('title', '后台管理系统');
|
||||
view::layout(false);
|
||||
return View::fetch();
|
||||
}
|
||||
|
||||
public function welcome()
|
||||
{
|
||||
return 'welcome';
|
||||
}
|
||||
public function menu()
|
||||
{
|
||||
$uid = $this->auth->model->id;
|
||||
$info = \ywxapp\model\BackendAdmin::with('roles')->find($uid);
|
||||
if (! $info) {
|
||||
return json(['msg' => 'Member not found'], 404);
|
||||
}
|
||||
// 获取扁平权限列表(供前端按钮控制)
|
||||
//$permissions = $user->getAllPermissions();
|
||||
$flatMenus = $info->getAccessibleMenus();
|
||||
|
||||
$this->result->success($flatMenus);
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -0,0 +1,213 @@
|
||||
<?php
|
||||
/*
|
||||
* @Author: YwxApp <ywx@ywxapp.cn>
|
||||
* @Date: 2026-05-09 00:41:41
|
||||
* @LastEditors: YwxApp <ywx@ywxapp.cn>
|
||||
* @LastEditTime: 2026-07-23 00:00:00
|
||||
* @Description: 友情链接管理
|
||||
* @FilePath: \ywxapp_dev\app\backend\controller\Links.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\Links as LinksModel;
|
||||
use app\backend\validate\Links as LinksValidate;
|
||||
|
||||
/**
|
||||
* 友情链接管理
|
||||
*
|
||||
* @author ywxapp <admin@ywxapp.cn>
|
||||
*/
|
||||
class Links extends BackendBase
|
||||
{
|
||||
protected $noNeedLogin = [];
|
||||
protected $noNeedVerify = [];
|
||||
|
||||
protected function initialize()
|
||||
{
|
||||
$this->model = new LinksModel();
|
||||
}
|
||||
|
||||
/**
|
||||
* 列表
|
||||
*/
|
||||
public function index()
|
||||
{
|
||||
$page = $this->request->param('page/d', 1);
|
||||
$limit = $this->request->param('limit/d', 15);
|
||||
$title = $this->request->param('title', '');
|
||||
|
||||
if ($this->request->isAjax()) {
|
||||
$query = $this->model->newQuery();
|
||||
if ($title !== '') {
|
||||
$query->where('title', 'like', '%' . $title . '%');
|
||||
}
|
||||
$list = $query->order('sort', 'desc')
|
||||
->paginate(['page' => $page, 'list_rows' => $limit]);
|
||||
$this->result->setCount($list->total())->success($list->items());
|
||||
}
|
||||
|
||||
return $this->fetch();
|
||||
}
|
||||
|
||||
/**
|
||||
* 保存
|
||||
*/
|
||||
public function save()
|
||||
{
|
||||
if (! $this->request->isPost()) {
|
||||
$this->result->error('请求方式错误', 405);
|
||||
}
|
||||
$params = $this->request->only(
|
||||
['title', 'url', 'logo', 'description', 'sort', 'status'],
|
||||
'post'
|
||||
);
|
||||
try {
|
||||
validate(LinksValidate::class)->check($params);
|
||||
$this->model->create($params);
|
||||
$this->result->success('', '添加成功');
|
||||
} catch (ValidateException $e) {
|
||||
$this->result->error($e->getMessage());
|
||||
} catch (DbException $e) {
|
||||
$this->result->error('添加失败: ' . $e->getMessage(), 2);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 编辑(抽屉表单直接读取行数据,此接口可用于回显)
|
||||
*/
|
||||
public function edit($id = null)
|
||||
{
|
||||
$id = $id ?: $this->request->param('id');
|
||||
$info = $this->model->find($id);
|
||||
if (! $info) {
|
||||
$this->result->error('友链不存在', 404);
|
||||
}
|
||||
$this->result->success(['info' => $info]);
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新
|
||||
*/
|
||||
public function update()
|
||||
{
|
||||
if (! ($this->request->isAjax() && $this->request->isPut())) {
|
||||
$this->result->error('请求方式错误', 405);
|
||||
}
|
||||
$params = $this->request->param();
|
||||
$id = $params['id'] ?? null;
|
||||
$info = $this->model->find($id);
|
||||
if (! $info) {
|
||||
$this->result->error('友链不存在', 404);
|
||||
}
|
||||
|
||||
// 仅切换状态时不走完整校验(状态开关走此分支)
|
||||
$statusOnly = isset($params['status'])
|
||||
&& count(array_diff(array_keys($params), ['id', 'status'])) === 0;
|
||||
|
||||
if (! $statusOnly) {
|
||||
try {
|
||||
validate(LinksValidate::class)->check($params);
|
||||
} catch (ValidateException $e) {
|
||||
$this->result->error($e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
$info->save($params);
|
||||
$this->result->success($info, '更新成功');
|
||||
}
|
||||
|
||||
/**
|
||||
* 回收站列表
|
||||
*/
|
||||
public function recyclebin()
|
||||
{
|
||||
$page = $this->request->param('page/d', 1);
|
||||
$limit = $this->request->param('limit/d', 15);
|
||||
|
||||
if ($this->request->isAjax()) {
|
||||
$list = $this->model->onlyTrashed()
|
||||
->order('sort', 'desc')
|
||||
->paginate(['page' => $page, 'list_rows' => $limit]);
|
||||
$this->result->setCount($list->total())->success($list->items());
|
||||
}
|
||||
|
||||
return $this->fetch('links/index');
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除(软删除;force=1 物理删除)
|
||||
*/
|
||||
public function delete()
|
||||
{
|
||||
if ($this->request->isAjax() && $this->request->isDelete()) {
|
||||
$ids = $this->request->param('ids', '');
|
||||
$force = $this->request->param('force', false);
|
||||
if (empty($ids)) {
|
||||
$this->result->error('请选择要删除的数据');
|
||||
}
|
||||
$idsArray = array_filter(explode(',', $ids), 'is_numeric');
|
||||
if (empty($idsArray)) {
|
||||
$this->result->error('参数错误');
|
||||
}
|
||||
try {
|
||||
Db::transaction(function () use ($idsArray, $force) {
|
||||
if ($force) {
|
||||
$this->model->onlyTrashed()
|
||||
->whereIn('id', $idsArray)
|
||||
->select()
|
||||
->each(function ($item) {
|
||||
$item->force()->delete();
|
||||
});
|
||||
} else {
|
||||
$this->model->destroy($idsArray);
|
||||
}
|
||||
});
|
||||
$this->result->success();
|
||||
} catch (\think\exception\HttpResponseException $e) {
|
||||
throw $e;
|
||||
} catch (\Exception $e) {
|
||||
\think\facade\Log::error('批量删除友链失败', [
|
||||
'exception' => $e->__toString(),
|
||||
'ids' => $idsArray,
|
||||
]);
|
||||
$this->result->error('删除失败: ' . ($e->getMessage() ?: $e->__toString()), 500);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 从回收站恢复
|
||||
*/
|
||||
public function restore($ids = '')
|
||||
{
|
||||
if ($this->request->isAjax() && $this->request->isPost()) {
|
||||
$ids = $this->request->param('ids', '');
|
||||
if (empty($ids)) {
|
||||
$this->result->error('请选择要恢复的数据');
|
||||
}
|
||||
$idsArray = array_filter(explode(',', $ids), 'is_numeric');
|
||||
Db::startTrans();
|
||||
try {
|
||||
$this->model->withTrashed()
|
||||
->where('id', 'in', $idsArray)
|
||||
->select()
|
||||
->each(function ($item) {
|
||||
$item->restore();
|
||||
});
|
||||
Db::commit();
|
||||
$this->result->success('恢复成功');
|
||||
} catch (DbException $e) {
|
||||
Db::rollback();
|
||||
$this->result->error('恢复失败: ' . $e->getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,111 @@
|
||||
<?php
|
||||
/*
|
||||
* @Author: YwxApp <ywx@ywxapp.cn>
|
||||
* @Date: 2026-04-29 02:32:33
|
||||
* @LastEditors: YwxApp <ywx@ywxapp.cn>
|
||||
* @LastEditTime: 2026-08-10 09:32:39
|
||||
* @Description:
|
||||
* @FilePath: \ywxapp_dev\app\backend\controller\Login.php
|
||||
* @CustomString: Copyright (c) 2026 YwxApp
|
||||
*/
|
||||
|
||||
declare(strict_types=1);
|
||||
namespace app\backend\controller;
|
||||
|
||||
use app\backend\validate\Login as LoginValidate;
|
||||
use think\exception\ValidateException;
|
||||
use think\facade\View;
|
||||
use think\Request;
|
||||
use ywxapp\controller\BackendBase;
|
||||
|
||||
/**
|
||||
* Login 类
|
||||
*
|
||||
* @author ywxapp <admin@ywxapp.cn>
|
||||
*/
|
||||
class Login extends BackendBase
|
||||
{
|
||||
/**
|
||||
* Summary of needLogin
|
||||
* @var array
|
||||
*/
|
||||
protected $noNeedLogin = ['*'];
|
||||
|
||||
/**
|
||||
* Summary of needRight
|
||||
* @var array
|
||||
*/
|
||||
protected $noNeedVerify = ['*'];
|
||||
|
||||
/**
|
||||
* 控制器初始化 initialize
|
||||
* @return void
|
||||
*/
|
||||
|
||||
protected function initialize()
|
||||
{
|
||||
$this->model = new \ywxapp\model\BackendAdmin();
|
||||
}
|
||||
|
||||
/**
|
||||
* 显示资源列表
|
||||
*
|
||||
* @param \think\Request $request
|
||||
* @return \think\Response
|
||||
*/
|
||||
|
||||
public function index()
|
||||
{
|
||||
View::assign('title', '登录');
|
||||
View::layout(false);
|
||||
return View::fetch();
|
||||
}
|
||||
|
||||
/**
|
||||
* 保存新建的资源
|
||||
*
|
||||
* @param \think\Request $request
|
||||
* @return \think\Response
|
||||
*/
|
||||
|
||||
public function save()
|
||||
{
|
||||
if ($this->request->isAjax() && $this->request->isPost()) {
|
||||
$data = $this->request->param();
|
||||
try {
|
||||
validate(LoginValidate::class)->check($data);
|
||||
$this->auth->login($data['username'], $data['password']);
|
||||
if ($this->auth->isLogin) {
|
||||
$adminId = $this->auth->model->id ?? null;
|
||||
$info = $this->model->with('roles')->find($adminId);
|
||||
if (! $info) {
|
||||
$this->result->error(lang('Member not found'), 1);
|
||||
}
|
||||
// 获取扁平权限列表(供前端按钮控制):返回权限 key 字符串数组
|
||||
$permissions = $info->getPermissionNames();
|
||||
$flatMenus = $info->getAccessibleMenus();
|
||||
$this->result->success([
|
||||
'permissions' => $permissions,
|
||||
'menus' => $flatMenus
|
||||
]);
|
||||
}
|
||||
} catch (ValidateException $e) {
|
||||
$this->result->error('验证错误: ' . $e->getMessage(), 1);
|
||||
}
|
||||
}
|
||||
$this->result->error("访问错误");
|
||||
}
|
||||
|
||||
/**
|
||||
* 退出登录
|
||||
*
|
||||
* @param \think\Request $request
|
||||
* @return \think\Response
|
||||
*/
|
||||
|
||||
public function logout()
|
||||
{
|
||||
$this->auth->logout();
|
||||
$this->result->success('退出登录成功');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,186 @@
|
||||
<?php
|
||||
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\Medal as MedalModel;
|
||||
use app\backend\validate\Medal as MedalValidate;
|
||||
|
||||
/**
|
||||
* 勋章中心管理
|
||||
*/
|
||||
class Medal extends BackendBase
|
||||
{
|
||||
protected $noNeedLogin = [];
|
||||
protected $noNeedVerify = [];
|
||||
|
||||
protected function initialize()
|
||||
{
|
||||
$this->model = new MedalModel();
|
||||
}
|
||||
|
||||
public function index()
|
||||
{
|
||||
$page = $this->request->param('page/d', 1);
|
||||
$limit = $this->request->param('limit/d', 15);
|
||||
$title = $this->request->param('title', '');
|
||||
|
||||
if ($this->request->isAjax()) {
|
||||
$query = $this->model->newQuery();
|
||||
if ($title !== '') {
|
||||
$query->where('title', 'like', '%' . $title . '%');
|
||||
}
|
||||
$list = $query->order('sort', 'desc')
|
||||
->paginate(['page' => $page, 'list_rows' => $limit]);
|
||||
$this->result->setCount($list->total())->success($list->items());
|
||||
}
|
||||
return $this->fetch();
|
||||
}
|
||||
|
||||
public function save()
|
||||
{
|
||||
if (! $this->request->isPost()) {
|
||||
$this->result->error('请求方式错误', 405);
|
||||
}
|
||||
$params = $this->request->only(['title', 'image', 'description', 'sort', 'status'], 'post');
|
||||
try {
|
||||
validate(MedalValidate::class)->check($params);
|
||||
$this->model->create($params);
|
||||
$this->result->success('', '添加成功');
|
||||
} catch (ValidateException $e) {
|
||||
$this->result->error($e->getMessage());
|
||||
} catch (DbException $e) {
|
||||
$this->result->error('添加失败: ' . $e->getMessage(), 2);
|
||||
}
|
||||
}
|
||||
|
||||
public function edit($id = null)
|
||||
{
|
||||
$id = $id ?: $this->request->param('id');
|
||||
$info = $this->model->find($id);
|
||||
if (! $info) {
|
||||
$this->result->error('记录不存在', 404);
|
||||
}
|
||||
$this->result->success(['info' => $info]);
|
||||
}
|
||||
|
||||
public function update()
|
||||
{
|
||||
if (! ($this->request->isAjax() && $this->request->isPut())) {
|
||||
$this->result->error('请求方式错误', 405);
|
||||
}
|
||||
$params = $this->request->param();
|
||||
$id = $params['id'] ?? null;
|
||||
$info = $this->model->find($id);
|
||||
if (! $info) {
|
||||
$this->result->error('记录不存在', 404);
|
||||
}
|
||||
$statusOnly = isset($params['status'])
|
||||
&& count(array_diff(array_keys($params), ['id', 'status'])) === 0;
|
||||
if (! $statusOnly) {
|
||||
try {
|
||||
validate(MedalValidate::class)->check($params);
|
||||
} catch (ValidateException $e) {
|
||||
$this->result->error($e->getMessage());
|
||||
}
|
||||
}
|
||||
$info->save($params);
|
||||
$this->result->success($info, '更新成功');
|
||||
}
|
||||
|
||||
public function recyclebin()
|
||||
{
|
||||
$page = $this->request->param('page/d', 1);
|
||||
$limit = $this->request->param('limit/d', 15);
|
||||
if ($this->request->isAjax()) {
|
||||
$list = $this->model->onlyTrashed()
|
||||
->order('sort', 'desc')
|
||||
->paginate(['page' => $page, 'list_rows' => $limit]);
|
||||
$this->result->setCount($list->total())->success($list->items());
|
||||
}
|
||||
return $this->fetch('medal/index');
|
||||
}
|
||||
|
||||
public function delete()
|
||||
{
|
||||
if ($this->request->isAjax() && $this->request->isDelete()) {
|
||||
$ids = $this->request->param('ids', '');
|
||||
$force = $this->request->param('force', false);
|
||||
if (empty($ids)) {
|
||||
$this->result->error('请选择要删除的数据');
|
||||
}
|
||||
$idsArray = array_filter(explode(',', $ids), 'is_numeric');
|
||||
if (empty($idsArray)) {
|
||||
$this->result->error('参数错误');
|
||||
}
|
||||
try {
|
||||
Db::transaction(function () use ($idsArray, $force) {
|
||||
if ($force) {
|
||||
$this->model->onlyTrashed()->whereIn('id', $idsArray)->select()
|
||||
->each(function ($item) { $item->force()->delete(); });
|
||||
} else {
|
||||
$this->model->destroy($idsArray);
|
||||
}
|
||||
});
|
||||
$this->result->success();
|
||||
} catch (\think\exception\HttpResponseException $e) {
|
||||
throw $e;
|
||||
} catch (\Exception $e) {
|
||||
$this->result->error('删除失败: ' . ($e->getMessage() ?: $e->__toString()), 500);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public function restore($ids = '')
|
||||
{
|
||||
if ($this->request->isAjax() && $this->request->isPost()) {
|
||||
$ids = $this->request->param('ids', '');
|
||||
if (empty($ids)) {
|
||||
$this->result->error('请选择要恢复的数据');
|
||||
}
|
||||
$idsArray = array_filter(explode(',', $ids), 'is_numeric');
|
||||
Db::startTrans();
|
||||
try {
|
||||
$this->model->withTrashed()->where('id', 'in', $idsArray)->select()
|
||||
->each(function ($item) { $item->restore(); });
|
||||
Db::commit();
|
||||
$this->result->success('恢复成功');
|
||||
} catch (DbException $e) {
|
||||
Db::rollback();
|
||||
$this->result->error('恢复失败: ' . $e->getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 手动授予勋章(给指定用户发放)
|
||||
*/
|
||||
public function grant()
|
||||
{
|
||||
if (! ($this->request->isAjax() && $this->request->isPost())) {
|
||||
$this->result->error('请求方式错误', 405);
|
||||
}
|
||||
$uid = (int) $this->request->param('uid', 0);
|
||||
$medalId = (int) $this->request->param('medal_id', 0);
|
||||
if ($uid <= 0) {
|
||||
$this->result->error('请输入有效的用户ID');
|
||||
}
|
||||
if ($medalId <= 0) {
|
||||
$this->result->error('请选择勋章');
|
||||
}
|
||||
// 校验用户存在
|
||||
$user = Db::name('member')->where('uid', $uid)->find();
|
||||
if (! $user) {
|
||||
$this->result->error('用户不存在');
|
||||
}
|
||||
[$ok, $msg] = MedalModel::grant($uid, $medalId);
|
||||
if ($ok) {
|
||||
$this->result->success([], $msg);
|
||||
}
|
||||
$this->result->error($msg);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,145 @@
|
||||
<?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\controller;
|
||||
|
||||
use think\facade\Request;
|
||||
use think\facade\View;
|
||||
use app\backend\model\NavMenu;
|
||||
use ywxapp\controller\BackendBase;
|
||||
|
||||
/**
|
||||
* 前台主导航菜单管理(支持两级下拉,可后台配置).
|
||||
*/
|
||||
class Navbar extends BackendBase
|
||||
{
|
||||
protected function initialize()
|
||||
{
|
||||
}
|
||||
|
||||
/**
|
||||
* 列表页.
|
||||
*/
|
||||
public function index()
|
||||
{
|
||||
if (Request::isAjax()) {
|
||||
$list = NavMenu::getAdminTree();
|
||||
return json(['code' => 0, 'msg' => 'ok', 'data' => $list, 'count' => count($list)]);
|
||||
}
|
||||
return View::fetch();
|
||||
}
|
||||
|
||||
/**
|
||||
* 添加/编辑页(页面式,复用 edit.html).
|
||||
*/
|
||||
public function edit($id = 0)
|
||||
{
|
||||
$row = $id ? NavMenu::find($id) : null;
|
||||
if (Request::isPost()) {
|
||||
$data = [
|
||||
'parent_id' => (int) Request::post('parent_id', 0),
|
||||
'title' => trim((string) Request::post('title', '')),
|
||||
'url' => trim((string) Request::post('url', '')),
|
||||
'icon' => trim((string) Request::post('icon', '')),
|
||||
'sort' => (int) Request::post('sort', 0),
|
||||
'status' => (int) Request::post('status', 1),
|
||||
];
|
||||
if ($data['title'] === '') {
|
||||
return json(['code' => 1, 'msg' => '请输入菜单名称']);
|
||||
}
|
||||
// 不能把自己设为自己的父级
|
||||
if ($id && $data['parent_id'] === (int) $id) {
|
||||
return json(['code' => 1, 'msg' => '父级不能选择自己']);
|
||||
}
|
||||
if ($id) {
|
||||
$row = NavMenu::find($id);
|
||||
$row->save($data);
|
||||
return json(['code' => 0, 'msg' => '已保存']);
|
||||
}
|
||||
NavMenu::create($data);
|
||||
return json(['code' => 0, 'msg' => '已添加']);
|
||||
}
|
||||
// 父级下拉选项(仅一级)
|
||||
$parents = NavMenu::where('parent_id', 0)
|
||||
->where('delete_at', 0)
|
||||
->order('sort', 'asc')
|
||||
->field('id,title')
|
||||
->select();
|
||||
View::assign('parents', $parents);
|
||||
View::assign('row', $row);
|
||||
return View::fetch();
|
||||
}
|
||||
|
||||
/**
|
||||
* 保存(弹窗式 AJAX 提交,与前端 nav.js 对应).
|
||||
*/
|
||||
public function save()
|
||||
{
|
||||
$data = [
|
||||
'parent_id' => (int) Request::post('parent_id', 0),
|
||||
'title' => trim((string) Request::post('title', '')),
|
||||
'url' => trim((string) Request::post('url', '')),
|
||||
'icon' => trim((string) Request::post('icon', '')),
|
||||
'sort' => (int) Request::post('sort', 0),
|
||||
'status' => (int) Request::post('status', 1),
|
||||
];
|
||||
if ($data['title'] === '') {
|
||||
return json(['code' => 1, 'msg' => '请输入菜单名称']);
|
||||
}
|
||||
$id = (int) Request::post('id', 0);
|
||||
if ($id) {
|
||||
if ($data['parent_id'] === $id) {
|
||||
return json(['code' => 1, 'msg' => '父级不能选择自己']);
|
||||
}
|
||||
NavMenu::find($id)->save($data);
|
||||
return json(['code' => 0, 'msg' => '已保存']);
|
||||
}
|
||||
NavMenu::create($data);
|
||||
return json(['code' => 0, 'msg' => '已添加']);
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新(与 save 同逻辑,兼容 PUT).
|
||||
*/
|
||||
public function update()
|
||||
{
|
||||
return $this->save();
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除(支持单 id;有子项时拒绝,避免孤儿).
|
||||
*/
|
||||
public function delete($id = 0)
|
||||
{
|
||||
$id = $id ?: (int) Request::post('id', 0);
|
||||
if (! $id) {
|
||||
return json(['code' => 1, 'msg' => '请选择要删除的项']);
|
||||
}
|
||||
$hasChild = NavMenu::where('parent_id', $id)->where('delete_at', 0)->count();
|
||||
if ($hasChild) {
|
||||
return json(['code' => 1, 'msg' => '请先删除该菜单下的子项']);
|
||||
}
|
||||
NavMenu::destroy($id);
|
||||
return json(['code' => 0, 'msg' => '已删除']);
|
||||
}
|
||||
|
||||
/**
|
||||
* 切换显示/隐藏.
|
||||
*/
|
||||
public function status($id = 0, $status = 1)
|
||||
{
|
||||
$id = $id ?: (int) Request::post('id', 0);
|
||||
if (! $id) {
|
||||
return json(['code' => 1, 'msg' => '参数错误']);
|
||||
}
|
||||
NavMenu::find($id)->save(['status' => (int) $status]);
|
||||
return json(['code' => 0, 'msg' => '已更新']);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,197 @@
|
||||
<?php
|
||||
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\Notice as NoticeModel;
|
||||
use app\backend\validate\Notice as NoticeValidate;
|
||||
|
||||
/**
|
||||
* 站点公告管理
|
||||
*/
|
||||
class Notice extends BackendBase
|
||||
{
|
||||
protected $noNeedLogin = [];
|
||||
protected $noNeedVerify = [];
|
||||
|
||||
protected function initialize()
|
||||
{
|
||||
$this->model = new NoticeModel();
|
||||
NoticeModel::ensureSchema();
|
||||
}
|
||||
|
||||
public function index()
|
||||
{
|
||||
$page = $this->request->param('page/d', 1);
|
||||
$limit = $this->request->param('limit/d', 15);
|
||||
$title = $this->request->param('title', '');
|
||||
$type = $this->request->param('type/d', 0);
|
||||
|
||||
if ($this->request->isAjax()) {
|
||||
$query = $this->model->newQuery();
|
||||
if ($title !== '') {
|
||||
$query->where('title', 'like', '%' . $title . '%');
|
||||
}
|
||||
if ($type > 0) {
|
||||
$query->where('type', $type);
|
||||
}
|
||||
$list = $query->order('is_top', 'desc')
|
||||
->order('sort', 'desc')
|
||||
->order('id', 'desc')
|
||||
->paginate(['page' => $page, 'list_rows' => $limit]);
|
||||
$this->result->setCount($list->total())->success($list->items());
|
||||
}
|
||||
return $this->fetch();
|
||||
}
|
||||
|
||||
public function save()
|
||||
{
|
||||
if (! $this->request->isPost()) {
|
||||
$this->result->error('请求方式错误', 405);
|
||||
}
|
||||
$params = $this->request->only(
|
||||
['title', 'content', 'author', 'type', 'is_top', 'start_time', 'end_time', 'sort', 'status'],
|
||||
'post'
|
||||
);
|
||||
// 未填写发布人时取当前登录管理员
|
||||
if (empty($params['author']) && isset($this->auth->info['username'])) {
|
||||
$params['author'] = $this->auth->info['username'];
|
||||
}
|
||||
$params = $this->parseTime($params);
|
||||
try {
|
||||
validate(NoticeValidate::class)->check($params);
|
||||
$this->model->create($params);
|
||||
$this->result->success('', '添加成功');
|
||||
} catch (ValidateException $e) {
|
||||
$this->result->error($e->getMessage());
|
||||
} catch (DbException $e) {
|
||||
$this->result->error('添加失败: ' . $e->getMessage(), 2);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 将前端传来的日期字符串转为时间戳(空则置 0=长期)
|
||||
*/
|
||||
private function parseTime(array $params): array
|
||||
{
|
||||
foreach (['start_time', 'end_time'] as $f) {
|
||||
if (isset($params[$f])) {
|
||||
$params[$f] = $params[$f] === '' || $params[$f] === null
|
||||
? 0
|
||||
: (is_numeric($params[$f]) ? (int)$params[$f] : strtotime($params[$f]));
|
||||
} else {
|
||||
$params[$f] = 0;
|
||||
}
|
||||
}
|
||||
return $params;
|
||||
}
|
||||
|
||||
public function edit($id = null)
|
||||
{
|
||||
$id = $id ?: $this->request->param('id');
|
||||
$info = $this->model->find($id);
|
||||
if (! $info) {
|
||||
$this->result->error('记录不存在', 404);
|
||||
}
|
||||
// 时间戳转日期字符串,方便表单回填
|
||||
$data = $info->toArray();
|
||||
$data['start_time'] = ! empty($data['start_time']) ? date('Y-m-d H:i:s', $data['start_time']) : '';
|
||||
$data['end_time'] = ! empty($data['end_time']) ? date('Y-m-d H:i:s', $data['end_time']) : '';
|
||||
$this->result->success(['info' => $data]);
|
||||
}
|
||||
|
||||
public function update()
|
||||
{
|
||||
if (! ($this->request->isAjax() && $this->request->isPut())) {
|
||||
$this->result->error('请求方式错误', 405);
|
||||
}
|
||||
$params = $this->request->param();
|
||||
$id = $params['id'] ?? null;
|
||||
$info = $this->model->find($id);
|
||||
if (! $info) {
|
||||
$this->result->error('记录不存在', 404);
|
||||
}
|
||||
$statusOnly = isset($params['status'])
|
||||
&& count(array_diff(array_keys($params), ['id', 'status'])) === 0;
|
||||
if (! $statusOnly) {
|
||||
$params = $this->parseTime($params);
|
||||
try {
|
||||
validate(NoticeValidate::class)->check($params);
|
||||
} catch (ValidateException $e) {
|
||||
$this->result->error($e->getMessage());
|
||||
}
|
||||
} else {
|
||||
$params = $this->parseTime($params);
|
||||
}
|
||||
$info->save($params);
|
||||
$this->result->success($info, '更新成功');
|
||||
}
|
||||
|
||||
public function recyclebin()
|
||||
{
|
||||
$page = $this->request->param('page/d', 1);
|
||||
$limit = $this->request->param('limit/d', 15);
|
||||
if ($this->request->isAjax()) {
|
||||
$list = $this->model->onlyTrashed()
|
||||
->order('sort', 'desc')
|
||||
->paginate(['page' => $page, 'list_rows' => $limit]);
|
||||
$this->result->setCount($list->total())->success($list->items());
|
||||
}
|
||||
return $this->fetch('notice/index');
|
||||
}
|
||||
|
||||
public function delete()
|
||||
{
|
||||
if ($this->request->isAjax() && $this->request->isDelete()) {
|
||||
$ids = $this->request->param('ids', '');
|
||||
$force = $this->request->param('force', false);
|
||||
if (empty($ids)) {
|
||||
$this->result->error('请选择要删除的数据');
|
||||
}
|
||||
$idsArray = array_filter(explode(',', $ids), 'is_numeric');
|
||||
if (empty($idsArray)) {
|
||||
$this->result->error('参数错误');
|
||||
}
|
||||
try {
|
||||
Db::transaction(function () use ($idsArray, $force) {
|
||||
if ($force) {
|
||||
$this->model->onlyTrashed()->whereIn('id', $idsArray)->select()
|
||||
->each(function ($item) { $item->force()->delete(); });
|
||||
} else {
|
||||
$this->model->destroy($idsArray);
|
||||
}
|
||||
});
|
||||
$this->result->success();
|
||||
} catch (\think\exception\HttpResponseException $e) {
|
||||
throw $e;
|
||||
} catch (\Exception $e) {
|
||||
$this->result->error('删除失败: ' . ($e->getMessage() ?: $e->__toString()), 500);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public function restore($ids = '')
|
||||
{
|
||||
if ($this->request->isAjax() && $this->request->isPost()) {
|
||||
$ids = $this->request->param('ids', '');
|
||||
if (empty($ids)) {
|
||||
$this->result->error('请选择要恢复的数据');
|
||||
}
|
||||
$idsArray = array_filter(explode(',', $ids), 'is_numeric');
|
||||
Db::startTrans();
|
||||
try {
|
||||
$this->model->withTrashed()->where('id', 'in', $idsArray)->select()
|
||||
->each(function ($item) { $item->restore(); });
|
||||
Db::commit();
|
||||
$this->result->success('恢复成功');
|
||||
} catch (DbException $e) {
|
||||
Db::rollback();
|
||||
$this->result->error('恢复失败: ' . $e->getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,121 @@
|
||||
<?php
|
||||
/*
|
||||
* @Author: YwxApp <ywx@ywxapp.cn>
|
||||
* @Date: 2026-04-29 02:32:33
|
||||
* @LastEditors: YwxApp <ywx@ywxapp.cn>
|
||||
* @LastEditTime: 2026-07-21 23:19:33
|
||||
* @Description:
|
||||
* @FilePath: \ywxapp_dev\app\backend\controller\Other.php
|
||||
* @CustomString: Copyright (c) 2026 YwxApp
|
||||
*/
|
||||
|
||||
declare (strict_types = 1);
|
||||
namespace app\backend\controller;
|
||||
|
||||
use think\Request;
|
||||
use think\facade\View;
|
||||
use think\facade\Db;
|
||||
use think\facade\Config;
|
||||
|
||||
/**
|
||||
* Other 类
|
||||
*
|
||||
* @author ywxapp <admin@ywxapp.cn>
|
||||
*/
|
||||
class Other
|
||||
{
|
||||
/**
|
||||
* Summary of needLogin
|
||||
* @var array
|
||||
*/
|
||||
protected $noNeedLogin = [ ];
|
||||
|
||||
/**
|
||||
* Summary of needRight
|
||||
* @var array
|
||||
*/
|
||||
protected $noNeedVerify = ['*'];
|
||||
|
||||
/**
|
||||
* 控制器初始化 initialize
|
||||
* @return void
|
||||
*/
|
||||
protected function initialize()
|
||||
{}
|
||||
|
||||
/**
|
||||
* 关于
|
||||
*/
|
||||
public function about()
|
||||
{
|
||||
if (request()->isAjax()) {
|
||||
return json([
|
||||
'name' => 'YwxApp',
|
||||
'version' => \think\facade\App::version(),
|
||||
'php' => PHP_VERSION,
|
||||
]);
|
||||
}
|
||||
return View::fetch();
|
||||
}
|
||||
|
||||
/**
|
||||
* 全局搜索(文章标题)
|
||||
*/
|
||||
public function search(Request $request)
|
||||
{
|
||||
$q = $request->param('q', '');
|
||||
if (request()->isAjax() && $q) {
|
||||
$list = Db::name('articles_article')
|
||||
->where('title', 'like', '%' . $q . '%')
|
||||
->limit(20)
|
||||
->select();
|
||||
return json(['code' => 0, 'data' => $list]);
|
||||
}
|
||||
return View::fetch();
|
||||
}
|
||||
|
||||
/**
|
||||
* 显示指定的资源
|
||||
*
|
||||
* @param int $id
|
||||
* @return \think\Response
|
||||
*/
|
||||
public function read($id)
|
||||
{
|
||||
//
|
||||
}
|
||||
|
||||
/**
|
||||
* 显示编辑资源表单页.
|
||||
*
|
||||
* @param int $id
|
||||
* @return \think\Response
|
||||
*/
|
||||
public function edit($id = null)
|
||||
{
|
||||
//
|
||||
}
|
||||
|
||||
/**
|
||||
* 保存更新的资源
|
||||
*
|
||||
* @param \think\Request $request
|
||||
* @param int $id
|
||||
* @return \think\Response
|
||||
*/
|
||||
public function update(Request $request, $id)
|
||||
{
|
||||
//
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除指定资源
|
||||
*
|
||||
* @param int $id
|
||||
* @return \think\Response
|
||||
*/
|
||||
public function delete($id)
|
||||
{
|
||||
//
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,286 @@
|
||||
<?php
|
||||
/*
|
||||
* @Author: YwxApp <ywx@ywxapp.cn>
|
||||
* @Date: 2026-04-29 02:32:33
|
||||
* @LastEditors: YwxApp <ywx@ywxapp.cn>
|
||||
* @LastEditTime: 2026-07-23 18:17:36
|
||||
* @Description:
|
||||
* @FilePath: \ywxapp_dev\app\backend\controller\Power.php
|
||||
* @CustomString: Copyright (c) 2026 YwxApp
|
||||
*/
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace app\backend\controller;
|
||||
|
||||
use app\backend\service\PowerService;
|
||||
use app\backend\validate\Power as PowerValidate;
|
||||
use think\exception\ValidateException;
|
||||
use think\facade\Db;
|
||||
use think\facade\View;
|
||||
use think\Request;
|
||||
use think\Response;
|
||||
use ywxapp\controller\BackendBase;
|
||||
use ywxapp\model\BackendPower as PowerModel;
|
||||
use ywxapp\model\BackendRolePower;
|
||||
|
||||
/**
|
||||
* Power 类
|
||||
*
|
||||
* @author ywxapp <admin@ywxapp.cn>
|
||||
*/
|
||||
class Power extends BackendBase
|
||||
{
|
||||
|
||||
/**
|
||||
* Summary of needLogin
|
||||
* @var array
|
||||
*/
|
||||
protected $noNeedLogin = [];
|
||||
|
||||
/**
|
||||
* Summary of needRight
|
||||
* @var array
|
||||
*/
|
||||
protected $noNeedVerify = [];
|
||||
|
||||
/**
|
||||
* 控制器初始化 initialize
|
||||
* @return void
|
||||
*/
|
||||
protected function initialize()
|
||||
{
|
||||
$this->model = new PowerModel();
|
||||
$this->view->assign('title', '权限管理');
|
||||
}
|
||||
|
||||
/**
|
||||
* 显示资源列表
|
||||
*
|
||||
* @param \think\Request $request
|
||||
* @return \think\Response
|
||||
*/
|
||||
public function index()
|
||||
{
|
||||
$page = $this->request->param('page', 1);
|
||||
$size = $this->request->param('size', 100);
|
||||
if ($this->request->isAjax()) {
|
||||
$data = PowerModel::cache(false)->paginate([
|
||||
'page' => $page,
|
||||
'list_rows' => $size,
|
||||
]);
|
||||
$this->result->success($data->items());
|
||||
}
|
||||
return view('index', [
|
||||
'name' => 'ThinkPHP',
|
||||
'email' => 'thinkphp@qq.com'
|
||||
]);
|
||||
return $this->view->fetch();
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建权限
|
||||
*
|
||||
* @param \think\Request $request
|
||||
* @return \think\Response
|
||||
*/
|
||||
public function create()
|
||||
{
|
||||
if ($this->request->isAjax()) {
|
||||
$power = PowerModel::cateTree(PowerModel::select()->toArray());
|
||||
$this->result->success(['power' => $power]);
|
||||
}
|
||||
//
|
||||
}
|
||||
|
||||
/**
|
||||
* 保存权限
|
||||
*
|
||||
* @param \think\Request $request
|
||||
* @return \think\Response
|
||||
*/
|
||||
public function save()
|
||||
{
|
||||
if ($this->request->isPost()) {
|
||||
$params = $this->request->post();
|
||||
validate(PowerValidate::class)->check($params);
|
||||
try {
|
||||
Db::transaction(function () use ($params) {
|
||||
$data = PowerModel::create($params);
|
||||
});
|
||||
$this->result->success($params, '保存成功');
|
||||
} catch (ValidateException $e) {
|
||||
$this->result->error('保存失败: ' . $e->getMessage(), 2, $params);
|
||||
} catch (\Throwable $th) {
|
||||
$this->result->error('保存失败: ' . $th->getMessage(), 1, $params);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 显示编辑权限表单页.
|
||||
*
|
||||
* @param \think\Request $request
|
||||
* @return \think\Response
|
||||
*/
|
||||
public function edit($id = null)
|
||||
{
|
||||
$id = $this->request->param('id');
|
||||
$power = PowerModel::find($id);
|
||||
if (! $power) {
|
||||
$this->result->error('权限不存在');
|
||||
}
|
||||
if ($this->request->isAjax()) {
|
||||
|
||||
$powers = PowerModel::cateTree(PowerModel::select()->toArray());
|
||||
$this->result->success(['power' => $powers, 'info' => $power]);
|
||||
}
|
||||
//View::assign('power', $power);
|
||||
//return View::fetch();
|
||||
}
|
||||
|
||||
/**
|
||||
* 显示编辑权限表单页.
|
||||
*
|
||||
* @param \think\Request $request
|
||||
* @return \think\Response
|
||||
*/
|
||||
public function update($id = null)
|
||||
{
|
||||
$id = $id ? $id : $this->request->param('id');
|
||||
if ($this->request->isAjax() && $this->request->isPut()) {
|
||||
$params = $this->request->param();
|
||||
validate(PowerValidate::class)->check($params);
|
||||
try {
|
||||
$res = Db::transaction(function () use ($params, $id) {
|
||||
$power = PowerModel::find($id);
|
||||
if (! $power) {
|
||||
throw new ValidateException('权限不存在');
|
||||
}
|
||||
$power->save($params);
|
||||
});
|
||||
$this->result->success($res, '更新成功');
|
||||
} catch (ValidateException $e) {
|
||||
$this->result->error('更新失败: ' . $e->getMessage());
|
||||
} catch (\Throwable $th) {
|
||||
$this->result->error('更新失败: ' . $th->getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 显示回收站列表
|
||||
*
|
||||
* @param \think\Request $request
|
||||
* @return \think\Response
|
||||
*/
|
||||
public function recyclebin()
|
||||
{
|
||||
$page = $this->request->param('page', 1);
|
||||
$size = $this->request->param('size', 15);
|
||||
if ($this->request->isAjax()) {
|
||||
$data = PowerModel::onlyTrashed()->paginate([
|
||||
'page' => $page,
|
||||
'list_rows' => $size,
|
||||
]);
|
||||
$this->result->success($data->items());
|
||||
}
|
||||
View::assign('title', '回收站');
|
||||
return View::fetch('power/index');
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除权限
|
||||
*
|
||||
* @param \think\Request $request
|
||||
* @return \think\Response
|
||||
*/
|
||||
public function delete()
|
||||
{
|
||||
if ($this->request->isAjax() && $this->request->isDelete()) {
|
||||
$ids = $this->request->param('ids', '');
|
||||
$force = $this->request->param('force', false);
|
||||
if (empty($ids)) {
|
||||
$this->result->error('请选择要删除的数据');
|
||||
}
|
||||
try {
|
||||
Db::transaction(function () use ($ids, $force) {
|
||||
if ($force) {
|
||||
PowerModel::onlyTrashed()->whereIn('id', $ids)->select()->each(function ($item) {
|
||||
$item->force()->delete();
|
||||
});
|
||||
} else {
|
||||
PowerModel::destroy($ids);
|
||||
}
|
||||
});
|
||||
$this->result->success();
|
||||
} catch (\think\exception\HttpResponseException $e) {
|
||||
throw $e; // 不要 return,不要吞掉!
|
||||
} catch (\Throwable $th) {
|
||||
$this->result->error('删除失败: ' . $th->getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 还原权限
|
||||
*
|
||||
* @param \think\Request $request
|
||||
* @return \think\Response
|
||||
*/
|
||||
public function restore($ids = null)
|
||||
{
|
||||
if ($this->request->isAjax() && $this->request->isPut()) {
|
||||
$ids = $this->request->param('ids', '');
|
||||
if (empty($ids)) {
|
||||
$this->result->error('请选择要还原的数据');
|
||||
}
|
||||
$idsArray = explode(',', $ids);
|
||||
try {
|
||||
Db::transaction(function () use ($idsArray) {
|
||||
PowerModel::onlyTrashed()->whereIn('id', $idsArray)->select()->each(function ($item) {
|
||||
$item->restore();
|
||||
});
|
||||
});
|
||||
$this->result->success();
|
||||
} catch (\think\exception\HttpResponseException $e) {
|
||||
throw $e; // 不要 return,不要吞掉!
|
||||
} catch (\Throwable $th) {
|
||||
$this->result->error('还原失败: ' . $th->getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// 更新角色权限
|
||||
|
||||
public function updateRolePowers($roleId): Response
|
||||
{
|
||||
$powerIds = $this->request->post('powerIds', []);
|
||||
|
||||
Db::startTrans();
|
||||
try {
|
||||
// 删除旧的角色权限关联
|
||||
BackendRolePower::where('role_id', $roleId)->delete();
|
||||
|
||||
// 添加新的角色权限关联
|
||||
if (! empty($powerIds)) {
|
||||
$rolePowerData = [];
|
||||
foreach ($powerIds as $powerId) {
|
||||
$rolePowerData[] = [
|
||||
'role_id' => $roleId,
|
||||
'power_id' => $powerId,
|
||||
'create_at' => date('Y-m-d H:i:s'),
|
||||
];
|
||||
}
|
||||
(new BackendRolePower())->saveAll($rolePowerData);
|
||||
}
|
||||
|
||||
Db::commit();
|
||||
return json(['code' => 200, 'message' => '权限分配成功']);
|
||||
} catch (\Exception $e) {
|
||||
Db::rollback();
|
||||
return json(['code' => 500, 'message' => '权限分配失败: ' . $e->getMessage()]);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,173 @@
|
||||
<?php
|
||||
/*
|
||||
* @Author: YwxApp <ywx@ywxapp.cn>
|
||||
* @Date: 2026-04-29 02:32:33
|
||||
* @LastEditors: YwxApp <ywx@ywxapp.cn>
|
||||
* @LastEditTime: 2026-07-21 23:20:37
|
||||
* @Description:
|
||||
* @FilePath: \ywxapp_dev\app\backend\controller\Profile.php
|
||||
* @CustomString: Copyright (c) 2026 YwxApp
|
||||
*/
|
||||
|
||||
declare (strict_types = 1);
|
||||
namespace app\backend\controller;
|
||||
|
||||
use think\Request;
|
||||
use ywxapp\controller\BackendBase;
|
||||
use ywxapp\model\BackendAdmin as AdminModel;
|
||||
use ywxapp\service\FileStorageService;
|
||||
|
||||
/**
|
||||
* Profile 类
|
||||
*
|
||||
* @author ywxapp <admin@ywxapp.cn>
|
||||
*/
|
||||
class Profile extends BackendBase
|
||||
{
|
||||
/**
|
||||
* Summary of needLogin
|
||||
* @var array
|
||||
*/
|
||||
protected $noNeedLogin = [];
|
||||
|
||||
/**
|
||||
* Summary of needRight
|
||||
* @var array
|
||||
*/
|
||||
protected $noNeedVerify = ['*'];
|
||||
|
||||
/**
|
||||
* 控制器初始化 initialize
|
||||
* @return void
|
||||
*/
|
||||
protected function initialize()
|
||||
{
|
||||
$this->model = new AdminModel;
|
||||
}
|
||||
|
||||
/**
|
||||
* 个人资料页
|
||||
*/
|
||||
public function index()
|
||||
{
|
||||
$info = $this->auth->info;
|
||||
$this->assign('info', $info);
|
||||
return $this->fetch();
|
||||
}
|
||||
|
||||
/**
|
||||
* 头像设置页 / 头像上传
|
||||
*/
|
||||
public function avatar()
|
||||
{
|
||||
if ($this->request->isPost()) {
|
||||
$file = $this->request->file('file');
|
||||
if (! $file || ! $file->isValid()) {
|
||||
$this->result->error('请选择上传文件', 400);
|
||||
}
|
||||
$type = $this->request->param('type', 'avatar');
|
||||
$storage = new FileStorageService();
|
||||
$result = $storage->upload($file, $type . '/' . date('Ymd'));
|
||||
$info = $this->auth->model;
|
||||
if ($info) {
|
||||
$info->avatar = $result['storage'] == 'local' ? '/storage/' . $result['path'] : $result['url'];
|
||||
$info->save();
|
||||
$this->result->success(['src' => $info->avatar], '头像上传成功');
|
||||
} else {
|
||||
$this->result->error($file->getError(), 500);
|
||||
}
|
||||
}
|
||||
return $this->fetch();
|
||||
}
|
||||
|
||||
/**
|
||||
* 修改密码页
|
||||
*/
|
||||
public function password()
|
||||
{
|
||||
return $this->fetch();
|
||||
}
|
||||
|
||||
/**
|
||||
* 保存新密码
|
||||
*/
|
||||
public function passwordSave()
|
||||
{
|
||||
if (! $this->request->isPost()) {
|
||||
$this->result->error('请求方式错误');
|
||||
}
|
||||
$old = (string) $this->request->param('old_password', '');
|
||||
$new = (string) $this->request->param('password', '');
|
||||
$new2 = (string) $this->request->param('password_confirm', '');
|
||||
|
||||
if ($new === '' || $new !== $new2) {
|
||||
$this->result->error('两次新密码不一致或为空');
|
||||
}
|
||||
$info = $this->auth->model;
|
||||
if (! $info->checkPassword($old)) {
|
||||
$this->result->error('原密码错误');
|
||||
}
|
||||
$info->resetPassword($new);
|
||||
$this->result->success('密码修改成功');
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取当前管理员资料
|
||||
*/
|
||||
public function read($id)
|
||||
{
|
||||
$id = $id ?: ($this->auth->info['id'] ?? 0);
|
||||
$info = AdminModel::with(['roles'])->find($id);
|
||||
if (! $info) {
|
||||
$this->result->error('管理员不存在', 404);
|
||||
}
|
||||
$this->result->success(['info' => $info]);
|
||||
}
|
||||
|
||||
/**
|
||||
* 编辑表单数据
|
||||
*/
|
||||
public function edit($id = null)
|
||||
{
|
||||
$id = $id ?: ($this->auth->info['id'] ?? 0);
|
||||
$info = AdminModel::with(['roles'])->find($id);
|
||||
if (! $info) {
|
||||
$this->result->error('管理员不存在', 404);
|
||||
}
|
||||
$this->result->success(['info' => $info]);
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新个人资料(昵称/邮箱/头像/手机/密码)
|
||||
*/
|
||||
public function update(Request $request, $id)
|
||||
{
|
||||
$id = $id ?: ($this->auth->info['id'] ?? 0);
|
||||
$info = AdminModel::find($id);
|
||||
if (! $info) {
|
||||
$this->result->error('管理员不存在', 404);
|
||||
}
|
||||
$data = $request->only(['nickname', 'email', 'avatar', 'mobile'], 'put');
|
||||
if ($request->param('password')) {
|
||||
$info->password = $request->param('password'); // 模型自动哈希
|
||||
}
|
||||
$info->save($data);
|
||||
$this->result->success($info, '资料更新成功');
|
||||
}
|
||||
|
||||
/**
|
||||
* 个人资料不支持新建
|
||||
*/
|
||||
public function save(Request $request)
|
||||
{
|
||||
$this->result->error('个人资料不支持该操作');
|
||||
}
|
||||
|
||||
/**
|
||||
* 个人资料不支持删除
|
||||
*/
|
||||
public function delete($id)
|
||||
{
|
||||
$this->result->error('个人资料不支持该操作');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,158 @@
|
||||
<?php
|
||||
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\Prop as PropModel;
|
||||
use app\backend\validate\Prop as PropValidate;
|
||||
|
||||
/**
|
||||
* 道具中心管理
|
||||
*/
|
||||
class Prop extends BackendBase
|
||||
{
|
||||
protected $noNeedLogin = [];
|
||||
protected $noNeedVerify = [];
|
||||
|
||||
protected function initialize()
|
||||
{
|
||||
$this->model = new PropModel();
|
||||
}
|
||||
|
||||
public function index()
|
||||
{
|
||||
$page = $this->request->param('page/d', 1);
|
||||
$limit = $this->request->param('limit/d', 15);
|
||||
$title = $this->request->param('title', '');
|
||||
|
||||
if ($this->request->isAjax()) {
|
||||
$query = $this->model->newQuery();
|
||||
if ($title !== '') {
|
||||
$query->where('title', 'like', '%' . $title . '%');
|
||||
}
|
||||
$list = $query->order('sort', 'desc')
|
||||
->paginate(['page' => $page, 'list_rows' => $limit]);
|
||||
$this->result->setCount($list->total())->success($list->items());
|
||||
}
|
||||
return $this->fetch();
|
||||
}
|
||||
|
||||
public function save()
|
||||
{
|
||||
if (! $this->request->isPost()) {
|
||||
$this->result->error('请求方式错误', 405);
|
||||
}
|
||||
$params = $this->request->only(['title', 'icon', 'price', 'description', 'sort', 'status'], 'post');
|
||||
try {
|
||||
validate(PropValidate::class)->check($params);
|
||||
$this->model->create($params);
|
||||
$this->result->success('', '添加成功');
|
||||
} catch (ValidateException $e) {
|
||||
$this->result->error($e->getMessage());
|
||||
} catch (DbException $e) {
|
||||
$this->result->error('添加失败: ' . $e->getMessage(), 2);
|
||||
}
|
||||
}
|
||||
|
||||
public function edit($id = null)
|
||||
{
|
||||
$id = $id ?: $this->request->param('id');
|
||||
$info = $this->model->find($id);
|
||||
if (! $info) {
|
||||
$this->result->error('记录不存在', 404);
|
||||
}
|
||||
$this->result->success(['info' => $info]);
|
||||
}
|
||||
|
||||
public function update()
|
||||
{
|
||||
if (! ($this->request->isAjax() && $this->request->isPut())) {
|
||||
$this->result->error('请求方式错误', 405);
|
||||
}
|
||||
$params = $this->request->param();
|
||||
$id = $params['id'] ?? null;
|
||||
$info = $this->model->find($id);
|
||||
if (! $info) {
|
||||
$this->result->error('记录不存在', 404);
|
||||
}
|
||||
$statusOnly = isset($params['status'])
|
||||
&& count(array_diff(array_keys($params), ['id', 'status'])) === 0;
|
||||
if (! $statusOnly) {
|
||||
try {
|
||||
validate(PropValidate::class)->check($params);
|
||||
} catch (ValidateException $e) {
|
||||
$this->result->error($e->getMessage());
|
||||
}
|
||||
}
|
||||
$info->save($params);
|
||||
$this->result->success($info, '更新成功');
|
||||
}
|
||||
|
||||
public function recyclebin()
|
||||
{
|
||||
$page = $this->request->param('page/d', 1);
|
||||
$limit = $this->request->param('limit/d', 15);
|
||||
if ($this->request->isAjax()) {
|
||||
$list = $this->model->onlyTrashed()
|
||||
->order('sort', 'desc')
|
||||
->paginate(['page' => $page, 'list_rows' => $limit]);
|
||||
$this->result->setCount($list->total())->success($list->items());
|
||||
}
|
||||
return $this->fetch('prop/index');
|
||||
}
|
||||
|
||||
public function delete()
|
||||
{
|
||||
if ($this->request->isAjax() && $this->request->isDelete()) {
|
||||
$ids = $this->request->param('ids', '');
|
||||
$force = $this->request->param('force', false);
|
||||
if (empty($ids)) {
|
||||
$this->result->error('请选择要删除的数据');
|
||||
}
|
||||
$idsArray = array_filter(explode(',', $ids), 'is_numeric');
|
||||
if (empty($idsArray)) {
|
||||
$this->result->error('参数错误');
|
||||
}
|
||||
try {
|
||||
Db::transaction(function () use ($idsArray, $force) {
|
||||
if ($force) {
|
||||
$this->model->onlyTrashed()->whereIn('id', $idsArray)->select()
|
||||
->each(function ($item) { $item->force()->delete(); });
|
||||
} else {
|
||||
$this->model->destroy($idsArray);
|
||||
}
|
||||
});
|
||||
$this->result->success();
|
||||
} catch (\think\exception\HttpResponseException $e) {
|
||||
throw $e;
|
||||
} catch (\Exception $e) {
|
||||
$this->result->error('删除失败: ' . ($e->getMessage() ?: $e->__toString()), 500);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public function restore($ids = '')
|
||||
{
|
||||
if ($this->request->isAjax() && $this->request->isPost()) {
|
||||
$ids = $this->request->param('ids', '');
|
||||
if (empty($ids)) {
|
||||
$this->result->error('请选择要恢复的数据');
|
||||
}
|
||||
$idsArray = array_filter(explode(',', $ids), 'is_numeric');
|
||||
Db::startTrans();
|
||||
try {
|
||||
$this->model->withTrashed()->where('id', 'in', $idsArray)->select()
|
||||
->each(function ($item) { $item->restore(); });
|
||||
Db::commit();
|
||||
$this->result->success('恢复成功');
|
||||
} catch (DbException $e) {
|
||||
Db::rollback();
|
||||
$this->result->error('恢复失败: ' . $e->getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,321 @@
|
||||
<?php
|
||||
/*
|
||||
* @Author: YwxApp <ywx@ywxapp.cn>
|
||||
* @Date: 2026-04-29 02:32:33
|
||||
* @LastEditors: YwxApp <ywx@ywxapp.cn>
|
||||
* @LastEditTime: 2026-07-21 23:20:59
|
||||
* @Description:
|
||||
* @FilePath: \ywxapp_dev\app\backend\controller\Role.php
|
||||
* @CustomString: Copyright (c) 2026 YwxApp
|
||||
*/
|
||||
|
||||
declare (strict_types = 1);
|
||||
namespace app\backend\controller;
|
||||
|
||||
use app\backend\validate\AdminRole as RoleValidate;
|
||||
use think\facade\Db;
|
||||
use think\facade\View;
|
||||
use think\Request;
|
||||
use ywxapp\model\BackendPower as PermissionModel;
|
||||
use ywxapp\model\BackendRole as RoleModel;
|
||||
use ywxapp\model\BackendRolePower as RolePowerModel;
|
||||
use ywxapp\controller\BackendBase;
|
||||
|
||||
/**
|
||||
* Role 类
|
||||
*
|
||||
* @author ywxapp <admin@ywxapp.cn>
|
||||
*/
|
||||
class Role extends BackendBase
|
||||
{
|
||||
/**
|
||||
* Summary of needLogin
|
||||
* @var array
|
||||
*/
|
||||
protected $noNeedLogin = [];
|
||||
|
||||
/**
|
||||
* Summary of needRight
|
||||
* @var array
|
||||
*/
|
||||
protected $noNeedVerify = [];
|
||||
|
||||
/**
|
||||
* 控制器初始化 initialize
|
||||
* @return void
|
||||
*/
|
||||
protected function initialize()
|
||||
{}
|
||||
|
||||
/**
|
||||
* 获取用户列表
|
||||
*/
|
||||
public function index()
|
||||
{
|
||||
$page = $this->request->param('page/d', 1);
|
||||
$limit = $this->request->param('limit/d', 15);
|
||||
if ($this->request->isAjax()) {
|
||||
$admins = RoleModel::field('id,name,title,status,description,create_at,update_at')
|
||||
->page($page, $limit)
|
||||
->select();
|
||||
$this->result->success($admins);
|
||||
}
|
||||
View::assign('title', '登录');
|
||||
return View::fetch();
|
||||
}
|
||||
|
||||
/**
|
||||
* 数据创建
|
||||
*
|
||||
* @param Request $request
|
||||
* @return \think\Response
|
||||
*/
|
||||
public function create()
|
||||
{
|
||||
if ($this->request->isAjax()) {
|
||||
$roles = RoleModel::field('id,name,title,status')->where('status', 1)->select();
|
||||
$this->result->success(['roles' => $roles]);
|
||||
}
|
||||
View::assign('title', '登录');
|
||||
return View::fetch('role/index');
|
||||
}
|
||||
|
||||
/**
|
||||
* 数据保存
|
||||
*
|
||||
* @param Request $request
|
||||
* @return \think\Response
|
||||
*/
|
||||
public function save()
|
||||
{
|
||||
$params = $this->request->only(['name', 'title', 'status', 'description'], 'post');
|
||||
try {
|
||||
validate(RoleValidate::class)->check($params);
|
||||
Db::transaction(function () use ($params) {
|
||||
$data = RoleModel::create($params);
|
||||
});
|
||||
$this->result->success();
|
||||
} catch (ValidateException $e) {
|
||||
$this->result->error($e->getMessage());
|
||||
} catch (DbException $e) {
|
||||
$this->result->error($e->getMessage(), 2);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 数据编辑
|
||||
*
|
||||
* @param Request $request
|
||||
* @param int|null $ids
|
||||
* @return \think\Response
|
||||
*/
|
||||
public function edit($id = null)
|
||||
{
|
||||
if ($this->request->isAjax()) {
|
||||
$data = RoleModel::where('id', $id)->find();
|
||||
$this->result->success(['info' => $data]);
|
||||
}
|
||||
View::assign('title', '登录');
|
||||
return View::fetch('role/index');
|
||||
}
|
||||
|
||||
/**
|
||||
* 数据更新
|
||||
*
|
||||
* @param Request $request
|
||||
* @param int|null $ids
|
||||
* @return \think\Response
|
||||
*/
|
||||
public function update()
|
||||
{
|
||||
$params = $this->request->only(['id', 'name', 'title', 'status', 'description'], 'put');
|
||||
$info = RoleModel::find($params['id']);
|
||||
if (! $info) {
|
||||
$this->result->error('角色不存在', 404);
|
||||
}
|
||||
// 禁止编辑超级管理员角色
|
||||
if ($params['id'] == config('ywxapp.superAdmin', 1) && $info->name === 'superadmin') {
|
||||
$this->result->error('超级管理员角色不可编辑', 403);
|
||||
}
|
||||
$info->save($params);
|
||||
$this->result->success($info, "角色更新成功");
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取详情
|
||||
*/
|
||||
public function read()
|
||||
{
|
||||
$info = RoleModel::with(['roles', 'permissions'])
|
||||
->find($id);
|
||||
if (! $info) {
|
||||
$this->result->error('角色不存在', 404);
|
||||
}
|
||||
$this->result->success($info, "角色更新成功");
|
||||
}
|
||||
|
||||
/**
|
||||
* 数据回收站
|
||||
*
|
||||
* @param Request $request
|
||||
* @return \think\Response
|
||||
*/
|
||||
public function recyclebin()
|
||||
{
|
||||
$page = $this->request->param('page', 1);
|
||||
$size = $this->request->param('size', 15);
|
||||
if ($this->request->isAjax()) {
|
||||
$roles = RoleModel::onlyTrashed()
|
||||
->paginate([
|
||||
'page' => $page,
|
||||
'list_rows' => $size,
|
||||
]);
|
||||
$this->result->success($roles->items());
|
||||
}
|
||||
View::assign('title', '回收站');
|
||||
return View::fetch('role/index');
|
||||
}
|
||||
|
||||
/**
|
||||
* 数据删除
|
||||
*/
|
||||
public function delete()
|
||||
{
|
||||
if ($this->request->isAjax() && $this->request->isDelete()) {
|
||||
$ids = $this->request->param('ids', '');
|
||||
$force = $this->request->param('force', false);
|
||||
if (empty($ids)) {
|
||||
$this->result->error('请选择要删除的数据');
|
||||
}
|
||||
$idsArray = explode(',', $ids);
|
||||
// 禁止删除超级管理员角色
|
||||
if (in_array((int)config('ywxapp.superAdmin', 1), array_map('intval', $idsArray), true)) {
|
||||
$this->result->error('超级管理员角色不可删除', 403);
|
||||
}
|
||||
try {
|
||||
Db::transaction(function () use ($idsArray, $force) {
|
||||
if ($force) {
|
||||
RoleModel::onlyTrashed()->whereIn('id', $idsArray)->select()->each(function ($item) {
|
||||
$item->permissions()->detach();
|
||||
$item->force()->delete();
|
||||
});
|
||||
} else {
|
||||
RoleModel::destroy($idsArray);
|
||||
}
|
||||
});
|
||||
$this->result->success();
|
||||
} catch (\think\exception\HttpResponseException $e) {
|
||||
throw $e; // 不要 return,不要吞掉!
|
||||
} catch (\Exception $e) {
|
||||
\think\facade\Log::error('批量删除管理员失败', [
|
||||
'exception' => $e->__toString(),
|
||||
'admin_ids' => $idsArray,
|
||||
]);
|
||||
$this->result->error('删除失败: ' . ($e->getMessage() ?: $e->__toString()), 500);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public function restore($ids = '')
|
||||
{
|
||||
if ($this->request->isAjax() && $this->request->isPut()) {
|
||||
$ids = $this->request->param('ids', '');
|
||||
if (empty($ids)) {
|
||||
$this->result->error('请选择要恢复的数据');
|
||||
}
|
||||
$idsArray = explode(',', $ids);
|
||||
Db::startTrans();
|
||||
try {
|
||||
RoleModel::withTrashed()
|
||||
->where('id', 'in', $idsArray)
|
||||
->select()
|
||||
->each(function ($item) {
|
||||
$item->restore();
|
||||
});
|
||||
Db::commit();
|
||||
$this->result->success('恢复成功');
|
||||
} catch (DbException $e) {
|
||||
Db::rollback();
|
||||
$this->result->error('恢复失败: ' . $e->getMessage());
|
||||
}
|
||||
}
|
||||
$this->result->error('请求错误! ' );
|
||||
}
|
||||
|
||||
|
||||
public function permission()
|
||||
{
|
||||
$id = $this->request->param('id', 0);
|
||||
if ($this->request->isGet()) {
|
||||
$permissions = PermissionModel::field('id,pid,name,code,type,status,sort')
|
||||
->where('status', 1)
|
||||
->order('sort', 'asc')
|
||||
->select()
|
||||
->toArray();
|
||||
$ownedPermissions = RolePowerModel::where('role_id', $id)->column('power_id');
|
||||
// 超级管理员角色:权限恒为 *(全部),前端展示为全部勾选且不可编辑
|
||||
$role = RoleModel::field('id,name')->find($id);
|
||||
$isSuper = $role && ($role->name === 'superadmin' || $id == config('ywxapp.superAdmin', 1));
|
||||
if ($isSuper) {
|
||||
$ownedPermissions = array_column($permissions, 'id');
|
||||
}
|
||||
$treeData = $this->buildTreeWithChecked($permissions, $ownedPermissions);
|
||||
$this->result->success($treeData, $isSuper ? '超级管理员拥有全部权限(*)' : '获取成功');
|
||||
}
|
||||
|
||||
if ($this->request->isAjax() && $this->request->isPut()) {
|
||||
// 禁止修改超级管理员权限
|
||||
$role = RoleModel::field('id,name')->find($id);
|
||||
if ($role && ($role->name === 'superadmin' || $id == config('ywxapp.superAdmin', 1))) {
|
||||
$this->result->error('超级管理员权限不可修改', 403);
|
||||
}
|
||||
$permissionIds = $this->request->param('permissions/a', []);
|
||||
try {
|
||||
Db::startTrans();
|
||||
RolePowerModel::where('role_id', $id)->delete();
|
||||
if (!empty($permissionIds)) {
|
||||
$batchData = [];
|
||||
foreach ($permissionIds as $permissionId) {
|
||||
$batchData[] = [
|
||||
'role_id' => $id,
|
||||
'power_id' => $permissionId,
|
||||
];
|
||||
}
|
||||
// 批量插入,提升性能
|
||||
RolePowerModel::insertAll($batchData);
|
||||
}
|
||||
Db::commit();
|
||||
$this->result->success([], '权限分配成功');
|
||||
} catch (\Exception $e) {
|
||||
Db::rollback();
|
||||
$this->result->error('保存失败:' . $e->getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private function buildTreeWithChecked($items, $checkedIds, $parentId = 0)
|
||||
{
|
||||
$tree = [];
|
||||
foreach ($items as $item) {
|
||||
if ($item['pid'] == $parentId) {
|
||||
$isChecked = in_array($item['id'], $checkedIds);
|
||||
$children = $this->buildTreeWithChecked($items, $checkedIds, $item['id']);
|
||||
$item['spread'] = true;
|
||||
$item['checked'] = $isChecked;
|
||||
$item['children'] = $children;
|
||||
$tree[] = $item;
|
||||
// $tree[] = [
|
||||
// 'id' => $item['id'],
|
||||
// 'title' => $item['name'],
|
||||
// 'spread' => true,
|
||||
// 'checked' => $isChecked, // 设置选中状态
|
||||
// 'children' => $children,
|
||||
// ];
|
||||
}
|
||||
}
|
||||
return $tree;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,228 @@
|
||||
<?php
|
||||
/*
|
||||
* @Author: YwxApp <ywx@ywxapp.cn>
|
||||
* @Date: 2026-04-29 02:32:33
|
||||
* @LastEditors: YwxApp <ywx@ywxapp.cn>
|
||||
* @LastEditTime: 2026-07-21 23:21:38
|
||||
* @Description:
|
||||
* @FilePath: \ywxapp_dev\app\backend\controller\Rule.php
|
||||
* @CustomString: Copyright (c) 2026 YwxApp
|
||||
*/
|
||||
|
||||
declare (strict_types = 1);
|
||||
namespace app\backend\controller;
|
||||
|
||||
use think\facade\Db;
|
||||
use think\facade\View;
|
||||
use think\facade\Request;
|
||||
use ywxapp\controller\BackendBase;
|
||||
use ywxapp\model\MemberRule as RuleModel;
|
||||
use app\backend\validate\UserRule as RuleValidate;
|
||||
use think\exception\ValidateException;
|
||||
use ywxapp\model\MemberGroupRule;
|
||||
|
||||
/**
|
||||
* Rule 类
|
||||
*
|
||||
* @author ywxapp <admin@ywxapp.cn>
|
||||
*/
|
||||
class Rule extends BackendBase
|
||||
{
|
||||
/**
|
||||
* Summary of needLogin
|
||||
* @var array
|
||||
*/
|
||||
protected $noNeedLogin = [];
|
||||
|
||||
/**
|
||||
* Summary of needRight
|
||||
* @var array
|
||||
*/
|
||||
protected $noNeedVerify = [];
|
||||
|
||||
/**
|
||||
* 控制器初始化 initialize
|
||||
* @return void
|
||||
*/
|
||||
protected function initialize() {}
|
||||
|
||||
/**
|
||||
* 显示资源列表
|
||||
*
|
||||
* @return \think\Response
|
||||
*/
|
||||
public function index()
|
||||
{
|
||||
$page = $this->request->param('page', 1);
|
||||
$size = $this->request->param('size', 15);
|
||||
if ($this->request->isAjax()) {
|
||||
$data = RuleModel::cache(false)->paginate([
|
||||
'page' => $page,
|
||||
'list_rows' => $size,
|
||||
]);
|
||||
$this->result->success($data->items());
|
||||
}
|
||||
View::assign('title', '登录');
|
||||
return View::fetch();
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建权限
|
||||
*
|
||||
* @return \think\Response
|
||||
*/
|
||||
public function create()
|
||||
{
|
||||
if ($this->request->isAjax()) {
|
||||
$power = RuleModel::cateTree(RuleModel::select()->toArray());
|
||||
$this->result->success(['power' => $power]);
|
||||
}
|
||||
//
|
||||
}
|
||||
|
||||
/**
|
||||
* 保存权限
|
||||
*
|
||||
* @return \think\Response
|
||||
*/
|
||||
public function save()
|
||||
{
|
||||
if ($this->request->isPost()) {
|
||||
$params = $this->request->post();
|
||||
Db::startTrans();
|
||||
try {
|
||||
validate(RuleValidate::class)->check($params);
|
||||
$data = RuleModel::create($params);
|
||||
Db::commit();
|
||||
$this->result->success($params, '保存成功');
|
||||
} catch (ValidateException $e) {
|
||||
Db::rollback();
|
||||
$this->result->error('保存失败: ' . $e->getMessage(), 2, $params);
|
||||
} catch (\Throwable $th) {
|
||||
Db::rollback();
|
||||
$this->result->error('保存失败: ' . $th->getMessage(), 1, $params);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 显示编辑权限表单页.
|
||||
*
|
||||
* @param \think\Request $request
|
||||
* @return \think\Response
|
||||
*/
|
||||
public function edit($id = null)
|
||||
{
|
||||
$id = $this->request->param('id');
|
||||
$power = RuleModel::find($id);
|
||||
if (! $power) {
|
||||
$this->result->error('权限不存在');
|
||||
}
|
||||
if ($this->request->isAjax()) {
|
||||
$powers = RuleModel::cateTree(RuleModel::select()->toArray());
|
||||
$this->result->success(['power' => $powers, 'info' => $power]);
|
||||
}
|
||||
//View::assign('power', $power);
|
||||
//return View::fetch();
|
||||
}
|
||||
|
||||
/**
|
||||
* 显示编辑权限表单页.
|
||||
*
|
||||
* @param \think\Request $request
|
||||
* @return \think\Response
|
||||
*/
|
||||
public function update($id = null)
|
||||
{
|
||||
$id = $id ? $id : $this->request->param('id');
|
||||
if ($this->request->isAjax() && $this->request->isPut()) {
|
||||
$params = $this->request->param();
|
||||
Db::startTrans();
|
||||
try {
|
||||
validate(RuleValidate::class)->check($params);
|
||||
|
||||
$power = RuleModel::find($id);
|
||||
if (! $power) {
|
||||
throw new ValidateException('权限不存在');
|
||||
}
|
||||
$power->save($params);
|
||||
|
||||
Db::commit();
|
||||
$this->result->success($power, '更新成功');
|
||||
} catch (ValidateException $e) {
|
||||
Db::rollback();
|
||||
$this->result->error('验证失败: ' . $e->getMessage());
|
||||
} catch (\Exception $e) {
|
||||
Db::rollback();
|
||||
$this->result->error('位置错误: ' . $e->getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 显示回收站列表
|
||||
*
|
||||
* @param \think\Request $request
|
||||
* @return \think\Response
|
||||
*/
|
||||
public function recyclebin()
|
||||
{
|
||||
$page = $this->request->param('page', 1);
|
||||
$size = $this->request->param('size', 15);
|
||||
if ($this->request->isAjax()) {
|
||||
$data = RuleModel::onlyTrashed()->paginate([
|
||||
'page' => $page,
|
||||
'list_rows' => $size,
|
||||
]);
|
||||
$this->result->success($data->items());
|
||||
}
|
||||
View::assign('title', '回收站');
|
||||
return View::fetch('rule/index');
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除权限
|
||||
*
|
||||
* @param \think\Request $request
|
||||
* @return \think\Response
|
||||
*/
|
||||
public function delete()
|
||||
{
|
||||
if ($this->request->isAjax() && $this->request->isDelete()) {
|
||||
$ids = $this->request->param('ids', '');
|
||||
$force = $this->request->param('force', false);
|
||||
if (empty($ids)) {
|
||||
$this->result->error('请选择要删除的数据');
|
||||
}
|
||||
try {
|
||||
Db::transaction(function () use ($ids, $force) {
|
||||
if ($force) {
|
||||
RuleModel::onlyTrashed()->whereIn('id', $ids)->select()->each(function ($item) {
|
||||
$item->force()->delete();
|
||||
});
|
||||
} else {
|
||||
RuleModel::destroy($ids);
|
||||
}
|
||||
});
|
||||
$this->result->success();
|
||||
} catch (\think\exception\HttpResponseException $e) {
|
||||
throw $e; // 不要 return,不要吞掉!
|
||||
} catch (\Exception $e) {
|
||||
$this->result->error('删除失败: ' . $e->getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 获取角色权限
|
||||
|
||||
public function rolePowers($roleId)
|
||||
{
|
||||
$powerIds = MemberGroupRule::where('gid', $roleId)->column('rid');
|
||||
|
||||
return json([
|
||||
'code' => 200,
|
||||
'message' => 'success',
|
||||
'data' => $powerIds,
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,185 @@
|
||||
<?php
|
||||
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\Score as ScoreModel;
|
||||
use app\backend\validate\Score as ScoreValidate;
|
||||
|
||||
/**
|
||||
* 积分规则 & 流水管理
|
||||
*/
|
||||
class Score extends BackendBase
|
||||
{
|
||||
protected $noNeedLogin = [];
|
||||
protected $noNeedVerify = [];
|
||||
|
||||
protected function initialize()
|
||||
{
|
||||
$this->model = new ScoreModel();
|
||||
// 触发 score_rule / score_log 自愈
|
||||
ScoreModel::ensureSchema();
|
||||
}
|
||||
|
||||
// ============ 积分规则 ============
|
||||
public function index()
|
||||
{
|
||||
$page = $this->request->param('page/d', 1);
|
||||
$limit = $this->request->param('limit/d', 15);
|
||||
$name = $this->request->param('name', '');
|
||||
|
||||
if ($this->request->isAjax()) {
|
||||
$query = $this->model->newQuery();
|
||||
if ($name !== '') {
|
||||
$query->where('name', 'like', '%' . $name . '%');
|
||||
}
|
||||
$list = $query->order('sort', 'desc')
|
||||
->paginate(['page' => $page, 'list_rows' => $limit]);
|
||||
$typeList = ScoreModel::typeList();
|
||||
$items = $list->items();
|
||||
foreach ($items as &$item) {
|
||||
$item['type_text'] = $typeList[$item['type']] ?? '-';
|
||||
}
|
||||
$this->result->setCount($list->total())->success($items);
|
||||
}
|
||||
$this->assign('typeList', ScoreModel::typeList());
|
||||
return $this->fetch();
|
||||
}
|
||||
|
||||
public function save()
|
||||
{
|
||||
if (! $this->request->isPost()) {
|
||||
$this->result->error('请求方式错误', 405);
|
||||
}
|
||||
$params = $this->request->only(['name', 'action', 'type', 'value', 'sort', 'status'], 'post');
|
||||
try {
|
||||
validate(ScoreValidate::class)->check($params);
|
||||
$this->model->create($params);
|
||||
$this->result->success('', '添加成功');
|
||||
} catch (ValidateException $e) {
|
||||
$this->result->error($e->getMessage());
|
||||
} catch (DbException $e) {
|
||||
$this->result->error('添加失败: ' . $e->getMessage(), 2);
|
||||
}
|
||||
}
|
||||
|
||||
public function edit($id = null)
|
||||
{
|
||||
$id = $id ?: $this->request->param('id');
|
||||
$info = $this->model->find($id);
|
||||
if (! $info) {
|
||||
$this->result->error('记录不存在', 404);
|
||||
}
|
||||
$this->result->success(['info' => $info]);
|
||||
}
|
||||
|
||||
public function update()
|
||||
{
|
||||
if (! ($this->request->isAjax() && $this->request->isPut())) {
|
||||
$this->result->error('请求方式错误', 405);
|
||||
}
|
||||
$params = $this->request->param();
|
||||
$id = $params['id'] ?? null;
|
||||
$info = $this->model->find($id);
|
||||
if (! $info) {
|
||||
$this->result->error('记录不存在', 404);
|
||||
}
|
||||
$statusOnly = isset($params['status'])
|
||||
&& count(array_diff(array_keys($params), ['id', 'status'])) === 0;
|
||||
if (! $statusOnly) {
|
||||
try {
|
||||
validate(ScoreValidate::class)->check($params);
|
||||
} catch (ValidateException $e) {
|
||||
$this->result->error($e->getMessage());
|
||||
}
|
||||
}
|
||||
$info->save($params);
|
||||
$this->result->success($info, '更新成功');
|
||||
}
|
||||
|
||||
public function recyclebin()
|
||||
{
|
||||
$page = $this->request->param('page/d', 1);
|
||||
$limit = $this->request->param('limit/d', 15);
|
||||
if ($this->request->isAjax()) {
|
||||
$list = $this->model->onlyTrashed()
|
||||
->order('sort', 'desc')
|
||||
->paginate(['page' => $page, 'list_rows' => $limit]);
|
||||
$this->result->setCount($list->total())->success($list->items());
|
||||
}
|
||||
return $this->fetch('score/index');
|
||||
}
|
||||
|
||||
public function delete()
|
||||
{
|
||||
if ($this->request->isAjax() && $this->request->isDelete()) {
|
||||
$ids = $this->request->param('ids', '');
|
||||
$force = $this->request->param('force', false);
|
||||
if (empty($ids)) {
|
||||
$this->result->error('请选择要删除的数据');
|
||||
}
|
||||
$idsArray = array_filter(explode(',', $ids), 'is_numeric');
|
||||
if (empty($idsArray)) {
|
||||
$this->result->error('参数错误');
|
||||
}
|
||||
try {
|
||||
Db::transaction(function () use ($idsArray, $force) {
|
||||
if ($force) {
|
||||
$this->model->onlyTrashed()->whereIn('id', $idsArray)->select()
|
||||
->each(function ($item) { $item->force()->delete(); });
|
||||
} else {
|
||||
$this->model->destroy($idsArray);
|
||||
}
|
||||
});
|
||||
$this->result->success();
|
||||
} catch (\think\exception\HttpResponseException $e) {
|
||||
throw $e;
|
||||
} catch (\Exception $e) {
|
||||
$this->result->error('删除失败: ' . ($e->getMessage() ?: $e->__toString()), 500);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public function restore($ids = '')
|
||||
{
|
||||
if ($this->request->isAjax() && $this->request->isPost()) {
|
||||
$ids = $this->request->param('ids', '');
|
||||
if (empty($ids)) {
|
||||
$this->result->error('请选择要恢复的数据');
|
||||
}
|
||||
$idsArray = array_filter(explode(',', $ids), 'is_numeric');
|
||||
Db::startTrans();
|
||||
try {
|
||||
$this->model->withTrashed()->where('id', 'in', $idsArray)->select()
|
||||
->each(function ($item) { $item->restore(); });
|
||||
Db::commit();
|
||||
$this->result->success('恢复成功');
|
||||
} catch (DbException $e) {
|
||||
Db::rollback();
|
||||
$this->result->error('恢复失败: ' . $e->getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ============ 积分流水 ============
|
||||
public function log()
|
||||
{
|
||||
$page = $this->request->param('page/d', 1);
|
||||
$limit = $this->request->param('limit/d', 15);
|
||||
$uid = $this->request->param('uid/d', 0);
|
||||
|
||||
if ($this->request->isAjax()) {
|
||||
$res = ScoreModel::logList($uid, $page, $limit);
|
||||
$typeList = ScoreModel::typeList();
|
||||
foreach ($res['list'] as &$item) {
|
||||
$item['type_text'] = $item['type'] == 2 ? '支出' : '收入';
|
||||
}
|
||||
$this->result->setCount($res['count'])->success($res['list']);
|
||||
}
|
||||
return $this->fetch('score/log');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,158 @@
|
||||
<?php
|
||||
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\Shop as ShopModel;
|
||||
use app\backend\validate\Shop as ShopValidate;
|
||||
|
||||
/**
|
||||
* 电子商务管理
|
||||
*/
|
||||
class Shop extends BackendBase
|
||||
{
|
||||
protected $noNeedLogin = [];
|
||||
protected $noNeedVerify = [];
|
||||
|
||||
protected function initialize()
|
||||
{
|
||||
$this->model = new ShopModel();
|
||||
}
|
||||
|
||||
public function index()
|
||||
{
|
||||
$page = $this->request->param('page/d', 1);
|
||||
$limit = $this->request->param('limit/d', 15);
|
||||
$title = $this->request->param('title', '');
|
||||
|
||||
if ($this->request->isAjax()) {
|
||||
$query = $this->model->newQuery();
|
||||
if ($title !== '') {
|
||||
$query->where('title', 'like', '%' . $title . '%');
|
||||
}
|
||||
$list = $query->order('sort', 'desc')
|
||||
->paginate(['page' => $page, 'list_rows' => $limit]);
|
||||
$this->result->setCount($list->total())->success($list->items());
|
||||
}
|
||||
return $this->fetch();
|
||||
}
|
||||
|
||||
public function save()
|
||||
{
|
||||
if (! $this->request->isPost()) {
|
||||
$this->result->error('请求方式错误', 405);
|
||||
}
|
||||
$params = $this->request->only(['title', 'price', 'stock', 'description', 'sort', 'status'], 'post');
|
||||
try {
|
||||
validate(ShopValidate::class)->check($params);
|
||||
$this->model->create($params);
|
||||
$this->result->success('', '添加成功');
|
||||
} catch (ValidateException $e) {
|
||||
$this->result->error($e->getMessage());
|
||||
} catch (DbException $e) {
|
||||
$this->result->error('添加失败: ' . $e->getMessage(), 2);
|
||||
}
|
||||
}
|
||||
|
||||
public function edit($id = null)
|
||||
{
|
||||
$id = $id ?: $this->request->param('id');
|
||||
$info = $this->model->find($id);
|
||||
if (! $info) {
|
||||
$this->result->error('记录不存在', 404);
|
||||
}
|
||||
$this->result->success(['info' => $info]);
|
||||
}
|
||||
|
||||
public function update()
|
||||
{
|
||||
if (! ($this->request->isAjax() && $this->request->isPut())) {
|
||||
$this->result->error('请求方式错误', 405);
|
||||
}
|
||||
$params = $this->request->param();
|
||||
$id = $params['id'] ?? null;
|
||||
$info = $this->model->find($id);
|
||||
if (! $info) {
|
||||
$this->result->error('记录不存在', 404);
|
||||
}
|
||||
$statusOnly = isset($params['status'])
|
||||
&& count(array_diff(array_keys($params), ['id', 'status'])) === 0;
|
||||
if (! $statusOnly) {
|
||||
try {
|
||||
validate(ShopValidate::class)->check($params);
|
||||
} catch (ValidateException $e) {
|
||||
$this->result->error($e->getMessage());
|
||||
}
|
||||
}
|
||||
$info->save($params);
|
||||
$this->result->success($info, '更新成功');
|
||||
}
|
||||
|
||||
public function recyclebin()
|
||||
{
|
||||
$page = $this->request->param('page/d', 1);
|
||||
$limit = $this->request->param('limit/d', 15);
|
||||
if ($this->request->isAjax()) {
|
||||
$list = $this->model->onlyTrashed()
|
||||
->order('sort', 'desc')
|
||||
->paginate(['page' => $page, 'list_rows' => $limit]);
|
||||
$this->result->setCount($list->total())->success($list->items());
|
||||
}
|
||||
return $this->fetch('shop/index');
|
||||
}
|
||||
|
||||
public function delete()
|
||||
{
|
||||
if ($this->request->isAjax() && $this->request->isDelete()) {
|
||||
$ids = $this->request->param('ids', '');
|
||||
$force = $this->request->param('force', false);
|
||||
if (empty($ids)) {
|
||||
$this->result->error('请选择要删除的数据');
|
||||
}
|
||||
$idsArray = array_filter(explode(',', $ids), 'is_numeric');
|
||||
if (empty($idsArray)) {
|
||||
$this->result->error('参数错误');
|
||||
}
|
||||
try {
|
||||
Db::transaction(function () use ($idsArray, $force) {
|
||||
if ($force) {
|
||||
$this->model->onlyTrashed()->whereIn('id', $idsArray)->select()
|
||||
->each(function ($item) { $item->force()->delete(); });
|
||||
} else {
|
||||
$this->model->destroy($idsArray);
|
||||
}
|
||||
});
|
||||
$this->result->success();
|
||||
} catch (\think\exception\HttpResponseException $e) {
|
||||
throw $e;
|
||||
} catch (\Exception $e) {
|
||||
$this->result->error('删除失败: ' . ($e->getMessage() ?: $e->__toString()), 500);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public function restore($ids = '')
|
||||
{
|
||||
if ($this->request->isAjax() && $this->request->isPost()) {
|
||||
$ids = $this->request->param('ids', '');
|
||||
if (empty($ids)) {
|
||||
$this->result->error('请选择要恢复的数据');
|
||||
}
|
||||
$idsArray = array_filter(explode(',', $ids), 'is_numeric');
|
||||
Db::startTrans();
|
||||
try {
|
||||
$this->model->withTrashed()->where('id', 'in', $idsArray)->select()
|
||||
->each(function ($item) { $item->restore(); });
|
||||
Db::commit();
|
||||
$this->result->success('恢复成功');
|
||||
} catch (DbException $e) {
|
||||
Db::rollback();
|
||||
$this->result->error('恢复失败: ' . $e->getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,158 @@
|
||||
<?php
|
||||
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\Sms as SmsModel;
|
||||
use app\backend\validate\Sms as SmsValidate;
|
||||
|
||||
/**
|
||||
* 短信服务管理
|
||||
*/
|
||||
class Sms extends BackendBase
|
||||
{
|
||||
protected $noNeedLogin = [];
|
||||
protected $noNeedVerify = [];
|
||||
|
||||
protected function initialize()
|
||||
{
|
||||
$this->model = new SmsModel();
|
||||
}
|
||||
|
||||
public function index()
|
||||
{
|
||||
$page = $this->request->param('page/d', 1);
|
||||
$limit = $this->request->param('limit/d', 15);
|
||||
$title = $this->request->param('title', '');
|
||||
|
||||
if ($this->request->isAjax()) {
|
||||
$query = $this->model->newQuery();
|
||||
if ($title !== '') {
|
||||
$query->where('title', 'like', '%' . $title . '%');
|
||||
}
|
||||
$list = $query->order('sort', 'desc')
|
||||
->paginate(['page' => $page, 'list_rows' => $limit]);
|
||||
$this->result->setCount($list->total())->success($list->items());
|
||||
}
|
||||
return $this->fetch();
|
||||
}
|
||||
|
||||
public function save()
|
||||
{
|
||||
if (! $this->request->isPost()) {
|
||||
$this->result->error('请求方式错误', 405);
|
||||
}
|
||||
$params = $this->request->only(['title', 'code', 'content', 'sort', 'status'], 'post');
|
||||
try {
|
||||
validate(SmsValidate::class)->check($params);
|
||||
$this->model->create($params);
|
||||
$this->result->success('', '添加成功');
|
||||
} catch (ValidateException $e) {
|
||||
$this->result->error($e->getMessage());
|
||||
} catch (DbException $e) {
|
||||
$this->result->error('添加失败: ' . $e->getMessage(), 2);
|
||||
}
|
||||
}
|
||||
|
||||
public function edit($id = null)
|
||||
{
|
||||
$id = $id ?: $this->request->param('id');
|
||||
$info = $this->model->find($id);
|
||||
if (! $info) {
|
||||
$this->result->error('记录不存在', 404);
|
||||
}
|
||||
$this->result->success(['info' => $info]);
|
||||
}
|
||||
|
||||
public function update()
|
||||
{
|
||||
if (! ($this->request->isAjax() && $this->request->isPut())) {
|
||||
$this->result->error('请求方式错误', 405);
|
||||
}
|
||||
$params = $this->request->param();
|
||||
$id = $params['id'] ?? null;
|
||||
$info = $this->model->find($id);
|
||||
if (! $info) {
|
||||
$this->result->error('记录不存在', 404);
|
||||
}
|
||||
$statusOnly = isset($params['status'])
|
||||
&& count(array_diff(array_keys($params), ['id', 'status'])) === 0;
|
||||
if (! $statusOnly) {
|
||||
try {
|
||||
validate(SmsValidate::class)->check($params);
|
||||
} catch (ValidateException $e) {
|
||||
$this->result->error($e->getMessage());
|
||||
}
|
||||
}
|
||||
$info->save($params);
|
||||
$this->result->success($info, '更新成功');
|
||||
}
|
||||
|
||||
public function recyclebin()
|
||||
{
|
||||
$page = $this->request->param('page/d', 1);
|
||||
$limit = $this->request->param('limit/d', 15);
|
||||
if ($this->request->isAjax()) {
|
||||
$list = $this->model->onlyTrashed()
|
||||
->order('sort', 'desc')
|
||||
->paginate(['page' => $page, 'list_rows' => $limit]);
|
||||
$this->result->setCount($list->total())->success($list->items());
|
||||
}
|
||||
return $this->fetch('sms/index');
|
||||
}
|
||||
|
||||
public function delete()
|
||||
{
|
||||
if ($this->request->isAjax() && $this->request->isDelete()) {
|
||||
$ids = $this->request->param('ids', '');
|
||||
$force = $this->request->param('force', false);
|
||||
if (empty($ids)) {
|
||||
$this->result->error('请选择要删除的数据');
|
||||
}
|
||||
$idsArray = array_filter(explode(',', $ids), 'is_numeric');
|
||||
if (empty($idsArray)) {
|
||||
$this->result->error('参数错误');
|
||||
}
|
||||
try {
|
||||
Db::transaction(function () use ($idsArray, $force) {
|
||||
if ($force) {
|
||||
$this->model->onlyTrashed()->whereIn('id', $idsArray)->select()
|
||||
->each(function ($item) { $item->force()->delete(); });
|
||||
} else {
|
||||
$this->model->destroy($idsArray);
|
||||
}
|
||||
});
|
||||
$this->result->success();
|
||||
} catch (\think\exception\HttpResponseException $e) {
|
||||
throw $e;
|
||||
} catch (\Exception $e) {
|
||||
$this->result->error('删除失败: ' . ($e->getMessage() ?: $e->__toString()), 500);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public function restore($ids = '')
|
||||
{
|
||||
if ($this->request->isAjax() && $this->request->isPost()) {
|
||||
$ids = $this->request->param('ids', '');
|
||||
if (empty($ids)) {
|
||||
$this->result->error('请选择要恢复的数据');
|
||||
}
|
||||
$idsArray = array_filter(explode(',', $ids), 'is_numeric');
|
||||
Db::startTrans();
|
||||
try {
|
||||
$this->model->withTrashed()->where('id', 'in', $idsArray)->select()
|
||||
->each(function ($item) { $item->restore(); });
|
||||
Db::commit();
|
||||
$this->result->success('恢复成功');
|
||||
} catch (DbException $e) {
|
||||
Db::rollback();
|
||||
$this->result->error('恢复失败: ' . $e->getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,174 @@
|
||||
<?php
|
||||
/*
|
||||
* @Author: YwxApp <ywx@ywxapp.cn>
|
||||
* @Date: 2026-07-28 00:00:00
|
||||
* @Description: 搜索蜘蛛统计
|
||||
* @FilePath: \ywxapp_dev\app\backend\controller\Spider.php
|
||||
* @CustomString: Copyright (c) 2026 YwxApp
|
||||
*/
|
||||
|
||||
declare(strict_types=1);
|
||||
namespace app\backend\controller;
|
||||
|
||||
use think\facade\Db;
|
||||
use ywxapp\controller\BackendBase;
|
||||
use ywxapp\library\SpiderDetect;
|
||||
|
||||
/**
|
||||
* 搜索蜘蛛统计
|
||||
*
|
||||
* 数据来源:全局中间件 ywxapp\middleware\SpiderStat 写入的
|
||||
* spider_log(明细)与 spider_stat(按日聚合)两张表。
|
||||
*
|
||||
* @author ywxapp <admin@ywxapp.cn>
|
||||
*/
|
||||
class Spider extends BackendBase
|
||||
{
|
||||
protected $noNeedLogin = [];
|
||||
protected $noNeedVerify = [];
|
||||
|
||||
/**
|
||||
* 抓取明细列表(页面 + Ajax)
|
||||
*/
|
||||
public function index()
|
||||
{
|
||||
if ($this->request->isAjax()) {
|
||||
$page = $this->request->param('page/d', 1);
|
||||
$limit = $this->request->param('limit/d', 15);
|
||||
$spider = $this->request->param('spider', '');
|
||||
$ip = $this->request->param('ip', '');
|
||||
$url = $this->request->param('url', '');
|
||||
$date = $this->request->param('date', ''); // YYYY-MM-DD
|
||||
|
||||
$query = Db::name('spider_log');
|
||||
if ($spider !== '') {
|
||||
$query->where('spider', $spider);
|
||||
}
|
||||
if ($ip !== '') {
|
||||
$query->where('ip', 'like', $ip . '%');
|
||||
}
|
||||
if ($url !== '') {
|
||||
$query->where('url', 'like', '%' . $url . '%');
|
||||
}
|
||||
if ($date !== '' && ($ts = strtotime($date)) !== false) {
|
||||
$query->whereBetween('create_at', [$ts, $ts + 86399]);
|
||||
}
|
||||
|
||||
try {
|
||||
$list = $query->order('id', 'desc')
|
||||
->paginate(['page' => $page, 'list_rows' => $limit]);
|
||||
$items = $list->items();
|
||||
$labels = SpiderDetect::labels();
|
||||
foreach ($items as &$item) {
|
||||
$item['spider_text'] = $labels[$item['spider']] ?? $item['spider'];
|
||||
$item['time_text'] = date('Y-m-d H:i:s', (int) $item['create_at']);
|
||||
}
|
||||
unset($item);
|
||||
$this->result->setCount($list->total())->success($items);
|
||||
} catch (\Throwable $e) {
|
||||
// 表尚未创建(还没有蜘蛛来访过):返回空列表而非报错
|
||||
$this->result->setCount(0)->success([]);
|
||||
}
|
||||
}
|
||||
|
||||
return $this->fetch();
|
||||
}
|
||||
|
||||
/**
|
||||
* 汇总统计:各蜘蛛 今日/7日/30日/总量 + 近30日逐日趋势(图表用)
|
||||
*/
|
||||
public function stats()
|
||||
{
|
||||
$labels = SpiderDetect::labels();
|
||||
$today = date('Y-m-d');
|
||||
$d7 = date('Y-m-d', strtotime('-6 days'));
|
||||
$d30 = date('Y-m-d', strtotime('-29 days'));
|
||||
|
||||
$summary = [];
|
||||
$trend = ['dates' => [], 'series' => []];
|
||||
|
||||
try {
|
||||
$rows = Db::name('spider_stat')
|
||||
->where('stat_date', '>=', $d30)
|
||||
->select()
|
||||
->toArray();
|
||||
$totalRows = Db::name('spider_stat')
|
||||
->field('spider, SUM(`count`) AS total')
|
||||
->group('spider')
|
||||
->select()
|
||||
->toArray();
|
||||
|
||||
// 各蜘蛛汇总
|
||||
$map = [];
|
||||
foreach ($totalRows as $r) {
|
||||
$map[$r['spider']] = [
|
||||
'spider' => $r['spider'],
|
||||
'label' => $labels[$r['spider']] ?? $r['spider'],
|
||||
'today' => 0,
|
||||
'week' => 0,
|
||||
'month' => 0,
|
||||
'total' => (int) $r['total'],
|
||||
];
|
||||
}
|
||||
foreach ($rows as $r) {
|
||||
$key = $r['spider'];
|
||||
if (! isset($map[$key])) {
|
||||
continue;
|
||||
}
|
||||
$c = (int) $r['count'];
|
||||
$map[$key]['month'] += $c;
|
||||
if ($r['stat_date'] >= $d7) {
|
||||
$map[$key]['week'] += $c;
|
||||
}
|
||||
if ($r['stat_date'] === $today) {
|
||||
$map[$key]['today'] += $c;
|
||||
}
|
||||
}
|
||||
usort($map, fn ($a, $b) => $b['total'] <=> $a['total']);
|
||||
$summary = array_values($map);
|
||||
|
||||
// 近30日逐日趋势(每蜘蛛一条线)
|
||||
$dates = [];
|
||||
for ($i = 29; $i >= 0; $i--) {
|
||||
$dates[] = date('Y-m-d', strtotime("-{$i} days"));
|
||||
}
|
||||
$trend['dates'] = $dates;
|
||||
$bySpider = [];
|
||||
foreach ($rows as $r) {
|
||||
$bySpider[$r['spider']][$r['stat_date']] = (int) $r['count'];
|
||||
}
|
||||
foreach ($summary as $s) {
|
||||
$key = $s['spider'];
|
||||
$line = [];
|
||||
foreach ($dates as $d) {
|
||||
$line[] = $bySpider[$key][$d] ?? 0;
|
||||
}
|
||||
$trend['series'][] = ['name' => $s['label'], 'data' => $line];
|
||||
}
|
||||
} catch (\Throwable $e) {
|
||||
// 表未创建:返回空数据
|
||||
}
|
||||
|
||||
$this->result->success(['summary' => $summary, 'trend' => $trend]);
|
||||
}
|
||||
|
||||
/**
|
||||
* 清理明细日志(保留最近 N 天,聚合表不动,历史报表不受影响)
|
||||
*/
|
||||
public function clear()
|
||||
{
|
||||
if (! ($this->request->isAjax() && $this->request->isPost())) {
|
||||
$this->result->error('请求方式错误', 405);
|
||||
}
|
||||
$days = $this->request->param('days/d', 30);
|
||||
$days = max(1, min(365, $days));
|
||||
try {
|
||||
$count = Db::name('spider_log')
|
||||
->where('create_at', '<', time() - $days * 86400)
|
||||
->delete();
|
||||
$this->result->success('', "已清理 {$count} 条 {$days} 天前的明细日志");
|
||||
} catch (\Throwable $e) {
|
||||
$this->result->error('清理失败: ' . $e->getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,161 @@
|
||||
<?php
|
||||
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\Task as TaskModel;
|
||||
use ywxapp\model\Prop as PropModel;
|
||||
use app\backend\validate\Task as TaskValidate;
|
||||
|
||||
/**
|
||||
* 站点任务管理
|
||||
*/
|
||||
class Task extends BackendBase
|
||||
{
|
||||
protected $noNeedLogin = [];
|
||||
protected $noNeedVerify = [];
|
||||
|
||||
protected function initialize()
|
||||
{
|
||||
$this->model = new TaskModel();
|
||||
}
|
||||
|
||||
public function index()
|
||||
{
|
||||
$page = $this->request->param('page/d', 1);
|
||||
$limit = $this->request->param('limit/d', 15);
|
||||
$title = $this->request->param('title', '');
|
||||
|
||||
if ($this->request->isAjax()) {
|
||||
$query = $this->model->newQuery();
|
||||
if ($title !== '') {
|
||||
$query->where('title', 'like', '%' . $title . '%');
|
||||
}
|
||||
$list = $query->order('sort', 'desc')
|
||||
->paginate(['page' => $page, 'list_rows' => $limit]);
|
||||
$this->result->setCount($list->total())->success($list->items());
|
||||
}
|
||||
$propList = PropModel::where('status', 1)->order('id', 'asc')->column('title', 'id') ?: [];
|
||||
$this->view->assign('propList', $propList);
|
||||
return $this->fetch();
|
||||
}
|
||||
|
||||
public function save()
|
||||
{
|
||||
if (! $this->request->isPost()) {
|
||||
$this->result->error('请求方式错误', 405);
|
||||
}
|
||||
$params = $this->request->only(['title', 'description', 'reward_type', 'reward_num', 'sort', 'status'], 'post');
|
||||
try {
|
||||
validate(TaskValidate::class)->check($params);
|
||||
$this->model->create($params);
|
||||
$this->result->success('', '添加成功');
|
||||
} catch (ValidateException $e) {
|
||||
$this->result->error($e->getMessage());
|
||||
} catch (DbException $e) {
|
||||
$this->result->error('添加失败: ' . $e->getMessage(), 2);
|
||||
}
|
||||
}
|
||||
|
||||
public function edit($id = null)
|
||||
{
|
||||
$id = $id ?: $this->request->param('id');
|
||||
$info = $this->model->find($id);
|
||||
if (! $info) {
|
||||
$this->result->error('记录不存在', 404);
|
||||
}
|
||||
$this->result->success(['info' => $info]);
|
||||
}
|
||||
|
||||
public function update()
|
||||
{
|
||||
if (! ($this->request->isAjax() && $this->request->isPut())) {
|
||||
$this->result->error('请求方式错误', 405);
|
||||
}
|
||||
$params = $this->request->param();
|
||||
$id = $params['id'] ?? null;
|
||||
$info = $this->model->find($id);
|
||||
if (! $info) {
|
||||
$this->result->error('记录不存在', 404);
|
||||
}
|
||||
$statusOnly = isset($params['status'])
|
||||
&& count(array_diff(array_keys($params), ['id', 'status'])) === 0;
|
||||
if (! $statusOnly) {
|
||||
try {
|
||||
validate(TaskValidate::class)->check($params);
|
||||
} catch (ValidateException $e) {
|
||||
$this->result->error($e->getMessage());
|
||||
}
|
||||
}
|
||||
$info->save($params);
|
||||
$this->result->success($info, '更新成功');
|
||||
}
|
||||
|
||||
public function recyclebin()
|
||||
{
|
||||
$page = $this->request->param('page/d', 1);
|
||||
$limit = $this->request->param('limit/d', 15);
|
||||
if ($this->request->isAjax()) {
|
||||
$list = $this->model->onlyTrashed()
|
||||
->order('sort', 'desc')
|
||||
->paginate(['page' => $page, 'list_rows' => $limit]);
|
||||
$this->result->setCount($list->total())->success($list->items());
|
||||
}
|
||||
return $this->fetch('task/index');
|
||||
}
|
||||
|
||||
public function delete()
|
||||
{
|
||||
if ($this->request->isAjax() && $this->request->isDelete()) {
|
||||
$ids = $this->request->param('ids', '');
|
||||
$force = $this->request->param('force', false);
|
||||
if (empty($ids)) {
|
||||
$this->result->error('请选择要删除的数据');
|
||||
}
|
||||
$idsArray = array_filter(explode(',', $ids), 'is_numeric');
|
||||
if (empty($idsArray)) {
|
||||
$this->result->error('参数错误');
|
||||
}
|
||||
try {
|
||||
Db::transaction(function () use ($idsArray, $force) {
|
||||
if ($force) {
|
||||
$this->model->onlyTrashed()->whereIn('id', $idsArray)->select()
|
||||
->each(function ($item) { $item->force()->delete(); });
|
||||
} else {
|
||||
$this->model->destroy($idsArray);
|
||||
}
|
||||
});
|
||||
$this->result->success();
|
||||
} catch (\think\exception\HttpResponseException $e) {
|
||||
throw $e;
|
||||
} catch (\Exception $e) {
|
||||
$this->result->error('删除失败: ' . ($e->getMessage() ?: $e->__toString()), 500);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public function restore($ids = '')
|
||||
{
|
||||
if ($this->request->isAjax() && $this->request->isPost()) {
|
||||
$ids = $this->request->param('ids', '');
|
||||
if (empty($ids)) {
|
||||
$this->result->error('请选择要恢复的数据');
|
||||
}
|
||||
$idsArray = array_filter(explode(',', $ids), 'is_numeric');
|
||||
Db::startTrans();
|
||||
try {
|
||||
$this->model->withTrashed()->where('id', 'in', $idsArray)->select()
|
||||
->each(function ($item) { $item->restore(); });
|
||||
Db::commit();
|
||||
$this->result->success('恢复成功');
|
||||
} catch (DbException $e) {
|
||||
Db::rollback();
|
||||
$this->result->error('恢复失败: ' . $e->getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,330 @@
|
||||
<?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\controller;
|
||||
|
||||
use think\facade\View;
|
||||
use Exception;
|
||||
use ywxapp\controller\BackendBase;
|
||||
use ywxapp\library\TemplateManager;
|
||||
use ywxapp\library\TemplateInstaller;
|
||||
|
||||
/**
|
||||
* 模板管理(核心后台内置模块,参考 Discuz! 后台「界面」分类)。
|
||||
* 站点级皮肤能力由 ywxapp/library/TemplateManager、TemplateInstaller 提供,
|
||||
* 与插件解耦,本控制器仅承载后台管理 UI。
|
||||
*/
|
||||
class Template extends BackendBase
|
||||
{
|
||||
protected $noNeedLogin = [];
|
||||
protected $noNeedVerify = [];
|
||||
|
||||
/**
|
||||
* 模板中心页面 + 数据接口。
|
||||
*/
|
||||
public function index()
|
||||
{
|
||||
if ($this->request->isAjax()) {
|
||||
$map = TemplateManager::getActiveMap();
|
||||
$settings = TemplateManager::getSettings();
|
||||
$data = [
|
||||
'installed' => TemplateManager::listInstalled(),
|
||||
'addon' => TemplateManager::listaddon(),
|
||||
'active_map' => $map,
|
||||
'global_default' => $map['*'] ?? 'default',
|
||||
'allow_member_select' => ! empty($settings['allow_member_select']),
|
||||
];
|
||||
return $this->result->success($data, '获取成功');
|
||||
}
|
||||
return View::fetch('template/index');
|
||||
}
|
||||
|
||||
/**
|
||||
* 界面设置(Discuz 式:全站默认模板 + 会员自选开关)。
|
||||
* GET 渲染页面;POST 保存。
|
||||
*/
|
||||
public function setting()
|
||||
{
|
||||
if ($this->request->isAjax() && $this->request->isPost()) {
|
||||
try {
|
||||
$default = (string) input('default_template', 'default');
|
||||
$allow = (string) input('allow_member_select', '0');
|
||||
if (! preg_match('/^[a-zA-Z0-9_]*$/', $default)) {
|
||||
return $this->result->error('默认模板标识非法');
|
||||
}
|
||||
// 写入全站默认('default' 表示回退自带视图)
|
||||
TemplateManager::setActive('*', $default);
|
||||
$settings = TemplateManager::getSettings();
|
||||
$settings['allow_member_select'] = ($allow === '1' || $allow === 'on');
|
||||
TemplateManager::setSettings($settings);
|
||||
return $this->result->success([], '已保存');
|
||||
} catch (Exception $e) {
|
||||
return $this->result->error($e->getMessage());
|
||||
}
|
||||
}
|
||||
return View::fetch('template/setting');
|
||||
}
|
||||
|
||||
/**
|
||||
* 设为全站默认模板(写入 active_map['*'])。
|
||||
*/
|
||||
public function setDefault()
|
||||
{
|
||||
try {
|
||||
$template = (string) input('template', '');
|
||||
if (! preg_match('/^[a-zA-Z0-9_]*$/', $template)) {
|
||||
return $this->result->error('模板标识非法');
|
||||
}
|
||||
TemplateManager::setActive('*', $template);
|
||||
TemplateManager::clearOverlayCache();
|
||||
$label = $template === 'default' ? '默认(自带视图)' : $template;
|
||||
return $this->result->success([], '已设全站默认:' . $label);
|
||||
} catch (Exception $e) {
|
||||
return $this->result->error($e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 启用/禁用模板(禁用后即使被绑定也不生效)。
|
||||
*/
|
||||
public function toggle()
|
||||
{
|
||||
try {
|
||||
$name = (string) input('name', '');
|
||||
$enabled = (int) input('enabled', 1);
|
||||
if (! preg_match('/^[a-zA-Z][a-zA-Z0-9_]*$/', $name)) {
|
||||
return $this->result->error('模板标识非法');
|
||||
}
|
||||
TemplateManager::setEnabled($name, $enabled === 1);
|
||||
TemplateManager::clearOverlayCache();
|
||||
return $this->result->success([], $enabled === 1 ? '已启用' : '已禁用');
|
||||
} catch (Exception $e) {
|
||||
return $this->result->error($e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置某插件激活的模板。
|
||||
* POST {addon, template};template='default'|'' 表示回退自带视图。
|
||||
*/
|
||||
public function setActive()
|
||||
{
|
||||
try {
|
||||
$addon = (string) input('addon', '');
|
||||
$template = (string) input('template', '');
|
||||
if (!preg_match('/^[a-zA-Z][a-zA-Z0-9_]*$/', $addon)) {
|
||||
return $this->result->error('插件标识非法');
|
||||
}
|
||||
if (!preg_match('/^[a-zA-Z0-9_]*$/', $template)) {
|
||||
return $this->result->error('模板标识非法');
|
||||
}
|
||||
TemplateManager::setActive($addon, $template);
|
||||
return $this->result->success([], '已保存');
|
||||
} catch (Exception $e) {
|
||||
return $this->result->error($e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 保存某模板的变量(配色)覆盖值。
|
||||
* POST {name, vars: {primary:'#fff', ...}}
|
||||
*/
|
||||
public function saveVariables()
|
||||
{
|
||||
try {
|
||||
$name = (string) input('name', '');
|
||||
if (! preg_match('/^[a-zA-Z][a-zA-Z0-9_]*$/', $name)) {
|
||||
return $this->result->error('模板标识非法');
|
||||
}
|
||||
$raw = input('vars/a', []);
|
||||
if (! is_array($raw)) {
|
||||
$raw = [];
|
||||
}
|
||||
TemplateManager::saveVariables($name, $raw);
|
||||
return $this->result->success([], '配色已保存');
|
||||
} catch (Exception $e) {
|
||||
return $this->result->error($e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 上传并安装模板 zip 包(由 TemplateInstaller 落地文件)。
|
||||
*/
|
||||
public function upload()
|
||||
{
|
||||
try {
|
||||
if (! $this->request->isPost()) {
|
||||
return $this->result->error('仅允许 POST 上传');
|
||||
}
|
||||
$file = $this->request->file('file');
|
||||
if (empty($file)) {
|
||||
return $this->result->error('未收到上传文件');
|
||||
}
|
||||
// 来源校验(同源):非同源直接拒绝,轻量防 CSRF
|
||||
$host = $this->request->host();
|
||||
$referer = $this->request->server('HTTP_REFERER', '');
|
||||
if ($referer !== '' && $host !== '' && stripos($referer, $host) === false) {
|
||||
return $this->result->error('来源校验失败');
|
||||
}
|
||||
// 仅允许 zip 包(扩展名 + MIME 双重校验)
|
||||
$ext = strtolower($file->getOriginalExtension());
|
||||
if ($ext !== 'zip') {
|
||||
return $this->result->error('仅支持 .zip 模板包');
|
||||
}
|
||||
if (! $file->checkMime(['application/zip', 'application/x-zip-compressed', 'application/octet-stream'])) {
|
||||
return $this->result->error('文件类型不合法');
|
||||
}
|
||||
$tmpDir = runtime_path() . 'templates' . DIRECTORY_SEPARATOR . '_upload_' . time() . DIRECTORY_SEPARATOR;
|
||||
if (!is_dir($tmpDir)) {
|
||||
@mkdir($tmpDir, 0755, true);
|
||||
}
|
||||
$moved = $file->move($tmpDir, $file->getOriginalName());
|
||||
if (!$moved) {
|
||||
return $this->result->error('文件保存失败:' . $file->getError());
|
||||
}
|
||||
$zipPath = $tmpDir . $moved->getSaveName();
|
||||
$info = TemplateInstaller::install($zipPath, root_path());
|
||||
// 全量清理渲染缓存
|
||||
TemplateManager::clearOverlayCache();
|
||||
@unlink($zipPath);
|
||||
@rmdir($tmpDir);
|
||||
return $this->result->success($info, '模板已安装:' . ($info['title'] ?? $info['name']));
|
||||
} catch (Exception $e) {
|
||||
return $this->result->error($e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 卸载模板(删除文件 + 解除绑定)。
|
||||
*/
|
||||
public function uninstall()
|
||||
{
|
||||
try {
|
||||
$name = (string) input('name', '');
|
||||
if (!preg_match('/^[a-zA-Z][a-zA-Z0-9_]*$/', $name)) {
|
||||
return $this->result->error('模板标识非法');
|
||||
}
|
||||
TemplateManager::uninstall($name);
|
||||
return $this->result->success([], '已卸载');
|
||||
} catch (Exception $e) {
|
||||
return $this->result->error($e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 导出当前模板配置(active_map + settings + variables)为 JSON 文件下载。
|
||||
* 用于备份 / 跨站点克隆皮肤配置。
|
||||
*/
|
||||
public function exportConfig()
|
||||
{
|
||||
$config = [
|
||||
'type' => 'ywxapp-template-config',
|
||||
'version' => '1.0',
|
||||
'exported_at' => date('Y-m-d H:i:s'),
|
||||
'active_map' => TemplateManager::getActiveMap(),
|
||||
'settings' => TemplateManager::getSettings(),
|
||||
];
|
||||
$json = json_encode($config, JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT);
|
||||
$filename = 'template-config-' . date('Ymd-His') . '.json';
|
||||
return response($json)->header([
|
||||
'Content-Type' => 'application/octet-stream',
|
||||
'Content-Disposition' => 'attachment; filename="' . $filename . '"',
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* 导入模板配置文件(JSON,由 exportConfig 生成)。
|
||||
* 仅采纳「已安装模板」的绑定与变量;未安装的模板绑定会被安全跳过,避免脏数据。
|
||||
*/
|
||||
public function importConfig()
|
||||
{
|
||||
try {
|
||||
if (! $this->request->isPost()) {
|
||||
return $this->result->error('仅允许 POST 上传');
|
||||
}
|
||||
// 同源校验(轻量防 CSRF)
|
||||
$host = $this->request->host();
|
||||
$referer = $this->request->server('HTTP_REFERER', '');
|
||||
if ($referer !== '' && $host !== '' && stripos($referer, $host) === false) {
|
||||
return $this->result->error('来源校验失败');
|
||||
}
|
||||
$file = $this->request->file('file');
|
||||
if (empty($file)) {
|
||||
return $this->result->error('未收到上传文件');
|
||||
}
|
||||
if (strtolower($file->getOriginalExtension()) !== 'json') {
|
||||
return $this->result->error('仅支持 .json 配置文件');
|
||||
}
|
||||
$cfg = json_decode(file_get_contents($file->getRealPath()), true);
|
||||
if (! is_array($cfg) || ($cfg['type'] ?? '') !== 'ywxapp-template-config') {
|
||||
return $this->result->error('不是有效的模板配置文件');
|
||||
}
|
||||
$installed = array_column(TemplateManager::listInstalled(), 'name');
|
||||
$installedSet = array_fill_keys($installed, true);
|
||||
|
||||
// 清洗 active_map:仅保留合法标识且已安装的模板
|
||||
$map = [];
|
||||
$srcMap = $cfg['active_map'] ?? [];
|
||||
foreach ($srcMap as $addon => $tpl) {
|
||||
if ($addon !== '*' && ! preg_match('/^[a-zA-Z][a-zA-Z0-9_]*$/', (string) $addon)) {
|
||||
continue;
|
||||
}
|
||||
$tpl = (string) $tpl;
|
||||
if ($tpl === '' || $tpl === 'default') {
|
||||
$map[$addon] = 'default';
|
||||
} elseif (preg_match('/^[a-zA-Z][a-zA-Z0-9_]*$/', $tpl) && isset($installedSet[$tpl])) {
|
||||
$map[$addon] = $tpl;
|
||||
}
|
||||
}
|
||||
|
||||
// 清洗 settings:allow_member_select / disabled / variables
|
||||
$settings = TemplateManager::getSettings();
|
||||
$src = $cfg['settings'] ?? [];
|
||||
if (is_array($src)) {
|
||||
$settings['allow_member_select'] = ! empty($src['allow_member_select']);
|
||||
$disabled = [];
|
||||
foreach (($src['disabled'] ?? []) as $d) {
|
||||
$d = (string) $d;
|
||||
if (preg_match('/^[a-zA-Z][a-zA-Z0-9_]*$/', $d) && isset($installedSet[$d])) {
|
||||
$disabled[] = $d;
|
||||
}
|
||||
}
|
||||
$settings['disabled'] = $disabled;
|
||||
$variables = [];
|
||||
foreach (($src['variables'] ?? []) as $name => $vars) {
|
||||
$name = (string) $name;
|
||||
if (! preg_match('/^[a-zA-Z][a-zA-Z0-9_]*$/', $name) || ! isset($installedSet[$name]) || ! is_array($vars)) {
|
||||
continue;
|
||||
}
|
||||
$clean = [];
|
||||
foreach ($vars as $k => $v) {
|
||||
$k = (string) $k;
|
||||
if (preg_match('/^[a-zA-Z][a-zA-Z0-9_]*$/', $k)) {
|
||||
$clean[$k] = (string) $v;
|
||||
}
|
||||
}
|
||||
$variables[$name] = $clean;
|
||||
}
|
||||
$settings['variables'] = $variables;
|
||||
}
|
||||
|
||||
TemplateManager::writeConfig($map, $settings);
|
||||
TemplateManager::clearOverlayCache();
|
||||
|
||||
$msg = '配置已导入';
|
||||
if (count($map) < count($srcMap)) {
|
||||
$msg .= '(部分未安装模板的绑定已跳过)';
|
||||
}
|
||||
return $this->result->success([], $msg);
|
||||
} catch (Exception $e) {
|
||||
return $this->result->error($e->getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,95 @@
|
||||
<?php
|
||||
/*
|
||||
* @Author: YwxApp <ywx@ywxapp.cn>
|
||||
* @Date: 2026-07-21 16:29:59
|
||||
* @LastEditors: YwxApp <ywx@ywxapp.cn>
|
||||
* @LastEditTime: 2026-08-04 11:25:35
|
||||
* @Description:
|
||||
* @FilePath: \ywxapp_dev\app\backend\controller\Upgrade.php
|
||||
* @CustomString: Copyright (c) 2026 YwxApp
|
||||
*/
|
||||
|
||||
declare(strict_types=1);
|
||||
namespace app\backend\controller;
|
||||
|
||||
use think\facade\View;
|
||||
use ywxapp\service\FrameworkService;
|
||||
use ywxapp\controller\BackendBase;
|
||||
/**
|
||||
* 主框架在线升级(客户端后台)
|
||||
*
|
||||
* 检测中心站 upgrade 插件发布的版本,并一键下载覆盖升级。
|
||||
* 访问:/backend/framework/index
|
||||
*/
|
||||
class Upgrade extends BackendBase
|
||||
{
|
||||
|
||||
public function initialize() {}
|
||||
|
||||
/**
|
||||
* 升级页面 / 检测接口
|
||||
*/
|
||||
public function index()
|
||||
{
|
||||
if ($this->request->isAjax()) {
|
||||
$svc = FrameworkService::instance();
|
||||
$info = $svc->checkVersion(true);
|
||||
return $this->result->success($info, $info['has_update'] ? '有可用更新' : '已是最新版本');
|
||||
}
|
||||
View::assign('current', config('ywxapp.version'));
|
||||
return View::fetch('upgrade/index');
|
||||
}
|
||||
|
||||
/**
|
||||
* 检测更新(AJAX):/admin/framework/check
|
||||
*/
|
||||
public function check()
|
||||
{
|
||||
$svc = FrameworkService::instance();
|
||||
$info = $svc->checkVersion(true);
|
||||
return $this->result->success($info, $info['has_update'] ? '有可用更新' : '已是最新版本');
|
||||
}
|
||||
|
||||
/**
|
||||
* 执行升级:下载核心包 → 备份 → 覆盖 → 更新版本号
|
||||
*/
|
||||
public function upgrade()
|
||||
{
|
||||
try {
|
||||
set_time_limit(0);
|
||||
$svc = FrameworkService::instance();
|
||||
$check = $svc->checkVersion(true);
|
||||
if (!$check['has_update']) {
|
||||
return $this->result->error('当前已是最新版本');
|
||||
}
|
||||
if (!empty($check['error'])) {
|
||||
return $this->result->error($check['error']);
|
||||
}
|
||||
$version = $check['latest'];
|
||||
// 手动选择升级包类型:auto(默认,按 use_patch 择优)/ full(完整包)/ patch(增量小包)
|
||||
$sel = strtolower(trim((string) $this->request->param('type', 'auto')));
|
||||
if ($sel === 'auto') {
|
||||
$usePatch = !empty($check['use_patch']);
|
||||
} elseif ($sel === 'patch') {
|
||||
if (empty($check['use_patch'])) {
|
||||
return $this->result->error('当前版本(' . ($check['current'] ?? '') . ')不是增量补丁的基础版本('
|
||||
. ($check['patch_from'] ?? '') . '),无法使用增量小包,请改用完整包');
|
||||
}
|
||||
$usePatch = true;
|
||||
} elseif ($sel === 'full') {
|
||||
if (empty($check['has_full'])) {
|
||||
return $this->result->error('当前无可用的完整包,请改用增量小包');
|
||||
}
|
||||
$usePatch = false;
|
||||
} else {
|
||||
return $this->result->error('未知的升级包类型:' . $sel . '(可选 auto/full/patch)');
|
||||
}
|
||||
$zip = $svc->download($version, $usePatch ? 'patch' : 'full');
|
||||
$res = $svc->apply($zip, $version, $usePatch ? 1 : 0);
|
||||
$mode = ($res['mode'] ?? 'full') === 'patch' ? '(增量补丁)' : '(整包)';
|
||||
return $this->result->success($res, '升级成功' . $mode . ',当前版本:' . $version);
|
||||
} catch (\Exception $e) {
|
||||
return $this->result->error($e->getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,244 @@
|
||||
<?php
|
||||
/*
|
||||
* @Author: YwxApp <ywx@ywxapp.cn>
|
||||
* @Date: 2026-04-29 02:32:33
|
||||
* @LastEditors: YwxApp <ywx@ywxapp.cn>
|
||||
* @LastEditTime: 2026-07-21 23:24:13
|
||||
* @Description:
|
||||
* @FilePath: \ywxapp_dev\app\backend\controller\User.php
|
||||
* @CustomString: Copyright (c) 2026 YwxApp
|
||||
*/
|
||||
declare(strict_types=1);
|
||||
namespace app\backend\controller;
|
||||
|
||||
use think\facade\Db;
|
||||
use think\facade\View;
|
||||
use think\Request;
|
||||
use ywxapp\controller\BackendBase;
|
||||
use ywxapp\model\MemberUser as UserModel;
|
||||
use ywxapp\model\MemberGroup as GroupModel;
|
||||
use app\backend\validate\User as UserValidate;
|
||||
use think\exception\ValidateException;
|
||||
use think\db\exception\DbException;
|
||||
|
||||
/**
|
||||
* Member 类
|
||||
*
|
||||
* @author ywxapp <admin@ywxapp.cn>
|
||||
*/
|
||||
class User extends BackendBase
|
||||
{
|
||||
/**
|
||||
* Summary of needLogin
|
||||
* @var array
|
||||
*/
|
||||
protected $noNeedLogin = [];
|
||||
|
||||
/**
|
||||
* Summary of needRight
|
||||
* @var array
|
||||
*/
|
||||
protected $noNeedVerify = [];
|
||||
|
||||
/**
|
||||
* 控制器初始化 initialize
|
||||
* @return void
|
||||
*/
|
||||
protected function initialize()
|
||||
{}
|
||||
|
||||
/**
|
||||
* 获取用户列表
|
||||
*/
|
||||
public function index()
|
||||
{
|
||||
$page = $this->request->param('page/d', 1);
|
||||
$limit = $this->request->param('limit/d', 15);
|
||||
if ($this->request->isAjax()) {
|
||||
$admins = UserModel::with(['groups'])->page($page, $limit)->select();
|
||||
$this->result->success($admins);
|
||||
}
|
||||
View::assign('title', '登录');
|
||||
return View::fetch();
|
||||
}
|
||||
|
||||
/**
|
||||
* 数据创建
|
||||
*
|
||||
* @param Request $request
|
||||
* @return \think\Response
|
||||
*/
|
||||
public function create()
|
||||
{
|
||||
if ($this->request->isAjax()) {
|
||||
$groups = GroupModel::where('status', 1)->select();
|
||||
$this->result->success(['groups' => $groups]);
|
||||
}
|
||||
View::assign('title', '登录');
|
||||
return View::fetch('user/index');
|
||||
}
|
||||
|
||||
/**
|
||||
* 数据保存
|
||||
*
|
||||
* @param Request $request
|
||||
* @return \think\Response
|
||||
*/
|
||||
public function save()
|
||||
{
|
||||
$params = $this->request->only(['account', 'nickname', 'password', 'confirmpass', 'mobile', 'email', 'role_ids', 'status'], 'post');
|
||||
$groupIds = $this->request->param('group_ids/a', []);
|
||||
try {
|
||||
validate(UserValidate::class)->check($params);
|
||||
Db::transaction(function () use ($params, $groupIds) {
|
||||
$data = UserModel::create($params);
|
||||
$data->groups()->saveAll($groupIds);
|
||||
});
|
||||
$this->result->success();
|
||||
} catch (ValidateException $e) {
|
||||
$this->result->error($e->getMessage());
|
||||
} catch (DbException $e) {
|
||||
$this->result->error($e->getMessage(), 2);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 数据编辑
|
||||
*
|
||||
* @param Request $request
|
||||
* @param int|null $ids
|
||||
* @return \think\Response
|
||||
*/
|
||||
public function edit( $id = null)
|
||||
{
|
||||
if ($this->request->isAjax()) {
|
||||
$data = UserModel::with(['groups'])
|
||||
->where('id', $id)
|
||||
->find();
|
||||
$groups = GroupModel::where('status', 1)->select();
|
||||
$this->result->success(['info' => $data, 'groups' => $groups]);
|
||||
}
|
||||
View::assign('title', '登录');
|
||||
return View::fetch('user/index');
|
||||
}
|
||||
|
||||
/**
|
||||
* 数据更新
|
||||
*
|
||||
* @param Request $request
|
||||
* @param int|null $ids
|
||||
* @return \think\Response
|
||||
*/
|
||||
public function update()
|
||||
{
|
||||
$data = $this->request->only(['id', 'account', 'nickname', 'password', 'confirmpass', 'mobile', 'email', 'status','group_ids'], 'put');
|
||||
$groupIds = $this->request->put('group_ids/a', []);
|
||||
$info = UserModel::find($data['id']);
|
||||
if (! $info) {
|
||||
$this->result->error('用户不存在',404);
|
||||
}
|
||||
$info->save($data);
|
||||
$info->groups()->sync($groupIds);
|
||||
$this->result->success($info,"用户更新成功");
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取详情
|
||||
*/
|
||||
public function read()
|
||||
{
|
||||
$id = $this->request->param('id/d', 0);
|
||||
$info = UserModel::with(['groups', 'rules'])
|
||||
->find($id);
|
||||
if (! $info) {
|
||||
$this->result->error('用户不存在',404);
|
||||
}
|
||||
$this->result->success($info,"用户更新成功");
|
||||
}
|
||||
|
||||
/**
|
||||
* 数据回收站
|
||||
*
|
||||
* @param Request $request
|
||||
* @return \think\Response
|
||||
*/
|
||||
public function recyclebin()
|
||||
{
|
||||
$page = $this->request->param('page', 1);
|
||||
$size = $this->request->param('size', 15);
|
||||
if ($this->request->isAjax()) {
|
||||
$admins = UserModel::onlyTrashed()->with(['groups'])
|
||||
->paginate([
|
||||
'page' => $page,
|
||||
'list_rows' => $size,
|
||||
]);
|
||||
$this->result->success($admins->items());
|
||||
}
|
||||
View::assign('title', '回收站');
|
||||
return View::fetch('user/index');
|
||||
}
|
||||
|
||||
/**
|
||||
* 数据删除
|
||||
*/
|
||||
public function delete()
|
||||
{
|
||||
if ($this->request->isAjax() && $this->request->isDelete()) {
|
||||
$ids = $this->request->param('ids', '');
|
||||
$force = $this->request->param('force', false);
|
||||
if (empty($ids)) {
|
||||
$this->result->error('请选择要删除的数据');
|
||||
}
|
||||
$idsArray = explode(',', $ids);
|
||||
try {
|
||||
Db::transaction(function () use ($idsArray, $force) {
|
||||
if ($force) {
|
||||
UserModel::onlyTrashed()->whereIn('id', $idsArray)->select()->each(function ($item) {
|
||||
$item->roles()->detach();
|
||||
$item->force()->delete();
|
||||
});
|
||||
} else {
|
||||
UserModel::destroy($idsArray);
|
||||
}
|
||||
});
|
||||
$this->result->success();
|
||||
} catch (\think\exception\HttpResponseException $e) {
|
||||
throw $e; // 不要 return,不要吞掉!
|
||||
} catch (\Exception $e) {
|
||||
\think\facade\Log::error('批量删除管理员失败', [
|
||||
'exception' => $e->__toString(),
|
||||
'admin_ids' => $idsArray,
|
||||
]);
|
||||
$this->result->error('删除失败: ' . ($e->getMessage() ?: $e->__toString()), 500);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public function restore( $ids = '')
|
||||
{
|
||||
if ($this->request->isAjax() && $this->request->isPost()) {
|
||||
$ids = $this->request->param('ids', '');
|
||||
if (empty($ids)) {
|
||||
$this->result->error('请选择要恢复的数据');
|
||||
}
|
||||
$idsArray = explode(',', $ids);
|
||||
Db::startTrans();
|
||||
try {
|
||||
UserModel::withTrashed()
|
||||
->where('id', 'in', $idsArray)
|
||||
->select()
|
||||
->each(function ($item) {
|
||||
$item->restore();
|
||||
});
|
||||
Db::commit();
|
||||
$this->result->success('恢复成功');
|
||||
} catch (DbException $e) {
|
||||
Db::rollback();
|
||||
$this->result->error('恢复失败: ' . $e->getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
<?php
|
||||
/*
|
||||
* @Author: YwxApp <ywx@ywxapp.cn>
|
||||
* @Date: 2026-04-29 02:32:33
|
||||
* @LastEditors: YwxApp <ywx@ywxapp.cn>
|
||||
* @LastEditTime: 2026-07-21 23:27:10
|
||||
* @Description:
|
||||
* @FilePath: \ywxapp_dev\app\backend\validate\Admin.php
|
||||
* @CustomString: Copyright (c) 2026 YwxApp
|
||||
*/
|
||||
|
||||
declare (strict_types = 1);
|
||||
return [
|
||||
'bind' => [
|
||||
// 更多事件绑定
|
||||
],
|
||||
'listen' => [
|
||||
'AdminLog' => ['app\backend\listener\AdminLog'],
|
||||
// 更多事件监听
|
||||
],
|
||||
];
|
||||
@@ -0,0 +1,22 @@
|
||||
<?php
|
||||
/*
|
||||
* @Author: YwxApp <ywx@ywxapp.cn>
|
||||
* @Date: 2026-04-29 02:32:33
|
||||
* @LastEditors: YwxApp <ywx@ywxapp.cn>
|
||||
* @LastEditTime: 2026-07-21 23:24:33
|
||||
* @Description:
|
||||
* @FilePath: \ywxapp_dev\app\backend\event\AdminLog.php
|
||||
* @CustomString: Copyright (c) 2026 YwxApp
|
||||
*/
|
||||
|
||||
declare (strict_types = 1);
|
||||
namespace app\backend\event;
|
||||
|
||||
/**
|
||||
* AdminLog 类
|
||||
*
|
||||
* @author ywxapp <admin@ywxapp.cn>
|
||||
*/
|
||||
class AdminLog
|
||||
{
|
||||
}
|
||||
@@ -0,0 +1,204 @@
|
||||
<?php
|
||||
// +----------------------------------------------------------------------
|
||||
// | YwxApp [ WE CAN DO IT JUST THINK ]
|
||||
// +----------------------------------------------------------------------
|
||||
// | Copyright (c) 2026-2036 http://ywxapp.cn All rights reserved.
|
||||
// +----------------------------------------------------------------------
|
||||
// | Author: ywxapp<admin@ywxapp.cn>
|
||||
// +----------------------------------------------------------------------
|
||||
|
||||
return [
|
||||
"Site name"=>"站点名称",
|
||||
"contact" => "联系我们",
|
||||
"Telephone" => "联系电话",
|
||||
"site_qq" => "QQ",
|
||||
'User id' => '会员ID',
|
||||
'Username' => '用户名',
|
||||
'Nickname' => '昵称',
|
||||
'Password' => '密码',
|
||||
'Sign up' => '注 册',
|
||||
'Sign in' => '登 录',
|
||||
'Sign out' => '注 销',
|
||||
'Keep login' => '保持会话',
|
||||
'Guest' => '游客',
|
||||
'Welcome' => '%s,你好!',
|
||||
'View' => '查看',
|
||||
'Add' => '添加',
|
||||
'Edit' => '编辑',
|
||||
'Del' => '删除',
|
||||
'Delete' => '删除',
|
||||
'Import' => '导入',
|
||||
'Export' => '导出',
|
||||
'All' => '全部',
|
||||
'Detail' => '详情',
|
||||
'Multi' => '批量更新',
|
||||
'Setting' => '配置',
|
||||
'Move' => '移动',
|
||||
'Name' => '名称',
|
||||
'Status' => '状态',
|
||||
'Weigh' => '权重',
|
||||
'Operate' => '操作',
|
||||
'Warning' => '温馨提示',
|
||||
'Default' => '默认',
|
||||
'Article' => '文章',
|
||||
'Page' => '单页',
|
||||
'OK' => '确定',
|
||||
'Apply' => '应用',
|
||||
'Cancel' => '取消',
|
||||
'Clear' => '清空',
|
||||
'Custom Range' => '自定义',
|
||||
'Today' => '今天',
|
||||
'Yesterday' => '昨天',
|
||||
'Last 7 days' => '最近7天',
|
||||
'Last 30 days' => '最近30天',
|
||||
'Last month' => '上月',
|
||||
'This month' => '本月',
|
||||
'Loading' => '加载中',
|
||||
'Money' => '余额',
|
||||
'Score' => '积分',
|
||||
'More' => '更多',
|
||||
'Yes' => '是',
|
||||
'No' => '否',
|
||||
'Normal' => '正常',
|
||||
'Hidden' => '隐藏',
|
||||
'Locked' => '锁定',
|
||||
'Submit' => '提交',
|
||||
'Reset' => '重置',
|
||||
'Execute' => '执行',
|
||||
'Close' => '关闭',
|
||||
'Choose' => '选择',
|
||||
'Go' => '跳转',
|
||||
'Search' => '搜索',
|
||||
'Refresh' => '刷新',
|
||||
'Install' => '安装',
|
||||
'Uninstall' => '卸载',
|
||||
'First' => '首页',
|
||||
'Previous' => '上一页',
|
||||
'Next' => '下一页',
|
||||
'Last' => '末页',
|
||||
'None' => '无',
|
||||
'Home' => '主页',
|
||||
'Online' => '在线',
|
||||
'Login' => '登录',
|
||||
'Logout' => '注销',
|
||||
'Profile' => '个人资料',
|
||||
'Index' => '首页',
|
||||
'Hot' => '热门',
|
||||
'Recommend' => '推荐',
|
||||
'Upload' => '上传',
|
||||
'Uploading' => '上传中',
|
||||
'Code' => '编号',
|
||||
'Message' => '内容',
|
||||
'Line' => '行号',
|
||||
'File' => '文件',
|
||||
'Menu' => '菜单',
|
||||
'Type' => '类型',
|
||||
'Title' => '标题',
|
||||
'Content' => '内容',
|
||||
'Append' => '追加',
|
||||
'Select' => '选择',
|
||||
'Memo' => '备注',
|
||||
'Parent' => '父级',
|
||||
'Params' => '参数',
|
||||
'Permission' => '权限',
|
||||
'Check all' => '选中全部',
|
||||
'Expand all' => '展开全部',
|
||||
'Begin time' => '开始时间',
|
||||
'End time' => '结束时间',
|
||||
'Create time' => '创建时间',
|
||||
'Update time' => '更新时间',
|
||||
'Flag' => '标志',
|
||||
'Drag to sort' => '拖动进行排序',
|
||||
'Redirect now' => '立即跳转',
|
||||
'Key' => '键',
|
||||
'Value' => '值',
|
||||
'Common search' => '普通搜索',
|
||||
'Search %s' => '搜索 %s',
|
||||
'View %s' => '查看 %s',
|
||||
'%d second%s ago' => '%d秒前',
|
||||
'%d minute%s ago' => '%d分钟前',
|
||||
'%d hour%s ago' => '%d小时前',
|
||||
'%d day%s ago' => '%d天前',
|
||||
'%d week%s ago' => '%d周前',
|
||||
'%d month%s ago' => '%d月前',
|
||||
'%d year%s ago' => '%d年前',
|
||||
'%d second%s after' => '%d秒后',
|
||||
'%d minute%s after' => '%d分钟后',
|
||||
'%d hour%s after' => '%d小时后',
|
||||
'%d day%s after' => '%d天后',
|
||||
'%d week%s after' => '%d周后',
|
||||
'%d month%s after' => '%d月后',
|
||||
'%d year%s after' => '%d年后',
|
||||
'Set to normal' => '设为正常',
|
||||
'Set to hidden' => '设为隐藏',
|
||||
'Recycle bin' => '回收站',
|
||||
'Restore' => '还原',
|
||||
'Restore all' => '还原全部',
|
||||
'Destroy' => '销毁',
|
||||
'Destroy all' => '清空回收站',
|
||||
'Nothing need restore' => '没有需要还原的数据',
|
||||
//提示
|
||||
'Go back' => '返回首页',
|
||||
'Jump now' => '立即跳转',
|
||||
'Click to search %s' => '点击搜索 %s',
|
||||
'Click to toggle' => '点击切换',
|
||||
'Operation completed' => '操作成功!',
|
||||
'Operation failed' => '操作失败!',
|
||||
'Unknown data format' => '未知的数据格式!',
|
||||
'Network error' => '网络错误!',
|
||||
'Invalid parameters' => '未知参数',
|
||||
'No results were found' => '记录未找到',
|
||||
'No rows were inserted' => '未插入任何行',
|
||||
'No rows were deleted' => '未删除任何行',
|
||||
'No rows were updated' => '未更新任何行',
|
||||
'Parameter %s can not be empty' => '参数%s不能为空',
|
||||
'Are you sure you want to delete the %s selected item?' => '确定删除选中的 %s 项?',
|
||||
'Are you sure you want to delete this item?' => '确定删除此项?',
|
||||
'Are you sure you want to delete or turncate?' => '确定删除或清空?',
|
||||
'Are you sure you want to truncate?' => '确定清空?',
|
||||
'Token verification error' => 'Token验证错误!',
|
||||
'You have no permission' => '你没有权限访问',
|
||||
'Please enter your username' => '请输入你的用户名',
|
||||
'Please enter your password' => '请输入你的密码',
|
||||
'Please login first' => '请登录后操作',
|
||||
'You can upload up to %d file%s' => '你最多还可以上传%d个文件',
|
||||
'You can choose up to %d file%s' => '你最多还可以选择%d个文件',
|
||||
'An unexpected error occurred' => '发生了一个意外错误,程序猿正在紧急处理中',
|
||||
'This page will be re-directed in %s seconds' => '页面将在 %s 秒后自动跳转',
|
||||
//菜单
|
||||
'Dashboard' => '控制台',
|
||||
'General' => '常规管理',
|
||||
'Category' => '分类管理',
|
||||
'Addon' => '插件管理',
|
||||
'Auth' => '权限管理',
|
||||
'Config' => '系统配置',
|
||||
'Attachment' => '附件管理',
|
||||
'Admin' => '管理员管理',
|
||||
'Admin log' => '管理员日志',
|
||||
'Group' => '角色组',
|
||||
'Rule' => '菜单规则',
|
||||
'User' => '会员管理',
|
||||
'User group' => '会员分组',
|
||||
'User rule' => '会员规则',
|
||||
'Select attachment' => '选择附件',
|
||||
'Update profile' => '更新个人信息',
|
||||
'Local install' => '本地安装',
|
||||
'Update state' => '禁用启用',
|
||||
'Admin group' => '超级管理组',
|
||||
'Second group' => '二级管理组',
|
||||
'Third group' => '三级管理组',
|
||||
'Second group 2' => '二级管理组2',
|
||||
'Third group 2' => '三级管理组2',
|
||||
'Dashboard tips' => '用于展示当前系统中的统计数据、统计报表及重要实时数据',
|
||||
'Config tips' => '可以在此增改系统的变量和分组,也可以自定义分组和变量,如果需要删除请从数据库中删除',
|
||||
'Category tips' => '用于统一管理网站的所有分类,分类可进行无限级分类,分类类型请在常规管理->系统配置->字典配置中添加',
|
||||
'Attachment tips' => '主要用于管理上传到服务器或第三方存储的数据',
|
||||
'Addon tips' => '可在线安装、卸载、禁用、启用插件,同时支持添加本地插件。YFCMF-TP6已上线插件商店 ,你可以发布你的免费或付费插件:<a href="https://www.iuok.cn/store.html" target="_blank">https://www.iuok.cn/store.html</a>',
|
||||
'Admin tips' => '一个管理员可以有多个角色组,左侧的菜单根据管理员所拥有的权限进行生成',
|
||||
'Admin log tips' => '管理员可以查看自己所拥有的权限的管理员日志',
|
||||
'Group tips' => '角色组可以有多个,角色有上下级层级关系,如果子角色有角色组和管理员的权限则可以派生属于自己组别的下级角色组或管理员',
|
||||
'Rule tips' => '规则通常对应一个控制器的方法,同时左侧的菜单栏数据也从规则中体现,通常建议通过命令行进行生成规则节点',
|
||||
'Access is allowed only to the super management group' => '仅超级管理组能访问',
|
||||
'Local addon' => '本地插件',
|
||||
'Security tips' => '<i class="fa fa-warning"></i> 安全提示:为了你的后台安全,请勿将后台管理入口设置为admin或yfcmf',
|
||||
];
|
||||
@@ -0,0 +1,112 @@
|
||||
<?php
|
||||
// +----------------------------------------------------------------------
|
||||
// | YwxApp [ WE CAN DO IT JUST THINK ]
|
||||
// +----------------------------------------------------------------------
|
||||
// | Copyright (c) 2026-2036 http://ywxapp.cn All rights reserved.
|
||||
// +----------------------------------------------------------------------
|
||||
// | Author: ywxapp<admin@ywxapp.cn>
|
||||
// +----------------------------------------------------------------------
|
||||
|
||||
return [
|
||||
'Id' => 'ID',
|
||||
'Title' => '名称',
|
||||
'Value' => '配置值',
|
||||
'Array key' => '键',
|
||||
'Array value' => '值',
|
||||
'File' => '文件',
|
||||
'Donate' => '打赏作者',
|
||||
'Warmtips' => '温馨提示',
|
||||
'Pay now' => '立即支付',
|
||||
'Offline install' => '离线安装',
|
||||
'Refresh addon cache' => '刷新插件缓存',
|
||||
'Userinfo' => '会员信息',
|
||||
'Online store' => '在线商店',
|
||||
'Local addon' => '本地插件',
|
||||
'Conflict tips' => '此插件中发现和现有系统中部分文件发现冲突!以下文件将会被影响,请备份好相关文件后再继续操作',
|
||||
'Login tips' => '此处登录账号为<a href="https://www.iuok.cn" target="_blank">YFCMF-TP6官网账号</a>',
|
||||
'Logined tips' => '你好!%s<br />当前你已经登录,将同步保存你的购买记录',
|
||||
'Pay tips' => '扫码支付后如果仍然无法立即下载,请不要重复支付,请稍后再重试安装!',
|
||||
'Pay click tips' => '请点击这里在新窗口中进行支付!',
|
||||
'Pay new window tips' => '请在新弹出的窗口中进行支付,支付完成后再重新点击安装按钮进行安装!',
|
||||
'Upgrade tips' => '确认升级<b>《%s》</b>?<p class="text-danger">1、请务必做好代码和数据库备份!备份!备份!<br>2、升级后如出现冗余数据,请根据需要移除即可!<br>3、不建议在生产环境升级,请在本地完成升级测试</p>如有重要数据请备份后再操作!',
|
||||
'Offline installed tips' => '安装成功!清除浏览器缓存和框架缓存后生效!',
|
||||
'Online installed tips' => '安装成功!清除浏览器缓存和框架缓存后生效!',
|
||||
'Not login tips' => '你当前未登录YFCMF-TP6,登录后将同步已购买的记录,下载时无需二次付费!',
|
||||
'Please login and try to install' => '请登录YFCMF-TP6后再进行离线安装!',
|
||||
'Not installed tips' => '请安装后再访问插件前台页面!',
|
||||
'Not enabled tips' => '插件已经禁用,请启用后再访问插件前台页面!',
|
||||
'New version tips' => '发现新版本:%s 点击查看更新日志',
|
||||
'Store now available tips' => '插件市场暂不可用,是否切换到本地插件?',
|
||||
'Switch to the local' => '切换到本地插件',
|
||||
'try to reload' => '重新尝试加载',
|
||||
'Please disable the add before trying to upgrade' => '请先禁用插件再进行升级',
|
||||
'Please disable the add before trying to uninstall' => '请先禁用插件再进行卸载',
|
||||
'Login now' => '立即登录',
|
||||
'Continue install' => '继续安装',
|
||||
'View addon home page' => '查看插件介绍和帮助',
|
||||
'View addon index page' => '查看插件前台首页',
|
||||
'View addon screenshots' => '点击查看插件截图',
|
||||
'Click to toggle status' => '点击切换插件状态',
|
||||
'Click to contact developer' => '点击与插件开发者取得联系',
|
||||
'My addon' => '我购买的插件',
|
||||
'Index' => '前台',
|
||||
'All' => '全部',
|
||||
'Uncategoried' => '未归类',
|
||||
'Recommend' => '推荐',
|
||||
'Hot' => '热门',
|
||||
'New' => '新',
|
||||
'Paying' => '付费',
|
||||
'Free' => '免费',
|
||||
'Sale' => '折扣',
|
||||
'No image' => '暂无缩略图',
|
||||
'Price' => '价格',
|
||||
'Downloads' => '下载',
|
||||
'Author' => '作者',
|
||||
'Identify' => '标识',
|
||||
'Homepage' => '主页',
|
||||
'Intro' => '介绍',
|
||||
'Version' => '版本',
|
||||
'New version' => '新版本',
|
||||
'Createtime' => '添加时间',
|
||||
'Releasetime' => '更新时间',
|
||||
'Detail' => '插件详情',
|
||||
'Document' => '文档',
|
||||
'Demo' => '演示',
|
||||
'Feedback' => '反馈BUG',
|
||||
'Install' => '安装',
|
||||
'Uninstall' => '卸载',
|
||||
'Upgrade' => '升级',
|
||||
'Setting' => '配置',
|
||||
'Disable' => '禁用',
|
||||
'Enable' => '启用',
|
||||
'Your username or email' => '你的手机号、用户名或邮箱',
|
||||
'Your password' => '你的密码',
|
||||
'Login' => '登录',
|
||||
'Logout' => '退出登录',
|
||||
'Register' => '注册账号',
|
||||
'You\'re not login' => '当前未登录',
|
||||
'Continue uninstall' => '继续卸载',
|
||||
'Continue operate' => '继续操作',
|
||||
'Install successful' => '安装成功',
|
||||
'Uninstall successful' => '卸载成功',
|
||||
'Operate successful' => '操作成功',
|
||||
'Addon name incorrect' => '插件名称不正确',
|
||||
'Addon info file was not found' => '插件配置文件未找到',
|
||||
'Addon info file data incorrect' => '插件配置信息不正确',
|
||||
'Addon already exists' => '插件已经存在',
|
||||
'Addon package download failed' => '插件下载失败',
|
||||
'Conflicting file found' => '发现冲突文件',
|
||||
'Invalid addon package' => '未验证的插件',
|
||||
'No permission to write temporary files' => '没有权限写入临时文件',
|
||||
'The addon file does not exist' => '插件主启动程序不存在',
|
||||
'The configuration file content is incorrect' => '配置文件不完整',
|
||||
'Unable to open the zip file' => '无法打开ZIP文件',
|
||||
'Unable to extract the file' => '无法解压ZIP文件',
|
||||
'Unable to open file \'%s\' for writing' => '文件(%s)没有写入权限',
|
||||
'Are you sure you want to unstall %s?' => '确认卸载<b>《%s》</b>?',
|
||||
'Delete all the addon file and cannot be recovered!' => '卸载将会删除所有插件文件且不可找回!!!',
|
||||
'Delete all the addon database and cannot be recovered!' => '删除所有插件相关数据表且不可找回!!!',
|
||||
'Please backup important data manually before uninstall!' => '如有重要数据请备份后再操作!!!',
|
||||
'The following data tables will be deleted' => '以下插件数据表将会被删除',
|
||||
'The Addon did not create a data table' => '插件未创建任何数据表',
|
||||
];
|
||||
@@ -0,0 +1,16 @@
|
||||
<?php
|
||||
// +----------------------------------------------------------------------
|
||||
// | YwxApp [ WE CAN DO IT JUST THINK ]
|
||||
// +----------------------------------------------------------------------
|
||||
// | Copyright (c) 2026-2036 http://ywxapp.cn All rights reserved.
|
||||
// +----------------------------------------------------------------------
|
||||
// | Author: ywxapp<admin@ywxapp.cn>
|
||||
// +----------------------------------------------------------------------
|
||||
|
||||
return [
|
||||
'Group' => '所属组别',
|
||||
'Loginfailure' => '登录失败次数',
|
||||
'Login time' => '最后登录',
|
||||
'Please input correct username' => '用户名只能由3-12位数字、字母、下划线组合',
|
||||
'Please input correct password' => '密码长度必须在6-16位之间,不能包含空格',
|
||||
];
|
||||
@@ -0,0 +1,15 @@
|
||||
<?php
|
||||
// +----------------------------------------------------------------------
|
||||
// | YwxApp [ WE CAN DO IT JUST THINK ]
|
||||
// +----------------------------------------------------------------------
|
||||
// | Copyright (c) 2026-2036 http://ywxapp.cn All rights reserved.
|
||||
// +----------------------------------------------------------------------
|
||||
// | Author: ywxapp<admin@ywxapp.cn>
|
||||
// +----------------------------------------------------------------------
|
||||
|
||||
return [
|
||||
'No file upload or server upload limit exceeded' => '未上传文件或超出服务器上传限制',
|
||||
'Uploaded file format is limited' => '上传文件格式受限制',
|
||||
'Uploaded file is not a valid image' => '上传文件不是有效的图片文件',
|
||||
'Upload successful' => '上传成功',
|
||||
];
|
||||
@@ -0,0 +1,25 @@
|
||||
<?php
|
||||
// +----------------------------------------------------------------------
|
||||
// | YwxApp [ WE CAN DO IT JUST THINK ]
|
||||
// +----------------------------------------------------------------------
|
||||
// | Copyright (c) 2026-2036 http://ywxapp.cn All rights reserved.
|
||||
// +----------------------------------------------------------------------
|
||||
// | Author: ywxapp<admin@ywxapp.cn>
|
||||
// +----------------------------------------------------------------------
|
||||
|
||||
return [
|
||||
'Id' => 'ID',
|
||||
'Pid' => '父ID',
|
||||
'Type' => '类型',
|
||||
'All' => '全部',
|
||||
'Image' => '图片',
|
||||
'Keywords' => '关键字',
|
||||
'Description' => '描述',
|
||||
'Diyname' => '自定义名称',
|
||||
'Createtime' => '创建时间',
|
||||
'Updatetime' => '更新时间',
|
||||
'Weigh' => '权重',
|
||||
'Category warmtips' => '温馨提示:栏目类型请前往<b>常规管理</b>-><b>系统配置</b>-><b>字典配置</b>中进行管理',
|
||||
'Can not change the parent to child or itself' => '父组别不能是它的子组别或它自己',
|
||||
'Status' => '状态',
|
||||
];
|
||||
@@ -0,0 +1,89 @@
|
||||
<?php
|
||||
// +----------------------------------------------------------------------
|
||||
// | YwxApp [ WE CAN DO IT JUST THINK ]
|
||||
// +----------------------------------------------------------------------
|
||||
// | Copyright (c) 2026-2036 http://ywxapp.cn All rights reserved.
|
||||
// +----------------------------------------------------------------------
|
||||
// | Author: ywxapp<admin@ywxapp.cn>
|
||||
// +----------------------------------------------------------------------
|
||||
|
||||
return [
|
||||
'Name' => '变量名',
|
||||
'Tip' => '提示信息',
|
||||
'Group' => '分组',
|
||||
'Type' => '类型',
|
||||
'Title' => '变量标题',
|
||||
'Value' => '变量值',
|
||||
'Basic' => '基础配置',
|
||||
'Email' => '邮件配置',
|
||||
'Attachment' => '附件配置',
|
||||
'Dictionary' => '字典配置',
|
||||
'User' => '会员配置',
|
||||
'Example' => '示例分组',
|
||||
'Extend' => '扩展属性',
|
||||
'String' => '字符',
|
||||
'Password' => '密码',
|
||||
'Text' => '文本',
|
||||
'Editor' => '编辑器',
|
||||
'Number' => '数字',
|
||||
'Date' => '日期',
|
||||
'Time' => '时间',
|
||||
'Datetime' => '日期时间',
|
||||
'Datetimerange' => '日期时间区间',
|
||||
'Image' => '图片',
|
||||
'Images' => '图片(多)',
|
||||
'File' => '文件',
|
||||
'Files' => '文件(多)',
|
||||
'Select' => '列表',
|
||||
'Selects' => '列表(多选)',
|
||||
'Switch' => '开关',
|
||||
'Checkbox' => '复选',
|
||||
'Radio' => '单选',
|
||||
'Array' => '数组',
|
||||
'Array key' => '键名',
|
||||
'Array value' => '键值',
|
||||
'City' => '城市地区',
|
||||
'Selectpage' => '关联表',
|
||||
'Selectpages' => '关联表(多选)',
|
||||
'Custom' => '自定义',
|
||||
'Please select table' => '关联表',
|
||||
'Selectpage table' => '关联表',
|
||||
'Selectpage primarykey' => '存储字段',
|
||||
'Selectpage field' => '显示字段',
|
||||
'Selectpage conditions' => '筛选条件',
|
||||
'Field title' => '字段名',
|
||||
'Field value' => '字段值',
|
||||
'Content' => '数据列表',
|
||||
'Rule' => '校验规则',
|
||||
'Site name' => '站点名称',
|
||||
'Beian' => '备案号',
|
||||
'Cdn url' => 'CDN地址',
|
||||
'Version' => '版本号',
|
||||
'Timezone' => '时区',
|
||||
'Forbidden ip' => '禁止IP',
|
||||
'Languages' => '语言',
|
||||
'Fixed page' => '后台固定页',
|
||||
'Category type' => '分类类型',
|
||||
'Config group' => '配置分组',
|
||||
'Attachment category' => '附件类别',
|
||||
'Category1' => '分类一',
|
||||
'Category2' => '分类二',
|
||||
'Rule tips' => '校验规则使用请参考Nice-validator文档',
|
||||
'Extend tips' => '扩展属性支持{id}、{name}、{group}、{title}、{value}、{content}、{rule}替换',
|
||||
'Mail type' => '邮件发送方式',
|
||||
'Mail smtp host' => 'SMTP服务器',
|
||||
'Mail smtp port' => 'SMTP端口',
|
||||
'Mail smtp user' => 'SMTP用户名',
|
||||
'Mail smtp password' => 'SMTP密码',
|
||||
'Mail vertify type' => 'SMTP验证方式',
|
||||
'Mail from' => '发件人邮箱',
|
||||
'Site name incorrect' => '网站名称错误',
|
||||
'Name already exist' => '变量名称已经存在',
|
||||
'Add new config' => '点击添加新的配置',
|
||||
'Send a test message' => '发送测试邮件',
|
||||
'Only work at development environment' => '只允许在开发环境开操作',
|
||||
'This is a test mail content' => '这是一封来自%s的校验邮件,用于校验邮件配置是否正常!',
|
||||
'This is a test mail' => '这是一封来自%s的邮件',
|
||||
'Please input your email' => '请输入测试接收者邮箱',
|
||||
'Please input correct email' => '请输入正确的邮箱地址',
|
||||
];
|
||||
@@ -0,0 +1,54 @@
|
||||
<?php
|
||||
// +----------------------------------------------------------------------
|
||||
// | YwxApp [ WE CAN DO IT JUST THINK ]
|
||||
// +----------------------------------------------------------------------
|
||||
// | Copyright (c) 2026-2036 http://ywxapp.cn All rights reserved.
|
||||
// +----------------------------------------------------------------------
|
||||
// | Author: ywxapp<admin@ywxapp.cn>
|
||||
// +----------------------------------------------------------------------
|
||||
|
||||
return [
|
||||
'Custom' => '自定义',
|
||||
'Pid' => '父ID',
|
||||
'Type' => '栏目类型',
|
||||
'Image' => '图片',
|
||||
'Total user' => '总会员数',
|
||||
'Total addon' => '总插件数',
|
||||
'Total category' => '总分类数',
|
||||
'Total admin' => '总管理员数',
|
||||
'Today user signup' => '今日注册',
|
||||
'Today user login' => '今日登录',
|
||||
'Today order' => '今日订单',
|
||||
'Unsettle order' => '未处理订单',
|
||||
'Three dnu' => '三日新增',
|
||||
'Seven dnu' => '七日新增',
|
||||
'Seven dau' => '七日活跃',
|
||||
'Thirty dau' => '月活跃',
|
||||
'Custom zone' => '这里是你的自定义数据',
|
||||
'Register user' => '注册用户数',
|
||||
'Real time' => '实时',
|
||||
'Category count' => '分类统计',
|
||||
'Category count tips' => '当前分类总记录数',
|
||||
'Database count' => '数据库统计',
|
||||
'Database table nums' => '数据表数量',
|
||||
'Database size' => '占用空间',
|
||||
'Attachment count' => '附件统计',
|
||||
'Attachment nums' => '附件数量',
|
||||
'Attachment size' => '附件大小',
|
||||
'Attachment count tips' => '当前上传的附件数量',
|
||||
'Picture count' => '图片统计',
|
||||
'Picture nums' => '图片数量',
|
||||
'Picture size' => '图片大小',
|
||||
'Server info' => '服务器信息',
|
||||
'PHP version' => 'PHP版本',
|
||||
'Sapi name' => '运行方式',
|
||||
'Debug mode' => '调试模式',
|
||||
'Software' => '环境信息',
|
||||
'Upload mode' => '上传模式',
|
||||
'Upload url' => '上传URL',
|
||||
'Upload cdn url' => '上传CDN',
|
||||
'Cdn url' => '静态资源CDN',
|
||||
'Timezone' => '时区',
|
||||
'Language' => '语言',
|
||||
'View more' => '查看更多',
|
||||
];
|
||||
@@ -0,0 +1,64 @@
|
||||
<?php
|
||||
// +----------------------------------------------------------------------
|
||||
// | YwxApp [ WE CAN DO IT JUST THINK ]
|
||||
// +----------------------------------------------------------------------
|
||||
// | Copyright (c) 2026-2036 http://ywxapp.cn All rights reserved.
|
||||
// +----------------------------------------------------------------------
|
||||
// | Author: ywxapp<admin@ywxapp.cn>
|
||||
// +----------------------------------------------------------------------
|
||||
|
||||
return [
|
||||
'Title' => '标题',
|
||||
'Search menu' => '搜索菜单',
|
||||
'Layout Options' => '布局设定',
|
||||
'Fixed Layout' => '固定布局',
|
||||
'You can\'t use fixed and boxed layouts together' => '盒子模型和固定布局不能同时启作用',
|
||||
'Boxed Layout' => '盒子布局',
|
||||
'Activate the boxed layout' => '盒子布局最大宽度将被限定为1250px',
|
||||
'Toggle Sidebar' => '切换菜单栏',
|
||||
'Toggle the left sidebar\'s state (open or collapse)' => '切换菜单栏的展示或收起',
|
||||
'Sidebar Expand on Hover' => '菜单栏自动展开',
|
||||
'Let the sidebar mini expand on hover' => '鼠标移到菜单栏自动展开',
|
||||
'Toggle Right Sidebar Slide' => '切换右侧操作栏',
|
||||
'Toggle between slide over content and push content effects' => '切换右侧操作栏覆盖或独占',
|
||||
'Toggle Right Sidebar Skin' => '切换右侧操作栏背景',
|
||||
'Toggle between dark and light skins for the right sidebar' => '将右侧操作栏背景亮色或深色切换',
|
||||
'Show sub menu' => '显示菜单栏子菜单',
|
||||
'Always show sub menu' => '菜单栏子菜单将始终显示',
|
||||
'Disable top menu badge' => '禁用顶部彩色小角标',
|
||||
'Disable top menu badge without left menu' => '左边菜单栏的彩色小角标不受影响',
|
||||
'Skins' => '皮肤',
|
||||
'You\'ve logged in, do not login again' => '你已经登录,无需重复登录',
|
||||
'Username or password can not be empty' => '用户名密码不能为空',
|
||||
'Username or password is incorrect' => '用户名或密码不正确',
|
||||
'Username is incorrect' => '用户名不正确',
|
||||
'Password is incorrect' => '密码不正确',
|
||||
'Admin is forbidden' => '管理员已经被禁止登录',
|
||||
'Please try again after 1 day' => '请于1天后再尝试登录',
|
||||
'Login successful' => '登录成功!',
|
||||
'Logout successful' => '退出成功!',
|
||||
'Verification code is incorrect' => '验证码不正确',
|
||||
'Wipe cache completed' => '清除缓存成功',
|
||||
'Wipe cache failed' => '清除缓存失败',
|
||||
'Wipe cache' => '清空缓存',
|
||||
'Wipe all cache' => '一键清除缓存',
|
||||
'Wipe content cache' => '清空内容缓存',
|
||||
'Wipe template cache' => '清除模板缓存',
|
||||
'Wipe addon cache' => '清除插件缓存',
|
||||
'Check for updates' => '检测更新',
|
||||
'Discover new version' => '发现新版本',
|
||||
'Go to download' => '去下载更新',
|
||||
'Currently is the latest version' => '当前已经是最新版本',
|
||||
'Ignore this version' => '忽略此次更新',
|
||||
'Do not remind again' => '不再提示',
|
||||
'Your current version' => '你的版本是',
|
||||
'New version' => '新版本',
|
||||
'Release notes' => '更新说明',
|
||||
'Latest news' => '最新消息',
|
||||
'View more' => '查看更多',
|
||||
'Links' => '相关链接',
|
||||
'Docs' => '官方文档',
|
||||
'Forum' => '交流社区',
|
||||
'QQ qun' => 'QQ交流群',
|
||||
'Captcha' => '验证码',
|
||||
];
|
||||
@@ -0,0 +1,27 @@
|
||||
<?php
|
||||
// +----------------------------------------------------------------------
|
||||
// | YwxApp [ WE CAN DO IT JUST THINK ]
|
||||
// +----------------------------------------------------------------------
|
||||
// | Copyright (c) 2026-2036 http://ywxapp.cn All rights reserved.
|
||||
// +----------------------------------------------------------------------
|
||||
// | Author: ywxapp<admin@ywxapp.cn>
|
||||
// +----------------------------------------------------------------------
|
||||
|
||||
return [
|
||||
'Toggle all' => '显示全部',
|
||||
'Condition' => '规则条件',
|
||||
'Remark' => '备注',
|
||||
'Icon' => '图标',
|
||||
'Alert' => '警告',
|
||||
'Name' => '规则',
|
||||
'Controller/Action' => '控制器名/方法名',
|
||||
'Ismenu' => '菜单',
|
||||
'Search icon' => '搜索图标',
|
||||
'Toggle menu visible' => '点击切换菜单显示',
|
||||
'Toggle sub menu' => '点击切换子菜单',
|
||||
'Menu tips' => '父级菜单无需匹配控制器和方法,子级菜单请使用控制器名',
|
||||
'Node tips' => '控制器/方法名,如果有目录请使用 目录名/控制器名/方法名',
|
||||
'The non-menu rule must have parent' => '非菜单规则节点必须有父级',
|
||||
'Can not change the parent to child' => '父组别不能是它的子组别',
|
||||
'Name only supports letters, numbers, underscore and slash' => 'URL规则只能是小写字母、数字、下划线和/组成',
|
||||
];
|
||||
@@ -0,0 +1,19 @@
|
||||
<?php
|
||||
// +----------------------------------------------------------------------
|
||||
// | YwxApp [ WE CAN DO IT JUST THINK ]
|
||||
// +----------------------------------------------------------------------
|
||||
// | Copyright (c) 2026-2036 http://ywxapp.cn All rights reserved.
|
||||
// +----------------------------------------------------------------------
|
||||
// | Author: ywxapp<admin@ywxapp.cn>
|
||||
// +----------------------------------------------------------------------
|
||||
|
||||
return [
|
||||
'The parent group can not be its own child' => '父组别不能是自身的子组别',
|
||||
'The parent group can not found' => '父组别未找到',
|
||||
'Group not found' => '组别未找到',
|
||||
'Can not change the parent to child' => '父组别不能是它的子组别',
|
||||
'Can not change the parent to self' => '父组别不能是它的子组别',
|
||||
'You can not delete group that contain child group and administrators' => '你不能删除含有子组和管理员的组',
|
||||
'The parent group exceeds permission limit' => '父组别超出权限范围',
|
||||
'The parent group can not be its own child or itself' => '父组别不能是它的子组别及本身',
|
||||
];
|
||||
@@ -0,0 +1,27 @@
|
||||
<?php
|
||||
// +----------------------------------------------------------------------
|
||||
// | YwxApp [ WE CAN DO IT JUST THINK ]
|
||||
// +----------------------------------------------------------------------
|
||||
// | Copyright (c) 2026-2036 http://ywxapp.cn All rights reserved.
|
||||
// +----------------------------------------------------------------------
|
||||
// | Author: ywxapp<admin@ywxapp.cn>
|
||||
// +----------------------------------------------------------------------
|
||||
|
||||
return [
|
||||
'Toggle all' => '显示全部',
|
||||
'Condition' => '规则条件',
|
||||
'Remark' => '备注',
|
||||
'Icon' => '图标',
|
||||
'Alert' => '警告',
|
||||
'Name' => '规则',
|
||||
'Controller/Action' => '控制器名/方法名',
|
||||
'Ismenu' => '菜单',
|
||||
'Search icon' => '搜索图标',
|
||||
'Toggle menu visible' => '点击切换菜单显示',
|
||||
'Toggle sub menu' => '点击切换子菜单',
|
||||
'Menu tips' => '父级菜单无需匹配控制器和方法,子级菜单请使用控制器名',
|
||||
'Node tips' => '控制器/方法名,如果有目录请使用 目录名/控制器名/方法名',
|
||||
'The non-menu rule must have parent' => '非菜单规则节点必须有父级',
|
||||
'Can not change the parent to child' => '父组别不能是它的子组别',
|
||||
'Name only supports letters, numbers, underscore and slash' => 'URL规则只能是小写字母、数字、下划线和/组成',
|
||||
];
|
||||
@@ -0,0 +1,31 @@
|
||||
<?php
|
||||
/*
|
||||
* @Author: YwxApp <ywx@ywxapp.cn>
|
||||
* @Date: 2026-04-29 02:32:33
|
||||
* @LastEditors: YwxApp <ywx@ywxapp.cn>
|
||||
* @LastEditTime: 2026-07-21 23:25:08
|
||||
* @Description:
|
||||
* @FilePath: \ywxapp_dev\app\backend\listener\AdminLog.php
|
||||
* @CustomString: Copyright (c) 2026 YwxApp
|
||||
*/
|
||||
|
||||
declare (strict_types = 1);
|
||||
namespace app\backend\listener;
|
||||
|
||||
/**
|
||||
* AdminLog 类
|
||||
*
|
||||
* @author ywxapp <admin@ywxapp.cn>
|
||||
*/
|
||||
class AdminLog
|
||||
{
|
||||
/**
|
||||
* 事件监听处理
|
||||
*
|
||||
* @return mixed
|
||||
*/
|
||||
public function handle($event)
|
||||
{
|
||||
//
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
<?php
|
||||
/*
|
||||
* @Author: YwxApp <ywx@ywxapp.cn>
|
||||
* @Date: 2026-04-29 02:32:33
|
||||
* @LastEditors: YwxApp <ywx@ywxapp.cn>
|
||||
* @LastEditTime: 2026-07-21 23:27:10
|
||||
* @Description:
|
||||
* @FilePath: \ywxapp_dev\app\backend\validate\Admin.php
|
||||
* @CustomString: Copyright (c) 2026 YwxApp
|
||||
*/
|
||||
|
||||
declare (strict_types = 1);
|
||||
return [
|
||||
\think\middleware\AllowCrossDomain::class,
|
||||
];
|
||||
@@ -0,0 +1,114 @@
|
||||
<?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;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
<?php
|
||||
/*
|
||||
* @Author: YwxApp <ywx@ywxapp.cn>
|
||||
* @Date: 2026-04-29 02:32:33
|
||||
* @LastEditors: YwxApp <ywx@ywxapp.cn>
|
||||
* @LastEditTime: 2026-08-11 14:03:07
|
||||
* @Description:
|
||||
* @FilePath: \ywxapp_dev\app\backend\route\app.php
|
||||
* @CustomString: Copyright (c) 2026 YwxApp
|
||||
*/
|
||||
|
||||
use think\facade\Route;
|
||||
// // 支持批量添加
|
||||
// Route::pattern([
|
||||
// 'name' => '\w+',
|
||||
// 'id' => '\d+',
|
||||
// ]);
|
||||
// // 指向一级控制器
|
||||
|
||||
Route::get('/', 'Index/index');
|
||||
// 用户相关路由
|
||||
Route::group('member', function () {
|
||||
Route::get('index', 'user/index'); // 用户列表
|
||||
Route::post('add', 'user/add'); // 添加用户
|
||||
Route::put('update', 'user/update'); // 更新用户
|
||||
Route::delete('delete/:id', 'user/delete'); // 删除用户
|
||||
Route::patch('status/:id/:status', 'user/status'); // 更新用户状态
|
||||
Route::get('roles', 'user/roles'); // 获取所有角色
|
||||
});
|
||||
// 用户相关路由
|
||||
Route::group('admin', function () {
|
||||
Route::get('list', 'admin/index'); // 用户列表
|
||||
Route::post('add', 'admin/add'); // 添加用户
|
||||
Route::put('update', 'admin/update'); // 更新用户
|
||||
Route::delete('delete/:id', 'admin/delete'); // 删除用户
|
||||
Route::patch('status/:id/:status', 'admin/status'); // 更新用户状态
|
||||
Route::get('roles', 'admin/roles'); // 获取所有角色
|
||||
});
|
||||
// 角色相关路由
|
||||
Route::group('role', function () {
|
||||
Route::get('list', 'Role/index'); // 角色列表
|
||||
Route::post('add', 'Role/add'); // 添加角色
|
||||
Route::put('update', 'Role/update'); // 更新角色
|
||||
Route::delete('delete/:id', 'Role/delete'); // 删除角色
|
||||
Route::patch('status/:id/:status', 'Role/status'); // 更新角色状态
|
||||
Route::get('tree', 'Role/tree'); // 获取角色树
|
||||
});
|
||||
|
||||
// 权限相关路由
|
||||
Route::group('permission', function () {
|
||||
Route::get('tree', 'Permission/tree'); // 获取权限树
|
||||
Route::get('menus', 'Permission/menus'); // 获取菜单树
|
||||
Route::get('permissions', 'Permission/permissions'); // 获取权限树
|
||||
Route::get('role/:roleId', 'Permission/rolePermissions'); // 获取角色权限
|
||||
Route::put('role/:roleId', 'Permission/updateRolePermissions'); // 更新角色权限
|
||||
});
|
||||
|
||||
// 主框架在线升级(客户端,对接中心站 upgrade 插件)
|
||||
Route::group('framework', function () {
|
||||
Route::get('index', 'Upgrade/index'); // 升级页面 / 检测接口
|
||||
Route::get('check', 'Upgrade/check'); // 检测更新(AJAX)
|
||||
Route::post('upgrade', 'Upgrade/upgrade'); // 执行升级
|
||||
});
|
||||
|
||||
// 插件:发布已安装插件到官方市场(需配置 DEVELOPER_TOKEN 开发者令牌)
|
||||
Route::post('addon/submitOfficial', 'addon/submitOfficial');
|
||||
|
||||
// 前台主导航菜单管理(后台可配置,支持两级下拉)
|
||||
Route::group('navbar', function () {
|
||||
Route::get('index', 'Navbar/index'); // 列表页
|
||||
Route::get('edit/:id', 'Navbar/edit'); // 添加/编辑页
|
||||
Route::post('edit', 'Navbar/edit'); // 添加/编辑提交
|
||||
Route::post('save', 'Navbar/save'); // 弹窗保存
|
||||
Route::post('update', 'Navbar/update'); // 弹窗更新
|
||||
Route::delete('delete/:id', 'Navbar/delete'); // 删除
|
||||
Route::post('delete', 'Navbar/delete'); // 删除(兼容)
|
||||
Route::patch('status/:id/:status', 'Navbar/status'); // 状态切换
|
||||
});
|
||||
@@ -0,0 +1,139 @@
|
||||
<?php
|
||||
/*
|
||||
* @Author: YwxApp <ywx@ywxapp.cn>
|
||||
* @Date: 2026-04-29 02:32:33
|
||||
* @LastEditors: YwxApp <ywx@ywxapp.cn>
|
||||
* @LastEditTime: 2026-07-21 23:25:55
|
||||
* @Description:
|
||||
* @FilePath: \ywxapp_dev\app\backend\service\PermissionService.php
|
||||
* @CustomString: Copyright (c) 2026 YwxApp
|
||||
*/
|
||||
|
||||
declare(strict_types=1);
|
||||
namespace app\backend\service;
|
||||
|
||||
use ywxapp\model\BackendAdmin as AdminModel;
|
||||
use ywxapp\model\RolePermission;
|
||||
use ywxapp\model\Permission as PermissionModel;
|
||||
|
||||
/**
|
||||
* 权限服务类
|
||||
*
|
||||
* // 获取权限列表
|
||||
* $permissionList = PermissionService::getUserPermissions($userId);
|
||||
* // 或者获取权限树
|
||||
* $permissionTree = PermissionService::getUserPermissionTree($userId);
|
||||
* // 或者只获取权限编码
|
||||
* $permissionCodes = PermissionService::getUserPermissionCodes($userId);
|
||||
*/
|
||||
class PermissionService
|
||||
{
|
||||
/**
|
||||
* 获取当前用户的权限列表
|
||||
* @param int $userId 用户ID
|
||||
* @return array
|
||||
*/
|
||||
public static function getUserPermissions($userId)
|
||||
{
|
||||
// 1. 获取用户角色
|
||||
$user = AdminModel::with(['roles'])->find($userId);
|
||||
if (!$user) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$roleIds = array_column($user->roles->toArray(), 'id');
|
||||
if (empty($roleIds)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
// 2. 获取角色权限ID
|
||||
$permissionIds = RolePermission::where('role_id', 'in', $roleIds)
|
||||
->column('permission_id');
|
||||
|
||||
if (empty($permissionIds)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
// 3. 获取权限详情
|
||||
return PermissionModel::where('id', 'in', $permissionIds)
|
||||
->select()
|
||||
->toArray();
|
||||
}
|
||||
/**
|
||||
* 获取当前用户的权限列表
|
||||
* @param int $userId 用户ID
|
||||
* @return array
|
||||
*/
|
||||
public static function getUserMenus($userId)
|
||||
{
|
||||
// 1. 获取用户角色
|
||||
$user = AdminModel::with(['roles'])->find($userId);
|
||||
if (!$user) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$roleIds = array_column($user->roles->toArray(), 'id');
|
||||
if (empty($roleIds)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
// 2. 获取角色权限ID
|
||||
$permissionIds = RolePermission::where('role_id', 'in', $roleIds)
|
||||
->column('permission_id');
|
||||
|
||||
if (empty($permissionIds)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
// 3. 获取权限详情
|
||||
$permissions = PermissionModel::where('type',1)->where('id', 'in', $permissionIds)
|
||||
->select()
|
||||
->toArray();
|
||||
|
||||
|
||||
return self::buildPermissionTree($permissions);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取当前用户的权限树
|
||||
* @param int $userId 用户ID
|
||||
* @return array
|
||||
*/
|
||||
public static function getUserPermissionTree($userId)
|
||||
{
|
||||
$permissions = self::getUserPermissions($userId);
|
||||
return self::buildPermissionTree($permissions);
|
||||
}
|
||||
|
||||
/**
|
||||
* 构建权限树
|
||||
* @param array $permissions 权限列表
|
||||
* @param int $parentId 父级ID
|
||||
* @return array
|
||||
*/
|
||||
protected static function buildPermissionTree($permissions, $parentId = 0)
|
||||
{
|
||||
$tree = [];
|
||||
foreach ($permissions as $permission) {
|
||||
if ($permission['pid'] == $parentId) {
|
||||
$children = self::buildPermissionTree($permissions, $permission['id']);
|
||||
if ($children) {
|
||||
$permission['children'] = $children;
|
||||
}
|
||||
$tree[] = $permission;
|
||||
}
|
||||
}
|
||||
return $tree;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取当前用户的权限编码列表
|
||||
* @param int $userId 用户ID
|
||||
* @return array
|
||||
*/
|
||||
public static function getUserPermissionCodes($userId)
|
||||
{
|
||||
$permissions = self::getUserPermissions($userId);
|
||||
return array_column($permissions, 'code');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
namespace app\backend\validate;
|
||||
|
||||
use think\Validate;
|
||||
|
||||
class Ad extends Validate
|
||||
{
|
||||
protected $rule = [
|
||||
'title' => 'require|max:200',
|
||||
'type' => 'in:1,2,3',
|
||||
'position' => 'in:home_top,home_side,popup,list_bottom,home_bottom,content_top,content_bottom,sidebar,float',
|
||||
'content' => 'require',
|
||||
'url' => 'max:255',
|
||||
'image' => 'max:255',
|
||||
'sort' => 'number',
|
||||
'status' => 'in:0,1',
|
||||
];
|
||||
|
||||
protected $message = [
|
||||
];
|
||||
|
||||
protected $scene = [
|
||||
'add' => ['title', 'type', 'position', 'content', 'url', 'image', 'sort', 'status'],
|
||||
'edit' => ['title', 'type', 'position', 'content', 'url', 'image', 'sort', 'status'],
|
||||
];
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
<?php
|
||||
/*
|
||||
* @Author: YwxApp <ywx@ywxapp.cn>
|
||||
* @Date: 2026-04-29 02:32:33
|
||||
* @LastEditors: YwxApp <ywx@ywxapp.cn>
|
||||
* @LastEditTime: 2026-07-21 23:27:10
|
||||
* @Description:
|
||||
* @FilePath: \ywxapp_dev\app\backend\validate\Admin.php
|
||||
* @CustomString: Copyright (c) 2026 YwxApp
|
||||
*/
|
||||
|
||||
declare (strict_types = 1);
|
||||
namespace app\backend\validate;
|
||||
|
||||
use think\Validate;
|
||||
|
||||
/**
|
||||
* Admin 类
|
||||
*
|
||||
* @author ywxapp <admin@ywxapp.cn>
|
||||
*/
|
||||
class Admin extends Validate
|
||||
{
|
||||
protected $rule = [
|
||||
'account' => 'require|min:5|max:20',
|
||||
'nickname' => 'require|min:2|max:20',
|
||||
'password' => 'require|min:6|max:20',
|
||||
'confirmpass' => 'require|confirm:password',
|
||||
'email' => 'require|email',
|
||||
'mobile' => 'require|mobile',
|
||||
//'status' => 'require|in:0,1',
|
||||
];
|
||||
|
||||
protected $message = [
|
||||
'account.require' => '用户名不能为空',
|
||||
'account.min' => '用户名长度不能少于3个字符',
|
||||
'account.max' => '用户名长度不能超过20个字符',
|
||||
'nickname.require' => '昵称不能为空',
|
||||
'nickname.min' => '昵称长度不能少于2个字符',
|
||||
'nickname.max' => '昵称长度不能超过20个字符',
|
||||
'password.require' => '密码不能为空',
|
||||
'password.min' => '密码长度不能少于6个字符',
|
||||
'password.max' => '密码长度不能超过20个字符',
|
||||
'confirmpass.require' => '确认密码不能为空',
|
||||
'confirmpass.confirm' => '两次输入的密码不一致',
|
||||
'email.require' => '邮箱不能为空',
|
||||
'email.email' => '邮箱格式不正确',
|
||||
'mobile.require' => '电话不能为空',
|
||||
'mobile.mobile' => '电话格式不正确',
|
||||
//'status.require' => '状态不能为空',
|
||||
//'status.in' => '状态值不正确',
|
||||
];
|
||||
|
||||
// 更新场景
|
||||
|
||||
public function sceneUpdate()
|
||||
{
|
||||
return $this->remove('password', 'require');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
<?php
|
||||
/*
|
||||
* @Author: YwxApp <ywx@ywxapp.cn>
|
||||
* @Date: 2026-04-29 02:32:33
|
||||
* @LastEditors: YwxApp <ywx@ywxapp.cn>
|
||||
* @LastEditTime: 2026-07-21 23:27:10
|
||||
* @Description:
|
||||
* @FilePath: \ywxapp_dev\app\backend\validate\Admin.php
|
||||
* @CustomString: Copyright (c) 2026 YwxApp
|
||||
*/
|
||||
|
||||
declare (strict_types = 1);
|
||||
namespace app\backend\validate;
|
||||
|
||||
use think\Validate;
|
||||
|
||||
/**
|
||||
* AdminPermission 类
|
||||
*
|
||||
* @author ywxapp <admin@ywxapp.cn>
|
||||
*/
|
||||
class AdminPermission extends Validate
|
||||
{
|
||||
/**
|
||||
* 定义验证规则
|
||||
* 格式:'字段名' => ['规则1','规则2'...]
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $rule = [
|
||||
'title' => 'require',
|
||||
'name' => 'require|unique:permission,name',
|
||||
'type' => 'in:1,2,3',
|
||||
];
|
||||
|
||||
/**
|
||||
* 定义错误信息
|
||||
* 格式:'字段名.规则名' => '错误信息'
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $message = [];
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
<?php
|
||||
/*
|
||||
* @Author: YwxApp <ywx@ywxapp.cn>
|
||||
* @Date: 2026-04-29 02:32:33
|
||||
* @LastEditors: YwxApp <ywx@ywxapp.cn>
|
||||
* @LastEditTime: 2026-07-21 23:27:10
|
||||
* @Description:
|
||||
* @FilePath: \ywxapp_dev\app\backend\validate\Admin.php
|
||||
* @CustomString: Copyright (c) 2026 YwxApp
|
||||
*/
|
||||
|
||||
declare (strict_types = 1);
|
||||
namespace app\backend\validate;
|
||||
|
||||
use think\Validate;
|
||||
|
||||
/**
|
||||
* AdminProfile 类
|
||||
*
|
||||
* @author ywxapp <admin@ywxapp.cn>
|
||||
*/
|
||||
class AdminProfile extends Validate
|
||||
{
|
||||
/**
|
||||
* 定义验证规则
|
||||
* 格式:'字段名' => ['规则1','规则2'...]
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $rule = [];
|
||||
|
||||
/**
|
||||
* 定义错误信息
|
||||
* 格式:'字段名.规则名' => '错误信息'
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $message = [];
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
<?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\validate;
|
||||
|
||||
use think\Validate;
|
||||
|
||||
/**
|
||||
* AdminRole 类
|
||||
*
|
||||
* @author ywxapp <admin@ywxapp.cn>
|
||||
*/
|
||||
class AdminRole extends Validate
|
||||
{
|
||||
/**
|
||||
* 定义验证规则
|
||||
* 格式:'字段名' => ['规则1','规则2'...]
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $rule = [];
|
||||
|
||||
/**
|
||||
* 定义错误信息
|
||||
* 格式:'字段名.规则名' => '错误信息'
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $message = [];
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
namespace app\backend\validate;
|
||||
|
||||
use think\Validate;
|
||||
|
||||
class Card extends Validate
|
||||
{
|
||||
protected $rule = [
|
||||
'cardno' => 'require|max:100',
|
||||
'password' => 'require|max:100',
|
||||
'amount' => 'float',
|
||||
'status' => 'in:0,1,2',
|
||||
'use_time' => 'number',
|
||||
'sort' => 'number',
|
||||
];
|
||||
|
||||
protected $message = [
|
||||
];
|
||||
|
||||
protected $scene = [
|
||||
'add' => ['cardno', 'password', 'amount', 'status', 'use_time', 'sort'],
|
||||
'edit' => ['cardno', 'password', 'amount', 'status', 'use_time', 'sort'],
|
||||
];
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
namespace app\backend\validate;
|
||||
|
||||
use think\Validate;
|
||||
|
||||
class Help extends Validate
|
||||
{
|
||||
protected $rule = [
|
||||
'title' => 'require|max:200',
|
||||
'content' => 'require',
|
||||
'sort' => 'number',
|
||||
'status' => 'in:0,1',
|
||||
];
|
||||
|
||||
protected $message = [
|
||||
];
|
||||
|
||||
protected $scene = [
|
||||
'add' => ['title', 'content', 'sort', 'status'],
|
||||
'edit' => ['title', 'content', 'sort', 'status'],
|
||||
];
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
<?php
|
||||
/*
|
||||
* @Author: YwxApp <ywx@ywxapp.cn>
|
||||
* @Date: 2026-04-29 02:32:33
|
||||
* @LastEditors: YwxApp <ywx@ywxapp.cn>
|
||||
* @LastEditTime: 2026-07-23 00:00:00
|
||||
* @Description: 友情链接验证器
|
||||
* @FilePath: \ywxapp_dev\app\backend\validate\Links.php
|
||||
* @CustomString: Copyright (c) 2026 YwxApp
|
||||
*/
|
||||
|
||||
declare (strict_types = 1);
|
||||
namespace app\backend\validate;
|
||||
|
||||
use think\Validate;
|
||||
|
||||
class Links extends Validate
|
||||
{
|
||||
protected $rule = [
|
||||
'title' => 'require|max:255',
|
||||
'url' => 'require|url|max:255',
|
||||
'logo' => 'max:255',
|
||||
'description' => 'max:500',
|
||||
'sort' => 'integer',
|
||||
'status' => 'in:0,1',
|
||||
];
|
||||
|
||||
protected $message = [
|
||||
'title.require' => '网站标题不能为空',
|
||||
'title.max' => '网站标题过长',
|
||||
'url.require' => '链接地址不能为空',
|
||||
'url.url' => '链接地址格式不正确',
|
||||
'url.max' => '链接地址过长',
|
||||
'logo.max' => 'Logo 地址过长',
|
||||
'description.max' => '网站描述过长',
|
||||
'sort.integer' => '排序必须为整数',
|
||||
'status.in' => '状态值非法',
|
||||
];
|
||||
|
||||
// 仅保存/完整编辑时校验标题与链接;状态开关不需要
|
||||
protected $scene = [
|
||||
'save' => ['title', 'url', 'logo', 'description', 'sort', 'status'],
|
||||
];
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
<?php
|
||||
/*
|
||||
* @Author: YwxApp <ywx@ywxapp.cn>
|
||||
* @Date: 2026-04-29 02:32:33
|
||||
* @LastEditors: YwxApp <ywx@ywxapp.cn>
|
||||
* @LastEditTime: 2026-07-21 23:27:10
|
||||
* @Description:
|
||||
* @FilePath: \ywxapp_dev\app\backend\validate\Admin.php
|
||||
* @CustomString: Copyright (c) 2026 YwxApp
|
||||
*/
|
||||
|
||||
declare (strict_types = 1);
|
||||
namespace app\backend\validate;
|
||||
|
||||
use think\Validate;
|
||||
|
||||
/**
|
||||
* Login 类
|
||||
*
|
||||
* @author ywxapp <admin@ywxapp.cn>
|
||||
*/
|
||||
class Login extends Validate
|
||||
{
|
||||
/**
|
||||
* 定义验证规则
|
||||
* 格式:'字段名' => ['规则1','规则2'...]
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $rule = [
|
||||
'username|用户名' => 'require|max:25',
|
||||
'password|密码' => 'require',
|
||||
'captcha|验证码' => 'require|captcha',
|
||||
];
|
||||
|
||||
/**
|
||||
* 定义错误信息
|
||||
* 格式:'字段名.规则名' => '错误信息'
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $message = [
|
||||
'username.require' => '请输入用户名',
|
||||
'username.max' => '名称最多不能超过25个字符',
|
||||
'password.require' => '密码错误',
|
||||
];
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
namespace app\backend\validate;
|
||||
|
||||
use think\Validate;
|
||||
|
||||
class Medal extends Validate
|
||||
{
|
||||
protected $rule = [
|
||||
'title' => 'require|max:200',
|
||||
'image' => 'max:255',
|
||||
'sort' => 'number',
|
||||
'status' => 'in:0,1',
|
||||
];
|
||||
|
||||
protected $message = [
|
||||
];
|
||||
|
||||
protected $scene = [
|
||||
'add' => ['title', 'image', 'description', 'sort', 'status'],
|
||||
'edit' => ['title', 'image', 'description', 'sort', 'status'],
|
||||
];
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
namespace app\backend\validate;
|
||||
|
||||
use think\Validate;
|
||||
|
||||
class Notice extends Validate
|
||||
{
|
||||
protected $rule = [
|
||||
'title' => 'require|max:200',
|
||||
'content' => 'require',
|
||||
'author' => 'max:50',
|
||||
'type' => 'in:1,2,3,4',
|
||||
'is_top' => 'in:0,1',
|
||||
'start_time' => 'integer',
|
||||
'end_time' => 'integer',
|
||||
'sort' => 'number',
|
||||
'status' => 'in:0,1',
|
||||
];
|
||||
|
||||
protected $message = [
|
||||
'title.require' => '请输入公告标题',
|
||||
'content.require' => '请输入公告内容',
|
||||
'type.in' => '公告类型不正确',
|
||||
];
|
||||
|
||||
protected $scene = [
|
||||
'add' => ['title', 'content', 'author', 'type', 'is_top', 'start_time', 'end_time', 'sort', 'status'],
|
||||
'edit' => ['title', 'content', 'author', 'type', 'is_top', 'start_time', 'end_time', 'sort', 'status'],
|
||||
];
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
namespace app\backend\validate;
|
||||
|
||||
use think\Validate;
|
||||
|
||||
class Prop extends Validate
|
||||
{
|
||||
protected $rule = [
|
||||
'title' => 'require|max:200',
|
||||
'icon' => 'max:255',
|
||||
'price' => 'float',
|
||||
'sort' => 'number',
|
||||
'status' => 'in:0,1',
|
||||
];
|
||||
|
||||
protected $message = [
|
||||
];
|
||||
|
||||
protected $scene = [
|
||||
'add' => ['title', 'icon', 'price', 'description', 'sort', 'status'],
|
||||
'edit' => ['title', 'icon', 'price', 'description', 'sort', 'status'],
|
||||
];
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
<?php
|
||||
/*
|
||||
* @Author: YwxApp <ywx@ywxapp.cn>
|
||||
* @Date: 2026-04-29 02:32:33
|
||||
* @LastEditors: YwxApp <ywx@ywxapp.cn>
|
||||
* @LastEditTime: 2026-07-21 23:27:10
|
||||
* @Description:
|
||||
* @FilePath: \ywxapp_dev\app\backend\validate\Admin.php
|
||||
* @CustomString: Copyright (c) 2026 YwxApp
|
||||
*/
|
||||
|
||||
declare (strict_types = 1);
|
||||
namespace app\backend\validate;
|
||||
|
||||
use think\Validate;
|
||||
|
||||
/**
|
||||
* Role 类
|
||||
*
|
||||
* @author ywxapp <admin@ywxapp.cn>
|
||||
*/
|
||||
class Role extends Validate
|
||||
{
|
||||
protected $rule = [
|
||||
'name' => 'require|min:2|max:50',
|
||||
'code' => 'require|min:2|max:50|alphaDash',
|
||||
'description' => 'max:255',
|
||||
'status' => 'require|in:0,1',
|
||||
];
|
||||
|
||||
protected $message = [
|
||||
'name.require' => '角色名称不能为空',
|
||||
'name.min' => '角色名称长度不能少于2个字符',
|
||||
'name.max' => '角色名称长度不能超过50个字符',
|
||||
'code.require' => '角色编码不能为空',
|
||||
'code.min' => '角色编码长度不能少于2个字符',
|
||||
'code.max' => '角色编码长度不能超过50个字符',
|
||||
'code.alphaDash' => '角色编码只能包含字母、数字、下划线和破折号',
|
||||
'description.max' => '角色描述长度不能超过255个字符',
|
||||
'status.require' => '状态不能为空',
|
||||
'status.in' => '状态值不正确',
|
||||
];
|
||||
|
||||
// 更新场景
|
||||
|
||||
public function sceneUpdate()
|
||||
{
|
||||
return $this->append('id', 'require|number');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
namespace app\backend\validate;
|
||||
|
||||
use think\Validate;
|
||||
|
||||
class Score extends Validate
|
||||
{
|
||||
protected $rule = [
|
||||
'name' => 'require|max:100',
|
||||
'action' => 'max:50',
|
||||
'type' => 'require|in:1,2',
|
||||
'value' => 'require|integer|gt:0',
|
||||
'sort' => 'number',
|
||||
'status' => 'in:0,1',
|
||||
];
|
||||
|
||||
protected $message = [
|
||||
'name.require' => '请输入规则名称',
|
||||
'name.max' => '规则名称最多100个字符',
|
||||
'type.require' => '请选择规则类型',
|
||||
'type.in' => '规则类型只能是获取或消费',
|
||||
'value.require' => '请输入变动值',
|
||||
'value.integer' => '变动值必须是整数',
|
||||
'value.gt' => '变动值必须大于0',
|
||||
];
|
||||
|
||||
protected $scene = [
|
||||
'add' => ['name', 'action', 'type', 'value', 'sort', 'status'],
|
||||
'edit' => ['name', 'action', 'type', 'value', 'sort', 'status'],
|
||||
];
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
namespace app\backend\validate;
|
||||
|
||||
use think\Validate;
|
||||
|
||||
class Shop extends Validate
|
||||
{
|
||||
protected $rule = [
|
||||
'title' => 'require|max:200',
|
||||
'price' => 'float',
|
||||
'stock' => 'number',
|
||||
'sort' => 'number',
|
||||
'status' => 'in:0,1',
|
||||
];
|
||||
|
||||
protected $message = [
|
||||
];
|
||||
|
||||
protected $scene = [
|
||||
'add' => ['title', 'price', 'stock', 'description', 'sort', 'status'],
|
||||
'edit' => ['title', 'price', 'stock', 'description', 'sort', 'status'],
|
||||
];
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
namespace app\backend\validate;
|
||||
|
||||
use think\Validate;
|
||||
|
||||
class Sms extends Validate
|
||||
{
|
||||
protected $rule = [
|
||||
'title' => 'require|max:200',
|
||||
'code' => 'require|max:50',
|
||||
'content' => 'require',
|
||||
'sort' => 'number',
|
||||
'status' => 'in:0,1',
|
||||
];
|
||||
|
||||
protected $message = [
|
||||
];
|
||||
|
||||
protected $scene = [
|
||||
'add' => ['title', 'code', 'content', 'sort', 'status'],
|
||||
'edit' => ['title', 'code', 'content', 'sort', 'status'],
|
||||
];
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
namespace app\backend\validate;
|
||||
|
||||
use think\Validate;
|
||||
|
||||
class Task extends Validate
|
||||
{
|
||||
protected $rule = [
|
||||
'title' => 'require|max:200',
|
||||
'reward_type' => 'in:1,2,3',
|
||||
'reward_num' => 'number',
|
||||
'sort' => 'number',
|
||||
'status' => 'in:0,1',
|
||||
];
|
||||
|
||||
protected $message = [
|
||||
];
|
||||
|
||||
protected $scene = [
|
||||
'add' => ['title', 'description', 'reward_type', 'reward_num', 'sort', 'status'],
|
||||
'edit' => ['title', 'description', 'reward_type', 'reward_num', 'sort', 'status'],
|
||||
];
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
<?php
|
||||
/*
|
||||
* @Author: YwxApp <ywx@ywxapp.cn>
|
||||
* @Date: 2026-04-29 02:32:33
|
||||
* @LastEditors: YwxApp <ywx@ywxapp.cn>
|
||||
* @LastEditTime: 2026-07-21 23:27:10
|
||||
* @Description:
|
||||
* @FilePath: \ywxapp_dev\app\backend\validate\Admin.php
|
||||
* @CustomString: Copyright (c) 2026 YwxApp
|
||||
*/
|
||||
|
||||
declare (strict_types = 1);
|
||||
namespace app\backend\validate;
|
||||
|
||||
use think\Validate;
|
||||
|
||||
/**
|
||||
* User 类
|
||||
*
|
||||
* @author ywxapp <admin@ywxapp.cn>
|
||||
*/
|
||||
class User extends Validate
|
||||
{
|
||||
protected $rule = [
|
||||
'username' => 'require|min:3|max:20',
|
||||
'nickname' => 'require|min:2|max:20',
|
||||
'password' => 'require|min:6|max:20',
|
||||
'email' => 'require|email',
|
||||
'phone' => 'require|mobile',
|
||||
'status' => 'require|in:0,1',
|
||||
'roleIds' => 'array',
|
||||
];
|
||||
|
||||
protected $message = [
|
||||
'username.require' => '用户名不能为空',
|
||||
'username.min' => '用户名长度不能少于3个字符',
|
||||
'username.max' => '用户名长度不能超过20个字符',
|
||||
'nickname.require' => '昵称不能为空',
|
||||
'nickname.min' => '昵称长度不能少于2个字符',
|
||||
'nickname.max' => '昵称长度不能超过20个字符',
|
||||
'password.require' => '密码不能为空',
|
||||
'password.min' => '密码长度不能少于6个字符',
|
||||
'password.max' => '密码长度不能超过20个字符',
|
||||
'email.require' => '邮箱不能为空',
|
||||
'email.email' => '邮箱格式不正确',
|
||||
'phone.require' => '电话不能为空',
|
||||
'phone.mobile' => '电话格式不正确',
|
||||
'status.require' => '状态不能为空',
|
||||
'status.in' => '状态值不正确',
|
||||
];
|
||||
|
||||
// 更新场景
|
||||
|
||||
public function sceneUpdate()
|
||||
{
|
||||
return $this->remove('password', 'require');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
<?php
|
||||
/*
|
||||
* @Author: YwxApp <ywx@ywxapp.cn>
|
||||
* @Date: 2026-04-29 02:32:33
|
||||
* @LastEditors: YwxApp <ywx@ywxapp.cn>
|
||||
* @LastEditTime: 2026-07-21 23:27:10
|
||||
* @Description:
|
||||
* @FilePath: \ywxapp_dev\app\backend\validate\Admin.php
|
||||
* @CustomString: Copyright (c) 2026 YwxApp
|
||||
*/
|
||||
|
||||
declare (strict_types = 1);
|
||||
namespace app\backend\validate;
|
||||
|
||||
use think\Validate;
|
||||
|
||||
/**
|
||||
* UserRule 类
|
||||
*
|
||||
* @author ywxapp <admin@ywxapp.cn>
|
||||
*/
|
||||
class UserRule extends Validate
|
||||
{
|
||||
/**
|
||||
* 定义验证规则
|
||||
* 格式:'字段名' => ['规则1','规则2'...]
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $rule = [
|
||||
'module' => 'require|max:25',
|
||||
'name' => 'unique:user_rule',
|
||||
'pid' => 'require|number',
|
||||
'sort' => 'require|number',
|
||||
'status' => 'require|number',
|
||||
'title' => 'require|max:25',
|
||||
'type' => 'require|number',
|
||||
];
|
||||
|
||||
/**
|
||||
* 定义错误信息
|
||||
* 格式:'字段名.规则名' => '错误信息'
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $message = [
|
||||
'name.require' => '名称必须',
|
||||
'name.max' => '名称最多不能超过25个字符',
|
||||
'module.require' => '年龄必须是数字',
|
||||
];
|
||||
|
||||
|
||||
public function sceneEdit()
|
||||
{
|
||||
return $this->append('id', 'number');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,114 @@
|
||||
<div class="layui-fluid">
|
||||
<div class="layui-card">
|
||||
<div class="layui-card-header">站点广告管理</div>
|
||||
<div class="layui-card-body">
|
||||
<form class="layui-form" lay-filter="data-search-form">
|
||||
<div class="layui-form-item">
|
||||
<div class="layui-inline">
|
||||
<input type="text" name="title" placeholder="标题" autocomplete="off" class="layui-input">
|
||||
</div>
|
||||
<div class="layui-inline">
|
||||
<select name="position" lay-search="">
|
||||
<option value="">全部广告位</option>
|
||||
{volist name="positionList" id="p"}
|
||||
<option value="{$key}">{$p}</option>
|
||||
{/volist}
|
||||
</select>
|
||||
</div>
|
||||
<div class="layui-inline">
|
||||
<button class="layui-btn layui-btn-normal" lay-submit lay-filter="data-search-btn"><i class="layui-icon"></i> 搜索</button>
|
||||
<button type="button" class="layui-btn layui-btn-primary" id="btn-reset">重置</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
<table class="layui-hide" id="dataTable" lay-filter="dataTable"></table>
|
||||
<script type="text/html" id="tableBar">
|
||||
<div class="layui-btn-container">
|
||||
<button class="layui-btn layui-btn-sm" lay-event="dataCreate" data-perm="ad:add"><i class="layui-icon"></i> 新增</button>
|
||||
<button class="layui-btn layui-btn-sm layui-btn-danger" lay-event="dataDelete" data-perm="ad:del"><i class="layui-icon"></i> 删除</button>
|
||||
<button class="layui-btn layui-btn-sm" lay-event="dataRecybin" data-perm="ad:restore"><i class="layui-icon"></i> 回收站</button>
|
||||
</div>
|
||||
</script>
|
||||
<script type="text/html" id="dataBar">
|
||||
<a class="layui-btn layui-btn-xs" lay-event="update" data-perm="ad:edit">编辑</a>
|
||||
<a class="layui-btn layui-btn-xs layui-btn-danger" lay-event="delete" data-perm="ad:del">删除</a>
|
||||
</script>
|
||||
<script type="text/html" id="statusTpl">
|
||||
<input type="checkbox" name="status" value="{{ d.id }}" lay-skin="switch" lay-text="开|关" lay-filter="statusSwitch" {{ d.status == 1 ? 'checked' : '' }}>
|
||||
</script>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script type="text/html" id="dataFormTpl">
|
||||
<form class="layui-form" id="wxapp-form" lay-filter="wxapp-form" style="padding:15px;">
|
||||
<input type="hidden" name="id" value="{{ d.id || '' }}">
|
||||
<div class="layui-form-item">
|
||||
<label class="layui-form-label">标题</label>
|
||||
<div class="layui-input-block">
|
||||
<input type="text" name="title" placeholder="请输入标题" class="layui-input">
|
||||
</div>
|
||||
</div>
|
||||
<div class="layui-form-item">
|
||||
<label class="layui-form-label">广告位</label>
|
||||
<div class="layui-input-block">
|
||||
<select name="position" lay-search="">
|
||||
{volist name="positionList" id="p"}
|
||||
<option value="{$key}">{$p}</option>
|
||||
{/volist}
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="layui-form-item">
|
||||
<label class="layui-form-label">类型</label>
|
||||
<div class="layui-input-block">
|
||||
<select name="type">
|
||||
{volist name="typeList" id="t"}
|
||||
<option value="{$key}">{$t}</option>
|
||||
{/volist}
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="layui-form-item">
|
||||
<label class="layui-form-label">内容</label>
|
||||
<div class="layui-input-block">
|
||||
<textarea name="content" placeholder="请输入内容" class="layui-textarea"></textarea>
|
||||
</div>
|
||||
</div>
|
||||
<div class="layui-form-item">
|
||||
<label class="layui-form-label">链接</label>
|
||||
<div class="layui-input-block">
|
||||
<input type="text" name="url" placeholder="请输入链接" class="layui-input">
|
||||
</div>
|
||||
</div>
|
||||
<div class="layui-form-item">
|
||||
<label class="layui-form-label">图片</label>
|
||||
<div class="layui-input-block">
|
||||
<input type="text" name="image" placeholder="请输入图片" class="layui-input">
|
||||
</div>
|
||||
</div>
|
||||
<div class="layui-form-item">
|
||||
<label class="layui-form-label">排序</label>
|
||||
<div class="layui-input-block">
|
||||
<input type="text" name="sort" placeholder="请输入排序" class="layui-input">
|
||||
</div>
|
||||
</div>
|
||||
<div class="layui-form-item">
|
||||
<label class="layui-form-label">状态</label>
|
||||
<div class="layui-input-block">
|
||||
<input type="checkbox" name="status" lay-skin="switch" lay-text="启用|禁用" value="1" checked>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</script>
|
||||
|
||||
<script type="text/html" id="dataRecybinTpl">
|
||||
<table class="layui-hide" id="dataRecybinTable" lay-filter="dataRecybinTable"></table>
|
||||
<script type="text/html" id="dataRecybinBarTpl">
|
||||
<a class="layui-btn layui-btn-xs" lay-event="restore" data-perm="ad:restore">恢复</a>
|
||||
<a class="layui-btn layui-btn-xs layui-btn-danger" lay-event="forcedelete" data-perm="ad:destroy">彻底删除</a>
|
||||
</script>
|
||||
</script>
|
||||
<script>
|
||||
layui.use('ad', layui.factory('ad'));
|
||||
</script>
|
||||
@@ -0,0 +1,374 @@
|
||||
<div class="layui-card" style="margin:15px;">
|
||||
<div class="layui-card-header">
|
||||
插件设计器 <span id="addonName" class="layui-badge layui-bg-blue"></span>
|
||||
<a href="{:url('addon/index')}" class="layui-btn layui-btn-sm layui-btn-primary" style="float:right;"><i class="layui-icon layui-icon-return"></i> 返回插件列表</a>
|
||||
</div>
|
||||
<div class="layui-card-body">
|
||||
|
||||
<!-- 新建模式:填写插件标识与基础信息 -->
|
||||
<div id="createBox">
|
||||
<fieldset class="layui-elem-field layui-field-title"><legend>新建插件</legend></fieldset>
|
||||
<form class="layui-form" lay-filter="createForm" style="max-width:640px;">
|
||||
<div class="layui-form-item">
|
||||
<label class="layui-form-label">插件标识</label>
|
||||
<div class="layui-input-inline" style="width:300px;">
|
||||
<input type="text" name="name" id="createName" class="layui-input" placeholder="如 myaddon(字母开头)">
|
||||
</div>
|
||||
<div class="layui-form-mid layui-word-aux">目录名 / 命名空间标识,创建后不可改</div>
|
||||
</div>
|
||||
<div class="layui-form-item">
|
||||
<label class="layui-form-label">名称</label>
|
||||
<div class="layui-input-inline" style="width:300px;"><input type="text" name="title" class="layui-input" placeholder="插件显示名称"></div>
|
||||
</div>
|
||||
<div class="layui-form-item">
|
||||
<label class="layui-form-label">简介</label>
|
||||
<div class="layui-input-inline" style="width:300px;"><input type="text" name="intro" class="layui-input"></div>
|
||||
</div>
|
||||
<div class="layui-form-item">
|
||||
<label class="layui-form-label">作者</label>
|
||||
<div class="layui-input-inline" style="width:300px;"><input type="text" name="author" class="layui-input"></div>
|
||||
</div>
|
||||
<div class="layui-form-item">
|
||||
<label class="layui-form-label">官网</label>
|
||||
<div class="layui-input-inline" style="width:300px;"><input type="text" name="website" class="layui-input"></div>
|
||||
</div>
|
||||
<div class="layui-form-item">
|
||||
<label class="layui-form-label">版本</label>
|
||||
<div class="layui-input-inline" style="width:300px;"><input type="text" name="version" value="1.0.0" class="layui-input"></div>
|
||||
</div>
|
||||
<div class="layui-form-item">
|
||||
<label class="layui-form-label">前台入口</label>
|
||||
<div class="layui-input-inline" style="width:300px;"><input type="text" name="url" class="layui-input" placeholder="如 /myaddon"></div>
|
||||
</div>
|
||||
<div class="layui-form-item">
|
||||
<div class="layui-input-block">
|
||||
<button class="layui-btn layui-btn-normal" lay-submit lay-filter="createBtn"><i class="layui-icon layui-icon-add-1"></i> 创建骨架</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<!-- 设计模式 -->
|
||||
<div id="designBox" style="display:none;">
|
||||
<div class="layui-tab" lay-filter="designTab">
|
||||
<ul class="layui-tab-title">
|
||||
<li class="layui-this">基础信息</li>
|
||||
<li>配置项</li>
|
||||
<li>菜单</li>
|
||||
<li>事件/中间件</li>
|
||||
<li>路由</li>
|
||||
<li>页面模块</li>
|
||||
</ul>
|
||||
<div class="layui-tab-content">
|
||||
|
||||
<!-- 基础信息 -->
|
||||
<div class="layui-tab-item layui-show">
|
||||
<form class="layui-form" lay-filter="basicForm" style="max-width:640px;">
|
||||
<div class="layui-form-item"><label class="layui-form-label">标识</label><div class="layui-input-inline" style="width:300px;"><input type="text" name="name" id="basicName" class="layui-input" readonly></div></div>
|
||||
<div class="layui-form-item"><label class="layui-form-label">名称</label><div class="layui-input-inline" style="width:300px;"><input type="text" name="title" class="layui-input"></div></div>
|
||||
<div class="layui-form-item"><label class="layui-form-label">简介</label><div class="layui-input-inline" style="width:300px;"><input type="text" name="intro" class="layui-input"></div></div>
|
||||
<div class="layui-form-item"><label class="layui-form-label">作者</label><div class="layui-input-inline" style="width:300px;"><input type="text" name="author" class="layui-input"></div></div>
|
||||
<div class="layui-form-item"><label class="layui-form-label">官网</label><div class="layui-input-inline" style="width:300px;"><input type="text" name="website" class="layui-input"></div></div>
|
||||
<div class="layui-form-item"><label class="layui-form-label">版本</label><div class="layui-input-inline" style="width:300px;"><input type="text" name="version" class="layui-input"></div></div>
|
||||
<div class="layui-form-item"><label class="layui-form-label">前台入口</label><div class="layui-input-inline" style="width:300px;"><input type="text" name="url" class="layui-input"></div></div>
|
||||
<div class="layui-form-item"><label class="layui-form-label">授权</label><div class="layui-input-inline" style="width:300px;"><input type="text" name="license" class="layui-input"></div></div>
|
||||
<div class="layui-form-item"><div class="layui-input-block"><button class="layui-btn" lay-submit lay-filter="basicBtn">保存基础信息</button></div></div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<!-- 配置项 -->
|
||||
<div class="layui-tab-item">
|
||||
<button class="layui-btn layui-btn-sm" id="cfgAdd"><i class="layui-icon layui-icon-add-1"></i> 新增配置项</button>
|
||||
<table class="layui-table" style="margin-top:10px;">
|
||||
<thead><tr><th>名称(name)</th><th>标题(title)</th><th>类型(type)</th><th>默认值(value)</th><th>提示(tip)</th><th>操作</th></tr></thead>
|
||||
<tbody id="cfgRows"></tbody>
|
||||
</table>
|
||||
<button class="layui-btn" id="cfgSave">保存配置项</button>
|
||||
</div>
|
||||
|
||||
<!-- 菜单 -->
|
||||
<div class="layui-tab-item">
|
||||
<div class="layui-form-item"><label class="layui-form-label">后台菜单</label><div class="layui-input-block"><textarea id="menuBackend" class="layui-textarea" style="min-height:120px;"></textarea></div></div>
|
||||
<div class="layui-form-item"><label class="layui-form-label">会员菜单</label><div class="layui-input-block"><textarea id="menuMember" class="layui-textarea" style="min-height:80px;"></textarea></div></div>
|
||||
<div class="layui-form-item"><label class="layui-form-label">前台菜单</label><div class="layui-input-block"><textarea id="menuFrontend" class="layui-textarea" style="min-height:80px;"></textarea></div></div>
|
||||
<button class="layui-btn" id="menuSave">保存菜单(JSON)</button>
|
||||
<div class="layui-form-mid layui-word-aux">格式参考现有插件 menu.json;保存时会校验 JSON。</div>
|
||||
</div>
|
||||
|
||||
<!-- 事件/中间件 -->
|
||||
<div class="layui-tab-item">
|
||||
<div class="layui-form-item"><label class="layui-form-label">events</label><div class="layui-input-block"><textarea id="evEvents" class="layui-textarea" style="min-height:100px;"></textarea></div></div>
|
||||
<div class="layui-form-item"><label class="layui-form-label">middleware</label><div class="layui-input-block"><textarea id="evMiddleware" class="layui-textarea" style="min-height:100px;"></textarea></div></div>
|
||||
<div class="layui-form-item"><label class="layui-form-label">services</label><div class="layui-input-block"><textarea id="evServices" class="layui-textarea" style="min-height:80px;"></textarea></div></div>
|
||||
<button class="layui-btn" id="hooksSave">保存事件/中间件/服务</button>
|
||||
<div class="layui-form-mid layui-word-aux">填写合法 JSON;events 含 bind/listen/subscribe,middleware 含 alias/priority。</div>
|
||||
</div>
|
||||
|
||||
<!-- 路由 -->
|
||||
<div class="layui-tab-item">
|
||||
<button class="layui-btn layui-btn-sm" id="routeAdd"><i class="layui-icon layui-icon-add-1"></i> 新增路由</button>
|
||||
<table class="layui-table" style="margin-top:10px;">
|
||||
<thead><tr><th>方法</th><th>路径(path)</th><th>控制器(controller)</th><th>方法(action)</th><th>操作</th></tr></thead>
|
||||
<tbody id="routeRows"></tbody>
|
||||
</table>
|
||||
<button class="layui-btn" id="routeSave">保存路由</button>
|
||||
</div>
|
||||
|
||||
<!-- 页面模块 -->
|
||||
<div class="layui-tab-item">
|
||||
<form class="layui-form" lay-filter="genForm" style="max-width:560px;">
|
||||
<div class="layui-form-item">
|
||||
<label class="layui-form-label">类型</label>
|
||||
<div class="layui-input-inline" style="width:200px;">
|
||||
<select name="gtype" id="genType">
|
||||
<option value="controller">控制器 Controller</option>
|
||||
<option value="model">模型 Model</option>
|
||||
<option value="event">事件 Event</option>
|
||||
<option value="listener">监听器 Listener</option>
|
||||
<option value="middleware">中间件 Middleware</option>
|
||||
<option value="service">服务 Service</option>
|
||||
<option value="subscribe">订阅者 Subscribe</option>
|
||||
<option value="validate">验证器 Validate</option>
|
||||
<option value="command">命令行 Command</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="layui-form-item" id="genLayerBox">
|
||||
<label class="layui-form-label">层级</label>
|
||||
<div class="layui-input-inline" style="width:200px;">
|
||||
<select name="layer" id="genLayer">
|
||||
<option value="frontend">前台 frontend</option>
|
||||
<option value="backend">后台 backend</option>
|
||||
<option value="member">会员 member</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="layui-form-item" id="genKindBox">
|
||||
<label class="layui-form-label">风格</label>
|
||||
<div class="layui-input-inline" style="width:200px;">
|
||||
<select name="kind" id="genKind">
|
||||
<option value="default">资源(默认)</option>
|
||||
<option value="api">API</option>
|
||||
<option value="plain">空白</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="layui-form-item">
|
||||
<label class="layui-form-label">类名</label>
|
||||
<div class="layui-input-inline" style="width:200px;"><input type="text" name="name" id="genName" class="layui-input" placeholder="如 Category"></div>
|
||||
<div class="layui-form-mid layui-word-aux">不含命名空间</div>
|
||||
</div>
|
||||
<div class="layui-form-item" id="genCmdBox" style="display:none;">
|
||||
<label class="layui-form-label">命令名</label>
|
||||
<div class="layui-input-inline" style="width:200px;"><input type="text" name="command" id="genCmd" class="layui-input" placeholder="如 myaddon:demo"></div>
|
||||
</div>
|
||||
<div class="layui-form-item"><div class="layui-input-block"><button class="layui-btn layui-btn-normal" id="genBtn">生成文件</button></div></div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<hr>
|
||||
<button class="layui-btn layui-btn-normal" id="btnInstall"><i class="layui-icon layui-icon-ok"></i> 开发安装(建表/注入菜单/启用)</button>
|
||||
<button class="layui-btn layui-btn-danger" id="btnRemove"><i class="layui-icon layui-icon-delete"></i> 删除插件</button>
|
||||
<span class="layui-word-aux">开发安装后改动实时生效,无需重新打包。</span>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
<script src="/assets/layui/layui.js"></script>
|
||||
<script>
|
||||
layui.use(['form', 'layer', 'element', 'jquery', 'http'], function () {
|
||||
var form = layui.form, layer = layui.layer, element = layui.element, $ = layui.jquery, http = layui.http;
|
||||
var addon = '{$addon|default=""}';
|
||||
|
||||
function showCreate(defName) {
|
||||
$('#createBox').show();
|
||||
$('#designBox').hide();
|
||||
if (defName) $('#createName').val(defName);
|
||||
}
|
||||
function showDesign() {
|
||||
$('#createBox').hide();
|
||||
$('#designBox').show();
|
||||
}
|
||||
|
||||
// 初始化:有 addon 则尝试读取,否则新建
|
||||
if (addon) {
|
||||
http.get('designRead?addon=' + encodeURIComponent(addon)).then(function (res) {
|
||||
if (res.code === 0) {
|
||||
fillForm(res.data);
|
||||
showDesign();
|
||||
} else {
|
||||
showCreate(addon);
|
||||
}
|
||||
}).catch(function () { showCreate(addon); });
|
||||
} else {
|
||||
showCreate('');
|
||||
}
|
||||
|
||||
function fillForm(d) {
|
||||
$('#addonName').text(d.name || '');
|
||||
addon = d.name || addon;
|
||||
var info = d.info || {};
|
||||
form.val('basicForm', {
|
||||
name: info.name || '', title: info.title || '', intro: info.intro || '',
|
||||
author: info.author || '', website: info.website || '', version: info.version || '',
|
||||
url: info.url || '', license: info.license || ''
|
||||
});
|
||||
// 配置项
|
||||
$('#cfgRows').empty();
|
||||
(d.config || []).forEach(function (f) { addCfgRow(f); });
|
||||
// 菜单
|
||||
var m = d.menu || {};
|
||||
$('#menuBackend').val(JSON.stringify(m.backend || [], null, 2));
|
||||
$('#menuMember').val(JSON.stringify(m.member || [], null, 2));
|
||||
$('#menuFrontend').val(JSON.stringify(m.frontend || [], null, 2));
|
||||
// 事件/中间件
|
||||
var ev = info.events || {};
|
||||
$('#evEvents').val(JSON.stringify(ev, null, 2));
|
||||
$('#evMiddleware').val(JSON.stringify(info.middleware || {}, null, 2));
|
||||
$('#evServices').val(JSON.stringify(info.services || [], null, 2));
|
||||
// 路由(从 route_raw 解析较复杂,这里留空由用户手动加;如需可解析 TODO)
|
||||
}
|
||||
|
||||
// ===== 新建骨架 =====
|
||||
form.on('submit(createBtn)', function (data) {
|
||||
http.post('designCreate', data.field).then(function (res) {
|
||||
if (res.code === 0) {
|
||||
layer.msg('创建成功', { icon: 1 });
|
||||
addon = data.field.name;
|
||||
location.href = 'design?addon=' + encodeURIComponent(addon);
|
||||
} else {
|
||||
layer.msg(res.message || '创建失败', { icon: 2 });
|
||||
}
|
||||
}).catch(function () { layer.msg('请求失败', { icon: 2 }); });
|
||||
return false;
|
||||
});
|
||||
|
||||
// ===== 基础信息保存 =====
|
||||
form.on('submit(basicBtn)', function (data) {
|
||||
var fd = data.field; fd.addon = addon; fd.type = 'basic';
|
||||
http.post('designSave', fd).then(function (res) {
|
||||
layer.msg(res.code === 0 ? '已保存' : (res.message || '失败'), { icon: res.code === 0 ? 1 : 2 });
|
||||
}).catch(function () { layer.msg('请求失败', { icon: 2 }); });
|
||||
return false;
|
||||
});
|
||||
|
||||
// ===== 配置项行 =====
|
||||
function addCfgRow(f) {
|
||||
f = f || {};
|
||||
var tr = $('<tr>').append(
|
||||
$('<td>').append($('<input class="layui-input cfg-name" value="' + (f.name||'') + '">')),
|
||||
$('<td>').append($('<input class="layui-input cfg-title" value="' + (f.title||'') + '">')),
|
||||
$('<td>').append($('<input class="layui-input cfg-type" value="' + (f.type||'string') + '">')),
|
||||
$('<td>').append($('<input class="layui-input cfg-value" value="' + (f.value||'') + '">')),
|
||||
$('<td>').append($('<input class="layui-input cfg-tip" value="' + (f.tip||'') + '">')),
|
||||
$('<td>').append($('<button class="layui-btn layui-btn-xs layui-btn-danger cfg-del">删除</button>'))
|
||||
);
|
||||
$('#cfgRows').append(tr);
|
||||
}
|
||||
$('#cfgAdd').on('click', function () { addCfgRow(); });
|
||||
$('#cfgRows').on('click', '.cfg-del', function () { $(this).closest('tr').remove(); });
|
||||
$('#cfgSave').on('click', function () {
|
||||
var fields = [];
|
||||
$('#cfgRows tr').each(function () {
|
||||
var t = $(this);
|
||||
fields.push({ name: t.find('.cfg-name').val(), title: t.find('.cfg-title').val(),
|
||||
type: t.find('.cfg-type').val(), value: t.find('.cfg-value').val(), tip: t.find('.cfg-tip').val() });
|
||||
});
|
||||
http.post('designSave', { addon: addon, type: 'config', fields: JSON.stringify(fields) })
|
||||
.then(function (res) { layer.msg(res.code === 0 ? '配置已保存' : (res.message || '失败'), { icon: res.code === 0 ? 1 : 2 }); })
|
||||
.catch(function () { layer.msg('请求失败', { icon: 2 }); });
|
||||
});
|
||||
|
||||
// ===== 菜单保存 =====
|
||||
$('#menuSave').on('click', function () {
|
||||
try {
|
||||
var menu = {
|
||||
backend: JSON.parse($('#menuBackend').val() || '[]'),
|
||||
member: JSON.parse($('#menuMember').val() || '[]'),
|
||||
frontend: JSON.parse($('#menuFrontend').val() || '[]')
|
||||
};
|
||||
http.post('designSave', { addon: addon, type: 'menu', menu: JSON.stringify(menu) })
|
||||
.then(function (res) { layer.msg(res.code === 0 ? '菜单已保存' : (res.message || '失败'), { icon: res.code === 0 ? 1 : 2 }); })
|
||||
.catch(function () { layer.msg('请求失败', { icon: 2 }); });
|
||||
} catch (e) { layer.msg('菜单 JSON 格式错误:' + e.message, { icon: 2 }); }
|
||||
});
|
||||
|
||||
// ===== 事件/中间件保存 =====
|
||||
$('#hooksSave').on('click', function () {
|
||||
try {
|
||||
var events = JSON.parse($('#evEvents').val() || '{}');
|
||||
var middleware = JSON.parse($('#evMiddleware').val() || '{}');
|
||||
var services = JSON.parse($('#evServices').val() || '[]');
|
||||
http.post('designSave', { addon: addon, type: 'hooks',
|
||||
events: JSON.stringify(events), middleware: JSON.stringify(middleware), services: JSON.stringify(services) })
|
||||
.then(function (res) { layer.msg(res.code === 0 ? '已保存' : (res.message || '失败'), { icon: res.code === 0 ? 1 : 2 }); })
|
||||
.catch(function () { layer.msg('请求失败', { icon: 2 }); });
|
||||
} catch (e) { layer.msg('JSON 格式错误:' + e.message, { icon: 2 }); }
|
||||
});
|
||||
|
||||
// ===== 路由行 =====
|
||||
function addRouteRow(r) {
|
||||
r = r || {};
|
||||
var tr = $('<tr>').append(
|
||||
$('<td>').append($('<select class="layui-input rt-method"><option>get</option><option>post</option><option>put</option><option>delete</option><option>any</option></select>').val(r.method||'get')),
|
||||
$('<td>').append($('<input class="layui-input rt-path" value="' + (r.path||'') + '">')),
|
||||
$('<td>').append($('<input class="layui-input rt-ctrl" value="' + (r.controller||'') + '">')),
|
||||
$('<td>').append($('<input class="layui-input rt-action" value="' + (r.action||'index') + '">')),
|
||||
$('<td>').append($('<button class="layui-btn layui-btn-xs layui-btn-danger rt-del">删除</button>'))
|
||||
);
|
||||
$('#routeRows').append(tr);
|
||||
}
|
||||
$('#routeAdd').on('click', function () { addRouteRow(); });
|
||||
$('#routeRows').on('click', '.rt-del', function () { $(this).closest('tr').remove(); });
|
||||
$('#routeSave').on('click', function () {
|
||||
var routes = [];
|
||||
$('#routeRows tr').each(function () {
|
||||
var t = $(this);
|
||||
routes.push({ method: t.find('.rt-method').val(), path: t.find('.rt-path').val(),
|
||||
controller: t.find('.rt-ctrl').val(), action: t.find('.rt-action').val() });
|
||||
});
|
||||
http.post('designSave', { addon: addon, type: 'route', routes: JSON.stringify(routes) })
|
||||
.then(function (res) { layer.msg(res.code === 0 ? '路由已保存' : (res.message || '失败'), { icon: res.code === 0 ? 1 : 2 }); })
|
||||
.catch(function () { layer.msg('请求失败', { icon: 2 }); });
|
||||
});
|
||||
|
||||
// ===== 页面模块生成 =====
|
||||
$('#genType').on('change', function () {
|
||||
var t = $(this).val();
|
||||
var isCtrl = (t === 'controller');
|
||||
$('#genLayerBox').toggle(isCtrl);
|
||||
$('#genKindBox').toggle(isCtrl);
|
||||
$('#genCmdBox').toggle(t === 'command');
|
||||
});
|
||||
$('#genBtn').on('click', function () {
|
||||
var gtype = $('#genType').val();
|
||||
var opts = { name: $('#genName').val() };
|
||||
if (gtype === 'controller') { opts.layer = $('#genLayer').val(); opts.kind = $('#genKind').val(); }
|
||||
if (gtype === 'command') { opts.command = $('#genCmd').val(); }
|
||||
if (!opts.name) { layer.msg('请填写类名', { icon: 2 }); return; }
|
||||
http.post('designGenerate', { addon: addon, gtype: gtype, opts: JSON.stringify(opts) })
|
||||
.then(function (res) { layer.msg(res.code === 0 ? ('已生成:' + (res.data.file||'')) : (res.message || '失败'), { icon: res.code === 0 ? 1 : 2 }); })
|
||||
.catch(function () { layer.msg('请求失败', { icon: 2 }); });
|
||||
});
|
||||
|
||||
// ===== 开发安装 / 删除 =====
|
||||
$('#btnInstall').on('click', function () {
|
||||
http.post('designInstall', { addon: addon }).then(function (res) {
|
||||
if (res.code === 0) { layer.msg('开发安装成功,菜单已注入', { icon: 1 }); }
|
||||
else { layer.msg(res.message || '失败', { icon: 2 }); }
|
||||
}).catch(function () { layer.msg('请求失败', { icon: 2 }); });
|
||||
});
|
||||
$('#btnRemove').on('click', function () {
|
||||
layer.confirm('确定删除该插件目录?(开发调试用,不可恢复)', function (index) {
|
||||
layer.close(index);
|
||||
http.post('designRemove', { addon: addon }).then(function (res) {
|
||||
if (res.code === 0) { layer.msg('已删除', { icon: 1 }); location.href = 'index'; }
|
||||
else { layer.msg(res.message || '失败', { icon: 2 }); }
|
||||
}).catch(function () { layer.msg('请求失败', { icon: 2 }); });
|
||||
});
|
||||
});
|
||||
});
|
||||
</script>
|
||||
@@ -0,0 +1,85 @@
|
||||
<!--
|
||||
* @Author: YwxApp <ywx@ywxapp.cn>
|
||||
* @Date: 2026-04-29 02:32:33
|
||||
* @LastEditors: YwxApp <ywx@ywxapp.cn>
|
||||
* @LastEditTime: 2026-07-31 14:57:37
|
||||
* @Description:
|
||||
* @FilePath: \ywxapp_dev\app\backend\view\addon\index.html
|
||||
* @CustomString: Copyright (c) 2026 YwxApp
|
||||
-->
|
||||
|
||||
<div class="plugin-container layui-card" style="margin-bottom: 5px;">
|
||||
<!-- 1. 顶部搜索与操作栏 -->
|
||||
<div class="layui-card-header layui-row layui-col-space15">
|
||||
<div class="layui-col-md12">
|
||||
<form class="layui-form" lay-filter="searchForm">
|
||||
<div class="layui-form-item" style="margin-bottom: 0;">
|
||||
<div class="layui-inline">
|
||||
<label class="layui-form-label" style="width: auto;">插件搜索</label>
|
||||
<div class="layui-input-inline">
|
||||
<input type="text" name="keyword" placeholder="输入插件名称..." class="layui-input">
|
||||
</div>
|
||||
</div>
|
||||
<div class="layui-inline">
|
||||
<label class="layui-form-label" style="width: auto;">运行状态</label>
|
||||
<div class="layui-input-inline">
|
||||
<select name="status">
|
||||
<option value="">全部</option>
|
||||
<option value="1">已启用</option>
|
||||
<option value="0">已停用</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="layui-inline">
|
||||
<button class="layui-btn layui-btn-normal" lay-submit lay-filter="searchBtn">
|
||||
<i class="layui-icon layui-icon-search"></i> 搜索
|
||||
</button>
|
||||
<button type="reset" class="layui-btn layui-btn-primary">重置</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<hr class="layui-border-green">
|
||||
|
||||
<!-- 2. 功能性操作按钮组 -->
|
||||
<div class="layui-card-header" style="margin: 15px;">
|
||||
<button class="layui-btn" data-type="install" id="buttonInstall" data-perm="addon:*"><i class="layui-icon layui-icon-add-1"></i> 安装新插件</button>
|
||||
<a class="layui-btn layui-btn-normal" href="{:url('addon/market')}" data-perm="addon:*"><i class="layui-icon layui-icon-app"></i> 应用市场</a>
|
||||
<a class="layui-btn layui-btn-normal" href="{:url('addon/my')}" data-perm="addon:*"><i class="layui-icon layui-icon-username"></i> 我的插件</a>
|
||||
<button class="layui-btn layui-btn-danger" data-type="batchDel" data-perm="addon:delete"><i class="layui-icon layui-icon-delete"></i> 批量卸载</button>
|
||||
<button class="layui-btn layui-btn-warm" data-type="refresh"><i class="layui-icon layui-icon-refresh"></i> 刷新列表</button>
|
||||
{if config('ywxapp.addon_developer')}
|
||||
<a class="layui-btn layui-btn-warm" href="{:url('addon/design')}"><i class="layui-icon layui-icon-set"></i> 设计插件</a>
|
||||
{/if}
|
||||
</div>
|
||||
<diiv class="layui-card-body">
|
||||
<!-- 3. 核心数据表格 -->
|
||||
<table class="layui-hide" id="dataTable" lay-filter="dataTable"></table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 状态开关模板 (控制启用/禁用) -->
|
||||
<script type="text/html" id="switchTpl">
|
||||
<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="operateTpl">
|
||||
<div class="layui-clear-space">
|
||||
{{# if(d.hasConfig){ }}
|
||||
<a class="layui-btn layui-btn-xs" lay-event="config" data-perm="addon:config"><i class="layui-icon layui-icon-set"></i> 配置</a>
|
||||
{{# } }}
|
||||
<a class="layui-btn layui-btn-xs layui-btn-danger" lay-event="uninstall" data-perm="addon:delete"><i class="layui-icon layui-icon-close"></i> 卸载</a>
|
||||
{{# if(layui.app.devToken){ }}
|
||||
<a class="layui-btn layui-btn-xs" lay-event="more"> 更多 <i class="layui-icon layui-icon-down"></i></a>
|
||||
{{# } }}
|
||||
</div>
|
||||
</script>
|
||||
<script>
|
||||
console.log(window)
|
||||
layui.use(['addon', 'table']);
|
||||
</script>
|
||||
@@ -0,0 +1,354 @@
|
||||
<div class="layui-card" style="margin-bottom:5px;">
|
||||
<div class="layui-card-header" style="display:flex;align-items:center;justify-content:space-between;">
|
||||
<div>
|
||||
<span class="title" style="font-size:18px;font-weight:700;">应用市场</span>
|
||||
{if $is_client}
|
||||
<span class="mode-badge client">客户端模式 · 连接:{$api_url}</span>
|
||||
{else /}
|
||||
<span class="mode-badge center">服务中心模式(本地市场)</span>
|
||||
{/if}
|
||||
</div>
|
||||
<span>
|
||||
<a href="{:url('addon/my')}" class="layui-btn layui-btn-normal layui-btn-sm">我的插件</a>
|
||||
<a href="{:url('addon/index')}" class="layui-btn layui-btn-primary layui-btn-sm"><i class="layui-icon layui-icon-return"></i> 返回已安装插件</a>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{if $error}
|
||||
<div class="store-banner err">{$error}</div>
|
||||
{/if}
|
||||
|
||||
<div class="layui-card-body">
|
||||
<form class="store-search" method="get" action="{:url('addon/market')}" style="margin-bottom:14px;display:flex;gap:10px;flex-wrap:wrap;align-items:center;">
|
||||
<input type="text" name="keyword" value="{$keyword|default=''}" placeholder="搜索插件名称/作者/描述" class="layui-input" style="width:240px;">
|
||||
<select name="category" class="layui-input" style="width:140px;">
|
||||
<option value="">全部分类</option>
|
||||
{volist name="categories" id="c"}
|
||||
<option value="{$c}" {eq name="category" value="$c"}selected{/eq}>{$c}</option>
|
||||
{/volist}
|
||||
</select>
|
||||
<select name="type" class="layui-input" style="width:120px;">
|
||||
<option value="">全部类型</option>
|
||||
<option value="addon" {eq name="type" value="addon"}selected{/eq}>插件</option>
|
||||
<option value="template" {eq name="type" value="template"}selected{/eq}>模板</option>
|
||||
</select>
|
||||
<select name="order" class="layui-input" style="width:130px;">
|
||||
<option value="new" {eq name="order" value="new"}selected{/eq}>最新上架</option>
|
||||
<option value="hot" {eq name="order" value="hot"}selected{/eq}>最热门</option>
|
||||
<option value="price_asc" {eq name="order" value="price_asc"}selected{/eq}>价格升序</option>
|
||||
<option value="price_desc" {eq name="order" value="price_desc"}selected{/eq}>价格降序</option>
|
||||
</select>
|
||||
<button type="submit" class="layui-btn layui-btn-sm">筛选</button>
|
||||
{if $keyword || $category || $type}<a href="{:url('addon/market')}" class="layui-btn layui-btn-sm layui-btn-primary">清空</a>{/if}
|
||||
</form>
|
||||
<div class="store-wrap">
|
||||
{if empty($list)}
|
||||
<div class="empty">暂无可用插件</div>
|
||||
{else /}
|
||||
{volist name="list" id="it"}
|
||||
<div class="layui-card plugin-card" data-logo="{$it.logo|htmlspecialchars}" data-title="{$it.title|htmlspecialchars}" data-version="{$it.version|htmlspecialchars}" data-author="{$it.author|htmlspecialchars}" data-price="{$it.price}" data-rating="{$it.rating|default=0}" data-intro="{$it.intro|htmlspecialchars}" data-screenshots="{$it.screenshots|default=''|htmlspecialchars}" data-tags="{$it.tags|default=''|htmlspecialchars}" data-name="{$it.name|htmlspecialchars}" data-installed="{$it.installed}" data-upgradable="{$it.upgradable}" data-localversion="{$it.local_version|default=''|htmlspecialchars}" data-category="{$it.category|default=''|htmlspecialchars}" data-download_count="{$it.download_count|default=0}" data-update_at="{$it.update_at|default=0}" data-changelog="{$it.changelog|default=''|htmlspecialchars}" data-require_framework="{$it.require_framework|default=''|htmlspecialchars}" data-type="{$it.type|default='addon'}" data-versions="{:htmlspecialchars(json_encode($it['versions'] ?? []))}">
|
||||
<div class="layui-card-body">
|
||||
{if $it.logo}
|
||||
<img class="plugin-logo" src="{$it.logo}" alt="">
|
||||
{/if}
|
||||
<div class="plugin-title">{$it.title}<span class="ver">v{$it.version}</span>{eq name="it.type" value="template"}<span class="type-badge tpl">模板</span>{/eq}</div>
|
||||
{if $it.category}<span class="plugin-cat">{$it.category}</span>{/if}
|
||||
<div class="plugin-author">作者:{$it.author}</div>
|
||||
{if $it.rating > 0}<div class="plugin-rate">★ <span>{$it.rating|default='0.0'}</span></div>{/if}
|
||||
{if !empty($it.tag_list)}<div class="plugin-tags">{volist name="it.tag_list" id="t"}<span class="plugin-cat">{$t}</span> {/volist}</div>{/if}
|
||||
<div class="plugin-intro">{$it.intro}</div>
|
||||
<div class="plugin-foot">
|
||||
{if $it.price > 0}
|
||||
<span class="price">¥{$it.price}</span>
|
||||
{else /}
|
||||
<span class="price free">免费</span>
|
||||
{/if}
|
||||
{if $it.installed}
|
||||
{if $it.upgradable}
|
||||
<button class="layui-btn layui-btn-xs layui-btn-warm" data-act="install" data-name="{$it.name}" data-version="{$it.version}" data-type="{$it.type|default='addon'}">升级</button>
|
||||
{else /}
|
||||
<span class="layui-badge layui-bg-green">已安装 v{$it.local_version}</span>
|
||||
{/if}
|
||||
{else /}
|
||||
<button class="layui-btn layui-btn-xs layui-btn-normal" data-act="install" data-name="{$it.name}" data-version="{$it.version}" data-type="{$it.type|default='addon'}">安装</button>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/volist}
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<style>
|
||||
.store-wrap{max-width:1100px;margin:20px auto;padding:0 14px;display:flex;flex-wrap:wrap;gap:14px;}
|
||||
.plugin-card{width:calc(33.333% - 10px);min-width:260px;flex:1 1 260px;cursor:pointer;transition:box-shadow .2s,transform .2s;}
|
||||
.plugin-card:hover{box-shadow:0 6px 20px rgba(0,0,0,.12);transform:translateY(-2px);}
|
||||
.plugin-card .layui-card-body{padding:18px;}
|
||||
.plugin-logo{width:48px;height:48px;border-radius:8px;object-fit:cover;float:left;margin-right:12px;background:#eee;}
|
||||
.plugin-title{font-size:16px;font-weight:700;line-height:24px;}
|
||||
.plugin-title .ver{color:#999;font-size:12px;font-weight:400;margin-left:6px;}
|
||||
.plugin-author{color:#999;font-size:12px;line-height:20px;}
|
||||
.plugin-cat{display:inline-block;font-size:11px;color:#1e80ff;background:#e8f3ff;border-radius:10px;padding:1px 8px;margin:2px 4px 2px 0;vertical-align:middle;}
|
||||
.type-badge{display:inline-block;font-size:11px;font-weight:400;border-radius:10px;padding:1px 8px;margin-left:6px;vertical-align:middle;}
|
||||
.type-badge.tpl{color:#722ed1;background:#f4eefe;}
|
||||
.plugin-rate{color:#ff9f00;font-size:12px;line-height:20px;}
|
||||
.plugin-rate span{color:#ff9f00;font-weight:700;}
|
||||
.plugin-tags{margin:4px 0;}
|
||||
.store-search .layui-input{display:inline-block;}
|
||||
.plugin-intro{color:#666;font-size:13px;line-height:22px;margin:12px 0;min-height:44px;}
|
||||
.plugin-foot{display:flex;align-items:center;justify-content:space-between;}
|
||||
.price{color:#e4393c;font-weight:700;font-size:16px;}
|
||||
.price.free{color:#18a058;}
|
||||
.empty{text-align:center;color:#999;padding:80px 0;width:100%;}
|
||||
.mode-badge{display:inline-block;font-size:12px;font-weight:400;padding:2px 10px;border-radius:12px;margin-left:10px;}
|
||||
.mode-badge.client{background:#e8f3ff;color:#1e80ff;}
|
||||
.mode-badge.center{background:#e8f7ef;color:#18a058;}
|
||||
.store-banner{padding:10px 20px;font-size:13px;}
|
||||
.store-banner.err{background:#fde2e2;color:#d03050;}
|
||||
|
||||
/* 详情弹窗 */
|
||||
.detail-pop{padding:20px;color:#333;}
|
||||
.dp-head{display:flex;align-items:flex-start;gap:14px;}
|
||||
.dp-logo{width:64px;height:64px;border-radius:10px;object-fit:cover;background:#eee;flex:0 0 auto;}
|
||||
.dp-meta{flex:1 1 auto;min-width:0;}
|
||||
.dp-title{font-size:18px;font-weight:700;}
|
||||
.dp-title .ver{color:#999;font-size:12px;font-weight:400;margin-left:6px;}
|
||||
.dp-author{color:#999;font-size:12px;line-height:22px;}
|
||||
.dp-rate{color:#ff9f00;font-size:13px;line-height:22px;}
|
||||
.dp-rate .rate-num{color:#999;margin-left:4px;}
|
||||
.dp-tags{margin-top:4px;}
|
||||
.dp-price{text-align:right;flex:0 0 auto;}
|
||||
.dp-gal{margin:16px 0;}
|
||||
.dp-main{width:100%;max-height:320px;object-fit:contain;background:#f5f5f5;border-radius:8px;}
|
||||
.dp-thumbs{display:flex;gap:8px;margin-top:8px;flex-wrap:wrap;}
|
||||
.shot-thumb{width:64px;height:64px;object-fit:cover;border-radius:6px;border:2px solid transparent;cursor:pointer;background:#eee;}
|
||||
.shot-thumb.active{border-color:#1e80ff;}
|
||||
.dp-intro{color:#555;font-size:14px;line-height:24px;margin:12px 0;}
|
||||
.dp-changelog{color:#555;font-size:13px;line-height:1.8;margin:4px 0;white-space:normal;word-break:break-word;}
|
||||
.dp-foot{text-align:right;margin-top:10px;}
|
||||
.dp-ver-sel{font-size:13px;font-weight:400;color:#333;border:1px solid #d9d9d9;border-radius:4px;padding:2px 6px;margin-left:6px;vertical-align:middle;background:#fff;cursor:pointer;}
|
||||
.dp-meta-extra{display:flex;flex-wrap:wrap;gap:16px;margin-top:10px;padding-top:10px;border-top:1px dashed #eee;color:#777;font-size:13px;}
|
||||
.dp-meta-extra .mi b{color:#333;font-weight:600;}
|
||||
.dp-section{margin-top:18px;}
|
||||
.dp-section h4{font-size:14px;font-weight:700;color:#333;margin:0 0 10px;padding-left:8px;border-left:3px solid #1e80ff;}
|
||||
.dp-versions{display:flex;flex-direction:column;gap:8px;}
|
||||
.ver-item{display:flex;align-items:center;justify-content:space-between;padding:10px 12px;border:1px solid #eee;border-radius:8px;cursor:pointer;transition:border-color .15s,background .15s;}
|
||||
.ver-item:hover{border-color:#1e80ff;}
|
||||
.ver-item.active{background:#e8f3ff;border-color:#1e80ff;}
|
||||
.ver-item .ver-name{font-weight:600;color:#333;}
|
||||
.ver-item .ver-date{color:#999;font-size:12px;}
|
||||
.ver-item .ver-price{color:#e4393c;font-weight:600;font-size:13px;}
|
||||
.ver-item .ver-price.free{color:#18a058;}
|
||||
@media (max-width:768px){.plugin-card{width:100%;}}
|
||||
</style>
|
||||
<script>
|
||||
layui.use(['layer', 'http'], function () {
|
||||
var layer = layui.layer, http = layui.http, $ = layui.$;
|
||||
|
||||
// 解析逗号分隔或 JSON 数组文本为列表
|
||||
function parseList(s) {
|
||||
s = (s || '').trim();
|
||||
if (!s) return [];
|
||||
if (s.charAt(0) === '[') {
|
||||
try { return JSON.parse(s); } catch (e) {}
|
||||
}
|
||||
return s.split(',').map(function (x) { return x.trim(); }).filter(Boolean);
|
||||
}
|
||||
|
||||
// 评分星级
|
||||
function starsHtml(r) {
|
||||
r = parseFloat(r) || 0;
|
||||
var html = '';
|
||||
for (var i = 1; i <= 5; i++) {
|
||||
if (r >= i) html += '<i class="star on">★</i>';
|
||||
else if (r >= i - 0.5) html += '<i class="star half">★</i>';
|
||||
else html += '<i class="star">★</i>';
|
||||
}
|
||||
return html + ' <span class="rate-num">' + r.toFixed(1) + '</span>';
|
||||
}
|
||||
|
||||
// 简单版本号比较:a>b 返回 1,a<b 返回 -1,相等 0
|
||||
function cmpVer(a, b) {
|
||||
var pa = String(a || '0').split('.'), pb = String(b || '0').split('.');
|
||||
for (var i = 0; i < Math.max(pa.length, pb.length); i++) {
|
||||
var x = parseInt(pa[i] || 0, 10), y = parseInt(pb[i] || 0, 10);
|
||||
if (x !== y) return x > y ? 1 : -1;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
function priceHtmlOf(p) {
|
||||
return (parseFloat(p) > 0)
|
||||
? '<span class="price">¥' + p + '</span>'
|
||||
: '<span class="price free">免费</span>';
|
||||
}
|
||||
|
||||
// 按所选版本生成操作按钮(已装同版=徽章;已装低版=升级;未装=安装)
|
||||
function actBtnOf(d, ver) {
|
||||
var tAttr = ' data-type="' + (d.type || 'addon') + '"';
|
||||
if (d.installed) {
|
||||
if (cmpVer(ver, d.localVersion) > 0) {
|
||||
return '<button class="layui-btn layui-btn-warm layui-btn-sm" data-act="install" data-name="' + d.name + '" data-version="' + ver + '"' + tAttr + '>升级到 v' + ver + '</button>';
|
||||
}
|
||||
if (cmpVer(ver, d.localVersion) === 0) {
|
||||
return '<span class="layui-badge layui-bg-green">已安装 v' + d.localVersion + '</span>';
|
||||
}
|
||||
return '<button class="layui-btn layui-btn-primary layui-btn-sm" data-act="install" data-name="' + d.name + '" data-version="' + ver + '"' + tAttr + '>安装 v' + ver + '(低于本地 v' + d.localVersion + ')</button>';
|
||||
}
|
||||
return '<button class="layui-btn layui-btn-normal layui-btn-sm" data-act="install" data-name="' + d.name + '" data-version="' + ver + '"' + tAttr + '>安装 v' + ver + '</button>';
|
||||
}
|
||||
|
||||
// 时间戳格式化为 YYYY-MM-DD
|
||||
function fmtDate(ts) {
|
||||
ts = parseInt(ts || 0, 10);
|
||||
if (!ts) return '';
|
||||
var d = new Date(ts * 1000);
|
||||
var p = function (n) { return (n < 10 ? '0' : '') + n; };
|
||||
return d.getFullYear() + '-' + p(d.getMonth() + 1) + '-' + p(d.getDate());
|
||||
}
|
||||
|
||||
// HTML 转义(防 XSS)
|
||||
function escapeHtml(s) {
|
||||
return String(s == null ? '' : s)
|
||||
.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>')
|
||||
.replace(/"/g, '"').replace(/'/g, ''');
|
||||
}
|
||||
// 换行转 <br>
|
||||
function nl2br(s) {
|
||||
return String(s == null ? '' : s).replace(/\r\n|\r|\n/g, '<br>');
|
||||
}
|
||||
|
||||
function openDetail(d) {
|
||||
var shots = parseList(d.screenshots);
|
||||
var mainShot = shots.length ? shots[0] : (d.logo || '');
|
||||
var thumbs = shots.map(function (u, i) {
|
||||
return '<img class="shot-thumb' + (i === 0 ? ' active' : '') + '" src="' + u + '" data-src="' + u + '">';
|
||||
}).join('');
|
||||
var tags = parseList(d.tags).map(function (t) {
|
||||
return '<span class="plugin-cat">' + t + '</span>';
|
||||
}).join('');
|
||||
|
||||
// 历史版本(服务端已按版本号降序返回);无 versions 时退化为当前版本单项
|
||||
var versions = [];
|
||||
try { versions = JSON.parse(d.versions || '[]'); } catch (e) {}
|
||||
if (!versions || !versions.length) versions = [{ version: d.version, price: d.price, update_at: d.updateAt || 0 }];
|
||||
|
||||
// 版本选择:多版本时渲染下拉,否则只显示版本号
|
||||
var verSelHtml;
|
||||
if (versions.length > 1) {
|
||||
var opts = versions.map(function (v, i) {
|
||||
var tag = (parseFloat(v.price) > 0) ? '¥' + v.price : '免费';
|
||||
return '<option value="' + i + '"' + (i === 0 ? ' selected' : '') + '>v' + v.version + '(' + tag + ')' + (i === 0 ? ' · 最新' : '') + '</option>';
|
||||
}).join('');
|
||||
verSelHtml = '<select class="dp-ver-sel" id="dpVerSel">' + opts + '</select>';
|
||||
} else {
|
||||
verSelHtml = '<span class="ver">v' + versions[0].version + '</span>';
|
||||
}
|
||||
|
||||
// 版本历史区块(DZ 风格:列出全部版本,点击切换)
|
||||
var verHistory = versions.map(function (v, i) {
|
||||
var pt = (parseFloat(v.price) > 0) ? '¥' + v.price : '免费';
|
||||
return '<div class="ver-item' + (i === 0 ? ' active' : '') + '" data-idx="' + i + '">'
|
||||
+ '<span class="ver-name">v' + v.version + (i === 0 ? ' · 最新' : '') + '</span>'
|
||||
+ '<span class="ver-date">' + (v.update_at ? fmtDate(v.update_at) : '—') + '</span>'
|
||||
+ '<span class="ver-price' + (parseFloat(v.price) > 0 ? '' : ' free') + '">' + pt + '</span>'
|
||||
+ '</div>';
|
||||
}).join('');
|
||||
|
||||
var cur = versions[0];
|
||||
// 信息栏:评分 / 下载量 / 更新时间 / 分类
|
||||
var extra = ''
|
||||
+ (d.rating > 0 ? '<span class="mi">评分:<b>' + (parseFloat(d.rating) || 0).toFixed(1) + '</b></span>' : '')
|
||||
+ (d.downloadCount ? '<span class="mi">下载:<b>' + d.downloadCount + '</b></span>' : '')
|
||||
+ (d.updateAt && fmtDate(d.updateAt) ? '<span class="mi">更新:<b>' + fmtDate(d.updateAt) + '</b></span>' : '')
|
||||
+ (d.category ? '<span class="mi">分类:<b>' + d.category + '</b></span>' : '')
|
||||
+ (d.requireFramework ? '<span class="mi">最低框架:<b>v' + d.requireFramework + '</b></span>' : '');
|
||||
|
||||
var html = ''
|
||||
+ '<div class="detail-pop">'
|
||||
+ '<div class="dp-head">'
|
||||
+ (d.logo ? '<img class="dp-logo" src="' + d.logo + '">' : '')
|
||||
+ '<div class="dp-meta">'
|
||||
+ '<div class="dp-title">' + d.title + ' ' + verSelHtml + '</div>'
|
||||
+ '<div class="dp-author">作者:' + d.author + '</div>'
|
||||
+ '<div class="dp-rate">' + starsHtml(d.rating) + '</div>'
|
||||
+ (tags ? '<div class="dp-tags">' + tags + '</div>' : '')
|
||||
+ '</div>'
|
||||
+ '<div class="dp-price" id="dpPrice">' + priceHtmlOf(cur.price) + '</div>'
|
||||
+ '</div>'
|
||||
+ (extra ? '<div class="dp-meta-extra">' + extra + '</div>' : '')
|
||||
+ (shots.length ? '<div class="dp-gal"><img class="dp-main" src="' + mainShot + '"><div class="dp-thumbs">' + thumbs + '</div></div>' : '')
|
||||
+ '<div class="dp-intro">' + (d.intro || '暂无简介') + '</div>'
|
||||
+ (d.changelog ? '<div class="dp-section"><h4>更新日志</h4><div class="dp-changelog">' + nl2br(escapeHtml(d.changelog)) + '</div></div>' : '')
|
||||
+ (versions.length > 1 ? '<div class="dp-section"><h4>历史版本</h4><div class="dp-versions" id="dpVers">' + verHistory + '</div></div>' : '')
|
||||
+ '<div class="dp-foot" id="dpFoot">' + actBtnOf(d, cur.version) + '</div>'
|
||||
+ '</div>';
|
||||
layer.open({ type: 1, title: false, area: ['680px', 'auto'], shadeClose: true, content: html, success: function () {
|
||||
bindDetailEvents(versions, d);
|
||||
}});
|
||||
}
|
||||
|
||||
// 绑定详情弹窗内交互:截略图切换、版本切换(下拉/历史列表联动价格与安装按钮)
|
||||
function bindDetailEvents(versions, d) {
|
||||
function apply(idx) {
|
||||
var v = versions[idx] || versions[0];
|
||||
$('#dpPrice').html(priceHtmlOf(v.price));
|
||||
$('#dpFoot').html(actBtnOf(d, v.version));
|
||||
$('#dpVerSel').val(String(idx));
|
||||
$('#dpVers .ver-item').removeClass('active').filter('[data-idx="' + idx + '"]').addClass('active');
|
||||
}
|
||||
$('.dp-thumbs .shot-thumb').off('click').on('click', function () {
|
||||
$('.dp-main').attr('src', $(this).data('src'));
|
||||
$('.dp-thumbs .shot-thumb').removeClass('active');
|
||||
$(this).addClass('active');
|
||||
});
|
||||
$('#dpVerSel').off('change').on('change', function () { apply(parseInt(this.value, 10)); });
|
||||
$('#dpVers .ver-item').off('click').on('click', function () { apply(parseInt($(this).data('idx'), 10)); });
|
||||
}
|
||||
|
||||
// 卡片点击(安装按钮除外)打开详情
|
||||
$(document).on('click', '.plugin-card', function (e) {
|
||||
if ($(e.target).closest('[data-act="install"]').length) return;
|
||||
var $c = $(this);
|
||||
openDetail({
|
||||
logo: $c.data('logo'), title: $c.data('title'), version: $c.data('version'),
|
||||
author: $c.data('author'), price: $c.data('price'), rating: $c.data('rating'),
|
||||
intro: $c.data('intro'), screenshots: $c.data('screenshots'), tags: $c.data('tags'),
|
||||
name: $c.data('name'), installed: $c.data('installed') === '1',
|
||||
upgradable: $c.data('upgradable') === '1', localVersion: $c.data('localversion'),
|
||||
category: $c.data('category'), downloadCount: $c.data('download_count'), updateAt: $c.data('update_at'),
|
||||
changelog: $c.data('changelog'), requireFramework: $c.data('require_framework'),
|
||||
type: $c.data('type') || 'addon',
|
||||
versions: $c.attr('data-versions') || '[]'
|
||||
});
|
||||
});
|
||||
|
||||
// 安装 / 升级(卡片与弹窗内按钮共用)
|
||||
$(document).on('click', '[data-act="install"]', function () {
|
||||
var $btn = $(this);
|
||||
var name = $btn.data('name');
|
||||
var version = $btn.data('version');
|
||||
var type = $btn.data('type') || 'addon';
|
||||
var label = type === 'template' ? '模板' : '';
|
||||
layer.confirm('确认安装' + label + '「' + name + '」' + (version ? ' v' + version : '') + '?', function (idx) {
|
||||
layer.close(idx);
|
||||
var loading = layer.load();
|
||||
http.post('{:url("addon/downloadInstall")}', { name: name, version: version, type: type })
|
||||
.then(function (res) {
|
||||
layer.close(loading);
|
||||
if (res.code === 0) {
|
||||
layer.msg(res.message || '安装成功', { icon: 1 });
|
||||
setTimeout(function () { location.reload(); }, 800);
|
||||
} else {
|
||||
layer.msg(res.message || '安装失败', { icon: 2 });
|
||||
}
|
||||
})
|
||||
.catch(function () {
|
||||
layer.close(loading);
|
||||
layer.msg('请求失败', { icon: 2 });
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
</script>
|
||||
@@ -0,0 +1,60 @@
|
||||
<div class="layui-card" style="margin-bottom:5px;">
|
||||
<div class="layui-card-header" style="display:flex;align-items:center;justify-content:space-between;">
|
||||
<div>
|
||||
<span class="title" style="font-size:18px;font-weight:700;">我的插件</span>
|
||||
{if $is_client}
|
||||
<span class="mode-badge client">客户端模式 · 连接:{$api_url|default=''}</span>
|
||||
{else /}
|
||||
<span class="mode-badge center">服务中心模式(本地市场)</span>
|
||||
{/if}
|
||||
</div>
|
||||
<span>
|
||||
<a href="{:url('addon/market')}" class="layui-btn layui-btn-normal layui-btn-sm">应用市场</a>
|
||||
<a href="{:url('addon/index')}" class="layui-btn layui-btn-primary layui-btn-sm">完整管理</a>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div class="layui-card-body">
|
||||
<div class="store-wrap">
|
||||
{if empty($list)}
|
||||
<div class="empty">本机尚未安装任何插件,去 <a href="{:url('addon/market')}">应用市场</a> 看看吧</div>
|
||||
{else /}
|
||||
{volist name="list" id="it"}
|
||||
<div class="layui-card plugin-card">
|
||||
<div class="layui-card-body">
|
||||
<div class="plugin-title">{$it.title}<span class="ver">v{$it.version}</span></div>
|
||||
<div class="plugin-author">标识:{$it.name} · 作者:{$it.author|default='官方'}</div>
|
||||
<div class="plugin-intro">{$it.description|default=''}</div>
|
||||
<div class="plugin-foot">
|
||||
{if $it.status == 1}
|
||||
<span class="layui-badge layui-bg-green">已启用</span>
|
||||
{else /}
|
||||
<span class="layui-badge">已停用</span>
|
||||
{/if}
|
||||
<span>
|
||||
<a href="{:url('addon/setting')}?addon={$it.name}" class="layui-btn layui-btn-xs layui-btn-primary">配置</a>
|
||||
<a href="{:url('addon/index')}" class="layui-btn layui-btn-xs layui-btn-normal">升级/卸载</a>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/volist}
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<style>
|
||||
.store-wrap{max-width:1100px;margin:20px auto;padding:0 14px;display:flex;flex-wrap:wrap;gap:14px;}
|
||||
.plugin-card{width:calc(33.333% - 10px);min-width:260px;flex:1 1 260px;}
|
||||
.plugin-card .layui-card-body{padding:18px;}
|
||||
.plugin-title{font-size:16px;font-weight:700;line-height:24px;}
|
||||
.plugin-title .ver{color:#999;font-size:12px;font-weight:400;margin-left:6px;}
|
||||
.plugin-author{color:#999;font-size:12px;line-height:20px;}
|
||||
.plugin-intro{color:#666;font-size:13px;line-height:22px;margin:12px 0;min-height:44px;}
|
||||
.plugin-foot{display:flex;align-items:center;justify-content:space-between;}
|
||||
.empty{text-align:center;color:#999;padding:80px 0;width:100%;}
|
||||
.mode-badge{display:inline-block;font-size:12px;font-weight:400;padding:2px 10px;border-radius:12px;margin-left:10px;}
|
||||
.mode-badge.client{background:#e8f3ff;color:#1e80ff;}
|
||||
.mode-badge.center{background:#e8f7ef;color:#18a058;}
|
||||
@media (max-width:768px){.plugin-card{width:100%;}}
|
||||
</style>
|
||||
@@ -0,0 +1,134 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="renderer" content="webkit">
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1">
|
||||
<link href="/assets/layui/css/layui.css" rel="stylesheet">
|
||||
<link href="/assets/ywxapp/css/ywxapp.css" rel="stylesheet">
|
||||
<style>
|
||||
html,
|
||||
body {
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
body {
|
||||
padding-bottom: 64px;
|
||||
}
|
||||
|
||||
.layui-card {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.layui-card-body {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.layui-table-view {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.layui-table-box {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.layui-table-body {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex: 1;
|
||||
}
|
||||
</style>
|
||||
<script src="/assets/layui/layui.js"></script>
|
||||
<script type="text/javascript">
|
||||
// 全局路径配置:仅注入部署参数,路由方法来自 layui 的 route 模块。
|
||||
// app 为 ThinkPHP 真实应用目录名(backend/member/frontend/home),对应 public/static/<app> 目录。
|
||||
layui.app = {
|
||||
root: "{$site.root|default=''}",
|
||||
assetUrl: "/assets",
|
||||
module: "{$site.module}",
|
||||
realModule: "{$site.app}",
|
||||
routeBase: "{$route_base}",
|
||||
controller: "{$site.controller}",
|
||||
action: "{$site.action}"
|
||||
};
|
||||
</script>
|
||||
<script src="/assets/ywxapp/ywxapp.js" module="{$site.app}"></script>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div class="layui-card" style="margin:15px;">
|
||||
<div class="layui-card-header">插件配置:{$addon}</div>
|
||||
<div class="layui-card-body">
|
||||
{if empty($fields)}
|
||||
<div class="layui-form-mid layui-word-aux">该插件未定义 config.php,暂无可配置项。</div>
|
||||
{else /}
|
||||
<form class="layui-form" lay-filter="cfgForm">
|
||||
{volist name="fields" id="f"}
|
||||
<div class="layui-form-item">
|
||||
<label class="layui-form-label">{$f.title}</label>
|
||||
<div class="layui-input-block">
|
||||
{if $f.type == 'textarea'}
|
||||
<textarea name="{$f.name}" class="layui-textarea">{$f.value}</textarea>
|
||||
{elseif $f.type == 'select' /}
|
||||
<select name="{$f.name}">
|
||||
{volist name="$f.options" id="opt"}
|
||||
<option value="{$key}" {if $key==$f.value}selected{/if}>{$opt}</option>
|
||||
{/volist}
|
||||
</select>
|
||||
{elseif $f.type == 'radio' /}
|
||||
{volist name="$f.options" id="opt"}
|
||||
<input type="radio" name="{$f.name}" value="{$key}" title="{$opt}" {if $key==$f.value}checked{/if}>
|
||||
{/volist}
|
||||
{elseif $f.type == 'checkbox' /}
|
||||
{volist name="$f.options" id="opt"}
|
||||
<input type="checkbox" name="{$f.name}[]" value="{$key}" title="{$opt}" {if in_array($key,(array)$f.value)}checked{/if}>
|
||||
{/volist}
|
||||
{elseif $f.type == 'number' /}
|
||||
<input type="number" name="{$f.name}" value="{$f.value}" class="layui-input">
|
||||
{else /}
|
||||
<input type="text" name="{$f.name}" value="{$f.value}" class="layui-input">
|
||||
{/if}
|
||||
{if $f.tip}<div class="layui-form-mid layui-word-aux">{$f.tip}</div>{/if}
|
||||
</div>
|
||||
</div>
|
||||
{/volist}
|
||||
<div class="layui-form-item">
|
||||
<div class="layui-input-block">
|
||||
<button class="layui-btn" lay-submit lay-filter="cfgForm">保存</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
<script src="/assets/layui/layui.js"></script>
|
||||
<script>
|
||||
layui.use(['form', 'layer'], function () {
|
||||
var form = layui.form, $ = layui.jquery, layer = layui.layer;
|
||||
form.on('submit(cfgForm)', function (data) {
|
||||
$.post(window.location.href, data.field, function (r) {
|
||||
if (r.code === 0) {
|
||||
layer.msg(r.message || '保存成功', { icon: 1 });
|
||||
} else {
|
||||
layer.msg(r.message || '保存失败', { icon: 2 });
|
||||
}
|
||||
}, 'json');
|
||||
return false;
|
||||
});
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
@@ -0,0 +1,394 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>插件性能监控</title>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<link rel="stylesheet" href="/assets/layui/css/layui.css">
|
||||
<style>
|
||||
body { background: #f2f3f5; padding: 15px; }
|
||||
.stat-card {
|
||||
background: #fff;
|
||||
border-radius: 4px;
|
||||
padding: 20px;
|
||||
margin-bottom: 15px;
|
||||
box-shadow: 0 1px 3px rgba(0,0,0,0.1);
|
||||
}
|
||||
.stat-card .value {
|
||||
font-size: 28px;
|
||||
font-weight: bold;
|
||||
color: #1e9fff;
|
||||
margin: 10px 0;
|
||||
}
|
||||
.stat-card .label {
|
||||
color: #666;
|
||||
font-size: 14px;
|
||||
}
|
||||
.stat-card.warning .value { color: #ffb800; }
|
||||
.stat-card.danger .value { color: #ff5722; }
|
||||
.stat-card.success .value { color: #5fb878; }
|
||||
|
||||
.chart-container {
|
||||
background: #fff;
|
||||
border-radius: 4px;
|
||||
padding: 20px;
|
||||
margin-bottom: 15px;
|
||||
box-shadow: 0 1px 3px rgba(0,0,0,0.1);
|
||||
min-height: 400px;
|
||||
}
|
||||
|
||||
.addon-list-item {
|
||||
background: #fff;
|
||||
border-radius: 4px;
|
||||
padding: 15px;
|
||||
margin-bottom: 10px;
|
||||
box-shadow: 0 1px 3px rgba(0,0,0,0.1);
|
||||
}
|
||||
|
||||
.addon-status {
|
||||
display: inline-block;
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
border-radius: 50%;
|
||||
margin-right: 8px;
|
||||
}
|
||||
.addon-status.healthy { background: #5fb878; }
|
||||
.addon-status.warning { background: #ffb800; }
|
||||
.addon-status.danger { background: #ff5722; }
|
||||
|
||||
.performance-bar {
|
||||
height: 8px;
|
||||
background: #f0f0f0;
|
||||
border-radius: 4px;
|
||||
overflow: hidden;
|
||||
margin-top: 5px;
|
||||
}
|
||||
.performance-bar-fill {
|
||||
height: 100%;
|
||||
background: #1e9fff;
|
||||
transition: width 0.3s;
|
||||
}
|
||||
.performance-bar-fill.warning { background: #ffb800; }
|
||||
.performance-bar-fill.danger { background: #ff5722; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="layui-fluid">
|
||||
<!-- 统计卡片 -->
|
||||
<div class="layui-row layui-col-space15">
|
||||
<div class="layui-col-md3">
|
||||
<div class="stat-card">
|
||||
<div class="label">总插件数</div>
|
||||
<div class="value" id="totaladdon">-</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="layui-col-md3">
|
||||
<div class="stat-card success">
|
||||
<div class="label">健康插件</div>
|
||||
<div class="value" id="healthyaddon">-</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="layui-col-md3">
|
||||
<div class="stat-card warning">
|
||||
<div class="label">问题插件</div>
|
||||
<div class="value" id="problematicaddon">-</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="layui-col-md3">
|
||||
<div class="stat-card">
|
||||
<div class="label">平均执行时间</div>
|
||||
<div class="value"><span id="avgExecutionTime">-</span> ms</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 性能图表 -->
|
||||
<div class="layui-row layui-col-space15" style="margin-top: 15px;">
|
||||
<div class="layui-col-md6">
|
||||
<div class="chart-container">
|
||||
<div style="margin-bottom: 15px;">
|
||||
<button class="layui-btn layui-btn-sm layui-btn-primary" data-type="execution_time">执行时间</button>
|
||||
<button class="layui-btn layui-btn-sm layui-btn-primary" data-type="memory_usage">内存使用</button>
|
||||
<button class="layui-btn layui-btn-sm layui-btn-primary" data-type="success_rate">成功率</button>
|
||||
<button class="layui-btn layui-btn-sm layui-btn-primary" data-type="call_count">调用次数</button>
|
||||
</div>
|
||||
<div id="performanceChart" style="height: 350px;"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="layui-col-md6">
|
||||
<div class="chart-container">
|
||||
<div style="margin-bottom: 15px;">
|
||||
<span style="font-weight: bold;">插件性能列表</span>
|
||||
<button class="layui-btn layui-btn-sm layui-btn-normal" id="refreshBtn" style="float: right;">
|
||||
<i class="layui-icon layui-icon-refresh"></i> 刷新
|
||||
</button>
|
||||
</div>
|
||||
<div id="addonList" style="height: 350px; overflow-y: auto;">
|
||||
<div style="text-align: center; color: #999; padding-top: 100px;">
|
||||
加载中...
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 热重载统计 -->
|
||||
<div class="layui-row layui-col-space15" style="margin-top: 15px;">
|
||||
<div class="layui-col-md12">
|
||||
<div class="stat-card">
|
||||
<div style="margin-bottom: 15px;">
|
||||
<span style="font-weight: bold;">热重载统计</span>
|
||||
<button class="layui-btn layui-btn-sm layui-btn-danger" id="clearPerfBtn" style="float: right;">
|
||||
<i class="layui-icon layui-icon-delete"></i> 清除性能数据
|
||||
</button>
|
||||
</div>
|
||||
<div id="reloadStats">
|
||||
<div style="text-align: center; color: #999; padding: 20px;">
|
||||
加载中...
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="/assets/layui/layui.js"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/echarts@5.4.3/dist/echarts.min.js"></script>
|
||||
<script>
|
||||
layui.use(['jquery', 'layer', 'http'], function() {
|
||||
var $ = layui.jquery;
|
||||
var layer = layui.layer;
|
||||
var http = layui.http;
|
||||
var performanceChart = null;
|
||||
var currentChartType = 'execution_time';
|
||||
|
||||
// 初始化图表
|
||||
function initChart() {
|
||||
performanceChart = echarts.init(document.getElementById('performanceChart'));
|
||||
loadChartData(currentChartType);
|
||||
}
|
||||
|
||||
// 加载图表数据
|
||||
function loadChartData(type) {
|
||||
currentChartType = type;
|
||||
http.get('/backend/addon_monitor/chart', { type: type }).then(function(res) {
|
||||
if (res.code === 0) {
|
||||
updateChart(res.data.chart_data, type);
|
||||
} else {
|
||||
layer.msg('加载图表数据失败', {icon: 2});
|
||||
}
|
||||
}).catch(function() { layer.msg('请求失败', {icon: 2}); });
|
||||
}
|
||||
|
||||
// 更新图表
|
||||
function updateChart(data, type) {
|
||||
var names = data.map(item => item.name);
|
||||
var values = data.map(item => item.value);
|
||||
var title = '';
|
||||
var unit = '';
|
||||
|
||||
switch(type) {
|
||||
case 'execution_time':
|
||||
title = '执行时间 (ms)';
|
||||
unit = 'ms';
|
||||
break;
|
||||
case 'memory_usage':
|
||||
title = '内存使用 (KB)';
|
||||
unit = 'KB';
|
||||
break;
|
||||
case 'success_rate':
|
||||
title = '成功率 (%)';
|
||||
unit = '%';
|
||||
break;
|
||||
case 'call_count':
|
||||
title = '调用次数';
|
||||
unit = '次';
|
||||
break;
|
||||
}
|
||||
|
||||
var option = {
|
||||
title: {
|
||||
text: title,
|
||||
left: 'center'
|
||||
},
|
||||
tooltip: {
|
||||
trigger: 'axis',
|
||||
formatter: function(params) {
|
||||
return params[0].name + ': ' + params[0].value + ' ' + unit;
|
||||
}
|
||||
},
|
||||
xAxis: {
|
||||
type: 'category',
|
||||
data: names,
|
||||
axisLabel: {
|
||||
rotate: 45,
|
||||
interval: 0
|
||||
}
|
||||
},
|
||||
yAxis: {
|
||||
type: 'value',
|
||||
name: unit
|
||||
},
|
||||
series: [{
|
||||
data: values,
|
||||
type: 'bar',
|
||||
itemStyle: {
|
||||
color: '#1e9fff'
|
||||
}
|
||||
}]
|
||||
};
|
||||
|
||||
performanceChart.setOption(option);
|
||||
}
|
||||
|
||||
// 加载概览数据
|
||||
function loadOverview() {
|
||||
http.get('/backend/addon_monitor', { action: 'overview' }).then(function(res) {
|
||||
if (res.code === 0) {
|
||||
var data = res.data;
|
||||
$('#totaladdon').text(data.total_addon);
|
||||
$('#healthyaddon').text(data.healthy_addon);
|
||||
$('#problematicaddon').text(data.problematic_addon);
|
||||
$('#avgExecutionTime').text(data.avg_execution_time);
|
||||
|
||||
// 更新重载统计
|
||||
updateReloadStats(data.reload_stats);
|
||||
} else {
|
||||
layer.msg('加载概览数据失败', {icon: 2});
|
||||
}
|
||||
}).catch(function() { layer.msg('请求失败', {icon: 2}); });
|
||||
}
|
||||
|
||||
// 更新重载统计
|
||||
function updateReloadStats(stats) {
|
||||
if (!stats || stats.total_reloads === 0) {
|
||||
$('#reloadStats').html('<div style="text-align: center; color: #999; padding: 20px;">暂无重载数据</div>');
|
||||
return;
|
||||
}
|
||||
|
||||
var html = '<div class="layui-row">';
|
||||
html += '<div class="layui-col-md3"><div class="label">总重载次数</div><div class="value">' + stats.total_reloads + '</div></div>';
|
||||
html += '<div class="layui-col-md3"><div class="label">成功次数</div><div class="value success">' + stats.successful_reloads + '</div></div>';
|
||||
html += '<div class="layui-col-md3"><div class="label">失败次数</div><div class="value danger">' + stats.failed_reloads + '</div></div>';
|
||||
html += '<div class="layui-col-md3"><div class="label">成功率</div><div class="value">' + (stats.total_reloads > 0 ? ((stats.successful_reloads / stats.total_reloads) * 100).toFixed(2) : 0) + '%</div></div>';
|
||||
html += '</div>';
|
||||
|
||||
if (stats.addon && Object.keys(stats.addon).length > 0) {
|
||||
html += '<div style="margin-top: 20px;"><strong>各插件重载状态:</strong></div>';
|
||||
html += '<div style="margin-top: 10px;">';
|
||||
|
||||
for (var addon in stats.addon) {
|
||||
var status = stats.addon[addon];
|
||||
var statusClass = status.status === 'success' ? 'success' : 'danger';
|
||||
var statusText = status.status === 'success' ? '成功' : '失败';
|
||||
var lastReload = status.reload_time ? new Date(status.reload_time * 1000).toLocaleString() : '未重载';
|
||||
|
||||
html += '<div style="padding: 8px 0; border-bottom: 1px solid #f0f0f0;">';
|
||||
html += '<span class="layui-badge layui-bg-' + statusClass + '">' + addon + '</span>';
|
||||
html += ' <span style="color: #666;">' + lastReload + '</span>';
|
||||
html += ' <span style="color: ' + (status.status === 'success' ? '#5fb878' : '#ff5722') + ';">' + statusText + '</span>';
|
||||
html += '</div>';
|
||||
}
|
||||
|
||||
html += '</div>';
|
||||
}
|
||||
|
||||
$('#reloadStats').html(html);
|
||||
}
|
||||
|
||||
// 加载插件列表
|
||||
function loadAddonList() {
|
||||
http.get('/backend/addon_monitor', { action: 'performance' }).then(function(res) {
|
||||
if (res.code === 0) {
|
||||
renderAddonList(res.data);
|
||||
} else {
|
||||
layer.msg('加载插件列表失败', {icon: 2});
|
||||
}
|
||||
}).catch(function() { layer.msg('请求失败', {icon: 2}); });
|
||||
}
|
||||
|
||||
// 渲染插件列表
|
||||
function renderAddonList(addon) {
|
||||
if (!addon || Object.keys(addon).length === 0) {
|
||||
$('#addonList').html('<div style="text-align: center; color: #999; padding-top: 100px;">暂无插件数据</div>');
|
||||
return;
|
||||
}
|
||||
|
||||
var html = '';
|
||||
for (var addon in addon) {
|
||||
var stats = addon[addon];
|
||||
var statusClass = 'healthy';
|
||||
var statusText = '健康';
|
||||
|
||||
if (stats.success_rate < 90 || stats.avg_execution_time > 1.0) {
|
||||
statusClass = 'danger';
|
||||
statusText = '异常';
|
||||
} else if (stats.avg_execution_time > 0.5 || stats.success_rate < 95) {
|
||||
statusClass = 'warning';
|
||||
statusText = '警告';
|
||||
}
|
||||
|
||||
var execTimePercent = Math.min((stats.avg_execution_time / 2.0) * 100, 100);
|
||||
var execTimeClass = stats.avg_execution_time > 1.0 ? 'danger' : (stats.avg_execution_time > 0.5 ? 'warning' : '');
|
||||
|
||||
html += '<div class="addon-list-item">';
|
||||
html += '<div style="display: flex; justify-content: space-between; align-items: center;">';
|
||||
html += '<div><span class="addon-status ' + statusClass + '"></span><strong>' + addon + '</strong></div>';
|
||||
html += '<span style="font-size: 12px; color: #666;">' + statusText + '</span>';
|
||||
html += '</div>';
|
||||
html += '<div style="margin-top: 10px; font-size: 12px; color: #666;">';
|
||||
html += '<div>调用次数: ' + stats.total_calls + ' | 成功率: ' + stats.success_rate.toFixed(2) + '%</div>';
|
||||
html += '<div style="margin-top: 5px;">平均执行时间: ' + (stats.avg_execution_time * 1000).toFixed(2) + ' ms</div>';
|
||||
html += '<div class="performance-bar">';
|
||||
html += '<div class="performance-bar-fill ' + execTimeClass + '" style="width: ' + execTimePercent + '%;"></div>';
|
||||
html += '</div>';
|
||||
html += '</div>';
|
||||
html += '</div>';
|
||||
}
|
||||
|
||||
$('#addonList').html(html);
|
||||
}
|
||||
|
||||
// 绑定事件
|
||||
$('button[data-type]').on('click', function() {
|
||||
var type = $(this).data('type');
|
||||
$('button[data-type]').removeClass('layui-btn-normal').addClass('layui-btn-primary');
|
||||
$(this).removeClass('layui-btn-primary').addClass('layui-btn-normal');
|
||||
loadChartData(type);
|
||||
});
|
||||
|
||||
$('#refreshBtn').on('click', function() {
|
||||
loadOverview();
|
||||
loadAddonList();
|
||||
loadChartData(currentChartType);
|
||||
layer.msg('数据已刷新', {icon: 1});
|
||||
});
|
||||
|
||||
$('#clearPerfBtn').on('click', function() {
|
||||
layer.confirm('确定要清除所有性能数据吗?', function(index) {
|
||||
http.get('/backend/addon_monitor', { action: 'clear_performance' }).then(function(res) {
|
||||
layer.close(index);
|
||||
if (res.code === 0) {
|
||||
layer.msg('性能数据已清除', {icon: 1});
|
||||
loadOverview();
|
||||
loadAddonList();
|
||||
} else {
|
||||
layer.msg('清除失败', {icon: 2});
|
||||
}
|
||||
}).catch(function() { layer.msg('请求失败', {icon: 2}); });
|
||||
});
|
||||
});
|
||||
|
||||
// 初始化
|
||||
initChart();
|
||||
loadOverview();
|
||||
loadAddonList();
|
||||
|
||||
// 定时刷新
|
||||
setInterval(function() {
|
||||
loadOverview();
|
||||
}, 30000); // 30秒刷新一次
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,130 @@
|
||||
<div class="layui-fluid">
|
||||
<div class="layui-card">
|
||||
<div class="layui-card-header"> </div>
|
||||
<div class="layui-card-body">
|
||||
<table class="layui-hide" id="dataTable" lay-filter="dataTable"></table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<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"> <i class="layui-icon layui-icon-add-1"></i> </a>
|
||||
<a class="layui-btn layui-btn-sm layui-btn-primary" title="删除资源" lay-event="dataDelete"><i class="layui-icon layui-icon-delete"></i> </a>
|
||||
<a class="layui-btn layui-btn-sm layui-btn-primary" title="资源回收站" lay-event="dataRecybin"><i class="layui-icon layui-icon-home"></i> </a>
|
||||
</div>
|
||||
</script>
|
||||
<script type="text/html" id="dataBar">
|
||||
<div class="layui-btn-group">
|
||||
<a class="layui-btn layui-btn-sm layui-btn-primary" title="编辑节点" lay-event="update" data-perm="admin:edit"><i class="layui-icon layui-icon-edit"></i> </a>
|
||||
{{# if(!d.is_super) { }}
|
||||
<a class="layui-btn layui-btn-sm layui-btn-primary" title="删除节点" lay-event="delete" data-perm="admin:delete"><i class="layui-icon layui-icon-delete"></i> </a>
|
||||
{{# } }}
|
||||
</div>
|
||||
</script>
|
||||
<!-- 表格回收站 -->
|
||||
<script type="text/html" id="dataRecybinTpl">
|
||||
<div class="layui-card">
|
||||
<div class="layui-card-header"> </div>
|
||||
<div class="layui-card-body">
|
||||
<table class="layui-hide" id="dataRecybinTable" lay-filter="dataRecybinTable"></table>
|
||||
</div>
|
||||
</div>
|
||||
</script>
|
||||
<!-- 回收站数据工具条 -->
|
||||
<script type="text/html" id="dataRecybinBarTpl">
|
||||
<div class="layui-btn-group">
|
||||
<a class="layui-btn layui-btn-sm layui-btn-primary" title="恢复数据" lay-event="update"><i class="layui-icon layui-icon-edit"></i> </a>
|
||||
<a class="layui-btn layui-btn-sm layui-btn-primary" title="删除节点" lay-event="delete"><i class="layui-icon layui-icon-delete"></i> </a>
|
||||
</div>
|
||||
</script>
|
||||
<!-- 状态开关 -->
|
||||
<script type="text/html" id="statusTpl">
|
||||
<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="roleTpl">
|
||||
{{# if(d.is_super == 1) { }}
|
||||
<span class="role-super">超级管理员</span>
|
||||
{{# } else if(d.role_id == 1) { }}
|
||||
<span class="role-super">超级管理员</span>
|
||||
{{# } else if(d.role_id == 2) { }}
|
||||
<span>普通管理员</span>
|
||||
{{# } else { }}
|
||||
<span>未知角色</span>
|
||||
{{# } }}
|
||||
</script>
|
||||
<!-- 登录信息 -->
|
||||
<script type="text/html" id="loginInfoTpl">
|
||||
<div>登录次数:{{d.login_count}}</div>
|
||||
<div>最后登录:{{ d.login_time }}</div>
|
||||
</script>
|
||||
|
||||
|
||||
<!-- 添加/编辑管理员表单 -->
|
||||
<script type="text/html" id="dataFormTpl">
|
||||
<form class="layui-form" style="padding: 20px 20px 0;">
|
||||
<input type="hidden" name="id" value="{{d.id||''}}">
|
||||
<div class="layui-form-item">
|
||||
<label class="layui-form-label">账户</label>
|
||||
<div class="layui-input-block">
|
||||
<input type="text" name="account" required lay-verify="required|account" placeholder="请输入用户名" autocomplete="off" class="layui-input" value="{{d.account||''}}">
|
||||
</div>
|
||||
</div>
|
||||
<div class="layui-form-item">
|
||||
<label class="layui-form-label">昵称</label>
|
||||
<div class="layui-input-block">
|
||||
<input type="text" name="nickname" required lay-verify="required" placeholder="请输入真实姓名" autocomplete="off" class="layui-input" value="{{d.nickname||''}}">
|
||||
</div>
|
||||
</div>
|
||||
<div class="layui-form-item">
|
||||
<label class="layui-form-label">角色</label>
|
||||
<div class="layui-input-block">
|
||||
<select name="role_id" lay-verify="required">
|
||||
<option value="">请选择角色</option>
|
||||
<option value="1" {{ (d.role_id==1||d.is_super==1) ? 'selected' : '' }}>超级管理员</option>
|
||||
<option value="2" {{ d.role_id==2 ? 'selected' : '' }}>普通管理员</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="layui-form-item">
|
||||
<label class="layui-form-label">邮箱</label>
|
||||
<div class="layui-input-block">
|
||||
<input type="text" name="email" lay-verify="email" placeholder="请输入邮箱" autocomplete="off" class="layui-input" value="{{d.email||''}}">
|
||||
</div>
|
||||
</div>
|
||||
<div class="layui-form-item">
|
||||
<label class="layui-form-label">手机号</label>
|
||||
<div class="layui-input-block">
|
||||
<input type="text" name="mobile" lay-verify="phone" placeholder="请输入手机号" autocomplete="off" class="layui-input" value="{{d.mobile||''}}">
|
||||
</div>
|
||||
</div>
|
||||
<div class="layui-form-item">
|
||||
<label class="layui-form-label">状态</label>
|
||||
<div class="layui-input-block">
|
||||
<input type="radio" name="status" value="1" title="启用" {{ d.status==undefined || d.status==1 ? 'checked' : '' }}>
|
||||
<input type="radio" name="status" value="0" title="禁用" {{ d.status==0 ? 'checked' : '' }}>
|
||||
</div>
|
||||
</div>
|
||||
<div class="layui-form-item" id="passwordGroup" {{ d.id ? 'style="display:none;"' : '' }}>
|
||||
<label class="layui-form-label">密码</label>
|
||||
<div class="layui-input-block">
|
||||
<input type="password" name="password" required lay-verify="required|pass" placeholder="请输入密码" autocomplete="off" class="layui-input">
|
||||
</div>
|
||||
</div>
|
||||
<div class="layui-form-item layui-hide" id="resetPwdGroup">
|
||||
<label class="layui-form-label">新密码</label>
|
||||
<div class="layui-input-block">
|
||||
<input type="password" name="new_password" required lay-verify="required|pass" placeholder="请输入新密码" autocomplete="off" class="layui-input">
|
||||
</div>
|
||||
</div>
|
||||
<div class="layui-form-item layui-form-text">
|
||||
<label class="layui-form-label">备注</label>
|
||||
<div class="layui-input-block">
|
||||
<textarea name="remark" placeholder="请输入备注信息" class="layui-textarea">{{d.remark||''}}</textarea>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</script>
|
||||
<script>
|
||||
layui.use('admin', function () { });
|
||||
</script>
|
||||
@@ -0,0 +1,126 @@
|
||||
<div class="layui-fluid">
|
||||
<div class="layui-card">
|
||||
<div class="layui-card-header">充值卡密管理</div>
|
||||
<div class="layui-card-body">
|
||||
<form class="layui-form" lay-filter="data-search-form">
|
||||
<div class="layui-form-item">
|
||||
<div class="layui-inline">
|
||||
<input type="text" name="cardno" placeholder="卡号" autocomplete="off" class="layui-input">
|
||||
</div>
|
||||
<div class="layui-inline">
|
||||
<select name="status">
|
||||
<option value="">全部状态</option>
|
||||
<option value="0">未售</option>
|
||||
<option value="1">已售</option>
|
||||
<option value="2">已兑换</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="layui-inline">
|
||||
<button class="layui-btn layui-btn-normal" lay-submit lay-filter="data-search-btn"><i class="layui-icon"></i> 搜索</button>
|
||||
<button type="button" class="layui-btn layui-btn-primary" id="btn-reset">重置</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
<table class="layui-hide" id="dataTable" lay-filter="dataTable"></table>
|
||||
<script type="text/html" id="tableBar">
|
||||
<div class="layui-btn-container">
|
||||
<button class="layui-btn layui-btn-sm" lay-event="dataCreate" data-perm="card:add"><i class="layui-icon"></i> 新增</button>
|
||||
<button class="layui-btn layui-btn-sm layui-btn-warm" lay-event="dataBatch" data-perm="card:add"><i class="layui-icon"></i> 批量生成</button>
|
||||
<button class="layui-btn layui-btn-sm layui-btn-danger" lay-event="dataDelete" data-perm="card:del"><i class="layui-icon"></i> 删除</button>
|
||||
<button class="layui-btn layui-btn-sm" lay-event="dataRecybin" data-perm="card:restore"><i class="layui-icon"></i> 回收站</button>
|
||||
</div>
|
||||
</script>
|
||||
<script type="text/html" id="dataBar">
|
||||
<a class="layui-btn layui-btn-xs" lay-event="update" data-perm="card:edit">编辑</a>
|
||||
<a class="layui-btn layui-btn-xs layui-btn-danger" lay-event="delete" data-perm="card:del">删除</a>
|
||||
</script>
|
||||
<script type="text/html" id="statusTpl">
|
||||
<input type="checkbox" name="status" value="{{ d.id }}" lay-skin="switch" lay-text="开|关" lay-filter="statusSwitch" {{ d.status == 1 ? 'checked' : '' }}>
|
||||
</script>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script type="text/html" id="dataFormTpl">
|
||||
<form class="layui-form" id="wxapp-form" lay-filter="wxapp-form" style="padding:15px;">
|
||||
<input type="hidden" name="id" value="{{ d.id || '' }}">
|
||||
<div class="layui-form-item">
|
||||
<label class="layui-form-label">卡号</label>
|
||||
<div class="layui-input-block">
|
||||
<input type="text" name="cardno" placeholder="请输入卡号" class="layui-input">
|
||||
</div>
|
||||
</div>
|
||||
<div class="layui-form-item">
|
||||
<label class="layui-form-label">密码</label>
|
||||
<div class="layui-input-block">
|
||||
<input type="text" name="password" placeholder="请输入密码" class="layui-input">
|
||||
</div>
|
||||
</div>
|
||||
<div class="layui-form-item">
|
||||
<label class="layui-form-label">面值</label>
|
||||
<div class="layui-input-block">
|
||||
<input type="text" name="amount" placeholder="请输入面值" class="layui-input">
|
||||
</div>
|
||||
</div>
|
||||
<div class="layui-form-item">
|
||||
<label class="layui-form-label">状态</label>
|
||||
<div class="layui-input-block">
|
||||
<input type="checkbox" name="status" lay-skin="switch" lay-text="启用|禁用" value="1" checked>
|
||||
</div>
|
||||
</div>
|
||||
<div class="layui-form-item">
|
||||
<label class="layui-form-label">排序</label>
|
||||
<div class="layui-input-block">
|
||||
<input type="text" name="sort" placeholder="请输入排序" class="layui-input">
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</script>
|
||||
|
||||
<script type="text/html" id="batchFormTpl">
|
||||
<form class="layui-form" id="wxapp-batch-form" lay-filter="wxapp-batch-form" style="padding:15px;">
|
||||
<div class="layui-form-item">
|
||||
<label class="layui-form-label">生成数量</label>
|
||||
<div class="layui-input-block">
|
||||
<input type="number" name="count" value="10" min="1" max="200" class="layui-input" lay-verify="required">
|
||||
</div>
|
||||
</div>
|
||||
<div class="layui-form-item">
|
||||
<label class="layui-form-label">面值(元)</label>
|
||||
<div class="layui-input-block">
|
||||
<input type="number" name="amount" value="10" step="0.01" min="0.01" class="layui-input" lay-verify="required">
|
||||
</div>
|
||||
</div>
|
||||
<div class="layui-form-item">
|
||||
<label class="layui-form-label">卡号前缀</label>
|
||||
<div class="layui-input-block">
|
||||
<input type="text" name="prefix" placeholder="可选,如 YX" class="layui-input">
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</script>
|
||||
|
||||
<script type="text/html" id="batchResultTpl">
|
||||
<div class="layui-form" style="padding:10px;">
|
||||
<blockquote class="layui-elem-quote">已生成 {{ d.list.length }} 张卡密,请妥善保管:</blockquote>
|
||||
<table class="layui-table">
|
||||
<thead><tr><th>卡号</th><th>密码</th><th>面值</th></tr></thead>
|
||||
<tbody>
|
||||
{{# layui.each(d.list, function(i, item){ }}
|
||||
<tr><td>{{ item.cardno }}</td><td>{{ item.password }}</td><td>{{ d.amount }}</td></tr>
|
||||
{{# }); }}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</script>
|
||||
|
||||
<script type="text/html" id="dataRecybinTpl">
|
||||
<table class="layui-hide" id="dataRecybinTable" lay-filter="dataRecybinTable"></table>
|
||||
<script type="text/html" id="dataRecybinBarTpl">
|
||||
<a class="layui-btn layui-btn-xs" lay-event="restore" data-perm="card:restore">恢复</a>
|
||||
<a class="layui-btn layui-btn-xs layui-btn-danger" lay-event="forcedelete" data-perm="card:destroy">彻底删除</a>
|
||||
</script>
|
||||
</script>
|
||||
<script>
|
||||
layui.use('card', layui.factory('card'));
|
||||
</script>
|
||||
@@ -0,0 +1,11 @@
|
||||
<div class="layui-card" style="margin:15px;">
|
||||
<div class="layui-card-header">聊天管理</div>
|
||||
<div class="layui-card-body">
|
||||
<p>聊天功能由 <b>wxchat</b> 插件(基于 GatewayWorker)提供,本页为后台入口占位说明。</p>
|
||||
<ul class="layui-text" style="line-height:2;">
|
||||
<li>实时聊天、好友 / 群组、消息记录由 wxchat 插件服务承载;</li>
|
||||
<li>相关配置请在「插件管理 → 配置」中维护 <b>wxchat</b> 插件;</li>
|
||||
<li>若需查看在线连接 / 消息吞吐,请前往 MQTT 代理(<b>mqttbroker</b> 插件)后台。</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user