chore: 重写初始提交(清空历史,整理后全量提交)
This commit is contained in:
@@ -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());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user