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, '.', ''); } }