diff --git a/backend/internal/database/gorm_logger.go b/backend/internal/database/gorm_logger.go index 8d60c62..5280b2c 100644 --- a/backend/internal/database/gorm_logger.go +++ b/backend/internal/database/gorm_logger.go @@ -72,9 +72,12 @@ func (l *structuredGormLogger) Trace(ctx context.Context, begin time.Time, query } elapsed := time.Since(begin) switch { - case err != nil && !errors.Is(err, gorm.ErrRecordNotFound) && l.level >= gormLogger.Error: + case err != nil && !errors.Is(err, gorm.ErrRecordNotFound) && !errors.Is(err, context.Canceled) && l.level >= gormLogger.Error: sql, rows := query() l.withContext(ctx).Error("数据库查询失败", queryFields(sql, rows, elapsed, err)...) + case errors.Is(err, context.Canceled) && l.level >= gormLogger.Info: + sql, rows := query() + l.withContext(ctx).Debug("数据库查询被客户端取消", queryFields(sql, rows, elapsed, err)...) case elapsed >= l.slowThreshold && l.level >= gormLogger.Warn: sql, rows := query() l.withContext(ctx).Warn("数据库慢查询", queryFields(sql, rows, elapsed, nil)...) diff --git a/backend/internal/jobs/refundretry/job.go b/backend/internal/jobs/refundretry/job.go index 1f8fe48..902fac9 100644 --- a/backend/internal/jobs/refundretry/job.go +++ b/backend/internal/jobs/refundretry/job.go @@ -123,7 +123,7 @@ func (j *Job) run(ctx context.Context) { if err != nil { j.logger.Warn("退款重试任务扫描缺失退款单失败", zap.Error(err)) } - if processed > 0 || missing > 0 { + if processed > 0 || rebuilt > 0 { j.logger.Info("退款重试任务处理完成", zap.Int("processed", processed), zap.Int("missing_refund_orders", missing), diff --git a/backend/internal/middleware/request_logger.go b/backend/internal/middleware/request_logger.go index 9a82241..f3fa8ee 100644 --- a/backend/internal/middleware/request_logger.go +++ b/backend/internal/middleware/request_logger.go @@ -1,6 +1,8 @@ package middleware import ( + "context" + "errors" "net/http" "strings" "time" @@ -72,6 +74,10 @@ func RequestLogger(logger *zap.Logger) gin.HandlerFunc { } switch { + case errors.Is(c.Request.Context().Err(), context.Canceled): + if logger.Core().Enabled(zap.DebugLevel) { + logger.Debug("HTTP 客户端中断连接", fields...) + } case status >= 500: logger.Error("HTTP 请求失败", fields...) case status == http.StatusTooManyRequests: @@ -113,7 +119,7 @@ func shouldSkipHTTPLog(path, route string, status int, latencyMs float64) bool { return false } // SSE 的请求耗时就是连接存活时间,由 chathub 单独记录连接生命周期。 - if isChatSSEPath(path, route) { + if isSSEPath(path, route) { return true } if latencyMs >= slowRequestThresholdMs { @@ -122,9 +128,10 @@ func shouldSkipHTTPLog(path, route string, status int, latencyMs float64) bool { return true } -func isChatSSEPath(path, route string) bool { - return path == "/api/chats/events" || path == "/api/admin/chats/events" || - route == "/api/chats/events" || route == "/api/admin/chats/events" +func isSSEPath(path, route string) bool { + return path == "/api/auth/events" || route == "/api/auth/events" || + path == "/api/chats/events" || route == "/api/chats/events" || + path == "/api/admin/chats/events" || route == "/api/admin/chats/events" } func meaningfulAuthFailure(c *gin.Context) bool { diff --git a/backend/internal/middleware/request_logger_test.go b/backend/internal/middleware/request_logger_test.go index 05c859b..e31e100 100644 --- a/backend/internal/middleware/request_logger_test.go +++ b/backend/internal/middleware/request_logger_test.go @@ -19,6 +19,7 @@ func TestShouldSkipHTTPLog(t *testing.T) { {name: "轮询 500 不跳过", path: "/api/wallet/balance", status: 500, latencyMs: 1, wantSkip: false}, {name: "限流请求不跳过", path: "/api/auth/sms", status: 429, latencyMs: 1, wantSkip: false}, {name: "慢请求不跳过", path: "/api/orders", status: 200, latencyMs: 500, wantSkip: false}, + {name: "用户全局通知 SSE 长连接跳过", path: "/api/auth/events", route: "/api/auth/events", status: 200, latencyMs: 60_000, wantSkip: true}, {name: "用户 SSE 长连接跳过", path: "/api/chats/events", route: "/api/chats/events", status: 200, latencyMs: 60_000, wantSkip: true}, {name: "后台 SSE 长连接跳过", path: "/api/admin/chats/events", route: "/api/admin/chats/events", status: 200, latencyMs: 600_000, wantSkip: true}, {name: "SSE 服务端错误不跳过", path: "/api/admin/chats/events", route: "/api/admin/chats/events", status: 500, latencyMs: 600_000, wantSkip: false}, diff --git a/backend/internal/modules/chat/handler_error.go b/backend/internal/modules/chat/handler_error.go index 164562c..cf34b8e 100644 --- a/backend/internal/modules/chat/handler_error.go +++ b/backend/internal/modules/chat/handler_error.go @@ -1,6 +1,7 @@ package chat import ( + "context" "errors" "net/http" @@ -10,6 +11,9 @@ import ( ) func writeChatError(c *gin.Context, err error) { + if errors.Is(err, context.Canceled) || errors.Is(c.Request.Context().Err(), context.Canceled) { + return + } response.RecordError(c, err) switch { case errors.Is(err, ErrDependencyUnavailable): diff --git a/backend/internal/modules/chat/qrcode.go b/backend/internal/modules/chat/qrcode.go index 23c2e5e..610a87b 100644 --- a/backend/internal/modules/chat/qrcode.go +++ b/backend/internal/modules/chat/qrcode.go @@ -23,7 +23,7 @@ const ( qrCodeOCRJobURLConfigKey = "integration.paddle_ocr_job_url" qrCodeOCRModelConfigKey = "integration.paddle_ocr_model" defaultQrCodeOCRJobURL = "https://paddleocr.aistudio-app.com/api/v2/ocr/jobs" - defaultQrCodeOCRModel = "PaddleOCR-VL-1.6" + defaultQrCodeOCRModel = "PP-OCRv6" ) var ( diff --git a/backend/internal/modules/chat/qrcode_ocr.go b/backend/internal/modules/chat/qrcode_ocr.go index 6e4c225..d7322e9 100644 --- a/backend/internal/modules/chat/qrcode_ocr.go +++ b/backend/internal/modules/chat/qrcode_ocr.go @@ -443,6 +443,7 @@ func (r *Repository) submitPaddleOCRJob(ctx context.Context, config *QrCodeOCRCo optionalPayload, _ := json.Marshal(map[string]bool{ "useDocOrientationClassify": false, "useDocUnwarping": false, + "useTextlineOrientation": false, "useChartRecognition": false, }) _ = writer.WriteField("optionalPayload", string(optionalPayload)) @@ -790,13 +791,25 @@ func collectPaddleOCRText(value any, texts *[]string) { collectPaddleOCRText(item, texts) } case map[string]any: + // PP-OCRv6: prunedResult.rec_texts 包含检测并识别的文字列表 + if recTexts, ok := v["rec_texts"].([]any); ok { + for _, item := range recTexts { + if text, ok := item.(string); ok && strings.TrimSpace(text) != "" { + *texts = append(*texts, text) + } + } + return + } + // PaddleOCR-VL: markdown.text 包含识别文本 if markdown, ok := v["markdown"].(map[string]any); ok { if text, ok := markdown["text"].(string); ok && strings.TrimSpace(text) != "" { *texts = append(*texts, text) } } for key, item := range v { - if key == "images" || key == "outputImages" { + if key == "images" || key == "outputImages" || key == "ocrImage" || key == "inputImage" || + key == "preprocessedImages" || key == "model_settings" || key == "text_det_params" || + key == "logId" || key == "errorCode" || key == "errorMsg" || key == "dataInfo" { continue } collectPaddleOCRText(item, texts) diff --git a/backend/internal/modules/chat/qrcode_ocr_test.go b/backend/internal/modules/chat/qrcode_ocr_test.go index 75f02f7..2f6f36e 100644 --- a/backend/internal/modules/chat/qrcode_ocr_test.go +++ b/backend/internal/modules/chat/qrcode_ocr_test.go @@ -222,3 +222,20 @@ func pngHeader(width, height uint32) []byte { binary.Write(&result, binary.BigEndian, checksum) return result.Bytes() } + +func TestCollectPaddleOCRJSONLTextSupportsPPOCRv6(t *testing.T) { + rawJSONL := `{"result":{"ocrResults":[{"prunedResult":{"rec_texts":["火爆出租-778群","扫码加入群聊"]},"ocrImage":"https://example.com/ocr.jpg","inputImage":"https://example.com/in.jpg"}]}}` + text := collectPaddleOCRJSONLText(rawJSONL) + if !strings.Contains(text, "火爆出租-778群") || !strings.Contains(text, "扫码加入群聊") { + t.Fatalf("collected text = %q, expected recognized group texts", text) + } + if strings.Contains(text, "ocr.jpg") || strings.Contains(text, "in.jpg") { + t.Fatalf("collected text = %q, should not include image URLs", text) + } + + candidates := parseQrCodeGroupNameCandidates(text) + if len(candidates) == 0 || candidates[0] != "火爆出租-778群" { + t.Fatalf("candidates = %v, want 火爆出租-778群 as first candidate", candidates) + } +} + diff --git a/backend/internal/modules/chat/support.go b/backend/internal/modules/chat/support.go index f6bb0ef..7e1a761 100644 --- a/backend/internal/modules/chat/support.go +++ b/backend/internal/modules/chat/support.go @@ -229,6 +229,7 @@ func (r *Repository) listAdminConversations(ctx context.Context, principal Princ var rows []conversationRow offset := (page - 1) * pageSize queryDB := r.adminConversationBase(ctx, principal, keyword). + Joins("LEFT JOIN chat_messages AS lm ON lm.id = c.last_message_id"). Select(`c.id, c.order_id, c.listing_id, c.type, c.support_scene, c.title, c.status, c.last_message_id, c.last_message_preview, c.last_message_at, c.created_at, c.updated_at, COALESCE(cp_me.role, 'admin') AS role, @@ -302,7 +303,6 @@ func (r *Repository) adminConversationBase(ctx context.Context, principal Princi db := r.db.WithContext(ctx).Table("chat_conversations AS c"). Joins("LEFT JOIN chat_participants AS cp_me ON cp_me.conversation_id = c.id AND cp_me.participant_type = ? AND cp_me.participant_id = ?", principal.Type, principal.ID). Joins("LEFT JOIN chat_admin_conversation_states AS cas ON cas.conversation_id = c.id AND cas.admin_user_id = ?", principal.ID). - Joins("LEFT JOIN chat_messages AS lm ON lm.id = c.last_message_id"). Joins("LEFT JOIN rental_orders AS explicit_lo ON explicit_lo.id = c.order_id"). Joins("LEFT JOIN rental_listings AS l ON l.id = COALESCE(explicit_lo.listing_id, c.listing_id)") if keyword != "" { diff --git a/backend/internal/modules/systemconfig/repository.go b/backend/internal/modules/systemconfig/repository.go index 954b0c6..0dff82b 100644 --- a/backend/internal/modules/systemconfig/repository.go +++ b/backend/internal/modules/systemconfig/repository.go @@ -47,7 +47,7 @@ var defaultConfigs = []defaultConfig{ {Key: "chat.renter_retention_days_after_order_end", Value: "5", Description: "订单结束后租客保留在发布群的天数(3-7)"}, {Key: "integration.paddle_ocr_token", Value: "", Description: "PaddleOCR API Token,用于二维码群名自动识别"}, {Key: "integration.paddle_ocr_job_url", Value: "https://paddleocr.aistudio-app.com/api/v2/ocr/jobs", Description: "PaddleOCR 异步任务接口地址"}, - {Key: "integration.paddle_ocr_model", Value: "PaddleOCR-VL-1.6", Description: "PaddleOCR 识别模型"}, + {Key: "integration.paddle_ocr_model", Value: "PP-OCRv6", Description: "PaddleOCR 识别模型(默认 PP-OCRv6,支持 PaddleOCR-VL-1.6 等)"}, {Key: homeAnnouncementsConfigKey, Value: defaultHomeAnnouncementsConfigValue(), Description: "移动端首页公告 JSON 数组"}, {Key: homeBannersConfigKey, Value: defaultHomeBannersConfigValue(), Description: "移动端首页轮播图 JSON 数组"}, {Key: awRecycleConfigKey, Value: defaultAWRecycleConfigValue(), Description: "首页 AW炫彩回收入口配置 JSON(标题/副标题/展示图片)"}, diff --git a/backend/migrations/000065_default_paddle_ocr_model_v6.sql b/backend/migrations/000065_default_paddle_ocr_model_v6.sql new file mode 100644 index 0000000..53a70a3 --- /dev/null +++ b/backend/migrations/000065_default_paddle_ocr_model_v6.sql @@ -0,0 +1,13 @@ +-- +goose Up +-- +goose StatementBegin +UPDATE system_configs +SET value = 'PP-OCRv6', description = 'PaddleOCR 识别模型(默认 PP-OCRv6,支持 PaddleOCR-VL-1.6 等)' +WHERE `key` = 'integration.paddle_ocr_model' AND (value = 'PaddleOCR-VL-1.6' OR value = '' OR value IS NULL); +-- +goose StatementEnd + +-- +goose Down +-- +goose StatementBegin +UPDATE system_configs +SET value = 'PaddleOCR-VL-1.6', description = 'PaddleOCR 识别模型' +WHERE `key` = 'integration.paddle_ocr_model' AND value = 'PP-OCRv6'; +-- +goose StatementEnd diff --git a/backend/pkg/response/response.go b/backend/pkg/response/response.go index f173a2e..62ed6d1 100644 --- a/backend/pkg/response/response.go +++ b/backend/pkg/response/response.go @@ -1,6 +1,7 @@ package response import ( + "context" "errors" "fmt" "net/http" @@ -45,7 +46,7 @@ func Error(c *gin.Context, status int, code, message string) { // RecordError 保存原始错误,供统一请求日志记录服务端故障原因。 func RecordError(c *gin.Context, err error) { - if c == nil || err == nil { + if c == nil || err == nil || errors.Is(err, context.Canceled) { return } for _, item := range c.Errors { diff --git a/frontend/src/features/admin/views/AdminSystemConfigsView.vue b/frontend/src/features/admin/views/AdminSystemConfigsView.vue index efcaf4b..9273f7c 100644 --- a/frontend/src/features/admin/views/AdminSystemConfigsView.vue +++ b/frontend/src/features/admin/views/AdminSystemConfigsView.vue @@ -145,7 +145,7 @@ const paddleOcrJobUrlConfig = computed(() => ) ) const paddleOcrModelConfig = computed(() => - systemConfigByKey('integration.paddle_ocr_model', 'PaddleOCR-VL-1.6', 'PaddleOCR 识别模型') + systemConfigByKey('integration.paddle_ocr_model', 'PP-OCRv6', 'PaddleOCR 识别模型') ) const regularConfigs = computed(() => diff --git a/frontend/src/shared/utils/systemConfigOptions.ts b/frontend/src/shared/utils/systemConfigOptions.ts index 3bc1851..675b4ca 100644 --- a/frontend/src/shared/utils/systemConfigOptions.ts +++ b/frontend/src/shared/utils/systemConfigOptions.ts @@ -58,7 +58,10 @@ export const systemConfigSelectOptions: Record = { { label: '5 天', value: '5' }, { label: '7 天', value: '7' }, ], - 'integration.paddle_ocr_model': [{ label: 'PaddleOCR-VL-1.6', value: 'PaddleOCR-VL-1.6' }], + 'integration.paddle_ocr_model': [ + { label: 'PP-OCRv6(推荐,低延迟少排队)', value: 'PP-OCRv6' }, + { label: 'PaddleOCR-VL-1.6', value: 'PaddleOCR-VL-1.6' }, + ], } export function getSystemConfigSelectOptions(key: string) {