Files
huangjingfen/pro_v3.5.1_副本/tests/hjf/PointsCalculationTest.php

233 lines
7.7 KiB
PHP
Raw Normal View History

feat(fsgx): 完成全部24项开发任务 Phase1-7 Phase1 后端核心: - 新增 fsgx_v1.sql 迁移脚本(is_queue_goods/frozen_points/available_points/no_assess) - SystemConfigServices 返佣设置扩展(周期人数/分档比例/范围/时机) - StoreOrderCreateServices 周期循环佣金计算 - StoreOrderTakeServices 佣金发放后同步冻结积分 - StoreProductServices/StoreProduct 保存 is_queue_goods Phase2 后端接口: - GET /api/hjf/brokerage/progress 佣金周期进度 - GET /api/hjf/assets/overview 资产总览 - HjfPointsServices 每日 frozen_points 0.4‰ 释放定时任务 - PUT /adminapi/hjf/member/{uid}/no_assess 不考核接口 - GET /adminapi/hjf/points/release_log 积分日志接口 Phase3 前端清理: - hjfCustom.js 路由精简(仅保留 points/log) - hjfQueue.js/hjfMember.js API 清理/重定向至 CRMEB 原生接口 - pages.json 公排→推荐佣金/佣金记录/佣金规则 Phase4-5 前端改造: - queue/status.vue 推荐佣金进度页整体重写 - 商品详情/订单确认/支付结果页文案与逻辑改造 - 个人中心/资产页/引导页/规则页文案改造 - HjfQueueProgress/HjfRefundNotice/HjfAssetCard 组件改造 - 推广中心嵌入佣金进度摘要 - hjfMockData.js 全量更新(公排字段→佣金字段) Phase6 Admin 增强: - 用户列表新增 frozen_points/available_points 列及不考核操作按钮 - hjfPoints.js USE_MOCK=false 对接真实积分日志接口 Phase7 配置文档: - docs/fsgx-phase7-config-checklist.md 后台配置与全链路验收清单 Made-with: Cursor
2026-03-23 22:32:19 +08:00
<?php
declare(strict_types=1);
namespace tests\hjf;
use PHPUnit\Framework\TestCase;
/**
* 积分计算单元测试
*
* 覆盖点:
* 1. 级差计算:直推 / 伞下 / 多层级差公式正确性
* 2. 每日释放精度bcmath 计算 frozen × rate / 1000
* 3. 边界值frozen_points=0 不释放rate=0 不释放
* 4. 伞下业绩分离:云店级别下级业绩不计入上级
*
* Class PointsCalculationTest
* @package tests\hjf
*/
class PointsCalculationTest extends TestCase
{
// -----------------------------------------------------------------------
// 每日释放精度bcmath
// -----------------------------------------------------------------------
/**
* @test
* 每日释放1,000,000 frozen × 4 = 4,000(无浮点误差)
*/
public function testDailyReleaseCalculationPrecision(): void
{
$frozen = '1000000';
$rate = '4';
// Formula: FLOOR(frozen × rate / 1000)
$releaseAmount = (int)bcdiv(bcmul($frozen, $rate), '1000');
$this->assertEquals(4000, $releaseAmount, '每日释放精度测试1000000×4‰=4000');
}
/**
* @test
* 每日释放:小数点精度 —— 999 × 4 = 3FLOOR不是3.996
*/
public function testDailyReleaseFloorRounding(): void
{
$frozen = '999';
$rate = '4';
$releaseAmount = (int)bcdiv(bcmul($frozen, $rate), '1000');
$this->assertEquals(3, $releaseAmount, '每日释放应使用 FLOOR 取整999×4‰=3');
}
/**
* @test
* frozen_points = 0 时不释放
*/
public function testDailyReleaseZeroFrozenPoints(): void
{
$frozen = '0';
$rate = '4';
$releaseAmount = (int)bcdiv(bcmul($frozen, $rate), '1000');
$this->assertEquals(0, $releaseAmount, 'frozen_points=0 时释放量应为 0');
}
/**
* @test
* rate = 0 时不释放(防止除零配置错误)
*/
public function testDailyReleaseZeroRate(): void
{
$frozen = '100000';
$rate = '0';
$releaseAmount = (int)bcdiv(bcmul($frozen, $rate), '1000');
$this->assertEquals(0, $releaseAmount, 'rate=0 时释放量应为 0');
}
/**
* @test
* 浮点陷阱验证PHP 原生浮点 vs bcmath 结果对比
*/
public function testBcmathVsNativeFloat(): void
{
$frozen = 1234567;
$rate = 4;
// Native float (may have precision errors)
$nativeResult = (int)($frozen * $rate / 1000);
// bcmath (exact)
$bcResult = (int)bcdiv(bcmul((string)$frozen, (string)$rate), '1000');
// Both should equal 4938 for this input
$expected = (int)floor($frozen * $rate / 1000);
$this->assertEquals($expected, $bcResult, 'bcmath 结果与预期一致');
$this->assertEquals($bcResult, $nativeResult, '此用例下两者结果应相同(验证无浮点偏差)');
}
// -----------------------------------------------------------------------
// 级差计算
// -----------------------------------------------------------------------
/**
* @test
* 直推奖励level=1 (创客) 应获得 500 积分
*/
public function testDirectRewardForLevel1(): void
{
// Default config: level1 direct = 500
$defaults = [0 => 0, 1 => 500, 2 => 800, 3 => 1000, 4 => 1300];
$level = 1;
$reward = $defaults[$level];
$this->assertEquals(500, $reward, '创客直推奖励默认应为 500');
}
/**
* @test
* 级差计算:上级 level=2(云店,800) 下级已获得 level=1(创客,500) = 差额 300
*/
public function testUmbrellaRewardCascadeLevel2(): void
{
$directDefaults = [0 => 0, 1 => 500, 2 => 800, 3 => 1000, 4 => 1300];
$umbrellaDefaults = [0 => 0, 1 => 0, 2 => 300, 3 => 200, 4 => 300];
$upperLevel = 2; // 云店
$lowerReward = $directDefaults[1]; // 下级(创客)已获得的直推奖励 500
$umbrellaReward = $umbrellaDefaults[$upperLevel]; // 云店伞下奖励 300
$actual = max(0, $umbrellaReward - $lowerReward);
// 级差300 - 500 = -200 → max(0, -200) = 0
$this->assertEquals(0, $actual, '级差为负数时实发为 0云店伞下300 < 创客直推500');
}
/**
* @test
* 级差计算:上级 level=3(服务商, umbrella=200) 下级已获得 umbrella(云店=300) max(0, 200-300) = 0
*/
public function testUmbrellaRewardCascadeLevel3(): void
{
$umbrellaDefaults = [0 => 0, 1 => 0, 2 => 300, 3 => 200, 4 => 300];
$upperLevel = 3;
$lowerReward = $umbrellaDefaults[2]; // 下级云店获得的伞下奖励 300
$actual = max(0, $umbrellaDefaults[$upperLevel] - $lowerReward);
$this->assertEquals(0, $actual, '服务商伞下200 < 云店伞下300级差为0');
}
/**
* @test
* 级差计算:上级 level=4(分公司, umbrella=300) 下级已获得 umbrella(服务商=200) = 100
*/
public function testUmbrellaRewardCascadeLevel4(): void
{
$umbrellaDefaults = [0 => 0, 1 => 0, 2 => 300, 3 => 200, 4 => 300];
$upperLevel = 4;
$lowerReward = $umbrellaDefaults[3]; // 服务商已获得 200
$actual = max(0, $umbrellaDefaults[$upperLevel] - $lowerReward);
$this->assertEquals(100, $actual, '分公司伞下300 - 服务商伞下200 = 级差100');
}
/**
* @test
* 传递参数验证propagateReward 向上传递的是"应得额"而非"实发额"
*
* 场景level3 实发 0(因级差为负),但向上传递的 lowerReward 仍为其"应得额"200
*/
public function testCascadePropagatesExpectedNotActual(): void
{
$umbrellaDefaults = [0 => 0, 1 => 0, 2 => 300, 3 => 200, 4 => 300];
// Level3 应得 = 200, 下级已得 300 → 实发 = max(0, 200-300) = 0
$level3Expected = $umbrellaDefaults[3]; // 200
$level3Actual = max(0, $level3Expected - $umbrellaDefaults[2]); // max(0, 200-300) = 0
// 但向上传递时使用 level3Expected200而非 level3Actual0
$level4Expected = $umbrellaDefaults[4]; // 300
$level4Actual = max(0, $level4Expected - $level3Expected); // max(0, 300-200) = 100
$this->assertEquals(0, $level3Actual, 'Level3 实发为 0级差为负');
$this->assertEquals(100, $level4Actual, 'Level4 基于 level3 应得额200计算实发 100');
}
// -----------------------------------------------------------------------
// 每日释放批量处理
// -----------------------------------------------------------------------
/**
* @test
* 批量释放:多用户同时处理,每人独立计算(统计结果正确)
*/
public function testBatchReleaseAggregation(): void
{
$rate = 4;
$users = [
['uid' => 1, 'frozen_points' => 10000],
['uid' => 2, 'frozen_points' => 5000],
['uid' => 3, 'frozen_points' => 250], // 250×4/1000 = 1FLOOR
['uid' => 4, 'frozen_points' => 0], // skip
];
$totalReleased = 0;
$processed = 0;
foreach ($users as $user) {
$frozen = $user['frozen_points'];
$releaseAmount = (int)bcdiv(bcmul((string)$frozen, (string)$rate), '1000');
if ($releaseAmount <= 0) {
continue;
}
$totalReleased += $releaseAmount;
$processed++;
}
$this->assertEquals(3, $processed, '应处理3个用户frozen>0且释放额>0');
$this->assertEquals(61, $totalReleased, '总释放积分应为 40+20+1=61');
}
}