Files
cmautobuy/admin/main.go
T
chengmaandClaude Opus 5 f0d7da37cf feat: 采集采购页,采集与采购统一展示 (#19)
原来的采购任务页是骨架:var rows []gin.H 从不查库,页面永远为空;
TODO 写的还是只查 task_type = 'purchase'。所以 #18 建出来的采集任务
在界面上哪儿都看不到——用户点完「创建采集任务」只能盯着
collect_status 猜。

模块名定为「采集采购」(用户指定),直接点出这页装的是哪两类任务,
比泛称「任务」更能让人一眼知道点进去看什么。路由 /tasks 不变,
改路由会让已有书签和文档链接全失效,没有收益。

列不按类型并列——两种任务字段完全不同,并列会让采集任务行一半是空列。
改成固定列 + 一列「目标」把业务信息概括成一句话:
  采集  PDD 737116531267
  采购  SO-001 · M/黑色 · 2件 · ≤¥42.00
拼接逻辑在 service 层,模板只负责显示。

统计和列表共用同一个筛选条件拼装函数。分开写的话总有一天会忘了
给统计也加条件,数字和表格对不上,操作员会以为页面坏了。

无主任务的客户端列显示「—」。#17 之后采集任务默认无主,这列会大量为空。

详情弹窗只读,采购专有字段(数量、价格上限、目标规格)在采集任务里
整段不出现,不显示空行。复用 #18 的弹窗机制,app.js 无需改动。

顺带清掉 PDD 页加进来之后一直没跟上的模块计数:多处「四个模块/四个页面」
改成五个。其中 06-quality-security.md 那两处是验证清单,
照着做的人只会测四个页面,PDD 页永远不在回归范围里。

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-07 15:16:05 +08:00

89 lines
2.3 KiB
Go

// Admin 是本地运行的 Web 管理端:管理蝦皮商品、PDD 商品、顺运宝货运单、
// 采集采购和客户端,并给 Client 提供三个接口。
//
// 启动:go run . 然后打开 http://127.0.0.1:8080
// 这个命令是安全的——Admin 只管理数据,不会连手机、不会下单。
//
// 改动前必读 admin/AGENTS.md 和根目录 AGENTS.md 的红线。
package main
import (
"embed"
"flag"
"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 := gin.Default()
// 模板:每个文件用 {{define "目录/名字"}} 声明自己的名字,
// 所以一次全部解析进来不会冲突。
tmpl, err := template.ParseFS(templateFS, "templates/*/*.html")
if err != nil {
log.Fatalf("解析模板失败: %v", err)
}
r.SetHTMLTemplate(tmpl)
// 静态文件
staticSub, err := fs.Sub(staticFS, "static")
if err != nil {
log.Fatalf("准备静态文件失败: %v", err)
}
r.StaticFS("/static", http.FS(staticSub))
// 4. 路由
web.Register(r, db, config.OnlineThreshold) // 给浏览器的页面
api.Register(r, db) // 给 Client 的接口
log.Printf("Admin 已启动: http://%s", *addr)
if err := r.Run(*addr); err != nil {
log.Fatalf("启动失败: %v", err)
}
}