Prechádzať zdrojové kódy

buildin pagination temporary transaction

zeuszhao 1 rok pred
rodič
commit
beed738a9f

+ 15 - 0
buildin/int.go

@@ -0,0 +1,15 @@
+package buildin
+
+import (
+	"math/rand"
+	"time"
+)
+
+// RangeInt 按照指定范围生成随机数
+func RangeInt(min, max uint) int {
+	if min >= max {
+		return int(min)
+	}
+	rand.Seed(time.Now().UnixNano())
+	return int(min + uint(rand.Intn(int(max-min+1))))
+}

+ 22 - 0
buildin/int_test.go

@@ -0,0 +1,22 @@
+package buildin
+
+import "testing"
+
+func TestRangeInt(t *testing.T) {
+	// Test case 1: min equals max
+	min1 := uint(5)
+	max1 := uint(5)
+	expected1 := 5
+	result1 := RangeInt(min1, max1)
+	if result1 != expected1 {
+		t.Errorf("Test case 1 failed. Expected %v, got %v", expected1, result1)
+	}
+
+	// Test case 2: min is less than max
+	min2 := uint(1)
+	max2 := uint(10)
+	result2 := RangeInt(min2, max2)
+	if result2 < int(min2) || result2 > int(max2) {
+		t.Errorf("Test case 2 failed. Result %v is out of range", result2)
+	}
+}

+ 45 - 0
buildin/slice.go

@@ -0,0 +1,45 @@
+package buildin
+
+// InSlice 判断自字符串是否在另一个字符串内
+func InSlice[T comparable](item T, slice []T) bool {
+	for _, s := range slice {
+		if s == item {
+			return true
+		}
+	}
+	return false
+}
+
+// Intersection 求交集
+func Intersection[T comparable](a, b []T) []T {
+	var res []T
+	if len(a) == 0 || len(b) == 0 {
+		return res
+	}
+	m := make(map[T]bool)
+	for _, v := range a {
+		m[v] = true
+	}
+
+	for _, v := range b {
+		if m[v] {
+			res = append(res, v)
+		}
+	}
+	return res
+}
+
+// RemoveDuplicates 去除重复值
+func RemoveDuplicates[T comparable](slice []T) []T {
+	unique := make(map[T]bool)
+	result := make([]T, 0, len(slice))
+
+	for _, s := range slice {
+		if !unique[s] {
+			unique[s] = true
+			result = append(result, s)
+		}
+	}
+
+	return result
+}

+ 73 - 0
buildin/slice_test.go

@@ -0,0 +1,73 @@
+package buildin
+
+import (
+	"reflect"
+	"testing"
+)
+
+func TestIntersection(t *testing.T) {
+	// Test case 1
+	a := []string{"apple", "banana", "orange"}
+	b := []string{"banana", "grape", "orange"}
+	expected1 := []string{"banana", "orange"}
+	result1 := Intersection(a, b)
+	if !reflect.DeepEqual(result1, expected1) {
+		t.Errorf("Test case 1 failed. Expected %v, got %v", expected1, result1)
+	}
+
+	// Test case 2
+	c := []int{1, 2, 3, 4}
+	d := []int{2, 3, 4, 5}
+	expected2 := []int{2, 3, 4}
+	result2 := Intersection(c, d)
+	if !reflect.DeepEqual(result2, expected2) {
+		t.Errorf("Test case 2 failed. Expected %v, got %v", expected2, result2)
+	}
+}
+
+func TestInSlice(t *testing.T) {
+	// Test case 1: int
+	slice1 := []int{1, 2, 3, 4, 5}
+	item1 := 3
+	expected1 := true
+	result1 := InSlice(item1, slice1)
+	if result1 != expected1 {
+		t.Errorf("Test case 1 failed. Expected %v, got %v", expected1, result1)
+	}
+
+	// Test case 2: string
+	slice2 := []string{"apple", "banana", "orange"}
+	item2 := "grape"
+	expected2 := false
+	result2 := InSlice(item2, slice2)
+	if result2 != expected2 {
+		t.Errorf("Test case 2 failed. Expected %v, got %v", expected2, result2)
+	}
+
+	// Test case 3: bool
+	slice3 := []bool{true, false}
+	item3 := true
+	expected3 := true
+	result3 := InSlice(item3, slice3)
+	if result3 != expected3 {
+		t.Errorf("Test case 3 failed. Expected %v, got %v", expected3, result3)
+	}
+}
+
+func TestRemoveDuplicates(t *testing.T) {
+	// Test case 1: []string
+	slice1 := []string{"apple", "banana", "apple", "orange", "banana"}
+	result1 := RemoveDuplicates(slice1)
+	expected1 := []string{"apple", "banana", "orange"}
+	if !reflect.DeepEqual(result1, expected1) {
+		panic("Test case 1 failed")
+	}
+
+	// Test case 2: []int
+	slice2 := []int{1, 2, 2, 3, 4, 4, 5}
+	result2 := RemoveDuplicates(slice2)
+	expected2 := []int{1, 2, 3, 4, 5}
+	if !reflect.DeepEqual(result2, expected2) {
+		panic("Test case 2 failed")
+	}
+}

