初始化项目框架,完成部分接口开发

This commit is contained in:
2025-07-10 21:04:29 +08:00
commit b2871ec0d2
168 changed files with 6399 additions and 0 deletions

17
utility/ecode/common.go Normal file
View File

@ -0,0 +1,17 @@
package ecode
var (
OK = New(0, "success")
Sub = New(1, "") // 自定义错误信息
Fail = New(2, "server_error")
InvalidOperation = New(3, "invalid_operation")
Params = New(4, "params_error")
Logout = New(5, "not_login")
Disabled = New(6, "account_disabled")
Denied = New(7, "permission_denied")
Expire = New(8, "token_expired")
Auth = New(1000, "auth_failed")
Password = New(1001, "password_incorrect")
EmailExist = New(1002, "email_exists")
NotFound = New(1003, "not_found")
)

90
utility/ecode/ecode.go Normal file
View File

@ -0,0 +1,90 @@
package ecode
import (
"context"
"fmt"
"server/utility/i18n"
"github.com/gogf/gf/v2/errors/gcode"
)
type Error struct {
code int
message string
sub string
params []interface{}
}
func New(code int, message string) Error {
return Error{
code: code,
message: message,
}
}
func (e Error) Params(params ...interface{}) Error {
e.params = append(e.params, params...)
return e
}
func (e Error) Error() string {
return e.Message()
}
func (e Error) Sub(sub string) Error {
e.sub = sub
return e
}
func (e Error) Message() string {
if e.message != "" && len(e.params) > 0 {
e.message = fmt.Sprintf(e.message, e.params...)
}
if e.sub != "" {
if e.message != "" {
if len(e.params) > 0 {
e.message = fmt.Sprintf(e.message, e.params...)
}
return fmt.Sprintf("%s:%s", e.message, e.sub)
}
return e.sub
}
return e.message
}
// MessageI18n 返回国际化消息
func (e Error) MessageI18n(ctx context.Context) string {
// 如果有子消息,优先使用子消息的国际化
if e.sub != "" {
return i18n.T(ctx, e.sub)
}
// 否则使用主消息的国际化
if e.message != "" {
// 尝试从国际化系统获取消息
i18nMsg := i18n.T(ctx, e.message)
if i18nMsg != e.message {
// 如果找到了国际化消息,使用它
if len(e.params) > 0 {
return fmt.Sprintf(i18nMsg, e.params...)
}
return i18nMsg
}
// 如果没有找到国际化消息,使用原来的逻辑
if len(e.params) > 0 {
return fmt.Sprintf(e.message, e.params...)
}
return e.message
}
return ""
}
func (e Error) Code() gcode.Code {
return gcode.New(e.code, e.Message(), "customer")
}
func (e Error) Detail() interface{} {
return "customer"
}

View File

@ -0,0 +1,44 @@
package encrypt
import "golang.org/x/crypto/bcrypt"
// EncryptPassword 使用 bcrypt 算法对明文密码进行加密。
//
// 参数:
// - password: 明文密码字符串。
//
// 返回值:
// - 加密后的密码哈希string
// - 可能出现的错误error
//
// 示例:
//
// hashed, err := EncryptPassword("mySecret123")
// if err != nil {
// // 处理错误
// }
func EncryptPassword(password string) (string, error) {
// 使用 bcrypt 的默认成本因子10进行加密
hashedPassword, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)
if err != nil {
return "", err
}
return string(hashedPassword), nil
}
// ComparePassword 比较明文密码与加密后的密码哈希是否匹配。
//
// 参数:
// - hashedPassword: 已加密的密码哈希。
// - password: 用户输入的明文密码。
//
// 返回值:
// - 如果匹配返回 true否则返回 false。
//
// 示例:
//
// match := ComparePassword(storedHash, "userInput")
func ComparePassword(hashedPassword, password string) bool {
err := bcrypt.CompareHashAndPassword([]byte(hashedPassword), []byte(password))
return err == nil
}

275
utility/i18n/i18n.go Normal file
View File

