24.有限状态机(三)表驱动法Go实现


代码地址: https://gitee.com/lymgoforIT/golang-trick/tree/master/17-fsm-go

一:需求描述

假设我们有要实现一个订单下单功能,下图是订单状态的流转图,方框为订单的状态,箭头旁的文字为事件。

在这里插入图片描述

二:前置结构定义(DB,订单,FSM)

本文所有代码目录结构如下
在这里插入图片描述

2.1 数据库与订单

首先应该初始化DB,实际工作中一般在实例启动的时候便会在main函数中初始化DB,这里为了简化,进行了模拟。模拟数据库事务、查询所有订单、更新订单状态等。

package database

import "fmt"

// DB 模拟数据库对象 实际工作中一般会用gorm框架
type DB struct {
}

// Transaction 模拟事务
func (db *DB) Transaction(f func() error) error {
	fmt.Println("事务执行开始")
	defer func() {
		if err := recover(); err != any(nil) {
			fmt.Println("事务回滚")
		}
	}()
	err := f()
	if err != nil {
		return err
	}
	fmt.Println("事务提交")
	return nil
}

// Order 订单 为了简化,省去了很多不必要的业务字段type,实际应该放到model包中
type Order struct {
	ID    int64 // 主键id
	State int   // 订单状态
}

type OrderList []*Order

// ListAllOrder 查询所有订单 实际应该放dao包中
func ListAllOrder() (OrderList, error) {
	orderList := OrderList{ // 模拟从DB查出了三条订单数据,用于后面的单测
		{1, 0},
		{2, 1},
		{2, 2},
	}
	return orderList, nil
}

// UpdateOrderState 更新订单状态
func UpdateOrderState(curOrder *Order, srcState, dstState int) error {
	// 如果要更新的订单状态和给定的现态一致,说明可以更新
	// 实际工作中,这里可能还需要做很多前置校验,如curOrder的状态已经是dstState状态了,应该提示状态已经是dstState了,无需更新
	if curOrder.State == srcState {
		curOrder.State = dstState
	}
	fmt.Printf("更新id为 %v 的订单状态,从现态[%v]到次态[%v]\n", curOrder.ID, srcState, dstState)
	return nil
}

2.2 FSM结构定义

状态机包含的元素有两个状态转移图(注册表)和状态转移函数,为了防止包外直接调用,这两个元素都设为了不可导出的。

  • 状态转移图说明了状态机的状态流转情况,包含现态、事件、次态和动作,一旦现态和事件确定,那么状态流转的唯一次态和动作就随之确定。
  • 状态转移函数定义了在状态转移的过程中需要做的事情,在创建状态时指定,如更新数据库实体(order)的状态。

状态机的动作(接口interface)又包含三个:Before、Execute和After

  • Before操作在是事务前执行,由于此时没有翻状态,所以该步可能会被重复执行。
  • Execute操作是和状态转移函数在同一事务中执行的,同时成功或同时失败。
  • After操作是在事务后执行,因为在执行前状态已经翻转,所以最多会执行一次,在业务上允许执行失败或未执行。

状态机的主要方法有两个:

  • SetTransitionMap方法用来设置状态机的状态转移图
  • Push方法用来根据现态和事件推动状态机进行状态翻转。Push方法中会先执行Before动作,再在同一事务中执行Execute动作和状态转移函数,最后执行After动作。在执行Push方法的时候会将params参数传递给状态转移函数。

注:动作并不负责将最新状态更新到DB,而是统一由状态转移函数负责

package fsm

import (
	"errors"
	"fmt"
	"golang-trick/17-fsm-go/database"
	"reflect"
)

// FSMState 状态机的状态类型
type FSMState int

// FSMEvent 状态机的事件类型
type FSMEvent string

// FsmAction 状态机的动作
type FsmAction interface {
	Before(bizParams map[string]interface{}) error                   // 状态转移前执行
	Execute(bizParams map[string]interface{}, tx *database.DB) error // 状态转移中执行
	After(bizParams map[string]interface{}) error                    // 状态转移后执行
}

// FSMDstStateAndAction 状态机的次态和动作,现态和事件一旦确定,次态和动作就唯一确定
type FSMDstStateAndAction struct {
	DstState FSMState  // 次态
	Action   FsmAction // 动作
}

