Files
cmautobuy/admin/handler/web/web.go
T
chengmaandClaude Opus 5 64d5e74d0d feat: 蝦皮数据页改为商品级列表与规格弹窗 (#41)
原来是 SKU 级一行,问题不在行数(6092→5195 只少 15%),在语义错了:
pdd_goods_url / pdd_goods_id 在 shopee_products 上,是商品级字段。
商品 24420648774 有 62 个 SKU,列表里 62 行显示同一个 PDD 链接,
将来双击任意一行编辑的也是同一个字段,操作员会以为改的是这一行。

加了两列,都是防止聚合后丢信息:

「SKU 数」—— 只显示颜色数和尺码数会严重误导。实测 602 个商品(11.6%)
的颜色数×尺码数大于实际 SKU 数,最悬殊的 26886533818 是 15×5=75 但
实际只有 28 个。蝦皮报表只含有销售成绩的 SKU,数据本来就不全。

「待补」—— 原来解析失败的行整行标黄,聚合成数量后这个信号会丢。
23 条失败分布在 6 个商品里,其中 4 个是「部分失败」:数字看着正常,
坏数据被吃掉,永远没人去补。现在待补>0 整行标黄。

颜色数/尺码数只统计 parse_ok=1,否则空值会被算进 DISTINCT 多出
一个「空颜色」。

弹窗只读,待补的行显示 spec_raw 原文——不显示的话人工不知道该填什么,
保留原文这个设计就白做了。复用 #18 的弹窗机制,app.js 未改动。

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-08 11:12:25 +08:00

101 lines
3.3 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// Package web 是给浏览器用的页面处理器。
//
// 和 handler/api 分开是有意的:页面出错要渲染错误页、要 CSRF 防护,
// 接口出错要返回 JSON、要认证。混在一起迟早写错。
//
// 改动本文件前必读 admin/AGENTS.md。两条最容易踩:
// - 本层**不写业务逻辑、不拼 SQL**,只做取参数 → 调 service → 渲染;
// - 删除、导入这类破坏性操作用 POST,**不得用 GET**,
// 浏览器和插件会预取 GET 链接。
package web
import (
"database/sql"
"net/http"
"time"
"github.com/gin-gonic/gin"
)
// Handler 持有各页面共用的依赖。
type Handler struct {
db *sql.DB
// onlineThreshold 是判定客户端"在线"的时间窗。
// 取「轮询周期 + 最长任务时长」,宽松一点,
// 免得客户端执行长任务期间被误判成离线。
onlineThreshold time.Duration
}
// Register 把五个模块的页面路由挂上去。
//
// 五个模块的页面结构完全一致(顶部工具条 / 中间带勾选的表格 / 底部状态条),
// 这是有意的,见 docs/admin/05-ui-specification.md §2。
func Register(r *gin.Engine, db *sql.DB, onlineThreshold time.Duration) {
h := &Handler{db: db, onlineThreshold: onlineThreshold}
// CSRF 只挂在页面路由上。
// 给 Client 的 /api/v1/client/* 绝不能加——它不是浏览器、没有 Cookie。
pages := r.Group("/", CSRFMiddleware())
// 打开根路径直接进第一个模块
pages.GET("/", func(c *gin.Context) {
c.Redirect(http.StatusFound, "/shopee")
})
// 1. 蝦皮数据
pages.GET("/shopee", h.ShopeeList)
pages.GET("/shopee/detail", h.ShopeeDetail) // 双击行时前端来取弹窗内容
pages.POST("/shopee/import", h.ShopeeImport)
pages.POST("/shopee/save", h.ShopeeSave)
pages.POST("/shopee/delete", h.ShopeeDelete)
pages.POST("/shopee/collect", h.ShopeeCollect)
// 2. PDD 商品
pages.GET("/pdd", h.PddList)
pages.GET("/pdd/detail", h.PddDetail) // 双击行时前端来取弹窗内容
pages.POST("/pdd/create", h.PddCreate)
pages.POST("/pdd/save", h.PddSave)
pages.POST("/pdd/collect", h.PddCollect)
pages.POST("/pdd/delete", h.PddDelete)
// 3. 顺运宝数据
pages.GET("/syb", h.SybList)
pages.POST("/syb/sync", h.SybSync)
pages.POST("/syb/match", h.SybMatch)
pages.POST("/syb/create-task", h.SybCreateTask)
pages.POST("/syb/delete", h.SybDelete)
// 4. 采集采购
pages.GET("/tasks", h.TaskList)
pages.GET("/tasks/detail", h.TaskDetail) // 双击行时前端来取弹窗内容
pages.POST("/tasks/delete", h.TaskDelete)
// 5. 客户端列表
pages.GET("/clients", h.ClientList)
pages.POST("/clients/delete", h.ClientDelete)
}
// page 组装每个页面都要的公共数据(导航高亮、标题、CSRF token)。
func page(c *gin.Context, active, title string, extra gin.H) gin.H {
data := gin.H{
"Active": active,
"Title": title,
"CSRFToken": csrfToken(c),
}
for k, v := range extra {
data[k] = v
}
return data
}
// fail 渲染一个错误页。
//
// 错误信息要说清三件事:发生了什么、保住了什么、下一步做什么。
// Go 的错误堆栈只写日志,不要贴到页面上,见 docs/admin/06-quality-security.md §5。
func fail(c *gin.Context, status int, message string) {
c.HTML(status, "partials/error", gin.H{
"Title": "出错了",
"Message": message,
})
}