永久会员与模板库后台化:预览门控、轻量首页与 Admin CRUD

- 会员改为永久激活方案;云端文件不再拦截非会员;预览弹窗内导出/打印才校验
- 首页最近文件支持本地记录,登录后与云端合并;移除独立会员页与订阅页
- 模板库入库管理:soon_templates 表、Admin 上传 CRUD、/templates 轻量列表与按需下载

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
24kycj
2026-06-08 23:52:56 +08:00
parent 152228d41f
commit ebe191b06d
45 changed files with 1616 additions and 1259 deletions
+32 -21
View File
@@ -5,7 +5,6 @@ namespace Soon\Api\Services;
use Soon\Api\Core\Config;
use Soon\Api\Core\Db;
use Soon\Api\Core\Json;
/**
* 会员与配额服务。
@@ -16,7 +15,8 @@ final class MembershipService
{
$stmt = Db::pdo()->query(
'SELECT id, code, name, description, price_cents, quota_mb, max_files, duration_days, '
. 'features, sort_order, is_recommended, is_active FROM plans WHERE is_active = 1 '
. 'features, sort_order, is_recommended, is_active FROM plans '
. 'WHERE is_active = 1 AND code = "member_lifetime" AND price_cents > 0 '
. 'ORDER BY sort_order ASC, price_cents ASC'
);
$items = [];
@@ -42,15 +42,25 @@ final class MembershipService
if ($row) {
$plan = self::enrichPlan($row);
$expiresAt = (string)($row['subscription_expires_at'] ?? '');
$daysRemaining = self::daysUntil($expiresAt);
$plan['subscription'] = [
'id' => (int)$row['subscription_id'],
'started_at' => $row['subscription_started_at'] ?? null,
'expires_at' => $expiresAt,
'days_remaining' => $daysRemaining,
'status' => $daysRemaining <= 7 ? 'expiring' : 'active',
];
$durationDays = (int)($row['duration_days'] ?? 0);
$startedAt = $row['subscription_started_at'] ?? null;
if ($durationDays <= 0 || (string)($row['code'] ?? '') === 'member_lifetime') {
$plan['subscription'] = [
'type' => 'lifetime',
'activated_at' => $startedAt,
'status' => 'active',
];
} else {
$expiresAt = (string)($row['subscription_expires_at'] ?? '');
$daysRemaining = self::daysUntil($expiresAt);
$plan['subscription'] = [
'id' => (int)$row['subscription_id'],
'started_at' => $startedAt,
'expires_at' => $expiresAt,
'days_remaining' => $daysRemaining,
'status' => $daysRemaining <= 7 ? 'expiring' : 'active',
];
}
$plan['usage'] = $usage;
$plan['tier'] = 'member';
$plan['is_member'] = true;
@@ -69,7 +79,7 @@ final class MembershipService
$plan = self::enrichPlan([
'id' => 0,
'code' => 'free',
'name' => '免费版',
'name' => '普通用户',
'description' => '适合个人体验与轻量设计',
'price_cents' => 0,
'quota_mb' => $freeQuota,
@@ -105,13 +115,6 @@ final class MembershipService
return (bool)$stmt->fetchColumn();
}
public static function requireActiveMember(int $userId): void
{
if (!self::isActiveMember($userId)) {
Json::fail('membership_required', '此功能需要订阅后使用', 403);
}
}
/** @return array<int, array<string, mixed>> */
public static function recentOrders(int $userId, int $limit = 8): array
{
@@ -158,9 +161,15 @@ final class MembershipService
return ['items' => $items, 'total' => $total, 'page' => $page, 'size' => $size];
}
public static function previewAllowed(int $userId): bool
private const LIFETIME_EXPIRES = '9999-12-31 23:59:59';
public static function subscriptionExpiresAt(int $durationDays, ?int $baseTs = null): string
{
return self::isActiveMember($userId);
if ($durationDays <= 0) {
return self::LIFETIME_EXPIRES;
}
$baseTs = $baseTs ?? time();
return date('Y-m-d H:i:s', $baseTs + $durationDays * 86400);
}
public static function settings(): array
@@ -255,6 +264,8 @@ final class MembershipService
}
if ($durationDays > 0) {
$core[] = '订阅周期:' . self::periodLabel($durationDays);
} elseif ($code === 'member_lifetime' || $durationDays <= 0) {
$core[] = '永久有效';
}
return $core;
}