array formula 报错及数组写入 database 的问题
结论
quantity、amount 和 discount 都是数组,不能直接相乘或相减。需要按照相同下标计算每条明细的金额,再把结果相加:
明细金额 = 单价 × 数量 × (1 - 折扣百分比 / 100)
发票总额 = 所有明细金额之和
表单输入框的名称应使用 [],例如 quantity[]。PHP 收到这些数组后,通过循环逐项解析,并使用预处理语句将明细逐行写入数据库。
fatal error 的原因
如果代码执行了类似下面的运算:
$total = $amount_array * $quantity_array;
PHP 会尝试让两个数组相乘,因此会报出类似 Unsupported operand types 的错误。array_sum() 只能对单个数组中的数值求和,不能完成三个数组之间的逐项计算。
下面的写法也不符合业务逻辑:
$total = array_sum($amount) * array_sum($quantity);
这段代码会先分别计算所有单价与所有数量的总和,再将两者相乘,导致不同商品之间发生交叉计算。
例如:
单价:[10, 20]
数量:[2, 3]
正确结果是:
10 × 2 + 20 × 3 = 80
分别求和后再相乘,结果则是:
(10 + 20) × (2 + 3) = 150
正确设置表单字段
每组输入框都应使用数组形式的 name:
<input type="text" name="description[]" required>
<input type="number" name="quantity[]" min="1" step="1" required>
<input type="number" name="amount[]" min="0" step="0.01" required>
<input type="number" name="discount[]" min="0" max="100" step="0.01" value="0">
动态添加的输入框也要使用相同的名称。例如:
function addInvoiceItem() {
const row = document.createElement('div');
row.className = 'invoice-item';
row.innerHTML = `
<input type="text" name="description[]" required>
<input type="number" name="quantity[]" min="1" step="1" required>
<input type="number" name="amount[]" min="0" step="0.01" required>
<input type="number" name="discount[]" min="0" max="100"
step="0.01" value="0">
<button type="button" class="remove-item">删除</button>
`;
row.querySelector('.remove-item').addEventListener('click', function () {
row.remove();
});
document.getElementById('invoice-items').appendChild(row);
}
对应的容器可以这样写:
<div id="invoice-items">
<div class="invoice-item">
<input type="text" name="description[]" required>
<input type="number" name="quantity[]" min="1" step="1" required>
<input type="number" name="amount[]" min="0" step="0.01" required>
<input type="number" name="discount[]" min="0" max="100"
step="0.01" value="0">
</div>
</div>
不建议反复使用 innerHTML += 添加字段,因为这种写法会重新创建容器内已有的 DOM 节点,可能造成事件监听器或输入状态丢失。
在 PHP 中计算明细金额和发票总额
假设折扣字段保存的是百分比,例如 10 表示九折,可以按下面的方式处理:
<?php
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
exit('Invalid request method.');
}
$descriptions = $_POST['description'] ?? [];
$quantities = $_POST['quantity'] ?? [];
$amounts = $_POST['amount'] ?? [];
$discounts = $_POST['discount'] ?? [];
if (
!is_array($descriptions) ||
!is_array($quantities) ||
!is_array($amounts) ||
!is_array($discounts)
) {
exit('Invalid invoice data.');
}
$count = count($descriptions);
if (
$count === 0 ||
count($quantities) !== $count ||
count($amounts) !== $count ||
count($discounts) !== $count
) {
exit('Invoice item arrays do not match.');
}
$items = [];
$invoiceTotal = 0.0;
for ($i = 0; $i < $count; $i++) {
$description = trim((string) $descriptions[$i]);
if (
$description === '' ||
!is_numeric($quantities[$i]) ||
!is_numeric($amounts[$i]) ||
!is_numeric($discounts[$i])
) {
exit('Invalid item at row ' . ($i + 1));
}
$quantity = (int) $quantities[$i];
$amount = (float) $amounts[$i];
$discount = (float) $discounts[$i];
if (
$quantity <= 0 ||
$amount < 0 ||
$discount < 0 ||
$discount > 100
) {
exit('Invalid value at row ' . ($i + 1));
}
$lineTotal = $amount * $quantity * (1 - $discount / 100);
$lineTotal = round($lineTotal, 2);
$items[] = [
'description' => $description,
'quantity' => $quantity,
'amount' => $amount,
'discount' => $discount,
'total' => $lineTotal,
];
$invoiceTotal += $lineTotal;
}
$invoiceTotal = round($invoiceTotal, 2);
如果确实需要一个 total_amt_array,可以在循环中保存每条明细的金额,再通过 array_sum() 求和:
$totalAmtArray = [];
for ($i = 0; $i < $count; $i++) {
$quantity = (float) $quantities[$i];
$amount = (float) $amounts[$i];
$discount = (float) $discounts[$i];
$totalAmtArray[] = round(
$amount * $quantity * (1 - $discount / 100),
2
);
}
$invoiceTotal = round(array_sum($totalAmtArray), 2);
如果后续不会使用这些明细金额,直接在循环中累加 $invoiceTotal 会更简单,也不用额外保存一个数组。
将明细写入数据库
通常需要分别保存发票主体和发票明细:
invoices
- id
- customer_id
- invoice_total
- created_at
invoice_items
- id
- invoice_id
- description
- quantity
- unit_amount
- discount_percent
- line_total
不要把多个商品数组转换成字符串后存入同一个字段,否则会给查询、统计和后续修改带来麻烦。可以先创建发票,再逐条插入明细,同时使用事务,保证所有数据要么全部写入成功,要么全部回滚。
下面的示例使用 mysqli:
<?php
mysqli_begin_transaction($connection);
try {
$invoiceStmt = mysqli_prepare(
$connection,
'INSERT INTO invoices (invoice_total, created_at)
VALUES (?, NOW())'
);
if (!$invoiceStmt) {
throw new RuntimeException(mysqli_error($connection));
}
mysqli_stmt_bind_param($invoiceStmt, 'd', $invoiceTotal);
if (!mysqli_stmt_execute($invoiceStmt)) {
throw new RuntimeException(mysqli_stmt_error($invoiceStmt));
}
$invoiceId = mysqli_insert_id($connection);
$itemStmt = mysqli_prepare(
$connection,
'INSERT INTO invoice_items
(invoice_id, description, quantity, unit_amount,
discount_percent, line_total)
VALUES (?, ?, ?, ?, ?, ?)'
);
if (!$itemStmt) {
throw new RuntimeException(mysqli_error($connection));
}
foreach ($items as $item) {
mysqli_stmt_bind_param(
$itemStmt,
'isiddd',
$invoiceId,
$item['description'],
$item['quantity'],
$item['amount'],
$item['discount'],
$item['total']
);
if (!mysqli_stmt_execute($itemStmt)) {
throw new RuntimeException(mysqli_stmt_error($itemStmt));
}
}
mysqli_commit($connection);
} catch (Throwable $e) {
mysqli_rollback($connection);
// 生产环境应记录具体错误,但不要直接向用户输出数据库信息。
exit('Invoice could not be saved.');
}
预处理语句可以正确处理商品描述中的引号等字符,也能避免将用户输入直接拼接到 SQL 中所带来的 SQL 注入风险。
金额计算注意事项
PHP 的 float 使用二进制浮点数表示,计算某些十进制金额时可能产生精度误差。简单场景可以在计算每条明细后调用 round($value, 2),并为数据库字段选择合适的 DECIMAL 类型,例如:
unit_amount DECIMAL(12, 2)
line_total DECIMAL(12, 2)
discount_percent DECIMAL(5, 2)
如果系统对账务精度要求较高,可以按最小货币单位计算。例如,将金额转换为“分”并使用整数,或者使用 BCMath。还要提前确定舍入规则,是先对每条明细舍入再求和,还是先计算总额再统一舍入。这两种方式在少数情况下可能相差一分钱。
不要信任浏览器提交的 line_total 或发票总额。前端可以显示即时计算结果,但写入数据库的金额必须由服务器根据单价、数量和折扣重新计算。
备注:内容仅供参考。
