七月迭代短暂完结,还有很多后端的关键东西没有弄,这是一版赶时间做的东西
All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 8m26s
All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 8m26s
This commit is contained in:
141
internal/infrastructure/packageexpiry/reminder_publisher.go
Normal file
141
internal/infrastructure/packageexpiry/reminder_publisher.go
Normal file
@@ -0,0 +1,141 @@
|
||||
// Package packageexpiry 提供套餐临期提醒的 PostgreSQL 与 Outbox Adapter。
|
||||
package packageexpiry
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"gorm.io/gorm"
|
||||
|
||||
notificationapp "github.com/break/junhong_cmp_fiber/internal/application/notification"
|
||||
"github.com/break/junhong_cmp_fiber/internal/infrastructure/messaging/outbox"
|
||||
"github.com/break/junhong_cmp_fiber/internal/model/dto"
|
||||
"github.com/break/junhong_cmp_fiber/pkg/constants"
|
||||
"github.com/break/junhong_cmp_fiber/pkg/errors"
|
||||
)
|
||||
|
||||
var shanghaiLocation = time.FixedZone("Asia/Shanghai", 8*60*60)
|
||||
|
||||
// ReminderPublisher 将临期节点事实转换为个人客户通知 Outbox。
|
||||
type ReminderPublisher struct {
|
||||
db *gorm.DB
|
||||
outbox *outbox.Repository
|
||||
}
|
||||
|
||||
// NewReminderPublisher 创建套餐临期通知 Outbox Adapter。
|
||||
func NewReminderPublisher(db *gorm.DB, repository *outbox.Repository) *ReminderPublisher {
|
||||
return &ReminderPublisher{db: db, outbox: repository}
|
||||
}
|
||||
|
||||
type recipientRow struct {
|
||||
AssetID uint
|
||||
CustomerID uint
|
||||
}
|
||||
|
||||
// Publish 批量解析资产关联个人客户,并在同一事务内幂等追加通知事件。
|
||||
func (p *ReminderPublisher) Publish(ctx context.Context, candidates []dto.ExpiringAssetItem) error {
|
||||
if p == nil || p.db == nil || p.outbox == nil {
|
||||
return errors.New(errors.CodeInternalError, "套餐临期通知 Publisher 未配置")
|
||||
}
|
||||
recipients, err := p.loadRecipients(ctx, candidates)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return p.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
|
||||
for _, candidate := range candidates {
|
||||
if candidate.DaysUntilFinalExpiry == nil || candidate.EstimatedFinalExpiresAt == nil {
|
||||
continue
|
||||
}
|
||||
key := recipientKey(candidate.AssetType, candidate.AssetID)
|
||||
for customerID := range recipients[key] {
|
||||
if err := p.appendNotification(ctx, tx, candidate, customerID); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
func (p *ReminderPublisher) loadRecipients(ctx context.Context, candidates []dto.ExpiringAssetItem) (map[string]map[uint]struct{}, error) {
|
||||
cardIDs := make([]uint, 0)
|
||||
deviceIDs := make([]uint, 0)
|
||||
for _, candidate := range candidates {
|
||||
if candidate.AssetType == constants.AssetTypeIotCard {
|
||||
cardIDs = append(cardIDs, candidate.AssetID)
|
||||
} else if candidate.AssetType == constants.AssetTypeDevice {
|
||||
deviceIDs = append(deviceIDs, candidate.AssetID)
|
||||
}
|
||||
}
|
||||
result := make(map[string]map[uint]struct{})
|
||||
queries := []struct {
|
||||
assetType string
|
||||
ids []uint
|
||||
sql string
|
||||
}{
|
||||
{constants.AssetTypeIotCard, cardIDs, `
|
||||
SELECT c.id AS asset_id, b.customer_id
|
||||
FROM tb_iot_card c
|
||||
JOIN tb_personal_customer_device b ON c.virtual_no <> '' AND b.virtual_no = c.virtual_no
|
||||
WHERE c.id IN ? AND c.deleted_at IS NULL AND b.deleted_at IS NULL AND b.status = 1`},
|
||||
{constants.AssetTypeIotCard, cardIDs, `
|
||||
SELECT c.id AS asset_id, b.customer_id
|
||||
FROM tb_iot_card c
|
||||
JOIN tb_personal_customer_iccid b ON c.virtual_no = '' AND (b.iccid = c.iccid OR (b.iccid_19 <> '' AND b.iccid_19 = c.iccid_19))
|
||||
WHERE c.id IN ? AND c.deleted_at IS NULL AND b.deleted_at IS NULL AND b.status = 1`},
|
||||
{constants.AssetTypeDevice, deviceIDs, `
|
||||
SELECT d.id AS asset_id, b.customer_id
|
||||
FROM tb_device d
|
||||
JOIN tb_personal_customer_device b ON b.virtual_no = CASE WHEN d.virtual_no <> '' THEN d.virtual_no ELSE d.imei END
|
||||
WHERE d.id IN ? AND d.deleted_at IS NULL AND b.deleted_at IS NULL AND b.status = 1`},
|
||||
}
|
||||
for _, query := range queries {
|
||||
if len(query.ids) == 0 {
|
||||
continue
|
||||
}
|
||||
var rows []recipientRow
|
||||
if err := p.db.WithContext(ctx).Raw(query.sql, query.ids).Scan(&rows).Error; err != nil {
|
||||
return nil, errors.Wrap(errors.CodeDatabaseError, err, "批量查询临期资产关联客户失败")
|
||||
}
|
||||
for _, row := range rows {
|
||||
key := recipientKey(query.assetType, row.AssetID)
|
||||
if result[key] == nil {
|
||||
result[key] = make(map[uint]struct{})
|
||||
}
|
||||
result[key][row.CustomerID] = struct{}{}
|
||||
}
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func (p *ReminderPublisher) appendNotification(ctx context.Context, tx *gorm.DB, candidate dto.ExpiringAssetItem, customerID uint) error {
|
||||
node := *candidate.DaysUntilFinalExpiry
|
||||
expiryDate := candidate.EstimatedFinalExpiresAt.In(shanghaiLocation).Format("20060102")
|
||||
assetCode := "c"
|
||||
if candidate.AssetType == constants.AssetTypeDevice {
|
||||
assetCode = "d"
|
||||
}
|
||||
eventID := fmt.Sprintf("pex:%s:%d:%s:%d:%d", assetCode, candidate.AssetID, expiryDate, node, customerID)
|
||||
assetID := strconv.FormatUint(uint64(candidate.AssetID), 10)
|
||||
_, err := p.outbox.AppendIdempotent(ctx, tx, outbox.Envelope{
|
||||
EventID: eventID, EventType: constants.OutboxEventTypePersonalCustomerDirectNotification,
|
||||
PayloadVersion: constants.NotificationPayloadVersionV1,
|
||||
AggregateType: "package_expiry", AggregateID: strconv.FormatUint(uint64(candidate.PackageUsageID), 10),
|
||||
ResourceType: candidate.AssetType, ResourceID: assetID, BusinessKey: eventID,
|
||||
Payload: notificationapp.PersonalCustomerDirectPayload{
|
||||
RecipientID: customerID, NotificationType: constants.NotificationTypePackageExpiring,
|
||||
TemplateData: map[string]string{}, RefType: constants.NotificationRefTypeAsset,
|
||||
RefID: assetID, RefKey: candidate.Identifier, ExpiresAt: candidate.EstimatedFinalExpiresAt,
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
return errors.Wrap(errors.CodeDatabaseError, err, "写入套餐临期通知事件失败")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func recipientKey(assetType string, assetID uint) string {
|
||||
return fmt.Sprintf("%s:%d", assetType, assetID)
|
||||
}
|
||||
Reference in New Issue
Block a user