temp
This commit is contained in:
495
internal/shared/cache/gorm_cache_plugin.go
vendored
Normal file
495
internal/shared/cache/gorm_cache_plugin.go
vendored
Normal file
@@ -0,0 +1,495 @@
|
||||
package cache
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/md5"
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"reflect"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"go.uber.org/zap"
|
||||
"gorm.io/gorm"
|
||||
|
||||
"tyapi-server/internal/shared/interfaces"
|
||||
)
|
||||
|
||||
// GormCachePlugin GORM缓存插件
|
||||
type GormCachePlugin struct {
|
||||
cache interfaces.CacheService
|
||||
logger *zap.Logger
|
||||
config CacheConfig
|
||||
}
|
||||
|
||||
// CacheConfig 缓存配置
|
||||
type CacheConfig struct {
|
||||
// 基础配置
|
||||
DefaultTTL time.Duration `json:"default_ttl"` // 默认TTL
|
||||
TablePrefix string `json:"table_prefix"` // 表前缀
|
||||
EnabledTables []string `json:"enabled_tables"` // 启用缓存的表
|
||||
DisabledTables []string `json:"disabled_tables"` // 禁用缓存的表
|
||||
|
||||
// 查询配置
|
||||
MaxCacheSize int `json:"max_cache_size"` // 单次查询最大缓存记录数
|
||||
CacheComplexSQL bool `json:"cache_complex_sql"` // 是否缓存复杂SQL
|
||||
|
||||
// 高级特性
|
||||
EnableStats bool `json:"enable_stats"` // 启用统计
|
||||
EnableWarmup bool `json:"enable_warmup"` // 启用预热
|
||||
PenetrationGuard bool `json:"penetration_guard"` // 缓存穿透保护
|
||||
BloomFilter bool `json:"bloom_filter"` // 布隆过滤器
|
||||
|
||||
// 失效策略
|
||||
AutoInvalidate bool `json:"auto_invalidate"` // 自动失效
|
||||
InvalidateDelay time.Duration `json:"invalidate_delay"` // 延迟失效时间
|
||||
}
|
||||
|
||||
// DefaultCacheConfig 默认缓存配置
|
||||
func DefaultCacheConfig() CacheConfig {
|
||||
return CacheConfig{
|
||||
DefaultTTL: 30 * time.Minute,
|
||||
TablePrefix: "gorm_cache",
|
||||
MaxCacheSize: 1000,
|
||||
CacheComplexSQL: false,
|
||||
EnableStats: true,
|
||||
EnableWarmup: false,
|
||||
PenetrationGuard: true,
|
||||
BloomFilter: false,
|
||||
AutoInvalidate: true,
|
||||
InvalidateDelay: 100 * time.Millisecond,
|
||||
}
|
||||
}
|
||||
|
||||
// NewGormCachePlugin 创建GORM缓存插件
|
||||
func NewGormCachePlugin(cache interfaces.CacheService, logger *zap.Logger, config ...CacheConfig) *GormCachePlugin {
|
||||
cfg := DefaultCacheConfig()
|
||||
if len(config) > 0 {
|
||||
cfg = config[0]
|
||||
}
|
||||
|
||||
return &GormCachePlugin{
|
||||
cache: cache,
|
||||
logger: logger,
|
||||
config: cfg,
|
||||
}
|
||||
}
|
||||
|
||||
// Name 插件名称
|
||||
func (p *GormCachePlugin) Name() string {
|
||||
return "gorm-cache-plugin"
|
||||
}
|
||||
|
||||
// Initialize 初始化插件
|
||||
func (p *GormCachePlugin) Initialize(db *gorm.DB) error {
|
||||
p.logger.Info("初始化GORM缓存插件",
|
||||
zap.Duration("default_ttl", p.config.DefaultTTL),
|
||||
zap.Bool("auto_invalidate", p.config.AutoInvalidate),
|
||||
zap.Bool("penetration_guard", p.config.PenetrationGuard),
|
||||
)
|
||||
|
||||
// 注册回调函数
|
||||
return p.registerCallbacks(db)
|
||||
}
|
||||
|
||||
// registerCallbacks 注册GORM回调
|
||||
func (p *GormCachePlugin) registerCallbacks(db *gorm.DB) error {
|
||||
// Query回调 - 查询时检查缓存
|
||||
db.Callback().Query().Before("gorm:query").Register("cache:before_query", p.beforeQuery)
|
||||
db.Callback().Query().After("gorm:query").Register("cache:after_query", p.afterQuery)
|
||||
|
||||
// Create回调 - 创建时失效缓存
|
||||
db.Callback().Create().After("gorm:create").Register("cache:after_create", p.afterCreate)
|
||||
|
||||
// Update回调 - 更新时失效缓存
|
||||
db.Callback().Update().After("gorm:update").Register("cache:after_update", p.afterUpdate)
|
||||
|
||||
// Delete回调 - 删除时失效缓存
|
||||
db.Callback().Delete().After("gorm:delete").Register("cache:after_delete", p.afterDelete)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// ================ 查询回调 ================
|
||||
|
||||
// beforeQuery 查询前回调
|
||||
func (p *GormCachePlugin) beforeQuery(db *gorm.DB) {
|
||||
// 检查是否启用缓存
|
||||
if !p.shouldCache(db) {
|
||||
return
|
||||
}
|
||||
|
||||
ctx := db.Statement.Context
|
||||
if ctx == nil {
|
||||
ctx = context.Background()
|
||||
}
|
||||
|
||||
// 生成缓存键
|
||||
cacheKey := p.generateCacheKey(db)
|
||||
|
||||
// 从缓存获取结果
|
||||
var cachedResult CachedResult
|
||||
if err := p.cache.Get(ctx, cacheKey, &cachedResult); err == nil {
|
||||
p.logger.Debug("缓存命中",
|
||||
zap.String("cache_key", cacheKey),
|
||||
zap.String("table", db.Statement.Table),
|
||||
)
|
||||
|
||||
// 恢复查询结果
|
||||
if err := p.restoreFromCache(db, &cachedResult); err == nil {
|
||||
// 设置标记,跳过实际查询
|
||||
db.Statement.Set("cache:hit", true)
|
||||
db.Statement.Set("cache:key", cacheKey)
|
||||
|
||||
// 更新统计
|
||||
if p.config.EnableStats {
|
||||
p.updateStats("hit", db.Statement.Table)
|
||||
}
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// 缓存未命中,设置标记
|
||||
db.Statement.Set("cache:miss", true)
|
||||
db.Statement.Set("cache:key", cacheKey)
|
||||
|
||||
if p.config.EnableStats {
|
||||
p.updateStats("miss", db.Statement.Table)
|
||||
}
|
||||
}
|
||||
|
||||
// afterQuery 查询后回调
|
||||
func (p *GormCachePlugin) afterQuery(db *gorm.DB) {
|
||||
// 检查是否缓存未命中
|
||||
if _, ok := db.Statement.Get("cache:miss"); !ok {
|
||||
return
|
||||
}
|
||||
|
||||
// 检查查询是否成功
|
||||
if db.Error != nil {
|
||||
return
|
||||
}
|
||||
|
||||
ctx := db.Statement.Context
|
||||
if ctx == nil {
|
||||
ctx = context.Background()
|
||||
}
|
||||
|
||||
cacheKey, _ := db.Statement.Get("cache:key")
|
||||
|
||||
// 将查询结果保存到缓存
|
||||
if err := p.saveToCache(ctx, cacheKey.(string), db); err != nil {
|
||||
p.logger.Warn("保存查询结果到缓存失败",
|
||||
zap.String("cache_key", cacheKey.(string)),
|
||||
zap.Error(err),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// ================ CUD回调 ================
|
||||
|
||||
// afterCreate 创建后回调
|
||||
func (p *GormCachePlugin) afterCreate(db *gorm.DB) {
|
||||
if !p.config.AutoInvalidate || db.Error != nil {
|
||||
return
|
||||
}
|
||||
|
||||
p.invalidateTableCache(db.Statement.Context, db.Statement.Table)
|
||||
}
|
||||
|
||||
// afterUpdate 更新后回调
|
||||
func (p *GormCachePlugin) afterUpdate(db *gorm.DB) {
|
||||
if !p.config.AutoInvalidate || db.Error != nil {
|
||||
return
|
||||
}
|
||||
|
||||
p.invalidateTableCache(db.Statement.Context, db.Statement.Table)
|
||||
}
|
||||
|
||||
// afterDelete 删除后回调
|
||||
func (p *GormCachePlugin) afterDelete(db *gorm.DB) {
|
||||
if !p.config.AutoInvalidate || db.Error != nil {
|
||||
return
|
||||
}
|
||||
|
||||
p.invalidateTableCache(db.Statement.Context, db.Statement.Table)
|
||||
}
|
||||
|
||||
// ================ 缓存管理方法 ================
|
||||
|
||||
// shouldCache 判断是否应该缓存
|
||||
func (p *GormCachePlugin) shouldCache(db *gorm.DB) bool {
|
||||
// 检查是否明确禁用缓存
|
||||
if value, ok := db.Statement.Get("cache:disabled"); ok && value.(bool) {
|
||||
return false
|
||||
}
|
||||
|
||||
// 检查是否明确启用缓存
|
||||
if value, ok := db.Statement.Get("cache:enabled"); ok && value.(bool) {
|
||||
return true
|
||||
}
|
||||
|
||||
// 检查表是否在禁用列表中
|
||||
for _, table := range p.config.DisabledTables {
|
||||
if table == db.Statement.Table {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
// 检查表是否在启用列表中(如果配置了启用列表)
|
||||
if len(p.config.EnabledTables) > 0 {
|
||||
for _, table := range p.config.EnabledTables {
|
||||
if table == db.Statement.Table {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// 检查是否为复杂查询
|
||||
if !p.config.CacheComplexSQL && p.isComplexQuery(db) {
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
// isComplexQuery 判断是否为复杂查询
|
||||
func (p *GormCachePlugin) isComplexQuery(db *gorm.DB) bool {
|
||||
sql := db.Statement.SQL.String()
|
||||
|
||||
// 检查是否包含复杂操作
|
||||
complexKeywords := []string{
|
||||
"JOIN", "UNION", "SUBQUERY", "GROUP BY",
|
||||
"HAVING", "WINDOW", "RECURSIVE",
|
||||
}
|
||||
|
||||
upperSQL := strings.ToUpper(sql)
|
||||
for _, keyword := range complexKeywords {
|
||||
if strings.Contains(upperSQL, keyword) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
// generateCacheKey 生成缓存键
|
||||
func (p *GormCachePlugin) generateCacheKey(db *gorm.DB) string {
|
||||
// 构建缓存键的组成部分
|
||||
keyParts := []string{
|
||||
p.config.TablePrefix,
|
||||
db.Statement.Table,
|
||||
}
|
||||
|
||||
// 添加SQL语句hash
|
||||
sqlHash := p.hashSQL(db.Statement.SQL.String(), db.Statement.Vars)
|
||||
keyParts = append(keyParts, sqlHash)
|
||||
|
||||
return strings.Join(keyParts, ":")
|
||||
}
|
||||
|
||||
// hashSQL 对SQL语句和参数进行hash
|
||||
func (p *GormCachePlugin) hashSQL(sql string, vars []interface{}) string {
|
||||
// 将SQL和参数组合
|
||||
combined := sql
|
||||
for _, v := range vars {
|
||||
combined += fmt.Sprintf(":%v", v)
|
||||
}
|
||||
|
||||
// 计算MD5 hash
|
||||
hasher := md5.New()
|
||||
hasher.Write([]byte(combined))
|
||||
return hex.EncodeToString(hasher.Sum(nil))
|
||||
}
|
||||
|
||||
// CachedResult 缓存结果结构
|
||||
type CachedResult struct {
|
||||
Data interface{} `json:"data"`
|
||||
RowCount int64 `json:"row_count"`
|
||||
Timestamp time.Time `json:"timestamp"`
|
||||
}
|
||||
|
||||
// saveToCache 保存结果到缓存
|
||||
func (p *GormCachePlugin) saveToCache(ctx context.Context, cacheKey string, db *gorm.DB) error {
|
||||
// 检查结果大小限制
|
||||
if db.Statement.RowsAffected > int64(p.config.MaxCacheSize) {
|
||||
p.logger.Debug("查询结果过大,跳过缓存",
|
||||
zap.String("cache_key", cacheKey),
|
||||
zap.Int64("rows", db.Statement.RowsAffected),
|
||||
)
|
||||
return nil
|
||||
}
|
||||
|
||||
// 获取查询结果
|
||||
dest := db.Statement.Dest
|
||||
if dest == nil {
|
||||
return fmt.Errorf("查询结果为空")
|
||||
}
|
||||
|
||||
// 构建缓存结果
|
||||
result := CachedResult{
|
||||
Data: dest,
|
||||
RowCount: db.Statement.RowsAffected,
|
||||
Timestamp: time.Now(),
|
||||
}
|
||||
|
||||
// 获取TTL
|
||||
ttl := p.getTTL(db)
|
||||
|
||||
// 保存到缓存
|
||||
if err := p.cache.Set(ctx, cacheKey, result, ttl); err != nil {
|
||||
return fmt.Errorf("保存到缓存失败: %w", err)
|
||||
}
|
||||
|
||||
p.logger.Debug("查询结果已缓存",
|
||||
zap.String("cache_key", cacheKey),
|
||||
zap.Int64("rows", db.Statement.RowsAffected),
|
||||
zap.Duration("ttl", ttl),
|
||||
)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// restoreFromCache 从缓存恢复结果
|
||||
func (p *GormCachePlugin) restoreFromCache(db *gorm.DB, cachedResult *CachedResult) error {
|
||||
if cachedResult.Data == nil {
|
||||
return fmt.Errorf("缓存数据为空")
|
||||
}
|
||||
|
||||
// 反序列化到目标对象
|
||||
destValue := reflect.ValueOf(db.Statement.Dest)
|
||||
if destValue.Kind() != reflect.Ptr || destValue.IsNil() {
|
||||
return fmt.Errorf("目标对象必须是指针")
|
||||
}
|
||||
|
||||
// 将缓存数据复制到目标
|
||||
cachedValue := reflect.ValueOf(cachedResult.Data)
|
||||
if !cachedValue.Type().AssignableTo(destValue.Elem().Type()) {
|
||||
// 尝试JSON转换
|
||||
jsonData, err := json.Marshal(cachedResult.Data)
|
||||
if err != nil {
|
||||
return fmt.Errorf("缓存数据类型不匹配")
|
||||
}
|
||||
|
||||
if err := json.Unmarshal(jsonData, db.Statement.Dest); err != nil {
|
||||
return fmt.Errorf("JSON反序列化失败: %w", err)
|
||||
}
|
||||
} else {
|
||||
destValue.Elem().Set(cachedValue)
|
||||
}
|
||||
|
||||
// 设置影响行数
|
||||
db.Statement.RowsAffected = cachedResult.RowCount
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// getTTL 获取TTL
|
||||
func (p *GormCachePlugin) getTTL(db *gorm.DB) time.Duration {
|
||||
// 检查是否设置了自定义TTL
|
||||
if value, ok := db.Statement.Get("cache:ttl"); ok {
|
||||
if ttl, ok := value.(time.Duration); ok {
|
||||
return ttl
|
||||
}
|
||||
}
|
||||
|
||||
return p.config.DefaultTTL
|
||||
}
|
||||
|
||||
// invalidateTableCache 失效表相关缓存
|
||||
func (p *GormCachePlugin) invalidateTableCache(ctx context.Context, table string) {
|
||||
if ctx == nil {
|
||||
ctx = context.Background()
|
||||
}
|
||||
|
||||
// 延迟失效(避免并发问题)
|
||||
if p.config.InvalidateDelay > 0 {
|
||||
time.AfterFunc(p.config.InvalidateDelay, func() {
|
||||
p.doInvalidateTableCache(ctx, table)
|
||||
})
|
||||
} else {
|
||||
p.doInvalidateTableCache(ctx, table)
|
||||
}
|
||||
}
|
||||
|
||||
// doInvalidateTableCache 执行缓存失效
|
||||
func (p *GormCachePlugin) doInvalidateTableCache(ctx context.Context, table string) {
|
||||
pattern := fmt.Sprintf("%s:%s:*", p.config.TablePrefix, table)
|
||||
|
||||
if err := p.cache.DeletePattern(ctx, pattern); err != nil {
|
||||
p.logger.Warn("失效表缓存失败",
|
||||
zap.String("table", table),
|
||||
zap.String("pattern", pattern),
|
||||
zap.Error(err),
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
p.logger.Debug("表缓存已失效",
|
||||
zap.String("table", table),
|
||||
zap.String("pattern", pattern),
|
||||
)
|
||||
}
|
||||
|
||||
// updateStats 更新统计信息
|
||||
func (p *GormCachePlugin) updateStats(operation, table string) {
|
||||
// 这里可以接入Prometheus等监控系统
|
||||
p.logger.Debug("缓存统计",
|
||||
zap.String("operation", operation),
|
||||
zap.String("table", table),
|
||||
)
|
||||
}
|
||||
|
||||
// ================ 高级功能 ================
|
||||
|
||||
// WarmupCache 预热缓存
|
||||
func (p *GormCachePlugin) WarmupCache(ctx context.Context, db *gorm.DB, queries []string) error {
|
||||
if !p.config.EnableWarmup {
|
||||
return fmt.Errorf("缓存预热未启用")
|
||||
}
|
||||
|
||||
for _, query := range queries {
|
||||
if err := db.Raw(query).Error; err != nil {
|
||||
p.logger.Warn("缓存预热失败",
|
||||
zap.String("query", query),
|
||||
zap.Error(err),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetCacheStats 获取缓存统计
|
||||
func (p *GormCachePlugin) GetCacheStats(ctx context.Context) (map[string]interface{}, error) {
|
||||
stats, err := p.cache.Stats(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return map[string]interface{}{
|
||||
"hits": stats.Hits,
|
||||
"misses": stats.Misses,
|
||||
"keys": stats.Keys,
|
||||
"memory": stats.Memory,
|
||||
"connections": stats.Connections,
|
||||
"config": p.config,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// SetCacheEnabled 设置缓存启用状态
|
||||
func (p *GormCachePlugin) SetCacheEnabled(db *gorm.DB, enabled bool) *gorm.DB {
|
||||
return db.Set("cache:enabled", enabled)
|
||||
}
|
||||
|
||||
// SetCacheDisabled 设置缓存禁用状态
|
||||
func (p *GormCachePlugin) SetCacheDisabled(db *gorm.DB, disabled bool) *gorm.DB {
|
||||
return db.Set("cache:disabled", disabled)
|
||||
}
|
||||
|
||||
// SetCacheTTL 设置缓存TTL
|
||||
func (p *GormCachePlugin) SetCacheTTL(db *gorm.DB, ttl time.Duration) *gorm.DB {
|
||||
return db.Set("cache:ttl", ttl)
|
||||
}
|
||||
246
internal/shared/database/base_repository.go
Normal file
246
internal/shared/database/base_repository.go
Normal file
@@ -0,0 +1,246 @@
|
||||
package database
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"tyapi-server/internal/shared/interfaces"
|
||||
|
||||
"go.uber.org/zap"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
// BaseRepositoryImpl 基础仓储实现
|
||||
// 提供统一的数据库连接、事务处理和通用辅助方法
|
||||
type BaseRepositoryImpl struct {
|
||||
db *gorm.DB
|
||||
logger *zap.Logger
|
||||
}
|
||||
|
||||
// NewBaseRepositoryImpl 创建基础仓储实现
|
||||
func NewBaseRepositoryImpl(db *gorm.DB, logger *zap.Logger) *BaseRepositoryImpl {
|
||||
return &BaseRepositoryImpl{
|
||||
db: db,
|
||||
logger: logger,
|
||||
}
|
||||
}
|
||||
|
||||
// ================ 核心工具方法 ================
|
||||
|
||||
// GetDB 获取数据库连接,优先使用事务
|
||||
// 这是Repository层统一的数据库连接获取方法
|
||||
func (r *BaseRepositoryImpl) GetDB(ctx context.Context) *gorm.DB {
|
||||
if tx, ok := GetTx(ctx); ok {
|
||||
return tx.WithContext(ctx)
|
||||
}
|
||||
return r.db.WithContext(ctx)
|
||||
}
|
||||
|
||||
// GetLogger 获取日志记录器
|
||||
func (r *BaseRepositoryImpl) GetLogger() *zap.Logger {
|
||||
return r.logger
|
||||
}
|
||||
|
||||
// WithTx 使用事务创建新的Repository实例
|
||||
func (r *BaseRepositoryImpl) WithTx(tx *gorm.DB) *BaseRepositoryImpl {
|
||||
return &BaseRepositoryImpl{
|
||||
db: tx,
|
||||
logger: r.logger,
|
||||
}
|
||||
}
|
||||
|
||||
// ExecuteInTransaction 在事务中执行函数
|
||||
func (r *BaseRepositoryImpl) ExecuteInTransaction(ctx context.Context, fn func(*gorm.DB) error) error {
|
||||
db := r.GetDB(ctx)
|
||||
|
||||
// 如果已经在事务中,直接执行
|
||||
if _, ok := GetTx(ctx); ok {
|
||||
return fn(db)
|
||||
}
|
||||
|
||||
// 否则开启新事务
|
||||
return db.Transaction(fn)
|
||||
}
|
||||
|
||||
// IsInTransaction 检查当前是否在事务中
|
||||
func (r *BaseRepositoryImpl) IsInTransaction(ctx context.Context) bool {
|
||||
_, ok := GetTx(ctx)
|
||||
return ok
|
||||
}
|
||||
|
||||
// ================ 通用查询辅助方法 ================
|
||||
|
||||
// FindWhere 根据条件查找实体列表
|
||||
func (r *BaseRepositoryImpl) FindWhere(ctx context.Context, entities interface{}, condition string, args ...interface{}) error {
|
||||
return r.GetDB(ctx).Where(condition, args...).Find(entities).Error
|
||||
}
|
||||
|
||||
// FindOne 根据条件查找单个实体
|
||||
func (r *BaseRepositoryImpl) FindOne(ctx context.Context, entity interface{}, condition string, args ...interface{}) error {
|
||||
return r.GetDB(ctx).Where(condition, args...).First(entity).Error
|
||||
}
|
||||
|
||||
// CountWhere 根据条件统计数量
|
||||
func (r *BaseRepositoryImpl) CountWhere(ctx context.Context, entity interface{}, condition string, args ...interface{}) (int64, error) {
|
||||
var count int64
|
||||
err := r.GetDB(ctx).Model(entity).Where(condition, args...).Count(&count).Error
|
||||
return count, err
|
||||
}
|
||||
|
||||
// ExistsWhere 根据条件检查是否存在
|
||||
func (r *BaseRepositoryImpl) ExistsWhere(ctx context.Context, entity interface{}, condition string, args ...interface{}) (bool, error) {
|
||||
count, err := r.CountWhere(ctx, entity, condition, args...)
|
||||
return count > 0, err
|
||||
}
|
||||
|
||||
// ================ CRUD辅助方法 ================
|
||||
|
||||
// CreateEntity 创建实体(辅助方法)
|
||||
func (r *BaseRepositoryImpl) CreateEntity(ctx context.Context, entity interface{}) error {
|
||||
return r.GetDB(ctx).Create(entity).Error
|
||||
}
|
||||
|
||||
// GetEntityByID 根据ID获取实体(辅助方法)
|
||||
func (r *BaseRepositoryImpl) GetEntityByID(ctx context.Context, id string, entity interface{}) error {
|
||||
return r.GetDB(ctx).Where("id = ?", id).First(entity).Error
|
||||
}
|
||||
|
||||
// UpdateEntity 更新实体(辅助方法)
|
||||
func (r *BaseRepositoryImpl) UpdateEntity(ctx context.Context, entity interface{}) error {
|
||||
return r.GetDB(ctx).Save(entity).Error
|
||||
}
|
||||
|
||||
// DeleteEntity 删除实体(辅助方法)
|
||||
func (r *BaseRepositoryImpl) DeleteEntity(ctx context.Context, id string, entity interface{}) error {
|
||||
return r.GetDB(ctx).Delete(entity, "id = ?", id).Error
|
||||
}
|
||||
|
||||
// ExistsEntity 检查实体是否存在(辅助方法)
|
||||
func (r *BaseRepositoryImpl) ExistsEntity(ctx context.Context, id string, entity interface{}) (bool, error) {
|
||||
var count int64
|
||||
err := r.GetDB(ctx).Model(entity).Where("id = ?", id).Count(&count).Error
|
||||
return count > 0, err
|
||||
}
|
||||
|
||||
// ================ 批量操作辅助方法 ================
|
||||
|
||||
// CreateBatchEntity 批量创建实体(辅助方法)
|
||||
func (r *BaseRepositoryImpl) CreateBatchEntity(ctx context.Context, entities interface{}) error {
|
||||
return r.GetDB(ctx).Create(entities).Error
|
||||
}
|
||||
|
||||
// GetEntitiesByIDs 根据ID列表获取实体(辅助方法)
|
||||
func (r *BaseRepositoryImpl) GetEntitiesByIDs(ctx context.Context, ids []string, entities interface{}) error {
|
||||
return r.GetDB(ctx).Where("id IN ?", ids).Find(entities).Error
|
||||
}
|
||||
|
||||
// UpdateBatchEntity 批量更新实体(辅助方法)
|
||||
func (r *BaseRepositoryImpl) UpdateBatchEntity(ctx context.Context, entities interface{}) error {
|
||||
return r.GetDB(ctx).Save(entities).Error
|
||||
}
|
||||
|
||||
// DeleteBatchEntity 批量删除实体(辅助方法)
|
||||
func (r *BaseRepositoryImpl) DeleteBatchEntity(ctx context.Context, ids []string, entity interface{}) error {
|
||||
return r.GetDB(ctx).Delete(entity, "id IN ?", ids).Error
|
||||
}
|
||||
|
||||
// ================ 软删除辅助方法 ================
|
||||
|
||||
// SoftDeleteEntity 软删除实体(辅助方法)
|
||||
func (r *BaseRepositoryImpl) SoftDeleteEntity(ctx context.Context, id string, entity interface{}) error {
|
||||
return r.GetDB(ctx).Delete(entity, "id = ?", id).Error
|
||||
}
|
||||
|
||||
// RestoreEntity 恢复软删除的实体(辅助方法)
|
||||
func (r *BaseRepositoryImpl) RestoreEntity(ctx context.Context, id string, entity interface{}) error {
|
||||
return r.GetDB(ctx).Unscoped().Model(entity).Where("id = ?", id).Update("deleted_at", nil).Error
|
||||
}
|
||||
|
||||
// ================ 高级查询辅助方法 ================
|
||||
|
||||
// ListWithOptions 获取实体列表(支持ListOptions,辅助方法)
|
||||
func (r *BaseRepositoryImpl) ListWithOptions(ctx context.Context, entity interface{}, entities interface{}, options interfaces.ListOptions) error {
|
||||
query := r.GetDB(ctx).Model(entity)
|
||||
|
||||
// 应用筛选条件
|
||||
if options.Filters != nil {
|
||||
for key, value := range options.Filters {
|
||||
query = query.Where(key+" = ?", value)
|
||||
}
|
||||
}
|
||||
|
||||
// 应用搜索条件(基础实现,具体Repository应该重写)
|
||||
if options.Search != "" {
|
||||
query = query.Where("name LIKE ? OR description LIKE ?", "%"+options.Search+"%", "%"+options.Search+"%")
|
||||
}
|
||||
|
||||
// 应用预加载
|
||||
for _, include := range options.Include {
|
||||
query = query.Preload(include)
|
||||
}
|
||||
|
||||
// 应用排序
|
||||
if options.Sort != "" {
|
||||
order := "ASC"
|
||||
if options.Order == "desc" || options.Order == "DESC" {
|
||||
order = "DESC"
|
||||
}
|
||||
query = query.Order(options.Sort + " " + order)
|
||||
} else {
|
||||
// 默认按创建时间倒序
|
||||
query = query.Order("created_at DESC")
|
||||
}
|
||||
|
||||
// 应用分页
|
||||
if options.Page > 0 && options.PageSize > 0 {
|
||||
offset := (options.Page - 1) * options.PageSize
|
||||
query = query.Offset(offset).Limit(options.PageSize)
|
||||
}
|
||||
|
||||
return query.Find(entities).Error
|
||||
}
|
||||
|
||||
// CountWithOptions 统计实体数量(支持CountOptions,辅助方法)
|
||||
func (r *BaseRepositoryImpl) CountWithOptions(ctx context.Context, entity interface{}, options interfaces.CountOptions) (int64, error) {
|
||||
var count int64
|
||||
query := r.GetDB(ctx).Model(entity)
|
||||
|
||||
// 应用筛选条件
|
||||
if options.Filters != nil {
|
||||
for key, value := range options.Filters {
|
||||
query = query.Where(key+" = ?", value)
|
||||
}
|
||||
}
|
||||
|
||||
// 应用搜索条件(基础实现,具体Repository应该重写)
|
||||
if options.Search != "" {
|
||||
query = query.Where("name LIKE ? OR description LIKE ?", "%"+options.Search+"%", "%"+options.Search+"%")
|
||||
}
|
||||
|
||||
err := query.Count(&count).Error
|
||||
return count, err
|
||||
}
|
||||
|
||||
// ================ 常用查询模式 ================
|
||||
|
||||
// FindByField 根据单个字段查找实体列表
|
||||
func (r *BaseRepositoryImpl) FindByField(ctx context.Context, entities interface{}, field string, value interface{}) error {
|
||||
return r.GetDB(ctx).Where(field+" = ?", value).Find(entities).Error
|
||||
}
|
||||
|
||||
// FindOneByField 根据单个字段查找单个实体
|
||||
func (r *BaseRepositoryImpl) FindOneByField(ctx context.Context, entity interface{}, field string, value interface{}) error {
|
||||
return r.GetDB(ctx).Where(field+" = ?", value).First(entity).Error
|
||||
}
|
||||
|
||||
// CountByField 根据单个字段统计数量
|
||||
func (r *BaseRepositoryImpl) CountByField(ctx context.Context, entity interface{}, field string, value interface{}) (int64, error) {
|
||||
var count int64
|
||||
err := r.GetDB(ctx).Model(entity).Where(field+" = ?", value).Count(&count).Error
|
||||
return count, err
|
||||
}
|
||||
|
||||
// ExistsByField 根据单个字段检查是否存在
|
||||
func (r *BaseRepositoryImpl) ExistsByField(ctx context.Context, entity interface{}, field string, value interface{}) (bool, error) {
|
||||
count, err := r.CountByField(ctx, entity, field, value)
|
||||
return count > 0, err
|
||||
}
|
||||
367
internal/shared/database/cached_base_repository.go
Normal file
367
internal/shared/database/cached_base_repository.go
Normal file
@@ -0,0 +1,367 @@
|
||||
package database
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"go.uber.org/zap"
|
||||
"gorm.io/gorm"
|
||||
|
||||
"tyapi-server/internal/shared/interfaces"
|
||||
)
|
||||
|
||||
// CachedBaseRepositoryImpl 支持缓存的基础仓储实现
|
||||
// 在BaseRepositoryImpl基础上增加智能缓存管理
|
||||
type CachedBaseRepositoryImpl struct {
|
||||
*BaseRepositoryImpl
|
||||
tableName string
|
||||
}
|
||||
|
||||
// NewCachedBaseRepositoryImpl 创建支持缓存的基础仓储实现
|
||||
func NewCachedBaseRepositoryImpl(db *gorm.DB, logger *zap.Logger, tableName string) *CachedBaseRepositoryImpl {
|
||||
return &CachedBaseRepositoryImpl{
|
||||
BaseRepositoryImpl: NewBaseRepositoryImpl(db, logger),
|
||||
tableName: tableName,
|
||||
}
|
||||
}
|
||||
|
||||
// ================ 智能缓存方法 ================
|
||||
|
||||
// GetWithCache 带缓存的单条查询
|
||||
func (r *CachedBaseRepositoryImpl) GetWithCache(ctx context.Context, dest interface{}, ttl time.Duration, where string, args ...interface{}) error {
|
||||
db := r.GetDB(ctx).
|
||||
Set("cache:enabled", true).
|
||||
Set("cache:ttl", ttl)
|
||||
|
||||
return db.Where(where, args...).First(dest).Error
|
||||
}
|
||||
|
||||
// FindWithCache 带缓存的多条查询
|
||||
func (r *CachedBaseRepositoryImpl) FindWithCache(ctx context.Context, dest interface{}, ttl time.Duration, where string, args ...interface{}) error {
|
||||
db := r.GetDB(ctx).
|
||||
Set("cache:enabled", true).
|
||||
Set("cache:ttl", ttl)
|
||||
|
||||
return db.Where(where, args...).Find(dest).Error
|
||||
}
|
||||
|
||||
// CountWithCache 带缓存的计数查询
|
||||
func (r *CachedBaseRepositoryImpl) CountWithCache(ctx context.Context, count *int64, ttl time.Duration, entity interface{}, where string, args ...interface{}) error {
|
||||
db := r.GetDB(ctx).
|
||||
Set("cache:enabled", true).
|
||||
Set("cache:ttl", ttl).
|
||||
Model(entity)
|
||||
|
||||
return db.Where(where, args...).Count(count).Error
|
||||
}
|
||||
|
||||
// ListWithCache 带缓存的列表查询
|
||||
func (r *CachedBaseRepositoryImpl) ListWithCache(ctx context.Context, dest interface{}, ttl time.Duration, options CacheListOptions) error {
|
||||
db := r.GetDB(ctx).
|
||||
Set("cache:enabled", true).
|
||||
Set("cache:ttl", ttl)
|
||||
|
||||
// 应用where条件
|
||||
if options.Where != "" {
|
||||
db = db.Where(options.Where, options.Args...)
|
||||
}
|
||||
|
||||
// 应用预加载
|
||||
for _, preload := range options.Preloads {
|
||||
db = db.Preload(preload)
|
||||
}
|
||||
|
||||
// 应用排序
|
||||
if options.Order != "" {
|
||||
db = db.Order(options.Order)
|
||||
}
|
||||
|
||||
// 应用分页
|
||||
if options.Limit > 0 {
|
||||
db = db.Limit(options.Limit)
|
||||
}
|
||||
if options.Offset > 0 {
|
||||
db = db.Offset(options.Offset)
|
||||
}
|
||||
|
||||
return db.Find(dest).Error
|
||||
}
|
||||
|
||||
// CacheListOptions 缓存列表查询选项
|
||||
type CacheListOptions struct {
|
||||
Where string `json:"where"`
|
||||
Args []interface{} `json:"args"`
|
||||
Order string `json:"order"`
|
||||
Limit int `json:"limit"`
|
||||
Offset int `json:"offset"`
|
||||
Preloads []string `json:"preloads"`
|
||||
}
|
||||
|
||||
// ================ 缓存控制方法 ================
|
||||
|
||||
// WithCache 启用缓存
|
||||
func (r *CachedBaseRepositoryImpl) WithCache(ttl time.Duration) *CachedBaseRepositoryImpl {
|
||||
// 创建新实例避免状态污染
|
||||
return &CachedBaseRepositoryImpl{
|
||||
BaseRepositoryImpl: &BaseRepositoryImpl{
|
||||
db: r.db.Set("cache:enabled", true).Set("cache:ttl", ttl),
|
||||
logger: r.logger,
|
||||
},
|
||||
tableName: r.tableName,
|
||||
}
|
||||
}
|
||||
|
||||
// WithoutCache 禁用缓存
|
||||
func (r *CachedBaseRepositoryImpl) WithoutCache() *CachedBaseRepositoryImpl {
|
||||
return &CachedBaseRepositoryImpl{
|
||||
BaseRepositoryImpl: &BaseRepositoryImpl{
|
||||
db: r.db.Set("cache:disabled", true),
|
||||
logger: r.logger,
|
||||
},
|
||||
tableName: r.tableName,
|
||||
}
|
||||
}
|
||||
|
||||
// WithShortCache 短期缓存(5分钟)
|
||||
func (r *CachedBaseRepositoryImpl) WithShortCache() *CachedBaseRepositoryImpl {
|
||||
return r.WithCache(5 * time.Minute)
|
||||
}
|
||||
|
||||
// WithMediumCache 中期缓存(30分钟)
|
||||
func (r *CachedBaseRepositoryImpl) WithMediumCache() *CachedBaseRepositoryImpl {
|
||||
return r.WithCache(30 * time.Minute)
|
||||
}
|
||||
|
||||
// WithLongCache 长期缓存(2小时)
|
||||
func (r *CachedBaseRepositoryImpl) WithLongCache() *CachedBaseRepositoryImpl {
|
||||
return r.WithCache(2 * time.Hour)
|
||||
}
|
||||
|
||||
// ================ 智能查询方法 ================
|
||||
|
||||
// SmartGetByID 智能ID查询(自动缓存)
|
||||
func (r *CachedBaseRepositoryImpl) SmartGetByID(ctx context.Context, id string, dest interface{}) error {
|
||||
return r.GetWithCache(ctx, dest, 30*time.Minute, "id = ?", id)
|
||||
}
|
||||
|
||||
// SmartGetByField 智能字段查询(自动缓存)
|
||||
func (r *CachedBaseRepositoryImpl) SmartGetByField(ctx context.Context, dest interface{}, field string, value interface{}, ttl ...time.Duration) error {
|
||||
cacheTTL := 15 * time.Minute
|
||||
if len(ttl) > 0 {
|
||||
cacheTTL = ttl[0]
|
||||
}
|
||||
|
||||
return r.GetWithCache(ctx, dest, cacheTTL, field+" = ?", value)
|
||||
}
|
||||
|
||||
// SmartList 智能列表查询(根据查询复杂度自动选择缓存策略)
|
||||
func (r *CachedBaseRepositoryImpl) SmartList(ctx context.Context, dest interface{}, options interfaces.ListOptions) error {
|
||||
// 根据查询复杂度决定缓存策略
|
||||
cacheTTL := r.calculateCacheTTL(options)
|
||||
useCache := r.shouldUseCache(options)
|
||||
|
||||
db := r.GetDB(ctx)
|
||||
if useCache {
|
||||
db = db.Set("cache:enabled", true).Set("cache:ttl", cacheTTL)
|
||||
} else {
|
||||
db = db.Set("cache:disabled", true)
|
||||
}
|
||||
|
||||
// 应用筛选条件
|
||||
if options.Filters != nil {
|
||||
for key, value := range options.Filters {
|
||||
db = db.Where(key+" = ?", value)
|
||||
}
|
||||
}
|
||||
|
||||
// 应用搜索条件
|
||||
if options.Search != "" {
|
||||
// 这里应该由具体Repository实现搜索逻辑
|
||||
r.logger.Debug("搜索查询默认禁用缓存", zap.String("search", options.Search))
|
||||
db = db.Set("cache:disabled", true)
|
||||
}
|
||||
|
||||
// 应用预加载
|
||||
for _, include := range options.Include {
|
||||
db = db.Preload(include)
|
||||
}
|
||||
|
||||
// 应用排序
|
||||
if options.Sort != "" {
|
||||
order := "ASC"
|
||||
if options.Order == "desc" || options.Order == "DESC" {
|
||||
order = "DESC"
|
||||
}
|
||||
db = db.Order(options.Sort + " " + order)
|
||||
} else {
|
||||
db = db.Order("created_at DESC")
|
||||
}
|
||||
|
||||
// 应用分页
|
||||
if options.Page > 0 && options.PageSize > 0 {
|
||||
offset := (options.Page - 1) * options.PageSize
|
||||
db = db.Offset(offset).Limit(options.PageSize)
|
||||
}
|
||||
|
||||
return db.Find(dest).Error
|
||||
}
|
||||
|
||||
// calculateCacheTTL 计算缓存TTL
|
||||
func (r *CachedBaseRepositoryImpl) calculateCacheTTL(options interfaces.ListOptions) time.Duration {
|
||||
// 基础TTL
|
||||
baseTTL := 15 * time.Minute
|
||||
|
||||
// 如果有搜索,缩短TTL
|
||||
if options.Search != "" {
|
||||
return 2 * time.Minute
|
||||
}
|
||||
|
||||
// 如果有复杂筛选,缩短TTL
|
||||
if len(options.Filters) > 3 {
|
||||
return 5 * time.Minute
|
||||
}
|
||||
|
||||
// 如果是简单查询,延长TTL
|
||||
if len(options.Filters) == 0 && options.Search == "" {
|
||||
return 30 * time.Minute
|
||||
}
|
||||
|
||||
return baseTTL
|
||||
}
|
||||
|
||||
// shouldUseCache 判断是否应该使用缓存
|
||||
func (r *CachedBaseRepositoryImpl) shouldUseCache(options interfaces.ListOptions) bool {
|
||||
// 如果有搜索,不使用缓存(搜索结果变化频繁)
|
||||
if options.Search != "" {
|
||||
return false
|
||||
}
|
||||
|
||||
// 如果筛选条件过多,不使用缓存
|
||||
if len(options.Filters) > 5 {
|
||||
return false
|
||||
}
|
||||
|
||||
// 如果分页页数过大,不使用缓存
|
||||
if options.Page > 10 {
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
// ================ 缓存预热方法 ================
|
||||
|
||||
// WarmupCommonQueries 预热常用查询
|
||||
func (r *CachedBaseRepositoryImpl) WarmupCommonQueries(ctx context.Context, queries []WarmupQuery) error {
|
||||
r.logger.Info("开始预热缓存",
|
||||
zap.String("table", r.tableName),
|
||||
zap.Int("queries", len(queries)),
|
||||
)
|
||||
|
||||
for _, query := range queries {
|
||||
if err := r.executeWarmupQuery(ctx, query); err != nil {
|
||||
r.logger.Warn("缓存预热失败",
|
||||
zap.String("query", query.Name),
|
||||
zap.Error(err),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// WarmupQuery 预热查询定义
|
||||
type WarmupQuery struct {
|
||||
Name string `json:"name"`
|
||||
SQL string `json:"sql"`
|
||||
Args []interface{} `json:"args"`
|
||||
TTL time.Duration `json:"ttl"`
|
||||
Dest interface{} `json:"dest"`
|
||||
}
|
||||
|
||||
// executeWarmupQuery 执行预热查询
|
||||
func (r *CachedBaseRepositoryImpl) executeWarmupQuery(ctx context.Context, query WarmupQuery) error {
|
||||
db := r.GetDB(ctx).
|
||||
Set("cache:enabled", true).
|
||||
Set("cache:ttl", query.TTL)
|
||||
|
||||
if query.SQL != "" {
|
||||
return db.Raw(query.SQL, query.Args...).Scan(query.Dest).Error
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// ================ 高级缓存特性 ================
|
||||
|
||||
// GetOrCreate 获取或创建(带缓存)
|
||||
func (r *CachedBaseRepositoryImpl) GetOrCreate(ctx context.Context, dest interface{}, where string, args []interface{}, createFn func() interface{}) error {
|
||||
// 先尝试从缓存获取
|
||||
if err := r.GetWithCache(ctx, dest, 15*time.Minute, where, args...); err == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
// 缓存未命中,尝试从数据库获取
|
||||
if err := r.GetDB(ctx).Where(where, args...).First(dest).Error; err == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
// 数据库也没有,创建新记录
|
||||
if createFn != nil {
|
||||
newEntity := createFn()
|
||||
if err := r.CreateEntity(ctx, newEntity); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// 将新创建的实体复制到dest
|
||||
// 这里需要反射或其他方式复制
|
||||
return nil
|
||||
}
|
||||
|
||||
return gorm.ErrRecordNotFound
|
||||
}
|
||||
|
||||
// BatchGetWithCache 批量获取(带缓存)
|
||||
func (r *CachedBaseRepositoryImpl) BatchGetWithCache(ctx context.Context, ids []string, dest interface{}, ttl time.Duration) error {
|
||||
if len(ids) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
return r.FindWithCache(ctx, dest, ttl, "id IN ?", ids)
|
||||
}
|
||||
|
||||
// RefreshCache 刷新缓存
|
||||
func (r *CachedBaseRepositoryImpl) RefreshCache(ctx context.Context, pattern string) error {
|
||||
r.logger.Info("刷新缓存",
|
||||
zap.String("table", r.tableName),
|
||||
zap.String("pattern", pattern),
|
||||
)
|
||||
|
||||
// 这里需要调用缓存服务的删除模式方法
|
||||
// 具体实现取决于你的CacheService接口
|
||||
return nil
|
||||
}
|
||||
|
||||
// ================ 缓存统计方法 ================
|
||||
|
||||
// GetCacheInfo 获取缓存信息
|
||||
func (r *CachedBaseRepositoryImpl) GetCacheInfo() map[string]interface{} {
|
||||
return map[string]interface{}{
|
||||
"table_name": r.tableName,
|
||||
"cache_enabled": true,
|
||||
"default_ttl": "30m",
|
||||
"cache_patterns": []string{
|
||||
fmt.Sprintf("gorm_cache:%s:*", r.tableName),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// LogCacheOperation 记录缓存操作
|
||||
func (r *CachedBaseRepositoryImpl) LogCacheOperation(operation, details string) {
|
||||
r.logger.Debug("缓存操作",
|
||||
zap.String("table", r.tableName),
|
||||
zap.String("operation", operation),
|
||||
zap.String("details", details),
|
||||
)
|
||||
}
|
||||
301
internal/shared/database/transaction.go
Normal file
301
internal/shared/database/transaction.go
Normal file
@@ -0,0 +1,301 @@
|
||||
package database
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"go.uber.org/zap"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
// 自定义错误类型
|
||||
var (
|
||||
ErrTransactionRollback = errors.New("事务回滚失败")
|
||||
ErrTransactionCommit = errors.New("事务提交失败")
|
||||
)
|
||||
|
||||
// 定义context key
|
||||
type txKey struct{}
|
||||
|
||||
// WithTx 将事务对象存储到context中
|
||||
func WithTx(ctx context.Context, tx *gorm.DB) context.Context {
|
||||
return context.WithValue(ctx, txKey{}, tx)
|
||||
}
|
||||
|
||||
// GetTx 从context中获取事务对象
|
||||
func GetTx(ctx context.Context) (*gorm.DB, bool) {
|
||||
tx, ok := ctx.Value(txKey{}).(*gorm.DB)
|
||||
return tx, ok
|
||||
}
|
||||
|
||||
// TransactionManager 事务管理器
|
||||
type TransactionManager struct {
|
||||
db *gorm.DB
|
||||
logger *zap.Logger
|
||||
}
|
||||
|
||||
// NewTransactionManager 创建事务管理器
|
||||
func NewTransactionManager(db *gorm.DB, logger *zap.Logger) *TransactionManager {
|
||||
return &TransactionManager{
|
||||
db: db,
|
||||
logger: logger,
|
||||
}
|
||||
}
|
||||
|
||||
// ExecuteInTx 在事务中执行函数(推荐使用)
|
||||
// 自动处理事务的开启、提交和回滚
|
||||
func (tm *TransactionManager) ExecuteInTx(ctx context.Context, fn func(context.Context) error) error {
|
||||
// 检查是否已经在事务中
|
||||
if _, ok := GetTx(ctx); ok {
|
||||
// 如果已经在事务中,直接执行函数,避免嵌套事务
|
||||
return fn(ctx)
|
||||
}
|
||||
|
||||
tx := tm.db.Begin()
|
||||
if tx.Error != nil {
|
||||
return tx.Error
|
||||
}
|
||||
|
||||
// 创建带事务的context
|
||||
txCtx := WithTx(ctx, tx)
|
||||
|
||||
// 执行函数
|
||||
if err := fn(txCtx); err != nil {
|
||||
// 回滚事务
|
||||
if rbErr := tx.Rollback().Error; rbErr != nil {
|
||||
tm.logger.Error("事务回滚失败",
|
||||
zap.Error(err),
|
||||
zap.Error(rbErr),
|
||||
)
|
||||
return errors.Join(err, ErrTransactionRollback, rbErr)
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
// 提交事务
|
||||
if err := tx.Commit().Error; err != nil {
|
||||
tm.logger.Error("事务提交失败", zap.Error(err))
|
||||
return errors.Join(ErrTransactionCommit, err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// ExecuteInTxWithTimeout 在事务中执行函数(带超时)
|
||||
func (tm *TransactionManager) ExecuteInTxWithTimeout(ctx context.Context, timeout time.Duration, fn func(context.Context) error) error {
|
||||
ctx, cancel := context.WithTimeout(ctx, timeout)
|
||||
defer cancel()
|
||||
|
||||
return tm.ExecuteInTx(ctx, fn)
|
||||
}
|
||||
|
||||
// BeginTx 开始事务(手动管理)
|
||||
func (tm *TransactionManager) BeginTx() *gorm.DB {
|
||||
return tm.db.Begin()
|
||||
}
|
||||
|
||||
// TxWrapper 事务包装器(手动管理)
|
||||
type TxWrapper struct {
|
||||
tx *gorm.DB
|
||||
}
|
||||
|
||||
// NewTxWrapper 创建事务包装器
|
||||
func (tm *TransactionManager) NewTxWrapper() *TxWrapper {
|
||||
return &TxWrapper{
|
||||
tx: tm.BeginTx(),
|
||||
}
|
||||
}
|
||||
|
||||
// Commit 提交事务
|
||||
func (tx *TxWrapper) Commit() error {
|
||||
return tx.tx.Commit().Error
|
||||
}
|
||||
|
||||
// Rollback 回滚事务
|
||||
func (tx *TxWrapper) Rollback() error {
|
||||
return tx.tx.Rollback().Error
|
||||
}
|
||||
|
||||
// GetDB 获取事务数据库实例
|
||||
func (tx *TxWrapper) GetDB() *gorm.DB {
|
||||
return tx.tx
|
||||
}
|
||||
|
||||
// WithTx 在事务中执行函数(兼容旧接口)
|
||||
func (tm *TransactionManager) WithTx(fn func(*gorm.DB) error) error {
|
||||
tx := tm.BeginTx()
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
tx.Rollback()
|
||||
panic(r)
|
||||
}
|
||||
}()
|
||||
|
||||
if err := fn(tx); err != nil {
|
||||
tx.Rollback()
|
||||
return err
|
||||
}
|
||||
|
||||
return tx.Commit().Error
|
||||
}
|
||||
|
||||
// TransactionOptions 事务选项
|
||||
type TransactionOptions struct {
|
||||
Timeout time.Duration
|
||||
ReadOnly bool // 是否只读事务
|
||||
}
|
||||
|
||||
// ExecuteInTxWithOptions 在事务中执行函数(带选项)
|
||||
func (tm *TransactionManager) ExecuteInTxWithOptions(ctx context.Context, options *TransactionOptions, fn func(context.Context) error) error {
|
||||
// 设置事务选项
|
||||
tx := tm.db.Begin()
|
||||
if tx.Error != nil {
|
||||
return tx.Error
|
||||
}
|
||||
|
||||
// 设置只读事务
|
||||
if options != nil && options.ReadOnly {
|
||||
tx = tx.Session(&gorm.Session{})
|
||||
// 注意:GORM的只读事务需要数据库支持,这里只是标记
|
||||
}
|
||||
|
||||
// 创建带事务的context
|
||||
txCtx := WithTx(ctx, tx)
|
||||
|
||||
// 设置超时
|
||||
if options != nil && options.Timeout > 0 {
|
||||
var cancel context.CancelFunc
|
||||
txCtx, cancel = context.WithTimeout(txCtx, options.Timeout)
|
||||
defer cancel()
|
||||
}
|
||||
|
||||
// 执行函数
|
||||
if err := fn(txCtx); err != nil {
|
||||
// 回滚事务
|
||||
if rbErr := tx.Rollback().Error; rbErr != nil {
|
||||
return err
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
// 提交事务
|
||||
return tx.Commit().Error
|
||||
}
|
||||
|
||||
// TransactionStats 事务统计信息
|
||||
type TransactionStats struct {
|
||||
TotalTransactions int64
|
||||
SuccessfulTransactions int64
|
||||
FailedTransactions int64
|
||||
AverageDuration time.Duration
|
||||
}
|
||||
|
||||
// GetStats 获取事务统计信息(预留接口)
|
||||
func (tm *TransactionManager) GetStats() *TransactionStats {
|
||||
// TODO: 实现事务统计
|
||||
return &TransactionStats{}
|
||||
}
|
||||
|
||||
// RetryableTransactionOptions 可重试事务选项
|
||||
type RetryableTransactionOptions struct {
|
||||
MaxRetries int // 最大重试次数
|
||||
RetryDelay time.Duration // 重试延迟
|
||||
RetryBackoff float64 // 退避倍数
|
||||
}
|
||||
|
||||
// DefaultRetryableOptions 默认重试选项
|
||||
func DefaultRetryableOptions() *RetryableTransactionOptions {
|
||||
return &RetryableTransactionOptions{
|
||||
MaxRetries: 3,
|
||||
RetryDelay: 100 * time.Millisecond,
|
||||
RetryBackoff: 2.0,
|
||||
}
|
||||
}
|
||||
|
||||
// ExecuteInTxWithRetry 在事务中执行函数(支持重试)
|
||||
// 适用于处理死锁等临时性错误
|
||||
func (tm *TransactionManager) ExecuteInTxWithRetry(ctx context.Context, options *RetryableTransactionOptions, fn func(context.Context) error) error {
|
||||
if options == nil {
|
||||
options = DefaultRetryableOptions()
|
||||
}
|
||||
|
||||
var lastErr error
|
||||
delay := options.RetryDelay
|
||||
|
||||
for attempt := 0; attempt <= options.MaxRetries; attempt++ {
|
||||
// 检查上下文是否已取消
|
||||
if ctx.Err() != nil {
|
||||
return ctx.Err()
|
||||
}
|
||||
|
||||
err := tm.ExecuteInTx(ctx, fn)
|
||||
if err == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
// 检查是否是可重试的错误(死锁、连接错误等)
|
||||
if !isRetryableError(err) {
|
||||
return err
|
||||
}
|
||||
|
||||
lastErr = err
|
||||
|
||||
// 如果不是最后一次尝试,等待后重试
|
||||
if attempt < options.MaxRetries {
|
||||
tm.logger.Warn("事务执行失败,准备重试",
|
||||
zap.Int("attempt", attempt+1),
|
||||
zap.Int("max_retries", options.MaxRetries),
|
||||
zap.Duration("delay", delay),
|
||||
zap.Error(err),
|
||||
)
|
||||
|
||||
select {
|
||||
case <-time.After(delay):
|
||||
delay = time.Duration(float64(delay) * options.RetryBackoff)
|
||||
case <-ctx.Done():
|
||||
return ctx.Err()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
tm.logger.Error("事务执行失败,已超过最大重试次数",
|
||||
zap.Int("max_retries", options.MaxRetries),
|
||||
zap.Error(lastErr),
|
||||
)
|
||||
|
||||
return lastErr
|
||||
}
|
||||
|
||||
// isRetryableError 判断是否是可重试的错误
|
||||
func isRetryableError(err error) bool {
|
||||
if err == nil {
|
||||
return false
|
||||
}
|
||||
|
||||
errStr := err.Error()
|
||||
|
||||
// MySQL 死锁错误
|
||||
if contains(errStr, "Deadlock found") {
|
||||
return true
|
||||
}
|
||||
|
||||
// MySQL 锁等待超时
|
||||
if contains(errStr, "Lock wait timeout exceeded") {
|
||||
return true
|
||||
}
|
||||
|
||||
// 连接错误
|
||||
if contains(errStr, "connection") {
|
||||
return true
|
||||
}
|
||||
|
||||
// 可以根据需要添加更多的可重试错误类型
|
||||
return false
|
||||
}
|
||||
|
||||
// contains 检查字符串是否包含子字符串(不区分大小写)
|
||||
func contains(s, substr string) bool {
|
||||
return strings.Contains(strings.ToLower(s), strings.ToLower(substr))
|
||||
}
|
||||
293
internal/shared/esign/README.md
Normal file
293
internal/shared/esign/README.md
Normal file
@@ -0,0 +1,293 @@
|
||||
# e签宝 SDK - 重构版本
|
||||
|
||||
这是重构后的e签宝Go SDK,提供了更清晰、更易用的API接口。
|
||||
|
||||
## 架构设计
|
||||
|
||||
### 主要组件
|
||||
|
||||
1. **Client (client.go)** - 统一的客户端入口
|
||||
2. **Config (config.go)** - 配置管理
|
||||
3. **HTTPClient (http.go)** - HTTP请求处理
|
||||
4. **服务模块**:
|
||||
- **TemplateService** - 模板操作服务
|
||||
- **SignFlowService** - 签署流程服务
|
||||
- **OrgAuthService** - 机构认证服务
|
||||
- **FileOpsService** - 文件操作服务
|
||||
|
||||
### 设计特点
|
||||
|
||||
- ✅ **模块化设计**:功能按模块分离,职责清晰
|
||||
- ✅ **统一入口**:通过Client提供统一的API
|
||||
- ✅ **易于使用**:提供高级业务接口和底层操作接口
|
||||
- ✅ **配置管理**:集中的配置验证和管理
|
||||
- ✅ **错误处理**:统一的错误处理和响应验证
|
||||
- ✅ **类型安全**:完整的类型定义和结构体
|
||||
|
||||
## 快速开始
|
||||
|
||||
### 1. 创建客户端
|
||||
|
||||
```go
|
||||
package main
|
||||
|
||||
import (
|
||||
"github.com/your-org/tyapi-server-gin/internal/shared/esign"
|
||||
)
|
||||
|
||||
func main() {
|
||||
// 创建配置
|
||||
config, err := esign.NewConfig(
|
||||
"your_app_id",
|
||||
"your_app_secret",
|
||||
"https://smlopenapi.esign.cn",
|
||||
"your_template_id",
|
||||
)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
// 创建客户端
|
||||
client := esign.NewClient(config)
|
||||
}
|
||||
```
|
||||
|
||||
### 2. 基础用法 - 一键合同签署
|
||||
|
||||
```go
|
||||
// 最简单的合同签署
|
||||
result, err := client.GenerateContractSigning(&esign.ContractSigningRequest{
|
||||
CompanyName: "我的公司",
|
||||
UnifiedSocialCode: "123456789012345678",
|
||||
LegalPersonName: "张三",
|
||||
LegalPersonID: "123456789012345678",
|
||||
LegalPersonPhone: "13800138000",
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
log.Fatal("签署失败:", err)
|
||||
}
|
||||
|
||||
fmt.Printf("请访问链接进行签署: %s\n", result.SignURL)
|
||||
```
|
||||
|
||||
### 3. 企业认证
|
||||
|
||||
```go
|
||||
// 企业认证
|
||||
authResult, err := client.GenerateEnterpriseAuth(&esign.EnterpriseAuthRequest{
|
||||
CompanyName: "我的公司",
|
||||
UnifiedSocialCode: "123456789012345678",
|
||||
LegalPersonName: "张三",
|
||||
LegalPersonID: "123456789012345678",
|
||||
TransactorName: "李四",
|
||||
TransactorPhone: "13800138001",
|
||||
TransactorID: "123456789012345679",
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
log.Fatal("企业认证失败:", err)
|
||||
}
|
||||
|
||||
fmt.Printf("请访问链接进行企业认证: %s\n", authResult.AuthURL)
|
||||
```
|
||||
|
||||
## 高级用法
|
||||
|
||||
### 分步操作
|
||||
|
||||
如果需要更精细的控制,可以使用分步操作:
|
||||
|
||||
```go
|
||||
// 1. 填写模板
|
||||
templateData := map[string]string{
|
||||
"JFQY": "甲方公司",
|
||||
"JFFR": "甲方法人",
|
||||
"YFQY": "乙方公司",
|
||||
"YFFR": "乙方法人",
|
||||
"QDRQ": "2024年01月01日",
|
||||
}
|
||||
|
||||
fileID, err := client.FillTemplate(templateData)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// 2. 创建签署流程
|
||||
signFlowReq := &esign.CreateSignFlowRequest{
|
||||
FileID: fileID,
|
||||
SignerAccount: "123456789012345678",
|
||||
SignerName: "乙方公司",
|
||||
TransactorPhone: "13800138000",
|
||||
TransactorName: "乙方法人",
|
||||
TransactorIDCardNum: "123456789012345678",
|
||||
TransactorMobile: "13800138000",
|
||||
}
|
||||
|
||||
signFlowID, err := client.CreateSignFlow(signFlowReq)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// 3. 获取签署链接
|
||||
signURL, shortURL, err := client.GetSignURL(signFlowID, "13800138000", "乙方公司")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// 4. 查询签署状态
|
||||
status, err := client.GetSignFlowStatus(signFlowID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// 5. 检查是否完成
|
||||
completed, err := client.IsSignFlowCompleted(signFlowID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
```
|
||||
|
||||
### 自定义模板数据
|
||||
|
||||
```go
|
||||
customData := map[string]string{
|
||||
"custom_field_1": "自定义值1",
|
||||
"custom_field_2": "自定义值2",
|
||||
"contract_date": "2024年01月01日",
|
||||
}
|
||||
|
||||
result, err := client.GenerateContractSigning(&esign.ContractSigningRequest{
|
||||
CompanyName: "我的公司",
|
||||
UnifiedSocialCode: "123456789012345678",
|
||||
LegalPersonName: "张三",
|
||||
LegalPersonID: "123456789012345678",
|
||||
LegalPersonPhone: "13800138000",
|
||||
CustomData: customData, // 使用自定义数据
|
||||
})
|
||||
```
|
||||
|
||||
## API参考
|
||||
|
||||
### 主要接口
|
||||
|
||||
#### 合同签署
|
||||
- `GenerateContractSigning(req *ContractSigningRequest) (*ContractSigningResult, error)` - 一键生成合同签署
|
||||
|
||||
#### 企业认证
|
||||
- `GenerateEnterpriseAuth(req *EnterpriseAuthRequest) (*EnterpriseAuthResult, error)` - 一键企业认证
|
||||
|
||||
#### 模板操作
|
||||
- `FillTemplate(components map[string]string) (string, error)` - 填写模板
|
||||
- `FillTemplateWithDefaults(partyA, legalRepA, partyB, legalRepB string) (string, error)` - 使用默认数据填写模板
|
||||
|
||||
#### 签署流程
|
||||
- `CreateSignFlow(req *CreateSignFlowRequest) (string, error)` - 创建签署流程
|
||||
- `GetSignURL(signFlowID, psnAccount, orgName string) (string, string, error)` - 获取签署链接
|
||||
- `QuerySignFlowDetail(signFlowID string) (*QuerySignFlowDetailResponse, error)` - 查询流程详情
|
||||
- `IsSignFlowCompleted(signFlowID string) (bool, error)` - 检查是否完成
|
||||
|
||||
#### 机构认证
|
||||
- `GetOrgAuthURL(req *OrgAuthRequest) (string, string, string, error)` - 获取认证链接
|
||||
- `ValidateOrgAuthInfo(req *OrgAuthRequest) error` - 验证认证信息
|
||||
|
||||
#### 文件操作
|
||||
- `DownloadSignedFile(signFlowID string) (*DownloadSignedFileResponse, error)` - 下载已签署文件
|
||||
- `GetSignFlowStatus(signFlowID string) (string, error)` - 获取流程状态
|
||||
|
||||
## 配置管理
|
||||
|
||||
### 配置结构
|
||||
|
||||
```go
|
||||
type Config struct {
|
||||
AppID string `json:"app_id"` // 应用ID
|
||||
AppSecret string `json:"app_secret"` // 应用密钥
|
||||
ServerURL string `json:"server_url"` // 服务器URL
|
||||
TemplateID string `json:"template_id"` // 模板ID
|
||||
}
|
||||
```
|
||||
|
||||
### 配置验证
|
||||
|
||||
SDK会自动验证配置的完整性:
|
||||
|
||||
```go
|
||||
config, err := esign.NewConfig("", "", "", "")
|
||||
// 返回错误:应用ID不能为空
|
||||
|
||||
// 手动验证
|
||||
err := config.Validate()
|
||||
```
|
||||
|
||||
## 错误处理
|
||||
|
||||
SDK提供统一的错误处理:
|
||||
|
||||
```go
|
||||
result, err := client.GenerateContractSigning(req)
|
||||
if err != nil {
|
||||
// 错误包含详细的错误信息
|
||||
log.Printf("签署失败: %v", err)
|
||||
return
|
||||
}
|
||||
```
|
||||
|
||||
## 迁移指南
|
||||
|
||||
### 从旧版本迁移
|
||||
|
||||
旧版本:
|
||||
```go
|
||||
service := service.NewEQService(config)
|
||||
result, err := service.ExecuteSignProcess(req)
|
||||
```
|
||||
|
||||
新版本:
|
||||
```go
|
||||
client := esign.NewClient(config)
|
||||
result, err := client.GenerateContractSigning(req)
|
||||
```
|
||||
|
||||
### 主要变化
|
||||
|
||||
1. **包名变更**:`service` → `esign`
|
||||
2. **入口简化**:`EQService` → `Client`
|
||||
3. **方法重命名**:更语义化的方法名
|
||||
4. **结构重组**:按功能模块划分
|
||||
5. **类型优化**:更简洁的请求/响应结构
|
||||
|
||||
## 示例代码
|
||||
|
||||
完整的示例代码请参考 `example.go` 文件。
|
||||
|
||||
## 注意事项
|
||||
|
||||
1. **配置安全**:请妥善保管AppID和AppSecret
|
||||
2. **网络超时**:默认HTTP超时为30秒
|
||||
3. **并发安全**:Client实例是并发安全的
|
||||
4. **错误重试**:建议实现适当的重试机制
|
||||
5. **日志记录**:SDK会输出调试信息,生产环境请注意日志级别
|
||||
|
||||
## 常见问题
|
||||
|
||||
### Q: 如何更新配置?
|
||||
```go
|
||||
newConfig, _ := esign.NewConfig("new_app_id", "new_secret", "new_url", "new_template")
|
||||
client.UpdateConfig(newConfig)
|
||||
```
|
||||
|
||||
### Q: 如何处理网络错误?
|
||||
```go
|
||||
result, err := client.GenerateContractSigning(req)
|
||||
if err != nil {
|
||||
if strings.Contains(err.Error(), "timeout") {
|
||||
// 处理超时
|
||||
} else if strings.Contains(err.Error(), "API调用失败") {
|
||||
// 处理API错误
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Q: 如何自定义HTTP客户端?
|
||||
当前版本使用内置的HTTP客户端,如需自定义,可以修改`http.go`中的客户端配置。
|
||||
269
internal/shared/esign/client.go
Normal file
269
internal/shared/esign/client.go
Normal file
@@ -0,0 +1,269 @@
|
||||
package esign
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
)
|
||||
|
||||
// Client e签宝客户端
|
||||
// 提供统一的e签宝服务接口,整合所有功能模块
|
||||
type Client struct {
|
||||
config *Config // 配置信息
|
||||
httpClient *HTTPClient // HTTP客户端
|
||||
template *TemplateService // 模板服务
|
||||
signFlow *SignFlowService // 签署流程服务
|
||||
orgAuth *OrgAuthService // 机构认证服务
|
||||
fileOps *FileOpsService // 文件操作服务
|
||||
}
|
||||
|
||||
// NewClient 创建e签宝客户端
|
||||
// 使用配置信息初始化客户端及所有服务模块
|
||||
//
|
||||
// 参数:
|
||||
// - config: e签宝配置信息
|
||||
//
|
||||
// 返回: 客户端实例
|
||||
func NewClient(config *Config) *Client {
|
||||
httpClient := NewHTTPClient(config)
|
||||
|
||||
client := &Client{
|
||||
config: config,
|
||||
httpClient: httpClient,
|
||||
}
|
||||
|
||||
// 初始化各个服务模块
|
||||
client.template = NewTemplateService(httpClient, config)
|
||||
client.signFlow = NewSignFlowService(httpClient, config)
|
||||
client.orgAuth = NewOrgAuthService(httpClient, config)
|
||||
client.fileOps = NewFileOpsService(httpClient, config)
|
||||
|
||||
return client
|
||||
}
|
||||
|
||||
// GetConfig 获取当前配置
|
||||
func (c *Client) GetConfig() *Config {
|
||||
return c.config
|
||||
}
|
||||
|
||||
// UpdateConfig 更新配置
|
||||
func (c *Client) UpdateConfig(config *Config) {
|
||||
c.config = config
|
||||
c.httpClient.UpdateConfig(config)
|
||||
|
||||
// 更新各服务模块的配置
|
||||
c.template.UpdateConfig(config)
|
||||
c.signFlow.UpdateConfig(config)
|
||||
c.orgAuth.UpdateConfig(config)
|
||||
c.fileOps.UpdateConfig(config)
|
||||
}
|
||||
|
||||
// ==================== 模板操作 ====================
|
||||
|
||||
// FillTemplate 填写模板
|
||||
// 使用自定义数据填写模板生成文件
|
||||
func (c *Client) FillTemplate(components map[string]string) (*FillTemplate, error) {
|
||||
return c.template.FillWithCustomData(components)
|
||||
}
|
||||
|
||||
// FillTemplateWithDefaults 使用默认数据填写模板
|
||||
func (c *Client) FillTemplateWithDefaults(partyA, legalRepA, partyB, legalRepB string) (*FillTemplate, error) {
|
||||
return c.template.FillWithDefaults(partyA, legalRepA, partyB, legalRepB)
|
||||
}
|
||||
|
||||
// ==================== 签署流程 ====================
|
||||
|
||||
// CreateSignFlow 创建签署流程
|
||||
func (c *Client) CreateSignFlow(req *CreateSignFlowRequest) (string, error) {
|
||||
return c.signFlow.Create(req)
|
||||
}
|
||||
|
||||
// GetSignURL 获取签署链接
|
||||
func (c *Client) GetSignURL(signFlowID, psnAccount, orgName string) (string, string, error) {
|
||||
return c.signFlow.GetSignURL(signFlowID, psnAccount, orgName)
|
||||
}
|
||||
|
||||
// QuerySignFlowDetail 查询签署流程详情
|
||||
func (c *Client) QuerySignFlowDetail(signFlowID string) (*QuerySignFlowDetailResponse, error) {
|
||||
return c.fileOps.QuerySignFlowDetail(signFlowID)
|
||||
}
|
||||
|
||||
// IsSignFlowCompleted 检查签署流程是否完成
|
||||
func (c *Client) IsSignFlowCompleted(signFlowID string) (bool, error) {
|
||||
result, err := c.QuerySignFlowDetail(signFlowID)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
// 状态码2表示已完成
|
||||
return result.Data.SignFlowStatus == 2, nil
|
||||
}
|
||||
|
||||
// ==================== 机构认证 ====================
|
||||
|
||||
// GetOrgAuthURL 获取机构认证链接
|
||||
func (c *Client) GetOrgAuthURL(req *OrgAuthRequest) (string, string, string, error) {
|
||||
return c.orgAuth.GetAuthURL(req)
|
||||
}
|
||||
|
||||
// ValidateOrgAuthInfo 验证机构认证信息
|
||||
func (c *Client) ValidateOrgAuthInfo(req *OrgAuthRequest) error {
|
||||
return c.orgAuth.ValidateAuthInfo(req)
|
||||
}
|
||||
|
||||
// ==================== 文件操作 ====================
|
||||
|
||||
// DownloadSignedFile 下载已签署文件
|
||||
func (c *Client) DownloadSignedFile(signFlowID string) (*DownloadSignedFileResponse, error) {
|
||||
return c.fileOps.DownloadSignedFile(signFlowID)
|
||||
}
|
||||
|
||||
// GetSignFlowStatus 获取签署流程状态
|
||||
func (c *Client) GetSignFlowStatus(signFlowID string) (string, error) {
|
||||
detail, err := c.QuerySignFlowDetail(signFlowID)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return GetSignFlowStatusText(detail.Data.SignFlowStatus), nil
|
||||
}
|
||||
|
||||
// ==================== 业务集成接口 ====================
|
||||
|
||||
// ContractSigningRequest 合同签署请求
|
||||
type ContractSigningRequest struct {
|
||||
// 企业信息
|
||||
CompanyName string `json:"companyName"` // 企业名称
|
||||
UnifiedSocialCode string `json:"unifiedSocialCode"` // 统一社会信用代码
|
||||
LegalPersonName string `json:"legalPersonName"` // 法人姓名
|
||||
LegalPersonID string `json:"legalPersonId"` // 法人身份证号
|
||||
LegalPersonPhone string `json:"legalPersonPhone"` // 法人手机号
|
||||
|
||||
// 经办人信息(可选,如果与法人不同)
|
||||
TransactorName string `json:"transactorName,omitempty"` // 经办人姓名
|
||||
TransactorPhone string `json:"transactorPhone,omitempty"` // 经办人手机号
|
||||
TransactorID string `json:"transactorId,omitempty"` // 经办人身份证号
|
||||
|
||||
// 模板数据(可选)
|
||||
CustomData map[string]string `json:"customData,omitempty"` // 自定义模板数据
|
||||
}
|
||||
|
||||
// ContractSigningResult 合同签署结果
|
||||
type ContractSigningResult struct {
|
||||
FileID string `json:"fileId"` // 文件ID
|
||||
SignFlowID string `json:"signFlowId"` // 签署流程ID
|
||||
SignURL string `json:"signUrl"` // 签署链接
|
||||
ShortURL string `json:"shortUrl"` // 短链接
|
||||
}
|
||||
|
||||
// GenerateContractSigning 生成合同签署
|
||||
// 一站式合同签署服务:填写模板 -> 创建签署流程 -> 获取签署链接
|
||||
func (c *Client) GenerateContractSigning(req *ContractSigningRequest) (*ContractSigningResult, error) {
|
||||
// 1. 准备模板数据
|
||||
var err error
|
||||
var fillTemplate *FillTemplate
|
||||
if len(req.CustomData) > 0 {
|
||||
// 使用自定义数据
|
||||
fillTemplate, err = c.FillTemplate(req.CustomData)
|
||||
} else {
|
||||
// 使用默认数据
|
||||
fillTemplate, err = c.FillTemplateWithDefaults(
|
||||
"海南省学宇思网络科技有限公司",
|
||||
"刘福思",
|
||||
req.CompanyName,
|
||||
req.LegalPersonName,
|
||||
)
|
||||
}
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("填写模板失败: %w", err)
|
||||
}
|
||||
|
||||
// 2. 确定签署人信息
|
||||
signerName := req.LegalPersonName
|
||||
transactorName := req.LegalPersonName
|
||||
transactorPhone := req.LegalPersonPhone
|
||||
transactorID := req.LegalPersonID
|
||||
|
||||
if req.TransactorName != "" {
|
||||
signerName = req.TransactorName
|
||||
transactorName = req.TransactorName
|
||||
transactorPhone = req.TransactorPhone
|
||||
transactorID = req.TransactorID
|
||||
}
|
||||
|
||||
// 3. 创建签署流程
|
||||
signFlowReq := &CreateSignFlowRequest{
|
||||
FileID: fillTemplate.FileID,
|
||||
SignerAccount: req.UnifiedSocialCode,
|
||||
SignerName: signerName,
|
||||
TransactorPhone: transactorPhone,
|
||||
TransactorName: transactorName,
|
||||
TransactorIDCardNum: transactorID,
|
||||
}
|
||||
|
||||
signFlowID, err := c.CreateSignFlow(signFlowReq)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("创建签署流程失败: %w", err)
|
||||
}
|
||||
|
||||
// 4. 获取签署链接
|
||||
signURL, shortURL, err := c.GetSignURL(signFlowID, transactorPhone, signerName)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("获取签署链接失败: %w", err)
|
||||
}
|
||||
|
||||
return &ContractSigningResult{
|
||||
FileID: fillTemplate.FileID,
|
||||
SignFlowID: signFlowID,
|
||||
SignURL: signURL,
|
||||
ShortURL: shortURL,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// EnterpriseAuthRequest 企业认证请求
|
||||
type EnterpriseAuthRequest struct {
|
||||
// 企业信息
|
||||
CompanyName string `json:"companyName"` // 企业名称
|
||||
UnifiedSocialCode string `json:"unifiedSocialCode"` // 统一社会信用代码
|
||||
LegalPersonName string `json:"legalPersonName"` // 法人姓名
|
||||
LegalPersonID string `json:"legalPersonId"` // 法人身份证号
|
||||
|
||||
// 经办人信息
|
||||
TransactorName string `json:"transactorName"` // 经办人姓名
|
||||
TransactorMobile string `json:"transactorMobile"` // 经办人手机号
|
||||
TransactorID string `json:"transactorId"` // 经办人身份证号
|
||||
}
|
||||
|
||||
// EnterpriseAuthResult 企业认证结果
|
||||
type EnterpriseAuthResult struct {
|
||||
AuthFlowID string `json:"authFlowId"` // 认证流程ID
|
||||
AuthURL string `json:"authUrl"` // 认证链接
|
||||
AuthShortURL string `json:"authShortUrl"` // 短链接
|
||||
}
|
||||
|
||||
// GenerateEnterpriseAuth 生成企业认证
|
||||
// 一站式企业认证服务
|
||||
func (c *Client) GenerateEnterpriseAuth(req *EnterpriseAuthRequest) (*EnterpriseAuthResult, error) {
|
||||
authReq := &OrgAuthRequest{
|
||||
OrgName: req.CompanyName,
|
||||
OrgIDCardNum: req.UnifiedSocialCode,
|
||||
LegalRepName: req.LegalPersonName,
|
||||
LegalRepIDCardNum: req.LegalPersonID,
|
||||
TransactorName: req.TransactorName,
|
||||
TransactorIDCardNum: req.TransactorID,
|
||||
TransactorMobile: req.TransactorMobile,
|
||||
}
|
||||
|
||||
// 验证信息
|
||||
if err := c.ValidateOrgAuthInfo(authReq); err != nil {
|
||||
return nil, fmt.Errorf("认证信息验证失败: %w", err)
|
||||
}
|
||||
|
||||
// 获取认证链接
|
||||
authFlowID, authURL, shortURL, err := c.GetOrgAuthURL(authReq)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("获取认证链接失败: %w", err)
|
||||
}
|
||||
|
||||
return &EnterpriseAuthResult{
|
||||
AuthFlowID: authFlowID,
|
||||
AuthURL: authURL,
|
||||
AuthShortURL: shortURL,
|
||||
}, nil
|
||||
}
|
||||
83
internal/shared/esign/config.go
Normal file
83
internal/shared/esign/config.go
Normal file
@@ -0,0 +1,83 @@
|
||||
package esign
|
||||
|
||||
import "fmt"
|
||||
|
||||
// Config e签宝服务配置结构体
|
||||
// 包含应用ID、密钥、服务器URL和模板ID等基础配置信息
|
||||
type Config struct {
|
||||
AppID string `json:"appId"` // 应用ID
|
||||
AppSecret string `json:"appSecret"` // 应用密钥
|
||||
ServerURL string `json:"serverUrl"` // 服务器URL
|
||||
TemplateID string `json:"templateId"` // 模板ID
|
||||
}
|
||||
|
||||
// NewConfig 创建新的配置实例
|
||||
// 提供配置验证和默认值设置
|
||||
func NewConfig(appID, appSecret, serverURL, templateID string) (*Config, error) {
|
||||
if appID == "" {
|
||||
return nil, fmt.Errorf("应用ID不能为空")
|
||||
}
|
||||
if appSecret == "" {
|
||||
return nil, fmt.Errorf("应用密钥不能为空")
|
||||
}
|
||||
if serverURL == "" {
|
||||
return nil, fmt.Errorf("服务器URL不能为空")
|
||||
}
|
||||
if templateID == "" {
|
||||
return nil, fmt.Errorf("模板ID不能为空")
|
||||
}
|
||||
|
||||
return &Config{
|
||||
AppID: appID,
|
||||
AppSecret: appSecret,
|
||||
ServerURL: serverURL,
|
||||
TemplateID: templateID,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// Validate 验证配置的完整性
|
||||
func (c *Config) Validate() error {
|
||||
if c.AppID == "" {
|
||||
return fmt.Errorf("应用ID不能为空")
|
||||
}
|
||||
if c.AppSecret == "" {
|
||||
return fmt.Errorf("应用密钥不能为空")
|
||||
}
|
||||
if c.ServerURL == "" {
|
||||
return fmt.Errorf("服务器URL不能为空")
|
||||
}
|
||||
if c.TemplateID == "" {
|
||||
return fmt.Errorf("模板ID不能为空")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// 认证模式常量
|
||||
const (
|
||||
// 个人认证模式
|
||||
AuthModeMobile3 = "PSN_MOBILE3" // 手机号三要素认证
|
||||
AuthModeIDCard = "PSN_IDCARD" // 身份证认证
|
||||
AuthModeBank = "PSN_BANK" // 银行卡认证
|
||||
|
||||
// 意愿认证模式
|
||||
WillingnessAuthSMS = "CODE_SMS" // 短信验证码
|
||||
WillingnessAuthEmail = "CODE_EMAIL" // 邮箱验证码
|
||||
|
||||
// 证件类型常量
|
||||
IDCardTypeChina = "CRED_PSN_CH_IDCARD" // 中国大陆居民身份证
|
||||
OrgCardTypeUSCC = "CRED_ORG_USCC" // 统一社会信用代码
|
||||
|
||||
// 签署区样式常量
|
||||
SignFieldStyleNormal = 1 // 普通签章
|
||||
SignFieldStyleSeam = 2 // 骑缝签章
|
||||
|
||||
// 签署人类型常量
|
||||
SignerTypePerson = 0 // 个人
|
||||
SignerTypeOrg = 1 // 机构
|
||||
|
||||
// URL类型常量
|
||||
UrlTypeSign = 2 // 签署链接
|
||||
|
||||
// 客户端类型常量
|
||||
ClientTypeAll = "ALL" // 所有客户端
|
||||
)
|
||||
193
internal/shared/esign/example.go
Normal file
193
internal/shared/esign/example.go
Normal file
@@ -0,0 +1,193 @@
|
||||
package esign
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
)
|
||||
|
||||
// Example 展示如何使用重构后的e签宝SDK
|
||||
func Example() {
|
||||
// 1. 创建配置
|
||||
config, err := NewConfig(
|
||||
"your_app_id",
|
||||
"your_app_secret",
|
||||
"https://smlopenapi.esign.cn",
|
||||
"your_template_id",
|
||||
)
|
||||
if err != nil {
|
||||
log.Fatal("配置创建失败:", err)
|
||||
}
|
||||
|
||||
// 2. 创建客户端
|
||||
client := NewClient(config)
|
||||
|
||||
// 示例1: 简单合同签署流程
|
||||
fmt.Println("=== 示例1: 简单合同签署流程 ===")
|
||||
contractReq := &ContractSigningRequest{
|
||||
CompanyName: "测试公司",
|
||||
UnifiedSocialCode: "123456789012345678",
|
||||
LegalPersonName: "张三",
|
||||
LegalPersonID: "123456789012345678",
|
||||
LegalPersonPhone: "13800138000",
|
||||
}
|
||||
|
||||
result, err := client.GenerateContractSigning(contractReq)
|
||||
if err != nil {
|
||||
log.Printf("合同签署失败: %v", err)
|
||||
} else {
|
||||
fmt.Printf("合同签署成功: %+v\n", result)
|
||||
}
|
||||
|
||||
// 示例2: 企业认证流程
|
||||
fmt.Println("\n=== 示例2: 企业认证流程 ===")
|
||||
authReq := &EnterpriseAuthRequest{
|
||||
CompanyName: "测试公司",
|
||||
UnifiedSocialCode: "123456789012345678",
|
||||
LegalPersonName: "张三",
|
||||
LegalPersonID: "123456789012345678",
|
||||
TransactorName: "李四",
|
||||
TransactorMobile: "13800138001",
|
||||
TransactorID: "123456789012345679",
|
||||
}
|
||||
|
||||
authResult, err := client.GenerateEnterpriseAuth(authReq)
|
||||
if err != nil {
|
||||
log.Printf("企业认证失败: %v", err)
|
||||
} else {
|
||||
fmt.Printf("企业认证成功: %+v\n", authResult)
|
||||
}
|
||||
|
||||
// 示例3: 分步操作
|
||||
fmt.Println("\n=== 示例3: 分步操作 ===")
|
||||
|
||||
// 3.1 填写模板
|
||||
templateData := map[string]string{
|
||||
"JFQY": "甲方公司",
|
||||
"JFFR": "甲方法人",
|
||||
"YFQY": "乙方公司",
|
||||
"YFFR": "乙方法人",
|
||||
"QDRQ": "2024年01月01日",
|
||||
}
|
||||
|
||||
fileID, err := client.FillTemplate(templateData)
|
||||
if err != nil {
|
||||
log.Printf("模板填写失败: %v", err)
|
||||
return
|
||||
}
|
||||
fmt.Printf("模板填写成功,文件ID: %s\n", fileID)
|
||||
|
||||
// 3.2 创建签署流程
|
||||
signFlowReq := &CreateSignFlowRequest{
|
||||
FileID: fileID.FileID,
|
||||
SignerAccount: "123456789012345678",
|
||||
SignerName: "乙方公司",
|
||||
TransactorPhone: "13800138000",
|
||||
TransactorName: "乙方法人",
|
||||
TransactorIDCardNum: "123456789012345678",
|
||||
}
|
||||
|
||||
signFlowID, err := client.CreateSignFlow(signFlowReq)
|
||||
if err != nil {
|
||||
log.Printf("创建签署流程失败: %v", err)
|
||||
return
|
||||
}
|
||||
fmt.Printf("签署流程创建成功,流程ID: %s\n", signFlowID)
|
||||
|
||||
// 3.3 获取签署链接
|
||||
signURL, shortURL, err := client.GetSignURL(signFlowID, "13800138000", "乙方公司")
|
||||
if err != nil {
|
||||
log.Printf("获取签署链接失败: %v", err)
|
||||
return
|
||||
}
|
||||
fmt.Printf("签署链接: %s\n", signURL)
|
||||
fmt.Printf("短链接: %s\n", shortURL)
|
||||
|
||||
// 3.4 查询签署状态
|
||||
status, err := client.GetSignFlowStatus(signFlowID)
|
||||
if err != nil {
|
||||
log.Printf("查询签署状态失败: %v", err)
|
||||
return
|
||||
}
|
||||
fmt.Printf("签署状态: %s\n", status)
|
||||
|
||||
// 3.5 检查是否完成
|
||||
completed, err := client.IsSignFlowCompleted(signFlowID)
|
||||
if err != nil {
|
||||
log.Printf("检查签署状态失败: %v", err)
|
||||
return
|
||||
}
|
||||
fmt.Printf("签署是否完成: %t\n", completed)
|
||||
}
|
||||
|
||||
// ExampleBasicUsage 基础用法示例
|
||||
func ExampleBasicUsage() {
|
||||
// 最简单的用法 - 一行代码完成合同签署
|
||||
config, _ := NewConfig("app_id", "app_secret", "server_url", "template_id")
|
||||
client := NewClient(config)
|
||||
|
||||
// 快速合同签署
|
||||
result, err := client.GenerateContractSigning(&ContractSigningRequest{
|
||||
CompanyName: "我的公司",
|
||||
UnifiedSocialCode: "123456789012345678",
|
||||
LegalPersonName: "张三",
|
||||
LegalPersonID: "123456789012345678",
|
||||
LegalPersonPhone: "13800138000",
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
log.Fatal("签署失败:", err)
|
||||
}
|
||||
|
||||
fmt.Printf("请访问以下链接进行签署: %s\n", result.SignURL)
|
||||
}
|
||||
|
||||
// ExampleWithCustomData 自定义数据示例
|
||||
func ExampleWithCustomData() {
|
||||
config, _ := NewConfig("app_id", "app_secret", "server_url", "template_id")
|
||||
client := NewClient(config)
|
||||
|
||||
// 使用自定义模板数据
|
||||
customData := map[string]string{
|
||||
"custom_field_1": "自定义值1",
|
||||
"custom_field_2": "自定义值2",
|
||||
"contract_date": "2024年01月01日",
|
||||
}
|
||||
|
||||
result, err := client.GenerateContractSigning(&ContractSigningRequest{
|
||||
CompanyName: "我的公司",
|
||||
UnifiedSocialCode: "123456789012345678",
|
||||
LegalPersonName: "张三",
|
||||
LegalPersonID: "123456789012345678",
|
||||
LegalPersonPhone: "13800138000",
|
||||
CustomData: customData,
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
log.Fatal("签署失败:", err)
|
||||
}
|
||||
|
||||
fmt.Printf("自定义合同签署链接: %s\n", result.SignURL)
|
||||
}
|
||||
|
||||
// ExampleEnterpriseAuth 企业认证示例
|
||||
func ExampleEnterpriseAuth() {
|
||||
config, _ := NewConfig("app_id", "app_secret", "server_url", "template_id")
|
||||
client := NewClient(config)
|
||||
|
||||
// 企业认证
|
||||
authResult, err := client.GenerateEnterpriseAuth(&EnterpriseAuthRequest{
|
||||
CompanyName: "我的公司",
|
||||
UnifiedSocialCode: "123456789012345678",
|
||||
LegalPersonName: "张三",
|
||||
LegalPersonID: "123456789012345678",
|
||||
TransactorName: "李四",
|
||||
TransactorMobile: "13800138001",
|
||||
TransactorID: "123456789012345679",
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
log.Fatal("企业认证失败:", err)
|
||||
}
|
||||
|
||||
fmt.Printf("请访问以下链接进行企业认证: %s\n", authResult.AuthURL)
|
||||
}
|
||||
208
internal/shared/esign/fileops_service.go
Normal file
208
internal/shared/esign/fileops_service.go
Normal file
@@ -0,0 +1,208 @@
|
||||
package esign
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
)
|
||||
|
||||
// FileOpsService 文件操作服务
|
||||
// 处理文件下载、流程查询等操作
|
||||
type FileOpsService struct {
|
||||
httpClient *HTTPClient
|
||||
config *Config
|
||||
}
|
||||
|
||||
// NewFileOpsService 创建文件操作服务
|
||||
func NewFileOpsService(httpClient *HTTPClient, config *Config) *FileOpsService {
|
||||
return &FileOpsService{
|
||||
httpClient: httpClient,
|
||||
config: config,
|
||||
}
|
||||
}
|
||||
|
||||
// UpdateConfig 更新配置
|
||||
func (s *FileOpsService) UpdateConfig(config *Config) {
|
||||
s.config = config
|
||||
}
|
||||
|
||||
// DownloadSignedFile 下载已签署文件及附属材料
|
||||
// 获取签署完成后的文件下载链接和证书下载链接
|
||||
//
|
||||
// 参数说明:
|
||||
// - signFlowId: 签署流程ID
|
||||
//
|
||||
// 返回: 下载文件响应和错误信息
|
||||
func (s *FileOpsService) DownloadSignedFile(signFlowId string) (*DownloadSignedFileResponse, error) {
|
||||
fmt.Println("开始下载已签署文件及附属材料...")
|
||||
|
||||
// 发送API请求
|
||||
urlPath := fmt.Sprintf("/v3/sign-flow/%s/attachments", signFlowId)
|
||||
responseBody, err := s.httpClient.Request("GET", urlPath, nil)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("下载已签署文件失败: %v", err)
|
||||
}
|
||||
|
||||
// 解析响应
|
||||
var response DownloadSignedFileResponse
|
||||
if err := UnmarshalResponse(responseBody, &response); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err := CheckResponseCode(response.Code, response.Message); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
fmt.Printf("已签署文件下载信息获取成功!\n")
|
||||
fmt.Printf("文件数量: %d\n", len(response.Data.Files))
|
||||
fmt.Printf("附属材料数量: %d\n", len(response.Data.Attachments))
|
||||
if response.Data.CertificateDownloadUrl != "" {
|
||||
fmt.Printf("证书下载链接: %s\n", response.Data.CertificateDownloadUrl)
|
||||
}
|
||||
|
||||
return &response, nil
|
||||
}
|
||||
|
||||
// QuerySignFlowDetail 查询签署流程详情
|
||||
// 获取签署流程的详细状态和参与方信息
|
||||
//
|
||||
// 参数说明:
|
||||
// - signFlowId: 签署流程ID
|
||||
//
|
||||
// 返回: 流程详情响应和错误信息
|
||||
func (s *FileOpsService) QuerySignFlowDetail(signFlowId string) (*QuerySignFlowDetailResponse, error) {
|
||||
fmt.Println("开始查询签署流程详情...")
|
||||
|
||||
// 发送API请求
|
||||
urlPath := fmt.Sprintf("/v3/sign-flow/%s/detail", signFlowId)
|
||||
responseBody, err := s.httpClient.Request("GET", urlPath, nil)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("查询签署流程详情失败: %v", err)
|
||||
}
|
||||
|
||||
// 解析响应
|
||||
var response QuerySignFlowDetailResponse
|
||||
if err := UnmarshalResponse(responseBody, &response); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err := CheckResponseCode(response.Code, response.Message); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
fmt.Printf("查询签署流程详情响应: %+v\n", response)
|
||||
return &response, nil
|
||||
}
|
||||
|
||||
// GetSignedFileDownloadUrls 获取已签署文件的下载链接
|
||||
// 从下载响应中提取所有文件的下载链接
|
||||
//
|
||||
// 参数说明:
|
||||
// - downloadResponse: 下载文件响应
|
||||
//
|
||||
// 返回: 文件下载链接映射
|
||||
func GetSignedFileDownloadUrls(downloadResponse *DownloadSignedFileResponse) map[string]string {
|
||||
urls := make(map[string]string)
|
||||
|
||||
// 添加已签署文件
|
||||
for _, file := range downloadResponse.Data.Files {
|
||||
urls[file.FileName] = file.DownloadUrl
|
||||
}
|
||||
|
||||
// 添加附属材料
|
||||
for _, attachment := range downloadResponse.Data.Attachments {
|
||||
urls[attachment.FileName] = attachment.DownloadUrl
|
||||
}
|
||||
|
||||
return urls
|
||||
}
|
||||
|
||||
// GetSignFlowStatusText 获取签署流程状态文本
|
||||
// 从流程详情中提取状态信息
|
||||
//
|
||||
// 参数说明:
|
||||
// - status: 流程状态码
|
||||
//
|
||||
// 返回: 流程状态描述
|
||||
func GetSignFlowStatusText(status int32) string {
|
||||
switch status {
|
||||
case 1:
|
||||
return "草稿"
|
||||
case 2:
|
||||
return "签署中"
|
||||
case 3:
|
||||
return "已完成"
|
||||
case 4:
|
||||
return "已撤销"
|
||||
case 5:
|
||||
return "已过期"
|
||||
case 6:
|
||||
return "已拒绝"
|
||||
default:
|
||||
return fmt.Sprintf("未知状态(%d)", status)
|
||||
}
|
||||
}
|
||||
|
||||
// GetSignerStatus 获取签署人状态
|
||||
// 从流程详情中提取指定签署人的状态
|
||||
//
|
||||
// 参数说明:
|
||||
// - detailResponse: 流程详情响应
|
||||
// - signerName: 签署人姓名
|
||||
//
|
||||
// 返回: 签署人状态描述
|
||||
func GetSignerStatus(detailResponse *QuerySignFlowDetailResponse, signerName string) string {
|
||||
for _, signer := range detailResponse.Data.Signers {
|
||||
var name string
|
||||
if signer.PsnSigner != nil {
|
||||
name = signer.PsnSigner.PsnName
|
||||
} else if signer.OrgSigner != nil {
|
||||
name = signer.OrgSigner.OrgName
|
||||
}
|
||||
|
||||
if name == signerName {
|
||||
switch signer.SignStatus {
|
||||
case 1:
|
||||
return "待签署"
|
||||
case 2:
|
||||
return "已签署"
|
||||
case 3:
|
||||
return "已拒绝"
|
||||
case 4:
|
||||
return "已过期"
|
||||
default:
|
||||
return fmt.Sprintf("未知状态(%d)", signer.SignStatus)
|
||||
}
|
||||
}
|
||||
}
|
||||
return "未找到签署人"
|
||||
}
|
||||
|
||||
// IsSignFlowCompleted 检查签署流程是否完成
|
||||
// 根据状态码判断签署流程是否已完成
|
||||
//
|
||||
// 参数说明:
|
||||
// - detailResponse: 流程详情响应
|
||||
//
|
||||
// 返回: 是否完成
|
||||
func IsSignFlowCompleted(detailResponse *QuerySignFlowDetailResponse) bool {
|
||||
// 状态码2表示已完成
|
||||
return detailResponse.Data.SignFlowStatus == 2
|
||||
}
|
||||
|
||||
// GetFileList 获取文件列表
|
||||
// 从下载响应中获取所有文件信息
|
||||
//
|
||||
// 参数说明:
|
||||
// - downloadResponse: 下载文件响应
|
||||
//
|
||||
// 返回: 文件信息列表
|
||||
func GetFileList(downloadResponse *DownloadSignedFileResponse) []SignedFileInfo {
|
||||
var files []SignedFileInfo
|
||||
|
||||
// 添加已签署文件
|
||||
files = append(files, downloadResponse.Data.Files...)
|
||||
|
||||
// 添加附属材料
|
||||
files = append(files, downloadResponse.Data.Attachments...)
|
||||
|
||||
return files
|
||||
}
|
||||
199
internal/shared/esign/http.go
Normal file
199
internal/shared/esign/http.go
Normal file
@@ -0,0 +1,199 @@
|
||||
package esign
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"sort"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
// HTTPClient e签宝HTTP客户端
|
||||
// 处理所有e签宝API的HTTP请求,包括签名生成、请求头设置等
|
||||
type HTTPClient struct {
|
||||
config *Config
|
||||
client *http.Client
|
||||
}
|
||||
|
||||
// NewHTTPClient 创建HTTP客户端
|
||||
func NewHTTPClient(config *Config) *HTTPClient {
|
||||
return &HTTPClient{
|
||||
config: config,
|
||||
client: &http.Client{Timeout: 30 * time.Second},
|
||||
}
|
||||
}
|
||||
|
||||
// UpdateConfig 更新配置
|
||||
func (h *HTTPClient) UpdateConfig(config *Config) {
|
||||
h.config = config
|
||||
}
|
||||
|
||||
// Request e签宝通用请求函数
|
||||
// 处理所有e签宝API的HTTP请求,包括签名生成、请求头设置等
|
||||
//
|
||||
// 参数说明:
|
||||
// - method: HTTP方法(GET、POST等)
|
||||
// - urlPath: API路径
|
||||
// - body: 请求体字节数组
|
||||
//
|
||||
// 返回: 响应体字节数组和错误信息
|
||||
func (h *HTTPClient) Request(method, urlPath string, body []byte) ([]byte, error) {
|
||||
// 生成签名所需参数
|
||||
timestamp := getCurrentTimestamp()
|
||||
nonce := generateNonce()
|
||||
date := getCurrentDate()
|
||||
|
||||
// 计算Content-MD5
|
||||
contentMD5 := ""
|
||||
if len(body) > 0 {
|
||||
contentMD5 = getContentMD5(body)
|
||||
}
|
||||
|
||||
// 根据Java示例,Headers为空字符串
|
||||
headers := ""
|
||||
|
||||
// 生成签名
|
||||
signature := generateSignature(h.config.AppSecret, method, "*/*", contentMD5, "application/json", date, headers, urlPath)
|
||||
|
||||
// 创建HTTP请求
|
||||
url := h.config.ServerURL + urlPath
|
||||
req, err := http.NewRequest(method, url, bytes.NewBuffer(body))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("创建HTTP请求失败: %v", err)
|
||||
}
|
||||
|
||||
// 设置请求头
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
req.Header.Set("Content-MD5", contentMD5)
|
||||
req.Header.Set("Date", date)
|
||||
req.Header.Set("Accept", "*/*")
|
||||
req.Header.Set("X-Tsign-Open-App-Id", h.config.AppID)
|
||||
req.Header.Set("X-Tsign-Open-Auth-Mode", "Signature")
|
||||
req.Header.Set("X-Tsign-Open-Ca-Timestamp", timestamp)
|
||||
req.Header.Set("X-Tsign-Open-Nonce", nonce)
|
||||
req.Header.Set("X-Tsign-Open-Ca-Signature", signature)
|
||||
|
||||
// 发送请求
|
||||
client := &http.Client{Timeout: 30 * time.Second}
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("发送HTTP请求失败: %v", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
// 读取响应
|
||||
responseBody, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("读取响应失败: %v", err)
|
||||
}
|
||||
|
||||
// 打印响应内容用于调试
|
||||
fmt.Printf("API响应状态码: %d\n", resp.StatusCode)
|
||||
fmt.Printf("API响应内容: %s\n", string(responseBody))
|
||||
|
||||
// 检查响应状态码
|
||||
if resp.StatusCode != 200 {
|
||||
return nil, fmt.Errorf("API请求失败,状态码: %d", resp.StatusCode)
|
||||
}
|
||||
|
||||
return responseBody, nil
|
||||
}
|
||||
|
||||
// MarshalRequest 序列化请求数据为JSON
|
||||
//
|
||||
// 参数:
|
||||
// - data: 要序列化的数据
|
||||
//
|
||||
// 返回: JSON字节数组和错误信息
|
||||
func MarshalRequest(data interface{}) ([]byte, error) {
|
||||
jsonData, err := json.Marshal(data)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("序列化请求数据失败: %v", err)
|
||||
}
|
||||
return jsonData, nil
|
||||
}
|
||||
|
||||
// UnmarshalResponse 反序列化响应数据
|
||||
//
|
||||
// 参数:
|
||||
// - responseBody: 响应体字节数组
|
||||
// - response: 目标响应结构体指针
|
||||
//
|
||||
// 返回: 错误信息
|
||||
func UnmarshalResponse(responseBody []byte, response interface{}) error {
|
||||
if err := json.Unmarshal(responseBody, response); err != nil {
|
||||
return fmt.Errorf("解析响应失败: %v,响应内容: %s", err, string(responseBody))
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// CheckResponseCode 检查API响应码
|
||||
//
|
||||
// 参数:
|
||||
// - code: 响应码
|
||||
// - message: 响应消息
|
||||
//
|
||||
// 返回: 错误信息
|
||||
func CheckResponseCode(code int, message string) error {
|
||||
if code != 0 {
|
||||
return fmt.Errorf("API调用失败: %s", message)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// sortURLQueryParams 对URL查询参数按字典序(ASCII码)升序排序
|
||||
//
|
||||
// 参数:
|
||||
// - urlPath: 包含查询参数的URL路径
|
||||
//
|
||||
// 返回: 排序后的URL路径
|
||||
func sortURLQueryParams(urlPath string) string {
|
||||
// 检查是否包含查询参数
|
||||
if !strings.Contains(urlPath, "?") {
|
||||
return urlPath
|
||||
}
|
||||
|
||||
// 分离路径和查询参数
|
||||
parts := strings.SplitN(urlPath, "?", 2)
|
||||
if len(parts) != 2 {
|
||||
return urlPath
|
||||
}
|
||||
|
||||
basePath := parts[0]
|
||||
queryString := parts[1]
|
||||
|
||||
// 解析查询参数
|
||||
values, err := url.ParseQuery(queryString)
|
||||
if err != nil {
|
||||
// 如果解析失败,返回原始路径
|
||||
return urlPath
|
||||
}
|
||||
|
||||
// 获取所有参数键并排序
|
||||
var keys []string
|
||||
for key := range values {
|
||||
keys = append(keys, key)
|
||||
}
|
||||
sort.Strings(keys)
|
||||
|
||||
// 重新构建查询字符串
|
||||
var sortedPairs []string
|
||||
for _, key := range keys {
|
||||
for _, value := range values[key] {
|
||||
sortedPairs = append(sortedPairs, key+"="+value)
|
||||
}
|
||||
}
|
||||
|
||||
// 组合排序后的查询参数
|
||||
sortedQueryString := strings.Join(sortedPairs, "&")
|
||||
|
||||
// 返回完整的URL路径
|
||||
if sortedQueryString != "" {
|
||||
return basePath + "?" + sortedQueryString
|
||||
}
|
||||
return basePath
|
||||
}
|
||||
63
internal/shared/esign/org_identity.go
Normal file
63
internal/shared/esign/org_identity.go
Normal file
@@ -0,0 +1,63 @@
|
||||
package esign
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/url"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// QueryOrgIdentityInfo 查询机构认证信息
|
||||
// 根据orgId、orgName或orgIDCardNum查询机构实名认证信息
|
||||
func (s *OrgAuthService) QueryOrgIdentityInfo(req *QueryOrgIdentityRequest) (*QueryOrgIdentityResponse, error) {
|
||||
// 构建查询参数
|
||||
params := url.Values{}
|
||||
if req.OrgID != "" {
|
||||
params.Add("orgId", req.OrgID)
|
||||
} else if req.OrgName != "" {
|
||||
params.Add("orgName", req.OrgName)
|
||||
} else if req.OrgIDCardNum != "" {
|
||||
params.Add("orgIDCardNum", req.OrgIDCardNum)
|
||||
if req.OrgIDCardType != "" {
|
||||
params.Add("orgIDCardType", string(req.OrgIDCardType))
|
||||
}
|
||||
} else {
|
||||
return nil, fmt.Errorf("至少提供orgId, orgName或orgIDCardNum之一")
|
||||
}
|
||||
|
||||
// 构建urlPath带query - 不使用URL编码,保持原始参数值
|
||||
urlPath := "/v3/organizations/identity-info"
|
||||
if len(params) > 0 {
|
||||
var queryParts []string
|
||||
for key, values := range params {
|
||||
for _, value := range values {
|
||||
queryParts = append(queryParts, key+"="+value)
|
||||
}
|
||||
}
|
||||
urlPath += "?" + strings.Join(queryParts, "&")
|
||||
}
|
||||
|
||||
// 发送API请求
|
||||
responseBody, err := s.httpClient.Request("GET", urlPath, nil)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("查询机构认证信息失败: %v", err)
|
||||
}
|
||||
|
||||
// 解析响应
|
||||
var response QueryOrgIdentityResponse
|
||||
if err := UnmarshalResponse(responseBody, &response); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err := CheckResponseCode(int(response.Code), response.Message); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
fmt.Printf("查询机构认证信息成功!\n")
|
||||
return &response, nil
|
||||
}
|
||||
|
||||
// QueryOrgIdentityInfo 查询机构认证信息(客户端方法)
|
||||
// 通过Client提供的便捷方法
|
||||
func (c *Client) QueryOrgIdentityInfo(req *QueryOrgIdentityRequest) (*QueryOrgIdentityResponse, error) {
|
||||
return c.orgAuth.QueryOrgIdentityInfo(req)
|
||||
}
|
||||
205
internal/shared/esign/orgauth_service.go
Normal file
205
internal/shared/esign/orgauth_service.go
Normal file
@@ -0,0 +1,205 @@
|
||||
package esign
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
)
|
||||
|
||||
// OrgAuthService 机构认证服务
|
||||
// 处理机构认证和授权相关操作
|
||||
type OrgAuthService struct {
|
||||
httpClient *HTTPClient
|
||||
config *Config
|
||||
}
|
||||
|
||||
// NewOrgAuthService 创建机构认证服务
|
||||
func NewOrgAuthService(httpClient *HTTPClient, config *Config) *OrgAuthService {
|
||||
return &OrgAuthService{
|
||||
httpClient: httpClient,
|
||||
config: config,
|
||||
}
|
||||
}
|
||||
|
||||
// UpdateConfig 更新配置
|
||||
func (s *OrgAuthService) UpdateConfig(config *Config) {
|
||||
s.config = config
|
||||
}
|
||||
|
||||
// OrgAuthRequest 机构认证请求
|
||||
type OrgAuthRequest struct {
|
||||
OrgName string `json:"orgName"` // 机构名称
|
||||
OrgIDCardNum string `json:"orgIdCardNum"` // 机构证件号
|
||||
LegalRepName string `json:"legalRepName"` // 法定代表人姓名
|
||||
LegalRepIDCardNum string `json:"legalRepIdCardNum"` // 法定代表人身份证号
|
||||
TransactorName string `json:"transactorName"` // 经办人姓名
|
||||
TransactorIDCardNum string `json:"transactorIdCardNum"` // 经办人身份证号
|
||||
TransactorMobile string `json:"transactorMobile"` // 经办人手机号
|
||||
}
|
||||
|
||||
// GetAuthURL 获取机构认证&授权页面链接
|
||||
// 为机构用户获取认证和授权页面链接,用于机构身份认证
|
||||
func (s *OrgAuthService) GetAuthURL(req *OrgAuthRequest) (string, string, string, error) {
|
||||
// 构建请求数据
|
||||
requestData := GetOrgAuthUrlRequest{
|
||||
OrgAuthConfig: &OrgAuthConfig{
|
||||
OrgName: req.OrgName,
|
||||
OrgInfo: &OrgAuthInfo{
|
||||
OrgIDCardNum: req.OrgIDCardNum,
|
||||
OrgIDCardType: OrgCardTypeUSCC,
|
||||
LegalRepName: req.LegalRepName,
|
||||
LegalRepIDCardNum: req.LegalRepIDCardNum,
|
||||
LegalRepIDCardType: IDCardTypeChina,
|
||||
},
|
||||
TransactorAuthPageConfig: &TransactorAuthPageConfig{
|
||||
PsnAvailableAuthModes: []string{AuthModeMobile3},
|
||||
PsnDefaultAuthMode: AuthModeMobile3,
|
||||
PsnEditableFields: []string{},
|
||||
},
|
||||
TransactorInfo: &TransactorAuthInfo{
|
||||
PsnAccount: req.TransactorMobile,
|
||||
PsnInfo: &PsnAuthInfo{
|
||||
PsnName: req.TransactorName,
|
||||
PsnIDCardNum: req.TransactorIDCardNum,
|
||||
PsnIDCardType: IDCardTypeChina,
|
||||
PsnMobile: req.TransactorMobile,
|
||||
PsnIdentityVerify: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
ClientType: ClientTypeAll,
|
||||
}
|
||||
|
||||
// 序列化请求数据
|
||||
jsonData, err := MarshalRequest(requestData)
|
||||
if err != nil {
|
||||
return "", "", "", err
|
||||
}
|
||||
|
||||
fmt.Printf("获取机构认证&授权页面链接请求数据: %s\n", string(jsonData))
|
||||
|
||||
// 发送API请求
|
||||
responseBody, err := s.httpClient.Request("POST", "/v3/org-auth-url", jsonData)
|
||||
if err != nil {
|
||||
return "", "", "", fmt.Errorf("获取机构认证&授权页面链接失败: %v", err)
|
||||
}
|
||||
|
||||
// 解析响应
|
||||
var response GetOrgAuthUrlResponse
|
||||
if err := UnmarshalResponse(responseBody, &response); err != nil {
|
||||
return "", "", "", err
|
||||
}
|
||||
|
||||
if err := CheckResponseCode(response.Code, response.Message); err != nil {
|
||||
return "", "", "", err
|
||||
}
|
||||
|
||||
fmt.Printf("机构认证&授权页面链接获取成功!\n")
|
||||
fmt.Printf("认证流程ID: %s\n", response.Data.AuthFlowId)
|
||||
fmt.Printf("完整链接: %s\n", response.Data.AuthUrl)
|
||||
fmt.Printf("短链接: %s\n", response.Data.AuthShortUrl)
|
||||
|
||||
return response.Data.AuthFlowId, response.Data.AuthUrl, response.Data.AuthShortUrl, nil
|
||||
}
|
||||
|
||||
// CreateAuthConfig 创建机构认证配置
|
||||
// 构建机构认证所需的配置信息
|
||||
func (s *OrgAuthService) CreateAuthConfig(req *OrgAuthRequest) *OrgAuthConfig {
|
||||
return &OrgAuthConfig{
|
||||
OrgName: req.OrgName,
|
||||
OrgInfo: &OrgAuthInfo{
|
||||
OrgIDCardNum: req.OrgIDCardNum,
|
||||
OrgIDCardType: OrgCardTypeUSCC,
|
||||
LegalRepName: req.LegalRepName,
|
||||
LegalRepIDCardNum: req.LegalRepIDCardNum,
|
||||
LegalRepIDCardType: IDCardTypeChina,
|
||||
},
|
||||
TransactorAuthPageConfig: &TransactorAuthPageConfig{
|
||||
PsnAvailableAuthModes: []string{AuthModeMobile3},
|
||||
PsnDefaultAuthMode: AuthModeMobile3,
|
||||
PsnEditableFields: []string{},
|
||||
},
|
||||
TransactorInfo: &TransactorAuthInfo{
|
||||
PsnAccount: req.TransactorMobile,
|
||||
PsnInfo: &PsnAuthInfo{
|
||||
PsnName: req.TransactorName,
|
||||
PsnIDCardNum: req.TransactorIDCardNum,
|
||||
PsnIDCardType: IDCardTypeChina,
|
||||
PsnMobile: req.TransactorMobile,
|
||||
PsnIdentityVerify: true,
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// ValidateAuthInfo 验证机构认证信息
|
||||
// 检查机构认证信息的完整性和格式
|
||||
func (s *OrgAuthService) ValidateAuthInfo(req *OrgAuthRequest) error {
|
||||
if req.OrgName == "" {
|
||||
return fmt.Errorf("机构名称不能为空")
|
||||
}
|
||||
if req.OrgIDCardNum == "" {
|
||||
return fmt.Errorf("机构证件号不能为空")
|
||||
}
|
||||
if req.LegalRepName == "" {
|
||||
return fmt.Errorf("法定代表人姓名不能为空")
|
||||
}
|
||||
if req.LegalRepIDCardNum == "" {
|
||||
return fmt.Errorf("法定代表人身份证号不能为空")
|
||||
}
|
||||
if req.TransactorName == "" {
|
||||
return fmt.Errorf("经办人姓名不能为空")
|
||||
}
|
||||
if req.TransactorIDCardNum == "" {
|
||||
return fmt.Errorf("经办人身份证号不能为空")
|
||||
}
|
||||
if req.TransactorMobile == "" {
|
||||
return fmt.Errorf("经办人手机号不能为空")
|
||||
}
|
||||
|
||||
// 验证统一社会信用代码格式(18位)
|
||||
if len(req.OrgIDCardNum) != 18 {
|
||||
return fmt.Errorf("机构证件号(统一社会信用代码)必须是18位")
|
||||
}
|
||||
|
||||
// 验证身份证号格式(18位)
|
||||
if len(req.LegalRepIDCardNum) != 18 {
|
||||
return fmt.Errorf("法定代表人身份证号必须是18位")
|
||||
}
|
||||
if len(req.TransactorIDCardNum) != 18 {
|
||||
return fmt.Errorf("经办人身份证号必须是18位")
|
||||
}
|
||||
|
||||
// 验证手机号格式(11位)
|
||||
if len(req.TransactorMobile) != 11 {
|
||||
return fmt.Errorf("经办人手机号必须是11位")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// QueryOrgIdentity 查询机构认证信息
|
||||
// 查询机构的实名认证状态和信息
|
||||
func (s *OrgAuthService) QueryOrgIdentity(req *QueryOrgIdentityRequest) (*QueryOrgIdentityResponse, error) {
|
||||
// 序列化请求数据
|
||||
jsonData, err := MarshalRequest(req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 发送API请求
|
||||
responseBody, err := s.httpClient.Request("POST", "/v3/organizations/identity", jsonData)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("查询机构认证信息失败: %v", err)
|
||||
}
|
||||
|
||||
// 解析响应
|
||||
var response QueryOrgIdentityResponse
|
||||
if err := UnmarshalResponse(responseBody, &response); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err := CheckResponseCode(int(response.Code), response.Message); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &response, nil
|
||||
}
|
||||
215
internal/shared/esign/signflow_service.go
Normal file
215
internal/shared/esign/signflow_service.go
Normal file
@@ -0,0 +1,215 @@
|
||||
package esign
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
)
|
||||
|
||||
// SignFlowService 签署流程服务
|
||||
// 处理签署流程创建、链接获取等操作
|
||||
type SignFlowService struct {
|
||||
httpClient *HTTPClient
|
||||
config *Config
|
||||
}
|
||||
|
||||
// NewSignFlowService 创建签署流程服务
|
||||
func NewSignFlowService(httpClient *HTTPClient, config *Config) *SignFlowService {
|
||||
return &SignFlowService{
|
||||
httpClient: httpClient,
|
||||
config: config,
|
||||
}
|
||||
}
|
||||
|
||||
// UpdateConfig 更新配置
|
||||
func (s *SignFlowService) UpdateConfig(config *Config) {
|
||||
s.config = config
|
||||
}
|
||||
|
||||
// Create 创建签署流程
|
||||
// 创建包含多个签署人的签署流程,支持自动盖章和手动签署
|
||||
func (s *SignFlowService) Create(req *CreateSignFlowRequest) (string, error) {
|
||||
fmt.Println("开始创建签署流程...")
|
||||
fmt.Println("(将创建包含甲方自动盖章和乙方手动签署的流程)")
|
||||
|
||||
// 构建甲方签署人信息(自动盖章)
|
||||
partyASigner := s.buildPartyASigner(req.FileID)
|
||||
|
||||
// 构建乙方签署人信息(手动签署)
|
||||
partyBSigner := s.buildPartyBSigner(req.FileID, req.SignerAccount, req.SignerName, req.TransactorPhone, req.TransactorName, req.TransactorIDCardNum)
|
||||
|
||||
signers := []SignerInfo{partyASigner, partyBSigner}
|
||||
|
||||
// 构建请求数据
|
||||
requestData := CreateSignFlowByFileRequest{
|
||||
Docs: []DocInfo{
|
||||
{
|
||||
FileId: req.FileID,
|
||||
FileName: "天远数据API合作协议.pdf",
|
||||
},
|
||||
},
|
||||
SignFlowConfig: s.buildSignFlowConfig(),
|
||||
Signers: signers,
|
||||
}
|
||||
|
||||
// 序列化请求数据
|
||||
jsonData, err := MarshalRequest(requestData)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
fmt.Printf("发起签署请求数据: %s\n", string(jsonData))
|
||||
|
||||
// 发送API请求
|
||||
responseBody, err := s.httpClient.Request("POST", "/v3/sign-flow/create-by-file", jsonData)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("发起签署失败: %v", err)
|
||||
}
|
||||
|
||||
// 解析响应
|
||||
var response CreateSignFlowByFileResponse
|
||||
if err := UnmarshalResponse(responseBody, &response); err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
if err := CheckResponseCode(response.Code, response.Message); err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
fmt.Printf("签署流程创建成功,流程ID: %s\n", response.Data.SignFlowId)
|
||||
return response.Data.SignFlowId, nil
|
||||
}
|
||||
|
||||
// GetSignURL 获取签署页面链接
|
||||
// 为指定的签署人获取签署页面链接
|
||||
func (s *SignFlowService) GetSignURL(signFlowID, psnAccount, orgName string) (string, string, error) {
|
||||
fmt.Println("开始获取签署页面链接...")
|
||||
|
||||
// 构建请求数据
|
||||
requestData := GetSignUrlRequest{
|
||||
NeedLogin: false,
|
||||
UrlType: UrlTypeSign,
|
||||
Operator: &Operator{
|
||||
PsnAccount: psnAccount,
|
||||
},
|
||||
Organization: &Organization{
|
||||
OrgName: orgName,
|
||||
},
|
||||
ClientType: ClientTypeAll,
|
||||
}
|
||||
|
||||
// 序列化请求数据
|
||||
jsonData, err := MarshalRequest(requestData)
|
||||
if err != nil {
|
||||
return "", "", err
|
||||
}
|
||||
|
||||
fmt.Printf("获取签署页面链接请求数据: %s\n", string(jsonData))
|
||||
|
||||
// 发送API请求
|
||||
urlPath := fmt.Sprintf("/v3/sign-flow/%s/sign-url", signFlowID)
|
||||
responseBody, err := s.httpClient.Request("POST", urlPath, jsonData)
|
||||
if err != nil {
|
||||
return "", "", fmt.Errorf("获取签署页面链接失败: %v", err)
|
||||
}
|
||||
|
||||
// 解析响应
|
||||
var response GetSignUrlResponse
|
||||
if err := UnmarshalResponse(responseBody, &response); err != nil {
|
||||
return "", "", err
|
||||
}
|
||||
|
||||
if err := CheckResponseCode(response.Code, response.Message); err != nil {
|
||||
return "", "", err
|
||||
}
|
||||
|
||||
fmt.Printf("签署页面链接获取成功!\n")
|
||||
fmt.Printf("完整链接: %s\n", response.Data.Url)
|
||||
fmt.Printf("短链接: %s\n", response.Data.ShortUrl)
|
||||
|
||||
return response.Data.Url, response.Data.ShortUrl, nil
|
||||
}
|
||||
|
||||
// buildPartyASigner 构建甲方签署人信息(自动盖章)
|
||||
func (s *SignFlowService) buildPartyASigner(fileID string) SignerInfo {
|
||||
return SignerInfo{
|
||||
SignConfig: &SignConfig{SignOrder: 1},
|
||||
SignerType: SignerTypeOrg,
|
||||
SignFields: []SignField{
|
||||
{
|
||||
CustomBizNum: "甲方签章",
|
||||
FileId: fileID,
|
||||
NormalSignFieldConfig: &NormalSignFieldConfig{
|
||||
AutoSign: true,
|
||||
SignFieldStyle: SignFieldStyleNormal,
|
||||
SignFieldPosition: &SignFieldPosition{
|
||||
PositionPage: "1",
|
||||
PositionX: 200,
|
||||
PositionY: 200,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// buildPartyBSigner 构建乙方签署人信息(手动签署)
|
||||
func (s *SignFlowService) buildPartyBSigner(fileID, signerAccount, signerName, transactorPhone, transactorName, transactorIDCardNum string) SignerInfo {
|
||||
return SignerInfo{
|
||||
SignConfig: &SignConfig{
|
||||
SignOrder: 2,
|
||||
},
|
||||
AuthConfig: &AuthConfig{
|
||||
PsnAvailableAuthModes: []string{AuthModeMobile3},
|
||||
WillingnessAuthModes: []string{WillingnessAuthSMS},
|
||||
},
|
||||
SignerType: SignerTypeOrg,
|
||||
OrgSignerInfo: &OrgSignerInfo{
|
||||
OrgName: signerName,
|
||||
OrgInfo: &OrgInfo{
|
||||
LegalRepName: transactorName,
|
||||
LegalRepIDCardNum: transactorIDCardNum,
|
||||
LegalRepIDCardType: IDCardTypeChina,
|
||||
OrgIDCardNum: signerAccount,
|
||||
OrgIDCardType: OrgCardTypeUSCC,
|
||||
},
|
||||
TransactorInfo: &TransactorInfo{
|
||||
PsnAccount: transactorPhone,
|
||||
PsnInfo: &PsnInfo{
|
||||
PsnName: transactorName,
|
||||
PsnIDCardNum: transactorIDCardNum,
|
||||
PsnIDCardType: IDCardTypeChina,
|
||||
},
|
||||
},
|
||||
},
|
||||
SignFields: []SignField{
|
||||
{
|
||||
CustomBizNum: "乙方签章",
|
||||
FileId: fileID,
|
||||
NormalSignFieldConfig: &NormalSignFieldConfig{
|
||||
AutoSign: false,
|
||||
SignFieldStyle: SignFieldStyleNormal,
|
||||
SignFieldPosition: &SignFieldPosition{
|
||||
PositionPage: "1",
|
||||
PositionX: 458,
|
||||
PositionY: 200,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// buildSignFlowConfig 构建签署流程配置
|
||||
func (s *SignFlowService) buildSignFlowConfig() SignFlowConfig {
|
||||
return SignFlowConfig{
|
||||
SignFlowTitle: "天远数据API合作协议签署",
|
||||
SignFlowExpireTime: calculateExpireTime(7), // 7天后过期
|
||||
AutoFinish: true, // 所有签署方完成后自动完结
|
||||
AuthConfig: &AuthConfig{
|
||||
PsnAvailableAuthModes: []string{AuthModeMobile3},
|
||||
WillingnessAuthModes: []string{WillingnessAuthSMS},
|
||||
},
|
||||
ContractConfig: &ContractConfig{
|
||||
AllowToRescind: false,
|
||||
},
|
||||
}
|
||||
}
|
||||
167
internal/shared/esign/template_service.go
Normal file
167
internal/shared/esign/template_service.go
Normal file
@@ -0,0 +1,167 @@
|
||||
package esign
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"time"
|
||||
)
|
||||
|
||||
// TemplateService 模板服务
|
||||
// 处理模板填写和文件生成相关操作
|
||||
type TemplateService struct {
|
||||
httpClient *HTTPClient
|
||||
config *Config
|
||||
}
|
||||
|
||||
// NewTemplateService 创建模板服务
|
||||
func NewTemplateService(httpClient *HTTPClient, config *Config) *TemplateService {
|
||||
return &TemplateService{
|
||||
httpClient: httpClient,
|
||||
config: config,
|
||||
}
|
||||
}
|
||||
|
||||
// UpdateConfig 更新配置
|
||||
func (s *TemplateService) UpdateConfig(config *Config) {
|
||||
s.config = config
|
||||
}
|
||||
|
||||
// Fill 填写模板生成文件
|
||||
// 根据模板ID和填写内容生成包含填写内容的文档
|
||||
//
|
||||
// 参数说明:
|
||||
// - components: 需要填写的组件列表,包含字段键名和值
|
||||
//
|
||||
// 返回: 生成的文件ID和错误信息
|
||||
func (s *TemplateService) Fill(components []Component) (*FillTemplate, error) {
|
||||
fmt.Println("开始填写模板生成文件...")
|
||||
|
||||
// 生成带时间戳的文件名
|
||||
fileName := generateFileName("天远数据API合作协议", "pdf")
|
||||
|
||||
// 构建请求数据
|
||||
requestData := FillTemplateRequest{
|
||||
DocTemplateID: s.config.TemplateID,
|
||||
FileName: fileName,
|
||||
Components: components,
|
||||
}
|
||||
|
||||
// 序列化请求数据
|
||||
jsonData, err := MarshalRequest(requestData)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 发送API请求
|
||||
responseBody, err := s.httpClient.Request("POST", "/v3/files/create-by-doc-template", jsonData)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("填写模板失败: %v", err)
|
||||
}
|
||||
|
||||
// 解析响应
|
||||
var response FillTemplateResponse
|
||||
if err := UnmarshalResponse(responseBody, &response); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 检查响应状态
|
||||
if err := CheckResponseCode(response.Code, response.Message); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
fmt.Printf("模板填写成功,文件ID: %s\n", response.Data.FileID)
|
||||
return &FillTemplate{
|
||||
FileID: response.Data.FileID,
|
||||
FileDownloadUrl: response.Data.FileDownloadUrl,
|
||||
FileName: fileName,
|
||||
TemplateID: s.config.TemplateID,
|
||||
FillTime: time.Now(),
|
||||
}, nil
|
||||
}
|
||||
|
||||
// FillWithDefaults 使用默认数据填写模板
|
||||
// 使用预设的默认数据填写模板,适用于测试或标准流程
|
||||
//
|
||||
// 参数说明:
|
||||
// - partyA: 甲方企业名称
|
||||
// - legalRepA: 甲方法人姓名
|
||||
// - partyB: 乙方企业名称
|
||||
// - legalRepB: 乙方法人姓名
|
||||
//
|
||||
// 返回: 生成的文件ID和错误信息
|
||||
func (s *TemplateService) FillWithDefaults(partyA, legalRepA, partyB, legalRepB string) (*FillTemplate, error) {
|
||||
// 构建默认填写组件
|
||||
components := []Component{
|
||||
{
|
||||
ComponentKey: "JFQY",
|
||||
ComponentValue: partyA,
|
||||
},
|
||||
{
|
||||
ComponentKey: "JFFR",
|
||||
ComponentValue: legalRepA,
|
||||
},
|
||||
{
|
||||
ComponentKey: "YFQY",
|
||||
ComponentValue: partyB,
|
||||
},
|
||||
{
|
||||
ComponentKey: "YFFR",
|
||||
ComponentValue: legalRepB,
|
||||
},
|
||||
{
|
||||
ComponentKey: "QDRQ",
|
||||
ComponentValue: formatDateForTemplate(),
|
||||
},
|
||||
}
|
||||
|
||||
return s.Fill(components)
|
||||
}
|
||||
|
||||
// FillWithCustomData 使用自定义数据填写模板
|
||||
// 允许传入自定义的组件数据来填写模板
|
||||
//
|
||||
// 参数说明:
|
||||
// - customComponents: 自定义组件数据
|
||||
//
|
||||
// 返回: 生成的文件ID和错误信息
|
||||
func (s *TemplateService) FillWithCustomData(customComponents map[string]string) (*FillTemplate, error) {
|
||||
var components []Component
|
||||
|
||||
// 将map转换为Component切片
|
||||
for key, value := range customComponents {
|
||||
components = append(components, Component{
|
||||
ComponentKey: key,
|
||||
ComponentValue: value,
|
||||
})
|
||||
}
|
||||
|
||||
return s.Fill(components)
|
||||
}
|
||||
|
||||
// CreateDefaultComponents 创建默认模板数据
|
||||
// 返回用于测试的默认模板填写数据
|
||||
//
|
||||
// 返回: 默认组件数据
|
||||
func CreateDefaultComponents() []Component {
|
||||
return []Component{
|
||||
{
|
||||
ComponentKey: "JFQY",
|
||||
ComponentValue: "海南省学宇思网络科技有限公司",
|
||||
},
|
||||
{
|
||||
ComponentKey: "JFFR",
|
||||
ComponentValue: "刘福思",
|
||||
},
|
||||
{
|
||||
ComponentKey: "YFQY",
|
||||
ComponentValue: "测试企业",
|
||||
},
|
||||
{
|
||||
ComponentKey: "YFFR",
|
||||
ComponentValue: "测试法人",
|
||||
},
|
||||
{
|
||||
ComponentKey: "QDRQ",
|
||||
ComponentValue: time.Now().Format("2006年01月02日"),
|
||||
},
|
||||
}
|
||||
}
|
||||
571
internal/shared/esign/types.go
Normal file
571
internal/shared/esign/types.go
Normal file
@@ -0,0 +1,571 @@
|
||||
package esign
|
||||
|
||||
import "time"
|
||||
|
||||
// ==================== 模板填写相关结构体 ====================
|
||||
|
||||
// FillTemplateRequest 模板填写请求结构体
|
||||
// 用于根据模板ID生成包含填写内容的文档
|
||||
type FillTemplateRequest struct {
|
||||
DocTemplateID string `json:"docTemplateId"` // 文档模板ID
|
||||
FileName string `json:"fileName"` // 生成的文件名
|
||||
Components []Component `json:"components"` // 填写组件列表
|
||||
}
|
||||
|
||||
// Component 控件结构体
|
||||
// 定义模板中需要填写的字段信息
|
||||
type Component struct {
|
||||
ComponentID string `json:"componentId,omitempty"` // 控件ID(可选)
|
||||
ComponentKey string `json:"componentKey,omitempty"` // 控件键名(可选)
|
||||
ComponentValue string `json:"componentValue"` // 控件值
|
||||
}
|
||||
|
||||
// FillTemplateResponse 模板填写响应结构体
|
||||
type FillTemplateResponse struct {
|
||||
Code int `json:"code"` // 响应码
|
||||
Message string `json:"message"` // 响应消息
|
||||
Data struct {
|
||||
FileID string `json:"fileId"` // 生成的文件ID
|
||||
FileDownloadUrl string `json:"fileDownloadUrl"` // 文件下载URL
|
||||
} `json:"data"`
|
||||
}
|
||||
type FillTemplate struct {
|
||||
FileID string `json:"fileId"` // 生成的文件ID
|
||||
FileDownloadUrl string `json:"fileDownloadUrl"` // 文件下载URL
|
||||
FileName string `json:"fileName"` // 文件名
|
||||
TemplateID string `json:"templateId"` // 模板ID
|
||||
FillTime time.Time `json:"fillTime"` // 填写时间
|
||||
}
|
||||
|
||||
// ==================== 签署流程相关结构体 ====================
|
||||
|
||||
// CreateSignFlowByFileRequest 发起签署请求结构体
|
||||
// 用于创建基于文件的签署流程
|
||||
type CreateSignFlowByFileRequest struct {
|
||||
Docs []DocInfo `json:"docs"` // 文档信息列表
|
||||
SignFlowConfig SignFlowConfig `json:"signFlowConfig"` // 签署流程配置
|
||||
Signers []SignerInfo `json:"signers"` // 签署人列表
|
||||
}
|
||||
|
||||
// CreateSignFlowByFileResponse 发起签署响应结构体
|
||||
type CreateSignFlowByFileResponse struct {
|
||||
Code int `json:"code"` // 响应码
|
||||
Message string `json:"message"` // 响应消息
|
||||
Data struct {
|
||||
SignFlowId string `json:"signFlowId"` // 签署流程ID
|
||||
} `json:"data"`
|
||||
}
|
||||
|
||||
// DocInfo 文档信息
|
||||
type DocInfo struct {
|
||||
FileId string `json:"fileId"` // 文件ID
|
||||
FileName string `json:"fileName"` // 文件名
|
||||
}
|
||||
|
||||
// SignFlowConfig 签署流程配置
|
||||
type SignFlowConfig struct {
|
||||
SignFlowTitle string `json:"signFlowTitle"` // 签署流程标题
|
||||
SignFlowExpireTime int64 `json:"signFlowExpireTime,omitempty"` // 签署流程过期时间
|
||||
AutoFinish bool `json:"autoFinish"` // 是否自动完结
|
||||
NotifyUrl string `json:"notifyUrl,omitempty"` // 回调通知URL
|
||||
RedirectConfig *RedirectConfig `json:"redirectConfig,omitempty"` // 重定向配置
|
||||
AuthConfig *AuthConfig `json:"authConfig,omitempty"` // 认证配置
|
||||
ContractConfig *ContractConfig `json:"contractConfig,omitempty"` // 合同配置
|
||||
}
|
||||
|
||||
// RedirectConfig 重定向配置
|
||||
type RedirectConfig struct {
|
||||
RedirectUrl string `json:"redirectUrl"` // 重定向URL
|
||||
}
|
||||
|
||||
// AuthConfig 认证配置
|
||||
type AuthConfig struct {
|
||||
PsnAvailableAuthModes []string `json:"psnAvailableAuthModes"` // 个人可用认证模式
|
||||
OrgAvailableAuthModes []string `json:"orgAvailableAuthModes"` // 机构可用认证模式
|
||||
WillingnessAuthModes []string `json:"willingnessAuthModes"` // 意愿认证模式
|
||||
AudioVideoTemplateId string `json:"audioVideoTemplateId"` // 音视频模板ID
|
||||
}
|
||||
|
||||
// ContractConfig 合同配置
|
||||
type ContractConfig struct {
|
||||
AllowToRescind bool `json:"allowToRescind"` // 是否允许撤销
|
||||
}
|
||||
|
||||
// ==================== 签署人相关结构体 ====================
|
||||
|
||||
// SignerInfo 签署人信息结构体
|
||||
type SignerInfo struct {
|
||||
SignConfig *SignConfig `json:"signConfig"` // 签署配置
|
||||
AuthConfig *AuthConfig `json:"authConfig"` // 认证配置
|
||||
NoticeConfig *NoticeConfig `json:"noticeConfig"` // 通知配置
|
||||
SignerType int `json:"signerType"` // 签署人类型:0-个人,1-机构
|
||||
PsnSignerInfo *PsnSignerInfo `json:"psnSignerInfo,omitempty"` // 个人签署人信息
|
||||
OrgSignerInfo *OrgSignerInfo `json:"orgSignerInfo,omitempty"` // 机构签署人信息
|
||||
SignFields []SignField `json:"signFields"` // 签署区列表
|
||||
}
|
||||
|
||||
// SignConfig 签署配置
|
||||
type SignConfig struct {
|
||||
SignOrder int `json:"signOrder"` // 签署顺序
|
||||
}
|
||||
|
||||
// NoticeConfig 通知配置
|
||||
type NoticeConfig struct {
|
||||
NoticeTypes string `json:"noticeTypes"` // 通知类型:1-短信,2-邮件,3-短信+邮件
|
||||
}
|
||||
|
||||
// PsnSignerInfo 个人签署人信息
|
||||
type PsnSignerInfo struct {
|
||||
PsnAccount string `json:"psnAccount"` // 个人账号
|
||||
PsnInfo *PsnInfo `json:"psnInfo"` // 个人信息
|
||||
}
|
||||
|
||||
// PsnInfo 个人基本信息
|
||||
type PsnInfo struct {
|
||||
PsnName string `json:"psnName"` // 个人姓名
|
||||
PsnIDCardNum string `json:"psnIDCardNum,omitempty"` // 身份证号
|
||||
PsnIDCardType string `json:"psnIDCardType,omitempty"` // 证件类型
|
||||
BankCardNum string `json:"bankCardNum,omitempty"` // 银行卡号
|
||||
}
|
||||
|
||||
// OrgSignerInfo 机构签署人信息
|
||||
type OrgSignerInfo struct {
|
||||
OrgName string `json:"orgName"` // 机构名称
|
||||
OrgInfo *OrgInfo `json:"orgInfo"` // 机构信息
|
||||
TransactorInfo *TransactorInfo `json:"transactorInfo"` // 经办人信息
|
||||
}
|
||||
|
||||
// OrgInfo 机构信息
|
||||
type OrgInfo struct {
|
||||
LegalRepName string `json:"legalRepName"` // 法定代表人姓名
|
||||
LegalRepIDCardNum string `json:"legalRepIDCardNum"` // 法定代表人身份证号
|
||||
LegalRepIDCardType string `json:"legalRepIDCardType"` // 法定代表人证件类型
|
||||
OrgIDCardNum string `json:"orgIDCardNum"` // 机构证件号
|
||||
OrgIDCardType string `json:"orgIDCardType"` // 机构证件类型
|
||||
}
|
||||
|
||||
// TransactorInfo 经办人信息
|
||||
type TransactorInfo struct {
|
||||
PsnAccount string `json:"psnAccount"` // 经办人账号
|
||||
PsnInfo *PsnInfo `json:"psnInfo"` // 经办人信息
|
||||
}
|
||||
|
||||
// ==================== 签署区相关结构体 ====================
|
||||
|
||||
// SignField 签署区信息
|
||||
type SignField struct {
|
||||
CustomBizNum string `json:"customBizNum"` // 自定义业务号
|
||||
FileId string `json:"fileId"` // 文件ID
|
||||
NormalSignFieldConfig *NormalSignFieldConfig `json:"normalSignFieldConfig"` // 普通签署区配置
|
||||
}
|
||||
|
||||
// NormalSignFieldConfig 普通签署区配置
|
||||
type NormalSignFieldConfig struct {
|
||||
AutoSign bool `json:"autoSign,omitempty"` // 是否自动签署
|
||||
SignFieldStyle int `json:"signFieldStyle"` // 签署区样式:1-普通签章,2-骑缝签章
|
||||
SignFieldPosition *SignFieldPosition `json:"signFieldPosition"` // 签署区位置
|
||||
}
|
||||
|
||||
// SignFieldPosition 签署区位置
|
||||
type SignFieldPosition struct {
|
||||
PositionPage string `json:"positionPage"` // 页码
|
||||
PositionX float64 `json:"positionX"` // X坐标
|
||||
PositionY float64 `json:"positionY"` // Y坐标
|
||||
}
|
||||
|
||||
// ==================== 签署页面链接相关结构体 ====================
|
||||
|
||||
// GetSignUrlRequest 获取签署页面链接请求结构体
|
||||
type GetSignUrlRequest struct {
|
||||
NeedLogin bool `json:"needLogin,omitempty"` // 是否需要登录
|
||||
UrlType int `json:"urlType,omitempty"` // URL类型
|
||||
Operator *Operator `json:"operator"` // 操作人信息
|
||||
Organization *Organization `json:"organization,omitempty"` // 机构信息
|
||||
RedirectConfig *RedirectConfig `json:"redirectConfig,omitempty"` // 重定向配置
|
||||
ClientType string `json:"clientType,omitempty"` // 客户端类型
|
||||
AppScheme string `json:"appScheme,omitempty"` // 应用协议
|
||||
}
|
||||
|
||||
// Operator 操作人信息
|
||||
type Operator struct {
|
||||
PsnAccount string `json:"psnAccount,omitempty"` // 个人账号
|
||||
PsnId string `json:"psnId,omitempty"` // 个人ID
|
||||
}
|
||||
|
||||
// Organization 机构信息
|
||||
type Organization struct {
|
||||
OrgId string `json:"orgId,omitempty"` // 机构ID
|
||||
OrgName string `json:"orgName,omitempty"` // 机构名称
|
||||
}
|
||||
|
||||
// GetSignUrlResponse 获取签署页面链接响应结构体
|
||||
type GetSignUrlResponse struct {
|
||||
Code int `json:"code"` // 响应码
|
||||
Message string `json:"message"` // 响应消息
|
||||
Data struct {
|
||||
ShortUrl string `json:"shortUrl"` // 短链接
|
||||
Url string `json:"url"` // 完整链接
|
||||
} `json:"data"`
|
||||
}
|
||||
|
||||
// ==================== 文件下载相关结构体 ====================
|
||||
|
||||
// DownloadSignedFileResponse 下载已签署文件响应结构体
|
||||
type DownloadSignedFileResponse struct {
|
||||
Code int `json:"code"` // 响应码
|
||||
Message string `json:"message"` // 响应消息
|
||||
Data struct {
|
||||
Files []SignedFileInfo `json:"files"` // 已签署文件列表
|
||||
Attachments []SignedFileInfo `json:"attachments"` // 附属材料列表
|
||||
CertificateDownloadUrl string `json:"certificateDownloadUrl"` // 证书下载链接
|
||||
} `json:"data"`
|
||||
}
|
||||
|
||||
// SignedFileInfo 已签署文件信息
|
||||
type SignedFileInfo struct {
|
||||
FileId string `json:"fileId"` // 文件ID
|
||||
FileName string `json:"fileName"` // 文件名
|
||||
DownloadUrl string `json:"downloadUrl"` // 下载链接
|
||||
}
|
||||
|
||||
// ==================== 流程查询相关结构体 ====================
|
||||
|
||||
// QuerySignFlowDetailResponse 查询签署流程详情响应结构体
|
||||
type QuerySignFlowDetailResponse struct {
|
||||
Code int `json:"code"` // 响应码
|
||||
Message string `json:"message"` // 响应消息
|
||||
Data struct {
|
||||
SignFlowStatus int32 `json:"signFlowStatus"` // 签署流程状态
|
||||
SignFlowDescription string `json:"signFlowDescription"` // 签署流程描述
|
||||
RescissionStatus int32 `json:"rescissionStatus"` // 撤销状态
|
||||
RescissionSignFlowIds []string `json:"rescissionSignFlowIds"` // 撤销的签署流程ID列表
|
||||
RevokeReason string `json:"revokeReason"` // 撤销原因
|
||||
SignFlowCreateTime int64 `json:"signFlowCreateTime"` // 签署流程创建时间
|
||||
SignFlowStartTime int64 `json:"signFlowStartTime"` // 签署流程开始时间
|
||||
SignFlowFinishTime int64 `json:"signFlowFinishTime"` // 签署流程完成时间
|
||||
SignFlowInitiator *SignFlowInitiator `json:"signFlowInitiator"` // 签署流程发起方
|
||||
SignFlowConfig *SignFlowConfigDetail `json:"signFlowConfig"` // 签署流程配置详情
|
||||
Docs []DocDetail `json:"docs"` // 文档详情列表
|
||||
Attachments []AttachmentDetail `json:"attachments"` // 附属材料详情列表
|
||||
Signers []SignerDetail `json:"signers"` // 签署人详情列表
|
||||
Copiers []CopierDetail `json:"copiers"` // 抄送方详情列表
|
||||
} `json:"data"`
|
||||
}
|
||||
|
||||
// SignFlowInitiator 签署流程发起方
|
||||
type SignFlowInitiator struct {
|
||||
PsnInitiator *PsnInitiator `json:"psnInitiator"` // 个人发起方
|
||||
OrgInitiator *OrgInitiator `json:"orgInitiator"` // 机构发起方
|
||||
}
|
||||
|
||||
// PsnInitiator 个人发起方
|
||||
type PsnInitiator struct {
|
||||
PsnId string `json:"psnId"` // 个人ID
|
||||
PsnName string `json:"psnName"` // 个人姓名
|
||||
}
|
||||
|
||||
// OrgInitiator 机构发起方
|
||||
type OrgInitiator struct {
|
||||
OrgId string `json:"orgId"` // 机构ID
|
||||
OrgName string `json:"orgName"` // 机构名称
|
||||
Transactor *Transactor `json:"transactor"` // 经办人
|
||||
}
|
||||
|
||||
// Transactor 经办人
|
||||
type Transactor struct {
|
||||
PsnId string `json:"psnId"` // 个人ID
|
||||
PsnName string `json:"psnName"` // 个人姓名
|
||||
}
|
||||
|
||||
// SignFlowConfigDetail 签署流程配置详情
|
||||
type SignFlowConfigDetail struct {
|
||||
SignFlowTitle string `json:"signFlowTitle"` // 签署流程标题
|
||||
ContractGroupIds []string `json:"contractGroupIds"` // 合同组ID列表
|
||||
AutoFinish bool `json:"autoFinish"` // 是否自动完结
|
||||
SignFlowExpireTime int64 `json:"signFlowExpireTime"` // 签署流程过期时间
|
||||
NotifyUrl string `json:"notifyUrl"` // 回调通知URL
|
||||
ChargeConfig *ChargeConfig `json:"chargeConfig"` // 计费配置
|
||||
NoticeConfig *NoticeConfig `json:"noticeConfig"` // 通知配置
|
||||
SignConfig *SignConfigDetail `json:"signConfig"` // 签署配置详情
|
||||
AuthConfig *AuthConfig `json:"authConfig"` // 认证配置
|
||||
}
|
||||
|
||||
// ChargeConfig 计费配置
|
||||
type ChargeConfig struct {
|
||||
ChargeMode int `json:"chargeMode"` // 计费模式
|
||||
OrderType string `json:"orderType"` // 订单类型
|
||||
BarrierCode string `json:"barrierCode"` // 障碍码
|
||||
}
|
||||
|
||||
// SignConfigDetail 签署配置详情
|
||||
type SignConfigDetail struct {
|
||||
AvailableSignClientTypes string `json:"availableSignClientTypes"` // 可用签署客户端类型
|
||||
ShowBatchDropSealButton bool `json:"showBatchDropSealButton"` // 是否显示批量盖章按钮
|
||||
SignTipsTitle string `json:"signTipsTitle"` // 签署提示标题
|
||||
SignTipsContent string `json:"signTipsContent"` // 签署提示内容
|
||||
SignMode string `json:"signMode"` // 签署模式
|
||||
DedicatedCloudId string `json:"dedicatedCloudId"` // 专属云ID
|
||||
}
|
||||
|
||||
// DocDetail 文档详情
|
||||
type DocDetail struct {
|
||||
FileId string `json:"fileId"` // 文件ID
|
||||
FileName string `json:"fileName"` // 文件名
|
||||
FileEditPwd string `json:"fileEditPwd"` // 文件编辑密码
|
||||
ContractNum string `json:"contractNum"` // 合同编号
|
||||
ContractBizTypeId string `json:"contractBizTypeId"` // 合同业务类型ID
|
||||
}
|
||||
|
||||
// AttachmentDetail 附属材料详情
|
||||
type AttachmentDetail struct {
|
||||
FileId string `json:"fileId"` // 文件ID
|
||||
FileName string `json:"fileName"` // 文件名
|
||||
SignerUpload bool `json:"signerUpload"` // 是否签署人上传
|
||||
}
|
||||
|
||||
// CopierDetail 抄送方详情
|
||||
type CopierDetail struct {
|
||||
CopierPsnInfo *CopierPsnInfo `json:"copierPsnInfo"` // 个人抄送方
|
||||
CopierOrgInfo *CopierOrgInfo `json:"copierOrgInfo"` // 机构抄送方
|
||||
}
|
||||
|
||||
// CopierPsnInfo 个人抄送方
|
||||
type CopierPsnInfo struct {
|
||||
PsnId string `json:"psnId"` // 个人ID
|
||||
PsnAccount string `json:"psnAccount"` // 个人账号
|
||||
}
|
||||
|
||||
// CopierOrgInfo 机构抄送方
|
||||
type CopierOrgInfo struct {
|
||||
OrgId string `json:"orgId"` // 机构ID
|
||||
OrgName string `json:"orgName"` // 机构名称
|
||||
}
|
||||
|
||||
// SignerDetail 签署人详情
|
||||
type SignerDetail struct {
|
||||
PsnSigner *PsnSignerDetail `json:"psnSigner,omitempty"` // 个人签署人详情
|
||||
OrgSigner *OrgSignerDetail `json:"orgSigner,omitempty"` // 机构签署人详情
|
||||
SignerType int `json:"signerType"` // 签署人类型
|
||||
SignOrder int `json:"signOrder"` // 签署顺序
|
||||
SignStatus int `json:"signStatus"` // 签署状态
|
||||
SignFields []SignFieldDetail `json:"signFields"` // 签署区详情列表
|
||||
}
|
||||
|
||||
// PsnSignerDetail 个人签署人详情
|
||||
type PsnSignerDetail struct {
|
||||
PsnId string `json:"psnId"` // 个人ID
|
||||
PsnName string `json:"psnName"` // 个人姓名
|
||||
PsnAccount *PsnAccount `json:"psnAccount"` // 个人账号信息
|
||||
}
|
||||
|
||||
// PsnAccount 个人账号信息
|
||||
type PsnAccount struct {
|
||||
AccountMobile string `json:"accountMobile"` // 账号手机号
|
||||
AccountEmail string `json:"accountEmail"` // 账号邮箱
|
||||
}
|
||||
|
||||
// OrgSignerDetail 机构签署人详情
|
||||
type OrgSignerDetail struct {
|
||||
OrgId string `json:"orgId"` // 机构ID
|
||||
OrgName string `json:"orgName"` // 机构名称
|
||||
OrgAccount string `json:"orgAccount"` // 机构账号
|
||||
}
|
||||
|
||||
// SignFieldDetail 签署区详情
|
||||
type SignFieldDetail struct {
|
||||
SignFieldId string `json:"signFieldId"` // 签署区ID
|
||||
SignFieldStatus string `json:"signFieldStatus"` // 签署区状态
|
||||
SealApprovalFlowId string `json:"sealApprovalFlowId"` // 印章审批流程ID
|
||||
StatusUpdateTime int64 `json:"statusUpdateTime"` // 状态更新时间
|
||||
FailReason string `json:"failReason"` // 失败原因
|
||||
CustomBizNum string `json:"customBizNum"` // 自定义业务号
|
||||
FileId string `json:"fileId"` // 文件ID
|
||||
SignFieldType int `json:"signFieldType"` // 签署区类型
|
||||
MustSign bool `json:"mustSign"` // 是否必须签署
|
||||
SignFieldSealType int `json:"signFieldSealType"` // 签署区印章类型
|
||||
NormalSignFieldConfig *NormalSignFieldDetail `json:"normalSignFieldConfig"` // 普通签署区配置详情
|
||||
}
|
||||
|
||||
// NormalSignFieldDetail 普通签署区配置详情
|
||||
type NormalSignFieldDetail struct {
|
||||
FreeMode bool `json:"freeMode"` // 是否自由模式
|
||||
SignFieldStyle int `json:"signFieldStyle"` // 签署区样式
|
||||
SignFieldPosition *SignFieldPosition `json:"signFieldPosition"` // 签署区位置
|
||||
MovableSignField bool `json:"movableSignField"` // 是否可移动签署区
|
||||
AutoSign bool `json:"autoSign"` // 是否自动签署
|
||||
SealStyle string `json:"sealStyle"` // 印章样式
|
||||
SealId string `json:"sealId"` // 印章ID
|
||||
}
|
||||
|
||||
// ==================== 机构认证相关结构体 ====================
|
||||
|
||||
// GetOrgAuthUrlRequest 获取机构认证&授权页面链接请求结构体
|
||||
type GetOrgAuthUrlRequest struct {
|
||||
OrgAuthConfig *OrgAuthConfig `json:"orgAuthConfig"` // 机构认证配置
|
||||
AuthorizeConfig *AuthorizeConfig `json:"authorizeConfig,omitempty"` // 授权配置
|
||||
RedirectConfig *RedirectConfig `json:"redirectConfig,omitempty"` // 重定向配置
|
||||
ClientType string `json:"clientType,omitempty"` // 客户端类型
|
||||
NotifyUrl string `json:"notifyUrl,omitempty"` // 回调通知URL
|
||||
AppScheme string `json:"appScheme,omitempty"` // 应用协议
|
||||
}
|
||||
|
||||
// OrgAuthConfig 机构认证授权相关结构体
|
||||
type OrgAuthConfig struct {
|
||||
OrgName string `json:"orgName,omitempty"` // 机构名称
|
||||
OrgId string `json:"orgId,omitempty"` // 机构ID
|
||||
OrgInfo *OrgAuthInfo `json:"orgInfo,omitempty"` // 机构信息
|
||||
TransactorAuthPageConfig *TransactorAuthPageConfig `json:"transactorAuthPageConfig,omitempty"` // 经办人认证页面配置
|
||||
TransactorInfo *TransactorAuthInfo `json:"transactorInfo,omitempty"` // 经办人信息
|
||||
}
|
||||
|
||||
// OrgAuthInfo 机构认证信息
|
||||
type OrgAuthInfo struct {
|
||||
OrgIDCardNum string `json:"orgIDCardNum,omitempty"` // 机构证件号
|
||||
OrgIDCardType string `json:"orgIDCardType,omitempty"` // 机构证件类型
|
||||
LegalRepName string `json:"legalRepName,omitempty"` // 法定代表人姓名
|
||||
LegalRepIDCardNum string `json:"legalRepIDCardNum,omitempty"` // 法定代表人身份证号
|
||||
LegalRepIDCardType string `json:"legalRepIDCardType,omitempty"` // 法定代表人证件类型
|
||||
OrgBankAccountNum string `json:"orgBankAccountNum,omitempty"` // 机构银行账号
|
||||
}
|
||||
|
||||
// TransactorAuthPageConfig 经办人认证页面配置
|
||||
type TransactorAuthPageConfig struct {
|
||||
PsnAvailableAuthModes []string `json:"psnAvailableAuthModes,omitempty"` // 个人可用认证模式
|
||||
PsnDefaultAuthMode string `json:"psnDefaultAuthMode,omitempty"` // 个人默认认证模式
|
||||
PsnEditableFields []string `json:"psnEditableFields,omitempty"` // 个人可编辑字段
|
||||
}
|
||||
|
||||
// TransactorAuthInfo 经办人认证信息
|
||||
type TransactorAuthInfo struct {
|
||||
PsnAccount string `json:"psnAccount,omitempty"` // 经办人账号
|
||||
PsnInfo *PsnAuthInfo `json:"psnInfo,omitempty"` // 经办人信息
|
||||
}
|
||||
|
||||
// PsnAuthInfo 个人认证信息
|
||||
type PsnAuthInfo struct {
|
||||
PsnName string `json:"psnName,omitempty"` // 个人姓名
|
||||
PsnIDCardNum string `json:"psnIDCardNum,omitempty"` // 身份证号
|
||||
PsnIDCardType string `json:"psnIDCardType,omitempty"` // 证件类型
|
||||
PsnMobile string `json:"psnMobile,omitempty"` // 手机号
|
||||
PsnIdentityVerify bool `json:"psnIdentityVerify,omitempty"` // 是否身份验证
|
||||
}
|
||||
|
||||
// AuthorizeConfig 授权配置
|
||||
type AuthorizeConfig struct {
|
||||
AuthorizedScopes []string `json:"authorizedScopes,omitempty"` // 授权范围
|
||||
}
|
||||
|
||||
// GetOrgAuthUrlResponse 获取机构认证&授权页面链接响应结构体
|
||||
type GetOrgAuthUrlResponse struct {
|
||||
Code int `json:"code"` // 响应码
|
||||
Message string `json:"message"` // 响应消息
|
||||
Data struct {
|
||||
AuthFlowId string `json:"authFlowId"` // 认证流程ID
|
||||
AuthUrl string `json:"authUrl"` // 认证链接
|
||||
AuthShortUrl string `json:"authShortUrl"` // 认证短链接
|
||||
} `json:"data"`
|
||||
}
|
||||
|
||||
// ==================== 机构认证查询相关结构体 ====================
|
||||
type OrgIDCardType string
|
||||
|
||||
const (
|
||||
OrgIDCardTypeUSCC OrgIDCardType = "CRED_ORG_USCC" // 统一社会信用代码
|
||||
OrgIDCardTypeREGCODE OrgIDCardType = "CRED_ORG_REGCODE" // 工商注册号
|
||||
)
|
||||
|
||||
// QueryOrgIdentityRequest 查询机构认证信息请求
|
||||
type QueryOrgIdentityRequest struct {
|
||||
OrgID string `json:"orgId,omitempty"` // 机构账号ID
|
||||
OrgName string `json:"orgName,omitempty"` // 组织机构名称
|
||||
OrgIDCardNum string `json:"orgIDCardNum,omitempty"` // 组织机构证件号
|
||||
OrgIDCardType OrgIDCardType `json:"orgIDCardType,omitempty"` // 组织机构证件类型,只能为OrgIDCardTypeUSCC或OrgIDCardTypeREGCODE
|
||||
}
|
||||
|
||||
// QueryOrgIdentityResponse 查询机构认证信息响应
|
||||
type QueryOrgIdentityResponse struct {
|
||||
Code int32 `json:"code"` // 业务码,0表示成功
|
||||
Message string `json:"message"` // 业务信息
|
||||
Data struct {
|
||||
RealnameStatus int32 `json:"realnameStatus"` // 实名认证状态 (0-未实名, 1-已实名)
|
||||
AuthorizeUserInfo bool `json:"authorizeUserInfo"` // 是否授权身份信息给当前应用
|
||||
OrgID string `json:"orgId"` // 机构账号ID
|
||||
OrgName string `json:"orgName"` // 机构名称
|
||||
OrgAuthMode string `json:"orgAuthMode"` // 机构实名认证方式
|
||||
OrgInfo struct {
|
||||
OrgIDCardNum string `json:"orgIDCardNum"` // 组织机构证件号
|
||||
OrgIDCardType string `json:"orgIDCardType"` // 组织机构证件号类型
|
||||
LegalRepName string `json:"legalRepName"` // 法定代表人姓名
|
||||
LegalRepIDCardNum string `json:"legalRepIDCardNum"` // 法定代表人证件号
|
||||
LegalRepIDCardType string `json:"legalRepIDCardType"` // 法定代表人证件类型
|
||||
CorporateAccount string `json:"corporateAccount"` // 机构对公账户名称
|
||||
OrgBankAccountNum string `json:"orgBankAccountNum"` // 机构对公打款银行卡号
|
||||
CnapsCode string `json:"cnapsCode"` // 机构对公打款银行联行号
|
||||
AuthorizationDownloadUrl string `json:"authorizationDownloadUrl"` // 授权委托书下载地址
|
||||
LicenseDownloadUrl string `json:"licenseDownloadUrl"` // 营业执照照片下载地址
|
||||
AdminName string `json:"adminName"` // 机构管理员姓名(脱敏)
|
||||
AdminAccount string `json:"adminAccount"` // 机构管理员联系方式(脱敏)
|
||||
} `json:"orgInfo"`
|
||||
} `json:"data"`
|
||||
}
|
||||
|
||||
// ==================== 结果结构体 ====================
|
||||
|
||||
// SignResult 签署结果结构体
|
||||
// 包含签署流程的完整结果信息
|
||||
type SignResult struct {
|
||||
FileID string `json:"fileId"` // 文件ID
|
||||
SignFlowID string `json:"signFlowId"` // 签署流程ID
|
||||
SignUrl string `json:"signUrl"` // 签署链接
|
||||
ShortUrl string `json:"shortUrl"` // 短链接
|
||||
DownloadSignedFileResult *DownloadSignedFileResponse `json:"downloadSignedFileResult,omitempty"` // 下载已签署文件结果
|
||||
QuerySignFlowDetailResult *QuerySignFlowDetailResponse `json:"querySignFlowDetailResult,omitempty"` // 查询签署流程详情结果
|
||||
}
|
||||
|
||||
// ==================== 请求结构体优化 ====================
|
||||
|
||||
// SignProcessRequest 签署流程请求结构体
|
||||
type SignProcessRequest struct {
|
||||
SignerAccount string `json:"signerAccount"` // 签署人账号(统一社会信用代码)
|
||||
SignerName string `json:"signerName"` // 签署人名称
|
||||
TransactorPhone string `json:"transactorPhone"` // 经办人手机号
|
||||
TransactorName string `json:"transactorName"` // 经办人姓名
|
||||
TransactorIDCardNum string `json:"transactorIdCardNum"` // 经办人身份证号
|
||||
TransactorMobile string `json:"transactorMobile"` // 经办人手机号
|
||||
IncludeDownloadAndQuery bool `json:"includeDownloadAndQuery"` // 是否包含下载和查询步骤
|
||||
CustomComponents map[string]string `json:"customComponents,omitempty"` // 自定义模板组件数据
|
||||
}
|
||||
|
||||
// OrgAuthUrlRequest 机构认证链接请求结构体
|
||||
type OrgAuthUrlRequest struct {
|
||||
OrgName string `json:"orgName"` // 机构名称
|
||||
OrgIDCardNum string `json:"orgIdCardNum"` // 机构证件号
|
||||
LegalRepName string `json:"legalRepName"` // 法定代表人姓名
|
||||
LegalRepIDCardNum string `json:"legalRepIdCardNum"` // 法定代表人身份证号
|
||||
TransactorPhone string `json:"transactorPhone"` // 经办人手机号
|
||||
TransactorName string `json:"transactorName"` // 经办人姓名
|
||||
TransactorIDCardNum string `json:"transactorIdCardNum"` // 经办人身份证号
|
||||
TransactorMobile string `json:"transactorMobile"` // 经办人手机号
|
||||
}
|
||||
|
||||
// CreateSignFlowRequest 创建签署流程请求结构体
|
||||
type CreateSignFlowRequest struct {
|
||||
FileID string `json:"fileId"` // 文件ID
|
||||
SignerAccount string `json:"signerAccount"` // 签署人账号
|
||||
SignerName string `json:"signerName"` // 签署人名称
|
||||
TransactorPhone string `json:"transactorPhone"` // 经办人手机号
|
||||
TransactorName string `json:"transactorName"` // 经办人姓名
|
||||
TransactorIDCardNum string `json:"transactorIdCardNum"` // 经办人身份证号
|
||||
}
|
||||
|
||||
// SimplifiedGetSignUrlRequest 简化获取签署链接请求结构体 (避免与现有冲突)
|
||||
type SimplifiedGetSignUrlRequest struct {
|
||||
SignFlowID string `json:"signFlowId"` // 签署流程ID
|
||||
PsnAccount string `json:"psnAccount"` // 个人账号(手机号)
|
||||
OrgName string `json:"orgName"` // 机构名称
|
||||
}
|
||||
|
||||
// SimplifiedFillTemplateRequest 简化填写模板请求结构体
|
||||
type SimplifiedFillTemplateRequest struct {
|
||||
Components []Component `json:"components"` // 填写组件列表
|
||||
}
|
||||
104
internal/shared/esign/utils.go
Normal file
104
internal/shared/esign/utils.go
Normal file
@@ -0,0 +1,104 @@
|
||||
package esign
|
||||
|
||||
import (
|
||||
"crypto/hmac"
|
||||
"crypto/md5"
|
||||
"crypto/sha256"
|
||||
"encoding/base64"
|
||||
"strconv"
|
||||
"time"
|
||||
)
|
||||
|
||||
// generateSignature 生成e签宝API请求签名
|
||||
// 使用HMAC-SHA256算法对请求参数进行签名
|
||||
//
|
||||
// 参数说明:
|
||||
// - appSecret: 应用密钥
|
||||
// - httpMethod: HTTP方法(GET、POST等)
|
||||
// - accept: Accept头值
|
||||
// - contentMD5: 请求体MD5值
|
||||
// - contentType: Content-Type头值
|
||||
// - date: Date头值
|
||||
// - headers: 自定义头部信息
|
||||
// - pathAndParameters: 请求路径和参数
|
||||
//
|
||||
// 返回: Base64编码的签名字符串
|
||||
func generateSignature(appSecret, httpMethod, accept, contentMD5, contentType, date, headers, pathAndParameters string) string {
|
||||
// 构建待签名字符串,按照e签宝API规范拼接
|
||||
signStr := httpMethod + "\n" + accept + "\n" + contentMD5 + "\n" + contentType + "\n" + date + "\n" + headers + pathAndParameters
|
||||
|
||||
// 使用HMAC-SHA256计算签名
|
||||
h := hmac.New(sha256.New, []byte(appSecret))
|
||||
h.Write([]byte(signStr))
|
||||
digestBytes := h.Sum(nil)
|
||||
|
||||
// 对摘要结果进行Base64编码
|
||||
signature := base64.StdEncoding.EncodeToString(digestBytes)
|
||||
|
||||
return signature
|
||||
}
|
||||
|
||||
// generateNonce 生成随机字符串
|
||||
// 使用当前时间的纳秒数作为随机字符串
|
||||
//
|
||||
// 返回: 纳秒时间戳字符串
|
||||
func generateNonce() string {
|
||||
return strconv.FormatInt(time.Now().UnixNano(), 10)
|
||||
}
|
||||
|
||||
// getContentMD5 计算请求体的MD5值
|
||||
// 对请求体进行MD5哈希计算,然后进行Base64编码
|
||||
//
|
||||
// 参数:
|
||||
// - body: 请求体字节数组
|
||||
//
|
||||
// 返回: Base64编码的MD5值
|
||||
func getContentMD5(body []byte) string {
|
||||
md5Sum := md5.Sum(body)
|
||||
return base64.StdEncoding.EncodeToString(md5Sum[:])
|
||||
}
|
||||
|
||||
// getCurrentTimestamp 获取当前时间戳(毫秒)
|
||||
//
|
||||
// 返回: 毫秒级时间戳字符串
|
||||
func getCurrentTimestamp() string {
|
||||
return strconv.FormatInt(time.Now().UnixNano()/1e6, 10)
|
||||
}
|
||||
|
||||
// getCurrentDate 获取当前UTC时间字符串
|
||||
// 格式: "Mon, 02 Jan 2006 15:04:05 GMT"
|
||||
//
|
||||
// 返回: RFC1123格式的UTC时间字符串
|
||||
func getCurrentDate() string {
|
||||
return time.Now().UTC().Format("Mon, 02 Jan 2006 15:04:05 GMT")
|
||||
}
|
||||
|
||||
// formatDateForTemplate 格式化日期用于模板填写
|
||||
// 格式: "2006年01月02日"
|
||||
//
|
||||
// 返回: 中文格式的日期字符串
|
||||
func formatDateForTemplate() string {
|
||||
return time.Now().Format("2006年01月02日")
|
||||
}
|
||||
|
||||
// generateFileName 生成带时间戳的文件名
|
||||
//
|
||||
// 参数:
|
||||
// - baseName: 基础文件名
|
||||
// - extension: 文件扩展名
|
||||
//
|
||||
// 返回: 带时间戳的文件名
|
||||
func generateFileName(baseName, extension string) string {
|
||||
timestamp := time.Now().Format("20060102_150405")
|
||||
return baseName + "_" + timestamp + "." + extension
|
||||
}
|
||||
|
||||
// calculateExpireTime 计算过期时间戳
|
||||
//
|
||||
// 参数:
|
||||
// - days: 过期天数
|
||||
//
|
||||
// 返回: 毫秒级时间戳
|
||||
func calculateExpireTime(days int) int64 {
|
||||
return time.Now().AddDate(0, 0, days).UnixMilli()
|
||||
}
|
||||
@@ -1,236 +0,0 @@
|
||||
package http
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"tyapi-server/internal/shared/interfaces"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/go-playground/locales/zh"
|
||||
ut "github.com/go-playground/universal-translator"
|
||||
"github.com/go-playground/validator/v10"
|
||||
zh_translations "github.com/go-playground/validator/v10/translations/zh"
|
||||
)
|
||||
|
||||
// RequestValidatorZh 中文验证器实现
|
||||
type RequestValidatorZh struct {
|
||||
response interfaces.ResponseBuilder
|
||||
translator ut.Translator
|
||||
}
|
||||
|
||||
// NewRequestValidatorZh 创建支持中文翻译的请求验证器
|
||||
func NewRequestValidatorZh(response interfaces.ResponseBuilder) interfaces.RequestValidator {
|
||||
// 创建中文locale
|
||||
zhLocale := zh.New()
|
||||
uni := ut.New(zhLocale, zhLocale)
|
||||
|
||||
// 获取中文翻译器
|
||||
trans, _ := uni.GetTranslator("zh")
|
||||
|
||||
// 注册官方中文翻译
|
||||
zh_translations.RegisterDefaultTranslations(validator.New(), trans)
|
||||
|
||||
// 注册自定义翻译
|
||||
registerCustomTranslations(trans)
|
||||
|
||||
return &RequestValidatorZh{
|
||||
response: response,
|
||||
translator: trans,
|
||||
}
|
||||
}
|
||||
|
||||
// registerCustomTranslations 注册自定义翻译
|
||||
func registerCustomTranslations(trans ut.Translator) {
|
||||
// 自定义 eqfield 翻译(更友好的提示)
|
||||
_ = trans.Add("eqfield", "{0}必须与{1}一致", true)
|
||||
|
||||
// 自定义 required 翻译
|
||||
_ = trans.Add("required", "{0}不能为空", true)
|
||||
|
||||
// 自定义 min 翻译
|
||||
_ = trans.Add("min", "{0}长度不能少于{1}位", true)
|
||||
|
||||
// 自定义 max 翻译
|
||||
_ = trans.Add("max", "{0}长度不能超过{1}位", true)
|
||||
|
||||
// 自定义 len 翻译
|
||||
_ = trans.Add("len", "{0}长度必须为{1}位", true)
|
||||
|
||||
// 自定义 email 翻译
|
||||
_ = trans.Add("email", "{0}必须是有效的邮箱地址", true)
|
||||
|
||||
// 自定义手机号翻译
|
||||
_ = trans.Add("phone", "{0}必须是有效的手机号", true)
|
||||
|
||||
// 自定义用户名翻译
|
||||
_ = trans.Add("username", "{0}格式不正确,只能包含字母、数字、下划线,且不能以数字开头", true)
|
||||
|
||||
// 自定义强密码翻译
|
||||
_ = trans.Add("strong_password", "{0}强度不足,必须包含大小写字母和数字,且不少于8位", true)
|
||||
}
|
||||
|
||||
// Validate 验证请求体
|
||||
func (v *RequestValidatorZh) Validate(c *gin.Context, dto interface{}) error {
|
||||
// 直接使用 Gin 的绑定和验证
|
||||
return v.BindAndValidate(c, dto)
|
||||
}
|
||||
|
||||
// ValidateQuery 验证查询参数
|
||||
func (v *RequestValidatorZh) ValidateQuery(c *gin.Context, dto interface{}) error {
|
||||
if err := c.ShouldBindQuery(dto); err != nil {
|
||||
// 处理查询参数验证错误
|
||||
if _, ok := err.(validator.ValidationErrors); ok {
|
||||
validationErrors := v.formatValidationErrorsZh(err)
|
||||
v.response.ValidationError(c, validationErrors)
|
||||
} else {
|
||||
v.response.BadRequest(c, "查询参数格式错误", err.Error())
|
||||
}
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// ValidateParam 验证路径参数
|
||||
func (v *RequestValidatorZh) ValidateParam(c *gin.Context, dto interface{}) error {
|
||||
if err := c.ShouldBindUri(dto); err != nil {
|
||||
// 处理路径参数验证错误
|
||||
if _, ok := err.(validator.ValidationErrors); ok {
|
||||
validationErrors := v.formatValidationErrorsZh(err)
|
||||
v.response.ValidationError(c, validationErrors)
|
||||
} else {
|
||||
v.response.BadRequest(c, "路径参数格式错误", err.Error())
|
||||
}
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// BindAndValidate 绑定并验证请求
|
||||
func (v *RequestValidatorZh) BindAndValidate(c *gin.Context, dto interface{}) error {
|
||||
// 绑定请求体(Gin 会自动进行 binding 标签验证)
|
||||
if err := c.ShouldBindJSON(dto); err != nil {
|
||||
// 处理 Gin binding 验证错误
|
||||
if _, ok := err.(validator.ValidationErrors); ok {
|
||||
// 所有验证错误都使用 422 状态码
|
||||
validationErrors := v.formatValidationErrorsZh(err)
|
||||
v.response.ValidationError(c, validationErrors)
|
||||
} else {
|
||||
v.response.BadRequest(c, "请求体格式错误", err.Error())
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// formatValidationErrorsZh 格式化验证错误(中文翻译版)
|
||||
func (v *RequestValidatorZh) formatValidationErrorsZh(err error) map[string][]string {
|
||||
errors := make(map[string][]string)
|
||||
|
||||
if validationErrors, ok := err.(validator.ValidationErrors); ok {
|
||||
for _, fieldError := range validationErrors {
|
||||
fieldName := v.getFieldNameZh(fieldError)
|
||||
|
||||
// 获取友好的中文错误消息
|
||||
errorMessage := v.getFriendlyErrorMessage(fieldError)
|
||||
|
||||
if _, exists := errors[fieldName]; !exists {
|
||||
errors[fieldName] = []string{}
|
||||
}
|
||||
errors[fieldName] = append(errors[fieldName], errorMessage)
|
||||
}
|
||||
}
|
||||
|
||||
return errors
|
||||
}
|
||||
|
||||
// getFriendlyErrorMessage 获取友好的中文错误消息
|
||||
func (v *RequestValidatorZh) getFriendlyErrorMessage(fieldError validator.FieldError) string {
|
||||
field := fieldError.Field()
|
||||
tag := fieldError.Tag()
|
||||
param := fieldError.Param()
|
||||
|
||||
fieldDisplayName := v.getFieldDisplayName(field)
|
||||
|
||||
// 优先使用官方翻译器
|
||||
errorMessage := fieldError.Translate(v.translator)
|
||||
|
||||
// 如果官方翻译成功且不是英文,使用官方翻译
|
||||
if errorMessage != fieldError.Error() {
|
||||
// 替换字段名为中文
|
||||
if fieldDisplayName != fieldError.Field() {
|
||||
errorMessage = strings.ReplaceAll(errorMessage, fieldError.Field(), fieldDisplayName)
|
||||
}
|
||||
return errorMessage
|
||||
}
|
||||
|
||||
// 回退到自定义翻译
|
||||
switch tag {
|
||||
case "required":
|
||||
return fmt.Sprintf("%s不能为空", fieldDisplayName)
|
||||
case "email":
|
||||
return fmt.Sprintf("%s必须是有效的邮箱地址", fieldDisplayName)
|
||||
case "min":
|
||||
return fmt.Sprintf("%s长度不能少于%s位", fieldDisplayName, param)
|
||||
case "max":
|
||||
return fmt.Sprintf("%s长度不能超过%s位", fieldDisplayName, param)
|
||||
case "len":
|
||||
return fmt.Sprintf("%s长度必须为%s位", fieldDisplayName, param)
|
||||
case "eqfield":
|
||||
paramDisplayName := v.getFieldDisplayName(param)
|
||||
return fmt.Sprintf("%s必须与%s一致", fieldDisplayName, paramDisplayName)
|
||||
case "phone":
|
||||
return fmt.Sprintf("%s必须是有效的手机号", fieldDisplayName)
|
||||
case "username":
|
||||
return fmt.Sprintf("%s格式不正确,只能包含字母、数字、下划线,且不能以数字开头", fieldDisplayName)
|
||||
case "strong_password":
|
||||
return fmt.Sprintf("%s强度不足,必须包含大小写字母和数字,且不少于8位", fieldDisplayName)
|
||||
default:
|
||||
// 默认错误消息
|
||||
return fmt.Sprintf("%s格式不正确", fieldDisplayName)
|
||||
}
|
||||
}
|
||||
|
||||
// getFieldNameZh 获取字段名(JSON标签优先)
|
||||
func (v *RequestValidatorZh) getFieldNameZh(fieldError validator.FieldError) string {
|
||||
fieldName := fieldError.Field()
|
||||
return v.toSnakeCase(fieldName)
|
||||
}
|
||||
|
||||
// getFieldDisplayName 获取字段显示名称(中文)
|
||||
func (v *RequestValidatorZh) getFieldDisplayName(field string) string {
|
||||
fieldNames := map[string]string{
|
||||
"phone": "手机号",
|
||||
"password": "密码",
|
||||
"confirm_password": "确认密码",
|
||||
"old_password": "原密码",
|
||||
"new_password": "新密码",
|
||||
"confirm_new_password": "确认新密码",
|
||||
"code": "验证码",
|
||||
"username": "用户名",
|
||||
"email": "邮箱",
|
||||
"display_name": "显示名称",
|
||||
"scene": "使用场景",
|
||||
"Password": "密码",
|
||||
"NewPassword": "新密码",
|
||||
"ConfirmPassword": "确认密码",
|
||||
}
|
||||
|
||||
if displayName, exists := fieldNames[field]; exists {
|
||||
return displayName
|
||||
}
|
||||
return field
|
||||
}
|
||||
|
||||
// toSnakeCase 转换为snake_case
|
||||
func (v *RequestValidatorZh) toSnakeCase(str string) string {
|
||||
var result strings.Builder
|
||||
for i, r := range str {
|
||||
if i > 0 && (r >= 'A' && r <= 'Z') {
|
||||
result.WriteRune('_')
|
||||
}
|
||||
result.WriteRune(r)
|
||||
}
|
||||
return strings.ToLower(result.String())
|
||||
}
|
||||
@@ -24,7 +24,7 @@ type BaseRepository interface {
|
||||
Restore(ctx context.Context, id string) error
|
||||
}
|
||||
|
||||
// Repository 通用仓储接口,支持泛型
|
||||
// Repository 仓储接口
|
||||
type Repository[T any] interface {
|
||||
BaseRepository
|
||||
|
||||
@@ -39,11 +39,8 @@ type Repository[T any] interface {
|
||||
UpdateBatch(ctx context.Context, entities []T) error
|
||||
DeleteBatch(ctx context.Context, ids []string) error
|
||||
|
||||
// 查询操作
|
||||
// 列表查询
|
||||
List(ctx context.Context, options ListOptions) ([]T, error)
|
||||
|
||||
// 事务支持
|
||||
WithTx(tx interface{}) Repository[T]
|
||||
}
|
||||
|
||||
// ListOptions 列表查询选项
|
||||
|
||||
@@ -31,6 +31,11 @@ func (m *JWTAuthMiddleware) GetName() string {
|
||||
return "jwt_auth"
|
||||
}
|
||||
|
||||
// GetExpiresIn 返回JWT过期时间
|
||||
func (m *JWTAuthMiddleware) GetExpiresIn() time.Duration {
|
||||
return m.config.JWT.ExpiresIn
|
||||
}
|
||||
|
||||
// GetPriority 返回中间件优先级
|
||||
func (m *JWTAuthMiddleware) GetPriority() int {
|
||||
return 60 // 中等优先级,在日志之后,业务处理之前
|
||||
@@ -74,6 +79,8 @@ func (m *JWTAuthMiddleware) Handle() gin.HandlerFunc {
|
||||
c.Set("user_id", claims.UserID)
|
||||
c.Set("username", claims.Username)
|
||||
c.Set("email", claims.Email)
|
||||
c.Set("phone", claims.Phone)
|
||||
c.Set("user_type", claims.UserType)
|
||||
c.Set("token_claims", claims)
|
||||
|
||||
c.Next()
|
||||
@@ -90,6 +97,8 @@ type JWTClaims struct {
|
||||
UserID string `json:"user_id"`
|
||||
Username string `json:"username"`
|
||||
Email string `json:"email"`
|
||||
Phone string `json:"phone"`
|
||||
UserType string `json:"user_type"` // 新增:用户类型
|
||||
jwt.RegisteredClaims
|
||||
}
|
||||
|
||||
@@ -128,13 +137,15 @@ func (m *JWTAuthMiddleware) respondUnauthorized(c *gin.Context, message string)
|
||||
}
|
||||
|
||||
// GenerateToken 生成JWT token
|
||||
func (m *JWTAuthMiddleware) GenerateToken(userID, username, email string) (string, error) {
|
||||
func (m *JWTAuthMiddleware) GenerateToken(userID, phone, email, userType string) (string, error) {
|
||||
now := time.Now()
|
||||
|
||||
claims := &JWTClaims{
|
||||
UserID: userID,
|
||||
Username: username,
|
||||
Username: phone, // 普通用户用手机号,管理员用用户名
|
||||
Email: email,
|
||||
Phone: phone,
|
||||
UserType: userType, // 新增:用户类型
|
||||
RegisteredClaims: jwt.RegisteredClaims{
|
||||
Issuer: "tyapi-server",
|
||||
Subject: userID,
|
||||
@@ -249,6 +260,8 @@ func (m *OptionalAuthMiddleware) Handle() gin.HandlerFunc {
|
||||
c.Set("user_id", claims.UserID)
|
||||
c.Set("username", claims.Username)
|
||||
c.Set("email", claims.Email)
|
||||
c.Set("phone", claims.Phone)
|
||||
c.Set("user_type", claims.UserType)
|
||||
c.Set("token_claims", claims)
|
||||
|
||||
c.Next()
|
||||
@@ -259,3 +272,108 @@ func (m *OptionalAuthMiddleware) Handle() gin.HandlerFunc {
|
||||
func (m *OptionalAuthMiddleware) IsGlobal() bool {
|
||||
return false
|
||||
}
|
||||
|
||||
// AdminAuthMiddleware 管理员认证中间件
|
||||
type AdminAuthMiddleware struct {
|
||||
jwtAuth *JWTAuthMiddleware
|
||||
logger *zap.Logger
|
||||
}
|
||||
|
||||
// NewAdminAuthMiddleware 创建管理员认证中间件
|
||||
func NewAdminAuthMiddleware(jwtAuth *JWTAuthMiddleware, logger *zap.Logger) *AdminAuthMiddleware {
|
||||
return &AdminAuthMiddleware{
|
||||
jwtAuth: jwtAuth,
|
||||
logger: logger,
|
||||
}
|
||||
}
|
||||
|
||||
// GetName 返回中间件名称
|
||||
func (m *AdminAuthMiddleware) GetName() string {
|
||||
return "admin_auth"
|
||||
}
|
||||
|
||||
// GetPriority 返回中间件优先级
|
||||
func (m *AdminAuthMiddleware) GetPriority() int {
|
||||
return 60 // 与JWT认证中间件相同
|
||||
}
|
||||
|
||||
// Handle 管理员认证处理
|
||||
func (m *AdminAuthMiddleware) Handle() gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
// 首先进行JWT认证
|
||||
authHeader := c.GetHeader("Authorization")
|
||||
if authHeader == "" {
|
||||
m.respondUnauthorized(c, "缺少认证头部")
|
||||
return
|
||||
}
|
||||
|
||||
// 检查Bearer前缀
|
||||
const bearerPrefix = "Bearer "
|
||||
if !strings.HasPrefix(authHeader, bearerPrefix) {
|
||||
m.respondUnauthorized(c, "认证头部格式无效")
|
||||
return
|
||||
}
|
||||
|
||||
// 提取token
|
||||
tokenString := authHeader[len(bearerPrefix):]
|
||||
if tokenString == "" {
|
||||
m.respondUnauthorized(c, "缺少认证令牌")
|
||||
return
|
||||
}
|
||||
|
||||
// 验证token
|
||||
claims, err := m.jwtAuth.validateToken(tokenString)
|
||||
if err != nil {
|
||||
m.logger.Warn("无效的认证令牌",
|
||||
zap.Error(err),
|
||||
zap.String("request_id", c.GetString("request_id")))
|
||||
m.respondUnauthorized(c, "认证令牌无效")
|
||||
return
|
||||
}
|
||||
|
||||
// 检查用户类型是否为管理员
|
||||
if claims.UserType != "admin" {
|
||||
m.respondForbidden(c, "需要管理员权限")
|
||||
return
|
||||
}
|
||||
|
||||
// 设置用户信息到上下文
|
||||
c.Set("user_id", claims.UserID)
|
||||
c.Set("username", claims.Username)
|
||||
c.Set("email", claims.Email)
|
||||
c.Set("phone", claims.Phone)
|
||||
c.Set("user_type", claims.UserType)
|
||||
c.Set("token_claims", claims)
|
||||
|
||||
c.Next()
|
||||
}
|
||||
}
|
||||
|
||||
// IsGlobal 是否为全局中间件
|
||||
func (m *AdminAuthMiddleware) IsGlobal() bool {
|
||||
return false
|
||||
}
|
||||
|
||||
// respondForbidden 返回禁止访问响应
|
||||
func (m *AdminAuthMiddleware) respondForbidden(c *gin.Context, message string) {
|
||||
c.JSON(http.StatusForbidden, gin.H{
|
||||
"success": false,
|
||||
"message": "权限不足",
|
||||
"error": message,
|
||||
"request_id": c.GetString("request_id"),
|
||||
"timestamp": time.Now().Unix(),
|
||||
})
|
||||
c.Abort()
|
||||
}
|
||||
|
||||
// respondUnauthorized 返回未授权响应
|
||||
func (m *AdminAuthMiddleware) respondUnauthorized(c *gin.Context, message string) {
|
||||
c.JSON(http.StatusUnauthorized, gin.H{
|
||||
"success": false,
|
||||
"message": "认证失败",
|
||||
"error": message,
|
||||
"request_id": c.GetString("request_id"),
|
||||
"timestamp": time.Now().Unix(),
|
||||
})
|
||||
c.Abort()
|
||||
}
|
||||
|
||||
@@ -610,3 +610,121 @@ func (sm *SagaManager) Shutdown(ctx context.Context) error {
|
||||
sm.logger.Info("Saga manager service shutdown")
|
||||
return nil
|
||||
}
|
||||
|
||||
// ==================== Saga构建器 ====================
|
||||
|
||||
// StepBuilder Saga步骤构建器
|
||||
type StepBuilder struct {
|
||||
name string
|
||||
action func(ctx context.Context, data interface{}) error
|
||||
compensate func(ctx context.Context, data interface{}) error
|
||||
timeout time.Duration
|
||||
maxRetries int
|
||||
}
|
||||
|
||||
// Step 创建步骤构建器
|
||||
func Step(name string) *StepBuilder {
|
||||
return &StepBuilder{
|
||||
name: name,
|
||||
timeout: 30 * time.Second,
|
||||
maxRetries: 3,
|
||||
}
|
||||
}
|
||||
|
||||
// Action 设置正向操作
|
||||
func (sb *StepBuilder) Action(action func(ctx context.Context, data interface{}) error) *StepBuilder {
|
||||
sb.action = action
|
||||
return sb
|
||||
}
|
||||
|
||||
// Compensate 设置补偿操作
|
||||
func (sb *StepBuilder) Compensate(compensate func(ctx context.Context, data interface{}) error) *StepBuilder {
|
||||
sb.compensate = compensate
|
||||
return sb
|
||||
}
|
||||
|
||||
// Timeout 设置超时时间
|
||||
func (sb *StepBuilder) Timeout(timeout time.Duration) *StepBuilder {
|
||||
sb.timeout = timeout
|
||||
return sb
|
||||
}
|
||||
|
||||
// MaxRetries 设置最大重试次数
|
||||
func (sb *StepBuilder) MaxRetries(maxRetries int) *StepBuilder {
|
||||
sb.maxRetries = maxRetries
|
||||
return sb
|
||||
}
|
||||
|
||||
// Build 构建Saga步骤
|
||||
func (sb *StepBuilder) Build() *SagaStep {
|
||||
return &SagaStep{
|
||||
Name: sb.name,
|
||||
Action: sb.action,
|
||||
Compensate: sb.compensate,
|
||||
Status: StepPending,
|
||||
MaxRetries: sb.maxRetries,
|
||||
Timeout: sb.timeout,
|
||||
}
|
||||
}
|
||||
|
||||
// SagaBuilder Saga构建器
|
||||
type SagaBuilder struct {
|
||||
manager *SagaManager
|
||||
saga *Saga
|
||||
steps []*SagaStep
|
||||
}
|
||||
|
||||
// NewSagaBuilder 创建Saga构建器
|
||||
func NewSagaBuilder(manager *SagaManager, id, name string) *SagaBuilder {
|
||||
saga := manager.CreateSaga(id, name)
|
||||
return &SagaBuilder{
|
||||
manager: manager,
|
||||
saga: saga,
|
||||
steps: make([]*SagaStep, 0),
|
||||
}
|
||||
}
|
||||
|
||||
// AddStep 添加步骤
|
||||
func (sb *SagaBuilder) AddStep(step *SagaStep) *SagaBuilder {
|
||||
sb.steps = append(sb.steps, step)
|
||||
sb.saga.AddStepWithConfig(step.Name, step.Action, step.Compensate, step.MaxRetries, step.Timeout)
|
||||
return sb
|
||||
}
|
||||
|
||||
// AddSteps 批量添加步骤
|
||||
func (sb *SagaBuilder) AddSteps(steps ...*SagaStep) *SagaBuilder {
|
||||
for _, step := range steps {
|
||||
sb.AddStep(step)
|
||||
}
|
||||
return sb
|
||||
}
|
||||
|
||||
// Execute 执行Saga
|
||||
func (sb *SagaBuilder) Execute(ctx context.Context, data interface{}) error {
|
||||
return sb.saga.Execute(ctx, data)
|
||||
}
|
||||
|
||||
// GetSaga 获取Saga实例
|
||||
func (sb *SagaBuilder) GetSaga() *Saga {
|
||||
return sb.saga
|
||||
}
|
||||
|
||||
// 便捷函数
|
||||
|
||||
// CreateSaga 快速创建Saga
|
||||
func CreateSaga(manager *SagaManager, name string) *SagaBuilder {
|
||||
id := fmt.Sprintf("%s_%d", name, time.Now().Unix())
|
||||
return NewSagaBuilder(manager, id, name)
|
||||
}
|
||||
|
||||
// ExecuteSaga 快速执行Saga
|
||||
func ExecuteSaga(manager *SagaManager, name string, steps []*SagaStep, data interface{}, logger *zap.Logger) error {
|
||||
saga := CreateSaga(manager, name)
|
||||
saga.AddSteps(steps...)
|
||||
|
||||
logger.Info("开始执行Saga",
|
||||
zap.String("saga_name", name),
|
||||
zap.Int("steps_count", len(steps)))
|
||||
|
||||
return saga.Execute(context.Background(), data)
|
||||
}
|
||||
|
||||
230
internal/shared/validator/README.md
Normal file
230
internal/shared/validator/README.md
Normal file
@@ -0,0 +1,230 @@
|
||||
# Validator 验证器包
|
||||
|
||||
这是一个功能完整的验证器包,提供了HTTP请求验证和业务逻辑验证的完整解决方案。
|
||||
|
||||
## 📁 包结构
|
||||
|
||||
```
|
||||
internal/shared/validator/
|
||||
├── validator.go # HTTP请求验证器主逻辑
|
||||
├── custom_validators.go # 自定义验证器实现
|
||||
├── translations.go # 中文翻译
|
||||
├── business.go # 业务逻辑验证接口
|
||||
└── README.md # 使用说明
|
||||
```
|
||||
|
||||
## 🚀 特性
|
||||
|
||||
### 1. HTTP请求验证
|
||||
- 自动绑定和验证请求体、查询参数、路径参数
|
||||
- 中文错误消息
|
||||
- 集成到Gin框架
|
||||
- 统一的错误响应格式
|
||||
|
||||
### 2. 业务逻辑验证
|
||||
- 独立的业务验证器
|
||||
- 可在任何地方调用的验证方法
|
||||
- 丰富的预定义验证规则
|
||||
|
||||
### 3. 自定义验证规则
|
||||
- 手机号验证 (`phone`)
|
||||
- 强密码验证 (`strong_password`)
|
||||
- 用户名验证 (`username`)
|
||||
- 统一社会信用代码验证 (`social_credit_code`)
|
||||
- 身份证号验证 (`id_card`)
|
||||
- UUID验证 (`uuid`)
|
||||
- URL验证 (`url`)
|
||||
- 产品代码验证 (`product_code`)
|
||||
- 价格验证 (`price`)
|
||||
- 排序方向验证 (`sort_order`)
|
||||
|
||||
## 📖 使用方法
|
||||
|
||||
### 1. HTTP请求验证
|
||||
|
||||
在Handler中使用:
|
||||
|
||||
```go
|
||||
type UserHandler struct {
|
||||
validator interfaces.RequestValidator
|
||||
// ... 其他依赖
|
||||
}
|
||||
|
||||
func (h *UserHandler) Register(c *gin.Context) {
|
||||
var cmd commands.RegisterUserCommand
|
||||
|
||||
// 自动绑定和验证请求体
|
||||
if err := h.validator.BindAndValidate(c, &cmd); err != nil {
|
||||
return // 验证失败会自动返回错误响应
|
||||
}
|
||||
|
||||
// 验证查询参数
|
||||
var query queries.UserListQuery
|
||||
if err := h.validator.ValidateQuery(c, &query); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
// 验证路径参数
|
||||
var param queries.UserIDParam
|
||||
if err := h.validator.ValidateParam(c, ¶m); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
// 继续业务逻辑...
|
||||
}
|
||||
```
|
||||
|
||||
### 2. DTO定义
|
||||
|
||||
在DTO中使用验证标签:
|
||||
|
||||
```go
|
||||
type RegisterUserCommand struct {
|
||||
Phone string `json:"phone" binding:"required,phone"`
|
||||
Password string `json:"password" binding:"required,strong_password"`
|
||||
ConfirmPassword string `json:"confirm_password" binding:"required,eqfield=Password"`
|
||||
Email string `json:"email" binding:"omitempty,email"`
|
||||
Username string `json:"username" binding:"required,username"`
|
||||
}
|
||||
|
||||
type EnterpriseInfoCommand struct {
|
||||
CompanyName string `json:"company_name" binding:"required,min=2,max=100"`
|
||||
UnifiedSocialCode string `json:"unified_social_code" binding:"required,social_credit_code"`
|
||||
LegalPersonName string `json:"legal_person_name" binding:"required,min=2,max=20"`
|
||||
LegalPersonID string `json:"legal_person_id" binding:"required,id_card"`
|
||||
LegalPersonPhone string `json:"legal_person_phone" binding:"required,phone"`
|
||||
}
|
||||
|
||||
type ProductCommand struct {
|
||||
Name string `json:"name" binding:"required,min=2,max=100"`
|
||||
Code string `json:"code" binding:"required,product_code"`
|
||||
Price float64 `json:"price" binding:"price,min=0"`
|
||||
CategoryID string `json:"category_id" binding:"required,uuid"`
|
||||
WebsiteURL string `json:"website_url" binding:"omitempty,url"`
|
||||
}
|
||||
```
|
||||
|
||||
### 3. 业务逻辑验证
|
||||
|
||||
在Service中使用:
|
||||
|
||||
```go
|
||||
import "tyapi-server/internal/shared/validator"
|
||||
|
||||
type UserService struct {
|
||||
businessValidator *validator.BusinessValidator
|
||||
}
|
||||
|
||||
func NewUserService() *UserService {
|
||||
return &UserService{
|
||||
businessValidator: validator.NewBusinessValidator(),
|
||||
}
|
||||
}
|
||||
|
||||
func (s *UserService) ValidateUserData(phone, password string) error {
|
||||
// 验证手机号
|
||||
if err := s.businessValidator.ValidatePhone(phone); err != nil {
|
||||
return fmt.Errorf("手机号验证失败: %w", err)
|
||||
}
|
||||
|
||||
// 验证密码强度
|
||||
if err := s.businessValidator.ValidatePassword(password); err != nil {
|
||||
return fmt.Errorf("密码验证失败: %w", err)
|
||||
}
|
||||
|
||||
// 验证结构体
|
||||
userData := UserData{Phone: phone, Password: password}
|
||||
if err := s.businessValidator.ValidateStruct(userData); err != nil {
|
||||
return fmt.Errorf("用户数据验证失败: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *UserService) ValidateEnterpriseInfo(code, idCard string) error {
|
||||
// 验证统一社会信用代码
|
||||
if err := s.businessValidator.ValidateSocialCreditCode(code); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// 验证身份证号
|
||||
if err := s.businessValidator.ValidateIDCard(idCard); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
```
|
||||
|
||||
## 🔧 可用的验证规则
|
||||
|
||||
### 标准验证规则
|
||||
- `required` - 必填
|
||||
- `omitempty` - 可为空
|
||||
- `min=n` - 最小长度/值
|
||||
- `max=n` - 最大长度/值
|
||||
- `len=n` - 固定长度
|
||||
- `email` - 邮箱格式
|
||||
- `oneof=a b c` - 枚举值
|
||||
- `eqfield=Field` - 字段相等
|
||||
- `gt=n` - 大于某值
|
||||
|
||||
### 自定义验证规则
|
||||
- `phone` - 中国手机号 (1[3-9]xxxxxxxxx)
|
||||
- `strong_password` - 强密码 (8位以上,包含大小写字母和数字)
|
||||
- `username` - 用户名 (字母开头,3-20位字母数字下划线)
|
||||
- `social_credit_code` - 统一社会信用代码 (18位)
|
||||
- `id_card` - 身份证号 (18位)
|
||||
- `uuid` - UUID格式
|
||||
- `url` - URL格式
|
||||
- `product_code` - 产品代码 (3-50位字母数字下划线连字符)
|
||||
- `price` - 价格 (非负数)
|
||||
- `sort_order` - 排序方向 (asc/desc)
|
||||
|
||||
## 🌐 错误消息
|
||||
|
||||
所有错误消息都已本地化为中文:
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 422,
|
||||
"message": "请求参数验证失败",
|
||||
"data": null,
|
||||
"errors": {
|
||||
"phone": ["手机号必须是有效的手机号"],
|
||||
"password": ["密码强度不足,必须包含大小写字母和数字,且不少于8位"],
|
||||
"confirm_password": ["确认密码必须与密码一致"]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 🔄 依赖注入
|
||||
|
||||
在 `container.go` 中已配置:
|
||||
|
||||
```go
|
||||
fx.Provide(
|
||||
validator.NewRequestValidator, // HTTP请求验证器
|
||||
),
|
||||
```
|
||||
|
||||
业务验证器可以在需要时创建:
|
||||
|
||||
```go
|
||||
bv := validator.NewBusinessValidator()
|
||||
```
|
||||
|
||||
## 📝 最佳实践
|
||||
|
||||
1. **DTO验证**: 在DTO中使用binding标签进行声明式验证
|
||||
2. **业务验证**: 在业务逻辑中使用BusinessValidator进行程序化验证
|
||||
3. **错误处理**: 验证错误会自动返回统一格式的HTTP响应
|
||||
4. **性能**: 验证器实例可以复用,建议在依赖注入中管理
|
||||
|
||||
## 🧪 测试示例
|
||||
|
||||
参考 `examples/validator_usage.go` 文件中的完整使用示例。
|
||||
|
||||
---
|
||||
|
||||
这个验证器包提供了完整的验证解决方案,既可以用于HTTP请求的自动验证,也可以在业务逻辑中进行程序化验证,确保数据的完整性和正确性。
|
||||
239
internal/shared/validator/business.go
Normal file
239
internal/shared/validator/business.go
Normal file
@@ -0,0 +1,239 @@
|
||||
package validator
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/url"
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
"github.com/go-playground/validator/v10"
|
||||
)
|
||||
|
||||
// BusinessValidator 业务验证器
|
||||
type BusinessValidator struct {
|
||||
validator *validator.Validate
|
||||
}
|
||||
|
||||
// NewBusinessValidator 创建业务验证器
|
||||
func NewBusinessValidator() *BusinessValidator {
|
||||
validate := validator.New()
|
||||
RegisterCustomValidators(validate)
|
||||
|
||||
return &BusinessValidator{
|
||||
validator: validate,
|
||||
}
|
||||
}
|
||||
|
||||
// ValidateStruct 验证结构体
|
||||
func (bv *BusinessValidator) ValidateStruct(data interface{}) error {
|
||||
return bv.validator.Struct(data)
|
||||
}
|
||||
|
||||
// ValidateField 验证单个字段
|
||||
func (bv *BusinessValidator) ValidateField(field interface{}, tag string) error {
|
||||
return bv.validator.Var(field, tag)
|
||||
}
|
||||
|
||||
// 以下是具体的业务验证方法,可以在业务逻辑中直接调用
|
||||
|
||||
// ValidatePhone 验证手机号
|
||||
func (bv *BusinessValidator) ValidatePhone(phone string) error {
|
||||
if phone == "" {
|
||||
return fmt.Errorf("手机号不能为空")
|
||||
}
|
||||
matched, _ := regexp.MatchString(`^1[3-9]\d{9}$`, phone)
|
||||
if !matched {
|
||||
return fmt.Errorf("手机号格式不正确")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// ValidatePassword 验证密码强度
|
||||
func (bv *BusinessValidator) ValidatePassword(password string) error {
|
||||
if password == "" {
|
||||
return fmt.Errorf("密码不能为空")
|
||||
}
|
||||
if len(password) < 8 {
|
||||
return fmt.Errorf("密码长度不能少于8位")
|
||||
}
|
||||
hasUpper := regexp.MustCompile(`[A-Z]`).MatchString(password)
|
||||
hasLower := regexp.MustCompile(`[a-z]`).MatchString(password)
|
||||
hasDigit := regexp.MustCompile(`\d`).MatchString(password)
|
||||
|
||||
if !hasUpper {
|
||||
return fmt.Errorf("密码必须包含大写字母")
|
||||
}
|
||||
if !hasLower {
|
||||
return fmt.Errorf("密码必须包含小写字母")
|
||||
}
|
||||
if !hasDigit {
|
||||
return fmt.Errorf("密码必须包含数字")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// ValidateUsername 验证用户名
|
||||
func (bv *BusinessValidator) ValidateUsername(username string) error {
|
||||
if username == "" {
|
||||
return fmt.Errorf("用户名不能为空")
|
||||
}
|
||||
matched, _ := regexp.MatchString(`^[a-zA-Z][a-zA-Z0-9_]{2,19}$`, username)
|
||||
if !matched {
|
||||
return fmt.Errorf("用户名格式不正确,只能包含字母、数字、下划线,且必须以字母开头,长度3-20位")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// ValidateSocialCreditCode 验证统一社会信用代码
|
||||
func (bv *BusinessValidator) ValidateSocialCreditCode(code string) error {
|
||||
if code == "" {
|
||||
return fmt.Errorf("统一社会信用代码不能为空")
|
||||
}
|
||||
matched, _ := regexp.MatchString(`^[0-9A-HJ-NPQRTUWXY]{2}\d{6}[0-9A-HJ-NPQRTUWXY]{10}$`, code)
|
||||
if !matched {
|
||||
return fmt.Errorf("统一社会信用代码格式不正确,必须是18位统一社会信用代码")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// ValidateIDCard 验证身份证号
|
||||
func (bv *BusinessValidator) ValidateIDCard(idCard string) error {
|
||||
if idCard == "" {
|
||||
return fmt.Errorf("身份证号不能为空")
|
||||
}
|
||||
matched, _ := regexp.MatchString(`^[1-9]\d{5}(18|19|20)\d{2}((0[1-9])|(1[0-2]))(([0-2][1-9])|10|20|30|31)\d{3}[\dXx]$`, idCard)
|
||||
if !matched {
|
||||
return fmt.Errorf("身份证号格式不正确,必须是18位身份证号")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// ValidateUUID 验证UUID
|
||||
func (bv *BusinessValidator) ValidateUUID(uuid string) error {
|
||||
if uuid == "" {
|
||||
return fmt.Errorf("UUID不能为空")
|
||||
}
|
||||
matched, _ := regexp.MatchString(`^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$`, uuid)
|
||||
if !matched {
|
||||
return fmt.Errorf("UUID格式不正确")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// ValidateURL 验证URL
|
||||
func (bv *BusinessValidator) ValidateURL(urlStr string) error {
|
||||
if urlStr == "" {
|
||||
return fmt.Errorf("URL不能为空")
|
||||
}
|
||||
_, err := url.ParseRequestURI(urlStr)
|
||||
if err != nil {
|
||||
return fmt.Errorf("URL格式不正确: %v", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// ValidateProductCode 验证产品代码
|
||||
func (bv *BusinessValidator) ValidateProductCode(code string) error {
|
||||
if code == "" {
|
||||
return fmt.Errorf("产品代码不能为空")
|
||||
}
|
||||
matched, _ := regexp.MatchString(`^[a-zA-Z0-9_-]{3,50}$`, code)
|
||||
if !matched {
|
||||
return fmt.Errorf("产品代码格式不正确,只能包含字母、数字、下划线、连字符,长度3-50位")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// ValidateEmail 验证邮箱
|
||||
func (bv *BusinessValidator) ValidateEmail(email string) error {
|
||||
if email == "" {
|
||||
return fmt.Errorf("邮箱不能为空")
|
||||
}
|
||||
matched, _ := regexp.MatchString(`^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$`, email)
|
||||
if !matched {
|
||||
return fmt.Errorf("邮箱格式不正确")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// ValidateSortOrder 验证排序方向
|
||||
func (bv *BusinessValidator) ValidateSortOrder(sortOrder string) error {
|
||||
if sortOrder == "" {
|
||||
return nil // 允许为空
|
||||
}
|
||||
if sortOrder != "asc" && sortOrder != "desc" {
|
||||
return fmt.Errorf("排序方向必须是 asc 或 desc")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// ValidatePrice 验证价格
|
||||
func (bv *BusinessValidator) ValidatePrice(price float64) error {
|
||||
if price < 0 {
|
||||
return fmt.Errorf("价格不能为负数")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// ValidateStringLength 验证字符串长度
|
||||
func (bv *BusinessValidator) ValidateStringLength(str string, fieldName string, min, max int) error {
|
||||
length := len(strings.TrimSpace(str))
|
||||
if min > 0 && length < min {
|
||||
return fmt.Errorf("%s长度不能少于%d位", fieldName, min)
|
||||
}
|
||||
if max > 0 && length > max {
|
||||
return fmt.Errorf("%s长度不能超过%d位", fieldName, max)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// ValidateRequired 验证必填字段
|
||||
func (bv *BusinessValidator) ValidateRequired(value interface{}, fieldName string) error {
|
||||
if value == nil {
|
||||
return fmt.Errorf("%s不能为空", fieldName)
|
||||
}
|
||||
|
||||
switch v := value.(type) {
|
||||
case string:
|
||||
if strings.TrimSpace(v) == "" {
|
||||
return fmt.Errorf("%s不能为空", fieldName)
|
||||
}
|
||||
case *string:
|
||||
if v == nil || strings.TrimSpace(*v) == "" {
|
||||
return fmt.Errorf("%s不能为空", fieldName)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// ValidateRange 验证数值范围
|
||||
func (bv *BusinessValidator) ValidateRange(value float64, fieldName string, min, max float64) error {
|
||||
if value < min {
|
||||
return fmt.Errorf("%s不能小于%v", fieldName, min)
|
||||
}
|
||||
if value > max {
|
||||
return fmt.Errorf("%s不能大于%v", fieldName, max)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// ValidateSliceNotEmpty 验证切片不为空
|
||||
func (bv *BusinessValidator) ValidateSliceNotEmpty(slice interface{}, fieldName string) error {
|
||||
if slice == nil {
|
||||
return fmt.Errorf("%s不能为空", fieldName)
|
||||
}
|
||||
|
||||
switch v := slice.(type) {
|
||||
case []string:
|
||||
if len(v) == 0 {
|
||||
return fmt.Errorf("%s不能为空", fieldName)
|
||||
}
|
||||
case []int:
|
||||
if len(v) == 0 {
|
||||
return fmt.Errorf("%s不能为空", fieldName)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
114
internal/shared/validator/custom_validators.go
Normal file
114
internal/shared/validator/custom_validators.go
Normal file
@@ -0,0 +1,114 @@
|
||||
package validator
|
||||
|
||||
import (
|
||||
"net/url"
|
||||
"regexp"
|
||||
|
||||
"github.com/go-playground/validator/v10"
|
||||
)
|
||||
|
||||
// RegisterCustomValidators 注册所有自定义验证器
|
||||
func RegisterCustomValidators(validate *validator.Validate) {
|
||||
// 手机号验证器
|
||||
validate.RegisterValidation("phone", validatePhone)
|
||||
|
||||
// 用户名验证器(字母开头,允许字母数字下划线,3-20位)
|
||||
validate.RegisterValidation("username", validateUsername)
|
||||
|
||||
// 强密码验证器(至少8位,包含大小写字母和数字)
|
||||
validate.RegisterValidation("strong_password", validateStrongPassword)
|
||||
|
||||
// 统一社会信用代码验证器
|
||||
validate.RegisterValidation("social_credit_code", validateSocialCreditCode)
|
||||
|
||||
// 身份证号验证器
|
||||
validate.RegisterValidation("id_card", validateIDCard)
|
||||
|
||||
// 价格验证器(非负数)
|
||||
validate.RegisterValidation("price", validatePrice)
|
||||
|
||||
// 排序方向验证器
|
||||
validate.RegisterValidation("sort_order", validateSortOrder)
|
||||
|
||||
// 产品代码验证器(字母数字下划线连字符,3-50位)
|
||||
validate.RegisterValidation("product_code", validateProductCode)
|
||||
|
||||
// UUID验证器
|
||||
validate.RegisterValidation("uuid", validateUUID)
|
||||
|
||||
// URL验证器
|
||||
validate.RegisterValidation("url", validateURL)
|
||||
}
|
||||
|
||||
// validatePhone 手机号验证
|
||||
func validatePhone(fl validator.FieldLevel) bool {
|
||||
phone := fl.Field().String()
|
||||
matched, _ := regexp.MatchString(`^1[3-9]\d{9}$`, phone)
|
||||
return matched
|
||||
}
|
||||
|
||||
// validateUsername 用户名验证
|
||||
func validateUsername(fl validator.FieldLevel) bool {
|
||||
username := fl.Field().String()
|
||||
matched, _ := regexp.MatchString(`^[a-zA-Z][a-zA-Z0-9_]{2,19}$`, username)
|
||||
return matched
|
||||
}
|
||||
|
||||
// validateStrongPassword 强密码验证
|
||||
func validateStrongPassword(fl validator.FieldLevel) bool {
|
||||
password := fl.Field().String()
|
||||
if len(password) < 8 {
|
||||
return false
|
||||
}
|
||||
hasUpper := regexp.MustCompile(`[A-Z]`).MatchString(password)
|
||||
hasLower := regexp.MustCompile(`[a-z]`).MatchString(password)
|
||||
hasDigit := regexp.MustCompile(`\d`).MatchString(password)
|
||||
return hasUpper && hasLower && hasDigit
|
||||
}
|
||||
|
||||
// validateSocialCreditCode 统一社会信用代码验证
|
||||
func validateSocialCreditCode(fl validator.FieldLevel) bool {
|
||||
code := fl.Field().String()
|
||||
matched, _ := regexp.MatchString(`^[0-9A-HJ-NPQRTUWXY]{2}\d{6}[0-9A-HJ-NPQRTUWXY]{10}$`, code)
|
||||
return matched
|
||||
}
|
||||
|
||||
// validateIDCard 身份证号验证
|
||||
func validateIDCard(fl validator.FieldLevel) bool {
|
||||
idCard := fl.Field().String()
|
||||
matched, _ := regexp.MatchString(`^[1-9]\d{5}(18|19|20)\d{2}((0[1-9])|(1[0-2]))(([0-2][1-9])|10|20|30|31)\d{3}[\dXx]$`, idCard)
|
||||
return matched
|
||||
}
|
||||
|
||||
// validatePrice 价格验证
|
||||
func validatePrice(fl validator.FieldLevel) bool {
|
||||
price := fl.Field().Float()
|
||||
return price >= 0
|
||||
}
|
||||
|
||||
// validateSortOrder 排序方向验证
|
||||
func validateSortOrder(fl validator.FieldLevel) bool {
|
||||
sortOrder := fl.Field().String()
|
||||
return sortOrder == "" || sortOrder == "asc" || sortOrder == "desc"
|
||||
}
|
||||
|
||||
// validateProductCode 产品代码验证
|
||||
func validateProductCode(fl validator.FieldLevel) bool {
|
||||
code := fl.Field().String()
|
||||
matched, _ := regexp.MatchString(`^[a-zA-Z0-9_-]{3,50}$`, code)
|
||||
return matched
|
||||
}
|
||||
|
||||
// validateUUID UUID验证
|
||||
func validateUUID(fl validator.FieldLevel) bool {
|
||||
uuid := fl.Field().String()
|
||||
matched, _ := regexp.MatchString(`^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$`, uuid)
|
||||
return matched
|
||||
}
|
||||
|
||||
// validateURL URL验证
|
||||
func validateURL(fl validator.FieldLevel) bool {
|
||||
urlStr := fl.Field().String()
|
||||
_, err := url.ParseRequestURI(urlStr)
|
||||
return err == nil
|
||||
}
|
||||
253
internal/shared/validator/translations.go
Normal file
253
internal/shared/validator/translations.go
Normal file
@@ -0,0 +1,253 @@
|
||||
package validator
|
||||
|
||||
import (
|
||||
ut "github.com/go-playground/universal-translator"
|
||||
"github.com/go-playground/validator/v10"
|
||||
)
|
||||
|
||||
// RegisterCustomTranslations 注册所有自定义翻译
|
||||
func RegisterCustomTranslations(validate *validator.Validate, trans ut.Translator) {
|
||||
// 注册标准字段翻译
|
||||
registerStandardTranslations(validate, trans)
|
||||
|
||||
// 注册自定义字段翻译
|
||||
registerCustomFieldTranslations(validate, trans)
|
||||
}
|
||||
|
||||
// registerStandardTranslations 注册标准翻译
|
||||
func registerStandardTranslations(validate *validator.Validate, trans ut.Translator) {
|
||||
// 必填字段翻译
|
||||
validate.RegisterTranslation("required", trans, func(ut ut.Translator) error {
|
||||
return ut.Add("required", "{0}不能为空", true)
|
||||
}, func(ut ut.Translator, fe validator.FieldError) string {
|
||||
t, _ := ut.T("required", getFieldDisplayName(fe.Field()))
|
||||
return t
|
||||
})
|
||||
|
||||
// 字段相等翻译
|
||||
validate.RegisterTranslation("eqfield", trans, func(ut ut.Translator) error {
|
||||
return ut.Add("eqfield", "{0}必须与{1}一致", true)
|
||||
}, func(ut ut.Translator, fe validator.FieldError) string {
|
||||
t, _ := ut.T("eqfield", getFieldDisplayName(fe.Field()), getFieldDisplayName(fe.Param()))
|
||||
return t
|
||||
})
|
||||
|
||||
// 最小长度翻译
|
||||
validate.RegisterTranslation("min", trans, func(ut ut.Translator) error {
|
||||
return ut.Add("min", "{0}长度不能少于{1}位", true)
|
||||
}, func(ut ut.Translator, fe validator.FieldError) string {
|
||||
t, _ := ut.T("min", getFieldDisplayName(fe.Field()), fe.Param())
|
||||
return t
|
||||
})
|
||||
|
||||
// 最大长度翻译
|
||||
validate.RegisterTranslation("max", trans, func(ut ut.Translator) error {
|
||||
return ut.Add("max", "{0}长度不能超过{1}位", true)
|
||||
}, func(ut ut.Translator, fe validator.FieldError) string {
|
||||
t, _ := ut.T("max", getFieldDisplayName(fe.Field()), fe.Param())
|
||||
return t
|
||||
})
|
||||
|
||||
// 固定长度翻译
|
||||
validate.RegisterTranslation("len", trans, func(ut ut.Translator) error {
|
||||
return ut.Add("len", "{0}长度必须为{1}位", true)
|
||||
}, func(ut ut.Translator, fe validator.FieldError) string {
|
||||
t, _ := ut.T("len", getFieldDisplayName(fe.Field()), fe.Param())
|
||||
return t
|
||||
})
|
||||
|
||||
// 邮箱翻译
|
||||
validate.RegisterTranslation("email", trans, func(ut ut.Translator) error {
|
||||
return ut.Add("email", "{0}必须是有效的邮箱地址", true)
|
||||
}, func(ut ut.Translator, fe validator.FieldError) string {
|
||||
t, _ := ut.T("email", getFieldDisplayName(fe.Field()))
|
||||
return t
|
||||
})
|
||||
|
||||
// 枚举值翻译
|
||||
validate.RegisterTranslation("oneof", trans, func(ut ut.Translator) error {
|
||||
return ut.Add("oneof", "{0}必须是以下值之一: {1}", true)
|
||||
}, func(ut ut.Translator, fe validator.FieldError) string {
|
||||
t, _ := ut.T("oneof", getFieldDisplayName(fe.Field()), fe.Param())
|
||||
return t
|
||||
})
|
||||
|
||||
// 大于翻译
|
||||
validate.RegisterTranslation("gt", trans, func(ut ut.Translator) error {
|
||||
return ut.Add("gt", "{0}必须大于{1}", true)
|
||||
}, func(ut ut.Translator, fe validator.FieldError) string {
|
||||
t, _ := ut.T("gt", getFieldDisplayName(fe.Field()), fe.Param())
|
||||
return t
|
||||
})
|
||||
}
|
||||
|
||||
// registerCustomFieldTranslations 注册自定义字段翻译
|
||||
func registerCustomFieldTranslations(validate *validator.Validate, trans ut.Translator) {
|
||||
// 手机号翻译
|
||||
validate.RegisterTranslation("phone", trans, func(ut ut.Translator) error {
|
||||
return ut.Add("phone", "{0}必须是有效的手机号", true)
|
||||
}, func(ut ut.Translator, fe validator.FieldError) string {
|
||||
t, _ := ut.T("phone", getFieldDisplayName(fe.Field()))
|
||||
return t
|
||||
})
|
||||
|
||||
// 用户名翻译
|
||||
validate.RegisterTranslation("username", trans, func(ut ut.Translator) error {
|
||||
return ut.Add("username", "{0}格式不正确,只能包含字母、数字、下划线,且必须以字母开头,长度3-20位", true)
|
||||
}, func(ut ut.Translator, fe validator.FieldError) string {
|
||||
t, _ := ut.T("username", getFieldDisplayName(fe.Field()))
|
||||
return t
|
||||
})
|
||||
|
||||
// 强密码翻译
|
||||
validate.RegisterTranslation("strong_password", trans, func(ut ut.Translator) error {
|
||||
return ut.Add("strong_password", "{0}强度不足,必须包含大小写字母和数字,且不少于8位", true)
|
||||
}, func(ut ut.Translator, fe validator.FieldError) string {
|
||||
t, _ := ut.T("strong_password", getFieldDisplayName(fe.Field()))
|
||||
return t
|
||||
})
|
||||
|
||||
// 统一社会信用代码翻译
|
||||
validate.RegisterTranslation("social_credit_code", trans, func(ut ut.Translator) error {
|
||||
return ut.Add("social_credit_code", "{0}格式不正确,必须是18位统一社会信用代码", true)
|
||||
}, func(ut ut.Translator, fe validator.FieldError) string {
|
||||
t, _ := ut.T("social_credit_code", getFieldDisplayName(fe.Field()))
|
||||
return t
|
||||
})
|
||||
|
||||
// 身份证号翻译
|
||||
validate.RegisterTranslation("id_card", trans, func(ut ut.Translator) error {
|
||||
return ut.Add("id_card", "{0}格式不正确,必须是18位身份证号", true)
|
||||
}, func(ut ut.Translator, fe validator.FieldError) string {
|
||||
t, _ := ut.T("id_card", getFieldDisplayName(fe.Field()))
|
||||
return t
|
||||
})
|
||||
|
||||
// 价格翻译
|
||||
validate.RegisterTranslation("price", trans, func(ut ut.Translator) error {
|
||||
return ut.Add("price", "{0}必须是非负数", true)
|
||||
}, func(ut ut.Translator, fe validator.FieldError) string {
|
||||
t, _ := ut.T("price", getFieldDisplayName(fe.Field()))
|
||||
return t
|
||||
})
|
||||
|
||||
// 排序方向翻译
|
||||
validate.RegisterTranslation("sort_order", trans, func(ut ut.Translator) error {
|
||||
return ut.Add("sort_order", "{0}必须是 asc 或 desc", true)
|
||||
}, func(ut ut.Translator, fe validator.FieldError) string {
|
||||
t, _ := ut.T("sort_order", getFieldDisplayName(fe.Field()))
|
||||
return t
|
||||
})
|
||||
|
||||
// 产品代码翻译
|
||||
validate.RegisterTranslation("product_code", trans, func(ut ut.Translator) error {
|
||||
return ut.Add("product_code", "{0}格式不正确,只能包含字母、数字、下划线、连字符,长度3-50位", true)
|
||||
}, func(ut ut.Translator, fe validator.FieldError) string {
|
||||
t, _ := ut.T("product_code", getFieldDisplayName(fe.Field()))
|
||||
return t
|
||||
})
|
||||
|
||||
// UUID翻译
|
||||
validate.RegisterTranslation("uuid", trans, func(ut ut.Translator) error {
|
||||
return ut.Add("uuid", "{0}必须是有效的UUID格式", true)
|
||||
}, func(ut ut.Translator, fe validator.FieldError) string {
|
||||
t, _ := ut.T("uuid", getFieldDisplayName(fe.Field()))
|
||||
return t
|
||||
})
|
||||
|
||||
// URL翻译
|
||||
validate.RegisterTranslation("url", trans, func(ut ut.Translator) error {
|
||||
return ut.Add("url", "{0}必须是有效的URL地址", true)
|
||||
}, func(ut ut.Translator, fe validator.FieldError) string {
|
||||
t, _ := ut.T("url", getFieldDisplayName(fe.Field()))
|
||||
return t
|
||||
})
|
||||
}
|
||||
|
||||
// getFieldDisplayName 获取字段显示名称(中文)
|
||||
func getFieldDisplayName(field string) string {
|
||||
fieldNames := map[string]string{
|
||||
"phone": "手机号",
|
||||
"password": "密码",
|
||||
"confirm_password": "确认密码",
|
||||
"old_password": "原密码",
|
||||
"new_password": "新密码",
|
||||
"confirm_new_password": "确认新密码",
|
||||
"code": "验证码",
|
||||
"username": "用户名",
|
||||
"email": "邮箱",
|
||||
"display_name": "显示名称",
|
||||
"scene": "使用场景",
|
||||
"Password": "密码",
|
||||
"NewPassword": "新密码",
|
||||
"ConfirmPassword": "确认密码",
|
||||
"name": "名称",
|
||||
"Name": "名称",
|
||||
"description": "描述",
|
||||
"Description": "描述",
|
||||
"price": "价格",
|
||||
"Price": "价格",
|
||||
"category_id": "分类ID",
|
||||
"CategoryID": "分类ID",
|
||||
"product_id": "产品ID",
|
||||
"ProductID": "产品ID",
|
||||
"user_id": "用户ID",
|
||||
"UserID": "用户ID",
|
||||
"page": "页码",
|
||||
"Page": "页码",
|
||||
"page_size": "每页数量",
|
||||
"PageSize": "每页数量",
|
||||
"keyword": "关键词",
|
||||
"Keyword": "关键词",
|
||||
"sort_by": "排序字段",
|
||||
"SortBy": "排序字段",
|
||||
"sort_order": "排序方向",
|
||||
"SortOrder": "排序方向",
|
||||
"company_name": "企业名称",
|
||||
"CompanyName": "企业名称",
|
||||
"unified_social_code": "统一社会信用代码",
|
||||
"UnifiedSocialCode": "统一社会信用代码",
|
||||
"legal_person_name": "法定代表人姓名",
|
||||
"LegalPersonName": "法定代表人姓名",
|
||||
"legal_person_id": "法定代表人身份证号",
|
||||
"LegalPersonID": "法定代表人身份证号",
|
||||
"legal_person_phone": "法定代表人手机号",
|
||||
"LegalPersonPhone": "法定代表人手机号",
|
||||
"verification_code": "验证码",
|
||||
"VerificationCode": "验证码",
|
||||
"contract_url": "合同URL",
|
||||
"ContractURL": "合同URL",
|
||||
"amount": "金额",
|
||||
"Amount": "金额",
|
||||
"balance": "余额",
|
||||
"Balance": "余额",
|
||||
"is_active": "是否激活",
|
||||
"IsActive": "是否激活",
|
||||
"is_enabled": "是否启用",
|
||||
"IsEnabled": "是否启用",
|
||||
"is_visible": "是否可见",
|
||||
"IsVisible": "是否可见",
|
||||
"is_package": "是否组合包",
|
||||
"IsPackage": "是否组合包",
|
||||
"Code": "编号",
|
||||
"content": "内容",
|
||||
"Content": "内容",
|
||||
"sort": "排序",
|
||||
"Sort": "排序",
|
||||
"seo_title": "SEO标题",
|
||||
"SEOTitle": "SEO标题",
|
||||
"seo_description": "SEO描述",
|
||||
"SEODescription": "SEO描述",
|
||||
"seo_keywords": "SEO关键词",
|
||||
"SEOKeywords": "SEO关键词",
|
||||
"id": "ID",
|
||||
"ID": "ID",
|
||||
"ids": "ID列表",
|
||||
"IDs": "ID列表",
|
||||
}
|
||||
|
||||
if displayName, exists := fieldNames[field]; exists {
|
||||
return displayName
|
||||
}
|
||||
return field
|
||||
}
|
||||
216
internal/shared/validator/validator.go
Normal file
216
internal/shared/validator/validator.go
Normal file
@@ -0,0 +1,216 @@
|
||||
package validator
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"tyapi-server/internal/shared/interfaces"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/gin-gonic/gin/binding"
|
||||
"github.com/go-playground/locales/zh"
|
||||
ut "github.com/go-playground/universal-translator"
|
||||
"github.com/go-playground/validator/v10"
|
||||
zh_translations "github.com/go-playground/validator/v10/translations/zh"
|
||||
)
|
||||
|
||||
// RequestValidator HTTP请求验证器
|
||||
type RequestValidator struct {
|
||||
response interfaces.ResponseBuilder
|
||||
translator ut.Translator
|
||||
validator *validator.Validate
|
||||
}
|
||||
|
||||
// NewRequestValidator 创建HTTP请求验证器
|
||||
func NewRequestValidator(response interfaces.ResponseBuilder) interfaces.RequestValidator {
|
||||
// 创建中文locale
|
||||
zhLocale := zh.New()
|
||||
uni := ut.New(zhLocale, zhLocale)
|
||||
|
||||
// 获取中文翻译器
|
||||
trans, _ := uni.GetTranslator("zh")
|
||||
|
||||
// 获取gin默认的validator实例
|
||||
ginValidator := binding.Validator.Engine().(*validator.Validate)
|
||||
|
||||
// 注册官方中文翻译
|
||||
zh_translations.RegisterDefaultTranslations(ginValidator, trans)
|
||||
|
||||
// 注册自定义验证器到gin的全局validator
|
||||
RegisterCustomValidators(ginValidator)
|
||||
|
||||
// 注册自定义翻译
|
||||
RegisterCustomTranslations(ginValidator, trans)
|
||||
|
||||
return &RequestValidator{
|
||||
response: response,
|
||||
translator: trans,
|
||||
validator: ginValidator,
|
||||
}
|
||||
}
|
||||
|
||||
// Validate 验证请求体
|
||||
func (v *RequestValidator) Validate(c *gin.Context, dto interface{}) error {
|
||||
return v.BindAndValidate(c, dto)
|
||||
}
|
||||
|
||||
// ValidateQuery 验证查询参数
|
||||
func (v *RequestValidator) ValidateQuery(c *gin.Context, dto interface{}) error {
|
||||
if err := c.ShouldBindQuery(dto); err != nil {
|
||||
if validationErrors, ok := err.(validator.ValidationErrors); ok {
|
||||
validationErrorsMap := v.formatValidationErrors(validationErrors)
|
||||
v.response.ValidationError(c, validationErrorsMap)
|
||||
} else {
|
||||
v.response.BadRequest(c, "查询参数格式错误")
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// ValidateParam 验证路径参数
|
||||
func (v *RequestValidator) ValidateParam(c *gin.Context, dto interface{}) error {
|
||||
if err := c.ShouldBindUri(dto); err != nil {
|
||||
if validationErrors, ok := err.(validator.ValidationErrors); ok {
|
||||
validationErrorsMap := v.formatValidationErrors(validationErrors)
|
||||
v.response.ValidationError(c, validationErrorsMap)
|
||||
} else {
|
||||
v.response.BadRequest(c, "路径参数格式错误")
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// BindAndValidate 绑定并验证请求
|
||||
func (v *RequestValidator) BindAndValidate(c *gin.Context, dto interface{}) error {
|
||||
if err := c.ShouldBindJSON(dto); err != nil {
|
||||
if validationErrors, ok := err.(validator.ValidationErrors); ok {
|
||||
validationErrorsMap := v.formatValidationErrors(validationErrors)
|
||||
v.response.ValidationError(c, validationErrorsMap)
|
||||
} else {
|
||||
v.response.BadRequest(c, "请求体格式错误")
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// formatValidationErrors 格式化验证错误
|
||||
func (v *RequestValidator) formatValidationErrors(validationErrors validator.ValidationErrors) map[string][]string {
|
||||
errors := make(map[string][]string)
|
||||
|
||||
for _, fieldError := range validationErrors {
|
||||
fieldName := v.getFieldName(fieldError)
|
||||
errorMessage := v.getErrorMessage(fieldError)
|
||||
|
||||
if _, exists := errors[fieldName]; !exists {
|
||||
errors[fieldName] = []string{}
|
||||
}
|
||||
errors[fieldName] = append(errors[fieldName], errorMessage)
|
||||
}
|
||||
|
||||
return errors
|
||||
}
|
||||
|
||||
// getErrorMessage 获取错误消息
|
||||
func (v *RequestValidator) getErrorMessage(fieldError validator.FieldError) string {
|
||||
fieldDisplayName := getFieldDisplayName(fieldError.Field())
|
||||
|
||||
// 优先使用翻译器
|
||||
errorMessage := fieldError.Translate(v.translator)
|
||||
if errorMessage != fieldError.Error() {
|
||||
// 替换字段名为中文
|
||||
if fieldDisplayName != fieldError.Field() {
|
||||
errorMessage = strings.ReplaceAll(errorMessage, fieldError.Field(), fieldDisplayName)
|
||||
}
|
||||
return errorMessage
|
||||
}
|
||||
|
||||
// 回退到手动翻译
|
||||
return v.getFallbackErrorMessage(fieldError, fieldDisplayName)
|
||||
}
|
||||
|
||||
// getFallbackErrorMessage 获取回退错误消息
|
||||
func (v *RequestValidator) getFallbackErrorMessage(fieldError validator.FieldError, fieldDisplayName string) string {
|
||||
tag := fieldError.Tag()
|
||||
param := fieldError.Param()
|
||||
|
||||
switch tag {
|
||||
case "required":
|
||||
return fmt.Sprintf("%s不能为空", fieldDisplayName)
|
||||
case "email":
|
||||
return fmt.Sprintf("%s必须是有效的邮箱地址", fieldDisplayName)
|
||||
case "min":
|
||||
return fmt.Sprintf("%s长度不能少于%s位", fieldDisplayName, param)
|
||||
case "max":
|
||||
return fmt.Sprintf("%s长度不能超过%s位", fieldDisplayName, param)
|
||||
case "len":
|
||||
return fmt.Sprintf("%s长度必须为%s位", fieldDisplayName, param)
|
||||
case "eqfield":
|
||||
paramDisplayName := getFieldDisplayName(param)
|
||||
return fmt.Sprintf("%s必须与%s一致", fieldDisplayName, paramDisplayName)
|
||||
case "phone":
|
||||
return fmt.Sprintf("%s必须是有效的手机号", fieldDisplayName)
|
||||
case "username":
|
||||
return fmt.Sprintf("%s格式不正确,只能包含字母、数字、下划线,且必须以字母开头,长度3-20位", fieldDisplayName)
|
||||
case "strong_password":
|
||||
return fmt.Sprintf("%s强度不足,必须包含大小写字母和数字,且不少于8位", fieldDisplayName)
|
||||
case "social_credit_code":
|
||||
return fmt.Sprintf("%s格式不正确,必须是18位统一社会信用代码", fieldDisplayName)
|
||||
case "id_card":
|
||||
return fmt.Sprintf("%s格式不正确,必须是18位身份证号", fieldDisplayName)
|
||||
case "price":
|
||||
return fmt.Sprintf("%s必须是非负数", fieldDisplayName)
|
||||
case "sort_order":
|
||||
return fmt.Sprintf("%s必须是 asc 或 desc", fieldDisplayName)
|
||||
case "product_code":
|
||||
return fmt.Sprintf("%s格式不正确,只能包含字母、数字、下划线、连字符,长度3-50位", fieldDisplayName)
|
||||
case "uuid":
|
||||
return fmt.Sprintf("%s必须是有效的UUID格式", fieldDisplayName)
|
||||
case "url":
|
||||
return fmt.Sprintf("%s必须是有效的URL地址", fieldDisplayName)
|
||||
case "oneof":
|
||||
return fmt.Sprintf("%s必须是以下值之一: %s", fieldDisplayName, param)
|
||||
case "gt":
|
||||
return fmt.Sprintf("%s必须大于%s", fieldDisplayName, param)
|
||||
default:
|
||||
return fmt.Sprintf("%s格式不正确", fieldDisplayName)
|
||||
}
|
||||
}
|
||||
|
||||
// getFieldName 获取字段名
|
||||
func (v *RequestValidator) getFieldName(fieldError validator.FieldError) string {
|
||||
fieldName := fieldError.Field()
|
||||
return v.toSnakeCase(fieldName)
|
||||
}
|
||||
|
||||
// toSnakeCase 转换为snake_case
|
||||
func (v *RequestValidator) toSnakeCase(str string) string {
|
||||
var result strings.Builder
|
||||
for i, r := range str {
|
||||
if i > 0 && (r >= 'A' && r <= 'Z') {
|
||||
result.WriteRune('_')
|
||||
}
|
||||
result.WriteRune(r)
|
||||
}
|
||||
return strings.ToLower(result.String())
|
||||
}
|
||||
|
||||
// GetValidator 获取validator实例(用于业务逻辑)
|
||||
func (v *RequestValidator) GetValidator() *validator.Validate {
|
||||
return v.validator
|
||||
}
|
||||
|
||||
// ValidateValue 验证单个值(用于业务逻辑)
|
||||
func (v *RequestValidator) ValidateValue(field interface{}, tag string) error {
|
||||
return v.validator.Var(field, tag)
|
||||
}
|
||||
|
||||
// ValidateStruct 验证结构体(用于业务逻辑)
|
||||
func (v *RequestValidator) ValidateStruct(s interface{}) error {
|
||||
return v.validator.Struct(s)
|
||||
}
|
||||
Reference in New Issue
Block a user