feat: 完成分类、worksheet 接口开发,资源迁移到七牛

This commit is contained in:
R524809
2026-10-16 17:32:23 +08:00
parent e6ded00a4c
commit 8827110f2f
107 changed files with 12775 additions and 125 deletions
+75 -1
View File
@@ -10,7 +10,7 @@ export const SERVER_CONFIG = Symbol('SERVER_CONFIG');
export const API_PREFIX = 'api/v1';
export type LogLevel = 'error' | 'warn' | 'info' | 'debug';
/** 规范化的启动配置;仅在服务端使用,包含不得下发或记录的 JWT 密钥。 */
/** 规范化的启动配置;仅在服务端使用,JWT 和微信密钥不得下发或记录。 */
export interface ServerConfig extends RuntimeOptions {
nodeEnv: 'development' | 'test' | 'production';
logLevel: LogLevel;
@@ -22,6 +22,13 @@ export interface ServerConfig extends RuntimeOptions {
jwtIssuer: string;
jwtAudience: string;
jwtExpiresInSeconds: number;
/** 微信凭据仅服务端持有,登录接入后两项必须完整。 */
wechatMiniAppId: string;
wechatMiniAppSecret: string;
databaseUrl: string;
/** 新品依据首次发布时间判断;公共资源域名缺失时不返回七牛对象地址。 */
worksheetNewWindowDays: number;
qiniuPublicDomain: string | null;
}
/** 按十进制整数字符串解析并检查范围,防止空串、科学计数法等被 Number 宽松接受。 */
@@ -78,6 +85,41 @@ export function getServerConfig(
if (!jwtIssuer || !jwtAudience) {
throw new Error('JWT_ISSUER and JWT_AUDIENCE must not be empty');
}
const wechatMiniAppId = (env.WECHAT_MINI_APP_ID ?? '').trim();
const wechatMiniAppSecret = (env.WECHAT_MINI_APP_SECRET ?? '').trim();
// 已接入真实登录;缺失凭据必须启动失败,错误不回显配置值。
if (!wechatMiniAppId || !wechatMiniAppSecret) {
throw new Error(
'WECHAT_MINI_APP_ID and WECHAT_MINI_APP_SECRET are required',
);
}
if (/\s/.test(wechatMiniAppId) || /\s/.test(wechatMiniAppSecret)) {
throw new Error(
'WECHAT_MINI_APP_ID and WECHAT_MINI_APP_SECRET must not contain whitespace',
);
}
const databaseUrl = env.DATABASE_URL ?? '';
try {
const url = new URL(databaseUrl);
if (
!['postgresql:', 'postgres:'].includes(url.protocol) ||
!url.hostname ||
url.pathname.length < 2 ||
url.hash ||
/\s/.test(databaseUrl)
)
throw new Error();
// 本阶段连接 public 中的既有 SQL 表,拒绝悄悄连接到其他 Prisma schema。
if (
url.searchParams.has('schema') &&
url.searchParams.get('schema') !== 'public'
)
throw new Error();
} catch {
throw new Error(
'DATABASE_URL must be a PostgreSQL connection URL using the public schema',
);
}
// 只接受可直接比较的完整 origin;路径、凭证、通配符不属于白名单项。
const corsOrigins = (env.CORS_ORIGINS ?? '')
.split(',')
@@ -113,6 +155,27 @@ export function getServerConfig(
'TRUSTED_PROXY_CIDRS must contain explicit proxy addresses or CIDRs',
);
}
const rawPublicDomain = (env.QINIU_PUBLIC_DOMAIN ?? '').trim();
let qiniuPublicDomain: string | null = null;
if (rawPublicDomain) {
try {
const url = new URL(rawPublicDomain);
if (
url.protocol !== 'https:' ||
url.username ||
url.password ||
url.pathname !== '/' ||
url.search ||
url.hash
)
throw new Error();
qiniuPublicDomain = url.origin;
} catch {
throw new Error(
'QINIU_PUBLIC_DOMAIN must be an HTTPS origin without credentials, paths or query',
);
}
}
return {
...getRuntimeOptions(env),
nodeEnv: nodeEnv as ServerConfig['nodeEnv'],
@@ -130,6 +193,17 @@ export function getServerConfig(
jwtSecret,
jwtIssuer,
jwtAudience,
wechatMiniAppId,
wechatMiniAppSecret,
databaseUrl,
worksheetNewWindowDays: integer(
env,
'WORKSHEET_NEW_WINDOW_DAYS',
30,
1,
365,
),
qiniuPublicDomain,
jwtExpiresInSeconds: integer(
env,
'JWT_EXPIRES_IN_SECONDS',