781 lines
31 KiB
PHP
781 lines
31 KiB
PHP
<?php
|
||
/*
|
||
* @Author: YwxApp <ywx@ywxapp.cn>
|
||
* @Date: 2026-04-29 02:32:33
|
||
* @LastEditors: YwxApp <ywx@ywxapp.cn>
|
||
* @LastEditTime: 2026-08-16 15:34:28
|
||
* @Description:
|
||
* @FilePath: \ywxapp_dev\app\backend\controller\Addons.php
|
||
* @CustomString: Copyright (c) 2026 YwxApp
|
||
*/
|
||
|
||
declare(strict_types=1);
|
||
|
||
namespace app\backend\controller;
|
||
|
||
use think\exception\HttpException;
|
||
use think\facade\Config;
|
||
use think\facade\Request;
|
||
use think\facade\View;
|
||
use ywxapp\exception\AddonException;
|
||
use ywxapp\service\AddonService;
|
||
use ywxapp\service\AddonDevService;
|
||
use ywxapp\service\RemoteService;
|
||
use Exception;
|
||
use ywxapp\controller\BackendBase;
|
||
/**
|
||
* addon 类
|
||
*
|
||
* @author ywxapp <admin@ywxapp.cn>
|
||
*/
|
||
class Addons extends BackendBase
|
||
{
|
||
|
||
/**
|
||
* 控制器初始化 initialize
|
||
* @return void
|
||
*/
|
||
public function initialize() {}
|
||
|
||
/**
|
||
* 插件列表
|
||
*/
|
||
public function index()
|
||
{
|
||
if ($this->request->isAjax()) {
|
||
$results = scandir(ADDON_PATH);
|
||
$list = [];
|
||
foreach ($results as $name) {
|
||
if ($name === '.' or $name === '..' or is_file(ADDON_PATH . $name)) {
|
||
continue;
|
||
}
|
||
$addonDir = ADDON_PATH . $name . DIRECTORY_SEPARATOR;
|
||
if (! is_dir($addonDir)) {
|
||
continue;
|
||
}
|
||
$infoFile = $addonDir . 'info.php';
|
||
if (! is_file($infoFile)) {
|
||
continue;
|
||
}
|
||
$info = include $infoFile;
|
||
if (! isset($info['name'])) {
|
||
continue;
|
||
}
|
||
// 统一字段,适配前端 layui 表格(列: title/author/version/description/status/id)
|
||
$info['id'] = $info['name'];
|
||
$info['description'] = $info['intro'] ?? '';
|
||
$info['status'] = $info['state'] ?? 0;
|
||
$info['hasConfig'] = is_file($addonDir . 'config.php');
|
||
$list[] = $info;
|
||
}
|
||
|
||
// 关键字搜索
|
||
$keyword = $this->request->param('keyword', '');
|
||
if ($keyword !== '') {
|
||
$list = array_values(array_filter($list, function ($it) use ($keyword) {
|
||
return stripos((string) ($it['title'] ?? ''), $keyword) !== false
|
||
|| stripos((string) ($it['name'] ?? ''), $keyword) !== false;
|
||
}));
|
||
}
|
||
// 运行状态筛选
|
||
$status = $this->request->param('status', '');
|
||
if ($status !== '') {
|
||
$list = array_values(array_filter($list, function ($it) use ($status) {
|
||
return (string) ($it['status'] ?? '') === (string) $status;
|
||
}));
|
||
}
|
||
|
||
// 分页
|
||
$total = count($list);
|
||
$page = (int) $this->request->param('page', 1);
|
||
$limit = (int) $this->request->param('limit', 10);
|
||
$pageList = array_slice($list, max(0, ($page - 1) * $limit), $limit);
|
||
|
||
$this->result->setCount($total)->success($pageList, '获取成功');
|
||
}
|
||
return View::fetch('addon/index');
|
||
}
|
||
|
||
/**
|
||
* 应用市场(Discuz 式远程插件市场,核心后台「插件管理」)
|
||
*
|
||
* 服务中心(本机,ywxapp.api_url 为空或指向自己):直接读取本地市场目录(wxapp_appmarket_addon_list),
|
||
* 无需依赖外部地址,应用商店即刻可见本机托管的插件。
|
||
* 客户机(ywxapp.api_url 指向其他服务器,见 is_market_client()):经中心站 API(RemoteService::lists)拉取远程市场列表,
|
||
* 可一键安装到本客户机(downloadInstall)。客户机不安装 market 插件,应用商店功能统一落在本核心控制器。
|
||
*/
|
||
public function market()
|
||
{
|
||
$apiUrl = Config::get('ywxapp.api_url', '');
|
||
$isClient = (bool) is_market_client();
|
||
$keyword = trim((string) $this->request->param('keyword', ''));
|
||
$category = trim((string) $this->request->param('category', ''));
|
||
$type = trim((string) $this->request->param('type', ''));
|
||
$order = trim((string) $this->request->param('order', 'new'));
|
||
$list = [];
|
||
$error = '';
|
||
$categories = [];
|
||
|
||
if ($isClient) {
|
||
// 客户机:经服务中心 API 拉取市场列表
|
||
if ($apiUrl) {
|
||
try {
|
||
$params = [];
|
||
if ($keyword !== '') {
|
||
$params['keyword'] = $keyword;
|
||
}
|
||
if ($category !== '') {
|
||
$params['category'] = $category;
|
||
}
|
||
if ($order !== '' && $order !== 'new') {
|
||
$params['order'] = $order;
|
||
}
|
||
if ($type !== '') {
|
||
$params['type'] = $type;
|
||
}
|
||
$resp = (new RemoteService())->lists($params);
|
||
if (!empty($resp['success']) && !empty($resp['data']['list'])) {
|
||
$list = $resp['data']['list'];
|
||
} else {
|
||
$error = $resp['message'] ?? '获取市场列表失败';
|
||
}
|
||
} catch (\Throwable $e) {
|
||
$error = '连接市场失败:' . $e->getMessage();
|
||
}
|
||
} else {
|
||
$error = '未配置市场地址(请在后台配置 ywxapp.api_url)';
|
||
}
|
||
} elseif (!class_exists(\addon\appmall\service\MarketService::class)) {
|
||
// 本机即中心站但未安装 appmall 插件:市场目录不可用
|
||
$error = '本机未安装 appmall 插件:请安装插件以启用市场目录,或配置 ywxapp.api_url 指向中心站';
|
||
} else {
|
||
// 服务中心(本机):委托 appmall 插件读取本地市场目录(中心域逻辑全部在插件侧)
|
||
try {
|
||
$r = \addon\appmall\service\MarketService::instance()->catalog($keyword, $category, $order, $type);
|
||
$list = $r['list'];
|
||
$categories = $r['categories'];
|
||
if (empty($list)) {
|
||
$error = '本机市场目录为空:可导入 docs/*-appmarket_addon_list.sql 测试记录,或在「开发者中心」上传插件';
|
||
}
|
||
} catch (\Throwable $e) {
|
||
$error = '读取本机市场目录失败:' . $e->getMessage();
|
||
}
|
||
}
|
||
|
||
// 标注本地安装状态与可升级性(addon 看 addon/<name>/info.php;template 看 templates/<name>/template.json)
|
||
foreach ($list as &$it) {
|
||
$name = $it['name'] ?? '';
|
||
$installed = false;
|
||
$localVersion = '';
|
||
if (($it['type'] ?? 'addon') === 'template') {
|
||
$tplJson = root_path() . 'templates' . DIRECTORY_SEPARATOR . $name . DIRECTORY_SEPARATOR . 'template.json';
|
||
if (is_file($tplJson)) {
|
||
$tj = (array) json_decode((string) @file_get_contents($tplJson), true);
|
||
$localVersion = (string) ($tj['version'] ?? '');
|
||
$installed = true;
|
||
}
|
||
} else {
|
||
$localInfoFile = ADDON_PATH . $name . DIRECTORY_SEPARATOR . 'info.php';
|
||
if (is_file($localInfoFile)) {
|
||
$li = include $localInfoFile;
|
||
$localVersion = $li['version'] ?? '';
|
||
$installed = true;
|
||
}
|
||
}
|
||
$it['installed'] = $installed;
|
||
$it['local_version'] = $localVersion;
|
||
$it['upgradable'] = $installed && $localVersion !== ''
|
||
&& isset($it['version'])
|
||
&& version_compare($it['version'], $localVersion, '>');
|
||
// 预解析 tags 为数组,供前端标签展示(避免模板内嵌 PHP)
|
||
$rawTags = trim((string) ($it['tags'] ?? ''));
|
||
$it['tag_list'] = $rawTags === '' ? [] : array_values(array_filter(
|
||
array_map('trim', explode(',', $rawTags)),
|
||
function ($t) { return $t !== ''; }
|
||
));
|
||
// 元数据键兜底:旧库无 category/rating/screenshots 列或远程列表未返回时,
|
||
// 避免模板({$it.category} / {$it.rating})触发 Undefined array key 报错。
|
||
foreach (['category', 'screenshots', 'rating'] as $mk) {
|
||
if (!isset($it[$mk])) {
|
||
$it[$mk] = '';
|
||
}
|
||
}
|
||
if (!isset($it['type']) || $it['type'] === '') {
|
||
$it['type'] = 'addon';
|
||
}
|
||
}
|
||
unset($it);
|
||
|
||
// 客户机模式:分类下拉数据源从返回列表中动态归纳
|
||
if ($isClient && empty($categories) && !empty($list)) {
|
||
$seen = [];
|
||
foreach ($list as $it) {
|
||
$c = (string) ($it['category'] ?? '');
|
||
if ($c !== '' && !in_array($c, $seen, true)) {
|
||
$seen[] = $c;
|
||
}
|
||
}
|
||
$categories = $seen;
|
||
}
|
||
|
||
View::assign([
|
||
'list' => $list,
|
||
'error' => $error,
|
||
'is_client' => $isClient,
|
||
'api_url' => $apiUrl,
|
||
'keyword' => $keyword,
|
||
'category' => $category,
|
||
'type' => $type,
|
||
'order' => $order,
|
||
'categories' => $categories,
|
||
]);
|
||
return View::fetch('addon/market');
|
||
}
|
||
|
||
/**
|
||
* 我的插件(核心后台「插件管理」)
|
||
* 列出本机已安装插件,便于从应用市场跳转后集中管理(配置/升级/启停/卸载见「插件管理」列表)。
|
||
*/
|
||
public function my()
|
||
{
|
||
$list = [];
|
||
if (is_dir(ADDON_PATH)) {
|
||
foreach (scandir(ADDON_PATH) as $name) {
|
||
if ($name === '.' || $name === '..' || !is_dir(ADDON_PATH . $name)) {
|
||
continue;
|
||
}
|
||
$infoFile = ADDON_PATH . $name . DIRECTORY_SEPARATOR . 'info.php';
|
||
if (!is_file($infoFile)) {
|
||
continue;
|
||
}
|
||
$info = include $infoFile;
|
||
if (!isset($info['name'])) {
|
||
continue;
|
||
}
|
||
$info['id'] = $info['name'];
|
||
$info['description'] = $info['intro'] ?? '';
|
||
$info['status'] = $info['state'] ?? 0;
|
||
$info['hasConfig'] = is_file(ADDON_PATH . $name . DIRECTORY_SEPARATOR . 'config.php');
|
||
$list[] = $info;
|
||
}
|
||
}
|
||
View::assign('list', $list);
|
||
View::assign('is_client', (bool) is_market_client());
|
||
return View::fetch('addon/my');
|
||
}
|
||
|
||
/**
|
||
* 运营退款(后台视角):按插件名退本地最新「已支付」订单,
|
||
* 吊销授权 + 订单置已退款 + 收益冲正(与会员端 /api/v1/addon/refund 逻辑一致)。
|
||
* 仅中心站模式(ywxapp.api_url 为空或指向自己)支持;客户机模式订单在中心站、属会员 uid,
|
||
* 后台无会员信息,需到会员中心申请退款。
|
||
*/
|
||
public function refund(Request $request)
|
||
{
|
||
if (is_market_client()) {
|
||
return $this->result->error('远程模式订单存于中心站,请于会员中心申请退款');
|
||
}
|
||
if (!class_exists(\addon\appmall\service\MarketService::class)) {
|
||
return $this->result->error('本机未安装 appmall 插件,无法执行退款');
|
||
}
|
||
try {
|
||
\addon\appmall\service\MarketService::instance()
|
||
->operatorRefund((string) $request->post('name', ''));
|
||
} catch (\Throwable $e) {
|
||
return $this->result->error($e->getMessage());
|
||
}
|
||
return $this->result->success([], '退款成功,授权已吊销');
|
||
}
|
||
|
||
|
||
|
||
/**
|
||
* 发布到官方公共市场(www.ywxapp.cn)
|
||
* 打包本机已安装插件并上传到官方市场提交接口,进入「待审核」状态,
|
||
* 运营审核通过后才在公开市场可见。需先在 .env 配置 APPMARKET_DEV_TOKEN。
|
||
*/
|
||
public function submitOfficial()
|
||
{
|
||
try {
|
||
$name = input('name', '');
|
||
if (!$name || !preg_match('/^[a-zA-Z0-9_]+$/', $name)) {
|
||
return $this->result->error('插件名称格式不正确');
|
||
}
|
||
$token = config('ywxapp.developer_token', '');
|
||
if (!$token) {
|
||
return $this->result->error('未配置开发者令牌:请在 .env 设置 DEVELOPER_TOKEN');
|
||
}
|
||
$AddonService = AddonService::instance($name);
|
||
if (!$AddonService->isInstalled()) {
|
||
return $this->result->error('插件未安装,无法发布');
|
||
}
|
||
// 打包(返回本地 zip 路径)
|
||
$zipFile = $AddonService->package();
|
||
$infoFile = ADDON_PATH . $name . DIRECTORY_SEPARATOR . 'info.php';
|
||
$info = is_file($infoFile) ? (array) include $infoFile : [];
|
||
$meta = [
|
||
'name' => $info['name'] ?? $name,
|
||
'title' => $info['title'] ?? $name,
|
||
'author' => $info['author'] ?? '',
|
||
'version' => $info['version'] ?? '',
|
||
'price' => $info['price'] ?? 0,
|
||
'description' => $info['intro'] ?? ($info['description'] ?? ''),
|
||
];
|
||
if (empty($meta['version'])) {
|
||
return $this->result->error('插件版本号缺失,无法提交');
|
||
}
|
||
|
||
$client = new \GuzzleHttp\Client([
|
||
'base_uri' => config('ywxapp.api_url'),
|
||
'timeout' => 60,
|
||
'verify' => (bool) config('appmall.ssl_verify', true),
|
||
]);
|
||
$response = $client->post('/appmall/api/addon/submit', [
|
||
'multipart' => [
|
||
['name' => 'token', 'contents' => $token],
|
||
['name' => 'name', 'contents' => $meta['name']],
|
||
['name' => 'title', 'contents' => $meta['title']],
|
||
['name' => 'author', 'contents' => $meta['author']],
|
||
['name' => 'version', 'contents' => $meta['version']],
|
||
['name' => 'price', 'contents' => (string) $meta['price']],
|
||
['name' => 'description', 'contents' => $meta['description']],
|
||
['name' => 'file', 'contents' => fopen($zipFile, 'r'), 'filename' => basename($zipFile)],
|
||
],
|
||
]);
|
||
$json = json_decode($response->getBody()->getContents(), true);
|
||
if (empty($json) || (int) ($json['code'] ?? 0) !== 1) {
|
||
return $this->result->error('官方市场返回:' . ($json['msg'] ?? '未知错误'));
|
||
}
|
||
return $this->result->success($json['data'] ?? [], '已提交,等待官方审核');
|
||
} catch (\GuzzleHttp\Exception\RequestException $e) {
|
||
$msg = $e->getMessage();
|
||
if (stripos($msg, 'SSL certificate') !== false || stripos($msg, 'cURL error 60') !== false) {
|
||
$msg = 'SSL 证书验证失败(cURL error 60):请配置 php.ini 的 curl.cainfo,或在 .env 临时设置 APPMARKET_SSL_VERIFY=false。';
|
||
}
|
||
return $this->result->error('提交到官方市场失败:' . $msg);
|
||
} catch (AddonException $e) {
|
||
return $this->result->error($e->getMessage());
|
||
} catch (\Exception $e) {
|
||
return $this->result->error($e->getMessage());
|
||
}
|
||
}
|
||
|
||
|
||
public function pack()
|
||
{
|
||
$addonName = input('name');
|
||
if (!$addonName) {
|
||
return $this->result->error('请指定插件名称');
|
||
}
|
||
$AddonService = AddonService::instance($addonName);
|
||
if (!$AddonService->isInstalled()) {
|
||
return $this->result->error('插件不存在');
|
||
}
|
||
try {
|
||
$zipFile = $AddonService->package();
|
||
return download($zipFile, $addonName . '-' . $AddonService->getVersion() . '.zip');
|
||
} catch (\Exception $e) {
|
||
return $this->result->error($e->getMessage());
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 上传安装插件
|
||
*/
|
||
public function upload()
|
||
{
|
||
if (Request::isAjax()) {
|
||
Config::set(['default_return_type' => 'json'], 'app');
|
||
$info = [];
|
||
$file = $this->request->file('file');
|
||
try {
|
||
$uid = $this->request->post("uid");
|
||
$token = $this->request->post("token");
|
||
$faversion = $this->request->post("faversion");
|
||
// 鉴权由 Backend 中间件统一处理;uid/token 仅作为离线安装校验参数透传
|
||
$extend = [
|
||
'uid' => $uid,
|
||
'token' => $token,
|
||
'faversion' => $faversion,
|
||
];
|
||
$info = AddonService::instance()->local($file, $extend);
|
||
} catch (AddonException $e) {
|
||
$this->result->error(LANG($e->getMessage(), $e->getCode()));
|
||
} catch (\Exception $e) {
|
||
$this->result->error(lang($e->getMessage()));
|
||
}
|
||
$this->result->success(['addon' => $info], lang('Offline installed tips'),);
|
||
}
|
||
return View::fetch('addon/index');
|
||
}
|
||
|
||
|
||
|
||
|
||
/**
|
||
* 安装插件
|
||
*/
|
||
public function install()
|
||
{
|
||
try {
|
||
$file = Request::file('addon_file');
|
||
if (!$file) {
|
||
return $this->result->error('请上传插件文件');
|
||
}
|
||
$AddonService = AddonService::instance();
|
||
$extend = [
|
||
'install_user' => session('user_id'),
|
||
'install_ip' => Request::ip(),
|
||
'install_time' => time()
|
||
];
|
||
$info = $AddonService->local($file, $extend);
|
||
return $this->result->success($info, '插件安装成功');
|
||
} catch (AddonException $e) {
|
||
return $this->result->error($e->getMessage());
|
||
} catch (\Exception $e) {
|
||
return $this->result->error($e->getMessage());
|
||
}
|
||
}
|
||
/**
|
||
* 卸载插件
|
||
*/
|
||
public function uninstall()
|
||
{
|
||
try {
|
||
$addonName = input('name'); // 从请求参数获取插件名
|
||
// 验证插件名称
|
||
if (!$addonName || !preg_match('/^[a-zA-Z0-9_]+$/', $addonName)) {
|
||
return json(['code' => 0, 'msg' => '插件名称格式不正确']);
|
||
}
|
||
$AddonService = AddonService::instance($addonName);
|
||
$result = $AddonService->uninstall();
|
||
if ($result) {
|
||
$this->result->success([], '插件卸载成功');
|
||
} else {
|
||
$this->result->error('插件卸载失败');
|
||
}
|
||
} catch (AddonException $e) {
|
||
$this->result->error('插件卸载失败:' . $e->getMessage());
|
||
} catch (\Exception $e) {
|
||
$this->result->error('插件卸载失败:' . $e->getMessage());
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 启用/禁用插件
|
||
*/
|
||
public function toggle()
|
||
{
|
||
try {
|
||
$addonName = input('name');
|
||
$action = input('action'); // enable or disable
|
||
$AddonService = AddonService::instance($addonName);
|
||
if (!$AddonService->isInstalled()) {
|
||
$this->result->error('插件不存在');
|
||
}
|
||
if ($action === 'enable') {
|
||
$AddonService->enable();
|
||
$this->result->success([], '插件已启用');
|
||
} else {
|
||
$AddonService->disable();
|
||
$this->result->success([], '插件已禁用');
|
||
}
|
||
} catch (\Exception $e) {
|
||
$this->result->error($e->getMessage());
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 打包插件
|
||
*/
|
||
public function package()
|
||
{
|
||
try {
|
||
$addonName = input('name');
|
||
$AddonService = AddonService::instance($addonName);
|
||
if (!$AddonService->isInstalled()) {
|
||
return $this->result->error('插件不存在');
|
||
}
|
||
$zipFile = $AddonService->package();
|
||
return download($zipFile, $addonName . '-' . $AddonService->getVersion() . '.zip');
|
||
} catch (Exception $e) {
|
||
return $this->result->error($e->getMessage());
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 下载并安装
|
||
*/
|
||
public function downloadInstall()
|
||
{
|
||
try {
|
||
$addonName = input('name');
|
||
$version = input('version', '');
|
||
// 商品类型:addon=插件(默认)/ template=模板(下载后走 TemplateInstaller 落地)
|
||
$type = strtolower((string) input('type', 'addon'));
|
||
$AddonService = AddonService::instance($addonName);
|
||
$extend = [];
|
||
if ($version) {
|
||
$extend['version'] = $version;
|
||
}
|
||
// 运营安装:透传运营者身份用于审计(不消耗会员下载额度;付费插件按运营特权放行)。
|
||
// 会员端的真实下载限额由 RemoteService::downloadBinary 携带 uid 触发,本路径不重复计限。
|
||
$extend['operator_id'] = session('user_id') ?? 0;
|
||
// 下载
|
||
$zipFile = $AddonService->download($extend);
|
||
// 模板商品:不走插件安装流程,交给 TemplateInstaller 还原 templates/<name>/ 与静态资源
|
||
if ($type === 'template') {
|
||
$tplInfo = \ywxapp\library\TemplateInstaller::install($zipFile, root_path());
|
||
@unlink($zipFile);
|
||
return $this->result->success($tplInfo, '模板下载并安装成功,请到「模板中心」启用');
|
||
}
|
||
// 安装(download 返回本地路径,包装为 File 后走离线安装流程)
|
||
$file = new \think\File($zipFile);
|
||
$info = $AddonService->local($file, $extend);
|
||
return $this->result->success($info, '下载并安装成功');
|
||
} catch (AddonException $e) {
|
||
return $this->result->error($e->getMessage());
|
||
} catch (Exception $e) {
|
||
return $this->result->error($e->getMessage());
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 在线升级插件(从市场下载新版本覆盖安装)
|
||
* 访问:POST /backend/addon/upgrade {name, version?}
|
||
*/
|
||
public function upgrade()
|
||
{
|
||
try {
|
||
$addonName = input('name', '');
|
||
$version = input('version', '');
|
||
if (!$addonName || !preg_match('/^[a-zA-Z0-9_]+$/', $addonName)) {
|
||
return $this->result->error('插件名称格式不正确');
|
||
}
|
||
$AddonService = AddonService::instance($addonName);
|
||
if (!$AddonService->isInstalled()) {
|
||
return $this->result->error('插件未安装,无法升级');
|
||
}
|
||
$res = $AddonService->onlineUpgrade($version);
|
||
return $this->result->success(
|
||
$res,
|
||
'升级成功:' . ($res['from'] ?? '') . ' → ' . ($res['to'] ?? '')
|
||
);
|
||
} catch (AddonException $e) {
|
||
return $this->result->error($e->getMessage());
|
||
} catch (\Exception $e) {
|
||
return $this->result->error($e->getMessage());
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 插件配置(通用后台配置页)
|
||
* 依据插件 config.php 的字段定义渲染表单,保存写入统一配置表(数据库独立项 + 缓存)。
|
||
* 访问:/backend/addon/setting?addon=<插件标识>
|
||
*/
|
||
public function setting()
|
||
{
|
||
$addon = input('addon', '');
|
||
if (!$addon || !preg_match('/^[a-zA-Z0-9_]+$/', $addon) || !is_dir(ADDON_PATH . $addon)) {
|
||
return $this->result->error('插件不存在');
|
||
}
|
||
$configFile = ADDON_PATH . $addon . DIRECTORY_SEPARATOR . 'config.php';
|
||
$fields = is_file($configFile) ? (array) include $configFile : [];
|
||
if ($this->request->isPost()) {
|
||
$post = input('post.');
|
||
$data = [];
|
||
foreach ($fields as $f) {
|
||
$n = $f['name'] ?? '';
|
||
if ($n !== '' && array_key_exists($n, $post)) {
|
||
$data[$n] = $post[$n];
|
||
}
|
||
}
|
||
AddonService::config($addon, $data);
|
||
AddonService::clearConfigCache($addon);
|
||
return $this->result->success('保存成功');
|
||
}
|
||
$saved = AddonService::config($addon);
|
||
foreach ($fields as &$f) {
|
||
if (isset($saved[$f['name']])) {
|
||
$f['value'] = $saved[$f['name']];
|
||
}
|
||
}
|
||
unset($f);
|
||
View::assign(['addon' => $addon, 'fields' => $fields]);
|
||
return View::fetch('addon/setting');
|
||
}
|
||
|
||
// ==================== 开发模式:插件设计器(抄 Discuz!) ====================
|
||
|
||
/**
|
||
* 设计器页面(仅开发模式可访问)
|
||
*/
|
||
public function design()
|
||
{
|
||
if (!AddonDevService::enabled()) {
|
||
return $this->result->error('开发模式未开启,请在 .env 设置 ADDON_DEVELOPER=true');
|
||
}
|
||
$name = input('addon', '');
|
||
View::assign('addon', $name);
|
||
return View::fetch('addon/design');
|
||
}
|
||
|
||
/**
|
||
* 创建插件骨架
|
||
*/
|
||
public function designCreate()
|
||
{
|
||
if (!AddonDevService::enabled()) {
|
||
return $this->result->error('开发模式未开启');
|
||
}
|
||
try {
|
||
$name = input('name', '');
|
||
if (!$name) {
|
||
return $this->result->error('请填写插件标识');
|
||
}
|
||
$meta = [
|
||
'title' => input('title', ''),
|
||
'intro' => input('intro', ''),
|
||
'author' => input('author', ''),
|
||
'website' => input('website', ''),
|
||
'version' => input('version', '1.0.0'),
|
||
'url' => input('url', ''),
|
||
'license' => input('license', ''),
|
||
];
|
||
$info = AddonDevService::instance($name)->createSkeleton($meta);
|
||
return $this->result->success($info, '插件骨架创建成功');
|
||
} catch (\Exception $e) {
|
||
return $this->result->error($e->getMessage());
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 读取插件现有数据(供设计器回显)
|
||
*/
|
||
public function designRead()
|
||
{
|
||
if (!AddonDevService::enabled()) {
|
||
return $this->result->error('开发模式未开启');
|
||
}
|
||
$name = input('addon', '');
|
||
if (!$name) {
|
||
return $this->result->error('缺少插件标识');
|
||
}
|
||
try {
|
||
$data = AddonDevService::instance($name)->readAll();
|
||
return $this->result->success($data);
|
||
} catch (\Exception $e) {
|
||
return $this->result->error($e->getMessage());
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 保存插件配置(type: basic/config/menu/hooks/route)
|
||
*/
|
||
public function designSave()
|
||
{
|
||
if (!AddonDevService::enabled()) {
|
||
return $this->result->error('开发模式未开启');
|
||
}
|
||
$name = input('addon', '');
|
||
$type = input('type', '');
|
||
if (!$name) {
|
||
return $this->result->error('缺少插件标识');
|
||
}
|
||
try {
|
||
$svc = AddonDevService::instance($name);
|
||
switch ($type) {
|
||
case 'basic':
|
||
$svc->saveBasic(input('post.'));
|
||
break;
|
||
case 'config':
|
||
$fields = json_decode(input('fields', '[]'), true) ?: [];
|
||
$svc->saveConfig($fields);
|
||
break;
|
||
case 'menu':
|
||
$menu = json_decode(input('menu', '[]'), true) ?: [];
|
||
$svc->saveMenu($menu);
|
||
break;
|
||
case 'hooks':
|
||
$events = json_decode(input('events', '[]'), true) ?: [];
|
||
$middleware = json_decode(input('middleware', '[]'), true) ?: [];
|
||
$services = json_decode(input('services', '[]'), true) ?: [];
|
||
$svc->saveHooks($events, $middleware, $services);
|
||
break;
|
||
case 'route':
|
||
$routes = json_decode(input('routes', '[]'), true) ?: [];
|
||
$svc->saveRoute($routes);
|
||
break;
|
||
default:
|
||
return $this->result->error('未知保存类型:' . $type);
|
||
}
|
||
return $this->result->success([], '保存成功');
|
||
} catch (\Exception $e) {
|
||
return $this->result->error($e->getMessage());
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 生成源码文件(gtype: controller/model/event/listener/middleware/service/subscribe/validate/command)
|
||
*/
|
||
public function designGenerate()
|
||
{
|
||
if (!AddonDevService::enabled()) {
|
||
return $this->result->error('开发模式未开启');
|
||
}
|
||
$name = input('addon', '');
|
||
$type = input('gtype', '');
|
||
if (!$name || !$type) {
|
||
return $this->result->error('缺少参数');
|
||
}
|
||
try {
|
||
$opts = json_decode(input('opts', '[]'), true) ?: [];
|
||
$file = AddonDevService::instance($name)->generate($type, $opts);
|
||
$rel = ltrim(str_replace(ADDON_PATH . $name . DIRECTORY_SEPARATOR, '', $file), DIRECTORY_SEPARATOR);
|
||
return $this->result->success(['file' => $rel], '生成成功:' . $rel);
|
||
} catch (\Exception $e) {
|
||
return $this->result->error($e->getMessage());
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 开发模式安装(原地建表/注入菜单/启用,免打包,实时生效)
|
||
*/
|
||
public function designInstall()
|
||
{
|
||
if (!AddonDevService::enabled()) {
|
||
return $this->result->error('开发模式未开启');
|
||
}
|
||
$name = input('addon', '');
|
||
if (!$name) {
|
||
return $this->result->error('缺少插件标识');
|
||
}
|
||
try {
|
||
$info = AddonDevService::instance($name)->developInstall();
|
||
return $this->result->success($info, '开发模式安装成功,菜单已注入并启用');
|
||
} catch (\Exception $e) {
|
||
return $this->result->error($e->getMessage());
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 删除插件目录(开发调试用)
|
||
*/
|
||
public function designRemove()
|
||
{
|
||
if (!AddonDevService::enabled()) {
|
||
return $this->result->error('开发模式未开启');
|
||
}
|
||
$name = input('addon', '');
|
||
if (!$name) {
|
||
return $this->result->error('缺少插件标识');
|
||
}
|
||
try {
|
||
AddonDevService::instance($name)->remove();
|
||
return $this->result->success([], '插件目录已删除');
|
||
} catch (\Exception $e) {
|
||
return $this->result->error($e->getMessage());
|
||
}
|
||
}
|
||
}
|