feat: 建立可靠通知、吊销与跨实例状态

This commit is contained in:
weijuesen
2026-08-14 01:41:14 +08:00
parent a6a996a518
commit a25bc7abc6
22 changed files with 1017 additions and 245 deletions
+139 -12
View File
@@ -10,6 +10,11 @@ import (
"net/url"
"sync"
"time"
"silk-server-go/internal/model"
"gorm.io/gorm"
"gorm.io/gorm/clause"
)
const wechatAPIBase = "https://api.weixin.qq.com"
@@ -168,9 +173,21 @@ func (s *WechatService) getAccessToken(ctx context.Context) (string, error) {
// SendSubscribe 发送订阅消息
func (s *WechatService) SendSubscribe(ctx context.Context, openid, templateID string, data map[string]map[string]string, page string) error {
_, err := s.SendSubscribeResult(ctx, openid, templateID, data, page)
return err
}
// WechatSendResult 微信订阅消息响应。
type WechatSendResult struct {
ErrCode int `json:"errcode"`
ErrMsg string `json:"errmsg"`
}
// SendSubscribeResult 发送订阅消息并返回微信响应码。
func (s *WechatService) SendSubscribeResult(ctx context.Context, openid, templateID string, data map[string]map[string]string, page string) (WechatSendResult, error) {
token, err := s.getAccessToken(ctx)
if err != nil {
return err
return WechatSendResult{ErrCode: -1, ErrMsg: err.Error()}, err
}
payload := map[string]any{
"touser": openid,
@@ -182,32 +199,142 @@ func (s *WechatService) SendSubscribe(ctx context.Context, openid, templateID st
}
raw, err := json.Marshal(payload)
if err != nil {
return err
return WechatSendResult{ErrCode: -1, ErrMsg: err.Error()}, err
}
u := s.baseURL + "/cgi-bin/message/subscribe/send?access_token=" + url.QueryEscape(token)
req, err := http.NewRequestWithContext(ctx, http.MethodPost, u, bytes.NewReader(raw))
if err != nil {
return err
return WechatSendResult{ErrCode: -1, ErrMsg: err.Error()}, err
}
req.Header.Set("Content-Type", "application/json")
resp, err := s.httpClient.Do(req)
if err != nil {
return err
return WechatSendResult{ErrCode: -1, ErrMsg: err.Error()}, err
}
defer resp.Body.Close()
body, err := io.ReadAll(resp.Body)
if err != nil {
return err
}
var out struct {
ErrCode int `json:"errcode"`
ErrMsg string `json:"errmsg"`
return WechatSendResult{ErrCode: -1, ErrMsg: err.Error()}, err
}
var out WechatSendResult
if err := json.Unmarshal(body, &out); err != nil {
return err
return WechatSendResult{ErrCode: -1, ErrMsg: err.Error()}, err
}
if out.ErrCode != 0 {
return fmt.Errorf("微信订阅消息发送失败 (%d): %s", out.ErrCode, out.ErrMsg)
return out, fmt.Errorf("微信订阅消息发送失败 (%d): %s", out.ErrCode, out.ErrMsg)
}
return nil
return out, nil
}
// WechatSubscribePayload outbox 微信订阅事件载荷。
type WechatSubscribePayload struct {
UserID string `json:"userId"`
OpenID string `json:"openId"`
TemplateKey string `json:"templateKey"`
TemplateID string `json:"templateId"`
Page string `json:"page"`
Title string `json:"title"`
Body string `json:"body"`
Data map[string]map[string]string `json:"data"`
BusinessType string `json:"businessType"`
BusinessID string `json:"businessId"`
}
// NewWechatOutboxHandler 创建微信订阅 outbox 处理器。
func NewWechatOutboxHandler(db *gorm.DB, wechat *WechatService) EventHandler {
return func(ctx context.Context, event model.OutboxEvent) error {
if event.EventType != OutboxEventWechatSubscribe {
return nil
}
var payload WechatSubscribePayload
if err := json.Unmarshal(event.Payload, &payload); err != nil {
return err
}
return handleWechatSubscribe(ctx, db, wechat, event, payload)
}
}
func handleWechatSubscribe(ctx context.Context, db *gorm.DB, wechat *WechatService, event model.OutboxEvent, payload WechatSubscribePayload) error {
var binding model.WechatBinding
if err := db.Where("user_id = ?", payload.UserID).First(&binding).Error; err != nil {
message := "微信未绑定"
return saveWechatNotification(ctx, db, event, payload, "", false, WechatSendResult{ErrCode: 40001, ErrMsg: message}, "cancelled", &message)
}
var authorized []string
if len(binding.AuthorizedTemplates) > 0 {
_ = json.Unmarshal(binding.AuthorizedTemplates, &authorized)
}
if !IsAuthorized(authorized, payload.TemplateKey) {
message := "用户未授权订阅模板"
return saveWechatNotification(ctx, db, event, payload, binding.OpenID, false, WechatSendResult{ErrCode: 43101, ErrMsg: message}, "cancelled", &message)
}
if payload.TemplateID == "" {
message := "订阅消息模板 ID 未配置"
if err := saveWechatNotification(ctx, db, event, payload, binding.OpenID, true, WechatSendResult{ErrCode: -1, ErrMsg: message}, "retry", &message); err != nil {
return err
}
return fmt.Errorf("订阅消息模板 ID 未配置")
}
result, sendErr := wechat.SendSubscribeResult(ctx, binding.OpenID, payload.TemplateID, payload.Data, payload.Page)
status := "sent"
var lastErr *string
maxAttempts := event.MaxAttempts
if maxAttempts <= 0 {
maxAttempts = 5
}
if sendErr != nil {
message := sendErr.Error()
lastErr = &message
status = "retry"
if event.Attempts+1 >= maxAttempts {
status = "failed"
}
}
if err := saveWechatNotification(ctx, db, event, payload, binding.OpenID, true, result, status, lastErr); err != nil {
return err
}
return sendErr
}
func saveWechatNotification(ctx context.Context, db *gorm.DB, event model.OutboxEvent, payload WechatSubscribePayload, openID string, authorized bool, response WechatSendResult, status string, lastErr *string) error {
data, _ := json.Marshal(payload.Data)
providerResponse, _ := json.Marshal(response)
userID := payload.UserID
eventID := event.EventID
notification := model.Notification{
UserID: &userID,
Channel: "wechat",
Target: openID,
Title: payload.Title,
Body: payload.Body,
Status: status,
Attempts: event.Attempts + 1,
NextAttemptAt: time.Now(),
LastError: lastErr,
TemplateID: strPtrOrNil(payload.TemplateID),
OpenID: strPtrOrNil(openID),
Authorized: authorized,
ProviderResponse: providerResponse,
Data: data,
BusinessType: strPtrOrNil(payload.BusinessType),
BusinessID: strPtrOrNil(payload.BusinessID),
EventID: &eventID,
}
return db.WithContext(ctx).
Clauses(clause.OnConflict{
Columns: []clause.Column{{Name: "event_id"}},
DoUpdates: clause.Assignments(map[string]interface{}{
"status": status,
"attempts": event.Attempts + 1,
"next_attempt_at": time.Now(),
"last_error": lastErr,
"authorized": authorized,
"provider_response": providerResponse,
"updated_at": time.Now(),
}),
}).
Create(&notification).Error
}