init repo

This commit is contained in:
liuyuanchuang
2025-12-10 18:33:37 +08:00
commit 48e63894eb
2408 changed files with 1053045 additions and 0 deletions

49
pkg/common/errors.go Normal file
View File

@@ -0,0 +1,49 @@
package common
type ErrorCode int
const (
CodeSuccess = 200
CodeParamError = 400
CodeUnauthorized = 401
CodeForbidden = 403
CodeNotFound = 404
CodeInvalidStatus = 405
CodeDBError = 500
CodeSystemError = 501
CodeTaskNotComplete = 1001
CodeRecordRepeat = 1002
CodeSmsCodeError = 1003
)
const (
CodeSuccessMsg = "success"
CodeParamErrorMsg = "param error"
CodeUnauthorizedMsg = "unauthorized"
CodeForbiddenMsg = "forbidden"
CodeNotFoundMsg = "not found"
CodeInvalidStatusMsg = "invalid status"
CodeDBErrorMsg = "database error"
CodeSystemErrorMsg = "system error"
CodeTaskNotCompleteMsg = "task not complete"
CodeRecordRepeatMsg = "record repeat"
CodeSmsCodeErrorMsg = "sms code error"
)
type BusinessError struct {
Code ErrorCode
Message string
Err error
}
func (e *BusinessError) Error() string {
return e.Message
}
func NewError(code ErrorCode, message string, err error) *BusinessError {
return &BusinessError{
Code: code,
Message: message,
Err: err,
}
}

59
pkg/common/middleware.go Normal file
View File

@@ -0,0 +1,59 @@
package common
import (
"context"
"net/http"
"strings"
"gitea.com/bitwsd/document_ai/pkg/constant"
"gitea.com/bitwsd/document_ai/pkg/jwt"
"github.com/gin-gonic/gin"
)
func MiddlewareContext(c *gin.Context) {
c.Set(constant.ContextIP, c.ClientIP())
}
func GetIPFromContext(ctx context.Context) string {
return ctx.Value(constant.ContextIP).(string)
}
func GetUserIDFromContext(ctx *gin.Context) int64 {
return ctx.GetInt64(constant.ContextUserID)
}
func AuthMiddleware(ctx *gin.Context) {
token := ctx.GetHeader("Authorization")
if token == "" {
ctx.JSON(http.StatusOK, ErrorResponse(ctx, CodeUnauthorized, CodeUnauthorizedMsg))
ctx.Abort()
}
token = strings.TrimPrefix(token, "Bearer ")
claims, err := jwt.ParseToken(token)
if err != nil {
ctx.JSON(http.StatusOK, ErrorResponse(ctx, CodeUnauthorized, CodeUnauthorizedMsg))
ctx.Abort()
}
if claims == nil {
ctx.JSON(http.StatusOK, ErrorResponse(ctx, CodeUnauthorized, CodeUnauthorizedMsg))
ctx.Abort()
}
ctx.Set(constant.ContextUserID, claims.UserId)
}
func GetAuthMiddleware() gin.HandlerFunc {
return func(ctx *gin.Context) {
token := ctx.GetHeader("Authorization")
if token != "" {
token = strings.TrimPrefix(token, "Bearer ")
claims, err := jwt.ParseToken(token)
if err == nil {
ctx.Set(constant.ContextUserID, claims.UserId)
}
}
}
}

31
pkg/common/response.go Normal file
View File

@@ -0,0 +1,31 @@
package common
import (
"context"
"gitea.com/bitwsd/document_ai/pkg/constant"
)
type Response struct {
RequestID string `json:"request_id"`
Code int `json:"code"`
Message string `json:"message"`
Data interface{} `json:"data"`
}
func ErrorResponse(ctx context.Context, code int, message string) *Response {
return &Response{
RequestID: ctx.Value(constant.ContextRequestID).(string),
Code: code,
Message: message,
}
}
func SuccessResponse(ctx context.Context, data interface{}) *Response {
return &Response{
RequestID: ctx.Value(constant.ContextRequestID).(string),
Code: CodeSuccess,
Message: "success",
Data: data,
}
}

7
pkg/constant/context.go Normal file
View File

@@ -0,0 +1,7 @@
package constant
const (
ContextRequestID = "request_id"
ContextUserID = "user_id"
ContextIP = "ip"
)

5
pkg/constant/header.go Normal file
View File

@@ -0,0 +1,5 @@
package constant
const (
HeaderRequestID = "X-Request-ID"
)

5
pkg/constant/limit.go Normal file
View File

