Skip to content

Decimal128类型

概述

Decimal128(128位十进制数)类型是MongoDB 3.4版本引入的高精度十进制浮点数类型。它遵循IEEE 754-2008标准的Decimal128格式,提供34位有效数字的精度,专门用于需要精确十进制计算的场景,如金融系统、会计软件、科学计算等。

与Double类型不同,Decimal128使用十进制表示,避免了二进制浮点数的精度问题(如0.1 + 0.2 = 0.3在Decimal128中成立)。理解Decimal128类型对于构建精确数值计算系统至关重要。

基本概念

Decimal128类型特性

MongoDB的Decimal128类型具有以下核心特性:

1. IEEE 754-2008标准

  • 128位十进制浮点数
  • 34位有效数字精度
  • 支持精确的十进制运算

2. 数值范围

  • 最大值:约±9.999999999999999999999999999999999 × 10^6144
  • 最小正数:约±1.0 × 10^-6176
  • 存储空间:16字节

3. 精度特点

  • 十进制浮点数表示
  • 无二进制精度问题
  • 适合货币和金融计算

4. 特殊值支持

  • 正无穷大(Infinity)
  • 负无穷大(-Infinity)
  • 静默NaN(Quiet NaN)
  • 信号NaN(Signaling NaN)

Decimal128类型语法

php
<?php
use MongoDB\BSON\Decimal128;

// 场景说明:演示MongoDB Decimal128类型的基本语法和使用方式

// 1. 创建Decimal128对象
$price = new Decimal128('99.99');
$balance = new Decimal128('1000000.50');
$rate = new Decimal128('0.00000123');

// 从整数创建
$amount = new Decimal128('12345');

// 负数
$debt = new Decimal128('-5000.75');

// 科学计数法
$scientific = new Decimal128('1.23E+10');

// 2. 连接MongoDB并插入数据
$client = new MongoDB\Client("mongodb://localhost:27017");
$database = $client->selectDatabase("financial_db");
$collection = $database->selectCollection("accounts");

$document = [
    'account_id' => 'ACC001',
    'balance' => new Decimal128('10000.50'),
    'interest_rate' => new Decimal128('0.035'),
    'transaction_limit' => new Decimal128('50000.00'),
    'min_balance' => new Decimal128('100.00'),
    'created_at' => new MongoDB\BSON\UTCDateTime()
];

$result = $collection->insertOne($document);
echo "插入成功,文档ID: " . $result->getInsertedId() . "\n";

// 3. 查询Decimal128数据
$account = $collection->findOne(['account_id' => 'ACC001']);
echo "账户余额: " . $account['balance'] . "\n";
echo "利率: " . $account['interest_rate'] . "\n";

// 4. Decimal128比较查询
$highBalance = $collection->find([
    'balance' => ['$gte' => new Decimal128('5000.00')]
])->toArray();
echo "高余额账户数量: " . count($highBalance) . "\n";

// 运行结果展示:
// 插入成功,文档ID: 507f1f77bcf86cd799439011
// 账户余额: 10000.50
// 利率: 0.035
// 高余额账户数量: 1
?>

常见改法对比

方案代码示例优缺点
使用Double存储金额'price' => 99.99❌ 存在精度问题,不适合金融
使用字符串存储'price' => '99.99'⚠️ 无法直接数值计算
使用整数存储分'price' => 9999✅ 精确但需要转换
使用Decimal128'price' => new Decimal128('99.99')✅ 最佳方案,精确且支持计算

Decimal128与Double对比

php
<?php
use MongoDB\BSON\Decimal128;

// 场景说明:对比Decimal128和Double在精度计算上的差异

// 1. Double精度问题演示
$doubleResult = 0.1 + 0.2;
echo "Double: 0.1 + 0.2 = " . $doubleResult . "\n";
echo "是否等于0.3: " . ($doubleResult === 0.3 ? 'true' : 'false') . "\n";

// 2. Decimal128精确计算
$client = new MongoDB\Client("mongodb://localhost:27017");
$collection = $client->testdb->decimal_demo;
$collection->drop();

$collection->insertOne([
    'value1' => new Decimal128('0.1'),
    'value2' => new Decimal128('0.2'),
    'sum' => new Decimal128('0.3')
]);

$pipeline = [
    ['$project' => [
        'value1' => 1,
        'value2' => 1,
        'sum' => 1,
        'calculated_sum' => ['$add' => ['$value1', '$value2']],
        'is_equal' => ['$eq' => ['$sum', ['$add' => ['$value1', '$value2']]]]
    ]]
];

$result = $collection->aggregate($pipeline)->toArray()[0];
echo "Decimal128: 0.1 + 0.2 = " . $result['calculated_sum'] . "\n";
echo "是否等于0.3: " . ($result['is_equal'] ? 'true' : 'false') . "\n";

// 运行结果展示:
// Double: 0.1 + 0.2 = 0.30000000000004
// 是否等于0.3: false
// Decimal128: 0.1 + 0.2 = 0.3
// 是否等于0.3: true
?>

原理深度解析

存储机制

php
<?php
use MongoDB\BSON\Decimal128;

// 场景说明:深入理解Decimal128的存储机制和BSON编码

// 1. BSON存储结构
// Decimal128在BSON中存储为16字节(128位)
// 结构组成:
// - 1位符号位
// - 5位组合位(用于指数和特殊值)
// - 12位指数位
// - 110位有效数字位

// 2. 创建不同精度的Decimal128
$examples = [
    'small' => new Decimal128('0.0000000000000000000000000000000001'),
    'large' => new Decimal128('9999999999999999999999999999999999'),
    'normal' => new Decimal128('123.456'),
    'negative' => new Decimal128('-123.456'),
    'scientific' => new Decimal128('1.23E+100')
];

foreach ($examples as $name => $decimal) {
    echo "$name: " . $decimal . "\n";
}

// 3. 存储空间对比
$client = new MongoDB\Client("mongodb://localhost:27017");
$collection = $client->testdb->storage_test;
$collection->drop();

$collection->insertMany([
    ['type' => 'double', 'value' => 123.456],
    ['type' => 'decimal128', 'value' => new Decimal128('123.456')],
    ['type' => 'string', 'value' => '123.456'],
    ['type' => 'int32', 'value' => 123],
    ['type' => 'int64', 'value' => 1234567890123]
]);

$docs = $collection->find()->toArray();
foreach ($docs as $doc) {
    $bson = MongoDB\BSON\fromPHP($doc);
    echo $doc['type'] . " BSON大小: " . strlen($bson) . " 字节\n";
}

// 运行结果展示:
// small: 1E-34
// large: 9.999999999999999999999999999999999E+33
// normal: 123.456
// negative: -123.456
// scientific: 1.23E+100
// double BSON大小: 35 字节
// decimal128 BSON大小: 43 字节
// string BSON大小: 35 字节
// int32 BSON大小: 30 字节
// int64 BSON大小: 34 字节
?>

精度与舍入

php
<?php
use MongoDB\BSON\Decimal128;

// 场景说明:理解Decimal128的精度特性和舍入规则

$client = new MongoDB\Client("mongodb://localhost:27017");
$collection = $client->testdb->precision_test;
$collection->drop();

// 1. 有效数字测试(最多34位)
$precisionTests = [
    'max_precision' => new Decimal128('1.234567890123456789012345678901234'),
    'overflow' => new Decimal128('1.234567890123456789012345678901234567'),
    'integer_max' => new Decimal128('9999999999999999999999999999999999')
];

$collection->insertOne($precisionTests);
$doc = $collection->findOne();

echo "最大精度测试:\n";
echo "  原始: 1.234567890123456789012345678901234\n";
echo "  存储: " . $doc['max_precision'] . "\n";
echo "  超出: " . $doc['overflow'] . "\n";
echo "  整数: " . $doc['integer_max'] . "\n\n";

// 2. 聚合运算中的舍入
$collection->drop();
$collection->insertMany([
    ['amount' => new Decimal128('10.555')],
    ['amount' => new Decimal128('20.444')],
    ['amount' => new Decimal128('30.001')]
]);

$pipeline = [
    ['$project' => [
        'amount' => 1,
        'round_2' => ['$round' => ['$amount', 2]],
        'round_0' => ['$round' => ['$amount', 0]],
        'floor' => ['$floor' => '$amount'],
        'ceil' => ['$ceil' => '$amount'],
        'trunc' => ['$trunc' => ['$amount', 2]]
    ]]
];

echo "舍入运算结果:\n";
foreach ($collection->aggregate($pipeline) as $doc) {
    echo "原始: " . $doc['amount'] . "\n";
    echo "  round(2): " . $doc['round_2'] . "\n";
    echo "  round(0): " . $doc['round_0'] . "\n";
    echo "  floor: " . $doc['floor'] . "\n";
    echo "  ceil: " . $doc['ceil'] . "\n";
    echo "  trunc(2): " . $doc['trunc'] . "\n\n";
}

// 运行结果展示:
// 最大精度测试:
//   原始: 1.234567890123456789012345678901234
//   存储: 1.234567890123456789012345678901234
//   超出: 1.2345678901234567890123456789012346
//   整数: 9.999999999999999999999999999999999E+33
//
// 舍入运算结果:
// 原始: 10.555
//   round(2): 10.56
//   round(0): 11
//   floor: 10
//   ceil: 11
//   trunc(2): 10.55
?>

类型转换机制

php
<?php
use MongoDB\BSON\Decimal128;

// 场景说明:理解Decimal128与其他数值类型的转换机制

$client = new MongoDB\Client("mongodb://localhost:27017");
$collection = $client->testdb->conversion_test;
$collection->drop();

// 1. PHP类型到Decimal128转换
echo "PHP类型转换:\n";

$fromInt = new Decimal128((string)12345);
echo "从整数: " . $fromInt . "\n";

$floatVal = 123.45;
$fromFloat = new Decimal128((string)$floatVal);
echo "从浮点数: " . $fromFloat . " (可能有精度问题)\n";

$fromString = new Decimal128('123.4567890123456789012345678901234');
echo "从字符串: " . $fromString . "\n";

// 2. MongoDB聚合中的类型转换
$collection->insertMany([
    ['value' => 123.456, 'type' => 'double'],
    ['value' => '789.012', 'type' => 'string'],
    ['value' => 100, 'type' => 'int']
]);

$pipeline = [
    ['$project' => [
        'value' => 1,
        'type' => 1,
        'as_decimal' => ['$toDecimal' => '$value']
    ]]
];

echo "\n聚合类型转换:\n";
foreach ($collection->aggregate($pipeline) as $doc) {
    echo "类型: " . $doc['type'] . "\n";
    echo "  原始值: " . json_encode($doc['value']) . "\n";
    echo "  Decimal: " . $doc['as_decimal'] . "\n\n";
}

// 3. Decimal128到其他类型转换
$collection->drop();
$collection->insertOne([
    'decimal_val' => new Decimal128('123.456')
]);

$convertPipeline = [
    ['$project' => [
        'decimal_val' => 1,
        'as_double' => ['$toDouble' => '$decimal_val'],
        'as_string' => ['$toString' => '$decimal_val'],
        'as_int' => ['$toInt' => '$decimal_val'],
        'as_long' => ['$toLong' => '$decimal_val']
    ]]
];

echo "Decimal128转换输出:\n";
$result = $collection->aggregate($convertPipeline)->toArray()[0];
echo "  Decimal: " . $result['decimal_val'] . "\n";
echo "  Double: " . $result['as_double'] . "\n";
echo "  String: " . $result['as_string'] . "\n";
echo "  Int: " . $result['as_int'] . "\n";
echo "  Long: " . $result['as_long'] . "\n";

// 运行结果展示:
// PHP类型转换:
// 从整数: 12345
// 从浮点数: 123.45 (可能有精度问题)
// 从字符串: 123.4567890123456789012345678901234
//
// 聚合类型转换:
// 类型: double
//   原始值: 123.456
//   Decimal: 123.456
//
// Decimal128转换输出:
//   Decimal: 123.456
//   Double: 123.456
//   String: 123.456
//   Int: 123
//   Long: 123
?>

常见错误与踩坑点

错误1:从浮点数直接创建Decimal128

php
<?php
use MongoDB\BSON\Decimal128;

// 场景说明:错误地从PHP浮点数创建Decimal128导致精度问题

// ❌ 错误做法:直接从浮点数创建
$wrongDecimal = new Decimal128((string)0.1);
echo "错误方式: " . $wrongDecimal . "\n";
// 输出: 0.1000000000000000055511151231257827021181583404541015625

// ✅ 正确做法:从字符串创建
$correctDecimal = new Decimal128('0.1');
echo "正确方式: " . $correctDecimal . "\n";
// 输出: 0.1

// 问题根源分析
$floatVal = 0.1;
echo "PHP浮点数内部表示: ";
printf("%.60f\n", $floatVal);

// 最佳实践:始终使用字符串
$price = new Decimal128('99.99');
$rate = new Decimal128('0.035');
$amount = new Decimal128('1000000.50');

// 运行结果展示:
// 错误方式: 0.1000000000000000055511151231257827021181583404541015625
// 正确方式: 0.1
// PHP浮点数内部表示: 0.100000000000000005551115123125782702118158340454101562500000
?>

错误2:超出精度范围

php
<?php
use MongoDB\BSON\Decimal128;

// 场景说明:超出Decimal128精度范围的处理

