1196 lines
29 KiB
Go
1196 lines
29 KiB
Go
// 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 := `<?xml version="1.0" encoding="UTF-8"?>
|
|
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
|
|
<url>
|
|
<loc>https://zhangsan.dev/</loc>
|
|
<lastmod>2026-07-13</lastmod>
|
|
<changefreq>weekly</changefreq>
|
|
<priority>1.0</priority>
|
|
</url>
|
|
<url>
|
|
<loc>https://zhangsan.dev/portfolio</loc>
|
|
<lastmod>2026-07-13</lastmod>
|
|
<changefreq>monthly</changefreq>
|
|
<priority>0.8</priority>
|
|
</url>
|
|
<url>
|
|
<loc>https://zhangsan.dev/contact</loc>
|
|
<lastmod>2026-07-13</lastmod>
|
|
<changefreq>yearly</changefreq>
|
|
<priority>0.5</priority>
|
|
</url>
|
|
</urlset>`
|
|
|
|
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, "简历生成成功")
|
|
}
|