文章编辑器问题
This commit is contained in:
146
soul-api/cmd/migrate-base64-images/main.go
Normal file
146
soul-api/cmd/migrate-base64-images/main.go
Normal 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)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user