基础架构
This commit is contained in:
515
internal/infrastructure/external/notification/wechat_work_service.go
vendored
Normal file
515
internal/infrastructure/external/notification/wechat_work_service.go
vendored
Normal file
@@ -0,0 +1,515 @@
|
||||
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)
|
||||
}
|
||||
505
internal/infrastructure/external/ocr/baidu_ocr_service.go
vendored
Normal file
505
internal/infrastructure/external/ocr/baidu_ocr_service.go
vendored
Normal file
@@ -0,0 +1,505 @@
|
||||
package ocr
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"go.uber.org/zap"
|
||||
|
||||
"tyapi-server/internal/application/certification/dto/responses"
|
||||
)
|
||||
|
||||
// BaiduOCRService 百度OCR服务
|
||||
type BaiduOCRService struct {
|
||||
apiKey string
|
||||
secretKey string
|
||||
endpoint string
|
||||
timeout time.Duration
|
||||
logger *zap.Logger
|
||||
}
|
||||
|
||||
// NewBaiduOCRService 创建百度OCR服务
|
||||
func NewBaiduOCRService(apiKey, secretKey string, logger *zap.Logger) *BaiduOCRService {
|
||||
return &BaiduOCRService{
|
||||
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) (*responses.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并进行URL编码
|
||||
imageBase64 := base64.StdEncoding.EncodeToString(imageBytes)
|
||||
imageBase64UrlEncoded := url.QueryEscape(imageBase64)
|
||||
|
||||
// 构建请求URL(只包含access_token)
|
||||
apiURL := fmt.Sprintf("%s/rest/2.0/ocr/v1/business_license?access_token=%s", s.endpoint, accessToken)
|
||||
|
||||
// 构建POST请求体
|
||||
payload := strings.NewReader(fmt.Sprintf("image=%s", imageBase64UrlEncoded))
|
||||
resp, err := s.sendRequest(ctx, "POST", apiURL, payload)
|
||||
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.LegalPersonName),
|
||||
zap.String("registered_capital", licenseResult.RegisteredCapital),
|
||||
)
|
||||
|
||||
return licenseResult, nil
|
||||
}
|
||||
|
||||
// RecognizeIDCard 识别身份证
|
||||
func (s *BaiduOCRService) RecognizeIDCard(ctx context.Context, imageBytes []byte, side string) (*responses.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并进行URL编码
|
||||
imageBase64 := base64.StdEncoding.EncodeToString(imageBytes)
|
||||
imageBase64UrlEncoded := url.QueryEscape(imageBase64)
|
||||
|
||||
// 构建请求URL(只包含access_token)
|
||||
apiURL := fmt.Sprintf("%s/rest/2.0/ocr/v1/idcard?access_token=%s", s.endpoint, accessToken)
|
||||
|
||||
// 构建POST请求体
|
||||
payload := strings.NewReader(fmt.Sprintf("image=%s&side=%s", imageBase64UrlEncoded, side))
|
||||
resp, err := s.sendRequest(ctx, "POST", apiURL, payload)
|
||||
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.IDCardNumber),
|
||||
zap.String("side", side),
|
||||
)
|
||||
|
||||
return idCardResult, nil
|
||||
}
|
||||
|
||||
// RecognizeGeneralText 通用文字识别
|
||||
func (s *BaiduOCRService) RecognizeGeneralText(ctx context.Context, imageBytes []byte) (*responses.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并进行URL编码
|
||||
imageBase64 := base64.StdEncoding.EncodeToString(imageBytes)
|
||||
imageBase64UrlEncoded := url.QueryEscape(imageBase64)
|
||||
|
||||
// 构建请求URL(只包含access_token)
|
||||
apiURL := fmt.Sprintf("%s/rest/2.0/ocr/v1/general_basic?access_token=%s", s.endpoint, accessToken)
|
||||
|
||||
// 构建POST请求体
|
||||
payload := strings.NewReader(fmt.Sprintf("image=%s", imageBase64UrlEncoded))
|
||||
resp, err := s.sendRequest(ctx, "POST", apiURL, payload)
|
||||
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{}) *responses.BusinessLicenseResult {
|
||||
wordsResult := result["words_result"].(map[string]interface{})
|
||||
|
||||
// 提取企业信息
|
||||
companyName := ""
|
||||
if companyNameObj, ok := wordsResult["单位名称"].(map[string]interface{}); ok {
|
||||
companyName = companyNameObj["words"].(string)
|
||||
}
|
||||
|
||||
unifiedSocialCode := ""
|
||||
if socialCreditCodeObj, ok := wordsResult["社会信用代码"].(map[string]interface{}); ok {
|
||||
unifiedSocialCode = socialCreditCodeObj["words"].(string)
|
||||
}
|
||||
|
||||
legalPersonName := ""
|
||||
if legalPersonObj, ok := wordsResult["法人"].(map[string]interface{}); ok {
|
||||
legalPersonName = legalPersonObj["words"].(string)
|
||||
}
|
||||
|
||||
// 提取注册资本等其他信息
|
||||
registeredCapital := ""
|
||||
if registeredCapitalObj, ok := wordsResult["注册资本"].(map[string]interface{}); ok {
|
||||
registeredCapital = registeredCapitalObj["words"].(string)
|
||||
}
|
||||
|
||||
// 计算置信度(这里简化处理,实际应该从OCR结果中获取)
|
||||
confidence := 0.9 // 默认置信度
|
||||
|
||||
return &responses.BusinessLicenseResult{
|
||||
CompanyName: companyName,
|
||||
UnifiedSocialCode: unifiedSocialCode,
|
||||
LegalPersonName: legalPersonName,
|
||||
RegisteredCapital: registeredCapital,
|
||||
Confidence: confidence,
|
||||
}
|
||||
}
|
||||
|
||||
// parseIDCardResult 解析身份证识别结果
|
||||
func (s *BaiduOCRService) parseIDCardResult(result map[string]interface{}, side string) *responses.IDCardResult {
|
||||
wordsResult := result["words_result"].(map[string]interface{})
|
||||
|
||||
idCardResult := &responses.IDCardResult{
|
||||
Side: side,
|
||||
Confidence: s.extractConfidence(result),
|
||||
}
|
||||
|
||||
if side == "front" {
|
||||
if name, ok := wordsResult["姓名"]; ok {
|
||||
if word, ok := name.(map[string]interface{}); ok {
|
||||
idCardResult.Name = word["words"].(string)
|
||||
}
|
||||
}
|
||||
if gender, ok := wordsResult["性别"]; ok {
|
||||
if word, ok := gender.(map[string]interface{}); ok {
|
||||
idCardResult.Gender = word["words"].(string)
|
||||
}
|
||||
}
|
||||
if nation, ok := wordsResult["民族"]; ok {
|
||||
if word, ok := nation.(map[string]interface{}); ok {
|
||||
idCardResult.Nation = word["words"].(string)
|
||||
}
|
||||
}
|
||||
if birthday, ok := wordsResult["出生"]; ok {
|
||||
if word, ok := birthday.(map[string]interface{}); ok {
|
||||
idCardResult.Birthday = 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.IDCardNumber = word["words"].(string)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if issuingAgency, ok := wordsResult["签发机关"]; ok {
|
||||
if word, ok := issuingAgency.(map[string]interface{}); ok {
|
||||
idCardResult.IssuingAgency = word["words"].(string)
|
||||
}
|
||||
}
|
||||
if validPeriod, ok := wordsResult["有效期限"]; ok {
|
||||
if word, ok := validPeriod.(map[string]interface{}); ok {
|
||||
idCardResult.ValidPeriod = word["words"].(string)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return idCardResult
|
||||
}
|
||||
|
||||
// parseGeneralTextResult 解析通用文字识别结果
|
||||
func (s *BaiduOCRService) parseGeneralTextResult(result map[string]interface{}) *responses.GeneralTextResult {
|
||||
wordsResult := result["words_result"].([]interface{})
|
||||
|
||||
textResult := &responses.GeneralTextResult{
|
||||
Confidence: s.extractConfidence(result),
|
||||
Words: make([]responses.TextLine, 0, len(wordsResult)),
|
||||
}
|
||||
|
||||
for _, word := range wordsResult {
|
||||
if wordMap, ok := word.(map[string]interface{}); ok {
|
||||
line := responses.TextLine{
|
||||
Text: wordMap["words"].(string),
|
||||
Confidence: 1.0, // 百度返回的通用文字识别没有单独置信度
|
||||
}
|
||||
textResult.Words = append(textResult.Words, line)
|
||||
}
|
||||
}
|
||||
|
||||
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 *responses.BusinessLicenseResult) error {
|
||||
if result.Confidence < 0.8 {
|
||||
return fmt.Errorf("识别置信度过低: %.2f", result.Confidence)
|
||||
}
|
||||
if result.CompanyName == "" {
|
||||
return fmt.Errorf("未能识别公司名称")
|
||||
}
|
||||
if result.LegalPersonName == "" {
|
||||
return fmt.Errorf("未能识别法定代表人")
|
||||
}
|
||||
if result.UnifiedSocialCode == "" {
|
||||
return fmt.Errorf("未能识别统一社会信用代码")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// ValidateIDCard 验证身份证识别结果
|
||||
func (s *BaiduOCRService) ValidateIDCard(result *responses.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.IDCardNumber == "" {
|
||||
return fmt.Errorf("未能识别身份证号码")
|
||||
}
|
||||
} else {
|
||||
if result.IssuingAgency == "" {
|
||||
return fmt.Errorf("未能识别签发机关")
|
||||
}
|
||||
if result.ValidPeriod == "" {
|
||||
return fmt.Errorf("未能识别有效期限")
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
123
internal/infrastructure/external/sms/sms_service.go
vendored
Normal file
123
internal/infrastructure/external/sms/sms_service.go
vendored
Normal file
@@ -0,0 +1,123 @@
|
||||
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"
|
||||
)
|
||||
|
||||
// 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
|
||||
}
|
||||
281
internal/infrastructure/external/storage/qiniu_storage_service.go
vendored
Normal file
281
internal/infrastructure/external/storage/qiniu_storage_service.go
vendored
Normal file
@@ -0,0 +1,281 @@
|
||||
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"
|
||||
|
||||
sharedStorage "tyapi-server/internal/shared/storage"
|
||||
)
|
||||
|
||||
// QiNiuStorageService 七牛云存储服务
|
||||
type QiNiuStorageService struct {
|
||||
accessKey string
|
||||
secretKey string
|
||||
bucket string
|
||||
domain 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"`
|
||||
}
|
||||
|
||||
// NewQiNiuStorageService 创建七牛云存储服务
|
||||
func NewQiNiuStorageService(accessKey, secretKey, bucket, domain string, logger *zap.Logger) *QiNiuStorageService {
|
||||
mac := qbox.NewMac(accessKey, secretKey)
|
||||
// 使用默认配置,不需要指定region
|
||||
cfg := storage.Config{}
|
||||
bucketManager := storage.NewBucketManager(mac, &cfg)
|
||||
|
||||
return &QiNiuStorageService{
|
||||
accessKey: accessKey,
|
||||
secretKey: secretKey,
|
||||
bucket: bucket,
|
||||
domain: domain,
|
||||
logger: logger,
|
||||
mac: mac,
|
||||
bucketManager: bucketManager,
|
||||
}
|
||||
}
|
||||
|
||||
// UploadFile 上传文件到七牛云
|
||||
func (s *QiNiuStorageService) UploadFile(ctx context.Context, fileBytes []byte, fileName string) (*sharedStorage.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{}
|
||||
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 &sharedStorage.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) (*sharedStorage.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 &sharedStorage.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) ([]*sharedStorage.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 []*sharedStorage.FileInfo
|
||||
for _, entry := range entries {
|
||||
fileInfo := &sharedStorage.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) (*sharedStorage.UploadResult, error) {
|
||||
// 读取文件内容
|
||||
fileBytes, err := io.ReadAll(reader)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("读取文件失败: %w", err)
|
||||
}
|
||||
|
||||
return s.UploadFile(ctx, fileBytes, fileName)
|
||||
}
|
||||
Reference in New Issue
Block a user