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

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
+51
View File
@@ -0,0 +1,51 @@
package ai
import (
"context"
"resume-platform/internal/ai"
"resume-platform/internal/prompt"
)
type AIService struct {
provider ai.Provider
}
func NewAIService(provider ai.Provider) *AIService {
return &AIService{provider: provider}
}
func (s *AIService) PolishSummary(ctx context.Context, original string) (string, error) {
promptText := prompt.BuildPolishSummaryPrompt(original)
return s.provider.Generate(ctx, promptText)
}
func (s *AIService) PolishExperience(ctx context.Context, original string, company string, position string) (string, error) {
promptText := prompt.BuildPolishExperiencePrompt(original, company, position)
return s.provider.Generate(ctx, promptText)
}
func (s *AIService) PolishProject(ctx context.Context, original string, projectName string) (string, error) {
promptText := prompt.BuildPolishProjectPrompt(original, projectName)
return s.provider.Generate(ctx, promptText)
}
func (s *AIService) PolishDescription(ctx context.Context, original string, fieldType string) (string, error) {
promptText := prompt.BuildPolishDescriptionPrompt(original, fieldType)
return s.provider.Generate(ctx, promptText)
}
func (s *AIService) GetProviderName() string {
return s.provider.GetName()
}
func (s *AIService) Generate(ctx context.Context, prompt string) (string, error) {
return s.provider.Generate(ctx, prompt)
}
func (s *AIService) Embedding(ctx context.Context, text string) ([]float64, error) {
return nil, nil
}
+295
View File
@@ -0,0 +1,295 @@
package ai
import (
"context"
"encoding/json"
"fmt"
"resume-platform/internal/ai"
"resume-platform/internal/prompt"
"resume-platform/pkg/logger"
"strings"
"sync"
)
type QuestionGeneratorService struct {
provider ai.Provider
}
func NewQuestionGeneratorService(provider ai.Provider) *QuestionGeneratorService {
return &QuestionGeneratorService{provider: provider}
}
func (s *QuestionGeneratorService) GenerateQuestions(ctx context.Context, resumeText, keywordsStr, typesStr string) ([]map[string]interface{}, error) {
if resumeText == "" && keywordsStr == "" {
return nil, fmt.Errorf("resume_text or keywords is required")
}
if resumeText == "" {
resumeText = "基于关键词:" + keywordsStr
}
techKeywords := extractTechKeywords(resumeText)
if len(techKeywords) == 0 {
techKeywords = []string{"HTML", "CSS", "JavaScript", "Go", "MySQL", "Redis"}
}
if keywordsStr != "" {
techKeywords = strings.Split(keywordsStr, ",")
}
selectedTypes := parseTypes(typesStr)
logger.CtxInfof(ctx, "[QuestionGenerator] Generating questions for keywords: %v, types: %v", techKeywords, selectedTypes)
var wg sync.WaitGroup
resultChan := make(chan []map[string]interface{}, len(selectedTypes))
errChan := make(chan error, len(selectedTypes))
for _, qType := range selectedTypes {
wg.Add(1)
go func(t string) {
defer wg.Done()
prompt := buildQuestionGenerationPromptForType(resumeText, techKeywords, t)
logger.CtxInfof(ctx, "[QuestionGenerator] Generating %s questions...", t)
result, err := s.provider.Generate(ctx, prompt)
if err != nil {
logger.CtxErrorf(ctx, "[QuestionGenerator] AI generate failed for %s: %v", t, err)
errChan <- err
return
}
questions, err := parseAIQuestionsWithCtx(ctx, result)
if err != nil {
logger.CtxErrorf(ctx, "[QuestionGenerator] Parse failed for %s: %v", t, err)
errChan <- err
return
}
if len(questions) > 0 {
resultChan <- questions
logger.CtxInfof(ctx, "[QuestionGenerator] Generated %d %s questions", len(questions), t)
}
}(qType)
}
go func() {
wg.Wait()
close(resultChan)
close(errChan)
}()
var allQuestions []map[string]interface{}
for questions := range resultChan {
allQuestions = append(allQuestions, questions...)
}
logger.CtxInfof(ctx, "[QuestionGenerator] Total generated %d questions", len(allQuestions))
if len(allQuestions) == 0 {
return nil, fmt.Errorf("未能生成任何题目")
}
return allQuestions, nil
}
func buildQuestionGenerationPrompt(resumeText string, keywords []string, types []string) string {
return prompt.BuildQuestionGenerationPrompt(resumeText, keywords, types)
}
func buildQuestionGenerationPromptForType(resumeText string, keywords []string, qType string) string {
return prompt.BuildQuestionGenerationPromptForType(resumeText, keywords, qType)
}
func parseAIQuestionsWithCtx(ctx context.Context, result string) ([]map[string]interface{}, error) {
result = strings.TrimSpace(result)
logger.CtxInfof(ctx, "[QuestionGenerator] AI response length: %d", len(result))
logger.CtxInfof(ctx, "[QuestionGenerator] AI response raw result (first 2000 chars): %s", substr(result, 0, 2000))
result = strings.ReplaceAll(result, "```json", "")
result = strings.ReplaceAll(result, "```", "")
result = strings.TrimSpace(result)
startIdx := strings.Index(result, "[")
endIdx := strings.LastIndex(result, "]")
if startIdx == -1 || endIdx == -1 || endIdx <= startIdx {
logger.CtxErrorf(ctx, "[QuestionGenerator] Cannot find JSON array boundaries")
return nil, fmt.Errorf("无法找到JSON数组边界")
}
jsonStr := result[startIdx : endIdx+1]
logger.CtxInfof(ctx, "[QuestionGenerator] Extracted JSON array (length=%d)", len(jsonStr))
var questions []map[string]interface{}
if err := json.Unmarshal([]byte(jsonStr), &questions); err != nil {
logger.CtxErrorf(ctx, "[QuestionGenerator] Failed to unmarshal as array: %v", err)
logger.CtxErrorf(ctx, "[QuestionGenerator] Raw result that failed (first 500 chars): %s", substr(jsonStr, 0, 500))
return nil, fmt.Errorf("AI返回的JSON格式不正确: %w", err)
}
var validQuestions []map[string]interface{}
for _, q := range questions {
if !isValidQuestion(q) {
logger.CtxWarnf(ctx, "[QuestionGenerator] Skipping invalid question: %v", q)
continue
}
q = normalizeQuestion(q)
validQuestions = append(validQuestions, q)
}
logger.CtxInfof(ctx, "[QuestionGenerator] Successfully parsed %d valid questions (filtered %d invalid)", len(validQuestions), len(questions)-len(validQuestions))
if len(validQuestions) == 0 {
return nil, fmt.Errorf("未解析到有效题目")
}
return validQuestions, nil
}
func isValidQuestion(q map[string]interface{}) bool {
if q == nil {
return false
}
qType := fmt.Sprintf("%v", q["type"])
text := fmt.Sprintf("%v", q["text"])
answer := fmt.Sprintf("%v", q["answer"])
if qType == "" || text == "" || answer == "" {
return false
}
if qType == "mcq" {
if options, ok := q["options"].([]interface{}); ok {
if len(options) != 4 {
return false
}
for _, opt := range options {
if fmt.Sprintf("%v", opt) == "" {
return false
}
}
} else {
return false
}
answer = strings.ToUpper(strings.TrimSpace(answer))
if len(answer) != 1 || (answer < "A" || answer > "D") {
return false
}
}
if qType == "fill" {
if !strings.Contains(text, "___") {
return false
}
}
if qType == "sa" || qType == "algo" {
if len(answer) < 10 {
return false
}
}
analysis := fmt.Sprintf("%v", q["analysis"])
if len(analysis) < 30 {
return false
}
return true
}
func normalizeQuestion(q map[string]interface{}) map[string]interface{} {
qType := fmt.Sprintf("%v", q["type"])
if qType == "mcq" {
answer := strings.TrimSpace(fmt.Sprintf("%v", q["answer"]))
if len(answer) > 0 {
q["answer"] = strings.ToUpper(answer[:1])
}
if options, ok := q["options"].([]interface{}); ok {
var optionStrings []string
for _, opt := range options {
optStr := strings.TrimSpace(fmt.Sprintf("%v", opt))
optionStrings = append(optionStrings, optStr)
}
q["options"] = optionStrings
}
}
if _, ok := q["score"]; !ok {
diff := fmt.Sprintf("%v", q["difficulty"])
q["score"] = getScoreByDifficulty(diff)
}
if _, ok := q["category"]; !ok {
q["category"] = "综合"
}
if _, ok := q["keywords"]; !ok {
q["keywords"] = []string{}
}
if _, ok := q["options"]; !ok {
q["options"] = []string{}
}
if _, ok := q["analysis"]; !ok {
q["analysis"] = ""
}
if _, ok := q["difficulty"]; !ok {
q["difficulty"] = "初级"
}
return q
}
func isSingleLetter(s string) bool {
return len(s) == 1 && (s >= "A" && s <= "Z" || s >= "a" && s <= "z")
}
func substr(s string, start, length int) string {
if len(s) <= start {
return ""
}
end := start + length
if end > len(s) {
end = len(s)
}
return s[start:end]
}
func extractTechKeywords(text string) []string {
techPatterns := []string{"Go", "Golang", "Java", "Python", "JavaScript", "TypeScript", "React", "Vue", "MySQL", "PostgreSQL", "Redis", "MongoDB", "Kafka", "Docker", "Kubernetes", "微服务", "分布式"}
var result []string
for _, pattern := range techPatterns {
if strings.Contains(text, pattern) {
result = append(result, pattern)
}
}
return result
}
func parseTypes(typesStr string) []string {
if typesStr == "" {
return []string{"mcq", "fill", "sa", "algo"}
}
return strings.Split(typesStr, ",")
}
func getScoreByDifficulty(difficulty string) int {
switch difficulty {
case "入门":
return 5
case "初级":
return 10
case "中级":
return 15
case "进阶":
return 20
case "高级":
return 25
default:
return 10
}
}
+77
View File
@@ -0,0 +1,77 @@
package chat
import (
"context"
"fmt"
"resume-platform/internal/model"
"resume-platform/internal/prompt"
"resume-platform/internal/repository"
"resume-platform/internal/service/ai"
"github.com/google/uuid"
)
type ChatService interface {
Chat(ctx context.Context, userID string, question string) (string, error)
GetChatHistory(ctx context.Context, userID string) ([]*model.ChatHistory, error)
DeleteChatHistory(ctx context.Context, userID string) error
}
type chatService struct {
repo repository.ResumeRepository
ai *ai.AIService
}
func NewChatService(repo repository.ResumeRepository, ai *ai.AIService) ChatService {
return &chatService{repo: repo, ai: ai}
}
func (s *chatService) Chat(ctx context.Context, userID string, question string) (string, error) {
chunks, err := s.repo.GetDocumentChunksByUserID(userID)
if err != nil {
return "", err
}
if len(chunks) == 0 {
return s.ai.Generate(ctx, question)
}
var chunkList []model.ChunkWithScore
for _, chunk := range chunks {
chunkList = append(chunkList, model.ChunkWithScore{
Content: chunk.Content,
Metadata: chunk.Embedding,
})
}
var contextStr string
for i, chunk := range chunkList[:min(5, len(chunkList))] {
contextStr += fmt.Sprintf("[文档片段%d]\n%s\n\n", i+1, chunk.Content)
}
promptText := prompt.BuildDocumentAnalysisPrompt(contextStr, question)
answer, err := s.ai.Generate(ctx, promptText)
if err != nil {
return "", err
}
history := &model.ChatHistory{
ID: uuid.New().String(),
UserID: userID,
Question: question,
Answer: answer,
Context: contextStr,
}
s.repo.CreateChatHistory(history)
return answer, nil
}
func (s *chatService) GetChatHistory(ctx context.Context, userID string) ([]*model.ChatHistory, error) {
return s.repo.GetChatHistoryByUserID(userID)
}
func (s *chatService) DeleteChatHistory(ctx context.Context, userID string) error {
return nil
}
+34
View File
@@ -0,0 +1,34 @@
package chat
import (
"context"
"resume-platform/internal/agent"
"resume-platform/internal/ai"
)
type IMService struct {
aiProvider ai.Provider
resumeAgent *agent.ResumeAgent
}
func NewIMService(provider ai.Provider, resumeAgent *agent.ResumeAgent) *IMService {
return &IMService{aiProvider: provider, resumeAgent: resumeAgent}
}
func (s *IMService) GetAIProvider() ai.Provider {
return s.aiProvider
}
func (s *IMService) GetResumeAgent() *agent.ResumeAgent {
return s.resumeAgent
}
func (s *IMService) GenerateWithAgent(ctx context.Context, query string) (string, error) {
if s.resumeAgent != nil {
return s.resumeAgent.Run(ctx, query)
}
if s.aiProvider != nil {
return s.aiProvider.Generate(ctx, query)
}
return "", nil
}
@@ -0,0 +1,165 @@
package document
import (
"bytes"
"context"
"encoding/json"
"fmt"
"github.com/google/uuid"
"resume-platform/internal/model"
"resume-platform/internal/parser"
"resume-platform/internal/repository"
"resume-platform/internal/service/knowledge"
"resume-platform/internal/service/resume"
"resume-platform/pkg/logger"
)
type DocumentService interface {
UploadDocument(ctx context.Context, userID string, fileName string, fileType string, fileSize int64, content string) (*model.Document, error)
GetDocuments(ctx context.Context, userID string) ([]*model.Document, error)
GetDocument(ctx context.Context, userID string, documentID string) (*model.Document, error)
DeleteDocument(ctx context.Context, userID string, documentID string) error
ProcessDocument(ctx context.Context, documentID string) error
ParseAndGenerateResume(ctx context.Context, userID string, fileName string, content []byte) (*model.Resume, error)
}
type documentService struct {
repo repository.ResumeRepository
resumeGenerator *resume.ResumeGeneratorService
knowledgeBaseService *knowledge.KnowledgeBaseService
}
func NewDocumentService(repo repository.ResumeRepository, resumeGenerator *resume.ResumeGeneratorService) DocumentService {
return &documentService{repo: repo, resumeGenerator: resumeGenerator}
}
func NewDocumentServiceWithKnowledge(repo repository.ResumeRepository, resumeGenerator *resume.ResumeGeneratorService, knowledgeBaseService *knowledge.KnowledgeBaseService) DocumentService {
return &documentService{repo: repo, resumeGenerator: resumeGenerator, knowledgeBaseService: knowledgeBaseService}
}
func (s *documentService) UploadDocument(ctx context.Context, userID string, fileName string, fileType string, fileSize int64, content string) (*model.Document, error) {
document := &model.Document{
ID: generateID(),
UserID: userID,
Name: fileName,
Type: fileType,
Size: fileSize,
Content: content,
Status: "uploading",
}
err := s.repo.CreateDocument(document)
if err != nil {
return nil, err
}
go s.ProcessDocument(ctx, document.ID)
return document, nil
}
func (s *documentService) GetDocuments(ctx context.Context, userID string) ([]*model.Document, error) {
return s.repo.GetDocumentsByUserID(userID)
}
func (s *documentService) GetDocument(ctx context.Context, userID string, documentID string) (*model.Document, error) {
return s.repo.GetDocument(documentID, userID)
}
func (s *documentService) DeleteDocument(ctx context.Context, userID string, documentID string) error {
return s.repo.DeleteDocument(documentID, userID)
}
func (s *documentService) ProcessDocument(ctx context.Context, documentID string) error {
document, err := s.repo.GetDocumentByID(documentID)
if err != nil {
return err
}
if document.Content == "" {
document.Status = "failed"
document.ErrorMsg = "document content is empty"
return s.repo.UpdateDocument(document)
}
chunks := s.splitText(document.Content, 512)
for i, chunk := range chunks {
metadata := map[string]interface{}{
"document_id": document.ID,
"chunk_index": i,
"user_id": document.UserID,
}
metadataJSON, _ := json.Marshal(metadata)
documentChunk := &model.DocumentChunk{
ID: generateID(),
DocumentID: document.ID,
ChunkIndex: i,
Content: chunk,
Metadata: string(metadataJSON),
}
err := s.repo.CreateDocumentChunk(documentChunk)
if err != nil {
document.Status = "failed"
document.ErrorMsg = err.Error()
s.repo.UpdateDocument(document)
return err
}
}
if s.knowledgeBaseService != nil {
err = s.knowledgeBaseService.AddDocument(ctx, document.ID, document.Content, document.UserID)
if err != nil {
logger.Warnf("Failed to add document to knowledge base: %v", err)
} else {
logger.Infof("Document %s added to knowledge base", document.ID)
}
}
document.Status = "processed"
return s.repo.UpdateDocument(document)
}
func (s *documentService) splitText(text string, chunkSize int) []string {
var chunks []string
words := []rune(text)
for i := 0; i < len(words); i += chunkSize {
end := i + chunkSize
if end > len(words) {
end = len(words)
}
chunks = append(chunks, string(words[i:end]))
}
return chunks
}
func generateID() string {
return uuid.New().String()
}
func (s *documentService) ParseAndGenerateResume(ctx context.Context, userID string, fileName string, content []byte) (*model.Resume, error) {
documentText, err := parser.ParseFile(fileName, bytes.NewReader(content))
if err != nil {
logger.Warnf("Failed to parse document %s: %v", fileName, err)
return nil, err
}
if documentText == "" {
return nil, fmt.Errorf("document content is empty after parsing")
}
resume, err := s.resumeGenerator.GenerateFromDocument(ctx, documentText)
if err != nil {
return nil, err
}
resume.UserID = userID
fmt.Printf("DEBUG document.go: resume.UserID set to: '%s'\n", resume.UserID)
return resume, nil
}
@@ -0,0 +1,132 @@
package knowledge
import (
"context"
"crypto/sha256"
"encoding/hex"
"encoding/json"
"fmt"
"github.com/google/uuid"
"resume-platform/internal/ai"
"resume-platform/internal/model"
"resume-platform/internal/repository"
)
type EmbeddingService struct {
aiProvider ai.Provider
repo repository.ResumeRepository
}
func NewEmbeddingService(provider ai.Provider, repo repository.ResumeRepository) *EmbeddingService {
return &EmbeddingService{aiProvider: provider, repo: repo}
}
func (s *EmbeddingService) Generate(ctx context.Context, text string) ([]float64, error) {
if text == "" {
return nil, fmt.Errorf("empty text")
}
contentHash := s.hashContent(text)
cachedEmbedding, err := s.repo.GetEmbeddingByContentHash(contentHash)
if err == nil && cachedEmbedding != nil && cachedEmbedding.Embedding != "" {
var embedding []float64
if err := json.Unmarshal([]byte(cachedEmbedding.Embedding), &embedding); err == nil && len(embedding) > 0 {
return embedding, nil
}
}
prompt := fmt.Sprintf(`请将以下文本转换为向量嵌入。直接输出JSON数组,不要包含任何其他内容。
文本内容:
%s`, text)
result, err := s.aiProvider.Generate(ctx, prompt)
if err != nil {
return nil, err
}
result = cleanEmbeddingResult(result)
var embedding []float64
if err := json.Unmarshal([]byte(result), &embedding); err != nil {
return nil, fmt.Errorf("failed to parse embedding: %w", err)
}
return embedding, nil
}
func (s *EmbeddingService) GenerateAndStore(ctx context.Context, text, sourceType, sourceID, sourceName, userID string) ([]float64, error) {
embedding, err := s.Generate(ctx, text)
if err != nil {
return nil, err
}
contentHash := s.hashContent(text)
embeddingJSON, _ := json.Marshal(embedding)
err = s.repo.CreateEmbedding(&model.Embedding{
ID: uuid.New().String(),
ContentHash: contentHash,
Embedding: string(embeddingJSON),
SourceType: sourceType,
SourceID: sourceID,
SourceName: sourceName,
UserID: userID,
})
return embedding, err
}
func (s *EmbeddingService) BatchGenerate(ctx context.Context, texts []string) ([][]float64, error) {
var results [][]float64
for _, text := range texts {
embedding, err := s.Generate(ctx, text)
if err != nil {
return nil, err
}
results = append(results, embedding)
}
return results, nil
}
func (s *EmbeddingService) hashContent(content string) string {
h := sha256.New()
h.Write([]byte(content))
return hex.EncodeToString(h.Sum(nil))
}
func cleanEmbeddingResult(result string) string {
result = trimString(result)
if idx := findFirstIndex(result, '['); idx >= 0 {
result = result[idx:]
}
if idx := findLastIndex(result, ']'); idx >= 0 {
result = result[:idx+1]
}
return result
}
func trimString(s string) string {
return s
}
func findFirstIndex(s string, c byte) int {
for i := 0; i < len(s); i++ {
if s[i] == c {
return i
}
}
return -1
}
func findLastIndex(s string, c byte) int {
for i := len(s) - 1; i >= 0; i-- {
if s[i] == c {
return i
}
}
return -1
}
@@ -0,0 +1,362 @@
package knowledge
import (
"context"
"encoding/json"
"fmt"
"github.com/google/uuid"
"resume-platform/internal/model"
"resume-platform/internal/repository"
"resume-platform/pkg/logger"
"strings"
)
type KnowledgeBaseService struct {
embeddingService *EmbeddingService
vectorService *VectorService
repo repository.ResumeRepository
}
func NewKnowledgeBaseService(embeddingService *EmbeddingService, vectorService *VectorService, repo repository.ResumeRepository) *KnowledgeBaseService {
return &KnowledgeBaseService{
embeddingService: embeddingService,
vectorService: vectorService,
repo: repo,
}
}
func (s *KnowledgeBaseService) AddDocument(ctx context.Context, documentID, content, userID string) error {
if content == "" {
return fmt.Errorf("empty content")
}
chunks := s.chunkContent(content, 500, 50)
doc, _ := s.repo.GetDocumentByID(documentID)
sourceName := documentID[:8]
if doc != nil && doc.Name != "" {
sourceName = doc.Name
}
for i, chunk := range chunks {
documentChunk := &model.DocumentChunk{
ID: uuid.New().String(),
DocumentID: documentID,
ChunkIndex: i,
Content: chunk,
Metadata: "",
}
err := s.vectorService.StoreChunk(ctx, documentChunk, userID, sourceName)
if err != nil {
return err
}
}
logger.Infof("Added %d chunks to document %s", len(chunks), documentID)
return nil
}
func (s *KnowledgeBaseService) AddResume(ctx context.Context, resumeID string, resume *model.Resume) error {
if resume == nil {
return fmt.Errorf("nil resume")
}
resumeText := s.serializeResume(resume)
if resumeText == "" {
return fmt.Errorf("empty resume content")
}
chunks := s.chunkContent(resumeText, 500, 50)
sourceName := resume.BasicInfo.Name
if sourceName == "" {
sourceName = resumeID[:8]
}
for i, chunk := range chunks {
documentChunk := &model.DocumentChunk{
ID: uuid.New().String(),
DocumentID: resumeID,
ChunkIndex: i,
Content: chunk,
Metadata: "",
}
err := s.vectorService.StoreChunk(ctx, documentChunk, resume.UserID, sourceName)
if err != nil {
return err
}
}
err := s.vectorService.Store(ctx, resumeText, "resume", resumeID, sourceName, resume.UserID)
if err != nil {
return err
}
logger.Infof("Added resume %s to knowledge base with %d chunks", resumeID, len(chunks))
return nil
}
func (s *KnowledgeBaseService) AddText(ctx context.Context, text, sourceType, sourceID, sourceName, userID string) error {
if text == "" {
return fmt.Errorf("empty text")
}
return s.vectorService.Store(ctx, text, sourceType, sourceID, sourceName, userID)
}
func (s *KnowledgeBaseService) Retrieve(ctx context.Context, query string, topK int, userID string) ([]model.ChunkWithScore, error) {
return s.vectorService.SearchWithChunks(ctx, query, topK, userID)
}
func (s *KnowledgeBaseService) RetrieveAll(ctx context.Context, query string, topK int) ([]model.ChunkWithScore, error) {
return s.vectorService.Search(ctx, query, topK, "")
}
func (s *KnowledgeBaseService) DeleteDocument(ctx context.Context, documentID string) error {
err := s.repo.DeleteEmbeddingsBySource("document", documentID)
if err != nil {
return err
}
return s.vectorService.DeleteDocumentChunks(ctx, documentID)
}
func (s *KnowledgeBaseService) DeleteResume(ctx context.Context, resumeID string) error {
return s.repo.DeleteEmbeddingsBySource("resume", resumeID)
}
func (s *KnowledgeBaseService) GetStats(ctx context.Context, userID string) (map[string]int, error) {
var stats = map[string]int{}
if userID != "" {
embeddings, err := s.repo.GetEmbeddingsByUserID(userID)
if err != nil {
return nil, err
}
stats["embeddings"] = len(embeddings)
chunks, err := s.repo.GetDocumentChunksByUserID(userID)
if err != nil {
return nil, err
}
stats["chunks"] = len(chunks)
} else {
embeddings, err := s.repo.GetAllEmbeddings()
if err != nil {
return nil, err
}
stats["embeddings"] = len(embeddings)
chunks, err := s.repo.GetAllDocumentChunks()
if err != nil {
return nil, err
}
stats["chunks"] = len(chunks)
}
return stats, nil
}
func (s *KnowledgeBaseService) chunkContent(content string, chunkSize, overlap int) []string {
if content == "" {
return nil
}
content = strings.ReplaceAll(content, "\r\n", "\n")
content = strings.ReplaceAll(content, "\r", "\n")
var chunks []string
start := 0
contentLen := len(content)
for start < contentLen {
end := start + chunkSize
if end > contentLen {
end = contentLen
}
if end < contentLen {
for i := end; i > start && i > start+chunkSize-overlap; i-- {
c := rune(content[i])
if c == '\n' || c == ';' || c == '\r' {
end = i + 1
break
}
}
}
chunk := strings.TrimSpace(content[start:end])
if chunk != "" {
chunks = append(chunks, chunk)
}
start = end - overlap
if start < 0 {
start = 0
}
if start >= contentLen {
break
}
}
return chunks
}
func (s *KnowledgeBaseService) serializeResume(resume *model.Resume) string {
var builder strings.Builder
if resume.BasicInfo.Name != "" {
builder.WriteString("姓名:")
builder.WriteString(resume.BasicInfo.Name)
builder.WriteString("\n")
}
if resume.BasicInfo.Title != "" {
builder.WriteString("职位:")
builder.WriteString(resume.BasicInfo.Title)
builder.WriteString("\n")
}
if resume.BasicInfo.Email != "" {
builder.WriteString("邮箱:")
builder.WriteString(resume.BasicInfo.Email)
builder.WriteString("\n")
}
if resume.BasicInfo.Phone != "" {
builder.WriteString("电话:")
builder.WriteString(resume.BasicInfo.Phone)
builder.WriteString("\n")
}
if resume.BasicInfo.Location != "" {
builder.WriteString("所在地:")
builder.WriteString(resume.BasicInfo.Location)
builder.WriteString("\n")
}
if resume.BasicInfo.Summary != "" {
builder.WriteString("个人简介:")
builder.WriteString(resume.BasicInfo.Summary)
builder.WriteString("\n")
}
if resume.BasicInfo.JobTarget != "" {
builder.WriteString("求职目标:")
builder.WriteString(resume.BasicInfo.JobTarget)
builder.WriteString("\n")
}
if len(resume.Experience) > 0 {
builder.WriteString("\n【工作经历】\n")
for _, exp := range resume.Experience {
builder.WriteString("公司:")
builder.WriteString(exp.Company)
builder.WriteString("\n职位:")
builder.WriteString(exp.Position)
builder.WriteString("\n时间:")
builder.WriteString(exp.StartDate)
if exp.EndDate != "" {
builder.WriteString(" - ")
builder.WriteString(exp.EndDate)
}
builder.WriteString("\n职责:")
builder.WriteString(exp.Description)
builder.WriteString("\n")
if len(exp.Highlights) > 0 {
builder.WriteString("亮点:")
builder.WriteString(strings.Join(exp.Highlights, ""))
builder.WriteString("\n")
}
builder.WriteString("\n")
}
}
if len(resume.Education) > 0 {
builder.WriteString("\n【教育背景】\n")
for _, edu := range resume.Education {
builder.WriteString("学校:")
builder.WriteString(edu.School)
builder.WriteString("\n学位:")
builder.WriteString(edu.Degree)
builder.WriteString("\n专业:")
builder.WriteString(edu.Major)
builder.WriteString("\n时间:")
builder.WriteString(edu.StartDate)
if edu.EndDate != "" {
builder.WriteString(" - ")
builder.WriteString(edu.EndDate)
}
builder.WriteString("\n")
}
}
if len(resume.Skills) > 0 {
builder.WriteString("\n【专业技能】\n")
for _, skill := range resume.Skills {
builder.WriteString(skill.Name)
if skill.Level != "" {
builder.WriteString("")
builder.WriteString(skill.Level)
builder.WriteString("")
}
if skill.Category != "" {
builder.WriteString(" - ")
builder.WriteString(skill.Category)
}
builder.WriteString("\n")
}
}
if len(resume.Projects) > 0 {
builder.WriteString("\n【项目经验】\n")
for _, proj := range resume.Projects {
builder.WriteString("项目名称:")
builder.WriteString(proj.Name)
builder.WriteString("\n描述:")
builder.WriteString(proj.Description)
builder.WriteString("\n")
if len(proj.TechStack) > 0 {
builder.WriteString("技术栈:")
builder.WriteString(strings.Join(proj.TechStack, "、"))
builder.WriteString("\n")
}
if len(proj.Highlights) > 0 {
builder.WriteString("亮点:")
builder.WriteString(strings.Join(proj.Highlights, ""))
builder.WriteString("\n")
}
if len(proj.Achievements) > 0 {
builder.WriteString("成果:")
builder.WriteString(strings.Join(proj.Achievements, ""))
builder.WriteString("\n")
}
builder.WriteString("\n")
}
}
result := builder.String()
if len(result) > 15000 {
result = result[:15000]
}
return result
}
func (s *KnowledgeBaseService) BuildIndex(ctx context.Context) error {
documents, err := s.repo.GetAllDocumentChunks()
if err != nil {
return err
}
for _, chunk := range documents {
if chunk.Embedding == "" && chunk.Content != "" {
embedding, err := s.embeddingService.Generate(ctx, chunk.Content)
if err != nil {
return err
}
embeddingJSON, _ := json.Marshal(embedding)
chunk.Embedding = string(embeddingJSON)
err = s.repo.CreateDocumentChunk(chunk)
if err != nil {
return err
}
}
}
return nil
}
+46
View File
@@ -0,0 +1,46 @@
package knowledge
import (
"context"
"fmt"
"resume-platform/internal/ai"
"resume-platform/internal/prompt"
"strings"
)
type RAGService struct {
aiProvider ai.Provider
knowledgeBaseService *KnowledgeBaseService
}
func NewRAGService(provider ai.Provider, knowledgeBaseService *KnowledgeBaseService) *RAGService {
return &RAGService{aiProvider: provider, knowledgeBaseService: knowledgeBaseService}
}
func (s *RAGService) Generate(ctx context.Context, query string, userID string) (string, error) {
contextResults, err := s.knowledgeBaseService.Retrieve(ctx, query, 5, userID)
if err != nil {
return "", err
}
var contextStr string
for _, result := range contextResults {
contextStr += fmt.Sprintf("【来源:%s】\n%s\n\n", result.SourceName, result.Content)
}
if contextStr == "" {
return s.aiProvider.Generate(ctx, query)
}
promptText := prompt.BuildRAGAnswerPrompt(contextStr, query)
return s.aiProvider.Generate(ctx, promptText)
}
func (s *RAGService) GenerateWithContext(ctx context.Context, query string, context []string) (string, error) {
contextStr := strings.Join(context, "\n\n")
promptText := prompt.BuildRAGAnswerPrompt(contextStr, query)
return s.aiProvider.Generate(ctx, promptText)
}
@@ -0,0 +1,201 @@
package knowledge
import (
"context"
"encoding/json"
"fmt"
"math"
"resume-platform/internal/model"
"resume-platform/internal/repository"
"sort"
)
type VectorService struct {
embeddingService *EmbeddingService
repo repository.ResumeRepository
}
func NewVectorService(embeddingService *EmbeddingService, repo repository.ResumeRepository) *VectorService {
return &VectorService{embeddingService: embeddingService, repo: repo}
}
func (s *VectorService) Store(ctx context.Context, content, sourceType, sourceID, sourceName, userID string) error {
_, err := s.embeddingService.GenerateAndStore(ctx, content, sourceType, sourceID, sourceName, userID)
return err
}
func (s *VectorService) StoreChunk(ctx context.Context, chunk *model.DocumentChunk, userID string, sourceName string) error {
if chunk.Content == "" {
return nil
}
embedding, err := s.embeddingService.Generate(ctx, chunk.Content)
if err != nil {
return err
}
embeddingJSON, _ := json.Marshal(embedding)
chunk.Embedding = string(embeddingJSON)
err = s.repo.CreateDocumentChunk(chunk)
if err != nil {
return err
}
contentHash := s.embeddingService.hashContent(chunk.Content)
err = s.repo.CreateEmbedding(&model.Embedding{
ID: chunk.ID,
ContentHash: contentHash,
Embedding: string(embeddingJSON),
SourceType: "document",
SourceID: chunk.DocumentID,
SourceName: sourceName,
UserID: userID,
})
return err
}
func (s *VectorService) Search(ctx context.Context, query string, topK int, userID string) ([]model.ChunkWithScore, error) {
if query == "" {
return nil, fmt.Errorf("empty query")
}
queryEmbedding, err := s.embeddingService.Generate(ctx, query)
if err != nil {
return nil, err
}
var allEmbeddings []*model.Embedding
if userID != "" {
allEmbeddings, err = s.repo.GetEmbeddingsByUserID(userID)
} else {
allEmbeddings, err = s.repo.GetAllEmbeddings()
}
if err != nil {
return nil, err
}
var results []model.ChunkWithScore
for _, emb := range allEmbeddings {
if emb.Embedding == "" {
continue
}
var embedding []float64
if err := json.Unmarshal([]byte(emb.Embedding), &embedding); err != nil {
continue
}
score := cosineSimilarity(queryEmbedding, embedding)
if score > 0.3 {
results = append(results, model.ChunkWithScore{
Content: emb.SourceName,
Score: score,
SourceID: emb.SourceID,
SourceName: emb.SourceName,
SourceType: emb.SourceType,
})
}
}
sort.Slice(results, func(i, j int) bool {
return results[i].Score > results[j].Score
})
if len(results) > topK {
results = results[:topK]
}
return results, nil
}
func (s *VectorService) SearchWithChunks(ctx context.Context, query string, topK int, userID string) ([]model.ChunkWithScore, error) {
if query == "" {
return nil, fmt.Errorf("empty query")
}
queryEmbedding, err := s.embeddingService.Generate(ctx, query)
if err != nil {
return nil, err
}
var allChunks []*model.DocumentChunk
if userID != "" {
allChunks, err = s.repo.GetDocumentChunksByUserID(userID)
} else {
allChunks, err = s.repo.GetAllDocumentChunks()
}
if err != nil {
return nil, err
}
var results []model.ChunkWithScore
for _, chunk := range allChunks {
if chunk.Embedding == "" || chunk.Content == "" {
continue
}
var embedding []float64
if err := json.Unmarshal([]byte(chunk.Embedding), &embedding); err != nil {
continue
}
score := cosineSimilarity(queryEmbedding, embedding)
if score > 0.3 {
doc, _ := s.repo.GetDocumentByID(chunk.DocumentID)
sourceName := ""
if doc != nil {
sourceName = doc.Name
}
results = append(results, model.ChunkWithScore{
Content: chunk.Content,
Score: score,
SourceID: chunk.DocumentID,
SourceName: sourceName,
SourceType: "document",
})
}
}
sort.Slice(results, func(i, j int) bool {
return results[i].Score > results[j].Score
})
if len(results) > topK {
results = results[:topK]
}
return results, nil
}
func (s *VectorService) Delete(ctx context.Context, sourceType, sourceID string) error {
return s.repo.DeleteEmbeddingsBySource(sourceType, sourceID)
}
func (s *VectorService) DeleteDocumentChunks(ctx context.Context, documentID string) error {
_, err := s.repo.GetDocumentChunksByDocumentID(documentID)
if err != nil {
return err
}
return nil
}
func cosineSimilarity(a, b []float64) float64 {
if len(a) != len(b) {
return 0
}
var dotProduct, magA, magB float64
for i := range a {
dotProduct += a[i] * b[i]
magA += a[i] * a[i]
magB += b[i] * b[i]
}
if magA == 0 || magB == 0 {
return 0
}
return dotProduct / (math.Sqrt(magA) * math.Sqrt(magB))
}
@@ -0,0 +1,52 @@
package portfolio
import (
"resume-platform/internal/model"
"resume-platform/internal/repository"
)
type PortfolioService interface {
GetPortfolioItems() ([]*model.PortfolioItem, error)
GetPortfolioItemsWithPagination(page, pageSize int) ([]*model.PortfolioItem, int, error)
GetPortfolioItemsByUserID(userID string) ([]*model.PortfolioItem, error)
GetPortfolioItem(id uint) (*model.PortfolioItem, error)
CreatePortfolioItem(item *model.PortfolioItem) error
UpdatePortfolioItem(id uint, item *model.PortfolioItem) error
DeletePortfolioItem(id uint) error
}
type portfolioService struct {
repo repository.ResumeRepository
}
func NewPortfolioService(repo repository.ResumeRepository) PortfolioService {
return &portfolioService{repo: repo}
}
func (s *portfolioService) GetPortfolioItems() ([]*model.PortfolioItem, error) {
return s.repo.GetPortfolioItems()
}
func (s *portfolioService) GetPortfolioItemsWithPagination(page, pageSize int) ([]*model.PortfolioItem, int, error) {
return s.repo.GetPortfolioItemsWithPagination(page, pageSize)
}
func (s *portfolioService) GetPortfolioItemsByUserID(userID string) ([]*model.PortfolioItem, error) {
return s.repo.GetPortfolioItemsByUserID(userID)
}
func (s *portfolioService) GetPortfolioItem(id uint) (*model.PortfolioItem, error) {
return s.repo.GetPortfolioItem(id)
}
func (s *portfolioService) CreatePortfolioItem(item *model.PortfolioItem) error {
return s.repo.CreatePortfolioItem(item)
}
func (s *portfolioService) UpdatePortfolioItem(id uint, item *model.PortfolioItem) error {
return s.repo.UpdatePortfolioItem(id, item)
}
func (s *portfolioService) DeletePortfolioItem(id uint) error {
return s.repo.DeletePortfolioItem(id)
}
+65
View File
@@ -0,0 +1,65 @@
package quiz
import (
"resume-platform/internal/model"
"resume-platform/internal/repository"
"github.com/google/uuid"
)
type QuizService interface {
CreateQuizRecord(record *model.QuizRecord) error
GetQuizRecordsByResume(resumeRoute string) ([]*model.QuizRecord, error)
GetQuizRecord(id string) (*model.QuizRecord, error)
DeleteQuizRecord(id string) error
CreateFavoriteQuestion(fav *model.FavoriteQuestion) error
GetFavoriteQuestions(resumeRoute string, isFavorite, isCorrect *bool) ([]*model.FavoriteQuestion, error)
DeleteFavoriteQuestion(id string) error
ToggleFavorite(id string) error
}
type quizService struct {
repo repository.ResumeRepository
}
func NewQuizService(repo repository.ResumeRepository) QuizService {
return &quizService{repo: repo}
}
func (s *quizService) CreateQuizRecord(record *model.QuizRecord) error {
if record.ID == "" {
record.ID = uuid.New().String()
}
return s.repo.CreateQuizRecord(record)
}
func (s *quizService) GetQuizRecordsByResume(resumeRoute string) ([]*model.QuizRecord, error) {
return s.repo.GetQuizRecordsByResume(resumeRoute)
}
func (s *quizService) GetQuizRecord(id string) (*model.QuizRecord, error) {
return s.repo.GetQuizRecord(id)
}
func (s *quizService) DeleteQuizRecord(id string) error {
return s.repo.DeleteQuizRecord(id)
}
func (s *quizService) CreateFavoriteQuestion(fav *model.FavoriteQuestion) error {
if fav.ID == "" {
fav.ID = uuid.New().String()
}
return s.repo.CreateFavoriteQuestion(fav)
}
func (s *quizService) GetFavoriteQuestions(resumeRoute string, isFavorite, isCorrect *bool) ([]*model.FavoriteQuestion, error) {
return s.repo.GetFavoriteQuestions(resumeRoute, isFavorite, isCorrect)
}
func (s *quizService) DeleteFavoriteQuestion(id string) error {
return s.repo.DeleteFavoriteQuestion(id)
}
func (s *quizService) ToggleFavorite(id string) error {
return s.repo.ToggleFavorite(id)
}
+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
}
+85
View File
@@ -0,0 +1,85 @@
package system
import (
"resume-platform/internal/model"
"resume-platform/internal/repository"
"github.com/google/uuid"
)
type SystemService interface {
GetAllMenus() ([]*model.Menu, error)
CreateMenu(menu *model.Menu) error
UpdateMenu(id string, menu *model.Menu) error
DeleteMenu(id string) error
GetSystemConfig() (*model.SystemConfig, error)
CreateSystemConfig(config *model.SystemConfig) error
UpdateSystemConfig(id string, config *model.SystemConfig) error
InitDefaultMenus() error
}
type systemService struct {
repo repository.ResumeRepository
}
func NewSystemService(repo repository.ResumeRepository) SystemService {
return &systemService{repo: repo}
}
func (s *systemService) GetAllMenus() ([]*model.Menu, error) {
return s.repo.GetAllMenus()
}
func (s *systemService) CreateMenu(menu *model.Menu) error {
if menu.ID == "" {
menu.ID = uuid.New().String()
}
return s.repo.CreateMenu(menu)
}
func (s *systemService) UpdateMenu(id string, menu *model.Menu) error {
return s.repo.UpdateMenu(id, menu)
}
func (s *systemService) DeleteMenu(id string) error {
return s.repo.DeleteMenu(id)
}
func (s *systemService) GetSystemConfig() (*model.SystemConfig, error) {
return s.repo.GetSystemConfig()
}
func (s *systemService) CreateSystemConfig(config *model.SystemConfig) error {
if config.ID == "" {
config.ID = "default"
}
return s.repo.CreateSystemConfig(config)
}
func (s *systemService) UpdateSystemConfig(id string, config *model.SystemConfig) error {
return s.repo.UpdateSystemConfig(id, config)
}
func (s *systemService) InitDefaultMenus() error {
menus, err := s.repo.GetAllMenus()
if err != nil {
return err
}
if len(menus) > 0 {
return nil
}
defaultMenus := []*model.Menu{
{ID: "menu-user", Name: "人员管理", Icon: "fas fa-users", Path: "user", ParentID: "", SortOrder: 1, IsFixed: true, IsEnabled: true},
{ID: "menu-menu", Name: "菜单管理", Icon: "fas fa-list", Path: "menu", ParentID: "", SortOrder: 2, IsFixed: true, IsEnabled: true},
{ID: "menu-settings", Name: "系统设置", Icon: "fas fa-cog", Path: "settings", ParentID: "", SortOrder: 3, IsFixed: true, IsEnabled: true},
}
for _, menu := range defaultMenus {
if err := s.repo.CreateMenu(menu); err != nil {
return err
}
}
return nil
}
+57
View File
@@ -0,0 +1,57 @@
package user
import (
"resume-platform/internal/model"
"resume-platform/internal/repository"
"github.com/google/uuid"
)
type UserService interface {
CreateUser(user *model.User) error
GetUserByUsername(username string) (*model.User, error)
GetUserByID(id string) (*model.User, error)
GetAllUsers() ([]*model.User, error)
GetUsersWithPagination(page, pageSize int) ([]*model.User, int, error)
UpdateUser(id string, user *model.User) error
DeleteUser(id string) error
}
type userService struct {
repo repository.ResumeRepository
}
func NewUserService(repo repository.ResumeRepository) UserService {
return &userService{repo: repo}
}
func (s *userService) CreateUser(user *model.User) error {
if user.ID == "" {
user.ID = uuid.New().String()
}
return s.repo.CreateUser(user)
}
func (s *userService) GetUserByUsername(username string) (*model.User, error) {
return s.repo.GetUserByUsername(username)
}
func (s *userService) GetUserByID(id string) (*model.User, error) {
return s.repo.GetUserByID(id)
}
func (s *userService) GetAllUsers() ([]*model.User, error) {
return s.repo.GetAllUsers()
}
func (s *userService) GetUsersWithPagination(page, pageSize int) ([]*model.User, int, error) {
return s.repo.GetUsersWithPagination(page, pageSize)
}
func (s *userService) UpdateUser(id string, user *model.User) error {
return s.repo.UpdateUser(id, user)
}
func (s *userService) DeleteUser(id string) error {
return s.repo.DeleteUser(id)
}