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) {
|
||||
ids := c.PostFormArray("ids")
|
||||
prices := c.PostFormArray("max_price_yuan")
|
||||
mappingKeys := c.PostFormArray("mapping_option_key")
|
||||
contextVersions := c.PostFormArray("context_version")
|
||||
if len(ids) == 0 {
|
||||
h.sybRedirect(c, "没有选择货运单明细,没有创建采购任务。")
|
||||
return
|
||||
@@ -623,12 +625,21 @@ func (h *Handler) SybCreateTask(c *gin.Context) {
|
||||
preFailures = append(preFailures, service.TaskCreateError{SybID: sybID, Reason: "人民币价格上限不能为空"})
|
||||
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])
|
||||
if err != nil {
|
||||
preFailures = append(preFailures, service.TaskCreateError{SybID: sybID, Reason: err.Error()})
|
||||
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(
|
||||
h.db, currentUser(c), requests,
|
||||
|
||||
@@ -157,8 +157,10 @@ func mappingIsValid(context repository.SybOrderContext) bool {
|
||||
|
||||
// PurchaseTaskRequest 是一条采购任务的人工确认输入。价格单位固定为人民币分。
|
||||
type PurchaseTaskRequest struct {
|
||||
SybID string
|
||||
MaxPriceCent int64
|
||||
SybID string
|
||||
MaxPriceCent int64
|
||||
MappingOptionKey string
|
||||
ContextVersion string
|
||||
}
|
||||
|
||||
const LivePurchaseConfirmation = "创建未付款订单"
|
||||
@@ -308,6 +310,12 @@ func validatePurchaseRequest(q repository.Execer, request PurchaseTaskRequest) (
|
||||
if choice == 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 {
|
||||
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" {
|
||||
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) {
|
||||
@@ -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) {
|
||||
for raw, want := range map[string]int64{"39.90": 3990, "1": 100, "0.01": 1, "12.3": 1230} {
|
||||
got, err := ParsePriceYuanToCent(raw)
|
||||
|
||||
+30
-28
@@ -917,22 +917,25 @@ 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
|
||||
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
|
||||
SybID string
|
||||
OrderNo string
|
||||
Title string
|
||||
ProductSpec string
|
||||
ShopeeGoodsID string
|
||||
Quantity int
|
||||
PriceText string // "NT$239.00",和人民币价格一眼分得清
|
||||
ImageURL string
|
||||
Stage string
|
||||
StageText string
|
||||
StageHelp string
|
||||
ActionText string
|
||||
NeedsAttention bool
|
||||
CanPurchase bool
|
||||
DefaultMaxPrice string
|
||||
MappedPddChoice string
|
||||
MappingOptionKey string
|
||||
ContextVersion string
|
||||
UpdatedAt string
|
||||
}
|
||||
|
||||
const (
|
||||
@@ -1117,7 +1120,17 @@ func sybOrderViewFor(context repository.SybOrderContext) SybOrderView {
|
||||
UpdatedAt: formatLocalTime(o.UpdatedAt)}
|
||||
v.Stage, v.StageText, v.StageHelp, v.ActionText = sybStageFor(context)
|
||||
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
|
||||
if o.PriceTwdCent > 0 {
|
||||
v.PriceText = fmt.Sprintf("NT$%.2f", float64(o.PriceTwdCent)/100)
|
||||
@@ -1133,17 +1146,6 @@ func sybOrderViewFor(context repository.SybOrderContext) SybOrderView {
|
||||
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 是顺运宝“下一步”弹窗第一阶段需要的上下文。
|
||||
type SybProcessingDetail struct {
|
||||
SybID string
|
||||
|
||||
@@ -192,8 +192,12 @@
|
||||
{{range .Rows}}
|
||||
<div class="purchase-confirm-row" data-purchase-row="{{.SybID}}" hidden>
|
||||
<input type="hidden" name="ids" value="{{.SybID}}" disabled>
|
||||
<div><strong>{{.OrderNo}}</strong> · {{.Title}} · {{.ProductSpec}} × {{.Quantity}}</div>
|
||||
<label>人民币价格上限(元)
|
||||
<input type="hidden" name="mapping_option_key" value="{{.MappingOptionKey}}" disabled>
|
||||
<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>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user