feat(*): go 后端项目脚手架
This commit is contained in:
25
internal/api/api.go
Normal file
25
internal/api/api.go
Normal file
@@ -0,0 +1,25 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"github.com/gin-gonic/gin"
|
||||
swaggerfiles "github.com/swaggo/files"
|
||||
ginSwagger "github.com/swaggo/gin-swagger"
|
||||
docs "github.com/zhilv666/navsite/docs"
|
||||
"github.com/zhilv666/navsite/internal/api/handler"
|
||||
"github.com/zhilv666/navsite/internal/model"
|
||||
)
|
||||
|
||||
func RegisterRouter(r *gin.Engine) {
|
||||
model.Init()
|
||||
docs.SwaggerInfo.BasePath = "/"
|
||||
r.GET("/swagger/*any", ginSwagger.WrapHandler(swaggerfiles.Handler))
|
||||
|
||||
v1 := r.Group("/api/v1")
|
||||
{
|
||||
handler.RegisterRouterAuth(v1.Group("/auth"))
|
||||
handler.RegisterRouterUser(v1.Group("/user"))
|
||||
handler.RegisterRouterCateage(v1.Group("/cateage"))
|
||||
v1.GET("/proxy", handler.Proxy)
|
||||
}
|
||||
|
||||
}
|
||||
102
internal/api/handler/auth.go
Normal file
102
internal/api/handler/auth.go
Normal file
@@ -0,0 +1,102 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/zhilv666/navsite/internal/model"
|
||||
"github.com/zhilv666/navsite/internal/service"
|
||||
"github.com/zhilv666/navsite/pkg/common"
|
||||
"github.com/zhilv666/navsite/pkg/utils"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
type Req struct {
|
||||
Username string `json:"username" binding:"required" example:"admin"`
|
||||
Password string `json:"password" binding:"required" example:"123456"`
|
||||
OtpCode string `json:"otp_code" example:"123456"`
|
||||
}
|
||||
|
||||
type LoginResp struct {
|
||||
Token string `json:"token" example:"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ...zYyfQ.bQeIyXvkOExxD4DAy5Eyjgwj9FbjE-AO6FCLF-YFGVA"`
|
||||
}
|
||||
|
||||
// login
|
||||
// @Summary 用户登录
|
||||
// @Tag Auth
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param login body Req true "登录参数"
|
||||
// @Success 200 {object} common.Response[LoginResp] "登录成功"
|
||||
// @Failure 400 {object} common.Response[any] "登录失败"
|
||||
// @Router /api/v1/auth/login [post]
|
||||
func login(c *gin.Context) {
|
||||
var req Req
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
common.Fail(c, err.Error())
|
||||
return
|
||||
}
|
||||
user, err := service.GetUserByName(req.Username)
|
||||
if err != nil {
|
||||
common.Fail(c, err.Error())
|
||||
return
|
||||
}
|
||||
if user == nil {
|
||||
common.Fail(c, "账号或密码不正确")
|
||||
return
|
||||
}
|
||||
if len(req.Password) < 32 {
|
||||
req.Password = common.Sha1(req.Password)
|
||||
}
|
||||
if user.Password != req.Password {
|
||||
common.Fail(c, "账号或密码不正确")
|
||||
return
|
||||
}
|
||||
token := utils.GenerateJwtToken(user.ID, user.Email, user.SsoID)
|
||||
common.Succ(c, LoginResp{Token: token})
|
||||
}
|
||||
|
||||
// register
|
||||
// @Summary 用户注册
|
||||
// @Tag Auth
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param register body Req true "注册参数"
|
||||
// @Success 200 {object} common.Response[Req] "注册成功"
|
||||
// @Failure 400 {object} common.Response[any] "注册失败"
|
||||
// @Router /api/v1/auth/register [post]
|
||||
func register(c *gin.Context) {
|
||||
var req Req
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
common.Fail(c, err.Error())
|
||||
return
|
||||
}
|
||||
user, err := service.GetUserByName(req.Username)
|
||||
if err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
} else {
|
||||
common.Fail(c, "查询用户失败")
|
||||
return
|
||||
}
|
||||
}
|
||||
if user != nil {
|
||||
common.Fail(c, "该账号已注册")
|
||||
return
|
||||
}
|
||||
if len(req.Password) < 32 {
|
||||
req.Password = common.Sha1(req.Password)
|
||||
}
|
||||
err = service.AddUser(&model.User{
|
||||
Username: req.Username,
|
||||
Password: req.Password,
|
||||
})
|
||||
if err != nil {
|
||||
common.Fail(c, err.Error())
|
||||
return
|
||||
}
|
||||
common.Ok(c, "账号注册成功")
|
||||
}
|
||||
|
||||
func RegisterRouterAuth(g *gin.RouterGroup) {
|
||||
g.POST("/login", login)
|
||||
g.POST("/register", register)
|
||||
}
|
||||
20
internal/api/handler/cateage.go
Normal file
20
internal/api/handler/cateage.go
Normal file
@@ -0,0 +1,20 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/zhilv666/navsite/internal/service"
|
||||
"github.com/zhilv666/navsite/pkg/common"
|
||||
)
|
||||
|
||||
func GetAllCateageWithSites(c *gin.Context) {
|
||||
cateagies, err := service.GetAllCateageWithSites()
|
||||
if err != nil {
|
||||
common.Fail(c, err.Error())
|
||||
return
|
||||
}
|
||||
common.Succ(c, cateagies)
|
||||
}
|
||||
|
||||
func RegisterRouterCateage(g *gin.RouterGroup) {
|
||||
g.GET("/", GetAllCateageWithSites)
|
||||
}
|
||||
42
internal/api/handler/proxy.go
Normal file
42
internal/api/handler/proxy.go
Normal file
@@ -0,0 +1,42 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"io"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/zhilv666/navsite/pkg/common"
|
||||
)
|
||||
|
||||
func Proxy(c *gin.Context) {
|
||||
_url := c.Query("url")
|
||||
if _url == "" {
|
||||
common.Fail(c, "missing url")
|
||||
return
|
||||
}
|
||||
// 请求目标 URL
|
||||
resp, err := http.Get(_url)
|
||||
if err != nil {
|
||||
common.Fail(c, "unable to fetch image")
|
||||
return
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
// 检查 Content-Type 是否为图片
|
||||
contentType := resp.Header.Get("Content-Type")
|
||||
if !strings.HasPrefix(contentType, "image/") {
|
||||
common.Fail(c, "not an image file")
|
||||
return
|
||||
}
|
||||
|
||||
// 设置响应 Content-Type
|
||||
c.Writer.Header().Set("Content-Type", contentType)
|
||||
|
||||
// 将图片数据直接写入响应
|
||||
_, err = io.Copy(c.Writer, resp.Body)
|
||||
if err != nil {
|
||||
common.Fail(c, "failed to write image")
|
||||
return
|
||||
}
|
||||
}
|
||||
35
internal/api/handler/user.go
Normal file
35
internal/api/handler/user.go
Normal file
@@ -0,0 +1,35 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/zhilv666/navsite/internal/service"
|
||||
"github.com/zhilv666/navsite/pkg/common"
|
||||
"strconv"
|
||||
)
|
||||
|
||||
// getUserByID
|
||||
// @Summary 获取指定 ID 用户
|
||||
// @Tag User
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param id path int true "用户 ID"
|
||||
// @Success 200 {object} common.Response[model.User] "获取成功"
|
||||
// @Failure 400 {object} common.Response[any] "获取失败"
|
||||
// @Router /api/v1/user/{id} [get]
|
||||
func getUserByID(c *gin.Context) {
|
||||
id := c.Param("id")
|
||||
idInt, err := strconv.Atoi(id)
|
||||
if err != nil {
|
||||
common.Fail(c, err.Error())
|
||||
}
|
||||
user, err := service.GetUserByID(uint(idInt))
|
||||
if err != nil {
|
||||
common.Fail(c, err.Error())
|
||||
return
|
||||
}
|
||||
common.Succ(c, user)
|
||||
}
|
||||
|
||||
func RegisterRouterUser(g *gin.RouterGroup) {
|
||||
g.GET(":id", getUserByID)
|
||||
}
|
||||
16
internal/model/cateage.go
Normal file
16
internal/model/cateage.go
Normal file
@@ -0,0 +1,16 @@
|
||||
package model
|
||||
|
||||
import (
|
||||
"time"
|
||||
)
|
||||
|
||||
type Cateage struct {
|
||||
ID uint `gorm:"primaryKey" json:"id"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
DeletedAt *time.Time `gorm:"index" json:"-"`
|
||||
|
||||
AnchorId string `gorm:"size:100;not null;unique" json:"anchor_id"`
|
||||
Name string `gorm:"size:100;not null;unique" json:"name"`
|
||||
Sites []Site `gorm:"foreignKey:CateageID" json:"sites"` // 方便获取分类下所有网站
|
||||
}
|
||||
16
internal/model/model.go
Normal file
16
internal/model/model.go
Normal file
@@ -0,0 +1,16 @@
|
||||
package model
|
||||
|
||||
import (
|
||||
"github.com/zhilv666/navsite/pkg/db"
|
||||
"github.com/zhilv666/navsite/pkg/logger"
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
func Init() {
|
||||
err := db.GetDB().AutoMigrate(
|
||||
new(User), new(Cateage), new(Site),
|
||||
)
|
||||
if err != nil {
|
||||
logger.Error("database migrate error", zap.Stack("migrate"))
|
||||
}
|
||||
}
|
||||
18
internal/model/site.go
Normal file
18
internal/model/site.go
Normal file
@@ -0,0 +1,18 @@
|
||||
package model
|
||||
|
||||
import "time"
|
||||
|
||||
type Site struct {
|
||||
ID uint `gorm:"primaryKey" json:"id"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
DeletedAt *time.Time `gorm:"index" json:"-"`
|
||||
|
||||
Name string `gorm:"size:100;not null;unique" json:"name"`
|
||||
Describe string `gorm:"size:100" json:"describe"`
|
||||
CateageID uint `gorm:"not null;index" json:"cateage_id"`
|
||||
Cateage Cateage `gorm:"foreignKey:CateageID;constraint:OnUpdate:CASCADE,OnDelete:SET NULL;" json:"cateage"` // 外键关联
|
||||
Url string `gorm:"size:500;not null" json:"url"`
|
||||
Cors bool `gorm:"default:false" json:"cors"`
|
||||
Icon string `gorm:"size:500;default:'https://cos.kmux.cn/md/20251115174627218.png'" json:"icon"`
|
||||
}
|
||||
23
internal/model/user.go
Normal file
23
internal/model/user.go
Normal file
@@ -0,0 +1,23 @@
|
||||
package model
|
||||
|
||||
import (
|
||||
"time"
|
||||
)
|
||||
|
||||
type User struct {
|
||||
ID uint `gorm:"primarykey" json:"id"`
|
||||
CreatedAt time.Time `gorm:"column:created_at" json:"created_at"`
|
||||
UpdatedAt time.Time `gorm:"column:updated_at" json:"updated_at"`
|
||||
DeletedAt *time.Time `gorm:"index" json:"-"`
|
||||
|
||||
Username string `gorm:"uniqueIndex;size:50;not null" json:"username"` // 邮箱注册
|
||||
Email string `gorm:"uniqueIndex;size:100;not null" json:"email"`
|
||||
Password string `json:"-" gorm:"size:100;not null"` // SHA1 密码
|
||||
Salt string `json:"-" gorm:"size:32;not null"` // 密码盐
|
||||
IsAdmin bool `json:"is_admin"` // true = 管理员
|
||||
Disabled bool `json:"disabled" gorm:"default:false"` // 禁用标志
|
||||
SsoID string `json:"sso_id" gorm:"size:100;unique"` // OAuth2 唯一标识
|
||||
Verified bool `json:"verified" gorm:"default:false"` // 是否通过邮箱验证/管理员审核
|
||||
VerifyAt *time.Time `json:"verify_at,omitempty"` // 验证时间
|
||||
Points int `json:"points" gorm:"default:0"` // 积分
|
||||
}
|
||||
14
internal/router/router.go
Normal file
14
internal/router/router.go
Normal file
@@ -0,0 +1,14 @@
|
||||
package router
|
||||
|
||||
import (
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/zhilv666/navsite/internal/api"
|
||||
)
|
||||
|
||||
func SetupRouter() *gin.Engine {
|
||||
r := gin.Default()
|
||||
|
||||
api.RegisterRouter(r)
|
||||
|
||||
return r
|
||||
}
|
||||
14
internal/service/cateage.go
Normal file
14
internal/service/cateage.go
Normal file
@@ -0,0 +1,14 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"github.com/pkg/errors"
|
||||
"github.com/zhilv666/navsite/internal/model"
|
||||
)
|
||||
|
||||
func GetAllCateageWithSites() ([]model.Cateage, error) {
|
||||
var cateagies []model.Cateage
|
||||
if err := DB().Preload("Sites").Find(&cateagies).Error; err != nil {
|
||||
return nil, errors.Wrap(err, "failed get cateagies")
|
||||
}
|
||||
return cateagies, nil
|
||||
}
|
||||
18
internal/service/service.go
Normal file
18
internal/service/service.go
Normal file
@@ -0,0 +1,18 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"github.com/zhilv666/navsite/pkg/config"
|
||||
"github.com/zhilv666/navsite/pkg/db"
|
||||
"github.com/zhilv666/navsite/pkg/logger"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
func DB() *gorm.DB {
|
||||
_db := db.GetDB()
|
||||
if _db == nil {
|
||||
db.InitDB(config.GetConfig().Database, logger.GetLogger())
|
||||
return db.GetDB()
|
||||
}
|
||||
|
||||
return _db
|
||||
}
|
||||
46
internal/service/user.go
Normal file
46
internal/service/user.go
Normal file
@@ -0,0 +1,46 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"github.com/pkg/errors"
|
||||
|
||||
"github.com/zhilv666/navsite/internal/model"
|
||||
)
|
||||
|
||||
func GetUserByID(id uint) (*model.User, error) {
|
||||
var user model.User
|
||||
if err := DB().First(&user, id).Error; err != nil {
|
||||
return nil, errors.Wrapf(err, "failed get old user")
|
||||
}
|
||||
return &user, nil
|
||||
}
|
||||
|
||||
func GetUserByName(username string) (*model.User, error) {
|
||||
user := model.User{Username: username}
|
||||
if err := DB().Where(user).First(&user).Error; err != nil {
|
||||
return nil, errors.Wrapf(err, "failed get old user")
|
||||
}
|
||||
return &user, nil
|
||||
}
|
||||
|
||||
func GetUsers(pageIndex, pageSize int) (users []*model.User, count int64, err error) {
|
||||
userDB := DB().Model(&model.User{})
|
||||
if err := userDB.Count(&count).Error; err != nil {
|
||||
return nil, 0, errors.Wrapf(err, "failed get counts")
|
||||
}
|
||||
if err := userDB.Order(columnName("id")).Offset((pageIndex - 1) * pageSize).Limit(pageSize).Find(&users).Error; err != nil {
|
||||
return nil, 0, errors.Wrapf(err, "failed get users")
|
||||
}
|
||||
return users, count, nil
|
||||
}
|
||||
|
||||
func AddUser(user *model.User) error {
|
||||
return errors.WithStack(DB().Create(&user).Error)
|
||||
}
|
||||
|
||||
func UpdateUser(user *model.User) error {
|
||||
return errors.WithStack(DB().Save(&user).Error)
|
||||
}
|
||||
|
||||
func DeleteUser(id uint) error {
|
||||
return errors.WithStack(DB().Delete(&model.User{}, id).Error)
|
||||
}
|
||||
19
internal/service/utils.go
Normal file
19
internal/service/utils.go
Normal file
@@ -0,0 +1,19 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/zhilv666/navsite/pkg/config"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
func columnName(name string) string {
|
||||
if config.GetConfig().Database.Driver == "postgres" {
|
||||
return fmt.Sprintf(`"%s"`, name)
|
||||
}
|
||||
return fmt.Sprintf("`%s`", name)
|
||||
}
|
||||
|
||||
func addStorageOrder(db *gorm.DB) *gorm.DB {
|
||||
return DB().Order(fmt.Sprintf("%s, %s", columnName("order"), columnName("id")))
|
||||
}
|
||||
Reference in New Issue
Block a user