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
233 lines
7.7 KiB
PHP
233 lines
7.7 KiB
PHP
<?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‰ = 3(FLOOR,不是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
|
||
|
||
// 但向上传递时使用 level3Expected(200),而非 level3Actual(0)
|
||
$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 = 1(FLOOR)
|
||
['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');
|
||
}
|
||
}
|