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>
This commit is contained in:
@@ -6,6 +6,7 @@ import (
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
|
||||
@@ -15,12 +16,39 @@ import (
|
||||
|
||||
// ShopeeList 渲染蝦皮数据列表页。
|
||||
//
|
||||
// 表格按 SKU 展开显示,数据来自 shopee_products 和 shopee_skus 联查。
|
||||
// 列定义见 docs/admin/05-ui-specification.md §4.2。
|
||||
// **商品级一行**(不是 SKU 级),数据来自 shopee_products 和 shopee_skus
|
||||
// 聚合联查。列定义见 docs/admin/05-ui-specification.md §4.2、工单 #41。
|
||||
func (h *Handler) ShopeeList(c *gin.Context) {
|
||||
h.renderShopeeList(c, c.Query("goods_id"), c.Query("msg"), nil)
|
||||
}
|
||||
|
||||
// ShopeeDetail 渲染双击行弹出的那个弹窗的**内容**(不是整页)。
|
||||
//
|
||||
// 复用 #18 的弹窗机制:行上写 data-detail-id="{{.GoodsID}}",
|
||||
// 前端拿 goods_id 拼到 data-detail-url 后面,以 "id" 作为查询参数名
|
||||
// (固定写法,见 static/js/app.js「双击行打开详情弹窗」)。
|
||||
//
|
||||
// `[必须]` 这个弹窗只读,不含保存表单——ShopeeSave 仍是 501,见 #41。
|
||||
func (h *Handler) ShopeeDetail(c *gin.Context) {
|
||||
goodsID := strings.TrimSpace(c.Query("id"))
|
||||
if goodsID == "" {
|
||||
fail(c, http.StatusBadRequest, "商品编号不对,请刷新页面后重试。")
|
||||
return
|
||||
}
|
||||
|
||||
detail, err := service.GetShopeeProductDetail(h.db, goodsID)
|
||||
if err != nil {
|
||||
fail(c, http.StatusInternalServerError, "读取商品详情失败,数据没有被改动。")
|
||||
return
|
||||
}
|
||||
if detail == nil {
|
||||
fail(c, http.StatusNotFound, "这个商品不存在,请刷新页面。")
|
||||
return
|
||||
}
|
||||
|
||||
c.HTML(http.StatusOK, "shopee/detail_modal", gin.H{"D": detail})
|
||||
}
|
||||
|
||||
// ShopeeImport 处理 Excel 上传导入。
|
||||
//
|
||||
// 关键规则(写错会丢数据,见 docs/admin/03-data-model.md §3.3):
|
||||
@@ -109,7 +137,7 @@ func (h *Handler) ShopeeImport(c *gin.Context) {
|
||||
|
||||
// renderShopeeList 是 ShopeeList 和 ShopeeImport 共用的渲染逻辑。
|
||||
func (h *Handler) renderShopeeList(c *gin.Context, keyword, msg string, failures []service.ImportFailure) {
|
||||
result, err := service.ListShopeeSKUs(h.db, keyword)
|
||||
result, err := service.ListShopeeProducts(h.db, keyword)
|
||||
if err != nil {
|
||||
fail(c, http.StatusInternalServerError,
|
||||
"读取蝦皮数据失败,数据没有被改动。刷新页面重试;一直失败请把这句话报给维护者。")
|
||||
|
||||
@@ -44,6 +44,7 @@ func Register(r *gin.Engine, db *sql.DB, onlineThreshold time.Duration) {
|
||||
|
||||
// 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)
|
||||
|
||||
Reference in New Issue
Block a user