Files

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, "简历生成成功")
}