feat(framework): 回灌 OpenFlare 分层、安全与运行时改进

将平台域持久化收敛为 repository 唯一入口,model 去掉 IO。
邮件头写入前清除 CR/LF,防止 header 注入。
httppool 支持可配置 Transport;batchwriter 增加 MinBatchSize/Stats,flush 失败交回批次;任务 PermanentError 作为 SkipRetry 终态。
设置与推送页的确认改为 AlertDialog;axios 去尾斜杠并按 Gin 数组序列化查询参数。
升级共享 Go 依赖(Gin、Asynq、OTel、GORM、Redis 等)。
This commit is contained in:
ryan
2026-08-16 11:07:20 +08:00
parent b9b42e3174
commit 6a53619dd2
58 changed files with 2201 additions and 1175 deletions
+24 -1
View File
@@ -5,7 +5,7 @@ import axios, {
InternalAxiosRequestConfig,
} from 'axios';
import { toast } from 'sonner';
import { apiConfig } from './config';
import { apiConfig, getApiBaseUrl } from './config';
import {
ApiErrorBase,
ForbiddenError,
@@ -26,11 +26,28 @@ const apiClient = axios.create({
baseURL: apiConfig.baseURL,
timeout: apiConfig.timeout,
withCredentials: apiConfig.withCredentials,
// Fail fast on slash redirect loops between Next dev proxy and Gin legacy routes.
maxRedirects: 5,
// Gin QueryArray("hosts") expects hosts=a&hosts=b, not hosts[]=a.
paramsSerializer: {
indexes: null,
},
headers: {
'Content-Type': 'application/json',
},
});
/** Normalize legacy /api/* paths: strip trailing slash to avoid 308/301 loops in dev proxy. */
function normalizeApiPath(url?: string): string | undefined {
if (!url || !url.startsWith('/api/')) {
return url;
}
if (url.length > 5 && url.endsWith('/')) {
return url.replace(/\/+$/, '');
}
return url;
}
/**
* 请求取消令牌存储
*/
@@ -73,6 +90,12 @@ function getRequestKey(config: {
*/
apiClient.interceptors.request.use(
(config: InternalAxiosRequestConfig) => {
// Browser must use same-origin relative URLs so Session Cookie hits Next rewrite.
if (typeof window !== 'undefined') {
config.baseURL = getApiBaseUrl();
}
config.url = normalizeApiPath(config.url);
const requestKey = getRequestKey(config);
const source = axios.CancelToken.source();
config.cancelToken = source.token;