516 lines
14 KiB
Go
516 lines
14 KiB
Go
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)
|
||
}
|