@@ -0,0 +1,5 @@
package constant
const (
VLMFormulaCount = 20
)

136
pkg/httpclient/client.go Normal file
View File

@@ -0,0 +1,136 @@
package httpclient
import (
"context"
"crypto/tls"
"fmt"
"io"
"math"
"net"
"net/http"
"time"
"gitea.com/bitwsd/core/common/log"
)
// RetryConfig 重试配置
type RetryConfig struct {
MaxRetries int // 最大重试次数
InitialInterval time.Duration // 初始重试间隔
MaxInterval time.Duration // 最大重试间隔
SkipTLSVerify bool // 是否跳过TLS验证
}
// DefaultRetryConfig 默认重试配置
var DefaultRetryConfig = RetryConfig{
MaxRetries: 2,
InitialInterval: 100 * time.Millisecond,
MaxInterval: 5 * time.Second,
SkipTLSVerify: true,
}
// Client HTTP客户端封装
type Client struct {
client *http.Client
config RetryConfig
}
// NewClient 创建新的HTTP客户端
func NewClient(config *RetryConfig) *Client {
cfg := DefaultRetryConfig
if config != nil {
cfg = *config
}
tr := &http.Transport{
TLSClientConfig: &tls.Config{
InsecureSkipVerify: cfg.SkipTLSVerify,
},
}
return &Client{
client: &http.Client{
Transport: tr,
},
config: cfg,
}
}
// RequestWithRetry 执行带重试的HTTP请求
func (c *Client) RequestWithRetry(ctx context.Context, method, url string, body io.Reader, headers map[string]string) (*http.Response, error) {
var lastErr error
for attempt := 0; attempt < c.config.MaxRetries; attempt++ {
if attempt > 0 {
backoff := c.calculateBackoff(attempt)
select {
case <-ctx.Done():
return nil, ctx.Err()
case <-time.After(backoff):
}
// 如果body是可以Seek的则重置到开始位置
if seeker, ok := body.(io.Seeker); ok {
_, err := seeker.Seek(0, io.SeekStart)
if err != nil {
return nil, fmt.Errorf("failed to reset request body: %w", err)
}
}
log.Info(ctx, "func", "RequestWithRetry", "msg", "正在重试请求",
"attempt", attempt+1, "max_retries", c.config.MaxRetries, "backoff", backoff)
}
// 检查 context 是否已经取消
if ctx.Err() != nil {
return nil, ctx.Err()
}
req, err := http.NewRequestWithContext(ctx, method, url, body)
if err != nil {
lastErr = fmt.Errorf("create request failed: %w", err)
continue
}
for k, v := range headers {
req.Header.Set(k, v)
}
resp, err := c.client.Do(req)
if err != nil {
lastErr = fmt.Errorf("request failed: %w", err)
// 如果是超时错误,直接返回不再重试
if netErr, ok := err.(net.Error); ok && netErr.Timeout() {
return nil, fmt.Errorf("request timeout: %w", err)
}
log.Error(ctx, "func", "RequestWithRetry", "msg", "请求失败",
"error", err, "attempt", attempt+1)
continue
}
if resp.StatusCode != http.StatusOK {
body, _ := io.ReadAll(resp.Body)
fmt.Println(string(body))
resp.Body.Close()
lastErr = fmt.Errorf("unexpected status code: %d", resp.StatusCode)
log.Error(ctx, "func", "RequestWithRetry", "msg", "请求返回非200状态码",
"status", resp.StatusCode, "attempt", attempt+1)
continue
}
return resp, nil
}
return nil, fmt.Errorf("max retries reached: %w", lastErr)
}
// calculateBackoff 计算退避时间
func (c *Client) calculateBackoff(attempt int) time.Duration {
backoff := float64(c.config.InitialInterval) * math.Pow(2, float64(attempt))
if backoff > float64(c.config.MaxInterval) {
backoff = float64(c.config.MaxInterval)
}
return time.Duration(backoff)
}

61
pkg/jwt/jwt.go Normal file
View File

