PAYATHON 2026

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 或发票总额。前端可以显示即时计算结果,但写入数据库的金额必须由服务器根据单价、数量和折扣重新计算。

备注:内容仅供参考。