首次提交:初始化项目代码

This commit is contained in:
sunct
2026-07-31 14:44:48 +08:00
commit a4fe393571
119 changed files with 29105 additions and 0 deletions
+257
View File
@@ -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[内容已截断,仅保留关键信息]"
}
+222
View File
@@ -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
}