feat: add syj promote workflow

This commit is contained in:
apple
2026-05-03 14:44:12 +08:00
parent 12c2431d4e
commit 0e07a65e3f
36 changed files with 1972 additions and 1 deletions

View File

@@ -0,0 +1,119 @@
<?php
declare(strict_types=1);
namespace app\controller\admin\v1\syj;
use app\controller\admin\AuthController;
use app\services\syj\SyjPromoteConfigServices;
use app\services\syj\SyjPromoteRewardTriggerServices;
use app\services\syj\SyjPromoteSettlementServices;
use app\services\syj\SyjPromoteTaskServices;
use think\annotation\Inject;
class PromoteController extends AuthController
{
#[Inject]
protected SyjPromoteTaskServices $services;
#[Inject]
protected SyjPromoteSettlementServices $settlementServices;
#[Inject]
protected SyjPromoteConfigServices $configServices;
#[Inject]
protected SyjPromoteRewardTriggerServices $triggerServices;
public function taskList(): mixed
{
$where = $this->request->getMore([
['keyword', ''],
['status', ''],
['reward_trigger_status', ''],
['start_time', ''],
['end_time', ''],
['page', 1],
['limit', 20],
]);
$page = (int)$where['page'];
$limit = (int)$where['limit'];
unset($where['page'], $where['limit']);
return $this->success($this->services->adminTasks($where, $page, $limit));
}
public function taskDetail(int $id): mixed
{
return $this->success($this->services->adminTaskDetail($id));
}
public function taskRecords(int $id): mixed
{
return $this->success($this->services->records($id));
}
public function cashoutList(): mixed
{
$where = $this->request->getMore([
['keyword', ''],
['audit_status', ''],
['page', 1],
['limit', 20],
]);
$where['settle_type'] = 'early_cashout';
$page = (int)$where['page'];
$limit = (int)$where['limit'];
unset($where['page'], $where['limit']);
return $this->success($this->settlementServices->adminList($where, $page, $limit));
}
public function settlementList(): mixed
{
$where = $this->request->getMore([
['keyword', ''],
['audit_status', ''],
['settle_type', ''],
['page', 1],
['limit', 20],
]);
$page = (int)$where['page'];
$limit = (int)$where['limit'];
unset($where['page'], $where['limit']);
return $this->success($this->settlementServices->adminList($where, $page, $limit));
}
public function auditCashout(int $id): mixed
{
$data = $this->request->postMore([
['status', 1],
['remark', ''],
]);
$this->settlementServices->auditCashout($id, (int)$this->adminId, (int)$data['status'], (string)$data['remark']);
return $this->success('审核成功');
}
public function getConfig(): mixed
{
return $this->success($this->configServices->getConfig());
}
public function saveConfig(): mixed
{
$data = $this->request->postMore([
['base_amount', 4333],
['target_count', 4],
['reward_rates', [10, 20, 30, 40]],
['early_cashout_fee_rate', 7],
['task_generate_timing', 'on_confirm'],
['task_order_dedupe', 'order'],
['reward_trigger_enable', 1],
]);
$this->configServices->saveConfig($data);
return $this->success('保存成功');
}
public function retryTrigger(int $taskId): mixed
{
$this->triggerServices->retry($taskId);
return $this->success('重试完成');
}
}

View File

@@ -0,0 +1,66 @@
<?php
declare(strict_types=1);
namespace app\controller\api\v1\syj;
use app\Request;
use app\services\syj\SyjPromoteSettlementServices;
use app\services\syj\SyjPromoteTaskServices;
use think\annotation\Inject;
class PromoteController
{
#[Inject]
protected SyjPromoteTaskServices $services;
#[Inject]
protected SyjPromoteSettlementServices $settlementServices;
public function overview(Request $request): mixed
{
return app('json')->success($this->services->overview((int)$request->uid()));
}
public function taskList(Request $request): mixed
{
[$page, $limit] = $this->page($request);
$where = ['status' => $request->param('status', '')];
return app('json')->success($this->services->userTasks((int)$request->uid(), $where, $page, $limit));
}
public function taskDetail(Request $request, int $id): mixed
{
return app('json')->success($this->services->userTaskDetail((int)$request->uid(), $id));
}
public function taskRecords(Request $request, int $id): mixed
{
$task = $this->services->userTaskDetail((int)$request->uid(), $id);
return app('json')->success($task['records'] ?? []);
}
public function cashout(Request $request, int $id): mixed
{
return app('json')->success($this->settlementServices->applyCashout((int)$request->uid(), $id), '提交成功');
}
public function amountLog(Request $request): mixed
{
[$page, $limit] = $this->page($request);
return app('json')->success($this->services->amountLogs((int)$request->uid(), $page, $limit));
}
public function settlementList(Request $request): mixed
{
[$page, $limit] = $this->page($request);
return app('json')->success($this->settlementServices->listForUser((int)$request->uid(), $page, $limit));
}
private function page(Request $request): array
{
return [
max(1, (int)$request->param('page', 1)),
min(50, max(1, (int)$request->param('limit', 15))),
];
}
}

View File

@@ -0,0 +1,23 @@
<?php
declare(strict_types=1);
namespace app\dao\syj;
use app\dao\BaseDao;
use app\model\syj\PromoteAmountLog;
class PromoteAmountLogDao extends BaseDao
{
protected function setModel(): string
{
return PromoteAmountLog::class;
}
public function getUserList(int $uid, int $page, int $limit): array
{
$model = $this->getModel()->where('uid', $uid);
$count = (clone $model)->count();
$list = $model->order('add_time', 'desc')->page($page, $limit)->select()->toArray();
return compact('list', 'count');
}
}

