-

+
+
+
+
+
+
+
+
+
-
+
+
+
{$site.name|htmlentities} · {:date('Y')}
-
+
{include file="common/script" /}
diff --git a/public/assets/js/backend/index.js b/public/assets/js/backend/index.js
index 3c3f69f..47580c9 100755
--- a/public/assets/js/backend/index.js
+++ b/public/assets/js/backend/index.js
@@ -406,10 +406,26 @@ define(['jquery', 'bootstrap', 'backend', 'addtabs', 'adminlte', 'form'], functi
var lastlogin = localStorage.getItem("lastlogin");
if (lastlogin) {
lastlogin = JSON.parse(lastlogin);
- $("#profile-img").attr("src", Backend.api.cdnurl(lastlogin.avatar));
- $("#profile-name").val(lastlogin.username);
+ if (lastlogin.avatar) {
+ $("#profile-img").attr("src", Backend.api.cdnurl(lastlogin.avatar)).addClass("is-ready");
+ }
+ if (lastlogin.username) {
+ $("#profile-name").text(lastlogin.username);
+ $("#pd-form-username").val(lastlogin.username);
+ $(".login-greet").addClass("has-user");
+ }
}
+ $(document).on("click", "#toggle-password", function () {
+ var $btn = $(this);
+ var $pwd = $("#pd-form-password");
+ var show = $pwd.attr("type") === "password";
+ $pwd.attr("type", show ? "text" : "password");
+ $btn.toggleClass("is-on", show)
+ .attr("aria-pressed", show ? "true" : "false")
+ .attr("aria-label", show ? ($btn.data("hide") || "隐藏密码") : ($btn.data("show") || "显示密码"));
+ });
+
//让错误提示框居中
Fast.config.toastr.positionClass = "toast-top-center";
@@ -419,6 +435,10 @@ define(['jquery', 'bootstrap', 'backend', 'addtabs', 'adminlte', 'form'], functi
$.each(errors, function (i, j) {
Toastr.error(j);
});
+ var $first = $("#login-form").find(".n-invalid:first");
+ if ($first.length) {
+ $first.trigger("focus");
+ }
},
target: '#errtips'
});
diff --git a/tests/partner/AgentDetailTest.php b/tests/partner/AgentDetailTest.php
index 02e3afb..c42c4c6 100644
--- a/tests/partner/AgentDetailTest.php
+++ b/tests/partner/AgentDetailTest.php
@@ -5,9 +5,13 @@ 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';
+require_once __DIR__ . '/../../application/common.php';
\think\Config::set(\think\Config::load(APP_PATH . 'config.php'));
-\think\Config::set(\think\Config::load(APP_PATH . 'database.php'));
+\think\Config::set(include APP_PATH . 'database.php', 'database');
+\think\Loader::addNamespace('app', APP_PATH);
+require_once __DIR__ . '/../../addons/shopro/helper/helper.php';
use app\admin\controller\shopro\commission\Agent;
use think\Request;
diff --git a/tests/partner/MakerPeerTest.php b/tests/partner/MakerPeerTest.php
new file mode 100644
index 0000000..97647f3
--- /dev/null
+++ b/tests/partner/MakerPeerTest.php
@@ -0,0 +1,353 @@
+activeMap[$userId] = $isActive;
+ }
+
+ public function isActive($agent, $atTime = null)
+ {
+ $userId = is_array($agent) ? ($agent['user_id'] ?? 0) : ($agent->user_id ?? 0);
+ if (isset($this->activeMap[$userId])) {
+ return $this->activeMap[$userId];
+ }
+ return true; // 默认均活跃
+ }
+}
+
+class MakerPeerTest
+{
+ private $passed = 0;
+ private $failed = 0;
+
+ public function run()
+ {
+ echo "========================================================\n";
+ echo " 创客两代平级级联收益计算测试 (Maker Peer Cascading) \n";
+ echo "========================================================\n\n";
+
+ $this->testThreePeerCascade();
+ $this->testFourPeerCapLimit();
+ $this->testInactiveIntermediaryCascade();
+ $this->testV1PeerCascade();
+ $this->testMultiSegmentPeers();
+
+ 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++;
+ }
+ }
+
+ /**
+ * 测试 1:经典 3 平级创客(A -> B -> C,均为基数 60 的 V3)
+ * 规则验证:
+ * A(2): 实拿极差全额 60.00
+ * B(3): 第1代平级 60 * 10% = 6.00
+ * C(4): 第2代平级 6 * 10% = 0.60
+ */
+ public function testThreePeerCascade()
+ {
+ $testName = "场景1: 3个平级创客(A->B->C, V3) 第1代拿6元,第2代拿0.60元";
+
+ Db::startTrans();
+ try {
+ $order = ['id' => 9901, 'user_id' => 7];
+ $buyerId = 7;
+
+ // 关系链:买家(7) -> A(2, V3/level 4) -> B(3, V3/level 4) -> C(4, V3/level 4)
+ $chain = [
+ ['user_id' => 2, 'level' => 4], // Level 4 基数 60 (V3)
+ ['user_id' => 3, 'level' => 4],
+ ['user_id' => 4, 'level' => 4],
+ ];
+
+ $diffResult = [
+ 'seen' => 60.0,
+ 'paid' => [
+ ['user_id' => 2, 'level' => 4, 'cap' => 60.0, 'amount' => 60.0]
+ ]
+ ];
+
+ $active = new MockActiveService();
+ $reward = new MakerPeerReward();
+ $reward->run($order, $buyerId, $chain, $diffResult, $active);
+
+ $records = Db::name('shopro_partner_reward')
+ ->where('order_id', 9901)
+ ->where('reward_type', 'maker_peer')
+ ->order('id asc')
+ ->select();
+
+ $bReward = null;
+ $cReward = null;
+ foreach ($records as $r) {
+ if ($r['user_id'] == 3) $bReward = $r;
+ if ($r['user_id'] == 4) $cReward = $r;
+ }
+
+ $bAmount = $bReward ? floatval($bReward['amount']) : 0;
+ $cAmount = $cReward ? floatval($cReward['amount']) : 0;
+ $bExt = $bReward ? json_decode($bReward['ext'], true) : [];
+ $cExt = $cReward ? json_decode($cReward['ext'], true) : [];
+
+ $bPass = ($bAmount == 6.00) && (($bExt['generation'] ?? 0) == 1);
+ $cPass = ($cAmount == 0.60) && (($cExt['generation'] ?? 0) == 2);
+
+ $this->assert($bPass && $cPass, $testName, "B金额={$bAmount}(期6.00), C金额={$cAmount}(期0.60)");
+ } catch (\Exception $e) {
+ $this->assert(false, $testName, "异常: " . $e->getMessage());
+ } finally {
+ Db::rollback();
+ }
+ }
+
+ /**
+ * 测试 2:4 平级创客深度封顶(A -> B -> C -> D,均为 V3)
+ * 规则验证:
+ * 第3代创客 D 应严格封顶截断,不产生任何平级奖记录
+ */
+ public function testFourPeerCapLimit()
+ {
+ $testName = "场景2: 4个平级创客(A->B->C->D) 第3代D严格封顶截断不分佣";
+
+ Db::startTrans();
+ try {
+ $order = ['id' => 9902, 'user_id' => 7];
+ $buyerId = 7;
+ $chain = [
+ ['user_id' => 2, 'level' => 4],
+ ['user_id' => 3, 'level' => 4],
+ ['user_id' => 4, 'level' => 4],
+ ['user_id' => 5, 'level' => 4],
+ ];
+
+ $diffResult = [
+ 'seen' => 60.0,
+ 'paid' => [
+ ['user_id' => 2, 'level' => 4, 'cap' => 60.0, 'amount' => 60.0]
+ ]
+ ];
+
+ $active = new MockActiveService();
+ $reward = new MakerPeerReward();
+ $reward->run($order, $buyerId, $chain, $diffResult, $active);
+
+ $dReward = Db::name('shopro_partner_reward')
+ ->where('order_id', 9902)
+ ->where('user_id', 5)
+ ->where('reward_type', 'maker_peer')
+ ->find();
+
+ $this->assert(empty($dReward), $testName, "第3代平级创客D应无平级奖记录");
+ } catch (\Exception $e) {
+ $this->assert(false, $testName, "异常: " . $e->getMessage());
+ } finally {
+ Db::rollback();
+ }
+ }
+
+ /**
+ * 测试 3:中间创客未自购场景(A -> B(不活跃) -> C(活跃))
+ * 规则验证:
+ * B 产生 skip 记录(status=-1, skip_reason=inactive_30d)
+ * C 仍然作为合格第2代拿到 0.60 元
+ */
+ public function testInactiveIntermediaryCascade()
+ {
+ $testName = "场景3: 中间平级B 30日无自购被跳过,上级C仍正常获得第2代平级0.60元";
+
+ Db::startTrans();
+ try {
+ $order = ['id' => 9903, 'user_id' => 7];
+ $buyerId = 7;
+ $chain = [
+ ['user_id' => 2, 'level' => 4],
+ ['user_id' => 3, 'level' => 4],
+ ['user_id' => 4, 'level' => 4],
+ ];
+
+ $diffResult = [
+ 'seen' => 60.0,
+ 'paid' => [
+ ['user_id' => 2, 'level' => 4, 'cap' => 60.0, 'amount' => 60.0]
+ ]
+ ];
+
+ $active = new MockActiveService();
+ $active->setActive(3, false); // B 不活跃
+ $active->setActive(4, true); // C 活跃
+
+ $reward = new MakerPeerReward();
+ $reward->run($order, $buyerId, $chain, $diffResult, $active);
+
+ $bSkip = Db::name('shopro_partner_reward')
+ ->where('order_id', 9903)
+ ->where('user_id', 3)
+ ->where('status', -1)
+ ->find();
+
+ $cReward = Db::name('shopro_partner_reward')
+ ->where('order_id', 9903)
+ ->where('user_id', 4)
+ ->where('status', 1)
+ ->find();
+
+ $bSkipped = !empty($bSkip) && floatval($bSkip['amount']) == 0;
+ $cReceived = !empty($cReward) && floatval($cReward['amount']) == 0.60;
+
+ $this->assert($bSkipped && $cReceived, $testName, "B被跳过记录, C正常结算0.60元");
+ } catch (\Exception $e) {
+ $this->assert(false, $testName, "异常: " . $e->getMessage());
+ } finally {
+ Db::rollback();
+ }
+ }
+
+ /**
+ * 测试 4:V1 等级(level 2,基数 20 元)两代级联
+ * 规则验证:
+ * A(2): 极差 20.00
+ * B(3): 第1代 20 * 10% = 2.00
+ * C(4): 第2代 2 * 10% = 0.20
+ */
+ public function testV1PeerCascade()
+ {
+ $testName = "场景4: V1等级(基数20元)两代级联 B得2.00元, C得0.20元";
+
+ Db::startTrans();
+ try {
+ $order = ['id' => 9904, 'user_id' => 7];
+ $buyerId = 7;
+ $chain = [
+ ['user_id' => 2, 'level' => 2], // Level 2 基数 20 (V1)
+ ['user_id' => 3, 'level' => 2],
+ ['user_id' => 4, 'level' => 2],
+ ];
+
+ $diffResult = [
+ 'seen' => 20.0,
+ 'paid' => [
+ ['user_id' => 2, 'level' => 2, 'cap' => 20.0, 'amount' => 20.0]
+ ]
+ ];
+
+ $active = new MockActiveService();
+ $reward = new MakerPeerReward();
+ $reward->run($order, $buyerId, $chain, $diffResult, $active);
+
+ $bReward = Db::name('shopro_partner_reward')->where('order_id', 9904)->where('user_id', 3)->find();
+ $cReward = Db::name('shopro_partner_reward')->where('order_id', 9904)->where('user_id', 4)->find();
+
+ $bAmount = $bReward ? floatval($bReward['amount']) : 0;
+ $cAmount = $cReward ? floatval($cReward['amount']) : 0;
+
+ $this->assert($bAmount == 2.00 && $cAmount == 0.20, $testName, "B金额={$bAmount}(期2.00), C金额={$cAmount}(期0.20)");
+ } catch (\Exception $e) {
+ $this->assert(false, $testName, "异常: " . $e->getMessage());
+ } finally {
+ Db::rollback();
+ }
+ }
+
+ /**
+ * 测试 5:多段平级并存(V1平级 + V3平级在同一推荐链)
+ * 链条:买家(7) -> A(2, V1) -> B(3, V1) -> C(4, V3) -> D(5, V3) -> E(6, V3)
+ * diff paid: A (level 2, cap 20), C (level 4, cap 60)
+ * 期望:
+ * A(2): 极差 20.00
+ * B(3): V1第1代平级 = 20 * 10% = 2.00
+ * C(4): 极差 60 - 20 = 40.00
+ * D(5): V3第1代平级 = 60 * 10% = 6.00
+ * E(6): V3第2代平级 = 6 * 10% = 0.60
+ */
+ public function testMultiSegmentPeers()
+ {
+ $testName = "场景5: 混合等级链条多段平级互不干扰独立级联";
+
+ Db::startTrans();
+ try {
+ $order = ['id' => 9905, 'user_id' => 7];
+ $buyerId = 7;
+ $chain = [
+ ['user_id' => 2, 'level' => 2], // V1
+ ['user_id' => 3, 'level' => 2], // V1
+ ['user_id' => 4, 'level' => 4], // V3
+ ['user_id' => 5, 'level' => 4], // V3
+ ['user_id' => 6, 'level' => 4], // V3
+ ];
+
+ $diffResult = [
+ 'seen' => 60.0,
+ 'paid' => [
+ ['user_id' => 2, 'level' => 2, 'cap' => 20.0, 'amount' => 20.0],
+ ['user_id' => 4, 'level' => 4, 'cap' => 60.0, 'amount' => 40.0]
+ ]
+ ];
+
+ $active = new MockActiveService();
+ $reward = new MakerPeerReward();
+ $reward->run($order, $buyerId, $chain, $diffResult, $active);
+
+ $bReward = Db::name('shopro_partner_reward')->where('order_id', 9905)->where('user_id', 3)->find();
+ $dReward = Db::name('shopro_partner_reward')->where('order_id', 9905)->where('user_id', 5)->find();
+ $eReward = Db::name('shopro_partner_reward')->where('order_id', 9905)->where('user_id', 6)->find();
+
+ $bAmount = $bReward ? floatval($bReward['amount']) : 0;
+ $dAmount = $dReward ? floatval($dReward['amount']) : 0;
+ $eAmount = $eReward ? floatval($eReward['amount']) : 0;
+
+ $this->assert(
+ $bAmount == 2.00 && $dAmount == 6.00 && $eAmount == 0.60,
+ $testName,
+ "B(V1平级1)={$bAmount}(期2.00), D(V3平级1)={$dAmount}(期6.00), E(V3平级2)={$eAmount}(期0.60)"
+ );
+ } catch (\Exception $e) {
+ $this->assert(false, $testName, "异常: " . $e->getMessage());
+ } finally {
+ Db::rollback();
+ }
+ }
+}
+
+// 执行测试
+if (php_sapi_name() === 'cli') {
+ $tester = new MakerPeerTest();
+ exit($tester->run());
+}
diff --git a/tests/partner/PartnerFinanceTest.php b/tests/partner/PartnerFinanceTest.php
index 3fd9a72..f2efcd9 100644
--- a/tests/partner/PartnerFinanceTest.php
+++ b/tests/partner/PartnerFinanceTest.php
@@ -6,9 +6,13 @@ 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';
+require_once __DIR__ . '/../../application/common.php';
\think\Config::set(\think\Config::load(APP_PATH . 'config.php'));
-\think\Config::set(\think\Config::load(APP_PATH . 'database.php'));
+\think\Config::set(include APP_PATH . 'database.php', 'database');
+\think\Loader::addNamespace('app', APP_PATH);
+require_once __DIR__ . '/../../addons/shopro/helper/helper.php';
use addons\shopro\service\partner\DailySettleService;
use addons\shopro\service\partner\PartnerConfig;
diff --git a/tests/partner/TeamCenterTest.php b/tests/partner/TeamCenterTest.php
index e9f9075..4e910f3 100644
--- a/tests/partner/TeamCenterTest.php
+++ b/tests/partner/TeamCenterTest.php
@@ -6,9 +6,13 @@ 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';
+require_once __DIR__ . '/../../application/common.php';
\think\Config::set(\think\Config::load(APP_PATH . 'config.php'));
\think\Config::set(\think\Config::load(APP_PATH . 'database.php'));
+\think\Loader::addNamespace('app', APP_PATH);
+require_once __DIR__ . '/../../addons/shopro/helper/helper.php';
use addons\shopro\controller\commission\Agent;
use think\Request;