Files
smartmate/backend/services/notification/dao/channel.go
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

128 lines
4.0 KiB
Go
Raw 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 dao
import (
"context"
"errors"
"time"
notificationmodel "github.com/LoveLosita/smartflow/backend/services/notification/model"
"gorm.io/gorm"
"gorm.io/gorm/clause"
)
// ChannelDAO 管理用户外部通知通道配置。
//
// 职责边界:
// 1. 只负责 user_notification_channels 的基础读写;
// 2. 不负责 webhook 请求发送、notification_records 状态机或 outbox 消费;
// 3. webhook_url / bearer_token 的脱敏由 service 层处理DAO 保持真实持久化值。
type ChannelDAO struct {
db *gorm.DB
}
func NewChannelDAO(db *gorm.DB) *ChannelDAO {
return &ChannelDAO{db: db}
}
func (d *ChannelDAO) WithTx(tx *gorm.DB) *ChannelDAO {
return &ChannelDAO{db: tx}
}
func (d *ChannelDAO) ensureDB() error {
if d == nil || d.db == nil {
return errors.New("notification channel dao 未初始化")
}
return nil
}
// UpsertUserNotificationChannel 按 user_id + channel 幂等保存用户通知配置。
//
// 说明:
// 1. 只覆盖开关、webhook、鉴权配置和 updated_at
// 2. 不清空 last_test_*,避免用户保存配置后丢掉最近一次测试结果;
// 3. channel.ID 由数据库自增,调用方不应依赖传入 ID。
func (d *ChannelDAO) UpsertUserNotificationChannel(ctx context.Context, channel *notificationmodel.UserNotificationChannel) error {
if err := d.ensureDB(); err != nil {
return err
}
if channel == nil || channel.UserID <= 0 || channel.Channel == "" {
return errors.New("notification channel 必须包含 user_id 和 channel")
}
now := time.Now()
values := map[string]any{
"user_id": channel.UserID,
"channel": channel.Channel,
"enabled": channel.Enabled,
"webhook_url": channel.WebhookURL,
"auth_type": channel.AuthType,
"bearer_token": channel.BearerToken,
"created_at": now,
"updated_at": now,
}
return d.db.WithContext(ctx).
Model(&notificationmodel.UserNotificationChannel{}).
Clauses(clause.OnConflict{
Columns: []clause.Column{{Name: "user_id"}, {Name: "channel"}},
DoUpdates: clause.Assignments(map[string]any{
"enabled": channel.Enabled,
"webhook_url": channel.WebhookURL,
"auth_type": channel.AuthType,
"bearer_token": channel.BearerToken,
"updated_at": now,
}),
}).
Create(values).Error
}
// GetUserNotificationChannel 查询用户指定通知通道配置。
func (d *ChannelDAO) GetUserNotificationChannel(ctx context.Context, userID int, channel string) (*notificationmodel.UserNotificationChannel, error) {
if err := d.ensureDB(); err != nil {
return nil, err
}
if userID <= 0 || channel == "" {
return nil, gorm.ErrRecordNotFound
}
var row notificationmodel.UserNotificationChannel
err := d.db.WithContext(ctx).
Where("user_id = ? AND channel = ?", userID, channel).
First(&row).Error
if err != nil {
return nil, err
}
return &row, nil
}
// DeleteUserNotificationChannel 删除用户指定通知通道配置。
//
// 说明:当前表不保留软删除列;删除后再次保存会重新创建配置。
func (d *ChannelDAO) DeleteUserNotificationChannel(ctx context.Context, userID int, channel string) error {
if err := d.ensureDB(); err != nil {
return err
}
if userID <= 0 || channel == "" {
return nil
}
return d.db.WithContext(ctx).
Where("user_id = ? AND channel = ?", userID, channel).
Delete(&notificationmodel.UserNotificationChannel{}).Error
}
// UpdateUserNotificationChannelTestResult 回写用户 webhook 测试结果。
func (d *ChannelDAO) UpdateUserNotificationChannelTestResult(ctx context.Context, userID int, channel string, status string, testErr string, testedAt time.Time) error {
if err := d.ensureDB(); err != nil {
return err
}
if userID <= 0 || channel == "" {
return errors.New("user_id 和 channel 不能为空")
}
updates := map[string]any{
"last_test_status": status,
"last_test_error": testErr,
"last_test_at": &testedAt,
}
return d.db.WithContext(ctx).
Model(&notificationmodel.UserNotificationChannel{}).
Where("user_id = ? AND channel = ?", userID, channel).
Updates(updates).Error
}