ThinkPHP项目小数精度与舍入

wen PHP项目 11

本文目录导读:

ThinkPHP项目小数精度与舍入

  1. MySQL 数据库层面的处理
  2. PHP 端处理(推荐)
  3. ThinkPHP 框架集成
  4. 完整实践案例
  5. 最佳实践建议

在ThinkPHP项目中进行小数精度与舍入处理时,主要有以下几种方案:

MySQL 数据库层面的处理

字段类型设计

-- 推荐使用 DECIMAL 类型
CREATE TABLE `orders` (
  `price` DECIMAL(10,2) NOT NULL DEFAULT '0.00',
  `discount` DECIMAL(10,4) DEFAULT '1.0000',
  `total` DECIMAL(10,2) GENERATED ALWAYS AS (price * discount) STORED
);

常用 SQL 函数

// 在 ThinkPHP 查询中使用
Db::name('orders')
    ->field('id, ROUND(price, 2) as rounded_price, TRUNCATE(price, 2) as truncated_price')
    ->select();

PHP 端处理(推荐)

使用 bcmath 扩展(最精确)

use think\facade\Db;
class PriceService
{
    /**
     * 使用 bcmath 进行高精度计算
     */
    public function bcmathExample()
    {
        $price = '19.99';
        $quantity = 3;
        // 乘法
        $total = bcmul($price, $quantity, 2);  // '59.97'
        // 加法
        $sum = bcadd('1.5', '2.3', 2);        // '3.80'
        // 除法
        $avg = bcdiv($total, $quantity, 2);   // '19.99'
        // 比较
        $result = bccomp($price, '20.00', 2); // -1 (小于)
        // 取模
        $mod = bcmod('10', '3');              // '1'
        // 幂运算
        $pow = bcpow('1.1', '2', 2);          // '1.21'
        // 平方根
        $sqrt = bcsqrt('4', 2);               // '2.00'
    }
}

自定义舍入函数

class MoneyHelper
{
    /**
     * 四舍五入(默认)
     */
    public static function round($number, $precision = 2)
    {
        return round((float)$number, $precision, PHP_ROUND_HALF_UP);
    }
    /**
     * 向下取整(截断)
     */
    public static function floor($number, $precision = 2)
    {
        $factor = pow(10, $precision);
        return floor((float)$number * $factor) / $factor;
    }
    /**
     * 向上取整
     */
    public static function ceil($number, $precision = 2)
    {
        $factor = pow(10, $precision);
        return ceil((float)$number * $factor) / $factor;
    }
    /**
     * 银行家舍入(四舍六入五成双)
     */
    public static function bankerRound($number, $precision = 2)
    {
        return round((float)$number, $precision, PHP_ROUND_HALF_EVEN);
    }
    /**
     * 格式化金额(千分位)
     */
    public static function format($number, $precision = 2)
    {
        return number_format((float)$number, $precision, '.', ',');
    }
}
// 使用示例
$result = MoneyHelper::ceil('19.991', 2);     // 20.00
$result = MoneyHelper::floor('19.999', 2);    // 19.99

ThinkPHP 框架集成

模型事件自动处理

namespace app\models;
use think\Model;
use think\facade\Db;
class Order extends Model
{
    protected $table = 'orders';
    // 类型转换
    protected $type = [
        'price'       => 'float',
        'discount'    => 'float',
        'total'       => 'float'
    ];
    // 自动时间戳
    protected $autoWriteTimestamp = true;
    // 模型钩子:保存前处理
    public static function onBeforeInsert($order)
    {
        $order->price  = MoneyHelper::round($order->price, 2);
        $order->total  = bcmul($order->price, $order->quantity, 2);
    }
    // 模型钩子:查询后处理
    public static function onAfterRead($order)
    {
        $order->price_fmt = MoneyHelper::format($order->price);
    }
    // 访问器
    public function getPriceAttr($value)
    {
        return $this->castToDecimal($value, 2);
    }
    private function castToDecimal($value, $precision)
    {
        return number_format((float)$value, $precision, '.', '');
    }
}

全局服务提供

