sync: 同步 v1.0.4 并脱敏配置与镜像
This commit is contained in:
@@ -166,9 +166,6 @@ func methodsForEvent(event Event) []string {
|
||||
seen[value] = struct{}{}
|
||||
result = append(result, value)
|
||||
}
|
||||
if event.AppNotifyEnabled {
|
||||
result = append(result, MethodWechat)
|
||||
}
|
||||
sort.SliceStable(result, func(i, j int) bool {
|
||||
return methodOrder(result[i]) < methodOrder(result[j])
|
||||
})
|
||||
|
||||
@@ -13,15 +13,20 @@ func applyContent(batch *Batch) {
|
||||
if batch == nil {
|
||||
return
|
||||
}
|
||||
batch.SMSContent = buildSMSContent(*batch)
|
||||
batch.EmailSubject = buildEmailSubject(*batch)
|
||||
batch.EmailBody = buildEmailBody(*batch)
|
||||
batch.PlatformTitle = buildPlatformTitle(*batch)
|
||||
batch.PlatformContent = buildPlatformContent(*batch)
|
||||
switch batch.Method {
|
||||
case MethodSMS:
|
||||
batch.SMSContent = buildSMSContent(*batch)
|
||||
case MethodEmail:
|
||||
batch.EmailSubject = buildEmailSubject(*batch)
|
||||
batch.EmailBody = buildEmailBody(*batch)
|
||||
case MethodPlatform:
|
||||
batch.PlatformTitle = buildPlatformTitle(*batch)
|
||||
batch.PlatformContent = buildPlatformContent(*batch)
|
||||
}
|
||||
}
|
||||
|
||||
func buildSMSContent(batch Batch) string {
|
||||
count := len(batch.Items)
|
||||
count := batchItemCount(batch)
|
||||
if count == 0 {
|
||||
return ""
|
||||
}
|
||||
@@ -29,7 +34,7 @@ func buildSMSContent(batch Batch) string {
|
||||
summary := batchObjectSummary(batch)
|
||||
title := warningTitle(batch)
|
||||
action := ""
|
||||
if hasAffectOperation(batch.Items) {
|
||||
if batchHasAffectOperation(batch) {
|
||||
action = ",涉及停机或断网操作"
|
||||
}
|
||||
|
||||
@@ -42,7 +47,7 @@ func buildSMSContent(batch Batch) string {
|
||||
}
|
||||
|
||||
func buildEmailSubject(batch Batch) string {
|
||||
count := len(batch.Items)
|
||||
count := batchItemCount(batch)
|
||||
if count == 0 {
|
||||
return "物联网预警提醒"
|
||||
}
|
||||
@@ -50,15 +55,15 @@ func buildEmailSubject(batch Batch) string {
|
||||
}
|
||||
|
||||
func buildEmailBody(batch Batch) string {
|
||||
count := len(batch.Items)
|
||||
count := batchItemCount(batch)
|
||||
if count == 0 {
|
||||
return ""
|
||||
}
|
||||
|
||||
var builder strings.Builder
|
||||
builder.WriteString(`<div ` + warningEmailHTMLSignature + ` style="font-family:Arial,'Microsoft YaHei',sans-serif;font-size:14px;line-height:1.6;color:#1f2937;">`)
|
||||
fmt.Fprintf(&builder, `<p style="margin:0 0 12px;">%s,共 %d 条,最高级别:%s。</p>`, htmlEscape(warningTitle(batch)), count, htmlEscape(levelText(highestLevel(batch.Items))))
|
||||
if hasAffectOperation(batch.Items) {
|
||||
fmt.Fprintf(&builder, `<p style="margin:0 0 12px;">%s,共 %d 条,最高级别:%s。</p>`, htmlEscape(warningTitle(batch)), count, htmlEscape(levelText(batchHighestLevel(batch))))
|
||||
if batchHasAffectOperation(batch) {
|
||||
builder.WriteString(`<p style="margin:0 0 12px;color:#b42318;">本次预警包含停机或断网类操作,请重点确认。</p>`)
|
||||
}
|
||||
builder.WriteString(`<table style="width:100%;border-collapse:collapse;border:1px solid #d0d5dd;">`)
|
||||
@@ -92,13 +97,13 @@ func buildPlatformTitle(batch Batch) string {
|
||||
}
|
||||
|
||||
func buildPlatformContent(batch Batch) string {
|
||||
count := len(batch.Items)
|
||||
count := batchItemCount(batch)
|
||||
if count == 0 {
|
||||
return ""
|
||||
}
|
||||
|
||||
content := buildPlatformSummaryContent(batch)
|
||||
if hasAffectOperation(batch.Items) {
|
||||
if batchHasAffectOperation(batch) {
|
||||
content += "本次提醒包含停机或断网类操作,请及时确认。"
|
||||
}
|
||||
|
||||
@@ -110,15 +115,15 @@ func buildPlatformSummaryContent(batch Batch) string {
|
||||
return fmt.Sprintf(
|
||||
"%s有%d张卡临近到期,最高档位:%s,请登录控制台查看。",
|
||||
firstObjectCode(batch),
|
||||
len(batch.Items),
|
||||
batchItemCount(batch),
|
||||
buildPlatformTitle(batch),
|
||||
)
|
||||
}
|
||||
return fmt.Sprintf(
|
||||
"%s触发%s,最高级别:%s,请登录控制台查看。",
|
||||
batchObjectSummary(batch),
|
||||
platformObjectSummary(batch),
|
||||
buildPlatformTitle(batch),
|
||||
levelText(highestLevel(batch.Items)),
|
||||
levelText(batchHighestLevel(batch)),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -201,7 +206,7 @@ func itemDetailSummary(item Event) string {
|
||||
}
|
||||
|
||||
func batchObjectSummary(batch Batch) string {
|
||||
count := len(batch.Items)
|
||||
count := batchItemCount(batch)
|
||||
if count == 0 {
|
||||
return ""
|
||||
}
|
||||
@@ -427,3 +432,17 @@ func htmlEscape(value string) string {
|
||||
}
|
||||
return html.EscapeString(value)
|
||||
}
|
||||
|
||||
func platformObjectSummary(batch Batch) string {
|
||||
if len(batch.Items) == 0 {
|
||||
return ""
|
||||
}
|
||||
if batchItemCount(batch) > 5 {
|
||||
return fmt.Sprintf("\u5171%d%s", batchItemCount(batch), objectCountUnit(batch.Items[0]))
|
||||
}
|
||||
codes := make([]string, 0, len(batch.Items))
|
||||
for _, item := range batch.Items {
|
||||
codes = append(codes, displayObjectCode(item))
|
||||
}
|
||||
return strings.Join(codes, "\u3001")
|
||||
}
|
||||
|
||||
@@ -0,0 +1,150 @@
|
||||
package warningnotify
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"testing"
|
||||
|
||||
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/consts"
|
||||
)
|
||||
|
||||
type failureRecordingSender struct{ calls int }
|
||||
|
||||
func (sender *failureRecordingSender) Send(ctx context.Context, batch Batch) error {
|
||||
sender.calls++
|
||||
return errors.New("send failed")
|
||||
}
|
||||
|
||||
func TestDispatchContinuesAfterDeliveryFailure(t *testing.T) {
|
||||
sender := &failureRecordingSender{}
|
||||
restore := SetSender(sender)
|
||||
defer restore()
|
||||
events := []Event{{TenantID: "tenant", ItemType: 1, ItemCode: "card", Category: "traffic", WarnMethods: MethodEmail, WarnEmails: "xxx@example.com", AppNotifyEnabled: true}}
|
||||
expected := len(BuildBatches(events))
|
||||
if expected < 2 {
|
||||
t.Fatal("expected multiple channels")
|
||||
}
|
||||
if err := Dispatch(context.Background(), events); err == nil {
|
||||
t.Fatal("failure must propagate")
|
||||
}
|
||||
if sender.calls != expected {
|
||||
t.Fatalf("calls=%d expected=%d", sender.calls, expected)
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuildBatchesOnlyCreatesPlatformAndConfiguredChannels(t *testing.T) {
|
||||
batches := BuildBatches([]Event{{
|
||||
Business: BusinessSimWarning,
|
||||
TenantID: "tenant",
|
||||
ItemCode: "card-1",
|
||||
WarnMethods: MethodEmail,
|
||||
WarnEmails: "xxx@example.com",
|
||||
AppNotifyEnabled: false,
|
||||
}})
|
||||
|
||||
methods := make(map[string]bool, len(batches))
|
||||
for _, batch := range batches {
|
||||
methods[batch.Method] = true
|
||||
}
|
||||
|
||||
for _, method := range []string{MethodPlatform, MethodEmail} {
|
||||
if !methods[method] {
|
||||
t.Errorf("expected %q batch to be generated", method)
|
||||
}
|
||||
}
|
||||
if methods[MethodWechat] {
|
||||
t.Error("wechat subscription batch must not be generated")
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuildBatchesIgnoresLegacyWechatFlag(t *testing.T) {
|
||||
for _, enabled := range []bool{false, true} {
|
||||
for _, business := range []string{BusinessSimWarning, BusinessTrafficCutoff} {
|
||||
batches := BuildBatches([]Event{{
|
||||
Business: business,
|
||||
TenantID: "tenant",
|
||||
ItemCode: "card-1",
|
||||
AppNotifyEnabled: enabled,
|
||||
}})
|
||||
if len(batches) != 1 || batches[0].Method != MethodPlatform {
|
||||
t.Errorf("business=%q enabled=%t: expected platform only, got %+v", business, enabled, batches)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestEnterpriseWarningRecipientEligibility(t *testing.T) {
|
||||
if !enterpriseWarningRecipientEligible(consts.SysUserTypeSys, 175, 175, "") {
|
||||
t.Fatal("same-organization enterprise creator should be eligible")
|
||||
}
|
||||
for _, test := range []struct {
|
||||
name string
|
||||
userType string
|
||||
userDeptID int64
|
||||
cardDeptID int64
|
||||
}{
|
||||
{name: "app user", userType: consts.SysUserTypeAPP, userDeptID: 175, cardDeptID: 175},
|
||||
{name: "different organization", userType: consts.SysUserTypeSys, userDeptID: 176, cardDeptID: 175},
|
||||
{name: "missing organization", userType: consts.SysUserTypeSys, userDeptID: 0, cardDeptID: 175},
|
||||
} {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
if enterpriseWarningRecipientEligible(test.userType, test.userDeptID, test.cardDeptID, "") {
|
||||
t.Fatal("recipient should not be eligible")
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
func TestEnterpriseWarningAncestorScope(t *testing.T) {
|
||||
for _, test := range []struct {
|
||||
name string
|
||||
ancestors string
|
||||
expected bool
|
||||
}{
|
||||
{"direct descendant", "0,100,175,", true},
|
||||
{"deep descendant", "0,100,175,185", true},
|
||||
{"whitespace", "0, 175 ,185", true},
|
||||
{"partial identifier", "0,100,1175,1750", false},
|
||||
{"unrelated branch", "0,100,176", false},
|
||||
{"missing path", "", false},
|
||||
} {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
if actual := enterpriseWarningRecipientEligible(consts.SysUserTypeSys, 175, 195, test.ancestors); actual != test.expected {
|
||||
t.Fatalf("eligible = %v, want %v", actual, test.expected)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestWechatTokenInvalidCodes(t *testing.T) {
|
||||
for _, code := range []int{40001, 40014, 42001} {
|
||||
if !warningWechatTokenInvalid(code) {
|
||||
t.Fatalf("code %d must refresh token", code)
|
||||
}
|
||||
}
|
||||
for _, code := range []int{0, 43101, 47003, 40003} {
|
||||
if warningWechatTokenInvalid(code) {
|
||||
t.Fatalf("code %d must not refresh token", code)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestWechatDefaultTemplateMatchesMiniProgram(t *testing.T) {
|
||||
const expected = "DMDP2PKktdtG1ES_3Ic1JcR0LGELQmWE6LunYyerHCY"
|
||||
if actual := wechatTemplateID(nil, consts.WarningWechatTemplateIDDefault); actual != expected {
|
||||
t.Fatalf("default template = %q, expected %q", actual, expected)
|
||||
}
|
||||
}
|
||||
|
||||
func TestTrafficCutoffPlatformRecipientsExcludeCreator(t *testing.T) {
|
||||
got := platformAgentRecipientIDs(BusinessTrafficCutoff, []int64{130}, 1)
|
||||
if len(got) != 1 || got[0] != 130 {
|
||||
t.Fatalf("traffic cutoff recipients = %v, want [130] without admin creator 1", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestOtherPlatformRecipientsRetainCreator(t *testing.T) {
|
||||
got := platformAgentRecipientIDs(BusinessSimWarning, []int64{130}, 1)
|
||||
if len(got) != 2 || got[0] != 130 || got[1] != 1 {
|
||||
t.Fatalf("regular warning recipients = %v, want [130 1]", got)
|
||||
}
|
||||
}
|
||||
@@ -91,7 +91,7 @@ func (s MessageCenterSender) sendPlatform(ctx context.Context, batch Batch) erro
|
||||
len(batch.Items),
|
||||
err,
|
||||
)
|
||||
return nil
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -205,9 +205,12 @@ func (s MessageCenterSender) writePlatform(ctx context.Context, batch Batch) err
|
||||
}
|
||||
|
||||
func resolvePlatformDeliveries(ctx context.Context, batch Batch) ([]platformDelivery, error) {
|
||||
if batch.prepared != nil {
|
||||
return []platformDelivery{{Group: *batch.prepared, Batch: batch}}, nil
|
||||
}
|
||||
deliveries := make([]platformDelivery, 0, 2)
|
||||
|
||||
agentUserIDs, err := queryAgentUserIDs(ctx, batch.TenantID, batch.DeptID, batch.CreatedBy)
|
||||
agentUserIDs, err := queryAgentUserIDs(ctx, batch.TenantID, batch.DeptID, batch.CreatedBy, batch.Business)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -219,6 +222,8 @@ func resolvePlatformDeliveries(ctx context.Context, batch Batch) ([]platformDeli
|
||||
},
|
||||
Batch: batch,
|
||||
})
|
||||
} else if batch.Business == BusinessTrafficCutoff {
|
||||
g.Log().Warningf(ctx, "WarningNotify traffic cutoff skipped department recipients, tenantId:%s, deptId:%d, category:%s, stage:%d, count:%d", batch.TenantID, batch.DeptID, batch.Category, batch.WarnStage, len(batch.Items))
|
||||
}
|
||||
|
||||
if hasCardWarningItems(batch.Items) {
|
||||
@@ -228,11 +233,11 @@ func resolvePlatformDeliveries(ctx context.Context, batch Batch) ([]platformDeli
|
||||
return deliveries, nil
|
||||
}
|
||||
|
||||
func queryAgentUserIDs(ctx context.Context, tenantID string, deptID int64, createdBy int64) ([]int64, error) {
|
||||
userIDs := make([]int64, 0)
|
||||
seen := make(map[int64]struct{})
|
||||
func queryAgentUserIDs(ctx context.Context, tenantID string, deptID int64, createdBy int64, business string) ([]int64, error) {
|
||||
deptUserIDs := make([]int64, 0)
|
||||
creatorUserID := int64(0)
|
||||
if strings.TrimSpace(tenantID) == "" {
|
||||
return userIDs, nil
|
||||
return deptUserIDs, nil
|
||||
}
|
||||
|
||||
columns := dao.SysUser.Columns()
|
||||
@@ -248,11 +253,11 @@ func queryAgentUserIDs(ctx context.Context, tenantID string, deptID int64, creat
|
||||
return nil, err
|
||||
}
|
||||
for _, user := range users {
|
||||
userIDs = appendUniqueInt64(userIDs, seen, user.UserId)
|
||||
deptUserIDs = append(deptUserIDs, user.UserId)
|
||||
}
|
||||
}
|
||||
|
||||
if createdBy > 0 {
|
||||
if business != BusinessTrafficCutoff && createdBy > 0 {
|
||||
var creator *entity.SysUser
|
||||
if err := dao.SysUser.Ctx(ctx).
|
||||
Fields(columns.UserId).
|
||||
@@ -264,11 +269,23 @@ func queryAgentUserIDs(ctx context.Context, tenantID string, deptID int64, creat
|
||||
return nil, err
|
||||
}
|
||||
if creator != nil {
|
||||
userIDs = appendUniqueInt64(userIDs, seen, creator.UserId)
|
||||
creatorUserID = creator.UserId
|
||||
}
|
||||
}
|
||||
|
||||
return userIDs, nil
|
||||
return platformAgentRecipientIDs(business, deptUserIDs, creatorUserID), nil
|
||||
}
|
||||
|
||||
func platformAgentRecipientIDs(business string, deptUserIDs []int64, createdBy int64) []int64 {
|
||||
userIDs := make([]int64, 0, len(deptUserIDs)+1)
|
||||
seen := make(map[int64]struct{})
|
||||
for _, userID := range deptUserIDs {
|
||||
userIDs = appendUniqueInt64(userIDs, seen, userID)
|
||||
}
|
||||
if business != BusinessTrafficCutoff {
|
||||
userIDs = appendUniqueInt64(userIDs, seen, createdBy)
|
||||
}
|
||||
return userIDs
|
||||
}
|
||||
|
||||
func queryCreatedTerminalUserIDs(ctx context.Context, tenantID string, createdBy int64) ([]int64, error) {
|
||||
@@ -295,6 +312,9 @@ func queryCreatedTerminalUserIDs(ctx context.Context, tenantID string, createdBy
|
||||
}
|
||||
|
||||
func resolveTerminalPlatformDeliveries(ctx context.Context, batch Batch) []platformDelivery {
|
||||
if batch.prepared != nil {
|
||||
return []platformDelivery{{Group: *batch.prepared, Batch: batch}}
|
||||
}
|
||||
itemsByUser := make(map[int64][]Event)
|
||||
seenByUser := make(map[int64]map[int]struct{})
|
||||
|
||||
@@ -408,16 +428,26 @@ func queryTerminalUserCardRowsByColumn(ctx context.Context, tenantID string, col
|
||||
if strings.TrimSpace(tenantID) == "" || len(values) == 0 {
|
||||
return rows, nil
|
||||
}
|
||||
err := g.DB().Model(simTerminalTable+" st").Ctx(ctx).Safe().
|
||||
LeftJoin(terminalTable+" t", "t.terminal_id = st.terminal_id AND t.tenant_id = st.tenant_id").
|
||||
LeftJoin(dao.SysUser.Table()+" u", "(u.user_id = t.leader OR u.user_id = st.terminal_id) AND u.tenant_id = st.tenant_id").
|
||||
Fields("DISTINCT u.user_id, st.iccid, st.msisdn").
|
||||
Where("st.tenant_id", tenantID).
|
||||
Where("u.user_type", consts.SysUserTypeAPP).
|
||||
Where("u.status", consts.SysDeptStatusNormal).
|
||||
WhereIn(column, values).
|
||||
Scan(&rows)
|
||||
return rows, err
|
||||
for offset := 0; offset < len(values); offset += 500 {
|
||||
if err := ctx.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
end := min(offset+500, len(values))
|
||||
var page []terminalUserCardRow
|
||||
if err := g.DB().Model(simTerminalTable+" st").Ctx(ctx).Safe().
|
||||
LeftJoin(terminalTable+" t", "t.terminal_id = st.terminal_id AND t.tenant_id = st.tenant_id").
|
||||
LeftJoin(dao.SysUser.Table()+" u", "(u.user_id = t.leader OR u.user_id = st.terminal_id) AND u.tenant_id = st.tenant_id").
|
||||
Fields("DISTINCT u.user_id, st.iccid, st.msisdn").
|
||||
Where("st.tenant_id", tenantID).
|
||||
Where("u.user_type", consts.SysUserTypeAPP).
|
||||
Where("u.status", consts.SysDeptStatusNormal).
|
||||
WhereIn(column, values[offset:end]).
|
||||
Scan(&page); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
rows = append(rows, page...)
|
||||
}
|
||||
return rows, nil
|
||||
}
|
||||
|
||||
func hasCardWarningItems(items []Event) bool {
|
||||
|
||||
@@ -0,0 +1,83 @@
|
||||
package warningnotify
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestSmallAndLargePlatformSummary(t *testing.T) {
|
||||
b := Batch{Method: MethodPlatform, WarnStage: 2}
|
||||
for i := 0; i < 2; i++ {
|
||||
b.Items = append(b.Items, Event{ItemType: 1, ItemCode: fmt.Sprintf("898600000000000000%02d", i), WarnLevel: 3})
|
||||
}
|
||||
applyContent(&b)
|
||||
for _, item := range b.Items {
|
||||
if !strings.Contains(b.PlatformContent, item.ItemCode) {
|
||||
t.Fatal("missing card")
|
||||
}
|
||||
}
|
||||
if b.EmailBody != "" || b.SMSContent != "" {
|
||||
t.Fatal("unrelated content generated")
|
||||
}
|
||||
for i := 2; i < 10000; i++ {
|
||||
b.Items = append(b.Items, Event{ItemType: 1, ItemCode: "89860000000000000000", WarnLevel: 3})
|
||||
}
|
||||
applyContent(&b)
|
||||
if !strings.Contains(b.PlatformContent, "10000") || len([]rune(b.PlatformContent)) > 150 {
|
||||
t.Fatal("unbounded summary")
|
||||
}
|
||||
b.Method = MethodWechat
|
||||
b.EmailBody = ""
|
||||
applyContent(&b)
|
||||
if b.EmailBody != "" {
|
||||
t.Fatal("wechat generated email")
|
||||
}
|
||||
payload := buildWechatPayload(b, "openid", "template", "pages/message/index")
|
||||
if payload.Data["character_string6"].Value != "TOTAL-10000" {
|
||||
t.Fatal("missing total")
|
||||
}
|
||||
if !strings.Contains(payload.Data["thing9"].Value, "10000") || !strings.Contains(payload.Data["thing9"].Value, levelText(3)) {
|
||||
t.Fatal("missing count or severity")
|
||||
}
|
||||
}
|
||||
|
||||
func TestCollectorKeepsFullCountWithBoundedRecipientPreview(t *testing.T) {
|
||||
collector := NewCollector()
|
||||
recipient := &platformRecipientGroup{SendUserType: 2, UserIDs: []int64{11}}
|
||||
other := &platformRecipientGroup{SendUserType: 2, UserIDs: []int64{12}}
|
||||
for page := 0; page < 50; page++ {
|
||||
batch := Batch{Method: MethodPlatform, Business: BusinessSimWarning, TenantID: "tenant-a", Category: "traffic", WarnStage: 2}
|
||||
for index := 0; index < 200; index++ {
|
||||
batch.Items = append(batch.Items, Event{ItemCode: fmt.Sprintf("8986%016d", page*200+index), WarnLevel: 3, ItemType: 1})
|
||||
}
|
||||
collector.merge(batch, recipient)
|
||||
batch.Method = MethodWechat
|
||||
collector.merge(batch, recipient)
|
||||
}
|
||||
collector.merge(Batch{Method: MethodPlatform, TenantID: "tenant-a", Business: BusinessSimWarning, Category: "traffic", Items: []Event{{ItemCode: "private", ItemType: 1}}}, other)
|
||||
if len(collector.batches) != 3 {
|
||||
t.Fatalf("channels or recipients merged: %d", len(collector.batches))
|
||||
}
|
||||
for _, batch := range collector.batches {
|
||||
if len(batch.Items) > 5 {
|
||||
t.Fatal("preview grows with cards")
|
||||
}
|
||||
if batch.prepared.UserIDs[0] == 11 {
|
||||
if batchItemCount(*batch) != 10000 {
|
||||
t.Fatal("count lost across pages")
|
||||
}
|
||||
for _, item := range batch.Items {
|
||||
if item.ItemCode == "private" {
|
||||
t.Fatal("recipient data leak")
|
||||
}
|
||||
}
|
||||
if batch.Method == MethodWechat {
|
||||
payload := buildWechatPayload(*batch, "openid", "template", "pages/message/index")
|
||||
if payload.Data["character_string6"].Value != "TOTAL-10000" {
|
||||
t.Fatal("preview count used as total")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
package warningnotify
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/dao"
|
||||
)
|
||||
|
||||
func warningWechatEnabled(ctx context.Context, tenantID string, userID int64) (bool, error) {
|
||||
var preference *struct {
|
||||
Value string `orm:"config_value"`
|
||||
}
|
||||
err := dao.SysConfig.Ctx(ctx).Fields("config_value").Where("tenant_id", tenantID).
|
||||
Where("config_key", fmt.Sprintf("app.wechat.warning.enabled.%d", userID)).OrderDesc("config_id").Scan(&preference)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
return preference == nil || preference.Value == "1", nil
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
package warningnotify
|
||||
|
||||
import (
|
||||
"context"
|
||||
_ "github.com/gogf/gf/contrib/drivers/sqlite/v2"
|
||||
"github.com/gogf/gf/v2/database/gdb"
|
||||
"github.com/gogf/gf/v2/frame/g"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/model/do"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestWarningWechatPreferenceIsolation(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
previous := gdb.GetConfig("default")
|
||||
if err := gdb.SetConfigGroup("default", gdb.ConfigGroup{{Type: "sqlite", Link: "sqlite::@file(:memory:)", MaxOpenConnCount: 1}}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
db := g.DB()
|
||||
t.Cleanup(func() {
|
||||
_ = db.GetCore().ClearCacheAll(ctx)
|
||||
_ = db.Close(ctx)
|
||||
_ = gdb.SetConfigGroup("default", previous)
|
||||
})
|
||||
if _, err := db.Exec(ctx, "CREATE TABLE sys_config (config_id INTEGER PRIMARY KEY AUTOINCREMENT, tenant_id TEXT, config_key TEXT, config_value TEXT)"); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
enabled, err := warningWechatEnabled(ctx, "tenant", 1)
|
||||
if err != nil || !enabled {
|
||||
t.Fatalf("legacy default: %v %v", enabled, err)
|
||||
}
|
||||
for _, item := range []struct{ tenant, value string }{{"tenant", "0"}, {"other", "1"}} {
|
||||
if _, err := db.Model("sys_config").Data(do.SysConfig{TenantId: item.tenant, ConfigKey: "app.wechat.warning.enabled.1", ConfigValue: item.value}).Insert(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
enabled, err = warningWechatEnabled(ctx, "tenant", 1)
|
||||
if err != nil || enabled {
|
||||
t.Fatalf("disabled preference ignored: %v %v", enabled, err)
|
||||
}
|
||||
enabled, err = warningWechatEnabled(ctx, "other", 1)
|
||||
if err != nil || !enabled {
|
||||
t.Fatalf("other tenant affected: %v %v", enabled, err)
|
||||
}
|
||||
enabled, err = warningWechatEnabled(ctx, "tenant", 2)
|
||||
if err != nil || !enabled {
|
||||
t.Fatalf("other user affected: %v %v", enabled, err)
|
||||
}
|
||||
if _, err := db.Model("sys_config").Where("tenant_id", "tenant").Data(do.SysConfig{ConfigValue: "1"}).Update(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
enabled, err = warningWechatEnabled(ctx, "tenant", 1)
|
||||
if err != nil || !enabled {
|
||||
t.Fatalf("stale preference after enable: %v %v", enabled, err)
|
||||
}
|
||||
if _, err := db.Exec(ctx, "DROP TABLE sys_config"); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
_ = db.GetCore().ClearCacheAll(ctx)
|
||||
enabled, err = warningWechatEnabled(ctx, "tenant", 1)
|
||||
if err == nil || enabled {
|
||||
t.Fatal("read errors must fail closed")
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,112 @@
|
||||
package warningnotify
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"github.com/gogf/gf/v2/errors/gerror"
|
||||
"sort"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type Collector struct{ batches map[string]*Batch }
|
||||
|
||||
func NewCollector() *Collector { return &Collector{batches: make(map[string]*Batch)} }
|
||||
func (c *Collector) Add(ctx context.Context, events []Event) error {
|
||||
for _, batch := range BuildBatches(events) {
|
||||
switch batch.Method {
|
||||
case MethodPlatform:
|
||||
deliveries, err := resolvePlatformDeliveries(ctx, batch)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for _, delivery := range deliveries {
|
||||
c.merge(delivery.Batch, &delivery.Group)
|
||||
}
|
||||
case MethodWechat:
|
||||
for _, delivery := range resolveWechatDeliveries(ctx, batch) {
|
||||
c.merge(delivery.Batch, &delivery.Group)
|
||||
}
|
||||
default:
|
||||
c.merge(batch, nil)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
func (c *Collector) merge(batch Batch, recipient *platformRecipientGroup) {
|
||||
keyBytes, _ := json.Marshal(struct {
|
||||
Group groupKey
|
||||
Recipient *platformRecipientGroup
|
||||
}{groupKeyForEvent(batch.Method, batch.Recipients, Event{Business: batch.Business, TenantID: batch.TenantID, DeptID: batch.DeptID, CreatedBy: batch.CreatedBy, Category: batch.Category}), recipient})
|
||||
key := string(keyBytes)
|
||||
current := c.batches[key]
|
||||
if current == nil {
|
||||
copy := batch
|
||||
copy.Items = nil
|
||||
copy.TotalCount = 0
|
||||
copy.prepared = recipient
|
||||
current = ©
|
||||
c.batches[key] = current
|
||||
}
|
||||
current.TotalCount += len(batch.Items)
|
||||
current.WarnStage = max(current.WarnStage, batch.WarnStage)
|
||||
current.HighestLevel = max(current.HighestLevel, highestLevel(batch.Items))
|
||||
current.AffectOperation = current.AffectOperation || hasAffectOperation(batch.Items)
|
||||
latest := latestEvent(batch.Items).WarnTime
|
||||
if latest.After(current.LatestTime) {
|
||||
current.LatestTime = latest
|
||||
}
|
||||
current.Items = append(current.Items, batch.Items...)
|
||||
if recipient != nil {
|
||||
sort.SliceStable(current.Items, func(i, j int) bool {
|
||||
if current.Items[i].WarnLevel != current.Items[j].WarnLevel {
|
||||
return current.Items[i].WarnLevel > current.Items[j].WarnLevel
|
||||
}
|
||||
if current.Items[i].WarnStage != current.Items[j].WarnStage {
|
||||
return current.Items[i].WarnStage > current.Items[j].WarnStage
|
||||
}
|
||||
return current.Items[i].ItemCode < current.Items[j].ItemCode
|
||||
})
|
||||
if len(current.Items) > 5 {
|
||||
preview := append([]Event(nil), current.Items[:5]...)
|
||||
current.Items = preview
|
||||
}
|
||||
}
|
||||
}
|
||||
func (c *Collector) Flush(ctx context.Context) error {
|
||||
senderMu.RLock()
|
||||
sender := currentSender
|
||||
senderMu.RUnlock()
|
||||
if sender == nil {
|
||||
sender = LogSender{}
|
||||
}
|
||||
keys := make([]string, 0, len(c.batches))
|
||||
for key := range c.batches {
|
||||
keys = append(keys, key)
|
||||
}
|
||||
sort.Strings(keys)
|
||||
var failures []string
|
||||
for _, key := range keys {
|
||||
if err := ctx.Err(); err != nil {
|
||||
return err
|
||||
}
|
||||
batch := c.batches[key]
|
||||
applyContent(batch)
|
||||
if err := sender.Send(ctx, *batch); err != nil {
|
||||
failures = append(failures, err.Error())
|
||||
}
|
||||
}
|
||||
if len(failures) > 0 {
|
||||
return gerror.New(strings.Join(failures, "; "))
|
||||
}
|
||||
return nil
|
||||
}
|
||||
func batchItemCount(batch Batch) int {
|
||||
if batch.TotalCount > 0 {
|
||||
return batch.TotalCount
|
||||
}
|
||||
return len(batch.Items)
|
||||
}
|
||||
func batchHighestLevel(batch Batch) int { return max(batch.HighestLevel, highestLevel(batch.Items)) }
|
||||
func batchHasAffectOperation(batch Batch) bool {
|
||||
return batch.AffectOperation || hasAffectOperation(batch.Items)
|
||||
}
|
||||
@@ -0,0 +1,151 @@
|
||||
package warningnotify
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/model/do"
|
||||
|
||||
"github.com/gogf/gf/v2/database/gdb"
|
||||
"github.com/gogf/gf/v2/frame/g"
|
||||
)
|
||||
|
||||
type batchRecordingSender struct{ batches []Batch }
|
||||
|
||||
func (sender *batchRecordingSender) Send(ctx context.Context, batch Batch) error {
|
||||
sender.batches = append(sender.batches, batch)
|
||||
return nil
|
||||
}
|
||||
|
||||
func TestCollectorSkipsWechatEvenForEligibleEnterpriseCreators(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
previous := gdb.GetConfig("default")
|
||||
if err := gdb.SetConfigGroup("default", gdb.ConfigGroup{{Type: "sqlite", Link: "sqlite::@file(:memory:)", MaxOpenConnCount: 1}}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
db := g.DB()
|
||||
t.Cleanup(func() {
|
||||
_ = db.GetCore().ClearCacheAll(ctx)
|
||||
_ = db.Close(ctx)
|
||||
_ = gdb.SetConfigGroup("default", previous)
|
||||
})
|
||||
for _, statement := range []string{
|
||||
"CREATE TABLE sys_dept (dept_id INTEGER PRIMARY KEY, tenant_id TEXT, ancestors TEXT, deleted_at DATETIME)",
|
||||
"CREATE TABLE sys_user (user_id INTEGER PRIMARY KEY, tenant_id TEXT, user_type TEXT, dept_id INTEGER, status TEXT, deleted_at DATETIME)",
|
||||
"CREATE TABLE t_sim_card_info (id INTEGER PRIMARY KEY, tenant_id TEXT, dept_id INTEGER)",
|
||||
"CREATE TABLE t_sim_terminal (terminal_id INTEGER, tenant_id TEXT, iccid TEXT, msisdn TEXT)",
|
||||
"CREATE TABLE t_terminal (terminal_id INTEGER, tenant_id TEXT, leader INTEGER, deleted_at DATETIME)",
|
||||
} {
|
||||
if _, err := db.Exec(ctx, statement); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
for _, dept := range []do.SysDept{
|
||||
{DeptId: 175, TenantId: "tenant", Ancestors: "0,100,"},
|
||||
{DeptId: 176, TenantId: "tenant", Ancestors: "0,100,"},
|
||||
{DeptId: 185, TenantId: "tenant", Ancestors: "0,100,175,"},
|
||||
{DeptId: 195, TenantId: "tenant", Ancestors: "0,100,175,185"},
|
||||
{DeptId: 100, TenantId: "tenant", Ancestors: "0"},
|
||||
} {
|
||||
if _, err := db.Model("sys_dept").Data(dept).Insert(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
for _, user := range []do.SysUser{
|
||||
{UserId: 133, TenantId: "tenant", UserType: "sys_user", DeptId: 175, Status: "0"},
|
||||
{UserId: 134, TenantId: "tenant", UserType: "sys_user", DeptId: 175, Status: "0"},
|
||||
{UserId: 135, TenantId: "other", UserType: "sys_user", DeptId: 175, Status: "0"},
|
||||
{UserId: 136, TenantId: "tenant", UserType: "sys_user", DeptId: 175, Status: "1"},
|
||||
{UserId: 137, TenantId: "tenant", UserType: "sys_user", DeptId: 175, Status: "0"},
|
||||
} {
|
||||
if _, err := db.Model("sys_user").Data(user).Insert(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
if _, err := db.Model("sys_user").Where("user_id", 137).Delete(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
for _, card := range []do.TSimCardInfo{
|
||||
{Id: 84189, TenantId: "tenant", DeptId: 175},
|
||||
{Id: 84190, TenantId: "tenant", DeptId: 176},
|
||||
{Id: 84191, TenantId: "other", DeptId: 175},
|
||||
{Id: 84199, TenantId: "tenant", DeptId: 185},
|
||||
{Id: 84200, TenantId: "tenant", DeptId: 195},
|
||||
{Id: 84201, TenantId: "tenant", DeptId: 100},
|
||||
{Id: 84202, TenantId: "tenant", DeptId: 999},
|
||||
} {
|
||||
if _, err := db.Model("t_sim_card_info").Data(card).Insert(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
base := Event{Business: BusinessSimWarning, TenantID: "tenant", DeptID: 175, CreatedBy: 133, ItemType: 1, ItemID: 84189, ItemCode: "89860401791508277319", WarnMethods: MethodPlatform, WarnStage: 2}
|
||||
for _, test := range []struct {
|
||||
name string
|
||||
creator int64
|
||||
card int64
|
||||
expected int
|
||||
}{
|
||||
{"enterprise creator", 133, 84189, 1},
|
||||
{"another creator in same department", 134, 84189, 1},
|
||||
{"sibling organization", 133, 84190, 0},
|
||||
{"direct descendant card", 133, 84199, 1},
|
||||
{"deep descendant card", 133, 84200, 1},
|
||||
{"ancestor organization", 133, 84201, 0},
|
||||
{"missing card organization", 133, 84202, 0},
|
||||
{"different card tenant", 133, 84191, 0},
|
||||
{"different user tenant", 135, 84189, 0},
|
||||
{"disabled creator", 136, 84189, 0},
|
||||
{"deleted creator", 137, 84189, 0},
|
||||
{"missing creator", 999, 84189, 0},
|
||||
} {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
event := base
|
||||
event.CreatedBy = test.creator
|
||||
event.ItemID = test.card
|
||||
collector := NewCollector()
|
||||
for page := 0; page < 2; page++ {
|
||||
event.EventID = int64(page + 1)
|
||||
if err := collector.Add(ctx, []Event{event}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
sender := &batchRecordingSender{}
|
||||
restore := SetSender(sender)
|
||||
defer restore()
|
||||
if err := collector.Flush(ctx); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
platformCount := 0
|
||||
for _, batch := range sender.batches {
|
||||
if batch.Method == MethodWechat {
|
||||
t.Fatal("wechat subscription batch must not be sent")
|
||||
}
|
||||
if batch.Method == MethodPlatform {
|
||||
platformCount++
|
||||
}
|
||||
}
|
||||
if test.expected > 0 && platformCount == 0 {
|
||||
t.Fatal("eligible creator lost in-app notification")
|
||||
}
|
||||
legacy := Batch{Method: MethodWechat, Business: BusinessSimWarning, TenantID: "tenant", Items: []Event{event}}
|
||||
eligible := 0
|
||||
for _, delivery := range resolveWechatDeliveries(ctx, legacy) {
|
||||
if delivery.Group.SendUserType == sendUserTypeAgent && len(delivery.Group.UserIDs) == 1 && delivery.Group.UserIDs[0] == test.creator {
|
||||
eligible++
|
||||
}
|
||||
}
|
||||
if eligible != test.expected {
|
||||
t.Fatalf("legacy eligibility = %d, want %d", eligible, test.expected)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestWechatPreparedDeliveryDoesNotResolveAgain(t *testing.T) {
|
||||
batch := Batch{Business: BusinessSimWarning, Method: MethodWechat, prepared: &platformRecipientGroup{SendUserType: sendUserTypeAgent, UserIDs: []int64{133}}, Items: []Event{{CreatedBy: 133, DeptID: 175, ItemType: 1, ItemID: 84189}}}
|
||||
deliveries := resolveWechatDeliveries(context.Background(), batch)
|
||||
if len(deliveries) != 1 || len(deliveries[0].Group.UserIDs) != 1 || deliveries[0].Group.UserIDs[0] != 133 {
|
||||
t.Fatalf("unexpected prepared deliveries: %+v", deliveries)
|
||||
}
|
||||
}
|
||||
@@ -48,6 +48,11 @@ type Event struct {
|
||||
}
|
||||
|
||||
type Batch struct {
|
||||
TotalCount int
|
||||
HighestLevel int
|
||||
LatestTime time.Time
|
||||
AffectOperation bool
|
||||
prepared *platformRecipientGroup
|
||||
Business string
|
||||
Method string
|
||||
TenantID string
|
||||
|
||||
@@ -10,6 +10,7 @@ import (
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
@@ -83,7 +84,7 @@ func sendWarningWechat(ctx context.Context, batch Batch) error {
|
||||
batch.DeptID,
|
||||
batch.Category,
|
||||
batch.WarnStage,
|
||||
len(batch.Items),
|
||||
batchItemCount(batch),
|
||||
)
|
||||
return nil
|
||||
}
|
||||
@@ -92,40 +93,61 @@ func sendWarningWechat(ctx context.Context, batch Batch) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
deliveries := resolveTerminalPlatformDeliveries(ctx, batch)
|
||||
deliveries := resolveWechatDeliveries(ctx, batch)
|
||||
if len(deliveries) == 0 {
|
||||
g.Log().Infof(ctx, "WarningNotify wechat subscribe skipped, reason:no terminal recipients, tenantId:%s, count:%d", batch.TenantID, len(batch.Items))
|
||||
return nil
|
||||
}
|
||||
|
||||
var sendErrs []string
|
||||
for _, delivery := range deliveries {
|
||||
if len(delivery.Group.UserIDs) == 0 || len(delivery.Batch.Items) == 0 {
|
||||
if len(delivery.Group.UserIDs) == 0 || batchItemCount(delivery.Batch) == 0 {
|
||||
continue
|
||||
}
|
||||
userID := delivery.Group.UserIDs[0]
|
||||
if batch.Business == BusinessSimWarning {
|
||||
enabled, err := warningWechatEnabled(ctx, batch.TenantID, userID)
|
||||
if err != nil {
|
||||
sendErrs = append(sendErrs, fmt.Sprintf("userId=%d query preference: %v", userID, err))
|
||||
continue
|
||||
}
|
||||
if !enabled {
|
||||
g.Log().Infof(ctx, "WarningNotify wechat subscribe skipped, reason:user preference disabled, tenantId:%s, userId:%d, count:%d", batch.TenantID, userID, batchItemCount(delivery.Batch))
|
||||
continue
|
||||
}
|
||||
}
|
||||
bindings, err := queryWechatBindings(ctx, batch.TenantID, userID)
|
||||
if err != nil {
|
||||
sendErrs = append(sendErrs, fmt.Sprintf("userId=%d query binding: %v", userID, err))
|
||||
continue
|
||||
}
|
||||
if len(bindings) == 0 {
|
||||
g.Log().Infof(ctx, "WarningNotify wechat subscribe skipped, reason:no openid, tenantId:%s, userId:%d, count:%d", batch.TenantID, userID, len(delivery.Batch.Items))
|
||||
g.Log().Infof(ctx, "WarningNotify wechat subscribe skipped, reason:no openid, tenantId:%s, userId:%d, count:%d", batch.TenantID, userID, batchItemCount(delivery.Batch))
|
||||
continue
|
||||
}
|
||||
|
||||
acquired, sentKey, err := acquireWechatSendDedupe(ctx, delivery.Batch, userID)
|
||||
if err != nil {
|
||||
g.Log().Warningf(ctx, "WarningNotify wechat subscribe dedupe check failed, tenantId:%s, userId:%d, category:%s, stage:%d, count:%d, err:%+v",
|
||||
batch.TenantID, userID, delivery.Batch.Category, delivery.Batch.WarnStage, len(delivery.Batch.Items), err)
|
||||
batch.TenantID, userID, delivery.Batch.Category, delivery.Batch.WarnStage, batchItemCount(delivery.Batch), err)
|
||||
} else if !acquired {
|
||||
g.Log().Infof(ctx, "WarningNotify wechat subscribe skipped, reason:duplicated, tenantId:%s, userId:%d, key:%s, category:%s, stage:%d, count:%d",
|
||||
batch.TenantID, userID, sentKey, delivery.Batch.Category, delivery.Batch.WarnStage, len(delivery.Batch.Items))
|
||||
batch.TenantID, userID, sentKey, delivery.Batch.Category, delivery.Batch.WarnStage, batchItemCount(delivery.Batch))
|
||||
continue
|
||||
}
|
||||
|
||||
successCount := 0
|
||||
for _, binding := range bindings {
|
||||
if batch.Business == BusinessSimWarning {
|
||||
enabled, err := warningWechatEnabled(ctx, batch.TenantID, userID)
|
||||
if err != nil {
|
||||
sendErrs = append(sendErrs, fmt.Sprintf("userId=%d query preference: %v", userID, err))
|
||||
continue
|
||||
}
|
||||
if !enabled {
|
||||
g.Log().Infof(ctx, "WarningNotify wechat subscribe skipped, reason:user preference disabled before send, tenantId:%s, userId:%d", batch.TenantID, userID)
|
||||
continue
|
||||
}
|
||||
}
|
||||
templateID := wechatTemplateID(delivery.Batch.Items, cfg.TemplateID)
|
||||
if templateID == "" {
|
||||
sendErrs = append(sendErrs, fmt.Sprintf("userId=%d template id empty", userID))
|
||||
@@ -135,11 +157,11 @@ func sendWarningWechat(ctx context.Context, batch Batch) error {
|
||||
if err := sendWechatSubscription(ctx, cfg, payload); err != nil {
|
||||
sendErrs = append(sendErrs, fmt.Sprintf("userId=%d openid=%s: %v", userID, maskOpenID(binding.OpenID), err))
|
||||
g.Log().Errorf(ctx, "WarningNotify wechat subscribe failed, tenantId:%s, userId:%d, openid:%s, category:%s, stage:%d, count:%d, err:%+v",
|
||||
batch.TenantID, userID, maskOpenID(binding.OpenID), delivery.Batch.Category, delivery.Batch.WarnStage, len(delivery.Batch.Items), err)
|
||||
batch.TenantID, userID, maskOpenID(binding.OpenID), delivery.Batch.Category, delivery.Batch.WarnStage, batchItemCount(delivery.Batch), err)
|
||||
continue
|
||||
}
|
||||
g.Log().Infof(ctx, "WarningNotify wechat subscribe sent, tenantId:%s, userId:%d, openid:%s, category:%s, stage:%d, count:%d",
|
||||
batch.TenantID, userID, maskOpenID(binding.OpenID), delivery.Batch.Category, delivery.Batch.WarnStage, len(delivery.Batch.Items))
|
||||
batch.TenantID, userID, maskOpenID(binding.OpenID), delivery.Batch.Category, delivery.Batch.WarnStage, batchItemCount(delivery.Batch))
|
||||
successCount++
|
||||
}
|
||||
if acquired && successCount == 0 {
|
||||
@@ -153,6 +175,107 @@ func sendWarningWechat(ctx context.Context, batch Batch) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func resolveWechatDeliveries(ctx context.Context, batch Batch) []platformDelivery {
|
||||
if batch.prepared != nil {
|
||||
return []platformDelivery{{Group: *batch.prepared, Batch: batch}}
|
||||
}
|
||||
deliveries := resolveTerminalPlatformDeliveries(ctx, batch)
|
||||
appendEnterpriseWechatDeliveries(ctx, batch, &deliveries)
|
||||
if len(deliveries) == 0 {
|
||||
g.Log().Infof(ctx, "WarningNotify wechat subscribe skipped, reason:no eligible recipients, tenantId:%s, count:%d", batch.TenantID, batchItemCount(batch))
|
||||
}
|
||||
return deliveries
|
||||
}
|
||||
|
||||
func appendEnterpriseWechatDeliveries(ctx context.Context, batch Batch, deliveries *[]platformDelivery) {
|
||||
if batch.Business != BusinessSimWarning {
|
||||
return
|
||||
}
|
||||
itemsByUser := make(map[int64][]Event)
|
||||
seenByUser := make(map[int64]map[int]struct{})
|
||||
for index, item := range batch.Items {
|
||||
if item.ItemType != 1 {
|
||||
continue
|
||||
}
|
||||
createdBy := item.CreatedBy
|
||||
if createdBy <= 0 {
|
||||
createdBy = batch.CreatedBy
|
||||
}
|
||||
if createdBy <= 0 || item.ItemID <= 0 {
|
||||
continue
|
||||
}
|
||||
userID, ok, err := queryEnterpriseWarningUser(ctx, batch.TenantID, createdBy, item.ItemID)
|
||||
if err != nil {
|
||||
g.Log().Warningf(ctx, "WarningNotify query enterprise user failed, tenantId:%s, userId:%d, cardId:%d, err:%+v", batch.TenantID, createdBy, item.ItemID, err)
|
||||
continue
|
||||
}
|
||||
if ok {
|
||||
appendTerminalEvent(itemsByUser, seenByUser, userID, index, item)
|
||||
}
|
||||
}
|
||||
userIDs := make([]int64, 0, len(itemsByUser))
|
||||
for userID := range itemsByUser {
|
||||
userIDs = append(userIDs, userID)
|
||||
}
|
||||
sort.Slice(userIDs, func(i, j int) bool { return userIDs[i] < userIDs[j] })
|
||||
for _, userID := range userIDs {
|
||||
items := itemsByUser[userID]
|
||||
enterpriseBatch := batch
|
||||
enterpriseBatch.Items = append([]Event(nil), items...)
|
||||
enterpriseBatch.WarnStage = highestStage(enterpriseBatch.Items)
|
||||
*deliveries = append(*deliveries, platformDelivery{
|
||||
Group: platformRecipientGroup{SendUserType: sendUserTypeAgent, UserIDs: []int64{userID}},
|
||||
Batch: enterpriseBatch,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func enterpriseWarningRecipientEligible(userType string, userDeptID, cardDeptID int64, ancestors string) bool {
|
||||
if userType != consts.SysUserTypeSys || userDeptID <= 0 || cardDeptID <= 0 {
|
||||
return false
|
||||
}
|
||||
if userDeptID == cardDeptID {
|
||||
return true
|
||||
}
|
||||
target := strconv.FormatInt(userDeptID, 10)
|
||||
for _, ancestor := range strings.Split(ancestors, ",") {
|
||||
if strings.TrimSpace(ancestor) == target {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
func queryEnterpriseWarningUser(ctx context.Context, tenantID string, userID, cardID int64) (int64, bool, error) {
|
||||
if strings.TrimSpace(tenantID) == "" || userID <= 0 || cardID <= 0 {
|
||||
return 0, false, nil
|
||||
}
|
||||
var result struct {
|
||||
UserID int64 `orm:"user_id"`
|
||||
UserType string `orm:"user_type"`
|
||||
DeptID int64 `orm:"dept_id"`
|
||||
CardDeptID int64 `orm:"card_dept_id"`
|
||||
Ancestors string `orm:"ancestors"`
|
||||
}
|
||||
err := g.DB().Model(dao.TSimCardInfo.Table()+" c").Ctx(ctx).Safe().
|
||||
LeftJoin(dao.SysUser.Table()+" u", "u.user_id = "+strconv.FormatInt(userID, 10)).
|
||||
InnerJoin(dao.SysDept.Table()+" d", "d.dept_id = c.dept_id AND d.tenant_id = c.tenant_id").
|
||||
InnerJoin(dao.SysDept.Table()+" ud", "ud.dept_id = u.dept_id AND ud.tenant_id = u.tenant_id").
|
||||
Fields("u.user_id, u.user_type, u.dept_id, c.dept_id AS card_dept_id, d.ancestors").
|
||||
Where("c.tenant_id", tenantID).
|
||||
Where("c.id", cardID).
|
||||
Where("u.tenant_id", tenantID).
|
||||
Where("u.user_type", consts.SysUserTypeSys).
|
||||
Where("u.status", consts.SysDeptStatusNormal).
|
||||
Where("u.deleted_at IS NULL").
|
||||
Where("d.deleted_at IS NULL").
|
||||
Where("ud.deleted_at IS NULL").
|
||||
Scan(&result)
|
||||
if err != nil {
|
||||
return 0, false, err
|
||||
}
|
||||
return result.UserID, result.UserID > 0 && enterpriseWarningRecipientEligible(result.UserType, result.DeptID, result.CardDeptID, result.Ancestors), nil
|
||||
}
|
||||
|
||||
func loadWarningWechatConfig(ctx context.Context) (warningWechatConfig, bool) {
|
||||
var cfg warningWechatConfig
|
||||
loaded := false
|
||||
@@ -358,6 +481,9 @@ func wechatBatchHasExpireKind(batch Batch) bool {
|
||||
|
||||
func wechatBatchTime(batch Batch) time.Time {
|
||||
latest := latestEvent(batch.Items)
|
||||
if !batch.LatestTime.IsZero() {
|
||||
latest.WarnTime = batch.LatestTime
|
||||
}
|
||||
if latest.WarnTime.IsZero() {
|
||||
return time.Now()
|
||||
}
|
||||
@@ -392,6 +518,9 @@ func wechatTemplateID(items []Event, fallback string) string {
|
||||
|
||||
func buildWechatPayload(batch Batch, openID string, templateID string, page string) warningWechatPayload {
|
||||
latest := latestEvent(batch.Items)
|
||||
if !batch.LatestTime.IsZero() {
|
||||
latest.WarnTime = batch.LatestTime
|
||||
}
|
||||
return warningWechatPayload{
|
||||
ToUser: strings.TrimSpace(openID),
|
||||
TemplateID: strings.TrimSpace(templateID),
|
||||
@@ -417,25 +546,25 @@ func latestEvent(items []Event) Event {
|
||||
}
|
||||
|
||||
func wechatDeviceText(batch Batch) string {
|
||||
if len(batch.Items) == 0 {
|
||||
if batchItemCount(batch) == 0 {
|
||||
return "--"
|
||||
}
|
||||
first := displayObjectCode(batch.Items[0])
|
||||
if len(batch.Items) == 1 {
|
||||
if batchItemCount(batch) == 1 {
|
||||
return first
|
||||
}
|
||||
return fmt.Sprintf("%s等%d张卡", first, len(batch.Items))
|
||||
return fmt.Sprintf("%s等%d张卡", first, batchItemCount(batch))
|
||||
}
|
||||
|
||||
func wechatDeviceCharacterText(batch Batch) string {
|
||||
if len(batch.Items) == 0 {
|
||||
if batchItemCount(batch) == 0 {
|
||||
return "NA"
|
||||
}
|
||||
first := displayObjectCode(batch.Items[0])
|
||||
if len(batch.Items) == 1 {
|
||||
if batchItemCount(batch) == 1 {
|
||||
return first
|
||||
}
|
||||
return fmt.Sprintf("%sx%d", first, len(batch.Items))
|
||||
return fmt.Sprintf("TOTAL-%d", batchItemCount(batch))
|
||||
}
|
||||
|
||||
func wechatTypeText(batch Batch) string {
|
||||
@@ -443,25 +572,28 @@ func wechatTypeText(batch Batch) string {
|
||||
if title == "" {
|
||||
title = "设备预警"
|
||||
}
|
||||
if len(batch.Items) > 1 {
|
||||
return fmt.Sprintf("%s,共%d张卡", title, len(batch.Items))
|
||||
if batchItemCount(batch) > 1 {
|
||||
return fmt.Sprintf("%s,共%d张卡", title, batchItemCount(batch))
|
||||
}
|
||||
return title
|
||||
}
|
||||
|
||||
func wechatReasonText(batch Batch) string {
|
||||
if len(batch.Items) == 1 {
|
||||
if batchItemCount(batch) == 1 {
|
||||
return emailWarningReason(batch.Items[0])
|
||||
}
|
||||
reason := fmt.Sprintf("共%d张卡触发预警,最高级别:%s", len(batch.Items), levelText(highestLevel(batch.Items)))
|
||||
if hasAffectOperation(batch.Items) {
|
||||
reason := fmt.Sprintf("%s\uff0c\u5171%d\u5f20\u5361", levelText(batchHighestLevel(batch)), batchItemCount(batch))
|
||||
if batchHasAffectOperation(batch) {
|
||||
reason += ",涉及停机或断网"
|
||||
}
|
||||
return reason
|
||||
}
|
||||
|
||||
func wechatValueText(batch Batch) string {
|
||||
if len(batch.Items) == 0 {
|
||||
if batchItemCount(batch) > 1 {
|
||||
return fmt.Sprintf("TOTAL-%d", batchItemCount(batch))
|
||||
}
|
||||
if batchItemCount(batch) == 0 {
|
||||
return "NA"
|
||||
}
|
||||
item := batch.Items[0]
|
||||
|
||||
Reference in New Issue
Block a user