View File

@@ -0,0 +1,15 @@
<?php
declare(strict_types=1);
namespace app\dao\syj;
use app\dao\BaseDao;
use app\model\syj\PromoteRewardTrigger;
class PromoteRewardTriggerDao extends BaseDao
{
protected function setModel(): string
{
return PromoteRewardTrigger::class;
}
}

View File

@@ -0,0 +1,43 @@
<?php
declare(strict_types=1);
namespace app\dao\syj;
use app\dao\BaseDao;
use app\model\syj\PromoteSettlement;
class PromoteSettlementDao extends BaseDao
{
protected function setModel(): string
{
return PromoteSettlement::class;
}
public function getUserList(int $uid, int $page, int $limit): array
{
$model = $this->getModel()->where('uid', $uid);
$count = (clone $model)->count();
$list = $model->order('add_time', 'desc')->page($page, $limit)->select()->toArray();
return compact('list', 'count');
}
public function getAdminList(array $where, int $page, int $limit): array
{
$model = $this->getModel()->alias('s')
->leftJoin('user u', 'u.uid = s.uid')
->leftJoin('syj_promote_task t', 't.id = s.task_id')
->field('s.*,t.task_no,u.nickname,u.phone');
if (isset($where['audit_status']) && $where['audit_status'] !== '') {
$model = $model->where('s.audit_status', (int)$where['audit_status']);
}
if (!empty($where['settle_type'])) {
$model = $model->where('s.settle_type', $where['settle_type']);
}
if (!empty($where['keyword'])) {
$model = $model->where('t.task_no|u.nickname|u.phone|u.uid', 'like', '%' . $where['keyword'] . '%');
}
$count = (clone $model)->count();
$list = $model->order('s.add_time', 'desc')->page($page, $limit)->select()->toArray();
return compact('list', 'count');
}
}

View File

@@ -0,0 +1,73 @@
<?php
declare(strict_types=1);
namespace app\dao\syj;
use app\dao\BaseDao;
use app\model\syj\PromoteTask;
class PromoteTaskDao extends BaseDao
{
protected function setModel(): string
{
return PromoteTask::class;
}
public function getUserList(int $uid, array $where, int $page, int $limit): array
{
$model = $this->getModel()->where('uid', $uid);
if (isset($where['status']) && $where['status'] !== '') {
$model = $model->where('status', (int)$where['status']);
}
$count = (clone $model)->count();
$list = $model->order('add_time', 'desc')->page($page, $limit)->select()->toArray();
return compact('list', 'count');
}
public function getAdminList(array $where, int $page, int $limit): array
{
$model = $this->getModel()->alias('t')
->leftJoin('user u', 'u.uid = t.uid')
->field('t.*,u.nickname,u.phone');
if (!empty($where['keyword'])) {
$keyword = '%' . $where['keyword'] . '%';
$model = $model->where('t.task_no|t.source_order_no|u.nickname|u.phone|u.uid', 'like', $keyword);
}
if (isset($where['status']) && $where['status'] !== '') {
$model = $model->where('t.status', (int)$where['status']);
}
if (isset($where['reward_trigger_status']) && $where['reward_trigger_status'] !== '') {
$model = $model->where('t.reward_trigger_status', (int)$where['reward_trigger_status']);
}
if (!empty($where['start_time'])) {
$model = $model->where('t.add_time', '>=', strtotime($where['start_time']));
}
if (!empty($where['end_time'])) {
$model = $model->where('t.add_time', '<=', strtotime($where['end_time']) + 86399);
}
$count = (clone $model)->count();
$list = $model->order('t.add_time', 'desc')->page($page, $limit)->select()->toArray();
return compact('list', 'count');
}
public function getEarliestActiveTask(int $uid): ?array
{
$row = $this->getModel()
->where('uid', $uid)
->where('status', 0)
->order('add_time', 'asc')
->lock(true)
->find();
return $row ? $row->toArray() : null;
}
public function getCashoutAvailableTasks(int $uid): array
{
return $this->getModel()
->where('uid', $uid)
->where('status', 0)
->whereBetween('progress_count', [1, 3])
->select()
->toArray();
}
}

View File

@@ -0,0 +1,24 @@
<?php
declare(strict_types=1);
namespace app\dao\syj;
use app\dao\BaseDao;
use app\model\syj\PromoteTaskRecord;
class PromoteTaskRecordDao extends BaseDao
{
protected function setModel(): string
{
return PromoteTaskRecord::class;
}
public function getTaskRecords(int $taskId, int $uid = 0): array
{
$model = $this->getModel()->where('task_id', $taskId);
if ($uid > 0) {
$model = $model->where('uid', $uid);
}
return $model->order('step_no', 'asc')->select()->toArray();
}
}

View File

@@ -0,0 +1,21 @@
<?php
declare(strict_types=1);
namespace app\dao\syj;
use app\dao\BaseDao;
use app\model\syj\PromoteUserAmount;
class PromoteUserAmountDao extends BaseDao
{
protected function setModel(): string
{
return PromoteUserAmount::class;
}
public function lockByUid(int $uid): ?array
{
$row = $this->getModel()->where('uid', $uid)->lock(true)->find();
return $row ? $row->toArray() : null;
}
}

View File

