书籍列表接口新增参数
This commit is contained in:
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