折后价校验必须前后端双重验证,前端需用 parseFloat() 实时监听 input 事件比较数值,禁用字符串比较,为空时清空校验状态。

折后价校验必须在提交前做,且不能只靠前端
浏览器端的 checkValidity() 和自定义验证能拦住明显错误,但绕过太容易。真实业务中,后端必须重复校验 discounted_price <= original_price,否则恶意构造请求就能跳过所有前端逻辑。
前端校验的核心是:拿到两个输入框的值 → 转成数字 → 比较 → 设置 setCustomValidity() → 触发重绘。别用字符串直接比,"100" > "999" 会返回 true。
用 input 事件实时校验比 submit 更友好
用户改完原价或折后价,立刻提示,而不是等到点提交才报错。
- 监听两个
<input type="number">的input事件(不是change,后者要失焦) - 任一值为空时,清空校验状态(
setCustomValidity("")),避免残留错误 - 转数字时用
parseFloat(),不推荐Number()—— 后者对空字符串返回0,导致0 误判通过 - 校验失败时,调用
reportValidity()可强制显示浏览器默认气泡(但样式难控,生产环境常配合自定义提示)
<input type="number" id="original-price" name="original_price">
<input type="number" id="discounted-price" name="discounted_price">
<p><script>
const original = document.getElementById('original-price');
const discounted = document.getElementById('discounted-price');</p><p>[original, discounted].forEach(el => {
el.addEventListener('input', () => {
const orig = parseFloat(original.value) || null;
const disc = parseFloat(discounted.value) || null;</p><pre class='brush:php;toolbar:false;'>if (orig !== null && disc !== null && disc > orig) {
discounted.setCustomValidity('折后价格不能高于原价');
} else {
discounted.setCustomValidity('');
}}); }); </script>
required 和 min 属性只能辅助,不能替代逻辑判断
required 能防空,min="0" 能防负数,但它们无法表达“小于等于另一个字段”的关系。试图用 max 动态绑定会失败 —— HTML 原生属性不支持 JS 表达式。
立即学习“前端免费学习笔记(深入)”;
常见翻车点:
- 把
original_price设为type="text",结果parseFloat("¥199")得到199,但用户可能输"199元"或空格,建议加inputmode="decimal"+pattern="[0-9.]*"限制键盘输入 - 没处理小数精度问题,比如
199.99 - 20算出179.99000000000004,虽不影响比较,但展示难看,建议后端统一用整数分存储,前端除以 100 渲染
后端校验被忽略时,前端再严也没用
哪怕你用 WebAssembly 写了个价格验证模块,只要 API 接口没做服务端校验,攻击者 curl 一下就绕过。
关键动作只有两个:
- 接收参数后,立刻检查
discounted_price ,类型必须是数字,不是字符串 - 错误时返回明确 HTTP 状态码(如
400 Bad Request)和字段级错误信息,例如{"discounted_price": ["折后价格不能高于原价"]}
真正容易被忽略的,是前后端对“价格”数据类型的约定 —— 前端传字符串 "199.99",后端没 floatval() 或 parseFloat() 就直接比较,结果永远是 false。



















