diff --git a/conf/email-template/cla-updated.tmpl b/conf/email-template/cla-updated.tmpl index 5992c62..805224f 100644 --- a/conf/email-template/cla-updated.tmpl +++ b/conf/email-template/cla-updated.tmpl @@ -2,7 +2,7 @@ Dear {{.AdminName}}, A new version of CLA is published. Please login to the CLA management system to see the details. -The CLA management system login URL is {{.URLOfCLAPlatform}}. +The CLA management system login URL is {{.URLOfCLAPlatform}} Have questions or need help? Just reply to this email and the {{.Org}} Community Support Team will help you sort it out. diff --git a/models/individual_signing_test.go b/models/individual_signing_test.go index db763d2..e7b28b2 100644 --- a/models/individual_signing_test.go +++ b/models/individual_signing_test.go @@ -27,7 +27,6 @@ func TestIndividualSignedJSONSerialization(t *testing.T) { Type: "corporation", Signed: true, VersionMatched: true, - Status: "valid", }, want: map[string]interface{}{ "type": "corporation", @@ -42,7 +41,6 @@ func TestIndividualSignedJSONSerialization(t *testing.T) { Type: "corporation", Signed: true, VersionMatched: false, - Status: "expired", }, want: map[string]interface{}{ "type": "corporation", @@ -57,7 +55,6 @@ func TestIndividualSignedJSONSerialization(t *testing.T) { Type: "individual", Signed: false, VersionMatched: false, - Status: "not_signed", }, want: map[string]interface{}{ "type": "individual", @@ -106,7 +103,6 @@ func TestIndividualSignedDebugInfoOmittedWhenNil(t *testing.T) { Type: "individual", Signed: true, VersionMatched: true, - Status: "valid", } raw, err := json.Marshal(in) diff --git a/signing/app/cla.go b/signing/app/cla.go index 1e41b4a..8fb6420 100644 --- a/signing/app/cla.go +++ b/signing/app/cla.go @@ -62,11 +62,13 @@ func (s *claService) Update(cmd *CmdToUpdateCLA) error { return err } - if err = s.cs.SetPendingCLAForLink(cmd.LinkId, cla.Id); err != nil { - logs.Error("set pending CLA for link failed: %s, err: %v", cmd.LinkId, err) + if !dp.IsCLATypeIndividual(cla.Type) { + if err = s.cs.SetPendingCLAForLink(cmd.LinkId, cla.Id); err != nil { + logs.Error("set pending CLA for link failed: %s, err: %v", cmd.LinkId, err) + } } - watch.TriggerNotify() + watch.TriggerNotify(cla.Type) return nil } diff --git a/signing/app/individual_signing.go b/signing/app/individual_signing.go index a002d2a..02953da 100644 --- a/signing/app/individual_signing.go +++ b/signing/app/individual_signing.go @@ -86,16 +86,17 @@ func (s *individualSigningService) AgreeNewCLA(cmd *CmdToSignIndividualCLA) erro return err } - if !s.cla.ContainsCla(cmd.Link.Id, cmd.Link.CLAId) { - return domain.NewDomainError(domain.ErrorCodeCLANotExists) - } - sign, err := s.repo.Find(cmd.Link.Id, cmd.Rep.EmailAddr) if err != nil { return err } - if err = sign.AgreeNewCLA(cmd.Link.CLAId); err != nil { + latestClaId := s.cla.GetClaId(sign.Link.Id, dp.CLATypeIndividual, sign.Link.Language) + if latestClaId == "" { + return domain.NewDomainError(domain.ErrorCodeCLANotExists) + } + + if err = sign.AgreeNewCLA(latestClaId); err != nil { return err } diff --git a/signing/watch/config.go b/signing/watch/config.go index bfd8f7f..68d0792 100644 --- a/signing/watch/config.go +++ b/signing/watch/config.go @@ -54,8 +54,8 @@ type NotifyAdminConfig struct { SendEmailInterval int `json:"send_email_interval"` NotifyCorpAdminInterval int `json:"notify_corp_admin_interval"` NotifyIndividualInterval int `json:"notify_individual_interval"` - NotifyIndividualRemindDays int `json:"notify_individual_remind_days"` - NotifyCorpAdminRemindDays int `json:"notify_corp_admin_remind_days"` + NotifyIndividualRemindDays *int `json:"notify_individual_remind_days"` + NotifyCorpAdminRemindDays *int `json:"notify_corp_admin_remind_days"` NotifyBatchSize int `json:"notify_batch_size"` EnabledCommunityOrgs []string `json:"enabled_community_orgs"` NotifyEmailTo string `json:"notify_email_to"` @@ -74,12 +74,14 @@ func (cfg *NotifyAdminConfig) SetDefault() { cfg.NotifyIndividualInterval = 86400 } - if cfg.NotifyIndividualRemindDays <= 0 { - cfg.NotifyIndividualRemindDays = 90 + if cfg.NotifyIndividualRemindDays == nil { + v := 90 + cfg.NotifyIndividualRemindDays = &v } - if cfg.NotifyCorpAdminRemindDays <= 0 { - cfg.NotifyCorpAdminRemindDays = 90 + if cfg.NotifyCorpAdminRemindDays == nil { + v := 90 + cfg.NotifyCorpAdminRemindDays = &v } if cfg.NotifyBatchSize <= 0 { @@ -100,11 +102,11 @@ func (cfg *NotifyAdminConfig) genNotifyIndividualInterval() time.Duration { } func (cfg *NotifyAdminConfig) genNotifyIndividualRemindDays() int { - return cfg.NotifyIndividualRemindDays + return *cfg.NotifyIndividualRemindDays } func (cfg *NotifyAdminConfig) genNotifyCorpAdminRemindDays() int { - return cfg.NotifyCorpAdminRemindDays + return *cfg.NotifyCorpAdminRemindDays } func (cfg *NotifyAdminConfig) genNotifyBatchSize() int { diff --git a/signing/watch/config_test.go b/signing/watch/config_test.go index ce48cd5..285114f 100644 --- a/signing/watch/config_test.go +++ b/signing/watch/config_test.go @@ -9,6 +9,34 @@ import ( "github.com/opensourceways/app-cla-server/signing/domain/repository" ) +func TestGetEffectiveGracePeriodDays(t *testing.T) { + impl := ¬ifyAdminWatchImpl{defaultGracePeriodDays: 30} + intPtr := func(v int) *int { return &v } + + tests := []struct { + name string + linkDays *int + defaultDay int + want int + }{ + {"link nil (字段缺失) uses default", nil, 30, 30}, + {"link override positive", intPtr(15), 30, 15}, + {"link zero returns zero", intPtr(0), 30, 0}, + {"link negative uses default", intPtr(-1), 30, 30}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + link := &repository.LinkCLA{GracePeriodDays: tt.linkDays} + impl.defaultGracePeriodDays = tt.defaultDay + got := impl.getEffectiveGracePeriodDays(link) + if got != tt.want { + t.Errorf("got %d, want %d", got, tt.want) + } + }) + } +} + func TestIsCorpSigningLatest(t *testing.T) { impl := ¬ifyAdminWatchImpl{} @@ -28,68 +56,47 @@ func TestIsCorpSigningLatest(t *testing.T) { } } -func TestGetLatestIndividualCLA(t *testing.T) { +func TestGetLatestCorpClaId(t *testing.T) { impl := ¬ifyAdminWatchImpl{} - clas := []domain.CLA{ - {Id: "10", Type: dp.CLATypeCorp, Language: dp.CreateLanguage("en")}, - {Id: "11", Type: dp.CLATypeIndividual, Language: dp.CreateLanguage("en")}, - {Id: "12", Type: dp.CLATypeIndividual, Language: dp.CreateLanguage("zh")}, + link := &repository.LinkCLA{ + Clas: []domain.CLA{ + {Id: "10", Type: dp.CLATypeCorp, Language: dp.CreateLanguage("en")}, + {Id: "11", Type: dp.CLATypeIndividual, Language: dp.CreateLanguage("en")}, + {Id: "12", Type: dp.CLATypeCorp, Language: dp.CreateLanguage("zh")}, + }, } - if cla := impl.getLatestIndividualCLA(clas, dp.CreateLanguage("en")); cla == nil || cla.Id != "11" { - t.Errorf("getLatestIndividualCLA en: got %v, want id=11", cla) + if id := impl.getLatestCorpClaId(link, dp.CreateLanguage("en")); id != "10" { + t.Errorf("getLatestCorpClaId en: got %s, want 10", id) } - if cla := impl.getLatestIndividualCLA(clas, dp.CreateLanguage("zh")); cla == nil || cla.Id != "12" { - t.Errorf("getLatestIndividualCLA zh: got %v, want id=12", cla) + if id := impl.getLatestCorpClaId(link, dp.CreateLanguage("zh")); id != "12" { + t.Errorf("getLatestCorpClaId zh: got %s, want 12", id) } - if cla := impl.getLatestIndividualCLA(clas, dp.CreateLanguage("fr")); cla != nil { - t.Errorf("getLatestIndividualCLA fr: got %v, want nil", cla) + if id := impl.getLatestCorpClaId(link, dp.CreateLanguage("fr")); id != "" { + t.Errorf("getLatestCorpClaId fr: got %s, want empty", id) } } -func TestIsIndividualSigningLatest(t *testing.T) { +func TestGetLatestIndividualClaId(t *testing.T) { impl := ¬ifyAdminWatchImpl{} - clas := []domain.CLA{ - {Id: "10", Type: dp.CLATypeIndividual, Language: dp.CreateLanguage("en")}, - {Id: "11", Type: dp.CLATypeCorp, Language: dp.CreateLanguage("en")}, + link := &repository.LinkCLA{ + Clas: []domain.CLA{ + {Id: "10", Type: dp.CLATypeCorp, Language: dp.CreateLanguage("en")}, + {Id: "11", Type: dp.CLATypeIndividual, Language: dp.CreateLanguage("en")}, + {Id: "12", Type: dp.CLATypeIndividual, Language: dp.CreateLanguage("zh")}, + }, } - signedInfo := domain.CLAInfo{CLAId: "10", Language: dp.CreateLanguage("en")} - if !impl.isIndividualSigningLatest(clas, signedInfo) { - t.Error("isIndividualSigningLatest should return true for matching CLA") - } - - signedInfo2 := domain.CLAInfo{CLAId: "9", Language: dp.CreateLanguage("en")} - if impl.isIndividualSigningLatest(clas, signedInfo2) { - t.Error("isIndividualSigningLatest should return false for outdated CLA") + if id := impl.getLatestIndividualClaId(link, dp.CreateLanguage("en")); id != "11" { + t.Errorf("getLatestIndividualClaId en: got %s, want 11", id) } -} - -func TestGetEffectiveGracePeriodDays(t *testing.T) { - intPtr := func(v int) *int { return &v } - - tests := []struct { - name string - linkDays *int - defaultDay int - want int - }{ - {"link nil uses default", nil, 30, 30}, - {"link override positive", intPtr(15), 30, 15}, - {"link zero returns zero", intPtr(0), 30, 0}, - {"link negative uses default", intPtr(-1), 30, 30}, + if id := impl.getLatestIndividualClaId(link, dp.CreateLanguage("zh")); id != "12" { + t.Errorf("getLatestIndividualClaId zh: got %s, want 12", id) } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - link := &repository.LinkCLA{GracePeriodDays: tt.linkDays} - got := link.GetEffectiveGracePeriodDays(tt.defaultDay) - if got != tt.want { - t.Errorf("got %d, want %d", got, tt.want) - } - }) + if id := impl.getLatestIndividualClaId(link, dp.CreateLanguage("fr")); id != "" { + t.Errorf("getLatestIndividualClaId fr: got %s, want empty", id) } } @@ -100,24 +107,13 @@ func TestNotifyAdminConfigSetDefault(t *testing.T) { if cfg.SendEmailInterval != 10 { t.Errorf("SendEmailInterval: got %d, want 10", cfg.SendEmailInterval) } - if cfg.NotifyCorpAdminInterval != 1200 { - t.Errorf("NotifyCorpAdminInterval: got %d, want 1200", cfg.NotifyCorpAdminInterval) - } - if cfg.NotifyIndividualInterval != 86400 { - t.Errorf("NotifyIndividualInterval: got %d, want 86400", cfg.NotifyIndividualInterval) - } - if cfg.NotifyIndividualRemindDays != 90 { - t.Errorf("NotifyIndividualRemindDays: got %d, want 90", cfg.NotifyIndividualRemindDays) + if cfg.NotifyCorpAdminInterval != 86400 { + t.Errorf("NotifyCorpAdminInterval: got %d, want 86400", cfg.NotifyCorpAdminInterval) } } func TestNotifyAdminConfigSetDefaultPreserves(t *testing.T) { - cfg := NotifyAdminConfig{ - SendEmailInterval: 20, - NotifyCorpAdminInterval: 600, - NotifyIndividualInterval: 43200, - NotifyIndividualRemindDays: 45, - } + cfg := NotifyAdminConfig{SendEmailInterval: 20, NotifyCorpAdminInterval: 600} cfg.SetDefault() if cfg.SendEmailInterval != 20 { @@ -126,12 +122,6 @@ func TestNotifyAdminConfigSetDefaultPreserves(t *testing.T) { if cfg.NotifyCorpAdminInterval != 600 { t.Errorf("NotifyCorpAdminInterval should be preserved: got %d, want 600", cfg.NotifyCorpAdminInterval) } - if cfg.NotifyIndividualInterval != 43200 { - t.Errorf("NotifyIndividualInterval should be preserved: got %d, want 43200", cfg.NotifyIndividualInterval) - } - if cfg.NotifyIndividualRemindDays != 45 { - t.Errorf("NotifyIndividualRemindDays should be preserved: got %d, want 45", cfg.NotifyIndividualRemindDays) - } } func TestNotifyAdminConfigGenSendEmailInterval(t *testing.T) { @@ -150,22 +140,6 @@ func TestNotifyAdminConfigGenNotifyCorpAdminInterval(t *testing.T) { } } -func TestNotifyAdminConfigGenNotifyIndividualInterval(t *testing.T) { - cfg := NotifyAdminConfig{NotifyIndividualInterval: 200} - d := cfg.genNotifyIndividualInterval() - if d != 200*time.Second { - t.Errorf("genNotifyIndividualInterval: got %v, want 200s", d) - } -} - -func TestNotifyAdminConfigGenNotifyIndividualRemindDays(t *testing.T) { - cfg := NotifyAdminConfig{NotifyIndividualRemindDays: 7} - d := cfg.genNotifyIndividualRemindDays() - if d != 7 { - t.Errorf("genNotifyIndividualRemindDays: got %d, want 7", d) - } -} - func TestCLAUpdateConfigSetDefault(t *testing.T) { cfg := CLAUpdateConfig{} cfg.SetDefault() @@ -221,3 +195,19 @@ func TestWatchConfigSetDefaultPreserves(t *testing.T) { t.Errorf("Interval preserved: got %d, want 7200", cfg.Interval) } } + +func TestWatchConfigConfigItems(t *testing.T) { + cfg := Config{} + items := cfg.ConfigItems() + if len(items) != 2 { + t.Errorf("ConfigItems length: got %d, want 2", len(items)) + } +} + +func TestWatchConfigIntervalDuration(t *testing.T) { + cfg := Config{Interval: 60} + d := cfg.intervalDuration() + if d != 60*time.Second { + t.Errorf("intervalDuration: got %v, want 60s", d) + } +} diff --git a/signing/watch/notify_corp_admin.go b/signing/watch/notify_corp_admin.go index 76f54f5..500dcb0 100644 --- a/signing/watch/notify_corp_admin.go +++ b/signing/watch/notify_corp_admin.go @@ -30,6 +30,14 @@ func NotifyAdminWatchStart(cfg *NotifyAdminConfig, lk repoLink, corp corpSigning individualTrigger: make(chan struct{}, 1), } + logs.Info("notify admin watch config: interval_corp=%ds interval_ind=%ds remind_corp=%dd remind_ind=%dd batch=%d communities=%v", + int(cfg.genNotifyCorpAdminInterval().Seconds()), + int(cfg.genNotifyIndividualInterval().Seconds()), + cfg.genNotifyCorpAdminRemindDays(), + cfg.genNotifyIndividualRemindDays(), + cfg.genNotifyBatchSize(), + cfg.EnabledCommunityOrgs) + notifyAdminWatchInstance.start() } @@ -42,18 +50,22 @@ func NotifyAdminWatchStop() { } // TriggerNotify 立即触发一次通知扫描,不等待定时器周期。 -// CLA 更新后应调用此函数,确保企业和个人都在最短时间内收到通知。 -func TriggerNotify() { +// t 指定要触发的 CLA 类型,只触发对应类型的扫描器。 +func TriggerNotify(t dp.CLAType) { if notifyAdminWatchInstance == nil { return } - select { - case notifyAdminWatchInstance.corpTrigger <- struct{}{}: - default: + if !dp.IsCLATypeIndividual(t) { + select { + case notifyAdminWatchInstance.corpTrigger <- struct{}{}: + default: + } } - select { - case notifyAdminWatchInstance.individualTrigger <- struct{}{}: - default: + if dp.IsCLATypeIndividual(t) { + select { + case notifyAdminWatchInstance.individualTrigger <- struct{}{}: + default: + } } } @@ -135,6 +147,8 @@ func (impl *notifyAdminWatchImpl) handleNotifyJob() { return } + logs.Debug("corp notify job start, links=%d, batch=%d", len(links), batchSize) + for i := range links { if needStop() { return @@ -152,6 +166,8 @@ func (impl *notifyAdminWatchImpl) handleNotifyJob() { continue } + logs.Debug("corp notify: link=%s org=%s, signings=%d", link.Id, link.Org.Alias, len(corpsSummary)) + for j := range corpsSummary { if needStop() { return @@ -166,6 +182,8 @@ func (impl *notifyAdminWatchImpl) handleNotifyJob() { } } } + + logs.Debug("corp notify job done, sent=%d", sendCount) } func (impl *notifyAdminWatchImpl) handleCorpSigning(link *repository.LinkCLA, corp *repository.CorpSigningSummary) bool { @@ -175,6 +193,7 @@ func (impl *notifyAdminWatchImpl) handleCorpSigning(link *repository.LinkCLA, co latestCLA := impl.getLatestCorpCLA(link.Clas, corp.Link.Language) if latestCLA == nil { + logs.Debug("corp notify skip: no matching CLA, link=%s corp=%s lang=%s", link.Id, corp.Id, corp.Link.Language.Language()) return false } @@ -185,6 +204,7 @@ func (impl *notifyAdminWatchImpl) handleCorpSigning(link *repository.LinkCLA, co corp.CLANotify = latestCLA.Id corp.ClaNotifyCount = 0 corp.ClaNotifyTime = 0 + logs.Debug("corp notify reset: corp=%s old_notify=%s new_notify=%s", corp.Id, corp.CLANotify, latestCLA.Id) } daysSinceLastNotify := 0 @@ -193,6 +213,7 @@ func (impl *notifyAdminWatchImpl) handleCorpSigning(link *repository.LinkCLA, co } if daysSinceLastNotify < remindDays && corp.ClaNotifyCount > 0 { + logs.Debug("corp notify skip: in cool-down, corp=%s count=%d days=%d remind=%d", corp.Id, corp.ClaNotifyCount, daysSinceLastNotify, remindDays) return false } @@ -293,6 +314,8 @@ func (impl *notifyAdminWatchImpl) handleIndividualNotifyJob() { return } + logs.Debug("individual notify job start, links=%d, batch=%d", len(links), batchSize) + for i := range links { if needStop() { return @@ -310,6 +333,8 @@ func (impl *notifyAdminWatchImpl) handleIndividualNotifyJob() { continue } + logs.Debug("individual notify: link=%s org=%s, signings=%d", link.Id, link.Org.Alias, len(individuals)) + for j := range individuals { if needStop() { return @@ -324,20 +349,24 @@ func (impl *notifyAdminWatchImpl) handleIndividualNotifyJob() { } } } + + logs.Debug("individual notify job done, sent=%d", sendCount) } func (impl *notifyAdminWatchImpl) handleIndividualSigning(link *repository.LinkCLA, is *domain.IndividualSigning) bool { - // 检查是否签署了最新版本 if impl.isIndividualSigningLatest(link.Clas, is.Link.CLAInfo) { return false } - // 获取当前 CLA 的更新时间 latestCLA := impl.getLatestIndividualCLA(link.Clas, is.Link.Language) if latestCLA == nil { + logs.Debug("individual notify skip: no matching CLA, link=%s email=%s lang=%s", link.Id, is.Rep.EmailAddr.EmailAddr(), is.Link.Language.Language()) return false } + logs.Info("individual notify candidate: email=%s signed_cla=%s latest_cla=%s cla_notify=%s count=%d time=%d", + is.Rep.EmailAddr.EmailAddr(), is.Link.CLAId, latestCLA.Id, is.ClaNotify, is.ClaNotifyCount, is.ClaNotifyTime) + remindDays := impl.config.genNotifyIndividualRemindDays() nowUnix := time.Now().Unix() @@ -345,6 +374,7 @@ func (impl *notifyAdminWatchImpl) handleIndividualSigning(link *repository.LinkC is.ClaNotify = latestCLA.Id is.ClaNotifyCount = 0 is.ClaNotifyTime = 0 + logs.Debug("individual notify reset: email=%s old_notify=%s new_notify=%s", is.Rep.EmailAddr.EmailAddr(), is.ClaNotify, latestCLA.Id) } daysSinceLastNotify := 0 @@ -354,6 +384,7 @@ func (impl *notifyAdminWatchImpl) handleIndividualSigning(link *repository.LinkC // 如果距上次通知不足 remindDays 天,跳过 if daysSinceLastNotify < remindDays && is.ClaNotifyCount > 0 { + logs.Debug("individual notify skip: in cool-down, email=%s count=%d days=%d remind=%d", is.Rep.EmailAddr.EmailAddr(), is.ClaNotifyCount, daysSinceLastNotify, remindDays) return false } @@ -368,6 +399,8 @@ func (impl *notifyAdminWatchImpl) handleIndividualSigning(link *repository.LinkC logs.Error("update individual cla notify failed: ", is.Rep.EmailAddr.EmailAddr(), err) } + logs.Info("individual notify sent: email=%s link=%s count=%d", is.Rep.EmailAddr.EmailAddr(), link.Id, is.ClaNotifyCount) + return true } @@ -487,3 +520,23 @@ func (impl *notifyAdminWatchImpl) nextDelay(interval time.Duration) time.Duratio } return interval } + +func (impl *notifyAdminWatchImpl) getEffectiveGracePeriodDays(link *repository.LinkCLA) int { + return link.GetEffectiveGracePeriodDays(impl.defaultGracePeriodDays) +} + +func (impl *notifyAdminWatchImpl) getLatestCorpClaId(link *repository.LinkCLA, language dp.Language) string { + cla := impl.getLatestCorpCLA(link.Clas, language) + if cla == nil { + return "" + } + return cla.Id +} + +func (impl *notifyAdminWatchImpl) getLatestIndividualClaId(link *repository.LinkCLA, language dp.Language) string { + cla := impl.getLatestIndividualCLA(link.Clas, language) + if cla == nil { + return "" + } + return cla.Id +} diff --git a/signing/watch/notify_corp_admin_test.go b/signing/watch/notify_corp_admin_test.go index 27e9476..0a6fb27 100644 --- a/signing/watch/notify_corp_admin_test.go +++ b/signing/watch/notify_corp_admin_test.go @@ -9,10 +9,11 @@ import ( ) func newTestImpl() *notifyAdminWatchImpl { + remindDays := 90 return ¬ifyAdminWatchImpl{ config: &NotifyAdminConfig{ - NotifyCorpAdminRemindDays: 90, - NotifyIndividualRemindDays: 90, + NotifyCorpAdminRemindDays: &remindDays, + NotifyIndividualRemindDays: &remindDays, NotifyBatchSize: 500, }, defaultGracePeriodDays: 30,