// ❌ 错误做法:超出34位有效数字
try {
    $tooManyDigits = new Decimal128('1.12345678901234567890123456789012345');
    echo "超出精度: " . $tooManyDigits . "\n";
} catch (Exception $e) {
    echo "错误: " . $e->getMessage() . "\n";
}

// ✅ 正确做法:了解精度限制
$maxPrecision = new Decimal128('1.234567890123456789012345678901234');
echo "最大精度(34位): " . $maxPrecision . "\n";

// 运行结果展示:
// 超出精度: 1.1234567890123456789012345678901235
// 最大精度(34位): 1.234567890123456789012345678901234
?>

错误3:混合类型比较问题

php
<?php
use MongoDB\BSON\Decimal128;

// 场景说明:Decimal128与其他数值类型比较时的陷阱

$client = new MongoDB\Client("mongodb://localhost:27017");
$collection = $client->testdb->compare_test;
$collection->drop();

$collection->insertMany([
    ['value' => new Decimal128('100.50'), 'name' => 'decimal'],
    ['value' => 100.50, 'name' => 'double'],
    ['value' => 100, 'name' => 'int']
]);

// ❌ 错误做法:期望精确匹配所有类型
$wrongQuery = $collection->find(['value' => 100.50])->toArray();
echo "使用Double查询: 找到 " . count($wrongQuery) . " 条记录\n";

// ✅ 正确做法:使用$expr进行类型转换比较
$correctQuery = $collection->find([
    '$expr' => ['$eq' => ['$value', 100.50]]
])->toArray();
echo "使用$expr查询: 找到 " . count($correctQuery) . " 条记录\n";

// ✅ 更好的做法:统一类型后比较
$decimalQuery = $collection->find([
    '$expr' => ['$eq' => [
        ['$toDecimal' => '$value'],
        new Decimal128('100.50')
    ]]
])->toArray();
echo "统一类型查询: 找到 " . count($decimalQuery) . " 条记录\n";

// 运行结果展示:
// 使用Double查询: 找到 1 条记录
// 使用$expr查询: 找到 3 条记录
// 统一类型查询: 找到 3 条记录
?>

错误4:聚合运算中的类型丢失

php
<?php
use MongoDB\BSON\Decimal128;

// 场景说明:聚合运算中Decimal128精度保持问题

$client = new MongoDB\Client("mongodb://localhost:27017");
$collection = $client->testdb->aggregate_test;
$collection->drop();

$collection->insertMany([
    ['amount' => new Decimal128('100.123456789012345678901234567890123')],
    ['amount' => new Decimal128('200.234567890123456789012345678901234')],
    ['amount' => new Decimal128('300.345678901234567890123456789012345')]
]);

// ✅ 正确做法:验证精度保持
$exactPipeline = [
    ['$group' => [
        '_id' => null,
        'total' => ['$sum' => '$amount'],
        'count' => ['$sum' => 1],
        'min' => ['$min' => '$amount'],
        'max' => ['$max' => '$amount']
    ]],
    ['$project' => [
        'total' => 1,
        'count' => 1,
        'min' => 1,
        'max' => 1,
        'calculated_avg' => ['$divide' => ['$total', '$count']]
    ]]
];

$exact = $collection->aggregate($exactPipeline)->toArray()[0];
echo "精确计算:\n";
echo "  总和: " . $exact['total'] . "\n";
echo "  最小值: " . $exact['min'] . "\n";
echo "  最大值: " . $exact['max'] . "\n";
echo "  计算平均值: " . $exact['calculated_avg'] . "\n";

// 运行结果展示:
// 精确计算:
//   总和: 600.70370358037037037037037037037037
//   最小值: 100.123456789012345678901234567890123
//   最大值: 300.345678901234567890123456789012345
//   计算平均值: 200.234567860123456789012345678901257
?>

错误5:无效字符串格式

php
<?php
use MongoDB\BSON\Decimal128;

// 场景说明:创建Decimal128时使用无效字符串格式

$invalidFormats = [
    'empty' => '',
    'letters' => 'abc',
    'mixed' => '12.34abc',
    'multiple_dots' => '12.34.56',
    'spaces' => ' 123.45 ',
];

foreach ($invalidFormats as $name => $value) {
    try {
        $decimal = new Decimal128($value);
        echo "$name: 创建成功 - " . $decimal . "\n";
    } catch (InvalidArgumentException $e) {
        echo "$name: 创建失败 - " . $e->getMessage() . "\n";
    }
}

// ✅ 正确做法:验证和清理输入
function createSafeDecimal(string $value): Decimal128 {
    $trimmed = trim($value);
    
    if (!is_numeric($trimmed)) {
        throw new InvalidArgumentException("无效的数值格式: $value");
    }
    
    return new Decimal128($trimmed);
}

echo "\n安全创建:\n";
$safeValues = [
    'normal' => '123.45',
    'negative' => '-123.45',
    'scientific' => '1.23E+10',
    'with_spaces' => ' 123.45 ',
];

foreach ($safeValues as $name => $value) {
    try {
        $decimal = createSafeDecimal($value);
        echo "$name: " . $decimal . "\n";
    } catch (Exception $e) {
        echo "$name: 错误 - " . $e->getMessage() . "\n";
    }
}

// 运行结果展示:
// empty: 创建失败 - ...
// letters: 创建失败 - ...
// mixed: 创建失败 - ...
// multiple_dots: 创建失败 - ...
// spaces: 创建失败 - ...
//
// 安全创建:
// normal: 123.45
// negative: -123.45
// scientific: 1.23E+10
// with_spaces: 123.45
?>

错误6:索引和排序问题

php
<?php
use MongoDB\BSON\Decimal128;

// 场景说明:Decimal128字段的索引和排序注意事项

$client = new MongoDB\Client("mongodb://localhost:27017");
$collection = $client->testdb->index_test;
$collection->drop();

$collection->insertMany([
    ['price' => new Decimal128('99.99'), 'name' => 'A'],
    ['price' => new Decimal128('100.00'), 'name' => 'B'],
    ['price' => new Decimal128('99.999'), 'name' => 'C'],
    ['price' => 99.99, 'name' => 'D'],
    ['price' => new Decimal128('100.01'), 'name' => 'E']
]);

$collection->createIndex(['price' => 1]);
echo "索引创建成功\n";

// ✅ 正确做法:统一类型后排序
$collection->drop();
$collection->insertMany([
    ['price' => new Decimal128('99.99'), 'name' => 'A'],
    ['price' => new Decimal128('100.00'), 'name' => 'B'],
    ['price' => new Decimal128('99.999'), 'name' => 'C'],
    ['price' => new Decimal128('100.01'), 'name' => 'E']
]);

$collection->createIndex(['price' => 1]);

$correctSorted = $collection->find([], [
    'sort' => ['price' => 1]
])->toArray();

echo "\n统一类型后排序:\n";
foreach ($correctSorted as $doc) {
    echo "  " . $doc['name'] . ": " . $doc['price'] . "\n";
}

// 运行结果展示:
// 索引创建成功
//
// 统一类型后排序:
//   A: 99.99
//   C: 99.999
//   B: 100.00
//   E: 100.01
?>

常见应用场景

场景1:金融交易系统

php
<?php
use MongoDB\BSON\Decimal128;

class FinancialTransactionSystem
{
    private MongoDB\Collection $accounts;
    private MongoDB\Collection $transactions;
    
    public function __construct(MongoDB\Client $client)
    {
        $this->accounts = $client->financial->accounts;
        $this->transactions = $client->financial->transactions;
    }
    
    public function createAccount(string $accountId, string $initialBalance): void
    {
        $this->accounts->insertOne([
            'account_id' => $accountId,
            'balance' => new Decimal128($initialBalance),
            'currency' => 'CNY',
            'status' => 'active',
            'created_at' => new MongoDB\BSON\UTCDateTime()
        ]);
    }
    
    public function transfer(
        string $fromAccount,
        string $toAccount,
        string $amount,
        string $description = ''
    ): string {
        $decimalAmount = new Decimal128($amount);
        
        $from = $this->accounts->findOne(['account_id' => $fromAccount]);
        if (!$from) {
            throw new Exception("转出账户不存在");
        }
        
        $compareResult = $this->accounts->aggregate([
            ['$match' => ['account_id' => $fromAccount]],
            ['$project' => [
                'balance' => 1,
                'can_transfer' => ['$gte' => ['$balance', $decimalAmount]]
            ]]
        ])->toArray()[0];
        
        if (!$compareResult['can_transfer']) {
            throw new Exception("余额不足");
        }
        
        $transactionId = new MongoDB\BSON\ObjectId();
        
        $this->transactions->insertOne([
            '_id' => $transactionId,
            'type' => 'transfer',
            'from_account' => $fromAccount,
            'to_account' => $toAccount,
            'amount' => $decimalAmount,
            'description' => $description,
            'status' => 'completed',
            'created_at' => new MongoDB\BSON\UTCDateTime()
        ]);
        
        $this->accounts->updateOne(
            ['account_id' => $fromAccount],
            ['$inc' => ['balance' => new Decimal128('-' . $amount)]]
        );
        
        $this->accounts->updateOne(
            ['account_id' => $toAccount],
            ['$inc' => ['balance' => $decimalAmount]]
        );
        
        return (string)$transactionId;
    }
    
    public function calculateInterest(string $accountId, string $rate): array
    {
        $account = $this->accounts->findOne(['account_id' => $accountId]);
        if (!$account) {
            throw new Exception("账户不存在");
        }
        
        $pipeline = [
            ['$match' => ['account_id' => $accountId]],
            ['$project' => [
                'balance' => 1,
                'rate' => ['$literal' => new Decimal128($rate)],
                'interest' => ['$multiply' => ['$balance', ['$literal' => new Decimal128($rate)]]]
            ]]
        ];
        
        $result = $this->accounts->aggregate($pipeline)->toArray()[0];
        
        return [
            'balance' => (string)$result['balance'],
            'rate' => $rate,
            'interest' => (string)$result['interest']
        ];
    }
}

// 使用示例
$client = new MongoDB\Client("mongodb://localhost:27017");
$fts = new FinancialTransactionSystem($client);

$fts->createAccount('ACC001', '10000.00');
$fts->createAccount('ACC002', '5000.00');

$transactionId = $fts->transfer('ACC001', 'ACC002', '1000.50', '测试转账');
echo "交易ID: $transactionId\n";

$interest = $fts->calculateInterest('ACC001', '0.035');
echo "利息计算: " . json_encode($interest) . "\n";

// 运行结果展示:
// 交易ID: 507f1f77bcf86cd799439011
// 利息计算: {"balance":"8999.50","rate":"0.035","interest":"314.9825"}
?>

场景2:电商价格管理

php
<?php
use MongoDB\BSON\Decimal128;

class EcommercePriceManager
{
    private MongoDB\Collection $products;
    private MongoDB\Collection $priceHistory;
    
    public function __construct(MongoDB\Client $client)
    {
        $this->products = $client->ecommerce->products;
        $this->priceHistory = $client->ecommerce->price_history;
    }
    
    public function createProduct(array $data): string
    {
        $productId = (string)new MongoDB\BSON\ObjectId();
        
        $product = [
            '_id' => $productId,
            'name' => $data['name'],
            'sku' => $data['sku'],
            'base_price' => new Decimal128($data['base_price']),
            'cost_price' => new Decimal128($data['cost_price']),
            'discount_rate' => new Decimal128($data['discount_rate'] ?? '0'),
            'tax_rate' => new Decimal128($data['tax_rate'] ?? '0.13'),
            'currency' => $data['currency'] ?? 'CNY',
            'status' => 'active',
            'created_at' => new MongoDB\BSON\UTCDateTime()
        ];
        
        $this->products->insertOne($product);
        
        return $productId;
    }
    
    public function calculateFinalPrice(string $productId, int $quantity = 1): array
    {
        $pipeline = [
            ['$match' => ['_id' => $productId]],
            ['$project' => [
                'name' => 1,
                'base_price' => 1,
                'discount_rate' => 1,
                'tax_rate' => 1,
                'discount_amount' => ['$multiply' => ['$base_price', '$discount_rate']],
                'price_after_discount' => [
                    '$multiply' => ['$base_price', ['$subtract' => [1, '$discount_rate']]]
                ]
            ]],
            ['$project' => [
                'name' => 1,
                'base_price' => 1,
                'discount_rate' => 1,
                'tax_rate' => 1,
                'discount_amount' => 1,
                'price_after_discount' => 1,
                'tax_amount' => ['$multiply' => ['$price_after_discount', '$tax_rate']],
                'final_price' => [
                    '$multiply' => ['$price_after_discount', ['$add' => [1, '$tax_rate']]]
                ]
            ]]
        ];
        
        $result = $this->products->aggregate($pipeline)->toArray();
        
        if (empty($result)) {
            throw new Exception("产品不存在");
        }
        
        $price = $result[0];
        
        return [
            'product_name' => $price['name'],
            'base_price' => (string)$price['base_price'],
            'discount_rate' => (string)$price['discount_rate'],
            'discount_amount' => (string)$price['discount_amount'],
            'tax_rate' => (string)$price['tax_rate'],
            'tax_amount' => (string)$price['tax_amount'],
            'unit_price' => (string)$price['final_price'],
            'quantity' => $quantity,
            'total_price' => bcmul((string)$price['final_price'], (string)$quantity, 2)
        ];
    }
    