// FSMTransitionMap 状态机的状态转移图类型(表驱动法:注册表),现态和事件一旦确定,次态和动作就唯一确定
type FSMTransitionMap map[FSMState]map[FSMEvent]FSMDstStateAndAction

// FSMTransitionFunc 状态机的状态转移函数类型
type FSMTransitionFunc func(params map[string]interface{}, srcState, dstState FSMState) error

// FSM 状态机,元素均为不可导出
type FSM struct {
	transitionMap  FSMTransitionMap  // 状态转移图(注册表)根据现态和事件查询次态和动作,注:动作并不负责将最新状态更新到DB,而是统一由状态转移函数负责
	transitionFunc FSMTransitionFunc // 状态转移函数,用于所有事件情况下,通用的将最新状态写入DB
}

// CreateNewFSM 创建一个新的状态机
func CreateNewFSM(transitionFunc FSMTransitionFunc) *FSM {
	return &FSM{
		transitionMap:  make(FSMTransitionMap),
		transitionFunc: transitionFunc,
	}
}

// SetTransitionMap 设置状态机的状态转移图
// 现态、事件、次态、动作
func (fsm *FSM) SetTransitionMap(srcState FSMState, event FSMEvent, dstState FSMState, action FsmAction) error {
	if int(srcState) < 0 || len(event) <= 0 || int(dstState) < 0 {
		fmt.Println("现态|事件|次态非法")
		return errors.New("现态|事件|次态非法")
	}

	// 注册表初始化
	transitionMap := fsm.transitionMap
	if transitionMap == nil {
		transitionMap = make(FSMTransitionMap)
	}

	// 二级map初始化,现态可以有多个不同事件转到不同状态,所以有一个二级map
	if _, ok := transitionMap[srcState]; !ok {
		transitionMap[srcState] = make(map[FSMEvent]FSMDstStateAndAction)
	}

	// 注册
	if _, ok := transitionMap[srcState][event]; !ok {
		dstStateAndAction := FSMDstStateAndAction{
			DstState: dstState,
			Action:   action,
		}
		transitionMap[srcState][event] = dstStateAndAction
	} else { // 重复定义
		fmt.Printf("现态[%v]+事件[%v]+次态[%v]已定义过,请勿重复定义。\n", srcState, event, dstState)
		return errors.New(fmt.Sprintf("现态[%v]+事件[%v]+次态[%v]已定义过,请勿重复定义。\n", srcState, event, dstState))
	}

	fsm.transitionMap = transitionMap
	return nil
}

// Push 状态机的状态转移 根据现态和事件推动到次态以及执行动作
func (fsm *FSM) Push(tx *database.DB, params map[string]interface{}, currentState FSMState, event FSMEvent) error {
	// 根据现态和事件从状态转移图获取次态和动作
	transitionMap := fsm.transitionMap // 使用副本,避免竞争冲突
	eventMap, ok := transitionMap[currentState]
	if !ok {
		return fmt.Errorf("现态[%v]未配置迁移事件", currentState)
	}
	dstStateAndAction, ok := eventMap[event]
	if !ok {
		return fmt.Errorf("现态[%v]+迁移事件[%v]未配置次态和动作", currentState, event)
	}
	dstState := dstStateAndAction.DstState
	action := dstStateAndAction.Action

	// 执行before方法
	if action != nil {
		// action 为interface,故用反射获取其实际类型,方便调试
		fsmActionName := reflect.ValueOf(action).String()
		fmt.Printf("现态[%v]+迁移事件[%v]->次态[%v], [%v].before\n", currentState, event, dstState, fsmActionName)
		if err := action.Before(params); err != nil {
			return fmt.Errorf("现态[%v]+迁移事件[%v]->次态[%v]失败, [%v].before, err: %v", currentState, event, dstState, fsmActionName, err)
		}
	}

	// 事务执行execute方法和transitionFunc(状态转移函数)
	if tx == nil {
		tx = new(database.DB)
	}
	err := tx.Transaction(func() error {
		fsmActionName := reflect.ValueOf(action).String()
		fmt.Printf("现态[%v] + 迁移事件[%v] -> 次态[%v],[%v].execute\n", currentState, event, dstState, fsmActionName)

		if action != nil {
			if err := action.Execute(params, tx); err != nil {
				return fmt.Errorf("现态[%v] + 迁移事件[%v]对应的动作[%v].execute执行失败\n", currentState, event, fsmActionName)
			}
		}

		fmt.Printf("现态[%v]+迁移事件[%v]->次态[%v], transitionFunc\n", currentState, event, dstState)
		// 业务逻辑执行无误后,进行状态转移,将新状态更新到DB
		if err := fsm.transitionFunc(params, currentState, dstState); err != nil {
			return fmt.Errorf("执行状态转移函数出错: %v", err)
		}
		return nil
	})
	if err != nil {
		return err
	}

	// 执行after方法
	if action != nil {
		fsmActionName := reflect.ValueOf(action).String()
		fmt.Printf("现态[%v]+迁移事件[%v]->次态[%v], [%v].after\n", currentState, event, dstState, fsmActionName)
		if err := action.After(params); err != nil {
			return fmt.Errorf("现态[%v]+迁移事件[%v]->次态[%v]失败, [%v].after, err: %v", currentState, event, dstState, fsmActionName, err)
		}
	}

	return nil
}