@@ -0,0 +1,61 @@
package jwt
import (
"errors"
"time"
"github.com/dgrijalva/jwt-go"
)
var JwtKey = []byte("bitwsd@hello qinshihuang")
var ValidTime = 3600 * 24 * 7
type User struct {
UserId int64 `json:"user_id"`
}
type CustomClaims struct {
User
jwt.StandardClaims
}
func CreateToken(user User) (string, error) {
expire := time.Now().Add(time.Duration(ValidTime) * time.Second)
claims := &CustomClaims{
User: user,
StandardClaims: jwt.StandardClaims{
ExpiresAt: expire.Unix(),
IssuedAt: time.Now().Unix(),
},
}
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
t, err := token.SignedString(JwtKey)
if err != nil {
return "", err
}
return "Bearer " + t, nil
}
func ParseToken(signToken string) (*CustomClaims, error) {
token, err := jwt.ParseWithClaims(signToken, &CustomClaims{}, func(token *jwt.Token) (interface{}, error) {
return JwtKey, nil
})
if err != nil {
if ve, ok := err.(*jwt.ValidationError); ok {
if ve.Errors&jwt.ValidationErrorExpired != 0 {
return nil, errors.New("token expired")
}
}
return nil, err
}
claims, _ := token.Claims.(*CustomClaims)
if claims == nil || !token.Valid {
return nil, errors.New("token invalid")
}
return claims, nil
}

30
pkg/oss/config.go Normal file
View File

@@ -0,0 +1,30 @@
package oss
// var (
//
// AccessKeyId = os.Getenv("OSS_ACCESS_KEY_ID")
// AccessKeySecret = os.Getenv("OSS_ACCESS_KEY_SECRET")
// Host = "http://${your-bucket}.${your-endpoint}"
// UploadDir = "user-dir-prefix/"
// ExpireTime = int64(3600)
// Endpoint = os.Getenv("OSS_ENDPOINT")
// BucketName = os.Getenv("OSS_BUCKET_NAME")
//
// )
const (
ExpireTime = int64(600) // 签名有效期
FormulaDir = "formula/"
)
type ConfigStruct struct {
Expiration string `json:"expiration"`
Conditions [][]interface{} `json:"conditions"`
}
type PolicyToken struct {
AccessKeyId string `json:"ossAccessKeyId"`
Host string `json:"host"`
Signature string `json:"signature"`
Policy string `json:"policy"`
Directory string `json:"dir"`
}

175
pkg/oss/policy.go Normal file
View File

