fix(pl): 司机回执缺品牌数量时仍保存单据,并按业务结果提示成败。

未填写品牌数量不再拦截照片和备注;已填写的品牌数量仍校验。小程序在 HTTP 200 的业务失败下不再弹出保存成功。

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
wuzilongandCursor committed 2026-11-06 23:57:34 +08:00
1 parent 13ed7f5be0
commit af79aa50a2
7 files changed
+97 -4

No files matched your search

+2 -1
View File
@@ -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],
+28 -1
View File
@@ -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 }), '请求失败')
})
+10 -1
View File
@@ -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
}
+15
View File
@@ -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 || '请求失败'
}