Compare commits
123 Commits
v1.0.0.beta26
...
v1.2.1
| Author | SHA1 | Date | |
|---|---|---|---|
| a41341e732 | |||
| ecfa50359b | |||
| 2879336ce4 | |||
| f706968618 | |||
| 69e198f745 | |||
| 70f907d909 | |||
| 3a3d17e9b4 | |||
| d560c9f678 | |||
| 7a9b49794c | |||
| e3f8dc8aac | |||
| 1228c30a75 | |||
| 4762e71308 | |||
| 2082ee3e68 | |||
| 10cb7d7ac6 | |||
| 261153067c | |||
| 19a13a0ad2 | |||
| 5d7f79a619 | |||
| 0934994f51 | |||
| ef61d405ba | |||
| 60165e4851 | |||
| 39cb44bc22 | |||
| 9fa83657e3 | |||
| 5a244c329e | |||
| 93f696d4df | |||
| 988cc7be6f | |||
| b890929d0b | |||
| adbf885d95 | |||
| 99c58a3d5f | |||
| c89f7e9bbb | |||
| a6babba00d | |||
| 4c7bc950c7 | |||
| b857d55e25 | |||
| ca20be8b7e | |||
| ce98ce4afe | |||
| c65318c545 | |||
| 63b91629fa | |||
| 658a9c2b3e | |||
| 60789ded9d | |||
| 7d8998d83f | |||
| 8974bd26a5 | |||
| 6f25652183 | |||
| a8f4a2afa6 | |||
| d14ddc212d | |||
| 55fc94c37f | |||
| a590f58c6c | |||
| 0959ca117c | |||
| 3641fb1782 | |||
| d0e7652bf6 | |||
| 07e2438f93 | |||
| d1b954ab8e | |||
| 24da8bde3a | |||
| 16692177d4 | |||
| 5f81a7ba5e | |||
| 58fb282fe8 | |||
| f64b4b5498 | |||
| 84620989ad | |||
| af7fcc4cb4 | |||
| c206a0863e | |||
| b068e178da | |||
| 02737ef743 | |||
| 74a1134cf1 | |||
| b6f777f997 | |||
| 2dc233631e | |||
| 0c60eb5ae4 | |||
| 450c026a6f | |||
| d9d6acee5f | |||
| 164d8956de | |||
| 3f1a08374e | |||
| 334d597f9a | |||
| 98eb15f4e4 | |||
| 3a2e679b85 | |||
| 921192a914 | |||
| 107b472cf7 | |||
| 14108f6ba3 | |||
| a22f559889 | |||
| ececfa344d | |||
| 5ba9e38e1a | |||
| 6eb4bc6d8a | |||
| 17e6c0a789 | |||
| f067ed2ad0 | |||
| 7bfc7a58ab | |||
| 1ad0c285f2 | |||
| 498c51c585 | |||
| 6a6716655b | |||
| ddc0949e56 | |||
| a723733562 | |||
| e7307182dd | |||
| 92ccebfa4e | |||
| 30dd8a8dfe | |||
| 788f1f4de7 | |||
| 54d81b5ba8 | |||
| 4912a27348 | |||
| 085820892f | |||
| 391a4c2ccb | |||
| 92363c8185 | |||
| 73b6c01cf9 | |||
| f703344485 | |||
| d9049095e3 | |||
| b2f0f91712 | |||
| ac93a5385e | |||
| 644ceed436 | |||
| 77ea55eda2 | |||
| 0f83cfc9b9 | |||
| 83749fc647 | |||
| bd5f8a9b15 | |||
| 8889be8ca2 | |||
| 6d58222143 | |||
| c3019b741c | |||
| b5b2103372 | |||
| b234c87048 | |||
| f982aa5e0c | |||
| 22823328c0 | |||
| 323e49e375 | |||
| 526e4ac198 | |||
| 7469813e37 | |||
| 8eb4e40b57 | |||
| 311cf18708 | |||
| 1bde0af01d | |||
| 765ec6fb32 | |||
| 5cd8329519 | |||
| 1ea9170d89 | |||
| 29fc69d022 | |||
| 09e7a82882 |
@@ -4,7 +4,7 @@ permissions:
|
||||
on: [release]
|
||||
env:
|
||||
IMAGE: RecordingTheSmile/likedream-go
|
||||
REGISTRY: git.likedream.xyz
|
||||
REGISTRY: mycoder.cc.cd
|
||||
jobs:
|
||||
Build-And-Upload:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
+3
-1
@@ -4,4 +4,6 @@
|
||||
node_modules
|
||||
.claude
|
||||
*.exe
|
||||
.serena
|
||||
.serena
|
||||
|
||||
device_code_cfworker
|
||||
@@ -0,0 +1,130 @@
|
||||
when:
|
||||
- event: push
|
||||
branch: [main, master]
|
||||
- event: tag
|
||||
branch: [v*]
|
||||
|
||||
clone:
|
||||
git:
|
||||
image: woodpeckerci/plugin-git
|
||||
settings:
|
||||
remote: http://10.10.10.128:3000/becomer/likedream.git
|
||||
|
||||
steps:
|
||||
build-image:
|
||||
image: docker
|
||||
commands:
|
||||
- docker build -t likedream-go:latest .
|
||||
- docker tag likedream-go:latest likedream-go:${CI_COMMIT_TAG:-${CI_COMMIT_SHA:0:8}}
|
||||
volumes:
|
||||
- /var/run/docker.sock:/var/run/docker.sock
|
||||
|
||||
deploy:
|
||||
image: docker
|
||||
commands: |
|
||||
# ============================================================
|
||||
# 蓝绿部署:零宕机切换 likedream-go 容器
|
||||
# 判断当前代理使用的端口,另一个作为备用
|
||||
# ============================================================
|
||||
|
||||
set -e
|
||||
|
||||
IMAGE_TAG="$CI_COMMIT_TAG"
|
||||
if [ -z "$IMAGE_TAG" ]; then IMAGE_TAG="latest"; fi
|
||||
|
||||
# 确保网络存在
|
||||
docker network create likedream-net 2>/dev/null || true
|
||||
|
||||
# 读取 1Panel 当前代理端口
|
||||
CURRENT_PORT=$(docker exec 1Panel-openresty-ESmn \
|
||||
grep -o 'proxy_pass http://10.10.10.128:[0-9]\+' \
|
||||
/www/sites/tianshen/proxy/api.conf | grep -o '[0-9]\+$')
|
||||
echo "=== 当前 1Panel 反代端口: $CURRENT_PORT ==="
|
||||
|
||||
if [ "$CURRENT_PORT" = "8091" ]; then
|
||||
PORT_ACTIVE=8091
|
||||
PORT_STANDBY=8092
|
||||
elif [ "$CURRENT_PORT" = "8092" ]; then
|
||||
PORT_ACTIVE=8092
|
||||
PORT_STANDBY=8091
|
||||
else
|
||||
echo "未知当前端口 $CURRENT_PORT,默认使用 8091/8092"
|
||||
PORT_ACTIVE=8091
|
||||
PORT_STANDBY=8092
|
||||
fi
|
||||
echo " 活跃: $PORT_ACTIVE | 备用: $PORT_STANDBY"
|
||||
|
||||
# 清理之前可能残留的临时容器
|
||||
docker rm -f likedream-go-new 2>/dev/null || true
|
||||
|
||||
echo "=== 蓝绿部署: 启动新容器 on :$PORT_STANDBY (image: $IMAGE_TAG) ==="
|
||||
|
||||
docker run -d --name likedream-go-new \
|
||||
--restart unless-stopped \
|
||||
--network likedream-net \
|
||||
-p $PORT_STANDBY:8091 \
|
||||
-v /opt/likedream-go/config.toml:/app/config.toml:ro \
|
||||
likedream-go:$IMAGE_TAG
|
||||
|
||||
# 健康检查:等待新容器就绪
|
||||
HEALTHY=false
|
||||
for i in $(seq 1 15); do
|
||||
if wget -q --timeout=3 -O /dev/null http://10.10.10.128:$PORT_STANDBY/health 2>/dev/null; then
|
||||
HEALTHY=true
|
||||
break
|
||||
fi
|
||||
echo " 等待新容器就绪... ($i/15)"
|
||||
sleep 2
|
||||
done
|
||||
|
||||
if [ "$HEALTHY" = false ]; then
|
||||
echo "ERROR: 新容器未通过健康检查,回滚"
|
||||
docker stop likedream-go-new
|
||||
docker rm likedream-go-new
|
||||
exit 1
|
||||
fi
|
||||
echo " 新容器已就绪 ✓"
|
||||
|
||||
# 切换 1Panel 代理到备用端口
|
||||
echo "=== 切换 1Panel 反代 :$PORT_ACTIVE → :$PORT_STANDBY ==="
|
||||
docker exec 1Panel-openresty-ESmn \
|
||||
sed -i "s|:$PORT_ACTIVE|:$PORT_STANDBY|g" \
|
||||
/www/sites/tianshen/proxy/api.conf
|
||||
docker exec 1Panel-openresty-ESmn nginx -s reload
|
||||
echo " Nginx 热加载完成(零中断)✓"
|
||||
|
||||
# 停止旧容器
|
||||
echo "=== 清理旧容器 ==="
|
||||
docker stop likedream-go 2>/dev/null || true
|
||||
docker rm likedream-go 2>/dev/null || true
|
||||
|
||||
# 在活跃端口启动正式容器
|
||||
echo "=== 启动正式容器 on :$PORT_ACTIVE ==="
|
||||
docker run -d --name likedream-go \
|
||||
--restart unless-stopped \
|
||||
--network likedream-net \
|
||||
-p $PORT_ACTIVE:8091 \
|
||||
-v /opt/likedream-go/config.toml:/app/config.toml:ro \
|
||||
likedream-go:$IMAGE_TAG
|
||||
|
||||
# 连接到验证码服务网络
|
||||
docker network connect ocr-go_default likedream-go 2>/dev/null || true
|
||||
|
||||
# 切回 1Panel 代理到活跃端口
|
||||
echo "=== 切回 1Panel 反代 :$PORT_STANDBY → :$PORT_ACTIVE ==="
|
||||
docker exec 1Panel-openresty-ESmn \
|
||||
sed -i "s|:$PORT_STANDBY|:$PORT_ACTIVE|g" \
|
||||
/www/sites/tianshen/proxy/api.conf
|
||||
docker exec 1Panel-openresty-ESmn nginx -s reload
|
||||
echo " Nginx 热加载完成(零中断)✓"
|
||||
|
||||
# 清理临时容器和旧镜像
|
||||
echo "=== 清理 ==="
|
||||
docker rm -f likedream-go-new 2>/dev/null || true
|
||||
# 保留 latest 和最近 3 个 tag 镜像
|
||||
docker images likedream-go --format '{{.Tag}}' | grep -v latest | head -n -3 | while read tag; do
|
||||
docker rmi likedream-go:$tag 2>/dev/null || true
|
||||
done
|
||||
echo "=== 部署完成 ✓ ==="
|
||||
volumes:
|
||||
- /var/run/docker.sock:/var/run/docker.sock
|
||||
+8
-11
@@ -1,20 +1,17 @@
|
||||
FROM gocv/opencv:latest AS backend-builder
|
||||
RUN echo "开始安装依赖包..."
|
||||
RUN apt update && apt install -y upx-ucl
|
||||
|
||||
FROM golang:1.25-alpine AS backend-builder
|
||||
WORKDIR /build
|
||||
ENV GOPROXY=https://goproxy.cn,direct
|
||||
COPY . .
|
||||
ENV CGO_ENABLED=1
|
||||
RUN echo "开始打包二进制文件"
|
||||
|
||||
RUN go build -ldflags="-s -w" -o bin/server
|
||||
RUN echo "开始压缩二进制文件"
|
||||
|
||||
RUN apk add --no-cache upx
|
||||
|
||||
RUN upx --best --lzma bin/server
|
||||
|
||||
FROM gocv/opencv:latest
|
||||
|
||||
FROM alpine:latest
|
||||
COPY --from=backend-builder /build/bin/server /app/server
|
||||
#COPY static /app/static
|
||||
#COPY static/*.so /app/
|
||||
|
||||
WORKDIR /app
|
||||
EXPOSE 8080
|
||||
RUN ls -a
|
||||
|
||||
@@ -2,4 +2,4 @@ ent-new:
|
||||
cd internal;go run -mod=mod entgo.io/ent/cmd/ent new $(Name)
|
||||
|
||||
ent-gen:
|
||||
go run . g
|
||||
go run main.go g
|
||||
+2
-2
@@ -50,8 +50,8 @@ func Run() {
|
||||
g.Log().Info(ctx, "server is going to shut down...")
|
||||
api.StopServer()
|
||||
g.Log().Info(ctx, "api server stopped")
|
||||
//schedule.Stop()
|
||||
//g.Log().Info(ctx, "schedule stopped")
|
||||
schedule.Stop()
|
||||
g.Log().Info(ctx, "schedule stopped")
|
||||
redis.Shutdown()
|
||||
g.Log().Info(ctx, "redis client closed")
|
||||
os.Exit(0)
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"likedream-goframe/internal/application/cpdaily/login"
|
||||
|
||||
"github.com/gogf/gf/v2/os/gctx"
|
||||
)
|
||||
|
||||
func main() {
|
||||
const testSmsPhoneNumber = ""
|
||||
handler := login.SmsLoginHandler{}
|
||||
ctx := gctx.GetInitCtx()
|
||||
sendSmsData := handler.SendSms(ctx, testSmsPhoneNumber)
|
||||
// 读取验证码手动输入
|
||||
var code string
|
||||
println("请输入收到的验证码:")
|
||||
_, _ = fmt.Scanln(&code)
|
||||
loginData := handler.LoginByCode(ctx, testSmsPhoneNumber, code, sendSmsData.CookieJar)
|
||||
handler.SetLoginCookie(ctx, sendSmsData.CookieJar, loginData)
|
||||
}
|
||||
+39
-10
@@ -1,13 +1,30 @@
|
||||
[ocr]
|
||||
url = "https://devocr.likedream.xyz"
|
||||
|
||||
# ===== 应用品牌配置 =====
|
||||
# brand: "default"(老系统/若梦系统/MD3风格邮件) 或 "new"(新系统/天神系统/shadcn风格邮件)
|
||||
# 未设置时默认使用 "default"
|
||||
[app]
|
||||
brand = "default"
|
||||
name = "若梦系统"
|
||||
copyright = "© 2025 若梦系统. 保留所有权利."
|
||||
# 邮件中的链接地址(会覆盖 server 段中的同名配置)
|
||||
# emailConfirmBaseUrl = "https://ujob.us.ci/confirm/"
|
||||
# emailResetPasswordUrl = "https://ujob.us.ci/reset-password/"
|
||||
# balanceMigrateUrl = "https://ujob.us.ci/balance-migrate/"
|
||||
|
||||
[quesbank]
|
||||
url = "http://tk.swk.tw/api/search.php"
|
||||
key = "sk-acacac439b346036df45e90c85c177c375568ef72c469e49"
|
||||
|
||||
[database]
|
||||
type = "mysql"
|
||||
dsn = "test_likedream_go:EpXckXmxawDEQGxJ@tcp(database.out:3306)/test_likedream_go?parseTime=true"
|
||||
dsn2 = "host=database.out user=dev password=JPmKjmpcaG3EYAJZ dbname=dev port=5432 sslmode=disable TimeZone=Asia/Shanghai"
|
||||
# host=localhost user=gorm password=gorm dbname=gorm port=9920 sslmode=disable TimeZone=Asia/Shanghai
|
||||
dsn = "test_likedream_go:EpXckXmxawDEQGxJ@tcp(database.out:3306)/test_likedream_go?parseTime=true&loc=Asia%2FShanghai"
|
||||
dsn2 = "host=pg.out user=dev password=JPmKjmpcaG3EYAJZ dbname=dev port=5432 sslmode=disable TimeZone=Asia/Shanghai"
|
||||
#type = "postgres"
|
||||
#dsn = "host=pg.out user=dev password=JPmKjmpcaG3EYAJZ dbname=dev port=5432 sslmode=disable TimeZone=Asia/Shanghai"
|
||||
[server]
|
||||
port = 8090
|
||||
port = 8091
|
||||
url = "https://devocr.likedream.xyz"
|
||||
openapiPath= "/api.json"
|
||||
swaggerPath= "/swagger"
|
||||
@@ -18,8 +35,10 @@ balanceMigrateUrl = "http://localhost:3200/balance-migrate/"
|
||||
# 可选值: dev, prod
|
||||
mode = "dev"
|
||||
|
||||
[ruoliCaptcha]
|
||||
baseUrl = ""
|
||||
[captcha]
|
||||
#baseUrl = "http://captcha-resolver.out"
|
||||
baseUrl = "http://likedream-go-captcha:8890"
|
||||
apiKey = "baca122..."
|
||||
[redis]
|
||||
url = "redis://:jhkdjhkjdhsIUTYURTU_pEGmFP@database.out:56379"
|
||||
|
||||
@@ -28,10 +47,10 @@ url = "redis://:jhkdjhkjdhsIUTYURTU_pEGmFP@database.out:56379"
|
||||
smtpHost = "smtp-mail.outlook.com"
|
||||
smtpPort = 587
|
||||
# 发件人邮箱和密码(需要使用授权码,不是登录密码)
|
||||
smtpUsername = "demo@likedream.xyz"
|
||||
smtpUsername = "reply@likedream.xyz"
|
||||
smtpPassword = "baca122..."
|
||||
# 发件人邮箱和显示名称
|
||||
fromEmail = "demo@likedream.xyz"
|
||||
fromEmail = "reply@likedream.xyz"
|
||||
fromName = "若梦系统"
|
||||
|
||||
|
||||
@@ -42,6 +61,16 @@ baseUrl = "http://s3.out"
|
||||
bucket = "likedream"
|
||||
|
||||
[cpdaily]
|
||||
appVersion = "9.7.2"
|
||||
appVersion = "9.8.8"
|
||||
systemVersion = "iOS 17"
|
||||
calVersion = "firstv"
|
||||
calVersion = "firstv"
|
||||
sliderIds = ["1018764144934965", "sdhyxy", "hqdx"]
|
||||
|
||||
|
||||
|
||||
[basePoint]
|
||||
cpdailyTask = "0.5"
|
||||
cpdailyLeave = "4"
|
||||
cpdailyPhone = "0.25"
|
||||
[task]
|
||||
concurrency = 80
|
||||
@@ -0,0 +1,18 @@
|
||||
services:
|
||||
likedream-go:
|
||||
image: likedream-go:latest
|
||||
container_name: likedream-go
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
- "8091:8091"
|
||||
volumes:
|
||||
- /opt/likedream-go/config.toml:/app/config.toml:ro
|
||||
networks:
|
||||
- likedream-net
|
||||
- ocr-go_default
|
||||
|
||||
networks:
|
||||
likedream-net:
|
||||
external: true
|
||||
ocr-go_default:
|
||||
external: true
|
||||
@@ -0,0 +1,289 @@
|
||||
# AI 答题配置接口文档
|
||||
|
||||
## 概述
|
||||
|
||||
AI 答题功能用于在刷课过程中自动完成作业任务点。系统支持多种 AI 服务商,管理员可以通过配置接口设置 AI 参数。
|
||||
|
||||
核心代码位于 `internal/application/quesbank` 包,提供通用的题库答题能力,可被不同平台(学习通、智慧教育等)复用。
|
||||
|
||||
## 接口列表
|
||||
|
||||
### 1. 获取支持的 AI 类型列表
|
||||
|
||||
获取系统支持的所有 AI 类型及其说明。
|
||||
|
||||
**请求**
|
||||
|
||||
```
|
||||
GET /v1/system/ai-config/types
|
||||
```
|
||||
|
||||
**响应**
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 0,
|
||||
"message": "success",
|
||||
"data": {
|
||||
"types": [
|
||||
{
|
||||
"type": "chatglm",
|
||||
"name": "智谱AI (ChatGLM)",
|
||||
"description": "智谱AI大模型,支持glm-4等模型",
|
||||
"needUrl": false
|
||||
},
|
||||
{
|
||||
"type": "xinghuo",
|
||||
"name": "讯飞星火",
|
||||
"description": "讯飞星火认知大模型",
|
||||
"needUrl": false
|
||||
},
|
||||
{
|
||||
"type": "tongyi",
|
||||
"name": "通义千问",
|
||||
"description": "阿里云通义千问大模型",
|
||||
"needUrl": false
|
||||
},
|
||||
{
|
||||
"type": "doubao",
|
||||
"name": "豆包",
|
||||
"description": "字节跳动豆包大模型",
|
||||
"needUrl": false
|
||||
},
|
||||
{
|
||||
"type": "openai",
|
||||
"name": "OpenAI",
|
||||
"description": "OpenAI GPT系列模型",
|
||||
"needUrl": false
|
||||
},
|
||||
{
|
||||
"type": "other",
|
||||
"name": "其他 (兼容OpenAI格式)",
|
||||
"description": "支持任何兼容OpenAI API格式的服务",
|
||||
"needUrl": true
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 2. 获取当前 AI 配置
|
||||
|
||||
获取当前系统配置的 AI 参数(API密钥会脱敏显示)。
|
||||
|
||||
**请求**
|
||||
|
||||
```
|
||||
GET /v1/system/ai-config
|
||||
```
|
||||
|
||||
**响应**
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 0,
|
||||
"message": "success",
|
||||
"data": {
|
||||
"type": "chatglm",
|
||||
"url": "",
|
||||
"model": "glm-4",
|
||||
"apiKey": "sk-1****************************xyz"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**字段说明**
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
|------|------|------|
|
||||
| type | string | AI类型,可选值见类型列表 |
|
||||
| url | string | API地址,仅 `other` 类型需要 |
|
||||
| model | string | 模型名称 |
|
||||
| apiKey | string | API密钥(脱敏显示) |
|
||||
|
||||
---
|
||||
|
||||
### 3. 更新 AI 配置
|
||||
|
||||
更新系统的 AI 配置参数。
|
||||
|
||||
**请求**
|
||||
|
||||
```
|
||||
PUT /v1/system/ai-config
|
||||
```
|
||||
|
||||
**权限**
|
||||
|
||||
需要 `sys:ai:edit` 权限
|
||||
|
||||
**请求体**
|
||||
|
||||
```json
|
||||
{
|
||||
"type": "chatglm",
|
||||
"url": "",
|
||||
"model": "glm-4",
|
||||
"apiKey": "sk-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
|
||||
}
|
||||
```
|
||||
|
||||
**参数说明**
|
||||
|
||||
| 参数 | 类型 | 必填 | 说明 |
|
||||
|------|------|------|------|
|
||||
| type | string | 是 | AI类型,可选值:`chatglm`, `xinghuo`, `tongyi`, `doubao`, `openai`, `other` |
|
||||
| url | string | 否 | API地址,当 type 为 `other` 时必填 |
|
||||
| model | string | 否 | 模型名称,不填则使用默认模型 |
|
||||
| apiKey | string | 是 | API密钥 |
|
||||
|
||||
**各 AI 类型默认模型**
|
||||
|
||||
| AI类型 | 默认模型 |
|
||||
|--------|----------|
|
||||
| chatglm | glm-4 |
|
||||
| xinghuo | generalv3.5 |
|
||||
| tongyi | qwen-plus |
|
||||
| doubao | 需指定 |
|
||||
| openai | gpt-3.5-turbo |
|
||||
| other | 需指定 |
|
||||
|
||||
**响应**
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 0,
|
||||
"message": "AI配置更新成功"
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 4. 测试 AI 配置
|
||||
|
||||
测试 AI 配置是否可用,发送测试消息验证连接。
|
||||
|
||||
**请求**
|
||||
|
||||
```
|
||||
POST /v1/system/ai-config/test
|
||||
```
|
||||
|
||||
**权限**
|
||||
|
||||
需要 `sys:ai:edit` 权限
|
||||
|
||||
**请求体**
|
||||
|
||||
```json
|
||||
{
|
||||
"type": "chatglm",
|
||||
"url": "",
|
||||
"model": "glm-4",
|
||||
"apiKey": "sk-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
|
||||
}
|
||||
```
|
||||
|
||||
**响应**
|
||||
|
||||
成功:
|
||||
```json
|
||||
{
|
||||
"code": 0,
|
||||
"message": "success",
|
||||
"data": {
|
||||
"success": true,
|
||||
"message": "AI连接测试成功,响应: 测试成功..."
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
失败:
|
||||
```json
|
||||
{
|
||||
"code": 0,
|
||||
"message": "success",
|
||||
"data": {
|
||||
"success": false,
|
||||
"message": "AI连接测试失败: failed to execute request: ..."
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## AI 服务商配置指南
|
||||
|
||||
### 智谱AI (ChatGLM)
|
||||
|
||||
1. 访问 [智谱AI开放平台](https://open.bigmodel.cn/)
|
||||
2. 注册并创建 API Key
|
||||
3. 配置参数:
|
||||
- type: `chatglm`
|
||||
- model: `glm-4` 或 `glm-3-turbo`
|
||||
- apiKey: 你的 API Key
|
||||
|
||||
### 讯飞星火
|
||||
|
||||
1. 访问 [讯飞开放平台](https://www.xfyun.cn/)
|
||||
2. 创建应用并获取 API Key
|
||||
3. 配置参数:
|
||||
- type: `xinghuo`
|
||||
- model: `generalv3.5` 或其他版本
|
||||
- apiKey: 你的 API Key
|
||||
|
||||
### 通义千问
|
||||
|
||||
1. 访问 [阿里云百炼](https://dashscope.aliyun.com/)
|
||||
2. 开通服务并获取 API Key
|
||||
3. 配置参数:
|
||||
- type: `tongyi`
|
||||
- model: `qwen-plus` 或 `qwen-turbo`
|
||||
- apiKey: 你的 API Key
|
||||
|
||||
### 豆包
|
||||
|
||||
1. 访问 [火山引擎](https://www.volcengine.com/)
|
||||
2. 开通豆包大模型服务
|
||||
3. 配置参数:
|
||||
- type: `doubao`
|
||||
- model: 你的模型端点 ID
|
||||
- apiKey: 你的 API Key
|
||||
|
||||
### OpenAI
|
||||
|
||||
1. 访问 [OpenAI Platform](https://platform.openai.com/)
|
||||
2. 创建 API Key
|
||||
3. 配置参数:
|
||||
- type: `openai`
|
||||
- model: `gpt-3.5-turbo` 或 `gpt-4`
|
||||
- apiKey: 你的 API Key
|
||||
|
||||
### 其他兼容服务
|
||||
|
||||
支持任何兼容 OpenAI API 格式的服务(如 DeepSeek、Moonshot 等)。
|
||||
|
||||
配置参数:
|
||||
- type: `other`
|
||||
- url: API 地址(如 `https://api.deepseek.com/v1/chat/completions`)
|
||||
- model: 模型名称
|
||||
- apiKey: 你的 API Key
|
||||
|
||||
---
|
||||
|
||||
## 使用说明
|
||||
|
||||
1. 配置 AI 后,系统在执行刷课任务时会自动使用 AI 完成作业任务点
|
||||
2. 作业答案默认仅保存不提交,需要用户手动确认后提交
|
||||
3. 如果未配置 AI,作业任务点将被跳过
|
||||
4. 建议先使用测试接口验证配置是否正确
|
||||
|
||||
## 错误码
|
||||
|
||||
| 错误码 | 说明 |
|
||||
|--------|------|
|
||||
| 400 | 参数错误或配置不完整 |
|
||||
| 403 | 无权限操作 |
|
||||
| 500 | 服务器内部错误 |
|
||||
@@ -0,0 +1,204 @@
|
||||
# 四川智慧教育刷课 API 文档
|
||||
|
||||
## 基础信息
|
||||
|
||||
- 基础路径: `/v1/course`
|
||||
- 需要认证: 是 (Bearer Token)
|
||||
- Content-Type: `application/json`
|
||||
|
||||
---
|
||||
|
||||
## 1. 查询课程列表
|
||||
|
||||
登录四川智慧教育平台,获取用户的课程列表。
|
||||
|
||||
**请求**
|
||||
|
||||
```
|
||||
POST /v1/course/smartedu-sc/query
|
||||
```
|
||||
|
||||
**请求参数**
|
||||
|
||||
| 参数名 | 类型 | 必填 | 说明 |
|
||||
|--------|------|------|------|
|
||||
| username | string | 是 | 登录账号 |
|
||||
| password | string | 是 | 登录密码 |
|
||||
|
||||
**请求示例**
|
||||
|
||||
```json
|
||||
{
|
||||
"username": "13800138000",
|
||||
"password": "your_password"
|
||||
}
|
||||
```
|
||||
|
||||
**响应示例**
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 200,
|
||||
"msg": "success",
|
||||
"data": [
|
||||
{
|
||||
"id": "course_id_1",
|
||||
"courseName": "课程名称1",
|
||||
"image": "https://xxx.com/image.jpg"
|
||||
},
|
||||
{
|
||||
"id": "course_id_2",
|
||||
"courseName": "课程名称2",
|
||||
"image": "https://xxx.com/image2.jpg"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 2. 添加刷课任务
|
||||
|
||||
添加一个新的刷课任务,提交后立即开始秒刷。
|
||||
|
||||
**请求**
|
||||
|
||||
```
|
||||
POST /v1/course/smartedu-sc
|
||||
```
|
||||
|
||||
**请求参数**
|
||||
|
||||
| 参数名 | 类型 | 必填 | 说明 |
|
||||
|--------|------|------|------|
|
||||
| username | string | 是 | 登录账号 |
|
||||
| password | string | 是 | 登录密码 |
|
||||
| course_name | string | 是 | 课程名称 |
|
||||
| course_id | string | 是 | 课程ID(从查询接口获取) |
|
||||
| desc | string | 否 | 备注说明 |
|
||||
|
||||
**请求示例**
|
||||
|
||||
```json
|
||||
{
|
||||
"username": "13800138000",
|
||||
"password": "your_password",
|
||||
"course_name": "教师培训课程",
|
||||
"course_id": "abc123",
|
||||
"desc": "2024年培训"
|
||||
}
|
||||
```
|
||||
|
||||
**响应示例**
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 200,
|
||||
"msg": "任务已提交,正在刷课中"
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 3. 获取刷课任务列表
|
||||
|
||||
获取当前用户的四川智慧教育刷课任务列表。
|
||||
|
||||
**请求**
|
||||
|
||||
```
|
||||
GET /v1/course/smartedu-sc
|
||||
```
|
||||
|
||||
**请求参数**
|
||||
|
||||
| 参数名 | 类型 | 必填 | 说明 |
|
||||
|--------|------|------|------|
|
||||
| username | string | 否 | 按账号筛选 |
|
||||
| status | string | 否 | 按状态筛选: not_start/waiting/doing/done/fail/pause |
|
||||
| page | int | 否 | 页码,默认1 |
|
||||
| size | int | 否 | 每页数量,默认10 |
|
||||
|
||||
**响应示例**
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 200,
|
||||
"msg": "success",
|
||||
"data": [
|
||||
{
|
||||
"id": 1,
|
||||
"createTime": "2024-12-09 10:00:00",
|
||||
"updateTime": "2024-12-09 10:05:00",
|
||||
"username": "13800138000",
|
||||
"type": "smartedu_sc",
|
||||
"course_name": "教师培训课程",
|
||||
"course_id": "abc123",
|
||||
"need": "task",
|
||||
"status": "done",
|
||||
"task_count": 20,
|
||||
"success_count": 20,
|
||||
"violence": "yes",
|
||||
"desc": "2024年培训"
|
||||
}
|
||||
],
|
||||
"total": 1
|
||||
}
|
||||
```
|
||||
|
||||
**状态说明**
|
||||
|
||||
| 状态值 | 说明 |
|
||||
|--------|------|
|
||||
| not_start | 未开始 |
|
||||
| waiting | 等待中 |
|
||||
| doing | 进行中 |
|
||||
| done | 已完成 |
|
||||
| fail | 失败 |
|
||||
| pause | 已暂停 |
|
||||
|
||||
---
|
||||
|
||||
## 4. 删除刷课任务
|
||||
|
||||
删除指定的刷课任务。
|
||||
|
||||
**请求**
|
||||
|
||||
```
|
||||
DELETE /v1/course/smartedu-sc/{id}
|
||||
```
|
||||
|
||||
**路径参数**
|
||||
|
||||
| 参数名 | 类型 | 必填 | 说明 |
|
||||
|--------|------|------|------|
|
||||
| id | int64 | 是 | 任务ID |
|
||||
|
||||
**响应示例**
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 200,
|
||||
"msg": "删除成功"
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 错误码说明
|
||||
|
||||
| 错误码 | 说明 |
|
||||
|--------|------|
|
||||
| 200 | 成功 |
|
||||
| 400 | 请求参数错误 |
|
||||
| 401 | 未授权 |
|
||||
| 403 | 无权限 |
|
||||
| 500 | 服务器内部错误 |
|
||||
|
||||
## 使用流程
|
||||
|
||||
1. 调用 **查询课程列表** 接口,获取用户在四川智慧教育平台的课程
|
||||
2. 选择需要刷的课程,调用 **添加刷课任务** 接口
|
||||
3. 任务提交后自动开始秒刷,可通过 **获取刷课任务列表** 查看进度
|
||||
4. 刷课完成后状态变为 `done`
|
||||
@@ -1,6 +1,6 @@
|
||||
module likedream-goframe
|
||||
|
||||
go 1.24.0
|
||||
go 1.25.0
|
||||
|
||||
require (
|
||||
entgo.io/ent v0.14.5
|
||||
@@ -19,22 +19,21 @@ require (
|
||||
github.com/google/uuid v1.6.0
|
||||
github.com/hibiken/asynq v0.25.1
|
||||
github.com/joomcode/errorx v1.2.0
|
||||
github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646
|
||||
github.com/redis/go-redis/v9 v9.11.0
|
||||
github.com/samber/oops v1.19.0
|
||||
github.com/shirou/gopsutil/v3 v3.24.5
|
||||
github.com/shopspring/decimal v1.4.0
|
||||
github.com/thedevsaddam/gojsonq v1.9.1
|
||||
github.com/tidwall/gjson v1.18.0
|
||||
github.com/tidwall/match v1.1.1
|
||||
github.com/wenlng/go-captcha-assets v1.0.7
|
||||
github.com/wenlng/go-captcha/v2 v2.0.4
|
||||
github.com/wneessen/go-mail v0.6.2
|
||||
github.com/yalue/onnxruntime_go v1.12.1
|
||||
github.com/xuri/excelize/v2 v2.10.0
|
||||
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
|
||||
golang.org/x/net v0.46.0
|
||||
golang.org/x/oauth2 v0.36.0
|
||||
gorm.io/driver/postgres v1.6.0
|
||||
gorm.io/gorm v1.31.1
|
||||
resty.dev/v3 v3.0.0-beta.3
|
||||
@@ -90,15 +89,20 @@ require (
|
||||
github.com/oklog/ulid/v2 v2.1.1 // indirect
|
||||
github.com/olekukonko/tablewriter v0.0.5 // indirect
|
||||
github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c // indirect
|
||||
github.com/richardlehane/mscfb v1.0.4 // indirect
|
||||
github.com/richardlehane/msoleps v1.0.4 // indirect
|
||||
github.com/rivo/uniseg v0.4.7 // indirect
|
||||
github.com/robfig/cron/v3 v3.0.1 // indirect
|
||||
github.com/samber/lo v1.51.0 // indirect
|
||||
github.com/shoenig/go-m1cpu v0.1.6 // indirect
|
||||
github.com/spf13/cast v1.9.2 // indirect
|
||||
github.com/tidwall/pretty v1.2.0 // indirect
|
||||
github.com/tiendc/go-deepcopy v1.7.1 // indirect
|
||||
github.com/tklauser/go-sysconf v0.3.12 // indirect
|
||||
github.com/tklauser/numcpus v0.6.1 // indirect
|
||||
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
|
||||
github.com/xuri/efp v0.0.1 // indirect
|
||||
github.com/xuri/nfp v0.0.2-0.20250530014748-2ddeb826f9a9 // indirect
|
||||
github.com/yusufpapurcu/wmi v1.2.4 // indirect
|
||||
github.com/zclconf/go-cty v1.14.4 // indirect
|
||||
github.com/zclconf/go-cty-yaml v1.1.0 // indirect
|
||||
@@ -110,7 +114,6 @@ require (
|
||||
golang.org/x/arch v0.0.0-20210923205945-b76863e36670 // indirect
|
||||
golang.org/x/image v0.29.0 // indirect
|
||||
golang.org/x/mod v0.28.0 // indirect
|
||||
golang.org/x/net v0.45.0 // indirect
|
||||
golang.org/x/sync v0.18.0 // indirect
|
||||
golang.org/x/sys v0.37.0 // indirect
|
||||
golang.org/x/text v0.30.0 // indirect
|
||||
|
||||
@@ -150,8 +150,6 @@ github.com/mattn/go-sqlite3 v1.14.22 h1:2gZY6PC6kBnID23Tichd1K+Z0oS6nE/XwU+Vz/5o
|
||||
github.com/mattn/go-sqlite3 v1.14.22/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
|
||||
github.com/mitchellh/go-wordwrap v1.0.1 h1:TLuKupo69TCn6TQSyGxwI1EblZZEsQ0vMlAFQflz0v0=
|
||||
github.com/mitchellh/go-wordwrap v1.0.1/go.mod h1:R62XHJLzvMFRBbcrT7m7WgmE1eOyTSsCt+hzestvNj0=
|
||||
github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646 h1:zYyBkD/k9seD2A7fsi6Oo2LfFZAehjjQMERAvZLEDnQ=
|
||||
github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646/go.mod h1:jpp1/29i3P1S/RLdc7JQKbRpFeM1dOBd8T9ki5s+AY8=
|
||||
github.com/oklog/ulid/v2 v2.1.1 h1:suPZ4ARWLOJLegGFiZZ1dFAkqzhMjL3J1TzI+5wHz8s=
|
||||
github.com/oklog/ulid/v2 v2.1.1/go.mod h1:rcEKHmBBKfef9DhnvX7y1HZBYxjXb0cP5ExxNsTT1QQ=
|
||||
github.com/olekukonko/tablewriter v0.0.5 h1:P2Ga83D34wi1o9J6Wh1mRuqd4mF/x/lgBS7N7AbDhec=
|
||||
@@ -165,6 +163,11 @@ github.com/prashantv/gostub v1.1.0 h1:BTyx3RfQjRHnUWaGF9oQos79AlQ5k8WNktv7VGvVH4
|
||||
github.com/prashantv/gostub v1.1.0/go.mod h1:A5zLQHz7ieHGG7is6LLXLz7I8+3LZzsrV0P1IAHhP5U=
|
||||
github.com/redis/go-redis/v9 v9.11.0 h1:E3S08Gl/nJNn5vkxd2i78wZxWAPNZgUNTp8WIJUAiIs=
|
||||
github.com/redis/go-redis/v9 v9.11.0/go.mod h1:huWgSWd8mW6+m0VPhJjSSQ+d6Nh1VICQ6Q5lHuCH/Iw=
|
||||
github.com/richardlehane/mscfb v1.0.4 h1:WULscsljNPConisD5hR0+OyZjwK46Pfyr6mPu5ZawpM=
|
||||
github.com/richardlehane/mscfb v1.0.4/go.mod h1:YzVpcZg9czvAuhk9T+a3avCpcFPMUWm7gK3DypaEsUk=
|
||||
github.com/richardlehane/msoleps v1.0.1/go.mod h1:BWev5JBpU9Ko2WAgmZEuiz4/u3ZYTKbjLycmwiWUfWg=
|
||||
github.com/richardlehane/msoleps v1.0.4 h1:WuESlvhX3gH2IHcd8UqyCuFY5yiq/GR/yqaSM/9/g00=
|
||||
github.com/richardlehane/msoleps v1.0.4/go.mod h1:BWev5JBpU9Ko2WAgmZEuiz4/u3ZYTKbjLycmwiWUfWg=
|
||||
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
|
||||
github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
|
||||
github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
|
||||
@@ -197,16 +200,16 @@ github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/
|
||||
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
|
||||
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
|
||||
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
|
||||
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||
github.com/thedevsaddam/gojsonq v1.9.1 h1:zQulEP43nwmq5EKrNWyIgJVbqDeMdC1qzXM/f5O15a0=
|
||||
github.com/thedevsaddam/gojsonq v1.9.1/go.mod h1:RBcQaITThgJAAYKH7FNp2onYodRz8URfsuEGpAch0NA=
|
||||
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
|
||||
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
|
||||
github.com/tidwall/gjson v1.18.0 h1:FIDeeyB800efLX89e5a8Y0BNH+LOngJyGrIWxG2FKQY=
|
||||
github.com/tidwall/gjson v1.18.0/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
|
||||
github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA=
|
||||
github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM=
|
||||
github.com/tidwall/pretty v1.2.0 h1:RWIZEg2iJ8/g6fDDYzMpobmaoGh5OLl4AXtGUGPcqCs=
|
||||
github.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU=
|
||||
github.com/tiendc/go-deepcopy v1.7.1 h1:LnubftI6nYaaMOcaz0LphzwraqN8jiWTwm416sitff4=
|
||||
github.com/tiendc/go-deepcopy v1.7.1/go.mod h1:4bKjNC2r7boYOkD2IOuZpYjmlDdzjbpTRyCx+goBCJQ=
|
||||
github.com/tklauser/go-sysconf v0.3.12 h1:0QaGUFOdQaIVdPgfITYzaTegZvdCjmYO52cSFAEVmqU=
|
||||
github.com/tklauser/go-sysconf v0.3.12/go.mod h1:Ho14jnntGE1fpdOqQEEaiKRpvIavV0hSfmBq8nJbHYI=
|
||||
github.com/tklauser/numcpus v0.6.1 h1:ng9scYS7az0Bk4OZLvrNXNSAO2Pxr1XXRAPyjhIx+Fk=
|
||||
@@ -219,8 +222,12 @@ github.com/wenlng/go-captcha/v2 v2.0.4 h1:5cSUF36ZyA03qeDMjKmeXGpbYJMXEexZIYK3Vg
|
||||
github.com/wenlng/go-captcha/v2 v2.0.4/go.mod h1:5hac1em3uXoyC5ipZ0xFv9umNM/waQvYAQdr0cx/h34=
|
||||
github.com/wneessen/go-mail v0.6.2 h1:c6V7c8D2mz868z9WJ+8zDKtUyLfZ1++uAZmo2GRFji8=
|
||||
github.com/wneessen/go-mail v0.6.2/go.mod h1:L/PYjPK3/2ZlNb2/FjEBIn9n1rUWjW+Toy531oVmeb4=
|
||||
github.com/yalue/onnxruntime_go v1.12.1 h1:joCCmBnNjHy04jK9EMP/UV6oPPqySXlRgf3gcUcnI/g=
|
||||
github.com/yalue/onnxruntime_go v1.12.1/go.mod h1:b4X26A8pekNb1ACJ58wAXgNKeUCGEAQ9dmACut9Sm/4=
|
||||
github.com/xuri/efp v0.0.1 h1:fws5Rv3myXyYni8uwj2qKjVaRP30PdjeYe2Y6FDsCL8=
|
||||
github.com/xuri/efp v0.0.1/go.mod h1:ybY/Jr0T0GTCnYjKqmdwxyxn2BQf2RcQIIvex5QldPI=
|
||||
github.com/xuri/excelize/v2 v2.10.0 h1:8aKsP7JD39iKLc6dH5Tw3dgV3sPRh8uRVXu/fMstfW4=
|
||||
github.com/xuri/excelize/v2 v2.10.0/go.mod h1:SC5TzhQkaOsTWpANfm+7bJCldzcnU/jrhqkTi/iBHBU=
|
||||
github.com/xuri/nfp v0.0.2-0.20250530014748-2ddeb826f9a9 h1:+C0TIdyyYmzadGaL/HBLbf3WdLgC29pgyhTjAT/0nuE=
|
||||
github.com/xuri/nfp v0.0.2-0.20250530014748-2ddeb826f9a9/go.mod h1:WwHg+CVyzlv/TX9xqBFXEZAuxOPxn2k1GNHwG41IIUQ=
|
||||
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
|
||||
github.com/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo0=
|
||||
github.com/yusufpapurcu/wmi v1.2.4/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0=
|
||||
@@ -244,8 +251,6 @@ 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=
|
||||
@@ -276,8 +281,10 @@ golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk=
|
||||
golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44=
|
||||
golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM=
|
||||
golang.org/x/net v0.33.0/go.mod h1:HXLR5J+9DxmrqMwG9qjGCxZ+zKXxBru04zlTvWlWuN4=
|
||||
golang.org/x/net v0.45.0 h1:RLBg5JKixCy82FtLJpeNlVM0nrSqpCRYzVU1n8kj0tM=
|
||||
golang.org/x/net v0.45.0/go.mod h1:ECOoLqd5U3Lhyeyo/QDCEVQ4sNgYsqvCZ722XogGieY=
|
||||
golang.org/x/net v0.46.0 h1:giFlY12I07fugqwPuWJi68oOnpfqFnJIJzaIIm2JVV4=
|
||||
golang.org/x/net v0.46.0/go.mod h1:Q9BGdFy1y4nkUwiLvT5qtyhAnEHgnQ/zd8PfU6nc210=
|
||||
golang.org/x/oauth2 v0.36.0 h1:peZ/1z27fi9hUOFCAZaHyrpWG5lwe0RJEEEeH0ThlIs=
|
||||
golang.org/x/oauth2 v0.36.0/go.mod h1:YDBUJMTkDnJS+A4BP4eZBjCqtokkg1hODuPjwiGPO7Q=
|
||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
|
||||
+25
-15
@@ -2,10 +2,14 @@ package api
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"likedream-goframe/internal/common/cst"
|
||||
"likedream-goframe/internal/controller/agent"
|
||||
"likedream-goframe/internal/controller/balance"
|
||||
"likedream-goframe/internal/controller/card"
|
||||
"likedream-goframe/internal/controller/common"
|
||||
"likedream-goframe/internal/controller/config"
|
||||
"likedream-goframe/internal/controller/course"
|
||||
"likedream-goframe/internal/controller/cpdaily"
|
||||
"likedream-goframe/internal/controller/menu"
|
||||
"likedream-goframe/internal/controller/music"
|
||||
@@ -23,47 +27,53 @@ import (
|
||||
|
||||
func StartServer(ctx context.Context) {
|
||||
s := g.Server()
|
||||
// 设置端口
|
||||
|
||||
port, err := g.Cfg().Get(ctx, cst.ConfigServerPort)
|
||||
utils.OkOrPanic(err)
|
||||
|
||||
s.SetNameToUriType(3)
|
||||
// 设置中间件
|
||||
s.Use(
|
||||
middleware.ExceptionHandler,
|
||||
)
|
||||
s.Use(middleware.ExceptionHandler)
|
||||
s.BindStatusHandler(404, common.NewBaseV1().NotFound)
|
||||
// 业务路由
|
||||
|
||||
s.Group("/v1", func(group *ghttp.RouterGroup) {
|
||||
group.Bind(user_login.NewUserLoginV1())
|
||||
// 系统设置路由
|
||||
group.Bind(config.NewMailOAuthCtrlV1())
|
||||
|
||||
systemGroup := group.Group("/system").Middleware(middleware.AuthHandler, middleware.PermissionHandler)
|
||||
systemGroup.Bind(role.NewV1())
|
||||
systemGroup.Bind(menu.NewV1())
|
||||
systemGroup.Bind(user.NewUserCtrlV1())
|
||||
//systemGroup.Bind(agent.NewV1())
|
||||
// 用户信息路由
|
||||
systemGroup.Bind(config.NewConfigCtrlV1())
|
||||
systemGroup.Bind(config.NewAIConfigCtrlV1())
|
||||
|
||||
userGroup := group.Group("/user").Middleware(middleware.AuthHandler, middleware.PermissionHandler)
|
||||
userGroup.Bind(user_login.NewUserInfoV1())
|
||||
// 代理信息路由
|
||||
userGroup.Bind(balance.NewBalanceCtrlV1())
|
||||
|
||||
agentGroup := group.Group("/agent").Middleware(middleware.AuthHandler, middleware.PermissionHandler)
|
||||
agentGroup.Bind(agent.NewV1())
|
||||
agentGroup.Bind(card.NewCardCtrlV1())
|
||||
// 今日校园
|
||||
|
||||
cpdailyGroup := group.Group("/cpdaily").Middleware(middleware.AuthHandler, middleware.PermissionHandler)
|
||||
cpdailyGroup.Bind(cpdaily.NewCpUserCtrlV1())
|
||||
cpdailyGroup.Bind(cpdaily.NewCpTaskCtrlV1())
|
||||
cpdailyGroup.Bind(cpdaily.NewCpImageCtrlV1())
|
||||
cpdailyGroup.Bind(cpdaily.NewCpTaskLogCtrlV1())
|
||||
cpdailyGroup.Bind(cpdaily.NewCpLeaveCtrlV1())
|
||||
|
||||
musicGroup := group.Group("/music").Middleware(middleware.AuthHandler, middleware.PermissionHandler)
|
||||
musicGroup.Bind(music.NewMusicCtrlV1())
|
||||
|
||||
courseGroup := group.Group("/course").Middleware(middleware.AuthHandler, middleware.PermissionHandler)
|
||||
courseGroup.Bind(course.NewXxtCtrlV1())
|
||||
courseGroup.Bind(course.NewSmartEduSCCtrlV1())
|
||||
})
|
||||
|
||||
s.SetPort(port.Int())
|
||||
|
||||
s.GetOpenApi().Config.IgnorePkgPath = true
|
||||
// 设置全局异常捕获
|
||||
e := s.Start()
|
||||
utils.OkOrPanic(e, oops.WithContext(ctx).Wrap)
|
||||
|
||||
err = s.Start()
|
||||
utils.OkOrPanic(err, oops.WithContext(ctx).Wrap)
|
||||
}
|
||||
|
||||
func StopServer() {
|
||||
|
||||
@@ -0,0 +1,60 @@
|
||||
package v1
|
||||
|
||||
import (
|
||||
"github.com/gogf/gf/v2/frame/g"
|
||||
)
|
||||
|
||||
// AIConfigGetReq 获取AI配置请求
|
||||
type AIConfigGetReq struct {
|
||||
g.Meta `path:"/ai-config" method:"get" tags:"AIConfig" summary:"获取AI配置"`
|
||||
}
|
||||
|
||||
// AIConfigGetResp AI配置响应
|
||||
type AIConfigGetResp struct {
|
||||
Type string `json:"type"` // AI类型: chatglm, xinghuo, tongyi, doubao, openai, other
|
||||
URL string `json:"url"` // API地址(other类型必填)
|
||||
Model string `json:"model"` // 模型名称
|
||||
APIKey string `json:"apiKey"` // API密钥(返回时脱敏)
|
||||
}
|
||||
|
||||
// AIConfigUpdateReq 更新AI配置请求
|
||||
type AIConfigUpdateReq struct {
|
||||
g.Meta `path:"/ai-config" method:"put" tags:"AIConfig" summary:"更新AI配置" permission:"sys:ai:edit"`
|
||||
Type string `json:"type" v:"required|in:chatglm,xinghuo,tongyi,doubao,openai,other#AI类型不能为空|AI类型不合法"`
|
||||
URL string `json:"url"` // API地址(other类型必填)
|
||||
Model string `json:"model"` // 模型名称
|
||||
APIKey string `json:"apiKey" v:"required#API密钥不能为空"` // API密钥
|
||||
}
|
||||
|
||||
// AIConfigTestReq 测试AI配置请求
|
||||
type AIConfigTestReq struct {
|
||||
g.Meta `path:"/ai-config/test" method:"post" tags:"AIConfig" summary:"测试AI配置" permission:"sys:ai:edit"`
|
||||
Type string `json:"type" v:"required|in:chatglm,xinghuo,tongyi,doubao,openai,other#AI类型不能为空|AI类型不合法"`
|
||||
URL string `json:"url"`
|
||||
Model string `json:"model"`
|
||||
APIKey string `json:"apiKey" v:"required#API密钥不能为空"`
|
||||
}
|
||||
|
||||
// AIConfigTestResp 测试AI配置响应
|
||||
type AIConfigTestResp struct {
|
||||
Success bool `json:"success"` // 是否成功
|
||||
Message string `json:"message"` // 测试结果消息
|
||||
}
|
||||
|
||||
// AITypeListReq 获取支持的AI类型列表请求
|
||||
type AITypeListReq struct {
|
||||
g.Meta `path:"/ai-config/types" method:"get" tags:"AIConfig" summary:"获取支持的AI类型列表"`
|
||||
}
|
||||
|
||||
// AITypeItem AI类型项
|
||||
type AITypeItem struct {
|
||||
Type string `json:"type"` // 类型标识
|
||||
Name string `json:"name"` // 显示名称
|
||||
Description string `json:"description"` // 描述
|
||||
NeedURL bool `json:"needUrl"` // 是否需要自定义URL
|
||||
}
|
||||
|
||||
// AITypeListResp AI类型列表响应
|
||||
type AITypeListResp struct {
|
||||
Types []AITypeItem `json:"types"`
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
package v1
|
||||
|
||||
import "github.com/gogf/gf/v2/frame/g"
|
||||
|
||||
type BalanceRecordListReq struct {
|
||||
g.Meta `path:"/balance/records" method:"get" tags:"Balance" summary:"获取用户余额变动记录列表"`
|
||||
Type string `json:"type" dc:"变动类型: increase-增加, decrease-减少"`
|
||||
Page int `json:"page" d:"1"`
|
||||
Size int `json:"size" d:"10"`
|
||||
}
|
||||
@@ -0,0 +1,85 @@
|
||||
package v1
|
||||
|
||||
import (
|
||||
"github.com/gogf/gf/v2/frame/g"
|
||||
)
|
||||
|
||||
type ConfigGetReq struct {
|
||||
g.Meta `path:"/config/{key}" method:"get" tags:"Config" summary:"获取配置"`
|
||||
Key string `json:"key" v:"required#配置键不能为空"`
|
||||
}
|
||||
|
||||
type ConfigUpdateReq struct {
|
||||
g.Meta `path:"/config/{key}" method:"put" tags:"Config" summary:"更新配置" permission:"sys:config:edit"`
|
||||
Key string `json:"key" v:"required#配置键不能为空"`
|
||||
Value string `json:"value" v:"required#配置值不能为空"`
|
||||
Description string `json:"description"`
|
||||
}
|
||||
|
||||
type ConfigListReq struct {
|
||||
g.Meta `path:"/config" method:"get" tags:"Config" summary:"获取配置列表"`
|
||||
Key string `json:"key"`
|
||||
Page int `json:"page" d:"1"`
|
||||
Size int `json:"size" d:"10"`
|
||||
}
|
||||
|
||||
type ConfigPublicGetReq struct {
|
||||
g.Meta `path:"/config/public/{key}" method:"get" tags:"Config" summary:"获取公开配置" auth:"false"`
|
||||
Key string `json:"key" v:"required#配置键不能为空"`
|
||||
}
|
||||
|
||||
type SmtpConfigGetReq struct {
|
||||
g.Meta `path:"/config/smtp" method:"get" tags:"Config" summary:"获取邮件配置" permission:"sys:config:edit"`
|
||||
}
|
||||
|
||||
type SmtpConfigUpdateReq struct {
|
||||
g.Meta `path:"/config/smtp" method:"put" tags:"Config" summary:"更新邮件配置" permission:"sys:config:edit"`
|
||||
|
||||
AuthMode string `json:"authMode" v:"required#请选择邮件认证模式"`
|
||||
|
||||
SmtpHost string `json:"smtpHost"`
|
||||
SmtpPort int `json:"smtpPort"`
|
||||
SmtpUsername string `json:"smtpUsername"`
|
||||
SmtpPassword string `json:"smtpPassword"`
|
||||
|
||||
FromEmail string `json:"fromEmail" v:"required|email#发件人邮箱不能为空|请输入正确的发件人邮箱"`
|
||||
FromName string `json:"fromName" v:"required#发件人名称不能为空"`
|
||||
|
||||
OAuthTenant string `json:"oauthTenant"`
|
||||
OAuthClientID string `json:"oauthClientId"`
|
||||
OAuthClientSecret string `json:"oauthClientSecret"`
|
||||
OAuthRedirectURL string `json:"oauthRedirectUrl"`
|
||||
OAuthRefreshToken string `json:"oauthRefreshToken"`
|
||||
OAuthScope string `json:"oauthScope"`
|
||||
}
|
||||
|
||||
type SmtpTestReq struct {
|
||||
g.Meta `path:"/config/smtp/test" method:"post" tags:"Config" summary:"发送测试邮件" permission:"sys:config:edit"`
|
||||
ToEmail string `json:"toEmail" v:"required|email#收件人邮箱不能为空|请输入正确的收件人邮箱"`
|
||||
}
|
||||
|
||||
type OutlookOAuthAuthorizeURLReq struct {
|
||||
g.Meta `path:"/config/smtp/oauth/outlook/authorize-url" method:"post" tags:"Config" summary:"获取 Outlook OAuth2 授权地址" permission:"sys:config:edit"`
|
||||
|
||||
OAuthTenant string `json:"oauthTenant"`
|
||||
OAuthClientID string `json:"oauthClientId" v:"required#Client ID 不能为空"`
|
||||
OAuthClientSecret string `json:"oauthClientSecret" v:"required#Client Secret 不能为空"`
|
||||
OAuthScope string `json:"oauthScope"`
|
||||
FromEmail string `json:"fromEmail" v:"required|email#发件人邮箱不能为空|请输入正确的发件人邮箱"`
|
||||
FrontendOrigin string `json:"frontendOrigin" v:"required#前端来源地址不能为空"`
|
||||
CallbackBaseURL string `json:"callbackBaseUrl" v:"required#API 基础地址不能为空"`
|
||||
}
|
||||
|
||||
type OutlookOAuthAuthorizeURLResp struct {
|
||||
AuthURL string `json:"authUrl"`
|
||||
CallbackURL string `json:"callbackUrl"`
|
||||
State string `json:"state"`
|
||||
}
|
||||
|
||||
type OutlookOAuthCallbackReq struct {
|
||||
g.Meta `path:"/mail/oauth/outlook/callback" method:"get" tags:"MailOAuth" summary:"Outlook OAuth2 回调" auth:"false"`
|
||||
Code string `json:"code"`
|
||||
State string `json:"state"`
|
||||
Error string `json:"error"`
|
||||
ErrorDescription string `json:"error_description"`
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
package v1
|
||||
|
||||
import (
|
||||
"likedream-goframe/internal/ent/courseentity"
|
||||
|
||||
"github.com/gogf/gf/v2/frame/g"
|
||||
)
|
||||
|
||||
type CourseListReq struct {
|
||||
g.Meta `path:"/course" method:"get" tags:"Course" summary:"获取刷课列表"`
|
||||
Type courseentity.Type `json:"type"`
|
||||
Username string `json:"username"`
|
||||
Status courseentity.Status `json:"status"`
|
||||
Page int `json:"page" d:"1"`
|
||||
Size int `json:"size" d:"10"`
|
||||
}
|
||||
|
||||
type AddCourseReq struct {
|
||||
g.Meta `path:"/course" method:"post" tags:"Course" summary:"添加刷课"`
|
||||
Username string `json:"username" v:"required#账号不能为空"`
|
||||
Password string `json:"password" v:"required#密码不能为空"`
|
||||
Type courseentity.Type `json:"type" v:"required#类型不能为空"`
|
||||
CourseName string `json:"course_name" v:"required#课程名称不能为空"`
|
||||
CourseID string `json:"course_id" v:"required#课程ID不能为空"`
|
||||
Need courseentity.Need `json:"need" v:"required#需要刷的内容不能为空"`
|
||||
Violence courseentity.Violence `json:"violence" d:"no"`
|
||||
Desc string `json:"desc"`
|
||||
}
|
||||
|
||||
type UpdateCoursePasswordReq struct {
|
||||
g.Meta `path:"/course/password/{id}" method:"put" tags:"Course" summary:"修改刷课密码"`
|
||||
ID int64 `json:"id"`
|
||||
Password string `json:"password" v:"required#密码不能为空"`
|
||||
}
|
||||
|
||||
type RestartCourseReq struct {
|
||||
g.Meta `path:"/course/restart/{id}" method:"post" tags:"Course" summary:"重刷课程"`
|
||||
ID int64 `json:"id"`
|
||||
}
|
||||
|
||||
type PauseCourseReq struct {
|
||||
g.Meta `path:"/course/pause/{id}" method:"post" tags:"Course" summary:"暂停刷课"`
|
||||
ID int64 `json:"id"`
|
||||
}
|
||||
|
||||
type DeleteCourseReq struct {
|
||||
g.Meta `path:"/course/{id}" method:"delete" tags:"Course" summary:"删除刷课"`
|
||||
ID int64 `json:"id"`
|
||||
}
|
||||
|
||||
type QueryCourseReq struct {
|
||||
g.Meta `path:"/course/query" method:"post" tags:"Course" summary:"查询课程列表"`
|
||||
Username string `json:"username" v:"required#账号不能为空"`
|
||||
Password string `json:"password" v:"required#密码不能为空"`
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
package v1
|
||||
|
||||
// TaskStatisticsItem 每日任务统计项(旧版)
|
||||
type TaskStatisticsItem struct {
|
||||
Date string `json:"date"` // 日期 格式:2006-01-02
|
||||
PendingTask int `json:"pendingTask"` // 需要完成的任务数
|
||||
SuccessTask int `json:"successTask"` // 成功执行的任务数
|
||||
}
|
||||
|
||||
// TaskStatisticsV2Item 每日任务统计项(新版:从日志表按任务id去重)
|
||||
type TaskStatisticsV2Item struct {
|
||||
Date string `json:"date"` // 日期 格式:2006-01-02
|
||||
SuccessTask int `json:"successTask"` // 成功任务数(任务id去重)
|
||||
FailTask int `json:"failTask"` // 失败任务数(任务id去重)
|
||||
PendingTask int `json:"pendingTask"` // 待执行任务数(仅当天)
|
||||
SuccessTaskIds []int64 `json:"successTaskIds,omitempty"` // 成功任务ID列表
|
||||
FailTaskIds []int64 `json:"failTaskIds,omitempty"` // 失败任务ID列表
|
||||
PendingTaskIds []int64 `json:"pendingTaskIds,omitempty"` // 待执行任务ID列表(仅当天)
|
||||
}
|
||||
|
||||
// DashboardSummary 首页概览数据(新版)
|
||||
type DashboardSummary struct {
|
||||
Balance string `json:"balance"` // 当前余额
|
||||
TodayCost string `json:"todayCost"` // 今日消费
|
||||
YesterdayCost string `json:"yesterdayCost"` // 昨日消费
|
||||
Week1Cost string `json:"week1Cost"` // 近7日消费
|
||||
Week2Cost string `json:"week2Cost"` // 上7日消费
|
||||
TodayTaskCount int `json:"todayTaskCount"` // 今日任务数(成功+失败,任务id去重)
|
||||
YdayTaskCount int `json:"ydayTaskCount"` // 昨日任务数(成功+失败,任务id去重)
|
||||
}
|
||||
@@ -1,16 +1,33 @@
|
||||
package v1
|
||||
|
||||
import (
|
||||
"likedream-goframe/internal/ent/cptask"
|
||||
|
||||
"github.com/gogf/gf/v2/frame/g"
|
||||
"github.com/gogf/gf/v2/net/ghttp"
|
||||
)
|
||||
|
||||
type UploadImageReq struct {
|
||||
g.Meta `path:"/image/upload" method:"post" tags:"Image" summary:"上传图片"`
|
||||
File *ghttp.UploadFile `json:"file" v:"required#请上传图片"`
|
||||
g.Meta `path:"/image/upload" method:"post" tags:"Image" summary:"上传图片"`
|
||||
File *ghttp.UploadFile `json:"file" v:"required#请上传图片"`
|
||||
CpUserId int64 `json:"cpUserId" v:"required#请指定今日校园用户"`
|
||||
TaskType cptask.TaskType `json:"taskType" v:"required#请指定任务类型"`
|
||||
}
|
||||
|
||||
type GetUploadPolicyReq struct {
|
||||
g.Meta `path:"/image/policy" method:"post" tags:"Image" summary:"获取图片上传策略"`
|
||||
CpUserId int64 `json:"cpUserId" v:"required#请指定今日校园用户"`
|
||||
TaskType cptask.TaskType `json:"taskType" v:"required#请指定任务类型"`
|
||||
}
|
||||
|
||||
type GetPreviewUrlReq struct {
|
||||
g.Meta `path:"/image/previewUrl" method:"post" tags:"Image" summary:"获取图片预览地址"`
|
||||
CpUserId int64 `json:"cpUserId" v:"required#请指定今日校园用户"`
|
||||
TaskType cptask.TaskType `json:"taskType" v:"required#请指定任务类型"`
|
||||
FileKey string `json:"fileKey" v:"required#请指定文件key"`
|
||||
}
|
||||
|
||||
type PreviewImageReq struct {
|
||||
g.Meta `path:"/image/preview" method:"get" tags:"Image" summary:"预览图片"`
|
||||
Key string `json:"key" v:"required#请输入图片key"`
|
||||
Key string `json:"key" v:"required#请指定图片key" in:"query"`
|
||||
}
|
||||
|
||||
@@ -1,6 +1,17 @@
|
||||
package v1
|
||||
|
||||
type UploadImageRes struct {
|
||||
FileKey string `json:"key"`
|
||||
FileSize *int64 `json:"size"`
|
||||
PreviewUrl string `json:"previewUrl"`
|
||||
}
|
||||
|
||||
type UploadPolicyRes struct {
|
||||
Host string `json:"host"`
|
||||
AccessId string `json:"accessId"`
|
||||
FileName string `json:"fileName"`
|
||||
Policy string `json:"policy"`
|
||||
Signature string `json:"signature"`
|
||||
}
|
||||
|
||||
type PreviewUrlRes struct {
|
||||
PreviewUrl string `json:"previewUrl"`
|
||||
}
|
||||
|
||||
@@ -0,0 +1,81 @@
|
||||
package v1
|
||||
|
||||
import (
|
||||
"github.com/gogf/gf/v2/frame/g"
|
||||
"github.com/gogf/gf/v2/net/ghttp"
|
||||
)
|
||||
|
||||
type CpLeaveListReq struct {
|
||||
g.Meta `path:"/leave" method:"get" tags:"CpLeave" summary:"Get leave records"`
|
||||
CpUserId int64 `json:"cpUserId"`
|
||||
Username string `json:"username"`
|
||||
Status string `json:"status"`
|
||||
Page int `json:"page" d:"1"`
|
||||
Size int `json:"size" d:"10"`
|
||||
}
|
||||
|
||||
type GetLeaveListFromSystemReq struct {
|
||||
g.Meta `path:"/leave/system/{cpUserId}" method:"get" tags:"CpLeave" summary:"Get system leave list"`
|
||||
CpUserId int64 `json:"cpUserId" v:"required#今日校园账号ID不能为空"`
|
||||
}
|
||||
|
||||
type CpLeaveReportReq struct {
|
||||
g.Meta `path:"/leave/report" method:"post" tags:"CpLeave" summary:"Submit leave report"`
|
||||
CpUserId int64 `json:"cpUserId" v:"required#今日校园账号ID不能为空"`
|
||||
LeaveId string `json:"leaveId" v:"required#请假记录ID不能为空"`
|
||||
Title string `json:"title" v:"required#请假记录标题不能为空"`
|
||||
Longitude string `json:"longitude"`
|
||||
Latitude string `json:"latitude"`
|
||||
Address string `json:"address"`
|
||||
ImageUrl string `json:"imageUrl"`
|
||||
}
|
||||
|
||||
type CpLeaveUploadReq struct {
|
||||
g.Meta `path:"/leave/upload" method:"post" tags:"CpLeave" summary:"Upload leave image"`
|
||||
File *ghttp.UploadFile `json:"file"`
|
||||
CpUserId int64 `json:"cpUserId"`
|
||||
}
|
||||
|
||||
type CpLeaveCheckReq struct {
|
||||
g.Meta `path:"/leave/check/{cpUserId}" method:"get" tags:"CpLeave" summary:"Check leave permission"`
|
||||
CpUserId int64 `json:"cpUserId" v:"required#今日校园账号ID不能为空"`
|
||||
}
|
||||
|
||||
type CpLeaveTypesReq struct {
|
||||
g.Meta `path:"/leave/types/{cpUserId}" method:"get" tags:"CpLeave" summary:"Get leave types"`
|
||||
CpUserId int64 `json:"cpUserId" v:"required#今日校园账号ID不能为空"`
|
||||
}
|
||||
|
||||
type CpLeaveTypeCfgReq struct {
|
||||
g.Meta `path:"/leave/typeCfg" method:"post" tags:"CpLeave" summary:"Get leave type config"`
|
||||
CpUserId int64 `json:"cpUserId" v:"required#今日校园账号ID不能为空"`
|
||||
Code *int `json:"code"`
|
||||
}
|
||||
|
||||
type CpLeaveApproveFlowReq struct {
|
||||
g.Meta `path:"/leave/approveFlow" method:"post" tags:"CpLeave" summary:"Get approve flow"`
|
||||
CpUserId int64 `json:"cpUserId" v:"required#今日校园账号ID不能为空"`
|
||||
LeaveType *int `json:"leaveType"`
|
||||
StartTime string `json:"startTime" v:"required#开始时间不能为空"`
|
||||
EndTime string `json:"endTime" v:"required#结束时间不能为空"`
|
||||
}
|
||||
|
||||
type CpLeaveApplyReq struct {
|
||||
g.Meta `path:"/leave/apply" method:"post" tags:"CpLeave" summary:"Apply leave"`
|
||||
CpUserId int64 `json:"cpUserId" v:"required#今日校园账号ID不能为空"`
|
||||
LeaveType *int `json:"leaveType"`
|
||||
StartTime string `json:"startTime" v:"required#开始时间不能为空"`
|
||||
EndTime string `json:"endTime" v:"required#结束时间不能为空"`
|
||||
UrgencyMobile string `json:"urgencyMobile"`
|
||||
LeaveReason string `json:"leaveReason" v:"required#请假原因不能为空"`
|
||||
OutStatus bool `json:"outStatus"`
|
||||
Address string `json:"address"`
|
||||
DetailAddress string `json:"detailAddress"`
|
||||
Longitude string `json:"longitude" v:"required#经度不能为空"`
|
||||
Latitude string `json:"latitude" v:"required#纬度不能为空"`
|
||||
LocationAddr string `json:"locationAddr" v:"required#定位地址不能为空"`
|
||||
ImageUrl string `json:"imageUrl"`
|
||||
ApproverWid string `json:"approverWid"`
|
||||
ApproverId string `json:"approverId"`
|
||||
ApproverName string `json:"approverName"`
|
||||
}
|
||||
@@ -24,6 +24,7 @@ type CpTaskListReq struct {
|
||||
TaskType cptask.TaskType `json:"type"`
|
||||
Username string `json:"username"`
|
||||
TaskStatus cptask.TaskStatus `json:"task_status"`
|
||||
Ids string `json:"ids"`
|
||||
Page int `json:"page" d:"1"`
|
||||
Size int `json:"size" d:"10"`
|
||||
}
|
||||
@@ -51,6 +52,13 @@ type PostCpTaskExecuteReq struct {
|
||||
NotificationType cptask.NotificationType `json:"notification_type"`
|
||||
ExecuteHour int `json:"execute_hour"`
|
||||
ExecuteMinute int `json:"execute_minute"`
|
||||
ExecuteDays []int `json:"execute_days"`
|
||||
}
|
||||
|
||||
type UpdateTaskTitleReq struct {
|
||||
g.Meta `path:"/task/title/{id}" method:"put" tags:"CpTask" summary:"修改任务标题"`
|
||||
Id int64 `json:"id"`
|
||||
Title string `json:"title" v:"required#标题不能为空"`
|
||||
}
|
||||
|
||||
type DeleteTaskReq struct {
|
||||
@@ -67,3 +75,20 @@ type PauseTaskReq struct {
|
||||
g.Meta `path:"/task/pause/{id}" method:"post" tags:"CpTask" summary:"暂停任务"`
|
||||
Id int64 `json:"id"`
|
||||
}
|
||||
|
||||
type FindLocationReq struct {
|
||||
g.Meta `path:"/task/location" method:"get" tags:"CpTask" summary:"查找地址"`
|
||||
Address string `json:"address"`
|
||||
}
|
||||
|
||||
type GetTaskStatisticsReq struct {
|
||||
g.Meta `path:"/task/statistics" method:"get" tags:"CpTask" summary:"获取近七日任务统计数据"`
|
||||
}
|
||||
|
||||
type GetTaskStatisticsV2Req struct {
|
||||
g.Meta `path:"/task/statisticsV2" method:"get" tags:"CpTask" summary:"获取近七日任务统计数据V2"`
|
||||
}
|
||||
|
||||
type GetDashboardSummaryReq struct {
|
||||
g.Meta `path:"/task/dashboard" method:"get" tags:"CpTask" summary:"获取首页概览数据"`
|
||||
}
|
||||
|
||||
@@ -1,12 +1,19 @@
|
||||
package v1
|
||||
|
||||
import "github.com/gogf/gf/v2/frame/g"
|
||||
import (
|
||||
"likedream-goframe/internal/framework/captcha"
|
||||
|
||||
"github.com/gogf/gf/v2/frame/g"
|
||||
)
|
||||
|
||||
type CpSchoolListReq struct {
|
||||
g.Meta `path:"/school" method:"get" tags:"CpSchool" summary:"获取cp学校列表"`
|
||||
Name string `json:"name"`
|
||||
}
|
||||
|
||||
type CpUserLoginUrlReq struct {
|
||||
g.Meta `path:"/loginUrl" method:"get" tags:"CpUser" summary:"获取登录url" auth:"false"`
|
||||
Name string `json:"name"`
|
||||
}
|
||||
type CpUserAddReq struct {
|
||||
g.Meta `path:"/user" method:"post" tags:"CpUser" summary:"添加cp用户"`
|
||||
Username string `json:"username" v:"required"`
|
||||
@@ -14,6 +21,24 @@ type CpUserAddReq struct {
|
||||
SchoolId string `json:"school_id"`
|
||||
Email string `json:"email" v:"email|regex:^.*@qq\\.com$#邮箱格式不正确|必须是QQ邮箱"`
|
||||
}
|
||||
type LoginGetCode struct {
|
||||
g.Meta `path:"/user/getCode" method:"post" tags:"CpUser" summary:"获取手机号登录验证码"`
|
||||
Mobile string `json:"mobile" v:"required#手机号不能为空"`
|
||||
CaptchaKey string `json:"captchaKey" v:"required#验证码不能不能为空"`
|
||||
Dots []captcha.ConfirmData `json:"dots" v:"required#验证码不能为空"`
|
||||
}
|
||||
|
||||
type AddByCode struct {
|
||||
g.Meta `path:"/user/addByCode" method:"post" tags:"CpUser" summary:"手机号登录"`
|
||||
Id int64 `json:"id"`
|
||||
CaptchaKey string `json:"captchaKey" v:"required#验证码不能不能为空"`
|
||||
Dots []captcha.ConfirmData `json:"dots" v:"required#验证码不能为空"`
|
||||
Mobile string `json:"mobile" v:"required#手机号不能为空"`
|
||||
SchoolId string `json:"school_id"`
|
||||
Code string `json:"code"`
|
||||
Email string `json:"email" v:"email|regex:^.*@qq\\.com$#邮箱格式不正确|必须是QQ邮箱"`
|
||||
}
|
||||
|
||||
type CpUserConfirmAddReq struct {
|
||||
g.Meta `path:"/user/confirm" method:"post" tags:"CpUser" summary:"多因子确认添加cp用户"`
|
||||
Key string `json:"key"`
|
||||
@@ -60,7 +85,20 @@ type CpUserAuthorizeConfirmReq struct {
|
||||
DataKey string `json:"dataKey"`
|
||||
}
|
||||
|
||||
type CpUserAuthorizeRandomReq struct {
|
||||
g.Meta `path:"/user/authorizeRandom" method:"post" tags:"CpUser" summary:"随机设备码"`
|
||||
Id int64 `json:"id"`
|
||||
type CpUserCustomDeviceReq struct {
|
||||
g.Meta `path:"/user/customDevice" method:"post" tags:"CpUser" summary:"自定义设备码"`
|
||||
Id int64 `json:"id" v:"required#请指定用户"`
|
||||
MobileCode string `json:"mobileCode" v:"required#请输入设备码"`
|
||||
MobileType string `json:"mobileType" v:"required|in:random,manual#请指定设备码类型|设备码类型无效"`
|
||||
}
|
||||
|
||||
// ---- 二维码获取设备码流程 ----
|
||||
|
||||
type CpUserGenerateQrReq struct {
|
||||
g.Meta `path:"/user/generateQr" method:"post" tags:"CpUser" summary:"生成设备码二维码"`
|
||||
}
|
||||
|
||||
type CpUserCheckQrReq struct {
|
||||
g.Meta `path:"/user/checkQr" method:"get" tags:"CpUser" summary:"校验二维码UUID是否有效" auth:"false"`
|
||||
Uuid string `json:"uuid" v:"required#请提供二维码UUID"`
|
||||
}
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
package v1
|
||||
|
||||
import (
|
||||
"github.com/gogf/gf/v2/frame/g"
|
||||
)
|
||||
|
||||
// PriceConfigGetReq 获取所有费率配置
|
||||
type PriceConfigGetReq struct {
|
||||
g.Meta `path:"/config/price" method:"get" tags:"PriceConfig" summary:"获取所有费率配置"`
|
||||
}
|
||||
|
||||
// PriceConfigUpdateReq 更新费率配置
|
||||
type PriceConfigUpdateReq struct {
|
||||
g.Meta `path:"/config/price" method:"put" tags:"PriceConfig" summary:"更新费率配置" permission:"sys:config:edit"`
|
||||
CpdailyTask string `json:"cpdaily_task"` // 今日校园签到单价
|
||||
CpdailyLeave string `json:"cpdaily_leave"` // 今日校园请假单价
|
||||
CpdailyPhone string `json:"cpdaily_phone"` // 今日校园手机号单价
|
||||
Xxt string `json:"xxt"` // 学习通单价
|
||||
SmarteduSc string `json:"smartedu_sc"` // 四川智慧教育单价
|
||||
}
|
||||
|
||||
// PriceConfigResp 费率配置响应
|
||||
type PriceConfigResp struct {
|
||||
CpdailyTask string `json:"cpdaily_task"`
|
||||
CpdailyLeave string `json:"cpdaily_leave"`
|
||||
CpdailyPhone string `json:"cpdaily_phone"`
|
||||
Xxt string `json:"xxt"`
|
||||
SmarteduSc string `json:"smartedu_sc"`
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
package v1
|
||||
|
||||
import (
|
||||
"github.com/gogf/gf/v2/frame/g"
|
||||
)
|
||||
|
||||
// SmartEduSCQueryCourseReq 查询四川智慧教育课程列表
|
||||
type SmartEduSCQueryCourseReq struct {
|
||||
g.Meta `path:"/smartedu-sc/query" method:"post" tags:"SmartEduSC" summary:"查询四川智慧教育课程列表"`
|
||||
Username string `json:"username" v:"required#账号不能为空"`
|
||||
Password string `json:"password" v:"required#密码不能为空"`
|
||||
}
|
||||
|
||||
// SmartEduSCAddCourseReq 添加四川智慧教育刷课任务
|
||||
type SmartEduSCAddCourseReq struct {
|
||||
g.Meta `path:"/smartedu-sc" method:"post" tags:"SmartEduSC" summary:"添加四川智慧教育刷课任务"`
|
||||
Username string `json:"username" v:"required#账号不能为空"`
|
||||
Password string `json:"password" v:"required#密码不能为空"`
|
||||
CourseName string `json:"course_name" v:"required#课程名称不能为空"`
|
||||
CourseID string `json:"course_id" v:"required#课程ID不能为空"`
|
||||
Desc string `json:"desc"`
|
||||
}
|
||||
|
||||
// SmartEduSCListReq 获取四川智慧教育刷课列表
|
||||
type SmartEduSCListReq struct {
|
||||
g.Meta `path:"/smartedu-sc" method:"get" tags:"SmartEduSC" summary:"获取四川智慧教育刷课列表"`
|
||||
Username string `json:"username"`
|
||||
Status string `json:"status"`
|
||||
Page int `json:"page" d:"1"`
|
||||
Size int `json:"size" d:"10"`
|
||||
}
|
||||
|
||||
// SmartEduSCDeleteReq 删除四川智慧教育刷课任务
|
||||
type SmartEduSCDeleteReq struct {
|
||||
g.Meta `path:"/smartedu-sc/{id}" method:"delete" tags:"SmartEduSC" summary:"删除四川智慧教育刷课任务"`
|
||||
ID int64 `json:"id"`
|
||||
}
|
||||
@@ -4,6 +4,10 @@ import (
|
||||
"github.com/gogf/gf/v2/frame/g"
|
||||
)
|
||||
|
||||
type RefreshTokenReq struct {
|
||||
g.Meta `path:"/refreshToken" method:"GET" tags:"UserInfo" summary:"刷新Token"`
|
||||
}
|
||||
|
||||
type ResetPasswordReq struct {
|
||||
g.Meta `path:"/resetPassword" method:"POST" tags:"UserInfo" summary:"重置密码"`
|
||||
OldPassword string `json:"OldPassword"`
|
||||
|
||||
@@ -0,0 +1,77 @@
|
||||
package captcha
|
||||
|
||||
import (
|
||||
"encoding/base64"
|
||||
"likedream-goframe/internal/application/httpclient"
|
||||
"likedream-goframe/internal/common/cst"
|
||||
"likedream-goframe/internal/utils"
|
||||
|
||||
"github.com/gogf/gf/v2/frame/g"
|
||||
"github.com/gogf/gf/v2/os/gctx"
|
||||
"github.com/samber/oops"
|
||||
"github.com/tidwall/gjson"
|
||||
)
|
||||
|
||||
func ApiSemiOCRVerification(bytes []byte, shape1, shape2 int64) string {
|
||||
data := Bytes2Base64(bytes)
|
||||
ctx := gctx.GetInitCtx()
|
||||
baseUrl := g.Config().MustGetWithEnv(ctx, cst.ConfigCaptchaBaseUrl).String() + "/semiOcr"
|
||||
apiKey := g.Config().MustGetWithEnv(ctx, cst.ConfigCaptchaApiKey).String()
|
||||
client := httpclient.Cpdaily.Clone(ctx)
|
||||
client.SetDoNotParseResponse(false)
|
||||
post, err := client.R().SetBody(map[string]any{
|
||||
"base64": data,
|
||||
"shape1": shape1,
|
||||
"shape2": shape2,
|
||||
}).SetHeader("X-Api-Key", apiKey).Post(baseUrl)
|
||||
utils.OkOrPanic(err, func(err error) error {
|
||||
return oops.
|
||||
With("SemiOCRVerification", baseUrl).
|
||||
WithContext(ctx).
|
||||
Wrap(err)
|
||||
})
|
||||
result := post.String()
|
||||
return gjson.Get(result, "data").String()
|
||||
}
|
||||
|
||||
func ApiAutoOCRVerification(bytes []byte) string {
|
||||
data := Bytes2Base64(bytes)
|
||||
ctx := gctx.GetInitCtx()
|
||||
baseUrl := g.Config().MustGetWithEnv(ctx, cst.ConfigCaptchaBaseUrl).String() + "/ocr"
|
||||
apiKey := g.Config().MustGetWithEnv(ctx, cst.ConfigCaptchaApiKey).String()
|
||||
client := httpclient.Cpdaily.Clone(ctx)
|
||||
client.SetDoNotParseResponse(false)
|
||||
post, err := client.R().SetBody(map[string]any{
|
||||
"base64": data,
|
||||
}).SetHeader("X-Api-Key", apiKey).Post(baseUrl)
|
||||
utils.OkOrPanic(err, func(err error) error {
|
||||
return oops.
|
||||
With("AutoOCRVerification", baseUrl).
|
||||
WithContext(ctx).
|
||||
Wrap(err)
|
||||
})
|
||||
result := post.String()
|
||||
return gjson.Get(result, "data").String()
|
||||
}
|
||||
|
||||
func ApiAutoCalcVerification(b64 string) string {
|
||||
ctx := gctx.GetInitCtx()
|
||||
baseUrl := g.Config().MustGetWithEnv(ctx, cst.ConfigCaptchaBaseUrl).String() + "/calc"
|
||||
apiKey := g.Config().MustGetWithEnv(ctx, cst.ConfigCaptchaApiKey).String()
|
||||
client := httpclient.Cpdaily.Clone(ctx)
|
||||
client.SetDoNotParseResponse(false)
|
||||
post, err := client.R().SetBody(map[string]any{
|
||||
"base64": b64,
|
||||
}).SetHeader("X-Api-Key", apiKey).Post(baseUrl)
|
||||
utils.OkOrPanic(err, func(err error) error {
|
||||
return oops.
|
||||
With("AutoOCRVerification", baseUrl).
|
||||
WithContext(ctx).
|
||||
Wrap(err)
|
||||
})
|
||||
result := post.String()
|
||||
return gjson.Get(result, "data").String()
|
||||
}
|
||||
func Bytes2Base64(data []byte) string {
|
||||
return base64.StdEncoding.EncodeToString(data)
|
||||
}
|
||||
@@ -8,20 +8,21 @@ import (
|
||||
"likedream-goframe/internal/utils"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
"sync"
|
||||
)
|
||||
|
||||
type domainMap map[string]cookieMap
|
||||
|
||||
// AllSavePersistCookieJar 支持泛域名的 Cookie Jar
|
||||
// 不同的二级域名会共享相同主域名的 cookies
|
||||
// 例如: passport2.chaoxing.com 和 mooc1-api.chaoxing.com 共享 .chaoxing.com 的 cookies
|
||||
// AllSavePersistCookieJar 完全无限制的 Cookie Jar
|
||||
// 所有域名的 cookies 都保存在一起,访问任何域名都返回所有 cookies
|
||||
// 不检查域名、路径等限制,适用于需要跨域共享 session 的场景
|
||||
type AllSavePersistCookieJar struct {
|
||||
domainMap domainMap
|
||||
*sync.RWMutex
|
||||
}
|
||||
|
||||
const globalDomain = "*" // 使用全局域名存储所有 cookies
|
||||
|
||||
func NewAllSavePersistCookieJar() *AllSavePersistCookieJar {
|
||||
return &AllSavePersistCookieJar{
|
||||
domainMap: make(domainMap),
|
||||
@@ -29,45 +30,36 @@ func NewAllSavePersistCookieJar() *AllSavePersistCookieJar {
|
||||
}
|
||||
}
|
||||
|
||||
// extractMainDomain 提取主域名
|
||||
// 例如: passport2.chaoxing.com -> chaoxing.com
|
||||
//
|
||||
// mooc1-api.chaoxing.com -> chaoxing.com
|
||||
// www.example.co.uk -> example.co.uk
|
||||
func extractMainDomain(host string) string {
|
||||
// 移除端口号
|
||||
if colonIndex := strings.Index(host, ":"); colonIndex != -1 {
|
||||
host = host[:colonIndex]
|
||||
}
|
||||
|
||||
parts := strings.Split(host, ".")
|
||||
if len(parts) <= 2 {
|
||||
// 已经是主域名或单级域名
|
||||
return host
|
||||
}
|
||||
|
||||
// 处理特殊的二级顶级域名,如 .co.uk, .com.cn 等
|
||||
// 简化处理:取最后两部分
|
||||
// 更复杂的情况可以使用 publicsuffix 包
|
||||
return strings.Join(parts[len(parts)-2:], ".")
|
||||
}
|
||||
|
||||
func (a *AllSavePersistCookieJar) SetCookies(u *url.URL, cookies []*http.Cookie) {
|
||||
a.Lock()
|
||||
defer a.Unlock()
|
||||
|
||||
// 提取主域名
|
||||
mainDomain := extractMainDomain(u.Host)
|
||||
|
||||
if _, exists := a.domainMap[mainDomain]; !exists {
|
||||
a.domainMap[mainDomain] = make(cookieMap)
|
||||
// 使用全局域名,所有 cookies 保存在一起
|
||||
if _, exists := a.domainMap[globalDomain]; !exists {
|
||||
a.domainMap[globalDomain] = make(cookieMap)
|
||||
}
|
||||
|
||||
for _, cookie := range cookies {
|
||||
if a.domainMap[mainDomain] == nil {
|
||||
a.domainMap[mainDomain] = make(cookieMap)
|
||||
if a.domainMap[globalDomain] == nil {
|
||||
a.domainMap[globalDomain] = make(cookieMap)
|
||||
}
|
||||
|
||||
// 查找是否存在相同 name 的 cookie(忽略 path 和 domain)
|
||||
existingCookies := a.domainMap[globalDomain][cookie.Name]
|
||||
found := false
|
||||
for i, existing := range existingCookies {
|
||||
if existing.Path == cookie.Path && existing.Domain == cookie.Domain {
|
||||
// 找到完全匹配的 cookie,替换它
|
||||
existingCookies[i] = cookie
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if !found {
|
||||
// 没有找到匹配的,添加新的 cookie
|
||||
a.domainMap[globalDomain][cookie.Name] = append(a.domainMap[globalDomain][cookie.Name], cookie)
|
||||
}
|
||||
a.domainMap[mainDomain][cookie.Name] = cookie
|
||||
}
|
||||
}
|
||||
|
||||
@@ -75,13 +67,21 @@ func (a *AllSavePersistCookieJar) Cookies(u *url.URL) []*http.Cookie {
|
||||
a.RLock()
|
||||
defer a.RUnlock()
|
||||
|
||||
// 提取主域名
|
||||
mainDomain := extractMainDomain(u.Host)
|
||||
// 直接返回全局域名下的所有 cookies,不检查域名和路径
|
||||
if cookiesList, exists := a.domainMap[globalDomain]; exists {
|
||||
result := make([]*http.Cookie, 0, len(cookiesList))
|
||||
|
||||
if cookies, exists := a.domainMap[mainDomain]; exists {
|
||||
result := make([]*http.Cookie, 0, len(cookies))
|
||||
for _, cookie := range cookies {
|
||||
result = append(result, cookie)
|
||||
for _, cookies := range cookiesList {
|
||||
// 如果只有一个 cookie,直接返回
|
||||
if len(cookies) == 1 {
|
||||
result = append(result, cookies[0])
|
||||
continue
|
||||
}
|
||||
|
||||
// 有多个同名 cookie,返回最后设置的那个(列表最后一个)
|
||||
if len(cookies) > 0 {
|
||||
result = append(result, cookies[len(cookies)-1])
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
@@ -109,7 +109,30 @@ func (a *AllSavePersistCookieJar) Deserialize(content string) {
|
||||
|
||||
var domainMap domainMap
|
||||
err := sonic.Unmarshal([]byte(content), &domainMap)
|
||||
utils.OkOrPanic(err)
|
||||
|
||||
// 如果反序列化失败,尝试从旧格式转换
|
||||
if err != nil {
|
||||
// 旧格式: map[string]map[string]*http.Cookie
|
||||
type oldCookieMap map[string]*http.Cookie
|
||||
type oldDomainMap map[string]oldCookieMap
|
||||
|
||||
var oldMap oldDomainMap
|
||||
oldErr := sonic.Unmarshal([]byte(content), &oldMap)
|
||||
if oldErr != nil {
|
||||
// 两种格式都失败,抛出原始错误
|
||||
utils.OkOrPanic(err)
|
||||
return
|
||||
}
|
||||
|
||||
// 转换旧格式到新格式
|
||||
domainMap = make(map[string]cookieMap)
|
||||
for domain, cookies := range oldMap {
|
||||
domainMap[domain] = make(map[string][]*http.Cookie)
|
||||
for name, cookie := range cookies {
|
||||
domainMap[domain][name] = []*http.Cookie{cookie}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
a.domainMap = domainMap
|
||||
}
|
||||
@@ -142,7 +165,31 @@ func (a *AllSavePersistCookieJar) DeserializeBytes(content []byte) {
|
||||
var domainMap domainMap
|
||||
err := gob.NewDecoder(retReader).
|
||||
Decode(&domainMap)
|
||||
utils.OkOrPanic(err)
|
||||
|
||||
// 如果反序列化失败,尝试从旧格式转换
|
||||
if err != nil {
|
||||
// 旧格式: map[string]map[string]*http.Cookie
|
||||
type oldCookieMap map[string]*http.Cookie
|
||||
type oldDomainMap map[string]oldCookieMap
|
||||
|
||||
retReader = bytes.NewBuffer(content)
|
||||
var oldMap oldDomainMap
|
||||
oldErr := gob.NewDecoder(retReader).Decode(&oldMap)
|
||||
if oldErr != nil {
|
||||
// 两种格式都失败,抛出原始错误
|
||||
utils.OkOrPanic(err)
|
||||
return
|
||||
}
|
||||
|
||||
// 转换旧格式到新格式
|
||||
domainMap = make(map[string]cookieMap)
|
||||
for domain, cookies := range oldMap {
|
||||
domainMap[domain] = make(map[string][]*http.Cookie)
|
||||
for name, cookie := range cookies {
|
||||
domainMap[domain][name] = []*http.Cookie{cookie}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
a.domainMap = domainMap
|
||||
}
|
||||
@@ -151,10 +198,36 @@ func (a *AllSavePersistCookieJar) DeserializeReader(reader io.Reader) {
|
||||
a.Lock()
|
||||
defer a.Unlock()
|
||||
|
||||
var domainMap domainMap
|
||||
err := gob.NewDecoder(reader).
|
||||
Decode(&domainMap)
|
||||
// 先读取所有数据,以便在失败时可以重试
|
||||
data, err := io.ReadAll(reader)
|
||||
utils.OkOrPanic(err)
|
||||
|
||||
var domainMap domainMap
|
||||
err = gob.NewDecoder(bytes.NewBuffer(data)).Decode(&domainMap)
|
||||
|
||||
// 如果反序列化失败,尝试从旧格式转换
|
||||
if err != nil {
|
||||
// 旧格式: map[string]map[string]*http.Cookie
|
||||
type oldCookieMap map[string]*http.Cookie
|
||||
type oldDomainMap map[string]oldCookieMap
|
||||
|
||||
var oldMap oldDomainMap
|
||||
oldErr := gob.NewDecoder(bytes.NewBuffer(data)).Decode(&oldMap)
|
||||
if oldErr != nil {
|
||||
// 两种格式都失败,抛出原始错误
|
||||
utils.OkOrPanic(err)
|
||||
return
|
||||
}
|
||||
|
||||
// 转换旧格式到新格式
|
||||
domainMap = make(map[string]cookieMap)
|
||||
for domain, cookies := range oldMap {
|
||||
domainMap[domain] = make(map[string][]*http.Cookie)
|
||||
for name, cookie := range cookies {
|
||||
domainMap[domain][name] = []*http.Cookie{cookie}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
a.domainMap = domainMap
|
||||
}
|
||||
|
||||
@@ -3,47 +3,19 @@ package cookiejar
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/gob"
|
||||
"fmt"
|
||||
"github.com/bytedance/sonic"
|
||||
"io"
|
||||
"likedream-goframe/internal/utils"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"github.com/bytedance/sonic"
|
||||
)
|
||||
|
||||
type hostMap map[string]cookieMap
|
||||
|
||||
type cookieMap map[string]*http.Cookie
|
||||
|
||||
// cookieKey 生成 cookie 的唯一标识 key,使用 name 和 path 的组合
|
||||
func cookieKey(name, path string) string {
|
||||
return fmt.Sprintf("%s|%s", name, path)
|
||||
}
|
||||
|
||||
// pathMatch 检查请求路径是否匹配 cookie 的 path
|
||||
// 根据 RFC 6265 Section 5.1.4
|
||||
func pathMatch(requestPath, cookiePath string) bool {
|
||||
// 如果路径完全相同
|
||||
if requestPath == cookiePath {
|
||||
return true
|
||||
}
|
||||
|
||||
// 如果 cookie path 是请求 path 的前缀
|
||||
if strings.HasPrefix(requestPath, cookiePath) {
|
||||
// cookie path 以 / 结尾,或者请求 path 在 cookie path 后紧跟 /
|
||||
if strings.HasSuffix(cookiePath, "/") {
|
||||
return true
|
||||
}
|
||||
if len(requestPath) > len(cookiePath) && requestPath[len(cookiePath)] == '/' {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
type cookieMap map[string][]*http.Cookie
|
||||
type PersistCookieJar struct {
|
||||
hostMap hostMap
|
||||
*sync.RWMutex
|
||||
@@ -63,7 +35,7 @@ func (p *PersistCookieJar) SetCookies(u *url.URL, cookies []*http.Cookie) {
|
||||
host := u.Host
|
||||
|
||||
if _, exists := p.hostMap[host]; !exists {
|
||||
p.hostMap = make(hostMap)
|
||||
p.hostMap[host] = make(cookieMap)
|
||||
}
|
||||
|
||||
for _, cookie := range cookies {
|
||||
@@ -71,22 +43,13 @@ func (p *PersistCookieJar) SetCookies(u *url.URL, cookies []*http.Cookie) {
|
||||
p.hostMap[host] = make(cookieMap)
|
||||
}
|
||||
|
||||
// 如果 cookie 没有设置 Path,使用默认值 "/"
|
||||
cookiePath := cookie.Path
|
||||
if cookiePath == "" {
|
||||
cookiePath = "/"
|
||||
// 检查并删除全局 cookie 中同名的 cookie
|
||||
if globalCookies, exists := p.hostMap[""]; exists {
|
||||
delete(globalCookies, cookie.Name)
|
||||
}
|
||||
|
||||
// 使用 name 和 path 的组合作为 key
|
||||
key := cookieKey(cookie.Name, cookiePath)
|
||||
|
||||
// 如果 MaxAge=0 或 MaxAge<0,表示删除 cookie
|
||||
// 只删除完全匹配的 cookie(name 和 path 都相同)
|
||||
if cookie.MaxAge < 0 || (cookie.MaxAge == 0 && cookie.Value == "") {
|
||||
delete(p.hostMap[host], key)
|
||||
} else {
|
||||
p.hostMap[host][key] = cookie
|
||||
}
|
||||
// 同一 host 下同名 cookie 直接覆盖,避免多个同名 cookie 导致旧值被选中
|
||||
p.hostMap[host][cookie.Name] = []*http.Cookie{cookie}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -101,22 +64,105 @@ func (p *PersistCookieJar) Cookies(u *url.URL) []*http.Cookie {
|
||||
requestPath = "/"
|
||||
}
|
||||
|
||||
if cookies, exists := p.hostMap[host]; exists {
|
||||
result := make([]*http.Cookie, 0, len(cookies))
|
||||
for _, cookie := range cookies {
|
||||
// 检查 cookie 的 path 是否匹配请求的 path
|
||||
cookiePath := cookie.Path
|
||||
if cookiePath == "" {
|
||||
cookiePath = "/"
|
||||
result := make([]*http.Cookie, 0)
|
||||
addedNames := make(map[string]bool)
|
||||
|
||||
// 先添加匹配 host 的 cookie
|
||||
if cookiesList, exists := p.hostMap[host]; exists {
|
||||
for cookieName, cookies := range cookiesList {
|
||||
// 如果只有一个 cookie,直接返回
|
||||
if len(cookies) == 1 {
|
||||
result = append(result, cookies[0])
|
||||
addedNames[cookieName] = true
|
||||
continue
|
||||
}
|
||||
|
||||
if pathMatch(requestPath, cookiePath) {
|
||||
result = append(result, cookie)
|
||||
// 有多个同名 cookie,选择最匹配的(path 最长且匹配请求路径的)
|
||||
var bestMatch *http.Cookie
|
||||
bestMatchLen := -1
|
||||
|
||||
for _, cookie := range cookies {
|
||||
cookiePath := cookie.Path
|
||||
if cookiePath == "" {
|
||||
cookiePath = "/"
|
||||
}
|
||||
|
||||
// 检查 path 是否匹配
|
||||
if pathMatch(requestPath, cookiePath) {
|
||||
if len(cookiePath) > bestMatchLen {
|
||||
bestMatch = cookie
|
||||
bestMatchLen = len(cookiePath)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 如果找到匹配的,添加到结果中
|
||||
if bestMatch != nil {
|
||||
result = append(result, bestMatch)
|
||||
addedNames[cookieName] = true
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
return nil
|
||||
|
||||
// 再添加没有 host 的全局 cookie(避免重复)
|
||||
if globalCookies, exists := p.hostMap[""]; exists {
|
||||
for cookieName, cookies := range globalCookies {
|
||||
// 如果该名称的 cookie 已经从 host 中添加了,跳过
|
||||
if addedNames[cookieName] {
|
||||
continue
|
||||
}
|
||||
|
||||
// 如果只有一个 cookie,直接返回
|
||||
if len(cookies) == 1 {
|
||||
result = append(result, cookies[0])
|
||||
continue
|
||||
}
|
||||
|
||||
// 有多个同名 cookie,选择最匹配的
|
||||
var bestMatch *http.Cookie
|
||||
bestMatchLen := -1
|
||||
|
||||
for _, cookie := range cookies {
|
||||
cookiePath := cookie.Path
|
||||
if cookiePath == "" {
|
||||
cookiePath = "/"
|
||||
}
|
||||
|
||||
if pathMatch(requestPath, cookiePath) {
|
||||
if len(cookiePath) > bestMatchLen {
|
||||
bestMatch = cookie
|
||||
bestMatchLen = len(cookiePath)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if bestMatch != nil {
|
||||
result = append(result, bestMatch)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
// pathMatch 检查请求路径是否匹配 cookie 的 path
|
||||
// 根据 RFC 6265 的规则
|
||||
func pathMatch(requestPath, cookiePath string) bool {
|
||||
if requestPath == cookiePath {
|
||||
return true
|
||||
}
|
||||
|
||||
if strings.HasPrefix(requestPath, cookiePath) {
|
||||
// cookiePath 以 / 结尾,或者 requestPath 的下一个字符是 /
|
||||
if strings.HasSuffix(cookiePath, "/") {
|
||||
return true
|
||||
}
|
||||
if len(requestPath) > len(cookiePath) && requestPath[len(cookiePath)] == '/' {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
func (p *PersistCookieJar) Serialize() string {
|
||||
@@ -142,7 +188,29 @@ func (p *PersistCookieJar) Deserialize(content string) {
|
||||
var hostMap hostMap
|
||||
err := sonic.Unmarshal([]byte(content), &hostMap)
|
||||
|
||||
utils.OkOrPanic(err)
|
||||
// 如果反序列化失败,尝试从旧格式转换
|
||||
if err != nil {
|
||||
// 旧格式: map[string]map[string]*http.Cookie
|
||||
type oldCookieMap map[string]*http.Cookie
|
||||
type oldHostMap map[string]oldCookieMap
|
||||
|
||||
var oldMap oldHostMap
|
||||
oldErr := sonic.Unmarshal([]byte(content), &oldMap)
|
||||
if oldErr != nil {
|
||||
// 两种格式都失败,抛出原始错误
|
||||
utils.OkOrPanic(err)
|
||||
return
|
||||
}
|
||||
|
||||
// 转换旧格式到新格式
|
||||
hostMap = make(map[string]cookieMap)
|
||||
for host, cookies := range oldMap {
|
||||
hostMap[host] = make(map[string][]*http.Cookie)
|
||||
for name, cookie := range cookies {
|
||||
hostMap[host][name] = []*http.Cookie{cookie}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
p.hostMap = hostMap
|
||||
}
|
||||
@@ -175,7 +243,31 @@ func (p *PersistCookieJar) DeserializeBytes(content []byte) {
|
||||
var hostMap hostMap
|
||||
err := gob.NewDecoder(retReader).
|
||||
Decode(&hostMap)
|
||||
utils.OkOrPanic(err)
|
||||
|
||||
// 如果反序列化失败,尝试从旧格式转换
|
||||
if err != nil {
|
||||
// 旧格式: map[string]map[string]*http.Cookie
|
||||
type oldCookieMap map[string]*http.Cookie
|
||||
type oldHostMap map[string]oldCookieMap
|
||||
|
||||
retReader = bytes.NewBuffer(content)
|
||||
var oldMap oldHostMap
|
||||
oldErr := gob.NewDecoder(retReader).Decode(&oldMap)
|
||||
if oldErr != nil {
|
||||
// 两种格式都失败,抛出原始错误
|
||||
utils.OkOrPanic(err)
|
||||
return
|
||||
}
|
||||
|
||||
// 转换旧格式到新格式
|
||||
hostMap = make(map[string]cookieMap)
|
||||
for host, cookies := range oldMap {
|
||||
hostMap[host] = make(map[string][]*http.Cookie)
|
||||
for name, cookie := range cookies {
|
||||
hostMap[host][name] = []*http.Cookie{cookie}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
p.hostMap = hostMap
|
||||
}
|
||||
@@ -184,10 +276,63 @@ func (p *PersistCookieJar) DeserializeReader(reader io.Reader) {
|
||||
p.Lock()
|
||||
defer p.Unlock()
|
||||
|
||||
var hostMap hostMap
|
||||
err := gob.NewDecoder(reader).
|
||||
Decode(&hostMap)
|
||||
// 先读取所有数据,以便在失败时可以重试
|
||||
data, err := io.ReadAll(reader)
|
||||
utils.OkOrPanic(err)
|
||||
|
||||
var hostMap hostMap
|
||||
err = gob.NewDecoder(bytes.NewBuffer(data)).Decode(&hostMap)
|
||||
|
||||
// 如果反序列化失败,尝试从旧格式转换
|
||||
if err != nil {
|
||||
// 旧格式: map[string]map[string]*http.Cookie
|
||||
type oldCookieMap map[string]*http.Cookie
|
||||
type oldHostMap map[string]oldCookieMap
|
||||
|
||||
var oldMap oldHostMap
|
||||
oldErr := gob.NewDecoder(bytes.NewBuffer(data)).Decode(&oldMap)
|
||||
if oldErr != nil {
|
||||
// 两种格式都失败,抛出原始错误
|
||||
utils.OkOrPanic(err)
|
||||
return
|
||||
}
|
||||
|
||||
// 转换旧格式到新格式
|
||||
hostMap = make(map[string]cookieMap)
|
||||
for host, cookies := range oldMap {
|
||||
hostMap[host] = make(map[string][]*http.Cookie)
|
||||
for name, cookie := range cookies {
|
||||
hostMap[host][name] = []*http.Cookie{cookie}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
p.hostMap = hostMap
|
||||
}
|
||||
|
||||
// RemoveCookieInfo 将所有 host 的 cookie 移除 host 信息,存储为全局 cookie
|
||||
func (p *PersistCookieJar) RemoveCookieInfo() {
|
||||
p.Lock()
|
||||
defer p.Unlock()
|
||||
|
||||
// 确保全局 cookie map 存在
|
||||
if _, exists := p.hostMap[""]; !exists {
|
||||
p.hostMap[""] = make(cookieMap)
|
||||
}
|
||||
|
||||
// 将所有 host 的 cookie 移动到全局
|
||||
for host, hostCookies := range p.hostMap {
|
||||
// 跳过全局 cookie 本身
|
||||
if host == "" {
|
||||
continue
|
||||
}
|
||||
|
||||
// 将该 host 的所有 cookie 移动到全局
|
||||
for cookieName, cookies := range hostCookies {
|
||||
p.hostMap[""][cookieName] = cookies
|
||||
}
|
||||
|
||||
// 删除该 host 的 cookie
|
||||
delete(p.hostMap, host)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -114,3 +114,10 @@ func TestPersistCookieJar_DeserializeReader(t *testing.T) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestDeseralizeCookie(t *testing.T) {
|
||||
var str = `{"cdpc.campusphere.net":{"MOD_AUTH_CAS":{"Name":"MOD_AUTH_CAS","Value":"ST-iap:1021988086918169:ST:8d7fe60b-dc7d-4fcd-b26b-5517e3ca1926:20251113162807","Quoted":false,"Path":"/","Domain":"","Expires":"0001-01-01T00:00:00Z","RawExpires":"","MaxAge":0,"Secure":false,"HttpOnly":true,"SameSite":0,"Partitioned":false,"Raw":"MOD_AUTH_CAS=ST-iap:1021988086918169:ST:8d7fe60b-dc7d-4fcd-b26b-5517e3ca1926:20251113162807; path=/; Httponly","Unparsed":null},"HWWAFSESID":{"Name":"HWWAFSESID","Value":"88d84731cf506df0bf","Quoted":false,"Path":"/","Domain":"","Expires":"0001-01-01T00:00:00Z","RawExpires":"","MaxAge":0,"Secure":false,"HttpOnly":false,"SameSite":0,"Partitioned":false,"Raw":"HWWAFSESID=88d84731cf506df0bf; path=/","Unparsed":null},"HWWAFSESTIME":{"Name":"HWWAFSESTIME","Value":"1763022486728","Quoted":false,"Path":"/","Domain":"","Expires":"0001-01-01T00:00:00Z","RawExpires":"","MaxAge":0,"Secure":false,"HttpOnly":false,"SameSite":0,"Partitioned":false,"Raw":"HWWAFSESTIME=1763022486728; path=/","Unparsed":null},"CONVERSATION":{"Name":"CONVERSATION","Value":"iap-1021988086918169-CONV-83bee566-7010-4ef6-ad17-4b9eebcccdf8","Quoted":false,"Path":"/iap","Domain":"","Expires":"0001-01-01T00:00:00Z","RawExpires":"","MaxAge":0,"Secure":false,"HttpOnly":true,"SameSite":0,"Partitioned":false,"Raw":"CONVERSATION=iap-1021988086918169-CONV-83bee566-7010-4ef6-ad17-4b9eebcccdf8; Path=/iap; HttpOnly","Unparsed":null},"CASTGC":{"Name":"CASTGC","Value":"iap-1021988086918169-TGT-717aa5db-92cb-46ac-a178-64b630946f43","Quoted":false,"Path":"/iap","Domain":"","Expires":"0001-01-01T00:00:00Z","RawExpires":"","MaxAge":0,"Secure":true,"HttpOnly":true,"SameSite":4,"Partitioned":false,"Raw":"CASTGC=iap-1021988086918169-TGT-717aa5db-92cb-46ac-a178-64b630946f43; Path=/iap; HttpOnly; Secure; SameSite=None","Unparsed":null}}}`
|
||||
jar := NewPersistCookieJar()
|
||||
jar.Deserialize(str)
|
||||
t.Log(jar)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
package cryptoutil
|
||||
|
||||
import (
|
||||
"encoding/base64"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestAesDecrypt(t *testing.T) {
|
||||
str := "gTIjjNBTwN0Vdv2o5ZlTZ6gHbFcIA0MV0caRqvv/50tptfoSO+VASLxpQZWy Xa86lY21QtmXhwlvTsfd3XR819O0IUi2FynPOM6s0pDsSndM2JhhqoRNXBTK 4WqfMgt0RCQJ38KP8XtM5O+Ktkj1YHHM/OVEGqOxRcUh2M++JJCheJbSs+aQ 2T3va13/GWJH7PEd8JVEJgBiO3wXLOb9+CvvWuU/Frc9nNNxQL7LrgZTdQAv C9Yt+5/a2n41reOeOs7Bu5C4FFlHQSs71Rom/HtIhL67x1ecIre1rbgK9xxz 1AhTEHY6nhdZmktPGJD5CfxvLVWIeLv09CjKqdSaNodkP4e9YTScNEZ2s8TK y8Xhv8L0r6gllC9B3nla4GRe+lqMvhDPDZx7PFu7kxFspf9yoM4eOpgWfCr9 xbDwxO9cXphiktfWsQiX28W4g2jCFFEazh01WBtpjM9LsLfbBgZA+z3jgfYa AXZuOvFQMJuUnLyOIGPtsaSTwXFDlJwNRgEw+Ctl22JSJbbu1yOoM9S6Ge27 rUVMhbAS5snVbNbLhgAymQLQQVLRwfsMLbtUfLTZuCC8zO8d16eOYw56+DqR QWNG1EXQz3CLeFDs2hFqcNWrAt773qANQ3zLPA65"
|
||||
str = strings.ReplaceAll(str, " ", "")
|
||||
decodeString, _ := base64.StdEncoding.DecodeString(str)
|
||||
decrypt := AesDecryptForCat(decodeString)
|
||||
t.Log(string(decrypt))
|
||||
}
|
||||
@@ -0,0 +1,838 @@
|
||||
package cryptoutil
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"math"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// RSA Constants
|
||||
const (
|
||||
biRadixBits = 16
|
||||
bitsPerDigit = biRadixBits
|
||||
biRadix = 1 << 16 // 65536
|
||||
biHalfRadix = biRadix >> 1
|
||||
biRadixSquared = biRadix * biRadix
|
||||
maxDigitVal = biRadix - 1
|
||||
maxInteger = 9999999999999998
|
||||
dpl10 = 15
|
||||
)
|
||||
|
||||
var (
|
||||
maxDigits int
|
||||
zeroArray []int
|
||||
bigZero *BigInt
|
||||
bigOne *BigInt
|
||||
lr10 *BigInt
|
||||
highBitMasks = []int{0x0000, 0x8000, 0xC000, 0xE000, 0xF000, 0xF800,
|
||||
0xFC00, 0xFE00, 0xFF00, 0xFF80, 0xFFC0, 0xFFE0,
|
||||
0xFFF0, 0xFFF8, 0xFFFC, 0xFFFE, 0xFFFF}
|
||||
lowBitMasks = []int{0x0000, 0x0001, 0x0003, 0x0007, 0x000F, 0x001F,
|
||||
0x003F, 0x007F, 0x00FF, 0x01FF, 0x03FF, 0x07FF,
|
||||
0x0FFF, 0x1FFF, 0x3FFF, 0x7FFF, 0xFFFF}
|
||||
hexToChar = []byte{'0', '1', '2', '3', '4', '5', '6', '7', '8', '9',
|
||||
'a', 'b', 'c', 'd', 'e', 'f'}
|
||||
hexatrigesimalToChar = []byte{
|
||||
'0', '1', '2', '3', '4', '5', '6', '7', '8', '9',
|
||||
'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j',
|
||||
'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't',
|
||||
'u', 'v', 'w', 'x', 'y', 'z'}
|
||||
)
|
||||
|
||||
// BigInt represents a big integer
|
||||
type BigInt struct {
|
||||
digits []int
|
||||
isNeg bool
|
||||
}
|
||||
|
||||
// NewBigInt creates a new BigInt
|
||||
func NewBigInt() *BigInt {
|
||||
digits := make([]int, len(zeroArray))
|
||||
copy(digits, zeroArray)
|
||||
return &BigInt{
|
||||
digits: digits,
|
||||
isNeg: false,
|
||||
}
|
||||
}
|
||||
|
||||
// NewBigIntEmpty creates an empty BigInt
|
||||
func NewBigIntEmpty() *BigInt {
|
||||
return &BigInt{
|
||||
digits: nil,
|
||||
isNeg: false,
|
||||
}
|
||||
}
|
||||
|
||||
// SetMaxDigits sets the maximum number of digits
|
||||
func SetMaxDigits(value int) {
|
||||
maxDigits = value
|
||||
zeroArray = make([]int, maxDigits)
|
||||
bigZero = NewBigInt()
|
||||
bigOne = NewBigInt()
|
||||
bigOne.digits[0] = 1
|
||||
lr10 = BiFromNumber(1000000000000000)
|
||||
}
|
||||
|
||||
// BiFromNumber creates a BigInt from a number
|
||||
func BiFromNumber(i int64) *BigInt {
|
||||
result := NewBigInt()
|
||||
result.isNeg = i < 0
|
||||
i = int64(math.Abs(float64(i)))
|
||||
j := 0
|
||||
for i > 0 {
|
||||
result.digits[j] = int(i & maxDigitVal)
|
||||
j++
|
||||
i = i / biRadix
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
// BiFromDecimal creates a BigInt from a decimal string
|
||||
func BiFromDecimal(s string) *BigInt {
|
||||
isNeg := len(s) > 0 && s[0] == '-'
|
||||
i := 0
|
||||
if isNeg {
|
||||
i = 1
|
||||
}
|
||||
|
||||
// Skip leading zeros
|
||||
for i < len(s) && s[i] == '0' {
|
||||
i++
|
||||
}
|
||||
|
||||
if i == len(s) {
|
||||
return NewBigInt()
|
||||
}
|
||||
|
||||
digitCount := len(s) - i
|
||||
fgl := digitCount % dpl10
|
||||
if fgl == 0 {
|
||||
fgl = dpl10
|
||||
}
|
||||
|
||||
num := parseNumber(s[i : i+fgl])
|
||||
result := BiFromNumber(num)
|
||||
i += fgl
|
||||
|
||||
for i < len(s) {
|
||||
result = BiAdd(BiMultiply(result, lr10), BiFromNumber(parseNumber(s[i:min(i+dpl10, len(s))])))
|
||||
i += dpl10
|
||||
}
|
||||
|
||||
result.isNeg = isNeg
|
||||
return result
|
||||
}
|
||||
|
||||
// BiCopy copies a BigInt
|
||||
func BiCopy(bi *BigInt) *BigInt {
|
||||
result := NewBigIntEmpty()
|
||||
result.digits = make([]int, len(bi.digits))
|
||||
copy(result.digits, bi.digits)
|
||||
result.isNeg = bi.isNeg
|
||||
return result
|
||||
}
|
||||
|
||||
// ReverseStr reverses a string
|
||||
func ReverseStr(s string) string {
|
||||
runes := []rune(s)
|
||||
for i, j := 0, len(runes)-1; i < j; i, j = i+1, j-1 {
|
||||
runes[i], runes[j] = runes[j], runes[i]
|
||||
}
|
||||
return string(runes)
|
||||
}
|
||||
|
||||
// BiToString converts BigInt to string with given radix
|
||||
func BiToString(x *BigInt, radix int) string {
|
||||
b := NewBigInt()
|
||||
b.digits[0] = radix
|
||||
qr := BiDivideModulo(x, b)
|
||||
result := string(hexatrigesimalToChar[qr[1].digits[0]])
|
||||
|
||||
for BiCompare(qr[0], bigZero) == 1 {
|
||||
qr = BiDivideModulo(qr[0], b)
|
||||
result += string(hexatrigesimalToChar[qr[1].digits[0]])
|
||||
}
|
||||
|
||||
prefix := ""
|
||||
if x.isNeg {
|
||||
prefix = "-"
|
||||
}
|
||||
return prefix + ReverseStr(result)
|
||||
}
|
||||
|
||||
// BiToDecimal converts BigInt to decimal string
|
||||
func BiToDecimal(x *BigInt) string {
|
||||
b := NewBigInt()
|
||||
b.digits[0] = 10
|
||||
qr := BiDivideModulo(x, b)
|
||||
result := fmt.Sprintf("%d", qr[1].digits[0])
|
||||
|
||||
for BiCompare(qr[0], bigZero) == 1 {
|
||||
qr = BiDivideModulo(qr[0], b)
|
||||
result += fmt.Sprintf("%d", qr[1].digits[0])
|
||||
}
|
||||
|
||||
prefix := ""
|
||||
if x.isNeg {
|
||||
prefix = "-"
|
||||
}
|
||||
return prefix + ReverseStr(result)
|
||||
}
|
||||
|
||||
// DigitToHex converts a digit to hex string
|
||||
func DigitToHex(n int) string {
|
||||
mask := 0xf
|
||||
result := ""
|
||||
for i := 0; i < 4; i++ {
|
||||
result += string(hexToChar[n&mask])
|
||||
n >>= 4
|
||||
}
|
||||
return ReverseStr(result)
|
||||
}
|
||||
|
||||
// BiToHex converts BigInt to hex string
|
||||
func BiToHex(x *BigInt) string {
|
||||
result := ""
|
||||
for i := BiHighIndex(x); i > -1; i-- {
|
||||
result += DigitToHex(x.digits[i])
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
// CharToHex converts a character code to hex value
|
||||
func CharToHex(c byte) int {
|
||||
const (
|
||||
ZERO = 48
|
||||
NINE = ZERO + 9
|
||||
littleA = 97
|
||||
littleZ = littleA + 25
|
||||
bigA = 65
|
||||
bigZ = 65 + 25
|
||||
)
|
||||
|
||||
if c >= ZERO && c <= NINE {
|
||||
return int(c - ZERO)
|
||||
} else if c >= bigA && c <= bigZ {
|
||||
return 10 + int(c-bigA)
|
||||
} else if c >= littleA && c <= littleZ {
|
||||
return 10 + int(c-littleA)
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
// HexToDigit converts hex string to digit
|
||||
func HexToDigit(s string) int {
|
||||
result := 0
|
||||
sl := min(len(s), 4)
|
||||
for i := 0; i < sl; i++ {
|
||||
result <<= 4
|
||||
result |= CharToHex(s[i])
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
// BiFromHex creates BigInt from hex string
|
||||
func BiFromHex(s string) *BigInt {
|
||||
result := NewBigInt()
|
||||
sl := len(s)
|
||||
for i, j := sl, 0; i > 0; i, j = i-4, j+1 {
|
||||
start := max(i-4, 0)
|
||||
length := min(i, 4)
|
||||
if start+length > sl {
|
||||
length = sl - start
|
||||
}
|
||||
result.digits[j] = HexToDigit(s[start : start+length])
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
// BiFromString creates BigInt from string with given radix
|
||||
func BiFromString(s string, radix int) *BigInt {
|
||||
isNeg := len(s) > 0 && s[0] == '-'
|
||||
istop := 0
|
||||
if isNeg {
|
||||
istop = 1
|
||||
}
|
||||
|
||||
result := NewBigInt()
|
||||
place := NewBigInt()
|
||||
place.digits[0] = 1
|
||||
|
||||
for i := len(s) - 1; i >= istop; i-- {
|
||||
c := s[i]
|
||||
digit := CharToHex(c)
|
||||
biDigit := BiMultiplyDigit(place, digit)
|
||||
result = BiAdd(result, biDigit)
|
||||
place = BiMultiplyDigit(place, radix)
|
||||
}
|
||||
|
||||
result.isNeg = isNeg
|
||||
return result
|
||||
}
|
||||
|
||||
// BiAdd adds two BigInts
|
||||
func BiAdd(x, y *BigInt) *BigInt {
|
||||
var result *BigInt
|
||||
|
||||
if x.isNeg != y.isNeg {
|
||||
y.isNeg = !y.isNeg
|
||||
result = BiSubtract(x, y)
|
||||
y.isNeg = !y.isNeg
|
||||
} else {
|
||||
result = NewBigInt()
|
||||
c := 0
|
||||
for i := 0; i < len(x.digits); i++ {
|
||||
n := x.digits[i] + y.digits[i] + c
|
||||
result.digits[i] = n % biRadix
|
||||
if n >= biRadix {
|
||||
c = 1
|
||||
} else {
|
||||
c = 0
|
||||
}
|
||||
}
|
||||
result.isNeg = x.isNeg
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
// BiSubtract subtracts two BigInts
|
||||
func BiSubtract(x, y *BigInt) *BigInt {
|
||||
var result *BigInt
|
||||
|
||||
if x.isNeg != y.isNeg {
|
||||
y.isNeg = !y.isNeg
|
||||
result = BiAdd(x, y)
|
||||
y.isNeg = !y.isNeg
|
||||
} else {
|
||||
result = NewBigInt()
|
||||
c := 0
|
||||
|
||||
for i := 0; i < len(x.digits); i++ {
|
||||
n := x.digits[i] - y.digits[i] + c
|
||||
result.digits[i] = n % biRadix
|
||||
if result.digits[i] < 0 {
|
||||
result.digits[i] += biRadix
|
||||
}
|
||||
if n < 0 {
|
||||
c = -1
|
||||
} else {
|
||||
c = 0
|
||||
}
|
||||
}
|
||||
|
||||
if c == -1 {
|
||||
c = 0
|
||||
for i := 0; i < len(x.digits); i++ {
|
||||
n := 0 - result.digits[i] + c
|
||||
result.digits[i] = n % biRadix
|
||||
if result.digits[i] < 0 {
|
||||
result.digits[i] += biRadix
|
||||
}
|
||||
if n < 0 {
|
||||
c = -1
|
||||
} else {
|
||||
c = 0
|
||||
}
|
||||
}
|
||||
result.isNeg = !x.isNeg
|
||||
} else {
|
||||
result.isNeg = x.isNeg
|
||||
}
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
// BiHighIndex returns the highest non-zero digit index
|
||||
func BiHighIndex(x *BigInt) int {
|
||||
result := len(x.digits) - 1
|
||||
for result > 0 && x.digits[result] == 0 {
|
||||
result--
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
// BiNumBits returns the number of bits
|
||||
func BiNumBits(x *BigInt) int {
|
||||
n := BiHighIndex(x)
|
||||
d := x.digits[n]
|
||||
m := (n + 1) * bitsPerDigit
|
||||
result := m
|
||||
for result > m-bitsPerDigit {
|
||||
if (d & 0x8000) != 0 {
|
||||
break
|
||||
}
|
||||
d <<= 1
|
||||
result--
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
// BiMultiply multiplies two BigInts
|
||||
func BiMultiply(x, y *BigInt) *BigInt {
|
||||
result := NewBigInt()
|
||||
n := BiHighIndex(x)
|
||||
t := BiHighIndex(y)
|
||||
|
||||
for i := 0; i <= t; i++ {
|
||||
c := 0
|
||||
k := i
|
||||
for j := 0; j <= n; j, k = j+1, k+1 {
|
||||
uv := result.digits[k] + x.digits[j]*y.digits[i] + c
|
||||
result.digits[k] = uv & maxDigitVal
|
||||
c = uv >> biRadixBits
|
||||
}
|
||||
result.digits[i+n+1] = c
|
||||
}
|
||||
|
||||
result.isNeg = x.isNeg != y.isNeg
|
||||
return result
|
||||
}
|
||||
|
||||
// BiMultiplyDigit multiplies BigInt by a digit
|
||||
func BiMultiplyDigit(x *BigInt, y int) *BigInt {
|
||||
result := NewBigInt()
|
||||
n := BiHighIndex(x)
|
||||
c := 0
|
||||
|
||||
for j := 0; j <= n; j++ {
|
||||
uv := result.digits[j] + x.digits[j]*y + c
|
||||
result.digits[j] = uv & maxDigitVal
|
||||
c = uv >> biRadixBits
|
||||
}
|
||||
result.digits[1+n] = c
|
||||
return result
|
||||
}
|
||||
|
||||
// ArrayCopy copies array elements
|
||||
func ArrayCopy(src []int, srcStart int, dest []int, destStart int, n int) {
|
||||
m := min(srcStart+n, len(src))
|
||||
for i, j := srcStart, destStart; i < m; i, j = i+1, j+1 {
|
||||
dest[j] = src[i]
|
||||
}
|
||||
}
|
||||
|
||||
// BiShiftLeft shifts BigInt left by n bits
|
||||
func BiShiftLeft(x *BigInt, n int) *BigInt {
|
||||
digitCount := n / bitsPerDigit
|
||||
result := NewBigInt()
|
||||
ArrayCopy(x.digits, 0, result.digits, digitCount, len(result.digits)-digitCount)
|
||||
|
||||
bits := n % bitsPerDigit
|
||||
rightBits := bitsPerDigit - bits
|
||||
|
||||
for i, i1 := len(result.digits)-1, len(result.digits)-2; i > 0; i, i1 = i-1, i1-1 {
|
||||
result.digits[i] = ((result.digits[i] << bits) & maxDigitVal) |
|
||||
((result.digits[i1] & highBitMasks[bits]) >> rightBits)
|
||||
}
|
||||
result.digits[0] = ((result.digits[0] << bits) & maxDigitVal)
|
||||
result.isNeg = x.isNeg
|
||||
return result
|
||||
}
|
||||
|
||||
// BiShiftRight shifts BigInt right by n bits
|
||||
func BiShiftRight(x *BigInt, n int) *BigInt {
|
||||
digitCount := n / bitsPerDigit
|
||||
result := NewBigInt()
|
||||
ArrayCopy(x.digits, digitCount, result.digits, 0, len(x.digits)-digitCount)
|
||||
|
||||
bits := n % bitsPerDigit
|
||||
leftBits := bitsPerDigit - bits
|
||||
|
||||
for i, i1 := 0, 1; i < len(result.digits)-1; i, i1 = i+1, i1+1 {
|
||||
result.digits[i] = (result.digits[i] >> bits) |
|
||||
((result.digits[i1] & lowBitMasks[bits]) << leftBits)
|
||||
}
|
||||
result.digits[len(result.digits)-1] >>= bits
|
||||
result.isNeg = x.isNeg
|
||||
return result
|
||||
}
|
||||
|
||||
// BiMultiplyByRadixPower multiplies BigInt by radix^n
|
||||
func BiMultiplyByRadixPower(x *BigInt, n int) *BigInt {
|
||||
result := NewBigInt()
|
||||
ArrayCopy(x.digits, 0, result.digits, n, len(result.digits)-n)
|
||||
return result
|
||||
}
|
||||
|
||||
// BiDivideByRadixPower divides BigInt by radix^n
|
||||
func BiDivideByRadixPower(x *BigInt, n int) *BigInt {
|
||||
result := NewBigInt()
|
||||
ArrayCopy(x.digits, n, result.digits, 0, len(result.digits)-n)
|
||||
return result
|
||||
}
|
||||
|
||||
// BiModuloByRadixPower computes BigInt mod radix^n
|
||||
func BiModuloByRadixPower(x *BigInt, n int) *BigInt {
|
||||
result := NewBigInt()
|
||||
ArrayCopy(x.digits, 0, result.digits, 0, n)
|
||||
return result
|
||||
}
|
||||
|
||||
// BiCompare compares two BigInts
|
||||
func BiCompare(x, y *BigInt) int {
|
||||
if x.isNeg != y.isNeg {
|
||||
if x.isNeg {
|
||||
return -1
|
||||
}
|
||||
return 1
|
||||
}
|
||||
|
||||
for i := len(x.digits) - 1; i >= 0; i-- {
|
||||
if x.digits[i] != y.digits[i] {
|
||||
if x.isNeg {
|
||||
if x.digits[i] > y.digits[i] {
|
||||
return -1
|
||||
}
|
||||
return 1
|
||||
} else {
|
||||
if x.digits[i] < y.digits[i] {
|
||||
return -1
|
||||
}
|
||||
return 1
|
||||
}
|
||||
}
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
// BiDivideModulo divides two BigInts and returns quotient and remainder
|
||||
func BiDivideModulo(x, y *BigInt) []*BigInt {
|
||||
nb := BiNumBits(x)
|
||||
tb := BiNumBits(y)
|
||||
origYIsNeg := y.isNeg
|
||||
var q, r *BigInt
|
||||
|
||||
if nb < tb {
|
||||
if x.isNeg {
|
||||
q = BiCopy(bigOne)
|
||||
q.isNeg = !y.isNeg
|
||||
x.isNeg = false
|
||||
y.isNeg = false
|
||||
r = BiSubtract(y, x)
|
||||
x.isNeg = true
|
||||
y.isNeg = origYIsNeg
|
||||
} else {
|
||||
q = NewBigInt()
|
||||
r = BiCopy(x)
|
||||
}
|
||||
return []*BigInt{q, r}
|
||||
}
|
||||
|
||||
q = NewBigInt()
|
||||
r = x
|
||||
|
||||
t := int(math.Ceil(float64(tb)/float64(bitsPerDigit))) - 1
|
||||
lambda := 0
|
||||
for y.digits[t] < biHalfRadix {
|
||||
y = BiShiftLeft(y, 1)
|
||||
lambda++
|
||||
tb++
|
||||
t = int(math.Ceil(float64(tb)/float64(bitsPerDigit))) - 1
|
||||
}
|
||||
|
||||
r = BiShiftLeft(r, lambda)
|
||||
nb += lambda
|
||||
n := int(math.Ceil(float64(nb)/float64(bitsPerDigit))) - 1
|
||||
|
||||
b := BiMultiplyByRadixPower(y, n-t)
|
||||
for BiCompare(r, b) != -1 {
|
||||
q.digits[n-t]++
|
||||
r = BiSubtract(r, b)
|
||||
}
|
||||
|
||||
for i := n; i > t; i-- {
|
||||
ri := 0
|
||||
if i < len(r.digits) {
|
||||
ri = r.digits[i]
|
||||
}
|
||||
ri1 := 0
|
||||
if i-1 < len(r.digits) {
|
||||
ri1 = r.digits[i-1]
|
||||
}
|
||||
ri2 := 0
|
||||
if i-2 < len(r.digits) {
|
||||
ri2 = r.digits[i-2]
|
||||
}
|
||||
yt := 0
|
||||
if t < len(y.digits) {
|
||||
yt = y.digits[t]
|
||||
}
|
||||
yt1 := 0
|
||||
if t-1 < len(y.digits) {
|
||||
yt1 = y.digits[t-1]
|
||||
}
|
||||
|
||||
if ri == yt {
|
||||
q.digits[i-t-1] = maxDigitVal
|
||||
} else {
|
||||
q.digits[i-t-1] = (ri*biRadix + ri1) / yt
|
||||
}
|
||||
|
||||
c1 := q.digits[i-t-1] * ((yt * biRadix) + yt1)
|
||||
c2 := (ri * biRadixSquared) + ((ri1 * biRadix) + ri2)
|
||||
|
||||
for c1 > c2 {
|
||||
q.digits[i-t-1]--
|
||||
c1 = q.digits[i-t-1] * ((yt * biRadix) | yt1)
|
||||
c2 = (ri * biRadix * biRadix) + ((ri1 * biRadix) + ri2)
|
||||
}
|
||||
|
||||
b = BiMultiplyByRadixPower(y, i-t-1)
|
||||
r = BiSubtract(r, BiMultiplyDigit(b, q.digits[i-t-1]))
|
||||
if r.isNeg {
|
||||
r = BiAdd(r, b)
|
||||
q.digits[i-t-1]--
|
||||
}
|
||||
}
|
||||
|
||||
r = BiShiftRight(r, lambda)
|
||||
q.isNeg = x.isNeg != origYIsNeg
|
||||
|
||||
if x.isNeg {
|
||||
if origYIsNeg {
|
||||
q = BiAdd(q, bigOne)
|
||||
} else {
|
||||
q = BiSubtract(q, bigOne)
|
||||
}
|
||||
y = BiShiftRight(y, lambda)
|
||||
r = BiSubtract(y, r)
|
||||
}
|
||||
|
||||
if r.digits[0] == 0 && BiHighIndex(r) == 0 {
|
||||
r.isNeg = false
|
||||
}
|
||||
|
||||
return []*BigInt{q, r}
|
||||
}
|
||||
|
||||
// BiDivide divides two BigInts
|
||||
func BiDivide(x, y *BigInt) *BigInt {
|
||||
return BiDivideModulo(x, y)[0]
|
||||
}
|
||||
|
||||
// BiModulo computes x mod y
|
||||
func BiModulo(x, y *BigInt) *BigInt {
|
||||
return BiDivideModulo(x, y)[1]
|
||||
}
|
||||
|
||||
// BiMultiplyMod computes (x * y) mod m
|
||||
func BiMultiplyMod(x, y, m *BigInt) *BigInt {
|
||||
return BiModulo(BiMultiply(x, y), m)
|
||||
}
|
||||
|
||||
// BiPowMod computes x^y mod m
|
||||
func BiPowMod(x, y, m *BigInt) *BigInt {
|
||||
result := BiCopy(bigOne)
|
||||
a := x
|
||||
k := y
|
||||
|
||||
for {
|
||||
if (k.digits[0] & 1) != 0 {
|
||||
result = BiMultiplyMod(result, a, m)
|
||||
}
|
||||
k = BiShiftRight(k, 1)
|
||||
if k.digits[0] == 0 && BiHighIndex(k) == 0 {
|
||||
break
|
||||
}
|
||||
a = BiMultiplyMod(a, a, m)
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
// BarrettMu represents Barrett reduction
|
||||
type BarrettMu struct {
|
||||
modulus *BigInt
|
||||
k int
|
||||
mu *BigInt
|
||||
bkplus1 *BigInt
|
||||
}
|
||||
|
||||
// NewBarrettMu creates a new BarrettMu
|
||||
func NewBarrettMu(m *BigInt) *BarrettMu {
|
||||
bm := &BarrettMu{
|
||||
modulus: BiCopy(m),
|
||||
k: BiHighIndex(m) + 1,
|
||||
}
|
||||
|
||||
b2k := NewBigInt()
|
||||
if 2*bm.k < len(b2k.digits) {
|
||||
b2k.digits[2*bm.k] = 1
|
||||
}
|
||||
bm.mu = BiDivide(b2k, bm.modulus)
|
||||
|
||||
bm.bkplus1 = NewBigInt()
|
||||
if bm.k+1 < len(bm.bkplus1.digits) {
|
||||
bm.bkplus1.digits[bm.k+1] = 1
|
||||
}
|
||||
|
||||
return bm
|
||||
}
|
||||
|
||||
// Modulo performs Barrett modulo
|
||||
func (bm *BarrettMu) Modulo(x *BigInt) *BigInt {
|
||||
q1 := BiDivideByRadixPower(x, bm.k-1)
|
||||
q2 := BiMultiply(q1, bm.mu)
|
||||
q3 := BiDivideByRadixPower(q2, bm.k+1)
|
||||
r1 := BiModuloByRadixPower(x, bm.k+1)
|
||||
r2term := BiMultiply(q3, bm.modulus)
|
||||
r2 := BiModuloByRadixPower(r2term, bm.k+1)
|
||||
r := BiSubtract(r1, r2)
|
||||
|
||||
if r.isNeg {
|
||||
r = BiAdd(r, bm.bkplus1)
|
||||
}
|
||||
|
||||
for BiCompare(r, bm.modulus) >= 0 {
|
||||
r = BiSubtract(r, bm.modulus)
|
||||
}
|
||||
|
||||
return r
|
||||
}
|
||||
|
||||
// MultiplyMod performs modular multiplication
|
||||
func (bm *BarrettMu) MultiplyMod(x, y *BigInt) *BigInt {
|
||||
xy := BiMultiply(x, y)
|
||||
return bm.Modulo(xy)
|
||||
}
|
||||
|
||||
// PowMod performs modular exponentiation
|
||||
func (bm *BarrettMu) PowMod(x, y *BigInt) *BigInt {
|
||||
result := NewBigInt()
|
||||
result.digits[0] = 1
|
||||
a := x
|
||||
k := y
|
||||
|
||||
for {
|
||||
if (k.digits[0] & 1) != 0 {
|
||||
result = bm.MultiplyMod(result, a)
|
||||
}
|
||||
k = BiShiftRight(k, 1)
|
||||
if k.digits[0] == 0 && BiHighIndex(k) == 0 {
|
||||
break
|
||||
}
|
||||
a = bm.MultiplyMod(a, a)
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
// NutRSAKeyPair represents an RSA key pair
|
||||
type NutRSAKeyPair struct {
|
||||
e *BigInt
|
||||
d *BigInt
|
||||
m *BigInt
|
||||
chunkSize int
|
||||
radix int
|
||||
barrett *BarrettMu
|
||||
}
|
||||
|
||||
// NewNutRSAKeyPair creates a new RSA key pair
|
||||
func NewNutRSAKeyPair(encryptionExponent, decryptionExponent, modulus string) *NutRSAKeyPair {
|
||||
kp := &NutRSAKeyPair{
|
||||
e: BiFromHex(encryptionExponent),
|
||||
d: BiFromHex(decryptionExponent),
|
||||
m: BiFromHex(modulus),
|
||||
radix: 16,
|
||||
}
|
||||
kp.chunkSize = 2 * BiHighIndex(kp.m)
|
||||
kp.barrett = NewBarrettMu(kp.m)
|
||||
return kp
|
||||
}
|
||||
|
||||
// EncryptedString encrypts a string
|
||||
func (kp *NutRSAKeyPair) EncryptedString(s string) string {
|
||||
a := make([]int, 0)
|
||||
for i := 0; i < len(s); i++ {
|
||||
a = append(a, int(s[i]))
|
||||
}
|
||||
|
||||
// Padding
|
||||
for len(a)%kp.chunkSize != 0 {
|
||||
a = append(a, 0)
|
||||
}
|
||||
|
||||
result := ""
|
||||
for i := 0; i < len(a); i += kp.chunkSize {
|
||||
block := NewBigInt()
|
||||
j := 0
|
||||
for k := i; k < i+kp.chunkSize; j++ {
|
||||
if k < len(a) {
|
||||
block.digits[j] = a[k]
|
||||
k++
|
||||
}
|
||||
if k < len(a) {
|
||||
block.digits[j] += a[k] << 8
|
||||
k++
|
||||
}
|
||||
}
|
||||
crypt := kp.barrett.PowMod(block, kp.e)
|
||||
var text string
|
||||
if kp.radix == 16 {
|
||||
text = BiToHex(crypt)
|
||||
} else {
|
||||
text = BiToString(crypt, kp.radix)
|
||||
}
|
||||
result += text + " "
|
||||
}
|
||||
|
||||
if len(result) > 0 {
|
||||
return result[:len(result)-1]
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
// DecryptedString decrypts a string
|
||||
func (kp *NutRSAKeyPair) DecryptedString(s string) string {
|
||||
blocks := strings.Split(s, " ")
|
||||
result := ""
|
||||
|
||||
for i := 0; i < len(blocks); i++ {
|
||||
var bi *BigInt
|
||||
if kp.radix == 16 {
|
||||
bi = BiFromHex(blocks[i])
|
||||
} else {
|
||||
bi = BiFromString(blocks[i], kp.radix)
|
||||
}
|
||||
block := kp.barrett.PowMod(bi, kp.d)
|
||||
|
||||
for j := 0; j <= BiHighIndex(block); j++ {
|
||||
result += string(rune(block.digits[j] & 255))
|
||||
result += string(rune(block.digits[j] >> 8))
|
||||
}
|
||||
}
|
||||
|
||||
// Remove trailing null
|
||||
if len(result) > 0 && result[len(result)-1] == 0 {
|
||||
result = result[:len(result)-1]
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
// Helper functions
|
||||
func min(a, b int) int {
|
||||
if a < b {
|
||||
return a
|
||||
}
|
||||
return b
|
||||
}
|
||||
|
||||
func max(a, b int) int {
|
||||
if a > b {
|
||||
return a
|
||||
}
|
||||
return b
|
||||
}
|
||||
|
||||
func parseNumber(s string) int64 {
|
||||
var result int64
|
||||
fmt.Sscanf(s, "%d", &result)
|
||||
return result
|
||||
}
|
||||
|
||||
func init() {
|
||||
SetMaxDigits(130)
|
||||
}
|
||||
@@ -12,6 +12,7 @@ import (
|
||||
"likedream-goframe/internal/utils"
|
||||
|
||||
"github.com/bytedance/sonic"
|
||||
"github.com/gogf/gf/v2/net/ghttp"
|
||||
"github.com/google/uuid"
|
||||
"github.com/samber/oops"
|
||||
"resty.dev/v3"
|
||||
@@ -80,11 +81,54 @@ func UploadImageFromStorage(ctx context.Context, host string, imageId uuid.UUID,
|
||||
return previewUrl
|
||||
}
|
||||
|
||||
func UploadImageFromFile(ctx context.Context, host string, file *ghttp.UploadFile, taskType types.TaskType, client *resty.Client) string {
|
||||
|
||||
imageUploadPolicy := GetUploadPolicyAsync(ctx, host, taskType, client)
|
||||
open, _ := file.Open()
|
||||
resp, e := client.R().
|
||||
SetMultipartFields(&resty.MultipartField{
|
||||
Name: "x-obs-acl",
|
||||
Values: []string{"public-read"},
|
||||
}, &resty.MultipartField{
|
||||
Name: "AccessKeyId",
|
||||
Values: []string{imageUploadPolicy.AccessId},
|
||||
}, &resty.MultipartField{
|
||||
Name: "key",
|
||||
Values: []string{imageUploadPolicy.FileName},
|
||||
}, &resty.MultipartField{
|
||||
Name: "signature",
|
||||
Values: []string{imageUploadPolicy.Signature},
|
||||
}, &resty.MultipartField{
|
||||
Name: "policy",
|
||||
Values: []string{imageUploadPolicy.Policy},
|
||||
}, &resty.MultipartField{
|
||||
Name: "content-type",
|
||||
Values: []string{file.Header.Get("Content-Type")},
|
||||
}, &resty.MultipartField{
|
||||
Name: "file",
|
||||
FileName: "file",
|
||||
Reader: open,
|
||||
}).
|
||||
Post(imageUploadPolicy.Host)
|
||||
|
||||
utils.OkOrPanic(e, oops.WithContext(ctx).Wrap)
|
||||
|
||||
if resp.StatusCode() > 299 {
|
||||
utils.OkOrPanic(errors.NewBusinessError(503, "上传图片失败: "+utils.ReadString(resp.Body)), oops.WithContext(ctx).With("response", utils.ReadString(resp.Body)).Wrap)
|
||||
}
|
||||
|
||||
previewUrl := GetPreviewUrlAsync(ctx, host, imageUploadPolicy.FileName, taskType, client)
|
||||
|
||||
return previewUrl
|
||||
}
|
||||
|
||||
// GetPreviewUrlAsync 用于获取图片预览地址
|
||||
func GetPreviewUrlAsync(ctx context.Context, host string, fileKey string, taskType types.TaskType, client *resty.Client) string {
|
||||
var apiPath string
|
||||
if taskType == types.TaskTypeSleepCheck {
|
||||
apiPath = "/previewAttachment"
|
||||
} else if taskType == types.TaskTypeLeave {
|
||||
apiPath = "/obs/previewAttachment"
|
||||
} else {
|
||||
apiPath = "/sign/previewAttachment"
|
||||
}
|
||||
@@ -164,6 +208,8 @@ func GetImageUploadUrlPrefix(ctx context.Context, taskType types.TaskType) strin
|
||||
return "/wec-counselor-sign-apps/stu"
|
||||
case types.TaskTypeCollection:
|
||||
return "/wec-counselor-collector-apps/student/collector"
|
||||
case types.TaskTypeLeave:
|
||||
return "/wec-counselor-leave-apps/leave"
|
||||
default:
|
||||
utils.OkOrPanic(errors.NewBusinessError(503, "未知的任务类型"), oops.WithContext(ctx).Wrap)
|
||||
panic("unreachable")
|
||||
|
||||
@@ -0,0 +1,167 @@
|
||||
package leave
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
v1 "likedream-goframe/internal/api/v1"
|
||||
"likedream-goframe/internal/application/cpdaily"
|
||||
"likedream-goframe/internal/application/cpdaily/leave/model"
|
||||
"likedream-goframe/internal/application/cpdaily/taskhelper"
|
||||
"likedream-goframe/internal/common/errors"
|
||||
"likedream-goframe/internal/utils"
|
||||
|
||||
"github.com/gogf/gf/v2/container/gvar"
|
||||
"github.com/tidwall/gjson"
|
||||
)
|
||||
|
||||
// CheckOnceLeave 检查是否允许请假
|
||||
func (c *CpdailyReportApp) CheckOnceLeave() error {
|
||||
post, err := c.client.R().SetBody(map[string]any{}).
|
||||
Post(fmt.Sprintf("%s/wec-counselor-leave-apps/leave/stu/checkOnceLeave", c.host))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
result := utils.ReadGjsonResult(post.Body)
|
||||
if result.Get("code").String() != "0" {
|
||||
return errors.NewBusinessError(400, result.Get("message").String())
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// QueryLeaveTypes 获取请假类型列表
|
||||
func (c *CpdailyReportApp) QueryLeaveTypes() gjson.Result {
|
||||
post, err := c.client.R().SetBody(map[string]any{}).
|
||||
Post(fmt.Sprintf("%s/wec-counselor-leave-apps/leave/queryLeaveTypes", c.host))
|
||||
utils.OkOrPanic(err)
|
||||
result := utils.ReadGjsonResult(post.Body)
|
||||
if result.Get("code").String() != "0" {
|
||||
utils.OkOrPanic(errors.NewBusinessError(503, result.Get("message").String()))
|
||||
}
|
||||
return result.Get("datas.leaveType")
|
||||
}
|
||||
|
||||
// QueryLeaveTypeCfg 查询请假类型配置
|
||||
func (c *CpdailyReportApp) QueryLeaveTypeCfg(code int) gjson.Result {
|
||||
post, err := c.client.R().SetBody(map[string]any{"code": code}).
|
||||
Post(fmt.Sprintf("%s/wec-counselor-leave-apps/leave/stu/queryLeaveTypeCfg", c.host))
|
||||
utils.OkOrPanic(err)
|
||||
result := utils.ReadGjsonResult(post.Body)
|
||||
if result.Get("code").String() != "0" {
|
||||
utils.OkOrPanic(errors.NewBusinessError(503, result.Get("message").String()))
|
||||
}
|
||||
return result.Get("datas")
|
||||
}
|
||||
|
||||
// ObtainApproveFlow 获取审批人列表
|
||||
func (c *CpdailyReportApp) ObtainApproveFlow(leaveType int, startTime, endTime string) gjson.Result {
|
||||
post, err := c.client.R().SetBody(map[string]any{
|
||||
"leaveType": leaveType,
|
||||
"startTime": startTime,
|
||||
"endTime": endTime,
|
||||
}).Post(fmt.Sprintf("%s/wec-counselor-leave-apps/leave/stu/obtainApproveFlow", c.host))
|
||||
utils.OkOrPanic(err)
|
||||
result := utils.ReadGjsonResult(post.Body)
|
||||
if result.Get("code").String() != "0" {
|
||||
utils.OkOrPanic(errors.NewBusinessError(503, result.Get("message").String()))
|
||||
}
|
||||
return result.Get("datas")
|
||||
}
|
||||
|
||||
// SubmitLeaveApply 提交请假申请
|
||||
func (c *CpdailyReportApp) SubmitLeaveApply(ctx context.Context, req *v1.CpLeaveApplyReq, username, deviceId string) {
|
||||
cpdailyInfo := cpdaily.GetInfo(ctx)
|
||||
leaveType := 0
|
||||
if req.LeaveType != nil {
|
||||
leaveType = *req.LeaveType
|
||||
}
|
||||
urgencyMobile := req.UrgencyMobile
|
||||
address := req.Address
|
||||
detailAddress := req.DetailAddress
|
||||
destination := ""
|
||||
if req.OutStatus {
|
||||
destination = address
|
||||
if detailAddress != "" {
|
||||
if destination != "" {
|
||||
destination += "/"
|
||||
}
|
||||
destination += detailAddress
|
||||
}
|
||||
} else {
|
||||
urgencyMobile = ""
|
||||
address = ""
|
||||
detailAddress = ""
|
||||
}
|
||||
|
||||
// 构造请假申请体
|
||||
applyBody := map[string]any{
|
||||
"leaveType": leaveType,
|
||||
"startTime": req.StartTime,
|
||||
"endTime": req.EndTime,
|
||||
"urgencyMobile": urgencyMobile,
|
||||
"leaveReason": req.LeaveReason,
|
||||
"isAttendance": nil,
|
||||
"outStatus": req.OutStatus,
|
||||
"address": address,
|
||||
"detailAddress": detailAddress,
|
||||
"destination": destination,
|
||||
"holidayAutoDefineVos": []any{},
|
||||
"appVersion": cpdailyInfo.AppVersion,
|
||||
"deviceId": deviceId,
|
||||
"ccInfo": map[string]any{"teachers": []any{}, "students": []any{}},
|
||||
"cheatType": nil,
|
||||
"source": 1,
|
||||
"location": map[string]any{
|
||||
"deviceId": deviceId,
|
||||
"systemName": "Android",
|
||||
"systemVersion": cpdailyInfo.SystemVersion,
|
||||
"model": "Redmi Note 8 Pro",
|
||||
"address": req.LocationAddr,
|
||||
"latitude": req.Latitude,
|
||||
"longitude": req.Longitude,
|
||||
},
|
||||
"locationType": 0,
|
||||
"disclaimers": "本人承诺填写的信息真实有效,并对本次提交请假申请的信息真实性负责。",
|
||||
"uaIsCpadaily": "true",
|
||||
"leaveApplyExplain": nil,
|
||||
}
|
||||
|
||||
// 审批人信息
|
||||
if req.ApproverWid != "" {
|
||||
applyBody["approverWid"] = req.ApproverWid
|
||||
applyBody["approverId"] = req.ApproverId
|
||||
applyBody["approverName"] = req.ApproverName
|
||||
}
|
||||
|
||||
// 图片附件
|
||||
if req.ImageUrl != "" {
|
||||
applyBody["applyAttach"] = []model.Attach{{Type: 1, Url: req.ImageUrl}}
|
||||
} else {
|
||||
applyBody["applyAttach"] = []any{}
|
||||
}
|
||||
|
||||
// 构造加密扩展头
|
||||
extensionForm := map[string]*gvar.Var{
|
||||
"lon": gvar.New(req.Longitude),
|
||||
"lat": gvar.New(req.Latitude),
|
||||
"model": gvar.New("Redmi Note 8 Pro"),
|
||||
"appVersion": gvar.New(cpdailyInfo.AppVersion),
|
||||
"systemVersion": gvar.New(cpdailyInfo.SystemVersion),
|
||||
"userId": gvar.New(username),
|
||||
"systemName": gvar.New("android"),
|
||||
"deviceId": gvar.New(deviceId),
|
||||
}
|
||||
extensionString := taskhelper.GetCpdailyExtension(ctx, extensionForm)
|
||||
|
||||
// 提交请假
|
||||
response, e := c.client.R().
|
||||
SetBody(applyBody).
|
||||
WithContext(ctx).
|
||||
SetHeader("Cpdaily-Extension", extensionString).
|
||||
Post(fmt.Sprintf("%s/wec-counselor-leave-apps/leave/apply", c.host))
|
||||
|
||||
utils.OkOrPanic(e)
|
||||
responseJSON := utils.ReadGjsonResult(response.Body)
|
||||
if responseJSON.Get("code").String() != "0" {
|
||||
utils.OkOrPanic(errors.NewBusinessError(503, responseJSON.Get("message").String()))
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
package model
|
||||
|
||||
type Attach struct {
|
||||
Type int `json:"type"`
|
||||
Url string `json:"url"`
|
||||
}
|
||||
@@ -0,0 +1,210 @@
|
||||
package leave
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
v1 "likedream-goframe/internal/api/v1"
|
||||
"likedream-goframe/internal/application/cpdaily"
|
||||
"likedream-goframe/internal/application/cpdaily/cryptoutil"
|
||||
"likedream-goframe/internal/application/cpdaily/imageuploader"
|
||||
"likedream-goframe/internal/application/cpdaily/leave/model"
|
||||
"likedream-goframe/internal/application/cpdaily/task/leavetype"
|
||||
"likedream-goframe/internal/application/cpdaily/taskhelper"
|
||||
"likedream-goframe/internal/application/cpdaily/types"
|
||||
"likedream-goframe/internal/common/errors"
|
||||
"likedream-goframe/internal/utils"
|
||||
|
||||
"github.com/gogf/gf/v2/container/gvar"
|
||||
"github.com/gogf/gf/v2/net/ghttp"
|
||||
"github.com/google/uuid"
|
||||
"github.com/samber/oops"
|
||||
"github.com/tidwall/gjson"
|
||||
"resty.dev/v3"
|
||||
)
|
||||
|
||||
/**
|
||||
* @Description: 销假相关功能
|
||||
*/
|
||||
type CpdailyReportApp struct {
|
||||
client *resty.Client
|
||||
host string
|
||||
}
|
||||
|
||||
func NewCpdailyReportApp(client *resty.Client, host string) *CpdailyReportApp {
|
||||
|
||||
return &CpdailyReportApp{
|
||||
client: client,
|
||||
host: host,
|
||||
}
|
||||
}
|
||||
|
||||
// 获取请假记录
|
||||
func (c *CpdailyReportApp) GetLeaveList() []leavetype.LeaveListOne {
|
||||
// https://fzfu.campusphere.net/wec-counselor-leave-apps/leave/stu/listOther
|
||||
post, err := c.client.R().SetBody(map[string]int{
|
||||
"pageNumber": 1,
|
||||
"pageSize": 20,
|
||||
"needApproval": 1,
|
||||
}).Post(fmt.Sprintf("%s/wec-counselor-leave-apps/leave/stu/listOther", c.host))
|
||||
|
||||
utils.OkOrPanic(err, func(err error) error {
|
||||
return oops.
|
||||
With("host", c.host).
|
||||
In("CpdailyReport.GetLeaveList").
|
||||
Wrap(err)
|
||||
})
|
||||
responseJSON := utils.ReadGjsonResult(post.Body)
|
||||
rows := responseJSON.Get("datas.rows")
|
||||
|
||||
if !rows.Exists() {
|
||||
utils.OkOrPanic(errors.NewBusinessError(503, "没有获取到待销假信息"), func(err error) error {
|
||||
return oops.
|
||||
With("host", c.host).
|
||||
In("CpdailyReport.GetLeaveList").
|
||||
Wrap(err)
|
||||
})
|
||||
}
|
||||
|
||||
var listList []leavetype.LeaveListOne
|
||||
for _, row := range rows.Array() {
|
||||
// 已完成
|
||||
if row.Get("status").String() == "4" {
|
||||
continue
|
||||
}
|
||||
id := row.Get("id").String()
|
||||
detail := c.LeaveDetail(id)
|
||||
leaveOne := leavetype.LeaveListOne{
|
||||
Id: row.Get("id").String(),
|
||||
LeaveType: row.Get("leaveType").String(),
|
||||
CreateTime: row.Get("createTime").String(),
|
||||
Reason: row.Get("leaveReason").String(),
|
||||
AceEndTime: row.Get("actEndTime").String(),
|
||||
Status: row.Get("status").String(),
|
||||
ChooseAttendance: detail.Get("leaveType.chooseAttendance").Int(),
|
||||
//Longitude: detail.Get("detail.applyLocation.longitude").String(),
|
||||
//Latitude: detail.Get("detail.applyLocation.latitude").String(),
|
||||
//Address: detail.Get("detail.applyLocation.address").String(),
|
||||
|
||||
Longitude: detail.Get("leaveExtends.0.longitude").String(),
|
||||
Latitude: detail.Get("leaveExtends.0.latitude").String(),
|
||||
Address: detail.Get("leaveExtends.0.address").String(),
|
||||
}
|
||||
|
||||
listList = append(listList, leaveOne)
|
||||
}
|
||||
return listList
|
||||
}
|
||||
|
||||
func (c *CpdailyReportApp) LeaveDetail(leaveId string) gjson.Result {
|
||||
// https://fzfu.campusphere.net/wec-counselor-leave-apps/leave/stu/detailOther
|
||||
post, err := c.client.R().SetBody(map[string]any{
|
||||
"id": leaveId,
|
||||
"needApproval": 1,
|
||||
"uaIsCpadaily": "true",
|
||||
}).Post(fmt.Sprintf("%s/wec-counselor-leave-apps/leave/stu/detailOther", c.host))
|
||||
utils.OkOrPanic(err, func(err error) error {
|
||||
return oops.
|
||||
With("host", c.host).
|
||||
In("CpdailyReport.LeaveDetail").
|
||||
Wrap(err)
|
||||
})
|
||||
datas := utils.ReadGjsonResult(post.Body).Get("datas")
|
||||
if !datas.Exists() {
|
||||
utils.OkOrPanic(errors.NewBusinessError(503, "没有获取到待销假详情信息"), func(err error) error {
|
||||
return oops.
|
||||
With("host", c.host).
|
||||
In("CpdailyReport.LeaveDetail").
|
||||
Wrap(err)
|
||||
})
|
||||
}
|
||||
return datas
|
||||
}
|
||||
|
||||
func (c *CpdailyReportApp) UploadImage(ctx context.Context, file *ghttp.UploadFile) string {
|
||||
return imageuploader.UploadImageFromFile(ctx, c.host, file, types.TaskTypeLeave, c.client)
|
||||
}
|
||||
|
||||
func (c *CpdailyReportApp) FillReportForm(detail gjson.Result, req *v1.CpLeaveReportReq) map[string]*gvar.Var {
|
||||
resultForm := make(map[string]*gvar.Var)
|
||||
resultForm["id"] = gvar.New(detail.Get("detail.id").String())
|
||||
resultForm["needApproval"] = gvar.New(1)
|
||||
resultForm["leaveType"] = gvar.New(detail.Get("leaveType.code").String())
|
||||
if req.Address == "" {
|
||||
resultForm["reportLocation"] = gvar.New(detail.Get("leaveExtends.0.address").String())
|
||||
} else {
|
||||
resultForm["reportLocation"] = gvar.New(req.Address)
|
||||
}
|
||||
resultForm["approverWid"] = gvar.New("")
|
||||
resultForm["reportApplyExplain"] = gvar.New(nil)
|
||||
resultForm["uaIsCpadaily"] = gvar.New(true)
|
||||
if req.ImageUrl != "" {
|
||||
var attach []model.Attach
|
||||
attach = append(attach, model.Attach{
|
||||
Type: 1,
|
||||
Url: req.ImageUrl,
|
||||
})
|
||||
resultForm["reportAttach"] = gvar.New(attach)
|
||||
} else {
|
||||
resultForm["reportAttach"] = gvar.New([]string{})
|
||||
}
|
||||
return resultForm
|
||||
}
|
||||
|
||||
func (c *CpdailyReportApp) GetLocation(detail gjson.Result) (lon, lat string) {
|
||||
applyLocation := detail.Get("detail.applyLocation")
|
||||
lon = applyLocation.Get("longitude").String()
|
||||
lat = applyLocation.Get("latitude").String()
|
||||
return
|
||||
}
|
||||
func (c *CpdailyReportApp) SubmitReport(ctx context.Context, form map[string]*gvar.Var, lon, lat, username, deviceId string) {
|
||||
cpdailyInfo := cpdaily.GetInfo(ctx)
|
||||
|
||||
extensionForm := map[string]*gvar.Var{
|
||||
"lon": gvar.New(lon),
|
||||
"lat": gvar.New(lat),
|
||||
"model": gvar.New(uuid.New().String()),
|
||||
"appVersion": gvar.New(cpdailyInfo.AppVersion),
|
||||
"systemVersion": gvar.New(cpdailyInfo.SystemVersion),
|
||||
"userId": gvar.New(username),
|
||||
"systemName": gvar.New("android"),
|
||||
"deviceId": gvar.New(deviceId),
|
||||
}
|
||||
|
||||
extensionString := taskhelper.GetCpdailyExtension(ctx, extensionForm)
|
||||
bodyString := taskhelper.GetBodyString(ctx, form)
|
||||
// 构造提交数据
|
||||
submitData := map[string]*gvar.Var{
|
||||
"lon": gvar.New(lon),
|
||||
"version": gvar.New(cryptoutil.EncryptVersion),
|
||||
"calVersion": gvar.New(cpdailyInfo.CalVersion),
|
||||
"deviceId": extensionForm["deviceId"],
|
||||
"userId": gvar.New(username),
|
||||
"systemName": gvar.New("android"),
|
||||
"bodyString": gvar.New(bodyString),
|
||||
"lat": gvar.New(lat),
|
||||
"systemVersion": gvar.New(cpdailyInfo.SystemVersion),
|
||||
"appVersion": gvar.New(cpdailyInfo.AppVersion),
|
||||
"model": extensionForm["model"],
|
||||
}
|
||||
// https://fzfu.campusphere.net/wec-counselor-leave-apps/leave/report
|
||||
response, e := c.client.R().
|
||||
SetBody(submitData).
|
||||
WithContext(ctx).
|
||||
SetHeader("Cpdaily-Extension", extensionString).
|
||||
Post(fmt.Sprintf("%s/wec-counselor-leave-apps/leave/report", c.host))
|
||||
|
||||
utils.OkOrPanic(e, func(err error) error {
|
||||
return oops.
|
||||
With("host", c.host).
|
||||
With("username", username).
|
||||
With("deviceId", deviceId).
|
||||
In("CpdailySleepCheckTask.Submit").
|
||||
Wrap(err)
|
||||
})
|
||||
|
||||
responseJSON := utils.ReadGjsonResult(response.Body)
|
||||
|
||||
if responseJSON.Get("code").String() != "0" {
|
||||
utils.OkOrPanic(errors.NewBusinessError(503, responseJSON.Get("message").String()), oops.WithContext(ctx).Wrap)
|
||||
}
|
||||
}
|
||||
File diff suppressed because one or more lines are too long
@@ -204,6 +204,6 @@ func (c CpdailyAhjzuLogin) GetClient() *resty.Client {
|
||||
|
||||
func NewAhjzu(ctx context.Context) *CpdailyAhjzuLogin {
|
||||
return &CpdailyAhjzuLogin{
|
||||
client: httpclient.Cpdaily.Clone(ctx),
|
||||
client: httpclient.NewCpdailyClient(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,6 +13,17 @@ func GetCpdailyLoginBySchoolInfo(ctx context.Context, info *school.SchoolDetailI
|
||||
// 安徽建筑大学
|
||||
case "ahjzu":
|
||||
return NewAhjzu(ctx)
|
||||
// 南宁师范大学
|
||||
case "2ba0ea59-a764-4482-bb2b-c924b726b04c":
|
||||
return NewNnnu(ctx)
|
||||
// 长春大学
|
||||
case "ccu":
|
||||
fallthrough
|
||||
// 河北石油职业技术大学
|
||||
case "01e270d1-00e2-4ce9-b0af-5d5fbd462b8a":
|
||||
return NewIap(ctx)
|
||||
case "676c9395-a29c-4068-97a8-b48e05dae499":
|
||||
return NewHnuu(ctx)
|
||||
default:
|
||||
if info.IsIap() {
|
||||
return NewIap(ctx)
|
||||
|
||||
@@ -23,6 +23,7 @@ import (
|
||||
"github.com/PuerkitoBio/goquery"
|
||||
"github.com/avast/retry-go/v4"
|
||||
"github.com/deatil/go-cryptobin/cryptobin/crypto"
|
||||
"github.com/gogf/gf/v2/frame/g"
|
||||
"github.com/samber/oops"
|
||||
"github.com/tidwall/gjson"
|
||||
"resty.dev/v3"
|
||||
@@ -39,7 +40,7 @@ type CpdailyCasLogin struct {
|
||||
|
||||
func NewCas(ctx context.Context) CpdailyLogin {
|
||||
return &CpdailyCasLogin{
|
||||
client: httpclient.Cpdaily.Clone(ctx),
|
||||
client: httpclient.NewCpdailyClient(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -157,11 +158,11 @@ func (c *CpdailyCasLogin) Login(ctx context.Context, schoolDetail *school.School
|
||||
allInputs[name] = value
|
||||
}
|
||||
} else {
|
||||
utils.OkOrPanic(oops.With("schoolDetail", schoolDetail).
|
||||
utils.OkOrPanic(errors.NewBusinessError(503, "页面中未找到登录表单"), oops.With("schoolDetail", schoolDetail).
|
||||
With("username", username).
|
||||
With("password", password).
|
||||
WithContext(ctx).
|
||||
Wrap(errors.NewBusinessError(503, "页面中未找到登录表单")))
|
||||
Wrap)
|
||||
}
|
||||
|
||||
// 修改部分参数
|
||||
@@ -192,7 +193,8 @@ func (c *CpdailyCasLogin) Login(ctx context.Context, schoolDetail *school.School
|
||||
}
|
||||
|
||||
// 声明需要滑块验证码的学校id
|
||||
var SliderCaptchaIds = []string{"1018764144934965", "sdhyxy"}
|
||||
var SliderCaptchaIds = g.Cfg().MustGet(ctx, cst.ConfigSliderSchoolIds).Strings()
|
||||
// []string{"1018764144934965", "sdhyxy", "hqdx"}
|
||||
// 判断是否需要验证码
|
||||
if isCasNeedCaptcha(ctx, loginResponseUrl, loginType, username, c.client) {
|
||||
// 如果是滑块验证码
|
||||
@@ -289,7 +291,7 @@ func (c *CpdailyCasLogin) ConfirmAdd(ctx context.Context, sc *school.SchoolDetai
|
||||
utils.OkOrPanic(err)
|
||||
|
||||
// 提交确认添加
|
||||
client := httpclient.Cpdaily.Clone(ctx)
|
||||
client := httpclient.NewCpdailyClient()
|
||||
client.SetCookieJar(cookieJar)
|
||||
post, err := client.R().SetFormData(map[string]string{
|
||||
"service": service,
|
||||
@@ -389,6 +391,10 @@ func handleCasLoginSuccess(ctx context.Context, response *resty.Response, school
|
||||
utils.OkOrPanic(errors.NewBusinessError(201, "多因子验证码认证").WithData(key))
|
||||
// 否则应该是登录成功
|
||||
} else if statusCode == 200 || statusCode == 404 {
|
||||
all, _ := io.ReadAll(verifyResponse.Body)
|
||||
if strings.Contains(string(all), "完善资料") || strings.Contains(string(all), "为了你的帐号安全") {
|
||||
utils.OkOrPanic(errors.NewBusinessError(503, "请手动学工号登录完善信息,修改密码").WithData(schoolInfo.WecloudUrl))
|
||||
}
|
||||
ActivateSession(ctx, schoolInfo.WecloudUrl, client)
|
||||
} else {
|
||||
utils.OkOrPanic(errors.NewBusinessError(503, "未知错误导致今日校园登录失败"),
|
||||
@@ -450,11 +456,17 @@ func submitCasKickOut(ctx context.Context, requestUrl string, doc *goquery.Docum
|
||||
}
|
||||
|
||||
func getCasValidLoginUrl(info *school.SchoolDetailInfo) string {
|
||||
if strings.Contains(info.AmpUrl, "campusphere") {
|
||||
// 特殊学校单独处理
|
||||
// 甘肃农业大学 西安体育学院
|
||||
ids := []string{"gsau", "ddf0639d-394d-48a2-a166-07ed84b7e6f0"}
|
||||
if slices.Contains(ids, info.Id) {
|
||||
return info.IdsUrl
|
||||
}
|
||||
if strings.Contains(info.AmpUrl, "campusphere") || strings.Contains(info.AmpUrl, "wec-portal-mobile") {
|
||||
return info.AmpUrl
|
||||
} else if strings.Contains(info.AmpUrl2, "campusphere") {
|
||||
} else if strings.Contains(info.AmpUrl2, "campusphere") || strings.Contains(info.AmpUrl2, "wec-portal-mobile") {
|
||||
return info.AmpUrl2
|
||||
} else if strings.Contains(info.AmpUrl3, "campusphere") {
|
||||
} else if strings.Contains(info.AmpUrl3, "campusphere") || strings.Contains(info.AmpUrl3, "wec-portal-mobile") {
|
||||
return info.AmpUrl3
|
||||
}
|
||||
|
||||
|
||||
@@ -4,16 +4,25 @@ import (
|
||||
"context"
|
||||
"fmt"
|
||||
"likedream-goframe/internal/application/cookiejar"
|
||||
"likedream-goframe/internal/application/cpdaily/school"
|
||||
"likedream-goframe/internal/application/httpclient"
|
||||
"likedream-goframe/internal/common/errors"
|
||||
"likedream-goframe/internal/ent/cpuser"
|
||||
"likedream-goframe/internal/framework/logger"
|
||||
"likedream-goframe/internal/utils"
|
||||
"strings"
|
||||
|
||||
"github.com/samber/oops"
|
||||
"go.uber.org/zap"
|
||||
"resty.dev/v3"
|
||||
)
|
||||
|
||||
func ActivateSession(ctx context.Context, host string, client *resty.Client) {
|
||||
// 处理cookie,干掉path
|
||||
jar := client.CookieJar()
|
||||
realJar := jar.(*cookiejar.PersistCookieJar)
|
||||
realJar.RemoveCookieInfo()
|
||||
client.SetCookieJar(realJar)
|
||||
client.SetRedirectPolicy(resty.FlexibleRedirectPolicy(20))
|
||||
|
||||
host = strings.TrimSuffix(host, "/")
|
||||
@@ -27,7 +36,7 @@ func ActivateSession(ctx context.Context, host string, client *resty.Client) {
|
||||
|
||||
defer sessionResponse.Body.Close()
|
||||
|
||||
if !sessionResponse.IsSuccess() {
|
||||
if !sessionResponse.IsSuccess() && sessionResponse.StatusCode() != 502 {
|
||||
utils.OkOrPanic(errors.NewBusinessError(503, "Session激活失败"),
|
||||
func(err error) error {
|
||||
return oops.
|
||||
@@ -81,11 +90,8 @@ func ActivateSession(ctx context.Context, host string, client *resty.Client) {
|
||||
client.SetRedirectPolicy(resty.NoRedirectPolicy())
|
||||
}
|
||||
|
||||
func CookieEnabled(ctx context.Context, host string, client *resty.Client) bool {
|
||||
client.SetRedirectPolicy(resty.FlexibleRedirectPolicy(20))
|
||||
defer func() {
|
||||
client.SetRedirectPolicy(resty.NoRedirectPolicy())
|
||||
}()
|
||||
func CookieEnabled(ctx context.Context, host string, client *resty.Client, loginType cpuser.LoginType) bool {
|
||||
client.SetRedirectPolicy(resty.NoRedirectPolicy())
|
||||
host = strings.TrimSuffix(host, "/")
|
||||
sessionResponse, err := client.R().
|
||||
WithContext(ctx).
|
||||
@@ -93,19 +99,77 @@ func CookieEnabled(ctx context.Context, host string, client *resty.Client) bool
|
||||
Post(fmt.Sprintf("%s/wec-counselor-attendance-apps/student/attendance/getStuAttendacesInOneDay", host))
|
||||
|
||||
utils.OkOrPanic(err)
|
||||
defer sessionResponse.Body.Close()
|
||||
needLogin := sessionResponse.StatusCode() == 302
|
||||
return !needLogin
|
||||
}
|
||||
|
||||
if !sessionResponse.IsSuccess() {
|
||||
return sessionResponse.StatusCode() == 405
|
||||
// RefreshCookieParams 刷新 cookie 所需的用户参数
|
||||
type RefreshCookieParams struct {
|
||||
ID int64
|
||||
Cookie string
|
||||
Tgc string
|
||||
LoginType cpuser.LoginType
|
||||
Username string
|
||||
Password string
|
||||
SchoolID string
|
||||
}
|
||||
|
||||
// RefreshCookieResult 刷新 cookie 的结果
|
||||
type RefreshCookieResult struct {
|
||||
Client *resty.Client
|
||||
Cookie string
|
||||
Tgc string
|
||||
}
|
||||
|
||||
// EnsureCookieValid 检测 cookie 是否过期,过期则自动刷新。
|
||||
// phone 类型用 TGC 刷新,account 类型用账号密码重新登录。
|
||||
func EnsureCookieValid(ctx context.Context, params *RefreshCookieParams, schoolDetail *school.SchoolDetailInfo) *RefreshCookieResult {
|
||||
client := GetClient(ctx, params.Cookie)
|
||||
if CookieEnabled(ctx, schoolDetail.WecloudUrl, client, params.LoginType) {
|
||||
logger.Logger.Info("[TGC] cookie有效,无需刷新",
|
||||
zap.Int64("userId", params.ID),
|
||||
zap.String("loginType", string(params.LoginType)),
|
||||
)
|
||||
return &RefreshCookieResult{Client: client, Cookie: params.Cookie, Tgc: params.Tgc}
|
||||
}
|
||||
|
||||
logger.Logger.Info("[TGC] cookie已失效,开始刷新",
|
||||
zap.Int64("userId", params.ID),
|
||||
zap.String("loginType", string(params.LoginType)),
|
||||
zap.Bool("tgcEmpty", params.Tgc == ""),
|
||||
)
|
||||
|
||||
var newCookie, newTgc string
|
||||
|
||||
if params.LoginType == cpuser.LoginTypePhone {
|
||||
if params.Tgc == "" {
|
||||
utils.OkOrPanic(errors.NewBusinessError(500, "TGC为空,请重新通过短信验证码登录"))
|
||||
}
|
||||
smsHandler := NewSmsWithoutDeviceID()
|
||||
newCookie, newTgc = smsHandler.RefreshByTgc(ctx, params.Tgc, params.SchoolID)
|
||||
logger.Logger.Info("[TGC] RefreshByTgc完成",
|
||||
zap.Int64("userId", params.ID),
|
||||
zap.Bool("cookieChanged", newCookie != params.Cookie),
|
||||
)
|
||||
} else {
|
||||
loginExecutor := GetCpdailyLoginBySchoolInfo(ctx, schoolDetail)
|
||||
loginExecutor.Login(ctx, schoolDetail, params.Username, params.Password)
|
||||
newCookie = loginExecutor.GetCookieString()
|
||||
newTgc = params.Tgc
|
||||
}
|
||||
|
||||
return &RefreshCookieResult{
|
||||
Client: GetClient(ctx, newCookie),
|
||||
Cookie: newCookie,
|
||||
Tgc: newTgc,
|
||||
}
|
||||
responseContentType := sessionResponse.Header().Get("Content-Type")
|
||||
return strings.Contains(responseContentType, "json")
|
||||
}
|
||||
|
||||
func GetClient(ctx context.Context, cookie string) *resty.Client {
|
||||
cookieJar := cookiejar.NewPersistCookieJar()
|
||||
cookieJar.Deserialize(cookie)
|
||||
client := httpclient.Cpdaily.Clone(ctx)
|
||||
// 不使用 Clone,创建全新的独立 client 实例避免状态共享
|
||||
client := httpclient.NewCpdailyClient()
|
||||
client.SetCookieJar(cookieJar)
|
||||
return client
|
||||
}
|
||||
|
||||
@@ -0,0 +1,180 @@
|
||||
package login
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"likedream-goframe/internal/application/cookiejar"
|
||||
"likedream-goframe/internal/application/cpdaily/cryptoutil"
|
||||
"likedream-goframe/internal/application/cpdaily/school"
|
||||
"likedream-goframe/internal/application/httpclient"
|
||||
"likedream-goframe/internal/common/errors"
|
||||
"likedream-goframe/internal/utils"
|
||||
"net/http"
|
||||
"net/url"
|
||||
|
||||
"github.com/PuerkitoBio/goquery"
|
||||
"github.com/samber/oops"
|
||||
"resty.dev/v3"
|
||||
)
|
||||
|
||||
var RSA_KEY_MAP = map[string]string{
|
||||
"public_exponent": "010001",
|
||||
"private_exponent": "413798867d69babed22e0dd3d4031c635f3e9dbca0fa50a32974a0e230787b7f7ba78caefbee828a051c690357a8cc31dba8efc738b4db22e887571ef1ec5a5a55b6d866f6a67527f6a7d78a127c9f687008bb540228b50aa2d1ca5a4ff71107234f936b611ac46432a26da9c302eaa7180820df70593353b3f8c0247fe97a45",
|
||||
"modulus": "00b5eeb166e069920e80bebd1fea4829d3d1f3216f2aabe79b6c47a3c18dcee5fd22c2e7ac519cab59198ece036dcf289ea8201e2a0b9ded307f8fb704136eaeb670286f5ad44e691005ba9ea5af04ada5367cd724b5a26fdb5120cc95b6431604bd219c6b7d83a6f8f24b43918ea988a76f93c333aa5a20991493d4eb1117e7b1",
|
||||
}
|
||||
|
||||
// 淮南联合大学登录实现 RSA加密 & 验证码必出(加法验证码)
|
||||
type CpdailyHnuuLogin struct {
|
||||
client *resty.Client
|
||||
}
|
||||
|
||||
func (c CpdailyHnuuLogin) Login(ctx context.Context, schoolDetail *school.SchoolDetailInfo, username, password string) {
|
||||
jar := cookiejar.NewPersistCookieJar()
|
||||
|
||||
c.client.SetCookieJar(jar).SetRedirectPolicy(resty.FlexibleRedirectPolicy(20))
|
||||
// 获取合法的登录URL
|
||||
loginUrl := getCasValidLoginUrl(schoolDetail)
|
||||
|
||||
// 请求登录页面并获取真正的登录URL
|
||||
loginPageResponse, err := c.client.R().
|
||||
WithContext(ctx).
|
||||
Get(loginUrl)
|
||||
|
||||
utils.OkOrPanic(err)
|
||||
|
||||
defer loginPageResponse.Body.Close()
|
||||
loginPageBytes, err := io.ReadAll(loginPageResponse.Body)
|
||||
utils.OkOrPanic(err)
|
||||
loginResponseUrl := loginPageResponse.RawResponse.Request.URL
|
||||
// 解析登录页面
|
||||
loginPage, err := goquery.NewDocumentFromReader(bytes.NewReader(loginPageBytes))
|
||||
utils.OkOrPanic(err)
|
||||
// 判断登录类型
|
||||
var (
|
||||
allInputs = map[string]string{}
|
||||
formNode = loginPage.Find(".ant-form").First()
|
||||
)
|
||||
inputs := formNode.Find("input")
|
||||
for _, input := range inputs.EachIter() {
|
||||
name, exists := input.Attr("name")
|
||||
if !exists {
|
||||
continue
|
||||
}
|
||||
value, _ := input.Attr("value")
|
||||
allInputs[name] = value
|
||||
}
|
||||
|
||||
allInputs["username"] = username
|
||||
pair := cryptoutil.NewNutRSAKeyPair(RSA_KEY_MAP["public_exponent"], "", RSA_KEY_MAP["modulus"])
|
||||
// 处理密码
|
||||
allInputs["password"] = pair.EncryptedString(password)
|
||||
|
||||
// 解决验证码
|
||||
// 解决常规图片验证码
|
||||
captchaUrl := getHnuuCaptchaUrl(loginResponseUrl)
|
||||
code, uid := ResolveCalcCaptcha(ctx, captchaUrl, c.client)
|
||||
|
||||
allInputs["code"] = code
|
||||
allInputs["id"] = uid
|
||||
jar = cookiejar.NewPersistCookieJar()
|
||||
c.client.SetCookieJar(jar)
|
||||
// 登录提交
|
||||
loginPostResponse, err := c.client.R().
|
||||
SetFormData(allInputs).
|
||||
WithContext(ctx).
|
||||
Post(getHnuuLoginPostUrl(loginResponseUrl))
|
||||
utils.OkOrPanic(err, func(err error) error {
|
||||
return oops.
|
||||
WithContext(ctx).
|
||||
With("loginResponseUrl", loginResponseUrl).
|
||||
With("allInputs", allInputs).
|
||||
Wrap(err)
|
||||
})
|
||||
defer loginPostResponse.Body.Close()
|
||||
|
||||
type LoginResponse struct {
|
||||
Meta struct {
|
||||
Success bool `json:"success"`
|
||||
StatusCode int `json:"statusCode"`
|
||||
Message string `json:"message"`
|
||||
} `json:"meta"`
|
||||
Data struct {
|
||||
Code string `json:"code"`
|
||||
} `json:"data"`
|
||||
Tgt string `json:"tgt"`
|
||||
Ticket string `json:"ticket"`
|
||||
}
|
||||
json := utils.ReadJson[LoginResponse](loginPostResponse.Body)
|
||||
if json.Tgt != "" {
|
||||
handleHnuuLoginSuccess(ctx, c.client, loginResponseUrl.Query().Get("service"), json.Ticket, json.Tgt, schoolDetail.WecloudUrl)
|
||||
} else {
|
||||
errMsg := ""
|
||||
if json.Data.Code == "CODEFALSE" {
|
||||
errMsg = "验证码错误"
|
||||
} else if json.Data.Code == "FALSE" {
|
||||
errMsg = "请检查账号密码是否正确"
|
||||
} else if json.Data.Code != "PASSERROR" {
|
||||
errMsg = "密码错误"
|
||||
} else if json.Data.Code != "NOUSER" {
|
||||
errMsg = "账号不存在"
|
||||
} else if json.Data.Code != "USERLOCK" {
|
||||
errMsg = "账号锁定"
|
||||
} else {
|
||||
errMsg = "未知错误" + json.Data.Code
|
||||
}
|
||||
utils.OkOrPanic(errors.NewBusinessError(500, errMsg))
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func handleHnuuLoginSuccess(ctx context.Context, client *resty.Client, url, ticket string, tgt string, host string) {
|
||||
redirectUrl := fmt.Sprintf("%s&ticket=%s", url, ticket)
|
||||
client.R().SetHeader("Referer", "https://cas.hnuu.edu.cn/").Get(redirectUrl)
|
||||
response, err2 := client.R().SetHeader("Referer", "https://cas.hnuu.edu.cn/").Get("https://hnuu.campusphere.net/iap/login")
|
||||
utils.OkOrPanic(err2)
|
||||
newUrl := response.RawResponse.Request.URL
|
||||
ticketUrl := fmt.Sprintf("https://cas.hnuu.edu.cn/lyuapServer/v1/tickets/%s", tgt)
|
||||
get, err := client.R().SetFormData(map[string]string{
|
||||
"service": url,
|
||||
"loginToken": "loginToken",
|
||||
}).Post(ticketUrl)
|
||||
utils.OkOrPanic(err)
|
||||
defer get.Body.Close()
|
||||
ticket = utils.ReadString(get.Body)
|
||||
redirectUrl = fmt.Sprintf("%s&ticket=%s", newUrl.Query().Get("service"), ticket)
|
||||
client.R().SetHeader("Referer", "https://cas.hnuu.edu.cn/").Get(redirectUrl)
|
||||
ActivateSession(ctx, host, client)
|
||||
}
|
||||
|
||||
func getHnuuLoginPostUrl(url *url.URL) string {
|
||||
// https://cas.hnuu.edu.cn/lyuapServer/v1/tickets
|
||||
return fmt.Sprintf("%s://%s/lyuapServer/v1/tickets", url.Scheme, url.Host)
|
||||
}
|
||||
|
||||
func getHnuuCaptchaUrl(url *url.URL) string {
|
||||
// https://cas.hnuu.edu.cn/lyuapServer/kaptcha?_t=1766803822&uid=
|
||||
return fmt.Sprintf("%s://%s/lyuapServer/kaptcha?_t=%d&uid=", url.Scheme, url.Host, utils.GetCurrentTimeMillis())
|
||||
}
|
||||
|
||||
func (c CpdailyHnuuLogin) GetCookieJar() http.CookieJar {
|
||||
return c.client.CookieJar()
|
||||
}
|
||||
|
||||
func (c CpdailyHnuuLogin) GetCookieString() string {
|
||||
cookieJar := c.GetCookieJar()
|
||||
serializableJar := cookieJar.(*cookiejar.PersistCookieJar)
|
||||
return serializableJar.Serialize()
|
||||
}
|
||||
|
||||
func (c CpdailyHnuuLogin) GetClient() *resty.Client {
|
||||
return c.client
|
||||
}
|
||||
|
||||
func NewHnuu(ctx context.Context) CpdailyLogin {
|
||||
return &CpdailyHnuuLogin{
|
||||
client: httpclient.NewCpdailyClient(),
|
||||
}
|
||||
}
|
||||
@@ -27,7 +27,7 @@ type CpdailyIapLogin struct {
|
||||
|
||||
func NewIap(ctx context.Context) *CpdailyIapLogin {
|
||||
return &CpdailyIapLogin{
|
||||
client: httpclient.Cpdaily.Clone(ctx),
|
||||
client: httpclient.NewCpdailyClient(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -45,16 +45,17 @@ func (c *CpdailyIapLogin) Login(ctx context.Context, schoolDetail *school.School
|
||||
preLoginUrl := loginUrl + "/portal/login"
|
||||
c.client.SetRedirectPolicy(resty.FlexibleRedirectPolicy(20))
|
||||
loginRedirectResponse, err := c.client.R().Get(preLoginUrl)
|
||||
utils.OkOrPanic(err, func(err error) error {
|
||||
return oops.
|
||||
In("CpdailyIapLogin.Login").
|
||||
With("schoolDetail", schoolDetail).
|
||||
With("username", username).
|
||||
With("password", password).
|
||||
Wrap(err)
|
||||
})
|
||||
u := loginRedirectResponse.RawResponse.Request.URL
|
||||
oldLt := strings.Split(u.String(), "=")[1]
|
||||
oldLt := ""
|
||||
// 部分学校无法直接访问登录页
|
||||
if err == nil {
|
||||
u := loginRedirectResponse.RawResponse.Request.URL
|
||||
oldLt = strings.Split(u.String(), "=")[1]
|
||||
c.client.SetRedirectPolicy(resty.NoRedirectPolicy())
|
||||
} else {
|
||||
preLoginUrl = loginUrl + "/iap/login/mobile.html"
|
||||
c.client.SetRedirectPolicy(resty.FlexibleRedirectPolicy(20))
|
||||
loginRedirectResponse, err = c.client.R().Get(preLoginUrl)
|
||||
}
|
||||
c.client.SetRedirectPolicy(resty.NoRedirectPolicy())
|
||||
ltInfoResponse, err := c.client.R().
|
||||
WithContext(ctx).
|
||||
@@ -172,12 +173,11 @@ func (c *CpdailyIapLogin) Login(ctx context.Context, schoolDetail *school.School
|
||||
In("Cpdaily.Login").New("Iap登录失败, 用户名或密码错误")
|
||||
})
|
||||
default:
|
||||
utils.OkOrPanic(errors.NewBusinessError(503, fmt.Sprintf("Iap登录失败, 状态识别码: %s", resultCode)), func(err error) error {
|
||||
return oops.With("username", username).
|
||||
utils.OkOrPanic(errors.NewBusinessError(503, fmt.Sprintf("Iap登录失败, 状态识别码: %s", resultCode)),
|
||||
oops.With("username", username).
|
||||
With("password", password).
|
||||
With("userId", utils.GetUserIdNotPanic(ctx)).
|
||||
In("Cpdaily.Login").New("Iap登录失败, 状态识别码: %s" + resultCode)
|
||||
})
|
||||
In("Cpdaily.Login").Wrap)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,149 @@
|
||||
package login
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"likedream-goframe/internal/application/cookiejar"
|
||||
"likedream-goframe/internal/application/cpdaily/cryptoutil"
|
||||
"likedream-goframe/internal/application/cpdaily/school"
|
||||
"likedream-goframe/internal/application/httpclient"
|
||||
"likedream-goframe/internal/common/errors"
|
||||
"likedream-goframe/internal/utils"
|
||||
"net/http"
|
||||
"net/url"
|
||||
|
||||
"github.com/PuerkitoBio/goquery"
|
||||
"github.com/samber/oops"
|
||||
"resty.dev/v3"
|
||||
)
|
||||
|
||||
// 南宁师范大学登录实现
|
||||
type CpdailyNnnuLogin struct {
|
||||
client *resty.Client
|
||||
}
|
||||
|
||||
func (c CpdailyNnnuLogin) Login(ctx context.Context, schoolDetail *school.SchoolDetailInfo, username, password string) {
|
||||
jar := cookiejar.NewPersistCookieJar()
|
||||
|
||||
c.client.SetCookieJar(jar).SetRedirectPolicy(resty.FlexibleRedirectPolicy(20))
|
||||
|
||||
// 获取合法的登录URL
|
||||
loginUrl := getCasValidLoginUrl(schoolDetail)
|
||||
|
||||
// 请求登录页面并获取真正的登录URL
|
||||
loginPageResponse, err := c.client.R().
|
||||
WithContext(ctx).
|
||||
Get(loginUrl)
|
||||
|
||||
defer loginPageResponse.Body.Close()
|
||||
loginPageBytes, err := io.ReadAll(loginPageResponse.Body)
|
||||
utils.OkOrPanic(err)
|
||||
loginResponseUrl := loginPageResponse.RawResponse.Request.URL
|
||||
// 关闭client的重定向
|
||||
c.client.SetRedirectPolicy(resty.NoRedirectPolicy())
|
||||
// 解析登录页面
|
||||
loginPage, err := goquery.NewDocumentFromReader(bytes.NewReader(loginPageBytes))
|
||||
utils.OkOrPanic(err)
|
||||
// 判断登录类型
|
||||
var (
|
||||
allInputs = map[string]string{}
|
||||
form = loginPage.Find("form").First()
|
||||
)
|
||||
inputs := form.Find("input")
|
||||
|
||||
for _, input := range inputs.EachIter() {
|
||||
name, exists := input.Attr("name")
|
||||
if !exists {
|
||||
continue
|
||||
}
|
||||
value, _ := input.Attr("value")
|
||||
allInputs[name] = value
|
||||
}
|
||||
|
||||
allInputs["username"] = username
|
||||
|
||||
// 处理密码
|
||||
allInputs["password"] = cryptoutil.MD5(password)
|
||||
|
||||
// 解决常规图片验证码
|
||||
captchaUrl := getAhjzuCaptchaUrl(loginResponseUrl)
|
||||
captchaResult := ResolveOcrCaptcha(ctx, captchaUrl, c.client)
|
||||
|
||||
allInputs["validateCode"] = captchaResult
|
||||
|
||||
// 登录提交
|
||||
loginPostResponse, err := c.client.R().
|
||||
SetFormData(allInputs).
|
||||
WithContext(ctx).
|
||||
Post(loginResponseUrl.String())
|
||||
|
||||
utils.OkOrPanic(err, func(err error) error {
|
||||
return oops.
|
||||
WithContext(ctx).
|
||||
With("loginResponseUrl", loginResponseUrl).
|
||||
With("allInputs", allInputs).
|
||||
Wrap(err)
|
||||
})
|
||||
|
||||
defer loginPostResponse.Body.Close()
|
||||
|
||||
switch loginPostResponse.StatusCode() {
|
||||
case 302:
|
||||
handleCasLoginSuccess(ctx, loginPostResponse, schoolDetail, username, password, c.client)
|
||||
case 200:
|
||||
fallthrough
|
||||
case 401:
|
||||
doc, err := goquery.NewDocumentFromReader(loginPostResponse.Body)
|
||||
utils.OkOrPanic(err, func(err error) error {
|
||||
return oops.
|
||||
WithContext(ctx).
|
||||
With("loginResponseUrl", loginResponseUrl).
|
||||
With("allInputs", allInputs).
|
||||
Wrap(err)
|
||||
})
|
||||
|
||||
if isCasNeedKickOut(doc) {
|
||||
kickoutResponse := submitCasKickOut(ctx, loginResponseUrl.String(), doc, c.client)
|
||||
handleCasLoginSuccess(ctx, kickoutResponse, schoolDetail, username, password, c.client)
|
||||
return
|
||||
}
|
||||
|
||||
message := getCasErrorMessage(doc)
|
||||
|
||||
utils.OkOrPanic(errors.NewBusinessError(503, fmt.Sprintf("今日校园登录失败: %s", message)), func(err error) error {
|
||||
return oops.
|
||||
WithContext(ctx).
|
||||
With("loginResponseUrl", loginResponseUrl).
|
||||
With("allInputs", allInputs).
|
||||
With("errorMessage", message).
|
||||
Wrap(errors.NewBusinessError(503, message))
|
||||
})
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func getNuuuCaptchaUrl(url *url.URL) string {
|
||||
return fmt.Sprintf("%s://%s/sso/code/code.jsp", url.Scheme, url.Host)
|
||||
}
|
||||
|
||||
func (c CpdailyNnnuLogin) GetCookieJar() http.CookieJar {
|
||||
return c.client.CookieJar()
|
||||
}
|
||||
|
||||
func (c CpdailyNnnuLogin) GetCookieString() string {
|
||||
cookieJar := c.GetCookieJar()
|
||||
serializableJar := cookieJar.(*cookiejar.PersistCookieJar)
|
||||
return serializableJar.Serialize()
|
||||
}
|
||||
|
||||
func (c CpdailyNnnuLogin) GetClient() *resty.Client {
|
||||
return c.client
|
||||
}
|
||||
|
||||
func NewNnnu(ctx context.Context) CpdailyLogin {
|
||||
return &CpdailyNnnuLogin{
|
||||
client: httpclient.NewCpdailyClient(),
|
||||
}
|
||||
}
|
||||
@@ -81,10 +81,8 @@ func (c CpdailyNutLogin) Login(ctx context.Context, schoolDetail *school.SchoolD
|
||||
pubKeyBytes, err := io.ReadAll(pubKeyResponse.Body)
|
||||
utils.OkOrPanic(err)
|
||||
sonic.Unmarshal(pubKeyBytes, &pubKey)
|
||||
pair, err := cryptoutil.NewRSAKeyPair(pubKey.Exponent, "", pubKey.Modulus)
|
||||
utils.OkOrPanic(err)
|
||||
password, err = pair.Encrypt(reverseStringASCII(password))
|
||||
utils.OkOrPanic(err)
|
||||
rsa := cryptoutil.NewNutRSAKeyPair(pubKey.Exponent, "", pubKey.Modulus)
|
||||
password = rsa.EncryptedString(reverseStringASCII(password))
|
||||
allInputs["password"] = password
|
||||
if isNutNeedCaptcha(ctx, loginResponseUrl, c.client) {
|
||||
// 解决常规图片验证码
|
||||
@@ -201,7 +199,7 @@ func (c CpdailyNutLogin) GetClient() *resty.Client {
|
||||
|
||||
func NewNut(ctx context.Context) *CpdailyNutLogin {
|
||||
return &CpdailyNutLogin{
|
||||
client: httpclient.Cpdaily.Clone(ctx),
|
||||
client: httpclient.NewCpdailyClient(),
|
||||
}
|
||||
}
|
||||
func reverseStringASCII(s string) string {
|
||||
|
||||
@@ -1,11 +1,9 @@
|
||||
package login
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"image"
|
||||
"io"
|
||||
"likedream-goframe/internal/application/recognizer/ddddocr"
|
||||
"likedream-goframe/internal/application/captcha"
|
||||
"likedream-goframe/internal/utils"
|
||||
|
||||
"github.com/samber/oops"
|
||||
@@ -14,15 +12,33 @@ import (
|
||||
|
||||
func ResolveOcrCaptcha(ctx context.Context, url string, client *resty.Client) string {
|
||||
byteData := DownCode2Byte(ctx, url, client)
|
||||
img, err := BytesToImage(byteData)
|
||||
return captcha.ApiAutoOCRVerification(byteData)
|
||||
}
|
||||
|
||||
func ResolveCalcCaptcha(ctx context.Context, url string, client *resty.Client) (string, string) {
|
||||
b64, uid := DownBase64(ctx, url, client)
|
||||
return captcha.ApiAutoCalcVerification(b64), uid
|
||||
}
|
||||
|
||||
func DownBase64(ctx context.Context, url string, client *resty.Client) (string, string) {
|
||||
res, err := client.R().
|
||||
WithContext(ctx).
|
||||
Get(url)
|
||||
utils.OkOrPanic(err, func(err error) error {
|
||||
return oops.
|
||||
With("BytesToImage", url).
|
||||
With("captchaUrl", url).
|
||||
WithContext(ctx).
|
||||
Wrap(err)
|
||||
})
|
||||
data := ddddocr.AutoOCRVerification(img)
|
||||
return data
|
||||
defer res.Body.Close()
|
||||
type CaptchResponse struct {
|
||||
KaptchaType string `json:"kaptchaType"`
|
||||
UID string `json:"uid"`
|
||||
Content string `json:"content"`
|
||||
Timeout int `json:"timeout"`
|
||||
}
|
||||
json := utils.ReadJson[CaptchResponse](res.Body)
|
||||
return json.Content, json.UID
|
||||
}
|
||||
|
||||
func DownCode2Byte(ctx context.Context, url string, client *resty.Client) []byte {
|
||||
@@ -45,12 +61,3 @@ func DownCode2Byte(ctx context.Context, url string, client *resty.Client) []byte
|
||||
})
|
||||
return all
|
||||
}
|
||||
|
||||
// 通用解码函数,自动检测格式
|
||||
func BytesToImage(imgBytes []byte) (image.Image, error) {
|
||||
img, _, err := image.Decode(bytes.NewReader(imgBytes))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return img, nil
|
||||
}
|
||||
|
||||
@@ -2,14 +2,17 @@ package login
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"likedream-goframe/internal/application/recognizer/slider"
|
||||
"likedream-goframe/internal/application/httpclient"
|
||||
"likedream-goframe/internal/common/cst"
|
||||
"likedream-goframe/internal/common/errors"
|
||||
"likedream-goframe/internal/utils"
|
||||
|
||||
"github.com/bytedance/sonic"
|
||||
"github.com/gogf/gf/v2/frame/g"
|
||||
"github.com/gogf/gf/v2/os/gctx"
|
||||
"github.com/samber/oops"
|
||||
"github.com/tidwall/gjson"
|
||||
"resty.dev/v3"
|
||||
)
|
||||
|
||||
@@ -39,12 +42,13 @@ func ResolveCasSliderCaptcha(ctx context.Context, captchaUrl string, verifyUrl s
|
||||
bytes, _ := io.ReadAll(res.Body)
|
||||
var captcha CasSliderCaptchaReponse
|
||||
sonic.Unmarshal(bytes, &captcha)
|
||||
width := slider.VerifySliderWidthByBase64(captcha.BigImage, captcha.SmallImage)
|
||||
//width := slider.VerifySliderWidthByBase64(captcha.BigImage, captcha.SmallImage)
|
||||
width := ApiVerifySliderWidthByBase64(captcha.BigImage, captcha.SmallImage)
|
||||
res2, err2 := client.R().
|
||||
WithContext(ctx).
|
||||
SetFormData(map[string]string{
|
||||
"canvasLength": "280",
|
||||
"moveLength": fmt.Sprintf("%d", width),
|
||||
"moveLength": width,
|
||||
}).
|
||||
Post(verifyUrl)
|
||||
utils.OkOrPanic(err2, func(err error) error {
|
||||
@@ -65,3 +69,22 @@ func ResolveCasSliderCaptcha(ctx context.Context, captchaUrl string, verifyUrl s
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func ApiVerifySliderWidthByBase64(bigImage string, smallImage string) string {
|
||||
ctx := gctx.GetInitCtx()
|
||||
baseUrl := g.Config().MustGetWithEnv(ctx, cst.ConfigCaptchaBaseUrl).String() + "/slider"
|
||||
client := httpclient.Cpdaily.Clone(ctx)
|
||||
client.SetDoNotParseResponse(false)
|
||||
post, err := client.R().SetBody(map[string]any{
|
||||
"big_image": bigImage,
|
||||
"small_image": smallImage,
|
||||
}).Post(baseUrl)
|
||||
utils.OkOrPanic(err, func(err error) error {
|
||||
return oops.
|
||||
With("AutoOCRVerification", baseUrl).
|
||||
WithContext(ctx).
|
||||
Wrap(err)
|
||||
})
|
||||
result := post.String()
|
||||
return gjson.Get(result, "data").String()
|
||||
}
|
||||
|
||||
@@ -0,0 +1,374 @@
|
||||
package login
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
"likedream-goframe/internal/application/cookiejar"
|
||||
"likedream-goframe/internal/application/cpdaily/cryptoutil"
|
||||
"likedream-goframe/internal/application/cpdaily/school"
|
||||
"likedream-goframe/internal/application/httpclient"
|
||||
"likedream-goframe/internal/common/cst"
|
||||
"likedream-goframe/internal/common/errors"
|
||||
"likedream-goframe/internal/framework/logger"
|
||||
"likedream-goframe/internal/utils"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
|
||||
"github.com/bytedance/sonic"
|
||||
"github.com/samber/oops"
|
||||
"go.uber.org/zap"
|
||||
"resty.dev/v3"
|
||||
)
|
||||
|
||||
type cpdailySmsCommonResponse[T any] struct {
|
||||
ErrCode int `json:"errCode"`
|
||||
ErrMsg string `json:"errMsg"`
|
||||
Data T `json:"data"`
|
||||
}
|
||||
|
||||
func (c *cpdailySmsCommonResponse[T]) EnsureSuccess() {
|
||||
if c.ErrCode != 0 {
|
||||
utils.OkOrPanic(errors.NewBusinessError(503, c.ErrMsg))
|
||||
}
|
||||
}
|
||||
|
||||
type CpdailySmsSendSmsData struct {
|
||||
Countdown int `json:"countdown"`
|
||||
TipMsg string `json:"tipMsg"`
|
||||
Status int `json:"status"`
|
||||
CookieJar *cookiejar.PersistCookieJar `json:"-"`
|
||||
}
|
||||
|
||||
type CpdailySmsLoginByCodeData struct {
|
||||
AuthID string `json:"authId"`
|
||||
AuthStatus string `json:"authStatus"`
|
||||
CompleteUserInfoStatus string `json:"completeUserInfoStatus"`
|
||||
DeviceExceptionMsg string `json:"deviceExceptionMsg"`
|
||||
DeviceStatus string `json:"deviceStatus"`
|
||||
FirstLogin bool `json:"firstLogin"`
|
||||
Mobile string `json:"mobile"`
|
||||
MsgTip string `json:"msgTip"`
|
||||
Name string `json:"name"`
|
||||
OpenID string `json:"openId"`
|
||||
PersonID string `json:"personId"`
|
||||
SessionToken string `json:"sessionToken"`
|
||||
Status string `json:"status"`
|
||||
TenantID string `json:"tenantId"`
|
||||
TenantName string `json:"tenantName"`
|
||||
Tgc string `json:"tgc"`
|
||||
TgcStatus string `json:"tgcStatus"`
|
||||
UserID string `json:"userId"`
|
||||
UserType string `json:"userType"`
|
||||
}
|
||||
|
||||
func NewSms(deviceID string) *SmsLoginHandler {
|
||||
return &SmsLoginHandler{
|
||||
deviceID: deviceID,
|
||||
}
|
||||
}
|
||||
|
||||
func NewSmsWithoutDeviceID() *SmsLoginHandler {
|
||||
return &SmsLoginHandler{
|
||||
deviceID: "",
|
||||
}
|
||||
}
|
||||
|
||||
type SmsLoginHandler struct {
|
||||
deviceID string
|
||||
client *resty.Client
|
||||
}
|
||||
|
||||
func (s *SmsLoginHandler) SendSms(ctx context.Context, phone string) *CpdailySmsSendSmsData {
|
||||
jar := cookiejar.NewPersistCookieJar()
|
||||
|
||||
phoneEncrypted := base64.StdEncoding.EncodeToString(cryptoutil.AesEncrypt([]byte(phone)))
|
||||
s.client = httpclient.NewCpdailyClient()
|
||||
req := s.client.
|
||||
SetCookieJar(jar).
|
||||
R().
|
||||
SetContext(utils.NewCpdailySmsContextBuilder().
|
||||
WithDeviceID(s.deviceID).
|
||||
Build(ctx)).
|
||||
SetBody(map[string]string{
|
||||
"a": phoneEncrypted,
|
||||
"b": cryptoutil.EncryptVersion,
|
||||
})
|
||||
|
||||
setSmsRequestHeaders(ctx, req)
|
||||
|
||||
resp, e := req.
|
||||
Post("https://mobile.campushoy.com/app/auth/authentication/mobile/messageCode/v-8222")
|
||||
utils.OkOrPanic(e, oops.WithContext(ctx).Wrap)
|
||||
|
||||
respJson := utils.ReadJson[cpdailySmsCommonResponse[CpdailySmsSendSmsData]](resp.Body)
|
||||
respJson.EnsureSuccess()
|
||||
|
||||
respJson.Data.CookieJar = jar
|
||||
|
||||
return &respJson.Data
|
||||
}
|
||||
|
||||
func (s *SmsLoginHandler) LoginByCode(ctx context.Context, phone, code string, jar http.CookieJar) *CpdailySmsLoginByCodeData {
|
||||
payload := map[string]any{
|
||||
"d": code,
|
||||
"e": "CPDAILY",
|
||||
"c": phone,
|
||||
"f": "86",
|
||||
}
|
||||
|
||||
payloadString, e := sonic.Marshal(payload)
|
||||
utils.OkOrPanic(e, oops.WithContext(ctx).Wrap)
|
||||
|
||||
encryptedPayload := cryptoutil.AesEncrypt(payloadString)
|
||||
encodedPayload := base64.StdEncoding.EncodeToString(encryptedPayload)
|
||||
|
||||
payload = map[string]any{
|
||||
"a": encodedPayload,
|
||||
"b": cryptoutil.EncryptVersion,
|
||||
}
|
||||
s.client = httpclient.NewCpdailyClient()
|
||||
s.client.SetCookieJar(jar)
|
||||
req := s.client.R().
|
||||
SetBody(payload).
|
||||
SetContext(utils.NewCpdailySmsContextBuilder().
|
||||
WithDeviceID(s.deviceID).
|
||||
Build(ctx))
|
||||
setSmsRequestHeaders(ctx, req)
|
||||
|
||||
resp, e := req.
|
||||
Post("https://mobile.campushoy.com/app/auth/authentication/mobileLogin/v-8222")
|
||||
utils.OkOrPanic(e, oops.WithContext(ctx).Wrap)
|
||||
|
||||
respJson := utils.ReadJson[cpdailySmsCommonResponse[string]](resp.Body)
|
||||
respJson.EnsureSuccess()
|
||||
|
||||
data := decodeData[CpdailySmsLoginByCodeData](respJson.Data)
|
||||
|
||||
return &data
|
||||
}
|
||||
|
||||
func (s *SmsLoginHandler) SetLoginCookie(ctx context.Context, jar http.CookieJar, loginData *CpdailySmsLoginByCodeData) {
|
||||
detail := school.GetSchoolDetail(ctx, loginData.TenantID)
|
||||
|
||||
getCookieUrl := fmt.Sprintf("%s%s", detail.WecloudUrl, "/wec-im-group/group/imUserToken/getImUserToken")
|
||||
|
||||
cookieMap := map[string]any{
|
||||
"AMP2": []map[string]any{
|
||||
{
|
||||
"path": "/",
|
||||
"value": loginData.SessionToken,
|
||||
"domain": "",
|
||||
"name": "sessionToken",
|
||||
},
|
||||
},
|
||||
"AMP1": []map[string]any{
|
||||
{
|
||||
"path": "/",
|
||||
"value": loginData.SessionToken,
|
||||
"domain": "",
|
||||
"name": "sessionToken",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
ampTokenString, e := sonic.Marshal(cookieMap)
|
||||
utils.OkOrPanic(e, oops.WithContext(ctx).Wrap)
|
||||
|
||||
ampTokenEncrypted := cryptoutil.DesEncrypt(ampTokenString, cryptoutil.Normal)
|
||||
ampToken := base64.StdEncoding.EncodeToString(ampTokenEncrypted)
|
||||
|
||||
// 设置cookie到jar: clientType=cpdaily_student; tenantId=ahnu; standAlone=0; sessionToken=5e55bfa7-d17e-4f5a-a06d-99a649855521;
|
||||
targetSchoolUrl, e := url.Parse(detail.WecloudUrl)
|
||||
utils.OkOrPanic(e, oops.WithContext(ctx).Wrap)
|
||||
targetApiUrl, e := url.Parse("https://api.campushoy.com")
|
||||
utils.OkOrPanic(e, oops.WithContext(ctx).Wrap)
|
||||
|
||||
cookieNameValues := map[string]string{
|
||||
"clientType": "cpdaily_student",
|
||||
"tenantId": detail.Id,
|
||||
"standAlone": "0",
|
||||
"sessionToken": loginData.SessionToken,
|
||||
}
|
||||
|
||||
for name, value := range cookieNameValues {
|
||||
cookie := &http.Cookie{
|
||||
Name: name,
|
||||
Value: value,
|
||||
}
|
||||
|
||||
jar.SetCookies(targetSchoolUrl, []*http.Cookie{cookie})
|
||||
jar.SetCookies(targetApiUrl, []*http.Cookie{cookie})
|
||||
}
|
||||
|
||||
s.client.SetRedirectPolicy(resty.FlexibleRedirectPolicy(50))
|
||||
req := s.client.R().
|
||||
SetHeader("AmpCookies", ampToken).
|
||||
SetContext(ctx)
|
||||
|
||||
ctx = utils.NewCpdailySmsContextBuilder().
|
||||
WithDeviceID(s.deviceID).
|
||||
WithTenantID(detail.Id).
|
||||
WithSessionToken(loginData.SessionToken).
|
||||
Build(ctx)
|
||||
|
||||
setSmsRequestHeaders(ctx, req)
|
||||
|
||||
resp, e := req.
|
||||
Get(getCookieUrl)
|
||||
utils.OkOrPanic(e, oops.WithContext(ctx).Wrap)
|
||||
|
||||
respJson := utils.ReadJson[cpdailySmsCommonResponse[any]](resp.Body)
|
||||
respJson.EnsureSuccess()
|
||||
}
|
||||
|
||||
func decodeData[T any](encodedData string) T {
|
||||
decodedBytes, e := base64.StdEncoding.DecodeString(encodedData)
|
||||
utils.OkOrPanic(e)
|
||||
|
||||
decryptedBytes := cryptoutil.AesDecrypt(decodedBytes)
|
||||
|
||||
result := utils.ReadJson[T](bytes.NewReader(decryptedBytes))
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
func setSmsRequestHeaders(ctx context.Context, request *resty.Request) {
|
||||
sessionToken, ok := ctx.Value(cst.CtxCpdailySessionToken).(string)
|
||||
if ok {
|
||||
encryptedSessionToken := cryptoutil.DesEncrypt([]byte(sessionToken), cryptoutil.Normal)
|
||||
encodedSessionToken := base64.StdEncoding.EncodeToString(encryptedSessionToken)
|
||||
request.SetHeader("SessionToken", encodedSessionToken)
|
||||
request.SetHeader("sessionTokenKey", encodedSessionToken)
|
||||
} else {
|
||||
request.SetHeader("SessionToken", sessionToken)
|
||||
request.SetHeader("sessionTokenKey", sessionToken)
|
||||
}
|
||||
|
||||
deviceId, _ := ctx.Value(cst.CtxCpdailyDeviceID).(string)
|
||||
|
||||
cpdailyInfoDetail := map[string]any{
|
||||
"deviceId": deviceId,
|
||||
"systemName": "android",
|
||||
"userId": "",
|
||||
"appVersion": "9.8.8",
|
||||
"model": "Unknown",
|
||||
"lon": "0",
|
||||
"lat": "0",
|
||||
"cpdailyVersion": "9.8.8",
|
||||
"systemVersion": "11",
|
||||
}
|
||||
|
||||
cpdailyInfoStr, e := sonic.Marshal(cpdailyInfoDetail)
|
||||
utils.OkOrPanic(e, oops.WithContext(ctx).Wrap)
|
||||
|
||||
encryptedCpdailyInfo := cryptoutil.DesEncrypt(cpdailyInfoStr, cryptoutil.Normal)
|
||||
encodedCpdailyInfo := base64.StdEncoding.EncodeToString(encryptedCpdailyInfo)
|
||||
|
||||
request.SetHeaders(map[string]string{
|
||||
"clientType": "cpdaily_student",
|
||||
"deviceType": "1",
|
||||
"CpdailyClientType": "CPDAILY",
|
||||
"CpdailyStandAlone": "0",
|
||||
"User-Agent": "Mozilla/5.0 (Linux; Android 11; Redmi Note 8 Pro Build/RP1A.230805.003; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/83.0.4103.106 Mobile Safari/537.36 cpdaily/9.8.8 wisedu/9.8.8",
|
||||
"CpdailyInfo": encodedCpdailyInfo,
|
||||
})
|
||||
}
|
||||
|
||||
func (s *SmsLoginHandler) GetCookieString() string {
|
||||
cookieJar := s.client.CookieJar()
|
||||
serializableJar := cookieJar.(*cookiejar.PersistCookieJar)
|
||||
return serializableJar.Serialize()
|
||||
}
|
||||
|
||||
// RefreshByTgc 使用保存的完整 cookie(包含 CASTGC 等长期凭证)重新去学校换取 CAS cookie。
|
||||
// tgc 参数是 SetLoginCookie 之后序列化的完整 cookie jar,包含 CASTGC(有效期10年)、
|
||||
// sessionToken 等认证信息。流程:先用 sessionToken 调用 getImUserToken 换取新的 CAS cookie,
|
||||
// 再激活 session。
|
||||
func (s *SmsLoginHandler) RefreshByTgc(ctx context.Context, tgc string, schoolId string) (cookie string, newTgc string) {
|
||||
jar := cookiejar.NewPersistCookieJar()
|
||||
jar.Deserialize(tgc)
|
||||
// 合并所有 host 的 cookie 到 global,消除因多 host 存储导致的重复 cookie
|
||||
jar.RemoveCookieInfo()
|
||||
s.client = httpclient.NewCpdailyClient()
|
||||
s.client.SetCookieJar(jar)
|
||||
|
||||
detail := school.GetSchoolDetail(ctx, schoolId)
|
||||
|
||||
logger.Logger.Info("[TGC] RefreshByTgc 开始用TGC换取CAS cookie",
|
||||
zap.String("schoolId", schoolId),
|
||||
zap.String("wecloudUrl", detail.WecloudUrl),
|
||||
zap.Int("tgcLen", len(tgc)),
|
||||
)
|
||||
|
||||
// 从 cookie jar 中提取 sessionToken,用于换取新的 CAS cookie
|
||||
schoolUrl, _ := url.Parse(detail.WecloudUrl)
|
||||
sessionToken := ""
|
||||
for _, c := range jar.Cookies(schoolUrl) {
|
||||
if c.Name == "sessionToken" {
|
||||
sessionToken = c.Value
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if sessionToken != "" {
|
||||
// 换取新的 CAS cookie(与 SetLoginCookie 相同的流程)
|
||||
getCookieUrl := fmt.Sprintf("%s%s", detail.WecloudUrl, "/wec-im-group/group/imUserToken/getImUserToken")
|
||||
|
||||
ampCookieMap := map[string]any{
|
||||
"AMP2": []map[string]any{
|
||||
{"path": "/", "value": sessionToken, "domain": "", "name": "sessionToken"},
|
||||
},
|
||||
"AMP1": []map[string]any{
|
||||
{"path": "/", "value": sessionToken, "domain": "", "name": "sessionToken"},
|
||||
},
|
||||
}
|
||||
|
||||
ampTokenString, e := sonic.Marshal(ampCookieMap)
|
||||
utils.OkOrPanic(e, oops.WithContext(ctx).Wrap)
|
||||
|
||||
ampTokenEncrypted := cryptoutil.DesEncrypt(ampTokenString, cryptoutil.Normal)
|
||||
ampToken := base64.StdEncoding.EncodeToString(ampTokenEncrypted)
|
||||
|
||||
ctx = utils.NewCpdailySmsContextBuilder().
|
||||
WithDeviceID(s.deviceID).
|
||||
WithTenantID(detail.Id).
|
||||
WithSessionToken(sessionToken).
|
||||
Build(ctx)
|
||||
|
||||
// 使用不复制 Cookie 头的重定向策略,避免旧 Cookie 覆盖 jar 中更新的值
|
||||
s.client.SetRedirectPolicy(resty.RedirectPolicyFunc(func(req *http.Request, via []*http.Request) error {
|
||||
if len(via) >= 50 {
|
||||
return fmt.Errorf("resty: stopped after 50 redirects")
|
||||
}
|
||||
// 只复制非 Cookie 头,让 jar 自动管理 Cookie
|
||||
for key, val := range via[0].Header {
|
||||
if !strings.EqualFold(key, "Cookie") {
|
||||
req.Header[key] = val
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}))
|
||||
req := s.client.R().
|
||||
SetHeader("AmpCookies", ampToken).
|
||||
SetContext(ctx)
|
||||
|
||||
setSmsRequestHeaders(ctx, req)
|
||||
|
||||
resp, e := req.Get(getCookieUrl)
|
||||
utils.OkOrPanic(e, oops.WithContext(ctx).Wrap)
|
||||
|
||||
respJson := utils.ReadJson[cpdailySmsCommonResponse[any]](resp.Body)
|
||||
respJson.EnsureSuccess()
|
||||
|
||||
logger.Logger.Info("[TGC] RefreshByTgc 换取CAS cookie完成")
|
||||
} else {
|
||||
logger.Logger.Warn("[TGC] RefreshByTgc 未找到sessionToken,跳过换取CAS cookie步骤")
|
||||
}
|
||||
|
||||
newTgc = tgc // TGC 保持不变(CASTGC 有效期10年)
|
||||
cookie = s.GetCookieString()
|
||||
return
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
package login
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"likedream-goframe/internal/application/cookiejar"
|
||||
"testing"
|
||||
|
||||
"github.com/gogf/gf/v2/os/gctx"
|
||||
)
|
||||
|
||||
const (
|
||||
testSmsPhoneNumber = ""
|
||||
testSmsCode = ""
|
||||
)
|
||||
|
||||
func TestSmsLoginHandler_SendSms(t *testing.T) {
|
||||
handler := SmsLoginHandler{}
|
||||
data := handler.SendSms(context.Background(), testSmsPhoneNumber)
|
||||
|
||||
t.Logf("SendSms Data: %+v", data)
|
||||
}
|
||||
|
||||
func TestSmsLoginHandler_LoginByCode(t *testing.T) {
|
||||
handler := SmsLoginHandler{}
|
||||
jar := cookiejar.NewPersistCookieJar()
|
||||
data := handler.LoginByCode(context.Background(), testSmsPhoneNumber, testSmsCode, jar)
|
||||
t.Logf("LoginByCode Data: %+v", data)
|
||||
|
||||
handler.SetLoginCookie(context.Background(), jar, data)
|
||||
}
|
||||
|
||||
func TestLogin(t *testing.T) {
|
||||
handler := SmsLoginHandler{}
|
||||
ctx := gctx.GetInitCtx()
|
||||
sendSmsData := handler.SendSms(ctx, testSmsPhoneNumber)
|
||||
// 读取验证码手动输入
|
||||
var code string
|
||||
println("请输入收到的验证码:")
|
||||
_, _ = fmt.Scanln(&code)
|
||||
loginData := handler.LoginByCode(ctx, testSmsPhoneNumber, code, sendSmsData.CookieJar)
|
||||
handler.SetLoginCookie(ctx, sendSmsData.CookieJar, loginData)
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
package map_info
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"likedream-goframe/internal/application/httpclient"
|
||||
"likedream-goframe/internal/common/errors"
|
||||
"likedream-goframe/internal/utils"
|
||||
"regexp"
|
||||
"time"
|
||||
|
||||
"github.com/bytedance/sonic"
|
||||
"github.com/samber/oops"
|
||||
)
|
||||
|
||||
func SearchAddressLongitudeAndLatitude(ctx context.Context, address string) []MapPoi {
|
||||
client := httpclient.Cpdaily.Clone(ctx).SetDoNotParseResponse(false)
|
||||
// 首先访问获取ak
|
||||
response, err := client.R().Get("https://lbsyun.baidu.com/jsdemo/demo/webgl0_5.htm")
|
||||
utils.OkOrPanic(err, func(err error) error {
|
||||
return oops.WithContext(ctx).With("url", "https://lbsyun.baidu.com/jsdemo/demo/webgl0_5.htm").In("SearchAddressLongitudeAndLatitude.GetAk").Wrap(err)
|
||||
})
|
||||
ak := ParseAkString(response.String())
|
||||
timestamp := fmt.Sprintf("%d", time.Now().UnixMilli())
|
||||
mapResponse, err := client.R().
|
||||
SetQueryParams(map[string]string{
|
||||
"qt": "placesug",
|
||||
"query": address,
|
||||
"callback": "BMapGL._rd._cbk25241",
|
||||
"region": "131",
|
||||
"city_limit": "false",
|
||||
"t": timestamp,
|
||||
"from": "jsapi",
|
||||
"ak": ak,
|
||||
"v": "gl",
|
||||
"timeStamp": timestamp,
|
||||
}).
|
||||
SetHeaders(map[string]string{
|
||||
"Accept": "*/*",
|
||||
"Accept-Language": "zh-CN,zh;q=0.9",
|
||||
"Connection": "keep-alive",
|
||||
"Referer": "https://lbsyun.baidu.com/",
|
||||
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/142.0.0.0 Safari/537.36",
|
||||
}).
|
||||
Get("https://api.map.baidu.com/")
|
||||
utils.OkOrPanic(err, func(err error) error {
|
||||
return oops.WithContext(ctx).With("address", address).In("SearchAddressLongitudeAndLatitude.QueryMap").Wrap(err)
|
||||
})
|
||||
|
||||
// 从JSONP响应中提取JSON数据
|
||||
str := ParseJsonpToJson(mapResponse.String())
|
||||
var jsonData MapJSONData
|
||||
sonic.Unmarshal([]byte(str), &jsonData)
|
||||
return jsonData.Result.PoiList
|
||||
}
|
||||
|
||||
// 正则提取ak // <script type="text/javascript" src="//api.map.baidu.com/api?type=webgl&v=1.0&ak=E4805d16520de693a3fe707cdc962045"></script>
|
||||
func ParseAkString(data string) string {
|
||||
reg := regexp.MustCompile(`ak=([^"]+)`)
|
||||
matches := reg.FindStringSubmatch(data)
|
||||
if len(matches) < 2 {
|
||||
utils.OkOrPanic(errors.NewBusinessError(500, "获取ak出错"))
|
||||
}
|
||||
return matches[1]
|
||||
}
|
||||
|
||||
// 从JSONP响应中提取JSON数据
|
||||
// 输入: /**/BMapGL._rd._cbk25241 && BMapGL._rd._cbk25241({...})
|
||||
// 输出: {...}
|
||||
func ParseJsonpToJson(jsonpData string) string {
|
||||
// 匹配 callback(json) 格式,提取括号中的内容
|
||||
reg := regexp.MustCompile(`\((\{.*\})\)`)
|
||||
matches := reg.FindStringSubmatch(jsonpData)
|
||||
if len(matches) < 2 {
|
||||
utils.OkOrPanic(errors.NewBusinessError(500, "解析JSONP数据出错"))
|
||||
}
|
||||
return matches[1]
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
package map_info
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"testing"
|
||||
|
||||
"github.com/gogf/gf/v2/os/gctx"
|
||||
)
|
||||
|
||||
func TestMapSearch(t *testing.T) {
|
||||
res := SearchAddressLongitudeAndLatitude(gctx.New(), "河北石油职业技术大学c座")
|
||||
marshal, _ := json.Marshal(res)
|
||||
t.Log(string(marshal))
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
package map_info
|
||||
|
||||
type MapJSONData struct {
|
||||
Status int `json:"status"`
|
||||
Message string `json:"message"`
|
||||
Result MapResult `json:"result"`
|
||||
}
|
||||
type MapLocation struct {
|
||||
Lat float64 `json:"lat"`
|
||||
Lng float64 `json:"lng"`
|
||||
}
|
||||
type MapPoi struct {
|
||||
Name string `json:"name"`
|
||||
Location MapLocation `json:"location"`
|
||||
UID string `json:"uid"`
|
||||
Province string `json:"province"`
|
||||
City string `json:"city"`
|
||||
District string `json:"district"`
|
||||
Business string `json:"business"`
|
||||
Cityid string `json:"cityid"`
|
||||
Tag string `json:"tag"`
|
||||
Address string `json:"address"`
|
||||
Children []interface{} `json:"children"`
|
||||
Adcode string `json:"adcode"`
|
||||
}
|
||||
type MapResult struct {
|
||||
Q string `json:"q"`
|
||||
PoiList []MapPoi `json:"poi_list"`
|
||||
}
|
||||
@@ -1,22 +1,27 @@
|
||||
package school
|
||||
|
||||
import "C"
|
||||
import (
|
||||
"context"
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
cryptoutil2 "likedream-goframe/internal/application/cpdaily/cryptoutil"
|
||||
"likedream-goframe/internal/application/httpclient"
|
||||
cst2 "likedream-goframe/internal/common/cst"
|
||||
"likedream-goframe/internal/common/errors"
|
||||
"likedream-goframe/internal/framework/redis"
|
||||
"likedream-goframe/internal/utils"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/bytedance/sonic"
|
||||
"github.com/samber/oops"
|
||||
"github.com/tidwall/gjson"
|
||||
)
|
||||
|
||||
var lock = sync.RWMutex{}
|
||||
|
||||
func GetSchoolList(ctx context.Context) []CpdailySchoolBriefInfo {
|
||||
response, err := httpclient.Cpdaily.R().
|
||||
response, err := httpclient.Cpdaily.Clone(ctx).R().
|
||||
WithContext(ctx).
|
||||
Get("https://static.campushoy.com/apicache/tenantListSort")
|
||||
|
||||
@@ -52,7 +57,19 @@ func GetSchoolList(ctx context.Context) []CpdailySchoolBriefInfo {
|
||||
}
|
||||
|
||||
func GetSchoolDetail(ctx context.Context, tenantId string) SchoolDetailInfo {
|
||||
response, err := httpclient.Cpdaily.R().Clone(ctx).
|
||||
lock.Lock()
|
||||
defer lock.Unlock()
|
||||
if !utils.IsDebug() {
|
||||
client := redis.GetClient()
|
||||
redisResult := client.Get(ctx, cst2.RedisSchoolInfo+tenantId)
|
||||
if redisResult.Err() == nil {
|
||||
var ret SchoolDetailInfo
|
||||
err := sonic.Unmarshal([]byte(redisResult.Val()), &ret)
|
||||
utils.OkOrPanic(err)
|
||||
return parseSpecialSchool(ret)
|
||||
}
|
||||
}
|
||||
response, err := httpclient.Cpdaily.Clone(ctx).R().
|
||||
SetHeaders(map[string]string{
|
||||
"deviceType": "2",
|
||||
"CpdailyClientType": "CPDAILY",
|
||||
@@ -89,6 +106,17 @@ func GetSchoolDetail(ctx context.Context, tenantId string) SchoolDetailInfo {
|
||||
WithContext(ctx).
|
||||
Wrap(errors.NewBusinessError(503, "学校ID不存在").Decorate("empty school detail")))
|
||||
}
|
||||
|
||||
return ret[0]
|
||||
if !utils.IsDebug() {
|
||||
client := redis.GetClient()
|
||||
client.Set(ctx, cst2.RedisSchoolInfo+tenantId, ret[0], time.Duration(cst2.RedisSchoolInfoExpire)*time.Second)
|
||||
}
|
||||
return parseSpecialSchool(ret[0])
|
||||
}
|
||||
|
||||
func parseSpecialSchool(s SchoolDetailInfo) SchoolDetailInfo {
|
||||
// 甘肃农业大学
|
||||
if s.Id == "gsau" {
|
||||
s.WecloudUrl = "https://fdm.gsau.edu.cn"
|
||||
}
|
||||
return s
|
||||
}
|
||||
|
||||
@@ -1,181 +1,182 @@
|
||||
package submitcaptcha
|
||||
|
||||
import (
|
||||
"context"
|
||||
"io"
|
||||
"likedream-goframe/internal/application/cpdaily/types"
|
||||
"likedream-goframe/internal/application/recognizer/classifier"
|
||||
"likedream-goframe/internal/common/errors"
|
||||
"likedream-goframe/internal/utils"
|
||||
"strings"
|
||||
|
||||
"github.com/bytedance/sonic"
|
||||
"resty.dev/v3"
|
||||
)
|
||||
|
||||
type AIResolver struct {
|
||||
client *resty.Client
|
||||
host string
|
||||
deviceId string
|
||||
taskType types.TaskType
|
||||
}
|
||||
|
||||
func NewAIResolver(client *resty.Client, host, deviceId string, taskType types.TaskType) SubmitCaptchaResolver {
|
||||
return &AIResolver{
|
||||
client: client,
|
||||
host: host,
|
||||
deviceId: deviceId,
|
||||
taskType: taskType,
|
||||
}
|
||||
}
|
||||
func (r *AIResolver) GetValidationInfo(ctx context.Context) ValidationInfo {
|
||||
// GetValidationInfo 获取验证信息
|
||||
reqData := map[string]string{
|
||||
"deviceId": r.deviceId,
|
||||
}
|
||||
|
||||
resp, err := r.client.R().
|
||||
SetBody(reqData).
|
||||
Post(r.getValidateUrl())
|
||||
utils.OkOrPanic(err)
|
||||
|
||||
apiResp := utils.ReadJson[ApiResponse](resp.Body)
|
||||
|
||||
if apiResp.Datas == nil {
|
||||
message := "获取提交验证码信息失败"
|
||||
if apiResp.Message != "" {
|
||||
message = apiResp.Message
|
||||
}
|
||||
utils.OkOrPanic(errors.NewBusinessError(503, message))
|
||||
}
|
||||
|
||||
var validationInfo ValidationInfo
|
||||
err = sonic.Unmarshal(apiResp.Datas, &validationInfo)
|
||||
utils.OkOrPanic(err)
|
||||
|
||||
return validationInfo
|
||||
|
||||
}
|
||||
func (r *AIResolver) GetCaptchaTicket(ctx context.Context, info ValidationInfo) string {
|
||||
captchaInfo := r.createCaptcha(info)
|
||||
codes := r.getResult(captchaInfo)
|
||||
return r.submitCaptcha(info, captchaInfo, codes)
|
||||
}
|
||||
|
||||
func (r *AIResolver) getResult(data CpSubmitCaptchaForAI) []string {
|
||||
name := data.Name
|
||||
infos := data.ImageInfos
|
||||
var result []string
|
||||
for _, item := range infos {
|
||||
url := item.Path
|
||||
get, err := r.client.R().Get(url)
|
||||
utils.OkOrPanic(err)
|
||||
defer get.Body.Close()
|
||||
bytes, _ := io.ReadAll(get.Body)
|
||||
//todo 将图片保存方便后面训练
|
||||
//os.WriteFile(item.Code+".png", bytes, 0666)
|
||||
if classifier.ClassifyImage(bytes, name) {
|
||||
result = append(result, item.Code)
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
// getValidateUrl 根据任务类型获取验证URL
|
||||
func (r *AIResolver) getValidateUrl() string {
|
||||
baseUrl := strings.TrimSuffix(r.host, "/")
|
||||
|
||||
switch r.taskType {
|
||||
case types.TaskTypeCollection:
|
||||
return baseUrl + "/wec-counselor-collector-apps/stu/collector/checkValidation"
|
||||
case types.TaskTypeSign:
|
||||
return baseUrl + "/wec-counselor-sign-apps/stu/sign/checkValidation"
|
||||
case types.TaskTypeSleepCheck:
|
||||
return baseUrl + "/wec-counselor-attendance-apps/student/attendance/checkValidation"
|
||||
default:
|
||||
utils.OkOrPanic(errors.NewBusinessError(400, "不支持的任务类型"))
|
||||
return ""
|
||||
}
|
||||
}
|
||||
|
||||
// createCaptcha 创建验证码
|
||||
func (r *AIResolver) createCaptcha(info ValidationInfo) CpSubmitCaptchaForAI {
|
||||
baseUrl := strings.TrimSuffix(r.host, "/")
|
||||
url := baseUrl + "/captcha-open-api/v1/captcha/create/scenesImage"
|
||||
|
||||
resp, err := r.client.R().
|
||||
SetHeader("deviceId", r.deviceId).
|
||||
SetMultipartFormData(map[string]string{
|
||||
"accountKey": info.AccountKey,
|
||||
"sceneCode": info.SceneCode,
|
||||
"tenantId": info.TenantId,
|
||||
"userId": info.UserId,
|
||||
}).
|
||||
Post(url)
|
||||
utils.OkOrPanic(err)
|
||||
|
||||
apiResp := utils.ReadJson[ApiResponse](resp.Body)
|
||||
|
||||
if apiResp.Result == nil {
|
||||
message := "获取提交验证码失败"
|
||||
if apiResp.Message != "" {
|
||||
message = apiResp.Message
|
||||
}
|
||||
utils.OkOrPanic(errors.NewBusinessError(503, message))
|
||||
}
|
||||
|
||||
var result CpSubmitCaptchaForAI
|
||||
err = sonic.Unmarshal(apiResp.Result, &result)
|
||||
utils.OkOrPanic(err)
|
||||
return result
|
||||
}
|
||||
|
||||
// submitCaptcha 提交验证码
|
||||
func (r *AIResolver) submitCaptcha(info ValidationInfo, submitCaptcha CpSubmitCaptchaForAI, codes []string) string {
|
||||
baseUrl := strings.TrimSuffix(r.host, "/")
|
||||
url := baseUrl + "/captcha-open-api/v1/captcha/validate/scenesImage"
|
||||
|
||||
formData := map[string]string{
|
||||
"accountKey": info.AccountKey,
|
||||
"sceneCode": info.SceneCode,
|
||||
"tenantId": info.TenantId,
|
||||
"userId": info.UserId,
|
||||
"scenesImageCode": submitCaptcha.Code,
|
||||
}
|
||||
|
||||
// 创建multipart form request
|
||||
req := r.client.R().SetHeader("deviceId", r.deviceId)
|
||||
|
||||
// 添加基本表单数据
|
||||
for key, value := range formData {
|
||||
req = req.SetMultipartFormData(map[string]string{key: value})
|
||||
}
|
||||
|
||||
// 添加验证码列表
|
||||
for _, code := range codes {
|
||||
req = req.SetMultipartField("scenesImageCodes", "", "", strings.NewReader(code))
|
||||
}
|
||||
|
||||
resp, err := req.Post(url)
|
||||
utils.OkOrPanic(err)
|
||||
|
||||
apiResp := utils.ReadJson[ApiResponse](resp.Body)
|
||||
|
||||
if apiResp.Code != "200" {
|
||||
message := "提交验证码失败"
|
||||
if apiResp.Message != "" {
|
||||
message = apiResp.Message
|
||||
}
|
||||
utils.OkOrPanic(errors.NewBusinessError(503, message))
|
||||
}
|
||||
|
||||
if apiResp.Result == nil {
|
||||
utils.OkOrPanic(errors.NewBusinessError(503, "今日校园返回空验证码结果"))
|
||||
}
|
||||
|
||||
var result string
|
||||
err = sonic.Unmarshal(apiResp.Result, &result)
|
||||
utils.OkOrPanic(err)
|
||||
|
||||
return result
|
||||
}
|
||||
//
|
||||
//import (
|
||||
// "context"
|
||||
// "io"
|
||||
// "likedream-goframe/internal/application/cpdaily/types"
|
||||
// "likedream-goframe/internal/application/recognizer/classifier"
|
||||
// "likedream-goframe/internal/common/errors"
|
||||
// "likedream-goframe/internal/utils"
|
||||
// "strings"
|
||||
//
|
||||
// "github.com/bytedance/sonic"
|
||||
// "resty.dev/v3"
|
||||
//)
|
||||
//
|
||||
//type AIResolver struct {
|
||||
// client *resty.Client
|
||||
// host string
|
||||
// deviceId string
|
||||
// taskType types.TaskType
|
||||
//}
|
||||
//
|
||||
//func NewAIResolver(client *resty.Client, host, deviceId string, taskType types.TaskType) SubmitCaptchaResolver {
|
||||
// return &AIResolver{
|
||||
// client: client,
|
||||
// host: host,
|
||||
// deviceId: deviceId,
|
||||
// taskType: taskType,
|
||||
// }
|
||||
//}
|
||||
//func (r *AIResolver) GetValidationInfo(ctx context.Context) ValidationInfo {
|
||||
// // GetValidationInfo 获取验证信息
|
||||
// reqData := map[string]string{
|
||||
// "deviceId": r.deviceId,
|
||||
// }
|
||||
//
|
||||
// resp, err := r.client.R().
|
||||
// SetBody(reqData).
|
||||
// Post(r.getValidateUrl())
|
||||
// utils.OkOrPanic(err)
|
||||
//
|
||||
// apiResp := utils.ReadJson[ApiResponse](resp.Body)
|
||||
//
|
||||
// if apiResp.Datas == nil {
|
||||
// message := "获取提交验证码信息失败"
|
||||
// if apiResp.Message != "" {
|
||||
// message = apiResp.Message
|
||||
// }
|
||||
// utils.OkOrPanic(errors.NewBusinessError(503, message))
|
||||
// }
|
||||
//
|
||||
// var validationInfo ValidationInfo
|
||||
// err = sonic.Unmarshal(apiResp.Datas, &validationInfo)
|
||||
// utils.OkOrPanic(err)
|
||||
//
|
||||
// return validationInfo
|
||||
//
|
||||
//}
|
||||
//func (r *AIResolver) GetCaptchaTicket(ctx context.Context, info ValidationInfo) string {
|
||||
// captchaInfo := r.createCaptcha(info)
|
||||
// codes := r.getResult(captchaInfo)
|
||||
// return r.submitCaptcha(info, captchaInfo, codes)
|
||||
//}
|
||||
//
|
||||
//func (r *AIResolver) getResult(data CpSubmitCaptchaForAI) []string {
|
||||
// name := data.Name
|
||||
// infos := data.ImageInfos
|
||||
// var result []string
|
||||
// for _, item := range infos {
|
||||
// url := item.Path
|
||||
// get, err := r.client.R().Get(url)
|
||||
// utils.OkOrPanic(err)
|
||||
// defer get.Body.Close()
|
||||
// bytes, _ := io.ReadAll(get.Body)
|
||||
// //todo 将图片保存方便后面训练
|
||||
// //os.WriteFile(item.Code+".png", bytes, 0666)
|
||||
// if classifier.ClassifyImage(bytes, name) {
|
||||
// result = append(result, item.Code)
|
||||
// }
|
||||
// }
|
||||
// return result
|
||||
//}
|
||||
//
|
||||
//// getValidateUrl 根据任务类型获取验证URL
|
||||
//func (r *AIResolver) getValidateUrl() string {
|
||||
// baseUrl := strings.TrimSuffix(r.host, "/")
|
||||
//
|
||||
// switch r.taskType {
|
||||
// case types.TaskTypeCollection:
|
||||
// return baseUrl + "/wec-counselor-collector-apps/stu/collector/checkValidation"
|
||||
// case types.TaskTypeSign:
|
||||
// return baseUrl + "/wec-counselor-sign-apps/stu/sign/checkValidation"
|
||||
// case types.TaskTypeSleepCheck:
|
||||
// return baseUrl + "/wec-counselor-attendance-apps/student/attendance/checkValidation"
|
||||
// default:
|
||||
// utils.OkOrPanic(errors.NewBusinessError(400, "不支持的任务类型"))
|
||||
// return ""
|
||||
// }
|
||||
//}
|
||||
//
|
||||
//// createCaptcha 创建验证码
|
||||
//func (r *AIResolver) createCaptcha(info ValidationInfo) CpSubmitCaptchaForAI {
|
||||
// baseUrl := strings.TrimSuffix(r.host, "/")
|
||||
// url := baseUrl + "/captcha-open-api/v1/captcha/create/scenesImage"
|
||||
//
|
||||
// resp, err := r.client.R().
|
||||
// SetHeader("deviceId", r.deviceId).
|
||||
// SetMultipartFormData(map[string]string{
|
||||
// "accountKey": info.AccountKey,
|
||||
// "sceneCode": info.SceneCode,
|
||||
// "tenantId": info.TenantId,
|
||||
// "userId": info.UserId,
|
||||
// }).
|
||||
// Post(url)
|
||||
// utils.OkOrPanic(err)
|
||||
//
|
||||
// apiResp := utils.ReadJson[ApiResponse](resp.Body)
|
||||
//
|
||||
// if apiResp.Result == nil {
|
||||
// message := "获取提交验证码失败"
|
||||
// if apiResp.Message != "" {
|
||||
// message = apiResp.Message
|
||||
// }
|
||||
// utils.OkOrPanic(errors.NewBusinessError(503, message))
|
||||
// }
|
||||
//
|
||||
// var result CpSubmitCaptchaForAI
|
||||
// err = sonic.Unmarshal(apiResp.Result, &result)
|
||||
// utils.OkOrPanic(err)
|
||||
// return result
|
||||
//}
|
||||
//
|
||||
//// submitCaptcha 提交验证码
|
||||
//func (r *AIResolver) submitCaptcha(info ValidationInfo, submitCaptcha CpSubmitCaptchaForAI, codes []string) string {
|
||||
// baseUrl := strings.TrimSuffix(r.host, "/")
|
||||
// url := baseUrl + "/captcha-open-api/v1/captcha/validate/scenesImage"
|
||||
//
|
||||
// formData := map[string]string{
|
||||
// "accountKey": info.AccountKey,
|
||||
// "sceneCode": info.SceneCode,
|
||||
// "tenantId": info.TenantId,
|
||||
// "userId": info.UserId,
|
||||
// "scenesImageCode": submitCaptcha.Code,
|
||||
// }
|
||||
//
|
||||
// // 创建multipart form request
|
||||
// req := r.client.R().SetHeader("deviceId", r.deviceId)
|
||||
//
|
||||
// // 添加基本表单数据
|
||||
// for key, value := range formData {
|
||||
// req = req.SetMultipartFormData(map[string]string{key: value})
|
||||
// }
|
||||
//
|
||||
// // 添加验证码列表
|
||||
// for _, code := range codes {
|
||||
// req = req.SetMultipartField("scenesImageCodes", "", "", strings.NewReader(code))
|
||||
// }
|
||||
//
|
||||
// resp, err := req.Post(url)
|
||||
// utils.OkOrPanic(err)
|
||||
//
|
||||
// apiResp := utils.ReadJson[ApiResponse](resp.Body)
|
||||
//
|
||||
// if apiResp.Code != "200" {
|
||||
// message := "提交验证码失败"
|
||||
// if apiResp.Message != "" {
|
||||
// message = apiResp.Message
|
||||
// }
|
||||
// utils.OkOrPanic(errors.NewBusinessError(503, message))
|
||||
// }
|
||||
//
|
||||
// if apiResp.Result == nil {
|
||||
// utils.OkOrPanic(errors.NewBusinessError(503, "今日校园返回空验证码结果"))
|
||||
// }
|
||||
//
|
||||
// var result string
|
||||
// err = sonic.Unmarshal(apiResp.Result, &result)
|
||||
// utils.OkOrPanic(err)
|
||||
//
|
||||
// return result
|
||||
//}
|
||||
|
||||
@@ -42,9 +42,7 @@ type ApiResponse struct {
|
||||
type ITStarResponse struct {
|
||||
Code json.Number `json:"code"`
|
||||
Msg string `json:"msg"`
|
||||
Data struct {
|
||||
SuccCode []string `json:"succCode"`
|
||||
} `json:"data"`
|
||||
Data []string `json:"data"`
|
||||
}
|
||||
|
||||
type SubmitCaptchaResolver interface {
|
||||
|
||||
@@ -16,7 +16,7 @@ import (
|
||||
"resty.dev/v3"
|
||||
)
|
||||
|
||||
type RuoliSubmitCaptchaResolver struct {
|
||||
type NormalSubmitCaptchaResolver struct {
|
||||
client *resty.Client
|
||||
host string
|
||||
deviceId string
|
||||
@@ -24,12 +24,12 @@ type RuoliSubmitCaptchaResolver struct {
|
||||
itstarUrl string
|
||||
}
|
||||
|
||||
var _ SubmitCaptchaResolver = (*RuoliSubmitCaptchaResolver)(nil)
|
||||
var _ SubmitCaptchaResolver = (*NormalSubmitCaptchaResolver)(nil)
|
||||
|
||||
// NewRuoliResolver 创建若李验证码解析器
|
||||
func NewRuoliResolver(client *resty.Client, host, deviceId string, taskType types.TaskType) *RuoliSubmitCaptchaResolver {
|
||||
baseUrl := g.Config().MustGetWithEnv(gctx.GetInitCtx(), cst.ConfigRuoliCaptchaBaseUrl).String()
|
||||
return &RuoliSubmitCaptchaResolver{
|
||||
// NewNormalResolver 创建若李验证码解析器
|
||||
func NewNormalResolver(client *resty.Client, host, deviceId string, taskType types.TaskType) *NormalSubmitCaptchaResolver {
|
||||
baseUrl := g.Config().MustGetWithEnv(gctx.GetInitCtx(), cst.ConfigCaptchaBaseUrl).String() + "/classifier"
|
||||
return &NormalSubmitCaptchaResolver{
|
||||
client: client,
|
||||
host: host,
|
||||
deviceId: deviceId,
|
||||
@@ -39,12 +39,12 @@ func NewRuoliResolver(client *resty.Client, host, deviceId string, taskType type
|
||||
}
|
||||
|
||||
// getITStarUrl 获取ITStar服务URL
|
||||
func (r *RuoliSubmitCaptchaResolver) getITStarUrl() string {
|
||||
func (r *NormalSubmitCaptchaResolver) getITStarUrl() string {
|
||||
if r.itstarUrl != "" {
|
||||
return r.itstarUrl
|
||||
}
|
||||
|
||||
r.itstarUrl = g.Config().MustGetWithEnv(gctx.GetInitCtx(), cst.ConfigRuoliCaptchaBaseUrl).String()
|
||||
r.itstarUrl = g.Config().MustGetWithEnv(gctx.GetInitCtx(), cst.ConfigCaptchaBaseUrl).String() + "/classifier"
|
||||
if r.itstarUrl == "" {
|
||||
utils.OkOrPanic(errors.NewBusinessError(500, "未配置ITStarUrl"))
|
||||
}
|
||||
@@ -53,7 +53,7 @@ func (r *RuoliSubmitCaptchaResolver) getITStarUrl() string {
|
||||
}
|
||||
|
||||
// getValidateUrl 根据任务类型获取验证URL
|
||||
func (r *RuoliSubmitCaptchaResolver) getValidateUrl() string {
|
||||
func (r *NormalSubmitCaptchaResolver) getValidateUrl() string {
|
||||
baseUrl := strings.TrimSuffix(r.host, "/")
|
||||
|
||||
switch r.taskType {
|
||||
@@ -70,7 +70,7 @@ func (r *RuoliSubmitCaptchaResolver) getValidateUrl() string {
|
||||
}
|
||||
|
||||
// GetValidationInfo 获取验证信息
|
||||
func (r *RuoliSubmitCaptchaResolver) GetValidationInfo(ctx context.Context) ValidationInfo {
|
||||
func (r *NormalSubmitCaptchaResolver) GetValidationInfo(ctx context.Context) ValidationInfo {
|
||||
reqData := map[string]string{
|
||||
"deviceId": r.deviceId,
|
||||
}
|
||||
@@ -98,14 +98,14 @@ func (r *RuoliSubmitCaptchaResolver) GetValidationInfo(ctx context.Context) Vali
|
||||
}
|
||||
|
||||
// GetCaptchaTicket 获取验证码票据
|
||||
func (r *RuoliSubmitCaptchaResolver) GetCaptchaTicket(ctx context.Context, info ValidationInfo) string {
|
||||
func (r *NormalSubmitCaptchaResolver) GetCaptchaTicket(ctx context.Context, info ValidationInfo) string {
|
||||
captchaInfo := r.createCaptcha(info)
|
||||
codes := r.getResultFromItStar(captchaInfo.RawResponseJson)
|
||||
return r.submitCaptcha(info, captchaInfo, codes)
|
||||
}
|
||||
|
||||
// createCaptcha 创建验证码
|
||||
func (r *RuoliSubmitCaptchaResolver) createCaptcha(info ValidationInfo) CpSubmitCaptcha {
|
||||
func (r *NormalSubmitCaptchaResolver) createCaptcha(info ValidationInfo) CpSubmitCaptcha {
|
||||
baseUrl := strings.TrimSuffix(r.host, "/")
|
||||
url := baseUrl + "/captcha-open-api/v1/captcha/create/scenesImage"
|
||||
|
||||
@@ -139,16 +139,13 @@ func (r *RuoliSubmitCaptchaResolver) createCaptcha(info ValidationInfo) CpSubmit
|
||||
}
|
||||
|
||||
// getResultFromItStar 从ITStar获取识别结果
|
||||
func (r *RuoliSubmitCaptchaResolver) getResultFromItStar(rawResponse json.RawMessage) []string {
|
||||
func (r *NormalSubmitCaptchaResolver) getResultFromItStar(rawResponse json.RawMessage) []string {
|
||||
// 解析原始响应为map以便添加authKey
|
||||
var responseMap map[string]any
|
||||
err := sonic.Unmarshal(rawResponse, &responseMap)
|
||||
utils.OkOrPanic(err)
|
||||
|
||||
responseMap["authKey"] = "ITStarYYDS"
|
||||
|
||||
itstarUrl := strings.TrimSuffix(r.getITStarUrl(), "/")
|
||||
url := itstarUrl + "/captcha/unLimitValidate"
|
||||
url := r.getITStarUrl()
|
||||
|
||||
resp, err := r.client.R().
|
||||
SetBody(responseMap).
|
||||
@@ -161,11 +158,11 @@ func (r *RuoliSubmitCaptchaResolver) getResultFromItStar(rawResponse json.RawMes
|
||||
utils.OkOrPanic(errors.NewBusinessError(503, fmt.Sprintf("验证码识别出错: %s", itstarResp.Msg)))
|
||||
}
|
||||
|
||||
return itstarResp.Data.SuccCode
|
||||
return itstarResp.Data
|
||||
}
|
||||
|
||||
// submitCaptcha 提交验证码
|
||||
func (r *RuoliSubmitCaptchaResolver) submitCaptcha(info ValidationInfo, submitCaptcha CpSubmitCaptcha, codes []string) string {
|
||||
func (r *NormalSubmitCaptchaResolver) submitCaptcha(info ValidationInfo, submitCaptcha CpSubmitCaptcha, codes []string) string {
|
||||
baseUrl := strings.TrimSuffix(r.host, "/")
|
||||
url := baseUrl + "/captcha-open-api/v1/captcha/validate/scenesImage"
|
||||
|
||||
|
||||
@@ -10,7 +10,7 @@ import (
|
||||
func TestRuoliSubmitCaptchaResolver_GetCaptchaTicket(t *testing.T) {
|
||||
loginClient, detail := testutil.LoginTestUser()
|
||||
|
||||
resolver := NewRuoliResolver(loginClient, detail.WecloudUrl, "abcdefg", types.TaskTypeSleepCheck)
|
||||
resolver := NewNormalResolver(loginClient, detail.WecloudUrl, "abcdefg", types.TaskTypeSleepCheck)
|
||||
|
||||
info := resolver.GetValidationInfo(context.Background())
|
||||
|
||||
|
||||
@@ -5,10 +5,8 @@ import (
|
||||
"fmt"
|
||||
"likedream-goframe/internal/application/cpdaily"
|
||||
"likedream-goframe/internal/application/cpdaily/cryptoutil"
|
||||
"likedream-goframe/internal/application/cpdaily/imageuploader"
|
||||
"likedream-goframe/internal/application/cpdaily/task/formtype"
|
||||
"likedream-goframe/internal/application/cpdaily/taskhelper"
|
||||
"likedream-goframe/internal/application/cpdaily/types"
|
||||
"likedream-goframe/internal/common/errors"
|
||||
"likedream-goframe/internal/utils"
|
||||
"maps"
|
||||
@@ -454,31 +452,26 @@ func (c *CpdailyCollectionTask) FillCustomForm(ctx context.Context, form map[str
|
||||
newRowObj["value"] = gvar.New(strings.Join(values, ","))
|
||||
}
|
||||
} else if c.isPicType(fieldType) {
|
||||
// 图片类型
|
||||
// 图片类型 - content 中存储的是今日校园的 previewUrl,逗号分隔
|
||||
if param.Content == "" {
|
||||
utils.OkOrPanic(errors.NewBusinessError(400, "查寝需要图片,但是当前图片为空"), oops.WithContext(ctx).Wrap)
|
||||
}
|
||||
|
||||
imageUuidSeq := strings.Split(param.Content, ",")
|
||||
var imageUuids []string
|
||||
for _, imageUuid := range imageUuidSeq {
|
||||
imageUuid = strings.TrimSpace(imageUuid)
|
||||
if imageUuid == "" {
|
||||
continue
|
||||
previewUrls := strings.Split(param.Content, ",")
|
||||
var validUrls []string
|
||||
for _, url := range previewUrls {
|
||||
url = strings.TrimSpace(url)
|
||||
if url != "" {
|
||||
validUrls = append(validUrls, url)
|
||||
}
|
||||
imageUuids = append(imageUuids, imageUuid)
|
||||
}
|
||||
|
||||
if len(imageUuids) == 0 {
|
||||
if len(validUrls) == 0 {
|
||||
utils.OkOrPanic(errors.NewBusinessError(400, "查寝照片未上传"), oops.WithContext(ctx).Wrap)
|
||||
}
|
||||
|
||||
// 随机选择一张照片
|
||||
imageUuid := imageUuids[rand.IntN(len(imageUuids))]
|
||||
imageId, e := uuid.Parse(imageUuid)
|
||||
utils.OkOrPanic(e, oops.WithContext(ctx).Wrap)
|
||||
|
||||
previewUrl := imageuploader.UploadImageFromStorage(ctx, c.host, imageId, types.TaskTypeCollection, c.client)
|
||||
previewUrl := validUrls[rand.IntN(len(validUrls))]
|
||||
newRowObj["value"] = gvar.New(previewUrl)
|
||||
} else {
|
||||
utils.OkOrPanic(errors.NewBusinessError(500, "出现未知类型表单,请反馈表单类型:"+fieldType), oops.WithContext(ctx).Wrap)
|
||||
|
||||
@@ -15,4 +15,5 @@ type CpTaskFormItem struct {
|
||||
Sort uint `json:"sort,omitempty"`
|
||||
CacheKey string `json:"cacheKey,omitempty"`
|
||||
Show bool `json:"show,omitempty"`
|
||||
Tip string `json:"tip,omitempty"`
|
||||
}
|
||||
|
||||
@@ -8,4 +8,5 @@ const (
|
||||
ItemTypeMulti
|
||||
ItemTypeImage
|
||||
ItemTypeAddress
|
||||
ItemTypeLatAndLon
|
||||
)
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
package leavetype
|
||||
|
||||
type LeaveListOne struct {
|
||||
Id string `json:"id"`
|
||||
LeaveType string `json:"leaveType"`
|
||||
CreateTime string `json:"createTime"`
|
||||
AceEndTime string `json:"aceEndTime"`
|
||||
Reason string `json:"reason"`
|
||||
Status string `json:"status"`
|
||||
// 销假时填充的位置信息
|
||||
Longitude string `json:"longitude"` // 经度
|
||||
Latitude string `json:"latitude"` // 纬度
|
||||
Address string `json:"address"` // 地址
|
||||
ChooseAttendance int64 `json:"chooseAttendance"` // 是否需要图片 1需要 0 不需要
|
||||
}
|
||||
@@ -5,14 +5,15 @@ import (
|
||||
"fmt"
|
||||
"likedream-goframe/internal/application/cpdaily"
|
||||
"likedream-goframe/internal/application/cpdaily/cryptoutil"
|
||||
"likedream-goframe/internal/application/cpdaily/imageuploader"
|
||||
"likedream-goframe/internal/application/cpdaily/submitcaptcha"
|
||||
"likedream-goframe/internal/application/cpdaily/task/formtype"
|
||||
"likedream-goframe/internal/application/cpdaily/taskhelper"
|
||||
"likedream-goframe/internal/application/cpdaily/types"
|
||||
"likedream-goframe/internal/common/cst"
|
||||
"likedream-goframe/internal/common/errors"
|
||||
"likedream-goframe/internal/utils"
|
||||
"slices"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
@@ -20,6 +21,7 @@ import (
|
||||
"github.com/gogf/gf/v2/container/gvar"
|
||||
"github.com/google/uuid"
|
||||
"github.com/samber/oops"
|
||||
"github.com/tidwall/gjson"
|
||||
"github.com/tidwall/match"
|
||||
"resty.dev/v3"
|
||||
)
|
||||
@@ -221,6 +223,88 @@ func (c *CpdailyAutoSignTask) QueryFormByTitle(ctx context.Context, titleGlob st
|
||||
result["realTitle"] = gvar.New(realTitle)
|
||||
result["taskInfo"] = gvar.New(matchTask)
|
||||
result["rows"] = gvar.New(formDatas)
|
||||
|
||||
// 尝试从已签到的历史任务中获取历史数据用于预填充
|
||||
signedMatch := findMatchTitle(signedTasks)
|
||||
if signedMatch != nil {
|
||||
// 如果已签到任务与当前匹配任务是同一个,直接用已有数据作为历史(避免重复API调用)
|
||||
if matchTask != nil && signedMatch["signInstanceWid"] != nil && matchTask["signInstanceWid"] != nil &&
|
||||
signedMatch["signInstanceWid"].String() == matchTask["signInstanceWid"].String() {
|
||||
result["historyRows"] = result["rows"]
|
||||
} else {
|
||||
histResp, histErr := c.client.R().
|
||||
SetBody(map[string]*gvar.Var{
|
||||
"taskName": gvar.New(signedMatch["taskName"].String()),
|
||||
"signWid": signedMatch["signWid"],
|
||||
"signInstanceWid": signedMatch["signInstanceWid"],
|
||||
}).
|
||||
WithContext(ctx).
|
||||
Post(c.getReqUrl("detailSignInstance"))
|
||||
if histErr == nil {
|
||||
histJSON := utils.ReadJson[map[string]*gvar.Var](histResp.Body)
|
||||
histDatas := histJSON["datas"].MapStrVar()
|
||||
if len(histDatas) > 0 {
|
||||
result["historyRows"] = gvar.New(histDatas)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// 今天没有已签到的同名任务,从历史月份中只查 signedTasks
|
||||
if result["historyRows"] == nil {
|
||||
func() {
|
||||
defer func() { recover() }()
|
||||
now := time.Now().In(cst.TimeZoneShangHai)
|
||||
year := strconv.Itoa(now.Year())
|
||||
month := strconv.Itoa(int(now.Month()))
|
||||
if len(month) == 1 {
|
||||
month = "0" + month
|
||||
}
|
||||
resp, e := c.client.R().
|
||||
SetBody(map[string]any{
|
||||
"statisticYearMonth": fmt.Sprintf("%s-%s", year, month),
|
||||
}).
|
||||
WithContext(ctx).
|
||||
Post(c.getReqUrl("getStuSignInfosByWeekMonth"))
|
||||
if e != nil {
|
||||
return
|
||||
}
|
||||
respJSON := utils.ReadJson[map[string]*gvar.Var](resp.Body)
|
||||
monthDatas := respJSON["datas"].MapStrVar()
|
||||
monthRows := monthDatas["rows"].Vars()
|
||||
|
||||
for _, row := range monthRows {
|
||||
rowMap := row.MapStrVar()
|
||||
signed := rowMap["signedTasks"].Vars()
|
||||
for _, st := range signed {
|
||||
item := st.MapStrVar()
|
||||
tn, exist := item["taskName"]
|
||||
if !exist || tn.String() == "" {
|
||||
continue
|
||||
}
|
||||
if match.Match(tn.String(), titleGlob) {
|
||||
dr, de := c.client.R().
|
||||
SetBody(map[string]*gvar.Var{
|
||||
"signInstanceWid": item["signInstanceWid"],
|
||||
"signWid": item["signWid"],
|
||||
"taskName": gvar.New(tn.String()),
|
||||
}).
|
||||
WithContext(ctx).
|
||||
Post(c.getReqUrl("detailSignInstance"))
|
||||
if de != nil {
|
||||
continue
|
||||
}
|
||||
dj := utils.ReadJson[map[string]*gvar.Var](dr.Body)
|
||||
dd := dj["datas"].MapStrVar()
|
||||
if len(dd) > 0 {
|
||||
result["historyRows"] = gvar.New(dd)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
@@ -356,22 +440,75 @@ func (c *CpdailyAutoSignTask) ParseFormJson(ctx context.Context, rawJson map[str
|
||||
utils.OkOrPanic(errors.NewBusinessError(503, "任务表单为空"), oops.WithContext(ctx).Wrap)
|
||||
}
|
||||
|
||||
// 优先从 historyRows(已签到的历史任务详情)中提取历史值,否则从 rows 中提取
|
||||
histRows := rows
|
||||
if hr := rawJson["historyRows"]; hr != nil {
|
||||
if hrMap := hr.MapStrVar(); len(hrMap) > 0 {
|
||||
histRows = hrMap
|
||||
}
|
||||
}
|
||||
|
||||
var list []*formtype.CpTaskFormItem
|
||||
|
||||
// 经纬度
|
||||
dto := &formtype.CpTaskFormItem{
|
||||
Title: "经度",
|
||||
ColName: "lon",
|
||||
Type: formtype.ItemTypeInput,
|
||||
Required: true,
|
||||
}
|
||||
list = append(list, dto)
|
||||
// 从任务详情中提取历史值用于预填充
|
||||
histLatAndLon := ""
|
||||
histAddress := ""
|
||||
histSignPhotoUrl := ""
|
||||
histIsMalposition := ""
|
||||
|
||||
dto = &formtype.CpTaskFormItem{
|
||||
Title: "纬度",
|
||||
ColName: "lat",
|
||||
Type: formtype.ItemTypeInput,
|
||||
Required: true,
|
||||
// 优先从 signPlaceSelected 提取经纬度和地址
|
||||
if sps := histRows["signPlaceSelected"]; sps != nil && sps.String() != "" && sps.String() != "[]" {
|
||||
placeArr := gjson.Parse(sps.String()).Array()
|
||||
if len(placeArr) > 0 {
|
||||
place := placeArr[0]
|
||||
lng := place.Get("longitude").String()
|
||||
lat := place.Get("latitude").String()
|
||||
addr := place.Get("address").String()
|
||||
if lng != "" && lat != "" {
|
||||
histLatAndLon = lng + "," + lat
|
||||
}
|
||||
if addr != "" {
|
||||
histAddress = addr
|
||||
}
|
||||
}
|
||||
}
|
||||
if histLatAndLon == "" {
|
||||
if lon := histRows["longitude"]; lon != nil && lon.String() != "" {
|
||||
if lat := histRows["latitude"]; lat != nil && lat.String() != "" {
|
||||
histLatAndLon = lon.String() + "," + lat.String()
|
||||
}
|
||||
}
|
||||
}
|
||||
if histAddress == "" {
|
||||
if pos := histRows["position"]; pos != nil && pos.String() != "" {
|
||||
histAddress = pos.String()
|
||||
}
|
||||
if histAddress == "" {
|
||||
if addr := histRows["signAddress"]; addr != nil && addr.String() != "" {
|
||||
histAddress = addr.String()
|
||||
}
|
||||
}
|
||||
}
|
||||
if photo := histRows["signPhotoUrl"]; photo != nil && photo.String() != "" {
|
||||
histSignPhotoUrl = photo.String()
|
||||
}
|
||||
if mal := histRows["isMalposition"]; mal != nil {
|
||||
if mal.Int() == 1 {
|
||||
histIsMalposition = "是"
|
||||
} else {
|
||||
histIsMalposition = "否"
|
||||
}
|
||||
}
|
||||
|
||||
dto := &formtype.CpTaskFormItem{
|
||||
Title: "经纬度",
|
||||
ColName: "latAndLon",
|
||||
Type: formtype.ItemTypeLatAndLon,
|
||||
Required: true,
|
||||
Content: histLatAndLon,
|
||||
Default: histLatAndLon,
|
||||
OtherValue: histAddress,
|
||||
Tip: "您可以通过地址搜索签到经纬度,如果觉得不够准确可以自行修改",
|
||||
}
|
||||
list = append(list, dto)
|
||||
|
||||
@@ -380,6 +517,9 @@ func (c *CpdailyAutoSignTask) ParseFormJson(ctx context.Context, rawJson map[str
|
||||
ColName: "address",
|
||||
Type: formtype.ItemTypeInput,
|
||||
Required: true,
|
||||
Content: histAddress,
|
||||
Default: histAddress,
|
||||
Tip: "这个就是指签到完毕后查看时最底部那个最详细的地址信息哦",
|
||||
}
|
||||
list = append(list, dto)
|
||||
|
||||
@@ -389,6 +529,9 @@ func (c *CpdailyAutoSignTask) ParseFormJson(ctx context.Context, rawJson map[str
|
||||
ColName: "signImg",
|
||||
Type: formtype.ItemTypeImage,
|
||||
Required: true,
|
||||
Content: histSignPhotoUrl,
|
||||
Default: histSignPhotoUrl,
|
||||
Tip: "您可以上传多张图片,会顺序轮换更新的哦~",
|
||||
}
|
||||
list = append(list, dto)
|
||||
}
|
||||
@@ -399,6 +542,8 @@ func (c *CpdailyAutoSignTask) ParseFormJson(ctx context.Context, rawJson map[str
|
||||
ColName: "isMalposition",
|
||||
Type: formtype.ItemTypeSingle,
|
||||
Required: true,
|
||||
Content: histIsMalposition,
|
||||
Default: histIsMalposition,
|
||||
Options: []formtype.CpTaskFormItemOption{
|
||||
{
|
||||
Content: "是",
|
||||
@@ -410,6 +555,7 @@ func (c *CpdailyAutoSignTask) ParseFormJson(ctx context.Context, rawJson map[str
|
||||
IsOtherItems: false,
|
||||
},
|
||||
},
|
||||
Tip: "一般在学校就是选否,如果在外实习、请假之类的才选是",
|
||||
}
|
||||
list = append(list, dto)
|
||||
|
||||
@@ -484,6 +630,11 @@ func (c *CpdailyAutoSignTask) FillCustomForm(ctx context.Context, form map[strin
|
||||
lon = v.Content
|
||||
case "address":
|
||||
address = v.Content
|
||||
case "latAndLon":
|
||||
{
|
||||
res := strings.Split(v.Content, ",")
|
||||
lat, lon = res[1], res[0]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -511,16 +662,12 @@ func (c *CpdailyAutoSignTask) FillCustomForm(ctx context.Context, form map[strin
|
||||
utils.OkOrPanic(errors.NewBusinessError(400, "签到照片未上传"), oops.WithContext(ctx).Wrap)
|
||||
}
|
||||
|
||||
imageId, e := uuid.Parse(imgId)
|
||||
utils.OkOrPanic(e, oops.WithContext(ctx).Wrap)
|
||||
|
||||
previewUrl := imageuploader.UploadImageFromStorage(ctx, c.host, imageId, types.TaskTypeSign, c.client)
|
||||
|
||||
resultForm["signPhotoUrl"] = gvar.New(previewUrl)
|
||||
// imgId 现在直接就是今日校园的 previewUrl,无需再上传
|
||||
resultForm["signPhotoUrl"] = gvar.New(imgId)
|
||||
}
|
||||
|
||||
// 处理验证码
|
||||
submitCaptchaResolver := submitcaptcha.NewRuoliResolver(c.client, c.host, deviceId, types.TaskTypeSign)
|
||||
submitCaptchaResolver := submitcaptcha.NewNormalResolver(c.client, c.host, deviceId, types.TaskTypeSign)
|
||||
captchaInfo := submitCaptchaResolver.GetValidationInfo(ctx)
|
||||
|
||||
if captchaInfo.Validation {
|
||||
|
||||
@@ -5,7 +5,6 @@ import (
|
||||
"fmt"
|
||||
"likedream-goframe/internal/application/cpdaily"
|
||||
"likedream-goframe/internal/application/cpdaily/cryptoutil"
|
||||
"likedream-goframe/internal/application/cpdaily/imageuploader"
|
||||
"likedream-goframe/internal/application/cpdaily/submitcaptcha"
|
||||
"likedream-goframe/internal/application/cpdaily/task/formtype"
|
||||
"likedream-goframe/internal/application/cpdaily/taskhelper"
|
||||
@@ -277,6 +276,91 @@ func (c *CpdailySleepCheckTask) QueryFormByTitle(ctx context.Context, titleGlob
|
||||
result["realTitle"] = gvar.New(realTitle)
|
||||
result["taskInfo"] = gvar.New(matchTask)
|
||||
result["rows"] = gvar.New(datas)
|
||||
|
||||
// 尝试从已签到的历史任务中获取历史数据用于预填充
|
||||
// 无论当前任务是否已签到,都尝试获取历史数据
|
||||
signedMatch := findMatchTitle(signedTasks)
|
||||
if signedMatch != nil {
|
||||
// 如果已签到任务与当前匹配任务是同一个,直接用已有数据作为历史(避免重复API调用)
|
||||
if matchTask != nil && signedMatch["signInstanceWid"] != nil && matchTask["signInstanceWid"] != nil &&
|
||||
signedMatch["signInstanceWid"].String() == matchTask["signInstanceWid"].String() {
|
||||
result["historyRows"] = result["rows"]
|
||||
} else {
|
||||
histResp, histErr := c.client.R().
|
||||
SetBody(map[string]*gvar.Var{
|
||||
"taskName": gvar.New(signedMatch["taskName"].String()),
|
||||
"signWid": signedMatch["signWid"],
|
||||
"signInstanceWid": signedMatch["signInstanceWid"],
|
||||
}).
|
||||
WithContext(ctx).
|
||||
Post(c.getReqUrl("detailSignInstance"))
|
||||
if histErr == nil {
|
||||
histJSON := utils.ReadJson[map[string]*gvar.Var](histResp.Body)
|
||||
histDatas := histJSON["datas"].MapStrVar()
|
||||
if len(histDatas) > 0 {
|
||||
result["historyRows"] = gvar.New(histDatas)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// 今天没有已签到的同名任务,从历史月份中只查 signedTasks
|
||||
if result["historyRows"] == nil {
|
||||
func() {
|
||||
defer func() { recover() }()
|
||||
now := time.Now().In(cst.TimeZoneShangHai)
|
||||
year := strconv.Itoa(now.Year())
|
||||
month := strconv.Itoa(int(now.Month()))
|
||||
if len(month) == 1 {
|
||||
month = "0" + month
|
||||
}
|
||||
resp, e := c.client.R().
|
||||
SetBody(map[string]any{
|
||||
"statisticYearMonth": fmt.Sprintf("%s-%s", year, month),
|
||||
}).
|
||||
WithContext(ctx).
|
||||
Post(c.getReqUrl("getStuSignInfosByWeekMonth"))
|
||||
if e != nil {
|
||||
return
|
||||
}
|
||||
respJSON := utils.ReadJson[map[string]*gvar.Var](resp.Body)
|
||||
monthDatas := respJSON["datas"].MapStrVar()
|
||||
monthRows := monthDatas["rows"].Vars()
|
||||
|
||||
// 只从 signedTasks 中查找匹配的已签到任务
|
||||
for _, row := range monthRows {
|
||||
rowMap := row.MapStrVar()
|
||||
signed := rowMap["signedTasks"].Vars()
|
||||
for _, st := range signed {
|
||||
item := st.MapStrVar()
|
||||
tn, exist := item["taskName"]
|
||||
if !exist || tn.String() == "" {
|
||||
continue
|
||||
}
|
||||
if match.Match(tn.String(), titleGlob) {
|
||||
// 找到已签到的历史任务,获取详情
|
||||
dr, de := c.client.R().
|
||||
SetBody(map[string]*gvar.Var{
|
||||
"signInstanceWid": item["signInstanceWid"],
|
||||
"signWid": item["signWid"],
|
||||
"taskName": gvar.New(tn.String()),
|
||||
}).
|
||||
WithContext(ctx).
|
||||
Post(c.getReqUrl("detailSignInstance"))
|
||||
if de != nil {
|
||||
continue
|
||||
}
|
||||
dj := utils.ReadJson[map[string]*gvar.Var](dr.Body)
|
||||
dd := dj["datas"].MapStrVar()
|
||||
if len(dd) > 0 {
|
||||
result["historyRows"] = gvar.New(dd)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
@@ -432,22 +516,77 @@ func (c *CpdailySleepCheckTask) ParseFormJson(ctx context.Context, rawJson map[s
|
||||
utils.OkOrPanic(errors.NewBusinessError(503, "任务表单为空"), oops.WithContext(ctx).Wrap)
|
||||
}
|
||||
|
||||
// 优先从 historyRows(已签到的历史任务详情)中提取历史值,否则从 rows 中提取
|
||||
histRows := rows
|
||||
if hr := rawJson["historyRows"]; hr != nil {
|
||||
if hrMap := hr.MapStrVar(); len(hrMap) > 0 {
|
||||
histRows = hrMap
|
||||
}
|
||||
}
|
||||
|
||||
var list []*formtype.CpTaskFormItem
|
||||
|
||||
// 经纬度
|
||||
dto := &formtype.CpTaskFormItem{
|
||||
Title: "经度",
|
||||
ColName: "lon",
|
||||
Type: formtype.ItemTypeInput,
|
||||
Required: true,
|
||||
}
|
||||
list = append(list, dto)
|
||||
// 从任务详情中提取历史值用于预填充
|
||||
// signPlaceSelected 包含签到地点信息(经纬度、地址)
|
||||
histLatAndLon := ""
|
||||
histAddress := ""
|
||||
histSignPhotoUrl := ""
|
||||
histIsMalposition := ""
|
||||
|
||||
dto = &formtype.CpTaskFormItem{
|
||||
Title: "纬度",
|
||||
ColName: "lat",
|
||||
Type: formtype.ItemTypeInput,
|
||||
Required: true,
|
||||
// 优先从 signPlaceSelected 提取经纬度和地址
|
||||
if sps := histRows["signPlaceSelected"]; sps != nil && sps.String() != "" && sps.String() != "[]" {
|
||||
placeArr := gjson.Parse(sps.String()).Array()
|
||||
if len(placeArr) > 0 {
|
||||
place := placeArr[0]
|
||||
lng := place.Get("longitude").String()
|
||||
lat := place.Get("latitude").String()
|
||||
addr := place.Get("address").String()
|
||||
if lng != "" && lat != "" {
|
||||
histLatAndLon = lng + "," + lat
|
||||
}
|
||||
if addr != "" {
|
||||
histAddress = addr
|
||||
}
|
||||
}
|
||||
}
|
||||
// 降级:直接从 longitude/latitude/position 字段取
|
||||
if histLatAndLon == "" {
|
||||
if lon := histRows["longitude"]; lon != nil && lon.String() != "" {
|
||||
if lat := histRows["latitude"]; lat != nil && lat.String() != "" {
|
||||
histLatAndLon = lon.String() + "," + lat.String()
|
||||
}
|
||||
}
|
||||
}
|
||||
if histAddress == "" {
|
||||
if pos := histRows["position"]; pos != nil && pos.String() != "" {
|
||||
histAddress = pos.String()
|
||||
}
|
||||
if histAddress == "" {
|
||||
if addr := histRows["signAddress"]; addr != nil && addr.String() != "" {
|
||||
histAddress = addr.String()
|
||||
}
|
||||
}
|
||||
}
|
||||
if photo := histRows["signPhotoUrl"]; photo != nil && photo.String() != "" {
|
||||
histSignPhotoUrl = photo.String()
|
||||
}
|
||||
if mal := histRows["isMalposition"]; mal != nil {
|
||||
if mal.Int() == 1 {
|
||||
histIsMalposition = "是"
|
||||
} else {
|
||||
histIsMalposition = "否"
|
||||
}
|
||||
}
|
||||
|
||||
dto := &formtype.CpTaskFormItem{
|
||||
Title: "经纬度",
|
||||
ColName: "latAndLon",
|
||||
Type: formtype.ItemTypeLatAndLon,
|
||||
Required: true,
|
||||
Content: histLatAndLon,
|
||||
Default: histLatAndLon,
|
||||
OtherValue: histAddress,
|
||||
Tip: "您可以通过地址搜索签到经纬度,如果觉得不够准确可以自行修改",
|
||||
}
|
||||
list = append(list, dto)
|
||||
dto = &formtype.CpTaskFormItem{
|
||||
@@ -455,6 +594,9 @@ func (c *CpdailySleepCheckTask) ParseFormJson(ctx context.Context, rawJson map[s
|
||||
ColName: "address",
|
||||
Type: formtype.ItemTypeInput,
|
||||
Required: true,
|
||||
Content: histAddress,
|
||||
Default: histAddress,
|
||||
Tip: "这个就是指签到完毕后查看时最底部那个最详细的地址信息哦",
|
||||
}
|
||||
list = append(list, dto)
|
||||
|
||||
@@ -464,6 +606,9 @@ func (c *CpdailySleepCheckTask) ParseFormJson(ctx context.Context, rawJson map[s
|
||||
ColName: "signImg",
|
||||
Type: formtype.ItemTypeImage,
|
||||
Required: true,
|
||||
Content: histSignPhotoUrl,
|
||||
Default: histSignPhotoUrl,
|
||||
Tip: "您可以上传多张图片,会顺序轮换更新的哦~",
|
||||
}
|
||||
list = append(list, dto)
|
||||
|
||||
@@ -475,6 +620,8 @@ func (c *CpdailySleepCheckTask) ParseFormJson(ctx context.Context, rawJson map[s
|
||||
ColName: "isMalposition",
|
||||
Type: formtype.ItemTypeSingle,
|
||||
Required: true,
|
||||
Content: histIsMalposition,
|
||||
Default: histIsMalposition,
|
||||
Options: []formtype.CpTaskFormItemOption{
|
||||
{
|
||||
Content: "是",
|
||||
@@ -486,6 +633,7 @@ func (c *CpdailySleepCheckTask) ParseFormJson(ctx context.Context, rawJson map[s
|
||||
IsOtherItems: false,
|
||||
},
|
||||
},
|
||||
Tip: "一般在学校就是选否,如果在外实习、请假之类的才选是",
|
||||
}
|
||||
list = append(list, dto)
|
||||
|
||||
@@ -514,6 +662,12 @@ func (c *CpdailySleepCheckTask) FillCustomForm(ctx context.Context, form map[str
|
||||
lon = v.Content
|
||||
case "address":
|
||||
address = v.Content
|
||||
case "latAndLon":
|
||||
{
|
||||
res := strings.Split(v.Content, ",")
|
||||
lat, lon = res[1], res[0]
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -523,8 +677,8 @@ func (c *CpdailySleepCheckTask) FillCustomForm(ctx context.Context, form map[str
|
||||
resultForm["uaIsCpadaily"] = gvar.New(true)
|
||||
|
||||
// 处理验证码
|
||||
//submitCaptchaResolver := submitcaptcha.NewRuoliResolver(c.client, c.host, deviceId, types.TaskTypeSleepCheck)
|
||||
submitCaptchaResolver := submitcaptcha.NewAIResolver(c.client, c.host, deviceId, types.TaskTypeSleepCheck)
|
||||
submitCaptchaResolver := submitcaptcha.NewNormalResolver(c.client, c.host, deviceId, types.TaskTypeSleepCheck)
|
||||
//submitCaptchaResolver := submitcaptcha.NewAIResolver(c.client, c.host, deviceId, types.TaskTypeSleepCheck)
|
||||
captchaInfo := submitCaptchaResolver.GetValidationInfo(ctx)
|
||||
|
||||
if captchaInfo.Validation {
|
||||
@@ -534,7 +688,7 @@ func (c *CpdailySleepCheckTask) FillCustomForm(ctx context.Context, form map[str
|
||||
return utils.Catch(func() {
|
||||
ticket = submitCaptchaResolver.GetCaptchaTicket(ctx, captchaInfo)
|
||||
})
|
||||
}, retry.Delay(15*time.Second), retry.Attempts(5), retry.Context(ctx))
|
||||
}, retry.Delay(10*time.Second), retry.Attempts(5), retry.Context(ctx))
|
||||
if e != nil {
|
||||
utils.OkOrPanic(errors.NewBusinessError(503, "提交验证码识别失败").WithInnerError(e), oops.WithContext(ctx).Wrap)
|
||||
}
|
||||
@@ -560,12 +714,8 @@ func (c *CpdailySleepCheckTask) FillCustomForm(ctx context.Context, form map[str
|
||||
utils.OkOrPanic(errors.NewBusinessError(503, "未找到查寝照片参数,请上传照片"), oops.WithContext(ctx).Wrap)
|
||||
}
|
||||
|
||||
imageId, e := uuid.Parse(imgId)
|
||||
utils.OkOrPanic(e, oops.WithContext(ctx).Wrap)
|
||||
|
||||
previewUrl := imageuploader.UploadImageFromStorage(ctx, c.host, imageId, types.TaskTypeSleepCheck, c.client)
|
||||
|
||||
resultForm["signPhotoUrl"] = gvar.New(previewUrl)
|
||||
// imgId 现在直接就是今日校园的 previewUrl,无需再上传
|
||||
resultForm["signPhotoUrl"] = gvar.New(imgId)
|
||||
}
|
||||
|
||||
// 判断是否存在二维码查寝
|
||||
@@ -676,3 +826,18 @@ func (c *CpdailySleepCheckTask) Submit(ctx context.Context, form map[string]*gva
|
||||
utils.OkOrPanic(errors.NewBusinessError(503, responseJSON.Get("message").String()), oops.WithContext(ctx).Wrap)
|
||||
}
|
||||
}
|
||||
|
||||
func (c *CpdailySleepCheckTask) GetYearAndMonth(ctx context.Context) (string, string) {
|
||||
// Get current time in Shanghai timezone
|
||||
timeNow := time.Now().In(cst.TimeZoneShangHai)
|
||||
|
||||
// Check if current day is within first 2 days of the month
|
||||
if timeNow.Day() <= 2 {
|
||||
// Return previous month
|
||||
prevTime := timeNow.AddDate(0, -1, 0)
|
||||
return strconv.Itoa(prevTime.Year()), strconv.Itoa(int(prevTime.Month()))
|
||||
} else {
|
||||
// Return current month
|
||||
return strconv.Itoa(timeNow.Year()), strconv.Itoa(int(timeNow.Month()))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,135 +0,0 @@
|
||||
package task
|
||||
|
||||
import (
|
||||
"context"
|
||||
"likedream-goframe/internal/testutil"
|
||||
"testing"
|
||||
|
||||
"github.com/joomcode/errorx"
|
||||
)
|
||||
|
||||
func TestCpdailySleepCheckTask_QueryFormByTitle(t *testing.T) {
|
||||
loginClient, schoolDetail := testutil.LoginTestUser()
|
||||
|
||||
task := NewSleepCheckTask(loginClient, schoolDetail.WecloudUrl)
|
||||
tasks := task.QueryFormByTitle(context.Background(), "查寝")
|
||||
|
||||
if len(tasks) == 0 {
|
||||
t.Error("未查询到查寝任务")
|
||||
} else {
|
||||
t.Logf("查询到查寝任务: %v", tasks)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCpdailySleepCheckTask_GetRecentTaskTitle(t *testing.T) {
|
||||
loginClient, schoolDetail := testutil.LoginTestUser()
|
||||
|
||||
task := NewSleepCheckTask(loginClient, schoolDetail.WecloudUrl)
|
||||
|
||||
titles := task.GetRecentTaskTitle(context.Background())
|
||||
|
||||
if len(titles) == 0 {
|
||||
t.Error("未查询到查寝任务标题")
|
||||
} else {
|
||||
t.Logf("查询到查寝任务标题: %v", titles)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCpdailySleepCheckTask_QueryFormByYearMonth(t *testing.T) {
|
||||
loginClient, schoolDetail := testutil.LoginTestUser()
|
||||
|
||||
task := NewSleepCheckTask(loginClient, schoolDetail.WecloudUrl)
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
titleGlob string
|
||||
year string
|
||||
month string
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
name: "查询当前月份查寝任务",
|
||||
titleGlob: "查寝",
|
||||
year: "2025",
|
||||
month: "6",
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "查询单数字月份查寝任务",
|
||||
titleGlob: "查寝",
|
||||
year: "2025",
|
||||
month: "6", // 将会被格式化为 "06"
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "使用通配符模式匹配",
|
||||
titleGlob: "*查寝*",
|
||||
year: "2025",
|
||||
month: "6",
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "查询不存在的任务",
|
||||
titleGlob: "不存在的任务",
|
||||
year: "2025",
|
||||
month: "6",
|
||||
wantErr: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
oopsError, _ := errorx.ErrorFromPanic(r)
|
||||
if !tt.wantErr {
|
||||
t.Errorf("QueryFormByYearMonth() 意外的panic: %+v", oopsError)
|
||||
} else {
|
||||
t.Logf("预期的错误: %+v", oopsError)
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
form := task.QueryFormByYearMonth(context.Background(), tt.titleGlob, tt.year, tt.month)
|
||||
|
||||
if tt.wantErr {
|
||||
t.Error("预期应该出现错误,但没有出现")
|
||||
return
|
||||
}
|
||||
|
||||
if form == nil {
|
||||
t.Error("未查询到查寝任务表单")
|
||||
return
|
||||
}
|
||||
|
||||
// 验证返回结果的结构
|
||||
if realTitle := form["realTitle"].String(); realTitle == "" {
|
||||
t.Error("返回结果中缺少 realTitle 或 realTitle 为空")
|
||||
} else {
|
||||
t.Logf("任务真实标题: %s", realTitle)
|
||||
}
|
||||
|
||||
if taskInfo := form["taskInfo"].Map(); taskInfo == nil {
|
||||
t.Error("返回结果中缺少 taskInfo 或 taskInfo 为空")
|
||||
} else {
|
||||
t.Logf("任务信息包含字段: %d", len(taskInfo))
|
||||
|
||||
// 验证关键字段是否存在
|
||||
requiredFields := []string{"taskName", "signWid", "signInstanceWid"}
|
||||
for _, field := range requiredFields {
|
||||
if _, exists := taskInfo[field]; !exists {
|
||||
t.Errorf("taskInfo 中缺少必要字段: %s", field)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if rows := form["rows"].Map(); rows == nil {
|
||||
t.Error("返回结果中缺少 rows 或 rows 为空")
|
||||
} else {
|
||||
t.Logf("表单数据包含字段: %d", len(rows))
|
||||
}
|
||||
|
||||
t.Logf("查询到查寝任务表单,标题: %v", form["realTitle"])
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
package task
|
||||
|
||||
import (
|
||||
"context"
|
||||
"likedream-goframe/internal/testutil"
|
||||
"testing"
|
||||
|
||||
"github.com/bytedance/sonic"
|
||||
)
|
||||
|
||||
func TestGetHistoryForm(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
loginClient, schoolDetail := testutil.LoginTestUser()
|
||||
task := NewSleepCheckTask(loginClient, schoolDetail.WecloudUrl)
|
||||
y, m := task.GetYearAndMonth(ctx)
|
||||
month := task.QueryFormByYearMonth(ctx, "晚查寝", y, m)
|
||||
marshal, _ := sonic.Marshal(month)
|
||||
t.Log(string(marshal))
|
||||
}
|
||||
@@ -6,4 +6,5 @@ const (
|
||||
TaskTypeCollection = TaskType(iota)
|
||||
TaskTypeSign
|
||||
TaskTypeSleepCheck
|
||||
TaskTypeLeave
|
||||
)
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
package httpclient
|
||||
|
||||
import (
|
||||
"resty.dev/v3"
|
||||
"likedream-goframe/internal/utils"
|
||||
"time"
|
||||
|
||||
"resty.dev/v3"
|
||||
)
|
||||
|
||||
var Common = resty.New().
|
||||
@@ -10,3 +12,11 @@ var Common = resty.New().
|
||||
SetRetryCount(5).
|
||||
SetTimeout(30*time.Second).
|
||||
SetHeader("User-Agent", "likedream")
|
||||
|
||||
func init() {
|
||||
if !utils.IsDebug() {
|
||||
Common.SetDebug(false)
|
||||
} else {
|
||||
Common.SetProxy("http://127.0.0.1:9000")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,11 +3,25 @@ package httpclient
|
||||
import (
|
||||
"crypto/tls"
|
||||
"likedream-goframe/internal/utils"
|
||||
"net/http"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
"resty.dev/v3"
|
||||
)
|
||||
|
||||
var transport = &http.Transport{
|
||||
TLSClientConfig: &tls.Config{
|
||||
InsecureSkipVerify: true,
|
||||
MinVersion: tls.VersionTLS10,
|
||||
},
|
||||
TLSHandshakeTimeout: 15 * time.Second,
|
||||
}
|
||||
|
||||
func init() {
|
||||
_ = os.Setenv("GODEBUG", "tlsrsakex=1")
|
||||
}
|
||||
|
||||
var Cpdaily = resty.New().
|
||||
SetCookieJar(nil).
|
||||
SetTimeout(30*time.Second).
|
||||
@@ -24,3 +38,23 @@ func init() {
|
||||
Cpdaily.SetProxy("http://127.0.0.1:9000")
|
||||
}
|
||||
}
|
||||
|
||||
// NewCpdailyClient 创建一个全新的独立 client 实例,避免 Clone 可能的浅拷贝问题
|
||||
// 每个实例都是完全独立的,适用于并发场景,不会共享状态
|
||||
func NewCpdailyClient() *resty.Client {
|
||||
client := resty.New().
|
||||
SetCookieJar(nil).
|
||||
SetTimeout(30*time.Second).
|
||||
SetDoNotParseResponse(true).
|
||||
SetTransport(transport).
|
||||
SetHeader("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/125.0.0.0 Safari/537.36")
|
||||
|
||||
// 根据调试模式设置代理和调试选项
|
||||
if !utils.IsDebug() {
|
||||
client.SetDebug(false)
|
||||
} else {
|
||||
client.SetProxy("http://127.0.0.1:9000")
|
||||
}
|
||||
|
||||
return client
|
||||
}
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
package httpclient
|
||||
|
||||
import (
|
||||
"crypto/tls"
|
||||
"likedream-goframe/internal/utils"
|
||||
"time"
|
||||
|
||||
"resty.dev/v3"
|
||||
)
|
||||
|
||||
func NewMogudingClient() *resty.Client {
|
||||
client := resty.New().
|
||||
SetCookieJar(nil).
|
||||
SetTimeout(30*time.Second).
|
||||
SetDoNotParseResponse(false).
|
||||
SetTLSClientConfig(&tls.Config{
|
||||
InsecureSkipVerify: true,
|
||||
}).
|
||||
SetHeader("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/125.0.0.0 Safari/537.36")
|
||||
|
||||
// 根据调试模式设置代理和调试选项
|
||||
if !utils.IsDebug() {
|
||||
client.SetDebug(false)
|
||||
} else {
|
||||
client.SetProxy("http://127.0.0.1:9000")
|
||||
}
|
||||
|
||||
return client
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
package httpclient
|
||||
|
||||
import (
|
||||
"crypto/tls"
|
||||
"likedream-goframe/internal/application/cookiejar"
|
||||
"likedream-goframe/internal/utils"
|
||||
"time"
|
||||
|
||||
"resty.dev/v3"
|
||||
)
|
||||
|
||||
var SmartEdu = resty.New().
|
||||
SetCookieJar(cookiejar.NewAllSavePersistCookieJar()).
|
||||
SetRedirectPolicy(resty.FlexibleRedirectPolicy(20)).
|
||||
SetTimeout(30 * time.Second).
|
||||
SetTLSClientConfig(&tls.Config{
|
||||
InsecureSkipVerify: true,
|
||||
})
|
||||
|
||||
func init() {
|
||||
if !utils.IsDebug() {
|
||||
Xuexitong.SetDebug(false)
|
||||
} else {
|
||||
Xuexitong.SetProxy("http://127.0.0.1:9000")
|
||||
}
|
||||
}
|
||||
|
||||
// NewSmartEdu 创建一个全新的独立 client 实例,避免 Clone 可能的浅拷贝问题
|
||||
// 每个实例都是完全独立的,适用于并发场景,不会共享状态
|
||||
func NewSmartEdu() *resty.Client {
|
||||
client := resty.New().
|
||||
SetCookieJar(nil).
|
||||
SetTimeout(30*time.Second).
|
||||
SetDoNotParseResponse(false).
|
||||
SetTLSClientConfig(&tls.Config{
|
||||
InsecureSkipVerify: true,
|
||||
}).
|
||||
SetHeader("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/125.0.0.0 Safari/537.36")
|
||||
|
||||
// 根据调试模式设置代理和调试选项
|
||||
if !utils.IsDebug() {
|
||||
client.SetDebug(false)
|
||||
} else {
|
||||
client.SetProxy("http://127.0.0.1:9000")
|
||||
}
|
||||
jar := cookiejar.NewAllSavePersistCookieJar()
|
||||
client.SetCookieJar(jar)
|
||||
return client
|
||||
}
|
||||
@@ -24,3 +24,25 @@ func init() {
|
||||
Xuexitong.SetProxy("http://127.0.0.1:9000")
|
||||
}
|
||||
}
|
||||
|
||||
// NewXuexitongClient 创建一个全新的独立 client 实例,避免 Clone 可能的浅拷贝问题
|
||||
// 每个实例都是完全独立的,适用于并发场景,不会共享状态
|
||||
func NewXuexitongClient() *resty.Client {
|
||||
client := resty.New().
|
||||
SetCookieJar(nil).
|
||||
SetTimeout(30*time.Second).
|
||||
SetDoNotParseResponse(false).
|
||||
SetTLSClientConfig(&tls.Config{
|
||||
InsecureSkipVerify: true,
|
||||
}).
|
||||
SetHeader("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/125.0.0.0 Safari/537.36")
|
||||
|
||||
// 根据调试模式设置代理和调试选项
|
||||
if !utils.IsDebug() {
|
||||
client.SetDebug(false)
|
||||
} else {
|
||||
client.SetProxy("http://127.0.0.1:9000")
|
||||
}
|
||||
|
||||
return client
|
||||
}
|
||||
|
||||
@@ -0,0 +1,47 @@
|
||||
package cryptoutil
|
||||
|
||||
import (
|
||||
"crypto/aes"
|
||||
"crypto/md5"
|
||||
"encoding/hex"
|
||||
)
|
||||
|
||||
const (
|
||||
// AES 密钥
|
||||
Secret = "23DbtQHR2UMbH6mJ"
|
||||
// MD5 签名后缀
|
||||
SignSuffix = "3478cbbc33f84bd00d75d7dfa69e0daa"
|
||||
)
|
||||
|
||||
// Encrypt 使用 AES ECB PKCS7 加密并返回十六进制字符串
|
||||
func Encrypt(plaintext string) (string, error) {
|
||||
key := []byte(Secret)
|
||||
block, err := aes.NewCipher(key)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
// PKCS7 填充
|
||||
plainBytes := []byte(plaintext)
|
||||
padding := aes.BlockSize - len(plainBytes)%aes.BlockSize
|
||||
padText := make([]byte, len(plainBytes)+padding)
|
||||
copy(padText, plainBytes)
|
||||
for i := len(plainBytes); i < len(padText); i++ {
|
||||
padText[i] = byte(padding)
|
||||
}
|
||||
|
||||
// ECB 模式加密
|
||||
encrypted := make([]byte, len(padText))
|
||||
for i := 0; i < len(padText); i += aes.BlockSize {
|
||||
block.Encrypt(encrypted[i:i+aes.BlockSize], padText[i:i+aes.BlockSize])
|
||||
}
|
||||
|
||||
return hex.EncodeToString(encrypted), nil
|
||||
}
|
||||
|
||||
// Sign2 MD5 签名
|
||||
func Sign2(str string) string {
|
||||
str += SignSuffix
|
||||
hash := md5.Sum([]byte(str))
|
||||
return hex.EncodeToString(hash[:])
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
package entity
|
||||
|
||||
// MogudingUser 蘑菇钉用户实体
|
||||
type MogudingUser struct {
|
||||
Username string `json:"username"` // 账号
|
||||
Password string `json:"password"` // 密码
|
||||
UserId string `json:"userId"`
|
||||
Token string `json:"token"`
|
||||
UserType string `json:"userType"`
|
||||
PlanId string `json:"planId"`
|
||||
PlanName string `json:"planName"`
|
||||
}
|
||||
|
||||
// LoginDto 登录请求 DTO
|
||||
type LoginDto struct {
|
||||
Phone string `json:"phone"`
|
||||
Password string `json:"password"`
|
||||
Captcha string `json:"captcha"`
|
||||
LoginType string `json:"loginType"`
|
||||
Uuid string `json:"uuid"`
|
||||
Device string `json:"device"`
|
||||
Version string `json:"version"`
|
||||
T string `json:"t"`
|
||||
}
|
||||
|
||||
// LoginResponse 登录响应
|
||||
type LoginResponse struct {
|
||||
Code int `json:"code"`
|
||||
Msg string `json:"msg"`
|
||||
Data struct {
|
||||
UserId string `json:"userId"`
|
||||
Token string `json:"token"`
|
||||
UserType string `json:"userType"`
|
||||
} `json:"data"`
|
||||
}
|
||||
|
||||
// PlanResponse 获取 PlanId 响应
|
||||
type PlanResponse struct {
|
||||
Code int `json:"code"`
|
||||
Msg string `json:"msg"`
|
||||
Data []struct {
|
||||
PlanId string `json:"planId"`
|
||||
PlanName string `json:"planName"`
|
||||
} `json:"data"`
|
||||
}
|
||||
|
||||
// SignRequest 签到请求
|
||||
type SignRequest struct {
|
||||
Country string `json:"country"`
|
||||
Address string `json:"address"`
|
||||
Province string `json:"province"`
|
||||
City string `json:"city"`
|
||||
State string `json:"state"`
|
||||
Area string `json:"area"`
|
||||
Latitude string `json:"latitude"`
|
||||
Longitude string `json:"longitude"`
|
||||
Description string `json:"description"`
|
||||
PlanId string `json:"planId"`
|
||||
Type string `json:"type"` // START 或 END
|
||||
Device string `json:"device"`
|
||||
}
|
||||
|
||||
// SignResponse 签到响应
|
||||
type SignResponse struct {
|
||||
Code int `json:"code"`
|
||||
Msg string `json:"msg"`
|
||||
Data any `json:"data"`
|
||||
}
|
||||
@@ -0,0 +1,230 @@
|
||||
package moguding
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"likedream-goframe/internal/application/httpclient"
|
||||
"likedream-goframe/internal/application/moguding/cryptoutil"
|
||||
"likedream-goframe/internal/application/moguding/entity"
|
||||
"likedream-goframe/internal/framework/logger"
|
||||
"time"
|
||||
|
||||
"go.uber.org/zap"
|
||||
"resty.dev/v3"
|
||||
)
|
||||
|
||||
const (
|
||||
// API 基础地址
|
||||
ApiBase = "https://api.moguding.net:9000"
|
||||
// 登录接口
|
||||
ApiLogin = ApiBase + "/session/user/v6/login"
|
||||
// 获取实习计划接口
|
||||
ApiGetPlan = ApiBase + "/practice/plan/v3/getPlanByStu"
|
||||
// 打卡签到接口
|
||||
ApiSign = ApiBase + "/attendence/clock/v4/save"
|
||||
)
|
||||
|
||||
type MogudingClient struct {
|
||||
client *resty.Client
|
||||
user *entity.MogudingUser
|
||||
}
|
||||
|
||||
// NewMogudingClient 创建蘑菇钉客户端
|
||||
func NewMogudingClient(ctx context.Context) *MogudingClient {
|
||||
client := httpclient.NewMogudingClient()
|
||||
|
||||
return &MogudingClient{
|
||||
client: client,
|
||||
user: &entity.MogudingUser{},
|
||||
}
|
||||
}
|
||||
|
||||
// Login 登录
|
||||
func (c *MogudingClient) Login(ctx context.Context, username, password string) error {
|
||||
if username == "" || password == "" {
|
||||
return fmt.Errorf("账号或密码不能为空")
|
||||
}
|
||||
|
||||
// 加密账号密码
|
||||
encryptedPhone, err := cryptoutil.Encrypt(username)
|
||||
if err != nil {
|
||||
logger.Logger.Error("加密账号失败", zap.Error(err))
|
||||
return err
|
||||
}
|
||||
|
||||
encryptedPassword, err := cryptoutil.Encrypt(password)
|
||||
if err != nil {
|
||||
logger.Logger.Error("加密密码失败", zap.Error(err))
|
||||
return err
|
||||
}
|
||||
|
||||
// 加密时间戳
|
||||
timestamp := fmt.Sprintf("%d", time.Now().UnixMilli())
|
||||
encryptedTime, err := cryptoutil.Encrypt(timestamp)
|
||||
if err != nil {
|
||||
logger.Logger.Error("加密时间戳失败", zap.Error(err))
|
||||
return err
|
||||
}
|
||||
|
||||
// 构造登录请求
|
||||
loginDto := entity.LoginDto{
|
||||
Phone: encryptedPhone,
|
||||
Password: encryptedPassword,
|
||||
T: encryptedTime,
|
||||
LoginType: "android",
|
||||
}
|
||||
|
||||
// 发送登录请求
|
||||
resp, err := c.client.R().
|
||||
SetBody(loginDto).
|
||||
Post(ApiLogin)
|
||||
|
||||
if err != nil {
|
||||
logger.Logger.Error("登录请求失败", zap.Error(err))
|
||||
return err
|
||||
}
|
||||
|
||||
// 解析响应
|
||||
var loginResp entity.LoginResponse
|
||||
if err := json.Unmarshal(resp.Bytes(), &loginResp); err != nil {
|
||||
logger.Logger.Error("解析登录响应失败", zap.Error(err))
|
||||
return err
|
||||
}
|
||||
|
||||
if loginResp.Code != 200 {
|
||||
return fmt.Errorf("登录失败:%s", loginResp.Msg)
|
||||
}
|
||||
|
||||
// 保存用户信息
|
||||
c.user.Username = username
|
||||
c.user.Password = password
|
||||
c.user.UserId = loginResp.Data.UserId
|
||||
c.user.Token = loginResp.Data.Token
|
||||
c.user.UserType = loginResp.Data.UserType
|
||||
|
||||
logger.Logger.Info("登录成功",
|
||||
zap.String("userId", c.user.UserId),
|
||||
zap.String("userType", c.user.UserType))
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetPlanId 获取实习计划 ID
|
||||
func (c *MogudingClient) GetPlanId(ctx context.Context) error {
|
||||
if c.user.Token == "" {
|
||||
return fmt.Errorf("请先登录")
|
||||
}
|
||||
|
||||
// 计算签名
|
||||
signStr := c.user.UserId + c.user.UserType
|
||||
sign := cryptoutil.Sign2(signStr)
|
||||
|
||||
// 发送请求
|
||||
resp, err := c.client.R().
|
||||
SetHeaders(map[string]string{
|
||||
"roleKey": c.user.UserType,
|
||||
"sign": sign,
|
||||
"authorization": c.user.Token,
|
||||
}).
|
||||
SetBody(map[string]string{"state": ""}).
|
||||
Post(ApiGetPlan)
|
||||
|
||||
if err != nil {
|
||||
logger.Logger.Error("获取 PlanId 请求失败", zap.Error(err))
|
||||
return err
|
||||
}
|
||||
|
||||
// 解析响应
|
||||
var planResp entity.PlanResponse
|
||||
if err := json.Unmarshal(resp.Bytes(), &planResp); err != nil {
|
||||
logger.Logger.Error("解析 PlanId 响应失败", zap.Error(err))
|
||||
return err
|
||||
}
|
||||
|
||||
if planResp.Code != 200 {
|
||||
return fmt.Errorf("获取 PlanId 失败:%s", planResp.Msg)
|
||||
}
|
||||
|
||||
if len(planResp.Data) == 0 {
|
||||
return fmt.Errorf("未找到实习计划")
|
||||
}
|
||||
|
||||
// 保存第一个计划信息
|
||||
c.user.PlanId = planResp.Data[0].PlanId
|
||||
c.user.PlanName = planResp.Data[0].PlanName
|
||||
|
||||
logger.Logger.Info("获取 PlanId 成功",
|
||||
zap.String("planId", c.user.PlanId),
|
||||
zap.String("planName", c.user.PlanName))
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Sign 打卡签到
|
||||
func (c *MogudingClient) Sign(ctx context.Context, signReq *entity.SignRequest) error {
|
||||
if c.user.Token == "" {
|
||||
return fmt.Errorf("请先登录")
|
||||
}
|
||||
|
||||
if c.user.PlanId == "" {
|
||||
return fmt.Errorf("请先获取实习计划")
|
||||
}
|
||||
|
||||
// 设置默认值
|
||||
if signReq.PlanId == "" {
|
||||
signReq.PlanId = c.user.PlanId
|
||||
}
|
||||
if signReq.Device == "" {
|
||||
signReq.Device = "Android"
|
||||
}
|
||||
if signReq.Type == "" {
|
||||
signReq.Type = "START"
|
||||
}
|
||||
|
||||
// 计算签名
|
||||
signStr := signReq.Device + signReq.Type + signReq.PlanId + c.user.UserId + signReq.Address
|
||||
sign := cryptoutil.Sign2(signStr)
|
||||
|
||||
// 发送请求
|
||||
resp, err := c.client.R().
|
||||
SetHeaders(map[string]string{
|
||||
"roleKey": c.user.UserType,
|
||||
"sign": sign,
|
||||
"authorization": c.user.Token,
|
||||
}).
|
||||
SetBody(signReq).
|
||||
Post(ApiSign)
|
||||
|
||||
if err != nil {
|
||||
logger.Logger.Error("签到请求失败", zap.Error(err))
|
||||
return err
|
||||
}
|
||||
|
||||
// 解析响应
|
||||
var signResp entity.SignResponse
|
||||
if err := json.Unmarshal(resp.Bytes(), &signResp); err != nil {
|
||||
logger.Logger.Error("解析签到响应失败", zap.Error(err))
|
||||
return err
|
||||
}
|
||||
|
||||
if signResp.Code != 200 {
|
||||
return fmt.Errorf("签到失败:%s", signResp.Msg)
|
||||
}
|
||||
|
||||
logger.Logger.Info("签到成功",
|
||||
zap.String("type", signReq.Type),
|
||||
zap.String("address", signReq.Address))
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetUser 获取用户信息
|
||||
func (c *MogudingClient) GetUser() *entity.MogudingUser {
|
||||
return c.user
|
||||
}
|
||||
|
||||
// SetUser 设置用户信息(用于恢复登录状态)
|
||||
func (c *MogudingClient) SetUser(user *entity.MogudingUser) {
|
||||
c.user = user
|
||||
}
|
||||
@@ -0,0 +1,157 @@
|
||||
package moguding
|
||||
|
||||
import (
|
||||
"context"
|
||||
"likedream-goframe/internal/application/moguding/entity"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestMogudingLogin(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
client := NewMogudingClient(ctx)
|
||||
|
||||
// 替换为实际的测试账号
|
||||
username := "13307425047"
|
||||
password := "wgz2021322L"
|
||||
|
||||
err := client.Login(ctx, username, password)
|
||||
if err != nil {
|
||||
t.Fatalf("登录失败: %v", err)
|
||||
}
|
||||
|
||||
t.Logf("登录成功,UserId: %s, Token: %s", client.user.UserId, client.user.Token)
|
||||
}
|
||||
|
||||
func TestMogudingGetPlanId(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
client := NewMogudingClient(ctx)
|
||||
|
||||
// 先登录
|
||||
username := "your_username"
|
||||
password := "your_password"
|
||||
|
||||
err := client.Login(ctx, username, password)
|
||||
if err != nil {
|
||||
t.Fatalf("登录失败: %v", err)
|
||||
}
|
||||
|
||||
// 获取 PlanId
|
||||
err = client.GetPlanId(ctx)
|
||||
if err != nil {
|
||||
t.Fatalf("获取 PlanId 失败: %v", err)
|
||||
}
|
||||
|
||||
t.Logf("获取 PlanId 成功,PlanId: %s, PlanName: %s", client.user.PlanId, client.user.PlanName)
|
||||
}
|
||||
|
||||
func TestMogudingSign(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
client := NewMogudingClient(ctx)
|
||||
|
||||
// 先登录
|
||||
username := "your_username"
|
||||
password := "your_password"
|
||||
|
||||
err := client.Login(ctx, username, password)
|
||||
if err != nil {
|
||||
t.Fatalf("登录失败: %v", err)
|
||||
}
|
||||
|
||||
// 获取 PlanId
|
||||
err = client.GetPlanId(ctx)
|
||||
if err != nil {
|
||||
t.Fatalf("获取 PlanId 失败: %v", err)
|
||||
}
|
||||
|
||||
// 打卡签到
|
||||
signReq := &entity.SignRequest{
|
||||
Country: "中国",
|
||||
Address: "四川省 · 成都市 · 武侯区 · 咕噜咕噜",
|
||||
Province: "四川省",
|
||||
City: "成都市",
|
||||
State: "1",
|
||||
Area: "武侯区",
|
||||
Latitude: "33.100039",
|
||||
Longitude: "101.987848",
|
||||
Description: "测试打卡",
|
||||
Type: "START", // START 表示上班打卡,END 表示下班打卡
|
||||
Device: "Android",
|
||||
}
|
||||
|
||||
err = client.Sign(ctx, signReq)
|
||||
if err != nil {
|
||||
t.Fatalf("打卡失败: %v", err)
|
||||
}
|
||||
|
||||
t.Logf("打卡成功")
|
||||
}
|
||||
|
||||
func TestMogudingFullProcess(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
client := NewMogudingClient(ctx)
|
||||
|
||||
// 1. 登录
|
||||
username := "your_username"
|
||||
password := "your_password"
|
||||
|
||||
t.Log("步骤1: 登录")
|
||||
err := client.Login(ctx, username, password)
|
||||
if err != nil {
|
||||
t.Fatalf("登录失败: %v", err)
|
||||
}
|
||||
t.Logf("登录成功,UserId: %s", client.user.UserId)
|
||||
|
||||
// 2. 获取 PlanId
|
||||
t.Log("步骤2: 获取实习计划")
|
||||
err = client.GetPlanId(ctx)
|
||||
if err != nil {
|
||||
t.Fatalf("获取 PlanId 失败: %v", err)
|
||||
}
|
||||
t.Logf("获取 PlanId 成功,PlanId: %s, PlanName: %s", client.user.PlanId, client.user.PlanName)
|
||||
|
||||
// 3. 上班打卡
|
||||
t.Log("步骤3: 上班打卡")
|
||||
signInReq := &entity.SignRequest{
|
||||
Country: "中国",
|
||||
Address: "四川省 · 成都市 · 武侯区 · 咕噜咕噜",
|
||||
Province: "四川省",
|
||||
City: "成都市",
|
||||
State: "1",
|
||||
Area: "武侯区",
|
||||
Latitude: "33.100039",
|
||||
Longitude: "101.987848",
|
||||
Description: "上班打卡",
|
||||
Type: "START",
|
||||
Device: "Android",
|
||||
}
|
||||
|
||||
err = client.Sign(ctx, signInReq)
|
||||
if err != nil {
|
||||
t.Fatalf("上班打卡失败: %v", err)
|
||||
}
|
||||
t.Log("上班打卡成功")
|
||||
|
||||
// 4. 下班打卡(可选)
|
||||
t.Log("步骤4: 下班打卡")
|
||||
signOutReq := &entity.SignRequest{
|
||||
Country: "中国",
|
||||
Address: "四川省 · 成都市 · 武侯区 · 咕噜咕噜",
|
||||
Province: "四川省",
|
||||
City: "成都市",
|
||||
State: "1",
|
||||
Area: "武侯区",
|
||||
Latitude: "33.100039",
|
||||
Longitude: "101.987848",
|
||||
Description: "下班打卡",
|
||||
Type: "END",
|
||||
Device: "Android",
|
||||
}
|
||||
|
||||
err = client.Sign(ctx, signOutReq)
|
||||
if err != nil {
|
||||
t.Fatalf("下班打卡失败: %v", err)
|
||||
}
|
||||
t.Log("下班打卡成功")
|
||||
|
||||
t.Log("完整流程测试通过!")
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
package music
|
||||
|
||||
import (
|
||||
"testing"
|
||||
)
|
||||
|
||||
func Test_GetMusic(t *testing.T) {
|
||||
ctx := t.Context()
|
||||
result := MusicDownloader.Search(ctx, "天外来物")
|
||||
t.Log(result)
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
package quesbank
|
||||
|
||||
// AIType AI类型
|
||||
type AIType string
|
||||
|
||||
const (
|
||||
AITypeChatGLM AIType = "chatglm" // 智谱AI
|
||||
AITypeXingHuo AIType = "xinghuo" // 讯飞星火
|
||||
AITypeTongYi AIType = "tongyi" // 通义千问
|
||||
AITypeDouBao AIType = "doubao" // 豆包
|
||||
AITypeOpenAI AIType = "openai" // OpenAI
|
||||
AITypeOther AIType = "other" // 其他兼容OpenAI格式的API
|
||||
)
|
||||
|
||||
// AIConfig AI配置
|
||||
type AIConfig struct {
|
||||
Type AIType `json:"type"` // AI类型
|
||||
URL string `json:"url"` // API地址(Other类型必填)
|
||||
Model string `json:"model"` // 模型名称
|
||||
APIKey string `json:"apiKey"` // API密钥
|
||||
}
|
||||
|
||||
// AIChatMessage 聊天消息
|
||||
type AIChatMessage struct {
|
||||
Role string `json:"role"`
|
||||
Content string `json:"content"`
|
||||
}
|
||||
|
||||
// AIChatMessages 聊天消息列表
|
||||
type AIChatMessages struct {
|
||||
Messages []AIChatMessage `json:"messages"`
|
||||
}
|
||||
@@ -0,0 +1,173 @@
|
||||
package quesbank
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
// AIAnswerService AI答题服务
|
||||
type AIAnswerService struct {
|
||||
config AIConfig
|
||||
mu sync.Mutex
|
||||
}
|
||||
|
||||
// NewAIAnswerService 创建AI答题服务
|
||||
func NewAIAnswerService(config AIConfig) *AIAnswerService {
|
||||
return &AIAnswerService{
|
||||
config: config,
|
||||
}
|
||||
}
|
||||
|
||||
// GetAnswer 获取AI答案
|
||||
func (s *AIAnswerService) GetAnswer(ctx context.Context, messages AIChatMessages) (string, error) {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
|
||||
switch s.config.Type {
|
||||
case AITypeChatGLM:
|
||||
return s.chatGLMRequest(ctx, messages)
|
||||
case AITypeXingHuo:
|
||||
return s.xingHuoRequest(ctx, messages)
|
||||
case AITypeTongYi:
|
||||
return s.tongYiRequest(ctx, messages)
|
||||
case AITypeDouBao:
|
||||
return s.douBaoRequest(ctx, messages)
|
||||
case AITypeOpenAI:
|
||||
return s.openAIRequest(ctx, messages)
|
||||
case AITypeOther:
|
||||
return s.otherRequest(ctx, messages)
|
||||
default:
|
||||
return "", fmt.Errorf("unsupported AI type: %s", s.config.Type)
|
||||
}
|
||||
}
|
||||
|
||||
// chatGLMRequest 智谱AI请求
|
||||
func (s *AIAnswerService) chatGLMRequest(ctx context.Context, messages AIChatMessages) (string, error) {
|
||||
url := "https://open.bigmodel.cn/api/paas/v4/chat/completions"
|
||||
model := s.config.Model
|
||||
if model == "" {
|
||||
model = "glm-4"
|
||||
}
|
||||
return s.doOpenAIStyleRequest(ctx, url, model, messages)
|
||||
}
|
||||
|
||||
// xingHuoRequest 讯飞星火请求
|
||||
func (s *AIAnswerService) xingHuoRequest(ctx context.Context, messages AIChatMessages) (string, error) {
|
||||
url := "https://spark-api-open.xf-yun.com/v1/chat/completions"
|
||||
model := s.config.Model
|
||||
if model == "" {
|
||||
model = "generalv3.5"
|
||||
}
|
||||
return s.doOpenAIStyleRequest(ctx, url, model, messages)
|
||||
}
|
||||
|
||||
// tongYiRequest 通义千问请求
|
||||
func (s *AIAnswerService) tongYiRequest(ctx context.Context, messages AIChatMessages) (string, error) {
|
||||
url := "https://dashscope.aliyuncs.com/compatible-mode/v1/chat/completions"
|
||||
model := s.config.Model
|
||||
if model == "" {
|
||||
model = "qwen-plus"
|
||||
}
|
||||
return s.doOpenAIStyleRequest(ctx, url, model, messages)
|
||||
}
|
||||
|
||||
// douBaoRequest 豆包请求
|
||||
func (s *AIAnswerService) douBaoRequest(ctx context.Context, messages AIChatMessages) (string, error) {
|
||||
url := "https://ark.cn-beijing.volces.com/api/v3/chat/completions"
|
||||
return s.doOpenAIStyleRequest(ctx, url, s.config.Model, messages)
|
||||
}
|
||||
|
||||
// openAIRequest OpenAI请求
|
||||
func (s *AIAnswerService) openAIRequest(ctx context.Context, messages AIChatMessages) (string, error) {
|
||||
url := "https://api.openai.com/v1/chat/completions"
|
||||
model := s.config.Model
|
||||
if model == "" {
|
||||
model = "gpt-3.5-turbo"
|
||||
}
|
||||
return s.doOpenAIStyleRequest(ctx, url, model, messages)
|
||||
}
|
||||
|
||||
// otherRequest 其他兼容OpenAI格式的API请求
|
||||
func (s *AIAnswerService) otherRequest(ctx context.Context, messages AIChatMessages) (string, error) {
|
||||
if s.config.URL == "" {
|
||||
return "", errors.New("API URL is required for 'other' AI type")
|
||||
}
|
||||
return s.doOpenAIStyleRequest(ctx, s.config.URL, s.config.Model, messages)
|
||||
}
|
||||
|
||||
// doOpenAIStyleRequest 执行OpenAI风格的API请求
|
||||
func (s *AIAnswerService) doOpenAIStyleRequest(ctx context.Context, url, model string, messages AIChatMessages) (string, error) {
|
||||
requestBody := map[string]interface{}{
|
||||
"model": model,
|
||||
"messages": messages.Messages,
|
||||
}
|
||||
|
||||
jsonData, err := json.Marshal(requestBody)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to marshal request: %v", err)
|
||||
}
|
||||
|
||||
var lastErr error
|
||||
for retry := 0; retry < 3; retry++ {
|
||||
result, err := s.doHTTPRequest(ctx, url, jsonData)
|
||||
if err == nil {
|
||||
return result, nil
|
||||
}
|
||||
lastErr = err
|
||||
time.Sleep(100 * time.Millisecond)
|
||||
}
|
||||
return "", lastErr
|
||||
}
|
||||
|
||||
// doHTTPRequest 执行HTTP请求
|
||||
func (s *AIAnswerService) doHTTPRequest(ctx context.Context, url string, jsonData []byte) (string, error) {
|
||||
client := &http.Client{Timeout: 60 * time.Second}
|
||||
|
||||
req, err := http.NewRequestWithContext(ctx, "POST", url, bytes.NewBuffer(jsonData))
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to create request: %v", err)
|
||||
}
|
||||
|
||||
req.Header.Set("Authorization", "Bearer "+s.config.APIKey)
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to execute request: %v", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
body, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to read response: %v", err)
|
||||
}
|
||||
|
||||
var responseMap map[string]interface{}
|
||||
if err := json.Unmarshal(body, &responseMap); err != nil {
|
||||
return "", fmt.Errorf("failed to parse response: %v, body: %s", err, string(body))
|
||||
}
|
||||
|
||||
choices, ok := responseMap["choices"].([]interface{})
|
||||
if !ok || len(choices) == 0 {
|
||||
return "", fmt.Errorf("AI response not found, response: %s", string(body))
|
||||
}
|
||||
|
||||
message, ok := choices[0].(map[string]interface{})["message"].(map[string]interface{})
|
||||
if !ok {
|
||||
return "", errors.New("failed to parse message from response")
|
||||
}
|
||||
|
||||
content, ok := message["content"].(string)
|
||||
if !ok {
|
||||
return "", errors.New("content field missing in response")
|
||||
}
|
||||
|
||||
return content, nil
|
||||
}
|
||||
@@ -0,0 +1,97 @@
|
||||
package quesbank
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// ParseAIAnswer 解析AI答案
|
||||
func ParseAIAnswer(aiResponse string) ([]string, error) {
|
||||
aiResponse = strings.TrimSpace(aiResponse)
|
||||
|
||||
// 尝试直接解析JSON数组
|
||||
var answers []string
|
||||
if err := json.Unmarshal([]byte(aiResponse), &answers); err == nil {
|
||||
return answers, nil
|
||||
}
|
||||
|
||||
// 尝试从响应中提取JSON数组
|
||||
start := strings.Index(aiResponse, "[")
|
||||
end := strings.LastIndex(aiResponse, "]")
|
||||
if start != -1 && end != -1 && end > start {
|
||||
jsonStr := aiResponse[start : end+1]
|
||||
if err := json.Unmarshal([]byte(jsonStr), &answers); err == nil {
|
||||
return answers, nil
|
||||
}
|
||||
}
|
||||
|
||||
// 如果都失败,返回原始响应作为单个答案
|
||||
return []string{aiResponse}, nil
|
||||
}
|
||||
|
||||
// MatchAnswerToOption 将AI答案匹配到选项
|
||||
func MatchAnswerToOption(answer string, options map[string]string) string {
|
||||
// 精确匹配
|
||||
for key, val := range options {
|
||||
if val == answer || strings.TrimSpace(val) == strings.TrimSpace(answer) {
|
||||
return key
|
||||
}
|
||||
}
|
||||
|
||||
// 模糊匹配(包含关系)
|
||||
for key, val := range options {
|
||||
if strings.Contains(val, answer) || strings.Contains(answer, val) {
|
||||
return key
|
||||
}
|
||||
}
|
||||
|
||||
// 相似度匹配
|
||||
bestKey := ""
|
||||
bestScore := 0.0
|
||||
for key, val := range options {
|
||||
score := similarity(answer, val)
|
||||
if score > bestScore {
|
||||
bestScore = score
|
||||
bestKey = key
|
||||
}
|
||||
}
|
||||
if bestScore >= 0.5 {
|
||||
return bestKey
|
||||
}
|
||||
|
||||
return ""
|
||||
}
|
||||
|
||||
// similarity 计算字符串相似度(简单实现)
|
||||
func similarity(s1, s2 string) float64 {
|
||||
if s1 == s2 {
|
||||
return 1.0
|
||||
}
|
||||
if len(s1) == 0 || len(s2) == 0 {
|
||||
return 0.0
|
||||
}
|
||||
|
||||
// 使用最长公共子序列计算相似度
|
||||
lcs := longestCommonSubsequence(s1, s2)
|
||||
return float64(lcs) / float64(max(len(s1), len(s2)))
|
||||
}
|
||||
|
||||
// longestCommonSubsequence 计算最长公共子序列长度
|
||||
func longestCommonSubsequence(s1, s2 string) int {
|
||||
m, n := len(s1), len(s2)
|
||||
dp := make([][]int, m+1)
|
||||
for i := range dp {
|
||||
dp[i] = make([]int, n+1)
|
||||
}
|
||||
|
||||
for i := 1; i <= m; i++ {
|
||||
for j := 1; j <= n; j++ {
|
||||
if s1[i-1] == s2[j-1] {
|
||||
dp[i][j] = dp[i-1][j-1] + 1
|
||||
} else {
|
||||
dp[i][j] = max(dp[i-1][j], dp[i][j-1])
|
||||
}
|
||||
}
|
||||
}
|
||||
return dp[m][n]
|
||||
}
|
||||
@@ -0,0 +1,151 @@
|
||||
package quesbank
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// BuildQuestionPrompt 构建题目提示词
|
||||
func BuildQuestionPrompt(question Question) AIChatMessages {
|
||||
var messages []AIChatMessage
|
||||
|
||||
switch question.Type {
|
||||
case QuestionTypeSingleChoice:
|
||||
messages = buildSingleChoicePrompt(question)
|
||||
case QuestionTypeMultipleChoice:
|
||||
messages = buildMultipleChoicePrompt(question)
|
||||
case QuestionTypeTrueFalse:
|
||||
messages = buildTrueFalsePrompt(question)
|
||||
case QuestionTypeFillBlank:
|
||||
messages = buildFillBlankPrompt(question)
|
||||
case QuestionTypeShortAnswer:
|
||||
messages = buildShortAnswerPrompt(question)
|
||||
default:
|
||||
messages = buildDefaultPrompt(question)
|
||||
}
|
||||
|
||||
return AIChatMessages{Messages: messages}
|
||||
}
|
||||
|
||||
// buildSingleChoicePrompt 构建单选题提示词
|
||||
func buildSingleChoicePrompt(q Question) []AIChatMessage {
|
||||
context := buildQuestionContext(q)
|
||||
return []AIChatMessage{
|
||||
{Role: "user", Content: "接下来你只需要回答选项对应内容即可...格式:[\"选项1\"]"},
|
||||
{Role: "user", Content: "就算你不知道选什么也随机选...无需回答任何解释!!!"},
|
||||
{Role: "user", Content: singleChoiceExample()},
|
||||
{Role: "user", Content: fmt.Sprintf("题目类型:单选题\n题目内容:%s", context)},
|
||||
}
|
||||
}
|
||||
|
||||
// buildMultipleChoicePrompt 构建多选题提示词
|
||||
func buildMultipleChoicePrompt(q Question) []AIChatMessage {
|
||||
context := buildQuestionContext(q)
|
||||
return []AIChatMessage{
|
||||
{Role: "user", Content: "接下来你只需要回答选项对应内容即可...格式:[\"选项1\",\"选项2\"]"},
|
||||
{Role: "user", Content: "就算你不知道选什么也随机选...无需回答任何解释!!!"},
|
||||
{Role: "user", Content: multipleChoiceExample()},
|
||||
{Role: "user", Content: fmt.Sprintf("题目类型:多选题\n题目内容:%s", context)},
|
||||
}
|
||||
}
|
||||
|
||||
// buildTrueFalsePrompt 构建判断题提示词
|
||||
func buildTrueFalsePrompt(q Question) []AIChatMessage {
|
||||
context := buildQuestionContext(q)
|
||||
return []AIChatMessage{
|
||||
{Role: "user", Content: "接下来你只需要回答\"正确\"或者\"错误\"即可...格式:[\"正确\"]"},
|
||||
{Role: "user", Content: "就算你不知道选什么也随机选...无需回答任何解释!!!"},
|
||||
{Role: "user", Content: trueFalseExample()},
|
||||
{Role: "user", Content: fmt.Sprintf("题目类型:判断题\n题目内容:%s", context)},
|
||||
}
|
||||
}
|
||||
|
||||
// buildFillBlankPrompt 构建填空题提示词
|
||||
func buildFillBlankPrompt(q Question) []AIChatMessage {
|
||||
return []AIChatMessage{
|
||||
{Role: "user", Content: "其中,\"( )\"相关字样的地方是你需要填写答案的地方...格式:[\"答案1\",\"答案2\"]"},
|
||||
{Role: "user", Content: "就算你不知道填什么也随机填...无需回答任何解释!!!"},
|
||||
{Role: "user", Content: fillBlankExample()},
|
||||
{Role: "user", Content: fmt.Sprintf("题目类型:填空题\n题目内容:%s", q.Text)},
|
||||
}
|
||||
}
|
||||
|
||||
// buildShortAnswerPrompt 构建简答题提示词
|
||||
func buildShortAnswerPrompt(q Question) []AIChatMessage {
|
||||
return []AIChatMessage{
|
||||
{Role: "user", Content: "这是一个简答题...格式:[\"答案\"],注意不要拆分答案!!!"},
|
||||
{Role: "user", Content: "就算你不知道答什么也随机答...无需回答任何解释!!!"},
|
||||
{Role: "user", Content: shortAnswerExample()},
|
||||
{Role: "user", Content: fmt.Sprintf("题目类型:简答题\n题目内容:%s", q.Text)},
|
||||
}
|
||||
}
|
||||
|
||||
// buildDefaultPrompt 构建默认提示词
|
||||
func buildDefaultPrompt(q Question) []AIChatMessage {
|
||||
context := buildQuestionContext(q)
|
||||
return []AIChatMessage{
|
||||
{Role: "user", Content: "请回答以下题目,格式:[\"答案\"]"},
|
||||
{Role: "user", Content: context},
|
||||
}
|
||||
}
|
||||
|
||||
// buildQuestionContext 构建题目上下文
|
||||
func buildQuestionContext(q Question) string {
|
||||
var sb strings.Builder
|
||||
sb.WriteString(q.Text)
|
||||
sb.WriteString("\n")
|
||||
for key, val := range q.Options {
|
||||
sb.WriteString(fmt.Sprintf("%s. %s\n", key, val))
|
||||
}
|
||||
return sb.String()
|
||||
}
|
||||
|
||||
func singleChoiceExample() string {
|
||||
return `比如:
|
||||
题目类型:单选题
|
||||
题目内容:新中国是什么时候成立的
|
||||
A. 1949年10月5日
|
||||
B. 1949年10月1日
|
||||
C. 1949年09月1日
|
||||
D. 2002年10月1日
|
||||
|
||||
那么你应该回答选项B的内容:"["1949年10月1日"]"`
|
||||
}
|
||||
|
||||
func multipleChoiceExample() string {
|
||||
return `比如:
|
||||
题目类型:多选题
|
||||
题目内容:马克思关于资本积累的学说是剩余价值理论的重要组成部分...
|
||||
A. 资本主义扩大再生产的源泉
|
||||
B. 资本有机构成呈现不断降低趋势的根本原因
|
||||
C. 社会财富占有两极分化的重要原因
|
||||
D. 资本主义社会失业现象产生的根源
|
||||
|
||||
那么你应该回答选项A、C、D的内容:"["资本主义扩大再生产的源泉","社会财富占有两极分化的重要原因","资本主义社会失业现象产生的根源"]"`
|
||||
}
|
||||
|
||||
func trueFalseExample() string {
|
||||
return `比如:
|
||||
题目类型:判断题
|
||||
题目内容:新中国是1949年10月1日成立的吗?
|
||||
A. 正确
|
||||
B. 错误
|
||||
|
||||
那么你应该回答选项A的内容:"["正确"]"`
|
||||
}
|
||||
|
||||
func fillBlankExample() string {
|
||||
return `比如:
|
||||
题目类型:填空题
|
||||
题目内容:新中国成立于( )年。
|
||||
|
||||
那么你应该回答:"["1949"]"`
|
||||
}
|
||||
|
||||
func shortAnswerExample() string {
|
||||
return `比如:
|
||||
题目类型:简答题
|
||||
题目内容:请简述新中国成立的历史意义
|
||||
|
||||
那么你应该回答:"["新中国的成立标志着中国人民从此站起来了,结束了半殖民地半封建社会的历史..."]"`
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
package quesbank
|
||||
@@ -0,0 +1,23 @@
|
||||
package quesbank
|
||||
|
||||
// QuestionType 题目类型
|
||||
type QuestionType int
|
||||
|
||||
const (
|
||||
QuestionTypeSingleChoice QuestionType = 0 // 单选题
|
||||
QuestionTypeMultipleChoice QuestionType = 1 // 多选题
|
||||
QuestionTypeFillBlank QuestionType = 2 // 填空题
|
||||
QuestionTypeTrueFalse QuestionType = 3 // 判断题
|
||||
QuestionTypeShortAnswer QuestionType = 4 // 简答题
|
||||
)
|
||||
|
||||
// Question 通用题目结构
|
||||
type Question struct {
|
||||
Qid string `json:"qid"` // 题目ID
|
||||
Type QuestionType `json:"type"` // 题目类型
|
||||
Text string `json:"text"` // 题目内容
|
||||
Options map[string]string `json:"options"` // 选项 (A: xxx, B: xxx)
|
||||
Answers []string `json:"answers"` // 答案
|
||||
BlankCount int `json:"blankCount"` // 填空题空格数量
|
||||
BlankAnswer map[string]string `json:"blankAnswer"` // 填空题答案
|
||||
}
|
||||
@@ -1,196 +0,0 @@
|
||||
package classifier
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"fmt"
|
||||
_ "image/jpeg"
|
||||
_ "image/png"
|
||||
"os"
|
||||
|
||||
ort "github.com/yalue/onnxruntime_go"
|
||||
)
|
||||
|
||||
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) {
|
||||
// 加载类别名称
|
||||
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,8 +0,0 @@
|
||||
package classifier
|
||||
|
||||
// ClassifyImage 图片数据 & 类别名称
|
||||
// return 是否匹配
|
||||
func ClassifyImage(imgData []byte, className string) bool {
|
||||
name, _, _ := imageClassifier.PredictFromBytes(imgData)
|
||||
return className == name
|
||||
}
|
||||
@@ -1,53 +0,0 @@
|
||||
package classifier
|
||||
|
||||
import (
|
||||
"embed"
|
||||
_ "embed"
|
||||
"likedream-goframe/internal/application/recognizer"
|
||||
"likedream-goframe/internal/framework/logger"
|
||||
"likedream-goframe/internal/utils"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
//go:embed static/*
|
||||
var fs embed.FS
|
||||
var imageClassifier *ImageClassifier
|
||||
|
||||
func init() {
|
||||
if !recognizer.OnnxIsInit {
|
||||
recognizer.InitOnnx()
|
||||
}
|
||||
modelPath, classesPath := initModel()
|
||||
classifier, err := NewImageClassifier(modelPath, classesPath)
|
||||
if err != nil {
|
||||
logger.Logger.Panic("初始化ai模型失败", zap.Error(err))
|
||||
}
|
||||
imageClassifier = classifier
|
||||
}
|
||||
func initModel() (modelPath, classesPath string) {
|
||||
// 检测文件是否存在
|
||||
exePath := strings.ToLower(os.Args[0])
|
||||
// 获取绝对路径
|
||||
absPath, _ := filepath.Abs(exePath)
|
||||
staticPath := filepath.Join(filepath.Dir(absPath), "static")
|
||||
if _, err := os.Stat(staticPath); os.IsNotExist(err) {
|
||||
os.Mkdir(staticPath, 0755)
|
||||
}
|
||||
modelPath = filepath.Join(staticPath, "model.onnx")
|
||||
modelBytes, err := fs.ReadFile("static/model.onnx")
|
||||
utils.OkOrPanic(err)
|
||||
if _, err := os.Stat(modelPath); os.IsNotExist(err) {
|
||||
os.WriteFile(modelPath, modelBytes, 0644)
|
||||
}
|
||||
classesPath = filepath.Join(staticPath, "classes.txt")
|
||||
classesBytes, err := fs.ReadFile("static/classes.txt")
|
||||
utils.OkOrPanic(err)
|
||||
if _, err := os.Stat(classesPath); os.IsNotExist(err) {
|
||||
os.WriteFile(classesPath, classesBytes, 0644)
|
||||
}
|
||||
return
|
||||
}
|
||||
@@ -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,23 +0,0 @@
|
||||
公交车
|
||||
向日葵
|
||||
多肉
|
||||
摩托车
|
||||
樱桃
|
||||
汽车
|
||||
满天星
|
||||
狗
|
||||
狮子
|
||||
猫
|
||||
老虎
|
||||
自行车
|
||||
芒果
|
||||
苹果
|
||||
荷花
|
||||
葡萄
|
||||
西瓜
|
||||
豹子
|
||||
长颈鹿
|
||||
青桔
|
||||
飞机
|
||||
香蕉
|
||||
高铁
|
||||
Binary file not shown.
@@ -1,109 +0,0 @@
|
||||
package ddddocr
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
"github.com/thedevsaddam/gojsonq"
|
||||
ort "github.com/yalue/onnxruntime_go"
|
||||
"image"
|
||||
"log"
|
||||
"strconv"
|
||||
)
|
||||
|
||||
// SemiOCRVerification 半自动验证码识别(要自己传入outputShape值)
|
||||
func SemiOCRVerification(img image.Image, outputShape ort.Shape) string {
|
||||
img1 := ResizeImage(img, uint(64*img.Bounds().Dx()/img.Bounds().Dy()), 64)
|
||||
imgGray := ConvertToGray(img1)
|
||||
|
||||
inputData := ImageToGrayFloatArray(imgGray)
|
||||
inputShape := ort.NewShape(1, 1, 64, int64(imgGray.Bounds().Dx()))
|
||||
inputTensor, err := ort.NewTensor[float32](inputShape, inputData)
|
||||
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
defer inputTensor.Destroy()
|
||||
// This hypothetical network maps a 2x5 input -> 2x3x4 output.
|
||||
outputTensor, err := ort.NewEmptyTensor[int64](outputShape)
|
||||
defer outputTensor.Destroy()
|
||||
onnxPath := initModel()
|
||||
session, err := ort.NewAdvancedSession(onnxPath,
|
||||
[]string{"input1"}, []string{"output"},
|
||||
[]ort.Value{inputTensor}, []ort.Value{outputTensor}, nil)
|
||||
defer session.Destroy()
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
err = session.Run()
|
||||
if err != nil {
|
||||
fmt.Errorf(err.Error())
|
||||
}
|
||||
|
||||
outputData := outputTensor.GetData()
|
||||
codeResult := ""
|
||||
for i := 0; i < len(outputData); i++ {
|
||||
if outputData[i] != 0 {
|
||||
codeResult += gojsonq.New().JSONString(getCharCode()).Find("[" + strconv.Itoa(int(outputData[i])) + "]").(string)
|
||||
}
|
||||
}
|
||||
return codeResult
|
||||
}
|
||||
|
||||
// AutoOCRVerification 全自动
|
||||
func AutoOCRVerification(img image.Image) string {
|
||||
img1 := ResizeImage(img, uint(64*img.Bounds().Dx()/img.Bounds().Dy()), 64)
|
||||
imgGray := ConvertToGray(img1)
|
||||
inputData := ImageToGrayFloatArray(imgGray)
|
||||
inputShape := ort.NewShape(1, 1, 64, int64(imgGray.Bounds().Dx()))
|
||||
inputTensor, err := ort.NewTensor[float32](inputShape, inputData)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
defer inputTensor.Destroy()
|
||||
var outputShape = ort.NewShape(1, 16)
|
||||
codeResult := ""
|
||||
//失败一次获取输出纬度
|
||||
for {
|
||||
outputTensor, err := ort.NewEmptyTensor[int64](outputShape)
|
||||
defer outputTensor.Destroy()
|
||||
onnxPath := initModel()
|
||||
session, err := ort.NewAdvancedSession(onnxPath,
|
||||
[]string{"input1"}, []string{"output"},
|
||||
[]ort.Value{inputTensor}, []ort.Value{outputTensor}, nil)
|
||||
defer session.Destroy()
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
err1 := session.Run()
|
||||
if err1 != nil {
|
||||
if strings.Contains(err1.Error(), "OrtValue shape verification failed. Current shape:") {
|
||||
regex := regexp.MustCompile(`Requested shape:{(\d+),(\d+)}`)
|
||||
matches := regex.FindStringSubmatch(err1.Error())
|
||||
if len(matches) == 3 {
|
||||
c1, _ := strconv.Atoi(matches[1])
|
||||
c2, _ := strconv.Atoi(matches[2])
|
||||
outputShape = ort.NewShape(int64(c1), int64(c2))
|
||||
continue
|
||||
}
|
||||
}
|
||||
fmt.Errorf(err1.Error())
|
||||
return codeResult
|
||||
}
|
||||
|
||||
outputData := outputTensor.GetData()
|
||||
|
||||
for i := 0; i < len(outputData); i++ {
|
||||
if outputData[i] != 0 {
|
||||
codeResult += gojsonq.New().JSONString(getCharCode()).Find("[" + strconv.Itoa(int(outputData[i])) + "]").(string)
|
||||
}
|
||||
}
|
||||
break
|
||||
}
|
||||
return codeResult
|
||||
}
|
||||
@@ -1,870 +0,0 @@
|
||||
package ddddocr
|
||||
|
||||
// 获取映射字符串
|
||||
func getCharCode() string {
|
||||
return `["", "掀", "袜", "顧", "徕", "榱", "荪", "浡", "其", "炎", "玉", "恩", "劣", "徽", "廉", "桂", "拂",
|
||||
"鳊", "撤",
|
||||
"赏", "哮", "侄", "蓮", "И", "进", "饭", "饱", "优", "楸", "礻", "蜉", "營", "伙", "杌", "修", "榜",
|
||||
"准", "铒",
|
||||
"戏", "赭", "襟", "彘", "彩", "雁", "闽", "坎", "聂", "氡", "辜", "苁", "潆", "摁", "月", "稇", "而",
|
||||
"醴", "簉",
|
||||
"卑", "妖", "埽", "嘡", "醛", "見", "煎", "汪", "秽", "迄", "噭", "焉", "钌", "瑕", "玻", "仙", "蹑",
|
||||
"钀", "翦",
|
||||
"丰", "矗", "2", "胚", "镊", "镡", "鍊", "帖", "僰", "淀", "吒", "冲", "挡", "粼", "螈", "缵", "孺",
|
||||
"侦", "曷",
|
||||
"渐", "敷", "投", "宸", "祉", "柳", "尖", "梃", "淘", "臁", "躇", "撖", "惭", "狄", "聢", "官", "狴",
|
||||
"诬", "骄",
|
||||
"跻", "場", "姻", "钎", "藥", "綉", "驾", "舻", "黢", "鲦", "蜣", "渖", "绹", "佰", "怜", "三", "痪",
|
||||
"眍", "养",
|
||||
"角", "薜", "濑", "劳", "戟", "傎", "纫", "徉", "收", "稍", "虫", "螋", "鬲", "捌", "陡", "蓟", "邳",
|
||||
"蹢", "涉",
|
||||
"煋", "端", "懷", "椤", "埶", "廊", "免", "秫", "猢", "睐", "臺", "擀", "布", "麃", "彗", "汊", "芄",
|
||||
"遣", "胙",
|
||||
"另", "癯", "徭", "疢", "茆", "忡", "'", "烃", "笕", "薤", "肆", "熛", "過", "盖", "跷", "呷", "痿",
|
||||
"沖", "魍",
|
||||
"讣", "庤", "弑", "诩", "庵", "履", "暮", "始", "滟", "矅", "蛹", "鸿", "啃", "铋", "沿", "鐾", "酆",
|
||||
"團", "恙",
|
||||
"閥", "聒", "讵", "颠", "沾", "堅", "踣", "陴", "覃", "滙", "浐", "钇", "脆", "炙", "亮", "觌", "産",
|
||||
"汩", "鸭",
|
||||
"斄", "堆", "掭", "揞", "鹂", "郫", "瘅", "蚂", "揩", "学", "组", "浸", "腙", "耀", "嗛", "局", "蠓",
|
||||
"肠", "昏",
|
||||
"I", "岑", "镯", "憧", "油", "泸", "鸟", "潇", "蕻", "褒", "瞧", "旸", "昭", "庐", "鞒", "内", "痈",
|
||||
"己", "曙",
|
||||
"怠", "锟", "晞", "耢", "鲢", "醦", "糕", "療", "寇", "梵", "黾", "呻", "苒", "ü", "校", "嘏", "昃",
|
||||
"Ⅰ", "蕰",
|
||||
"凖", "嵛", "裨", "筏", "匜", "咋", "乏", "婵", "镂", "珰", "感", "蔗", "蚵", "庞", "弢", "槟", "口",
|
||||
"漉", "﹒",
|
||||
"咂", "俩", "增", "硐", "襙", "绉", "卿", "距", "璱", "猖", "铚", "郚", "嬖", "缒", "阃", "扞", "V",
|
||||
"望", "最",
|
||||
"浔", "骜", "赃", "闻", "砍", "奸", "灶", "以", "获", "鳎", "浦", "罐", "孓", "纭", "瘀", "普", "氰",
|
||||
"塮", "症",
|
||||
"顷", "们", "螓", "蛸", "鵰", "册", "美", "萨", "沘", "犰", "嫌", "名", ")", "懦", "滇", "F", "垡",
|
||||
"声", "毅",
|
||||
"隅", "鲎", "煨", "萦", "宜", "唇", "鯨", "邛", "杲", "赜", "长", "魂", "桠", "锇", "搓", "俘", "仰",
|
||||
"膘", "宦",
|
||||
"歹", "遁", "猃", "噉", "幂", "糜", "嗤", "周", "剂", "曦", "暧", "焖", "髻", "釐", "泰", "窟", "檎",
|
||||
"旧", "犀",
|
||||
"镄", "百", "取", "岍", "逗", "叽", "呃", "鲪", "萬", "陈", "7", "習", "区", "逄", "宏", "罡", "漭",
|
||||
"盗", "郿",
|
||||
"般", "谢", "倪", "纵", "婶", "砧", "揖", "扪", "濒", "愤", "茓", "浞", "子", "揄", "旌", "趄", "樊",
|
||||
"醑", "遄",
|
||||
"婚", "汶", "矩", "裈", "弊", "呱", "铳", "勿", "蚴", "忿", "褓", "缚", "酱", "璞", "庆", "除", "礌",
|
||||
"珩", "榨",
|
||||
"鼢", "逞", "容", "圯", "猛", "陌", "-", "嚯", "镘", "鱾", "睚", "猬", "杜", "鳓", "燈", "計", "咣",
|
||||
"炜", "睁",
|
||||
"箱", "邮", "略", "馇", "逐", "雀", "僬", "髯", "奖", "俱", "-", "绗", "犏", "辱", "忑", "挽", "康",
|
||||
"蝼", "栏",
|
||||
"模", "辒", "•", "儋", "罱", "墈", "会", "秀", "栈", "缔", "醜", "蚣", "阮", "鼗", "眼", "湧", "沁",
|
||||
"夥", "毕",
|
||||
"媚", "瘳", "痣", "搴", "闿", "遍", "焰", "岣", "舱", "埌", "麿", "嘿", "靽", "体", "想", "霓", "钛",
|
||||
"摽", "苑",
|
||||
"芳", "技", "綮", "钅", "燠", "栾", "年", "悱", "腹", "员", "呕", "闇", "嗫", "檩", "荒", "溱", "舨",
|
||||
"峙", "卒",
|
||||
"洑", "预", "弯", "蔷", "叵", "锯", "慈", "牧", "患", "贇", "偷", "鲜", "锓", "躔", "嚬", "烈", "娌",
|
||||
"嘲", "详",
|
||||
"麺", "舒", "厨", "徵", "葹", "只", "篦", "鹀", "剕", "驳", "聍", "黧", "砾", "暅", "褫", "呈", "森",
|
||||
"结", "龛",
|
||||
"钲", "轧", "扔", "蕹", "赵", "涒", "冯", "渲", "缭", "坚", "趼", "鲑", "倫", "门", "班", "垚", "鞍",
|
||||
"菘", "畐",
|
||||
"僇", "侉", "禢", "轳", "饦", "兽", "呯", "捂", "樨", "卧", "栝", "豭", "冶", "鉰", "申", "蜈", "印",
|
||||
"缨", "镫",
|
||||
"蕾", "圜", "扑", "娉", "烦", "缳", "广", "峄", "獒", "铔", "奁", "醚", "倥", "蹇", "阚", "镆", "煺",
|
||||
"德", "颉",
|
||||
"嗅", "绷", "蒯", "祺", "崧", "往", "枨", "涡", "鲲", "瓅", "岌", "肘", "飔", "缘", "千", "棱", "溶",
|
||||
"窣", "篼",
|
||||
"代", "捡", "送", "咡", "术", "滑", "茜", "晾", "挤", "曳", "糈", "G", "翊", "殴", "妹", "溥", "璆",
|
||||
"烩", "拙",
|
||||
"襄", "几", "嘴", "D", "驮", "淙", "蹐", "合", "環", "剑", "怪", "褂", "畑", "燏", "订", "珪", "≥",
|
||||
"瘟", "耷",
|
||||
"槑", "衷", "猕", "迁", "霎", "槜", "﹖", "鋈", "苹", "嫣", "祜", "李", "鄒", "噢", "萄", "仝", "纨",
|
||||
"直", "悛",
|
||||
"拣", "远", "诏", "圧", "躬", "蝟", "總", "眆", "筻", "硇", "鳁", "眠", "钆", "泞", "猱", "宾", "酞",
|
||||
"募", "螳",
|
||||
"腴", "念", "宠", "唯", "怊", "勃", "M", "兿", "蟑", "妁", "掸", "拌", "铸", "讼", "诟", "锺", "Ω",
|
||||
"竟", "羚",
|
||||
"剽", "C", "苦", "煳", "罢", "跨", "~", "豸", "±", "俬", "捺", "彦", "钣", "鋆", "用", "缤", "搁",
|
||||
"徼", "谦",
|
||||
"筘", "嗨", "扮", "旇", "折", "咯", "昆", "叟", "垂", "箐", "捻", "燕", "島", "瞀", "鮮", "屡", "點",
|
||||
"瘭", "恚",
|
||||
"旚", "丟", "捽", "菁", "瀑", "炕", "蹩", "芒", "r", "是", "媾", "鹝", "囵", "萤", "拷", "频", "埴",
|
||||
"课", "癍",
|
||||
"袱", "螯", "谘", "榛", "Y", "缣", "裔", "憩", "相", "觀", "晗", "坳", "炔", "勉", "汆", "钡", "舐",
|
||||
"衫", "疫",
|
||||
"鲙", "蘩", "穈", "殁", "九", "泻", "咤", "構", "谆", "陕", "装", "蔡", "画", "介", "苋", "務", "敝",
|
||||
"俟", "帇",
|
||||
"鸺", "贸", "茗", "肃", "滪", "输", "瘗", "菽", "饹", "诉", "遐", "浑", "扎", "卟", "铀", "邗", "觋",
|
||||
"嘎", "塑",
|
||||
"潏", "金", "姘", "潋", "逵", "鲻", "逯", "炮", "甄", "髡", "剩", "嗬", "芴", "屋", "改", "骣", "芪",
|
||||
"邠", "痋",
|
||||
"珑", "帆", "狙", "八", "奔", "族", "轵", "氖", "雕", "痧", "眊", "胛", "酉", "鲼", "砣", "猸", "餮",
|
||||
"郇", "沫",
|
||||
"跖", "蝉", "屑", "辘", "閣", "涑", "邡", "篃", "交", "笼", "颇", "贻", "魄", "黡", "劂", "糠", "炅",
|
||||
"帨", "苍",
|
||||
"瓴", "粤", "莎", "朿", "埔", "绸", "齁", "鱿", "惨", "腢", "郡", "棠", "猫", "脑", "風", "蚱", "捐",
|
||||
"嵌", "胱",
|
||||
"馗", "竽", "泥", "辍", "怖", "雾", "絮", "淼", "筝", "碲", "悼", "龀", "の", "珥", "忐", "溲", "昕",
|
||||
"荔", "掂",
|
||||
"瘦", "僭", "蔌", "抺", "椅", "誉", "扯", "僜", "停", "衉", "汇", "赔", "眄", "呙", "咙", "剿", "次",
|
||||
"蛟", "嗓",
|
||||
"』", "汕", "詈", "帘", "踧", "姁", "血", "堪", "喜", "滩", "璎", "胄", "俨", "眚", "凌", "拽", "滔",
|
||||
"⑿", "嬃",
|
||||
"―", "汐", "潭", "阡", "呓", "婷", "执", "妊", "恂", "妥", "鳘", "蔫", "设", "睒", "笪", "謇", "鞋",
|
||||
"谍", "黯",
|
||||
"虍", "馬", "蚧", "骑", "峤", "舾", "儀", "駡", "β", "蓑", "柏", "痒", "蒇", "痕", "妍", "熠", "僻",
|
||||
"爬", "迭",
|
||||
"畫", "绰", "湯", "凭", "菼", "懈", "顒", "午", "箪", "糙", "址", "钼", "堵", "佘", "侍", "卤", "(",
|
||||
"榚", "泽",
|
||||
"溘", "蟹", "b", "燁", "颂", "菠", "榉", "鲡", "埸", "荛", "歘", "断", "邸", "贡", "礞", "蔼", "脸",
|
||||
"爪", "帜",
|
||||
"翡", "仟", "皎", "辆", "滫", "昔", "™", "柬", "弓", "遇", "杪", "侨", "娓", "镪", "觑", "一", "踌",
|
||||
"牟", "褡",
|
||||
"厩", "晌", "每", "娘", "渤", "c", "咫", "成", "颏", "孩", "鼓", "瞌", "槁", "捒", "阉", "伉", "癣",
|
||||
"胞", "鲟",
|
||||
"瓤", "杅", "紡", "喂", "掠", "镜", "镧", "侞", "赦", "貝", "丕", "臧", "L", "池", "彷", "棓", "锽",
|
||||
"渊", "食",
|
||||
"饨", "堡", "玥", "氣", "讽", "敬", "闺", "帡", "携", "哫", "珈", "魆", "哄", "旁", "喻", "泄", "畎",
|
||||
"郁", "唅",
|
||||
"葜", "繪", "飐", "谶", "聆", "斝", "谥", "辉", "髅", "進", "吧", "蹀", "铛", "笛", "睥", "楼", "凝",
|
||||
"況", "鸷",
|
||||
"苠", "饺", "沙", "缴", "块", "梢", "慝", "珐", "鄏", "霰", "迸", "氆", "趵", "棣", "鳔", "祆", "☆",
|
||||
"苯", "恁",
|
||||
"螨", "庭", "缠", "槠", "津", "髋", "诔", "葶", "蜾", "坻", "蒹", "摔", "向", "垩", "蹭", "淇", "筛",
|
||||
"滬", "玡",
|
||||
"铺", "逼", "劵", "绲", "团", "鳀", "常", "玖", "擢", "株", "铵", "樽", "弭", "醇", "糨", "璈", "曩",
|
||||
"潔", "祘",
|
||||
"磨", "希", "鲅", "擂", "谗", "唳", "欷", "欧", "绋", "庙", "琬", "稳", "糊", "拥", "霪", "浼", "翎",
|
||||
"俜", "摸",
|
||||
"筚", "巯", "墼", "苫", "缩", "镚", "婪", "圹", "咚", "儿", "蒽", "婆", "鲐", "雹", "霞", "嶪", "濠",
|
||||
"琉", "澌",
|
||||
"媢", "禤", "摺", "掏", "矢", "艄", "围", "呸", "寺", "拤", "氐", "柝", "跎", "僖", "挢", "茨", "涮",
|
||||
"缫", "撸",
|
||||
"荨", "嶷", "廋", "魋", "付", "喋", "蜗", "邙", "棹", "璪", "倡", "鞭", "游", "錦", "眬", "抒", "眈",
|
||||
"培", "夏",
|
||||
"黔", "獐", "皋", "戛", "鲀", "垒", "耽", "纤", "漩", "铈", "握", "窝", "芋", "濞", "截", "零", "敖",
|
||||
"眸", "怦",
|
||||
"噎", "簋", "掳", "妣", "湃", "璠", "殄", "觞", "桅", "笋", "鲞", "踯", "傀", "犨", "抵", "疰", "暌",
|
||||
"耖", "供",
|
||||
"枳", "怂", "娶", "鸩", "捣", "庸", "逡", "懋", "颃", "長", "鼫", "姮", "蹈", "耵", "乂", "骐", "殇",
|
||||
"膏", "仳",
|
||||
"冥", "梭", "洵", "碣", "昝", "仉", "軒", "隍", "更", "な", "嵕", "拜", "粑", "鲴", "吇", "秃", "尕",
|
||||
"魃", "狨",
|
||||
"臛", "蟥", "胨", "注", "谁", "张", "才", "尸", "派", "矮", "洳", "舟", "溺", "锴", "寓", "籴", "夕",
|
||||
"叭", "荠",
|
||||
"澼", "劃", "久", "私", "炉", "娟", "麤", "稂", "河", "纴", "夺", "亏", "焙", "。", "塗", "蜩", "栌",
|
||||
"渡", "薰",
|
||||
"崋", "揿", "漤", "啾", "郏", "舣", "卉", "爱", "牚", "撵", "钺", "再", "企", "笺", "疾", "承", "俾",
|
||||
"瞈", "邰",
|
||||
"汾", "瘛", "檫", "蒎", "觅", "绀", "掎", "U", "赓", "匳", "聘", "蛤", "跤", "嗜", "洼", "歔", "弟",
|
||||
"飕", "莼",
|
||||
"嫉", "那", "滈", "践", "僦", "偎", "扢", "绚", "乕", "旳", "招", "饯", "®", "攸", "鞁", "囫", "铨",
|
||||
"陒", "鷄",
|
||||
"畀", "韨", "經", "纾", "萸", "肴", "→", "宗", "迳", "鳞", "亚", "搂", "喀", "狮", "坦", "瞥", "采",
|
||||
"姝", "钳",
|
||||
"□", "剌", "維", "葸", "鼩", "公", "刀", "沩", "喔", "泺", "哉", "徨", "篝", "掊", "沕", "运", "偆",
|
||||
"浒", "语",
|
||||
"乇", "仪", "萝", "疍", "踽", "碡", "熰", "荞", "嚓", "天", "饰", "泵", "械", "孑", "蛰", "荟", "源",
|
||||
"峡", "矜",
|
||||
"睬", "噬", "腆", "婉", "‘", "等", "誓", "辀", "岖", "琖", "碜", "霍", "怼", "唛", "弈", "淑", "疆",
|
||||
"晴", "镴",
|
||||
"鸡", "埚", "焕", "芦", "唻", "踅", "吴", "殡", "唏", "吨", "寡", "鹉", "絲", "坉", "會", "埭", "Ⅲ",
|
||||
"捏", "墅",
|
||||
"卓", "叙", "徇", "柜", "各", "荭", "J", "恝", "囐", "蓉", "犋", "叡", "莺", "颌", "蒸", "饸", "疋",
|
||||
"玊", "兢",
|
||||
"鱽", "藍", "杳", "辂", "獘", "拔", "侪", "湍", "膂", "渔", "瘊", "雉", "稁", "職", "僤", "鄳", "祁",
|
||||
"稱", "I",
|
||||
"裴", "锉", "曹", "鲶", "挨", "哑", "鷪", "鏠", "煞", "师", "蛲", "牁", "琅", "告", "媒", "祭", "确",
|
||||
"荚", "亰",
|
||||
"蝗", "阗", "歩", "疲", "f", "唣", "愛", "郾", "棍", "山", "狲", "纽", "蚡", "栂", "馓", "诊", "猴",
|
||||
"喤", "来",
|
||||
"继", "桎", "嬛", "骞", "邴", "暄", "贼", "昴", "廿", "克", "耔", "彤", "鹭", "葓", "骢", "龁", "鏡",
|
||||
"瀚", "赅",
|
||||
"韩", "譄", "榷", "殚", "膛", "须", "、", "砖", "唶", "番", "蛘", "畴", "铠", "亢", "氓", "铰", "炻",
|
||||
"筫", "迢",
|
||||
"兰", "玺", "砻", "积", "莜", "吸", "监", "膦", "迪", "迷", "冷", "哀", "贳", "瞄", "器", "鹡", "惺",
|
||||
"徐", "酢",
|
||||
"寒", "Ⓡ", "倾", "飞", "楽", "涢", "队", "舆", "赤", "璩", "戳", "殳", "掮", "舴", "蜷", "宄", "拴",
|
||||
"癌", "舛",
|
||||
"婀", "抟", "靡", "骍", "揸", "思", "慧", "平", "橘", "臭", "硖", "卬", "畈", "兠", "茸", "脂", "魚",
|
||||
"晩", "御",
|
||||
"龋", "涣", "罨", "爍", "糌", "汧", "缐", "贽", "要", "祀", "鲊", "爼", "獯", "瀣", "棋", "肈", "佣",
|
||||
"娣", "柩",
|
||||
"枸", "偃", "v", "唷", "劍", "榴", "槐", "漫", "洽", "蒡", "籼", "魔", "峋", "第", "歙", "萧", "谮",
|
||||
"埯", "撮",
|
||||
"马", "绡", "裘", "鹋", "蓬", "显", "噶", "倒", "镳", "艽", "窬", "拳", "樯", "跋", "詹", "钥", "心",
|
||||
"嶽", "嚋",
|
||||
"戎", "吕", "涂", "悃", "麦", "骋", "推", "箩", "硚", "匆", "村", "五", "杨", "凑", "鞫", "镰", "伥",
|
||||
"诒", "纣",
|
||||
"崃", "鸻", "翰", "辌", "廛", "證", "舢", "盼", "腿", "圳", "贱", "皿", "隆", "屈", "龏", "瓒", "顏",
|
||||
"↓", "赈",
|
||||
"煙", "窍", "韧", "壁", "莰", "箬", "蹋", "褰", "峥", "悚", "坜", "环", "回", "疼", "渍", "蝄", "东",
|
||||
"臂", "坩",
|
||||
"走", "痍", "或", "蜀", "熳", "蜻", "佐", "懿", "嚅", "紗", "螭", "忖", "顶", "狡", "吲", "洣", "帛",
|
||||
"呶", "柞",
|
||||
"柫", "酿", "粥", "琢", "呵", "踝", "榀", "呲", "價", "鼋", "欺", "此", "背", "猎", "昱", "濡", "稚",
|
||||
"欠", "暇",
|
||||
"茬", "牙", "迹", "尼", "氛", "膠", "缯", "娼", "骚", "姒", "鬟", "霁", "鲔", "者", "驰", "倩", "馉",
|
||||
"工", "芬",
|
||||
"烙", "卦", "C", "裂", "垲", "摆", "珮", "缏", "杞", "绘", "司", "如", "姞", "荆", "挖", "跗", "伍",
|
||||
"氚", "钘",
|
||||
"郢", "轱", "篆", "吭", "夡", "鹫", "讷", "轺", "!", "匈", "待", "聱", "黏", "海", "蹶", "趋", "鎮",
|
||||
"觊", "江",
|
||||
"咸", "富", "艴", "稗", "钜", "搏", "壶", "鲮", "薪", "猞", "轰", "踪", "赣", "循", "序", "噻", "若",
|
||||
"裾", "许",
|
||||
"癞", "吓", "判", "踔", "查", "蚀", "[", "樓", "坌", "岳", "榄", "役", "倜", "⒂", "旭", "溆", "惯",
|
||||
"咀", "跫",
|
||||
"选", "囱", "污", "镶", "⒁", "淠", "氮", "酯", "寅", "芼", "炊", "夯", "郪", "农", "褲", "嘬", "蹻",
|
||||
"烔", "罄",
|
||||
"开", "靴", "镇", "杯", "羰", "硪", "籍", "摘", "馀", "餐", "眯", "⑴", "呗", "巫", "幤", "蒤", "蒗",
|
||||
"镥", "檵",
|
||||
"盛", "純", "娃", "●", "耿", "巡", "婴", "槔", "i", "颊", "Ⅳ", "栅", "绅", "邘", "冉", "碧", "使",
|
||||
"熨", "羞",
|
||||
"扼", "漳", "觯", "楊", "励", "逑", "咄", "之", "斤", "嘣", "鹰", "媸", "鲂", "褚", "磚", "琨", "聪",
|
||||
"牖", "太",
|
||||
"蓍", "涫", "≤", "虽", "鸽", "燧", "褊", "聿", "壬", "然", "疚", "莲", "悴", "簃", "颓", "坠", "瞬",
|
||||
"汳", "l",
|
||||
"登", "瘼", "窳", "桤", "縯", "匣", "坡", "↑", "愦", "攘", "渭", "嬢", "鲰", "性", "楚", "澈", "赪",
|
||||
"達", "鄯",
|
||||
"罅", "帽", "茠", "底", "嫜", "奏", "浅", "荽", "楹", "鼍", "枵", "嗔", "滍", "椴", "嵩", "氤", "搠",
|
||||
"两", "榔",
|
||||
"树", "吝", "基", "峂", "栎", "侮", "舸", "遂", "颡", "锷", "杼", "酔", "幄", "哽", "睢", "陔", "※",
|
||||
"嚆", "宬",
|
||||
"宽", "髦", "笾", "保", "蹊", "榕", "咏", "椋", "丧", "裤", "骛", "逧", "弇", "崆", "樘", "疤", "鸤",
|
||||
"伞", "抚",
|
||||
"诎", "诵", "豢", "佳", "差", "埝", "极", "黍", "煜", "曰", "阱", "悞", "叹", "垤", "藁", "嗵", "崔",
|
||||
"卫", "珂",
|
||||
"憯", "蔬", "菜", "碑", "扈", "铆", "夹", "衡", "弱", "挈", "徜", "疠", "丶", "遠", "提", "斧", "炟",
|
||||
"肺", "B",
|
||||
"她", "晟", "谎", "邱", "粳", "酽", "爨", "鬼", "伧", "兹", "嶓", "谤", "饕", "揶", "谱", "歡", "髪",
|
||||
"餍", "泳",
|
||||
"郞", "谣", "汉", "褐", "非", "刽", "缅", "饴", "齐", "兴", "涯", "芫", "凡", "褶", "晡", "努", "蚶",
|
||||
"彥", "皤",
|
||||
"砌", "黼", "吹", "指", "㙟", "蓁", "鹜", "話", "拊", "辨", "盎", "肌", "旘", "软", "颍", "甏", "滚",
|
||||
"旦", "滨",
|
||||
"间", "尴", "对", "鄘", "称", "镗", "咅", "璐", "怔", "垛", "洎", "瓮", "绨", "脚", "遒", "吊", "纸",
|
||||
"蹅", "经",
|
||||
"泉", "武", "汀", "歪", "败", "拾", "铪", "吼", "邹", "磊", "论", "岛", "厍", "锛", "芎", "芭", "音",
|
||||
"澧", "镕",
|
||||
"锒", "宙", "牵", "忱", "嫔", "麯", "澉", "擐", "砥", "撞", "痴", "盹", "畿", "厾", "酸", "俑", "脽",
|
||||
"鸈", "枷",
|
||||
"咨", "蔹", "诂", "胰", "董", "脶", "黩", "髓", "鉵", "澎", "鲽", "梧", "樱", "诜", "鲯", "跂", "盂",
|
||||
"浴", "苻",
|
||||
"锅", "實", "碁", "嘛", "氕", "艮", "涟", "绢", "姿", "茝", "砘", "簿", "穷", "镃", "∈", "抽", "事",
|
||||
"誜", "窅",
|
||||
"瀘", "鲹", "兖", "嵎", "陧", "榍", "轶", "柿", "藤", "薏", "娆", "骷", "梅", "摒", "睪", "剪", "羸",
|
||||
"忧", "邝",
|
||||
"跺", "旆", "堕", "伫", "绍", "疵", "樟", "–", "绾", "蜴", "靸", "侃", "瘘", "珧", "遨", "縠", "信",
|
||||
"充", "桔",
|
||||
"黇", "劬", "脒", "良", "俵", "颙", "轹", "犿", "屐", "牾", "4", "兮", "澝", "汗", "沼", "铲", "濋",
|
||||
"鹬", "丝",
|
||||
"妫", "重", "蒺", "磲", "曚", "尔", "国", "桐", "俣", "剐", "哼", "恹", "哧", "藔", "谓", "轨", "眩",
|
||||
"痞", "添",
|
||||
"鬯", "库", "梱", "婕", "蜢", "贿", "敕", "泯", "羟", "龇", "垸", "左", "肖", "辎", "鞣", "谄", "可",
|
||||
"腺", "末",
|
||||
"狞", "贷", "嗌", "仕", "楞", "膻", "臻", "欻", "洲", "所", "檀", "抔", "罹", "牒", "仫", "芨", "柄",
|
||||
"嫩", "酒",
|
||||
"祙", "渠", "的", "笨", "鳐", "楡", "过", "苡", "核", "拖", "阢", "莒", "凤", "锋", "` + "`" + `", "硎", "弁",
|
||||
"鬶", "朐",
|
||||
"忏", "於", "昊", "剟", "咳", "湘", "日", "满", "哨", "螵", "餪", "放", "佶", "葵", "硷", "c", "抱",
|
||||
"锥", "芮",
|
||||
"啻", "惊", "峁", "琊", "嶲", "撺", "煅", "屏", "袗", "鄞", "梓", "鹌", "宅", "赂", "鱼", "洱", "騳",
|
||||
"E", "物",
|
||||
"觏", "雙", "瑀", "上", "淩", "愀", "❋", "鄙", "憝", "沛", "硫", "产", "垯", "亁", "枭", "堰", "赑",
|
||||
"趾", "庹",
|
||||
"腭", "迨", "拚", "晒", "蜇", "扣", "纰", "闵", "窭", "椽", "菏", "嘁", "伛", "郸", "素", "殷", "表",
|
||||
"躞", "笸",
|
||||
"耻", "荧", "辛", "篑", "馈", "壮", "耩", "宛", "慰", "盡", "塆", "铯", "苏", "王", "桕", "⑧", "°",
|
||||
"浚", "栉",
|
||||
"朘", "虚", "骆", "坂", "秤", "鲋", "蕊", "渝", "呦", "潼", "驱", "诼", "峇", "盤", "趴", "肄", "笑",
|
||||
"讹", "貋",
|
||||
"穂", "啼", "趟", "暽", "傣", "蜎", "挎", "陳", "勖", "戴", "旃", "瞎", "舌", "幻", "喾", "赁", "E",
|
||||
"播", "诀",
|
||||
"蟛", "鹛", "骶", "輸", "連", "醳", "逅", "奉", "崖", "娩", "幔", "佃", "扅", "阔", "生", "贬", "疯",
|
||||
"珀", "苶",
|
||||
"屯", "裣", "蹯", "蝮", "解", "陂", "疝", "茈", "帑", "议", "仲", "埙", "竺", "峰", "遮", "涎", "穸",
|
||||
"阂", "潵",
|
||||
"镱", "例", "荑", "u", "脎", "衍", "轲", "⑵", "虾", "颚", "钞", "²", "伴", "根", "沣", "腌", "户",
|
||||
"~", "辙",
|
||||
"愧", "噤", "觥", "波", "铗", "纂", "鲺", "僚", "毐", "〇", "桼", "祗", "慢", "啵", "坏", "吗", "嗞",
|
||||
"甬", "曈",
|
||||
"徹", "灏", "混", "渌", "括", "脖", "汝", "現", "訇", "紅", "飘", "虢", "腱", "旄", "嬴", "昨", "孀",
|
||||
"蚁", "呛",
|
||||
"讳", "病", ",", "喈", "蒋", "镭", "葩", "耲", "鳈", "锄", "喘", "返", "傕", "咆", "享", "枥", "瓠",
|
||||
"茳", "铱",
|
||||
"脘", "暹", "廒", "爝", "橹", "瞑", "铎", "岢", "叁", "翏", "捭", "賀", "悉", "帝", "芥", "牀", "闌",
|
||||
"毯", "亍",
|
||||
"弧", "锆", "币", "祊", "纔", "齑", "肟", "绤", "獨", "翚", "颢", "係", "鍪", "粉", "统", "诗", "娜",
|
||||
"褥", "鈺",
|
||||
"湔", "呤", "犸", "湨", "泣", "蟾", "犾", "烛", "斐", "朦", "室", "诨", "榭", "煦", "醺", "敞", "燮",
|
||||
"糅", "衽",
|
||||
"孔", "猄", "疭", "辰", "钽", "胁", "釆", "钉", "胤", "涧", "弼", "濯", "汨", "颖", "茫", "皑", "遏",
|
||||
"捃", "坭",
|
||||
"燴", "肩", "滞", "玢", "巽", "砺", "蜿", "毁", "億", "骥", "本", "忽", "肚", "搽", "靰", "郴", "跆",
|
||||
"客", "酣",
|
||||
"α", "屎", "辩", "殂", "垝", "紫", "秦", "喇", "凶", "傧", "铐", "蘊", "補", "贤", "竿", "途", "慗",
|
||||
"榖", "券",
|
||||
"莠", "逆", "鳇", "误", "崟", "妇", "磷", "捧", "莸", "⇋", "绺", "稻", "填", "逋", "侈", "隶", "侵",
|
||||
"翥", "惘",
|
||||
"惧", "鸥", "赠", "壳", "芯", "巩", "獗", "硅", "搎", "鲛", "9", "夸", "穆", "缜", "诓", "观", "薛",
|
||||
"咎", "杧",
|
||||
"页", "饫", "瑟", "率", "礤", "悭", "畔", "匯", "匮", "鼠", "犒", "芡", "傍", "嫂", "啸", "鄉", "哭",
|
||||
"鄱", "捷",
|
||||
"靺", "嚒", "嘀", "哒", "#", "拼", "钚", "魁", "霣", "眶", "郊", "死", "愁", "箭", "鼙", "签", "害",
|
||||
"斛", "睑",
|
||||
"蟜", "余", "墨", "様", "读", "養", "貉", "较", "浆", "翩", "徂", "冕", "铧", "列", "诈", "穝", "缑",
|
||||
"纲", "志",
|
||||
"舀", "甾", "举", "馁", "ä", "畹", "榼", "垢", "襁", "麟", "灭", "佴", "镩", "酝", "柒", "梯", "傈",
|
||||
"萭", "悫",
|
||||
"莨", "搞", "+", "兄", "偲", "攀", "曝", "嵝", "喳", "从", "遶", "撴", ".", "鄄", "欲", "挺", "娡",
|
||||
"发", "速",
|
||||
"胲", "褀", "态", "行", "蚓", "坼", "适", "厦", "寐", "带", "緃", "醤", "珽", "‧", "溍", "斋", "鐀",
|
||||
"朝", "欢",
|
||||
"传", "築", "咪", "据", "蹜", "医", "妄", "肇", "囝", "怡", "镎", "桩", "轩", "岔", "腐", "矽", "媵",
|
||||
"搒", "菔",
|
||||
"拘", "D", "欃", "唧", "瞒", "郈", "绦", "吟", "撝", "醉", "镣", "匝", "拎", "砒", "顸", "袁", "驼",
|
||||
"愔", "实",
|
||||
"國", "奧", "胩", "府", "逾", "愕", "廷", "碌", "锖", "狩", "褴", "镢", "芷", "娥", "唤", "┌", "云",
|
||||
"О", "檔",
|
||||
"驴", "躯", "驺", "洃", "檑", "窴", "(", "腕", "立", "楯", "齮", "〔", "漆", "k", "芍", "蹽", "鬓",
|
||||
"概", "楣",
|
||||
"唐", "闲", "糗", "旱", "幸", "腽", "嗄", "迂", "镠", "顿", "扥", "圃", "烜", "馍", "佝", "岷", "童",
|
||||
"悦", "┐",
|
||||
"铌", "袈", "靓", "骸", "和", "乔", "灸", "泓", "临", "睿", "掖", "偿", "鐘", "犁", "祓", "鈴", "搌",
|
||||
"授", "鹳",
|
||||
"赢", "怅", "絪", "硬", "芙", "螅", "”", "傢", "避", "裕", "歁", "全", "衰", "仃", "媛", "鬻", "跽",
|
||||
"沌", "急",
|
||||
"猷", "激", "巉", "哝", "渣", "笫", "跳", "螫", "熜", "Z", "筷", "佩", "啶", "萃", "頫", "荙", "出",
|
||||
"孽", "钟",
|
||||
"戡", "釉", "咬", "滦", "鹇", "贯", "鹮", "具", "翁", "机", "濱", "谳", "釣", "懑", "葛", "袯", "谭",
|
||||
"质", "胴",
|
||||
"誊", "侗", "⑩", "静", "蚜", "溋", "嫪", "嗲", "瑭", "座", "舫", "靶", "棘", "泊", "嵖", "摧", "勋",
|
||||
"僡", "藉",
|
||||
"疖", "巂", "随", "罾", "崚", "猹", "憨", "苘", "斓", "鼷", "利", "谲", "剔", "艺", "箓", "蛀", "鲚",
|
||||
"搐", "裟",
|
||||
"捶", "绌", "揪", "帮", "缥", "匍", "冀", "杻", "逛", "邑", "禾", "郰", "黜", "丘", "樂", "滌", "緣",
|
||||
"胃", "苄",
|
||||
"巾", "瑜", "元", "蝶", "层", "烧", "级", "岭", "蘭", "繇", "蝓", "洞", "奢", "则", "政", "矾", "啭",
|
||||
"瘠", "碴",
|
||||
"忤", "身", "匠", "警", "饩", "犬", "皲", "箔", "豕", "虑", "草", "喟", "芤", "逭", "艳", "幡", "姚",
|
||||
"賓", "饪",
|
||||
"卯", "敌", "烽", "嫚", "黝", "豺", "㭗", "教", "偕", "板", "茹", "孤", "人", "狻", "寰", "厕", "玲",
|
||||
"璨", "锵",
|
||||
"搛", "勍", "匾", "聃", "奘", "垃", "焓", "喽", "嫫", "貌", "瘐", "嚰", "孟", "衔", "郎", "账", "础",
|
||||
"电", "黑",
|
||||
"骁", "拨", "濆", "圉", "刮", "闭", "竣", "铅", "羔", "硌", "筑", "难", "管", "苕", "眺", "嫄", "竖",
|
||||
"榟", "崴",
|
||||
"摭", "狐", "娑", "②", "罽", "谊", "←", "狳", "铫", "凯", "狉", "9", "肪", "崤", "莊", "妨", "缶",
|
||||
"滃", "瀦",
|
||||
"揉", "肫", "恧", "糯", "嵬", "5", "裆", "嚷", "稣", "隐", "仂", "て", "驹", "籽", "肢", "尘", "苈",
|
||||
"撷", "镲",
|
||||
"趹", "晤", "唱", "鉏", "篌", "驩", "雍", "闳", "拄", "藜", "朴", "伺", "诳", "房", "吱", "Й", "鳄",
|
||||
"罿", "祧",
|
||||
"酩", "郅", "耎", "尜", "绝", "禅", "揠", "鎏", "慕", "麥", "呜", "鸫", "党", "尝", "砑", "牌", "踉",
|
||||
"刨", "襻",
|
||||
"㾄", "螽", "谌", "止", "抑", "爻", "磬", "铄", "蓠", "委", "汲", "鹑", "╱", "嚣", "彝", "穄", "穹",
|
||||
"態", "醋",
|
||||
"⒀", "叼", "婳", "簌", "渥", "很", "甸", "帅", "锏", "与", "樾", "泷", "棼", "湲", "越", "祥", "短",
|
||||
"顼", "阘",
|
||||
"宋", "馘", "鈉", "未", "囍", "浏", "叻", "箜", "鑽", "法", "曲", "淤", "僮", "做", "强", "析", "磕",
|
||||
"谠", "染",
|
||||
"促", "朊", "隼", "铉", "莆", "蝣", "孛", "薮", "s", "惴", "秘", "妩", "訄", "蔓", "喷", "诡", "犷",
|
||||
"酐", "酇",
|
||||
"刹", "壅", "甫", "史", "孃", "髌", "螬", "擤", "漏", "寞", "奡", "悢", "颔", "岁", "耄", ";", "又",
|
||||
"锭", "鲤",
|
||||
"癔", "杰", "孥", "酲", "蓐", "耋", "捆", "庖", "面", "鹈", "殊", "剡", "峪", "识", "锨", "归", "茴",
|
||||
"—", "菤",
|
||||
"汁", "攝", "液", "鼐", "示", "讠", "男", "凍", "ò", "明", "莓", "砜", "崎", "蜂", "斡", "榫", "娅",
|
||||
"钪", "昙",
|
||||
"胜", "欣", "怨", "◆", "粗", "秷", "节", "市", "贩", "祟", "弍", "蒟", "烁", "糧", "蠃", "編", "黙",
|
||||
"壕", "戚",
|
||||
"犊", "桥", "仺", "孳", "怯", "皓", "倆", "垮", "扩", "诮", "钝", "脯", "晏", "帔", "葫", "瑾", "運",
|
||||
"孬", "跄",
|
||||
"掣", "癜", "掌", "墀", "禇", "耸", "蜓", "鹆", "鄢", "攰", "瘢", "暝", "鸣", "峧", "遵", "笃", "畚",
|
||||
"帧", "晨",
|
||||
"镔", "搜", "靠", "咐", "韓", "绮", "觉", "拦", "斲", "疽", "掐", "尽", "許", "矶", "镉", "豹", "粞",
|
||||
"袋", "酵",
|
||||
"蛙", "戕", "劉", "髀", "彭", "玎", "囿", "郐", "善", "睃", "結", "拧", "邯", "讧", "召", "椭", "瑪",
|
||||
"痼", "庼",
|
||||
"反", "疱", "屠", "荣", "君", "胍", "乙", "臬", "头", "诰", "讪", "席", "晁", ":", "理", "槿", "璘",
|
||||
"禧", "呢",
|
||||
"蹙", "擒", "鸲", "丐", "苓", "壑", "滥", "⑾", "炗", "礴", "耕", "卅", "唿", "苛", "寵", "窖", "麻",
|
||||
"蕨", "沤",
|
||||
"氢", "虔", "癃", "及", "崛", "爽", "蛔", "颤", "膲", "桢", "坐", "蟞", "儇", "葚", "骤", "誤", "寝",
|
||||
"嘭", "灰",
|
||||
"汹", "韂", "铮", "慒", "寶", "肽", "摅", "紧", "亞", "潸", "悯", "橛", "檗", "闹", "愿", "担", "袄",
|
||||
"棚", "垟",
|
||||
"塄", "婞", "麈", "麸", "暗", "咦", "跞", "谡", "盈", "磐", "慎", "瘰", "掼", "憔", "研", "被", "贮",
|
||||
"莛", "至",
|
||||
"呀", "庑", "矫", "摛", "怃", "缙", "磺", "即", "驻", "瘤", "偏", "℃", "嫘", "癫", "汈", "鹟", "搅",
|
||||
"辅", "璀",
|
||||
"阊", "绻", "瑙", "蓂", "棺", "孢", "铊", "鼒", "果", "砮", "飾", "凰", "Я", "遗", "祛", "纮", "劲",
|
||||
"霹", "骃",
|
||||
"绔", "薅", "瀵", "垅", "?", "轻", "惇", "怕", "啥", "哙", "燎", "缆", "匡", "怫", "卞", "朋", "酏",
|
||||
"阑", "爾",
|
||||
"伏", "敏", "埼", "罩", "菹", "艋", "肭", "鯭", "杋", "裀", "撬", "蕺", "惠", "大", "爇", "笈", "絷",
|
||||
"琳", "谫",
|
||||
"诛", "糇", "袢", "倓", "髃", "觽", "埏", "寖", "個", "筴", "外", "漯", "樭", "喁", "杀", "臑", "缇",
|
||||
"裸", "巅",
|
||||
"毹", "茅", "忆", "琼", "唑", "烷", "项", "隋", "约", "排", "吮", "谂", "宝", "牲", "瘫", "娄", "沂",
|
||||
"醫", "拭",
|
||||
"纺", "蹰", "哞", "风", "霆", "值", "酺", "侠", "螾", "埂", "育", "夷", "鮼", "怍", "鸠", "Θ", "瞳",
|
||||
"阇", "耥",
|
||||
"羝", "伽", "洴", "記", "楔", "颼", "沪", "邢", "冰", "昀", "阙", "洌", "嫦", "杂", "仔", "芑", "潴",
|
||||
"痄", "桨",
|
||||
"连", "碓", "塈", "F", "昇", "何", "桦", "晥", "驵", "旋", "药", "银", "奋", "灣", "俐", "絡", "嫁",
|
||||
"浮", "为",
|
||||
"鞅", "科", "颦", "潽", "镍", "鸨", "粵", "骂", "拱", "韫", "盆", "赎", "尿", "钿", "坍", "唁", "秧",
|
||||
"昌", "曆",
|
||||
"颋", "遭", "秭", "褔", "腋", "〉", "吉", "漓", "臆", "焘", "已", "制", "钹", "鴨", "咖", "莘", "P",
|
||||
"碥", "互",
|
||||
"治", "标", "膝", "伪", "浿", "纛", "郗", "看", "佧", "糖", "篓", "亡", "´", "骙", "澡", "影", "窂",
|
||||
"紬", "镅",
|
||||
"慌", "框", "晋", "説", "丢", "凹", "卖", "巧", "蹉", "乾", "莫", "Z", "谔", "矧", "铑", "暴", "庄",
|
||||
"湿", "活",
|
||||
"穿", "腩", "筣", "水", "6", "琦", "迈", "伯", "洄", "抡", "▪", "酋", "荤", "雒", "粕", "簠", "菰",
|
||||
"髁", "枇",
|
||||
"陲", "多", "仗", "央", "滁", "胸", "梏", "痉", "姑", "襞", "﹑", "齿", "弩", "花", "吆", "赫", "岵",
|
||||
"佪", "谑",
|
||||
"锤", "轴", "盐", "馄", "臜", "戢", "涠", "鸸", "糟", "孪", "禁", "蒲", "化", "疏", "痰", "脾", "刈",
|
||||
"應", "珍",
|
||||
"膺", "扌", "廙", "汜", "牍", "虐", "婿", "啕", "彻", "赝", "陶", "蠲", ">", "位", "屁", "醍", "粢",
|
||||
"挪", "臌",
|
||||
"滹", "遴", "馨", "n", "稼", "徊", "酌", "轸", "债", "朰", "程", "辞", "痊", "插", "鹩", "郄", "铝",
|
||||
"狱", "叱",
|
||||
"同", "寄", "搪", "蚯", "魭", "舍", "旷", "闰", "涝", "民", "嗡", "苌", "馕", "姥", "屉", "啧", "枢",
|
||||
"❤", "窕",
|
||||
"钊", "矬", "菂", "佑", "≠", "獬", "桁", "墟", "皖", "鼻", "它", "歇", "独", "好", "晕", "蚝", "锞",
|
||||
"颈", "豚",
|
||||
"聖", "裉", "扫", "岿", "悒", "佥", "苗", "妞", "晚", "圭", "茼", "脲", "摊", "窠", "狸", "抻", "场",
|
||||
"呼", "囟",
|
||||
"噗", "狺", "困", "瀹", "削", "衬", "谰", "蛆", "訓", "鉄", "痃", "炱", "蝻", "我", "暨", "骓", "馋",
|
||||
"埤", "脞",
|
||||
"晃", "螟", "洮", "泛", "掾", "穑", "米", "蕲", "玦", "讙", "逢", "劐", "袭", "凫", "僳", "畛", "晷",
|
||||
"鳕", "Ë",
|
||||
"愬", "坫", "鳡", "鞯", "叔", "胂", "囚", "筋", "青", "度", "涕", "琰", "﹔", "径", "陇", "睛", "链",
|
||||
"状", "逶",
|
||||
"蘅", "“", "庇", "邽", "纥", "踶", "爺", "狭", "钫", "桃", "弛", "淳", "办", "茕", "砸", "喱", "仅",
|
||||
"潞", "杈",
|
||||
"得", "咕", "俞", "检", "借", "恋", "驿", "倌", "钢", "琐", "哆", "撙", "箫", "川", "猥", "牢", "蹁",
|
||||
"城", "馏",
|
||||
"锡", "楝", "蛱", "奈", "瑶", "桺", "耆", "翟", "阒", "稲", "橐", "萱", "惹", "蘼", "主", "擦", "蟒",
|
||||
"台", "佬",
|
||||
"荫", "廖", "笏", "铕", "衣", "洇", "炒", "瀍", "崭", "圻", "洚", "契", "嫱", "倏", "晶", "了", "堠",
|
||||
"勰", "椎",
|
||||
"询", "梗", "飒", "锰", "览", "溇", "寻", "蓅", "【", "碇", "井", "露", "顔", "堌", "庳", "踩", "i",
|
||||
"饷", "俊",
|
||||
"楫", "條", "搭", "奍", "羽", "憋", "岘", "毡", "曜", "乃", "′", "针", "羲", "菓", "吩", "咩", "鞘",
|
||||
"尊", "宫",
|
||||
"舜", "啖", "惗", "北", "懊", "骇", "阄", "躅", "权", "缲", "肥", "铜", "《", "录", "也", "棬", "煮",
|
||||
"舄", "厮",
|
||||
"'", "順", "受", "霜", "新", "售", "牞", "圣", "妗", "犴", "宥", "哦", "陀", "卺", "冚", "蹒", "亸",
|
||||
"禮", "骰",
|
||||
"瑢", "弒", "抛", "谷", "嫰", "動", "嘌", "惩", "枣", "忌", "茡", "爵", "嘚", "郧", "丨", "敲", "帚",
|
||||
"沭", "槊",
|
||||
"⑶", "專", "毶", "圄", "磅", "蛭", "由", "蠹", "剜", "诫", "秆", "愠", "藓", "母", "请", "衩", "忸",
|
||||
"蜕", "饽",
|
||||
"晦", "倔", "腠", "痛", "品", "簧", "父", "锐", "描", "蓰", "蛴", "箍", "兕", "苜", "饼", "奚", "泗",
|
||||
"裥", "皂",
|
||||
"嵚", ",", "澶", "蠖", "沅", "馎", "籀", "菝", "眵", "糥", "铽", "痤", "颟", "淄", "作", "抉", "俄",
|
||||
"么", "郑",
|
||||
"耒", "佛", "1", "纡", "鸢", "④", "鎚", "壖", "遢", "鬈", "拢", "托", "哈", "節", "橦", "冼", "六",
|
||||
"耗", "樵",
|
||||
"涔", "舳", "龌", "衿", "婧", "栓", "椹", "嘘", "膊", "茁", "丹", "螃", "剖", "洧", "珞", "潺", "孱",
|
||||
"呐", "萩",
|
||||
"刷", "引", "说", "熟", "/", "靖", "酷", "耠", "饬", "菌", "洙", "荃", "饲", "酾", "阁", "陬", "铿",
|
||||
"倻", "牮",
|
||||
"鞡", "撕", "倘", "盒", "曺", "襦", "辄", "算", "塬", "潢", "羖", "湾", "续", "△", "疙", "谖", "嘅",
|
||||
"遑", "篚",
|
||||
"筮", "氍", "递", "尧", "G", "{", "分", "埒", "@", "蜍", "荼", "襆", "槭", "檠", "縢", "濉", "梆",
|
||||
"隔", "镛",
|
||||
"倞", "润", "瓯", "瓢", "蟊", "沐", "啷", "砚", "皱", "剅", "儙", "错", "幌", "滓", "砗", "郤", "喧",
|
||||
"峣", "簸",
|
||||
"毖", "踏", "锕", "…", "悖", "谧", "醵", "加", "镐", "泐", "傫", "胪", "缄", "卩", "蓼", "丸", "垌",
|
||||
"汞", "宴",
|
||||
"膙", "圊", "矻", "嚏", "漾", "幕", "駕", "葒", "绪", "袪", "镋", "杭", "澴", "鬃", "粟", "偻", "饳",
|
||||
"抨", "亟",
|
||||
"温", "韶", "轿", "罟", "际", "诖", "复", "坯", "骗", "*", "副", "裢", "憬", "邾", "崇", "蕈", "疮",
|
||||
"粽", "炝",
|
||||
"珲", "莅", "衾", "爲", "枯", "汛", "仁", "熏", "馥", "㎡", "檐", "锦", "竭", "颁", "遽", "瘙", "样",
|
||||
"遛", "殍",
|
||||
"湄", "消", "鳌", "痫", "鳏", "瓶", "窈", "谚", "麒", "鸹", "蟋", "横", "唠", "瘪", "媪", "侔", "鐵",
|
||||
"系", "杖",
|
||||
"m", "叉", "沟", "衢", "寘", "■", "弗", "建", "疣", "珣", "綦", "劈", "道", "嘈", "先", "芝", "降",
|
||||
"滕", "邵",
|
||||
"邺", "給", ")", "廨", "郛", "势", "氇", "坤", "昂", "焼", "奕", "闱", "朓", "毽", "还", "坨", "銭",
|
||||
"龂", "銎",
|
||||
"壽", "矸", "窒", "①", "玷", "蝽", "泃", "烀", "魈", "★", "慶", "K", "嘶", "酶", "呖", "殿", "乡",
|
||||
"䄂", "阳",
|
||||
"轪", "碱", "譬", "摩", "鳖", "刳", "地", "包", "貊", "悝", "圩", "今", "嚭", "凳", "谕", "馃", "捎",
|
||||
"佯", "侬",
|
||||
"愆", "微", "涤", "舔", "蛇", "筲", "助", "锾", "剧", "缧", "簪", "惚", "柢", "庾", "虹", "雪", "猡",
|
||||
"脔", "亶",
|
||||
"烨", "T", "锗", "芈", "女", "动", "偬", "琥", "县", "诣", "精", "嬗", "栀", "艨", "智", "冗", "闼",
|
||||
"嗝", "z",
|
||||
"夢", "拿", "鹲", "尤", "啮", "﹐", "ɔ", "钓", "施", "萼", "邻", "竞", "碶", "艰", "》", "翻", "馆",
|
||||
"橪", "逝",
|
||||
"臀", "淫", "枉", "羿", "拇", "溷", "徒", "涓", "關", "聋", "嵊", "殖", "叛", "敫", "舵", "亊", "诽",
|
||||
"菱", "苎",
|
||||
"破", "腚", "A", "嵋", "扊", "挂", "篷", "棂", "碟", "復", "劾", "韪", "疔", "粒", "鲵", "毙", "店",
|
||||
"锻", "衮",
|
||||
"寳", "◎", "斯", "倦", "醢", "曾", "茚", "荐", "隗", "芊", "豪", "亻", "哂", "堃", "宇", "桑", "匋",
|
||||
"植", "亥",
|
||||
"撂", "棒", "蟠", "W", "迟", "蚋", "溊", "缌", "鞚", "蚤", "適", "赌", "卣", "厚", "鲾", "匙", "槃",
|
||||
"郎", "鬏",
|
||||
"玳", "龄", "丈", "圮", "冑", "院", "葬", "嵐", "瓦", "孵", "漶", "星", "吐", "獍", "藠", "萍", "振",
|
||||
"潜", "龉",
|
||||
"匦", "粹", "諾", "畵", "峦", "&", "埕", "朵", "戒", "炳", "酪", "绂", "篁", "测", "殆", "涌", "业",
|
||||
"盏", "醊",
|
||||
"笆", "孰", "骊", "湛", "踰", "汎", "哲", "澙", "鲷", "√", "鄣", "亿", "螺", "吠", "伟", "凛", "骡",
|
||||
"恻", "巨",
|
||||
"扶", "泡", "峯", "韵", "腎", "睦", "栖", "}", "笙", "疌", "绶", "忒", "哥", "价", "纻", "薨", "漂",
|
||||
"濮", "缮",
|
||||
"勐", "妮", "傩", "陛", "陷", "柆", "瞭", "鲳", "烬", "喉", "固", "桡", "聊", "逦", "猊", "梻", "涵",
|
||||
"栒", "逍",
|
||||
"饥", "凼", "早", "姣", "蕤", "塌", "桀", "亳", "虻", "鹨", "典", "情", "怄", "商", "钍", "赚", "塥",
|
||||
"煽", "垱",
|
||||
"蝴", "乓", "籁", "帷", "锢", "圪", "快", "赘", "杵", "漠", "滴", "斩", "拈", "蚕", "陽", "篡", "郦",
|
||||
"瞻", "郯",
|
||||
"鳍", "幽", "旅", "乖", "鹖", "斫", "痂", "肸", "右", "锂", "永", "泾", "茎", "觱", "彼", "擎", "䨱",
|
||||
"翱", "徝",
|
||||
"醅", "求", "湫", "転", "溴", "師", "瓣", "蝠", "铭", "社", "苞", "仇", "噌", "你", "嗾", "雳", "榧",
|
||||
"駹", "雯",
|
||||
"叨", "遫", "氏", "航", "辗", "溢", "历", "楷", "诱", "雏", "梳", "藕", "屺", "槎", "钐", "燘", "棽",
|
||||
"驸", "褪",
|
||||
"清", "十", "廰", "移", "筌", "揾", "瞠", "姽", "馑", "恢", "逸", "p", "瑚", "茄", "鹧", "俗", "璟",
|
||||
"栊", "买",
|
||||
"瀛", "镒", "球", "氲", "缛", "講", "胀", "焒", "悲", "翕", "拗", "T", "桌", "脓", "闪", "稀", "狎",
|
||||
"火", "柁",
|
||||
"琴", "澍", "嗟", "龚", "楮", "噼", "隽", "栩", "焻", "哩", "藻", "瘸", "含", "偶", "界", "嘃", "昶",
|
||||
"澄", "頤",
|
||||
"绒", "鲁", "麝", "决", "撒", "岙", "季", "刿", "肝", "蒉", "蓇", "财", "完", "蠔", "脉", "肱", "谙",
|
||||
"蜮", "郭",
|
||||
"慨", "晔", "髂", "蛏", "眨", "钗", "葺", "惆", "娈", "瞵", "踞", "棁", "蝢", "嚎", "猝", "必", "剞",
|
||||
"关", "咛",
|
||||
"劫", "闸", "肯", "№", "莩", "哇", "蛑", "镬", "羡", "驊", "茂", "塍", "沓", "筱", "杉", "战", "茧",
|
||||
"耙", "击",
|
||||
"需", "腊", "酎", "畦", "葙", "鹘", "韭", "嚚", "争", "域", "伢", "鞲", "哳", "栲", "某", "翌", "哗",
|
||||
"焚", "螗",
|
||||
"懲", "躲", "約", "镖", "凿", "饶", "够", "剁", "铥", "应", "署", "杮", "蒂", " ", "坷", "礅", "款",
|
||||
"梁", "鄜",
|
||||
"髹", "選", "伤", "路", "З", "亲", "野", "啦", "捯", "憷", "鲩", "札", "怏", "塘", "绊", "愍", "簦",
|
||||
"牦", "黥",
|
||||
"鳜", "唉", "W", "沱", "蚺", "甪", "摉", "协", "耨", "娱", "桄", "仆", "类", "搡", "滤", "岗", "休",
|
||||
"坶", "谒",
|
||||
"忭", "飨", "闷", "菟", "鲣", "驷", "湜", "疡", "蚩", "萊", "䝉", "硒", "贺", "弃", "徘", "陨", "否",
|
||||
"遥", "妒",
|
||||
"X", "間", "觜", "跬", "夬", "羮", "喙", "赇", "鹗", "『", "砀", "残", "绿", "小", "勘", "瀌", "扉",
|
||||
"耧", "衅",
|
||||
"挟", "乐", "鹏", "墁", "澜", "噍", "坊", "術", "嗖", "知", "盉", "圆", "嗈", "蘖", "资", "爭", "=",
|
||||
"刑", "裒",
|
||||
"〈", "淸", "定", "袒", "戗", "钤", "吵", "旯", "蓝", "裎", "溅", "贰", "荏", "甥", "悌", "勤", "炽",
|
||||
"换", "躜",
|
||||
"!", "薄", "痱", "双", "匕", "肷", "挥", "茑", "船", "砝", "煤", "荜", "弘", "▏", "陆", "稔", "朽",
|
||||
"冤", "頉",
|
||||
"遊", "砰", "迎", "碎", "唪", "醪", "稆", "练", "锸", "阵", "皇", "香", "镀", "嫡", "持", "桶", "垄",
|
||||
"阍", "戥",
|
||||
"臣", "琛", "涘", "惶", "赙", "葆", "住", "舊", "枝", "媲", "蓣", "龅", "搦", "_", "图", "力", "纪",
|
||||
"悍", "麗",
|
||||
"戽", "腧", "绣", "跟", "哕", "打", "蝰", "Φ", "吞", "功", "夀", "劓", "沇", "熔", "占", "隰", "命",
|
||||
"佻", "豁",
|
||||
"苣", "楦", "掇", "蛛", "唢", "郜", "霉", "鲏", "予", "沸", "殻", "俯", "探", "篪", "荇", "邈", "烯",
|
||||
"忮", "伸",
|
||||
"岬", "×", "锧", "窸", "毪", "纩", "蛋", "讯", "骼", "叶", "楂", "犟", "站", "盘", "隈", "喝", "儣",
|
||||
"兵", "尚",
|
||||
"孙", "爿", "芜", "羁", "旖", "溽", "迩", "京", "7", "龃", "狝", "缦", "缁", "鲃", "怒", "故", "據",
|
||||
"枫", "髙",
|
||||
"亭", "耳", "飚", "O", "编", "箸", "幼", "氘", "鞮", "匐", "祯", "臃", "辫", "磋", "溝", "墙", "诚",
|
||||
"阻", "档",
|
||||
"歆", "璃", "悻", "婤", "映", "瑞", "牂", "话", "忠", "潘", "惋", "冬", "氦", "腔", "胬", "盔", "\"",
|
||||
"饮", "贶",
|
||||
"嚄", "儆", "溜", "砷", "樇", "跏", "泩", "馌", "埃", "莙", "革", "珙", "乌", "鍋", "穴", "石", "珺",
|
||||
"熹", "诞",
|
||||
"<", "腉", "姊", "钧", "罪", "拆", "赊", "殒", "堇", "仑", "掺", "塃", "獴", "迥", "盦", "檬", "益",
|
||||
"居", "鼑",
|
||||
"异", "嘻", "悔", "旮", "况", "時", "阋", "洛", "線", "#", "型", "迕", "睇", "橱", "笊", "蛞", "愚",
|
||||
"茉", "镈",
|
||||
"镞", "垭", "扁", "泫", "搬", "古", "书", "疸", "痨", "黟", "墉", "料", "并", "ㆍ", "裳", "鞑", "湮",
|
||||
"柠", "颐",
|
||||
"形", "━", "逹", "硁", "置", "韦", "瓞", "象", "殽", "均", "浓", "瞓", "椐", "洨", "乱", "襜", "终",
|
||||
"優", "睹",
|
||||
"敦", "鼬", "唆", "佼", "財", "瘃", "H", "痳", "勺", "依", "虎", "蕖", "玄", "缓", "滢", "^", "骅",
|
||||
"诘", "弋",
|
||||
":", "∩", "廪", "缈", "造", "蕉", "孖", "嫒", "寨", "意", "岽", "庶", "罗", "瞢", "酹", "蔟", "赴",
|
||||
"烂", "栋",
|
||||
"格", "矛", "驯", "词", "嗦", "剀", "蓓", "期", "鏢", "羑", "奴", "椱", "A", "狗", "烟", "蹬", "案",
|
||||
"记", "讴",
|
||||
"鳑", "侯", "霏", "焜", "沬", "份", "酦", "芗", "庚", "瑗", "鹎", "穗", "鲠", "肛", "厄", "蜔", "學",
|
||||
"伊", "⑥",
|
||||
"琪", "邒", "少", "霖", "蓖", "猜", "塾", "肾", "罃", "伐", "钩", "骈", "溟", "饵", "莉", "é", "刖",
|
||||
"洯", "堉",
|
||||
"锝", "趔", "七", "萁", "竹", "憾", "蚨", "离", "柔", "替", "侑", "飙", "气", "震", "厥", "备", "刻",
|
||||
"顽", "瞽",
|
||||
"腄", "雄", "燃", "旬", "简", "翠", "熥", "◇", "吃", "囡", "玙", "铷", "暖", "配", "傻", "窄", "皈",
|
||||
"夼", "舂",
|
||||
"乜", "苩", "攉", "雠", "茇", "锈", "酰", "粮", "祝", "考", "堍", "鳅", "彬", "▲", "孝", "蠊", "顇",
|
||||
"娲", "腥",
|
||||
"$", "珠", "厂", "诠", "蹓", "轼", "嵫", "捩", "硗", "胺", "证", "膀", "」", "胯", "钷", "毂", "柙",
|
||||
"深", "沄",
|
||||
"匹", "8", "爷", "礳", "秏", "窜", "魑", "d", "转", "烆", "屿", "眙", "極", "袤", "護", "V", "狂",
|
||||
"柑", "玠",
|
||||
"氩", "’", "馊", "玛", "坢", "%", "燔", "颗", "舅", "暂", "艾", "芹", "溏", "晰", "件", "琚", "仿",
|
||||
"祾", "酤",
|
||||
"騠", "揳", "鲫", "蜥", "仨", "牺", "步", "讓", "港", "煲", "铴", "腦", "鳝", "危", "鋪", "冠", "正",
|
||||
"柽", "抍",
|
||||
"掘", "控", "娴", "娀", "離", "手", "臾", "酗", "筼", "煸", "弹", "照", "哎", "毒", "颀", "诙", "刚",
|
||||
"搢", "䧳",
|
||||
"峒", "滋", "\\", "匀", "黉", "毓", "娠", "床", "浪", "祐", "铟", "4", "?", "凄", "飗", "蚍", "葑",
|
||||
"抗", "鹞",
|
||||
"糸", "红", "英", "违", "橡", "眷", "防", "缬", "龠", "察", "仍", "辇", "减", "闫", "箴", "龍", "館",
|
||||
"屙", "翙",
|
||||
"媽", "涴", "到", "旻", "删", "瞾", "鏖", "咭", "豨", "荘", "炭", "畼", "构", "锘", "鉫", "候", "扇",
|
||||
"繄", "猩",
|
||||
"瘵", "恺", "贵", "榦", "息", "恽", "胎", "狰", "雜", "辋", "璜", "硈", "泠", "呔", "蹿", "踹", "摄",
|
||||
"炀", "坞",
|
||||
"蹄", "裝", "赛", "蝥", "塔", "靳", "荬", "找", "仡", "淮", "比", "淆", "义", "淝", "卢", "辟", "寂",
|
||||
"庒", "鳯",
|
||||
"暲", "景", "邪", "腻", "赍", "甍", "讲", "哌", "嶝", "鎌", "总", "缱", "问", "磛", "谅", "拉", "靈",
|
||||
"奭", "沆",
|
||||
"茔", "羅", "鄠", "網", "吏", "懵", "鑫", "歌", "黹", "嵘", "涞", "碳", "崂", "婥", "赞", "镑", "購",
|
||||
"幺", "鸰",
|
||||
"饟", "蝌", "忝", "懒", "禺", "梽", "齉", "恳", "拯", "弥", "荡", "芾", "幪", "厌", "馒", "蜘", "欸",
|
||||
"吣", "1",
|
||||
"却", "榻", "碾", "袂", "錎", "钬", "無", "嬉", "笞", "蹴", "视", "雇", "创", "椟", "6", "瘁", "斜",
|
||||
"傥", "喃",
|
||||
"炷", "秾", "嘱", "茀", "犄", "窑", "庀", "潍", "伦", "䀲", "凉", "Р", "撻", "萜", "二", "倨", "蔑",
|
||||
"捕", "勚",
|
||||
"士", "鈇", "踺", "啤", "彧", "缪", "述", "傅", "颅", "畸", "畜", "滗", "慭", "琎", "斌", "参", "胳",
|
||||
"骖", "稠",
|
||||
"汰", "铻", "闯", "留", "蘘", "沏", "亦", "择", "華", "禽", "砟", "祼", "狃", "噫", "狼", "寤", "跪",
|
||||
"浠", "·",
|
||||
"费", "瓘", "鼹", "锪", "箢", "垣", "慊", "虏", "秩", "偉", "镏", "钯", "恐", "鹃", "菇", "炸", "潮",
|
||||
"蟀", "硂",
|
||||
"偌", "哏", "验", "桉", "阴", "初", "掴", "鹺", "峨", "赋", "舉", "裹", "赶", "土", "淋", "瘌", "沔",
|
||||
"r", "赀",
|
||||
"淖", "茯", "怛", "谜", "洗", "似", "舡", "纳", "晓", "R", "诐", "痹", "漪", "顺", "挛", "阎", "贝",
|
||||
"钰", "惬",
|
||||
"疬", "菀", "埘", "怙", "部", "译", "鲭", "窋", "敢", "夜", "撰", "珅", "特", "襕", "癖", "胡", "⒃",
|
||||
"附", "擘",
|
||||
"痢", "尬", "鉴", "瞋", "膨", "阽", "挲", "⒄", "骎", "帕", "缕", "计", "障", "鳆", "隹", "朔", "碹",
|
||||
"当", "迦",
|
||||
"氙", "蘑", "妓", "炬", "苊", "萎", "浈", "沥", "绯", "壤", "噱", "蹾", "驶", "葱", "孕", "羹", "钻",
|
||||
"農", "勝",
|
||||
"膈", "灿", "赆", "靿", "耱", "陪", "忙", "缰", "奶", "儒", "个", "朱", "燹", "琮", "轷", "錾", "箅",
|
||||
"澳", "嗥",
|
||||
"攥", "没", "匿", "鲆", "|", "矣", "他", "鸶", "芸", "B", "髑", "街", "巿", "廣", "盯", "監", "鲸",
|
||||
"胭", "凬",
|
||||
"寿", "挝", "绽", "+", "劝", "究", "眢", "集", "衙", "卷", "j", "跶", "牡", "畯", "貅", "销", "發",
|
||||
"咱", "蓊",
|
||||
"揣", "咝", "琶", "荦", "阌", "盅", "嘹", "苟", "醮", "洪", "鲧", "钒", "柱", "氨", "旰", "冽", "茭",
|
||||
"嵇", "粲",
|
||||
"蛾", "訾", "辔", "N", "尹", "趿", "蹲", "疟", "祠", "段", "車", "网", "⒉", "舷", "廐", "侣", "棵",
|
||||
"粜", "觐",
|
||||
"铼", "锁", "兒", "舁", "时", "垦", "版", "摈", "扳", "见", "腮", "嫖", "痭", "呆", "簖", "伋", "鳙",
|
||||
"珊", "麂",
|
||||
"既", "谴", "热", "超", "蠕", "铞", "e", "殓", "因", "锿", "文", "禊", "皙", "鑙", "爹", "鋼", "忻",
|
||||
"秣", "镁",
|
||||
"奠", "橉", "畺", "笮", "疹", "湝", "龟", "殃", "毵", "溃", "勢", "索", "砉", "阼", "堞", "酥", "冁",
|
||||
"喊", "¥",
|
||||
"幛", "娇", "锲", "蕃", "铘", "铍", "鴿", "响", "傲", "脏", "杓", "罕", "笥", "弦", "但", "缃", "扬",
|
||||
"盲", "碚",
|
||||
"幢", "鎖", "缺", "钋", "麽", "禳", "浃", "啄", "昧", "蒴", "帙", "琏", "咧", "舰", "亵", "浊", "豳",
|
||||
"衲", "俏",
|
||||
"镵", "浩", "勾", "槛", "榈", "徙", "鹤", "洹", "铂", "揎", "棕", "挦", "挫", "阆", "衹", "甚", "近",
|
||||
"】", "簏",
|
||||
"汽", "踮", "淌", "檇", "痔", "谝", "钙", "蕞", "蔯", "兆", "蔽", "后", "蚬", "谸", "芟", "枞", "叫",
|
||||
"栗", "餘",
|
||||
"营", "郝", "氯", "㺃", "狍", "冏", "庛", "纱", "泼", "碍", "认", "邓", "茵", "饧", "闟", "惝", "裙",
|
||||
"噙", "忘",
|
||||
"虬", "群", "S", "佗", "恼", "坟", "肮", "皮", "玃", "在", "赧", "孚", "偾", "镨", "恨", "葡", "西",
|
||||
"缞", "挠",
|
||||
"逃", "吾", "膪", "焦", "翘", "桧", "变", "渗", "繁", "際", "痘", "撼", "筅", "坑", "前", "玑", "数",
|
||||
"融", "鲌",
|
||||
"讦", "窃", "鄌", "伾", "众", "攻", "彪", "锎", "焐", "殛", "锊", "嗉", "枓", "抢", "鞠", "掩", "贾",
|
||||
"搔", "皁",
|
||||
"拶", "朗", "渺", "跛", "㛃", "鏾", "慥", "杆", "沈", "戍", "豫", "楠", "爆", "汤", "昉", "耘", "缡",
|
||||
".", "允",
|
||||
"揜", "责", "艟", "裁", "喬", "砹", "鹣", "裼", "啉", "蛳", "酮", "听", "维", "阪", "獾", "浣", "訂",
|
||||
"瘿", "蜡",
|
||||
"泖", "蔚", "貔", "致", "禨", "尓", "糺", "绐", "遯", "笄", "邦", "圈", "洟", "缟", "槲", "桹", "镓",
|
||||
"骒", "髫",
|
||||
"暾", "像", "縻", "戊", "飧", "驽", "干", "万", "绕", "披", "雅", "桊", "卡", "贲", "吡", "沧", "鳟",
|
||||
"堂", "扺",
|
||||
"岱", "封", "鄭", "螣", "瞩", "幞", "邕", "睫", "涩", "自", "趱", "愣", "威", "酊", "罂", "慑", "袴",
|
||||
"架", "烘",
|
||||
"现", "灞", "钔", "股", "興", "乍", "噜", "济", "碛", "兀", "诅", "柴", "瓿", "[", "怿", "竦", "白",
|
||||
"黄", "阶",
|
||||
"务", "榮", "澹", "谏", "垓", "跸", "繻", "窿", "紊", "陟", "劁", "嗑", "牯", "厉", "敛", "鮕", "嘉",
|
||||
"蔻", "鼎",
|
||||
"恒", "硝", "溉", "骘", "窘", "任", "裱", "处", "旨", "舶", "缸", "囹", "笠", "讥", "泜", "脊", "煊",
|
||||
"淦", "牝",
|
||||
"硕", "胧", "泚", "溪", "贪", "牛", "答", "瘴", "Q", "炯", "⑤", "篾", "銀", "乩", "杶", "垆", "蛐",
|
||||
"苔", "啪",
|
||||
"y", "玮", "琫", "寮", "邂", "後", "僵", "贴", "硭", "枚", "姆", "乎", "讶", "醭", "橥", "脱", "蒈",
|
||||
"擞", "忪",
|
||||
"顾", "柚", "褿", "忲", "辖", "铡", "螠", "殉", "喆", "爡", "轮", "棰", "鲉", "跃", "韬", "睡", "嘧",
|
||||
"袅", "圗",
|
||||
"檄", "踊", "阀", "题", "桫", "林", "沉", "禚", "散", "麇", "沦", "秋", "导", "斑", "宰", "嘞", "暑",
|
||||
"笱", "搋",
|
||||
"擅", "镤", "锶", "L", "厣", "有", "猗", "袆", "绞", "甭", "歧", "跣", "潦", "専", "绑", "飱", "廓",
|
||||
"磔", "接",
|
||||
"腓", "窎", "瑁", "飓", "蟪", "俎", "П", "缉", "䘵", "夙", "潟", "桷", "淡", "虺", "恶", "|", "驭",
|
||||
"怀", "邋",
|
||||
"辢", "逻", "晖", "蜃", "蜊", "溻", "冢", "尻", "礼", "厝", "亘", "酴", "饔", "悸", "戆", "什", "玚",
|
||||
"馔", "哔",
|
||||
"沃", "竑", "葭", "垞", "鏂", "抃", "弄", "去", "焊", "焌", "x", "苇", "與", "炼", "蛄", "莴", "阏",
|
||||
"薷", "禀",
|
||||
"鸯", "栽", "冒", "姓", "0", "尃", "蜞", "毗", "骟", "秸", "荸", "柈", "恬", "赡", "侏", "兑", "蝤",
|
||||
"荷", "徳",
|
||||
"押", "挣", "腰", "宣", "鸵", "葳", "遘", "讨", "狒", "涿", "囤", "邃", "蒜", "疑", "脍", "嘟", "鹠",
|
||||
"吻", "鄹",
|
||||
"耦", "华", "霸", "侥", "勒", "挞", "臊", "尺", "让", "榆", "阝", "鳚", "灾", "鲬", "艿", "Ⅱ", "锩",
|
||||
"攮", "蚊",
|
||||
"蔁", "唝", "涅", "挹", "淏", "鏊", "氵", "鹕", "律", "對", "粱", "恫", "挻", "滏", "叮", "‰", "鼯",
|
||||
"绫", "秉",
|
||||
"怩", "質", "岐", "菊", "佤", "帏", "骺", "爰", "珉", "耪", "乞", "郕", "鲱", "雷", "蒿", "不", "啐",
|
||||
"侓", "郓",
|
||||
"歼", "拒", "胗", "寕", "旒", "勁", "婢", "诌", "蹂", "姐", "媳", "歃", "拐", "辐", "拟", "醯", "雌",
|
||||
"点", "玟",
|
||||
"您", "鲨", "载", "藩", "罔", "噀", "抹", "萏", "补", "蝎", "辈", "劢", "乚", "唔", "瓜", "恿", "蟭",
|
||||
"涸", "纬",
|
||||
"睽", "樣", "帻", "蓥", "谯", "柯", "渴", "脁", "诲", "福", "③", "熊", "羌", "疴", "袖", "虿", "杏",
|
||||
"覌", "易",
|
||||
"醾", "筒", "肤", "苷", "柃", "榇", "酬", "癸", "啰", "眛", "稷", "展", "鸾", "祢", "蝾", "敵", "毛",
|
||||
"痦", "老",
|
||||
"赐", "单", "淹", "畋", "符", "奎", "绥", "轾", "鄺", "濩", "眦", "觎", "忾", "璋", "刘", "翳", "菪",
|
||||
"簟", "胼",
|
||||
"孫", "機", "獻", "低", "o", "捅", "鳥", "⑨", "卸", "废", "启", "呋", "窥", "巢", "揭", "咴", "趺",
|
||||
"鲥", "空",
|
||||
"膄", "崮", "锜", "络", "納", "恤", "刭", "批", "霭", "氧", "钮", "甑", "祲", "粘", "辏", "∶", "臨",
|
||||
"│", "粪",
|
||||
"惰", "肼", "浉", "橄", "5", "東", "8", "漁", "浜", "忍", "奂", "遹", "扃", "扦", "入", "欤", "豆",
|
||||
"悠", "蕴",
|
||||
"萑", "媄", "龈", "÷", "磙", "鸬", "缎", "嗯", "浕", "木", "陋", "柰", "瘩", "箨", "松", "躁", "鲇",
|
||||
"彰", "恕",
|
||||
"楗", "姨", "撅", "诹", "戮", "桓", "棉", "束", "嗍", "庋", "瞿", "郂", "哪", "町", "a", "铁", "洫",
|
||||
"失", "栳",
|
||||
"篇", "鳗", "眇", "椿", "義", "就", "都", "镌", "阖", "夭", "拃", "跚", "業", "圬", "演", "篥", "3",
|
||||
"昼", "從",
|
||||
"瞍", "蓦", "鼾", "坪", "觫", "鲍", "馿", "妾", "密", "奥", "耰", "佟", "嘤", "貘", "薯", "稽", "届",
|
||||
"褛", "钠",
|
||||
"猿", "佈", "倍", "铩", "铙", "踵", "o", "捱", "鹐", "當", "狷", "写", "遆", "钱", "姗", "寸", "综",
|
||||
"挑", "礶",
|
||||
"靼", "溯", "湟", "漱", "碰", "职", "味", "Λ", "璁", "壴", "给", "碘", "恭", "苴", "酚", "套", "宕",
|
||||
"辽", "窀",
|
||||
"催", "踢", "惟", "璧", "翔", "稿", "癀", "霈", "光", "膑", "妆", "庡", "圾", "躺", "惮", "切", "目",
|
||||
"梦", "岫",
|
||||
"飑", "叠", "累", "鴻", "透", "量", "妻", "沨", "俪", "屣", "挚", "w", "厅", "货", "箧", "漦", "隙",
|
||||
"瘥", "妃",
|
||||
"門", "鹦", "隳", "龆", "瞪", "跑", "帼", "刺", "起", "鞴", "岸", "渫", "莽", "髭", "耜", "瑛", "葉",
|
||||
"宵", "绠",
|
||||
"眭", "畅", "茱", "卜", "濂", "浍", "冈", "脬", "厘", "夔", "纟", "槚", "哐", "萋", "曼", "膳", "潲",
|
||||
"啜", "啊",
|
||||
"猾", "捉", "箕", "㐂", "藝", "艘", "園", "妲", "灵", "蚌", "范", "&", "疥", "陑", "洒", "唾", "a",
|
||||
"婄", "谋",
|
||||
"t", "唬", "宓", "瓷", "︰", "焗", "默", "噔", "菡", "恃", "亩", "边", "啟", "颞", "啬", "盟", "墩",
|
||||
"汭", "缝",
|
||||
"魇", "酡", "疗", "梾", "尾", "鹿", "锔", "溦", "酃", "囊", "掉", "罥", "报", "诤", "喹", "蕙", "割",
|
||||
"蠢", "兼",
|
||||
"俺", "升", "屦", "獠", "辣", "跹", "颧", "宪", "抬", "咿", "沲", "旗", "荩", "傒", "鳳", "變", "偈",
|
||||
"薹", "钭",
|
||||
"垧", "谪", "躐", "谀", "R", "字", "怆", "陵", "追", "迓", "舞", "卲", "捲", "麾", "税", "方", "竴",
|
||||
"壹", "射",
|
||||
"轫", "塞", "僧", "椠", "突", "阕", "惫", "U", "佞", "秒", "友", "視", "莹", "攒", "荻", "俅", "筐",
|
||||
"能", "刓",
|
||||
"拍", "浥", "揽", "谨", "军", "巷", "怵", "额", "恪", "荀", "糍", "湎", "褙", "觇", "效", "估", "噩",
|
||||
"恍", "聚",
|
||||
"涛", "式", "兜", "汔", "祃", "銘", "烺", "瑄", "枘", "丁", "樗", "堙", "窗", "號", "鬣", "阈", "乳",
|
||||
"簇", "绳",
|
||||
"淞", "]", "征", "聩", "凸", "礓", "柘", "懂", "㧎", "纷", "里", "爸", "靥", "莞", "馅", "仞", "芏",
|
||||
"莳", "殪",
|
||||
"煌", "落", "佚", "帱", "诶", "家", "将", "浯", "抿", "讫", "趸", "筢", "绩", "原", "专", "滂", "嗳",
|
||||
"踬", "泱",
|
||||
"體", "蜱", "绵", "伻", "埗", "妤", "蔺", "赳", "嘢", "梨", "鹪", "烤", "镦", "赉", "崞", "蚰", "骝",
|
||||
"幅", "汴",
|
||||
"丽", "访", "叩", "羼", "亓", "恸", "燥", "笤", "丑", "枰", "守", "蜚", "戈", "高", "q", "瓻", "隘",
|
||||
"H", "南",
|
||||
"剎", "扭", "骠", "孜", "园", "锬", "审", "¥", "罚", "购", "裰", "泔", "醌", "醒", "绎", "莪", "掬",
|
||||
"睾", "鹁",
|
||||
"蛊", "票", "剃", "释", "调", "黛", "撩", "篱", "茌", "敓", "蜜", "魉", "鳤", "昽", "俳", "辁", "键",
|
||||
"犯", "嶙",
|
||||
"狈", "邬", "枋", "婊", "憎", "督", "救", "措", "足", "码", "栟", "虼", "棻", "阅", "肊", "坛", "鸱",
|
||||
"侩", "烫",
|
||||
"湉", "脐", "戾", "旎", "膚", "椒", "境", "绛", "濛", "蓄", "章", "压", "窦", "彖", "阜", "咻", "神",
|
||||
"存", "诺",
|
||||
"課", "覺", "鲈", "渚", "「", "缢", "仓", "氅", "筇", "桴", "荥", "些", "纯", "肋", "退", "妪", "别",
|
||||
"書", "租",
|
||||
"嚼", "芘", "笳", "涰", "馐", "熵", "犹", "朕", "猪", "蔸", "參", "觳", "舖", "蝙", "骏", "织", "躏",
|
||||
"纹", "锹",
|
||||
"蛉", "撑", "氽", "茶", "俛", "誠", "胖", "崽", "炫", "乘", "把", "趑", "谵", "骧", "犍", "鸮", "灌",
|
||||
"焯", "倭",
|
||||
"狁", "盱", "踱", "滠", "儡", "诸", "芽", "駆", "蹼", "虱", "祇", " ", "蠡", "衄", "谬", "蒍", "婺",
|
||||
"蘧", "博",
|
||||
"捍", "磁", "慷", "釂", "蛩", "钕", "祎", "呑", "贫", "斗", "菅", "操", "规", "硼", "惕", "丫", "俭",
|
||||
"肿", "骀",
|
||||
"砼", "句", "茛", "闶", "钏", "饻", "圠", "萌", "魏", "铃", "摞", "┅", "伎", "獭", "田", "钴", "峭",
|
||||
"魅", "捋",
|
||||
"唼", "鹅", "祚", "嬷", "圖", "抄", "嶂", "鸳", "溧", "钵", "嗽", "墠", "锌", "愈", "併", "踟", "羯",
|
||||
"翅", "纠",
|
||||
"勻", "岚", "菖", "便", "祈", "毳", "屹", "掰", "倬", "扛", "巴", "拮", "绁", "跌", "飯", "嵯", "翮",
|
||||
"堤", "诃",
|
||||
"腑", "皆", "鄂", "胾", "片", "这", "浙", "雨", "鼱", "袼", "鹊", "厢", "蛣", "摇", "蛮", "揍", "⒅",
|
||||
"啱", "薇",
|
||||
"岈", "兔", "谇", "纶", "肉", "崩", "甩", "涪", "馼", "铣", "锚", "丙", "雲", "烹", "钦", "撄", "铬",
|
||||
"令", "虮",
|
||||
"湊", "禹", "抖", "喏", "旺", "畲", "戬", "嗷", "釜", "车", "缀", "玕", "谐", "啁", "怎", "下", "惑",
|
||||
"恅", "藿",
|
||||
"筰", "帐", "祸", "镝", "喵", "刁", "习", "藏", "墓", "护", "聲", "箦", "严", "按", "谛", "睨", "艚",
|
||||
"歉", "蟮",
|
||||
"胶", "盍", "鳃", "狯", "垫", "杠", "线", "3", "葖", "t", "熬", "虞", "嶶", "篮", "黻", "墒", "氟",
|
||||
"嫠", "漕",
|
||||
"腾", "哟", "玫", "撇", "垍", "靛", "翼", "淅", "省", "斥", "稞", "蠼", "谩", "埠", "蘸", "刊", "烝",
|
||||
"宁", "鹚",
|
||||
"龊", "苪", "袷", "诧", "细", "蒌", "焅", "2", "筹", "扒", "卮", "捞", "净", "菲", "逮", "槍", "蛎",
|
||||
"莱", "黠",
|
||||
"逖", "辚", "剥", "啴", "诿", "楱", "氪", "领", "嗒", "藐", "惜", "甘", "佾", "嵴", "胫", ";", "晳",
|
||||
"锣", "瞅",
|
||||
"缷", "耶", "搤", "策", "咽", "邀", "霾", "悟", "属", "鸪", "牴", "贞", "趁", "丞", "瘆", "豌", "著",
|
||||
"饿", "筠",
|
||||
"划", "璇", "损", "卵", "腒", "畏", "盥", "耐", "圏", "拓", "蒙", "鋫", "劙", "蹦", "熘", "烊", "匏",
|
||||
"咔", "轘",
|
||||
"沽", "菩", "罴", "磉", "炖", "假", "枪", "龙", "俸", "焱", "四", "─", "毫", "涨", "浇", "椰", "賣",
|
||||
"蘇", "真",
|
||||
"安", "坝", "枕", "鸼", "昵", "亨", "苤", "祷", "枧", "赟", "菑", "鳂", "戌", "悄", "種", "鳢", "嗣",
|
||||
"電", "颥",
|
||||
"妯", "谟", "蜒", "训", "泍", "洁", "勇", "哿", "扰", "蟆", "螂", "刃", "絜", "曪", "乒", "湖", "鞨",
|
||||
"懜", "夤",
|
||||
"哓", "胥", "桞", "俇", "肣", "半", "于", "橼", "锑", "熙", "甜", "槌", "盾", "屃", "缗", "共", "碗",
|
||||
"凇", "笔",
|
||||
"阿", "擗", "袍", "敉", "钾", "俶", "蚪", "琤", "凱", "辕", "à", "恣", "皴", "創", "寥", "妳", "腈",
|
||||
"畤", "迤",
|
||||
"垠", "触", "趙", "铤", "逊", "羊", "碉", "锱", "骨", "仄", "斟", "俚", "啡", "芩", "迫", "杷", "种",
|
||||
"鹄", "牗",
|
||||
"耑", "艇", "芰", "整", "慙", "飛", "甓", "岩", "鸦", "黎", "僊", "糁", "谈", "洋", "椑", "健", "〕",
|
||||
"內", "言",
|
||||
"掷", "倚", "姬", "矿", "灯", "阐", "凋", "銮", "豇", "瑰", "抓", "噪", "堋", "吁", "妈", "庥", "彳",
|
||||
"鄗", "闩",
|
||||
"夫", "0", "庠", "悬", "妙", "琵", "着", "首", "熣", "瞰", "揆", "燚", "条", "姜", "滘", "麓", "鳉",
|
||||
"椀", "蓿",
|
||||
"廠", "泌", "蝈", "倕", "丛", "耍", "且", "蝇", "凉", "豐", "泪", "臼", "服", "刍", "織", "渎", "尥",
|
||||
"甙", "埋",
|
||||
"珏", "援", "祖", "彊", "臱", "惦", "葴", "礁", "达", "橋", "钨", "崦", "醐", "巳", "中", "颜", "溠",
|
||||
"铹", "负",
|
||||
"抠", "愎", "罘", "雩", "胝", "冱", "筵", "篙", "材", "肓", "○", "迅", "腼", "橙", "仵", "茏", "慵",
|
||||
"齌", "琯",
|
||||
"疃", "貢", "豉", "瞟", "忉", "禄", "通", "咒", "愫", "秕", "筜", "觖", "州", "渑", "胆", "喑", "張",
|
||||
"衖", "洺",
|
||||
"眉", "榞", "色", "邶", "攫", "堑", "淬", "嗪", "肐", "殣", "辊", "隧", "献", "潤", "蓺", "函", "鹱",
|
||||
"轭", "劭",
|
||||
"椁", "膜", "亹", "侧", "貂", "哚", "磴", "蠋", "囔", "险", "伶", "世", "菥", "莶", "瘾", "燫", "延",
|
||||
"∵", "毋",
|
||||
"羧", "无", "雎", "曛", "沚", "巍", "g", "熄", "恰", "伷", "開", "冻", "颛", "支", "⑦", "鞥", "赖",
|
||||
"试", "泮",
|
||||
"联", "沮", "穰", "è", "峻", "滿", "豊", "刎", "鴈", "覆", "串", "锃", "春", "储", "矍", "哺", "评",
|
||||
"猁", "愉",
|
||||
"疳", "閃", "奄", "甲", "墦", "頭", "锫", "俦", "玩", "搀", "砭", "流", "橇", "泅", "琇", "趣", "∧",
|
||||
"辑", "灊",
|
||||
"貴", "迮", "摹", "霄", "濟", "限", "彀", "匪", "缂", "觚", "奇", "诋", "灼", "萘", "狠", "澥", "岂",
|
||||
"悺", "闾",
|
||||
"麋", "号", "槽", "姹", "陉", "瑯", "尉", "h", "绖", "宿", "戋", "粝", "砂", "该", "鞧", "翯", "釘",
|
||||
"铢", "窨",
|
||||
"設", "⒆"]`
|
||||
}
|
||||
File diff suppressed because one or more lines are too long
@@ -1,33 +0,0 @@
|
||||
package ddddocr
|
||||
|
||||
import (
|
||||
"embed"
|
||||
_ "embed"
|
||||
"likedream-goframe/internal/application/recognizer"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
)
|
||||
|
||||
//go:embed static/*
|
||||
var fs embed.FS
|
||||
|
||||
func initModel() string {
|
||||
if !recognizer.OnnxIsInit {
|
||||
recognizer.InitOnnx()
|
||||
}
|
||||
// 检测文件是否存在
|
||||
exePath := strings.ToLower(os.Args[0])
|
||||
// 获取绝对路径
|
||||
absPath, _ := filepath.Abs(exePath)
|
||||
staticPath := filepath.Join(filepath.Dir(absPath), "static")
|
||||
if _, err := os.Stat(staticPath); os.IsNotExist(err) {
|
||||
os.Mkdir(staticPath, 0755)
|
||||
}
|
||||
modelPath := filepath.Join(staticPath, "common_old.onnx")
|
||||
modelBytes, _ := fs.ReadFile("static/common_old.onnx")
|
||||
if _, err := os.Stat(modelPath); os.IsNotExist(err) {
|
||||
os.WriteFile(modelPath, modelBytes, 0644)
|
||||
}
|
||||
return modelPath
|
||||
}
|
||||
Binary file not shown.
@@ -1,59 +0,0 @@
|
||||
package recognizer
|
||||
|
||||
import (
|
||||
"embed"
|
||||
_ "embed"
|
||||
"likedream-goframe/internal/framework/logger"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"strings"
|
||||
|
||||
ort "github.com/yalue/onnxruntime_go"
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
//go:embed static/*
|
||||
var fs embed.FS
|
||||
|
||||
// OnnxIsInit 初始化onnx配置
|
||||
var OnnxIsInit = false
|
||||
|
||||
func InitOnnx() {
|
||||
name, bytes := getOnnxRuntimeName()
|
||||
// 检测文件是否存在
|
||||
exePath := strings.ToLower(os.Args[0])
|
||||
// 获取绝对路径
|
||||
absPath, _ := filepath.Abs(exePath)
|
||||
filePath := filepath.Join(filepath.Dir(absPath), name)
|
||||
if _, err := os.Stat(filePath); os.IsNotExist(err) {
|
||||
os.WriteFile(filePath, bytes, 0644)
|
||||
}
|
||||
ort.SetSharedLibraryPath(filePath)
|
||||
err := ort.InitializeEnvironment()
|
||||
if err != nil {
|
||||
logger.Logger.Panic("初始化onnx:", zap.String("绝对路径", filePath))
|
||||
return
|
||||
}
|
||||
OnnxIsInit = true
|
||||
}
|
||||
|
||||
func getOnnxRuntimeName() (onnxRuntimeFileName string, onnxRuntimeFileBytes []byte) {
|
||||
systemOS := runtime.GOOS
|
||||
switch systemOS {
|
||||
case "windows":
|
||||
onnxRuntimeFileName = "onnxruntime.dll"
|
||||
onnxRuntimeFileBytes, _ = fs.ReadFile("static/onnxruntime.dll")
|
||||
return
|
||||
case "darwin":
|
||||
onnxRuntimeFileName = "onnxruntime.dylib"
|
||||
onnxRuntimeFileBytes, _ = fs.ReadFile("static/libonnxruntime.1.23.2.dylib")
|
||||
return
|
||||
case "linux":
|
||||
onnxRuntimeFileName = "onnxruntime.so"
|
||||
onnxRuntimeFileBytes, _ = fs.ReadFile("static/libonnxruntime.so.1.23.2")
|
||||
return
|
||||
}
|
||||
logger.Logger.Panic("暂不支持的操作系统:", zap.String("系统名称", systemOS))
|
||||
return "", nil
|
||||
}
|
||||
File diff suppressed because one or more lines are too long
@@ -1,192 +0,0 @@
|
||||
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
|
||||
}
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user