sync: 同步 v1.0.4 并脱敏配置与镜像

This commit is contained in:
MiFaZhan
2026-10-29 14:36:49 +08:00
parent 743e8e6e6e
commit 2206a393f7
516 changed files with 18119 additions and 2253 deletions
@@ -0,0 +1,319 @@
package simwarning
import (
"context"
"encoding/json"
"fmt"
"strconv"
"strings"
"time"
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/consts"
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/dao"
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/library/contexts"
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/logic/smsgateway"
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/model"
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/service"
"github.com/gogf/gf/v2/encoding/gjson"
"github.com/gogf/gf/v2/frame/g"
)
const (
monthlyExpiryPageSize = 100
monthlyExpiryCurrentSMSCode = "SMS_512596191"
monthlyExpiryNextSMSCode = "SMS_512646156"
monthlyExpirySMSSignName = "示例企业"
monthlyExpirySentTTL = 93 * 24 * time.Hour
)
var monthlyExpiryLocation = time.FixedZone("Asia/Shanghai", 8*60*60)
type monthlyExpiryCompany struct {
DeptID int64 `orm:"dept_id"`
TenantID string `orm:"tenant_id"`
LeaderID int64 `orm:"leader"`
}
type monthlyExpirySummary struct {
Month string `orm:"expiry_month"`
Count int64 `orm:"card_count"`
FirstDate string `orm:"first_date"`
LastDate string `orm:"last_date"`
}
type monthlyExpiryLeader struct {
UserID int64 `orm:"user_id"`
Phone string `orm:"phonenumber"`
}
type monthlyExpiryNotice struct {
Company monthlyExpiryCompany
Summary monthlyExpirySummary
Leader monthlyExpiryLeader
RunTime time.Time
}
type monthlyExpiryStats struct {
Companies, Groups, Sent, Duplicate, Errors int
}
// 回调隔离查询和投递,使分页及部分失败可以在不连库、不发送消息的情况下测试。
type monthlyExpiryRunner struct {
companies func(context.Context, int64) ([]monthlyExpiryCompany, error)
summaries func(context.Context, monthlyExpiryCompany, time.Time, time.Time) ([]monthlyExpirySummary, error)
leader func(context.Context, monthlyExpiryCompany) (*monthlyExpiryLeader, error)
deliver func(context.Context, monthlyExpiryNotice, string) (bool, error)
}
// NotifyMonthlyExpiry 由独立的 NotifyMonthlyPackageExpiry 任务在每月15日09:00调用。
// 实时读取 flow_pkg_end_at(包括已办理的未来续费),不依赖 Redis 逐卡候选。
func (s *sSimWarning) NotifyMonthlyExpiry(ctx context.Context) {
if ctx == nil {
ctx = context.Background()
}
runTime := time.Now().In(monthlyExpiryLocation)
// 企业顺序发送,短信最多每秒5次,避免企业多时集中冲击网关。
ticker := time.NewTicker(200 * time.Millisecond)
defer ticker.Stop()
runner := monthlyExpiryRunner{
companies: queryMonthlyExpiryCompanies,
summaries: queryMonthlyExpirySummaries,
leader: queryMonthlyExpiryLeader,
deliver: func(ctx context.Context, notice monthlyExpiryNotice, channel string) (bool, error) {
return deliverMonthlyExpiry(ctx, notice, channel, func() error {
if channel == "platform" {
return writeMonthlyExpiryMessage(ctx, notice)
}
select {
case <-ctx.Done():
return ctx.Err()
case <-ticker.C:
return sendMonthlyExpirySMS(ctx, notice)
}
})
},
}
stats, err := runner.run(ctx, runTime)
if err != nil {
g.Log().Errorf(ctx, "SimWarning.MonthlyExpiry interrupted, err:%+v", err)
}
g.Log().Infof(ctx, "SimWarning.MonthlyExpiry end, runMonth:%s, companies:%d, groups:%d, sent:%d, duplicate:%d, errors:%d",
runTime.Format("2006-01"), stats.Companies, stats.Groups, stats.Sent, stats.Duplicate, stats.Errors)
}
func (r monthlyExpiryRunner) run(ctx context.Context, runTime time.Time) (monthlyExpiryStats, error) {
stats := monthlyExpiryStats{}
runTime = runTime.In(monthlyExpiryLocation)
begin := calendarMonthBegin(runTime)
end := begin.AddDate(0, 2, 0)
lastID := int64(0)
for {
if err := ctx.Err(); err != nil {
return stats, err
}
companies, err := r.companies(ctx, lastID)
if err != nil {
return stats, err
}
if len(companies) == 0 {
return stats, nil
}
for _, company := range companies {
if err := ctx.Err(); err != nil {
return stats, err
}
lastID = company.DeptID
stats.Companies++
summaries, err := r.summaries(ctx, company, begin, end)
if err != nil {
stats.Errors++
g.Log().Errorf(ctx, "SimWarning.MonthlyExpiry count failed, tenant:%s, dept:%d, err:%+v", company.TenantID, company.DeptID, err)
continue
}
if len(summaries) == 0 {
continue
}
leader, err := r.leader(ctx, company)
if err != nil || leader == nil || leader.UserID <= 0 {
stats.Errors++
g.Log().Warningf(ctx, "SimWarning.MonthlyExpiry leader unavailable, tenant:%s, dept:%d, leader:%d, err:%+v", company.TenantID, company.DeptID, company.LeaderID, err)
continue
}
for _, summary := range summaries {
if summary.Count <= 0 {
continue
}
stats.Groups++
notice := monthlyExpiryNotice{Company: company, Summary: summary, Leader: *leader, RunTime: runTime}
for _, channel := range []string{"platform", "sms"} {
if err := ctx.Err(); err != nil {
return stats, err
}
sent, err := r.deliver(ctx, notice, channel)
if err != nil {
stats.Errors++
if sent {
stats.Sent++
g.Log().Errorf(ctx, "SimWarning.MonthlyExpiry sent but dedupe mark failed, tenant:%s, dept:%d, month:%s, channel:%s, err:%+v", company.TenantID, company.DeptID, summary.Month, channel, err)
} else {
g.Log().Errorf(ctx, "SimWarning.MonthlyExpiry delivery failed, tenant:%s, dept:%d, month:%s, channel:%s, err:%+v", company.TenantID, company.DeptID, summary.Month, channel, err)
}
} else if sent {
stats.Sent++
g.Log().Infof(ctx, "SimWarning.MonthlyExpiry sent, tenant:%s, dept:%d, month:%s, channel:%s", company.TenantID, company.DeptID, summary.Month, channel)
} else {
stats.Duplicate++
g.Log().Infof(ctx, "SimWarning.MonthlyExpiry duplicate, tenant:%s, dept:%d, month:%s, channel:%s", company.TenantID, company.DeptID, summary.Month, channel)
}
}
}
}
if len(companies) < monthlyExpiryPageSize {
return stats, nil
}
}
}
func queryMonthlyExpiryCompanies(ctx context.Context, lastID int64) ([]monthlyExpiryCompany, error) {
var rows []monthlyExpiryCompany
err := dao.SysDept.Ctx(ctx).
Fields("dept_id, tenant_id, leader").
WhereGT("dept_id", lastID).
Where("dept_type", 1).
Where("status", consts.SysDeptStatusNormal).
WhereNull("deleted_at").
OrderAsc("dept_id").Limit(monthlyExpiryPageSize).Scan(&rows)
return rows, err
}
func queryMonthlyExpirySummaries(ctx context.Context, company monthlyExpiryCompany, begin, end time.Time) ([]monthlyExpirySummary, error) {
var rows []monthlyExpirySummary
// 按当前归属企业计数,不加载卡明细,不加旧规则的卡状态/套餐状态/池卡限制。
// 时间条件保留原列以使用已有 dept_id / flow_pkg_end_at 索引。
err := dao.TSimCardInfo.Ctx(ctx).
Fields("DATE_FORMAT(flow_pkg_end_at, '%Y-%m') AS expiry_month, COUNT(*) AS card_count, DATE_FORMAT(MIN(flow_pkg_end_at), '%Y-%m-%d') AS first_date, DATE_FORMAT(MAX(flow_pkg_end_at), '%Y-%m-%d') AS last_date").
Where("tenant_id", company.TenantID).Where("dept_id", company.DeptID).
WhereGTE("flow_pkg_end_at", begin.Format("2006-01-02 15:04:05")).
WhereLT("flow_pkg_end_at", end.Format("2006-01-02 15:04:05")).
Group("expiry_month").OrderAsc("expiry_month").Scan(&rows)
return rows, err
}
func queryMonthlyExpiryLeader(ctx context.Context, company monthlyExpiryCompany) (*monthlyExpiryLeader, error) {
if company.LeaderID <= 0 {
return nil, nil
}
var row *monthlyExpiryLeader
err := dao.SysUser.Ctx(ctx).Fields("user_id, phonenumber").
Where("tenant_id", company.TenantID).Where("user_id", company.LeaderID).
Where("user_type", consts.SysUserTypeSys).Where("status", consts.SysDeptStatusNormal).
WhereNull("deleted_at").Scan(&row)
return row, err
}
func (n monthlyExpiryNotice) nextMonth() bool {
return n.Summary.Month == calendarMonthBegin(n.RunTime.In(monthlyExpiryLocation)).AddDate(0, 1, 0).Format("2006-01")
}
func (n monthlyExpiryNotice) expiryDateText() string {
if n.Summary.FirstDate == n.Summary.LastDate {
return n.Summary.FirstDate
}
return n.Summary.FirstDate + "至" + n.Summary.LastDate
}
func (n monthlyExpiryNotice) content() string {
deadline, stopText := "本月", "本月底"
if n.nextMonth() {
deadline, stopText = "次月", ""
}
return fmt.Sprintf("尊敬的客户:\n您好!您名下共有【%d】张物联网卡将于【%s】套餐到期,请及时办理续费,避免业务中断。\n请最晚于%s25日前完成续费,续费后卡片可正常保持通信使用。若套餐到期未续费,%s卡片将执行停机,后续会做销户处理,销户后卡片将无法恢复使用,请您知悉。\n如有疑问,可联系您的销售经理咨询。", n.Summary.Count, n.expiryDateText(), deadline, stopText)
}
func writeMonthlyExpiryMessage(ctx context.Context, n monthlyExpiryNotice) error {
messageCtx := contexts.Set(ctx, &model.Context{User: &model.Identity{BaseClaims: model.BaseClaims{
TenantId: n.Company.TenantID, DeptId: n.Company.DeptID,
}}})
return service.CommonTMessageLog().Edit(messageCtx, monthlyExpiryMessageParam(n))
}
func monthlyExpiryMessageParam(n monthlyExpiryNotice) *model.TMessageLogEditParam {
messageType, sendUserType, isRecall := uint(1), uint(1), uint(0)
title, content, link := "到期提醒", n.content(), ""
return &model.TMessageLogEditParam{
MessageType: &messageType, SendUserType: &sendUserType,
UserIds: gjson.New([]int64{n.Leader.UserID}),
MessageTitle: &title, MessageContent: &content, MessageLink: &link, IsRecall: &isRecall,
}
}
func sendMonthlyExpirySMS(ctx context.Context, n monthlyExpiryNotice) error {
phone := strings.TrimSpace(n.Leader.Phone)
if phone == "" {
return fmt.Errorf("企业负责人未设置手机号")
}
code, key := monthlyExpiryCurrentSMSCode, "warning.expire.monthly.currentMonthParamsTemplate"
if n.nextMonth() {
code, key = monthlyExpiryNextSMSCode, "warning.expire.monthly.nextMonthParamsTemplate"
}
value, err := g.Cfg().Get(ctx, key)
if err != nil {
return err
}
content, err := renderMonthlyExpirySMSParams(value.String(), n.Summary.Count, n.expiryDateText())
if err != nil {
return fmt.Errorf("短信模板%s参数未就绪(配置%s): %w", code, key, err)
}
// 模板和签名均已核实,供应商凭证由短信服务持有;不改通用流量预警模板。
sent, err := smsgateway.Send(ctx, smsgateway.Request{
TemplateID: code, SignName: monthlyExpirySMSSignName, PhoneNumber: phone, MsgContent: content,
})
if err != nil {
return err
}
if !sent {
return fmt.Errorf("短信网关未启用或未配置地址")
}
return nil
}
// 核实供应商变量名后配置 JSON 字符串对象,值中使用 {count} 和 {expireDate}。
// 未核实前保持配置为空,禁止猜变量名或用验证码模板替代。
func renderMonthlyExpirySMSParams(template string, count int64, expiryDate string) (string, error) {
if strings.TrimSpace(template) == "" {
return "", fmt.Errorf("请先核实已审核模板变量并配置参数模板")
}
var params map[string]string
if err := json.Unmarshal([]byte(template), &params); err != nil || len(params) == 0 {
return "", fmt.Errorf("参数模板必须为非空 JSON 字符串对象")
}
replacer := strings.NewReplacer("{count}", strconv.FormatInt(count, 10), "{expireDate}", expiryDate)
for key, value := range params {
params[key] = replacer.Replace(value)
}
encoded, err := json.Marshal(params)
return string(encoded), err
}
func monthlyExpirySentKey(n monthlyExpiryNotice, channel string) string {
return fmt.Sprintf("iotlcm:monthly-expiry:%s:%s:%d:%s:%s", n.RunTime.In(monthlyExpiryLocation).Format("200601"), n.Company.TenantID, n.Company.DeptID, n.Summary.Month, channel)
}
func deliverMonthlyExpiry(ctx context.Context, n monthlyExpiryNotice, channel string, send func() error) (bool, error) {
key := monthlyExpirySentKey(n, channel)
value, err := g.Redis().Do(ctx, "GET", key)
if err != nil {
return false, err
}
if value.String() == "sent" {
return false, nil
}
if err := send(); err != nil {
return false, err
}
// 外层任务锁串行化扫描;成功才记去重,两渠道互不阻断。
// 外部网关和 Redis 无分布式事务,失败/未知结果须核对后手动重跑,不自动重试短信。
_, err = g.Redis().Do(ctx, "SET", key, "sent", "EX", int(monthlyExpirySentTTL.Seconds()))
return true, err // 已投递;若标记失败,上层分别记录成功投递与去重错误。
}
@@ -0,0 +1,220 @@
package simwarning
import (
"context"
"encoding/json"
"errors"
"fmt"
"net/http"
"net/http/httptest"
"strings"
"testing"
"time"
"github.com/gogf/gf/v2/frame/g"
"github.com/gogf/gf/v2/os/gcfg"
)
func TestMonthlyExpiryRunPagesAndPartialFailures(t *testing.T) {
firstPage := make([]monthlyExpiryCompany, monthlyExpiryPageSize)
for i := range firstPage {
firstPage[i] = monthlyExpiryCompany{TenantID: "000000", DeptID: int64(i + 1), LeaderID: int64(i + 1001)}
}
calls := make(map[string]int)
pages := make([]int64, 0, 2)
runner := monthlyExpiryRunner{
companies: func(_ context.Context, lastID int64) ([]monthlyExpiryCompany, error) {
pages = append(pages, lastID)
if lastID == 0 {
return firstPage, nil
}
if lastID == int64(monthlyExpiryPageSize) {
return []monthlyExpiryCompany{{TenantID: "000000", DeptID: 101, LeaderID: 1101}}, nil
}
return nil, fmt.Errorf("unexpected cursor %d", lastID)
},
summaries: func(_ context.Context, company monthlyExpiryCompany, begin, end time.Time) ([]monthlyExpirySummary, error) {
if begin.Format("2006-01-02") != "2026-12-01" || end.Format("2006-01-02") != "2027-02-01" {
t.Errorf("wrong query window %s..%s", begin, end)
}
if company.DeptID == 2 {
return nil, errors.New("count failed")
}
return []monthlyExpirySummary{{Month: "2026-12", Count: 3, FirstDate: "2026-12-01", LastDate: "2026-12-31"},
{Month: "2027-01", Count: 6, FirstDate: "2027-01-31", LastDate: "2027-01-31"}}, nil
},
leader: func(_ context.Context, company monthlyExpiryCompany) (*monthlyExpiryLeader, error) {
if company.DeptID == 3 {
return nil, nil
}
return &monthlyExpiryLeader{UserID: company.LeaderID, Phone: "13800000000"}, nil
},
deliver: func(_ context.Context, notice monthlyExpiryNotice, channel string) (bool, error) {
calls[notice.Summary.Month+"/"+channel]++
if notice.Company.DeptID == 1 && channel == "sms" {
return false, errors.New("sms failed")
}
if notice.Company.DeptID == 1 && notice.Summary.Month == "2027-01" && channel == "platform" {
return false, nil
}
if notice.Company.DeptID == 4 && notice.Summary.Month == "2026-12" && channel == "platform" {
return true, errors.New("dedupe mark failed after delivery")
}
return true, nil
},
}
stats, err := runner.run(context.Background(), time.Date(2026, 12, 15, 9, 0, 0, 0, monthlyExpiryLocation))
if err != nil {
t.Fatal(err)
}
if stats.Companies != 101 || stats.Groups != 198 || stats.Sent != 393 || stats.Duplicate != 1 || stats.Errors != 5 {
t.Fatalf("unexpected stats: %+v", stats)
}
if len(pages) != 2 || pages[0] != 0 || pages[1] != 100 {
t.Fatalf("wrong page cursors: %v", pages)
}
if calls["2026-12/platform"] != 99 || calls["2027-01/platform"] != 99 {
t.Fatalf("wrong delivered group counts: %+v", calls)
}
}
func TestMonthlyExpiryNoticeContentAndRecipient(t *testing.T) {
base := monthlyExpiryNotice{
Company: monthlyExpiryCompany{TenantID: "000000", DeptID: 117, LeaderID: 12},
Leader: monthlyExpiryLeader{UserID: 12},
RunTime: time.Date(2026, 10, 15, 9, 0, 0, 0, monthlyExpiryLocation),
Summary: monthlyExpirySummary{Month: "2026-10", Count: 7, FirstDate: "2026-10-21", LastDate: "2026-10-31"},
}
current := base.content()
for _, text := range []string{"【7】张", "2026-10-21至2026-10-31", "本月25日前", "本月底卡片将执行停机"} {
if !strings.Contains(current, text) {
t.Errorf("current notice missing %q: %s", text, current)
}
}
param := monthlyExpiryMessageParam(base)
if *param.MessageType != 1 || *param.SendUserType != 1 || *param.MessageTitle != "到期提醒" || !strings.Contains(param.UserIds.String(), "12") {
t.Fatalf("wrong targeted system message: %+v", param)
}
base.Summary = monthlyExpirySummary{Month: "2026-11", Count: 9, FirstDate: "2026-11-30", LastDate: "2026-11-30"}
if got := base.content(); !strings.Contains(got, "次月25日前") || strings.Contains(got, "本月底卡片") {
t.Fatalf("next month notice incorrect: %s", got)
}
if base.expiryDateText() != "2026-11-30" {
t.Errorf("single-day range not collapsed: %s", base.expiryDateText())
}
if key := monthlyExpirySentKey(base, "sms"); !strings.Contains(key, ":000000:117:2026-11:sms") {
t.Errorf("missing dedupe dimensions: %s", key)
}
}
func TestMonthlyExpirySMSParamsRequireConfirmedTemplate(t *testing.T) {
for _, invalid := range []string{"", "invalid", "{}", `{"count":7}`} {
if _, err := renderMonthlyExpirySMSParams(invalid, 3, "2026-11-30"); err == nil {
t.Errorf("unconfirmed/invalid template accepted: %q", invalid)
}
}
content, err := renderMonthlyExpirySMSParams(`{"actualCount":"{count}","actualExpire":"{expireDate}"}`, 5, "2026-11-01至2026-11-30")
if err != nil {
t.Fatal(err)
}
var params map[string]string
if err := json.Unmarshal([]byte(content), &params); err != nil || params["actualCount"] != "5" || params["actualExpire"] != "2026-11-01至2026-11-30" {
t.Fatalf("incorrect template params: %s, %v", content, err)
}
}
func TestMonthlyExpirySMSLocalGateway(t *testing.T) {
// 所有请求只发到本机测试服务;清除环境覆盖,绝不读取真实网关配置。
for _, name := range []string{"IOTLCM_SMS_GATEWAY_ENABLED", "IOTLCM_SMS_GATEWAY_URL", "IOTLCM_SMS_GATEWAY_GRPC_ADDR", "IOTLCM_SMS_GATEWAY_TIMEOUT", "IOTLCM_SMS_GATEWAY_ROUTER_ID"} {
t.Setenv(name, "")
}
requests := make([]map[string]map[string]string, 0, 3)
fail := false
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
var body map[string]map[string]string
if err := json.NewDecoder(r.Body).Decode(&body); err != nil {
t.Errorf("decode request: %v", err)
}
requests = append(requests, body)
if fail {
_, _ = w.Write([]byte(`{"code":502,"message":"supplier failure"}`))
return
}
_, _ = w.Write([]byte(`{"code":0,"message":"OK"}`))
}))
defer server.Close()
adapter, err := gcfg.NewAdapterContent()
if err != nil {
t.Fatal(err)
}
original := g.Cfg().GetAdapter()
g.Cfg().SetAdapter(adapter)
t.Cleanup(func() { g.Cfg().SetAdapter(original) })
configure := func(enabled bool, template string) {
t.Helper()
data, err := json.Marshal(g.Map{
"sms": g.Map{"gateway": g.Map{"enabled": enabled, "url": server.URL, "timeout": "1s"}},
"warning": g.Map{"expire": g.Map{"monthly": g.Map{
"currentMonthParamsTemplate": template, "nextMonthParamsTemplate": template,
}}},
})
if err != nil {
t.Fatal(err)
}
if err := adapter.SetContent(string(data)); err != nil {
t.Fatal(err)
}
}
configure(true, `{"card_num":"{count}","expire_date":"{expireDate}"}`)
notice := monthlyExpiryNotice{
RunTime: time.Date(2026, 10, 15, 9, 0, 0, 0, monthlyExpiryLocation),
Leader: monthlyExpiryLeader{UserID: 12, Phone: "13800000000"},
Summary: monthlyExpirySummary{Month: "2026-10", Count: 7, FirstDate: "2026-10-31", LastDate: "2026-10-31"},
}
if err := sendMonthlyExpirySMS(context.Background(), notice); err != nil {
t.Fatal(err)
}
notice.Summary = monthlyExpirySummary{Month: "2026-11", Count: 9, FirstDate: "2026-11-01", LastDate: "2026-11-30"}
if err := sendMonthlyExpirySMS(context.Background(), notice); err != nil {
t.Fatal(err)
}
if len(requests) != 2 || requests[0]["config"]["templateId"] != "SMS_512596191" || requests[1]["config"]["templateId"] != "SMS_512646156" {
t.Fatalf("incorrect gateway templates: %+v", requests)
}
for _, request := range requests {
if request["config"]["signName"] != "示例企业" {
t.Fatalf("incorrect gateway sign: %+v", request)
}
}
var params map[string]string
if err := json.Unmarshal([]byte(requests[1]["params"]["msgContent"]), &params); err != nil || params["card_num"] != "9" || params["expire_date"] != "2026-11-01至2026-11-30" {
t.Fatalf("incorrect gateway params: %+v, %v", params, err)
}
fail = true
if err := sendMonthlyExpirySMS(context.Background(), notice); err == nil {
t.Fatal("gateway business failure must propagate")
}
configure(true, "")
if err := sendMonthlyExpirySMS(context.Background(), notice); err == nil || len(requests) != 3 {
t.Fatal("unconfirmed template must block before calling gateway")
}
configure(false, `{"card_num":"{count}","expire_date":"{expireDate}"}`)
if err := sendMonthlyExpirySMS(context.Background(), notice); err == nil || len(requests) != 3 {
t.Fatal("disabled gateway must not count as successful delivery")
}
}
func TestMonthlyExpiryCancellationStopsBeforeDelivery(t *testing.T) {
ctx, cancel := context.WithCancel(context.Background())
cancel()
runner := monthlyExpiryRunner{
companies: func(context.Context, int64) ([]monthlyExpiryCompany, error) {
t.Fatal("canceled run should not query")
return nil, nil
},
}
_, err := runner.run(ctx, time.Now())
if !errors.Is(err, context.Canceled) {
t.Fatalf("expected cancellation, got %v", err)
}
}
+28 -9
View File
@@ -4,6 +4,7 @@ import (
"context"
"errors"
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/dao"
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/logic/warningnotify"
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/model/entity"
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/service"
"strconv"
@@ -46,16 +47,17 @@ func (s *sSimWarning) scan(ctx context.Context, scope scanScope) {
scope = normalizeScanScope(scope)
runTime := time.Now()
stats := scanStats{}
pendingWarnings := make([]pendingWarning, 0)
collector := warningnotify.NewCollector()
g.Log().Infof(ctx, "SimWarning.Scan begin, runTime:%s, scope:%s, mode:warn_log_only", runTime.Format("2006-01-02 15:04:05"), scope)
var lastID int64
for page := 1; ; page++ {
if err := ctx.Err(); err != nil {
stats.ErrorCount++
g.Log().Warningf(ctx, "SimWarning.Scan canceled, page:%d, scope:%s, err:%+v", page, scope, err)
break
}
configs, err := s.queryConfigs(ctx, page, scope)
configs, err := s.queryConfigs(ctx, lastID, scope)
if err != nil {
stats.ErrorCount++
g.Log().Errorf(ctx, "SimWarning.Scan query configs failed, page:%d, err:%+v", page, err)
@@ -64,6 +66,7 @@ func (s *sSimWarning) scan(ctx context.Context, scope scanScope) {
if len(configs) == 0 {
break
}
lastID = configs[len(configs)-1].Id
stats.ConfigCount += len(configs)
cardMap, err := s.queryCards(ctx, configs)
@@ -78,19 +81,30 @@ func (s *sSimWarning) scan(ctx context.Context, scope scanScope) {
g.Log().Errorf(ctx, "SimWarning.Scan query flow pools failed, page:%d, err:%+v", page, err)
break
}
pendingWarnings := make([]pendingWarning, 0, len(configs))
for _, cfg := range configs {
if pending := s.scanConfig(ctx, cfg, cardMap[itemKey(cfg.TenantId, cfg.ItemId)], poolMap[itemKey(cfg.TenantId, cfg.ItemId)], runTime, &stats); pending != nil {
pendingWarnings = append(pendingWarnings, *pending)
}
}
stored := s.storePendingWarningsWithoutNotify(ctx, pendingWarnings, runTime, &stats)
events := make([]warningnotify.Event, 0, len(stored))
for _, item := range stored {
events = append(events, warningNotifyEvent(ctx, item, runTime))
s.scanAutoAction(ctx, item.Hit, item.Record.WarnLogID, runTime, &stats)
}
if err := collector.Add(ctx, events); err != nil {
stats.ErrorCount++
g.Log().Errorf(ctx, "warning recipient aggregation failed: %+v", err)
}
if len(configs) < scanPageSize {
break
}
}
storedWarnings := s.storePendingWarnings(ctx, pendingWarnings, runTime, &stats)
for _, stored := range storedWarnings {
s.scanAutoAction(ctx, stored.Hit, stored.Record.WarnLogID, runTime, &stats)
if err := collector.Flush(ctx); err != nil {
stats.ErrorCount++
g.Log().Errorf(ctx, "warning notification failed: %+v", err)
}
g.Log().Infof(
@@ -107,7 +121,7 @@ func (s *sSimWarning) scan(ctx context.Context, scope scanScope) {
)
}
func (s *sSimWarning) queryConfigs(ctx context.Context, page int, scope scanScope) ([]warnConfig, error) {
func (s *sSimWarning) queryConfigs(ctx context.Context, lastID int64, scope scanScope) ([]warnConfig, error) {
scope = normalizeScanScope(scope)
columns := dao.TWarnConfig.Columns()
mod := dao.TWarnConfig.Ctx(ctx).
@@ -162,8 +176,8 @@ func (s *sSimWarning) queryConfigs(ctx context.Context, page int, scope scanScop
}
entities := make([]entity.TWarnConfig, 0)
err := mod.OrderAsc(columns.Id).
Page(page, scanPageSize).
err := mod.WhereGT(columns.Id, lastID).OrderAsc(columns.Id).
Limit(scanPageSize).
Scan(&entities)
if err != nil {
return nil, err
@@ -279,6 +293,12 @@ func (s *sSimWarning) scanFlowPoolConfig(ctx context.Context, cfg warnConfig, po
}
func (s *sSimWarning) storePendingWarnings(ctx context.Context, pendingWarnings []pendingWarning, runTime time.Time, stats *scanStats) []storedWarning {
stored := s.storePendingWarningsWithoutNotify(ctx, pendingWarnings, runTime, stats)
notifyStoredWarnings(ctx, stored, runTime)
return stored
}
func (s *sSimWarning) storePendingWarningsWithoutNotify(ctx context.Context, pendingWarnings []pendingWarning, runTime time.Time, stats *scanStats) []storedWarning {
if len(pendingWarnings) == 0 {
return nil
}
@@ -298,7 +318,6 @@ func (s *sSimWarning) storePendingWarnings(ctx context.Context, pendingWarnings
for _, item := range storedWarnings {
logWarningHit(ctx, item.Hit, item.Record)
}
notifyStoredWarnings(ctx, storedWarnings, runTime)
return storedWarnings
}