+ 73 - 0
buildin/string.go

@@ -0,0 +1,73 @@
+package buildin
+
+import (
+	"fmt"
+	"math/rand"
+	"time"
+	"unicode/utf8"
+	"unsafe"
+)
+
+const (
+	// 6 bits to represent a letter index
+	letterIdBits = 6
+	// All 1-bits as many as letterIdBits
+	letterIdMask = 1<<letterIdBits - 1
+	letterIdMax  = 63 / letterIdBits
+)
+
+// SplitStringByLength 将字符串按长度划分为数组,汉字2个长度英文1个长度
+func SplitStringByLength(str string, maxLength int) []string {
+	var arr []string
+	index := 0
+	s := ""
+	for _, v := range str {
+		s = s + string(v)
+		if utf8.RuneLen(v) == 1 {
+			index += 1
+		} else {
+			index += 2
+		}
+		if index >= maxLength {
+			arr = append(arr, s)
+			index = 0
+			s = ""
+		}
+	}
+	if s != "" {
+		arr = append(arr, s)
+	}
+	return arr
+}
+
+// RandString 随机生成字符串 最强性能!
+func RandString(n int) string {
+	letters := "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"
+	var src = rand.NewSource(time.Now().UnixNano())
+
+	b := make([]byte, n)
+	// A rand.Int63() generates 63 random bits, enough for letterIdMax letters!
+	for i, cache, remain := n-1, src.Int63(), letterIdMax; i >= 0; {
+		if remain == 0 {
+			cache, remain = src.Int63(), letterIdMax
+		}
+		if idx := int(cache & letterIdMask); idx < len(letters) {
+			b[i] = letters[idx]
+			i--
+		}
+		cache >>= letterIdBits
+		remain--
+	}
+	return *(*string)(unsafe.Pointer(&b))
+}
+
+// RandNumberString 返回特定长度的数据随机数字字符串
+func RandNumberString(bit uint8) string {
+	rand.Seed(time.Now().UnixNano())
+	s := ""
+	for i := 0; uint8(i) < bit; i++ {
+		r := rand.Intn(9)
+		s += fmt.Sprintf("%d", r)
+	}
+	return s
+}

+ 97 - 0
buildin/string_test.go

