310 lines
12 KiB
PHP
310 lines
12 KiB
PHP
<?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, '.', '');
|
|
}
|
|
}
|