- main.go 改为 Web 服务入口(Gin + 内嵌前端 dist + 自动打开浏览器) - internal/service: 新增扫描整理后台任务,支持状态机与实时日志捕获 - internal/api: 新增统一响应、媒体列表/统计、扫描开始/状态接口 - internal/store: 追加 ListMedia/CountMedia/Stats 查询,MediaInfo 补 JSON tag - internal/config: application.properties 配置加载(端口/数据库/自动开浏览器) - frontend: Vue2 + ElementUI + Vite 工程,含扫描整理/统计看板/媒体库三页 - build.sh 整合前端构建,输出单一 kenaito-media-picker.exe
118 lines
3.1 KiB
Go
118 lines
3.1 KiB
Go
// kenaito-media-picker 主入口:启动 Web 服务(内嵌前端),媒体库扫描整理由页面点击触发。
|
||
package main
|
||
|
||
import (
|
||
"fmt"
|
||
"io"
|
||
"io/fs"
|
||
"mime"
|
||
"net/http"
|
||
"os"
|
||
"os/exec"
|
||
"path/filepath"
|
||
"runtime"
|
||
"strings"
|
||
"time"
|
||
|
||
"kenaito-media-picker/frontend"
|
||
"kenaito-media-picker/internal/api"
|
||
"kenaito-media-picker/internal/config"
|
||
"kenaito-media-picker/internal/store"
|
||
|
||
"github.com/gin-gonic/gin"
|
||
)
|
||
|
||
func main() {
|
||
config.LoadAppConfig()
|
||
cfg := config.GetAppConfig()
|
||
|
||
// 打开数据库;失败时服务照常启动,仅媒体库查询不可用、扫描不记录信息
|
||
st, err := store.Open(cfg.Dsn)
|
||
if err != nil {
|
||
fmt.Printf("=== 打开数据库失败(媒体库查询不可用): %v\n", err)
|
||
st = nil
|
||
} else {
|
||
defer st.Close()
|
||
}
|
||
|
||
r := api.NewRouter(st)
|
||
// 请求日志输出到 stderr,避免占用被扫描流程捕获的 stdout
|
||
r.Use(gin.LoggerWithWriter(os.Stderr))
|
||
r.NoRoute(staticHandler())
|
||
|
||
addr := fmt.Sprintf(":%d", cfg.Port)
|
||
url := "http://localhost" + addr
|
||
fmt.Printf("=== Web 服务已启动: %s\n", url)
|
||
fmt.Printf("=== 数据库: %s\n", cfg.Dsn)
|
||
fmt.Printf("=== 在页面上点击【开始扫描】执行媒体整理(会移动文件,注意备份)\n")
|
||
if cfg.AutoOpen {
|
||
go func() {
|
||
time.Sleep(600 * time.Millisecond) // 等服务端口就绪再打开
|
||
openBrowser(url)
|
||
}()
|
||
}
|
||
if err := r.Run(addr); err != nil {
|
||
fmt.Printf("=== 服务启动失败: %v\n", err)
|
||
}
|
||
}
|
||
|
||
// openBrowser 调用系统默认浏览器打开指定 URL(Windows/macOS/Linux)
|
||
func openBrowser(url string) {
|
||
var cmd *exec.Cmd
|
||
switch runtime.GOOS {
|
||
case "windows":
|
||
cmd = exec.Command("cmd", "/c", "start", "", url)
|
||
case "darwin":
|
||
cmd = exec.Command("open", url)
|
||
default:
|
||
cmd = exec.Command("xdg-open", url)
|
||
}
|
||
if err := cmd.Start(); err != nil {
|
||
fmt.Printf("=== 自动打开浏览器失败: %v\n", err)
|
||
}
|
||
}
|
||
|
||
// staticHandler 服务内嵌前端产物:命中静态资源则返回,否则回退 index.html(hash 路由)。
|
||
func staticHandler() gin.HandlerFunc {
|
||
distFS, err := fs.Sub(frontend.Dist, "dist")
|
||
if err != nil {
|
||
return func(c *gin.Context) {
|
||
c.String(http.StatusNotFound, "前端产物缺失,请先 npm run build 后重新编译")
|
||
}
|
||
}
|
||
return func(c *gin.Context) {
|
||
if strings.HasPrefix(c.Request.URL.Path, "/api") {
|
||
c.JSON(http.StatusNotFound, gin.H{"code": 404, "message": "接口不存在", "data": nil})
|
||
return
|
||
}
|
||
path := strings.TrimPrefix(c.Request.URL.Path, "/")
|
||
if path == "" {
|
||
path = "index.html"
|
||
}
|
||
file, err := distFS.Open(path)
|
||
if err != nil {
|
||
file, err = distFS.Open("index.html")
|
||
}
|
||
if err != nil {
|
||
c.String(http.StatusNotFound, "页面不存在")
|
||
return
|
||
}
|
||
defer file.Close()
|
||
info, statErr := file.Stat()
|
||
if statErr != nil {
|
||
c.String(http.StatusInternalServerError, "读取前端资源失败")
|
||
return
|
||
}
|
||
if rs, ok := file.(io.ReadSeeker); ok {
|
||
http.ServeContent(c.Writer, c.Request, info.Name(), info.ModTime(), rs)
|
||
return
|
||
}
|
||
body, err := io.ReadAll(file)
|
||
if err != nil {
|
||
c.String(http.StatusInternalServerError, "读取前端资源失败")
|
||
return
|
||
}
|
||
c.Data(http.StatusOK, mime.TypeByExtension(filepath.Ext(path)), body)
|
||
}
|
||
}
|