书籍列表接口新增参数

This commit is contained in:
2025-08-13 15:19:42 +08:00
parent 6ccc87f2bf
commit 8afe651c64
201 changed files with 6987 additions and 1066 deletions

80
utility/encrypt/aes.go Normal file
View 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
}

View 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)
}
})
}
}

View File

@ -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 {

View File

@ -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
}

View File

@ -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}

View File

@ -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)
}

View File

@ -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)
}

View 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{}
}

View 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])
}
}