基础架构
This commit is contained in:
284
internal/shared/cache/redis_cache.go
vendored
284
internal/shared/cache/redis_cache.go
vendored
@@ -1,284 +0,0 @@
|
||||
package cache
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/redis/go-redis/v9"
|
||||
"go.uber.org/zap"
|
||||
|
||||
"tyapi-server/internal/shared/interfaces"
|
||||
)
|
||||
|
||||
// RedisCache Redis缓存实现
|
||||
type RedisCache struct {
|
||||
client *redis.Client
|
||||
logger *zap.Logger
|
||||
prefix string
|
||||
|
||||
// 统计信息
|
||||
hits int64
|
||||
misses int64
|
||||
}
|
||||
|
||||
// NewRedisCache 创建Redis缓存实例
|
||||
func NewRedisCache(client *redis.Client, logger *zap.Logger, prefix string) *RedisCache {
|
||||
return &RedisCache{
|
||||
client: client,
|
||||
logger: logger,
|
||||
prefix: prefix,
|
||||
}
|
||||
}
|
||||
|
||||
// Name 返回服务名称
|
||||
func (r *RedisCache) Name() string {
|
||||
return "redis-cache"
|
||||
}
|
||||
|
||||
// Initialize 初始化服务
|
||||
func (r *RedisCache) Initialize(ctx context.Context) error {
|
||||
// 测试连接
|
||||
_, err := r.client.Ping(ctx).Result()
|
||||
if err != nil {
|
||||
r.logger.Error("Failed to connect to Redis", zap.Error(err))
|
||||
return fmt.Errorf("redis connection failed: %w", err)
|
||||
}
|
||||
|
||||
r.logger.Info("Redis cache service initialized")
|
||||
return nil
|
||||
}
|
||||
|
||||
// HealthCheck 健康检查
|
||||
func (r *RedisCache) HealthCheck(ctx context.Context) error {
|
||||
_, err := r.client.Ping(ctx).Result()
|
||||
return err
|
||||
}
|
||||
|
||||
// Shutdown 关闭服务
|
||||
func (r *RedisCache) Shutdown(ctx context.Context) error {
|
||||
return r.client.Close()
|
||||
}
|
||||
|
||||
// Get 获取缓存值
|
||||
func (r *RedisCache) Get(ctx context.Context, key string, dest interface{}) error {
|
||||
fullKey := r.getFullKey(key)
|
||||
|
||||
val, err := r.client.Get(ctx, fullKey).Result()
|
||||
if err != nil {
|
||||
if err == redis.Nil {
|
||||
r.misses++
|
||||
return fmt.Errorf("cache miss: key %s not found", key)
|
||||
}
|
||||
r.logger.Error("Failed to get cache", zap.String("key", key), zap.Error(err))
|
||||
return err
|
||||
}
|
||||
|
||||
r.hits++
|
||||
return json.Unmarshal([]byte(val), dest)
|
||||
}
|
||||
|
||||
// Set 设置缓存值
|
||||
func (r *RedisCache) Set(ctx context.Context, key string, value interface{}, ttl ...interface{}) error {
|
||||
fullKey := r.getFullKey(key)
|
||||
|
||||
data, err := json.Marshal(value)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to marshal value: %w", err)
|
||||
}
|
||||
|
||||
var expiration time.Duration
|
||||
if len(ttl) > 0 {
|
||||
switch v := ttl[0].(type) {
|
||||
case time.Duration:
|
||||
expiration = v
|
||||
case int:
|
||||
expiration = time.Duration(v) * time.Second
|
||||
case string:
|
||||
expiration, _ = time.ParseDuration(v)
|
||||
default:
|
||||
expiration = 24 * time.Hour // 默认24小时
|
||||
}
|
||||
} else {
|
||||
expiration = 24 * time.Hour // 默认24小时
|
||||
}
|
||||
|
||||
err = r.client.Set(ctx, fullKey, data, expiration).Err()
|
||||
if err != nil {
|
||||
r.logger.Error("Failed to set cache", zap.String("key", key), zap.Error(err))
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Delete 删除缓存
|
||||
func (r *RedisCache) Delete(ctx context.Context, keys ...string) error {
|
||||
if len(keys) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
fullKeys := make([]string, len(keys))
|
||||
for i, key := range keys {
|
||||
fullKeys[i] = r.getFullKey(key)
|
||||
}
|
||||
|
||||
err := r.client.Del(ctx, fullKeys...).Err()
|
||||
if err != nil {
|
||||
r.logger.Error("Failed to delete cache", zap.Strings("keys", keys), zap.Error(err))
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Exists 检查键是否存在
|
||||
func (r *RedisCache) Exists(ctx context.Context, key string) (bool, error) {
|
||||
fullKey := r.getFullKey(key)
|
||||
|
||||
count, err := r.client.Exists(ctx, fullKey).Result()
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
return count > 0, nil
|
||||
}
|
||||
|
||||
// GetMultiple 批量获取
|
||||
func (r *RedisCache) GetMultiple(ctx context.Context, keys []string) (map[string]interface{}, error) {
|
||||
if len(keys) == 0 {
|
||||
return make(map[string]interface{}), nil
|
||||
}
|
||||
|
||||
fullKeys := make([]string, len(keys))
|
||||
for i, key := range keys {
|
||||
fullKeys[i] = r.getFullKey(key)
|
||||
}
|
||||
|
||||
values, err := r.client.MGet(ctx, fullKeys...).Result()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
result := make(map[string]interface{})
|
||||
for i, val := range values {
|
||||
if val != nil {
|
||||
var data interface{}
|
||||
if err := json.Unmarshal([]byte(val.(string)), &data); err == nil {
|
||||
result[keys[i]] = data
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// SetMultiple 批量设置
|
||||
func (r *RedisCache) SetMultiple(ctx context.Context, data map[string]interface{}, ttl ...interface{}) error {
|
||||
if len(data) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
var expiration time.Duration
|
||||
if len(ttl) > 0 {
|
||||
switch v := ttl[0].(type) {
|
||||
case time.Duration:
|
||||
expiration = v
|
||||
case int:
|
||||
expiration = time.Duration(v) * time.Second
|
||||
default:
|
||||
expiration = 24 * time.Hour
|
||||
}
|
||||
} else {
|
||||
expiration = 24 * time.Hour
|
||||
}
|
||||
|
||||
pipe := r.client.Pipeline()
|
||||
for key, value := range data {
|
||||
fullKey := r.getFullKey(key)
|
||||
jsonData, err := json.Marshal(value)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
pipe.Set(ctx, fullKey, jsonData, expiration)
|
||||
}
|
||||
|
||||
_, err := pipe.Exec(ctx)
|
||||
return err
|
||||
}
|
||||
|
||||
// DeletePattern 按模式删除
|
||||
func (r *RedisCache) DeletePattern(ctx context.Context, pattern string) error {
|
||||
fullPattern := r.getFullKey(pattern)
|
||||
|
||||
keys, err := r.client.Keys(ctx, fullPattern).Result()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if len(keys) > 0 {
|
||||
return r.client.Del(ctx, keys...).Err()
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Keys 获取匹配的键
|
||||
func (r *RedisCache) Keys(ctx context.Context, pattern string) ([]string, error) {
|
||||
fullPattern := r.getFullKey(pattern)
|
||||
|
||||
keys, err := r.client.Keys(ctx, fullPattern).Result()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 移除前缀
|
||||
result := make([]string, len(keys))
|
||||
prefixLen := len(r.prefix) + 1 // +1 for ":"
|
||||
for i, key := range keys {
|
||||
if len(key) > prefixLen {
|
||||
result[i] = key[prefixLen:]
|
||||
} else {
|
||||
result[i] = key
|
||||
}
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// Stats 获取缓存统计
|
||||
func (r *RedisCache) Stats(ctx context.Context) (interfaces.CacheStats, error) {
|
||||
dbSize, _ := r.client.DBSize(ctx).Result()
|
||||
|
||||
return interfaces.CacheStats{
|
||||
Hits: r.hits,
|
||||
Misses: r.misses,
|
||||
Keys: dbSize,
|
||||
Memory: 0, // 暂时设为0,后续可解析Redis info
|
||||
Connections: 0, // 暂时设为0,后续可解析Redis info
|
||||
}, nil
|
||||
}
|
||||
|
||||
// getFullKey 获取完整键名
|
||||
func (r *RedisCache) getFullKey(key string) string {
|
||||
if r.prefix == "" {
|
||||
return key
|
||||
}
|
||||
return fmt.Sprintf("%s:%s", r.prefix, key)
|
||||
}
|
||||
|
||||
// Flush 清空所有缓存
|
||||
func (r *RedisCache) Flush(ctx context.Context) error {
|
||||
if r.prefix == "" {
|
||||
return r.client.FlushDB(ctx).Err()
|
||||
}
|
||||
|
||||
// 只删除带前缀的键
|
||||
return r.DeletePattern(ctx, "*")
|
||||
}
|
||||
|
||||
// GetClient 获取原始Redis客户端
|
||||
func (r *RedisCache) GetClient() *redis.Client {
|
||||
return r.client
|
||||
}
|
||||
@@ -1,199 +0,0 @@
|
||||
package database
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"gorm.io/driver/postgres"
|
||||
"gorm.io/gorm"
|
||||
"gorm.io/gorm/logger"
|
||||
"gorm.io/gorm/schema"
|
||||
)
|
||||
|
||||
// Config 数据库配置
|
||||
type Config struct {
|
||||
Host string
|
||||
Port string
|
||||
User string
|
||||
Password string
|
||||
Name string
|
||||
SSLMode string
|
||||
Timezone string
|
||||
MaxOpenConns int
|
||||
MaxIdleConns int
|
||||
ConnMaxLifetime time.Duration
|
||||
}
|
||||
|
||||
// DB 数据库包装器
|
||||
type DB struct {
|
||||
*gorm.DB
|
||||
config Config
|
||||
}
|
||||
|
||||
// NewConnection 创建新的数据库连接
|
||||
func NewConnection(config Config) (*DB, error) {
|
||||
// 构建DSN
|
||||
dsn := buildDSN(config)
|
||||
|
||||
// 配置GORM
|
||||
gormConfig := &gorm.Config{
|
||||
Logger: logger.Default.LogMode(logger.Info),
|
||||
NamingStrategy: schema.NamingStrategy{
|
||||
SingularTable: true, // 使用单数表名
|
||||
},
|
||||
DisableForeignKeyConstraintWhenMigrating: true,
|
||||
NowFunc: func() time.Time {
|
||||
return time.Now().In(time.FixedZone("CST", 8*3600)) // 强制使用北京时间
|
||||
},
|
||||
}
|
||||
|
||||
// 连接数据库
|
||||
db, err := gorm.Open(postgres.Open(dsn), gormConfig)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("连接数据库失败: %w", err)
|
||||
}
|
||||
|
||||
// 获取底层sql.DB
|
||||
sqlDB, err := db.DB()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("获取数据库实例失败: %w", err)
|
||||
}
|
||||
|
||||
// 配置连接池
|
||||
sqlDB.SetMaxOpenConns(config.MaxOpenConns)
|
||||
sqlDB.SetMaxIdleConns(config.MaxIdleConns)
|
||||
sqlDB.SetConnMaxLifetime(config.ConnMaxLifetime)
|
||||
|
||||
// 测试连接
|
||||
if err := sqlDB.Ping(); err != nil {
|
||||
return nil, fmt.Errorf("数据库连接测试失败: %w", err)
|
||||
}
|
||||
|
||||
return &DB{
|
||||
DB: db,
|
||||
config: config,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// buildDSN 构建数据库连接字符串
|
||||
func buildDSN(config Config) string {
|
||||
return fmt.Sprintf(
|
||||
"host=%s user=%s password=%s dbname=%s port=%s sslmode=%s TimeZone=%s options='-c timezone=%s'",
|
||||
config.Host,
|
||||
config.User,
|
||||
config.Password,
|
||||
config.Name,
|
||||
config.Port,
|
||||
config.SSLMode,
|
||||
config.Timezone,
|
||||
config.Timezone,
|
||||
)
|
||||
}
|
||||
|
||||
// Close 关闭数据库连接
|
||||
func (db *DB) Close() error {
|
||||
sqlDB, err := db.DB.DB()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return sqlDB.Close()
|
||||
}
|
||||
|
||||
// Ping 检查数据库连接
|
||||
func (db *DB) Ping() error {
|
||||
sqlDB, err := db.DB.DB()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return sqlDB.Ping()
|
||||
}
|
||||
|
||||
// GetStats 获取连接池统计信息
|
||||
func (db *DB) GetStats() (map[string]interface{}, error) {
|
||||
sqlDB, err := db.DB.DB()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
stats := sqlDB.Stats()
|
||||
return map[string]interface{}{
|
||||
"max_open_connections": stats.MaxOpenConnections,
|
||||
"open_connections": stats.OpenConnections,
|
||||
"in_use": stats.InUse,
|
||||
"idle": stats.Idle,
|
||||
"wait_count": stats.WaitCount,
|
||||
"wait_duration": stats.WaitDuration,
|
||||
"max_idle_closed": stats.MaxIdleClosed,
|
||||
"max_idle_time_closed": stats.MaxIdleTimeClosed,
|
||||
"max_lifetime_closed": stats.MaxLifetimeClosed,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// BeginTx 开始事务
|
||||
func (db *DB) BeginTx() *gorm.DB {
|
||||
return db.DB.Begin()
|
||||
}
|
||||
|
||||
// Migrate 执行数据库迁移
|
||||
func (db *DB) Migrate(models ...interface{}) error {
|
||||
return db.DB.AutoMigrate(models...)
|
||||
}
|
||||
|
||||
// IsHealthy 检查数据库健康状态
|
||||
func (db *DB) IsHealthy() bool {
|
||||
return db.Ping() == nil
|
||||
}
|
||||
|
||||
// WithContext 返回带上下文的数据库实例
|
||||
func (db *DB) WithContext(ctx interface{}) *gorm.DB {
|
||||
if c, ok := ctx.(context.Context); ok {
|
||||
return db.DB.WithContext(c)
|
||||
}
|
||||
return db.DB
|
||||
}
|
||||
|
||||
// 事务包装器
|
||||
type TxWrapper struct {
|
||||
tx *gorm.DB
|
||||
}
|
||||
|
||||
// NewTxWrapper 创建事务包装器
|
||||
func (db *DB) NewTxWrapper() *TxWrapper {
|
||||
return &TxWrapper{
|
||||
tx: db.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 (db *DB) WithTx(fn func(*gorm.DB) error) error {
|
||||
tx := db.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
|
||||
}
|
||||
@@ -29,7 +29,7 @@ type Repository[T any] interface {
|
||||
BaseRepository
|
||||
|
||||
// 基础CRUD操作
|
||||
Create(ctx context.Context, entity T) error
|
||||
Create(ctx context.Context, entity T) (T, error)
|
||||
GetByID(ctx context.Context, id string) (T, error)
|
||||
Update(ctx context.Context, entity T) error
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@ import (
|
||||
"context"
|
||||
"errors"
|
||||
|
||||
"tyapi-server/internal/domains/user/dto"
|
||||
"tyapi-server/internal/application/user/dto/commands"
|
||||
"tyapi-server/internal/domains/user/entities"
|
||||
)
|
||||
|
||||
@@ -30,13 +30,13 @@ type UserService interface {
|
||||
Service
|
||||
|
||||
// 用户注册
|
||||
Register(ctx context.Context, req *dto.RegisterRequest) (*entities.User, error)
|
||||
Register(ctx context.Context, req *commands.RegisterUserCommand) (*entities.User, error)
|
||||
// 密码登录
|
||||
LoginWithPassword(ctx context.Context, req *dto.LoginWithPasswordRequest) (*entities.User, error)
|
||||
LoginWithPassword(ctx context.Context, req *commands.LoginWithPasswordCommand) (*entities.User, error)
|
||||
// 短信验证码登录
|
||||
LoginWithSMS(ctx context.Context, req *dto.LoginWithSMSRequest) (*entities.User, error)
|
||||
LoginWithSMS(ctx context.Context, req *commands.LoginWithSMSCommand) (*entities.User, error)
|
||||
// 修改密码
|
||||
ChangePassword(ctx context.Context, userID string, req *dto.ChangePasswordRequest) error
|
||||
ChangePassword(ctx context.Context, userID string, req *commands.ChangePasswordCommand) error
|
||||
// 根据ID获取用户
|
||||
GetByID(ctx context.Context, id string) (*entities.User, error)
|
||||
}
|
||||
|
||||
@@ -177,6 +177,11 @@ func (l *ZapLogger) Sync() error {
|
||||
return l.logger.Sync()
|
||||
}
|
||||
|
||||
// GetZapLogger 获取内部的zap.Logger实例
|
||||
func (l *ZapLogger) GetZapLogger() *zap.Logger {
|
||||
return l.logger
|
||||
}
|
||||
|
||||
// getTraceIDFromContext 从上下文获取追踪ID
|
||||
func getTraceIDFromContext(ctx context.Context) string {
|
||||
if traceID := ctx.Value("trace_id"); traceID != nil {
|
||||
|
||||
@@ -1,515 +0,0 @@
|
||||
package notification
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"crypto/hmac"
|
||||
"crypto/sha256"
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
// WeChatWorkService 企业微信通知服务
|
||||
type WeChatWorkService struct {
|
||||
webhookURL string
|
||||
secret string
|
||||
timeout time.Duration
|
||||
logger *zap.Logger
|
||||
}
|
||||
|
||||
// WechatWorkConfig 企业微信配置
|
||||
type WechatWorkConfig struct {
|
||||
WebhookURL string `yaml:"webhook_url"`
|
||||
Timeout time.Duration `yaml:"timeout"`
|
||||
}
|
||||
|
||||
// WechatWorkMessage 企业微信消息
|
||||
type WechatWorkMessage struct {
|
||||
MsgType string `json:"msgtype"`
|
||||
Text *WechatWorkText `json:"text,omitempty"`
|
||||
Markdown *WechatWorkMarkdown `json:"markdown,omitempty"`
|
||||
}
|
||||
|
||||
// WechatWorkText 文本消息
|
||||
type WechatWorkText struct {
|
||||
Content string `json:"content"`
|
||||
MentionedList []string `json:"mentioned_list,omitempty"`
|
||||
MentionedMobileList []string `json:"mentioned_mobile_list,omitempty"`
|
||||
}
|
||||
|
||||
// WechatWorkMarkdown Markdown消息
|
||||
type WechatWorkMarkdown struct {
|
||||
Content string `json:"content"`
|
||||
}
|
||||
|
||||
// NewWeChatWorkService 创建企业微信通知服务
|
||||
func NewWeChatWorkService(webhookURL, secret string, logger *zap.Logger) *WeChatWorkService {
|
||||
return &WeChatWorkService{
|
||||
webhookURL: webhookURL,
|
||||
secret: secret,
|
||||
timeout: 30 * time.Second,
|
||||
logger: logger,
|
||||
}
|
||||
}
|
||||
|
||||
// SendTextMessage 发送文本消息
|
||||
func (s *WeChatWorkService) SendTextMessage(ctx context.Context, content string, mentionedList []string, mentionedMobileList []string) error {
|
||||
s.logger.Info("发送企业微信文本消息",
|
||||
zap.String("content", content),
|
||||
zap.Strings("mentioned_list", mentionedList),
|
||||
)
|
||||
|
||||
message := map[string]interface{}{
|
||||
"msgtype": "text",
|
||||
"text": map[string]interface{}{
|
||||
"content": content,
|
||||
"mentioned_list": mentionedList,
|
||||
"mentioned_mobile_list": mentionedMobileList,
|
||||
},
|
||||
}
|
||||
|
||||
return s.sendMessage(ctx, message)
|
||||
}
|
||||
|
||||
// SendMarkdownMessage 发送Markdown消息
|
||||
func (s *WeChatWorkService) SendMarkdownMessage(ctx context.Context, content string) error {
|
||||
s.logger.Info("发送企业微信Markdown消息", zap.String("content", content))
|
||||
|
||||
message := map[string]interface{}{
|
||||
"msgtype": "markdown",
|
||||
"markdown": map[string]interface{}{
|
||||
"content": content,
|
||||
},
|
||||
}
|
||||
|
||||
return s.sendMessage(ctx, message)
|
||||
}
|
||||
|
||||
// SendCardMessage 发送卡片消息
|
||||
func (s *WeChatWorkService) SendCardMessage(ctx context.Context, title, description, url string, btnText string) error {
|
||||
s.logger.Info("发送企业微信卡片消息",
|
||||
zap.String("title", title),
|
||||
zap.String("description", description),
|
||||
)
|
||||
|
||||
message := map[string]interface{}{
|
||||
"msgtype": "template_card",
|
||||
"template_card": map[string]interface{}{
|
||||
"card_type": "text_notice",
|
||||
"source": map[string]interface{}{
|
||||
"icon_url": "https://example.com/icon.png",
|
||||
"desc": "企业认证系统",
|
||||
},
|
||||
"main_title": map[string]interface{}{
|
||||
"title": title,
|
||||
},
|
||||
"horizontal_content_list": []map[string]interface{}{
|
||||
{
|
||||
"keyname": "描述",
|
||||
"value": description,
|
||||
},
|
||||
},
|
||||
"jump_list": []map[string]interface{}{
|
||||
{
|
||||
"type": "1",
|
||||
"title": btnText,
|
||||
"url": url,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
return s.sendMessage(ctx, message)
|
||||
}
|
||||
|
||||
// SendCertificationNotification 发送认证相关通知
|
||||
func (s *WeChatWorkService) SendCertificationNotification(ctx context.Context, notificationType string, data map[string]interface{}) error {
|
||||
s.logger.Info("发送认证通知", zap.String("type", notificationType))
|
||||
|
||||
switch notificationType {
|
||||
case "new_application":
|
||||
return s.sendNewApplicationNotification(ctx, data)
|
||||
case "ocr_success":
|
||||
return s.sendOCRSuccessNotification(ctx, data)
|
||||
case "ocr_failed":
|
||||
return s.sendOCRFailedNotification(ctx, data)
|
||||
case "face_verify_success":
|
||||
return s.sendFaceVerifySuccessNotification(ctx, data)
|
||||
case "face_verify_failed":
|
||||
return s.sendFaceVerifyFailedNotification(ctx, data)
|
||||
case "admin_approved":
|
||||
return s.sendAdminApprovedNotification(ctx, data)
|
||||
case "admin_rejected":
|
||||
return s.sendAdminRejectedNotification(ctx, data)
|
||||
case "contract_signed":
|
||||
return s.sendContractSignedNotification(ctx, data)
|
||||
case "certification_completed":
|
||||
return s.sendCertificationCompletedNotification(ctx, data)
|
||||
default:
|
||||
return fmt.Errorf("不支持的通知类型: %s", notificationType)
|
||||
}
|
||||
}
|
||||
|
||||
// sendNewApplicationNotification 发送新申请通知
|
||||
func (s *WeChatWorkService) sendNewApplicationNotification(ctx context.Context, data map[string]interface{}) error {
|
||||
companyName := data["company_name"].(string)
|
||||
applicantName := data["applicant_name"].(string)
|
||||
applicationID := data["application_id"].(string)
|
||||
|
||||
content := fmt.Sprintf(`## 🆕 新的企业认证申请
|
||||
|
||||
**企业名称**: %s
|
||||
**申请人**: %s
|
||||
**申请ID**: %s
|
||||
**申请时间**: %s
|
||||
|
||||
请管理员及时审核处理。`,
|
||||
companyName,
|
||||
applicantName,
|
||||
applicationID,
|
||||
time.Now().Format("2006-01-02 15:04:05"))
|
||||
|
||||
return s.SendMarkdownMessage(ctx, content)
|
||||
}
|
||||
|
||||
// sendOCRSuccessNotification 发送OCR识别成功通知
|
||||
func (s *WeChatWorkService) sendOCRSuccessNotification(ctx context.Context, data map[string]interface{}) error {
|
||||
companyName := data["company_name"].(string)
|
||||
confidence := data["confidence"].(float64)
|
||||
applicationID := data["application_id"].(string)
|
||||
|
||||
content := fmt.Sprintf(`## ✅ OCR识别成功
|
||||
|
||||
**企业名称**: %s
|
||||
**识别置信度**: %.2f%%
|
||||
**申请ID**: %s
|
||||
**识别时间**: %s
|
||||
|
||||
营业执照信息已自动提取,请用户确认信息。`,
|
||||
companyName,
|
||||
confidence*100,
|
||||
applicationID,
|
||||
time.Now().Format("2006-01-02 15:04:05"))
|
||||
|
||||
return s.SendMarkdownMessage(ctx, content)
|
||||
}
|
||||
|
||||
// sendOCRFailedNotification 发送OCR识别失败通知
|
||||
func (s *WeChatWorkService) sendOCRFailedNotification(ctx context.Context, data map[string]interface{}) error {
|
||||
applicationID := data["application_id"].(string)
|
||||
errorMsg := data["error_message"].(string)
|
||||
|
||||
content := fmt.Sprintf(`## ❌ OCR识别失败
|
||||
|
||||
**申请ID**: %s
|
||||
**错误信息**: %s
|
||||
**失败时间**: %s
|
||||
|
||||
请检查营业执照图片质量或联系技术支持。`,
|
||||
applicationID,
|
||||
errorMsg,
|
||||
time.Now().Format("2006-01-02 15:04:05"))
|
||||
|
||||
return s.SendMarkdownMessage(ctx, content)
|
||||
}
|
||||
|
||||
// sendFaceVerifySuccessNotification 发送人脸识别成功通知
|
||||
func (s *WeChatWorkService) sendFaceVerifySuccessNotification(ctx context.Context, data map[string]interface{}) error {
|
||||
applicantName := data["applicant_name"].(string)
|
||||
applicationID := data["application_id"].(string)
|
||||
confidence := data["confidence"].(float64)
|
||||
|
||||
content := fmt.Sprintf(`## ✅ 人脸识别成功
|
||||
|
||||
**申请人**: %s
|
||||
**申请ID**: %s
|
||||
**识别置信度**: %.2f%%
|
||||
**识别时间**: %s
|
||||
|
||||
身份验证通过,可以进行下一步操作。`,
|
||||
applicantName,
|
||||
applicationID,
|
||||
confidence*100,
|
||||
time.Now().Format("2006-01-02 15:04:05"))
|
||||
|
||||
return s.SendMarkdownMessage(ctx, content)
|
||||
}
|
||||
|
||||
// sendFaceVerifyFailedNotification 发送人脸识别失败通知
|
||||
func (s *WeChatWorkService) sendFaceVerifyFailedNotification(ctx context.Context, data map[string]interface{}) error {
|
||||
applicantName := data["applicant_name"].(string)
|
||||
applicationID := data["application_id"].(string)
|
||||
errorMsg := data["error_message"].(string)
|
||||
|
||||
content := fmt.Sprintf(`## ❌ 人脸识别失败
|
||||
|
||||
**申请人**: %s
|
||||
**申请ID**: %s
|
||||
**错误信息**: %s
|
||||
**失败时间**: %s
|
||||
|
||||
请重新进行人脸识别或联系技术支持。`,
|
||||
applicantName,
|
||||
applicationID,
|
||||
errorMsg,
|
||||
time.Now().Format("2006-01-02 15:04:05"))
|
||||
|
||||
return s.SendMarkdownMessage(ctx, content)
|
||||
}
|
||||
|
||||
// sendAdminApprovedNotification 发送管理员审核通过通知
|
||||
func (s *WeChatWorkService) sendAdminApprovedNotification(ctx context.Context, data map[string]interface{}) error {
|
||||
companyName := data["company_name"].(string)
|
||||
applicationID := data["application_id"].(string)
|
||||
adminName := data["admin_name"].(string)
|
||||
comment := data["comment"].(string)
|
||||
|
||||
content := fmt.Sprintf(`## ✅ 管理员审核通过
|
||||
|
||||
**企业名称**: %s
|
||||
**申请ID**: %s
|
||||
**审核人**: %s
|
||||
**审核意见**: %s
|
||||
**审核时间**: %s
|
||||
|
||||
认证申请已通过审核,请用户签署电子合同。`,
|
||||
companyName,
|
||||
applicationID,
|
||||
adminName,
|
||||
comment,
|
||||
time.Now().Format("2006-01-02 15:04:05"))
|
||||
|
||||
return s.SendMarkdownMessage(ctx, content)
|
||||
}
|
||||
|
||||
// sendAdminRejectedNotification 发送管理员审核拒绝通知
|
||||
func (s *WeChatWorkService) sendAdminRejectedNotification(ctx context.Context, data map[string]interface{}) error {
|
||||
companyName := data["company_name"].(string)
|
||||
applicationID := data["application_id"].(string)
|
||||
adminName := data["admin_name"].(string)
|
||||
reason := data["reason"].(string)
|
||||
|
||||
content := fmt.Sprintf(`## ❌ 管理员审核拒绝
|
||||
|
||||
**企业名称**: %s
|
||||
**申请ID**: %s
|
||||
**审核人**: %s
|
||||
**拒绝原因**: %s
|
||||
**审核时间**: %s
|
||||
|
||||
认证申请被拒绝,请根据反馈意见重新提交。`,
|
||||
companyName,
|
||||
applicationID,
|
||||
adminName,
|
||||
reason,
|
||||
time.Now().Format("2006-01-02 15:04:05"))
|
||||
|
||||
return s.SendMarkdownMessage(ctx, content)
|
||||
}
|
||||
|
||||
// sendContractSignedNotification 发送合同签署通知
|
||||
func (s *WeChatWorkService) sendContractSignedNotification(ctx context.Context, data map[string]interface{}) error {
|
||||
companyName := data["company_name"].(string)
|
||||
applicationID := data["application_id"].(string)
|
||||
signerName := data["signer_name"].(string)
|
||||
|
||||
content := fmt.Sprintf(`## 📝 电子合同已签署
|
||||
|
||||
**企业名称**: %s
|
||||
**申请ID**: %s
|
||||
**签署人**: %s
|
||||
**签署时间**: %s
|
||||
|
||||
电子合同签署完成,系统将自动生成钱包和Access Key。`,
|
||||
companyName,
|
||||
applicationID,
|
||||
signerName,
|
||||
time.Now().Format("2006-01-02 15:04:05"))
|
||||
|
||||
return s.SendMarkdownMessage(ctx, content)
|
||||
}
|
||||
|
||||
// sendCertificationCompletedNotification 发送认证完成通知
|
||||
func (s *WeChatWorkService) sendCertificationCompletedNotification(ctx context.Context, data map[string]interface{}) error {
|
||||
companyName := data["company_name"].(string)
|
||||
applicationID := data["application_id"].(string)
|
||||
walletAddress := data["wallet_address"].(string)
|
||||
|
||||
content := fmt.Sprintf(`## 🎉 企业认证完成
|
||||
|
||||
**企业名称**: %s
|
||||
**申请ID**: %s
|
||||
**钱包地址**: %s
|
||||
**完成时间**: %s
|
||||
|
||||
恭喜!企业认证流程已完成,钱包和Access Key已生成。`,
|
||||
companyName,
|
||||
applicationID,
|
||||
walletAddress,
|
||||
time.Now().Format("2006-01-02 15:04:05"))
|
||||
|
||||
return s.SendMarkdownMessage(ctx, content)
|
||||
}
|
||||
|
||||
// sendMessage 发送消息到企业微信
|
||||
func (s *WeChatWorkService) sendMessage(ctx context.Context, message map[string]interface{}) error {
|
||||
// 生成签名URL
|
||||
signedURL := s.generateSignedURL()
|
||||
|
||||
// 序列化消息
|
||||
messageBytes, err := json.Marshal(message)
|
||||
if err != nil {
|
||||
return fmt.Errorf("序列化消息失败: %w", err)
|
||||
}
|
||||
|
||||
// 创建HTTP客户端
|
||||
client := &http.Client{
|
||||
Timeout: s.timeout,
|
||||
}
|
||||
|
||||
// 创建请求
|
||||
req, err := http.NewRequestWithContext(ctx, "POST", signedURL, bytes.NewBuffer(messageBytes))
|
||||
if err != nil {
|
||||
return fmt.Errorf("创建请求失败: %w", err)
|
||||
}
|
||||
|
||||
// 设置请求头
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
req.Header.Set("User-Agent", "tyapi-server/1.0")
|
||||
|
||||
// 发送请求
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
return fmt.Errorf("发送请求失败: %w", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
// 检查响应状态
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return fmt.Errorf("请求失败,状态码: %d", resp.StatusCode)
|
||||
}
|
||||
|
||||
// 解析响应
|
||||
var response map[string]interface{}
|
||||
if err := json.NewDecoder(resp.Body).Decode(&response); err != nil {
|
||||
return fmt.Errorf("解析响应失败: %w", err)
|
||||
}
|
||||
|
||||
// 检查错误码
|
||||
if errCode, ok := response["errcode"].(float64); ok && errCode != 0 {
|
||||
errMsg := response["errmsg"].(string)
|
||||
return fmt.Errorf("企业微信API错误: %d - %s", int(errCode), errMsg)
|
||||
}
|
||||
|
||||
s.logger.Info("企业微信消息发送成功", zap.Any("response", response))
|
||||
return nil
|
||||
}
|
||||
|
||||
// generateSignedURL 生成带签名的URL
|
||||
func (s *WeChatWorkService) generateSignedURL() string {
|
||||
if s.secret == "" {
|
||||
return s.webhookURL
|
||||
}
|
||||
|
||||
// 生成时间戳
|
||||
timestamp := time.Now().Unix()
|
||||
|
||||
// 生成随机字符串(这里简化处理,实际应该使用随机字符串)
|
||||
nonce := fmt.Sprintf("%d", timestamp)
|
||||
|
||||
// 构建签名字符串
|
||||
signStr := fmt.Sprintf("%d\n%s", timestamp, s.secret)
|
||||
|
||||
// 计算签名
|
||||
h := hmac.New(sha256.New, []byte(s.secret))
|
||||
h.Write([]byte(signStr))
|
||||
signature := base64.StdEncoding.EncodeToString(h.Sum(nil))
|
||||
|
||||
// 构建签名URL
|
||||
return fmt.Sprintf("%s×tamp=%d&nonce=%s&sign=%s",
|
||||
s.webhookURL, timestamp, nonce, signature)
|
||||
}
|
||||
|
||||
// SendSystemAlert 发送系统告警
|
||||
func (s *WeChatWorkService) SendSystemAlert(ctx context.Context, level, title, message string) error {
|
||||
s.logger.Info("发送系统告警",
|
||||
zap.String("level", level),
|
||||
zap.String("title", title),
|
||||
)
|
||||
|
||||
// 根据告警级别选择图标
|
||||
var icon string
|
||||
switch level {
|
||||
case "info":
|
||||
icon = "ℹ️"
|
||||
case "warning":
|
||||
icon = "⚠️"
|
||||
case "error":
|
||||
icon = "🚨"
|
||||
case "critical":
|
||||
icon = "💥"
|
||||
default:
|
||||
icon = "📢"
|
||||
}
|
||||
|
||||
content := fmt.Sprintf(`## %s 系统告警
|
||||
|
||||
**级别**: %s
|
||||
**标题**: %s
|
||||
**消息**: %s
|
||||
**时间**: %s
|
||||
|
||||
请相关人员及时处理。`,
|
||||
icon,
|
||||
level,
|
||||
title,
|
||||
message,
|
||||
time.Now().Format("2006-01-02 15:04:05"))
|
||||
|
||||
return s.SendMarkdownMessage(ctx, content)
|
||||
}
|
||||
|
||||
// SendDailyReport 发送每日报告
|
||||
func (s *WeChatWorkService) SendDailyReport(ctx context.Context, reportData map[string]interface{}) error {
|
||||
s.logger.Info("发送每日报告")
|
||||
|
||||
content := fmt.Sprintf(`## 📊 企业认证系统每日报告
|
||||
|
||||
**报告日期**: %s
|
||||
|
||||
### 统计数据
|
||||
- **新增申请**: %d
|
||||
- **OCR识别成功**: %d
|
||||
- **OCR识别失败**: %d
|
||||
- **人脸识别成功**: %d
|
||||
- **人脸识别失败**: %d
|
||||
- **审核通过**: %d
|
||||
- **审核拒绝**: %d
|
||||
- **认证完成**: %d
|
||||
|
||||
### 系统状态
|
||||
- **系统运行时间**: %s
|
||||
- **API调用次数**: %d
|
||||
- **错误次数**: %d
|
||||
|
||||
祝您工作愉快!`,
|
||||
time.Now().Format("2006-01-02"),
|
||||
reportData["new_applications"],
|
||||
reportData["ocr_success"],
|
||||
reportData["ocr_failed"],
|
||||
reportData["face_verify_success"],
|
||||
reportData["face_verify_failed"],
|
||||
reportData["admin_approved"],
|
||||
reportData["admin_rejected"],
|
||||
reportData["certification_completed"],
|
||||
reportData["uptime"],
|
||||
reportData["api_calls"],
|
||||
reportData["errors"])
|
||||
|
||||
return s.SendMarkdownMessage(ctx, content)
|
||||
}
|
||||
@@ -1,548 +0,0 @@
|
||||
package ocr
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"time"
|
||||
|
||||
"go.uber.org/zap"
|
||||
|
||||
"tyapi-server/internal/domains/certification/dto"
|
||||
)
|
||||
|
||||
// BaiduOCRService 百度OCR服务
|
||||
type BaiduOCRService struct {
|
||||
appID string
|
||||
apiKey string
|
||||
secretKey string
|
||||
endpoint string
|
||||
timeout time.Duration
|
||||
logger *zap.Logger
|
||||
}
|
||||
|
||||
// NewBaiduOCRService 创建百度OCR服务
|
||||
func NewBaiduOCRService(appID, apiKey, secretKey string, logger *zap.Logger) *BaiduOCRService {
|
||||
return &BaiduOCRService{
|
||||
appID: appID,
|
||||
apiKey: apiKey,
|
||||
secretKey: secretKey,
|
||||
endpoint: "https://aip.baidubce.com",
|
||||
timeout: 30 * time.Second,
|
||||
logger: logger,
|
||||
}
|
||||
}
|
||||
|
||||
// RecognizeBusinessLicense 识别营业执照
|
||||
func (s *BaiduOCRService) RecognizeBusinessLicense(ctx context.Context, imageBytes []byte) (*dto.BusinessLicenseResult, error) {
|
||||
s.logger.Info("开始识别营业执照", zap.Int("image_size", len(imageBytes)))
|
||||
|
||||
// 获取访问令牌
|
||||
accessToken, err := s.getAccessToken(ctx)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("获取访问令牌失败: %w", err)
|
||||
}
|
||||
|
||||
// 将图片转换为base64
|
||||
imageBase64 := base64.StdEncoding.EncodeToString(imageBytes)
|
||||
|
||||
// 构建请求参数
|
||||
params := url.Values{}
|
||||
params.Set("access_token", accessToken)
|
||||
params.Set("image", imageBase64)
|
||||
|
||||
// 发送请求
|
||||
apiURL := fmt.Sprintf("%s/rest/2.0/ocr/v1/business_license?%s", s.endpoint, params.Encode())
|
||||
resp, err := s.sendRequest(ctx, "POST", apiURL, nil)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("营业执照识别请求失败: %w", err)
|
||||
}
|
||||
|
||||
// 解析响应
|
||||
var result map[string]interface{}
|
||||
if err := json.Unmarshal(resp, &result); err != nil {
|
||||
return nil, fmt.Errorf("解析响应失败: %w", err)
|
||||
}
|
||||
|
||||
// 检查错误
|
||||
if errCode, ok := result["error_code"].(float64); ok && errCode != 0 {
|
||||
errorMsg := result["error_msg"].(string)
|
||||
return nil, fmt.Errorf("OCR识别失败: %s", errorMsg)
|
||||
}
|
||||
|
||||
// 解析识别结果
|
||||
licenseResult := s.parseBusinessLicenseResult(result)
|
||||
|
||||
s.logger.Info("营业执照识别成功",
|
||||
zap.String("company_name", licenseResult.CompanyName),
|
||||
zap.String("legal_representative", licenseResult.LegalRepresentative),
|
||||
zap.String("registered_capital", licenseResult.RegisteredCapital),
|
||||
)
|
||||
|
||||
return licenseResult, nil
|
||||
}
|
||||
|
||||
// RecognizeIDCard 识别身份证
|
||||
func (s *BaiduOCRService) RecognizeIDCard(ctx context.Context, imageBytes []byte, side string) (*dto.IDCardResult, error) {
|
||||
s.logger.Info("开始识别身份证", zap.String("side", side), zap.Int("image_size", len(imageBytes)))
|
||||
|
||||
// 获取访问令牌
|
||||
accessToken, err := s.getAccessToken(ctx)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("获取访问令牌失败: %w", err)
|
||||
}
|
||||
|
||||
// 将图片转换为base64
|
||||
imageBase64 := base64.StdEncoding.EncodeToString(imageBytes)
|
||||
|
||||
// 构建请求参数
|
||||
params := url.Values{}
|
||||
params.Set("access_token", accessToken)
|
||||
params.Set("image", imageBase64)
|
||||
params.Set("side", side)
|
||||
|
||||
// 发送请求
|
||||
apiURL := fmt.Sprintf("%s/rest/2.0/ocr/v1/idcard?%s", s.endpoint, params.Encode())
|
||||
resp, err := s.sendRequest(ctx, "POST", apiURL, nil)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("身份证识别请求失败: %w", err)
|
||||
}
|
||||
|
||||
// 解析响应
|
||||
var result map[string]interface{}
|
||||
if err := json.Unmarshal(resp, &result); err != nil {
|
||||
return nil, fmt.Errorf("解析响应失败: %w", err)
|
||||
}
|
||||
|
||||
// 检查错误
|
||||
if errCode, ok := result["error_code"].(float64); ok && errCode != 0 {
|
||||
errorMsg := result["error_msg"].(string)
|
||||
return nil, fmt.Errorf("OCR识别失败: %s", errorMsg)
|
||||
}
|
||||
|
||||
// 解析识别结果
|
||||
idCardResult := s.parseIDCardResult(result, side)
|
||||
|
||||
s.logger.Info("身份证识别成功",
|
||||
zap.String("name", idCardResult.Name),
|
||||
zap.String("id_number", idCardResult.IDNumber),
|
||||
zap.String("side", side),
|
||||
)
|
||||
|
||||
return idCardResult, nil
|
||||
}
|
||||
|
||||
// RecognizeGeneralText 通用文字识别
|
||||
func (s *BaiduOCRService) RecognizeGeneralText(ctx context.Context, imageBytes []byte) (*dto.GeneralTextResult, error) {
|
||||
s.logger.Info("开始通用文字识别", zap.Int("image_size", len(imageBytes)))
|
||||
|
||||
// 获取访问令牌
|
||||
accessToken, err := s.getAccessToken(ctx)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("获取访问令牌失败: %w", err)
|
||||
}
|
||||
|
||||
// 将图片转换为base64
|
||||
imageBase64 := base64.StdEncoding.EncodeToString(imageBytes)
|
||||
|
||||
// 构建请求参数
|
||||
params := url.Values{}
|
||||
params.Set("access_token", accessToken)
|
||||
params.Set("image", imageBase64)
|
||||
|
||||
// 发送请求
|
||||
apiURL := fmt.Sprintf("%s/rest/2.0/ocr/v1/general_basic?%s", s.endpoint, params.Encode())
|
||||
resp, err := s.sendRequest(ctx, "POST", apiURL, nil)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("通用文字识别请求失败: %w", err)
|
||||
}
|
||||
|
||||
// 解析响应
|
||||
var result map[string]interface{}
|
||||
if err := json.Unmarshal(resp, &result); err != nil {
|
||||
return nil, fmt.Errorf("解析响应失败: %w", err)
|
||||
}
|
||||
|
||||
// 检查错误
|
||||
if errCode, ok := result["error_code"].(float64); ok && errCode != 0 {
|
||||
errorMsg := result["error_msg"].(string)
|
||||
return nil, fmt.Errorf("OCR识别失败: %s", errorMsg)
|
||||
}
|
||||
|
||||
// 解析识别结果
|
||||
textResult := s.parseGeneralTextResult(result)
|
||||
|
||||
s.logger.Info("通用文字识别成功",
|
||||
zap.Int("word_count", len(textResult.Words)),
|
||||
zap.Float64("confidence", textResult.Confidence),
|
||||
)
|
||||
|
||||
return textResult, nil
|
||||
}
|
||||
|
||||
// RecognizeFromURL 从URL识别图片
|
||||
func (s *BaiduOCRService) RecognizeFromURL(ctx context.Context, imageURL string, ocrType string) (interface{}, error) {
|
||||
s.logger.Info("从URL识别图片", zap.String("url", imageURL), zap.String("type", ocrType))
|
||||
|
||||
// 下载图片
|
||||
imageBytes, err := s.downloadImage(ctx, imageURL)
|
||||
if err != nil {
|
||||
s.logger.Error("下载图片失败", zap.Error(err))
|
||||
return nil, fmt.Errorf("下载图片失败: %w", err)
|
||||
}
|
||||
|
||||
// 根据类型调用相应的识别方法
|
||||
switch ocrType {
|
||||
case "business_license":
|
||||
return s.RecognizeBusinessLicense(ctx, imageBytes)
|
||||
case "idcard_front":
|
||||
return s.RecognizeIDCard(ctx, imageBytes, "front")
|
||||
case "idcard_back":
|
||||
return s.RecognizeIDCard(ctx, imageBytes, "back")
|
||||
case "general_text":
|
||||
return s.RecognizeGeneralText(ctx, imageBytes)
|
||||
default:
|
||||
return nil, fmt.Errorf("不支持的OCR类型: %s", ocrType)
|
||||
}
|
||||
}
|
||||
|
||||
// getAccessToken 获取百度API访问令牌
|
||||
func (s *BaiduOCRService) getAccessToken(ctx context.Context) (string, error) {
|
||||
// 构建获取访问令牌的URL
|
||||
tokenURL := fmt.Sprintf("%s/oauth/2.0/token?grant_type=client_credentials&client_id=%s&client_secret=%s",
|
||||
s.endpoint, s.apiKey, s.secretKey)
|
||||
|
||||
// 发送请求
|
||||
resp, err := s.sendRequest(ctx, "POST", tokenURL, nil)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("获取访问令牌请求失败: %w", err)
|
||||
}
|
||||
|
||||
// 解析响应
|
||||
var result map[string]interface{}
|
||||
if err := json.Unmarshal(resp, &result); err != nil {
|
||||
return "", fmt.Errorf("解析访问令牌响应失败: %w", err)
|
||||
}
|
||||
|
||||
// 检查错误
|
||||
if errCode, ok := result["error"].(string); ok && errCode != "" {
|
||||
errorDesc := result["error_description"].(string)
|
||||
return "", fmt.Errorf("获取访问令牌失败: %s - %s", errCode, errorDesc)
|
||||
}
|
||||
|
||||
// 提取访问令牌
|
||||
accessToken, ok := result["access_token"].(string)
|
||||
if !ok {
|
||||
return "", fmt.Errorf("响应中未找到访问令牌")
|
||||
}
|
||||
|
||||
return accessToken, nil
|
||||
}
|
||||
|
||||
// sendRequest 发送HTTP请求
|
||||
func (s *BaiduOCRService) sendRequest(ctx context.Context, method, url string, body io.Reader) ([]byte, error) {
|
||||
// 创建HTTP客户端
|
||||
client := &http.Client{
|
||||
Timeout: s.timeout,
|
||||
}
|
||||
|
||||
// 创建请求
|
||||
req, err := http.NewRequestWithContext(ctx, method, url, body)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("创建请求失败: %w", err)
|
||||
}
|
||||
|
||||
// 设置请求头
|
||||
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
|
||||
req.Header.Set("User-Agent", "tyapi-server/1.0")
|
||||
|
||||
// 发送请求
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("发送请求失败: %w", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
// 检查响应状态
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return nil, fmt.Errorf("请求失败,状态码: %d", resp.StatusCode)
|
||||
}
|
||||
|
||||
// 读取响应内容
|
||||
responseBody, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("读取响应内容失败: %w", err)
|
||||
}
|
||||
|
||||
return responseBody, nil
|
||||
}
|
||||
|
||||
// parseBusinessLicenseResult 解析营业执照识别结果
|
||||
func (s *BaiduOCRService) parseBusinessLicenseResult(result map[string]interface{}) *dto.BusinessLicenseResult {
|
||||
// 解析百度OCR返回的结果
|
||||
wordsResult := result["words_result"].(map[string]interface{})
|
||||
|
||||
licenseResult := &dto.BusinessLicenseResult{
|
||||
Confidence: s.extractConfidence(result),
|
||||
Words: s.extractWords(result),
|
||||
}
|
||||
|
||||
// 提取关键字段
|
||||
if companyName, ok := wordsResult["单位名称"]; ok {
|
||||
if word, ok := companyName.(map[string]interface{}); ok {
|
||||
licenseResult.CompanyName = word["words"].(string)
|
||||
}
|
||||
}
|
||||
|
||||
if legalRep, ok := wordsResult["法人"]; ok {
|
||||
if word, ok := legalRep.(map[string]interface{}); ok {
|
||||
licenseResult.LegalRepresentative = word["words"].(string)
|
||||
}
|
||||
}
|
||||
|
||||
if regCapital, ok := wordsResult["注册资本"]; ok {
|
||||
if word, ok := regCapital.(map[string]interface{}); ok {
|
||||
licenseResult.RegisteredCapital = word["words"].(string)
|
||||
}
|
||||
}
|
||||
|
||||
if regAddress, ok := wordsResult["地址"]; ok {
|
||||
if word, ok := regAddress.(map[string]interface{}); ok {
|
||||
licenseResult.RegisteredAddress = word["words"].(string)
|
||||
}
|
||||
}
|
||||
|
||||
if regNumber, ok := wordsResult["社会信用代码"]; ok {
|
||||
if word, ok := regNumber.(map[string]interface{}); ok {
|
||||
licenseResult.RegistrationNumber = word["words"].(string)
|
||||
}
|
||||
}
|
||||
|
||||
if businessScope, ok := wordsResult["经营范围"]; ok {
|
||||
if word, ok := businessScope.(map[string]interface{}); ok {
|
||||
licenseResult.BusinessScope = word["words"].(string)
|
||||
}
|
||||
}
|
||||
|
||||
if regDate, ok := wordsResult["成立日期"]; ok {
|
||||
if word, ok := regDate.(map[string]interface{}); ok {
|
||||
licenseResult.RegistrationDate = word["words"].(string)
|
||||
}
|
||||
}
|
||||
|
||||
if validDate, ok := wordsResult["营业期限"]; ok {
|
||||
if word, ok := validDate.(map[string]interface{}); ok {
|
||||
licenseResult.ValidDate = word["words"].(string)
|
||||
}
|
||||
}
|
||||
|
||||
return licenseResult
|
||||
}
|
||||
|
||||
// parseIDCardResult 解析身份证识别结果
|
||||
func (s *BaiduOCRService) parseIDCardResult(result map[string]interface{}, side string) *dto.IDCardResult {
|
||||
wordsResult := result["words_result"].(map[string]interface{})
|
||||
|
||||
idCardResult := &dto.IDCardResult{
|
||||
Side: side,
|
||||
Confidence: s.extractConfidence(result),
|
||||
Words: s.extractWords(result),
|
||||
}
|
||||
|
||||
if side == "front" {
|
||||
// 正面信息
|
||||
if name, ok := wordsResult["姓名"]; ok {
|
||||
if word, ok := name.(map[string]interface{}); ok {
|
||||
idCardResult.Name = word["words"].(string)
|
||||
}
|
||||
}
|
||||
|
||||
if sex, ok := wordsResult["性别"]; ok {
|
||||
if word, ok := sex.(map[string]interface{}); ok {
|
||||
idCardResult.Sex = word["words"].(string)
|
||||
}
|
||||
}
|
||||
|
||||
if nation, ok := wordsResult["民族"]; ok {
|
||||
if word, ok := nation.(map[string]interface{}); ok {
|
||||
idCardResult.Nation = word["words"].(string)
|
||||
}
|
||||
}
|
||||
|
||||
if birth, ok := wordsResult["出生"]; ok {
|
||||
if word, ok := birth.(map[string]interface{}); ok {
|
||||
idCardResult.BirthDate = word["words"].(string)
|
||||
}
|
||||
}
|
||||
|
||||
if address, ok := wordsResult["住址"]; ok {
|
||||
if word, ok := address.(map[string]interface{}); ok {
|
||||
idCardResult.Address = word["words"].(string)
|
||||
}
|
||||
}
|
||||
|
||||
if idNumber, ok := wordsResult["公民身份号码"]; ok {
|
||||
if word, ok := idNumber.(map[string]interface{}); ok {
|
||||
idCardResult.IDNumber = word["words"].(string)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// 背面信息
|
||||
if authority, ok := wordsResult["签发机关"]; ok {
|
||||
if word, ok := authority.(map[string]interface{}); ok {
|
||||
idCardResult.IssuingAuthority = word["words"].(string)
|
||||
}
|
||||
}
|
||||
|
||||
if validDate, ok := wordsResult["有效期限"]; ok {
|
||||
if word, ok := validDate.(map[string]interface{}); ok {
|
||||
idCardResult.ValidDate = word["words"].(string)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return idCardResult
|
||||
}
|
||||
|
||||
// parseGeneralTextResult 解析通用文字识别结果
|
||||
func (s *BaiduOCRService) parseGeneralTextResult(result map[string]interface{}) *dto.GeneralTextResult {
|
||||
wordsResult := result["words_result"].([]interface{})
|
||||
|
||||
textResult := &dto.GeneralTextResult{
|
||||
Confidence: s.extractConfidence(result),
|
||||
Words: make([]string, 0, len(wordsResult)),
|
||||
}
|
||||
|
||||
// 提取所有识别的文字
|
||||
for _, word := range wordsResult {
|
||||
if wordMap, ok := word.(map[string]interface{}); ok {
|
||||
if words, ok := wordMap["words"].(string); ok {
|
||||
textResult.Words = append(textResult.Words, words)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return textResult
|
||||
}
|
||||
|
||||
// extractConfidence 提取置信度
|
||||
func (s *BaiduOCRService) extractConfidence(result map[string]interface{}) float64 {
|
||||
if confidence, ok := result["confidence"].(float64); ok {
|
||||
return confidence
|
||||
}
|
||||
return 0.0
|
||||
}
|
||||
|
||||
// extractWords 提取识别的文字
|
||||
func (s *BaiduOCRService) extractWords(result map[string]interface{}) []string {
|
||||
words := make([]string, 0)
|
||||
|
||||
if wordsResult, ok := result["words_result"]; ok {
|
||||
switch v := wordsResult.(type) {
|
||||
case map[string]interface{}:
|
||||
// 营业执照等结构化文档
|
||||
for _, word := range v {
|
||||
if wordMap, ok := word.(map[string]interface{}); ok {
|
||||
if wordsStr, ok := wordMap["words"].(string); ok {
|
||||
words = append(words, wordsStr)
|
||||
}
|
||||
}
|
||||
}
|
||||
case []interface{}:
|
||||
// 通用文字识别
|
||||
for _, word := range v {
|
||||
if wordMap, ok := word.(map[string]interface{}); ok {
|
||||
if wordsStr, ok := wordMap["words"].(string); ok {
|
||||
words = append(words, wordsStr)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return words
|
||||
}
|
||||
|
||||
// downloadImage 下载图片
|
||||
func (s *BaiduOCRService) downloadImage(ctx context.Context, imageURL string) ([]byte, error) {
|
||||
// 创建HTTP客户端
|
||||
client := &http.Client{
|
||||
Timeout: 30 * time.Second,
|
||||
}
|
||||
|
||||
// 创建请求
|
||||
req, err := http.NewRequestWithContext(ctx, "GET", imageURL, nil)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("创建请求失败: %w", err)
|
||||
}
|
||||
|
||||
// 发送请求
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("下载图片失败: %w", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
// 检查响应状态
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return nil, fmt.Errorf("下载图片失败,状态码: %d", resp.StatusCode)
|
||||
}
|
||||
|
||||
// 读取响应内容
|
||||
imageBytes, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("读取图片内容失败: %w", err)
|
||||
}
|
||||
|
||||
return imageBytes, nil
|
||||
}
|
||||
|
||||
// ValidateBusinessLicense 验证营业执照识别结果
|
||||
func (s *BaiduOCRService) ValidateBusinessLicense(result *dto.BusinessLicenseResult) error {
|
||||
if result.Confidence < 0.8 {
|
||||
return fmt.Errorf("识别置信度过低: %.2f", result.Confidence)
|
||||
}
|
||||
|
||||
if result.CompanyName == "" {
|
||||
return fmt.Errorf("未能识别公司名称")
|
||||
}
|
||||
|
||||
if result.LegalRepresentative == "" {
|
||||
return fmt.Errorf("未能识别法定代表人")
|
||||
}
|
||||
|
||||
if result.RegistrationNumber == "" {
|
||||
return fmt.Errorf("未能识别统一社会信用代码")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// ValidateIDCard 验证身份证识别结果
|
||||
func (s *BaiduOCRService) ValidateIDCard(result *dto.IDCardResult) error {
|
||||
if result.Confidence < 0.8 {
|
||||
return fmt.Errorf("识别置信度过低: %.2f", result.Confidence)
|
||||
}
|
||||
|
||||
if result.Side == "front" {
|
||||
if result.Name == "" {
|
||||
return fmt.Errorf("未能识别姓名")
|
||||
}
|
||||
if result.IDNumber == "" {
|
||||
return fmt.Errorf("未能识别身份证号码")
|
||||
}
|
||||
} else {
|
||||
if result.IssuingAuthority == "" {
|
||||
return fmt.Errorf("未能识别签发机关")
|
||||
}
|
||||
if result.ValidDate == "" {
|
||||
return fmt.Errorf("未能识别有效期限")
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -2,23 +2,31 @@ package ocr
|
||||
|
||||
import (
|
||||
"context"
|
||||
"tyapi-server/internal/domains/certification/dto"
|
||||
"tyapi-server/internal/application/certification/dto/responses"
|
||||
)
|
||||
|
||||
// OCRService OCR识别服务接口
|
||||
type OCRService interface {
|
||||
// 识别营业执照
|
||||
RecognizeBusinessLicense(ctx context.Context, imageURL string) (*dto.OCREnterpriseInfo, error)
|
||||
RecognizeBusinessLicenseFromBytes(ctx context.Context, imageBytes []byte) (*dto.OCREnterpriseInfo, error)
|
||||
RecognizeBusinessLicense(ctx context.Context, imageBytes []byte) (*responses.BusinessLicenseResult, error)
|
||||
|
||||
// 识别身份证
|
||||
RecognizeIDCard(ctx context.Context, imageURL string, side string) (*IDCardInfo, error)
|
||||
RecognizeIDCard(ctx context.Context, imageBytes []byte, side string) (*responses.IDCardResult, error)
|
||||
|
||||
// 通用文字识别
|
||||
RecognizeGeneralText(ctx context.Context, imageURL string) (*GeneralTextResult, error)
|
||||
RecognizeGeneralText(ctx context.Context, imageBytes []byte) (*responses.GeneralTextResult, error)
|
||||
|
||||
// 从URL识别图片
|
||||
RecognizeFromURL(ctx context.Context, imageURL string, ocrType string) (interface{}, error)
|
||||
|
||||
// 验证营业执照结果
|
||||
ValidateBusinessLicense(result *responses.BusinessLicenseResult) error
|
||||
|
||||
// 验证身份证结果
|
||||
ValidateIDCard(result *responses.IDCardResult) error
|
||||
}
|
||||
|
||||
// IDCardInfo 身份证识别信息
|
||||
// IDCardInfo 身份证识别信息(保留兼容性)
|
||||
type IDCardInfo struct {
|
||||
Name string `json:"name"` // 姓名
|
||||
IDCardNumber string `json:"id_card_number"` // 身份证号
|
||||
@@ -31,7 +39,7 @@ type IDCardInfo struct {
|
||||
Confidence float64 `json:"confidence"` // 识别置信度
|
||||
}
|
||||
|
||||
// GeneralTextResult 通用文字识别结果
|
||||
// GeneralTextResult 通用文字识别结果(保留兼容性)
|
||||
type GeneralTextResult struct {
|
||||
Words []TextLine `json:"words"` // 识别的文字行
|
||||
Confidence float64 `json:"confidence"` // 整体置信度
|
||||
|
||||
@@ -1,129 +0,0 @@
|
||||
package sms
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/rand"
|
||||
"fmt"
|
||||
"math/big"
|
||||
|
||||
"github.com/aliyun/alibaba-cloud-sdk-go/services/dysmsapi"
|
||||
"go.uber.org/zap"
|
||||
|
||||
"tyapi-server/internal/config"
|
||||
)
|
||||
|
||||
// Service 短信服务接口
|
||||
type Service interface {
|
||||
SendVerificationCode(ctx context.Context, phone string, code string) error
|
||||
GenerateCode(length int) string
|
||||
}
|
||||
|
||||
// AliSMSService 阿里云短信服务实现
|
||||
type AliSMSService struct {
|
||||
client *dysmsapi.Client
|
||||
config config.SMSConfig
|
||||
logger *zap.Logger
|
||||
}
|
||||
|
||||
// NewAliSMSService 创建阿里云短信服务
|
||||
func NewAliSMSService(cfg config.SMSConfig, logger *zap.Logger) (*AliSMSService, error) {
|
||||
client, err := dysmsapi.NewClientWithAccessKey("cn-hangzhou", cfg.AccessKeyID, cfg.AccessKeySecret)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("创建短信客户端失败: %w", err)
|
||||
}
|
||||
return &AliSMSService{
|
||||
client: client,
|
||||
config: cfg,
|
||||
logger: logger,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// SendVerificationCode 发送验证码
|
||||
func (s *AliSMSService) SendVerificationCode(ctx context.Context, phone string, code string) error {
|
||||
request := dysmsapi.CreateSendSmsRequest()
|
||||
request.Scheme = "https"
|
||||
request.PhoneNumbers = phone
|
||||
request.SignName = s.config.SignName
|
||||
request.TemplateCode = s.config.TemplateCode
|
||||
request.TemplateParam = fmt.Sprintf(`{"code":"%s"}`, code)
|
||||
|
||||
response, err := s.client.SendSms(request)
|
||||
if err != nil {
|
||||
s.logger.Error("Failed to send SMS",
|
||||
zap.String("phone", phone),
|
||||
zap.Error(err))
|
||||
return fmt.Errorf("短信发送失败: %w", err)
|
||||
}
|
||||
|
||||
if response.Code != "OK" {
|
||||
s.logger.Error("SMS send failed",
|
||||
zap.String("phone", phone),
|
||||
zap.String("code", response.Code),
|
||||
zap.String("message", response.Message))
|
||||
return fmt.Errorf("短信发送失败: %s - %s", response.Code, response.Message)
|
||||
}
|
||||
|
||||
s.logger.Info("SMS sent successfully",
|
||||
zap.String("phone", phone),
|
||||
zap.String("bizId", response.BizId))
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// GenerateCode 生成验证码
|
||||
func (s *AliSMSService) GenerateCode(length int) string {
|
||||
if length <= 0 {
|
||||
length = 6
|
||||
}
|
||||
|
||||
// 生成指定长度的数字验证码
|
||||
max := big.NewInt(int64(pow10(length)))
|
||||
n, _ := rand.Int(rand.Reader, max)
|
||||
|
||||
// 格式化为指定长度,不足时前面补0
|
||||
format := fmt.Sprintf("%%0%dd", length)
|
||||
return fmt.Sprintf(format, n.Int64())
|
||||
}
|
||||
|
||||
// pow10 计算10的n次方
|
||||
func pow10(n int) int {
|
||||
result := 1
|
||||
for i := 0; i < n; i++ {
|
||||
result *= 10
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
// MockSMSService 模拟短信服务(用于开发和测试)
|
||||
type MockSMSService struct {
|
||||
logger *zap.Logger
|
||||
}
|
||||
|
||||
// NewMockSMSService 创建模拟短信服务
|
||||
func NewMockSMSService(logger *zap.Logger) *MockSMSService {
|
||||
return &MockSMSService{
|
||||
logger: logger,
|
||||
}
|
||||
}
|
||||
|
||||
// SendVerificationCode 模拟发送验证码
|
||||
func (s *MockSMSService) SendVerificationCode(ctx context.Context, phone string, code string) error {
|
||||
s.logger.Info("Mock SMS sent",
|
||||
zap.String("phone", phone),
|
||||
zap.String("code", code))
|
||||
return nil
|
||||
}
|
||||
|
||||
// GenerateCode 生成验证码
|
||||
func (s *MockSMSService) GenerateCode(length int) string {
|
||||
if length <= 0 {
|
||||
length = 6
|
||||
}
|
||||
|
||||
// 开发环境使用固定验证码便于测试
|
||||
result := ""
|
||||
for i := 0; i < length; i++ {
|
||||
result += "1"
|
||||
}
|
||||
return result
|
||||
}
|
||||
@@ -1,332 +0,0 @@
|
||||
package storage
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/hmac"
|
||||
"crypto/sha1"
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
"io"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/qiniu/go-sdk/v7/auth/qbox"
|
||||
"github.com/qiniu/go-sdk/v7/storage"
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
// QiNiuStorageService 七牛云存储服务
|
||||
type QiNiuStorageService struct {
|
||||
accessKey string
|
||||
secretKey string
|
||||
bucket string
|
||||
domain string
|
||||
region string
|
||||
logger *zap.Logger
|
||||
mac *qbox.Mac
|
||||
bucketManager *storage.BucketManager
|
||||
}
|
||||
|
||||
// QiNiuStorageConfig 七牛云存储配置
|
||||
type QiNiuStorageConfig struct {
|
||||
AccessKey string `yaml:"access_key"`
|
||||
SecretKey string `yaml:"secret_key"`
|
||||
Bucket string `yaml:"bucket"`
|
||||
Domain string `yaml:"domain"`
|
||||
Region string `yaml:"region"`
|
||||
}
|
||||
|
||||
// NewQiNiuStorageService 创建七牛云存储服务
|
||||
func NewQiNiuStorageService(accessKey, secretKey, bucket, domain, region string, logger *zap.Logger) *QiNiuStorageService {
|
||||
mac := qbox.NewMac(accessKey, secretKey)
|
||||
cfg := storage.Config{
|
||||
Region: &storage.Zone{
|
||||
RsHost: fmt.Sprintf("rs-%s.qiniu.com", region),
|
||||
},
|
||||
}
|
||||
bucketManager := storage.NewBucketManager(mac, &cfg)
|
||||
|
||||
return &QiNiuStorageService{
|
||||
accessKey: accessKey,
|
||||
secretKey: secretKey,
|
||||
bucket: bucket,
|
||||
domain: domain,
|
||||
region: region,
|
||||
logger: logger,
|
||||
mac: mac,
|
||||
bucketManager: bucketManager,
|
||||
}
|
||||
}
|
||||
|
||||
// UploadFile 上传文件到七牛云
|
||||
func (s *QiNiuStorageService) UploadFile(ctx context.Context, fileBytes []byte, fileName string) (*UploadResult, error) {
|
||||
s.logger.Info("开始上传文件到七牛云",
|
||||
zap.String("file_name", fileName),
|
||||
zap.Int("file_size", len(fileBytes)),
|
||||
)
|
||||
|
||||
// 生成唯一的文件key
|
||||
key := s.generateFileKey(fileName)
|
||||
|
||||
// 创建上传凭证
|
||||
putPolicy := storage.PutPolicy{
|
||||
Scope: s.bucket,
|
||||
}
|
||||
upToken := putPolicy.UploadToken(s.mac)
|
||||
|
||||
// 配置上传参数
|
||||
cfg := storage.Config{
|
||||
Region: &storage.Zone{
|
||||
RsHost: fmt.Sprintf("rs-%s.qiniu.com", s.region),
|
||||
},
|
||||
}
|
||||
formUploader := storage.NewFormUploader(&cfg)
|
||||
ret := storage.PutRet{}
|
||||
|
||||
// 上传文件
|
||||
err := formUploader.Put(ctx, &ret, upToken, key, strings.NewReader(string(fileBytes)), int64(len(fileBytes)), &storage.PutExtra{})
|
||||
if err != nil {
|
||||
s.logger.Error("文件上传失败",
|
||||
zap.String("file_name", fileName),
|
||||
zap.String("key", key),
|
||||
zap.Error(err),
|
||||
)
|
||||
return nil, fmt.Errorf("文件上传失败: %w", err)
|
||||
}
|
||||
|
||||
// 构建文件URL
|
||||
fileURL := s.GetFileURL(ctx, key)
|
||||
|
||||
s.logger.Info("文件上传成功",
|
||||
zap.String("file_name", fileName),
|
||||
zap.String("key", key),
|
||||
zap.String("url", fileURL),
|
||||
)
|
||||
|
||||
return &UploadResult{
|
||||
Key: key,
|
||||
URL: fileURL,
|
||||
MimeType: s.getMimeType(fileName),
|
||||
Size: int64(len(fileBytes)),
|
||||
Hash: ret.Hash,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// GenerateUploadToken 生成上传凭证
|
||||
func (s *QiNiuStorageService) GenerateUploadToken(ctx context.Context, key string) (string, error) {
|
||||
putPolicy := storage.PutPolicy{
|
||||
Scope: s.bucket,
|
||||
// 设置过期时间(1小时)
|
||||
Expires: uint64(time.Now().Add(time.Hour).Unix()),
|
||||
}
|
||||
|
||||
token := putPolicy.UploadToken(s.mac)
|
||||
return token, nil
|
||||
}
|
||||
|
||||
// GetFileURL 获取文件访问URL
|
||||
func (s *QiNiuStorageService) GetFileURL(ctx context.Context, key string) string {
|
||||
// 如果是私有空间,需要生成带签名的URL
|
||||
if s.isPrivateBucket() {
|
||||
deadline := time.Now().Add(time.Hour).Unix() // 1小时过期
|
||||
privateAccessURL := storage.MakePrivateURL(s.mac, s.domain, key, deadline)
|
||||
return privateAccessURL
|
||||
}
|
||||
|
||||
// 公开空间直接返回URL
|
||||
return fmt.Sprintf("%s/%s", s.domain, key)
|
||||
}
|
||||
|
||||
// GetPrivateFileURL 获取私有文件访问URL
|
||||
func (s *QiNiuStorageService) GetPrivateFileURL(ctx context.Context, key string, expires int64) (string, error) {
|
||||
baseURL := s.GetFileURL(ctx, key)
|
||||
|
||||
// TODO: 实际集成七牛云SDK生成私有URL
|
||||
s.logger.Info("生成七牛云私有文件URL",
|
||||
zap.String("key", key),
|
||||
zap.Int64("expires", expires),
|
||||
)
|
||||
|
||||
// 模拟返回私有URL
|
||||
return fmt.Sprintf("%s?token=mock_private_token&expires=%d", baseURL, expires), nil
|
||||
}
|
||||
|
||||
// DeleteFile 删除文件
|
||||
func (s *QiNiuStorageService) DeleteFile(ctx context.Context, key string) error {
|
||||
s.logger.Info("删除七牛云文件", zap.String("key", key))
|
||||
|
||||
err := s.bucketManager.Delete(s.bucket, key)
|
||||
if err != nil {
|
||||
s.logger.Error("删除文件失败",
|
||||
zap.String("key", key),
|
||||
zap.Error(err),
|
||||
)
|
||||
return fmt.Errorf("删除文件失败: %w", err)
|
||||
}
|
||||
|
||||
s.logger.Info("文件删除成功", zap.String("key", key))
|
||||
return nil
|
||||
}
|
||||
|
||||
// FileExists 检查文件是否存在
|
||||
func (s *QiNiuStorageService) FileExists(ctx context.Context, key string) (bool, error) {
|
||||
// TODO: 实际集成七牛云SDK检查文件存在性
|
||||
s.logger.Info("检查七牛云文件存在性", zap.String("key", key))
|
||||
|
||||
// 模拟文件存在
|
||||
return true, nil
|
||||
}
|
||||
|
||||
// GetFileInfo 获取文件信息
|
||||
func (s *QiNiuStorageService) GetFileInfo(ctx context.Context, key string) (*FileInfo, error) {
|
||||
fileInfo, err := s.bucketManager.Stat(s.bucket, key)
|
||||
if err != nil {
|
||||
s.logger.Error("获取文件信息失败",
|
||||
zap.String("key", key),
|
||||
zap.Error(err),
|
||||
)
|
||||
return nil, fmt.Errorf("获取文件信息失败: %w", err)
|
||||
}
|
||||
|
||||
return &FileInfo{
|
||||
Key: key,
|
||||
Size: fileInfo.Fsize,
|
||||
MimeType: fileInfo.MimeType,
|
||||
Hash: fileInfo.Hash,
|
||||
PutTime: fileInfo.PutTime,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// ListFiles 列出文件
|
||||
func (s *QiNiuStorageService) ListFiles(ctx context.Context, prefix string, limit int) ([]*FileInfo, error) {
|
||||
entries, _, _, hasMore, err := s.bucketManager.ListFiles(s.bucket, prefix, "", "", limit)
|
||||
if err != nil {
|
||||
s.logger.Error("列出文件失败",
|
||||
zap.String("prefix", prefix),
|
||||
zap.Error(err),
|
||||
)
|
||||
return nil, fmt.Errorf("列出文件失败: %w", err)
|
||||
}
|
||||
|
||||
var fileInfos []*FileInfo
|
||||
for _, entry := range entries {
|
||||
fileInfo := &FileInfo{
|
||||
Key: entry.Key,
|
||||
Size: entry.Fsize,
|
||||
MimeType: entry.MimeType,
|
||||
Hash: entry.Hash,
|
||||
PutTime: entry.PutTime,
|
||||
}
|
||||
fileInfos = append(fileInfos, fileInfo)
|
||||
}
|
||||
|
||||
_ = hasMore // 暂时忽略hasMore
|
||||
return fileInfos, nil
|
||||
}
|
||||
|
||||
// generateFileKey 生成文件key
|
||||
func (s *QiNiuStorageService) generateFileKey(fileName string) string {
|
||||
// 生成时间戳
|
||||
timestamp := time.Now().Format("20060102_150405")
|
||||
// 生成随机字符串
|
||||
randomStr := fmt.Sprintf("%d", time.Now().UnixNano()%1000000)
|
||||
// 获取文件扩展名
|
||||
ext := filepath.Ext(fileName)
|
||||
// 构建key: 日期/时间戳_随机数.扩展名
|
||||
key := fmt.Sprintf("certification/%s/%s_%s%s",
|
||||
time.Now().Format("20060102"), timestamp, randomStr, ext)
|
||||
|
||||
return key
|
||||
}
|
||||
|
||||
// getMimeType 根据文件名获取MIME类型
|
||||
func (s *QiNiuStorageService) getMimeType(fileName string) string {
|
||||
ext := strings.ToLower(filepath.Ext(fileName))
|
||||
switch ext {
|
||||
case ".jpg", ".jpeg":
|
||||
return "image/jpeg"
|
||||
case ".png":
|
||||
return "image/png"
|
||||
case ".pdf":
|
||||
return "application/pdf"
|
||||
case ".gif":
|
||||
return "image/gif"
|
||||
case ".bmp":
|
||||
return "image/bmp"
|
||||
case ".webp":
|
||||
return "image/webp"
|
||||
default:
|
||||
return "application/octet-stream"
|
||||
}
|
||||
}
|
||||
|
||||
// isPrivateBucket 判断是否为私有空间
|
||||
func (s *QiNiuStorageService) isPrivateBucket() bool {
|
||||
// 这里可以根据配置或域名特征判断
|
||||
// 私有空间的域名通常包含特定标识
|
||||
return strings.Contains(s.domain, "private") ||
|
||||
strings.Contains(s.domain, "auth") ||
|
||||
strings.Contains(s.domain, "secure")
|
||||
}
|
||||
|
||||
// generateSignature 生成签名(用于私有空间访问)
|
||||
func (s *QiNiuStorageService) generateSignature(data string) string {
|
||||
h := hmac.New(sha1.New, []byte(s.secretKey))
|
||||
h.Write([]byte(data))
|
||||
return base64.URLEncoding.EncodeToString(h.Sum(nil))
|
||||
}
|
||||
|
||||
// UploadFromReader 从Reader上传文件
|
||||
func (s *QiNiuStorageService) UploadFromReader(ctx context.Context, reader io.Reader, fileName string, fileSize int64) (*UploadResult, error) {
|
||||
s.logger.Info("从Reader上传文件到七牛云",
|
||||
zap.String("file_name", fileName),
|
||||
zap.Int64("file_size", fileSize),
|
||||
)
|
||||
|
||||
// 生成唯一的文件key
|
||||
key := s.generateFileKey(fileName)
|
||||
|
||||
// 创建上传凭证
|
||||
putPolicy := storage.PutPolicy{
|
||||
Scope: s.bucket,
|
||||
}
|
||||
upToken := putPolicy.UploadToken(s.mac)
|
||||
|
||||
// 配置上传参数
|
||||
cfg := storage.Config{
|
||||
Region: &storage.Zone{
|
||||
RsHost: fmt.Sprintf("rs-%s.qiniu.com", s.region),
|
||||
},
|
||||
}
|
||||
formUploader := storage.NewFormUploader(&cfg)
|
||||
ret := storage.PutRet{}
|
||||
|
||||
// 上传文件
|
||||
err := formUploader.Put(ctx, &ret, upToken, key, reader, fileSize, &storage.PutExtra{})
|
||||
if err != nil {
|
||||
s.logger.Error("从Reader上传文件失败",
|
||||
zap.String("file_name", fileName),
|
||||
zap.String("key", key),
|
||||
zap.Error(err),
|
||||
)
|
||||
return nil, fmt.Errorf("文件上传失败: %w", err)
|
||||
}
|
||||
|
||||
// 构建文件URL
|
||||
fileURL := s.GetFileURL(ctx, key)
|
||||
|
||||
s.logger.Info("从Reader上传文件成功",
|
||||
zap.String("file_name", fileName),
|
||||
zap.String("key", key),
|
||||
zap.String("url", fileURL),
|
||||
)
|
||||
|
||||
return &UploadResult{
|
||||
Key: key,
|
||||
URL: fileURL,
|
||||
MimeType: s.getMimeType(fileName),
|
||||
Size: fileSize,
|
||||
Hash: ret.Hash,
|
||||
}, nil
|
||||
}
|
||||
@@ -8,7 +8,7 @@ import (
|
||||
"go.opentelemetry.io/otel/attribute"
|
||||
"go.uber.org/zap"
|
||||
|
||||
"tyapi-server/internal/domains/user/dto"
|
||||
"tyapi-server/internal/application/user/dto/commands"
|
||||
"tyapi-server/internal/domains/user/entities"
|
||||
"tyapi-server/internal/shared/interfaces"
|
||||
)
|
||||
@@ -118,7 +118,7 @@ func (t *TracedUserService) Shutdown(ctx context.Context) error {
|
||||
return t.wrapper.TraceServiceCall(ctx, "user", "shutdown", t.service.Shutdown)
|
||||
}
|
||||
|
||||
func (t *TracedUserService) Register(ctx context.Context, req *dto.RegisterRequest) (*entities.User, error) {
|
||||
func (t *TracedUserService) Register(ctx context.Context, req *commands.RegisterUserCommand) (*entities.User, error) {
|
||||
var result *entities.User
|
||||
var err error
|
||||
|
||||
@@ -134,7 +134,7 @@ func (t *TracedUserService) Register(ctx context.Context, req *dto.RegisterReque
|
||||
return result, err
|
||||
}
|
||||
|
||||
func (t *TracedUserService) LoginWithPassword(ctx context.Context, req *dto.LoginWithPasswordRequest) (*entities.User, error) {
|
||||
func (t *TracedUserService) LoginWithPassword(ctx context.Context, req *commands.LoginWithPasswordCommand) (*entities.User, error) {
|
||||
var result *entities.User
|
||||
var err error
|
||||
|
||||
@@ -150,7 +150,7 @@ func (t *TracedUserService) LoginWithPassword(ctx context.Context, req *dto.Logi
|
||||
return result, err
|
||||
}
|
||||
|
||||
func (t *TracedUserService) LoginWithSMS(ctx context.Context, req *dto.LoginWithSMSRequest) (*entities.User, error) {
|
||||
func (t *TracedUserService) LoginWithSMS(ctx context.Context, req *commands.LoginWithSMSCommand) (*entities.User, error) {
|
||||
var result *entities.User
|
||||
var err error
|
||||
|
||||
@@ -166,7 +166,7 @@ func (t *TracedUserService) LoginWithSMS(ctx context.Context, req *dto.LoginWith
|
||||
return result, err
|
||||
}
|
||||
|
||||
func (t *TracedUserService) ChangePassword(ctx context.Context, userID string, req *dto.ChangePasswordRequest) error {
|
||||
func (t *TracedUserService) ChangePassword(ctx context.Context, userID string, req *commands.ChangePasswordCommand) error {
|
||||
return t.wrapper.TraceServiceCall(ctx, "user", "change_password", func(ctx context.Context) error {
|
||||
return t.service.ChangePassword(ctx, userID, req)
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user