三:需求实现

3.1 订单状态机初始化

order.go

order.go中做的事情主要有:

  • 定义订单状态机的状态和事件;
  • 创建一个订单状态机,并初始化状态转移图以及设置状态转移函数;
  • 接收客户端请求,执行订单任务,推动状态转移。
package order

import (
	"fmt"
	"golang-trick/17-fsm-go/database"
	"golang-trick/17-fsm-go/fsm"
)

// 状态 实际工作中,一般会单独放到common包中,方便管理这些状态枚举
var (
	StateOrderInit          = fsm.FSMState(0) // 初始状态
	StateOrderToBePaid      = fsm.FSMState(1) // 待支付
	StateOrderToBeDelivered = fsm.FSMState(2) // 待发货
	StateOrderCancel        = fsm.FSMState(3) // 订单取消
	StateOrderToBeReceived  = fsm.FSMState(4) // 待收货
	StateOrderDone          = fsm.FSMState(5) // 订单完成
)

// 事件
var (
	EventOrderPlace      = fsm.FSMEvent("EventOrderPlace")      // 下单
	EventOrderPay        = fsm.FSMEvent("EventOrderPay")        // 支付
	EventOrderPayTimeout = fsm.FSMEvent("EventOrderPayTimeout") // 支付超时
	EventOrderDeliver    = fsm.FSMEvent("EventOrderDeliver")    // 发货
	EventOrderReceive    = fsm.FSMEvent("EventOrderReceive")    // 收货
)

// 订单状态机
var orderFSM *fsm.FSM

// Init 状态机的状态转移图初始化(初始化注册表)
func Init() {
	orderFSM = fsm.CreateNewFSM(orderTransitionFunc)
	// 初态 + 事件 --> 次态 + 动作(动作不是必需的)
	orderFSM.SetTransitionMap(StateOrderInit, EventOrderPlace, StateOrderToBePaid, PlaceAction{})                  // 初始化+下单 -> 待支付
	orderFSM.SetTransitionMap(StateOrderToBePaid, EventOrderPay, StateOrderToBeDelivered, PayAction{})             // 待支付+支付 -> 待发货
	orderFSM.SetTransitionMap(StateOrderToBePaid, EventOrderPayTimeout, StateOrderCancel, nil)                     // 待支付+支付超时 -> 订单取消
	orderFSM.SetTransitionMap(StateOrderToBeDelivered, EventOrderDeliver, StateOrderToBeReceived, DeliverAction{}) // 待发货+发货 -> 待收货
	orderFSM.SetTransitionMap(StateOrderToBeReceived, EventOrderReceive, StateOrderDone, ReceiveAction{})          // 待收货+收货 -> 订单完成
}

// orderTransitionFunc 订单状态转移函数,负责所有事件情况下,统一将最新状态写入DB
func orderTransitionFunc(params map[string]interface{}, srcState, dstState fsm.FSMState) error {
	// 从params中解析order参数
	key, ok := params["order"]
	if !ok {
		return fmt.Errorf("params[\"order\"]不存在。")
	}
	curOrder := key.(*database.Order)
	fmt.Printf("order.Id : %v,order.State:%v\n", curOrder.ID, curOrder.State)

	// 订单状态转移
	err := database.UpdateOrderState(curOrder, int(srcState), int(dstState))
	if err != nil {
		return err
	}
	return nil
}