@@ -0,0 +1,175 @@
package oss
import (
"context"
"crypto/hmac"
"crypto/sha1"
"encoding/base64"
"encoding/json"
"fmt"
"io"
"path/filepath"
"strings"
"time"
"gitea.com/bitwsd/core/common/log"
"gitea.com/bitwsd/document_ai/config"
"github.com/aliyun/aliyun-oss-go-sdk/oss"
)
func GetGMTISO8601(expireEnd int64) string {
return time.Unix(expireEnd, 0).UTC().Format("2006-01-02T15:04:05Z")
}
func GetPolicyToken() (string, error) {
now := time.Now().Unix()
expireEnd := now + ExpireTime
tokenExpire := GetGMTISO8601(expireEnd)
conf := ConfigStruct{
Expiration: tokenExpire,
}
// Add file prefix restriction
// config.Conditions = append(config.Conditions, []interface{}{"starts-with", "$key", FormulaDir})
// Add file size restriction (1KB to 10MB)
minSize := int64(1024)
maxSize := int64(3 * 1024 * 1024)
conf.Conditions = append(conf.Conditions, []interface{}{"content-length-range", minSize, maxSize})
result, err := json.Marshal(conf)
if err != nil {
return "", fmt.Errorf("marshal config error: %w", err)
}
encodedResult := base64.StdEncoding.EncodeToString(result)
h := hmac.New(sha1.New, []byte(config.GlobalConfig.Aliyun.OSS.AccessKeySecret))
io.WriteString(h, encodedResult)
signedStr := base64.StdEncoding.EncodeToString(h.Sum(nil))
policyToken := PolicyToken{
AccessKeyId: config.GlobalConfig.Aliyun.OSS.AccessKeyID,
Host: config.GlobalConfig.Aliyun.OSS.Endpoint,
Signature: signedStr,
Policy: encodedResult,
Directory: FormulaDir,
}
response, err := json.Marshal(policyToken)
if err != nil {
return "", fmt.Errorf("marshal policy token error: %w", err)
}
return string(response), nil
}
func GetPolicyURL(ctx context.Context, path string) (string, error) {
// Create OSS client
client, err := oss.New(config.GlobalConfig.Aliyun.OSS.Endpoint, config.GlobalConfig.Aliyun.OSS.AccessKeyID, config.GlobalConfig.Aliyun.OSS.AccessKeySecret)
if err != nil {
log.Error(ctx, "func", "GetPolicyURL", "msg", "create oss client failed", "error", err)
return "", err
}
// Get bucket instance
bucket, err := client.Bucket(config.GlobalConfig.Aliyun.OSS.BucketName)
if err != nil {
log.Error(ctx, "func", "GetPolicyURL", "msg", "get bucket failed", "error", err)
return "", err
}
// Set options for the signed URL
var contentType string
ext := filepath.Ext(path)
switch ext {
case ".jpg", ".jpeg":
contentType = "image/jpeg"
case ".png":
contentType = "image/png"
case ".gif":
contentType = "image/gif"
case ".bmp":
contentType = "image/bmp"
case ".webp":
contentType = "image/webp"
case ".tiff":
contentType = "image/tiff"
case ".svg":
contentType = "image/svg+xml"
default:
return "", fmt.Errorf("unsupported file type: %s", ext)
}
options := []oss.Option{
oss.ContentType(contentType),
}
// Generate signed URL valid for 10 minutes
signedURL, err := bucket.SignURL(path, oss.HTTPPut, ExpireTime, options...)
if err != nil {
log.Error(ctx, "func", "GetPolicyURL", "msg", "sign url failed", "error", err)
return "", err
}
// http 转 https
signedURL = strings.Replace(signedURL, "http://", "https://", 1)
return signedURL, nil
}
// DownloadFile downloads a file from OSS and returns the reader, caller should close the reader
func DownloadFile(ctx context.Context, ossPath string) (io.ReadCloser, error) {
endpoint := config.GlobalConfig.Aliyun.OSS.InnerEndpoint
if config.GlobalConfig.Server.IsDebug() {
endpoint = config.GlobalConfig.Aliyun.OSS.Endpoint
}
// Create OSS client
client, err := oss.New(endpoint,
config.GlobalConfig.Aliyun.OSS.AccessKeyID,
config.GlobalConfig.Aliyun.OSS.AccessKeySecret)
if err != nil {
log.Error(ctx, "func", "DownloadFile", "msg", "create oss client failed", "error", err)
return nil, err
}
// Get bucket instance
bucket, err := client.Bucket(config.GlobalConfig.Aliyun.OSS.BucketName)
if err != nil {
log.Error(ctx, "func", "DownloadFile", "msg", "get bucket failed", "error", err)
return nil, err
}
// Download the file
reader, err := bucket.GetObject(ossPath)
if err != nil {
log.Error(ctx, "func", "DownloadFile", "msg", "download file failed", "ossPath", ossPath, "error", err)
return nil, err
}
return reader, nil
}
func GetDownloadURL(ctx context.Context, ossPath string) (string, error) {
endpoint := config.GlobalConfig.Aliyun.OSS.Endpoint
client, err := oss.New(endpoint, config.GlobalConfig.Aliyun.OSS.AccessKeyID, config.GlobalConfig.Aliyun.OSS.AccessKeySecret)
if err != nil {
log.Error(ctx, "func", "GetDownloadURL", "msg", "create oss client failed", "error", err)
return "", err
}
bucket, err := client.Bucket(config.GlobalConfig.Aliyun.OSS.BucketName)
if err != nil {
log.Error(ctx, "func", "GetDownloadURL", "msg", "get bucket failed", "error", err)
return "", err
}
signURL, err := bucket.SignURL(ossPath, oss.HTTPGet, 60)
if err != nil {
log.Error(ctx, "func", "GetDownloadURL", "msg", "get object failed", "error", err)
return "", err
}
return signURL, nil
}

65
pkg/sms/sms.go Normal file
View File

@@ -0,0 +1,65 @@
package sms
import (
"errors"
"sync"
"gitea.com/bitwsd/document_ai/config"
openapi "github.com/alibabacloud-go/darabonba-openapi/client"
dysmsapi "github.com/alibabacloud-go/dysmsapi-20170525/v2/client"
aliutil "github.com/alibabacloud-go/tea-utils/service"
"github.com/alibabacloud-go/tea/tea"
)
const (
Signature = "北京比特智源科技"
TemplateCode = "SMS_291510729"
TemplateParam = `{"code":"%s"}`
VerifyCodeLength = 6
VerifyCodeExpire = 3 * 60 // 1 minutes
)
var (
MsgClient *dysmsapi.Client
once sync.Once
)
func InitSmsClient() *dysmsapi.Client {
once.Do(func() {
key := tea.String(config.GlobalConfig.Aliyun.Sms.AccessKeyID)
secret := tea.String(config.GlobalConfig.Aliyun.Sms.AccessKeySecret)
config := &openapi.Config{AccessKeyId: key, AccessKeySecret: secret}
client, err := dysmsapi.NewClient(config)
if err != nil {
panic(err)
}
MsgClient = client
})
return MsgClient
}
type SendSmsRequest struct {
PhoneNumbers string
SignName string
TemplateCode string
TemplateParam string
}
func SendMessage(req *SendSmsRequest) (err error) {
client := MsgClient
request := &dysmsapi.SendSmsRequest{
PhoneNumbers: tea.String(req.PhoneNumbers),
SignName: tea.String(req.SignName),
TemplateCode: tea.String(req.TemplateCode),
TemplateParam: tea.String(req.TemplateParam),
}
resp, err := client.SendSms(request)
if err != nil {
return
}
if !tea.BoolValue(aliutil.EqualString(resp.Body.Code, tea.String("OK"))) {
err = errors.New(*resp.Body.Code)
return
}
return
}

