首次提交:初始化项目代码
This commit is contained in:
@@ -0,0 +1,257 @@
|
||||
package resume
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"resume-platform/internal/ai"
|
||||
"resume-platform/internal/model"
|
||||
"resume-platform/internal/prompt"
|
||||
"resume-platform/pkg/logger"
|
||||
"resume-platform/pkg/utils"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type ResumeGeneratorService struct {
|
||||
aiProvider ai.Provider
|
||||
}
|
||||
|
||||
func NewResumeGeneratorService(provider ai.Provider) *ResumeGeneratorService {
|
||||
return &ResumeGeneratorService{aiProvider: provider}
|
||||
}
|
||||
|
||||
func (s *ResumeGeneratorService) GenerateFromDocument(ctx context.Context, documentContent string) (*model.Resume, error) {
|
||||
if s.aiProvider == nil {
|
||||
return nil, fmt.Errorf("AI provider not configured")
|
||||
}
|
||||
|
||||
cleanedContent := cleanDocumentContent(documentContent)
|
||||
if cleanedContent == "" {
|
||||
return nil, fmt.Errorf("document content is empty after cleaning")
|
||||
}
|
||||
|
||||
maxContentLength := 15000
|
||||
if len(cleanedContent) > maxContentLength {
|
||||
cleanedContent = truncateContent(cleanedContent, maxContentLength)
|
||||
}
|
||||
|
||||
return s.extractAllSections(ctx, cleanedContent)
|
||||
}
|
||||
|
||||
func (s *ResumeGeneratorService) extractAllSections(ctx context.Context, content string) (*model.Resume, error) {
|
||||
promptText := prompt.BuildResumeExtractorPrompt(content)
|
||||
|
||||
result, err := s.aiProvider.Generate(ctx, promptText)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
cleanedResult := utils.CleanAIResponse(result)
|
||||
|
||||
var rawData struct {
|
||||
BasicInfo struct {
|
||||
Name string `json:"name"`
|
||||
Title string `json:"title"`
|
||||
Email string `json:"email"`
|
||||
Phone string `json:"phone"`
|
||||
Location string `json:"location"`
|
||||
Website string `json:"website"`
|
||||
Summary string `json:"summary"`
|
||||
ExperienceYears interface{} `json:"experience_years"`
|
||||
JobTarget string `json:"job_target"`
|
||||
Industry string `json:"industry"`
|
||||
CoreAdvantages []interface{} `json:"core_advantages"`
|
||||
} `json:"basic_info"`
|
||||
Experience []struct {
|
||||
Company string `json:"company"`
|
||||
Position string `json:"position"`
|
||||
StartDate string `json:"start_date"`
|
||||
EndDate string `json:"end_date"`
|
||||
Description string `json:"description"`
|
||||
Highlights []interface{} `json:"highlights"`
|
||||
Platforms []interface{} `json:"platforms"`
|
||||
} `json:"experience"`
|
||||
Education []struct {
|
||||
School string `json:"school"`
|
||||
Degree string `json:"degree"`
|
||||
Major string `json:"major"`
|
||||
StartDate string `json:"start_date"`
|
||||
EndDate string `json:"end_date"`
|
||||
GPA string `json:"gpa"`
|
||||
} `json:"education"`
|
||||
Skills []struct {
|
||||
Name string `json:"name"`
|
||||
Level string `json:"level"`
|
||||
Category string `json:"category"`
|
||||
} `json:"skills"`
|
||||
Projects []struct {
|
||||
Name string `json:"name"`
|
||||
Description string `json:"description"`
|
||||
TechStack []interface{} `json:"tech_stack"`
|
||||
URL string `json:"url"`
|
||||
Highlights []interface{} `json:"highlights"`
|
||||
Achievements []interface{} `json:"achievements"`
|
||||
StartDate string `json:"start_date"`
|
||||
EndDate string `json:"end_date"`
|
||||
} `json:"projects"`
|
||||
}
|
||||
|
||||
if err := json.Unmarshal([]byte(cleanedResult), &rawData); err != nil {
|
||||
logger.Errorf("Failed to parse AI result: %v, raw result: %s", err, cleanedResult)
|
||||
return nil, fmt.Errorf("failed to parse AI result: %w", err)
|
||||
}
|
||||
|
||||
coreAdvantages := make([]string, 0, len(rawData.BasicInfo.CoreAdvantages))
|
||||
for _, item := range rawData.BasicInfo.CoreAdvantages {
|
||||
coreAdvantages = append(coreAdvantages, utils.ConvertToString(item))
|
||||
}
|
||||
|
||||
experience := make([]model.Experience, 0, len(rawData.Experience))
|
||||
for _, exp := range rawData.Experience {
|
||||
if exp.Company == "" && exp.Position == "" {
|
||||
continue
|
||||
}
|
||||
highlights := make([]string, 0, len(exp.Highlights))
|
||||
for _, item := range exp.Highlights {
|
||||
highlights = append(highlights, utils.ConvertToString(item))
|
||||
}
|
||||
platforms := make([]string, 0, len(exp.Platforms))
|
||||
for _, item := range exp.Platforms {
|
||||
platforms = append(platforms, utils.ConvertToString(item))
|
||||
}
|
||||
experience = append(experience, model.Experience{
|
||||
Company: exp.Company,
|
||||
Position: exp.Position,
|
||||
StartDate: exp.StartDate,
|
||||
EndDate: exp.EndDate,
|
||||
Description: exp.Description,
|
||||
Highlights: highlights,
|
||||
Platforms: platforms,
|
||||
})
|
||||
}
|
||||
|
||||
education := make([]model.Education, 0, len(rawData.Education))
|
||||
for _, edu := range rawData.Education {
|
||||
if edu.School == "" {
|
||||
continue
|
||||
}
|
||||
education = append(education, model.Education{
|
||||
School: edu.School,
|
||||
Degree: edu.Degree,
|
||||
Major: edu.Major,
|
||||
StartDate: edu.StartDate,
|
||||
EndDate: edu.EndDate,
|
||||
GPA: edu.GPA,
|
||||
})
|
||||
}
|
||||
|
||||
skills := make([]model.Skill, 0, len(rawData.Skills))
|
||||
for _, skill := range rawData.Skills {
|
||||
if skill.Name == "" {
|
||||
continue
|
||||
}
|
||||
skills = append(skills, model.Skill{
|
||||
Name: skill.Name,
|
||||
Level: skill.Level,
|
||||
Category: skill.Category,
|
||||
})
|
||||
}
|
||||
|
||||
projects := make([]model.Project, 0, len(rawData.Projects))
|
||||
for _, proj := range rawData.Projects {
|
||||
if proj.Name == "" && proj.Description == "" {
|
||||
continue
|
||||
}
|
||||
techStack := make([]string, 0, len(proj.TechStack))
|
||||
for _, item := range proj.TechStack {
|
||||
techStack = append(techStack, utils.ConvertToString(item))
|
||||
}
|
||||
highlights := make([]string, 0, len(proj.Highlights))
|
||||
for _, item := range proj.Highlights {
|
||||
highlights = append(highlights, utils.ConvertToString(item))
|
||||
}
|
||||
achievements := make([]string, 0, len(proj.Achievements))
|
||||
for _, item := range proj.Achievements {
|
||||
achievements = append(achievements, utils.ConvertToString(item))
|
||||
}
|
||||
projects = append(projects, model.Project{
|
||||
Name: proj.Name,
|
||||
Description: proj.Description,
|
||||
TechStack: techStack,
|
||||
URL: proj.URL,
|
||||
Highlights: highlights,
|
||||
Achievements: achievements,
|
||||
StartDate: proj.StartDate,
|
||||
EndDate: proj.EndDate,
|
||||
ShowInResume: false,
|
||||
})
|
||||
}
|
||||
|
||||
resume := &model.Resume{
|
||||
ID: uuid.New().String(),
|
||||
Route: generateRouteFromName(rawData.BasicInfo.Name),
|
||||
BasicInfo: model.BasicInfo{
|
||||
Name: rawData.BasicInfo.Name,
|
||||
Title: rawData.BasicInfo.Title,
|
||||
Email: rawData.BasicInfo.Email,
|
||||
Phone: rawData.BasicInfo.Phone,
|
||||
Location: rawData.BasicInfo.Location,
|
||||
Website: rawData.BasicInfo.Website,
|
||||
Summary: rawData.BasicInfo.Summary,
|
||||
ExperienceYears: utils.ConvertToString(rawData.BasicInfo.ExperienceYears),
|
||||
JobTarget: rawData.BasicInfo.JobTarget,
|
||||
Industry: rawData.BasicInfo.Industry,
|
||||
CoreAdvantages: coreAdvantages,
|
||||
},
|
||||
Experience: experience,
|
||||
Education: education,
|
||||
Skills: skills,
|
||||
Projects: projects,
|
||||
Template: "modern",
|
||||
ShowInHome: false,
|
||||
}
|
||||
|
||||
return resume, nil
|
||||
}
|
||||
|
||||
func cleanDocumentContent(content string) string {
|
||||
return utils.CleanText(content)
|
||||
}
|
||||
|
||||
func generateRouteFromName(name string) string {
|
||||
if name == "" {
|
||||
return uuid.New().String()[:8]
|
||||
}
|
||||
route := ""
|
||||
for _, c := range name {
|
||||
if (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || (c >= '0' && c <= '9') {
|
||||
route += string(c)
|
||||
}
|
||||
}
|
||||
if route == "" {
|
||||
return uuid.New().String()[:8]
|
||||
}
|
||||
return strings.ToLower(route)
|
||||
}
|
||||
|
||||
func truncateContent(content string, maxLength int) string {
|
||||
if len(content) <= maxLength {
|
||||
return content
|
||||
}
|
||||
|
||||
truncated := content[:maxLength]
|
||||
|
||||
for i := len(truncated) - 1; i >= 0; i-- {
|
||||
c := rune(truncated[i])
|
||||
if c == '。' || c == '!' || c == '?' || c == ';' ||
|
||||
c == '.' || c == '!' || c == '?' || c == ';' ||
|
||||
c == '\n' || c == '\r' {
|
||||
truncated = truncated[:i+1]
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
return truncated + "\n\n[内容已截断,仅保留关键信息]"
|
||||
}
|
||||
@@ -0,0 +1,222 @@
|
||||
package resume
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"regexp"
|
||||
"resume-platform/internal/model"
|
||||
"resume-platform/internal/repository"
|
||||
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
type ResumeService interface {
|
||||
GetResume(id string) (*model.Resume, error)
|
||||
GetResumeByRoute(route string) (*model.Resume, error)
|
||||
RouteExists(route string) bool
|
||||
GetAllResumes() ([]*model.Resume, error)
|
||||
GetResumesByUserID(userID string) ([]*model.Resume, error)
|
||||
GetResumesWithPagination(page, pageSize int) ([]*model.Resume, int, error)
|
||||
GetResumeWithMask(id string) (*model.Resume, error)
|
||||
CreateResume(resume *model.Resume) (*model.Resume, error)
|
||||
CreateResumeWithRoute(route string, resume *model.Resume) (*model.Resume, error)
|
||||
UpdateResume(id string, update *model.ResumeUpdate) (*model.Resume, error)
|
||||
DeleteResume(id string) error
|
||||
UpdateResumeUserID(resumeID, userID string) error
|
||||
InitDefaultResume() (*model.Resume, error)
|
||||
}
|
||||
|
||||
type resumeService struct {
|
||||
repo repository.ResumeRepository
|
||||
}
|
||||
|
||||
func NewResumeService(repo repository.ResumeRepository) ResumeService {
|
||||
return &resumeService{repo: repo}
|
||||
}
|
||||
|
||||
func (s *resumeService) GetResume(id string) (*model.Resume, error) {
|
||||
return s.repo.GetResume(id)
|
||||
}
|
||||
|
||||
func (s *resumeService) GetResumeByRoute(route string) (*model.Resume, error) {
|
||||
return s.repo.GetResumeByRoute(route)
|
||||
}
|
||||
|
||||
func (s *resumeService) RouteExists(route string) bool {
|
||||
return s.repo.RouteExists(route)
|
||||
}
|
||||
|
||||
func (s *resumeService) GetAllResumes() ([]*model.Resume, error) {
|
||||
return s.repo.GetAllResumes()
|
||||
}
|
||||
|
||||
func (s *resumeService) GetResumesWithPagination(page, pageSize int) ([]*model.Resume, int, error) {
|
||||
return s.repo.GetResumesWithPagination(page, pageSize)
|
||||
}
|
||||
|
||||
func (s *resumeService) CreateResume(resume *model.Resume) (*model.Resume, error) {
|
||||
if resume.ID == "" {
|
||||
resume.ID = uuid.New().String()
|
||||
}
|
||||
if resume.Template == "" {
|
||||
resume.Template = "modern"
|
||||
}
|
||||
err := s.repo.CreateResume(resume)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return s.repo.GetResume(resume.ID)
|
||||
}
|
||||
|
||||
func (s *resumeService) CreateResumeWithRoute(route string, resume *model.Resume) (*model.Resume, error) {
|
||||
if s.repo.RouteExists(route) {
|
||||
return nil, errors.New("route already exists")
|
||||
}
|
||||
|
||||
if resume.ID == "" {
|
||||
resume.ID = uuid.New().String()
|
||||
}
|
||||
if resume.Template == "" {
|
||||
resume.Template = "modern"
|
||||
}
|
||||
resume.Route = route
|
||||
|
||||
err := s.repo.CreateResume(resume)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return s.repo.GetResume(resume.ID)
|
||||
}
|
||||
|
||||
func (s *resumeService) GetResumesByUserID(userID string) ([]*model.Resume, error) {
|
||||
return s.repo.GetResumesByUserID(userID)
|
||||
}
|
||||
|
||||
func (s *resumeService) UpdateResume(id string, update *model.ResumeUpdate) (*model.Resume, error) {
|
||||
if !s.repo.Exists(id) {
|
||||
return nil, errors.New("resume not found")
|
||||
}
|
||||
err := s.repo.UpdateResume(id, update)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return s.repo.GetResume(id)
|
||||
}
|
||||
|
||||
func (s *resumeService) DeleteResume(id string) error {
|
||||
if !s.repo.Exists(id) {
|
||||
return errors.New("resume not found")
|
||||
}
|
||||
return s.repo.DeleteResume(id)
|
||||
}
|
||||
|
||||
func (s *resumeService) UpdateResumeUserID(resumeID, userID string) error {
|
||||
if !s.repo.Exists(resumeID) {
|
||||
return errors.New("resume not found")
|
||||
}
|
||||
return s.repo.UpdateResumeUserID(resumeID, userID)
|
||||
}
|
||||
|
||||
func (s *resumeService) InitDefaultResume() (*model.Resume, error) {
|
||||
defaultID := "default"
|
||||
if s.repo.Exists(defaultID) {
|
||||
return s.repo.GetResume(defaultID)
|
||||
}
|
||||
|
||||
defaultResume := &model.Resume{
|
||||
ID: defaultID,
|
||||
Route: defaultID,
|
||||
Template: "modern",
|
||||
BasicInfo: model.BasicInfo{
|
||||
Name: "张三",
|
||||
Title: "全栈开发工程师",
|
||||
Email: "zhangsan@example.com",
|
||||
Phone: "13800138000",
|
||||
Location: "北京市",
|
||||
Website: "https://zhangsan.dev",
|
||||
Summary: "5年以上软件开发经验,专注于Web全栈开发,熟悉Go、React、Vue等技术栈。热爱技术,善于解决复杂问题。",
|
||||
Avatar: "",
|
||||
},
|
||||
Experience: []model.Experience{
|
||||
{
|
||||
Company: "某知名互联网公司",
|
||||
Position: "高级开发工程师",
|
||||
StartDate: "2022-01",
|
||||
EndDate: "至今",
|
||||
Description: "负责核心业务系统的架构设计与开发",
|
||||
Highlights: []string{"主导微服务架构改造", "优化系统性能提升50%", "带领5人团队完成多个重要项目"},
|
||||
},
|
||||
{
|
||||
Company: "创业公司",
|
||||
Position: "技术负责人",
|
||||
StartDate: "2019-06",
|
||||
EndDate: "2021-12",
|
||||
Description: "从0到1搭建技术团队和产品",
|
||||
Highlights: []string{"建立完整技术体系", "打造百万级用户产品", "获得天使轮融资"},
|
||||
},
|
||||
},
|
||||
Education: []model.Education{
|
||||
{
|
||||
School: "清华大学",
|
||||
Degree: "本科",
|
||||
Major: "计算机科学与技术",
|
||||
StartDate: "2015-09",
|
||||
EndDate: "2019-06",
|
||||
GPA: "3.8/4.0",
|
||||
},
|
||||
},
|
||||
Skills: []model.Skill{
|
||||
{Name: "Go", Level: "expert", Category: "后端"},
|
||||
{Name: "Python", Level: "advanced", Category: "后端"},
|
||||
{Name: "React", Level: "advanced", Category: "前端"},
|
||||
{Name: "Vue", Level: "advanced", Category: "前端"},
|
||||
{Name: "MySQL", Level: "advanced", Category: "数据库"},
|
||||
{Name: "Redis", Level: "intermediate", Category: "数据库"},
|
||||
{Name: "Docker", Level: "advanced", Category: "运维"},
|
||||
{Name: "Kubernetes", Level: "intermediate", Category: "运维"},
|
||||
},
|
||||
Projects: []model.Project{
|
||||
{
|
||||
Name: "电商平台",
|
||||
Description: "基于微服务架构的B2C电商平台",
|
||||
TechStack: []string{"Go", "React", "MySQL", "Redis", "Kubernetes"},
|
||||
URL: "https://github.com/example/ecommerce",
|
||||
Highlights: []string{"日订单量突破10万", "系统可用性99.9%", "支持百万级并发"},
|
||||
},
|
||||
{
|
||||
Name: "任务管理系统",
|
||||
Description: "团队协作任务管理工具",
|
||||
TechStack: []string{"Vue", "Node.js", "MongoDB"},
|
||||
URL: "https://github.com/example/taskmanager",
|
||||
Highlights: []string{"500+企业客户", "月活跃用户10万"},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
err := s.repo.CreateResume(defaultResume)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return defaultResume, nil
|
||||
}
|
||||
|
||||
func maskPhone(phone string) string {
|
||||
re := regexp.MustCompile(`(\d{3})\d{4}(\d{4})`)
|
||||
return re.ReplaceAllString(phone, "$1****$2")
|
||||
}
|
||||
|
||||
func maskEmail(email string) string {
|
||||
re := regexp.MustCompile(`(\w{1,3})[\w.]*@`)
|
||||
return re.ReplaceAllString(email, "$1***@")
|
||||
}
|
||||
|
||||
func (s *resumeService) GetResumeWithMask(id string) (*model.Resume, error) {
|
||||
resume, err := s.repo.GetResume(id)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
resume.BasicInfo.Phone = maskPhone(resume.BasicInfo.Phone)
|
||||
resume.BasicInfo.Email = maskEmail(resume.BasicInfo.Email)
|
||||
|
||||
return resume, nil
|
||||
}
|
||||
Reference in New Issue
Block a user