@@ -0,0 +1,97 @@
+package buildin
+
+import "testing"
+
+func TestSplitStringByLength(t *testing.T) {
+	// 测试用例1:字符串长度小于 maxLength
+	str := "Hello"
+	maxLength := 10
+	expected := []string{"Hello"}
+
+	result := SplitStringByLength(str, maxLength)
+	if len(result) != len(expected) {
+		t.Errorf("Expected %d elements, but got %d", len(expected), len(result))
+	}
+
+	for i := range result {
+		if result[i] != expected[i] {
+			t.Errorf("Expected %s, but got %s", expected[i], result[i])
+		}
+	}
+
+	// 测试用例2:字符串长度大于 maxLength
+	str = "你好,Hello,世界!"
+	maxLength = 5
+	expected = []string{"你好,", "Hello", ",世界", "!"}
+
+	result = SplitStringByLength(str, maxLength)
+	if len(result) != len(expected) {
+		t.Errorf("Expected %d elements, but got %d", len(expected), len(result))
+	}
+
+	for i := range result {
+		if result[i] != expected[i] {
+			t.Errorf("Expected %s, but got %s", expected[i], result[i])
+		}
+	}
+}
+
+func TestRandString(t *testing.T) {
+	// 测试用例1:生成长度为10的随机字符串
+	n := 10
+	result := RandString(n)
+	if len(result) != n {
+		t.Errorf("Expected string length %d, but got %d", n, len(result))
+	}
+
+	// 测试用例2:生成长度为20的随机字符串
+	n = 20
+	result = RandString(n)
+	if len(result) != n {
+		t.Errorf("Expected string length %d, but got %d", n, len(result))
+	}
+
+	// 测试用例3:生成长度为0的随机字符串
+	n = 0
+	result = RandString(n)
+	if len(result) != n {
+		t.Errorf("Expected string length %d, but got %d", n, len(result))
+	}
+}
+
+func BenchmarkRandString(b *testing.B) {
+	n := 10
+	for i := 0; i < b.N; i++ {
+		RandString(n)
+	}
+}
+
+func TestRandNumberString(t *testing.T) {
+	// 测试用例1:生成3位数字字符串
+	bit := uint8(3)
+	result := RandNumberString(bit)
+	if len(result) != int(bit) {
+		t.Errorf("Expected string length %d, but got %d", bit, len(result))
+	}
+
+	// 测试用例2:生成5位数字字符串
+	bit = uint8(5)
+	result = RandNumberString(bit)
+	if len(result) != int(bit) {
+		t.Errorf("Expected string length %d, but got %d", bit, len(result))
+	}
+
+	// 测试用例3:生成0位数字字符串
+	bit = uint8(0)
+	result = RandNumberString(bit)
+	if len(result) != int(bit) {
+		t.Errorf("Expected string length %d, but got %d", bit, len(result))
+	}
+}
+
+func BenchmarkRandNumberString(b *testing.B) {
+	bit := uint8(10)
+	for i := 0; i < b.N; i++ {
+		RandNumberString(bit)
+	}
+}

+ 17 - 0
constant/mysql.go

@@ -0,0 +1,17 @@
+package constant
+
+import "github.com/go-sql-driver/mysql"
+
+var (
+	// ErrorMysqlDuplicateEntryCode 命中唯一索引
+	ErrorMysqlDuplicateEntryCode = 1062
+)
+
+// MysqlErrorCode 根据mysql错误信息返回错误代码
+func MysqlErrorCode(err error) int {
+	mysqlErr, ok := err.(*mysql.MySQLError)
+	if !ok {
+		return 0
+	}
+	return int(mysqlErr.Number)
+}

+ 6 - 1
go.mod

@@ -4,10 +4,15 @@ go 1.19
 
 require (
 	github.com/go-redis/redis/v8 v8.11.5
+	github.com/go-sql-driver/mysql v1.7.1
 	github.com/patrickmn/go-cache v2.1.0+incompatible
+	google.golang.org/genproto/googleapis/rpc v0.0.0-20230815205213-6bfd019c3878
+	google.golang.org/grpc v1.57.0
 )
 
 require (
-	github.com/cespare/xxhash/v2 v2.1.2 // indirect
+	github.com/cespare/xxhash/v2 v2.2.0 // indirect
 	github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
+	github.com/golang/protobuf v1.5.3 // indirect
+	google.golang.org/protobuf v1.31.0 // indirect
 )

+ 21 - 5
go.sum