21
pkg/utils/arr.go Normal file
View File

@@ -0,0 +1,21 @@
package utils
import "math/rand"
func InArray[T comparable](needle T, haystack []T) bool {
for _, item := range haystack {
if item == needle {
return true
}
}
return false
}
func NewRandNumber(length int) (string, error) {
letters := []byte{'0', '1', '2', '3', '4', '5', '6', '7', '8', '9'}
b := make([]byte, length)
for i := range b {
b[i] = letters[rand.Intn(len(letters))]
}
return string(b), nil
}

29
pkg/utils/context.go Normal file
View File

@@ -0,0 +1,29 @@
package utils
import (
"context"
"github.com/google/uuid"
)
type contextKey string
const RequestIDKey contextKey = "request_id"
const RequestIDHeaderKey = "X-Request-ID"
func NewContextWithRequestID(ctx context.Context, requestID string) context.Context {
newCtx := context.Background()
newCtx = context.WithValue(newCtx, RequestIDKey, requestID)
return newCtx
}
func NewUUID() string {
return uuid.New().String()
}
func GetRequestIDFromContext(ctx context.Context) string {
if requestID, ok := ctx.Value(RequestIDKey).(string); ok {
return requestID
}
return ""
}

115
pkg/utils/katex.go Normal file
View File

