书籍列表接口新增参数
This commit is contained in:
80
utility/encrypt/aes.go
Normal file
80
utility/encrypt/aes.go
Normal file
@ -0,0 +1,80 @@
|
||||
package encrypt
|
||||
|
||||
import (
|
||||
"crypto/aes"
|
||||
"crypto/cipher"
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"server/internal/model"
|
||||
)
|
||||
|
||||
// 内部常量,外部无法访问
|
||||
const (
|
||||
aesKey = "K8mN2pQ9rS5vX1zA" // 16字节密钥
|
||||
aesIV = "H7jL4oP8qR6uW0yZ" // 16字节初始化向量
|
||||
)
|
||||
|
||||
// DecryptAdsData 解密广告数据并反序列化
|
||||
func DecryptAdsData(encryptedData string) (*model.AdsData, error) {
|
||||
// 解码base64数据
|
||||
ciphertext, err := base64.StdEncoding.DecodeString(encryptedData)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("base64 decode failed: %v", err)
|
||||
}
|
||||
|
||||
// 创建AES cipher
|
||||
block, err := aes.NewCipher([]byte(aesKey))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("aes new cipher failed: %v", err)
|
||||
}
|
||||
|
||||
// 检查数据长度
|
||||
if len(ciphertext) < aes.BlockSize {
|
||||
return nil, fmt.Errorf("ciphertext too short")
|
||||
}
|
||||
|
||||
// 创建CBC模式
|
||||
mode := cipher.NewCBCDecrypter(block, []byte(aesIV))
|
||||
|
||||
// 解密
|
||||
plaintext := make([]byte, len(ciphertext))
|
||||
mode.CryptBlocks(plaintext, ciphertext)
|
||||
|
||||
// 去除PKCS7填充
|
||||
plaintext, err = pkcs7Unpad(plaintext)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("pkcs7 unpad failed: %v", err)
|
||||
}
|
||||
|
||||
// 反序列化JSON
|
||||
var adsData model.AdsData
|
||||
err = json.Unmarshal(plaintext, &adsData)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("json unmarshal failed: %v", err)
|
||||
}
|
||||
|
||||
return &adsData, nil
|
||||
}
|
||||
|
||||
// pkcs7Unpad 去除PKCS7填充
|
||||
func pkcs7Unpad(data []byte) ([]byte, error) {
|
||||
length := len(data)
|
||||
if length == 0 {
|
||||
return nil, fmt.Errorf("invalid padding")
|
||||
}
|
||||
|
||||
padding := int(data[length-1])
|
||||
if padding > length {
|
||||
return nil, fmt.Errorf("invalid padding")
|
||||
}
|
||||
|
||||
// 验证填充
|
||||
for i := length - padding; i < length; i++ {
|
||||
if data[i] != byte(padding) {
|
||||
return nil, fmt.Errorf("invalid padding")
|
||||
}
|
||||
}
|
||||
|
||||
return data[:length-padding], nil
|
||||
}
|
||||
52
utility/encrypt/password_test.go
Normal file
52
utility/encrypt/password_test.go
Normal file
@ -0,0 +1,52 @@
|
||||
package encrypt
|
||||
|
||||
import "testing"
|
||||
|
||||
func TestEncryptPassword(t *testing.T) {
|
||||
type args struct {
|
||||
password string
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
want string
|
||||
wantErr bool
|
||||
}{
|
||||
// TODO: Add test cases.
|
||||
{"case1", args{"Yh243480917"}, "$2a$10$Q6yGw6W./eG0M9uKO1KFUeTs9FEndsPzHL0iTgvf4y/cJ9L3Rnqb.", false},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got, err := EncryptPassword(tt.args.password)
|
||||
if (err != nil) != tt.wantErr {
|
||||
t.Errorf("EncryptPassword() error = %v, wantErr %v", err, tt.wantErr)
|
||||
return
|
||||
}
|
||||
if got != tt.want {
|
||||
t.Errorf("EncryptPassword() got = %v, want %v", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestComparePassword(t *testing.T) {
|
||||
type args struct {
|
||||
hashedPassword string
|
||||
password string
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
want bool
|
||||
}{
|
||||
// TODO: Add test cases.
|
||||
{"case1", args{"$2a$10$lKkwK05oskB.YPnXcGH2pupQxoK.02GDdGxWpstxc1keiWVFekhJ6", "Y"}, true},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
if got := ComparePassword(tt.args.hashedPassword, tt.args.password); got != tt.want {
|
||||
t.Errorf("ComparePassword() = %v, want %v", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@ -4,6 +4,7 @@ import (
|
||||
"context"
|
||||
"fmt"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"github.com/gogf/gf/v2/frame/g"
|
||||
)
|
||||
@ -111,13 +112,20 @@ var languageMap = map[string]map[string]string{
|
||||
"user_follow_author_create_failed": "关注作者失败",
|
||||
"user_follow_author_not_found": "关注记录不存在",
|
||||
"user_follow_author_delete_failed": "取消关注失败",
|
||||
"user_follow_author_update_failed": "关注作者更新失败",
|
||||
// 作者相关
|
||||
"author_query_failed": "作者查询失败",
|
||||
"author_user_exists": "该用户已绑定作者",
|
||||
"author_create_failed": "作者创建失败",
|
||||
"author_not_found": "作者不存在",
|
||||
"author_update_failed": "作者更新失败",
|
||||
"author_delete_failed": "作者删除失败",
|
||||
"author_query_failed": "作者查询失败",
|
||||
"author_user_exists": "该用户已绑定作者",
|
||||
"author_create_failed": "作者创建失败",
|
||||
"author_not_found": "作者不存在",
|
||||
"author_update_failed": "作者更新失败",
|
||||
"author_delete_failed": "作者删除失败",
|
||||
"author_info_failed": "获取作者信息失败",
|
||||
"not_author": "当前用户不是作者",
|
||||
"author_id_required": "作者ID不能为空",
|
||||
"author_review_failed": "作者审核失败",
|
||||
"author_review_status_invalid": "审核状态无效",
|
||||
"author_review_remark_too_long": "审核备注过长",
|
||||
// 书架相关
|
||||
"bookshelve_query_failed": "书架查询失败",
|
||||
"bookshelve_exists": "该书已在书架中",
|
||||
@ -142,6 +150,60 @@ var languageMap = map[string]map[string]string{
|
||||
"bookshelf_update_failed": "书架更新失败",
|
||||
"chapter_count_failed": "章节统计失败",
|
||||
"read_chapter_count_failed": "已读章节统计失败",
|
||||
// 图片上传相关
|
||||
"image_file_required": "图片文件不能为空",
|
||||
"image_type_invalid": "只允许上传图片文件",
|
||||
"image_format_invalid": "仅支持 jpg、png、gif、webp 格式的图片",
|
||||
"image_size_exceeded": "图片大小不能超过1MB",
|
||||
"image_read_failed": "无法读取图片内容",
|
||||
"image_upload_failed": "图片上传失败",
|
||||
// 推荐相关
|
||||
"book_recommendation_query_failed": "推荐查询失败",
|
||||
"book_recommendation_exists": "该类型下该书籍已存在推荐",
|
||||
"book_recommendation_create_failed": "推荐创建失败",
|
||||
"book_recommendation_not_found": "推荐不存在",
|
||||
"book_recommendation_update_failed": "推荐更新失败",
|
||||
"book_recommendation_delete_failed": "推荐删除失败",
|
||||
// 签到奖励规则相关
|
||||
"sign_in_reward_rule_query_failed": "签到奖励规则查询失败",
|
||||
"sign_in_reward_rule_exists": "规则名称已存在",
|
||||
"sign_in_reward_rule_create_failed": "签到奖励规则创建失败",
|
||||
"sign_in_reward_rule_not_found": "签到奖励规则不存在",
|
||||
"sign_in_reward_rule_update_failed": "签到奖励规则更新失败",
|
||||
"sign_in_reward_rule_delete_failed": "签到奖励规则删除失败",
|
||||
// 签到奖励明细相关
|
||||
"sign_in_reward_detail_query_failed": "签到奖励明细查询失败",
|
||||
"sign_in_reward_detail_exists": "该规则下该天奖励已存在",
|
||||
"sign_in_reward_detail_create_failed": "签到奖励明细创建失败",
|
||||
"sign_in_reward_detail_not_found": "签到奖励明细不存在",
|
||||
"sign_in_reward_detail_update_failed": "签到奖励明细更新失败",
|
||||
"sign_in_reward_detail_delete_failed": "签到奖励明细删除失败",
|
||||
// 签到日志相关
|
||||
"user_sign_in_log_query_failed": "签到日志查询失败",
|
||||
"user_sign_in_log_create_failed": "签到日志创建失败",
|
||||
"user_points_log_create_failed": "积分日志创建失败",
|
||||
"user_points_update_failed": "用户积分更新失败",
|
||||
// 用户阅读历史相关
|
||||
"user_read_history_query_failed": "历史记录查询失败",
|
||||
"user_read_history_update_failed": "历史记录更新失败",
|
||||
"user_read_history_create_failed": "历史记录创建失败",
|
||||
"user_read_history_delete_failed": "历史记录删除失败",
|
||||
"user_read_history_not_found": "历史记录不存在",
|
||||
"user_id_or_book_id_or_chapter_id_invalid": "用户ID、书籍ID或章节ID无效",
|
||||
"user_id_or_book_ids_invalid": "用户ID或书籍ID列表无效",
|
||||
// 任务相关
|
||||
"task_query_failed": "任务查询失败",
|
||||
"task_add_failed": "任务添加失败",
|
||||
"task_edit_failed": "任务编辑失败",
|
||||
"task_delete_failed": "任务删除失败",
|
||||
"task_not_found": "任务不存在",
|
||||
"task_log_query_failed": "任务日志查询失败",
|
||||
// 任务类型相关
|
||||
"task_type_query_failed": "任务类型查询失败",
|
||||
"task_type_add_failed": "任务类型添加失败",
|
||||
"task_type_edit_failed": "任务类型编辑失败",
|
||||
"task_type_delete_failed": "任务类型删除失败",
|
||||
"task_type_not_found": "任务类型不存在",
|
||||
},
|
||||
"en-US": {
|
||||
"hello": "Hello World!",
|
||||
@ -239,12 +301,20 @@ var languageMap = map[string]map[string]string{
|
||||
"user_follow_author_not_found": "Follow record not found",
|
||||
"user_follow_author_delete_failed": "Unfollow failed",
|
||||
// Author related
|
||||
"author_query_failed": "Author query failed",
|
||||
"author_user_exists": "User already has an author profile",
|
||||
"author_create_failed": "Author creation failed",
|
||||
"author_not_found": "Author not found",
|
||||
"author_update_failed": "Author update failed",
|
||||
"author_delete_failed": "Author deletion failed",
|
||||
"author_query_failed": "Author query failed",
|
||||
"author_user_exists": "User already has an author profile",
|
||||
"author_create_failed": "Author creation failed",
|
||||
"author_not_found": "Author not found",
|
||||
"author_update_failed": "Author update failed",
|
||||
"author_delete_failed": "Author deletion failed",
|
||||
"author_info_failed": "Failed to get author info",
|
||||
"not_author": "Current user is not an author",
|
||||
"author_id_required": "Author ID cannot be empty",
|
||||
"author_review_failed": "Author review failed",
|
||||
"author_review_status_invalid": "Invalid review status",
|
||||
"author_review_remark_too_long": "Review remark is too long",
|
||||
"user_follow_author_update_failed": "Author update failed",
|
||||
|
||||
// Bookshelve related
|
||||
"bookshelve_query_failed": "Bookshelf query failed",
|
||||
"bookshelve_exists": "Book already in bookshelf",
|
||||
@ -269,11 +339,92 @@ var languageMap = map[string]map[string]string{
|
||||
"bookshelf_update_failed": "Bookshelf update failed",
|
||||
"chapter_count_failed": "Chapter count failed",
|
||||
"read_chapter_count_failed": "Read chapter count failed",
|
||||
// 图片上传相关
|
||||
"image_file_required": "Image file is required",
|
||||
"image_type_invalid": "Only image files are allowed",
|
||||
"image_format_invalid": "Only jpg, png, gif, webp formats are supported",
|
||||
"image_size_exceeded": "Image size cannot exceed 1MB",
|
||||
"image_read_failed": "Failed to read image file",
|
||||
"image_upload_failed": "Image upload failed",
|
||||
// Recommendation related
|
||||
"book_recommendation_query_failed": "Recommendation query failed",
|
||||
"book_recommendation_exists": "The book already exists in this recommendation type",
|
||||
"book_recommendation_create_failed": "Recommendation creation failed",
|
||||
"book_recommendation_not_found": "Recommendation not found",
|
||||
"book_recommendation_update_failed": "Recommendation update failed",
|
||||
"book_recommendation_delete_failed": "Recommendation deletion failed",
|
||||
// 签到奖励规则相关
|
||||
"sign_in_reward_rule_query_failed": "Sign-in reward rule query failed",
|
||||
"sign_in_reward_rule_exists": "Rule name already exists",
|
||||
"sign_in_reward_rule_create_failed": "Sign-in reward rule creation failed",
|
||||
"sign_in_reward_rule_not_found": "Sign-in reward rule not found",
|
||||
"sign_in_reward_rule_update_failed": "Sign-in reward rule update failed",
|
||||
"sign_in_reward_rule_delete_failed": "Sign-in reward rule deletion failed",
|
||||
// Sign-in reward detail related
|
||||
"sign_in_reward_detail_query_failed": "Sign-in reward detail query failed",
|
||||
"sign_in_reward_detail_exists": "Reward for this day already exists under the rule",
|
||||
"sign_in_reward_detail_create_failed": "Sign-in reward detail creation failed",
|
||||
"sign_in_reward_detail_not_found": "Sign-in reward detail not found",
|
||||
"sign_in_reward_detail_update_failed": "Sign-in reward detail update failed",
|
||||
"sign_in_reward_detail_delete_failed": "Sign-in reward detail deletion failed",
|
||||
// Sign-in log related
|
||||
"user_sign_in_log_query_failed": "Sign-in log query failed",
|
||||
"user_sign_in_log_create_failed": "Sign-in log creation failed",
|
||||
"user_points_log_create_failed": "Points log creation failed",
|
||||
"user_points_update_failed": "User points update failed",
|
||||
// User read history related
|
||||
"user_read_history_query_failed": "Read history query failed",
|
||||
"user_read_history_update_failed": "Read history update failed",
|
||||
"user_read_history_create_failed": "Read history creation failed",
|
||||
"user_read_history_delete_failed": "Read history deletion failed",
|
||||
"user_read_history_not_found": "Read history not found",
|
||||
"user_id_or_book_id_or_chapter_id_invalid": "User ID, Book ID or Chapter ID is invalid",
|
||||
"user_id_or_book_ids_invalid": "User ID or Book IDs is invalid",
|
||||
// Task related
|
||||
"task_query_failed": "Task query failed",
|
||||
"task_add_failed": "Task add failed",
|
||||
"task_edit_failed": "Task edit failed",
|
||||
"task_delete_failed": "Task delete failed",
|
||||
"task_not_found": "Task not found",
|
||||
"task_log_query_failed": "Task log query failed",
|
||||
// TaskType related
|
||||
"task_type_query_failed": "Task type query failed",
|
||||
"task_type_add_failed": "Task type add failed",
|
||||
"task_type_edit_failed": "Task type edit failed",
|
||||
"task_type_delete_failed": "Task type delete failed",
|
||||
"task_type_not_found": "Task type not found",
|
||||
},
|
||||
}
|
||||
|
||||
// I18n 单例结构体
|
||||
type I18n struct {
|
||||
languageMap map[string]map[string]string
|
||||
mu sync.RWMutex
|
||||
}
|
||||
|
||||
var (
|
||||
instance *I18n
|
||||
once sync.Once
|
||||
)
|
||||
|
||||
// GetInstance 获取单例实例
|
||||
func GetInstance() *I18n {
|
||||
once.Do(func() {
|
||||
instance = &I18n{
|
||||
languageMap: languageMap,
|
||||
}
|
||||
})
|
||||
return instance
|
||||
}
|
||||
|
||||
// init 初始化函数
|
||||
func init() {
|
||||
// 确保单例实例被创建
|
||||
GetInstance()
|
||||
}
|
||||
|
||||
// GetLanguage 从请求头或查询参数获取语言设置
|
||||
func GetLanguage(ctx context.Context) string {
|
||||
func (i *I18n) GetLanguage(ctx context.Context) string {
|
||||
// 优先从请求头获取
|
||||
if r := g.RequestFromCtx(ctx); r != nil {
|
||||
// 从 Accept-Language 头获取
|
||||
@ -302,9 +453,12 @@ func GetLanguage(ctx context.Context) string {
|
||||
}
|
||||
|
||||
// T 翻译消息
|
||||
func T(ctx context.Context, key string) string {
|
||||
lang := GetLanguage(ctx)
|
||||
if messages, exists := languageMap[lang]; exists {
|
||||
func (i *I18n) T(ctx context.Context, key string) string {
|
||||
lang := i.GetLanguage(ctx)
|
||||
i.mu.RLock()
|
||||
defer i.mu.RUnlock()
|
||||
|
||||
if messages, exists := i.languageMap[lang]; exists {
|
||||
if message, exists := messages[key]; exists {
|
||||
return message
|
||||
}
|
||||
@ -312,7 +466,7 @@ func T(ctx context.Context, key string) string {
|
||||
|
||||
// 如果当前语言没有找到,尝试默认语言
|
||||
if lang != DefaultLanguage {
|
||||
if messages, exists := languageMap[DefaultLanguage]; exists {
|
||||
if messages, exists := i.languageMap[DefaultLanguage]; exists {
|
||||
if message, exists := messages[key]; exists {
|
||||
return message
|
||||
}
|
||||
@ -324,14 +478,27 @@ func T(ctx context.Context, key string) string {
|
||||
}
|
||||
|
||||
// Tf 翻译消息并格式化
|
||||
func Tf(ctx context.Context, key string, args ...interface{}) string {
|
||||
message := T(ctx, key)
|
||||
func (i *I18n) Tf(ctx context.Context, key string, args ...interface{}) string {
|
||||
message := i.T(ctx, key)
|
||||
if len(args) > 0 {
|
||||
message = fmt.Sprintf(message, args...)
|
||||
}
|
||||
return message
|
||||
}
|
||||
|
||||
// 为了保持向后兼容,提供全局函数
|
||||
func GetLanguage(ctx context.Context) string {
|
||||
return GetInstance().GetLanguage(ctx)
|
||||
}
|
||||
|
||||
func T(ctx context.Context, key string) string {
|
||||
return GetInstance().T(ctx, key)
|
||||
}
|
||||
|
||||
func Tf(ctx context.Context, key string, args ...interface{}) string {
|
||||
return GetInstance().Tf(ctx, key, args...)
|
||||
}
|
||||
|
||||
// isSupportedLanguage 检查是否为支持的语言
|
||||
func isSupportedLanguage(lang string) bool {
|
||||
for _, supported := range SupportedLanguages {
|
||||
|
||||
@ -1,7 +1,9 @@
|
||||
package jwt
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"github.com/gogf/gf/v2/frame/g"
|
||||
"server/utility/ecode"
|
||||
"strings"
|
||||
"time"
|
||||
@ -70,9 +72,23 @@ func ParseToken(tokenString string) (*TokenOut, error) {
|
||||
return nil, ecode.InvalidOperation.Sub("invalid_token")
|
||||
}
|
||||
|
||||
blacklist, err := isBlacklist(claims.JTI)
|
||||
if err != nil {
|
||||
return nil, ecode.Fail.Sub("token_parse_failed")
|
||||
}
|
||||
if blacklist {
|
||||
return nil, ecode.InvalidOperation.Sub("token_expired")
|
||||
}
|
||||
return &TokenOut{
|
||||
UserId: claims.UserId,
|
||||
Role: claims.Role,
|
||||
JTI: claims.JTI,
|
||||
}, nil
|
||||
}
|
||||
func isBlacklist(uuid string) (bool, error) {
|
||||
exitst, err := g.Redis().Exists(context.Background(), "blacklist:"+uuid)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
return exitst > 0, nil
|
||||
}
|
||||
|
||||
@ -46,8 +46,11 @@ func init() {
|
||||
enforcer.AddPolicy("guest", "/chapter/app/list", "GET", "App获取章节列表")
|
||||
enforcer.AddPolicy("guest", "/chapter/app/detail", "GET", "App获取章节详情")
|
||||
enforcer.AddPolicy("guest", "/category", "GET", "获取分类列表")
|
||||
enforcer.AddPolicy("guest", "/recommend/app/list", "GET", "App获取推荐列表")
|
||||
enforcer.AddPolicy("guest", "/activity/sign", "GET", "用户签到任务列表")
|
||||
enforcer.AddPolicy("guest", "/task/appList", "GET", "App端任务列表")
|
||||
enforcer.AddPolicy("guest", "/system/version", "GET", "获取系统版本信息")
|
||||
}
|
||||
|
||||
// user
|
||||
{
|
||||
// book
|
||||
@ -67,6 +70,9 @@ func init() {
|
||||
// author follow/unfollow
|
||||
enforcer.AddPolicy("user", "/author/follow", "POST", "关注作者")
|
||||
enforcer.AddPolicy("user", "/author/unfollow", "POST", "取消关注作者")
|
||||
enforcer.AddPolicy("user", "/author/detail", "GET", "获取作者详情")
|
||||
enforcer.AddPolicy("user", "/activity/sign", "POST", "用户签到")
|
||||
enforcer.AddPolicy("user", "/author/apply", "POST", "申请成为作者")
|
||||
}
|
||||
// author
|
||||
{
|
||||
@ -75,17 +81,22 @@ func init() {
|
||||
enforcer.AddPolicy("author", "/book", "POST", "新增图书")
|
||||
enforcer.AddPolicy("author", "/book", "PUT", "编辑图书")
|
||||
enforcer.AddPolicy("author", "/book", "DELETE", "删除图书")
|
||||
enforcer.AddPolicy("author", "/book/coverImage", "POST", "上传图书封面图")
|
||||
|
||||
// chapter
|
||||
enforcer.AddPolicy("author", "/chapter", "GET", "获取章节列表")
|
||||
enforcer.AddPolicy("author", "/chapter", "POST", "创建章节")
|
||||
enforcer.AddPolicy("author", "/chapter", "PUT", "更新章节")
|
||||
enforcer.AddPolicy("author", "/chapter", "DELETE", "删除章节")
|
||||
|
||||
enforcer.AddPolicy("author", "/author/info", "GET", "获取作者基础信息")
|
||||
}
|
||||
// admin
|
||||
{
|
||||
// book
|
||||
enforcer.AddPolicy("admin", "/book/set-featured", "POST", "设置书籍精选状态")
|
||||
enforcer.AddPolicy("admin", "/book/set-recommended", "POST", "设置书籍推荐状态")
|
||||
enforcer.AddPolicy("admin", "/book/set-hot", "POST", "设置书籍最热状态")
|
||||
// author
|
||||
enforcer.AddPolicy("admin", "/author", "GET", "获取作者列表")
|
||||
enforcer.AddPolicy("admin", "/author", "POST", "创建作者")
|
||||
@ -100,6 +111,37 @@ func init() {
|
||||
// admin
|
||||
enforcer.AddPolicy("admin", "/admin/info", "GET", "获取管理员用户信息")
|
||||
enforcer.AddPolicy("admin", "/admin/editPass", "POST", "管理员修改密码")
|
||||
enforcer.AddPolicy("admin", "/author/review", "POST", "审核作者申请")
|
||||
enforcer.AddPolicy("admin", "/task", "GET", "获取任务列表")
|
||||
enforcer.AddPolicy("admin", "/task", "POST", "新增任务")
|
||||
enforcer.AddPolicy("admin", "/task", "PUT", "编辑任务")
|
||||
enforcer.AddPolicy("admin", "/task", "DELETE", "删除任务")
|
||||
enforcer.AddPolicy("admin", "/system/save", "POST", "获取系统版本信息")
|
||||
|
||||
}
|
||||
// recommend
|
||||
{
|
||||
enforcer.AddPolicy("admin", "/recommend", "GET", "获取推荐列表")
|
||||
enforcer.AddPolicy("admin", "/recommend", "POST", "新增推荐")
|
||||
enforcer.AddPolicy("admin", "/recommend", "PUT", "编辑推荐")
|
||||
enforcer.AddPolicy("admin", "/recommend", "DELETE", "删除推荐")
|
||||
enforcer.AddPolicy("admin", "/recommend/set-status", "POST", "设置推荐状态")
|
||||
enforcer.AddPolicy("admin", "/recommend/sort-order", "POST", "设置推荐排序")
|
||||
enforcer.AddPolicy("admin", "/recommend/info", "GET", "获取推荐详情")
|
||||
}
|
||||
// activity 签到奖励规则相关接口
|
||||
{
|
||||
enforcer.AddPolicy("admin", "/activity", "GET", "获取签到奖励规则全信息")
|
||||
enforcer.AddPolicy("admin", "/activity", "POST", "新增签到奖励规则全信息")
|
||||
enforcer.AddPolicy("admin", "/activity/*", "PUT", "编辑签到奖励规则全信息")
|
||||
enforcer.AddPolicy("admin", "/activity/*", "DELETE", "删除签到奖励规则全信息")
|
||||
enforcer.AddPolicy("admin", "/activity/*/status", "PATCH", "设置签到奖励规则状态全信息")
|
||||
enforcer.AddPolicy("admin", "/activity/items", "GET", "获取签到奖励明细列表")
|
||||
enforcer.AddPolicy("admin", "/activity/item", "POST", "新增签到奖励明细")
|
||||
enforcer.AddPolicy("admin", "/activity/item/*", "PUT", "编辑签到奖励明细")
|
||||
enforcer.AddPolicy("admin", "/activity/item/*", "DELETE", "删除签到奖励明细")
|
||||
enforcer.AddPolicy("admin", "/activity/item/*", "GET", "获取单个签到奖励明细")
|
||||
enforcer.AddPolicy("admin", "/activity/item/*/status", "PATCH", "设置签到奖励明细状态")
|
||||
}
|
||||
instance = &myCasbin{Enforcer: enforcer}
|
||||
|
||||
|
||||
@ -2,6 +2,9 @@ package amazons3
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"github.com/gogf/gf/v2/frame/g"
|
||||
"github.com/gogf/gf/v2/net/ghttp"
|
||||
"server/utility/oss"
|
||||
|
||||
"github.com/aws/aws-sdk-go/aws"
|
||||
@ -9,65 +12,42 @@ import (
|
||||
"github.com/aws/aws-sdk-go/aws/session"
|
||||
"github.com/aws/aws-sdk-go/service/s3/s3manager"
|
||||
"github.com/gogf/gf/crypto/gmd5"
|
||||
"github.com/gogf/gf/errors/gerror"
|
||||
"github.com/gogf/gf/frame/g"
|
||||
"github.com/gogf/gf/net/ghttp"
|
||||
"github.com/gogf/gf/os/gfile"
|
||||
"github.com/gogf/gf/os/glog"
|
||||
"github.com/gogf/gf/os/gtime"
|
||||
"github.com/gogf/gf/util/grand"
|
||||
)
|
||||
|
||||
type AmazonS3Client struct{}
|
||||
|
||||
func (c *AmazonS3Client) initSess() (*session.Session, error) {
|
||||
key := g.Config().GetString("aws.s3.key")
|
||||
secret := g.Config().GetString("aws.s3.secret")
|
||||
region := g.Config().GetString("aws.s3.region")
|
||||
if len(key) == 0 || len(secret) == 0 {
|
||||
return nil, gerror.New("aws s3 配置错误")
|
||||
}
|
||||
config := &aws.Config{
|
||||
Region: aws.String(region),
|
||||
Credentials: credentials.NewStaticCredentials(key, secret, ""),
|
||||
}
|
||||
sess := session.Must(session.NewSession(config))
|
||||
return sess, nil
|
||||
type AmazonS3Client struct {
|
||||
key string
|
||||
secret string
|
||||
region string
|
||||
bucket string
|
||||
sess *session.Session
|
||||
}
|
||||
|
||||
func (c *AmazonS3Client) Upload(file interface{}) (string, error) {
|
||||
uploadFile, ok := file.(*ghttp.UploadFile)
|
||||
if !ok {
|
||||
return "", gerror.New("参数类型错误")
|
||||
}
|
||||
bucket := g.Config().GetString("aws.s3.bucket")
|
||||
if len(bucket) == 0 {
|
||||
glog.Warning("bucket为空:", bucket)
|
||||
return "", gerror.New("aws s3 配置错误")
|
||||
}
|
||||
sess, err := c.initSess()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
uploader := s3manager.NewUploader(sess)
|
||||
f, err := uploadFile.Open()
|
||||
var amazonS3Client *AmazonS3Client
|
||||
|
||||
func (c *AmazonS3Client) Upload(file *ghttp.UploadFile, folder string) (string, error) {
|
||||
uploader := s3manager.NewUploader(c.sess)
|
||||
f, err := file.Open()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
defer f.Close()
|
||||
body := make([]byte, uploadFile.Size)
|
||||
body := make([]byte, file.Size)
|
||||
_, err = f.Read(body)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
key, err := gmd5.Encrypt(uploadFile.Filename + gtime.Datetime() + grand.Digits(6))
|
||||
ext := gfile.Ext(uploadFile.Filename)
|
||||
key, err := gmd5.Encrypt(file.Filename + gtime.Datetime() + grand.Digits(6))
|
||||
ext := gfile.Ext(file.Filename)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
r, err := uploader.Upload(&s3manager.UploadInput{
|
||||
Bucket: aws.String(bucket),
|
||||
Key: aws.String(key + ext),
|
||||
Bucket: aws.String(c.bucket),
|
||||
Key: aws.String(folder + "/" + key + ext),
|
||||
Body: bytes.NewReader(body),
|
||||
})
|
||||
if err != nil {
|
||||
@ -77,16 +57,7 @@ func (c *AmazonS3Client) Upload(file interface{}) (string, error) {
|
||||
}
|
||||
|
||||
func (c *AmazonS3Client) UploadLocalFile(path, name string) (string, error) {
|
||||
bucket := g.Config().GetString("aws.s3.bucket")
|
||||
if len(bucket) == 0 {
|
||||
glog.Warning("bucket为空:", bucket)
|
||||
return "", gerror.New("aws s3 配置错误")
|
||||
}
|
||||
sess, err := c.initSess()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
uploader := s3manager.NewUploader(sess)
|
||||
uploader := s3manager.NewUploader(c.sess)
|
||||
f, err := gfile.Open(path)
|
||||
if err != nil {
|
||||
return "", err
|
||||
@ -98,7 +69,7 @@ func (c *AmazonS3Client) UploadLocalFile(path, name string) (string, error) {
|
||||
return "", err
|
||||
}
|
||||
r, err := uploader.Upload(&s3manager.UploadInput{
|
||||
Bucket: aws.String(bucket),
|
||||
Bucket: aws.String(c.bucket),
|
||||
Key: aws.String(name),
|
||||
Body: bytes.NewReader(body),
|
||||
})
|
||||
@ -110,5 +81,26 @@ func (c *AmazonS3Client) UploadLocalFile(path, name string) (string, error) {
|
||||
}
|
||||
|
||||
func init() {
|
||||
oss.RegisterClient("amazon_s3", &AmazonS3Client{})
|
||||
ctx := context.Background()
|
||||
key := g.Config().MustGet(ctx, "oss.s3.key").String()
|
||||
secret := g.Config().MustGet(ctx, "oss.s3.secret").String()
|
||||
region := g.Config().MustGet(ctx, "oss.s3.region").String()
|
||||
bucket := g.Config().MustGet(ctx, "oss.s3.bucket").String()
|
||||
|
||||
if key == "" || secret == "" || region == "" || bucket == "" {
|
||||
glog.Error("请配置 OSS 配置")
|
||||
}
|
||||
config := &aws.Config{
|
||||
Region: aws.String(region),
|
||||
Credentials: credentials.NewStaticCredentials(key, secret, ""),
|
||||
}
|
||||
sess := session.Must(session.NewSession(config))
|
||||
amazonS3Client := AmazonS3Client{
|
||||
bucket: bucket,
|
||||
key: key,
|
||||
region: region,
|
||||
secret: secret,
|
||||
sess: sess,
|
||||
}
|
||||
oss.RegisterClient("amazon_s3", &amazonS3Client)
|
||||
}
|
||||
|
||||
@ -1,7 +1,9 @@
|
||||
package oss
|
||||
|
||||
import "github.com/gogf/gf/v2/net/ghttp"
|
||||
|
||||
type OSSClient interface {
|
||||
Upload(file interface{}) (string, error)
|
||||
Upload(file *ghttp.UploadFile, folder string) (string, error)
|
||||
UploadLocalFile(path, name string) (string, error)
|
||||
}
|
||||
|
||||
|
||||
115
utility/state_machine/ads_state_machine.go
Normal file
115
utility/state_machine/ads_state_machine.go
Normal file
@ -0,0 +1,115 @@
|
||||
package state_machine
|
||||
|
||||
import (
|
||||
"context"
|
||||
"sync"
|
||||
|
||||
"github.com/gogf/gf/v2/os/glog"
|
||||
|
||||
"server/internal/consts"
|
||||
"server/utility/ecode"
|
||||
)
|
||||
|
||||
// AdStateMachine 广告状态机
|
||||
type AdStateMachine struct {
|
||||
mu sync.RWMutex // 读写锁,保证并发安全
|
||||
// 状态流转规则
|
||||
transitions map[consts.AdState][]consts.AdState
|
||||
// 终止状态
|
||||
terminalStates map[consts.AdState]bool
|
||||
}
|
||||
|
||||
// NewAdStateMachine 创建新的广告状态机
|
||||
func NewAdStateMachine() *AdStateMachine {
|
||||
sm := &AdStateMachine{
|
||||
transitions: make(map[consts.AdState][]consts.AdState),
|
||||
terminalStates: make(map[consts.AdState]bool),
|
||||
}
|
||||
|
||||
// 初始化状态流转规则
|
||||
sm.initTransitions()
|
||||
return sm
|
||||
}
|
||||
|
||||
// initTransitions 初始化状态流转规则
|
||||
func (sm *AdStateMachine) initTransitions() {
|
||||
// 定义状态流转规则
|
||||
sm.transitions = map[consts.AdState][]consts.AdState{
|
||||
consts.StateFetchSuccess: {consts.StateDisplayFailed, consts.StateDisplaySuccess},
|
||||
consts.StateDisplaySuccess: {consts.StateNotWatched, consts.StateWatched},
|
||||
consts.StateWatched: {consts.StateNotClicked, consts.StateClicked},
|
||||
consts.StateClicked: {consts.StateNotDownloaded, consts.StateDownloaded},
|
||||
consts.StateNotDownloaded: {consts.StateDownloaded},
|
||||
}
|
||||
|
||||
// 定义终止状态
|
||||
sm.terminalStates = map[consts.AdState]bool{
|
||||
consts.StateFetchFailed: true,
|
||||
consts.StateDisplayFailed: true,
|
||||
consts.StateNotWatched: true,
|
||||
consts.StateNotClicked: true,
|
||||
consts.StateDownloaded: true,
|
||||
}
|
||||
}
|
||||
|
||||
// CanTransition 检查是否可以转换到目标状态
|
||||
func (sm *AdStateMachine) CanTransition(fromState, toState consts.AdState) bool {
|
||||
sm.mu.RLock()
|
||||
defer sm.mu.RUnlock()
|
||||
|
||||
// 检查是否为终止状态
|
||||
if sm.terminalStates[fromState] {
|
||||
return false
|
||||
}
|
||||
|
||||
// 检查允许的转换
|
||||
allowedStates, exists := sm.transitions[fromState]
|
||||
if !exists {
|
||||
return false
|
||||
}
|
||||
|
||||
for _, allowed := range allowedStates {
|
||||
if allowed == toState {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
// Transition 执行状态转换
|
||||
func (sm *AdStateMachine) Transition(ctx context.Context, flowID string, userID int64, fromState, toState consts.AdState, reason string) error {
|
||||
sm.mu.Lock()
|
||||
defer sm.mu.Unlock()
|
||||
|
||||
// 验证状态转换
|
||||
if !sm.CanTransition(fromState, toState) {
|
||||
glog.Warningf(ctx, "Invalid state transition: %s -> %s, FlowID: %s, UserID: %d, Reason: %s",
|
||||
consts.GetStateDescription(fromState), consts.GetStateDescription(toState), flowID, userID, reason)
|
||||
return ecode.Params.Sub("invalid_state_transition")
|
||||
}
|
||||
|
||||
// 记录状态转换日志
|
||||
glog.Infof(ctx, "State transition: %s -> %s, FlowID: %s, UserID: %d, Reason: %s",
|
||||
consts.GetStateDescription(fromState), consts.GetStateDescription(toState), flowID, userID, reason)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// IsTerminalState 检查是否为终止状态
|
||||
func (sm *AdStateMachine) IsTerminalState(state consts.AdState) bool {
|
||||
sm.mu.RLock()
|
||||
defer sm.mu.RUnlock()
|
||||
return sm.terminalStates[state]
|
||||
}
|
||||
|
||||
// GetAvailableTransitions 获取当前状态可用的转换
|
||||
func (sm *AdStateMachine) GetAvailableTransitions(currentState consts.AdState) []consts.AdState {
|
||||
sm.mu.RLock()
|
||||
defer sm.mu.RUnlock()
|
||||
|
||||
if transitions, exists := sm.transitions[currentState]; exists {
|
||||
return transitions
|
||||
}
|
||||
return []consts.AdState{}
|
||||
}
|
||||
190
utility/state_machine/ads_state_machine_test.go
Normal file
190
utility/state_machine/ads_state_machine_test.go
Normal file
@ -0,0 +1,190 @@
|
||||
// Package state_machine 提供广告状态机的单元测试
|
||||
package state_machine
|
||||
|
||||
import (
|
||||
"server/internal/consts"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// TestNewAdStateMachine 测试创建新的广告状态机
|
||||
func TestNewAdStateMachine(t *testing.T) {
|
||||
sm := NewAdStateMachine()
|
||||
if sm == nil {
|
||||
t.Fatal("期望创建非空的状态机,但得到了nil")
|
||||
}
|
||||
if sm.transitions == nil {
|
||||
t.Error("期望transitions非空,但得到了nil")
|
||||
}
|
||||
if sm.terminalStates == nil {
|
||||
t.Error("期望terminalStates非空,但得到了nil")
|
||||
}
|
||||
}
|
||||
|
||||
// TestAdStateMachine_CanTransition 测试状态转换验证功能
|
||||
func TestAdStateMachine_CanTransition(t *testing.T) {
|
||||
sm := NewAdStateMachine()
|
||||
|
||||
// 测试有效转换
|
||||
if !sm.CanTransition(consts.StateFetchSuccess, consts.StateDisplaySuccess) {
|
||||
t.Error("期望从拉取成功到显示成功的转换是有效的")
|
||||
}
|
||||
|
||||
// 测试无效转换
|
||||
if sm.CanTransition(consts.StateFetchSuccess, consts.StateNotWatched) {
|
||||
t.Error("期望从拉取成功到未观看完成的转换是无效的")
|
||||
}
|
||||
|
||||
// 测试从终止状态转换
|
||||
if sm.CanTransition(consts.StateFetchFailed, consts.StateDisplaySuccess) {
|
||||
t.Error("期望从拉取失败终止状态无法转换到其他状态")
|
||||
}
|
||||
}
|
||||
|
||||
// TestAdStateMachine_IsTerminalState 测试终止状态检查功能
|
||||
func TestAdStateMachine_IsTerminalState(t *testing.T) {
|
||||
sm := NewAdStateMachine()
|
||||
|
||||
// 测试终止状态
|
||||
if !sm.IsTerminalState(consts.StateDisplayFailed) {
|
||||
t.Error("期望显示失败状态是终止状态")
|
||||
}
|
||||
|
||||
// 测试非终止状态
|
||||
if sm.IsTerminalState(consts.StateFetchSuccess) {
|
||||
t.Error("期望拉取成功状态不是终止状态")
|
||||
}
|
||||
}
|
||||
|
||||
// TestAdStateMachine_GetAvailableTransitions 测试获取可用转换状态功能
|
||||
func TestAdStateMachine_GetAvailableTransitions(t *testing.T) {
|
||||
sm := NewAdStateMachine()
|
||||
|
||||
// 测试有可用转换的状态
|
||||
transitions := sm.GetAvailableTransitions(consts.StateFetchSuccess)
|
||||
if len(transitions) == 0 {
|
||||
t.Error("期望拉取成功状态有可用的转换状态")
|
||||
}
|
||||
|
||||
// 测试终止状态
|
||||
transitions = sm.GetAvailableTransitions(consts.StateDisplayFailed)
|
||||
if len(transitions) != 0 {
|
||||
t.Error("期望显示失败终止状态没有可用的转换状态")
|
||||
}
|
||||
|
||||
// 测试不存在的状态
|
||||
transitions = sm.GetAvailableTransitions(999) // 不存在的状态
|
||||
if len(transitions) != 0 {
|
||||
t.Error("期望不存在的状态没有可用的转换状态")
|
||||
}
|
||||
}
|
||||
|
||||
// TestAdStateMachine_StateTransitionFlow 测试状态流转路径
|
||||
func TestAdStateMachine_StateTransitionFlow(t *testing.T) {
|
||||
sm := NewAdStateMachine()
|
||||
|
||||
// 测试路径1:拉取失败是终止状态
|
||||
if !sm.IsTerminalState(consts.StateFetchFailed) {
|
||||
t.Error("期望拉取失败是终止状态")
|
||||
}
|
||||
|
||||
// 测试路径2:拉取成功 -> 显示失败 -> 终止
|
||||
if !sm.CanTransition(consts.StateFetchSuccess, consts.StateDisplayFailed) {
|
||||
t.Error("期望从拉取成功到显示失败的转换是有效的")
|
||||
}
|
||||
if !sm.IsTerminalState(consts.StateDisplayFailed) {
|
||||
t.Error("期望显示失败是终止状态")
|
||||
}
|
||||
|
||||
// 测试路径3:拉取成功 -> 显示成功 -> 未观看完成 -> 终止
|
||||
if !sm.CanTransition(consts.StateFetchSuccess, consts.StateDisplaySuccess) {
|
||||
t.Error("期望从拉取成功到显示成功的转换是有效的")
|
||||
}
|
||||
if !sm.CanTransition(consts.StateDisplaySuccess, consts.StateNotWatched) {
|
||||
t.Error("期望从显示成功到未观看完成的转换是有效的")
|
||||
}
|
||||
if !sm.IsTerminalState(consts.StateNotWatched) {
|
||||
t.Error("期望未观看完成是终止状态")
|
||||
}
|
||||
|
||||
// 测试路径4:拉取成功 -> 显示成功 -> 观看完成 -> 未点击 -> 终止
|
||||
if !sm.CanTransition(consts.StateDisplaySuccess, consts.StateWatched) {
|
||||
t.Error("期望从显示成功到观看完成的转换是有效的")
|
||||
}
|
||||
if !sm.CanTransition(consts.StateWatched, consts.StateNotClicked) {
|
||||
t.Error("期望从观看完成到未点击的转换是有效的")
|
||||
}
|
||||
if !sm.IsTerminalState(consts.StateNotClicked) {
|
||||
t.Error("期望未点击是终止状态")
|
||||
}
|
||||
|
||||
// 测试路径5:拉取成功 -> 显示成功 -> 观看完成 -> 已点击 -> 未下载 -> 已下载
|
||||
if !sm.CanTransition(consts.StateWatched, consts.StateClicked) {
|
||||
t.Error("期望从观看完成到已点击的转换是有效的")
|
||||
}
|
||||
if !sm.CanTransition(consts.StateClicked, consts.StateNotDownloaded) {
|
||||
t.Error("期望从已点击到未下载的转换是有效的")
|
||||
}
|
||||
if sm.IsTerminalState(consts.StateNotDownloaded) {
|
||||
t.Error("期望未下载不是终止状态")
|
||||
}
|
||||
if !sm.CanTransition(consts.StateNotDownloaded, consts.StateDownloaded) {
|
||||
t.Error("期望从未下载到已下载的转换是有效的")
|
||||
}
|
||||
|
||||
// 测试路径6:拉取成功 -> 显示成功 -> 观看完成 -> 已点击 -> 已下载 -> 终止
|
||||
if !sm.CanTransition(consts.StateClicked, consts.StateDownloaded) {
|
||||
t.Error("期望从已点击到已下载的转换是有效的")
|
||||
}
|
||||
if !sm.IsTerminalState(consts.StateDownloaded) {
|
||||
t.Error("期望已下载是终止状态")
|
||||
}
|
||||
}
|
||||
|
||||
// TestAdStateMachine_InvalidTransitions 测试无效的状态转换
|
||||
func TestAdStateMachine_InvalidTransitions(t *testing.T) {
|
||||
sm := NewAdStateMachine()
|
||||
|
||||
// 测试不允许的跳跃转换
|
||||
if sm.CanTransition(consts.StateFetchSuccess, consts.StateWatched) {
|
||||
t.Error("不应该允许从拉取成功直接转换到观看完成")
|
||||
}
|
||||
if sm.CanTransition(consts.StateDisplaySuccess, consts.StateClicked) {
|
||||
t.Error("不应该允许从显示成功直接转换到已点击")
|
||||
}
|
||||
if sm.CanTransition(consts.StateWatched, consts.StateDownloaded) {
|
||||
t.Error("不应该允许从观看完成直接转换到已下载")
|
||||
}
|
||||
|
||||
// 测试反向转换
|
||||
if sm.CanTransition(consts.StateDisplaySuccess, consts.StateFetchSuccess) {
|
||||
t.Error("不应该允许从显示成功转换回拉取成功")
|
||||
}
|
||||
if sm.CanTransition(consts.StateWatched, consts.StateDisplaySuccess) {
|
||||
t.Error("不应该允许从观看完成转换回显示成功")
|
||||
}
|
||||
if sm.CanTransition(consts.StateClicked, consts.StateWatched) {
|
||||
t.Error("不应该允许从已点击转换回观看完成")
|
||||
}
|
||||
if sm.CanTransition(consts.StateDownloaded, consts.StateClicked) {
|
||||
t.Error("不应该允许从已下载转换回已点击")
|
||||
}
|
||||
}
|
||||
|
||||
// TestAdStateMachine_TransitionFromNotDownloaded 测试从未下载状态的转换
|
||||
func TestAdStateMachine_TransitionFromNotDownloaded(t *testing.T) {
|
||||
sm := NewAdStateMachine()
|
||||
|
||||
// 测试从未下载到已下载的转换
|
||||
if !sm.CanTransition(consts.StateNotDownloaded, consts.StateDownloaded) {
|
||||
t.Error("期望从未下载到已下载的转换是有效的")
|
||||
}
|
||||
|
||||
// 确认未下载状态的可用转换只有已下载
|
||||
transitions := sm.GetAvailableTransitions(consts.StateNotDownloaded)
|
||||
if len(transitions) != 1 {
|
||||
t.Errorf("期望未下载状态有1个可用转换,但得到了%d个", len(transitions))
|
||||
}
|
||||
if len(transitions) > 0 && transitions[0] != consts.StateDownloaded {
|
||||
t.Errorf("期望未下载状态的可用转换是已下载,但得到了%d", transitions[0])
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user