@ -0,0 +1,275 @@
package i18n
import (
"context"
"fmt"
"strings"
"github.com/gogf/gf/v2/frame/g"
)
// 支持的语言列表
var SupportedLanguages = []string{"zh-CN", "en-US"}
// 默认语言
const DefaultLanguage = "en-US"
// 语言映射表
var languageMap = map[string]map[string]string{
"zh-CN": {
"hello": "你好,世界",
// 通用消息
"success": "操作成功",
"server_error": "服务器内部错误",
"invalid_operation": "非法的操作请求",
"params_error": "请求参数错误",
"not_login": "用户未登录",
"account_disabled": "账户已被禁用",
"permission_denied": "没有权限执行该操作",
"token_expired": "token已过期",
"not_found": "资源不存在",
"forbidden": "禁止访问",
"unauthorized": "未授权访问",
"unknown_error": "未知错误",
// 用户相关
"auth_failed": "账户名或密码不正确",
"password_incorrect": "密码不正确",
"email_exists": "该邮箱已被注册",
"password_mismatch": "两次密码输入不一致",
"database_query_failed": "数据库查询失败",
"data_conversion_failed": "数据转换失败",
"token_generation_failed": "Token 生成失败",
"password_encryption_failed": "密码加密失败",
"registration_failed": "注册失败",
"user_not_found": "用户不存在或已被禁用",
"email_not_found": "未找到该邮箱注册账户",
// 管理员相关
"admin_not_found": "管理员不存在",
"admin_query_failed": "查询管理员信息失败",
"invalid_token_format": "无效的token格式",
"password_update_failed": "密码更新失败",
"token_parse_failed": "Token解析失败",
"invalid_token": "无效的Token",
// 小说相关
"book_query_failed": "小说查询失败",
"book_exists": "小说已存在",
"book_create_failed": "小说创建失败",
"book_not_found": "小说不存在",
"book_update_failed": "小说更新失败",
"book_delete_failed": "小说删除失败",
"chapter_not_found": "章节不存在",
"insufficient_points": "积分不足",
"chapter_locked": "章节已锁定,需要积分解锁",
// 分类相关
"category_query_failed": "分类查询失败",
"category_exists": "分类已存在",
"category_create_failed": "分类创建失败",
"category_not_found": "分类不存在",
"category_update_failed": "分类更新失败",
"category_delete_failed": "分类删除失败",
"category_type_invalid": "分类类型无效只能为1男频或2女频",
// 标签相关
"tag_query_failed": "标签查询失败",
"tag_exists": "标签已存在",
"tag_create_failed": "标签创建失败",
"tag_not_found": "标签不存在",
"tag_update_failed": "标签更新失败",
"tag_delete_failed": "标签删除失败",
// 章节相关
"chapter_query_failed": "章节查询失败",
"chapter_create_failed": "章节创建失败",
"chapter_update_failed": "章节更新失败",
"chapter_delete_failed": "章节删除失败",
// 反馈相关
"feedback_create_failed": "反馈提交失败",
// 阅读记录相关
"read_record_create_failed": "阅读记录创建失败",
"read_record_query_failed": "阅读记录查询失败",
"read_record_not_found": "阅读记录不存在",
"read_record_delete_failed": "阅读记录删除失败",
// 关注作者相关
"user_follow_author_query_failed": "关注作者查询失败",
"user_follow_author_exists": "已关注该作者",
"user_follow_author_create_failed": "关注作者失败",
"user_follow_author_not_found": "关注记录不存在",
"user_follow_author_delete_failed": "取消关注失败",
},
"en-US": {
"hello": "Hello World!",
// Common messages
"success": "Operation successful",
"server_error": "Internal server error",
"invalid_operation": "Invalid operation request",
"params_error": "Request parameter error",
"not_login": "User not logged in",
"account_disabled": "Account has been disabled",
"permission_denied": "No permission to perform this operation",
"token_expired": "Token has expired",
"not_found": "Resource not found",
"forbidden": "Access forbidden",
"unauthorized": "Unauthorized access",
"unknown_error": "Unknown error",
// User related
"auth_failed": "Incorrect username or password",
"password_incorrect": "Incorrect password",
"email_exists": "This email has already been registered",
"password_mismatch": "Passwords do not match",
"database_query_failed": "Database query failed",
"data_conversion_failed": "Data conversion failed",
"token_generation_failed": "Token generation failed",
"password_encryption_failed": "Password encryption failed",
"registration_failed": "Registration failed",
"user_not_found": "User does not exist or has been disabled",
"email_not_found": "No registered account found for this email",
// Admin related
"admin_not_found": "Administrator not found",
"admin_query_failed": "Failed to query administrator information",
"invalid_token_format": "Invalid token format",
"password_update_failed": "Password update failed",
"token_parse_failed": "Token parsing failed",
"invalid_token": "Invalid token",
// Novel related
"book_query_failed": "Book query failed",
"book_exists": "Book already exists",
"book_create_failed": "Book creation failed",
"book_not_found": "Book not found",
"book_update_failed": "Book update failed",
"book_delete_failed": "Book deletion failed",
"chapter_not_found": "Chapter not found",
"insufficient_points": "Insufficient points",
"chapter_locked": "Chapter is locked, requires points to unlock",
// Category related
"category_query_failed": "Category query failed",
"category_exists": "Category already exists",
"category_create_failed": "Category creation failed",
"category_not_found": "Category not found",
"category_update_failed": "Category update failed",
"category_delete_failed": "Category deletion failed",
"category_type_invalid": "Invalid category type, must be 1 (male) or 2 (female)",
// Tag related
"tag_query_failed": "Tag query failed",
"tag_exists": "Tag already exists",
"tag_create_failed": "Tag creation failed",
"tag_not_found": "Tag not found",
"tag_update_failed": "Tag update failed",
"tag_delete_failed": "Tag deletion failed",
// Chapter related
"chapter_query_failed": "Chapter query failed",
"chapter_create_failed": "Chapter creation failed",
"chapter_update_failed": "Chapter update failed",
"chapter_delete_failed": "Chapter deletion failed",
// Feedback related
"feedback_create_failed": "Feedback creation failed",
// ReadRecord related
"read_record_create_failed": "Read record creation failed",
"read_record_query_failed": "Read record query failed",
"read_record_not_found": "Read record not found",
"read_record_delete_failed": "Read record deletion failed",
// UserFollowAuthor related
"user_follow_author_query_failed": "User follow author query failed",
"user_follow_author_exists": "Already followed this author",
"user_follow_author_create_failed": "User follow author creation failed",
"user_follow_author_not_found": "Follow record not found",
"user_follow_author_delete_failed": "Unfollow failed",
},
}
// GetLanguage 从请求头或查询参数获取语言设置
func GetLanguage(ctx context.Context) string {
// 优先从请求头获取
if r := g.RequestFromCtx(ctx); r != nil {
// 从 Accept-Language 头获取
acceptLang := r.GetHeader("Accept-Language")
if acceptLang != "" {
lang := parseAcceptLanguage(acceptLang)
if isSupportedLanguage(lang) {
return lang
}
}
// 从查询参数获取
lang := r.Get("lang").String()
if isSupportedLanguage(lang) {
return lang
}
// 从请求头获取自定义语言头
lang = r.GetHeader("X-Language")
if isSupportedLanguage(lang) {
return lang
}
}
return DefaultLanguage
}
// T 翻译消息
func T(ctx context.Context, key string) string {
lang := GetLanguage(ctx)
if messages, exists := languageMap[lang]; exists {
if message, exists := messages[key]; exists {
return message
}
}
// 如果当前语言没有找到,尝试默认语言
if lang != DefaultLanguage {
if messages, exists := languageMap[DefaultLanguage]; exists {
if message, exists := messages[key]; exists {
return message
}
}
}
// 如果都没有找到返回key本身
return key
}
// Tf 翻译消息并格式化
func Tf(ctx context.Context, key string, args ...interface{}) string {
message := T(ctx, key)
if len(args) > 0 {
message = fmt.Sprintf(message, args...)
}
return message
}
// isSupportedLanguage 检查是否为支持的语言
func isSupportedLanguage(lang string) bool {
for _, supported := range SupportedLanguages {
if supported == lang {
return true
}
}
return false
}
// parseAcceptLanguage 解析Accept-Language头
func parseAcceptLanguage(acceptLang string) string {
// 简单的解析,取第一个语言代码
parts := strings.Split(acceptLang, ",")
if len(parts) > 0 {
lang := strings.TrimSpace(parts[0])
// 移除质量值
if idx := strings.Index(lang, ";"); idx != -1 {
lang = lang[:idx]
}
return lang
}
return ""
}
// GetSupportedLanguages 获取支持的语言列表
func GetSupportedLanguages() []string {
return SupportedLanguages
}

