Files

168 lines
4.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 sms
import (
"context"
"encoding/json"
"strings"
"time"
"hfb_sys/backend/internal/model"
"gorm.io/datatypes"
"gorm.io/gorm"
"gorm.io/gorm/clause"
)
type Repository struct {
db *gorm.DB
}
func NewRepository(db *gorm.DB) *Repository {
return &Repository{db: db}
}
// Entry 描述一条待发送的节点短信。
// 去重口径为 (Node, BizType, BizID, UserID):
// - 审核未通过:同一账号只发一次;
// - 被租用需辅助:每个订单发一次,同一账号被反复租用会分别提醒;
// - 上架未成交:同一账号只发一次。
type Entry struct {
UserID uint64
Phone string
Node string
BizType string
BizID uint64
DistributionSiteID *uint64
Params map[string]string
}
// Enqueue 在业务事务内写入待发送记录,返回是否真正插入了新记录。
// 与站内信 notification.Append 相同的用法:只落库不发送,
// 短信通道异常不会影响业务提交;重复入队由唯一索引静默忽略(返回 false, nil)。
func Enqueue(tx *gorm.DB, entry Entry) (bool, error) {
if tx == nil {
return false, nil
}
node := strings.TrimSpace(entry.Node)
if node == "" || entry.UserID == 0 || !IsKnownNode(node) {
return false, nil
}
phone := strings.TrimSpace(entry.Phone)
if phone == "" {
var user model.User
if err := tx.Select("id", "phone").First(&user, entry.UserID).Error; err == nil {
phone = strings.TrimSpace(user.Phone)
}
}
var params datatypes.JSON
if len(entry.Params) > 0 {
raw, err := json.Marshal(entry.Params)
if err != nil {
return false, err
}
params = datatypes.JSON(raw)
}
row := model.SmsMessage{
UserID: entry.UserID,
Phone: phone,
Node: node,
BizType: strings.TrimSpace(entry.BizType),
BizID: entry.BizID,
DistributionSiteID: entry.DistributionSiteID,
TemplateParams: params,
Status: model.SmsStatusPending,
}
result := tx.Clauses(clause.OnConflict{DoNothing: true}).Create(&row)
if result.Error != nil {
return false, result.Error
}
return result.RowsAffected > 0, nil
}
// ListPending 拉取待发送记录。重试过的记录要等到 retryAfter 之后才再次尝试,避免密集重试。
func (r *Repository) ListPending(ctx context.Context, limit int, retryAfter time.Time) ([]model.SmsMessage, error) {
if r == nil || r.db == nil {
return nil, gorm.ErrInvalidDB
}
if limit <= 0 {
limit = 100
}
var rows []model.SmsMessage
err := r.db.WithContext(ctx).
Where("status = ? AND (retry_count = 0 OR updated_at <= ?)", model.SmsStatusPending, retryAfter).
Order("id ASC").
Limit(limit).
Find(&rows).Error
return rows, err
}
// MarkSent 记录发送成功,并回写实际使用的模板 CODE 与签名。
func (r *Repository) MarkSent(ctx context.Context, id uint64, templateCode string, signName string, sentAt time.Time) error {
return r.update(ctx, id, map[string]any{
"status": model.SmsStatusSent,
"template_code": strings.TrimSpace(templateCode),
"sign_name": strings.TrimSpace(signName),
"last_error": "",
"skip_reason": "",
"sent_at": sentAt,
})
}
// MarkRetry 记录一次失败并留在待发送状态,由下一轮任务重试。
func (r *Repository) MarkRetry(ctx context.Context, id uint64, retryCount int, lastError string) error {
return r.update(ctx, id, map[string]any{
"status": model.SmsStatusPending,
"retry_count": retryCount,
"last_error": truncateError(lastError),
})
}
// MarkFailed 重试次数用尽后标记为失败。
func (r *Repository) MarkFailed(ctx context.Context, id uint64, retryCount int, lastError string) error {
return r.update(ctx, id, map[string]any{
"status": model.SmsStatusFailed,
"retry_count": retryCount,
"last_error": truncateError(lastError),
})
}
// MarkSkipped 记录因配置缺失(总开关关闭、模板未配置等)未发送。
func (r *Repository) MarkSkipped(ctx context.Context, id uint64, reason string) error {
return r.update(ctx, id, map[string]any{
"status": model.SmsStatusSkipped,
"skip_reason": truncate(reason, 128),
})
}
func (r *Repository) update(ctx context.Context, id uint64, fields map[string]any) error {
if r == nil || r.db == nil {
return gorm.ErrInvalidDB
}
if id == 0 {
return nil
}
return r.db.WithContext(ctx).
Model(&model.SmsMessage{}).
Where("id = ?", id).
Updates(fields).Error
}
func truncateError(value string) string {
return truncate(value, 512)
}
func truncate(value string, limit int) string {
value = strings.TrimSpace(value)
if limit <= 0 {
return value
}
runes := []rune(value)
if len(runes) <= limit {
return value
}
return string(runes[:limit])
}