项目初始化

This commit is contained in:
2026-03-18 17:19:03 +08:00
commit ac6079b9ff
602 changed files with 58291 additions and 0 deletions

View 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();
}
}

View 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()
]);
}
}

View 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]);
}
}

View 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);
}
}

View 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;
}
}

View 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);
}
}

View 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;
}
}

View 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'));
}
}

View 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;
}
}

View 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(),
]);
}
}

View 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 中对应的方法至此进行重写
*/
}

View 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]);
}
}

View 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'));
}
}

View 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~'));
}
}

View 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;
}
}

View 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;
}
}

View 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;
}
}

View 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'));
}
}

View 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;
}
}

View 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]);
}
}

View 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]);
}
}

View 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]);
}
}

View 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
View 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',
];

View 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',
];

View 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!',
];

View 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.',
];

View File

@@ -0,0 +1,6 @@
<?php
return [
'type' => 'Rule type',
'title' => 'Rule title',
'name' => 'Rule name',
];

View 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',
];

View 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!",
];

View 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',
];

View 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',
];

View 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!',
];

View 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.',
];

View File

@@ -0,0 +1,7 @@
<?php
return [
'Name' => 'Rule Name',
'Controller' => 'Controller',
'Data Table' => 'Corresponding data table',
'Primary Key' => 'Data table primary key',
];

View File

@@ -0,0 +1,4 @@
<?php
return [
'No rows were restore' => 'No records have been restored',
];

View 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',
];

View File

@@ -0,0 +1,4 @@
<?php
return [
'No rows were rollback' => 'No records have been roll-back',
];

View 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',
];

View 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
View 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' => '分组名称数组',
];

View 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' => '数据表不存在~',
];

View 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!' => '您没有权限向此分组添加管理员!',
];

View 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' => '为保障系统安全,角色组本身的上下级关系仅供参考,系统的实际上下级划分是根据`权限多寡`来确定的,两位管理员的权限节点:相同被认为是`同级`、包含且有额外权限才被认为是`上级`,同级不可管理同级,上级可为下级分配自己拥有的权限节点;若有特殊情况管理员需转`上级`,可建立一个虚拟权限节点',
];

View File

@@ -0,0 +1,6 @@
<?php
return [
'type' => '规则类型',
'title' => '规则标题',
'name' => '规则名称',
];

View 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' => '加载云端数据失败,请稍后重试!',
];

View File

@@ -0,0 +1,4 @@
<?php
return [
'Remark lang' => '开源等于互助;开源需要大家一起来支持,支持的方式有很多种,比如使用、推荐、写教程、保护生态、贡献代码、回答问题、分享经验、打赏赞助等;欢迎您加入我们!',
];

View 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' => '注销登录',
];

View 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 存在冲突',
];

View File

@@ -0,0 +1,6 @@
<?php
return [
'Please input correct username' => '请输入正确的用户名',
'Please input correct password' => '请输入正确的密码',
'Avatar modified successfully!' => '头像修改成功!',
];

View File

@@ -0,0 +1,5 @@
<?php
return [
'Remark lang' => '同一文件被多次上传时,只会保存一份至磁盘和增加一条附件记录;删除附件记录,将自动删除对应文件!',
'%d records and files have been deleted' => '删除了%d条记录和文件',
];

View 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' => '后台入口请以 / 开头,且只包含数字和字母。',
];

View File

@@ -0,0 +1,8 @@
<?php
return [
'Name' => '规则名称',
'Controller' => '控制器',
'Data Table' => '对应数据表',
'Primary Key' => '数据表主键',
'Remark lang' => '在此定义需要回收的数据,实现数据自动统一回收',
];

View File

@@ -0,0 +1,4 @@
<?php
return [
'No rows were restore' => '没有记录被还原',
];

View File

@@ -0,0 +1,9 @@
<?php
return [
'Name' => '规则名称',
'Controller' => '控制器',
'Data Table' => '对应数据表',
'Primary Key' => '数据表主键',
'Data Fields' => '敏感数据字段',
'Remark lang' => '在此定义需要保护的敏感字段,随后系统将自动监听该字段的修改操作,并提供了敏感字段的修改回滚功能',
];

