diff --git a/docs/code-review-issues.md b/docs/code-review-issues.md index a50217d..16563a7 100644 --- a/docs/code-review-issues.md +++ b/docs/code-review-issues.md @@ -100,3 +100,7 @@ 4. **V-17/V-18/V-20/V-13b**(秒传一致性、会话回收扩 failed+merging、前端 token、Swagger 开关质量) 5. **二节死代码 + 三节双轨 + 四节文档**(一批小清理) 6. **五节过分拆**(4 硬+5 轻+2 组可选+seam 统一——纯文件级减法,零行为变更) + +## 已修复(划线标记) + +本轮已确认并完成:~~V-11~~、~~V-15~~、~~V-16b~~、~~V-17~~、~~V-18~~、~~V-19~~、~~V-20(移除控制台泄露)~~、~~V-13b(补充 prod/live/staging 环境屏蔽)~~、~~Z-1~~、~~Y-1~~。 diff --git a/internal/biz/integration/integration_config.go b/internal/biz/integration/integration_config.go index bbd0449..44903a8 100644 --- a/internal/biz/integration/integration_config.go +++ b/internal/biz/integration/integration_config.go @@ -395,6 +395,19 @@ func integrationFirst(values map[string]any, keys ...string) string { return "" } +func IsIntegrationSecretKey(key string) bool { + normalized := strings.ToLower(strings.ReplaceAll(key, "-", "_")) + if strings.Contains(normalized, "secret") || strings.Contains(normalized, "password") || strings.Contains(normalized, "private") || strings.Contains(normalized, "credential") || strings.Contains(normalized, "token") { + return true + } + for _, item := range []string{"key", "key_pem", "api_key", "mch_key", "client_key", "certificate", "cert", "cert_pem", "p12", "pkcs12", "public_key", "platform_cert", "root_cert"} { + if normalized == item || strings.HasSuffix(normalized, "_"+item) { + return true + } + } + return false +} + func integrationInt64(values map[string]any, key string, fallback int64) int64 { value := integrationText(values, key) if value == "" { diff --git a/internal/biz/system/media_upload.go b/internal/biz/system/media_upload.go index d15c452..274f8c0 100644 --- a/internal/biz/system/media_upload.go +++ b/internal/biz/system/media_upload.go @@ -97,7 +97,13 @@ func (uc *MediaUsecase) InitUpload(ctx context.Context, userID uint, name, hash completed, err := uc.FindCompletedSession(ctx, userID, strings.ToLower(hash)) if err == nil && completed.MediaID != 0 { if media, findErr := uc.FindMedia(ctx, completed.MediaID); findErr == nil { - copy := &MediaFile{Name: name, URL: media.URL, Tag: media.Tag, Key: media.Key, Size: media.Size, MD5: media.MD5, Mime: media.Mime, UserID: userID} + copy := &MediaFile{Name: name, URL: media.URL, Tag: strings.TrimPrefix(filepath.Ext(name), "."), Key: media.Key, Size: media.Size, MD5: media.MD5, Mime: media.Mime, UserID: userID} + if strings.TrimSpace(copy.Mime) == "" { + copy.Mime = stdmime.TypeByExtension(strings.ToLower(filepath.Ext(name))) + if copy.Mime == "" { + copy.Mime = "application/octet-stream" + } + } if createErr := uc.CreateMedia(ctx, copy); createErr == nil { return nil, copy, nil, nil } diff --git a/internal/data/integration/integration_config.go b/internal/data/integration/integration_config.go index b03bd51..f20e4a5 100644 --- a/internal/data/integration/integration_config.go +++ b/internal/data/integration/integration_config.go @@ -224,7 +224,7 @@ func integrationObject(raw json.RawMessage) map[string]any { func maskIntegrationSecrets(kind, provider string, values map[string]any) { secretFields := integrationSecretFields(kind, provider) for key, value := range values { - if secretFields[key] || likelyIntegrationSecret(key) { + if secretFields[key] || integrationbiz.IsIntegrationSecretKey(key) { if text, ok := value.(string); ok && text != "" { values[key] = config.MaskedSecret } @@ -239,10 +239,12 @@ func maskIntegrationSecrets(kind, provider string, values map[string]any) { func mergeIntegrationSecrets(kind, provider string, values, old map[string]any) { secretFields := integrationSecretFields(kind, provider) for key, value := range values { - if secretFields[key] || likelyIntegrationSecret(key) { + if secretFields[key] || integrationbiz.IsIntegrationSecretKey(key) { if text, ok := value.(string); ok && text == config.MaskedSecret { if prior, exists := old[key]; exists { values[key] = prior + } else { + values[key] = "" } } continue @@ -252,6 +254,23 @@ func mergeIntegrationSecrets(kind, provider string, values, old map[string]any) mergeIntegrationSecrets(kind, provider, nested, prior) } } + if nested, ok := value.([]any); ok { + if prior, ok := old[key].([]any); ok { + mergeIntegrationSecretArrays(kind, provider, nested, prior) + } + } + } +} + +func mergeIntegrationSecretArrays(kind, provider string, values, old []any) { + for i, item := range values { + if nested, ok := item.(map[string]any); ok { + if i < len(old) { + if prior, ok := old[i].(map[string]any); ok { + mergeIntegrationSecrets(kind, provider, nested, prior) + } + } + } } } @@ -266,16 +285,3 @@ func integrationSecretFields(kind, provider string) map[string]bool { } return result } - -func likelyIntegrationSecret(key string) bool { - normalized := strings.ToLower(strings.ReplaceAll(key, "-", "_")) - if strings.Contains(normalized, "secret") || strings.Contains(normalized, "private") || strings.Contains(normalized, "password") || strings.Contains(normalized, "credential") { - return true - } - for _, item := range []string{"key", "token", "access_token", "api_key", "mch_key", "client_key", "certificate", "cert", "p12", "pkcs12", "public_key", "platform_cert", "root_cert"} { - if normalized == item || strings.HasSuffix(normalized, "_"+item) { - return true - } - } - return false -} diff --git a/internal/data/system/media_upload.go b/internal/data/system/media_upload.go index 41198d1..bad3294 100644 --- a/internal/data/system/media_upload.go +++ b/internal/data/system/media_upload.go @@ -120,7 +120,7 @@ func (r *mediaRepo) DeleteChunks(ctx context.Context, uploadID uint) error { } func (r *mediaRepo) StaleUploadSessionIDs(ctx context.Context, before time.Time) ([]uint, error) { var ids []uint - err := r.data.DB().WithContext(ctx).Model(&uploadSessionPO{}).Where("status = ? AND updated_at < ?", "uploading", before).Pluck("id", &ids).Error + err := r.data.DB().WithContext(ctx).Model(&uploadSessionPO{}).Where("status IN ? AND updated_at < ?", []string{"uploading", "failed", "merging"}, before).Pluck("id", &ids).Error return ids, err } func (r *mediaRepo) DeleteUploadData(ctx context.Context, uploadID uint) error { diff --git a/internal/integration/payment/alipay.go b/internal/integration/payment/alipay.go index 2dae01e..62e7802 100644 --- a/internal/integration/payment/alipay.go +++ b/internal/integration/payment/alipay.go @@ -438,7 +438,7 @@ func alipayCreateMethod(extra, config map[string]any) (string, error) { } normalized := paymentkit.NormalizePaymentMethod(value) switch normalized { - case "", "create", "trade_create", "alipay_trade_create", "jsapi", "js_api", "miniapp", "mini_app", "mini_program": + case "", "create", "trade_create", "alipay_trade_create", "jsapi", "js_api", "mini", "miniapp", "mini_app", "miniprogram", "mini_program": return "alipay.trade.create", nil case "pay", "trade_pay", "alipay_trade_pay", "barcode", "barcode_pay", "micropay", "face_to_face": return "alipay.trade.pay", nil diff --git a/internal/integration/payment/douyin.go b/internal/integration/payment/douyin.go index 50b84cd..4b19ddc 100644 --- a/internal/integration/payment/douyin.go +++ b/internal/integration/payment/douyin.go @@ -113,7 +113,7 @@ func douyinCreateMethod(extra, config map[string]any) (string, error) { } normalized := paymentkit.NormalizePaymentMethod(value) switch normalized { - case "", "jsapi", "js_api", "mini", "mini_program", "miniprogram", "applet": + case "", "jsapi", "js_api", "mini", "miniapp", "mini_app", "mini_program", "miniprogram", "applet": return "jsapi", nil case "app", "app_pay", "apporder": return "app", nil diff --git a/internal/integration/payment/lakala.go b/internal/integration/payment/lakala.go index e5dda5d..e9a5c69 100644 --- a/internal/integration/payment/lakala.go +++ b/internal/integration/payment/lakala.go @@ -38,7 +38,7 @@ func (a *lakalaAdapter) Create(ctx context.Context, req *bizpayment.PaymentReque rsp, err = client.CreateJSAPIOrder(ctx, req.TradeNo, bm) case "h5", "h5_pay", "mobile_h5", "mobile_h5_pay": rsp, err = client.CreateH5PayOrder(ctx, req.TradeNo, bm) - case "mini", "miniapp", "mini_program", "miniprogram", "microapp": + case "mini", "miniapp", "mini_app", "mini_program", "miniprogram", "microapp": rsp, err = client.CreateMiniProgramOrder(ctx, req.TradeNo, bm) case "native", "native_qrcode", "native_qr": rsp, err = client.CreateNativeQRCodeOrder(ctx, req.TradeNo, bm) diff --git a/internal/server/gin.go b/internal/server/gin.go index 036e7d0..6ab9b0b 100644 --- a/internal/server/gin.go +++ b/internal/server/gin.go @@ -61,7 +61,11 @@ func NewGinEngineWithRuntime(runtime *config.Store, access *systemservice.Access if ws != nil && ws.Enabled() { engine.GET(ws.Path(), handleWebSocket) } - if snapshot == nil || snapshot.Admin == nil || snapshot.Admin.App == nil || strings.ToLower(strings.TrimSpace(snapshot.Admin.App.Env)) != "production" { + env := "" + if snapshot != nil && snapshot.Admin != nil && snapshot.Admin.App != nil { + env = strings.ToLower(strings.TrimSpace(snapshot.Admin.App.Env)) + } + if env != "production" && env != "prod" && env != "live" && env != "staging" { registerSwagger(engine, prefix, version, logger) } staticfiles.Register(engine, runtime) diff --git a/internal/server/httpx/response.go b/internal/server/httpx/response.go index 3b501b7..52696d4 100644 --- a/internal/server/httpx/response.go +++ b/internal/server/httpx/response.go @@ -30,6 +30,9 @@ type PageResult struct { } func Write(c *gin.Context, code int, data any, message string) { + if code == CodeError { + message = sanitizeFailureMessage(message) + } c.JSON(http.StatusOK, Response{Code: code, Data: data, Msg: message}) } @@ -39,6 +42,20 @@ func OKWithData(c *gin.Context, data any) { Write(c, CodeSuccess, data, "成功" func Fail(c *gin.Context, message string) { Write(c, CodeError, gin.H{}, message) } +func sanitizeFailureMessage(message string) string { + message = strings.TrimSpace(message) + lower := strings.ToLower(message) + for _, marker := range []string{"gorm", "sql:", "redis", "mysql", "mongo", "dial ", "connection", "provider", "sdk", "json:", "serialize", "timeout", "http 4", "http 5", "tls:"} { + if strings.Contains(lower, marker) { + return "操作失败" + } + } + if strings.Contains(message, "失败:") { + return strings.TrimSpace(strings.SplitN(message, "失败:", 2)[0]) + "失败" + } + return message +} + func NoAuth(c *gin.Context, message string) { c.AbortWithStatusJSON(http.StatusUnauthorized, Response{Code: CodeError, Data: nil, Msg: message}) } diff --git a/internal/server/middleware/access_log.go b/internal/server/middleware/access_log.go index 9dee004..7066f13 100644 --- a/internal/server/middleware/access_log.go +++ b/internal/server/middleware/access_log.go @@ -40,10 +40,11 @@ func AccessLog(runtime *config.Store, logger *slog.Logger, version string) gin.H } bodyLimit := defaultRequestBodyLimit if mediaUpload { - bodyLimit = system.DefaultMaxMediaFileSize + (1 << 20) - if admin != nil && admin.Media != nil && admin.Media.MaxFileSize > 0 { - bodyLimit = admin.Media.MaxFileSize + (1 << 20) + mediaSettings := system.MediaSettings{} + if admin != nil && admin.Media != nil { + mediaSettings.MaxFileSize = admin.Media.MaxFileSize } + bodyLimit = mediaSettings.EffectiveMaxFileSize() + (1 << 20) } bytesIn := c.Request.ContentLength logLimit := 1024 @@ -96,6 +97,7 @@ func AccessLog(runtime *config.Store, logger *slog.Logger, version string) gin.H requestText = redactJSON(requestBody, c.GetHeader("Content-Type"), logLimit) } c.Set(ctxReqBodyKey, requestText) + c.Set(ctxRespTruncatedKey, writer.Truncated()) if paymentCallback { // Callback acknowledgements and provider payloads must not flow into // the generic response/error audit pipeline. @@ -110,6 +112,9 @@ func AccessLog(runtime *config.Store, logger *slog.Logger, version string) gin.H return } responseText := redactJSON(writer.body.Bytes(), c.Writer.Header().Get("Content-Type"), logLimit) + if writer.Truncated() { + responseText = "[超出记录长度]" + } if paymentCallback { responseText = "[支付回调响应已省略]" } diff --git a/internal/server/middleware/audit.go b/internal/server/middleware/audit.go index a0f2056..8fd383a 100644 --- a/internal/server/middleware/audit.go +++ b/internal/server/middleware/audit.go @@ -43,8 +43,7 @@ func OperationAudit(runtime *config.Store, recorder *systemservice.AuditRecorder } if c.Request.Method == http.MethodGet { requestBody = operationQueryBody(c.Request.URL.RawQuery) - } else if value, ok := c.Get(ctxReqBodyKey); ok { - requestBody = []byte(stringValue(value)) + } else if _, ok := c.Get(ctxReqBodyKey); ok { } else if c.Request.Body != nil { // Fallback for tests or custom middleware chains that omit AccessLog. if strings.Contains(c.GetHeader("Content-Type"), "multipart/form-data") { @@ -79,13 +78,13 @@ func OperationAudit(runtime *config.Store, recorder *systemservice.AuditRecorder responseBody = redactJSON(body.Bytes(), c.Writer.Header().Get("Content-Type"), maxBytes) } } - if isDownloadResponse(c) && len(responseBody) > maxBytes { + if truncated, _ := c.Get(ctxRespTruncatedKey); truncated == true { responseBody = "[超出记录长度]" } errorMessage := c.Errors.ByType(gin.ErrorTypePrivate).String() operationBody := "" capturedBody := false - if c.Request.Method != http.MethodGet { + if c.Request.Method != "GET" { if value, ok := c.Get(ctxReqBodyKey); ok { // AccessLog has already captured, redacted, summarized, and bounded the // request body. Treat that value as final so audit does not process it a @@ -162,27 +161,3 @@ func maskOperationBody(value any) { } } } - -// operationDownloadHeaders lists the response headers whose presence marks a -// download: the value is the substring that identifies it. -var operationDownloadHeaders = [...]struct{ header, marker string }{ - {"Pragma", "public"}, - {"Expires", "0"}, - {"Cache-Control", "must-revalidate, post-check=0, pre-check=0"}, - {"Content-Type", "application/force-download"}, - {"Content-Type", "application/octet-stream"}, - {"Content-Type", "application/vnd.ms-excel"}, - {"Content-Type", "application/download"}, - {"Content-Disposition", "attachment"}, - {"Content-Transfer-Encoding", "binary"}, -} - -func isDownloadResponse(c *gin.Context) bool { - header := c.Writer.Header() - for _, candidate := range operationDownloadHeaders { - if strings.Contains(header.Get(candidate.header), candidate.marker) { - return true - } - } - return false -} diff --git a/internal/server/middleware/capture.go b/internal/server/middleware/capture.go index 5ce97b2..0876015 100644 --- a/internal/server/middleware/capture.go +++ b/internal/server/middleware/capture.go @@ -12,14 +12,16 @@ import ( // OperationAudit consumes these values after the handler returns, avoiding a // second body read/writer wrapper. const ( - ctxReqBodyKey = "kra_req_body" - ctxRespBufferKey = "kra_resp_buffer" + ctxReqBodyKey = "kra_req_body" + ctxRespBufferKey = "kra_resp_buffer" + ctxRespTruncatedKey = "kra_resp_truncated" ) type captureWriter struct { gin.ResponseWriter - body bytes.Buffer - maxBytes int + body bytes.Buffer + maxBytes int + truncated bool } func (w *captureWriter) Write(data []byte) (int, error) { @@ -31,6 +33,7 @@ func (w *captureWriter) Write(data []byte) (int, error) { remaining := limit - w.body.Len() if len(data) > remaining { _, _ = w.body.Write(data[:remaining]) + w.truncated = true } else { _, _ = w.body.Write(data) } @@ -38,6 +41,8 @@ func (w *captureWriter) Write(data []byte) (int, error) { return w.ResponseWriter.Write(data) } +func (w *captureWriter) Truncated() bool { return w != nil && w.truncated } + func redactJSON(raw []byte, contentType string, limit int) string { if len(raw) == 0 { return "" diff --git a/internal/service/integration/integration_config.go b/internal/service/integration/integration_config.go index f70f6e5..fcee561 100644 --- a/internal/service/integration/integration_config.go +++ b/internal/service/integration/integration_config.go @@ -4,7 +4,6 @@ import ( "context" "encoding/json" integrationbiz "kra/internal/biz/integration" - "strings" "kra/internal/service/dto" ) @@ -76,8 +75,7 @@ func redactIntegrationJSON(raw json.RawMessage) json.RawMessage { switch x := v.(type) { case map[string]any: for k, item := range x { - n := strings.ToLower(strings.ReplaceAll(k, "-", "_")) - if strings.Contains(n, "secret") || strings.Contains(n, "password") || strings.Contains(n, "private") || strings.Contains(n, "credential") || strings.Contains(n, "token") || strings.HasSuffix(n, "_key") || strings.Contains(n, "cert") || strings.Contains(n, "p12") { + if integrationbiz.IsIntegrationSecretKey(k) { x[k] = "******" } else { walk(item) diff --git a/web/src/components/upload/QR-code.vue b/web/src/components/upload/QR-code.vue index d969266..a937021 100644 --- a/web/src/components/upload/QR-code.vue +++ b/web/src/components/upload/QR-code.vue @@ -54,7 +54,6 @@ const createQrCode = () => { const local = window.location codeUrl.value = local.protocol + '//' + local.host + '/#/scanUpload?id=' + props.classId + '&token=' + userStore.token + '&t=' + Date.now() dialogVisible.value = true - console.log(codeUrl.value) } const onFinished = () => {