This commit is contained in:
2026-03-13 15:51:59 +08:00
parent 4db2386bbf
commit 4e91f4cede
133 changed files with 19502 additions and 37 deletions

View File

@@ -0,0 +1,107 @@
package config
import (
"time"
"github.com/spf13/viper"
)
// Config 全局配置
type Config struct {
Server ServerConfig `mapstructure:"server"`
Database DatabaseConfig `mapstructure:"database"`
Redis RedisConfig `mapstructure:"redis"`
Security SecurityConfig `mapstructure:"security"`
JWT JWTConfig `mapstructure:"jwt"`
Notify NotifyConfig `mapstructure:"notify"`
Reconciliation ReconciliationConfig `mapstructure:"reconciliation"`
Merchant MerchantConfig `mapstructure:"merchant"`
Log LogConfig `mapstructure:"log"`
Channels ChannelsConfig `mapstructure:"channels"`
}
// ChannelsConfig 各渠道网关地址配置
type ChannelsConfig struct {
Heepay HeepayURLConfig `mapstructure:"heepay"`
}
// HeepayURLConfig 汇元网关地址
type HeepayURLConfig struct {
PayURL string `mapstructure:"pay_url"` // 支付网关
MerchantURL string `mapstructure:"merchant_url"` // 进件网关
}
type ServerConfig struct {
Port int `mapstructure:"port"`
ReadTimeout time.Duration `mapstructure:"read_timeout"`
WriteTimeout time.Duration `mapstructure:"write_timeout"`
}
type DatabaseConfig struct {
DSN string `mapstructure:"dsn"`
MaxOpenConns int `mapstructure:"max_open_conns"`
MaxIdleConns int `mapstructure:"max_idle_conns"`
ConnMaxLifetime time.Duration `mapstructure:"conn_max_lifetime"`
}
type RedisConfig struct {
Addr string `mapstructure:"addr"`
Password string `mapstructure:"password"`
DB int `mapstructure:"db"`
PoolSize int `mapstructure:"pool_size"`
}
type SecurityConfig struct {
FieldEncryptKey string `mapstructure:"field_encrypt_key"`
AppSecretSalt string `mapstructure:"app_secret_salt"`
}
type JWTConfig struct {
Secret string `mapstructure:"secret"`
ExpireHours int `mapstructure:"expire_hours"`
}
type NotifyConfig struct {
PollerInterval time.Duration `mapstructure:"poller_interval"`
PollerBatch int `mapstructure:"poller_batch"`
HTTPTimeout time.Duration `mapstructure:"http_timeout"`
}
type ReconciliationConfig struct {
Cron string `mapstructure:"cron"`
BillRetryTimes int `mapstructure:"bill_retry_times"`
}
type MerchantConfig struct {
StatusCheckCron string `mapstructure:"status_check_cron"`
}
type LogConfig struct {
Level string `mapstructure:"level"`
Format string `mapstructure:"format"`
}
// Load 加载配置文件
func Load(cfgFile string) (*Config, error) {
v := viper.New()
if cfgFile != "" {
v.SetConfigFile(cfgFile)
} else {
v.SetConfigName("config.local")
v.SetConfigType("yaml")
v.AddConfigPath("./configs")
v.AddConfigPath(".")
}
v.AutomaticEnv()
if err := v.ReadInConfig(); err != nil {
return nil, err
}
var cfg Config
if err := v.Unmarshal(&cfg); err != nil {
return nil, err
}
return &cfg, nil
}

View File

@@ -0,0 +1,38 @@
package config
import (
"fmt"
"log/slog"
"gorm.io/driver/mysql"
"gorm.io/gorm"
"gorm.io/gorm/logger"
)
// NewDB 初始化 MySQL 连接
func NewDB(cfg DatabaseConfig) (*gorm.DB, error) {
gormCfg := &gorm.Config{
Logger: logger.Default.LogMode(logger.Warn),
}
db, err := gorm.Open(mysql.Open(cfg.DSN), gormCfg)
if err != nil {
return nil, fmt.Errorf("open mysql: %w", err)
}
sqlDB, err := db.DB()
if err != nil {
return nil, err
}
sqlDB.SetMaxOpenConns(cfg.MaxOpenConns)
sqlDB.SetMaxIdleConns(cfg.MaxIdleConns)
sqlDB.SetConnMaxLifetime(cfg.ConnMaxLifetime)
if err := sqlDB.Ping(); err != nil {
return nil, fmt.Errorf("ping mysql: %w", err)
}
slog.Info("mysql connected")
return db, nil
}

View File