78
utility/jwt/jwt.go Normal file
View File

@ -0,0 +1,78 @@
package jwt
import (
"errors"
"server/utility/ecode"
"strings"
"time"
"github.com/golang-jwt/jwt/v5"
"github.com/google/uuid"
)
var (
secretKey = []byte("1a40c1d5b7a1b0f0fasdfaf835e0b24ca292a6")
issuer = "novel"
)
type (
TokenIn struct {
UserId int64 // 用户 ID
Role string // 权限标识
}
TokenOut struct {
UserId int64 // 用户 ID
Role string // 权限标识
JTI string // JWT 唯一标识
}
jwtClaims struct {
UserId int64 `json:"user_id"` // 用户 ID
Role string `json:"Role"` // 权限标识
JTI string `json:"jti"` // 唯一标识
jwt.RegisteredClaims
}
)
func GenerateToken(in *TokenIn) (string, error) {
claims := jwtClaims{
UserId: in.UserId,
Role: in.Role,
JTI: uuid.NewString(),
RegisteredClaims: jwt.RegisteredClaims{
IssuedAt: jwt.NewNumericDate(time.Now()),
NotBefore: jwt.NewNumericDate(time.Now()),
Issuer: issuer,
},
}
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
return token.SignedString(secretKey)
}
func ParseToken(tokenString string) (*TokenOut, error) {
if strings.HasPrefix(tokenString, "Bearer ") {
tokenString = strings.TrimPrefix(tokenString, "Bearer ")
}
token, err := jwt.ParseWithClaims(tokenString, &jwtClaims{}, func(token *jwt.Token) (interface{}, error) {
return secretKey, nil
})
if err != nil {
if errors.Is(err, jwt.ErrTokenExpired) {
return nil, ecode.Expire.Sub("token_expired")
}
return nil, ecode.Fail.Sub("token_parse_failed")
}
claims, ok := token.Claims.(*jwtClaims)
if !ok || !token.Valid {
return nil, ecode.InvalidOperation.Sub("invalid_token")
}
return &TokenOut{
UserId: claims.UserId,
Role: claims.Role,
JTI: claims.JTI,
}, nil
}