@@ -1,17 +1,33 @@
-github.com/cespare/xxhash/v2 v2.1.2 h1:YRXhKfTDauu4ajMg1TPgFO5jnlC2HCbmLXMcTG5cbYE=
-github.com/cespare/xxhash/v2 v2.1.2/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
+github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44=
+github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
 github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78=
 github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc=
 github.com/fsnotify/fsnotify v1.4.9 h1:hsms1Qyu0jgnwNXIxa+/V/PDsU6CfLf6CNO8H7IWoS4=
 github.com/go-redis/redis/v8 v8.11.5 h1:AcZZR7igkdvfVmQTPnu9WE37LRrO/YrBH5zWyjDC0oI=
 github.com/go-redis/redis/v8 v8.11.5/go.mod h1:gREzHqY1hg6oD9ngVRbLStwAWKhA0FEgq8Jd4h5lpwo=
+github.com/go-sql-driver/mysql v1.7.1 h1:lUIinVbN1DY0xBg0eMOzmmtGoHwWBbvnWubQUrtU8EI=
+github.com/go-sql-driver/mysql v1.7.1/go.mod h1:OXbVy3sEdcQ2Doequ6Z5BW6fXNQTmx+9S1MCJN5yJMI=
+github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
+github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg=
+github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
+github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
+github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
 github.com/nxadm/tail v1.4.8 h1:nPr65rt6Y5JFSKQO7qToXr7pePgD6Gwiw05lkbyAQTE=
 github.com/onsi/ginkgo v1.16.5 h1:8xi0RTUf59SOSfEtZMvwTvXYMzG4gV23XVHOZiXNtnE=
 github.com/onsi/gomega v1.18.1 h1:M1GfJqGRrBrrGGsbxzV5dqM2U2ApXefZCQpkukxYRLE=
 github.com/patrickmn/go-cache v2.1.0+incompatible h1:HRMgzkcYKYpi3C8ajMPV8OFXaaRUnok+kx1WdO15EQc=
 github.com/patrickmn/go-cache v2.1.0+incompatible/go.mod h1:3Qf8kWWT7OJRJbdiICTKqZju1ZixQ/KpMGzzAfe6+WQ=
-golang.org/x/net v0.0.0-20210428140749-89ef3d95e781 h1:DzZ89McO9/gWPsQXS/FVKAlG02ZjaQ6AlZRBimEYOd0=
-golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e h1:fLOSk5Q00efkSvAm+4xcoXD+RRmLmmulPn5I3Y9F2EM=
-golang.org/x/text v0.3.6 h1:aRYxNxv6iGQlyVaZmk6ZgYEDa+Jg18DxebPSrd6bg1M=
+golang.org/x/net v0.9.0 h1:aWJ/m6xSmxWBx+V0XRHTlrYrPG56jKsLdTFmsSsCzOM=
+golang.org/x/sys v0.7.0 h1:3jlCCIQZPdOYu1h8BkNvLz8Kgwtae2cagcG/VamtZRU=
+golang.org/x/text v0.9.0 h1:2sjJmO8cDvYveuX97RDLsxlyUxLl+GHoLxBiRdHllBE=
+golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
+google.golang.org/genproto/googleapis/rpc v0.0.0-20230815205213-6bfd019c3878 h1:lv6/DhyiFFGsmzxbsUUTOkN29II+zeWHxvT8Lpdxsv0=
+google.golang.org/genproto/googleapis/rpc v0.0.0-20230815205213-6bfd019c3878/go.mod h1:+Bk1OCOj40wS2hwAMA+aCW9ypzm63QTBBHp6lQ3p+9M=
+google.golang.org/grpc v1.57.0 h1:kfzNeI/klCGD2YPMUlaGNT3pxvYfga7smW3Vth8Zsiw=
+google.golang.org/grpc v1.57.0/go.mod h1:Sd+9RMTACXwmub0zcNY2c4arhtrbBYD1AUHI/dt16Mo=
+google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
+google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
+google.golang.org/protobuf v1.31.0 h1:g0LDEJHgrBl9N9r17Ru3sqWhkIx2NB67okBHPwC7hs8=
+google.golang.org/protobuf v1.31.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
 gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ=
 gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=

