fix(fsgx): 积分释放定时任务、佣金轮巡与 UniApp 体验

- PointsReleaseServices: 使用 Db::table 查询与更新,构造函数注入 UserDao;日志与账单独立 try/catch\n- SystemTimer: implement_timer 捕获后重新抛出异常,便于 run_now 返回错误\n- SystemTimerServices / 控制器: runNow 返回任务结果并在 API 中带回 result\n- StoreOrderCreateServices: 报单佣金位次修正与多件轮巡\n- UniApp: 佣金记录跳转 type=2、余额提现免手续费展示、状态页与资产页头部渐变与提现页一致\n- docs: 增加 fsgx-issues-0328-1 问题跟踪

Made-with: Cursor
This commit is contained in:
panchengyong
2026-03-29 10:36:52 +08:00
parent 35b6d76d50
commit ac86ec57cf
9 changed files with 141 additions and 102 deletions

View File

@@ -119,8 +119,8 @@ class SystemTimer extends AuthController
return $this->fail('定时任务标识不存在');
}
try {
$this->services->runNow($mark);
return $this->success('任务已触发并执行成功');
$result = $this->services->runNow($mark);
return $this->success('任务已触发并执行成功', ['result' => $result]);
} catch (\Throwable $e) {
return $this->fail($e->getMessage());
}

View File

@@ -206,6 +206,7 @@ class SystemTimer extends Cron implements ListenerInterface
'line' => $e->getLine(),
'msg' => $e->getMessage()
]);
throw $e;
}
}
}

View File

