Files
Losita abe3b4960e Version: 0.9.68.dev.260504
后端:
1. 阶段 3 notification 服务边界落地,新增 `cmd/notification`、`services/notification`、`gateway/notification`、`shared/contracts/notification` 和 notification port,按 userauth 同款最小手搓 zrpc 样板收口
2. notification outbox consumer、relay 和 retry loop 迁入独立服务入口,处理 `notification.feishu.requested`,gateway 改为通过 zrpc client 调用 notification
3. 清退旧单体 notification DAO/model/service/provider/runner 和 `service/events/notification_feishu.go`,旧实现不再作为活跃编译路径
4. 修复 outbox 路由归属、dispatch 启动扫描、Kafka topic 探测/投递超时、sending 租约恢复、毒消息 MarkDead 错误回传和 RPC timeout 边界
5. 同步调整 active-scheduler 触发通知事件、核心 outbox handler、MySQL 迁移边界和 notification 配置

文档:
1. 更新微服务迁移计划,将阶段 3 notification 标记为已完成,并明确下一阶段从 active-scheduler 开始
2026-05-04 18:40:39 +08:00

140 lines
3.7 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package feishu
import (
"context"
"fmt"
"sync"
"time"
)
// MockMode 描述 mock provider 下一次返回哪类结果。
type MockMode string
const (
MockModeSuccess MockMode = "success"
MockModeTemporaryFail MockMode = "temporary_fail"
MockModePermanentFail MockMode = "permanent_fail"
)
// MockProvider 是进程内 mock provider。
//
// 职责边界:
// 1. 只用于本地联调、单元测试和阶段性验收;
// 2. 不做真实 HTTP 调用,直接根据预设 mode 返回 success / temporary_fail / permanent_fail
// 3. 保留调用历史,方便测试断言“有没有重复发飞书”。
type MockProvider struct {
mu sync.Mutex
defaultMode MockMode
queuedModes []MockMode
calls []SendRequest
}
func NewMockProvider(defaultMode MockMode) *MockProvider {
if defaultMode == "" {
defaultMode = MockModeSuccess
}
return &MockProvider{defaultMode: defaultMode}
}
func (p *MockProvider) SetDefaultMode(mode MockMode) {
p.mu.Lock()
defer p.mu.Unlock()
if mode == "" {
mode = MockModeSuccess
}
p.defaultMode = mode
}
// PushModes 追加一组“一次性模式”。
//
// 说明:
// 1. 先进先出消费,便于测试“先失败再成功”的重试路径;
// 2. 队列用尽后回退到 defaultMode
// 3. 空模式会被自动忽略,避免测试代码误塞脏数据。
func (p *MockProvider) PushModes(modes ...MockMode) {
p.mu.Lock()
defer p.mu.Unlock()
for _, mode := range modes {
if mode == "" {
continue
}
p.queuedModes = append(p.queuedModes, mode)
}
}
func (p *MockProvider) Calls() []SendRequest {
p.mu.Lock()
defer p.mu.Unlock()
copied := make([]SendRequest, len(p.calls))
copy(copied, p.calls)
return copied
}
// Send 按预设模式返回模拟结果。
//
// 步骤说明:
// 1. 先记录本次请求,方便测试校验是否发生重复投递;
// 2. 再按 queuedModes -> defaultMode 的顺序决定 outcome
// 3. 最后返回可落库审计的 request/response 摘要。
func (p *MockProvider) Send(_ context.Context, req SendRequest) (SendResult, error) {
p.mu.Lock()
p.calls = append(p.calls, req)
mode := p.defaultMode
if len(p.queuedModes) > 0 {
mode = p.queuedModes[0]
p.queuedModes = p.queuedModes[1:]
}
p.mu.Unlock()
switch mode {
case MockModeTemporaryFail:
return SendResult{
Outcome: SendOutcomeTemporaryFail,
ErrorCode: ErrorCodeProviderTimeout,
ErrorMessage: "mock feishu provider temporary failure",
RequestPayload: map[string]any{
"notification_id": req.NotificationID,
"user_id": req.UserID,
"preview_id": req.PreviewID,
"target_url": req.TargetURL,
},
ResponsePayload: map[string]any{
"mode": string(mode),
"reason": "mock temporary failure",
},
}, nil
case MockModePermanentFail:
return SendResult{
Outcome: SendOutcomePermanentFail,
ErrorCode: ErrorCodePayloadInvalid,
ErrorMessage: "mock feishu provider permanent failure",
RequestPayload: map[string]any{
"notification_id": req.NotificationID,
"user_id": req.UserID,
"preview_id": req.PreviewID,
"target_url": req.TargetURL,
},
ResponsePayload: map[string]any{
"mode": string(mode),
"reason": "mock permanent failure",
},
}, nil
default:
return SendResult{
Outcome: SendOutcomeSuccess,
ProviderMessageID: fmt.Sprintf("mock_feishu_%d", time.Now().UnixNano()),
RequestPayload: map[string]any{
"notification_id": req.NotificationID,
"user_id": req.UserID,
"preview_id": req.PreviewID,
"target_url": req.TargetURL,
},
ResponsePayload: map[string]any{
"mode": string(MockModeSuccess),
"status": "ok",
},
}, nil
}
}