chore: 清理敏感与开发文档,仅同步代码

- 永久忽略并从仓库移除 开发文档/
- 移除并忽略 .env 与小程序私有配置
- 同步小程序/管理端/API与脚本改动

Made-with: Cursor
This commit is contained in:
卡若
2026-03-17 17:50:12 +08:00
parent 868b0a10d9
commit 76965adb23
443 changed files with 24175 additions and 64154 deletions

View File

@@ -0,0 +1,146 @@
// migrate-base64-images 将 chapters 表中 content 内的 base64 图片提取为文件并替换为 URL
// 用法cd soul-api && go run ./cmd/migrate-base64-images [--dry-run]
// 测试环境APP_ENV=development 时加载 .env.development请先在测试库验证
package main
import (
"encoding/base64"
"flag"
"fmt"
"log"
"math/rand"
"os"
"path/filepath"
"regexp"
"strings"
"time"
"soul-api/internal/config"
"soul-api/internal/database"
"soul-api/internal/model"
)
// data:image/png;base64,iVBORw0KG... 或 data:image/jpeg;base64,/9j/4AAQ...
var base64ImgRe = regexp.MustCompile(`(?i)src=["'](data:image/([^;"']+);base64,([A-Za-z0-9+/=]+))["']`)
func main() {
dryRun := flag.Bool("dry-run", false, "仅统计和预览,不写入文件与数据库")
flag.Parse()
cfg, err := config.Load()
if err != nil {
log.Fatal("load config: ", err)
}
config.SetCurrent(cfg)
if err := database.Init(cfg.DBDSN); err != nil {
log.Fatal("database: ", err)
}
uploadDir := cfg.UploadDir
if uploadDir == "" {
uploadDir = "uploads"
}
bookImagesDir := filepath.Join(uploadDir, "book-images")
if !*dryRun {
if err := os.MkdirAll(bookImagesDir, 0755); err != nil {
log.Fatal("mkdir book-images: ", err)
}
}
db := database.DB()
var chapters []model.Chapter
if err := db.Select("id", "mid", "section_title", "content").Where("content LIKE ?", "%data:image%").Find(&chapters).Error; err != nil {
log.Fatal("query chapters: ", err)
}
log.Printf("找到 %d 篇含 base64 图片的章节", len(chapters))
if len(chapters) == 0 {
return
}
rand.Seed(time.Now().UnixNano())
const letters = "abcdefghijklmnopqrstuvwxyz0123456789"
randomStr := func(n int) string {
b := make([]byte, n)
for i := range b {
b[i] = letters[rand.Intn(len(letters))]
}
return string(b)
}
mimeToExt := map[string]string{
"png": ".png",
"jpeg": ".jpg",
"jpg": ".jpg",
"gif": ".gif",
"webp": ".webp",
}
totalReplaced := 0
totalFiles := 0
for _, ch := range chapters {
matches := base64ImgRe.FindAllStringSubmatch(ch.Content, -1)
if len(matches) == 0 {
continue
}
newContent := ch.Content
for _, m := range matches {
fullDataURL := m[1]
mime := strings.ToLower(strings.TrimSpace(m[2]))
b64 := m[3]
ext := mimeToExt[mime]
if ext == "" {
ext = ".png"
}
decoded, err := base64.StdEncoding.DecodeString(b64)
if err != nil {
log.Printf(" [%s] base64 解码失败: %v", ch.ID, err)
continue
}
name := fmt.Sprintf("%d_%s%s", time.Now().UnixNano(), randomStr(6), ext)
dst := filepath.Join(bookImagesDir, name)
url := "/uploads/" + filepath.ToSlash(filepath.Join("book-images", name))
if !*dryRun {
if err := os.WriteFile(dst, decoded, 0644); err != nil {
log.Printf(" [%s] 写入文件失败 %s: %v", ch.ID, name, err)
continue
}
}
oldSrc := `src="` + fullDataURL + `"`
newSrc := `src="` + url + `"`
if strings.Contains(newContent, oldSrc) {
newContent = strings.Replace(newContent, oldSrc, newSrc, 1)
} else {
oldSrc2 := `src='` + fullDataURL + `'`
newSrc2 := `src="` + url + `"`
newContent = strings.Replace(newContent, oldSrc2, newSrc2, 1)
}
totalFiles++
log.Printf(" [%s] %s -> %s (%d bytes)", ch.ID, mime, name, len(decoded))
}
if newContent != ch.Content {
totalReplaced++
oldLen := len(ch.Content)
newLen := len(newContent)
if !*dryRun {
if err := db.Model(&model.Chapter{}).Where("id = ?", ch.ID).Update("content", newContent).Error; err != nil {
log.Printf(" [%s] 更新数据库失败: %v", ch.ID, err)
continue
}
}
log.Printf(" [%s] 已更新content 长度 %d -> %d (减少 %d)", ch.ID, oldLen, newLen, oldLen-newLen)
}
}
if *dryRun {
log.Printf("[dry-run] 将处理 %d 篇章节,共 %d 张 base64 图片", totalReplaced, totalFiles)
log.Printf("[dry-run] 去掉 --dry-run 后执行以实际写入")
} else {
log.Printf("完成:更新 %d 篇章节,提取 %d 张图片到 uploads/book-images/", totalReplaced, totalFiles)
}
}

View File

@@ -12,6 +12,7 @@ import (
"soul-api/internal/config"
"soul-api/internal/database"
"soul-api/internal/handler"
"soul-api/internal/redis"
"soul-api/internal/router"
"soul-api/internal/wechat"
)
@@ -31,6 +32,13 @@ func main() {
if err := wechat.InitTransfer(cfg); err != nil {
log.Fatal("wechat transfer: ", err)
}
if cfg.RedisURL != "" && cfg.RedisURL != "disable" {
if err := redis.Init(cfg.RedisURL); err != nil {
log.Printf("redis: 连接失败,跳过(%v", err)
} else {
defer redis.Close()
}
}
r := router.Setup(cfg)
srv := &http.Server{
@@ -38,9 +46,19 @@ func main() {
Handler: r,
}
// 预热 all-chapters、book/parts 缓存,避免首请求冷启动 502
go func() {
time.Sleep(2 * time.Second) // 等 DB 完全就绪
handler.WarmAllChaptersCache()
handler.WarmBookPartsCache()
}()
go func() {
log.Printf("soul-api listen on :%s (mode=%s)", cfg.Port, cfg.Mode)
log.Printf(" -> 访问地址: http://localhost:%s (健康检查: http://localhost:%s/health)", cfg.Port, cfg.Port)
if cfg.UploadDir != "" {
log.Printf(" -> 上传目录: %s", cfg.UploadDir)
}
if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed {
log.Fatal("listen: ", err)
}