Compare commits
14 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 22735aeaae | |||
| dd6eeb5dfe | |||
| ac6409f3e2 | |||
| 36a6932056 | |||
| 72dab0e968 | |||
| b716434f62 | |||
| 8ed7dd77e4 | |||
| ae1c9c695e | |||
| 66995c0c78 | |||
| 120e690795 | |||
| bc2bb25578 | |||
| b37fc5cb24 | |||
| 614dd9917f | |||
| 00bc3432bd |
+8
-7
@@ -1,16 +1,17 @@
|
||||
FROM golang:latest AS backend-builder
|
||||
RUN apt update
|
||||
RUN apt install -y build-essential
|
||||
FROM gocv/opencv:latest AS backend-builder
|
||||
RUN echo "开始安装依赖包..."
|
||||
RUN apt update && apt install -y upx-ucl
|
||||
|
||||
WORKDIR /build
|
||||
COPY . .
|
||||
ENV CGO_ENABLED=1
|
||||
RUN echo "开始打包二进制文件"
|
||||
RUN go build -ldflags="-s -w" -o bin/server
|
||||
|
||||
RUN apt install -y upx-ucl
|
||||
|
||||
RUN echo "开始压缩二进制文件"
|
||||
RUN upx --best --lzma bin/server
|
||||
|
||||
FROM golang:latest
|
||||
FROM gocv/opencv:latest
|
||||
|
||||
COPY --from=backend-builder /build/bin/server /app/server
|
||||
#COPY static /app/static
|
||||
#COPY static/*.so /app/
|
||||
|
||||
@@ -33,6 +33,7 @@ require (
|
||||
github.com/yalue/onnxruntime_go v1.12.1
|
||||
go.uber.org/automaxprocs v1.6.0
|
||||
go.uber.org/zap v1.27.0
|
||||
gocv.io/x/gocv v0.42.0
|
||||
golang.org/x/crypto v0.43.0
|
||||
gorm.io/driver/postgres v1.6.0
|
||||
gorm.io/gorm v1.31.1
|
||||
|
||||
@@ -244,6 +244,8 @@ go.uber.org/multierr v1.10.0 h1:S0h4aNzvfcFsC3dRF1jLoaov7oRaKqRGC/pUEJ2yvPQ=
|
||||
go.uber.org/multierr v1.10.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
|
||||
go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8=
|
||||
go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E=
|
||||
gocv.io/x/gocv v0.42.0 h1:AAsrFJH2aIsQHukkCovWqj0MCGZleQpVyf5gNVRXjQI=
|
||||
gocv.io/x/gocv v0.42.0/go.mod h1:zYdWMj29WAEznM3Y8NsU3A0TRq/wR/cy75jeUypThqU=
|
||||
golang.org/x/arch v0.0.0-20210923205945-b76863e36670 h1:18EFjUmQOcUvxNYSkA6jO9VAiXCnxFY6NyDX0bHDmkU=
|
||||
golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
|
||||
@@ -1,225 +0,0 @@
|
||||
package classifier
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"fmt"
|
||||
_ "image/jpeg"
|
||||
_ "image/png"
|
||||
"likedream-goframe/internal/framework/logger"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"strings"
|
||||
|
||||
ort "github.com/yalue/onnxruntime_go"
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
type ImageClassifier struct {
|
||||
session *ort.AdvancedSession
|
||||
inputShape []int64
|
||||
classes []string
|
||||
inputTensor *ort.Tensor[float32]
|
||||
outputTensor *ort.Tensor[float32]
|
||||
}
|
||||
|
||||
// NewImageClassifier 创建新的图片分类器
|
||||
func NewImageClassifier(modelPath, classesPath string) (*ImageClassifier, error) {
|
||||
// 初始化ONNX Runtime
|
||||
exePath := strings.ToLower(os.Args[0])
|
||||
// 获取绝对路径
|
||||
absPath, err := filepath.Abs(exePath)
|
||||
dir := filepath.Dir(absPath)
|
||||
systemOS := runtime.GOOS
|
||||
switch systemOS {
|
||||
case "windows":
|
||||
dir = filepath.Join(dir, "onnxruntime.dll")
|
||||
break
|
||||
case "darwin":
|
||||
dir = filepath.Join(dir, "onnxruntime.dylib")
|
||||
break
|
||||
case "linux":
|
||||
dir = filepath.Join(dir, "onnxruntime.so")
|
||||
break
|
||||
}
|
||||
ort.SetSharedLibraryPath(dir)
|
||||
err = ort.InitializeEnvironment()
|
||||
if err != nil {
|
||||
logger.Logger.Error("初始化onnx:", zap.String("绝对路径", absPath), zap.String("父文件夹", dir))
|
||||
return nil, fmt.Errorf("初始化ONNX Runtime失败: %v", err)
|
||||
}
|
||||
|
||||
// 加载类别名称
|
||||
classes, err := loadClasses(classesPath)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("加载类别失败: %v", err)
|
||||
}
|
||||
|
||||
// 创建session选项
|
||||
options, err := ort.NewSessionOptions()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("创建session选项失败: %v", err)
|
||||
}
|
||||
defer options.Destroy()
|
||||
|
||||
// 创建输入输出tensor
|
||||
inputShape := ort.NewShape(1, 3, 224, 224)
|
||||
outputShape := ort.NewShape(1, int64(len(classes)))
|
||||
|
||||
inputTensor, err := ort.NewEmptyTensor[float32](inputShape)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("创建输入tensor失败: %v", err)
|
||||
}
|
||||
|
||||
outputTensor, err := ort.NewEmptyTensor[float32](outputShape)
|
||||
if err != nil {
|
||||
inputTensor.Destroy()
|
||||
return nil, fmt.Errorf("创建输出tensor失败: %v", err)
|
||||
}
|
||||
|
||||
// 加载模型
|
||||
session, err := ort.NewAdvancedSession(modelPath,
|
||||
[]string{"input"}, []string{"output"},
|
||||
[]ort.ArbitraryTensor{inputTensor}, []ort.ArbitraryTensor{outputTensor},
|
||||
options)
|
||||
if err != nil {
|
||||
inputTensor.Destroy()
|
||||
outputTensor.Destroy()
|
||||
return nil, fmt.Errorf("加载模型失败: %v", err)
|
||||
}
|
||||
|
||||
return &ImageClassifier{
|
||||
session: session,
|
||||
inputShape: []int64{1, 3, 224, 224},
|
||||
classes: classes,
|
||||
inputTensor: inputTensor,
|
||||
outputTensor: outputTensor,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// loadClasses 从文件加载类别名称
|
||||
func loadClasses(path string) ([]string, error) {
|
||||
file, err := os.Open(path)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
var classes []string
|
||||
scanner := bufio.NewScanner(file)
|
||||
for scanner.Scan() {
|
||||
classes = append(classes, scanner.Text())
|
||||
}
|
||||
|
||||
if err := scanner.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return classes, nil
|
||||
}
|
||||
|
||||
// Predict 预测图片类别
|
||||
func (ic *ImageClassifier) Predict(imgPath string) (string, float32, error) {
|
||||
// 加载并预处理图片
|
||||
inputData, err := ic.preprocessImage(imgPath)
|
||||
if err != nil {
|
||||
return "", 0, fmt.Errorf("预处理图片失败: %v", err)
|
||||
}
|
||||
|
||||
// 设置输入数据
|
||||
copy(ic.inputTensor.GetData(), inputData)
|
||||
|
||||
// 运行推理
|
||||
err = ic.session.Run()
|
||||
if err != nil {
|
||||
return "", 0, fmt.Errorf("推理失败: %v", err)
|
||||
}
|
||||
|
||||
// 获取输出数据
|
||||
outputData := ic.outputTensor.GetData()
|
||||
|
||||
// 找到最大概率的类别
|
||||
maxIdx := 0
|
||||
maxProb := outputData[0]
|
||||
for i, prob := range outputData {
|
||||
if prob > maxProb {
|
||||
maxProb = prob
|
||||
maxIdx = i
|
||||
}
|
||||
}
|
||||
|
||||
// 应用softmax获取概率
|
||||
expSum := float32(0)
|
||||
for _, v := range outputData {
|
||||
expSum += exp(v)
|
||||
}
|
||||
probability := exp(outputData[maxIdx]) / expSum
|
||||
|
||||
return ic.classes[maxIdx], probability, nil
|
||||
}
|
||||
|
||||
// PredictFromBytes 从字节数据预测图片类别
|
||||
func (ic *ImageClassifier) PredictFromBytes(imgData []byte) (string, float32, error) {
|
||||
// 预处理图片
|
||||
inputData, err := ic.preprocessImageBytes(imgData)
|
||||
if err != nil {
|
||||
return "", 0, fmt.Errorf("预处理图片失败: %v", err)
|
||||
}
|
||||
|
||||
// 设置输入数据
|
||||
copy(ic.inputTensor.GetData(), inputData)
|
||||
|
||||
// 运行推理
|
||||
err = ic.session.Run()
|
||||
if err != nil {
|
||||
return "", 0, fmt.Errorf("推理失败: %v", err)
|
||||
}
|
||||
|
||||
// 获取输出数据
|
||||
outputData := ic.outputTensor.GetData()
|
||||
|
||||
// 找到最大概率的类别
|
||||
maxIdx := 0
|
||||
maxProb := outputData[0]
|
||||
for i, prob := range outputData {
|
||||
if prob > maxProb {
|
||||
maxProb = prob
|
||||
maxIdx = i
|
||||
}
|
||||
}
|
||||
|
||||
// 应用softmax获取概率
|
||||
expSum := float32(0)
|
||||
for _, v := range outputData {
|
||||
expSum += exp(v)
|
||||
}
|
||||
probability := exp(outputData[maxIdx]) / expSum
|
||||
|
||||
return ic.classes[maxIdx], probability, nil
|
||||
}
|
||||
|
||||
// Close 关闭分类器
|
||||
func (ic *ImageClassifier) Close() error {
|
||||
if ic.inputTensor != nil {
|
||||
ic.inputTensor.Destroy()
|
||||
}
|
||||
if ic.outputTensor != nil {
|
||||
ic.outputTensor.Destroy()
|
||||
}
|
||||
if ic.session != nil {
|
||||
ic.session.Destroy()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// exp 计算e的x次方(简单实现)
|
||||
func exp(x float32) float32 {
|
||||
// 使用泰勒级数近似
|
||||
result := float32(1.0)
|
||||
term := float32(1.0)
|
||||
for i := 1; i < 20; i++ {
|
||||
term *= x / float32(i)
|
||||
result += term
|
||||
}
|
||||
return result
|
||||
}
|
||||
@@ -1,120 +0,0 @@
|
||||
package classifier
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"image"
|
||||
"os"
|
||||
)
|
||||
|
||||
// preprocessImage 预处理图片文件
|
||||
func (ic *ImageClassifier) preprocessImage(imgPath string) ([]float32, error) {
|
||||
file, err := os.Open(imgPath)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
img, _, err := image.Decode(file)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return ic.processImage(img)
|
||||
}
|
||||
|
||||
// preprocessImageBytes 预处理图片字节数据
|
||||
func (ic *ImageClassifier) preprocessImageBytes(imgData []byte) ([]float32, error) {
|
||||
img, _, err := image.Decode(bytes.NewReader(imgData))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return ic.processImage(img)
|
||||
}
|
||||
|
||||
// processImage 处理图片为模型输入格式
|
||||
func (ic *ImageClassifier) processImage(img image.Image) ([]float32, error) {
|
||||
// 调整图片大小到224x224
|
||||
resized := resizeImage(img, 224, 224)
|
||||
|
||||
// 转换为float32数组,并进行归一化
|
||||
// ImageNet的均值和标准差
|
||||
mean := []float32{0.485, 0.456, 0.406}
|
||||
std := []float32{0.229, 0.224, 0.225}
|
||||
|
||||
data := make([]float32, 3*224*224)
|
||||
|
||||
for y := 0; y < 224; y++ {
|
||||
for x := 0; x < 224; x++ {
|
||||
r, g, b, _ := resized.At(x, y).RGBA()
|
||||
|
||||
// 转换为0-1范围
|
||||
rNorm := float32(r) / 65535.0
|
||||
gNorm := float32(g) / 65535.0
|
||||
bNorm := float32(b) / 65535.0
|
||||
|
||||
// 应用归一化
|
||||
idx := y*224 + x
|
||||
data[idx] = (rNorm - mean[0]) / std[0] // R通道
|
||||
data[224*224+idx] = (gNorm - mean[1]) / std[1] // G通道
|
||||
data[2*224*224+idx] = (bNorm - mean[2]) / std[2] // B通道
|
||||
}
|
||||
}
|
||||
|
||||
return data, nil
|
||||
}
|
||||
|
||||
// resizeImage 调整图片大小
|
||||
func resizeImage(img image.Image, width, height int) image.Image {
|
||||
bounds := img.Bounds()
|
||||
srcW := bounds.Dx()
|
||||
srcH := bounds.Dy()
|
||||
|
||||
dst := image.NewRGBA(image.Rect(0, 0, width, height))
|
||||
|
||||
for y := 0; y < height; y++ {
|
||||
for x := 0; x < width; x++ {
|
||||
// 简单的最近邻插值
|
||||
srcX := x * srcW / width
|
||||
srcY := y * srcH / height
|
||||
|
||||
if srcX >= srcW {
|
||||
srcX = srcW - 1
|
||||
}
|
||||
if srcY >= srcH {
|
||||
srcY = srcH - 1
|
||||
}
|
||||
|
||||
dst.Set(x, y, img.At(srcX, srcY))
|
||||
}
|
||||
}
|
||||
|
||||
return dst
|
||||
}
|
||||
|
||||
// centerCrop 中心裁剪图片
|
||||
func centerCrop(img image.Image, size int) image.Image {
|
||||
bounds := img.Bounds()
|
||||
width := bounds.Dx()
|
||||
height := bounds.Dy()
|
||||
|
||||
var x0, y0 int
|
||||
if width > height {
|
||||
x0 = (width - height) / 2
|
||||
y0 = 0
|
||||
width = height
|
||||
} else {
|
||||
x0 = 0
|
||||
y0 = (height - width) / 2
|
||||
height = width
|
||||
}
|
||||
|
||||
cropped := image.NewRGBA(image.Rect(0, 0, width, height))
|
||||
for y := 0; y < height; y++ {
|
||||
for x := 0; x < width; x++ {
|
||||
cropped.Set(x, y, img.At(x0+x, y0+y))
|
||||
}
|
||||
}
|
||||
|
||||
return resizeImage(cropped, size, size)
|
||||
}
|
||||
@@ -1,17 +0,0 @@
|
||||
package classifier
|
||||
|
||||
import "likedream-goframe/internal/utils"
|
||||
|
||||
var imageClassifier, _ = NewImageClassifier("static/model.onnx", "static/classes.txt")
|
||||
|
||||
// 图片数据 & 类别名称
|
||||
// return 是否匹配
|
||||
func ClassifyImage(imgData []byte, className string) bool {
|
||||
if imageClassifier == nil {
|
||||
c, err := NewImageClassifier("static/model.onnx", "static/classes.txt")
|
||||
utils.OkOrPanic(err)
|
||||
imageClassifier = c
|
||||
}
|
||||
name, _, _ := imageClassifier.PredictFromBytes(imgData)
|
||||
return className == name
|
||||
}
|
||||
@@ -16,10 +16,12 @@ import (
|
||||
"net/http"
|
||||
"net/url"
|
||||
"regexp"
|
||||
"slices"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/PuerkitoBio/goquery"
|
||||
"github.com/avast/retry-go/v4"
|
||||
"github.com/deatil/go-cryptobin/cryptobin/crypto"
|
||||
"github.com/samber/oops"
|
||||
"github.com/tidwall/gjson"
|
||||
@@ -188,20 +190,35 @@ func (c *CpdailyCasLogin) Login(ctx context.Context, schoolDetail *school.School
|
||||
} else {
|
||||
allInputs["password"] = encryptCasPassword(password, salt)
|
||||
}
|
||||
|
||||
var SliderCaptchaIds = []string{"1018764144934965"}
|
||||
// 判断是否需要验证码
|
||||
if isCasNeedCaptcha(ctx, loginResponseUrl, loginType, username, c.client) {
|
||||
// 解决验证码
|
||||
captchaUrl := getCasCaptchaUrl(loginResponseUrl, loginType)
|
||||
// 如果是滑块验证码
|
||||
if slices.Contains(SliderCaptchaIds, schoolDetail.Id) {
|
||||
e := retry.Do(func() error {
|
||||
return utils.Catch(func() {
|
||||
captchaUrl := getCasSliderCaptchaUrl(loginResponseUrl)
|
||||
verifyUrl := getCasSliderVerifyUrl(loginResponseUrl)
|
||||
ResolveCasSliderCaptcha(ctx, captchaUrl, verifyUrl, c.client)
|
||||
})
|
||||
}, retry.MaxDelay(15*time.Second), retry.Attempts(5), retry.Context(ctx))
|
||||
if e != nil {
|
||||
utils.OkOrPanic(errors.NewBusinessError(503, "滑块验证码解析失败").WithInnerError(e), oops.WithContext(ctx).Wrap)
|
||||
}
|
||||
} else {
|
||||
// 解决常规图片验证码
|
||||
captchaUrl := getCasCaptchaUrl(loginResponseUrl, loginType)
|
||||
|
||||
captchaResult := ResolveOcrCaptcha(ctx, captchaUrl, c.client)
|
||||
captchaResult := ResolveOcrCaptcha(ctx, captchaUrl, c.client)
|
||||
|
||||
switch loginType {
|
||||
case 0:
|
||||
allInputs["captchaResponse"] = captchaResult
|
||||
default:
|
||||
allInputs["captcha"] = captchaResult
|
||||
switch loginType {
|
||||
case 0:
|
||||
allInputs["captchaResponse"] = captchaResult
|
||||
default:
|
||||
allInputs["captcha"] = captchaResult
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
loginPostResponse, err := c.client.R().
|
||||
@@ -529,3 +546,12 @@ func getCasCaptchaUrl(url *url.URL, loginType int) string {
|
||||
return fmt.Sprintf("%s://%s/authserver/getCaptcha.htl", url.Scheme, url.Host)
|
||||
}
|
||||
}
|
||||
|
||||
func getCasSliderCaptchaUrl(url *url.URL) string {
|
||||
// /authserver/common/openSliderCaptcha.htl?_=1762873725346
|
||||
return fmt.Sprintf("%s://%s/authserver/common/openSliderCaptcha.htl", url.Scheme, url.Host)
|
||||
}
|
||||
|
||||
func getCasSliderVerifyUrl(url *url.URL) string {
|
||||
return fmt.Sprintf("%s://%s/authserver/common/verifySliderCaptcha.htl", url.Scheme, url.Host)
|
||||
}
|
||||
|
||||
@@ -2,71 +2,53 @@ package login
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"likedream-goframe/internal/application/httpclient"
|
||||
"likedream-goframe/internal/common/cst"
|
||||
"likedream-goframe/internal/application/recognizer/ddddocr"
|
||||
"likedream-goframe/internal/framework/logger"
|
||||
"likedream-goframe/internal/utils"
|
||||
"os"
|
||||
|
||||
"github.com/gogf/gf/v2/frame/g"
|
||||
"github.com/samber/oops"
|
||||
"github.com/tidwall/gjson"
|
||||
"go.uber.org/zap"
|
||||
"resty.dev/v3"
|
||||
)
|
||||
|
||||
func ResolveOcrCaptcha(ctx context.Context, url string, client *resty.Client) string {
|
||||
captchaResponse, err := client.R().
|
||||
WithContext(ctx).
|
||||
Get(url)
|
||||
|
||||
utils.OkOrPanic(err, func(err error) error {
|
||||
return oops.
|
||||
With("captchaUrl", url).
|
||||
WithContext(ctx).
|
||||
Wrap(err)
|
||||
})
|
||||
|
||||
defer func() {
|
||||
_ = captchaResponse.Body.Close()
|
||||
}()
|
||||
|
||||
ocrUrl := g.Cfg().MustGetWithEnv(ctx, cst.ConfigOcrServerUrl).String()
|
||||
|
||||
ocrResponse, err := httpclient.Common.R().
|
||||
SetMultipartFields(&resty.MultipartField{
|
||||
Name: "file",
|
||||
FileName: "captcha.jpg",
|
||||
ContentType: captchaResponse.Header().Get("Content-Type"),
|
||||
Reader: captchaResponse.Body,
|
||||
}).
|
||||
WithContext(ctx).
|
||||
Post(ocrUrl + "/ocr")
|
||||
|
||||
utils.OkOrPanic(err, func(err error) error {
|
||||
return oops.
|
||||
With("captchaUrl", url).
|
||||
WithContext(ctx).
|
||||
Wrap(err)
|
||||
})
|
||||
|
||||
defer func() {
|
||||
_ = ocrResponse.Body.Close()
|
||||
}()
|
||||
|
||||
responseBytes, err := io.ReadAll(ocrResponse.Body)
|
||||
utils.OkOrPanic(err)
|
||||
|
||||
responseJSON := gjson.ParseBytes(responseBytes)
|
||||
|
||||
data := responseJSON.Get("data").String()
|
||||
|
||||
if data == "" {
|
||||
utils.OkOrPanic(oops.
|
||||
In("ResolveOcrCaptcha").
|
||||
With("captchaUrl", url).
|
||||
With("message", responseJSON.Get("message").String()).
|
||||
WithContext(ctx).
|
||||
New("OCR服务返回数据为空"))
|
||||
}
|
||||
|
||||
codePath := DownCode2File(ctx, url, client)
|
||||
img, _ := utils.ReadImg(codePath) //读取验证码图片
|
||||
data := ddddocr.AutoOCRVerification(img)
|
||||
return data
|
||||
}
|
||||
|
||||
func DownCode2File(ctx context.Context, url string, client *resty.Client) string {
|
||||
res, err := client.R().
|
||||
WithContext(ctx).
|
||||
Get(url)
|
||||
utils.OkOrPanic(err, func(err error) error {
|
||||
return oops.
|
||||
With("captchaUrl", url).
|
||||
WithContext(ctx).
|
||||
Wrap(err)
|
||||
})
|
||||
defer res.Body.Close()
|
||||
|
||||
codeFileName := "code" + utils.GenerateRandomString(16) //生成验证码文件名称
|
||||
codeFileName += ".png"
|
||||
utils.PathExistForCreate("./assets/code/") //检测是否存在路径,如果不存在则创建
|
||||
filepath := fmt.Sprintf("./assets/code/%s", codeFileName)
|
||||
file, err := os.Create(filepath)
|
||||
if err != nil {
|
||||
logger.Logger.Error("生成验证码失败", zap.Error(err))
|
||||
return ""
|
||||
}
|
||||
|
||||
_, err = io.Copy(file, res.Body)
|
||||
if err != nil {
|
||||
logger.Logger.Error("生成验证码失败", zap.Error(err))
|
||||
return ""
|
||||
}
|
||||
|
||||
file.Close()
|
||||
return filepath
|
||||
}
|
||||
|
||||
@@ -0,0 +1,67 @@
|
||||
package login
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"likedream-goframe/internal/application/recognizer/slider"
|
||||
"likedream-goframe/internal/common/errors"
|
||||
"likedream-goframe/internal/utils"
|
||||
|
||||
"github.com/bytedance/sonic"
|
||||
"github.com/samber/oops"
|
||||
"resty.dev/v3"
|
||||
)
|
||||
|
||||
type CasSliderCaptchaReponse struct {
|
||||
BigImage string `json:"bigImage"`
|
||||
SmallImage string `json:"smallImage"`
|
||||
TagWidth int `json:"tagWidth"`
|
||||
YHeight int `json:"yHeight"`
|
||||
}
|
||||
|
||||
type CasSliderResult struct {
|
||||
ErrorCode int `json:"errorCode"`
|
||||
ErrorMsg string `json:"errorMsg"`
|
||||
}
|
||||
|
||||
func ResolveCasSliderCaptcha(ctx context.Context, captchaUrl string, verifyUrl string, client *resty.Client) {
|
||||
res, err := client.R().
|
||||
WithContext(ctx).
|
||||
Get(captchaUrl)
|
||||
utils.OkOrPanic(err, func(err error) error {
|
||||
return oops.
|
||||
With("captchaUrl", captchaUrl).
|
||||
WithContext(ctx).
|
||||
Wrap(err)
|
||||
})
|
||||
defer res.Body.Close()
|
||||
bytes, _ := io.ReadAll(res.Body)
|
||||
var captcha CasSliderCaptchaReponse
|
||||
sonic.Unmarshal(bytes, &captcha)
|
||||
width := slider.VerifySliderWidthByBase64(captcha.BigImage, captcha.SmallImage)
|
||||
res2, err2 := client.R().
|
||||
WithContext(ctx).
|
||||
SetFormData(map[string]string{
|
||||
"canvasLength": "280",
|
||||
"moveLength": fmt.Sprintf("%d", width),
|
||||
}).
|
||||
Post(verifyUrl)
|
||||
utils.OkOrPanic(err2, func(err error) error {
|
||||
return oops.
|
||||
With("verifyUrl", verifyUrl).
|
||||
WithContext(ctx).
|
||||
Wrap(err)
|
||||
})
|
||||
defer res2.Body.Close()
|
||||
bytes, _ = io.ReadAll(res2.Body)
|
||||
str := string(bytes)
|
||||
if len(str) > 0 {
|
||||
var result CasSliderResult
|
||||
sonic.Unmarshal(bytes, &result)
|
||||
if result.ErrorCode != 1 {
|
||||
utils.OkOrPanic(oops.WithContext(ctx).Wrap(errors.NewBusinessError(500, "滑块解析错误")))
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -354,7 +354,7 @@ func (c *CpdailyCollectionTask) ParseFormJson(ctx context.Context, rawJson map[s
|
||||
return list
|
||||
}
|
||||
|
||||
func (c *CpdailyCollectionTask) FillCustomForm(ctx context.Context, form map[string]*gvar.Var, paramList []*formtype.CpTaskFormItem, deviceId, username string) map[string]*gvar.Var {
|
||||
func (c *CpdailyCollectionTask) FillCustomForm(ctx context.Context, form map[string]*gvar.Var, paramList []*formtype.CpTaskFormItem, deviceId, imgId string) map[string]*gvar.Var {
|
||||
taskInfo := form["taskInfo"].MapStrVar()
|
||||
rows := form["rows"].Array()
|
||||
filledForm := make([]map[string]*gvar.Var, 0)
|
||||
|
||||
@@ -12,6 +12,6 @@ type CpdailyTask interface {
|
||||
QueryFormByTitle(ctx context.Context, titleGlob string) map[string]*gvar.Var
|
||||
QueryFormByYearMonth(ctx context.Context, titleGlob string, year, month string) map[string]*gvar.Var
|
||||
ParseFormJson(ctx context.Context, rawJson map[string]*gvar.Var) []*formtype.CpTaskFormItem
|
||||
FillCustomForm(ctx context.Context, form map[string]*gvar.Var, paramList []*formtype.CpTaskFormItem, deviceId, username string) map[string]*gvar.Var
|
||||
FillCustomForm(ctx context.Context, form map[string]*gvar.Var, paramList []*formtype.CpTaskFormItem, deviceId, imgId string) map[string]*gvar.Var
|
||||
Submit(ctx context.Context, form map[string]*gvar.Var, username, deviceId string)
|
||||
}
|
||||
|
||||
@@ -12,7 +12,6 @@ import (
|
||||
"likedream-goframe/internal/application/cpdaily/types"
|
||||
"likedream-goframe/internal/common/errors"
|
||||
"likedream-goframe/internal/utils"
|
||||
"math/rand/v2"
|
||||
"slices"
|
||||
"strings"
|
||||
"time"
|
||||
@@ -384,18 +383,14 @@ func (c *CpdailyAutoSignTask) ParseFormJson(ctx context.Context, rawJson map[str
|
||||
}
|
||||
list = append(list, dto)
|
||||
|
||||
var current uint = 1
|
||||
|
||||
if isPhoto := rows["isPhoto"]; isPhoto != nil && isPhoto.Int() == 1 {
|
||||
dto = &formtype.CpTaskFormItem{
|
||||
Title: "签到照片",
|
||||
ColName: "signImg",
|
||||
Type: formtype.ItemTypeImage,
|
||||
Required: true,
|
||||
Number: current,
|
||||
}
|
||||
list = append(list, dto)
|
||||
current++
|
||||
}
|
||||
|
||||
// 是否范围外签到
|
||||
@@ -404,7 +399,6 @@ func (c *CpdailyAutoSignTask) ParseFormJson(ctx context.Context, rawJson map[str
|
||||
ColName: "isMalposition",
|
||||
Type: formtype.ItemTypeSingle,
|
||||
Required: true,
|
||||
Number: current,
|
||||
Options: []formtype.CpTaskFormItemOption{
|
||||
{
|
||||
Content: "是",
|
||||
@@ -418,7 +412,6 @@ func (c *CpdailyAutoSignTask) ParseFormJson(ctx context.Context, rawJson map[str
|
||||
},
|
||||
}
|
||||
list = append(list, dto)
|
||||
current++
|
||||
|
||||
// 范围外签到原因
|
||||
dto = &formtype.CpTaskFormItem{
|
||||
@@ -427,16 +420,15 @@ func (c *CpdailyAutoSignTask) ParseFormJson(ctx context.Context, rawJson map[str
|
||||
Type: formtype.ItemTypeInput,
|
||||
Required: true,
|
||||
LogicWid: "abnormalReason",
|
||||
Number: current,
|
||||
}
|
||||
list = append(list, dto)
|
||||
current++
|
||||
|
||||
current := uint(1)
|
||||
// 处理额外字段
|
||||
if isNeedExtra := rows["isNeedExtra"]; isNeedExtra != nil && isNeedExtra.Int() == 1 {
|
||||
extraField := rows["extraField"].Array()
|
||||
for i, field := range extraField {
|
||||
fieldObj := field.(map[string]*gvar.Var)
|
||||
for _, field := range extraField {
|
||||
fieldObj := gvar.New(field).MapStrVar()
|
||||
title := fieldObj["title"].String()
|
||||
isRequired := fieldObj["isRequired"].Bool()
|
||||
hasOtherItems := fieldObj["hasOtherItems"].Bool()
|
||||
@@ -445,16 +437,17 @@ func (c *CpdailyAutoSignTask) ParseFormJson(ctx context.Context, rawJson map[str
|
||||
Title: title,
|
||||
ColName: title,
|
||||
Type: formtype.ItemTypeSingle,
|
||||
Number: uint(i),
|
||||
Number: current,
|
||||
Required: isRequired,
|
||||
IsOther: hasOtherItems,
|
||||
}
|
||||
current++
|
||||
|
||||
// 处理选项
|
||||
extraFieldItems := fieldObj["extraFieldItems"].Array()
|
||||
var options []formtype.CpTaskFormItemOption
|
||||
for _, item := range extraFieldItems {
|
||||
itemObj := item.(map[string]*gvar.Var)
|
||||
itemObj := gvar.New(item).MapStrVar()
|
||||
option := formtype.CpTaskFormItemOption{
|
||||
Content: itemObj["content"].String(),
|
||||
IsOtherItems: itemObj["isOtherItems"].Bool(),
|
||||
@@ -473,7 +466,7 @@ func (c *CpdailyAutoSignTask) ParseFormJson(ctx context.Context, rawJson map[str
|
||||
return list
|
||||
}
|
||||
|
||||
func (c *CpdailyAutoSignTask) FillCustomForm(ctx context.Context, form map[string]*gvar.Var, paramList []*formtype.CpTaskFormItem, deviceId, username string) map[string]*gvar.Var {
|
||||
func (c *CpdailyAutoSignTask) FillCustomForm(ctx context.Context, form map[string]*gvar.Var, paramList []*formtype.CpTaskFormItem, deviceId, imgId string) map[string]*gvar.Var {
|
||||
var resultForm map[string]*gvar.Var
|
||||
|
||||
// 填充必要变量
|
||||
@@ -514,27 +507,11 @@ func (c *CpdailyAutoSignTask) FillCustomForm(ctx context.Context, form map[strin
|
||||
utils.OkOrPanic(errors.NewBusinessError(400, "签到需要图片,但是当前图片为空"), oops.WithContext(ctx).Wrap)
|
||||
}
|
||||
|
||||
imageUuidSeq := strings.SplitSeq(targetParam.Content, ",")
|
||||
|
||||
var imageUuids []string
|
||||
|
||||
for imageUuid := range imageUuidSeq {
|
||||
imageUuid = strings.TrimSpace(imageUuid)
|
||||
if imageUuid == "" {
|
||||
continue
|
||||
}
|
||||
|
||||
imageUuids = append(imageUuids, imageUuid)
|
||||
}
|
||||
|
||||
if len(imageUuids) == 0 {
|
||||
if imgId == "" {
|
||||
utils.OkOrPanic(errors.NewBusinessError(400, "签到照片未上传"), oops.WithContext(ctx).Wrap)
|
||||
}
|
||||
|
||||
// 随机选择一张照片
|
||||
imageUuid := imageUuids[rand.IntN(len(imageUuids))]
|
||||
|
||||
imageId, e := uuid.Parse(imageUuid)
|
||||
imageId, e := uuid.Parse(imgId)
|
||||
utils.OkOrPanic(e, oops.WithContext(ctx).Wrap)
|
||||
|
||||
previewUrl := imageuploader.UploadImageFromStorage(ctx, c.host, imageId, types.TaskTypeSign, c.client)
|
||||
@@ -554,7 +531,9 @@ func (c *CpdailyAutoSignTask) FillCustomForm(ctx context.Context, form map[strin
|
||||
ticket = submitCaptchaResolver.GetCaptchaTicket(ctx, captchaInfo)
|
||||
})
|
||||
}, retry.MaxDelay(15*time.Second), retry.Attempts(5), retry.Context(ctx))
|
||||
utils.OkOrPanic(errors.NewBusinessError(503, "提交验证码识别失败").WithInnerError(e), oops.WithContext(ctx).Wrap)
|
||||
if e != nil {
|
||||
utils.OkOrPanic(errors.NewBusinessError(503, "提交验证码识别失败").WithInnerError(e), oops.WithContext(ctx).Wrap)
|
||||
}
|
||||
|
||||
resultForm["ticket"] = gvar.New(ticket)
|
||||
} else {
|
||||
@@ -577,8 +556,9 @@ func (c *CpdailyAutoSignTask) FillCustomForm(ctx context.Context, form map[strin
|
||||
var extraFieldItemValues []map[string]*gvar.Var
|
||||
|
||||
for i, field := range extraField {
|
||||
current := uint(i + 1)
|
||||
// 检查数字是否在范围内
|
||||
found := slices.Contains(numbers, uint(i))
|
||||
found := slices.Contains(numbers, current)
|
||||
if !found {
|
||||
utils.OkOrPanic(errors.NewBusinessError(500, "非法的额外信息数据"), oops.WithContext(ctx).Wrap)
|
||||
}
|
||||
@@ -586,7 +566,7 @@ func (c *CpdailyAutoSignTask) FillCustomForm(ctx context.Context, form map[strin
|
||||
// 找到对应的参数
|
||||
var targetParam *formtype.CpTaskFormItem
|
||||
for _, v := range paramList {
|
||||
if v.Number == uint(i) {
|
||||
if v.Number == current {
|
||||
targetParam = v
|
||||
break
|
||||
}
|
||||
@@ -596,7 +576,7 @@ func (c *CpdailyAutoSignTask) FillCustomForm(ctx context.Context, form map[strin
|
||||
continue
|
||||
}
|
||||
|
||||
fieldObj := field.(map[string]*gvar.Var)
|
||||
fieldObj := gvar.New(field).MapStrVar()
|
||||
fieldTitle := fieldObj["title"].String()
|
||||
|
||||
if targetParam.Title != fieldTitle {
|
||||
@@ -607,7 +587,7 @@ func (c *CpdailyAutoSignTask) FillCustomForm(ctx context.Context, form map[strin
|
||||
var extraFieldItemValue map[string]*gvar.Var
|
||||
|
||||
for _, item := range extraFieldItems {
|
||||
itemObj := item.(map[string]*gvar.Var)
|
||||
itemObj := gvar.New(item).MapStrVar()
|
||||
content := itemObj["content"].String()
|
||||
|
||||
if content == targetParam.Content {
|
||||
|
||||
@@ -13,7 +13,6 @@ import (
|
||||
"likedream-goframe/internal/common/cst"
|
||||
"likedream-goframe/internal/common/errors"
|
||||
"likedream-goframe/internal/utils"
|
||||
"math/rand/v2"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
@@ -463,18 +462,15 @@ func (c *CpdailySleepCheckTask) ParseFormJson(ctx context.Context, rawJson map[s
|
||||
}
|
||||
list = append(list, dto)
|
||||
|
||||
var current uint = 1
|
||||
|
||||
if isPhoto := rows["isPhoto"]; isPhoto != nil && isPhoto.Int() == 1 {
|
||||
dto = &formtype.CpTaskFormItem{
|
||||
Title: "查寝照片",
|
||||
ColName: "signImg",
|
||||
Type: formtype.ItemTypeImage,
|
||||
Required: true,
|
||||
Number: current,
|
||||
}
|
||||
list = append(list, dto)
|
||||
current++
|
||||
|
||||
}
|
||||
|
||||
// 是否范围外签到
|
||||
@@ -483,7 +479,6 @@ func (c *CpdailySleepCheckTask) ParseFormJson(ctx context.Context, rawJson map[s
|
||||
ColName: "isMalposition",
|
||||
Type: formtype.ItemTypeSingle,
|
||||
Required: true,
|
||||
Number: current,
|
||||
Options: []formtype.CpTaskFormItemOption{
|
||||
{
|
||||
Content: "是",
|
||||
@@ -497,7 +492,6 @@ func (c *CpdailySleepCheckTask) ParseFormJson(ctx context.Context, rawJson map[s
|
||||
},
|
||||
}
|
||||
list = append(list, dto)
|
||||
current++
|
||||
|
||||
for key, item := range list {
|
||||
list[key].Show = item.LogicWid == ""
|
||||
@@ -506,7 +500,7 @@ func (c *CpdailySleepCheckTask) ParseFormJson(ctx context.Context, rawJson map[s
|
||||
return list
|
||||
}
|
||||
|
||||
func (c *CpdailySleepCheckTask) FillCustomForm(ctx context.Context, form map[string]*gvar.Var, paramList []*formtype.CpTaskFormItem, deviceId, username string) map[string]*gvar.Var {
|
||||
func (c *CpdailySleepCheckTask) FillCustomForm(ctx context.Context, form map[string]*gvar.Var, paramList []*formtype.CpTaskFormItem, deviceId, imgId string) map[string]*gvar.Var {
|
||||
var resultForm map[string]*gvar.Var
|
||||
|
||||
// 填充必要变量
|
||||
@@ -566,27 +560,11 @@ func (c *CpdailySleepCheckTask) FillCustomForm(ctx context.Context, form map[str
|
||||
utils.OkOrPanic(errors.NewBusinessError(503, "未找到查寝照片参数,请上传照片"), oops.WithContext(ctx).Wrap)
|
||||
}
|
||||
|
||||
imageUuidSeq := strings.SplitSeq(targetParam.Content, ",")
|
||||
|
||||
var imageUuids []string
|
||||
|
||||
for imageUuid := range imageUuidSeq {
|
||||
imageUuid = strings.TrimSpace(imageUuid)
|
||||
if imageUuid == "" {
|
||||
continue
|
||||
}
|
||||
|
||||
imageUuids = append(imageUuids, imageUuid)
|
||||
}
|
||||
|
||||
if len(imageUuids) == 0 {
|
||||
if imgId == "" {
|
||||
utils.OkOrPanic(errors.NewBusinessError(503, "未找到查寝照片参数,请上传照片"), oops.WithContext(ctx).Wrap)
|
||||
}
|
||||
|
||||
// 随机选择一张照片
|
||||
imageUuid := imageUuids[rand.IntN(len(imageUuids))]
|
||||
|
||||
imageId, e := uuid.Parse(imageUuid)
|
||||
imageId, e := uuid.Parse(imgId)
|
||||
utils.OkOrPanic(e, oops.WithContext(ctx).Wrap)
|
||||
|
||||
previewUrl := imageuploader.UploadImageFromStorage(ctx, c.host, imageId, types.TaskTypeSleepCheck, c.client)
|
||||
|
||||
@@ -43,15 +43,15 @@ func getOnnxRuntimeName() (onnxRuntimeFileName string, onnxRuntimeFileBytes []by
|
||||
switch systemOS {
|
||||
case "windows":
|
||||
onnxRuntimeFileName = "onnxruntime.dll"
|
||||
onnxRuntimeFileBytes, _ = fs.ReadFile("static/" + onnxRuntimeFileName)
|
||||
onnxRuntimeFileBytes, _ = fs.ReadFile("static/onnxruntime.dll")
|
||||
return
|
||||
case "darwin":
|
||||
onnxRuntimeFileName = "onnxruntime.dylib"
|
||||
onnxRuntimeFileBytes, _ = fs.ReadFile("static/" + onnxRuntimeFileName)
|
||||
onnxRuntimeFileBytes, _ = fs.ReadFile("static/libonnxruntime.1.23.2.dylib")
|
||||
return
|
||||
case "linux":
|
||||
onnxRuntimeFileName = "onnxruntime.so"
|
||||
onnxRuntimeFileBytes, _ = fs.ReadFile("static/" + onnxRuntimeFileName)
|
||||
onnxRuntimeFileBytes, _ = fs.ReadFile("static/libonnxruntime.so.1.23.2")
|
||||
return
|
||||
}
|
||||
logger.Logger.Panic("暂不支持的操作系统:", zap.String("系统名称", systemOS))
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -0,0 +1,192 @@
|
||||
package slider
|
||||
|
||||
import (
|
||||
"encoding/base64"
|
||||
"os"
|
||||
|
||||
"gocv.io/x/gocv"
|
||||
)
|
||||
|
||||
func VerifySliderWidth(bgImgPath, sliderImgPath string) int {
|
||||
// 检查背景图片文件是否存在
|
||||
if _, err := os.Stat(bgImgPath); os.IsNotExist(err) {
|
||||
return -1
|
||||
}
|
||||
|
||||
// 检查滑块图片文件是否存在
|
||||
if _, err := os.Stat(sliderImgPath); os.IsNotExist(err) {
|
||||
return -1
|
||||
}
|
||||
|
||||
// 读取背景图片
|
||||
bgImg := gocv.IMRead(bgImgPath, gocv.IMReadColor)
|
||||
if bgImg.Empty() {
|
||||
return -1
|
||||
}
|
||||
defer bgImg.Close()
|
||||
|
||||
// 读取滑块图片(包含 alpha 通道)
|
||||
sliderImgWithAlpha := gocv.IMRead(sliderImgPath, gocv.IMReadUnchanged)
|
||||
if sliderImgWithAlpha.Empty() {
|
||||
return -1
|
||||
}
|
||||
defer sliderImgWithAlpha.Close()
|
||||
|
||||
// 分离滑块图片的通道
|
||||
channels := gocv.Split(sliderImgWithAlpha)
|
||||
defer func() {
|
||||
for i := range channels {
|
||||
channels[i].Close()
|
||||
}
|
||||
}()
|
||||
|
||||
var sliderImg gocv.Mat
|
||||
var mask gocv.Mat
|
||||
|
||||
// 如果有 alpha 通道(4通道),提取 mask
|
||||
if len(channels) == 4 {
|
||||
// 前3个通道是 BGR
|
||||
sliderImg = gocv.NewMat()
|
||||
defer sliderImg.Close()
|
||||
gocv.Merge(channels[:3], &sliderImg)
|
||||
|
||||
// 使用 alpha 通道作为 mask
|
||||
mask = channels[3].Clone()
|
||||
defer mask.Close()
|
||||
} else if len(channels) == 3 {
|
||||
// 没有透明通道,直接使用原图
|
||||
sliderImg = sliderImgWithAlpha.Clone()
|
||||
defer sliderImg.Close()
|
||||
mask = gocv.NewMat() // 空 mask
|
||||
defer mask.Close()
|
||||
} else {
|
||||
return -1
|
||||
}
|
||||
|
||||
// 创建结果矩阵用于存储匹配结果
|
||||
result := gocv.NewMat()
|
||||
defer result.Close()
|
||||
|
||||
// 直接在原图上使用模板匹配找到滑块位置
|
||||
if mask.Empty() {
|
||||
gocv.MatchTemplate(bgImg, sliderImg, &result, gocv.TmCcoeffNormed, gocv.NewMat())
|
||||
} else {
|
||||
// 带 mask 的模板匹配
|
||||
gocv.MatchTemplate(bgImg, sliderImg, &result, gocv.TmCcoeffNormed, mask)
|
||||
}
|
||||
|
||||
// 找到最佳匹配位置
|
||||
_, maxVal, _, maxLoc := gocv.MinMaxLoc(result)
|
||||
|
||||
// 如果匹配度太低,返回 -1 表示失败
|
||||
if maxVal < 0.6 {
|
||||
return -1
|
||||
}
|
||||
|
||||
// 获取原图上的 x 坐标,然后按比例缩放到目标尺寸
|
||||
originalWidth := bgImg.Cols()
|
||||
targetWidth := 280
|
||||
scale := float64(targetWidth) / float64(originalWidth)
|
||||
scaledX := int(float64(maxLoc.X) * scale)
|
||||
|
||||
// 返回缩放后的 x 坐标(滑块需要移动的距离)
|
||||
return scaledX
|
||||
}
|
||||
|
||||
// VerifySliderWidthByBase64 通过 base64 编码的图片计算滑块验证码需要移动的距离
|
||||
// bgImgBase64: 背景图片的 base64 编码
|
||||
// sliderImgBase64: 滑块图片的 base64 编码
|
||||
// 返回: 滑块需要移动的 x 距离(已缩放到 280 宽度),失败返回 -1
|
||||
func VerifySliderWidthByBase64(bgImgBase64, sliderImgBase64 string) int {
|
||||
// 解码背景图片的 base64
|
||||
bgImgData, err := base64.StdEncoding.DecodeString(bgImgBase64)
|
||||
if err != nil {
|
||||
return -1
|
||||
}
|
||||
|
||||
// 解码滑块图片的 base64
|
||||
sliderImgData, err := base64.StdEncoding.DecodeString(sliderImgBase64)
|
||||
if err != nil {
|
||||
return -1
|
||||
}
|
||||
|
||||
// 从内存中读取背景图片
|
||||
bgImg, err := gocv.IMDecode(bgImgData, gocv.IMReadColor)
|
||||
if err != nil {
|
||||
return -1
|
||||
}
|
||||
if bgImg.Empty() {
|
||||
return -1
|
||||
}
|
||||
defer bgImg.Close()
|
||||
|
||||
// 从内存中读取滑块图片(包含 alpha 通道)
|
||||
sliderImgWithAlpha, err := gocv.IMDecode(sliderImgData, gocv.IMReadUnchanged)
|
||||
if err != nil {
|
||||
return -1
|
||||
}
|
||||
if sliderImgWithAlpha.Empty() {
|
||||
return -1
|
||||
}
|
||||
defer sliderImgWithAlpha.Close()
|
||||
|
||||
// 分离滑块图片的通道
|
||||
channels := gocv.Split(sliderImgWithAlpha)
|
||||
defer func() {
|
||||
for i := range channels {
|
||||
channels[i].Close()
|
||||
}
|
||||
}()
|
||||
|
||||
var sliderImg gocv.Mat
|
||||
var mask gocv.Mat
|
||||
|
||||
// 如果有 alpha 通道(4通道),提取 mask
|
||||
if len(channels) == 4 {
|
||||
// 前3个通道是 BGR
|
||||
sliderImg = gocv.NewMat()
|
||||
defer sliderImg.Close()
|
||||
gocv.Merge(channels[:3], &sliderImg)
|
||||
|
||||
// 使用 alpha 通道作为 mask
|
||||
mask = channels[3].Clone()
|
||||
defer mask.Close()
|
||||
} else if len(channels) == 3 {
|
||||
// 没有透明通道,直接使用原图
|
||||
sliderImg = sliderImgWithAlpha.Clone()
|
||||
defer sliderImg.Close()
|
||||
mask = gocv.NewMat() // 空 mask
|
||||
defer mask.Close()
|
||||
} else {
|
||||
return -1
|
||||
}
|
||||
|
||||
// 创建结果矩阵用于存储匹配结果
|
||||
result := gocv.NewMat()
|
||||
defer result.Close()
|
||||
|
||||
// 直接在原图上使用模板匹配找到滑块位置
|
||||
if mask.Empty() {
|
||||
gocv.MatchTemplate(bgImg, sliderImg, &result, gocv.TmCcoeffNormed, gocv.NewMat())
|
||||
} else {
|
||||
// 带 mask 的模板匹配
|
||||
gocv.MatchTemplate(bgImg, sliderImg, &result, gocv.TmCcoeffNormed, mask)
|
||||
}
|
||||
|
||||
// 找到最佳匹配位置
|
||||
_, maxVal, _, maxLoc := gocv.MinMaxLoc(result)
|
||||
|
||||
// 如果匹配度太低,返回 -1 表示失败
|
||||
if maxVal < 0.6 {
|
||||
return -1
|
||||
}
|
||||
|
||||
// 获取原图上的 x 坐标,然后按比例缩放到目标尺寸
|
||||
originalWidth := bgImg.Cols()
|
||||
targetWidth := 280
|
||||
scale := float64(targetWidth) / float64(originalWidth)
|
||||
scaledX := int(float64(maxLoc.X) * scale)
|
||||
|
||||
// 返回缩放后的 x 坐标(滑块需要移动的距离)
|
||||
return scaledX
|
||||
}
|
||||
+12
-1
@@ -39,6 +39,8 @@ type CpTask struct {
|
||||
SuccessCount int `json:"success_count,omitempty"`
|
||||
// FormData holds the value of the "form_data" field.
|
||||
FormData string `json:"form_data,omitempty"`
|
||||
// UsedImg holds the value of the "used_img" field.
|
||||
UsedImg string `json:"used_img,omitempty"`
|
||||
// Step holds the value of the "step" field.
|
||||
Step cptask.Step `json:"step,omitempty"`
|
||||
// NotificationType holds the value of the "notification_type" field.
|
||||
@@ -109,7 +111,7 @@ func (*CpTask) scanValues(columns []string) ([]any, error) {
|
||||
values[i] = new(ent2.CustomTime)
|
||||
case cptask.FieldID, cptask.FieldCpUserID, cptask.FieldUserID, cptask.FieldFrequency, cptask.FieldSuccessCount, cptask.FieldExecuteHour, cptask.FieldExecuteMinute, cptask.FieldNextExecuteTime:
|
||||
values[i] = new(sql.NullInt64)
|
||||
case cptask.FieldTaskType, cptask.FieldTitle, cptask.FieldTaskStatus, cptask.FieldFormData, cptask.FieldStep, cptask.FieldNotificationType:
|
||||
case cptask.FieldTaskType, cptask.FieldTitle, cptask.FieldTaskStatus, cptask.FieldFormData, cptask.FieldUsedImg, cptask.FieldStep, cptask.FieldNotificationType:
|
||||
values[i] = new(sql.NullString)
|
||||
default:
|
||||
values[i] = new(sql.UnknownType)
|
||||
@@ -192,6 +194,12 @@ func (_m *CpTask) assignValues(columns []string, values []any) error {
|
||||
} else if value.Valid {
|
||||
_m.FormData = value.String
|
||||
}
|
||||
case cptask.FieldUsedImg:
|
||||
if value, ok := values[i].(*sql.NullString); !ok {
|
||||
return fmt.Errorf("unexpected type %T for field used_img", values[i])
|
||||
} else if value.Valid {
|
||||
_m.UsedImg = value.String
|
||||
}
|
||||
case cptask.FieldStep:
|
||||
if value, ok := values[i].(*sql.NullString); !ok {
|
||||
return fmt.Errorf("unexpected type %T for field step", values[i])
|
||||
@@ -306,6 +314,9 @@ func (_m *CpTask) String() string {
|
||||
builder.WriteString("form_data=")
|
||||
builder.WriteString(_m.FormData)
|
||||
builder.WriteString(", ")
|
||||
builder.WriteString("used_img=")
|
||||
builder.WriteString(_m.UsedImg)
|
||||
builder.WriteString(", ")
|
||||
builder.WriteString("step=")
|
||||
builder.WriteString(fmt.Sprintf("%v", _m.Step))
|
||||
builder.WriteString(", ")
|
||||
|
||||
@@ -36,6 +36,8 @@ const (
|
||||
FieldSuccessCount = "success_count"
|
||||
// FieldFormData holds the string denoting the form_data field in the database.
|
||||
FieldFormData = "form_data"
|
||||
// FieldUsedImg holds the string denoting the used_img field in the database.
|
||||
FieldUsedImg = "used_img"
|
||||
// FieldStep holds the string denoting the step field in the database.
|
||||
FieldStep = "step"
|
||||
// FieldNotificationType holds the string denoting the notification_type field in the database.
|
||||
@@ -90,6 +92,7 @@ var Columns = []string{
|
||||
FieldTaskStatus,
|
||||
FieldSuccessCount,
|
||||
FieldFormData,
|
||||
FieldUsedImg,
|
||||
FieldStep,
|
||||
FieldNotificationType,
|
||||
FieldExecuteHour,
|
||||
@@ -284,6 +287,11 @@ func ByFormData(opts ...sql.OrderTermOption) OrderOption {
|
||||
return sql.OrderByField(FieldFormData, opts...).ToFunc()
|
||||
}
|
||||
|
||||
// ByUsedImg orders the results by the used_img field.
|
||||
func ByUsedImg(opts ...sql.OrderTermOption) OrderOption {
|
||||
return sql.OrderByField(FieldUsedImg, opts...).ToFunc()
|
||||
}
|
||||
|
||||
// ByStep orders the results by the step field.
|
||||
func ByStep(opts ...sql.OrderTermOption) OrderOption {
|
||||
return sql.OrderByField(FieldStep, opts...).ToFunc()
|
||||
|
||||
@@ -96,6 +96,11 @@ func FormData(v string) predicate.CpTask {
|
||||
return predicate.CpTask(sql.FieldEQ(FieldFormData, v))
|
||||
}
|
||||
|
||||
// UsedImg applies equality check predicate on the "used_img" field. It's identical to UsedImgEQ.
|
||||
func UsedImg(v string) predicate.CpTask {
|
||||
return predicate.CpTask(sql.FieldEQ(FieldUsedImg, v))
|
||||
}
|
||||
|
||||
// ExecuteHour applies equality check predicate on the "execute_hour" field. It's identical to ExecuteHourEQ.
|
||||
func ExecuteHour(v int) predicate.CpTask {
|
||||
return predicate.CpTask(sql.FieldEQ(FieldExecuteHour, v))
|
||||
@@ -551,6 +556,81 @@ func FormDataContainsFold(v string) predicate.CpTask {
|
||||
return predicate.CpTask(sql.FieldContainsFold(FieldFormData, v))
|
||||
}
|
||||
|
||||
// UsedImgEQ applies the EQ predicate on the "used_img" field.
|
||||
func UsedImgEQ(v string) predicate.CpTask {
|
||||
return predicate.CpTask(sql.FieldEQ(FieldUsedImg, v))
|
||||
}
|
||||
|
||||
// UsedImgNEQ applies the NEQ predicate on the "used_img" field.
|
||||
func UsedImgNEQ(v string) predicate.CpTask {
|
||||
return predicate.CpTask(sql.FieldNEQ(FieldUsedImg, v))
|
||||
}
|
||||
|
||||
// UsedImgIn applies the In predicate on the "used_img" field.
|
||||
func UsedImgIn(vs ...string) predicate.CpTask {
|
||||
return predicate.CpTask(sql.FieldIn(FieldUsedImg, vs...))
|
||||
}
|
||||
|
||||
// UsedImgNotIn applies the NotIn predicate on the "used_img" field.
|
||||
func UsedImgNotIn(vs ...string) predicate.CpTask {
|
||||
return predicate.CpTask(sql.FieldNotIn(FieldUsedImg, vs...))
|
||||
}
|
||||
|
||||
// UsedImgGT applies the GT predicate on the "used_img" field.
|
||||
func UsedImgGT(v string) predicate.CpTask {
|
||||
return predicate.CpTask(sql.FieldGT(FieldUsedImg, v))
|
||||
}
|
||||
|
||||
// UsedImgGTE applies the GTE predicate on the "used_img" field.
|
||||
func UsedImgGTE(v string) predicate.CpTask {
|
||||
return predicate.CpTask(sql.FieldGTE(FieldUsedImg, v))
|
||||
}
|
||||
|
||||
// UsedImgLT applies the LT predicate on the "used_img" field.
|
||||
func UsedImgLT(v string) predicate.CpTask {
|
||||
return predicate.CpTask(sql.FieldLT(FieldUsedImg, v))
|
||||
}
|
||||
|
||||
// UsedImgLTE applies the LTE predicate on the "used_img" field.
|
||||
func UsedImgLTE(v string) predicate.CpTask {
|
||||
return predicate.CpTask(sql.FieldLTE(FieldUsedImg, v))
|
||||
}
|
||||
|
||||
// UsedImgContains applies the Contains predicate on the "used_img" field.
|
||||
func UsedImgContains(v string) predicate.CpTask {
|
||||
return predicate.CpTask(sql.FieldContains(FieldUsedImg, v))
|
||||
}
|
||||
|
||||
// UsedImgHasPrefix applies the HasPrefix predicate on the "used_img" field.
|
||||
func UsedImgHasPrefix(v string) predicate.CpTask {
|
||||
return predicate.CpTask(sql.FieldHasPrefix(FieldUsedImg, v))
|
||||
}
|
||||
|
||||
// UsedImgHasSuffix applies the HasSuffix predicate on the "used_img" field.
|
||||
func UsedImgHasSuffix(v string) predicate.CpTask {
|
||||
return predicate.CpTask(sql.FieldHasSuffix(FieldUsedImg, v))
|
||||
}
|
||||
|
||||
// UsedImgIsNil applies the IsNil predicate on the "used_img" field.
|
||||
func UsedImgIsNil() predicate.CpTask {
|
||||
return predicate.CpTask(sql.FieldIsNull(FieldUsedImg))
|
||||
}
|
||||
|
||||
// UsedImgNotNil applies the NotNil predicate on the "used_img" field.
|
||||
func UsedImgNotNil() predicate.CpTask {
|
||||
return predicate.CpTask(sql.FieldNotNull(FieldUsedImg))
|
||||
}
|
||||
|
||||
// UsedImgEqualFold applies the EqualFold predicate on the "used_img" field.
|
||||
func UsedImgEqualFold(v string) predicate.CpTask {
|
||||
return predicate.CpTask(sql.FieldEqualFold(FieldUsedImg, v))
|
||||
}
|
||||
|
||||
// UsedImgContainsFold applies the ContainsFold predicate on the "used_img" field.
|
||||
func UsedImgContainsFold(v string) predicate.CpTask {
|
||||
return predicate.CpTask(sql.FieldContainsFold(FieldUsedImg, v))
|
||||
}
|
||||
|
||||
// StepEQ applies the EQ predicate on the "step" field.
|
||||
func StepEQ(v Step) predicate.CpTask {
|
||||
return predicate.CpTask(sql.FieldEQ(FieldStep, v))
|
||||
|
||||
@@ -109,6 +109,20 @@ func (_c *CpTaskCreate) SetNillableFormData(v *string) *CpTaskCreate {
|
||||
return _c
|
||||
}
|
||||
|
||||
// SetUsedImg sets the "used_img" field.
|
||||
func (_c *CpTaskCreate) SetUsedImg(v string) *CpTaskCreate {
|
||||
_c.mutation.SetUsedImg(v)
|
||||
return _c
|
||||
}
|
||||
|
||||
// SetNillableUsedImg sets the "used_img" field if the given value is not nil.
|
||||
func (_c *CpTaskCreate) SetNillableUsedImg(v *string) *CpTaskCreate {
|
||||
if v != nil {
|
||||
_c.SetUsedImg(*v)
|
||||
}
|
||||
return _c
|
||||
}
|
||||
|
||||
// SetStep sets the "step" field.
|
||||
func (_c *CpTaskCreate) SetStep(v cptask.Step) *CpTaskCreate {
|
||||
_c.mutation.SetStep(v)
|
||||
@@ -382,6 +396,10 @@ func (_c *CpTaskCreate) createSpec() (*CpTask, *sqlgraph.CreateSpec) {
|
||||
_spec.SetField(cptask.FieldFormData, field.TypeString, value)
|
||||
_node.FormData = value
|
||||
}
|
||||
if value, ok := _c.mutation.UsedImg(); ok {
|
||||
_spec.SetField(cptask.FieldUsedImg, field.TypeString, value)
|
||||
_node.UsedImg = value
|
||||
}
|
||||
if value, ok := _c.mutation.Step(); ok {
|
||||
_spec.SetField(cptask.FieldStep, field.TypeEnum, value)
|
||||
_node.Step = value
|
||||
@@ -633,6 +651,24 @@ func (u *CpTaskUpsert) ClearFormData() *CpTaskUpsert {
|
||||
return u
|
||||
}
|
||||
|
||||
// SetUsedImg sets the "used_img" field.
|
||||
func (u *CpTaskUpsert) SetUsedImg(v string) *CpTaskUpsert {
|
||||
u.Set(cptask.FieldUsedImg, v)
|
||||
return u
|
||||
}
|
||||
|
||||
// UpdateUsedImg sets the "used_img" field to the value that was provided on create.
|
||||
func (u *CpTaskUpsert) UpdateUsedImg() *CpTaskUpsert {
|
||||
u.SetExcluded(cptask.FieldUsedImg)
|
||||
return u
|
||||
}
|
||||
|
||||
// ClearUsedImg clears the value of the "used_img" field.
|
||||
func (u *CpTaskUpsert) ClearUsedImg() *CpTaskUpsert {
|
||||
u.SetNull(cptask.FieldUsedImg)
|
||||
return u
|
||||
}
|
||||
|
||||
// SetStep sets the "step" field.
|
||||
func (u *CpTaskUpsert) SetStep(v cptask.Step) *CpTaskUpsert {
|
||||
u.Set(cptask.FieldStep, v)
|
||||
@@ -921,6 +957,27 @@ func (u *CpTaskUpsertOne) ClearFormData() *CpTaskUpsertOne {
|
||||
})
|
||||
}
|
||||
|
||||
// SetUsedImg sets the "used_img" field.
|
||||
func (u *CpTaskUpsertOne) SetUsedImg(v string) *CpTaskUpsertOne {
|
||||
return u.Update(func(s *CpTaskUpsert) {
|
||||
s.SetUsedImg(v)
|
||||
})
|
||||
}
|
||||
|
||||
// UpdateUsedImg sets the "used_img" field to the value that was provided on create.
|
||||
func (u *CpTaskUpsertOne) UpdateUsedImg() *CpTaskUpsertOne {
|
||||
return u.Update(func(s *CpTaskUpsert) {
|
||||
s.UpdateUsedImg()
|
||||
})
|
||||
}
|
||||
|
||||
// ClearUsedImg clears the value of the "used_img" field.
|
||||
func (u *CpTaskUpsertOne) ClearUsedImg() *CpTaskUpsertOne {
|
||||
return u.Update(func(s *CpTaskUpsert) {
|
||||
s.ClearUsedImg()
|
||||
})
|
||||
}
|
||||
|
||||
// SetStep sets the "step" field.
|
||||
func (u *CpTaskUpsertOne) SetStep(v cptask.Step) *CpTaskUpsertOne {
|
||||
return u.Update(func(s *CpTaskUpsert) {
|
||||
@@ -1391,6 +1448,27 @@ func (u *CpTaskUpsertBulk) ClearFormData() *CpTaskUpsertBulk {
|
||||
})
|
||||
}
|
||||
|
||||
// SetUsedImg sets the "used_img" field.
|
||||
func (u *CpTaskUpsertBulk) SetUsedImg(v string) *CpTaskUpsertBulk {
|
||||
return u.Update(func(s *CpTaskUpsert) {
|
||||
s.SetUsedImg(v)
|
||||
})
|
||||
}
|
||||
|
||||
// UpdateUsedImg sets the "used_img" field to the value that was provided on create.
|
||||
func (u *CpTaskUpsertBulk) UpdateUsedImg() *CpTaskUpsertBulk {
|
||||
return u.Update(func(s *CpTaskUpsert) {
|
||||
s.UpdateUsedImg()
|
||||
})
|
||||
}
|
||||
|
||||
// ClearUsedImg clears the value of the "used_img" field.
|
||||
func (u *CpTaskUpsertBulk) ClearUsedImg() *CpTaskUpsertBulk {
|
||||
return u.Update(func(s *CpTaskUpsert) {
|
||||
s.ClearUsedImg()
|
||||
})
|
||||
}
|
||||
|
||||
// SetStep sets the "step" field.
|
||||
func (u *CpTaskUpsertBulk) SetStep(v cptask.Step) *CpTaskUpsertBulk {
|
||||
return u.Update(func(s *CpTaskUpsert) {
|
||||
|
||||
@@ -171,6 +171,26 @@ func (_u *CpTaskUpdate) ClearFormData() *CpTaskUpdate {
|
||||
return _u
|
||||
}
|
||||
|
||||
// SetUsedImg sets the "used_img" field.
|
||||
func (_u *CpTaskUpdate) SetUsedImg(v string) *CpTaskUpdate {
|
||||
_u.mutation.SetUsedImg(v)
|
||||
return _u
|
||||
}
|
||||
|
||||
// SetNillableUsedImg sets the "used_img" field if the given value is not nil.
|
||||
func (_u *CpTaskUpdate) SetNillableUsedImg(v *string) *CpTaskUpdate {
|
||||
if v != nil {
|
||||
_u.SetUsedImg(*v)
|
||||
}
|
||||
return _u
|
||||
}
|
||||
|
||||
// ClearUsedImg clears the value of the "used_img" field.
|
||||
func (_u *CpTaskUpdate) ClearUsedImg() *CpTaskUpdate {
|
||||
_u.mutation.ClearUsedImg()
|
||||
return _u
|
||||
}
|
||||
|
||||
// SetStep sets the "step" field.
|
||||
func (_u *CpTaskUpdate) SetStep(v cptask.Step) *CpTaskUpdate {
|
||||
_u.mutation.SetStep(v)
|
||||
@@ -460,6 +480,12 @@ func (_u *CpTaskUpdate) sqlSave(ctx context.Context) (_node int, err error) {
|
||||
if _u.mutation.FormDataCleared() {
|
||||
_spec.ClearField(cptask.FieldFormData, field.TypeString)
|
||||
}
|
||||
if value, ok := _u.mutation.UsedImg(); ok {
|
||||
_spec.SetField(cptask.FieldUsedImg, field.TypeString, value)
|
||||
}
|
||||
if _u.mutation.UsedImgCleared() {
|
||||
_spec.ClearField(cptask.FieldUsedImg, field.TypeString)
|
||||
}
|
||||
if value, ok := _u.mutation.Step(); ok {
|
||||
_spec.SetField(cptask.FieldStep, field.TypeEnum, value)
|
||||
}
|
||||
@@ -765,6 +791,26 @@ func (_u *CpTaskUpdateOne) ClearFormData() *CpTaskUpdateOne {
|
||||
return _u
|
||||
}
|
||||
|
||||
// SetUsedImg sets the "used_img" field.
|
||||
func (_u *CpTaskUpdateOne) SetUsedImg(v string) *CpTaskUpdateOne {
|
||||
_u.mutation.SetUsedImg(v)
|
||||
return _u
|
||||
}
|
||||
|
||||
// SetNillableUsedImg sets the "used_img" field if the given value is not nil.
|
||||
func (_u *CpTaskUpdateOne) SetNillableUsedImg(v *string) *CpTaskUpdateOne {
|
||||
if v != nil {
|
||||
_u.SetUsedImg(*v)
|
||||
}
|
||||
return _u
|
||||
}
|
||||
|
||||
// ClearUsedImg clears the value of the "used_img" field.
|
||||
func (_u *CpTaskUpdateOne) ClearUsedImg() *CpTaskUpdateOne {
|
||||
_u.mutation.ClearUsedImg()
|
||||
return _u
|
||||
}
|
||||
|
||||
// SetStep sets the "step" field.
|
||||
func (_u *CpTaskUpdateOne) SetStep(v cptask.Step) *CpTaskUpdateOne {
|
||||
_u.mutation.SetStep(v)
|
||||
@@ -1084,6 +1130,12 @@ func (_u *CpTaskUpdateOne) sqlSave(ctx context.Context) (_node *CpTask, err erro
|
||||
if _u.mutation.FormDataCleared() {
|
||||
_spec.ClearField(cptask.FieldFormData, field.TypeString)
|
||||
}
|
||||
if value, ok := _u.mutation.UsedImg(); ok {
|
||||
_spec.SetField(cptask.FieldUsedImg, field.TypeString, value)
|
||||
}
|
||||
if _u.mutation.UsedImgCleared() {
|
||||
_spec.ClearField(cptask.FieldUsedImg, field.TypeString)
|
||||
}
|
||||
if value, ok := _u.mutation.Step(); ok {
|
||||
_spec.SetField(cptask.FieldStep, field.TypeEnum, value)
|
||||
}
|
||||
|
||||
@@ -84,6 +84,7 @@ var schemaGraph = func() *sqlgraph.Schema {
|
||||
cptask.FieldTaskStatus: {Type: field.TypeEnum, Column: cptask.FieldTaskStatus},
|
||||
cptask.FieldSuccessCount: {Type: field.TypeInt, Column: cptask.FieldSuccessCount},
|
||||
cptask.FieldFormData: {Type: field.TypeString, Column: cptask.FieldFormData},
|
||||
cptask.FieldUsedImg: {Type: field.TypeString, Column: cptask.FieldUsedImg},
|
||||
cptask.FieldStep: {Type: field.TypeEnum, Column: cptask.FieldStep},
|
||||
cptask.FieldNotificationType: {Type: field.TypeEnum, Column: cptask.FieldNotificationType},
|
||||
cptask.FieldExecuteHour: {Type: field.TypeInt, Column: cptask.FieldExecuteHour},
|
||||
@@ -905,6 +906,11 @@ func (f *CpTaskFilter) WhereFormData(p entql.StringP) {
|
||||
f.Where(p.Field(cptask.FieldFormData))
|
||||
}
|
||||
|
||||
// WhereUsedImg applies the entql string predicate on the used_img field.
|
||||
func (f *CpTaskFilter) WhereUsedImg(p entql.StringP) {
|
||||
f.Where(p.Field(cptask.FieldUsedImg))
|
||||
}
|
||||
|
||||
// WhereStep applies the entql string predicate on the step field.
|
||||
func (f *CpTaskFilter) WhereStep(p entql.StringP) {
|
||||
f.Where(p.Field(cptask.FieldStep))
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -60,6 +60,7 @@ var (
|
||||
{Name: "task_status", Type: field.TypeEnum, Enums: []string{"not_start", "doing", "done", "fail", "pause", "queue", "unknown"}},
|
||||
{Name: "success_count", Type: field.TypeInt, Default: 0},
|
||||
{Name: "form_data", Type: field.TypeString, Nullable: true, Size: 2147483647},
|
||||
{Name: "used_img", Type: field.TypeString, Nullable: true},
|
||||
{Name: "step", Type: field.TypeEnum, Enums: []string{"form", "execute", "complete"}},
|
||||
{Name: "notification_type", Type: field.TypeEnum, Enums: []string{"none", "cp_user_email", "system_user_email"}},
|
||||
{Name: "execute_hour", Type: field.TypeInt, Nullable: true},
|
||||
@@ -76,13 +77,13 @@ var (
|
||||
ForeignKeys: []*schema.ForeignKey{
|
||||
{
|
||||
Symbol: "cp_tasks_cp_users_cptasks",
|
||||
Columns: []*schema.Column{CpTasksColumns[14]},
|
||||
Columns: []*schema.Column{CpTasksColumns[15]},
|
||||
RefColumns: []*schema.Column{CpUsersColumns[0]},
|
||||
OnDelete: schema.NoAction,
|
||||
},
|
||||
{
|
||||
Symbol: "cp_tasks_sys_user_cp_task",
|
||||
Columns: []*schema.Column{CpTasksColumns[15]},
|
||||
Columns: []*schema.Column{CpTasksColumns[16]},
|
||||
RefColumns: []*schema.Column{SysUserColumns[0]},
|
||||
OnDelete: schema.Cascade,
|
||||
},
|
||||
|
||||
@@ -1467,6 +1467,7 @@ type CpTaskMutation struct {
|
||||
success_count *int
|
||||
addsuccess_count *int
|
||||
form_data *string
|
||||
used_img *string
|
||||
step *cptask.Step
|
||||
notification_type *cptask.NotificationType
|
||||
execute_hour *int
|
||||
@@ -1999,6 +2000,55 @@ func (m *CpTaskMutation) ResetFormData() {
|
||||
delete(m.clearedFields, cptask.FieldFormData)
|
||||
}
|
||||
|
||||
// SetUsedImg sets the "used_img" field.
|
||||
func (m *CpTaskMutation) SetUsedImg(s string) {
|
||||
m.used_img = &s
|
||||
}
|
||||
|
||||
// UsedImg returns the value of the "used_img" field in the mutation.
|
||||
func (m *CpTaskMutation) UsedImg() (r string, exists bool) {
|
||||
v := m.used_img
|
||||
if v == nil {
|
||||
return
|
||||
}
|
||||
return *v, true
|
||||
}
|
||||
|
||||
// OldUsedImg returns the old "used_img" field's value of the CpTask entity.
|
||||
// If the CpTask object wasn't provided to the builder, the object is fetched from the database.
|
||||
// An error is returned if the mutation operation is not UpdateOne, or the database query fails.
|
||||
func (m *CpTaskMutation) OldUsedImg(ctx context.Context) (v string, err error) {
|
||||
if !m.op.Is(OpUpdateOne) {
|
||||
return v, errors.New("OldUsedImg is only allowed on UpdateOne operations")
|
||||
}
|
||||
if m.id == nil || m.oldValue == nil {
|
||||
return v, errors.New("OldUsedImg requires an ID field in the mutation")
|
||||
}
|
||||
oldValue, err := m.oldValue(ctx)
|
||||
if err != nil {
|
||||
return v, fmt.Errorf("querying old value for OldUsedImg: %w", err)
|
||||
}
|
||||
return oldValue.UsedImg, nil
|
||||
}
|
||||
|
||||
// ClearUsedImg clears the value of the "used_img" field.
|
||||
func (m *CpTaskMutation) ClearUsedImg() {
|
||||
m.used_img = nil
|
||||
m.clearedFields[cptask.FieldUsedImg] = struct{}{}
|
||||
}
|
||||
|
||||
// UsedImgCleared returns if the "used_img" field was cleared in this mutation.
|
||||
func (m *CpTaskMutation) UsedImgCleared() bool {
|
||||
_, ok := m.clearedFields[cptask.FieldUsedImg]
|
||||
return ok
|
||||
}
|
||||
|
||||
// ResetUsedImg resets all changes to the "used_img" field.
|
||||
func (m *CpTaskMutation) ResetUsedImg() {
|
||||
m.used_img = nil
|
||||
delete(m.clearedFields, cptask.FieldUsedImg)
|
||||
}
|
||||
|
||||
// SetStep sets the "step" field.
|
||||
func (m *CpTaskMutation) SetStep(c cptask.Step) {
|
||||
m.step = &c
|
||||
@@ -2436,7 +2486,7 @@ func (m *CpTaskMutation) Type() string {
|
||||
// order to get all numeric fields that were incremented/decremented, call
|
||||
// AddedFields().
|
||||
func (m *CpTaskMutation) Fields() []string {
|
||||
fields := make([]string, 0, 15)
|
||||
fields := make([]string, 0, 16)
|
||||
if m.create_time != nil {
|
||||
fields = append(fields, cptask.FieldCreateTime)
|
||||
}
|
||||
@@ -2467,6 +2517,9 @@ func (m *CpTaskMutation) Fields() []string {
|
||||
if m.form_data != nil {
|
||||
fields = append(fields, cptask.FieldFormData)
|
||||
}
|
||||
if m.used_img != nil {
|
||||
fields = append(fields, cptask.FieldUsedImg)
|
||||
}
|
||||
if m.step != nil {
|
||||
fields = append(fields, cptask.FieldStep)
|
||||
}
|
||||
@@ -2510,6 +2563,8 @@ func (m *CpTaskMutation) Field(name string) (ent.Value, bool) {
|
||||
return m.SuccessCount()
|
||||
case cptask.FieldFormData:
|
||||
return m.FormData()
|
||||
case cptask.FieldUsedImg:
|
||||
return m.UsedImg()
|
||||
case cptask.FieldStep:
|
||||
return m.Step()
|
||||
case cptask.FieldNotificationType:
|
||||
@@ -2549,6 +2604,8 @@ func (m *CpTaskMutation) OldField(ctx context.Context, name string) (ent.Value,
|
||||
return m.OldSuccessCount(ctx)
|
||||
case cptask.FieldFormData:
|
||||
return m.OldFormData(ctx)
|
||||
case cptask.FieldUsedImg:
|
||||
return m.OldUsedImg(ctx)
|
||||
case cptask.FieldStep:
|
||||
return m.OldStep(ctx)
|
||||
case cptask.FieldNotificationType:
|
||||
@@ -2638,6 +2695,13 @@ func (m *CpTaskMutation) SetField(name string, value ent.Value) error {
|
||||
}
|
||||
m.SetFormData(v)
|
||||
return nil
|
||||
case cptask.FieldUsedImg:
|
||||
v, ok := value.(string)
|
||||
if !ok {
|
||||
return fmt.Errorf("unexpected type %T for field %s", value, name)
|
||||
}
|
||||
m.SetUsedImg(v)
|
||||
return nil
|
||||
case cptask.FieldStep:
|
||||
v, ok := value.(cptask.Step)
|
||||
if !ok {
|
||||
@@ -2769,6 +2833,9 @@ func (m *CpTaskMutation) ClearedFields() []string {
|
||||
if m.FieldCleared(cptask.FieldFormData) {
|
||||
fields = append(fields, cptask.FieldFormData)
|
||||
}
|
||||
if m.FieldCleared(cptask.FieldUsedImg) {
|
||||
fields = append(fields, cptask.FieldUsedImg)
|
||||
}
|
||||
if m.FieldCleared(cptask.FieldExecuteHour) {
|
||||
fields = append(fields, cptask.FieldExecuteHour)
|
||||
}
|
||||
@@ -2795,6 +2862,9 @@ func (m *CpTaskMutation) ClearField(name string) error {
|
||||
case cptask.FieldFormData:
|
||||
m.ClearFormData()
|
||||
return nil
|
||||
case cptask.FieldUsedImg:
|
||||
m.ClearUsedImg()
|
||||
return nil
|
||||
case cptask.FieldExecuteHour:
|
||||
m.ClearExecuteHour()
|
||||
return nil
|
||||
@@ -2842,6 +2912,9 @@ func (m *CpTaskMutation) ResetField(name string) error {
|
||||
case cptask.FieldFormData:
|
||||
m.ResetFormData()
|
||||
return nil
|
||||
case cptask.FieldUsedImg:
|
||||
m.ResetUsedImg()
|
||||
return nil
|
||||
case cptask.FieldStep:
|
||||
m.ResetStep()
|
||||
return nil
|
||||
|
||||
@@ -25,6 +25,7 @@ func (CpTask) Fields() []ent.Field {
|
||||
field.Enum("task_status").Values("not_start", "doing", "done", "fail", "pause", "queue", "unknown"),
|
||||
field.Int("success_count").Default(0),
|
||||
field.Text("form_data").Optional(),
|
||||
field.String("used_img").Optional(),
|
||||
field.Enum("step").Values("form", "execute", "complete"),
|
||||
field.Enum("notification_type").Values("none", "cp_user_email", "system_user_email"),
|
||||
field.Int("execute_hour").Optional().Nillable(),
|
||||
|
||||
@@ -18,6 +18,8 @@ import (
|
||||
"likedream-goframe/internal/framework/model/email"
|
||||
email2 "likedream-goframe/internal/service/email"
|
||||
"likedream-goframe/internal/utils"
|
||||
"slices"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/bytedance/sonic"
|
||||
@@ -131,13 +133,14 @@ func (c *CpdailyTaskHandler) ProcessTask(ctx context.Context, task *asynq.Task)
|
||||
default:
|
||||
utils.OkOrPanic(errors.NewBusinessErrorWithDetail(503, "不支持的设备类型", targetTask.Edges.CpUser.MobileType))
|
||||
}
|
||||
|
||||
submitForm := taskExecutor.FillCustomForm(ctx, cpdailyRealTask, customFormItems, deviceID, targetTask.Edges.CpUser.Username)
|
||||
// 固定获取图片id
|
||||
imgId := GetNextImgId(customFormItems, targetTask.UsedImg)
|
||||
submitForm := taskExecutor.FillCustomForm(ctx, cpdailyRealTask, customFormItems, deviceID, imgId)
|
||||
taskExecutor.Submit(ctx, submitForm, targetTask.Edges.CpUser.Username, deviceID)
|
||||
|
||||
// 任务完成后重置状态为 TaskStatusDone,更新下次执行时间 以便下次继续执行
|
||||
executeTime := utils.GetCpdailyNextExecuteTime(targetTask)
|
||||
db.Global.CpTask.UpdateOneID(taskPayload.CpTaskID).SetTaskStatus(cptask.TaskStatusDone).SetNextExecuteTime(executeTime).AddSuccessCount(1).ExecX(ctx)
|
||||
db.Global.CpTask.UpdateOneID(taskPayload.CpTaskID).SetTaskStatus(cptask.TaskStatusDone).SetUsedImg(imgId).SetNextExecuteTime(executeTime).AddSuccessCount(1).ExecX(ctx)
|
||||
|
||||
// 创建日志
|
||||
db.Global.CpTaskLog.Create().
|
||||
@@ -157,6 +160,26 @@ func (c *CpdailyTaskHandler) ProcessTask(ctx context.Context, task *asynq.Task)
|
||||
|
||||
return
|
||||
}
|
||||
func GetNextImgId(forms []*formtype.CpTaskFormItem, imgId string) string {
|
||||
var allImgIds []string
|
||||
for _, item := range forms {
|
||||
if item.ColName == "signImg" {
|
||||
allImgIds = strings.Split(item.Content, ",")
|
||||
break
|
||||
}
|
||||
}
|
||||
if len(allImgIds) == 0 {
|
||||
return ""
|
||||
}
|
||||
if imgId == "" {
|
||||
return allImgIds[0]
|
||||
}
|
||||
index := slices.Index(allImgIds, imgId)
|
||||
if index == -1 || index == len(allImgIds)-1 {
|
||||
return allImgIds[0]
|
||||
}
|
||||
return allImgIds[index+1]
|
||||
}
|
||||
|
||||
// sendTaskNotificationEmail 发送任务通知邮件
|
||||
func (c *CpdailyTaskHandler) sendTaskNotificationEmail(ctx context.Context, task *ent.CpTask, status email.TaskStatus, details map[string]string) {
|
||||
|
||||
@@ -2,9 +2,9 @@ package schedule
|
||||
|
||||
import (
|
||||
"context"
|
||||
"likedream-goframe/internal/utils"
|
||||
"likedream-goframe/internal/framework/logger"
|
||||
|
||||
"github.com/samber/oops"
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
// StopTask 让任务队列停止执行某任务
|
||||
@@ -17,5 +17,8 @@ func StopTask(ctx context.Context, queue, taskID string) {
|
||||
queue = "default"
|
||||
}
|
||||
e := globalInspector.DeleteTask(queue, taskID)
|
||||
utils.OkOrPanic(e, oops.WithContext(ctx).With("queue", queue, "taskID", taskID).Wrap)
|
||||
if e != nil {
|
||||
logger.Logger.Error("删除定时任务失败", zap.Error(e))
|
||||
}
|
||||
//utils.OkOrPanic(e, oops.WithContext(ctx).With("queue", queue, "taskID", taskID).Wrap)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user