更新前后端配置并清理仓库内容

This commit is contained in:
2026-04-12 16:37:36 +08:00
parent dbd72610c1
commit d373ce1da9
20 changed files with 362 additions and 63 deletions

View File

@@ -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

View File

@@ -0,0 +1,23 @@
# 复制为 .env.production 后按需填写(勿提交真实密钥)
APP_ENV=production
APP_PORT=5002
# ===== MySQL容器内访问宿主机时勿用 127.0.0.1=====
# 例:宿主机 MySQL 监听 3306 → DB_HOST=172.17.0.1Linux 默认网桥)或宿主机局域网 IP
# Docker DesktopWindows/MacDB_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=

View File

@@ -2,3 +2,7 @@
.env
.env.*
!.env.example
!.env.production.example
# 本机构建的 Linux 二进制Dockerfile.prebuilt 使用)
dist/

View File

@@ -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

View File

@@ -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"]

View File

@@ -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

View File

@@ -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 或宿主机内网 IPDocker 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:

View File

@@ -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()

View File

@@ -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()`)。