+ 42 - 0
http/codes.go

@@ -0,0 +1,42 @@
+package http
+
+import (
+	"google.golang.org/grpc/codes"
+	"net/http"
+)
+
+func httpStatusFromGrpcCode(code codes.Code) int {
+	switch code {
+	case codes.OK:
+		return http.StatusOK
+	case codes.Canceled:
+		return http.StatusRequestTimeout
+	case codes.Unknown:
+		return http.StatusInternalServerError
+	case codes.InvalidArgument:
+		return http.StatusBadRequest
+	case codes.DeadlineExceeded:
+		return http.StatusGatewayTimeout
+	case codes.ResourceExhausted:
+		return http.StatusTooManyRequests
+	case codes.FailedPrecondition:
+		return http.StatusBadRequest
+	case codes.OutOfRange:
+		return http.StatusBadRequest
+	case codes.Unimplemented:
+		return http.StatusNotImplemented
+	case codes.Internal:
+		return http.StatusInternalServerError
+	case codes.Unavailable:
+		return http.StatusServiceUnavailable
+	case codes.DataLoss:
+		return http.StatusInternalServerError
+	case codes.NotFound:
+		return http.StatusNotFound
+	case codes.Unauthenticated:
+		return http.StatusUnauthorized
+	case codes.PermissionDenied:
+		return http.StatusForbidden
+	}
+	return http.StatusInternalServerError
+}

+ 118 - 0
http/response.go

@@ -0,0 +1,118 @@
+package http
+
+import (
+	"encoding/json"
+	"fmt"
+	"google.golang.org/genproto/googleapis/rpc/errdetails"
+	"google.golang.org/grpc/status"
+	"io"
+	"net/http"
+	"strings"
+)
+
+type Response struct {
+	Code     int         `json:"code"`
+	Reason   string      `json:"reason"`
+	Message  string      `json:"message"`
+	Metadata interface{} `json:"metadata"`
+
+	w  http.ResponseWriter
+	ct string
+}
+
+func NewResponse(w http.ResponseWriter) *Response {
+	return &Response{
+		w: w,
+	}
+}
+
+func (r *Response) StatusCode(code int) *Response {
+	r.Code = code
+	return r
+}
+
+func (r *Response) ContentType(ct string) *Response {
+	r.ct = ct
+	return r
+}
+
+func (r *Response) getCode(defaultCode int) int {
+	if r.Code != 0 {
+		return r.Code
+	}
+	return defaultCode
+}
+
+func (r *Response) getContentType(defaultContentType string) string {
+	if r.ct != "" {
+		return r.ct
+	}
+	return defaultContentType
+}
+
+func (r *Response) Error(err error) {
+	r.buildResponseBody(err)
+	r.w.Header().Set("Content-Type", r.getContentType("application/json"))
+	r.w.WriteHeader(r.getCode(http.StatusInternalServerError))
+
+	body, err := json.Marshal(r)
+	if err != nil {
+		r.w.Write([]byte(err.Error()))
+		return
+	}
+	r.w.Write(body)
+}
+
+func (r *Response) Json(body []byte) {
+	r.w.Header().Set("Content-Type", r.getContentType("application/json"))
+	r.w.WriteHeader(r.getCode(http.StatusOK))
+	r.w.Write(body)
+}
+
+func (r *Response) Raw(rc io.Reader) {
+	r.w.Header().Set("Content-Type", r.getContentType("application/octet-stream"))
+	r.w.WriteHeader(r.getCode(http.StatusOK))
+	io.Copy(r.w, rc)
+}
+
+func (r *Response) Redirect(url string) {
+	r.w.Header().Set("Location", url)
+	r.w.WriteHeader(r.getCode(http.StatusOK))
+	r.w.Write([]byte(""))
+}
+
+func (r *Response) JsonOk(ok bool) {
+	j, _ := json.Marshal(map[string]bool{
+		"ok": ok,
+	})
+	r.Json(j)
+}
+
+func (r *Response) Abort() {
+	r.ContentType("text/plain").Raw(strings.NewReader(http.StatusText(r.getCode(http.StatusInternalServerError))))
+}
+
+func (r *Response) Download(body io.Reader, filename string) {
+	r.w.Header().Set("Content-Disposition", fmt.Sprintf("attachment; filename=%s", filename))
+	r.Raw(body)
+}
+
+func (r *Response) buildResponseBody(err error) {
+	s := status.Convert(err)
+	pb := s.Proto()
+	st := httpStatusFromGrpcCode(s.Code())
+	if len(s.Details()) > 0 {
+		for _, detail := range s.Details() {
+			switch d := detail.(type) {
+			case *errdetails.ErrorInfo:
+				r.Reason = d.Reason
+				r.Metadata = d.Metadata
+				break
+			}
+		}
+	} else {
+		r.Reason = "UNKNOWN_ERROR"
+	}
+	r.Message = pb.GetMessage()
+	r.Code = st
+}

