feat: 采购价格仅展示选定规格 (#113)
This commit is contained in:
@@ -612,6 +612,8 @@ func (h *Handler) SybMatch(c *gin.Context) {
|
|||||||
func (h *Handler) SybCreateTask(c *gin.Context) {
|
func (h *Handler) SybCreateTask(c *gin.Context) {
|
||||||
ids := c.PostFormArray("ids")
|
ids := c.PostFormArray("ids")
|
||||||
prices := c.PostFormArray("max_price_yuan")
|
prices := c.PostFormArray("max_price_yuan")
|
||||||
|
mappingKeys := c.PostFormArray("mapping_option_key")
|
||||||
|
contextVersions := c.PostFormArray("context_version")
|
||||||
if len(ids) == 0 {
|
if len(ids) == 0 {
|
||||||
h.sybRedirect(c, "没有选择货运单明细,没有创建采购任务。")
|
h.sybRedirect(c, "没有选择货运单明细,没有创建采购任务。")
|
||||||
return
|
return
|
||||||
@@ -623,12 +625,21 @@ func (h *Handler) SybCreateTask(c *gin.Context) {
|
|||||||
preFailures = append(preFailures, service.TaskCreateError{SybID: sybID, Reason: "人民币价格上限不能为空"})
|
preFailures = append(preFailures, service.TaskCreateError{SybID: sybID, Reason: "人民币价格上限不能为空"})
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
if i >= len(mappingKeys) || i >= len(contextVersions) ||
|
||||||
|
strings.TrimSpace(mappingKeys[i]) == "" || strings.TrimSpace(contextVersions[i]) == "" {
|
||||||
|
preFailures = append(preFailures, service.TaskCreateError{SybID: sybID, Reason: "规格确认信息已失效,请刷新页面后重试"})
|
||||||
|
continue
|
||||||
|
}
|
||||||
cent, err := service.ParsePriceYuanToCent(prices[i])
|
cent, err := service.ParsePriceYuanToCent(prices[i])
|
||||||
if err != nil {
|
if err != nil {
|
||||||
preFailures = append(preFailures, service.TaskCreateError{SybID: sybID, Reason: err.Error()})
|
preFailures = append(preFailures, service.TaskCreateError{SybID: sybID, Reason: err.Error()})
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
requests = append(requests, service.PurchaseTaskRequest{SybID: sybID, MaxPriceCent: cent})
|
requests = append(requests, service.PurchaseTaskRequest{
|
||||||
|
SybID: sybID, MaxPriceCent: cent,
|
||||||
|
MappingOptionKey: strings.TrimSpace(mappingKeys[i]),
|
||||||
|
ContextVersion: strings.TrimSpace(contextVersions[i]),
|
||||||
|
})
|
||||||
}
|
}
|
||||||
result, err := service.CreatePurchaseTasksWithOptions(
|
result, err := service.CreatePurchaseTasksWithOptions(
|
||||||
h.db, currentUser(c), requests,
|
h.db, currentUser(c), requests,
|
||||||
|
|||||||
@@ -159,6 +159,8 @@ func mappingIsValid(context repository.SybOrderContext) bool {
|
|||||||
type PurchaseTaskRequest struct {
|
type PurchaseTaskRequest struct {
|
||||||
SybID string
|
SybID string
|
||||||
MaxPriceCent int64
|
MaxPriceCent int64
|
||||||
|
MappingOptionKey string
|
||||||
|
ContextVersion string
|
||||||
}
|
}
|
||||||
|
|
||||||
const LivePurchaseConfirmation = "创建未付款订单"
|
const LivePurchaseConfirmation = "创建未付款订单"
|
||||||
@@ -308,6 +310,12 @@ func validatePurchaseRequest(q repository.Execer, request PurchaseTaskRequest) (
|
|||||||
if choice == nil {
|
if choice == nil {
|
||||||
return "已保存的 PDD 规格已失效,请重新匹配", context, "", nil
|
return "已保存的 PDD 规格已失效,请重新匹配", context, "", nil
|
||||||
}
|
}
|
||||||
|
if request.MappingOptionKey != "" && request.MappingOptionKey != context.MappingOptionKey {
|
||||||
|
return "规格映射已变化,请刷新页面后重新确认", context, "", nil
|
||||||
|
}
|
||||||
|
if request.ContextVersion != "" && request.ContextVersion != mappingContextVersion(*context) {
|
||||||
|
return "商品或规格数据已变化,请刷新页面后重新确认", context, "", nil
|
||||||
|
}
|
||||||
if context.Order.Quantity <= 0 {
|
if context.Order.Quantity <= 0 {
|
||||||
return "采购数量必须大于 0", context, "", nil
|
return "采购数量必须大于 0", context, "", nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -146,6 +146,11 @@ func TestSybMapping_动态维度保存并复用(t *testing.T) {
|
|||||||
if list.Total != 1 || !list.Rows[0].CanPurchase || list.Rows[0].DefaultMaxPrice != "39.90" {
|
if list.Total != 1 || !list.Rows[0].CanPurchase || list.Rows[0].DefaultMaxPrice != "39.90" {
|
||||||
t.Fatalf("可采购阶段不对: %+v", list)
|
t.Fatalf("可采购阶段不对: %+v", list)
|
||||||
}
|
}
|
||||||
|
row := list.Rows[0]
|
||||||
|
if row.MappedPddChoice != "颜色:黑色 / 尺码:M码 / 款式:常规" ||
|
||||||
|
strings.Contains(row.MappedPddChoice, "白色") || row.MappingOptionKey != key || row.ContextVersion == "" {
|
||||||
|
t.Fatalf("采购确认只能带出当前映射组合,实际 %+v", row)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestSybMapping_同商品同规格第二条明细自动命中(t *testing.T) {
|
func TestSybMapping_同商品同规格第二条明细自动命中(t *testing.T) {
|
||||||
@@ -446,6 +451,31 @@ func TestCreatePurchaseTasksWithOptions_页面打开后客户端离线会被拒
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestCreatePurchaseTasksWithOptions_拒绝弹窗打开后变化的规格映射(t *testing.T) {
|
||||||
|
db := newTestDB(t)
|
||||||
|
blackKey := seedPurchasableWorkflow(t, db, "SYB-STALE")
|
||||||
|
SaveSybMapping(db, "SYB-STALE", blackKey, "USR-1")
|
||||||
|
context, err := repository.GetSybOrderContext(db, "SYB-STALE")
|
||||||
|
if err != nil || context == nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
oldVersion := mappingContextVersion(*context)
|
||||||
|
whiteKey, _ := OptionKey(map[string]string{"color": "白色", "size": "L码", "style": "加绒"})
|
||||||
|
SaveSybMapping(db, "SYB-STALE", whiteKey, "USR-2")
|
||||||
|
admin, _, _ := prepareClientAssignmentUsers(t, db)
|
||||||
|
RegisterClient(db, model.Client{ClientID: "CLIENT-LIVE", Capabilities: `{"purchase_mode":"live"}`}, true)
|
||||||
|
|
||||||
|
result, err := CreatePurchaseTasksWithOptions(db, admin,
|
||||||
|
[]PurchaseTaskRequest{{SybID: "SYB-STALE", MaxPriceCent: 3990,
|
||||||
|
MappingOptionKey: blackKey, ContextVersion: oldVersion}},
|
||||||
|
PurchaseTaskOptions{ClientID: "CLIENT-LIVE", ExecutionMode: model.TaskExecutionLive,
|
||||||
|
LiveAcknowledged: true, LiveConfirmation: LivePurchaseConfirmation})
|
||||||
|
if err != nil || result.Created != 0 || len(result.Failures) != 1 ||
|
||||||
|
!strings.Contains(result.Failures[0].Reason, "已变化") {
|
||||||
|
t.Fatalf("旧弹窗必须因映射变化被拒绝: result=%+v err=%v", result, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestParsePriceYuanToCent_精确转换(t *testing.T) {
|
func TestParsePriceYuanToCent_精确转换(t *testing.T) {
|
||||||
for raw, want := range map[string]int64{"39.90": 3990, "1": 100, "0.01": 1, "12.3": 1230} {
|
for raw, want := range map[string]int64{"39.90": 3990, "1": 100, "0.01": 1, "12.3": 1230} {
|
||||||
got, err := ParsePriceYuanToCent(raw)
|
got, err := ParsePriceYuanToCent(raw)
|
||||||
|
|||||||
+14
-12
@@ -932,6 +932,9 @@ type SybOrderView struct {
|
|||||||
NeedsAttention bool
|
NeedsAttention bool
|
||||||
CanPurchase bool
|
CanPurchase bool
|
||||||
DefaultMaxPrice string
|
DefaultMaxPrice string
|
||||||
|
MappedPddChoice string
|
||||||
|
MappingOptionKey string
|
||||||
|
ContextVersion string
|
||||||
UpdatedAt string
|
UpdatedAt string
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1117,7 +1120,17 @@ func sybOrderViewFor(context repository.SybOrderContext) SybOrderView {
|
|||||||
UpdatedAt: formatLocalTime(o.UpdatedAt)}
|
UpdatedAt: formatLocalTime(o.UpdatedAt)}
|
||||||
v.Stage, v.StageText, v.StageHelp, v.ActionText = sybStageFor(context)
|
v.Stage, v.StageText, v.StageHelp, v.ActionText = sybStageFor(context)
|
||||||
v.CanPurchase = v.Stage == SybStagePurchaseReady
|
v.CanPurchase = v.Stage == SybStagePurchaseReady
|
||||||
v.DefaultMaxPrice = defaultMaxPrice(context)
|
if v.CanPurchase {
|
||||||
|
v.MappingOptionKey = context.MappingOptionKey
|
||||||
|
v.ContextVersion = mappingContextVersion(context)
|
||||||
|
choice, err := findPddChoice(context.PddSkusJSON, context.MappingOptionKey)
|
||||||
|
if err == nil && choice != nil {
|
||||||
|
v.MappedPddChoice = choice.Label
|
||||||
|
if choice.HasPrice {
|
||||||
|
v.DefaultMaxPrice = fmt.Sprintf("%d.%02d", choice.PriceCent/100, choice.PriceCent%100)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
v.NeedsAttention = v.Stage != SybStagePddCollecting && v.Stage != SybStageTaskCreated
|
v.NeedsAttention = v.Stage != SybStagePddCollecting && v.Stage != SybStageTaskCreated
|
||||||
if o.PriceTwdCent > 0 {
|
if o.PriceTwdCent > 0 {
|
||||||
v.PriceText = fmt.Sprintf("NT$%.2f", float64(o.PriceTwdCent)/100)
|
v.PriceText = fmt.Sprintf("NT$%.2f", float64(o.PriceTwdCent)/100)
|
||||||
@@ -1133,17 +1146,6 @@ func sybOrderViewFor(context repository.SybOrderContext) SybOrderView {
|
|||||||
return v
|
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)
|
|
||||||
}
|
|
||||||
|
|
||||||
// SybProcessingDetail 是顺运宝“下一步”弹窗第一阶段需要的上下文。
|
// SybProcessingDetail 是顺运宝“下一步”弹窗第一阶段需要的上下文。
|
||||||
type SybProcessingDetail struct {
|
type SybProcessingDetail struct {
|
||||||
SybID string
|
SybID string
|
||||||
|
|||||||
@@ -192,8 +192,12 @@
|
|||||||
{{range .Rows}}
|
{{range .Rows}}
|
||||||
<div class="purchase-confirm-row" data-purchase-row="{{.SybID}}" hidden>
|
<div class="purchase-confirm-row" data-purchase-row="{{.SybID}}" hidden>
|
||||||
<input type="hidden" name="ids" value="{{.SybID}}" disabled>
|
<input type="hidden" name="ids" value="{{.SybID}}" disabled>
|
||||||
<div><strong>{{.OrderNo}}</strong> · {{.Title}} · {{.ProductSpec}} × {{.Quantity}}</div>
|
<input type="hidden" name="mapping_option_key" value="{{.MappingOptionKey}}" disabled>
|
||||||
<label>人民币价格上限(元)
|
<input type="hidden" name="context_version" value="{{.ContextVersion}}" disabled>
|
||||||
|
<div><strong>{{.OrderNo}}</strong> · {{.Title}} × {{.Quantity}}</div>
|
||||||
|
<div class="hint">顺运宝规格:{{.ProductSpec}}</div>
|
||||||
|
<div><strong>已选 PDD 规格:</strong>{{.MappedPddChoice}}</div>
|
||||||
|
<label>选定颜色的人民币价格上限(元)
|
||||||
<input type="number" name="max_price_yuan" min="0.01" step="0.01" value="{{.DefaultMaxPrice}}" required disabled>
|
<input type="number" name="max_price_yuan" min="0.01" step="0.01" value="{{.DefaultMaxPrice}}" required disabled>
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -559,6 +559,11 @@ HTML 片段,只替换弹窗内部的表格、总数和分页。主货运单表
|
|||||||
的 PDD 采集价;没有价格时留空强制填写。新任务固定为真实下单(不支付),不显示执行
|
的 PDD 采集价;没有价格时留空强制填写。新任务固定为真实下单(不支付),不显示执行
|
||||||
模式选择;风险说明、复选确认和“创建未付款订单”确认短语始终显示。
|
模式选择;风险说明、复选确认和“创建未付款订单”确认短语始终显示。
|
||||||
|
|
||||||
|
每条确认行同时显示顺运宝规格原文和当前映射的唯一 PDD 规格组合。价格输入只对应该
|
||||||
|
组合中的选定颜色,不显示同一 PDD 商品的其他颜色或价格。批量创建时每条顺运宝明细
|
||||||
|
各自显示一个选定组合和一个价格输入;映射或商品上下文在弹窗打开后变化时,服务端拒绝
|
||||||
|
旧提交并要求刷新。
|
||||||
|
|
||||||
`[必须]` 校验不过的**不要静默跳过**,要列出来告诉操作员缺什么:
|
`[必须]` 校验不过的**不要静默跳过**,要列出来告诉操作员缺什么:
|
||||||
|
|
||||||
- 同一批中合法行可以创建,失败行逐条显示明细 ID 和原因;数据库错误则整批回滚。
|
- 同一批中合法行可以创建,失败行逐条显示明细 ID 和原因;数据库错误则整批回滚。
|
||||||
|
|||||||
Reference in New Issue
Block a user