From 6b3fcc1791a8f94b9b91d2121b4078f9ea0fc4fa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=A0=91=E8=90=8C=E8=8A=BD?= <3205788256@qq.com> Date: Fri, 3 Apr 2026 16:10:12 +0800 Subject: [PATCH] security: stop tracking env files; admin gate via VITE_SITE_ADMIN_GATE; Redis/cache/docs Remove InfoGenie-frontend and Go .env from version control; add .env.example templates; ignore .claude local settings. Admin UI reads site gate from env only. Note: rotate secrets if repo history was ever public. Made-with: Cursor --- .gitignore | 8 + InfoGenie-frontend/.env.development | 7 - InfoGenie-frontend/.env.example | 7 + InfoGenie-frontend/.env.production | 7 - InfoGenie-frontend/src/components/Header.js | 9 +- .../src/components/SmallGameIcons.js | 159 +----- InfoGenie-frontend/src/config/env.js | 4 + InfoGenie-frontend/src/pages/AdminPage.js | 375 ++++++++++---- InfoGenie-frontend/前端文档.md | 75 +-- README.md | 489 ++---------------- infogenie-backend-go/.env.development | 30 -- infogenie-backend-go/.env.example | 22 + infogenie-backend-go/.gitignore | 7 +- infogenie-backend-go/ai_config.json | 13 - infogenie-backend-go/cmd/server/main.go | 8 + infogenie-backend-go/config/config.go | 69 +++ infogenie-backend-go/go.mod | 18 +- infogenie-backend-go/go.sum | 27 +- infogenie-backend-go/internal/cache/redis.go | 119 +++++ .../internal/handler/feature_card_clicks.go | 22 +- .../internal/handler/siteconfig.go | 121 ++++- .../internal/router/router.go | 13 + .../scripts/redis_keyspace/main.go | 84 +++ infogenie-backend-go/后端文档.md | 290 ++++++----- 万象口袋项目经历.md | 67 +-- 25 files changed, 1078 insertions(+), 972 deletions(-) delete mode 100644 InfoGenie-frontend/.env.development create mode 100644 InfoGenie-frontend/.env.example delete mode 100644 InfoGenie-frontend/.env.production delete mode 100644 infogenie-backend-go/.env.development create mode 100644 infogenie-backend-go/.env.example delete mode 100644 infogenie-backend-go/ai_config.json create mode 100644 infogenie-backend-go/internal/cache/redis.go create mode 100644 infogenie-backend-go/scripts/redis_keyspace/main.go diff --git a/.gitignore b/.gitignore index bc27e412..9723a872 100644 --- a/.gitignore +++ b/.gitignore @@ -5,6 +5,7 @@ debug-logs/ # IDE / OS .idea/ .vscode/ +.claude/settings.local.json .DS_Store Thumbs.db @@ -31,3 +32,10 @@ infogenie-backend-go/go.work* InfoGenie-backend/.env .env.local .env.*.local +**/.env +**/.env.development +**/.env.production +**/.env.local +# 允许提交模板(勿填真实密钥) +!infogenie-frontend/.env.example +!infogenie-backend-go/.env.example diff --git a/InfoGenie-frontend/.env.development b/InfoGenie-frontend/.env.development deleted file mode 100644 index 1d89ce1b..00000000 --- a/InfoGenie-frontend/.env.development +++ /dev/null @@ -1,7 +0,0 @@ -# 复制为 .env.local 后可按需覆盖本地开发值 -VITE_API_URL=http://127.0.0.1:5002 -VITE_AUTH_URL=https://auth.shumengya.top -VITE_AUTH_API_URL=https://auth.api.shumengya.top -VITE_NAME=InfoGenie -VITE_VERSION=1.0.0 -VITE_DEBUG=true diff --git a/InfoGenie-frontend/.env.example b/InfoGenie-frontend/.env.example new file mode 100644 index 00000000..d337d768 --- /dev/null +++ b/InfoGenie-frontend/.env.example @@ -0,0 +1,7 @@ +# 复制为 .env.development / .env.production 后填写;勿提交真实密钥 +VITE_API_URL=http://127.0.0.1:5002 +VITE_AUTH_URL=https://auth.shumengya.top +VITE_AUTH_API_URL=https://auth.api.shumengya.top +VITE_DEBUG=true +# 须与 Go 后端 INFOGENIE_SITE_ADMIN_TOKEN 一致 +VITE_SITE_ADMIN_GATE= diff --git a/InfoGenie-frontend/.env.production b/InfoGenie-frontend/.env.production deleted file mode 100644 index 5b26b406..00000000 --- a/InfoGenie-frontend/.env.production +++ /dev/null @@ -1,7 +0,0 @@ -# 生产前端环境变量 -VITE_API_URL=https://infogenie.api.shumengya.top -VITE_AUTH_URL=https://auth.shumengya.top -VITE_AUTH_API_URL=https://auth.api.shumengya.top -VITE_NAME=InfoGenie -VITE_VERSION=1.0.0 -VITE_DEBUG=false diff --git a/InfoGenie-frontend/src/components/Header.js b/InfoGenie-frontend/src/components/Header.js index 79f2e8d5..01353513 100755 --- a/InfoGenie-frontend/src/components/Header.js +++ b/InfoGenie-frontend/src/components/Header.js @@ -4,11 +4,10 @@ import { FiUser, FiMenu, FiX, FiLogOut } from 'react-icons/fi'; import { useUser } from '../contexts/UserContext'; import toast from 'react-hot-toast'; import clsx from 'clsx'; +import { ENV_CONFIG } from '../config/env'; const logoSrc = '/assets/logo.png'; -const ADMIN_TOKEN = 'shumengya520'; - const Header = () => { const { user, isLoggedIn, logout } = useUser(); const [isMenuOpen, setIsMenuOpen] = useState(false); @@ -27,7 +26,7 @@ const Header = () => { if (clickCountRef.current >= 5) { clickCountRef.current = 0; - if (localStorage.getItem('admin_token') === ADMIN_TOKEN) { + if (ENV_CONFIG.SITE_ADMIN_GATE && localStorage.getItem('admin_token') === ENV_CONFIG.SITE_ADMIN_GATE) { navigate('/admin'); } else { setAdminInput(''); @@ -44,8 +43,8 @@ const Header = () => { }, [navigate]); const handleAdminSubmit = () => { - if (adminInput === ADMIN_TOKEN) { - localStorage.setItem('admin_token', ADMIN_TOKEN); + if (ENV_CONFIG.SITE_ADMIN_GATE && adminInput === ENV_CONFIG.SITE_ADMIN_GATE) { + localStorage.setItem('admin_token', ENV_CONFIG.SITE_ADMIN_GATE); setShowAdminModal(false); toast.success('管理员验证通过'); navigate('/admin'); diff --git a/InfoGenie-frontend/src/components/SmallGameIcons.js b/InfoGenie-frontend/src/components/SmallGameIcons.js index 3e756014..1b3bbc64 100644 --- a/InfoGenie-frontend/src/components/SmallGameIcons.js +++ b/InfoGenie-frontend/src/components/SmallGameIcons.js @@ -1,146 +1,31 @@ import React from 'react'; -const accent = '#22c55e'; -const soft = 'rgba(74, 222, 128, 0.18)'; -const stroke = 1.8; +/** 与 StaticPageConfig SMALL_GAMES 的 id 对齐 */ +const GAME_EMOJI = { + '2048': { emoji: '🔢', label: '2048' }, + 'white-tile': { emoji: '🎹', label: '别踩白方块' }, + tetris: { emoji: '🧱', label: '俄罗斯方块' }, + snake: { emoji: '🐍', label: '贪吃蛇' }, + minesweeper: { emoji: '💣', label: '扫雷' }, + 'dodge-leaves': { emoji: '🍃', label: '躲树叶' }, + plane: { emoji: '✈️', label: '打飞机' }, + parkour: { emoji: '🏃', label: '跑酷' }, + floppybird: { emoji: '🐦', label: 'Floppy Bird' }, + h5cube: { emoji: '🧩', label: '网页魔方' }, + sokoban: { emoji: '📦', label: '推箱子' }, +}; -function SvgBox({ children, ...rest }) { +export function SmallGameIcon({ gameId }) { + const meta = GAME_EMOJI[gameId] || { emoji: '🎮', label: '小游戏' }; return ( - - {children} - + {meta.emoji} + ); } -/** 按 SMALL_GAMES 的 id 渲染矢量图标(纯 SVG,无 Emoji) */ -export function SmallGameIcon({ gameId }) { - switch (gameId) { - case '2048': - return ( - - - - - - - ); - case 'white-tile': - return ( - - - - - - ); - case 'tetris': - return ( - - - - - - - ); - case 'snake': - return ( - - - - - - ); - case 'minesweeper': - return ( - - - - - - - ); - case 'dodge-leaves': - return ( - - - - - ); - case 'plane': - return ( - - - - - - ); - case 'parkour': - return ( - - - - - - - ); - case 'floppybird': - return ( - - - - - - ); - case 'h5cube': - return ( - - - - - - ); - case 'sokoban': - return ( - - - - - - - ); - default: - return ( - - - - - ); - } -} - export default SmallGameIcon; diff --git a/InfoGenie-frontend/src/config/env.js b/InfoGenie-frontend/src/config/env.js index 221a6619..0f1d9450 100644 --- a/InfoGenie-frontend/src/config/env.js +++ b/InfoGenie-frontend/src/config/env.js @@ -45,6 +45,9 @@ const config = { LOG_LEVEL: 'debug', }; +/** 与后端 INFOGENIE_SITE_ADMIN_TOKEN 一致;不设则无法进入 /admin */ +const siteAdminGate = String(import.meta.env.VITE_SITE_ADMIN_GATE || '').trim(); + export const ENV_CONFIG = { API_URL: config.API_URL, AUTH_URL: config.AUTH_URL, @@ -55,6 +58,7 @@ export const ENV_CONFIG = { APP_VERSION: '2.2.3', CLIENT_ID: 'infogenie', CLIENT_NAME: '万象口袋', + SITE_ADMIN_GATE: siteAdminGate, }; export const getEnvVar = (key, defaultValue = '') => { diff --git a/InfoGenie-frontend/src/pages/AdminPage.js b/InfoGenie-frontend/src/pages/AdminPage.js index 7005a372..d1701ffc 100644 --- a/InfoGenie-frontend/src/pages/AdminPage.js +++ b/InfoGenie-frontend/src/pages/AdminPage.js @@ -11,8 +11,6 @@ import { ENV_CONFIG } from '../config/env'; import { API_CATEGORIES, TOOLBOX_ITEMS, API_SOURCES } from '../config/Api60sConfig'; import { AI_MODEL_APPS } from '../config/StaticPageConfig'; -const ADMIN_TOKEN = 'shumengya520'; - const InfoCard = ({ children }) => (
{children}
); @@ -52,6 +50,7 @@ function normalizeHealthPayload(data) { : data.database === 'connected'; const mysqlStatus = mysql?.status || data.database || (mysqlOk ? 'connected' : 'unknown'); const sixty = data.sixty_api && typeof data.sixty_api === 'object' ? data.sixty_api : null; + const redis = data.redis && typeof data.redis === 'object' ? data.redis : null; return { overall: data.status || 'unknown', ts: data.timestamp, @@ -59,11 +58,13 @@ function normalizeHealthPayload(data) { mysqlOk, mysqlStatus, sixty, + redis, }; } -const StatusPill = ({ ok, pending, unknown }) => { +const StatusPill = ({ ok, pending, unknown, muted, mutedText = '未启用' }) => { if (pending) return 检测中…; + if (muted) return {mutedText}; if (unknown) return 未上报; return ( { ); }; +function formatPasswordConfigured(v) { + if (v === true) return '已配置'; + if (v === false) return '未配置'; + return '—'; +} + const ADMIN_SECTIONS = [ { id: 'overview', label: '概览', short: '概览', Icon: FiHome }, + { id: 'monitoring', label: '状态监控', short: '状态', Icon: FiActivity }, { id: 'ai-upstream', label: 'AI 上游配置', short: 'AI 上游', Icon: FiCpu }, { id: 'sixty-node', label: '60s 数据源', short: '60s 节点', Icon: FiRadio }, { id: 'sixty-display', label: '60s 前台展示', short: '60s 展示', Icon: FiGrid }, @@ -114,6 +122,8 @@ const AdminPage = () => { const [healthRaw, setHealthRaw] = useState(null); const [healthLoading, setHealthLoading] = useState(false); const [healthFetchError, setHealthFetchError] = useState(null); + const [diagnostics, setDiagnostics] = useState(null); + const [diagError, setDiagError] = useState(null); const disabled60sSet = useMemo(() => new Set(disabled60s), [disabled60s]); const disabledAIModelsSet = useMemo(() => new Set(disabledAIModels), [disabledAIModels]); @@ -145,18 +155,46 @@ const AdminPage = () => { setLoading(false); }, []); - const fetchAggregateHealth = useCallback(async () => { + const fetchMonitoringData = useCallback(async () => { setHealthLoading(true); setHealthFetchError(null); - try { - const r = await fetch(`${ENV_CONFIG.API_URL}/api/health`); - if (!r.ok) throw new Error(`HTTP ${r.status}`); - const d = await r.json(); - setHealthRaw(d); - } catch (e) { - console.error(e); + setDiagError(null); + const token = localStorage.getItem('admin_token') || ''; + const healthP = fetch(`${ENV_CONFIG.API_URL}/api/health`).then(async (r) => { + if (!r.ok) throw new Error(`健康检查 HTTP ${r.status}`); + return r.json(); + }); + const diagP = fetch(`${ENV_CONFIG.API_URL}/api/admin/site/diagnostics`, { + headers: { 'X-Site-Admin-Token': token }, + }).then(async (r) => { + if (!r.ok) { + let msg = `HTTP ${r.status}`; + try { + const j = await r.json(); + if (j.error === 'forbidden') { + msg = '诊断接口拒绝访问(X-Site-Admin-Token 与后端 INFOGENIE_SITE_ADMIN_TOKEN 不一致)'; + } else if (j.message) { + msg = j.message; + } + } catch { /* ignore */ } + throw new Error(msg); + } + return r.json(); + }); + const [hSettled, dSettled] = await Promise.allSettled([healthP, diagP]); + if (hSettled.status === 'fulfilled') { + setHealthRaw(hSettled.value); + setHealthFetchError(null); + } else { setHealthRaw(null); - setHealthFetchError(e.message || '请求失败'); + setHealthFetchError(hSettled.reason?.message || '健康检查失败'); + } + if (dSettled.status === 'fulfilled') { + setDiagnostics(dSettled.value); + setDiagError(null); + } else { + setDiagnostics(null); + setDiagError(dSettled.reason?.message || '诊断加载失败'); } setHealthLoading(false); }, []); @@ -164,7 +202,7 @@ const AdminPage = () => { const healthNorm = useMemo(() => normalizeHealthPayload(healthRaw), [healthRaw]); const loadAiRuntime = useCallback(async () => { - if (localStorage.getItem('admin_token') !== ADMIN_TOKEN) return; + if (localStorage.getItem('admin_token') !== ENV_CONFIG.SITE_ADMIN_GATE || !ENV_CONFIG.SITE_ADMIN_GATE) return; setLoadingAi(true); try { const token = localStorage.getItem('admin_token') || ''; @@ -186,7 +224,7 @@ const AdminPage = () => { }, []); useEffect(() => { fetchSysInfo(); }, [fetchSysInfo]); - useEffect(() => { fetchAggregateHealth(); }, [fetchAggregateHealth]); + useEffect(() => { fetchMonitoringData(); }, [fetchMonitoringData]); useEffect(() => { load60sConfig(); }, [load60sConfig]); useEffect(() => { loadAiRuntime(); }, [loadAiRuntime]); @@ -319,7 +357,7 @@ const AdminPage = () => { }; const storedToken = localStorage.getItem('admin_token'); - if (storedToken !== ADMIN_TOKEN) return ; + if (!ENV_CONFIG.SITE_ADMIN_GATE || storedToken !== ENV_CONFIG.SITE_ADMIN_GATE) return ; const handleLogout = () => { localStorage.removeItem('admin_token'); @@ -440,96 +478,6 @@ const AdminPage = () => { ))} -

- - 状态监控 -

- -
-

- 数据源:GET /api/health - 。含 MySQL Ping 与当前配置的 60s 上游探测(请求其公开接口 /v2/ip)。 - {healthNorm?.overall === 'degraded' && ( - 当前整体为 degraded。 - )} -

- -
- {healthFetchError && ( -

- 无法连接健康检查接口:{healthFetchError}(请确认 Go 后端已启动且已更新到包含聚合健康字段的版本) -

- )} -
-
-
-
万象后端 API(InfoGenie Go)
-
{ENV_CONFIG.API_URL}
-
- -
-
-
-
MySQL 数据库
-
- {healthNorm ? `状态字段:${healthNorm.mysqlStatus}` : '—'} -
-
- -
-
-
-
60s API 上游(当前节点)
- {healthNorm?.sixty ? ( -
-
- {healthNorm.sixty.label || healthNorm.sixty.source_id} - {healthNorm.sixty.base_url ? ` · ${healthNorm.sixty.base_url}` : ''} -
- {typeof healthNorm.sixty.latency_ms === 'number' && ( -
探测耗时 {healthNorm.sixty.latency_ms} ms
- )} - {healthNorm.sixty.probe_url && ( -
探测 URL:{healthNorm.sixty.probe_url}
- )} - {healthNorm.sixty.error ? ( -
错误:{String(healthNorm.sixty.error)}
- ) : null} - {typeof healthNorm.sixty.http_status === 'number' && healthNorm.sixty.http_status > 0 ? ( -
HTTP {healthNorm.sixty.http_status}
- ) : null} -
- ) : ( -
- {!healthFetchError && healthRaw ? '响应中无 sixty_api 字段,请升级后端。' : '—'} -
- )} -
- -
-
-
-

系统信息

@@ -557,6 +505,223 @@ const AdminPage = () => { )} + {adminSection === 'monitoring' && ( + +
+

+ 实时探测: + GET /api/health + (MySQL Ping、60s 上游 /v2/ip + 、可选 Redis Ping)。 + 配置快照: + GET /api/admin/site/diagnostics + (监听地址、MySQL/Redis 连接目标、库名与用户、SMTP 与认证中心 URL 等;不含密码明文)。 + {healthNorm?.overall === 'degraded' && ( + 当前整体为 degraded。 + )} +

+ +
+ {healthFetchError && ( +

+ 健康检查失败:{healthFetchError} +

+ )} + {diagError && ( +

+ 配置快照失败:{diagError} +

+ )} +
+
+
+
万象后端 API(Go 进程)
+ +
+
+
前端当前 API 基址:{ENV_CONFIG.API_URL}
+ {diagnostics?.app && ( + <> +
+ 进程监听: + {diagnostics.app.listen_addr || `0.0.0.0:${diagnostics.app.listen_port || '—'}`} + {diagnostics.app.listen_port ? `(端口 ${diagnostics.app.listen_port})` : ''} +
+
APP_ENV:{diagnostics.app.env || '—'}
+ + )} +
+
+ +
+
+
MySQL 数据库
+ +
+
+ {healthNorm && ( +
实时状态字段:{healthNorm.mysqlStatus}
+ )} + {diagnostics?.mysql && ( + <> +
+ 连接: + {diagnostics.mysql.host || '—'}:{diagnostics.mysql.port || '—'} +
+
数据库名:{diagnostics.mysql.database || '—'}
+
用户:{diagnostics.mysql.user || '—'}
+
+ 密码(环境变量): + {formatPasswordConfigured(diagnostics.mysql.password_configured)} +
+ + )} +
+
+ +
+
+
60s API 上游
+ +
+
+ {diagnostics?.sixty_upstream && ( +
+ 配置的节点: + {diagnostics.sixty_upstream.label || diagnostics.sixty_upstream.source_id} + {diagnostics.sixty_upstream.base_url ? ` · ${diagnostics.sixty_upstream.base_url}` : ''} +
+ )} + {healthNorm?.sixty ? ( + <> +
+ 探测目标: + {healthNorm.sixty.label || healthNorm.sixty.source_id} + {healthNorm.sixty.base_url ? ` · ${healthNorm.sixty.base_url}` : ''} +
+ {typeof healthNorm.sixty.latency_ms === 'number' && ( +
探测耗时 {healthNorm.sixty.latency_ms} ms
+ )} + {healthNorm.sixty.probe_url && ( +
探测 URL:{healthNorm.sixty.probe_url}
+ )} + {healthNorm.sixty.error ? ( +
错误:{String(healthNorm.sixty.error)}
+ ) : null} + {typeof healthNorm.sixty.http_status === 'number' && healthNorm.sixty.http_status > 0 ? ( +
HTTP {healthNorm.sixty.http_status}
+ ) : null} + + ) : ( +
+ {!healthFetchError && healthRaw ? '响应中无 sixty_api 字段,请升级后端。' : '—'} +
+ )} +
+
+ +
+
+
Redis 缓存
+ {healthLoading && !healthRaw && !healthFetchError ? ( + + ) : healthNorm?.redis == null && healthRaw && !healthFetchError ? ( + + ) : healthNorm?.redis && healthNorm.redis.enabled === false ? ( + + ) : healthNorm?.redis && healthNorm.redis.enabled === true ? ( + + ) : ( + + )} +
+
+ {!healthFetchError && healthRaw && healthNorm && healthNorm.redis == null && ( +
响应中无 redis 字段,请升级 Go 后端。
+ )} + {healthNorm?.redis && healthNorm.redis.enabled === false && ( +
+ 后端未启用(REDIS_ENABLED 未打开);读站点配置时直连 MySQL。 +
+ )} + {healthNorm?.redis && healthNorm.redis.enabled === true && healthNorm.redis.ok === false && healthNorm.redis.error && ( +
Ping 错误:{String(healthNorm.redis.error)}
+ )} + {healthNorm?.redis && healthNorm.redis.enabled === true && healthNorm.redis.ok === true && ( +
健康检查 Redis Ping 成功(与业务共用同一套连接参数)。
+ )} + {diagnostics?.redis && ( + <> +
开关:{diagnostics.redis.enabled ? '已启用' : '未启用'}
+ {diagnostics.redis.enabled ? ( + <> +
地址(host:port):{diagnostics.redis.addr || '—'}
+
逻辑库编号:{typeof diagnostics.redis.logical_db === 'number' ? diagnostics.redis.logical_db : '—'}
+
Key 前缀:{diagnostics.redis.key_prefix || '—'}
+
+ 站点缓存 TTL: + {typeof diagnostics.redis.site_cache_ttl_sec === 'number' + ? `${diagnostics.redis.site_cache_ttl_sec} s` + : '—'} +
+
+ 密码(环境变量): + {formatPasswordConfigured(diagnostics.redis.password_configured)} +
+ + ) : null} + + )} +
+
+ + {(diagnostics?.auth_center || diagnostics?.mail) && ( +
+
其他依赖(快照)
+ {diagnostics.auth_center && ( +
+
萌芽认证中心 API
+
{diagnostics.auth_center.api_url || '—'}
+
+ )} + {diagnostics.mail && ( +
+
邮件 SMTP
+
+ {diagnostics.mail.host || '—'}:{diagnostics.mail.port ?? '—'} +
+
发件账号:{diagnostics.mail.username || '—'}
+
+ 密码(环境变量):{formatPasswordConfigured(diagnostics.mail.password_configured)} +
+
+ )} +
+ )} +
+
+ )} + {adminSection === 'ai-upstream' && (

diff --git a/InfoGenie-frontend/前端文档.md b/InfoGenie-frontend/前端文档.md index d5a37150..26abd717 100644 --- a/InfoGenie-frontend/前端文档.md +++ b/InfoGenie-frontend/前端文档.md @@ -10,7 +10,7 @@ | 命令 | 说明 | |------|------| -| `npm run dev` / `npm start` | 开发服务器(默认端口 3000) | +| `npm run dev` / `npm start` | 开发服务器(Vite,默认端口 3000) | | `npm run build` | 生产构建,输出 `dist/` | | `npm run preview` | 本地预览 `dist/` 构建结果 | @@ -23,7 +23,7 @@ infogenie-frontend/ ├── index.html ← Vite HTML 入口(根目录,非 public/) ├── vite.config.js ← Vite 配置(React 插件、Tailwind 插件、路径别名) ├── package.json -├── .env.development ← 开发环境变量 +├── .env.development ← 开发环境变量(VITE_*) ├── .env.production ← 生产环境变量 ├── public/ ← 静态资源(不参与构建,直接复制到 dist/) │ ├── assets/logo.png @@ -34,12 +34,12 @@ infogenie-frontend/ └── src/ ├── index.js ← React 根挂载 ├── App.js ← 路由、布局、全局 Provider - ├── components/ ← 公共组件 - ├── pages/ ← 页面组件 + ├── components/ ← 公共组件(含 FullscreenEmbed、SmallGameIcons 等) + ├── pages/ ← 页面组件(含 AdminPage、SmallGamePage) ├── contexts/ ← 全局 Context - ├── hooks/ ← 自定义 Hooks + ├── hooks/ ← 自定义 Hooks(如 useFeatureCardClickStats) ├── utils/ ← Axios 封装、可见性过滤等 - ├── config/ ← 环境变量解析、路由/内容配置 + ├── config/ ← 环境变量解析、Api60sConfig、StaticPageConfig └── styles/ ├── index.css ← Tailwind 入口 + 全局 base 样式 + 自定义动画 └── shared.js ← 设计系统组件(Tailwind 函数组件) @@ -52,7 +52,7 @@ infogenie-frontend/ | 特性 | 说明 | |------|------| | `@vitejs/plugin-react` | React Fast Refresh + JSX 转换(含 `.js` 扩展名支持) | -| `@tailwindcss/vite` | Tailwind CSS v4 Vite 插件,零配置文件 | +| `@tailwindcss/vite` | Tailwind CSS v4 Vite 插件 | | `treat-js-as-jsx` | 内联插件,使 `.js` 文件中的 JSX 正常编译 | | `resolve.alias['@']` | `@/` 映射到 `src/` | | `base: '/'` | 部署根路径 | @@ -62,18 +62,19 @@ infogenie-frontend/ ## 环境变量(`src/config/env.js`) -变量名使用 Vite 规范的 `VITE_` 前缀(通过 `import.meta.env` 访问): +变量名使用 Vite 规范的 **`VITE_`** 前缀(通过 `import.meta.env` 访问): | 变量 | 作用 | 开发默认 / 生产默认 | |------|------|---------------------| -| `VITE_API_URL` | 万象口袋 **Go 后端** 根地址 | dev:`http://127.0.0.1:5002`;prod:`https://infogenie.api.shumengya.top` | +| `VITE_API_URL` | 万象口袋 **Go 后端** 根地址 | dev:`http://127.0.0.1:5002`;prod 见 `.env.production` | | `VITE_AUTH_URL` | 认证中心 **页面** 域名 | `https://auth.shumengya.top` | | `VITE_AUTH_API_URL` | 认证中心 **API** 根地址 | `https://auth.api.shumengya.top` | | `VITE_DEBUG` | 调试开关 | `'true'` 开启 | +| `VITE_SITE_ADMIN_GATE` | 管理员后台口令,须与后端 `INFOGENIE_SITE_ADMIN_TOKEN` 一致 | 本地写在 `.env.development`,勿提交 | -运行时可通过 `window.ENV_CONFIG` 查看解析结果。 +运行时可通过 `window.ENV_CONFIG` 查看解析结果(若已注入)。 -> **迁移注意**:从 CRA 迁移时原 `REACT_APP_*` 变量已全部重命名为 `VITE_*`。 +> **迁移注意**:由 CRA 迁出后,原 `REACT_APP_*` 已统一为 `VITE_*`。 --- @@ -91,13 +92,27 @@ infogenie-frontend/ | `/toolbox` | 工具箱 | | `/aimodel` | AI 应用(需登录) | | `/profile` | 个人中心 | -| `/admin` | 管理 | +| `/admin` | **管理员后台**(本地 `admin_token` 校验) | | `*` | 重定向首页 | -### 关键组件 +### 管理员后台(`src/pages/AdminPage.js`) -- **`RandomSiteBackground`**:全站随机背景(`https://randbg.api.smyhub.com/api/random?format=json`),毛玻璃模糊,会话级缓存。 -- **`Header` / `Footer`**:半透明绿 + `backdrop-filter`;**移动端 `Navigation`** 底栏为不透明渐变 + 圆角顶。 +- 浏览器 **`localStorage.admin_token`** 与内置口令一致方可进入(与后端 **`INFOGENIE_SITE_ADMIN_TOKEN`** 用于接口时是同一套站点管理员概念)。 +- **功能分区**(左侧):概览、**状态监控**、AI 上游、60s 数据源、60s 前台展示、AI 前台展示。 +- **状态监控**: + - 并行请求 **`GET /api/health`**(MySQL / 60s 上游 / Redis Ping)与 **`GET /api/admin/site/diagnostics`**(进程监听、MySQL/Redis 连接目标、库名/用户、SMTP、认证中心 URL 等;**不含密码**)。 + - 请求诊断接口时携带头 **`X-Site-Admin-Token: `**。 + - 「刷新探测与配置」会同时刷新上述两个数据源。 + +### 休闲游戏页(`src/pages/SmallGamePage.js`) + +- 游戏列表来自 **`src/config/StaticPageConfig.js`** 的 `SMALL_GAMES`。 +- 卡片图标由 **`src/components/SmallGameIcons.js`** 按 `game.id` 渲染 **Emoji**(如 2048→🔢、俄罗斯方块→🧱 等),嵌入在 **`FeatureCardIcon`** 内。 + +### 其他关键组件 + +- **`RandomSiteBackground`**:全站随机背景,毛玻璃模糊,会话级缓存。 +- **`Header` / `Footer` / `Navigation`**:导航与布局。 - **`FullscreenEmbed`**:全屏 iframe(游戏、工具箱、AI 静态页),支持注入 token、加载超时提示。 - **`ParticleEffect`**:全局点击粒子动画。 @@ -119,17 +134,12 @@ infogenie-frontend/ ### 设计系统(`src/styles/shared.js`) -所有组件均为 **React 函数组件**,接受 `className` prop(可与额外 Tailwind 类合并): +所有组件均为 **React 函数组件**,接受 `className` prop(可与 Tailwind 类合并): -- **`PageWrapper`**:页面容器,带 `animate-page-enter` 入场动画 -- **`Container`**:最大宽度容器,`$narrow`(800px)/ 默认(1200px) -- **`FeatureGrid`**:响应式 5→4→3→2 列网格,带 `animate-fade-up` -- **`CatalogCard`** / **`FeatureCard`**:统一卡片样式,顶条渐变色动态注入(`$c` prop) +- **`PageWrapper`**、**`Container`**、**`FeatureGrid`**、**`CatalogCard`**、**`FeatureCard`** - **`FeatureCardUseCount`**:卡片右上角点击次数角标 -- **`ModuleCard`**:首页大模块横向卡片(包装 `react-router-dom` `Link`) -- **`accentFromGradient`**:工具函数,从 gradient 字符串提取首色 - -> 动态样式(渐变色、动态 grid 列数等)通过 inline `style` prop 注入,Tailwind 负责静态部分。 +- **`ModuleCard`**:首页大模块卡片(`Link`) +- **`accentFromGradient`**:从 gradient 字符串提取首色 --- @@ -146,9 +156,9 @@ infogenie-frontend/ 与 Vite 主站并列的大量**免构建**页面: - **`public/aimodelapp/<应用名>/`**:AI 小应用(HTML + `script.js` + 可选 `env.js`)。 - - 统一聊天入口:**`public/aimodelapp/shared/ai-chat.js`** 的 `AiChat.complete()`。 + - 统一聊天入口:**`public/aimodelapp/shared/ai-chat.js`**。 - **优先**请求 **`POST /api/aimodelapp/chat/stream`**(SSE),失败或无内容时回退 **`POST /api/aimodelapp/chat`**。 -- **`public/smallgame/`**、**`public/toolbox/`**:独立小游戏与工具页,由 `FullscreenEmbed` 打开。 +- **`public/smallgame/`**、**`public/toolbox/`**:独立页,由 **`FullscreenEmbed`** 打开。 各 `aimodelapp` 子目录的 **`env.js`** / **`API_CONFIG`** 需指向与主站一致的 Go 后端地址(通常与 `VITE_API_URL` 同源或同网段)。 @@ -156,14 +166,15 @@ infogenie-frontend/ ## 与后端协作要点 -1. **登录**:token 存 `localStorage`,AI 与受保护接口依赖 JWT。 -2. **AI**:静态页通过 **同源或配置的 API** 调 Go 的 `/api/aimodelapp/*`;流式响应类型为 **`text/event-stream`**。 -3. **站点开关**:60s / AI 应用显隐由 `GET /api/site/*-disabled` 等驱动(见后端文档)。 -4. **卡片统计**:四大板块列表页的 `CatalogCard` 已接 `feature-card-clicks` 接口。 +1. **登录**:token 存 `localStorage`,AI 与受保护接口依赖 JWT(萌芽认证中心签发)。 +2. **AI**:静态页通过配置的 API 调 Go 的 `/api/aimodelapp/*`;流式为 **`text/event-stream`**。 +3. **站点开关**:60s / AI 应用显隐由 `GET /api/site/*-disabled` 等驱动。 +4. **卡片统计**:列表页 `CatalogCard` 与 **`feature-card-clicks`** 接口对接;可选 Redis 缓存由后端控制。 +5. **管理员**:写配置类接口需后端配置 **`INFOGENIE_SITE_ADMIN_TOKEN`**,与前端管理员口令一致。 --- ## 其他文档 -- **Go 后端 API 与表结构**:[`infogenie-backend-go/后端文档.md`](../infogenie-backend-go/后端文档.md) -- **仓库与工具说明**:[`../.claude/README.md`](../.claude/README.md) +- **Go 后端 API、Redis、健康与诊断**:[`infogenie-backend-go/后端文档.md`](../infogenie-backend-go/后端文档.md) +- **仓库总览与多模块说明**:[`../README.md`](../README.md) diff --git a/README.md b/README.md index d08ee83a..acefc408 100755 --- a/README.md +++ b/README.md @@ -1,472 +1,77 @@ -# InfoGenie 前端架构文档 +# InfoGenie(万象口袋) -## 项目概述 +多模块仓库:**React + Vite 前端**为主界面,**Go(Gin + GORM)** 为当前主力 API,另有 **Java Spring Boot** 占位模块。大量 **60s 类接口**通过上游开放 API 提供;本站负责聚合导航、认证跳转、AI 应用壳与小游戏嵌入等。 -InfoGenie 是一个基于前后端分离架构的全栈 Web 应用,前端采用 React 单页应用(SPA)架构,结合静态 HTML 页面实现丰富的功能模块。后端提供 RESTful API 接口,支持用户认证、数据获取等核心功能。项目实现了移动优先的响应式设计,通过统一的组件系统和数据流管理,提供了包括API数据展示、小游戏、AI工具等多样化功能模块。 +--- -## 技术栈 +## 仓库结构 -### 核心框架 -- **React 18.2.0**: 前端 UI 框架,使用函数式组件和 Hooks -- **React Router DOM 6.15.0**: 客户端路由管理 -- **Axios 1.5.0**: HTTP 客户端,用于后端 API 调用 +| 目录 | 说明 | +|------|------| +| [`infogenie-frontend/`](infogenie-frontend/) | 主站 SPA(Vite 6、React 18、Tailwind v4) | +| [`infogenie-backend-go/`](infogenie-backend-go/) | **万象后端 API**(Go):站点配置、AI 代理、健康检查、可选 Redis 缓存等 | +| [`infogenie-backend-java/`](infogenie-backend-java/) | Spring Boot 3.5 骨架(当前无业务 API 层) | +| [`InfoGenie-go-backend/`](InfoGenie-go-backend/) | 历史/备用目录,通常仅 README | -### 样式和 UI -- **styled-components 6.0.7**: CSS-in-JS 样式解决方案 -- **react-icons 4.11.0**: 图标库 -- **react-hot-toast 2.4.1**: 通知提示组件 +详细 API、表结构、Redis 与诊断接口见 **[infogenie-backend-go/后端文档.md](infogenie-backend-go/后端文档.md)**。 -### 开发工具 -- **Create React App**: 项目脚手架 -- **ESLint**: 代码规范检查 -- **Service Worker**: PWA 支持 +前端路由、Vite、管理员「状态监控」、静态页协作见 **[infogenie-frontend/前端文档.md](infogenie-frontend/前端文档.md)**。 -## 架构设计 +开发与 AI 助手指南见根目录 **[CLAUDE.md](CLAUDE.md)**(若存在)。 -### 整体架构 -``` -前端应用 -├── React SPA (主要页面) -│ ├── 用户认证系统 -│ ├── 导航和布局 -│ ├── 页面路由 -│ └── 用户管理 -└── 静态 HTML 页面 - ├── API 数据展示页面 - ├── 小游戏页面 - └── AI 模型工具页面 -``` +--- -### 文件结构 -``` -src/ -├── components/ # 公共组件 -│ ├── Header.js # 顶部导航栏 -│ ├── Navigation.js # 底部导航栏(移动端) -│ └── Footer.js # 页脚 -├── pages/ # 页面组件 -│ ├── HomePage.js # 首页 -│ ├── LoginPage.js # 登录页面 -│ ├── Api60sPage.js # API 60s 页面 -│ ├── SmallGamePage.js # 小游戏页面 -│ ├── AiModelPage.js # AI 模型页面 -│ └── UserProfilePage.js # 用户资料页面 -├── contexts/ # React Context -│ └── UserContext.js # 用户状态管理 -├── config/ # 配置文件 -│ └── StaticPageConfig.js # 静态页面配置 -├── utils/ # 工具函数 -│ └── api.js # API 调用封装 -└── styles/ # 全局样式 -``` +## 快速启动(本地) -## API 接口设计 +### 前端(`infogenie-frontend/`) -### 基础配置 -- **Base URL**: `https://infogenie.api.shumengya.top` (这是生产环境)(可通过环境变量 `REACT_APP_API_URL` 配置测试环境) -- **认证方式**: JWT Bearer Token -- **请求格式**: JSON -- **响应格式**: JSON -- **超时时间**: 10秒 - -### 认证相关接口 - -#### 发送验证码 -```http -POST /api/auth/send-verification -Content-Type: application/json - -{ - "email": "user@example.com" -} -``` - -#### 验证验证码 -```http -POST /api/auth/verify-code -Content-Type: application/json - -{ - "email": "user@example.com", - "code": "123456" -} -``` - -#### 用户登录 -```http -POST /api/auth/login -Content-Type: application/json - -{ - "email": "user@example.com", - "password": "password" -} -``` - -#### 用户注册 -```http -POST /api/auth/register -Content-Type: application/json - -{ - "email": "user@example.com", - "password": "password", - "verification_code": "123456" -} -``` - -#### 用户登出 -```http -POST /api/auth/logout -Authorization: Bearer -``` - -#### 检查登录状态 -```http -GET /api/auth/check -Authorization: Bearer -``` - -### 用户管理接口 - -#### 获取用户资料 -```http -GET /api/user/profile -Authorization: Bearer -``` - -#### 修改密码 -```http -POST /api/user/change-password -Authorization: Bearer -Content-Type: application/json - -{ - "old_password": "old_password", - "new_password": "new_password" -} -``` - -#### 获取用户统计 -```http -GET /api/user/stats -Authorization: Bearer -``` - -#### 获取游戏数据 -```http -GET /api/user/game-data -Authorization: Bearer -``` - -#### 用户签到 -```http -POST /api/user/checkin -Authorization: Bearer -``` - -#### 删除账户 -```http -POST /api/user/delete -Authorization: Bearer -Content-Type: application/json - -{ - "password": "password" -} -``` - -### 数据展示接口 - -前端包含大量静态页面用于展示各种 API 数据,这些页面直接调用后端提供的公开接口: - -#### 热搜榜单系列 -- 百度实时热搜: `GET /v2/baidu/realtime` -- 百度贴吧话题榜: `GET /v2/baidu/tieba` -- 哔哩哔哩热搜榜: `GET /v2/bilibili/hot` -- 抖音热搜榜: `GET /v2/douyin/hot` -- 头条热搜榜: `GET /v2/toutiao/hot` -- 微博热搜榜: `GET /v2/weibo/hot` -- 小红书热点: `GET /v2/xiaohongshu/hot` -- 知乎热门话题: `GET /v2/zhihu/hot` -- Hacker News 榜单: `GET /v2/hackernews` - -#### 日更资讯系列 -- 必应每日壁纸: `GET /v2/bing/wallpaper` -- 历史上的今天: `GET /v2/history/today` -- 每日国际汇率: `GET /v2/exchange/rates` -- 每天60s读懂世界: `GET /v2/60s/world` - -#### 实用功能系列 -- 百度百科词条: `GET /v2/baike/search?keyword={keyword}` -- 公网IP地址: `GET /v2/ip/public` -- 哈希解压压缩: `POST /v2/hash/{algorithm}` -- 链接OG信息: `GET /v2/og?url={url}` -- 密码强度检测: `POST /v2/password/strength` -- 农历信息: `GET /v2/calendar/lunar?date={date}` -- 配色方案: `GET /v2/color/schemes` -- 身体健康分析: `POST /v2/health/analysis` -- 生成二维码: `POST /v2/qrcode/generate` -- 实时天气: `GET /v2/weather?location={location}` -- 随机密码生成器: `GET /v2/password/random` -- 随机颜色: `GET /v2/color/random` -- 天气预报: `GET /v2/weather/forecast?location={location}` -- 在线翻译: `POST /v2/translate` -- EpicGames免费游戏: `GET /v2/epic/free-games` - -#### 娱乐消遣系列 -- 随机唱歌音频: `GET /v2/entertainment/random-song` -- 随机发病文学: `GET /v2/entertainment/random-meme` -- 随机搞笑段子: `GET /v2/entertainment/random-joke` -- 随机冷笑话: `GET /v2/entertainment/random-pun` -- 随机一言: `GET /v2/entertainment/random-quote` -- 随机运势: `GET /v2/entertainment/random-fortune` -- 随机JavaScript趣味题: `GET /v2/entertainment/random-js-quiz` -- 随机KFC文案: `GET /v2/entertainment/random-kfc` - -## 状态管理 - -### 用户状态管理 -使用 React Context 进行全局状态管理: - -```javascript -const UserContext = createContext(); - -export const UserProvider = ({ children }) => { - const [user, setUser] = useState(null); - const [isLoading, setIsLoading] = useState(true); - const [isLoggedIn, setIsLoggedIn] = useState(false); - - // 用户登录、登出、状态检查等方法 -}; -``` - -### 本地存储 -- 用户信息和 Token 存储在 localStorage 中 -- 页面刷新后自动恢复用户状态 - -## 路由设计 - -```javascript -const App = () => { - return ( - - - } /> - } /> - } /> - } /> - } /> - } /> - } /> - - - ); -}; -``` - -## 响应式设计 - -- 移动优先设计理念 -- 使用 CSS Grid 和 Flexbox 实现响应式布局 -- 媒体查询适配不同屏幕尺寸 -- 移动端使用底部导航栏,桌面端使用顶部导航 - -## 安全考虑 - -### 前端安全措施 -- JWT Token 自动过期和刷新 -- XSS 防护:使用 React 自动转义 -- CSRF 防护:使用 SameSite Cookie -- 输入验证:前端表单验证 - -### API 安全要求 -- 所有敏感接口需要 JWT 认证 -- Token 存储在 localStorage,需要后端验证 -- 密码等敏感信息前端不存储明文 -- API 请求包含 CORS 配置 - -## 部署和构建 - -### 构建命令 ```bash -npm run build # 生产环境构建 -npm start # 开发环境启动 +cd infogenie-frontend +npm install +npm run dev ``` -### 环境变量 -- `REACT_APP_API_URL`: 后端 API 基础地址 -- 支持 `.env` 文件配置不同环境的变量 +默认 **http://localhost:3000**。环境变量见 `infogenie-frontend/.env.development`(`VITE_API_URL` 默认指向本机 Go **5002**)。 -### PWA 支持 -- 注册 Service Worker 实现离线缓存 -- Web App Manifest 支持安装到桌面 +### Go 后端(`infogenie-backend-go/`) -## 与后端协作要点 - -1. **API 接口约定**: 遵循 RESTful 设计原则,统一响应格式 -2. **错误处理**: 后端返回统一的错误格式,前端统一处理 -3. **认证流程**: JWT Token 的生成、验证和刷新机制 -4. **数据格式**: 前后端约定清晰的数据结构 -5. **跨域配置**: 后端需要配置 CORS 允许前端域名 -6. **API 版本管理**: 使用 `/v2/` 前缀进行版本控制 -7. **性能优化**: 考虑 API 响应时间和前端缓存策略 - -## 萌芽币消费系统 - -### 系统概述 -萌芽币是平台内部的虚拟货币,用于限制和管理用户对AI功能的使用频率。每次调用AI功能需消耗100萌芽币,当用户萌芽币不足时,无法使用AI功能。 - -### 技术实现 -1. **萌芽币管理器**: `/public/aimodelapp/coin-manager.js` - - 管理用户萌芽币余额和使用记录 - - 提供UI组件显示萌芽币信息 - - 实现API调用前的余额检查 - -2. **API集成**: - - 在 `/src/utils/api.js` 中添加萌芽币查询接口 - - 所有AI功能API调用必须添加JWT Token认证 - - API调用后自动刷新萌芽币余额显示 - -3. **用户体验**: - - 在页面右上角显示萌芽币余额和使用记录 - - 当萌芽币不足时,提供友好的提示 - - 引导用户通过签到等方式获取更多萌芽币 - -### 接口设计 -```http -GET /api/aimodelapp/coins -Authorization: Bearer - -响应: -{ - "success": true, - "data": { - "coins": 200, - "ai_cost": 100, - "can_use_ai": true, - "username": "用户名", - "usage_count": 5, - "recent_usage": [ - { - "api_type": "chat", - "cost": 100, - "timestamp": "2025-09-16T11:15:47.285720" - }, - ... - ] - }, - "message": "当前萌芽币余额: 200" -} +```bash +cd infogenie-backend-go +# 按需配置 .env.development(APP_ENV、DB_*、可选 REDIS_*) +go run ./cmd/server ``` -### 页面集成流程 -1. 引入萌芽币管理器 JavaScript 文件 -2. 在API调用前检查萌芽币余额 -3. 处理API响应中的萌芽币相关错误 -4. API调用后刷新萌芽币信息 +默认监听 **http://127.0.0.1:5002**。健康检查:**GET /api/health**。 -详细集成步骤请参考 [前端萌芽币消费系统集成文档](/前端萌芽币消费系统集成文档.md) +### Java 后端(可选) -## 技术架构亮点 +```bash +cd infogenie-backend-java +mvn spring-boot:run +``` -### 1. 混合SPA与静态页面的创新架构 +--- -InfoGenie 采用了创新的混合架构设计,将 React SPA 与大量静态 HTML 页面无缝集成,充分发挥两者的优势: -- **React SPA 核心层**:处理用户认证、全局状态管理和主要导航逻辑,确保一致的用户体验 -- **静态 HTML 模块**:大量功能模块(API数据展示、小游戏、AI工具)使用原生HTML/CSS/JS实现,降低加载时间和资源消耗 -- **通信机制**:通过 postMessage API 和共享环境配置实现 SPA 与静态页面的数据交换和状态同步 +## 环境变量要点 -### 2. 前端性能优化策略 +| 模块 | 说明 | +|------|------| +| 前端 | `VITE_API_URL`、`VITE_AUTH_URL`、`VITE_AUTH_API_URL`、`VITE_DEBUG`(见前端文档) | +| Go | `APP_ENV`、`APP_PORT`、`DB_*`、`INFOGENIE_SITE_ADMIN_TOKEN`、可选 `REDIS_*`、`AUTH_CENTER_API_URL` 等(见后端文档) | -项目实现了全面的性能优化,确保在各种设备上的流畅体验: -- **代码分割**:基于路由的动态导入,减少首屏加载时间 -- **资源预加载**:关键资源预加载与懒加载策略结合 -- **缓存策略**:通过 Service Worker 实现静态资源和API响应的智能缓存 -- **性能监控**:页面加载性能关键指标(FCP、LCP、CLS)的实时监控与分析 +**不要**将含真实密码的 `.env` 提交到版本库。 -### 3. 模块化与组件设计 +--- -采用高度模块化的组件设计模式,提升代码可维护性和扩展性: -- **原子设计系统**:从原子级别组件到页面级别的多层组件设计体系 -- **状态与UI分离**:清晰的关注点分离,提高组件复用性 -- **统一样式系统**:基于 styled-components 的主题化设计系统 -- **组件文档化**:关键组件的详细使用说明和示例代码 +## 对外能力摘要(Go) -### 4. 安全与用户体验融合 +- **用户**:JWT 校验;登录由萌芽认证中心完成;`/api/user/profile` 等。 +- **站点**:60s / AI 显隐、60s 上游、功能卡片点击统计;管理员 `X-Site-Admin-Token` 写入配置。 +- **AI**:`/api/aimodelapp/chat`、`/chat/stream`(SSE)及多个垂直能力;模型白名单在后端维护。 +- **可观测性**:`/api/health`;管理员 **`/api/admin/site/diagnostics`**(连接摘要,无密码明文)。 -通过创新的用户体验设计,在保证安全性的同时提供流畅体验: -- **无感刷新认证**:JWT Token 自动刷新机制,避免频繁登录 -- **智能错误处理**:集中式的错误处理系统,提供用户友好的错误提示 -- **渐进式数据加载**:重要数据优先加载,提升感知性能 -- **离线访问支持**:关键功能支持离线访问,增强用户体验 +--- -## 后续扩展建议 +## 许可证与备注 -1. **状态管理升级**: 可考虑引入 Redux 或 Zustand 进行更复杂的状态管理 -2. **组件库**: 可引入 Ant Design 或 Material-UI 统一 UI 组件 -3. **测试覆盖**: 添加单元测试和集成测试,提高代码质量和可靠性 - - Jest 与 React Testing Library 进行组件测试 - - Cypress 进行端到端测试 - - MSW 进行 API 模拟测试 -4. **性能监控**: 集成 Lighthouse CI 和 Web Vitals 进行性能监控 -5. **国际化**: 使用 i18next 支持多语言切换功能 -6. **萌芽币系统扩展**: - - 实现萌芽币充值功能 - - 针对不同AI功能设置差异化定价 - - 添加萌芽币消费统计和分析功能 -7. **微前端架构**: 考虑将大型功能模块转换为微前端架构,提高扩展性和团队协作效率 -8. **构建优化**: 实施更先进的构建优化策略,如 Tree Shaking、代码分割、资源压缩等 -9. **渐进式Web应用升级**: 强化PWA能力,增强离线使用体验和桌面安装功能 - -## 技术债务与优化建议 - -### 1. 代码质量与一致性 - -当前项目在开发过程中出现了一些不一致的编码风格和实践,需要通过以下措施进行标准化: - -- **代码规范统一**: 引入 ESLint + Prettier 强制执行统一的代码风格 -- **类型安全增强**: 考虑引入 TypeScript 或 PropTypes 进行类型检查,降低运行时错误 -- **代码审查流程**: 建立明确的代码审查规范和流程,确保代码质量 -- **文档完善**: 补充关键功能模块和核心组件的详细文档 - -### 2. 架构优化与重构 - -以下是需要关注的架构层面优化点: - -- **API 层抽象**: 当前的 API 调用过于分散,建议引入统一的 API 请求层和数据模型层 -- **组件结构优化**: 部分组件承担过多职责,应按照单一职责原则进行拆分 -- **状态管理重构**: 当前 Context API 的使用在大型应用中可能导致过度渲染,考虑引入更高效的状态管理方案 -- **静态资源优化**: 优化图像、字体等静态资源的加载策略,减少页面加载时间 - -### 3. 前端基础设施升级 - -为提高开发效率和项目可维护性,建议升级以下基础设施: - -- **现代化构建工具**: 考虑从 Create React App 迁移至 Vite,提升开发和构建速度 -- **自动化测试框架**: 建立单元测试、集成测试和 E2E 测试的完整体系 -- **CI/CD 流程**: 优化持续集成和部署流程,实现更敏捷的开发和发布 -- **前端监控系统**: 引入错误跟踪和性能监控系统,主动发现并解决问题 - -## 前端技术栈演进路线 - -### 近期优化(0-3个月) - -1. **架构文档完善**: 补充架构决策记录(ADR)和技术选型依据 -2. **代码质量工具集成**: ESLint、Prettier、Husky 配置统一 -3. **性能优化第一阶段**: 首屏加载优化、资源懒加载实现 -4. **核心组件库重构**: 提取可复用组件,建立组件文档 - -### 中期规划(3-6个月) - -1. **TypeScript 迁移**: 核心模块向 TypeScript 迁移 -2. **状态管理升级**: 评估并引入更适合的状态管理方案 -3. **自动化测试覆盖**: 关键功能的单元测试和集成测试实现 -4. **微前端架构设计**: 设计微前端架构方案,逐步迁移大型功能模块 - -### 长期展望(6-12个月) - -1. **新技术栈评估与升级**: 评估 React 18+ 新特性应用 -2. **前端国际化支持**: 实现多语言和本地化支持 -3. **无障碍访问标准实现**: 符合 WCAG 2.1 AA 级标准 -4. **性能指标达成**: 优化应用性能,使所有关键指标达到业界标准 \ No newline at end of file +具体许可证以前端 `package.json` 与各子项目声明为准。若文档与代码不一致,以当前代码与 **`后端文档.md` / `前端文档.md`** 为准。 diff --git a/infogenie-backend-go/.env.development b/infogenie-backend-go/.env.development deleted file mode 100644 index aaa7a512..00000000 --- a/infogenie-backend-go/.env.development +++ /dev/null @@ -1,30 +0,0 @@ -# InfoGenie Go Backend - 开发环境配置 -APP_ENV=development -APP_PORT=5002 - -# MySQL 测试数据库 -DB_HOST=10.1.1.100 -DB_PORT=3306 -DB_NAME=infogenie-test -DB_USER=infogenie-test -DB_PASSWORD=infogenie-test - -# JWT -JWT_SECRET=d7e8f9a0b1c2d3e4f5a6b7c8d9e0f1a2b3c4d5e6f7a8b9c0d1e2f3a4b5c6d7e8 -JWT_EXPIRE_DAYS=7 - -# 邮件服务 -MAIL_HOST=smtp.qiye.aliyun.com -MAIL_PORT=465 -MAIL_USERNAME=notice@smyhub.com -MAIL_PASSWORD=tyh@19900420 - -# AI 配置文件路径 -AI_CONFIG_PATH=ai_config.json - -# 萌芽账户认证中心 -AUTH_CENTER_API_URL=https://auth.api.shumengya.top -AUTH_CENTER_ADMIN_TOKEN= - -# 站点前台配置(与前端管理员口令一致,用于保存 60s 功能展示开关) -INFOGENIE_SITE_ADMIN_TOKEN=shumengya520 diff --git a/infogenie-backend-go/.env.example b/infogenie-backend-go/.env.example new file mode 100644 index 00000000..d24f1c2e --- /dev/null +++ b/infogenie-backend-go/.env.example @@ -0,0 +1,22 @@ +APP_ENV=development +APP_PORT=5002 +DB_HOST=127.0.0.1 +DB_PORT=3306 +DB_NAME=infogenie-test +DB_USER=infogenie-test +DB_PASSWORD= +JWT_SECRET= +JWT_EXPIRE_DAYS=7 +MAIL_HOST= +MAIL_PORT=465 +MAIL_USERNAME= +MAIL_PASSWORD= +AUTH_CENTER_API_URL=https://auth.api.shumengya.top +AUTH_CENTER_ADMIN_TOKEN= +INFOGENIE_SITE_ADMIN_TOKEN= +# REDIS_ENABLED=false +# REDIS_ADDR=127.0.0.1:6379 +# REDIS_PASSWORD= +# REDIS_DB=10 +# REDIS_KEY_PREFIX=infogenie:go:v1: +# REDIS_SITE_TTL=60 diff --git a/infogenie-backend-go/.gitignore b/infogenie-backend-go/.gitignore index ce98d634..6cf90272 100644 --- a/infogenie-backend-go/.gitignore +++ b/infogenie-backend-go/.gitignore @@ -1,3 +1,4 @@ -# 含数据库密码、SMTP 等,勿提交 -.env.production -.env.local +# 含数据库密码、SMTP、JWT、Redis 等,勿提交 +.env +.env.* +!.env.example diff --git a/infogenie-backend-go/ai_config.json b/infogenie-backend-go/ai_config.json deleted file mode 100644 index 05b14b91..00000000 --- a/infogenie-backend-go/ai_config.json +++ /dev/null @@ -1,13 +0,0 @@ -{ - "deepseek": { - "api_key": "sk-832f8e5250464de08a31523c7fd712", - "api_base": "https://api.deepseek.com", - "model": ["deepseek-chat","deepseek-reasoner"] - }, - - "kimi": { - "api_key": "sk-zdg9NBpTlhOcDDpoWfaBKu0KNDdGv18SipORnL2utawja", - "api_base": "https://api.moonshot.cn", - "model": ["kimi-k2-0905-preview","kimi-k2-0711-preview"] - } -} \ No newline at end of file diff --git a/infogenie-backend-go/cmd/server/main.go b/infogenie-backend-go/cmd/server/main.go index b5ce6c0e..d31a4c98 100644 --- a/infogenie-backend-go/cmd/server/main.go +++ b/infogenie-backend-go/cmd/server/main.go @@ -7,6 +7,7 @@ import ( "github.com/gin-gonic/gin" "infogenie-backend/config" + "infogenie-backend/internal/cache" "infogenie-backend/internal/database" "infogenie-backend/internal/router" ) @@ -23,6 +24,13 @@ func main() { log.Fatalf("数据库初始化失败: %v", err) } + if err := cache.Init(cfg.Redis); err != nil { + log.Fatalf("Redis 初始化失败: %v", err) + } + if cfg.Redis.Enabled { + log.Printf("Redis 已启用: %s DB=%d prefix=%s TTL=%s", cfg.Redis.Addr, cfg.Redis.DB, cfg.Redis.KeyPrefix, cfg.Redis.SiteTTL) + } + if err := database.AutoMigrate(); err != nil { log.Fatalf("数据库迁移失败: %v", err) } diff --git a/infogenie-backend-go/config/config.go b/infogenie-backend-go/config/config.go index 684d11ae..cc1d68a0 100644 --- a/infogenie-backend-go/config/config.go +++ b/infogenie-backend-go/config/config.go @@ -5,6 +5,7 @@ import ( "os" "strconv" "strings" + "time" "github.com/joho/godotenv" ) @@ -17,10 +18,21 @@ type AppConfig struct { Mail MailConfig AI AIConfig AuthCenter AuthCenterConfig + Redis RedisConfig // SiteAdminToken 与前端管理员口令一致,用于更新站点展示配置(如 60s 功能开关);为空则禁止写入 SiteAdminToken string } +// RedisConfig 可选缓存;Enabled 为 false 时不连接 Redis,行为与未接入缓存时一致 +type RedisConfig struct { + Enabled bool + Addr string + Password string + DB int + KeyPrefix string + SiteTTL time.Duration +} + type DBConfig struct { Host string Port string @@ -67,6 +79,10 @@ const ( defaultDevDBName = "infogenie-test" defaultDevDBUser = "infogenie-test" defaultDevDBPassword = "infogenie-test" + + defaultDevRedisAddr = "10.1.1.100:6379" + defaultRedisDB = 10 + defaultRedisKeyPrefix = "infogenie:go:v1:" ) func Load() (*AppConfig, error) { @@ -129,6 +145,12 @@ func Load() (*AppConfig, error) { return nil, err } + redisCfg, err := loadRedisConfig(env) + if err != nil { + return nil, err + } + cfg.Redis = redisCfg + // AI配置现在完全从数据库读取,不再加载ai_config.json文件 cfg.AI = AIConfig{Providers: make(map[string]AIProviderConfig)} @@ -136,6 +158,53 @@ func Load() (*AppConfig, error) { return cfg, nil } +func loadRedisConfig(env string) (RedisConfig, error) { + if !parseBoolEnv(os.Getenv("REDIS_ENABLED")) { + return RedisConfig{}, nil + } + var addr string + var err error + if env == envProduction { + addr, err = getEnvByEnvironment(env, "REDIS_ADDR", "") + if err != nil { + return RedisConfig{}, err + } + } else { + addr = getEnv("REDIS_ADDR", defaultDevRedisAddr) + } + if strings.TrimSpace(addr) == "" { + return RedisConfig{}, fmt.Errorf("已启用 REDIS_ENABLED 但 REDIS_ADDR 为空") + } + dbIdx, err := strconv.Atoi(strings.TrimSpace(getEnv("REDIS_DB", strconv.Itoa(defaultRedisDB)))) + if err != nil || dbIdx < 0 { + return RedisConfig{}, fmt.Errorf("无效的 REDIS_DB: %s", getEnv("REDIS_DB", "")) + } + ttlSec, err := strconv.Atoi(strings.TrimSpace(getEnv("REDIS_SITE_TTL", "60"))) + if err != nil || ttlSec < 1 { + return RedisConfig{}, fmt.Errorf("无效的 REDIS_SITE_TTL: %s", getEnv("REDIS_SITE_TTL", "")) + } + prefix := strings.TrimSpace(getEnv("REDIS_KEY_PREFIX", defaultRedisKeyPrefix)) + if prefix == "" { + prefix = defaultRedisKeyPrefix + } + if !strings.HasSuffix(prefix, ":") { + prefix += ":" + } + return RedisConfig{ + Enabled: true, + Addr: addr, + Password: getEnv("REDIS_PASSWORD", ""), + DB: dbIdx, + KeyPrefix: prefix, + SiteTTL: time.Duration(ttlSec) * time.Second, + }, nil +} + +func parseBoolEnv(raw string) bool { + s := strings.ToLower(strings.TrimSpace(raw)) + return s == "1" || s == "true" || s == "yes" || s == "on" +} + func loadEnvFile(env string) error { envFile := fmt.Sprintf(".env.%s", env) if _, err := os.Stat(envFile); err == nil { diff --git a/infogenie-backend-go/go.mod b/infogenie-backend-go/go.mod index 07ff732c..30a214cf 100644 --- a/infogenie-backend-go/go.mod +++ b/infogenie-backend-go/go.mod @@ -2,26 +2,33 @@ module infogenie-backend go 1.25.0 +require ( + github.com/gin-contrib/cors v1.7.6 + github.com/gin-gonic/gin v1.12.0 + github.com/joho/godotenv v1.5.1 + github.com/redis/go-redis/v9 v9.18.0 + gorm.io/driver/mysql v1.6.0 + gorm.io/gorm v1.31.1 +) + require ( filippo.io/edwards25519 v1.1.0 // indirect github.com/bytedance/gopkg v0.1.3 // indirect github.com/bytedance/sonic v1.15.0 // indirect github.com/bytedance/sonic/loader v0.5.0 // indirect + github.com/cespare/xxhash/v2 v2.3.0 // indirect github.com/cloudwego/base64x v0.1.6 // indirect + github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect github.com/gabriel-vasile/mimetype v1.4.12 // indirect - github.com/gin-contrib/cors v1.7.6 // indirect github.com/gin-contrib/sse v1.1.0 // indirect - github.com/gin-gonic/gin v1.12.0 // indirect github.com/go-playground/locales v0.14.1 // indirect github.com/go-playground/universal-translator v0.18.1 // indirect github.com/go-playground/validator/v10 v10.30.1 // indirect github.com/go-sql-driver/mysql v1.9.3 // indirect github.com/goccy/go-json v0.10.5 // indirect github.com/goccy/go-yaml v1.19.2 // indirect - github.com/golang-jwt/jwt/v5 v5.3.1 // indirect github.com/jinzhu/inflection v1.0.0 // indirect github.com/jinzhu/now v1.1.5 // indirect - github.com/joho/godotenv v1.5.1 // indirect github.com/json-iterator/go v1.1.12 // indirect github.com/klauspost/cpuid/v2 v2.3.0 // indirect github.com/leodido/go-urn v1.4.0 // indirect @@ -34,12 +41,11 @@ require ( github.com/twitchyliquid64/golang-asm v0.15.1 // indirect github.com/ugorji/go/codec v1.3.1 // indirect go.mongodb.org/mongo-driver/v2 v2.5.0 // indirect + go.uber.org/atomic v1.11.0 // indirect golang.org/x/arch v0.22.0 // indirect golang.org/x/crypto v0.49.0 // indirect golang.org/x/net v0.51.0 // indirect golang.org/x/sys v0.42.0 // indirect golang.org/x/text v0.35.0 // indirect google.golang.org/protobuf v1.36.10 // indirect - gorm.io/driver/mysql v1.6.0 // indirect - gorm.io/gorm v1.31.1 // indirect ) diff --git a/infogenie-backend-go/go.sum b/infogenie-backend-go/go.sum index 931d2a78..9e6f29f0 100644 --- a/infogenie-backend-go/go.sum +++ b/infogenie-backend-go/go.sum @@ -1,15 +1,24 @@ filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA= filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4= +github.com/bsm/ginkgo/v2 v2.12.0 h1:Ny8MWAHyOepLGlLKYmXG4IEkioBysk6GpaRTLC8zwWs= +github.com/bsm/ginkgo/v2 v2.12.0/go.mod h1:SwYbGRRDovPVboqFv0tPTcG1sN61LM1Z4ARdbAV9g4c= +github.com/bsm/gomega v1.27.10 h1:yeMWxP2pV2fG3FgAODIY8EiRE3dy0aeFYt4l7wh6yKA= +github.com/bsm/gomega v1.27.10/go.mod h1:JyEr/xRbxbtgWNi8tIEVPUYZ5Dzef52k01W3YH0H+O0= github.com/bytedance/gopkg v0.1.3 h1:TPBSwH8RsouGCBcMBktLt1AymVo2TVsBVCY4b6TnZ/M= github.com/bytedance/gopkg v0.1.3/go.mod h1:576VvJ+eJgyCzdjS+c4+77QF3p7ubbtiKARP3TxducM= github.com/bytedance/sonic v1.15.0 h1:/PXeWFaR5ElNcVE84U0dOHjiMHQOwNIx3K4ymzh/uSE= github.com/bytedance/sonic v1.15.0/go.mod h1:tFkWrPz0/CUCLEF4ri4UkHekCIcdnkqXw9VduqpJh0k= github.com/bytedance/sonic/loader v0.5.0 h1:gXH3KVnatgY7loH5/TkeVyXPfESoqSBSBEiDd5VjlgE= github.com/bytedance/sonic/loader v0.5.0/go.mod h1:AR4NYCk5DdzZizZ5djGqQ92eEhCCcdf5x77udYiSJRo= +github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= +github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/cloudwego/base64x v0.1.6 h1:t11wG9AECkCDk5fMSoxmufanudBtJ+/HemLstXDLI2M= github.com/cloudwego/base64x v0.1.6/go.mod h1:OFcloc187FXDaYHvrNIjxSe8ncn0OOM8gEHfghB2IPU= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78= +github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc= github.com/gabriel-vasile/mimetype v1.4.12 h1:e9hWvmLYvtp846tLHam2o++qitpguFiYCKbn0w9jyqw= github.com/gabriel-vasile/mimetype v1.4.12/go.mod h1:d+9Oxyo1wTzWdyVUPMmXFvp4F9tea18J8ufA774AB3s= github.com/gin-contrib/cors v1.7.6 h1:3gQ8GMzs1Ylpf70y8bMw4fVpycXIeX1ZemuSQIsnQQY= @@ -18,6 +27,8 @@ github.com/gin-contrib/sse v1.1.0 h1:n0w2GMuUpWDVp7qSpvze6fAu9iRxJY4Hmj6AmBOU05w github.com/gin-contrib/sse v1.1.0/go.mod h1:hxRZ5gVpWMT7Z0B0gSNYqqsSCNIJMjzvm6fqCz9vjwM= github.com/gin-gonic/gin v1.12.0 h1:b3YAbrZtnf8N//yjKeU2+MQsh2mY5htkZidOM7O0wG8= github.com/gin-gonic/gin v1.12.0/go.mod h1:VxccKfsSllpKshkBWgVgRniFFAzFb9csfngsqANjnLc= +github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s= +github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA= github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY= github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY= @@ -30,8 +41,8 @@ github.com/goccy/go-json v0.10.5 h1:Fq85nIqj+gXn/S5ahsiTlK3TmC85qgirsdTP/+DeaC4= github.com/goccy/go-json v0.10.5/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M= github.com/goccy/go-yaml v1.19.2 h1:PmFC1S6h8ljIz6gMRBopkjP1TVT7xuwrButHID66PoM= github.com/goccy/go-yaml v1.19.2/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA= -github.com/golang-jwt/jwt/v5 v5.3.1 h1:kYf81DTWFe7t+1VvL7eS+jKFVWaUnK9cB1qbwn63YCY= -github.com/golang-jwt/jwt/v5 v5.3.1/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE= +github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= +github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E= github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc= @@ -54,11 +65,14 @@ github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9G github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4= github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/quic-go/qpack v0.6.0 h1:g7W+BMYynC1LbYLSqRt8PBg5Tgwxn214ZZR34VIOjz8= github.com/quic-go/qpack v0.6.0/go.mod h1:lUpLKChi8njB4ty2bFLX2x4gzDqXwUpaO1DP9qMDZII= github.com/quic-go/quic-go v0.59.0 h1:OLJkp1Mlm/aS7dpKgTc6cnpynnD2Xg7C1pwL6vy/SAw= github.com/quic-go/quic-go v0.59.0/go.mod h1:upnsH4Ju1YkqpLXC305eW3yDZ4NfnNbmQRCMWS58IKU= +github.com/redis/go-redis/v9 v9.18.0 h1:pMkxYPkEbMPwRdenAzUNyFNrDgHx9U+DrBabWNfSRQs= +github.com/redis/go-redis/v9 v9.18.0/go.mod h1:k3ufPphLU5YXwNTUcCRXGxUoF1fqxnhFQmscfkCoDA0= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= @@ -68,12 +82,20 @@ github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/ github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= +github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI= github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08= github.com/ugorji/go/codec v1.3.1 h1:waO7eEiFDwidsBN6agj1vJQ4AG7lh2yqXyOXqhgQuyY= github.com/ugorji/go/codec v1.3.1/go.mod h1:pRBVtBSKl77K30Bv8R2P+cLSGaTtex6fsA2Wjqmfxj4= +github.com/zeebo/xxh3 v1.0.2 h1:xZmwmqxHZA8AI603jOQ0tMqmBr9lPeFwGg6d+xy9DC0= +github.com/zeebo/xxh3 v1.0.2/go.mod h1:5NWz9Sef7zIDm2JHfFlcQvNekmcEl9ekUZQQKCYaDcA= go.mongodb.org/mongo-driver/v2 v2.5.0 h1:yXUhImUjjAInNcpTcAlPHiT7bIXhshCTL3jVBkF3xaE= go.mongodb.org/mongo-driver/v2 v2.5.0/go.mod h1:yOI9kBsufol30iFsl1slpdq1I0eHPzybRWdyYUs8K/0= +go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE= +go.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0= +go.uber.org/mock v0.6.0 h1:hyF9dfmbgIX5EfOdasqLsWD6xqpNZlXblLB/Dbnwv3Y= +go.uber.org/mock v0.6.0/go.mod h1:KiVJ4BqZJaMj4svdfmHM0AUx4NJYO8ZNpPnZn1Z+BBU= golang.org/x/arch v0.22.0 h1:c/Zle32i5ttqRXjdLyyHZESLD/bB90DCU1g9l/0YBDI= golang.org/x/arch v0.22.0/go.mod h1:dNHoOeKiyja7GTvF9NJS1l3Z2yntpQNzgrjh1cU103A= golang.org/x/crypto v0.49.0 h1:+Ng2ULVvLHnJ/ZFEq4KdcDd/cfjrrjjNSXNzxg0Y4U4= @@ -89,6 +111,7 @@ google.golang.org/protobuf v1.36.10 h1:AYd7cD/uASjIL6Q9LiTjz8JLcrh/88q5UObnmY3aO google.golang.org/protobuf v1.36.10/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gorm.io/driver/mysql v1.6.0 h1:eNbLmNTpPpTOVZi8MMxCi2aaIm0ZpInbORNXDwyLGvg= gorm.io/driver/mysql v1.6.0/go.mod h1:D/oCC2GWK3M/dqoLxnOlaNKmXz8WNTfcS9y5ovaSqKo= diff --git a/infogenie-backend-go/internal/cache/redis.go b/infogenie-backend-go/internal/cache/redis.go new file mode 100644 index 00000000..63653e7c --- /dev/null +++ b/infogenie-backend-go/internal/cache/redis.go @@ -0,0 +1,119 @@ +package cache + +import ( + "context" + "encoding/json" + "fmt" + "log" + "time" + + "github.com/redis/go-redis/v9" + + "infogenie-backend/config" +) + +// Key suffixes(完整 key = KeyPrefix + suffix) +const ( + KeySite60sDisabled = "site:60s-disabled" + KeySite60sSource = "site:60s-source" + KeySiteAIModelDisabled = "site:ai-model-disabled" +) + +func KeySiteFeatureClicks(section string) string { + return "site:feature-clicks:" + section +} + +var ( + rdb *redis.Client + keyPrefix string + siteCacheTTL time.Duration + redisOn bool +) + +// Init 在 REDIS_ENABLED 时连接并 Ping;未启用时为空操作 +func Init(rc config.RedisConfig) error { + if !rc.Enabled { + redisOn = false + rdb = nil + return nil + } + redisOn = true + keyPrefix = rc.KeyPrefix + siteCacheTTL = rc.SiteTTL + if siteCacheTTL <= 0 { + siteCacheTTL = 60 * time.Second + } + rdb = redis.NewClient(&redis.Options{ + Addr: rc.Addr, + Password: rc.Password, + DB: rc.DB, + }) + if err := rdb.Ping(context.Background()).Err(); err != nil { + return fmt.Errorf("redis: %w", err) + } + return nil +} + +// Enabled 表示已初始化且客户端可用 +func Enabled() bool { + return redisOn && rdb != nil +} + +func fullKey(suffix string) string { + return keyPrefix + suffix +} + +// GetJSON 命中返回 true;未启用或未命中返回 false(err 仅表示 Redis/JSON 异常) +func GetJSON(ctx context.Context, suffix string, dest interface{}) (bool, error) { + if !Enabled() { + return false, nil + } + val, err := rdb.Get(ctx, fullKey(suffix)).Bytes() + if err == redis.Nil { + return false, nil + } + if err != nil { + return false, err + } + if err := json.Unmarshal(val, dest); err != nil { + return false, err + } + return true, nil +} + +// SetJSON ttl<=0 时使用配置的 SiteTTL +func SetJSON(ctx context.Context, suffix string, v interface{}, ttl time.Duration) error { + if !Enabled() { + return nil + } + b, err := json.Marshal(v) + if err != nil { + return err + } + if ttl <= 0 { + ttl = siteCacheTTL + } + return rdb.Set(ctx, fullKey(suffix), b, ttl).Err() +} + +// Delete 删除若干后缀对应的 key;失败仅打日志 +func Delete(ctx context.Context, suffixes ...string) { + if !Enabled() || len(suffixes) == 0 { + return + } + keys := make([]string, 0, len(suffixes)) + for _, s := range suffixes { + keys = append(keys, fullKey(s)) + } + if err := rdb.Del(ctx, keys...).Err(); err != nil { + log.Printf("redis DEL 失败: %v keys=%v", err, keys) + } +} + +// Ping 未启用时返回 nil +func Ping(ctx context.Context) error { + if !Enabled() { + return nil + } + return rdb.Ping(ctx).Err() +} diff --git a/infogenie-backend-go/internal/handler/feature_card_clicks.go b/infogenie-backend-go/internal/handler/feature_card_clicks.go index c36ea0e8..a62a46e5 100644 --- a/infogenie-backend-go/internal/handler/feature_card_clicks.go +++ b/infogenie-backend-go/internal/handler/feature_card_clicks.go @@ -1,11 +1,13 @@ package handler import ( + "log" "net/http" "strings" "github.com/gin-gonic/gin" + "infogenie-backend/internal/cache" "infogenie-backend/internal/database" "infogenie-backend/internal/model" ) @@ -43,6 +45,19 @@ func (h *SiteConfigHandler) GetFeatureCardClicks(c *gin.Context) { c.JSON(http.StatusBadRequest, gin.H{"error": "invalid_section"}) return } + ctx := c.Request.Context() + key := cache.KeySiteFeatureClicks(section) + var cached struct { + Section string `json:"section"` + Counts map[string]uint64 `json:"counts"` + } + if hit, err := cache.GetJSON(ctx, key, &cached); err != nil { + log.Printf("redis GET %s: %v", key, err) + } else if hit { + c.JSON(http.StatusOK, gin.H{"section": cached.Section, "counts": cached.Counts}) + return + } + var rows []model.SiteFeatureCardClick if err := database.DB.Where("section = ?", section).Find(&rows).Error; err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "db_error"}) @@ -52,7 +67,11 @@ func (h *SiteConfigHandler) GetFeatureCardClicks(c *gin.Context) { for _, r := range rows { counts[r.ItemID] = r.ClickCount } - c.JSON(http.StatusOK, gin.H{"section": section, "counts": counts}) + payload := gin.H{"section": section, "counts": counts} + if err := cache.SetJSON(ctx, key, payload, 0); err != nil { + log.Printf("redis SET %s: %v", key, err) + } + c.JSON(http.StatusOK, payload) } type postFeatureCardClickBody struct { @@ -91,5 +110,6 @@ ON DUPLICATE KEY UPDATE click_count = click_count + 1, updated_at = NOW()` c.JSON(http.StatusInternalServerError, gin.H{"error": "db_error"}) return } + cache.Delete(c.Request.Context(), cache.KeySiteFeatureClicks(section)) c.JSON(http.StatusOK, gin.H{"section": section, "item_id": itemID, "count": row.ClickCount}) } diff --git a/infogenie-backend-go/internal/handler/siteconfig.go b/infogenie-backend-go/internal/handler/siteconfig.go index 40a64334..ce5ef56e 100644 --- a/infogenie-backend-go/internal/handler/siteconfig.go +++ b/infogenie-backend-go/internal/handler/siteconfig.go @@ -3,6 +3,7 @@ package handler import ( "crypto/subtle" "errors" + "log" "net/http" "strings" @@ -10,6 +11,7 @@ import ( "gorm.io/gorm" "infogenie-backend/config" + "infogenie-backend/internal/cache" "infogenie-backend/internal/database" "infogenie-backend/internal/model" ) @@ -31,6 +33,17 @@ func siteAdminTokenOK(headerToken string) bool { // Get60sDisabled 公开:返回当前隐藏的 60s 功能 id 列表(与前端 item.id 对应) func (h *SiteConfigHandler) Get60sDisabled(c *gin.Context) { + ctx := c.Request.Context() + var cached struct { + Disabled []string `json:"disabled"` + } + if hit, err := cache.GetJSON(ctx, cache.KeySite60sDisabled, &cached); err != nil { + log.Printf("redis GET %s: %v", cache.KeySite60sDisabled, err) + } else if hit { + c.JSON(http.StatusOK, gin.H{"disabled": cached.Disabled}) + return + } + var rows []model.Site60sDisabled if err := database.DB.Order("feature_id").Find(&rows).Error; err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "db_error"}) @@ -40,7 +53,11 @@ func (h *SiteConfigHandler) Get60sDisabled(c *gin.Context) { for _, r := range rows { ids = append(ids, r.FeatureID) } - c.JSON(http.StatusOK, gin.H{"disabled": ids}) + payload := gin.H{"disabled": ids} + if err := cache.SetJSON(ctx, cache.KeySite60sDisabled, payload, 0); err != nil { + log.Printf("redis SET %s: %v", cache.KeySite60sDisabled, err) + } + c.JSON(http.StatusOK, payload) } type put60sDisabledBody struct { @@ -102,6 +119,7 @@ func (h *SiteConfigHandler) Put60sDisabled(c *gin.Context) { c.JSON(http.StatusInternalServerError, gin.H{"error": "db_error"}) return } + cache.Delete(c.Request.Context(), cache.KeySite60sDisabled) c.JSON(http.StatusOK, gin.H{"ok": true, "count": len(clean)}) } @@ -146,14 +164,35 @@ func EffectiveSixtyUpstream(db *gorm.DB) (sourceID string, base string, label st // Get60sSource 公开:当前站点使用的 60s 上游 base_url(供静态页 iframe 传参) func (h *SiteConfigHandler) Get60sSource(c *gin.Context) { + ctx := c.Request.Context() + var cached struct { + SourceID string `json:"source_id"` + BaseURL string `json:"base_url"` + Label string `json:"label"` + } + if hit, err := cache.GetJSON(ctx, cache.KeySite60sSource, &cached); err != nil { + log.Printf("redis GET %s: %v", cache.KeySite60sSource, err) + } else if hit { + c.JSON(http.StatusOK, gin.H{ + "source_id": cached.SourceID, + "base_url": cached.BaseURL, + "label": cached.Label, + }) + return + } + var row model.Site60sUpstream _ = database.DB.First(&row, 1).Error sid, info := resolve60sUpstream(row.SourceID) - c.JSON(http.StatusOK, gin.H{ + payload := gin.H{ "source_id": sid, "base_url": info.Base, "label": info.Label, - }) + } + if err := cache.SetJSON(ctx, cache.KeySite60sSource, payload, 0); err != nil { + log.Printf("redis SET %s: %v", cache.KeySite60sSource, err) + } + c.JSON(http.StatusOK, payload) } type put60sSourceBody struct { @@ -195,6 +234,7 @@ func (h *SiteConfigHandler) Put60sSource(c *gin.Context) { c.JSON(http.StatusInternalServerError, gin.H{"error": "db_error"}) return } + cache.Delete(c.Request.Context(), cache.KeySite60sSource) _, info := resolve60sUpstream(sid) c.JSON(http.StatusOK, gin.H{"ok": true, "source_id": sid, "base_url": info.Base, "label": info.Label}) } @@ -203,6 +243,17 @@ func (h *SiteConfigHandler) Put60sSource(c *gin.Context) { // GetAIModelDisabled 公开:返回当前隐藏的 AI 应用 id 列表(与前端 StaticPageConfig 中 AI_MODEL_APPS 的索引对应) func (h *SiteConfigHandler) GetAIModelDisabled(c *gin.Context) { + ctx := c.Request.Context() + var cached struct { + Disabled []string `json:"disabled"` + } + if hit, err := cache.GetJSON(ctx, cache.KeySiteAIModelDisabled, &cached); err != nil { + log.Printf("redis GET %s: %v", cache.KeySiteAIModelDisabled, err) + } else if hit { + c.JSON(http.StatusOK, gin.H{"disabled": cached.Disabled}) + return + } + var rows []model.SiteAIModelDisabled if err := database.DB.Order("app_id").Find(&rows).Error; err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "db_error"}) @@ -212,7 +263,11 @@ func (h *SiteConfigHandler) GetAIModelDisabled(c *gin.Context) { for _, r := range rows { ids = append(ids, r.AppID) } - c.JSON(http.StatusOK, gin.H{"disabled": ids}) + payload := gin.H{"disabled": ids} + if err := cache.SetJSON(ctx, cache.KeySiteAIModelDisabled, payload, 0); err != nil { + log.Printf("redis SET %s: %v", cache.KeySiteAIModelDisabled, err) + } + c.JSON(http.StatusOK, payload) } type putAIModelDisabledBody struct { @@ -274,5 +329,63 @@ func (h *SiteConfigHandler) PutAIModelDisabled(c *gin.Context) { c.JSON(http.StatusInternalServerError, gin.H{"error": "db_error"}) return } + cache.Delete(c.Request.Context(), cache.KeySiteAIModelDisabled) c.JSON(http.StatusOK, gin.H{"ok": true, "count": len(clean)}) } + +// GetDiagnostics 返回当前进程解析到的连接配置摘要(不含密码/密钥明文),需 X-Site-Admin-Token +func (h *SiteConfigHandler) GetDiagnostics(c *gin.Context) { + if config.Cfg == nil || strings.TrimSpace(config.Cfg.SiteAdminToken) == "" { + c.JSON(http.StatusServiceUnavailable, gin.H{ + "error": "admin_not_configured", + "message": "服务端未配置 INFOGENIE_SITE_ADMIN_TOKEN", + }) + return + } + if !siteAdminTokenOK(c.GetHeader("X-Site-Admin-Token")) { + c.JSON(http.StatusForbidden, gin.H{"error": "forbidden"}) + return + } + cfg := config.Cfg + + sixtyID, sixtyBase, sixtyLabel := EffectiveSixtyUpstream(database.DB) + + redisOut := gin.H{"enabled": cfg.Redis.Enabled} + if cfg.Redis.Enabled { + redisOut["addr"] = cfg.Redis.Addr + redisOut["logical_db"] = cfg.Redis.DB + redisOut["key_prefix"] = cfg.Redis.KeyPrefix + redisOut["site_cache_ttl_sec"] = int(cfg.Redis.SiteTTL.Seconds()) + redisOut["password_configured"] = strings.TrimSpace(cfg.Redis.Password) != "" + } + + c.JSON(http.StatusOK, gin.H{ + "app": gin.H{ + "env": cfg.Env, + "listen_addr": "0.0.0.0:" + cfg.Port, + "listen_port": cfg.Port, + }, + "mysql": gin.H{ + "host": cfg.DB.Host, + "port": cfg.DB.Port, + "database": cfg.DB.Name, + "user": cfg.DB.User, + "password_configured": strings.TrimSpace(cfg.DB.Password) != "", + }, + "redis": redisOut, + "sixty_upstream": gin.H{ + "source_id": sixtyID, + "base_url": sixtyBase, + "label": sixtyLabel, + }, + "auth_center": gin.H{ + "api_url": cfg.AuthCenter.APIURL, + }, + "mail": gin.H{ + "host": cfg.Mail.Host, + "port": cfg.Mail.Port, + "username": cfg.Mail.Username, + "password_configured": strings.TrimSpace(cfg.Mail.Password) != "", + }, + }) +} diff --git a/infogenie-backend-go/internal/router/router.go b/infogenie-backend-go/internal/router/router.go index 7ebf813e..45472af3 100644 --- a/infogenie-backend-go/internal/router/router.go +++ b/infogenie-backend-go/internal/router/router.go @@ -9,6 +9,8 @@ import ( "github.com/gin-gonic/gin" + "infogenie-backend/config" + "infogenie-backend/internal/cache" "infogenie-backend/internal/database" "infogenie-backend/internal/handler" "infogenie-backend/internal/middleware" @@ -91,6 +93,15 @@ func Setup(r *gin.Engine) { overall = "degraded" } + redisHealth := gin.H{"enabled": false} + if config.Cfg != nil && config.Cfg.Redis.Enabled { + if err := cache.Ping(ctx); err != nil { + redisHealth = gin.H{"enabled": true, "ok": false, "error": err.Error()} + } else { + redisHealth = gin.H{"enabled": true, "ok": true} + } + } + c.JSON(http.StatusOK, gin.H{ "status": overall, "timestamp": time.Now().Format(time.RFC3339), @@ -99,6 +110,7 @@ func Setup(r *gin.Engine) { "ok": mysqlOK, "status": dbStatus, }, + "redis": redisHealth, "backend_api": gin.H{ "ok": true, }, @@ -136,6 +148,7 @@ func Setup(r *gin.Engine) { r.PUT("/api/admin/site/ai-model-disabled", siteH.PutAIModelDisabled) r.GET("/api/admin/site/ai-runtime", aiRtH.GetAIRuntime) r.PUT("/api/admin/site/ai-runtime", aiRtH.PutAIRuntime) + r.GET("/api/admin/site/diagnostics", siteH.GetDiagnostics) ai := r.Group("/api/aimodelapp") { diff --git a/infogenie-backend-go/scripts/redis_keyspace/main.go b/infogenie-backend-go/scripts/redis_keyspace/main.go new file mode 100644 index 00000000..4039f243 --- /dev/null +++ b/infogenie-backend-go/scripts/redis_keyspace/main.go @@ -0,0 +1,84 @@ +package main + +import ( + "context" + "fmt" + "os" + "strconv" + "strings" + "time" + + "github.com/redis/go-redis/v9" +) + +// 用法: go run . [password] +// 例: go run . 10.1.1.100:6379 +func main() { + addr := "10.1.1.100:6379" + pass := os.Getenv("REDIS_PASSWORD") + if len(os.Args) >= 2 { + addr = os.Args[1] + } + if len(os.Args) >= 3 { + pass = os.Args[2] + } + ctx, cancel := context.WithTimeout(context.Background(), 8*time.Second) + defer cancel() + + r := redis.NewClient(&redis.Options{Addr: addr, Password: pass}) + defer r.Close() + if err := r.Ping(ctx).Err(); err != nil { + fmt.Fprintf(os.Stderr, "连接失败 %s: %v\n", addr, err) + os.Exit(1) + } + + s, err := r.Info(ctx, "keyspace").Result() + if err != nil { + fmt.Fprintf(os.Stderr, "INFO keyspace: %v\n", err) + os.Exit(1) + } + + used := make(map[int]int64) + for _, line := range strings.Split(s, "\r\n") { + line = strings.TrimSpace(line) + if line == "" || strings.HasPrefix(line, "#") { + continue + } + if !strings.HasPrefix(line, "db") { + continue + } + colon := strings.IndexByte(line, ':') + if colon <= 2 { + continue + } + dbStr := line[2:colon] + dbNum, err := strconv.Atoi(dbStr) + if err != nil { + continue + } + var keys int64 + for _, part := range strings.Split(line[colon+1:], ",") { + part = strings.TrimSpace(part) + if strings.HasPrefix(part, "keys=") { + keys, _ = strconv.ParseInt(strings.TrimPrefix(part, "keys="), 10, 64) + break + } + } + used[dbNum] = keys + } + + fmt.Printf("Redis %s — 逻辑库 key 统计(INFO keyspace)\n\n", addr) + for i := 0; i < 16; i++ { + k, ok := used[i] + if !ok { + fmt.Printf(" db%-2d : 无记录(通常为 0 个 key / 未写入过)\n", i) + } else if k == 0 { + fmt.Printf(" db%-2d : 0 keys\n", i) + } else { + fmt.Printf(" db%-2d : %d keys (已占用)\n", i, k) + } + } + fmt.Println() + fmt.Println("说明: Redis 默认 16 个逻辑库(0-15);「无记录」一般可当空闲选用。") + fmt.Println("若 redis.conf 里 databases=N,实际库数量可能更大,可用 redis-cli CONFIG GET databases 查看。") +} diff --git a/infogenie-backend-go/后端文档.md b/infogenie-backend-go/后端文档.md index 5606857e..bca45331 100644 --- a/infogenie-backend-go/后端文档.md +++ b/infogenie-backend-go/后端文档.md @@ -1,117 +1,173 @@ -# 万象口袋 — Go 后端文档 - -**技术栈**:Go 1.25+ · Gin · GORM · MySQL - -**模块路径**:`infogenie-backend`(见 `go.mod`) - -**入口**:`cmd/server/main.go` — 加载配置、连接数据库、`AutoMigrate`、启动 HTTP 服务。 - ---- - -## 运行与配置 - -- 环境由 **`APP_ENV`** 决定:`development` 或 `production`(见 `config.Load()`)。 -- 若存在 **`.env.development`** / **`.env.production`**,会通过 `godotenv` 加载对应文件。 -- **`APP_PORT`** 默认 **5002**(与前端 `REACT_APP_API_URL` 开发默认一致)。 -- 数据库、邮件、认证中心、`INFOGENIE_SITE_ADMIN_TOKEN` 等从环境变量读取,详见 `config/config.go`。 - -**健康检查**:`GET /api/health` — 返回服务状态与数据库 `Ping` 结果。 - -**根路径**:`GET /` — 返回服务说明与主要 endpoint 分组(`version` 当前为 **3.3.0-go**)。 - ---- - -## 数据库(GORM AutoMigrate) - -启动时会迁移以下模型(见 `internal/database/mysql.go`): - -| 模型 | 用途 | -|------|------| -| `AIConfig` | 多厂商 AI Key / Base / 模型列表(如 deepseek、kimi) | -| `Site60sDisabled` | 60s 功能在前端隐藏的 `feature_id` | -| `SiteAIRuntime` | DeepSeek 兼容网关(Base + Key + 默认模型),优先级高于部分 AIConfig | -| `Site60sUpstream` | 60s 上游节点(单例 id=1) | -| `SiteAIModelDisabled` | AI 应用在前端隐藏的 `app_id` | -| `SiteFeatureCardClick` | 四大板块功能卡片点击统计(`section` + `item_id` 联合主键) | - ---- - -## 路由概览(`internal/router/router.go`) - -### CORS - -全局 `middleware.CORS()`,放行常用 Method/Header(含 `Authorization`、`X-Site-Admin-Token`)。 - -### 认证与用户 - -| 方法 | 路径 | 说明 | -|------|------|------| -| GET | `/api/auth/check` | 可选 JWT:校验登录态 | -| GET | `/api/user/profile` | **需 JWT**:用户资料 | - -实际登录、发 token 由 **萌芽账户认证中心** 完成;后端校验 JWT。 - -### 站点公开配置(无需登录) - -| 方法 | 路径 | 说明 | -|------|------|------| -| GET | `/api/site/60s-disabled` | 被隐藏的 60s `feature_id` 列表 | -| GET | `/api/site/60s-source` | 60s 上游 `source_id` / `base_url` | -| GET | `/api/site/ai-model-disabled` | 被隐藏的 AI 应用 id 列表 | -| GET | `/api/site/feature-card-clicks?section=` | 功能卡片点击次数(见下) | -| POST | `/api/site/feature-card-clicks/increment` | 上报一次点击,返回最新 count | - -**`section` 合法值**:`60sapi` · `smallgame` · `toolbox` · `aimodel` - -**increment 请求体**:`{ "section": "...", "item_id": "..." }` - -### 站点管理(需 `X-Site-Admin-Token`,与环境变量 `INFOGENIE_SITE_ADMIN_TOKEN` 一致) - -| 方法 | 路径 | 说明 | -|------|------|------| -| PUT | `/api/admin/site/60s-disabled` | 更新 60s 隐藏列表 | -| PUT | `/api/admin/site/60s-source` | 切换 60s 上游 | -| PUT | `/api/admin/site/ai-model-disabled` | 更新 AI 应用隐藏列表 | -| GET/PUT | `/api/admin/site/ai-runtime` | 读取/更新 DeepSeek 兼容运行时配置 | - -### AI 应用(`/api/aimodelapp`,默认 **需 JWT**) - -| 方法 | 路径 | 说明 | -|------|------|------| -| POST | `/chat` | 非流式对话,JSON 返回全文 | -| POST | `/chat/stream` | **SSE 流式**:透传上游 OpenAI 兼容流(`text/event-stream`) | -| POST | `/name-analysis` 等 | 各垂直能力(姓名、变量命名、写诗、翻译等) | -| GET | `/models` | 模型列表 | - -**流式说明**(`internal/handler/aimodel.go` + `internal/service/ai.go`): - -- 上游请求带 `stream: true`,成功后将上游 body **分块写入并 Flush** 到客户端。 -- 支持 **deepseek**(运行时或 `AIConfig`)与 **kimi**(`AIConfig`)。 -- 与 `/chat` 共用同一套 `bindAIModelChat` 校验(消息条数、长度、模型白名单等)。 - -**模型白名单**:见 `internal/handler/aimodel.go` 中 `allowedModels`(如 deepseek-chat、deepseek-reasoner、部分 kimi 模型)。 - ---- - -## 核心源码目录 - -``` -cmd/server/ # main -config/ # 配置加载 -internal/ - database/ # MySQL 初始化、AutoMigrate - handler/ # HTTP 处理器(auth、user、aimodel、siteconfig、ai_runtime、feature_card_clicks) - middleware/ # CORS、JWT - model/ # GORM 模型 - router/ # 路由注册 - service/ # AI 调用(含 OpenAI 兼容非流式与流式) -``` - ---- - -## 与其他工程的关系 - -- **前端 SPA** 通过 `REACT_APP_API_URL` 指向本服务(开发默认 `http://127.0.0.1:5002`)。 -- **`public/aimodelapp/*/shared/ai-chat.js`** 优先调用 `/api/aimodelapp/chat/stream`,失败时回退 `/chat`。 - -更完整的前端集成说明见 **`infogenie-frontend/前端文档.md`**。 +# 万象口袋 — Go 后端文档 + +**技术栈**:Go 1.25+ · Gin · GORM · MySQL ·(可选)Redis(go-redis v9) + +**模块路径**:`infogenie-backend`(见 `go.mod`) + +**入口**:`cmd/server/main.go` — 加载配置、连接 MySQL、`cache.Init`(若启用 Redis)、`AutoMigrate`、启动 HTTP 服务。 + +--- + +## 运行与配置 + +- 环境由 `**APP_ENV`** 决定:`development` 或 `production`(见 `config.Load()`)。 +- 若存在 `**.env.development**` / `**.env.production**`,会通过 `godotenv` 加载对应文件。 +- `**APP_PORT**` 默认 **5002**(与前端 `VITE_API_URL` 开发默认一致)。 +- 数据库、邮件、认证中心、`INFOGENIE_SITE_ADMIN_TOKEN`、可选 Redis 等从环境变量读取,详见 `config/config.go`。 + +### Redis(可选) + + +| 变量 | 说明 | +| ------------------ | --------------------------------------------------------- | +| `REDIS_ENABLED` | `true` / `1` / `yes` / `on` 时启用;未启用则不连 Redis,站点接口直连 MySQL | +| `REDIS_ADDR` | `host:port`;生产环境必填;开发未设时默认 `10.1.1.100:6379` | +| `REDIS_PASSWORD` | 可选 | +| `REDIS_DB` | 逻辑库编号,默认 **10**(与 `db0` 等业务隔离) | +| `REDIS_KEY_PREFIX` | Key 前缀,默认 `infogenie:go:v1:`(可自动补 `:`) | +| `REDIS_SITE_TTL` | 站点类 JSON 缓存 TTL(秒),默认 **60** | + + +启用时启动阶段会 **Ping**;失败则进程退出。站点只读接口对 `60s-disabled`、`60s-source`、`ai-model-disabled`、`feature-card-clicks` 等做 cache-aside,管理端写入成功后删对应 Key(见 `internal/cache/redis.go` 与 `internal/handler/siteconfig.go`、`feature_card_clicks.go`)。 + +--- + +## 健康与诊断 + +### `GET /api/health`(公开) + +- `**status`**:`running` 或 `degraded`(MySQL 未连通 **或** 60s 上游探测失败时为 degraded;**不**因 Redis 失败而 degraded)。 +- `**mysql`**:`ok`、`status`(`connected` / `disconnected` / `not_initialized`)。 +- `**sixty_api**`:当前生效的上游 `source_id`、`base_url`、`label`,以及对 `…/v2/ip` 的探测结果(`probe_url`、`http_status`、`latency_ms`、`error`)。 +- `**redis**`:未启用时 `{ "enabled": false }`;启用时 `{ "enabled": true, "ok": bool, "error"?: string }`。 + +### `GET /api/admin/site/diagnostics`(需管理员) + +请求头 `**X-Site-Admin-Token**` 须与 `**INFOGENIE_SITE_ADMIN_TOKEN**` 一致。 + +返回**不含密码明文**,仅连接与进程摘要,供运维/后台展示: + + +| 字段 | 内容 | +| ---------------- | ------------------------------------------------------------------------------------------- | +| `app` | `env`、`listen_addr`(`0.0.0.0:端口`)、`listen_port` | +| `mysql` | `host`、`port`、`database`、`user`、`password_configured` | +| `redis` | `enabled`;若启用则含 `addr`、`logical_db`、`key_prefix`、`site_cache_ttl_sec`、`password_configured` | +| `sixty_upstream` | 库内当前 60s 节点 `source_id` / `base_url` / `label` | +| `auth_center` | `api_url` | +| `mail` | SMTP `host`、`port`、`username`、`password_configured` | + + +--- + +**根路径**:`GET /` — 返回服务说明与主要 endpoint 分组(`version` 当前为 **3.3.0-go**)。 + +--- + +## 数据库(GORM AutoMigrate) + +启动时会迁移以下模型(见 `internal/database/mysql.go`): + + +| 模型 | 用途 | +| ---------------------- | ------------------------------------------------- | +| `AIConfig` | 多厂商 AI Key / Base / 模型列表(如 deepseek、kimi) | +| `Site60sDisabled` | 60s 功能在前端隐藏的 `feature_id` | +| `SiteAIRuntime` | DeepSeek 兼容网关(Base + Key + 默认模型),优先级高于部分 AIConfig | +| `Site60sUpstream` | 60s 上游节点(单例 id=1) | +| `SiteAIModelDisabled` | AI 应用在前端隐藏的 `app_id` | +| `SiteFeatureCardClick` | 四大板块功能卡片点击统计(`section` + `item_id` 联合主键) | + + +--- + +## 路由概览(`internal/router/router.go`) + +### CORS + +全局 `middleware.CORS()`,放行常用 Method/Header(含 `Authorization`、`X-Site-Admin-Token`)。 + +### 认证与用户 + + +| 方法 | 路径 | 说明 | +| --- | ------------------- | -------------- | +| GET | `/api/auth/check` | 可选 JWT:校验登录态 | +| GET | `/api/user/profile` | **需 JWT**:用户资料 | + + +实际登录、发 token 由 **萌芽账户认证中心** 完成;后端校验 JWT。 + +### 站点公开配置(无需登录) + + +| 方法 | 路径 | 说明 | +| ---- | ----------------------------------------- | ------------------------------- | +| GET | `/api/site/60s-disabled` | 被隐藏的 60s `feature_id` 列表 | +| GET | `/api/site/60s-source` | 60s 上游 `source_id` / `base_url` | +| GET | `/api/site/ai-model-disabled` | 被隐藏的 AI 应用 id 列表 | +| GET | `/api/site/feature-card-clicks?section=` | 功能卡片点击次数(见下) | +| POST | `/api/site/feature-card-clicks/increment` | 上报一次点击,返回最新 count | + + +`**section` 合法值**:`60sapi` · `smallgame` · `toolbox` · `aimodel` + +**increment 请求体**:`{ "section": "...", "item_id": "..." }` + +### 站点管理(需 `X-Site-Admin-Token`,与环境变量 `INFOGENIE_SITE_ADMIN_TOKEN` 一致) + + +| 方法 | 路径 | 说明 | +| ------- | ----------------------------------- | ---------------------- | +| PUT | `/api/admin/site/60s-disabled` | 更新 60s 隐藏列表 | +| PUT | `/api/admin/site/60s-source` | 切换 60s 上游 | +| PUT | `/api/admin/site/ai-model-disabled` | 更新 AI 应用隐藏列表 | +| GET/PUT | `/api/admin/site/ai-runtime` | 读取/更新 DeepSeek 兼容运行时配置 | +| GET | `/api/admin/site/diagnostics` | 连接与进程配置快照(不含密钥明文) | + + +### AI 应用(`/api/aimodelapp`,默认 **需 JWT**) + + +| 方法 | 路径 | 说明 | +| ---- | ------------------ | ----------------------------------------------- | +| POST | `/chat` | 非流式对话,JSON 返回全文 | +| POST | `/chat/stream` | **SSE 流式**:透传上游 OpenAI 兼容流(`text/event-stream`) | +| POST | `/name-analysis` 等 | 各垂直能力(姓名、变量命名、写诗、翻译等) | +| GET | `/models` | 模型列表 | + + +**流式说明**(`internal/handler/aimodel.go` + `internal/service/ai.go`): + +- 上游请求带 `stream: true`,成功后将上游 body **分块写入并 Flush** 到客户端。 +- 支持 **deepseek**(运行时或 `AIConfig`)与 **kimi**(`AIConfig`)。 +- 与 `/chat` 共用同一套 `bindAIModelChat` 校验(消息条数、长度、模型白名单等)。 + +**模型白名单**:见 `internal/handler/aimodel.go` 中 `allowedModels`(如 deepseek-chat、deepseek-reasoner、部分 kimi 模型)。 + +--- + +## 核心源码目录 + +``` +cmd/server/ # main +config/ # 配置加载(含 Redis) +internal/ + cache/ # Redis 可选封装(Get/Set JSON、Delete、Ping) + database/ # MySQL 初始化、AutoMigrate + handler/ # HTTP 处理器(auth、user、aimodel、siteconfig、ai_runtime、feature_card_clicks) + middleware/ # CORS、JWT + model/ # GORM 模型 + router/ # 路由注册(含 /api/health、diagnostics) + service/ # AI 调用(含 OpenAI 兼容非流式与流式) +scripts/redis_keyspace/ # 可选:本地查看各逻辑库 keyspace(go run,需 REDIS_PASSWORD) +``` + +--- + +## 与其他工程的关系 + +- **前端 SPA** 通过 `VITE_API_URL` 指向本服务(开发默认 `http://127.0.0.1:5002`)。 +- `**public/aimodelapp/*/shared/ai-chat.js`** 优先调用 `/api/aimodelapp/chat/stream`,失败时回退 `/chat`。 + +更完整的前端集成说明见 `**infogenie-frontend/前端文档.md**`。 \ No newline at end of file diff --git a/万象口袋项目经历.md b/万象口袋项目经历.md index 7fbed010..019b0ca2 100644 --- a/万象口袋项目经历.md +++ b/万象口袋项目经历.md @@ -1,61 +1,6 @@ -# 万象口袋项目-跨平台一站式信息聚合式软件 面试资料 - -**项目名称**:万象口袋项目-跨平台一站式信息聚合式软件 -**时间**:2025.08 - 2026.01 -**技术栈**:Golang,Gin,MySQL,React、 -**技术栈**:Java,SpringBoot,MySQL,React、 -**描述**:跨平台一站式信息聚合式软件 -**上线地址**:https://infogenie.shumengya.top -**开源地址**:https://github.com/shumengya/infogenie - -**本地实现技术栈(当前仓库版本)** -- 前端:React 18.2.0、React Router DOM 6.15.0、Styled Components 6.0.7、Axios 1.5.0、React Hot Toast、React Icons、Create React App -- 前端架构:React Context API(用户态管理)、混合架构(SPA + public 静态HTML模块)、PWA(Service Worker 离线缓存与 SWR 策略) -- 后端:Go 1.25、Gin 1.12、GORM 1.31 + MySQL、gin-contrib/cors、go-playground/validator、joho/godotenv -- 后端能力:外部认证中心 JWT 校验与用户态透传、AI供应商(DeepSeek/Kimi) OpenAI兼容接口调用、并发扣费互斥与重试回退、站点展示配置与AI上游运行时管理 -- 数据库:MySQL(AIUsageRecord、Site60sDisabled、Site60sUpstream、SiteAIRuntime 等表,GORM 自动迁移) -- 工程化:后端 Dockerfile + docker-compose,.env 多环境配置;前端 CRA 构建与 Service Worker 预缓存 - -## 技术特点总结(可用于简历) -- 前后端分离与RESTful:Gin 路由分组与中间件清晰,统一 JSON 响应;前端 Axios 拦截器统一鉴权与错误提示 -- 混合前端架构:React SPA 与 public 静态HTML模块并存,静态页通过 iframe + window.parent 环境注入与本地 token 传递,兼顾体验与加载速度 -- 认证与用户态:对接外部认证中心,后端中间件验证 JWT 并透传用户态;前端 Context 管理登录态与本地持久化 -- AI接口与计费:DeepSeek/Kimi OpenAI兼容调用、超时与指数回退;并发扣费互斥锁、扣费失败与余额不足的严格返回码 -- 站点运行时配置:管理员口令保护的 60s 展示开关与数据源切换、AI上游 base/key/model 运行时配置写入数据库并脱敏展示 -- 安全与合规:CORS 策略受控、敏感配置环境变量化、后端仅在开发打印基础设施地址;管理员端采用常量口令 + 服务端令牌比对 -- 性能优化:PWA 预缓存入口与 runtime 缓存、SW 导航离线回退、静态模块直出;前端渐变背景与轻量组件保持流畅交互 -- 工程化与部署:后端 Dockerfile + docker-compose 支持;前端 CRA 标准构建流程与静态资源缓存策略;多环境 .env - -## 架构设计亮点 -- SPA核心层:React Router v6 管理路由;UserContext 统一登录态;Navigation/Header/Footer 组成稳定布局 -- 静态模块:60sAPI/小游戏/工具箱大量原生HTML/CSS/JS模块,FullscreenEmbed 全屏嵌入并注入 token -- 站点后台:AdminPage 管理 60s 展示开关、数据源与 AI 上游;与后端 X-Site-Admin-Token 双向校验 -- 后端分层:router/middleware/handler/service/database/model 分层清晰;GORM 自动迁移与表结构约束 -- 可靠性:AI 调用指数退避重试、健康检查检测数据库连接状态、并发扣费互斥锁规避竞态 - -## 关键功能模块 -- 60s聚合信息展示:周期资讯/热门榜单/实用功能/消遣娱乐分类配置;支持节点切换(self/official),静态页按 itemId 映射 -- 小游戏集合:2048、俄罗斯方块、别踩白方块、贪吃蛇、扫雷、跑酷、打飞机、FloppyBird、网页魔方等,移动端优化即点即玩 -- AI工具集:变量命名、写诗、姓名评测、语言翻译、文章转文言文、表情生成、Linux命令、Markdown排版、亲戚称呼计算器 -- 认证与用户:认证中心接入,后端 JWT 验证并透传用户资料;前端登录回调与本地存储持久化 -- 计费与统计:每次 AI 调用扣除 100 萌芽币、扣费通过管理员接口更新认证中心;记录 AIUsageRecord 便于统计 - -## 安全与性能实践 -- 认证与保护:后端 JWT 验证中间件、管理员令牌常量 + 服务端校验;并发扣费互斥锁避免竞态 -- 配置安全:环境变量/.env 管理敏感信息,管理员口令与服务端令牌一致性校验;AI Key 脱敏回显与保留策略 -- 前端性能:PWA 预缓存 + runtime SWR、离线导航回退、静态模块直出;Axios 错误统一提示与 401 清理会话 -- 后端健壮:AI 调用超时与指数退避、健康检查检测数据库连接状态、GORM 连接池与生命周期管理 - -## 部署与运维 -- 前端:npm run build 标准构建,Service Worker 自动预缓存入口;支持静态资源离线加载 -- 后端:Dockerfile + docker-compose 一键编排,环境变量控制端口与数据库;GORM 自动迁移表结构 -- 可观测性:AIUsageRecord 与站点配置表为运营与审计提供基础数据;脚本与工具支撑静态页一致性校验 - -## 个人职责与成果(示例措辞) -- 推动 React SPA + 静态HTML 混合架构落地,封装 FullscreenEmbed 组件实现 token 注入与全屏嵌入 -- 设计后端 Gin 分层与中间件体系,接入外部认证中心,完成 JWT 验证与用户态透传 -- 实现 AI 调用链路(DeepSeek/Kimi),加入指数回退与超时控制;并发扣费互斥锁与扣费回滚提示 -- 搭建后端 Docker 与 compose,GORM 自动迁移与连接池优化;完善 .env 多环境配置 -- 开发管理员后台:60s 展示开关、数据源切换、AI 上游运行时配置(密钥脱敏与保留策略) -- 完成 PWA 缓存策略与离线回退、Axios 拦截器统一错误提示,保证移动端与弱网下的可用性 - +- **项目名称**:万象口袋- 跨平台信息聚合网站 +- **时间**:2025.08 - 至今 +- **技术栈**:Golang,Gin,GORM,MySQL,Redis,React,Vite,Tailwind,Docker +- **描述**:产品定位为一站式资讯与工具导航:首页与栏目以配置驱动,整合 60s 类 API 浏览、休闲小游戏、实用工具箱及多款 AI 应用(后端 OpenAI 格式兼容与 SSE 流式输出)。后端采用 Go + Gin + GORM,MySQL 自动迁移存站点开关;可选接入 Redis 对高频只读配置做 cache-aside,独立逻辑库与 Key 前缀避免污染其他业务。提供聚合健康检查与仅管理员可调用的诊断快照接口。前端基于 React 与 Vite,Tailwind 统一视觉,axios 封装与路由守卫;对接萌芽认证中心 JWT,管理员后台支持 AI 上游、前台显隐配置及运行状态监控。支持多环境变量与开发/生产数据库隔离约束,兼顾可运维与交付效率。 +- **项目地址**:[https://infogenie.smyhub.com](https://infogenie.smyhub.com) **|** **开源地址**:[https://github.com/shumengya/infogenie](https://github.com/shumengya/infogenie) +