feat: 交付官方 Docker 运行时,外观改背景图并下线自定义 CSS/JS

站点/后台分轨背景与用户列表排序一并落地;生产 CORS 改走 SITE_URL,健康检查带版本号。

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-09-18 05:27:33 +08:00
parent 5f7193042f
commit d784b0ea7a
68 changed files with 2588 additions and 768 deletions

View File

@@ -12,12 +12,14 @@ import (
// Config 应用全局配置
type Config struct {
WorkPath string
Port int
DataDir string
JWTSecret string
DBDSN string
DevMode bool
WorkPath string
Port int
DataDir string
JWTSecret string
DBDSN string
DevMode bool
SiteURL string // 对外站点 origin,如 https://bbs.example.com;生产 CORS 用
CORSOrigins []string // 额外允许的 CORS origin(逗号分隔环境变量)
}
// Parse 解析配置:环境变量 > app.ini > 默认值。
@@ -84,6 +86,19 @@ func Parse() (*Config, error) {
if v := os.Getenv("DEV_MODE"); v != "" {
cfg.DevMode = strings.EqualFold(v, "true") || v == "1"
}
if v := strings.TrimSpace(os.Getenv("SITE_URL")); v != "" {
cfg.SiteURL = strings.TrimRight(v, "/")
}
if v := strings.TrimSpace(os.Getenv("CORS_ORIGINS")); v != "" {
cfg.CORSOrigins = splitCSVOrigins(v)
}
if v := strings.TrimSpace(os.Getenv("DATA_DIR")); v != "" {
abs, err := filepath.Abs(v)
if err != nil {
return nil, fmt.Errorf("解析 DATA_DIR 失败: %w", err)
}
cfg.DataDir = abs
}
if err := rejectStrayDataDir(cfg.DataDir); err != nil {
return nil, err
@@ -108,6 +123,40 @@ func Parse() (*Config, error) {
return cfg, nil
}
// AllowOrigin 供 CORS 中间件判断浏览器 Origin。
// 开发态只放行 localhost / 127.0.0.1 的 3000 端口(与 next dev 一致);
// 生产态放行 SITE_URL 与 CORS_ORIGINS。同源反代下浏览器不依赖 CORS,此列表作兜底。
func (c *Config) AllowOrigin(origin string) bool {
if origin == "" {
return false
}
origin = strings.TrimRight(origin, "/")
if c.DevMode {
return origin == "http://localhost:3000" || origin == "http://127.0.0.1:3000"
}
if c.SiteURL != "" && origin == c.SiteURL {
return true
}
for _, o := range c.CORSOrigins {
if origin == o {
return true
}
}
return false
}
func splitCSVOrigins(v string) []string {
parts := strings.Split(v, ",")
out := make([]string, 0, len(parts))
for _, p := range parts {
s := strings.TrimRight(strings.TrimSpace(p), "/")
if s != "" {
out = append(out, s)
}
}
return out
}
// resolveWorkPath 定位后端根目录(app.ini 与 data/ 所在处)。
// 优先级:JIANG13_WORK_PATH > 从 cwd 向上找 app.ini > 源码树 backend/ > cwd。
func resolveWorkPath() (string, error) {

View File

@@ -88,3 +88,54 @@ func TestRejectStrayDataDir(t *testing.T) {
t.Fatal(err)
}
}
func TestAllowOriginDevAndProd(t *testing.T) {
dev := &Config{DevMode: true}
if !dev.AllowOrigin("http://localhost:3000") || !dev.AllowOrigin("http://127.0.0.1:3000") {
t.Fatal("开发态应放行 next dev Origin")
}
if dev.AllowOrigin("https://bbs.example.com") {
t.Fatal("开发态不应放行生产域名")
}
prod := &Config{
DevMode: false,
SiteURL: "https://bbs.example.com",
CORSOrigins: []string{"https://mirror.example.com"},
}
if !prod.AllowOrigin("https://bbs.example.com/") {
t.Fatal("生产态应放行 SITE_URL")
}
if !prod.AllowOrigin("https://mirror.example.com") {
t.Fatal("生产态应放行 CORS_ORIGINS")
}
if prod.AllowOrigin("http://localhost:3000") {
t.Fatal("生产态不应放行 localhost")
}
}
func TestParseSiteURLAndDataDir(t *testing.T) {
work := t.TempDir()
data := t.TempDir()
t.Setenv("JIANG13_WORK_PATH", work)
t.Setenv("DEV_MODE", "false")
t.Setenv("SITE_URL", "https://bbs.example.com/")
t.Setenv("CORS_ORIGINS", " https://a.example.com ,https://b.example.com/ ")
t.Setenv("DATA_DIR", data)
t.Setenv("JWT_SECRET", "test-secret-not-for-prod")
cfg, err := Parse()
if err != nil {
t.Fatal(err)
}
if cfg.DevMode {
t.Fatal("DEV_MODE=false 应关闭开发态")
}
if cfg.SiteURL != "https://bbs.example.com" {
t.Fatalf("SITE_URL 未去尾斜杠: %q", cfg.SiteURL)
}
if len(cfg.CORSOrigins) != 2 || cfg.CORSOrigins[0] != "https://a.example.com" || cfg.CORSOrigins[1] != "https://b.example.com" {
t.Fatalf("CORS_ORIGINS 解析错误: %#v", cfg.CORSOrigins)
}
assertPath(t, cfg.DataDir, data)
}