仰望星辰工作室

clearlove2.1

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