重构 monorepo 并完善网页端订阅与首页体验

- 迁移为 frontend-web、frontend-electron、backend-web 与 docker 部署结构
- 网页端:订阅门禁二次弹窗、套餐/支付组件化、顶栏分组对齐
- 首页:最近文件与模板库布局优化,缩略图对齐,下载与删除操作
- 新增管理后台、支付与云端文件 API,更新 README 与项目规范

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
24kycj
2026-06-08 18:17:39 +08:00
parent 5814b7bc0e
commit 88c6ce8ccc
511 changed files with 189528 additions and 22804 deletions
+141
View File
@@ -0,0 +1,141 @@
<?php
declare(strict_types=1);
$host = getenv('SOON_DB_HOST') ?: 'mysql';
$port = (int)(getenv('SOON_DB_PORT') ?: 3306);
$user = getenv('SOON_DB_USER') ?: 'soondesign';
$pass = getenv('SOON_DB_PASS') ?: 'soondesign';
$db = getenv('SOON_DB_NAME') ?: 'soondesign';
try {
$pdo = new PDO(
sprintf('mysql:host=%s;port=%d;dbname=%s;charset=utf8mb4', $host, $port, $db),
$user,
$pass,
[PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION]
);
} catch (Throwable $e) {
fwrite(STDERR, 'migrate-dev-schema: ' . $e->getMessage() . PHP_EOL);
exit(1);
}
function columnExists(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;
}
$alters = [];
if (!columnExists($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 (!columnExists($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 (!columnExists($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 (!columnExists($pdo, 'users', 'admin_level')) {
$alters[] = "ALTER TABLE users ADD COLUMN admin_level ENUM('full','ops') DEFAULT NULL AFTER role";
}
foreach ($alters as $sql) {
$pdo->exec($sql);
fwrite(STDOUT, "migrate-dev-schema: {$sql}" . PHP_EOL);
}
if (columnExists($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, 'migrate-dev-schema: admin_level backfill for existing admins' . PHP_EOL);
}
if (columnExists($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_monthly', '月度订阅', '按月灵活使用,随时续订', 1999, $quota, $maxFiles, 30,
'["高清预览","成品打印","云端保存","导出设计文件","订阅周期:1 个月"]', 10, 1, 1],
['member_quarterly', '季度订阅', '连续三个月,更省心', 5299, $quota, $maxFiles, 90,
'["高清预览","成品打印","云端保存","导出设计文件","订阅周期:1 季"]', 15, 0, 1],
['member_yearly', '年度订阅', '全年畅享,性价比更高', 19999, $quota, $maxFiles, 365,
'["高清预览","成品打印","云端保存","导出设计文件","订阅周期:1 年"]', 20, 0, 1],
];
$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), '
. '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, 'migrate-dev-schema: 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, 'migrate-dev-schema: legacy plan copy refreshed' . PHP_EOL);
}