+ 48 - 0
pagination/pagination.go

@@ -0,0 +1,48 @@
+package pagination
+
+type Pagination interface {
+	OffsetLimit() *offsetLimit
+	Page() *page
+}
+
+type page struct {
+	Now  uint32
+	Size uint32
+}
+
+type offsetLimit struct {
+	Offset uint32
+	Limit  uint32
+}
+
+type custom struct {
+	Now  uint32
+	Size uint32
+}
+
+func (p *custom) OffsetLimit() *offsetLimit {
+	pageNow := p.Now
+	if pageNow > 1 {
+		pageNow = pageNow - 1
+	} else {
+		pageNow = 0
+	}
+	return &offsetLimit{
+		Offset: pageNow * p.Size,
+		Limit:  p.Size,
+	}
+}
+
+func (p *custom) Page() *page {
+	return &page{
+		Now:  p.Now,
+		Size: p.Size,
+	}
+}
+
+func New(now, size uint32) Pagination {
+	return &custom{
+		Now:  now,
+		Size: size,
+	}
+}

+ 57 - 0
temporary/temporary.go

@@ -0,0 +1,57 @@
+package temporary
+
+import (
+	"context"
+	"errors"
+	"fmt"
+	"git.ttmylife.com/zeuszhao/kit/buildin"
+	"git.ttmylife.com/zeuszhao/kit/cache"
+	"time"
+)
+
+var ErrorCode = errors.New("验证码错误")
+
+type VerificationCode struct {
+	c   cache.Cache
+	key string
+}
+
+func NewVerificationCode(c cache.Cache, target string) *VerificationCode {
+	return &VerificationCode{
+		c:   c,
+		key: target,
+	}
+}
+
+func (t *VerificationCode) makeKey(target string) string {
+	return fmt.Sprintf("temporary:auth:code:%s", target)
+}
+
+func (t *VerificationCode) Make(ctx context.Context, bit uint8, ti time.Duration) (string, error) {
+	randStr := buildin.RandNumberString(bit)
+	k := t.makeKey(t.key)
+	_, err := t.c.Set(ctx, k, randStr, ti)
+	if err != nil {
+		return "", err
+	}
+	return randStr, nil
+}
+
+func (t *VerificationCode) Verify(ctx context.Context, code string) (bool, error) {
+	k := t.makeKey(t.key)
+	vv, err := t.c.Get(ctx, k)
+	if err != nil {
+		return false, ErrorCode
+	}
+
+	if vv == code {
+		return true, nil
+	}
+
+	return false, nil
+}
+
+func (t *VerificationCode) Destroy(ctx context.Context) {
+	k := t.makeKey(t.key)
+	t.c.Delete(ctx, k)
+}

+ 7 - 0
transaction/transaction.go

@@ -0,0 +1,7 @@
+package transaction
+
+import "context"
+
+type Transaction interface {
+	Transaction(context.Context, func(ctx context.Context) error) error
+}