238 lines
7.8 KiB
Go
238 lines
7.8 KiB
Go
package handler
|
||
|
||
import (
|
||
"net/http"
|
||
"strconv"
|
||
|
||
"tcm-agent/internal/crawler"
|
||
"tcm-agent/internal/dao"
|
||
"tcm-agent/internal/service"
|
||
|
||
"github.com/gin-gonic/gin"
|
||
)
|
||
|
||
// ========================================================================
|
||
// KBCrawlHandler —— 药品抓取任务管理 API
|
||
// ========================================================================
|
||
// 暴露 7 个端点(前缀 /api/v1/kb/admin/crawl,复用 KB 管理鉴权:
|
||
// 口令头或面板 JWT 均可):
|
||
//
|
||
// GET /sources 可用抓取源列表(前端下拉)
|
||
// GET /tasks 任务列表(含实时 running 标记)
|
||
// POST /tasks 创建任务
|
||
// PUT /tasks/:id 更新任务(名称/目标库/调度/限量/启停)
|
||
// DELETE /tasks/:id 软删除任务
|
||
// POST /tasks/:id/run 立即抓取(异步,立即返回)
|
||
// GET /tasks/:id/logs 任务运行历史(最近 N 条)
|
||
// ========================================================================
|
||
|
||
// KBCrawlHandler 抓取任务处理器(无状态,直接调 dao/service)
|
||
type KBCrawlHandler struct{}
|
||
|
||
// NewKBCrawlHandler 构造
|
||
func NewKBCrawlHandler() *KBCrawlHandler { return &KBCrawlHandler{} }
|
||
|
||
// ListSources 可用抓取源
|
||
// GET /api/v1/kb/admin/crawl/sources
|
||
func (h *KBCrawlHandler) ListSources(c *gin.Context) {
|
||
c.JSON(http.StatusOK, gin.H{"code": 200, "data": crawler.ListSources()})
|
||
}
|
||
|
||
// ListTasks 任务列表
|
||
// GET /api/v1/kb/admin/crawl/tasks
|
||
//
|
||
// 每行附加 running 字段(内存实时状态)——DB 的 last_status 有落库延迟,
|
||
// 前端旋转图标要跟内存状态走
|
||
func (h *KBCrawlHandler) ListTasks(c *gin.Context) {
|
||
rows, err := dao.KBCrawlTaskList()
|
||
if err != nil {
|
||
c.JSON(http.StatusOK, gin.H{"code": 500, "message": err.Error()})
|
||
return
|
||
}
|
||
out := make([]gin.H, 0, len(rows))
|
||
for _, t := range rows {
|
||
out = append(out, gin.H{
|
||
"id": t.ID, "name": t.Name, "source": t.Source,
|
||
"library_id": t.LibraryID, "schedule_type": t.ScheduleType,
|
||
"interval_hours": t.IntervalHours, "run_at_hour": t.RunAtHour,
|
||
"run_at_weekday": t.RunAtWeekday, "items_per_run": t.ItemsPerRun,
|
||
"progress_offset": t.ProgressOffset, "status": t.Status,
|
||
"last_run_at": t.LastRunAt, "last_status": t.LastStatus,
|
||
"last_message": t.LastMessage, "created_at": t.CreatedAt,
|
||
"running": service.CrawlTaskRunning(t.ID),
|
||
})
|
||
}
|
||
c.JSON(http.StatusOK, gin.H{"code": 200, "data": out})
|
||
}
|
||
|
||
// crawlTaskRequest 创建/更新任务的入参
|
||
type crawlTaskRequest struct {
|
||
Name string `json:"name"`
|
||
Source string `json:"source"`
|
||
LibraryID uint `json:"library_id"`
|
||
ScheduleType string `json:"schedule_type"`
|
||
IntervalHours int `json:"interval_hours"`
|
||
RunAtHour int `json:"run_at_hour"`
|
||
RunAtWeekday int `json:"run_at_weekday"`
|
||
ItemsPerRun int `json:"items_per_run"`
|
||
Status *int `json:"status"` // 指针区分"没传"和"传了 0(禁用)"
|
||
}
|
||
|
||
// normalize 归一化 + 校验入参(创建和更新共用)
|
||
func (r *crawlTaskRequest) normalize() string {
|
||
if r.Name == "" {
|
||
return "任务名称不能为空"
|
||
}
|
||
if r.Source == "" {
|
||
r.Source = "zhongyoo"
|
||
}
|
||
if _, ok := crawler.GetSource(r.Source); !ok {
|
||
return "抓取源不存在: " + r.Source
|
||
}
|
||
if r.LibraryID == 0 {
|
||
return "请选择目标知识库"
|
||
}
|
||
switch r.ScheduleType {
|
||
case "", "manual":
|
||
r.ScheduleType = "manual"
|
||
case "interval":
|
||
if r.IntervalHours < 1 {
|
||
r.IntervalHours = 24
|
||
}
|
||
case "daily", "weekly":
|
||
if r.RunAtHour < 0 || r.RunAtHour > 23 {
|
||
r.RunAtHour = 3
|
||
}
|
||
if r.RunAtWeekday < 0 || r.RunAtWeekday > 6 {
|
||
r.RunAtWeekday = 1
|
||
}
|
||
default:
|
||
return "调度类型不合法: " + r.ScheduleType
|
||
}
|
||
if r.ItemsPerRun < 1 || r.ItemsPerRun > 500 {
|
||
r.ItemsPerRun = 50
|
||
}
|
||
return ""
|
||
}
|
||
|
||
// CreateTask 创建任务
|
||
// POST /api/v1/kb/admin/crawl/tasks
|
||
func (h *KBCrawlHandler) CreateTask(c *gin.Context) {
|
||
var req crawlTaskRequest
|
||
if err := c.ShouldBindJSON(&req); err != nil {
|
||
c.JSON(http.StatusOK, gin.H{"code": 400, "message": "参数格式错误: " + err.Error()})
|
||
return
|
||
}
|
||
if msg := req.normalize(); msg != "" {
|
||
c.JSON(http.StatusOK, gin.H{"code": 400, "message": msg})
|
||
return
|
||
}
|
||
// 目标库必须真实存在(防手滑填错 ID,跑的时候才发现)
|
||
if _, err := dao.KBGetLibrary(req.LibraryID); err != nil {
|
||
c.JSON(http.StatusOK, gin.H{"code": 400, "message": "目标知识库不存在,请先在知识库管理里创建"})
|
||
return
|
||
}
|
||
row := &dao.KBCrawlTaskRow{
|
||
Name: req.Name, Source: req.Source, LibraryID: req.LibraryID,
|
||
ScheduleType: req.ScheduleType, IntervalHours: req.IntervalHours,
|
||
RunAtHour: req.RunAtHour, RunAtWeekday: req.RunAtWeekday,
|
||
ItemsPerRun: req.ItemsPerRun, Status: 1,
|
||
}
|
||
if err := dao.KBCrawlTaskCreate(row); err != nil {
|
||
c.JSON(http.StatusOK, gin.H{"code": 500, "message": err.Error()})
|
||
return
|
||
}
|
||
c.JSON(http.StatusOK, gin.H{"code": 200, "data": row, "message": "任务已创建"})
|
||
}
|
||
|
||
// UpdateTask 更新任务
|
||
// PUT /api/v1/kb/admin/crawl/tasks/:id
|
||
func (h *KBCrawlHandler) UpdateTask(c *gin.Context) {
|
||
id, err := parseUintParam(c, "id")
|
||
if err != nil {
|
||
c.JSON(http.StatusOK, gin.H{"code": 400, "message": err.Error()})
|
||
return
|
||
}
|
||
var req crawlTaskRequest
|
||
if err := c.ShouldBindJSON(&req); err != nil {
|
||
c.JSON(http.StatusOK, gin.H{"code": 400, "message": "参数格式错误: " + err.Error()})
|
||
return
|
||
}
|
||
if msg := req.normalize(); msg != "" {
|
||
c.JSON(http.StatusOK, gin.H{"code": 400, "message": msg})
|
||
return
|
||
}
|
||
if _, err := dao.KBGetLibrary(req.LibraryID); err != nil {
|
||
c.JSON(http.StatusOK, gin.H{"code": 400, "message": "目标知识库不存在"})
|
||
return
|
||
}
|
||
fields := map[string]any{
|
||
"name": req.Name, "source": req.Source, "library_id": req.LibraryID,
|
||
"schedule_type": req.ScheduleType, "interval_hours": req.IntervalHours,
|
||
"run_at_hour": req.RunAtHour, "run_at_weekday": req.RunAtWeekday,
|
||
"items_per_run": req.ItemsPerRun,
|
||
}
|
||
if req.Status != nil {
|
||
fields["status"] = *req.Status
|
||
}
|
||
if err := dao.KBCrawlTaskUpdate(id, fields); err != nil {
|
||
c.JSON(http.StatusOK, gin.H{"code": 500, "message": err.Error()})
|
||
return
|
||
}
|
||
c.JSON(http.StatusOK, gin.H{"code": 200, "message": "任务已更新"})
|
||
}
|
||
|
||
// DeleteTask 软删除任务
|
||
// DELETE /api/v1/kb/admin/crawl/tasks/:id
|
||
func (h *KBCrawlHandler) DeleteTask(c *gin.Context) {
|
||
id, err := parseUintParam(c, "id")
|
||
if err != nil {
|
||
c.JSON(http.StatusOK, gin.H{"code": 400, "message": err.Error()})
|
||
return
|
||
}
|
||
if service.CrawlTaskRunning(id) {
|
||
c.JSON(http.StatusOK, gin.H{"code": 400, "message": "任务正在运行中,等本次跑完再删除"})
|
||
return
|
||
}
|
||
if err := dao.KBCrawlTaskDelete(id); err != nil {
|
||
c.JSON(http.StatusOK, gin.H{"code": 500, "message": err.Error()})
|
||
return
|
||
}
|
||
c.JSON(http.StatusOK, gin.H{"code": 200, "message": "任务已删除"})
|
||
}
|
||
|
||
// RunTask 立即抓取(异步触发)
|
||
// POST /api/v1/kb/admin/crawl/tasks/:id/run
|
||
func (h *KBCrawlHandler) RunTask(c *gin.Context) {
|
||
id, err := parseUintParam(c, "id")
|
||
if err != nil {
|
||
c.JSON(http.StatusOK, gin.H{"code": 400, "message": err.Error()})
|
||
return
|
||
}
|
||
if err := service.TriggerCrawlTask(id, "manual"); err != nil {
|
||
c.JSON(http.StatusOK, gin.H{"code": 400, "message": err.Error()})
|
||
return
|
||
}
|
||
c.JSON(http.StatusOK, gin.H{
|
||
"code": 200,
|
||
"message": "抓取已在后台启动,可在运行历史里查看进度(每批约需 1-3 分钟)",
|
||
})
|
||
}
|
||
|
||
// ListLogs 任务运行历史
|
||
// GET /api/v1/kb/admin/crawl/tasks/:id/logs?limit=20
|
||
func (h *KBCrawlHandler) ListLogs(c *gin.Context) {
|
||
id, err := parseUintParam(c, "id")
|
||
if err != nil {
|
||
c.JSON(http.StatusOK, gin.H{"code": 400, "message": err.Error()})
|
||
return
|
||
}
|
||
limit, _ := strconv.Atoi(c.DefaultQuery("limit", "20"))
|
||
rows, err := dao.KBCrawlLogList(id, limit)
|
||
if err != nil {
|
||
c.JSON(http.StatusOK, gin.H{"code": 500, "message": err.Error()})
|
||
return
|
||
}
|
||
c.JSON(http.StatusOK, gin.H{"code": 200, "data": rows})
|
||
}
|