diff --git a/.gitignore b/.gitignore index 9723a872..d21f8982 100644 --- a/.gitignore +++ b/.gitignore @@ -39,3 +39,7 @@ InfoGenie-backend/.env # 允许提交模板(勿填真实密钥) !infogenie-frontend/.env.example !infogenie-backend-go/.env.example + +# Local docs / prompt assets not meant for repository upload +万象口袋项目经历-*.md +infogenie-frontend/public/aimodelapp/shared/ai-prompts.js diff --git a/InfoGenie-frontend/index.html b/InfoGenie-frontend/index.html index 63191cc8..717118ba 100644 --- a/InfoGenie-frontend/index.html +++ b/InfoGenie-frontend/index.html @@ -38,8 +38,8 @@ - - + + 万象口袋 diff --git a/InfoGenie-frontend/public/manifest.json b/InfoGenie-frontend/public/manifest.json index 6c2dbde1..152e3122 100755 --- a/InfoGenie-frontend/public/manifest.json +++ b/InfoGenie-frontend/public/manifest.json @@ -1,7 +1,7 @@ { "short_name": "万象口袋", - "name": "万象口袋 - 多功能聚合应用", - "description": "万象口袋 - 多功能聚合应用", + "name": "万象口袋", + "description": "万象口袋", "icons": [ { "src": "/icons/icon-192.png", @@ -36,9 +36,9 @@ "categories": ["utilities", "productivity", "entertainment"], "screenshots": [ { - "src": "data:image/svg+xml,万象口袋", - "type": "image/svg+xml", - "sizes": "400x800", + "src": "/icons/icon-192.png", + "type": "image/png", + "sizes": "192x192", "form_factor": "narrow" } ] diff --git a/InfoGenie-frontend/src/config/Api60sConfig.js b/InfoGenie-frontend/src/config/Api60sConfig.js index 365410fa..501b0101 100644 --- a/InfoGenie-frontend/src/config/Api60sConfig.js +++ b/InfoGenie-frontend/src/config/Api60sConfig.js @@ -1,13 +1,15 @@ /** * 60s API 聚合接口配置 - * 基于 https://docs.60s-api.viki.moe/ 文档整理 + * 基于 60s-api 官方文档整理 * 所有 endpoint 均为 /v2/ 前缀的相对路径,配合 apiBaseUrl 使用 */ +import { tlsHost } from './urlJoin.js'; + // 可选的 API 数据源列表,前端可切换 export const API_SOURCES = [ - { id: 'self', label: '萌芽节点', baseUrl: 'https://60s.api.shumengya.top' }, - { id: 'official', label: '官方节点', baseUrl: 'https://60s.viki.moe' }, + { id: 'self', label: '萌芽节点', baseUrl: tlsHost('60s.api.shumengya.top') }, + { id: 'official', label: '官方节点', baseUrl: tlsHost('60s.viki.moe') }, ]; // 默认 API 数据源 @@ -126,6 +128,9 @@ const TOOLBOX_IMAGE = [ { id: 'tb-img-b64', title: '图片转 Base64', icon: '🔀', link: '/toolbox/图片处理/图片转Base64编码/index.html', desc: '编码转换', offlineOk: true }, { id: 'tb-img-gif-split', title: 'GIF拆帧', icon: '🎞️', link: '/toolbox/图片处理/GIF拆帧/index.html', desc: 'GIF帧提取', offlineOk: true }, { id: 'tb-img-webp', title: '图片转 WebP', icon: '🌐', link: '/toolbox/图片处理/图片转webp格式/index.html', desc: 'WebP格式转换', offlineOk: true }, + { id: 'tb-img-watermark', title: '图片加文字水印', icon: '✏️', link: '/toolbox/图片处理/图片加文字水印/图片加文字水印.html', desc: '为图片添加文字水印', offlineOk: true }, + { id: 'tb-img-compress', title: '图片压缩', icon: '🗜️', link: '/toolbox/图片处理/图片压缩处理/图片压缩处理.html', desc: '压缩图片体积', offlineOk: true }, + { id: 'tb-img-png-jpg', title: 'PNG / JPG 互转', icon: '🔁', link: '/toolbox/图片处理/png和jpg格式互转/png和jpg相互转化.html', desc: 'PNG 与 JPG 格式转换', offlineOk: true }, ]; const TOOLBOX_UTIL = [ @@ -135,6 +140,7 @@ const TOOLBOX_UTIL = [ { id: 'tb-json', title: 'JSON 编辑器', icon: '📑', link: '/toolbox/实用工具/Json编辑器/index.html', desc: '格式化与校验', offlineOk: true }, { id: 'tb-markdown', title: 'Markdown', icon: '⌨️', link: '/toolbox/实用工具/Markdown解析器/index.html', desc: '实时预览', offlineOk: true }, { id: 'tb-js', title: 'JavaScript', icon: '🟩', link: '/toolbox/实用工具/JavaScript编译器/index.html', desc: '本地运行脚本', offlineOk: true }, + { id: 'tb-c-lang', title: 'C 语言编译器', icon: '⚙️', link: '/toolbox/实用工具/C语言编译器/网页C语言编译器.html', desc: '网页端编写与运行 C', offlineOk: true }, { id: 'tb-random-num', title: '随机数', icon: '🎲', link: '/toolbox/实用工具/随机数生成器/index.html', desc: '自定义范围', offlineOk: true }, { id: 'tb-wheel', title: '做决定转盘', icon: '🎡', link: '/toolbox/实用工具/做决定转盘/index.html', desc: '随机选择', offlineOk: true }, { id: 'tb-calc', title: '计算器', icon: '🔢', link: '/toolbox/实用工具/计算器/index.html', desc: '基础运算', offlineOk: true }, diff --git a/InfoGenie-frontend/src/config/env.js b/InfoGenie-frontend/src/config/env.js index 0f1d9450..846ff6b0 100644 --- a/InfoGenie-frontend/src/config/env.js +++ b/InfoGenie-frontend/src/config/env.js @@ -1,14 +1,16 @@ +import { tlsHost, loopbackPort } from './urlJoin.js'; + const resolveApiUrl = () => { const mode = import.meta.env.MODE; const envApiUrl = import.meta.env.VITE_API_URL; if (mode === 'production') { if (envApiUrl && envApiUrl.trim() !== '') return envApiUrl; - return 'https://infogenie.api.shumengya.top'; + return tlsHost('infogenie.api.shumengya.top'); } if (envApiUrl && envApiUrl.trim() !== '') return envApiUrl; - return 'http://127.0.0.1:5002'; + return loopbackPort('127.0.0.1', '5002'); }; const resolveAuthUrl = () => { @@ -17,11 +19,11 @@ const resolveAuthUrl = () => { if (mode === 'production') { if (envAuthUrl && envAuthUrl.trim() !== '') return envAuthUrl; - return 'https://auth.shumengya.top'; + return tlsHost('auth.shumengya.top'); } if (envAuthUrl && envAuthUrl.trim() !== '') return envAuthUrl; - return 'https://auth.shumengya.top'; + return tlsHost('auth.shumengya.top'); }; const resolveAuthApiUrl = () => { @@ -30,11 +32,11 @@ const resolveAuthApiUrl = () => { if (mode === 'production') { if (envAuthApiUrl && envAuthApiUrl.trim() !== '') return envAuthApiUrl; - return 'https://auth.api.shumengya.top'; + return tlsHost('auth.api.shumengya.top'); } if (envAuthApiUrl && envAuthApiUrl.trim() !== '') return envAuthApiUrl; - return 'https://auth.api.shumengya.top'; + return tlsHost('auth.api.shumengya.top'); }; const config = { diff --git a/InfoGenie-frontend/src/config/urlJoin.js b/InfoGenie-frontend/src/config/urlJoin.js new file mode 100644 index 00000000..b8af7f4c --- /dev/null +++ b/InfoGenie-frontend/src/config/urlJoin.js @@ -0,0 +1,12 @@ +/** 避免源码中出现 https/http 连续字符,否则 Tailwind 扫描会误生成无效任意属性类 */ +const c = String.fromCharCode(58); +const tlsScheme = String.fromCharCode(104, 116, 116, 112, 115); +const plainScheme = String.fromCharCode(104, 116, 116, 112); + +export function tlsHost(host) { + return `${tlsScheme}${c}//${host}`; +} + +export function loopbackPort(host, port) { + return `${plainScheme}${c}//${host}${c}${port}`; +} diff --git a/InfoGenie-frontend/src/pages/AdminPage.js b/InfoGenie-frontend/src/pages/AdminPage.js index 8f5b105f..dd884f60 100644 --- a/InfoGenie-frontend/src/pages/AdminPage.js +++ b/InfoGenie-frontend/src/pages/AdminPage.js @@ -11,6 +11,10 @@ import { ENV_CONFIG } from '../config/env'; import { API_CATEGORIES, TOOLBOX_ITEMS, API_SOURCES } from '../config/Api60sConfig'; import { AI_MODEL_APPS } from '../config/StaticPageConfig'; +const gridOverviewStats = { gridTemplateColumns: 'repeat(auto-fit, minmax(200px, 1fr))' }; +const gridQuickActions = { gridTemplateColumns: 'repeat(auto-fit, minmax(180px, 1fr))' }; +const gridAdminItems = { gridTemplateColumns: 'repeat(auto-fill, minmax(210px, 1fr))' }; + const InfoCard = ({ children }) => (
{children}
); @@ -458,7 +462,7 @@ const AdminPage = () => { {adminSection === 'overview' && ( <> -
+
{[ { icon: FiGrid, bg: 'linear-gradient(135deg,#4ade80,#22c55e)', label: '60sAPI', value: apiCount }, { icon: FiBox, bg: 'linear-gradient(135deg,#60a5fa,#3b82f6)', label: '休闲游戏', value: gameCount }, @@ -496,7 +500,7 @@ const AdminPage = () => {

快捷操作

-
+
刷新后端状态 清除 SW 缓存 清理本地存储 @@ -733,7 +737,7 @@ const AdminPage = () => { ) : ( <> {[ - { id: 'ai-base', label: 'API Base URL', type: 'text', value: aiBase, onChange: setAiBase, placeholder: 'https://api.deepseek.com' }, + { id: 'ai-base', label: 'API Base URL', type: 'text', value: aiBase, onChange: setAiBase, placeholder: '例如 api.deepseek.com' }, { id: 'ai-key', label: `API Key${aiKeySet ? `(已配置:${aiKeyHint})` : ''}`, type: 'password', value: aiKey, onChange: setAiKey, placeholder: aiKeySet ? '留空保留原密钥;填写则覆盖' : '必填,保存后仅显示脱敏尾号', autoComplete: 'new-password' }, { id: 'ai-model', label: '默认模型 ID', type: 'text', value: aiModel, onChange: setAiModel, placeholder: 'deepseek-chat' }, ].map(({ id, label, type, value, onChange, placeholder, autoComplete }) => ( @@ -756,11 +760,11 @@ const AdminPage = () => {

选择 60s 类接口的上游根地址(与 Api60sConfig.API_SOURCES 对齐)。

-
+
{API_SOURCES.map((s) => ( setSixtySrcId(s.id)} /> - {s.label} ({s.baseUrl.replace(/^https?:\/\//, '')}) + {s.label} ({s.baseUrl.replace(/^[^/]+\/\//, '')}) ))}
@@ -783,7 +787,7 @@ const AdminPage = () => { {API_CATEGORIES.map((cat) => (

{cat.icon} {cat.title}

-
+
{cat.items.map((item) => ( setItemVisible(item.id, e.target.checked)} /> @@ -811,7 +815,7 @@ const AdminPage = () => {

正在加载配置…

) : ( <> -
+
{AI_MODEL_APPS.map((app) => ( setAIModelVisible(app.id, e.target.checked)} /> diff --git a/InfoGenie-frontend/src/styles/shared.js b/InfoGenie-frontend/src/styles/shared.js index 6839737c..810e8c44 100644 --- a/InfoGenie-frontend/src/styles/shared.js +++ b/InfoGenie-frontend/src/styles/shared.js @@ -187,9 +187,9 @@ export function accentFromGradient(gradient) { export const ModuleGrid = ({ children, className = '' }) => (
diff --git a/InfoGenie-frontend/vite.config.js b/InfoGenie-frontend/vite.config.js index 3611c4a7..e0bef994 100644 --- a/InfoGenie-frontend/vite.config.js +++ b/InfoGenie-frontend/vite.config.js @@ -7,6 +7,8 @@ import { fileURLToPath } from 'url'; const __dirname = path.dirname(fileURLToPath(import.meta.url)); export default defineConfig({ + // 若为 true,esbuild 会对 Tailwind 误生成的 gri 等无效规则告警;可改回 true 并接受提示 + build: { cssMinify: false }, plugins: [ // Allow JSX in .js files (all files under src/) { diff --git a/infogenie-backend-go/.env.example b/infogenie-backend-go/.env.example deleted file mode 100644 index 6ee2888c..00000000 --- a/infogenie-backend-go/.env.example +++ /dev/null @@ -1,22 +0,0 @@ -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/.env.production.example b/infogenie-backend-go/.env.production.example new file mode 100644 index 00000000..e754f95d --- /dev/null +++ b/infogenie-backend-go/.env.production.example @@ -0,0 +1,23 @@ +# 复制为 .env.production 后按需填写(勿提交真实密钥) +APP_ENV=production +APP_PORT=5002 + +# ===== MySQL(容器内访问宿主机时勿用 127.0.0.1)===== +# 例:宿主机 MySQL 监听 3306 → DB_HOST=172.17.0.1(Linux 默认网桥)或宿主机局域网 IP +# Docker Desktop(Windows/Mac):DB_HOST=host.docker.internal +DB_HOST=172.17.0.1 +DB_PORT=3306 +DB_NAME=infogenie +DB_USER=infogenie +DB_PASSWORD= + +# ===== Redis(可选;启用则必须能 Ping 通,否则服务无法启动)===== +# REDIS_ENABLED=false +# REDIS_ADDR=172.17.0.1:6379 +# REDIS_PASSWORD= +# REDIS_DB=10 +# REDIS_KEY_PREFIX=infogenie:go:v1: +# REDIS_SITE_TTL=60 + +AUTH_CENTER_API_URL=https://auth.api.shumengya.top +INFOGENIE_SITE_ADMIN_TOKEN= diff --git a/infogenie-backend-go/.gitignore b/infogenie-backend-go/.gitignore index 6cf90272..0e68bc3b 100644 --- a/infogenie-backend-go/.gitignore +++ b/infogenie-backend-go/.gitignore @@ -2,3 +2,7 @@ .env .env.* !.env.example +!.env.production.example + +# 本机构建的 Linux 二进制(Dockerfile.prebuilt 使用) +dist/ diff --git a/infogenie-backend-go/Dockerfile b/infogenie-backend-go/Dockerfile index d7155ad8..479fc4ad 100644 --- a/infogenie-backend-go/Dockerfile +++ b/infogenie-backend-go/Dockerfile @@ -1,4 +1,4 @@ -# 多阶段构建:生产镜像仅含二进制与 ai_config.json,敏感配置通过运行时环境变量或 env_file 注入(勿将 .env.production 打入镜像) +# 多阶段构建:生产镜像仅含二进制,敏感配置通过运行时环境变量或 env_file 注入 FROM golang:1.24-alpine AS builder WORKDIR /src RUN apk add --no-cache git ca-certificates diff --git a/infogenie-backend-go/Dockerfile.prebuilt b/infogenie-backend-go/Dockerfile.prebuilt new file mode 100644 index 00000000..1f9b0a05 --- /dev/null +++ b/infogenie-backend-go/Dockerfile.prebuilt @@ -0,0 +1,12 @@ +# 使用本机/CI 预先构建的 Linux amd64 二进制(见 build-linux-amd64.bat),镜像内不再执行 go build +# 构建前请先运行: build-linux-amd64.bat 生成 dist\server +FROM alpine:3.21 +RUN apk --no-cache add ca-certificates tzdata +ENV TZ=Asia/Shanghai +WORKDIR /app +COPY dist/server ./server +RUN chmod +x ./server +EXPOSE 5002 +ENV APP_ENV=production +ENV APP_PORT=5002 +CMD ["./server"] diff --git a/infogenie-backend-go/build-linux-amd64.bat b/infogenie-backend-go/build-linux-amd64.bat new file mode 100644 index 00000000..c62cf4bc --- /dev/null +++ b/infogenie-backend-go/build-linux-amd64.bat @@ -0,0 +1,21 @@ +@echo off +setlocal EnableExtensions +cd /d "%~dp0" + +REM 一键交叉编译 Linux amd64,供 Dockerfile.prebuilt 直接 COPY dist\server(服务器上无需再 go build) +if not exist "dist" mkdir "dist" + +set "CGO_ENABLED=0" +set "GOOS=linux" +set "GOARCH=amd64" +set "GOTOOLCHAIN=auto" + +echo [build-linux-amd64] GOOS=%GOOS% GOARCH=%GOARCH% GOTOOLCHAIN=%GOTOOLCHAIN% +go build -trimpath -ldflags="-s -w" -o "dist\server" .\cmd\server +if errorlevel 1 ( + echo [build-linux-amd64] FAILED + exit /b 1 +) + +echo [build-linux-amd64] OK: dist\server +exit /b 0 diff --git a/infogenie-backend-go/docker-compose.yml b/infogenie-backend-go/docker-compose.yml index 3c079399..987c920b 100644 --- a/infogenie-backend-go/docker-compose.yml +++ b/infogenie-backend-go/docker-compose.yml @@ -1,10 +1,24 @@ # 生产:宿主机 12364 -> 容器内 5002;容器名 infogenie-backend-go -# 使用:在同级目录准备 .env.production(数据库等),然后 docker compose up -d --build +# +# 推荐(不在服务器编译 Go):先在 Windows 运行 build-linux-amd64.bat 生成 dist\server, +# 再执行: docker compose up -d --build +# 镜像使用 Dockerfile.prebuilt,仅打包 Alpine + 二进制。 +# +# 若要在 Docker 内多阶段编译,改用: docker compose -f docker-compose.yml -f docker-compose.sourcebuild.yml up -d --build +# +# ========== 前端连不上 Docker 后端时排查 ========== +# 1) 容器是否起来: docker ps 看 infogenie-backend-go;宿主机 curl http://127.0.0.1:12364/api/health +# 2) .env.production 里 DB_HOST/REDIS_ADDR:容器内 127.0.0.1 是容器自己,不是宿主机。 +# MySQL/Redis 在宿主机时:Linux 常用 172.17.0.1 或宿主机内网 IP;Docker Desktop 可用 host.docker.internal +# 3) REDIS_ENABLED=true 时 Redis 必须可达,否则进程启动失败(main 里 cache.Init 会报错退出) +# 4) 前端生产构建的 VITE_API_URL 必须是浏览器能访问的地址(HTTPS 域名反代到 12364,或公网 IP+端口), +# 勿指向仅内网可达的 IP;与 env.js 里生产默认 https://infogenie.api.shumengya.top 一致时需 DNS/反代已配置 +# 5) 云服务器安全组/防火墙放行 12364(若直连端口) services: infogenie-backend-go: build: context: . - dockerfile: Dockerfile + dockerfile: Dockerfile.prebuilt image: infogenie-backend-go:latest container_name: infogenie-backend-go ports: diff --git a/infogenie-backend-go/scripts/sync_mysql_test_to_prod.py b/infogenie-backend-go/scripts/sync_mysql_test_to_prod.py new file mode 100644 index 00000000..65854cab --- /dev/null +++ b/infogenie-backend-go/scripts/sync_mysql_test_to_prod.py @@ -0,0 +1,219 @@ +#!/usr/bin/env python3 +""" +将 InfoGenie Go 后端使用的 MySQL 表从「测试库」全量同步到「生产库」。 +同步时在目标库对每张表执行:DROP TABLE IF EXISTS → 按源库 SHOW CREATE TABLE 重建 → 插入源库数据, +确保列结构与测试库一致(旧生产表结构会被覆盖)。 + +使用前务必备份生产库。会删除并重建生产库中同名表。 + +环境变量(未设置时沿用仓库内 test/test_mysql_connect.py 的约定): + 源库:DB_SRC_HOST, DB_SRC_PORT, DB_SRC_NAME, DB_SRC_USER, DB_SRC_PASSWORD + 目标:DB_DST_HOST, DB_DST_PORT, DB_DST_NAME, DB_DST_USER, DB_DST_PASSWORD + +示例(PowerShell): + $env:DB_DST_HOST='192.168.1.100' + $env:DB_DST_NAME='infogenie' + $env:DB_DST_USER='infogenie' + $env:DB_DST_PASSWORD='***' + python scripts/sync_mysql_test_to_prod.py --dry-run + python scripts/sync_mysql_test_to_prod.py --execute +""" + +from __future__ import annotations + +import argparse +import os +import sys +from typing import Any, List, Sequence, Tuple + +try: + import pymysql +except ImportError: + print("缺少依赖: pip install pymysql", file=sys.stderr) + sys.exit(2) + +# 与 internal/database/mysql.go AutoMigrate 一致 +TABLES: Tuple[str, ...] = ( + "ai_configs", + "site_60s_disabled", + "site_ai_runtime", + "site_60s_upstream", + "site_ai_model_disabled", + "site_feature_card_clicks", +) + +DEFAULT_SRC = { + "host": "10.1.1.100", + "port": 3306, + "name": "infogenie-test", + "user": "infogenie-test", + "password": "infogenie-test", +} # 仅作文档占位;实际以 DB_SRC_* 为准 + +DEFAULT_DST = { + "host": "192.168.1.100", + "port": 3306, + "name": "infogenie", + "user": "infogenie", + "password": "infogenie", +} + + +def _env_int(key: str, default: int) -> int: + raw = os.environ.get(key) + if raw is None or str(raw).strip() == "": + return default + return int(str(raw).strip()) + + +def conn_params_src() -> dict: + return { + "host": os.environ.get("DB_SRC_HOST", DEFAULT_SRC["host"]), + "port": _env_int("DB_SRC_PORT", DEFAULT_SRC["port"]), + "database": os.environ.get("DB_SRC_NAME", DEFAULT_SRC["name"]), + "user": os.environ.get("DB_SRC_USER", DEFAULT_SRC["user"]), + "password": os.environ.get("DB_SRC_PASSWORD", DEFAULT_SRC["password"]), + } + + +def conn_params_dst() -> dict: + return { + "host": os.environ.get("DB_DST_HOST", DEFAULT_DST["host"]), + "port": _env_int("DB_DST_PORT", DEFAULT_DST["port"]), + "database": os.environ.get("DB_DST_NAME", DEFAULT_DST["name"]), + "user": os.environ.get("DB_DST_USER", DEFAULT_DST["user"]), + "password": os.environ.get("DB_DST_PASSWORD", DEFAULT_DST["password"]), + } + + +def open_conn(**kw: Any): + return pymysql.connect( + host=kw["host"], + port=int(kw["port"]), + user=kw["user"], + password=kw["password"], + database=kw["database"], + charset="utf8mb4", + connect_timeout=15, + cursorclass=pymysql.cursors.Cursor, + ) + + +def fetch_all_rows(cur, table: str) -> Tuple[List[str], List[tuple]]: + cur.execute(f"SELECT * FROM `{table}`") + rows = cur.fetchall() + cols = [d[0] for d in cur.description] if cur.description else [] + return cols, list(rows) + + +def table_exists(cur, table: str) -> bool: + cur.execute("SHOW TABLES LIKE %s", (table,)) + return cur.fetchone() is not None + + +def recreate_table_from_source(source, target, table: str) -> None: + """用源库 DDL 在目标库 DROP 后重建,保证列与测试库一致(覆盖旧结构)。""" + with source.cursor() as sc: + sc.execute(f"SHOW CREATE TABLE `{table}`") + row = sc.fetchone() + if not row: + raise RuntimeError(f"源库不存在表: {table}") + ddl = row[1] + with target.cursor() as tc: + tc.execute("SET FOREIGN_KEY_CHECKS=0") + tc.execute(f"DROP TABLE IF EXISTS `{table}`") + tc.execute(ddl) + tc.execute("SET FOREIGN_KEY_CHECKS=1") + target.commit() + + +def dry_run(src: dict, dst: dict, tables: Sequence[str]) -> None: + s = open_conn(**src) + d = open_conn(**dst) + try: + with s.cursor() as sc, d.cursor() as dc: + for t in tables: + sc.execute(f"SELECT COUNT(*) FROM `{t}`") + (n_src,) = sc.fetchone() + if not table_exists(dc, t): + print(f"{t}: source_rows={n_src} target_rows=(表不存在)") + else: + dc.execute(f"SELECT COUNT(*) FROM `{t}`") + (n_dst,) = dc.fetchone() + print(f"{t}: source_rows={n_src} target_rows={n_dst}") + finally: + s.close() + d.close() + + +def sync_tables(src: dict, dst: dict, tables: Sequence[str]) -> None: + source = open_conn(**src) + target = open_conn(**dst) + try: + with source.cursor() as sc: + for table in tables: + recreate_table_from_source(source, target, table) + print(f"++ {table}: 目标表已按源库 DDL 重建") + cols, rows = fetch_all_rows(sc, table) + if not cols: + raise RuntimeError(f"无法读取列: {table}") + placeholders = ",".join(["%s"] * len(cols)) + col_sql = ",".join(f"`{c}`" for c in cols) + insert_sql = f"INSERT INTO `{table}` ({col_sql}) VALUES ({placeholders})" + with target.cursor() as tc: + if rows: + tc.executemany(insert_sql, rows) + target.commit() + print(f"OK {table}: copied {len(rows)} rows") + finally: + source.close() + target.close() + + +def main() -> None: + ap = argparse.ArgumentParser(description="Sync InfoGenie MySQL tables from test to production.") + ap.add_argument( + "--execute", + action="store_true", + help="执行同步(默认仅打印说明;不加此参数则退出码 1)", + ) + ap.add_argument("--dry-run", action="store_true", help="仅统计源/目标行数,不写生产库") + ap.add_argument( + "--tables", + type=str, + default="", + help="逗号分隔表名子集;默认同步全部 GORM 表", + ) + args = ap.parse_args() + + src = conn_params_src() + dst = conn_params_dst() + + tables: Tuple[str, ...] + if args.tables.strip(): + tables = tuple(x.strip() for x in args.tables.split(",") if x.strip()) + bad = [t for t in tables if t not in TABLES] + if bad: + print(f"未知表名(不在白名单): {bad}", file=sys.stderr) + sys.exit(1) + else: + tables = TABLES + + print(f"SOURCE: {src['user']}@{src['host']}:{src['port']}/{src['database']}") + print(f"TARGET: {dst['user']}@{dst['host']}:{dst['port']}/{dst['database']}") + print(f"TABLES: {', '.join(tables)}") + + if args.dry_run: + dry_run(src, dst, tables) + return + + if not args.execute: + print("未执行:请加 --dry-run 查看行数,或加 --execute 在确认备份后同步。", file=sys.stderr) + sys.exit(1) + + sync_tables(src, dst, tables) + print("完成。") + + +if __name__ == "__main__": + main() diff --git a/infogenie-backend-go/后端文档.md b/infogenie-backend-go/后端文档.md index bca45331..6d1234af 100644 --- a/infogenie-backend-go/后端文档.md +++ b/infogenie-backend-go/后端文档.md @@ -8,6 +8,16 @@ --- +## Docker 生产部署(预编译二进制) + +1. **本机(Windows)**:在 `infogenie-backend-go` 目录运行 `build-linux-amd64.bat`,生成 `dist/server`(Linux amd64)。 +2. **构建镜像**:`docker compose up -d --build`(默认使用 `Dockerfile.prebuilt`,不在服务器执行 `go build`)。 +3. **若要在镜像内编译**:`docker compose -f docker-compose.yml -f docker-compose.sourcebuild.yml up -d --build`(使用原 `Dockerfile` 多阶段构建)。 +4. **端口**:`docker-compose.yml` 映射 `12364:5002`;浏览器访问的 API 基址须与前端 `VITE_API_URL` 一致(HTTPS 反代或公网可达地址)。 +5. **`.env.production`**:容器内 `DB_HOST=127.0.0.1` 指向容器自身,**不能**访问宿主机 MySQL。MySQL/Redis 在宿主机时见仓库内 `.env.production.example` 注释(如 `172.17.0.1`、`host.docker.internal`)。`REDIS_ENABLED=true` 时 Redis 必须可达,否则进程启动即失败。 + +--- + ## 运行与配置 - 环境由 `**APP_ENV`** 决定:`development` 或 `production`(见 `config.Load()`)。 diff --git a/万象口袋项目经历-go.md b/万象口袋项目经历-go.md deleted file mode 100644 index 019b0ca2..00000000 --- a/万象口袋项目经历-go.md +++ /dev/null @@ -1,6 +0,0 @@ -- **项目名称**:万象口袋- 跨平台信息聚合网站 -- **时间**: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) - diff --git a/万象口袋项目经历-java.md b/万象口袋项目经历-java.md deleted file mode 100644 index 09b995f4..00000000 --- a/万象口袋项目经历-java.md +++ /dev/null @@ -1,6 +0,0 @@ -- **项目名称**:万象口袋- 跨平台信息聚合网站 -- **时间**:2025.08 - 2026.01 -- **技术栈**:Java 17,Spring Boot,Spring Web,Spring Data JPA,Hibernate,MySQL,Redis,Lettuce,React,Tailwind,Maven -- **描述**:产品定位为一站式资讯与工具导航:首页与栏目以配置驱动,整合 60s 类 API 浏览、休闲小游戏、实用工具箱及多款 AI 应用(后端 OpenAI 格式兼容与 SSE 流式输出)。后端采用 Spring Boot 分层架构(Controller / Service / Repository / DTO),JPA `ddl-auto=update` 维护与产品一致的 MySQL 业务表;可选 Redis(`app.redis-enabled`)对站点只读配置做 cache-aside,独立逻辑库与 Key 前缀隔离。公开 `/api/health` 与管理员 `/api/admin/site/diagnostics` 输出结构化 JSON,供管理后台监控中间件与连接摘要;Bearer 由过滤器转发认证中心校验,管理写入依赖 `X-Site-Admin-Token`,接口入参 snake_case 与前端约定一致。前端基于 React 与 Tailwind,axios 与路由守卫;管理员后台支持 AI 上游、前台显隐与运行状态。Java 侧侧重开发与联调,可与前端通过 `REACT_APP_API_URL` 指向本服务端口对接。 -- **项目地址**:[https://infogenie.smyhub.com](https://infogenie.smyhub.com) **|** **开源地址**:[https://github.com/shumengya/infogenie](https://github.com/shumengya/infogenie) -