chore: 重写初始提交(清空历史,整理后全量提交)

This commit is contained in:
ywxapp
2026-08-16 16:54:14 +08:00
commit 6c1a106bc1
1808 changed files with 238144 additions and 0 deletions
+161
View File
@@ -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());
}
}
}
}
+280
View File
@@ -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
]);
}
}
+780
View File
@@ -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()):经中心站 APIRemoteService::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.phptemplate 看 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());
}
}
}
+263
View File
@@ -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());
}
}
}
}
+117
View File
@@ -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()]);
}
}
}
+205
View File
@@ -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());
}
}
}
}
+123
View File
@@ -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());
}
}
}
+123
View File
@@ -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);
}
}
+312
View File
@@ -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;
}
}
+161
View File
@@ -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());
}
}
}
}
+89
View File
@@ -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);
}
}
+213
View File
@@ -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());
}
}
}
}
+111
View File
@@ -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('退出登录成功');
}
}
+186
View File
@@ -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);
}
}
+145
View File
@@ -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' => '已更新']);
}
}
+197
View File
@@ -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());
}
}
}
}
+121
View File
@@ -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)
{
//
}
}
+286
View File
@@ -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()]);
}
}
}
+173
View File
@@ -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('个人资料不支持该操作');
}
}
+158
View File
@@ -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());
}
}
}
}
+321
View File
@@ -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;
}
}
+228
View File
@@ -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,
]);
}
}
+185
View File
@@ -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');
}
}
+158
View File
@@ -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());
}
}
}
}
+158
View File
@@ -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());
}
}
}
}
+174
View File
@@ -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());
}
}
}
+161
View File
@@ -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());
}
}
}
}
+330
View File
@@ -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;
}
}
// 清洗 settingsallow_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());
}
}
}
+95
View File
@@ -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());
}
}
}
+244
View File
@@ -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());
}
}
}
}