// Admin 是本地运行的 Web 管理端:管理蝦皮商品、PDD 商品、顺运宝货运单、 // 采集采购和客户端,并给 Client 提供三个接口。 // // 启动:go run . 然后打开 http://127.0.0.1:8080 // 这个命令是安全的——Admin 只管理数据,不会连手机、不会下单。 // // 改动前必读 admin/AGENTS.md 和根目录 AGENTS.md 的红线。 package main import ( "database/sql" "embed" "flag" "fmt" "html/template" "io/fs" "log" "net/http" "github.com/gin-gonic/gin" "cmautobuy/admin/config" "cmautobuy/admin/handler/api" "cmautobuy/admin/handler/web" "cmautobuy/admin/repository" ) // 模板和静态文件打进二进制,这样打包后只有一个 exe, // 用户不会误删或改坏,见 docs/admin/06-quality-security.md §9。 // //go:embed templates var templateFS embed.FS //go:embed static var staticFS embed.FS func main() { addr := flag.String("addr", "127.0.0.1:8080", "监听地址。默认只听本机,要给内网用需单独评估安全性") flag.Parse() // 1. 数据目录 dataDir, err := config.DataDir() if err != nil { log.Fatalf("无法确定 data 目录: %v", err) } log.Printf("数据目录: %s", dataDir) // 2. 数据库 db, err := repository.Open(dataDir) if err != nil { log.Fatalf("打开数据库失败: %v", err) } defer db.Close() if err := repository.Migrate(db); err != nil { log.Fatalf("数据库迁移失败: %v", err) } if err := repository.CheckSchema(db); err != nil { log.Fatalf("%v", err) } log.Printf("数据库已就绪") // 3. Web 引擎 r, err := newRouter(db) if err != nil { log.Fatalf("准备 Web 页面失败: %v", err) } log.Printf("Admin 已启动: http://%s", *addr) if err := r.Run(*addr); err != nil { log.Fatalf("启动失败: %v", err) } } // newRouter 组装 Admin 的模板、静态文件和路由。 // 单独提成函数是为了让页面冒烟测试无需真的占用 TCP 端口。 func newRouter(db *sql.DB) (*gin.Engine, error) { r := gin.New() r.Use(gin.Logger(), gin.Recovery()) // 模板:每个文件用 {{define "目录/名字"}} 声明自己的名字, // 所以一次全部解析进来不会冲突。 tmpl, err := template.ParseFS(templateFS, "templates/*/*.html") if err != nil { return nil, fmt.Errorf("解析模板失败: %w", err) } r.SetHTMLTemplate(tmpl) // 静态文件 staticSub, err := fs.Sub(staticFS, "static") if err != nil { return nil, fmt.Errorf("准备静态文件失败: %w", err) } r.StaticFS("/static", http.FS(staticSub)) // 4. 路由 web.Register(r, db, config.OnlineThreshold) // 给浏览器的页面 api.Register(r, db) // 给 Client 的接口 return r, nil }