    public function calculateProfitMargin(string $productId): array
    {
        $pipeline = [
            ['$match' => ['_id' => $productId]],
            ['$project' => [
                'name' => 1,
                'base_price' => 1,
                'cost_price' => 1,
                'profit' => ['$subtract' => ['$base_price', '$cost_price']],
                'profit_margin' => [
                    '$cond' => [
                        ['$eq' => ['$base_price', 0]],
                        0,
                        ['$divide' => [
                            ['$subtract' => ['$base_price', '$cost_price']],
                            '$base_price'
                        ]]
                    ]
                ]
            ]]
        ];
        
        $result = $this->products->aggregate($pipeline)->toArray()[0];
        
        return [
            'product_name' => $result['name'],
            'selling_price' => (string)$result['base_price'],
            'cost_price' => (string)$result['cost_price'],
            'profit' => (string)$result['profit'],
            'profit_margin' => (string)$result['profit_margin']
        ];
    }
}

// 使用示例
$client = new MongoDB\Client("mongodb://localhost:27017");
$pm = new EcommercePriceManager($client);

$productId = $pm->createProduct([
    'name' => '高端笔记本电脑',
    'sku' => 'LAPTOP-001',
    'base_price' => '8999.00',
    'cost_price' => '6500.00',
    'discount_rate' => '0.10',
    'tax_rate' => '0.13'
]);

echo "产品ID: $productId\n";

$finalPrice = $pm->calculateFinalPrice($productId, 2);
echo "价格计算: " . json_encode($finalPrice, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE) . "\n";

$profit = $pm->calculateProfitMargin($productId);
echo "利润分析: " . json_encode($profit, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE) . "\n";

// 运行结果展示:
// 产品ID: 507f1f77bcf86cd799439011
// 价格计算: {
//     "product_name": "高端笔记本电脑",
//     "base_price": "8999.00",
//     "discount_rate": "0.10",
//     "discount_amount": "899.90",
//     "tax_rate": "0.13",
//     "tax_amount": "1052.877",
//     "unit_price": "9152.877",
//     "quantity": 2,
//     "total_price": "18305.75"
// }
?>

场景3:税务计算系统

php
<?php
use MongoDB\BSON\Decimal128;

class TaxCalculationSystem
{
    private MongoDB\Collection $taxRates;
    private MongoDB\Collection $taxRecords;
    
    public function __construct(MongoDB\Client $client)
    {
        $this->taxRates = $client->tax->rates;
        $this->taxRecords = $client->tax->records;
    }
    
    public function setupTaxRates(): void
    {
        $this->taxRates->insertMany([
            [
                'type' => 'VAT',
                'name' => '增值税',
                'rate' => new Decimal128('0.13'),
                'category' => 'standard'
            ],
            [
                'type' => 'VAT',
                'name' => '增值税(简易征收)',
                'rate' => new Decimal128('0.03'),
                'category' => 'simplified'
            ],
            [
                'type' => 'SURTAX',
                'name' => '城建税',
                'rate' => new Decimal128('0.07'),
                'base' => 'VAT',
                'category' => 'urban'
            ],
            [
                'type' => 'SURTAX',
                'name' => '教育费附加',
                'rate' => new Decimal128('0.03'),
                'base' => 'VAT',
                'category' => 'education'
            }
        ]);
    }
    
    public function calculateVAT(string $amount, string $taxCategory = 'standard'): array
    {
        $rate = $this->taxRates->findOne([
            'type' => 'VAT',
            'category' => $taxCategory
        ]);
        
        if (!$rate) {
            throw new Exception("税率不存在");
        }
        
        $pipeline = [
            ['$match' => ['type' => 'VAT', 'category' => $taxCategory]],
            ['$project' => [
                'rate' => 1,
                'name' => 1,
                'amount' => ['$literal' => new Decimal128($amount)],
                'tax_amount' => ['$multiply' => [['$literal' => new Decimal128($amount)], '$rate']],
                'amount_with_tax' => ['$multiply' => [['$literal' => new Decimal128($amount)], ['$add' => [1, '$rate']]]]
            ]]
        ];
        
        $result = $this->taxRates->aggregate($pipeline)->toArray()[0];
        
        return [
            'tax_name' => $result['name'],
            'rate' => (string)$result['rate'],
            'amount' => $amount,
            'tax_amount' => (string)$result['tax_amount'],
            'amount_with_tax' => (string)$result['amount_with_tax']
        ];
    }
    
    public function calculateSurtax(string $vatAmount): array
    {
        $surtaxes = $this->taxRates->find(['type' => 'SURTAX'])->toArray();
        
        $results = [];
        $totalSurtax = '0';
        
        foreach ($surtaxes as $surtax) {
            $surtaxAmount = bcmul($vatAmount, (string)$surtax['rate'], 34);
            
            $results[] = [
                'name' => $surtax['name'],
                'rate' => (string)$surtax['rate'],
                'amount' => $surtaxAmount
            ];
            
            $totalSurtax = bcadd($totalSurtax, $surtaxAmount, 34);
        }
        
        return [
            'surtaxes' => $results,
            'total_surtax' => $totalSurtax
        ];
    }
    
    public function calculateTotalTax(string $amount, string $taxCategory = 'standard'): array
    {
        $vat = $this->calculateVAT($amount, $taxCategory);
        $surtax = $this->calculateSurtax($vat['tax_amount']);
        $totalTax = bcadd($vat['tax_amount'], $surtax['total_surtax'], 34);
        
        $recordId = $this->taxRecords->insertOne([
            'amount' => new Decimal128($amount),
            'tax_category' => $taxCategory,
            'vat' => [
                'rate' => new Decimal128($vat['rate']),
                'amount' => new Decimal128($vat['tax_amount'])
            ],
            'surtax' => [
                'total' => new Decimal128($surtax['total_surtax']),
                'details' => $surtax['surtaxes']
            ],
            'total_tax' => new Decimal128($totalTax),
            'calculated_at' => new MongoDB\BSON\UTCDateTime()
        ])->getInsertedId();
        
        return [
            'record_id' => (string)$recordId,
            'base_amount' => $amount,
            'vat' => $vat,
            'surtax' => $surtax,
            'total_tax' => $totalTax
        ];
    }
}

// 使用示例
$client = new MongoDB\Client("mongodb://localhost:27017");
$tcs = new TaxCalculationSystem($client);

$tcs->setupTaxRates();

$vat = $tcs->calculateVAT('10000.00', 'standard');
echo "增值税计算: " . json_encode($vat, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE) . "\n";

$totalTax = $tcs->calculateTotalTax('10000.00', 'standard');
echo "总税额: " . json_encode($totalTax, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE) . "\n";

// 运行结果展示:
// 增值税计算: {
//     "tax_name": "增值税",
//     "rate": "0.13",
//     "amount": "10000.00",
//     "tax_amount": "1300.00",
//     "amount_with_tax": "11300.00"
// }
// 总税额: {
//     "record_id": "507f1f77bcf86cd799439011",
//     "base_amount": "10000.00",
//     "total_tax": "1456.00"
// }
?>

场景4:货币兑换系统

php
<?php
use MongoDB\BSON\Decimal128;

class CurrencyExchangeSystem
{
    private MongoDB\Collection $rates;
    private MongoDB\Collection $transactions;
    
    public function __construct(MongoDB\Client $client)
    {
        $this->rates = $client->currency->rates;
        $this->transactions = $client->currency->transactions;
    }
    
    public function updateRate(string $fromCurrency, string $toCurrency, string $rate): void
    {
        $this->rates->updateOne(
            ['from_currency' => $fromCurrency, 'to_currency' => $toCurrency],
            [
                '$set' => [
                    'rate' => new Decimal128($rate),
                    'updated_at' => new MongoDB\BSON\UTCDateTime()
                ]
            ],
            ['upsert' => true]
        );
        
        $inverseRate = bcdiv('1', $rate, 34);
        $this->rates->updateOne(
            ['from_currency' => $toCurrency, 'to_currency' => $fromCurrency],
            [
                '$set' => [
                    'rate' => new Decimal128($inverseRate),
                    'updated_at' => new MongoDB\BSON\UTCDateTime()
                ]
            ],
            ['upsert' => true]
        );
    }
    
    public function exchange(
        string $fromCurrency,
        string $toCurrency,
        string $amount,
        string $feeRate = '0.01'
    ): array {
        $rateDoc = $this->rates->findOne([
            'from_currency' => $fromCurrency,
            'to_currency' => $toCurrency
        ]);
        
        if (!$rateDoc) {
            throw new Exception("汇率不存在");
        }
        
        $grossAmount = bcmul($amount, (string)$rateDoc['rate'], 34);
        $feeAmount = bcmul($grossAmount, $feeRate, 34);
        $netAmount = bcsub($grossAmount, $feeAmount, 34);
        
        $transactionId = $this->transactions->insertOne([
            'from_currency' => $fromCurrency,
            'to_currency' => $toCurrency,
            'amount' => new Decimal128($amount),
            'rate' => $rateDoc['rate'],
            'gross_amount' => new Decimal128($grossAmount),
            'fee_rate' => new Decimal128($feeRate),
            'fee_amount' => new Decimal128($feeAmount),
            'net_amount' => new Decimal128($netAmount),
            'created_at' => new MongoDB\BSON\UTCDateTime()
        ])->getInsertedId();
        
        return [
            'transaction_id' => (string)$transactionId,
            'from_currency' => $fromCurrency,
            'to_currency' => $toCurrency,
            'amount' => $amount,
            'rate' => (string)$rateDoc['rate'],
            'gross_amount' => $grossAmount,
            'fee_rate' => $feeRate,
            'fee_amount' => $feeAmount,
            'net_amount' => $netAmount
        ];
    }
}

// 使用示例
$client = new MongoDB\Client("mongodb://localhost:27017");
$ces = new CurrencyExchangeSystem($client);

$ces->updateRate('USD', 'CNY', '7.2500');
$ces->updateRate('EUR', 'CNY', '7.8500');

$result = $ces->exchange('USD', 'CNY', '1000.00', '0.005');
echo "兑换结果: " . json_encode($result, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE) . "\n";

// 运行结果展示:
// 兑换结果: {
//     "transaction_id": "507f1f77bcf86cd799439011",
//     "from_currency": "USD",
//     "to_currency": "CNY",
//     "amount": "1000.00",
//     "rate": "7.2500",
//     "gross_amount": "7250.00",
//     "fee_rate": "0.005",
//     "fee_amount": "36.25",
//     "net_amount": "7213.75"
// }
?>

场景5:预算管理系统

php
<?php
use MongoDB\BSON\Decimal128;

class BudgetManagementSystem
{
    private MongoDB\Collection $budgets;
    private MongoDB\Collection $budgetItems;
    private MongoDB\Collection $actuals;
    
    public function __construct(MongoDB\Client $client)
    {
        $this->budgets = $client->budget->budgets;
        $this->budgetItems = $client->budget->budget_items;
        $this->actuals = $client->budget->actuals;
    }
    
    public function createBudget(array $data): string
    {
        $budgetId = (string)new MongoDB\BSON\ObjectId();
        
        $budget = [
            '_id' => $budgetId,
            'name' => $data['name'],
            'department' => $data['department'],
            'fiscal_year' => $data['fiscal_year'],
            'total_amount' => new Decimal128($data['total_amount']),
            'used_amount' => new Decimal128('0'),
            'remaining_amount' => new Decimal128($data['total_amount']),
            'status' => 'active',
            'created_at' => new MongoDB\BSON\UTCDateTime()
        ];
        
        $this->budgets->insertOne($budget);
        
        foreach ($data['items'] as $item) {
            $this->budgetItems->insertOne([
                'budget_id' => $budgetId,
                'name' => $item['name'],
                'category' => $item['category'],
                'allocated_amount' => new Decimal128($item['allocated_amount']),
                'used_amount' => new Decimal128('0'),
                'remaining_amount' => new Decimal128($item['allocated_amount']),
                'created_at' => new MongoDB\BSON\UTCDateTime()
            ]);
        }
        
        return $budgetId;
    }
    
    public function recordExpense(array $data): string
    {
        $expenseId = (string)new MongoDB\BSON\ObjectId();
        
        $budgetItem = $this->budgetItems->findOne([
            'budget_id' => $data['budget_id'],
            'category' => $data['category']
        ]);
        
        if (!$budgetItem) {
            throw new Exception("预算项目不存在");
        }
        
        $amount = new Decimal128($data['amount']);
        
        $checkPipeline = [
            ['$match' => ['_id' => $budgetItem['_id']]],
            ['$project' => [
                'remaining_amount' => 1,
                'sufficient' => ['$gte' => ['$remaining_amount', $amount]]
            ]]
        ];
        
        $check = $this->budgetItems->aggregate($checkPipeline)->toArray()[0];
        
        if (!$check['sufficient']) {
            throw new Exception("预算不足");
        }
        
        $this->actuals->insertOne([
            '_id' => $expenseId,
            'budget_id' => $data['budget_id'],
            'budget_item_id' => $budgetItem['_id'],
            'category' => $data['category'],
            'amount' => $amount,
            'description' => $data['description'],
            'expense_date' => new MongoDB\BSON\UTCDateTime(strtotime($data['expense_date']) * 1000),
            'created_at' => new MongoDB\BSON\UTCDateTime()
        ]);
        
        $this->budgetItems->updateOne(
            ['_id' => $budgetItem['_id']],
            [
                '$inc' => [
                    'used_amount' => $amount,
                    'remaining_amount' => new Decimal128('-' . $data['amount'])
                ]
            ]
        );
        
        $this->budgets->updateOne(
            ['_id' => $data['budget_id']],
            [
                '$inc' => [
                    'used_amount' => $amount,
                    'remaining_amount' => new Decimal128('-' . $data['amount'])
                ]
            ]
        );
        
        return $expenseId;
    }
    
