clearlove2.1
1// ClearLove 表白墙 2.0 —— 单二进制部署的表白墙 Web 应用
2// 入口:加载配置 -> 连接数据库 -> 解析模板 -> 注册路由 -> 启动 HTTP 服务与 AI 巡查
3package main
4
5import (
6 "embed"
7 "fmt"
8 "io/fs"
9 "log"
10 "net/http"
11 "os"
12 "path/filepath"
13
14 "clearlove/internal/ai"
15 "clearlove/internal/config"
16 "clearlove/internal/database"
17 "clearlove/internal/handlers"
18 "clearlove/internal/middleware"
19 "clearlove/internal/migrate"
20)
21
22// webFS 嵌入全部模板与静态资源,保证单文件部署
23//
24//go:embed all:web/templates all:web/static
25var webFS embed.FS
26
27func main() {
28 // 子命令:从 PHP 旧版迁移数据
29 // ./clearlove migrate /path/to/old/data.db [-uploads /path/to/old/uploads] [-dry]
30 if len(os.Args) > 1 && os.Args[1] == "migrate" {
31 runMigrate(os.Args[2:])
32 return
33 }
34
35 config.Load()
36 // 运行时目录(单二进制部署时自动创建,无需手工准备)
37 if err := ensureDirs(); err != nil {
38 log.Fatalf("目录初始化失败: %v\n提示:可通过环境变量 CLEARLOVE_DATA_DIR / CLEARLOVE_UPLOAD_DIR 指定可写目录", err)
39 }
40
41 if config.Cfg.Installed {
42 if err := database.Connect(); err != nil {
43 log.Fatalf("数据库连接失败: %v", err)
44 }
45 if err := database.Migrate(); err != nil {
46 log.Fatalf("数据库迁移失败: %v", err)
47 }
48 }
49
50 handlers.SetupTemplates(webFS)
51 pluginFS, _ := fs.Sub(webFS, "web/static")
52 handlers.StaticFS = pluginFS
53
54 go ai.StartPatrol() // AI 自动巡查(每10分钟,仅在开启时工作)
55
56 // 监听端口:PORT 或 CLEARLOVE_PORT,默认 16868
57 addr := ":16868"
58 if p := os.Getenv("PORT"); p != "" {
59 addr = ":" + p
60 } else if p := os.Getenv("CLEARLOVE_PORT"); p != "" {
61 addr = ":" + p
62 }
63 log.Printf("ClearLove 表白墙 2.0 | 数据目录: %s | 上传目录: %s", config.Cfg.DataDir, config.Cfg.UploadDir)
64 if config.Cfg.Installed {
65 log.Printf("服务已启动: http://localhost%s (后台: /admin)", addr)
66 } else {
67 log.Printf("检测到未安装,请访问 http://localhost%s/install 完成安装向导", addr)
68 }
69 if err := http.ListenAndServe(addr, middleware.Use(buildMux())); err != nil {
70 log.Fatalf("HTTP 服务启动失败: %v", err)
71 }
72}
73
74// runMigrate 执行旧版(PHP 1.x)数据迁移,迁移前需先完成新版安装
75func runMigrate(args []string) {
76 config.Load()
77 oldPath, uploadsDir := "", ""
78 dryRun := false
79 for i := 0; i < len(args); i++ {
80 switch args[i] {
81 case "-uploads", "--uploads":
82 if i+1 < len(args) {
83 uploadsDir = args[i+1]
84 i++
85 }
86 case "-dry", "--dry", "-dry-run":
87 dryRun = true
88 default:
89 if oldPath == "" {
90 oldPath = args[i]
91 }
92 }
93 }
94 if oldPath == "" {
95 log.Fatalf("用法: %s migrate <旧版 data.db 路径> [-uploads 旧版 uploads 目录] [-dry]\n"+
96 "示例: %s migrate /www/wwwroot/old/data.db -uploads /www/wwwroot/old/uploads",
97 os.Args[0], os.Args[0])
98 }
99 if err := ensureDirs(); err != nil {
100 log.Fatalf("目录初始化失败: %v", err)
101 }
102 if !config.Cfg.Installed {
103 log.Fatal("新版尚未完成安装:请先启动服务并访问 /install 完成安装向导,再执行数据迁移")
104 }
105 if err := database.Connect(); err != nil {
106 log.Fatalf("数据库连接失败: %v", err)
107 }
108 if err := database.Migrate(); err != nil {
109 log.Fatalf("数据库迁移失败: %v", err)
110 }
111 log.Printf("开始从旧版迁移数据:%s", oldPath)
112 st, err := migrate.Run(oldPath, uploadsDir, dryRun)
113 if err != nil {
114 log.Fatalf("旧版数据迁移失败: %v", err)
115 }
116 prefix := ""
117 if dryRun {
118 prefix = "[试运行] "
119 }
120 log.Printf("%s迁移结果:帖子 %d、点赞补齐 %d、媒体 %d、评论 %d、话题 %d、公告 %d、用户 %d、管理员 %d、封禁 %d、举报 %d",
121 prefix, st.Posts, st.Likes, st.Media, st.Comments, st.Topics, st.Notices,
122 st.Users, st.Admins, st.Bans, st.Reports)
123 if st.Skipped > 0 || st.Missing > 0 || st.NoPostRef > 0 {
124 log.Printf("%s其中:跳过已存在记录 %d 条、媒体文件缺失 %d 个、关联帖子不存在的记录 %d 条",
125 prefix, st.Skipped, st.Missing, st.NoPostRef)
126 }
127 if dryRun {
128 log.Print("这是试运行,未写入任何数据;去掉 -dry 参数即可正式执行")
129 } else {
130 log.Print("迁移完成,重启服务后即可在新版看到数据")
131 }
132}
133
134// ensureDirs 创建并校验运行时目录可写性(提前失败,避免运行中出错)
135func ensureDirs() error {
136 dirs := []string{
137 config.Cfg.DataDir,
138 config.Cfg.UploadDir,
139 filepath.Join(config.Cfg.DataDir, "themes"),
140 filepath.Join(config.Cfg.DataDir, "plugins"),
141 }
142 for _, d := range dirs {
143 if err := os.MkdirAll(d, 0o755); err != nil {
144 return err
145 }
146 probe := filepath.Join(d, ".write_test")
147 if err := os.WriteFile(probe, []byte("ok"), 0o644); err != nil {
148 return fmt.Errorf("%s 不可写: %w", d, err)
149 }
150 _ = os.Remove(probe)
151 }
152 return nil
153}
154
155// buildMux 注册全部路由(Go 1.22+ 增强路由语法:方法 + 路径参数)
156func buildMux() *http.ServeMux {
157 mux := http.NewServeMux()
158
159 // 静态资源与上传文件
160 mux.Handle("GET /static/", http.StripPrefix("/static/", http.FileServer(http.FS(handlers.StaticFS))))
161 mux.HandleFunc("GET /favicon.svg", handlers.Favicon)
162 mux.HandleFunc("GET /theme.css", handlers.ThemeCSS)
163 mux.Handle("GET /uploads/", http.StripPrefix("/uploads/", http.FileServer(http.Dir(config.Cfg.UploadDir))))
164
165 // 安装向导
166 mux.HandleFunc("GET /install", handlers.InstallPage)
167 mux.HandleFunc("POST /install", handlers.InstallSubmit)
168
169 // 前台
170 mux.HandleFunc("GET /{$}", handlers.Index)
171 mux.HandleFunc("GET /post/{id}", handlers.PostDetail)
172 mux.HandleFunc("GET /post/{id}/edit", handlers.PostEdit)
173 mux.HandleFunc("POST /post/{id}/edit", handlers.PostEditSave)
174 mux.HandleFunc("POST /post/{id}/delete", handlers.PostDelete)
175 mux.HandleFunc("GET /compose", handlers.ComposePage)
176 mux.HandleFunc("POST /compose", handlers.ComposeSubmit)
177 mux.HandleFunc("GET /login", handlers.LoginPage)
178 mux.HandleFunc("POST /login", handlers.LoginSubmit)
179 mux.HandleFunc("GET /register", handlers.RegisterPage)
180 mux.HandleFunc("POST /register", handlers.RegisterSubmit)
181 mux.HandleFunc("POST /api/register-code", handlers.RegisterCode)
182 mux.HandleFunc("GET /logout", handlers.Logout)
183 mux.HandleFunc("GET /u/{id}", handlers.Profile)
184
185 // 通用 API(会话或 API Key 鉴权)
186 mux.HandleFunc("GET /api/captcha", handlers.CaptchaJSON)
187 mux.HandleFunc("GET /api/v1/posts", handlers.APIListPosts)
188 mux.HandleFunc("GET /api/v1/posts/{id}", handlers.APIGetPost)
189 mux.HandleFunc("POST /api/v1/posts", handlers.APICreatePost)
190 mux.HandleFunc("POST /api/v1/upload", handlers.APIUpload)
191 mux.HandleFunc("POST /api/v1/posts/{id}/like", handlers.APILike)
192 mux.HandleFunc("POST /api/v1/comments", handlers.APIComment)
193 mux.HandleFunc("GET /api/v1/topics", handlers.APITopics)
194 mux.HandleFunc("POST /api/v1/reports", handlers.APIReport)
195
196 // 后台管理
197 mux.HandleFunc("GET /admin/login", handlers.AdminLoginPage)
198 mux.HandleFunc("POST /admin/login", handlers.AdminLoginSubmit)
199 mux.HandleFunc("GET /admin/logout", handlers.AdminLogout)
200 mux.HandleFunc("GET /admin", handlers.AdminDashboard)
201 mux.HandleFunc("GET /admin/posts", handlers.AdminPosts)
202 mux.HandleFunc("POST /admin/posts/delete", handlers.AdminPostDelete)
203 mux.HandleFunc("POST /admin/posts/ban", handlers.AdminPostBan)
204 mux.HandleFunc("GET /admin/settings", handlers.AdminSettings)
205 mux.HandleFunc("POST /admin/settings", handlers.AdminSettingsSave)
206 mux.HandleFunc("POST /admin/theme/upload", handlers.AdminThemeUpload)
207 mux.HandleFunc("POST /admin/theme/switch", handlers.AdminThemeSwitch)
208 mux.HandleFunc("POST /admin/theme/delete", handlers.AdminThemeDelete)
209 mux.HandleFunc("GET /admin/reports", handlers.AdminReports)
210 mux.HandleFunc("POST /admin/reports/handle", handlers.AdminReportHandle)
211 mux.HandleFunc("POST /admin/reports/patrol", handlers.AdminPatrolStart)
212 mux.HandleFunc("GET /admin/reports/patrol/status", handlers.AdminPatrolStatus)
213 mux.HandleFunc("GET /admin/users", handlers.AdminUsers)
214 mux.HandleFunc("POST /admin/users/status", handlers.AdminUserStatus)
215 mux.HandleFunc("POST /admin/users/delete", handlers.AdminUserDelete)
216 mux.HandleFunc("GET /admin/admins", handlers.AdminAdmins)
217 mux.HandleFunc("POST /admin/admins/create", handlers.AdminCreate)
218 mux.HandleFunc("POST /admin/admins/delete", handlers.AdminDelete)
219 mux.HandleFunc("GET /admin/notices", handlers.AdminNotices)
220 mux.HandleFunc("POST /admin/notices", handlers.AdminNoticesSave)
221 mux.HandleFunc("GET /admin/plugins", handlers.AdminPlugins)
222 mux.HandleFunc("POST /admin/plugins/upload", handlers.AdminPluginUpload)
223 mux.HandleFunc("POST /admin/plugins/toggle", handlers.AdminPluginToggle)
224 mux.HandleFunc("POST /admin/plugins/delete", handlers.AdminPluginDelete)
225 mux.HandleFunc("POST /admin/plugins/config", handlers.AdminPluginConfig)
226 // 插件商店(对接云端插件社区)
227 mux.HandleFunc("GET /admin/store", handlers.AdminStore)
228 mux.HandleFunc("POST /admin/store/install", handlers.AdminStoreInstall)
229 mux.HandleFunc("GET /admin/apikeys", handlers.AdminAPIKeys)
230 mux.HandleFunc("POST /admin/apikeys/create", handlers.AdminAPIKeyCreate)
231 mux.HandleFunc("POST /admin/apikeys/delete", handlers.AdminAPIKeyDelete)
232 mux.HandleFunc("GET /admin/about", handlers.AdminAbout)
233 mux.HandleFunc("GET /admin/update", handlers.AdminUpdate)
234
235 return mux
236}