106
utility/myCasbin/casbin.go Normal file
View File

@ -0,0 +1,106 @@
package myCasbin
import (
"context"
"github.com/casbin/casbin/v2"
"github.com/gogf/gf/v2/frame/g"
"github.com/gogf/gf/v2/os/glog"
"github.com/hailaz/gf-casbin-adapter/v2"
"server/internal/consts"
"sync"
)
type myCasbin struct {
*casbin.Enforcer
}
var (
instance *myCasbin
once sync.Once
)
func init() {
ctx := context.Background()
once.Do(func() {
modelPath := g.Config().MustGet(ctx, "casbin.modelPath").String()
enforcer, err := casbin.NewEnforcer(modelPath, adapter.NewAdapter(
adapter.Options{
GDB: g.DB(),
FieldName: &adapter.FieldName{PType: "p_type"},
},
))
if err != nil {
glog.Errorf(ctx, "init casbin error: %v", err)
}
enforcer.LoadPolicy()
enforcer.AddGroupingPolicy(consts.UserRoleCode, consts.GuestRoleCode)
enforcer.AddGroupingPolicy(consts.AuthorRoleCode, consts.UserRoleCode)
enforcer.AddGroupingPolicy(consts.AdminRoleCode, consts.AuthorRoleCode)
// guest
{
}
// user
{
// book
// chapter
// feedback
// user
enforcer.AddPolicy("admin", "/user/info", "GET", "获取用户信息")
}
// author
{
// book
enforcer.AddPolicy("admin", "/book", "GET", "获取图书列表")
// chapter
// category
enforcer.AddPolicy("admin", "/category", "GET", "获取分类列表")
}
// admin
{
// feedback
enforcer.AddPolicy("admin", "/feedback", "GET", "获取反馈列表")
// category
enforcer.AddPolicy("admin", "/category", "POST", "创建分类")
enforcer.AddPolicy("admin", "/category", "PUT", "更新分类")
enforcer.AddPolicy("admin", "/category", "DELETE", "删除分类")
// admin
enforcer.AddPolicy("admin", "/admin/info", "GET", "获取管理员用户信息")
}
instance = &myCasbin{Enforcer: enforcer}
})
glog.Infof(ctx, "init casbin success")
}
func GetMyCasbin() *myCasbin {
if instance == nil {
panic("casbin not init")
}
return instance
}
// HasPermission 判断给定的权限标识是否拥有访问指定 URL 和方法的权限。
//
// 参数:
// - permission: 权限标识(如角色名或用户 ID
// - url: 请求的路径(如 "/api/user/list"
// - method: HTTP 请求方法(如 "GET", "POST"
//
// 返回:
// - access: 如果有权限则为 true否则为 false。
// - 若校验过程中发生错误,将记录日志并返回 false。
func (m *myCasbin) HasPermission(permission, url, method string) (access bool) {
enforce, err := m.Enforcer.Enforce(permission, url, method)
if err != nil {
glog.Errorf(context.Background(), "enforce error: %v", err)
return
}
return enforce
}