    public function getBudgetStatus(string $budgetId): array
    {
        $budget = $this->budgets->findOne(['_id' => $budgetId]);
        
        if (!$budget) {
            throw new Exception("预算不存在");
        }
        
        $pipeline = [
            ['$match' => ['budget_id' => $budgetId]],
            ['$project' => [
                'name' => 1,
                'category' => 1,
                'allocated_amount' => 1,
                'used_amount' => 1,
                'remaining_amount' => 1,
                'usage_percentage' => [
                    '$cond' => [
                        ['$eq' => ['$allocated_amount', 0]],
                        0,
                        ['$multiply' => [['$divide' => ['$used_amount', '$allocated_amount']], 100]]
                    ]
                ]
            ]],
            ['$sort' => ['usage_percentage' => -1]]
        ];
        
        $itemStatus = [];
        foreach ($this->budgetItems->aggregate($pipeline) as $item) {
            $itemStatus[] = [
                'name' => $item['name'],
                'category' => $item['category'],
                'allocated' => (string)$item['allocated_amount'],
                'used' => (string)$item['used_amount'],
                'remaining' => (string)$item['remaining_amount'],
                'usage_percentage' => round((float)(string)$item['usage_percentage'], 2)
            ];
        }
        
        $totalUsagePipeline = [
            ['$match' => ['_id' => $budgetId]],
            ['$project' => [
                'total_amount' => 1,
                'used_amount' => 1,
                'remaining_amount' => 1,
                'usage_percentage' => ['$multiply' => [['$divide' => ['$used_amount', '$total_amount']], 100]]
            ]]
        ];
        
        $totalUsage = $this->budgets->aggregate($totalUsagePipeline)->toArray()[0];
        
        return [
            'budget_id' => $budgetId,
            'name' => $budget['name'],
            'department' => $budget['department'],
            'fiscal_year' => $budget['fiscal_year'],
            'total_amount' => (string)$budget['total_amount'],
            'used_amount' => (string)$budget['used_amount'],
            'remaining_amount' => (string)$budget['remaining_amount'],
            'usage_percentage' => round((float)(string)$totalUsage['usage_percentage'], 2),
            'status' => $budget['status'],
            'items' => $itemStatus
        ];
    }
}

// 使用示例
$client = new MongoDB\Client("mongodb://localhost:27017");
$bms = new BudgetManagementSystem($client);

$budgetId = $bms->createBudget([
    'name' => '2024年度研发预算',
    'department' => '研发部',
    'fiscal_year' => '2024',
    'total_amount' => '1000000.00',
    'items' => [
        ['name' => '设备采购', 'category' => 'equipment', 'allocated_amount' => '500000.00'],
        ['name' => '人员成本', 'category' => 'personnel', 'allocated_amount' => '300000.00'],
        ['name' => '研发材料', 'category' => 'materials', 'allocated_amount' => '200000.00']
    ]
]);

echo "预算ID: $budgetId\n";

$expenseId = $bms->recordExpense([
    'budget_id' => $budgetId,
    'category' => 'equipment',
    'amount' => '50000.00',
    'description' => '服务器采购',
    'expense_date' => '2024-01-15'
]);

echo "支出ID: $expenseId\n";

$status = $bms->getBudgetStatus($budgetId);
echo "预算状态: " . json_encode($status, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE) . "\n";

// 运行结果展示:
// 预算ID: 507f1f77bcf86cd799439011
// 支出ID: 507f1f77bcf86cd799439012
// 预算状态: {
//     "budget_id": "507f1f77bcf86cd799439011",
//     "name": "2024年度研发预算",
//     "total_amount": "1000000.00",
//     "used_amount": "50000.00",
//     "remaining_amount": "950000.00",
//     "usage_percentage": 5.0,
//     "items": [...]
// }
?>

企业级进阶应用场景

场景1:分布式事务处理

php
<?php
use MongoDB\BSON\Decimal128;

class DistributedTransactionManager
{
    private MongoDB\Client $client;
    private MongoDB\Collection $accounts;
    private MongoDB\Collection $transactions;
    
    public function __construct(MongoDB\Client $client)
    {
        $this->client = $client;
        $this->accounts = $client->banking->accounts;
        $this->transactions = $client->banking->transactions;
    }
    
    public function transferWithTransaction(
        string $fromAccount,
        string $toAccount,
        string $amount
    ): array {
        $decimalAmount = new Decimal128($amount);
        
        $session = $this->client->startSession();
        $session->startTransaction([
            'readConcern' => new MongoDB\Driver\ReadConcern('snapshot'),
            'writeConcern' => new MongoDB\Driver\WriteConcern(MongoDB\Driver\WriteConcern::MAJORITY)
        ]);
        
        try {
            $from = $this->accounts->findOne(
                ['account_id' => $fromAccount],
                ['session' => $session]
            );
            
            if (!$from) {
                throw new Exception("转出账户不存在");
            }
            
            $comparePipeline = [
                ['$match' => ['account_id' => $fromAccount]],
                ['$project' => [
                    'balance' => 1,
                    'sufficient' => ['$gte' => ['$balance', $decimalAmount]]
                ]]
            ];
            
            $compare = $this->accounts->aggregate(
                $comparePipeline,
                ['session' => $session]
            )->toArray()[0];
            
            if (!$compare['sufficient']) {
                throw new Exception("余额不足");
            }
            
            $this->accounts->updateOne(
                ['account_id' => $fromAccount],
                ['$inc' => ['balance' => new Decimal128('-' . $amount)]],
                ['session' => $session]
            );
            
            $this->accounts->updateOne(
                ['account_id' => $toAccount],
                ['$inc' => ['balance' => $decimalAmount]],
                ['session' => $session]
            );
            
            $transactionId = $this->transactions->insertOne([
                'type' => 'transfer',
                'from_account' => $fromAccount,
                'to_account' => $toAccount,
                'amount' => $decimalAmount,
                'status' => 'completed',
                'created_at' => new MongoDB\BSON\UTCDateTime()
            ], ['session' => $session])->getInsertedId();
            
            $session->commitTransaction();
            
            return [
                'transaction_id' => (string)$transactionId,
                'status' => 'completed',
                'amount' => $amount
            ];
            
        } catch (Exception $e) {
            $session->abortTransaction();
            throw $e;
        } finally {
            $session->endSession();
        }
    }
    
    public function reconcileTransactions(string $date): array
    {
        $startOfDay = new MongoDB\BSON\UTCDateTime(strtotime($date . ' 00:00:00') * 1000);
        $endOfDay = new MongoDB\BSON\UTCDateTime(strtotime($date . ' 23:59:59') * 1000);
        
        $pipeline = [
            ['$match' => [
                'created_at' => ['$gte' => $startOfDay, '$lte' => $endOfDay],
                'status' => 'completed'
            ]],
            ['$group' => [
                '_id' => null,
                'total_amount' => ['$sum' => '$amount'],
                'transaction_count' => ['$sum' => 1],
                'min_amount' => ['$min' => '$amount'],
                'max_amount' => ['$max' => '$amount']
            ]]
        ];
        
        $summary = $this->transactions->aggregate($pipeline)->toArray();
        
        $balanceCheck = $this->accounts->aggregate([
            ['$group' => [
                '_id' => null,
                'total_balance' => ['$sum' => '$balance']
            ]]
        ])->toArray();
        
        return [
            'date' => $date,
            'transactions' => $summary[0] ?? ['total_amount' => '0', 'transaction_count' => 0],
            'balance_summary' => $balanceCheck[0] ?? ['total_balance' => '0']
        ];
    }
}

// 使用示例
$client = new MongoDB\Client("mongodb://localhost:27017");
$dtm = new DistributedTransactionManager($client);

try {
    $result = $dtm->transferWithTransaction('ACC001', 'ACC002', '5000.00');
    echo "转账成功: " . json_encode($result) . "\n";
} catch (Exception $e) {
    echo "转账失败: " . $e->getMessage() . "\n";
}

$reconciliation = $dtm->reconcileTransactions('2024-01-15');
echo "对账结果: " . json_encode($reconciliation, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE) . "\n";

// 运行结果展示:
// 转账成功: {"transaction_id":"507f1f77bcf86cd799439011","status":"completed","amount":"5000.00"}
// 对账结果: {
//     "date": "2024-01-15",
//     "transactions": {
//         "total_amount": "5000.00",
//         "transaction_count": 1
//     },
//     "balance_summary": {
//         "total_balance": "100000.00"
//     }
// }
?>

场景2:会计系统

php
<?php
use MongoDB\BSON\Decimal128;

class AccountingSystem
{
    private MongoDB\Collection $accounts;
    private MongoDB\Collection $journalEntries;
    private MongoDB\Collection $ledgers;
    
    public function __construct(MongoDB\Client $client)
    {
        $this->accounts = $client->accounting->accounts;
        $this->journalEntries = $client->accounting->journal_entries;
        $this->ledgers = $client->accounting->ledgers;
    }
    
    public function createAccount(array $data): string
    {
        $accountId = 'ACC' . str_pad(
            $this->accounts->countDocuments([]) + 1,
            6,
            '0',
            STR_PAD_LEFT
        );
        
        $this->accounts->insertOne([
            'account_id' => $accountId,
            'name' => $data['name'],
            'type' => $data['type'],
            'category' => $data['category'],
            'balance' => new Decimal128('0'),
            'currency' => $data['currency'] ?? 'CNY',
            'is_active' => true,
            'created_at' => new MongoDB\BSON\UTCDateTime()
        ]);
        
        return $accountId;
    }
    
    public function createJournalEntry(array $entry): string
    {
        $entryId = 'JE' . date('Ymd') . str_pad(
            $this->journalEntries->countDocuments([
                'created_at' => ['$gte' => new MongoDB\BSON\UTCDateTime(strtotime('today') * 1000)]
            ]) + 1,
            4,
            '0',
            STR_PAD_LEFT
        );
        
        $totalDebit = '0';
        $totalCredit = '0';
        
        foreach ($entry['lines'] as $line) {
            if (isset($line['debit'])) {
                $totalDebit = bcadd($totalDebit, $line['debit'], 34);
            }
            if (isset($line['credit'])) {
                $totalCredit = bcadd($totalCredit, $line['credit'], 34);
            }
        }
        
        if (bccomp($totalDebit, $totalCredit, 34) !== 0) {
            throw new Exception("借贷不平衡: 借方 {$totalDebit}, 贷方 {$totalCredit}");
        }
        
        $journalEntry = [
            'entry_id' => $entryId,
            'date' => new MongoDB\BSON\UTCDateTime(strtotime($entry['date'] ?? 'now') * 1000),
            'description' => $entry['description'],
            'lines' => array_map(function($line) {
                return [
                    'account_id' => $line['account_id'],
                    'debit' => isset($line['debit']) ? new Decimal128($line['debit']) : new Decimal128('0'),
                    'credit' => isset($line['credit']) ? new Decimal128($line['credit']) : new Decimal128('0'),
                    'description' => $line['description'] ?? null
                ];
            }, $entry['lines']),
            'total_debit' => new Decimal128($totalDebit),
            'total_credit' => new Decimal128($totalCredit),
            'status' => 'posted',
            'created_at' => new MongoDB\BSON\UTCDateTime()
        ];
        
        $this->journalEntries->insertOne($journalEntry);
        
        foreach ($entry['lines'] as $line) {
            $this->updateAccountBalance(
                $line['account_id'],
                $line['debit'] ?? '0',
                $line['credit'] ?? '0'
            );
            
            $this->ledgers->insertOne([
                'entry_id' => $entryId,
                'account_id' => $line['account_id'],
                'date' => $journalEntry['date'],
                'debit' => isset($line['debit']) ? new Decimal128($line['debit']) : new Decimal128('0'),
                'credit' => isset($line['credit']) ? new Decimal128($line['credit']) : new Decimal128('0'),
                'description' => $line['description'] ?? $entry['description'],
                'created_at' => new MongoDB\BSON\UTCDateTime()
            ]);
        }
        
        return $entryId;
    }
    
    private function updateAccountBalance(string $accountId, string $debit, string $credit): void
    {
        $account = $this->accounts->findOne(['account_id' => $accountId]);
        
        if (!$account) {
            throw new Exception("账户不存在: $accountId");
        }
        
        $isDebitAccount = in_array($account['type'], ['asset', 'expense']);
        
        if ($isDebitAccount) {
            $newBalance = bcadd(bcsub((string)$account['balance'], $credit, 34), $debit, 34);
        } else {
            $newBalance = bcadd(bcsub((string)$account['balance'], $debit, 34), $credit, 34);
        }
        
        $this->accounts->updateOne(
            ['account_id' => $accountId],
            [
                '$set' => [
                    'balance' => new Decimal128($newBalance),
                    'updated_at' => new MongoDB\BSON\UTCDateTime()
                ]
            ]
        );
    }
    
