Merge remote-tracking branch 'origin/master'

# Conflicts:
#	.env-example
This commit is contained in:
2026-03-21 11:19:28 +08:00
32 changed files with 327 additions and 66 deletions

View File

@@ -4,6 +4,7 @@ declare(strict_types=1);
namespace app\admin\model;
use app\common\model\traits\TimestampInteger;
use support\think\Model;
use support\think\Db;
@@ -23,6 +24,8 @@ use support\think\Db;
*/
class Admin extends Model
{
use TimestampInteger;
protected string $table = 'admin';
protected string $pk = 'id';
protected bool $autoWriteTimestamp = true;

View File

@@ -4,6 +4,7 @@ declare(strict_types=1);
namespace app\admin\model;
use app\common\model\traits\TimestampInteger;
use support\think\Model;
/**
@@ -11,6 +12,8 @@ use support\think\Model;
*/
class AdminGroup extends Model
{
use TimestampInteger;
protected string $table = 'admin_group';
protected string $pk = 'id';
protected bool $autoWriteTimestamp = true;

View File

@@ -6,6 +6,7 @@ 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;
@@ -14,6 +15,8 @@ use Webman\Http\Request;
*/
class AdminLog extends Model
{
use TimestampInteger;
protected string $table = 'admin_log';
protected string $pk = 'id';
protected bool $autoWriteTimestamp = true;

View File

@@ -4,10 +4,13 @@ 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;

View File

@@ -4,6 +4,7 @@ declare(strict_types=1);
namespace app\admin\model;
use app\common\model\traits\TimestampInteger;
use support\think\Model;
/**
@@ -11,6 +12,8 @@ use support\think\Model;
*/
class Config extends Model
{
use TimestampInteger;
public static string $cacheTag = 'sys_config';
protected string $table = 'config';

View File

@@ -14,6 +14,8 @@ class CrudLog extends Model
protected bool $updateTime = false;
protected array $type = [
'create_time' => 'integer',
'update_time' => 'integer',
'table' => 'array',
'fields' => 'array',
];

View File

@@ -4,10 +4,13 @@ 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

@@ -4,11 +4,14 @@ 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;

View File

@@ -13,6 +13,8 @@ class SensitiveData extends Model
protected bool $autoWriteTimestamp = true;
protected array $type = [
'create_time' => 'integer',
'update_time' => 'integer',
'data_fields' => 'array',
];
}

View File

@@ -4,11 +4,14 @@ 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;

View File

@@ -4,11 +4,14 @@ 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;

View File

@@ -4,10 +4,13 @@ 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

@@ -2,11 +2,14 @@
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;

View File

@@ -4,10 +4,13 @@ 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;

View File

@@ -2,11 +2,14 @@
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;

View File

@@ -2,6 +2,7 @@
namespace app\admin\model\mall;
use app\common\model\traits\TimestampInteger;
use support\think\Model;
/**
@@ -9,6 +10,8 @@ use support\think\Model;
*/
class Player extends Model
{
use TimestampInteger;
// 表名
protected $name = 'mall_player';

View File

@@ -14,6 +14,10 @@ use app\admin\model\Admin as AdminModel;
use app\admin\model\User as UserModel;
use support\Response;
use Webman\Http\Request;
use Phinx\Config\Config as PhinxConfig;
use Phinx\Migration\Manager as PhinxManager;
use Symfony\Component\Console\Input\ArrayInput;
use Symfony\Component\Console\Output\NullOutput;
/**
* 安装控制器
@@ -74,7 +78,7 @@ class Install extends Api
{
$this->setRequest($request);
if ($this->isInstallComplete()) {
return $this->error(__('The system has completed installation. If you need to reinstall, please delete the %s file first', ['public/' . self::$lockFileName]));
return $this->error(__('The system has completed installation. If you need to reinstall, please delete the %file% file first', ['%file%' => 'public/' . self::$lockFileName]));
}
(new Terminal())->exec(false);
@@ -85,7 +89,7 @@ class Install extends Api
{
$this->setRequest($request);
if ($this->isInstallComplete()) {
return $this->error(__('The system has completed installation. If you need to reinstall, please delete the %s file first', ['public/' . self::$lockFileName]));
return $this->error(__('The system has completed installation. If you need to reinstall, please delete the %file% file first', ['%file%' => 'public/' . self::$lockFileName]));
}
$newPackageManager = $request->post('manager', config('terminal.npm_package_manager'));
@@ -104,7 +108,7 @@ class Install extends Api
{
$this->setRequest($request);
if ($this->isInstallComplete()) {
return $this->error(__('The system has completed installation. If you need to reinstall, please delete the %s file first', ['public/' . self::$lockFileName]), []);
return $this->error(__('The system has completed installation. If you need to reinstall, please delete the %file% file first', ['%file%' => 'public/' . self::$lockFileName]), []);
}
if (($_ENV['DATABASE_TYPE'] ?? getenv('DATABASE_TYPE'))) {
return $this->error(__('The .env file with database configuration was detected. Please clean up and try again!'));
@@ -410,7 +414,7 @@ class Install extends Api
{
$this->setRequest($request);
if ($this->isInstallComplete()) {
return $this->error(__('The system has completed installation. If you need to reinstall, please delete the %s file first', ['public/' . self::$lockFileName]));
return $this->error(__('The system has completed installation. If you need to reinstall, please delete the %file% file first', ['%file%' => 'public/' . self::$lockFileName]));
}
$envOk = $this->commandExecutionCheck();
@@ -456,27 +460,32 @@ class Install extends Api
return $this->error(__('File has no write permission:%s', ['config/' . self::$dbConfigFileName]));
}
// 写入 dafuweng-webman/.env-example
$envFile = root_path() . '.env-example';
$envFileContent = @file_get_contents($envFile);
if ($envFileContent) {
$databasePos = stripos($envFileContent, '[DATABASE]');
if ($databasePos !== false) {
$envFileContent = substr($envFileContent, 0, $databasePos);
}
$envFileContent .= "\n" . '[DATABASE]' . "\n";
$envFileContent .= 'TYPE = mysql' . "\n";
$envFileContent .= 'HOSTNAME = ' . $databaseParam['hostname'] . "\n";
$envFileContent .= 'DATABASE = ' . $databaseParam['database'] . "\n";
$envFileContent .= 'USERNAME = ' . $databaseParam['username'] . "\n";
$envFileContent .= 'PASSWORD = ' . $databaseParam['password'] . "\n";
$envFileContent .= 'HOSTPORT = ' . $databaseParam['hostport'] . "\n";
$envFileContent .= 'PREFIX = ' . ($databaseParam['prefix'] ?? '') . "\n";
$envFileContent .= 'CHARSET = utf8mb4' . "\n";
$envFileContent .= 'DEBUG = true' . "\n";
$result = @file_put_contents($envFile, $envFileContent);
if (!$result) {
return $this->error(__('File has no write permission:%s', ['/' . $envFile]));
// 写入 .env 和 .env-example仅使用 Dotenv 可解析的 DATABASE_XXX 格式,避免 [DATABASE] 导致解析失败)
$databaseBlock = "\n# Database\n"
. 'DATABASE_TYPE = mysql' . "\n"
. 'DATABASE_HOSTNAME = ' . $databaseParam['hostname'] . "\n"
. 'DATABASE_DATABASE = ' . $databaseParam['database'] . "\n"
. 'DATABASE_USERNAME = ' . $databaseParam['username'] . "\n"
. 'DATABASE_PASSWORD = ' . $databaseParam['password'] . "\n"
. 'DATABASE_HOSTPORT = ' . $databaseParam['hostport'] . "\n"
. 'DATABASE_CHARSET = utf8mb4' . "\n"
. 'DATABASE_PREFIX = ' . ($databaseParam['prefix'] ?? '') . "\n";
foreach (['.env', '.env-example'] as $envName) {
$envFile = root_path() . $envName;
$envFileContent = is_file($envFile) ? @file_get_contents($envFile) : '';
if ($envFileContent !== false) {
$cutPos = strlen($envFileContent);
foreach (['[DATABASE]', "\n# Database\n", "\n# 数据库", "\nDATABASE_DRIVER", "\nDATABASE_TYPE"] as $marker) {
$pos = stripos($envFileContent, $marker);
if ($pos !== false && $pos < $cutPos) {
$cutPos = $pos;
}
}
$envFileContent = rtrim(substr($envFileContent, 0, $cutPos)) . $databaseBlock;
$result = @file_put_contents($envFile, $envFileContent);
if (!$result && is_file($envFile)) {
return $this->error(__('File has no write permission:%s', ['%s' => $envName]));
}
}
}
@@ -500,13 +509,66 @@ class Install extends Api
return $this->error(__('File has no write permission:%s', ['public/' . self::$lockFileName]));
}
// 自动执行数据库迁移(无需手动运行 phinx 命令)
$migrateResult = $this->runPhinxMigrate($databaseParam);
if ($migrateResult !== true) {
return $this->error($migrateResult);
}
return $this->success('', [
'rootPath' => $rootPath,
'executionWebCommand' => $envOk,
'migrateCommand' => $migrateCommand,
'rootPath' => $rootPath,
'executionWebCommand' => $envOk,
'migrateCommand' => $migrateCommand,
'migrationCompleted' => true,
]);
}
/**
* 程序化执行 Phinx 数据库迁移
* @param array $databaseParam 数据库连接参数
* @return true|string 成功返回 true失败返回错误信息
*/
private function runPhinxMigrate(array $databaseParam): true|string
{
try {
$baseDir = root_path();
$phinxConfigPath = $baseDir . 'phinx.php';
if (!is_file($phinxConfigPath)) {
return __('Failed to install SQL execution:%msg%', ['%msg%' => 'phinx.php not found']);
}
// 临时设置环境变量,供 phinx 读取数据库配置
$_ENV['DATABASE_HOSTNAME'] = $databaseParam['hostname'] ?? '127.0.0.1';
$_ENV['DATABASE_DATABASE'] = $databaseParam['database'] ?? '';
$_ENV['DATABASE_USERNAME'] = $databaseParam['username'] ?? 'root';
$_ENV['DATABASE_PASSWORD'] = $databaseParam['password'] ?? '';
$_ENV['DATABASE_HOSTPORT'] = $databaseParam['hostport'] ?? '3306';
$_ENV['DATABASE_PREFIX'] = $databaseParam['prefix'] ?? '';
putenv('DATABASE_HOSTNAME=' . $_ENV['DATABASE_HOSTNAME']);
putenv('DATABASE_DATABASE=' . $_ENV['DATABASE_DATABASE']);
putenv('DATABASE_USERNAME=' . $_ENV['DATABASE_USERNAME']);
putenv('DATABASE_PASSWORD=' . $_ENV['DATABASE_PASSWORD']);
putenv('DATABASE_HOSTPORT=' . $_ENV['DATABASE_HOSTPORT']);
putenv('DATABASE_PREFIX=' . $_ENV['DATABASE_PREFIX']);
$config = PhinxConfig::fromPhp($phinxConfigPath);
$input = new ArrayInput([]);
$output = new NullOutput();
$manager = new PhinxManager($config, $input, $output);
$environment = $config->getDefaultEnvironment();
$manager->migrate($environment);
return true;
} catch (Throwable $e) {
$msg = $e->getMessage();
if ($e->getPrevious()) {
$msg .= ' | ' . $e->getPrevious()->getMessage();
}
return __('Failed to install SQL execution:%msg%', ['%msg%' => $msg]);
}
}
protected function isInstallComplete(): bool
{
if (is_file(public_path(self::$lockFileName))) {
@@ -526,7 +588,7 @@ class Install extends Api
{
$this->setRequest($request);
if ($this->isInstallComplete()) {
return $this->error(__('The system has completed installation. If you need to reinstall, please delete the %s file first', ['public/' . self::$lockFileName]));
return $this->error(__('The system has completed installation. If you need to reinstall, please delete the %file% file first', ['%file%' => 'public/' . self::$lockFileName]));
}
$param = $request->only(['type', 'adminname', 'adminpassword', 'sitename']);
@@ -586,6 +648,36 @@ class Install extends Api
return $envOk;
}
/**
* 获取安装完成后的访问地址(根据请求来源区分 API 与前端开发模式)
* - 通过 API 访问8787index.html#/admin、index.html#/
* - 通过前端开发服务访问1818/#/admin、/#/
*/
public function accessUrls(Request $request): Response
{
$this->setRequest($request);
$host = $request->header('host', '127.0.0.1:8787');
$port = '8787';
if (str_contains($host, ':')) {
$port = substr($host, strrpos($host, ':') + 1);
}
$scheme = $request->header('x-forwarded-proto', 'http');
$base = rtrim($scheme . '://' . $host, '/');
if ($port === '1818') {
$adminUrl = $base . '/#/admin';
$frontUrl = $base . '/#/';
} else {
$adminUrl = $base . '/index.html#/admin';
$frontUrl = $base . '/index.html#/';
}
return $this->success('', [
'adminUrl' => $adminUrl,
'frontUrl' => $frontUrl,
]);
}
/**
* 安装指引
*/

View File

@@ -18,9 +18,9 @@ return [
'already installed' => 'Installed',
'Not installed' => 'Not installed',
'File has no write permission:%s' => 'File has no write permission:%s',
'The system has completed installation. If you need to reinstall, please delete the %s file first' => 'The system has been installed, if you need to reinstall, please delete the %s file first.',
'The system has completed installation. If you need to reinstall, please delete the %file% file first' => 'The system has been installed. If you need to reinstall, please delete the %file% file first.',
'Database connection failed:%s' => 'Database connection failure%s',
'Failed to install SQL execution:%s' => 'Installation SQL execution failed%s',
'Failed to install SQL execution:%msg%' => 'Installation SQL execution failed: %msg%',
'unknown' => 'Unknown',
'Database does not exist' => 'Database does not exist!',
'No built front-end file found, please rebuild manually!' => 'No built front-end file found, please rebuild manually.',

View File

@@ -18,9 +18,9 @@ return [
'already installed' => '已安装',
'Not installed' => '未安装',
'File has no write permission:%s' => '文件无写入权限:%s',
'The system has completed installation. If you need to reinstall, please delete the %s file first' => '系统已完成安装。如果需要重新安装,请先删除 %s 文件',
'The system has completed installation. If you need to reinstall, please delete the %file% file first' => '系统已完成安装。如果需要重新安装,请先删除 %file% 文件',
'Database connection failed:%s' => '数据库连接失败:%s',
'Failed to install SQL execution:%s' => '安装SQL执行失败%s',
'Failed to install SQL execution:%msg%' => '安装SQL执行失败%msg%',
'unknown' => '未知',
'Database does not exist' => '数据库不存在!',
'No built front-end file found, please rebuild manually!' => '没有找到构建好的前端文件,请手动重新构建!',

View File

@@ -26,12 +26,20 @@ class LoadLangPack implements MiddlewareInterface
protected function loadLang(Request $request): void
{
// 优先从请求头 think-lang 获取前端选择的语言(与前端 axios 发送的 header 对应)
// 安装页等未发送 think-lang 时,回退到 Accept-Language 或配置默认值
$headerLang = $request->header('think-lang');
$allowLangList = config('lang.allow_lang_list', ['zh-cn', 'en']);
if ($headerLang && in_array(str_replace('_', '-', strtolower($headerLang)), $allowLangList)) {
$langSet = str_replace('_', '-', strtolower($headerLang));
} else {
$langSet = config('lang.default_lang', config('translation.locale', 'zh-cn'));
$acceptLang = $request->header('accept-language', '');
if (preg_match('/^zh[-_]?cn|^zh/i', $acceptLang)) {
$langSet = 'zh-cn';
} elseif (preg_match('/^en/i', $acceptLang)) {
$langSet = 'en';
} else {
$langSet = config('lang.default_lang', config('translation.locale', 'zh-cn'));
}
$langSet = str_replace('_', '-', strtolower($langSet));
}
@@ -59,6 +67,7 @@ class LoadLangPack implements MiddlewareInterface
}
// 2. 加载控制器专用语言包(如 zh-cn/auth/group.php供 get_route_remark 等使用
// 同时加载到 messages 域,使 __() 能正确翻译控制器内的文案(如安装页错误提示)
$controllerPath = get_controller_path($request);
if ($controllerPath) {
$controllerPathForFile = str_replace('.', '/', $controllerPath);
@@ -68,6 +77,7 @@ class LoadLangPack implements MiddlewareInterface
$controllerLangFile = $appLangDir . $langSet . DIRECTORY_SEPARATOR . str_replace('/', DIRECTORY_SEPARATOR, $controllerPathForFile) . '.php';
if (is_file($controllerLangFile)) {
$translator->addResource('phpfile', $controllerLangFile, $langSet, $controllerPath);
$translator->addResource('phpfile', $controllerLangFile, $langSet, 'messages');
}
}
}

View File

@@ -4,11 +4,14 @@ declare(strict_types=1);
namespace app\common\model;
use app\common\model\traits\TimestampInteger;
use support\think\Model;
use think\model\relation\BelongsTo;
class Attachment extends Model
{
use TimestampInteger;
protected string $table = 'attachment';
protected string $pk = 'id';
protected bool $autoWriteTimestamp = true;

View File

@@ -2,6 +2,7 @@
namespace app\common\model;
use app\common\model\traits\TimestampInteger;
use support\think\Model;
/**
@@ -9,6 +10,8 @@ use support\think\Model;
*/
class User extends Model
{
use TimestampInteger;
protected string $table = 'user';
protected string $pk = 'id';
protected bool $autoWriteTimestamp = true;

View File

@@ -2,10 +2,13 @@
namespace app\common\model;
use app\common\model\traits\TimestampInteger;
use support\think\Model;
class UserMoneyLog extends Model
{
use TimestampInteger;
protected string $table = 'user_money_log';
protected string $pk = 'id';
protected bool $autoWriteTimestamp = true;

View File

@@ -2,10 +2,13 @@
namespace app\common\model;
use app\common\model\traits\TimestampInteger;
use support\think\Model;
class UserScoreLog extends Model
{
use TimestampInteger;
protected string $table = 'user_score_log';
protected string $pk = 'id';
protected bool $autoWriteTimestamp = true;

View File

@@ -0,0 +1,25 @@
<?php
declare(strict_types=1);
namespace app\common\model\traits;
/**
* 时间戳整型字段修复
*
* ThinkORM 对 MySQL bigint 类型识别为 'bigint',自动时间戳会错误地写入 'now' 字符串,
* 导致 SQLSTATE[HY000]: General error: 1366 Incorrect integer value: 'now' for column 'update_time'
*
* 通过显式指定 create_time、update_time 等为 integer 类型,使 ThinkORM 使用 time() 写入正确的时间戳。
*/
trait TimestampInteger
{
/**
* 字段类型映射:将 bigint 时间戳字段显式声明为 integer避免 ThinkORM 写入 'now' 字符串
*/
protected array $type = [
'create_time' => 'integer',
'update_time' => 'integer',
'last_login_time' => 'integer',
];
}