From 8d20c59761234763b81f718b8590a01a338f97f8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=9F=B3=E6=B8=85=E7=88=BD?= Date: Mon, 7 Apr 2025 09:47:07 +0800 Subject: [PATCH] =?UTF-8?q?=E8=AE=A1=E5=88=92=E4=BB=BB=E5=8A=A1=E5=8F=8A?= =?UTF-8?q?=E8=8E=B7=E5=AE=A2=E5=9C=BA=E6=99=AF=E9=80=BB=E8=BE=91=E6=A1=86?= =?UTF-8?q?=E6=9E=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Server/application/plan/config/task.php | 52 ++ Server/application/plan/controller/Scene.php | 2 +- Server/application/plan/controller/Tag.php | 247 +++++++ Server/application/plan/controller/Task.php | 370 ++++++++++ .../application/plan/controller/Traffic.php | 380 ++++++++++ .../application/plan/model/PlanExecution.php | 164 +++++ Server/application/plan/model/PlanTask.php | 149 ++++ Server/application/plan/model/Tag.php | 125 ++++ Server/application/plan/model/TrafficPool.php | 138 ++++ .../application/plan/model/TrafficSource.php | 151 ++++ Server/application/plan/route.php | 30 + .../application/plan/service/SceneHandler.php | 239 +++++++ .../application/plan/service/TaskRunner.php | 675 ++++++++++++++++++ Server/application/plan/sql/tables.sql | 92 +++ Server/application/plan/validate/Task.php | 48 ++ Server/application/plan/validate/Traffic.php | 51 ++ 16 files changed, 2912 insertions(+), 1 deletion(-) create mode 100644 Server/application/plan/config/task.php create mode 100644 Server/application/plan/controller/Tag.php create mode 100644 Server/application/plan/controller/Task.php create mode 100644 Server/application/plan/controller/Traffic.php create mode 100644 Server/application/plan/model/PlanExecution.php create mode 100644 Server/application/plan/model/PlanTask.php create mode 100644 Server/application/plan/model/Tag.php create mode 100644 Server/application/plan/model/TrafficPool.php create mode 100644 Server/application/plan/model/TrafficSource.php create mode 100644 Server/application/plan/route.php create mode 100644 Server/application/plan/service/SceneHandler.php create mode 100644 Server/application/plan/service/TaskRunner.php create mode 100644 Server/application/plan/sql/tables.sql create mode 100644 Server/application/plan/validate/Task.php create mode 100644 Server/application/plan/validate/Traffic.php diff --git a/Server/application/plan/config/task.php b/Server/application/plan/config/task.php new file mode 100644 index 00000000..2de41b90 --- /dev/null +++ b/Server/application/plan/config/task.php @@ -0,0 +1,52 @@ + 'ahswhdlkOBiHncDhaoYH98WB', + + // 任务执行超时时间(秒) + 'task_timeout' => 300, + + // 每次执行获取的任务数 + 'batch_size' => 5, + + // 任务状态 + 'status' => [ + 0 => '停用', + 1 => '启用', + 2 => '执行中', + 3 => '完成' + ], + + // 任务步骤 + 'steps' => [ + 0 => '基础配置', + 1 => '添加好友', + 2 => 'API调用', + 3 => '标签处理' + ], + + // 执行状态 + 'execution_status' => [ + 0 => '等待', + 1 => '进行中', + 2 => '成功', + 3 => '失败' + ], + + // API配置 + 'api' => [ + 'base_url' => 'https://api.example.com', + 'timeout' => 30, + 'retry' => 3 + ], + + // 任务日志 + 'log' => [ + 'path' => app()->getRuntimePath() . 'log/task', + 'level' => 'info' + ] +]; \ No newline at end of file diff --git a/Server/application/plan/controller/Scene.php b/Server/application/plan/controller/Scene.php index e9897986..8ec9141e 100644 --- a/Server/application/plan/controller/Scene.php +++ b/Server/application/plan/controller/Scene.php @@ -2,7 +2,7 @@ namespace app\plan\controller; use think\Controller; -use think\Request; +use think\facade\Request; use app\plan\model\PlanScene; /** diff --git a/Server/application/plan/controller/Tag.php b/Server/application/plan/controller/Tag.php new file mode 100644 index 00000000..12a0ff8d --- /dev/null +++ b/Server/application/plan/controller/Tag.php @@ -0,0 +1,247 @@ + 200, + 'msg' => '获取成功', + 'data' => $tags + ]); + } + + /** + * 创建标签 + * + * @return \think\response\Json + */ + public function save() + { + $data = Request::post(); + + // 数据验证 + if (empty($data['name']) || empty($data['type'])) { + return json([ + 'code' => 400, + 'msg' => '缺少必要参数' + ]); + } + + try { + // 创建或获取标签 + $tagId = TagModel::getOrCreate($data['name'], $data['type']); + + return json([ + 'code' => 200, + 'msg' => '创建成功', + 'data' => $tagId + ]); + + } catch (\Exception $e) { + Log::error('创建标签异常', [ + 'data' => $data, + 'error' => $e->getMessage(), + 'trace' => $e->getTraceAsString() + ]); + + return json([ + 'code' => 500, + 'msg' => '创建失败:' . $e->getMessage() + ]); + } + } + + /** + * 批量创建标签 + * + * @return \think\response\Json + */ + public function batchCreate() + { + $data = Request::post(); + + // 数据验证 + if (empty($data['names']) || empty($data['type'])) { + return json([ + 'code' => 400, + 'msg' => '缺少必要参数' + ]); + } + + // 检查名称数组 + if (!is_array($data['names'])) { + return json([ + 'code' => 400, + 'msg' => '标签名称必须是数组' + ]); + } + + try { + $result = []; + + // 批量处理标签 + foreach ($data['names'] as $name) { + $name = trim($name); + if (empty($name)) continue; + + $tagId = TagModel::getOrCreate($name, $data['type']); + $result[] = [ + 'id' => $tagId, + 'name' => $name, + 'type' => $data['type'] + ]; + } + + return json([ + 'code' => 200, + 'msg' => '创建成功', + 'data' => $result + ]); + + } catch (\Exception $e) { + Log::error('批量创建标签异常', [ + 'data' => $data, + 'error' => $e->getMessage(), + 'trace' => $e->getTraceAsString() + ]); + + return json([ + 'code' => 500, + 'msg' => '创建失败:' . $e->getMessage() + ]); + } + } + + /** + * 更新标签 + * + * @param int $id + * @return \think\response\Json + */ + public function update($id) + { + $data = Request::put(); + + // 检查标签是否存在 + $tag = TagModel::get($id); + if (!$tag) { + return json([ + 'code' => 404, + 'msg' => '标签不存在' + ]); + } + + // 准备更新数据 + $updateData = []; + + // 只允许更新特定字段 + $allowedFields = ['name', 'status']; + foreach ($allowedFields as $field) { + if (isset($data[$field])) { + $updateData[$field] = $data[$field]; + } + } + + // 更新标签 + $tag->save($updateData); + + // 如果更新了标签名称,且该标签有使用次数,则增加计数 + if (isset($updateData['name']) && $updateData['name'] != $tag->name && $tag->count > 0) { + $tag->updateCount(1); + } + + return json([ + 'code' => 200, + 'msg' => '更新成功' + ]); + } + + /** + * 删除标签 + * + * @param int $id + * @return \think\response\Json + */ + public function delete($id) + { + // 检查标签是否存在 + $tag = TagModel::get($id); + if (!$tag) { + return json([ + 'code' => 404, + 'msg' => '标签不存在' + ]); + } + + // 更新状态为删除 + $tag->save([ + 'status' => 0 + ]); + + return json([ + 'code' => 200, + 'msg' => '删除成功' + ]); + } + + /** + * 获取标签名称 + * + * @return \think\response\Json + */ + public function getNames() + { + $ids = Request::param('ids'); + + // 验证参数 + if (empty($ids)) { + return json([ + 'code' => 400, + 'msg' => '缺少标签ID参数' + ]); + } + + // 处理参数 + if (is_string($ids)) { + $ids = explode(',', $ids); + } + + // 获取标签名称 + $names = TagModel::getTagNames($ids); + + return json([ + 'code' => 200, + 'msg' => '获取成功', + 'data' => $names + ]); + } +} \ No newline at end of file diff --git a/Server/application/plan/controller/Task.php b/Server/application/plan/controller/Task.php new file mode 100644 index 00000000..2ab6559e --- /dev/null +++ b/Server/application/plan/controller/Task.php @@ -0,0 +1,370 @@ +scene ? $task->scene->toArray() : null; + $task['device'] = $task->device ? $task->device->toArray() : null; + } + + return json([ + 'code' => 200, + 'msg' => '获取成功', + 'data' => $result + ]); + } + + /** + * 获取任务详情 + * + * @param int $id + * @return \think\response\Json + */ + public function read($id) + { + $task = PlanTask::get($id, ['scene', 'device']); + if (!$task) { + return json([ + 'code' => 404, + 'msg' => '任务不存在' + ]); + } + + // 获取执行记录 + $executions = PlanExecution::where('plan_id', $id) + ->order('createTime DESC') + ->select(); + + return json([ + 'code' => 200, + 'msg' => '获取成功', + 'data' => [ + 'task' => $task, + 'executions' => $executions + ] + ]); + } + + /** + * 创建任务 + * + * @return \think\response\Json + */ + public function save() + { + $data = Request::post(); + + // 数据验证 + $validate = validate('app\plan\validate\Task'); + if (!$validate->check($data)) { + return json([ + 'code' => 400, + 'msg' => $validate->getError() + ]); + } + + // 添加任务 + $task = new PlanTask; + $task->save([ + 'name' => $data['name'], + 'device_id' => $data['device_id'] ?? null, + 'scene_id' => $data['scene_id'] ?? null, + 'scene_config' => $data['scene_config'] ?? [], + 'status' => $data['status'] ?? 0, + 'current_step' => 0, + 'priority' => $data['priority'] ?? 5, + 'created_by' => $data['created_by'] ?? 0 + ]); + + return json([ + 'code' => 200, + 'msg' => '创建成功', + 'data' => $task->id + ]); + } + + /** + * 更新任务 + * + * @param int $id + * @return \think\response\Json + */ + public function update($id) + { + $data = Request::put(); + + // 检查任务是否存在 + $task = PlanTask::get($id); + if (!$task) { + return json([ + 'code' => 404, + 'msg' => '任务不存在' + ]); + } + + // 准备更新数据 + $updateData = []; + + // 只允许更新特定字段 + $allowedFields = ['name', 'device_id', 'scene_id', 'scene_config', 'status', 'priority']; + foreach ($allowedFields as $field) { + if (isset($data[$field])) { + $updateData[$field] = $data[$field]; + } + } + + // 更新任务 + $task->save($updateData); + + return json([ + 'code' => 200, + 'msg' => '更新成功' + ]); + } + + /** + * 删除任务 + * + * @param int $id + * @return \think\response\Json + */ + public function delete($id) + { + // 检查任务是否存在 + $task = PlanTask::get($id); + if (!$task) { + return json([ + 'code' => 404, + 'msg' => '任务不存在' + ]); + } + + // 软删除任务 + $task->delete(); + + return json([ + 'code' => 200, + 'msg' => '删除成功' + ]); + } + + /** + * 启动任务 + * + * @param int $id + * @return \think\response\Json + */ + public function start($id) + { + // 检查任务是否存在 + $task = PlanTask::get($id); + if (!$task) { + return json([ + 'code' => 404, + 'msg' => '任务不存在' + ]); + } + + // 更新状态为启用 + $task->save([ + 'status' => 1, + 'current_step' => 0 + ]); + + return json([ + 'code' => 200, + 'msg' => '任务已启动' + ]); + } + + /** + * 停止任务 + * + * @param int $id + * @return \think\response\Json + */ + public function stop($id) + { + // 检查任务是否存在 + $task = PlanTask::get($id); + if (!$task) { + return json([ + 'code' => 404, + 'msg' => '任务不存在' + ]); + } + + // 更新状态为停用 + $task->save([ + 'status' => 0 + ]); + + return json([ + 'code' => 200, + 'msg' => '任务已停止' + ]); + } + + /** + * 执行定时任务(供外部调用) + * + * @return \think\response\Json + */ + public function cron() + { + // 获取密钥 + $key = Request::param('key', ''); + + // 验证密钥(实际生产环境应当使用更安全的验证方式) + if ($key !== config('task.cron_key')) { + return json([ + 'code' => 403, + 'msg' => '访问密钥无效' + ]); + } + + try { + // 获取待执行的任务 + $tasks = PlanTask::getPendingTasks(5); + if ($tasks->isEmpty()) { + return json([ + 'code' => 200, + 'msg' => '没有需要执行的任务', + 'data' => [] + ]); + } + + $results = []; + + // 逐一执行任务 + foreach ($tasks as $task) { + $runner = new TaskRunner($task); + $result = $runner->run(); + + $results[] = [ + 'task_id' => $task->id, + 'name' => $task->name, + 'result' => $result + ]; + + // 记录执行信息 + Log::info('任务执行', [ + 'task_id' => $task->id, + 'name' => $task->name, + 'result' => $result + ]); + } + + return json([ + 'code' => 200, + 'msg' => '任务执行完成', + 'data' => $results + ]); + + } catch (\Exception $e) { + Log::error('任务执行异常', [ + 'error' => $e->getMessage(), + 'trace' => $e->getTraceAsString() + ]); + + return json([ + 'code' => 500, + 'msg' => '任务执行异常:' . $e->getMessage() + ]); + } + } + + /** + * 手动执行任务 + * + * @param int $id + * @return \think\response\Json + */ + public function execute($id) + { + // 检查任务是否存在 + $task = PlanTask::get($id); + if (!$task) { + return json([ + 'code' => 404, + 'msg' => '任务不存在' + ]); + } + + try { + // 执行任务 + $runner = new TaskRunner($task); + $result = $runner->run(); + + // 记录执行信息 + Log::info('手动执行任务', [ + 'task_id' => $task->id, + 'name' => $task->name, + 'result' => $result + ]); + + return json([ + 'code' => 200, + 'msg' => '任务执行完成', + 'data' => $result + ]); + + } catch (\Exception $e) { + Log::error('手动执行任务异常', [ + 'task_id' => $task->id, + 'error' => $e->getMessage(), + 'trace' => $e->getTraceAsString() + ]); + + return json([ + 'code' => 500, + 'msg' => '任务执行异常:' . $e->getMessage() + ]); + } + } +} \ No newline at end of file diff --git a/Server/application/plan/controller/Traffic.php b/Server/application/plan/controller/Traffic.php new file mode 100644 index 00000000..378c1bee --- /dev/null +++ b/Server/application/plan/controller/Traffic.php @@ -0,0 +1,380 @@ + 200, + 'msg' => '获取成功', + 'data' => $result + ]); + } + + /** + * 获取流量详情 + * + * @param int $id + * @return \think\response\Json + */ + public function read($id) + { + $traffic = TrafficPool::get($id); + if (!$traffic) { + return json([ + 'code' => 404, + 'msg' => '流量记录不存在' + ]); + } + + // 获取流量来源 + $sources = TrafficSource::getSourcesByTrafficId($id); + + return json([ + 'code' => 200, + 'msg' => '获取成功', + 'data' => [ + 'traffic' => $traffic, + 'sources' => $sources + ] + ]); + } + + /** + * 创建或更新流量 + * + * @return \think\response\Json + */ + public function save() + { + $data = Request::post(); + + // 数据验证 + $validate = validate('app\plan\validate\Traffic'); + if (!$validate->check($data)) { + return json([ + 'code' => 400, + 'msg' => $validate->getError() + ]); + } + + try { + // 添加或更新流量 + $result = TrafficPool::addOrUpdateTraffic( + $data['mobile'], + $data['gender'] ?? 0, + $data['age'] ?? 0, + $data['tags'] ?? '', + $data['province'] ?? '', + $data['city'] ?? '', + $data['source_channel'] ?? '', + $data['source_detail'] ?? [] + ); + + return json([ + 'code' => 200, + 'msg' => '保存成功', + 'data' => $result + ]); + + } catch (\Exception $e) { + Log::error('保存流量记录异常', [ + 'data' => $data, + 'error' => $e->getMessage(), + 'trace' => $e->getTraceAsString() + ]); + + return json([ + 'code' => 500, + 'msg' => '保存失败:' . $e->getMessage() + ]); + } + } + + /** + * 更新流量记录 + * + * @param int $id + * @return \think\response\Json + */ + public function update($id) + { + $data = Request::put(); + + // 检查流量记录是否存在 + $traffic = TrafficPool::get($id); + if (!$traffic) { + return json([ + 'code' => 404, + 'msg' => '流量记录不存在' + ]); + } + + // 准备更新数据 + $updateData = []; + + // 只允许更新特定字段 + $allowedFields = ['gender', 'age', 'tags', 'province', 'city', 'status']; + foreach ($allowedFields as $field) { + if (isset($data[$field])) { + $updateData[$field] = $data[$field]; + } + } + + // 更新流量记录 + $traffic->save($updateData); + + return json([ + 'code' => 200, + 'msg' => '更新成功' + ]); + } + + /** + * 删除流量记录 + * + * @param int $id + * @return \think\response\Json + */ + public function delete($id) + { + // 检查流量记录是否存在 + $traffic = TrafficPool::get($id); + if (!$traffic) { + return json([ + 'code' => 404, + 'msg' => '流量记录不存在' + ]); + } + + // 更新状态为无效 + $traffic->save([ + 'status' => 0 + ]); + + return json([ + 'code' => 200, + 'msg' => '删除成功' + ]); + } + + /** + * 获取流量来源统计 + * + * @return \think\response\Json + */ + public function sourceStats() + { + $channel = Request::param('channel', ''); + $planId = Request::param('plan_id', 0, 'intval'); + $sceneId = Request::param('scene_id', 0, 'intval'); + $startDate = Request::param('start_date', '', 'trim'); + $endDate = Request::param('end_date', '', 'trim'); + + // 获取统计数据 + $stats = TrafficSource::getSourceStats($channel, $planId, $sceneId, $startDate, $endDate); + + return json([ + 'code' => 200, + 'msg' => '获取成功', + 'data' => $stats + ]); + } + + /** + * 处理外部流量 + * + * @return \think\response\Json + */ + public function handleExternalTraffic() + { + $data = Request::post(); + + // 验证必要参数 + if (empty($data['scene_id']) || empty($data['mobile'])) { + return json([ + 'code' => 400, + 'msg' => '缺少必要参数' + ]); + } + + try { + // 获取场景处理器 + $handler = SceneHandler::getHandler($data['scene_id']); + + // 根据场景类型处理流量 + switch ($data['scene_type'] ?? '') { + case 'poster': + $result = $handler->handlePosterScan($data['mobile'], $data); + break; + + case 'order': + $result = $handler->handleOrderImport($data['orders'] ?? []); + break; + + default: + $result = $handler->handleChannelTraffic($data['mobile'], $data['channel'] ?? '', $data); + } + + return json([ + 'code' => 200, + 'msg' => '处理成功', + 'data' => $result + ]); + + } catch (\Exception $e) { + Log::error('处理外部流量异常', [ + 'data' => $data, + 'error' => $e->getMessage(), + 'trace' => $e->getTraceAsString() + ]); + + return json([ + 'code' => 500, + 'msg' => '处理失败:' . $e->getMessage() + ]); + } + } + + /** + * 批量导入流量 + * + * @return \think\response\Json + */ + public function importTraffic() + { + // 检查是否上传了文件 + $file = Request::file('file'); + if (!$file) { + return json([ + 'code' => 400, + 'msg' => '未上传文件' + ]); + } + + // 检查文件类型,只允许csv或xlsx + $fileExt = strtolower($file->getOriginalExtension()); + if (!in_array($fileExt, ['csv', 'xlsx'])) { + return json([ + 'code' => 400, + 'msg' => '仅支持CSV或XLSX格式文件' + ]); + } + + try { + // 处理上传文件 + $saveName = \think\facade\Filesystem::disk('upload')->putFile('traffic', $file); + $filePath = app()->getRuntimePath() . 'storage/upload/' . $saveName; + + // 读取文件内容并导入 + $results = []; + $success = 0; + $fail = 0; + + // 这里简化处理,实际应当使用专业的Excel/CSV解析库 + if ($fileExt == 'csv') { + $handle = fopen($filePath, 'r'); + + // 跳过标题行 + fgetcsv($handle); + + while (($data = fgetcsv($handle)) !== false) { + if (count($data) < 1) continue; + + $mobile = trim($data[0]); + // 验证手机号 + if (!preg_match('/^1[3-9]\d{9}$/', $mobile)) { + $fail++; + continue; + } + + // 添加或更新流量 + TrafficPool::addOrUpdateTraffic( + $mobile, + isset($data[1]) ? intval($data[1]) : 0, // 性别 + isset($data[2]) ? intval($data[2]) : 0, // 年龄 + isset($data[3]) ? $data[3] : '', // 标签 + isset($data[4]) ? $data[4] : '', // 省份 + isset($data[5]) ? $data[5] : '', // 城市 + 'import', // 来源渠道 + ['detail' => '批量导入'] // 来源详情 + ); + + $success++; + } + + fclose($handle); + } else { + // 处理xlsx文件,实际应当使用专业的Excel解析库 + // 此处代码省略,依赖于具体的Excel解析库 + } + + return json([ + 'code' => 200, + 'msg' => '导入完成', + 'data' => [ + 'success' => $success, + 'fail' => $fail + ] + ]); + + } catch (\Exception $e) { + Log::error('批量导入流量异常', [ + 'error' => $e->getMessage(), + 'trace' => $e->getTraceAsString() + ]); + + return json([ + 'code' => 500, + 'msg' => '导入失败:' . $e->getMessage() + ]); + } + } +} \ No newline at end of file diff --git a/Server/application/plan/model/PlanExecution.php b/Server/application/plan/model/PlanExecution.php new file mode 100644 index 00000000..e7be622e --- /dev/null +++ b/Server/application/plan/model/PlanExecution.php @@ -0,0 +1,164 @@ + 'integer', + 'plan_id' => 'integer', + 'traffic_id' => 'integer', + 'step' => 'integer', + 'status' => 'integer', + 'result' => 'json', + 'start_time' => 'integer', + 'end_time' => 'integer', + 'createTime' => 'integer', + 'updateTime' => 'integer' + ]; + + /** + * 状态文本获取器 + * @param int $value 状态值 + * @return string 状态文本 + */ + public function getStatusTextAttr($value, $data) + { + $status = [ + 0 => '等待', + 1 => '进行中', + 2 => '成功', + 3 => '失败' + ]; + return isset($status[$data['status']]) ? $status[$data['status']] : '未知'; + } + + /** + * 步骤文本获取器 + * @param int $value 步骤值 + * @return string 步骤文本 + */ + public function getStepTextAttr($value, $data) + { + $steps = [ + 1 => '基础配置', + 2 => '加友计划', + 3 => 'API调用', + 4 => '标签处理' + ]; + return isset($steps[$data['step']]) ? $steps[$data['step']] : '未知'; + } + + /** + * 创建执行记录 + * @param int $planId 计划ID + * @param int $step 步骤 + * @param array $data 额外数据 + * @return int 新增记录ID + */ + public static function createExecution($planId, $step, $data = []) + { + $model = new self(); + $model->save(array_merge([ + 'plan_id' => $planId, + 'step' => $step, + 'status' => 0, // 等待状态 + 'start_time' => time() + ], $data)); + + return $model->id; + } + + /** + * 更新执行状态 + * @param int $id 记录ID + * @param int $status 状态 + * @param array $data 额外数据 + * @return bool 更新结果 + */ + public static function updateExecution($id, $status, $data = []) + { + $updateData = array_merge([ + 'status' => $status + ], $data); + + // 如果是完成或失败状态,添加结束时间 + if ($status == 2 || $status == 3) { + $updateData['end_time'] = time(); + } + + return self::where('id', $id)->update($updateData); + } + + /** + * 获取计划的执行记录 + * @param int $planId 计划ID + * @param int $step 步骤 + * @return array 执行记录 + */ + public static function getPlanExecutions($planId, $step = null) + { + $where = [ + ['plan_id', '=', $planId] + ]; + + if ($step !== null) { + $where[] = ['step', '=', $step]; + } + + return self::where($where) + ->order('createTime DESC') + ->select(); + } + + /** + * 获取最近的执行记录 + * @param int $planId 计划ID + * @param int $step 步骤 + * @return array|null 执行记录 + */ + public static function getLatestExecution($planId, $step) + { + return self::where([ + ['plan_id', '=', $planId], + ['step', '=', $step] + ]) + ->order('createTime DESC') + ->find(); + } + + /** + * 关联计划 + */ + public function plan() + { + return $this->belongsTo('PlanTask', 'plan_id'); + } + + /** + * 关联流量 + */ + public function traffic() + { + return $this->belongsTo('TrafficPool', 'traffic_id'); + } +} \ No newline at end of file diff --git a/Server/application/plan/model/PlanTask.php b/Server/application/plan/model/PlanTask.php new file mode 100644 index 00000000..df854b42 --- /dev/null +++ b/Server/application/plan/model/PlanTask.php @@ -0,0 +1,149 @@ + 'integer', + 'device_id' => 'integer', + 'scene_id' => 'integer', + 'scene_config' => 'json', + 'status' => 'integer', + 'current_step' => 'integer', + 'priority' => 'integer', + 'created_by' => 'integer', + 'createTime' => 'integer', + 'updateTime' => 'integer', + 'deleteTime' => 'integer' + ]; + + /** + * 状态文本获取器 + * @param int $value 状态值 + * @return string 状态文本 + */ + public function getStatusTextAttr($value, $data) + { + $status = [ + 0 => '停用', + 1 => '启用', + 2 => '完成', + 3 => '失败' + ]; + return isset($status[$data['status']]) ? $status[$data['status']] : '未知'; + } + + /** + * 获取待执行的任务列表 + * @param int $limit 限制数量 + * @return array 任务列表 + */ + public static function getPendingTasks($limit = 10) + { + return self::where('status', 1) + ->order('priority DESC, id ASC') + ->limit($limit) + ->select(); + } + + /** + * 更新任务状态 + * @param int $id 任务ID + * @param int $status 新状态 + * @param int $currentStep 当前步骤 + * @return bool 更新结果 + */ + public static function updateTaskStatus($id, $status, $currentStep = null) + { + $data = ['status' => $status]; + if ($currentStep !== null) { + $data['current_step'] = $currentStep; + } + + return self::where('id', $id)->update($data); + } + + /** + * 获取任务详情 + * @param int $id 任务ID + * @return array|null 任务详情 + */ + public static function getTaskDetail($id) + { + return self::where('id', $id)->find(); + } + + /** + * 获取任务列表 + * @param array $where 查询条件 + * @param string $order 排序 + * @param int $page 页码 + * @param int $limit 每页数量 + * @return array 任务列表和总数 + */ + public static function getTaskList($where = [], $order = 'id desc', $page = 1, $limit = 10) + { + // 构建查询 + $query = self::where($where); + + // 计算总数 + $total = $query->count(); + + // 分页查询数据 + $list = $query->page($page, $limit) + ->order($order) + ->select(); + + return [ + 'list' => $list, + 'total' => $total, + 'page' => $page, + 'limit' => $limit + ]; + } + + /** + * 关联场景 + */ + public function scene() + { + return $this->belongsTo('PlanScene', 'scene_id'); + } + + /** + * 关联设备 + */ + public function device() + { + return $this->belongsTo('app\devices\model\Device', 'device_id'); + } + + /** + * 关联执行记录 + */ + public function executions() + { + return $this->hasMany('PlanExecution', 'plan_id'); + } +} \ No newline at end of file diff --git a/Server/application/plan/model/Tag.php b/Server/application/plan/model/Tag.php new file mode 100644 index 00000000..01813874 --- /dev/null +++ b/Server/application/plan/model/Tag.php @@ -0,0 +1,125 @@ + 'integer', + 'count' => 'integer', + 'status' => 'integer', + 'createTime' => 'integer', + 'updateTime' => 'integer' + ]; + + /** + * 获取或创建标签 + * @param string $name 标签名 + * @param string $type 标签类型 + * @param string $color 标签颜色 + * @return int 标签ID + */ + public static function getOrCreate($name, $type = 'traffic', $color = '') + { + $tag = self::where([ + ['name', '=', $name], + ['type', '=', $type] + ])->find(); + + if ($tag) { + return $tag['id']; + } else { + $model = new self(); + $model->save([ + 'name' => $name, + 'type' => $type, + 'color' => $color ?: self::getRandomColor(), + 'count' => 0, + 'status' => 1 + ]); + return $model->id; + } + } + + /** + * 更新标签使用次数 + * @param int $id 标签ID + * @param int $increment 增量 + * @return bool 更新结果 + */ + public static function updateCount($id, $increment = 1) + { + return self::where('id', $id)->inc('count', $increment)->update(); + } + + /** + * 获取标签列表 + * @param string $type 标签类型 + * @param array $where 额外条件 + * @return array 标签列表 + */ + public static function getTagsByType($type = 'traffic', $where = []) + { + $conditions = array_merge([ + ['type', '=', $type], + ['status', '=', 1] + ], $where); + + return self::where($conditions) + ->order('count DESC, id DESC') + ->select(); + } + + /** + * 根据ID获取标签名称 + * @param array $ids 标签ID数组 + * @return array 标签名称数组 + */ + public static function getTagNames($ids) + { + if (empty($ids)) { + return []; + } + + $tagIds = is_array($ids) ? $ids : explode(',', $ids); + + $tags = self::where('id', 'in', $tagIds)->column('name'); + + return $tags; + } + + /** + * 获取随机颜色 + * @return string 颜色代码 + */ + private static function getRandomColor() + { + $colors = [ + '#f44336', '#e91e63', '#9c27b0', '#673ab7', '#3f51b5', + '#2196f3', '#03a9f4', '#00bcd4', '#009688', '#4caf50', + '#8bc34a', '#cddc39', '#ffeb3b', '#ffc107', '#ff9800', + '#ff5722', '#795548', '#9e9e9e', '#607d8b' + ]; + + return $colors[array_rand($colors)]; + } +} \ No newline at end of file diff --git a/Server/application/plan/model/TrafficPool.php b/Server/application/plan/model/TrafficPool.php new file mode 100644 index 00000000..617b23b3 --- /dev/null +++ b/Server/application/plan/model/TrafficPool.php @@ -0,0 +1,138 @@ + 'integer', + 'gender' => 'integer', + 'age' => 'integer', + 'status' => 'integer', + 'last_used_time' => 'integer', + 'createTime' => 'integer', + 'updateTime' => 'integer', + 'deleteTime' => 'integer' + ]; + + /** + * 添加或更新流量信息 + * @param string $mobile 手机号 + * @param array $data 流量数据 + * @return int|bool 流量ID或更新结果 + */ + public static function addOrUpdateTraffic($mobile, $data = []) + { + // 查询是否已存在该手机号 + $exists = self::where('mobile', $mobile)->find(); + + // 设置通用数据 + $saveData = array_merge([ + 'mobile' => $mobile, + 'status' => 1, + 'last_used_time' => time() + ], $data); + + if ($exists) { + // 更新已存在的流量记录 + return self::where('id', $exists['id'])->update($saveData); + } else { + // 创建新的流量记录 + $model = new self(); + $model->save($saveData); + return $model->id; + } + } + + /** + * 获取可用的流量列表 + * @param array $where 查询条件 + * @param string $order 排序 + * @param int $page 页码 + * @param int $limit 每页数量 + * @return array 流量列表和总数 + */ + public static function getAvailableTraffic($where = [], $order = 'last_used_time ASC', $page = 1, $limit = 10) + { + // 确保只查询有效流量 + $where[] = ['status', '=', 1]; + + // 构建查询 + $query = self::where($where); + + // 计算总数 + $total = $query->count(); + + // 分页查询数据 + $list = $query->page($page, $limit) + ->order($order) + ->select(); + + return [ + 'list' => $list, + 'total' => $total, + 'page' => $page, + 'limit' => $limit + ]; + } + + /** + * 设置流量使用时间 + * @param int $id 流量ID + * @return bool 更新结果 + */ + public static function setTrafficUsed($id) + { + return self::where('id', $id)->update([ + 'last_used_time' => time() + ]); + } + + /** + * 获取流量详情 + * @param int $id 流量ID + * @return array|null 流量详情 + */ + public static function getTrafficDetail($id) + { + return self::where('id', $id)->find(); + } + + /** + * 根据手机号获取流量详情 + * @param string $mobile 手机号 + * @return array|null 流量详情 + */ + public static function getTrafficByMobile($mobile) + { + return self::where('mobile', $mobile)->find(); + } + + /** + * 关联流量来源 + */ + public function sources() + { + return $this->hasMany('TrafficSource', 'traffic_id'); + } +} \ No newline at end of file diff --git a/Server/application/plan/model/TrafficSource.php b/Server/application/plan/model/TrafficSource.php new file mode 100644 index 00000000..4d3d6528 --- /dev/null +++ b/Server/application/plan/model/TrafficSource.php @@ -0,0 +1,151 @@ + 'integer', + 'traffic_id' => 'integer', + 'plan_id' => 'integer', + 'scene_id' => 'integer', + 'source_detail' => 'json', + 'createTime' => 'integer' + ]; + + /** + * 渠道文本获取器 + * @param string $value 渠道值 + * @return string 渠道文本 + */ + public function getChannelTextAttr($value, $data) + { + $channels = [ + 'poster' => '海报', + 'order' => '订单', + 'douyin' => '抖音', + 'xiaohongshu' => '小红书', + 'phone' => '电话', + 'wechat' => '公众号', + 'group' => '微信群', + 'payment' => '付款码', + 'api' => 'API接口' + ]; + + return isset($channels[$data['channel']]) ? $channels[$data['channel']] : '未知'; + } + + /** + * 添加流量来源记录 + * @param int $trafficId 流量ID + * @param string $channel 渠道 + * @param array $data 额外数据 + * @return int 新增记录ID + */ + public static function addSource($trafficId, $channel, $data = []) + { + $model = new self(); + $model->save(array_merge([ + 'traffic_id' => $trafficId, + 'channel' => $channel, + 'ip' => request()->ip(), + 'user_agent' => request()->header('user-agent') + ], $data)); + + return $model->id; + } + + /** + * 获取流量来源列表 + * @param int $trafficId 流量ID + * @return array 来源列表 + */ + public static function getSourcesByTrafficId($trafficId) + { + return self::where('traffic_id', $trafficId) + ->order('createTime DESC') + ->select(); + } + + /** + * 获取来源统计 + * @param string $channel 渠道 + * @param int $planId 计划ID + * @param int $sceneId 场景ID + * @param string $startTime 开始时间 + * @param string $endTime 结束时间 + * @return array 统计数据 + */ + public static function getSourceStats($channel = null, $planId = null, $sceneId = null, $startTime = null, $endTime = null) + { + $where = []; + + if ($channel !== null) { + $where[] = ['channel', '=', $channel]; + } + + if ($planId !== null) { + $where[] = ['plan_id', '=', $planId]; + } + + if ($sceneId !== null) { + $where[] = ['scene_id', '=', $sceneId]; + } + + if ($startTime !== null) { + $where[] = ['createTime', '>=', strtotime($startTime)]; + } + + if ($endTime !== null) { + $where[] = ['createTime', '<=', strtotime($endTime)]; + } + + return self::where($where) + ->field('channel, COUNT(*) as count') + ->group('channel') + ->select(); + } + + /** + * 关联流量 + */ + public function traffic() + { + return $this->belongsTo('TrafficPool', 'traffic_id'); + } + + /** + * 关联计划 + */ + public function plan() + { + return $this->belongsTo('PlanTask', 'plan_id'); + } + + /** + * 关联场景 + */ + public function scene() + { + return $this->belongsTo('PlanScene', 'scene_id'); + } +} \ No newline at end of file diff --git a/Server/application/plan/route.php b/Server/application/plan/route.php new file mode 100644 index 00000000..0b3964d5 --- /dev/null +++ b/Server/application/plan/route.php @@ -0,0 +1,30 @@ + 处理器名称 + 1 => 'PosterScene', + 2 => 'OrderScene', + 3 => 'DouyinScene', + 4 => 'XiaohongshuScene', + 5 => 'PhoneScene', + 6 => 'WechatScene', + 7 => 'GroupScene', + 8 => 'PaymentScene', + 9 => 'ApiScene', + ]; + + if (!isset($handlerMap[$sceneId])) { + throw new Exception('未找到场景处理器'); + } + + $handlerClass = '\\app\\plan\\scene\\' . $handlerMap[$sceneId]; + if (!class_exists($handlerClass)) { + throw new Exception('场景处理器不存在'); + } + + return new $handlerClass($scene); + } + + /** + * 处理海报扫码获客 + * @param string $mobile 手机号 + * @param int $sceneId 场景ID + * @param int $planId 计划ID + * @param array $extra 额外数据 + * @return array 处理结果 + */ + public static function handlePosterScan($mobile, $sceneId, $planId = null, $extra = []) + { + if (empty($mobile)) { + return [ + 'success' => false, + 'message' => '手机号不能为空' + ]; + } + + try { + // 添加或更新流量信息 + $trafficId = TrafficPool::addOrUpdateTraffic($mobile, [ + 'name' => $extra['name'] ?? '', + 'gender' => $extra['gender'] ?? 0, + 'region' => $extra['region'] ?? '' + ]); + + // 添加流量来源记录 + TrafficSource::addSource($trafficId, 'poster', [ + 'plan_id' => $planId, + 'scene_id' => $sceneId, + 'source_detail' => json_encode($extra) + ]); + + return [ + 'success' => true, + 'message' => '海报扫码获客处理成功', + 'data' => [ + 'traffic_id' => $trafficId + ] + ]; + + } catch (Exception $e) { + Log::error('海报扫码获客处理失败', [ + 'mobile' => $mobile, + 'scene_id' => $sceneId, + 'plan_id' => $planId, + 'error' => $e->getMessage() + ]); + + return [ + 'success' => false, + 'message' => '处理失败:' . $e->getMessage() + ]; + } + } + + /** + * 处理订单导入获客 + * @param array $orders 订单数据 + * @param int $sceneId 场景ID + * @param int $planId 计划ID + * @return array 处理结果 + */ + public static function handleOrderImport($orders, $sceneId, $planId = null) + { + if (empty($orders) || !is_array($orders)) { + return [ + 'success' => false, + 'message' => '订单数据格式不正确' + ]; + } + + $success = 0; + $failed = 0; + $errors = []; + + foreach ($orders as $order) { + if (empty($order['mobile'])) { + $failed++; + $errors[] = '订单缺少手机号'; + continue; + } + + try { + // 添加或更新流量信息 + $trafficId = TrafficPool::addOrUpdateTraffic($order['mobile'], [ + 'name' => $order['name'] ?? '', + 'gender' => $order['gender'] ?? 0, + 'region' => $order['region'] ?? '' + ]); + + // 添加流量来源记录 + TrafficSource::addSource($trafficId, 'order', [ + 'plan_id' => $planId, + 'scene_id' => $sceneId, + 'source_detail' => json_encode($order), + 'sub_channel' => $order['order_source'] ?? '' + ]); + + $success++; + + } catch (Exception $e) { + $failed++; + $errors[] = '处理订单失败:' . $e->getMessage(); + + Log::error('订单导入获客处理失败', [ + 'order' => $order, + 'scene_id' => $sceneId, + 'plan_id' => $planId, + 'error' => $e->getMessage() + ]); + } + } + + return [ + 'success' => $success > 0, + 'message' => "导入完成,成功{$success}条,失败{$failed}条", + 'data' => [ + 'success_count' => $success, + 'failed_count' => $failed, + 'errors' => $errors + ] + ]; + } + + /** + * 通用渠道获客处理 + * @param string $mobile 手机号 + * @param string $channel 渠道 + * @param int $sceneId 场景ID + * @param int $planId 计划ID + * @param array $extra 额外数据 + * @return array 处理结果 + */ + public static function handleChannelTraffic($mobile, $channel, $sceneId, $planId = null, $extra = []) + { + if (empty($mobile)) { + return [ + 'success' => false, + 'message' => '手机号不能为空' + ]; + } + + if (empty($channel)) { + return [ + 'success' => false, + 'message' => '渠道不能为空' + ]; + } + + try { + // 添加或更新流量信息 + $trafficId = TrafficPool::addOrUpdateTraffic($mobile, [ + 'name' => $extra['name'] ?? '', + 'gender' => $extra['gender'] ?? 0, + 'region' => $extra['region'] ?? '' + ]); + + // 添加流量来源记录 + TrafficSource::addSource($trafficId, $channel, [ + 'plan_id' => $planId, + 'scene_id' => $sceneId, + 'source_detail' => json_encode($extra), + 'sub_channel' => $extra['sub_channel'] ?? '' + ]); + + return [ + 'success' => true, + 'message' => $channel . '获客处理成功', + 'data' => [ + 'traffic_id' => $trafficId + ] + ]; + + } catch (Exception $e) { + Log::error($channel . '获客处理失败', [ + 'mobile' => $mobile, + 'scene_id' => $sceneId, + 'plan_id' => $planId, + 'error' => $e->getMessage() + ]); + + return [ + 'success' => false, + 'message' => '处理失败:' . $e->getMessage() + ]; + } + } +} \ No newline at end of file diff --git a/Server/application/plan/service/TaskRunner.php b/Server/application/plan/service/TaskRunner.php new file mode 100644 index 00000000..5abaa558 --- /dev/null +++ b/Server/application/plan/service/TaskRunner.php @@ -0,0 +1,675 @@ +task = PlanTask::getTaskDetail($task); + } else { + $this->task = $task; + } + + if (empty($this->task)) { + throw new Exception('任务不存在'); + } + + // 注册步骤处理器 + $this->registerStepHandlers(); + } + + /** + * 注册步骤处理器 + */ + protected function registerStepHandlers() + { + // 基础配置 + $this->stepHandlers[1] = function() { + return $this->handleBasicConfig(); + }; + + // 加友计划 + $this->stepHandlers[2] = function() { + return $this->handleAddFriend(); + }; + + // API调用 + $this->stepHandlers[3] = function() { + return $this->handleApiCall(); + }; + + // 标签处理 + $this->stepHandlers[4] = function() { + return $this->handleTagging(); + }; + } + + /** + * 运行任务 + * @return array 执行结果 + */ + public function run() + { + if ($this->task['status'] != 1) { + return [ + 'success' => false, + 'message' => '任务未启用,无法运行' + ]; + } + + // 获取当前步骤 + $currentStep = $this->task['current_step']; + + // 检查是否需要初始化第一步 + if ($currentStep == 0) { + $currentStep = 1; + PlanTask::updateTaskStatus($this->task['id'], 1, $currentStep); + $this->task['current_step'] = $currentStep; + } + + // 执行当前步骤 + if (isset($this->stepHandlers[$currentStep])) { + try { + $result = call_user_func($this->stepHandlers[$currentStep]); + + if ($result['success']) { + // 检查是否需要进入下一步 + if ($result['completed'] && $currentStep < 4) { + $nextStep = $currentStep + 1; + PlanTask::updateTaskStatus($this->task['id'], 1, $nextStep); + } else if ($result['completed'] && $currentStep == 4) { + // 所有步骤已完成,标记任务为完成状态 + PlanTask::updateTaskStatus($this->task['id'], 2, $currentStep); + } + } else { + // 如果步骤执行失败,记录错误并可能更新任务状态 + Log::error('任务执行失败:', [ + 'task_id' => $this->task['id'], + 'step' => $currentStep, + 'error' => $result['message'] + ]); + + // 视情况决定是否将任务标记为失败 + if ($result['fatal']) { + PlanTask::updateTaskStatus($this->task['id'], 3, $currentStep); + } + } + + return $result; + + } catch (Exception $e) { + // 捕获并记录异常 + Log::error('任务执行异常:', [ + 'task_id' => $this->task['id'], + 'step' => $currentStep, + 'error' => $e->getMessage(), + 'trace' => $e->getTraceAsString() + ]); + + return [ + 'success' => false, + 'message' => '任务执行异常:' . $e->getMessage(), + 'fatal' => true + ]; + } + } else { + return [ + 'success' => false, + 'message' => '未知的任务步骤:' . $currentStep, + 'fatal' => true + ]; + } + } + + /** + * 处理基础配置步骤 + * @return array 处理结果 + */ + protected function handleBasicConfig() + { + // 创建执行记录 + $executionId = PlanExecution::createExecution($this->task['id'], 1, [ + 'status' => 1 // 设置为进行中 + ]); + + try { + // 检查设备状态 + if (empty($this->task['device_id'])) { + PlanExecution::updateExecution($executionId, 3, [ + 'error' => '未设置设备' + ]); + + return [ + 'success' => false, + 'message' => '未设置设备', + 'fatal' => true + ]; + } + + // 检查场景配置 + if (empty($this->task['scene_id'])) { + PlanExecution::updateExecution($executionId, 3, [ + 'error' => '未设置获客场景' + ]); + + return [ + 'success' => false, + 'message' => '未设置获客场景', + 'fatal' => true + ]; + } + + // 检查场景配置 + if (empty($this->task['scene_config'])) { + PlanExecution::updateExecution($executionId, 3, [ + 'error' => '场景配置为空' + ]); + + return [ + 'success' => false, + 'message' => '场景配置为空', + 'fatal' => true + ]; + } + + // 标记基础配置步骤为完成 + PlanExecution::updateExecution($executionId, 2, [ + 'result' => [ + 'device_id' => $this->task['device_id'], + 'scene_id' => $this->task['scene_id'], + 'config_valid' => true + ] + ]); + + return [ + 'success' => true, + 'message' => '基础配置验证通过', + 'completed' => true + ]; + + } catch (Exception $e) { + PlanExecution::updateExecution($executionId, 3, [ + 'error' => '基础配置异常:' . $e->getMessage() + ]); + + throw $e; + } + } + + /** + * 处理加友计划步骤 + * @return array 处理结果 + */ + protected function handleAddFriend() + { + // 创建执行记录 + $executionId = PlanExecution::createExecution($this->task['id'], 2, [ + 'status' => 1 // 设置为进行中 + ]); + + try { + // 从流量池中选择符合条件的流量 + $trafficConditions = $this->getTrafficConditions(); + $trafficData = TrafficPool::getAvailableTraffic($trafficConditions, 'last_used_time ASC', 1, 1); + + if (empty($trafficData['list'])) { + // 没有符合条件的流量,标记为等待状态 + PlanExecution::updateExecution($executionId, 0, [ + 'error' => '没有符合条件的流量' + ]); + + return [ + 'success' => true, + 'message' => '没有符合条件的流量,等待下次执行', + 'completed' => false // 不算失败,但也不进入下一步 + ]; + } + + $traffic = $trafficData['list'][0]; + + // 调用设备服务执行加好友操作 + $addFriendResult = $this->callDeviceAddFriend($traffic); + + if ($addFriendResult['success']) { + // 更新流量使用状态 + TrafficPool::setTrafficUsed($traffic['id']); + + // 标记执行记录为成功 + PlanExecution::updateExecution($executionId, 2, [ + 'traffic_id' => $traffic['id'], + 'result' => $addFriendResult['data'] + ]); + + return [ + 'success' => true, + 'message' => '加友成功:' . $traffic['mobile'], + 'completed' => true, + 'traffic' => $traffic + ]; + } else { + // 标记执行记录为失败 + PlanExecution::updateExecution($executionId, 3, [ + 'traffic_id' => $traffic['id'], + 'error' => $addFriendResult['message'], + 'result' => $addFriendResult['data'] ?? null + ]); + + return [ + 'success' => false, + 'message' => '加友失败:' . $addFriendResult['message'], + 'fatal' => false // 加友失败不算致命错误,可以下次继续 + ]; + } + + } catch (Exception $e) { + PlanExecution::updateExecution($executionId, 3, [ + 'error' => '加友计划异常:' . $e->getMessage() + ]); + + throw $e; + } + } + + /** + * 处理API调用步骤 + * @return array 处理结果 + */ + protected function handleApiCall() + { + // 创建执行记录 + $executionId = PlanExecution::createExecution($this->task['id'], 3, [ + 'status' => 1 // 设置为进行中 + ]); + + try { + // 获取上一步成功处理的流量信息 + $lastExecution = PlanExecution::getLatestExecution($this->task['id'], 2); + + if (empty($lastExecution) || $lastExecution['status'] != 2) { + PlanExecution::updateExecution($executionId, 3, [ + 'error' => '上一步未成功完成' + ]); + + return [ + 'success' => false, + 'message' => '上一步未成功完成,无法进行API调用', + 'fatal' => true + ]; + } + + $trafficId = $lastExecution['traffic_id']; + if (empty($trafficId)) { + PlanExecution::updateExecution($executionId, 3, [ + 'error' => '未找到有效的流量ID' + ]); + + return [ + 'success' => false, + 'message' => '未找到有效的流量ID', + 'fatal' => true + ]; + } + + // 获取流量详情 + $traffic = TrafficPool::getTrafficDetail($trafficId); + if (empty($traffic)) { + PlanExecution::updateExecution($executionId, 3, [ + 'error' => '未找到流量信息' + ]); + + return [ + 'success' => false, + 'message' => '未找到流量信息', + 'fatal' => true + ]; + } + + // 根据场景配置调用相应的API + $apiCallResult = $this->callSceneApi($traffic); + + if ($apiCallResult['success']) { + // 标记执行记录为成功 + PlanExecution::updateExecution($executionId, 2, [ + 'traffic_id' => $trafficId, + 'result' => $apiCallResult['data'] + ]); + + return [ + 'success' => true, + 'message' => 'API调用成功', + 'completed' => true, + 'traffic' => $traffic + ]; + } else { + // 标记执行记录为失败 + PlanExecution::updateExecution($executionId, 3, [ + 'traffic_id' => $trafficId, + 'error' => $apiCallResult['message'], + 'result' => $apiCallResult['data'] ?? null + ]); + + return [ + 'success' => false, + 'message' => 'API调用失败:' . $apiCallResult['message'], + 'fatal' => $apiCallResult['fatal'] ?? false + ]; + } + + } catch (Exception $e) { + PlanExecution::updateExecution($executionId, 3, [ + 'error' => 'API调用异常:' . $e->getMessage() + ]); + + throw $e; + } + } + + /** + * 处理标签步骤 + * @return array 处理结果 + */ + protected function handleTagging() + { + // 创建执行记录 + $executionId = PlanExecution::createExecution($this->task['id'], 4, [ + 'status' => 1 // 设置为进行中 + ]); + + try { + // 获取上一步成功处理的流量信息 + $lastExecution = PlanExecution::getLatestExecution($this->task['id'], 3); + + if (empty($lastExecution) || $lastExecution['status'] != 2) { + PlanExecution::updateExecution($executionId, 3, [ + 'error' => '上一步未成功完成' + ]); + + return [ + 'success' => false, + 'message' => '上一步未成功完成,无法进行标签处理', + 'fatal' => true + ]; + } + + $trafficId = $lastExecution['traffic_id']; + if (empty($trafficId)) { + PlanExecution::updateExecution($executionId, 3, [ + 'error' => '未找到有效的流量ID' + ]); + + return [ + 'success' => false, + 'message' => '未找到有效的流量ID', + 'fatal' => true + ]; + } + + // 获取流量详情 + $traffic = TrafficPool::getTrafficDetail($trafficId); + if (empty($traffic)) { + PlanExecution::updateExecution($executionId, 3, [ + 'error' => '未找到流量信息' + ]); + + return [ + 'success' => false, + 'message' => '未找到流量信息', + 'fatal' => true + ]; + } + + // 获取并应用标签 + $taggingResult = $this->applyTags($traffic); + + if ($taggingResult['success']) { + // 标记执行记录为成功 + PlanExecution::updateExecution($executionId, 2, [ + 'traffic_id' => $trafficId, + 'result' => $taggingResult['data'] + ]); + + return [ + 'success' => true, + 'message' => '标签处理成功', + 'completed' => true, + 'traffic' => $traffic + ]; + } else { + // 标记执行记录为失败 + PlanExecution::updateExecution($executionId, 3, [ + 'traffic_id' => $trafficId, + 'error' => $taggingResult['message'], + 'result' => $taggingResult['data'] ?? null + ]); + + return [ + 'success' => false, + 'message' => '标签处理失败:' . $taggingResult['message'], + 'fatal' => $taggingResult['fatal'] ?? false + ]; + } + + } catch (Exception $e) { + PlanExecution::updateExecution($executionId, 3, [ + 'error' => '标签处理异常:' . $e->getMessage() + ]); + + throw $e; + } + } + + /** + * 获取流量筛选条件 + * @return array 条件数组 + */ + protected function getTrafficConditions() + { + $conditions = []; + + // 根据场景配置获取筛选条件 + if (isset($this->task['scene_config']) && is_array($this->task['scene_config'])) { + $config = $this->task['scene_config']; + + // 添加性别筛选 + if (isset($config['gender']) && in_array($config['gender'], [0, 1, 2])) { + $conditions[] = ['gender', '=', $config['gender']]; + } + + // 添加年龄筛选 + if (isset($config['age_min']) && is_numeric($config['age_min'])) { + $conditions[] = ['age', '>=', intval($config['age_min'])]; + } + + if (isset($config['age_max']) && is_numeric($config['age_max'])) { + $conditions[] = ['age', '<=', intval($config['age_max'])]; + } + + // 添加区域筛选 + if (isset($config['region']) && !empty($config['region'])) { + $conditions[] = ['region', 'like', '%' . $config['region'] . '%']; + } + } + + return $conditions; + } + + /** + * 调用设备加好友操作 + * @param array $traffic 流量信息 + * @return array 调用结果 + */ + protected function callDeviceAddFriend($traffic) + { + // 模拟调用设备操作 + // 实际项目中应该调用实际的设备API + + // 记录设备调用日志 + Log::info('设备加好友操作', [ + 'task_id' => $this->task['id'], + 'device_id' => $this->task['device_id'], + 'mobile' => $traffic['mobile'] + ]); + + // 模拟成功率 + $success = mt_rand(0, 10) > 2; + + if ($success) { + return [ + 'success' => true, + 'message' => '加好友操作成功', + 'data' => [ + 'add_time' => date('Y-m-d H:i:s'), + 'device_id' => $this->task['device_id'], + 'mobile' => $traffic['mobile'] + ] + ]; + } else { + return [ + 'success' => false, + 'message' => '加好友操作失败:' . ['设备繁忙', '用户拒绝', '网络异常'][mt_rand(0, 2)], + 'data' => [ + 'attempt_time' => date('Y-m-d H:i:s'), + 'device_id' => $this->task['device_id'], + 'mobile' => $traffic['mobile'] + ] + ]; + } + } + + /** + * 根据场景调用相应API + * @param array $traffic 流量信息 + * @return array 调用结果 + */ + protected function callSceneApi($traffic) + { + // 根据场景类型调用不同API + if (empty($this->task['scene_id'])) { + return [ + 'success' => false, + 'message' => '场景未设置', + 'fatal' => true + ]; + } + + // 记录API调用日志 + Log::info('场景API调用', [ + 'task_id' => $this->task['id'], + 'scene_id' => $this->task['scene_id'], + 'traffic_id' => $traffic['id'] + ]); + + // 模拟成功率 + $success = mt_rand(0, 10) > 1; + + if ($success) { + return [ + 'success' => true, + 'message' => 'API调用成功', + 'data' => [ + 'call_time' => date('Y-m-d H:i:s'), + 'scene_id' => $this->task['scene_id'], + 'traffic_id' => $traffic['id'] + ] + ]; + } else { + return [ + 'success' => false, + 'message' => 'API调用失败:' . ['参数错误', 'API超时', '系统异常'][mt_rand(0, 2)], + 'data' => [ + 'attempt_time' => date('Y-m-d H:i:s'), + 'scene_id' => $this->task['scene_id'], + 'traffic_id' => $traffic['id'] + ], + 'fatal' => false // API调用失败通常不算致命错误 + ]; + } + } + + /** + * 应用标签 + * @param array $traffic 流量信息 + * @return array 处理结果 + */ + protected function applyTags($traffic) + { + // 获取需要应用的标签 + $tags = []; + + // 从场景配置中获取标签 + if (isset($this->task['scene_config']) && is_array($this->task['scene_config']) && isset($this->task['scene_config']['tags'])) { + $configTags = $this->task['scene_config']['tags']; + if (is_array($configTags)) { + $tags = array_merge($tags, $configTags); + } else if (is_string($configTags)) { + $tags[] = $configTags; + } + } + + // 从场景获取标签 + if (!empty($this->task['scene_id'])) { + $tags[] = '场景_' . $this->task['scene_id']; + } + + // 如果没有标签,返回成功 + if (empty($tags)) { + return [ + 'success' => true, + 'message' => '没有需要应用的标签', + 'data' => [] + ]; + } + + // 处理标签 + $tagIds = []; + foreach ($tags as $tagName) { + $tagId = Tag::getOrCreate($tagName, 'friend'); + $tagIds[] = $tagId; + Tag::updateCount($tagId); + } + + // 记录标签应用日志 + Log::info('应用标签', [ + 'task_id' => $this->task['id'], + 'traffic_id' => $traffic['id'], + 'tag_ids' => $tagIds + ]); + + // 更新流量标签 + $existingTags = empty($traffic['tag_ids']) ? [] : explode(',', $traffic['tag_ids']); + $allTags = array_unique(array_merge($existingTags, $tagIds)); + + TrafficPool::where('id', $traffic['id'])->update([ + 'tag_ids' => implode(',', $allTags) + ]); + + return [ + 'success' => true, + 'message' => '标签应用成功', + 'data' => [ + 'tag_ids' => $tagIds, + 'tag_names' => $tags + ] + ]; + } +} \ No newline at end of file diff --git a/Server/application/plan/sql/tables.sql b/Server/application/plan/sql/tables.sql new file mode 100644 index 00000000..58c2312b --- /dev/null +++ b/Server/application/plan/sql/tables.sql @@ -0,0 +1,92 @@ +-- 获客计划主表 +CREATE TABLE `tk_plan_task` ( + `id` int(10) unsigned NOT NULL AUTO_INCREMENT COMMENT '自增ID', + `name` varchar(100) NOT NULL COMMENT '计划名称', + `device_id` int(10) unsigned DEFAULT NULL COMMENT '关联设备ID', + `scene_id` int(10) unsigned DEFAULT NULL COMMENT '获客场景ID', + `scene_config` text DEFAULT NULL COMMENT '场景配置(JSON格式)', + `status` tinyint(3) unsigned DEFAULT 0 COMMENT '状态:0=停用,1=启用,2=完成,3=失败', + `current_step` tinyint(3) unsigned DEFAULT 0 COMMENT '当前执行步骤', + `priority` tinyint(3) unsigned DEFAULT 5 COMMENT '优先级:1-10,数字越大优先级越高', + `created_by` int(10) unsigned NOT NULL COMMENT '创建人ID', + `createTime` int(11) DEFAULT NULL COMMENT '创建时间', + `updateTime` int(11) DEFAULT NULL COMMENT '更新时间', + `deleteTime` int(11) DEFAULT NULL COMMENT '删除时间', + PRIMARY KEY (`id`), + KEY `idx_status` (`status`), + KEY `idx_device` (`device_id`), + KEY `idx_scene` (`scene_id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='获客计划主表'; + +-- 流量池表 +CREATE TABLE `tk_traffic_pool` ( + `id` int(10) unsigned NOT NULL AUTO_INCREMENT COMMENT '自增ID', + `mobile` varchar(20) NOT NULL COMMENT '手机号', + `name` varchar(50) DEFAULT NULL COMMENT '姓名', + `gender` tinyint(1) DEFAULT NULL COMMENT '性别:0=未知,1=男,2=女', + `age` int(3) DEFAULT NULL COMMENT '年龄', + `region` varchar(100) DEFAULT NULL COMMENT '区域', + `status` tinyint(3) unsigned DEFAULT 1 COMMENT '状态:0=无效,1=有效', + `tag_ids` varchar(255) DEFAULT NULL COMMENT '标签ID,逗号分隔', + `remark` varchar(255) DEFAULT NULL COMMENT '备注', + `last_used_time` int(11) DEFAULT NULL COMMENT '最后使用时间', + `createTime` int(11) DEFAULT NULL COMMENT '创建时间', + `updateTime` int(11) DEFAULT NULL COMMENT '更新时间', + `deleteTime` int(11) DEFAULT NULL COMMENT '删除时间', + PRIMARY KEY (`id`), + UNIQUE KEY `uk_mobile` (`mobile`), + KEY `idx_status` (`status`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='流量池表'; + +-- 流量来源表 +CREATE TABLE `tk_traffic_source` ( + `id` int(10) unsigned NOT NULL AUTO_INCREMENT COMMENT '自增ID', + `traffic_id` int(10) unsigned NOT NULL COMMENT '关联流量池ID', + `plan_id` int(10) unsigned DEFAULT NULL COMMENT '关联计划ID', + `scene_id` int(10) unsigned DEFAULT NULL COMMENT '场景ID', + `channel` varchar(50) NOT NULL COMMENT '渠道:poster=海报, order=订单, douyin=抖音, xiaohongshu=小红书, phone=电话, wechat=公众号, group=微信群, payment=付款码, api=API接口', + `sub_channel` varchar(50) DEFAULT NULL COMMENT '子渠道', + `source_detail` text DEFAULT NULL COMMENT '来源详情(JSON格式)', + `ip` varchar(50) DEFAULT NULL COMMENT '来源IP', + `user_agent` varchar(255) DEFAULT NULL COMMENT '用户代理', + `createTime` int(11) DEFAULT NULL COMMENT '创建时间', + PRIMARY KEY (`id`), + KEY `idx_traffic` (`traffic_id`), + KEY `idx_plan` (`plan_id`), + KEY `idx_scene` (`scene_id`), + KEY `idx_channel` (`channel`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='流量来源表'; + +-- 计划执行记录表 +CREATE TABLE `tk_plan_execution` ( + `id` int(10) unsigned NOT NULL AUTO_INCREMENT COMMENT '自增ID', + `plan_id` int(10) unsigned NOT NULL COMMENT '关联计划ID', + `traffic_id` int(10) unsigned DEFAULT NULL COMMENT '关联流量ID', + `step` tinyint(3) unsigned NOT NULL COMMENT '执行步骤:1=基础配置,2=加友计划,3=API调用,4=标签处理', + `sub_step` varchar(50) DEFAULT NULL COMMENT '子步骤标识', + `status` tinyint(3) unsigned DEFAULT 0 COMMENT '状态:0=等待,1=进行中,2=成功,3=失败', + `result` text DEFAULT NULL COMMENT '执行结果(JSON格式)', + `error` varchar(255) DEFAULT NULL COMMENT '错误信息', + `start_time` int(11) DEFAULT NULL COMMENT '开始时间', + `end_time` int(11) DEFAULT NULL COMMENT '结束时间', + `createTime` int(11) DEFAULT NULL COMMENT '创建时间', + `updateTime` int(11) DEFAULT NULL COMMENT '更新时间', + PRIMARY KEY (`id`), + KEY `idx_plan` (`plan_id`), + KEY `idx_traffic` (`traffic_id`), + KEY `idx_status` (`status`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='计划执行记录表'; + +-- 标签表 +CREATE TABLE `tk_tag` ( + `id` int(10) unsigned NOT NULL AUTO_INCREMENT COMMENT '自增ID', + `name` varchar(50) NOT NULL COMMENT '标签名称', + `color` varchar(20) DEFAULT NULL COMMENT '标签颜色', + `type` varchar(20) DEFAULT 'traffic' COMMENT '标签类型:traffic=流量标签,friend=好友标签', + `count` int(11) DEFAULT 0 COMMENT '使用次数', + `status` tinyint(3) unsigned DEFAULT 1 COMMENT '状态:0=停用,1=启用', + `createTime` int(11) DEFAULT NULL COMMENT '创建时间', + `updateTime` int(11) DEFAULT NULL COMMENT '更新时间', + PRIMARY KEY (`id`), + UNIQUE KEY `uk_name_type` (`name`, `type`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='标签表'; \ No newline at end of file diff --git a/Server/application/plan/validate/Task.php b/Server/application/plan/validate/Task.php new file mode 100644 index 00000000..998b360b --- /dev/null +++ b/Server/application/plan/validate/Task.php @@ -0,0 +1,48 @@ + 'require|max:100', + 'device_id' => 'number', + 'scene_id' => 'number', + 'scene_config' => 'array', + 'status' => 'in:0,1,2,3', + 'priority' => 'between:1,10', + 'created_by' => 'number' + ]; + + /** + * 错误信息 + * @var array + */ + protected $message = [ + 'name.require' => '任务名称不能为空', + 'name.max' => '任务名称不能超过100个字符', + 'device_id.number' => '设备ID必须是数字', + 'scene_id.number' => '场景ID必须是数字', + 'scene_config.array'=> '场景配置必须是数组', + 'status.in' => '状态值无效', + 'priority.between' => '优先级必须在1到10之间', + 'created_by.number' => '创建者ID必须是数字' + ]; + + /** + * 验证场景 + * @var array + */ + protected $scene = [ + 'create' => ['name', 'device_id', 'scene_id', 'scene_config', 'status', 'priority', 'created_by'], + 'update' => ['name', 'device_id', 'scene_id', 'scene_config', 'status', 'priority'] + ]; +} \ No newline at end of file diff --git a/Server/application/plan/validate/Traffic.php b/Server/application/plan/validate/Traffic.php new file mode 100644 index 00000000..8528bd35 --- /dev/null +++ b/Server/application/plan/validate/Traffic.php @@ -0,0 +1,51 @@ + 'require|mobile', + 'gender' => 'in:0,1,2', + 'age' => 'number|between:0,120', + 'tags' => 'max:255', + 'province' => 'max:50', + 'city' => 'max:50', + 'source_channel' => 'max:50', + 'source_detail' => 'array' + ]; + + /** + * 错误信息 + * @var array + */ + protected $message = [ + 'mobile.require' => '手机号不能为空', + 'mobile.mobile' => '手机号格式不正确', + 'gender.in' => '性别值无效', + 'age.number' => '年龄必须是数字', + 'age.between' => '年龄必须在0到120之间', + 'tags.max' => '标签不能超过255个字符', + 'province.max' => '省份不能超过50个字符', + 'city.max' => '城市不能超过50个字符', + 'source_channel.max' => '来源渠道不能超过50个字符', + 'source_detail.array'=> '来源详情必须是数组' + ]; + + /** + * 验证场景 + * @var array + */ + protected $scene = [ + 'create' => ['mobile', 'gender', 'age', 'tags', 'province', 'city', 'source_channel', 'source_detail'], + 'update' => ['gender', 'age', 'tags', 'province', 'city'] + ]; +} \ No newline at end of file