@@ -3,12 +3,10 @@ declare(strict_types=1);
namespace app\services\hjf;
use app\dao\hjf\PointsReleaseLogDao;
use app\dao\user\UserDao;
use app\services\BaseServices;
use app\services\user\UserBillServices;
use crmeb\services\SystemConfigService;
use think\annotation\Inject;
use think\facade\Db;
use think\facade\Log;
@@ -24,14 +22,10 @@ use think\facade\Log;
*/
class PointsReleaseServices extends BaseServices
{
#[Inject]
protected PointsReleaseLogDao $logDao;
#[Inject]
protected UserDao $userDao;
#[Inject]
protected UserBillServices $userBillServices;
public function __construct(UserDao $dao)
{
$this->dao = $dao;
}
/**
* 执行今日积分释放(批量)
@@ -40,24 +34,21 @@ class PointsReleaseServices extends BaseServices
*/
public function executeRelease(): array
{
$rate = (int)SystemConfigService::get('hjf_release_rate', 4);
$rate = (int)SystemConfigService::get('hjf_release_rate', 4);
$releaseDate = date('Y-m-d');
$processed = 0;
$totalReleased = 0;
// 分批处理,每批 200 条,避免内存溢合
$page = 1;
$page = 1;
$limit = 200;
do {
$users = $this->userDao->selectList(
['frozen_points' => ['>', 0]],
'uid,frozen_points,available_points',
$page,
$limit,
'uid',
'asc'
);
$users = Db::table('eb_user')
->where('frozen_points', '>', 0)
->field('uid, frozen_points, available_points')
->page($page, $limit)
->select()
->toArray();
if (empty($users)) {
break;
@@ -65,52 +56,56 @@ class PointsReleaseServices extends BaseServices
foreach ($users as $user) {
$frozenBefore = (int)$user['frozen_points'];
// 使用 bcmath 确保精度
$releaseAmount = (int)bcdiv(bcmul((string)$frozenBefore, (string)$rate), '1000');
if ($releaseAmount <= 0) {
continue;
}
$frozenAfter = $frozenBefore - $releaseAmount;
$frozenAfter = $frozenBefore - $releaseAmount;
$newAvailable = (int)$user['available_points'] + $releaseAmount;
// 主更新:只更新积分字段,失败则跳过本用户
try {
Db::transaction(function () use ($user, $releaseAmount, $frozenBefore, $frozenAfter, $releaseDate) {
// 更新用户积分字段
$this->userDao->update($user['uid'], [
'frozen_points' => $frozenAfter,
'available_points' => Db::raw('available_points + ' . $releaseAmount),
], 'uid');
// 写 points_release_log本次每日释放记录
$this->logDao->save([
'uid' => $user['uid'],
'points' => $releaseAmount,
'pm' => 1,
'type' => 'release',
'title' => '每日释放',
'mark' => "积分每日自动解冻,释放日期 {$releaseDate}",
'status' => 'released',
'release_date' => $releaseDate,
]);
// 同步写入 eb_user_bill使管理后台积分日志页面可见
$integralBalance = (int)($this->userDao->value(['uid' => $user['uid']], 'integral') ?: 0);
$this->userBillServices->income(
'frozen_points_release',
(int)$user['uid'],
(int)$releaseAmount,
$integralBalance,
0,
0,
"积分每日自动解冻,释放日期 {$releaseDate}"
);
});
Db::table('eb_user')->where('uid', $user['uid'])->update([
'frozen_points' => max(0, $frozenAfter),
'available_points' => $newAvailable,
]);
$totalReleased += $releaseAmount;
$processed++;
} catch (\Throwable $e) {
Log::error("[PointsRelease] uid={$user['uid']} 释放失败: " . $e->getMessage());
Log::error("[PointsRelease] uid={$user['uid']} 积分更新失败: " . $e->getMessage());
continue;
}
// 日志写入独立处理,失败不影响已提交的积分更新
try {
Db::table('eb_points_release_log')->insert([
'uid' => $user['uid'],
'points' => $releaseAmount,
'pm' => 1,
'type' => 'release',
'title' => '每日释放',
'mark' => "积分每日自动解冻,释放日期 {$releaseDate}",
'status' => 'released',
'release_date' => $releaseDate,
'add_time' => time(),
]);
/** @var UserBillServices $billServices */
$billServices = app()->make(UserBillServices::class);
$billServices->income(
'frozen_points_release',
(int)$user['uid'],
(int)$releaseAmount,
$newAvailable,
0,
0,
"积分每日自动解冻,释放日期 {$releaseDate}"
);
} catch (\Throwable $e) {
Log::warning("[PointsRelease] uid={$user['uid']} 日志写入失败(积分已释放): " . $e->getMessage());
}
}
@@ -120,9 +115,9 @@ class PointsReleaseServices extends BaseServices
Log::info("[PointsRelease] 完成processed={$processed} total_released={$totalReleased}");
return [
'processed' => $processed,
'processed' => $processed,
'total_released' => $totalReleased,
'release_date' => $releaseDate,
'release_date' => $releaseDate,
];
}
}

View File

@@ -926,6 +926,9 @@ class StoreOrderCreateServices extends BaseServices
$storeBrokerageTwo = $spread_two_uid = 0;
}
// fsgx B-2B: 同一订单内多个报单商品件数累计偏移,保证位次轮巡
$queueGoodsOffset = 0;
foreach ($cartInfo as &$cart) {
$oneBrokerage = '0';//一级返佣金额
$twoBrokerage = '0';//二级返佣金额
@@ -1002,8 +1005,12 @@ class StoreOrderCreateServices extends BaseServices
$useCycleBrokerage = ($cycleCount > 0 && is_array($cycleRates) && count($cycleRates) > 0 && $isQueueGoods === 1);
if ($useCycleBrokerage && $spread_uid > 0) {
// fsgx B1 + B6用事务锁序列化位次计算防止并发竞态导致两笔订单拿到相同位次
$brokerageResult = \think\facade\Db::transaction(function () use ($spread_uid, $cycleCount, $cycleRates, $price) {
// fsgx B1 + B6 + B-2B:用事务锁序列化位次计算,支持多件商品逐件轮巡
$cartNumInt = (int)$cartNum;
// 计算单件价格,用于逐件计算佣金
$unitPrice = $cartNumInt > 0 ? bcdiv((string)$price, (string)$cartNumInt, 4) : '0';
$currentOffset = $queueGoodsOffset;
$brokerageResult = \think\facade\Db::transaction(function () use ($spread_uid, $cycleCount, $cycleRates, $unitPrice, $cartNumInt, $currentOffset) {
// 锁定推荐人行,确保同一推荐人同时只有一个事务在计算位次
\think\facade\Db::name('user')
->where('uid', $spread_uid)
@@ -1021,8 +1028,7 @@ class StoreOrderCreateServices extends BaseServices
return '0';
}
// fsgx B6按 id ASC 排序取位次,比 count 更精确且在锁保护下无竞态
// 当前订单在 paid=1 后已写入,这里取所有已完成订单并按序找到本单排名
// fsgx B6统计推荐人已完成的报单订单数,作为起始位次基准
$completedCount = (int)\think\facade\Db::name('store_order')
->where('spread_uid', $spread_uid)
->where('is_queue_goods', 1)
@@ -1030,14 +1036,20 @@ class StoreOrderCreateServices extends BaseServices
->where('is_del', 0)
->count();
$position = max(0, $completedCount - 1) % $cycleCount;
$cycleRatePercent = isset($cycleRates[$position]) ? (int)$cycleRates[$position] : (int)($cycleRates[0] ?? 0);
if ($cycleRatePercent > 0) {
return bcmul((string)$price, bcdiv((string)$cycleRatePercent, '100', 4), 2);
// fsgx B-2B逐件轮巡每件商品取下一个位次的佣金比例后累加
$total = '0';
for ($i = 0; $i < $cartNumInt; $i++) {
$position = ($completedCount + $currentOffset + $i) % $cycleCount;
$cycleRatePercent = isset($cycleRates[$position]) ? (int)$cycleRates[$position] : (int)($cycleRates[0] ?? 0);
if ($cycleRatePercent > 0) {
$total = bcadd($total, bcmul((string)$unitPrice, bcdiv((string)$cycleRatePercent, '100', 4), 2), 2);
}
}
return '0';
return $total;
});
$oneBrokerage = $brokerageResult;
// 当前购物车项的件数已消费,累加到偏移量
$queueGoodsOffset += $cartNumInt;
} else {
//一级返佣比例 小于等于零时直接返回 不返佣
if ($storeBrokerageRatio > 0) {

View File

@@ -236,15 +236,15 @@ class SystemTimerServices extends BaseServices
/**
* 手动立即执行指定定时任务HTTP 请求上下文,绕过 Swoole Cron 构造,用反射直接调用 implement_timer
* @param string $mark
* @return void
* @return mixed 任务返回值(如 executeRelease 返回的处理统计数组),可为 null
*/
public function runNow(string $mark): void
public function runNow(string $mark): mixed
{
$this->update(['mark' => $mark], ['last_execution_time' => time()]);
try {
$ref = new \ReflectionClass(SystemTimerListener::class);
$instance = $ref->newInstanceWithoutConstructor();
$instance->implement_timer($mark);
return $instance->implement_timer($mark);
} catch (\Throwable $e) {
$taskName = $this->taskName[$mark] ?? $mark;
throw new \RuntimeException("定时任务[{$taskName}]执行失败: " . $e->getMessage(), 0, $e);