后端: 1.接入主动调度 worker 与飞书通知链路 - 新增 due job scanner 与 active_schedule.triggered workflow - 接入 notification.feishu.requested handler、飞书 webhook provider 和用户通知配置接口 - 支持 notification_records 去重、重试、skipped/dead 状态流转 - 完成 api / worker / all 启动模式装配与主动调度验收记录 2.后续要做的就是补全从异常发生到给用户推送消息之间的逻辑缺口
128 lines
4.0 KiB
Go
128 lines
4.0 KiB
Go
package dao
|
||
|
||
import (
|
||
"context"
|
||
"errors"
|
||
"time"
|
||
|
||
"github.com/LoveLosita/smartflow/backend/model"
|
||
"gorm.io/gorm"
|
||
"gorm.io/gorm/clause"
|
||
)
|
||
|
||
// NotificationChannelDAO 管理用户外部通知通道配置。
|
||
//
|
||
// 职责边界:
|
||
// 1. 只负责 user_notification_channels 的基础读写;
|
||
// 2. 不负责 webhook 请求发送、notification_records 状态机或 outbox 消费;
|
||
// 3. webhook_url / bearer_token 的脱敏由 API/service 层处理,DAO 保持真实持久化值。
|
||
type NotificationChannelDAO struct {
|
||
db *gorm.DB
|
||
}
|
||
|
||
func NewNotificationChannelDAO(db *gorm.DB) *NotificationChannelDAO {
|
||
return &NotificationChannelDAO{db: db}
|
||
}
|
||
|
||
func (d *NotificationChannelDAO) WithTx(tx *gorm.DB) *NotificationChannelDAO {
|
||
return &NotificationChannelDAO{db: tx}
|
||
}
|
||
|
||
func (d *NotificationChannelDAO) 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 *NotificationChannelDAO) UpsertUserNotificationChannel(ctx context.Context, channel *model.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(&model.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 *NotificationChannelDAO) GetUserNotificationChannel(ctx context.Context, userID int, channel string) (*model.UserNotificationChannel, error) {
|
||
if err := d.ensureDB(); err != nil {
|
||
return nil, err
|
||
}
|
||
if userID <= 0 || channel == "" {
|
||
return nil, gorm.ErrRecordNotFound
|
||
}
|
||
var row model.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 *NotificationChannelDAO) 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(&model.UserNotificationChannel{}).Error
|
||
}
|
||
|
||
// UpdateUserNotificationChannelTestResult 回写用户 webhook 测试结果。
|
||
func (d *NotificationChannelDAO) 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(&model.UserNotificationChannel{}).
|
||
Where("user_id = ? AND channel = ?", userID, channel).
|
||
Updates(updates).Error
|
||
}
|