// ExecOrderTask 执行订单任务,推动状态转移
// 实际工作中,一般是请求方传入指定的订单信息,如订单编号,以及操作类型(事件)
// 服务端从DB查出订单详情,校验是否可以处理该请求(根据具体业务情况做一些校验,如订单已经支付,这里收到再次支付(事件)则不应该处理)
func ExecOrderTask(params map[string]interface{}) error {
	// 从params中解析order参数
	key, ok := params["order"]
	if !ok {
		return fmt.Errorf("params[\"order\"]不存在。")
	}
	curOrder := key.(*database.Order)

	// 下面是各种操作,实际工作中,初态会从DB查出,事件由客户端传入,因此不会用那么多if的
	// 这里为了演示每个状态都能转移成功,所以把初态,事件穷举了,看上去有多个if,看最后main.go的输出就明白了,订单为初始态时,一个下单事件就走遍下面每个if
	// 初始化+下单 -> 待支付
	if curOrder.State == int(StateOrderInit) {
		if err := orderFSM.Push(nil, params, StateOrderInit, EventOrderPlace); err != nil {
			return err
		}
	}

	// 待支付+支付 -> 待发货
	if curOrder.State == int(StateOrderToBePaid) {
		if err := orderFSM.Push(nil, params, StateOrderToBePaid, EventOrderPay); err != nil {
			return err
		}
	}

	// 待支付+支付超时 -> 订单取消
	if curOrder.State == int(StateOrderToBePaid) {
		if err := orderFSM.Push(nil, params, StateOrderToBePaid, EventOrderPayTimeout); err != nil {
			return err
		}
	}

	// 待发货+发货 -> 待收货
	if curOrder.State == int(StateOrderToBeDelivered) {
		if err := orderFSM.Push(nil, params, StateOrderToBeDelivered, EventOrderDeliver); err != nil {
			return err
		}
	}
	// 待收货+收货 -> 订单完成
	if curOrder.State == int(StateOrderToBeReceived) {
		if err := orderFSM.Push(nil, params, StateOrderToBeReceived, EventOrderReceive); err != nil {
			return err
		}
	}
	return nil
}

3.2 动作实现

order.goInit方法中,我们完成了注册表的初始化,里面用到了动作,这里就给出每个动作的实现,分别是下单、支付、发货、收货

下单:order_action_place.go

package order

import (
	"fmt"
	"golang-trick/17-fsm-go/database"
)

type PlaceAction struct {
}

// Before 事务前执行,业务上允许多次操作
func (receiver PlaceAction) Before(bizParams map[string]interface{}) error {
	// 一般用于一些前置校验
	fmt.Println("执行下单的Before方法。")
	return nil
}

// Execute 事务中执行,与状态转移在同一事务中
func (receiver PlaceAction) Execute(bizParams map[string]interface{}, tx *database.DB) error {
	// 如下单后,需要发MQ通知其他服务
	fmt.Println("执行下单的Execute方法。")
	return nil
}

// After 事务后执行,业务上允许执行失败或未执行
func (receiver PlaceAction) After(bizParams map[string]interface{}) error {
	fmt.Println("执行下单的After方法。")
	return nil
}

支付:order_action_pay.go

package order

import (
	"fmt"
	"golang-trick/17-fsm-go/database"
)

type PayAction struct {
}

// Before 事务前执行,业务上允许多次操作
func (receiver PayAction) Before(bizParams map[string]interface{}) error {
	fmt.Println("执行支付的Before方法。")
	return nil
}

// Execute 事务中执行,与状态转移在同一事务中
func (receiver PayAction) Execute(bizParams map[string]interface{}, tx *database.DB) error {
	fmt.Println("执行支付的Execute方法。")
	return nil
}

// After 事务后执行,业务上允许执行失败或未执行
func (receiver PayAction) After(bizParams map[string]interface{}) error {
	fmt.Println("执行支付的After方法。")
	return nil
}

发货:order_action_deliver.go

package order

import (
	"fmt"
	"golang-trick/17-fsm-go/database"
)

type DeliverAction struct {
}

// Before 事务前执行,业务上允许多次操作
func (receiver DeliverAction) Before(bizParams map[string]interface{}) error {
	fmt.Println("执行发货的Before方法。")
	return nil
}

