优化结构

This commit is contained in:
Yvan
2026-08-28 16:23:52 +08:00
parent 9b1daaf3f6
commit 295ee5c49e
24 changed files with 128 additions and 145 deletions
@@ -400,7 +400,7 @@ func IsIntegrationSecretKey(key string) bool {
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"} {
for _, item := range []string{"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
}
@@ -155,10 +155,10 @@ var integrationDefinitions = map[string][]IntegrationConfigDefinition{
integrationSelect("environment", "环境", false, "production", "sandbox"), integrationSelect("sign_type", "签名算法", false, "RSA2", "RSA"), integrationField("gateway_url", "网关地址", false, false, "url"),
integrationSelect("method", "默认支付方式", false, "alipay.trade.create", "alipay.trade.pay", "alipay.trade.precreate", "alipay.trade.app.pay", "alipay.trade.page.pay", "alipay.trade.wap.pay")),
paymentDefinition(bizpayment.PaymentAlipayV3, "支付宝 V3", "支付宝证书模式 V3 接口", map[string]any{"app_id": "", "private_key": "", "app_cert": "", "root_cert": "", "public_cert": "", "environment": "production", "api_base_url": "https://openapi.alipay.com", "gateway_url": "https://openapi.alipay.com/gateway.do", "method": "alipay.trade.create"},
integrationField("app_id", "应用 ID", true, false, "text"), integrationField("private_key", "应用私钥", false, true, "textarea"), integrationField("app_cert", "应用公钥证书", false, true, "textarea"), integrationField("root_cert", "支付宝根证书", false, true, "textarea"), integrationField("public_cert", "支付宝公钥证书", false, true, "textarea"),
integrationField("app_id", "应用 ID", true, false, "text"), integrationField("private_key", "应用私钥", true, true, "textarea"), integrationField("app_cert", "应用公钥证书", true, true, "textarea"), integrationField("root_cert", "支付宝根证书", true, true, "textarea"), integrationField("public_cert", "支付宝公钥证书", true, true, "textarea"),
integrationSelect("environment", "环境", false, "production", "sandbox"), integrationField("api_base_url", "API 地址", false, false, "url"), integrationField("gateway_url", "网关地址", false, false, "url"), integrationSelect("method", "默认支付方式", false, "alipay.trade.create", "alipay.trade.pay", "alipay.trade.precreate", "alipay.trade.app.pay", "alipay.trade.page.pay", "alipay.trade.wap.pay")),
paymentDefinition(bizpayment.PaymentWechatV2, "微信支付 V2", "微信支付 V2,含退款双向证书", map[string]any{"app_id": "", "merchant_id": "", "mch_key": "", "sign_type": "MD5", "trade_type": "NATIVE", "client_cert": "", "client_key": ""},
integrationField("app_id", "应用 ID", true, false, "text"), integrationField("merchant_id", "商户号", true, false, "text"), integrationField("mch_key", "API 密钥", true, true, "password"), integrationSelect("sign_type", "签名算法", false, "MD5", "HMAC-SHA256"), integrationSelect("trade_type", "默认交易类型", false, "JSAPI", "APP", "NATIVE", "MWEB", "MICROPAY"), integrationField("client_cert", "商户证书", false, true, "textarea"), integrationField("client_key", "证书私钥", false, true, "textarea")),
integrationField("app_id", "应用 ID", true, false, "text"), integrationField("merchant_id", "商户号", true, false, "text"), integrationField("mch_key", "API 密钥", true, true, "password"), integrationSelect("sign_type", "签名算法", false, "MD5", "HMAC-SHA256"), integrationSelect("trade_type", "默认交易类型", false, "JSAPI", "APP", "NATIVE", "MWEB", "MICROPAY"), integrationField("client_cert", "商户证书", true, true, "textarea"), integrationField("client_key", "证书私钥", true, true, "textarea")),
paymentDefinition(bizpayment.PaymentWechatV3, "微信支付 V3", "微信支付 API v3", map[string]any{"app_id": "", "merchant_id": "", "serial_no": "", "private_key": "", "api_v3_key": "", "platform_cert": "", "platform_serial_no": "", "trade_type": "jsapi"},
integrationField("app_id", "应用 ID", true, false, "text"), integrationField("merchant_id", "商户号", true, false, "text"), integrationField("serial_no", "商户证书序列号", true, false, "text"), integrationField("private_key", "商户私钥", true, true, "textarea"), integrationField("api_v3_key", "API v3 密钥", true, true, "password"), integrationField("platform_cert", "平台证书", true, true, "textarea"), integrationField("platform_serial_no", "平台证书序列号", false, false, "text"), integrationSelect("trade_type", "默认交易类型", false, "jsapi", "app", "native", "h5", "codepay")),
paymentDefinition(bizpayment.PaymentApple, "Apple IAP", "Apple App Store Server API", map[string]any{"issuer_id": "", "key_id": "", "bundle_id": "", "private_key": "", "price_divisor": 10, "environment": "production", "test_product_id": "", "test_transaction_id": ""},
+4
View File
@@ -965,6 +965,10 @@ func paymentOrderFingerprint(req *PaymentRequest, extra json.RawMessage) string
return hex.EncodeToString(sum[:])
}
func RequestFingerprint(req *PaymentRequest, extra json.RawMessage) string {
return paymentOrderFingerprint(req, extra)
}
func paymentResultFromOrder(order *PaymentOrder) *PaymentResult {
if order == nil {
return nil
+2 -1
View File
@@ -5,7 +5,8 @@ import (
)
var (
ErrUserNotFound = errors.NotFound("USER_NOT_FOUND", "user not found")
ErrUserNotFound = errors.NotFound("USER_NOT_FOUND", "user not found")
ErrMediaNotFound = errors.NotFound("MEDIA_NOT_FOUND", "media not found")
// ErrInvalidCredentials is returned for any failed login, regardless of
// whether the user exists, to avoid leaking which accounts are registered.
ErrInvalidCredentials = errors.Unauthorized("INVALID_CREDENTIALS", "invalid credentials")
+2
View File
@@ -110,10 +110,12 @@ func (uc *MediaUsecase) Upload(ctx context.Context, userID uint, name, suppliedM
if save {
count, countErr := uc.MediaKeyReferences(ctx, key)
if countErr != nil {
_ = uc.files.Delete(ctx, key)
return nil, countErr
}
if count == 0 {
if err = uc.CreateMedia(ctx, media); err != nil {
_ = uc.files.Delete(ctx, key)
return nil, err
}
}
+1
View File
@@ -260,6 +260,7 @@ func (uc *MediaUsecase) CompleteUpload(ctx context.Context, userID, uploadID uin
}
}
if err = uc.CreateMedia(ctx, media); err != nil {
_ = uc.files.Delete(ctx, key)
return fail(err)
}
if err = uc.CompleteUploadSession(ctx, uploadID, key, media.ID); err != nil {
+1
View File
@@ -23,6 +23,7 @@ type MediaSettings struct {
}
const DefaultMaxMediaFileSize int64 = 100 << 20
const UploadBodyOverhead int64 = 1 << 20
func (s MediaSettings) EffectiveMaxFileSize() int64 {
if s.MaxFileSize > 0 {
+3 -2
View File
@@ -4,7 +4,7 @@
configuration reloads, and repository implementations. File watching and
immutable configuration snapshots belong to `internal/config`.
- `system/`: system repositories and system table persistence
- `system/`: system repositories, media_* persistence, and system maintenance
- `task/`: timed-task tables and task persistence
- `integration/`: `sys_integration_configs` and integration configuration persistence
- `payment/`: payment-order persistence
@@ -23,9 +23,10 @@ reload locks. Do not split them into packages only to reduce file count.
当前内置数据模块为 `system`、`integration`、`task` 和 `payment`。其中:
- `system` 只拥有 `sys_*` 系统表、系统仓储、种子和系统维护清理;
- `system` 拥有 `sys_*` 系统表、media_* 媒体表、系统仓储、种子和系统维护清理;
- `integration` 唯一拥有 `sys_integration_configs` 表模型、配置仓储和通信默认值;
- `task` 只拥有定时任务与任务日志表;
- `provider/` 提供共享数据库 seam;
- `payment` 只拥有 `pay_orders` 等支付持久化,通过 `biz/integration.PaymentConfigReader`
读取支付配置,不感知 integration 的 PO 或表结构。
@@ -248,7 +248,7 @@ func mergeIntegrationSecrets(kind, provider string, values, old map[string]any)
secretFields := integrationSecretFields(kind, provider)
for key, value := range values {
if secretFields[key] || integrationbiz.IsIntegrationSecretKey(key) {
if text, ok := value.(string); ok && text == config.MaskedSecret {
if text, ok := value.(string); ok && config.IsMaskedSecret(strings.TrimSpace(text)) {
if prior, exists := old[key]; exists {
values[key] = prior
} else {
+1 -6
View File
@@ -115,7 +115,7 @@ func (r *paymentRepo) TestProvider(ctx context.Context, provider string) (*bizpa
TradeNo: req.TradeNo, Provider: provider, BusinessType: req.BusinessType, BusinessID: req.BusinessID,
Subject: req.Subject, PaymentMode: bizpayment.PaymentModeExternal, OriginalAmount: req.Amount, Amount: req.Amount,
Currency: req.Currency, PaymentStatus: bizpayment.PaymentStatusInitialized, FulfillmentStatus: bizpayment.FulfillmentStatusPending,
RefundStatus: bizpayment.RefundStatusNone, ConfirmationID: uuid.NewString(), RequestFingerprint: paymentTestFingerprint(req), Extra: extra,
RefundStatus: bizpayment.RefundStatusNone, ConfirmationID: uuid.NewString(), RequestFingerprint: bizpayment.RequestFingerprint(req, extra), Extra: extra,
})
if err != nil {
add("local_order", "failed", err.Error(), req.TradeNo, localStart)
@@ -224,11 +224,6 @@ func (r *paymentRepo) TestProvider(ctx context.Context, provider string) (*bizpa
return test, nil
}
func paymentTestFingerprint(req *bizpayment.PaymentRequest) string {
raw, _ := json.Marshal(req)
hash := sha256.Sum256(raw)
return hex.EncodeToString(hash[:])
}
func paymentTestProviderUpdate(result *bizpayment.PaymentResult) *bizpayment.PaymentProviderUpdate {
if result == nil {
+3
View File
@@ -60,6 +60,9 @@ func (r *mediaRepo) CreateMedia(ctx context.Context, v *system.MediaFile) error
func (r *mediaRepo) FindMedia(ctx context.Context, id uint) (*system.MediaFile, error) {
var po mediaPO
if err := r.data.DB().WithContext(ctx).First(&po, id).Error; err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, system.ErrMediaNotFound
}
return nil, err
}
return mediaFromPO(po), nil
+1 -1
View File
@@ -95,7 +95,7 @@ func (r *mediaRepo) CompleteUploadSession(ctx context.Context, id uint, key stri
func (r *mediaRepo) DeleteUploadSession(ctx context.Context, id uint) error {
// The compatible flow uses GORM's normal Delete here, retaining the soft-deleted session
// for audit/recovery rather than physically removing it.
return r.data.DB().WithContext(ctx).Delete(&uploadSessionPO{}, id).Error
return r.data.DB().WithContext(ctx).Unscoped().Delete(&uploadSessionPO{}, id).Error
}
func (r *mediaRepo) UpsertChunk(ctx context.Context, uploadID uint, v *system.UploadChunk) error {
po := uploadChunkPO{UploadID: uploadID, ChunkIndex: v.Index, ChunkHash: v.Hash, Size: v.Size}
+4 -44
View File
@@ -33,6 +33,10 @@ func Migrations() []migration.Step {
}
}
// Kept as a migration version for existing installations; the canonical
// communication surface seeder is idempotent and now owns the test endpoint.
func ensureCommunicationTestSurface(db *gorm.DB) error { return ensureCommunicationSurface(db) }
func ensureCommunicationSurface(db *gorm.DB) error {
if db == nil || !db.Migrator().HasTable(&menuPO{}) || !db.Migrator().HasTable(&apiPO{}) {
return nil
@@ -128,47 +132,3 @@ func ensureCommunicationSurface(db *gorm.DB) error {
return nil
})
}
func ensureCommunicationTestSurface(db *gorm.DB) error {
if db == nil || !db.Migrator().HasTable(&menuPO{}) || !db.Migrator().HasTable(&apiPO{}) {
return nil
}
return db.Transaction(func(tx *gorm.DB) error {
var existingMenus int64
if err := tx.Model(&menuPO{}).Where("name IN ?", []string{"extensions", "integrationConfig"}).Count(&existingMenus).Error; err != nil {
return err
}
if existingMenus == 0 {
return nil
}
api := apiPO{
Path: "/integration/configs/:kind/:provider/test",
Method: "POST",
APIGroup: "集成配置",
Description: "测试通信集成连接",
}
if err := tx.Where("path = ? AND method = ?", api.Path, api.Method).FirstOrCreate(&api).Error; err != nil {
return err
}
if !tx.Migrator().HasTable(&authorityPO{}) || !tx.Migrator().HasTable(&casbinRulePO{}) {
return nil
}
var authority authorityPO
if err := tx.Where("authority_id = ?", 888).First(&authority).Error; err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil
}
return err
}
exists, err := policyExists(tx, authority.AuthorityID, api.Path, api.Method)
if err != nil {
return err
}
if exists {
return nil
}
rule := newPolicyRule(authority.AuthorityID, api.Path, api.Method)
return tx.Create(&rule).Error
})
}
+16 -9
View File
@@ -339,24 +339,34 @@ func maskDataSecrets(value *config.Data) {
return
}
if value.Database != nil {
value.Database.Password = config.MaskedSecret
if value.Database.Password != "" {
value.Database.Password = config.MaskedSecret
}
value.Database.Source = ""
}
if value.Redis != nil {
value.Redis.Password = config.MaskedSecret
if value.Redis.Password != "" {
value.Redis.Password = config.MaskedSecret
}
}
if value.Mongo != nil {
value.Mongo.Password = config.MaskedSecret
if value.Mongo.Password != "" {
value.Mongo.Password = config.MaskedSecret
}
}
for _, item := range value.DatabaseList {
if item != nil {
item.Password = config.MaskedSecret
if item.Password != "" {
item.Password = config.MaskedSecret
}
item.Source = ""
}
}
for _, item := range value.RedisList {
if item != nil {
item.Password = config.MaskedSecret
if item.Password != "" {
item.Password = config.MaskedSecret
}
}
}
}
@@ -407,14 +417,11 @@ func preserveDatabaseListSecrets(next, current []*config.Database) {
byName[item.AliasName] = item
}
}
for index, item := range next {
for _, item := range next {
if item == nil {
continue
}
previous := byName[item.AliasName]
if previous == nil && index < len(current) {
previous = current[index]
}
if previous == nil {
continue
}
+2
View File
@@ -10,7 +10,9 @@ state, or translate provider-specific protocols into `biz` interfaces.
- `email`: SMTP email repository.
- `mq`: reloadable EMQX/MQTT and RabbitMQ/AMQP clients exposed through the shared `pkg/mq` interface.
- `payment`: payment-channel SDKs and callback/signature handling.
- `runtimeconfig`: runtime integration configuration store and subscriptions.
- `storage`: local and object-storage implementations of `biz.FileStorage`.
- `systeminfo`: system information integration helpers.
- `websocket`: reloadable Melody endpoint exposed through the shared
`pkg/websocket` wrapper.
+1 -1
View File
@@ -74,7 +74,7 @@ func (t *ConnectivityTester) restoreMaskedSecrets(kind, provider string, values
}
for key := range masked {
value, _ := values[key].(string)
if strings.TrimSpace(value) != config.MaskedSecret {
if !config.IsMaskedSecret(strings.TrimSpace(value)) {
continue
}
prior, _ := currentValues[key].(string)
+3 -3
View File
@@ -46,7 +46,7 @@ var routes = map[string]routeValue{
"DELETE /info/deleteInfo": {group: "公告", description: "删除公告", audit: true},
"DELETE /info/deleteInfoByIds": {group: "公告", description: "批量删除公告", audit: true},
"DELETE /integration/configs/:kind/:provider": {group: "集成配置", description: "删除集成配置", audit: true},
"DELETE /mediaUpload/:uploadId": {group: "媒体上传", description: "取消大文件上传"},
"DELETE /mediaUpload/:uploadId": {group: "媒体上传", description: "取消大文件上传", audit: true},
"DELETE /position/deletePosition": {group: "岗位", description: "删除岗位", audit: true},
"DELETE /sysDictionary/deleteSysDictionary": {group: "系统字典", description: "删除字典", audit: true},
"DELETE /sysDictionaryDetail/deleteSysDictionaryDetail": {group: "系统字典详情", description: "删除字典内容", audit: true},
@@ -155,8 +155,8 @@ var routes = map[string]routeValue{
"POST /department/setDepartmentUsers": {group: "部门", description: "设置部门成员(反向分配)", audit: true},
"POST /email/emailTest": {group: "email", description: "发送测试邮件", audit: true},
"POST /email/sendEmail": {group: "email", description: "发送邮件", audit: true},
"POST /fileUploadAndDownload/deleteFile": {group: "文件上传与下载", description: "删除文件"},
"POST /fileUploadAndDownload/deleteFiles": {group: "文件上传与下载", description: "批量删除文件"},
"POST /fileUploadAndDownload/deleteFile": {group: "文件上传与下载", description: "删除文件", audit: true},
"POST /fileUploadAndDownload/deleteFiles": {group: "文件上传与下载", description: "批量删除文件", audit: true},
"POST /fileUploadAndDownload/editFileName": {group: "文件上传与下载", description: "文件名或者备注编辑"},
"POST /fileUploadAndDownload/getFileList": {group: "文件上传与下载", description: "获取上传文件列表"},
"POST /fileUploadAndDownload/importURL": {group: "文件上传与下载", description: "导入URL"},
+2 -1
View File
@@ -2,6 +2,7 @@ package handler
import (
"errors"
"kra/internal/biz/system"
"kra/internal/service/dto"
systemservice "kra/internal/service/system"
"net/http"
@@ -16,7 +17,7 @@ func NewMedia(service *systemservice.MediaService) *Media { return &Media{servic
func (h *Media) limitMultipartBody(c *gin.Context) int64 {
limit := h.service.MediaConfig().EffectiveMaxFileSize()
c.Request.Body = http.MaxBytesReader(c.Writer, c.Request.Body, limit+(1<<20))
c.Request.Body = http.MaxBytesReader(c.Writer, c.Request.Body, limit+system.UploadBodyOverhead)
return limit
}
+1 -6
View File
@@ -45,7 +45,7 @@ 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:"} {
for _, marker := range []string{"gorm", "sql:", "redis", "mysql", "mongo", "dial ", "connection", "provider", "sdk", "json:", "serialize", "timeout", "http 4", "http 5", "tls:", "record not found", "error 1062"} {
if strings.Contains(lower, marker) {
return "操作失败"
}
@@ -53,11 +53,6 @@ func sanitizeFailureMessage(message string) string {
if strings.Contains(message, "失败:") {
return strings.TrimSpace(strings.SplitN(message, "失败:", 2)[0]) + "失败"
}
for _, marker := range []string{"参数", "不能为空", "无效", "不存在", "权限", "未登录", "验证码", "密码", "重复", "不支持", "超出", "分片", "上传", "文件", "名称", "令牌", "token"} {
if strings.Contains(message, marker) {
return message
}
}
return message
}
+1 -1
View File
@@ -44,7 +44,7 @@ func AccessLog(runtime *config.Store, logger *slog.Logger, version string) gin.H
if admin != nil && admin.Media != nil {
mediaSettings.MaxFileSize = admin.Media.MaxFileSize
}
bodyLimit = mediaSettings.EffectiveMaxFileSize() + (1 << 20)
bodyLimit = mediaSettings.EffectiveMaxFileSize() + system.UploadBodyOverhead
}
bytesIn := c.Request.ContentLength
logLimit := 1024