feat: 完成规格映射与采购任务创建 (#69)
This commit is contained in:
+153
-66
@@ -916,31 +916,36 @@ func EnsureSybSession(db *sql.DB, client *syb.Client, username string, now time.
|
||||
// SybOrderView 是列表页一行要显示的全部内容,已经格式化成字符串,
|
||||
// 模板里不做判断和格式化,和其余四个模块的做法一致。
|
||||
type SybOrderView struct {
|
||||
SybID string
|
||||
OrderNo string
|
||||
Title string
|
||||
ProductSpec string
|
||||
ShopeeGoodsID string
|
||||
ShopeeSKUID string
|
||||
Quantity int
|
||||
PriceText string // "NT$239.00",和人民币价格一眼分得清
|
||||
ImageURL string
|
||||
Stage string
|
||||
StageText string
|
||||
StageHelp string
|
||||
ActionText string
|
||||
NeedsAttention bool
|
||||
UpdatedAt string
|
||||
SybID string
|
||||
OrderNo string
|
||||
Title string
|
||||
ProductSpec string
|
||||
ShopeeGoodsID string
|
||||
ShopeeSKUID string
|
||||
Quantity int
|
||||
PriceText string // "NT$239.00",和人民币价格一眼分得清
|
||||
ImageURL string
|
||||
Stage string
|
||||
StageText string
|
||||
StageHelp string
|
||||
ActionText string
|
||||
NeedsAttention bool
|
||||
CanPurchase bool
|
||||
DefaultMaxPrice string
|
||||
UpdatedAt string
|
||||
}
|
||||
|
||||
const (
|
||||
SybStageMissingShopee = "missing_shopee"
|
||||
SybStageSKUPending = "sku_pending"
|
||||
SybStagePddMissing = "pdd_missing"
|
||||
SybStagePddPending = "pdd_pending"
|
||||
SybStagePddCollecting = "pdd_collecting"
|
||||
SybStagePddFailed = "pdd_failed"
|
||||
SybStageMappingPending = "pdd_collected"
|
||||
SybStageMissingShopee = "missing_shopee"
|
||||
SybStageSKUPending = "sku_pending"
|
||||
SybStagePddMissing = "pdd_missing"
|
||||
SybStagePddPending = "pdd_pending"
|
||||
SybStagePddCollecting = "pdd_collecting"
|
||||
SybStagePddFailed = "pdd_failed"
|
||||
SybStageMappingPending = "mapping_pending"
|
||||
SybStagePurchaseReady = "purchase_ready"
|
||||
SybStageTaskCreated = "task_created"
|
||||
SybStagePurchaseBlocked = "purchase_blocked"
|
||||
)
|
||||
|
||||
// SybStageOption 是顺运宝处理阶段筛选项。
|
||||
@@ -959,6 +964,9 @@ func SybStageOptions() []SybStageOption {
|
||||
{Value: SybStagePddCollecting, Text: "PDD 采集中"},
|
||||
{Value: SybStagePddFailed, Text: "PDD 采集失败"},
|
||||
{Value: SybStageMappingPending, Text: "规格待匹配"},
|
||||
{Value: SybStagePurchaseReady, Text: "可创建采购任务"},
|
||||
{Value: SybStageTaskCreated, Text: "已创建采购任务"},
|
||||
{Value: SybStagePurchaseBlocked, Text: "采购数据异常"},
|
||||
}
|
||||
}
|
||||
|
||||
@@ -998,8 +1006,14 @@ func sybStageFor(c repository.SybOrderContext) (stage, text, help, action string
|
||||
help += " 原因:" + c.PddCollectMsg
|
||||
}
|
||||
return SybStagePddFailed, "PDD 采集失败", help, "重新采集"
|
||||
default:
|
||||
case c.HasActiveTask:
|
||||
return SybStageTaskCreated, "已创建采购任务", "已有未结束的采购任务,请到采集采购页面查看。", "查看任务"
|
||||
case !mappingIsValid(c):
|
||||
return SybStageMappingPending, "规格待匹配", "PDD 数据已采集,下一步匹配采购规格。", "匹配规格"
|
||||
case c.Order.Quantity <= 0:
|
||||
return SybStagePurchaseBlocked, "采购数据异常", "顺运宝采购数量不是正数,不能创建采购任务。", "核对数据"
|
||||
default:
|
||||
return SybStagePurchaseReady, "可创建采购任务", "规格映射有效,可以勾选并创建采购任务。", "核对规格"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1021,6 +1035,10 @@ type SybListResult struct {
|
||||
// 顺运宝规格对应的蝦皮 SKU,不代表已经完成 PDD 规格映射。
|
||||
func ListSybOrdersView(db *sql.DB, keyword, stage string, page int) (*SybListResult, error) {
|
||||
stage = ParseSybStage(stage)
|
||||
if stage == SybStageMappingPending || stage == SybStagePurchaseReady ||
|
||||
stage == SybStageTaskCreated || stage == SybStagePurchaseBlocked {
|
||||
return listAdvancedSybStage(db, keyword, stage, page)
|
||||
}
|
||||
filter := repository.SybOrderFilter{Keyword: keyword, Stage: stage}
|
||||
total, err := repository.CountSybOrders(db, filter)
|
||||
if err != nil {
|
||||
@@ -1050,36 +1068,87 @@ func ListSybOrdersView(db *sql.DB, keyword, stage string, page int) (*SybListRes
|
||||
TotalPages: totalPages,
|
||||
}
|
||||
for _, context := range rows {
|
||||
o := context.Order
|
||||
v := SybOrderView{
|
||||
SybID: o.SybID,
|
||||
OrderNo: o.OrderNo,
|
||||
Title: o.Title,
|
||||
ProductSpec: o.ProductSpec,
|
||||
ShopeeGoodsID: o.ShopeeGoodsID,
|
||||
ShopeeSKUID: o.ShopeeSKUID,
|
||||
Quantity: o.Quantity,
|
||||
ImageURL: o.ImageURL,
|
||||
UpdatedAt: formatLocalTime(o.UpdatedAt),
|
||||
}
|
||||
v.Stage, v.StageText, v.StageHelp, v.ActionText = sybStageFor(context)
|
||||
v.NeedsAttention = v.Stage != SybStagePddCollecting
|
||||
if o.PriceTwdCent > 0 {
|
||||
v.PriceText = fmt.Sprintf("NT$%.2f", float64(o.PriceTwdCent)/100)
|
||||
} else {
|
||||
v.PriceText = placeholder
|
||||
}
|
||||
if o.Title == "" {
|
||||
v.Title = placeholder
|
||||
}
|
||||
if o.ProductSpec == "" {
|
||||
v.ProductSpec = placeholder
|
||||
}
|
||||
result.Rows = append(result.Rows, v)
|
||||
result.Rows = append(result.Rows, sybOrderViewFor(context))
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// listAdvancedSybStage 在 Service 解析最新 skus_json 后筛选映射相关阶段。
|
||||
// 这三个阶段不能只靠 SQL 判断:同一个 option key 是否仍存在,需要走唯一的
|
||||
// OptionKey 规范化逻辑。当前同步量是百到千级,先保证采购判断正确;普通列表
|
||||
// 和其余阶段仍在数据库分页。
|
||||
func listAdvancedSybStage(db *sql.DB, keyword, stage string, page int) (*SybListResult, error) {
|
||||
contexts, err := repository.ListSybOrderContexts(db,
|
||||
repository.SybOrderFilter{Keyword: keyword}, -1, 0)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
filtered := make([]repository.SybOrderContext, 0)
|
||||
for _, context := range contexts {
|
||||
value, _, _, _ := sybStageFor(context)
|
||||
if value == stage {
|
||||
filtered = append(filtered, context)
|
||||
}
|
||||
}
|
||||
total := len(filtered)
|
||||
totalPages := TotalPages(total)
|
||||
page = ClampPage(page, totalPages)
|
||||
start := (page - 1) * PageSize
|
||||
if start > total {
|
||||
start = total
|
||||
}
|
||||
end := start + PageSize
|
||||
if end > total {
|
||||
end = total
|
||||
}
|
||||
result := &SybListResult{Total: total, Stage: stage, Page: page, TotalPages: totalPages,
|
||||
IsFiltered: true, Rows: make([]SybOrderView, 0, end-start)}
|
||||
hasAny, err := repository.CountSybOrdersTotal(db)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
result.HasAny = hasAny > 0
|
||||
for _, context := range filtered[start:end] {
|
||||
result.Rows = append(result.Rows, sybOrderViewFor(context))
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func sybOrderViewFor(context repository.SybOrderContext) SybOrderView {
|
||||
o := context.Order
|
||||
v := SybOrderView{SybID: o.SybID, OrderNo: o.OrderNo, Title: o.Title,
|
||||
ProductSpec: o.ProductSpec, ShopeeGoodsID: o.ShopeeGoodsID,
|
||||
ShopeeSKUID: o.ShopeeSKUID, Quantity: o.Quantity, ImageURL: o.ImageURL,
|
||||
UpdatedAt: formatLocalTime(o.UpdatedAt)}
|
||||
v.Stage, v.StageText, v.StageHelp, v.ActionText = sybStageFor(context)
|
||||
v.CanPurchase = v.Stage == SybStagePurchaseReady
|
||||
v.DefaultMaxPrice = defaultMaxPrice(context)
|
||||
v.NeedsAttention = v.Stage != SybStagePddCollecting && v.Stage != SybStageTaskCreated
|
||||
if o.PriceTwdCent > 0 {
|
||||
v.PriceText = fmt.Sprintf("NT$%.2f", float64(o.PriceTwdCent)/100)
|
||||
} else {
|
||||
v.PriceText = placeholder
|
||||
}
|
||||
if v.Title == "" {
|
||||
v.Title = placeholder
|
||||
}
|
||||
if v.ProductSpec == "" {
|
||||
v.ProductSpec = placeholder
|
||||
}
|
||||
return v
|
||||
}
|
||||
|
||||
func defaultMaxPrice(context repository.SybOrderContext) string {
|
||||
if !mappingIsValid(context) {
|
||||
return ""
|
||||
}
|
||||
choice, err := findPddChoice(context.PddSkusJSON, context.MappingOptionKey)
|
||||
if err != nil || choice == nil || !choice.HasPrice {
|
||||
return ""
|
||||
}
|
||||
return fmt.Sprintf("%d.%02d", choice.PriceCent/100, choice.PriceCent%100)
|
||||
}
|
||||
|
||||
// AutoResolveSybShopeeSKU 只接受确定性唯一候选:先比规格原文,再比解析后的
|
||||
// 颜色和尺码。零候选或多候选都不写库,交给采购员确认。
|
||||
func AutoResolveSybShopeeSKU(q repository.Execer, order model.SybOrder) (string, error) {
|
||||
@@ -1165,23 +1234,27 @@ type SybSKUCandidateView struct {
|
||||
|
||||
// SybProcessingDetail 是顺运宝“下一步”弹窗第一阶段需要的上下文。
|
||||
type SybProcessingDetail struct {
|
||||
SybID string
|
||||
OrderNo string
|
||||
Title string
|
||||
ProductSpec string
|
||||
ShopeeGoodsID string
|
||||
ShopeeSKUID string
|
||||
Quantity int
|
||||
Stage string
|
||||
StageText string
|
||||
StageHelp string
|
||||
Candidates []SybSKUCandidateView
|
||||
ShopeeExists bool
|
||||
PddGoodsID string
|
||||
PddURL string
|
||||
CollectStatus string
|
||||
CollectMsg string
|
||||
CanCollect bool
|
||||
SybID string
|
||||
OrderNo string
|
||||
Title string
|
||||
ProductSpec string
|
||||
ShopeeGoodsID string
|
||||
ShopeeSKUID string
|
||||
Quantity int
|
||||
Stage string
|
||||
StageText string
|
||||
StageHelp string
|
||||
Candidates []SybSKUCandidateView
|
||||
ShopeeExists bool
|
||||
PddGoodsID string
|
||||
PddURL string
|
||||
CollectStatus string
|
||||
CollectMsg string
|
||||
CanCollect bool
|
||||
PddDimensionNames []string
|
||||
PddChoices []PddOptionChoice
|
||||
MappingValid bool
|
||||
HasActiveTask bool
|
||||
}
|
||||
|
||||
func GetSybProcessingDetail(db *sql.DB, sybID string) (*SybProcessingDetail, error) {
|
||||
@@ -1200,6 +1273,20 @@ func GetSybProcessingDetail(db *sql.DB, sybID string) (*SybProcessingDetail, err
|
||||
d.CanCollect = context.PddGoodsID != "" &&
|
||||
(context.PddCollectStatus == string(model.CollectPending) ||
|
||||
context.PddCollectStatus == string(model.CollectFailed))
|
||||
d.HasActiveTask = context.HasActiveTask
|
||||
if context.PddCollectStatus == string(model.CollectCollected) && context.PddSkusJSON != "" {
|
||||
choices, names, parseErr := pddOptionChoices(context.PddSkusJSON)
|
||||
if parseErr == nil {
|
||||
d.PddDimensionNames = names
|
||||
d.PddChoices = choices
|
||||
for i := range d.PddChoices {
|
||||
d.PddChoices[i].Selected = d.PddChoices[i].Key == context.MappingOptionKey
|
||||
if d.PddChoices[i].Selected {
|
||||
d.MappingValid = true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
d.Stage, d.StageText, d.StageHelp, _ = sybStageFor(*context)
|
||||
if context.ShopeeExists {
|
||||
skus, err := repository.ListShopeeSKUsByGoodsID(db, o.ShopeeGoodsID)
|
||||
|
||||
Reference in New Issue
Block a user