@@ -0,0 +1,115 @@
package utils
import (
"regexp"
"strings"
)
// Helper function to change patterns
func changeAll(text, oldIns, newIns, leftDelim, rightDelim, newLeftDelim, newRightDelim string) string {
pattern := regexp.MustCompile(regexp.QuoteMeta(oldIns) + `\s*` + regexp.QuoteMeta(leftDelim) + `(.*?)` + regexp.QuoteMeta(rightDelim))
return pattern.ReplaceAllString(text, newIns+newLeftDelim+"$1"+newRightDelim)
}
// Helper function to remove dollar surroundings
func rmDollarSurr(text string) string {
if strings.HasPrefix(text, "$") && strings.HasSuffix(text, "$") {
return text[1 : len(text)-1]
}
return text
}
// ToKatex converts LaTeX formula to KaTeX compatible format
func ToKatex(formula string) string {
res := formula
// Remove mbox surrounding
res = changeAll(res, `\mbox `, " ", "{", "}", "", "")
res = changeAll(res, `\mbox`, " ", "{", "}", "", "")
// Remove hbox surrounding
hboxPattern := regexp.MustCompile(`\\hbox to ?-? ?\d+\.\d+(pt)?\{`)
res = hboxPattern.ReplaceAllString(res, `\hbox{`)
res = changeAll(res, `\hbox`, " ", "{", "}", "", " ")
// Remove raise surrounding
raisePattern := regexp.MustCompile(`\\raise ?-? ?\d+\.\d+(pt)?`)
res = raisePattern.ReplaceAllString(res, " ")
// Remove makebox
makeboxPattern := regexp.MustCompile(`\\makebox ?\[\d+\.\d+(pt)?\]\{`)
res = makeboxPattern.ReplaceAllString(res, `\makebox{`)
res = changeAll(res, `\makebox`, " ", "{", "}", "", " ")
// Remove vbox, scalebox, raisebox surrounding
raisebox := regexp.MustCompile(`\\raisebox\{-? ?\d+\.\d+(pt)?\}\{`)
scalebox := regexp.MustCompile(`\\scalebox\{-? ?\d+\.\d+(pt)?\}\{`)
res = raisebox.ReplaceAllString(res, `\raisebox{`)
res = scalebox.ReplaceAllString(res, `\scalebox{`)
res = changeAll(res, `\scalebox`, " ", "{", "}", "", " ")
res = changeAll(res, `\raisebox`, " ", "{", "}", "", " ")
res = changeAll(res, `\vbox`, " ", "{", "}", "", " ")
// Handle size instructions
sizeInstructions := []string{
`\Huge`, `\huge`, `\LARGE`, `\Large`, `\large`,
`\normalsize`, `\small`, `\footnotesize`, `\tiny`,
}
for _, ins := range sizeInstructions {
res = changeAll(res, ins, ins, "$", "$", "{", "}")
}
// Handle boldmath
res = changeAll(res, `\boldmath `, `\bm`, "{", "}", "{", "}")
res = changeAll(res, `\boldmath`, `\bm`, "{", "}", "{", "}")
res = changeAll(res, `\boldmath `, `\bm`, "$", "$", "{", "}")
res = changeAll(res, `\boldmath`, `\bm`, "$", "$", "{", "}")
// Handle other instructions
res = changeAll(res, `\scriptsize`, `\scriptsize`, "$", "$", "{", "}")
res = changeAll(res, `\emph`, `\textit`, "{", "}", "{", "}")
res = changeAll(res, `\emph `, `\textit`, "{", "}", "{", "}")
// Handle math delimiters
delimiters := []string{
`\left`, `\middle`, `\right`, `\big`, `\Big`, `\bigg`, `\Bigg`,
`\bigl`, `\Bigl`, `\biggl`, `\Biggl`, `\bigm`, `\Bigm`, `\biggm`,
`\Biggm`, `\bigr`, `\Bigr`, `\biggr`, `\Biggr`,
}
for _, delim := range delimiters {
res = changeAll(res, delim, delim, "{", "}", "", "")
}
// Handle display math
displayMath := regexp.MustCompile(`\\\[(.*?)\\\]`)
res = displayMath.ReplaceAllString(res, "$1\\newline")
res = strings.TrimSuffix(res, `\newline`)
// Remove multiple spaces
spaces := regexp.MustCompile(`(\\,){1,}`)
res = spaces.ReplaceAllString(res, " ")
res = regexp.MustCompile(`(\\!){1,}`).ReplaceAllString(res, " ")
res = regexp.MustCompile(`(\\;){1,}`).ReplaceAllString(res, " ")
res = regexp.MustCompile(`(\\:){1,}`).ReplaceAllString(res, " ")
res = regexp.MustCompile(`\\vspace\{.*?}`).ReplaceAllString(res, "")
// Merge consecutive text
textPattern := regexp.MustCompile(`(\\text\{[^}]*\}\s*){2,}`)
res = textPattern.ReplaceAllStringFunc(res, func(match string) string {
texts := regexp.MustCompile(`\\text\{([^}]*)\}`).FindAllStringSubmatch(match, -1)
var merged strings.Builder
for _, t := range texts {
merged.WriteString(t[1])
}
return `\text{` + merged.String() + "}"
})
res = strings.ReplaceAll(res, `\bf `, "")
res = rmDollarSurr(res)
// Remove extra spaces
res = regexp.MustCompile(` +`).ReplaceAllString(res, " ")
return strings.TrimSpace(res)
}

18
pkg/utils/routine.go Normal file
View File

@@ -0,0 +1,18 @@
package utils
import (
"context"
"gitea.com/bitwsd/core/common/log"
)
func SafeGo(fn func()) {
go func() {
defer func() {
if err := recover(); err != nil {
log.Error(context.Background(), "panic recover", "err", err)
}
}()
fn()
}()
}

30
pkg/utils/sms.go Normal file
View File

@@ -0,0 +1,30 @@
package utils
import "strings"
// 校验手机号
// 规则:
// 1. 长度必须为11位
// 2. 必须以1开头
// 3. 第二位必须是3,4,5,6,7,8,9
// 4. 其余必须都是数字
func ValidatePhone(phone string) bool {
if len(phone) != 11 || !strings.HasPrefix(phone, "1") {
return false
}
// 检查第二位
secondDigit := phone[1]
if secondDigit < '3' || secondDigit > '9' {
return false
}
// 检查剩余数字
for i := 2; i < len(phone); i++ {
if phone[i] < '0' || phone[i] > '9' {
return false
}
}
return true
}

5
pkg/utils/token.go Normal file
View File

@@ -0,0 +1,5 @@
package utils
const (
SiliconFlowToken = "Bearer sk-akbroznlbxikkbiouzasspbbzwgxubnjjtqlujxmxsnvpmhn"
)