    public function getTrialBalance(string $asOfDate): array
    {
        $pipeline = [
            ['$match' => ['is_active' => true]],
            ['$project' => [
                'account_id' => 1,
                'name' => 1,
                'type' => 1,
                'balance' => 1,
                'debit_balance' => [
                    '$cond' => [
                        ['$in' => ['$type', ['asset', 'expense']]],
                        ['$max' => ['$balance', 0]],
                        ['$max' => ['$multiply' => ['$balance', -1]], 0]
                    ]
                ],
                'credit_balance' => [
                    '$cond' => [
                        ['$in' => ['$type', ['liability', 'equity', 'revenue']]],
                        ['$max' => ['$balance', 0]],
                        ['$max' => ['$multiply' => ['$balance', -1]], 0]
                    ]
                ]
            ]],
            ['$group' => [
                '_id' => null,
                'accounts' => ['$push' => '$$ROOT'],
                'total_debit' => ['$sum' => '$debit_balance'],
                'total_credit' => ['$sum' => '$credit_balance']
            ]]
        ];
        
        $result = $this->accounts->aggregate($pipeline)->toArray();
        
        if (empty($result)) {
            return ['accounts' => [], 'total_debit' => '0', 'total_credit' => '0', 'is_balanced' => true];
        }
        
        $data = $result[0];
        
        return [
            'as_of_date' => $asOfDate,
            'accounts' => array_map(function($acc) {
                return [
                    'account_id' => $acc['account_id'],
                    'name' => $acc['name'],
                    'type' => $acc['type'],
                    'debit' => (string)$acc['debit_balance'],
                    'credit' => (string)$acc['credit_balance']
                ];
            }, $data['accounts']),
            'total_debit' => (string)$data['total_debit'],
            'total_credit' => (string)$data['total_credit'],
            'is_balanced' => bccomp((string)$data['total_debit'], (string)$data['total_credit'], 34) === 0
        ];
    }
}

// 使用示例
$client = new MongoDB\Client("mongodb://localhost:27017");
$accounting = new AccountingSystem($client);

$cashAccountId = $accounting->createAccount([
    'name' => '现金',
    'type' => 'asset',
    'category' => 'current_asset'
]);

$revenueAccountId = $accounting->createAccount([
    'name' => '主营业务收入',
    'type' => 'revenue',
    'category' => 'operating_revenue'
]);

$entryId = $accounting->createJournalEntry([
    'date' => '2024-01-15',
    'description' => '销售商品收入',
    'lines' => [
        ['account_id' => $cashAccountId, 'debit' => '10000.00', 'description' => '收到现金'],
        ['account_id' => $revenueAccountId, 'credit' => '10000.00', 'description' => '确认收入']
    ]
]);

echo "日记账分录ID: $entryId\n";

$trialBalance = $accounting->getTrialBalance('2024-01-15');
echo "试算平衡: " . json_encode($trialBalance, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE) . "\n";

// 运行结果展示:
// 日记账分录ID: JE202401150001
// 试算平衡: {
//     "as_of_date": "2024-01-15",
//     "accounts": [...],
//     "total_debit": "10000.00",
//     "total_credit": "10000.00",
//     "is_balanced": true
// }
?>

场景3:投资组合管理

php
<?php
use MongoDB\BSON\Decimal128;

class InvestmentPortfolioManager
{
    private MongoDB\Collection $portfolios;
    private MongoDB\Collection $holdings;
    private MongoDB\Collection $transactions;
    
    public function __construct(MongoDB\Client $client)
    {
        $this->portfolios = $client->investment->portfolios;
        $this->holdings = $client->investment->holdings;
        $this->transactions = $client->investment->transactions;
    }
    
    public function createPortfolio(array $data): string
    {
        $portfolioId = (string)new MongoDB\BSON\ObjectId();
        
        $this->portfolios->insertOne([
            '_id' => $portfolioId,
            'name' => $data['name'],
            'owner_id' => $data['owner_id'],
            'base_currency' => $data['base_currency'] ?? 'CNY',
            'total_value' => new Decimal128('0'),
            'cash_balance' => new Decimal128($data['initial_cash'] ?? '0'),
            'cost_basis' => new Decimal128('0'),
            'unrealized_gain' => new Decimal128('0'),
            'realized_gain' => new Decimal128('0'),
            'created_at' => new MongoDB\BSON\UTCDateTime()
        ]);
        
        return $portfolioId;
    }
    
    public function buyAsset(
        string $portfolioId,
        string $symbol,
        string $quantity,
        string $price,
        string $commission = '0'
    ): string {
        $quantityDecimal = new Decimal128($quantity);
        $priceDecimal = new Decimal128($price);
        $commissionDecimal = new Decimal128($commission);
        
        $totalCost = bcadd(bcmul($quantity, $price, 34), $commission, 34);
        
        $portfolio = $this->portfolios->findOne(['_id' => $portfolioId]);
        
        $checkPipeline = [
            ['$match' => ['_id' => $portfolioId]],
            ['$project' => [
                'cash_balance' => 1,
                'sufficient' => ['$gte' => ['$cash_balance', new Decimal128($totalCost)]]
            ]]
        ];
        
        $check = $this->portfolios->aggregate($checkPipeline)->toArray()[0];
        
        if (!$check['sufficient']) {
            throw new Exception("现金余额不足");
        }
        
        $transactionId = $this->transactions->insertOne([
            'portfolio_id' => $portfolioId,
            'type' => 'buy',
            'symbol' => $symbol,
            'quantity' => $quantityDecimal,
            'price' => $priceDecimal,
            'commission' => $commissionDecimal,
            'total_cost' => new Decimal128($totalCost),
            'executed_at' => new MongoDB\BSON\UTCDateTime(),
            'created_at' => new MongoDB\BSON\UTCDateTime()
        ])->getInsertedId();
        
        $existingHolding = $this->holdings->findOne([
            'portfolio_id' => $portfolioId,
            'symbol' => $symbol
        ]);
        
        if ($existingHolding) {
            $newQuantity = bcadd((string)$existingHolding['quantity'], $quantity, 34);
            $newCostBasis = bcadd((string)$existingHolding['cost_basis'], $totalCost, 34);
            $newAvgCost = bcdiv($newCostBasis, $newQuantity, 34);
            
            $this->holdings->updateOne(
                ['portfolio_id' => $portfolioId, 'symbol' => $symbol],
                [
                    '$set' => [
                        'quantity' => new Decimal128($newQuantity),
                        'cost_basis' => new Decimal128($newCostBasis),
                        'avg_cost' => new Decimal128($newAvgCost),
                        'updated_at' => new MongoDB\BSON\UTCDateTime()
                    ]
                ]
            );
        } else {
            $this->holdings->insertOne([
                'portfolio_id' => $portfolioId,
                'symbol' => $symbol,
                'quantity' => $quantityDecimal,
                'cost_basis' => new Decimal128($totalCost),
                'avg_cost' => $priceDecimal,
                'created_at' => new MongoDB\BSON\UTCDateTime()
            ]);
        }
        
        $this->portfolios->updateOne(
            ['_id' => $portfolioId],
            [
                '$inc' => [
                    'cash_balance' => new Decimal128('-' . $totalCost),
                    'cost_basis' => new Decimal128($totalCost)
                ]
            ]
        );
        
        return (string)$transactionId;
    }
    
    public function updateMarketValues(array $marketPrices): void
    {
        foreach ($marketPrices as $symbol => $price) {
            $this->holdings->updateMany(
                ['symbol' => $symbol],
                ['$set' => ['market_price' => new Decimal128($price), 'updated_at' => new MongoDB\BSON\UTCDateTime()]]
            );
        }
        
        $portfolios = $this->portfolios->find()->toArray();
        
        foreach ($portfolios as $portfolio) {
            $pipeline = [
                ['$match' => ['portfolio_id' => $portfolio['_id']]],
                ['$project' => [
                    'quantity' => 1,
                    'market_price' => 1,
                    'cost_basis' => 1,
                    'market_value' => ['$multiply' => ['$quantity', '$market_price']],
                    'unrealized_gain' => ['$subtract' => [['$multiply' => ['$quantity', '$market_price']], '$cost_basis']]
                ]],
                ['$group' => [
                    '_id' => null,
                    'total_market_value' => ['$sum' => '$market_value'],
                    'total_unrealized_gain' => ['$sum' => '$unrealized_gain']
                ]]
            ];
            
            $result = $this->holdings->aggregate($pipeline)->toArray();
            
            if (!empty($result)) {
                $this->portfolios->updateOne(
                    ['_id' => $portfolio['_id']],
                    [
                        '$set' => [
                            'total_value' => $result[0]['total_market_value'],
                            'unrealized_gain' => $result[0]['total_unrealized_gain'],
                            'updated_at' => new MongoDB\BSON\UTCDateTime()
                        ]
                    ]
                );
            }
        }
    }
    
    public function getPortfolioSummary(string $portfolioId): array
    {
        $portfolio = $this->portfolios->findOne(['_id' => $portfolioId]);
        
        if (!$portfolio) {
            throw new Exception("投资组合不存在");
        }
        
        $holdingsPipeline = [
            ['$match' => ['portfolio_id' => $portfolioId]],
            ['$project' => [
                'symbol' => 1,
                'quantity' => 1,
                'cost_basis' => 1,
                'avg_cost' => 1,
                'market_price' => 1,
                'market_value' => ['$multiply' => ['$quantity', '$market_price']],
                'unrealized_gain' => ['$subtract' => [['$multiply' => ['$quantity', '$market_price']], '$cost_basis']],
                'return_percentage' => [
                    '$cond' => [
                        ['$eq' => ['$cost_basis', 0],
                        0,
                        ['$multiply' => [['$divide' => [['$subtract' => [['$multiply' => ['$quantity', '$market_price']], '$cost_basis']], '$cost_basis']], 100]]
                    ]
                ]
            ]],
            ['$sort' => ['market_value' => -1]]
        ];
        
        $holdings = [];
        foreach ($this->holdings->aggregate($holdingsPipeline) as $holding) {
            $holdings[] = [
                'symbol' => $holding['symbol'],
                'quantity' => (string)$holding['quantity'],
                'avg_cost' => (string)$holding['avg_cost'],
                'market_price' => (string)($holding['market_price'] ?? new Decimal128('0')),
                'market_value' => (string)$holding['market_value'],
                'unrealized_gain' => (string)$holding['unrealized_gain'],
                'return_percentage' => round((float)(string)$holding['return_percentage'], 2)
            ];
        }
        
        $totalGain = bcadd((string)$portfolio['realized_gain'], (string)$portfolio['unrealized_gain'], 34);
        $totalReturnPercentage = bccomp((string)$portfolio['cost_basis'], '0', 34) === 0
            ? '0'
            : bcmul(bcdiv($totalGain, (string)$portfolio['cost_basis'], 34), '100', 2);
        
        return [
            'portfolio_id' => $portfolioId,
            'name' => $portfolio['name'],
            'base_currency' => $portfolio['base_currency'],
            'total_value' => (string)$portfolio['total_value'],
            'cash_balance' => (string)$portfolio['cash_balance'],
            'cost_basis' => (string)$portfolio['cost_basis'],
            'realized_gain' => (string)$portfolio['realized_gain'],
            'unrealized_gain' => (string)$portfolio['unrealized_gain'],
            'total_gain' => $totalGain,
            'total_return_percentage' => $totalReturnPercentage,
            'holdings' => $holdings
        ];
    }
}

// 使用示例
$client = new MongoDB\Client("mongodb://localhost:27017");
$ipm = new InvestmentPortfolioManager($client);

$portfolioId = $ipm->createPortfolio([
    'name' => '我的股票组合',
    'owner_id' => 'user001',
    'base_currency' => 'CNY',
    'initial_cash' => '1000000.00'
]);

echo "投资组合ID: $portfolioId\n";

$ipm->buyAsset($portfolioId, 'AAPL', '100', '150.00', '10.00');
$ipm->buyAsset($portfolioId, 'GOOGL', '50', '2800.00', '15.00');

$ipm->updateMarketValues(['AAPL' => '175.00', 'GOOGL' => '2900.00']);

$summary = $ipm->getPortfolioSummary($portfolioId);
echo "投资组合摘要: " . json_encode($summary, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE) . "\n";

// 运行结果展示:
// 投资组合ID: 507f1f77bcf86cd799439011
// 投资组合摘要: {
//     "portfolio_id": "507f1f77bcf86cd799439011",
//     "name": "我的股票组合",
//     "total_value": "32000.00",
//     "cash_balance": "549975.00",
//     "unrealized_gain": "16490.00",
//     "holdings": [...]
// }
?>

场景4:保险精算系统

php
<?php
use MongoDB\BSON\Decimal128;

class InsuranceActuarialSystem
{
    private MongoDB\Collection $policies;
    private MongoDB\Collection $claims;
    private MongoDB\Collection $reserves;
    
    public function __construct(MongoDB\Client $client)
    {
        $this->policies = $client->insurance->policies;
        $this->claims = $client->insurance->claims;
        $this->reserves = $client->insurance->reserves;
    }
    
