init repo
This commit is contained in:
49
pkg/common/errors.go
Normal file
49
pkg/common/errors.go
Normal 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
59
pkg/common/middleware.go
Normal 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
31
pkg/common/response.go
Normal 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
7
pkg/constant/context.go
Normal file
@@ -0,0 +1,7 @@
|
||||
package constant
|
||||
|
||||
const (
|
||||
ContextRequestID = "request_id"
|
||||
ContextUserID = "user_id"
|
||||
ContextIP = "ip"
|
||||
)
|
||||
5
pkg/constant/header.go
Normal file
5
pkg/constant/header.go
Normal file
@@ -0,0 +1,5 @@
|
||||
package constant
|
||||
|
||||
const (
|
||||
HeaderRequestID = "X-Request-ID"
|
||||
)
|
||||
5
pkg/constant/limit.go
Normal file
5
pkg/constant/limit.go
Normal file
@@ -0,0 +1,5 @@
|
||||
package constant
|
||||
|
||||
const (
|
||||
VLMFormulaCount = 20
|
||||
)
|
||||
136
pkg/httpclient/client.go
Normal file
136
pkg/httpclient/client.go
Normal 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
61
pkg/jwt/jwt.go
Normal 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
30
pkg/oss/config.go
Normal 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
175
pkg/oss/policy.go
Normal 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
65
pkg/sms/sms.go
Normal 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
21
pkg/utils/arr.go
Normal 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
29
pkg/utils/context.go
Normal 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
115
pkg/utils/katex.go
Normal 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
18
pkg/utils/routine.go
Normal 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
30
pkg/utils/sms.go
Normal 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
5
pkg/utils/token.go
Normal file
@@ -0,0 +1,5 @@
|
||||
package utils
|
||||
|
||||
const (
|
||||
SiliconFlowToken = "Bearer sk-akbroznlbxikkbiouzasspbbzwgxubnjjtqlujxmxsnvpmhn"
|
||||
)
|
||||
Reference in New Issue
Block a user