Files
cmautobuy/admin/handler/web/shopee.go
T

237 lines
8.7 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
import (
"fmt"
"net/http"
"net/url"
"strconv"
"strings"
"github.com/gin-gonic/gin"
"cmautobuy/admin/repository"
"cmautobuy/admin/service"
)
// ShopeeList 渲染蝦皮数据列表页。
//
// **商品级一行**(不是 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("shop_name"), c.Query("status"), c.Query("shop"), c.Query("image"), c.Query("page"), c.Query("msg"))
}
// ShopeeDetail 渲染双击行弹出的那个弹窗的**内容**(不是整页)。
//
// 复用 #18 的弹窗机制:行上写 data-detail-id="{{.GoodsID}}",
// 前端拿 goods_id 拼到 data-detail-url 后面,以 "id" 作为查询参数名
// (固定写法,见 static/js/app.js「双击行打开详情弹窗」)。
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, "CSRFToken": csrfToken(c),
"Keyword": c.Query("goods_id"), "StatusFilter": c.Query("status"),
"ShopNameKeyword": strings.TrimSpace(c.Query("shop_name")),
"ShopFilter": service.ParseShopeePresence(c.Query("shop")),
"ImageFilter": service.ParseShopeePresence(c.Query("image")),
"CurrentPage": service.ParsePage(c.Query("page")),
})
}
// renderShopeeList 统一渲染蝦皮列表;数据写入改由商品目录接口负责。
//
// `[必须]` 分页控件用 <a href> 纯 GET 导航,翻页要保留关键词和三个下拉筛选,
// 所以这里把归一后的有效值透传回模板去拼下一页的链接(工单 #43、#150)。
func (h *Handler) renderShopeeList(c *gin.Context, keyword, shopName, statusRaw, shopRaw, imageRaw, pageRaw, msg string) {
filter := repository.ShopeeFilter{
Keyword: keyword,
ShopName: strings.TrimSpace(shopName),
Status: service.ParseShopeeStatus(statusRaw),
Shop: service.ParseShopeePresence(shopRaw),
Image: service.ParseShopeePresence(imageRaw),
}
// 不叫 page:本文件末尾要调用同名的 page(c, ...) 渲染辅助函数,
// 局部变量会把它遮住导致编译失败。
pageNum := service.ParsePage(pageRaw)
result, err := service.ListShopeeProducts(h.db, filter, pageNum)
if err != nil {
fail(c, http.StatusInternalServerError,
"读取蝦皮数据失败,数据没有被改动。刷新页面重试;一直失败请把这句话报给维护者。")
return
}
assignableClients, err := service.ListAssignableClients(h.db, currentUser(c), h.onlineThreshold)
if err != nil {
fail(c, http.StatusInternalServerError,
"读取可选客户端失败,数据没有被改动。刷新页面重试。")
return
}
status := msg
if status == "" {
status = shopeeStatusLine(filter.Status, result)
}
values := url.Values{}
if keyword != "" {
values.Set("goods_id", keyword)
}
if filter.ShopName != "" {
values.Set("shop_name", filter.ShopName)
}
if filter.Status != "" {
values.Set("status", filter.Status)
}
if filter.Shop != "" {
values.Set("shop", filter.Shop)
}
if filter.Image != "" {
values.Set("image", filter.Image)
}
c.HTML(http.StatusOK, "shopee/list", page(c, "shopee", "蝦皮数据", gin.H{
"Keyword": keyword,
"ShopNameKeyword": filter.ShopName,
"StatusFilter": filter.Status,
"StatusOptions": service.ShopeeStatusOptions(),
"ShopFilter": filter.Shop,
"ShopOptions": service.ShopeePresenceOptions("有店铺", "无店铺"),
"ImageFilter": filter.Image,
"ImageOptions": service.ShopeePresenceOptions("有图片", "无图片"),
"Rows": result.Rows,
"Status": status,
"HasAnyProducts": result.HasAnyProducts,
"IsFiltered": result.IsFiltered,
"Pagination": service.NewPaginationView(result.Page, result.TotalPages, values.Encode()),
"CurrentPage": result.Page,
"DetailURL": "/shopee/detail?" + detailValuesForShopee(values, result.Page),
"AssignableClients": assignableClients,
}))
}
func detailValuesForShopee(values url.Values, pageNum int) string {
detailValues := url.Values{}
for key, entries := range values {
detailValues[key] = append([]string(nil), entries...)
}
detailValues.Set("page", strconv.Itoa(pageNum))
return detailValues.Encode()
}
// shopeeStatusLine 组装底部状态条的默认文案(没有 msg 覆盖时)。
//
// `[必须]` 显示的是筛选后的**全量**总数(result.Total),不是本页行数,
// 筛选时前缀用状态文字("待补规格:6 个商品"),不筛选时用"共",
// 见工单 #43。
func shopeeStatusLine(statusFilter string, result *service.ShopeeListResult) string {
prefix := fmt.Sprintf("共 %d 个商品", result.Total)
if text := service.ShopeeStatusLabel(statusFilter); text != "" {
prefix = fmt.Sprintf("%s:%d 个商品", text, result.Total)
} else if result.IsFiltered {
prefix = fmt.Sprintf("筛选结果:%d 个商品", result.Total)
}
return fmt.Sprintf("%s · 第 %d/%d 页", prefix, result.Page, result.TotalPages)
}
// ShopeeSave 保存蝦皮商品当前关联的 PDD 商品。
func (h *Handler) ShopeeSave(c *gin.Context) {
goodsID, err := service.AssociateShopeePdd(h.db, c.PostForm("shopee_goods_id"),
c.PostForm("pdd_url"), c.PostForm("confirm_replace") == "1")
if err != nil {
h.shopeeRedirect(c, "PDD 关联未保存:"+err.Error()+"。请核对链接后重试。")
return
}
h.shopeeRedirect(c, fmt.Sprintf("已关联 PDD 商品 %s,可继续创建采集任务", goodsID))
}
// ShopeeDelete 批量删除勾选的行。
//
// `[不做]` 本工单(#38)明确不实现这个接口,保持 501。
func (h *Handler) ShopeeDelete(c *gin.Context) {
// TODO(骨架): 取 ids,二次确认已在前端做过,这里直接删
fail(c, http.StatusNotImplemented, "删除功能尚未实现。")
}
// ShopeeCollect 发起采集任务。
//
// 入口在编辑弹窗里,紧挨 PDD 链接输入框——不要放到表格每一行,
// 一个商品有 N 个 SKU 行,放行上就是 N 个按钮干同一件事。
//
// 规则:
// - PDD 链接为空不允许发起;
// - collect_status 已经是 collecting 的跳过并说明跳过了几个;
// - 批量采集要**按商品去重**后再建任务。
//
// `[不做]` 本工单(#38)明确不实现这个接口,保持 501。
func (h *Handler) ShopeeCollect(c *gin.Context) {
result, err := service.CreateShopeePddCollectTaskForUser(h.db, currentUser(c), c.PostForm("shopee_goods_id"))
if err != nil {
h.shopeeRedirect(c, "采集任务未创建:"+err.Error())
return
}
h.shopeeRedirect(c, service.FormatCollectTaskMessage(result))
}
// ShopeeCollectBatch 为勾选的蝦皮商品按当前 PDD 关联批量创建采集任务。
func (h *Handler) ShopeeCollectBatch(c *gin.Context) {
ids := c.PostFormArray("ids")
if len(ids) == 0 {
h.shopeeRedirect(c, "没有勾选任何蝦皮商品,没有创建任务")
return
}
result, err := service.CreateShopeePddCollectTasksForUser(
h.db, currentUser(c), ids, c.PostForm("client_id"))
if err != nil {
status := http.StatusInternalServerError
message := "创建采集任务失败,系统没有改动这一批数据。请稍后重试。"
if service.IsValidationError(err) {
status = http.StatusBadRequest
message = err.Error() + "。这一批任务整体没有创建,请重新选择。"
}
fail(c, status, message)
return
}
h.shopeeRedirect(c, service.FormatCollectTaskMessage(result))
}
func (h *Handler) shopeeRedirect(c *gin.Context, msg string) {
params := url.Values{}
if value := strings.TrimSpace(c.PostForm("list_goods_id")); value != "" {
params.Set("goods_id", value)
}
if value := strings.TrimSpace(c.PostForm("list_shop_name")); value != "" {
params.Set("shop_name", value)
}
if value := strings.TrimSpace(c.PostForm("status")); value != "" {
params.Set("status", service.ParseShopeeStatus(value))
}
if value := service.ParseShopeePresence(c.PostForm("shop")); value != "" {
params.Set("shop", value)
}
if value := service.ParseShopeePresence(c.PostForm("image")); value != "" {
params.Set("image", value)
}
if value := strings.TrimSpace(c.PostForm("page")); value != "" {
params.Set("page", value)
}
if msg != "" {
params.Set("msg", msg)
}
c.Redirect(http.StatusSeeOther, "/shopee?"+params.Encode())
}