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

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
+220
View File
@@ -0,0 +1,220 @@
// Package config 配置加载模块,基于 Viper 读取 YAML 配置并提供默认值
package config
import (
"errors"
"strings"
"github.com/spf13/viper"
)
const (
DefaultAdminPath = "/admin" // 默认后台管理路径
DefaultMaxLoginAttempts = 5 // 默认最大登录尝试次数
DefaultLockoutDuration = 15 // 默认锁定时长(分钟)
DefaultServerHost = "0.0.0.0" // 默认服务监听地址
DefaultServerPort = 8090 // 默认服务端口
DefaultServerMode = "release" // 默认运行模式
DefaultDatabaseType = "sqlite" // 默认数据库类型
DefaultDatabasePath = "./data/resume.db" // 默认数据库文件路径
DefaultLogLevel = "info" // 默认日志级别
DefaultTemplateDefault = "modern" // 默认简历模板
)
// Config 全局配置结构体
type Config struct {
Server ServerConfig `mapstructure:"server"` // 服务配置
Database DatabaseConfig `mapstructure:"database"` // 数据库配置
Auth AuthConfig `mapstructure:"auth"` // 认证配置
Template TemplateConfig `mapstructure:"template"` // 模板配置
Log LogConfig `mapstructure:"log"` // 日志配置
AI AIConfig `mapstructure:"ai"` // AI配置
}
// ServerConfig 服务配置
type ServerConfig struct {
Host string `mapstructure:"host"` // 监听地址
Port int `mapstructure:"port"` // 监听端口
Mode string `mapstructure:"mode"` // 运行模式(debug/release
Debug bool `mapstructure:"debug"` // 是否开启调试
}
// DatabaseConfig 数据库配置
type DatabaseConfig struct {
Type string `mapstructure:"type"` // 数据库类型
Path string `mapstructure:"path"` // 数据库文件路径
Password string `mapstructure:"password"` // 数据库加密密码
}
// AuthConfig 认证配置
type AuthConfig struct {
Enabled bool `mapstructure:"enabled"` // 是否启用认证
Username string `mapstructure:"username"` // 管理员用户名
Password string `mapstructure:"password"` // 管理员密码
AdminPath string `mapstructure:"admin_path"` // 后台路径
MaxLoginAttempts int `mapstructure:"max_login_attempts"` // 最大登录尝试
LockoutDuration int `mapstructure:"lockout_duration"` // 锁定时长(分钟)
}
// TemplateConfig 模板配置
type TemplateConfig struct {
Default string `mapstructure:"default"` // 默认模板名称
}
// LogConfig 日志配置
type LogConfig struct {
Level string `mapstructure:"level"` // 日志级别
Output string `mapstructure:"output"` // 输出文件路径
}
// AIConfig AI 大模型配置
type AIConfig struct {
Provider string `mapstructure:"provider"` // 默认供应商
Enabled bool `mapstructure:"enabled"` // 是否启用
Spark SparkConfig `mapstructure:"spark"` // 讯飞星火配置
Tongyi TongyiConfig `mapstructure:"tongyi"` // 通义千问配置
Doubao DoubaoConfig `mapstructure:"doubao"` // 豆包配置
DeepSeek DeepSeekConfig `mapstructure:"deepseek"` // DeepSeek配置
}
// SparkConfig 讯飞星火配置
type SparkConfig struct {
APPID string `mapstructure:"app_id"` // APP ID
APIKey string `mapstructure:"api_key"` // API Key
APISecret string `mapstructure:"api_secret"` // API Secret
APIPassword string `mapstructure:"api_password"` // API 密码(HTTP接口使用)
BaseURL string `mapstructure:"base_url"` // 接口地址
Model string `mapstructure:"model"` // 模型版本
MaxTokens int `mapstructure:"max_tokens"` // 最大token数
Timeout int `mapstructure:"timeout"` // 超时时间(秒)
WebSearch bool `mapstructure:"web_search"` // 是否启用联网搜索
ShowRefLabel bool `mapstructure:"show_ref_label"` // 是否显示引用标签
}
// TongyiConfig 通义千问配置
type TongyiConfig struct {
APIKey string `mapstructure:"api_key"` // API Key
BaseURL string `mapstructure:"base_url"` // 接口地址
Model string `mapstructure:"model"` // 模型名称
MaxTokens int `mapstructure:"max_tokens"` // 最大token数
Timeout int `mapstructure:"timeout"` // 超时时间(秒)
}
// DoubaoConfig 豆包配置
type DoubaoConfig struct {
APIKey string `mapstructure:"api_key"` // API Key
BaseURL string `mapstructure:"base_url"` // 接口地址
Model string `mapstructure:"model"` // 模型名称
MaxTokens int `mapstructure:"max_tokens"` // 最大token数
Timeout int `mapstructure:"timeout"` // 超时时间(秒)
}
// DeepSeekConfig DeepSeek 配置
type DeepSeekConfig struct {
APIKey string `mapstructure:"api_key"` // API Key
BaseURL string `mapstructure:"base_url"` // 接口地址
Model string `mapstructure:"model"` // 模型名称
MaxTokens int `mapstructure:"max_tokens"` // 最大token数
Timeout int `mapstructure:"timeout"` // 超时时间(秒)
}
// setDefaults 设置 Viper 默认值
// @author sunct
func setDefaults() {
viper.SetDefault("server.host", DefaultServerHost)
viper.SetDefault("server.port", DefaultServerPort)
viper.SetDefault("server.mode", DefaultServerMode)
viper.SetDefault("server.debug", false)
viper.SetDefault("database.type", DefaultDatabaseType)
viper.SetDefault("database.path", DefaultDatabasePath)
viper.SetDefault("auth.enabled", true)
viper.SetDefault("auth.admin_path", DefaultAdminPath)
viper.SetDefault("auth.max_login_attempts", DefaultMaxLoginAttempts)
viper.SetDefault("auth.lockout_duration", DefaultLockoutDuration)
viper.SetDefault("template.default", DefaultTemplateDefault)
viper.SetDefault("log.level", DefaultLogLevel)
}
// LoadConfig 加载并解析配置文件,支持多路径查找
// @return *Config 配置实例
// @return error 加载错误
// @author sunct
func LoadConfig() (*Config, error) {
setDefaults()
viper.SetConfigName("config")
viper.SetConfigType("yaml")
viper.AddConfigPath("./config")
viper.AddConfigPath(".")
viper.AddConfigPath("../config")
viper.AddConfigPath("../")
if err := viper.ReadInConfig(); err != nil {
var configFileNotFoundError viper.ConfigFileNotFoundError
if !errors.As(err, &configFileNotFoundError) {
return nil, err
}
}
var cfg Config
if err := viper.Unmarshal(&cfg); err != nil {
return nil, err
}
if err := validateAndNormalize(&cfg); err != nil {
return nil, err
}
return &cfg, nil
}
// validateAndNormalize 校验并规范化配置,补全默认值
// @param cfg 配置实例
// @return error 校验错误
// @author sunct
func validateAndNormalize(cfg *Config) error {
if cfg.Server.Port <= 0 || cfg.Server.Port > 65535 {
cfg.Server.Port = DefaultServerPort
}
if cfg.Server.Host == "" {
cfg.Server.Host = DefaultServerHost
}
if cfg.Server.Mode == "" {
cfg.Server.Mode = DefaultServerMode
}
if cfg.Database.Type == "" {
cfg.Database.Type = DefaultDatabaseType
}
if cfg.Database.Path == "" {
cfg.Database.Path = DefaultDatabasePath
}
if !strings.HasPrefix(cfg.Auth.AdminPath, "/") {
cfg.Auth.AdminPath = "/" + cfg.Auth.AdminPath
}
if cfg.Auth.AdminPath == "/" {
cfg.Auth.AdminPath = DefaultAdminPath
}
if cfg.Auth.MaxLoginAttempts <= 0 {
cfg.Auth.MaxLoginAttempts = DefaultMaxLoginAttempts
}
if cfg.Auth.LockoutDuration <= 0 {
cfg.Auth.LockoutDuration = DefaultLockoutDuration
}
if cfg.Template.Default == "" {
cfg.Template.Default = DefaultTemplateDefault
}
if cfg.Log.Level == "" {
cfg.Log.Level = DefaultLogLevel
}
return nil
}
+8
View File
@@ -0,0 +1,8 @@
package constant
const (
RequestIdKey = "request-id"
AppNameKey = "app-name"
RequestRouteKey = "request-route"
ClientIP = "client-ip"
)
+76
View File
@@ -0,0 +1,76 @@
package idgen
import (
"crypto/rand"
"fmt"
"sync"
"time"
)
var (
generator *FlakeIDGenerator
once sync.Once
)
type FlakeIDGenerator struct {
mu sync.Mutex
lastTs int64
seq int64
nodeID int64
}
func NewFlakeIDGenerator(nodeID int64) *FlakeIDGenerator {
return &FlakeIDGenerator{
nodeID: nodeID,
}
}
func (g *FlakeIDGenerator) GetID() (string, error) {
g.mu.Lock()
defer g.mu.Unlock()
now := time.Now().UnixNano() / 1e6
if now < g.lastTs {
return "", fmt.Errorf("clock moved backwards")
}
if now == g.lastTs {
g.seq++
if g.seq >= 1024 {
for now <= g.lastTs {
now = time.Now().UnixNano() / 1e6
}
g.seq = 0
}
} else {
g.seq = 0
}
g.lastTs = now
id := (now << 22) | (g.nodeID << 10) | g.seq
return fmt.Sprintf("%d", id), nil
}
func GetID() (string, error) {
once.Do(func() {
nodeID := int64(1)
b := make([]byte, 2)
_, err := rand.Read(b)
if err == nil {
nodeID = int64(b[0])<<8 | int64(b[1])
nodeID &= 0x3FF
}
generator = NewFlakeIDGenerator(nodeID)
})
return generator.GetID()
}
func MustGetID() string {
id, err := GetID()
if err != nil {
return fmt.Sprintf("%d", time.Now().UnixNano())
}
return id
}
+193
View File
@@ -0,0 +1,193 @@
// Package logger 日志模块,基于 logrus 封装统一的日志接口
package logger
import (
"context"
"io"
"os"
"path/filepath"
"resume-platform/pkg/constant"
"github.com/sirupsen/logrus"
)
var (
defaultLogger *logrus.Logger // 默认日志实例
)
// Config 日志配置
type Config struct {
Level string // 日志级别
Output string // 输出文件路径
}
// Init 初始化日志系统
// @param cfg 日志配置
// @return error 初始化错误
// @author sunct
func Init(cfg Config) error {
l := logrus.New()
level, err := logrus.ParseLevel(cfg.Level)
if err != nil {
level = logrus.InfoLevel
}
l.SetLevel(level)
l.SetFormatter(&logrus.TextFormatter{
FullTimestamp: true,
TimestampFormat: "2006-01-02 15:04:05",
})
if cfg.Output != "" {
logDir := filepath.Dir(cfg.Output)
if logDir != "." && logDir != "" {
if err := os.MkdirAll(logDir, 0755); err != nil {
return err
}
}
file, err := os.OpenFile(cfg.Output, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0644)
if err != nil {
return err
}
mw := io.MultiWriter(os.Stdout, file)
l.SetOutput(mw)
} else {
l.SetOutput(os.Stdout)
}
defaultLogger = l
return nil
}
// Get 获取默认日志实例
// @return *logrus.Logger 日志实例
// @author sunct
func Get() *logrus.Logger {
if defaultLogger == nil {
defaultLogger = logrus.New()
defaultLogger.SetFormatter(&logrus.TextFormatter{
FullTimestamp: true,
TimestampFormat: "2006-01-02 15:04:05",
})
}
return defaultLogger
}
// Debug 输出 Debug 级别日志
// @param args 日志内容
func Debug(args ...interface{}) { Get().Debug(args...) }
// Debugf 格式化输出 Debug 级别日志
// @param format 格式字符串
// @param args 格式化参数
func Debugf(format string, args ...interface{}) { Get().Debugf(format, args...) }
// Info 输出 Info 级别日志
// @param args 日志内容
func Info(args ...interface{}) { Get().Info(args...) }
// Infof 格式化输出 Info 级别日志
// @param format 格式字符串
// @param args 格式化参数
func Infof(format string, args ...interface{}) { Get().Infof(format, args...) }
// Warn 输出 Warn 级别日志
// @param args 日志内容
func Warn(args ...interface{}) { Get().Warn(args...) }
// Warnf 格式化输出 Warn 级别日志
// @param format 格式字符串
// @param args 格式化参数
func Warnf(format string, args ...interface{}) { Get().Warnf(format, args...) }
// Error 输出 Error 级别日志
// @param args 日志内容
func Error(args ...interface{}) { Get().Error(args...) }
// Errorf 格式化输出 Error 级别日志
// @param format 格式字符串
// @param args 格式化参数
func Errorf(format string, args ...interface{}) { Get().Errorf(format, args...) }
// Fatal 输出 Fatal 级别日志并退出
// @param args 日志内容
func Fatal(args ...interface{}) { Get().Fatal(args...) }
// Fatalf 格式化输出 Fatal 级别日志并退出
// @param format 格式字符串
// @param args 格式化参数
func Fatalf(format string, args ...interface{}) { Get().Fatalf(format, args...) }
// WithField 添加单字段上下文
// @param key 字段名
// @param value 字段值
// @return *logrus.Entry 带字段的日志条目
// @author sunct
func WithField(key string, value interface{}) *logrus.Entry {
return Get().WithField(key, value)
}
// WithFields 添加多字段上下文
// @param fields 字段映射
// @return *logrus.Entry 带字段的日志条目
// @author sunct
func WithFields(fields logrus.Fields) *logrus.Entry {
return Get().WithFields(fields)
}
func FromContext(ctx context.Context) *logrus.Entry {
entry := Get().WithField("app", "resume-platform")
if reqId := ctx.Value(constant.RequestIdKey); reqId != nil {
entry = entry.WithField("request-id", reqId)
}
if route := ctx.Value(constant.RequestRouteKey); route != nil {
entry = entry.WithField("route", route)
}
if clientIP := ctx.Value(constant.ClientIP); clientIP != nil {
entry = entry.WithField("client-ip", clientIP)
}
return entry
}
func CtxDebug(ctx context.Context, args ...interface{}) {
FromContext(ctx).Debug(args...)
}
func CtxDebugf(ctx context.Context, format string, args ...interface{}) {
FromContext(ctx).Debugf(format, args...)
}
func CtxInfo(ctx context.Context, args ...interface{}) {
FromContext(ctx).Info(args...)
}
func CtxInfof(ctx context.Context, format string, args ...interface{}) {
FromContext(ctx).Infof(format, args...)
}
func CtxWarn(ctx context.Context, args ...interface{}) {
FromContext(ctx).Warn(args...)
}
func CtxWarnf(ctx context.Context, format string, args ...interface{}) {
FromContext(ctx).Warnf(format, args...)
}
func CtxError(ctx context.Context, args ...interface{}) {
FromContext(ctx).Error(args...)
}
func CtxErrorf(ctx context.Context, format string, args ...interface{}) {
FromContext(ctx).Errorf(format, args...)
}
func CtxFatal(ctx context.Context, args ...interface{}) {
FromContext(ctx).Fatal(args...)
}
func CtxFatalf(ctx context.Context, format string, args ...interface{}) {
FromContext(ctx).Fatalf(format, args...)
}
+61
View File
@@ -0,0 +1,61 @@
package utils
import (
"encoding/json"
"strings"
)
func CleanAIResponse(response string) string {
result := strings.TrimSpace(response)
for i := 0; i < 10; i++ {
changed := false
if strings.HasPrefix(result, "```json") {
result = strings.TrimSpace(strings.TrimPrefix(result, "```json"))
changed = true
} else if strings.HasPrefix(result, "```") {
result = strings.TrimSpace(strings.TrimPrefix(result, "```"))
changed = true
}
if strings.HasSuffix(result, "```") {
result = strings.TrimSpace(strings.TrimSuffix(result, "```"))
changed = true
}
if !changed {
break
}
}
firstBrace := strings.Index(result, "{")
lastBrace := strings.LastIndex(result, "}")
if firstBrace >= 0 && lastBrace >= 0 && lastBrace > firstBrace {
result = result[firstBrace : lastBrace+1]
}
result = strings.ReplaceAll(result, "`", "")
result = strings.ReplaceAll(result, "\\n", "\n")
result = strings.ReplaceAll(result, "\\r", "\r")
result = strings.ReplaceAll(result, "\\t", "\t")
return strings.TrimSpace(result)
}
func IsValidJSON(s string) bool {
var js json.RawMessage
return json.Unmarshal([]byte(s), &js) == nil
}
func ExtractJSONFromText(text string) string {
firstBrace := strings.Index(text, "{")
lastBrace := strings.LastIndex(text, "}")
if firstBrace >= 0 && lastBrace >= 0 && lastBrace > firstBrace {
return text[firstBrace : lastBrace+1]
}
return ""
}
+32
View File
@@ -0,0 +1,32 @@
package utils
func Min(a, b int) int {
if a < b {
return a
}
return b
}
func Max(a, b int) int {
if a > b {
return a
}
return b
}
func Clamp(value, min, max int) int {
if value < min {
return min
}
if value > max {
return max
}
return value
}
func Abs(n int) int {
if n < 0 {
return -n
}
return n
}
+59
View File
@@ -0,0 +1,59 @@
package utils
import (
"fmt"
"strings"
"unicode"
)
func ConvertToString(v interface{}) string {
if v == nil {
return ""
}
switch val := v.(type) {
case string:
return val
case int, int8, int16, int32, int64:
return fmt.Sprintf("%d", val)
case float32, float64:
return fmt.Sprintf("%.0f", val)
default:
return fmt.Sprintf("%v", val)
}
}
func ConvertToStringSlice(v []interface{}) []interface{} {
if v == nil {
return []interface{}{}
}
result := make([]interface{}, 0, len(v))
for _, item := range v {
result = append(result, ConvertToString(item))
}
return result
}
func CleanText(text string) string {
var builder strings.Builder
for _, r := range text {
if unicode.IsPrint(r) || r == '\n' || r == '\r' || r == '\t' {
builder.WriteRune(r)
} else {
builder.WriteRune(' ')
}
}
result := builder.String()
result = strings.ReplaceAll(result, "\r\n", "\n")
result = strings.ReplaceAll(result, "\r", "\n")
for strings.Contains(result, "\n\n\n") {
result = strings.ReplaceAll(result, "\n\n\n", "\n\n")
}
for strings.Contains(result, " ") {
result = strings.ReplaceAll(result, " ", " ")
}
return strings.TrimSpace(result)
}