View File

@@ -0,0 +1,4 @@
<?php
return [
'No rows were rollback' => '没有记录被回滚',
];

View File

@@ -0,0 +1,8 @@
<?php
return [
'user_id' => '用户',
'money' => '变更金额',
'memo' => '变更备注',
"The user can't find it" => '用户找不到啦',
'Change note cannot be blank' => '变更备注不能为空',
];

View 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
View 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;
}
}

View 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 代码生成器 HelperWebman 迁移版)
*/
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;
}
}

View 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>

View 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>

View File

@@ -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 中对应的方法至此进行重写
*/
}

View 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(),
]);
}

View File

@@ -0,0 +1,6 @@
public function initialize(): void
{
parent::initialize();
$this->model = new \{%modelNamespace%}\{%modelName%}();{%filterRule%}
}

View 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]]);
}
}
}

View File

@@ -0,0 +1,5 @@
protected static function onBeforeInsert($model): void
{
{%setSnowFlakeIdCode%}
}

View File

@@ -0,0 +1,5 @@
public function {%relationMethod%}(): \think\model\relation\BelongsTo
{
return $this->{%relationMode%}({%relationClassName%}, '{%relationForeignKey%}', '{%relationPrimaryKey%}');
}

View File

@@ -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) : '';
}

View File

@@ -0,0 +1,5 @@
public function get{%field%}Attr($value): ?float
{
return is_null($value) ? null : (float)$value;
}

View File

@@ -0,0 +1,5 @@
public function get{%field%}Attr($value): string
{
return !$value ? '' : htmlspecialchars_decode($value);
}

View File

@@ -0,0 +1,5 @@
public function get{%field%}Attr($value): array
{
return !$value ? [] : json_decode($value, true);
}

View File

@@ -0,0 +1,7 @@
public function get{%field%}Attr($value, $row): array
{
return [
'{%labelFieldName%}' => {%className%}::whereIn('{%primaryKey%}', $row['{%foreignKey%}'])->column('{%labelFieldName%}'),
];
}

View File

@@ -0,0 +1,5 @@
public function get{%field%}Attr($value): string
{
return (string)$value;
}

View File

@@ -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;
}

View File

@@ -0,0 +1,2 @@
$pk = $model->getPk();
$model->$pk = \app\common\library\SnowFlake::generateParticle();

View 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%}
}

View File

@@ -0,0 +1,5 @@
public function set{%field%}Attr($value): string
{
return is_array($value) ? implode(',', $value) : $value;
}

View File

@@ -0,0 +1,5 @@
public function set{%field%}Attr($value): ?string
{
return $value ? date('H:i:s', strtotime($value)) : $value;
}

View 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' => [],
];
}

View 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;
}
}

View 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;
}
}

View File

@@ -0,0 +1,307 @@
<?php
declare(strict_types=1);
namespace app\admin\library\traits;
use Throwable;
use support\Response;
/**
* 后台控制器 traitWebman 迁移版)
* 提供 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
{
}
}

67
app/admin/model/Admin.php Normal file
View File

@@ -0,0 +1,67 @@
<?php
declare(strict_types=1);
namespace app\admin\model;
use app\common\model\traits\TimestampInteger;
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
{
use TimestampInteger;
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' => '']);
}
}

View File

@@ -0,0 +1,20 @@
<?php
declare(strict_types=1);
namespace app\admin\model;
use app\common\model\traits\TimestampInteger;
use support\think\Model;
/**
* AdminGroup 模型Webman 迁移版)
*/
class AdminGroup extends Model
{
use TimestampInteger;
protected string $table = 'admin_group';
protected string $pk = 'id';
protected bool $autoWriteTimestamp = true;
}

View File

@@ -0,0 +1,144 @@
<?php
declare(strict_types=1);
namespace app\admin\model;
use Throwable;
use app\admin\library\Auth;
use app\common\model\traits\TimestampInteger;
use support\think\Model;
use Webman\Http\Request;
/**
* AdminLog 模型Webman 迁移版)
*/
class AdminLog extends Model
{
use TimestampInteger;
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);
}
}