    public function createPolicy(array $data): string
    {
        $policyId = 'POL' . date('Ymd') . str_pad(
            $this->policies->countDocuments([]) + 1,
            6,
            '0',
            STR_PAD_LEFT
        );
        
        $premium = $this->calculatePremium($data);
        
        $this->policies->insertOne([
            'policy_id' => $policyId,
            'policyholder' => $data['policyholder'],
            'type' => $data['type'],
            'coverage_amount' => new Decimal128($data['coverage_amount']),
            'premium' => new Decimal128($premium),
            'deductible' => new Decimal128($data['deductible'] ?? '0'),
            'start_date' => new MongoDB\BSON\UTCDateTime(strtotime($data['start_date']) * 1000),
            'end_date' => new MongoDB\BSON\UTCDateTime(strtotime($data['end_date']) * 1000),
            'status' => 'active',
            'created_at' => new MongoDB\BSON\UTCDateTime()
        ]);
        
        return $policyId;
    }
    
    private function calculatePremium(array $data): string
    {
        $baseRate = $data['base_rate'] ?? '0.01';
        $coverageAmount = $data['coverage_amount'];
        
        $premium = bcmul($coverageAmount, $baseRate, 34);
        
        if (isset($data['risk_factor'])) {
            $premium = bcmul($premium, $data['risk_factor'], 34);
        }
        
        return $premium;
    }
    
    public function fileClaim(array $data): string
    {
        $claimId = 'CLM' . date('Ymd') . str_pad(
            $this->claims->countDocuments([]) + 1,
            6,
            '0',
            STR_PAD_LEFT
        );
        
        $policy = $this->policies->findOne(['policy_id' => $data['policy_id']]);
        
        if (!$policy) {
            throw new Exception("保单不存在");
        }
        
        $claimAmount = new Decimal128($data['claim_amount']);
        $deductible = $policy['deductible'];
        
        $pipeline = [
            ['$match' => ['policy_id' => $data['policy_id']]],
            ['$project' => [
                'coverage_amount' => 1,
                'deductible' => 1,
                'claim_amount' => ['$literal' => $claimAmount],
                'claim_after_deductible' => ['$subtract' => [['$literal' => $claimAmount], '$deductible']],
                'is_within_coverage' => ['$lte' => [['$literal' => $claimAmount], '$coverage_amount']]
            ]]
        ];
        
        $calc = $this->policies->aggregate($pipeline)->toArray()[0];
        
        if (!$calc['is_within_coverage']) {
            throw new Exception("索赔金额超过保额");
        }
        
        $payoutAmount = bccomp((string)$calc['claim_after_deductible'], '0', 34) > 0
            ? (string)$calc['claim_after_deductible']
            : '0';
        
        $this->claims->insertOne([
            'claim_id' => $claimId,
            'policy_id' => $data['policy_id'],
            'claim_amount' => $claimAmount,
            'deductible_applied' => $deductible,
            'payout_amount' => new Decimal128($payoutAmount),
            'description' => $data['description'],
            'status' => 'pending',
            'filed_at' => new MongoDB\BSON\UTCDateTime(),
            'created_at' => new MongoDB\BSON\UTCDateTime()
        ]);
        
        return $claimId;
    }
    
    public function calculateReserves(string $date): array
    {
        $asOfDate = new MongoDB\BSON\UTCDateTime(strtotime($date) * 1000);
        
        $pipeline = [
            ['$match' => [
                'status' => 'active',
                'end_date' => ['$gte' => $asOfDate]
            ]],
            ['$project' => [
                'policy_id' => 1,
                'premium' => 1,
                'coverage_amount' => 1,
                'start_date' => 1,
                'end_date' => 1,
                'policy_duration_days' => [
                    '$divide' => [
                        ['$subtract' => ['$end_date', '$start_date']],
                        86400000
                    ]
                ],
                'elapsed_days' => [
                    '$divide' => [
                        ['$subtract' => [['$literal' => $asOfDate], '$start_date']],
                        86400000
                    ]
                ]
            ]],
            ['$project' => [
                'policy_id' => 1,
                'premium' => 1,
                'coverage_amount' => 1,
                'earned_premium' => [
                    '$multiply' => [
                        '$premium',
                        ['$divide' => ['$elapsed_days', '$policy_duration_days']]
                    ]
                ],
                'unearned_premium' => [
                    '$multiply' => [
                        '$premium',
                        ['$subtract' => [1, ['$divide' => ['$elapsed_days', '$policy_duration_days']]]]
                    ]
                ]
            ]],
            ['$group' => [
                '_id' => null,
                'total_premium' => ['$sum' => '$premium'],
                'total_earned_premium' => ['$sum' => '$earned_premium'],
                'total_unearned_premium' => ['$sum' => '$unearned_premium'],
                'total_coverage' => ['$sum' => '$coverage_amount'],
                'policy_count' => ['$sum' => 1]
            ]]
        ];
        
        $result = $this->policies->aggregate($pipeline)->toArray();
        
        if (empty($result)) {
            return [
                'date' => $date,
                'total_premium' => '0',
                'earned_premium' => '0',
                'unearned_premium' => '0',
                'total_coverage' => '0',
                'policy_count' => 0
            ];
        }
        
        $data = $result[0];
        
        $this->reserves->insertOne([
            'date' => $asOfDate,
            'total_premium' => $data['total_premium'],
            'earned_premium' => $data['total_earned_premium'],
            'unearned_premium' => $data['total_unearned_premium'],
            'total_coverage' => $data['total_coverage'],
            'policy_count' => $data['policy_count'],
            'calculated_at' => new MongoDB\BSON\UTCDateTime()
        ]);
        
        return [
            'date' => $date,
            'total_premium' => (string)$data['total_premium'],
            'earned_premium' => (string)$data['total_earned_premium'],
            'unearned_premium' => (string)$data['total_unearned_premium'],
            'total_coverage' => (string)$data['total_coverage'],
            'policy_count' => $data['policy_count']
        ];
    }
    
    public function getLossRatio(string $startDate, string $endDate): array
    {
        $start = new MongoDB\BSON\UTCDateTime(strtotime($startDate) * 1000);
        $end = new MongoDB\BSON\UTCDateTime(strtotime($endDate) * 1000);
        
        $claimsPipeline = [
            ['$match' => [
                'status' => 'approved',
                'filed_at' => ['$gte' => $start, '$lte' => $end]
            ]],
            ['$group' => [
                '_id' => null,
                'total_claims' => ['$sum' => '$claim_amount'],
                'total_payouts' => ['$sum' => '$payout_amount'],
                'claim_count' => ['$sum' => 1]
            ]]
        ];
        
        $claimsResult = $this->claims->aggregate($claimsPipeline)->toArray()[0] ?? [
            'total_claims' => new Decimal128('0'),
            'total_payouts' => new Decimal128('0'),
            'claim_count' => 0
        ];
        
        $premiumPipeline = [
            ['$match' => [
                'status' => 'active',
                'start_date' => ['$gte' => $start, '$lte' => $end]
            ]],
            ['$group' => [
                '_id' => null,
                'total_premium' => ['$sum' => '$premium']
            ]]
        ];
        
        $premiumResult = $this->policies->aggregate($premiumPipeline)->toArray()[0] ?? [
            'total_premium' => new Decimal128('0')
        ];
        
        $totalPremium = (string)$premiumResult['total_premium'];
        $totalPayouts = (string)$claimsResult['total_payouts'];
        
        $lossRatio = bccomp($totalPremium, '0', 34) === 0
            ? '0'
            : bcmul(bcdiv($totalPayouts, $totalPremium, 34), '100', 2);
        
        return [
            'period' => ['start' => $startDate, 'end' => $endDate],
            'total_premium' => $totalPremium,
            'total_claims' => (string)$claimsResult['total_claims'],
            'total_payouts' => $totalPayouts,
            'claim_count' => $claimsResult['claim_count'],
            'loss_ratio' => $lossRatio . '%'
        ];
    }
}

// 使用示例
$client = new MongoDB\Client("mongodb://localhost:27017");
$ias = new InsuranceActuarialSystem($client);

$policyId = $ias->createPolicy([
    'policyholder' => '张三',
    'type' => 'health',
    'coverage_amount' => '500000.00',
    'deductible' => '1000.00',
    'base_rate' => '0.02',
    'start_date' => '2024-01-01',
    'end_date' => '2024-12-31'
]);

echo "保单ID: $policyId\n";

$claimId = $ias->fileClaim([
    'policy_id' => $policyId,
    'claim_amount' => '50000.00',
    'description' => '住院医疗费用'
]);

echo "理赔ID: $claimId\n";

$reserves = $ias->calculateReserves('2024-06-30');
echo "准备金计算: " . json_encode($reserves, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE) . "\n";

$lossRatio = $ias->getLossRatio('2024-01-01', '2024-12-31');
echo "赔付率: " . json_encode($lossRatio, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE) . "\n";

// 运行结果展示:
// 保单ID: POL20240115000001
// 理赔ID: CLM20240115000001
// 准备金计算: {
//     "date": "2024-06-30",
//     "total_premium": "10000.00",
//     "earned_premium": "5000.00",
//     "unearned_premium": "5000.00",
//     "total_coverage": "500000.00",
//     "policy_count": 1
// }
// 赔付率: {
//     "period": {"start": "2024-01-01", "end": "2024-12-31"},
//     "total_premium": "10000.00",
//     "total_payouts": "49000.00",
//     "loss_ratio": "490.00%"
// }
?>

行业最佳实践

实践1:类型统一策略

php
<?php
use MongoDB\BSON\Decimal128;

class DecimalTypeStrategy
{
    public static function fromString(string $value): Decimal128
    {
        $trimmed = trim($value);
        
        if (!is_numeric($trimmed)) {
            throw new InvalidArgumentException("Invalid decimal value: $value");
        }
        
        return new Decimal128($trimmed);
    }
    
    public static function fromInt(int $value): Decimal128
    {
        return new Decimal128((string)$value);
    }
    
    public static function add(Decimal128 $a, Decimal128 $b): Decimal128
    {
        return new Decimal128(bcadd((string)$a, (string)$b, 34));
    }
    
    public static function subtract(Decimal128 $a, Decimal128 $b): Decimal128
    {
        return new Decimal128(bcsub((string)$a, (string)$b, 34));
    }
    
    public static function multiply(Decimal128 $a, Decimal128 $b): Decimal128
    {
        return new Decimal128(bcmul((string)$a, (string)$b, 34));
    }
    
    public static function divide(Decimal128 $a, Decimal128 $b, int $scale = 34): Decimal128
    {
        if (bccomp((string)$b, '0', 34) === 0) {
            throw new InvalidArgumentException("Division by zero");
        }
        
        return new Decimal128(bcdiv((string)$a, (string)$b, $scale));
    }
    
    public static function compare(Decimal128 $a, Decimal128 $b): int
    {
        return bccomp((string)$a, (string)$b, 34);
    }
    
    public static function round(Decimal128 $value, int $precision = 2): Decimal128
    {
        $str = (string)$value;
        $rounded = round((float)$str, $precision);
        
        return new Decimal128(number_format($rounded, $precision, '.', ''));
    }
    
    public static function abs(Decimal128 $value): Decimal128
    {
        $str = (string)$value;
        
        if (bccomp($str, '0', 34) < 0) {
            return new Decimal128(bcmul($str, '-1', 34));
        }
        
        return $value;
    }
}

// 使用示例
$a = DecimalTypeStrategy::fromString('100.50');
$b = DecimalTypeStrategy::fromString('50.25');

$sum = DecimalTypeStrategy::add($a, $b);
echo "加法: $a + $b = $sum\n";

$difference = DecimalTypeStrategy::subtract($a, $b);
echo "减法: $a - $b = $difference\n";

$product = DecimalTypeStrategy::multiply($a, $b);
echo "乘法: $a × $b = $product\n";

$quotient = DecimalTypeStrategy::divide($a, $b, 4);
echo "除法: $a ÷ $b = $quotient\n";

$comparison = DecimalTypeStrategy::compare($a, $b);
echo "比较: $a vs $b = $comparison\n";

// 运行结果展示:
// 加法: 100.50 + 50.25 = 150.75
// 减法: 100.50 - 50.25 = 50.25
// 乘法: 100.50 × 50.25 = 5050.1250
// 除法: 100.50 ÷ 50.25 = 2.0000
// 比较: 100.50 vs 50.25 = 1
?>

实践2:验证和清理

php
<?php
use MongoDB\BSON\Decimal128;

class DecimalValidator
{
    public static function validate(string $value, array $rules = []): array
    {
        $errors = [];
        
        if (!is_numeric(trim($value))) {
            $errors[] = "无效的数值格式";
            return ['valid' => false, 'errors' => $errors];
        }
        
        $decimal = new Decimal128(trim($value));
        $strValue = (string)$decimal;
        
        if (isset($rules['min']) && bccomp($strValue, $rules['min'], 34) < 0) {
            $errors[] = "值不能小于 {$rules['min']}";
        }
        
        if (isset($rules['max']) && bccomp($strValue, $rules['max'], 34) > 0) {
            $errors[] = "值不能大于 {$rules['max']}";
        }
        
        if (isset($rules['scale'])) {
            $parts = explode('.', $strValue);
            if (isset($parts[1]) && strlen($parts[1]) > $rules['scale']) {
                $errors[] = "小数位数不能超过 {$rules['scale']} 位";
            }
        }
        
        if (isset($rules['positive']) && $rules['positive'] && bccomp($strValue, '0', 34) <= 0) {
            $errors[] = "值必须为正数";
        }
        
        return [
            'valid' => empty($errors),
            'errors' => $errors,
            'value' => $strValue,
            'decimal' => $decimal
        ];
    }
    
