fix(payment): 生成数量限定为 1-1000 整数并移除 spinner 死属性

生成数量不再受 step 网格约束,只要求「正整数且不超过 1000」即 (0, 1000]。

- 删除 spinner() 产出的 data-min / data-max / data-step:这三个属性在本
  项目全仓**没有任何消费方**(逐文件检索确认),属死代码;原生 min/max/step
  又会让条件显隐行触发 HTML5 校验并静默阻止提交(上一轮已定案)。参数随之
  收敛:spinner(string $name, int $value, string $unit),3 处调用点同步。
- 保留 step="any":省掉它 type="number" 会退回默认 step=1 的整数网格,把
  非整数输入判为非法,同样触发隐藏行的静默拒绝。
- 生成数量的范围收敛为 handle_add() 里的单一区间判定
  `$auto_num < 1 || $auto_num > 1000`,一次覆盖 非数字 / 0 / 负数 / 超上限,
  不再依赖下层静默夹取,也不另设分支;字段说明补上「1 - 1000 的整数」。

验证:
- 容器内真实 HTTP 边界:0 / -5 / 1001 / abc 全部拒绝,1000 / 1 / 3.9 通过
  (3.9 按整数截断)。其中 1000 这个上边界用「type=vip + 空时长」探到 ——
  数量校验通过后即报「请填写兑换会员参数」,零副作用,未真的生成 1000 张。
  正向生成 3 张后已清理,表回到原始 10 条。
- 渲染出的表单内原生约束属性 0 个、data-* 0 个;4 个 number 输入全部 step="any"。
- 契约测试:把「spinner 输出 data-*」的断言反转为「不得出现」,新增签名与
  调用点实参断言、(0,1000] 区间断言、字段说明断言;整文件属性扫描前先剥
  PHP 注释,否则文档注释里的 type="number" 字面量会污染计数。
- 负向对照:重新加回 data-min / 退回 !$auto_num / 签名加回 step 参数,
  三例均精确落到对应用例(各 5 pass / 1 fail),还原后字节一致、6/6 通过。
- tests/admin 四套件 24 pass / 0 fail;php -l 通过;两副本 SHA256 一致。
This commit is contained in:
2026-09-17 13:38:40 +08:00
parent 481f40378a
commit c1c21faf7e
2 changed files with 105 additions and 42 deletions
+21 -22
View File
@@ -334,9 +334,9 @@ class PayChargeCards {
// 自动生成
$this->field_row(
__('生成数量', 'aksrt'),
$this->spinner('auto_num', 20, 1, 1000, 10, __('张', 'aksrt')),
$this->spinner('auto_num', 20, __('张', 'aksrt')),
[['key' => 'add_type', 'op' => '==', 'val' => 'auto']],
__('需要生成多少张卡密(单次生成数量太多可能会对服务器性能造成影响)', 'aksrt')
__('需要生成多少张卡密(1 - 1000 的整数,单次生成数量太多可能会对服务器性能造成影响)', 'aksrt')
);
$this->field_row(
__('标识', 'aksrt'),
@@ -401,7 +401,7 @@ class PayChargeCards {
);
$this->field_row(
__('自定义卡号位数', 'aksrt'),
$this->spinner('auto_rand_number_limit', 20, 1, 50, 5, __('位数', 'aksrt')),
$this->spinner('auto_rand_number_limit', 20, __('位数', 'aksrt')),
[
['key' => 'add_type', 'op' => '==', 'val' => 'auto'],
['key' => 'auto_top_s', 'op' => 'checked'],
@@ -409,7 +409,7 @@ class PayChargeCards {
);
$this->field_row(
__('自定义密码位数', 'aksrt'),
$this->spinner('auto_rand_password_limit', 35, 1, 50, 5, __('位数', 'aksrt')),
$this->spinner('auto_rand_password_limit', 35, __('位数', 'aksrt')),
[
['key' => 'add_type', 'op' => '==', 'val' => 'auto'],
['key' => 'auto_top_s', 'op' => 'checked'],
@@ -691,10 +691,11 @@ class PayChargeCards {
return;
}
// 自动生成。
// 自动生成。数量限定为 (0, 1000] 的整数:非数字 / 空值经 (int) 转换归零,负数与超上限
// 一并落在区间外,因此用一个区间判定即可覆盖全部非法输入,不另设分支。
$auto_num = isset($_REQUEST['auto_num']) ? (int) $_REQUEST['auto_num'] : 0;
if (!$auto_num) {
$this->page_notice(__('错误!', 'aksrt'), __('请输入需要生成的数量', 'aksrt'), 'error');
if ($auto_num < 1 || $auto_num > 1000) {
$this->page_notice(__('错误!', 'aksrt'), __('生成数量需为 1 - 1000 的整数', 'aksrt'), 'error');
return;
}
@@ -1003,23 +1004,21 @@ class PayChargeCards {
}
/**
* 数字输入(对齐参考主题 CSF spinner 字段的语义)。
* 数字输入(参考主题 CSF spinner / number 字段的原生替代)。
*
* **min / max / step 只放在 data-* 上,绝不写成原生约束属性。**
* 参考主题 `CSF_Field_spinner::render()` 渲染的就是
* `data-min/data-max/data-step` + `step="any"`,原生 min/max/step 一个都不写。
* 移植时把它们平铺成原生属性,会引入两类问题:
* 1) 这些值彼此不自洽 —— 例如 default=20 / min=1 / step=10 的合法格点是 1,11,21…,
* 写成原生 step 会让**默认值当场非法**(20 不在格点上);
* 2) 本表单的行是条件显隐的。HTML5 约束校验会检查**隐藏**控件,而隐藏控件无法聚焦、
* 浏览器弹不出提示气泡,于是直接**静默拒绝提交**(只有控制台一句
* "An invalid form control ... is not focusable"),页面毫无反应 ——
* 用户看到的就是「确认提交按钮不起作用」。
* 取值范围由服务端兜底:`ChargeCard::generate_cards()` 内已对数量与位数做 max/min 夹取。
* **一律不写原生 min / max / step 约束属性,只写 `step="any"`:**
* - 本表单的行是条件显隐的(`field_row()` 会给不满足依赖的 `<tr>` 打上 `display:none`)。
* HTML5 约束校验会检查**隐藏**控件,而隐藏控件无法聚焦、浏览器弹不出提示气泡,
* 于是直接**静默拒绝提交**(控制台只有一句 "An invalid form control ... is not focusable"),
* 页面毫无反应 —— 用户看到的就是「确认提交按钮不起作用」。
* - `step="any"` 不能省:`type="number"` 缺省 `step=1`,会退回整数网格,把非整数输入判为非法,
* 同样触发上面的静默拒绝。
*
* 取值范围不在标记里表达,由服务端单点约束:生成数量见 `handle_add()` 的 (0, 1000] 判定,
* 卡号 / 密码位数见 `ChargeCard::generate_cards()`。
*/
private function spinner(string $name, int $value, int $min, int $max, int $step, string $unit): string {
return '<input type="number" class="small-text" step="any" name="' . esc_attr($name) . '" value="' . esc_attr((string) $value) . '"'
. ' data-min="' . esc_attr((string) $min) . '" data-max="' . esc_attr((string) $max) . '" data-step="' . esc_attr((string) $step) . '" />'
private function spinner(string $name, int $value, string $unit): string {
return '<input type="number" class="small-text" step="any" name="' . esc_attr($name) . '" value="' . esc_attr((string) $value) . '" />'
. ' <span class="description">' . esc_html($unit) . '</span>';
}