项目初始化
This commit is contained in:
206
app/admin/controller/Ajax.php
Normal file
206
app/admin/controller/Ajax.php
Normal file
@@ -0,0 +1,206 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace app\admin\controller;
|
||||
|
||||
use ba\Terminal;
|
||||
use ba\TableManager;
|
||||
use support\think\Db;
|
||||
use app\admin\model\AdminLog;
|
||||
use app\common\library\Upload;
|
||||
use app\common\library\upload\WebmanUploadedFile;
|
||||
use app\common\controller\Backend;
|
||||
use Webman\Http\Request;
|
||||
use support\Response;
|
||||
|
||||
class Ajax extends Backend
|
||||
{
|
||||
protected array $noNeedPermission = ['*'];
|
||||
protected array $noNeedLogin = ['terminal'];
|
||||
|
||||
public function upload(Request $request): Response
|
||||
{
|
||||
$response = $this->initializeBackend($request);
|
||||
if ($response !== null) return $response;
|
||||
|
||||
AdminLog::instance($request)->setTitle(__('upload'));
|
||||
$file = $request->file('file');
|
||||
if (!$file) {
|
||||
return $this->error(__('No files were uploaded'));
|
||||
}
|
||||
$file = new WebmanUploadedFile($file);
|
||||
$driver = $request->get('driver', $request->post('driver', 'local'));
|
||||
$topic = $request->get('topic', $request->post('topic', 'default'));
|
||||
try {
|
||||
$upload = new Upload();
|
||||
$attachment = $upload
|
||||
->setFile($file)
|
||||
->setDriver($driver)
|
||||
->setTopic($topic)
|
||||
->upload(null, $this->auth->id);
|
||||
unset($attachment['create_time'], $attachment['quote']);
|
||||
} catch (\Throwable $e) {
|
||||
return $this->error($e->getMessage());
|
||||
}
|
||||
return $this->success(__('File uploaded successfully'), [
|
||||
'file' => $attachment ?? []
|
||||
]);
|
||||
}
|
||||
|
||||
public function area(Request $request): Response
|
||||
{
|
||||
$response = $this->initializeBackend($request);
|
||||
if ($response !== null) return $response;
|
||||
|
||||
return $this->success('', get_area($request));
|
||||
}
|
||||
|
||||
public function buildSuffixSvg(Request $request): Response
|
||||
{
|
||||
$response = $this->initializeBackend($request);
|
||||
if ($response !== null) return $response;
|
||||
|
||||
$suffix = $request->get('suffix', $request->post('suffix', 'file'));
|
||||
$background = $request->get('background', $request->post('background'));
|
||||
$content = build_suffix_svg((string) $suffix, (string) $background);
|
||||
return response($content, 200, [
|
||||
'Content-Length' => strlen($content),
|
||||
'Content-Type' => 'image/svg+xml'
|
||||
]);
|
||||
}
|
||||
|
||||
public function getDatabaseConnectionList(Request $request): Response
|
||||
{
|
||||
$response = $this->initializeBackend($request);
|
||||
if ($response !== null) return $response;
|
||||
|
||||
$quickSearch = $request->get('quickSearch', '');
|
||||
$connections = config('thinkorm.connections', config('database.connections', []));
|
||||
$desensitization = [];
|
||||
foreach ($connections as $key => $connection) {
|
||||
$connConfig = TableManager::getConnectionConfig($key);
|
||||
$desensitization[] = [
|
||||
'type' => $connConfig['type'] ?? 'mysql',
|
||||
'database' => substr_replace($connConfig['database'] ?? '', '****', 1, strlen($connConfig['database'] ?? '') > 4 ? 2 : 1),
|
||||
'key' => $key,
|
||||
];
|
||||
}
|
||||
if ($quickSearch) {
|
||||
$desensitization = array_values(array_filter($desensitization, function ($item) use ($quickSearch) {
|
||||
return preg_match("/$quickSearch/i", $item['key']);
|
||||
}));
|
||||
}
|
||||
return $this->success('', ['list' => $desensitization]);
|
||||
}
|
||||
|
||||
public function getTablePk(Request $request, ?string $table = null, ?string $connection = null): Response
|
||||
{
|
||||
$response = $this->initializeBackend($request);
|
||||
if ($response !== null) return $response;
|
||||
|
||||
$table = $table ?? $request->get('table', $request->post('table'));
|
||||
$connection = $connection ?? $request->get('connection', $request->post('connection'));
|
||||
if (!$table) {
|
||||
return $this->error(__('Parameter error'));
|
||||
}
|
||||
$table = TableManager::tableName($table, true, $connection);
|
||||
if (!TableManager::phinxAdapter(false, $connection)->hasTable($table)) {
|
||||
return $this->error(__('Data table does not exist'));
|
||||
}
|
||||
$conn = TableManager::getConnection($connection);
|
||||
$tablePk = Db::connect($conn)->table($table)->getPk();
|
||||
return $this->success('', ['pk' => $tablePk]);
|
||||
}
|
||||
|
||||
public function getTableList(Request $request): Response
|
||||
{
|
||||
$response = $this->initializeBackend($request);
|
||||
if ($response !== null) return $response;
|
||||
|
||||
$quickSearch = $request->get('quickSearch', $request->post('quickSearch', ''));
|
||||
$connection = $request->get('connection', $request->post('connection'));
|
||||
$samePrefix = filter_var($request->get('samePrefix', $request->post('samePrefix', true)), FILTER_VALIDATE_BOOLEAN);
|
||||
$excludeTable = $request->get('excludeTable', $request->post('excludeTable', []));
|
||||
$excludeTable = is_array($excludeTable) ? $excludeTable : [];
|
||||
|
||||
$dbConfig = TableManager::getConnectionConfig($connection);
|
||||
$tables = TableManager::getTableList($connection);
|
||||
if ($quickSearch) {
|
||||
$tables = array_filter($tables, function ($comment) use ($quickSearch) {
|
||||
return preg_match("/$quickSearch/i", $comment);
|
||||
});
|
||||
}
|
||||
$pattern = '/^' . preg_quote($dbConfig['prefix'] ?? '', '/') . '/i';
|
||||
$outTables = [];
|
||||
foreach ($tables as $table => $comment) {
|
||||
if ($samePrefix && !preg_match($pattern, $table)) continue;
|
||||
$tableNoPrefix = preg_replace($pattern, '', $table);
|
||||
if (!in_array($tableNoPrefix, $excludeTable)) {
|
||||
$outTables[] = [
|
||||
'table' => $tableNoPrefix,
|
||||
'comment' => $comment,
|
||||
'connection' => $connection,
|
||||
'prefix' => $dbConfig['prefix'] ?? '',
|
||||
];
|
||||
}
|
||||
}
|
||||
return $this->success('', ['list' => $outTables]);
|
||||
}
|
||||
|
||||
public function getTableFieldList(Request $request): Response
|
||||
{
|
||||
$response = $this->initializeBackend($request);
|
||||
if ($response !== null) return $response;
|
||||
|
||||
$table = $request->get('table', $request->post('table'));
|
||||
$clean = filter_var($request->get('clean', $request->post('clean', true)), FILTER_VALIDATE_BOOLEAN);
|
||||
$connection = $request->get('connection', $request->post('connection'));
|
||||
if (!$table) {
|
||||
return $this->error(__('Parameter error'));
|
||||
}
|
||||
$conn = TableManager::getConnection($connection);
|
||||
$tablePk = Db::connect($conn)->name($table)->getPk();
|
||||
return $this->success('', [
|
||||
'pk' => $tablePk,
|
||||
'fieldList' => TableManager::getTableColumns($table, $clean, $conn),
|
||||
]);
|
||||
}
|
||||
|
||||
public function changeTerminalConfig(Request $request): Response
|
||||
{
|
||||
$response = $this->initializeBackend($request);
|
||||
if ($response !== null) return $response;
|
||||
|
||||
AdminLog::instance($request)->setTitle(__('Change terminal config'));
|
||||
if (Terminal::changeTerminalConfig()) {
|
||||
return $this->success();
|
||||
}
|
||||
return $this->error(__('Failed to modify the terminal configuration. Please modify the configuration file manually:%s', ['/config/terminal.php']));
|
||||
}
|
||||
|
||||
public function clearCache(Request $request): Response
|
||||
{
|
||||
$response = $this->initializeBackend($request);
|
||||
if ($response !== null) return $response;
|
||||
|
||||
AdminLog::instance($request)->setTitle(__('Clear cache'));
|
||||
$type = $request->post('type');
|
||||
if ($type === 'tp' || $type === 'all') {
|
||||
clear_config_cache();
|
||||
} else {
|
||||
return $this->error(__('Parameter error'));
|
||||
}
|
||||
event_trigger('cacheClearAfter');
|
||||
return $this->success(__('Cache cleaned~'));
|
||||
}
|
||||
|
||||
public function terminal(Request $request): Response
|
||||
{
|
||||
$response = $this->initializeBackend($request);
|
||||
if ($response !== null) return $response;
|
||||
|
||||
(new Terminal())->exec();
|
||||
return $this->success();
|
||||
}
|
||||
}
|
||||
22
app/admin/controller/Dashboard.php
Normal file
22
app/admin/controller/Dashboard.php
Normal file
@@ -0,0 +1,22 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace app\admin\controller;
|
||||
|
||||
use app\common\controller\Backend;
|
||||
use Webman\Http\Request;
|
||||
use support\Response;
|
||||
|
||||
class Dashboard extends Backend
|
||||
{
|
||||
public function index(Request $request): Response
|
||||
{
|
||||
$response = $this->initializeBackend($request);
|
||||
if ($response !== null) return $response;
|
||||
|
||||
return $this->success('', [
|
||||
'remark' => get_route_remark()
|
||||
]);
|
||||
}
|
||||
}
|
||||
151
app/admin/controller/Index.php
Normal file
151
app/admin/controller/Index.php
Normal file
@@ -0,0 +1,151 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace app\admin\controller;
|
||||
|
||||
use ba\ClickCaptcha;
|
||||
use ba\Random;
|
||||
use app\common\facade\Token;
|
||||
use app\admin\model\AdminLog;
|
||||
use app\common\controller\Backend;
|
||||
use support\validation\Validator;
|
||||
use support\validation\ValidationException;
|
||||
use Webman\Http\Request;
|
||||
use support\Response;
|
||||
|
||||
class Index extends Backend
|
||||
{
|
||||
protected array $noNeedLogin = ['logout', 'login'];
|
||||
protected array $noNeedPermission = ['index'];
|
||||
|
||||
public function index(Request $request): Response
|
||||
{
|
||||
$response = $this->initializeBackend($request);
|
||||
if ($response !== null) return $response;
|
||||
|
||||
$adminInfo = $this->auth->getInfo();
|
||||
$adminInfo['super'] = $this->auth->isSuperAdmin();
|
||||
unset($adminInfo['token'], $adminInfo['refresh_token']);
|
||||
|
||||
$menus = $this->auth->getMenus();
|
||||
if (!$menus) {
|
||||
return $this->error(__('No background menu, please contact super administrator!'));
|
||||
}
|
||||
|
||||
$apiUrl = config('buildadmin.api_url');
|
||||
if (!$apiUrl || $apiUrl === 'https://api.buildadmin.com') {
|
||||
$scheme = (!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off') ? 'https' : 'http';
|
||||
$apiUrl = $scheme . '://' . $request->host();
|
||||
}
|
||||
|
||||
return $this->success('', [
|
||||
'adminInfo' => $adminInfo,
|
||||
'menus' => $menus,
|
||||
'siteConfig' => [
|
||||
'siteName' => get_sys_config('site_name'),
|
||||
'version' => get_sys_config('version'),
|
||||
'apiUrl' => $apiUrl,
|
||||
'upload' => keys_to_camel_case(get_upload_config($request), ['max_size', 'save_name', 'allowed_suffixes', 'allowed_mime_types']),
|
||||
'cdnUrl' => full_url(),
|
||||
'cdnUrlParams' => config('buildadmin.cdn_url_params'),
|
||||
],
|
||||
'terminal' => [
|
||||
'phpDevelopmentServer' => str_contains($_SERVER['SERVER_SOFTWARE'] ?? '', 'Development Server'),
|
||||
'npmPackageManager' => config('terminal.npm_package_manager'),
|
||||
]
|
||||
]);
|
||||
}
|
||||
|
||||
public function login(Request $request): Response
|
||||
{
|
||||
$response = $this->initializeBackend($request);
|
||||
if ($response !== null) return $response;
|
||||
|
||||
if ($this->auth->isLogin()) {
|
||||
return $this->success(__('You have already logged in. There is no need to log in again~'), [
|
||||
'type' => $this->auth::LOGGED_IN
|
||||
], $this->auth::LOGIN_RESPONSE_CODE);
|
||||
}
|
||||
|
||||
$captchaSwitch = config('buildadmin.admin_login_captcha');
|
||||
|
||||
if ($request->method() === 'POST') {
|
||||
$username = $request->post('username');
|
||||
$password = $request->post('password');
|
||||
$keep = $request->post('keep');
|
||||
|
||||
$rules = [
|
||||
'username' => 'required|string|min:3|max:30',
|
||||
'password' => 'required|string|regex:/^(?!.*[&<>"\'\n\r]).{6,32}$/',
|
||||
];
|
||||
$data = ['username' => $username, 'password' => $password];
|
||||
if ($captchaSwitch) {
|
||||
$rules['captchaId'] = 'required|string';
|
||||
$rules['captchaInfo'] = 'required|string';
|
||||
$data['captchaId'] = $request->post('captchaId');
|
||||
$data['captchaInfo'] = $request->post('captchaInfo');
|
||||
}
|
||||
|
||||
try {
|
||||
Validator::make($data, $rules, [
|
||||
'username.required' => __('Username'),
|
||||
'password.required' => __('Password'),
|
||||
'password.regex' => __('Please input correct password'),
|
||||
])->validate();
|
||||
} catch (ValidationException $e) {
|
||||
return $this->error($e->getMessage());
|
||||
}
|
||||
|
||||
if ($captchaSwitch) {
|
||||
$captchaObj = new ClickCaptcha();
|
||||
if (!$captchaObj->check($data['captchaId'], $data['captchaInfo'])) {
|
||||
return $this->error(__('Captcha error'));
|
||||
}
|
||||
}
|
||||
|
||||
AdminLog::instance($request)->setTitle(__('Login'));
|
||||
|
||||
$res = $this->auth->login($username, $password, (bool) $keep);
|
||||
if ($res === true) {
|
||||
$userInfo = $this->auth->getInfo();
|
||||
$adminId = $this->auth->id;
|
||||
$keepTime = (int) config('buildadmin.admin_token_keep_time', 86400 * 3);
|
||||
// 兜底:若 getInfo 未返回 token,在控制器层生成并入库(login 成功时必有 adminId)
|
||||
if (empty($userInfo['token']) && $adminId) {
|
||||
$userInfo['token'] = Random::uuid();
|
||||
Token::set($userInfo['token'], \app\admin\library\Auth::TOKEN_TYPE, $adminId, $keepTime);
|
||||
}
|
||||
if (empty($userInfo['refresh_token']) && $keep && $adminId) {
|
||||
$userInfo['refresh_token'] = Random::uuid();
|
||||
Token::set($userInfo['refresh_token'], \app\admin\library\Auth::TOKEN_TYPE . '-refresh', $adminId, 2592000);
|
||||
}
|
||||
return $this->success(__('Login succeeded!'), [
|
||||
'userInfo' => $userInfo
|
||||
]);
|
||||
}
|
||||
$msg = $this->auth->getError();
|
||||
return $this->error($msg ?: __('Incorrect user name or password!'));
|
||||
}
|
||||
|
||||
return $this->success('', [
|
||||
'captcha' => $captchaSwitch
|
||||
]);
|
||||
}
|
||||
|
||||
public function logout(Request $request): Response
|
||||
{
|
||||
$response = $this->initializeBackend($request);
|
||||
if ($response !== null) return $response;
|
||||
|
||||
if ($request->method() === 'POST') {
|
||||
$refreshToken = $request->post('refreshToken', '');
|
||||
if ($refreshToken) {
|
||||
Token::delete((string) $refreshToken);
|
||||
}
|
||||
$this->auth->logout();
|
||||
return $this->success();
|
||||
}
|
||||
return $this->error(__('Method not allowed'), [], 0, ['statusCode' => 405]);
|
||||
}
|
||||
}
|
||||
147
app/admin/controller/Module.php
Normal file
147
app/admin/controller/Module.php
Normal file
@@ -0,0 +1,147 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace app\admin\controller;
|
||||
|
||||
use ba\Exception as BaException;
|
||||
use app\admin\model\AdminLog;
|
||||
use app\admin\library\module\Server;
|
||||
use app\admin\library\module\Manage;
|
||||
use app\common\controller\Backend;
|
||||
use Webman\Http\Request;
|
||||
use support\Response;
|
||||
|
||||
class Module extends Backend
|
||||
{
|
||||
protected array $noNeedPermission = ['state', 'dependentInstallComplete'];
|
||||
|
||||
public function index(Request $request): Response
|
||||
{
|
||||
$response = $this->initializeBackend($request);
|
||||
if ($response !== null) return $response;
|
||||
|
||||
return $this->success('', [
|
||||
'installed' => Server::installedList(root_path() . 'modules' . DIRECTORY_SEPARATOR),
|
||||
'sysVersion' => config('buildadmin.version'),
|
||||
'nuxtVersion' => Server::getNuxtVersion(),
|
||||
]);
|
||||
}
|
||||
|
||||
public function state(Request $request): Response
|
||||
{
|
||||
$response = $this->initializeBackend($request);
|
||||
if ($response !== null) return $response;
|
||||
|
||||
$uid = $request->get('uid', '');
|
||||
if (!$uid) {
|
||||
return $this->error(__('Parameter error'));
|
||||
}
|
||||
return $this->success('', [
|
||||
'state' => Manage::instance($uid)->getInstallState()
|
||||
]);
|
||||
}
|
||||
|
||||
public function install(Request $request): Response
|
||||
{
|
||||
$response = $this->initializeBackend($request);
|
||||
if ($response !== null) return $response;
|
||||
|
||||
AdminLog::instance($request)->setTitle(__('Install module'));
|
||||
$uid = $request->get('uid', $request->post('uid', ''));
|
||||
$update = filter_var($request->get('update', $request->post('update', false)), FILTER_VALIDATE_BOOLEAN);
|
||||
if (!$uid) {
|
||||
return $this->error(__('Parameter error'));
|
||||
}
|
||||
$res = [];
|
||||
try {
|
||||
$res = Manage::instance($uid)->install($update);
|
||||
} catch (BaException $e) {
|
||||
return $this->error(__($e->getMessage()), $e->getData(), $e->getCode());
|
||||
} catch (\Throwable $e) {
|
||||
return $this->error(__($e->getMessage()));
|
||||
}
|
||||
return $this->success('', ['data' => $res]);
|
||||
}
|
||||
|
||||
public function dependentInstallComplete(Request $request): Response
|
||||
{
|
||||
$response = $this->initializeBackend($request);
|
||||
if ($response !== null) return $response;
|
||||
|
||||
$uid = $request->get('uid', '');
|
||||
if (!$uid) {
|
||||
return $this->error(__('Parameter error'));
|
||||
}
|
||||
try {
|
||||
Manage::instance($uid)->dependentInstallComplete('all');
|
||||
} catch (BaException $e) {
|
||||
return $this->error(__($e->getMessage()), $e->getData(), $e->getCode());
|
||||
} catch (\Throwable $e) {
|
||||
return $this->error(__($e->getMessage()));
|
||||
}
|
||||
return $this->success();
|
||||
}
|
||||
|
||||
public function changeState(Request $request): Response
|
||||
{
|
||||
$response = $this->initializeBackend($request);
|
||||
if ($response !== null) return $response;
|
||||
|
||||
AdminLog::instance($request)->setTitle(__('Change module state'));
|
||||
$uid = $request->post('uid', '');
|
||||
$state = filter_var($request->post('state', false), FILTER_VALIDATE_BOOLEAN);
|
||||
if (!$uid) {
|
||||
return $this->error(__('Parameter error'));
|
||||
}
|
||||
$info = [];
|
||||
try {
|
||||
$info = Manage::instance($uid)->changeState($state);
|
||||
} catch (BaException $e) {
|
||||
return $this->error(__($e->getMessage()), $e->getData(), $e->getCode());
|
||||
} catch (\Throwable $e) {
|
||||
return $this->error(__($e->getMessage()));
|
||||
}
|
||||
return $this->success('', ['info' => $info]);
|
||||
}
|
||||
|
||||
public function uninstall(Request $request): Response
|
||||
{
|
||||
$response = $this->initializeBackend($request);
|
||||
if ($response !== null) return $response;
|
||||
|
||||
AdminLog::instance($request)->setTitle(__('Uninstall module'));
|
||||
$uid = $request->post('uid', '');
|
||||
if (!$uid) {
|
||||
return $this->error(__('Parameter error'));
|
||||
}
|
||||
try {
|
||||
Manage::instance($uid)->uninstall();
|
||||
} catch (BaException $e) {
|
||||
return $this->error(__($e->getMessage()), $e->getData(), $e->getCode());
|
||||
} catch (\Throwable $e) {
|
||||
return $this->error(__($e->getMessage()));
|
||||
}
|
||||
return $this->success();
|
||||
}
|
||||
|
||||
public function upload(Request $request): Response
|
||||
{
|
||||
$response = $this->initializeBackend($request);
|
||||
if ($response !== null) return $response;
|
||||
|
||||
AdminLog::instance($request)->setTitle(__('Upload module'));
|
||||
$file = $request->file('file');
|
||||
if (!$file) {
|
||||
return $this->error(__('Parameter error'));
|
||||
}
|
||||
try {
|
||||
$res = Manage::uploadFromRequest($request);
|
||||
} catch (BaException $e) {
|
||||
return $this->error(__($e->getMessage()), $e->getData(), $e->getCode());
|
||||
} catch (\Throwable $e) {
|
||||
return $this->error(__($e->getMessage()));
|
||||
}
|
||||
return $this->success('', $res);
|
||||
}
|
||||
}
|
||||
286
app/admin/controller/auth/Admin.php
Normal file
286
app/admin/controller/auth/Admin.php
Normal file
@@ -0,0 +1,286 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace app\admin\controller\auth;
|
||||
|
||||
use Throwable;
|
||||
use support\think\Db;
|
||||
use support\validation\Validator;
|
||||
use support\validation\ValidationException;
|
||||
use app\common\controller\Backend;
|
||||
use app\admin\model\Admin as AdminModel;
|
||||
use support\Response;
|
||||
use Webman\Http\Request;
|
||||
|
||||
class Admin extends Backend
|
||||
{
|
||||
protected ?object $model = null;
|
||||
|
||||
protected array|string $preExcludeFields = ['create_time', 'update_time', 'password', 'salt', 'login_failure', 'last_login_time', 'last_login_ip'];
|
||||
|
||||
protected array|string $quickSearchField = ['username', 'nickname'];
|
||||
|
||||
protected string|int|bool $dataLimit = 'allAuthAndOthers';
|
||||
|
||||
protected string $dataLimitField = 'id';
|
||||
|
||||
protected function initController(Request $request): ?Response
|
||||
{
|
||||
$this->model = new AdminModel();
|
||||
return null;
|
||||
}
|
||||
|
||||
public function index(Request $request): Response
|
||||
{
|
||||
$response = $this->initializeBackend($request);
|
||||
if ($response !== null) return $response;
|
||||
|
||||
if ($request->get('select') ?? $request->post('select')) {
|
||||
$selectRes = $this->select($request);
|
||||
if ($selectRes !== null) return $selectRes;
|
||||
}
|
||||
|
||||
list($where, $alias, $limit, $order) = $this->queryBuilder();
|
||||
$res = $this->model
|
||||
->withoutField('login_failure,password,salt')
|
||||
->withJoin($this->withJoinTable, $this->withJoinType)
|
||||
->alias($alias)
|
||||
->where($where)
|
||||
->order($order)
|
||||
->paginate($limit);
|
||||
|
||||
return $this->success('', [
|
||||
'list' => $res->items(),
|
||||
'total' => $res->total(),
|
||||
'remark' => get_route_remark(),
|
||||
]);
|
||||
}
|
||||
|
||||
public function add(Request $request): Response
|
||||
{
|
||||
$response = $this->initializeBackend($request);
|
||||
if ($response !== null) return $response;
|
||||
|
||||
if ($request->method() === 'POST') {
|
||||
$data = $request->post();
|
||||
if (!$data) {
|
||||
return $this->error(__('Parameter %s can not be empty', ['']));
|
||||
}
|
||||
|
||||
if ($this->modelValidate) {
|
||||
try {
|
||||
$rules = [
|
||||
'username' => 'required|string|regex:/^[a-zA-Z][a-zA-Z0-9_]{2,15}$/|unique:admin,username',
|
||||
'nickname' => 'required|string',
|
||||
'password' => 'required|string|regex:/^(?!.*[&<>"\'\n\r]).{6,32}$/',
|
||||
'email' => 'email|unique:admin,email',
|
||||
'mobile' => 'regex:/^1[3-9]\d{9}$/|unique:admin,mobile',
|
||||
'group_arr' => 'required|array',
|
||||
];
|
||||
$messages = [
|
||||
'username.regex' => __('Please input correct username'),
|
||||
'password.regex' => __('Please input correct password'),
|
||||
];
|
||||
Validator::make($data, $rules, $messages)->validate();
|
||||
} catch (ValidationException $e) {
|
||||
return $this->error($e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
$passwd = $data['password'] ?? '';
|
||||
$data = $this->excludeFields($data);
|
||||
$result = false;
|
||||
if (!empty($data['group_arr'])) {
|
||||
$authRes = $this->checkGroupAuth($data['group_arr']);
|
||||
if ($authRes !== null) return $authRes;
|
||||
}
|
||||
$this->model->startTrans();
|
||||
try {
|
||||
$result = $this->model->save($data);
|
||||
if (!empty($data['group_arr'])) {
|
||||
$groupAccess = [];
|
||||
foreach ($data['group_arr'] as $datum) {
|
||||
$groupAccess[] = [
|
||||
'uid' => $this->model->id,
|
||||
'group_id' => $datum,
|
||||
];
|
||||
}
|
||||
Db::name('admin_group_access')->insertAll($groupAccess);
|
||||
}
|
||||
$this->model->commit();
|
||||
|
||||
if (!empty($passwd)) {
|
||||
$this->model->resetPassword($this->model->id, $passwd);
|
||||
}
|
||||
} catch (Throwable $e) {
|
||||
$this->model->rollback();
|
||||
return $this->error($e->getMessage());
|
||||
}
|
||||
if ($result !== false) {
|
||||
return $this->success(__('Added successfully'));
|
||||
}
|
||||
return $this->error(__('No rows were added'));
|
||||
}
|
||||
|
||||
return $this->error(__('Parameter error'));
|
||||
}
|
||||
|
||||
public function edit(Request $request): Response
|
||||
{
|
||||
$response = $this->initializeBackend($request);
|
||||
if ($response !== null) return $response;
|
||||
|
||||
$pk = $this->model->getPk();
|
||||
$id = $request->get($pk) ?? $request->post($pk);
|
||||
$row = $this->model->find($id);
|
||||
if (!$row) {
|
||||
return $this->error(__('Record not found'));
|
||||
}
|
||||
|
||||
$dataLimitAdminIds = $this->getDataLimitAdminIds();
|
||||
if ($dataLimitAdminIds && !in_array($row[$this->dataLimitField], $dataLimitAdminIds)) {
|
||||
return $this->error(__('You have no permission'));
|
||||
}
|
||||
|
||||
if ($request->method() === 'POST') {
|
||||
$data = $request->post();
|
||||
if (!$data) {
|
||||
return $this->error(__('Parameter %s can not be empty', ['']));
|
||||
}
|
||||
|
||||
if ($this->modelValidate) {
|
||||
try {
|
||||
$rules = [
|
||||
'username' => 'required|string|regex:/^[a-zA-Z][a-zA-Z0-9_]{2,15}$/|unique:admin,username,' . $id,
|
||||
'nickname' => 'required|string',
|
||||
'password' => 'nullable|string|regex:/^(?!.*[&<>"\'\n\r]).{6,32}$/',
|
||||
'email' => 'email|unique:admin,email,' . $id,
|
||||
'mobile' => 'regex:/^1[3-9]\d{9}$/|unique:admin,mobile,' . $id,
|
||||
'group_arr' => 'required|array',
|
||||
];
|
||||
$messages = [
|
||||
'username.regex' => __('Please input correct username'),
|
||||
'password.regex' => __('Please input correct password'),
|
||||
];
|
||||
Validator::make($data, $rules, $messages)->validate();
|
||||
} catch (ValidationException $e) {
|
||||
return $this->error($e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
if ($this->auth->id == $data['id'] && ($data['status'] ?? '') == 'disable') {
|
||||
return $this->error(__('Please use another administrator account to disable the current account!'));
|
||||
}
|
||||
|
||||
if (!empty($data['password'])) {
|
||||
$this->model->resetPassword($row->id, $data['password']);
|
||||
}
|
||||
|
||||
$groupAccess = [];
|
||||
if (!empty($data['group_arr'])) {
|
||||
$checkGroups = [];
|
||||
$rowGroupArr = $row->group_arr ?? [];
|
||||
foreach ($data['group_arr'] as $datum) {
|
||||
if (!in_array($datum, $rowGroupArr)) {
|
||||
$checkGroups[] = $datum;
|
||||
}
|
||||
$groupAccess[] = [
|
||||
'uid' => $id,
|
||||
'group_id' => $datum,
|
||||
];
|
||||
}
|
||||
$authRes = $this->checkGroupAuth($checkGroups);
|
||||
if ($authRes !== null) return $authRes;
|
||||
}
|
||||
|
||||
Db::name('admin_group_access')
|
||||
->where('uid', $id)
|
||||
->delete();
|
||||
|
||||
$data = $this->excludeFields($data);
|
||||
$result = false;
|
||||
$this->model->startTrans();
|
||||
try {
|
||||
$result = $row->save($data);
|
||||
if ($groupAccess) {
|
||||
Db::name('admin_group_access')->insertAll($groupAccess);
|
||||
}
|
||||
$this->model->commit();
|
||||
} catch (Throwable $e) {
|
||||
$this->model->rollback();
|
||||
return $this->error($e->getMessage());
|
||||
}
|
||||
if ($result !== false) {
|
||||
return $this->success(__('Update successful'));
|
||||
}
|
||||
return $this->error(__('No rows updated'));
|
||||
}
|
||||
|
||||
unset($row['salt'], $row['login_failure']);
|
||||
$row['password'] = '';
|
||||
return $this->success('', [
|
||||
'row' => $row
|
||||
]);
|
||||
}
|
||||
|
||||
public function del(Request $request): Response
|
||||
{
|
||||
$response = $this->initializeBackend($request);
|
||||
if ($response !== null) return $response;
|
||||
|
||||
$where = [];
|
||||
$dataLimitAdminIds = $this->getDataLimitAdminIds();
|
||||
if ($dataLimitAdminIds) {
|
||||
$where[] = [$this->dataLimitField, 'in', $dataLimitAdminIds];
|
||||
}
|
||||
|
||||
$ids = $request->get('ids') ?? $request->post('ids') ?? [];
|
||||
$ids = is_array($ids) ? $ids : [];
|
||||
$where[] = [$this->model->getPk(), 'in', $ids];
|
||||
$data = $this->model->where($where)->select();
|
||||
|
||||
$count = 0;
|
||||
$this->model->startTrans();
|
||||
try {
|
||||
foreach ($data as $v) {
|
||||
if ($v->id != $this->auth->id) {
|
||||
$count += $v->delete();
|
||||
Db::name('admin_group_access')
|
||||
->where('uid', $v['id'])
|
||||
->delete();
|
||||
}
|
||||
}
|
||||
$this->model->commit();
|
||||
} catch (Throwable $e) {
|
||||
$this->model->rollback();
|
||||
return $this->error($e->getMessage());
|
||||
}
|
||||
if ($count) {
|
||||
return $this->success(__('Deleted successfully'));
|
||||
}
|
||||
return $this->error(__('No rows were deleted'));
|
||||
}
|
||||
|
||||
/**
|
||||
* 远程下拉(Admin 无自定义,走父类默认列表)
|
||||
*/
|
||||
public function select(Request $request): Response
|
||||
{
|
||||
return parent::select($request);
|
||||
}
|
||||
|
||||
private function checkGroupAuth(array $groups): ?Response
|
||||
{
|
||||
if ($this->auth->isSuperAdmin()) {
|
||||
return null;
|
||||
}
|
||||
$authGroups = $this->auth->getAllAuthGroups('allAuthAndOthers');
|
||||
foreach ($groups as $group) {
|
||||
if (!in_array($group, $authGroups)) {
|
||||
return $this->error(__('You have no permission to add an administrator to this group!'));
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
62
app/admin/controller/auth/AdminLog.php
Normal file
62
app/admin/controller/auth/AdminLog.php
Normal file
@@ -0,0 +1,62 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace app\admin\controller\auth;
|
||||
|
||||
use Throwable;
|
||||
use app\common\controller\Backend;
|
||||
use app\admin\model\AdminLog as AdminLogModel;
|
||||
use support\Response;
|
||||
use Webman\Http\Request;
|
||||
|
||||
class AdminLog extends Backend
|
||||
{
|
||||
protected ?object $model = null;
|
||||
|
||||
protected string|array $preExcludeFields = ['create_time', 'admin_id', 'username'];
|
||||
|
||||
protected string|array $quickSearchField = ['title'];
|
||||
|
||||
protected function initController(Request $request): ?Response
|
||||
{
|
||||
$this->model = new AdminLogModel();
|
||||
return null;
|
||||
}
|
||||
|
||||
public function index(Request $request): Response
|
||||
{
|
||||
$response = $this->initializeBackend($request);
|
||||
if ($response !== null) return $response;
|
||||
|
||||
if ($request->get('select') ?? $request->post('select')) {
|
||||
$selectRes = $this->select($request);
|
||||
if ($selectRes !== null) return $selectRes;
|
||||
}
|
||||
|
||||
list($where, $alias, $limit, $order) = $this->queryBuilder();
|
||||
if (!$this->auth->isSuperAdmin()) {
|
||||
$where[] = ['admin_id', '=', $this->auth->id];
|
||||
}
|
||||
$res = $this->model
|
||||
->withJoin($this->withJoinTable, $this->withJoinType)
|
||||
->alias($alias)
|
||||
->where($where)
|
||||
->order($order)
|
||||
->paginate($limit);
|
||||
|
||||
return $this->success('', [
|
||||
'list' => $res->items(),
|
||||
'total' => $res->total(),
|
||||
'remark' => get_route_remark(),
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* 远程下拉(AdminLog 无自定义,走父类默认列表)
|
||||
*/
|
||||
public function select(Request $request): Response
|
||||
{
|
||||
return parent::select($request);
|
||||
}
|
||||
}
|
||||
346
app/admin/controller/auth/Group.php
Normal file
346
app/admin/controller/auth/Group.php
Normal file
@@ -0,0 +1,346 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace app\admin\controller\auth;
|
||||
|
||||
use Throwable;
|
||||
use ba\Tree;
|
||||
use support\think\Db;
|
||||
use support\validation\Validator;
|
||||
use support\validation\ValidationException;
|
||||
use app\admin\model\AdminRule;
|
||||
use app\admin\model\AdminGroup;
|
||||
use app\common\controller\Backend;
|
||||
use support\Response;
|
||||
use Webman\Http\Request;
|
||||
|
||||
class Group extends Backend
|
||||
{
|
||||
protected string $authMethod = 'allAuthAndOthers';
|
||||
|
||||
protected ?object $model = null;
|
||||
|
||||
protected string|array $preExcludeFields = ['create_time', 'update_time'];
|
||||
|
||||
protected string|array $quickSearchField = 'name';
|
||||
|
||||
protected Tree $tree;
|
||||
|
||||
protected array $initValue = [];
|
||||
|
||||
protected string $keyword = '';
|
||||
|
||||
protected bool $assembleTree = true;
|
||||
|
||||
protected array $adminGroups = [];
|
||||
|
||||
protected function initController(Request $request): ?Response
|
||||
{
|
||||
$this->model = new AdminGroup();
|
||||
$this->tree = Tree::instance();
|
||||
|
||||
$isTree = $request->get('isTree') ?? $request->post('isTree') ?? true;
|
||||
$initValue = $request->get('initValue') ?? $request->post('initValue') ?? [];
|
||||
$this->initValue = is_array($initValue) ? array_filter($initValue) : [];
|
||||
$this->keyword = $request->get('quickSearch') ?? $request->post('quickSearch') ?? '';
|
||||
|
||||
$this->assembleTree = $isTree && !$this->initValue;
|
||||
|
||||
$this->adminGroups = Db::name('admin_group_access')->where('uid', $this->auth->id)->column('group_id');
|
||||
return null;
|
||||
}
|
||||
|
||||
public function index(Request $request): Response
|
||||
{
|
||||
$response = $this->initializeBackend($request);
|
||||
if ($response !== null) return $response;
|
||||
|
||||
if ($request->get('select') ?? $request->post('select')) {
|
||||
return $this->select($request);
|
||||
}
|
||||
|
||||
return $this->success('', [
|
||||
'list' => $this->getGroups($request),
|
||||
'group' => $this->adminGroups,
|
||||
'remark' => get_route_remark(),
|
||||
]);
|
||||
}
|
||||
|
||||
public function add(Request $request): Response
|
||||
{
|
||||
$response = $this->initializeBackend($request);
|
||||
if ($response !== null) return $response;
|
||||
|
||||
if ($request->method() === 'POST') {
|
||||
$data = $request->post();
|
||||
if (!$data) {
|
||||
return $this->error(__('Parameter %s can not be empty', ['']));
|
||||
}
|
||||
|
||||
$data = $this->excludeFields($data);
|
||||
$rulesRes = $this->handleRules($data);
|
||||
if ($rulesRes instanceof Response) return $rulesRes;
|
||||
|
||||
$result = false;
|
||||
$this->model->startTrans();
|
||||
try {
|
||||
if ($this->modelValidate) {
|
||||
try {
|
||||
$rules = [
|
||||
'name' => 'required|string',
|
||||
'rules' => 'required',
|
||||
];
|
||||
$messages = [
|
||||
'rules.required' => __('Please select rules'),
|
||||
];
|
||||
Validator::make($data, $rules, $messages)->validate();
|
||||
} catch (ValidationException $e) {
|
||||
throw $e;
|
||||
}
|
||||
}
|
||||
$result = $this->model->save($data);
|
||||
$this->model->commit();
|
||||
} catch (Throwable $e) {
|
||||
$this->model->rollback();
|
||||
return $this->error($e->getMessage());
|
||||
}
|
||||
if ($result !== false) {
|
||||
return $this->success(__('Added successfully'));
|
||||
}
|
||||
return $this->error(__('No rows were added'));
|
||||
}
|
||||
|
||||
return $this->error(__('Parameter error'));
|
||||
}
|
||||
|
||||
public function edit(Request $request): Response
|
||||
{
|
||||
$response = $this->initializeBackend($request);
|
||||
if ($response !== null) return $response;
|
||||
|
||||
$pk = $this->model->getPk();
|
||||
$id = $request->get($pk) ?? $request->post($pk);
|
||||
$row = $this->model->find($id);
|
||||
if (!$row) {
|
||||
return $this->error(__('Record not found'));
|
||||
}
|
||||
|
||||
$authRes = $this->checkAuth($id);
|
||||
if ($authRes !== null) return $authRes;
|
||||
|
||||
if ($request->method() === 'POST') {
|
||||
$data = $request->post();
|
||||
if (!$data) {
|
||||
return $this->error(__('Parameter %s can not be empty', ['']));
|
||||
}
|
||||
|
||||
$adminGroup = Db::name('admin_group_access')->where('uid', $this->auth->id)->column('group_id');
|
||||
if (in_array($data['id'], $adminGroup)) {
|
||||
return $this->error(__('You cannot modify your own management group!'));
|
||||
}
|
||||
|
||||
$data = $this->excludeFields($data);
|
||||
$rulesRes = $this->handleRules($data);
|
||||
if ($rulesRes instanceof Response) return $rulesRes;
|
||||
|
||||
$result = false;
|
||||
$this->model->startTrans();
|
||||
try {
|
||||
if ($this->modelValidate) {
|
||||
try {
|
||||
$rules = [
|
||||
'name' => 'required|string',
|
||||
'rules' => 'required',
|
||||
];
|
||||
$messages = [
|
||||
'rules.required' => __('Please select rules'),
|
||||
];
|
||||
Validator::make($data, $rules, $messages)->validate();
|
||||
} catch (ValidationException $e) {
|
||||
throw $e;
|
||||
}
|
||||
}
|
||||
$result = $row->save($data);
|
||||
$this->model->commit();
|
||||
} catch (Throwable $e) {
|
||||
$this->model->rollback();
|
||||
return $this->error($e->getMessage());
|
||||
}
|
||||
if ($result !== false) {
|
||||
return $this->success(__('Update successful'));
|
||||
}
|
||||
return $this->error(__('No rows updated'));
|
||||
}
|
||||
|
||||
$pidArr = AdminRule::field('pid')
|
||||
->distinct()
|
||||
->where('id', 'in', $row->rules)
|
||||
->select()
|
||||
->toArray();
|
||||
$rules = $row->rules ? explode(',', $row->rules) : [];
|
||||
foreach ($pidArr as $item) {
|
||||
$ruKey = array_search($item['pid'], $rules);
|
||||
if ($ruKey !== false) {
|
||||
unset($rules[$ruKey]);
|
||||
}
|
||||
}
|
||||
$rowData = $row->toArray();
|
||||
$rowData['rules'] = array_values($rules);
|
||||
return $this->success('', [
|
||||
'row' => $rowData
|
||||
]);
|
||||
}
|
||||
|
||||
public function del(Request $request): Response
|
||||
{
|
||||
$response = $this->initializeBackend($request);
|
||||
if ($response !== null) return $response;
|
||||
|
||||
$ids = $request->get('ids') ?? $request->post('ids') ?? [];
|
||||
$ids = is_array($ids) ? $ids : [];
|
||||
$data = $this->model->where($this->model->getPk(), 'in', $ids)->select();
|
||||
foreach ($data as $v) {
|
||||
$authRes = $this->checkAuth($v->id);
|
||||
if ($authRes !== null) return $authRes;
|
||||
}
|
||||
$subData = $this->model->where('pid', 'in', $ids)->column('pid', 'id');
|
||||
foreach ($subData as $key => $subDatum) {
|
||||
if (!in_array($key, $ids)) {
|
||||
return $this->error(__('Please delete the child element first, or use batch deletion'));
|
||||
}
|
||||
}
|
||||
|
||||
$adminGroup = Db::name('admin_group_access')->where('uid', $this->auth->id)->column('group_id');
|
||||
$count = 0;
|
||||
$this->model->startTrans();
|
||||
try {
|
||||
foreach ($data as $v) {
|
||||
if (!in_array($v['id'], $adminGroup)) {
|
||||
$count += $v->delete();
|
||||
}
|
||||
}
|
||||
$this->model->commit();
|
||||
} catch (Throwable $e) {
|
||||
$this->model->rollback();
|
||||
return $this->error($e->getMessage());
|
||||
}
|
||||
if ($count) {
|
||||
return $this->success(__('Deleted successfully'));
|
||||
}
|
||||
return $this->error(__('No rows were deleted'));
|
||||
}
|
||||
|
||||
public function select(Request $request): Response
|
||||
{
|
||||
$response = $this->initializeBackend($request);
|
||||
if ($response !== null) return $response;
|
||||
|
||||
$data = $this->getGroups($request, [['status', '=', 1]]);
|
||||
|
||||
if ($this->assembleTree) {
|
||||
$data = $this->tree->assembleTree($this->tree->getTreeArray($data));
|
||||
}
|
||||
return $this->success('', [
|
||||
'options' => $data
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array|Response
|
||||
*/
|
||||
private function handleRules(array &$data)
|
||||
{
|
||||
if (!empty($data['rules']) && is_array($data['rules'])) {
|
||||
$superAdmin = true;
|
||||
$checkedRules = [];
|
||||
$allRuleIds = AdminRule::column('id');
|
||||
|
||||
foreach ($data['rules'] as $postRuleId) {
|
||||
if (in_array($postRuleId, $allRuleIds)) {
|
||||
$checkedRules[] = $postRuleId;
|
||||
}
|
||||
}
|
||||
|
||||
foreach ($allRuleIds as $ruleId) {
|
||||
if (!in_array($ruleId, $checkedRules)) {
|
||||
$superAdmin = false;
|
||||
}
|
||||
}
|
||||
|
||||
if ($superAdmin && $this->auth->isSuperAdmin()) {
|
||||
$data['rules'] = '*';
|
||||
} else {
|
||||
$ownedRuleIds = $this->auth->getRuleIds();
|
||||
|
||||
if (!array_diff($ownedRuleIds, $checkedRules)) {
|
||||
return $this->error(__('Role group has all your rights, please contact the upper administrator to add or do not need to add!'));
|
||||
}
|
||||
|
||||
if (array_diff($checkedRules, $ownedRuleIds) && !$this->auth->isSuperAdmin()) {
|
||||
return $this->error(__('The group permission node exceeds the range that can be allocated'));
|
||||
}
|
||||
|
||||
$data['rules'] = implode(',', $checkedRules);
|
||||
}
|
||||
} else {
|
||||
unset($data['rules']);
|
||||
}
|
||||
return $data;
|
||||
}
|
||||
|
||||
private function getGroups(Request $request, array $where = []): array
|
||||
{
|
||||
$pk = $this->model->getPk();
|
||||
$initKey = $request->get('initKey') ?? $pk;
|
||||
|
||||
$absoluteAuth = $request->get('absoluteAuth') ?? false;
|
||||
|
||||
if ($this->keyword) {
|
||||
$keyword = explode(' ', $this->keyword);
|
||||
foreach ($keyword as $item) {
|
||||
$where[] = [$this->quickSearchField, 'like', '%' . $item . '%'];
|
||||
}
|
||||
}
|
||||
|
||||
if ($this->initValue) {
|
||||
$where[] = [$initKey, 'in', $this->initValue];
|
||||
}
|
||||
|
||||
if (!$this->auth->isSuperAdmin()) {
|
||||
$authGroups = $this->auth->getAllAuthGroups($this->authMethod, $where);
|
||||
if (!$absoluteAuth) {
|
||||
$authGroups = array_merge($this->adminGroups, $authGroups);
|
||||
}
|
||||
$where[] = ['id', 'in', $authGroups];
|
||||
}
|
||||
$data = $this->model->where($where)->select()->toArray();
|
||||
|
||||
foreach ($data as &$datum) {
|
||||
if ($datum['rules']) {
|
||||
if ($datum['rules'] == '*') {
|
||||
$datum['rules'] = __('Super administrator');
|
||||
} else {
|
||||
$rules = explode(',', $datum['rules']);
|
||||
if ($rules) {
|
||||
$rulesFirstTitle = AdminRule::where('id', $rules[0])->value('title');
|
||||
$datum['rules'] = count($rules) == 1 ? $rulesFirstTitle : __('%first% etc. %count% items', ['%first%' => $rulesFirstTitle, '%count%' => count($rules)]);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
$datum['rules'] = __('No permission');
|
||||
}
|
||||
}
|
||||
|
||||
return $this->assembleTree ? $this->tree->assembleChild($data) : $data;
|
||||
}
|
||||
|
||||
private function checkAuth($groupId): ?Response
|
||||
{
|
||||
$authGroups = $this->auth->getAllAuthGroups($this->authMethod, []);
|
||||
if (!$this->auth->isSuperAdmin() && !in_array($groupId, $authGroups)) {
|
||||
return $this->error(__($this->authMethod == 'allAuth' ? 'You need to have all permissions of this group to operate this group~' : 'You need to have all the permissions of the group and have additional permissions before you can operate the group~'));
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
303
app/admin/controller/auth/Rule.php
Normal file
303
app/admin/controller/auth/Rule.php
Normal file
@@ -0,0 +1,303 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace app\admin\controller\auth;
|
||||
|
||||
use Throwable;
|
||||
use ba\Tree;
|
||||
use app\common\library\Menu;
|
||||
use app\admin\model\AdminRule;
|
||||
use app\admin\model\AdminGroup;
|
||||
use app\admin\library\crud\Helper;
|
||||
use app\common\controller\Backend;
|
||||
use support\Response;
|
||||
use Webman\Http\Request;
|
||||
|
||||
class Rule extends Backend
|
||||
{
|
||||
protected string|array $preExcludeFields = ['create_time', 'update_time'];
|
||||
|
||||
protected string|array $defaultSortField = ['weigh' => 'desc'];
|
||||
|
||||
protected string|array $quickSearchField = 'title';
|
||||
|
||||
protected ?object $model = null;
|
||||
|
||||
protected Tree $tree;
|
||||
|
||||
protected array $initValue = [];
|
||||
|
||||
protected string $keyword = '';
|
||||
|
||||
protected bool $assembleTree = true;
|
||||
|
||||
protected bool $modelValidate = false;
|
||||
|
||||
protected function initController(Request $request): ?Response
|
||||
{
|
||||
$this->model = new AdminRule();
|
||||
$this->tree = Tree::instance();
|
||||
$isTree = $request->get('isTree') ?? $request->post('isTree') ?? true;
|
||||
$initValue = $request->get('initValue') ?? $request->post('initValue') ?? [];
|
||||
$this->initValue = is_array($initValue) ? array_filter($initValue) : [];
|
||||
$this->keyword = $request->get('quickSearch') ?? $request->post('quickSearch') ?? '';
|
||||
$this->assembleTree = $isTree && !$this->initValue;
|
||||
return null;
|
||||
}
|
||||
|
||||
public function index(Request $request): Response
|
||||
{
|
||||
$response = $this->initializeBackend($request);
|
||||
if ($response !== null) return $response;
|
||||
|
||||
if ($request->get('select') ?? $request->post('select')) {
|
||||
return $this->select($request);
|
||||
}
|
||||
|
||||
return $this->success('', [
|
||||
'list' => $this->getMenus($request),
|
||||
'remark' => get_route_remark(),
|
||||
]);
|
||||
}
|
||||
|
||||
public function add(Request $request): Response
|
||||
{
|
||||
$response = $this->initializeBackend($request);
|
||||
if ($response !== null) return $response;
|
||||
|
||||
if ($request->method() === 'POST') {
|
||||
$data = $request->post();
|
||||
if (!$data) {
|
||||
return $this->error(__('Parameter %s can not be empty', ['']));
|
||||
}
|
||||
|
||||
$data = $this->excludeFields($data);
|
||||
if ($this->dataLimit && $this->dataLimitFieldAutoFill) {
|
||||
$data[$this->dataLimitField] = $this->auth->id;
|
||||
}
|
||||
|
||||
$result = false;
|
||||
$this->model->startTrans();
|
||||
try {
|
||||
if ($this->modelValidate) {
|
||||
$validate = str_replace("\\model\\", "\\validate\\", get_class($this->model));
|
||||
if (class_exists($validate)) {
|
||||
$validate = new $validate();
|
||||
if ($this->modelSceneValidate) {
|
||||
$validate->scene('add');
|
||||
}
|
||||
$validate->check($data);
|
||||
}
|
||||
}
|
||||
$result = $this->model->save($data);
|
||||
|
||||
if (!empty($data['pid'])) {
|
||||
$this->autoAssignPermission($this->model->id, (int) $data['pid']);
|
||||
}
|
||||
|
||||
if (($data['type'] ?? '') == 'menu' && !empty($data['buttons'])) {
|
||||
$newButtons = [];
|
||||
foreach ($data['buttons'] as $button) {
|
||||
foreach (Helper::$menuChildren as $menuChild) {
|
||||
if ($menuChild['name'] == '/' . $button) {
|
||||
$menuChild['name'] = $data['name'] . $menuChild['name'];
|
||||
$newButtons[] = $menuChild;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!empty($newButtons)) {
|
||||
Menu::create($newButtons, $this->model->id, 'ignore');
|
||||
|
||||
$children = AdminRule::where('pid', $this->model->id)->select();
|
||||
foreach ($children as $child) {
|
||||
$this->autoAssignPermission($child['id'], $this->model->id);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$this->model->commit();
|
||||
} catch (Throwable $e) {
|
||||
$this->model->rollback();
|
||||
return $this->error($e->getMessage());
|
||||
}
|
||||
if ($result !== false) {
|
||||
return $this->success(__('Added successfully'));
|
||||
}
|
||||
return $this->error(__('No rows were added'));
|
||||
}
|
||||
|
||||
return $this->error(__('Parameter error'));
|
||||
}
|
||||
|
||||
public function edit(Request $request): Response
|
||||
{
|
||||
$response = $this->initializeBackend($request);
|
||||
if ($response !== null) return $response;
|
||||
|
||||
$id = $request->get($this->model->getPk()) ?? $request->post($this->model->getPk());
|
||||
$row = $this->model->find($id);
|
||||
if (!$row) {
|
||||
return $this->error(__('Record not found'));
|
||||
}
|
||||
|
||||
$dataLimitAdminIds = $this->getDataLimitAdminIds();
|
||||
if ($dataLimitAdminIds && !in_array($row[$this->dataLimitField], $dataLimitAdminIds)) {
|
||||
return $this->error(__('You have no permission'));
|
||||
}
|
||||
|
||||
if ($request->method() === 'POST') {
|
||||
$data = $request->post();
|
||||
if (!$data) {
|
||||
return $this->error(__('Parameter %s can not be empty', ['']));
|
||||
}
|
||||
|
||||
$data = $this->excludeFields($data);
|
||||
$result = false;
|
||||
$this->model->startTrans();
|
||||
try {
|
||||
if ($this->modelValidate) {
|
||||
$validate = str_replace("\\model\\", "\\validate\\", get_class($this->model));
|
||||
if (class_exists($validate)) {
|
||||
$validate = new $validate();
|
||||
if ($this->modelSceneValidate) {
|
||||
$validate->scene('edit');
|
||||
}
|
||||
$validate->check($data);
|
||||
}
|
||||
}
|
||||
if (isset($data['pid']) && $data['pid'] > 0) {
|
||||
$parent = $this->model->where('id', $data['pid'])->find();
|
||||
if ($parent && $parent['pid'] == $row['id']) {
|
||||
$parent->pid = 0;
|
||||
$parent->save();
|
||||
}
|
||||
}
|
||||
$result = $row->save($data);
|
||||
$this->model->commit();
|
||||
} catch (Throwable $e) {
|
||||
$this->model->rollback();
|
||||
return $this->error($e->getMessage());
|
||||
}
|
||||
if ($result !== false) {
|
||||
return $this->success(__('Update successful'));
|
||||
}
|
||||
return $this->error(__('No rows updated'));
|
||||
}
|
||||
|
||||
return $this->success('', [
|
||||
'row' => $row
|
||||
]);
|
||||
}
|
||||
|
||||
public function del(Request $request): Response
|
||||
{
|
||||
$response = $this->initializeBackend($request);
|
||||
if ($response !== null) return $response;
|
||||
|
||||
$ids = $request->get('ids') ?? $request->post('ids') ?? [];
|
||||
$ids = is_array($ids) ? $ids : [];
|
||||
|
||||
$subData = $this->model->where('pid', 'in', $ids)->column('pid', 'id');
|
||||
foreach ($subData as $key => $subDatum) {
|
||||
if (!in_array($key, $ids)) {
|
||||
return $this->error(__('Please delete the child element first, or use batch deletion'));
|
||||
}
|
||||
}
|
||||
|
||||
return $this->delFromTrait($request);
|
||||
}
|
||||
|
||||
public function select(Request $request): Response
|
||||
{
|
||||
$response = $this->initializeBackend($request);
|
||||
if ($response !== null) return $response;
|
||||
|
||||
$data = $this->getMenus($request, [['type', 'in', ['menu_dir', 'menu']], ['status', '=', 1]]);
|
||||
|
||||
if ($this->assembleTree) {
|
||||
$data = $this->tree->assembleTree($this->tree->getTreeArray($data, 'title'));
|
||||
}
|
||||
return $this->success('', [
|
||||
'options' => $data
|
||||
]);
|
||||
}
|
||||
|
||||
protected function getMenus(Request $request, array $where = []): array
|
||||
{
|
||||
$pk = $this->model->getPk();
|
||||
$initKey = $request->get('initKey') ?? $pk;
|
||||
|
||||
$ids = $this->auth->getRuleIds();
|
||||
|
||||
if (!in_array('*', $ids)) {
|
||||
$where[] = ['id', 'in', $ids];
|
||||
}
|
||||
|
||||
if ($this->keyword) {
|
||||
$keyword = explode(' ', $this->keyword);
|
||||
foreach ($keyword as $item) {
|
||||
$where[] = [$this->quickSearchField, 'like', '%' . $item . '%'];
|
||||
}
|
||||
}
|
||||
|
||||
if ($this->initValue) {
|
||||
$where[] = [$initKey, 'in', $this->initValue];
|
||||
}
|
||||
|
||||
$rules = $this->model
|
||||
->where($where)
|
||||
->order($this->queryOrderBuilder())
|
||||
->select()
|
||||
->toArray();
|
||||
|
||||
return $this->assembleTree ? $this->tree->assembleChild($rules) : $rules;
|
||||
}
|
||||
|
||||
private function autoAssignPermission(int $id, int $pid): void
|
||||
{
|
||||
$groups = AdminGroup::where('rules', '<>', '*')->select();
|
||||
foreach ($groups as $group) {
|
||||
/** @var AdminGroup $group */
|
||||
$rules = explode(',', (string) $group->rules);
|
||||
if (in_array($pid, $rules) && !in_array($id, $rules)) {
|
||||
$rules[] = $id;
|
||||
$group->rules = implode(',', $rules);
|
||||
$group->save();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 调用 trait 的 del 逻辑(因 Rule 重写了 del,需手动调用 trait)
|
||||
*/
|
||||
private function delFromTrait(Request $request): Response
|
||||
{
|
||||
$where = [];
|
||||
$dataLimitAdminIds = $this->getDataLimitAdminIds();
|
||||
if ($dataLimitAdminIds) {
|
||||
$where[] = [$this->dataLimitField, 'in', $dataLimitAdminIds];
|
||||
}
|
||||
|
||||
$ids = $request->get('ids') ?? $request->post('ids') ?? [];
|
||||
$ids = is_array($ids) ? $ids : [];
|
||||
$where[] = [$this->model->getPk(), 'in', $ids];
|
||||
$data = $this->model->where($where)->select();
|
||||
|
||||
$count = 0;
|
||||
$this->model->startTrans();
|
||||
try {
|
||||
foreach ($data as $v) {
|
||||
$count += $v->delete();
|
||||
}
|
||||
$this->model->commit();
|
||||
} catch (Throwable $e) {
|
||||
$this->model->rollback();
|
||||
return $this->error($e->getMessage());
|
||||
}
|
||||
if ($count) {
|
||||
return $this->success(__('Deleted successfully'));
|
||||
}
|
||||
return $this->error(__('No rows were deleted'));
|
||||
}
|
||||
}
|
||||
769
app/admin/controller/crud/Crud.php
Normal file
769
app/admin/controller/crud/Crud.php
Normal file
@@ -0,0 +1,769 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace app\admin\controller\crud;
|
||||
|
||||
use Throwable;
|
||||
use ba\Exception as BaException;
|
||||
use ba\Filesystem;
|
||||
use ba\TableManager;
|
||||
use app\admin\model\CrudLog;
|
||||
use app\admin\model\AdminLog;
|
||||
use app\admin\model\AdminRule;
|
||||
use app\common\controller\Backend;
|
||||
use app\common\library\Menu;
|
||||
use app\admin\library\crud\Helper;
|
||||
use Webman\Http\Request;
|
||||
use support\Response;
|
||||
use support\think\Db;
|
||||
|
||||
/**
|
||||
* CRUD 代码生成器(Webman 迁移版)
|
||||
*/
|
||||
class Crud extends Backend
|
||||
{
|
||||
protected array $modelData = [];
|
||||
protected array $controllerData = [];
|
||||
protected array $indexVueData = [];
|
||||
protected array $formVueData = [];
|
||||
protected string $webTranslate = '';
|
||||
protected array $langTsData = [];
|
||||
protected array $dtStringToArray = ['checkbox', 'selects', 'remoteSelects', 'city', 'images', 'files'];
|
||||
protected array $noNeedLogin = ['getFileData'];
|
||||
protected array $noNeedPermission = ['logStart', 'getFileData', 'parseFieldData', 'generateCheck', 'uploadCompleted'];
|
||||
|
||||
protected function initController(Request $request): ?Response
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
public function generate(Request $request): Response
|
||||
{
|
||||
$response = $this->initializeBackend($request);
|
||||
if ($response !== null) return $response;
|
||||
|
||||
$type = $request->post('type', '');
|
||||
$table = $request->post('table', []);
|
||||
$fields = $request->post('fields', []);
|
||||
|
||||
if (!$table || !$fields || !isset($table['name']) || !$table['name']) {
|
||||
return $this->error(__('Parameter error'));
|
||||
}
|
||||
|
||||
$crudLogId = 0;
|
||||
try {
|
||||
$crudLogId = Helper::recordCrudStatus([
|
||||
'table' => $table,
|
||||
'fields' => $fields,
|
||||
'status' => 'start',
|
||||
]);
|
||||
|
||||
$tableName = TableManager::tableName($table['name'], false, $table['databaseConnection'] ?? null);
|
||||
|
||||
if ($type == 'create' || ($table['rebuild'] ?? '') == 'Yes') {
|
||||
TableManager::phinxTable($tableName, [], true, $table['databaseConnection'] ?? null)->drop()->save();
|
||||
}
|
||||
|
||||
[$tablePk] = Helper::handleTableDesign($table, $fields);
|
||||
|
||||
$tableComment = mb_substr($table['comment'] ?? '', -1) == '表'
|
||||
? mb_substr($table['comment'], 0, -1) . '管理'
|
||||
: ($table['comment'] ?? '');
|
||||
|
||||
$modelFile = Helper::parseNameData($table['isCommonModel'] ?? false ? 'common' : 'admin', $tableName, 'model', $table['modelFile'] ?? '');
|
||||
$validateFile = Helper::parseNameData($table['isCommonModel'] ?? false ? 'common' : 'admin', $tableName, 'validate', $table['validateFile'] ?? '');
|
||||
$controllerFile = Helper::parseNameData('admin', $tableName, 'controller', $table['controllerFile'] ?? '');
|
||||
$webViewsDir = Helper::parseWebDirNameData($tableName, 'views', $table['webViewsDir'] ?? '');
|
||||
$webLangDir = Helper::parseWebDirNameData($tableName, 'lang', $table['webViewsDir'] ?? '');
|
||||
|
||||
$this->webTranslate = implode('.', $webLangDir['lang']) . '.';
|
||||
|
||||
$quickSearchField = $table['quickSearchField'] ?? [$tablePk];
|
||||
if (!in_array($tablePk, $quickSearchField)) {
|
||||
$quickSearchField[] = $tablePk;
|
||||
}
|
||||
$quickSearchFieldZhCnTitle = [];
|
||||
|
||||
$this->modelData = [
|
||||
'append' => [], 'methods' => [], 'fieldType' => [], 'createTime' => '', 'updateTime' => '',
|
||||
'beforeInsertMixins' => [], 'beforeInsert' => '', 'afterInsert' => '',
|
||||
'connection' => $table['databaseConnection'] ?? '', 'name' => $tableName,
|
||||
'className' => $modelFile['lastName'], 'namespace' => $modelFile['namespace'],
|
||||
'relationMethodList' => [],
|
||||
];
|
||||
$this->controllerData = [
|
||||
'use' => [], 'attr' => [], 'methods' => [], 'filterRule' => '',
|
||||
'className' => $controllerFile['lastName'], 'namespace' => $controllerFile['namespace'],
|
||||
'tableComment' => $tableComment, 'modelName' => $modelFile['lastName'],
|
||||
'modelNamespace' => $modelFile['namespace'],
|
||||
];
|
||||
$this->indexVueData = [
|
||||
'enableDragSort' => false, 'defaultItems' => [],
|
||||
'tableColumn' => [['type' => 'selection', 'align' => 'center', 'operator' => 'false']],
|
||||
'dblClickNotEditColumn' => ['undefined'], 'optButtons' => ['edit', 'delete'],
|
||||
'defaultOrder' => '',
|
||||
];
|
||||
$this->formVueData = ['bigDialog' => false, 'formFields' => [], 'formValidatorRules' => [], 'imports' => []];
|
||||
$this->langTsData = ['en' => [], 'zh-cn' => []];
|
||||
|
||||
$fieldsMap = [];
|
||||
foreach ($fields as $field) {
|
||||
$fieldsMap[$field['name']] = $field['designType'];
|
||||
Helper::analyseField($field);
|
||||
Helper::getDictData($this->langTsData['en'], $field, 'en');
|
||||
Helper::getDictData($this->langTsData['zh-cn'], $field, 'zh-cn');
|
||||
|
||||
if (in_array($field['name'], $quickSearchField)) {
|
||||
$quickSearchFieldZhCnTitle[] = $this->langTsData['zh-cn'][$field['name']] ?? $field['name'];
|
||||
}
|
||||
if (($field['designType'] ?? '') == 'switch') {
|
||||
$this->indexVueData['dblClickNotEditColumn'][] = $field['name'];
|
||||
}
|
||||
|
||||
$columnDict = $this->getColumnDict($field);
|
||||
if (in_array($field['name'], $table['formFields'] ?? [])) {
|
||||
$this->formVueData['formFields'][] = $this->getFormField($field, $columnDict, $table['databaseConnection'] ?? null);
|
||||
}
|
||||
if (in_array($field['name'], $table['columnFields'] ?? [])) {
|
||||
$this->indexVueData['tableColumn'][] = $this->getTableColumn($field, $columnDict);
|
||||
}
|
||||
if (in_array($field['designType'] ?? '', ['remoteSelect', 'remoteSelects'])) {
|
||||
$this->parseJoinData($field, $table);
|
||||
}
|
||||
$this->parseModelMethods($field, $this->modelData);
|
||||
$this->parseSundryData($field, $table);
|
||||
|
||||
if (!in_array($field['name'], $table['formFields'] ?? [])) {
|
||||
$this->controllerData['attr']['preExcludeFields'][] = $field['name'];
|
||||
}
|
||||
}
|
||||
|
||||
$this->langTsData['en']['quick Search Fields'] = implode(',', $quickSearchField);
|
||||
$this->langTsData['zh-cn']['quick Search Fields'] = implode('、', $quickSearchFieldZhCnTitle);
|
||||
$this->controllerData['attr']['quickSearchField'] = $quickSearchField;
|
||||
|
||||
$weighKey = array_search('weigh', $fieldsMap);
|
||||
if ($weighKey !== false) {
|
||||
$this->indexVueData['enableDragSort'] = true;
|
||||
$this->modelData['afterInsert'] = Helper::assembleStub('mixins/model/afterInsert', ['field' => $weighKey]);
|
||||
}
|
||||
|
||||
$this->indexVueData['tableColumn'][] = [
|
||||
'label' => "t('Operate')", 'align' => 'center',
|
||||
'width' => $this->indexVueData['enableDragSort'] ? 140 : 100,
|
||||
'render' => 'buttons', 'buttons' => 'optButtons', 'operator' => 'false',
|
||||
];
|
||||
if ($this->indexVueData['enableDragSort']) {
|
||||
array_unshift($this->indexVueData['optButtons'], 'weigh-sort');
|
||||
}
|
||||
|
||||
Helper::writeWebLangFile($this->langTsData, $webLangDir);
|
||||
Helper::writeModelFile($tablePk, $fieldsMap, $this->modelData, $modelFile);
|
||||
Helper::writeControllerFile($this->controllerData, $controllerFile);
|
||||
|
||||
$validateContent = Helper::assembleStub('mixins/validate/validate', [
|
||||
'namespace' => $validateFile['namespace'],
|
||||
'className' => $validateFile['lastName'],
|
||||
]);
|
||||
Helper::writeFile($validateFile['parseFile'], $validateContent);
|
||||
|
||||
$this->indexVueData['tablePk'] = $tablePk;
|
||||
$this->indexVueData['webTranslate'] = $this->webTranslate;
|
||||
Helper::writeIndexFile($this->indexVueData, $webViewsDir, $controllerFile);
|
||||
Helper::writeFormFile($this->formVueData, $webViewsDir, $fields, $this->webTranslate);
|
||||
|
||||
Helper::createMenu($webViewsDir, $tableComment);
|
||||
|
||||
Helper::recordCrudStatus(['id' => $crudLogId, 'status' => 'success']);
|
||||
} catch (BaException $e) {
|
||||
Helper::recordCrudStatus(['id' => $crudLogId ?: 0, 'status' => 'error']);
|
||||
return $this->error($e->getMessage());
|
||||
} catch (Throwable $e) {
|
||||
Helper::recordCrudStatus(['id' => $crudLogId ?: 0, 'status' => 'error']);
|
||||
if (env('app_debug', false)) throw $e;
|
||||
return $this->error($e->getMessage());
|
||||
}
|
||||
|
||||
return $this->success('', ['crudLog' => CrudLog::find($crudLogId)]);
|
||||
}
|
||||
|
||||
public function logStart(Request $request): Response
|
||||
{
|
||||
$response = $this->initializeBackend($request);
|
||||
if ($response !== null) return $response;
|
||||
|
||||
$id = $request->post('id');
|
||||
$type = $request->post('type', '');
|
||||
|
||||
if ($type == 'Cloud history') {
|
||||
try {
|
||||
$client = get_ba_client();
|
||||
$response = $client->request('GET', '/api/v6.Crud/info', [
|
||||
'query' => [
|
||||
'id' => $id,
|
||||
'server' => 1,
|
||||
'ba-user-token' => $request->post('token', ''),
|
||||
]
|
||||
]);
|
||||
$content = $response->getBody()->getContents();
|
||||
$statusCode = $response->getStatusCode();
|
||||
if ($content == '' || stripos($content, '<title>系统发生错误</title>') !== false || $statusCode != 200) {
|
||||
return $this->error(__('Failed to load cloud data'));
|
||||
}
|
||||
$json = json_decode($content, true);
|
||||
if (json_last_error() != JSON_ERROR_NONE || !is_array($json)) {
|
||||
return $this->error(__('Failed to load cloud data'));
|
||||
}
|
||||
if (($json['code'] ?? 0) != 1) {
|
||||
return $this->error($json['msg'] ?? __('Failed to load cloud data'));
|
||||
}
|
||||
$info = $json['data']['info'] ?? null;
|
||||
} catch (Throwable $e) {
|
||||
return $this->error(__('Failed to load cloud data'));
|
||||
}
|
||||
} else {
|
||||
$row = CrudLog::find($id);
|
||||
$info = $row ? $row->toArray() : null;
|
||||
}
|
||||
|
||||
if (!$info) {
|
||||
return $this->error(__('Record not found'));
|
||||
}
|
||||
|
||||
$connection = TableManager::getConnection($info['table']['databaseConnection'] ?? '');
|
||||
$tableName = TableManager::tableName($info['table']['name'], false, $connection);
|
||||
$adapter = TableManager::phinxAdapter(true, $connection);
|
||||
if ($adapter->hasTable($tableName)) {
|
||||
$info['table']['empty'] = Db::connect($connection)->name($tableName)->limit(1)->select()->isEmpty();
|
||||
} else {
|
||||
$info['table']['empty'] = true;
|
||||
}
|
||||
|
||||
AdminLog::instance($request)->setTitle(__('Log start'));
|
||||
|
||||
return $this->success('', [
|
||||
'table' => $info['table'],
|
||||
'fields' => $info['fields'],
|
||||
'sync' => $info['sync'] ?? 0,
|
||||
]);
|
||||
}
|
||||
|
||||
public function delete(Request $request): Response
|
||||
{
|
||||
$response = $this->initializeBackend($request);
|
||||
if ($response !== null) return $response;
|
||||
|
||||
$id = $request->post('id');
|
||||
$row = CrudLog::find($id);
|
||||
if (!$row) {
|
||||
return $this->error(__('Record not found'));
|
||||
}
|
||||
$info = $row->toArray();
|
||||
|
||||
$webLangDir = Helper::parseWebDirNameData($info['table']['name'], 'lang', $info['table']['webViewsDir'] ?? '');
|
||||
$files = [
|
||||
$webLangDir['en'] . '.ts',
|
||||
$webLangDir['zh-cn'] . '.ts',
|
||||
($info['table']['webViewsDir'] ?? '') . '/index.vue',
|
||||
($info['table']['webViewsDir'] ?? '') . '/popupForm.vue',
|
||||
$info['table']['controllerFile'] ?? '',
|
||||
$info['table']['modelFile'] ?? '',
|
||||
$info['table']['validateFile'] ?? '',
|
||||
];
|
||||
try {
|
||||
foreach ($files as $file) {
|
||||
$file = Filesystem::fsFit(root_path() . $file);
|
||||
if (file_exists($file)) {
|
||||
unlink($file);
|
||||
}
|
||||
Filesystem::delEmptyDir(dirname($file));
|
||||
}
|
||||
Menu::delete(Helper::getMenuName($webLangDir), true);
|
||||
Helper::recordCrudStatus(['id' => $id, 'status' => 'delete']);
|
||||
} catch (Throwable $e) {
|
||||
return $this->error($e->getMessage());
|
||||
}
|
||||
return $this->success(__('Deleted successfully'));
|
||||
}
|
||||
|
||||
public function getFileData(Request $request): Response
|
||||
{
|
||||
try {
|
||||
$response = $this->initializeBackend($request);
|
||||
if ($response !== null) return $response;
|
||||
|
||||
$table = $request->get('table');
|
||||
$commonModel = $request->get('commonModel', false);
|
||||
|
||||
if (!$table) {
|
||||
return $this->error(__('Parameter error'));
|
||||
}
|
||||
|
||||
$modelFile = Helper::parseNameData($commonModel ? 'common' : 'admin', $table, 'model');
|
||||
$validateFile = Helper::parseNameData($commonModel ? 'common' : 'admin', $table, 'validate');
|
||||
$controllerFile = Helper::parseNameData('admin', $table, 'controller');
|
||||
$webViewsDir = Helper::parseWebDirNameData($table, 'views');
|
||||
|
||||
$adminModelDir = root_path() . 'app' . DIRECTORY_SEPARATOR . 'admin' . DIRECTORY_SEPARATOR . 'model' . DIRECTORY_SEPARATOR;
|
||||
$commonModelDir = root_path() . 'app' . DIRECTORY_SEPARATOR . 'common' . DIRECTORY_SEPARATOR . 'model' . DIRECTORY_SEPARATOR;
|
||||
$adminModelFiles = is_dir($adminModelDir) ? Filesystem::getDirFiles($adminModelDir) : [];
|
||||
$commonModelFiles = is_dir($commonModelDir) ? Filesystem::getDirFiles($commonModelDir) : [];
|
||||
$adminControllerFiles = get_controller_list();
|
||||
|
||||
$modelFileList = [];
|
||||
$controllerFiles = [];
|
||||
foreach ($adminModelFiles as $item) {
|
||||
$item = Filesystem::fsFit('app/admin/model/' . $item);
|
||||
$modelFileList[$item] = $item;
|
||||
}
|
||||
foreach ($commonModelFiles as $item) {
|
||||
$item = Filesystem::fsFit('app/common/model/' . $item);
|
||||
$modelFileList[$item] = $item;
|
||||
}
|
||||
|
||||
$outExcludeController = ['Addon.php', 'Ajax.php', 'Dashboard.php', 'Index.php', 'Module.php', 'Terminal.php', 'routine/AdminInfo.php', 'routine/Config.php'];
|
||||
foreach ($adminControllerFiles as $item) {
|
||||
if (!in_array($item, $outExcludeController)) {
|
||||
$item = Filesystem::fsFit('app/admin/controller/' . $item);
|
||||
$controllerFiles[$item] = $item;
|
||||
}
|
||||
}
|
||||
|
||||
// 路径统一使用正斜杠,便于 UI 展示及跨平台一致(相对于 dafuweng-webman 项目根)
|
||||
$pathFit = fn(string $p): string => str_replace('\\', '/', $p);
|
||||
|
||||
return $this->success('', [
|
||||
'modelFile' => $pathFit($modelFile['rootFileName']),
|
||||
'controllerFile' => $pathFit($controllerFile['rootFileName']),
|
||||
'validateFile' => $pathFit($validateFile['rootFileName']),
|
||||
'controllerFileList' => array_map($pathFit, $controllerFiles),
|
||||
'modelFileList' => array_map($pathFit, $modelFileList),
|
||||
'webViewsDir' => $pathFit($webViewsDir['views']),
|
||||
]);
|
||||
} catch (Throwable $e) {
|
||||
return $this->error($e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
public function checkCrudLog(Request $request): Response
|
||||
{
|
||||
$response = $this->initializeBackend($request);
|
||||
if ($response !== null) return $response;
|
||||
|
||||
$table = $request->get('table');
|
||||
$connection = $request->get('connection', config('thinkorm.default', config('database.default', 'mysql')));
|
||||
|
||||
if (!$table) {
|
||||
return $this->error(__('Parameter error'));
|
||||
}
|
||||
|
||||
$crudLog = Db::name('crud_log')
|
||||
->where('table_name', $table)
|
||||
->where('connection', $connection)
|
||||
->order('create_time desc')
|
||||
->find();
|
||||
|
||||
$id = ($crudLog && isset($crudLog['status']) && $crudLog['status'] == 'success') ? $crudLog['id'] : 0;
|
||||
|
||||
return $this->success('', ['id' => $id]);
|
||||
}
|
||||
|
||||
public function parseFieldData(Request $request): Response
|
||||
{
|
||||
$response = $this->initializeBackend($request);
|
||||
if ($response !== null) return $response;
|
||||
|
||||
AdminLog::instance($request)->setTitle(__('Parse field data'));
|
||||
$type = $request->post('type');
|
||||
$table = $request->post('table');
|
||||
$connection = TableManager::getConnection($request->post('connection', ''));
|
||||
$table = TableManager::tableName($table, true, $connection);
|
||||
$connectionConfig = TableManager::getConnectionConfig($connection);
|
||||
|
||||
if ($type == 'db') {
|
||||
$sql = 'SELECT * FROM `information_schema`.`tables` WHERE TABLE_SCHEMA = ? AND table_name = ?';
|
||||
$tableInfo = Db::connect($connection)->query($sql, [$connectionConfig['database'], $table]);
|
||||
if (!$tableInfo) {
|
||||
return $this->error(__('Record not found'));
|
||||
}
|
||||
$adapter = TableManager::phinxAdapter(false, $connection);
|
||||
$empty = $adapter->hasTable($table)
|
||||
? Db::connect($connection)->table($table)->limit(1)->select()->isEmpty()
|
||||
: true;
|
||||
|
||||
return $this->success('', [
|
||||
'columns' => Helper::parseTableColumns($table, false, $connection),
|
||||
'comment' => $tableInfo[0]['TABLE_COMMENT'] ?? '',
|
||||
'empty' => $empty,
|
||||
]);
|
||||
}
|
||||
return $this->error(__('Parameter error'));
|
||||
}
|
||||
|
||||
public function generateCheck(Request $request): Response
|
||||
{
|
||||
$response = $this->initializeBackend($request);
|
||||
if ($response !== null) return $response;
|
||||
|
||||
AdminLog::instance($request)->setTitle(__('Generate check'));
|
||||
$table = $request->post('table');
|
||||
$connection = $request->post('connection', '');
|
||||
$webViewsDir = $request->post('webViewsDir', '');
|
||||
$controllerFile = $request->post('controllerFile', '');
|
||||
|
||||
if (!$table) {
|
||||
return $this->error(__('Parameter error'));
|
||||
}
|
||||
|
||||
try {
|
||||
$webViewsDir = Helper::parseWebDirNameData($table, 'views', $webViewsDir);
|
||||
$controllerFile = Helper::parseNameData('admin', $table, 'controller', $controllerFile)['rootFileName'];
|
||||
} catch (Throwable $e) {
|
||||
return $this->error($e->getMessage());
|
||||
}
|
||||
|
||||
$tableList = TableManager::getTableList($connection);
|
||||
$tableExist = array_key_exists(TableManager::tableName($table, true, $connection), $tableList);
|
||||
$controllerExist = file_exists(root_path() . $controllerFile);
|
||||
$menuName = Helper::getMenuName($webViewsDir);
|
||||
$menuExist = AdminRule::where('name', $menuName)->value('id');
|
||||
|
||||
if ($controllerExist || $tableExist || $menuExist) {
|
||||
return $this->error('', [
|
||||
'menu' => $menuExist,
|
||||
'table' => $tableExist,
|
||||
'controller' => $controllerExist,
|
||||
], -1);
|
||||
}
|
||||
return $this->success();
|
||||
}
|
||||
|
||||
public function uploadCompleted(Request $request): Response
|
||||
{
|
||||
$response = $this->initializeBackend($request);
|
||||
if ($response !== null) return $response;
|
||||
|
||||
$syncIds = $request->post('syncIds', []);
|
||||
$syncIds = is_array($syncIds) ? $syncIds : [];
|
||||
$cancelSync = $request->post('cancelSync', false);
|
||||
|
||||
$crudLogModel = new CrudLog();
|
||||
|
||||
if ($cancelSync) {
|
||||
$logData = $crudLogModel->where('id', 'in', array_keys($syncIds))->select();
|
||||
foreach ($logData as $logDatum) {
|
||||
if (isset($syncIds[$logDatum->id]) && $logDatum->sync == $syncIds[$logDatum->id]) {
|
||||
$logDatum->sync = 0;
|
||||
$logDatum->save();
|
||||
}
|
||||
}
|
||||
return $this->success();
|
||||
}
|
||||
|
||||
foreach ($syncIds as $key => $syncId) {
|
||||
$row = $crudLogModel->find($key);
|
||||
if ($row) {
|
||||
$row->sync = $syncId;
|
||||
$row->save();
|
||||
}
|
||||
}
|
||||
|
||||
return $this->success();
|
||||
}
|
||||
|
||||
private function parseJoinData($field, $table): void
|
||||
{
|
||||
$dictEn = [];
|
||||
$dictZhCn = [];
|
||||
if (empty($field['form']['relation-fields']) || empty($field['form']['remote-table'])) {
|
||||
return;
|
||||
}
|
||||
$columns = Helper::parseTableColumns($field['form']['remote-table'], true, $table['databaseConnection'] ?? null);
|
||||
$relationFields = explode(',', $field['form']['relation-fields']);
|
||||
$tableName = TableManager::tableName($field['form']['remote-table'], false, $table['databaseConnection'] ?? null);
|
||||
$rnPattern = '/(.*)(_ids|_id)$/';
|
||||
$relationName = preg_match($rnPattern, $field['name'])
|
||||
? parse_name(preg_replace($rnPattern, '$1', $field['name']), 1)
|
||||
: parse_name($field['name'] . '_table', 1);
|
||||
|
||||
if (empty($field['form']['remote-model']) || !file_exists(root_path() . $field['form']['remote-model'])) {
|
||||
$joinModelFile = Helper::parseNameData('admin', $tableName, 'model', $field['form']['remote-model'] ?? '');
|
||||
if (!file_exists(root_path() . $joinModelFile['rootFileName'])) {
|
||||
$joinModelData = [
|
||||
'append' => [], 'methods' => [], 'fieldType' => [], 'createTime' => '', 'updateTime' => '',
|
||||
'beforeInsertMixins' => [], 'beforeInsert' => '', 'afterInsert' => '',
|
||||
'connection' => $table['databaseConnection'] ?? '', 'name' => $tableName,
|
||||
'className' => $joinModelFile['lastName'], 'namespace' => $joinModelFile['namespace'],
|
||||
];
|
||||
$joinTablePk = 'id';
|
||||
$joinFieldsMap = [];
|
||||
foreach ($columns as $column) {
|
||||
$joinFieldsMap[$column['name']] = $column['designType'];
|
||||
$this->parseModelMethods($column, $joinModelData);
|
||||
if ($column['primaryKey']) $joinTablePk = $column['name'];
|
||||
}
|
||||
$weighKey = array_search('weigh', $joinFieldsMap);
|
||||
if ($weighKey !== false) {
|
||||
$joinModelData['afterInsert'] = Helper::assembleStub('mixins/model/afterInsert', ['field' => $weighKey]);
|
||||
}
|
||||
Helper::writeModelFile($joinTablePk, $joinFieldsMap, $joinModelData, $joinModelFile);
|
||||
}
|
||||
$field['form']['remote-model'] = $joinModelFile['rootFileName'];
|
||||
}
|
||||
|
||||
if ($field['designType'] == 'remoteSelect') {
|
||||
$this->controllerData['attr']['withJoinTable'][$relationName] = $relationName;
|
||||
$relationData = [
|
||||
'relationMethod' => $relationName, 'relationMode' => 'belongsTo',
|
||||
'relationPrimaryKey' => $field['form']['remote-pk'] ?? 'id',
|
||||
'relationForeignKey' => $field['name'],
|
||||
'relationClassName' => str_replace(['.php', '/'], ['', '\\'], '\\' . $field['form']['remote-model']) . "::class",
|
||||
];
|
||||
$this->modelData['relationMethodList'][$relationName] = Helper::assembleStub('mixins/model/belongsTo', $relationData);
|
||||
if ($relationFields) {
|
||||
$this->controllerData['relationVisibleFieldList'][$relationData['relationMethod']] = $relationFields;
|
||||
}
|
||||
} elseif ($field['designType'] == 'remoteSelects') {
|
||||
$this->modelData['append'][] = $relationName;
|
||||
$this->modelData['methods'][] = Helper::assembleStub('mixins/model/getters/remoteSelectLabels', [
|
||||
'field' => parse_name($relationName, 1),
|
||||
'className' => str_replace(['.php', '/'], ['', '\\'], '\\' . $field['form']['remote-model']),
|
||||
'primaryKey' => $field['form']['remote-pk'] ?? 'id',
|
||||
'foreignKey' => $field['name'],
|
||||
'labelFieldName' => $field['form']['remote-field'] ?? 'name',
|
||||
]);
|
||||
}
|
||||
|
||||
foreach ($relationFields as $relationField) {
|
||||
if (!array_key_exists($relationField, $columns)) continue;
|
||||
$relationFieldPrefix = $relationName . '.';
|
||||
$relationFieldLangPrefix = strtolower($relationName) . '__';
|
||||
Helper::getDictData($dictEn, $columns[$relationField], 'en', $relationFieldLangPrefix);
|
||||
Helper::getDictData($dictZhCn, $columns[$relationField], 'zh-cn', $relationFieldLangPrefix);
|
||||
|
||||
if (($columns[$relationField]['designType'] ?? '') == 'switch') {
|
||||
$this->indexVueData['dblClickNotEditColumn'][] = $field['name'];
|
||||
}
|
||||
|
||||
$columnDict = $this->getColumnDict($columns[$relationField], $relationFieldLangPrefix);
|
||||
$columns[$relationField]['designType'] = $field['designType'];
|
||||
$columns[$relationField]['form'] = ($field['form'] ?? []) + ($columns[$relationField]['form'] ?? []);
|
||||
$columns[$relationField]['table'] = ($field['table'] ?? []) + ($columns[$relationField]['table'] ?? []);
|
||||
|
||||
$remoteAttr = [
|
||||
'pk' => $this->getRemoteSelectPk($field),
|
||||
'field' => $field['form']['remote-field'] ?? 'name',
|
||||
'remoteUrl' => $this->getRemoteSelectUrl($field),
|
||||
];
|
||||
|
||||
if (($columns[$relationField]['table']['comSearchRender'] ?? '') == 'remoteSelect') {
|
||||
$renderColumn = $columns[$relationField];
|
||||
$renderColumn['table']['operator'] = 'false';
|
||||
unset($renderColumn['table']['comSearchRender']);
|
||||
$this->indexVueData['tableColumn'][] = $this->getTableColumn($renderColumn, $columnDict, $relationFieldPrefix, $relationFieldLangPrefix);
|
||||
$columns[$relationField]['table']['show'] = 'false';
|
||||
$columns[$relationField]['table']['label'] = "t('" . $this->webTranslate . $relationFieldLangPrefix . $columns[$relationField]['name'] . "')";
|
||||
$columns[$relationField]['name'] = $field['name'];
|
||||
if ($field['designType'] == 'remoteSelects') {
|
||||
$remoteAttr['multiple'] = 'true';
|
||||
}
|
||||
$columnData = $this->getTableColumn($columns[$relationField], $columnDict, '', $relationFieldLangPrefix);
|
||||
$columnData['comSearchInputAttr'] = array_merge($remoteAttr, $columnData['comSearchInputAttr'] ?? []);
|
||||
} else {
|
||||
$columnData = $this->getTableColumn($columns[$relationField], $columnDict, $relationFieldPrefix, $relationFieldLangPrefix);
|
||||
}
|
||||
$this->indexVueData['tableColumn'][] = $columnData;
|
||||
}
|
||||
$this->langTsData['en'] = array_merge($this->langTsData['en'], $dictEn);
|
||||
$this->langTsData['zh-cn'] = array_merge($this->langTsData['zh-cn'], $dictZhCn);
|
||||
}
|
||||
|
||||
private function parseModelMethods($field, &$modelData): void
|
||||
{
|
||||
if (($field['designType'] ?? '') == 'array') {
|
||||
$modelData['fieldType'][$field['name']] = 'json';
|
||||
} elseif (!in_array($field['name'], ['create_time', 'update_time', 'updatetime', 'createtime'])
|
||||
&& ($field['designType'] ?? '') == 'datetime'
|
||||
&& in_array($field['type'] ?? '', ['int', 'bigint'])) {
|
||||
$modelData['fieldType'][$field['name']] = 'timestamp:Y-m-d H:i:s';
|
||||
}
|
||||
if (($field['designType'] ?? '') == 'spk') {
|
||||
$modelData['beforeInsertMixins']['snowflake'] = Helper::assembleStub('mixins/model/mixins/beforeInsertWithSnowflake', []);
|
||||
}
|
||||
$fieldName = parse_name($field['name'], 1);
|
||||
if (in_array($field['designType'] ?? '', $this->dtStringToArray)) {
|
||||
$modelData['methods'][] = Helper::assembleStub('mixins/model/getters/stringToArray', ['field' => $fieldName]);
|
||||
$modelData['methods'][] = Helper::assembleStub('mixins/model/setters/arrayToString', ['field' => $fieldName]);
|
||||
} elseif (($field['designType'] ?? '') == 'array') {
|
||||
$modelData['methods'][] = Helper::assembleStub('mixins/model/getters/jsonDecode', ['field' => $fieldName]);
|
||||
} elseif (($field['designType'] ?? '') == 'time') {
|
||||
$modelData['methods'][] = Helper::assembleStub('mixins/model/setters/time', ['field' => $fieldName]);
|
||||
} elseif (($field['designType'] ?? '') == 'editor') {
|
||||
$modelData['methods'][] = Helper::assembleStub('mixins/model/getters/htmlDecode', ['field' => $fieldName]);
|
||||
} elseif (($field['designType'] ?? '') == 'spk') {
|
||||
$modelData['methods'][] = Helper::assembleStub('mixins/model/getters/string', ['field' => $fieldName]);
|
||||
} elseif (in_array($field['type'] ?? '', ['float', 'decimal', 'double'])) {
|
||||
$modelData['methods'][] = Helper::assembleStub('mixins/model/getters/float', ['field' => $fieldName]);
|
||||
}
|
||||
if (($field['designType'] ?? '') == 'city') {
|
||||
$modelData['append'][] = $field['name'] . '_text';
|
||||
$modelData['methods'][] = Helper::assembleStub('mixins/model/getters/cityNames', [
|
||||
'field' => $fieldName . 'Text',
|
||||
'originalFieldName' => $field['name'],
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
private function parseSundryData($field, $table): void
|
||||
{
|
||||
if (($field['designType'] ?? '') == 'editor') {
|
||||
$this->formVueData['bigDialog'] = true;
|
||||
// Webman Request 无 filter 方法,使用 inputFilter 由 Backend trait 在 add/edit 时应用
|
||||
$this->controllerData['filterRule'] = "\n" . Helper::tab(2) . '$this->inputFilter = \'clean_xss\';';
|
||||
}
|
||||
if (!empty($table['defaultSortField']) && !empty($table['defaultSortType'])) {
|
||||
$defaultSortField = "{$table['defaultSortField']},{$table['defaultSortType']}";
|
||||
if ($defaultSortField == 'id,desc') {
|
||||
$this->controllerData['attr']['defaultSortField'] = '';
|
||||
} else {
|
||||
$this->controllerData['attr']['defaultSortField'] = $defaultSortField;
|
||||
$this->indexVueData['defaultOrder'] = Helper::buildDefaultOrder($table['defaultSortField'], $table['defaultSortType']);
|
||||
}
|
||||
}
|
||||
if (($field['originalDesignType'] ?? '') == 'weigh' && $field['name'] != 'weigh') {
|
||||
$this->controllerData['attr']['weighField'] = $field['name'];
|
||||
}
|
||||
}
|
||||
|
||||
private function getFormField($field, $columnDict, ?string $dbConnection = null): array
|
||||
{
|
||||
$formField = [
|
||||
':label' => 't(\'' . $this->webTranslate . $field['name'] . '\')',
|
||||
'type' => $field['designType'],
|
||||
'v-model' => 'baTable.form.items!.' . $field['name'],
|
||||
'prop' => $field['name'],
|
||||
];
|
||||
if ($columnDict || in_array($field['designType'], ['radio', 'checkbox', 'select', 'selects'])) {
|
||||
$formField[':input-attr']['content'] = $columnDict;
|
||||
} elseif ($field['designType'] == 'textarea') {
|
||||
$formField[':input-attr']['rows'] = (int)($field['form']['rows'] ?? 3);
|
||||
$formField['@keyup.enter.stop'] = '';
|
||||
$formField['@keyup.ctrl.enter'] = 'baTable.onSubmit(formRef)';
|
||||
} elseif (in_array($field['designType'], ['remoteSelect', 'remoteSelects'])) {
|
||||
$formField[':input-attr']['pk'] = $this->getRemoteSelectPk($field);
|
||||
$formField[':input-attr']['field'] = $field['form']['remote-field'] ?? 'name';
|
||||
$formField[':input-attr']['remoteUrl'] = $this->getRemoteSelectUrl($field);
|
||||
} elseif ($field['designType'] == 'number') {
|
||||
$formField[':input-attr']['step'] = (int)($field['form']['step'] ?? 1);
|
||||
} elseif ($field['designType'] == 'icon') {
|
||||
$formField[':input-attr']['placement'] = 'top';
|
||||
} elseif ($field['designType'] == 'editor') {
|
||||
$formField['@keyup.enter.stop'] = '';
|
||||
$formField['@keyup.ctrl.enter'] = 'baTable.onSubmit(formRef)';
|
||||
}
|
||||
if (!in_array($field['designType'], ['image', 'images', 'file', 'files', 'switch'])) {
|
||||
if (in_array($field['designType'], ['radio', 'checkbox', 'datetime', 'year', 'date', 'time', 'select', 'selects', 'remoteSelect', 'remoteSelects', 'city', 'icon'])) {
|
||||
$formField[':placeholder'] = "t('Please select field', { field: t('" . $this->webTranslate . $field['name'] . "') })";
|
||||
} else {
|
||||
$formField[':placeholder'] = "t('Please input field', { field: t('" . $this->webTranslate . $field['name'] . "') })";
|
||||
}
|
||||
}
|
||||
if (($field['defaultType'] ?? '') == 'INPUT') {
|
||||
$this->indexVueData['defaultItems'][$field['name']] = $field['default'];
|
||||
}
|
||||
if ($field['designType'] == 'editor') {
|
||||
$this->indexVueData['defaultItems'][$field['name']] = (($field['defaultType'] ?? '') == 'INPUT' && ($field['default'] ?? '')) ? $field['default'] : '';
|
||||
} elseif ($field['designType'] == 'array') {
|
||||
$this->indexVueData['defaultItems'][$field['name']] = "[]";
|
||||
} elseif (($field['defaultType'] ?? '') == 'INPUT' && in_array($field['designType'], $this->dtStringToArray) && str_contains($field['default'] ?? '', ',')) {
|
||||
$this->indexVueData['defaultItems'][$field['name']] = Helper::buildSimpleArray(explode(',', $field['default']));
|
||||
} elseif (($field['defaultType'] ?? '') == 'INPUT' && in_array($field['designType'], ['number', 'float'])) {
|
||||
$this->indexVueData['defaultItems'][$field['name']] = (float)($field['default'] ?? 0);
|
||||
}
|
||||
if (isset($field['default']) && in_array($field['designType'], ['switch', 'number', 'float', 'remoteSelect']) && $field['default'] == 0) {
|
||||
unset($this->indexVueData['defaultItems'][$field['name']]);
|
||||
}
|
||||
return $formField;
|
||||
}
|
||||
|
||||
private function getRemoteSelectPk($field): string
|
||||
{
|
||||
$pk = $field['form']['remote-pk'] ?? 'id';
|
||||
$alias = '';
|
||||
if (!str_contains($pk, '.')) {
|
||||
if (($field['form']['remote-source-config-type'] ?? '') == 'crud' && !empty($field['form']['remote-model'])) {
|
||||
$alias = parse_name(basename(str_replace('\\', '/', $field['form']['remote-model']), '.php'));
|
||||
} else {
|
||||
$alias = $field['form']['remote-primary-table-alias'] ?? '';
|
||||
}
|
||||
}
|
||||
return !empty($alias) ? "$alias.$pk" : $pk;
|
||||
}
|
||||
|
||||
private function getRemoteSelectUrl($field): string
|
||||
{
|
||||
if (($field['form']['remote-source-config-type'] ?? '') == 'crud' && !empty($field['form']['remote-controller'])) {
|
||||
$pathArr = [];
|
||||
$controller = explode(DIRECTORY_SEPARATOR, $field['form']['remote-controller']);
|
||||
$controller = str_replace('.php', '', $controller);
|
||||
$redundantDir = ['app' => 0, 'admin' => 1, 'controller' => 2];
|
||||
foreach ($controller as $key => $item) {
|
||||
if (!array_key_exists($item, $redundantDir) || $key !== $redundantDir[$item]) {
|
||||
$pathArr[] = $item;
|
||||
}
|
||||
}
|
||||
$url = count($pathArr) > 1 ? implode('.', $pathArr) : ($pathArr[0] ?? '');
|
||||
return '/admin/' . $url . '/index';
|
||||
}
|
||||
return $field['form']['remote-url'] ?? '';
|
||||
}
|
||||
|
||||
private function getTableColumn($field, $columnDict, string $fieldNamePrefix = '', string $translationPrefix = ''): array
|
||||
{
|
||||
$column = [
|
||||
'label' => "t('" . $this->webTranslate . $translationPrefix . $field['name'] . "')",
|
||||
'prop' => $fieldNamePrefix . $field['name'] . (($field['designType'] ?? '') == 'city' ? '_text' : ''),
|
||||
'align' => 'center',
|
||||
];
|
||||
if (isset($field['table']['operator']) && $field['table']['operator'] == 'LIKE') {
|
||||
$column['operatorPlaceholder'] = "t('Fuzzy query')";
|
||||
}
|
||||
if (!empty($field['table'])) {
|
||||
$column = array_merge($column, $field['table']);
|
||||
$column['comSearchInputAttr'] = str_attr_to_array($column['comSearchInputAttr'] ?? '');
|
||||
}
|
||||
$columnReplaceValue = ['tag', 'tags', 'switch'];
|
||||
if (!in_array($field['designType'] ?? '', ['remoteSelect', 'remoteSelects'])
|
||||
&& ($columnDict || (isset($field['table']['render']) && in_array($field['table']['render'], $columnReplaceValue)))) {
|
||||
$column['replaceValue'] = $columnDict;
|
||||
}
|
||||
if (isset($column['render']) && $column['render'] == 'none') {
|
||||
unset($column['render']);
|
||||
}
|
||||
return $column;
|
||||
}
|
||||
|
||||
private function getColumnDict($column, string $translationPrefix = ''): array
|
||||
{
|
||||
$dict = [];
|
||||
if (in_array($column['type'] ?? '', ['enum', 'set'])) {
|
||||
$dataType = str_replace(' ', '', $column['dataType'] ?? '');
|
||||
$columnData = substr($dataType, stripos($dataType, '(') + 1, -1);
|
||||
$columnData = explode(',', str_replace(["'", '"'], '', $columnData));
|
||||
foreach ($columnData as $columnDatum) {
|
||||
$dict[$columnDatum] = $column['name'] . ' ' . $columnDatum;
|
||||
}
|
||||
}
|
||||
$dictData = [];
|
||||
Helper::getDictData($dictData, $column, 'zh-cn', $translationPrefix);
|
||||
if ($dictData) {
|
||||
unset($dictData[$translationPrefix . $column['name']]);
|
||||
foreach ($dictData as $key => $item) {
|
||||
$keyName = str_replace($translationPrefix . $column['name'] . ' ', '', $key);
|
||||
$dict[$keyName] = "t('" . $this->webTranslate . $key . "')";
|
||||
}
|
||||
}
|
||||
return $dict;
|
||||
}
|
||||
}
|
||||
53
app/admin/controller/crud/Log.php
Normal file
53
app/admin/controller/crud/Log.php
Normal file
@@ -0,0 +1,53 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace app\admin\controller\crud;
|
||||
|
||||
use app\admin\model\CrudLog;
|
||||
use app\common\controller\Backend;
|
||||
use Webman\Http\Request;
|
||||
use support\Response;
|
||||
|
||||
class Log extends Backend
|
||||
{
|
||||
protected ?object $model = null;
|
||||
|
||||
protected array|string $preExcludeFields = ['id', 'create_time'];
|
||||
protected array|string $quickSearchField = ['id', 'table_name', 'comment'];
|
||||
protected array $noNeedPermission = ['index'];
|
||||
|
||||
protected function initController(Request $request): ?Response
|
||||
{
|
||||
$this->model = new CrudLog();
|
||||
|
||||
if (!$this->auth->check('crud/crud/index')) {
|
||||
return $this->error(__('You have no permission'), [], 401);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public function index(Request $request): Response
|
||||
{
|
||||
$response = $this->initializeBackend($request);
|
||||
if ($response !== null) return $response;
|
||||
|
||||
if ($request->get('select') || $request->post('select')) {
|
||||
return $this->select($request);
|
||||
}
|
||||
|
||||
list($where, $alias, $limit, $order) = $this->queryBuilder();
|
||||
$res = $this->model
|
||||
->withJoin($this->withJoinTable ?? [], $this->withJoinType ?? 'LEFT')
|
||||
->alias($alias)
|
||||
->where($where)
|
||||
->order($order)
|
||||
->paginate($limit);
|
||||
|
||||
return $this->success('', [
|
||||
'list' => $res->items(),
|
||||
'total' => $res->total(),
|
||||
'remark' => get_route_remark(),
|
||||
]);
|
||||
}
|
||||
}
|
||||
149
app/admin/controller/mall/Player.php
Normal file
149
app/admin/controller/mall/Player.php
Normal file
@@ -0,0 +1,149 @@
|
||||
<?php
|
||||
|
||||
namespace app\admin\controller\mall;
|
||||
|
||||
use app\common\controller\Backend;
|
||||
use support\Response;
|
||||
use Throwable;
|
||||
use Webman\Http\Request;
|
||||
|
||||
/**
|
||||
* 积分商城用户
|
||||
*/
|
||||
class Player extends Backend
|
||||
{
|
||||
/**
|
||||
* Player模型对象
|
||||
* @var object|null
|
||||
* @phpstan-var \app\admin\model\mall\Player|null
|
||||
*/
|
||||
protected ?object $model = null;
|
||||
|
||||
protected array|string $preExcludeFields = ['id', 'create_time', 'update_time', 'password'];
|
||||
|
||||
protected string|array $quickSearchField = ['id'];
|
||||
|
||||
/** 列表不返回密码字段 */
|
||||
protected string|array $indexField = ['id', 'username', 'create_time', 'update_time', 'score'];
|
||||
|
||||
public function initialize(): void
|
||||
{
|
||||
parent::initialize();
|
||||
$this->model = new \app\admin\model\mall\Player();
|
||||
}
|
||||
|
||||
/**
|
||||
* 添加(重写以支持密码加密)
|
||||
*/
|
||||
public function add(Request $request): Response
|
||||
{
|
||||
$response = $this->initializeBackend($request);
|
||||
if ($response instanceof Response) {
|
||||
return $response;
|
||||
}
|
||||
|
||||
if ($request->method() !== 'POST') {
|
||||
$this->error(__('Parameter error'));
|
||||
}
|
||||
|
||||
$data = $request->post();
|
||||
if (!$data) {
|
||||
$this->error(__('Parameter %s can not be empty', ['']));
|
||||
}
|
||||
|
||||
$passwd = $data['password'] ?? '';
|
||||
if (empty($passwd)) {
|
||||
$this->error(__('Parameter %s can not be empty', [__('Password')]));
|
||||
}
|
||||
|
||||
$data = $this->applyInputFilter($data);
|
||||
$data = $this->excludeFields($data);
|
||||
|
||||
$result = false;
|
||||
$this->model->startTrans();
|
||||
try {
|
||||
if ($this->modelValidate) {
|
||||
$validate = str_replace("\\model\\", "\\validate\\", get_class($this->model));
|
||||
if (class_exists($validate)) {
|
||||
$validate = new $validate();
|
||||
if ($this->modelSceneValidate) {
|
||||
$validate->scene('add');
|
||||
}
|
||||
$validate->check($data);
|
||||
}
|
||||
}
|
||||
$result = $this->model->save($data);
|
||||
if ($result !== false && $passwd) {
|
||||
$this->model->resetPassword((int) $this->model->id, $passwd);
|
||||
}
|
||||
$this->model->commit();
|
||||
} catch (Throwable $e) {
|
||||
$this->model->rollback();
|
||||
$this->error($e->getMessage());
|
||||
}
|
||||
|
||||
$result !== false ? $this->success(__('Added successfully')) : $this->error(__('No rows were added'));
|
||||
}
|
||||
|
||||
/**
|
||||
* 编辑(重写以支持编辑时密码可选)
|
||||
*/
|
||||
public function edit(Request $request): Response
|
||||
{
|
||||
$response = $this->initializeBackend($request);
|
||||
if ($response instanceof Response) {
|
||||
return $response;
|
||||
}
|
||||
|
||||
$pk = $this->model->getPk();
|
||||
$id = $request->post($pk) ?? $request->get($pk);
|
||||
$row = $this->model->find($id);
|
||||
if (!$row) {
|
||||
$this->error(__('Record not found'));
|
||||
}
|
||||
|
||||
if ($request->method() === 'POST') {
|
||||
$data = $request->post();
|
||||
if (!$data) {
|
||||
$this->error(__('Parameter %s can not be empty', ['']));
|
||||
}
|
||||
|
||||
if (!empty($data['password'])) {
|
||||
$this->model->resetPassword((int) $row->id, $data['password']);
|
||||
}
|
||||
|
||||
$data = $this->applyInputFilter($data);
|
||||
$data = $this->excludeFields($data);
|
||||
|
||||
$result = false;
|
||||
$this->model->startTrans();
|
||||
try {
|
||||
if ($this->modelValidate) {
|
||||
$validate = str_replace("\\model\\", "\\validate\\", get_class($this->model));
|
||||
if (class_exists($validate)) {
|
||||
$validate = new $validate();
|
||||
if ($this->modelSceneValidate) {
|
||||
$validate->scene('edit');
|
||||
}
|
||||
$validate->check(array_merge($data, [$pk => $row[$pk]]));
|
||||
}
|
||||
}
|
||||
$result = $row->save($data);
|
||||
$this->model->commit();
|
||||
} catch (Throwable $e) {
|
||||
$this->model->rollback();
|
||||
$this->error($e->getMessage());
|
||||
}
|
||||
|
||||
return $result !== false ? $this->success(__('Update successful')) : $this->error(__('No rows updated'));
|
||||
}
|
||||
|
||||
unset($row['password']);
|
||||
$row['password'] = '';
|
||||
$this->success('', ['row' => $row]);
|
||||
}
|
||||
|
||||
/**
|
||||
* 若需重写查看、删除等方法,请复制 @see \app\admin\library\traits\Backend 中对应的方法至此进行重写
|
||||
*/
|
||||
}
|
||||
91
app/admin/controller/routine/AdminInfo.php
Normal file
91
app/admin/controller/routine/AdminInfo.php
Normal file
@@ -0,0 +1,91 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace app\admin\controller\routine;
|
||||
|
||||
use app\admin\model\Admin;
|
||||
use app\common\controller\Backend;
|
||||
use Webman\Http\Request;
|
||||
use support\Response;
|
||||
|
||||
class AdminInfo extends Backend
|
||||
{
|
||||
protected ?object $model = null;
|
||||
|
||||
protected array|string $preExcludeFields = ['username', 'last_login_time', 'password', 'salt', 'status'];
|
||||
protected array $authAllowFields = ['id', 'username', 'nickname', 'avatar', 'email', 'mobile', 'motto', 'last_login_time'];
|
||||
|
||||
protected function initController(Request $request): ?Response
|
||||
{
|
||||
$this->auth->setAllowFields($this->authAllowFields);
|
||||
$this->model = $this->auth->getAdmin();
|
||||
return null;
|
||||
}
|
||||
|
||||
public function index(Request $request): Response
|
||||
{
|
||||
$response = $this->initializeBackend($request);
|
||||
if ($response !== null) return $response;
|
||||
|
||||
$info = $this->auth->getInfo();
|
||||
return $this->success('', ['info' => $info]);
|
||||
}
|
||||
|
||||
public function edit(Request $request): Response
|
||||
{
|
||||
$response = $this->initializeBackend($request);
|
||||
if ($response !== null) return $response;
|
||||
|
||||
$pk = $this->model->getPk();
|
||||
$id = $request->post($pk) ?? $request->get($pk);
|
||||
$row = $this->model->find($id);
|
||||
if (!$row) {
|
||||
return $this->error(__('Record not found'));
|
||||
}
|
||||
|
||||
if ($request->method() === 'POST') {
|
||||
$data = $request->post();
|
||||
if (!$data) {
|
||||
return $this->error(__('Parameter %s can not be empty', ['']));
|
||||
}
|
||||
|
||||
if (!empty($data['avatar'])) {
|
||||
$row->avatar = $data['avatar'];
|
||||
if ($row->save()) {
|
||||
return $this->success(__('Avatar modified successfully!'));
|
||||
}
|
||||
}
|
||||
|
||||
if ($this->modelValidate) {
|
||||
$validateClass = str_replace("\\model\\", "\\validate\\", get_class($this->model));
|
||||
if (class_exists($validateClass)) {
|
||||
try {
|
||||
$validate = new $validateClass();
|
||||
$validate->scene('info')->check($data);
|
||||
} catch (\Throwable $e) {
|
||||
return $this->error($e->getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!empty($data['password'])) {
|
||||
$this->model->resetPassword($this->auth->id, $data['password']);
|
||||
}
|
||||
|
||||
$data = $this->excludeFields($data);
|
||||
$result = false;
|
||||
$this->model->startTrans();
|
||||
try {
|
||||
$result = $row->save($data);
|
||||
$this->model->commit();
|
||||
} catch (\Throwable $e) {
|
||||
$this->model->rollback();
|
||||
return $this->error($e->getMessage());
|
||||
}
|
||||
return $result !== false ? $this->success(__('Update successful')) : $this->error(__('No rows updated'));
|
||||
}
|
||||
|
||||
return $this->success('', ['row' => $row]);
|
||||
}
|
||||
}
|
||||
52
app/admin/controller/routine/Attachment.php
Normal file
52
app/admin/controller/routine/Attachment.php
Normal file
@@ -0,0 +1,52 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace app\admin\controller\routine;
|
||||
|
||||
use app\common\controller\Backend;
|
||||
use app\common\model\Attachment as AttachmentModel;
|
||||
use Webman\Http\Request;
|
||||
use support\Response;
|
||||
|
||||
class Attachment extends Backend
|
||||
{
|
||||
protected ?object $model = null;
|
||||
|
||||
protected array|string $quickSearchField = 'name';
|
||||
protected array $withJoinTable = ['admin', 'user'];
|
||||
protected array|string $defaultSortField = ['last_upload_time' => 'desc'];
|
||||
|
||||
protected function initController(Request $request): ?Response
|
||||
{
|
||||
$this->model = new AttachmentModel();
|
||||
return null;
|
||||
}
|
||||
|
||||
public function del(Request $request): Response
|
||||
{
|
||||
$response = $this->initializeBackend($request);
|
||||
if ($response !== null) return $response;
|
||||
|
||||
$where = [];
|
||||
$dataLimitAdminIds = $this->getDataLimitAdminIds();
|
||||
if ($dataLimitAdminIds) {
|
||||
$where[] = [$this->dataLimitField, 'in', $dataLimitAdminIds];
|
||||
}
|
||||
|
||||
$ids = $request->post('ids', $request->get('ids', []));
|
||||
$ids = is_array($ids) ? $ids : [];
|
||||
$where[] = [$this->model->getPk(), 'in', $ids];
|
||||
$data = $this->model->where($where)->select();
|
||||
|
||||
$count = 0;
|
||||
try {
|
||||
foreach ($data as $v) {
|
||||
$count += $v->delete();
|
||||
}
|
||||
} catch (\Throwable $e) {
|
||||
return $this->error(__('%d records and files have been deleted', [$count]) . $e->getMessage());
|
||||
}
|
||||
return $count ? $this->success(__('%d records and files have been deleted', [$count])) : $this->error(__('No rows were deleted'));
|
||||
}
|
||||
}
|
||||
169
app/admin/controller/routine/Config.php
Normal file
169
app/admin/controller/routine/Config.php
Normal file
@@ -0,0 +1,169 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace app\admin\controller\routine;
|
||||
|
||||
use ba\Filesystem;
|
||||
use app\common\library\Email;
|
||||
use PHPMailer\PHPMailer\PHPMailer;
|
||||
use app\common\controller\Backend;
|
||||
use app\admin\model\Config as ConfigModel;
|
||||
use PHPMailer\PHPMailer\Exception as PHPMailerException;
|
||||
use Webman\Http\Request;
|
||||
use support\Response;
|
||||
|
||||
class Config extends Backend
|
||||
{
|
||||
protected ?object $model = null;
|
||||
|
||||
protected array $filePath = [
|
||||
'appConfig' => 'config/app.php',
|
||||
'webAdminBase' => 'web/src/router/static/adminBase.ts',
|
||||
'backendEntranceStub' => 'app/admin/library/stubs/backendEntrance.stub',
|
||||
];
|
||||
|
||||
protected function initController(Request $request): ?Response
|
||||
{
|
||||
$this->model = new ConfigModel();
|
||||
return null;
|
||||
}
|
||||
|
||||
public function index(Request $request): Response
|
||||
{
|
||||
$response = $this->initializeBackend($request);
|
||||
if ($response !== null) return $response;
|
||||
|
||||
$configGroup = get_sys_config('config_group');
|
||||
$config = $this->model->order('weigh desc')->select()->toArray();
|
||||
|
||||
$list = [];
|
||||
$newConfigGroup = [];
|
||||
if (is_array($configGroup)) {
|
||||
foreach ($configGroup as $item) {
|
||||
$key = $item['key'] ?? $item;
|
||||
$val = $item['value'] ?? $item;
|
||||
$list[$key] = ['name' => $key, 'title' => __($val)];
|
||||
$newConfigGroup[$key] = $list[$key]['title'];
|
||||
}
|
||||
}
|
||||
foreach ($config as $item) {
|
||||
$group = $item['group'] ?? '';
|
||||
if (isset($newConfigGroup[$group])) {
|
||||
$item['title'] = __($item['title'] ?? '');
|
||||
$list[$group]['list'][] = $item;
|
||||
}
|
||||
}
|
||||
|
||||
return $this->success('', [
|
||||
'list' => $list,
|
||||
'remark' => get_route_remark(),
|
||||
'configGroup' => $newConfigGroup,
|
||||
'quickEntrance' => get_sys_config('config_quick_entrance'),
|
||||
]);
|
||||
}
|
||||
|
||||
public function edit(Request $request): Response
|
||||
{
|
||||
$response = $this->initializeBackend($request);
|
||||
if ($response !== null) return $response;
|
||||
|
||||
$all = $this->model->select();
|
||||
if ($request->method() === 'POST') {
|
||||
$this->modelValidate = false;
|
||||
$data = $request->post();
|
||||
if (!$data) {
|
||||
return $this->error(__('Parameter %s can not be empty', ['']));
|
||||
}
|
||||
|
||||
$data = $this->excludeFields($data);
|
||||
$configValue = [];
|
||||
foreach ($all as $item) {
|
||||
if (array_key_exists($item->name, $data)) {
|
||||
$configValue[] = [
|
||||
'id' => $item->id,
|
||||
'type' => $item->getData('type'),
|
||||
'value' => $data[$item->name]
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
$result = false;
|
||||
$this->model->startTrans();
|
||||
try {
|
||||
foreach ($configValue as $cv) {
|
||||
$this->model->where('id', $cv['id'])->update(['value' => $cv['value']]);
|
||||
}
|
||||
$this->model->commit();
|
||||
$result = true;
|
||||
} catch (\Throwable $e) {
|
||||
$this->model->rollback();
|
||||
return $this->error($e->getMessage());
|
||||
}
|
||||
return $result ? $this->success(__('The current page configuration item was updated successfully')) : $this->error(__('No rows updated'));
|
||||
}
|
||||
|
||||
return $this->error(__('Parameter error'));
|
||||
}
|
||||
|
||||
public function add(Request $request): Response
|
||||
{
|
||||
$response = $this->initializeBackend($request);
|
||||
if ($response !== null) return $response;
|
||||
|
||||
if ($request->method() !== 'POST') {
|
||||
return $this->error(__('Parameter error'));
|
||||
}
|
||||
|
||||
$data = $request->post();
|
||||
if (!$data) {
|
||||
return $this->error(__('Parameter %s can not be empty', ['']));
|
||||
}
|
||||
|
||||
$data = $this->excludeFields($data);
|
||||
$result = false;
|
||||
$this->model->startTrans();
|
||||
try {
|
||||
if ($this->modelValidate) {
|
||||
$validateClass = str_replace("\\model\\", "\\validate\\", get_class($this->model));
|
||||
if (class_exists($validateClass)) {
|
||||
$validate = new $validateClass();
|
||||
$validate->scene('add')->check($data);
|
||||
}
|
||||
}
|
||||
$result = $this->model->save($data);
|
||||
$this->model->commit();
|
||||
} catch (\Throwable $e) {
|
||||
$this->model->rollback();
|
||||
return $this->error($e->getMessage());
|
||||
}
|
||||
return $result !== false ? $this->success(__('Added successfully')) : $this->error(__('No rows were added'));
|
||||
}
|
||||
|
||||
public function sendTestMail(Request $request): Response
|
||||
{
|
||||
$response = $this->initializeBackend($request);
|
||||
if ($response !== null) return $response;
|
||||
|
||||
$data = $request->post();
|
||||
$mail = new Email();
|
||||
try {
|
||||
$mail->Host = $data['smtp_server'] ?? '';
|
||||
$mail->SMTPAuth = true;
|
||||
$mail->Username = $data['smtp_user'] ?? '';
|
||||
$mail->Password = $data['smtp_pass'] ?? '';
|
||||
$mail->SMTPSecure = ($data['smtp_verification'] ?? '') == 'SSL' ? PHPMailer::ENCRYPTION_SMTPS : PHPMailer::ENCRYPTION_STARTTLS;
|
||||
$mail->Port = $data['smtp_port'] ?? 465;
|
||||
$mail->setFrom($data['smtp_sender_mail'] ?? '', $data['smtp_user'] ?? '');
|
||||
$mail->isSMTP();
|
||||
$mail->addAddress($data['testMail'] ?? '');
|
||||
$mail->isHTML();
|
||||
$mail->setSubject(__('This is a test email') . '-' . get_sys_config('site_name'));
|
||||
$mail->Body = __('Congratulations, receiving this email means that your email service has been configured correctly');
|
||||
$mail->send();
|
||||
} catch (PHPMailerException) {
|
||||
return $this->error($mail->ErrorInfo ?? '');
|
||||
}
|
||||
return $this->success(__('Test mail sent successfully~'));
|
||||
}
|
||||
}
|
||||
155
app/admin/controller/security/DataRecycle.php
Normal file
155
app/admin/controller/security/DataRecycle.php
Normal file
@@ -0,0 +1,155 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace app\admin\controller\security;
|
||||
|
||||
use app\common\controller\Backend;
|
||||
use app\admin\model\DataRecycle as DataRecycleModel;
|
||||
use Webman\Http\Request;
|
||||
use support\Response;
|
||||
|
||||
class DataRecycle extends Backend
|
||||
{
|
||||
protected ?object $model = null;
|
||||
|
||||
protected array|string $preExcludeFields = ['update_time', 'create_time'];
|
||||
protected array|string $quickSearchField = 'name';
|
||||
|
||||
protected function initController(Request $request): ?Response
|
||||
{
|
||||
$this->model = new DataRecycleModel();
|
||||
return null;
|
||||
}
|
||||
|
||||
public function index(Request $request): Response
|
||||
{
|
||||
$response = $this->initializeBackend($request);
|
||||
if ($response !== null) return $response;
|
||||
|
||||
if ($request->get('select') || $request->post('select')) {
|
||||
return $this->select($request);
|
||||
}
|
||||
|
||||
list($where, $alias, $limit, $order) = $this->queryBuilder();
|
||||
$res = $this->model
|
||||
->withJoin($this->withJoinTable ?? [], $this->withJoinType ?? 'LEFT')
|
||||
->alias($alias)
|
||||
->where($where)
|
||||
->order($order)
|
||||
->paginate($limit);
|
||||
|
||||
return $this->success('', [
|
||||
'list' => $res->items(),
|
||||
'total' => $res->total(),
|
||||
'remark' => get_route_remark(),
|
||||
]);
|
||||
}
|
||||
|
||||
public function add(Request $request): Response
|
||||
{
|
||||
$response = $this->initializeBackend($request);
|
||||
if ($response !== null) return $response;
|
||||
|
||||
if ($request->method() === 'POST') {
|
||||
$data = $request->post();
|
||||
if (!$data) {
|
||||
return $this->error(__('Parameter %s can not be empty', ['']));
|
||||
}
|
||||
$data = $this->excludeFields($data);
|
||||
$data['controller_as'] = str_ireplace('.php', '', $data['controller'] ?? '');
|
||||
$data['controller_as'] = strtolower(str_ireplace(['\\', '.'], '/', $data['controller_as']));
|
||||
|
||||
$result = false;
|
||||
$this->model->startTrans();
|
||||
try {
|
||||
if ($this->modelValidate) {
|
||||
$validateClass = str_replace("\\model\\", "\\validate\\", get_class($this->model));
|
||||
if (class_exists($validateClass)) {
|
||||
$validate = new $validateClass();
|
||||
$validate->scene('add')->check($data);
|
||||
}
|
||||
}
|
||||
$result = $this->model->save($data);
|
||||
$this->model->commit();
|
||||
} catch (\Throwable $e) {
|
||||
$this->model->rollback();
|
||||
return $this->error($e->getMessage());
|
||||
}
|
||||
return $result !== false ? $this->success(__('Added successfully')) : $this->error(__('No rows were added'));
|
||||
}
|
||||
|
||||
return $this->success('', [
|
||||
'controllers' => $this->getControllerList(),
|
||||
]);
|
||||
}
|
||||
|
||||
public function edit(Request $request): Response
|
||||
{
|
||||
$response = $this->initializeBackend($request);
|
||||
if ($response !== null) return $response;
|
||||
|
||||
$pk = $this->model->getPk();
|
||||
$id = $request->post($pk) ?? $request->get($pk);
|
||||
$row = $this->model->find($id);
|
||||
if (!$row) {
|
||||
return $this->error(__('Record not found'));
|
||||
}
|
||||
|
||||
if ($request->method() === 'POST') {
|
||||
$data = $request->post();
|
||||
if (!$data) {
|
||||
return $this->error(__('Parameter %s can not be empty', ['']));
|
||||
}
|
||||
$data = $this->excludeFields($data);
|
||||
$data['controller_as'] = str_ireplace('.php', '', $data['controller'] ?? '');
|
||||
$data['controller_as'] = strtolower(str_ireplace(['\\', '.'], '/', $data['controller_as']));
|
||||
|
||||
$result = false;
|
||||
$this->model->startTrans();
|
||||
try {
|
||||
if ($this->modelValidate) {
|
||||
$validateClass = str_replace("\\model\\", "\\validate\\", get_class($this->model));
|
||||
if (class_exists($validateClass)) {
|
||||
$validate = new $validateClass();
|
||||
$validate->scene('edit')->check(array_merge($data, [$pk => $row[$pk]]));
|
||||
}
|
||||
}
|
||||
$result = $row->save($data);
|
||||
$this->model->commit();
|
||||
} catch (\Throwable $e) {
|
||||
$this->model->rollback();
|
||||
return $this->error($e->getMessage());
|
||||
}
|
||||
return $result !== false ? $this->success(__('Update successful')) : $this->error(__('No rows updated'));
|
||||
}
|
||||
|
||||
return $this->success('', [
|
||||
'row' => $row,
|
||||
'controllers' => $this->getControllerList(),
|
||||
]);
|
||||
}
|
||||
|
||||
private function getControllerList(): array
|
||||
{
|
||||
$outExcludeController = [
|
||||
'Addon.php',
|
||||
'Ajax.php',
|
||||
'Module.php',
|
||||
'Terminal.php',
|
||||
'Dashboard.php',
|
||||
'Index.php',
|
||||
'routine/AdminInfo.php',
|
||||
'user/MoneyLog.php',
|
||||
'user/ScoreLog.php',
|
||||
];
|
||||
$outControllers = [];
|
||||
$controllers = get_controller_list();
|
||||
foreach ($controllers as $key => $controller) {
|
||||
if (!in_array($controller, $outExcludeController)) {
|
||||
$outControllers[$key] = $controller;
|
||||
}
|
||||
}
|
||||
return $outControllers;
|
||||
}
|
||||
}
|
||||
119
app/admin/controller/security/DataRecycleLog.php
Normal file
119
app/admin/controller/security/DataRecycleLog.php
Normal file
@@ -0,0 +1,119 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace app\admin\controller\security;
|
||||
|
||||
use ba\TableManager;
|
||||
use support\think\Db;
|
||||
use app\common\controller\Backend;
|
||||
use app\admin\model\DataRecycleLog as DataRecycleLogModel;
|
||||
use Webman\Http\Request;
|
||||
use support\Response;
|
||||
|
||||
class DataRecycleLog extends Backend
|
||||
{
|
||||
protected ?object $model = null;
|
||||
|
||||
protected array|string $preExcludeFields = [];
|
||||
protected array|string $quickSearchField = 'recycle.name';
|
||||
protected array $withJoinTable = ['recycle', 'admin'];
|
||||
|
||||
protected function initController(Request $request): ?Response
|
||||
{
|
||||
$this->model = new DataRecycleLogModel();
|
||||
return null;
|
||||
}
|
||||
|
||||
public function index(Request $request): Response
|
||||
{
|
||||
$response = $this->initializeBackend($request);
|
||||
if ($response !== null) return $response;
|
||||
|
||||
if ($request->get('select') || $request->post('select')) {
|
||||
return $this->select($request);
|
||||
}
|
||||
|
||||
list($where, $alias, $limit, $order) = $this->queryBuilder();
|
||||
$res = $this->model
|
||||
->withJoin($this->withJoinTable, $this->withJoinType)
|
||||
->alias($alias)
|
||||
->where($where)
|
||||
->order($order)
|
||||
->paginate($limit);
|
||||
|
||||
return $this->success('', [
|
||||
'list' => $res->items(),
|
||||
'total' => $res->total(),
|
||||
'remark' => get_route_remark(),
|
||||
]);
|
||||
}
|
||||
|
||||
public function restore(Request $request): Response
|
||||
{
|
||||
$response = $this->initializeBackend($request);
|
||||
if ($response !== null) return $response;
|
||||
|
||||
$ids = $request->post('ids', $request->get('ids', []));
|
||||
$ids = is_array($ids) ? $ids : [];
|
||||
$data = $this->model->where('id', 'in', $ids)->select();
|
||||
if (!$data) {
|
||||
return $this->error(__('Record not found'));
|
||||
}
|
||||
|
||||
$count = 0;
|
||||
$this->model->startTrans();
|
||||
try {
|
||||
foreach ($data as $row) {
|
||||
$recycleData = json_decode($row['data'], true);
|
||||
if (is_array($recycleData) && Db::connect(TableManager::getConnection($row->connection))->name($row->data_table)->insert($recycleData)) {
|
||||
$row->delete();
|
||||
$count++;
|
||||
}
|
||||
}
|
||||
$this->model->commit();
|
||||
} catch (\Throwable $e) {
|
||||
$this->model->rollback();
|
||||
return $this->error($e->getMessage());
|
||||
}
|
||||
|
||||
return $count ? $this->success(__('Restore successful')) : $this->error(__('No rows were restore'));
|
||||
}
|
||||
|
||||
public function info(Request $request): Response
|
||||
{
|
||||
$response = $this->initializeBackend($request);
|
||||
if ($response !== null) return $response;
|
||||
|
||||
$pk = $this->model->getPk();
|
||||
$id = $request->get($pk) ?? $request->post($pk);
|
||||
$row = $this->model
|
||||
->withJoin($this->withJoinTable, $this->withJoinType)
|
||||
->where('data_recycle_log.id', $id)
|
||||
->find();
|
||||
if (!$row) {
|
||||
return $this->error(__('Record not found'));
|
||||
}
|
||||
$data = $this->jsonToArray($row['data']);
|
||||
if (is_array($data)) {
|
||||
foreach ($data as $key => $item) {
|
||||
$data[$key] = $this->jsonToArray($item);
|
||||
}
|
||||
}
|
||||
$row['data'] = $data;
|
||||
|
||||
return $this->success('', ['row' => $row]);
|
||||
}
|
||||
|
||||
protected function jsonToArray(mixed $value = ''): mixed
|
||||
{
|
||||
if (!is_string($value)) {
|
||||
return $value;
|
||||
}
|
||||
$data = json_decode($value, true);
|
||||
if (($data && is_object($data)) || (is_array($data) && !empty($data))) {
|
||||
return $data;
|
||||
}
|
||||
return $value;
|
||||
}
|
||||
}
|
||||
182
app/admin/controller/security/SensitiveData.php
Normal file
182
app/admin/controller/security/SensitiveData.php
Normal file
@@ -0,0 +1,182 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace app\admin\controller\security;
|
||||
|
||||
use app\common\controller\Backend;
|
||||
use app\admin\model\SensitiveData as SensitiveDataModel;
|
||||
use Webman\Http\Request;
|
||||
use support\Response;
|
||||
|
||||
class SensitiveData extends Backend
|
||||
{
|
||||
protected ?object $model = null;
|
||||
|
||||
protected array|string $preExcludeFields = ['update_time', 'create_time'];
|
||||
protected array|string $quickSearchField = 'controller';
|
||||
|
||||
protected function initController(Request $request): ?Response
|
||||
{
|
||||
$this->model = new SensitiveDataModel();
|
||||
return null;
|
||||
}
|
||||
|
||||
public function index(Request $request): Response
|
||||
{
|
||||
$response = $this->initializeBackend($request);
|
||||
if ($response !== null) return $response;
|
||||
|
||||
if ($request->get('select') || $request->post('select')) {
|
||||
return $this->select($request);
|
||||
}
|
||||
|
||||
list($where, $alias, $limit, $order) = $this->queryBuilder();
|
||||
$res = $this->model
|
||||
->withJoin($this->withJoinTable, $this->withJoinType)
|
||||
->alias($alias)
|
||||
->where($where)
|
||||
->order($order)
|
||||
->paginate($limit);
|
||||
|
||||
$items = $res->items();
|
||||
foreach ($items as $item) {
|
||||
if ($item->data_fields) {
|
||||
$fields = [];
|
||||
foreach ($item->data_fields as $key => $field) {
|
||||
$fields[] = $field ?: $key;
|
||||
}
|
||||
$item->data_fields = $fields;
|
||||
}
|
||||
}
|
||||
|
||||
return $this->success('', [
|
||||
'list' => $items,
|
||||
'total' => $res->total(),
|
||||
'remark' => get_route_remark(),
|
||||
]);
|
||||
}
|
||||
|
||||
public function add(Request $request): Response
|
||||
{
|
||||
$response = $this->initializeBackend($request);
|
||||
if ($response !== null) return $response;
|
||||
|
||||
if ($request->method() !== 'POST') {
|
||||
return $this->success('', ['controllers' => $this->getControllerList()]);
|
||||
}
|
||||
|
||||
$data = $request->post();
|
||||
if (!$data) {
|
||||
return $this->error(__('Parameter %s can not be empty', ['']));
|
||||
}
|
||||
|
||||
$data = $this->excludeFields($data);
|
||||
$data['controller_as'] = str_ireplace('.php', '', $data['controller'] ?? '');
|
||||
$data['controller_as'] = strtolower(str_ireplace(['\\', '.'], '/', $data['controller_as']));
|
||||
|
||||
if (is_array($data['fields'] ?? null)) {
|
||||
$data['data_fields'] = [];
|
||||
foreach ($data['fields'] as $field) {
|
||||
$data['data_fields'][$field['name']] = $field['value'];
|
||||
}
|
||||
}
|
||||
|
||||
$result = false;
|
||||
$this->model->startTrans();
|
||||
try {
|
||||
if ($this->modelValidate) {
|
||||
$validateClass = str_replace("\\model\\", "\\validate\\", get_class($this->model));
|
||||
if (class_exists($validateClass)) {
|
||||
$validate = new $validateClass();
|
||||
$validate->scene('add')->check($data);
|
||||
}
|
||||
}
|
||||
$result = $this->model->save($data);
|
||||
$this->model->commit();
|
||||
} catch (\Throwable $e) {
|
||||
$this->model->rollback();
|
||||
return $this->error($e->getMessage());
|
||||
}
|
||||
return $result !== false ? $this->success(__('Added successfully')) : $this->error(__('No rows were added'));
|
||||
}
|
||||
|
||||
public function edit(Request $request): Response
|
||||
{
|
||||
$response = $this->initializeBackend($request);
|
||||
if ($response !== null) return $response;
|
||||
|
||||
$pk = $this->model->getPk();
|
||||
$id = $request->get($pk) ?? $request->post($pk);
|
||||
$row = $this->model->find($id);
|
||||
if (!$row) {
|
||||
return $this->error(__('Record not found'));
|
||||
}
|
||||
|
||||
if ($request->method() !== 'POST') {
|
||||
return $this->success('', [
|
||||
'row' => $row,
|
||||
'controllers' => $this->getControllerList(),
|
||||
]);
|
||||
}
|
||||
|
||||
$data = $request->post();
|
||||
if (!$data) {
|
||||
return $this->error(__('Parameter %s can not be empty', ['']));
|
||||
}
|
||||
|
||||
$data = $this->excludeFields($data);
|
||||
$data['controller_as'] = str_ireplace('.php', '', $data['controller'] ?? '');
|
||||
$data['controller_as'] = strtolower(str_ireplace(['\\', '.'], '/', $data['controller_as']));
|
||||
|
||||
if (is_array($data['fields'] ?? null)) {
|
||||
$data['data_fields'] = [];
|
||||
foreach ($data['fields'] as $field) {
|
||||
$data['data_fields'][$field['name']] = $field['value'];
|
||||
}
|
||||
}
|
||||
|
||||
$result = false;
|
||||
$this->model->startTrans();
|
||||
try {
|
||||
if ($this->modelValidate) {
|
||||
$validateClass = str_replace("\\model\\", "\\validate\\", get_class($this->model));
|
||||
if (class_exists($validateClass)) {
|
||||
$validate = new $validateClass();
|
||||
$validate->scene('edit')->check(array_merge($data, [$pk => $row[$pk]]));
|
||||
}
|
||||
}
|
||||
$result = $row->save($data);
|
||||
$this->model->commit();
|
||||
} catch (\Throwable $e) {
|
||||
$this->model->rollback();
|
||||
return $this->error($e->getMessage());
|
||||
}
|
||||
return $result !== false ? $this->success(__('Update successful')) : $this->error(__('No rows updated'));
|
||||
}
|
||||
|
||||
private function getControllerList(): array
|
||||
{
|
||||
$outExcludeController = [
|
||||
'Addon.php',
|
||||
'Ajax.php',
|
||||
'Dashboard.php',
|
||||
'Index.php',
|
||||
'Module.php',
|
||||
'Terminal.php',
|
||||
'auth/AdminLog.php',
|
||||
'routine/AdminInfo.php',
|
||||
'routine/Config.php',
|
||||
'user/MoneyLog.php',
|
||||
'user/ScoreLog.php',
|
||||
];
|
||||
$outControllers = [];
|
||||
$controllers = get_controller_list();
|
||||
foreach ($controllers as $key => $controller) {
|
||||
if (!in_array($controller, $outExcludeController)) {
|
||||
$outControllers[$key] = $controller;
|
||||
}
|
||||
}
|
||||
return $outControllers;
|
||||
}
|
||||
}
|
||||
102
app/admin/controller/security/SensitiveDataLog.php
Normal file
102
app/admin/controller/security/SensitiveDataLog.php
Normal file
@@ -0,0 +1,102 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace app\admin\controller\security;
|
||||
|
||||
use ba\TableManager;
|
||||
use support\think\Db;
|
||||
use app\common\controller\Backend;
|
||||
use app\admin\model\SensitiveDataLog as SensitiveDataLogModel;
|
||||
use Webman\Http\Request;
|
||||
use support\Response;
|
||||
|
||||
class SensitiveDataLog extends Backend
|
||||
{
|
||||
protected ?object $model = null;
|
||||
|
||||
protected array|string $preExcludeFields = [];
|
||||
protected array|string $quickSearchField = 'sensitive.name';
|
||||
protected array $withJoinTable = ['sensitive', 'admin'];
|
||||
|
||||
protected function initController(Request $request): ?Response
|
||||
{
|
||||
$this->model = new SensitiveDataLogModel();
|
||||
return null;
|
||||
}
|
||||
|
||||
public function index(Request $request): Response
|
||||
{
|
||||
$response = $this->initializeBackend($request);
|
||||
if ($response !== null) return $response;
|
||||
|
||||
if ($request->get('select') || $request->post('select')) {
|
||||
return $this->select($request);
|
||||
}
|
||||
|
||||
list($where, $alias, $limit, $order) = $this->queryBuilder();
|
||||
$res = $this->model
|
||||
->withJoin($this->withJoinTable, $this->withJoinType)
|
||||
->alias($alias)
|
||||
->where($where)
|
||||
->order($order)
|
||||
->paginate($limit);
|
||||
|
||||
$items = $res->items();
|
||||
foreach ($items as $item) {
|
||||
$item->id_value = $item['primary_key'] . '=' . $item->id_value;
|
||||
}
|
||||
|
||||
return $this->success('', [
|
||||
'list' => $items,
|
||||
'total' => $res->total(),
|
||||
'remark' => get_route_remark(),
|
||||
]);
|
||||
}
|
||||
|
||||
public function info(Request $request): Response
|
||||
{
|
||||
$response = $this->initializeBackend($request);
|
||||
if ($response !== null) return $response;
|
||||
|
||||
$pk = $this->model->getPk();
|
||||
$id = $request->get($pk) ?? $request->post($pk);
|
||||
$row = $this->model
|
||||
->withJoin($this->withJoinTable, $this->withJoinType)
|
||||
->where('security_sensitive_data_log.id', $id)
|
||||
->find();
|
||||
if (!$row) {
|
||||
return $this->error(__('Record not found'));
|
||||
}
|
||||
return $this->success('', ['row' => $row]);
|
||||
}
|
||||
|
||||
public function rollback(Request $request): Response
|
||||
{
|
||||
$response = $this->initializeBackend($request);
|
||||
if ($response !== null) return $response;
|
||||
|
||||
$ids = $request->post('ids', $request->get('ids', []));
|
||||
$ids = is_array($ids) ? $ids : [];
|
||||
$data = $this->model->where('id', 'in', $ids)->select();
|
||||
if (!$data) {
|
||||
return $this->error(__('Record not found'));
|
||||
}
|
||||
|
||||
$count = 0;
|
||||
$this->model->startTrans();
|
||||
try {
|
||||
foreach ($data as $row) {
|
||||
$conn = Db::connect(TableManager::getConnection($row->connection));
|
||||
if ($conn->name($row->data_table)->where($row->primary_key, $row->id_value)->update([$row->data_field => $row->before])) {
|
||||
$count++;
|
||||
}
|
||||
}
|
||||
$this->model->commit();
|
||||
} catch (\Throwable $e) {
|
||||
$this->model->rollback();
|
||||
return $this->error($e->getMessage());
|
||||
}
|
||||
return $count ? $this->success(__('Rollback successful')) : $this->error(__('No rows were rolled back'));
|
||||
}
|
||||
}
|
||||
150
app/admin/controller/user/Group.php
Normal file
150
app/admin/controller/user/Group.php
Normal file
@@ -0,0 +1,150 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace app\admin\controller\user;
|
||||
|
||||
use Throwable;
|
||||
use app\admin\model\UserRule;
|
||||
use app\admin\model\UserGroup;
|
||||
use app\common\controller\Backend;
|
||||
use Webman\Http\Request;
|
||||
use support\Response;
|
||||
|
||||
class Group extends Backend
|
||||
{
|
||||
protected ?object $model = null;
|
||||
|
||||
protected array|string $preExcludeFields = ['update_time', 'create_time'];
|
||||
protected array|string $quickSearchField = 'name';
|
||||
|
||||
protected function initController(Request $request): ?Response
|
||||
{
|
||||
$this->model = new UserGroup();
|
||||
return null;
|
||||
}
|
||||
|
||||
public function select(Request $request): Response
|
||||
{
|
||||
$response = $this->initializeBackend($request);
|
||||
if ($response !== null) return $response;
|
||||
|
||||
list($where, $alias, $limit, $order) = $this->queryBuilder();
|
||||
$data = $this->model
|
||||
->alias($alias)
|
||||
->where($where)
|
||||
->order($order)
|
||||
->limit(9999)
|
||||
->select()
|
||||
->toArray();
|
||||
|
||||
return $this->success('', [
|
||||
'options' => $data,
|
||||
]);
|
||||
}
|
||||
|
||||
public function add(Request $request): Response
|
||||
{
|
||||
$response = $this->initializeBackend($request);
|
||||
if ($response !== null) return $response;
|
||||
|
||||
if ($request->method() !== 'POST') {
|
||||
return $this->error(__('Parameter error'));
|
||||
}
|
||||
|
||||
$data = $request->post();
|
||||
if (!$data) {
|
||||
return $this->error(__('Parameter %s can not be empty', ['']));
|
||||
}
|
||||
|
||||
$data = $this->excludeFields($data);
|
||||
$data = $this->handleRules($data);
|
||||
|
||||
$result = false;
|
||||
$this->model->startTrans();
|
||||
try {
|
||||
if ($this->modelValidate) {
|
||||
$validateClass = str_replace("\\model\\", "\\validate\\", get_class($this->model));
|
||||
if (class_exists($validateClass)) {
|
||||
$validate = new $validateClass();
|
||||
$validate->scene('add')->check($data);
|
||||
}
|
||||
}
|
||||
$result = $this->model->save($data);
|
||||
$this->model->commit();
|
||||
} catch (Throwable $e) {
|
||||
$this->model->rollback();
|
||||
return $this->error($e->getMessage());
|
||||
}
|
||||
return $result !== false ? $this->success(__('Added successfully')) : $this->error(__('No rows were added'));
|
||||
}
|
||||
|
||||
public function edit(Request $request): Response
|
||||
{
|
||||
$response = $this->initializeBackend($request);
|
||||
if ($response !== null) return $response;
|
||||
|
||||
$pk = $this->model->getPk();
|
||||
$id = $request->post($pk) ?? $request->get($pk);
|
||||
$row = $this->model->find($id);
|
||||
if (!$row) {
|
||||
return $this->error(__('Record not found'));
|
||||
}
|
||||
|
||||
if ($request->method() === 'POST') {
|
||||
$data = $request->post();
|
||||
if (!$data) {
|
||||
return $this->error(__('Parameter %s can not be empty', ['']));
|
||||
}
|
||||
$data = $this->excludeFields($data);
|
||||
$data = $this->handleRules($data);
|
||||
$result = false;
|
||||
$this->model->startTrans();
|
||||
try {
|
||||
if ($this->modelValidate) {
|
||||
$validateClass = str_replace("\\model\\", "\\validate\\", get_class($this->model));
|
||||
if (class_exists($validateClass)) {
|
||||
$validate = new $validateClass();
|
||||
$validate->scene('edit')->check(array_merge($data, [$pk => $row[$pk]]));
|
||||
}
|
||||
}
|
||||
$result = $row->save($data);
|
||||
$this->model->commit();
|
||||
} catch (Throwable $e) {
|
||||
$this->model->rollback();
|
||||
return $this->error($e->getMessage());
|
||||
}
|
||||
return $result !== false ? $this->success(__('Update successful')) : $this->error(__('No rows updated'));
|
||||
}
|
||||
|
||||
$pidArr = UserRule::field('pid')->distinct(true)->where('id', 'in', $row->rules)->select()->toArray();
|
||||
$rules = $row->rules ? explode(',', $row->rules) : [];
|
||||
foreach ($pidArr as $item) {
|
||||
$ruKey = array_search($item['pid'], $rules);
|
||||
if ($ruKey !== false) {
|
||||
unset($rules[$ruKey]);
|
||||
}
|
||||
}
|
||||
$rowData = $row->toArray();
|
||||
$rowData['rules'] = array_values($rules);
|
||||
return $this->success('', ['row' => $rowData]);
|
||||
}
|
||||
|
||||
private function handleRules(array $data): array
|
||||
{
|
||||
if (isset($data['rules']) && is_array($data['rules']) && $data['rules']) {
|
||||
$rules = UserRule::select();
|
||||
$super = true;
|
||||
foreach ($rules as $rule) {
|
||||
if (!in_array($rule['id'], $data['rules'])) {
|
||||
$super = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
$data['rules'] = $super ? '*' : implode(',', $data['rules']);
|
||||
} else {
|
||||
unset($data['rules']);
|
||||
}
|
||||
return $data;
|
||||
}
|
||||
}
|
||||
43
app/admin/controller/user/MoneyLog.php
Normal file
43
app/admin/controller/user/MoneyLog.php
Normal file
@@ -0,0 +1,43 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace app\admin\controller\user;
|
||||
|
||||
use app\admin\model\User;
|
||||
use app\admin\model\UserMoneyLog;
|
||||
use app\common\controller\Backend;
|
||||
use Webman\Http\Request;
|
||||
use support\Response;
|
||||
|
||||
class MoneyLog extends Backend
|
||||
{
|
||||
protected ?object $model = null;
|
||||
|
||||
protected array $withJoinTable = ['user'];
|
||||
protected array|string $preExcludeFields = ['create_time'];
|
||||
protected array|string $quickSearchField = ['user.username', 'user.nickname'];
|
||||
|
||||
protected function initController(Request $request): ?Response
|
||||
{
|
||||
$this->model = new UserMoneyLog();
|
||||
return null;
|
||||
}
|
||||
|
||||
public function add(Request $request): Response
|
||||
{
|
||||
$response = $this->initializeBackend($request);
|
||||
if ($response !== null) return $response;
|
||||
|
||||
if ($request->method() === 'POST') {
|
||||
return $this->_add();
|
||||
}
|
||||
|
||||
$userId = $request->get('userId', $request->post('userId', 0));
|
||||
$user = User::where('id', $userId)->find();
|
||||
if (!$user) {
|
||||
return $this->error(__("The user can't find it"));
|
||||
}
|
||||
return $this->success('', ['user' => $user]);
|
||||
}
|
||||
}
|
||||
98
app/admin/controller/user/Rule.php
Normal file
98
app/admin/controller/user/Rule.php
Normal file
@@ -0,0 +1,98 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace app\admin\controller\user;
|
||||
|
||||
use ba\Tree;
|
||||
use app\common\controller\Backend;
|
||||
use app\admin\model\UserRule;
|
||||
use Webman\Http\Request;
|
||||
use support\Response;
|
||||
|
||||
class Rule extends Backend
|
||||
{
|
||||
protected ?object $model = null;
|
||||
protected Tree $tree;
|
||||
protected array $initValue = [];
|
||||
protected bool $assembleTree = true;
|
||||
protected string $keyword = '';
|
||||
|
||||
protected array|string $preExcludeFields = ['create_time', 'update_time'];
|
||||
protected array|string $defaultSortField = ['weigh' => 'desc'];
|
||||
protected array|string $quickSearchField = 'title';
|
||||
|
||||
protected function initController(Request $request): ?Response
|
||||
{
|
||||
$this->model = new UserRule();
|
||||
$this->tree = Tree::instance();
|
||||
$isTree = filter_var($request->get('isTree', $request->post('isTree', true)), FILTER_VALIDATE_BOOLEAN);
|
||||
$initValue = $request->get('initValue') ?? $request->post('initValue') ?? [];
|
||||
$this->initValue = is_array($initValue) ? array_filter($initValue) : [];
|
||||
$this->keyword = $request->get('quickSearch', $request->post('quickSearch', ''));
|
||||
$this->assembleTree = $isTree && !$this->initValue;
|
||||
return null;
|
||||
}
|
||||
|
||||
public function index(Request $request): Response
|
||||
{
|
||||
$response = $this->initializeBackend($request);
|
||||
if ($response !== null) return $response;
|
||||
|
||||
if ($request->get('select') || $request->post('select')) {
|
||||
return $this->select($request);
|
||||
}
|
||||
|
||||
list($where, , , ) = $this->queryBuilder();
|
||||
$list = $this->model->where($where)->order($this->defaultSortField)->select();
|
||||
|
||||
if ($this->assembleTree) {
|
||||
$list = $this->tree->assembleChild($list->toArray());
|
||||
} else {
|
||||
$list = $list->toArray();
|
||||
}
|
||||
|
||||
return $this->success('', [
|
||||
'list' => $list,
|
||||
'remark' => get_route_remark(),
|
||||
]);
|
||||
}
|
||||
|
||||
public function add(Request $request): Response
|
||||
{
|
||||
$response = $this->initializeBackend($request);
|
||||
if ($response !== null) return $response;
|
||||
return $this->_add();
|
||||
}
|
||||
|
||||
public function edit(Request $request): Response
|
||||
{
|
||||
$response = $this->initializeBackend($request);
|
||||
if ($response !== null) return $response;
|
||||
return $this->_edit();
|
||||
}
|
||||
|
||||
public function del(Request $request): Response
|
||||
{
|
||||
$response = $this->initializeBackend($request);
|
||||
if ($response !== null) return $response;
|
||||
return $this->_del();
|
||||
}
|
||||
|
||||
public function select(Request $request): Response
|
||||
{
|
||||
$response = $this->initializeBackend($request);
|
||||
if ($response !== null) return $response;
|
||||
|
||||
list($where, , , ) = $this->queryBuilder();
|
||||
$list = $this->model->where($where)->order($this->defaultSortField)->select();
|
||||
|
||||
if ($this->assembleTree) {
|
||||
$list = $this->tree->assembleChild($list->toArray());
|
||||
} else {
|
||||
$list = $list->toArray();
|
||||
}
|
||||
|
||||
return $this->success('', ['list' => $list]);
|
||||
}
|
||||
}
|
||||
43
app/admin/controller/user/ScoreLog.php
Normal file
43
app/admin/controller/user/ScoreLog.php
Normal file
@@ -0,0 +1,43 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace app\admin\controller\user;
|
||||
|
||||
use app\admin\model\User;
|
||||
use app\admin\model\UserScoreLog;
|
||||
use app\common\controller\Backend;
|
||||
use Webman\Http\Request;
|
||||
use support\Response;
|
||||
|
||||
class ScoreLog extends Backend
|
||||
{
|
||||
protected ?object $model = null;
|
||||
|
||||
protected array $withJoinTable = ['user'];
|
||||
protected array|string $preExcludeFields = ['create_time'];
|
||||
protected array|string $quickSearchField = ['user.username', 'user.nickname'];
|
||||
|
||||
protected function initController(Request $request): ?Response
|
||||
{
|
||||
$this->model = new UserScoreLog();
|
||||
return null;
|
||||
}
|
||||
|
||||
public function add(Request $request): Response
|
||||
{
|
||||
$response = $this->initializeBackend($request);
|
||||
if ($response !== null) return $response;
|
||||
|
||||
if ($request->method() === 'POST') {
|
||||
return $this->_add();
|
||||
}
|
||||
|
||||
$userId = $request->get('userId', $request->post('userId', 0));
|
||||
$user = User::where('id', $userId)->find();
|
||||
if (!$user) {
|
||||
return $this->error(__("The user can't find it"));
|
||||
}
|
||||
return $this->success('', ['user' => $user]);
|
||||
}
|
||||
}
|
||||
161
app/admin/controller/user/User.php
Normal file
161
app/admin/controller/user/User.php
Normal file
@@ -0,0 +1,161 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace app\admin\controller\user;
|
||||
|
||||
use Throwable;
|
||||
use app\common\controller\Backend;
|
||||
use app\admin\model\User as UserModel;
|
||||
use Webman\Http\Request;
|
||||
use support\Response;
|
||||
|
||||
class User extends Backend
|
||||
{
|
||||
protected ?object $model = null;
|
||||
|
||||
protected array $withJoinTable = ['userGroup'];
|
||||
protected array|string $preExcludeFields = ['last_login_time', 'login_failure', 'password', 'salt'];
|
||||
protected array|string $quickSearchField = ['username', 'nickname', 'id'];
|
||||
|
||||
protected function initController(Request $request): ?Response
|
||||
{
|
||||
$this->model = new UserModel();
|
||||
return null;
|
||||
}
|
||||
|
||||
public function index(Request $request): Response
|
||||
{
|
||||
$response = $this->initializeBackend($request);
|
||||
if ($response !== null) return $response;
|
||||
|
||||
if ($request->get('select') || $request->post('select')) {
|
||||
return $this->select($request);
|
||||
}
|
||||
|
||||
list($where, $alias, $limit, $order) = $this->queryBuilder();
|
||||
$res = $this->model
|
||||
->withoutField('password,salt')
|
||||
->withJoin($this->withJoinTable, $this->withJoinType)
|
||||
->alias($alias)
|
||||
->where($where)
|
||||
->order($order)
|
||||
->paginate($limit);
|
||||
|
||||
return $this->success('', [
|
||||
'list' => $res->items(),
|
||||
'total' => $res->total(),
|
||||
'remark' => get_route_remark(),
|
||||
]);
|
||||
}
|
||||
|
||||
public function add(Request $request): Response
|
||||
{
|
||||
$response = $this->initializeBackend($request);
|
||||
if ($response !== null) return $response;
|
||||
|
||||
if ($request->method() !== 'POST') {
|
||||
return $this->error(__('Parameter error'));
|
||||
}
|
||||
|
||||
$data = $request->post();
|
||||
if (!$data) {
|
||||
return $this->error(__('Parameter %s can not be empty', ['']));
|
||||
}
|
||||
|
||||
$passwd = $data['password'] ?? '';
|
||||
$data = $this->excludeFields($data);
|
||||
|
||||
$result = false;
|
||||
$this->model->startTrans();
|
||||
try {
|
||||
if ($this->modelValidate) {
|
||||
$validateClass = str_replace("\\model\\", "\\validate\\", get_class($this->model));
|
||||
if (class_exists($validateClass)) {
|
||||
$validate = new $validateClass();
|
||||
if ($this->modelSceneValidate) $validate->scene('add');
|
||||
$validate->check($data);
|
||||
}
|
||||
}
|
||||
$result = $this->model->save($data);
|
||||
$this->model->commit();
|
||||
if ($passwd) {
|
||||
$this->model->resetPassword($this->model->id, $passwd);
|
||||
}
|
||||
} catch (Throwable $e) {
|
||||
$this->model->rollback();
|
||||
return $this->error($e->getMessage());
|
||||
}
|
||||
return $result !== false ? $this->success(__('Added successfully')) : $this->error(__('No rows were added'));
|
||||
}
|
||||
|
||||
public function edit(Request $request): Response
|
||||
{
|
||||
$response = $this->initializeBackend($request);
|
||||
if ($response !== null) return $response;
|
||||
|
||||
$pk = $this->model->getPk();
|
||||
$id = $request->post($pk) ?? $request->get($pk);
|
||||
$row = $this->model->find($id);
|
||||
if (!$row) {
|
||||
return $this->error(__('Record not found'));
|
||||
}
|
||||
|
||||
$dataLimitAdminIds = $this->getDataLimitAdminIds();
|
||||
if ($dataLimitAdminIds && !in_array($row[$this->dataLimitField], $dataLimitAdminIds)) {
|
||||
return $this->error(__('You have no permission'));
|
||||
}
|
||||
|
||||
if ($request->method() === 'POST') {
|
||||
$data = $request->post();
|
||||
if (!$data) {
|
||||
return $this->error(__('Parameter %s can not be empty', ['']));
|
||||
}
|
||||
if (!empty($data['password'])) {
|
||||
$this->model->resetPassword($row->id, $data['password']);
|
||||
}
|
||||
$data = $this->excludeFields($data);
|
||||
$result = false;
|
||||
$this->model->startTrans();
|
||||
try {
|
||||
if ($this->modelValidate) {
|
||||
$validateClass = str_replace("\\model\\", "\\validate\\", get_class($this->model));
|
||||
if (class_exists($validateClass)) {
|
||||
$validate = new $validateClass();
|
||||
$validate->scene('edit')->check(array_merge($data, [$pk => $row[$pk]]));
|
||||
}
|
||||
}
|
||||
$result = $row->save($data);
|
||||
$this->model->commit();
|
||||
} catch (Throwable $e) {
|
||||
$this->model->rollback();
|
||||
return $this->error($e->getMessage());
|
||||
}
|
||||
return $result !== false ? $this->success(__('Update successful')) : $this->error(__('No rows updated'));
|
||||
}
|
||||
|
||||
unset($row['password'], $row['salt']);
|
||||
$row['password'] = '';
|
||||
return $this->success('', ['row' => $row]);
|
||||
}
|
||||
|
||||
public function select(Request $request): Response
|
||||
{
|
||||
$response = $this->initializeBackend($request);
|
||||
if ($response !== null) return $response;
|
||||
|
||||
list($where, $alias, $limit, $order) = $this->queryBuilder();
|
||||
$res = $this->model
|
||||
->withoutField('password,salt')
|
||||
->withJoin($this->withJoinTable, $this->withJoinType)
|
||||
->alias($alias)
|
||||
->where($where)
|
||||
->order($order)
|
||||
->paginate($limit);
|
||||
|
||||
return $this->success('', [
|
||||
'list' => $res->items(),
|
||||
'total' => $res->total(),
|
||||
]);
|
||||
}
|
||||
}
|
||||
98
app/admin/lang/en.php
Normal file
98
app/admin/lang/en.php
Normal file
@@ -0,0 +1,98 @@
|
||||
<?php
|
||||
return [
|
||||
'Please login first' => 'Please login first',
|
||||
'No background menu, please contact super administrator!' => 'No background menu, please contact the super administrator!',
|
||||
'You have already logged in. There is no need to log in again~' => 'You have already logged in. There is no need to log in again~',
|
||||
'Login succeeded!' => 'Login succeeded!',
|
||||
'Incorrect user name or password!' => 'Incorrect username or password!',
|
||||
'Login' => 'Login',
|
||||
'Logout' => 'Logout',
|
||||
'Please input correct password' => 'Please enter the correct password',
|
||||
'You have no permission' => 'You have no permission to operate',
|
||||
'Username' => 'Username',
|
||||
'Password' => 'Password',
|
||||
'Nickname' => 'Nickname',
|
||||
'Email' => 'Email',
|
||||
'Mobile' => 'Mobile Number',
|
||||
'Captcha' => 'Captcha',
|
||||
'CaptchaId' => 'Captcha Id',
|
||||
'Please enter the correct verification code' => 'Please enter the correct Captcha!',
|
||||
'Captcha error' => 'Captcha error!',
|
||||
'Parameter %s can not be empty' => 'Parameter %s can not be empty',
|
||||
'Record not found' => 'Record not found',
|
||||
'No rows were added' => 'No rows were added',
|
||||
'No rows were deleted' => 'No rows were deleted',
|
||||
'No rows updated' => 'No rows updated',
|
||||
'Update successful' => 'Update successful!',
|
||||
'Added successfully' => 'Added successfully!',
|
||||
'Deleted successfully' => 'Deleted successfully!',
|
||||
'Parameter error' => 'Parameter error!',
|
||||
'File uploaded successfully' => 'File uploaded successfully',
|
||||
'No files were uploaded' => 'No files were uploaded',
|
||||
'The uploaded file format is not allowed' => 'The uploaded file format is no allowance.',
|
||||
'The uploaded image file is not a valid image' => 'The uploaded image file is not a valid image',
|
||||
'The uploaded file is too large (%sMiB), Maximum file size:%sMiB' => 'The uploaded file is too large (%sMiB), maximum file size:%sMiB',
|
||||
'No files have been uploaded or the file size exceeds the upload limit of the server' => 'No files have been uploaded or the file size exceeds the server upload limit.',
|
||||
'Unknown' => 'Unknown',
|
||||
'Super administrator' => 'Super administrator',
|
||||
'No permission' => 'No permission',
|
||||
'%first% etc. %count% items' => '%first% etc. %count% items',
|
||||
'Please select rules' => 'Please select rules',
|
||||
'You cannot modify your own management group!' => 'You cannot modify your own management group!',
|
||||
'You need to have all permissions of this group to operate this group~' => 'You need to have all permissions of this group to operate this group~',
|
||||
'You need to have all the permissions of the group and have additional permissions before you can operate the group~' => 'You need to have all the permissions of the group and have additional permissions before you can operate the group~',
|
||||
'Role group has all your rights, please contact the upper administrator to add or do not need to add!' => 'Role group has all your rights, please contact the upper administrator to add or do not need to add!',
|
||||
'The group permission node exceeds the range that can be allocated' => 'The group permission node exceeds the range that can be allocated, please refresh and try again~',
|
||||
'Account not exist' => 'Account does not exist',
|
||||
'Account disabled' => 'Account is disabled',
|
||||
'Token login failed' => 'Token login failed',
|
||||
'Username is incorrect' => 'Incorrect username',
|
||||
'Please try again after 1 day' => 'The number of login failures exceeds the limit, please try again after 24 hours',
|
||||
'Password is incorrect' => 'Wrong password',
|
||||
'You are not logged in' => 'You are not logged in',
|
||||
'Cache cleaned~' => 'The cache has been cleaned up, please refresh the page.',
|
||||
'Please use the %s field to sort before operating' => 'Please use the %s field to sort before operating',
|
||||
'Topic format error' => 'Upload storage subdirectory format error!',
|
||||
'Driver %s not supported' => 'Driver %s not supported',
|
||||
'Configuration write failed: %s' => 'Configuration write failed: %s',
|
||||
'Token expiration' => 'Token expired, please login again!',
|
||||
'Method not allowed' => 'Method not allowed',
|
||||
'Variable name' => 'Variable name',
|
||||
'type' => 'Type',
|
||||
'title' => 'Title',
|
||||
'name' => 'Name',
|
||||
'user_id' => 'User ID',
|
||||
'score' => 'Score',
|
||||
'memo' => 'Memo',
|
||||
'money' => 'Money',
|
||||
'Rollback successful' => 'Rollback successful!',
|
||||
'No rows were rolled back' => 'No rows were rolled back',
|
||||
'Avatar modified successfully!' => 'Avatar modified successfully!',
|
||||
'Failed to load cloud data' => 'Failed to load cloud data',
|
||||
'Log start' => 'Log start',
|
||||
'Parse field data' => 'Parse field data',
|
||||
'Generate check' => 'Generate check',
|
||||
'Install module' => 'Install module',
|
||||
'Change module state' => 'Change module state',
|
||||
'Uninstall module' => 'Uninstall module',
|
||||
'Upload module' => 'Upload module',
|
||||
'upload' => 'Upload',
|
||||
'Data table does not exist' => 'Data table does not exist',
|
||||
'Change terminal config' => 'Change terminal config',
|
||||
'Failed to modify the terminal configuration. Please modify the configuration file manually:%s' => 'Failed to modify the terminal configuration. Please modify the configuration file manually: %s',
|
||||
'Clear cache' => 'Clear cache',
|
||||
'The current page configuration item was updated successfully' => 'The current page configuration item was updated successfully!',
|
||||
'This is a test email' => 'This is a test email',
|
||||
'Congratulations, receiving this email means that your email service has been configured correctly' => 'Congratulations, receiving this email means that your email service has been configured correctly',
|
||||
'Test mail sent successfully~' => 'Test mail sent successfully~',
|
||||
'Name' => 'Name',
|
||||
'Data Fields' => 'Data Fields',
|
||||
'Controller' => 'Controller',
|
||||
'Data Table' => 'Data Table',
|
||||
'Primary Key' => 'Primary Key',
|
||||
'Restore successful' => 'Restore successful!',
|
||||
'No rows were restore' => 'No rows were restored',
|
||||
'%d records and files have been deleted' => '%d records and files have been deleted',
|
||||
'Please input correct username' => 'Please enter the correct username',
|
||||
'Group Name Arr' => 'Group Name Arr',
|
||||
];
|
||||
9
app/admin/lang/en/ajax.php
Normal file
9
app/admin/lang/en/ajax.php
Normal file
@@ -0,0 +1,9 @@
|
||||
<?php
|
||||
return [
|
||||
'Failed to switch package manager. Please modify the configuration file manually:%s' => 'Failed to switch package manager, please modify the configuration file manually:%s',
|
||||
'Failed to modify the terminal configuration. Please modify the configuration file manually:%s' => 'Failed to modify the terminal configuration, please modify the configuration file manually:%s',
|
||||
'upload' => 'Upload files',
|
||||
'Change terminal config' => 'Modify terminal configuration',
|
||||
'Clear cache' => 'Clear cache',
|
||||
'Data table does not exist' => 'Data table does not exist',
|
||||
];
|
||||
5
app/admin/lang/en/auth/admin.php
Normal file
5
app/admin/lang/en/auth/admin.php
Normal file
@@ -0,0 +1,5 @@
|
||||
<?php
|
||||
return [
|
||||
'Group Name Arr' => 'Administrator Grouping ',
|
||||
'Please use another administrator account to disable the current account!' => 'Disable the current account, please use another administrator account!',
|
||||
];
|
||||
13
app/admin/lang/en/auth/group.php
Normal file
13
app/admin/lang/en/auth/group.php
Normal file
@@ -0,0 +1,13 @@
|
||||
<?php
|
||||
return [
|
||||
'name' => 'Group name',
|
||||
'Please select rules' => 'Please select rules',
|
||||
'Super administrator' => 'Super administrator',
|
||||
'No permission' => 'No permission',
|
||||
'You cannot modify your own management group!' => 'You cannot modify your own management group!',
|
||||
'You need to have all permissions of this group to operate this group~' => 'You need to have all permissions of this group to operate this group~',
|
||||
'You need to have all the permissions of the group and have additional permissions before you can operate the group~' => 'You need to have all the permissions of the group and have additional permissions before you can operate the group~',
|
||||
'Role group has all your rights, please contact the upper administrator to add or do not need to add!' => 'Role group has all your rights, please contact the upper administrator to add or do not need to add!',
|
||||
'The group permission node exceeds the range that can be allocated' => 'The group permission node exceeds the range that can be allocated, please refresh and try again~',
|
||||
'Remark lang' => 'For system security, the hierarchical relationship of role groups is for reference only. The actual hierarchy is determined by the number of permission nodes: same permissions = peer, containing and having additional permissions = superior. Peers cannot manage peers; superiors can assign their permission nodes to subordinates. For special cases where an admin needs to become a superior, create a virtual permission node.',
|
||||
];
|
||||
6
app/admin/lang/en/auth/menu.php
Normal file
6
app/admin/lang/en/auth/menu.php
Normal file
@@ -0,0 +1,6 @@
|
||||
<?php
|
||||
return [
|
||||
'type' => 'Rule type',
|
||||
'title' => 'Rule title',
|
||||
'name' => 'Rule name',
|
||||
];
|
||||
7
app/admin/lang/en/crud/crud.php
Normal file
7
app/admin/lang/en/crud/crud.php
Normal file
@@ -0,0 +1,7 @@
|
||||
<?php
|
||||
return [
|
||||
'change-field-name fail not exist' => 'Field %s failed to be renamed because the field does not exist in the data table',
|
||||
'del-field fail not exist' => 'Failed to delete field %s because the field does not exist in the data table',
|
||||
'change-field-attr fail not exist' => 'Description Failed to modify the properties of field %s because the field does not exist in the data table',
|
||||
'add-field fail exist' => 'Failed to add field %s because the field already exists in the data table',
|
||||
];
|
||||
4
app/admin/lang/en/dashboard.php
Normal file
4
app/admin/lang/en/dashboard.php
Normal file
@@ -0,0 +1,4 @@
|
||||
<?php
|
||||
return [
|
||||
'Remark lang' => "Open source equals mutual assistance, and needs everyone's support. There are many ways to support it, such as using, recommending, writing tutorials, protecting the ecology, contributing code, answering questions, sharing experiences, donation, sponsorship and so on. Welcome to join us!",
|
||||
];
|
||||
9
app/admin/lang/en/index.php
Normal file
9
app/admin/lang/en/index.php
Normal file
@@ -0,0 +1,9 @@
|
||||
<?php
|
||||
return [
|
||||
'No background menu, please contact super administrator!' => 'No background menu, please contact the super administrator!',
|
||||
'You have already logged in. There is no need to log in again~' => 'You have already logged in, no need to log in again.',
|
||||
'Login succeeded!' => 'Login successful!',
|
||||
'Incorrect user name or password!' => 'Incorrect username or password!',
|
||||
'Login' => 'Login',
|
||||
'Logout' => 'Logout',
|
||||
];
|
||||
6
app/admin/lang/en/routine/admininfo.php
Normal file
6
app/admin/lang/en/routine/admininfo.php
Normal file
@@ -0,0 +1,6 @@
|
||||
<?php
|
||||
return [
|
||||
'Please input correct username' => 'Please enter the correct username',
|
||||
'Please input correct password' => 'Please enter the correct password',
|
||||
'Avatar modified successfully!' => 'Profile picture modified successfully!',
|
||||
];
|
||||
5
app/admin/lang/en/routine/attachment.php
Normal file
5
app/admin/lang/en/routine/attachment.php
Normal file
@@ -0,0 +1,5 @@
|
||||
<?php
|
||||
return [
|
||||
'%d records and files have been deleted' => '%d records and files have been deleted',
|
||||
'remark_text' => 'When the same file is uploaded multiple times, only one copy will be saved to the disk and an attachment record will be added; Deleting an attachment record will automatically delete the corresponding file!',
|
||||
];
|
||||
23
app/admin/lang/en/routine/config.php
Normal file
23
app/admin/lang/en/routine/config.php
Normal file
@@ -0,0 +1,23 @@
|
||||
<?php
|
||||
return [
|
||||
'Basics' => 'Basic configuration',
|
||||
'Mail' => 'Mail configuration',
|
||||
'Config group' => 'Configure grouping',
|
||||
'Site Name' => 'Site name',
|
||||
'Config Quick entrance' => 'Quick configuration entrance',
|
||||
'Record number' => 'Record Number',
|
||||
'Version number' => 'Version Number',
|
||||
'time zone' => 'Time zone',
|
||||
'No access ip' => 'No access IP',
|
||||
'smtp server' => 'SMTP server',
|
||||
'smtp port' => 'SMTP port',
|
||||
'smtp user' => 'SMTP username',
|
||||
'smtp pass' => 'SMTP password',
|
||||
'smtp verification' => 'SMTP verification mode',
|
||||
'smtp sender mail' => 'SMTP sender mailbox',
|
||||
'Variable name' => 'variable name',
|
||||
'Test mail sent successfully~' => 'Test message sent successfully',
|
||||
'This is a test email' => 'This is a test email',
|
||||
'Congratulations, receiving this email means that your email service has been configured correctly' => "Congratulations, when you receive this email, it means that your mail service is configures correctly. This is the email subject, <b>you can use HtmlL!</b> in the main body.",
|
||||
'Backend entrance rule' => 'The background entry must start with / and contain only numbers and letters.',
|
||||
];
|
||||
7
app/admin/lang/en/security/datarecycle.php
Normal file
7
app/admin/lang/en/security/datarecycle.php
Normal file
@@ -0,0 +1,7 @@
|
||||
<?php
|
||||
return [
|
||||
'Name' => 'Rule Name',
|
||||
'Controller' => 'Controller',
|
||||
'Data Table' => 'Corresponding data table',
|
||||
'Primary Key' => 'Data table primary key',
|
||||
];
|
||||
4
app/admin/lang/en/security/datarecyclelog.php
Normal file
4
app/admin/lang/en/security/datarecyclelog.php
Normal file
@@ -0,0 +1,4 @@
|
||||
<?php
|
||||
return [
|
||||
'No rows were restore' => 'No records have been restored',
|
||||
];
|
||||
8
app/admin/lang/en/security/sensitivedata.php
Normal file
8
app/admin/lang/en/security/sensitivedata.php
Normal file
@@ -0,0 +1,8 @@
|
||||
<?php
|
||||
return [
|
||||
'Name' => 'Rule name',
|
||||
'Controller' => 'Controller',
|
||||
'Data Table' => 'Corresponding data table',
|
||||
'Primary Key' => 'Data table primary key',
|
||||
'Data Fields' => 'Sensitive data fields',
|
||||
];
|
||||
4
app/admin/lang/en/security/sensitivedatalog.php
Normal file
4
app/admin/lang/en/security/sensitivedatalog.php
Normal file
@@ -0,0 +1,4 @@
|
||||
<?php
|
||||
return [
|
||||
'No rows were rollback' => 'No records have been roll-back',
|
||||
];
|
||||
8
app/admin/lang/en/user/moneylog.php
Normal file
8
app/admin/lang/en/user/moneylog.php
Normal file
@@ -0,0 +1,8 @@
|
||||
<?php
|
||||
return [
|
||||
'user_id' => 'User',
|
||||
'money' => 'Change amount',
|
||||
'memo' => 'Change Notes',
|
||||
"The user can't find it" => "User does not exist",
|
||||
'Change note cannot be blank' => 'Change Notes cannot be empty',
|
||||
];
|
||||
8
app/admin/lang/en/user/scorelog.php
Normal file
8
app/admin/lang/en/user/scorelog.php
Normal file
@@ -0,0 +1,8 @@
|
||||
<?php
|
||||
return [
|
||||
'user_id' => 'User',
|
||||
'score' => 'Change points',
|
||||
'memo' => 'Change Notes',
|
||||
"The user can't find it" => 'User does not exist',
|
||||
'Change note cannot be blank' => 'Change notes cannot be empty',
|
||||
];
|
||||
117
app/admin/lang/zh-cn.php
Normal file
117
app/admin/lang/zh-cn.php
Normal file
@@ -0,0 +1,117 @@
|
||||
<?php
|
||||
return [
|
||||
'Please login first' => '请先登录!',
|
||||
'No background menu, please contact super administrator!' => '无后台菜单,请联系超级管理员!',
|
||||
'You have already logged in. There is no need to log in again~' => '您已经登录过了,无需重复登录~',
|
||||
'Login succeeded!' => '登录成功!',
|
||||
'Incorrect user name or password!' => '用户名或密码不正确!',
|
||||
'Login' => '登录',
|
||||
'Logout' => '注销登录',
|
||||
'Please input correct password' => '请输入正确的密码',
|
||||
'You have no permission' => '没有权限操作!',
|
||||
'Username' => '用户名',
|
||||
'Password' => '密码',
|
||||
'Nickname' => '昵称',
|
||||
'Email' => '邮箱',
|
||||
'Mobile' => '手机号',
|
||||
'Captcha' => '验证码',
|
||||
'CaptchaId' => '验证码ID',
|
||||
'Please enter the correct verification code' => '请输入正确的验证码!',
|
||||
'Captcha error' => '验证码错误!',
|
||||
'Parameter %s can not be empty' => '参数%s不能为空',
|
||||
'Record not found' => '记录未找到',
|
||||
'No rows were added' => '未添加任何行',
|
||||
'No rows were deleted' => '未删除任何行',
|
||||
'No rows updated' => '未更新任何行',
|
||||
'Update successful' => '更新成功!',
|
||||
'Added successfully' => '添加成功!',
|
||||
'Deleted successfully' => '删除成功!',
|
||||
'Parameter error' => '参数错误!',
|
||||
'Please use the %s field to sort before operating' => '请使用 %s 字段排序后再操作',
|
||||
'File uploaded successfully' => '文件上传成功!',
|
||||
'No files were uploaded' => '没有文件被上传',
|
||||
'The uploaded file format is not allowed' => '上传的文件格式未被允许',
|
||||
'The uploaded image file is not a valid image' => '上传的图片文件不是有效的图像',
|
||||
'The uploaded file is too large (%sMiB), Maximum file size:%sMiB' => '上传的文件太大(%sM),最大文件大小:%sM',
|
||||
'No files have been uploaded or the file size exceeds the upload limit of the server' => '没有文件被上传或文件大小超出服务器上传限制!',
|
||||
'Topic format error' => '上传存储子目录格式错误!',
|
||||
'Driver %s not supported' => '不支持的驱动:%s',
|
||||
'Unknown' => '未知',
|
||||
// 权限类语言包-s
|
||||
'Super administrator' => '超级管理员',
|
||||
'No permission' => '无权限',
|
||||
'%first% etc. %count% items' => '%first% 等 %count% 项',
|
||||
'Please select rules' => '请选择权限',
|
||||
'You cannot modify your own management group!' => '不能修改自己所在的管理组!',
|
||||
'You need to have all permissions of this group to operate this group~' => '您需要拥有该分组的所有权限才可以操作该分组~',
|
||||
'You need to have all the permissions of the group and have additional permissions before you can operate the group~' => '您需要拥有该分组的所有权限且还有额外权限时,才可以操作该分组~',
|
||||
'Role group has all your rights, please contact the upper administrator to add or do not need to add!' => '角色组拥有您的全部权限,请联系上级管理员添加或无需添加!',
|
||||
'The group permission node exceeds the range that can be allocated' => '分组权限节点超出可分配范围,请刷新重试~',
|
||||
'Account not exist' => '帐户不存在',
|
||||
'Account disabled' => '帐户已禁用',
|
||||
'Token login failed' => '令牌登录失败',
|
||||
'Username is incorrect' => '用户名不正确',
|
||||
'Please try again after 1 day' => '登录失败次数超限,请在1天后再试',
|
||||
'Password is incorrect' => '密码不正确',
|
||||
'You are not logged in' => '你没有登录',
|
||||
// 权限类语言包-e
|
||||
// 时间格式化-s
|
||||
'%d second%s ago' => '%d秒前',
|
||||
'%d minute%s ago' => '%d分钟前',
|
||||
'%d hour%s ago' => '%d小时前',
|
||||
'%d day%s ago' => '%d天前',
|
||||
'%d week%s ago' => '%d周前',
|
||||
'%d month%s ago' => '%d月前',
|
||||
'%d year%s ago' => '%d年前',
|
||||
'%d second%s after' => '%d秒后',
|
||||
'%d minute%s after' => '%d分钟后',
|
||||
'%d hour%s after' => '%d小时后',
|
||||
'%d day%s after' => '%d天后',
|
||||
'%d week%s after' => '%d周后',
|
||||
'%d month%s after' => '%d月后',
|
||||
'%d year%s after' => '%d年后',
|
||||
// 时间格式化-e
|
||||
'Cache cleaned~' => '缓存已清理,请刷新后台~',
|
||||
'Please delete the child element first, or use batch deletion' => '请首先删除子元素,或使用批量删除!',
|
||||
'Configuration write failed: %s' => '配置写入失败:%s',
|
||||
'Token expiration' => '登录态过期,请重新登录!',
|
||||
'Method not allowed' => '方法不允许',
|
||||
'Variable name' => '变量名',
|
||||
'type' => '类型',
|
||||
'title' => '标题',
|
||||
'name' => '名称',
|
||||
'user_id' => '用户ID',
|
||||
'score' => '积分',
|
||||
'memo' => '备注',
|
||||
'money' => '金额',
|
||||
'Rollback successful' => '回滚成功!',
|
||||
'No rows were rolled back' => '未回滚任何行',
|
||||
'Avatar modified successfully!' => '头像修改成功!',
|
||||
'Failed to load cloud data' => '加载云端数据失败',
|
||||
'Log start' => '日志开始',
|
||||
'Parse field data' => '解析字段数据',
|
||||
'Generate check' => '生成校验',
|
||||
'Install module' => '安装模块',
|
||||
'Change module state' => '修改模块状态',
|
||||
'Uninstall module' => '卸载模块',
|
||||
'Upload module' => '上传模块',
|
||||
'upload' => '上传',
|
||||
'Data table does not exist' => '数据表不存在',
|
||||
'Change terminal config' => '修改终端配置',
|
||||
'Failed to modify the terminal configuration. Please modify the configuration file manually:%s' => '修改终端配置失败,请手动修改配置文件:%s',
|
||||
'Clear cache' => '清除缓存',
|
||||
'The current page configuration item was updated successfully' => '当前页配置项更新成功!',
|
||||
'This is a test email' => '这是一封测试邮件',
|
||||
'Congratulations, receiving this email means that your email service has been configured correctly' => '恭喜,收到此邮件说明您的邮件服务已配置正确',
|
||||
'Test mail sent successfully~' => '测试邮件发送成功~',
|
||||
'Name' => '名称',
|
||||
'Data Fields' => '数据字段',
|
||||
'Controller' => '控制器',
|
||||
'Data Table' => '数据表',
|
||||
'Primary Key' => '主键',
|
||||
'Restore successful' => '恢复成功!',
|
||||
'No rows were restore' => '未恢复任何行',
|
||||
'%d records and files have been deleted' => '已删除%d条记录和文件',
|
||||
'Please input correct username' => '请输入正确的用户名',
|
||||
'Group Name Arr' => '分组名称数组',
|
||||
];
|
||||
12
app/admin/lang/zh-cn/ajax.php
Normal file
12
app/admin/lang/zh-cn/ajax.php
Normal file
@@ -0,0 +1,12 @@
|
||||
<?php
|
||||
return [
|
||||
'Start the database migration' => '开始进行数据库迁移',
|
||||
'Start formatting the web project code' => '开始格式化前端代码(失败无影响,代码编辑器内按需的手动格式化即可)',
|
||||
'Start installing the composer dependencies' => '开始安装服务端依赖',
|
||||
'Start executing the build command of the web project' => '开始执行 web 工程的 build 命令,成功后会自动将构建产物移动至 根目录/public 目录下',
|
||||
'Failed to modify the terminal configuration. Please modify the configuration file manually:%s' => '修改终端配置失败,请手动修改配置文件:%s',
|
||||
'upload' => '上传文件',
|
||||
'Change terminal config' => '修改终端配置',
|
||||
'Clear cache' => '清理缓存',
|
||||
'Data table does not exist' => '数据表不存在~',
|
||||
];
|
||||
6
app/admin/lang/zh-cn/auth/admin.php
Normal file
6
app/admin/lang/zh-cn/auth/admin.php
Normal file
@@ -0,0 +1,6 @@
|
||||
<?php
|
||||
return [
|
||||
'Group Name Arr' => '管理员分组',
|
||||
'Please use another administrator account to disable the current account!' => '请使用另外的管理员账户禁用当前账户!',
|
||||
'You have no permission to add an administrator to this group!' => '您没有权限向此分组添加管理员!',
|
||||
];
|
||||
13
app/admin/lang/zh-cn/auth/group.php
Normal file
13
app/admin/lang/zh-cn/auth/group.php
Normal file
@@ -0,0 +1,13 @@
|
||||
<?php
|
||||
return [
|
||||
'name' => '组别名称',
|
||||
'Please select rules' => '请选择权限',
|
||||
'Super administrator' => '超级管理员',
|
||||
'No permission' => '无权限',
|
||||
'You cannot modify your own management group!' => '不能修改自己所在的管理组!',
|
||||
'You need to have all permissions of this group to operate this group~' => '您需要拥有该分组的所有权限才可以操作该分组~',
|
||||
'You need to have all the permissions of the group and have additional permissions before you can operate the group~' => '您需要拥有该分组的所有权限且还有额外权限时,才可以操作该分组~',
|
||||
'Role group has all your rights, please contact the upper administrator to add or do not need to add!' => '角色组拥有您的全部权限,请联系上级管理员添加或无需添加!',
|
||||
'The group permission node exceeds the range that can be allocated' => '分组权限节点超出可分配范围,请刷新重试~',
|
||||
'Remark lang' => '为保障系统安全,角色组本身的上下级关系仅供参考,系统的实际上下级划分是根据`权限多寡`来确定的,两位管理员的权限节点:相同被认为是`同级`、包含且有额外权限才被认为是`上级`,同级不可管理同级,上级可为下级分配自己拥有的权限节点;若有特殊情况管理员需转`上级`,可建立一个虚拟权限节点',
|
||||
];
|
||||
6
app/admin/lang/zh-cn/auth/rule.php
Normal file
6
app/admin/lang/zh-cn/auth/rule.php
Normal file
@@ -0,0 +1,6 @@
|
||||
<?php
|
||||
return [
|
||||
'type' => '规则类型',
|
||||
'title' => '规则标题',
|
||||
'name' => '规则名称',
|
||||
];
|
||||
11
app/admin/lang/zh-cn/crud/crud.php
Normal file
11
app/admin/lang/zh-cn/crud/crud.php
Normal file
@@ -0,0 +1,11 @@
|
||||
<?php
|
||||
return [
|
||||
'Parse field data' => 'CRUD代码生成-解析字段数据',
|
||||
'Log start' => 'CRUD代码生成-从历史记录开始',
|
||||
'Generate check' => 'CRUD代码生成-生成前预检',
|
||||
'change-field-name fail not exist' => '字段 %s 改名失败,数据表内不存在该字段',
|
||||
'del-field fail not exist' => '字段 %s 删除失败,数据表内不存在该字段',
|
||||
'change-field-attr fail not exist' => '修改字段 %s 的属性失败,数据表内不存在该字段',
|
||||
'add-field fail exist' => '添加字段 %s 失败,数据表内已经存在该字段',
|
||||
'Failed to load cloud data' => '加载云端数据失败,请稍后重试!',
|
||||
];
|
||||
4
app/admin/lang/zh-cn/dashboard.php
Normal file
4
app/admin/lang/zh-cn/dashboard.php
Normal file
@@ -0,0 +1,4 @@
|
||||
<?php
|
||||
return [
|
||||
'Remark lang' => '开源等于互助;开源需要大家一起来支持,支持的方式有很多种,比如使用、推荐、写教程、保护生态、贡献代码、回答问题、分享经验、打赏赞助等;欢迎您加入我们!',
|
||||
];
|
||||
9
app/admin/lang/zh-cn/index.php
Normal file
9
app/admin/lang/zh-cn/index.php
Normal file
@@ -0,0 +1,9 @@
|
||||
<?php
|
||||
return [
|
||||
'No background menu, please contact super administrator!' => '无后台菜单,请联系超级管理员!',
|
||||
'You have already logged in. There is no need to log in again~' => '您已经登录过了,无需重复登录~',
|
||||
'Login succeeded!' => '登录成功!',
|
||||
'Incorrect user name or password!' => '用户名或密码不正确!',
|
||||
'Login' => '登录',
|
||||
'Logout' => '注销登录',
|
||||
];
|
||||
29
app/admin/lang/zh-cn/module.php
Normal file
29
app/admin/lang/zh-cn/module.php
Normal file
@@ -0,0 +1,29 @@
|
||||
<?php
|
||||
return [
|
||||
'Order not found' => '订单找不到啦!',
|
||||
'Module already exists' => '模块已存在!',
|
||||
'package download failed' => '包下载失败!',
|
||||
'package check failed' => '包检查失败!',
|
||||
'No permission to write temporary files' => '没有权限写入临时文件!',
|
||||
'Zip file not found' => '找不到压缩包文件',
|
||||
'Unable to open the zip file' => '无法打开压缩包文件',
|
||||
'Unable to extract ZIP file' => '无法提取ZIP文件',
|
||||
'Unable to package zip file' => '无法打包zip文件',
|
||||
'Basic configuration of the Module is incomplete' => '模块基础配置不完整',
|
||||
'Module package file does not exist' => '模块包文件不存在',
|
||||
'Module file conflicts' => '模块文件存在冲突,请手动处理!',
|
||||
'Configuration file has no write permission' => '配置文件无写入权限',
|
||||
'The current state of the module cannot be set to disabled' => '模块当前状态无法设定为禁用',
|
||||
'The current state of the module cannot be set to enabled' => '模块当前状态无法设定为启用',
|
||||
'Module file updated' => '模块文件有更新',
|
||||
'Please disable the module first' => '请先禁用模块',
|
||||
'Please disable the module before updating' => '更新前请先禁用模块',
|
||||
'The directory required by the module is occupied' => '模块所需目录已被占用',
|
||||
'Install module' => '安装模块',
|
||||
'Unload module' => '卸载模块',
|
||||
'Update module' => '更新模块',
|
||||
'Change module state' => '改变模块状态',
|
||||
'Upload install module' => '上传安装模块',
|
||||
'Please login to the official website account first' => '请先使用BuildAdmin官网账户登录到模块市场~',
|
||||
'composer config %s conflict' => 'composer 配置项 %s 存在冲突',
|
||||
];
|
||||
6
app/admin/lang/zh-cn/routine/admininfo.php
Normal file
6
app/admin/lang/zh-cn/routine/admininfo.php
Normal file
@@ -0,0 +1,6 @@
|
||||
<?php
|
||||
return [
|
||||
'Please input correct username' => '请输入正确的用户名',
|
||||
'Please input correct password' => '请输入正确的密码',
|
||||
'Avatar modified successfully!' => '头像修改成功!',
|
||||
];
|
||||
5
app/admin/lang/zh-cn/routine/attachment.php
Normal file
5
app/admin/lang/zh-cn/routine/attachment.php
Normal file
@@ -0,0 +1,5 @@
|
||||
<?php
|
||||
return [
|
||||
'Remark lang' => '同一文件被多次上传时,只会保存一份至磁盘和增加一条附件记录;删除附件记录,将自动删除对应文件!',
|
||||
'%d records and files have been deleted' => '删除了%d条记录和文件',
|
||||
];
|
||||
25
app/admin/lang/zh-cn/routine/config.php
Normal file
25
app/admin/lang/zh-cn/routine/config.php
Normal file
@@ -0,0 +1,25 @@
|
||||
<?php
|
||||
return [
|
||||
'Basics' => '基础配置',
|
||||
'Mail' => '邮件配置',
|
||||
'Config group' => '配置分组',
|
||||
'Site Name' => '站点名称',
|
||||
'Backend entrance' => '自定义后台入口',
|
||||
'Config Quick entrance' => '快捷配置入口',
|
||||
'Record number' => '备案号',
|
||||
'Version number' => '版本号',
|
||||
'time zone' => '时区',
|
||||
'No access ip' => '禁止访问IP',
|
||||
'smtp server' => 'SMTP 服务器',
|
||||
'smtp port' => 'SMTP 端口',
|
||||
'smtp user' => 'SMTP 用户名',
|
||||
'smtp pass' => 'SMTP 密码',
|
||||
'smtp verification' => 'SMTP 验证方式',
|
||||
'smtp sender mail' => 'SMTP 发件人邮箱',
|
||||
'Variable name' => '变量名',
|
||||
'Test mail sent successfully~' => '测试邮件发送成功~',
|
||||
'This is a test email' => '这是一封测试邮件',
|
||||
'Congratulations, receiving this email means that your email service has been configured correctly' => '恭喜您,收到此邮件代表您的邮件服务已配置正确;这是邮件主体 <b>在主体中可以使用Html!</b>',
|
||||
'The current page configuration item was updated successfully' => '当前页配置项更新成功!',
|
||||
'Backend entrance rule' => '后台入口请以 / 开头,且只包含数字和字母。',
|
||||
];
|
||||
8
app/admin/lang/zh-cn/security/datarecycle.php
Normal file
8
app/admin/lang/zh-cn/security/datarecycle.php
Normal file
@@ -0,0 +1,8 @@
|
||||
<?php
|
||||
return [
|
||||
'Name' => '规则名称',
|
||||
'Controller' => '控制器',
|
||||
'Data Table' => '对应数据表',
|
||||
'Primary Key' => '数据表主键',
|
||||
'Remark lang' => '在此定义需要回收的数据,实现数据自动统一回收',
|
||||
];
|
||||
4
app/admin/lang/zh-cn/security/datarecyclelog.php
Normal file
4
app/admin/lang/zh-cn/security/datarecyclelog.php
Normal file
@@ -0,0 +1,4 @@
|
||||
<?php
|
||||
return [
|
||||
'No rows were restore' => '没有记录被还原',
|
||||
];
|
||||
9
app/admin/lang/zh-cn/security/sensitivedata.php
Normal file
9
app/admin/lang/zh-cn/security/sensitivedata.php
Normal file
@@ -0,0 +1,9 @@
|
||||
<?php
|
||||
return [
|
||||
'Name' => '规则名称',
|
||||
'Controller' => '控制器',
|
||||
'Data Table' => '对应数据表',
|
||||
'Primary Key' => '数据表主键',
|
||||
'Data Fields' => '敏感数据字段',
|
||||
'Remark lang' => '在此定义需要保护的敏感字段,随后系统将自动监听该字段的修改操作,并提供了敏感字段的修改回滚功能',
|
||||
];
|
||||
4
app/admin/lang/zh-cn/security/sensitivedatalog.php
Normal file
4
app/admin/lang/zh-cn/security/sensitivedatalog.php
Normal file
@@ -0,0 +1,4 @@
|
||||
<?php
|
||||
return [
|
||||
'No rows were rollback' => '没有记录被回滚',
|
||||
];
|
||||
8
app/admin/lang/zh-cn/user/moneylog.php
Normal file
8
app/admin/lang/zh-cn/user/moneylog.php
Normal file
@@ -0,0 +1,8 @@
|
||||
<?php
|
||||
return [
|
||||
'user_id' => '用户',
|
||||
'money' => '变更金额',
|
||||
'memo' => '变更备注',
|
||||
"The user can't find it" => '用户找不到啦',
|
||||
'Change note cannot be blank' => '变更备注不能为空',
|
||||
];
|
||||
8
app/admin/lang/zh-cn/user/scorelog.php
Normal file
8
app/admin/lang/zh-cn/user/scorelog.php
Normal file
@@ -0,0 +1,8 @@
|
||||
<?php
|
||||
return [
|
||||
'user_id' => '用户',
|
||||
'score' => '变更积分',
|
||||
'memo' => '变更备注',
|
||||
"The user can't find it" => '用户找不到啦',
|
||||
'Change note cannot be blank' => '变更备注不能为空',
|
||||
];
|
||||
369
app/admin/library/Auth.php
Normal file
369
app/admin/library/Auth.php
Normal file
@@ -0,0 +1,369 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace app\admin\library;
|
||||
|
||||
use Throwable;
|
||||
use ba\Random;
|
||||
use support\think\Db;
|
||||
use app\admin\model\Admin;
|
||||
use app\common\facade\Token;
|
||||
use app\admin\model\AdminGroup;
|
||||
|
||||
/**
|
||||
* 管理员权限类(Webman 迁移版)
|
||||
* @property int $id 管理员ID
|
||||
* @property string $username 管理员用户名
|
||||
* @property string $nickname 管理员昵称
|
||||
* @property string $email 管理员邮箱
|
||||
* @property string $mobile 管理员手机号
|
||||
*/
|
||||
class Auth extends \ba\Auth
|
||||
{
|
||||
public const LOGIN_RESPONSE_CODE = 303;
|
||||
public const NEED_LOGIN = 'need login';
|
||||
public const LOGGED_IN = 'logged in';
|
||||
public const TOKEN_TYPE = 'admin';
|
||||
|
||||
protected bool $loginEd = false;
|
||||
protected string $error = '';
|
||||
protected ?Admin $model = null;
|
||||
protected string $token = '';
|
||||
protected string $refreshToken = '';
|
||||
protected int $keepTime = 86400;
|
||||
protected int $refreshTokenKeepTime = 2592000;
|
||||
protected array $allowFields = ['id', 'username', 'nickname', 'avatar', 'last_login_time'];
|
||||
|
||||
public function __construct(array $config = [])
|
||||
{
|
||||
parent::__construct($config);
|
||||
$this->setKeepTime((int) config('buildadmin.admin_token_keep_time', 86400 * 3));
|
||||
}
|
||||
|
||||
public function __get($name): mixed
|
||||
{
|
||||
return $this->model?->$name;
|
||||
}
|
||||
|
||||
/**
|
||||
* 初始化(Webman:从 request 获取或创建新实例)
|
||||
*/
|
||||
public static function instance(array $options = []): Auth
|
||||
{
|
||||
$request = function_exists('request') ? request() : null;
|
||||
if ($request !== null && isset($request->adminAuth) && $request->adminAuth instanceof Auth) {
|
||||
return $request->adminAuth;
|
||||
}
|
||||
$auth = new static($options);
|
||||
if ($request !== null) {
|
||||
$request->adminAuth = $auth;
|
||||
}
|
||||
return $auth;
|
||||
}
|
||||
|
||||
public function init(string $token): bool
|
||||
{
|
||||
$tokenData = Token::get($token);
|
||||
if ($tokenData) {
|
||||
Token::tokenExpirationCheck($tokenData);
|
||||
|
||||
$userId = (int) $tokenData['user_id'];
|
||||
if ($tokenData['type'] === self::TOKEN_TYPE && $userId > 0) {
|
||||
$this->model = Admin::where('id', $userId)->find();
|
||||
if (!$this->model) {
|
||||
$this->setError('Account not exist');
|
||||
return false;
|
||||
}
|
||||
if ($this->model['status'] !== 'enable') {
|
||||
$this->setError('Account disabled');
|
||||
return false;
|
||||
}
|
||||
$this->token = $token;
|
||||
$this->loginSuccessful();
|
||||
return true;
|
||||
}
|
||||
}
|
||||
$this->setError('Token login failed');
|
||||
$this->reset();
|
||||
return false;
|
||||
}
|
||||
|
||||
public function login(string $username, string $password, bool $keep = false): bool
|
||||
{
|
||||
$this->model = Admin::where('username', $username)->find();
|
||||
if (!$this->model) {
|
||||
$this->setError('Username is incorrect');
|
||||
return false;
|
||||
}
|
||||
if ($this->model->status === 'disable') {
|
||||
$this->setError('Account disabled');
|
||||
return false;
|
||||
}
|
||||
|
||||
$adminLoginRetry = config('buildadmin.admin_login_retry');
|
||||
if ($adminLoginRetry) {
|
||||
$lastLoginTime = $this->model->getData('last_login_time');
|
||||
if ($lastLoginTime) {
|
||||
if ($this->model->login_failure > 0 && time() - $lastLoginTime >= 86400) {
|
||||
$this->model->login_failure = 0;
|
||||
$this->model->save();
|
||||
$this->model = Admin::where('username', $username)->find();
|
||||
}
|
||||
|
||||
if ($this->model->login_failure >= $adminLoginRetry) {
|
||||
$this->setError('Please try again after 1 day');
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!verify_password($password, $this->model->password, ['salt' => $this->model->salt ?? ''])) {
|
||||
$this->loginFailed();
|
||||
$this->setError('Password is incorrect');
|
||||
return false;
|
||||
}
|
||||
|
||||
if (config('buildadmin.admin_sso')) {
|
||||
Token::clear(self::TOKEN_TYPE, $this->model->id);
|
||||
Token::clear(self::TOKEN_TYPE . '-refresh', $this->model->id);
|
||||
}
|
||||
|
||||
if ($keep) {
|
||||
$this->setRefreshToken($this->refreshTokenKeepTime);
|
||||
}
|
||||
if (!$this->loginSuccessful()) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
public function setRefreshToken(int $keepTime = 0): void
|
||||
{
|
||||
$this->refreshToken = Random::uuid();
|
||||
Token::set($this->refreshToken, self::TOKEN_TYPE . '-refresh', $this->model->id, $keepTime);
|
||||
}
|
||||
|
||||
public function loginSuccessful(): bool
|
||||
{
|
||||
if (!$this->model) {
|
||||
return false;
|
||||
}
|
||||
$this->model->startTrans();
|
||||
try {
|
||||
$this->model->login_failure = 0;
|
||||
$this->model->last_login_time = time();
|
||||
$this->model->last_login_ip = function_exists('request') && request() ? request()->getRealIp() : '';
|
||||
$this->model->save();
|
||||
$this->loginEd = true;
|
||||
|
||||
if (!$this->token) {
|
||||
$this->token = Random::uuid();
|
||||
Token::set($this->token, self::TOKEN_TYPE, $this->model->id, $this->keepTime);
|
||||
}
|
||||
$this->model->commit();
|
||||
} catch (Throwable $e) {
|
||||
$this->model->rollback();
|
||||
$this->setError($e->getMessage());
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
public function loginFailed(): bool
|
||||
{
|
||||
if (!$this->model) {
|
||||
return false;
|
||||
}
|
||||
$this->model->startTrans();
|
||||
try {
|
||||
$this->model->login_failure++;
|
||||
$this->model->last_login_time = time();
|
||||
$this->model->last_login_ip = function_exists('request') && request() ? request()->getRealIp() : '';
|
||||
$this->model->save();
|
||||
$this->model->commit();
|
||||
} catch (Throwable $e) {
|
||||
$this->model->rollback();
|
||||
}
|
||||
return $this->reset();
|
||||
}
|
||||
|
||||
public function logout(): bool
|
||||
{
|
||||
if (!$this->loginEd) {
|
||||
$this->setError('You are not logged in');
|
||||
return false;
|
||||
}
|
||||
return $this->reset();
|
||||
}
|
||||
|
||||
public function isLogin(): bool
|
||||
{
|
||||
return $this->loginEd;
|
||||
}
|
||||
|
||||
public function getAdmin(): Admin
|
||||
{
|
||||
return $this->model;
|
||||
}
|
||||
|
||||
public function getToken(): string
|
||||
{
|
||||
return $this->token;
|
||||
}
|
||||
|
||||
public function getRefreshToken(): string
|
||||
{
|
||||
return $this->refreshToken;
|
||||
}
|
||||
|
||||
public function getInfo(): array
|
||||
{
|
||||
if (!$this->model) {
|
||||
return [];
|
||||
}
|
||||
$info = $this->model->toArray();
|
||||
$info = array_intersect_key($info, array_flip($this->getAllowFields()));
|
||||
// 与 ThinkPHP 一致:token 为主认证令牌,refresh_token 仅 keep=true 时有值
|
||||
$token = $this->token;
|
||||
if (!$token && $this->loginEd) {
|
||||
$token = Random::uuid();
|
||||
Token::set($token, self::TOKEN_TYPE, $this->model->id, $this->keepTime);
|
||||
$this->token = $token;
|
||||
}
|
||||
$info['token'] = $token ?: '';
|
||||
$info['refresh_token'] = $this->refreshToken ?: '';
|
||||
// last_login_time 与 ThinkPHP 一致返回整数时间戳
|
||||
if (isset($info['last_login_time'])) {
|
||||
$info['last_login_time'] = (int) $info['last_login_time'];
|
||||
}
|
||||
return $info;
|
||||
}
|
||||
|
||||
public function getAllowFields(): array
|
||||
{
|
||||
return $this->allowFields;
|
||||
}
|
||||
|
||||
public function setAllowFields($fields): void
|
||||
{
|
||||
$this->allowFields = $fields;
|
||||
}
|
||||
|
||||
public function setKeepTime(int $keepTime = 0): void
|
||||
{
|
||||
$this->keepTime = $keepTime;
|
||||
}
|
||||
|
||||
public function check(string $name, int $uid = 0, string $relation = 'or', string $mode = 'url'): bool
|
||||
{
|
||||
return parent::check($name, $uid ?: $this->id, $relation, $mode);
|
||||
}
|
||||
|
||||
public function getGroups(int $uid = 0): array
|
||||
{
|
||||
return parent::getGroups($uid ?: $this->id);
|
||||
}
|
||||
|
||||
public function getRuleList(int $uid = 0): array
|
||||
{
|
||||
return parent::getRuleList($uid ?: $this->id);
|
||||
}
|
||||
|
||||
public function getRuleIds(int $uid = 0): array
|
||||
{
|
||||
return parent::getRuleIds($uid ?: $this->id);
|
||||
}
|
||||
|
||||
public function getMenus(int $uid = 0): array
|
||||
{
|
||||
return parent::getMenus($uid ?: $this->id);
|
||||
}
|
||||
|
||||
public function isSuperAdmin(): bool
|
||||
{
|
||||
return in_array('*', $this->getRuleIds());
|
||||
}
|
||||
|
||||
public function getAdminChildGroups(): array
|
||||
{
|
||||
$groupIds = Db::name('admin_group_access')
|
||||
->where('uid', $this->id)
|
||||
->select();
|
||||
$children = [];
|
||||
foreach ($groupIds as $group) {
|
||||
$this->getGroupChildGroups($group['group_id'], $children);
|
||||
}
|
||||
return array_unique($children);
|
||||
}
|
||||
|
||||
public function getGroupChildGroups(int $groupId, array &$children): void
|
||||
{
|
||||
$childrenTemp = AdminGroup::where('pid', $groupId)
|
||||
->where('status', 1)
|
||||
->select();
|
||||
foreach ($childrenTemp as $item) {
|
||||
$children[] = $item['id'];
|
||||
$this->getGroupChildGroups($item['id'], $children);
|
||||
}
|
||||
}
|
||||
|
||||
public function getGroupAdmins(array $groups): array
|
||||
{
|
||||
return Db::name('admin_group_access')
|
||||
->where('group_id', 'in', $groups)
|
||||
->column('uid');
|
||||
}
|
||||
|
||||
public function getAllAuthGroups(string $dataLimit, array $groupQueryWhere = [['status', '=', 1]]): array
|
||||
{
|
||||
$rules = $this->getRuleIds();
|
||||
$allAuthGroups = [];
|
||||
$groups = AdminGroup::where($groupQueryWhere)->select();
|
||||
foreach ($groups as $group) {
|
||||
if ($group['rules'] === '*') {
|
||||
continue;
|
||||
}
|
||||
$groupRules = explode(',', $group['rules']);
|
||||
$all = true;
|
||||
foreach ($groupRules as $groupRule) {
|
||||
if (!in_array($groupRule, $rules)) {
|
||||
$all = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if ($all) {
|
||||
if ($dataLimit === 'allAuth' || ($dataLimit === 'allAuthAndOthers' && array_diff($rules, $groupRules))) {
|
||||
$allAuthGroups[] = $group['id'];
|
||||
}
|
||||
}
|
||||
}
|
||||
return $allAuthGroups;
|
||||
}
|
||||
|
||||
public function setError($error): Auth
|
||||
{
|
||||
$this->error = $error;
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getError(): string
|
||||
{
|
||||
return $this->error ? __($this->error) : '';
|
||||
}
|
||||
|
||||
protected function reset(bool $deleteToken = true): bool
|
||||
{
|
||||
if ($deleteToken && $this->token) {
|
||||
Token::delete($this->token);
|
||||
}
|
||||
|
||||
$this->token = '';
|
||||
$this->loginEd = false;
|
||||
$this->model = null;
|
||||
$this->refreshToken = '';
|
||||
$this->setError('');
|
||||
$this->setKeepTime((int) config('buildadmin.admin_token_keep_time', 86400 * 3));
|
||||
return true;
|
||||
}
|
||||
}
|
||||
928
app/admin/library/crud/Helper.php
Normal file
928
app/admin/library/crud/Helper.php
Normal file
@@ -0,0 +1,928 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace app\admin\library\crud;
|
||||
|
||||
use Throwable;
|
||||
use ba\Filesystem;
|
||||
use ba\TableManager;
|
||||
use app\common\library\Menu;
|
||||
use app\admin\model\AdminRule;
|
||||
use app\admin\model\CrudLog;
|
||||
use ba\Exception as BaException;
|
||||
use Phinx\Db\Adapter\MysqlAdapter;
|
||||
use Phinx\Db\Adapter\AdapterInterface;
|
||||
use support\think\Db;
|
||||
|
||||
/**
|
||||
* CRUD 代码生成器 Helper(Webman 迁移版)
|
||||
*/
|
||||
class Helper
|
||||
{
|
||||
protected static array $reservedKeywords = [
|
||||
'abstract', 'and', 'array', 'as', 'break', 'callable', 'case', 'catch', 'class', 'clone',
|
||||
'const', 'continue', 'declare', 'default', 'die', 'do', 'echo', 'else', 'elseif', 'empty',
|
||||
'enddeclare', 'endfor', 'endforeach', 'endif', 'endswitch', 'endwhile', 'eval', 'exit', 'extends',
|
||||
'final', 'for', 'foreach', 'function', 'global', 'goto', 'if', 'implements', 'include', 'include_once',
|
||||
'instanceof', 'insteadof', 'interface', 'isset', 'list', 'namespace', 'new', 'or', 'print', 'private',
|
||||
'protected', 'public', 'require', 'require_once', 'return', 'static', 'switch', 'throw', 'trait', 'try',
|
||||
'unset', 'use', 'var', 'while', 'xor', 'yield', 'match', 'readonly', 'fn',
|
||||
];
|
||||
|
||||
protected static array $parseNamePresets = [
|
||||
'controller' => [
|
||||
'user' => ['user', 'user'],
|
||||
'admin' => ['auth', 'admin'],
|
||||
'admin_group' => ['auth', 'group'],
|
||||
'attachment' => ['routine', 'attachment'],
|
||||
'admin_rule' => ['auth', 'rule'],
|
||||
],
|
||||
'model' => [],
|
||||
'validate' => [],
|
||||
];
|
||||
|
||||
public static array $menuChildren = [
|
||||
['type' => 'button', 'title' => '查看', 'name' => '/index', 'status' => 1],
|
||||
['type' => 'button', 'title' => '添加', 'name' => '/add', 'status' => 1],
|
||||
['type' => 'button', 'title' => '编辑', 'name' => '/edit', 'status' => 1],
|
||||
['type' => 'button', 'title' => '删除', 'name' => '/del', 'status' => 1],
|
||||
['type' => 'button', 'title' => '快速排序', 'name' => '/sortable', 'status' => 1],
|
||||
];
|
||||
|
||||
protected static array $inputTypeRule = [
|
||||
['type' => ['tinyint', 'int', 'enum'], 'suffix' => ['switch', 'toggle'], 'value' => 'switch'],
|
||||
['column_type' => ['tinyint(1)', 'char(1)', 'tinyint(1) unsigned'], 'suffix' => ['switch', 'toggle'], 'value' => 'switch'],
|
||||
['type' => ['longtext', 'text', 'mediumtext', 'smalltext', 'tinytext', 'bigtext'], 'suffix' => ['content', 'editor'], 'value' => 'editor'],
|
||||
['type' => ['varchar'], 'suffix' => ['textarea', 'multiline', 'rows'], 'value' => 'textarea'],
|
||||
['suffix' => ['array'], 'value' => 'array'],
|
||||
['type' => ['int'], 'suffix' => ['time', 'datetime'], 'value' => 'timestamp'],
|
||||
['type' => ['datetime', 'timestamp'], 'value' => 'datetime'],
|
||||
['type' => ['date'], 'value' => 'date'],
|
||||
['type' => ['year'], 'value' => 'year'],
|
||||
['type' => ['time'], 'value' => 'time'],
|
||||
['suffix' => ['select', 'list', 'data'], 'value' => 'select'],
|
||||
['suffix' => ['selects', 'multi', 'lists'], 'value' => 'selects'],
|
||||
['suffix' => ['_id'], 'value' => 'remoteSelect'],
|
||||
['suffix' => ['_ids'], 'value' => 'remoteSelects'],
|
||||
['suffix' => ['city'], 'value' => 'city'],
|
||||
['suffix' => ['image', 'avatar'], 'value' => 'image'],
|
||||
['suffix' => ['images', 'avatars'], 'value' => 'images'],
|
||||
['suffix' => ['file'], 'value' => 'file'],
|
||||
['suffix' => ['files'], 'value' => 'files'],
|
||||
['suffix' => ['icon'], 'value' => 'icon'],
|
||||
['column_type' => ['tinyint(1)', 'char(1)', 'tinyint(1) unsigned'], 'suffix' => ['status', 'state', 'type'], 'value' => 'radio'],
|
||||
['suffix' => ['number', 'int', 'num'], 'value' => 'number'],
|
||||
['type' => ['bigint', 'int', 'mediumint', 'smallint', 'tinyint', 'decimal', 'double', 'float'], 'value' => 'number'],
|
||||
['type' => ['longtext', 'text', 'mediumtext', 'smalltext', 'tinytext', 'bigtext'], 'value' => 'textarea'],
|
||||
['type' => ['enum'], 'value' => 'radio'],
|
||||
['type' => ['set'], 'value' => 'checkbox'],
|
||||
['suffix' => ['color'], 'value' => 'color'],
|
||||
];
|
||||
|
||||
protected static array $parseWebDirPresets = [
|
||||
'lang' => [],
|
||||
'views' => [
|
||||
'user' => ['user', 'user'],
|
||||
'admin' => ['auth', 'admin'],
|
||||
'admin_group' => ['auth', 'group'],
|
||||
'attachment' => ['routine', 'attachment'],
|
||||
'admin_rule' => ['auth', 'rule'],
|
||||
],
|
||||
];
|
||||
|
||||
protected static string $createTimeField = 'create_time';
|
||||
protected static string $updateTimeField = 'update_time';
|
||||
|
||||
protected static array $attrType = [
|
||||
'controller' => [
|
||||
'preExcludeFields' => 'array|string',
|
||||
'quickSearchField' => 'string|array',
|
||||
'withJoinTable' => 'array',
|
||||
'defaultSortField' => 'string|array',
|
||||
'weighField' => 'string',
|
||||
],
|
||||
];
|
||||
|
||||
public static function getDictData(array &$dict, array $field, string $lang, string $translationPrefix = ''): array
|
||||
{
|
||||
if (!$field['comment']) return [];
|
||||
$comment = str_replace([',', ':'], [',', ':'], $field['comment']);
|
||||
if (stripos($comment, ':') !== false && stripos($comment, ',') && stripos($comment, '=') !== false) {
|
||||
[$fieldTitle, $item] = explode(':', $comment);
|
||||
$dict[$translationPrefix . $field['name']] = $lang == 'en' ? $field['name'] : $fieldTitle;
|
||||
foreach (explode(',', $item) as $v) {
|
||||
$valArr = explode('=', $v);
|
||||
if (count($valArr) == 2) {
|
||||
[$key, $value] = $valArr;
|
||||
$dict[$translationPrefix . $field['name'] . ' ' . $key] = $lang == 'en' ? $field['name'] . ' ' . $key : $value;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
$dict[$translationPrefix . $field['name']] = $lang == 'en' ? $field['name'] : $comment;
|
||||
}
|
||||
return $dict;
|
||||
}
|
||||
|
||||
public static function recordCrudStatus(array $data): int
|
||||
{
|
||||
if (isset($data['id'])) {
|
||||
CrudLog::where('id', $data['id'])->update(['status' => $data['status']]);
|
||||
return $data['id'];
|
||||
}
|
||||
|
||||
$connection = $data['table']['databaseConnection'] ?? config('thinkorm.default', config('database.default', 'mysql'));
|
||||
$log = CrudLog::create([
|
||||
'table_name' => $data['table']['name'],
|
||||
'comment' => $data['table']['comment'],
|
||||
'table' => $data['table'],
|
||||
'fields' => $data['fields'],
|
||||
'connection' => $connection,
|
||||
'status' => $data['status'],
|
||||
]);
|
||||
return $log->id;
|
||||
}
|
||||
|
||||
public static function getPhinxFieldType(string $type, array $field): array
|
||||
{
|
||||
if ($type == 'tinyint') {
|
||||
if (
|
||||
(isset($field['dataType']) && $field['dataType'] == 'tinyint(1)') ||
|
||||
($field['default'] == '1' && ($field['defaultType'] ?? '') == 'INPUT')
|
||||
) {
|
||||
$type = 'boolean';
|
||||
}
|
||||
}
|
||||
$phinxFieldTypeMap = [
|
||||
'tinyint' => ['type' => AdapterInterface::PHINX_TYPE_INTEGER, 'limit' => MysqlAdapter::INT_TINY],
|
||||
'smallint' => ['type' => AdapterInterface::PHINX_TYPE_INTEGER, 'limit' => MysqlAdapter::INT_SMALL],
|
||||
'mediumint' => ['type' => AdapterInterface::PHINX_TYPE_INTEGER, 'limit' => MysqlAdapter::INT_MEDIUM],
|
||||
'int' => ['type' => AdapterInterface::PHINX_TYPE_INTEGER, 'limit' => null],
|
||||
'bigint' => ['type' => AdapterInterface::PHINX_TYPE_BIG_INTEGER, 'limit' => null],
|
||||
'boolean' => ['type' => AdapterInterface::PHINX_TYPE_BOOLEAN, 'limit' => null],
|
||||
'varchar' => ['type' => AdapterInterface::PHINX_TYPE_STRING, 'limit' => null],
|
||||
'tinytext' => ['type' => AdapterInterface::PHINX_TYPE_TEXT, 'limit' => MysqlAdapter::TEXT_TINY],
|
||||
'mediumtext' => ['type' => AdapterInterface::PHINX_TYPE_TEXT, 'limit' => MysqlAdapter::TEXT_MEDIUM],
|
||||
'longtext' => ['type' => AdapterInterface::PHINX_TYPE_TEXT, 'limit' => MysqlAdapter::TEXT_LONG],
|
||||
'tinyblob' => ['type' => AdapterInterface::PHINX_TYPE_BLOB, 'limit' => MysqlAdapter::BLOB_TINY],
|
||||
'mediumblob' => ['type' => AdapterInterface::PHINX_TYPE_BLOB, 'limit' => MysqlAdapter::BLOB_MEDIUM],
|
||||
'longblob' => ['type' => AdapterInterface::PHINX_TYPE_BLOB, 'limit' => MysqlAdapter::BLOB_LONG],
|
||||
];
|
||||
return array_key_exists($type, $phinxFieldTypeMap) ? $phinxFieldTypeMap[$type] : ['type' => $type, 'limit' => null];
|
||||
}
|
||||
|
||||
public static function analyseFieldLimit(string $type, array $field): array
|
||||
{
|
||||
$fieldType = ['decimal' => ['decimal', 'double', 'float'], 'values' => ['enum', 'set']];
|
||||
$dataTypeLimit = self::dataTypeLimit($field['dataType'] ?? '');
|
||||
if (in_array($type, $fieldType['decimal'])) {
|
||||
if ($dataTypeLimit) {
|
||||
return ['precision' => $dataTypeLimit[0], 'scale' => $dataTypeLimit[1] ?? 0];
|
||||
}
|
||||
$scale = isset($field['precision']) ? intval($field['precision']) : 0;
|
||||
return ['precision' => $field['length'] ?? 10, 'scale' => $scale];
|
||||
} elseif (in_array($type, $fieldType['values'])) {
|
||||
foreach ($dataTypeLimit as &$item) {
|
||||
$item = str_replace(['"', "'"], '', $item);
|
||||
}
|
||||
return ['values' => $dataTypeLimit];
|
||||
} elseif ($dataTypeLimit && $dataTypeLimit[0]) {
|
||||
return ['limit' => $dataTypeLimit[0]];
|
||||
} elseif (isset($field['length'])) {
|
||||
return ['limit' => $field['length']];
|
||||
}
|
||||
return [];
|
||||
}
|
||||
|
||||
public static function dataTypeLimit(string $dataType): array
|
||||
{
|
||||
preg_match("/\((.*?)\)/", $dataType, $matches);
|
||||
if (isset($matches[1]) && $matches[1]) {
|
||||
return explode(',', trim($matches[1], ','));
|
||||
}
|
||||
return [];
|
||||
}
|
||||
|
||||
public static function analyseFieldDefault(array $field): mixed
|
||||
{
|
||||
return match ($field['defaultType'] ?? 'NONE') {
|
||||
'EMPTY STRING' => '',
|
||||
'NULL' => null,
|
||||
default => $field['default'] ?? null,
|
||||
};
|
||||
}
|
||||
|
||||
public static function searchArray($fields, callable $myFunction): array|bool
|
||||
{
|
||||
foreach ($fields as $key => $field) {
|
||||
if (call_user_func($myFunction, $field, $key)) {
|
||||
return $field;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public static function getPhinxFieldData(array $field): array
|
||||
{
|
||||
$conciseType = self::analyseFieldType($field);
|
||||
$phinxTypeData = self::getPhinxFieldType($conciseType, $field);
|
||||
$phinxColumnOptions = self::analyseFieldLimit($conciseType, $field);
|
||||
if ($phinxTypeData['limit'] !== null) {
|
||||
$phinxColumnOptions['limit'] = $phinxTypeData['limit'];
|
||||
}
|
||||
$noDefaultValueFields = [
|
||||
'text', 'blob', 'geometry', 'geometrycollection', 'json', 'linestring', 'longblob', 'longtext', 'mediumblob',
|
||||
'mediumtext', 'multilinestring', 'multipoint', 'multipolygon', 'point', 'polygon', 'tinyblob',
|
||||
];
|
||||
if (($field['defaultType'] ?? '') != 'NONE' && !in_array($conciseType, $noDefaultValueFields)) {
|
||||
$phinxColumnOptions['default'] = self::analyseFieldDefault($field);
|
||||
}
|
||||
$phinxColumnOptions['null'] = (bool)($field['null'] ?? false);
|
||||
$phinxColumnOptions['comment'] = $field['comment'] ?? '';
|
||||
$phinxColumnOptions['signed'] = !($field['unsigned'] ?? false);
|
||||
$phinxColumnOptions['identity'] = $field['autoIncrement'] ?? false;
|
||||
return ['type' => $phinxTypeData['type'], 'options' => $phinxColumnOptions];
|
||||
}
|
||||
|
||||
public static function updateFieldOrder(string $tableName, array $fields, array $designChange, ?string $connection = null): void
|
||||
{
|
||||
if ($designChange) {
|
||||
$table = TableManager::phinxTable($tableName, [], false, $connection);
|
||||
foreach ($designChange as $item) {
|
||||
if (!$item['sync']) continue;
|
||||
if (!empty($item['after'])) {
|
||||
$fieldName = in_array($item['type'], ['add-field', 'change-field-name']) ? $item['newName'] : $item['oldName'];
|
||||
$field = self::searchArray($fields, fn($f) => $f['name'] == $fieldName);
|
||||
if (!$field) continue;
|
||||
$phinxFieldData = self::getPhinxFieldData($field);
|
||||
$phinxFieldData['options']['after'] = $item['after'] == 'FIRST FIELD' ? MysqlAdapter::FIRST : $item['after'];
|
||||
$table->changeColumn($fieldName, $phinxFieldData['type'], $phinxFieldData['options']);
|
||||
}
|
||||
}
|
||||
$table->update();
|
||||
}
|
||||
}
|
||||
|
||||
public static function handleTableDesign(array $table, array $fields): array
|
||||
{
|
||||
$name = TableManager::tableName($table['name'], true, $table['databaseConnection'] ?? null);
|
||||
$comment = $table['comment'] ?? '';
|
||||
$designChange = $table['designChange'] ?? [];
|
||||
$adapter = TableManager::phinxAdapter(false, $table['databaseConnection'] ?? null);
|
||||
|
||||
$pk = self::searchArray($fields, fn($item) => $item['primaryKey'] ?? false);
|
||||
$pk = $pk ? $pk['name'] : '';
|
||||
|
||||
if ($adapter->hasTable($name)) {
|
||||
if ($designChange) {
|
||||
$tableManager = TableManager::phinxTable($name, [], false, $table['databaseConnection'] ?? null);
|
||||
$tableManager->changeComment($comment)->update();
|
||||
$priorityOpt = false;
|
||||
foreach ($designChange as $item) {
|
||||
if (!$item['sync']) continue;
|
||||
if (in_array($item['type'], ['change-field-name', 'del-field']) && !$tableManager->hasColumn($item['oldName'])) {
|
||||
throw new BaException(__($item['type'] . ' fail not exist', [$item['oldName']]));
|
||||
}
|
||||
if ($item['type'] == 'change-field-name') {
|
||||
$priorityOpt = true;
|
||||
$tableManager->renameColumn($item['oldName'], $item['newName']);
|
||||
} elseif ($item['type'] == 'del-field') {
|
||||
$priorityOpt = true;
|
||||
$tableManager->removeColumn($item['oldName']);
|
||||
}
|
||||
}
|
||||
if ($priorityOpt) {
|
||||
$tableManager->update();
|
||||
}
|
||||
foreach ($designChange as $item) {
|
||||
if (!$item['sync']) continue;
|
||||
if ($item['type'] == 'change-field-attr') {
|
||||
if (!$tableManager->hasColumn($item['oldName'])) {
|
||||
throw new BaException(__($item['type'] . ' fail not exist', [$item['oldName']]));
|
||||
}
|
||||
$field = self::searchArray($fields, fn($f) => $f['name'] == $item['oldName']);
|
||||
if ($field) {
|
||||
$phinxFieldData = self::getPhinxFieldData($field);
|
||||
$tableManager->changeColumn($item['oldName'], $phinxFieldData['type'], $phinxFieldData['options']);
|
||||
}
|
||||
} elseif ($item['type'] == 'add-field') {
|
||||
if ($tableManager->hasColumn($item['newName'])) {
|
||||
throw new BaException(__($item['type'] . ' fail exist', [$item['newName']]));
|
||||
}
|
||||
$field = self::searchArray($fields, fn($f) => $f['name'] == $item['newName']);
|
||||
if ($field) {
|
||||
$phinxFieldData = self::getPhinxFieldData($field);
|
||||
$tableManager->addColumn($item['newName'], $phinxFieldData['type'], $phinxFieldData['options']);
|
||||
}
|
||||
}
|
||||
}
|
||||
$tableManager->update();
|
||||
self::updateFieldOrder($name, $fields, $designChange, $table['databaseConnection'] ?? null);
|
||||
}
|
||||
} else {
|
||||
$tableManager = TableManager::phinxTable($name, [
|
||||
'id' => false, 'comment' => $comment, 'row_format' => 'DYNAMIC',
|
||||
'primary_key' => $pk, 'collation' => 'utf8mb4_unicode_ci',
|
||||
], false, $table['databaseConnection'] ?? null);
|
||||
foreach ($fields as $field) {
|
||||
$phinxFieldData = self::getPhinxFieldData($field);
|
||||
$tableManager->addColumn($field['name'], $phinxFieldData['type'], $phinxFieldData['options']);
|
||||
}
|
||||
$tableManager->create();
|
||||
}
|
||||
return [$pk];
|
||||
}
|
||||
|
||||
public static function parseNameData($app, $table, $type, $value = ''): array
|
||||
{
|
||||
$pathArr = [];
|
||||
if ($value) {
|
||||
$value = str_replace('.php', '', $value);
|
||||
$value = str_replace(['.', '/', '\\', '_'], '/', $value);
|
||||
$pathArrTemp = explode('/', $value);
|
||||
$redundantDir = ['app' => 0, $app => 1, $type => 2];
|
||||
foreach ($pathArrTemp as $key => $item) {
|
||||
if (!array_key_exists($item, $redundantDir) || $key !== $redundantDir[$item]) {
|
||||
$pathArr[] = $item;
|
||||
}
|
||||
}
|
||||
} elseif (isset(self::$parseNamePresets[$type]) && array_key_exists($table, self::$parseNamePresets[$type])) {
|
||||
$pathArr = self::$parseNamePresets[$type][$table];
|
||||
} else {
|
||||
$table = str_replace(['.', '/', '\\', '_'], '/', $table);
|
||||
$pathArr = explode('/', $table);
|
||||
}
|
||||
$originalLastName = array_pop($pathArr);
|
||||
$pathArr = array_map('strtolower', $pathArr);
|
||||
$lastName = ucfirst($originalLastName);
|
||||
|
||||
if (in_array(strtolower($originalLastName), self::$reservedKeywords)) {
|
||||
throw new \Exception('Unable to use internal variable:' . $lastName);
|
||||
}
|
||||
|
||||
$appDir = root_path() . 'app' . DIRECTORY_SEPARATOR . $app . DIRECTORY_SEPARATOR;
|
||||
$namespace = "app\\$app\\$type" . ($pathArr ? '\\' . implode('\\', $pathArr) : '');
|
||||
$parseFile = $appDir . $type . DIRECTORY_SEPARATOR . ($pathArr ? implode(DIRECTORY_SEPARATOR, $pathArr) . DIRECTORY_SEPARATOR : '') . $lastName . '.php';
|
||||
$rootFileName = $namespace . "/$lastName" . '.php';
|
||||
|
||||
return [
|
||||
'lastName' => $lastName,
|
||||
'originalLastName' => $originalLastName,
|
||||
'path' => $pathArr,
|
||||
'namespace' => $namespace,
|
||||
'parseFile' => Filesystem::fsFit($parseFile),
|
||||
'rootFileName' => Filesystem::fsFit($rootFileName),
|
||||
];
|
||||
}
|
||||
|
||||
public static function parseWebDirNameData($table, $type, $value = ''): array
|
||||
{
|
||||
$pathArr = [];
|
||||
if ($value) {
|
||||
$value = str_replace(['.', '/', '\\', '_'], '/', $value);
|
||||
$pathArrTemp = explode('/', $value);
|
||||
$redundantDir = ['web' => 0, 'src' => 1, 'views' => 2, 'lang' => 2, 'backend' => 3, 'pages' => 3, 'en' => 4, 'zh-cn' => 4];
|
||||
foreach ($pathArrTemp as $key => $item) {
|
||||
if (!array_key_exists($item, $redundantDir) || $key !== $redundantDir[$item]) {
|
||||
$pathArr[] = $item;
|
||||
}
|
||||
}
|
||||
} elseif (isset(self::$parseWebDirPresets[$type]) && array_key_exists($table, self::$parseWebDirPresets[$type])) {
|
||||
$pathArr = self::$parseWebDirPresets[$type][$table];
|
||||
} else {
|
||||
$table = str_replace(['.', '/', '\\', '_'], '/', $table);
|
||||
$pathArr = explode('/', $table);
|
||||
}
|
||||
$originalLastName = array_pop($pathArr);
|
||||
$pathArr = array_map('strtolower', $pathArr);
|
||||
$lastName = lcfirst($originalLastName);
|
||||
|
||||
$webDir['path'] = $pathArr;
|
||||
$webDir['lastName'] = $lastName;
|
||||
$webDir['originalLastName'] = $originalLastName;
|
||||
if ($type == 'views') {
|
||||
$webDir['views'] = "web/src/views/backend" . ($pathArr ? '/' . implode('/', $pathArr) : '') . "/$lastName";
|
||||
} elseif ($type == 'lang') {
|
||||
$webDir['lang'] = array_merge($pathArr, [$lastName]);
|
||||
foreach (['en', 'zh-cn'] as $item) {
|
||||
$webDir[$item] = "web/src/lang/backend/$item" . ($pathArr ? '/' . implode('/', $pathArr) : '') . "/$lastName";
|
||||
}
|
||||
}
|
||||
foreach ($webDir as &$item) {
|
||||
if (is_string($item)) $item = Filesystem::fsFit($item);
|
||||
}
|
||||
return $webDir;
|
||||
}
|
||||
|
||||
public static function getMenuName(array $webDir): string
|
||||
{
|
||||
return ($webDir['path'] ? implode('/', $webDir['path']) . '/' : '') . $webDir['originalLastName'];
|
||||
}
|
||||
|
||||
public static function getStubFilePath(string $name): string
|
||||
{
|
||||
return root_path() . 'app' . DIRECTORY_SEPARATOR . 'admin' . DIRECTORY_SEPARATOR . 'library' . DIRECTORY_SEPARATOR . 'crud' . DIRECTORY_SEPARATOR . 'stubs' . DIRECTORY_SEPARATOR . Filesystem::fsFit($name) . '.stub';
|
||||
}
|
||||
|
||||
public static function arrayToString(mixed $value): string
|
||||
{
|
||||
if (is_array($value)) {
|
||||
foreach ($value as &$item) {
|
||||
$item = self::arrayToString($item);
|
||||
}
|
||||
return implode(PHP_EOL, $value);
|
||||
}
|
||||
if (is_string($value)) return $value;
|
||||
if (is_bool($value)) return $value ? 'true' : 'false';
|
||||
return $value === null ? '' : strval($value);
|
||||
}
|
||||
|
||||
public static function assembleStub(string $name, array $data, bool $escape = false): string
|
||||
{
|
||||
foreach ($data as &$datum) {
|
||||
$datum = self::arrayToString($datum);
|
||||
}
|
||||
$search = $replace = [];
|
||||
foreach ($data as $k => $v) {
|
||||
$search[] = "{%$k%}";
|
||||
$replace[] = $v;
|
||||
}
|
||||
$stubPath = self::getStubFilePath($name);
|
||||
$stubContent = file_get_contents($stubPath);
|
||||
$content = str_replace($search, $replace, $stubContent);
|
||||
return $escape ? self::escape($content) : $content;
|
||||
}
|
||||
|
||||
public static function escape(array|string $value): string
|
||||
{
|
||||
if (is_array($value)) {
|
||||
$value = json_encode($value, JSON_UNESCAPED_UNICODE);
|
||||
}
|
||||
return htmlspecialchars((string)$value, ENT_QUOTES, 'UTF-8', false);
|
||||
}
|
||||
|
||||
public static function tab(int $num = 1): string
|
||||
{
|
||||
return str_pad('', 4 * $num);
|
||||
}
|
||||
|
||||
public static function parseTableColumns(string $table, bool $analyseField = false, ?string $connection = null): array
|
||||
{
|
||||
$connection = TableManager::getConnection($connection);
|
||||
$connectionConfig = TableManager::getConnectionConfig($connection);
|
||||
|
||||
$sql = 'SELECT * FROM `information_schema`.`columns` '
|
||||
. 'WHERE TABLE_SCHEMA = ? AND table_name = ? '
|
||||
. 'ORDER BY ORDINAL_POSITION';
|
||||
|
||||
$tableColumn = Db::connect($connection)->query($sql, [$connectionConfig['database'], TableManager::tableName($table, true, $connection)]);
|
||||
|
||||
$columns = [];
|
||||
foreach ($tableColumn as $item) {
|
||||
$isNullAble = $item['IS_NULLABLE'] == 'YES';
|
||||
if (str_contains($item['COLUMN_TYPE'], '(')) {
|
||||
$dataType = substr_replace($item['COLUMN_TYPE'], '', stripos($item['COLUMN_TYPE'], ')') + 1);
|
||||
} else {
|
||||
$dataType = str_replace(' unsigned', '', $item['COLUMN_TYPE']);
|
||||
}
|
||||
|
||||
$default = '';
|
||||
if ($isNullAble && $item['COLUMN_DEFAULT'] === null) {
|
||||
$defaultType = 'NULL';
|
||||
} elseif ($item['COLUMN_DEFAULT'] == '' && in_array($item['DATA_TYPE'], ['varchar', 'char'])) {
|
||||
$defaultType = 'EMPTY STRING';
|
||||
} elseif (!$isNullAble && $item['COLUMN_DEFAULT'] === null) {
|
||||
$defaultType = 'NONE';
|
||||
} else {
|
||||
$defaultType = 'INPUT';
|
||||
$default = $item['COLUMN_DEFAULT'];
|
||||
}
|
||||
|
||||
$column = [
|
||||
'name' => $item['COLUMN_NAME'],
|
||||
'type' => $item['DATA_TYPE'],
|
||||
'dataType' => $dataType,
|
||||
'default' => $default,
|
||||
'defaultType' => $defaultType,
|
||||
'null' => $isNullAble,
|
||||
'primaryKey' => $item['COLUMN_KEY'] == 'PRI',
|
||||
'unsigned' => (bool)stripos($item['COLUMN_TYPE'], 'unsigned'),
|
||||
'autoIncrement' => stripos($item['EXTRA'], 'auto_increment') !== false,
|
||||
'comment' => $item['COLUMN_COMMENT'],
|
||||
'designType' => self::getTableColumnsDataType($item),
|
||||
'table' => [],
|
||||
'form' => [],
|
||||
];
|
||||
if ($analyseField) {
|
||||
self::analyseField($column);
|
||||
} else {
|
||||
self::handleTableColumn($column);
|
||||
}
|
||||
$columns[$item['COLUMN_NAME']] = $column;
|
||||
}
|
||||
return $columns;
|
||||
}
|
||||
|
||||
public static function handleTableColumn(&$column): void
|
||||
{
|
||||
}
|
||||
|
||||
public static function analyseFieldType(array $field): string
|
||||
{
|
||||
$dataType = (isset($field['dataType']) && $field['dataType']) ? $field['dataType'] : $field['type'];
|
||||
if (stripos($dataType, '(') !== false) {
|
||||
$typeName = explode('(', $dataType);
|
||||
return trim($typeName[0]);
|
||||
}
|
||||
return trim($dataType);
|
||||
}
|
||||
|
||||
public static function analyseFieldDataType(array $field): string
|
||||
{
|
||||
if (!empty($field['dataType'])) return $field['dataType'];
|
||||
$conciseType = self::analyseFieldType($field);
|
||||
$limit = self::analyseFieldLimit($conciseType, $field);
|
||||
if (isset($limit['precision'])) {
|
||||
return "$conciseType({$limit['precision']}, {$limit['scale']})";
|
||||
}
|
||||
if (isset($limit['values'])) {
|
||||
return "$conciseType(" . implode(',', $limit['values']) . ")";
|
||||
}
|
||||
return "$conciseType({$limit['limit']})";
|
||||
}
|
||||
|
||||
public static function analyseField(&$field): void
|
||||
{
|
||||
$field['type'] = self::analyseFieldType($field);
|
||||
$field['originalDesignType'] = $field['designType'];
|
||||
|
||||
$designTypeComparison = ['pk' => 'string', 'weigh' => 'number', 'timestamp' => 'datetime', 'float' => 'number'];
|
||||
if (array_key_exists($field['designType'], $designTypeComparison)) {
|
||||
$field['designType'] = $designTypeComparison[$field['designType']];
|
||||
}
|
||||
|
||||
$supportMultipleComparison = ['select', 'image', 'file', 'remoteSelect'];
|
||||
if (in_array($field['designType'], $supportMultipleComparison)) {
|
||||
$multiKey = $field['designType'] == 'remoteSelect' ? 'select-multi' : $field['designType'] . '-multi';
|
||||
if (isset($field['form'][$multiKey]) && $field['form'][$multiKey]) {
|
||||
$field['designType'] = $field['designType'] . 's';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static function getTableColumnsDataType($column): string
|
||||
{
|
||||
if (stripos($column['COLUMN_NAME'], 'id') !== false && stripos($column['EXTRA'], 'auto_increment') !== false) {
|
||||
return 'pk';
|
||||
}
|
||||
if ($column['COLUMN_NAME'] == 'weigh') return 'weigh';
|
||||
if (in_array($column['COLUMN_NAME'], ['createtime', 'updatetime', 'create_time', 'update_time'])) return 'timestamp';
|
||||
|
||||
foreach (self::$inputTypeRule as $item) {
|
||||
$typeBool = !isset($item['type']) || !$item['type'] || in_array($column['DATA_TYPE'], $item['type']);
|
||||
$suffixBool = !isset($item['suffix']) || !$item['suffix'] || self::isMatchSuffix($column['COLUMN_NAME'], $item['suffix']);
|
||||
$columnTypeBool = !isset($item['column_type']) || !$item['column_type'] || in_array($column['COLUMN_TYPE'], $item['column_type']);
|
||||
if ($typeBool && $suffixBool && $columnTypeBool) {
|
||||
return $item['value'];
|
||||
}
|
||||
}
|
||||
return 'string';
|
||||
}
|
||||
|
||||
protected static function isMatchSuffix(string $field, string|array $suffixArr): bool
|
||||
{
|
||||
$suffixArr = is_array($suffixArr) ? $suffixArr : explode(',', $suffixArr);
|
||||
foreach ($suffixArr as $v) {
|
||||
if (preg_match("/$v$/i", $field)) return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public static function createMenu($webViewsDir, $tableComment): void
|
||||
{
|
||||
$menuName = self::getMenuName($webViewsDir);
|
||||
if (AdminRule::where('name', $menuName)->value('id')) {
|
||||
return;
|
||||
}
|
||||
$menuChildren = self::$menuChildren;
|
||||
foreach ($menuChildren as &$item) {
|
||||
$item['name'] = $menuName . $item['name'];
|
||||
}
|
||||
$componentPath = str_replace(['\\', 'web/src'], ['/', '/src'], $webViewsDir['views'] . '/' . 'index.vue');
|
||||
|
||||
$menus = [
|
||||
'type' => 'menu',
|
||||
'title' => $tableComment ?: $webViewsDir['originalLastName'],
|
||||
'name' => $menuName,
|
||||
'path' => $menuName,
|
||||
'menu_type' => 'tab',
|
||||
'keepalive' => 1,
|
||||
'component' => $componentPath,
|
||||
'children' => $menuChildren,
|
||||
];
|
||||
$paths = array_reverse($webViewsDir['path']);
|
||||
foreach ($paths as $path) {
|
||||
$menus = [
|
||||
'type' => 'menu_dir',
|
||||
'title' => $path,
|
||||
'name' => $path,
|
||||
'path' => $path,
|
||||
'children' => [$menus],
|
||||
];
|
||||
}
|
||||
Menu::create([$menus], 0, 'ignore');
|
||||
}
|
||||
|
||||
public static function writeWebLangFile($langData, $webLangDir): void
|
||||
{
|
||||
foreach ($langData as $lang => $langDatum) {
|
||||
$langTsContent = '';
|
||||
foreach ($langDatum as $key => $item) {
|
||||
$quote = self::getQuote($item);
|
||||
$keyStr = self::formatObjectKey($key);
|
||||
$langTsContent .= self::tab() . $keyStr . ": $quote$item$quote,\n";
|
||||
}
|
||||
$langTsContent = "export default {\n" . $langTsContent . "}\n";
|
||||
self::writeFile(root_path() . $webLangDir[$lang] . '.ts', $langTsContent);
|
||||
}
|
||||
}
|
||||
|
||||
public static function writeFile($path, $content): bool|int
|
||||
{
|
||||
$path = Filesystem::fsFit($path);
|
||||
if (!is_dir(dirname($path))) {
|
||||
mkdir(dirname($path), 0755, true);
|
||||
}
|
||||
return file_put_contents($path, $content);
|
||||
}
|
||||
|
||||
public static function buildModelAppend($append): string
|
||||
{
|
||||
if (!$append) return '';
|
||||
return "\n" . self::tab() . "// 追加属性\n" . self::tab() . "protected \$append = " . self::buildFormatSimpleArray($append) . ";\n";
|
||||
}
|
||||
|
||||
public static function buildModelFieldType(array $fieldType): string
|
||||
{
|
||||
if (!$fieldType) return '';
|
||||
$maxStrLang = 0;
|
||||
foreach ($fieldType as $key => $item) {
|
||||
$maxStrLang = max(strlen($key), $maxStrLang);
|
||||
}
|
||||
$str = '';
|
||||
foreach ($fieldType as $key => $item) {
|
||||
$str .= self::tab(2) . "'$key'" . str_pad('=>', ($maxStrLang - strlen($key) + 3), ' ', STR_PAD_LEFT) . " '$item',\n";
|
||||
}
|
||||
return "\n" . self::tab() . "// 字段类型转换\n" . self::tab() . "protected \$type = [\n" . rtrim($str, "\n") . "\n" . self::tab() . "];\n";
|
||||
}
|
||||
|
||||
public static function writeModelFile(string $tablePk, array $fieldsMap, array $modelData, array $modelFile): void
|
||||
{
|
||||
if ($modelData['connection'] && $modelData['connection'] != config('thinkorm.default', config('database.default', 'mysql'))) {
|
||||
$modelData['connection'] = "\n" . self::tab() . "// 数据库连接配置标识\n" . self::tab() . 'protected $connection = ' . "'{$modelData['connection']}';\n";
|
||||
} else {
|
||||
$modelData['connection'] = '';
|
||||
}
|
||||
$modelData['pk'] = $tablePk == 'id' ? '' : "\n" . self::tab() . "// 表主键\n" . self::tab() . 'protected $pk = ' . "'$tablePk';\n";
|
||||
$modelData['autoWriteTimestamp'] = array_key_exists(self::$createTimeField, $fieldsMap) || array_key_exists(self::$updateTimeField, $fieldsMap) ? 'true' : 'false';
|
||||
if ($modelData['autoWriteTimestamp'] == 'true') {
|
||||
$modelData['createTime'] = array_key_exists(self::$createTimeField, $fieldsMap) ? '' : "\n" . self::tab() . "protected \$createTime = false;";
|
||||
$modelData['updateTime'] = array_key_exists(self::$updateTimeField, $fieldsMap) ? '' : "\n" . self::tab() . "protected \$updateTime = false;";
|
||||
}
|
||||
$modelMethodList = isset($modelData['relationMethodList']) ? array_merge($modelData['methods'], $modelData['relationMethodList']) : $modelData['methods'];
|
||||
$modelData['methods'] = $modelMethodList ? "\n" . implode("\n", $modelMethodList) : '';
|
||||
$modelData['append'] = self::buildModelAppend($modelData['append'] ?? []);
|
||||
$modelData['fieldType'] = self::buildModelFieldType($modelData['fieldType'] ?? []);
|
||||
|
||||
if (isset($modelData['beforeInsertMixins']['snowflake'])) {
|
||||
$modelData['beforeInsert'] = self::assembleStub('mixins/model/beforeInsert', [
|
||||
'setSnowFlakeIdCode' => $modelData['beforeInsertMixins']['snowflake']
|
||||
]);
|
||||
}
|
||||
if (($modelData['afterInsert'] ?? '') && ($modelData['beforeInsert'] ?? '')) {
|
||||
$modelData['afterInsert'] = "\n" . $modelData['afterInsert'];
|
||||
}
|
||||
|
||||
$modelFileContent = self::assembleStub('mixins/model/model', $modelData);
|
||||
self::writeFile($modelFile['parseFile'], $modelFileContent);
|
||||
}
|
||||
|
||||
public static function writeControllerFile(array $controllerData, array $controllerFile): void
|
||||
{
|
||||
if (isset($controllerData['relationVisibleFieldList']) && $controllerData['relationVisibleFieldList']) {
|
||||
$relationVisibleFields = '->visible([';
|
||||
foreach ($controllerData['relationVisibleFieldList'] as $cKey => $controllerDatum) {
|
||||
$relationVisibleFields .= "'$cKey' => ['" . implode("', '", $controllerDatum) . "'], ";
|
||||
}
|
||||
$relationVisibleFields = rtrim($relationVisibleFields, ', ') . '])';
|
||||
$controllerData['methods']['index'] = self::assembleStub('mixins/controller/index', [
|
||||
'relationVisibleFields' => $relationVisibleFields
|
||||
]);
|
||||
$controllerData['use']['Throwable'] = "\nuse Throwable;";
|
||||
unset($controllerData['relationVisibleFieldList']);
|
||||
}
|
||||
$controllerAttr = '';
|
||||
foreach ($controllerData['attr'] ?? [] as $key => $item) {
|
||||
$attrType = self::$attrType['controller'][$key] ?? '';
|
||||
if (is_array($item)) {
|
||||
$controllerAttr .= "\n" . self::tab() . "protected $attrType \$$key = ['" . implode("', '", $item) . "'];\n";
|
||||
} elseif ($item) {
|
||||
$controllerAttr .= "\n" . self::tab() . "protected $attrType \$$key = '$item';\n";
|
||||
}
|
||||
}
|
||||
$controllerData['attr'] = $controllerAttr;
|
||||
$controllerData['initialize'] = self::assembleStub('mixins/controller/initialize', [
|
||||
'modelNamespace' => $controllerData['modelNamespace'],
|
||||
'modelName' => $controllerData['modelName'],
|
||||
'filterRule' => $controllerData['filterRule'] ?? '',
|
||||
]);
|
||||
$contentFileContent = self::assembleStub('mixins/controller/controller', $controllerData);
|
||||
self::writeFile($controllerFile['parseFile'], $contentFileContent);
|
||||
}
|
||||
|
||||
public static function writeFormFile($formVueData, $webViewsDir, $fields, $webTranslate): void
|
||||
{
|
||||
$fieldHtml = "\n";
|
||||
$formVueData['bigDialog'] = $formVueData['bigDialog'] ? "\n" . self::tab(2) . 'width="70%"' : '';
|
||||
foreach ($formVueData['formFields'] ?? [] as $field) {
|
||||
$fieldHtml .= self::tab(5) . "<FormItem";
|
||||
foreach ($field as $key => $attr) {
|
||||
if (is_array($attr)) {
|
||||
$fieldHtml .= ' ' . $key . '="' . self::getJsonFromArray($attr) . '"';
|
||||
} else {
|
||||
$fieldHtml .= ' ' . $key . '="' . $attr . '"';
|
||||
}
|
||||
}
|
||||
$fieldHtml .= " />\n";
|
||||
}
|
||||
$formVueData['formFields'] = rtrim($fieldHtml, "\n");
|
||||
|
||||
foreach ($fields as $field) {
|
||||
if (isset($field['form']['validator'])) {
|
||||
foreach ($field['form']['validator'] as $item) {
|
||||
$message = isset($field['form']['validatorMsg']) && $field['form']['validatorMsg'] ? ", message: '{$field['form']['validatorMsg']}'" : '';
|
||||
$formVueData['formValidatorRules'][$field['name']][] = "buildValidatorData({ name: '$item', title: t('$webTranslate{$field['name']}')$message })";
|
||||
}
|
||||
}
|
||||
}
|
||||
if ($formVueData['formValidatorRules'] ?? []) {
|
||||
$formVueData['imports'][] = "import { buildValidatorData } from '/@/utils/validate'";
|
||||
}
|
||||
$formVueData['importExpand'] = self::buildImportExpand($formVueData['imports'] ?? []);
|
||||
$formVueData['formItemRules'] = self::buildFormValidatorRules($formVueData['formValidatorRules'] ?? []);
|
||||
$formVueContent = self::assembleStub('html/form', $formVueData);
|
||||
self::writeFile(root_path() . $webViewsDir['views'] . '/' . 'popupForm.vue', $formVueContent);
|
||||
}
|
||||
|
||||
public static function buildImportExpand(array $imports): string
|
||||
{
|
||||
$importExpand = '';
|
||||
foreach ($imports as $import) {
|
||||
$importExpand .= "\n$import";
|
||||
}
|
||||
return $importExpand;
|
||||
}
|
||||
|
||||
public static function buildFormValidatorRules(array $formValidatorRules): string
|
||||
{
|
||||
$rulesHtml = "";
|
||||
foreach ($formValidatorRules as $key => $formItemRule) {
|
||||
$rulesArrHtml = '';
|
||||
foreach ($formItemRule as $item) {
|
||||
$rulesArrHtml .= $item . ', ';
|
||||
}
|
||||
$rulesHtml .= self::tab() . $key . ': [' . rtrim($rulesArrHtml, ', ') . "],\n";
|
||||
}
|
||||
return $rulesHtml ? "\n" . $rulesHtml : '';
|
||||
}
|
||||
|
||||
public static function writeIndexFile($indexVueData, $webViewsDir, $controllerFile): void
|
||||
{
|
||||
$indexVueData['optButtons'] = self::buildSimpleArray($indexVueData['optButtons'] ?? []);
|
||||
$indexVueData['defaultItems'] = self::getJsonFromArray($indexVueData['defaultItems'] ?? []);
|
||||
$indexVueData['tableColumn'] = self::buildTableColumn($indexVueData['tableColumn'] ?? []);
|
||||
$indexVueData['dblClickNotEditColumn'] = self::buildSimpleArray($indexVueData['dblClickNotEditColumn'] ?? ['undefined']);
|
||||
$controllerFile['path'][] = $controllerFile['originalLastName'];
|
||||
$indexVueData['controllerUrl'] = '\'/admin/' . ($controllerFile['path'] ? implode('.', $controllerFile['path']) : '') . '/\'';
|
||||
$indexVueData['componentName'] = ($webViewsDir['path'] ? implode('/', $webViewsDir['path']) . '/' : '') . $webViewsDir['originalLastName'];
|
||||
$indexVueContent = self::assembleStub('html/index', $indexVueData);
|
||||
self::writeFile(root_path() . $webViewsDir['views'] . '/' . 'index.vue', $indexVueContent);
|
||||
}
|
||||
|
||||
public static function buildTableColumn($tableColumnList): string
|
||||
{
|
||||
$columnJson = '';
|
||||
$emptyUnset = ['comSearchInputAttr', 'replaceValue', 'custom'];
|
||||
foreach ($tableColumnList as $column) {
|
||||
foreach ($emptyUnset as $unsetKey) {
|
||||
if (empty($column[$unsetKey])) unset($column[$unsetKey]);
|
||||
}
|
||||
$columnJson .= self::tab(3) . '{';
|
||||
foreach ($column as $key => $item) {
|
||||
$columnJson .= self::buildTableColumnKey($key, $item);
|
||||
}
|
||||
$columnJson = rtrim($columnJson, ',') . " },\n";
|
||||
}
|
||||
return rtrim($columnJson, "\n");
|
||||
}
|
||||
|
||||
public static function buildTableColumnKey($key, $item): string
|
||||
{
|
||||
$key = self::formatObjectKey($key);
|
||||
if (is_array($item)) {
|
||||
$itemJson = ' ' . $key . ': {';
|
||||
foreach ($item as $ik => $iItem) {
|
||||
$itemJson .= self::buildTableColumnKey($ik, $iItem);
|
||||
}
|
||||
$itemJson = rtrim($itemJson, ',') . ' },';
|
||||
} elseif ($item === 'false' || $item === 'true') {
|
||||
$itemJson = ' ' . $key . ': ' . $item . ',';
|
||||
} elseif (in_array($key, ['label', 'width', 'buttons'], true) || str_starts_with((string)$item, "t('") || str_starts_with((string)$item, 't("')) {
|
||||
$itemJson = ' ' . $key . ': ' . $item . ',';
|
||||
} else {
|
||||
$itemJson = ' ' . $key . ': \'' . $item . '\',';
|
||||
}
|
||||
return $itemJson;
|
||||
}
|
||||
|
||||
public static function formatObjectKey(string $keyName): string
|
||||
{
|
||||
if (preg_match("/^[a-zA-Z_][a-zA-Z0-9_]+$/", $keyName)) {
|
||||
return $keyName;
|
||||
}
|
||||
$quote = self::getQuote($keyName);
|
||||
return "$quote$keyName$quote";
|
||||
}
|
||||
|
||||
public static function getQuote(string $value): string
|
||||
{
|
||||
return stripos($value, "'") === false ? "'" : '"';
|
||||
}
|
||||
|
||||
public static function buildFormatSimpleArray($arr, int $tab = 2): string
|
||||
{
|
||||
if (!$arr) return '[]';
|
||||
$str = '[' . PHP_EOL;
|
||||
foreach ($arr as $item) {
|
||||
if ($item == 'undefined' || $item == 'false' || is_numeric($item)) {
|
||||
$str .= self::tab($tab) . $item . ',' . PHP_EOL;
|
||||
} else {
|
||||
$quote = self::getQuote((string)$item);
|
||||
$str .= self::tab($tab) . "$quote$item$quote," . PHP_EOL;
|
||||
}
|
||||
}
|
||||
return $str . self::tab($tab - 1) . ']';
|
||||
}
|
||||
|
||||
public static function buildSimpleArray($arr): string
|
||||
{
|
||||
if (!$arr) return '[]';
|
||||
$str = '';
|
||||
foreach ($arr as $item) {
|
||||
if ($item == 'undefined' || $item == 'false' || is_numeric($item)) {
|
||||
$str .= $item . ', ';
|
||||
} else {
|
||||
$quote = self::getQuote((string)$item);
|
||||
$str .= "$quote$item$quote, ";
|
||||
}
|
||||
}
|
||||
return '[' . rtrim($str, ", ") . ']';
|
||||
}
|
||||
|
||||
public static function buildDefaultOrder(string $field, string $type): string
|
||||
{
|
||||
if ($field && $type) {
|
||||
$defaultOrderStub = self::getJsonFromArray(['prop' => $field, 'order' => $type]);
|
||||
if ($defaultOrderStub) {
|
||||
return "\n" . self::tab(2) . "defaultOrder: " . $defaultOrderStub . ',';
|
||||
}
|
||||
}
|
||||
return '';
|
||||
}
|
||||
|
||||
public static function getJsonFromArray($arr)
|
||||
{
|
||||
if (is_array($arr)) {
|
||||
$jsonStr = '';
|
||||
foreach ($arr as $key => $item) {
|
||||
$keyStr = ' ' . self::formatObjectKey($key) . ': ';
|
||||
if (is_array($item)) {
|
||||
$jsonStr .= $keyStr . self::getJsonFromArray($item) . ',';
|
||||
} elseif ($item === 'false' || $item === 'true') {
|
||||
$jsonStr .= $keyStr . ($item === 'false' ? 'false' : 'true') . ',';
|
||||
} elseif ($item === null) {
|
||||
$jsonStr .= $keyStr . 'null,';
|
||||
} elseif (str_starts_with((string)$item, "t('") || str_starts_with((string)$item, 't("') || $item == '[]' || in_array(gettype($item), ['integer', 'double'])) {
|
||||
$jsonStr .= $keyStr . $item . ',';
|
||||
} elseif (isset($item[0]) && $item[0] == '[' && str_ends_with((string)$item, ']')) {
|
||||
$jsonStr .= $keyStr . $item . ',';
|
||||
} else {
|
||||
$quote = self::getQuote((string)$item);
|
||||
$jsonStr .= $keyStr . "$quote$item$quote,";
|
||||
}
|
||||
}
|
||||
return $jsonStr ? '{' . rtrim($jsonStr, ',') . ' }' : '{}';
|
||||
}
|
||||
return $arr;
|
||||
}
|
||||
}
|
||||
63
app/admin/library/crud/stubs/html/form.stub
Normal file
63
app/admin/library/crud/stubs/html/form.stub
Normal file
@@ -0,0 +1,63 @@
|
||||
<template>
|
||||
<!-- 对话框表单 -->
|
||||
<!-- 建议使用 Prettier 格式化代码 -->
|
||||
<!-- el-form 内可以混用 el-form-item、FormItem、ba-input 等输入组件 -->
|
||||
<el-dialog
|
||||
class="ba-operate-dialog"
|
||||
:close-on-click-modal="false"
|
||||
:model-value="['Add', 'Edit'].includes(baTable.form.operate!)"
|
||||
@close="baTable.toggleForm"{%bigDialog%}
|
||||
>
|
||||
<template #header>
|
||||
<div class="title" v-drag="['.ba-operate-dialog', '.el-dialog__header']" v-zoom="'.ba-operate-dialog'">
|
||||
{{ baTable.form.operate ? t(baTable.form.operate) : '' }}
|
||||
</div>
|
||||
</template>
|
||||
<el-scrollbar v-loading="baTable.form.loading" class="ba-table-form-scrollbar">
|
||||
<div
|
||||
class="ba-operate-form"
|
||||
:class="'ba-' + baTable.form.operate + '-form'"
|
||||
:style="config.layout.shrink ? '':'width: calc(100% - ' + baTable.form.labelWidth! / 2 + 'px)'"
|
||||
>
|
||||
<el-form
|
||||
v-if="!baTable.form.loading"
|
||||
ref="formRef"
|
||||
@submit.prevent=""
|
||||
@keyup.enter="baTable.onSubmit(formRef)"
|
||||
:model="baTable.form.items"
|
||||
:label-position="config.layout.shrink ? 'top' : 'right'"
|
||||
:label-width="baTable.form.labelWidth + 'px'"
|
||||
:rules="rules"
|
||||
>{%formFields%}
|
||||
</el-form>
|
||||
</div>
|
||||
</el-scrollbar>
|
||||
<template #footer>
|
||||
<div :style="'width: calc(100% - ' + baTable.form.labelWidth! / 1.8 + 'px)'">
|
||||
<el-button @click="baTable.toggleForm()">{{ t('Cancel') }}</el-button>
|
||||
<el-button v-blur :loading="baTable.form.submitLoading" @click="baTable.onSubmit(formRef)" type="primary">
|
||||
{{ baTable.form.operateIds && baTable.form.operateIds.length > 1 ? t('Save and edit next item') : t('Save') }}
|
||||
</el-button>
|
||||
</div>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import type { FormItemRule } from 'element-plus'
|
||||
import { inject, reactive, useTemplateRef } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import FormItem from '/@/components/formItem/index.vue'
|
||||
import { useConfig } from '/@/stores/config'
|
||||
import type baTableClass from '/@/utils/baTable'{%importExpand%}
|
||||
|
||||
const config = useConfig()
|
||||
const formRef = useTemplateRef('formRef')
|
||||
const baTable = inject('baTable') as baTableClass
|
||||
|
||||
const { t } = useI18n()
|
||||
|
||||
const rules: Partial<Record<string, FormItemRule[]>> = reactive({{%formItemRules%}})
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss"></style>
|
||||
69
app/admin/library/crud/stubs/html/index.stub
Normal file
69
app/admin/library/crud/stubs/html/index.stub
Normal file
@@ -0,0 +1,69 @@
|
||||
<template>
|
||||
<div class="default-main ba-table-box">
|
||||
<el-alert class="ba-table-alert" v-if="baTable.table.remark" :title="baTable.table.remark" type="info" show-icon />
|
||||
|
||||
<!-- 表格顶部菜单 -->
|
||||
<!-- 自定义按钮请使用插槽,甚至公共搜索也可以使用具名插槽渲染,参见文档 -->
|
||||
<TableHeader
|
||||
:buttons="['refresh', 'add', 'edit', 'delete', 'comSearch', 'quickSearch', 'columnDisplay']"
|
||||
:quick-search-placeholder="t('Quick search placeholder', { fields: t('{%webTranslate%}quick Search Fields') })"
|
||||
></TableHeader>
|
||||
|
||||
<!-- 表格 -->
|
||||
<!-- 表格列有多种自定义渲染方式,比如自定义组件、具名插槽等,参见文档 -->
|
||||
<!-- 要使用 el-table 组件原有的属性,直接加在 Table 标签上即可 -->
|
||||
<Table ref="tableRef"></Table>
|
||||
|
||||
<!-- 表单 -->
|
||||
<PopupForm />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { onMounted, provide, useTemplateRef } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import PopupForm from './popupForm.vue'
|
||||
import { baTableApi } from '/@/api/common'
|
||||
import { defaultOptButtons } from '/@/components/table'
|
||||
import TableHeader from '/@/components/table/header/index.vue'
|
||||
import Table from '/@/components/table/index.vue'
|
||||
import baTableClass from '/@/utils/baTable'
|
||||
|
||||
defineOptions({
|
||||
name: '{%componentName%}',
|
||||
})
|
||||
|
||||
const { t } = useI18n()
|
||||
const tableRef = useTemplateRef('tableRef')
|
||||
const optButtons: OptButton[] = defaultOptButtons({%optButtons%})
|
||||
|
||||
/**
|
||||
* baTable 内包含了表格的所有数据且数据具备响应性,然后通过 provide 注入给了后代组件
|
||||
*/
|
||||
const baTable = new baTableClass(
|
||||
new baTableApi({%controllerUrl%}),
|
||||
{
|
||||
pk: '{%tablePk%}',
|
||||
column: [
|
||||
{%tableColumn%}
|
||||
],
|
||||
dblClickNotEditColumn: {%dblClickNotEditColumn%},{%defaultOrder%}
|
||||
},
|
||||
{
|
||||
defaultItems: {%defaultItems%},
|
||||
}
|
||||
)
|
||||
|
||||
provide('baTable', baTable)
|
||||
|
||||
onMounted(() => {
|
||||
baTable.table.ref = tableRef.value
|
||||
baTable.mount()
|
||||
baTable.getData()?.then(() => {
|
||||
baTable.initSort()
|
||||
baTable.dragSort()
|
||||
})
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss"></style>
|
||||
@@ -0,0 +1,24 @@
|
||||
<?php
|
||||
|
||||
namespace {%namespace%};
|
||||
{%use%}
|
||||
use app\common\controller\Backend;
|
||||
|
||||
/**
|
||||
* {%tableComment%}
|
||||
*/
|
||||
class {%className%} extends Backend
|
||||
{
|
||||
/**
|
||||
* {%modelName%}模型对象
|
||||
* @var object|null
|
||||
* @phpstan-var \{%modelNamespace%}\{%modelName%}|null
|
||||
*/
|
||||
protected ?object $model = null;
|
||||
{%attr%}{%initialize%}
|
||||
{%methods%}
|
||||
|
||||
/**
|
||||
* 若需重写查看、编辑、删除等方法,请复制 @see \app\admin\library\traits\Backend 中对应的方法至此进行重写
|
||||
*/
|
||||
}
|
||||
32
app/admin/library/crud/stubs/mixins/controller/index.stub
Normal file
32
app/admin/library/crud/stubs/mixins/controller/index.stub
Normal file
@@ -0,0 +1,32 @@
|
||||
|
||||
/**
|
||||
* 查看
|
||||
* @throws Throwable
|
||||
*/
|
||||
public function index(): void
|
||||
{
|
||||
// 如果是 select 则转发到 select 方法,若未重写该方法,其实还是继续执行 index
|
||||
if ($this->request->param('select')) {
|
||||
$this->select();
|
||||
}
|
||||
|
||||
/**
|
||||
* 1. withJoin 不可使用 alias 方法设置表别名,别名将自动使用关联模型名称(小写下划线命名规则)
|
||||
* 2. 以下的别名设置了主表别名,同时便于拼接查询参数等
|
||||
* 3. paginate 数据集可使用链式操作 each(function($item, $key) {}) 遍历处理
|
||||
*/
|
||||
list($where, $alias, $limit, $order) = $this->queryBuilder();
|
||||
$res = $this->model
|
||||
->withJoin($this->withJoinTable, $this->withJoinType)
|
||||
{%relationVisibleFields%}
|
||||
->alias($alias)
|
||||
->where($where)
|
||||
->order($order)
|
||||
->paginate($limit);
|
||||
|
||||
$this->success('', [
|
||||
'list' => $res->items(),
|
||||
'total' => $res->total(),
|
||||
'remark' => get_route_remark(),
|
||||
]);
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
|
||||
public function initialize(): void
|
||||
{
|
||||
parent::initialize();
|
||||
$this->model = new \{%modelNamespace%}\{%modelName%}();{%filterRule%}
|
||||
}
|
||||
12
app/admin/library/crud/stubs/mixins/model/afterInsert.stub
Normal file
12
app/admin/library/crud/stubs/mixins/model/afterInsert.stub
Normal file
@@ -0,0 +1,12 @@
|
||||
|
||||
protected static function onAfterInsert($model): void
|
||||
{
|
||||
if (is_null($model->{%field%})) {
|
||||
$pk = $model->getPk();
|
||||
if (strlen($model[$pk]) >= 19) {
|
||||
$model->where($pk, $model[$pk])->update(['{%field%}' => $model->count()]);
|
||||
} else {
|
||||
$model->where($pk, $model[$pk])->update(['{%field%}' => $model[$pk]]);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
|
||||
protected static function onBeforeInsert($model): void
|
||||
{
|
||||
{%setSnowFlakeIdCode%}
|
||||
}
|
||||
5
app/admin/library/crud/stubs/mixins/model/belongsTo.stub
Normal file
5
app/admin/library/crud/stubs/mixins/model/belongsTo.stub
Normal file
@@ -0,0 +1,5 @@
|
||||
|
||||
public function {%relationMethod%}(): \think\model\relation\BelongsTo
|
||||
{
|
||||
return $this->{%relationMode%}({%relationClassName%}, '{%relationForeignKey%}', '{%relationPrimaryKey%}');
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
|
||||
public function get{%field%}Attr($value, $row): string
|
||||
{
|
||||
if ($row['{%originalFieldName%}'] === '' || $row['{%originalFieldName%}'] === null) return '';
|
||||
$cityNames = \support\think\Db::name('area')->whereIn('id', $row['{%originalFieldName%}'])->column('name');
|
||||
return $cityNames ? implode(',', $cityNames) : '';
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
|
||||
public function get{%field%}Attr($value): ?float
|
||||
{
|
||||
return is_null($value) ? null : (float)$value;
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
|
||||
public function get{%field%}Attr($value): string
|
||||
{
|
||||
return !$value ? '' : htmlspecialchars_decode($value);
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
|
||||
public function get{%field%}Attr($value): array
|
||||
{
|
||||
return !$value ? [] : json_decode($value, true);
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
|
||||
public function get{%field%}Attr($value, $row): array
|
||||
{
|
||||
return [
|
||||
'{%labelFieldName%}' => {%className%}::whereIn('{%primaryKey%}', $row['{%foreignKey%}'])->column('{%labelFieldName%}'),
|
||||
];
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
|
||||
public function get{%field%}Attr($value): string
|
||||
{
|
||||
return (string)$value;
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
|
||||
public function get{%field%}Attr($value): array
|
||||
{
|
||||
if ($value === '' || $value === null) return [];
|
||||
if (!is_array($value)) {
|
||||
return explode(',', $value);
|
||||
}
|
||||
return $value;
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
$pk = $model->getPk();
|
||||
$model->$pk = \app\common\library\SnowFlake::generateParticle();
|
||||
18
app/admin/library/crud/stubs/mixins/model/model.stub
Normal file
18
app/admin/library/crud/stubs/mixins/model/model.stub
Normal file
@@ -0,0 +1,18 @@
|
||||
<?php
|
||||
|
||||
namespace {%namespace%};
|
||||
|
||||
use support\think\Model;
|
||||
|
||||
/**
|
||||
* {%className%}
|
||||
*/
|
||||
class {%className%} extends Model
|
||||
{{%connection%}{%pk%}
|
||||
// 表名
|
||||
protected $name = '{%name%}';
|
||||
|
||||
// 自动写入时间戳字段
|
||||
protected $autoWriteTimestamp = {%autoWriteTimestamp%};{%createTime%}{%updateTime%}
|
||||
{%append%}{%fieldType%}{%beforeInsert%}{%afterInsert%}{%methods%}
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
|
||||
public function set{%field%}Attr($value): string
|
||||
{
|
||||
return is_array($value) ? implode(',', $value) : $value;
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
|
||||
public function set{%field%}Attr($value): ?string
|
||||
{
|
||||
return $value ? date('H:i:s', strtotime($value)) : $value;
|
||||
}
|
||||
31
app/admin/library/crud/stubs/mixins/validate/validate.stub
Normal file
31
app/admin/library/crud/stubs/mixins/validate/validate.stub
Normal file
@@ -0,0 +1,31 @@
|
||||
<?php
|
||||
|
||||
namespace {%namespace%};
|
||||
|
||||
use think\Validate;
|
||||
|
||||
class {%className%} extends Validate
|
||||
{
|
||||
protected $failException = true;
|
||||
|
||||
/**
|
||||
* 验证规则
|
||||
*/
|
||||
protected $rule = [
|
||||
];
|
||||
|
||||
/**
|
||||
* 提示消息
|
||||
*/
|
||||
protected $message = [
|
||||
];
|
||||
|
||||
/**
|
||||
* 验证场景
|
||||
*/
|
||||
protected $scene = [
|
||||
'add' => [],
|
||||
'edit' => [],
|
||||
];
|
||||
|
||||
}
|
||||
900
app/admin/library/module/Manage.php
Normal file
900
app/admin/library/module/Manage.php
Normal file
@@ -0,0 +1,900 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace app\admin\library\module;
|
||||
|
||||
use Throwable;
|
||||
use ba\Version;
|
||||
use ba\Depends;
|
||||
use ba\Exception;
|
||||
use ba\Filesystem;
|
||||
use FilesystemIterator;
|
||||
use RecursiveDirectoryIterator;
|
||||
use RecursiveIteratorIterator;
|
||||
use Webman\Http\Request;
|
||||
|
||||
/**
|
||||
* 模块管理类(Webman 迁移版)
|
||||
*/
|
||||
class Manage
|
||||
{
|
||||
public const UNINSTALLED = 0;
|
||||
public const INSTALLED = 1;
|
||||
public const WAIT_INSTALL = 2;
|
||||
public const CONFLICT_PENDING = 3;
|
||||
public const DEPENDENT_WAIT_INSTALL = 4;
|
||||
public const DIRECTORY_OCCUPIED = 5;
|
||||
public const DISABLE = 6;
|
||||
|
||||
protected static ?Manage $instance = null;
|
||||
|
||||
protected string $installDir;
|
||||
|
||||
protected string $backupsDir;
|
||||
|
||||
protected string $uid;
|
||||
|
||||
protected string $modulesDir;
|
||||
|
||||
public static function instance(string $uid = ''): Manage
|
||||
{
|
||||
if (self::$instance === null) {
|
||||
self::$instance = new static($uid);
|
||||
}
|
||||
return self::$instance->setModuleUid($uid);
|
||||
}
|
||||
|
||||
public function __construct(string $uid)
|
||||
{
|
||||
$this->installDir = root_path() . 'modules' . DIRECTORY_SEPARATOR;
|
||||
$this->backupsDir = $this->installDir . 'backups' . DIRECTORY_SEPARATOR;
|
||||
if (!is_dir($this->installDir)) {
|
||||
mkdir($this->installDir, 0755, true);
|
||||
}
|
||||
if (!is_dir($this->backupsDir)) {
|
||||
mkdir($this->backupsDir, 0755, true);
|
||||
}
|
||||
|
||||
if ($uid) {
|
||||
$this->setModuleUid($uid);
|
||||
} else {
|
||||
$this->uid = '';
|
||||
$this->modulesDir = $this->installDir;
|
||||
}
|
||||
}
|
||||
|
||||
public function getInstallState(): int
|
||||
{
|
||||
if (!is_dir($this->modulesDir)) {
|
||||
return self::UNINSTALLED;
|
||||
}
|
||||
$info = $this->getInfo();
|
||||
if ($info && isset($info['state'])) {
|
||||
return $info['state'];
|
||||
}
|
||||
return Filesystem::dirIsEmpty($this->modulesDir) ? self::UNINSTALLED : self::DIRECTORY_OCCUPIED;
|
||||
}
|
||||
|
||||
/**
|
||||
* 从 Webman Request 上传安装(适配 Multipart 上传)
|
||||
* @return array 模块基本信息
|
||||
* @throws Throwable
|
||||
*/
|
||||
public static function uploadFromRequest(Request $request): array
|
||||
{
|
||||
$file = $request->file('file');
|
||||
if (!$file) {
|
||||
throw new Exception('Parameter error');
|
||||
}
|
||||
$token = $request->post('token', $request->get('token', ''));
|
||||
if (!$token) {
|
||||
throw new Exception('Please login to the official website account first');
|
||||
}
|
||||
$uploadDir = root_path() . 'public' . DIRECTORY_SEPARATOR . 'storage' . DIRECTORY_SEPARATOR . 'upload' . DIRECTORY_SEPARATOR;
|
||||
if (!is_dir($uploadDir)) {
|
||||
mkdir($uploadDir, 0755, true);
|
||||
}
|
||||
$saveName = 'temp' . DIRECTORY_SEPARATOR . date('YmdHis') . '_' . ($file->getUploadName() ?? 'module.zip');
|
||||
$savePath = $uploadDir . $saveName;
|
||||
$saveDir = dirname($savePath);
|
||||
if (!is_dir($saveDir)) {
|
||||
mkdir($saveDir, 0755, true);
|
||||
}
|
||||
$file->move($savePath);
|
||||
$relativePath = 'storage/upload/' . str_replace(DIRECTORY_SEPARATOR, '/', $saveName);
|
||||
try {
|
||||
return self::instance('')->doUpload($token, $relativePath);
|
||||
} finally {
|
||||
if (is_file($savePath)) {
|
||||
@unlink($savePath);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 下载模块文件
|
||||
* @throws Throwable
|
||||
*/
|
||||
public function download(): string
|
||||
{
|
||||
$req = function_exists('request') ? request() : null;
|
||||
$token = $req ? ($req->post('token', $req->get('token', ''))) : '';
|
||||
$version = $req ? ($req->post('version', $req->get('version', ''))) : '';
|
||||
$orderId = $req ? ($req->post('orderId', $req->get('orderId', 0))) : 0;
|
||||
|
||||
if (!$orderId) {
|
||||
throw new Exception('Order not found');
|
||||
}
|
||||
|
||||
$zipFile = Server::download($this->uid, $this->installDir, [
|
||||
'version' => $version,
|
||||
'orderId' => $orderId,
|
||||
'nuxtVersion' => Server::getNuxtVersion(),
|
||||
'sysVersion' => config('buildadmin.version', ''),
|
||||
'installed' => Server::getInstalledIds($this->installDir),
|
||||
'ba-user-token' => $token,
|
||||
]);
|
||||
|
||||
Filesystem::delDir($this->modulesDir);
|
||||
Filesystem::unzip($zipFile);
|
||||
@unlink($zipFile);
|
||||
|
||||
$this->checkPackage();
|
||||
|
||||
$this->setInfo([
|
||||
'state' => self::WAIT_INSTALL,
|
||||
]);
|
||||
|
||||
return $zipFile;
|
||||
}
|
||||
|
||||
/**
|
||||
* 上传安装(token + 文件相对路径)
|
||||
* @throws Throwable
|
||||
*/
|
||||
public function doUpload(string $token, string $file): array
|
||||
{
|
||||
$file = Filesystem::fsFit(root_path() . 'public' . DIRECTORY_SEPARATOR . str_replace('/', DIRECTORY_SEPARATOR, $file));
|
||||
if (!is_file($file)) {
|
||||
throw new Exception('Zip file not found');
|
||||
}
|
||||
|
||||
$copyTo = $this->installDir . 'uploadTemp' . date('YmdHis') . '.zip';
|
||||
copy($file, $copyTo);
|
||||
|
||||
$copyToDir = Filesystem::unzip($copyTo);
|
||||
$copyToDir .= DIRECTORY_SEPARATOR;
|
||||
|
||||
@unlink($file);
|
||||
@unlink($copyTo);
|
||||
|
||||
$info = Server::getIni($copyToDir);
|
||||
if (empty($info['uid'])) {
|
||||
Filesystem::delDir($copyToDir);
|
||||
throw new Exception('Basic configuration of the Module is incomplete');
|
||||
}
|
||||
|
||||
$this->setModuleUid($info['uid']);
|
||||
|
||||
$upgrade = false;
|
||||
if (is_dir($this->modulesDir)) {
|
||||
$oldInfo = $this->getInfo();
|
||||
if ($oldInfo && !empty($oldInfo['uid'])) {
|
||||
$versions = explode('.', $oldInfo['version'] ?? '0.0.0');
|
||||
if (isset($versions[2])) {
|
||||
$versions[2]++;
|
||||
}
|
||||
$nextVersion = implode('.', $versions);
|
||||
$upgrade = Version::compare($nextVersion, $info['version'] ?? '');
|
||||
if ($upgrade) {
|
||||
if (!in_array($oldInfo['state'], [self::UNINSTALLED, self::WAIT_INSTALL, self::DISABLE])) {
|
||||
Filesystem::delDir($copyToDir);
|
||||
throw new Exception('Please disable the module before updating');
|
||||
}
|
||||
} else {
|
||||
Filesystem::delDir($copyToDir);
|
||||
throw new Exception('Module already exists');
|
||||
}
|
||||
}
|
||||
|
||||
if (!Filesystem::dirIsEmpty($this->modulesDir) && !$upgrade) {
|
||||
Filesystem::delDir($copyToDir);
|
||||
throw new Exception('The directory required by the module is occupied');
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
Server::installPreCheck([
|
||||
'uid' => $info['uid'],
|
||||
'version' => $info['version'] ?? '',
|
||||
'sysVersion' => config('buildadmin.version', ''),
|
||||
'nuxtVersion' => Server::getNuxtVersion(),
|
||||
'moduleVersion' => $info['version'] ?? '',
|
||||
'ba-user-token' => $token,
|
||||
'installed' => Server::getInstalledIds($this->installDir),
|
||||
'server' => 1,
|
||||
]);
|
||||
} catch (Throwable $e) {
|
||||
Filesystem::delDir($copyToDir);
|
||||
throw $e;
|
||||
}
|
||||
|
||||
$newInfo = ['state' => self::WAIT_INSTALL];
|
||||
if ($upgrade) {
|
||||
$info['update'] = 1;
|
||||
Filesystem::delDir($this->modulesDir);
|
||||
}
|
||||
|
||||
rename($copyToDir, $this->modulesDir);
|
||||
|
||||
$this->checkPackage();
|
||||
|
||||
$this->setInfo($newInfo);
|
||||
|
||||
return $info;
|
||||
}
|
||||
|
||||
/**
|
||||
* 安装模块
|
||||
* @throws Throwable
|
||||
*/
|
||||
public function install(bool $update): array
|
||||
{
|
||||
$state = $this->getInstallState();
|
||||
|
||||
if ($update) {
|
||||
if (!in_array($state, [self::UNINSTALLED, self::WAIT_INSTALL, self::DISABLE])) {
|
||||
throw new Exception('Please disable the module before updating');
|
||||
}
|
||||
if ($state == self::UNINSTALLED || $state != self::WAIT_INSTALL) {
|
||||
$this->download();
|
||||
}
|
||||
} else {
|
||||
if ($state == self::INSTALLED || $state == self::DIRECTORY_OCCUPIED || $state == self::DISABLE) {
|
||||
throw new Exception('Module already exists');
|
||||
}
|
||||
if ($state == self::UNINSTALLED) {
|
||||
$this->download();
|
||||
}
|
||||
}
|
||||
|
||||
Server::importSql($this->modulesDir);
|
||||
|
||||
$info = $this->getInfo();
|
||||
if ($update) {
|
||||
$info['update'] = 1;
|
||||
Server::execEvent($this->uid, 'update');
|
||||
}
|
||||
|
||||
$req = function_exists('request') ? request() : null;
|
||||
$extend = $req ? ($req->post('extend') ?? []) : [];
|
||||
if (!isset($extend['conflictHandle'])) {
|
||||
Server::execEvent($this->uid, 'install');
|
||||
}
|
||||
|
||||
$this->enable('install');
|
||||
|
||||
return $info;
|
||||
}
|
||||
|
||||
/**
|
||||
* 卸载
|
||||
* @throws Throwable
|
||||
*/
|
||||
public function uninstall(): void
|
||||
{
|
||||
$info = $this->getInfo();
|
||||
if (($info['state'] ?? 0) != self::DISABLE) {
|
||||
throw new Exception('Please disable the module first', 0, [
|
||||
'uid' => $this->uid,
|
||||
]);
|
||||
}
|
||||
|
||||
Server::execEvent($this->uid, 'uninstall');
|
||||
|
||||
Filesystem::delDir($this->modulesDir);
|
||||
}
|
||||
|
||||
/**
|
||||
* 修改模块状态
|
||||
* @throws Throwable
|
||||
*/
|
||||
public function changeState(bool $state): array
|
||||
{
|
||||
$info = $this->getInfo();
|
||||
if (!$state) {
|
||||
$canDisable = [
|
||||
self::INSTALLED,
|
||||
self::CONFLICT_PENDING,
|
||||
self::DEPENDENT_WAIT_INSTALL,
|
||||
];
|
||||
if (!in_array($info['state'] ?? 0, $canDisable)) {
|
||||
throw new Exception('The current state of the module cannot be set to disabled', 0, [
|
||||
'uid' => $this->uid,
|
||||
'state' => $info['state'] ?? 0,
|
||||
]);
|
||||
}
|
||||
return $this->disable();
|
||||
}
|
||||
|
||||
if (($info['state'] ?? 0) != self::DISABLE) {
|
||||
throw new Exception('The current state of the module cannot be set to enabled', 0, [
|
||||
'uid' => $this->uid,
|
||||
'state' => $info['state'] ?? 0,
|
||||
]);
|
||||
}
|
||||
$this->setInfo([
|
||||
'state' => self::WAIT_INSTALL,
|
||||
]);
|
||||
return $info;
|
||||
}
|
||||
|
||||
/**
|
||||
* 启用
|
||||
* @throws Throwable
|
||||
*/
|
||||
public function enable(string $trigger): void
|
||||
{
|
||||
Server::installWebBootstrap($this->uid, $this->modulesDir);
|
||||
Server::createRuntime($this->modulesDir);
|
||||
$this->conflictHandle($trigger);
|
||||
Server::execEvent($this->uid, 'enable');
|
||||
$this->dependUpdateHandle();
|
||||
}
|
||||
|
||||
/**
|
||||
* 禁用
|
||||
* @throws Throwable
|
||||
*/
|
||||
public function disable(): array
|
||||
{
|
||||
$req = function_exists('request') ? request() : null;
|
||||
$update = $req ? filter_var($req->post('update', false), FILTER_VALIDATE_BOOLEAN) : false;
|
||||
$confirmConflict = $req ? filter_var($req->post('confirmConflict', false), FILTER_VALIDATE_BOOLEAN) : false;
|
||||
$dependConflictSolution = $req ? ($req->post('dependConflictSolution') ?? []) : [];
|
||||
|
||||
$info = $this->getInfo();
|
||||
$zipFile = $this->backupsDir . $this->uid . '-install.zip';
|
||||
$zipDir = false;
|
||||
if (is_file($zipFile)) {
|
||||
try {
|
||||
$zipDir = $this->backupsDir . $this->uid . '-install' . DIRECTORY_SEPARATOR;
|
||||
Filesystem::unzip($zipFile, $zipDir);
|
||||
} catch (Exception) {
|
||||
// skip
|
||||
}
|
||||
}
|
||||
|
||||
$conflictFile = Server::getFileList($this->modulesDir, true);
|
||||
$dependConflict = $this->disableDependCheck();
|
||||
if (($conflictFile || !self::isEmptyArray($dependConflict)) && !$confirmConflict) {
|
||||
$dependConflictTemp = [];
|
||||
foreach ($dependConflict as $env => $item) {
|
||||
foreach ($item as $depend => $v) {
|
||||
$dependConflictTemp[] = [
|
||||
'env' => $env,
|
||||
'depend' => $depend,
|
||||
'dependTitle' => $depend . ' ' . $v,
|
||||
'solution' => 'delete',
|
||||
];
|
||||
}
|
||||
}
|
||||
throw new Exception('Module file updated', -1, [
|
||||
'uid' => $this->uid,
|
||||
'conflictFile' => $conflictFile,
|
||||
'dependConflict' => $dependConflictTemp,
|
||||
]);
|
||||
}
|
||||
|
||||
Server::execEvent($this->uid, 'disable', ['update' => $update]);
|
||||
|
||||
$delNpmDepend = false;
|
||||
$delNuxtNpmDepend = false;
|
||||
$delComposerDepend = false;
|
||||
foreach ($dependConflictSolution as $env => $depends) {
|
||||
if (!$depends) continue;
|
||||
if ($env == 'require' || $env == 'require-dev') {
|
||||
$delComposerDepend = true;
|
||||
} elseif ($env == 'dependencies' || $env == 'devDependencies') {
|
||||
$delNpmDepend = true;
|
||||
} elseif ($env == 'nuxtDependencies' || $env == 'nuxtDevDependencies') {
|
||||
$delNuxtNpmDepend = true;
|
||||
}
|
||||
}
|
||||
|
||||
$dependJsonFiles = [
|
||||
'composer' => 'composer.json',
|
||||
'webPackage' => 'web' . DIRECTORY_SEPARATOR . 'package.json',
|
||||
'webNuxtPackage' => 'web-nuxt' . DIRECTORY_SEPARATOR . 'package.json',
|
||||
];
|
||||
$dependWaitInstall = [];
|
||||
if ($delComposerDepend) {
|
||||
$conflictFile[] = $dependJsonFiles['composer'];
|
||||
$dependWaitInstall[] = [
|
||||
'pm' => false,
|
||||
'command' => 'composer.update',
|
||||
'type' => 'composer_dependent_wait_install',
|
||||
];
|
||||
}
|
||||
if ($delNpmDepend) {
|
||||
$conflictFile[] = $dependJsonFiles['webPackage'];
|
||||
$dependWaitInstall[] = [
|
||||
'pm' => true,
|
||||
'command' => 'web-install',
|
||||
'type' => 'npm_dependent_wait_install',
|
||||
];
|
||||
}
|
||||
if ($delNuxtNpmDepend) {
|
||||
$conflictFile[] = $dependJsonFiles['webNuxtPackage'];
|
||||
$dependWaitInstall[] = [
|
||||
'pm' => true,
|
||||
'command' => 'nuxt-install',
|
||||
'type' => 'nuxt_npm_dependent_wait_install',
|
||||
];
|
||||
}
|
||||
if ($conflictFile) {
|
||||
$overwriteDir = Server::getOverwriteDir();
|
||||
foreach ($conflictFile as $key => $item) {
|
||||
$paths = explode(DIRECTORY_SEPARATOR, $item);
|
||||
if (in_array($paths[0], $overwriteDir) || in_array($item, $dependJsonFiles)) {
|
||||
$conflictFile[$key] = $item;
|
||||
} else {
|
||||
$conflictFile[$key] = Filesystem::fsFit(str_replace(root_path(), '', $this->modulesDir . $item));
|
||||
}
|
||||
if (!is_file(root_path() . $conflictFile[$key])) {
|
||||
unset($conflictFile[$key]);
|
||||
}
|
||||
}
|
||||
$backupsZip = $this->backupsDir . $this->uid . '-disable-' . date('YmdHis') . '.zip';
|
||||
Filesystem::zip($conflictFile, $backupsZip);
|
||||
}
|
||||
|
||||
$serverDepend = new Depends(root_path() . 'composer.json', 'composer');
|
||||
$webDep = new Depends(root_path() . 'web' . DIRECTORY_SEPARATOR . 'package.json');
|
||||
$webNuxtDep = new Depends(root_path() . 'web-nuxt' . DIRECTORY_SEPARATOR . 'package.json');
|
||||
foreach ($dependConflictSolution as $env => $depends) {
|
||||
if (!$depends) continue;
|
||||
$dev = stripos($env, 'dev') !== false;
|
||||
if ($env == 'require' || $env == 'require-dev') {
|
||||
$serverDepend->removeDepends($depends, $dev);
|
||||
} elseif ($env == 'dependencies' || $env == 'devDependencies') {
|
||||
$webDep->removeDepends($depends, $dev);
|
||||
} elseif ($env == 'nuxtDependencies' || $env == 'nuxtDevDependencies') {
|
||||
$webNuxtDep->removeDepends($depends, $dev);
|
||||
}
|
||||
}
|
||||
|
||||
$composerConfig = Server::getConfig($this->modulesDir, 'composerConfig');
|
||||
if ($composerConfig) {
|
||||
$serverDepend->removeComposerConfig($composerConfig);
|
||||
}
|
||||
|
||||
$protectedFiles = Server::getConfig($this->modulesDir, 'protectedFiles');
|
||||
foreach ($protectedFiles as &$protectedFile) {
|
||||
$protectedFile = Filesystem::fsFit(root_path() . $protectedFile);
|
||||
}
|
||||
$moduleFile = Server::getFileList($this->modulesDir);
|
||||
|
||||
foreach ($moduleFile as &$file) {
|
||||
$moduleFilePath = Filesystem::fsFit($this->modulesDir . $file);
|
||||
$file = Filesystem::fsFit(root_path() . $file);
|
||||
if (!file_exists($file)) continue;
|
||||
if (!file_exists($moduleFilePath)) {
|
||||
if (!is_dir(dirname($moduleFilePath))) {
|
||||
mkdir(dirname($moduleFilePath), 0755, true);
|
||||
}
|
||||
copy($file, $moduleFilePath);
|
||||
}
|
||||
|
||||
if (in_array($file, $protectedFiles)) {
|
||||
continue;
|
||||
}
|
||||
if (file_exists($file)) {
|
||||
unlink($file);
|
||||
}
|
||||
Filesystem::delEmptyDir(dirname($file));
|
||||
}
|
||||
|
||||
if ($zipDir) {
|
||||
$unrecoverableFiles = [
|
||||
Filesystem::fsFit(root_path() . 'composer.json'),
|
||||
Filesystem::fsFit(root_path() . 'web/package.json'),
|
||||
Filesystem::fsFit(root_path() . 'web-nuxt/package.json'),
|
||||
];
|
||||
foreach (
|
||||
new RecursiveIteratorIterator(
|
||||
new RecursiveDirectoryIterator($zipDir, FilesystemIterator::SKIP_DOTS),
|
||||
RecursiveIteratorIterator::SELF_FIRST
|
||||
) as $item
|
||||
) {
|
||||
$backupsFile = Filesystem::fsFit(root_path() . str_replace($zipDir, '', $item->getPathname()));
|
||||
|
||||
if (in_array($backupsFile, $moduleFile) && !in_array($backupsFile, $protectedFiles)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if ($item->isDir()) {
|
||||
if (!is_dir($backupsFile)) {
|
||||
mkdir($backupsFile, 0755, true);
|
||||
}
|
||||
} elseif (!in_array($backupsFile, $unrecoverableFiles)) {
|
||||
copy($item->getPathname(), $backupsFile);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if ($zipDir && is_dir($zipDir)) {
|
||||
Filesystem::delDir($zipDir);
|
||||
}
|
||||
|
||||
Server::uninstallWebBootstrap($this->uid);
|
||||
|
||||
$this->setInfo([
|
||||
'state' => self::DISABLE,
|
||||
]);
|
||||
|
||||
if ($update) {
|
||||
throw new Exception('update', -3, [
|
||||
'uid' => $this->uid,
|
||||
]);
|
||||
}
|
||||
|
||||
if (!empty($dependWaitInstall)) {
|
||||
throw new Exception('dependent wait install', -2, [
|
||||
'uid' => $this->uid,
|
||||
'wait_install' => $dependWaitInstall,
|
||||
]);
|
||||
}
|
||||
return $info;
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理依赖和文件冲突
|
||||
* @throws Throwable
|
||||
*/
|
||||
public function conflictHandle(string $trigger): bool
|
||||
{
|
||||
$info = $this->getInfo();
|
||||
if (!in_array($info['state'] ?? 0, [self::WAIT_INSTALL, self::CONFLICT_PENDING])) {
|
||||
return false;
|
||||
}
|
||||
$fileConflict = Server::getFileList($this->modulesDir, true);
|
||||
$dependConflict = Server::dependConflictCheck($this->modulesDir);
|
||||
$installFiles = Server::getFileList($this->modulesDir);
|
||||
$depends = Server::getDepend($this->modulesDir);
|
||||
|
||||
$coverFiles = [];
|
||||
$discardFiles = [];
|
||||
$serverDep = new Depends(root_path() . 'composer.json', 'composer');
|
||||
$webDep = new Depends(root_path() . 'web' . DIRECTORY_SEPARATOR . 'package.json');
|
||||
$webNuxtDep = new Depends(root_path() . 'web-nuxt' . DIRECTORY_SEPARATOR . 'package.json');
|
||||
|
||||
$req = function_exists('request') ? request() : null;
|
||||
$extend = $req ? ($req->post('extend') ?? []) : [];
|
||||
|
||||
if ($fileConflict || !self::isEmptyArray($dependConflict)) {
|
||||
if (!$extend) {
|
||||
$fileConflictTemp = [];
|
||||
foreach ($fileConflict as $key => $item) {
|
||||
$fileConflictTemp[$key] = [
|
||||
'newFile' => $this->uid . DIRECTORY_SEPARATOR . $item,
|
||||
'oldFile' => $item,
|
||||
'solution' => 'cover',
|
||||
];
|
||||
}
|
||||
$dependConflictTemp = [];
|
||||
foreach ($dependConflict as $env => $item) {
|
||||
$dev = stripos($env, 'dev') !== false;
|
||||
foreach ($item as $depend => $v) {
|
||||
$oldDepend = '';
|
||||
if (in_array($env, ['require', 'require-dev'])) {
|
||||
$oldDepend = $depend . ' ' . $serverDep->hasDepend($depend, $dev);
|
||||
} elseif (in_array($env, ['dependencies', 'devDependencies'])) {
|
||||
$oldDepend = $depend . ' ' . $webDep->hasDepend($depend, $dev);
|
||||
} elseif (in_array($env, ['nuxtDependencies', 'nuxtDevDependencies'])) {
|
||||
$oldDepend = $depend . ' ' . $webNuxtDep->hasDepend($depend, $dev);
|
||||
}
|
||||
$dependConflictTemp[] = [
|
||||
'env' => $env,
|
||||
'newDepend' => $depend . ' ' . $v,
|
||||
'oldDepend' => $oldDepend,
|
||||
'depend' => $depend,
|
||||
'solution' => 'cover',
|
||||
];
|
||||
}
|
||||
}
|
||||
$this->setInfo([
|
||||
'state' => self::CONFLICT_PENDING,
|
||||
]);
|
||||
throw new Exception('Module file conflicts', -1, [
|
||||
'fileConflict' => $fileConflictTemp,
|
||||
'dependConflict' => $dependConflictTemp,
|
||||
'uid' => $this->uid,
|
||||
'state' => self::CONFLICT_PENDING,
|
||||
]);
|
||||
}
|
||||
|
||||
if ($fileConflict && isset($extend['fileConflict'])) {
|
||||
foreach ($installFiles as $ikey => $installFile) {
|
||||
if (isset($extend['fileConflict'][$installFile])) {
|
||||
if ($extend['fileConflict'][$installFile] == 'discard') {
|
||||
$discardFiles[] = $installFile;
|
||||
unset($installFiles[$ikey]);
|
||||
} else {
|
||||
$coverFiles[] = $installFile;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!self::isEmptyArray($dependConflict) && isset($extend['dependConflict'])) {
|
||||
foreach ($depends as $fKey => $fItem) {
|
||||
foreach ($fItem as $cKey => $cItem) {
|
||||
if (isset($extend['dependConflict'][$fKey][$cKey])) {
|
||||
if ($extend['dependConflict'][$fKey][$cKey] == 'discard') {
|
||||
unset($depends[$fKey][$cKey]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if ($depends) {
|
||||
foreach ($depends as $key => $item) {
|
||||
if (!$item) continue;
|
||||
if ($key == 'require' || $key == 'require-dev') {
|
||||
$coverFiles[] = 'composer.json';
|
||||
continue;
|
||||
}
|
||||
if ($key == 'dependencies' || $key == 'devDependencies') {
|
||||
$coverFiles[] = 'web' . DIRECTORY_SEPARATOR . 'package.json';
|
||||
}
|
||||
if ($key == 'nuxtDependencies' || $key == 'nuxtDevDependencies') {
|
||||
$coverFiles[] = 'web-nuxt' . DIRECTORY_SEPARATOR . 'package.json';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if ($coverFiles) {
|
||||
$backupsZip = $trigger == 'install' ? $this->backupsDir . $this->uid . '-install.zip' : $this->backupsDir . $this->uid . '-cover-' . date('YmdHis') . '.zip';
|
||||
Filesystem::zip($coverFiles, $backupsZip);
|
||||
}
|
||||
|
||||
if ($depends) {
|
||||
$npm = false;
|
||||
$composer = false;
|
||||
$nuxtNpm = false;
|
||||
|
||||
$composerConfig = Server::getConfig($this->modulesDir, 'composerConfig');
|
||||
if ($composerConfig) {
|
||||
$serverDep->setComposerConfig($composerConfig);
|
||||
}
|
||||
|
||||
foreach ($depends as $key => $item) {
|
||||
if (!$item) continue;
|
||||
if ($key == 'require') {
|
||||
$composer = true;
|
||||
$serverDep->addDepends($item, false, true);
|
||||
} elseif ($key == 'require-dev') {
|
||||
$composer = true;
|
||||
$serverDep->addDepends($item, true, true);
|
||||
} elseif ($key == 'dependencies') {
|
||||
$npm = true;
|
||||
$webDep->addDepends($item, false, true);
|
||||
} elseif ($key == 'devDependencies') {
|
||||
$npm = true;
|
||||
$webDep->addDepends($item, true, true);
|
||||
} elseif ($key == 'nuxtDependencies') {
|
||||
$nuxtNpm = true;
|
||||
$webNuxtDep->addDepends($item, false, true);
|
||||
} elseif ($key == 'nuxtDevDependencies') {
|
||||
$nuxtNpm = true;
|
||||
$webNuxtDep->addDepends($item, true, true);
|
||||
}
|
||||
}
|
||||
if ($npm) {
|
||||
$info['npm_dependent_wait_install'] = 1;
|
||||
$info['state'] = self::DEPENDENT_WAIT_INSTALL;
|
||||
}
|
||||
if ($composer) {
|
||||
$info['composer_dependent_wait_install'] = 1;
|
||||
$info['state'] = self::DEPENDENT_WAIT_INSTALL;
|
||||
}
|
||||
if ($nuxtNpm) {
|
||||
$info['nuxt_npm_dependent_wait_install'] = 1;
|
||||
$info['state'] = self::DEPENDENT_WAIT_INSTALL;
|
||||
}
|
||||
$info = $info ?? $this->getInfo();
|
||||
if (($info['state'] ?? 0) != self::DEPENDENT_WAIT_INSTALL) {
|
||||
$this->setInfo(['state' => self::INSTALLED]);
|
||||
} else {
|
||||
$this->setInfo([], $info);
|
||||
}
|
||||
} else {
|
||||
$this->setInfo(['state' => self::INSTALLED]);
|
||||
}
|
||||
|
||||
$overwriteDir = Server::getOverwriteDir();
|
||||
foreach ($overwriteDir as $dirItem) {
|
||||
$baseDir = $this->modulesDir . $dirItem;
|
||||
$destDir = root_path() . $dirItem;
|
||||
if (!is_dir($baseDir)) continue;
|
||||
|
||||
foreach (
|
||||
new RecursiveIteratorIterator(
|
||||
new RecursiveDirectoryIterator($baseDir, FilesystemIterator::SKIP_DOTS),
|
||||
RecursiveIteratorIterator::SELF_FIRST
|
||||
) as $item
|
||||
) {
|
||||
$destDirItem = Filesystem::fsFit($destDir . DIRECTORY_SEPARATOR . str_replace($baseDir, '', $item->getPathname()));
|
||||
if ($item->isDir()) {
|
||||
Filesystem::mkdir($destDirItem);
|
||||
} elseif (!in_array(str_replace(root_path(), '', $destDirItem), $discardFiles)) {
|
||||
Filesystem::mkdir(dirname($destDirItem));
|
||||
copy($item->getPathname(), $destDirItem);
|
||||
}
|
||||
}
|
||||
if (config('buildadmin.module_pure_install', false)) {
|
||||
Filesystem::delDir($baseDir);
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* 依赖升级处理
|
||||
* @throws Throwable
|
||||
*/
|
||||
public function dependUpdateHandle(): void
|
||||
{
|
||||
$info = $this->getInfo();
|
||||
if (($info['state'] ?? 0) == self::DEPENDENT_WAIT_INSTALL) {
|
||||
$waitInstall = [];
|
||||
if (isset($info['composer_dependent_wait_install'])) {
|
||||
$waitInstall[] = 'composer_dependent_wait_install';
|
||||
}
|
||||
if (isset($info['npm_dependent_wait_install'])) {
|
||||
$waitInstall[] = 'npm_dependent_wait_install';
|
||||
}
|
||||
if (isset($info['nuxt_npm_dependent_wait_install'])) {
|
||||
$waitInstall[] = 'nuxt_npm_dependent_wait_install';
|
||||
}
|
||||
if ($waitInstall) {
|
||||
throw new Exception('dependent wait install', -2, [
|
||||
'uid' => $this->uid,
|
||||
'state' => self::DEPENDENT_WAIT_INSTALL,
|
||||
'wait_install' => $waitInstall,
|
||||
]);
|
||||
} else {
|
||||
$this->setInfo(['state' => self::INSTALLED]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 依赖安装完成标记
|
||||
* @throws Throwable
|
||||
*/
|
||||
public function dependentInstallComplete(string $type): void
|
||||
{
|
||||
$info = $this->getInfo();
|
||||
if (($info['state'] ?? 0) == self::DEPENDENT_WAIT_INSTALL) {
|
||||
if ($type == 'npm') {
|
||||
unset($info['npm_dependent_wait_install']);
|
||||
}
|
||||
if ($type == 'nuxt_npm') {
|
||||
unset($info['nuxt_npm_dependent_wait_install']);
|
||||
}
|
||||
if ($type == 'composer') {
|
||||
unset($info['composer_dependent_wait_install']);
|
||||
}
|
||||
if ($type == 'all') {
|
||||
unset($info['npm_dependent_wait_install'], $info['composer_dependent_wait_install'], $info['nuxt_npm_dependent_wait_install']);
|
||||
}
|
||||
if (!isset($info['npm_dependent_wait_install']) && !isset($info['composer_dependent_wait_install']) && !isset($info['nuxt_npm_dependent_wait_install'])) {
|
||||
$info['state'] = self::INSTALLED;
|
||||
}
|
||||
$this->setInfo([], $info);
|
||||
}
|
||||
}
|
||||
|
||||
public function disableDependCheck(): array
|
||||
{
|
||||
$depend = Server::getDepend($this->modulesDir);
|
||||
if (!$depend) return [];
|
||||
|
||||
$serverDep = new Depends(root_path() . 'composer.json', 'composer');
|
||||
$webDep = new Depends(root_path() . 'web' . DIRECTORY_SEPARATOR . 'package.json');
|
||||
$webNuxtDep = new Depends(root_path() . 'web-nuxt' . DIRECTORY_SEPARATOR . 'package.json');
|
||||
|
||||
foreach ($depend as $key => $depends) {
|
||||
$dev = stripos($key, 'dev') !== false;
|
||||
if ($key == 'require' || $key == 'require-dev') {
|
||||
foreach ($depends as $dependKey => $dependItem) {
|
||||
if (!$serverDep->hasDepend($dependKey, $dev)) {
|
||||
unset($depends[$dependKey]);
|
||||
}
|
||||
}
|
||||
$depend[$key] = $depends;
|
||||
} elseif ($key == 'dependencies' || $key == 'devDependencies') {
|
||||
foreach ($depends as $dependKey => $dependItem) {
|
||||
if (!$webDep->hasDepend($dependKey, $dev)) {
|
||||
unset($depends[$dependKey]);
|
||||
}
|
||||
}
|
||||
$depend[$key] = $depends;
|
||||
} elseif ($key == 'nuxtDependencies' || $key == 'nuxtDevDependencies') {
|
||||
foreach ($depends as $dependKey => $dependItem) {
|
||||
if (!$webNuxtDep->hasDepend($dependKey, $dev)) {
|
||||
unset($depends[$dependKey]);
|
||||
}
|
||||
}
|
||||
$depend[$key] = $depends;
|
||||
}
|
||||
}
|
||||
return $depend;
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查包是否完整
|
||||
* @throws Throwable
|
||||
*/
|
||||
public function checkPackage(): bool
|
||||
{
|
||||
if (!is_dir($this->modulesDir)) {
|
||||
throw new Exception('Module package file does not exist');
|
||||
}
|
||||
$info = $this->getInfo();
|
||||
$infoKeys = ['uid', 'title', 'intro', 'author', 'version', 'state'];
|
||||
foreach ($infoKeys as $value) {
|
||||
if (!array_key_exists($value, $info)) {
|
||||
Filesystem::delDir($this->modulesDir);
|
||||
throw new Exception('Basic configuration of the Module is incomplete');
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
public function getInfo(): array
|
||||
{
|
||||
return Server::getIni($this->modulesDir);
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws Throwable
|
||||
*/
|
||||
public function setInfo(array $kv = [], array $arr = []): bool
|
||||
{
|
||||
if ($kv) {
|
||||
$info = $this->getInfo();
|
||||
foreach ($kv as $k => $v) {
|
||||
$info[$k] = $v;
|
||||
}
|
||||
return Server::setIni($this->modulesDir, $info);
|
||||
}
|
||||
if ($arr) {
|
||||
return Server::setIni($this->modulesDir, $arr);
|
||||
}
|
||||
throw new Exception('Parameter error');
|
||||
}
|
||||
|
||||
public static function isEmptyArray($arr): bool
|
||||
{
|
||||
foreach ($arr as $item) {
|
||||
if (is_array($item)) {
|
||||
if (!self::isEmptyArray($item)) return false;
|
||||
} elseif ($item) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
public function setModuleUid(string $uid): static
|
||||
{
|
||||
$this->uid = $uid;
|
||||
$this->modulesDir = $this->installDir . $uid . DIRECTORY_SEPARATOR;
|
||||
return $this;
|
||||
}
|
||||
}
|
||||
551
app/admin/library/module/Server.php
Normal file
551
app/admin/library/module/Server.php
Normal file
@@ -0,0 +1,551 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace app\admin\library\module;
|
||||
|
||||
use Throwable;
|
||||
use ba\Depends;
|
||||
use ba\Exception;
|
||||
use ba\Filesystem;
|
||||
use support\think\Db;
|
||||
use FilesystemIterator;
|
||||
use RecursiveIteratorIterator;
|
||||
use RecursiveDirectoryIterator;
|
||||
use think\db\exception\PDOException;
|
||||
use app\admin\library\crud\Helper;
|
||||
use GuzzleHttp\Exception\TransferException;
|
||||
|
||||
/**
|
||||
* 模块服务类(Webman 迁移版)
|
||||
*/
|
||||
class Server
|
||||
{
|
||||
private static string $apiBaseUrl = '/api/v7.store/';
|
||||
|
||||
/**
|
||||
* 下载
|
||||
* @throws Throwable
|
||||
*/
|
||||
public static function download(string $uid, string $dir, array $extend = []): string
|
||||
{
|
||||
$tmpFile = $dir . $uid . ".zip";
|
||||
try {
|
||||
$client = get_ba_client();
|
||||
$response = $client->get(self::$apiBaseUrl . 'download', ['query' => array_merge(['uid' => $uid, 'server' => 1], $extend)]);
|
||||
$body = $response->getBody();
|
||||
$content = $body->getContents();
|
||||
if ($content == '' || stripos($content, '<title>系统发生错误</title>') !== false) {
|
||||
throw new Exception('package download failed', 0);
|
||||
}
|
||||
if (str_starts_with($content, '{')) {
|
||||
$json = (array)json_decode($content, true);
|
||||
throw new Exception($json['msg'], $json['code'], $json['data'] ?? []);
|
||||
}
|
||||
} catch (TransferException $e) {
|
||||
throw new Exception('package download failed', 0, ['msg' => $e->getMessage()]);
|
||||
}
|
||||
|
||||
if ($write = fopen($tmpFile, 'w')) {
|
||||
fwrite($write, $content);
|
||||
fclose($write);
|
||||
return $tmpFile;
|
||||
}
|
||||
throw new Exception("No permission to write temporary files");
|
||||
}
|
||||
|
||||
/**
|
||||
* 安装预检
|
||||
* @throws Throwable
|
||||
*/
|
||||
public static function installPreCheck(array $query = []): bool
|
||||
{
|
||||
try {
|
||||
$client = get_ba_client();
|
||||
$response = $client->get(self::$apiBaseUrl . 'preCheck', ['query' => $query]);
|
||||
$body = $response->getBody();
|
||||
$statusCode = $response->getStatusCode();
|
||||
$content = $body->getContents();
|
||||
if ($content == '' || stripos($content, '<title>系统发生错误</title>') !== false || $statusCode != 200) {
|
||||
return true;
|
||||
}
|
||||
if (str_starts_with($content, '{')) {
|
||||
$json = json_decode($content, true);
|
||||
if ($json && $json['code'] == 0) {
|
||||
throw new Exception($json['msg'], $json['code'], $json['data'] ?? []);
|
||||
}
|
||||
}
|
||||
} catch (TransferException $e) {
|
||||
throw new Exception('package check failed', 0, ['msg' => $e->getMessage()]);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
public static function getConfig(string $dir, $key = ''): array
|
||||
{
|
||||
$configFile = $dir . 'config.json';
|
||||
if (!is_dir($dir) || !is_file($configFile)) {
|
||||
return [];
|
||||
}
|
||||
$configContent = @file_get_contents($configFile);
|
||||
$configContent = json_decode($configContent, true);
|
||||
if (!$configContent) {
|
||||
return [];
|
||||
}
|
||||
if ($key) {
|
||||
return $configContent[$key] ?? [];
|
||||
}
|
||||
return $configContent;
|
||||
}
|
||||
|
||||
public static function getDepend(string $dir, string $key = ''): array
|
||||
{
|
||||
if ($key) {
|
||||
return self::getConfig($dir, $key);
|
||||
}
|
||||
$configContent = self::getConfig($dir);
|
||||
$dependKey = ['require', 'require-dev', 'dependencies', 'devDependencies', 'nuxtDependencies', 'nuxtDevDependencies'];
|
||||
$dependArray = [];
|
||||
foreach ($dependKey as $item) {
|
||||
if (array_key_exists($item, $configContent) && $configContent[$item]) {
|
||||
$dependArray[$item] = $configContent[$item];
|
||||
}
|
||||
}
|
||||
return $dependArray;
|
||||
}
|
||||
|
||||
/**
|
||||
* 依赖冲突检查
|
||||
* @throws Throwable
|
||||
*/
|
||||
public static function dependConflictCheck(string $dir): array
|
||||
{
|
||||
$depend = self::getDepend($dir);
|
||||
$serverDep = new Depends(root_path() . 'composer.json', 'composer');
|
||||
$webDep = new Depends(root_path() . 'web' . DIRECTORY_SEPARATOR . 'package.json');
|
||||
$webNuxtDep = new Depends(root_path() . 'web-nuxt' . DIRECTORY_SEPARATOR . 'package.json');
|
||||
$sysDepend = [
|
||||
'require' => $serverDep->getDepends(),
|
||||
'require-dev' => $serverDep->getDepends(true),
|
||||
'dependencies' => $webDep->getDepends(),
|
||||
'devDependencies' => $webDep->getDepends(true),
|
||||
'nuxtDependencies' => $webNuxtDep->getDepends(),
|
||||
'nuxtDevDependencies' => $webNuxtDep->getDepends(true),
|
||||
];
|
||||
|
||||
$conflict = [];
|
||||
foreach ($depend as $key => $item) {
|
||||
$conflict[$key] = array_uintersect_assoc($item, $sysDepend[$key] ?? [], function ($a, $b) {
|
||||
return $a == $b ? -1 : 0;
|
||||
});
|
||||
}
|
||||
return $conflict;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取模块[冲突]文件列表
|
||||
*/
|
||||
public static function getFileList(string $dir, bool $onlyConflict = false): array
|
||||
{
|
||||
if (!is_dir($dir)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$fileList = [];
|
||||
$overwriteDir = self::getOverwriteDir();
|
||||
$moduleFileList = self::getRuntime($dir, 'files');
|
||||
|
||||
if ($moduleFileList) {
|
||||
if ($onlyConflict) {
|
||||
$excludeFile = ['info.ini'];
|
||||
foreach ($moduleFileList as $file) {
|
||||
$path = Filesystem::fsFit(str_replace($dir, '', $file['path']));
|
||||
$paths = explode(DIRECTORY_SEPARATOR, $path);
|
||||
$overwriteFile = in_array($paths[0], $overwriteDir) ? root_path() . $path : $dir . $path;
|
||||
if (is_file($overwriteFile) && !in_array($path, $excludeFile) && (filesize($overwriteFile) != $file['size'] || md5_file($overwriteFile) != $file['md5'])) {
|
||||
$fileList[] = $path;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
foreach ($overwriteDir as $item) {
|
||||
$baseDir = $dir . $item;
|
||||
foreach ($moduleFileList as $file) {
|
||||
if (!str_starts_with($file['path'], $baseDir)) continue;
|
||||
$fileList[] = Filesystem::fsFit(str_replace($dir, '', $file['path']));
|
||||
}
|
||||
}
|
||||
}
|
||||
return $fileList;
|
||||
}
|
||||
|
||||
foreach ($overwriteDir as $item) {
|
||||
$baseDir = $dir . $item;
|
||||
if (!is_dir($baseDir)) {
|
||||
continue;
|
||||
}
|
||||
$files = new RecursiveIteratorIterator(
|
||||
new RecursiveDirectoryIterator($baseDir, FilesystemIterator::SKIP_DOTS), RecursiveIteratorIterator::CHILD_FIRST
|
||||
);
|
||||
foreach ($files as $file) {
|
||||
if ($file->isFile()) {
|
||||
$filePath = $file->getPathName();
|
||||
$path = str_replace($dir, '', $filePath);
|
||||
$path = str_replace(['/', '\\'], DIRECTORY_SEPARATOR, $path);
|
||||
|
||||
if ($onlyConflict) {
|
||||
$overwriteFile = root_path() . $path;
|
||||
if (is_file($overwriteFile) && (filesize($overwriteFile) != filesize($filePath) || md5_file($overwriteFile) != md5_file($filePath))) {
|
||||
$fileList[] = $path;
|
||||
}
|
||||
} else {
|
||||
$fileList[] = $path;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return $fileList;
|
||||
}
|
||||
|
||||
public static function getOverwriteDir(): array
|
||||
{
|
||||
return [
|
||||
'app',
|
||||
'config',
|
||||
'database',
|
||||
'extend',
|
||||
'modules',
|
||||
'public',
|
||||
'vendor',
|
||||
'web',
|
||||
'web-nuxt',
|
||||
];
|
||||
}
|
||||
|
||||
public static function importSql(string $dir): bool
|
||||
{
|
||||
$sqlFile = $dir . 'install.sql';
|
||||
$tempLine = '';
|
||||
$prefix = config('thinkorm.connections.mysql.prefix', config('database.connections.mysql.prefix', ''));
|
||||
if (is_file($sqlFile)) {
|
||||
$lines = file($sqlFile);
|
||||
foreach ($lines as $line) {
|
||||
if (str_starts_with($line, '--') || $line == '' || str_starts_with($line, '/*')) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$tempLine .= $line;
|
||||
if (str_ends_with(trim($line), ';')) {
|
||||
$tempLine = str_ireplace('__PREFIX__', $prefix, $tempLine);
|
||||
$tempLine = str_ireplace('INSERT INTO ', 'INSERT IGNORE INTO ', $tempLine);
|
||||
try {
|
||||
Db::execute($tempLine);
|
||||
} catch (PDOException) {
|
||||
// ignore
|
||||
}
|
||||
$tempLine = '';
|
||||
}
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
public static function installedList(string $dir): array
|
||||
{
|
||||
if (!is_dir($dir)) {
|
||||
return [];
|
||||
}
|
||||
$installedDir = scandir($dir);
|
||||
$installedList = [];
|
||||
foreach ($installedDir as $item) {
|
||||
if ($item === '.' or $item === '..' || is_file($dir . $item)) {
|
||||
continue;
|
||||
}
|
||||
$tempDir = $dir . $item . DIRECTORY_SEPARATOR;
|
||||
if (!is_dir($tempDir)) {
|
||||
continue;
|
||||
}
|
||||
$info = self::getIni($tempDir);
|
||||
if (!isset($info['uid'])) {
|
||||
continue;
|
||||
}
|
||||
$installedList[] = $info;
|
||||
}
|
||||
return $installedList;
|
||||
}
|
||||
|
||||
public static function getInstalledIds(string $dir): array
|
||||
{
|
||||
$installedIds = [];
|
||||
$installed = self::installedList($dir);
|
||||
foreach ($installed as $item) {
|
||||
$installedIds[] = $item['uid'];
|
||||
}
|
||||
return $installedIds;
|
||||
}
|
||||
|
||||
public static function getIni(string $dir): array
|
||||
{
|
||||
$infoFile = $dir . 'info.ini';
|
||||
$info = [];
|
||||
if (is_file($infoFile)) {
|
||||
$info = parse_ini_file($infoFile, true, INI_SCANNER_TYPED) ?: [];
|
||||
if (!$info) return [];
|
||||
}
|
||||
return $info;
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws Throwable
|
||||
*/
|
||||
public static function setIni(string $dir, array $arr): bool
|
||||
{
|
||||
$infoFile = $dir . 'info.ini';
|
||||
$ini = [];
|
||||
foreach ($arr as $key => $val) {
|
||||
if (is_array($val)) {
|
||||
$ini[] = "[$key]";
|
||||
foreach ($val as $ikey => $ival) {
|
||||
$ini[] = "$ikey = $ival";
|
||||
}
|
||||
} else {
|
||||
$ini[] = "$key = $val";
|
||||
}
|
||||
}
|
||||
if (!file_put_contents($infoFile, implode("\n", $ini) . "\n", LOCK_EX)) {
|
||||
throw new Exception("Configuration file has no write permission");
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
public static function getClass(string $uid, string $type = 'event', ?string $class = null): string
|
||||
{
|
||||
$name = parse_name($uid);
|
||||
if (!is_null($class) && strpos($class, '.')) {
|
||||
$class = explode('.', $class);
|
||||
$class[count($class) - 1] = parse_name(end($class), 1);
|
||||
$class = implode('\\', $class);
|
||||
} else {
|
||||
$class = parse_name(is_null($class) ? $name : $class, 1);
|
||||
}
|
||||
$namespace = match ($type) {
|
||||
'controller' => '\\modules\\' . $name . '\\controller\\' . $class,
|
||||
default => '\\modules\\' . $name . '\\' . $class,
|
||||
};
|
||||
return class_exists($namespace) ? $namespace : '';
|
||||
}
|
||||
|
||||
public static function execEvent(string $uid, string $event, array $params = []): void
|
||||
{
|
||||
$eventClass = self::getClass($uid);
|
||||
if (class_exists($eventClass)) {
|
||||
$handle = new $eventClass();
|
||||
if (method_exists($eventClass, $event)) {
|
||||
$handle->$event($params);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static function analysisWebBootstrap(string $uid, string $dir): array
|
||||
{
|
||||
$bootstrapFile = $dir . 'webBootstrap.stub';
|
||||
if (!file_exists($bootstrapFile)) return [];
|
||||
$bootstrapContent = file_get_contents($bootstrapFile);
|
||||
$pregArr = [
|
||||
'mainTsImport' => '/#main.ts import code start#([\s\S]*?)#main.ts import code end#/i',
|
||||
'mainTsStart' => '/#main.ts start code start#([\s\S]*?)#main.ts start code end#/i',
|
||||
'appVueImport' => '/#App.vue import code start#([\s\S]*?)#App.vue import code end#/i',
|
||||
'appVueOnMounted' => '/#App.vue onMounted code start#([\s\S]*?)#App.vue onMounted code end#/i',
|
||||
'nuxtAppVueImport' => '/#web-nuxt\/app.vue import code start#([\s\S]*?)#web-nuxt\/app.vue import code end#/i',
|
||||
'nuxtAppVueStart' => '/#web-nuxt\/app.vue start code start#([\s\S]*?)#web-nuxt\/app.vue start code end#/i',
|
||||
];
|
||||
$codeStrArr = [];
|
||||
foreach ($pregArr as $key => $item) {
|
||||
preg_match($item, $bootstrapContent, $matches);
|
||||
if (isset($matches[1]) && $matches[1]) {
|
||||
$mainImportCodeArr = array_filter(preg_split('/\r\n|\r|\n/', $matches[1]));
|
||||
if ($mainImportCodeArr) {
|
||||
$codeStrArr[$key] = "\n";
|
||||
if (count($mainImportCodeArr) == 1) {
|
||||
foreach ($mainImportCodeArr as $codeItem) {
|
||||
$codeStrArr[$key] .= $codeItem . self::buildMarkStr('module-line-mark', $uid, $key);
|
||||
}
|
||||
} else {
|
||||
$codeStrArr[$key] .= self::buildMarkStr('module-multi-line-mark-start', $uid, $key);
|
||||
foreach ($mainImportCodeArr as $codeItem) {
|
||||
$codeStrArr[$key] .= $codeItem . "\n";
|
||||
}
|
||||
$codeStrArr[$key] .= self::buildMarkStr('module-multi-line-mark-end', $uid, $key);
|
||||
}
|
||||
}
|
||||
}
|
||||
unset($matches);
|
||||
}
|
||||
return $codeStrArr;
|
||||
}
|
||||
|
||||
public static function installWebBootstrap(string $uid, string $dir): void
|
||||
{
|
||||
$bootstrapCode = self::analysisWebBootstrap($uid, $dir);
|
||||
if (!$bootstrapCode) {
|
||||
return;
|
||||
}
|
||||
|
||||
$webPath = root_path() . 'web' . DIRECTORY_SEPARATOR . 'src' . DIRECTORY_SEPARATOR;
|
||||
$webNuxtPath = root_path() . 'web-nuxt' . DIRECTORY_SEPARATOR;
|
||||
$filePaths = [
|
||||
'mainTsImport' => $webPath . 'main.ts',
|
||||
'mainTsStart' => $webPath . 'main.ts',
|
||||
'appVueImport' => $webPath . 'App.vue',
|
||||
'appVueOnMounted' => $webPath . 'App.vue',
|
||||
'nuxtAppVueImport' => $webNuxtPath . 'app.vue',
|
||||
'nuxtAppVueStart' => $webNuxtPath . 'app.vue',
|
||||
];
|
||||
|
||||
$marks = [
|
||||
'mainTsImport' => self::buildMarkStr('import-root-mark'),
|
||||
'mainTsStart' => self::buildMarkStr('start-root-mark'),
|
||||
'appVueImport' => self::buildMarkStr('import-root-mark'),
|
||||
'appVueOnMounted' => self::buildMarkStr('onMounted-root-mark'),
|
||||
'nuxtAppVueImport' => self::buildMarkStr('import-root-mark'),
|
||||
'nuxtAppVueStart' => self::buildMarkStr('start-root-mark'),
|
||||
];
|
||||
|
||||
foreach ($bootstrapCode as $key => $item) {
|
||||
if ($item && isset($marks[$key]) && isset($filePaths[$key]) && is_file($filePaths[$key])) {
|
||||
$content = file_get_contents($filePaths[$key]);
|
||||
$markPos = stripos($content, $marks[$key]);
|
||||
if ($markPos && strripos($content, self::buildMarkStr('module-line-mark', $uid, $key)) === false && strripos($content, self::buildMarkStr('module-multi-line-mark-start', $uid, $key)) === false) {
|
||||
$content = substr_replace($content, $item, $markPos + strlen($marks[$key]), 0);
|
||||
file_put_contents($filePaths[$key], $content);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static function uninstallWebBootstrap(string $uid): void
|
||||
{
|
||||
$webPath = root_path() . 'web' . DIRECTORY_SEPARATOR . 'src' . DIRECTORY_SEPARATOR;
|
||||
$webNuxtPath = root_path() . 'web-nuxt' . DIRECTORY_SEPARATOR;
|
||||
$filePaths = [
|
||||
'mainTsImport' => $webPath . 'main.ts',
|
||||
'mainTsStart' => $webPath . 'main.ts',
|
||||
'appVueImport' => $webPath . 'App.vue',
|
||||
'appVueOnMounted' => $webPath . 'App.vue',
|
||||
'nuxtAppVueImport' => $webNuxtPath . 'app.vue',
|
||||
'nuxtAppVueStart' => $webNuxtPath . 'app.vue',
|
||||
];
|
||||
|
||||
$marksKey = [
|
||||
'mainTsImport',
|
||||
'mainTsStart',
|
||||
'appVueImport',
|
||||
'appVueOnMounted',
|
||||
'nuxtAppVueImport',
|
||||
'nuxtAppVueStart',
|
||||
];
|
||||
|
||||
foreach ($marksKey as $item) {
|
||||
if (!isset($filePaths[$item]) || !is_file($filePaths[$item])) {
|
||||
continue;
|
||||
}
|
||||
$content = file_get_contents($filePaths[$item]);
|
||||
$moduleLineMark = self::buildMarkStr('module-line-mark', $uid, $item);
|
||||
$moduleMultiLineMarkStart = self::buildMarkStr('module-multi-line-mark-start', $uid, $item);
|
||||
$moduleMultiLineMarkEnd = self::buildMarkStr('module-multi-line-mark-end', $uid, $item);
|
||||
|
||||
$moduleLineMarkPos = strripos($content, $moduleLineMark);
|
||||
if ($moduleLineMarkPos !== false) {
|
||||
$delStartTemp = explode($moduleLineMark, $content);
|
||||
$delStartPos = strripos(rtrim($delStartTemp[0], "\n"), "\n");
|
||||
$delEndPos = stripos($content, "\n", $moduleLineMarkPos);
|
||||
$content = substr_replace($content, '', $delStartPos, $delEndPos - $delStartPos);
|
||||
}
|
||||
|
||||
$moduleMultiLineMarkStartPos = stripos($content, $moduleMultiLineMarkStart);
|
||||
if ($moduleMultiLineMarkStartPos !== false) {
|
||||
$moduleMultiLineMarkStartPos--;
|
||||
$moduleMultiLineMarkEndPos = stripos($content, $moduleMultiLineMarkEnd);
|
||||
$delLang = ($moduleMultiLineMarkEndPos + strlen($moduleMultiLineMarkEnd)) - $moduleMultiLineMarkStartPos;
|
||||
$content = substr_replace($content, '', $moduleMultiLineMarkStartPos, $delLang);
|
||||
}
|
||||
|
||||
if (($moduleLineMarkPos ?? false) !== false || ($moduleMultiLineMarkStartPos ?? false) !== false) {
|
||||
file_put_contents($filePaths[$item], $content);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static function buildMarkStr(string $type, string $uid = '', string $extend = ''): string
|
||||
{
|
||||
$nonTabKeys = ['mti', 'avi', 'navi', 'navs'];
|
||||
$extend = match ($extend) {
|
||||
'mainTsImport' => 'mti',
|
||||
'mainTsStart' => 'mts',
|
||||
'appVueImport' => 'avi',
|
||||
'appVueOnMounted' => 'avo',
|
||||
'nuxtAppVueImport' => 'navi',
|
||||
'nuxtAppVueStart' => 'navs',
|
||||
default => '',
|
||||
};
|
||||
return match ($type) {
|
||||
'import-root-mark' => '// modules import mark, Please do not remove.',
|
||||
'start-root-mark' => '// modules start mark, Please do not remove.',
|
||||
'onMounted-root-mark' => '// Modules onMounted mark, Please do not remove.',
|
||||
'module-line-mark' => ' // Code from module \'' . $uid . "'" . ($extend ? "($extend)" : ''),
|
||||
'module-multi-line-mark-start' => (in_array($extend, $nonTabKeys) ? '' : Helper::tab()) . "// Code from module '$uid' start" . ($extend ? "($extend)" : '') . "\n",
|
||||
'module-multi-line-mark-end' => (in_array($extend, $nonTabKeys) ? '' : Helper::tab()) . "// Code from module '$uid' end",
|
||||
default => '',
|
||||
};
|
||||
}
|
||||
|
||||
public static function getNuxtVersion(): mixed
|
||||
{
|
||||
$nuxtPackageJsonPath = Filesystem::fsFit(root_path() . 'web-nuxt/package.json');
|
||||
if (is_file($nuxtPackageJsonPath)) {
|
||||
$nuxtPackageJson = file_get_contents($nuxtPackageJsonPath);
|
||||
$nuxtPackageJson = json_decode($nuxtPackageJson, true);
|
||||
if ($nuxtPackageJson && isset($nuxtPackageJson['version'])) {
|
||||
return $nuxtPackageJson['version'];
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public static function createRuntime(string $dir): void
|
||||
{
|
||||
$runtimeFilePath = $dir . '.runtime';
|
||||
$files = new RecursiveIteratorIterator(
|
||||
new RecursiveDirectoryIterator($dir), RecursiveIteratorIterator::LEAVES_ONLY
|
||||
);
|
||||
$filePaths = [];
|
||||
foreach ($files as $file) {
|
||||
if (!$file->isDir()) {
|
||||
$pathName = $file->getPathName();
|
||||
if ($pathName == $runtimeFilePath) continue;
|
||||
$filePaths[] = [
|
||||
'path' => Filesystem::fsFit($pathName),
|
||||
'size' => filesize($pathName),
|
||||
'md5' => md5_file($pathName),
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
file_put_contents($runtimeFilePath, json_encode([
|
||||
'files' => $filePaths,
|
||||
'pure' => config('buildadmin.module_pure_install', false),
|
||||
]));
|
||||
}
|
||||
|
||||
public static function getRuntime(string $dir, string $key = ''): mixed
|
||||
{
|
||||
$runtimeFilePath = $dir . '.runtime';
|
||||
$runtimeContent = @file_get_contents($runtimeFilePath);
|
||||
$runtimeContentArr = json_decode($runtimeContent, true);
|
||||
if (!$runtimeContentArr) return [];
|
||||
|
||||
if ($key) {
|
||||
return $runtimeContentArr[$key] ?? [];
|
||||
}
|
||||
return $runtimeContentArr;
|
||||
}
|
||||
}
|
||||
307
app/admin/library/traits/Backend.php
Normal file
307
app/admin/library/traits/Backend.php
Normal file
@@ -0,0 +1,307 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace app\admin\library\traits;
|
||||
|
||||
use Throwable;
|
||||
use support\Response;
|
||||
|
||||
/**
|
||||
* 后台控制器 trait(Webman 迁移版)
|
||||
* 提供 CRUD 方法:index、add、edit、del、sortable
|
||||
* 方法需返回 Response,供 Webman 路由直接返回
|
||||
*/
|
||||
trait Backend
|
||||
{
|
||||
/**
|
||||
* 排除入库字段
|
||||
* 时间戳字段(create_time/update_time)由模型自动维护,禁止前端传入非法值(如 'now')
|
||||
*/
|
||||
protected function excludeFields(array $params): array
|
||||
{
|
||||
if (!is_array($this->preExcludeFields)) {
|
||||
$this->preExcludeFields = explode(',', (string) $this->preExcludeFields);
|
||||
}
|
||||
|
||||
$exclude = array_merge(
|
||||
$this->preExcludeFields,
|
||||
['create_time', 'update_time', 'createtime', 'updatetime']
|
||||
);
|
||||
foreach ($exclude as $field) {
|
||||
$field = trim($field);
|
||||
if ($field !== '' && array_key_exists($field, $params)) {
|
||||
unset($params[$field]);
|
||||
}
|
||||
}
|
||||
return $params;
|
||||
}
|
||||
|
||||
/**
|
||||
* 查看(内部实现,由 Backend::index(Request) 调用)
|
||||
*/
|
||||
protected function _index(): Response
|
||||
{
|
||||
if ($this->request && $this->request->get('select')) {
|
||||
return $this->select($this->request);
|
||||
}
|
||||
|
||||
list($where, $alias, $limit, $order) = $this->queryBuilder();
|
||||
$res = $this->model
|
||||
->field($this->indexField)
|
||||
->withJoin($this->withJoinTable, $this->withJoinType)
|
||||
->alias($alias)
|
||||
->where($where)
|
||||
->order($order)
|
||||
->paginate($limit);
|
||||
|
||||
return $this->success('', [
|
||||
'list' => $res->items(),
|
||||
'total' => $res->total(),
|
||||
'remark' => get_route_remark(),
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* 递归应用输入过滤(如 clean_xss)
|
||||
*/
|
||||
protected function applyInputFilter(array $data): array
|
||||
{
|
||||
if (!$this->inputFilter || !function_exists($this->inputFilter)) {
|
||||
return $data;
|
||||
}
|
||||
$filter = $this->inputFilter;
|
||||
foreach ($data as $k => $v) {
|
||||
if (is_string($v)) {
|
||||
$data[$k] = call_user_func($filter, $v);
|
||||
} elseif (is_array($v)) {
|
||||
$data[$k] = $this->applyInputFilter($v);
|
||||
}
|
||||
}
|
||||
return $data;
|
||||
}
|
||||
|
||||
/**
|
||||
* 添加(内部实现)
|
||||
*/
|
||||
protected function _add(): Response
|
||||
{
|
||||
if ($this->request && $this->request->method() === 'POST') {
|
||||
$data = $this->request->post();
|
||||
if (!$data) {
|
||||
return $this->error(__('Parameter %s can not be empty', ['']));
|
||||
}
|
||||
|
||||
$data = $this->applyInputFilter($data);
|
||||
$data = $this->excludeFields($data);
|
||||
if ($this->dataLimit && $this->dataLimitFieldAutoFill) {
|
||||
$data[$this->dataLimitField] = $this->auth->id;
|
||||
}
|
||||
|
||||
$result = false;
|
||||
$this->model->startTrans();
|
||||
try {
|
||||
if ($this->modelValidate) {
|
||||
$validate = str_replace("\\model\\", "\\validate\\", get_class($this->model));
|
||||
if (class_exists($validate)) {
|
||||
$validate = new $validate();
|
||||
if ($this->modelSceneValidate) {
|
||||
$validate->scene('add');
|
||||
}
|
||||
$validate->check($data);
|
||||
}
|
||||
}
|
||||
$result = $this->model->save($data);
|
||||
$this->model->commit();
|
||||
} catch (Throwable $e) {
|
||||
$this->model->rollback();
|
||||
return $this->error($e->getMessage());
|
||||
}
|
||||
if ($result !== false) {
|
||||
return $this->success(__('Added successfully'));
|
||||
}
|
||||
return $this->error(__('No rows were added'));
|
||||
}
|
||||
|
||||
return $this->error(__('Parameter error'));
|
||||
}
|
||||
|
||||
/**
|
||||
* 编辑(内部实现)
|
||||
*/
|
||||
protected function _edit(): Response
|
||||
{
|
||||
$pk = $this->model->getPk();
|
||||
$id = $this->request ? ($this->request->post($pk) ?? $this->request->get($pk)) : null;
|
||||
$row = $this->model->find($id);
|
||||
if (!$row) {
|
||||
return $this->error(__('Record not found'));
|
||||
}
|
||||
|
||||
$dataLimitAdminIds = $this->getDataLimitAdminIds();
|
||||
if ($dataLimitAdminIds && !in_array($row[$this->dataLimitField], $dataLimitAdminIds)) {
|
||||
return $this->error(__('You have no permission'));
|
||||
}
|
||||
|
||||
if ($this->request && $this->request->method() === 'POST') {
|
||||
$data = $this->request->post();
|
||||
if (!$data) {
|
||||
return $this->error(__('Parameter %s can not be empty', ['']));
|
||||
}
|
||||
|
||||
$data = $this->applyInputFilter($data);
|
||||
$data = $this->excludeFields($data);
|
||||
$result = false;
|
||||
$this->model->startTrans();
|
||||
try {
|
||||
if ($this->modelValidate) {
|
||||
$validate = str_replace("\\model\\", "\\validate\\", get_class($this->model));
|
||||
if (class_exists($validate)) {
|
||||
$validate = new $validate();
|
||||
if ($this->modelSceneValidate) {
|
||||
$validate->scene('edit');
|
||||
}
|
||||
$data[$pk] = $row[$pk];
|
||||
$validate->check($data);
|
||||
}
|
||||
}
|
||||
$result = $row->save($data);
|
||||
$this->model->commit();
|
||||
} catch (Throwable $e) {
|
||||
$this->model->rollback();
|
||||
return $this->error($e->getMessage());
|
||||
}
|
||||
if ($result !== false) {
|
||||
return $this->success(__('Update successful'));
|
||||
}
|
||||
return $this->error(__('No rows updated'));
|
||||
}
|
||||
|
||||
return $this->success('', [
|
||||
'row' => $row
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除(内部实现)
|
||||
*/
|
||||
protected function _del(): Response
|
||||
{
|
||||
$where = [];
|
||||
$dataLimitAdminIds = $this->getDataLimitAdminIds();
|
||||
if ($dataLimitAdminIds) {
|
||||
$where[] = [$this->dataLimitField, 'in', $dataLimitAdminIds];
|
||||
}
|
||||
|
||||
$ids = $this->request ? ($this->request->post('ids') ?? $this->request->get('ids') ?? []) : [];
|
||||
$ids = is_array($ids) ? $ids : [];
|
||||
$where[] = [$this->model->getPk(), 'in', $ids];
|
||||
$data = $this->model->where($where)->select();
|
||||
|
||||
$count = 0;
|
||||
$this->model->startTrans();
|
||||
try {
|
||||
foreach ($data as $v) {
|
||||
$count += $v->delete();
|
||||
}
|
||||
$this->model->commit();
|
||||
} catch (Throwable $e) {
|
||||
$this->model->rollback();
|
||||
return $this->error($e->getMessage());
|
||||
}
|
||||
if ($count) {
|
||||
return $this->success(__('Deleted successfully'));
|
||||
}
|
||||
return $this->error(__('No rows were deleted'));
|
||||
}
|
||||
|
||||
/**
|
||||
* 排序 - 增量重排法(内部实现)
|
||||
*/
|
||||
protected function _sortable(): Response
|
||||
{
|
||||
$pk = $this->model->getPk();
|
||||
$move = $this->request ? $this->request->post('move') ?? $this->request->get('move') : null;
|
||||
$target = $this->request ? $this->request->post('target') ?? $this->request->get('target') : null;
|
||||
$order = $this->request ? ($this->request->post('order') ?? $this->request->get('order')) : null;
|
||||
$order = $order ?: $this->defaultSortField;
|
||||
$direction = $this->request ? ($this->request->post('direction') ?? $this->request->get('direction')) : null;
|
||||
|
||||
$dataLimitWhere = [];
|
||||
$dataLimitAdminIds = $this->getDataLimitAdminIds();
|
||||
if ($dataLimitAdminIds) {
|
||||
$dataLimitWhere[] = [$this->dataLimitField, 'in', $dataLimitAdminIds];
|
||||
}
|
||||
|
||||
$moveRow = $this->model->where($dataLimitWhere)->find($move);
|
||||
$targetRow = $this->model->where($dataLimitWhere)->find($target);
|
||||
|
||||
if ($move == $target || !$moveRow || !$targetRow || !$direction) {
|
||||
return $this->error(__('Record not found'));
|
||||
}
|
||||
|
||||
if ($order && is_string($order)) {
|
||||
$order = explode(',', $order);
|
||||
$order = [$order[0] => $order[1] ?? 'asc'];
|
||||
}
|
||||
if (!is_array($order) || !array_key_exists($this->weighField, $order)) {
|
||||
return $this->error(__('Please use the %s field to sort before operating', [$this->weighField]));
|
||||
}
|
||||
|
||||
$order = $this->queryOrderBuilder();
|
||||
$weigh = $targetRow[$this->weighField];
|
||||
|
||||
$updateMethod = $order[$this->weighField] == 'desc' ? ($direction == 'up' ? 'dec' : 'inc') : ($direction == 'up' ? 'inc' : 'dec');
|
||||
|
||||
$weighRowIds = $this->model
|
||||
->where($dataLimitWhere)
|
||||
->where($this->weighField, $weigh)
|
||||
->order($order)
|
||||
->column($pk);
|
||||
$weighRowsCount = count($weighRowIds);
|
||||
|
||||
$this->model->where($dataLimitWhere)
|
||||
->where($this->weighField, $updateMethod == 'dec' ? '<' : '>', $weigh)
|
||||
->whereNotIn($pk, [$moveRow->$pk])
|
||||
->$updateMethod($this->weighField, $weighRowsCount)
|
||||
->save();
|
||||
|
||||
if ($direction == 'down') {
|
||||
$weighRowIds = array_reverse($weighRowIds);
|
||||
}
|
||||
|
||||
$moveComplete = 0;
|
||||
$weighRowIdsStr = implode(',', $weighRowIds);
|
||||
$weighRows = $this->model->where($dataLimitWhere)
|
||||
->where($pk, 'in', $weighRowIdsStr)
|
||||
->orderRaw("field($pk,$weighRowIdsStr)")
|
||||
->select();
|
||||
|
||||
foreach ($weighRows as $key => $weighRow) {
|
||||
if ($moveRow[$pk] == $weighRow[$pk]) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$rowWeighVal = $updateMethod == 'dec' ? $weighRow[$this->weighField] - $key : $weighRow[$this->weighField] + $key;
|
||||
|
||||
if ($weighRow[$pk] == $targetRow[$pk]) {
|
||||
$moveComplete = 1;
|
||||
$moveRow[$this->weighField] = $rowWeighVal;
|
||||
$moveRow->save();
|
||||
}
|
||||
|
||||
$rowWeighVal = $updateMethod == 'dec' ? $rowWeighVal - $moveComplete : $rowWeighVal + $moveComplete;
|
||||
$weighRow[$this->weighField] = $rowWeighVal;
|
||||
$weighRow->save();
|
||||
}
|
||||
|
||||
return $this->success();
|
||||
}
|
||||
|
||||
/**
|
||||
* 加载为 select(远程下拉选择框)数据,子类可覆盖
|
||||
*/
|
||||
protected function _select(): void
|
||||
{
|
||||
}
|
||||
}
|
||||
64
app/admin/model/Admin.php
Normal file
64
app/admin/model/Admin.php
Normal file
@@ -0,0 +1,64 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace app\admin\model;
|
||||
|
||||
use support\think\Model;
|
||||
use support\think\Db;
|
||||
|
||||
/**
|
||||
* Admin 模型(Webman 迁移版)
|
||||
* @property int $id 管理员ID
|
||||
* @property string $username 管理员用户名
|
||||
* @property string $nickname 管理员昵称
|
||||
* @property string $email 管理员邮箱
|
||||
* @property string $mobile 管理员手机号
|
||||
* @property string $last_login_ip 上次登录IP
|
||||
* @property string $last_login_time 上次登录时间
|
||||
* @property int $login_failure 登录失败次数
|
||||
* @property string $password 密码密文
|
||||
* @property string $salt 密码盐
|
||||
* @property string $status 状态:enable=启用,disable=禁用
|
||||
*/
|
||||
class Admin extends Model
|
||||
{
|
||||
protected string $table = 'admin';
|
||||
protected string $pk = 'id';
|
||||
protected bool $autoWriteTimestamp = true;
|
||||
|
||||
protected array $append = [
|
||||
'group_arr',
|
||||
'group_name_arr',
|
||||
];
|
||||
|
||||
public function getGroupArrAttr($value, $row): array
|
||||
{
|
||||
return Db::name('admin_group_access')
|
||||
->where('uid', $row['id'])
|
||||
->column('group_id');
|
||||
}
|
||||
|
||||
public function getGroupNameArrAttr($value, $row): array
|
||||
{
|
||||
$groupAccess = Db::name('admin_group_access')
|
||||
->where('uid', $row['id'])
|
||||
->column('group_id');
|
||||
return AdminGroup::whereIn('id', $groupAccess)->column('name');
|
||||
}
|
||||
|
||||
public function getAvatarAttr($value): string
|
||||
{
|
||||
return full_url($value ?? '', false, config('buildadmin.default_avatar'));
|
||||
}
|
||||
|
||||
public function setAvatarAttr($value): string
|
||||
{
|
||||
return $value === full_url('', false, config('buildadmin.default_avatar')) ? '' : $value;
|
||||
}
|
||||
|
||||
public function resetPassword(int|string $uid, string $newPassword): int
|
||||
{
|
||||
return $this->where(['id' => $uid])->update(['password' => hash_password($newPassword), 'salt' => '']);
|
||||
}
|
||||
}
|
||||
17
app/admin/model/AdminGroup.php
Normal file
17
app/admin/model/AdminGroup.php
Normal file
@@ -0,0 +1,17 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace app\admin\model;
|
||||
|
||||
use support\think\Model;
|
||||
|
||||
/**
|
||||
* AdminGroup 模型(Webman 迁移版)
|
||||
*/
|
||||
class AdminGroup extends Model
|
||||
{
|
||||
protected string $table = 'admin_group';
|
||||
protected string $pk = 'id';
|
||||
protected bool $autoWriteTimestamp = true;
|
||||
}
|
||||
141
app/admin/model/AdminLog.php
Normal file
141
app/admin/model/AdminLog.php
Normal file
@@ -0,0 +1,141 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace app\admin\model;
|
||||
|
||||
use Throwable;
|
||||
use app\admin\library\Auth;
|
||||
use support\think\Model;
|
||||
use Webman\Http\Request;
|
||||
|
||||
/**
|
||||
* AdminLog 模型(Webman 迁移版)
|
||||
*/
|
||||
class AdminLog extends Model
|
||||
{
|
||||
protected string $table = 'admin_log';
|
||||
protected string $pk = 'id';
|
||||
protected bool $autoWriteTimestamp = true;
|
||||
protected bool $updateTime = false;
|
||||
|
||||
protected string $title = '';
|
||||
/** 日志内容(勿用 $data,会与 ThinkORM Model 的 data 选项冲突) */
|
||||
protected string|array $logData = '';
|
||||
protected array $urlIgnoreRegex = [
|
||||
'/^(.*)\/(select|index|logout)$/i',
|
||||
];
|
||||
protected array $desensitizationRegex = [
|
||||
'/(password|salt|token)/i'
|
||||
];
|
||||
|
||||
public static function instance(?Request $request = null): self
|
||||
{
|
||||
$request = $request ?? (function_exists('request') ? request() : null);
|
||||
if ($request !== null && isset($request->adminLog) && $request->adminLog instanceof self) {
|
||||
return $request->adminLog;
|
||||
}
|
||||
$log = new static();
|
||||
if ($request !== null) {
|
||||
$request->adminLog = $log;
|
||||
}
|
||||
return $log;
|
||||
}
|
||||
|
||||
public function setTitle(string $title): void
|
||||
{
|
||||
$this->title = $title;
|
||||
}
|
||||
|
||||
/** 设置日志内容(BuildAdmin 控制器调用) */
|
||||
public function setLogData(string|array $data): void
|
||||
{
|
||||
$this->logData = $data;
|
||||
}
|
||||
|
||||
public function setUrlIgnoreRegex(array|string $regex = []): void
|
||||
{
|
||||
$this->urlIgnoreRegex = array_merge($this->urlIgnoreRegex, is_array($regex) ? $regex : [$regex]);
|
||||
}
|
||||
|
||||
public function setDesensitizationRegex(array|string $regex = []): void
|
||||
{
|
||||
$this->desensitizationRegex = array_merge($this->desensitizationRegex, is_array($regex) ? $regex : [$regex]);
|
||||
}
|
||||
|
||||
protected function desensitization(array|string $data): array|string
|
||||
{
|
||||
if (!is_array($data) || !$this->desensitizationRegex) {
|
||||
return $data;
|
||||
}
|
||||
foreach ($data as $index => &$item) {
|
||||
foreach ($this->desensitizationRegex as $reg) {
|
||||
if (preg_match($reg, (string) $index)) {
|
||||
$item = '***';
|
||||
} elseif (is_array($item)) {
|
||||
$item = $this->desensitization($item);
|
||||
}
|
||||
}
|
||||
}
|
||||
return $data;
|
||||
}
|
||||
|
||||
public function record(string $title = '', string|array|null $data = null, ?Request $request = null): void
|
||||
{
|
||||
$request = $request ?? (function_exists('request') ? request() : null);
|
||||
if (!$request) {
|
||||
return;
|
||||
}
|
||||
|
||||
$auth = Auth::instance();
|
||||
$adminId = $auth->isLogin() ? $auth->id : 0;
|
||||
$username = $auth->isLogin() ? $auth->username : ($request->get('username') ?? $request->post('username') ?? __('Unknown'));
|
||||
|
||||
$controllerPath = get_controller_path($request) ?? '';
|
||||
$pathParts = explode('/', trim($request->path(), '/'));
|
||||
$action = $pathParts[array_key_last($pathParts)] ?? '';
|
||||
$path = $controllerPath . ($action ? '/' . $action : '');
|
||||
|
||||
foreach ($this->urlIgnoreRegex as $item) {
|
||||
if (preg_match($item, $path)) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
$data = $data ?: $this->logData;
|
||||
if (!$data) {
|
||||
$data = array_merge($request->get(), $request->post());
|
||||
}
|
||||
$data = $this->desensitization($data);
|
||||
$title = $title ?: $this->title;
|
||||
if (!$title && class_exists(\app\admin\model\AdminRule::class)) {
|
||||
$controllerTitle = \app\admin\model\AdminRule::where('name', $controllerPath)->value('title');
|
||||
$pathTitle = \app\admin\model\AdminRule::where('name', $path)->value('title');
|
||||
$title = $pathTitle ?: __('Unknown') . '(' . $action . ')';
|
||||
$title = $controllerTitle ? ($controllerTitle . '-' . $title) : $title;
|
||||
}
|
||||
if (!$title) {
|
||||
$title = __('Unknown');
|
||||
}
|
||||
|
||||
$url = $request->url();
|
||||
$url = strlen($url) > 1500 ? substr($url, 0, 1500) : $url;
|
||||
$useragent = $request->header('user-agent', '');
|
||||
$useragent = strlen($useragent) > 255 ? substr($useragent, 0, 255) : $useragent;
|
||||
|
||||
self::create([
|
||||
'admin_id' => $adminId,
|
||||
'username' => $username,
|
||||
'url' => $url,
|
||||
'title' => $title,
|
||||
'data' => !is_scalar($data) ? json_encode($data, JSON_UNESCAPED_UNICODE) : $data,
|
||||
'ip' => $request->getRealIp(),
|
||||
'useragent' => $useragent,
|
||||
]);
|
||||
}
|
||||
|
||||
public function admin()
|
||||
{
|
||||
return $this->belongsTo(Admin::class);
|
||||
}
|
||||
}
|
||||
20
app/admin/model/AdminRule.php
Normal file
20
app/admin/model/AdminRule.php
Normal file
@@ -0,0 +1,20 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace app\admin\model;
|
||||
|
||||
use support\think\Model;
|
||||
|
||||
class AdminRule extends Model
|
||||
{
|
||||
protected string $table = 'admin_rule';
|
||||
protected string $pk = 'id';
|
||||
protected bool $autoWriteTimestamp = true;
|
||||
|
||||
public function setComponentAttr($value)
|
||||
{
|
||||
if ($value) $value = str_replace('\\', '/', $value);
|
||||
return $value;
|
||||
}
|
||||
}
|
||||
127
app/admin/model/Config.php
Normal file
127
app/admin/model/Config.php
Normal file
@@ -0,0 +1,127 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace app\admin\model;
|
||||
|
||||
use support\think\Model;
|
||||
|
||||
/**
|
||||
* 系统配置模型(Webman 迁移版)
|
||||
*/
|
||||
class Config extends Model
|
||||
{
|
||||
public static string $cacheTag = 'sys_config';
|
||||
|
||||
protected string $table = 'config';
|
||||
protected string $pk = 'id';
|
||||
protected bool $autoWriteTimestamp = true;
|
||||
|
||||
protected array $append = ['value', 'content', 'extend', 'input_extend'];
|
||||
protected array $jsonDecodeType = ['checkbox', 'array', 'selects'];
|
||||
protected array $needContent = ['radio', 'checkbox', 'select', 'selects'];
|
||||
|
||||
public static function onBeforeInsert(Config $model): void
|
||||
{
|
||||
if (!in_array($model->getData('type'), $model->needContent)) {
|
||||
$model->content = null;
|
||||
} else {
|
||||
$model->content = json_encode(str_attr_to_array($model->getData('content')));
|
||||
}
|
||||
if (is_array($model->rule)) {
|
||||
$model->rule = implode(',', $model->rule);
|
||||
}
|
||||
if ($model->getData('extend') || $model->getData('inputExtend')) {
|
||||
$extend = str_attr_to_array($model->getData('extend'));
|
||||
$inputExtend = str_attr_to_array($model->getData('inputExtend'));
|
||||
if ($inputExtend) {
|
||||
$extend['baInputExtend'] = $inputExtend;
|
||||
}
|
||||
if ($extend) {
|
||||
$model->extend = json_encode($extend);
|
||||
}
|
||||
}
|
||||
$model->allow_del = 1;
|
||||
}
|
||||
|
||||
public static function onAfterWrite(): void
|
||||
{
|
||||
clear_config_cache();
|
||||
}
|
||||
|
||||
public function getValueAttr($value, $row)
|
||||
{
|
||||
if (!isset($row['type']) || $value == '0') return $value;
|
||||
if (in_array($row['type'], $this->jsonDecodeType)) {
|
||||
return empty($value) ? [] : json_decode($value, true);
|
||||
}
|
||||
if ($row['type'] == 'switch') {
|
||||
return (bool) $value;
|
||||
}
|
||||
if ($row['type'] == 'editor') {
|
||||
return !$value ? '' : htmlspecialchars_decode($value);
|
||||
}
|
||||
if (in_array($row['type'], ['city', 'remoteSelects'])) {
|
||||
if (!$value) return [];
|
||||
if (!is_array($value)) return explode(',', $value);
|
||||
return $value;
|
||||
}
|
||||
return $value ?: '';
|
||||
}
|
||||
|
||||
public function setValueAttr(mixed $value, $row): mixed
|
||||
{
|
||||
if (in_array($row['type'], $this->jsonDecodeType)) {
|
||||
return $value ? json_encode($value) : '';
|
||||
}
|
||||
if ($row['type'] == 'switch') {
|
||||
return $value ? '1' : '0';
|
||||
}
|
||||
if ($row['type'] == 'time') {
|
||||
return $value ? date('H:i:s', strtotime($value)) : '';
|
||||
}
|
||||
if ($row['type'] == 'city') {
|
||||
if ($value && is_array($value)) {
|
||||
return implode(',', $value);
|
||||
}
|
||||
return $value ?: '';
|
||||
}
|
||||
if (is_array($value)) {
|
||||
return implode(',', $value);
|
||||
}
|
||||
return $value;
|
||||
}
|
||||
|
||||
public function getContentAttr($value, $row)
|
||||
{
|
||||
if (!isset($row['type'])) return '';
|
||||
if (in_array($row['type'], $this->needContent)) {
|
||||
$arr = json_decode($value, true);
|
||||
return $arr ?: [];
|
||||
}
|
||||
return '';
|
||||
}
|
||||
|
||||
public function getExtendAttr($value)
|
||||
{
|
||||
if ($value) {
|
||||
$arr = json_decode($value, true);
|
||||
if ($arr) {
|
||||
unset($arr['baInputExtend']);
|
||||
return $arr;
|
||||
}
|
||||
}
|
||||
return [];
|
||||
}
|
||||
|
||||
public function getInputExtendAttr($value, $row)
|
||||
{
|
||||
if ($row && $row['extend']) {
|
||||
$arr = json_decode($row['extend'], true);
|
||||
if ($arr && isset($arr['baInputExtend'])) {
|
||||
return $arr['baInputExtend'];
|
||||
}
|
||||
}
|
||||
return [];
|
||||
}
|
||||
}
|
||||
20
app/admin/model/CrudLog.php
Normal file
20
app/admin/model/CrudLog.php
Normal file
@@ -0,0 +1,20 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace app\admin\model;
|
||||
|
||||
use support\think\Model;
|
||||
|
||||
class CrudLog extends Model
|
||||
{
|
||||
protected string $table = 'crud_log';
|
||||
protected string $pk = 'id';
|
||||
protected bool $autoWriteTimestamp = true;
|
||||
protected bool $updateTime = false;
|
||||
|
||||
protected array $type = [
|
||||
'table' => 'array',
|
||||
'fields' => 'array',
|
||||
];
|
||||
}
|
||||
14
app/admin/model/DataRecycle.php
Normal file
14
app/admin/model/DataRecycle.php
Normal file
@@ -0,0 +1,14 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace app\admin\model;
|
||||
|
||||
use support\think\Model;
|
||||
|
||||
class DataRecycle extends Model
|
||||
{
|
||||
protected string $table = 'security_data_recycle';
|
||||
protected string $pk = 'id';
|
||||
protected bool $autoWriteTimestamp = true;
|
||||
}
|
||||
26
app/admin/model/DataRecycleLog.php
Normal file
26
app/admin/model/DataRecycleLog.php
Normal file
@@ -0,0 +1,26 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace app\admin\model;
|
||||
|
||||
use support\think\Model;
|
||||
use think\model\relation\BelongsTo;
|
||||
|
||||
class DataRecycleLog extends Model
|
||||
{
|
||||
protected string $table = 'security_data_recycle_log';
|
||||
protected string $pk = 'id';
|
||||
protected bool $autoWriteTimestamp = true;
|
||||
protected bool $updateTime = false;
|
||||
|
||||
public function recycle(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(DataRecycle::class, 'recycle_id');
|
||||
}
|
||||
|
||||
public function admin(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(Admin::class, 'admin_id');
|
||||
}
|
||||
}
|
||||
18
app/admin/model/SensitiveData.php
Normal file
18
app/admin/model/SensitiveData.php
Normal file
@@ -0,0 +1,18 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace app\admin\model;
|
||||
|
||||
use support\think\Model;
|
||||
|
||||
class SensitiveData extends Model
|
||||
{
|
||||
protected string $table = 'security_sensitive_data';
|
||||
protected string $pk = 'id';
|
||||
protected bool $autoWriteTimestamp = true;
|
||||
|
||||
protected array $type = [
|
||||
'data_fields' => 'array',
|
||||
];
|
||||
}
|
||||
26
app/admin/model/SensitiveDataLog.php
Normal file
26
app/admin/model/SensitiveDataLog.php
Normal file
@@ -0,0 +1,26 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace app\admin\model;
|
||||
|
||||
use support\think\Model;
|
||||
use think\model\relation\BelongsTo;
|
||||
|
||||
class SensitiveDataLog extends Model
|
||||
{
|
||||
protected string $table = 'security_sensitive_data_log';
|
||||
protected string $pk = 'id';
|
||||
protected bool $autoWriteTimestamp = true;
|
||||
protected bool $updateTime = false;
|
||||
|
||||
public function sensitive(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(SensitiveData::class, 'sensitive_id');
|
||||
}
|
||||
|
||||
public function admin(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(Admin::class, 'admin_id');
|
||||
}
|
||||
}
|
||||
45
app/admin/model/User.php
Normal file
45
app/admin/model/User.php
Normal file
@@ -0,0 +1,45 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace app\admin\model;
|
||||
|
||||
use support\think\Model;
|
||||
use think\model\relation\BelongsTo;
|
||||
|
||||
class User extends Model
|
||||
{
|
||||
protected string $table = 'user';
|
||||
protected string $pk = 'id';
|
||||
protected bool $autoWriteTimestamp = true;
|
||||
|
||||
public function getAvatarAttr($value): string
|
||||
{
|
||||
return full_url($value ?? '', false, config('buildadmin.default_avatar'));
|
||||
}
|
||||
|
||||
public function setAvatarAttr($value): string
|
||||
{
|
||||
return $value === full_url('', false, config('buildadmin.default_avatar')) ? '' : $value;
|
||||
}
|
||||
|
||||
public function getMoneyAttr($value): string
|
||||
{
|
||||
return bcdiv((string) $value, '100', 2);
|
||||
}
|
||||
|
||||
public function setMoneyAttr($value): string
|
||||
{
|
||||
return bcmul((string) $value, '100', 2);
|
||||
}
|
||||
|
||||
public function userGroup(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(UserGroup::class, 'group_id');
|
||||
}
|
||||
|
||||
public function resetPassword(int|string $uid, string $newPassword): int
|
||||
{
|
||||
return $this->where(['id' => $uid])->update(['password' => hash_password($newPassword), 'salt' => '']);
|
||||
}
|
||||
}
|
||||
14
app/admin/model/UserGroup.php
Normal file
14
app/admin/model/UserGroup.php
Normal file
@@ -0,0 +1,14 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace app\admin\model;
|
||||
|
||||
use support\think\Model;
|
||||
|
||||
class UserGroup extends Model
|
||||
{
|
||||
protected string $table = 'user_group';
|
||||
protected string $pk = 'id';
|
||||
protected bool $autoWriteTimestamp = true;
|
||||
}
|
||||
69
app/admin/model/UserMoneyLog.php
Normal file
69
app/admin/model/UserMoneyLog.php
Normal file
@@ -0,0 +1,69 @@
|
||||
<?php
|
||||
|
||||
namespace app\admin\model;
|
||||
|
||||
use support\think\Model;
|
||||
use think\model\relation\BelongsTo;
|
||||
|
||||
class UserMoneyLog extends Model
|
||||
{
|
||||
protected string $table = 'user_money_log';
|
||||
protected string $pk = 'id';
|
||||
protected bool $autoWriteTimestamp = true;
|
||||
protected bool $updateTime = false;
|
||||
|
||||
public static function onBeforeInsert($model): void
|
||||
{
|
||||
$user = User::where('id', $model->user_id)->lock(true)->find();
|
||||
if (!$user) {
|
||||
throw new \Exception(__("The user can't find it"));
|
||||
}
|
||||
if (!$model->memo) {
|
||||
throw new \Exception(__("Change note cannot be blank"));
|
||||
}
|
||||
$model->before = $user->money;
|
||||
$user->money += $model->money;
|
||||
$user->save();
|
||||
$model->after = $user->money;
|
||||
}
|
||||
|
||||
public static function onBeforeDelete(): bool
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
public function getMoneyAttr($value): string
|
||||
{
|
||||
return bcdiv((string) $value, '100', 2);
|
||||
}
|
||||
|
||||
public function setMoneyAttr($value): string
|
||||
{
|
||||
return bcmul((string) $value, '100', 2);
|
||||
}
|
||||
|
||||
public function getBeforeAttr($value): string
|
||||
{
|
||||
return bcdiv((string) $value, '100', 2);
|
||||
}
|
||||
|
||||
public function setBeforeAttr($value): string
|
||||
{
|
||||
return bcmul((string) $value, '100', 2);
|
||||
}
|
||||
|
||||
public function getAfterAttr($value): string
|
||||
{
|
||||
return bcdiv((string) $value, '100', 2);
|
||||
}
|
||||
|
||||
public function setAfterAttr($value): string
|
||||
{
|
||||
return bcmul((string) $value, '100', 2);
|
||||
}
|
||||
|
||||
public function user(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(User::class, 'user_id');
|
||||
}
|
||||
}
|
||||
26
app/admin/model/UserRule.php
Normal file
26
app/admin/model/UserRule.php
Normal file
@@ -0,0 +1,26 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace app\admin\model;
|
||||
|
||||
use support\think\Model;
|
||||
|
||||
class UserRule extends Model
|
||||
{
|
||||
protected string $table = 'user_rule';
|
||||
protected string $pk = 'id';
|
||||
protected bool $autoWriteTimestamp = true;
|
||||
|
||||
protected static function onAfterInsert($model): void
|
||||
{
|
||||
$pk = $model->getPk();
|
||||
$model->where($pk, $model[$pk])->update(['weigh' => $model[$pk]]);
|
||||
}
|
||||
|
||||
public function setComponentAttr($value)
|
||||
{
|
||||
if ($value) $value = str_replace('\\', '/', $value);
|
||||
return $value;
|
||||
}
|
||||
}
|
||||
39
app/admin/model/UserScoreLog.php
Normal file
39
app/admin/model/UserScoreLog.php
Normal file
@@ -0,0 +1,39 @@
|
||||
<?php
|
||||
|
||||
namespace app\admin\model;
|
||||
|
||||
use support\think\Model;
|
||||
use think\model\relation\BelongsTo;
|
||||
|
||||
class UserScoreLog extends Model
|
||||
{
|
||||
protected string $table = 'user_score_log';
|
||||
protected string $pk = 'id';
|
||||
protected bool $autoWriteTimestamp = true;
|
||||
protected bool $updateTime = false;
|
||||
|
||||
public static function onBeforeInsert($model): void
|
||||
{
|
||||
$user = User::where('id', $model->user_id)->lock(true)->find();
|
||||
if (!$user) {
|
||||
throw new \Exception(__("The user can't find it"));
|
||||
}
|
||||
if (!$model->memo) {
|
||||
throw new \Exception(__("Change note cannot be blank"));
|
||||
}
|
||||
$model->before = $user->score;
|
||||
$user->score += $model->score;
|
||||
$user->save();
|
||||
$model->after = $user->score;
|
||||
}
|
||||
|
||||
public static function onBeforeDelete(): bool
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
public function user(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(User::class, 'user_id');
|
||||
}
|
||||
}
|
||||
25
app/admin/model/mall/Player.php
Normal file
25
app/admin/model/mall/Player.php
Normal file
@@ -0,0 +1,25 @@
|
||||
<?php
|
||||
|
||||
namespace app\admin\model\mall;
|
||||
|
||||
use support\think\Model;
|
||||
|
||||
/**
|
||||
* Player
|
||||
*/
|
||||
class Player extends Model
|
||||
{
|
||||
// 表名
|
||||
protected $name = 'mall_player';
|
||||
|
||||
// 自动写入时间戳字段
|
||||
protected $autoWriteTimestamp = true;
|
||||
|
||||
/**
|
||||
* 重置密码
|
||||
*/
|
||||
public function resetPassword(int $id, string $newPassword): bool
|
||||
{
|
||||
return $this->where(['id' => $id])->update(['password' => hash_password($newPassword)]) !== false;
|
||||
}
|
||||
}
|
||||
58
app/admin/validate/Admin.php
Normal file
58
app/admin/validate/Admin.php
Normal file
@@ -0,0 +1,58 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace app\admin\validate;
|
||||
|
||||
use think\Validate;
|
||||
|
||||
class Admin extends Validate
|
||||
{
|
||||
protected $failException = true;
|
||||
|
||||
protected $rule = [
|
||||
'username' => 'require|regex:^[a-zA-Z][a-zA-Z0-9_]{2,15}$|unique:admin',
|
||||
'nickname' => 'require',
|
||||
'password' => 'require|regex:^(?!.*[&<>"\'\n\r]).{6,32}$',
|
||||
'email' => 'email|unique:admin',
|
||||
'mobile' => 'mobile|unique:admin',
|
||||
'group_arr' => 'require|array',
|
||||
];
|
||||
|
||||
protected $message = [];
|
||||
|
||||
protected $field = [];
|
||||
|
||||
protected $scene = [
|
||||
'add' => ['username', 'nickname', 'password', 'email', 'mobile', 'group_arr'],
|
||||
];
|
||||
|
||||
public function sceneInfo(): static
|
||||
{
|
||||
return $this->only(['nickname', 'password', 'email', 'mobile'])
|
||||
->remove('password', 'require');
|
||||
}
|
||||
|
||||
public function sceneEdit(): static
|
||||
{
|
||||
return $this->only(['username', 'nickname', 'password', 'email', 'mobile', 'group_arr'])
|
||||
->remove('password', 'require');
|
||||
}
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->field = [
|
||||
'username' => __('Username'),
|
||||
'nickname' => __('Nickname'),
|
||||
'password' => __('Password'),
|
||||
'email' => __('Email'),
|
||||
'mobile' => __('Mobile'),
|
||||
'group_arr' => __('Group Name Arr'),
|
||||
];
|
||||
$this->message = array_merge($this->message, [
|
||||
'username.regex' => __('Please input correct username'),
|
||||
'password.regex' => __('Please input correct password')
|
||||
]);
|
||||
parent::__construct();
|
||||
}
|
||||
}
|
||||
37
app/admin/validate/AdminGroup.php
Normal file
37
app/admin/validate/AdminGroup.php
Normal file
@@ -0,0 +1,37 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace app\admin\validate;
|
||||
|
||||
use think\Validate;
|
||||
|
||||
class AdminGroup extends Validate
|
||||
{
|
||||
protected $failException = true;
|
||||
|
||||
protected $rule = [
|
||||
'name' => 'require',
|
||||
'rules' => 'require',
|
||||
];
|
||||
|
||||
protected $message = [];
|
||||
|
||||
protected $field = [];
|
||||
|
||||
protected $scene = [
|
||||
'add' => ['name', 'rules'],
|
||||
'edit' => ['name', 'rules'],
|
||||
];
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->field = [
|
||||
'name' => __('name'),
|
||||
];
|
||||
$this->message = [
|
||||
'rules' => __('Please select rules'),
|
||||
];
|
||||
parent::__construct();
|
||||
}
|
||||
}
|
||||
37
app/admin/validate/AdminRule.php
Normal file
37
app/admin/validate/AdminRule.php
Normal file
@@ -0,0 +1,37 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace app\admin\validate;
|
||||
|
||||
use think\Validate;
|
||||
|
||||
class AdminRule extends Validate
|
||||
{
|
||||
protected $failException = true;
|
||||
|
||||
protected $rule = [
|
||||
'type' => 'require',
|
||||
'title' => 'require',
|
||||
'name' => 'require|unique:admin_rule',
|
||||
];
|
||||
|
||||
protected $message = [];
|
||||
|
||||
protected $field = [];
|
||||
|
||||
protected $scene = [
|
||||
'add' => ['type', 'title', 'name'],
|
||||
'edit' => ['type', 'title', 'name'],
|
||||
];
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->field = [
|
||||
'type' => __('type'),
|
||||
'title' => __('title'),
|
||||
'name' => __('name'),
|
||||
];
|
||||
parent::__construct();
|
||||
}
|
||||
}
|
||||
32
app/admin/validate/Config.php
Normal file
32
app/admin/validate/Config.php
Normal file
@@ -0,0 +1,32 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace app\admin\validate;
|
||||
|
||||
use think\Validate;
|
||||
|
||||
class Config extends Validate
|
||||
{
|
||||
protected $failException = true;
|
||||
|
||||
protected $rule = [
|
||||
'name' => 'require|unique:config',
|
||||
];
|
||||
|
||||
protected $message = [];
|
||||
|
||||
protected $field = [];
|
||||
|
||||
protected $scene = [
|
||||
'add' => ['name'],
|
||||
];
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->field = [
|
||||
'name' => __('Variable name'),
|
||||
];
|
||||
parent::__construct();
|
||||
}
|
||||
}
|
||||
39
app/admin/validate/DataRecycle.php
Normal file
39
app/admin/validate/DataRecycle.php
Normal file
@@ -0,0 +1,39 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace app\admin\validate;
|
||||
|
||||
use think\Validate;
|
||||
|
||||
class DataRecycle extends Validate
|
||||
{
|
||||
protected $failException = true;
|
||||
|
||||
protected $rule = [
|
||||
'name' => 'require',
|
||||
'controller' => 'require|unique:security_data_recycle',
|
||||
'data_table' => 'require',
|
||||
'primary_key' => 'require',
|
||||
];
|
||||
|
||||
protected $message = [];
|
||||
|
||||
protected $field = [];
|
||||
|
||||
protected $scene = [
|
||||
'add' => ['name', 'controller', 'data_table', 'primary_key'],
|
||||
'edit' => ['name', 'controller', 'data_table', 'primary_key'],
|
||||
];
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->field = [
|
||||
'name' => __('Name'),
|
||||
'controller' => __('Controller'),
|
||||
'data_table' => __('Data Table'),
|
||||
'primary_key' => __('Primary Key'),
|
||||
];
|
||||
parent::__construct();
|
||||
}
|
||||
}
|
||||
41
app/admin/validate/SensitiveData.php
Normal file
41
app/admin/validate/SensitiveData.php
Normal file
@@ -0,0 +1,41 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace app\admin\validate;
|
||||
|
||||
use think\Validate;
|
||||
|
||||
class SensitiveData extends Validate
|
||||
{
|
||||
protected $failException = true;
|
||||
|
||||
protected $rule = [
|
||||
'name' => 'require',
|
||||
'controller' => 'require|unique:security_sensitive_data',
|
||||
'data_table' => 'require',
|
||||
'primary_key' => 'require',
|
||||
'data_fields' => 'require',
|
||||
];
|
||||
|
||||
protected $message = [];
|
||||
|
||||
protected $field = [];
|
||||
|
||||
protected $scene = [
|
||||
'add' => ['name', 'data_fields', 'controller', 'data_table', 'primary_key'],
|
||||
'edit' => ['name', 'data_fields', 'controller', 'data_table', 'primary_key'],
|
||||
];
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->field = [
|
||||
'name' => __('Name'),
|
||||
'data_fields' => __('Data Fields'),
|
||||
'controller' => __('Controller'),
|
||||
'data_table' => __('Data Table'),
|
||||
'primary_key' => __('Primary Key'),
|
||||
];
|
||||
parent::__construct();
|
||||
}
|
||||
}
|
||||
37
app/admin/validate/UserMoneyLog.php
Normal file
37
app/admin/validate/UserMoneyLog.php
Normal file
@@ -0,0 +1,37 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace app\admin\validate;
|
||||
|
||||
use think\Validate;
|
||||
|
||||
class UserMoneyLog extends Validate
|
||||
{
|
||||
protected $failException = true;
|
||||
|
||||
protected $rule = [
|
||||
'user_id' => 'require',
|
||||
'money' => 'require',
|
||||
'memo' => 'require',
|
||||
];
|
||||
|
||||
protected $message = [];
|
||||
|
||||
protected $field = [];
|
||||
|
||||
protected $scene = [
|
||||
'add' => ['user_id', 'money', 'memo'],
|
||||
'edit' => ['user_id', 'money', 'memo'],
|
||||
];
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->field = [
|
||||
'user_id' => __('user_id'),
|
||||
'money' => __('money'),
|
||||
'memo' => __('memo'),
|
||||
];
|
||||
parent::__construct();
|
||||
}
|
||||
}
|
||||
37
app/admin/validate/UserScoreLog.php
Normal file
37
app/admin/validate/UserScoreLog.php
Normal file
@@ -0,0 +1,37 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace app\admin\validate;
|
||||
|
||||
use think\Validate;
|
||||
|
||||
class UserScoreLog extends Validate
|
||||
{
|
||||
protected $failException = true;
|
||||
|
||||
protected $rule = [
|
||||
'user_id' => 'require',
|
||||
'score' => 'require',
|
||||
'memo' => 'require',
|
||||
];
|
||||
|
||||
protected $message = [];
|
||||
|
||||
protected $field = [];
|
||||
|
||||
protected $scene = [
|
||||
'add' => ['user_id', 'score', 'memo'],
|
||||
'edit' => ['user_id', 'score', 'memo'],
|
||||
];
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->field = [
|
||||
'user_id' => __('user_id'),
|
||||
'score' => __('score'),
|
||||
'memo' => __('memo'),
|
||||
];
|
||||
parent::__construct();
|
||||
}
|
||||
}
|
||||
31
app/admin/validate/mall/Player.php
Normal file
31
app/admin/validate/mall/Player.php
Normal file
@@ -0,0 +1,31 @@
|
||||
<?php
|
||||
|
||||
namespace app\admin\validate\mall;
|
||||
|
||||
use think\Validate;
|
||||
|
||||
class Player extends Validate
|
||||
{
|
||||
protected $failException = true;
|
||||
|
||||
/**
|
||||
* 验证规则
|
||||
*/
|
||||
protected $rule = [
|
||||
];
|
||||
|
||||
/**
|
||||
* 提示消息
|
||||
*/
|
||||
protected $message = [
|
||||
];
|
||||
|
||||
/**
|
||||
* 验证场景
|
||||
*/
|
||||
protected $scene = [
|
||||
'add' => [],
|
||||
'edit' => [],
|
||||
];
|
||||
|
||||
}
|
||||
269
app/api/controller/Account.php
Normal file
269
app/api/controller/Account.php
Normal file
@@ -0,0 +1,269 @@
|
||||
<?php
|
||||
|
||||
namespace app\api\controller;
|
||||
|
||||
use ba\Date;
|
||||
use ba\Captcha;
|
||||
use ba\Random;
|
||||
use app\common\model\User;
|
||||
use app\common\facade\Token;
|
||||
use app\common\model\UserScoreLog;
|
||||
use app\common\model\UserMoneyLog;
|
||||
use app\common\controller\Frontend;
|
||||
use support\validation\Validator;
|
||||
use support\validation\ValidationException;
|
||||
use Webman\Http\Request;
|
||||
use support\Response;
|
||||
|
||||
class Account extends Frontend
|
||||
{
|
||||
protected array $noNeedLogin = ['retrievePassword'];
|
||||
protected array $noNeedPermission = ['verification', 'changeBind'];
|
||||
|
||||
public function overview(Request $request): Response
|
||||
{
|
||||
$response = $this->initializeFrontend($request);
|
||||
if ($response !== null) return $response;
|
||||
|
||||
$sevenDays = Date::unixTime('day', -6);
|
||||
$score = $money = $days = [];
|
||||
for ($i = 0; $i < 7; $i++) {
|
||||
$days[$i] = date("Y-m-d", $sevenDays + ($i * 86400));
|
||||
$tempToday0 = strtotime($days[$i]);
|
||||
$tempToday24 = strtotime('+1 day', $tempToday0) - 1;
|
||||
$score[$i] = UserScoreLog::where('user_id', $this->auth->id)
|
||||
->where('create_time', 'BETWEEN', $tempToday0 . ',' . $tempToday24)
|
||||
->sum('score');
|
||||
$userMoneyTemp = UserMoneyLog::where('user_id', $this->auth->id)
|
||||
->where('create_time', 'BETWEEN', $tempToday0 . ',' . $tempToday24)
|
||||
->sum('money');
|
||||
$money[$i] = bcdiv((string) $userMoneyTemp, '100', 2);
|
||||
}
|
||||
|
||||
return $this->success('', [
|
||||
'days' => $days,
|
||||
'score' => $score,
|
||||
'money' => $money,
|
||||
]);
|
||||
}
|
||||
|
||||
public function profile(Request $request): Response
|
||||
{
|
||||
$response = $this->initializeFrontend($request);
|
||||
if ($response !== null) return $response;
|
||||
|
||||
if ($request->method() === 'POST') {
|
||||
$model = $this->auth->getUser();
|
||||
$data = $request->only(['avatar', 'username', 'nickname', 'gender', 'birthday', 'motto']);
|
||||
$data['id'] = $this->auth->id;
|
||||
if (!isset($data['birthday'])) {
|
||||
$data['birthday'] = null;
|
||||
}
|
||||
|
||||
try {
|
||||
Validator::make($data, [
|
||||
'username' => 'required|string|regex:/^[a-zA-Z][a-zA-Z0-9_]{2,15}$/',
|
||||
'nickname' => 'required|string|regex:/^[\x{4e00}-\x{9fa5}a-zA-Z0-9_-]+$/u',
|
||||
'birthday' => 'nullable|date',
|
||||
], [
|
||||
'nickname.regex' => __('nicknameChsDash'),
|
||||
])->validate();
|
||||
} catch (ValidationException $e) {
|
||||
return $this->error($e->getMessage());
|
||||
}
|
||||
|
||||
$existUser = User::where('username', $data['username'])->where('id', '<>', $this->auth->id)->find();
|
||||
if ($existUser) {
|
||||
return $this->error(__('Username') . ' ' . __('already exists'));
|
||||
}
|
||||
|
||||
$model->startTrans();
|
||||
try {
|
||||
$model->save($data);
|
||||
$model->commit();
|
||||
} catch (\Throwable $e) {
|
||||
$model->rollback();
|
||||
return $this->error($e->getMessage());
|
||||
}
|
||||
|
||||
return $this->success(__('Data updated successfully~'));
|
||||
}
|
||||
|
||||
return $this->success('', [
|
||||
'accountVerificationType' => get_account_verification_type()
|
||||
]);
|
||||
}
|
||||
|
||||
public function verification(Request $request): Response
|
||||
{
|
||||
$response = $this->initializeFrontend($request);
|
||||
if ($response !== null) return $response;
|
||||
|
||||
$captcha = new Captcha();
|
||||
$params = $request->only(['type', 'captcha']);
|
||||
$key = ($params['type'] == 'email' ? $this->auth->email : $this->auth->mobile) . "user_{$params['type']}_verify";
|
||||
if ($captcha->check($params['captcha'], $key)) {
|
||||
$uuid = Random::uuid();
|
||||
Token::set($uuid, $params['type'] . '-pass', $this->auth->id, 600);
|
||||
return $this->success('', [
|
||||
'type' => $params['type'],
|
||||
'accountVerificationToken' => $uuid,
|
||||
]);
|
||||
}
|
||||
return $this->error(__('Please enter the correct verification code'));
|
||||
}
|
||||
|
||||
public function changeBind(Request $request): Response
|
||||
{
|
||||
$response = $this->initializeFrontend($request);
|
||||
if ($response !== null) return $response;
|
||||
|
||||
$captcha = new Captcha();
|
||||
$params = $request->only(['type', 'captcha', 'email', 'mobile', 'accountVerificationToken', 'password']);
|
||||
$user = $this->auth->getUser();
|
||||
|
||||
if ($user[$params['type']]) {
|
||||
if (!Token::check($params['accountVerificationToken'], $params['type'] . '-pass', $user->id)) {
|
||||
return $this->error(__('You need to verify your account before modifying the binding information'));
|
||||
}
|
||||
} elseif (!isset($params['password']) || !verify_password($params['password'], $user->password, ['salt' => $user->salt])) {
|
||||
return $this->error(__('Password error'));
|
||||
}
|
||||
|
||||
if ($captcha->check($params['captcha'], $params[$params['type']] . "user_change_{$params['type']}")) {
|
||||
try {
|
||||
if ($params['type'] == 'email') {
|
||||
Validator::make($params, ['email' => 'required|email'])->validate();
|
||||
if (User::where('email', $params['email'])->find()) {
|
||||
return $this->error(__('Email') . ' ' . __('already exists'));
|
||||
}
|
||||
} else {
|
||||
Validator::make($params, ['mobile' => 'required|regex:/^1[3-9]\d{9}$/'])->validate();
|
||||
if (User::where('mobile', $params['mobile'])->find()) {
|
||||
return $this->error(__('Mobile') . ' ' . __('already exists'));
|
||||
}
|
||||
}
|
||||
} catch (ValidationException $e) {
|
||||
return $this->error(__($e->getMessage()));
|
||||
}
|
||||
if ($params['type'] == 'email') {
|
||||
$user->email = $params['email'];
|
||||
} else {
|
||||
$user->mobile = $params['mobile'];
|
||||
}
|
||||
Token::delete($params['accountVerificationToken']);
|
||||
$user->save();
|
||||
return $this->success();
|
||||
}
|
||||
return $this->error(__('Please enter the correct verification code'));
|
||||
}
|
||||
|
||||
public function changePassword(Request $request): Response
|
||||
{
|
||||
$response = $this->initializeFrontend($request);
|
||||
if ($response !== null) return $response;
|
||||
|
||||
if ($request->method() === 'POST') {
|
||||
$model = $this->auth->getUser();
|
||||
$params = $request->only(['oldPassword', 'newPassword']);
|
||||
|
||||
if (!verify_password($params['oldPassword'], $model->password, ['salt' => $model->salt])) {
|
||||
return $this->error(__('Old password error'));
|
||||
}
|
||||
|
||||
try {
|
||||
Validator::make(
|
||||
['password' => $params['newPassword']],
|
||||
['password' => 'required|string|regex:/^(?!.*[&<>"\'\n\r]).{6,32}$/'],
|
||||
['password.regex' => __('Please input correct password')]
|
||||
)->validate();
|
||||
} catch (ValidationException $e) {
|
||||
return $this->error($e->getMessage());
|
||||
}
|
||||
|
||||
$model->startTrans();
|
||||
try {
|
||||
$model->resetPassword($this->auth->id, $params['newPassword']);
|
||||
$model->commit();
|
||||
} catch (\Throwable $e) {
|
||||
$model->rollback();
|
||||
return $this->error($e->getMessage());
|
||||
}
|
||||
|
||||
$this->auth->logout();
|
||||
return $this->success(__('Password has been changed, please login again~'));
|
||||
}
|
||||
return $this->error(__('Method not allowed'), [], 0, ['statusCode' => 405]);
|
||||
}
|
||||
|
||||
public function integral(Request $request): Response
|
||||
{
|
||||
$response = $this->initializeFrontend($request);
|
||||
if ($response !== null) return $response;
|
||||
|
||||
$limit = $request->get('limit', $request->post('limit', 15));
|
||||
$res = UserScoreLog::where('user_id', $this->auth->id)
|
||||
->order('create_time', 'desc')
|
||||
->paginate($limit);
|
||||
|
||||
return $this->success('', [
|
||||
'list' => $res->items(),
|
||||
'total' => $res->total(),
|
||||
]);
|
||||
}
|
||||
|
||||
public function balance(Request $request): Response
|
||||
{
|
||||
$response = $this->initializeFrontend($request);
|
||||
if ($response !== null) return $response;
|
||||
|
||||
$limit = $request->get('limit', $request->post('limit', 15));
|
||||
$res = UserMoneyLog::where('user_id', $this->auth->id)
|
||||
->order('create_time', 'desc')
|
||||
->paginate($limit);
|
||||
|
||||
return $this->success('', [
|
||||
'list' => $res->items(),
|
||||
'total' => $res->total(),
|
||||
]);
|
||||
}
|
||||
|
||||
public function retrievePassword(Request $request): Response
|
||||
{
|
||||
$response = $this->initializeFrontend($request);
|
||||
if ($response !== null) return $response;
|
||||
|
||||
$params = $request->only(['type', 'account', 'captcha', 'password']);
|
||||
try {
|
||||
Validator::make($params, [
|
||||
'type' => 'required|in:email,mobile',
|
||||
'account' => 'required|string',
|
||||
'captcha' => 'required|string',
|
||||
'password' => 'required|string|regex:/^(?!.*[&<>"\'\n\r]).{6,32}$/',
|
||||
], [
|
||||
'password.regex' => __('Please input correct password'),
|
||||
])->validate();
|
||||
} catch (ValidationException $e) {
|
||||
return $this->error($e->getMessage());
|
||||
}
|
||||
|
||||
if ($params['type'] == 'email') {
|
||||
$user = User::where('email', $params['account'])->find();
|
||||
} else {
|
||||
$user = User::where('mobile', $params['account'])->find();
|
||||
}
|
||||
if (!$user) {
|
||||
return $this->error(__('Account does not exist~'));
|
||||
}
|
||||
|
||||
$captchaObj = new Captcha();
|
||||
if (!$captchaObj->check($params['captcha'], $params['account'] . 'user_retrieve_pwd')) {
|
||||
return $this->error(__('Please enter the correct verification code'));
|
||||
}
|
||||
|
||||
if ($user->resetPassword($user->id, $params['password'])) {
|
||||
return $this->success(__('Password has been changed~'));
|
||||
}
|
||||
return $this->error(__('Failed to modify password, please try again later~'));
|
||||
}
|
||||
}
|
||||
65
app/api/controller/Ajax.php
Normal file
65
app/api/controller/Ajax.php
Normal file
@@ -0,0 +1,65 @@
|
||||
<?php
|
||||
|
||||
namespace app\api\controller;
|
||||
|
||||
use app\common\controller\Frontend;
|
||||
use Webman\Http\Request;
|
||||
use support\Response;
|
||||
|
||||
class Ajax extends Frontend
|
||||
{
|
||||
protected array $noNeedLogin = ['area', 'buildSuffixSvg'];
|
||||
protected array $noNeedPermission = ['upload'];
|
||||
|
||||
public function upload(Request $request): Response
|
||||
{
|
||||
$response = $this->initializeFrontend($request);
|
||||
if ($response !== null) return $response;
|
||||
|
||||
$file = $request->file('file');
|
||||
if (!$file) {
|
||||
return $this->error(__('No files were uploaded'));
|
||||
}
|
||||
$file = new \app\common\library\upload\WebmanUploadedFile($file);
|
||||
$driver = $request->get('driver', $request->post('driver', 'local'));
|
||||
$topic = $request->get('topic', $request->post('topic', 'default'));
|
||||
|
||||
try {
|
||||
$upload = new \app\common\library\Upload();
|
||||
$attachment = $upload
|
||||
->setFile($file)
|
||||
->setDriver($driver)
|
||||
->setTopic($topic)
|
||||
->upload(null, 0, $this->auth->id);
|
||||
unset($attachment['create_time'], $attachment['quote']);
|
||||
} catch (\Throwable $e) {
|
||||
return $this->error($e->getMessage());
|
||||
}
|
||||
|
||||
return $this->success(__('File uploaded successfully'), [
|
||||
'file' => $attachment ?? []
|
||||
]);
|
||||
}
|
||||
|
||||
public function area(Request $request): Response
|
||||
{
|
||||
$response = $this->initializeFrontend($request);
|
||||
if ($response !== null) return $response;
|
||||
|
||||
return $this->success('', get_area($request));
|
||||
}
|
||||
|
||||
public function buildSuffixSvg(Request $request): Response
|
||||
{
|
||||
$response = $this->initializeFrontend($request);
|
||||
if ($response !== null) return $response;
|
||||
|
||||
$suffix = $request->get('suffix', $request->post('suffix', 'file'));
|
||||
$background = $request->get('background', $request->post('background'));
|
||||
$content = build_suffix_svg((string) $suffix, (string) $background);
|
||||
return response($content, 200, [
|
||||
'Content-Length' => strlen($content),
|
||||
'Content-Type' => 'image/svg+xml'
|
||||
]);
|
||||
}
|
||||
}
|
||||
84
app/api/controller/Common.php
Normal file
84
app/api/controller/Common.php
Normal file
@@ -0,0 +1,84 @@
|
||||
<?php
|
||||
|
||||
namespace app\api\controller;
|
||||
|
||||
use ba\Random;
|
||||
use ba\Captcha;
|
||||
use ba\ClickCaptcha;
|
||||
use app\common\controller\Api;
|
||||
use app\common\facade\Token;
|
||||
use app\admin\library\Auth as AdminAuth;
|
||||
use app\common\library\Auth as UserAuth;
|
||||
use Webman\Http\Request;
|
||||
use support\Response;
|
||||
|
||||
class Common extends Api
|
||||
{
|
||||
public function captcha(Request $request): Response
|
||||
{
|
||||
$response = $this->initializeApi($request);
|
||||
if ($response !== null) return $response;
|
||||
|
||||
$captchaId = $request->get('id', $request->post('id', ''));
|
||||
$config = [
|
||||
'codeSet' => '123456789',
|
||||
'fontSize' => 22,
|
||||
'useCurve' => false,
|
||||
'useNoise' => true,
|
||||
'length' => 4,
|
||||
'bg' => [255, 255, 255],
|
||||
];
|
||||
$captcha = new Captcha($config);
|
||||
return $captcha->entry($captchaId);
|
||||
}
|
||||
|
||||
public function clickCaptcha(Request $request): Response
|
||||
{
|
||||
$response = $this->initializeApi($request);
|
||||
if ($response !== null) return $response;
|
||||
|
||||
$id = $request->get('id', $request->post('id', ''));
|
||||
$captcha = new ClickCaptcha();
|
||||
return $this->success('', $captcha->creat($id));
|
||||
}
|
||||
|
||||
public function checkClickCaptcha(Request $request): Response
|
||||
{
|
||||
$response = $this->initializeApi($request);
|
||||
if ($response !== null) return $response;
|
||||
|
||||
$id = $request->post('id', '');
|
||||
$info = $request->post('info', '');
|
||||
$unset = filter_var($request->post('unset', false), FILTER_VALIDATE_BOOLEAN);
|
||||
$captcha = new ClickCaptcha();
|
||||
if ($captcha->check($id, $info, $unset)) return $this->success();
|
||||
return $this->error();
|
||||
}
|
||||
|
||||
public function refreshToken(Request $request): Response
|
||||
{
|
||||
$response = $this->initializeApi($request);
|
||||
if ($response !== null) return $response;
|
||||
|
||||
$refreshToken = $request->post('refreshToken', '');
|
||||
$refreshToken = Token::get($refreshToken);
|
||||
|
||||
if (!$refreshToken || $refreshToken['expire_time'] < time()) {
|
||||
return $this->error(__('Login expired, please login again.'));
|
||||
}
|
||||
|
||||
$newToken = Random::uuid();
|
||||
|
||||
if ($refreshToken['type'] == AdminAuth::TOKEN_TYPE . '-refresh') {
|
||||
Token::set($newToken, AdminAuth::TOKEN_TYPE, $refreshToken['user_id'], (int)config('buildadmin.admin_token_keep_time', 259200));
|
||||
}
|
||||
if ($refreshToken['type'] == UserAuth::TOKEN_TYPE . '-refresh') {
|
||||
Token::set($newToken, UserAuth::TOKEN_TYPE, $refreshToken['user_id'], (int)config('buildadmin.user_token_keep_time', 259200));
|
||||
}
|
||||
|
||||
return $this->success('', [
|
||||
'type' => $refreshToken['type'],
|
||||
'token' => $newToken
|
||||
]);
|
||||
}
|
||||
}
|
||||
99
app/api/controller/Ems.php
Normal file
99
app/api/controller/Ems.php
Normal file
@@ -0,0 +1,99 @@
|
||||
<?php
|
||||
|
||||
namespace app\api\controller;
|
||||
|
||||
use ba\Captcha;
|
||||
use ba\ClickCaptcha;
|
||||
use app\common\model\User;
|
||||
use app\common\library\Email;
|
||||
use app\common\controller\Frontend;
|
||||
use support\validation\Validator;
|
||||
use support\validation\ValidationException;
|
||||
use Webman\Http\Request;
|
||||
use support\Response;
|
||||
use PHPMailer\PHPMailer\Exception as PHPMailerException;
|
||||
|
||||
class Ems extends Frontend
|
||||
{
|
||||
protected array $noNeedLogin = ['send'];
|
||||
|
||||
public function send(Request $request): Response
|
||||
{
|
||||
$response = $this->initializeFrontend($request);
|
||||
if ($response !== null) return $response;
|
||||
|
||||
$params = $request->post(['email', 'event', 'captchaId', 'captchaInfo']);
|
||||
$mail = new Email();
|
||||
if (!$mail->configured) {
|
||||
return $this->error(__('Mail sending service unavailable'));
|
||||
}
|
||||
|
||||
try {
|
||||
Validator::make($params, [
|
||||
'email' => 'required|email',
|
||||
'event' => 'required|string',
|
||||
'captchaId' => 'required|string',
|
||||
'captchaInfo' => 'required|string',
|
||||
], [
|
||||
'email.required' => 'email format error',
|
||||
'event.required' => 'Parameter error',
|
||||
'captchaId.required' => 'Captcha error',
|
||||
'captchaInfo.required' => 'Captcha error',
|
||||
])->validate();
|
||||
} catch (ValidationException $e) {
|
||||
return $this->error(__($e->getMessage()));
|
||||
}
|
||||
|
||||
$captchaObj = new Captcha();
|
||||
$clickCaptcha = new ClickCaptcha();
|
||||
if (!$clickCaptcha->check($params['captchaId'], $params['captchaInfo'])) {
|
||||
return $this->error(__('Captcha error'));
|
||||
}
|
||||
|
||||
$captcha = $captchaObj->getCaptchaData($params['email'] . $params['event']);
|
||||
if ($captcha && time() - $captcha['create_time'] < 60) {
|
||||
return $this->error(__('Frequent email sending'));
|
||||
}
|
||||
|
||||
$userInfo = User::where('email', $params['email'])->find();
|
||||
if ($params['event'] == 'user_register' && $userInfo) {
|
||||
return $this->error(__('Email has been registered, please log in directly'));
|
||||
}
|
||||
if ($params['event'] == 'user_change_email' && $userInfo) {
|
||||
return $this->error(__('The email has been occupied'));
|
||||
}
|
||||
if (in_array($params['event'], ['user_retrieve_pwd', 'user_email_verify']) && !$userInfo) {
|
||||
return $this->error(__('Email not registered'));
|
||||
}
|
||||
|
||||
if ($params['event'] == 'user_email_verify') {
|
||||
if (!$this->auth->isLogin()) {
|
||||
return $this->error(__('Please login first'));
|
||||
}
|
||||
if ($this->auth->email != $params['email']) {
|
||||
return $this->error(__('Please use the account registration email to send the verification code'));
|
||||
}
|
||||
$password = $request->post('password');
|
||||
if (!verify_password($password, $this->auth->password, ['salt' => $this->auth->salt])) {
|
||||
return $this->error(__('Password error'));
|
||||
}
|
||||
}
|
||||
|
||||
$code = $captchaObj->create($params['email'] . $params['event']);
|
||||
$subject = __($params['event']) . '-' . get_sys_config('site_name');
|
||||
$body = __('Your verification code is: %s', [$code]);
|
||||
|
||||
try {
|
||||
$mail->isSMTP();
|
||||
$mail->addAddress($params['email']);
|
||||
$mail->isHTML();
|
||||
$mail->setSubject($subject);
|
||||
$mail->Body = $body;
|
||||
$mail->send();
|
||||
} catch (PHPMailerException) {
|
||||
return $this->error($mail->ErrorInfo);
|
||||
}
|
||||
|
||||
return $this->success(__('Mail sent successfully~'));
|
||||
}
|
||||
}
|
||||
71
app/api/controller/Index.php
Normal file
71
app/api/controller/Index.php
Normal file
@@ -0,0 +1,71 @@
|
||||
<?php
|
||||
|
||||
namespace app\api\controller;
|
||||
|
||||
use ba\Tree;
|
||||
use support\think\Db;
|
||||
use app\common\controller\Frontend;
|
||||
use app\common\library\token\TokenExpirationException;
|
||||
use Webman\Http\Request;
|
||||
|
||||
class Index extends Frontend
|
||||
{
|
||||
protected array $noNeedLogin = ['index'];
|
||||
|
||||
public function index(Request $request)
|
||||
{
|
||||
$response = $this->initializeFrontend($request);
|
||||
if ($response !== null) return $response;
|
||||
|
||||
$menus = [];
|
||||
if ($this->auth->isLogin()) {
|
||||
$rules = [];
|
||||
$userMenus = $this->auth->getMenus();
|
||||
foreach ($userMenus as $item) {
|
||||
if ($item['type'] == 'menu_dir') {
|
||||
$menus[] = $item;
|
||||
} elseif ($item['type'] != 'menu') {
|
||||
$rules[] = $item;
|
||||
}
|
||||
}
|
||||
$rules = array_values($rules);
|
||||
} else {
|
||||
$requiredLogin = filter_var($request->get('requiredLogin', false), FILTER_VALIDATE_BOOLEAN);
|
||||
if ($requiredLogin) {
|
||||
try {
|
||||
$token = get_auth_token(['ba', 'user', 'token'], $request);
|
||||
$this->auth->init($token);
|
||||
} catch (TokenExpirationException) {
|
||||
return $this->error(__('Token expiration'), [], 409);
|
||||
}
|
||||
return $this->error(__('Please login first'), [
|
||||
'type' => $this->auth::NEED_LOGIN
|
||||
], $this->auth::LOGIN_RESPONSE_CODE);
|
||||
}
|
||||
|
||||
$rules = Db::name('user_rule')
|
||||
->where('status', 1)
|
||||
->where('no_login_valid', 1)
|
||||
->where('type', 'in', ['route', 'nav', 'button'])
|
||||
->order('weigh', 'desc')
|
||||
->select()
|
||||
->toArray();
|
||||
$rules = Tree::instance()->assembleChild($rules);
|
||||
}
|
||||
|
||||
return $this->success('', [
|
||||
'site' => [
|
||||
'siteName' => get_sys_config('site_name'),
|
||||
'version' => get_sys_config('version'),
|
||||
'cdnUrl' => full_url(),
|
||||
'upload' => keys_to_camel_case(get_upload_config($request), ['max_size', 'save_name', 'allowed_suffixes', 'allowed_mime_types']),
|
||||
'recordNumber' => get_sys_config('record_number'),
|
||||
'cdnUrlParams' => config('buildadmin.cdn_url_params'),
|
||||
],
|
||||
'openMemberCenter' => config('buildadmin.open_member_center'),
|
||||
'userInfo' => $this->auth->getUserInfo(),
|
||||
'rules' => $rules,
|
||||
'menus' => $menus,
|
||||
]);
|
||||
}
|
||||
}
|
||||
676
app/api/controller/Install.php
Normal file
676
app/api/controller/Install.php
Normal file
@@ -0,0 +1,676 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace app\api\controller;
|
||||
|
||||
use Throwable;
|
||||
use ba\Random;
|
||||
use ba\Version;
|
||||
use ba\Terminal;
|
||||
use ba\Filesystem;
|
||||
use app\common\controller\Api;
|
||||
use app\admin\model\Admin as AdminModel;
|
||||
use app\admin\model\User as UserModel;
|
||||
use support\Response;
|
||||
use Webman\Http\Request;
|
||||
|
||||
/**
|
||||
* 安装控制器
|
||||
*/
|
||||
class Install extends Api
|
||||
{
|
||||
public const X64 = 'x64';
|
||||
|
||||
public const X86 = 'x86';
|
||||
|
||||
protected bool $useSystemSettings = false;
|
||||
|
||||
/**
|
||||
* 环境检查状态
|
||||
*/
|
||||
static string $ok = 'ok';
|
||||
static string $fail = 'fail';
|
||||
static string $warn = 'warn';
|
||||
|
||||
/**
|
||||
* 安装锁文件名称
|
||||
*/
|
||||
static string $lockFileName = 'install.lock';
|
||||
|
||||
/**
|
||||
* 配置文件
|
||||
*/
|
||||
static string $dbConfigFileName = 'thinkorm.php';
|
||||
static string $buildConfigFileName = 'buildadmin.php';
|
||||
|
||||
/**
|
||||
* 自动构建的前端文件的 outDir 相对于根目录
|
||||
*/
|
||||
static string $distDir = 'web' . DIRECTORY_SEPARATOR . 'dist';
|
||||
|
||||
/**
|
||||
* 需要的依赖版本
|
||||
*/
|
||||
static array $needDependentVersion = [
|
||||
'php' => '8.2.0',
|
||||
'npm' => '9.8.1',
|
||||
'cnpm' => '7.1.0',
|
||||
'node' => '20.14.0',
|
||||
'yarn' => '1.2.0',
|
||||
'pnpm' => '6.32.13',
|
||||
];
|
||||
|
||||
/**
|
||||
* 安装完成标记
|
||||
*/
|
||||
static string $InstallationCompletionMark = 'install-end';
|
||||
|
||||
/**
|
||||
* 命令执行窗口(exec 为 SSE 长连接,不会返回)
|
||||
* @throws Throwable
|
||||
*/
|
||||
public function terminal(Request $request): Response
|
||||
{
|
||||
$this->setRequest($request);
|
||||
if ($this->isInstallComplete()) {
|
||||
return $this->error(__('The system has completed installation. If you need to reinstall, please delete the %s file first', ['public/' . self::$lockFileName]));
|
||||
}
|
||||
|
||||
(new Terminal())->exec(false);
|
||||
return $this->success(); // unreachable: exec() blocks with SSE stream
|
||||
}
|
||||
|
||||
public function changePackageManager(Request $request): Response
|
||||
{
|
||||
$this->setRequest($request);
|
||||
if ($this->isInstallComplete()) {
|
||||
return $this->error(__('The system has completed installation. If you need to reinstall, please delete the %s file first', ['public/' . self::$lockFileName]));
|
||||
}
|
||||
|
||||
$newPackageManager = $request->post('manager', config('terminal.npm_package_manager'));
|
||||
if (Terminal::changeTerminalConfig()) {
|
||||
return $this->success('', [
|
||||
'manager' => $newPackageManager
|
||||
]);
|
||||
}
|
||||
return $this->error(__('Failed to switch package manager. Please modify the configuration file manually:%s', ['config/terminal.php']));
|
||||
}
|
||||
|
||||
/**
|
||||
* 环境基础检查
|
||||
*/
|
||||
public function envBaseCheck(Request $request): Response
|
||||
{
|
||||
$this->setRequest($request);
|
||||
if ($this->isInstallComplete()) {
|
||||
return $this->error(__('The system has completed installation. If you need to reinstall, please delete the %s file first', ['public/' . self::$lockFileName]), []);
|
||||
}
|
||||
if (($_ENV['DATABASE_TYPE'] ?? getenv('DATABASE_TYPE'))) {
|
||||
return $this->error(__('The .env file with database configuration was detected. Please clean up and try again!'));
|
||||
}
|
||||
|
||||
// php版本-start
|
||||
$phpVersion = phpversion();
|
||||
$phpBit = PHP_INT_SIZE == 8 ? self::X64 : self::X86;
|
||||
$phpVersionCompare = Version::compare(self::$needDependentVersion['php'], $phpVersion);
|
||||
if (!$phpVersionCompare) {
|
||||
$phpVersionLink = [
|
||||
[
|
||||
'name' => __('need') . ' >= ' . self::$needDependentVersion['php'],
|
||||
'type' => 'text'
|
||||
],
|
||||
[
|
||||
'name' => __('How to solve?'),
|
||||
'title' => __('Click to see how to solve it'),
|
||||
'type' => 'faq',
|
||||
'url' => 'https://doc.buildadmin.com/guide/install/preparePHP.html'
|
||||
]
|
||||
];
|
||||
} elseif ($phpBit != self::X64) {
|
||||
$phpVersionLink = [
|
||||
[
|
||||
'name' => __('need') . ' x64 PHP',
|
||||
'type' => 'text'
|
||||
],
|
||||
[
|
||||
'name' => __('How to solve?'),
|
||||
'title' => __('Click to see how to solve it'),
|
||||
'type' => 'faq',
|
||||
'url' => 'https://doc.buildadmin.com/guide/install/preparePHP.html'
|
||||
]
|
||||
];
|
||||
}
|
||||
// php版本-end
|
||||
|
||||
// 配置文件-start(分别检测目录和文件,便于定位问题)
|
||||
$configDir = rtrim(config_path(), '/\\');
|
||||
$dbConfigFile = $configDir . DIRECTORY_SEPARATOR . self::$dbConfigFileName;
|
||||
$configDirWritable = Filesystem::pathIsWritable($configDir);
|
||||
$dbConfigWritable = Filesystem::pathIsWritable($dbConfigFile);
|
||||
$configIsWritable = $configDirWritable && $dbConfigWritable;
|
||||
if (!$configIsWritable) {
|
||||
$configIsWritableLink = [
|
||||
[
|
||||
'name' => __('View reason'),
|
||||
'title' => __('Click to view the reason'),
|
||||
'type' => 'faq',
|
||||
'url' => 'https://doc.buildadmin.com/guide/install/dirNoPermission.html'
|
||||
]
|
||||
];
|
||||
}
|
||||
// 配置文件-end
|
||||
|
||||
// public-start
|
||||
$publicIsWritable = Filesystem::pathIsWritable(public_path());
|
||||
if (!$publicIsWritable) {
|
||||
$publicIsWritableLink = [
|
||||
[
|
||||
'name' => __('View reason'),
|
||||
'title' => __('Click to view the reason'),
|
||||
'type' => 'faq',
|
||||
'url' => 'https://doc.buildadmin.com/guide/install/dirNoPermission.html'
|
||||
]
|
||||
];
|
||||
}
|
||||
// public-end
|
||||
|
||||
// PDO-start
|
||||
$phpPdo = extension_loaded("PDO") && extension_loaded('pdo_mysql');
|
||||
if (!$phpPdo) {
|
||||
$phpPdoLink = [
|
||||
[
|
||||
'name' => __('PDO extensions need to be installed'),
|
||||
'type' => 'text'
|
||||
],
|
||||
[
|
||||
'name' => __('How to solve?'),
|
||||
'title' => __('Click to see how to solve it'),
|
||||
'type' => 'faq',
|
||||
'url' => 'https://doc.buildadmin.com/guide/install/missingExtension.html'
|
||||
]
|
||||
];
|
||||
}
|
||||
// PDO-end
|
||||
|
||||
// GD2和freeType-start
|
||||
$phpGd2 = extension_loaded('gd') && function_exists('imagettftext');
|
||||
if (!$phpGd2) {
|
||||
$phpGd2Link = [
|
||||
[
|
||||
'name' => __('The gd extension and freeType library need to be installed'),
|
||||
'type' => 'text'
|
||||
],
|
||||
[
|
||||
'name' => __('How to solve?'),
|
||||
'title' => __('Click to see how to solve it'),
|
||||
'type' => 'faq',
|
||||
'url' => 'https://doc.buildadmin.com/guide/install/gdFail.html'
|
||||
]
|
||||
];
|
||||
}
|
||||
// GD2和freeType-end
|
||||
|
||||
// proc_open
|
||||
$phpProc = function_exists('proc_open') && function_exists('proc_close') && function_exists('proc_get_status');
|
||||
if (!$phpProc) {
|
||||
$phpProcLink = [
|
||||
[
|
||||
'name' => __('View reason'),
|
||||
'title' => __('proc_open or proc_close functions in PHP Ini is disabled'),
|
||||
'type' => 'faq',
|
||||
'url' => 'https://doc.buildadmin.com/guide/install/disablement.html'
|
||||
],
|
||||
[
|
||||
'name' => __('How to modify'),
|
||||
'title' => __('Click to view how to modify'),
|
||||
'type' => 'faq',
|
||||
'url' => 'https://doc.buildadmin.com/guide/install/disablement.html'
|
||||
],
|
||||
[
|
||||
'name' => __('Security assurance?'),
|
||||
'title' => __('Using the installation service correctly will not cause any potential security problems. Click to view the details'),
|
||||
'type' => 'faq',
|
||||
'url' => 'https://doc.buildadmin.com/guide/install/senior.html'
|
||||
],
|
||||
];
|
||||
}
|
||||
// proc_open-end
|
||||
|
||||
return $this->success('', [
|
||||
'php_version' => [
|
||||
'describe' => $phpVersion . " ($phpBit)",
|
||||
'state' => $phpVersionCompare && $phpBit == self::X64 ? self::$ok : self::$fail,
|
||||
'link' => $phpVersionLink ?? [],
|
||||
],
|
||||
'config_is_writable' => [
|
||||
'describe' => $configIsWritable
|
||||
? self::writableStateDescribe(true)
|
||||
: (self::writableStateDescribe(false) . ' [' . $configDir . ']'),
|
||||
'state' => $configIsWritable ? self::$ok : self::$fail,
|
||||
'link' => $configIsWritableLink ?? []
|
||||
],
|
||||
'public_is_writable' => [
|
||||
'describe' => self::writableStateDescribe($publicIsWritable),
|
||||
'state' => $publicIsWritable ? self::$ok : self::$fail,
|
||||
'link' => $publicIsWritableLink ?? []
|
||||
],
|
||||
'php_pdo' => [
|
||||
'describe' => $phpPdo ? __('already installed') : __('Not installed'),
|
||||
'state' => $phpPdo ? self::$ok : self::$fail,
|
||||
'link' => $phpPdoLink ?? []
|
||||
],
|
||||
'php_gd2' => [
|
||||
'describe' => $phpGd2 ? __('already installed') : __('Not installed'),
|
||||
'state' => $phpGd2 ? self::$ok : self::$fail,
|
||||
'link' => $phpGd2Link ?? []
|
||||
],
|
||||
'php_proc' => [
|
||||
'describe' => $phpProc ? __('Allow execution') : __('disabled'),
|
||||
'state' => $phpProc ? self::$ok : self::$warn,
|
||||
'link' => $phpProcLink ?? []
|
||||
],
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* npm环境检查
|
||||
*/
|
||||
public function envNpmCheck(Request $request): Response
|
||||
{
|
||||
$this->setRequest($request);
|
||||
if ($this->isInstallComplete()) {
|
||||
return $this->error('', [], 2);
|
||||
}
|
||||
|
||||
$packageManager = $request->post('manager', 'none');
|
||||
|
||||
// npm
|
||||
$npmVersion = Version::getVersion('npm');
|
||||
$npmVersionCompare = Version::compare(self::$needDependentVersion['npm'], $npmVersion);
|
||||
if (!$npmVersionCompare || !$npmVersion) {
|
||||
$npmVersionLink = [
|
||||
[
|
||||
'name' => __('need') . ' >= ' . self::$needDependentVersion['npm'],
|
||||
'type' => 'text'
|
||||
],
|
||||
[
|
||||
'name' => __('How to solve?'),
|
||||
'title' => __('Click to see how to solve it'),
|
||||
'type' => 'faq',
|
||||
'url' => 'https://doc.buildadmin.com/guide/install/prepareNpm.html'
|
||||
]
|
||||
];
|
||||
}
|
||||
|
||||
// 包管理器
|
||||
$pmVersionLink = [];
|
||||
$pmVersion = __('nothing');
|
||||
$pmVersionCompare = false;
|
||||
if (in_array($packageManager, ['npm', 'cnpm', 'pnpm', 'yarn'])) {
|
||||
$pmVersion = Version::getVersion($packageManager);
|
||||
$pmVersionCompare = Version::compare(self::$needDependentVersion[$packageManager], $pmVersion);
|
||||
|
||||
if (!$pmVersion) {
|
||||
$pmVersionLink[] = [
|
||||
'name' => __('need') . ' >= ' . self::$needDependentVersion[$packageManager],
|
||||
'type' => 'text'
|
||||
];
|
||||
if ($npmVersionCompare) {
|
||||
$pmVersionLink[] = [
|
||||
'name' => __('Click Install %s', [$packageManager]),
|
||||
'title' => '',
|
||||
'type' => 'install-package-manager'
|
||||
];
|
||||
} else {
|
||||
$pmVersionLink[] = [
|
||||
'name' => __('Please install NPM first'),
|
||||
'type' => 'text'
|
||||
];
|
||||
}
|
||||
} elseif (!$pmVersionCompare) {
|
||||
$pmVersionLink[] = [
|
||||
'name' => __('need') . ' >= ' . self::$needDependentVersion[$packageManager],
|
||||
'type' => 'text'
|
||||
];
|
||||
$pmVersionLink[] = [
|
||||
'name' => __('Please upgrade %s version', [$packageManager]),
|
||||
'type' => 'text'
|
||||
];
|
||||
}
|
||||
} elseif ($packageManager == 'ni') {
|
||||
$pmVersionCompare = true;
|
||||
}
|
||||
|
||||
// nodejs
|
||||
$nodejsVersion = Version::getVersion('node');
|
||||
$nodejsVersionCompare = Version::compare(self::$needDependentVersion['node'], $nodejsVersion);
|
||||
if (!$nodejsVersionCompare || !$nodejsVersion) {
|
||||
$nodejsVersionLink = [
|
||||
[
|
||||
'name' => __('need') . ' >= ' . self::$needDependentVersion['node'],
|
||||
'type' => 'text'
|
||||
],
|
||||
[
|
||||
'name' => __('How to solve?'),
|
||||
'title' => __('Click to see how to solve it'),
|
||||
'type' => 'faq',
|
||||
'url' => 'https://doc.buildadmin.com/guide/install/prepareNodeJs.html'
|
||||
]
|
||||
];
|
||||
}
|
||||
|
||||
return $this->success('', [
|
||||
'npm_version' => [
|
||||
'describe' => $npmVersion ?: __('Acquisition failed'),
|
||||
'state' => $npmVersionCompare ? self::$ok : self::$warn,
|
||||
'link' => $npmVersionLink ?? [],
|
||||
],
|
||||
'nodejs_version' => [
|
||||
'describe' => $nodejsVersion ?: __('Acquisition failed'),
|
||||
'state' => $nodejsVersionCompare ? self::$ok : self::$warn,
|
||||
'link' => $nodejsVersionLink ?? []
|
||||
],
|
||||
'npm_package_manager' => [
|
||||
'describe' => $pmVersion ?: __('Acquisition failed'),
|
||||
'state' => $pmVersionCompare ? self::$ok : self::$warn,
|
||||
'link' => $pmVersionLink ?? [],
|
||||
]
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* 测试数据库连接
|
||||
*/
|
||||
public function testDatabase(Request $request): Response
|
||||
{
|
||||
$this->setRequest($request);
|
||||
$database = [
|
||||
'hostname' => $request->post('hostname'),
|
||||
'username' => $request->post('username'),
|
||||
'password' => $request->post('password'),
|
||||
'hostport' => $request->post('hostport'),
|
||||
'database' => '',
|
||||
];
|
||||
|
||||
$conn = $this->connectDb($database);
|
||||
if ($conn['code'] == 0) {
|
||||
return $this->error($conn['msg']);
|
||||
}
|
||||
return $this->success('', [
|
||||
'databases' => $conn['databases']
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* 系统基础配置
|
||||
* post请求=开始安装
|
||||
*/
|
||||
public function baseConfig(Request $request): Response
|
||||
{
|
||||
$this->setRequest($request);
|
||||
if ($this->isInstallComplete()) {
|
||||
return $this->error(__('The system has completed installation. If you need to reinstall, please delete the %s file first', ['public/' . self::$lockFileName]));
|
||||
}
|
||||
|
||||
$envOk = $this->commandExecutionCheck();
|
||||
$rootPath = str_replace('\\', '/', root_path());
|
||||
$migrateCommand = 'php vendor/bin/phinx migrate';
|
||||
if ($request->isGet()) {
|
||||
return $this->success('', [
|
||||
'rootPath' => $rootPath,
|
||||
'executionWebCommand' => $envOk,
|
||||
'migrateCommand' => $migrateCommand,
|
||||
]);
|
||||
}
|
||||
|
||||
$connectData = $databaseParam = $request->only(['hostname', 'username', 'password', 'hostport', 'database', 'prefix']);
|
||||
|
||||
// 数据库配置测试
|
||||
$connectData['database'] = '';
|
||||
$connect = $this->connectDb($connectData, true);
|
||||
if ($connect['code'] == 0) {
|
||||
return $this->error($connect['msg']);
|
||||
}
|
||||
|
||||
// 建立数据库
|
||||
if (!in_array($databaseParam['database'], $connect['databases'])) {
|
||||
$sql = "CREATE DATABASE IF NOT EXISTS `{$databaseParam['database']}` CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci";
|
||||
$connect['pdo']->exec($sql);
|
||||
}
|
||||
|
||||
// 写入数据库配置文件(thinkorm.php 使用 $env('database.xxx', 'default') 格式)
|
||||
$dbConfigFile = config_path(self::$dbConfigFileName);
|
||||
$dbConfigContent = @file_get_contents($dbConfigFile);
|
||||
if ($dbConfigContent === false || $dbConfigContent === '') {
|
||||
return $this->error(__('File has no write permission:%s', ['config/' . self::$dbConfigFileName]));
|
||||
}
|
||||
$callback = function ($matches) use ($databaseParam) {
|
||||
$key = $matches[1];
|
||||
$value = (string) ($databaseParam[$key] ?? '');
|
||||
return "\$env('database.{$key}', '" . addslashes($value) . "')";
|
||||
};
|
||||
$dbConfigText = preg_replace_callback("/\\\$env\('database\.(hostname|database|username|password|hostport|prefix)',\s*'[^']*'\)/", $callback, $dbConfigContent);
|
||||
$result = @file_put_contents($dbConfigFile, $dbConfigText);
|
||||
if (!$result) {
|
||||
return $this->error(__('File has no write permission:%s', ['config/' . self::$dbConfigFileName]));
|
||||
}
|
||||
|
||||
// 写入 dafuweng-webman/.env-example
|
||||
$envFile = root_path() . '.env-example';
|
||||
$envFileContent = @file_get_contents($envFile);
|
||||
if ($envFileContent) {
|
||||
$databasePos = stripos($envFileContent, '[DATABASE]');
|
||||
if ($databasePos !== false) {
|
||||
$envFileContent = substr($envFileContent, 0, $databasePos);
|
||||
}
|
||||
$envFileContent .= "\n" . '[DATABASE]' . "\n";
|
||||
$envFileContent .= 'TYPE = mysql' . "\n";
|
||||
$envFileContent .= 'HOSTNAME = ' . $databaseParam['hostname'] . "\n";
|
||||
$envFileContent .= 'DATABASE = ' . $databaseParam['database'] . "\n";
|
||||
$envFileContent .= 'USERNAME = ' . $databaseParam['username'] . "\n";
|
||||
$envFileContent .= 'PASSWORD = ' . $databaseParam['password'] . "\n";
|
||||
$envFileContent .= 'HOSTPORT = ' . $databaseParam['hostport'] . "\n";
|
||||
$envFileContent .= 'PREFIX = ' . ($databaseParam['prefix'] ?? '') . "\n";
|
||||
$envFileContent .= 'CHARSET = utf8mb4' . "\n";
|
||||
$envFileContent .= 'DEBUG = true' . "\n";
|
||||
$result = @file_put_contents($envFile, $envFileContent);
|
||||
if (!$result) {
|
||||
return $this->error(__('File has no write permission:%s', ['/' . $envFile]));
|
||||
}
|
||||
}
|
||||
|
||||
// 设置新的Token随机密钥key
|
||||
$oldTokenKey = config('buildadmin.token.key');
|
||||
$newTokenKey = Random::build('alnum', 32);
|
||||
$buildConfigFile = config_path(self::$buildConfigFileName);
|
||||
$buildConfigContent = @file_get_contents($buildConfigFile);
|
||||
if ($buildConfigContent === false || $buildConfigContent === '') {
|
||||
return $this->error(__('File has no write permission:%s', ['config/' . self::$buildConfigFileName]));
|
||||
}
|
||||
$buildConfigContent = preg_replace("/'key'(\s+)=>(\s+)'$oldTokenKey'/", "'key'\$1=>\$2'$newTokenKey'", $buildConfigContent);
|
||||
$result = @file_put_contents($buildConfigFile, $buildConfigContent);
|
||||
if (!$result) {
|
||||
return $this->error(__('File has no write permission:%s', ['config/' . self::$buildConfigFileName]));
|
||||
}
|
||||
|
||||
// 建立安装锁文件
|
||||
$result = @file_put_contents(public_path(self::$lockFileName), date('Y-m-d H:i:s'));
|
||||
if (!$result) {
|
||||
return $this->error(__('File has no write permission:%s', ['public/' . self::$lockFileName]));
|
||||
}
|
||||
|
||||
return $this->success('', [
|
||||
'rootPath' => $rootPath,
|
||||
'executionWebCommand' => $envOk,
|
||||
'migrateCommand' => $migrateCommand,
|
||||
]);
|
||||
}
|
||||
|
||||
protected function isInstallComplete(): bool
|
||||
{
|
||||
if (is_file(public_path(self::$lockFileName))) {
|
||||
$contents = @file_get_contents(public_path(self::$lockFileName));
|
||||
if ($contents == self::$InstallationCompletionMark) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* 标记命令执行完毕
|
||||
* @throws Throwable
|
||||
*/
|
||||
public function commandExecComplete(Request $request): Response
|
||||
{
|
||||
$this->setRequest($request);
|
||||
if ($this->isInstallComplete()) {
|
||||
return $this->error(__('The system has completed installation. If you need to reinstall, please delete the %s file first', ['public/' . self::$lockFileName]));
|
||||
}
|
||||
|
||||
$param = $request->only(['type', 'adminname', 'adminpassword', 'sitename']);
|
||||
if ($param['type'] == 'web') {
|
||||
$result = @file_put_contents(public_path(self::$lockFileName), self::$InstallationCompletionMark);
|
||||
if (!$result) {
|
||||
return $this->error(__('File has no write permission:%s', ['public/' . self::$lockFileName]));
|
||||
}
|
||||
} else {
|
||||
// 管理员配置入库
|
||||
$adminModel = new AdminModel();
|
||||
$defaultAdmin = $adminModel->where('username', 'admin')->find();
|
||||
$defaultAdmin->username = $param['adminname'];
|
||||
$defaultAdmin->nickname = ucfirst($param['adminname']);
|
||||
$defaultAdmin->save();
|
||||
|
||||
if (isset($param['adminpassword']) && $param['adminpassword']) {
|
||||
$adminModel->resetPassword($defaultAdmin->id, $param['adminpassword']);
|
||||
}
|
||||
|
||||
// 默认用户密码修改
|
||||
$user = new UserModel();
|
||||
$user->resetPassword(1, Random::build());
|
||||
|
||||
// 修改站点名称
|
||||
if (class_exists(\app\admin\model\Config::class)) {
|
||||
\app\admin\model\Config::where('name', 'site_name')->update([
|
||||
'value' => $param['sitename']
|
||||
]);
|
||||
}
|
||||
}
|
||||
return $this->success();
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取命令执行检查的结果
|
||||
* @return bool 是否拥有执行命令的条件
|
||||
*/
|
||||
private function commandExecutionCheck(): bool
|
||||
{
|
||||
$pm = config('terminal.npm_package_manager');
|
||||
if ($pm == 'none') {
|
||||
return false;
|
||||
}
|
||||
$check['phpPopen'] = function_exists('proc_open') && function_exists('proc_close');
|
||||
$check['npmVersionCompare'] = Version::compare(self::$needDependentVersion['npm'], Version::getVersion('npm'));
|
||||
$check['pmVersionCompare'] = Version::compare(self::$needDependentVersion[$pm], Version::getVersion($pm));
|
||||
$check['nodejsVersionCompare'] = Version::compare(self::$needDependentVersion['node'], Version::getVersion('node'));
|
||||
|
||||
$envOk = true;
|
||||
foreach ($check as $value) {
|
||||
if (!$value) {
|
||||
$envOk = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
return $envOk;
|
||||
}
|
||||
|
||||
/**
|
||||
* 安装指引
|
||||
*/
|
||||
public function manualInstall(Request $request): Response
|
||||
{
|
||||
$this->setRequest($request);
|
||||
return $this->success('', [
|
||||
'webPath' => str_replace('\\', '/', root_path() . 'web')
|
||||
]);
|
||||
}
|
||||
|
||||
public function mvDist(Request $request): Response
|
||||
{
|
||||
$this->setRequest($request);
|
||||
if (!is_file(root_path() . self::$distDir . DIRECTORY_SEPARATOR . 'index.html')) {
|
||||
return $this->error(__('No built front-end file found, please rebuild manually!'));
|
||||
}
|
||||
|
||||
if (Terminal::mvDist()) {
|
||||
return $this->success();
|
||||
}
|
||||
return $this->error(__('Failed to move the front-end file, please move it manually!'));
|
||||
}
|
||||
|
||||
/**
|
||||
* 目录是否可写
|
||||
* @param $writable
|
||||
* @return string
|
||||
*/
|
||||
private static function writableStateDescribe($writable): string
|
||||
{
|
||||
return $writable ? __('Writable') : __('No write permission');
|
||||
}
|
||||
|
||||
/**
|
||||
* 数据库连接-获取数据表列表(使用 raw PDO)
|
||||
* @param array $database hostname, hostport, username, password, database
|
||||
* @param bool $returnPdo
|
||||
* @return array
|
||||
*/
|
||||
private function connectDb(array $database, bool $returnPdo = false): array
|
||||
{
|
||||
$host = $database['hostname'] ?? '127.0.0.1';
|
||||
$port = $database['hostport'] ?? '3306';
|
||||
$user = $database['username'] ?? '';
|
||||
$pass = $database['password'] ?? '';
|
||||
$db = $database['database'] ?? '';
|
||||
|
||||
$dsn = "mysql:host={$host};port={$port};charset=utf8mb4";
|
||||
if ($db) {
|
||||
$dsn .= ";dbname={$db}";
|
||||
}
|
||||
|
||||
try {
|
||||
$pdo = new \PDO($dsn, $user, $pass, [
|
||||
\PDO::ATTR_ERRMODE => \PDO::ERRMODE_EXCEPTION,
|
||||
\PDO::MYSQL_ATTR_USE_BUFFERED_QUERY => true,
|
||||
]);
|
||||
$pdo->query("SELECT 1")->fetchAll(\PDO::FETCH_ASSOC);
|
||||
} catch (\PDOException $e) {
|
||||
$errorMsg = mb_convert_encoding($e->getMessage() ?: 'unknown', 'UTF-8', 'UTF-8,GBK,GB2312,BIG5');
|
||||
$template = __('Database connection failed:%s');
|
||||
return [
|
||||
'code' => 0,
|
||||
'msg' => strpos($template, '%s') !== false ? sprintf($template, $errorMsg) : $template . $errorMsg,
|
||||
];
|
||||
}
|
||||
|
||||
$databases = [];
|
||||
$databasesExclude = ['information_schema', 'mysql', 'performance_schema', 'sys'];
|
||||
$stmt = $pdo->query("SHOW DATABASES");
|
||||
$rows = $stmt->fetchAll(\PDO::FETCH_ASSOC);
|
||||
$stmt->closeCursor();
|
||||
foreach ($rows as $row) {
|
||||
$dbName = $row['Database'] ?? $row['database'] ?? '';
|
||||
if ($dbName && !in_array($dbName, $databasesExclude)) {
|
||||
$databases[] = $dbName;
|
||||
}
|
||||
}
|
||||
|
||||
return [
|
||||
'code' => 1,
|
||||
'msg' => '',
|
||||
'databases' => $databases,
|
||||
'pdo' => $returnPdo ? $pdo : null,
|
||||
];
|
||||
}
|
||||
}
|
||||
151
app/api/controller/User.php
Normal file
151
app/api/controller/User.php
Normal file
@@ -0,0 +1,151 @@
|
||||
<?php
|
||||
|
||||
namespace app\api\controller;
|
||||
|
||||
use ba\Captcha;
|
||||
use ba\ClickCaptcha;
|
||||
use app\common\controller\Frontend;
|
||||
use app\common\facade\Token;
|
||||
use support\validation\Validator;
|
||||
use support\validation\ValidationException;
|
||||
use Webman\Http\Request;
|
||||
use support\Response;
|
||||
|
||||
class User extends Frontend
|
||||
{
|
||||
protected array $noNeedLogin = ['checkIn', 'logout'];
|
||||
|
||||
public function checkIn(Request $request): Response
|
||||
{
|
||||
$response = $this->initializeFrontend($request);
|
||||
if ($response !== null) return $response;
|
||||
|
||||
$openMemberCenter = config('buildadmin.open_member_center');
|
||||
if (!$openMemberCenter) {
|
||||
return $this->error(__('Member center disabled'));
|
||||
}
|
||||
|
||||
if ($this->auth->isLogin()) {
|
||||
return $this->success(__('You have already logged in. There is no need to log in again~'), [
|
||||
'type' => $this->auth::LOGGED_IN
|
||||
], $this->auth::LOGIN_RESPONSE_CODE);
|
||||
}
|
||||
|
||||
$userLoginCaptchaSwitch = config('buildadmin.user_login_captcha');
|
||||
|
||||
if ($request->method() === 'POST') {
|
||||
$params = $request->post();
|
||||
$params = array_merge($params, [
|
||||
'tab' => $params['tab'] ?? '',
|
||||
'email' => $params['email'] ?? '',
|
||||
'mobile' => $params['mobile'] ?? '',
|
||||
'username' => $params['username'] ?? '',
|
||||
'password' => $params['password'] ?? '',
|
||||
'keep' => $params['keep'] ?? false,
|
||||
'captcha' => $params['captcha'] ?? '',
|
||||
'captchaId' => $params['captchaId'] ?? '',
|
||||
'captchaInfo' => $params['captchaInfo'] ?? '',
|
||||
'registerType' => $params['registerType'] ?? '',
|
||||
]);
|
||||
|
||||
if (!in_array($params['tab'], ['login', 'register'])) {
|
||||
return $this->error(__('Unknown operation'));
|
||||
}
|
||||
|
||||
try {
|
||||
$rules = $params['tab'] === 'login' ? $this->getLoginRules($userLoginCaptchaSwitch) : $this->getRegisterRules();
|
||||
Validator::make($params, $rules[0], $rules[1] ?? [], $rules[2] ?? [])->validate();
|
||||
} catch (ValidationException $e) {
|
||||
return $this->error($e->getMessage());
|
||||
}
|
||||
|
||||
if ($params['tab'] === 'login') {
|
||||
if ($userLoginCaptchaSwitch) {
|
||||
$captchaObj = new ClickCaptcha();
|
||||
if (!$captchaObj->check($params['captchaId'], $params['captchaInfo'])) {
|
||||
return $this->error(__('Captcha error'));
|
||||
}
|
||||
}
|
||||
$res = $this->auth->login($params['username'], $params['password'], !empty($params['keep']));
|
||||
} else {
|
||||
$captchaObj = new Captcha();
|
||||
if (!$captchaObj->check($params['captcha'], $params[$params['registerType']] . 'user_register')) {
|
||||
return $this->error(__('Please enter the correct verification code'));
|
||||
}
|
||||
$res = $this->auth->register($params['username'], $params['password'], $params['mobile'], $params['email']);
|
||||
}
|
||||
|
||||
if ($res === true) {
|
||||
return $this->success(__('Login succeeded!'), [
|
||||
'userInfo' => $this->auth->getUserInfo(),
|
||||
'routePath' => '/user'
|
||||
]);
|
||||
}
|
||||
$msg = $this->auth->getError();
|
||||
return $this->error($msg ?: __('Check in failed, please try again or contact the website administrator~'));
|
||||
}
|
||||
|
||||
return $this->success('', [
|
||||
'userLoginCaptchaSwitch' => $userLoginCaptchaSwitch,
|
||||
'accountVerificationType' => get_account_verification_type()
|
||||
]);
|
||||
}
|
||||
|
||||
private function getLoginRules(bool $captchaSwitch): array
|
||||
{
|
||||
$rules = [
|
||||
'username' => 'required|string',
|
||||
'password' => 'required|string|regex:/^(?!.*[&<>"\'\n\r]).{6,32}$/',
|
||||
];
|
||||
$messages = [
|
||||
'password.regex' => __('Please input correct password'),
|
||||
];
|
||||
if ($captchaSwitch) {
|
||||
$rules['captchaId'] = 'required|string';
|
||||
$rules['captchaInfo'] = 'required|string';
|
||||
}
|
||||
return [$rules, $messages, []];
|
||||
}
|
||||
|
||||
private function getRegisterRules(): array
|
||||
{
|
||||
return [
|
||||
[
|
||||
'username' => 'required|string|regex:/^[a-zA-Z][a-zA-Z0-9_]{2,15}$/|unique:user,username',
|
||||
'password' => 'required|string|regex:/^(?!.*[&<>"\'\n\r]).{6,32}$/',
|
||||
'registerType' => 'required|in:email,mobile',
|
||||
'email' => 'required_if:registerType,email|email|unique:user,email',
|
||||
'mobile' => 'required_if:registerType,mobile|regex:/^1[3-9]\d{9}$/|unique:user,mobile',
|
||||
'captcha' => 'required|string',
|
||||
],
|
||||
[
|
||||
'username.regex' => __('Please input correct username'),
|
||||
'password.regex' => __('Please input correct password'),
|
||||
],
|
||||
[
|
||||
'username' => __('Username'),
|
||||
'email' => __('Email'),
|
||||
'mobile' => __('Mobile'),
|
||||
'password' => __('Password'),
|
||||
'captcha' => __('captcha'),
|
||||
'registerType' => __('Register type'),
|
||||
]
|
||||
];
|
||||
}
|
||||
|
||||
public function logout(Request $request): Response
|
||||
{
|
||||
$response = $this->initializeFrontend($request);
|
||||
if ($response !== null) return $response;
|
||||
|
||||
if ($request->method() === 'POST') {
|
||||
$refreshToken = $request->post('refreshToken', '');
|
||||
if ($refreshToken) {
|
||||
Token::delete((string) $refreshToken);
|
||||
}
|
||||
$this->auth->logout();
|
||||
return $this->success();
|
||||
}
|
||||
return $this->error(__('Method not allowed'), [], 0, ['statusCode' => 405]);
|
||||
}
|
||||
}
|
||||
21
app/api/lang/en.php
Normal file
21
app/api/lang/en.php
Normal file
@@ -0,0 +1,21 @@
|
||||
<?php
|
||||
return [
|
||||
'Login expired, please login again.' => 'Login expired, please login again.',
|
||||
'Account not exist' => 'Account does not exist',
|
||||
'Account disabled' => 'Account is disabled',
|
||||
'Token login failed' => 'Token login failed',
|
||||
'Please try again after 1 day' => 'The number of failed login attempts has exceeded the limit, please try again after 24 hours.',
|
||||
'Password is incorrect' => 'Incorrect password',
|
||||
'You are not logged in' => 'You are not logged in.',
|
||||
'Unknown operation' => 'Unknown operation',
|
||||
'No action available, please contact the administrator~' => 'There is no action available, please contact the administrator~',
|
||||
'Please login first' => 'Please login first!',
|
||||
'You have no permission' => 'No permission to operate!',
|
||||
'Captcha error' => 'Captcha error!',
|
||||
// Member center account
|
||||
'Data updated successfully~' => 'Data updated successfully~',
|
||||
'Password has been changed~' => 'Password has been changed~',
|
||||
'Password has been changed, please login again~' => 'Password has been changed, please login again~',
|
||||
'already exists' => 'already exists',
|
||||
'nicknameChsDash' => 'Usernames can only be Chinese characters, letters, numbers, underscores_ and dashes-.',
|
||||
];
|
||||
16
app/api/lang/en/account.php
Normal file
16
app/api/lang/en/account.php
Normal file
@@ -0,0 +1,16 @@
|
||||
<?php
|
||||
return [
|
||||
'nickname' => 'Nickname',
|
||||
'birthday' => 'Birthday',
|
||||
'captcha' => 'Captcha',
|
||||
'Old password error' => 'Old password error',
|
||||
'Data updated successfully~' => 'Data updated successfully',
|
||||
'Please input correct password' => 'Please enter the correct password',
|
||||
'nicknameChsDash' => 'Usernames can only be Chinese characters, letters, numbers, underscores_ and dashes-.',
|
||||
'Password has been changed~' => 'Password has been changed~',
|
||||
'Password has been changed, please login again~' => 'Password has been changed, please login again~',
|
||||
'Account does not exist~' => 'Account does not exist',
|
||||
'Failed to modify password, please try again later~' => 'Failed to modify password, please try again later~',
|
||||
'Please enter the correct verification code' => 'Please enter the correct Captcha',
|
||||
'%s has been registered' => '%s has been registered, please login directly.',
|
||||
];
|
||||
16
app/api/lang/en/ems.php
Normal file
16
app/api/lang/en/ems.php
Normal file
@@ -0,0 +1,16 @@
|
||||
<?php
|
||||
return [
|
||||
'email format error' => 'email format error',
|
||||
'user_register' => 'Member registration verification',
|
||||
'user_retrieve_pwd' => 'Retrieve password verification',
|
||||
'user_change_email' => 'Modify mailbox validation',
|
||||
'user_email_verify' => 'Member Email Verification',
|
||||
'Your verification code is: %s' => 'Your Captcha is: %s,valid for 10 minutes~',
|
||||
'Mail sent successfully~' => 'Mail sent successfully',
|
||||
'Account does not exist~' => 'Account does not exist',
|
||||
'Mail sending service unavailable' => 'The mail sending service is not working, please contact the webmaster to configure it.',
|
||||
'Frequent email sending' => 'Frequent email sending',
|
||||
'Email has been registered, please log in directly' => 'Email has been registered, please log in directly~',
|
||||
'The email has been occupied' => 'The email has been occupied',
|
||||
'Email not registered' => 'Email not registered',
|
||||
];
|
||||
44
app/api/lang/en/install.php
Normal file
44
app/api/lang/en/install.php
Normal file
@@ -0,0 +1,44 @@
|
||||
<?php
|
||||
return [
|
||||
'Install the controller' => 'Install the controller',
|
||||
'need' => 'Need',
|
||||
'Click to see how to solve it' => 'Click to see how to solve.',
|
||||
'Please check the config directory permissions' => 'Please check the Config directory permissions',
|
||||
'Please check the public directory permissions' => 'Please check the Public directory permissions',
|
||||
'open' => 'Open',
|
||||
'close' => 'Close',
|
||||
'The installation can continue, and some operations need to be completed manually' => 'You can continue to install, and some operations need to be completed manually ',
|
||||
'Allow execution' => 'Allow execution',
|
||||
'disabled' => 'Disabled',
|
||||
'Allow operation' => 'Allow operation',
|
||||
'Acquisition failed' => 'Access failed',
|
||||
'Click Install %s' => 'Click Install %s',
|
||||
'Writable' => 'Writable',
|
||||
'No write permission' => 'No write permissions',
|
||||
'already installed' => 'Installed',
|
||||
'Not installed' => 'Not installed',
|
||||
'File has no write permission:%s' => 'File has no write permission:%s',
|
||||
'The system has completed installation. If you need to reinstall, please delete the %s file first' => 'The system has been installed, if you need to reinstall, please delete the %s file first.',
|
||||
'Database connection failed:%s' => 'Database connection failure:%s',
|
||||
'Failed to install SQL execution:%s' => 'Installation SQL execution failed:%s',
|
||||
'unknown' => 'Unknown',
|
||||
'Database does not exist' => 'Database does not exist!',
|
||||
'No built front-end file found, please rebuild manually!' => 'No built front-end file found, please rebuild manually.',
|
||||
'Failed to move the front-end file, please move it manually!' => 'Failed to move the front-end file, please move manually!',
|
||||
'How to solve?' => 'How to solve?',
|
||||
'View reason' => 'View reasons',
|
||||
'Click to view the reason' => 'Click to see the reason',
|
||||
'PDO extensions need to be installed' => 'pdo_mysql extensions need to be installed.',
|
||||
'proc_open or proc_close functions in PHP Ini is disabled' => 'proc_open and proc_close functions in PHP.Ini is disabled.',
|
||||
'How to modify' => 'How to modify?',
|
||||
'Click to view how to modify' => 'Click to see how to modify.',
|
||||
'Security assurance?' => 'Security assurance?',
|
||||
'Using the installation service correctly will not cause any potential security problems. Click to view the details' => 'The correct use of the installation service will not cause any potential security issues. Click to view the details.',
|
||||
'Please install NPM first' => 'Please install NPM first.',
|
||||
'Installation error:%s' => 'Installation error:%s',
|
||||
'Failed to switch package manager. Please modify the configuration file manually:%s' => 'Package manager switch failed, please modify the configuration file manually:%s.',
|
||||
'Please upgrade %s version' => 'Please upgrade the %s version',
|
||||
'nothing' => 'Nothing',
|
||||
'The gd extension and freeType library need to be installed' => 'The gd2 extension and freeType library need to be installed',
|
||||
'The .env file with database configuration was detected. Please clean up and try again!' => 'The .env file with database configuration was detected. Please clean up and try again!',
|
||||
];
|
||||
13
app/api/lang/en/user.php
Normal file
13
app/api/lang/en/user.php
Normal file
@@ -0,0 +1,13 @@
|
||||
<?php
|
||||
return [
|
||||
'captcha' => 'Captcha',
|
||||
'captchaId' => 'Captcha ID',
|
||||
'Please input correct username' => 'Please enter the correct username.',
|
||||
'Please input correct password' => 'Please enter the correct password.',
|
||||
'Registration parameter error' => 'Wrong registration parameter',
|
||||
'Login succeeded!' => 'Login succeeded!',
|
||||
'Please enter the correct verification code' => 'Please enter the correct Captcha.',
|
||||
'You have already logged in. There is no need to log in again~' => 'You have already logged in, no need to log in again.',
|
||||
'Check in failed, please try again or contact the website administrator~' => 'Check in failed,please try again or contact the webmaster.',
|
||||
'Member center disabled' => 'The member centre has been disabled, please contact the webmaster to turn it on.',
|
||||
];
|
||||
53
app/api/lang/zh-cn.php
Normal file
53
app/api/lang/zh-cn.php
Normal file
@@ -0,0 +1,53 @@
|
||||
<?php
|
||||
return [
|
||||
// 时间格式化-s
|
||||
'%d second%s ago' => '%d秒前',
|
||||
'%d minute%s ago' => '%d分钟前',
|
||||
'%d hour%s ago' => '%d小时前',
|
||||
'%d day%s ago' => '%d天前',
|
||||
'%d week%s ago' => '%d周前',
|
||||
'%d month%s ago' => '%d月前',
|
||||
'%d year%s ago' => '%d年前',
|
||||
'%d second%s after' => '%d秒后',
|
||||
'%d minute%s after' => '%d分钟后',
|
||||
'%d hour%s after' => '%d小时后',
|
||||
'%d day%s after' => '%d天后',
|
||||
'%d week%s after' => '%d周后',
|
||||
'%d month%s after' => '%d月后',
|
||||
'%d year%s after' => '%d年后',
|
||||
// 时间格式化-e
|
||||
// 文件上传-s
|
||||
'File uploaded successfully' => '文件上传成功!',
|
||||
'No files were uploaded' => '没有文件被上传',
|
||||
'The uploaded file format is not allowed' => '上传的文件格式未被允许',
|
||||
'The uploaded image file is not a valid image' => '上传的图片文件不是有效的图像',
|
||||
'The uploaded file is too large (%sMiB), Maximum file size:%sMiB' => '上传的文件太大(%sM),最大文件大小:%sM',
|
||||
'No files have been uploaded or the file size exceeds the upload limit of the server' => '没有文件被上传或文件大小超出服务器上传限制!',
|
||||
'Topic format error' => '上传存储子目录格式错误!',
|
||||
'Driver %s not supported' => '不支持的驱动:%s',
|
||||
// 文件上传-e
|
||||
'Username' => '用户名',
|
||||
'Email' => '邮箱',
|
||||
'Mobile' => '手机号',
|
||||
'Password' => '密码',
|
||||
'Login expired, please login again.' => '登录过期,请重新登录。',
|
||||
'Account not exist' => '帐户不存在',
|
||||
'Account disabled' => '帐户已禁用',
|
||||
'Token login failed' => '令牌登录失败',
|
||||
'Please try again after 1 day' => '登录失败次数超限,请在1天后再试',
|
||||
'Password is incorrect' => '密码不正确',
|
||||
'You are not logged in' => '你没有登录',
|
||||
'Unknown operation' => '未知操作',
|
||||
'No action available, please contact the administrator~' => '没有可用操作,请联系管理员~',
|
||||
'Please login first' => '请先登录!',
|
||||
'You have no permission' => '没有权限操作!',
|
||||
'Parameter error' => '参数错误!',
|
||||
'Token expiration' => '登录态过期,请重新登录!',
|
||||
'Captcha error' => '验证码错误!',
|
||||
// 会员中心 account
|
||||
'Data updated successfully~' => '资料更新成功~',
|
||||
'Password has been changed~' => '密码已修改~',
|
||||
'Password has been changed, please login again~' => '密码已修改,请重新登录~',
|
||||
'already exists' => '已存在',
|
||||
'nicknameChsDash' => '用户名只能是汉字、字母、数字和下划线_及破折号-',
|
||||
];
|
||||
22
app/api/lang/zh-cn/account.php
Normal file
22
app/api/lang/zh-cn/account.php
Normal file
@@ -0,0 +1,22 @@
|
||||
<?php
|
||||
return [
|
||||
'nickname' => '昵称',
|
||||
'birthday' => '生日',
|
||||
'captcha' => '验证码',
|
||||
'Old password error' => '旧密码错误',
|
||||
'Data updated successfully~' => '资料更新成功~',
|
||||
'Please input correct password' => '请输入正确的密码',
|
||||
'nicknameChsDash' => '用户名只能是汉字、字母、数字和下划线_及破折号-',
|
||||
'Password has been changed~' => '密码已修改~',
|
||||
'Password has been changed, please login again~' => '密码已修改,请重新登录~',
|
||||
'Account does not exist~' => '账户不存在~',
|
||||
'Failed to modify password, please try again later~' => '修改密码失败,请稍后重试~',
|
||||
'Please enter the correct verification code' => '请输入正确的验证码!',
|
||||
'%s has been registered' => '%s已被注册,请直接登录~',
|
||||
'email format error' => '电子邮箱格式错误!',
|
||||
'mobile format error' => '手机号格式错误!',
|
||||
'You need to verify your account before modifying the binding information' => '您需要先通过账户验证才能修改绑定信息!',
|
||||
'Password error' => '密码错误!',
|
||||
'email is occupied' => '电子邮箱地址已被占用!',
|
||||
'mobile is occupied' => '手机号已被占用!',
|
||||
];
|
||||
18
app/api/lang/zh-cn/ems.php
Normal file
18
app/api/lang/zh-cn/ems.php
Normal file
@@ -0,0 +1,18 @@
|
||||
<?php
|
||||
return [
|
||||
'email format error' => '电子邮箱格式错误',
|
||||
'user_register' => '会员注册验证',
|
||||
'user_change_email' => '修改邮箱验证',
|
||||
'user_retrieve_pwd' => '找回密码验证',
|
||||
'user_email_verify' => '会员身份验证',
|
||||
'Your verification code is: %s' => '您的验证码是:%s,十分钟内有效~',
|
||||
'Mail sent successfully~' => '邮件发送成功~',
|
||||
'Account does not exist~' => '账户不存在~',
|
||||
'Mail sending service unavailable' => '邮件发送服务不可用,请联系网站管理员进行配置~',
|
||||
'Frequent email sending' => '频繁发送电子邮件',
|
||||
'Email has been registered, please log in directly' => '电子邮箱已注册,请直接登录~',
|
||||
'The email has been occupied' => '电子邮箱已被占用!',
|
||||
'Email not registered' => '电子邮箱未注册',
|
||||
'Please use the account registration email to send the verification code' => '请使用账户注册邮箱发送验证码!',
|
||||
'Password error' => '密码错误!',
|
||||
];
|
||||
44
app/api/lang/zh-cn/install.php
Normal file
44
app/api/lang/zh-cn/install.php
Normal file
@@ -0,0 +1,44 @@
|
||||
<?php
|
||||
return [
|
||||
'Install the controller' => '安装控制器',
|
||||
'need' => '需要',
|
||||
'Click to see how to solve it' => '点击查看如何解决',
|
||||
'Please check the config directory permissions' => '请检查 config 目录权限',
|
||||
'Please check the public directory permissions' => '请检查 public 目录权限',
|
||||
'open' => '开启',
|
||||
'close' => '关闭',
|
||||
'The installation can continue, and some operations need to be completed manually' => '可以继续安装,部分操作需手动完成',
|
||||
'Allow execution' => '允许执行',
|
||||
'disabled' => '已禁用',
|
||||
'Allow operation' => '允许操作',
|
||||
'Acquisition failed' => '获取失败',
|
||||
'Click Install %s' => '点击安装%s',
|
||||
'Writable' => '可写',
|
||||
'No write permission' => '无写权限',
|
||||
'already installed' => '已安装',
|
||||
'Not installed' => '未安装',
|
||||
'File has no write permission:%s' => '文件无写入权限:%s',
|
||||
'The system has completed installation. If you need to reinstall, please delete the %s file first' => '系统已完成安装。如果需要重新安装,请先删除 %s 文件',
|
||||
'Database connection failed:%s' => '数据库连接失败:%s',
|
||||
'Failed to install SQL execution:%s' => '安装SQL执行失败:%s',
|
||||
'unknown' => '未知',
|
||||
'Database does not exist' => '数据库不存在!',
|
||||
'No built front-end file found, please rebuild manually!' => '没有找到构建好的前端文件,请手动重新构建!',
|
||||
'Failed to move the front-end file, please move it manually!' => '移动前端文件失败,请手动移动!',
|
||||
'How to solve?' => '如何解决?',
|
||||
'View reason' => '查看原因',
|
||||
'Click to view the reason' => '点击查看原因',
|
||||
'PDO extensions need to be installed' => '需要安装 pdo_mysql 扩展',
|
||||
'proc_open or proc_close functions in PHP Ini is disabled' => 'proc_open和proc_close函数在php.ini中被禁用掉了',
|
||||
'How to modify' => '如何修改',
|
||||
'Click to view how to modify' => '点击查看如何修改',
|
||||
'Security assurance?' => '安全保证?',
|
||||
'Using the installation service correctly will not cause any potential security problems. Click to view the details' => '安装服务使用正确不会造成任何潜在安全问题,点击查看详情',
|
||||
'Please install NPM first' => '请先安装npm',
|
||||
'Installation error:%s' => '安装出错:%s',
|
||||
'Failed to switch package manager. Please modify the configuration file manually:%s' => '包管理器切换失败,请手动修改配置文件:%s',
|
||||
'Please upgrade %s version' => '请升级%s版本',
|
||||
'nothing' => '无',
|
||||
'The gd extension and freeType library need to be installed' => '需要gd2扩展和freeType库',
|
||||
'The .env file with database configuration was detected. Please clean up and try again!' => '检测到带有数据库配置的 .env 文件。请清理后再试一次!',
|
||||
];
|
||||
14
app/api/lang/zh-cn/user.php
Normal file
14
app/api/lang/zh-cn/user.php
Normal file
@@ -0,0 +1,14 @@
|
||||
<?php
|
||||
return [
|
||||
'captcha' => '验证码',
|
||||
'captchaId' => '验证码标识',
|
||||
'Register type' => '注册类型',
|
||||
'Please input correct username' => '请输入正确的用户名',
|
||||
'Please input correct password' => '请输入正确的密码',
|
||||
'Registration parameter error' => '注册参数错误',
|
||||
'Login succeeded!' => '登录成功',
|
||||
'Please enter the correct verification code' => '请输入正确的验证码',
|
||||
'You have already logged in. There is no need to log in again~' => '您已经登录过了,无需重复登录~',
|
||||
'Check in failed, please try again or contact the website administrator~' => '签入失败,请重试或联系网站管理员~',
|
||||
'Member center disabled' => '会员中心已禁用,请联系网站管理员开启。',
|
||||
];
|
||||
46
app/api/validate/Account.php
Normal file
46
app/api/validate/Account.php
Normal file
@@ -0,0 +1,46 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace app\api\validate;
|
||||
|
||||
use think\Validate;
|
||||
|
||||
class Account extends Validate
|
||||
{
|
||||
protected $failException = true;
|
||||
|
||||
protected $rule = [
|
||||
'username' => 'require|regex:^[a-zA-Z][a-zA-Z0-9_]{2,15}$|unique:user',
|
||||
'nickname' => 'require|chsDash',
|
||||
'birthday' => 'date',
|
||||
'email' => 'require|email|unique:user',
|
||||
'mobile' => 'require|mobile|unique:user',
|
||||
'password' => 'require|regex:^(?!.*[&<>"\'\n\r]).{6,32}$',
|
||||
'account' => 'require',
|
||||
'captcha' => 'require',
|
||||
];
|
||||
|
||||
protected $scene = [
|
||||
'edit' => ['username', 'nickname', 'birthday'],
|
||||
'changePassword' => ['password'],
|
||||
'retrievePassword' => ['account', 'captcha', 'password'],
|
||||
];
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->field = [
|
||||
'username' => __('Username'),
|
||||
'email' => __('Email'),
|
||||
'mobile' => __('Mobile'),
|
||||
'password' => __('Password'),
|
||||
'nickname' => __('nickname'),
|
||||
'birthday' => __('birthday'),
|
||||
];
|
||||
$this->message = array_merge($this->message, [
|
||||
'nickname.chsDash' => __('nicknameChsDash'),
|
||||
'password.regex' => __('Please input correct password')
|
||||
]);
|
||||
parent::__construct();
|
||||
}
|
||||
}
|
||||
59
app/api/validate/User.php
Normal file
59
app/api/validate/User.php
Normal file
@@ -0,0 +1,59 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace app\api\validate;
|
||||
|
||||
use think\Validate;
|
||||
|
||||
class User extends Validate
|
||||
{
|
||||
protected $failException = true;
|
||||
|
||||
protected $rule = [
|
||||
'username' => 'require|regex:^[a-zA-Z][a-zA-Z0-9_]{2,15}$|unique:user',
|
||||
'password' => 'require|regex:^(?!.*[&<>"\'\n\r]).{6,32}$',
|
||||
'registerType' => 'require|in:email,mobile',
|
||||
'email' => 'email|unique:user|requireIf:registerType,email',
|
||||
'mobile' => 'mobile|unique:user|requireIf:registerType,mobile',
|
||||
'captcha' => 'require',
|
||||
'captchaId' => 'require',
|
||||
'captchaInfo' => 'require',
|
||||
];
|
||||
|
||||
protected $scene = [
|
||||
'register' => ['username', 'password', 'registerType', 'email', 'mobile', 'captcha'],
|
||||
];
|
||||
|
||||
public function sceneLogin(): static
|
||||
{
|
||||
$fields = ['username', 'password'];
|
||||
|
||||
$userLoginCaptchaSwitch = config('buildadmin.user_login_captcha');
|
||||
if ($userLoginCaptchaSwitch) {
|
||||
$fields[] = 'captchaId';
|
||||
$fields[] = 'captchaInfo';
|
||||
}
|
||||
|
||||
return $this->only($fields)->remove('username', ['regex', 'unique']);
|
||||
}
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->field = [
|
||||
'username' => __('Username'),
|
||||
'email' => __('Email'),
|
||||
'mobile' => __('Mobile'),
|
||||
'password' => __('Password'),
|
||||
'captcha' => __('captcha'),
|
||||
'captchaId' => __('captchaId'),
|
||||
'captchaInfo' => __('captcha'),
|
||||
'registerType' => __('Register type'),
|
||||
];
|
||||
$this->message = array_merge($this->message, [
|
||||
'username.regex' => __('Please input correct username'),
|
||||
'password.regex' => __('Please input correct password')
|
||||
]);
|
||||
parent::__construct();
|
||||
}
|
||||
}
|
||||
79
app/common/controller/Api.php
Normal file
79
app/common/controller/Api.php
Normal file
@@ -0,0 +1,79 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace app\common\controller;
|
||||
|
||||
use app\support\BaseController;
|
||||
use support\Response;
|
||||
use support\think\Db;
|
||||
use think\db\exception\PDOException;
|
||||
use Webman\Http\Request as WebmanRequest;
|
||||
|
||||
/**
|
||||
* API 控制器基类
|
||||
* 迁移自 app/common/controller/Api.php,适配 Webman
|
||||
*/
|
||||
class Api extends BaseController
|
||||
{
|
||||
/**
|
||||
* 默认响应输出类型
|
||||
*/
|
||||
protected string $responseType = 'json';
|
||||
|
||||
/**
|
||||
* 是否开启系统站点配置(数据库检查、ip_check、set_timezone)
|
||||
*/
|
||||
protected bool $useSystemSettings = true;
|
||||
|
||||
/**
|
||||
* API 初始化(需在控制器方法开头调用)
|
||||
* @param WebmanRequest $request
|
||||
* @return Response|null 若需直接返回(如 ip 禁止、数据库错误)则返回 Response,否则 null
|
||||
*/
|
||||
public function initializeApi(WebmanRequest $request): ?Response
|
||||
{
|
||||
$this->setRequest($request);
|
||||
|
||||
if ($this->useSystemSettings) {
|
||||
// 检查数据库连接
|
||||
try {
|
||||
Db::execute('SELECT 1');
|
||||
} catch (PDOException $e) {
|
||||
return $this->error(mb_convert_encoding($e->getMessage(), 'UTF-8', 'UTF-8,GBK,GB2312,BIG5'));
|
||||
}
|
||||
|
||||
// IP 检查
|
||||
$ipCheckResponse = ip_check(null, $request);
|
||||
if ($ipCheckResponse !== null) {
|
||||
return $ipCheckResponse;
|
||||
}
|
||||
|
||||
// 时区设定
|
||||
set_timezone();
|
||||
}
|
||||
|
||||
// 语言包由 LoadLangPack 中间件加载
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 从 Request 或路由解析控制器路径(如 user/user)
|
||||
* 优先从 $request->controller 解析,否则从 path 解析
|
||||
*/
|
||||
protected function getControllerPath(WebmanRequest $request): ?string
|
||||
{
|
||||
return get_controller_path($request);
|
||||
}
|
||||
|
||||
/**
|
||||
* 从 Request 解析应用名(api 或 admin)
|
||||
*/
|
||||
protected function getAppFromRequest(WebmanRequest $request): string
|
||||
{
|
||||
$path = trim($request->path(), '/');
|
||||
$parts = explode('/', $path);
|
||||
return $parts[0] ?? 'api';
|
||||
}
|
||||
}
|
||||
435
app/common/controller/Backend.php
Normal file
435
app/common/controller/Backend.php
Normal file
@@ -0,0 +1,435 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace app\common\controller;
|
||||
|
||||
use Throwable;
|
||||
use app\admin\library\Auth;
|
||||
use app\common\library\token\TokenExpirationException;
|
||||
use app\admin\library\traits\Backend as BackendTrait;
|
||||
use support\Response;
|
||||
use Webman\Http\Request as WebmanRequest;
|
||||
|
||||
/**
|
||||
* 后台控制器基类
|
||||
* 迁移 Auth 鉴权、权限校验、CRUD trait
|
||||
*/
|
||||
class Backend extends Api
|
||||
{
|
||||
use BackendTrait;
|
||||
/**
|
||||
* 无需登录的方法
|
||||
*/
|
||||
protected array $noNeedLogin = [];
|
||||
|
||||
/**
|
||||
* 无需鉴权的方法
|
||||
*/
|
||||
protected array $noNeedPermission = [];
|
||||
|
||||
/**
|
||||
* 权限类实例
|
||||
* @var Auth|null
|
||||
*/
|
||||
protected ?Auth $auth = null;
|
||||
|
||||
/**
|
||||
* 模型类实例(子类设置)
|
||||
* @var object|null
|
||||
*/
|
||||
protected ?object $model = null;
|
||||
|
||||
/**
|
||||
* 新增/编辑时排除字段
|
||||
*/
|
||||
protected array|string $preExcludeFields = [];
|
||||
|
||||
/**
|
||||
* 权重字段
|
||||
*/
|
||||
protected string $weighField = 'weigh';
|
||||
|
||||
/**
|
||||
* 默认排序
|
||||
*/
|
||||
protected string|array $defaultSortField = [];
|
||||
|
||||
/**
|
||||
* 有序保证
|
||||
*/
|
||||
protected string|array $orderGuarantee = [];
|
||||
|
||||
/**
|
||||
* 快速搜索字段
|
||||
*/
|
||||
protected string|array $quickSearchField = 'id';
|
||||
|
||||
/**
|
||||
* 数据限制
|
||||
*/
|
||||
protected bool|string|int $dataLimit = false;
|
||||
|
||||
/**
|
||||
* 数据限制字段
|
||||
*/
|
||||
protected string $dataLimitField = 'admin_id';
|
||||
|
||||
/**
|
||||
* 数据限制开启时自动填充
|
||||
*/
|
||||
protected bool $dataLimitFieldAutoFill = true;
|
||||
|
||||
/**
|
||||
* 查看请求返回的主表字段
|
||||
*/
|
||||
protected string|array $indexField = ['*'];
|
||||
|
||||
/**
|
||||
* 是否开启模型验证
|
||||
*/
|
||||
protected bool $modelValidate = true;
|
||||
|
||||
/**
|
||||
* 是否开启模型场景验证
|
||||
*/
|
||||
protected bool $modelSceneValidate = false;
|
||||
|
||||
/**
|
||||
* 关联查询方法名
|
||||
*/
|
||||
protected array $withJoinTable = [];
|
||||
|
||||
/**
|
||||
* 关联查询 JOIN 方式
|
||||
*/
|
||||
protected string $withJoinType = 'LEFT';
|
||||
|
||||
/**
|
||||
* 输入过滤函数名(如 clean_xss,CRUD 含 editor 字段时自动设置)
|
||||
*/
|
||||
protected string $inputFilter = '';
|
||||
|
||||
/**
|
||||
* 后台初始化(需在控制器方法开头调用,在 initializeApi 之后)
|
||||
* @return Response|null 需直接返回时返回 Response,否则 null
|
||||
*/
|
||||
public function initializeBackend(WebmanRequest $request): ?Response
|
||||
{
|
||||
$response = $this->initializeApi($request);
|
||||
if ($response !== null) {
|
||||
return $response;
|
||||
}
|
||||
|
||||
// 调用子类 initialize(CRUD 生成的控制器在此设置 model)
|
||||
$this->initialize();
|
||||
|
||||
$action = $this->getActionFromPath($request->path());
|
||||
$needLogin = !action_in_arr($this->noNeedLogin, $action);
|
||||
$needPermission = !action_in_arr($this->noNeedPermission, $action);
|
||||
|
||||
try {
|
||||
$this->auth = Auth::instance();
|
||||
$token = get_auth_token(['ba', 'token'], $request);
|
||||
if ($token) {
|
||||
$this->auth->init($token);
|
||||
}
|
||||
} catch (TokenExpirationException) {
|
||||
if ($needLogin) {
|
||||
return $this->error(__('Token expiration'), [], 409);
|
||||
}
|
||||
}
|
||||
|
||||
if ($needLogin) {
|
||||
if (!$this->auth->isLogin()) {
|
||||
return $this->error(__('Please login first'), [
|
||||
'type' => Auth::NEED_LOGIN,
|
||||
], 0, ['statusCode' => Auth::LOGIN_RESPONSE_CODE]);
|
||||
}
|
||||
if ($needPermission) {
|
||||
$controllerPath = $this->getControllerPath($request);
|
||||
$routePath = $controllerPath . '/' . $action;
|
||||
if (!$this->auth->check($routePath)) {
|
||||
return $this->error(__('You have no permission'), [], 401);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
event_trigger('backendInit', $this->auth);
|
||||
|
||||
if (method_exists($this, 'initController')) {
|
||||
$initResp = $this->initController($request);
|
||||
if ($initResp !== null) {
|
||||
return $initResp;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 子类可覆盖,用于初始化 model 等(替代原 initialize)
|
||||
* @return Response|null 需直接返回时返回 Response,否则 null
|
||||
*/
|
||||
protected function initController(WebmanRequest $request): ?Response
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
public function index(WebmanRequest $request): Response
|
||||
{
|
||||
$response = $this->initializeBackend($request);
|
||||
if ($response !== null) return $response;
|
||||
return $this->_index();
|
||||
}
|
||||
|
||||
public function add(WebmanRequest $request): Response
|
||||
{
|
||||
$response = $this->initializeBackend($request);
|
||||
if ($response !== null) return $response;
|
||||
return $this->_add();
|
||||
}
|
||||
|
||||
public function edit(WebmanRequest $request): Response
|
||||
{
|
||||
$response = $this->initializeBackend($request);
|
||||
if ($response !== null) return $response;
|
||||
return $this->_edit();
|
||||
}
|
||||
|
||||
public function del(WebmanRequest $request): Response
|
||||
{
|
||||
$response = $this->initializeBackend($request);
|
||||
if ($response !== null) return $response;
|
||||
return $this->_del();
|
||||
}
|
||||
|
||||
public function sortable(WebmanRequest $request): Response
|
||||
{
|
||||
$response = $this->initializeBackend($request);
|
||||
if ($response !== null) return $response;
|
||||
return $this->_sortable();
|
||||
}
|
||||
|
||||
public function select(WebmanRequest $request): Response
|
||||
{
|
||||
$response = $this->initializeBackend($request);
|
||||
if ($response !== null) return $response;
|
||||
$this->_select();
|
||||
return $this->success();
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询参数构建器
|
||||
*/
|
||||
public function queryBuilder(): array
|
||||
{
|
||||
if (empty($this->model) || !$this->request) {
|
||||
return [[], [], 10, []];
|
||||
}
|
||||
$pk = $this->model->getPk();
|
||||
$quickSearch = $this->request->get('quickSearch', '');
|
||||
$limit = $this->request->get('limit', 10);
|
||||
$limit = is_numeric($limit) ? intval($limit) : 10;
|
||||
$search = $this->request->get('search', []);
|
||||
$search = is_array($search) ? $search : [];
|
||||
$initKey = $this->request->get('initKey', $pk);
|
||||
$initValue = $this->request->get('initValue', '');
|
||||
$initOperator = $this->request->get('initOperator', 'in');
|
||||
|
||||
$where = [];
|
||||
$modelTable = strtolower($this->model->getTable());
|
||||
$alias = [];
|
||||
$alias[$modelTable] = parse_name(basename(str_replace('\\', '/', get_class($this->model))));
|
||||
$mainTableAlias = $alias[$modelTable] . '.';
|
||||
|
||||
if ($quickSearch) {
|
||||
$quickSearchArr = is_array($this->quickSearchField) ? $this->quickSearchField : explode(',', $this->quickSearchField);
|
||||
foreach ($quickSearchArr as $k => $v) {
|
||||
$quickSearchArr[$k] = str_contains($v, '.') ? $v : $mainTableAlias . $v;
|
||||
}
|
||||
$where[] = [implode('|', $quickSearchArr), 'LIKE', '%' . str_replace('%', '\%', $quickSearch) . '%'];
|
||||
}
|
||||
if ($initValue) {
|
||||
$where[] = [$initKey, $initOperator, $initValue];
|
||||
$limit = 999999;
|
||||
}
|
||||
|
||||
foreach ($search as $field) {
|
||||
if (!is_array($field) || !isset($field['operator']) || !isset($field['field']) || !isset($field['val'])) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$field['operator'] = $this->getOperatorByAlias($field['operator']);
|
||||
|
||||
if (str_contains($field['field'], '.')) {
|
||||
$fieldNameParts = explode('.', $field['field']);
|
||||
$fieldNamePartsLastKey = array_key_last($fieldNameParts);
|
||||
foreach ($fieldNameParts as $fieldNamePartsKey => $fieldNamePart) {
|
||||
if ($fieldNamePartsKey !== $fieldNamePartsLastKey) {
|
||||
$fieldNameParts[$fieldNamePartsKey] = parse_name($fieldNamePart);
|
||||
}
|
||||
}
|
||||
$fieldName = implode('.', $fieldNameParts);
|
||||
} else {
|
||||
$fieldName = $mainTableAlias . $field['field'];
|
||||
}
|
||||
|
||||
if (isset($field['render']) && $field['render'] == 'datetime') {
|
||||
if ($field['operator'] == 'RANGE') {
|
||||
$datetimeArr = explode(',', $field['val']);
|
||||
if (!isset($datetimeArr[1])) {
|
||||
continue;
|
||||
}
|
||||
$datetimeArr = array_filter(array_map('strtotime', $datetimeArr));
|
||||
$where[] = [$fieldName, str_replace('RANGE', 'BETWEEN', $field['operator']), $datetimeArr];
|
||||
continue;
|
||||
}
|
||||
$where[] = [$fieldName, '=', strtotime($field['val'])];
|
||||
continue;
|
||||
}
|
||||
|
||||
if ($field['operator'] == 'RANGE' || $field['operator'] == 'NOT RANGE') {
|
||||
$arr = explode(',', $field['val']);
|
||||
if (!isset($arr[0]) || $arr[0] === '') {
|
||||
$operator = $field['operator'] == 'RANGE' ? '<=' : '>';
|
||||
$arr = $arr[1];
|
||||
} elseif (!isset($arr[1]) || $arr[1] === '') {
|
||||
$operator = $field['operator'] == 'RANGE' ? '>=' : '<';
|
||||
$arr = $arr[0];
|
||||
} else {
|
||||
$operator = str_replace('RANGE', 'BETWEEN', $field['operator']);
|
||||
}
|
||||
$where[] = [$fieldName, $operator, $arr];
|
||||
continue;
|
||||
}
|
||||
|
||||
switch ($field['operator']) {
|
||||
case '=':
|
||||
case '<>':
|
||||
$where[] = [$fieldName, $field['operator'], strval($field['val'])];
|
||||
break;
|
||||
case 'LIKE':
|
||||
case 'NOT LIKE':
|
||||
$where[] = [$fieldName, $field['operator'], '%' . str_replace('%', '\%', $field['val']) . '%'];
|
||||
break;
|
||||
case '>':
|
||||
case '>=':
|
||||
case '<':
|
||||
case '<=':
|
||||
$where[] = [$fieldName, $field['operator'], intval($field['val'])];
|
||||
break;
|
||||
case 'FIND_IN_SET':
|
||||
if (is_array($field['val'])) {
|
||||
foreach ($field['val'] as $val) {
|
||||
$where[] = [$fieldName, 'find in set', $val];
|
||||
}
|
||||
} else {
|
||||
$where[] = [$fieldName, 'find in set', $field['val']];
|
||||
}
|
||||
break;
|
||||
case 'IN':
|
||||
case 'NOT IN':
|
||||
$where[] = [$fieldName, $field['operator'], is_array($field['val']) ? $field['val'] : explode(',', $field['val'])];
|
||||
break;
|
||||
case 'NULL':
|
||||
case 'NOT NULL':
|
||||
$where[] = [$fieldName, strtolower($field['operator']), ''];
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
$dataLimitAdminIds = $this->getDataLimitAdminIds();
|
||||
if ($dataLimitAdminIds) {
|
||||
$where[] = [$mainTableAlias . $this->dataLimitField, 'in', $dataLimitAdminIds];
|
||||
}
|
||||
|
||||
return [$where, $alias, $limit, $this->queryOrderBuilder()];
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询的排序参数构建器
|
||||
*/
|
||||
public function queryOrderBuilder(): array
|
||||
{
|
||||
$pk = $this->model->getPk();
|
||||
$order = $this->request ? $this->request->get('order') : null;
|
||||
$order = $order ?: $this->defaultSortField;
|
||||
|
||||
if ($order && is_string($order)) {
|
||||
$order = explode(',', $order);
|
||||
$order = [$order[0] => $order[1] ?? 'asc'];
|
||||
}
|
||||
if (!$this->orderGuarantee) {
|
||||
$this->orderGuarantee = [$pk => 'desc'];
|
||||
} elseif (is_string($this->orderGuarantee)) {
|
||||
$orderParts = explode(',', $this->orderGuarantee);
|
||||
$this->orderGuarantee = [$orderParts[0] => $orderParts[1] ?? 'asc'];
|
||||
}
|
||||
$orderGuaranteeKey = array_key_first($this->orderGuarantee);
|
||||
if (!is_array($order) || !array_key_exists($orderGuaranteeKey, $order)) {
|
||||
$order[$orderGuaranteeKey] = $this->orderGuarantee[$orderGuaranteeKey];
|
||||
}
|
||||
|
||||
return $order;
|
||||
}
|
||||
|
||||
/**
|
||||
* 数据权限控制 - 获取有权限访问的管理员 IDs
|
||||
*/
|
||||
protected function getDataLimitAdminIds(): array
|
||||
{
|
||||
if (!$this->dataLimit || !$this->auth || $this->auth->isSuperAdmin()) {
|
||||
return [];
|
||||
}
|
||||
$adminIds = [];
|
||||
if ($this->dataLimit == 'parent') {
|
||||
$parentGroups = $this->auth->getAdminChildGroups();
|
||||
if ($parentGroups) {
|
||||
$adminIds = $this->auth->getGroupAdmins($parentGroups);
|
||||
}
|
||||
} elseif (is_numeric($this->dataLimit) && $this->dataLimit > 0) {
|
||||
$adminIds = $this->auth->getGroupAdmins([$this->dataLimit]);
|
||||
return in_array($this->auth->id, $adminIds) ? [] : [$this->auth->id];
|
||||
} elseif ($this->dataLimit == 'allAuth' || $this->dataLimit == 'allAuthAndOthers') {
|
||||
$allAuthGroups = $this->auth->getAllAuthGroups($this->dataLimit);
|
||||
$adminIds = $this->auth->getGroupAdmins($allAuthGroups);
|
||||
}
|
||||
$adminIds[] = $this->auth->id;
|
||||
return array_unique($adminIds);
|
||||
}
|
||||
|
||||
/**
|
||||
* 从别名获取原始的逻辑运算符
|
||||
*/
|
||||
protected function getOperatorByAlias(string $operator): string
|
||||
{
|
||||
$alias = [
|
||||
'ne' => '<>',
|
||||
'eq' => '=',
|
||||
'gt' => '>',
|
||||
'egt' => '>=',
|
||||
'lt' => '<',
|
||||
'elt' => '<=',
|
||||
];
|
||||
return $alias[$operator] ?? $operator;
|
||||
}
|
||||
|
||||
/**
|
||||
* 从 path 解析 action
|
||||
*/
|
||||
protected function getActionFromPath(string $path): string
|
||||
{
|
||||
$parts = explode('/', trim($path, '/'));
|
||||
return $parts[array_key_last($parts)] ?? '';
|
||||
}
|
||||
|
||||
/**
|
||||
* 从 Request 或路由解析控制器路径(如 auth/admin)
|
||||
* 优先从 $request->controller(Webman 路由匹配时设置)解析,否则从 path 解析
|
||||
*/
|
||||
protected function getControllerPath(WebmanRequest $request): string
|
||||
{
|
||||
return get_controller_path($request);
|
||||
}
|
||||
}
|
||||
60
app/common/controller/Frontend.php
Normal file
60
app/common/controller/Frontend.php
Normal file
@@ -0,0 +1,60 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace app\common\controller;
|
||||
|
||||
use app\common\library\Auth;
|
||||
use app\common\library\token\TokenExpirationException;
|
||||
use support\Response;
|
||||
use Webman\Http\Request as WebmanRequest;
|
||||
|
||||
/**
|
||||
* 前台/会员中心控制器基类
|
||||
* 继承 Api,增加会员鉴权
|
||||
*/
|
||||
class Frontend extends Api
|
||||
{
|
||||
protected array $noNeedLogin = [];
|
||||
protected array $noNeedPermission = [];
|
||||
protected ?Auth $auth = null;
|
||||
|
||||
/**
|
||||
* 前台初始化(需在控制器方法开头调用)
|
||||
* @return Response|null 若需直接返回则返回 Response,否则 null
|
||||
*/
|
||||
public function initializeFrontend(WebmanRequest $request): ?Response
|
||||
{
|
||||
$response = $this->initializeApi($request);
|
||||
if ($response !== null) return $response;
|
||||
|
||||
$this->setRequest($request);
|
||||
$path = trim($request->path(), '/');
|
||||
$parts = explode('/', $path);
|
||||
$action = $parts[array_key_last($parts)] ?? '';
|
||||
$needLogin = !action_in_arr($this->noNeedLogin, $action);
|
||||
|
||||
try {
|
||||
$this->auth = Auth::instance(['request' => $request]);
|
||||
$token = get_auth_token(['ba', 'user', 'token'], $request);
|
||||
if ($token) $this->auth->init($token);
|
||||
} catch (TokenExpirationException) {
|
||||
if ($needLogin) return $this->error(__('Token expiration'), [], 409);
|
||||
}
|
||||
|
||||
if ($needLogin) {
|
||||
if (!$this->auth->isLogin()) {
|
||||
return $this->error(__('Please login first'), ['type' => Auth::NEED_LOGIN], Auth::LOGIN_RESPONSE_CODE);
|
||||
}
|
||||
if (!action_in_arr($this->noNeedPermission, $action)) {
|
||||
$routePath = get_controller_path($request) . '/' . $action;
|
||||
if (!$this->auth->check($routePath)) {
|
||||
return $this->error(__('You have no permission'), [], 401);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
event_trigger('frontendInit', $this->auth);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
154
app/common/event/Security.php
Normal file
154
app/common/event/Security.php
Normal file
@@ -0,0 +1,154 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace app\common\event;
|
||||
|
||||
/**
|
||||
* 后台安全事件(数据回收、敏感数据记录)
|
||||
* 迁移自 app/common/event/Security.php,需 Admin、Token、DataRecycle 等模型支持
|
||||
*/
|
||||
class Security
|
||||
{
|
||||
protected array $listenAction = ['edit', 'del'];
|
||||
|
||||
/**
|
||||
* @param mixed $auth Auth 实例(backendInit 传入)
|
||||
*/
|
||||
public function handle(mixed $auth): bool
|
||||
{
|
||||
$request = function_exists('request') ? request() : null;
|
||||
if (!$request) {
|
||||
return true;
|
||||
}
|
||||
|
||||
$action = $this->getActionFromPath($request->path());
|
||||
if (!in_array($action, $this->listenAction)) {
|
||||
return true;
|
||||
}
|
||||
if ($action === 'del' && $request->method() !== 'POST' && $request->method() !== 'DELETE') {
|
||||
return true;
|
||||
}
|
||||
if ($action === 'edit' && $request->method() !== 'POST') {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (!class_exists(\app\admin\model\DataRecycle::class) || !class_exists(\app\admin\model\SensitiveData::class)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if ($action === 'del') {
|
||||
return $this->handleDel($request, $auth);
|
||||
}
|
||||
return $this->handleEdit($request, $auth);
|
||||
}
|
||||
|
||||
protected function getActionFromPath(string $path): string
|
||||
{
|
||||
$parts = explode('/', trim($path, '/'));
|
||||
return $parts[array_key_last($parts)] ?? '';
|
||||
}
|
||||
|
||||
protected function handleDel($request, $auth): bool
|
||||
{
|
||||
$dataIds = $request->post('ids') ?? $request->get('ids');
|
||||
if (!$dataIds) {
|
||||
return true;
|
||||
}
|
||||
try {
|
||||
$controllerPath = get_controller_path($request) ?? '';
|
||||
$recycle = \app\admin\model\DataRecycle::where('status', 1)
|
||||
->where('controller_as', $controllerPath)
|
||||
->find();
|
||||
if (!$recycle) {
|
||||
return true;
|
||||
}
|
||||
$connection = class_exists(\ba\TableManager::class)
|
||||
? \ba\TableManager::getConnection($recycle['connection'])
|
||||
: null;
|
||||
$db = $connection ? \support\think\Db::connect($connection) : \support\think\Db::connect();
|
||||
$recycleData = $db->name($recycle['data_table'])
|
||||
->whereIn($recycle['primary_key'], $dataIds)
|
||||
->select()
|
||||
->toArray();
|
||||
$recycleDataArr = [];
|
||||
$adminId = $auth && $auth->isLogin() ? $auth->id : 0;
|
||||
foreach ($recycleData as $item) {
|
||||
$recycleDataArr[] = [
|
||||
'admin_id' => $adminId,
|
||||
'recycle_id' => $recycle['id'],
|
||||
'data' => json_encode($item, JSON_UNESCAPED_UNICODE),
|
||||
'connection' => $recycle['connection'],
|
||||
'data_table' => $recycle['data_table'],
|
||||
'primary_key' => $recycle['primary_key'],
|
||||
'ip' => $request->getRealIp(),
|
||||
'useragent' => substr($request->header('user-agent', ''), 0, 255),
|
||||
];
|
||||
}
|
||||
if ($recycleDataArr) {
|
||||
$model = new \app\admin\model\DataRecycleLog();
|
||||
$model->saveAll($recycleDataArr);
|
||||
}
|
||||
} catch (\Throwable $e) {
|
||||
\support\Log::warning('[DataSecurity] ' . $e->getMessage());
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
protected function handleEdit($request, $auth): bool
|
||||
{
|
||||
try {
|
||||
$controllerPath = get_controller_path($request) ?? '';
|
||||
$sensitiveData = \app\admin\model\SensitiveData::where('status', 1)
|
||||
->where('controller_as', $controllerPath)
|
||||
->find();
|
||||
if (!$sensitiveData) {
|
||||
return true;
|
||||
}
|
||||
$sensitiveData = $sensitiveData->toArray();
|
||||
$dataId = $request->post($sensitiveData['primary_key']) ?? $request->get($sensitiveData['primary_key']);
|
||||
$connection = class_exists(\ba\TableManager::class)
|
||||
? \ba\TableManager::getConnection($sensitiveData['connection'])
|
||||
: null;
|
||||
$db = $connection ? \support\think\Db::connect($connection) : \support\think\Db::connect();
|
||||
$editData = $db->name($sensitiveData['data_table'])
|
||||
->field(array_keys($sensitiveData['data_fields']))
|
||||
->where($sensitiveData['primary_key'], $dataId)
|
||||
->find();
|
||||
if (!$editData) {
|
||||
return true;
|
||||
}
|
||||
$newData = $request->post();
|
||||
$sensitiveDataLog = [];
|
||||
foreach ($sensitiveData['data_fields'] as $field => $title) {
|
||||
if (isset($editData[$field]) && isset($newData[$field]) && $editData[$field] != $newData[$field]) {
|
||||
$newVal = $newData[$field];
|
||||
if (stripos($field, 'password') !== false && $newVal) {
|
||||
$newVal = '******';
|
||||
}
|
||||
$sensitiveDataLog[] = [
|
||||
'admin_id' => $auth && $auth->isLogin() ? $auth->id : 0,
|
||||
'sensitive_id' => $sensitiveData['id'],
|
||||
'connection' => $sensitiveData['connection'],
|
||||
'data_table' => $sensitiveData['data_table'],
|
||||
'primary_key' => $sensitiveData['primary_key'],
|
||||
'data_field' => $field,
|
||||
'data_comment' => $title,
|
||||
'id_value' => $dataId,
|
||||
'before' => $editData[$field],
|
||||
'after' => $newVal,
|
||||
'ip' => $request->getRealIp(),
|
||||
'useragent' => substr($request->header('user-agent', ''), 0, 255),
|
||||
];
|
||||
}
|
||||
}
|
||||
if ($sensitiveDataLog) {
|
||||
$model = new \app\admin\model\SensitiveDataLog();
|
||||
$model->saveAll($sensitiveDataLog);
|
||||
}
|
||||
} catch (\Throwable $e) {
|
||||
\support\Log::warning('[DataSecurity] ' . $e->getMessage());
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
60
app/common/facade/Token.php
Normal file
60
app/common/facade/Token.php
Normal file
@@ -0,0 +1,60 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace app\common\facade;
|
||||
|
||||
use app\common\library\Token as TokenLibrary;
|
||||
|
||||
/**
|
||||
* Token 门面类(Webman 等价实现,替代 ThinkPHP Facade)
|
||||
* @see TokenLibrary
|
||||
* @method static array get(string $token)
|
||||
* @method static bool set(string $token, string $type, int $userId, ?int $expire = null)
|
||||
* @method static bool check(string $token, string $type, int $userId)
|
||||
* @method static bool delete(string $token)
|
||||
* @method static bool clear(string $type, int $userId)
|
||||
* @method static void tokenExpirationCheck(array $token)
|
||||
*/
|
||||
class Token
|
||||
{
|
||||
private static ?TokenLibrary $instance = null;
|
||||
|
||||
private static function getInstance(): TokenLibrary
|
||||
{
|
||||
if (self::$instance === null) {
|
||||
self::$instance = new TokenLibrary();
|
||||
}
|
||||
return self::$instance;
|
||||
}
|
||||
|
||||
public static function get(string $token): array
|
||||
{
|
||||
return self::getInstance()->get($token);
|
||||
}
|
||||
|
||||
public static function set(string $token, string $type, int $userId, ?int $expire = null): bool
|
||||
{
|
||||
return self::getInstance()->set($token, $type, $userId, $expire);
|
||||
}
|
||||
|
||||
public static function check(string $token, string $type, int $userId): bool
|
||||
{
|
||||
return self::getInstance()->check($token, $type, $userId);
|
||||
}
|
||||
|
||||
public static function delete(string $token): bool
|
||||
{
|
||||
return self::getInstance()->delete($token);
|
||||
}
|
||||
|
||||
public static function clear(string $type, int $userId): bool
|
||||
{
|
||||
return self::getInstance()->clear($type, $userId);
|
||||
}
|
||||
|
||||
public static function tokenExpirationCheck(array $token): void
|
||||
{
|
||||
self::getInstance()->tokenExpirationCheck($token);
|
||||
}
|
||||
}
|
||||
356
app/common/library/Auth.php
Normal file
356
app/common/library/Auth.php
Normal file
@@ -0,0 +1,356 @@
|
||||
<?php
|
||||
|
||||
namespace app\common\library;
|
||||
|
||||
use Throwable;
|
||||
use ba\Random;
|
||||
use support\think\Db;
|
||||
use app\common\model\User;
|
||||
use app\common\facade\Token;
|
||||
use Webman\Http\Request;
|
||||
|
||||
/**
|
||||
* 公共权限类(会员权限类)
|
||||
*/
|
||||
class Auth extends \ba\Auth
|
||||
{
|
||||
public const LOGIN_RESPONSE_CODE = 303;
|
||||
public const NEED_LOGIN = 'need login';
|
||||
public const LOGGED_IN = 'logged in';
|
||||
public const TOKEN_TYPE = 'user';
|
||||
|
||||
protected bool $loginEd = false;
|
||||
protected string $error = '';
|
||||
protected ?User $model = null;
|
||||
protected string $token = '';
|
||||
protected string $refreshToken = '';
|
||||
protected int $keepTime = 86400;
|
||||
protected int $refreshTokenKeepTime = 2592000;
|
||||
|
||||
protected array $allowFields = ['id', 'username', 'nickname', 'email', 'mobile', 'avatar', 'gender', 'birthday', 'money', 'score', 'join_time', 'motto', 'last_login_time', 'last_login_ip'];
|
||||
|
||||
public function __construct(array $config = [])
|
||||
{
|
||||
parent::__construct(array_merge([
|
||||
'auth_group' => 'user_group',
|
||||
'auth_group_access' => '',
|
||||
'auth_rule' => 'user_rule',
|
||||
], $config));
|
||||
|
||||
$this->setKeepTime((int)config('buildadmin.user_token_keep_time', 86400));
|
||||
}
|
||||
|
||||
public function __get($name): mixed
|
||||
{
|
||||
return $this->model?->$name;
|
||||
}
|
||||
|
||||
public static function instance(array $options = []): Auth
|
||||
{
|
||||
$request = $options['request'] ?? (function_exists('request') ? request() : null);
|
||||
unset($options['request']);
|
||||
if ($request && !isset($request->userAuth)) {
|
||||
$request->userAuth = new static($options);
|
||||
}
|
||||
return $request && isset($request->userAuth) ? $request->userAuth : new static($options);
|
||||
}
|
||||
|
||||
public function init($token): bool
|
||||
{
|
||||
$tokenData = Token::get($token);
|
||||
if ($tokenData) {
|
||||
Token::tokenExpirationCheck($tokenData);
|
||||
$userId = $tokenData['user_id'];
|
||||
if ($tokenData['type'] == self::TOKEN_TYPE && $userId > 0) {
|
||||
$this->model = User::where('id', $userId)->find();
|
||||
if (!$this->model) {
|
||||
$this->setError('Account not exist');
|
||||
return false;
|
||||
}
|
||||
if ($this->model->status != 'enable') {
|
||||
$this->setError('Account disabled');
|
||||
return false;
|
||||
}
|
||||
$this->token = $token;
|
||||
$this->loginSuccessful();
|
||||
return true;
|
||||
}
|
||||
}
|
||||
$this->setError('Token login failed');
|
||||
$this->reset();
|
||||
return false;
|
||||
}
|
||||
|
||||
public function register(string $username, string $password = '', string $mobile = '', string $email = '', int $group = 1, array $extend = []): bool
|
||||
{
|
||||
$request = function_exists('request') ? request() : null;
|
||||
$ip = $request ? $request->getRealIp() : '0.0.0.0';
|
||||
|
||||
if ($email && !filter_var($email, FILTER_VALIDATE_EMAIL)) {
|
||||
$this->setError(__('Email'));
|
||||
return false;
|
||||
}
|
||||
if ($username && !preg_match('/^[a-zA-Z][a-zA-Z0-9_]{2,15}$/', $username)) {
|
||||
$this->setError(__('Username'));
|
||||
return false;
|
||||
}
|
||||
if (User::where('email', $email)->find() && $email) {
|
||||
$this->setError(__('Email') . ' ' . __('already exists'));
|
||||
return false;
|
||||
}
|
||||
if (User::where('username', $username)->find()) {
|
||||
$this->setError(__('Username') . ' ' . __('already exists'));
|
||||
return false;
|
||||
}
|
||||
|
||||
$nickname = preg_replace_callback('/1[3-9]\d{9}/', fn($m) => substr($m[0], 0, 3) . '****' . substr($m[0], 7), $username);
|
||||
$time = time();
|
||||
$data = [
|
||||
'group_id' => $group,
|
||||
'nickname' => $nickname,
|
||||
'join_ip' => $ip,
|
||||
'join_time' => $time,
|
||||
'last_login_ip' => $ip,
|
||||
'last_login_time' => $time,
|
||||
'status' => 'enable',
|
||||
];
|
||||
$data = array_merge(compact('username', 'password', 'mobile', 'email'), $data, $extend);
|
||||
|
||||
Db::startTrans();
|
||||
try {
|
||||
$this->model = User::create($data);
|
||||
$this->token = Random::uuid();
|
||||
Token::set($this->token, self::TOKEN_TYPE, $this->model->id, $this->keepTime);
|
||||
Db::commit();
|
||||
|
||||
if ($password) {
|
||||
$this->model->resetPassword($this->model->id, $password);
|
||||
}
|
||||
|
||||
event_trigger('userRegisterSuccess', $this->model);
|
||||
} catch (Throwable $e) {
|
||||
$this->setError($e->getMessage());
|
||||
Db::rollback();
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
public function login(string $username, string $password, bool $keep): bool
|
||||
{
|
||||
$accountType = false;
|
||||
if (preg_match('/^1[3-9]\d{9}$/', $username)) $accountType = 'mobile';
|
||||
elseif (filter_var($username, FILTER_VALIDATE_EMAIL)) $accountType = 'email';
|
||||
elseif (preg_match('/^[a-zA-Z][a-zA-Z0-9_]{2,15}$/', $username)) $accountType = 'username';
|
||||
if (!$accountType) {
|
||||
$this->setError('Account not exist');
|
||||
return false;
|
||||
}
|
||||
|
||||
$this->model = User::where($accountType, $username)->find();
|
||||
if (!$this->model) {
|
||||
$this->setError('Account not exist');
|
||||
return false;
|
||||
}
|
||||
if ($this->model->status == 'disable') {
|
||||
$this->setError('Account disabled');
|
||||
return false;
|
||||
}
|
||||
|
||||
$userLoginRetry = config('buildadmin.user_login_retry');
|
||||
if ($userLoginRetry && $this->model->last_login_time) {
|
||||
$lastLoginTs = is_numeric($this->model->last_login_time) ? (int)$this->model->last_login_time : strtotime($this->model->last_login_time);
|
||||
if ($this->model->login_failure > 0 && $lastLoginTs > 0 && time() - $lastLoginTs >= 86400) {
|
||||
$this->model->login_failure = 0;
|
||||
$this->model->save();
|
||||
$this->model = User::where($accountType, $username)->find();
|
||||
}
|
||||
if ($this->model->login_failure >= $userLoginRetry) {
|
||||
$this->setError('Please try again after 1 day');
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
if (!verify_password($password, $this->model->password, ['salt' => $this->model->salt])) {
|
||||
$this->loginFailed();
|
||||
$this->setError('Password is incorrect');
|
||||
return false;
|
||||
}
|
||||
|
||||
if (config('buildadmin.user_sso')) {
|
||||
Token::clear(self::TOKEN_TYPE, $this->model->id);
|
||||
Token::clear(self::TOKEN_TYPE . '-refresh', $this->model->id);
|
||||
}
|
||||
|
||||
if ($keep) $this->setRefreshToken($this->refreshTokenKeepTime);
|
||||
return $this->loginSuccessful();
|
||||
}
|
||||
|
||||
public function direct(int $userId): bool
|
||||
{
|
||||
$this->model = User::find($userId);
|
||||
if (!$this->model) return false;
|
||||
if (config('buildadmin.user_sso')) {
|
||||
Token::clear(self::TOKEN_TYPE, $this->model->id);
|
||||
Token::clear(self::TOKEN_TYPE . '-refresh', $this->model->id);
|
||||
}
|
||||
return $this->loginSuccessful();
|
||||
}
|
||||
|
||||
public function loginSuccessful(): bool
|
||||
{
|
||||
if (!$this->model) return false;
|
||||
$request = function_exists('request') ? request() : null;
|
||||
$ip = $request ? $request->getRealIp() : '0.0.0.0';
|
||||
if (!$this->token) {
|
||||
$this->token = Random::uuid();
|
||||
Token::set($this->token, self::TOKEN_TYPE, $this->model->id, $this->keepTime);
|
||||
}
|
||||
$this->model->startTrans();
|
||||
try {
|
||||
$this->model->login_failure = 0;
|
||||
$this->model->last_login_time = time();
|
||||
$this->model->last_login_ip = $ip;
|
||||
$this->model->save();
|
||||
$this->loginEd = true;
|
||||
$this->model->commit();
|
||||
} catch (Throwable $e) {
|
||||
$this->model->rollback();
|
||||
if ($this->token) {
|
||||
Token::delete($this->token);
|
||||
$this->token = '';
|
||||
}
|
||||
$this->setError($e->getMessage());
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
public function loginFailed(): bool
|
||||
{
|
||||
if (!$this->model) return false;
|
||||
$request = function_exists('request') ? request() : null;
|
||||
$ip = $request ? $request->getRealIp() : '0.0.0.0';
|
||||
$this->model->startTrans();
|
||||
try {
|
||||
$this->model->login_failure++;
|
||||
$this->model->last_login_time = time();
|
||||
$this->model->last_login_ip = $ip;
|
||||
$this->model->save();
|
||||
$this->model->commit();
|
||||
} catch (Throwable $e) {
|
||||
$this->model->rollback();
|
||||
}
|
||||
return $this->reset();
|
||||
}
|
||||
|
||||
public function logout(): bool
|
||||
{
|
||||
if (!$this->loginEd) {
|
||||
$this->setError('You are not logged in');
|
||||
return false;
|
||||
}
|
||||
return $this->reset();
|
||||
}
|
||||
|
||||
public function isLogin(): bool
|
||||
{
|
||||
return $this->loginEd;
|
||||
}
|
||||
|
||||
public function getUser(): User
|
||||
{
|
||||
return $this->model;
|
||||
}
|
||||
|
||||
public function getToken(): string
|
||||
{
|
||||
return $this->token;
|
||||
}
|
||||
|
||||
public function setRefreshToken(int $keepTime = 0): void
|
||||
{
|
||||
$this->refreshToken = Random::uuid();
|
||||
Token::set($this->refreshToken, self::TOKEN_TYPE . '-refresh', $this->model->id, $keepTime);
|
||||
}
|
||||
|
||||
public function getRefreshToken(): string
|
||||
{
|
||||
return $this->refreshToken;
|
||||
}
|
||||
|
||||
public function getUserInfo(): array
|
||||
{
|
||||
if (!$this->model) return [];
|
||||
$info = $this->model->toArray();
|
||||
$info = array_intersect_key($info, array_flip($this->getAllowFields()));
|
||||
$info['token'] = $this->getToken();
|
||||
$info['refresh_token'] = $this->getRefreshToken();
|
||||
return $info;
|
||||
}
|
||||
|
||||
public function getAllowFields(): array
|
||||
{
|
||||
return $this->allowFields;
|
||||
}
|
||||
|
||||
public function setAllowFields($fields): void
|
||||
{
|
||||
$this->allowFields = $fields;
|
||||
}
|
||||
|
||||
public function setKeepTime(int $keepTime = 0): void
|
||||
{
|
||||
$this->keepTime = $keepTime;
|
||||
}
|
||||
|
||||
public function check(string $name, int $uid = 0, string $relation = 'or', string $mode = 'url'): bool
|
||||
{
|
||||
return parent::check($name, $uid ?: $this->id, $relation, $mode);
|
||||
}
|
||||
|
||||
public function getRuleList(int $uid = 0): array
|
||||
{
|
||||
return parent::getRuleList($uid ?: $this->id);
|
||||
}
|
||||
|
||||
public function getRuleIds(int $uid = 0): array
|
||||
{
|
||||
return parent::getRuleIds($uid ?: $this->id);
|
||||
}
|
||||
|
||||
public function getMenus(int $uid = 0): array
|
||||
{
|
||||
return parent::getMenus($uid ?: $this->id);
|
||||
}
|
||||
|
||||
public function isSuperUser(): bool
|
||||
{
|
||||
return in_array('*', $this->getRuleIds());
|
||||
}
|
||||
|
||||
public function setError(string $error): Auth
|
||||
{
|
||||
$this->error = $error;
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getError(): string
|
||||
{
|
||||
return $this->error ? __($this->error) : '';
|
||||
}
|
||||
|
||||
protected function reset(bool $deleteToken = true): bool
|
||||
{
|
||||
if ($deleteToken && $this->token) {
|
||||
Token::delete($this->token);
|
||||
}
|
||||
$this->token = '';
|
||||
$this->loginEd = false;
|
||||
$this->model = null;
|
||||
$this->refreshToken = '';
|
||||
$this->setError('');
|
||||
$this->setKeepTime((int)config('buildadmin.user_token_keep_time', 86400));
|
||||
return true;
|
||||
}
|
||||
}
|
||||
66
app/common/library/Email.php
Normal file
66
app/common/library/Email.php
Normal file
@@ -0,0 +1,66 @@
|
||||
<?php
|
||||
|
||||
namespace app\common\library;
|
||||
|
||||
use PHPMailer\PHPMailer\PHPMailer;
|
||||
|
||||
/**
|
||||
* 邮件类(Webman 迁移版)
|
||||
*/
|
||||
class Email extends PHPMailer
|
||||
{
|
||||
public bool $configured = false;
|
||||
|
||||
public array $options = [
|
||||
'charset' => 'utf-8',
|
||||
'debug' => true,
|
||||
'lang' => 'zh_cn',
|
||||
];
|
||||
|
||||
public function __construct(array $options = [])
|
||||
{
|
||||
$this->options = array_merge($this->options, $options);
|
||||
|
||||
parent::__construct($this->options['debug']);
|
||||
|
||||
$langSet = function_exists('locale') ? locale() : 'zh_CN';
|
||||
if ($langSet == 'zh-cn' || $langSet == 'zh_CN' || !$langSet) {
|
||||
$langSet = 'zh_cn';
|
||||
}
|
||||
$this->options['lang'] = $this->options['lang'] ?: $langSet;
|
||||
|
||||
$langPath = root_path() . 'vendor' . DIRECTORY_SEPARATOR . 'phpmailer' . DIRECTORY_SEPARATOR . 'phpmailer' . DIRECTORY_SEPARATOR . 'language' . DIRECTORY_SEPARATOR;
|
||||
if (is_dir($langPath)) {
|
||||
$this->setLanguage($this->options['lang'], $langPath);
|
||||
}
|
||||
$this->CharSet = $this->options['charset'];
|
||||
|
||||
$sysMailConfig = get_sys_config('', 'mail');
|
||||
$this->configured = true;
|
||||
if (is_array($sysMailConfig)) {
|
||||
foreach ($sysMailConfig as $item) {
|
||||
if (!$item) {
|
||||
$this->configured = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
$this->configured = false;
|
||||
}
|
||||
|
||||
if ($this->configured) {
|
||||
$this->Host = $sysMailConfig['smtp_server'];
|
||||
$this->SMTPAuth = true;
|
||||
$this->Username = $sysMailConfig['smtp_user'];
|
||||
$this->Password = $sysMailConfig['smtp_pass'];
|
||||
$this->SMTPSecure = ($sysMailConfig['smtp_verification'] ?? '') == 'SSL' ? self::ENCRYPTION_SMTPS : self::ENCRYPTION_STARTTLS;
|
||||
$this->Port = $sysMailConfig['smtp_port'] ?? 465;
|
||||
$this->setFrom($sysMailConfig['smtp_sender_mail'] ?? '', $sysMailConfig['smtp_user'] ?? '');
|
||||
}
|
||||
}
|
||||
|
||||
public function setSubject($subject): void
|
||||
{
|
||||
$this->Subject = "=?utf-8?B?" . base64_encode($subject) . "?=";
|
||||
}
|
||||
}
|
||||
133
app/common/library/Menu.php
Normal file
133
app/common/library/Menu.php
Normal file
@@ -0,0 +1,133 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace app\common\library;
|
||||
|
||||
use Throwable;
|
||||
use app\admin\model\AdminRule;
|
||||
use app\admin\model\UserRule;
|
||||
|
||||
/**
|
||||
* 菜单规则管理类(Webman 迁移版)
|
||||
*/
|
||||
class Menu
|
||||
{
|
||||
/**
|
||||
* @param array $menu
|
||||
* @param int|string $parent 父级规则name或id
|
||||
* @param string $mode 添加模式(规则重复时):cover=覆盖旧菜单,rename=重命名新菜单,ignore=忽略
|
||||
* @param string $position 位置:backend=后台,frontend=前台
|
||||
* @return void
|
||||
* @throws Throwable
|
||||
*/
|
||||
public static function create(array $menu, int|string $parent = 0, string $mode = 'cover', string $position = 'backend'): void
|
||||
{
|
||||
$pid = 0;
|
||||
$model = $position == 'backend' ? new AdminRule() : new UserRule();
|
||||
$parentRule = $model->where((is_numeric($parent) ? 'id' : 'name'), $parent)->find();
|
||||
if ($parentRule) {
|
||||
$pid = $parentRule['id'];
|
||||
}
|
||||
foreach ($menu as $item) {
|
||||
if (!self::requiredAttrCheck($item)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$item['status'] = 1;
|
||||
if (!isset($item['pid'])) {
|
||||
$item['pid'] = $pid;
|
||||
}
|
||||
|
||||
$sameOldMenu = $model->where('name', $item['name'])->find();
|
||||
if ($sameOldMenu) {
|
||||
if ($mode == 'cover') {
|
||||
$sameOldMenu->save($item);
|
||||
} elseif ($mode == 'rename') {
|
||||
$count = $model->where('name', $item['name'])->count();
|
||||
$item['name'] = $item['name'] . '-CONFLICT-' . $count;
|
||||
$item['path'] = $item['path'] . '-CONFLICT-' . $count;
|
||||
$item['title'] = $item['title'] . '-CONFLICT-' . $count;
|
||||
$sameOldMenu = $model->create($item);
|
||||
} elseif ($mode == 'ignore') {
|
||||
$sameOldMenu = $model
|
||||
->where('name', $item['name'])
|
||||
->where('pid', $item['pid'])
|
||||
->find();
|
||||
|
||||
if (!$sameOldMenu) {
|
||||
$sameOldMenu = $model->create($item);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
$sameOldMenu = $model->create($item);
|
||||
}
|
||||
if (!empty($item['children'])) {
|
||||
self::create($item['children'], $sameOldMenu['id'], $mode, $position);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static function delete(string|int $id, bool $recursion = false, string $position = 'backend'): bool
|
||||
{
|
||||
if (!$id) {
|
||||
return true;
|
||||
}
|
||||
$model = $position == 'backend' ? new AdminRule() : new UserRule();
|
||||
$menuRule = $model->where((is_numeric($id) ? 'id' : 'name'), $id)->find();
|
||||
if (!$menuRule) {
|
||||
return true;
|
||||
}
|
||||
|
||||
$children = $model->where('pid', $menuRule['id'])->select()->toArray();
|
||||
if ($recursion && $children) {
|
||||
foreach ($children as $child) {
|
||||
self::delete($child['id'], true, $position);
|
||||
}
|
||||
}
|
||||
|
||||
if (!$children || $recursion) {
|
||||
$menuRule->delete();
|
||||
self::delete($menuRule->pid, false, $position);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
public static function enable(string|int $id, string $position = 'backend'): bool
|
||||
{
|
||||
$model = $position == 'backend' ? new AdminRule() : new UserRule();
|
||||
$menuRule = $model->where((is_numeric($id) ? 'id' : 'name'), $id)->find();
|
||||
if (!$menuRule) {
|
||||
return false;
|
||||
}
|
||||
$menuRule->status = 1;
|
||||
$menuRule->save();
|
||||
return true;
|
||||
}
|
||||
|
||||
public static function disable(string|int $id, string $position = 'backend'): bool
|
||||
{
|
||||
$model = $position == 'backend' ? new AdminRule() : new UserRule();
|
||||
$menuRule = $model->where((is_numeric($id) ? 'id' : 'name'), $id)->find();
|
||||
if (!$menuRule) {
|
||||
return false;
|
||||
}
|
||||
$menuRule->status = 0;
|
||||
$menuRule->save();
|
||||
return true;
|
||||
}
|
||||
|
||||
public static function requiredAttrCheck($menu): bool
|
||||
{
|
||||
$attrs = ['type', 'title', 'name'];
|
||||
foreach ($attrs as $attr) {
|
||||
if (!array_key_exists($attr, $menu)) {
|
||||
return false;
|
||||
}
|
||||
if (!$menu[$attr]) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
146
app/common/library/Token.php
Normal file
146
app/common/library/Token.php
Normal file
@@ -0,0 +1,146 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace app\common\library;
|
||||
|
||||
use InvalidArgumentException;
|
||||
use app\common\library\token\TokenExpirationException;
|
||||
|
||||
/**
|
||||
* Token 管理类
|
||||
*/
|
||||
class Token
|
||||
{
|
||||
public array $instance = [];
|
||||
public ?object $handler = null;
|
||||
protected string $namespace = '\\app\\common\\library\\token\\driver\\';
|
||||
|
||||
public function getDriver(?string $name = null): object
|
||||
{
|
||||
if ($this->handler !== null) {
|
||||
return $this->handler;
|
||||
}
|
||||
$name = $name ?: $this->getDefaultDriver();
|
||||
if ($name === null) {
|
||||
throw new InvalidArgumentException(sprintf('Unable to resolve NULL driver for [%s].', static::class));
|
||||
}
|
||||
return $this->createDriver($name);
|
||||
}
|
||||
|
||||
protected function createDriver(string $name): object
|
||||
{
|
||||
$type = $this->resolveType($name);
|
||||
$params = $this->resolveParams($name);
|
||||
$class = $this->resolveClass($type);
|
||||
|
||||
if (isset($this->instance[$type])) {
|
||||
return $this->instance[$type];
|
||||
}
|
||||
return new $class(...$params);
|
||||
}
|
||||
|
||||
protected function getDefaultDriver(): string
|
||||
{
|
||||
return $this->getConfig('default');
|
||||
}
|
||||
|
||||
protected function getConfig(?string $name = null, mixed $default = null): array|string
|
||||
{
|
||||
$config = config('buildadmin.token', []);
|
||||
if ($name === null) {
|
||||
return $config;
|
||||
}
|
||||
$keys = explode('.', $name);
|
||||
$val = $config;
|
||||
foreach ($keys as $k) {
|
||||
if (!is_array($val) || !array_key_exists($k, $val)) {
|
||||
return $default;
|
||||
}
|
||||
$val = $val[$k];
|
||||
}
|
||||
return $val;
|
||||
}
|
||||
|
||||
protected function resolveParams(string $name): array
|
||||
{
|
||||
$config = $this->getStoreConfig($name);
|
||||
return [$config];
|
||||
}
|
||||
|
||||
protected function resolveClass(string $type): string
|
||||
{
|
||||
$class = str_contains($type, '\\') ? $type : $this->namespace . $this->studly($type);
|
||||
if (class_exists($class)) {
|
||||
return $class;
|
||||
}
|
||||
throw new InvalidArgumentException("Driver [{$type}] not supported.");
|
||||
}
|
||||
|
||||
protected function getStoreConfig(string $store, ?string $name = null, mixed $default = null): array|string
|
||||
{
|
||||
$config = $this->getConfig("stores.{$store}");
|
||||
if ($config === null) {
|
||||
throw new InvalidArgumentException("Store [{$store}] not found.");
|
||||
}
|
||||
if ($name === null) {
|
||||
return $config;
|
||||
}
|
||||
$keys = explode('.', $name);
|
||||
$val = $config;
|
||||
foreach ($keys as $k) {
|
||||
if (!is_array($val) || !array_key_exists($k, $val)) {
|
||||
return $default;
|
||||
}
|
||||
$val = $val[$k];
|
||||
}
|
||||
return $val;
|
||||
}
|
||||
|
||||
protected function resolveType(string $name): string
|
||||
{
|
||||
return $this->getStoreConfig($name, 'type', 'Mysql');
|
||||
}
|
||||
|
||||
private function studly(string $value): string
|
||||
{
|
||||
$value = ucwords(str_replace(['-', '_'], ' ', $value));
|
||||
return str_replace(' ', '', $value);
|
||||
}
|
||||
|
||||
public function set(string $token, string $type, int $userId, ?int $expire = null): bool
|
||||
{
|
||||
return $this->getDriver()->set($token, $type, $userId, $expire);
|
||||
}
|
||||
|
||||
public function get(string $token, bool $expirationException = true): array
|
||||
{
|
||||
return $this->getDriver()->get($token);
|
||||
}
|
||||
|
||||
public function check(string $token, string $type, int $userId, bool $expirationException = true): bool
|
||||
{
|
||||
return $this->getDriver()->check($token, $type, $userId);
|
||||
}
|
||||
|
||||
public function delete(string $token): bool
|
||||
{
|
||||
return $this->getDriver()->delete($token);
|
||||
}
|
||||
|
||||
public function clear(string $type, int $userId): bool
|
||||
{
|
||||
return $this->getDriver()->clear($type, $userId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Token 过期检查
|
||||
* @throws TokenExpirationException
|
||||
*/
|
||||
public function tokenExpirationCheck(array $token): void
|
||||
{
|
||||
if (isset($token['expire_time']) && $token['expire_time'] <= time()) {
|
||||
throw new TokenExpirationException();
|
||||
}
|
||||
}
|
||||
}
|
||||
260
app/common/library/Upload.php
Normal file
260
app/common/library/Upload.php
Normal file
@@ -0,0 +1,260 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace app\common\library;
|
||||
|
||||
use ba\Random;
|
||||
use ba\Filesystem;
|
||||
use app\common\model\Attachment;
|
||||
use app\common\library\upload\Driver;
|
||||
use app\common\library\upload\WebmanUploadedFile;
|
||||
use InvalidArgumentException;
|
||||
use RuntimeException;
|
||||
|
||||
/**
|
||||
* 上传(Webman 迁移版,使用 WebmanUploadedFile)
|
||||
*/
|
||||
class Upload
|
||||
{
|
||||
protected array $config = [];
|
||||
protected ?WebmanUploadedFile $file = null;
|
||||
protected bool $isImage = false;
|
||||
protected array $fileInfo = [];
|
||||
protected array $driver = [
|
||||
'name' => 'local',
|
||||
'handler' => [],
|
||||
'namespace' => '\\app\\common\\library\\upload\\driver\\',
|
||||
];
|
||||
protected string $topic = 'default';
|
||||
|
||||
public function __construct(?WebmanUploadedFile $file = null, array $config = [])
|
||||
{
|
||||
$upload = config('upload', []);
|
||||
$this->config = array_merge($upload, $config);
|
||||
|
||||
if ($file) {
|
||||
$this->setFile($file);
|
||||
}
|
||||
}
|
||||
|
||||
public function setFile(?WebmanUploadedFile $file): self
|
||||
{
|
||||
if (empty($file)) {
|
||||
throw new RuntimeException(__('No files were uploaded'));
|
||||
}
|
||||
|
||||
$suffix = strtolower($file->extension());
|
||||
$suffix = $suffix && preg_match("/^[a-zA-Z0-9]+$/", $suffix) ? $suffix : 'file';
|
||||
$this->fileInfo = [
|
||||
'suffix' => $suffix,
|
||||
'type' => $file->getMime(),
|
||||
'size' => $file->getSize(),
|
||||
'name' => $file->getOriginalName(),
|
||||
'sha1' => $file->sha1(),
|
||||
];
|
||||
$this->file = $file;
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function setDriver(string $driver): self
|
||||
{
|
||||
$this->driver['name'] = $driver;
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getDriver(?string $driver = null, bool $noDriveException = true): Driver|false
|
||||
{
|
||||
$driver = $driver ?? $this->driver['name'];
|
||||
if (!isset($this->driver['handler'][$driver])) {
|
||||
$class = $this->resolveDriverClass($driver);
|
||||
if ($class) {
|
||||
$this->driver['handler'][$driver] = new $class();
|
||||
} elseif ($noDriveException) {
|
||||
throw new InvalidArgumentException(__('Driver %s not supported', [$driver]));
|
||||
}
|
||||
}
|
||||
return $this->driver['handler'][$driver] ?? false;
|
||||
}
|
||||
|
||||
protected function resolveDriverClass(string $driver): string|false
|
||||
{
|
||||
if ($this->driver['namespace'] || str_contains($driver, '\\')) {
|
||||
$class = str_contains($driver, '\\') ? $driver : $this->driver['namespace'] . $this->studly($driver);
|
||||
if (class_exists($class)) {
|
||||
return $class;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
protected function studly(string $value): string
|
||||
{
|
||||
$value = ucwords(str_replace(['-', '_'], ' ', $value));
|
||||
return str_replace(' ', '', $value);
|
||||
}
|
||||
|
||||
public function setTopic(string $topic): self
|
||||
{
|
||||
$this->topic = $topic;
|
||||
return $this;
|
||||
}
|
||||
|
||||
protected function checkIsImage(): bool
|
||||
{
|
||||
if (in_array($this->fileInfo['type'], ['image/gif', 'image/jpg', 'image/jpeg', 'image/bmp', 'image/png', 'image/webp'])
|
||||
|| in_array($this->fileInfo['suffix'], ['gif', 'jpg', 'jpeg', 'bmp', 'png', 'webp'])) {
|
||||
$path = $this->file->getPathname();
|
||||
$imgInfo = is_file($path) ? getimagesize($path) : false;
|
||||
if (!$imgInfo || !isset($imgInfo[0]) || !isset($imgInfo[1])) {
|
||||
throw new RuntimeException(__('The uploaded image file is not a valid image'));
|
||||
}
|
||||
$this->fileInfo['width'] = $imgInfo[0];
|
||||
$this->fileInfo['height'] = $imgInfo[1];
|
||||
$this->isImage = true;
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public function isImage(): bool
|
||||
{
|
||||
return $this->isImage;
|
||||
}
|
||||
|
||||
public function getSuffix(): string
|
||||
{
|
||||
return $this->fileInfo['suffix'] ?? 'file';
|
||||
}
|
||||
|
||||
public function getSaveName(?string $saveName = null, ?string $filename = null, ?string $sha1 = null): string
|
||||
{
|
||||
if ($filename) {
|
||||
$suffix = strtolower(pathinfo($filename, PATHINFO_EXTENSION));
|
||||
$suffix = $suffix && preg_match("/^[a-zA-Z0-9]+$/", $suffix) ? $suffix : 'file';
|
||||
} else {
|
||||
$suffix = $this->fileInfo['suffix'];
|
||||
}
|
||||
$filename = $filename ?? $this->fileInfo['name'];
|
||||
$sha1 = $sha1 ?? $this->fileInfo['sha1'];
|
||||
$replaceArr = [
|
||||
'{topic}' => $this->topic,
|
||||
'{year}' => date('Y'),
|
||||
'{mon}' => date('m'),
|
||||
'{day}' => date('d'),
|
||||
'{hour}' => date('H'),
|
||||
'{min}' => date('i'),
|
||||
'{sec}' => date('s'),
|
||||
'{random}' => Random::build(),
|
||||
'{random32}' => Random::build('alnum', 32),
|
||||
'{fileName}' => $this->getFileNameSubstr($filename, $suffix),
|
||||
'{suffix}' => $suffix,
|
||||
'{.suffix}' => $suffix ? '.' . $suffix : '',
|
||||
'{fileSha1}' => $sha1,
|
||||
];
|
||||
$saveName = $saveName ?? $this->config['save_name'];
|
||||
return Filesystem::fsFit(str_replace(array_keys($replaceArr), array_values($replaceArr), $saveName));
|
||||
}
|
||||
|
||||
public function validates(): void
|
||||
{
|
||||
if (empty($this->file)) {
|
||||
throw new RuntimeException(__('No files have been uploaded or the file size exceeds the upload limit of the server'));
|
||||
}
|
||||
|
||||
$size = Filesystem::fileUnitToByte($this->config['max_size'] ?? '10M');
|
||||
$mime = $this->checkConfig($this->config['allowed_mime_types'] ?? []);
|
||||
$suffix = $this->checkConfig($this->config['allowed_suffixes'] ?? '');
|
||||
|
||||
if ($this->fileInfo['size'] > $size) {
|
||||
throw new RuntimeException(__('The uploaded file is too large (%sMiB), Maximum file size:%sMiB', [
|
||||
round($this->fileInfo['size'] / pow(1024, 2), 2),
|
||||
round($size / pow(1024, 2), 2)
|
||||
]));
|
||||
}
|
||||
|
||||
if ($suffix && !in_array(strtolower($this->fileInfo['suffix']), $suffix)) {
|
||||
throw new RuntimeException(__('The uploaded file format is not allowed'));
|
||||
}
|
||||
|
||||
if ($mime && !in_array(strtolower($this->fileInfo['type']), $mime)) {
|
||||
throw new RuntimeException(__('The uploaded file format is not allowed'));
|
||||
}
|
||||
|
||||
if (!preg_match('/^[a-zA-Z0-9_-]+$/', $this->topic)) {
|
||||
throw new RuntimeException(__('Topic format error'));
|
||||
}
|
||||
|
||||
if (!preg_match('/^[a-zA-Z0-9_-]+$/', $this->driver['name'])) {
|
||||
throw new RuntimeException(__('Driver %s not supported', [$this->driver['name']]));
|
||||
}
|
||||
|
||||
if ($this->checkIsImage()) {
|
||||
$maxW = $this->config['image_max_width'] ?? 0;
|
||||
$maxH = $this->config['image_max_height'] ?? 0;
|
||||
if ($maxW && $this->fileInfo['width'] > $maxW) {
|
||||
throw new RuntimeException(__('The uploaded image file is not a valid image'));
|
||||
}
|
||||
if ($maxH && $this->fileInfo['height'] > $maxH) {
|
||||
throw new RuntimeException(__('The uploaded image file is not a valid image'));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public function upload(?string $saveName = null, int $adminId = 0, int $userId = 0): array
|
||||
{
|
||||
$this->validates();
|
||||
|
||||
$driver = $this->getDriver();
|
||||
if (!$driver) {
|
||||
throw new RuntimeException(__('Driver %s not supported', [$this->driver['name']]));
|
||||
}
|
||||
|
||||
$saveName = $saveName ?: $this->getSaveName();
|
||||
$params = [
|
||||
'topic' => $this->topic,
|
||||
'admin_id' => $adminId,
|
||||
'user_id' => $userId,
|
||||
'url' => $driver->url($saveName, false),
|
||||
'width' => $this->fileInfo['width'] ?? 0,
|
||||
'height' => $this->fileInfo['height'] ?? 0,
|
||||
'name' => $this->getFileNameSubstr($this->fileInfo['name'], $this->fileInfo['suffix'], 100) . '.' . $this->fileInfo['suffix'],
|
||||
'size' => $this->fileInfo['size'],
|
||||
'mimetype' => $this->fileInfo['type'],
|
||||
'storage' => $this->driver['name'],
|
||||
'sha1' => $this->fileInfo['sha1'],
|
||||
];
|
||||
|
||||
$attachment = Attachment::where('sha1', $params['sha1'])
|
||||
->where('topic', $params['topic'])
|
||||
->where('storage', $params['storage'])
|
||||
->find();
|
||||
|
||||
if ($attachment && $driver->exists($attachment->url)) {
|
||||
$attachment->quote++;
|
||||
$attachment->last_upload_time = time();
|
||||
} else {
|
||||
$driver->save($this->file, $saveName);
|
||||
$attachment = new Attachment();
|
||||
$attachment->data(array_filter($params));
|
||||
}
|
||||
$attachment->save();
|
||||
return $attachment->toArray();
|
||||
}
|
||||
|
||||
public function getFileNameSubstr(string $fileName, string $suffix, int $length = 15): string
|
||||
{
|
||||
$pattern = "/[\s:@#?&\/=',+]+/u";
|
||||
$fileName = str_replace(".$suffix", '', $fileName);
|
||||
$fileName = preg_replace($pattern, '', $fileName);
|
||||
return mb_substr(htmlspecialchars(strip_tags($fileName)), 0, $length);
|
||||
}
|
||||
|
||||
protected function checkConfig(mixed $configItem): array
|
||||
{
|
||||
if (is_array($configItem)) {
|
||||
return array_map('strtolower', $configItem);
|
||||
}
|
||||
return $configItem ? explode(',', strtolower((string)$configItem)) : [];
|
||||
}
|
||||
}
|
||||
66
app/common/library/token/Driver.php
Normal file
66
app/common/library/token/Driver.php
Normal file
@@ -0,0 +1,66 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace app\common\library\token;
|
||||
|
||||
/**
|
||||
* Token 驱动抽象类
|
||||
*/
|
||||
abstract class Driver
|
||||
{
|
||||
/**
|
||||
* 具体驱动的句柄 Mysql|Redis
|
||||
* @var object
|
||||
*/
|
||||
protected object $handler;
|
||||
|
||||
/**
|
||||
* @var array 配置数据
|
||||
*/
|
||||
protected array $options = [];
|
||||
|
||||
/**
|
||||
* 设置 token
|
||||
*/
|
||||
abstract public function set(string $token, string $type, int $userId, ?int $expire = null): bool;
|
||||
|
||||
/**
|
||||
* 获取 token 的数据
|
||||
*/
|
||||
abstract public function get(string $token): array;
|
||||
|
||||
/**
|
||||
* 检查 token 是否有效
|
||||
*/
|
||||
abstract public function check(string $token, string $type, int $userId): bool;
|
||||
|
||||
/**
|
||||
* 删除一个 token
|
||||
*/
|
||||
abstract public function delete(string $token): bool;
|
||||
|
||||
/**
|
||||
* 清理一个用户的所有 token
|
||||
*/
|
||||
abstract public function clear(string $type, int $userId): bool;
|
||||
|
||||
/**
|
||||
* 返回句柄对象
|
||||
*/
|
||||
public function handler(): ?object
|
||||
{
|
||||
return $this->handler;
|
||||
}
|
||||
|
||||
protected function getEncryptedToken(string $token): string
|
||||
{
|
||||
$config = config('buildadmin.token');
|
||||
return hash_hmac($config['algo'], $token, $config['key']);
|
||||
}
|
||||
|
||||
protected function getExpiredIn(int $expireTime): int
|
||||
{
|
||||
return $expireTime ? max(0, $expireTime - time()) : 365 * 86400;
|
||||
}
|
||||
}
|
||||
27
app/common/library/token/TokenExpirationException.php
Normal file
27
app/common/library/token/TokenExpirationException.php
Normal file
@@ -0,0 +1,27 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace app\common\library\token;
|
||||
|
||||
use Exception;
|
||||
|
||||
/**
|
||||
* Token 过期异常
|
||||
*/
|
||||
class TokenExpirationException extends Exception
|
||||
{
|
||||
public function __construct(
|
||||
protected string $message = '',
|
||||
protected int $code = 409,
|
||||
protected array $data = [],
|
||||
?\Throwable $previous = null
|
||||
) {
|
||||
parent::__construct($message, $code, $previous);
|
||||
}
|
||||
|
||||
public function getData(): array
|
||||
{
|
||||
return $this->data;
|
||||
}
|
||||
}
|
||||
117
app/common/library/token/driver/Mysql.php
Normal file
117
app/common/library/token/driver/Mysql.php
Normal file
@@ -0,0 +1,117 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace app\common\library\token\driver;
|
||||
|
||||
use support\think\Db;
|
||||
use app\common\library\token\Driver;
|
||||
|
||||
/**
|
||||
* Token Mysql 驱动
|
||||
* @see Driver
|
||||
*/
|
||||
class Mysql extends Driver
|
||||
{
|
||||
protected array $options = [];
|
||||
|
||||
public function __construct(array $options = [])
|
||||
{
|
||||
if (!empty($options)) {
|
||||
$this->options = array_merge($this->options, $options);
|
||||
}
|
||||
|
||||
if (!empty($this->options['name'])) {
|
||||
$this->handler = Db::connect($this->options['name'])->name($this->options['table']);
|
||||
} else {
|
||||
$this->handler = Db::name($this->options['table']);
|
||||
}
|
||||
}
|
||||
|
||||
public function set(string $token, string $type, int $userId, ?int $expire = null): bool
|
||||
{
|
||||
if ($expire === null) {
|
||||
$expire = $this->options['expire'] ?? 2592000;
|
||||
}
|
||||
$expireTime = $expire !== 0 ? time() + $expire : 0;
|
||||
$encryptedToken = $this->getEncryptedToken($token);
|
||||
$this->handler->insert([
|
||||
'token' => $encryptedToken,
|
||||
'type' => $type,
|
||||
'user_id' => $userId,
|
||||
'create_time' => time(),
|
||||
'expire_time' => $expireTime,
|
||||
]);
|
||||
|
||||
// 每隔 48 小时清理一次过期 Token
|
||||
$time = time();
|
||||
$lastCacheCleanupTime = $this->getLastCacheCleanupTime();
|
||||
if (!$lastCacheCleanupTime || $lastCacheCleanupTime < $time - 172800) {
|
||||
$this->setLastCacheCleanupTime($time);
|
||||
$this->handler->where('expire_time', '<', time())->where('expire_time', '>', 0)->delete();
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
public function get(string $token): array
|
||||
{
|
||||
$data = $this->handler->where('token', $this->getEncryptedToken($token))->find();
|
||||
if (!$data) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$data['token'] = $token;
|
||||
$data['expires_in'] = $this->getExpiredIn($data['expire_time'] ?? 0);
|
||||
return $data;
|
||||
}
|
||||
|
||||
public function check(string $token, string $type, int $userId): bool
|
||||
{
|
||||
$data = $this->get($token);
|
||||
if (!$data || ($data['expire_time'] && $data['expire_time'] <= time())) {
|
||||
return false;
|
||||
}
|
||||
return $data['type'] == $type && $data['user_id'] == $userId;
|
||||
}
|
||||
|
||||
public function delete(string $token): bool
|
||||
{
|
||||
$this->handler->where('token', $this->getEncryptedToken($token))->delete();
|
||||
return true;
|
||||
}
|
||||
|
||||
public function clear(string $type, int $userId): bool
|
||||
{
|
||||
$this->handler->where('type', $type)->where('user_id', $userId)->delete();
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* 使用文件存储 last_cache_cleanup_time(兼容无 cache 插件环境)
|
||||
*/
|
||||
private function getLastCacheCleanupTime(): ?int
|
||||
{
|
||||
$path = $this->getCleanupTimePath();
|
||||
if (!is_file($path)) {
|
||||
return null;
|
||||
}
|
||||
$v = file_get_contents($path);
|
||||
return $v !== false && $v !== '' ? (int) $v : null;
|
||||
}
|
||||
|
||||
private function setLastCacheCleanupTime(int $time): void
|
||||
{
|
||||
$path = $this->getCleanupTimePath();
|
||||
$dir = dirname($path);
|
||||
if (!is_dir($dir)) {
|
||||
@mkdir($dir, 0755, true);
|
||||
}
|
||||
@file_put_contents($path, (string) $time);
|
||||
}
|
||||
|
||||
private function getCleanupTimePath(): string
|
||||
{
|
||||
$base = defined('RUNTIME_PATH') ? RUNTIME_PATH : (base_path() . DIRECTORY_SEPARATOR . 'runtime');
|
||||
return $base . DIRECTORY_SEPARATOR . 'cache' . DIRECTORY_SEPARATOR . 'token_last_cleanup.txt';
|
||||
}
|
||||
}
|
||||
35
app/common/library/upload/Driver.php
Normal file
35
app/common/library/upload/Driver.php
Normal file
@@ -0,0 +1,35 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace app\common\library\upload;
|
||||
|
||||
/**
|
||||
* 上传驱动抽象类(Webman 迁移版,支持 WebmanUploadedFile)
|
||||
*/
|
||||
abstract class Driver
|
||||
{
|
||||
protected array $options = [];
|
||||
|
||||
/**
|
||||
* 保存文件
|
||||
* @param WebmanUploadedFile $file
|
||||
* @param string $saveName
|
||||
*/
|
||||
abstract public function save(WebmanUploadedFile $file, string $saveName): bool;
|
||||
|
||||
/**
|
||||
* 删除文件
|
||||
*/
|
||||
abstract public function delete(string $saveName): bool;
|
||||
|
||||
/**
|
||||
* 获取资源 URL 地址
|
||||
*/
|
||||
abstract public function url(string $saveName, string|bool $domain = true, string $default = ''): string;
|
||||
|
||||
/**
|
||||
* 文件是否存在
|
||||
*/
|
||||
abstract public function exists(string $saveName): bool;
|
||||
}
|
||||
66
app/common/library/upload/WebmanUploadedFile.php
Normal file
66
app/common/library/upload/WebmanUploadedFile.php
Normal file
@@ -0,0 +1,66 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace app\common\library\upload;
|
||||
|
||||
use Webman\Http\UploadFile;
|
||||
|
||||
/**
|
||||
* Webman UploadFile 适配器,提供 ThinkPHP UploadedFile 兼容接口
|
||||
*/
|
||||
class WebmanUploadedFile
|
||||
{
|
||||
public function __construct(
|
||||
protected UploadFile $file
|
||||
) {
|
||||
}
|
||||
|
||||
public function extension(): string
|
||||
{
|
||||
$ext = $this->file->getUploadExtension();
|
||||
return $ext && preg_match("/^[a-zA-Z0-9]+$/", $ext) ? $ext : 'file';
|
||||
}
|
||||
|
||||
public function getMime(): string
|
||||
{
|
||||
return $this->file->getUploadMimeType() ?? '';
|
||||
}
|
||||
|
||||
public function getSize(): int
|
||||
{
|
||||
$size = $this->file->getSize();
|
||||
return $size !== false ? $size : 0;
|
||||
}
|
||||
|
||||
public function getOriginalName(): string
|
||||
{
|
||||
return $this->file->getUploadName() ?? '';
|
||||
}
|
||||
|
||||
public function sha1(): string
|
||||
{
|
||||
$path = $this->file->getPathname();
|
||||
return is_file($path) ? hash_file('sha1', $path) : '';
|
||||
}
|
||||
|
||||
public function getPathname(): string
|
||||
{
|
||||
return $this->file->getPathname();
|
||||
}
|
||||
|
||||
/**
|
||||
* 移动文件(兼容 ThinkPHP move($dir, $name) 与 Webman move($fullPath))
|
||||
*/
|
||||
public function move(string $directory, ?string $name = null): self
|
||||
{
|
||||
$destination = rtrim($directory, DIRECTORY_SEPARATOR) . DIRECTORY_SEPARATOR . ($name ?? basename($this->file->getUploadName()));
|
||||
$this->file->move($destination);
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getUploadFile(): UploadFile
|
||||
{
|
||||
return $this->file;
|
||||
}
|
||||
}
|
||||
105
app/common/library/upload/driver/Local.php
Normal file
105
app/common/library/upload/driver/Local.php
Normal file
@@ -0,0 +1,105 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace app\common\library\upload\driver;
|
||||
|
||||
use ba\Filesystem;
|
||||
use app\common\library\upload\Driver;
|
||||
use app\common\library\upload\WebmanUploadedFile;
|
||||
use RuntimeException;
|
||||
|
||||
/**
|
||||
* 上传到本地磁盘的驱动(Webman 迁移版)
|
||||
*/
|
||||
class Local extends Driver
|
||||
{
|
||||
protected array $options = [];
|
||||
|
||||
public function __construct(array $options = [])
|
||||
{
|
||||
$this->options = config('filesystem.disks.public', []);
|
||||
if (!empty($options)) {
|
||||
$this->options = array_merge($this->options, $options);
|
||||
}
|
||||
}
|
||||
|
||||
public function save(WebmanUploadedFile $file, string $saveName): bool
|
||||
{
|
||||
$savePathInfo = pathinfo($saveName);
|
||||
$saveFullPath = $this->getFullPath($saveName);
|
||||
$destination = rtrim($saveFullPath, DIRECTORY_SEPARATOR) . DIRECTORY_SEPARATOR . $savePathInfo['basename'];
|
||||
|
||||
$saveDir = dirname($destination);
|
||||
if (!is_dir($saveDir) && !@mkdir($saveDir, 0755, true)) {
|
||||
throw new RuntimeException(__('Failed to create upload directory'));
|
||||
}
|
||||
|
||||
$uploadFile = $file->getUploadFile();
|
||||
$uploadFile->move($destination);
|
||||
@chmod($destination, 0666 & ~umask());
|
||||
return true;
|
||||
}
|
||||
|
||||
public function delete(string $saveName): bool
|
||||
{
|
||||
$saveFullName = $this->getFullPath($saveName, true);
|
||||
if ($this->exists($saveFullName)) {
|
||||
@unlink($saveFullName);
|
||||
}
|
||||
Filesystem::delEmptyDir(dirname($saveFullName));
|
||||
return true;
|
||||
}
|
||||
|
||||
public function url(string $saveName, string|bool $domain = true, string $default = ''): string
|
||||
{
|
||||
$saveName = $this->clearRootPath($saveName);
|
||||
$saveName = $saveName ? '/' . ltrim(str_replace('\\', '/', $saveName), '/') : '';
|
||||
|
||||
if ($domain === true) {
|
||||
$req = function_exists('request') ? request() : null;
|
||||
$domain = $req && method_exists($req, 'host') ? '//' . $req->host() : '';
|
||||
} elseif ($domain === false) {
|
||||
$domain = '';
|
||||
}
|
||||
|
||||
$saveName = $saveName ?: $default;
|
||||
if (!$saveName) return $domain;
|
||||
|
||||
$regex = "/^((?:[a-z]+:)?\/\/|data:image\/)(.*)/i";
|
||||
if (preg_match('/^http(s)?:\/\//', $saveName) || preg_match($regex, $saveName) || $domain === false) {
|
||||
return $saveName;
|
||||
}
|
||||
return str_replace('\\', '/', $domain . $saveName);
|
||||
}
|
||||
|
||||
public function exists(string $saveName): bool
|
||||
{
|
||||
$saveFullName = $this->getFullPath($saveName, true);
|
||||
return file_exists($saveFullName);
|
||||
}
|
||||
|
||||
public function getFullPath(string $saveName, bool $baseName = false): string
|
||||
{
|
||||
$savePathInfo = pathinfo($saveName);
|
||||
$root = $this->getRootPath();
|
||||
$dirName = $savePathInfo['dirname'] . '/';
|
||||
|
||||
if (str_starts_with($saveName, $root)) {
|
||||
return Filesystem::fsFit($baseName || !isset($savePathInfo['extension']) ? $saveName : $dirName);
|
||||
}
|
||||
|
||||
return Filesystem::fsFit($root . $dirName . ($baseName ? $savePathInfo['basename'] : ''));
|
||||
}
|
||||
|
||||
public function clearRootPath(string $saveName): string
|
||||
{
|
||||
return str_replace($this->getRootPath(), '', Filesystem::fsFit($saveName));
|
||||
}
|
||||
|
||||
public function getRootPath(): string
|
||||
{
|
||||
$root = $this->options['root'] ?? rtrim(base_path(), DIRECTORY_SEPARATOR) . DIRECTORY_SEPARATOR . 'public';
|
||||
return Filesystem::fsFit($root);
|
||||
}
|
||||
}
|
||||
36
app/common/middleware/AdminLog.php
Normal file
36
app/common/middleware/AdminLog.php
Normal file
@@ -0,0 +1,36 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace app\common\middleware;
|
||||
|
||||
use Webman\MiddlewareInterface;
|
||||
use Webman\Http\Request;
|
||||
use Webman\Http\Response;
|
||||
use app\admin\model\AdminLog as AdminLogModel;
|
||||
|
||||
/**
|
||||
* 管理员操作日志中间件(Webman 迁移版)
|
||||
* 仅对 /admin 路由的 POST、DELETE 请求记录日志
|
||||
*/
|
||||
class AdminLog implements MiddlewareInterface
|
||||
{
|
||||
public function process(Request $request, callable $handler): Response
|
||||
{
|
||||
$response = $handler($request);
|
||||
|
||||
$path = trim($request->path(), '/');
|
||||
if (str_starts_with($path, 'admin/') && config('buildadmin.auto_write_admin_log', true)) {
|
||||
$method = $request->method();
|
||||
if ($method === 'POST' || $method === 'DELETE') {
|
||||
try {
|
||||
AdminLogModel::instance($request)->record();
|
||||
} catch (\Throwable $e) {
|
||||
\support\Log::warning('[AdminLog] ' . $e->getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $response;
|
||||
}
|
||||
}
|
||||
94
app/common/middleware/AllowCrossDomain.php
Normal file
94
app/common/middleware/AllowCrossDomain.php
Normal file
@@ -0,0 +1,94 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace app\common\middleware;
|
||||
|
||||
use Webman\MiddlewareInterface;
|
||||
use Webman\Http\Request;
|
||||
use Webman\Http\Response;
|
||||
|
||||
/**
|
||||
* 跨域请求支持(Webman 迁移版)
|
||||
* 安全起见,只支持配置中的域名
|
||||
*/
|
||||
class AllowCrossDomain implements MiddlewareInterface
|
||||
{
|
||||
protected array $header = [
|
||||
'Access-Control-Allow-Credentials' => 'true',
|
||||
'Access-Control-Max-Age' => '1800',
|
||||
'Access-Control-Allow-Methods' => '*',
|
||||
'Access-Control-Allow-Headers' => '*',
|
||||
];
|
||||
|
||||
/**
|
||||
* 返回 CORS 预检(OPTIONS)响应,供路由直接调用(Webman 未匹配路由时不走中间件)
|
||||
*/
|
||||
public static function optionsResponse(Request $request): Response
|
||||
{
|
||||
$header = [
|
||||
'Access-Control-Allow-Credentials' => 'true',
|
||||
'Access-Control-Max-Age' => '1800',
|
||||
'Access-Control-Allow-Methods' => 'GET, POST, PUT, DELETE, PATCH, OPTIONS',
|
||||
'Access-Control-Allow-Headers' => 'Content-Type, Authorization, batoken, ba-user-token, think-lang',
|
||||
];
|
||||
$origin = $request->header('origin');
|
||||
if (is_array($origin)) {
|
||||
$origin = $origin[0] ?? '';
|
||||
}
|
||||
$origin = is_string($origin) ? trim($origin) : '';
|
||||
if ($origin !== '') {
|
||||
$info = parse_url($origin);
|
||||
$host = $info['host'] ?? '';
|
||||
$corsDomain = array_map('trim', explode(',', config('buildadmin.cors_request_domain', '')));
|
||||
$corsDomain[] = $request->host(true);
|
||||
$allowed = in_array('*', $corsDomain)
|
||||
|| in_array($origin, $corsDomain)
|
||||
|| in_array($host, $corsDomain)
|
||||
|| ($host === 'localhost' || $host === '127.0.0.1');
|
||||
if ($allowed) {
|
||||
$header['Access-Control-Allow-Origin'] = $origin;
|
||||
}
|
||||
}
|
||||
return response('', 204, $header);
|
||||
}
|
||||
|
||||
public function process(Request $request, callable $handler): Response
|
||||
{
|
||||
$path = trim($request->path(), '/');
|
||||
if (!str_starts_with($path, 'api/') && !str_starts_with($path, 'admin/')) {
|
||||
return $handler($request);
|
||||
}
|
||||
|
||||
$header = $this->header;
|
||||
|
||||
$origin = $request->header('origin');
|
||||
if (is_array($origin)) {
|
||||
$origin = $origin[0] ?? '';
|
||||
}
|
||||
$origin = is_string($origin) ? trim($origin) : '';
|
||||
|
||||
if ($origin !== '') {
|
||||
$info = parse_url($origin);
|
||||
$host = $info['host'] ?? '';
|
||||
$corsDomain = array_map('trim', explode(',', config('buildadmin.cors_request_domain', '')));
|
||||
$corsDomain[] = $request->host(true);
|
||||
|
||||
$allowed = in_array('*', $corsDomain)
|
||||
|| in_array($origin, $corsDomain)
|
||||
|| in_array($host, $corsDomain)
|
||||
|| ($host === 'localhost' || $host === '127.0.0.1');
|
||||
|
||||
if ($allowed) {
|
||||
$header['Access-Control-Allow-Origin'] = $origin;
|
||||
}
|
||||
}
|
||||
|
||||
if ($request->method() === 'OPTIONS') {
|
||||
return response('', 204, $header);
|
||||
}
|
||||
|
||||
$response = $handler($request);
|
||||
return $response->withHeaders($header);
|
||||
}
|
||||
}
|
||||
74
app/common/middleware/LoadLangPack.php
Normal file
74
app/common/middleware/LoadLangPack.php
Normal file
@@ -0,0 +1,74 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace app\common\middleware;
|
||||
|
||||
use Webman\MiddlewareInterface;
|
||||
use Webman\Http\Request;
|
||||
use Webman\Http\Response;
|
||||
|
||||
/**
|
||||
* 加载控制器语言包中间件(Webman 迁移版,等价 ThinkPHP LoadLangPack)
|
||||
* 根据当前路由加载对应控制器的语言包到 Translator
|
||||
*/
|
||||
class LoadLangPack implements MiddlewareInterface
|
||||
{
|
||||
public function process(Request $request, callable $handler): Response
|
||||
{
|
||||
$path = trim($request->path(), '/');
|
||||
if (str_starts_with($path, 'api/') || str_starts_with($path, 'admin/')) {
|
||||
$this->loadLang($request);
|
||||
}
|
||||
return $handler($request);
|
||||
}
|
||||
|
||||
protected function loadLang(Request $request): void
|
||||
{
|
||||
// 优先从请求头 think-lang 获取前端选择的语言(与前端 axios 发送的 header 对应)
|
||||
$headerLang = $request->header('think-lang');
|
||||
$allowLangList = config('lang.allow_lang_list', ['zh-cn', 'en']);
|
||||
if ($headerLang && in_array(str_replace('_', '-', strtolower($headerLang)), $allowLangList)) {
|
||||
$langSet = str_replace('_', '-', strtolower($headerLang));
|
||||
} else {
|
||||
$langSet = config('lang.default_lang', config('translation.locale', 'zh-cn'));
|
||||
$langSet = str_replace('_', '-', strtolower($langSet));
|
||||
}
|
||||
|
||||
// 设置当前请求的翻译语言,使 __() 和 trans() 使用正确的语言
|
||||
if (function_exists('locale')) {
|
||||
locale($langSet);
|
||||
}
|
||||
|
||||
$path = trim($request->path(), '/');
|
||||
$parts = explode('/', $path);
|
||||
$app = $parts[0] ?? 'api';
|
||||
|
||||
$appLangDir = base_path() . DIRECTORY_SEPARATOR . 'app' . DIRECTORY_SEPARATOR . $app . DIRECTORY_SEPARATOR . 'lang' . DIRECTORY_SEPARATOR;
|
||||
|
||||
if (!class_exists(\support\Translation::class)) {
|
||||
return;
|
||||
}
|
||||
|
||||
$translator = \support\Translation::instance();
|
||||
|
||||
// 1. 加载根级语言包(zh-cn.php / en.php),供 common 翻译使用
|
||||
$rootLangFile = $appLangDir . $langSet . '.php';
|
||||
if (is_file($rootLangFile)) {
|
||||
$translator->addResource('phpfile', $rootLangFile, $langSet, 'messages');
|
||||
}
|
||||
|
||||
// 2. 加载控制器专用语言包(如 zh-cn/auth/group.php),供 get_route_remark 等使用
|
||||
$controllerPath = get_controller_path($request);
|
||||
if ($controllerPath) {
|
||||
$controllerPathForFile = str_replace('.', '/', $controllerPath);
|
||||
$controllerPathForFile = implode('/', array_map(function ($p) {
|
||||
return strtolower(preg_replace('/([a-z])([A-Z])/', '$1_$2', $p));
|
||||
}, explode('/', $controllerPathForFile)));
|
||||
$controllerLangFile = $appLangDir . $langSet . DIRECTORY_SEPARATOR . str_replace('/', DIRECTORY_SEPARATOR, $controllerPathForFile) . '.php';
|
||||
if (is_file($controllerLangFile)) {
|
||||
$translator->addResource('phpfile', $controllerLangFile, $langSet, $controllerPath);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
42
app/common/model/Attachment.php
Normal file
42
app/common/model/Attachment.php
Normal file
@@ -0,0 +1,42 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace app\common\model;
|
||||
|
||||
use support\think\Model;
|
||||
use think\model\relation\BelongsTo;
|
||||
|
||||
class Attachment extends Model
|
||||
{
|
||||
protected string $table = 'attachment';
|
||||
protected string $pk = 'id';
|
||||
protected bool $autoWriteTimestamp = true;
|
||||
protected bool $updateTime = false;
|
||||
|
||||
protected array $append = ['suffix', 'full_url'];
|
||||
|
||||
public function getSuffixAttr($value, $row): string
|
||||
{
|
||||
if ($row['name'] ?? '') {
|
||||
$suffix = strtolower(pathinfo($row['name'], PATHINFO_EXTENSION));
|
||||
return $suffix && preg_match("/^[a-zA-Z0-9]+$/", $suffix) ? $suffix : 'file';
|
||||
}
|
||||
return 'file';
|
||||
}
|
||||
|
||||
public function getFullUrlAttr($value, $row): string
|
||||
{
|
||||
return full_url($row['url'] ?? '');
|
||||
}
|
||||
|
||||
public function admin(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(\app\admin\model\Admin::class, 'admin_id');
|
||||
}
|
||||
|
||||
public function user(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(User::class, 'user_id');
|
||||
}
|
||||
}
|
||||
40
app/common/model/User.php
Normal file
40
app/common/model/User.php
Normal file
@@ -0,0 +1,40 @@
|
||||
<?php
|
||||
|
||||
namespace app\common\model;
|
||||
|
||||
use support\think\Model;
|
||||
|
||||
/**
|
||||
* 会员公共模型
|
||||
*/
|
||||
class User extends Model
|
||||
{
|
||||
protected string $table = 'user';
|
||||
protected string $pk = 'id';
|
||||
protected bool $autoWriteTimestamp = true;
|
||||
|
||||
public function getAvatarAttr($value): string
|
||||
{
|
||||
return full_url($value, false, config('buildadmin.default_avatar'));
|
||||
}
|
||||
|
||||
public function setAvatarAttr($value): string
|
||||
{
|
||||
return $value == full_url('', false, config('buildadmin.default_avatar')) ? '' : $value;
|
||||
}
|
||||
|
||||
public function resetPassword($uid, $newPassword)
|
||||
{
|
||||
return $this->where(['id' => $uid])->update(['password' => hash_password($newPassword), 'salt' => '']);
|
||||
}
|
||||
|
||||
public function getMoneyAttr($value): string
|
||||
{
|
||||
return bcdiv((string)$value, '100', 2);
|
||||
}
|
||||
|
||||
public function setMoneyAttr($value): string
|
||||
{
|
||||
return bcmul((string)$value, '100', 2);
|
||||
}
|
||||
}
|
||||
43
app/common/model/UserMoneyLog.php
Normal file
43
app/common/model/UserMoneyLog.php
Normal file
@@ -0,0 +1,43 @@
|
||||
<?php
|
||||
|
||||
namespace app\common\model;
|
||||
|
||||
use support\think\Model;
|
||||
|
||||
class UserMoneyLog extends Model
|
||||
{
|
||||
protected string $table = 'user_money_log';
|
||||
protected string $pk = 'id';
|
||||
protected bool $autoWriteTimestamp = true;
|
||||
protected bool $updateTime = false;
|
||||
|
||||
public function getMoneyAttr($value): string
|
||||
{
|
||||
return bcdiv((string) $value, '100', 2);
|
||||
}
|
||||
|
||||
public function setMoneyAttr($value): string
|
||||
{
|
||||
return bcmul((string) $value, '100', 2);
|
||||
}
|
||||
|
||||
public function getBeforeAttr($value): string
|
||||
{
|
||||
return bcdiv((string) $value, '100', 2);
|
||||
}
|
||||
|
||||
public function setBeforeAttr($value): string
|
||||
{
|
||||
return bcmul((string) $value, '100', 2);
|
||||
}
|
||||
|
||||
public function getAfterAttr($value): string
|
||||
{
|
||||
return bcdiv((string) $value, '100', 2);
|
||||
}
|
||||
|
||||
public function setAfterAttr($value): string
|
||||
{
|
||||
return bcmul((string) $value, '100', 2);
|
||||
}
|
||||
}
|
||||
13
app/common/model/UserScoreLog.php
Normal file
13
app/common/model/UserScoreLog.php
Normal file
@@ -0,0 +1,13 @@
|
||||
<?php
|
||||
|
||||
namespace app\common\model;
|
||||
|
||||
use support\think\Model;
|
||||
|
||||
class UserScoreLog extends Model
|
||||
{
|
||||
protected string $table = 'user_score_log';
|
||||
protected string $pk = 'id';
|
||||
protected bool $autoWriteTimestamp = true;
|
||||
protected bool $updateTime = false;
|
||||
}
|
||||
42
app/controller/IndexController.php
Normal file
42
app/controller/IndexController.php
Normal file
@@ -0,0 +1,42 @@
|
||||
<?php
|
||||
|
||||
namespace app\controller;
|
||||
|
||||
use support\Request;
|
||||
|
||||
class IndexController
|
||||
{
|
||||
public function index(Request $request)
|
||||
{
|
||||
return <<<EOF
|
||||
<style>
|
||||
* {
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
}
|
||||
iframe {
|
||||
border: none;
|
||||
overflow: scroll;
|
||||
}
|
||||
</style>
|
||||
<iframe
|
||||
src="https://www.workerman.net/wellcome"
|
||||
width="100%"
|
||||
height="100%"
|
||||
allow="clipboard-write"
|
||||
sandbox="allow-scripts allow-same-origin allow-popups allow-downloads"
|
||||
></iframe>
|
||||
EOF;
|
||||
}
|
||||
|
||||
public function view(Request $request)
|
||||
{
|
||||
return view('index/view', ['name' => 'webman']);
|
||||
}
|
||||
|
||||
public function json(Request $request)
|
||||
{
|
||||
return json(['code' => 0, 'msg' => 'ok']);
|
||||
}
|
||||
|
||||
}
|
||||
657
app/functions.php
Normal file
657
app/functions.php
Normal file
@@ -0,0 +1,657 @@
|
||||
<?php
|
||||
/**
|
||||
* BuildAdmin Webman 公共函数
|
||||
*/
|
||||
|
||||
// mb_split 兼容:mbstring 扩展未启用时,Illuminate 的 Str::studly 会报错,用 preg_split 兜底
|
||||
if (!function_exists('mb_split')) {
|
||||
function mb_split(string $pattern, string $string, int $limit = -1): array
|
||||
{
|
||||
$result = @preg_split('#' . $pattern . '#u', $string, $limit);
|
||||
return $result !== false ? $result : [];
|
||||
}
|
||||
}
|
||||
|
||||
use support\Response;
|
||||
|
||||
if (!function_exists('env')) {
|
||||
/**
|
||||
* 获取环境变量(兼容 dot 格式如 database.hostname)
|
||||
*/
|
||||
function env(string $key, mixed $default = null): mixed
|
||||
{
|
||||
$value = $_ENV[$key] ?? getenv($key);
|
||||
if ($value !== false && $value !== null) {
|
||||
return $value;
|
||||
}
|
||||
if (strpos($key, '.') !== false) {
|
||||
$parts = explode('.', $key);
|
||||
$upper = strtoupper(implode('_', $parts));
|
||||
$value = $_ENV[$upper] ?? getenv($upper);
|
||||
if ($value !== false && $value !== null) {
|
||||
return $value;
|
||||
}
|
||||
}
|
||||
return $default;
|
||||
}
|
||||
}
|
||||
|
||||
if (!function_exists('__')) {
|
||||
/**
|
||||
* 语言翻译(BuildAdmin 兼容)
|
||||
*/
|
||||
function __(string $name, array $vars = [], string $lang = ''): mixed
|
||||
{
|
||||
if (is_numeric($name) || !$name) {
|
||||
return $name;
|
||||
}
|
||||
return function_exists('trans') ? trans($name, $vars, null, $lang ?: null) : $name;
|
||||
}
|
||||
}
|
||||
|
||||
use Symfony\Component\HttpFoundation\IpUtils;
|
||||
|
||||
if (!function_exists('get_sys_config')) {
|
||||
/**
|
||||
* 获取系统配置(从数据库或 config)
|
||||
* 需 Config 模型支持,否则从 config 读取
|
||||
*/
|
||||
function get_sys_config(string $name = '', string $group = '', bool $concise = true): mixed
|
||||
{
|
||||
if (class_exists(\app\admin\model\Config::class)) {
|
||||
$configModel = \app\admin\model\Config::class;
|
||||
if ($name) {
|
||||
$config = $configModel::cache($name, null, $configModel::$cacheTag)->where('name', $name)->find();
|
||||
return $config ? $config['value'] : null;
|
||||
}
|
||||
if ($group) {
|
||||
$temp = $configModel::cache('group' . $group, null, $configModel::$cacheTag)->where('group', $group)->select()->toArray();
|
||||
} else {
|
||||
$temp = $configModel::cache('sys_config_all', null, $configModel::$cacheTag)->order('weigh desc')->select()->toArray();
|
||||
}
|
||||
if ($concise) {
|
||||
$config = [];
|
||||
foreach ($temp as $item) {
|
||||
$config[$item['name']] = $item['value'];
|
||||
}
|
||||
return $config;
|
||||
}
|
||||
return $temp;
|
||||
}
|
||||
return config("sys_config.{$name}", null);
|
||||
}
|
||||
}
|
||||
|
||||
if (!function_exists('clear_config_cache')) {
|
||||
/**
|
||||
* 清理配置缓存(Config 写入后调用)
|
||||
*/
|
||||
function clear_config_cache(): void
|
||||
{
|
||||
$cachePath = base_path() . DIRECTORY_SEPARATOR . 'runtime' . DIRECTORY_SEPARATOR . 'cache';
|
||||
if (!is_dir($cachePath)) {
|
||||
return;
|
||||
}
|
||||
$files = new \RecursiveIteratorIterator(
|
||||
new \RecursiveDirectoryIterator($cachePath, \RecursiveDirectoryIterator::SKIP_DOTS),
|
||||
\RecursiveIteratorIterator::CHILD_FIRST
|
||||
);
|
||||
foreach ($files as $file) {
|
||||
if ($file->isFile()) {
|
||||
@unlink($file->getRealPath());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!function_exists('ip_check')) {
|
||||
/**
|
||||
* IP 检查
|
||||
* @param string|null $ip 要检查的 IP,null 时从 request 获取
|
||||
* @param \Webman\Http\Request|null $request
|
||||
* @return Response|null 禁止访问时返回 Response,否则 null
|
||||
*/
|
||||
function ip_check(?string $ip = null, $request = null): ?Response
|
||||
{
|
||||
if ($ip === null && $request) {
|
||||
$ip = $request->getRealIp();
|
||||
}
|
||||
if (!$ip) {
|
||||
return null;
|
||||
}
|
||||
$noAccess = get_sys_config('no_access_ip');
|
||||
$noAccess = !$noAccess ? [] : array_filter(explode("\n", str_replace("\r\n", "\n", (string) $noAccess)));
|
||||
if ($noAccess && IpUtils::checkIp($ip, $noAccess)) {
|
||||
return response(json_encode(['msg' => 'No permission request']), 403, ['Content-Type' => 'application/json']);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
if (!function_exists('get_auth_token')) {
|
||||
/**
|
||||
* 获取鉴权 token
|
||||
* @param array $names 如 ['ba', 'token']
|
||||
* @param \Webman\Http\Request|null $request
|
||||
*/
|
||||
function get_auth_token(array $names = ['ba', 'token'], $request = null): string
|
||||
{
|
||||
$request = $request ?? (function_exists('request') ? request() : null);
|
||||
if (!$request) {
|
||||
return '';
|
||||
}
|
||||
$separators = [
|
||||
'header' => ['', '-'],
|
||||
'param' => ['', '-', '_'],
|
||||
'server' => ['_'],
|
||||
];
|
||||
$tokens = [];
|
||||
foreach ($separators as $source => $sps) {
|
||||
foreach ($sps as $sp) {
|
||||
$key = ($source === 'server' ? 'http_' : '') . implode($sp, $names);
|
||||
if ($source === 'header') {
|
||||
$val = $request->header($key);
|
||||
} elseif ($source === 'param') {
|
||||
$val = $request->get($key) ?? $request->post($key);
|
||||
} else {
|
||||
$val = $_SERVER[$key] ?? null;
|
||||
}
|
||||
if ($val) {
|
||||
$tokens[] = $val;
|
||||
}
|
||||
}
|
||||
}
|
||||
return $tokens[0] ?? '';
|
||||
}
|
||||
}
|
||||
|
||||
if (!function_exists('get_controller_path')) {
|
||||
/**
|
||||
* 从 Request 或路由获取控制器路径(等价于 ThinkPHP controllerPath)
|
||||
* 优先从 $request->controller(Webman 路由匹配时设置)解析,否则从 path 解析
|
||||
* @param \Webman\Http\Request|null $request
|
||||
* @return string|null 如 auth/admin、user/user
|
||||
*/
|
||||
function get_controller_path($request = null): ?string
|
||||
{
|
||||
$request = $request ?? (function_exists('request') ? request() : null);
|
||||
if (!$request) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// 优先从路由匹配的 controller 解析(Webman 在路由匹配后设置)
|
||||
$controller = $request->controller ?? null;
|
||||
if ($controller && is_string($controller)) {
|
||||
foreach (['app\\admin\\controller\\', 'app\\api\\controller\\'] as $prefix) {
|
||||
if (str_starts_with($controller, $prefix)) {
|
||||
$relative = substr($controller, strlen($prefix));
|
||||
$parts = explode('\\', $relative);
|
||||
$path = [];
|
||||
foreach ($parts as $p) {
|
||||
$path[] = strtolower(preg_replace('/([a-z])([A-Z])/', '$1_$2', $p));
|
||||
}
|
||||
return implode('/', $path);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 回退:从 path 解析(如 admin/auth/admin/index -> auth/admin)
|
||||
$path = trim($request->path(), '/');
|
||||
if (!$path) {
|
||||
return null;
|
||||
}
|
||||
$parts = explode('/', $path);
|
||||
if (count($parts) < 2) {
|
||||
return $parts[0] ?? null;
|
||||
}
|
||||
return implode('/', array_slice($parts, 1, -1)) ?: $parts[1];
|
||||
}
|
||||
}
|
||||
|
||||
if (!function_exists('action_in_arr')) {
|
||||
/**
|
||||
* 检测当前方法是否在数组中(用于 noNeedLogin、noNeedPermission)
|
||||
* @param array $arr
|
||||
* @param string|null $action 当前 action,null 时从 request path 解析
|
||||
*/
|
||||
function action_in_arr(array $arr, ?string $action = null): bool
|
||||
{
|
||||
if (!$arr) {
|
||||
return false;
|
||||
}
|
||||
$arr = array_map('strtolower', $arr);
|
||||
if (in_array('*', $arr)) {
|
||||
return true;
|
||||
}
|
||||
if ($action === null && function_exists('request')) {
|
||||
$req = request();
|
||||
$path = trim($req->path(), '/');
|
||||
$parts = explode('/', $path);
|
||||
$action = $parts[array_key_last($parts)] ?? '';
|
||||
}
|
||||
return $action ? in_array(strtolower($action), $arr) : false;
|
||||
}
|
||||
}
|
||||
|
||||
if (!function_exists('event_trigger')) {
|
||||
/**
|
||||
* 触发事件(BuildAdmin 兼容,替代 Event::trigger)
|
||||
* @param string $event
|
||||
* @param mixed ...$args
|
||||
*/
|
||||
function event_trigger(string $event, mixed ...$args): void
|
||||
{
|
||||
$listeners = config("events.listen.{$event}", []);
|
||||
foreach ($listeners as $listener) {
|
||||
try {
|
||||
if (is_string($listener) && class_exists($listener)) {
|
||||
$instance = new $listener();
|
||||
if (method_exists($instance, 'handle')) {
|
||||
$instance->handle(...$args);
|
||||
}
|
||||
} elseif (is_callable($listener)) {
|
||||
$listener(...$args);
|
||||
}
|
||||
} catch (\Throwable $e) {
|
||||
if (class_exists(\support\Log::class)) {
|
||||
\support\Log::warning("[event_trigger] {$event}: " . $e->getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!function_exists('set_timezone')) {
|
||||
/**
|
||||
* 设置时区
|
||||
*/
|
||||
function set_timezone(?string $timezone = null): void
|
||||
{
|
||||
$defaultTimezone = config('app.default_timezone', 'Asia/Shanghai');
|
||||
$timezone = $timezone ?? get_sys_config('time_zone');
|
||||
if ($timezone && $defaultTimezone !== $timezone) {
|
||||
date_default_timezone_set($timezone);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!function_exists('encrypt_password')) {
|
||||
/**
|
||||
* 加密密码(兼容旧版 md5)
|
||||
* @deprecated 使用 hash_password 代替
|
||||
*/
|
||||
function encrypt_password(string $password, string $salt = '', string $encrypt = 'md5'): string
|
||||
{
|
||||
return $encrypt($encrypt($password) . $salt);
|
||||
}
|
||||
}
|
||||
|
||||
if (!function_exists('hash_password')) {
|
||||
/**
|
||||
* 创建密码散列(hash)
|
||||
*/
|
||||
function hash_password(string $password): string
|
||||
{
|
||||
return password_hash($password, PASSWORD_DEFAULT);
|
||||
}
|
||||
}
|
||||
|
||||
if (!function_exists('verify_password')) {
|
||||
/**
|
||||
* 验证密码是否和散列值匹配
|
||||
*/
|
||||
function verify_password(string $password, string $hash, array $extend = []): bool
|
||||
{
|
||||
if (str_starts_with($hash, '$') || password_get_info($hash)['algoName'] !== 'unknown') {
|
||||
return password_verify($password, $hash);
|
||||
}
|
||||
return encrypt_password($password, $extend['salt'] ?? '') === $hash;
|
||||
}
|
||||
}
|
||||
|
||||
if (!function_exists('full_url')) {
|
||||
/**
|
||||
* 获取资源完整 url 地址
|
||||
*/
|
||||
function full_url(string $relativeUrl = '', string|bool $domain = true, string $default = ''): string
|
||||
{
|
||||
$cdnUrl = config('buildadmin.cdn_url');
|
||||
if (!$cdnUrl && function_exists('request')) {
|
||||
$req = request();
|
||||
$cdnUrl = $req ? '//' . $req->host() : '//localhost';
|
||||
} elseif (!$cdnUrl) {
|
||||
$cdnUrl = '//localhost';
|
||||
}
|
||||
|
||||
if ($domain === true) {
|
||||
$domain = $cdnUrl;
|
||||
} elseif ($domain === false) {
|
||||
$domain = '';
|
||||
}
|
||||
|
||||
$relativeUrl = $relativeUrl ?: $default;
|
||||
if (!$relativeUrl) {
|
||||
return $domain;
|
||||
}
|
||||
|
||||
$regex = "/^((?:[a-z]+:)?\/\/|data:image\/)(.*)/i";
|
||||
if (preg_match('/^http(s)?:\/\//', $relativeUrl) || preg_match($regex, $relativeUrl) || $domain === false) {
|
||||
return $relativeUrl;
|
||||
}
|
||||
|
||||
$url = $domain . $relativeUrl;
|
||||
$cdnUrlParams = config('buildadmin.cdn_url_params');
|
||||
if ($domain === $cdnUrl && $cdnUrlParams) {
|
||||
$separator = str_contains($url, '?') ? '&' : '?';
|
||||
$url .= $separator . $cdnUrlParams;
|
||||
}
|
||||
|
||||
return $url;
|
||||
}
|
||||
}
|
||||
|
||||
if (!function_exists('parse_name')) {
|
||||
/**
|
||||
* 命名转换(ThinkPHP 兼容)
|
||||
* @param string $name 名称
|
||||
* @param int $type 0=转小写+下划线 1=驼峰 2=首字母大写驼峰
|
||||
* @param string $delimiter 分隔符
|
||||
*/
|
||||
function parse_name(string $name, int $type = 0, string $delimiter = '_'): string
|
||||
{
|
||||
if ($type === 0) {
|
||||
return strtolower(preg_replace('/([A-Z])/', $delimiter . '$1', lcfirst($name)));
|
||||
}
|
||||
if ($type === 1) {
|
||||
$name = str_replace($delimiter, ' ', $name);
|
||||
return lcfirst(str_replace(' ', '', ucwords($name)));
|
||||
}
|
||||
if ($type === 2) {
|
||||
$name = str_replace($delimiter, ' ', $name);
|
||||
return str_replace(' ', '', ucwords($name));
|
||||
}
|
||||
return $name;
|
||||
}
|
||||
}
|
||||
|
||||
if (!function_exists('root_path')) {
|
||||
/**
|
||||
* 根路径(BuildAdmin 兼容,等价于 base_path)
|
||||
* 无参数时返回带尾部分隔符的路径,确保 root_path() . 'app' 拼接正确
|
||||
* @param string $path 子路径
|
||||
*/
|
||||
function root_path(string $path = ''): string
|
||||
{
|
||||
$base = base_path($path);
|
||||
if ($path === '' && $base !== '') {
|
||||
return rtrim($base, DIRECTORY_SEPARATOR . '/') . DIRECTORY_SEPARATOR;
|
||||
}
|
||||
return $base;
|
||||
}
|
||||
}
|
||||
|
||||
if (!function_exists('app_path')) {
|
||||
/**
|
||||
* 应用目录路径(Webman 兼容,用于 CRUD Helper 等)
|
||||
* @param string $path 子路径
|
||||
*/
|
||||
function app_path(string $path = ''): string
|
||||
{
|
||||
$base = rtrim(base_path(), DIRECTORY_SEPARATOR) . DIRECTORY_SEPARATOR . 'app';
|
||||
return $path ? $base . DIRECTORY_SEPARATOR . ltrim(str_replace('/', DIRECTORY_SEPARATOR, $path), DIRECTORY_SEPARATOR) : $base;
|
||||
}
|
||||
}
|
||||
|
||||
if (!function_exists('get_controller_list')) {
|
||||
/**
|
||||
* 获取控制器文件列表(递归)
|
||||
* @param string $app 应用名,默认 admin
|
||||
*/
|
||||
function get_controller_list(string $app = 'admin'): array
|
||||
{
|
||||
$controllerDir = root_path() . 'app' . DIRECTORY_SEPARATOR . $app . DIRECTORY_SEPARATOR . 'controller' . DIRECTORY_SEPARATOR;
|
||||
return (class_exists(\ba\Filesystem::class) && is_dir($controllerDir))
|
||||
? \ba\Filesystem::getDirFiles($controllerDir)
|
||||
: [];
|
||||
}
|
||||
}
|
||||
|
||||
if (!function_exists('get_route_remark')) {
|
||||
/**
|
||||
* 获取当前路由后台菜单规则的备注信息
|
||||
* 使用控制器 domain 翻译,以支持不同控制器对同一 key(如 Remark lang)的不同翻译
|
||||
*/
|
||||
function get_route_remark(): string
|
||||
{
|
||||
$controllerPath = get_controller_path() ?? '';
|
||||
$actionName = '';
|
||||
if (function_exists('request')) {
|
||||
$req = request();
|
||||
if ($req) {
|
||||
$path = trim($req->path(), '/');
|
||||
$parts = explode('/', $path);
|
||||
$actionName = $parts[array_key_last($parts)] ?? '';
|
||||
}
|
||||
}
|
||||
$path = str_replace('.', '/', $controllerPath);
|
||||
$names = [$path];
|
||||
if ($actionName) {
|
||||
$names[] = $path . '/' . $actionName;
|
||||
}
|
||||
$remark = \support\think\Db::name('admin_rule')
|
||||
->where('name', 'in', $names)
|
||||
->value('remark');
|
||||
$remarkStr = (string) ($remark ?? '');
|
||||
if (!$remarkStr) {
|
||||
return '';
|
||||
}
|
||||
return function_exists('trans') ? trans($remarkStr, [], $controllerPath ?: null) : $remarkStr;
|
||||
}
|
||||
}
|
||||
|
||||
if (!function_exists('keys_to_camel_case')) {
|
||||
function keys_to_camel_case(array $array, array $keys = []): array
|
||||
{
|
||||
$result = [];
|
||||
foreach ($array as $key => $value) {
|
||||
$camelCaseKey = ($keys && in_array($key, $keys)) ? parse_name($key, 1, '_') : $key;
|
||||
if (is_array($value)) {
|
||||
$result[$camelCaseKey] = keys_to_camel_case($value);
|
||||
} else {
|
||||
$result[$camelCaseKey] = $value;
|
||||
}
|
||||
}
|
||||
return $result;
|
||||
}
|
||||
}
|
||||
|
||||
if (!function_exists('get_upload_config')) {
|
||||
function get_upload_config($request = null): array
|
||||
{
|
||||
event_trigger('uploadConfigInit', null);
|
||||
$uploadConfig = config('upload', []);
|
||||
$uploadConfig['max_size'] = \ba\Filesystem::fileUnitToByte($uploadConfig['max_size'] ?? '10M');
|
||||
$request = $request ?? (function_exists('request') ? request() : null);
|
||||
$upload = $request && isset($request->upload) ? $request->upload : null;
|
||||
if (!$upload) {
|
||||
$uploadConfig['mode'] = 'local';
|
||||
return $uploadConfig;
|
||||
}
|
||||
unset($upload['cdn']);
|
||||
return array_merge($upload, $uploadConfig);
|
||||
}
|
||||
}
|
||||
|
||||
if (!function_exists('filter')) {
|
||||
function filter(string $string): string
|
||||
{
|
||||
$string = trim($string);
|
||||
$string = strip_tags($string);
|
||||
return htmlspecialchars($string, ENT_QUOTES | ENT_SUBSTITUTE | ENT_HTML401, 'UTF-8');
|
||||
}
|
||||
}
|
||||
|
||||
if (!function_exists('clean_xss')) {
|
||||
function clean_xss(string $string): string
|
||||
{
|
||||
if (class_exists(\voku\helper\AntiXSS::class)) {
|
||||
$antiXss = new \voku\helper\AntiXSS();
|
||||
$antiXss->removeEvilAttributes(['style']);
|
||||
$antiXss->setReplacement('cleanXss');
|
||||
return $antiXss->xss_clean($string);
|
||||
}
|
||||
return strip_tags($string);
|
||||
}
|
||||
}
|
||||
|
||||
if (!function_exists('htmlspecialchars_decode_improve')) {
|
||||
function htmlspecialchars_decode_improve(string $string, int $flags = ENT_QUOTES | ENT_SUBSTITUTE | ENT_HTML401): string
|
||||
{
|
||||
return htmlspecialchars_decode($string, $flags);
|
||||
}
|
||||
}
|
||||
|
||||
if (!function_exists('get_ba_client')) {
|
||||
/**
|
||||
* 获取请求 BuildAdmin 开源社区的 Guzzle Client(用于云端 CRUD 历史等)
|
||||
*/
|
||||
function get_ba_client(): \GuzzleHttp\Client
|
||||
{
|
||||
return new \GuzzleHttp\Client([
|
||||
'base_uri' => config('buildadmin.api_url', 'https://api.buildadmin.com'),
|
||||
'timeout' => 30,
|
||||
'connect_timeout' => 30,
|
||||
'verify' => false,
|
||||
'http_errors' => false,
|
||||
'headers' => [
|
||||
'X-REQUESTED-WITH' => 'XMLHttpRequest',
|
||||
'User-Agent' => 'BuildAdminClient',
|
||||
]
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
if (!function_exists('str_attr_to_array')) {
|
||||
function str_attr_to_array(string $attr): array
|
||||
{
|
||||
if (!$attr) return [];
|
||||
$attr = explode("\n", trim(str_replace("\r\n", "\n", $attr)));
|
||||
$attrTemp = [];
|
||||
foreach ($attr as $item) {
|
||||
$item = explode('=', $item);
|
||||
if (isset($item[0]) && isset($item[1])) {
|
||||
$attrVal = $item[1];
|
||||
if ($item[1] === 'false' || $item[1] === 'true') {
|
||||
$attrVal = !($item[1] === 'false');
|
||||
} elseif (is_numeric($item[1])) {
|
||||
$attrVal = (float)$item[1];
|
||||
}
|
||||
if (strpos($item[0], '.') !== false) {
|
||||
$attrKey = explode('.', $item[0]);
|
||||
if (isset($attrKey[0]) && isset($attrKey[1])) {
|
||||
$attrTemp[$attrKey[0]][$attrKey[1]] = $attrVal;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
$attrTemp[$item[0]] = $attrVal;
|
||||
}
|
||||
}
|
||||
return $attrTemp;
|
||||
}
|
||||
}
|
||||
|
||||
if (!function_exists('hsv2rgb')) {
|
||||
function hsv2rgb($h, $s, $v): array
|
||||
{
|
||||
$r = $g = $b = 0;
|
||||
$i = floor($h * 6);
|
||||
$f = $h * 6 - $i;
|
||||
$p = $v * (1 - $s);
|
||||
$q = $v * (1 - $f * $s);
|
||||
$t = $v * (1 - (1 - $f) * $s);
|
||||
switch ($i % 6) {
|
||||
case 0: $r = $v; $g = $t; $b = $p; break;
|
||||
case 1: $r = $q; $g = $v; $b = $p; break;
|
||||
case 2: $r = $p; $g = $v; $b = $t; break;
|
||||
case 3: $r = $p; $g = $q; $b = $v; break;
|
||||
case 4: $r = $t; $g = $p; $b = $v; break;
|
||||
case 5: $r = $v; $g = $p; $b = $q; break;
|
||||
}
|
||||
return [floor($r * 255), floor($g * 255), floor($b * 255)];
|
||||
}
|
||||
}
|
||||
|
||||
if (!function_exists('build_suffix_svg')) {
|
||||
function build_suffix_svg(string $suffix = 'file', ?string $background = null): string
|
||||
{
|
||||
$suffix = mb_substr(strtoupper($suffix), 0, 4);
|
||||
$total = unpack('L', hash('adler32', $suffix, true))[1];
|
||||
$hue = $total % 360;
|
||||
[$r, $g, $b] = hsv2rgb($hue / 360, 0.3, 0.9);
|
||||
$background = $background ?: "rgb($r,$g,$b)";
|
||||
return '<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" viewBox="0 0 512 512" style="enable-background:new 0 0 512 512;" xml:space="preserve">
|
||||
<path style="fill:#E2E5E7;" d="M128,0c-17.6,0-32,14.4-32,32v448c0,17.6,14.4,32,32,32h320c17.6,0,32-14.4,32-32V128L352,0H128z"/>
|
||||
<path style="fill:#B0B7BD;" d="M384,128h96L352,0v96C352,113.6,366.4,128,384,128z"/>
|
||||
<polygon style="fill:#CAD1D8;" points="480,224 384,128 480,128 "/>
|
||||
<path style="fill:' . $background . ';" d="M416,416c0,8.8-7.2,16-16,16H48c-8.8,0-16-7.2-16-16V256c0-8.8,7.2-16,16-16h352c8.8,0,16,7.2,16,16 V416z"/>
|
||||
<path style="fill:#CAD1D8;" d="M400,432H96v16h304c8.8,0,16-7.2,16-16v-16C416,424.8,408.8,432,400,432z"/>
|
||||
<g><text><tspan x="220" y="380" font-size="124" font-family="Verdana, Helvetica, Arial, sans-serif" fill="white" text-anchor="middle">' . $suffix . '</tspan></text></g>
|
||||
</svg>';
|
||||
}
|
||||
}
|
||||
|
||||
if (!function_exists('get_account_verification_type')) {
|
||||
/**
|
||||
* 获取可用的账户验证方式
|
||||
* @return string[] email=电子邮件,mobile=手机短信验证
|
||||
*/
|
||||
function get_account_verification_type(): array
|
||||
{
|
||||
$types = [];
|
||||
$sysMailConfig = get_sys_config('', 'mail');
|
||||
$configured = true;
|
||||
if (is_array($sysMailConfig)) {
|
||||
foreach ($sysMailConfig as $item) {
|
||||
if (!$item) {
|
||||
$configured = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
$configured = false;
|
||||
}
|
||||
if ($configured) {
|
||||
$types[] = 'email';
|
||||
}
|
||||
if (class_exists(\app\admin\library\module\Server::class)) {
|
||||
$sms = \app\admin\library\module\Server::getIni(\ba\Filesystem::fsFit(root_path() . 'modules/sms/'));
|
||||
if ($sms && ($sms['state'] ?? 0) == 1) {
|
||||
$types[] = 'mobile';
|
||||
}
|
||||
}
|
||||
return $types;
|
||||
}
|
||||
}
|
||||
|
||||
if (!function_exists('get_area')) {
|
||||
function get_area($request = null): array
|
||||
{
|
||||
$request = $request ?? (function_exists('request') ? request() : null);
|
||||
$province = $request ? $request->get('province', '') : '';
|
||||
$city = $request ? $request->get('city', '') : '';
|
||||
$where = ['pid' => 0, 'level' => 1];
|
||||
if ($province !== '') {
|
||||
$where['pid'] = $province;
|
||||
$where['level'] = 2;
|
||||
if ($city !== '') {
|
||||
$where['pid'] = $city;
|
||||
$where['level'] = 3;
|
||||
}
|
||||
}
|
||||
return \support\think\Db::name('area')
|
||||
->where($where)
|
||||
->field('id as value,name as label')
|
||||
->select()
|
||||
->toArray();
|
||||
}
|
||||
}
|
||||
42
app/middleware/StaticFile.php
Normal file
42
app/middleware/StaticFile.php
Normal file
@@ -0,0 +1,42 @@
|
||||
<?php
|
||||
/**
|
||||
* This file is part of webman.
|
||||
*
|
||||
* Licensed under The MIT License
|
||||
* For full copyright and license information, please see the MIT-LICENSE.txt
|
||||
* Redistributions of files must retain the above copyright notice.
|
||||
*
|
||||
* @author walkor<walkor@workerman.net>
|
||||
* @copyright walkor<walkor@workerman.net>
|
||||
* @link http://www.workerman.net/
|
||||
* @license http://www.opensource.org/licenses/mit-license.php MIT License
|
||||
*/
|
||||
|
||||
namespace app\middleware;
|
||||
|
||||
use Webman\MiddlewareInterface;
|
||||
use Webman\Http\Response;
|
||||
use Webman\Http\Request;
|
||||
|
||||
/**
|
||||
* Class StaticFile
|
||||
* @package app\middleware
|
||||
*/
|
||||
class StaticFile implements MiddlewareInterface
|
||||
{
|
||||
public function process(Request $request, callable $handler): Response
|
||||
{
|
||||
// Access to files beginning with. Is prohibited
|
||||
if (strpos($request->path(), '/.') !== false) {
|
||||
return response('<h1>403 forbidden</h1>', 403);
|
||||
}
|
||||
/** @var Response $response */
|
||||
$response = $handler($request);
|
||||
// Add cross domain HTTP header
|
||||
/*$response->withHeaders([
|
||||
'Access-Control-Allow-Origin' => '*',
|
||||
'Access-Control-Allow-Credentials' => 'true',
|
||||
]);*/
|
||||
return $response;
|
||||
}
|
||||
}
|
||||
29
app/model/Test.php
Normal file
29
app/model/Test.php
Normal file
@@ -0,0 +1,29 @@
|
||||
<?php
|
||||
|
||||
namespace app\model;
|
||||
|
||||
use support\Model;
|
||||
|
||||
class Test extends Model
|
||||
{
|
||||
/**
|
||||
* The table associated with the model.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $table = 'test';
|
||||
|
||||
/**
|
||||
* The primary key associated with the table.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $primaryKey = 'id';
|
||||
|
||||
/**
|
||||
* Indicates if the model should be timestamped.
|
||||
*
|
||||
* @var bool
|
||||
*/
|
||||
public $timestamps = false;
|
||||
}
|
||||
39
app/process/Http.php
Normal file
39
app/process/Http.php
Normal file
@@ -0,0 +1,39 @@
|
||||
<?php
|
||||
|
||||
namespace app\process;
|
||||
|
||||
use Webman\App;
|
||||
use Webman\Http\Response;
|
||||
|
||||
class Http extends App
|
||||
{
|
||||
/**
|
||||
* 在父类处理前拦截 OPTIONS 预检,直接返回 CORS 头(避免预检未命中路由时无 CORS)
|
||||
*/
|
||||
public function onMessage($connection, $request): void
|
||||
{
|
||||
$method = $request->method();
|
||||
if (is_string($method) && strtoupper($method) === 'OPTIONS') {
|
||||
$path = $request->path();
|
||||
$path = is_string($path) ? trim($path, '/') : '';
|
||||
$isApiOrAdmin = $path !== '' && (str_starts_with($path, 'api') || str_starts_with($path, 'admin'));
|
||||
if ($isApiOrAdmin) {
|
||||
$origin = $request->header('origin');
|
||||
$origin = is_array($origin) ? ($origin[0] ?? '') : (is_string($origin) ? trim($origin) : '');
|
||||
if ($origin === '') {
|
||||
$origin = '*';
|
||||
}
|
||||
$headers = [
|
||||
'Access-Control-Allow-Origin' => $origin,
|
||||
'Access-Control-Allow-Credentials' => 'true',
|
||||
'Access-Control-Max-Age' => '1800',
|
||||
'Access-Control-Allow-Methods' => 'GET, POST, PUT, DELETE, PATCH, OPTIONS',
|
||||
'Access-Control-Allow-Headers' => 'Content-Type, Authorization, batoken, ba-user-token, think-lang',
|
||||
];
|
||||
$connection->send(new Response(204, $headers, ''));
|
||||
return;
|
||||
}
|
||||
}
|
||||
parent::onMessage($connection, $request);
|
||||
}
|
||||
}
|
||||
305
app/process/Monitor.php
Normal file
305
app/process/Monitor.php
Normal file
@@ -0,0 +1,305 @@
|
||||
<?php
|
||||
/**
|
||||
* This file is part of webman.
|
||||
*
|
||||
* Licensed under The MIT License
|
||||
* For full copyright and license information, please see the MIT-LICENSE.txt
|
||||
* Redistributions of files must retain the above copyright notice.
|
||||
*
|
||||
* @author walkor<walkor@workerman.net>
|
||||
* @copyright walkor<walkor@workerman.net>
|
||||
* @link http://www.workerman.net/
|
||||
* @license http://www.opensource.org/licenses/mit-license.php MIT License
|
||||
*/
|
||||
|
||||
namespace app\process;
|
||||
|
||||
use FilesystemIterator;
|
||||
use RecursiveDirectoryIterator;
|
||||
use RecursiveIteratorIterator;
|
||||
use SplFileInfo;
|
||||
use Workerman\Timer;
|
||||
use Workerman\Worker;
|
||||
|
||||
/**
|
||||
* Class FileMonitor
|
||||
* @package process
|
||||
*/
|
||||
class Monitor
|
||||
{
|
||||
/**
|
||||
* @var array
|
||||
*/
|
||||
protected array $paths = [];
|
||||
|
||||
/**
|
||||
* @var array
|
||||
*/
|
||||
protected array $extensions = [];
|
||||
|
||||
/**
|
||||
* @var array
|
||||
*/
|
||||
protected array $loadedFiles = [];
|
||||
|
||||
/**
|
||||
* @var int
|
||||
*/
|
||||
protected int $ppid = 0;
|
||||
|
||||
/**
|
||||
* Pause monitor
|
||||
* @return void
|
||||
*/
|
||||
public static function pause(): void
|
||||
{
|
||||
file_put_contents(static::lockFile(), time());
|
||||
}
|
||||
|
||||
/**
|
||||
* Resume monitor
|
||||
* @return void
|
||||
*/
|
||||
public static function resume(): void
|
||||
{
|
||||
clearstatcache();
|
||||
if (is_file(static::lockFile())) {
|
||||
unlink(static::lockFile());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether monitor is paused
|
||||
* @return bool
|
||||
*/
|
||||
public static function isPaused(): bool
|
||||
{
|
||||
clearstatcache();
|
||||
return file_exists(static::lockFile());
|
||||
}
|
||||
|
||||
/**
|
||||
* Lock file
|
||||
* @return string
|
||||
*/
|
||||
protected static function lockFile(): string
|
||||
{
|
||||
return runtime_path('monitor.lock');
|
||||
}
|
||||
|
||||
/**
|
||||
* FileMonitor constructor.
|
||||
* @param $monitorDir
|
||||
* @param $monitorExtensions
|
||||
* @param array $options
|
||||
*/
|
||||
public function __construct($monitorDir, $monitorExtensions, array $options = [])
|
||||
{
|
||||
$this->ppid = function_exists('posix_getppid') ? posix_getppid() : 0;
|
||||
static::resume();
|
||||
$this->paths = (array)$monitorDir;
|
||||
$this->extensions = $monitorExtensions;
|
||||
foreach (get_included_files() as $index => $file) {
|
||||
$this->loadedFiles[$file] = $index;
|
||||
if (strpos($file, 'webman-framework/src/support/App.php')) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!Worker::getAllWorkers()) {
|
||||
return;
|
||||
}
|
||||
$disableFunctions = explode(',', ini_get('disable_functions'));
|
||||
if (in_array('exec', $disableFunctions, true)) {
|
||||
echo "\nMonitor file change turned off because exec() has been disabled by disable_functions setting in " . PHP_CONFIG_FILE_PATH . "/php.ini\n";
|
||||
} else {
|
||||
if ($options['enable_file_monitor'] ?? true) {
|
||||
Timer::add(1, function () {
|
||||
$this->checkAllFilesChange();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
$memoryLimit = $this->getMemoryLimit($options['memory_limit'] ?? null);
|
||||
if ($memoryLimit && ($options['enable_memory_monitor'] ?? true)) {
|
||||
Timer::add(60, [$this, 'checkMemory'], [$memoryLimit]);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param $monitorDir
|
||||
* @return bool
|
||||
*/
|
||||
public function checkFilesChange($monitorDir): bool
|
||||
{
|
||||
static $lastMtime, $tooManyFilesCheck;
|
||||
if (!$lastMtime) {
|
||||
$lastMtime = time();
|
||||
}
|
||||
clearstatcache();
|
||||
if (!is_dir($monitorDir)) {
|
||||
if (!is_file($monitorDir)) {
|
||||
return false;
|
||||
}
|
||||
$iterator = [new SplFileInfo($monitorDir)];
|
||||
} else {
|
||||
// recursive traversal directory
|
||||
$dirIterator = new RecursiveDirectoryIterator($monitorDir, FilesystemIterator::SKIP_DOTS | FilesystemIterator::FOLLOW_SYMLINKS);
|
||||
$iterator = new RecursiveIteratorIterator($dirIterator);
|
||||
}
|
||||
$count = 0;
|
||||
foreach ($iterator as $file) {
|
||||
$count ++;
|
||||
/** @var SplFileInfo $file */
|
||||
if (is_dir($file->getRealPath())) {
|
||||
continue;
|
||||
}
|
||||
// check mtime
|
||||
if (in_array($file->getExtension(), $this->extensions, true) && $lastMtime < $file->getMTime()) {
|
||||
$lastMtime = $file->getMTime();
|
||||
if (DIRECTORY_SEPARATOR === '/' && isset($this->loadedFiles[$file->getRealPath()])) {
|
||||
echo "$file updated but cannot be reloaded because only auto-loaded files support reload.\n";
|
||||
continue;
|
||||
}
|
||||
$var = 0;
|
||||
exec('"'.PHP_BINARY . '" -l ' . $file, $out, $var);
|
||||
if ($var) {
|
||||
continue;
|
||||
}
|
||||
// send SIGUSR1 signal to master process for reload
|
||||
if (DIRECTORY_SEPARATOR === '/') {
|
||||
if ($masterPid = $this->getMasterPid()) {
|
||||
echo $file . " updated and reload\n";
|
||||
posix_kill($masterPid, SIGUSR1);
|
||||
} else {
|
||||
echo "Master process has gone away and can not reload\n";
|
||||
}
|
||||
return true;
|
||||
}
|
||||
echo $file . " updated and reload\n";
|
||||
return true;
|
||||
}
|
||||
}
|
||||
if (!$tooManyFilesCheck && $count > 1000) {
|
||||
echo "Monitor: There are too many files ($count files) in $monitorDir which makes file monitoring very slow\n";
|
||||
$tooManyFilesCheck = 1;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return int
|
||||
*/
|
||||
public function getMasterPid(): int
|
||||
{
|
||||
if ($this->ppid === 0) {
|
||||
return 0;
|
||||
}
|
||||
if (function_exists('posix_kill') && !posix_kill($this->ppid, 0)) {
|
||||
echo "Master process has gone away\n";
|
||||
return $this->ppid = 0;
|
||||
}
|
||||
if (PHP_OS_FAMILY !== 'Linux') {
|
||||
return $this->ppid;
|
||||
}
|
||||
$cmdline = "/proc/$this->ppid/cmdline";
|
||||
if (!is_readable($cmdline) || !($content = file_get_contents($cmdline)) || (!str_contains($content, 'WorkerMan') && !str_contains($content, 'php'))) {
|
||||
// Process not exist
|
||||
$this->ppid = 0;
|
||||
}
|
||||
return $this->ppid;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return bool
|
||||
*/
|
||||
public function checkAllFilesChange(): bool
|
||||
{
|
||||
if (static::isPaused()) {
|
||||
return false;
|
||||
}
|
||||
foreach ($this->paths as $path) {
|
||||
if ($this->checkFilesChange($path)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param $memoryLimit
|
||||
* @return void
|
||||
*/
|
||||
public function checkMemory($memoryLimit): void
|
||||
{
|
||||
if (static::isPaused() || $memoryLimit <= 0) {
|
||||
return;
|
||||
}
|
||||
$masterPid = $this->getMasterPid();
|
||||
if ($masterPid <= 0) {
|
||||
echo "Master process has gone away\n";
|
||||
return;
|
||||
}
|
||||
|
||||
$childrenFile = "/proc/$masterPid/task/$masterPid/children";
|
||||
if (!is_file($childrenFile) || !($children = file_get_contents($childrenFile))) {
|
||||
return;
|
||||
}
|
||||
foreach (explode(' ', $children) as $pid) {
|
||||
$pid = (int)$pid;
|
||||
$statusFile = "/proc/$pid/status";
|
||||
if (!is_file($statusFile) || !($status = file_get_contents($statusFile))) {
|
||||
continue;
|
||||
}
|
||||
$mem = 0;
|
||||
if (preg_match('/VmRSS\s*?:\s*?(\d+?)\s*?kB/', $status, $match)) {
|
||||
$mem = $match[1];
|
||||
}
|
||||
$mem = (int)($mem / 1024);
|
||||
if ($mem >= $memoryLimit) {
|
||||
posix_kill($pid, SIGINT);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get memory limit
|
||||
* @param $memoryLimit
|
||||
* @return int
|
||||
*/
|
||||
protected function getMemoryLimit($memoryLimit): int
|
||||
{
|
||||
if ($memoryLimit === 0) {
|
||||
return 0;
|
||||
}
|
||||
$usePhpIni = false;
|
||||
if (!$memoryLimit) {
|
||||
$memoryLimit = ini_get('memory_limit');
|
||||
$usePhpIni = true;
|
||||
}
|
||||
|
||||
if ($memoryLimit == -1) {
|
||||
return 0;
|
||||
}
|
||||
$unit = strtolower($memoryLimit[strlen($memoryLimit) - 1]);
|
||||
$memoryLimit = (int)$memoryLimit;
|
||||
if ($unit === 'g') {
|
||||
$memoryLimit = 1024 * $memoryLimit;
|
||||
} else if ($unit === 'k') {
|
||||
$memoryLimit = ($memoryLimit / 1024);
|
||||
} else if ($unit === 'm') {
|
||||
$memoryLimit = (int)($memoryLimit);
|
||||
} else if ($unit === 't') {
|
||||
$memoryLimit = (1024 * 1024 * $memoryLimit);
|
||||
} else {
|
||||
$memoryLimit = ($memoryLimit / (1024 * 1024));
|
||||
}
|
||||
if ($memoryLimit < 50) {
|
||||
$memoryLimit = 50;
|
||||
}
|
||||
if ($usePhpIni) {
|
||||
$memoryLimit = (0.8 * $memoryLimit);
|
||||
}
|
||||
return (int)$memoryLimit;
|
||||
}
|
||||
|
||||
}
|
||||
101
app/support/BaseController.php
Normal file
101
app/support/BaseController.php
Normal file
@@ -0,0 +1,101 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace app\support;
|
||||
|
||||
use support\Response;
|
||||
use Webman\Http\Request as WebmanRequest;
|
||||
|
||||
use function response;
|
||||
|
||||
/**
|
||||
* 控制器基础类
|
||||
* - 适配 Webman Request 注入,子类方法签名:public function xxx(Webman\Http\Request $request)
|
||||
* - 移除对 think\App 的依赖,用注入的 $request 替代 $this->app->request
|
||||
* - 子类在方法开头调用 setRequest($request),之后可用 $this->request 访问
|
||||
*/
|
||||
abstract class BaseController
|
||||
{
|
||||
/**
|
||||
* 当前请求实例(由 setRequest($request) 设置,来源于方法参数 $request,非 think\App)
|
||||
* @var WebmanRequest|null
|
||||
*/
|
||||
protected ?WebmanRequest $request = null;
|
||||
|
||||
/**
|
||||
* 是否批量验证
|
||||
* @var bool
|
||||
*/
|
||||
protected bool $batchValidate = false;
|
||||
|
||||
/**
|
||||
* 初始化
|
||||
* @access protected
|
||||
*/
|
||||
protected function initialize(): void
|
||||
{
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置当前请求(子类方法开头调用)
|
||||
* @param WebmanRequest $request
|
||||
*/
|
||||
protected function setRequest(WebmanRequest $request): void
|
||||
{
|
||||
$this->request = $request;
|
||||
}
|
||||
|
||||
/**
|
||||
* 操作成功(封装为返回 Response 的方法,替代原 $this->success() 抛异常)
|
||||
* @param string $msg 提示消息
|
||||
* @param mixed $data 返回数据
|
||||
* @param int $code 业务状态码(1=成功)
|
||||
* @param array $header 可含 statusCode 指定 HTTP 状态
|
||||
*/
|
||||
protected function success(string $msg = '', mixed $data = null, int $code = 1, array $header = []): Response
|
||||
{
|
||||
return $this->result($msg, $data, $code, $header);
|
||||
}
|
||||
|
||||
/**
|
||||
* 操作失败(封装为返回 Response 的方法,替代原 $this->error() 抛异常)
|
||||
* @param string $msg 提示消息
|
||||
* @param mixed $data 返回数据
|
||||
* @param int $code 业务状态码(0=失败)
|
||||
* @param array $header 可含 statusCode 指定 HTTP 状态(如 401、409)
|
||||
*/
|
||||
protected function error(string $msg = '', mixed $data = null, int $code = 0, array $header = []): Response
|
||||
{
|
||||
return $this->result($msg, $data, $code, $header);
|
||||
}
|
||||
|
||||
/**
|
||||
* 返回 API 数据(BuildAdmin 格式:code, msg, time, data)
|
||||
* 使用 response() 生成 JSON Response
|
||||
*/
|
||||
protected function result(string $msg, mixed $data = null, int $code = 0, array $header = []): Response
|
||||
{
|
||||
$body = [
|
||||
'code' => $code,
|
||||
'msg' => $msg,
|
||||
'time' => time(),
|
||||
'data' => $data,
|
||||
];
|
||||
|
||||
$statusCode = $header['statusCode'] ?? 200;
|
||||
unset($header['statusCode']);
|
||||
|
||||
$headers = array_merge(['Content-Type' => 'application/json'], $header);
|
||||
$options = JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES;
|
||||
if (defined('JSON_INVALID_UTF8_SUBSTITUTE')) {
|
||||
$options |= JSON_INVALID_UTF8_SUBSTITUTE;
|
||||
}
|
||||
$jsonBody = json_encode($body, $options);
|
||||
if ($jsonBody === false) {
|
||||
$jsonBody = '{"code":0,"msg":"JSON encode error","time":' . time() . ',"data":[]}';
|
||||
}
|
||||
|
||||
return response($jsonBody, $statusCode, $headers);
|
||||
}
|
||||
}
|
||||
14
app/view/index/view.html
Normal file
14
app/view/index/view.html
Normal file
@@ -0,0 +1,14 @@
|
||||
<!doctype html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<link rel="shortcut icon" href="/favicon.ico"/>
|
||||
<title>webman</title>
|
||||
|
||||
</head>
|
||||
<body>
|
||||
hello <?=htmlspecialchars($name)?>
|
||||
</body>
|
||||
</html>
|
||||
Reference in New Issue
Block a user