    public static function sanitize(string $value, array $options = []): Decimal128
    {
        $config = array_merge([
            'trim' => true,
            'scale' => null
        ], $options);
        
        if ($config['trim']) {
            $value = trim($value);
        }
        
        $value = str_replace(',', '', $value);
        
        if (!is_numeric($value)) {
            throw new InvalidArgumentException("Invalid decimal value: $value");
        }
        
        if ($config['scale'] !== null) {
            $float = (float)$value;
            $rounded = round($float, $config['scale']);
            $value = number_format($rounded, $config['scale'], '.', '');
        }
        
        return new Decimal128($value);
    }
}

// 使用示例
$validation = DecimalValidator::validate('100.50', [
    'min' => '0',
    'max' => '1000',
    'scale' => 2,
    'positive' => true
]);

echo "验证结果: " . json_encode($validation, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE) . "\n";

$sanitized = DecimalValidator::sanitize(' 1,234.5678 ', ['scale' => 2]);
echo "清理结果: $sanitized\n";

// 运行结果展示:
// 验证结果: {
//     "valid": true,
//     "errors": [],
//     "value": "100.50",
//     "decimal": {}
// }
// 清理结果: 1234.57
?>

实践3:精度配置管理

php
<?php
use MongoDB\BSON\Decimal128;

class PrecisionConfig
{
    private static array $configs = [
        'currency' => ['scale' => 2, 'round_mode' => PHP_ROUND_HALF_UP],
        'tax_rate' => ['scale' => 4, 'round_mode' => PHP_ROUND_HALF_UP],
        'percentage' => ['scale' => 6, 'round_mode' => PHP_ROUND_HALF_UP],
        'scientific' => ['scale' => 34, 'round_mode' => PHP_ROUND_HALF_UP]
    ];
    
    public static function format(Decimal128 $value, string $type): string
    {
        $config = self::$configs[$type] ?? ['scale' => 2, 'round_mode' => PHP_ROUND_HALF_UP];
        
        $str = (string)$value;
        $float = (float)$str;
        $rounded = round($float, $config['scale'], $config['round_mode']);
        
        return number_format($rounded, $config['scale'], '.', '');
    }
    
    public static function create(string $value, string $type): Decimal128
    {
        $config = self::$configs[$type] ?? ['scale' => 2, 'round_mode' => PHP_ROUND_HALF_UP];
        
        $sanitized = self::sanitize($value, $config);
        
        return new Decimal128($sanitized);
    }
    
    private static function sanitize(string $value, array $config): string
    {
        $value = trim(str_replace(',', '', $value));
        
        if (!is_numeric($value)) {
            throw new InvalidArgumentException("Invalid decimal value: $value");
        }
        
        $float = (float)$value;
        $rounded = round($float, $config['scale'], $config['round_mode']);
        
        return number_format($rounded, $config['scale'], '.', '');
    }
}

// 使用示例
$price = PrecisionConfig::create('99.999', 'currency');
echo "货币格式: $price\n";

$rate = PrecisionConfig::create('0.12345678', 'tax_rate');
echo "税率格式: $rate\n";

$percentage = PrecisionConfig::create('0.123456789012', 'percentage');
echo "百分比格式: $percentage\n";

// 运行结果展示:
// 货币格式: 100.00
// 税率格式: 0.1235
// 百分比格式: 0.123457
?>

实践4:审计日志

php
<?php
use MongoDB\BSON\Decimal128;

class DecimalAuditLogger
{
    private MongoDB\Collection $auditLog;
    
    public function __construct(MongoDB\Client $client)
    {
        $this->auditLog = $client->audit->decimal_changes;
    }
    
    public function logChange(
        string $entityType,
        string $entityId,
        string $field,
        ?Decimal128 $oldValue,
        Decimal128 $newValue,
        string $reason,
        string $userId
    ): void {
        $this->auditLog->insertOne([
            'entity_type' => $entityType,
            'entity_id' => $entityId,
            'field' => $field,
            'old_value' => $oldValue,
            'new_value' => $newValue,
            'change_amount' => $oldValue
                ? new Decimal128(bcsub((string)$newValue, (string)$oldValue, 34))
                : $newValue,
            'reason' => $reason,
            'user_id' => $userId,
            'created_at' => new MongoDB\BSON\UTCDateTime()
        ]);
    }
    
    public function getChangeHistory(
        string $entityType,
        string $entityId,
        ?string $field = null
    ): array {
        $query = [
            'entity_type' => $entityType,
            'entity_id' => $entityId
        ];
        
        if ($field) {
            $query['field'] = $field;
        }
        
        $pipeline = [
            ['$match' => $query],
            ['$sort' => ['created_at' => -1]],
            ['$project' => [
                'field' => 1,
                'old_value' => 1,
                'new_value' => 1,
                'change_amount' => 1,
                'reason' => 1,
                'user_id' => 1,
                'created_at' => 1
            ]]
        ];
        
        $history = [];
        foreach ($this->auditLog->aggregate($pipeline) as $doc) {
            $history[] = [
                'field' => $doc['field'],
                'old_value' => $doc['old_value'] ? (string)$doc['old_value'] : null,
                'new_value' => (string)$doc['new_value'],
                'change_amount' => (string)$doc['change_amount'],
                'reason' => $doc['reason'],
                'user_id' => $doc['user_id'],
                'created_at' => $doc['created_at']->toDateTime()->format('Y-m-d H:i:s')
            ];
        }
        
        return $history;
    }
}

// 使用示例
$client = new MongoDB\Client("mongodb://localhost:27017");
$logger = new DecimalAuditLogger($client);

$logger->logChange(
    'account',
    'ACC001',
    'balance',
    new Decimal128('10000.00'),
    new Decimal128('9000.00'),
    '转账支出',
    'user001'
);

$history = $logger->getChangeHistory('account', 'ACC001', 'balance');
echo "变更历史: " . json_encode($history, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE) . "\n";

// 运行结果展示:
// 变更历史: [
//     {
//         "field": "balance",
//         "old_value": "10000.00",
//         "new_value": "9000.00",
//         "change_amount": "-1000.00",
//         "reason": "转账支出",
//         "user_id": "user001",
//         "created_at": "2024-01-15 10:30:00"
//     }
// ]
?>

常见问题答疑

问题1:Decimal128和Double应该如何选择?

:选择依据如下:

场景推荐类型原因
金融金额Decimal128需要精确计算,避免精度丢失
科学计算Decimal128需要高精度,34位有效数字
坐标数据Double精度足够,存储空间小
评分统计Double不需要绝对精确,性能更好
利率/税率Decimal128需要精确计算百分比
物理测量Double测量本身有误差,Double精度足够
php
<?php
use MongoDB\BSON\Decimal128;

// 金融场景必须用Decimal128
$price = new Decimal128('99.99');
$tax = new Decimal128('0.13');
$total = new Decimal128(bcmul((string)$price, bcadd('1', (string)$tax, 34), 34));
echo "含税价格: $total\n";

// 坐标场景可以用Double
$location = [
    'latitude' => 39.9042,
    'longitude' => 116.4074
];
?>

问题2:如何在PHP中进行Decimal128的数学运算?

:PHP中需要使用bcmath扩展进行精确运算:

php
<?php
use MongoDB\BSON\Decimal128;

class DecimalMath
{
    public static function add(Decimal128 $a, Decimal128 $b): Decimal128
    {
        return new Decimal128(bcadd((string)$a, (string)$b, 34));
    }
    
    public static function subtract(Decimal128 $a, Decimal128 $b): Decimal128
    {
        return new Decimal128(bcsub((string)$a, (string)$b, 34));
    }
    
    public static function multiply(Decimal128 $a, Decimal128 $b): Decimal128
    {
        return new Decimal128(bcmul((string)$a, (string)$b, 34));
    }
    
    public static function divide(Decimal128 $a, Decimal128 $b, int $scale = 34): Decimal128
    {
        return new Decimal128(bcdiv((string)$a, (string)$b, $scale));
    }
    
    public static function compare(Decimal128 $a, Decimal128 $b): int
    {
        return bccomp((string)$a, (string)$b, 34);
    }
}

// 使用示例
$a = new Decimal128('100.50');
$b = new Decimal128('3');

echo "加法: " . DecimalMath::add($a, $b) . "\n";
echo "减法: " . DecimalMath::subtract($a, $b) . "\n";
echo "乘法: " . DecimalMath::multiply($a, $b) . "\n";
echo "除法: " . DecimalMath::divide($a, $b, 4) . "\n";
echo "比较: " . DecimalMath::compare($a, $b) . "\n";

// 运行结果展示:
// 加法: 103.50
// 减法: 97.50
// 乘法: 301.50
// 除法: 33.5000
// 比较: 1
?>

问题3:Decimal128在聚合管道中如何使用?

:MongoDB聚合管道提供了丰富的数值运算操作符:

php
<?php
use MongoDB\BSON\Decimal128;

$client = new MongoDB\Client("mongodb://localhost:27017");
$collection = $client->testdb->products;

// 常用聚合操作符
$pipeline = [
    ['$project' => [
        'name' => 1,
        'price' => 1,
        'quantity' => 1,
        
        // 基本运算
        'total' => ['$multiply' => ['$price', '$quantity']],
        'discounted' => ['$multiply' => ['$price', ['$subtract' => [1, '$discountRate']]]],
        
        // 类型转换
        'price_as_double' => ['$toDouble' => '$price'],
        'price_as_string' => ['$toString' => '$price'],
        
        // 舍入
        'rounded' => ['$round' => ['$price', 2]],
        'floored' => ['$floor' => '$price'],
        'ceiled' => ['$ceil' => '$price'],
        
        // 比较
        'is_expensive' => ['$gte' => ['$price', new Decimal128('100')]]
    ]]
];

// 分组聚合
$groupPipeline = [
    ['$group' => [
        '_id' => '$category',
        'total_value' => ['$sum' => '$price'],
        'avg_price' => ['$avg' => '$price'],
        'min_price' => ['$min' => '$price'],
        'max_price' => ['$max' => '$price'],
        'count' => ['$sum' => 1]
    ]]
];
?>

问题4:如何处理Decimal128的序列化和反序列化?

:Decimal128的序列化处理:

php
<?php
use MongoDB\BSON\Decimal128;

class DecimalSerializer
{
    public static function toJson(Decimal128 $value): string
    {
        return json_encode(['$numberDecimal' => (string)$value]);
    }
    
    public static function fromJson(string $json): Decimal128
    {
        $data = json_decode($json, true);
        
        if (isset($data['$numberDecimal'])) {
            return new Decimal128($data['$numberDecimal']);
        }
        
        throw new InvalidArgumentException("Invalid Decimal128 JSON format");
    }
    
    public static function formatForDisplay(Decimal128 $value, string $format = 'currency'): string
    {
        $str = (string)$value;
        
        switch ($format) {
            case 'currency':
                return '¥' . number_format((float)$str, 2, '.', ',');
            case 'percent':
                return bcmul($str, '100', 2) . '%';
            case 'scientific':
                return sprintf('%.6E', (float)$str);
            default:
                return $str;
        }
    }
}

// 使用示例
$decimal = new Decimal128('12345.67');

echo "JSON: " . DecimalSerializer::toJson($decimal) . "\n";
echo "货币格式: " . DecimalSerializer::formatForDisplay($decimal, 'currency') . "\n";
echo "百分比格式: " . DecimalSerializer::formatForDisplay(new Decimal128('0.1234'), 'percent') . "\n";
echo "科学计数法: " . DecimalSerializer::formatForDisplay($decimal, 'scientific') . "\n";

// 运行结果展示:
// JSON: {"$numberDecimal":"12345.67"}
// 货币格式: ¥12,345.67
// 百分比格式: 12.34%
// 科学计数法: 1.234567E+4
?>

问题5:Decimal128在不同MongoDB版本中的兼容性如何?

:版本兼容性说明:

php
<?php
use MongoDB\BSON\Decimal128;

// MongoDB版本兼容性
// - MongoDB 3.4+: 支持Decimal128类型
// - MongoDB 3.2及以下: 不支持,会报错

// 驱动兼容性检查
function checkDecimal128Support(MongoDB\Client $client): array
{
    try {
        $admin = $client->admin;
        $result = $admin->command(['buildInfo' => 1]);
        $version = $result->toArray()[0]['version'];
        
        $versionParts = explode('.', $version);
        $majorVersion = (int)$versionParts[0];
        $minorVersion = (int)($versionParts[1] ?? 0);
        
        $supported = ($majorVersion > 3) || ($majorVersion === 3 && $minorVersion >= 4);
        
        return [
            'version' => $version,
            'supported' => $supported,
            'message' => $supported
                ? 'Decimal128 is supported'
                : 'Decimal128 requires MongoDB 3.4 or later'
        ];
    } catch (Exception $e) {
        return [
            'version' => 'unknown',
            'supported' => false,
            'message' => 'Unable to check version: ' . $e->getMessage()
        ];
    }
}

// 使用示例
$client = new MongoDB\Client("mongodb://localhost:27017");

$compatibility = checkDecimal128Support($client);
echo "兼容性检查: " . json_encode($compatibility, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE) . "\n";

// 运行结果展示:
// 兼容性检查: {
//     "version": "5.0.0",
//     "supported": true,
//     "message": "Decimal128 is supported"
// }
?>

问题6:如何优化Decimal128字段的查询性能?

:性能优化策略:

php
<?php
use MongoDB\BSON\Decimal128;

class DecimalQueryOptimizer
{
    private MongoDB\Collection $collection;
    
