fix(web): 本地优先保存与 design 页体验修复
- 保存先写本地缓存,登录后可选云端同步;退出登录不再因文件操作跳转登录 - 修复 Layui 遮罩残留、design2 保存后线条、toast 被 finally 提前关闭 - 保存过程恢复 loading spinner;成功提示 2.5 秒 - 云端 payload 瘦身与体积超限提示;后端 schema 迁移与 FileService 容错 Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -33,10 +33,26 @@ vi config/local.php
|
||||
改 `db`、`jwt_secret`、`site`、`storage` 路径。
|
||||
|
||||
```bash
|
||||
mysql -u root -p soondesign < schema.sql
|
||||
mysql -u root -p soondesign < database/schema.sql
|
||||
php scripts/migrate-schema.php
|
||||
```
|
||||
|
||||
再执行上面「权限」整段粘贴。
|
||||
`migrate-schema.php` 用于已有库增量升级(可重复执行)。再执行上面「权限」整段粘贴。
|
||||
|
||||
### MySQL 大包(云端保存失败时)
|
||||
|
||||
若保存报「文件过大」或 `max_allowed_packet`,在宝塔 / MySQL 配置中把 `max_allowed_packet` 调到 **32M** 或以上,重启 MySQL 后生效。可临时验证:
|
||||
|
||||
```sql
|
||||
SET GLOBAL max_allowed_packet = 33554432;
|
||||
```
|
||||
|
||||
持久化写入 `my.cnf`(示例):
|
||||
|
||||
```ini
|
||||
[mysqld]
|
||||
max_allowed_packet = 32M
|
||||
```
|
||||
|
||||
## 初始化管理员
|
||||
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* Incremental DB migration (reads config/local.php). Safe to run repeatedly.
|
||||
* Usage: cd backend-web && php scripts/migrate-schema.php
|
||||
*/
|
||||
if (PHP_SAPI !== 'cli') {
|
||||
fwrite(STDERR, "CLI only\n");
|
||||
exit(1);
|
||||
}
|
||||
|
||||
require dirname(__DIR__) . '/src/bootstrap.php';
|
||||
require __DIR__ . '/schema-migrate-lib.php';
|
||||
|
||||
use Soon\Api\Core\Db;
|
||||
|
||||
try {
|
||||
$count = soon_schema_migrate(Db::pdo());
|
||||
fwrite(STDOUT, 'migrate-schema: done (' . $count . " ALTER/CREATE executed)\n");
|
||||
} catch (Throwable $e) {
|
||||
fwrite(STDERR, 'migrate-schema: ' . $e->getMessage() . PHP_EOL);
|
||||
exit(1);
|
||||
}
|
||||
@@ -0,0 +1,160 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* Incremental schema patches (safe to run repeatedly).
|
||||
*
|
||||
* @param-out int $applied
|
||||
*/
|
||||
function soon_schema_column_exists(PDO $pdo, string $table, string $column): bool
|
||||
{
|
||||
$stmt = $pdo->prepare(
|
||||
'SELECT COUNT(*) FROM information_schema.COLUMNS '
|
||||
. 'WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = :t AND COLUMN_NAME = :c'
|
||||
);
|
||||
$stmt->execute(['t' => $table, 'c' => $column]);
|
||||
return (int)$stmt->fetchColumn() > 0;
|
||||
}
|
||||
|
||||
/** @return int Number of SQL statements executed */
|
||||
function soon_schema_migrate(PDO $pdo, string $logPrefix = 'migrate-schema'): int
|
||||
{
|
||||
$applied = 0;
|
||||
$run = static function (string $sql) use ($pdo, $logPrefix, &$applied): void {
|
||||
$pdo->exec($sql);
|
||||
++$applied;
|
||||
fwrite(STDOUT, "{$logPrefix}: {$sql}" . PHP_EOL);
|
||||
};
|
||||
|
||||
$alters = [];
|
||||
if (!soon_schema_column_exists($pdo, 'subscriptions', 'source_order_id')) {
|
||||
$alters[] = 'ALTER TABLE subscriptions ADD COLUMN source_order_id BIGINT UNSIGNED DEFAULT NULL AFTER plan_id';
|
||||
$alters[] = 'ALTER TABLE subscriptions ADD KEY source_order (source_order_id)';
|
||||
}
|
||||
if (!soon_schema_column_exists($pdo, 'pay_orders', 'refund_status')) {
|
||||
$alters[] = "ALTER TABLE pay_orders ADD COLUMN refund_status ENUM('none','pending','approved','rejected') NOT NULL DEFAULT 'none' AFTER status";
|
||||
$alters[] = 'ALTER TABLE pay_orders ADD COLUMN refund_reason TEXT DEFAULT NULL AFTER refund_status';
|
||||
$alters[] = 'ALTER TABLE pay_orders ADD COLUMN refund_note TEXT DEFAULT NULL AFTER refund_reason';
|
||||
$alters[] = 'ALTER TABLE pay_orders ADD COLUMN cancelled_at DATETIME DEFAULT NULL AFTER paid_at';
|
||||
$alters[] = 'ALTER TABLE pay_orders ADD COLUMN refunded_at DATETIME DEFAULT NULL AFTER cancelled_at';
|
||||
$alters[] = 'ALTER TABLE pay_orders ADD KEY refund_status (refund_status)';
|
||||
}
|
||||
if (!soon_schema_column_exists($pdo, 'plans', 'description')) {
|
||||
$alters[] = 'ALTER TABLE plans ADD COLUMN description VARCHAR(255) DEFAULT NULL AFTER name';
|
||||
$alters[] = 'ALTER TABLE plans ADD COLUMN max_files INT UNSIGNED NOT NULL DEFAULT 0 AFTER quota_mb';
|
||||
$alters[] = 'ALTER TABLE plans ADD COLUMN features TEXT DEFAULT NULL AFTER duration_days';
|
||||
$alters[] = 'ALTER TABLE plans ADD COLUMN sort_order INT NOT NULL DEFAULT 0 AFTER features';
|
||||
$alters[] = 'ALTER TABLE plans ADD COLUMN is_recommended TINYINT(1) NOT NULL DEFAULT 0 AFTER sort_order';
|
||||
}
|
||||
if (!soon_schema_column_exists($pdo, 'users', 'admin_level')) {
|
||||
$alters[] = "ALTER TABLE users ADD COLUMN admin_level ENUM('full','ops') DEFAULT NULL AFTER role";
|
||||
}
|
||||
if (!soon_schema_column_exists($pdo, 'soon_files', 'thumb')) {
|
||||
$alters[] = 'ALTER TABLE soon_files ADD COLUMN thumb MEDIUMTEXT DEFAULT NULL AFTER version';
|
||||
}
|
||||
|
||||
foreach ($alters as $sql) {
|
||||
$run($sql);
|
||||
}
|
||||
|
||||
if (soon_schema_column_exists($pdo, 'users', 'admin_level')) {
|
||||
$pdo->exec(
|
||||
"UPDATE users SET admin_level = 'full' WHERE role = 'admin' AND (admin_level IS NULL OR admin_level = '')"
|
||||
);
|
||||
fwrite(STDOUT, "{$logPrefix}: admin_level backfill for existing admins" . PHP_EOL);
|
||||
}
|
||||
|
||||
if (soon_schema_column_exists($pdo, 'plans', 'description')) {
|
||||
$legacyCodes = ['pro_monthly' => 'member_monthly', 'pro_yearly' => 'member_yearly'];
|
||||
foreach ($legacyCodes as $oldCode => $newCode) {
|
||||
$stmt = $pdo->prepare('SELECT id FROM plans WHERE code = :c LIMIT 1');
|
||||
$stmt->execute(['c' => $oldCode]);
|
||||
if ($stmt->fetch()) {
|
||||
$conflict = $pdo->prepare('SELECT id FROM plans WHERE code = :c LIMIT 1');
|
||||
$conflict->execute(['c' => $newCode]);
|
||||
if ($conflict->fetch()) {
|
||||
$pdo->prepare('UPDATE plans SET is_active = 0 WHERE code = :c')->execute(['c' => $oldCode]);
|
||||
} else {
|
||||
$pdo->prepare('UPDATE plans SET code = :new WHERE code = :old')->execute(['new' => $newCode, 'old' => $oldCode]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$quota = 2048;
|
||||
$maxFiles = 200;
|
||||
$seed = [
|
||||
['free', '免费版', '免费体验设计与编辑', 0, $quota, $maxFiles, 0,
|
||||
'["设计与编辑工具免费使用"]', 0, 0, 1],
|
||||
['member_lifetime', '永久会员', '一次激活,永久使用预览交付能力', 100, $quota, $maxFiles, 0,
|
||||
'["高清预览","成品打印","云端保存","导出设计文件","永久有效"]', 10, 1, 1],
|
||||
['member_monthly', '月度订阅', '按月灵活使用,随时续订', 1999, $quota, $maxFiles, 30,
|
||||
'["高清预览","成品打印","云端保存","导出设计文件","订阅周期:1 个月"]', 20, 0, 0],
|
||||
['member_quarterly', '季度订阅', '连续三个月,更省心', 5299, $quota, $maxFiles, 90,
|
||||
'["高清预览","成品打印","云端保存","导出设计文件","订阅周期:1 季"]', 25, 0, 0],
|
||||
['member_yearly', '年度订阅', '全年畅享,性价比更高', 19999, $quota, $maxFiles, 365,
|
||||
'["高清预览","成品打印","云端保存","导出设计文件","订阅周期:1 年"]', 30, 0, 0],
|
||||
];
|
||||
$upsert = $pdo->prepare(
|
||||
'INSERT INTO plans (code, name, description, price_cents, quota_mb, max_files, duration_days, features, sort_order, is_recommended, is_active) '
|
||||
. 'VALUES (:c, :n, :d, :p, :q, :mf, :dd, :f, :so, :ir, :a) '
|
||||
. 'ON DUPLICATE KEY UPDATE name=VALUES(name), description=VALUES(description), price_cents=VALUES(price_cents), '
|
||||
. 'quota_mb=VALUES(quota_mb), max_files=VALUES(max_files), duration_days=VALUES(duration_days), '
|
||||
. 'features=VALUES(features), sort_order=VALUES(sort_order), is_recommended=VALUES(is_recommended), is_active=VALUES(is_active)'
|
||||
);
|
||||
foreach ($seed as $row) {
|
||||
$upsert->execute([
|
||||
'c' => $row[0], 'n' => $row[1], 'd' => $row[2], 'p' => $row[3], 'q' => $row[4],
|
||||
'mf' => $row[5], 'dd' => $row[6], 'f' => $row[7], 'so' => $row[8], 'ir' => $row[9], 'a' => $row[10],
|
||||
]);
|
||||
}
|
||||
fwrite(STDOUT, "{$logPrefix}: membership plans catalog synced" . PHP_EOL);
|
||||
|
||||
$legacyFeatStmt = $pdo->query(
|
||||
'SELECT code, duration_days FROM plans WHERE features LIKE \'%预览成品效果%\' '
|
||||
. 'OR features LIKE \'%到期后恢复为非会员%\' OR features LIKE \'%会员权益一致%\' '
|
||||
. 'OR name LIKE \'%会员 ·%\''
|
||||
);
|
||||
$featUpd = $pdo->prepare('UPDATE plans SET features = :f WHERE code = :c');
|
||||
$coreFeatures = ['高清预览', '成品打印', '云端保存', '导出设计文件'];
|
||||
foreach ($legacyFeatStmt->fetchAll() as $legacyRow) {
|
||||
$code = (string)$legacyRow['code'];
|
||||
$days = (int)$legacyRow['duration_days'];
|
||||
if ($code === 'free') {
|
||||
$featUpd->execute([
|
||||
'f' => '["设计与编辑工具免费使用"]',
|
||||
'c' => $code,
|
||||
]);
|
||||
continue;
|
||||
}
|
||||
$lines = $coreFeatures;
|
||||
if ($days > 0) {
|
||||
$period = $days >= 365 ? '1 年' : ($days === 90 ? '1 季' : ($days >= 30 && $days % 30 === 0 ? ((int)($days / 30)) . ' 个月' : $days . ' 天'));
|
||||
$lines[] = '订阅周期:' . $period;
|
||||
}
|
||||
$featUpd->execute([
|
||||
'f' => json_encode($lines, JSON_UNESCAPED_UNICODE),
|
||||
'c' => $code,
|
||||
]);
|
||||
}
|
||||
fwrite(STDOUT, "{$logPrefix}: legacy plan copy refreshed" . PHP_EOL);
|
||||
}
|
||||
|
||||
$run(
|
||||
'CREATE TABLE IF NOT EXISTS `soon_templates` ('
|
||||
. '`id` INT UNSIGNED NOT NULL AUTO_INCREMENT,'
|
||||
. '`name` VARCHAR(120) NOT NULL,'
|
||||
. '`type` TINYINT UNSIGNED NOT NULL DEFAULT 1,'
|
||||
. '`thumb` MEDIUMTEXT DEFAULT NULL,'
|
||||
. '`file_size` INT UNSIGNED NOT NULL DEFAULT 0,'
|
||||
. '`sort_order` INT NOT NULL DEFAULT 0,'
|
||||
. '`is_active` TINYINT(1) NOT NULL DEFAULT 1,'
|
||||
. '`created_at` DATETIME NOT NULL,'
|
||||
. '`updated_at` DATETIME NOT NULL,'
|
||||
. 'PRIMARY KEY (`id`),'
|
||||
. 'KEY `active_sort` (`is_active`, `sort_order`, `id`)'
|
||||
. ') ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci'
|
||||
);
|
||||
fwrite(STDOUT, "{$logPrefix}: soon_templates ensured" . PHP_EOL);
|
||||
|
||||
return $applied;
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Soon\Api\Core;
|
||||
|
||||
use PDOException;
|
||||
use Throwable;
|
||||
|
||||
/**
|
||||
* API 未捕获异常 → JSON 响应(避免空白 500)。
|
||||
*/
|
||||
final class ErrorHandler
|
||||
{
|
||||
public static function register(): void
|
||||
{
|
||||
set_exception_handler([self::class, 'handleException']);
|
||||
}
|
||||
|
||||
public static function handleException(Throwable $e): void
|
||||
{
|
||||
$path = parse_url($_SERVER['REQUEST_URI'] ?? '/', PHP_URL_PATH) ?: '/';
|
||||
if (!str_starts_with($path, '/api/')) {
|
||||
http_response_code(500);
|
||||
exit;
|
||||
}
|
||||
$debug = (bool)Config::get('app.debug', false);
|
||||
$code = 'server_error';
|
||||
$msg = '服务器错误,请稍后重试';
|
||||
$status = 500;
|
||||
|
||||
if ($e instanceof PDOException) {
|
||||
$msg = '数据库操作失败';
|
||||
$sqlState = (string)$e->getCode();
|
||||
$detail = $e->getMessage();
|
||||
if (str_contains($detail, 'max_allowed_packet') || str_contains($detail, 'Packet too large')) {
|
||||
$code = 'payload_too_large';
|
||||
$msg = '文件过大,超出服务器数据库限制';
|
||||
$status = 413;
|
||||
} elseif (str_contains($detail, 'Unknown column') || str_contains($detail, "doesn't exist")) {
|
||||
$msg = '数据库结构不完整,请导入 database/schema.sql 或执行 php scripts/migrate-schema.php';
|
||||
} elseif ($sqlState === '22001' || str_contains($detail, 'Data too long')) {
|
||||
$code = 'payload_too_large';
|
||||
$msg = '文件内容过长,无法保存';
|
||||
$status = 413;
|
||||
}
|
||||
}
|
||||
|
||||
if ($debug) {
|
||||
$msg .= ' (' . $e->getMessage() . ')';
|
||||
}
|
||||
Json::fail($code, $msg, $status);
|
||||
}
|
||||
}
|
||||
@@ -6,12 +6,16 @@ namespace Soon\Api\Services;
|
||||
use Soon\Api\Core\Config;
|
||||
use Soon\Api\Core\Db;
|
||||
use Soon\Api\Core\Json;
|
||||
use PDOException;
|
||||
use Throwable;
|
||||
|
||||
/**
|
||||
* .soon 文件服务:JSON 数据流模型,乐观锁、配额、软删除。
|
||||
*/
|
||||
final class FileService
|
||||
{
|
||||
private const MAX_JSON_BYTES = 33554432;
|
||||
|
||||
public static function usersRoot(): string
|
||||
{
|
||||
$dir = (string)Config::get('storage.users_dir', SOON_SERVER_ROOT . '/storage/users');
|
||||
@@ -74,32 +78,40 @@ final class FileService
|
||||
|
||||
public static function create(int $userId, string $name, string $json): array
|
||||
{
|
||||
$maxFiles = MembershipService::maxFilesLimit($userId);
|
||||
if (self::fileCount($userId) >= $maxFiles) {
|
||||
Json::fail('file_limit_exceeded', '已达文件数量上限(' . $maxFiles . ' 个),请清理文件或续订', 413);
|
||||
$name = self::normalizeName($name);
|
||||
self::assertJsonPayload($json);
|
||||
try {
|
||||
$maxFiles = MembershipService::maxFilesLimit($userId);
|
||||
if (self::fileCount($userId) >= $maxFiles) {
|
||||
Json::fail('file_limit_exceeded', '已达文件数量上限(' . $maxFiles . ' 个),请清理文件或续订', 413);
|
||||
}
|
||||
$size = strlen($json);
|
||||
$quota = self::quotaBytes($userId);
|
||||
$used = self::usedBytes($userId);
|
||||
if ($quota > 0 && $used + $size > $quota) {
|
||||
Json::fail('quota_exceeded', '存储空间已满,请清理文件或续订', 413);
|
||||
}
|
||||
$now = date('Y-m-d H:i:s');
|
||||
$thumb = self::extractThumb($json);
|
||||
$stmt = Db::pdo()->prepare(
|
||||
'INSERT INTO soon_files (user_id, name, json, size, version, thumb, created_at, updated_at) '
|
||||
. 'VALUES (:u, :n, :j, :s, 1, :th, :created_at, :updated_at)'
|
||||
);
|
||||
$stmt->execute([
|
||||
'u' => $userId, 'n' => $name, 'j' => $json, 's' => $size, 'th' => $thumb !== '' ? $thumb : null,
|
||||
'created_at' => $now, 'updated_at' => $now,
|
||||
]);
|
||||
$id = (int)Db::pdo()->lastInsertId();
|
||||
return ['id' => $id, 'name' => $name, 'size' => $size, 'version' => 1, 'updated_at' => $now];
|
||||
} catch (PDOException $e) {
|
||||
self::failDb($e);
|
||||
}
|
||||
$size = strlen($json);
|
||||
$quota = self::quotaBytes($userId);
|
||||
$used = self::usedBytes($userId);
|
||||
if ($quota > 0 && $used + $size > $quota) {
|
||||
Json::fail('quota_exceeded', '存储空间已满,请清理文件或续订', 413);
|
||||
}
|
||||
$now = date('Y-m-d H:i:s');
|
||||
$thumb = TemplateService::thumbFromSoonJson($json);
|
||||
$stmt = Db::pdo()->prepare(
|
||||
'INSERT INTO soon_files (user_id, name, json, size, version, thumb, created_at, updated_at) '
|
||||
. 'VALUES (:u, :n, :j, :s, 1, :th, :created_at, :updated_at)'
|
||||
);
|
||||
$stmt->execute([
|
||||
'u' => $userId, 'n' => $name, 'j' => $json, 's' => $size, 'th' => $thumb !== '' ? $thumb : null,
|
||||
'created_at' => $now, 'updated_at' => $now,
|
||||
]);
|
||||
$id = (int)Db::pdo()->lastInsertId();
|
||||
return ['id' => $id, 'name' => $name, 'size' => $size, 'version' => 1, 'updated_at' => $now];
|
||||
}
|
||||
|
||||
public static function update(int $userId, int $id, string $name, string $json, ?int $expectedVersion): array
|
||||
{
|
||||
$name = self::normalizeName($name);
|
||||
self::assertJsonPayload($json);
|
||||
$newSize = strlen($json);
|
||||
$quota = self::quotaBytes($userId);
|
||||
$used = self::usedBytes($userId);
|
||||
@@ -121,7 +133,7 @@ final class FileService
|
||||
}
|
||||
$now = date('Y-m-d H:i:s');
|
||||
$newVersion = (int)$row['version'] + 1;
|
||||
$thumb = TemplateService::thumbFromSoonJson($json);
|
||||
$thumb = self::extractThumb($json);
|
||||
$upd = $pdo->prepare(
|
||||
'UPDATE soon_files SET name = :n, json = :j, size = :s, version = :v, thumb = :th, updated_at = :ts '
|
||||
. 'WHERE id = :id AND version = :cv'
|
||||
@@ -137,17 +149,77 @@ final class FileService
|
||||
$pdo->commit();
|
||||
return ['id' => $id, 'name' => $name, 'size' => $newSize, 'version' => $newVersion, 'updated_at' => $now];
|
||||
} catch (\RuntimeException $e) {
|
||||
if ($pdo->inTransaction()) $pdo->rollBack();
|
||||
if ($e->getMessage() === 'not_found') Json::fail('not_found', '文件不存在', 404);
|
||||
if ($e->getMessage() === 'version_conflict') Json::fail('conflict', '版本冲突,请刷新后重试', 409);
|
||||
if ($e->getMessage() === 'quota_exceeded') Json::fail('quota_exceeded', '存储空间已满', 413);
|
||||
if ($pdo->inTransaction()) {
|
||||
$pdo->rollBack();
|
||||
}
|
||||
if ($e->getMessage() === 'not_found') {
|
||||
Json::fail('not_found', '文件不存在', 404);
|
||||
}
|
||||
if ($e->getMessage() === 'version_conflict') {
|
||||
Json::fail('conflict', '版本冲突,请刷新后重试', 409);
|
||||
}
|
||||
if ($e->getMessage() === 'quota_exceeded') {
|
||||
Json::fail('quota_exceeded', '存储空间已满', 413);
|
||||
}
|
||||
throw $e;
|
||||
} catch (\Throwable $e) {
|
||||
if ($pdo->inTransaction()) $pdo->rollBack();
|
||||
} catch (PDOException $e) {
|
||||
if ($pdo->inTransaction()) {
|
||||
$pdo->rollBack();
|
||||
}
|
||||
self::failDb($e);
|
||||
} catch (Throwable $e) {
|
||||
if ($pdo->inTransaction()) {
|
||||
$pdo->rollBack();
|
||||
}
|
||||
throw $e;
|
||||
}
|
||||
}
|
||||
|
||||
private static function normalizeName(string $name): string
|
||||
{
|
||||
$name = trim($name);
|
||||
if ($name === '') {
|
||||
return 'untitled.soon';
|
||||
}
|
||||
if (mb_strlen($name) > 190) {
|
||||
Json::fail('bad_request', '文件名过长', 400);
|
||||
}
|
||||
return $name;
|
||||
}
|
||||
|
||||
private static function assertJsonPayload(string $json): void
|
||||
{
|
||||
if ($json === '') {
|
||||
Json::fail('bad_request', '文件内容为空', 400);
|
||||
}
|
||||
$size = strlen($json);
|
||||
if ($size > self::MAX_JSON_BYTES) {
|
||||
Json::fail('payload_too_large', '文件内容过大(最大 32MB)', 413);
|
||||
}
|
||||
}
|
||||
|
||||
private static function extractThumb(string $json): string
|
||||
{
|
||||
try {
|
||||
return TemplateService::thumbFromSoonJson($json);
|
||||
} catch (Throwable) {
|
||||
return '';
|
||||
}
|
||||
}
|
||||
|
||||
/** @return never */
|
||||
private static function failDb(PDOException $e): void
|
||||
{
|
||||
$detail = $e->getMessage();
|
||||
if (str_contains($detail, 'max_allowed_packet') || str_contains($detail, 'Packet too large')) {
|
||||
Json::fail('payload_too_large', '文件过大,超出服务器数据库限制', 413);
|
||||
}
|
||||
if (str_contains($detail, 'Unknown column') || str_contains($detail, "doesn't exist")) {
|
||||
Json::fail('server_error', '数据库结构不完整,请导入 database/schema.sql 或执行 php scripts/migrate-schema.php', 500);
|
||||
}
|
||||
throw $e;
|
||||
}
|
||||
|
||||
public static function softDelete(int $userId, int $id): void
|
||||
{
|
||||
$stmt = Db::pdo()->prepare('UPDATE soon_files SET deleted_at = :ts WHERE id = :id AND user_id = :u');
|
||||
|
||||
@@ -96,3 +96,5 @@ require SOON_API_ROOT . '/Services/PaymentConfigLoader.php';
|
||||
require SOON_API_ROOT . '/Services/SettingsConfigLoader.php';
|
||||
\Soon\Api\Services\PaymentConfigLoader::reload($config);
|
||||
\Soon\Api\Core\Config::init($config);
|
||||
require SOON_API_ROOT . '/Core/ErrorHandler.php';
|
||||
\Soon\Api\Core\ErrorHandler::register();
|
||||
|
||||
Reference in New Issue
Block a user