优化结构

This commit is contained in:
Yvan 2026-08-28 15:07:16 +08:00
parent ae5345de14
commit a4ed3a02df
15 changed files with 92 additions and 60 deletions

View File

@ -100,3 +100,7 @@
4. **V-17/V-18/V-20/V-13b**(秒传一致性、会话回收扩 failed+merging、前端 token、Swagger 开关质量) 4. **V-17/V-18/V-20/V-13b**(秒传一致性、会话回收扩 failed+merging、前端 token、Swagger 开关质量)
5. **二节死代码 + 三节双轨 + 四节文档**(一批小清理) 5. **二节死代码 + 三节双轨 + 四节文档**(一批小清理)
6. **五节过分拆**4 硬+5 轻+2 组可选+seam 统一——纯文件级减法,零行为变更) 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~~。

View File

@ -395,6 +395,19 @@ func integrationFirst(values map[string]any, keys ...string) string {
return "" 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 { func integrationInt64(values map[string]any, key string, fallback int64) int64 {
value := integrationText(values, key) value := integrationText(values, key)
if value == "" { if value == "" {

View File

@ -97,7 +97,13 @@ func (uc *MediaUsecase) InitUpload(ctx context.Context, userID uint, name, hash
completed, err := uc.FindCompletedSession(ctx, userID, strings.ToLower(hash)) completed, err := uc.FindCompletedSession(ctx, userID, strings.ToLower(hash))
if err == nil && completed.MediaID != 0 { if err == nil && completed.MediaID != 0 {
if media, findErr := uc.FindMedia(ctx, completed.MediaID); findErr == nil { 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 { if createErr := uc.CreateMedia(ctx, copy); createErr == nil {
return nil, copy, nil, nil return nil, copy, nil, nil
} }

View File

@ -224,7 +224,7 @@ func integrationObject(raw json.RawMessage) map[string]any {
func maskIntegrationSecrets(kind, provider string, values map[string]any) { func maskIntegrationSecrets(kind, provider string, values map[string]any) {
secretFields := integrationSecretFields(kind, provider) secretFields := integrationSecretFields(kind, provider)
for key, value := range values { for key, value := range values {
if secretFields[key] || likelyIntegrationSecret(key) { if secretFields[key] || integrationbiz.IsIntegrationSecretKey(key) {
if text, ok := value.(string); ok && text != "" { if text, ok := value.(string); ok && text != "" {
values[key] = config.MaskedSecret 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) { func mergeIntegrationSecrets(kind, provider string, values, old map[string]any) {
secretFields := integrationSecretFields(kind, provider) secretFields := integrationSecretFields(kind, provider)
for key, value := range values { 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 text, ok := value.(string); ok && text == config.MaskedSecret {
if prior, exists := old[key]; exists { if prior, exists := old[key]; exists {
values[key] = prior values[key] = prior
} else {
values[key] = ""
} }
} }
continue continue
@ -252,6 +254,23 @@ func mergeIntegrationSecrets(kind, provider string, values, old map[string]any)
mergeIntegrationSecrets(kind, provider, nested, prior) 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 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
}

View File

@ -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) { func (r *mediaRepo) StaleUploadSessionIDs(ctx context.Context, before time.Time) ([]uint, error) {
var ids []uint 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 return ids, err
} }
func (r *mediaRepo) DeleteUploadData(ctx context.Context, uploadID uint) error { func (r *mediaRepo) DeleteUploadData(ctx context.Context, uploadID uint) error {

View File

@ -438,7 +438,7 @@ func alipayCreateMethod(extra, config map[string]any) (string, error) {
} }
normalized := paymentkit.NormalizePaymentMethod(value) normalized := paymentkit.NormalizePaymentMethod(value)
switch normalized { 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 return "alipay.trade.create", nil
case "pay", "trade_pay", "alipay_trade_pay", "barcode", "barcode_pay", "micropay", "face_to_face": case "pay", "trade_pay", "alipay_trade_pay", "barcode", "barcode_pay", "micropay", "face_to_face":
return "alipay.trade.pay", nil return "alipay.trade.pay", nil

View File

@ -113,7 +113,7 @@ func douyinCreateMethod(extra, config map[string]any) (string, error) {
} }
normalized := paymentkit.NormalizePaymentMethod(value) normalized := paymentkit.NormalizePaymentMethod(value)
switch normalized { 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 return "jsapi", nil
case "app", "app_pay", "apporder": case "app", "app_pay", "apporder":
return "app", nil return "app", nil

View File

@ -38,7 +38,7 @@ func (a *lakalaAdapter) Create(ctx context.Context, req *bizpayment.PaymentReque
rsp, err = client.CreateJSAPIOrder(ctx, req.TradeNo, bm) rsp, err = client.CreateJSAPIOrder(ctx, req.TradeNo, bm)
case "h5", "h5_pay", "mobile_h5", "mobile_h5_pay": case "h5", "h5_pay", "mobile_h5", "mobile_h5_pay":
rsp, err = client.CreateH5PayOrder(ctx, req.TradeNo, bm) 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) rsp, err = client.CreateMiniProgramOrder(ctx, req.TradeNo, bm)
case "native", "native_qrcode", "native_qr": case "native", "native_qrcode", "native_qr":
rsp, err = client.CreateNativeQRCodeOrder(ctx, req.TradeNo, bm) rsp, err = client.CreateNativeQRCodeOrder(ctx, req.TradeNo, bm)

View File

@ -61,7 +61,11 @@ func NewGinEngineWithRuntime(runtime *config.Store, access *systemservice.Access
if ws != nil && ws.Enabled() { if ws != nil && ws.Enabled() {
engine.GET(ws.Path(), handleWebSocket) 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) registerSwagger(engine, prefix, version, logger)
} }
staticfiles.Register(engine, runtime) staticfiles.Register(engine, runtime)

View File

@ -30,6 +30,9 @@ type PageResult struct {
} }
func Write(c *gin.Context, code int, data any, message string) { 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}) 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 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) { func NoAuth(c *gin.Context, message string) {
c.AbortWithStatusJSON(http.StatusUnauthorized, Response{Code: CodeError, Data: nil, Msg: message}) c.AbortWithStatusJSON(http.StatusUnauthorized, Response{Code: CodeError, Data: nil, Msg: message})
} }

View File

@ -40,10 +40,11 @@ func AccessLog(runtime *config.Store, logger *slog.Logger, version string) gin.H
} }
bodyLimit := defaultRequestBodyLimit bodyLimit := defaultRequestBodyLimit
if mediaUpload { if mediaUpload {
bodyLimit = system.DefaultMaxMediaFileSize + (1 << 20) mediaSettings := system.MediaSettings{}
if admin != nil && admin.Media != nil && admin.Media.MaxFileSize > 0 { if admin != nil && admin.Media != nil {
bodyLimit = admin.Media.MaxFileSize + (1 << 20) mediaSettings.MaxFileSize = admin.Media.MaxFileSize
} }
bodyLimit = mediaSettings.EffectiveMaxFileSize() + (1 << 20)
} }
bytesIn := c.Request.ContentLength bytesIn := c.Request.ContentLength
logLimit := 1024 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) requestText = redactJSON(requestBody, c.GetHeader("Content-Type"), logLimit)
} }
c.Set(ctxReqBodyKey, requestText) c.Set(ctxReqBodyKey, requestText)
c.Set(ctxRespTruncatedKey, writer.Truncated())
if paymentCallback { if paymentCallback {
// Callback acknowledgements and provider payloads must not flow into // Callback acknowledgements and provider payloads must not flow into
// the generic response/error audit pipeline. // the generic response/error audit pipeline.
@ -110,6 +112,9 @@ func AccessLog(runtime *config.Store, logger *slog.Logger, version string) gin.H
return return
} }
responseText := redactJSON(writer.body.Bytes(), c.Writer.Header().Get("Content-Type"), logLimit) responseText := redactJSON(writer.body.Bytes(), c.Writer.Header().Get("Content-Type"), logLimit)
if writer.Truncated() {
responseText = "[超出记录长度]"
}
if paymentCallback { if paymentCallback {
responseText = "[支付回调响应已省略]" responseText = "[支付回调响应已省略]"
} }

View File

@ -43,8 +43,7 @@ func OperationAudit(runtime *config.Store, recorder *systemservice.AuditRecorder
} }
if c.Request.Method == http.MethodGet { if c.Request.Method == http.MethodGet {
requestBody = operationQueryBody(c.Request.URL.RawQuery) requestBody = operationQueryBody(c.Request.URL.RawQuery)
} else if value, ok := c.Get(ctxReqBodyKey); ok { } else if _, ok := c.Get(ctxReqBodyKey); ok {
requestBody = []byte(stringValue(value))
} else if c.Request.Body != nil { } else if c.Request.Body != nil {
// Fallback for tests or custom middleware chains that omit AccessLog. // Fallback for tests or custom middleware chains that omit AccessLog.
if strings.Contains(c.GetHeader("Content-Type"), "multipart/form-data") { 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) 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 = "[超出记录长度]" responseBody = "[超出记录长度]"
} }
errorMessage := c.Errors.ByType(gin.ErrorTypePrivate).String() errorMessage := c.Errors.ByType(gin.ErrorTypePrivate).String()
operationBody := "" operationBody := ""
capturedBody := false capturedBody := false
if c.Request.Method != http.MethodGet { if c.Request.Method != "GET" {
if value, ok := c.Get(ctxReqBodyKey); ok { if value, ok := c.Get(ctxReqBodyKey); ok {
// AccessLog has already captured, redacted, summarized, and bounded the // AccessLog has already captured, redacted, summarized, and bounded the
// request body. Treat that value as final so audit does not process it a // 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
}

View File

@ -12,14 +12,16 @@ import (
// OperationAudit consumes these values after the handler returns, avoiding a // OperationAudit consumes these values after the handler returns, avoiding a
// second body read/writer wrapper. // second body read/writer wrapper.
const ( const (
ctxReqBodyKey = "kra_req_body" ctxReqBodyKey = "kra_req_body"
ctxRespBufferKey = "kra_resp_buffer" ctxRespBufferKey = "kra_resp_buffer"
ctxRespTruncatedKey = "kra_resp_truncated"
) )
type captureWriter struct { type captureWriter struct {
gin.ResponseWriter gin.ResponseWriter
body bytes.Buffer body bytes.Buffer
maxBytes int maxBytes int
truncated bool
} }
func (w *captureWriter) Write(data []byte) (int, error) { 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() remaining := limit - w.body.Len()
if len(data) > remaining { if len(data) > remaining {
_, _ = w.body.Write(data[:remaining]) _, _ = w.body.Write(data[:remaining])
w.truncated = true
} else { } else {
_, _ = w.body.Write(data) _, _ = w.body.Write(data)
} }
@ -38,6 +41,8 @@ func (w *captureWriter) Write(data []byte) (int, error) {
return w.ResponseWriter.Write(data) return w.ResponseWriter.Write(data)
} }
func (w *captureWriter) Truncated() bool { return w != nil && w.truncated }
func redactJSON(raw []byte, contentType string, limit int) string { func redactJSON(raw []byte, contentType string, limit int) string {
if len(raw) == 0 { if len(raw) == 0 {
return "" return ""

View File

@ -4,7 +4,6 @@ import (
"context" "context"
"encoding/json" "encoding/json"
integrationbiz "kra/internal/biz/integration" integrationbiz "kra/internal/biz/integration"
"strings"
"kra/internal/service/dto" "kra/internal/service/dto"
) )
@ -76,8 +75,7 @@ func redactIntegrationJSON(raw json.RawMessage) json.RawMessage {
switch x := v.(type) { switch x := v.(type) {
case map[string]any: case map[string]any:
for k, item := range x { for k, item := range x {
n := strings.ToLower(strings.ReplaceAll(k, "-", "_")) if integrationbiz.IsIntegrationSecretKey(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") {
x[k] = "******" x[k] = "******"
} else { } else {
walk(item) walk(item)

View File

@ -54,7 +54,6 @@ const createQrCode = () => {
const local = window.location const local = window.location
codeUrl.value = local.protocol + '//' + local.host + '/#/scanUpload?id=' + props.classId + '&token=' + userStore.token + '&t=' + Date.now() codeUrl.value = local.protocol + '//' + local.host + '/#/scanUpload?id=' + props.classId + '&token=' + userStore.token + '&t=' + Date.now()
dialogVisible.value = true dialogVisible.value = true
console.log(codeUrl.value)
} }
const onFinished = () => { const onFinished = () => {