View File

@ -0,0 +1,99 @@
package aliyun
import (
"bytes"
"context"
"fmt"
"github.com/aliyun/aliyun-oss-go-sdk/oss"
"github.com/gogf/gf/v2/frame/g"
"github.com/gogf/gf/v2/os/glog"
"github.com/gogf/gf/v2/util/grand"
"server/internal/model"
ioss "server/utility/oss"
)
type aliyunClient struct {
bucketName string
key string
secret string
endpoint string
}
// 初始化并注册
func init() {
ctx := context.Background()
client := &aliyunClient{
endpoint: g.Config().MustGet(ctx, "oss.aliyun.endpoint").String(),
key: g.Config().MustGet(ctx, "oss.aliyun.key").String(),
secret: g.Config().MustGet(ctx, "oss.aliyun.secret").String(),
bucketName: g.Config().MustGet(ctx, "oss.aliyun.bucket").String(),
}
ioss.Register("aliyun", client)
glog.Infof(ctx, "注册阿里云OSS成功")
}
func (a *aliyunClient) client(ctx context.Context, endpoint, key, sercret string) (*oss.Client, error) {
client, err := oss.New(endpoint, key, sercret)
return client, err
}
func (a *aliyunClient) bucket(ctx context.Context, client *oss.Client) (*oss.Bucket, error) {
bucket, err := client.Bucket(a.bucketName)
return bucket, err
}
func (a *aliyunClient) bytes(ctx context.Context, in *model.OssBytesInput) (*model.OssOutput, error) {
client, err := a.client(ctx, a.endpoint, a.key, a.secret)
if err != nil {
return nil, err
}
bucket, err := a.bucket(ctx, client)
if err != nil {
return nil, err
}
if in.Name == "" {
in.Name = grand.Digits(32)
}
err = bucket.PutObject(in.Name, bytes.NewReader(in.Bytes))
if err != nil {
return nil, err
}
return &model.OssOutput{
Url: fmt.Sprintf("https://%s.%s/%s", a.bucketName, a.endpoint, in.Name),
}, nil
}
func (a *aliyunClient) UploadFile(ctx context.Context, in *model.OssUploadFileInput) (out *model.OssOutput, err error) {
f, err := in.File.Open()
if err != nil {
return nil, err
}
defer f.Close()
body := make([]byte, in.File.Size)
_, err = f.Read(body)
if err != nil {
return nil, err
}
return a.bytes(ctx, &model.OssBytesInput{
Name: in.Filename,
Bytes: body,
})
}
func (a *aliyunClient) GetFileURL(ctx context.Context, in *model.OssGetFileInput) (out *model.OssOutput, err error) {
client, err := a.client(ctx, a.endpoint, a.key, a.secret)
if err != nil {
return nil, err
}
bucket, err := a.bucket(ctx, client)
if err != nil {
return nil, err
}
if in.Name == "" {
in.Name = grand.Digits(32)
}
err = bucket.PutObjectFromFile(in.Name, in.FilePath)
if err != nil {
return nil, err
}
return &model.OssOutput{
Url: fmt.Sprintf("https://%s.%s/%s", a.bucketName, a.endpoint, in.Name),
}, nil
}

26
utility/oss/oss.go Normal file
View File

@ -0,0 +1,26 @@
package oss
import (
"context"
"server/internal/model"
)
// OssClient 是所有云存储平台要实现的统一接口
type OssClient interface {
UploadFile(ctx context.Context, in *model.OssUploadFileInput) (out *model.OssOutput, err error)
GetFileURL(ctx context.Context, in *model.OssGetFileInput) (out *model.OssOutput, err error)
}
// registry 存储各个平台的实现
var clients = make(map[string]OssClient)
// Register 用于注册平台实现
func Register(name string, client OssClient) {
clients[name] = client
}
// GetClient 获取指定平台的实现
func GetClient(name string) (OssClient, bool) {
client, ok := clients[name]
return client, ok
}