@@ -29,6 +29,7 @@ use app\services\order\StoreOrderComputedServices;
use app\services\order\StoreOrderCreateServices;
use app\services\order\StoreOrderInvoiceServices;
use app\services\order\StoreOrderTakeServices;
use app\services\syj\SyjPromoteTaskServices;
use app\services\user\channel\ChannelMerchantServices;
use app\services\user\UserMoneyServices;
use app\services\user\UserServices;
@@ -69,6 +70,17 @@ class Pay implements ListenerInterface
}
}
if (!empty($orderInfo['is_queue_goods'])) {
try {
/** @var SyjPromoteTaskServices $syjServices */
$syjServices = app()->make(SyjPromoteTaskServices::class);
$syjServices->handleOrderEffective(is_array($orderInfo) ? $orderInfo : $orderInfo->toArray(), 'order_pay');
$syjServices->handleRecommendedOrder(is_array($orderInfo) ? $orderInfo : $orderInfo->toArray(), 'order_pay');
} catch (\Throwable $e) {
Log::error('[SYJ] 支付事件处理失败 order_id=' . ($orderInfo['id'] ?? 0) . ': ' . $e->getMessage());
}
}
//创建拼团
if ($orderInfo['activity_id'] && !$orderInfo['refund_status']) {
//拼团

View File

@@ -11,6 +11,7 @@ use app\jobs\supplier\SupplierFinanceJob;use app\jobs\system\CapitalFlowJob;
use app\services\order\StoreOrderInvoiceServices;
use app\services\order\StoreOrderServices;
use app\services\order\StoreOrderStatusServices;
use app\services\syj\SyjPromoteTaskServices;
use app\services\user\UserServices;
use crmeb\interfaces\ListenerInterface;
@@ -48,6 +49,16 @@ class Refund implements ListenerInterface
//订单退款消息推送
event('notice.notice', [['data' => $data, 'order' => $order], 'order_refund']);
if (!empty($order['is_queue_goods'])) {
try {
/** @var SyjPromoteTaskServices $syjServices */
$syjServices = app()->make(SyjPromoteTaskServices::class);
$syjServices->handleRefund($order, $data);
} catch (\Throwable $e) {
\think\facade\Log::error('[SYJ] 退款事件处理失败 order_id=' . ($order['id'] ?? 0) . ': ' . $e->getMessage());
}
}
//检测主订单 是否全部退款
if ($order['pid']) {
$id = (int)$order['pid'];

View File

@@ -0,0 +1,23 @@
<?php
declare(strict_types=1);
namespace app\model\syj;
use crmeb\basic\BaseModel;
use crmeb\traits\ModelTrait;
class PromoteAmountLog extends BaseModel
{
use ModelTrait;
protected $pk = 'id';
protected $name = 'syj_promote_amount_log';
protected $autoWriteTimestamp = false;
public function setAddTimeAttr(): int
{
return time();
}
}

View File

@@ -0,0 +1,22 @@
<?php
declare(strict_types=1);
namespace app\model\syj;
use crmeb\basic\BaseModel;
use crmeb\traits\ModelTrait;
class PromoteRewardTrigger extends BaseModel
{
use ModelTrait;
protected $pk = 'id';
protected $name = 'syj_promote_reward_trigger';
protected $autoWriteTimestamp = 'int';
protected $createTime = 'add_time';
protected $updateTime = 'update_time';
}

View File

@@ -0,0 +1,22 @@
<?php
declare(strict_types=1);
namespace app\model\syj;
use crmeb\basic\BaseModel;
use crmeb\traits\ModelTrait;
class PromoteSettlement extends BaseModel
{
use ModelTrait;
protected $pk = 'id';
protected $name = 'syj_promote_settlement';
protected $autoWriteTimestamp = 'int';
protected $createTime = 'add_time';
protected $updateTime = 'update_time';
}

View File

@@ -0,0 +1,22 @@
<?php
declare(strict_types=1);
namespace app\model\syj;
use crmeb\basic\BaseModel;
use crmeb\traits\ModelTrait;
class PromoteTask extends BaseModel
{
use ModelTrait;
protected $pk = 'id';
protected $name = 'syj_promote_task';
protected $autoWriteTimestamp = 'int';
protected $createTime = 'add_time';
protected $updateTime = 'update_time';
}

View File

@@ -0,0 +1,22 @@
<?php
declare(strict_types=1);
namespace app\model\syj;
use crmeb\basic\BaseModel;
use crmeb\traits\ModelTrait;
class PromoteTaskRecord extends BaseModel
{
use ModelTrait;
protected $pk = 'id';
protected $name = 'syj_promote_task_record';
protected $autoWriteTimestamp = 'int';
protected $createTime = 'add_time';
protected $updateTime = 'update_time';
}

View File

@@ -0,0 +1,22 @@
<?php
declare(strict_types=1);
namespace app\model\syj;
use crmeb\basic\BaseModel;
use crmeb\traits\ModelTrait;
class PromoteUserAmount extends BaseModel
{
use ModelTrait;
protected $pk = 'id';
protected $name = 'syj_promote_user_amount';
protected $autoWriteTimestamp = 'int';
protected $createTime = 'add_time';
protected $updateTime = 'update_time';
}

View File

@@ -20,6 +20,7 @@ use app\services\hjf\PointsRewardServices;
use app\services\user\UserBillServices;
use app\services\user\UserBrokerageServices;
use app\services\user\UserServices;
use app\services\syj\SyjPromoteTaskServices;
use think\annotation\Inject;
use think\exception\ValidateException;
use think\facade\Log;
@@ -148,6 +149,17 @@ class StoreOrderTakeServices extends BaseServices
}, $isTran);
}
if ($res) {
if (!empty($order['is_queue_goods'])) {
try {
/** @var SyjPromoteTaskServices $syjServices */
$syjServices = app()->make(SyjPromoteTaskServices::class);
$orderData = is_array($order) ? $order : $order->toArray();
$syjServices->handleOrderEffective($orderData, 'order_confirm');
$syjServices->handleRecommendedOrder($orderData, 'order_confirm');
} catch (\Throwable $e) {
\think\facade\Log::error('[SYJ] 确认收货处理失败 order_id=' . ($order['id'] ?? 0) . ': ' . $e->getMessage());
}
}
//订单收货事件
event('order.take', [$order, $storeTitle, $isRecord]);
return true;

View File

@@ -0,0 +1,100 @@
<?php
declare(strict_types=1);
namespace app\services\syj;
use app\services\BaseServices;
use app\services\system\config\SystemConfigServices;
use crmeb\services\SystemConfigService;
use think\exception\ValidateException;
class SyjPromoteConfigServices extends BaseServices
{
public function getConfig(): array
{
return [
'base_amount' => $this->money('syj_task_base_amount', '4333'),
'target_count' => (int)SystemConfigService::get('syj_task_target_count', 4),
'reward_rates' => $this->rewardRates(),
'early_cashout_fee_rate' => $this->money('syj_early_cashout_fee_rate', '7'),
'task_generate_timing' => (string)SystemConfigService::get('syj_task_generate_timing', 'on_confirm'),
'task_order_dedupe' => (string)SystemConfigService::get('syj_task_order_dedupe', 'order'),
'reward_trigger_enable' => (int)SystemConfigService::get('syj_reward_trigger_enable', 1),
'brokerage_timing' => (string)SystemConfigService::get('brokerage_timing', 'on_confirm'),
];
}
public function saveConfig(array $data): void
{
$baseAmount = (float)($data['base_amount'] ?? 0);
$targetCount = (int)($data['target_count'] ?? 0);
$rates = $data['reward_rates'] ?? [];
if (is_string($rates)) {
$rates = json_decode($rates, true) ?: [];
}
$feeRate = (float)($data['early_cashout_fee_rate'] ?? -1);
$generateTiming = (string)($data['task_generate_timing'] ?? 'on_confirm');
if ($baseAmount <= 0) {
throw new ValidateException('任务基准金额必须大于0');
}
if ($targetCount <= 0) {
throw new ValidateException('目标单数必须大于0');
}
if (count($rates) !== $targetCount) {
throw new ValidateException('奖励比例数量必须与目标单数一致');
}
foreach ($rates as $rate) {
if ((float)$rate < 0 || (float)$rate > 100) {
throw new ValidateException('奖励比例必须在0到100之间');
}
}
if ($feeRate < 0 || $feeRate > 100) {
throw new ValidateException('扣费比例必须在0到100之间');
}
if (!in_array($generateTiming, ['on_confirm', 'on_pay'], true)) {
throw new ValidateException('任务生成节点不正确');
}
/** @var SystemConfigServices $configServices */
$configServices = app()->make(SystemConfigServices::class);
$map = [
'syj_task_base_amount' => $this->formatMoney($baseAmount),
'syj_task_target_count' => (string)$targetCount,
'syj_task_reward_rates' => json_encode(array_values(array_map('floatval', $rates)), JSON_UNESCAPED_UNICODE),
'syj_early_cashout_fee_rate' => $this->formatMoney($feeRate),
'syj_task_generate_timing' => $generateTiming,
'syj_task_order_dedupe' => (string)($data['task_order_dedupe'] ?? 'order'),
'syj_reward_trigger_enable' => (string)(int)($data['reward_trigger_enable'] ?? 1),
];
foreach ($map as $key => $value) {
$configServices->setConfig($key, $value);
}
}
public function rewardRates(): array
{
$value = SystemConfigService::get('syj_task_reward_rates', '[10,20,30,40]');
if (is_array($value)) {
return array_values(array_map('floatval', $value));
}
$decoded = json_decode((string)$value, true);
return is_array($decoded) ? array_values(array_map('floatval', $decoded)) : [10, 20, 30, 40];
}
public function rateForStep(int $step): float
{
$rates = $this->rewardRates();
return (float)($rates[$step - 1] ?? 0);
}
private function money(string $key, string $default): string
{
return $this->formatMoney((float)SystemConfigService::get($key, $default));
}
public function formatMoney(float|string $number): string
{
return number_format((float)$number, 2, '.', '');
}
}

View File

@@ -0,0 +1,104 @@
<?php
declare(strict_types=1);
namespace app\services\syj;
use app\dao\syj\PromoteRewardTriggerDao;
use app\services\BaseServices;
use app\services\agent\AgentLevelServices;
use app\services\hjf\PointsRewardServices;
use app\services\user\UserServices;
use think\annotation\Inject;
use think\facade\Log;
class SyjPromoteRewardTriggerServices extends BaseServices
{
#[Inject]
protected PromoteRewardTriggerDao $dao;
public function triggerForTask(array $task): void
{
if ((int)sys_config('syj_reward_trigger_enable', 1) !== 1) {
return;
}
$taskId = (int)$task['id'];
$triggerNo = $task['reward_trigger_no'] ?: 'SYJ-TASK-' . $taskId;
if (!$this->dao->be(['task_id' => $taskId])) {
$this->dao->save([
'task_id' => $taskId,
'uid' => (int)$task['uid'],
'trigger_no' => $triggerNo,
'trigger_amount' => $task['base_amount'],
'brokerage_status' => 0,
'points_status' => 0,
'level_task_status' => 0,
'error_msg' => '',
]);
}
$status = [
'points_status' => 1,
'level_task_status' => 1,
'brokerage_status' => 1,
'error_msg' => '',
];
try {
/** @var UserServices $userServices */
$userServices = app()->make(UserServices::class);
$uids = [(int)$task['uid']];
$user = $userServices->get((int)$task['uid'], ['uid', 'spread_uid', 'agent_level']);
$spreadUid = $user ? (int)($user['spread_uid'] ?? 0) : 0;
if ($spreadUid > 0) {
$uids[] = $spreadUid;
$spreadUser = $userServices->get($spreadUid, ['uid', 'spread_uid', 'agent_level']);
$twoUid = $spreadUser ? (int)($spreadUser['spread_uid'] ?? 0) : 0;
if ($twoUid > 0) {
$uids[] = $twoUid;
}
}
$preUpgradeLevels = [];
foreach (array_unique($uids) as $uid) {
if ($uid <= 0) continue;
$row = $userServices->get((int)$uid, ['uid', 'agent_level']);
$preUpgradeLevels[(int)$uid] = $row ? (int)($row['agent_level'] ?? 0) : 0;
}
/** @var AgentLevelServices $agentLevelServices */
$agentLevelServices = app()->make(AgentLevelServices::class);
$agentLevelServices->checkUserLevelFinish((int)$task['uid'], array_keys($preUpgradeLevels));
} catch (\Throwable $e) {
$status['level_task_status'] = 2;
$status['error_msg'] = '等级任务触发失败:' . $e->getMessage();
Log::error('[SYJ] 等级任务触发失败 task=' . $taskId . ' ' . $e->getMessage());
}
try {
/** @var PointsRewardServices $pointsRewardServices */
$pointsRewardServices = app()->make(PointsRewardServices::class);
$pointsRewardServices->reward((int)$task['uid'], $triggerNo, (int)$task['source_order_id'], $preUpgradeLevels ?? [], 1);
} catch (\Throwable $e) {
$status['points_status'] = 2;
$status['error_msg'] = trim($status['error_msg'] . ' 积分触发失败:' . $e->getMessage());
Log::error('[SYJ] 积分触发失败 task=' . $taskId . ' ' . $e->getMessage());
}
$this->dao->update(['task_id' => $taskId], $status + ['retry_count' => $this->dao->value(['task_id' => $taskId], 'retry_count')]);
}
public function retry(int $taskId): void
{
/** @var SyjPromoteTaskServices $taskServices */
$taskServices = app()->make(SyjPromoteTaskServices::class);
$task = $taskServices->getTask($taskId);
if (!$task) {
return;
}
$row = $this->dao->getOne(['task_id' => $taskId]);
if ($row) {
$this->dao->update(['task_id' => $taskId], ['retry_count' => (int)$row['retry_count'] + 1]);
}
$this->triggerForTask($task);
$taskServices->refreshRewardStatus($taskId);
}
}

View File

@@ -0,0 +1,168 @@
<?php
declare(strict_types=1);
namespace app\services\syj;
use app\dao\syj\PromoteSettlementDao;
use app\dao\syj\PromoteTaskDao;
use app\services\BaseServices;
use app\services\user\UserBrokerageServices;
use app\services\user\UserServices;
use think\annotation\Inject;
use think\exception\ValidateException;
use think\facade\Db;
class SyjPromoteSettlementServices extends BaseServices
{
#[Inject]
protected PromoteSettlementDao $dao;
#[Inject]
protected PromoteTaskDao $taskDao;
public function complete(array $task): array
{
return Db::transaction(function () use ($task) {
$taskId = (int)$task['id'];
if ($this->dao->be(['task_id' => $taskId, 'settle_type' => 'complete'])) {
return $this->dao->getOne(['task_id' => $taskId, 'settle_type' => 'complete'])->toArray();
}
$gross = $this->money($task['base_amount']);
$settlement = $this->dao->save([
'task_id' => $taskId,
'uid' => (int)$task['uid'],
'settle_type' => 'complete',
'gross_amount' => $gross,
'fee_rate' => '0.00',
'fee_amount' => '0.00',
'net_amount' => $gross,
'audit_status' => 1,
'audit_time' => time(),
])->toArray();
$brokerageId = $this->grantBrokerage((int)$task['uid'], $gross, $taskId, '推四免一任务完成结算,任务号:' . $task['task_no']);
$this->dao->update((int)$settlement['id'], ['brokerage_id' => $brokerageId]);
$this->taskDao->update($taskId, ['status' => 1, 'finish_time' => time()]);
return $settlement;
});
}
public function applyCashout(int $uid, int $taskId): array
{
return Db::transaction(function () use ($uid, $taskId) {
$task = $this->taskDao->get($taskId);
if (!$task || (int)$task['uid'] !== $uid) {
throw new ValidateException('推广任务不存在');
}
if ((int)$task['status'] !== 0) {
throw new ValidateException('当前任务状态不可提前兑现');
}
$progress = (int)$task['progress_count'];
if ($progress < 1 || $progress >= (int)$task['target_count']) {
throw new ValidateException('当前进度不可提前兑现');
}
if ($this->dao->be(['task_id' => $taskId, 'settle_type' => 'early_cashout'])) {
throw new ValidateException('已提交提前兑现申请');
}
[$gross, $fee, $net] = $this->calcEarlyCashout($task->toArray());
$settlement = $this->dao->save([
'task_id' => $taskId,
'uid' => $uid,
'settle_type' => 'early_cashout',
'gross_amount' => $gross,
'fee_rate' => $this->money($task['fee_rate']),
'fee_amount' => $fee,
'net_amount' => $net,
'audit_status' => 0,
])->toArray();
$this->taskDao->update($taskId, ['status' => 5]);
return $settlement;
});
}
public function auditCashout(int $settlementId, int $auditUid, int $status, string $remark = ''): void
{
Db::transaction(function () use ($settlementId, $auditUid, $status, $remark) {
$settlement = $this->dao->get($settlementId);
if (!$settlement || $settlement['settle_type'] !== 'early_cashout') {
throw new ValidateException('提前兑现申请不存在');
}
if ((int)$settlement['audit_status'] !== 0) {
throw new ValidateException('申请已审核');
}
$task = $this->taskDao->get((int)$settlement['task_id']);
if (!$task || (int)$task['status'] !== 5) {
throw new ValidateException('任务状态不可审核');
}
if ($status === 1) {
$brokerageId = $this->grantBrokerage((int)$settlement['uid'], (string)$settlement['net_amount'], (int)$task['id'], '推四免一提前兑现到账,任务号:' . $task['task_no']);
$this->dao->update($settlementId, [
'audit_status' => 1,
'audit_uid' => $auditUid,
'audit_remark' => $remark,
'audit_time' => time(),
'brokerage_id' => $brokerageId,
]);
$this->taskDao->update((int)$task['id'], ['status' => 2, 'cashout_time' => time()]);
} elseif ($status === 2) {
$this->dao->update($settlementId, [
'audit_status' => 2,
'audit_uid' => $auditUid,
'audit_remark' => $remark,
'audit_time' => time(),
]);
$this->taskDao->update((int)$task['id'], ['status' => 0]);
} else {
throw new ValidateException('审核状态不正确');
}
});
}
public function listForUser(int $uid, int $page, int $limit): array
{
return $this->dao->getUserList($uid, $page, $limit);
}
public function adminList(array $where, int $page, int $limit): array
{
return $this->dao->getAdminList($where, $page, $limit);
}
private function calcEarlyCashout(array $task): array
{
$rates = json_decode((string)$task['reward_rates'], true);
if (!is_array($rates)) {
$rates = [10, 20, 30, 40];
}
$step = max(1, (int)$task['progress_count']);
$rate = (float)($rates[$step - 1] ?? 0);
$gross = bcmul((string)$task['base_amount'], bcdiv((string)$rate, '100', 4), 2);
$fee = bcmul($gross, bcdiv((string)$task['fee_rate'], '100', 4), 2);
$net = bcsub($gross, $fee, 2);
return [$gross, $fee, $net];
}
private function grantBrokerage(int $uid, string $amount, int $taskId, string $mark): int
{
/** @var UserServices $userServices */
$userServices = app()->make(UserServices::class);
$current = (string)($userServices->value(['uid' => $uid], 'brokerage_price') ?: '0');
$balance = bcadd($current, $amount, 2);
$userServices->bcInc($uid, 'brokerage_price', $amount, 'uid');
/** @var UserBrokerageServices $brokerageServices */
$brokerageServices = app()->make(UserBrokerageServices::class);
$row = $brokerageServices->income('syj_promote_settlement', $uid, [
'number' => $amount,
'mark' => $mark,
], $balance, $taskId);
if ($row && method_exists($row, 'getData')) {
return (int)$row->getData('id');
}
return 0;
}
private function money(float|string $amount): string
{
return number_format((float)$amount, 2, '.', '');
}
}

View File

@@ -0,0 +1,309 @@
<?php
declare(strict_types=1);
namespace app\services\syj;
use app\dao\syj\PromoteAmountLogDao;
use app\dao\syj\PromoteRewardTriggerDao;
use app\dao\syj\PromoteSettlementDao;
use app\dao\syj\PromoteTaskDao;
use app\dao\syj\PromoteTaskRecordDao;
use app\dao\syj\PromoteUserAmountDao;
use app\services\BaseServices;
use think\annotation\Inject;
use think\exception\ValidateException;
use think\facade\Db;
use think\facade\Log;
class SyjPromoteTaskServices extends BaseServices
{
#[Inject]
protected PromoteTaskDao $taskDao;
#[Inject]
protected PromoteUserAmountDao $userAmountDao;
#[Inject]
protected PromoteAmountLogDao $amountLogDao;
#[Inject]
protected PromoteTaskRecordDao $recordDao;
#[Inject]
protected PromoteSettlementDao $settlementDao;
#[Inject]
protected PromoteRewardTriggerDao $triggerDao;
public function handleOrderEffective(array $order, string $sourceType = 'order_confirm'): void
{
if (!$this->isQueueOrder($order) || empty($order['uid'])) {
return;
}
$generateTiming = (string)sys_config('syj_task_generate_timing', 'on_confirm');
if (($generateTiming === 'on_confirm' && $sourceType !== 'order_confirm') || ($generateTiming === 'on_pay' && $sourceType !== 'order_pay')) {
return;
}
Db::transaction(function () use ($order) {
$orderId = (int)$order['id'];
if ($this->amountLogDao->be(['order_id' => $orderId, 'type' => 'income'])) {
return;
}
$uid = (int)$order['uid'];
$amount = $this->validAmount($order);
if (bccomp($amount, '0', 2) <= 0) {
return;
}
$summary = $this->lockUserAmount($uid);
$before = (string)$summary['pending_amount'];
$afterIncome = bcadd($before, $amount, 2);
$config = app()->make(SyjPromoteConfigServices::class)->getConfig();
$base = $config['base_amount'];
$taskCount = (int)floor((float)bcdiv($afterIncome, $base, 4));
$remaining = bcsub($afterIncome, bcmul((string)$taskCount, $base, 2), 2);
$this->amountLogDao->save([
'uid' => $uid,
'order_id' => $orderId,
'order_no' => (string)$order['order_id'],
'amount' => $amount,
'before_amount' => $before,
'after_amount' => $afterIncome,
'type' => 'income',
'mark' => '有效消费计入待推广金额',
]);
$this->userAmountDao->update((int)$summary['id'], ['pending_amount' => $remaining]);
for ($i = 1; $i <= $taskCount; $i++) {
$task = $this->createTask($uid, $order, $i, $config);
$this->amountLogDao->save([
'uid' => $uid,
'order_id' => $orderId,
'order_no' => (string)$order['order_id'],
'amount' => '-' . $base,
'before_amount' => $afterIncome,
'after_amount' => $remaining,
'type' => 'consume_task_' . $i,
'link_id' => (int)$task['id'],
'mark' => '生成推四免一推广任务',
]);
}
});
}
public function handleRecommendedOrder(array $order, string $sourceType): void
{
if (!$this->isQueueOrder($order) || empty($order['spread_uid'])) {
return;
}
$timing = (string)sys_config('brokerage_timing', 'on_confirm');
if (($timing === 'on_pay' && $sourceType !== 'order_pay') || ($timing === 'on_confirm' && $sourceType !== 'order_confirm')) {
return;
}
Db::transaction(function () use ($order, $sourceType, $timing) {
$orderId = (int)$order['id'];
if ($this->recordDao->be(['order_id' => $orderId])) {
return;
}
$task = $this->taskDao->getEarliestActiveTask((int)$order['spread_uid']);
if (!$task) {
return;
}
$step = (int)$task['progress_count'] + 1;
$rates = json_decode((string)$task['reward_rates'], true);
$rate = (float)($rates[$step - 1] ?? 0);
$this->recordDao->save([
'task_id' => (int)$task['id'],
'uid' => (int)$task['uid'],
'order_uid' => (int)$order['uid'],
'order_id' => $orderId,
'order_no' => (string)$order['order_id'],
'source_type' => $sourceType,
'trigger_timing' => $timing,
'step_no' => $step,
'reward_rate' => $rate,
'status' => 1,
]);
$this->taskDao->update((int)$task['id'], ['progress_count' => $step]);
$task['progress_count'] = $step;
if ($step >= (int)$task['target_count']) {
app()->make(SyjPromoteSettlementServices::class)->complete($task);
}
});
}
public function handleRefund(array $order, array $refundData = []): void
{
Db::transaction(function () use ($order, $refundData) {
$orderId = (int)$order['id'];
$sourceTasks = $this->taskDao->getColumn(['source_order_id' => $orderId], 'id,status', 'id');
if ($sourceTasks) {
foreach ($sourceTasks as $taskId => $status) {
if ((int)$status === 0) {
$this->taskDao->update((int)$taskId, ['status' => 4, 'exception_reason' => '来源订单退款']);
}
}
}
$record = $this->recordDao->getOne(['order_id' => $orderId, 'status' => 1]);
if ($record) {
$task = $this->taskDao->get((int)$record['task_id']);
$this->recordDao->update((int)$record['id'], ['status' => 2]);
if ($task && (int)$task['status'] === 0) {
$this->taskDao->update((int)$task['id'], ['progress_count' => max(0, (int)$task['progress_count'] - 1)]);
} elseif ($task) {
$this->taskDao->update((int)$task['id'], ['status' => 4, 'exception_reason' => '推荐订单退款,需人工复核']);
}
}
});
}
public function overview(int $uid): array
{
$summary = $this->userAmountDao->getOne(['uid' => $uid]);
$pending = $summary ? (string)$summary['pending_amount'] : '0.00';
$base = (string)sys_config('syj_task_base_amount', '4333');
return [
'pending_amount' => $this->money($pending),
'base_amount' => $this->money($base),
'active_task_count' => $this->taskDao->count(['uid' => $uid, 'status' => 0]),
'completed_task_count' => $this->taskDao->count(['uid' => $uid, 'status' => 1]),
'cashout_task_count' => $this->taskDao->count(['uid' => $uid, 'status' => 2]),
'available_cashout_amount' => $this->availableCashoutAmount($uid),
];
}
public function userTasks(int $uid, array $where, int $page, int $limit): array
{
return $this->taskDao->getUserList($uid, $where, $page, $limit);
}
public function adminTasks(array $where, int $page, int $limit): array
{
return $this->taskDao->getAdminList($where, $page, $limit);
}
public function getTask(int $taskId): ?array
{
$row = $this->taskDao->get($taskId);
return $row ? $row->toArray() : null;
}
public function userTaskDetail(int $uid, int $taskId): array
{
$task = $this->getTask($taskId);
if (!$task || (int)$task['uid'] !== $uid) {
throw new ValidateException('推广任务不存在');
}
return $this->decorateTask($task);
}
public function adminTaskDetail(int $taskId): array
{
$task = $this->getTask($taskId);
if (!$task) {
throw new ValidateException('推广任务不存在');
}
return $this->decorateTask($task);
}
public function records(int $taskId, int $uid = 0): array
{
return $this->recordDao->getTaskRecords($taskId, $uid);
}
public function amountLogs(int $uid, int $page, int $limit): array
{
return $this->amountLogDao->getUserList($uid, $page, $limit);
}
public function refreshRewardStatus(int $taskId): void
{
$trigger = $this->triggerDao->getOne(['task_id' => $taskId]);
if (!$trigger) {
return;
}
$failed = in_array(2, [(int)$trigger['points_status'], (int)$trigger['level_task_status'], (int)$trigger['brokerage_status']], true);
$success = (int)$trigger['points_status'] === 1 && (int)$trigger['level_task_status'] === 1 && (int)$trigger['brokerage_status'] === 1;
$this->taskDao->update($taskId, ['reward_trigger_status' => $success ? 1 : ($failed ? 2 : 3)]);
}
private function createTask(int $uid, array $order, int $splitIndex, array $config): array
{
$taskNo = 'SYJ' . date('YmdHis') . $uid . str_pad((string)$splitIndex, 2, '0', STR_PAD_LEFT) . mt_rand(100, 999);
$row = $this->taskDao->save([
'uid' => $uid,
'task_no' => $taskNo,
'source_order_id' => (int)$order['id'],
'source_order_no' => (string)$order['order_id'],
'source_split_index' => $splitIndex,
'base_amount' => $config['base_amount'],
'reward_rates' => json_encode($config['reward_rates'], JSON_UNESCAPED_UNICODE),
'fee_rate' => $config['early_cashout_fee_rate'],
'status' => 0,
'progress_count' => 0,
'target_count' => $config['target_count'],
'reward_trigger_status' => 3,
'reward_trigger_no' => $taskNo,
])->toArray();
try {
app()->make(SyjPromoteRewardTriggerServices::class)->triggerForTask($row);
$this->refreshRewardStatus((int)$row['id']);
} catch (\Throwable $e) {
Log::error('[SYJ] 任务奖励触发异常 task=' . $row['id'] . ' ' . $e->getMessage());
$this->taskDao->update((int)$row['id'], ['reward_trigger_status' => 2, 'exception_reason' => $e->getMessage()]);
}
return $row;
}
private function lockUserAmount(int $uid): array
{
$row = $this->userAmountDao->lockByUid($uid);
if (!$row) {
$this->userAmountDao->save(['uid' => $uid, 'pending_amount' => '0.00']);
$row = $this->userAmountDao->lockByUid($uid);
}
return $row;
}
private function isQueueOrder(array $order): bool
{
return !empty($order['is_queue_goods']) && empty($order['is_del']) && empty($order['is_system_del']) && (int)($order['paid'] ?? 1) === 1;
}
private function validAmount(array $order): string
{
$pay = (string)($order['pay_price'] ?? '0');
$refund = (string)($order['refund_price'] ?? '0');
$amount = bcsub($pay, $refund, 2);
return bccomp($amount, '0', 2) > 0 ? $amount : '0.00';
}
private function decorateTask(array $task): array
{
$task['records'] = $this->recordDao->getTaskRecords((int)$task['id']);
$task['settlement'] = ($this->settlementDao->getOne(['task_id' => (int)$task['id']]) ?: null)?->toArray();
$task['reward_trigger'] = ($this->triggerDao->getOne(['task_id' => (int)$task['id']]) ?: null)?->toArray();
return $task;
}
private function availableCashoutAmount(int $uid): string
{
$tasks = $this->taskDao->getCashoutAvailableTasks($uid);
$total = '0.00';
foreach ($tasks as $task) {
$rates = json_decode((string)$task['reward_rates'], true) ?: [10, 20, 30, 40];
$rate = (float)($rates[((int)$task['progress_count']) - 1] ?? 0);
$gross = bcmul((string)$task['base_amount'], bcdiv((string)$rate, '100', 4), 2);
$fee = bcmul($gross, bcdiv((string)$task['fee_rate'], '100', 4), 2);
$total = bcadd($total, bcsub($gross, $fee, 2), 2);
}
return $total;
}
private function money(float|string $amount): string
{
return number_format((float)$amount, 2, '.', '');
}
}

View File

@@ -324,6 +324,25 @@ class SystemConfigServices extends BaseServices implements ServeConfigInterface
}
}
/**
* 更新单个系统配置值
* @param string $configName
* @param mixed $value
* @return bool
*/
public function setConfig(string $configName, $value): bool
{
$config = $this->dao->getOne(['menu_name' => $configName]);
if (!$config) {
return false;
}
$config['value'] = $value;
$this->valiDateValue($config);
$this->dao->update($configName, ['value' => json_encode($value)], 'menu_name');
\crmeb\services\SystemConfigService::clear();
return true;
}
/**
* 获取配置并分页
* @param array $where

View File

@@ -95,6 +95,13 @@ class UserBrokerageServices extends BaseServices
'status' => 1,
'pm' => 0
],
'syj_promote_settlement' => [
'title' => '芍药居推广任务结算',
'type' => 'syj_promote_settlement',
'mark' => '{%mark%}',
'status' => 1,
'pm' => 1
],
'get_staff_brokerage' => [
'title' => '获得员工推广订单佣金',
'type' => 'staff_brokerage',