Files
huangjingfen/pro_v3.5.1_副本/tests/hjf/PointsCalculationTest.php
apple 434aa8c69d 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

233 lines
7.7 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<?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');
}
}