Просмотр исходного кода

feat(待接入工具): 添加详情信息管理功能

实现待接入工具详情信息的创建、更新和展示功能
- 在api.js中添加详情相关的API方法
- 修改PendingToolsDetail页面支持详情信息的编辑和展示
- 在服务端添加详情信息的路由处理
- 添加测试脚本验证详情API功能
- 调整AutoAccessTaskDetail页面的表单验证逻辑
max_liu 1 неделя назад
Родитель
Сommit
f90d9aa437
5 измененных файлов с 396 добавлено и 142 удалено
  1. 111 59
      server/routes/pendingTools.js
  2. 1 1
      src/pages/AutoAccessTaskDetail.js
  3. 233 82
      src/pages/PendingToolsDetail.js
  4. 2 0
      src/services/api.js
  5. 49 0
      test_detail_api.js

+ 111 - 59
server/routes/pendingTools.js

@@ -1,8 +1,8 @@
-const express = require('express');
+const express = require("express");
 const router = express.Router();
-const { executeQuery } = require('../config/database');
+const { executeQuery } = require("../config/database");
 
-router.get('/', async (req, res) => {
+router.get("/", async (req, res) => {
   try {
     const { search, toolsName, status, page = 1, pageSize = 10 } = req.query;
     const offset = (page - 1) * pageSize;
@@ -17,25 +17,25 @@ router.get('/', async (req, res) => {
 
     // 添加工具名称搜索条件
     if (toolsName) {
-      whereConditions.push('tools_name LIKE ?');
+      whereConditions.push("tools_name LIKE ?");
       params.push(`%${toolsName}%`);
     }
 
     // 添加状态搜索条件
-    if (status !== undefined && status !== '') {
-      whereConditions.push('status = ?');
+    if (status !== undefined && status !== "") {
+      whereConditions.push("status = ?");
       params.push(parseInt(status));
     }
 
     // 添加全文搜索条件
     if (search) {
-      whereConditions.push('(tools_name LIKE ? OR tools_function_name LIKE ? OR tools_function_desc LIKE ?)');
+      whereConditions.push("(tools_name LIKE ? OR tools_function_name LIKE ? OR tools_function_desc LIKE ?)");
       params.push(`%${search}%`, `%${search}%`, `%${search}%`);
     }
 
     // 构建WHERE子句
     if (whereConditions.length > 0) {
-      sql += ` WHERE ${whereConditions.join(' AND ')}`;
+      sql += ` WHERE ${whereConditions.join(" AND ")}`;
     }
 
     sql += ` ORDER BY create_time DESC LIMIT ? OFFSET ?`;
@@ -46,12 +46,12 @@ router.get('/', async (req, res) => {
     let countParams = [];
 
     if (whereConditions.length > 0) {
-      countSql += ` WHERE ${whereConditions.join(' AND ')}`;
+      countSql += ` WHERE ${whereConditions.join(" AND ")}`;
       // 重新构建计数查询的参数
       if (toolsName) {
         countParams.push(`%${toolsName}%`);
       }
-      if (status !== undefined && status !== '') {
+      if (status !== undefined && status !== "") {
         countParams.push(parseInt(status));
       }
       if (search) {
@@ -59,50 +59,41 @@ router.get('/', async (req, res) => {
       }
     }
 
-    const [data, countResult] = await Promise.all([
-      executeQuery(sql, params),
-      executeQuery(countSql, countParams)
-    ]);
+    const [data, countResult] = await Promise.all([executeQuery(sql, params), executeQuery(countSql, countParams)]);
 
     res.json({
       data,
       total: countResult[0].total,
       page: parseInt(page),
-      pageSize: parseInt(pageSize)
+      pageSize: parseInt(pageSize),
     });
   } catch (error) {
-    console.error('Error fetching pending tools:', error);
-    res.status(500).json({ error: 'Internal server error' });
+    console.error("Error fetching pending tools:", error);
+    res.status(500).json({ error: "Internal server error" });
   }
 });
 
 // 新增待接入工具
-router.post('/', async (req, res) => {
+router.post("/", async (req, res) => {
   try {
-    const {
-      search_task_id,
-      tools_name,
-      tools_function_name,
-      tools_function_desc,
-      status = 0
-    } = req.body;
+    const { search_task_id, tools_name, tools_function_name, tools_function_desc } = req.body;
 
     // 验证必填字段
     if (!search_task_id || !tools_name || !tools_function_name || !tools_function_desc) {
       return res.status(400).json({
         success: false,
-        message: '缺少必填字段'
+        message: "缺少必填字段",
       });
     }
 
     // 检查search_task_id是否已存在
-    const checkSql = 'SELECT search_task_id FROM tools_info_search_task WHERE search_task_id = ?';
+    const checkSql = "SELECT search_task_id FROM tools_info_search_task WHERE search_task_id = ?";
     const existingTool = await executeQuery(checkSql, [search_task_id]);
-    
+
     if (existingTool.length > 0) {
       return res.status(400).json({
         success: false,
-        message: '工具ID已存在'
+        message: "工具ID已存在",
       });
     }
 
@@ -112,36 +103,34 @@ router.post('/', async (req, res) => {
       (search_task_id, tools_name, tools_function_name, tools_function_desc, status, create_time, update_time)
       VALUES (?, ?, ?, ?, ?, NOW(), NOW())
     `;
-    
+
     const result = await executeQuery(insertSql, [
       search_task_id,
       tools_name,
       tools_function_name,
       tools_function_desc,
-      status
     ]);
 
     res.json({
       success: true,
-      message: '新增工具成功',
+      message: "新增工具成功",
       data: {
         search_task_id,
         tools_name,
         tools_function_name,
         tools_function_desc,
-        status
-      }
+      },
     });
   } catch (error) {
-    console.error('新增工具失败:', error);
+    console.error("新增工具失败:", error);
     res.status(500).json({
       success: false,
-      message: '服务器内部错误'
+      message: "服务器内部错误",
     });
   }
 });
 
-router.get('/:id', async (req, res) => {
+router.get("/:id", async (req, res) => {
   try {
     const { id } = req.params;
 
@@ -153,32 +142,29 @@ router.get('/:id', async (req, res) => {
     `;
 
     const detailSql = `
-      SELECT search_task_id, search_channel, search_result, fail_reason,
-             create_time, update_time
+      SELECT id, search_task_id, search_channel, query, status, 
+             search_result, fail_reason, create_time, update_time
       FROM tools_info_search_task_detail
       WHERE search_task_id = ?
     `;
 
-    const [taskData, detailData] = await Promise.all([
-      executeQuery(taskSql, [id]),
-      executeQuery(detailSql, [id])
-    ]);
+    const [taskData, detailData] = await Promise.all([executeQuery(taskSql, [id]), executeQuery(detailSql, [id])]);
 
     if (taskData.length === 0) {
-      return res.status(404).json({ error: 'Tool not found' });
+      return res.status(404).json({ error: "Tool not found" });
     }
 
     res.json({
       task: taskData[0],
-      detail: detailData[0] || null
+      detail: detailData.length > 0 ? detailData : null,
     });
   } catch (error) {
-    console.error('Error fetching pending tool detail:', error);
-    res.status(500).json({ error: 'Internal server error' });
+    console.error("Error fetching pending tool detail:", error);
+    res.status(500).json({ error: "Internal server error" });
   }
 });
 
-router.put('/:id', async (req, res) => {
+router.put("/:id", async (req, res) => {
   try {
     const { id } = req.params;
     const { tools_name, tools_function_name, tools_function_desc, status, fail_reason } = req.body;
@@ -190,27 +176,93 @@ router.put('/:id', async (req, res) => {
       WHERE search_task_id = ?
     `;
 
-    await executeQuery(sql, [tools_name, tools_function_name, tools_function_desc, status, fail_reason, id]);
+    await executeQuery(sql, [
+      tools_name ?? null,
+      tools_function_name ?? null,
+      tools_function_desc ?? null,
+      status ?? null,
+      fail_reason ?? null,
+      id,
+    ]);
+
+    res.json({ message: "Tool updated successfully" });
+  } catch (error) {
+    console.error("Error updating pending tool:", error);
+    res.status(500).json({ error: "Internal server error" });
+  }
+});
+
+// 创建详情信息
+router.post("/detail", async (req, res) => {
+  try {
+    const { search_task_id, search_channel, query, status, search_result, fail_reason } = req.body;
+
+    const sql = `
+      INSERT INTO tools_info_search_task_detail
+      (search_task_id, search_channel, query, status, search_result, fail_reason, create_time, update_time)
+      VALUES (?, ?, ?, ?, ?, ?, NOW(), NOW())
+    `;
+
+    const result = await executeQuery(sql, [
+      search_task_id,
+      search_channel ?? null,
+      query ?? null,
+      status ?? 1,
+      search_result ?? null,
+      fail_reason ?? null,
+    ]);
+
+    res.json({
+      message: "Detail created successfully",
+      id: result.insertId,
+    });
+  } catch (error) {
+    console.error("Error creating detail:", error);
+    res.status(500).json({ error: "Internal server error" });
+  }
+});
+
+// 更新详情信息
+router.put("/detail/:detailId", async (req, res) => {
+  try {
+    const { detailId } = req.params;
+    const { search_channel, query, status, search_result, fail_reason } = req.body;
+
+    const sql = `
+      UPDATE tools_info_search_task_detail
+      SET search_channel = ?, query = ?, status = ?, 
+          search_result = ?, fail_reason = ?, update_time = NOW()
+      WHERE id = ?
+    `;
+
+    await executeQuery(sql, [
+      search_channel ?? null,
+      query ?? null,
+      status ?? null,
+      search_result ?? null,
+      fail_reason ?? null,
+      detailId,
+    ]);
 
-    res.json({ message: 'Tool updated successfully' });
+    res.json({ message: "Detail updated successfully" });
   } catch (error) {
-    console.error('Error updating pending tool:', error);
-    res.status(500).json({ error: 'Internal server error' });
+    console.error("Error updating detail:", error);
+    res.status(500).json({ error: "Internal server error" });
   }
 });
 
-router.delete('/:id', async (req, res) => {
+router.delete("/:id", async (req, res) => {
   try {
     const { id } = req.params;
 
-    await executeQuery('DELETE FROM tools_info_search_task_detail WHERE search_task_id = ?', [id]);
-    await executeQuery('DELETE FROM tools_info_search_task WHERE search_task_id = ?', [id]);
+    await executeQuery("DELETE FROM tools_info_search_task_detail WHERE search_task_id = ?", [id]);
+    await executeQuery("DELETE FROM tools_info_search_task WHERE search_task_id = ?", [id]);
 
-    res.json({ message: 'Tool deleted successfully' });
+    res.json({ message: "Tool deleted successfully" });
   } catch (error) {
-    console.error('Error deleting pending tool:', error);
-    res.status(500).json({ error: 'Internal server error' });
+    console.error("Error deleting pending tool:", error);
+    res.status(500).json({ error: "Internal server error" });
   }
 });
 
-module.exports = router;
+module.exports = router;

+ 1 - 1
src/pages/AutoAccessTaskDetail.js

@@ -232,7 +232,7 @@ const AutoAccessTaskDetail = () => {
                   name="api_provider"
                   rules={[
                     {
-                      required: true,
+                      required: accessType === "api_no_crack" || accessType === "api_crack",
                       message: "请输入API提供方",
                     },
                   ]}

+ 233 - 82
src/pages/PendingToolsDetail.js

@@ -1,5 +1,5 @@
 import React, { useState, useEffect } from "react";
-import { Form, Input, Select, Button, Card, Descriptions, Tag, message, Spin } from "antd";
+import { Form, Input, Select, Button, Card, Descriptions, Tag, message, Spin, Row, Col } from "antd";
 import { useParams, useNavigate, useLocation } from "react-router-dom";
 import { ArrowLeftOutlined } from "@ant-design/icons";
 import { pendingToolsApi } from "../services/api";
@@ -30,6 +30,20 @@ const PendingToolsDetail = () => {
   const fetchData = async () => {
     try {
       const response = await pendingToolsApi.getDetail(id);
+      
+      // 如果没有详情数据,创建一个默认的详情项
+      if (!response.data.detail || response.data.detail.length === 0) {
+        response.data.detail = [{
+          id: null, // 新创建的详情项没有ID
+          search_task_id: id,
+          search_channel: '',
+          query: '',
+          status: 1,
+          search_result: '',
+          fail_reason: ''
+        }];
+      }
+      
       setData(response.data);
       if (response.data.task) {
         form.setFieldsValue(response.data.task);
@@ -44,10 +58,39 @@ const PendingToolsDetail = () => {
   const handleSave = async (values) => {
     setSaving(true);
     try {
+      // 保存基本信息到 tools_info_search_task 表
       await pendingToolsApi.update(id, values);
+
+      // 保存详情信息到 tools_info_search_task_detail 表
+      if (data.detail && data.detail.length > 0) {
+        for (const detail of data.detail) {
+          // 如果是新创建的详情项(id为null),使用创建API
+          if (detail.id === null || detail.id === undefined) {
+            await pendingToolsApi.createDetail({
+              search_task_id: detail.search_task_id,
+              search_channel: detail.search_channel,
+              query: detail.query,
+              status: detail.status,
+              search_result: detail.search_result,
+              fail_reason: detail.fail_reason
+            });
+          } else {
+            // 现有的详情项,使用更新API
+            await pendingToolsApi.updateDetail(detail.id, {
+              search_channel: detail.search_channel,
+              query: detail.query,
+              status: detail.status,
+              search_result: detail.search_result,
+              fail_reason: detail.fail_reason,
+            });
+          }
+        }
+      }
+
       message.success("更新成功");
       navigate("/pending-tools");
     } catch (error) {
+      console.error("更新失败:", error);
       message.error("更新失败");
     } finally {
       setSaving(false);
@@ -88,71 +131,162 @@ const PendingToolsDetail = () => {
       </div>
 
       {isEditMode ? (
-        <Card
-          title="编辑待接入工具"
-          className="shadow-lg border-0"
-        >
-          <Form
-            form={form}
-            layout="vertical"
-            onFinish={handleSave}
-            className="form-container"
+        <div className="space-y-6">
+          {/* 编辑表单 */}
+          <Card
+            title="基本信息"
+            className="shadow-lg border-0"
           >
-            <Form.Item
-              label="工具名称"
-              name="tools_name"
-              rules={[{ required: true, message: "请输入工具名称" }]}
+            <Form
+              form={form}
+              layout="vertical"
+              onFinish={handleSave}
+              className="w-full"
             >
-              <Input />
-            </Form.Item>
+              <Row gutter={16}>
+                <Col span={12}>
+                  <Form.Item
+                    label="工具名称"
+                    name="tools_name"
+                    rules={[{ required: true, message: "请输入工具名称" }]}
+                  >
+                    <Input />
+                  </Form.Item>
+                </Col>
+                <Col span={12}>
+                  <Form.Item
+                    label="工具功能名称"
+                    name="tools_function_name"
+                    rules={[{ required: false, message: "请输入工具功能名称" }]}
+                  >
+                    <Input />
+                  </Form.Item>
+                </Col>
+              </Row>
+              <Form.Item
+                label="状态"
+                name="status"
+                rules={[{ required: false, message: "请选择状态" }]}
+              >
+                <Select>
+                  <Option value={0}>待处理</Option>
+                  <Option value={1}>处理中</Option>
+                  <Option value={2}>已完成</Option>
+                  <Option value={3}>失败</Option>
+                  <Option value={4}>待审核</Option>
+                </Select>
+              </Form.Item>
 
-            <Form.Item
-              label="工具功能名称"
-              name="tools_function_name"
-              rules={[{ required: false, message: "请输入工具功能名称" }]}
-            >
-              <Input />
-            </Form.Item>
-            <Form.Item
-              label="状态"
-              name="status"
-              rules={[{ required: false, message: "请选择状态" }]}
-            >
-              <Select>
-                <Option value={0}>待处理</Option>
-                <Option value={1}>处理中</Option>
-                <Option value={2}>已完成</Option>
-                <Option value={3}>失败</Option>
-                <Option value={4}>待审核</Option>
-              </Select>
-            </Form.Item>
-
-            <Form.Item
-              label="工具功能描述"
-              name="tools_function_desc"
+              <Form.Item
+                label="工具功能描述"
+                name="tools_function_desc"
+              >
+                <TextArea rows={4} />
+              </Form.Item>
+            </Form>
+          </Card>
+
+          {/* 详情信息编辑 */}
+          {data.detail && data.detail.length > 0 && (
+            <div className="space-y-4">
+              <h3 className="text-lg font-medium text-gray-900">详情信息编辑</h3>
+              {data.detail.map((detail, index) => (
+                <Card
+                  key={index}
+                  title={`详细信息 ${index + 1}`}
+                  className="shadow-lg border-0"
+                >
+                  <Form
+                    layout="vertical"
+                    initialValues={{
+                      search_channel: detail.search_channel,
+                      query: detail.query,
+                      status: detail.status,
+                      search_result: detail.search_result,
+                      fail_reason: detail.fail_reason,
+                    }}
+                    onValuesChange={(changedValues, allValues) => {
+                      // 更新详情数据
+                      const newDetail = [...data.detail];
+                      newDetail[index] = { ...newDetail[index], ...allValues };
+                      setData({ ...data, detail: newDetail });
+                    }}
+                  >
+                    <Row gutter={16}>
+                      <Col span={12}>
+                        <Form.Item
+                          label="检索渠道"
+                          name="search_channel"
+                          rules={[{ required: true, message: "请选择检索渠道" }]}
+                        >
+                          <Select placeholder="请选择检索渠道">
+                            <Option value="302ai">302ai</Option>
+                            <Option value="crawl_system">crawl_system</Option>
+                            <Option value="ai_search">ai_search</Option>
+                          </Select>
+                        </Form.Item>
+                      </Col>
+                      <Col span={12}></Col>
+                    </Row>
+
+                    <Form.Item
+                      label="检索结果"
+                      name="search_result"
+                    >
+                      <TextArea
+                        rows={6}
+                        placeholder="请输入检索结果"
+                      />
+                    </Form.Item>
+                  </Form>
+                </Card>
+              ))}
+            </div>
+          )}
+
+          {/* 统一保存按钮 */}
+          <div className="flex justify-end space-x-4 pt-6 border-t">
+            <Button
+              size="large"
+              onClick={() => navigate("/pending-tools")}
             >
-              <TextArea rows={4} />
-            </Form.Item>
+              取消
+            </Button>
+            <Button
+              type="primary"
+              size="large"
+              loading={saving}
+              onClick={() => {
+                form
+                  .validateFields()
+                  .then((values) => {
+                    // 验证详情数据
+                    let hasError = false;
+                    if (data.detail && data.detail.length > 0) {
+                      for (let i = 0; i < data.detail.length; i++) {
+                        const detail = data.detail[i];
+                        if (!detail.search_channel) {
+                          message.error(`详细信息 ${i + 1} 的检索渠道不能为空`);
+                          hasError = true;
+                          break;
+                        }
+                      }
+                    }
 
-            <Form.Item
-              label="失败原因"
-              name="fail_reason"
+                    if (!hasError) {
+                      handleSave(values);
+                    }
+                  })
+                  .catch((error) => {
+                    console.error("表单验证失败:", error);
+                    message.error("请检查表单填写是否正确");
+                  });
+              }}
             >
-              <TextArea rows={3} />
-            </Form.Item>
-
-            <div className="button-group">
-              <Button onClick={() => navigate("/pending-tools")}>取消</Button>
-              <Button
-                type="primary"
-                htmlType="submit"
-                loading={saving}
-              >
-                保存
-              </Button>
-            </div>
-          </Form>
-        </Card>
+              保存所有修改
+            </Button>
+          </div>
+        </div>
       ) : (
         <div className="space-y-6">
           <Card
@@ -163,6 +297,7 @@ const PendingToolsDetail = () => {
               column={2}
               bordered
               size="small"
+              labelStyle={{ width: "120px" }}
             >
               <Descriptions.Item label="工具ID">{data.task.search_task_id}</Descriptions.Item>
               <Descriptions.Item label="工具名称">{data.task.tools_name}</Descriptions.Item>
@@ -191,29 +326,45 @@ const PendingToolsDetail = () => {
             </Descriptions>
           </Card>
 
-          {data.detail && (
-            <Card
-              title="详细信息"
-              className="shadow-lg border-0"
-            >
-              <Descriptions
-                column={2}
-                bordered
-                size="small"
-              >
-                <Descriptions.Item label="检索渠道">{data.detail.search_channel}</Descriptions.Item>
-                <Descriptions.Item
-                  label="检索结果"
-                  span={2}
+          {data.detail && data.detail.length > 0 && (
+            <div className="space-y-4">
+              {data.detail.map((detail, index) => (
+                <Card
+                  key={index}
+                  title={`详细信息 ${index + 1}`}
+                  className="shadow-lg border-0"
                 >
-                  <div style={{ maxHeight: "200px", overflow: "auto" }}>
-                    <pre style={{ whiteSpace: "pre-wrap", wordBreak: "break-word" }}>
-                      {data.detail.search_result || "无"}
-                    </pre>
-                  </div>
-                </Descriptions.Item>
-              </Descriptions>
-            </Card>
+                  <Descriptions
+                    column={2}
+                    bordered
+                    size="small"
+                    labelStyle={{ width: "120px" }}
+                  >
+                    <Descriptions.Item label="检索渠道">{detail.search_channel}</Descriptions.Item>
+                    <Descriptions.Item label="创建时间">
+                      {moment(detail.create_time).format("YYYY-MM-DD HH:mm:ss")}
+                    </Descriptions.Item>
+                    <Descriptions.Item
+                      label="检索结果"
+                      span={2}
+                    >
+                      <div style={{ maxHeight: "200px", overflow: "auto" }}>
+                        <pre style={{ whiteSpace: "pre-wrap", wordBreak: "break-word" }}>
+                          {detail.search_result || "无"}
+                        </pre>
+                      </div>
+                    </Descriptions.Item>
+
+                    {/* <Descriptions.Item
+                      label="失败原因"
+                      span={2}
+                    >
+                      {detail.fail_reason}
+                    </Descriptions.Item> */}
+                  </Descriptions>
+                </Card>
+              ))}
+            </div>
           )}
         </div>
       )}

+ 2 - 0
src/services/api.js

@@ -25,6 +25,8 @@ export const pendingToolsApi = {
   create: (data) => api.post("/pending-tools", data),
   update: (id, data) => api.put(`/pending-tools/${id}`, data),
   delete: (id) => api.delete(`/pending-tools/${id}`),
+  updateDetail: (detailId, data) => api.put(`/pending-tools/detail/${detailId}`, data),
+  createDetail: (data) => api.post("/pending-tools/detail", data),
 };
 
 export const autoAccessTasksApi = {

+ 49 - 0
test_detail_api.js

@@ -0,0 +1,49 @@
+const axios = require('axios');
+
+const API_BASE = 'http://localhost:3001/api';
+
+async function testDetailAPI() {
+  try {
+    console.log('1. 测试获取任务详情(没有详情数据的任务)...');
+    const response1 = await axios.get(`${API_BASE}/pending-tools/20250922200530991485377`);
+    console.log('响应数据:', JSON.stringify(response1.data, null, 2));
+    
+    console.log('\n2. 测试创建详情数据...');
+    const createResponse = await axios.post(`${API_BASE}/pending-tools/detail`, {
+      search_task_id: '20250922200530991485377',
+      search_channel: '302ai',
+      query: '测试查询内容',
+      status: 1,
+      search_result: '测试搜索结果',
+      fail_reason: ''
+    });
+    console.log('创建响应:', JSON.stringify(createResponse.data, null, 2));
+    
+    console.log('\n3. 再次获取任务详情(现在应该有详情数据了)...');
+    const response2 = await axios.get(`${API_BASE}/pending-tools/20250922200530991485377`);
+    console.log('响应数据:', JSON.stringify(response2.data, null, 2));
+    
+    if (response2.data.detail && response2.data.detail.length > 0) {
+      const detailId = response2.data.detail[0].id;
+      console.log(`\n4. 测试更新详情数据(ID: ${detailId})...`);
+      const updateResponse = await axios.put(`${API_BASE}/pending-tools/detail/${detailId}`, {
+        search_channel: '302ai',
+        query: '更新后的查询内容',
+        status: 2,
+        search_result: '更新后的搜索结果',
+        fail_reason: ''
+      });
+      console.log('更新响应:', JSON.stringify(updateResponse.data, null, 2));
+      
+      console.log('\n5. 最终验证 - 获取更新后的详情...');
+      const response3 = await axios.get(`${API_BASE}/pending-tools/20250922200530991485377`);
+      console.log('最终响应数据:', JSON.stringify(response3.data, null, 2));
+    }
+    
+    console.log('\n✅ 所有测试完成!');
+  } catch (error) {
+    console.error('❌ 测试失败:', error.response?.data || error.message);
+  }
+}
+
+testDetailAPI();