① 前端:取消二维码解析的后端降级。二维码一律由前端 jsQR 解码并提取 uuid; 识别失败直接提示用户换更清晰的图片,不再把图片回传后端。 ② 后端:删除 POST /task/qr/decode 接口(handler.DecodeTaskQr)及其路由; 删除 qrcode.go 中所有图片解码相关代码(DecodeQRFromImage / decodeQRImage / scaleImage / ResolveQrUuidFromContent / decodeQrTextToUuid)——后端不再接收二维码图片。 保留 ExtractQrUuid(纯正则文本提取),提交签到(fillScanSubmitForm)与历史值归一化仍在用。 ③ 依赖:go mod tidy 移除 github.com/makiuchi-d/gozxing(不再被引用)。 golang.org/x/image 因验证码字体(captcha.go)仍在用,保留。 删除仅供已删测试使用的 internal/service/cpdaily/testdata/test_qr.png。 ④ 前端 API:移除 auth.ts 的 decodeQr()。 ⑤ 验证:go build / go vet / go test ./...、vue-tsc --noEmit、npm run build 全过。
This commit is contained in:
@@ -95,7 +95,6 @@ func main() {
|
||||
task.Post("/form/:id", handler.SaveTaskForm)
|
||||
task.Put("/form/:id", handler.SaveTaskForm)
|
||||
task.Get("/form-data/:id", handler.GetTaskSavedForm)
|
||||
task.Post("/qr/decode", handler.DecodeTaskQr)
|
||||
task.Put("/update/:id", handler.UpdateTask)
|
||||
task.Delete("/delete/:id", handler.DeleteTask)
|
||||
task.Get("/execute/:id", handler.ExecuteTask)
|
||||
|
||||
@@ -8,7 +8,6 @@ require (
|
||||
github.com/go-resty/resty/v2 v2.16.5
|
||||
github.com/gofiber/fiber/v2 v2.52.13
|
||||
github.com/golang-jwt/jwt/v5 v5.3.1
|
||||
github.com/makiuchi-d/gozxing v0.1.1
|
||||
github.com/redis/go-redis/v9 v9.7.0
|
||||
golang.org/x/crypto v0.50.0
|
||||
golang.org/x/image v0.46.0
|
||||
@@ -37,5 +36,4 @@ require (
|
||||
golang.org/x/net v0.52.0 // indirect
|
||||
golang.org/x/sys v0.48.0 // indirect
|
||||
golang.org/x/text v0.42.0 // indirect
|
||||
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 // indirect
|
||||
)
|
||||
|
||||
@@ -33,8 +33,6 @@ github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ=
|
||||
github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
|
||||
github.com/klauspost/compress v1.17.9 h1:6KIumPrER1LHsvBVuDa0r5xaG0Es51mhhB9BQB2qeMA=
|
||||
github.com/klauspost/compress v1.17.9/go.mod h1:Di0epgTjJY877eYKx5yC51cX2A2Vl2ibi7bDH9ttBbw=
|
||||
github.com/makiuchi-d/gozxing v0.1.1 h1:xxqijhoedi+/lZlhINteGbywIrewVdVv2wl9r5O9S1I=
|
||||
github.com/makiuchi-d/gozxing v0.1.1/go.mod h1:eRIHbOjX7QWxLIDJoQuMLhuXg9LAuw6znsUtRkNw9DU=
|
||||
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
|
||||
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
|
||||
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
|
||||
@@ -132,8 +130,6 @@ golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
|
||||
golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58=
|
||||
golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk=
|
||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE=
|
||||
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
gorm.io/driver/mysql v1.6.0 h1:eNbLmNTpPpTOVZi8MMxCi2aaIm0ZpInbORNXDwyLGvg=
|
||||
gorm.io/driver/mysql v1.6.0/go.mod h1:D/oCC2GWK3M/dqoLxnOlaNKmXz8WNTfcS9y5ovaSqKo=
|
||||
gorm.io/driver/sqlite v1.6.0 h1:WHRRrIiulaPiPFmDcod6prc4l2VGVWHz80KspNsxSfQ=
|
||||
|
||||
@@ -415,36 +415,6 @@ func GetTaskSavedForm(c *fiber.Ctx) error {
|
||||
return utils.Success(c, fiber.Map{"formData": task.FormData})
|
||||
}
|
||||
|
||||
// DecodeTaskQrRequest 二维码解析请求
|
||||
type DecodeTaskQrRequest struct {
|
||||
// Content 支持三种形式:base64 图片(dataURI)、http(s) 图片链接、纯 uuid/链接文本
|
||||
Content string `json:"content"`
|
||||
}
|
||||
|
||||
// DecodeTaskQr 解析签到二维码,返回其中的 uuid。
|
||||
// 前端上传二维码图片后调用本接口,把 uuid 存入表单(colName=uuid),
|
||||
// 避免把整张 base64 图片写进 formData(历史数据曾因此单条达 2.9MB)。
|
||||
func DecodeTaskQr(c *fiber.Ctx) error {
|
||||
var req DecodeTaskQrRequest
|
||||
if err := c.BodyParser(&req); err != nil {
|
||||
return utils.FailParam(c, "请求参数错误")
|
||||
}
|
||||
|
||||
content := strings.TrimSpace(req.Content)
|
||||
if content == "" {
|
||||
return utils.Fail(c, 400, "尚未上传二维码")
|
||||
}
|
||||
|
||||
uuid, err := cpd.ResolveQrUuidFromContent(content)
|
||||
if err != nil {
|
||||
return utils.Fail(c, 400, "二维码解析失败: "+err.Error())
|
||||
}
|
||||
if uuid == "" {
|
||||
return utils.Fail(c, 400, "二维码中未找到有效的二维码ID")
|
||||
}
|
||||
|
||||
return utils.Success(c, fiber.Map{"uuid": uuid})
|
||||
}
|
||||
|
||||
// DeleteTask 删除任务
|
||||
func DeleteTask(c *fiber.Ctx) error {
|
||||
|
||||
@@ -1,22 +1,8 @@
|
||||
package cpdaily
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
"image"
|
||||
_ "image/gif"
|
||||
_ "image/jpeg"
|
||||
_ "image/png"
|
||||
"regexp"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/go-resty/resty/v2"
|
||||
"github.com/makiuchi-d/gozxing"
|
||||
"github.com/makiuchi-d/gozxing/qrcode"
|
||||
"golang.org/x/image/draw"
|
||||
_ "golang.org/x/image/webp"
|
||||
)
|
||||
|
||||
// qrUuidPattern 匹配二维码内容中的 uuid 参数(如 ...?uuid=1583a6...&isNeedExtra=0)
|
||||
@@ -33,6 +19,8 @@ var bareTokenPattern = regexp.MustCompile(`^[0-9a-zA-Z_-]{8,64}$`)
|
||||
|
||||
// ExtractQrUuid 从二维码文本内容中提取签到二维码 uuid。
|
||||
// 支持三种形式:完整链接(取 uuid= 参数值)、32 位 hex 裸 uuid、其它形式的裸 uuid token。
|
||||
//
|
||||
// 二维码图片的识别在前端完成(jsQR),后端只接收解析后的 uuid 文本,故此处不涉及图片解码。
|
||||
func ExtractQrUuid(text string) string {
|
||||
text = strings.TrimSpace(text)
|
||||
if text == "" {
|
||||
@@ -49,103 +37,3 @@ func ExtractQrUuid(text string) string {
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// DecodeQRFromImage 解码图片数据中的二维码,返回其中文本内容。
|
||||
// 先直接解码,失败后放大图片重试(应对小尺寸/低分辨率截图)。
|
||||
func DecodeQRFromImage(data []byte) (string, error) {
|
||||
img, _, err := image.Decode(bytes.NewReader(data))
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("识别图片格式失败: %w", err)
|
||||
}
|
||||
|
||||
text, err := decodeQRImage(img)
|
||||
if err == nil {
|
||||
return text, nil
|
||||
}
|
||||
// 放大 3 倍重试
|
||||
if text, err2 := decodeQRImage(scaleImage(img, 3)); err2 == nil {
|
||||
return text, nil
|
||||
}
|
||||
return "", fmt.Errorf("未能识别图片中的二维码: %w", err)
|
||||
}
|
||||
|
||||
func decodeQRImage(img image.Image) (string, error) {
|
||||
bmp, err := gozxing.NewBinaryBitmapFromImage(img)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
hints := map[gozxing.DecodeHintType]interface{}{
|
||||
gozxing.DecodeHintType_TRY_HARDER: true,
|
||||
}
|
||||
result, err := qrcode.NewQRCodeReader().Decode(bmp, hints)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return result.GetText(), nil
|
||||
}
|
||||
|
||||
// scaleImage 将图片按整数倍放大(用于提高小图二维码的识别率)
|
||||
func scaleImage(img image.Image, factor int) image.Image {
|
||||
b := img.Bounds()
|
||||
dst := image.NewRGBA(image.Rect(0, 0, b.Dx()*factor, b.Dy()*factor))
|
||||
draw.CatmullRom.Scale(dst, dst.Bounds(), img, b, draw.Over, nil)
|
||||
return dst
|
||||
}
|
||||
|
||||
// ResolveQrUuidFromContent 从用户提供的二维码内容中解析出签到 uuid。
|
||||
// 支持三种形式:
|
||||
// 1. 文本直填(二维码链接或纯 uuid)
|
||||
// 2. base64 图片(dataURI,前端上传的本地预览图)
|
||||
// 3. http(s) 图片链接(下载后解码)
|
||||
func ResolveQrUuidFromContent(content string) (string, error) {
|
||||
content = strings.TrimSpace(content)
|
||||
if content == "" {
|
||||
return "", fmt.Errorf("尚未上传签到二维码")
|
||||
}
|
||||
|
||||
// 1) 内容本身已是文本(链接或 uuid),直接提取
|
||||
if uuid := ExtractQrUuid(content); uuid != "" {
|
||||
return uuid, nil
|
||||
}
|
||||
|
||||
// 2) base64 图片(dataURI)
|
||||
if IsDataURIImage(content) {
|
||||
idx := strings.Index(content, ",")
|
||||
if idx < 0 {
|
||||
return "", fmt.Errorf("二维码图片数据格式不正确")
|
||||
}
|
||||
imgBytes, err := base64.StdEncoding.DecodeString(content[idx+1:])
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("解码二维码图片失败: %w", err)
|
||||
}
|
||||
return decodeQrTextToUuid(imgBytes)
|
||||
}
|
||||
|
||||
// 3) http(s) 图片链接:下载后解码
|
||||
if strings.HasPrefix(content, "http://") || strings.HasPrefix(content, "https://") {
|
||||
client := resty.New().SetTimeout(30 * time.Second)
|
||||
resp, err := client.R().
|
||||
SetHeader("User-Agent", "Mozilla/5.0 (Linux; Android 15) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.6778.81 Mobile Safari/537.36").
|
||||
Get(content)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("下载二维码图片失败: %w", err)
|
||||
}
|
||||
if resp.StatusCode() > 299 {
|
||||
return "", fmt.Errorf("下载二维码图片失败(HTTP %d)", resp.StatusCode())
|
||||
}
|
||||
return decodeQrTextToUuid(resp.Body())
|
||||
}
|
||||
|
||||
return "", fmt.Errorf("无法识别的二维码内容,请上传二维码图片或粘贴二维码链接")
|
||||
}
|
||||
|
||||
func decodeQrTextToUuid(imgBytes []byte) (string, error) {
|
||||
text, err := DecodeQRFromImage(imgBytes)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if uuid := ExtractQrUuid(text); uuid != "" {
|
||||
return uuid, nil
|
||||
}
|
||||
return "", fmt.Errorf("二维码内容中未找到 uuid: %s", text)
|
||||
}
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
package cpdaily
|
||||
|
||||
import (
|
||||
"encoding/base64"
|
||||
"os"
|
||||
"testing"
|
||||
)
|
||||
|
||||
@@ -16,12 +14,12 @@ func TestExtractQrUuid(t *testing.T) {
|
||||
{"no uuid here", ""},
|
||||
{"", ""},
|
||||
// 裸 uuid 回读(formData 只存 uuid 后的解析路径)
|
||||
{"abc123XYZ", "abc123XYZ"}, // 非 32-hex 的裸 uuid
|
||||
{"abc123XYZ", "abc123XYZ"}, // 非 32-hex 的裸 uuid
|
||||
{"1583a66272854585833eb13b56fc1937", "1583a66272854585833eb13b56fc1937"}, // 32-hex
|
||||
{"uuid_with-dash", "uuid_with-dash"}, // 含 _ 和 -
|
||||
{"ab", ""}, // 太短,不认为是 uuid
|
||||
{"has space x", ""}, // 含空白,不认为是 uuid
|
||||
{"https://x.edu.cn/a/b", ""}, // URL 含符号,不走裸 token 分支
|
||||
{"uuid_with-dash", "uuid_with-dash"}, // 含 _ 和 -
|
||||
{"ab", ""}, // 太短,不认为是 uuid
|
||||
{"has space x", ""}, // 含空白,不认为是 uuid
|
||||
{"https://x.edu.cn/a/b", ""}, // URL 含符号,不走裸 token 分支
|
||||
}
|
||||
for _, c := range cases {
|
||||
if got := ExtractQrUuid(c.in); got != c.want {
|
||||
@@ -29,39 +27,3 @@ func TestExtractQrUuid(t *testing.T) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestResolveQrUuidFromLinkText(t *testing.T) {
|
||||
uuid, err := ResolveQrUuidFromContent("https://ynu.campusphere.net/wec-counselor-sign-apps/stu/qrsign/index.html?uuid=1583a66272854585833eb13b56fc1937&isNeedExtra=0")
|
||||
if err != nil || uuid != testQrUuid {
|
||||
t.Fatalf("link text resolve = (%q, %v), want (%q, nil)", uuid, err, testQrUuid)
|
||||
}
|
||||
if _, err := ResolveQrUuidFromContent(""); err == nil {
|
||||
t.Fatal("empty content should return error")
|
||||
}
|
||||
if _, err := ResolveQrUuidFromContent("some random text"); err == nil {
|
||||
t.Fatal("unrecognized content should return error")
|
||||
}
|
||||
}
|
||||
|
||||
func TestDecodeQrImageAndResolve(t *testing.T) {
|
||||
imgBytes, err := os.ReadFile("testdata/test_qr.png")
|
||||
if err != nil {
|
||||
t.Fatalf("read test qr image: %v", err)
|
||||
}
|
||||
|
||||
// 1) 直接解码图片中的二维码
|
||||
text, err := DecodeQRFromImage(imgBytes)
|
||||
if err != nil {
|
||||
t.Fatalf("decode qr image: %v", err)
|
||||
}
|
||||
if got := ExtractQrUuid(text); got != testQrUuid {
|
||||
t.Fatalf("decoded text %q -> uuid %q, want %q", text, got, testQrUuid)
|
||||
}
|
||||
|
||||
// 2) dataURI 形式(前端本地上传的图片)
|
||||
dataURI := "data:image/png;base64," + base64.StdEncoding.EncodeToString(imgBytes)
|
||||
uuid, err := ResolveQrUuidFromContent(dataURI)
|
||||
if err != nil || uuid != testQrUuid {
|
||||
t.Fatalf("dataURI resolve = (%q, %v), want (%q, nil)", uuid, err, testQrUuid)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -974,9 +974,10 @@ func fillExtraFields(rows map[string]interface{}, formItems []cpdmodel.CpTaskFor
|
||||
}
|
||||
|
||||
// fillScanSubmitForm 生成扫码签到(signMode=2)的提交表单:
|
||||
// 取二维码ID(uuid)字段,解析出 uuid 后用 qrUuid 替代 signInstanceWid(两者不并存),
|
||||
// 取二维码ID(uuid)字段,提取 uuid 后用 qrUuid 替代 signInstanceWid(两者不并存),
|
||||
// 不携带经纬度等定位字段。
|
||||
// 字段名兼容:新数据为 uuid,历史数据为 qrCode。
|
||||
// 二维码图片的识别在前端完成,这里只处理已解析出的 uuid 文本。
|
||||
func fillScanSubmitForm(rows map[string]interface{}, formItems []cpdmodel.CpTaskFormItem) (map[string]interface{}, error) {
|
||||
qrContent := ""
|
||||
for _, v := range formItems {
|
||||
@@ -985,9 +986,9 @@ func fillScanSubmitForm(rows map[string]interface{}, formItems []cpdmodel.CpTask
|
||||
break
|
||||
}
|
||||
}
|
||||
uuid, err := ResolveQrUuidFromContent(qrContent)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("二维码签到任务缺少有效签到二维码: %w", err)
|
||||
uuid := ExtractQrUuid(qrContent)
|
||||
if uuid == "" {
|
||||
return nil, fmt.Errorf("二维码签到任务缺少有效的二维码ID(uuid),请重新上传二维码解析")
|
||||
}
|
||||
|
||||
result := map[string]interface{}{
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 1.4 KiB |
@@ -254,11 +254,6 @@ export function getTaskSavedForm(id: number) {
|
||||
return api.get<{ data: { formData: string } }>(`/task/form-data/${id}`)
|
||||
}
|
||||
|
||||
/** 解析签到二维码(图片 dataURI / http 链接 / 纯 uuid),返回其中的 uuid */
|
||||
export function decodeQr(content: string) {
|
||||
return api.post<{ data: { uuid: string } }>('/task/qr/decode', { content })
|
||||
}
|
||||
|
||||
/** 保存任务表单 */
|
||||
export function saveTaskForm(id: number, formData: FormItem[]) {
|
||||
return api.post(`/task/form/${id}`, { formData })
|
||||
|
||||
@@ -472,24 +472,7 @@ async function handleImageUpload(index: number, isEdit: boolean) {
|
||||
uploadingLabel.value = '解析二维码中...'
|
||||
try {
|
||||
const text = await decodeQrInBrowser(picked)
|
||||
let uuid = text ? extractQrUuid(text) : ''
|
||||
// 前端识别失败时,降级走后端已有识别能力(复用 gozxing,多尺度重试)
|
||||
if (!uuid) {
|
||||
const dataUrl = await new Promise<string>((resolve) => {
|
||||
const r = new FileReader()
|
||||
r.onload = (e) => resolve((e.target?.result as string) || '')
|
||||
r.onerror = () => resolve('')
|
||||
r.readAsDataURL(picked)
|
||||
})
|
||||
if (dataUrl) {
|
||||
try {
|
||||
const res = await authApi.decodeQr(dataUrl)
|
||||
uuid = res.data.data.uuid || ''
|
||||
} catch {
|
||||
// 忽略,走统一失败提示
|
||||
}
|
||||
}
|
||||
}
|
||||
const uuid = text ? extractQrUuid(text) : ''
|
||||
if (!uuid) {
|
||||
message.error('未识别到二维码,请换一张更清晰的二维码图片')
|
||||
return
|
||||
|
||||
Reference in New Issue
Block a user