This commit is contained in:
JACKYMYPERSON
2025-11-04 23:00:01 +08:00
parent 334752373e
commit 76b863b99d
10 changed files with 12 additions and 57 deletions

View File

@@ -8,50 +8,42 @@ import (
"sync" "sync"
"time" "time"
_ "modernc.org/sqlite" // 纯 Go 驱动,驱动名是 "sqlite" 而非 "sqlite3" _ "modernc.org/sqlite"
) )
var ( var (
GlobalDB *sql.DB GlobalDB *sql.DB
once sync.Once // 确保 InitCache 只执行一次(单例初始化) once sync.Once
) )
// InitCache 初始化 SQLite 数据库连接(全局唯一,兼容 CGO_ENABLED=0
func InitCache() { func InitCache() {
once.Do(func() { once.Do(func() {
// 1. 数据库文件路径(自定义为你的用户数据库路径)
dbPath := "./database/local/user.db" dbPath := "./database/local/user.db"
// 2. 创建数据库目录(确保上级目录存在,不存在则自动创建)
if err := os.MkdirAll(filepath.Dir(dbPath), 0755); err != nil { if err := os.MkdirAll(filepath.Dir(dbPath), 0755); err != nil {
panic(fmt.Sprintf("创建数据库目录失败:%v", err)) panic(fmt.Sprintf("创建数据库目录失败:%v", err))
} }
// 3. 打开 SQLite 连接:驱动名必须是 "sqlite"(对应 modernc.org/sqlite
db, err := sql.Open("sqlite", dbPath) db, err := sql.Open("sqlite", dbPath)
if err != nil { if err != nil {
panic(fmt.Sprintf("打开 SQLite 数据库失败:%v", err)) panic(fmt.Sprintf("打开 SQLite 数据库失败:%v", err))
} }
// 4. 验证连接有效性(必做,避免连接创建成功但不可用)
if err := db.Ping(); err != nil { if err := db.Ping(); err != nil {
_ = db.Close() // 连接失败时释放资源 _ = db.Close()
panic(fmt.Sprintf("验证 SQLite 连接失败:%v", err)) panic(fmt.Sprintf("验证 SQLite 连接失败:%v", err))
} }
// 5. 连接池配置(适配 SQLite 单文件特性) db.SetMaxOpenConns(1)
db.SetMaxOpenConns(1) // 最大打开连接数=1避免文件锁冲突 db.SetMaxIdleConns(1)
db.SetMaxIdleConns(1) // 最大空闲连接数=1与最大打开数一致 db.SetConnMaxLifetime(0)
db.SetConnMaxLifetime(0) // 连接生命周期无限制 db.SetConnMaxIdleTime(30 * time.Minute)
db.SetConnMaxIdleTime(30 * time.Minute) // 空闲连接30分钟超时释放
// 6. 赋值全局变量,初始化完成
GlobalDB = db GlobalDB = db
fmt.Printf("SQLite 数据库初始化成功,文件路径:%s\n", dbPath) fmt.Printf("SQLite 数据库初始化成功,文件路径:%s\n", dbPath)
}) })
} }
// GetCacheDB 获取全局唯一的 SQLite 连接(必须先调用 InitCache 初始化)
func GetCacheDB() *sql.DB { func GetCacheDB() *sql.DB {
if GlobalDB == nil { if GlobalDB == nil {
panic("数据库未初始化,请先调用 cache.InitCache()") panic("数据库未初始化,请先调用 cache.InitCache()")
@@ -59,7 +51,6 @@ func GetCacheDB() *sql.DB {
return GlobalDB return GlobalDB
} }
// CloseCache 关闭数据库连接(程序退出时调用,释放资源)
func CloseCache() error { func CloseCache() error {
if GlobalDB != nil { if GlobalDB != nil {
return GlobalDB.Close() return GlobalDB.Close()

View File

@@ -9,21 +9,16 @@ import (
func AVALogin(cfg *config.Config) http.HandlerFunc { func AVALogin(cfg *config.Config) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) { return func(w http.ResponseWriter, r *http.Request) {
// 核心:获取请求头中的 session_id与前端发送的头名称一致
sessionID := r.Header.Get("session_id") sessionID := r.Header.Get("session_id")
// 处理头不存在的情况(返回空字符串)
if sessionID == "" { if sessionID == "" {
w.WriteHeader(http.StatusUnauthorized) w.WriteHeader(http.StatusUnauthorized)
w.Write([]byte(`{"code":401,"msg":"未获取到 session_id请登录"}`)) w.Write([]byte(`{"code":401,"msg":"未获取到 session_id请登录"}`))
return return
} }
// 后续逻辑:验证 sessionID 有效性...
// 例如:查询数据库、缓存判断 session 是否有效
fmt.Printf("获取到的 session_id%s\n", sessionID) fmt.Printf("获取到的 session_id%s\n", sessionID)
// 响应成功(示例)
w.WriteHeader(http.StatusOK) w.WriteHeader(http.StatusOK)
w.Write([]byte(`{"code":0,"msg":"验证成功"}`)) w.Write([]byte(`{"code":0,"msg":"验证成功"}`))
} }

View File

@@ -23,7 +23,6 @@ func AdminLogin(cfg *config.Config) http.HandlerFunc {
return return
} }
// 验证必填参数
if req.Username == "" { if req.Username == "" {
httpx.ErrorCtx(r.Context(), w, errors.New("用户名不能为空")) httpx.ErrorCtx(r.Context(), w, errors.New("用户名不能为空"))
return return
@@ -43,7 +42,7 @@ func AdminLogin(cfg *config.Config) http.HandlerFunc {
mysqlCfg.Database, mysqlCfg.Database,
mysqlCfg.Charset, mysqlCfg.Charset,
) )
fmt.Println("接收到articlePost请求")
conn := sqlx.NewSqlConn("mysql", dsn) conn := sqlx.NewSqlConn("mysql", dsn)
AdminModel := model.NewAdminModel(conn) AdminModel := model.NewAdminModel(conn)
@@ -60,10 +59,8 @@ func AdminLogin(cfg *config.Config) http.HandlerFunc {
} }
func GetClientIP(r *http.Request) string { func GetClientIP(r *http.Request) string {
// 1. 优先从代理头获取如果经过反向代理如Nginx
ip := r.Header.Get("X-Forwarded-For") ip := r.Header.Get("X-Forwarded-For")
if ip != "" { if ip != "" {
// X-Forwarded-For 可能包含多个IP客户端IP, 代理1IP, 代理2IP...),取第一个
parts := strings.Split(ip, ",") parts := strings.Split(ip, ",")
if len(parts) > 0 { if len(parts) > 0 {
ip = strings.TrimSpace(parts[0]) ip = strings.TrimSpace(parts[0])
@@ -73,13 +70,11 @@ func GetClientIP(r *http.Request) string {
} }
} }
// 2. 其次从 X-Real-IP 获取(部分代理会设置此头)
ip = r.Header.Get("X-Real-IP") ip = r.Header.Get("X-Real-IP")
if ip != "" { if ip != "" {
return ip return ip
} }
// 3. 最后从 RemoteAddr 获取原始客户端IP可能包含端口
ip, _, err := net.SplitHostPort(r.RemoteAddr) ip, _, err := net.SplitHostPort(r.RemoteAddr)
if err != nil { if err != nil {
// 若解析失败,直接返回 RemoteAddr可能包含端口 // 若解析失败,直接返回 RemoteAddr可能包含端口

View File

@@ -33,7 +33,6 @@ func CreateAdminHandler(cfg *config.Config) http.HandlerFunc {
mysqlCfg.Database, mysqlCfg.Database,
mysqlCfg.Charset, mysqlCfg.Charset,
) )
fmt.Println("接收到articlePost请求")
conn := sqlx.NewSqlConn("mysql", dsn) conn := sqlx.NewSqlConn("mysql", dsn)
AdminModel := model.NewAdminModel(conn) AdminModel := model.NewAdminModel(conn)

View File

@@ -33,7 +33,6 @@ func DeleteAdminHandler(cfg *config.Config) http.HandlerFunc {
mysqlCfg.Database, mysqlCfg.Database,
mysqlCfg.Charset, mysqlCfg.Charset,
) )
fmt.Println("接收到articlePost请求")
conn := sqlx.NewSqlConn("mysql", dsn) conn := sqlx.NewSqlConn("mysql", dsn)
AdminModel := model.NewAdminModel(conn) AdminModel := model.NewAdminModel(conn)

View File

@@ -33,7 +33,6 @@ func GetAdminHandler(cfg *config.Config) http.HandlerFunc {
mysqlCfg.Database, mysqlCfg.Database,
mysqlCfg.Charset, mysqlCfg.Charset,
) )
fmt.Println("接收到articlePost请求")
conn := sqlx.NewSqlConn("mysql", dsn) conn := sqlx.NewSqlConn("mysql", dsn)
AdminModel := model.NewAdminModel(conn) AdminModel := model.NewAdminModel(conn)

View File

@@ -33,7 +33,6 @@ func ListAdminHandler(cfg *config.Config) http.HandlerFunc {
mysqlCfg.Database, mysqlCfg.Database,
mysqlCfg.Charset, mysqlCfg.Charset,
) )
fmt.Println("接收到articlePost请求")
conn := sqlx.NewSqlConn("mysql", dsn) conn := sqlx.NewSqlConn("mysql", dsn)
AdminModel := model.NewAdminModel(conn) AdminModel := model.NewAdminModel(conn)

View File

@@ -33,7 +33,6 @@ func UpdateAdminHandler(cfg *config.Config) http.HandlerFunc {
mysqlCfg.Database, mysqlCfg.Database,
mysqlCfg.Charset, mysqlCfg.Charset,
) )
fmt.Println("接收到articlePost请求")
conn := sqlx.NewSqlConn("mysql", dsn) conn := sqlx.NewSqlConn("mysql", dsn)
AdminModel := model.NewAdminModel(conn) AdminModel := model.NewAdminModel(conn)

View File

@@ -35,7 +35,6 @@ func NewLoginAdminLogic(ctx context.Context, cfg *config.Config, model model.Adm
} }
func (l *LoginAdminLogic) LoginAdmin(req *types.LoginReq, ip string) (resp *types.LoginResp, err error) { func (l *LoginAdminLogic) LoginAdmin(req *types.LoginReq, ip string) (resp *types.LoginResp, err error) {
// 1. 验证请求参数
if err := l.validateReq(req); err != nil { if err := l.validateReq(req); err != nil {
return &types.LoginResp{ return &types.LoginResp{
Code: 1, Code: 1,
@@ -43,17 +42,14 @@ func (l *LoginAdminLogic) LoginAdmin(req *types.LoginReq, ip string) (resp *type
}, nil }, nil
} }
// 2. 从数据库查询用户(通过 admin 表模型查询,符合 go-zero 分层规范)
admin, err := l.model.FindOneByUsername(l.ctx, req.Username) admin, err := l.model.FindOneByUsername(l.ctx, req.Username)
if err != nil { if err != nil {
// 处理查询错误:区分"用户不存在"和"数据库异常"
if err == model.ErrNotFound { if err == model.ErrNotFound {
return &types.LoginResp{ return &types.LoginResp{
Code: 1, Code: 1,
Msg: "用户名或密码错误", Msg: "用户名或密码错误",
}, nil }, nil
} }
// 数据库异常(记录日志,返回通用错误)
l.Logger.Errorf("查询管理员失败: %v, username: %s", err, req.Username) l.Logger.Errorf("查询管理员失败: %v, username: %s", err, req.Username)
return &types.LoginResp{ return &types.LoginResp{
Code: 500, Code: 500,
@@ -61,7 +57,6 @@ func (l *LoginAdminLogic) LoginAdmin(req *types.LoginReq, ip string) (resp *type
}, nil }, nil
} }
// 3. 检查账号状态1=启用0=禁用,与表结构一致)
if admin.Status != 1 { if admin.Status != 1 {
return &types.LoginResp{ return &types.LoginResp{
Code: 1, Code: 1,
@@ -69,7 +64,6 @@ func (l *LoginAdminLogic) LoginAdmin(req *types.LoginReq, ip string) (resp *type
}, nil }, nil
} }
// 4. 验证密码(数据库存储 bcrypt 加密后的密码)
if _, err := VerifyPassword(req.Password, admin.Password); err != nil { if _, err := VerifyPassword(req.Password, admin.Password); err != nil {
return &types.LoginResp{ return &types.LoginResp{
Code: 1, Code: 1,
@@ -96,7 +90,6 @@ func (l *LoginAdminLogic) LoginAdmin(req *types.LoginReq, ip string) (resp *type
}, nil }, nil
} }
// 验证请求参数
func (l *LoginAdminLogic) validateReq(req *types.LoginReq) error { func (l *LoginAdminLogic) validateReq(req *types.LoginReq) error {
if req.Username == "" { if req.Username == "" {
return errors.New("用户名不能为空") return errors.New("用户名不能为空")
@@ -113,11 +106,11 @@ func VerifyPassword(plainPassword, hashedPassword string) (bool, error) {
err := bcrypt.CompareHashAndPassword(hashedBytes, plainBytes) err := bcrypt.CompareHashAndPassword(hashedBytes, plainBytes)
if err == nil { if err == nil {
return true, nil // 匹配成功(密码正确) return true, nil
} else if err == bcrypt.ErrMismatchedHashAndPassword { } else if err == bcrypt.ErrMismatchedHashAndPassword {
return false, nil // 匹配失败(密码错误) return false, nil
} else { } else {
return false, err // 其他异常(如哈希格式错误、内存不足等) return false, err
} }
} }

View File

@@ -13,10 +13,7 @@ import (
) )
func main() { func main() {
// 目标开发环境go run找项目根目录的 config生产环境打包后找可执行文件同级的 config
var configPath string var configPath string
// 1. 先获取当前程序的路径(开发时是临时路径,生产时是打包后的路径)
exePath, err := os.Executable() exePath, err := os.Executable()
if err != nil { if err != nil {
fmt.Printf("获取程序路径失败:%v\n", err) fmt.Printf("获取程序路径失败:%v\n", err)
@@ -24,25 +21,17 @@ func main() {
} }
exeDir := filepath.Dir(exePath) exeDir := filepath.Dir(exePath)
// 2. 开发环境兼容:判断是否在 GoLand 的临时目录(或项目根目录)
// 逻辑:如果 exeDir 包含 "tmp"(临时目录特征),则去上级目录找项目根的 config
// 可根据你的项目结构调整判断逻辑(比如项目名包含 "myproject",也可以判断 strings.Contains(exeDir, "myproject")
if strings.Contains(strings.ToLower(exeDir), "tmp") { if strings.Contains(strings.ToLower(exeDir), "tmp") {
// 开发环境:从临时目录向上回溯,找到项目根目录(根据实际项目层级调整 ../ 的数量) projectRoot := filepath.Join(exeDir, "..")
// 示例:临时目录 → 项目根目录(假设临时目录在项目根下的 tmp 子目录需回溯1级
projectRoot := filepath.Join(exeDir, "..") // 若回溯不够,可改成 "../..",直到找到项目根
configPath = filepath.Join(projectRoot, "config", "config.yaml") configPath = filepath.Join(projectRoot, "config", "config.yaml")
// 验证如果没找到再尝试用当前工作目录go run 的工作目录默认是项目根)
if _, err := os.Stat(configPath); err != nil { if _, err := os.Stat(configPath); err != nil {
configPath = filepath.Join(".", "config", "config.yaml") configPath = filepath.Join(".", "config", "config.yaml")
} }
} else { } else {
// 生产环境(打包后):用可执行文件同级的 config 文件夹
configPath = filepath.Join(exeDir, "config", "config.yaml") configPath = filepath.Join(exeDir, "config", "config.yaml")
} }
// 3. 验证路径是否存在(可选,方便调试)
if _, err := os.Stat(configPath); err != nil { if _, err := os.Stat(configPath); err != nil {
fmt.Printf("配置文件不存在:%s\n", configPath) fmt.Printf("配置文件不存在:%s\n", configPath)
return return
@@ -61,13 +50,10 @@ func main() {
} }
}() }()
// 设置路由
r := router.SetupRouter(cfg) r := router.SetupRouter(cfg)
// 应用跨域中间件
r.Use(cors.CorsMiddleware(&cfg.Server)) r.Use(cors.CorsMiddleware(&cfg.Server))
// 启动服务
addr := fmt.Sprintf(":%s", cfg.Server.Port) addr := fmt.Sprintf(":%s", cfg.Server.Port)
fmt.Printf("后端服务启动成功地址http://localhost%s\n", addr) fmt.Printf("后端服务启动成功地址http://localhost%s\n", addr)
if err := r.Run(addr); err != nil { if err := r.Run(addr); err != nil {