This commit is contained in:
2025-07-20 20:53:26 +08:00
parent 83bf9aea7d
commit 8ad1d7288e
158 changed files with 18156 additions and 13188 deletions

View 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)
}

View 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
}

View 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),
)
}

View 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))
}

View 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`中的客户端配置。

View 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
}

View 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" // 所有客户端
)

View 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)
}

View 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
}

View 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
}

View 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)
}

View 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
}

View 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,
},
}
}

View 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日"),
},
}
}

View 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"` // 填写组件列表
}

View 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()
}

View File

@@ -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())
}

View File

@@ -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 列表查询选项

View File

@@ -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()
}

View File

@@ -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)
}

View 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, &param); 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请求的自动验证也可以在业务逻辑中进行程序化验证确保数据的完整性和正确性。

View 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
}

View 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
}

View 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
}

View 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)
}