diff --git a/backed/cmd/server/main.go b/backed/cmd/server/main.go index cd622d5..0655c62 100644 --- a/backed/cmd/server/main.go +++ b/backed/cmd/server/main.go @@ -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) diff --git a/backed/go.mod b/backed/go.mod index 6ed0e4b..8f0794d 100644 --- a/backed/go.mod +++ b/backed/go.mod @@ -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 ) diff --git a/backed/go.sum b/backed/go.sum index 603acbf..5ac93ea 100644 --- a/backed/go.sum +++ b/backed/go.sum @@ -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= diff --git a/backed/internal/handler/task.go b/backed/internal/handler/task.go index 8a73273..3a2be58 100644 --- a/backed/internal/handler/task.go +++ b/backed/internal/handler/task.go @@ -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 { diff --git a/backed/internal/service/cpdaily/qrcode.go b/backed/internal/service/cpdaily/qrcode.go index 28b7b9e..8e02760 100644 --- a/backed/internal/service/cpdaily/qrcode.go +++ b/backed/internal/service/cpdaily/qrcode.go @@ -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) -} diff --git a/backed/internal/service/cpdaily/qrcode_test.go b/backed/internal/service/cpdaily/qrcode_test.go index df88a2b..bfbf9ea 100644 --- a/backed/internal/service/cpdaily/qrcode_test.go +++ b/backed/internal/service/cpdaily/qrcode_test.go @@ -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) - } -} diff --git a/backed/internal/service/cpdaily/task.go b/backed/internal/service/cpdaily/task.go index 5ff95e9..945a937 100644 --- a/backed/internal/service/cpdaily/task.go +++ b/backed/internal/service/cpdaily/task.go @@ -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{}{ diff --git a/backed/internal/service/cpdaily/testdata/test_qr.png b/backed/internal/service/cpdaily/testdata/test_qr.png deleted file mode 100644 index d720598..0000000 Binary files a/backed/internal/service/cpdaily/testdata/test_qr.png and /dev/null differ diff --git a/frondend/src/api/auth.ts b/frondend/src/api/auth.ts index 5457a40..e630c12 100644 --- a/frondend/src/api/auth.ts +++ b/frondend/src/api/auth.ts @@ -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 }) diff --git a/frondend/src/views/home/HomeView.vue b/frondend/src/views/home/HomeView.vue index 0b4dedc..0847553 100644 --- a/frondend/src/views/home/HomeView.vue +++ b/frondend/src/views/home/HomeView.vue @@ -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((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