// 在公共函数文件中定义
if (!function_exists('money_format')) {
    function money_format($amount, $precision = 2)
    {
        return number_format((float)$amount, $precision, '.', '');
    }
}
// 使用示例
$total = money_format($order->total, 2);

完整实践案例

use think\facade\Db;
use think\exception\ValidateException;
class OrderService
{
    /**
     * 创建订单(包含金额处理)
     */
    public function createOrder(array $data)
    {
        Db::startTrans();
        try {
            // 商品单价精确计算
            $price = '0.00';
            $quantity = 0;
            $items = [];
            foreach ($data['items'] as $item) {
                // 从数据库获取商品价格(使用字符串比较避免精度问题)
                $product = Db::name('products')->where('id', $item['product_id'])->find();
                if (!$product) {
                    throw new ValidateException('商品不存在');
                }
                // 确保价格为字符串类型
                $unitPrice = (string)$product['price'];  // '19.99'
                $quantity = (int)$item['quantity'];
                // 计算小计:使用 bcmath
                $subtotal = bcmul($unitPrice, $quantity, 4);
                $items[] = [
                    'product_id' => $item['product_id'],
                    'quantity'   => $quantity,
                    'unit_price' => $unitPrice,
                    'subtotal'   => $subtotal
                ];
                // 累加总价
                $price = bcadd($price, $subtotal, 4);
            }
            // 计算折扣(比如8.8折)
            $discount = '0.88';
            $discountedPrice = bcmul($price, $discount, 4);
            // 运费
            $shippingFee = '10.00';
            $totalAmount = bcadd($discountedPrice, $shippingFee, 4);
            // 格式化最终金额(保留两位小数)
            $finalTotal = MoneyHelper::round($totalAmount, 2);
            // 插入订单
            $orderId = Db::name('orders')->insertGetId([
                'user_id'     => $data['user_id'],
                'total_amount' => $finalTotal,
                'discount'    => MoneyHelper::round($discount, 4),
                'status'      => 0,
                'create_time' => time()
            ]);
            // 插入订单详情
            foreach ($items as $item) {
                Db::name('order_items')->insert([
                    'order_id'   => $orderId,
                    'product_id' => $item['product_id'],
                    'quantity'   => $item['quantity'],
                    'unit_price' => $item['unit_price'],
                    'subtotal'   => $item['subtotal']
                ]);
            }
            Db::commit();
            return $orderId;
        } catch (\Exception $e) {
            Db::rollback();
            throw new \Exception('订单创建失败:' . $e->getMessage());
        }
    }
    /**
     * 金额一致性校验
     */
    public function verifyAmount($orderId)
    {
        $order = Db::name('orders')->where('id', $orderId)->find();
        $items = Db::name('order_items')->where('order_id', $orderId)->select();
        $calculatedTotal = '0.00';
        foreach ($items as $item) {
            $calculatedTotal = bcadd($calculatedTotal, $item['subtotal'], 4);
        }
        // 添加折扣和运费
        $calculatedTotal = bcmul($calculatedTotal, $order['discount'], 4);
        $calculatedTotal = bcadd($calculatedTotal, $order['shipping_fee'], 4);
        // 比较(允许1分钱误差)
        return bccomp($calculatedTotal, $order['total_amount'], 2) === 0;
    }
}

最佳实践建议

  1. 数据库层:金额字段使用 DECIMAL(10,2) 或更大精度
  2. 计算层:始终使用 bcmath 扩展进行金钱计算
  3. 显示层:使用 number_format() 格式化输出
  4. 比较判断:使用 bccomp() 而非直接
  5. 精度设置:中间计算保留4位小数,最终结果保留2位小数
// 添加 bcmath 扩展检查
if (!extension_loaded('bcmath')) {
    throw new \RuntimeException('请安装 bcmath 扩展');
}
// PHP.ini 配置建议
ini_set('precision', 14);
ini_set('serialize_precision', -1);

通过这些方法,可以在ThinkPHP项目中有效处理小数精度与舍入问题,避免财务计算中的精度丢失。

抱歉,评论功能暂时关闭!