    public function __construct(MongoDB\Collection $collection)
    {
        $this->collection = $collection;
    }
    
    public function createOptimalIndexes(): void
    {
        // 1. 单字段索引
        $this->collection->createIndex(['price' => 1]);
        
        // 2. 复合索引
        $this->collection->createIndex(['category' => 1, 'price' => -1]);
        
        // 3. 部分索引
        $this->collection->createIndex(
            ['price' => 1],
            [
                'partialFilterExpression' => [
                    'price' => ['$gte' => new Decimal128('0')]
                ]
            ]
        );
    }
    
    public function optimizedRangeQuery(string $field, string $min, string $max): array
    {
        return $this->collection->find([
            $field => [
                '$gte' => new Decimal128($min),
                '$lte' => new Decimal128($max)
            ]
        ], [
            'sort' => [$field => 1],
            'projection' => [$field => 1, 'name' => 1]
        ])->toArray();
    }
}

// 使用示例
$client = new MongoDB\Client("mongodb://localhost:27017");
$collection = $client->testdb->products;

$optimizer = new DecimalQueryOptimizer($collection);
$optimizer->createOptimalIndexes();

$results = $optimizer->optimizedRangeQuery('price', '100', '500');
echo "查询结果数量: " . count($results) . "\n";

// 运行结果展示:
// 查询结果数量: 10
?>

实战练习

练习1:创建金融账户系统

任务描述:创建一个简单的金融账户系统,实现账户创建、存款、取款和转账功能。

php
<?php
use MongoDB\BSON\Decimal128;

class SimpleBankAccount
{
    private MongoDB\Collection $accounts;
    
    public function __construct(MongoDB\Client $client)
    {
        $this->accounts = $client->bank->accounts;
    }
    
    public function createAccount(string $accountId, string $initialBalance = '0'): void
    {
        $this->accounts->insertOne([
            'account_id' => $accountId,
            'balance' => new Decimal128($initialBalance),
            'created_at' => new MongoDB\BSON\UTCDateTime()
        ]);
    }
    
    public function deposit(string $accountId, string $amount): void
    {
        $this->accounts->updateOne(
            ['account_id' => $accountId],
            ['$inc' => ['balance' => new Decimal128($amount)]]
        );
    }
    
    public function withdraw(string $accountId, string $amount): bool
    {
        $account = $this->accounts->findOne(['account_id' => $accountId]);
        
        if (bccomp((string)$account['balance'], $amount, 34) < 0) {
            return false;
        }
        
        $this->accounts->updateOne(
            ['account_id' => $accountId],
            ['$inc' => ['balance' => new Decimal128('-' . $amount)]]
        );
        
        return true;
    }
    
    public function transfer(string $fromId, string $toId, string $amount): bool
    {
        if (!$this->withdraw($fromId, $amount)) {
            return false;
        }
        
        $this->deposit($toId, $amount);
        return true;
    }
    
    public function getBalance(string $accountId): string
    {
        $account = $this->accounts->findOne(['account_id' => $accountId]);
        return $account ? (string)$account['balance'] : '0';
    }
}

// 测试代码
$client = new MongoDB\Client("mongodb://localhost:27017");
$bank = new SimpleBankAccount($client);

$bank->createAccount('A001', '1000.00');
$bank->createAccount('A002', '500.00');

$bank->deposit('A001', '200.00');
echo "A001余额: " . $bank->getBalance('A001') . "\n";

$bank->transfer('A001', 'A002', '300.00');
echo "A001余额: " . $bank->getBalance('A001') . "\n";
echo "A002余额: " . $bank->getBalance('A002') . "\n";

// 运行结果展示:
// A001余额: 1200.00
// A001余额: 900.00
// A002余额: 800.00
?>

练习2:实现价格计算器

任务描述:创建一个价格计算器,支持折扣、税费和总价计算。

php
<?php
use MongoDB\BSON\Decimal128;

class PriceCalculator
{
    private MongoDB\Collection $products;
    
    public function __construct(MongoDB\Client $client)
    {
        $this->products = $client->shop->products;
    }
    
    public function addProduct(string $name, string $price, string $discountRate = '0', string $taxRate = '0.13'): void
    {
        $this->products->insertOne([
            'name' => $name,
            'base_price' => new Decimal128($price),
            'discount_rate' => new Decimal128($discountRate),
            'tax_rate' => new Decimal128($taxRate)
        ]);
    }
    
    public function calculateFinalPrice(string $name, int $quantity = 1): array
    {
        $pipeline = [
            ['$match' => ['name' => $name]],
            ['$project' => [
                'name' => 1,
                'base_price' => 1,
                'discount_rate' => 1,
                'tax_rate' => 1,
                'discounted_price' => [
                    '$multiply' => ['$base_price', ['$subtract' => [1, '$discount_rate']]]
                ]
            ]],
            ['$project' => [
                'name' => 1,
                'base_price' => 1,
                'discount_rate' => 1,
                'tax_rate' => 1,
                'discounted_price' => 1,
                'tax_amount' => ['$multiply' => ['$discounted_price', '$tax_rate']],
                'final_unit_price' => [
                    '$multiply' => ['$discounted_price', ['$add' => [1, '$tax_rate']]]
                ]
            ]]
        ];
        
        $result = $this->products->aggregate($pipeline)->toArray()[0];
        
        $finalUnitPrice = (string)$result['final_unit_price'];
        $totalPrice = bcmul($finalUnitPrice, (string)$quantity, 2);
        
        return [
            'name' => $name,
            'base_price' => (string)$result['base_price'],
            'discount_rate' => (string)$result['discount_rate'],
            'discounted_price' => (string)$result['discounted_price'],
            'tax_rate' => (string)$result['tax_rate'],
            'tax_amount' => (string)$result['tax_amount'],
            'final_unit_price' => $finalUnitPrice,
            'quantity' => $quantity,
            'total_price' => $totalPrice
        ];
    }
}

// 测试代码
$client = new MongoDB\Client("mongodb://localhost:27017");
$calc = new PriceCalculator($client);

$calc->addProduct('笔记本电脑', '5999.00', '0.10', '0.13');
$calc->addProduct('手机', '2999.00', '0.05', '0.13');

$price1 = $calc->calculateFinalPrice('笔记本电脑', 2);
echo "价格计算: " . json_encode($price1, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE) . "\n";

$price2 = $calc->calculateFinalPrice('手机', 1);
echo "价格计算: " . json_encode($price2, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE) . "\n";

// 运行结果展示:
// 价格计算: {
//     "name": "笔记本电脑",
//     "base_price": "5999.00",
//     "discount_rate": "0.10",
//     "discounted_price": "5399.10",
//     "tax_rate": "0.13",
//     "tax_amount": "701.883",
//     "final_unit_price": "6100.983",
//     "quantity": 2,
//     "total_price": "12201.97"
// }
?>

练习3:实现预算跟踪系统

任务描述:创建一个预算跟踪系统,支持预算创建、支出记录和预算状态查询。

php
<?php
use MongoDB\BSON\Decimal128;

class BudgetTracker
{
    private MongoDB\Collection $budgets;
    private MongoDB\Collection $expenses;
    
    public function __construct(MongoDB\Client $client)
    {
        $this->budgets = $client->budget->budgets;
        $this->expenses = $client->budget->expenses;
    }
    
    public function createBudget(string $name, string $totalAmount): string
    {
        $budgetId = (string)new MongoDB\BSON\ObjectId();
        
        $this->budgets->insertOne([
            '_id' => $budgetId,
            'name' => $name,
            'total_amount' => new Decimal128($totalAmount),
            'spent_amount' => new Decimal128('0'),
            'remaining_amount' => new Decimal128($totalAmount),
            'created_at' => new MongoDB\BSON\UTCDateTime()
        ]);
        
        return $budgetId;
    }
    
    public function addExpense(string $budgetId, string $category, string $amount, string $description): string
    {
        $budget = $this->budgets->findOne(['_id' => $budgetId]);
        
        if (!$budget) {
            throw new Exception("预算不存在");
        }
        
        if (bccomp((string)$budget['remaining_amount'], $amount, 34) < 0) {
            throw new Exception("预算不足");
        }
        
        $expenseId = (string)new MongoDB\BSON\ObjectId();
        
        $this->expenses->insertOne([
            '_id' => $expenseId,
            'budget_id' => $budgetId,
            'category' => $category,
            'amount' => new Decimal128($amount),
            'description' => $description,
            'created_at' => new MongoDB\BSON\UTCDateTime()
        ]);
        
        $this->budgets->updateOne(
            ['_id' => $budgetId],
            [
                '$inc' => [
                    'spent_amount' => new Decimal128($amount),
                    'remaining_amount' => new Decimal128('-' . $amount)
                ]
            ]
        );
        
        return $expenseId;
    }
    
    public function getBudgetStatus(string $budgetId): array
    {
        $budget = $this->budgets->findOne(['_id' => $budgetId]);
        
        if (!$budget) {
            throw new Exception("预算不存在");
        }
        
        $usagePercentage = bcmul(
            bcdiv((string)$budget['spent_amount'], (string)$budget['total_amount'], 34),
            '100',
            2
        );
        
        return [
            'name' => $budget['name'],
            'total_amount' => (string)$budget['total_amount'],
            'spent_amount' => (string)$budget['spent_amount'],
            'remaining_amount' => (string)$budget['remaining_amount'],
            'usage_percentage' => $usagePercentage . '%'
        ];
    }
    
    public function getExpensesByCategory(string $budgetId): array
    {
        $pipeline = [
            ['$match' => ['budget_id' => $budgetId]],
            ['$group' => [
                '_id' => '$category',
                'total' => ['$sum' => '$amount'],
                'count' => ['$sum' => 1]
            ]],
            ['$sort' => ['total' => -1]]
        ];
        
        $result = [];
        foreach ($this->expenses->aggregate($pipeline) as $doc) {
            $result[] = [
                'category' => $doc['_id'],
                'total' => (string)$doc['total'],
                'count' => $doc['count']
            ];
        }
        
        return $result;
    }
}

// 测试代码
$client = new MongoDB\Client("mongodb://localhost:27017");
$tracker = new BudgetTracker($client);

$budgetId = $tracker->createBudget('月度生活预算', '5000.00');
echo "预算ID: $budgetId\n";

$tracker->addExpense($budgetId, '餐饮', '800.00', '日常餐饮');
$tracker->addExpense($budgetId, '交通', '300.00', '地铁公交');
$tracker->addExpense($budgetId, '餐饮', '200.00', '聚餐');

$status = $tracker->getBudgetStatus($budgetId);
echo "预算状态: " . json_encode($status, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE) . "\n";

$byCategory = $tracker->getExpensesByCategory($budgetId);
echo "分类统计: " . json_encode($byCategory, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE) . "\n";

// 运行结果展示:
// 预算ID: 507f1f77bcf86cd799439011
// 预算状态: {
//     "name": "月度生活预算",
//     "total_amount": "5000.00",
//     "spent_amount": "1300.00",
//     "remaining_amount": "3700.00",
//     "usage_percentage": "26.00%"
// }
// 分类统计: [
//     {"category": "餐饮", "total": "1000.00", "count": 2},
//     {"category": "交通", "total": "300.00", "count": 1}
// ]
?>

知识点总结

核心要点

  1. 存储机制

    • Decimal128使用16字节存储
    • 支持34位有效数字精度
    • 遵循IEEE 754-2008标准
  2. PHP操作

    • 使用MongoDB\BSON\Decimal128类
    • 始终从字符串创建,避免精度问题
    • 使用bcmath扩展进行数学运算
  3. 精度特点

    • 十进制浮点数表示
    • 无二进制精度问题
    • 适合金融和会计计算
  4. 聚合运算

    • 支持$add、$subtract、$multiply、$divide
    • 支持$round、$floor、$ceil
    • 支持$toDecimal、$toDouble类型转换

易错点回顾

  1. 从浮点数创建

    • 错误:new Decimal128((string)0.1)
    • 正确:new Decimal128('0.1')
  2. 超出精度范围

    • 最多34位有效数字
    • 超出部分会被自动舍入
  3. 混合类型比较

    • 使用$expr或$toDecimal统一类型
    • 避免直接比较不同类型
  4. 聚合运算精度

    • Decimal128保持精度
    • 使用$divide计算平均值
  5. 无效字符串格式

    • 必须是有效的数值字符串
    • 使用trim清理空白字符
  6. 索引和排序

    • 统一字段类型
    • 使用Decimal128对象查询

拓展参考资料

官方文档

学习路径

  1. 基础阶段

    • 掌握Decimal128的基本创建和查询
    • 理解精度问题和解决方案
    • 学会使用bcmath扩展
  2. 进阶阶段

    • 掌握聚合管道中的数值运算
    • 学习类型转换和比较
    • 理解索引优化策略
  3. 高级阶段

    • 分布式事务处理
    • 金融系统设计
    • 性能优化和监控

相关知识点

  • bcmath扩展:PHP高精度数学运算库
  • IEEE 754标准:浮点数运算标准
  • 金融计算:精确金额处理最佳实践
  • MongoDB聚合:数据处理和分析
  • 事务处理:ACID特性和分布式事务