fix(pl): 司机回执缺品牌数量时仍保存单据,并按业务结果提示成败。
未填写品牌数量不再拦截照片和备注;已填写的品牌数量仍校验。小程序在 HTTP 200 的业务失败下不再弹出保存成功。 Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
1 parent
13ed7f5be0
commit
af79aa50a2
7 files changed
+97
-4
No files matched your search
@@ -46,6 +46,7 @@ from app.schemas.pl.transport import (
|
||||
)
|
||||
from app.services.pl_dibang_bridge_service import plan_has_load_stop
|
||||
from app.services.pl_feedback_brand_qty import (
|
||||
driver_feedback_enforces_brand,
|
||||
persist_transport_product_feedback,
|
||||
require_brand_qtys_if_needed,
|
||||
)
|
||||
@@ -796,7 +797,7 @@ def save_public_driver_stop_feedback(
|
||||
required_brand_id=plan_product.brand_id if plan_product else None,
|
||||
actual_qty=actual_qty,
|
||||
rows=brand_qtys,
|
||||
enforce=True,
|
||||
enforce=driver_feedback_enforces_brand(brand_qtys),
|
||||
)
|
||||
except ValueError as exc:
|
||||
return error_response(400, str(exc))
|
||||
|
||||
@@ -31,6 +31,7 @@ from app.schemas.pl.transport import (
|
||||
PublicTransportPageRead,
|
||||
)
|
||||
from app.services.pl_feedback_brand_qty import (
|
||||
driver_feedback_enforces_brand,
|
||||
persist_transport_product_feedback,
|
||||
require_brand_qtys_if_needed,
|
||||
)
|
||||
@@ -552,7 +553,7 @@ def save_driver_stop_feedback_for_plan(
|
||||
required_brand_id=plan_product.brand_id if plan_product else None,
|
||||
actual_qty=actual_qty,
|
||||
rows=brand_qtys,
|
||||
enforce=True,
|
||||
enforce=driver_feedback_enforces_brand(brand_qtys),
|
||||
)
|
||||
except ValueError as exc:
|
||||
return False, str(exc)
|
||||
|
||||
@@ -20,6 +20,16 @@ def format_brand_qty_summary(rows: list[dict]) -> Optional[str]:
|
||||
return " / ".join(parts) if parts else None
|
||||
|
||||
|
||||
def driver_feedback_enforces_brand(rows: Optional[list]) -> bool:
|
||||
"""司机回执未填品牌数量时不拦截单据保存;已填写则继续校验。"""
|
||||
for row in rows or []:
|
||||
if not isinstance(row, dict):
|
||||
continue
|
||||
if Decimal(str(row.get("qty") or 0)) > 0:
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def require_brand_qtys_if_needed(
|
||||
*,
|
||||
brand_mode: Optional[str],
|
||||
|
||||
@@ -2,7 +2,11 @@ from decimal import Decimal
|
||||
|
||||
import pytest
|
||||
|
||||
from app.services.pl_feedback_brand_qty import format_brand_qty_summary, require_brand_qtys_if_needed
|
||||
from app.services.pl_feedback_brand_qty import (
|
||||
driver_feedback_enforces_brand,
|
||||
format_brand_qty_summary,
|
||||
require_brand_qtys_if_needed,
|
||||
)
|
||||
|
||||
|
||||
def test_format_brand_qty_summary():
|
||||
@@ -41,6 +45,29 @@ def test_require_skips_when_not_enforced():
|
||||
)
|
||||
|
||||
|
||||
def test_driver_feedback_missing_brand_qty_does_not_block_save():
|
||||
rows = []
|
||||
require_brand_qtys_if_needed(
|
||||
brand_mode="required",
|
||||
required_brand_id="b1",
|
||||
actual_qty=Decimal("0"),
|
||||
rows=rows,
|
||||
enforce=driver_feedback_enforces_brand(rows),
|
||||
)
|
||||
|
||||
|
||||
def test_driver_feedback_still_checks_brand_qty_when_submitted():
|
||||
rows = [{"brandId": "b1", "qty": Decimal("3")}]
|
||||
with pytest.raises(ValueError, match="必须等于"):
|
||||
require_brand_qtys_if_needed(
|
||||
brand_mode="required",
|
||||
required_brand_id="b1",
|
||||
actual_qty=Decimal("10"),
|
||||
rows=rows,
|
||||
enforce=driver_feedback_enforces_brand(rows),
|
||||
)
|
||||
|
||||
|
||||
def test_require_enforces_when_completed():
|
||||
with pytest.raises(ValueError, match="至少一个品牌数量"):
|
||||
require_brand_qtys_if_needed(
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
import assert from 'node:assert/strict'
|
||||
import test from 'node:test'
|
||||
|
||||
import { portalBusinessErrorMessage } from '../portalEnvelope.js'
|
||||
|
||||
test('portal business failure keeps the server message', () => {
|
||||
assert.equal(
|
||||
portalBusinessErrorMessage({
|
||||
code: 400,
|
||||
message: '有品牌产品须填写至少一个品牌数量',
|
||||
data: null
|
||||
}),
|
||||
'有品牌产品须填写至少一个品牌数量'
|
||||
)
|
||||
})
|
||||
|
||||
test('portal success and raw payloads are not errors', () => {
|
||||
assert.equal(
|
||||
portalBusinessErrorMessage({ code: 200, message: 'success', data: { success: true } }),
|
||||
null
|
||||
)
|
||||
assert.equal(portalBusinessErrorMessage({ code: 0, msg: 'ok' }), null)
|
||||
assert.equal(portalBusinessErrorMessage({ id: 'plan-1', planNo: 'PO1' }), null)
|
||||
assert.equal(portalBusinessErrorMessage({ code: 'ABC', name: '地磅单据' }), null)
|
||||
assert.equal(portalBusinessErrorMessage(null), null)
|
||||
})
|
||||
|
||||
test('portal failure without a message still fails', () => {
|
||||
assert.equal(portalBusinessErrorMessage({ code: '400', data: null }), '请求失败')
|
||||
})
|
||||
@@ -1,8 +1,9 @@
|
||||
/**
|
||||
* 地磅模块 HTTP(裸 JSON,非门户 code/data 信封)
|
||||
* 地磅与运输小程序 HTTP。裸 JSON 原样返回;门户信封在 HTTP 200 下若 code 不是 200/0,按业务失败处理。
|
||||
* 对齐 mobile-h5/src/utils/http/db.ts
|
||||
*/
|
||||
import { TOKEN_KEY, REFRESH_TOKEN_KEY, USER_INFO_KEY, resolveApiBase } from '@/config/api'
|
||||
import { portalBusinessErrorMessage } from '@/utils/portalEnvelope'
|
||||
|
||||
const DB_COLLECTION_ROOT = /^\/(plans|inventory|weighing)$/
|
||||
|
||||
@@ -83,6 +84,14 @@ export function dbRequest(options = {}) {
|
||||
return
|
||||
}
|
||||
if (status >= 200 && status < 300) {
|
||||
const businessError = portalBusinessErrorMessage(data)
|
||||
if (businessError) {
|
||||
if (showError) {
|
||||
uni.showToast({ title: businessError.slice(0, 40), icon: 'none' })
|
||||
}
|
||||
reject(new Error(businessError))
|
||||
return
|
||||
}
|
||||
resolve(data)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
/**
|
||||
* 门户信封 { code, message, data } 的业务失败文案。
|
||||
* HTTP 200 仍可能携带 code=400;code 缺失或非数字时视为裸 JSON。
|
||||
* @param {unknown} data
|
||||
* @returns {string | null}
|
||||
*/
|
||||
export function portalBusinessErrorMessage(data) {
|
||||
if (!data || typeof data !== 'object' || Array.isArray(data)) return null
|
||||
if (!Object.prototype.hasOwnProperty.call(data, 'code')) return null
|
||||
const code = Number(data.code)
|
||||
if (!Number.isFinite(code) || code === 200 || code === 0) return null
|
||||
const message = typeof data.message === 'string' ? data.message.trim() : ''
|
||||
const msg = typeof data.msg === 'string' ? data.msg.trim() : ''
|
||||
return message || msg || '请求失败'
|
||||
}
|
||||
Reference in new issue
Block a user