feat(partner): 完善马豆实时动态汇率与精度自适应,支持交易数据清理与汇率重置
这个提交包含在:
@@ -0,0 +1,375 @@
|
||||
<?php
|
||||
|
||||
namespace tests\partner;
|
||||
|
||||
@ini_set('session.use_cookies', '0');
|
||||
define('APP_PATH', __DIR__ . '/../../application/');
|
||||
define('ROOT_PATH', dirname(realpath(APP_PATH)) . '/');
|
||||
require_once __DIR__ . '/../../thinkphp/base.php';
|
||||
require_once __DIR__ . '/../../thinkphp/helper.php';
|
||||
if (is_file(ROOT_PATH . '.env')) {
|
||||
$env = parse_ini_file(ROOT_PATH . '.env', true);
|
||||
foreach ($env as $key => $val) {
|
||||
$name = 'PHP_' . strtoupper($key);
|
||||
if (is_array($val)) {
|
||||
foreach ($val as $k => $v) {
|
||||
$item = $name . '_' . strtoupper($k);
|
||||
putenv("$item=$v");
|
||||
}
|
||||
} else {
|
||||
putenv("$name=$val");
|
||||
}
|
||||
}
|
||||
}
|
||||
require_once __DIR__ . '/../../application/common.php';
|
||||
|
||||
\think\Config::set(\think\Config::load(APP_PATH . 'config.php'));
|
||||
\think\Config::set(include APP_PATH . 'database.php', 'database');
|
||||
\think\Loader::addNamespace('app', APP_PATH);
|
||||
require_once __DIR__ . '/../../addons/shopro/helper/helper.php';
|
||||
|
||||
// 初始化 CLI 下的 Session 并注入合法的 Admin 账号及 Auth 登录状态
|
||||
\think\Session::init();
|
||||
$auth = \app\admin\library\Auth::instance();
|
||||
$refAuth = new \ReflectionClass($auth);
|
||||
if ($refAuth->hasProperty('logined')) {
|
||||
$p = $refAuth->getProperty('logined');
|
||||
$p->setAccessible(true);
|
||||
$p->setValue($auth, true);
|
||||
}
|
||||
if ($refAuth->hasProperty('rules')) {
|
||||
$p = $refAuth->getProperty('rules');
|
||||
$p->setAccessible(true);
|
||||
$p->setValue($auth, ['*']);
|
||||
}
|
||||
|
||||
use app\admin\controller\shopro\user\User;
|
||||
use app\admin\model\shopro\user\User as UserModel;
|
||||
use app\admin\model\shopro\commission\Agent as AgentModel;
|
||||
use app\admin\model\shopro\partner\Reward as RewardModel;
|
||||
use think\Request;
|
||||
use think\Db;
|
||||
|
||||
class TestUserController extends User
|
||||
{
|
||||
public function _initialize()
|
||||
{
|
||||
$this->model = new UserModel();
|
||||
}
|
||||
}
|
||||
|
||||
class MultiAssetRechargeTest
|
||||
{
|
||||
private $passed = 0;
|
||||
private $failed = 0;
|
||||
private $testUserId = 0;
|
||||
|
||||
public function run()
|
||||
{
|
||||
echo "========================================================\n";
|
||||
echo " 会员多资产(余额/积分/马豆/兑换券)充值业务测试 \n";
|
||||
echo "========================================================\n\n";
|
||||
|
||||
$this->setUp();
|
||||
|
||||
try {
|
||||
$this->testInvalidAssetType();
|
||||
$this->testMoneyRechargeAndDeduct();
|
||||
$this->testMoneyOverdrawn();
|
||||
$this->testScoreRechargeAndDeduct();
|
||||
$this->testScoreOverdrawn();
|
||||
$this->testMadowRechargeAndDeduct();
|
||||
$this->testMadowOverdrawn();
|
||||
$this->testVoucherRechargeAndDeduct();
|
||||
$this->testVoucherOverdrawn();
|
||||
} finally {
|
||||
$this->tearDown();
|
||||
}
|
||||
|
||||
echo "\n--------------------------------------------------------\n";
|
||||
echo "测试执行结果: 通过 {$this->passed} 项, 失败 {$this->failed} 项\n";
|
||||
echo "========================================================\n";
|
||||
return $this->failed === 0 ? 0 : 1;
|
||||
}
|
||||
|
||||
private function assert($condition, $testName, $detail = '')
|
||||
{
|
||||
if ($condition) {
|
||||
echo " [PASS] {$testName}\n";
|
||||
$this->passed++;
|
||||
} else {
|
||||
echo " [FAIL] {$testName} - {$detail}\n";
|
||||
$this->failed++;
|
||||
}
|
||||
}
|
||||
|
||||
private function setUp()
|
||||
{
|
||||
// 查找或创建一个测试用户
|
||||
$user = UserModel::where('username', 'test_recharge_user')->find();
|
||||
if (!$user) {
|
||||
$now = time();
|
||||
$id = Db::name('user')->insertGetId([
|
||||
'username' => 'test_recharge_user',
|
||||
'nickname' => '充值测试员',
|
||||
'password' => md5('123456'),
|
||||
'salt' => '123456',
|
||||
'mobile' => '13988889999',
|
||||
'money' => 100.00,
|
||||
'score' => 500,
|
||||
'status' => 'normal',
|
||||
'createtime' => $now,
|
||||
'updatetime' => $now,
|
||||
]);
|
||||
$this->testUserId = $id;
|
||||
} else {
|
||||
$this->testUserId = $user->id;
|
||||
Db::name('user')->where('id', $this->testUserId)->update([
|
||||
'money' => 100.00,
|
||||
'score' => 500,
|
||||
]);
|
||||
}
|
||||
|
||||
// 初始化 agent 记录
|
||||
$agent = Db::name('shopro_commission_agent')->where('user_id', $this->testUserId)->find();
|
||||
if (!$agent) {
|
||||
Db::name('shopro_commission_agent')->insert([
|
||||
'user_id' => $this->testUserId,
|
||||
'level' => 1,
|
||||
'status' => 'normal',
|
||||
'madow' => '10.00000000',
|
||||
'voucher_total' => '200.00',
|
||||
'voucher_released' => '200.00',
|
||||
'voucher_used' => '50.00',
|
||||
'createtime' => time(),
|
||||
'updatetime' => time(),
|
||||
]);
|
||||
} else {
|
||||
Db::name('shopro_commission_agent')->where('user_id', $this->testUserId)->update([
|
||||
'madow' => '10.00000000',
|
||||
'voucher_total' => '200.00',
|
||||
'voucher_released' => '200.00',
|
||||
'voucher_used' => '50.00',
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
private function tearDown()
|
||||
{
|
||||
if ($this->testUserId) {
|
||||
try {
|
||||
Db::name('user')->where('id', $this->testUserId)->delete();
|
||||
Db::name('shopro_commission_agent')->where('user_id', $this->testUserId)->delete();
|
||||
Db::name('shopro_partner_reward')->where('user_id', $this->testUserId)->delete();
|
||||
Db::name('shopro_user_wallet_log')->where('user_id', $this->testUserId)->delete();
|
||||
Db::name('user_money_log')->where('user_id', $this->testUserId)->delete();
|
||||
Db::name('user_score_log')->where('user_id', $this->testUserId)->delete();
|
||||
} catch (\Exception $e) {
|
||||
// 忽略非关键日志表删除错误
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private function invokeRecharge($params)
|
||||
{
|
||||
$controller = new TestUserController();
|
||||
$request = Request::create('shopro/user/user/recharge', 'POST', $params, [], [], [
|
||||
'HTTP_X_REQUESTED_WITH' => 'xmlhttprequest',
|
||||
'HTTP_ACCEPT' => 'application/json',
|
||||
]);
|
||||
|
||||
// 绑定 request 到 控制器
|
||||
$ref = new \ReflectionClass($controller);
|
||||
$reqProp = $ref->getProperty('request');
|
||||
$reqProp->setAccessible(true);
|
||||
$reqProp->setValue($controller, $request);
|
||||
|
||||
// 设置 Request 全局单例
|
||||
$refInst = new \ReflectionClass(Request::class);
|
||||
$instProp = $refInst->getProperty('instance');
|
||||
$instProp->setAccessible(true);
|
||||
$instProp->setValue(null, $request);
|
||||
|
||||
try {
|
||||
$controller->recharge();
|
||||
} catch (\think\exception\HttpResponseException $e) {
|
||||
$data = $e->getResponse()->getData();
|
||||
if (is_string($data)) {
|
||||
$decoded = json_decode($data, true);
|
||||
if (is_array($decoded)) {
|
||||
return $decoded;
|
||||
}
|
||||
}
|
||||
if (!is_array($data)) {
|
||||
echo "[DEBUG RAW DATA] " . substr((string)$data, 0, 200) . "\n";
|
||||
}
|
||||
return is_array($data) ? $data : ['code' => 0, 'msg' => (string) $data];
|
||||
}
|
||||
return ['code' => 0, 'msg' => '未知响应'];
|
||||
}
|
||||
|
||||
public function testInvalidAssetType()
|
||||
{
|
||||
$res = $this->invokeRecharge([
|
||||
'id' => $this->testUserId,
|
||||
'type' => 'invalid_coin',
|
||||
'amount' => 10,
|
||||
'memo' => '测试非法资产类型',
|
||||
]);
|
||||
$this->assert($res['code'] === 0 && strpos($res['msg'], '不支持') !== false, '非法资产类型校验拦截', $res['msg'] ?? '');
|
||||
}
|
||||
|
||||
public function testMoneyRechargeAndDeduct()
|
||||
{
|
||||
// 充值 50 元
|
||||
$res1 = $this->invokeRecharge([
|
||||
'id' => $this->testUserId,
|
||||
'type' => 'money',
|
||||
'amount' => 50,
|
||||
'memo' => '充值50元',
|
||||
]);
|
||||
$u1 = UserModel::get($this->testUserId);
|
||||
$this->assert($res1['code'] === 1 && (float)$u1->money == 150.00, '余额充值成功且到账', "当前余额: {$u1->money}");
|
||||
|
||||
// 扣除 30 元
|
||||
$res2 = $this->invokeRecharge([
|
||||
'id' => $this->testUserId,
|
||||
'type' => 'money',
|
||||
'amount' => -30,
|
||||
'memo' => '扣除30元',
|
||||
]);
|
||||
$u2 = UserModel::get($this->testUserId);
|
||||
$this->assert($res2['code'] === 1 && (float)$u2->money == 120.00, '余额扣除成功且准确', "当前余额: {$u2->money}");
|
||||
}
|
||||
|
||||
public function testMoneyOverdrawn()
|
||||
{
|
||||
// 尝试扣除超过当前余额 (120元)
|
||||
$res = $this->invokeRecharge([
|
||||
'id' => $this->testUserId,
|
||||
'type' => 'money',
|
||||
'amount' => -200,
|
||||
'memo' => '超额扣除余额',
|
||||
]);
|
||||
$this->assert($res['code'] === 0 && strpos($res['msg'], '不足') !== false, '余额不足拦截超扣', $res['msg'] ?? '');
|
||||
}
|
||||
|
||||
public function testScoreRechargeAndDeduct()
|
||||
{
|
||||
// 充值 100 积分
|
||||
$res1 = $this->invokeRecharge([
|
||||
'id' => $this->testUserId,
|
||||
'type' => 'score',
|
||||
'amount' => 100,
|
||||
'memo' => '充值100积分',
|
||||
]);
|
||||
$u1 = UserModel::get($this->testUserId);
|
||||
$this->assert($res1['code'] === 1 && $u1->score == 600, '积分充值成功且到账', "当前积分: {$u1->score}");
|
||||
|
||||
// 扣除 200 积分
|
||||
$res2 = $this->invokeRecharge([
|
||||
'id' => $this->testUserId,
|
||||
'type' => 'score',
|
||||
'amount' => -200,
|
||||
'memo' => '扣除200积分',
|
||||
]);
|
||||
$u2 = UserModel::get($this->testUserId);
|
||||
$this->assert($res2['code'] === 1 && $u2->score == 400, '积分扣除成功且准确', "当前积分: {$u2->score}");
|
||||
}
|
||||
|
||||
public function testScoreOverdrawn()
|
||||
{
|
||||
// 尝试超额扣除积分
|
||||
$res = $this->invokeRecharge([
|
||||
'id' => $this->testUserId,
|
||||
'type' => 'score',
|
||||
'amount' => -1000,
|
||||
'memo' => '超额扣除积分',
|
||||
]);
|
||||
$this->assert($res['code'] === 0 && strpos($res['msg'], '不足') !== false, '积分不足拦截超扣', $res['msg'] ?? '');
|
||||
}
|
||||
|
||||
public function testMadowRechargeAndDeduct()
|
||||
{
|
||||
// 充值 5.12345678 颗马豆
|
||||
$res1 = $this->invokeRecharge([
|
||||
'id' => $this->testUserId,
|
||||
'type' => 'madow',
|
||||
'amount' => '5.12345678',
|
||||
'memo' => '充值马豆',
|
||||
]);
|
||||
$agent1 = AgentModel::where('user_id', $this->testUserId)->find();
|
||||
$this->assert($res1['code'] === 1 && $agent1->madow == '15.12345678', '马豆8位高精度充值到账', "当前马豆: {$agent1->madow}");
|
||||
|
||||
// 扣除 3.00000000 颗马豆
|
||||
$res2 = $this->invokeRecharge([
|
||||
'id' => $this->testUserId,
|
||||
'type' => 'madow',
|
||||
'amount' => '-3.00000000',
|
||||
'memo' => '扣除马豆',
|
||||
]);
|
||||
$agent2 = AgentModel::where('user_id', $this->testUserId)->find();
|
||||
$this->assert($res2['code'] === 1 && $agent2->madow == '12.12345678', '马豆高精度扣除成功', "当前马豆: {$agent2->madow}");
|
||||
|
||||
// 验证流水表记录
|
||||
$log = RewardModel::where('user_id', $this->testUserId)->where('reward_type', 'admin_madow')->order('id', 'desc')->find();
|
||||
$this->assert($log && $log->status == 1, '马豆充值写入收益流水明细');
|
||||
}
|
||||
|
||||
public function testMadowOverdrawn()
|
||||
{
|
||||
// 尝试超额扣除马豆
|
||||
$res = $this->invokeRecharge([
|
||||
'id' => $this->testUserId,
|
||||
'type' => 'madow',
|
||||
'amount' => '-99.00000000',
|
||||
'memo' => '超额扣除马豆',
|
||||
]);
|
||||
$this->assert($res['code'] === 0 && strpos($res['msg'], '不足') !== false, '马豆持仓不足拦截超扣', $res['msg'] ?? '');
|
||||
}
|
||||
|
||||
public function testVoucherRechargeAndDeduct()
|
||||
{
|
||||
// 当前 released=200, used=50, available=150
|
||||
// 充值 100 元兑换券
|
||||
$res1 = $this->invokeRecharge([
|
||||
'id' => $this->testUserId,
|
||||
'type' => 'voucher',
|
||||
'amount' => 100,
|
||||
'memo' => '充值兑换券',
|
||||
]);
|
||||
$agent1 = AgentModel::where('user_id', $this->testUserId)->find();
|
||||
$avail1 = $agent1->voucher_released - $agent1->voucher_used;
|
||||
$this->assert($res1['code'] === 1 && $avail1 == 250.00, '兑换券充值成功可用额度增加', "可用券: {$avail1}");
|
||||
|
||||
// 扣除 80 元兑换券
|
||||
$res2 = $this->invokeRecharge([
|
||||
'id' => $this->testUserId,
|
||||
'type' => 'voucher',
|
||||
'amount' => -80,
|
||||
'memo' => '扣除兑换券',
|
||||
]);
|
||||
$agent2 = AgentModel::where('user_id', $this->testUserId)->find();
|
||||
$avail2 = $agent2->voucher_released - $agent2->voucher_used;
|
||||
$this->assert($res2['code'] === 1 && $avail2 == 170.00, '兑换券扣除成功可用额度准确减少', "可用券: {$avail2}");
|
||||
|
||||
// 验证流水表记录
|
||||
$log = RewardModel::where('user_id', $this->testUserId)->where('reward_type', 'admin_voucher')->order('id', 'desc')->find();
|
||||
$this->assert($log && $log->amount == -80.00, '兑换券变动写入收益流水明细');
|
||||
}
|
||||
|
||||
public function testVoucherOverdrawn()
|
||||
{
|
||||
// 尝试超额扣除兑换券 (当前可用 170)
|
||||
$res = $this->invokeRecharge([
|
||||
'id' => $this->testUserId,
|
||||
'type' => 'voucher',
|
||||
'amount' => -300,
|
||||
'memo' => '超额扣除兑换券',
|
||||
]);
|
||||
$this->assert($res['code'] === 0 && strpos($res['msg'], '不足') !== false, '兑换券可用额度不足拦截超扣', $res['msg'] ?? '');
|
||||
}
|
||||
}
|
||||
|
||||
$test = new MultiAssetRechargeTest();
|
||||
exit($test->run());
|
||||
111
tests/partner/User10Verify.php
普通文件
111
tests/partner/User10Verify.php
普通文件
@@ -0,0 +1,111 @@
|
||||
<?php
|
||||
|
||||
namespace tests\partner;
|
||||
|
||||
define('APP_PATH', __DIR__ . '/../../application/');
|
||||
define('ROOT_PATH', dirname(realpath(APP_PATH)) . '/');
|
||||
require_once __DIR__ . '/../../thinkphp/base.php';
|
||||
require_once __DIR__ . '/../../thinkphp/helper.php';
|
||||
|
||||
if (is_file(ROOT_PATH . '.env')) {
|
||||
$env = parse_ini_file(ROOT_PATH . '.env', true);
|
||||
foreach ($env as $key => $val) {
|
||||
$name = 'PHP_' . strtoupper($key);
|
||||
if (is_array($val)) {
|
||||
foreach ($val as $k => $v) {
|
||||
$item = $name . '_' . strtoupper($k);
|
||||
putenv("$item=$v");
|
||||
}
|
||||
} else {
|
||||
putenv("$name=$val");
|
||||
}
|
||||
}
|
||||
}
|
||||
require_once __DIR__ . '/../../application/common.php';
|
||||
|
||||
\think\Config::set(\think\Config::load(APP_PATH . 'config.php'));
|
||||
\think\Config::set(include APP_PATH . 'database.php', 'database');
|
||||
\think\Loader::addNamespace('app', APP_PATH);
|
||||
\think\Loader::addNamespace('addons', ROOT_PATH . 'addons/');
|
||||
require_once __DIR__ . '/../../addons/shopro/helper/helper.php';
|
||||
|
||||
use think\Request;
|
||||
use think\Db;
|
||||
|
||||
class DirectTestController extends \app\admin\controller\shopro\user\User
|
||||
{
|
||||
public function _initialize()
|
||||
{
|
||||
$this->model = new \app\admin\model\shopro\user\User();
|
||||
}
|
||||
}
|
||||
|
||||
function doRecharge($type, $amount, $memo)
|
||||
{
|
||||
$ctrl = new DirectTestController();
|
||||
$req = Request::create('shopro/user/user/recharge', 'POST', [
|
||||
'id' => 10,
|
||||
'type' => $type,
|
||||
'amount' => $amount,
|
||||
'memo' => $memo
|
||||
], [], [], ['HTTP_X_REQUESTED_WITH' => 'xmlhttprequest', 'HTTP_ACCEPT' => 'application/json']);
|
||||
|
||||
$ref = new \ReflectionClass($ctrl);
|
||||
$p = $ref->getProperty('request');
|
||||
$p->setAccessible(true);
|
||||
$p->setValue($ctrl, $req);
|
||||
|
||||
$refInst = new \ReflectionClass(Request::class);
|
||||
$inst = $refInst->getProperty('instance');
|
||||
$inst->setAccessible(true);
|
||||
$inst->setValue(null, $req);
|
||||
|
||||
try {
|
||||
$ctrl->recharge();
|
||||
} catch (\think\exception\HttpResponseException $e) {
|
||||
$d = $e->getResponse()->getData();
|
||||
return is_array($d) ? $d : json_decode($d, true);
|
||||
}
|
||||
return ['code' => 0, 'msg' => '未知'];
|
||||
}
|
||||
|
||||
echo "=== 开始对会员 10 (尚善) 真实链路充值验证 ===\n";
|
||||
|
||||
// 1. 马豆充值 +8 颗
|
||||
$r1 = doRecharge('madow', '8.00000000', '真实环境测试充值马豆');
|
||||
$agent1 = Db::name('shopro_commission_agent')->where('user_id', 10)->find();
|
||||
echo "1. 充值马豆+8结果: " . json_encode($r1, JSON_UNESCAPED_UNICODE) . " 当前持仓: " . ($agent1['madow'] ?? '0') . "\n";
|
||||
|
||||
// 2. 马豆扣除 -8 颗
|
||||
$r2 = doRecharge('madow', '-8.00000000', '真实环境测试扣除马豆');
|
||||
$agent2 = Db::name('shopro_commission_agent')->where('user_id', 10)->find();
|
||||
echo "2. 扣除马豆-8结果: " . json_encode($r2, JSON_UNESCAPED_UNICODE) . " 恢复持仓: " . ($agent2['madow'] ?? '0') . "\n";
|
||||
|
||||
// 3. 兑换券充值 +20 元
|
||||
$r3 = doRecharge('voucher', '20.00', '真实环境测试充值兑换券');
|
||||
$agent3 = Db::name('shopro_commission_agent')->where('user_id', 10)->find();
|
||||
echo "3. 充值兑换券+20结果: " . json_encode($r3, JSON_UNESCAPED_UNICODE) . " released: " . ($agent3['voucher_released'] ?? '0') . " used: " . ($agent3['voucher_used'] ?? '0') . "\n";
|
||||
|
||||
// 4. 兑换券扣除 -20 元
|
||||
$r4 = doRecharge('voucher', '-20.00', '真实环境测试扣除兑换券');
|
||||
$agent4 = Db::name('shopro_commission_agent')->where('user_id', 10)->find();
|
||||
echo "4. 扣除兑换券-20结果: " . json_encode($r4, JSON_UNESCAPED_UNICODE) . " released: " . ($agent4['voucher_released'] ?? '0') . " used: " . ($agent4['voucher_used'] ?? '0') . "\n";
|
||||
|
||||
// 5. 积分充值 +10
|
||||
$r5 = doRecharge('score', 10, '真实环境测试充值积分');
|
||||
$u5 = Db::name('user')->where('id', 10)->find();
|
||||
echo "5. 充值积分+10结果: " . json_encode($r5, JSON_UNESCAPED_UNICODE) . " 当前积分: " . ($u5['score'] ?? 0) . "\n";
|
||||
|
||||
// 6. 积分扣除 -10
|
||||
$r6 = doRecharge('score', -10, '真实环境测试扣除积分');
|
||||
$u6 = Db::name('user')->where('id', 10)->find();
|
||||
echo "6. 扣除积分-10结果: " . json_encode($r6, JSON_UNESCAPED_UNICODE) . " 恢复积分: " . ($u6['score'] ?? 0) . "\n";
|
||||
|
||||
// 7. 查询收益中心流水表中新增的充值流水明细
|
||||
$rewards = Db::name('shopro_partner_reward')->where('user_id', 10)->whereIn('reward_type', ['admin_madow', 'admin_voucher'])->order('id', 'desc')->limit(4)->select();
|
||||
echo "\n=== 会员 10 收益流水落库检查 (共 " . count($rewards) . " 笔充值/扣除流水) ===\n";
|
||||
foreach ($rewards as $idx => $item) {
|
||||
echo " [" . ($idx + 1) . "] 类型: {$item['reward_type']} | 金额: {$item['amount']} | 马豆: {$item['madow']} | 备注: " . $item['ext'] . "\n";
|
||||
}
|
||||
|
||||
echo "\n=== 会员 10 真实测试全流程 100% 成功完毕 ===\n";
|
||||
@@ -0,0 +1,96 @@
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
let passed = 0;
|
||||
let failed = 0;
|
||||
|
||||
function assert(condition, testName, detail = '') {
|
||||
if (condition) {
|
||||
console.log(` [PASS] ${testName}`);
|
||||
passed++;
|
||||
} else {
|
||||
console.error(` [FAIL] ${testName} - ${detail}`);
|
||||
failed++;
|
||||
}
|
||||
}
|
||||
|
||||
console.log("========================================================");
|
||||
console.log(" 订单/分润数据清理与马豆汇率回归初始值规范自动化测试");
|
||||
console.log("========================================================\n");
|
||||
|
||||
// 1. 验证控制台清理命令 PartnerClean.php 存在且逻辑完整
|
||||
const cleanCmdPath = path.resolve(__dirname, '../addons/shopro/console/PartnerClean.php');
|
||||
assert(fs.existsSync(cleanCmdPath), 'PartnerClean.php 控制台命令文件存在');
|
||||
const cleanCmdContent = fs.readFileSync(cleanCmdPath, 'utf-8');
|
||||
|
||||
assert(
|
||||
cleanCmdContent.includes("shopro:partner:clean"),
|
||||
'命令名称定义为 shopro:partner:clean'
|
||||
);
|
||||
assert(
|
||||
cleanCmdContent.includes("shopro_order") && cleanCmdContent.includes("shopro_order_item") && cleanCmdContent.includes("shopro_order_aftersale"),
|
||||
'涵盖清理商城订单主表、明细表与售后表'
|
||||
);
|
||||
assert(
|
||||
cleanCmdContent.includes("shopro_partner_reward") && cleanCmdContent.includes("shopro_commission_order") && cleanCmdContent.includes("shopro_commission_reward"),
|
||||
'涵盖清理二开合伙人流水与分销佣金明细'
|
||||
);
|
||||
assert(
|
||||
cleanCmdContent.includes("shopro_partner_rate_log") && cleanCmdContent.includes("shopro_partner_daily_report") && cleanCmdContent.includes("shopro_partner_voucher_plan"),
|
||||
'涵盖清理马豆汇率大盘快照、日结对账报表与兑换券释放计划'
|
||||
);
|
||||
assert(
|
||||
cleanCmdContent.includes("`madow` = 0.00000000") && cleanCmdContent.includes("`direct_order_seq` = 0") && cleanCmdContent.includes("`voucher_total` = 0.00"),
|
||||
'涵盖重置分销商持仓马豆、推三返一序号、兑换券资产归零'
|
||||
);
|
||||
assert(
|
||||
cleanCmdContent.includes("cache('partner_madow_current_rate', null)"),
|
||||
'命令执行后清除马豆汇率缓存 partner_madow_current_rate'
|
||||
);
|
||||
assert(
|
||||
cleanCmdContent.includes("Db::startTrans()") && cleanCmdContent.includes("Db::commit()"),
|
||||
'命令具备严谨的事务控制与异常回滚机制'
|
||||
);
|
||||
|
||||
// 2. 验证 Shopro.php 已注册该 CLI 命令
|
||||
const shoproPath = path.resolve(__dirname, '../addons/shopro/Shopro.php');
|
||||
assert(fs.existsSync(shoproPath), 'Shopro.php 文件存在');
|
||||
const shoproContent = fs.readFileSync(shoproPath, 'utf-8');
|
||||
assert(
|
||||
shoproContent.includes("'addons\\shopro\\console\\PartnerClean'"),
|
||||
'Shopro.php 中已自动注册 PartnerClean CLI 控制台命令'
|
||||
);
|
||||
|
||||
// 3. 验证归档 SQL 文件
|
||||
const sqlPath = path.resolve(__dirname, '../docs/sql/clean_order_and_partner_data.sql');
|
||||
assert(fs.existsSync(sqlPath), 'clean_order_and_partner_data.sql 归档文件存在');
|
||||
const sqlContent = fs.readFileSync(sqlPath, 'utf-8');
|
||||
|
||||
assert(
|
||||
sqlContent.includes("TRUNCATE TABLE `fa_shopro_order`") && sqlContent.includes("TRUNCATE TABLE `fa_shopro_order_item`"),
|
||||
'SQL 脚本包含清空订单表与订单项表'
|
||||
);
|
||||
assert(
|
||||
sqlContent.includes("TRUNCATE TABLE `fa_shopro_partner_reward`"),
|
||||
'SQL 脚本包含清空二开收益中心流水表'
|
||||
);
|
||||
assert(
|
||||
sqlContent.includes("TRUNCATE TABLE `fa_shopro_partner_rate_log`"),
|
||||
'SQL 脚本包含清空马豆汇率历史快照日志'
|
||||
);
|
||||
assert(
|
||||
sqlContent.includes("`madow` = 0.00000000") && sqlContent.includes("`direct_order_seq` = 0"),
|
||||
'SQL 脚本包含重置分销商马豆与推三返一序号'
|
||||
);
|
||||
assert(
|
||||
sqlContent.includes("START TRANSACTION;") && sqlContent.includes("COMMIT;"),
|
||||
'SQL 脚本具备严格的事务包裹'
|
||||
);
|
||||
|
||||
console.log("\n--------------------------------------------------------");
|
||||
console.log(`测试执行结果: 通过 ${passed} 项, 失败 ${failed} 项`);
|
||||
console.log("========================================================\n");
|
||||
|
||||
if (failed > 0) {
|
||||
process.exit(1);
|
||||
}
|
||||
@@ -0,0 +1,110 @@
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
let passed = 0;
|
||||
let failed = 0;
|
||||
|
||||
function assert(condition, testName, detail = '') {
|
||||
if (condition) {
|
||||
console.log(` [PASS] ${testName}`);
|
||||
passed++;
|
||||
} else {
|
||||
console.error(` [FAIL] ${testName} - ${detail}`);
|
||||
failed++;
|
||||
}
|
||||
}
|
||||
|
||||
console.log("========================================================");
|
||||
console.log(" 马豆实时动态汇率、首单步进对齐与精度自适应自动化测试");
|
||||
console.log("========================================================\n");
|
||||
|
||||
// 1. RateService 实时汇率与缓存解耦验证
|
||||
const rateServicePath = path.resolve(__dirname, '../addons/shopro/service/partner/RateService.php');
|
||||
assert(fs.existsSync(rateServicePath), 'RateService.php 文件存在');
|
||||
const rateServiceContent = fs.readFileSync(rateServicePath, 'utf-8');
|
||||
|
||||
assert(
|
||||
!rateServiceContent.includes("RateLog::order('id', 'desc')->find()"),
|
||||
'currentRate() 已移除日结历史快照死锁逻辑,不再永久返回旧汇率'
|
||||
);
|
||||
assert(
|
||||
rateServiceContent.includes('currentRate($forceRefresh = false)'),
|
||||
'currentRate() 支持 $forceRefresh 参数穿透短缓存实时重算'
|
||||
);
|
||||
assert(
|
||||
rateServiceContent.includes('public static function clearCache()'),
|
||||
'RateService 包含 clearCache() 静态方法支持外部主动刷新'
|
||||
);
|
||||
assert(
|
||||
rateServiceContent.includes('$multiple = max(1, floor($n / $step))'),
|
||||
'calcRate() 算法严谨保留起步下限保底与线性步进'
|
||||
);
|
||||
|
||||
// 2. 订单支付监听器联动验证
|
||||
const listenerPath = path.resolve(__dirname, '../addons/shopro/listener/Partner.php');
|
||||
assert(fs.existsSync(listenerPath), 'Partner.php 监听器存在');
|
||||
const listenerContent = fs.readFileSync(listenerPath, 'utf-8');
|
||||
assert(
|
||||
listenerContent.includes('RateService::clearCache()'),
|
||||
'合格订单支付成功后自动调用 RateService::clearCache() 触发实时汇率联动'
|
||||
);
|
||||
|
||||
// 3. IdentityService 马豆格式化与精度优化验证
|
||||
const identityPath = path.resolve(__dirname, '../addons/shopro/service/partner/IdentityService.php');
|
||||
assert(fs.existsSync(identityPath), 'IdentityService.php 存在');
|
||||
const identityContent = fs.readFileSync(identityPath, 'utf-8');
|
||||
|
||||
assert(
|
||||
identityContent.includes("number_format((float) $value, 0, '.', '')"),
|
||||
'formatMadow() 保持历史纯整数输出契约'
|
||||
);
|
||||
assert(
|
||||
identityContent.includes('$floatVal > 0 && $floatVal < 1'),
|
||||
'formatMadow() 具备小于 1 豆的小数自适应保留能力,杜绝分润到账显示为 0 豆'
|
||||
);
|
||||
|
||||
// 4. 用户端 madow.vue 界面精度与兜底验证
|
||||
const madowVuePath = path.resolve(__dirname, '../uniapp/pages/commission/madow.vue');
|
||||
assert(fs.existsSync(madowVuePath), 'madow.vue 存在');
|
||||
const madowVueContent = fs.readFileSync(madowVuePath, 'utf-8');
|
||||
|
||||
assert(
|
||||
madowVueContent.includes('formatInt') && madowVueContent.includes('num > 0 && num < 1'),
|
||||
'madow.vue 中的 formatInt 支持微量小数自适应显示'
|
||||
);
|
||||
assert(
|
||||
madowVueContent.includes('<0.01'),
|
||||
'madow.vue 对微小市值(<0.01元)具备友好的 <0.01 优雅兜底展示'
|
||||
);
|
||||
|
||||
// 5. 纯算法逻辑与数值模拟测试
|
||||
function calcRateSim(n, base = 0.00000075, step = 1) {
|
||||
const multiple = Math.max(1, Math.floor(n / step));
|
||||
return Number((base * multiple).toFixed(10));
|
||||
}
|
||||
|
||||
assert(calcRateSim(0) === 0.00000075, '模拟测试:N=0 单时保底输出基准汇率 0.00000075');
|
||||
assert(calcRateSim(1) === 0.00000075, '模拟测试:N=1 单时等于 0.00000075');
|
||||
assert(calcRateSim(2) === 0.0000015, '模拟测试:N=2 单时递增至 0.00000150');
|
||||
assert(calcRateSim(10000) === 0.0075, '模拟测试:N=10,000 单时精确达到 0.0075 元/豆');
|
||||
|
||||
function formatMadowSim(val) {
|
||||
const num = parseFloat(val) || 0;
|
||||
if (num > 0 && num < 1) {
|
||||
return parseFloat(num.toFixed(4)).toString();
|
||||
}
|
||||
return String(Math.floor(num));
|
||||
}
|
||||
|
||||
assert(formatMadowSim(0.85) === '0.85', '模拟测试:0.85 豆自适应显示为 0.85');
|
||||
assert(formatMadowSim(0.005) === '0.005', '模拟测试:0.005 豆自适应显示为 0.005');
|
||||
assert(formatMadowSim(100) === '100', '模拟测试:100 豆整洁显示为 100');
|
||||
assert(formatMadowSim(0) === '0', '模拟测试:0 豆整洁显示为 0');
|
||||
|
||||
console.log("\n--------------------------------------------------------");
|
||||
console.log(`测试执行结果: 通过 ${passed} 项, 失败 ${failed} 项`);
|
||||
console.log("========================================================\n");
|
||||
|
||||
if (failed > 0) {
|
||||
process.exit(1);
|
||||
}
|
||||
在新工单中引用
屏蔽一个用户