280 lines
10 KiB
Go
280 lines
10 KiB
Go
package web
|
||
|
||
import (
|
||
"fmt"
|
||
"net/http"
|
||
"net/url"
|
||
"strconv"
|
||
"strings"
|
||
"time"
|
||
|
||
"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("q"), c.Query("search_field"), c.Query("status"),
|
||
c.Query("deleted"), c.Query("unlinked"), 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("q"), "SearchField": service.ParseShopeeSearchField(c.Query("search_field")),
|
||
"StatusFilter": service.ParseShopeeStatus(c.Query("status")),
|
||
"DeletedFilter": currentUser(c).IsAdmin() && c.Query("deleted") == "1",
|
||
"UnlinkedFilter": c.Query("unlinked") == "1",
|
||
"CurrentPage": service.ParsePage(c.Query("page")),
|
||
"CurrentPageSize": service.ParsePageSize(c.Query("page_size")),
|
||
})
|
||
}
|
||
|
||
// renderShopeeList 统一渲染蝦皮列表;数据写入改由商品目录接口负责。
|
||
//
|
||
// `[必须]` 分页控件用 <a href> 纯 GET 导航,翻页要保留状态、分类、关键词和内部范围,
|
||
// 所以这里把归一后的有效值透传回模板去拼下一页的链接(工单 #43、#213)。
|
||
func (h *Handler) renderShopeeList(c *gin.Context, keyword, searchFieldRaw, statusRaw, deletedRaw, unlinkedRaw, pageRaw, msg string) {
|
||
filter := repository.ShopeeFilter{
|
||
Keyword: strings.TrimSpace(keyword),
|
||
SearchField: service.ParseShopeeSearchField(searchFieldRaw),
|
||
Status: service.ParseShopeeStatus(statusRaw),
|
||
Unlinked: unlinkedRaw == "1",
|
||
}
|
||
if currentUser(c).IsAdmin() && deletedRaw == "1" {
|
||
filter.Deleted = true
|
||
}
|
||
// 不叫 page:本文件末尾要调用同名的 page(c, ...) 渲染辅助函数,
|
||
// 局部变量会把它遮住导致编译失败。
|
||
pageNum := service.ParsePage(pageRaw)
|
||
pageSize := service.ParsePageSize(c.Query("page_size"))
|
||
|
||
result, err := service.ListShopeeProductsWithPageSize(h.db, filter, pageNum, pageSize)
|
||
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 filter.Keyword != "" {
|
||
values.Set("q", filter.Keyword)
|
||
}
|
||
values.Set("search_field", filter.SearchField)
|
||
if filter.Status != "" {
|
||
values.Set("status", filter.Status)
|
||
}
|
||
if filter.Deleted {
|
||
values.Set("deleted", "1")
|
||
}
|
||
if filter.Unlinked {
|
||
values.Set("unlinked", "1")
|
||
}
|
||
values.Set("page_size", strconv.Itoa(pageSize))
|
||
|
||
c.HTML(http.StatusOK, "shopee/list", page(c, "shopee", "蝦皮数据", gin.H{
|
||
"Keyword": filter.Keyword,
|
||
"SearchField": filter.SearchField,
|
||
"SearchFieldOptions": service.ShopeeSearchFieldOptions(),
|
||
"StatusFilter": filter.Status,
|
||
"StatusOptions": service.ShopeeStatusOptions(),
|
||
"DeletedFilter": filter.Deleted,
|
||
"UnlinkedFilter": filter.Unlinked,
|
||
"Rows": result.Rows,
|
||
"Status": status,
|
||
"HasAnyProducts": result.HasAnyProducts,
|
||
"IsFiltered": result.IsFiltered,
|
||
"Pagination": service.NewPaginationView(result.Page, pageSize, result.TotalPages, values.Encode()),
|
||
"CurrentPage": result.Page,
|
||
"CurrentPageSize": pageSize,
|
||
"DetailURL": "/shopee/detail?" + detailValuesForShopee(values, result.Page),
|
||
"AutoOpenDetailID": strings.TrimSpace(c.Query("open_id")),
|
||
"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))
|
||
}
|
||
|
||
// ShopeeSpecSave 保存一条现有蝦皮 SKU 的人工颜色、尺码和建议说明。
|
||
func (h *Handler) ShopeeSpecSave(c *gin.Context) {
|
||
err := service.UpdateShopeeSKUManual(h.db, currentUser(c), c.PostForm("shopee_goods_id"),
|
||
c.PostForm("sku_record_id"), c.PostForm("color"), c.PostForm("size"),
|
||
c.PostForm("advice"), time.Now())
|
||
if err != nil {
|
||
if service.IsValidationError(err) {
|
||
h.shopeeRedirect(c, "规格未保存:"+err.Error())
|
||
return
|
||
}
|
||
fail(c, http.StatusInternalServerError, "保存蝦皮规格失败,数据没有被改动。请刷新页面后重试。")
|
||
return
|
||
}
|
||
h.shopeeRedirect(c, "蝦皮规格已保存,人工字段不会被后续导入覆盖")
|
||
}
|
||
|
||
// ShopeeDelete 批量删除勾选的行。
|
||
func (h *Handler) ShopeeDelete(c *gin.Context) {
|
||
count, err := service.DeleteShopeeProducts(h.db, currentUser(c), c.PostFormArray("ids"))
|
||
if err != nil {
|
||
if service.IsValidationError(err) {
|
||
h.shopeeRedirect(c, "删除失败:"+err.Error())
|
||
return
|
||
}
|
||
fail(c, http.StatusInternalServerError, "删除蝦皮商品失败,数据没有被改动。")
|
||
return
|
||
}
|
||
h.shopeeRedirect(c, fmt.Sprintf("已将 %d 个蝦皮商品移入已删除,可由管理员恢复", count))
|
||
}
|
||
|
||
// ShopeeRestore 恢复软删除商品及其原有 SKU、PDD 关联。
|
||
func (h *Handler) ShopeeRestore(c *gin.Context) {
|
||
count, err := service.RestoreShopeeProducts(h.db, currentUser(c), c.PostFormArray("ids"))
|
||
if err != nil {
|
||
if service.IsValidationError(err) {
|
||
h.shopeeRedirect(c, "恢复失败:"+err.Error())
|
||
return
|
||
}
|
||
fail(c, http.StatusInternalServerError, "恢复蝦皮商品失败,数据没有被改动。")
|
||
return
|
||
}
|
||
h.shopeeRedirect(c, fmt.Sprintf("已恢复 %d 个蝦皮商品", count))
|
||
}
|
||
|
||
// 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("q")); value != "" {
|
||
params.Set("q", value)
|
||
}
|
||
params.Set("search_field", service.ParseShopeeSearchField(c.PostForm("search_field")))
|
||
if value := strings.TrimSpace(c.PostForm("status")); value != "" {
|
||
params.Set("status", service.ParseShopeeStatus(value))
|
||
}
|
||
if currentUser(c).IsAdmin() && c.PostForm("deleted") == "1" {
|
||
params.Set("deleted", "1")
|
||
}
|
||
if c.PostForm("unlinked") == "1" {
|
||
params.Set("unlinked", "1")
|
||
}
|
||
if value := strings.TrimSpace(c.PostForm("page")); value != "" {
|
||
params.Set("page", value)
|
||
}
|
||
params.Set("page_size", strconv.Itoa(service.ParsePageSize(c.PostForm("page_size"))))
|
||
if value := strings.TrimSpace(c.PostForm("open_id")); value != "" {
|
||
params.Set("open_id", value)
|
||
}
|
||
if msg != "" {
|
||
params.Set("msg", msg)
|
||
}
|
||
c.Redirect(http.StatusSeeOther, "/shopee?"+params.Encode())
|
||
}
|