// Execute 事务中执行,与状态转移在同一事务中
func (receiver DeliverAction) Execute(bizParams map[string]interface{}, tx *database.DB) error {
	fmt.Println("执行发货的Execute方法。")
	return nil
}

// After 事务后执行,业务上允许执行失败或未执行
func (receiver DeliverAction) After(bizParams map[string]interface{}) error {
	fmt.Println("执行发货的After方法。")
	return nil
}

收货:order_action_receive.go

package order

import (
	"fmt"
	"golang-trick/17-fsm-go/database"
)

type ReceiveAction struct {
}

// Before 事务前执行,业务上允许多次操作
func (receiver ReceiveAction) Before(bizParams map[string]interface{}) error {
	fmt.Println("执行收货的Before方法。")
	return nil
}

// Execute 事务中执行,与状态转移在同一事务中
func (receiver ReceiveAction) Execute(bizParams map[string]interface{}, tx *database.DB) error {
	fmt.Println("执行收货的Execute方法。")
	return nil
}

// After 事务后执行,业务上允许执行失败或未执行
func (receiver ReceiveAction) After(bizParams map[string]interface{}) error {
	fmt.Println("执行收货的After方法。")
	return nil
}

四:客户端使用

这里客户端使用,其实是本案例的一个简单演示,类似单测,看看能不能跑通

最后在main包里先初始化一个订单状态机,查询所有订单,并使用状态机执行订单任务,推动订单状态转移。注意多个订单可以用同一个状态机来进行状态的迁移。

main.go

package main

import (
	"fmt"
	"golang-trick/17-fsm-go/database"
	"golang-trick/17-fsm-go/order"
)

func main() {
	order.Init()
	orderList, err := database.ListAllOrder()
	if err != nil {
		return
	}

	for _, curOrder := range orderList {
		params := make(map[string]interface{})
		params["order"] = curOrder
		err := order.ExecOrderTask(params)
		if err != nil {
			fmt.Printf("执行订单任务出错:%v\n", err)
		}
		fmt.Println()
		fmt.Println()
		fmt.Println()
	}
}

输出结果:

GOROOT=C:\Program Files\Go #gosetup
GOPATH=C:\Users\lym\go #gosetup
"C:\Program Files\Go\bin\go.exe" build -o C:\Users\lym\AppData\Local\Temp\GoLand\___go_build_golang_trick_17_fsm_go.exe golang-trick/17-fsm-go #gosetup
C:\Users\lym\AppData\Local\Temp\GoLand\___go_build_golang_trick_17_fsm_go.exe #gosetup
现态[0]+迁移事件[EventOrderPlace]->次态[1], [<order.PlaceAction Value>].before
执行下单的Before方法。
事务执行开始
现态[0] + 迁移事件[EventOrderPlace] -> 次态[1],[<order.PlaceAction Value>].execute
执行下单的Execute方法。
现态[0]+迁移事件[EventOrderPlace]->次态[1], transitionFunc
order.Id : 1,order.State:0
更新id为 1 的订单状态,从现态[0]到次态[1]
事务提交
现态[0]+迁移事件[EventOrderPlace]->次态[1], [<order.PlaceAction Value>].after
执行下单的After方法。
现态[1]+迁移事件[EventOrderPay]->次态[2], [<order.PayAction Value>].before
执行支付的Before方法。
事务执行开始
现态[1] + 迁移事件[EventOrderPay] -> 次态[2],[<order.PayAction Value>].execute
执行支付的Execute方法。
现态[1]+迁移事件[EventOrderPay]->次态[2], transitionFunc
order.Id : 1,order.State:1
更新id为 1 的订单状态,从现态[1]到次态[2]
事务提交
现态[1]+迁移事件[EventOrderPay]->次态[2], [<order.PayAction Value>].after
执行支付的After方法。
现态[2]+迁移事件[EventOrderDeliver]->次态[4], [<order.DeliverAction Value>].before
执行发货的Before方法。
事务执行开始
现态[2] + 迁移事件[EventOrderDeliver] -> 次态[4],[<order.DeliverAction Value>].execute
执行发货的Execute方法。
现态[2]+迁移事件[EventOrderDeliver]->次态[4], transitionFunc
order.Id : 1,order.State:2
更新id为 1 的订单状态,从现态[2]到次态[4]
事务提交
现态[2]+迁移事件[EventOrderDeliver]->次态[4], [<order.DeliverAction Value>].after
执行发货的After方法。
现态[4]+迁移事件[EventOrderReceive]->次态[5], [<order.ReceiveAction Value>].before
执行收货的Before方法。
事务执行开始
现态[4] + 迁移事件[EventOrderReceive] -> 次态[5],[<order.ReceiveAction Value>].execute
执行收货的Execute方法。
现态[4]+迁移事件[EventOrderReceive]->次态[5], transitionFunc
order.Id : 1,order.State:4
更新id为 1 的订单状态,从现态[4]到次态[5]
事务提交
现态[4]+迁移事件[EventOrderReceive]->次态[5], [<order.ReceiveAction Value>].after
执行收货的After方法。



