订单接口最小化与私有文件访问加固
- 订单列表使用独立最小 DTO 并分页,号主待办提供独立接口与统计 - 用户 token 增加版本控制,冻结/改密/退出即时撤销会话 - 移除 URL token 传参,SSE 与接口统一使用 HttpOnly Cookie - 私有文件按上传归属与业务关联授权,收款凭证转私有访问并校验归属 - 公开商品接口返回最小字段,隐藏号主身份与内部状态 - 每日清理超过 30 天未关联业务的上传归属,上传归属失败时补偿删除对象
This commit is contained in:
@@ -4,6 +4,7 @@ import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"net/url"
|
||||
"strings"
|
||||
|
||||
"hfb_sys/backend/internal/model"
|
||||
@@ -74,6 +75,9 @@ func (r *Repository) FindByID(ctx context.Context, userID, id uint64) (*PaymentA
|
||||
|
||||
func (r *Repository) Create(ctx context.Context, userID uint64, req CreatePaymentAccountRequest) (*PaymentAccountDTO, error) {
|
||||
db := r.db.WithContext(ctx)
|
||||
if err := r.validateCertificateURLs(ctx, userID, req.CertificateURLs, nil); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// 加密账号
|
||||
encryptedNo, err := r.encryptor.Encrypt(req.AccountNo)
|
||||
if err != nil {
|
||||
@@ -131,6 +135,11 @@ func (r *Repository) Update(ctx context.Context, userID, id uint64, req UpdatePa
|
||||
}
|
||||
|
||||
if len(req.CertificateURLs) > 0 {
|
||||
var existingURLs []string
|
||||
_ = json.Unmarshal(account.CertificateURLs, &existingURLs)
|
||||
if err := r.validateCertificateURLs(ctx, userID, req.CertificateURLs, existingURLs); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
certURLs, _ := json.Marshal(req.CertificateURLs)
|
||||
updates["certificate_urls"] = certURLs
|
||||
}
|
||||
@@ -152,6 +161,54 @@ func (r *Repository) Update(ctx context.Context, userID, id uint64, req UpdatePa
|
||||
return r.FindByID(ctx, userID, id)
|
||||
}
|
||||
|
||||
// validateCertificateURLs 确保新增收款凭证来自当前用户上传的 payment-cert 对象。
|
||||
// 已绑定在当前收款账号上的历史凭证允许保留,避免旧数据无法编辑。
|
||||
func (r *Repository) validateCertificateURLs(ctx context.Context, userID uint64, values []string, existing []string) error {
|
||||
if len(values) == 0 {
|
||||
return nil
|
||||
}
|
||||
existingKeys := make(map[string]struct{}, len(existing))
|
||||
for _, value := range existing {
|
||||
if key, ok := paymentCertificateKey(value); ok {
|
||||
existingKeys[key] = struct{}{}
|
||||
}
|
||||
}
|
||||
for _, value := range values {
|
||||
key, ok := paymentCertificateKey(value)
|
||||
if !ok {
|
||||
return ErrInvalidCertificateURL
|
||||
}
|
||||
if _, exists := existingKeys[key]; exists {
|
||||
continue
|
||||
}
|
||||
var count int64
|
||||
if err := r.db.WithContext(ctx).Table("file_upload_owners").
|
||||
Where("user_id = ? AND object_key = ?", userID, key).
|
||||
Count(&count).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
if count == 0 {
|
||||
return ErrInvalidCertificateURL
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func paymentCertificateKey(value string) (string, bool) {
|
||||
parsed, err := url.Parse(value)
|
||||
if err != nil {
|
||||
return "", false
|
||||
}
|
||||
if parsed.Path != "/api/files/object" && parsed.Path != "/api/public/files/object" {
|
||||
return "", false
|
||||
}
|
||||
key := parsed.Query().Get("key")
|
||||
if !strings.HasPrefix(key, "payment-cert/") {
|
||||
return "", false
|
||||
}
|
||||
return key, true
|
||||
}
|
||||
|
||||
func (r *Repository) Delete(ctx context.Context, userID, id uint64) error {
|
||||
db := r.db.WithContext(ctx)
|
||||
var account model.UserPaymentAccount
|
||||
@@ -269,6 +326,7 @@ func (r *Repository) toDTO(account model.UserPaymentAccount) (*PaymentAccountDTO
|
||||
if account.CertificateURLs != nil {
|
||||
json.Unmarshal(account.CertificateURLs, &certURLs)
|
||||
}
|
||||
certURLs = privateCertificateURLs(certURLs)
|
||||
|
||||
return &PaymentAccountDTO{
|
||||
ID: account.ID,
|
||||
@@ -286,6 +344,22 @@ func (r *Repository) toDTO(account model.UserPaymentAccount) (*PaymentAccountDTO
|
||||
}, nil
|
||||
}
|
||||
|
||||
// privateCertificateURLs 兼容历史公开收款凭证 URL,统一经私有对象接口读取。
|
||||
func privateCertificateURLs(values []string) []string {
|
||||
for index, value := range values {
|
||||
parsed, err := url.Parse(value)
|
||||
if err != nil || parsed.Path != "/api/public/files/object" {
|
||||
continue
|
||||
}
|
||||
if !strings.HasPrefix(parsed.Query().Get("key"), "payment-cert/") {
|
||||
continue
|
||||
}
|
||||
parsed.Path = "/api/files/object"
|
||||
values[index] = parsed.String()
|
||||
}
|
||||
return values
|
||||
}
|
||||
|
||||
// 账号脱敏
|
||||
func maskAccountNo(accountNo, accountType string) string {
|
||||
length := len(accountNo)
|
||||
|
||||
Reference in New Issue
Block a user