From 21a6907e528de9154165b27cca417fa77fc97a21 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=9F=B3=E6=B8=85=E7=88=BD?= Date: Wed, 7 May 2025 10:49:28 +0800 Subject: [PATCH] =?UTF-8?q?=E7=A7=81=E5=9F=9F=E6=93=8D=E7=9B=98=E6=89=8B?= =?UTF-8?q?=20-=20=E8=AE=A1=E5=88=92=E4=BB=BB=E5=8A=A1=E5=88=97=E8=A1=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Cunkebao/app/page.tsx | 24 +- Cunkebao/app/plans/new/page.tsx | 84 ++++++- Cunkebao/app/scenarios/[channel]/page.tsx | 150 ++++++++++- Server/application/cunkebao/config/route.php | 12 +- .../application/cunkebao/controller/Plan.php | 235 +++++++++++++++++- 5 files changed, 485 insertions(+), 20 deletions(-) diff --git a/Cunkebao/app/page.tsx b/Cunkebao/app/page.tsx index a14c56be..07f2a189 100644 --- a/Cunkebao/app/page.tsx +++ b/Cunkebao/app/page.tsx @@ -154,6 +154,7 @@ export default function Home() { color: "bg-orange-100 text-orange-600", value: 167, growth: 10, + sceneId: 7, }, ] @@ -209,7 +210,11 @@ export default function Home() { {scenarioFeatures .sort((a, b) => b.value - a.value) .map((scenario) => ( - +
router.push(`/scenarios/${scenario.id}?id=${scenario.sceneId || getSceneIdFromType(scenario.id)}`)} + >
{scenario.name} @@ -219,7 +224,7 @@ export default function Home() { {scenario.name}
- +
))} @@ -236,3 +241,18 @@ export default function Home() { ) } +function getSceneIdFromType(type: string): number { + const typeToIdMap: Record = { + 'douyin': 1, + 'xiaohongshu': 2, + 'weixinqun': 3, + 'gongzhonghao': 4, + 'kuaishou': 5, + 'weibo': 6, + 'haibao': 7, + 'phone': 8, + 'api': 9 + }; + return typeToIdMap[type] || 1; +} + diff --git a/Cunkebao/app/plans/new/page.tsx b/Cunkebao/app/plans/new/page.tsx index 99f4cf8c..16c75ac4 100644 --- a/Cunkebao/app/plans/new/page.tsx +++ b/Cunkebao/app/plans/new/page.tsx @@ -123,14 +123,82 @@ export default function NewAcquisitionPlan() { // 根据标签和计划名称自动判断场景 const scenario = formData.scenario || determineScenario(formData.planName, formData.tags) - console.log("计划已创建:", { ...formData, scenario }) - toast({ - title: "创建成功", - description: "获客计划已创建完成", - }) + // 准备请求数据 + const requestData = { + sceneId: getSceneIdFromScenario(scenario), // 获取场景ID + name: formData.planName, + status: formData.enabled ? 1 : 0, + reqConf: JSON.stringify({ + remarkType: formData.remarkType, + remarkKeyword: formData.remarkKeyword, + greeting: formData.greeting, + addFriendTimeStart: formData.addFriendTimeStart, + addFriendTimeEnd: formData.addFriendTimeEnd, + addFriendInterval: formData.addFriendInterval, + maxDailyFriends: formData.maxDailyFriends, + selectedDevices: formData.selectedDevices, + }), + msgConf: JSON.stringify({ + messageInterval: formData.messageInterval, + messagePlans: formData.messagePlans, + }), + tagConf: JSON.stringify({ + tags: formData.tags, + importedTags: formData.importedTags, + }) + } - // 跳转到首页 - router.push("/") + // 调用API创建获客计划 + fetch(`${process.env.NEXT_PUBLIC_API_BASE_URL}/v1/plan/scenes/create`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'Authorization': `Bearer ${localStorage.getItem('token')}` + }, + body: JSON.stringify(requestData) + }) + .then(response => response.json()) + .then(data => { + if (data.code === 200) { + toast({ + title: "创建成功", + description: "获客计划已创建完成", + }) + // 跳转到首页 + router.push("/") + } else { + toast({ + title: "创建失败", + description: data.msg || "服务器错误,请稍后重试", + variant: "destructive" + }) + } + }) + .catch(error => { + console.error("创建获客计划失败:", error); + toast({ + title: "创建失败", + description: "网络错误,请稍后重试", + variant: "destructive" + }) + }) + } + + // 将场景类型转换为场景ID + const getSceneIdFromScenario = (scenario: string): number => { + const scenarioMap: Record = { + 'douyin': 1, + 'xiaohongshu': 2, + 'weixinqun': 3, + 'gongzhonghao': 4, + 'kuaishou': 5, + 'weibo': 6, + 'haibao': 7, + 'phone': 8, + 'order': 9, + 'other': 10 + } + return scenarioMap[scenario] || 1 // 默认返回1 } const handlePrev = () => { @@ -206,7 +274,7 @@ export default function NewAcquisitionPlan() { case 3: return case 4: - return + return default: return null } diff --git a/Cunkebao/app/scenarios/[channel]/page.tsx b/Cunkebao/app/scenarios/[channel]/page.tsx index 072e8266..18004ce8 100644 --- a/Cunkebao/app/scenarios/[channel]/page.tsx +++ b/Cunkebao/app/scenarios/[channel]/page.tsx @@ -1,6 +1,6 @@ "use client" -import { useState } from "react" +import { useState, useEffect } from "react" import { ChevronLeft, Copy, Link, HelpCircle } from "lucide-react" import { Button } from "@/components/ui/button" import { useRouter } from "next/navigation" @@ -35,6 +35,7 @@ const getChannelName = (channel: string) => { return channelMap[channel] || channel } +// 恢复Task接口定义 interface Task { id: string name: string @@ -50,6 +51,21 @@ interface Task { trend: { date: string; customers: number }[] } +interface PlanItem { + id: number; + name: string; + status: number; + statusText: string; + createTime: number; + createTimeFormat: string; + deviceCount: number; + customerCount: number; + addedCount: number; + passRate: number; + lastExecutionTime: string; + nextExecutionTime: string; +} + interface DeviceStats { active: number } @@ -76,6 +92,23 @@ export default function ChannelPage({ params }: { params: { channel: string } }) const router = useRouter() const channel = params.channel const channelName = getChannelName(params.channel) + + // 从URL query参数获取场景ID + const [sceneId, setSceneId] = useState(null); + + // 获取URL中的查询参数 + useEffect(() => { + // 从URL获取id参数 + const urlParams = new URLSearchParams(window.location.search); + const idParam = urlParams.get('id'); + + if (idParam && !isNaN(Number(idParam))) { + setSceneId(Number(idParam)); + } else { + // 如果没有传递有效的ID,使用函数获取默认ID + setSceneId(getSceneIdFromChannel(channel)); + } + }, [channel]); const initialTasks = [ { @@ -114,7 +147,9 @@ export default function ChannelPage({ params }: { params: { channel: string } }) }, ] - const [tasks, setTasks] = useState(initialTasks) + const [tasks, setTasks] = useState([]) + const [loading, setLoading] = useState(true) + const [error, setError] = useState(null) const [deviceStats, setDeviceStats] = useState({ active: 5, @@ -144,7 +179,7 @@ export default function ChannelPage({ params }: { params: { channel: string } }) toast({ title: "计划已复制", description: `已成功复制"${taskToCopy.name}"`, - variant: "success", + variant: "default", }) } } @@ -156,7 +191,7 @@ export default function ChannelPage({ params }: { params: { channel: string } }) toast({ title: "计划已删除", description: `已成功删除"${taskToDelete.name}"`, - variant: "success", + variant: "default", }) } } @@ -167,7 +202,7 @@ export default function ChannelPage({ params }: { params: { channel: string } }) toast({ title: newStatus === "running" ? "计划已启动" : "计划已暂停", description: `已${newStatus === "running" ? "启动" : "暂停"}获客计划`, - variant: "success", + variant: "default", }) } @@ -192,10 +227,95 @@ export default function ChannelPage({ params }: { params: { channel: string } }) toast({ title: "已复制", description: withParams ? "接口地址(含示例参数)已复制到剪贴板" : "接口地址已复制到剪贴板", - variant: "success", + variant: "default", }) } + // 修改API数据处理部分 + useEffect(() => { + const fetchPlanList = async () => { + try { + setLoading(true); + // 如果sceneId还未确定,则等待 + if (sceneId === null) return; + + // 调用API获取计划列表 + const response = await fetch( + `${process.env.NEXT_PUBLIC_API_BASE_URL}/v1/plan/list?id=${sceneId}`, + { + method: 'GET', + headers: { + 'Content-Type': 'application/json', + 'Authorization': `Bearer ${localStorage.getItem('token')}` + } + } + ); + + const data = await response.json(); + + if (data.code === 1 && data.data && data.data.list) { + // 将API返回的数据转换为前端展示格式 + const transformedTasks = data.data.list.map((item: PlanItem) => { + // 确保返回的对象完全符合Task类型 + const status: "running" | "paused" | "completed" = + item.status === 1 ? "running" : "paused"; + + return { + id: item.id.toString(), + name: item.name, + status: status, + stats: { + devices: item.deviceCount, + acquired: item.customerCount, + added: item.addedCount, + }, + lastUpdated: item.createTimeFormat, + executionTime: item.lastExecutionTime || "--", + nextExecutionTime: item.nextExecutionTime || "--", + trend: Array.from({ length: 7 }, (_, i) => ({ + date: `2月${String(i + 1)}日`, + customers: Math.floor(Math.random() * 20) + 10, // 模拟数据 + })), + }; + }); + + // 使用类型断言解决类型冲突 + setTasks(transformedTasks as Task[]); + setError(null); + } else { + setError(data.msg || "获取计划列表失败"); + // 如果API返回错误,使用初始数据 + setTasks(initialTasks); + } + } catch (err) { + console.error("获取计划列表失败:", err); + setError("网络错误,无法获取计划列表"); + // 出错时使用初始数据 + setTasks(initialTasks); + } finally { + setLoading(false); + } + }; + + fetchPlanList(); + }, [channel, initialTasks, sceneId]); + + // 辅助函数:根据渠道获取场景ID + const getSceneIdFromChannel = (channel: string): number => { + const channelMap: Record = { + 'douyin': 1, + 'xiaohongshu': 2, + 'weixinqun': 3, + 'gongzhonghao': 4, + 'kuaishou': 5, + 'weibo': 6, + 'haibao': 7, + 'phone': 8, + 'api': 9 + }; + return channelMap[channel] || 6; + }; + return (
@@ -210,7 +330,21 @@ export default function ChannelPage({ params }: { params: { channel: string } })
- {tasks.length > 0 ? ( + {loading ? ( + // 添加加载状态 +
+
+
加载计划中...
+
+ ) : error ? ( + // 添加错误提示 +
+
{error}
+ +
+ ) : tasks.length > 0 ? ( tasks.map((task) => (
diff --git a/Server/application/cunkebao/config/route.php b/Server/application/cunkebao/config/route.php index cc691213..692e55d0 100644 --- a/Server/application/cunkebao/config/route.php +++ b/Server/application/cunkebao/config/route.php @@ -81,4 +81,14 @@ Route::group('v1/', function () { }); -})->middleware(['jwt']); \ No newline at end of file + // 计划任务相关路由 + Route::group('plan', function () { + // 添加计划任务 + Route::post('add', 'app\cunkebao\controller\Plan@index'); + // 获取计划任务列表 + Route::get('list', 'app\cunkebao\controller\Plan@getList'); + }); + +})->middleware(['jwt']); + +return []; \ No newline at end of file diff --git a/Server/application/cunkebao/controller/Plan.php b/Server/application/cunkebao/controller/Plan.php index 42de7dfd..2c5f1478 100644 --- a/Server/application/cunkebao/controller/Plan.php +++ b/Server/application/cunkebao/controller/Plan.php @@ -3,6 +3,9 @@ namespace app\cunkebao\controller; use think\Controller; +use think\Db; +use think\facade\Request; +use library\ResponseHelper; /** * 获客场景控制器 @@ -10,12 +13,242 @@ use think\Controller; class Plan extends Controller { /** - * 计划任务 + * 添加计划任务 * * @return \think\response\Json */ public function index() { + try { + // 获取表单数据 + $data = [ + 'name' => Request::post('name', ''), + 'sceneId' => Request::post('sceneId', 0), + 'status' => Request::post('status', 0), + 'reqConf' => Request::post('reqConf', ''), + 'msgConf' => Request::post('msgConf', ''), + 'tagConf' => Request::post('tagConf', ''), + 'createTime' => time(), + 'updateTime' => time() + ]; + // 验证必填字段 + if (empty($data['name'])) { + return ResponseHelper::error('计划名称不能为空', 400); + } + + if (empty($data['sceneId'])) { + return ResponseHelper::error('场景ID不能为空', 400); + } + + // 验证数据格式 + if (!$this->validateJson($data['reqConf'])) { + return ResponseHelper::error('好友申请设置格式不正确', 400); + } + + if (!$this->validateJson($data['msgConf'])) { + return ResponseHelper::error('消息设置格式不正确', 400); + } + + if (!$this->validateJson($data['tagConf'])) { + return ResponseHelper::error('标签设置格式不正确', 400); + } + + // 插入数据库 + $result = Db::name('friend_plan')->insert($data); + + if ($result) { + return ResponseHelper::success([], '添加计划任务成功'); + } else { + return ResponseHelper::error('添加计划任务失败', 500); + } + } catch (\Exception $e) { + return ResponseHelper::error('系统错误: ' . $e->getMessage(), 500); + } + } + + /** + * 获取计划任务列表 + * + * @return \think\response\Json + */ + public function getList() + { + try { + // 获取分页参数 + $id = Request::param('id', 1); + $page = Request::param('page', 1); + $pageSize = Request::param('pageSize', 10); + + // 构建查询条件 + $where = []; + + // 过滤已删除的记录 + $where[] = ['deleteTime', 'null']; + + // 查询总数 + $total = Db::name('friend_plan')->where('sceneId', $id)->count(); + + // 查询列表数据 + $list = Db::name('friend_plan') + ->where('sceneId', $id) + ->field('id, name, status, createTime, updateTime, sceneId') + ->order('createTime desc') + ->page($page, $pageSize) + ->select(); + // 遍历列表,获取每个计划的统计信息 + foreach ($list as &$item) { + // 获取计划的统计信息 + $stats = $this->getPlanStats($item['id']); + + // 合并统计信息到结果中 + $item = array_merge($item, $stats); + + // 格式化状态为文字描述 + $item['statusText'] = $item['status'] == 1 ? '进行中' : '已暂停'; + + // 格式化时间 + $item['createTimeFormat'] = date('Y-m-d H:i', $item['createTime']); + + // 获取最近一次执行时间 + $lastExecution = $this->getLastExecution($item['id']); + $item['lastExecutionTime'] = $lastExecution['lastTime'] ?? ''; + $item['nextExecutionTime'] = $lastExecution['nextTime'] ?? ''; + } + + // 返回结果 + $result = [ + 'total' => $total, + 'list' => $list, + 'page' => $page, + 'pageSize' => $pageSize + ]; + + return ResponseHelper::success($result); + } catch (\Exception $e) { + return ResponseHelper::error('获取数据失败: ' . $e->getMessage(), 500); + } + } + + /** + * 获取计划的统计信息 + * + * @param int $planId 计划ID + * @return array + */ + private function getPlanStats($planId) + { + try { + // 获取设备数 + $deviceCount = $this->getDeviceCount($planId); + + // 获取已获客数 + $customerCount = $this->getCustomerCount($planId); + + // 获取已添加数 + $addedCount = 1; //$this->getAddedCount($planId); + + // 计算通过率 + $passRate = $customerCount > 0 ? round(($addedCount / $customerCount) * 100) : 0; + + return [ + 'deviceCount' => $deviceCount, + 'customerCount' => $customerCount, + 'addedCount' => $addedCount, + 'passRate' => $passRate + ]; + } catch (\Exception $e) { + return [ + 'deviceCount' => 0, + 'customerCount' => 0, + 'addedCount' => 0, + 'passRate' => 0 + ]; + } + } + + /** + * 获取计划使用的设备数 + * + * @param int $planId 计划ID + * @return int + */ + private function getDeviceCount($planId) + { + try { + // 获取计划 + $plan = Db::name('friend_plan')->where('id', $planId)->find(); + if (!$plan) { + return 0; + } + + // 解析reqConf + $reqConf = json_decode($plan['reqConf'], true); + + // 返回设备数量 + return isset($reqConf['selectedDevices']) ? count($reqConf['selectedDevices']) : 0; + } catch (\Exception $e) { + return 0; + } + } + + /** + * 获取计划的已获客数 + * + * @param int $planId 计划ID + * @return int + */ + private function getCustomerCount($planId) + { + // 模拟数据,实际应从相关表获取 + return rand(10, 50); + } + + /** + * 获取计划的已添加数 + * + * @param int $planId 计划ID + * @return int + */ + private function getAddedCount($planId) + { + // 模拟数据,实际应从相关表获取 + $customerCount = $this->getCustomerCount($planId); + return rand(5, $customerCount); + } + + /** + * 获取计划的最近一次执行时间 + * + * @param int $planId 计划ID + * @return array + */ + private function getLastExecution($planId) + { + // 模拟数据,实际应从执行记录表获取 + $now = time(); + $lastTime = $now - rand(3600, 86400); + $nextTime = $now + rand(3600, 86400); + + return [ + 'lastTime' => date('Y-m-d H:i', $lastTime), + 'nextTime' => date('Y-m-d H:i:s', $nextTime) + ]; + } + + /** + * 验证JSON格式是否正确 + * + * @param string $string + * @return bool + */ + private function validateJson($string) + { + if (empty($string)) { + return true; + } + + json_decode($string); + return (json_last_error() == JSON_ERROR_NONE); } } \ No newline at end of file