@@ -0,0 +1,26 @@
package config
import (
"context"
"fmt"
"log/slog"
"github.com/go-redis/redis/v8"
)
// NewRedis 初始化 Redis 客户端
func NewRedis(cfg RedisConfig) (*redis.Client, error) {
rdb := redis.NewClient(&redis.Options{
Addr: cfg.Addr,
Password: cfg.Password,
DB: cfg.DB,
PoolSize: cfg.PoolSize,
})
if err := rdb.Ping(context.Background()).Err(); err != nil {
return nil, fmt.Errorf("ping redis: %w", err)
}
slog.Info("redis connected")
return rdb, nil
}

72
backend/pkg/crypto/aes.go Normal file
View File

@@ -0,0 +1,72 @@
package crypto
import (
"crypto/aes"
"crypto/cipher"
"crypto/rand"
"encoding/base64"
"errors"
"io"
)
// Encrypt 使用 AES-256-GCM 加密明文,返回 base64 编码的密文
func Encrypt(plaintext, key string) (string, error) {
keyBytes := []byte(key)
if len(keyBytes) != 32 {
return "", errors.New("encrypt key must be 32 bytes")
}
block, err := aes.NewCipher(keyBytes)
if err != nil {
return "", err
}
gcm, err := cipher.NewGCM(block)
if err != nil {
return "", err
}
nonce := make([]byte, gcm.NonceSize())
if _, err = io.ReadFull(rand.Reader, nonce); err != nil {
return "", err
}
ciphertext := gcm.Seal(nonce, nonce, []byte(plaintext), nil)
return base64.StdEncoding.EncodeToString(ciphertext), nil
}
// Decrypt 使用 AES-256-GCM 解密
func Decrypt(ciphertextB64, key string) (string, error) {
keyBytes := []byte(key)
if len(keyBytes) != 32 {
return "", errors.New("decrypt key must be 32 bytes")
}
ciphertext, err := base64.StdEncoding.DecodeString(ciphertextB64)
if err != nil {
return "", err
}
block, err := aes.NewCipher(keyBytes)
if err != nil {
return "", err
}
gcm, err := cipher.NewGCM(block)
if err != nil {
return "", err
}
nonceSize := gcm.NonceSize()
if len(ciphertext) < nonceSize {
return "", errors.New("ciphertext too short")
}
nonce, ciphertext := ciphertext[:nonceSize], ciphertext[nonceSize:]
plaintext, err := gcm.Open(nil, nonce, ciphertext, nil)
if err != nil {
return "", err
}
return string(plaintext), nil
}

View File

@@ -0,0 +1,31 @@
package logger
import (
"log/slog"
"os"
)
// Init 初始化全局 logger
func Init(level, format string) {
var lvl slog.Level
switch level {
case "debug":
lvl = slog.LevelDebug
case "warn":
lvl = slog.LevelWarn
case "error":
lvl = slog.LevelError
default:
lvl = slog.LevelInfo
}
opts := &slog.HandlerOptions{Level: lvl}
var handler slog.Handler
if format == "json" {
handler = slog.NewJSONHandler(os.Stdout, opts)
} else {
handler = slog.NewTextHandler(os.Stdout, opts)
}
slog.SetDefault(slog.New(handler))
}

View File

@@ -0,0 +1,50 @@
package sequence
import (
"context"
"fmt"
"time"
"pay-bridge/internal/model"
"pay-bridge/internal/repository"
)
// Service 订单编码服务
type Service struct {
repo *repository.SequenceRepository
}
func NewService(repo *repository.SequenceRepository) *Service {
return &Service{repo: repo}
}
// NextTradeNo 生成下一个交易号格式PAY{yyMMdd}{8位序号}
func (s *Service) NextTradeNo(ctx context.Context, appID string) (string, error) {
return s.next(ctx, appID, model.SeqTypeTrade)
}
// NextRefundNo 生成下一个退款单号格式REF{yyMMdd}{8位序号}
func (s *Service) NextRefundNo(ctx context.Context, appID string) (string, error) {
return s.next(ctx, appID, model.SeqTypeRefund)
}
// NextSharingNo 生成下一个分润单号格式SHA{yyMMdd}{8位序号}
func (s *Service) NextSharingNo(ctx context.Context, appID string) (string, error) {
return s.next(ctx, appID, model.SeqTypeSharing)
}
func (s *Service) next(ctx context.Context, appID string, seqType model.SeqType) (string, error) {
val, err := s.repo.IncrAndGet(ctx, appID, seqType)
if err != nil {
return "", err
}
prefix, err := s.repo.GetPrefix(ctx, appID, seqType)
if err != nil {
return "", err
}
date := time.Now().Format("060102") // yyMMdd
seq := fmt.Sprintf("%08d", val%100_000_000) // 8 位溢出归零每日最多1亿笔
return prefix + date + seq, nil
}