仰望星辰工作室

clearlove2.1

clearlove2.1/ main.go 9.5 KB · 242 行 原始文件
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 <旧版数据文件> [-uploads 旧版 uploads 目录] [-dry]\n"+
96 "支持旧版 SQLite 的 data.db 与 MySQL 导出的 .sql(自动识别)\n"+
97 "示例: %s migrate /www/wwwroot/old/data.db -uploads /www/wwwroot/old/uploads\n"+
98 "也可以在后台「数据迁移」页面上传数据库文件完成迁移",
99 os.Args[0], os.Args[0])
100 }
101 if err := ensureDirs(); err != nil {
102 log.Fatalf("目录初始化失败: %v", err)
103 }
104 if !config.Cfg.Installed {
105 log.Fatal("新版尚未完成安装:请先启动服务并访问 /install 完成安装向导,再执行数据迁移")
106 }
107 if err := database.Connect(); err != nil {
108 log.Fatalf("数据库连接失败: %v", err)
109 }
110 if err := database.Migrate(); err != nil {
111 log.Fatalf("数据库迁移失败: %v", err)
112 }
113 log.Printf("开始从旧版迁移数据:%s", oldPath)
114 st, err := migrate.Run(oldPath, uploadsDir, dryRun)
115 if err != nil {
116 log.Fatalf("旧版数据迁移失败: %v", err)
117 }
118 prefix := ""
119 if dryRun {
120 prefix = "[试运行] "
121 }
122 log.Printf("%s迁移结果:帖子 %d、点赞补齐 %d、媒体 %d、评论 %d、话题 %d、公告 %d、用户 %d、管理员 %d、封禁 %d、举报 %d",
123 prefix, st.Posts, st.Likes, st.Media, st.Comments, st.Topics, st.Notices,
124 st.Users, st.Admins, st.Bans, st.Reports)
125 if st.Skipped > 0 || st.Missing > 0 || st.NoPostRef > 0 {
126 log.Printf("%s其中:跳过已存在记录 %d 条、媒体文件缺失 %d 个、关联帖子不存在的记录 %d 条",
127 prefix, st.Skipped, st.Missing, st.NoPostRef)
128 }
129 if dryRun {
130 log.Print("这是试运行,未写入任何数据;去掉 -dry 参数即可正式执行")
131 } else {
132 log.Print("迁移完成,重启服务后即可在新版看到数据")
133 }
134}
135
136// ensureDirs 创建并校验运行时目录可写性(提前失败,避免运行中出错)
137func ensureDirs() error {
138 dirs := []string{
139 config.Cfg.DataDir,
140 config.Cfg.UploadDir,
141 filepath.Join(config.Cfg.DataDir, "themes"),
142 filepath.Join(config.Cfg.DataDir, "plugins"),
143 }
144 for _, d := range dirs {
145 if err := os.MkdirAll(d, 0o755); err != nil {
146 return err
147 }
148 probe := filepath.Join(d, ".write_test")
149 if err := os.WriteFile(probe, []byte("ok"), 0o644); err != nil {
150 return fmt.Errorf("%s 不可写: %w", d, err)
151 }
152 _ = os.Remove(probe)
153 }
154 return nil
155}
156
157// buildMux 注册全部路由(Go 1.22+ 增强路由语法:方法 + 路径参数)
158func buildMux() *http.ServeMux {
159 mux := http.NewServeMux()
160
161 // 静态资源与上传文件
162 mux.Handle("GET /static/", http.StripPrefix("/static/", http.FileServer(http.FS(handlers.StaticFS))))
163 mux.HandleFunc("GET /favicon.svg", handlers.Favicon)
164 mux.HandleFunc("GET /theme.css", handlers.ThemeCSS)
165 mux.Handle("GET /uploads/", http.StripPrefix("/uploads/", http.FileServer(http.Dir(config.Cfg.UploadDir))))
166
167 // 安装向导
168 mux.HandleFunc("GET /install", handlers.InstallPage)
169 mux.HandleFunc("POST /install", handlers.InstallSubmit)
170
171 // 前台
172 mux.HandleFunc("GET /{$}", handlers.Index)
173 mux.HandleFunc("GET /post/{id}", handlers.PostDetail)
174 mux.HandleFunc("GET /post/{id}/edit", handlers.PostEdit)
175 mux.HandleFunc("POST /post/{id}/edit", handlers.PostEditSave)
176 mux.HandleFunc("POST /post/{id}/delete", handlers.PostDelete)
177 mux.HandleFunc("GET /compose", handlers.ComposePage)
178 mux.HandleFunc("POST /compose", handlers.ComposeSubmit)
179 mux.HandleFunc("GET /login", handlers.LoginPage)
180 mux.HandleFunc("POST /login", handlers.LoginSubmit)
181 mux.HandleFunc("GET /register", handlers.RegisterPage)
182 mux.HandleFunc("POST /register", handlers.RegisterSubmit)
183 mux.HandleFunc("POST /api/register-code", handlers.RegisterCode)
184 mux.HandleFunc("GET /logout", handlers.Logout)
185 mux.HandleFunc("GET /u/{id}", handlers.Profile)
186
187 // 通用 API(会话或 API Key 鉴权)
188 mux.HandleFunc("GET /api/captcha", handlers.CaptchaJSON)
189 mux.HandleFunc("GET /api/v1/posts", handlers.APIListPosts)
190 mux.HandleFunc("GET /api/v1/posts/{id}", handlers.APIGetPost)
191 mux.HandleFunc("POST /api/v1/posts", handlers.APICreatePost)
192 mux.HandleFunc("POST /api/v1/upload", handlers.APIUpload)
193 mux.HandleFunc("POST /api/v1/posts/{id}/like", handlers.APILike)
194 mux.HandleFunc("POST /api/v1/comments", handlers.APIComment)
195 mux.HandleFunc("GET /api/v1/topics", handlers.APITopics)
196 mux.HandleFunc("POST /api/v1/reports", handlers.APIReport)
197
198 // 后台管理
199 mux.HandleFunc("GET /admin/login", handlers.AdminLoginPage)
200 mux.HandleFunc("POST /admin/login", handlers.AdminLoginSubmit)
201 mux.HandleFunc("GET /admin/logout", handlers.AdminLogout)
202 mux.HandleFunc("GET /admin", handlers.AdminDashboard)
203 mux.HandleFunc("GET /admin/posts", handlers.AdminPosts)
204 mux.HandleFunc("POST /admin/posts/delete", handlers.AdminPostDelete)
205 mux.HandleFunc("POST /admin/posts/ban", handlers.AdminPostBan)
206 mux.HandleFunc("GET /admin/settings", handlers.AdminSettings)
207 mux.HandleFunc("POST /admin/settings", handlers.AdminSettingsSave)
208 mux.HandleFunc("POST /admin/theme/upload", handlers.AdminThemeUpload)
209 mux.HandleFunc("POST /admin/theme/switch", handlers.AdminThemeSwitch)
210 mux.HandleFunc("POST /admin/theme/delete", handlers.AdminThemeDelete)
211 mux.HandleFunc("GET /admin/reports", handlers.AdminReports)
212 mux.HandleFunc("POST /admin/reports/handle", handlers.AdminReportHandle)
213 mux.HandleFunc("POST /admin/reports/patrol", handlers.AdminPatrolStart)
214 mux.HandleFunc("GET /admin/reports/patrol/status", handlers.AdminPatrolStatus)
215 mux.HandleFunc("GET /admin/users", handlers.AdminUsers)
216 mux.HandleFunc("POST /admin/users/status", handlers.AdminUserStatus)
217 mux.HandleFunc("POST /admin/users/delete", handlers.AdminUserDelete)
218 mux.HandleFunc("GET /admin/admins", handlers.AdminAdmins)
219 mux.HandleFunc("POST /admin/admins/create", handlers.AdminCreate)
220 mux.HandleFunc("POST /admin/admins/delete", handlers.AdminDelete)
221 mux.HandleFunc("GET /admin/notices", handlers.AdminNotices)
222 mux.HandleFunc("POST /admin/notices", handlers.AdminNoticesSave)
223 mux.HandleFunc("GET /admin/plugins", handlers.AdminPlugins)
224 mux.HandleFunc("POST /admin/plugins/upload", handlers.AdminPluginUpload)
225 mux.HandleFunc("POST /admin/plugins/toggle", handlers.AdminPluginToggle)
226 mux.HandleFunc("POST /admin/plugins/delete", handlers.AdminPluginDelete)
227 mux.HandleFunc("POST /admin/plugins/config", handlers.AdminPluginConfig)
228 // 旧版数据迁移(上传 data.db 或 MySQL 导出文件)
229 mux.HandleFunc("GET /admin/migrate", handlers.AdminMigrate)
230 mux.HandleFunc("POST /admin/migrate/start", handlers.AdminMigrateStart)
231 mux.HandleFunc("GET /admin/migrate/status", handlers.AdminMigrateStatus)
232 // 插件商店(对接云端插件社区)
233 mux.HandleFunc("GET /admin/store", handlers.AdminStore)
234 mux.HandleFunc("POST /admin/store/install", handlers.AdminStoreInstall)
235 mux.HandleFunc("GET /admin/apikeys", handlers.AdminAPIKeys)
236 mux.HandleFunc("POST /admin/apikeys/create", handlers.AdminAPIKeyCreate)
237 mux.HandleFunc("POST /admin/apikeys/delete", handlers.AdminAPIKeyDelete)
238 mux.HandleFunc("GET /admin/about", handlers.AdminAbout)
239 mux.HandleFunc("GET /admin/update", handlers.AdminUpdate)
240
241 return mux
242}