// Package handler 处理所有 HTTP 请求,负责参数校验、调用 Service 层和返回响应 // 按业务模块分为简历管理、后台管理、AI 能力、文档处理、聊天对话等处理器 package handler import ( "fmt" "io" "log" "net/http" "os" "path/filepath" "resume-platform/internal/model" "resume-platform/internal/service/document" "resume-platform/internal/service/portfolio" "resume-platform/internal/service/quiz" "resume-platform/internal/service/resume" "resume-platform/internal/service/system" "resume-platform/internal/service/user" "strings" "github.com/gin-gonic/gin" ) // ResumeHandler 简历相关 HTTP 请求处理器,封装简历、作品集、用户、题库等业务的接口处理 type ResumeHandler struct { resumeService resume.ResumeService // 简历业务服务层实例 documentService document.DocumentService // 文档服务层实例 userService user.UserService // 用户服务层实例 portfolioService portfolio.PortfolioService // 作品集服务层实例 quizService quiz.QuizService // 答题服务层实例 systemService system.SystemService // 系统服务层实例 } // NewResumeHandler 创建简历处理器实例 // @param resumeService 简历业务服务 // @param documentService 文档服务 // @param userService 用户服务 // @param portfolioService 作品集服务 // @param quizService 答题服务 // @param systemService 系统服务 // @return *ResumeHandler 处理器实例 // @author sunct func NewResumeHandler(resumeService resume.ResumeService, documentService document.DocumentService, userService user.UserService, portfolioService portfolio.PortfolioService, quizService quiz.QuizService, systemService system.SystemService) *ResumeHandler { return &ResumeHandler{ resumeService: resumeService, documentService: documentService, userService: userService, portfolioService: portfolioService, quizService: quizService, systemService: systemService, } } // ShowResume 渲染简历详情页面,根据路由参数获取简历数据并渲染对应模板 // 支持密码保护校验,通过 Cookie 记录验证状态 // @param c Gin 上下文 // @author sunct func (h *ResumeHandler) ShowResume(c *gin.Context) { route := c.Param("route") if route == "" || route == "/" { route = "default" } resume, err := h.resumeService.GetResumeByRoute(route) if err != nil { if route == "default" { resume, err = h.resumeService.InitDefaultResume() if err != nil { c.Status(http.StatusInternalServerError) return } } else { c.HTML(http.StatusNotFound, "web/404", nil) return } } if resume.Password != "" && resume.Password != "null" && resume.Password != "undefined" { sessionPassword, _ := c.Cookie("resume_password_" + route) if sessionPassword != resume.Password { config, _ := h.systemService.GetSystemConfig() if config == nil { config = &model.SystemConfig{ WebsiteDomain: "http://localhost:8090", WebsiteTitle: "Resume Platform", WebsiteDesc: "现代化的开源简历平台,支持多人简历管理、自定义模板、实时预览", WebsiteKeywords: "简历平台,开源,个人主页,作品集,简历管理", } } c.HTML(http.StatusOK, "web/password", gin.H{ "route": route, "SystemConfig": config, }) return } } tmplName := "web/" + resume.Template c.HTML(http.StatusOK, tmplName, resume) } // VerifyPassword 验证简历访问密码,验证通过后写入 Cookie // @param c Gin 上下文 // @author sunct func (h *ResumeHandler) VerifyPassword(c *gin.Context) { route := c.Param("route") password := c.PostForm("password") resume, err := h.resumeService.GetResumeByRoute(route) if err != nil { Unauthorized(c, "简历不存在") return } if password == resume.Password { c.SetCookie("resume_password_"+route, password, 3600, "/", "", false, true) Success(c, "验证成功") } else { Unauthorized(c, "密码错误") } } // GetResumeAPI 获取简历数据API,通过query参数route指定简历路由 // @param c Gin 上下文 // @author sunct func (h *ResumeHandler) GetResumeAPI(c *gin.Context) { route := c.Query("route") if route == "" { route = "default" } resume, err := h.resumeService.GetResumeByRoute(route) if err != nil { Error(c, err.Error()) return } OK(c, resume) } // GetResumeByIDAPI 根据ID获取简历API // @param c Gin 上下文 // @author sunct func (h *ResumeHandler) GetResumeByIDAPI(c *gin.Context) { id := c.Param("id") if id == "" { BadRequest(c, "id is required") return } resume, err := h.resumeService.GetResume(id) if err != nil { NotFound(c, "resume not found") return } OK(c, resume) } // UpdateResumeByIDAPI 根据ID更新简历API // @param c Gin 上下文 // @author sunct func (h *ResumeHandler) UpdateResumeByIDAPI(c *gin.Context) { id := c.Param("id") if id == "" { BadRequest(c, "id is required") return } var req struct { Route string `json:"route"` Template string `json:"template"` ShowInHome bool `json:"show_in_home"` BasicInfo *model.BasicInfo `json:"basic_info"` Experience []model.Experience `json:"experience"` Education []model.Education `json:"education"` Skills []model.Skill `json:"skills"` Projects []model.Project `json:"projects"` } if err := c.ShouldBindJSON(&req); err != nil { BadRequest(c, err.Error()) return } update := &model.ResumeUpdate{} if req.Route != "" { update.Route = &req.Route } if req.Template != "" { update.Template = &req.Template } update.ShowInHome = &req.ShowInHome if req.BasicInfo != nil { update.BasicInfo = req.BasicInfo } if len(req.Experience) > 0 { update.Experience = &req.Experience } if len(req.Education) > 0 { update.Education = &req.Education } if len(req.Skills) > 0 { update.Skills = &req.Skills } if len(req.Projects) > 0 { update.Projects = &req.Projects } resume, err := h.resumeService.UpdateResume(id, update) if err != nil { Error(c, err.Error()) return } OK(c, resume, "更新成功") } // UpdateResumeShowInHomeAPI 更新简历首页展示状态API // @param c Gin 上下文 // @author sunct func (h *ResumeHandler) UpdateResumeShowInHomeAPI(c *gin.Context) { id := c.Param("id") if id == "" { BadRequest(c, "id is required") return } var req struct { ShowInHome bool `json:"show_in_home"` } if err := c.ShouldBindJSON(&req); err != nil { BadRequest(c, err.Error()) return } update := &model.ResumeUpdate{ShowInHome: &req.ShowInHome} _, err := h.resumeService.UpdateResume(id, update) if err != nil { Error(c, err.Error()) return } Success(c, "更新成功") } // UpdateResumePasswordAPI 更新简历密码API // @param c Gin 上下文 // @author sunct func (h *ResumeHandler) UpdateResumePasswordAPI(c *gin.Context) { id := c.Param("id") if id == "" { BadRequest(c, "id is required") return } var req struct { Password string `json:"password"` } if err := c.ShouldBindJSON(&req); err != nil { BadRequest(c, err.Error()) return } update := &model.ResumeUpdate{Password: &req.Password} _, err := h.resumeService.UpdateResume(id, update) if err != nil { Error(c, err.Error()) return } Success(c, "密码更新成功") } // GetAllResumesAPI 获取所有简历列表API // @param c Gin 上下文 // @author sunct func (h *ResumeHandler) GetAllResumesAPI(c *gin.Context) { resumes, err := h.resumeService.GetAllResumes() if err != nil { Error(c, err.Error()) return } OK(c, resumes) } // CreateResumeAPI 创建简历API,需要提供route参数作为访问路径 // @param c Gin 上下文 // @author sunct func (h *ResumeHandler) CreateResumeAPI(c *gin.Context) { var req struct { Route string `json:"route"` // 简历访问路由 ShowInHome bool `json:"show_in_home"` // 是否在首页展示,默认true } if err := c.ShouldBindJSON(&req); err != nil { BadRequest(c, err.Error()) return } if req.Route == "" { BadRequest(c, "route is required") return } resume := &model.Resume{ShowInHome: req.ShowInHome} resume, err := h.resumeService.CreateResumeWithRoute(req.Route, resume) if err != nil { Error(c, err.Error()) return } Created(c, resume) } // DeleteResumeAPI 删除简历API,通过路径参数id指定简历ID // @param c Gin 上下文 // @author sunct func (h *ResumeHandler) DeleteResumeAPI(c *gin.Context) { id := c.Param("id") err := h.resumeService.DeleteResume(id) if err != nil { Error(c, err.Error()) return } Success(c, "删除成功") } // UpdateResumeUserIDAPI 更新简历的用户绑定API // @param c Gin 上下文 // @author sunct func (h *ResumeHandler) UpdateResumeUserIDAPI(c *gin.Context) { id := c.Param("id") var req struct { UserID string `json:"user_id"` } if err := c.ShouldBindJSON(&req); err != nil { BadRequest(c, err.Error()) return } err := h.resumeService.UpdateResumeUserID(id, req.UserID) if err != nil { Error(c, err.Error()) return } Success(c, "更新成功") } // AdminCreateResumeAPI 后台创建简历API,通过user_id参数关联用户 // @param c Gin 上下文 // @author sunct func (h *ResumeHandler) AdminCreateResumeAPI(c *gin.Context) { var req struct { UserID string `json:"user_id"` Route string `json:"route"` ShowInHome bool `json:"show_in_home"` BasicInfo *model.BasicInfo `json:"basic_info"` } if err := c.ShouldBindJSON(&req); err != nil { BadRequest(c, err.Error()) return } if req.UserID == "" { BadRequest(c, "user_id is required") return } user, err := h.userService.GetUserByID(req.UserID) if err != nil { NotFound(c, "用户不存在") return } route := req.Route if route == "" { route = user.Username if route == "" { route = "resume-" + req.UserID[:8] } } basicInfo := req.BasicInfo if basicInfo == nil { basicInfo = &model.BasicInfo{} } if basicInfo.Name == "" { basicInfo.Name = user.Name } if basicInfo.Email == "" { basicInfo.Email = user.Email } if basicInfo.Phone == "" { basicInfo.Phone = user.Phone } resume := &model.Resume{ ShowInHome: req.ShowInHome, UserID: req.UserID, BasicInfo: *basicInfo, } resume, err = h.resumeService.CreateResumeWithRoute(route, resume) if err != nil { Error(c, err.Error()) return } Created(c, resume, "简历创建成功") } // RouteCheckAPI 检查路由是否已存在API,通过query参数route查询 // @param c Gin 上下文 // @author sunct func (h *ResumeHandler) RouteCheckAPI(c *gin.Context) { route := c.Query("route") exists := h.resumeService.RouteExists(route) OK(c, gin.H{"exists": exists}) } // UpdateResumeAPI 更新简历API,通过query参数route指定简历路由 // @param c Gin 上下文 // @author sunct func (h *ResumeHandler) UpdateResumeAPI(c *gin.Context) { var update model.ResumeUpdate if err := c.ShouldBindJSON(&update); err != nil { BadRequest(c, err.Error()) return } route := c.Query("route") if route == "" { route = "default" } resume, err := h.resumeService.GetResumeByRoute(route) if err != nil { Error(c, err.Error()) return } updated, err := h.resumeService.UpdateResume(resume.ID, &update) if err != nil { Error(c, err.Error()) return } OK(c, updated) } // ShowHome 渲染产品首页,展示所有简历列表和统计数据 // @param c Gin 上下文 // @author sunct func (h *ResumeHandler) ShowHome(c *gin.Context) { resumes, err := h.resumeService.GetAllResumes() if err != nil { resumes = []*model.Resume{} } config, _ := h.systemService.GetSystemConfig() if config == nil { config = &model.SystemConfig{ WebsiteDomain: "http://localhost:8090", WebsiteTitle: "Resume Platform", WebsiteDesc: "现代化的开源简历平台,支持多人简历管理、自定义模板、实时预览", WebsiteKeywords: "简历平台,开源,个人主页,作品集,简历管理", } } type HomeStats struct { TotalUsers int TotalResumes int TotalPortfolio int TotalQuiz int } stats := HomeStats{} users, _ := h.userService.GetAllUsers() stats.TotalUsers = len(users) stats.TotalResumes = len(resumes) c.HTML(http.StatusOK, "web/home", gin.H{ "Resumes": resumes, "SystemConfig": config, "Stats": stats, }) } // ShowPortfolio 渲染作品集页面,根据路由参数获取对应简历的项目列表 // @param c Gin 上下文 // @author sunct func (h *ResumeHandler) ShowPortfolio(c *gin.Context) { route := c.Param("route") resume, err := h.resumeService.GetResumeByRoute(route) if err != nil { c.Status(http.StatusNotFound) return } c.HTML(http.StatusOK, "web/portfolio", gin.H{ "Resume": resume, }) } // ShowContact 渲染联系方式页面,根据路由参数获取对应简历的联系信息 // @param c Gin 上下文 // @author sunct func (h *ResumeHandler) ShowContact(c *gin.Context) { route := c.Param("route") resume, err := h.resumeService.GetResumeByRoute(route) if err != nil { c.Status(http.StatusNotFound) return } c.HTML(http.StatusOK, "web/contact", resume) } // Sitemap 生成站点地图XML,用于搜索引擎SEO优化 // @param c Gin 上下文 // @author sunct func (h *ResumeHandler) Sitemap(c *gin.Context) { sitemap := ` https://zhangsan.dev/ 2026-07-13 weekly 1.0 https://zhangsan.dev/portfolio 2026-07-13 monthly 0.8 https://zhangsan.dev/contact 2026-07-13 yearly 0.5 ` c.Header("Content-Type", "application/xml") c.String(http.StatusOK, sitemap) } // GetPortfolioItemsAPI 获取所有作品集项目列表API // @param c Gin 上下文 // @author sunct func (h *ResumeHandler) GetPortfolioItemsAPI(c *gin.Context) { items, err := h.portfolioService.GetPortfolioItems() if err != nil { Error(c, err.Error()) return } OK(c, items) } // GetPortfolioItemAPI 获取单个作品集项目API,通过路径参数id指定项目ID // @param c Gin 上下文 // @author sunct func (h *ResumeHandler) GetPortfolioItemAPI(c *gin.Context) { id := c.Param("id") var itemID uint if _, err := fmt.Sscanf(id, "%d", &itemID); err != nil { BadRequest(c, "invalid id") return } item, err := h.portfolioService.GetPortfolioItem(itemID) if err != nil { NotFound(c, "portfolio item not found") return } OK(c, item) } // CreatePortfolioItemAPI 创建作品集项目API // @param c Gin 上下文 // @author sunct func (h *ResumeHandler) CreatePortfolioItemAPI(c *gin.Context) { var item model.PortfolioItem if err := c.ShouldBindJSON(&item); err != nil { BadRequest(c, err.Error()) return } err := h.portfolioService.CreatePortfolioItem(&item) if err != nil { Error(c, err.Error()) return } createNotification("portfolio", "新作品集项目", "作品集项目 "+item.Name+" 已添加", "/dashboard/portfolios") Created(c, item, "作品集项目创建成功") } // UpdatePortfolioItemAPI 更新作品集项目API,通过路径参数id指定项目ID // @param c Gin 上下文 // @author sunct func (h *ResumeHandler) UpdatePortfolioItemAPI(c *gin.Context) { id := c.Param("id") var itemID uint if _, err := fmt.Sscanf(id, "%d", &itemID); err != nil { BadRequest(c, "invalid id") return } existing, err := h.portfolioService.GetPortfolioItem(itemID) if err != nil { Error(c, err.Error()) return } if existing == nil { NotFound(c, "portfolio item not found") return } var updates model.PortfolioItem if err := c.ShouldBindJSON(&updates); err != nil { BadRequest(c, err.Error()) return } if updates.Name != "" { existing.Name = updates.Name } if updates.Description != "" { existing.Description = updates.Description } if len(updates.TechStack) > 0 { existing.TechStack = updates.TechStack } if updates.URL != "" { existing.URL = updates.URL } if updates.StartDate != "" { existing.StartDate = updates.StartDate } if updates.EndDate != "" { existing.EndDate = updates.EndDate } if updates.Doc != "" { existing.Doc = updates.Doc } if updates.ResumeID != "" { existing.ResumeID = updates.ResumeID } if updates.ProjectID != 0 { existing.ProjectID = updates.ProjectID } if updates.Password != "" { existing.Password = updates.Password } existing.ShowInResume = updates.ShowInResume existing.Hidden = updates.Hidden err = h.portfolioService.UpdatePortfolioItem(itemID, existing) if err != nil { Error(c, err.Error()) return } OK(c, existing, "作品集项目更新成功") } // DeletePortfolioItemAPI 删除作品集项目API,通过路径参数id指定项目ID // @param c Gin 上下文 // @author sunct func (h *ResumeHandler) DeletePortfolioItemAPI(c *gin.Context) { id := c.Param("id") var itemID uint if _, err := fmt.Sscanf(id, "%d", &itemID); err != nil { BadRequest(c, "invalid id") return } if err := h.portfolioService.DeletePortfolioItem(itemID); err != nil { Error(c, err.Error()) return } Success(c, "删除成功") } // CreateQuizRecordAPI 创建面试题答题记录API // @param c Gin 上下文 // @author sunct func (h *ResumeHandler) CreateQuizRecordAPI(c *gin.Context) { var record model.QuizRecord if err := c.ShouldBindJSON(&record); err != nil { BadRequest(c, err.Error()) return } log.Printf("[DEBUG] CreateQuizRecordAPI received data: id=%s, resume_id=%s, resume_route=%s, title=%s, score=%f, correct_count=%d, total_count=%d, questions_len=%d", record.ID, record.ResumeID, record.ResumeRoute, record.Title, record.Score, record.CorrectCount, record.TotalCount, len(record.Questions)) if record.ResumeRoute == "" && record.ResumeID != "" { resume, err := h.resumeService.GetResume(record.ResumeID) if err == nil && resume != nil { record.ResumeRoute = resume.Route } } if record.ResumeRoute == "" { record.ResumeRoute = "custom-quiz" } err := h.quizService.CreateQuizRecord(&record) if err != nil { Error(c, err.Error()) return } createNotification("quiz", "新答题记录", "新的面试答题记录已生成,得分 "+fmt.Sprintf("%.1f", record.Score), "/dashboard/quiz") log.Printf("[DEBUG] CreateQuizRecordAPI success, record id=%s", record.ID) Created(c, record) } // GetQuizRecordsAPI 获取面试题答题记录列表API,通过query参数route指定简历路由 // @param c Gin 上下文 // @author sunct func (h *ResumeHandler) GetQuizRecordsAPI(c *gin.Context) { route := c.Query("route") if route == "" { BadRequest(c, "route is required") return } records, err := h.quizService.GetQuizRecordsByResume(route) if err != nil { Error(c, err.Error()) return } OK(c, records) } // GetQuizRecordAPI 获取单条面试题答题记录API,通过路径参数id指定记录ID // @param c Gin 上下文 // @author sunct func (h *ResumeHandler) GetQuizRecordAPI(c *gin.Context) { id := c.Param("id") if id == "" { BadRequest(c, "id is required") return } record, err := h.quizService.GetQuizRecord(id) if err != nil { NotFound(c, "record not found") return } OK(c, record) } // DeleteQuizRecordAPI 删除面试题答题记录API,通过路径参数id指定记录ID // @param c Gin 上下文 // @author sunct func (h *ResumeHandler) DeleteQuizRecordAPI(c *gin.Context) { id := c.Param("id") if id == "" { BadRequest(c, "id is required") return } err := h.quizService.DeleteQuizRecord(id) if err != nil { Error(c, err.Error()) return } Success(c, "删除成功") } // CreateFavoriteQuestionAPI 创建收藏/错题记录API // @param c Gin 上下文 // @author sunct func (h *ResumeHandler) CreateFavoriteQuestionAPI(c *gin.Context) { var fav model.FavoriteQuestion if err := c.ShouldBindJSON(&fav); err != nil { BadRequest(c, err.Error()) return } // 当 resume_route 为空但 resume_id 存在时,从简历中获取路由 if fav.ResumeRoute == "" && fav.ResumeID != "" { resume, err := h.resumeService.GetResume(fav.ResumeID) if err == nil && resume != nil { fav.ResumeRoute = resume.Route } } // 仍然为空时使用默认值 custom-quiz,保证自由练习的错题/收藏也能保存 if fav.ResumeRoute == "" { fav.ResumeRoute = "custom-quiz" } err := h.quizService.CreateFavoriteQuestion(&fav) if err != nil { Error(c, err.Error()) return } Created(c, fav) } // GetFavoriteQuestionsAPI 获取收藏/错题记录列表API,支持按收藏状态和对错筛选 // @param c Gin 上下文 // @author sunct func (h *ResumeHandler) GetFavoriteQuestionsAPI(c *gin.Context) { route := c.Query("route") if route == "" { BadRequest(c, "route is required") return } var isFavorite, isCorrect *bool if favStr := c.Query("is_favorite"); favStr != "" { fav := favStr == "true" isFavorite = &fav } if correctStr := c.Query("is_correct"); correctStr != "" { correct := correctStr == "true" isCorrect = &correct } favorites, err := h.quizService.GetFavoriteQuestions(route, isFavorite, isCorrect) if err != nil { Error(c, err.Error()) return } OK(c, favorites) } // DeleteFavoriteQuestionAPI 删除收藏/错题记录API // @param c Gin 上下文 // @author sunct func (h *ResumeHandler) DeleteFavoriteQuestionAPI(c *gin.Context) { id := c.Param("id") if id == "" { BadRequest(c, "id is required") return } err := h.quizService.DeleteFavoriteQuestion(id) if err != nil { Error(c, err.Error()) return } Success(c, "删除成功") } // ToggleFavoriteAPI 切换题目收藏状态API // @param c Gin 上下文 // @author sunct func (h *ResumeHandler) ToggleFavoriteAPI(c *gin.Context) { id := c.Param("id") if id == "" { BadRequest(c, "id is required") return } err := h.quizService.ToggleFavorite(id) if err != nil { Error(c, err.Error()) return } Success(c, "切换成功") } // CreateUserAPI 创建用户API // @param c Gin 上下文 // @author sunct func (h *ResumeHandler) CreateUserAPI(c *gin.Context) { var user model.User if err := c.ShouldBindJSON(&user); err != nil { BadRequest(c, "参数错误") return } if user.Username == "" { BadRequest(c, "用户名不能为空") return } err := h.userService.CreateUser(&user) if err != nil { if strings.Contains(err.Error(), "UNIQUE constraint") && strings.Contains(err.Error(), "username") { Conflict(c, "用户名已存在") } else { Error(c, err.Error()) } return } createNotification("user", "新用户添加", "用户 "+user.Name+"("+user.Username+") 已添加", "/dashboard/users") Success(c, "用户添加成功") } // GetAllUsersAPI 获取所有用户列表API // @param c Gin 上下文 // @author sunct func (h *ResumeHandler) GetAllUsersAPI(c *gin.Context) { users, err := h.userService.GetAllUsers() if err != nil { Error(c, err.Error()) return } OK(c, users) } // GetUserAPI 根据ID获取用户API // @param c Gin 上下文 // @author sunct func (h *ResumeHandler) GetUserAPI(c *gin.Context) { id := c.Param("id") if id == "" { BadRequest(c, "id is required") return } user, err := h.userService.GetUserByID(id) if err != nil { NotFound(c, "user not found") return } OK(c, user) } // UpdateUserAPI 更新用户信息API // @param c Gin 上下文 // @author sunct func (h *ResumeHandler) UpdateUserAPI(c *gin.Context) { id := c.Param("id") if id == "" { BadRequest(c, "id is required") return } var user model.User if err := c.ShouldBindJSON(&user); err != nil { BadRequest(c, err.Error()) return } err := h.userService.UpdateUser(id, &user) if err != nil { Error(c, err.Error()) return } Success(c, "更新成功") } // DeleteUserAPI 删除用户API // @param c Gin 上下文 // @author sunct func (h *ResumeHandler) DeleteUserAPI(c *gin.Context) { id := c.Param("id") if id == "" { BadRequest(c, "id is required") return } err := h.userService.DeleteUser(id) if err != nil { Error(c, err.Error()) return } Success(c, "删除成功") } // GetResumesByUserAPI 根据用户ID获取简历列表API // @param c Gin 上下文 // @author sunct func (h *ResumeHandler) GetResumesByUserAPI(c *gin.Context) { userID := c.Query("user_id") if userID == "" { BadRequest(c, "user_id is required") return } resumes, err := h.resumeService.GetResumesByUserID(userID) if err != nil { Error(c, err.Error()) return } OK(c, resumes) } // GetPortfolioItemsByUserAPI 根据用户ID获取作品集列表API // @param c Gin 上下文 // @author sunct func (h *ResumeHandler) GetPortfolioItemsByUserAPI(c *gin.Context) { userID := c.Query("user_id") if userID == "" { BadRequest(c, "user_id is required") return } items, err := h.portfolioService.GetPortfolioItemsByUserID(userID) if err != nil { Error(c, err.Error()) return } OK(c, items) } // GetAllMenusAPI 获取所有菜单列表API // @param c Gin 上下文 // @author sunct func (h *ResumeHandler) GetAllMenusAPI(c *gin.Context) { menus, err := h.systemService.GetAllMenus() if err != nil { Error(c, err.Error()) return } OK(c, menus) } // CreateMenuAPI 创建菜单API // @param c Gin 上下文 // @author sunct func (h *ResumeHandler) CreateMenuAPI(c *gin.Context) { var menu model.Menu if err := c.ShouldBindJSON(&menu); err != nil { BadRequest(c, err.Error()) return } if menu.Name == "" { BadRequest(c, "name is required") return } err := h.systemService.CreateMenu(&menu) if err != nil { Error(c, err.Error()) return } Created(c, menu) } // UpdateMenuAPI 更新菜单API // @param c Gin 上下文 // @author sunct func (h *ResumeHandler) UpdateMenuAPI(c *gin.Context) { id := c.Param("id") if id == "" { BadRequest(c, "id is required") return } var menu model.Menu if err := c.ShouldBindJSON(&menu); err != nil { BadRequest(c, err.Error()) return } err := h.systemService.UpdateMenu(id, &menu) if err != nil { Error(c, err.Error()) return } Success(c, "更新成功") } // DeleteMenuAPI 删除菜单API // @param c Gin 上下文 // @author sunct func (h *ResumeHandler) DeleteMenuAPI(c *gin.Context) { id := c.Param("id") if id == "" { BadRequest(c, "id is required") return } err := h.systemService.DeleteMenu(id) if err != nil { Error(c, err.Error()) return } Success(c, "删除成功") } // GetSystemConfigAPI 获取系统配置API // @param c Gin 上下文 // @author sunct func (h *ResumeHandler) GetSystemConfigAPI(c *gin.Context) { config, err := h.systemService.GetSystemConfig() if err != nil { if os.IsNotExist(err) { c.JSON(http.StatusOK, &model.SystemConfig{ ID: "default", WebsiteDomain: "http://localhost:8090", WebsiteTitle: "Resume Platform", }) return } c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } c.JSON(http.StatusOK, config) } // UpdateSystemConfigAPI 更新系统配置API // @param c Gin 上下文 // @author sunct func (h *ResumeHandler) UpdateSystemConfigAPI(c *gin.Context) { var config model.SystemConfig if err := c.ShouldBindJSON(&config); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } if config.ID == "" { config.ID = "default" } existing, _ := h.systemService.GetSystemConfig() if existing == nil { err := h.systemService.CreateSystemConfig(&config) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } } else { err := h.systemService.UpdateSystemConfig(config.ID, &config) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } } c.JSON(http.StatusOK, gin.H{"message": "updated"}) } // GenerateResumeFromDocumentAPI 从文档生成简历API,支持上传word、pdf、excel、markdown、txt等格式 // @param c Gin 上下文 // @author sunct func (h *ResumeHandler) GenerateResumeFromDocumentAPI(c *gin.Context) { userID := c.PostForm("user_id") if userID == "" { userID = c.GetHeader("X-User-ID") if userID == "" { userID = "1" } } file, err := c.FormFile("file") if err != nil { BadRequest(c, "请选择要上传的文件") return } fileExt := strings.ToLower(filepath.Ext(file.Filename)) allowedExts := map[string]bool{ ".pdf": true, ".docx": true, ".xlsx": true, ".md": true, ".markdown": true, ".txt": true, } if !allowedExts[fileExt] { BadRequest(c, "不支持的文件格式,支持:pdf, docx, xlsx, md, txt") return } f, err := file.Open() if err != nil { Error(c, "文件打开失败") return } defer f.Close() content, err := io.ReadAll(f) if err != nil { Error(c, "文件读取失败") return } resume, err := h.documentService.ParseAndGenerateResume(c.Request.Context(), userID, file.Filename, content) if err != nil { Error(c, "文档解析或简历生成失败: "+err.Error()) return } resume, err = h.resumeService.CreateResumeWithRoute(resume.Route, resume) if err != nil { Error(c, "简历保存失败: "+err.Error()) return } Created(c, resume, "简历生成成功") }