现态[1]+迁移事件[EventOrderPay]->次态[2], [<order.PayAction Value>].before
执行支付的Before方法。
事务执行开始
现态[1] + 迁移事件[EventOrderPay] -> 次态[2],[<order.PayAction Value>].execute
执行支付的Execute方法。
现态[1]+迁移事件[EventOrderPay]->次态[2], transitionFunc
order.Id : 2,order.State:1
更新id为 2 的订单状态,从现态[1]到次态[2]
事务提交
现态[1]+迁移事件[EventOrderPay]->次态[2], [<order.PayAction Value>].after
执行支付的After方法。
现态[2]+迁移事件[EventOrderDeliver]->次态[4], [<order.DeliverAction Value>].before
执行发货的Before方法。
事务执行开始
现态[2] + 迁移事件[EventOrderDeliver] -> 次态[4],[<order.DeliverAction Value>].execute
执行发货的Execute方法。
现态[2]+迁移事件[EventOrderDeliver]->次态[4], transitionFunc
order.Id : 2,order.State:2
更新id为 2 的订单状态,从现态[2]到次态[4]
事务提交
现态[2]+迁移事件[EventOrderDeliver]->次态[4], [<order.DeliverAction Value>].after
执行发货的After方法。
现态[4]+迁移事件[EventOrderReceive]->次态[5], [<order.ReceiveAction Value>].before
执行收货的Before方法。
事务执行开始
现态[4] + 迁移事件[EventOrderReceive] -> 次态[5],[<order.ReceiveAction Value>].execute
执行收货的Execute方法。
现态[4]+迁移事件[EventOrderReceive]->次态[5], transitionFunc
order.Id : 2,order.State:4
更新id为 2 的订单状态,从现态[4]到次态[5]
事务提交
现态[4]+迁移事件[EventOrderReceive]->次态[5], [<order.ReceiveAction Value>].after
执行收货的After方法。



现态[2]+迁移事件[EventOrderDeliver]->次态[4], [<order.DeliverAction Value>].before
执行发货的Before方法。
事务执行开始
现态[2] + 迁移事件[EventOrderDeliver] -> 次态[4],[<order.DeliverAction Value>].execute
执行发货的Execute方法。
现态[2]+迁移事件[EventOrderDeliver]->次态[4], transitionFunc
order.Id : 2,order.State:2
更新id为 2 的订单状态,从现态[2]到次态[4]
事务提交
现态[2]+迁移事件[EventOrderDeliver]->次态[4], [<order.DeliverAction Value>].after
执行发货的After方法。
现态[4]+迁移事件[EventOrderReceive]->次态[5], [<order.ReceiveAction Value>].before
执行收货的Before方法。
事务执行开始
现态[4] + 迁移事件[EventOrderReceive] -> 次态[5],[<order.ReceiveAction Value>].execute
执行收货的Execute方法。
现态[4]+迁移事件[EventOrderReceive]->次态[5], transitionFunc
order.Id : 2,order.State:4
更新id为 2 的订单状态,从现态[4]到次态[5]
事务提交
现态[4]+迁移事件[EventOrderReceive]->次态[5], [<order.ReceiveAction Value>].after
执行收货的After方法。




Process finished with the exit code 0

五:总结

状态机可以对一个复杂的业务流程进行模块化拆分,使得代码更为易读。并且扩展性更好,如果后续有新状态加入,只需要在原来的基础上
进行扩展即可,甚至不需要了解整个业务流程。其次,它将数据库实体的状态流转进行了模范化,避免了不同的开发人员在写更新数据库实
体状态代码时可能导致的问题。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值