View File

@@ -0,0 +1,23 @@
<?php
declare(strict_types=1);
namespace app\admin\model;
use app\common\model\traits\TimestampInteger;
use support\think\Model;
class AdminRule extends Model
{
use TimestampInteger;
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;
}
}

130
app/admin/model/Config.php Normal file
View File

@@ -0,0 +1,130 @@
<?php
declare(strict_types=1);
namespace app\admin\model;
use app\common\model\traits\TimestampInteger;
use support\think\Model;
/**
* 系统配置模型Webman 迁移版)
*/
class Config extends Model
{
use TimestampInteger;
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 [];
}
}

View File

@@ -0,0 +1,22 @@
<?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 = [
'create_time' => 'integer',
'update_time' => 'integer',
'table' => 'array',
'fields' => 'array',
];
}

View File

@@ -0,0 +1,17 @@
<?php
declare(strict_types=1);
namespace app\admin\model;
use app\common\model\traits\TimestampInteger;
use support\think\Model;
class DataRecycle extends Model
{
use TimestampInteger;
protected string $table = 'security_data_recycle';
protected string $pk = 'id';
protected bool $autoWriteTimestamp = true;
}

View File

@@ -0,0 +1,29 @@
<?php
declare(strict_types=1);
namespace app\admin\model;
use app\common\model\traits\TimestampInteger;
use support\think\Model;
use think\model\relation\BelongsTo;
class DataRecycleLog extends Model
{
use TimestampInteger;
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');
}
}

View File

@@ -0,0 +1,20 @@
<?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 = [
'create_time' => 'integer',
'update_time' => 'integer',
'data_fields' => 'array',
];
}

View File

@@ -0,0 +1,29 @@
<?php
declare(strict_types=1);
namespace app\admin\model;
use app\common\model\traits\TimestampInteger;
use support\think\Model;
use think\model\relation\BelongsTo;
class SensitiveDataLog extends Model
{
use TimestampInteger;
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');
}
}

48
app/admin/model/User.php Normal file
View File

@@ -0,0 +1,48 @@
<?php
declare(strict_types=1);
namespace app\admin\model;
use app\common\model\traits\TimestampInteger;
use support\think\Model;
use think\model\relation\BelongsTo;
class User extends Model
{
use TimestampInteger;
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' => '']);
}
}

View File

@@ -0,0 +1,17 @@
<?php
declare(strict_types=1);
namespace app\admin\model;
use app\common\model\traits\TimestampInteger;
use support\think\Model;
class UserGroup extends Model
{
use TimestampInteger;
protected string $table = 'user_group';
protected string $pk = 'id';
protected bool $autoWriteTimestamp = true;
}

View File

@@ -0,0 +1,72 @@
<?php
namespace app\admin\model;
use app\common\model\traits\TimestampInteger;
use support\think\Model;
use think\model\relation\BelongsTo;
class UserMoneyLog extends Model
{
use TimestampInteger;
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');
}
}

View File

@@ -0,0 +1,29 @@
<?php
declare(strict_types=1);
namespace app\admin\model;
use app\common\model\traits\TimestampInteger;
use support\think\Model;
class UserRule extends Model
{
use TimestampInteger;
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;
}
}

View File

@@ -0,0 +1,42 @@
<?php
namespace app\admin\model;
use app\common\model\traits\TimestampInteger;
use support\think\Model;
use think\model\relation\BelongsTo;
class UserScoreLog extends Model
{
use TimestampInteger;
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');
}
}

View File

@@ -0,0 +1,28 @@
<?php
namespace app\admin\model\mall;
use app\common\model\traits\TimestampInteger;
use support\think\Model;
/**
* Player
*/
class Player extends Model
{
use TimestampInteger;
// 表名
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;
}
}

View 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();
}
}

View 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();
}
}

View 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();
}
}

View 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();
}
}

View 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();
}
}

View 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();
}
}

View 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();
}
}

View 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();
}
}

View 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' => [],
];
}