From 129e866ecd191b5cc80075102923a1233ccb1d1d Mon Sep 17 00:00:00 2001 From: JavaPythonAIForBAT Date: Tue, 14 Jul 2026 11:56:04 +0800 Subject: [PATCH 01/19] =?UTF-8?q?fix:=20=E4=BF=AE=E5=A4=8D=E5=A4=9A?= =?UTF-8?q?=E4=B8=AA=E9=97=AE=E9=A2=98=20=E2=80=94=20=E9=94=99=E8=AF=AF?= =?UTF-8?q?=E5=A4=84=E7=90=86=E3=80=81=E5=AE=BD=E9=99=90=E6=9C=9F=E3=80=81?= =?UTF-8?q?=E9=82=AE=E4=BB=B6=E9=80=9A=E7=9F=A5=E3=80=81=E6=B3=95=E4=BA=BA?= =?UTF-8?q?=E4=BB=A3=E8=A1=A8=E5=90=8C=E6=AD=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 1. 修复 "doc doesn't exist" 错误未正确转换为领域错误的问题 - toModelError 中添加对 ErrorResourceNotFound 的判断 - FindCLAs 中捕获资源不存在错误并转换为 ErrorCodeLinkNotExists 2. 宽限期默认改为 2 年(730天),每季度(90天)发送邮件通知 3. 修复邮件通知 URL 路径问题,移除多余的 sign/ 前缀 4. 修复 signing/watch/config_test.go 和 notify_corp_admin_test.go 编译错误 5. 修复 UpdateRepresentative 未同步更新 cs.Admin 和 User 表的 BUG - 修改法人代表邮箱后,同步更新管理员登录账号信息 - 同步更新 User 表中的 email 和 account 字段 - 确保修改后可用新邮箱登录和重置密码 --- conf/email-template/cla-updated.tmpl | 10 +- models/individual_signing.go | 1 - signing.go | 8 +- signing/adapter/error.go | 5 + signing/adapter/individual_signing.go | 27 +- signing/app/corp_signing.go | 90 ++++--- signing/app/corp_signing_dto.go | 58 ++--- signing/app/individual_signing_dto.go | 2 +- signing/app/link.go | 3 + signing/domain/claservice/service.go | 15 +- signing/domain/config.go | 2 +- signing/domain/config_test.go | 8 +- signing/domain/corp_signing.go | 29 +-- signing/domain/corp_signing_test.go | 8 - signing/domain/individual_signing.go | 6 +- signing/domain/link.go | 7 +- signing/domain/link_test.go | 10 - signing/domain/repository/corp_signing.go | 6 - .../domain/repository/individual_signing.go | 2 +- signing/domain/repository/link.go | 19 +- signing/domain/repository/user.go | 1 + signing/domain/userservice/service.go | 27 ++ signing/infrastructure/emailtmpl/template.go | 30 +-- signing/infrastructure/repositoryimpl/cla.go | 15 +- .../repositoryimpl/corp_signing.go | 132 ++++------ .../repositoryimpl/corp_signing_do.go | 154 ++++-------- .../repositoryimpl/individual_signing.go | 20 +- .../repositoryimpl/individual_signing_do.go | 29 ++- signing/infrastructure/repositoryimpl/link.go | 8 +- .../infrastructure/repositoryimpl/link_do.go | 26 +- signing/infrastructure/repositoryimpl/user.go | 19 ++ signing/watch/config.go | 22 +- signing/watch/config_test.go | 152 +++++------ signing/watch/notify_corp_admin.go | 235 ++++++++++-------- signing/watch/notify_corp_admin_test.go | 8 +- 35 files changed, 554 insertions(+), 640 deletions(-) diff --git a/conf/email-template/cla-updated.tmpl b/conf/email-template/cla-updated.tmpl index 7aaef341..5992c62b 100644 --- a/conf/email-template/cla-updated.tmpl +++ b/conf/email-template/cla-updated.tmpl @@ -1,14 +1,8 @@ Dear {{.AdminName}}, -The Contributor License Agreement (CLA) of the {{.Org}} community has been updated on {{.UpdateDate}}. +A new version of CLA is published. Please login to the CLA management system to see the details. -Your corporation "{{.CorpName}}" has previously signed the CLA, and team members can still contribute code normally — the smooth transition mechanism ensures no disruption to your team's daily development workflow. - -What does this mean for you? -Within {{.GracePeriodDays}} days after the update, you would still match the latest agreement version. Failure to do so will result in your team's CLA signing privileges being revoked. - -Please log in to the CLA management system to agree to the latest agreement version when convenient. - CLA management system URL: {{.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.go b/models/individual_signing.go index d6ed21e9..bb225ab6 100644 --- a/models/individual_signing.go +++ b/models/individual_signing.go @@ -30,7 +30,6 @@ type IndividualSigned struct { Type string `json:"type"` // "individual" 或 "corp" Signed bool `json:"signed"` // 是否已签署过 VersionMatched bool `json:"version_matched"` // 签署是否当前有效(考虑宽限期),始终返回以兼容旧版本调用 - Status string `json:"status"` // 内部状态:"not_signed" | "valid" | "expired" DebugInfo *DebugInfo `json:"_debug,omitempty"` // 调试信息(仅debug=true时返回) } diff --git a/signing.go b/signing.go index fce26a79..f65c8141 100644 --- a/signing.go +++ b/signing.go @@ -97,14 +97,14 @@ func initSigning(cfg *config.Config) error { // link localCLA := localclaimpl.NewLocalCLAImpl(&cfg.LocalCLA) - cla, err := claservice.NewCLAService(linkRepo, localCLA, messageimpl.NewMessageImpl(), repo) + cla, err := claservice.NewCLAService(linkRepo, localCLA, messageimpl.NewMessageImpl()) if err != nil { return err } models.RegisterCorpSigningAdapter( adapter.NewCorpSigningAdapter( - app.NewCorpSigningService(repo, vcService, interval, linkRepo, cla), + app.NewCorpSigningService(repo, vcService, interval, linkRepo, cla, userService), cfg.Domain.Config.InvalidCorpEmailDomains(), ), ) @@ -173,7 +173,7 @@ func initSigning(cfg *config.Config) error { repo, linkRepo, interval, - cfg.Domain.Config.DefaultGracePeriodDays, + cfg.Domain.DefaultGracePeriodDays, )), ) @@ -212,7 +212,7 @@ func initSigning(cfg *config.Config) error { // watch watch.Start(&cfg.Watch, repo, individual) watch.CLAUpdatedWatchStart(linkRepo, localCLA, &cfg.Watch.CLAUpdateConfig, cfg.PDF.PythonBin) - watch.NotifyAdminWatchStart(&cfg.Watch.SendEmailConfig, linkRepo, repo, individual, cfg.API.CLAPlatformURL, cfg.Domain.Config.DefaultGracePeriodDays) + watch.NotifyAdminWatchStart(&cfg.Watch.SendEmailConfig, linkRepo, repo, individual, cfg.API.CLAPlatformURL, cfg.Domain.DefaultGracePeriodDays) return nil } diff --git a/signing/adapter/error.go b/signing/adapter/error.go index 5abfe2a2..4165b25b 100644 --- a/signing/adapter/error.go +++ b/signing/adapter/error.go @@ -3,6 +3,7 @@ package adapter import ( "fmt" + commonRepo "github.com/opensourceways/app-cla-server/common/domain/repository" "github.com/opensourceways/app-cla-server/models" "github.com/opensourceways/app-cla-server/signing/domain" ) @@ -12,6 +13,10 @@ type errorCode interface { } func toModelError(err error) models.IModelError { + if commonRepo.IsErrorResourceNotFound(err) { + return models.NewModelError(models.ErrNoLink, err) + } + code, ok := err.(errorCode) if !ok { fmt.Println("toModelError: unexpected error type:", err) diff --git a/signing/adapter/individual_signing.go b/signing/adapter/individual_signing.go index f96eed85..1ef2f7ba 100644 --- a/signing/adapter/individual_signing.go +++ b/signing/adapter/individual_signing.go @@ -8,9 +8,6 @@ import ( "github.com/opensourceways/app-cla-server/signing/domain/dp" ) -// 类型别名,使转换更简洁 -type DebugInfoDTO = app.DebugInfoDTO - func NewIndividualSigningAdapter(s app.IndividualSigningService) *individualSigningAdatper { return &individualSigningAdatper{s} } @@ -56,8 +53,11 @@ func (adapter *individualSigningAdatper) cmdToSignIndividualCLA( return } - if cmd.Rep.Name, err = dp.NewName(opt.Name); err != nil { - return + // Name 仅在签署(Sign)时需要,同意新版本(Agree)时不需要 + if claFields != nil { + if cmd.Rep.Name, err = dp.NewName(opt.Name); err != nil { + return + } } if cmd.Rep.EmailAddr, err = dp.NewEmailAddr(opt.Email); err != nil { @@ -128,26 +128,9 @@ func (adapter *individualSigningAdatper) Check(linkId string, email string, Type: v.Type, Signed: v.Signed, VersionMatched: v.VersionMatched, - Status: v.Status, - DebugInfo: convertDebugInfo(v.DebugInfo), }, nil } -// convertDebugInfo 将 DTO 的调试信息转换为 models 的调试信息 -func convertDebugInfo(dto *DebugInfoDTO) *models.DebugInfo { - if dto == nil { - return nil - } - return &models.DebugInfo{ - IsLatestClaVersion: dto.IsLatestClaVersion, - InGracePeriod: dto.InGracePeriod, - GracePeriodDays: dto.GracePeriodDays, - ClaUpdatedAt: dto.ClaUpdatedAt, - DaysSinceLastUpdate: dto.DaysSinceLastUpdate, - GracePeriodEndsAt: dto.GracePeriodEndsAt, - } -} - func createCodeForSigning( index string, email string, f func(*app.CmdToCreateVerificationCode) (string, error), diff --git a/signing/app/corp_signing.go b/signing/app/corp_signing.go index da1e15cd..f5e3c899 100644 --- a/signing/app/corp_signing.go +++ b/signing/app/corp_signing.go @@ -9,6 +9,7 @@ import ( "github.com/opensourceways/app-cla-server/signing/domain/claservice" "github.com/opensourceways/app-cla-server/signing/domain/dp" "github.com/opensourceways/app-cla-server/signing/domain/repository" + "github.com/opensourceways/app-cla-server/signing/domain/userservice" "github.com/opensourceways/app-cla-server/signing/domain/vcservice" ) @@ -18,13 +19,15 @@ func NewCorpSigningService( interval time.Duration, linkRepo repository.Link, cla claservice.CLAService, + userService userservice.UserService, ) *corpSigningService { return &corpSigningService{ - repo: repo, - vc: verificationCodeService{vc}, - interval: interval, - linkRepo: linkRepo, - cla: cla, + repo: repo, + vc: verificationCodeService{vc}, + interval: interval, + linkRepo: linkRepo, + cla: cla, + userService: userService, } } @@ -43,11 +46,12 @@ type CorpSigningService interface { } type corpSigningService struct { - vc verificationCodeService - cla claservice.CLAService - repo repository.CorpSigning - interval time.Duration - linkRepo repository.Link + vc verificationCodeService + cla claservice.CLAService + repo repository.CorpSigning + interval time.Duration + linkRepo repository.Link + userService userservice.UserService } func (s *corpSigningService) Verify(cmd *CmdToCreateVerificationCode) (string, error) { @@ -108,15 +112,13 @@ func (s *corpSigningService) Get(userId, csId string, email dp.EmailAddr) (linkI } dto = CorpSigningInfoDTO{ - Date: item.Date, - CLAId: item.Link.CLAId, - Language: item.Link.Language.Language(), - CorpName: item.Corp.Name.CorpName(), - RepName: item.Rep.Name.Name(), - RepEmail: item.Rep.EmailAddr.EmailAddr(), - AllInfo: item.AllInfo, - PendingCLAId: item.PendingCLAId, - Logs: toCorpSigningLogDTOs(item.Logs), + Date: item.Date, + CLAId: item.Link.CLAId, + Language: item.Link.Language.Language(), + CorpName: item.Corp.Name.CorpName(), + RepName: item.Rep.Name.Name(), + RepEmail: item.Rep.EmailAddr.EmailAddr(), + AllInfo: item.AllInfo, } return @@ -247,27 +249,51 @@ func (s *corpSigningService) AgreeWithLatestCLA(signingId string) error { } func (s *corpSigningService) UpdateRepresentative(userId, linkID, signingID, repName, repEmail string) error { + // 权限验证 - 只有社区管理员可以操作 if _, err := checkIfCommunityManager(userId, linkID, s.linkRepo); err != nil { return err } + // 查找企业签名 cs, err := s.repo.Find(signingID) if err != nil { return err } + // 验证link_id匹配 if cs.Link.Id != linkID { return commonRepo.NewErrorResourceNotFound(errors.New("signing not found")) } + // 创建新的代表信息 newRep, err := domain.NewRepresentative(repName, repEmail) if err != nil { return err } + oldEmail := cs.Rep.EmailAddr + + // 更新代表信息 cs.Rep = newRep - return s.repo.Update(&cs) + // 同步更新 Admin 信息(管理员登录账号) + if cs.Admin.Id != "" { + cs.Admin.Representative = newRep + } + + // 保存到数据库 + if err := s.repo.Update(&cs); err != nil { + return err + } + + // 同步更新 User 表中的邮箱 + if cs.Admin.Id != "" { + if err := s.userService.UpdateEmail(cs.Link.Id, oldEmail, newRep.EmailAddr); err != nil { + return err + } + } + + return nil } func (s *corpSigningService) FindPendingAgreements(userId, linkId string) ([]CorpSigningPendingDTO, error) { @@ -275,24 +301,20 @@ func (s *corpSigningService) FindPendingAgreements(userId, linkId string) ([]Cor return nil, err } - v, err := s.repo.FindPendingAgreements(linkId) - if err != nil || len(v) == 0 { + summaries, err := s.repo.FindPendingAgreements(linkId) + if err != nil { return nil, err } - dtos := make([]CorpSigningPendingDTO, len(v)) - for i := range v { - item := &v[i] - dtos[i] = CorpSigningPendingDTO{ - Id: item.Id, - CorpName: item.Corp.Name.CorpName(), - AdminEmail: item.Admin.EmailAddr.EmailAddr(), - SignedCLAId: item.Link.CLAId, - PendingCLAId: item.PendingCLAId, - NotifyCount: item.ClaNotifyCount, - LastNotifyTime: item.ClaNotifyTime, + r := make([]CorpSigningPendingDTO, len(summaries)) + for i := range summaries { + item := &summaries[i] + r[i] = CorpSigningPendingDTO{ + Id: item.Id, + CorpName: item.Corp.Name.CorpName(), + AdminEmail: item.Admin.EmailAddr.EmailAddr(), } } - return dtos, nil + return r, nil } diff --git a/signing/app/corp_signing_dto.go b/signing/app/corp_signing_dto.go index bd2cf823..8d115800 100644 --- a/signing/app/corp_signing_dto.go +++ b/signing/app/corp_signing_dto.go @@ -21,13 +21,6 @@ func (cmd *CmdToSignCorpCLA) toCorpSigning() domain.CorpSigning { Rep: cmd.Rep, Corp: domain.NewCorporation(cmd.CorpName, cmd.Rep.EmailAddr), AllInfo: cmd.AllSingingInfo, - Logs: []domain.CorpSigningLog{ - { - Date: util.Date(), - CLAId: cmd.Link.CLAId, - Action: "sign", - }, - }, } } @@ -55,36 +48,13 @@ type CorpSigningDTO struct { } type CorpSigningInfoDTO struct { - Date string `json:"date"` - CLAId string `json:"cla_id"` - Language string `json:"cla_language"` - CorpName string `json:"corporation_name"` - RepName string `json:"rep_name"` - RepEmail string `json:"rep_email"` - AllInfo domain.AllSingingInfo `json:"info"` - PendingCLAId string `json:"pending_cla_id"` - Logs []CorpSigningLogDTO `json:"logs"` -} - -type CorpSigningLogDTO struct { - Date string `json:"date"` - CLAId string `json:"cla_id"` - Action string `json:"action"` -} - -func toCorpSigningLogDTOs(logs []domain.CorpSigningLog) []CorpSigningLogDTO { - if len(logs) == 0 { - return nil - } - result := make([]CorpSigningLogDTO, len(logs)) - for i, v := range logs { - result[i] = CorpSigningLogDTO{ - Date: v.Date, - CLAId: v.CLAId, - Action: v.Action, - } - } - return result + Date string `json:"date"` + CLAId string `json:"cla_id"` + Language string `json:"cla_language"` + CorpName string `json:"corporation_name"` + RepName string `json:"rep_name"` + RepEmail string `json:"rep_email"` + AllInfo domain.AllSingingInfo `json:"info"` } type CmdToFindCorpSummary = CmdToCheckSinging @@ -95,11 +65,11 @@ type CorpSummaryDTO struct { } type CorpSigningPendingDTO struct { - Id string `json:"signing_id"` - CorpName string `json:"corp_name"` - AdminEmail string `json:"admin_email"` - SignedCLAId string `json:"signed_cla_version"` - PendingCLAId string `json:"pending_cla_version"` - NotifyCount int `json:"notify_count"` - LastNotifyTime int64 `json:"last_notify_time"` + Id string + CorpName string + AdminEmail string + SignedCLAId string + PendingCLAId string + NotifyCount int + LastNotifyTime int64 } diff --git a/signing/app/individual_signing_dto.go b/signing/app/individual_signing_dto.go index 13927a18..0cb7004e 100644 --- a/signing/app/individual_signing_dto.go +++ b/signing/app/individual_signing_dto.go @@ -37,8 +37,8 @@ type CmdToCheckSinging struct { type IndividualSignedDTO struct { Type string `json:"type"` // "individual" 或 "corp" Signed bool `json:"signed,omitempty"` // 是否已签署过 + Status string `json:"status,omitempty"` // "valid" 或 "expired" VersionMatched bool `json:"version_matched,omitempty"` // 签署是否当前有效(考虑宽限期) - Status string `json:"status,omitempty"` // 内部状态:"not_signed" | "valid" | "expired" DebugInfo *DebugInfoDTO `json:"_debug,omitempty"` // 调试信息(仅debug=true时返回) } diff --git a/signing/app/link.go b/signing/app/link.go index 2f9e71bd..c43b8141 100644 --- a/signing/app/link.go +++ b/signing/app/link.go @@ -103,6 +103,9 @@ func (s *linkService) List(userId string) ([]repository.LinkSummary, error) { func (s *linkService) FindCLAs(cmd *CmdToFindCLAs) ([]CLADetailDTO, error) { v, err := s.repo.Find(cmd.LinkId) if err != nil { + if commonRepo.IsErrorResourceNotFound(err) { + return nil, domain.NewDomainError(domain.ErrorCodeLinkNotExists) + } return nil, err } diff --git a/signing/domain/claservice/service.go b/signing/domain/claservice/service.go index f44fe90a..e46422b1 100644 --- a/signing/domain/claservice/service.go +++ b/signing/domain/claservice/service.go @@ -20,7 +20,6 @@ func NewCLAService( repo repository.Link, local localcla.LocalCLA, message message.Message, - corpRepo repository.CorpSigning, ) (CLAService, error) { cache, err := initLink(repo) if err != nil { @@ -32,7 +31,6 @@ func NewCLAService( local: local, message: message, linkCache: cache, - corpRepo: corpRepo, }, nil } @@ -55,7 +53,6 @@ type claService struct { repo repository.Link local localcla.LocalCLA message message.Message - corpRepo repository.CorpSigning } func (s *claService) Add(link *domain.Link, cla *domain.CLA) error { @@ -106,10 +103,6 @@ func (s *claService) Update(link *domain.Link, newCla *domain.CLA) error { OldCLAId: oldCLA.Id, NewCLAId: newCla.Id, }) - - if err = s.corpRepo.SetPendingCLAForLink(link.Id, newCla.Id); err != nil { - return err - } } return err @@ -191,14 +184,14 @@ func (s *claService) ContainsCla(linkId, claId string) bool { return s.linkCache.contains(linkId, claId) } -func (s *claService) GetLastUpdateTime(linkId string, claType dp.CLAType, language dp.Language) time.Time { - return s.linkCache.getLastUpdateTime(linkId, claType, language) -} - func (s *claService) GetClaId(linkId string, claType dp.CLAType, language dp.Language) string { return s.linkCache.getClaId(linkId, claType, language) } +func (s *claService) GetLastUpdateTime(linkId string, claType dp.CLAType, language dp.Language) time.Time { + return s.linkCache.getLastUpdateTime(linkId, claType, language) +} + func (s *claService) RemoveLink(linkId string) { s.linkCache.removeLink(linkId) } diff --git a/signing/domain/config.go b/signing/domain/config.go index dad87fcc..e6500eb4 100644 --- a/signing/domain/config.go +++ b/signing/domain/config.go @@ -84,7 +84,7 @@ func (cfg *Config) SetDefault() { } if cfg.DefaultGracePeriodDays <= 0 { - cfg.DefaultGracePeriodDays = 365 + cfg.DefaultGracePeriodDays = 730 } } diff --git a/signing/domain/config_test.go b/signing/domain/config_test.go index 645b308b..65df1afd 100644 --- a/signing/domain/config_test.go +++ b/signing/domain/config_test.go @@ -34,8 +34,8 @@ func TestConfigSetDefault(t *testing.T) { if cfg.CommunityManagerLinkId != "fake_link" { t.Errorf("CommunityManagerLinkId: got %s, want fake_link", cfg.CommunityManagerLinkId) } - if cfg.DefaultGracePeriodDays != 365 { - t.Errorf("DefaultGracePeriodDays: got %d, want 365", cfg.DefaultGracePeriodDays) + if cfg.DefaultGracePeriodDays != 730 { + t.Errorf("DefaultGracePeriodDays: got %d, want 730", cfg.DefaultGracePeriodDays) } } @@ -94,8 +94,8 @@ func TestConfigSetDefaultZeroValues(t *testing.T) { cfg.SetDefault() - if cfg.DefaultGracePeriodDays != 365 { - t.Errorf("DefaultGracePeriodDays 0 should default to 365: got %d", cfg.DefaultGracePeriodDays) + if cfg.DefaultGracePeriodDays != 730 { + t.Errorf("DefaultGracePeriodDays 0 should default to 730: got %d", cfg.DefaultGracePeriodDays) } } diff --git a/signing/domain/corp_signing.go b/signing/domain/corp_signing.go index 670d0650..fba32247 100644 --- a/signing/domain/corp_signing.go +++ b/signing/domain/corp_signing.go @@ -1,15 +1,10 @@ package domain -import ( - "github.com/opensourceways/app-cla-server/signing/domain/dp" - "github.com/opensourceways/app-cla-server/util" -) +import "github.com/opensourceways/app-cla-server/signing/domain/dp" const ( RoleAdmin = "admin" RoleManager = "manager" - - corpSigningActionAgree = "agree" ) type AllSingingInfo = map[string]string @@ -30,12 +25,6 @@ type LinkInfo struct { CLAInfo } -type CorpSigningLog struct { - Date string - CLAId string - Action string -} - type CorpSigning struct { Id string Date string @@ -44,17 +33,15 @@ type CorpSigning struct { Corp Corporation AllInfo AllSingingInfo - HasPDF bool + HasPDF bool // true if pdf has uploaded Admin Manager Managers []Manager Employees []EmployeeSigning Version int - PendingCLAId string - ClaNotifyCount int - ClaNotifyTime int64 - - Logs []CorpSigningLog + PendingCLAId string // 待同意的 CLA 版本 + ClaNotifyCount int // 已发送通知次数 + ClaNotifyTime int64 // 上次通知时间(unix timestamp) } func (cs *CorpSigning) CorpName() dp.CorpName { @@ -276,12 +263,6 @@ func (cs *CorpSigning) SetLatestClaId(latestClaId string) error { cs.ClaNotifyCount = 0 cs.ClaNotifyTime = 0 - cs.Logs = append(cs.Logs, CorpSigningLog{ - Date: util.Date(), - CLAId: latestClaId, - Action: corpSigningActionAgree, - }) - return nil } diff --git a/signing/domain/corp_signing_test.go b/signing/domain/corp_signing_test.go index af14aa66..9aba8599 100644 --- a/signing/domain/corp_signing_test.go +++ b/signing/domain/corp_signing_test.go @@ -23,7 +23,6 @@ func TestCorpSigningSetLatestClaIdWithExistingLogs(t *testing.T) { PendingCLAId: "5", ClaNotifyCount: 3, ClaNotifyTime: 2000, - Logs: []CorpSigningLog{{Date: "2025-01-01", CLAId: "3", Action: "sign"}}, } err := cs.SetLatestClaId("5") @@ -43,13 +42,6 @@ func TestCorpSigningSetLatestClaIdWithExistingLogs(t *testing.T) { if cs.ClaNotifyTime != 0 { t.Errorf("ClaNotifyTime: got %d, want 0", cs.ClaNotifyTime) } - if len(cs.Logs) != 2 { - t.Fatalf("Logs length: got %d, want 2", len(cs.Logs)) - } - last := cs.Logs[len(cs.Logs)-1] - if last.Action != "agree" || last.CLAId != "5" { - t.Errorf("last log: action=%s claId=%s, want agree/5", last.Action, last.CLAId) - } } func TestCorpSigningCanRemove(t *testing.T) { diff --git a/signing/domain/individual_signing.go b/signing/domain/individual_signing.go index e3ce8fa6..f6b575ca 100644 --- a/signing/domain/individual_signing.go +++ b/signing/domain/individual_signing.go @@ -15,9 +15,9 @@ type IndividualSigning struct { AllInfo AllSingingInfo Version int - ClaNotify string - ClaNotifyCount int - ClaNotifyTime int64 + ClaNotify string // 已通知的 CLA 版本 + ClaNotifyCount int // 已发送通知次数 + ClaNotifyTime int64 // 上次通知时间(unix timestamp) } type IndividualSigningLog struct { diff --git a/signing/domain/link.go b/signing/domain/link.go index bde995c2..416493c5 100644 --- a/signing/domain/link.go +++ b/signing/domain/link.go @@ -22,11 +22,9 @@ type Link struct { Org OrgInfo Email EmailInfo CLAs []CLA - Submitter string + Submitter string // community name which is in lowcase format. CLANum int Version int - - // nil 表示未显式配置,回退全局默认宽限期天数;非nil则是管理员显式设置的值(含0)。 GracePeriodDays *int } @@ -91,9 +89,6 @@ func (link *Link) GetCLA(t dp.CLAType, l dp.Language) *CLA { } func (link *Link) GetEffectiveGracePeriodDays(defaultDays int) int { - // nil:字段缺失/从未配置过(新建link、或功能上线前的历史数据) - // 负数:显式重置为“使用默认值” - // 二者都回退到全局默认宽限期天数,不需要任何数据迁移。 if link.GracePeriodDays == nil || *link.GracePeriodDays < 0 { return defaultDays } diff --git a/signing/domain/link_test.go b/signing/domain/link_test.go index 41a4d7d8..40687a6e 100644 --- a/signing/domain/link_test.go +++ b/signing/domain/link_test.go @@ -37,7 +37,6 @@ func TestCorpSigningSetLatestClaId(t *testing.T) { PendingCLAId: "5", ClaNotifyCount: 2, ClaNotifyTime: 1000, - Logs: []CorpSigningLog{}, } err := cs.SetLatestClaId("5") @@ -57,15 +56,6 @@ func TestCorpSigningSetLatestClaId(t *testing.T) { if cs.ClaNotifyTime != 0 { t.Errorf("ClaNotifyTime not reset: got %d, want 0", cs.ClaNotifyTime) } - if len(cs.Logs) != 1 { - t.Fatalf("Logs length: got %d, want 1", len(cs.Logs)) - } - if cs.Logs[0].Action != "agree" { - t.Errorf("Log action: got %s, want agree", cs.Logs[0].Action) - } - if cs.Logs[0].CLAId != "5" { - t.Errorf("Log CLAId: got %s, want 5", cs.Logs[0].CLAId) - } } func TestCorpSigningSetLatestClaIdAlreadyLatest(t *testing.T) { diff --git a/signing/domain/repository/corp_signing.go b/signing/domain/repository/corp_signing.go index 86636c6e..07064589 100644 --- a/signing/domain/repository/corp_signing.go +++ b/signing/domain/repository/corp_signing.go @@ -14,10 +14,6 @@ type CorpSigningSummary struct { Rep domain.Representative Corp domain.Corporation Admin domain.Manager - - PendingCLAId string - ClaNotifyCount int - ClaNotifyTime int64 } type EmployeeSigningSummary struct { @@ -71,7 +67,5 @@ type CorpSigning interface { HasSignedLink(linkId string) (bool, error) HasSignedCLA(*domain.CLAIndex, dp.CLAType) (bool, error) UpdateClaId(cs *domain.CorpSigning) error - UpdateCLANotify(summary *CorpSigningSummary) error - SetPendingCLAForLink(linkId, newClaId string) error FindPendingAgreements(linkId string) ([]CorpSigningSummary, error) } diff --git a/signing/domain/repository/individual_signing.go b/signing/domain/repository/individual_signing.go index 7074b266..b97e7a9d 100644 --- a/signing/domain/repository/individual_signing.go +++ b/signing/domain/repository/individual_signing.go @@ -17,5 +17,5 @@ type IndividualSigning interface { HasSignedLink(linkId string) (bool, error) HasSignedCLA(*domain.CLAIndex) (bool, error) SaveNewCLA(is *domain.IndividualSigning) error - UpdateCLANotify(linkId, email string, claId string, count int, notifyTime int64) error + UpdateCLANotify(is *domain.IndividualSigning) error } diff --git a/signing/domain/repository/link.go b/signing/domain/repository/link.go index 5d1f0cbe..84df9995 100644 --- a/signing/domain/repository/link.go +++ b/signing/domain/repository/link.go @@ -15,15 +15,21 @@ type LinkSummary struct { } type LinkCLA struct { - Id string - Org domain.OrgInfo - Email domain.EmailInfo - Clas []domain.CLA - RemovedCLAs []domain.CLA - // nil 表示未显式配置,由调用方回退到全局默认宽限期天数 + Id string + Org domain.OrgInfo + Email domain.EmailInfo + Clas []domain.CLA + RemovedCLAs []domain.CLA GracePeriodDays *int } +func (link *LinkCLA) GetEffectiveGracePeriodDays(defaultDays int) int { + if link.GracePeriodDays == nil || *link.GracePeriodDays < 0 { + return defaultDays + } + return *link.GracePeriodDays +} + type Link interface { NewLinkId() string Add(*domain.Link) error @@ -35,5 +41,6 @@ type Link interface { AddCLA(*domain.Link, *domain.CLA) error UpdateCLA(link *domain.Link, newCla *domain.CLA) error RemoveCLA(*domain.Link, *domain.CLA) error + UpdateGracePeriodDays(linkId string, days int) error } diff --git a/signing/domain/repository/user.go b/signing/domain/repository/user.go index 99d868e4..1ea60457 100644 --- a/signing/domain/repository/user.go +++ b/signing/domain/repository/user.go @@ -11,6 +11,7 @@ type User interface { Remove([]string) error RemoveByAccount(linkId string, accounts []dp.Account) error + Save(*domain.User) error SavePassword(*domain.User) error SavePrivacyConsent(*domain.User) error diff --git a/signing/domain/userservice/service.go b/signing/domain/userservice/service.go index ba82cc1c..d216de17 100644 --- a/signing/domain/userservice/service.go +++ b/signing/domain/userservice/service.go @@ -33,6 +33,7 @@ type UserService interface { RemoveByAccount(linkId string, accounts []dp.Account) ChangePassword(index string, old, newOne dp.Password) error ResetPassword(linkId string, email dp.EmailAddr, newOne dp.Password) error + UpdateEmail(linkId string, oldEmail, newEmail dp.EmailAddr) error } type userService struct { @@ -145,6 +146,32 @@ func (s *userService) ResetPassword(linkId string, email dp.EmailAddr, newOne dp return s.repo.SavePassword(&u) } +func (s *userService) UpdateEmail(linkId string, oldEmail, newEmail dp.EmailAddr) error { + if oldEmail.EmailAddr() == newEmail.EmailAddr() { + return nil + } + + u, err := s.repo.FindByEmail(linkId, oldEmail) + if err != nil { + return err + } + + u.EmailAddr = newEmail + + // account 格式为 {adminId}_{emailDomain},domain 变了 account 也要变 + oldAccount := u.Account.Account() + lastUnderscore := strings.LastIndex(oldAccount, "_") + if lastUnderscore > 0 { + newAccount, err := dp.NewAccount(oldAccount[:lastUnderscore] + "_" + newEmail.Domain()) + if err != nil { + return err + } + u.Account = newAccount + } + + return s.repo.Save(&u) +} + func (s *userService) IsAValidUser(linkId string, email dp.EmailAddr) (bool, error) { if _, err := s.repo.FindByEmail(linkId, email); err != nil { if commonRepo.IsErrorResourceNotFound(err) { diff --git a/signing/infrastructure/emailtmpl/template.go b/signing/infrastructure/emailtmpl/template.go index bfa11833..47757cd3 100644 --- a/signing/infrastructure/emailtmpl/template.go +++ b/signing/infrastructure/emailtmpl/template.go @@ -23,8 +23,8 @@ const ( TmplRemovingingEmployee = "removing employee" TmplPasswordRetrieval = "password retrieval" TmplEmailVerification = "email verification" - TmplCLAUpdated = "cla updated" - TmplIndividualCLAUpdated = "individual cla updated" + TmplCLAUpdated = "cla updated" + TmplCLAUpdatedIndividual = "cla updated individual" ) var msgTmpl = map[string]*template.Template{} @@ -47,8 +47,8 @@ func Init() error { TmplRemovingingEmployee: "./conf/email-template/removing-employee.tmpl", TmplPasswordRetrieval: "./conf/email-template/password-retrieval.tmpl", TmplEmailVerification: "./conf/email-template/email_verification.tmpl", - TmplCLAUpdated: "./conf/email-template/cla-updated.tmpl", - TmplIndividualCLAUpdated: "./conf/email-template/cla-updated-individual.tmpl", + TmplCLAUpdated: "./conf/email-template/cla-updated.tmpl", + TmplCLAUpdatedIndividual: "./conf/email-template/cla-updated-individual.tmpl", } for name, path := range items { @@ -236,28 +236,24 @@ func (data *EmailVerification) GenEmailMsg() (EmailMessage, error) { type CLAUpdated struct { Org string - CorpName string AdminName string - UpdateDate string ProjectURL string URLOfCLAPlatform string - GracePeriodDays int } func (data *CLAUpdated) GenEmailMsg() (EmailMessage, error) { return genEmailMsg(TmplCLAUpdated, data) } -type IndividualCLAUpdated struct { - Org string - Name string - UpdateDate string - ProjectURL string - URLOfCLAPlatform string - GracePeriodDays int - SignCLAURL string +type CLAUpdatedIndividual struct { + Name string + Org string + UpdateDate string + GracePeriodDays int + SignCLAURL string + ProjectURL string } -func (data *IndividualCLAUpdated) GenEmailMsg() (EmailMessage, error) { - return genEmailMsg(TmplIndividualCLAUpdated, data) +func (data *CLAUpdatedIndividual) GenEmailMsg() (EmailMessage, error) { + return genEmailMsg(TmplCLAUpdatedIndividual, data) } diff --git a/signing/infrastructure/repositoryimpl/cla.go b/signing/infrastructure/repositoryimpl/cla.go index 2361b323..2dfb961f 100644 --- a/signing/infrastructure/repositoryimpl/cla.go +++ b/signing/infrastructure/repositoryimpl/cla.go @@ -10,15 +10,11 @@ import ( "github.com/opensourceways/app-cla-server/signing/domain" ) -const fieldClaUpdatedAt = "updated_at" - func (impl *link) AddCLA(link *domain.Link, cla *domain.CLA) error { if err := impl.claContent.add(link.Id, cla); err != nil { return err } - // 只记录这份CLA自己的更新时间,不再往link级别写一个所有CLA共用的时间戳 - // (那样会导致改动其中一份CLA时,误把其他CLA的宽限期计时也一起刷新)。 cla.UpdatedAt = time.Now().Unix() do := toCLADO(cla) @@ -28,10 +24,10 @@ func (impl *link) AddCLA(link *domain.Link, cla *domain.CLA) error { } err = impl.dao.PushArraySingleItemAndUpdate( - impl.docFilter(link.Id), fieldCLAs, doc, - bson.M{ - fieldCLANum: link.CLANum, - }, + impl.docFilter(link.Id), + fieldCLAs, + doc, + bson.M{fieldCLANum: link.CLANum}, link.Version, ) if err != nil && impl.dao.IsDocNotExists(err) { @@ -69,8 +65,7 @@ func (impl *link) UpdateCLA(link *domain.Link, newCla *domain.CLA) error { newCla.UpdatedAt = time.Now().Unix() update := bson.M{ - fieldId: newCla.Id, - fieldUrl: newCla.URL, + fieldCLANum: link.CLANum, fieldClaUpdatedAt: newCla.UpdatedAt, } diff --git a/signing/infrastructure/repositoryimpl/corp_signing.go b/signing/infrastructure/repositoryimpl/corp_signing.go index 6517a825..f7cb69e8 100644 --- a/signing/infrastructure/repositoryimpl/corp_signing.go +++ b/signing/infrastructure/repositoryimpl/corp_signing.go @@ -8,7 +8,6 @@ import ( "github.com/opensourceways/app-cla-server/signing/domain" "github.com/opensourceways/app-cla-server/signing/domain/dp" "github.com/opensourceways/app-cla-server/signing/domain/repository" - "github.com/opensourceways/app-cla-server/util" ) // CorpSigningIndexes returns the index definitions that should exist on the @@ -157,18 +156,15 @@ func (impl *corpSigning) FindAll(linkId string) ([]repository.CorpSigningSummary filter := linkIdFilter(linkId) project := bson.M{ - fieldDate: 1, - fieldCLAId: 1, - fieldLang: 1, - fieldRep: 1, - fieldCorp: 1, - fieldAdmin: 1, - fieldLinkId: 1, - fieldHasPDF: 1, - fieldCLANotify: 1, - fieldPendingCLAId: 1, - fieldCLANotifyCount: 1, - fieldCLANotifyTime: 1, + fieldDate: 1, + fieldCLAId: 1, + fieldLang: 1, + fieldRep: 1, + fieldCorp: 1, + fieldAdmin: 1, + fieldLinkId: 1, + fieldHasPDF: 1, + fieldCLANotify: 1, } var dos []corpSigningDO @@ -196,18 +192,15 @@ func (impl *corpSigning) FindAllWithPagination(linkId string, offset, limit int) filter := linkIdFilter(linkId) project := bson.M{ - fieldDate: 1, - fieldCLAId: 1, - fieldLang: 1, - fieldRep: 1, - fieldCorp: 1, - fieldAdmin: 1, - fieldLinkId: 1, - fieldHasPDF: 1, - fieldCLANotify: 1, - fieldPendingCLAId: 1, - fieldCLANotifyCount: 1, - fieldCLANotifyTime: 1, + fieldDate: 1, + fieldCLAId: 1, + fieldLang: 1, + fieldRep: 1, + fieldCorp: 1, + fieldAdmin: 1, + fieldLinkId: 1, + fieldHasPDF: 1, + fieldCLANotify: 1, } var dos []corpSigningDO @@ -258,18 +251,15 @@ func (impl *corpSigning) FindPage(linkId string, intPage, intPageSize int, admin } project := bson.M{ - fieldDate: 1, - fieldCLAId: 1, - fieldLang: 1, - fieldRep: 1, - fieldCorp: 1, - fieldAdmin: 1, - fieldLinkId: 1, - fieldHasPDF: 1, - fieldCLANotify: 1, - fieldPendingCLAId: 1, - fieldCLANotifyCount: 1, - fieldCLANotifyTime: 1, + fieldDate: 1, + fieldCLAId: 1, + fieldLang: 1, + fieldRep: 1, + fieldCorp: 1, + fieldAdmin: 1, + fieldLinkId: 1, + fieldHasPDF: 1, + fieldCLANotify: 1, } // Single aggregation round-trip: $facet returns both total count and the @@ -367,20 +357,7 @@ func (impl *corpSigning) UpdateClaId(cs *domain.CorpSigning) error { return err } - setFields := bson.M{ - fieldCLAId: cs.Link.CLAId, - fieldPendingCLAId: "", - fieldCLANotifyCount: 0, - fieldCLANotifyTime: int64(0), - } - - pushItem := bson.M{ - "date": util.Date(), - "cla_id": cs.Link.CLAId, - "action": "agree", - } - - return impl.dao.PushArraySingleItemAndUpdate(filter, fieldLogs, pushItem, setFields, cs.Version) + return impl.dao.UpdateDoc(filter, bson.M{fieldCLAId: cs.Link.CLAId}, cs.Version) } func (impl *corpSigning) UpdateCLANotify(summary *repository.CorpSigningSummary) error { @@ -389,13 +366,7 @@ func (impl *corpSigning) UpdateCLANotify(summary *repository.CorpSigningSummary) return err } - update := bson.M{ - fieldCLANotify: summary.CLANotify, - fieldCLANotifyCount: summary.ClaNotifyCount, - fieldCLANotifyTime: summary.ClaNotifyTime, - } - - return impl.dao.UpdateDocsWithoutVersion(filter, update) + return impl.dao.UpdateDocsWithoutVersion(filter, bson.M{fieldCLANotify: summary.CLANotify}) } func (impl *corpSigning) Update(cs *domain.CorpSigning) error { @@ -404,54 +375,45 @@ func (impl *corpSigning) Update(cs *domain.CorpSigning) error { return err } + // 构建更新文档 updateDoc := bson.M{ childField(fieldRep, fieldName): cs.Rep.Name.Name(), childField(fieldRep, fieldEmail): cs.Rep.EmailAddr.EmailAddr(), } + // 同步更新 Admin 信息 + if cs.Admin.Id != "" { + updateDoc[childField(fieldAdmin, fieldName)] = cs.Admin.Name.Name() + updateDoc[childField(fieldAdmin, fieldEmail)] = cs.Admin.EmailAddr.EmailAddr() + } + return impl.dao.UpdateDoc(filter, updateDoc, cs.Version) } func (impl *corpSigning) SetPendingCLAForLink(linkId, newClaId string) error { - filter := linkIdFilter(linkId) - - update := bson.M{ - fieldPendingCLAId: newClaId, + filter := bson.M{ + fieldLinkId: linkId, } - return impl.dao.UpdateDocsWithoutVersion(filter, update) + return impl.dao.UpdateDocsWithoutVersion(filter, bson.M{fieldCLANotify: newClaId}) } func (impl *corpSigning) FindPendingAgreements(linkId string) ([]repository.CorpSigningSummary, error) { - filter := linkIdFilter(linkId) - filter[fieldPendingCLAId] = bson.M{"$ne": ""} - filter[fieldHasPDF] = true - - project := bson.M{ - fieldDate: 1, - fieldCLAId: 1, - fieldLang: 1, - fieldRep: 1, - fieldCorp: 1, - fieldAdmin: 1, - fieldLinkId: 1, - fieldHasPDF: 1, - fieldCLANotify: 1, - fieldPendingCLAId: 1, - fieldCLANotifyCount: 1, - fieldCLANotifyTime: 1, + filter := bson.M{ + fieldLinkId: linkId, + fieldCLANotify: bson.M{"$ne": ""}, } var dos []corpSigningDO - - if err := impl.dao.GetDocs(filter, project, &dos); err != nil { + err := impl.dao.GetDocs(filter, nil, &dos) + if err != nil || len(dos) == 0 { return nil, err } - v := make([]repository.CorpSigningSummary, len(dos)) + r := make([]repository.CorpSigningSummary, len(dos)) for i := range dos { - v[i] = dos[i].toCorpSigningSummary() + r[i] = dos[i].toCorpSigningSummary() } - return v, nil + return r, nil } diff --git a/signing/infrastructure/repositoryimpl/corp_signing_do.go b/signing/infrastructure/repositoryimpl/corp_signing_do.go index 3f662c8c..316b5fa1 100644 --- a/signing/infrastructure/repositoryimpl/corp_signing_do.go +++ b/signing/infrastructure/repositoryimpl/corp_signing_do.go @@ -10,42 +10,38 @@ import ( ) const ( - fieldPDF = "pdf" - fieldRep = "rep" - fieldType = "type" - fieldDate = "date" - fieldCorp = "corp" - fieldName = "name" - fieldLang = "lang" - fieldAdmin = "admin" - fieldEmail = "email" - fieldHasPDF = "has_pdf" - fieldLinkId = "link_id" - fieldDomain = "domain" - fieldDomains = "domains" - fieldDeleted = "deleted" - fieldVersion = "version" - fieldManagers = "managers" - fieldEmployees = "employees" - fieldTriggered = "triggered" - fieldCLANotify = "cla_notify" - fieldPendingCLAId = "pending_cla_id" - fieldCLANotifyCount = "cla_notify_count" - fieldCLANotifyTime = "cla_notify_time" + fieldPDF = "pdf" + fieldRep = "rep" + fieldType = "type" + fieldDate = "date" + fieldCorp = "corp" + fieldName = "name" + fieldLang = "lang" + fieldAdmin = "admin" + fieldEmail = "email" + fieldHasPDF = "has_pdf" + fieldLinkId = "link_id" + fieldDomain = "domain" + fieldDomains = "domains" + fieldDeleted = "deleted" + fieldVersion = "version" + fieldManagers = "managers" + fieldEmployees = "employees" + fieldTriggered = "triggered" + fieldCLANotify = "cla_notify" ) func toCorpSigningDO(v *domain.CorpSigning) corpSigningDO { link := &v.Link return corpSigningDO{ - Date: v.Date, - CLAId: link.CLAId, - LinkId: link.Id, - Language: link.Language.Language(), - Rep: toRepDO(&v.Rep), - Corp: toCorpDO(&v.Corp), - AllInfo: v.AllInfo, - CorpSigningLogsDO: toCorpSigningLogsDO(v.Logs), + Date: v.Date, + CLAId: link.CLAId, + LinkId: link.Id, + Language: link.Language.Language(), + Rep: toRepDO(&v.Rep), + Corp: toCorpDO(&v.Corp), + AllInfo: v.AllInfo, } } @@ -53,17 +49,16 @@ func toCorpSigningDOForMigrate(v *domain.CorpSigning) corpSigningDO { link := &v.Link return corpSigningDO{ - Date: v.Date, - CLAId: link.CLAId, - LinkId: link.Id, - Language: link.Language.Language(), - Rep: toRepDO(&v.Rep), - Corp: toCorpDO(&v.Corp), - AllInfo: v.AllInfo, - Admin: toManagerDO(&v.Admin), - Managers: toManagerDOs(v.Managers), - Employees: toEmployeeSigningDOs(v.Employees), - CorpSigningLogsDO: toCorpSigningLogsDO(v.Logs), + Date: v.Date, + CLAId: link.CLAId, + LinkId: link.Id, + Language: link.Language.Language(), + Rep: toRepDO(&v.Rep), + Corp: toCorpDO(&v.Corp), + AllInfo: v.AllInfo, + Admin: toManagerDO(&v.Admin), + Managers: toManagerDOs(v.Managers), + Employees: toEmployeeSigningDOs(v.Employees), } } @@ -87,12 +82,8 @@ type corpSigningDO struct { Version int `bson:"version" json:"-"` ClaNotify string `bson:"cla_notify" json:"cla_notify"` - PendingCLAId string `bson:"pending_cla_id" json:"pending_cla_id"` - ClaNotifyCount int `bson:"cla_notify_count" json:"cla_notify_count"` - ClaNotifyTime int64 `bson:"cla_notify_time" json:"cla_notify_time"` - - CorpSigningLogsDO `bson:",inline"` - + // uploading pdf or adding email domain will trigger individual signing checking + // which will delete the one that belongs to a corp. Triggered bool `bson:"triggered" json:"triggered,omitempty"` } @@ -117,12 +108,9 @@ func (do *corpSigningDO) toCorpSigningSummary() repository.CorpSigningSummary { Language: dp.CreateLanguage(do.Language), }, }, - Admin: do.Admin.toManager(), - HasPDF: do.HasPDF, - CLANotify: do.ClaNotify, - PendingCLAId: do.PendingCLAId, - ClaNotifyCount: do.ClaNotifyCount, - ClaNotifyTime: do.ClaNotifyTime, + Admin: do.Admin.toManager(), + HasPDF: do.HasPDF, + CLANotify: do.ClaNotify, } } @@ -148,16 +136,12 @@ func (do *corpSigningDO) toCorpSigning() domain.CorpSigning { Language: dp.CreateLanguage(do.Language), }, }, - Admin: do.Admin.toManager(), - HasPDF: do.HasPDF, - AllInfo: do.AllInfo, - Managers: do.toManagers(), - Employees: do.toEmployeeSignings(), - Version: do.Version, - PendingCLAId: do.PendingCLAId, - ClaNotifyCount: do.ClaNotifyCount, - ClaNotifyTime: do.ClaNotifyTime, - Logs: do.toCorpSigningLogs(), + Admin: do.Admin.toManager(), + HasPDF: do.HasPDF, + AllInfo: do.AllInfo, + Managers: do.toManagers(), + Employees: do.toEmployeeSignings(), + Version: do.Version, } } @@ -257,7 +241,7 @@ func toCorpDO(v *domain.Corporation) corpDO { } domain := "" - if v.PrimaryEmailDomain != "" { + if v.PrimaryEmailDomain != "" { // 如果PrimaryEmailDomain是string,零值就是空字符串 domain = v.PrimaryEmailDomain } @@ -272,45 +256,3 @@ func toCorpDO(v *domain.Corporation) corpDO { Domains: domains, } } - -type CorpSigningLogsDO struct { - Logs []corpSigningLogDO `bson:"logs" json:"logs"` -} - -type corpSigningLogDO struct { - Date string `bson:"date" json:"date"` - CLAId string `bson:"cla_id" json:"cla_id"` - Action string `bson:"action" json:"action"` -} - -func toCorpSigningLogsDO(logs []domain.CorpSigningLog) CorpSigningLogsDO { - var dos []corpSigningLogDO - for _, v := range logs { - dos = append(dos, toCorpSigningLogDO(v)) - } - return CorpSigningLogsDO{dos} -} - -func toCorpSigningLogDO(log domain.CorpSigningLog) corpSigningLogDO { - return corpSigningLogDO{ - Date: log.Date, - CLAId: log.CLAId, - Action: log.Action, - } -} - -func (do *corpSigningLogDO) toCorpSigningLog() domain.CorpSigningLog { - return domain.CorpSigningLog{ - Date: do.Date, - CLAId: do.CLAId, - Action: do.Action, - } -} - -func (do *corpSigningDO) toCorpSigningLogs() []domain.CorpSigningLog { - var logs []domain.CorpSigningLog - for _, v := range do.Logs { - logs = append(logs, v.toCorpSigningLog()) - } - return logs -} diff --git a/signing/infrastructure/repositoryimpl/individual_signing.go b/signing/infrastructure/repositoryimpl/individual_signing.go index 59ba9bab..b12c3f60 100644 --- a/signing/infrastructure/repositoryimpl/individual_signing.go +++ b/signing/infrastructure/repositoryimpl/individual_signing.go @@ -179,22 +179,22 @@ func (impl *individualSigning) SaveNewCLA(is *domain.IndividualSigning) error { } doc[fieldCLAId] = is.Link.CLAId doc[fieldCLANotify] = "" - doc[fieldCLANotifyCount] = 0 - doc[fieldCLANotifyTime] = int64(0) + doc["cla_notify_count"] = 0 + doc["cla_notify_time"] = int64(0) return impl.dao.UpdateDoc(filter, doc, is.Version) } -func (impl *individualSigning) UpdateCLANotify(linkId, email, claId string, count int, notifyTime int64) error { - filter := linkIdFilter(linkId) - filter[fieldEmail] = email +func (impl *individualSigning) UpdateCLANotify(is *domain.IndividualSigning) error { + filter := linkIdFilter(is.Link.Id) + filter[fieldEmail] = is.Rep.EmailAddr.EmailAddr() filter[fieldDeleted] = false - update := bson.M{ - fieldCLANotify: claId, - fieldCLANotifyCount: count, - fieldCLANotifyTime: notifyTime, + doc := bson.M{ + fieldCLANotify: is.ClaNotify, + "cla_notify_count": is.ClaNotifyCount, + "cla_notify_time": is.ClaNotifyTime, } - return impl.dao.UpdateDocsWithoutVersion(filter, update) + return impl.dao.UpdateDoc(filter, doc, is.Version) } diff --git a/signing/infrastructure/repositoryimpl/individual_signing_do.go b/signing/infrastructure/repositoryimpl/individual_signing_do.go index 7cc7e090..f725352b 100644 --- a/signing/infrastructure/repositoryimpl/individual_signing_do.go +++ b/signing/infrastructure/repositoryimpl/individual_signing_do.go @@ -35,14 +35,13 @@ type individualSigningDO struct { RepDO `bson:",inline"` IndividualSigningLogsDO `bson:",inline"` - Domain string `bson:"domain" json:"domain" required:"true"` - Version int `bson:"version" json:"-"` - Deleted bool `bson:"deleted" json:"deleted"` - DeletedAt int64 `bson:"deleted_at" json:"deleted_at,omitempty"` - - ClaNotify string `bson:"cla_notify" json:"cla_notify"` - ClaNotifyCount int `bson:"cla_notify_count" json:"cla_notify_count"` - ClaNotifyTime int64 `bson:"cla_notify_time" json:"cla_notify_time"` + Domain string `bson:"domain" json:"domain" required:"true"` + Version int `bson:"version" json:"-"` + Deleted bool `bson:"deleted" json:"deleted"` + DeletedAt int64 `bson:"deleted_at" json:"deleted_at,omitempty"` + ClaNotify string `bson:"cla_notify" json:"cla_notify"` + ClaNotifyCount int `bson:"cla_notify_count" json:"cla_notify_count"` + ClaNotifyTime int64 `bson:"cla_notify_time" json:"cla_notify_time"` } func (do *individualSigningDO) toIndividualSigning() domain.IndividualSigning { @@ -59,14 +58,14 @@ func (do *individualSigningDO) toIndividualSigning() domain.IndividualSigning { Language: dp.CreateLanguage(do.Language), }, }, - Rep: do.toRep(), - Date: do.Date, - AllInfo: do.AllInfo, - Version: do.Version, - Logs: logs, - ClaNotify: do.ClaNotify, + Rep: do.toRep(), + Date: do.Date, + AllInfo: do.AllInfo, + Version: do.Version, + Logs: logs, + ClaNotify: do.ClaNotify, ClaNotifyCount: do.ClaNotifyCount, - ClaNotifyTime: do.ClaNotifyTime, + ClaNotifyTime: do.ClaNotifyTime, } } diff --git a/signing/infrastructure/repositoryimpl/link.go b/signing/infrastructure/repositoryimpl/link.go index 20db77ec..2ab38ffe 100644 --- a/signing/infrastructure/repositoryimpl/link.go +++ b/signing/infrastructure/repositoryimpl/link.go @@ -164,11 +164,5 @@ func (impl *link) ListAll() ([]repository.LinkCLA, error) { func (impl *link) UpdateGracePeriodDays(linkId string, days int) error { filter := impl.docFilter(linkId) - filter[fieldDeleted] = false - - update := bson.M{ - fieldGracePeriodDays: days, - } - - return impl.dao.UpdateDocsWithoutVersion(filter, update) + return impl.dao.UpdateDocsWithoutVersion(filter, bson.M{"grace_period_days": days}) } diff --git a/signing/infrastructure/repositoryimpl/link_do.go b/signing/infrastructure/repositoryimpl/link_do.go index 68881971..91205782 100644 --- a/signing/infrastructure/repositoryimpl/link_do.go +++ b/signing/infrastructure/repositoryimpl/link_do.go @@ -8,17 +8,17 @@ import ( ) const ( - fieldOrg = "org" - fieldUrl = "url" - fieldCLAs = "clas" - fieldCLANum = "cla_num" - fieldRemoved = "removed" - fieldOrgAlias = "org_alias" - fieldOrgLogo = "org_logo" - fieldPlatform = "platform" - fieldSubmitter = "submitter" - fieldCLASFields = "clas.fields" - fieldGracePeriodDays = "grace_period_days" + fieldOrg = "org" + fieldUrl = "url" + fieldCLAs = "clas" + fieldCLANum = "cla_num" + fieldRemoved = "removed" + fieldOrgAlias = "org_alias" + fieldOrgLogo = "org_logo" + fieldPlatform = "platform" + fieldSubmitter = "submitter" + fieldCLASFields = "clas.fields" + fieldClaUpdatedAt = "updated_at" ) func toLinkDO(v *domain.Link) linkDO { @@ -47,12 +47,10 @@ type linkDO struct { Email emailInfoDO `bson:"email" json:"email" required:"true"` Submitter string `bson:"submitter" json:"submitter" required:"true"` CLAs []claDO `bson:"clas" json:"clas"` + RemovedCLAs []claDO `bson:"removed" json:"removed"` CLANum int `bson:"cla_num" json:"cla_num"` Version int `bson:"version" json:"-"` Deleted bool `bson:"deleted" json:"deleted"` - RemovedCLAs []claDO `bson:"removed" json:"removed"` - - // 指针类型:nil 表示该字段在文档里缺失/为null,即“从未显式配置过”, // 由上层回退到全局默认宽限期天数;非nil(含0)则是显式配置的值。 GracePeriodDays *int `bson:"grace_period_days" json:"grace_period_days"` } diff --git a/signing/infrastructure/repositoryimpl/user.go b/signing/infrastructure/repositoryimpl/user.go index 6890676e..846fd10f 100644 --- a/signing/infrastructure/repositoryimpl/user.go +++ b/signing/infrastructure/repositoryimpl/user.go @@ -100,6 +100,25 @@ func (impl *user) RemoveByAccount(linkId string, accounts []dp.Account) error { return impl.dao.DeleteDocs(filter) } +func (impl *user) Save(u *domain.User) error { + filter, err := impl.dao.DocIdFilter(u.Id) + if err != nil { + return err + } + + doc := bson.M{ + fieldEmail: u.EmailAddr.EmailAddr(), + fieldAccount: u.Account.Account(), + } + + err = impl.dao.UpdateDoc(filter, doc, u.Version) + if err != nil && impl.dao.IsDocNotExists(err) { + err = commonRepo.NewErrorConcurrentUpdating(err) + } + + return err +} + func (impl *user) SavePassword(u *domain.User) error { filter, err := impl.dao.DocIdFilter(u.Id) if err != nil { diff --git a/signing/watch/config.go b/signing/watch/config.go index 2cf8dcd8..3462e1f5 100644 --- a/signing/watch/config.go +++ b/signing/watch/config.go @@ -51,8 +51,10 @@ func (cfg *CLAUpdateConfig) genAllDiffInterval() time.Duration { } type NotifyAdminConfig struct { - SendEmailInterval int `json:"send_email_interval"` - NotifyCorpAdminInterval int `json:"notify_corp_admin_interval"` + 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"` } func (cfg *NotifyAdminConfig) SetDefault() { @@ -63,6 +65,14 @@ func (cfg *NotifyAdminConfig) SetDefault() { if cfg.NotifyCorpAdminInterval <= 0 { cfg.NotifyCorpAdminInterval = 1200 } + + if cfg.NotifyIndividualInterval <= 0 { + cfg.NotifyIndividualInterval = 86400 + } + + if cfg.NotifyIndividualRemindDays <= 0 { + cfg.NotifyIndividualRemindDays = 90 + } } func (cfg *NotifyAdminConfig) genSendEmailInterval() time.Duration { @@ -72,3 +82,11 @@ func (cfg *NotifyAdminConfig) genSendEmailInterval() time.Duration { func (cfg *NotifyAdminConfig) genNotifyCorpAdminInterval() time.Duration { return time.Second * time.Duration(cfg.NotifyCorpAdminInterval) } + +func (cfg *NotifyAdminConfig) genNotifyIndividualInterval() time.Duration { + return time.Second * time.Duration(cfg.NotifyIndividualInterval) +} + +func (cfg *NotifyAdminConfig) genNotifyIndividualRemindDays() int { + return cfg.NotifyIndividualRemindDays +} diff --git a/signing/watch/config_test.go b/signing/watch/config_test.go index c488fd62..ce48cd5d 100644 --- a/signing/watch/config_test.go +++ b/signing/watch/config_test.go @@ -9,34 +9,6 @@ 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{} @@ -56,47 +28,68 @@ func TestIsCorpSigningLatest(t *testing.T) { } } -func TestGetLatestCorpClaId(t *testing.T) { +func TestGetLatestIndividualCLA(t *testing.T) { impl := ¬ifyAdminWatchImpl{} - 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")}, - }, + 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")}, } - 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("en")); cla == nil || cla.Id != "11" { + t.Errorf("getLatestIndividualCLA en: got %v, want id=11", 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("zh")); cla == nil || cla.Id != "12" { + t.Errorf("getLatestIndividualCLA zh: got %v, want id=12", cla) } - if id := impl.getLatestCorpClaId(link, dp.CreateLanguage("fr")); id != "" { - t.Errorf("getLatestCorpClaId fr: got %s, want empty", id) + if cla := impl.getLatestIndividualCLA(clas, dp.CreateLanguage("fr")); cla != nil { + t.Errorf("getLatestIndividualCLA fr: got %v, want nil", cla) } } -func TestGetLatestIndividualClaId(t *testing.T) { +func TestIsIndividualSigningLatest(t *testing.T) { impl := ¬ifyAdminWatchImpl{} - 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")}, - }, + clas := []domain.CLA{ + {Id: "10", Type: dp.CLATypeIndividual, Language: dp.CreateLanguage("en")}, + {Id: "11", Type: dp.CLATypeCorp, Language: dp.CreateLanguage("en")}, } - if id := impl.getLatestIndividualClaId(link, dp.CreateLanguage("en")); id != "11" { - t.Errorf("getLatestIndividualClaId en: got %s, want 11", id) + 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("zh")); id != "12" { - t.Errorf("getLatestIndividualClaId zh: got %s, want 12", 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("fr")); id != "" { - t.Errorf("getLatestIndividualClaId fr: got %s, want empty", 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) + } + }) } } @@ -110,10 +103,21 @@ func TestNotifyAdminConfigSetDefault(t *testing.T) { 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) + } } func TestNotifyAdminConfigSetDefaultPreserves(t *testing.T) { - cfg := NotifyAdminConfig{SendEmailInterval: 20, NotifyCorpAdminInterval: 600} + cfg := NotifyAdminConfig{ + SendEmailInterval: 20, + NotifyCorpAdminInterval: 600, + NotifyIndividualInterval: 43200, + NotifyIndividualRemindDays: 45, + } cfg.SetDefault() if cfg.SendEmailInterval != 20 { @@ -122,6 +126,12 @@ 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) { @@ -140,6 +150,22 @@ 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() @@ -195,19 +221,3 @@ 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 f80ab791..c9e38d42 100644 --- a/signing/watch/notify_corp_admin.go +++ b/signing/watch/notify_corp_admin.go @@ -2,6 +2,7 @@ package watch import ( "fmt" + "net/url" "sync" "time" @@ -18,11 +19,11 @@ var notifyAdminWatchInstance *notifyAdminWatchImpl func NotifyAdminWatchStart(cfg *NotifyAdminConfig, lk repoLink, corp corpSigningRepo, individual individualSigningRepo, claPlatformURL string, defaultGracePeriodDays int) { notifyAdminWatchInstance = ¬ifyAdminWatchImpl{ - config: cfg, - link: lk, - corpSigningRepo: corp, - individualSigningRepo: individual, - claPlatformURL: claPlatformURL, + config: cfg, + link: lk, + corpSigningRepo: corp, + individualRepo: individual, + claPlatformURL: claPlatformURL, defaultGracePeriodDays: defaultGracePeriodDays, } @@ -43,17 +44,18 @@ type corpSigningRepo interface { } type individualSigningRepo interface { - FindAllWithPagination(linkId string, offset, limit int) ([]domain.IndividualSigning, error) - UpdateCLANotify(linkId, email, claId string, count int, notifyTime int64) error + FindAll(linkId string) ([]domain.IndividualSigning, error) + UpdateCLANotify(is *domain.IndividualSigning) error } type notifyAdminWatchImpl struct { config *NotifyAdminConfig - link repoLink - corpSigningRepo corpSigningRepo - individualSigningRepo individualSigningRepo - claPlatformURL string + link repoLink + corpSigningRepo corpSigningRepo + individualRepo individualSigningRepo + claPlatformURL string + defaultGracePeriodDays int wg sync.WaitGroup @@ -63,6 +65,9 @@ type notifyAdminWatchImpl struct { func (impl *notifyAdminWatchImpl) start() { impl.wg.Add(1) go impl.notifyCorpAdmin() + + impl.wg.Add(1) + go impl.notifyIndividualSigner() } func (impl *notifyAdminWatchImpl) exit() { @@ -122,39 +127,25 @@ func (impl *notifyAdminWatchImpl) handleNotifyJob() { impl.handleCorpSigning(link, &corpsSummary[j]) } - - impl.handleIndividualSignings(link, needStop) } } func (impl *notifyAdminWatchImpl) handleCorpSigning(link *repository.LinkCLA, corp *repository.CorpSigningSummary) { - if !corp.HasPDF { - return - } - if impl.isCorpSigningLatest(link.Clas, corp.Link.CLAInfo) { return } - latestClaId := impl.getLatestCorpClaId(link, corp.Link.Language) - if latestClaId == "" { + // Notification email has been sent. + if corp.CLANotify == corp.Link.CLAId { return } - if corp.CLANotify == latestClaId { - if time.Since(time.Unix(corp.ClaNotifyTime, 0)) < 7*24*time.Hour { - return - } - } - if err := impl.handleSendEmail(link, corp); err != nil { logs.Error("send cla notify email failed:", corp.Id, err) return } - corp.CLANotify = latestClaId - corp.ClaNotifyCount += 1 - corp.ClaNotifyTime = time.Now().Unix() + corp.CLANotify = corp.Link.CLAId if err := impl.corpSigningRepo.UpdateCLANotify(corp); err != nil { logs.Error("update cla notify failed: ", corp.Id, err) } @@ -178,15 +169,11 @@ func (impl *notifyAdminWatchImpl) handleSendEmail(link *repository.LinkCLA, corp if corp.Admin.Name == nil { return fmt.Errorf("failed to send email msg: admin name is null: %s", link.Id) } - graceDays := impl.getEffectiveGracePeriodDays(link) builder := emailtmpl.CLAUpdated{ Org: link.Org.Alias, - CorpName: corp.Corp.Name.CorpName(), AdminName: corp.Admin.Name.Name(), - UpdateDate: time.Now().Format("2006-01-02"), ProjectURL: link.Org.ProjectURL, - URLOfCLAPlatform: impl.claPlatformURL + "corporation-manager-login/" + link.Id, - GracePeriodDays: graceDays, + URLOfCLAPlatform: impl.rootURL() + "/" + link.Id, } emailMsg, err := builder.GenEmailMsg() if err != nil { @@ -195,105 +182,155 @@ func (impl *notifyAdminWatchImpl) handleSendEmail(link *repository.LinkCLA, corp emailMsg.From = link.Email.Addr.EmailAddr() emailMsg.To = []string{corp.Admin.EmailAddr.EmailAddr()} - - if corp.Link.Language.Language() == "en" { - emailMsg.Subject = fmt.Sprintf("%s CLA Has Been Updated - No Immediate Action Required", link.Org.Alias) - } else { - emailMsg.Subject = fmt.Sprintf("%s CLA 协议已更新 - 无需立即操作", link.Org.Alias) - } + emailMsg.Subject = "CLA has been updated" worker.GetEmailWorker().SendSimpleMessage(link.Email.Platform, &emailMsg) + // Sending email is done in goroutine. + // Prevent the concurrency from being too high, which would cause the email server refused to serve. time.Sleep(impl.config.genSendEmailInterval()) return nil } -func (impl *notifyAdminWatchImpl) getLatestCorpClaId(link *repository.LinkCLA, language dp.Language) string { - for i := range link.Clas { - if link.Clas[i].Type == dp.CLATypeCorp && link.Clas[i].Language == language { - return link.Clas[i].Id +func (impl *notifyAdminWatchImpl) notifyIndividualSigner() { + interval := impl.config.genNotifyIndividualInterval() + timer := time.NewTimer(interval) + for { + select { + case <-impl.stop: + timer.Stop() + impl.wg.Done() + return + case <-timer.C: + impl.handleIndividualNotifyJob() + timer.Reset(interval) } } - return "" } -func (impl *notifyAdminWatchImpl) handleIndividualSignings(link *repository.LinkCLA, needStop func() bool) { - limit := 100 - offset := 0 +func (impl *notifyAdminWatchImpl) handleIndividualNotifyJob() { + needStop := func() bool { + select { + case <-impl.stop: + return true + default: + return false + } + } - for { + links, err := impl.link.ListAll() + if err != nil { + logs.Error("list all link failed in individual notify job: ", err) + return + } + + for i := range links { if needStop() { return } - individuals, err := impl.individualSigningRepo.FindAllWithPagination(link.Id, offset, limit) + link := &links[i] + individuals, err := impl.individualRepo.FindAll(link.Id) if err != nil { logs.Error("list individual signing failed in notify job:", link.Id, err) - return - } - - if len(individuals) == 0 { - break + continue } - for i := range individuals { + for j := range individuals { if needStop() { return } - impl.handleIndividualSigning(link, &individuals[i]) + impl.handleIndividualSigning(link, &individuals[j]) } - - offset += limit } } func (impl *notifyAdminWatchImpl) handleIndividualSigning(link *repository.LinkCLA, is *domain.IndividualSigning) { - latestClaId := impl.getLatestIndividualClaId(link, is.Link.Language) - if latestClaId == "" { + // 检查是否签署了最新版本 + if impl.isIndividualSigningLatest(link.Clas, is.Link.CLAInfo) { return } - if is.HasSignedCLA(latestClaId) { + // 获取当前 CLA 的更新时间 + latestCLA := impl.getLatestIndividualCLA(link.Clas, is.Link.Language) + if latestCLA == nil { return } - if is.ClaNotify == latestClaId { - if time.Since(time.Unix(is.ClaNotifyTime, 0)) < 7*24*time.Hour { - return - } + remindDays := impl.config.genNotifyIndividualRemindDays() + nowUnix := time.Now().Unix() + + // 如果 CLA 版本已变更(与上次通知的版本不同),重置通知计数 + if is.ClaNotify != latestCLA.Id { + is.ClaNotify = latestCLA.Id + is.ClaNotifyCount = 0 + is.ClaNotifyTime = 0 } - if err := impl.handleSendIndividualEmail(link, is); err != nil { + // 计算距离上次通知的天数 + daysSinceLastNotify := 0 + if is.ClaNotifyTime > 0 { + daysSinceLastNotify = int((nowUnix - is.ClaNotifyTime) / 86400) + } + + // 如果距上次通知不足 remindDays 天,跳过 + if daysSinceLastNotify < remindDays && is.ClaNotifyCount > 0 { + return + } + + if err := impl.handleSendIndividualEmail(link, is, latestCLA); err != nil { logs.Error("send individual cla notify email failed:", is.Rep.EmailAddr.EmailAddr(), err) return } - is.ClaNotify = latestClaId - is.ClaNotifyCount += 1 - is.ClaNotifyTime = time.Now().Unix() - if err := impl.individualSigningRepo.UpdateCLANotify( - link.Id, is.Rep.EmailAddr.EmailAddr(), is.ClaNotify, is.ClaNotifyCount, is.ClaNotifyTime, - ); err != nil { + is.ClaNotifyCount++ + is.ClaNotifyTime = nowUnix + if err := impl.individualRepo.UpdateCLANotify(is); err != nil { logs.Error("update individual cla notify failed: ", is.Rep.EmailAddr.EmailAddr(), err) } } -func (impl *notifyAdminWatchImpl) handleSendIndividualEmail(link *repository.LinkCLA, is *domain.IndividualSigning) error { - if is.Rep.Name == nil { - return fmt.Errorf("failed to send email msg: individual name is null: %s", link.Id) +func (impl *notifyAdminWatchImpl) isIndividualSigningLatest(latestCLAs []domain.CLA, signedInfo domain.CLAInfo) bool { + for i := range latestCLAs { + if latestCLAs[i].Type == dp.CLATypeIndividual && + latestCLAs[i].Language == signedInfo.Language { + return latestCLAs[i].Id == signedInfo.CLAId + } } - graceDays := impl.getEffectiveGracePeriodDays(link) - signCLAURL := impl.claPlatformURL + "sign-cla/" + link.Id + "/individual?email=" + is.Rep.EmailAddr.EmailAddr() - builder := emailtmpl.IndividualCLAUpdated{ - Org: link.Org.Alias, - Name: is.Rep.Name.Name(), - UpdateDate: time.Now().Format("2006-01-02"), - ProjectURL: link.Org.ProjectURL, - URLOfCLAPlatform: impl.claPlatformURL + link.Id, - GracePeriodDays: graceDays, - SignCLAURL: signCLAURL, + return true +} + +func (impl *notifyAdminWatchImpl) getLatestIndividualCLA(clas []domain.CLA, language dp.Language) *domain.CLA { + for i := range clas { + if clas[i].Type == dp.CLATypeIndividual && clas[i].Language == language { + return &clas[i] + } + } + return nil +} + +func (impl *notifyAdminWatchImpl) handleSendIndividualEmail(link *repository.LinkCLA, is *domain.IndividualSigning, latestCLA *domain.CLA) error { + name := "" + if is.Rep.Name != nil { + name = is.Rep.Name.Name() + } + + // 格式化 CLA 更新时间 + updateDate := time.Unix(latestCLA.UpdatedAt, 0).Format("2006-01-02") + + // 构造个人签署 URL:从 claPlatformURL 提取 scheme+host,避免 /sign/sign-cla 双前缀 + signURL := fmt.Sprintf("%s/%s/individual-update?email=%s", + impl.rootURL(), link.Id, is.Rep.EmailAddr.EmailAddr()) + + builder := emailtmpl.CLAUpdatedIndividual{ + Name: name, + Org: link.Org.Alias, + UpdateDate: updateDate, + GracePeriodDays: link.GetEffectiveGracePeriodDays(impl.defaultGracePeriodDays), + SignCLAURL: signURL, + ProjectURL: link.Org.ProjectURL, } emailMsg, err := builder.GenEmailMsg() if err != nil { @@ -302,12 +339,7 @@ func (impl *notifyAdminWatchImpl) handleSendIndividualEmail(link *repository.Lin emailMsg.From = link.Email.Addr.EmailAddr() emailMsg.To = []string{is.Rep.EmailAddr.EmailAddr()} - - if is.Link.Language.Language() == "en" { - emailMsg.Subject = fmt.Sprintf("%s CLA Has Been Updated - No Immediate Action Required", link.Org.Alias) - } else { - emailMsg.Subject = fmt.Sprintf("%s CLA 协议已更新 - 无需立即操作", link.Org.Alias) - } + emailMsg.Subject = "CLA has been updated - Action Required" worker.GetEmailWorker().SendSimpleMessage(link.Email.Platform, &emailMsg) @@ -316,18 +348,13 @@ func (impl *notifyAdminWatchImpl) handleSendIndividualEmail(link *repository.Lin return nil } -func (impl *notifyAdminWatchImpl) getEffectiveGracePeriodDays(link *repository.LinkCLA) int { - if link.GracePeriodDays != nil && *link.GracePeriodDays >= 0 { - return *link.GracePeriodDays - } - return impl.defaultGracePeriodDays -} - -func (impl *notifyAdminWatchImpl) getLatestIndividualClaId(link *repository.LinkCLA, language dp.Language) string { - for i := range link.Clas { - if link.Clas[i].Type == dp.CLATypeIndividual && link.Clas[i].Language == language { - return link.Clas[i].Id - } +// rootURL 从 claPlatformURL 中提取 scheme+host(去掉路径前缀), +// 用于构造前端签署页面 URL,避免 /sign/sign-cla 双前缀问题。 +// 例如 https://clasign.osinfra.cn/sign/ -> https://clasign.osinfra.cn +func (impl *notifyAdminWatchImpl) rootURL() string { + u, err := url.Parse(impl.claPlatformURL) + if err != nil { + return impl.claPlatformURL } - return "" + return fmt.Sprintf("%s://%s", u.Scheme, u.Host) } diff --git a/signing/watch/notify_corp_admin_test.go b/signing/watch/notify_corp_admin_test.go index f9dcd3fd..804e1064 100644 --- a/signing/watch/notify_corp_admin_test.go +++ b/signing/watch/notify_corp_admin_test.go @@ -2,7 +2,6 @@ package watch import ( "testing" - "time" "github.com/opensourceways/app-cla-server/signing/domain" "github.com/opensourceways/app-cla-server/signing/domain/dp" @@ -61,10 +60,9 @@ func TestHandleCorpSigningNotifyWithin7Days(t *testing.T) { } corp := &repository.CorpSigningSummary{ - HasPDF: true, - CLANotify: "10", - Link: domain.LinkInfo{CLAInfo: domain.CLAInfo{CLAId: "9", Language: dp.CreateLanguage("en")}}, - ClaNotifyTime: time.Now().Unix() - 3*24*3600, + HasPDF: true, + CLANotify: "10", + Link: domain.LinkInfo{CLAInfo: domain.CLAInfo{CLAId: "9", Language: dp.CreateLanguage("en")}}, } impl.handleCorpSigning(link, corp) From 2fb45548ca3752917e9e4ed355163a62b4eabb51 Mon Sep 17 00:00:00 2001 From: JavaPythonAIForBAT Date: Tue, 14 Jul 2026 15:28:04 +0800 Subject: [PATCH 02/19] =?UTF-8?q?fix:=20=E4=BF=AE=E5=A4=8D=E9=82=AE?= =?UTF-8?q?=E4=BB=B6URL=E8=B7=AF=E5=BE=84=E5=92=8CStatus=E5=AD=97=E6=AE=B5?= =?UTF-8?q?=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 1. 修复企业管理员登录URL缺少 /corporation-manager-login/ 前缀 - URLOfCLAPlatform 改为 rootURL() + /corporation-manager-login/ + linkId - 例如: https://clasign.osinfra.cn/corporation-manager-login/{linkId} 2. 修复个人签署更新URL缺少 /sign-cla/ 前缀 - SignCLAURL 改为 rootURL() + /sign-cla/ + linkId + /individual-update?email=xxx - 例如: https://clasign.osinfra.cn/sign-cla/{linkId}/individual-update?email=xxx 3. 恢复 IndividualSigned.Status 字段并通过adapter传递 - API响应中恢复 status 字段("not_signed" | "valid" | "expired") --- .gitignore | 4 +++- models/individual_signing.go | 1 + signing/adapter/individual_signing.go | 1 + signing/watch/notify_corp_admin.go | 5 +++-- 4 files changed, 8 insertions(+), 3 deletions(-) diff --git a/.gitignore b/.gitignore index 429d0046..ad996ac5 100644 --- a/.gitignore +++ b/.gitignore @@ -4,4 +4,6 @@ lastupdate.tmp .trae *.b* config.yaml -CLAUDE.md \ No newline at end of file +CLAUDE.md +# Test binaries +*.test diff --git a/models/individual_signing.go b/models/individual_signing.go index bb225ab6..884ff7bd 100644 --- a/models/individual_signing.go +++ b/models/individual_signing.go @@ -29,6 +29,7 @@ type IndividualSigningInfo struct { type IndividualSigned struct { Type string `json:"type"` // "individual" 或 "corp" Signed bool `json:"signed"` // 是否已签署过 + Status string `json:"status"` // 内部状态:"not_signed" | "valid" | "expired" VersionMatched bool `json:"version_matched"` // 签署是否当前有效(考虑宽限期),始终返回以兼容旧版本调用 DebugInfo *DebugInfo `json:"_debug,omitempty"` // 调试信息(仅debug=true时返回) } diff --git a/signing/adapter/individual_signing.go b/signing/adapter/individual_signing.go index 1ef2f7ba..64fc2505 100644 --- a/signing/adapter/individual_signing.go +++ b/signing/adapter/individual_signing.go @@ -127,6 +127,7 @@ func (adapter *individualSigningAdatper) Check(linkId string, email string, return models.IndividualSigned{ Type: v.Type, Signed: v.Signed, + Status: v.Status, VersionMatched: v.VersionMatched, }, nil } diff --git a/signing/watch/notify_corp_admin.go b/signing/watch/notify_corp_admin.go index c9e38d42..f7e6a38d 100644 --- a/signing/watch/notify_corp_admin.go +++ b/signing/watch/notify_corp_admin.go @@ -173,7 +173,7 @@ func (impl *notifyAdminWatchImpl) handleSendEmail(link *repository.LinkCLA, corp Org: link.Org.Alias, AdminName: corp.Admin.Name.Name(), ProjectURL: link.Org.ProjectURL, - URLOfCLAPlatform: impl.rootURL() + "/" + link.Id, + URLOfCLAPlatform: impl.rootURL() + "/corporation-manager-login/" + link.Id, } emailMsg, err := builder.GenEmailMsg() if err != nil { @@ -321,7 +321,8 @@ func (impl *notifyAdminWatchImpl) handleSendIndividualEmail(link *repository.Lin updateDate := time.Unix(latestCLA.UpdatedAt, 0).Format("2006-01-02") // 构造个人签署 URL:从 claPlatformURL 提取 scheme+host,避免 /sign/sign-cla 双前缀 - signURL := fmt.Sprintf("%s/%s/individual-update?email=%s", + // 最终格式:https://clasign.osinfra.cn/sign-cla/{linkId}/individual-update?email=xxx + signURL := fmt.Sprintf("%s/sign-cla/%s/individual-update?email=%s", impl.rootURL(), link.Id, is.Rep.EmailAddr.EmailAddr()) builder := emailtmpl.CLAUpdatedIndividual{ From 2a88cda4d0d13f9acdf354a54030769bed74e079 Mon Sep 17 00:00:00 2001 From: JavaPythonAIForBAT Date: Tue, 14 Jul 2026 18:52:59 +0800 Subject: [PATCH 03/19] =?UTF-8?q?fix:=20CLA=E6=9B=B4=E6=96=B0=E6=97=B6URL?= =?UTF-8?q?=E6=9C=AA=E6=8C=81=E4=B9=85=E5=8C=96=E5=88=B0=E6=95=B0=E6=8D=AE?= =?UTF-8?q?=E5=BA=93=E7=9A=84=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit UpdateCLA 的 $set 操作只更新了 updated_at,没有更新 url/lang/type 字段, 导致更新 CLA PDF 后数据库中仍保留旧 URL,页面显示的也是旧 PDF。 修复:$set 操作包含 url、lang、type、updated_at 四个字段。 同时移除对 claDO 无意义的 fieldCLANum 字段更新(cla_num 是 link 级别字段)。 --- signing/infrastructure/repositoryimpl/cla.go | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/signing/infrastructure/repositoryimpl/cla.go b/signing/infrastructure/repositoryimpl/cla.go index 2dfb961f..a566e476 100644 --- a/signing/infrastructure/repositoryimpl/cla.go +++ b/signing/infrastructure/repositoryimpl/cla.go @@ -65,7 +65,9 @@ func (impl *link) UpdateCLA(link *domain.Link, newCla *domain.CLA) error { newCla.UpdatedAt = time.Now().Unix() update := bson.M{ - fieldCLANum: link.CLANum, + fieldUrl: newCla.URL.URL(), + fieldLang: newCla.Language.Language(), + fieldType: newCla.Type.CLAType(), fieldClaUpdatedAt: newCla.UpdatedAt, } From b8e31075a4ddaf48aa18422c78522b2ca5218888 Mon Sep 17 00:00:00 2001 From: JavaPythonAIForBAT Date: Tue, 14 Jul 2026 18:55:42 +0800 Subject: [PATCH 04/19] =?UTF-8?q?refactor:=20=E7=A7=BB=E9=99=A4=E5=86=97?= =?UTF-8?q?=E4=BD=99=E7=9A=84=20status=20=E5=AD=97=E6=AE=B5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit signed + version_matched 已完整覆盖所有签署状态: - signed=false, version_matched=false → 未签署 - signed=true, version_matched=false → 签署过但已过期 - signed=true, version_matched=true → 签署过且在有效期内 status 字段冗余,移除。 --- controllers/individual_signing.go | 2 +- models/individual_signing.go | 1 - signing/adapter/individual_signing.go | 1 - 3 files changed, 1 insertion(+), 3 deletions(-) diff --git a/controllers/individual_signing.go b/controllers/individual_signing.go index 3c373ee2..29972da2 100644 --- a/controllers/individual_signing.go +++ b/controllers/individual_signing.go @@ -141,7 +141,7 @@ func (ctl *IndividualSigningController) Check() { ctl.GetString(":link_id"), ctl.GetString("email"), ) - // status 是权威状态字段,始终返回;调试信息仅在 debug 模式下返回 + // signed + version_matched 覆盖全部状态;调试信息仅在 debug 模式下返回 if !debug { v.DebugInfo = nil } diff --git a/models/individual_signing.go b/models/individual_signing.go index 884ff7bd..bb225ab6 100644 --- a/models/individual_signing.go +++ b/models/individual_signing.go @@ -29,7 +29,6 @@ type IndividualSigningInfo struct { type IndividualSigned struct { Type string `json:"type"` // "individual" 或 "corp" Signed bool `json:"signed"` // 是否已签署过 - Status string `json:"status"` // 内部状态:"not_signed" | "valid" | "expired" VersionMatched bool `json:"version_matched"` // 签署是否当前有效(考虑宽限期),始终返回以兼容旧版本调用 DebugInfo *DebugInfo `json:"_debug,omitempty"` // 调试信息(仅debug=true时返回) } diff --git a/signing/adapter/individual_signing.go b/signing/adapter/individual_signing.go index 64fc2505..1ef2f7ba 100644 --- a/signing/adapter/individual_signing.go +++ b/signing/adapter/individual_signing.go @@ -127,7 +127,6 @@ func (adapter *individualSigningAdatper) Check(linkId string, email string, return models.IndividualSigned{ Type: v.Type, Signed: v.Signed, - Status: v.Status, VersionMatched: v.VersionMatched, }, nil } From eb48a0b09c3a0cfb1be69997a70baf9cbbee12eb Mon Sep 17 00:00:00 2001 From: JavaPythonAIForBAT Date: Tue, 14 Jul 2026 19:47:24 +0800 Subject: [PATCH 05/19] =?UTF-8?q?fix:=20ContainsCla=20=E7=BC=93=E5=AD=98?= =?UTF-8?q?=E6=9C=AA=E5=91=BD=E4=B8=AD=E6=97=B6=E5=85=9C=E5=BA=95=E6=9F=A5?= =?UTF-8?q?=E8=AF=A2=E6=95=B0=E6=8D=AE=E5=BA=93?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ContainsCla 只查内存缓存,当缓存因服务未重启等原因落后于数据库时, AgreeNewCLA 会错误地返回 "cla not exists"。 修复:缓存未命中时查询数据库,并将结果回填缓存。 --- signing/domain/claservice/service.go | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/signing/domain/claservice/service.go b/signing/domain/claservice/service.go index e46422b1..2ad8ae9f 100644 --- a/signing/domain/claservice/service.go +++ b/signing/domain/claservice/service.go @@ -181,6 +181,21 @@ func (s *claService) AddLink(link *domain.Link) error { } func (s *claService) ContainsCla(linkId, claId string) bool { + if s.linkCache.contains(linkId, claId) { + return true + } + + // 缓存未命中时兜底查询数据库,防止缓存因服务未重启等原因落后于数据库 + link, err := s.repo.Find(linkId) + if err != nil { + return false + } + + for i := range link.CLAs { + item := &link.CLAs[i] + s.linkCache.update(linkId, item) + } + return s.linkCache.contains(linkId, claId) } From 519b787e46e6f8af101f8f4611f49009d697f9a9 Mon Sep 17 00:00:00 2001 From: JavaPythonAIForBAT Date: Tue, 14 Jul 2026 20:07:17 +0800 Subject: [PATCH 06/19] =?UTF-8?q?fix:=20GetClaId/GetLastUpdateTime=20?= =?UTF-8?q?=E4=B9=9F=E5=8A=A0=E6=95=B0=E6=8D=AE=E5=BA=93=E5=85=9C=E5=BA=95?= =?UTF-8?q?=E6=9F=A5=E8=AF=A2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 与 ContainsCla 同样的问题:只查内存缓存,缓存未命中时返回空值, 导致 Check 接口的 isLatestClaVersion 误判、FindDiffCLAFile 对新签署 用户也生成 diff 文件。 修复:提取 fillCacheFromDB 公共方法,三个方法缓存未命中时都兜底查 DB。 --- signing/domain/claservice/service.go | 36 +++++++++++++++++++++------- 1 file changed, 27 insertions(+), 9 deletions(-) diff --git a/signing/domain/claservice/service.go b/signing/domain/claservice/service.go index 2ad8ae9f..2f42342b 100644 --- a/signing/domain/claservice/service.go +++ b/signing/domain/claservice/service.go @@ -186,27 +186,45 @@ func (s *claService) ContainsCla(linkId, claId string) bool { } // 缓存未命中时兜底查询数据库,防止缓存因服务未重启等原因落后于数据库 - link, err := s.repo.Find(linkId) - if err != nil { - return false - } - - for i := range link.CLAs { - item := &link.CLAs[i] - s.linkCache.update(linkId, item) - } + s.fillCacheFromDB(linkId) return s.linkCache.contains(linkId, claId) } func (s *claService) GetClaId(linkId string, claType dp.CLAType, language dp.Language) string { + id := s.linkCache.getClaId(linkId, claType, language) + if id != "" { + return id + } + + s.fillCacheFromDB(linkId) + return s.linkCache.getClaId(linkId, claType, language) } func (s *claService) GetLastUpdateTime(linkId string, claType dp.CLAType, language dp.Language) time.Time { + t := s.linkCache.getLastUpdateTime(linkId, claType, language) + if !t.IsZero() { + return t + } + + s.fillCacheFromDB(linkId) + return s.linkCache.getLastUpdateTime(linkId, claType, language) } +func (s *claService) fillCacheFromDB(linkId string) { + link, err := s.repo.Find(linkId) + if err != nil { + return + } + + for i := range link.CLAs { + item := &link.CLAs[i] + s.linkCache.update(linkId, item) + } +} + func (s *claService) RemoveLink(linkId string) { s.linkCache.removeLink(linkId) } From a49af97b6f909fcd1b6ca495c673e7eef7f628ac Mon Sep 17 00:00:00 2001 From: JavaPythonAIForBAT Date: Tue, 14 Jul 2026 20:29:31 +0800 Subject: [PATCH 07/19] =?UTF-8?q?fix:=20=E4=BC=81=E4=B8=9A=E5=90=8C?= =?UTF-8?q?=E6=84=8F=E6=96=B0CLA=E5=90=8E=E6=B8=85=E9=99=A4cla=5Fnotify?= =?UTF-8?q?=E5=AD=97=E6=AE=B5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit UpdateClaId 只更新了 cla_id,没有清除 cla_notify。导致企业管理员 同意新协议后,数据库中 cla_notify 仍为旧值,前端可能因此仍认为 有待同意项,展示旧版PDF而非新版。 修复:UpdateClaId 同时将 cla_notify 置空,与个人 SaveNewCLA 行为一致。 --- signing/infrastructure/repositoryimpl/corp_signing.go | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/signing/infrastructure/repositoryimpl/corp_signing.go b/signing/infrastructure/repositoryimpl/corp_signing.go index f7cb69e8..4f2536a4 100644 --- a/signing/infrastructure/repositoryimpl/corp_signing.go +++ b/signing/infrastructure/repositoryimpl/corp_signing.go @@ -357,7 +357,10 @@ func (impl *corpSigning) UpdateClaId(cs *domain.CorpSigning) error { return err } - return impl.dao.UpdateDoc(filter, bson.M{fieldCLAId: cs.Link.CLAId}, cs.Version) + return impl.dao.UpdateDoc(filter, bson.M{ + fieldCLAId: cs.Link.CLAId, + fieldCLANotify: "", + }, cs.Version) } func (impl *corpSigning) UpdateCLANotify(summary *repository.CorpSigningSummary) error { From c949753e70abb955b204a5540514fc5671747d97 Mon Sep 17 00:00:00 2001 From: JavaPythonAIForBAT Date: Tue, 14 Jul 2026 22:10:45 +0800 Subject: [PATCH 08/19] =?UTF-8?q?fix:=20CLA=E6=9B=B4=E6=96=B0=E6=9C=AA?= =?UTF-8?q?=E6=8C=81=E4=B9=85=E5=8C=96id=E5=AD=97=E6=AE=B5=E5=AF=BC?= =?UTF-8?q?=E8=87=B4=E5=89=8D=E7=AB=AF=E6=98=BE=E7=A4=BA=E6=97=A7=E5=8D=8F?= =?UTF-8?q?=E8=AE=AE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - UpdateCLA 的 操作补充 fieldId,确保 clas 中的 id 正确更新为新 CLA id - 企业管理员通知支持每90天重复提醒,与个人保持一致 - corpSigningDO 增加 cla_notify_count/cla_notify_time 字段进行持久化 - 补充 ErrorCodeCorpSigningCLAIsLatest 和 ErrorCodeIndividualSigningCLAIsLatest 的错误码映射 --- models/error.go | 1 + signing/adapter/error.go | 6 ++ signing/domain/repository/corp_signing.go | 18 ++--- signing/infrastructure/repositoryimpl/cla.go | 1 + .../repositoryimpl/corp_signing.go | 72 +++++++++++-------- .../repositoryimpl/corp_signing_do.go | 68 ++++++++++-------- signing/watch/config.go | 9 +++ signing/watch/notify_corp_admin.go | 34 ++++++++- 8 files changed, 140 insertions(+), 69 deletions(-) diff --git a/models/error.go b/models/error.go index 0a7846d9..b8baf12b 100644 --- a/models/error.go +++ b/models/error.go @@ -56,6 +56,7 @@ const ( ErrCLAIsUsed ModelErrCode = "cla_is_used" ErrLinkIsUsed ModelErrCode = "link_is_used" ErrNoPermission ModelErrCode = "no_permission" + ErrCLAIsLatest ModelErrCode = "cla_is_latest" ) type IModelError interface { diff --git a/signing/adapter/error.go b/signing/adapter/error.go index 4165b25b..618e4bf4 100644 --- a/signing/adapter/error.go +++ b/signing/adapter/error.go @@ -51,6 +51,9 @@ func codeMap(code string) models.ModelErrCode { case domain.ErrorCodeCorpSigningCanNotDelete: return models.ErrCorpManagerExists + case domain.ErrorCodeCorpSigningCLAIsLatest: + return models.ErrCLAIsLatest + // corp pdf case domain.ErrorCodeCorpPDFNotFound: return models.ErrUnuploaed @@ -108,6 +111,9 @@ func codeMap(code string) models.ModelErrCode { case domain.ErrorCodeIndividualSigningCorpExists: return models.ErrGoToSignEmployeeCLA + case domain.ErrorCodeIndividualSigningCLAIsLatest: + return models.ErrCLAIsLatest + // cla case domain.ErrorCodeCLAExists: return models.ErrCLAExists diff --git a/signing/domain/repository/corp_signing.go b/signing/domain/repository/corp_signing.go index 07064589..5dfa4b92 100644 --- a/signing/domain/repository/corp_signing.go +++ b/signing/domain/repository/corp_signing.go @@ -6,14 +6,16 @@ import ( ) type CorpSigningSummary struct { - Id string - Date string - HasPDF bool - CLANotify string - Link domain.LinkInfo - Rep domain.Representative - Corp domain.Corporation - Admin domain.Manager + Id string + Date string + HasPDF bool + CLANotify string + ClaNotifyCount int + ClaNotifyTime int64 + Link domain.LinkInfo + Rep domain.Representative + Corp domain.Corporation + Admin domain.Manager } type EmployeeSigningSummary struct { diff --git a/signing/infrastructure/repositoryimpl/cla.go b/signing/infrastructure/repositoryimpl/cla.go index a566e476..4c494c0c 100644 --- a/signing/infrastructure/repositoryimpl/cla.go +++ b/signing/infrastructure/repositoryimpl/cla.go @@ -65,6 +65,7 @@ func (impl *link) UpdateCLA(link *domain.Link, newCla *domain.CLA) error { newCla.UpdatedAt = time.Now().Unix() update := bson.M{ + fieldId: newCla.Id, fieldUrl: newCla.URL.URL(), fieldLang: newCla.Language.Language(), fieldType: newCla.Type.CLAType(), diff --git a/signing/infrastructure/repositoryimpl/corp_signing.go b/signing/infrastructure/repositoryimpl/corp_signing.go index 4f2536a4..8c239c85 100644 --- a/signing/infrastructure/repositoryimpl/corp_signing.go +++ b/signing/infrastructure/repositoryimpl/corp_signing.go @@ -156,15 +156,17 @@ func (impl *corpSigning) FindAll(linkId string) ([]repository.CorpSigningSummary filter := linkIdFilter(linkId) project := bson.M{ - fieldDate: 1, - fieldCLAId: 1, - fieldLang: 1, - fieldRep: 1, - fieldCorp: 1, - fieldAdmin: 1, - fieldLinkId: 1, - fieldHasPDF: 1, - fieldCLANotify: 1, + fieldDate: 1, + fieldCLAId: 1, + fieldLang: 1, + fieldRep: 1, + fieldCorp: 1, + fieldAdmin: 1, + fieldLinkId: 1, + fieldHasPDF: 1, + fieldCLANotify: 1, + fieldClaNotifyCount: 1, + fieldClaNotifyTime: 1, } var dos []corpSigningDO @@ -192,15 +194,17 @@ func (impl *corpSigning) FindAllWithPagination(linkId string, offset, limit int) filter := linkIdFilter(linkId) project := bson.M{ - fieldDate: 1, - fieldCLAId: 1, - fieldLang: 1, - fieldRep: 1, - fieldCorp: 1, - fieldAdmin: 1, - fieldLinkId: 1, - fieldHasPDF: 1, - fieldCLANotify: 1, + fieldDate: 1, + fieldCLAId: 1, + fieldLang: 1, + fieldRep: 1, + fieldCorp: 1, + fieldAdmin: 1, + fieldLinkId: 1, + fieldHasPDF: 1, + fieldCLANotify: 1, + fieldClaNotifyCount: 1, + fieldClaNotifyTime: 1, } var dos []corpSigningDO @@ -251,15 +255,17 @@ func (impl *corpSigning) FindPage(linkId string, intPage, intPageSize int, admin } project := bson.M{ - fieldDate: 1, - fieldCLAId: 1, - fieldLang: 1, - fieldRep: 1, - fieldCorp: 1, - fieldAdmin: 1, - fieldLinkId: 1, - fieldHasPDF: 1, - fieldCLANotify: 1, + fieldDate: 1, + fieldCLAId: 1, + fieldLang: 1, + fieldRep: 1, + fieldCorp: 1, + fieldAdmin: 1, + fieldLinkId: 1, + fieldHasPDF: 1, + fieldCLANotify: 1, + fieldClaNotifyCount: 1, + fieldClaNotifyTime: 1, } // Single aggregation round-trip: $facet returns both total count and the @@ -358,8 +364,10 @@ func (impl *corpSigning) UpdateClaId(cs *domain.CorpSigning) error { } return impl.dao.UpdateDoc(filter, bson.M{ - fieldCLAId: cs.Link.CLAId, - fieldCLANotify: "", + fieldCLAId: cs.Link.CLAId, + fieldCLANotify: "", + fieldClaNotifyCount: 0, + fieldClaNotifyTime: 0, }, cs.Version) } @@ -369,7 +377,11 @@ func (impl *corpSigning) UpdateCLANotify(summary *repository.CorpSigningSummary) return err } - return impl.dao.UpdateDocsWithoutVersion(filter, bson.M{fieldCLANotify: summary.CLANotify}) + return impl.dao.UpdateDocsWithoutVersion(filter, bson.M{ + fieldCLANotify: summary.CLANotify, + fieldClaNotifyCount: summary.ClaNotifyCount, + fieldClaNotifyTime: summary.ClaNotifyTime, + }) } func (impl *corpSigning) Update(cs *domain.CorpSigning) error { diff --git a/signing/infrastructure/repositoryimpl/corp_signing_do.go b/signing/infrastructure/repositoryimpl/corp_signing_do.go index 316b5fa1..b6562d63 100644 --- a/signing/infrastructure/repositoryimpl/corp_signing_do.go +++ b/signing/infrastructure/repositoryimpl/corp_signing_do.go @@ -28,20 +28,24 @@ const ( fieldManagers = "managers" fieldEmployees = "employees" fieldTriggered = "triggered" - fieldCLANotify = "cla_notify" + fieldCLANotify = "cla_notify" + fieldClaNotifyCount = "cla_notify_count" + fieldClaNotifyTime = "cla_notify_time" ) func toCorpSigningDO(v *domain.CorpSigning) corpSigningDO { link := &v.Link return corpSigningDO{ - Date: v.Date, - CLAId: link.CLAId, - LinkId: link.Id, - Language: link.Language.Language(), - Rep: toRepDO(&v.Rep), - Corp: toCorpDO(&v.Corp), - AllInfo: v.AllInfo, + Date: v.Date, + CLAId: link.CLAId, + LinkId: link.Id, + Language: link.Language.Language(), + Rep: toRepDO(&v.Rep), + Corp: toCorpDO(&v.Corp), + AllInfo: v.AllInfo, + ClaNotifyCount: v.ClaNotifyCount, + ClaNotifyTime: v.ClaNotifyTime, } } @@ -49,16 +53,18 @@ func toCorpSigningDOForMigrate(v *domain.CorpSigning) corpSigningDO { link := &v.Link return corpSigningDO{ - Date: v.Date, - CLAId: link.CLAId, - LinkId: link.Id, - Language: link.Language.Language(), - Rep: toRepDO(&v.Rep), - Corp: toCorpDO(&v.Corp), - AllInfo: v.AllInfo, - Admin: toManagerDO(&v.Admin), - Managers: toManagerDOs(v.Managers), - Employees: toEmployeeSigningDOs(v.Employees), + Date: v.Date, + CLAId: link.CLAId, + LinkId: link.Id, + Language: link.Language.Language(), + Rep: toRepDO(&v.Rep), + Corp: toCorpDO(&v.Corp), + AllInfo: v.AllInfo, + Admin: toManagerDO(&v.Admin), + Managers: toManagerDOs(v.Managers), + Employees: toEmployeeSigningDOs(v.Employees), + ClaNotifyCount: v.ClaNotifyCount, + ClaNotifyTime: v.ClaNotifyTime, } } @@ -80,7 +86,9 @@ type corpSigningDO struct { Employees []employeeSigningDO `bson:"employees" json:"employees"` Deleted []employeeSigningDO `bson:"deleted" json:"deleted"` Version int `bson:"version" json:"-"` - ClaNotify string `bson:"cla_notify" json:"cla_notify"` + ClaNotify string `bson:"cla_notify" json:"cla_notify"` + ClaNotifyCount int `bson:"cla_notify_count" json:"cla_notify_count"` + ClaNotifyTime int64 `bson:"cla_notify_time" json:"cla_notify_time"` // uploading pdf or adding email domain will trigger individual signing checking // which will delete the one that belongs to a corp. @@ -108,9 +116,11 @@ func (do *corpSigningDO) toCorpSigningSummary() repository.CorpSigningSummary { Language: dp.CreateLanguage(do.Language), }, }, - Admin: do.Admin.toManager(), - HasPDF: do.HasPDF, - CLANotify: do.ClaNotify, + Admin: do.Admin.toManager(), + HasPDF: do.HasPDF, + CLANotify: do.ClaNotify, + ClaNotifyCount: do.ClaNotifyCount, + ClaNotifyTime: do.ClaNotifyTime, } } @@ -136,12 +146,14 @@ func (do *corpSigningDO) toCorpSigning() domain.CorpSigning { Language: dp.CreateLanguage(do.Language), }, }, - Admin: do.Admin.toManager(), - HasPDF: do.HasPDF, - AllInfo: do.AllInfo, - Managers: do.toManagers(), - Employees: do.toEmployeeSignings(), - Version: do.Version, + Admin: do.Admin.toManager(), + HasPDF: do.HasPDF, + AllInfo: do.AllInfo, + Managers: do.toManagers(), + Employees: do.toEmployeeSignings(), + Version: do.Version, + ClaNotifyCount: do.ClaNotifyCount, + ClaNotifyTime: do.ClaNotifyTime, } } diff --git a/signing/watch/config.go b/signing/watch/config.go index 3462e1f5..f7c062ea 100644 --- a/signing/watch/config.go +++ b/signing/watch/config.go @@ -55,6 +55,7 @@ type NotifyAdminConfig struct { 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"` } func (cfg *NotifyAdminConfig) SetDefault() { @@ -73,6 +74,10 @@ func (cfg *NotifyAdminConfig) SetDefault() { if cfg.NotifyIndividualRemindDays <= 0 { cfg.NotifyIndividualRemindDays = 90 } + + if cfg.NotifyCorpAdminRemindDays <= 0 { + cfg.NotifyCorpAdminRemindDays = 90 + } } func (cfg *NotifyAdminConfig) genSendEmailInterval() time.Duration { @@ -90,3 +95,7 @@ func (cfg *NotifyAdminConfig) genNotifyIndividualInterval() time.Duration { func (cfg *NotifyAdminConfig) genNotifyIndividualRemindDays() int { return cfg.NotifyIndividualRemindDays } + +func (cfg *NotifyAdminConfig) genNotifyCorpAdminRemindDays() int { + return cfg.NotifyCorpAdminRemindDays +} diff --git a/signing/watch/notify_corp_admin.go b/signing/watch/notify_corp_admin.go index f7e6a38d..5d0df19f 100644 --- a/signing/watch/notify_corp_admin.go +++ b/signing/watch/notify_corp_admin.go @@ -135,8 +135,26 @@ func (impl *notifyAdminWatchImpl) handleCorpSigning(link *repository.LinkCLA, co return } - // Notification email has been sent. - if corp.CLANotify == corp.Link.CLAId { + latestCLA := impl.getLatestCorpCLA(link.Clas, corp.Link.Language) + if latestCLA == nil { + return + } + + remindDays := impl.config.genNotifyCorpAdminRemindDays() + nowUnix := time.Now().Unix() + + if corp.CLANotify != latestCLA.Id { + corp.CLANotify = latestCLA.Id + corp.ClaNotifyCount = 0 + corp.ClaNotifyTime = 0 + } + + daysSinceLastNotify := 0 + if corp.ClaNotifyTime > 0 { + daysSinceLastNotify = int((nowUnix - corp.ClaNotifyTime) / 86400) + } + + if daysSinceLastNotify < remindDays && corp.ClaNotifyCount > 0 { return } @@ -145,7 +163,8 @@ func (impl *notifyAdminWatchImpl) handleCorpSigning(link *repository.LinkCLA, co return } - corp.CLANotify = corp.Link.CLAId + corp.ClaNotifyCount++ + corp.ClaNotifyTime = nowUnix if err := impl.corpSigningRepo.UpdateCLANotify(corp); err != nil { logs.Error("update cla notify failed: ", corp.Id, err) } @@ -311,6 +330,15 @@ func (impl *notifyAdminWatchImpl) getLatestIndividualCLA(clas []domain.CLA, lang return nil } +func (impl *notifyAdminWatchImpl) getLatestCorpCLA(clas []domain.CLA, language dp.Language) *domain.CLA { + for i := range clas { + if clas[i].Type == dp.CLATypeCorp && clas[i].Language == language { + return &clas[i] + } + } + return nil +} + func (impl *notifyAdminWatchImpl) handleSendIndividualEmail(link *repository.LinkCLA, is *domain.IndividualSigning, latestCLA *domain.CLA) error { name := "" if is.Rep.Name != nil { From cdd42786530fb179c97e64519e1458dd753557d3 Mon Sep 17 00:00:00 2001 From: JavaPythonAIForBAT Date: Tue, 14 Jul 2026 23:09:15 +0800 Subject: [PATCH 09/19] =?UTF-8?q?feat:=20CLA=E6=9B=B4=E6=96=B0=E5=90=8E?= =?UTF-8?q?=E7=AB=8B=E5=8D=B3=E6=A0=87=E8=AE=B0=E4=BC=81=E4=B8=9A=E7=AD=BE?= =?UTF-8?q?=E7=BD=B2=E4=B8=BA=E5=BE=85=E9=80=9A=E7=9F=A5=E7=8A=B6=E6=80=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - CLA 更新成功后调用 SetPendingCLAForLink,将所有企业签署的 cla_notify 设置为新 CLA id,watcher 检测后立即发送通知 - SetPendingCLAForLink 同步重置 cla_notify_count/time,确保 第一次通知不被 90 天阈值误跳过 - Redis 缓存层补齐 ClaNotifyCount/ClaNotifyTime 字段 --- signing/app/cla.go | 6 +++++- signing/domain/repository/corp_signing.go | 1 + .../infrastructure/repositoryimpl/corp_signing.go | 6 +++++- .../repositoryimpl/corp_signing_cache.go | 15 +++++++++++---- 4 files changed, 22 insertions(+), 6 deletions(-) diff --git a/signing/app/cla.go b/signing/app/cla.go index dd3742bc..53779877 100644 --- a/signing/app/cla.go +++ b/signing/app/cla.go @@ -55,7 +55,11 @@ func (s *claService) Update(cmd *CmdToUpdateCLA) error { cla := cmd.newCLA() - return s.cla.Update(link, &cla) + if err = s.cla.Update(link, &cla); err != nil { + return err + } + + return s.cs.SetPendingCLAForLink(cmd.LinkId, cla.Id) } func (s *claService) Remove(cmd *CmdToRemoveCLA) error { diff --git a/signing/domain/repository/corp_signing.go b/signing/domain/repository/corp_signing.go index 5dfa4b92..45e3401f 100644 --- a/signing/domain/repository/corp_signing.go +++ b/signing/domain/repository/corp_signing.go @@ -70,4 +70,5 @@ type CorpSigning interface { HasSignedCLA(*domain.CLAIndex, dp.CLAType) (bool, error) UpdateClaId(cs *domain.CorpSigning) error FindPendingAgreements(linkId string) ([]CorpSigningSummary, error) + SetPendingCLAForLink(linkId, newClaId string) error } diff --git a/signing/infrastructure/repositoryimpl/corp_signing.go b/signing/infrastructure/repositoryimpl/corp_signing.go index 8c239c85..26118121 100644 --- a/signing/infrastructure/repositoryimpl/corp_signing.go +++ b/signing/infrastructure/repositoryimpl/corp_signing.go @@ -410,7 +410,11 @@ func (impl *corpSigning) SetPendingCLAForLink(linkId, newClaId string) error { fieldLinkId: linkId, } - return impl.dao.UpdateDocsWithoutVersion(filter, bson.M{fieldCLANotify: newClaId}) + return impl.dao.UpdateDocsWithoutVersion(filter, bson.M{ + fieldCLANotify: newClaId, + fieldClaNotifyCount: 0, + fieldClaNotifyTime: 0, + }) } func (impl *corpSigning) FindPendingAgreements(linkId string) ([]repository.CorpSigningSummary, error) { diff --git a/signing/infrastructure/repositoryimpl/corp_signing_cache.go b/signing/infrastructure/repositoryimpl/corp_signing_cache.go index bd36346c..464a4fde 100644 --- a/signing/infrastructure/repositoryimpl/corp_signing_cache.go +++ b/signing/infrastructure/repositoryimpl/corp_signing_cache.go @@ -52,6 +52,9 @@ type cachedCorpSigningSummary struct { AdminId string `json:"admin_id"` AdminName string `json:"admin_name"` AdminEmail string `json:"admin_email"` + // Notify + ClaNotifyCount int `json:"cla_notify_count"` + ClaNotifyTime int64 `json:"cla_notify_time"` } func toCache(p repository.CorpSigningSummaryPage) cachedCorpSigningPage { @@ -80,6 +83,8 @@ func toCache(p repository.CorpSigningSummaryPage) cachedCorpSigningPage { AdminId: s.Admin.Id, AdminName: adminName, AdminEmail: adminEmail, + ClaNotifyCount: s.ClaNotifyCount, + ClaNotifyTime: s.ClaNotifyTime, } } return cachedCorpSigningPage{Total: p.Total, Data: items} @@ -90,10 +95,12 @@ func fromCache(c cachedCorpSigningPage) repository.CorpSigningSummaryPage { for i := range c.Data { s := &c.Data[i] items[i] = repository.CorpSigningSummary{ - Id: s.Id, - Date: s.Date, - HasPDF: s.HasPDF, - CLANotify: s.CLANotify, + Id: s.Id, + Date: s.Date, + HasPDF: s.HasPDF, + CLANotify: s.CLANotify, + ClaNotifyCount: s.ClaNotifyCount, + ClaNotifyTime: s.ClaNotifyTime, Link: domain.LinkInfo{ Id: s.LinkId, CLAInfo: domain.CLAInfo{ From dead18e48b0dcdd4b91ff4106a450705417ce309 Mon Sep 17 00:00:00 2001 From: JavaPythonAIForBAT Date: Tue, 14 Jul 2026 23:12:03 +0800 Subject: [PATCH 10/19] =?UTF-8?q?fix:=20=E5=85=BC=E5=AE=B9=E5=AD=98?= =?UTF-8?q?=E9=87=8F=E6=95=B0=E6=8D=AE=EF=BC=8C=E9=81=BF=E5=85=8D=E4=B8=8A?= =?UTF-8?q?=E7=BA=BF=E6=97=B6=E5=A4=A7=E9=87=8F=E9=87=8D=E5=A4=8D=E5=8F=91?= =?UTF-8?q?=E9=80=81=E9=80=9A=E7=9F=A5=E9=82=AE=E4=BB=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 旧数据 cla_notify_count/time 为 0,上线后会被误判为未通知而立即发邮件。 新增兼容逻辑:当 cla_notify 已指向最新 CLA 但 count/time 为 0 时, 视为旧系统已通知过一次,初始化 count=1、time=now,跳过本次发送, 从当前时间起 90 天后才发送下次提醒。 --- signing/watch/notify_corp_admin.go | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/signing/watch/notify_corp_admin.go b/signing/watch/notify_corp_admin.go index 5d0df19f..f48cf421 100644 --- a/signing/watch/notify_corp_admin.go +++ b/signing/watch/notify_corp_admin.go @@ -149,6 +149,15 @@ func (impl *notifyAdminWatchImpl) handleCorpSigning(link *repository.LinkCLA, co corp.ClaNotifyTime = 0 } + if corp.ClaNotifyTime == 0 && corp.ClaNotifyCount == 0 && corp.CLANotify == latestCLA.Id { + corp.ClaNotifyCount = 1 + corp.ClaNotifyTime = nowUnix + if err := impl.corpSigningRepo.UpdateCLANotify(corp); err != nil { + logs.Error("init cla_notify_count/time failed: ", corp.Id, err) + } + return + } + daysSinceLastNotify := 0 if corp.ClaNotifyTime > 0 { daysSinceLastNotify = int((nowUnix - corp.ClaNotifyTime) / 86400) @@ -281,13 +290,21 @@ func (impl *notifyAdminWatchImpl) handleIndividualSigning(link *repository.LinkC remindDays := impl.config.genNotifyIndividualRemindDays() nowUnix := time.Now().Unix() - // 如果 CLA 版本已变更(与上次通知的版本不同),重置通知计数 if is.ClaNotify != latestCLA.Id { is.ClaNotify = latestCLA.Id is.ClaNotifyCount = 0 is.ClaNotifyTime = 0 } + if is.ClaNotifyTime == 0 && is.ClaNotifyCount == 0 && is.ClaNotify == latestCLA.Id { + is.ClaNotifyCount = 1 + is.ClaNotifyTime = nowUnix + if err := impl.individualRepo.UpdateCLANotify(is); err != nil { + logs.Error("init cla_notify_count/time failed: ", is.Rep.EmailAddr.EmailAddr(), err) + } + return + } + // 计算距离上次通知的天数 daysSinceLastNotify := 0 if is.ClaNotifyTime > 0 { From 370feaa530aad02a824e97d4b37e995ee1180593 Mon Sep 17 00:00:00 2001 From: JavaPythonAIForBAT Date: Wed, 15 Jul 2026 10:15:36 +0800 Subject: [PATCH 11/19] =?UTF-8?q?feat:=20CLA=E6=9B=B4=E6=96=B0=E5=90=8E?= =?UTF-8?q?=E7=AB=8B=E5=8D=B3=E8=A7=A6=E5=8F=91=E9=80=9A=E7=9F=A5=E6=89=AB?= =?UTF-8?q?=E6=8F=8F=EF=BC=8C=E4=B8=8D=E5=86=8D=E7=AD=89=E5=BE=85=E5=AE=9A?= =?UTF-8?q?=E6=97=B6=E5=99=A8=E5=91=A8=E6=9C=9F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - watch 包新增 TriggerNotify() 函数,向企业/个人 watcher 发送即时触发信号 - notifyCorpAdmin/notifyIndividualSigner 增加对触发通道的监听 - CLA 更新成功后调用 TriggerNotify(),实现秒级通知 更新后通知延迟: 企业管理员: 从 ≤20分钟 → 立即可达 个人用户: 从 ≤24小时 → 立即可达 --- signing/app/cla.go | 9 ++++++++- signing/watch/notify_corp_admin.go | 31 ++++++++++++++++++++++++++++-- 2 files changed, 37 insertions(+), 3 deletions(-) diff --git a/signing/app/cla.go b/signing/app/cla.go index 53779877..84ebb7c2 100644 --- a/signing/app/cla.go +++ b/signing/app/cla.go @@ -5,6 +5,7 @@ import ( "github.com/opensourceways/app-cla-server/signing/domain/claservice" "github.com/opensourceways/app-cla-server/signing/domain/dp" "github.com/opensourceways/app-cla-server/signing/domain/repository" + "github.com/opensourceways/app-cla-server/signing/watch" ) func NewCLAService( @@ -59,7 +60,13 @@ func (s *claService) Update(cmd *CmdToUpdateCLA) error { return err } - return s.cs.SetPendingCLAForLink(cmd.LinkId, cla.Id) + if err = s.cs.SetPendingCLAForLink(cmd.LinkId, cla.Id); err != nil { + return err + } + + watch.TriggerNotify() + + return nil } func (s *claService) Remove(cmd *CmdToRemoveCLA) error { diff --git a/signing/watch/notify_corp_admin.go b/signing/watch/notify_corp_admin.go index f48cf421..e953d850 100644 --- a/signing/watch/notify_corp_admin.go +++ b/signing/watch/notify_corp_admin.go @@ -25,6 +25,9 @@ func NotifyAdminWatchStart(cfg *NotifyAdminConfig, lk repoLink, corp corpSigning individualRepo: individual, claPlatformURL: claPlatformURL, defaultGracePeriodDays: defaultGracePeriodDays, + stop: make(chan struct{}), + corpTrigger: make(chan struct{}, 1), + individualTrigger: make(chan struct{}, 1), } notifyAdminWatchInstance.start() @@ -38,6 +41,22 @@ func NotifyAdminWatchStop() { } } +// TriggerNotify 立即触发一次通知扫描,不等待定时器周期。 +// CLA 更新后应调用此函数,确保企业和个人都在最短时间内收到通知。 +func TriggerNotify() { + if notifyAdminWatchInstance == nil { + return + } + select { + case notifyAdminWatchInstance.corpTrigger <- struct{}{}: + default: + } + select { + case notifyAdminWatchInstance.individualTrigger <- struct{}{}: + default: + } +} + type corpSigningRepo interface { FindAll(linkId string) ([]repository.CorpSigningSummary, error) UpdateCLANotify(summary *repository.CorpSigningSummary) error @@ -58,8 +77,10 @@ type notifyAdminWatchImpl struct { defaultGracePeriodDays int - wg sync.WaitGroup - stop chan struct{} + wg sync.WaitGroup + stop chan struct{} + corpTrigger chan struct{} + individualTrigger chan struct{} } func (impl *notifyAdminWatchImpl) start() { @@ -88,6 +109,9 @@ func (impl *notifyAdminWatchImpl) notifyCorpAdmin() { case <-timer.C: impl.handleNotifyJob() timer.Reset(interval) + case <-impl.corpTrigger: + impl.handleNotifyJob() + timer.Reset(interval) } } } @@ -233,6 +257,9 @@ func (impl *notifyAdminWatchImpl) notifyIndividualSigner() { case <-timer.C: impl.handleIndividualNotifyJob() timer.Reset(interval) + case <-impl.individualTrigger: + impl.handleIndividualNotifyJob() + timer.Reset(interval) } } } From 0c098c2bbf526b81cf6efc3a40756899f3629e30 Mon Sep 17 00:00:00 2001 From: JavaPythonAIForBAT Date: Wed, 15 Jul 2026 10:21:56 +0800 Subject: [PATCH 12/19] =?UTF-8?q?feat:=20=E9=80=9A=E7=9F=A5=E6=89=AB?= =?UTF-8?q?=E6=8F=8F=E6=94=B9=E4=B8=BA=E6=AF=8F=E6=97=A5=E5=A4=9C=E9=97=B4?= =?UTF-8?q?=E9=9B=B6=E7=82=B9=E6=89=A7=E8=A1=8C=EF=BC=8C=E5=87=8F=E5=B0=91?= =?UTF-8?q?=E4=B8=8D=E5=BF=85=E8=A6=81=E7=9A=84=E9=A2=91=E7=B9=81=E6=89=AB?= =?UTF-8?q?=E6=8F=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 企业和个人的定时扫描间隔统一从 20分钟/24小时 改为每日凌晨 0:00 - 首次启动自动计算到下一个午夜的时间 - CLA 更新后仍通过 TriggerNotify() 立即触发,不受午夜限制 - 触发后定时器也重置为下一个午夜,确保日常扫描始终在零点 --- signing/watch/config.go | 2 +- signing/watch/notify_corp_admin.go | 20 ++++++++++++-------- 2 files changed, 13 insertions(+), 9 deletions(-) diff --git a/signing/watch/config.go b/signing/watch/config.go index f7c062ea..c62d7cf3 100644 --- a/signing/watch/config.go +++ b/signing/watch/config.go @@ -64,7 +64,7 @@ func (cfg *NotifyAdminConfig) SetDefault() { } if cfg.NotifyCorpAdminInterval <= 0 { - cfg.NotifyCorpAdminInterval = 1200 + cfg.NotifyCorpAdminInterval = 86400 } if cfg.NotifyIndividualInterval <= 0 { diff --git a/signing/watch/notify_corp_admin.go b/signing/watch/notify_corp_admin.go index e953d850..8842abff 100644 --- a/signing/watch/notify_corp_admin.go +++ b/signing/watch/notify_corp_admin.go @@ -98,8 +98,7 @@ func (impl *notifyAdminWatchImpl) exit() { } func (impl *notifyAdminWatchImpl) notifyCorpAdmin() { - interval := impl.config.genNotifyCorpAdminInterval() - timer := time.NewTimer(interval) + timer := time.NewTimer(timeUntilMidnight()) for { select { case <-impl.stop: @@ -108,10 +107,10 @@ func (impl *notifyAdminWatchImpl) notifyCorpAdmin() { return case <-timer.C: impl.handleNotifyJob() - timer.Reset(interval) + timer.Reset(timeUntilMidnight()) case <-impl.corpTrigger: impl.handleNotifyJob() - timer.Reset(interval) + timer.Reset(timeUntilMidnight()) } } } @@ -246,8 +245,7 @@ func (impl *notifyAdminWatchImpl) handleSendEmail(link *repository.LinkCLA, corp } func (impl *notifyAdminWatchImpl) notifyIndividualSigner() { - interval := impl.config.genNotifyIndividualInterval() - timer := time.NewTimer(interval) + timer := time.NewTimer(timeUntilMidnight()) for { select { case <-impl.stop: @@ -256,10 +254,10 @@ func (impl *notifyAdminWatchImpl) notifyIndividualSigner() { return case <-timer.C: impl.handleIndividualNotifyJob() - timer.Reset(interval) + timer.Reset(timeUntilMidnight()) case <-impl.individualTrigger: impl.handleIndividualNotifyJob() - timer.Reset(interval) + timer.Reset(timeUntilMidnight()) } } } @@ -431,3 +429,9 @@ func (impl *notifyAdminWatchImpl) rootURL() string { } return fmt.Sprintf("%s://%s", u.Scheme, u.Host) } + +func timeUntilMidnight() time.Duration { + now := time.Now() + midnight := time.Date(now.Year(), now.Month(), now.Day()+1, 0, 0, 0, 0, now.Location()) + return midnight.Sub(now) +} From 63c69979e1be409d8f7b04e573647981c6a60469 Mon Sep 17 00:00:00 2001 From: JavaPythonAIForBAT Date: Wed, 15 Jul 2026 10:27:34 +0800 Subject: [PATCH 13/19] =?UTF-8?q?feat:=20=E5=AE=9A=E6=97=B6=E6=89=AB?= =?UTF-8?q?=E6=8F=8F=E6=94=B9=E4=B8=BA=E6=AF=8F=E6=97=A5=E4=B8=AD=E5=8D=88?= =?UTF-8?q?12=E7=82=B9=E5=92=8C=E5=87=8C=E6=99=A80=E7=82=B9=E5=90=84?= =?UTF-8?q?=E6=89=A7=E8=A1=8C=E4=B8=80=E6=AC=A1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- signing/watch/notify_corp_admin.go | 21 ++++++++++++++------- 1 file changed, 14 insertions(+), 7 deletions(-) diff --git a/signing/watch/notify_corp_admin.go b/signing/watch/notify_corp_admin.go index 8842abff..6bad8fe4 100644 --- a/signing/watch/notify_corp_admin.go +++ b/signing/watch/notify_corp_admin.go @@ -98,7 +98,7 @@ func (impl *notifyAdminWatchImpl) exit() { } func (impl *notifyAdminWatchImpl) notifyCorpAdmin() { - timer := time.NewTimer(timeUntilMidnight()) + timer := time.NewTimer(nextNoonOrMidnight()) for { select { case <-impl.stop: @@ -107,10 +107,10 @@ func (impl *notifyAdminWatchImpl) notifyCorpAdmin() { return case <-timer.C: impl.handleNotifyJob() - timer.Reset(timeUntilMidnight()) + timer.Reset(12 * time.Hour) case <-impl.corpTrigger: impl.handleNotifyJob() - timer.Reset(timeUntilMidnight()) + timer.Reset(nextNoonOrMidnight()) } } } @@ -245,7 +245,7 @@ func (impl *notifyAdminWatchImpl) handleSendEmail(link *repository.LinkCLA, corp } func (impl *notifyAdminWatchImpl) notifyIndividualSigner() { - timer := time.NewTimer(timeUntilMidnight()) + timer := time.NewTimer(nextNoonOrMidnight()) for { select { case <-impl.stop: @@ -254,10 +254,10 @@ func (impl *notifyAdminWatchImpl) notifyIndividualSigner() { return case <-timer.C: impl.handleIndividualNotifyJob() - timer.Reset(timeUntilMidnight()) + timer.Reset(12 * time.Hour) case <-impl.individualTrigger: impl.handleIndividualNotifyJob() - timer.Reset(timeUntilMidnight()) + timer.Reset(nextNoonOrMidnight()) } } } @@ -430,8 +430,15 @@ func (impl *notifyAdminWatchImpl) rootURL() string { return fmt.Sprintf("%s://%s", u.Scheme, u.Host) } -func timeUntilMidnight() time.Duration { +func nextNoonOrMidnight() time.Duration { now := time.Now() + noon := time.Date(now.Year(), now.Month(), now.Day(), 12, 0, 0, 0, now.Location()) + if now.After(noon) { + noon = noon.Add(24 * time.Hour) + } midnight := time.Date(now.Year(), now.Month(), now.Day()+1, 0, 0, 0, 0, now.Location()) + if noon.Before(midnight) { + return noon.Sub(now) + } return midnight.Sub(now) } From 4b413f48242ab8cf9cfb6b9fd6c885ec65c8f2c5 Mon Sep 17 00:00:00 2001 From: JavaPythonAIForBAT Date: Wed, 15 Jul 2026 10:33:04 +0800 Subject: [PATCH 14/19] =?UTF-8?q?fix:=20=E4=BF=AE=E5=A4=8D=E9=82=AE?= =?UTF-8?q?=E4=BB=B6=E9=80=9A=E7=9F=A5=E9=A6=96=E6=AC=A1=E8=A2=AB=E8=B7=B3?= =?UTF-8?q?=E8=BF=87=E7=AD=893=E4=B8=AA=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 1. 移除错误的存量兼容检查,避免 CLA 更新后首次通知被跳过 SetPendingCLAForLink 设置 count=0 后,兼容检查误判为已通知 2. isIndividualSigningLatest 默认值从 true 改为 false,与 isCorpSigningLatest 一致 3. SetPendingCLAForLink 失败不再阻断 CLA 更新成功响应,改为仅记录日志 4. 定时器重置统一为 nextNoonOrMidnight,避免时钟漂移 --- signing/app/cla.go | 4 +++- signing/watch/notify_corp_admin.go | 29 ++++++----------------------- 2 files changed, 9 insertions(+), 24 deletions(-) diff --git a/signing/app/cla.go b/signing/app/cla.go index 84ebb7c2..1e41b4a6 100644 --- a/signing/app/cla.go +++ b/signing/app/cla.go @@ -1,6 +1,8 @@ package app import ( + "github.com/beego/beego/v2/core/logs" + "github.com/opensourceways/app-cla-server/signing/domain" "github.com/opensourceways/app-cla-server/signing/domain/claservice" "github.com/opensourceways/app-cla-server/signing/domain/dp" @@ -61,7 +63,7 @@ func (s *claService) Update(cmd *CmdToUpdateCLA) error { } if err = s.cs.SetPendingCLAForLink(cmd.LinkId, cla.Id); err != nil { - return err + logs.Error("set pending CLA for link failed: %s, err: %v", cmd.LinkId, err) } watch.TriggerNotify() diff --git a/signing/watch/notify_corp_admin.go b/signing/watch/notify_corp_admin.go index 6bad8fe4..102933f8 100644 --- a/signing/watch/notify_corp_admin.go +++ b/signing/watch/notify_corp_admin.go @@ -107,7 +107,7 @@ func (impl *notifyAdminWatchImpl) notifyCorpAdmin() { return case <-timer.C: impl.handleNotifyJob() - timer.Reset(12 * time.Hour) + timer.Reset(nextNoonOrMidnight()) case <-impl.corpTrigger: impl.handleNotifyJob() timer.Reset(nextNoonOrMidnight()) @@ -172,15 +172,6 @@ func (impl *notifyAdminWatchImpl) handleCorpSigning(link *repository.LinkCLA, co corp.ClaNotifyTime = 0 } - if corp.ClaNotifyTime == 0 && corp.ClaNotifyCount == 0 && corp.CLANotify == latestCLA.Id { - corp.ClaNotifyCount = 1 - corp.ClaNotifyTime = nowUnix - if err := impl.corpSigningRepo.UpdateCLANotify(corp); err != nil { - logs.Error("init cla_notify_count/time failed: ", corp.Id, err) - } - return - } - daysSinceLastNotify := 0 if corp.ClaNotifyTime > 0 { daysSinceLastNotify = int((nowUnix - corp.ClaNotifyTime) / 86400) @@ -254,7 +245,7 @@ func (impl *notifyAdminWatchImpl) notifyIndividualSigner() { return case <-timer.C: impl.handleIndividualNotifyJob() - timer.Reset(12 * time.Hour) + timer.Reset(nextNoonOrMidnight()) case <-impl.individualTrigger: impl.handleIndividualNotifyJob() timer.Reset(nextNoonOrMidnight()) @@ -321,16 +312,6 @@ func (impl *notifyAdminWatchImpl) handleIndividualSigning(link *repository.LinkC is.ClaNotifyTime = 0 } - if is.ClaNotifyTime == 0 && is.ClaNotifyCount == 0 && is.ClaNotify == latestCLA.Id { - is.ClaNotifyCount = 1 - is.ClaNotifyTime = nowUnix - if err := impl.individualRepo.UpdateCLANotify(is); err != nil { - logs.Error("init cla_notify_count/time failed: ", is.Rep.EmailAddr.EmailAddr(), err) - } - return - } - - // 计算距离上次通知的天数 daysSinceLastNotify := 0 if is.ClaNotifyTime > 0 { daysSinceLastNotify = int((nowUnix - is.ClaNotifyTime) / 86400) @@ -354,13 +335,15 @@ func (impl *notifyAdminWatchImpl) handleIndividualSigning(link *repository.LinkC } func (impl *notifyAdminWatchImpl) isIndividualSigningLatest(latestCLAs []domain.CLA, signedInfo domain.CLAInfo) bool { + var matchedCLA *domain.CLA for i := range latestCLAs { if latestCLAs[i].Type == dp.CLATypeIndividual && latestCLAs[i].Language == signedInfo.Language { - return latestCLAs[i].Id == signedInfo.CLAId + matchedCLA = &latestCLAs[i] + break } } - return true + return matchedCLA != nil && matchedCLA.Id == signedInfo.CLAId } func (impl *notifyAdminWatchImpl) getLatestIndividualCLA(clas []domain.CLA, language dp.Language) *domain.CLA { From fe5b011a3d5871c29726100c5f47fe79c154a74e Mon Sep 17 00:00:00 2001 From: JavaPythonAIForBAT Date: Wed, 15 Jul 2026 12:42:11 +0800 Subject: [PATCH 15/19] =?UTF-8?q?feat:=20=E9=80=9A=E7=9F=A5=E6=89=AB?= =?UTF-8?q?=E6=8F=8F=E5=8A=A0=E5=85=A5=E6=89=B9=E6=AC=A1=E4=B8=8A=E9=99=90?= =?UTF-8?q?=EF=BC=8C=E6=AF=8F=E8=BD=AE=E6=9C=80=E5=A4=9A=E5=8F=91=E9=80=81?= =?UTF-8?q?N=E4=BA=BA=EF=BC=8C=E5=8F=AF=E9=85=8D=E7=BD=AE=E9=BB=98?= =?UTF-8?q?=E8=AE=A4500?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - NotifyBatchSize 配置项,默认 500,控制每轮定时扫描最大发送数 - handleCorpSigning/handleIndividualSigning 返回 bool 表示是否发送 - handleNotifyJob/handleIndividualNotifyJob 到达批次上限后停止本轮 - 下一轮扫描自动续上:已发送的跳过,未发送的继续 --- signing/watch/config.go | 9 ++++++ signing/watch/notify_corp_admin.go | 46 ++++++++++++++++++++++-------- 2 files changed, 43 insertions(+), 12 deletions(-) diff --git a/signing/watch/config.go b/signing/watch/config.go index c62d7cf3..0cdcfb4f 100644 --- a/signing/watch/config.go +++ b/signing/watch/config.go @@ -56,6 +56,7 @@ type NotifyAdminConfig struct { NotifyIndividualInterval int `json:"notify_individual_interval"` NotifyIndividualRemindDays int `json:"notify_individual_remind_days"` NotifyCorpAdminRemindDays int `json:"notify_corp_admin_remind_days"` + NotifyBatchSize int `json:"notify_batch_size"` } func (cfg *NotifyAdminConfig) SetDefault() { @@ -78,6 +79,10 @@ func (cfg *NotifyAdminConfig) SetDefault() { if cfg.NotifyCorpAdminRemindDays <= 0 { cfg.NotifyCorpAdminRemindDays = 90 } + + if cfg.NotifyBatchSize <= 0 { + cfg.NotifyBatchSize = 500 + } } func (cfg *NotifyAdminConfig) genSendEmailInterval() time.Duration { @@ -99,3 +104,7 @@ func (cfg *NotifyAdminConfig) genNotifyIndividualRemindDays() int { func (cfg *NotifyAdminConfig) genNotifyCorpAdminRemindDays() int { return cfg.NotifyCorpAdminRemindDays } + +func (cfg *NotifyAdminConfig) genNotifyBatchSize() int { + return cfg.NotifyBatchSize +} diff --git a/signing/watch/notify_corp_admin.go b/signing/watch/notify_corp_admin.go index 102933f8..eac6a38b 100644 --- a/signing/watch/notify_corp_admin.go +++ b/signing/watch/notify_corp_admin.go @@ -125,6 +125,9 @@ func (impl *notifyAdminWatchImpl) handleNotifyJob() { } } + sendCount := 0 + batchSize := impl.config.genNotifyBatchSize() + links, err := impl.link.ListAll() if err != nil { logs.Error("list all link failed in notify job: ", err) @@ -147,20 +150,26 @@ func (impl *notifyAdminWatchImpl) handleNotifyJob() { if needStop() { return } + if sendCount >= batchSize { + logs.Info("corp notify job reached batch limit: %d", batchSize) + return + } - impl.handleCorpSigning(link, &corpsSummary[j]) + if impl.handleCorpSigning(link, &corpsSummary[j]) { + sendCount++ + } } } } -func (impl *notifyAdminWatchImpl) handleCorpSigning(link *repository.LinkCLA, corp *repository.CorpSigningSummary) { +func (impl *notifyAdminWatchImpl) handleCorpSigning(link *repository.LinkCLA, corp *repository.CorpSigningSummary) bool { if impl.isCorpSigningLatest(link.Clas, corp.Link.CLAInfo) { - return + return false } latestCLA := impl.getLatestCorpCLA(link.Clas, corp.Link.Language) if latestCLA == nil { - return + return false } remindDays := impl.config.genNotifyCorpAdminRemindDays() @@ -178,12 +187,12 @@ func (impl *notifyAdminWatchImpl) handleCorpSigning(link *repository.LinkCLA, co } if daysSinceLastNotify < remindDays && corp.ClaNotifyCount > 0 { - return + return false } if err := impl.handleSendEmail(link, corp); err != nil { logs.Error("send cla notify email failed:", corp.Id, err) - return + return false } corp.ClaNotifyCount++ @@ -191,6 +200,8 @@ func (impl *notifyAdminWatchImpl) handleCorpSigning(link *repository.LinkCLA, co if err := impl.corpSigningRepo.UpdateCLANotify(corp); err != nil { logs.Error("update cla notify failed: ", corp.Id, err) } + + return true } func (impl *notifyAdminWatchImpl) isCorpSigningLatest(latestCLAs []domain.CLA, signedInfo domain.CLAInfo) bool { @@ -263,6 +274,9 @@ func (impl *notifyAdminWatchImpl) handleIndividualNotifyJob() { } } + sendCount := 0 + batchSize := impl.config.genNotifyBatchSize() + links, err := impl.link.ListAll() if err != nil { logs.Error("list all link failed in individual notify job: ", err) @@ -285,22 +299,28 @@ func (impl *notifyAdminWatchImpl) handleIndividualNotifyJob() { if needStop() { return } + if sendCount >= batchSize { + logs.Info("individual notify job reached batch limit: %d", batchSize) + return + } - impl.handleIndividualSigning(link, &individuals[j]) + if impl.handleIndividualSigning(link, &individuals[j]) { + sendCount++ + } } } } -func (impl *notifyAdminWatchImpl) handleIndividualSigning(link *repository.LinkCLA, is *domain.IndividualSigning) { +func (impl *notifyAdminWatchImpl) handleIndividualSigning(link *repository.LinkCLA, is *domain.IndividualSigning) bool { // 检查是否签署了最新版本 if impl.isIndividualSigningLatest(link.Clas, is.Link.CLAInfo) { - return + return false } // 获取当前 CLA 的更新时间 latestCLA := impl.getLatestIndividualCLA(link.Clas, is.Link.Language) if latestCLA == nil { - return + return false } remindDays := impl.config.genNotifyIndividualRemindDays() @@ -319,12 +339,12 @@ func (impl *notifyAdminWatchImpl) handleIndividualSigning(link *repository.LinkC // 如果距上次通知不足 remindDays 天,跳过 if daysSinceLastNotify < remindDays && is.ClaNotifyCount > 0 { - return + return false } if err := impl.handleSendIndividualEmail(link, is, latestCLA); err != nil { logs.Error("send individual cla notify email failed:", is.Rep.EmailAddr.EmailAddr(), err) - return + return false } is.ClaNotifyCount++ @@ -332,6 +352,8 @@ func (impl *notifyAdminWatchImpl) handleIndividualSigning(link *repository.LinkC if err := impl.individualRepo.UpdateCLANotify(is); err != nil { logs.Error("update individual cla notify failed: ", is.Rep.EmailAddr.EmailAddr(), err) } + + return true } func (impl *notifyAdminWatchImpl) isIndividualSigningLatest(latestCLAs []domain.CLA, signedInfo domain.CLAInfo) bool { From 93835fddf05537ce7d5ce39c1abea2e604f95d1b Mon Sep 17 00:00:00 2001 From: JavaPythonAIForBAT Date: Wed, 15 Jul 2026 13:01:43 +0800 Subject: [PATCH 16/19] =?UTF-8?q?fix:=20=E6=B5=8B=E8=AF=95=E7=94=A8?= =?UTF-8?q?=E4=BE=8B=E8=A1=A5=E5=85=85=20NotifyAdminConfig=20=E9=81=BF?= =?UTF-8?q?=E5=85=8D=20nil=20panic?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- signing/watch/notify_corp_admin_test.go | 23 +++++++++++++++++------ 1 file changed, 17 insertions(+), 6 deletions(-) diff --git a/signing/watch/notify_corp_admin_test.go b/signing/watch/notify_corp_admin_test.go index 804e1064..27e9476e 100644 --- a/signing/watch/notify_corp_admin_test.go +++ b/signing/watch/notify_corp_admin_test.go @@ -8,8 +8,19 @@ import ( "github.com/opensourceways/app-cla-server/signing/domain/repository" ) +func newTestImpl() *notifyAdminWatchImpl { + return ¬ifyAdminWatchImpl{ + config: &NotifyAdminConfig{ + NotifyCorpAdminRemindDays: 90, + NotifyIndividualRemindDays: 90, + NotifyBatchSize: 500, + }, + defaultGracePeriodDays: 30, + } +} + func TestHandleCorpSigningNoPDF(t *testing.T) { - impl := ¬ifyAdminWatchImpl{defaultGracePeriodDays: 30} + impl := newTestImpl() link := &repository.LinkCLA{Id: "link1"} corp := &repository.CorpSigningSummary{HasPDF: false} @@ -18,7 +29,7 @@ func TestHandleCorpSigningNoPDF(t *testing.T) { } func TestHandleCorpSigningAlreadyLatest(t *testing.T) { - impl := ¬ifyAdminWatchImpl{defaultGracePeriodDays: 30} + impl := newTestImpl() link := &repository.LinkCLA{ Id: "link1", @@ -35,7 +46,7 @@ func TestHandleCorpSigningAlreadyLatest(t *testing.T) { } func TestHandleCorpSigningNoLatestCorpCLA(t *testing.T) { - impl := ¬ifyAdminWatchImpl{defaultGracePeriodDays: 30} + impl := newTestImpl() link := &repository.LinkCLA{ Id: "link1", @@ -50,7 +61,7 @@ func TestHandleCorpSigningNoLatestCorpCLA(t *testing.T) { } func TestHandleCorpSigningNotifyWithin7Days(t *testing.T) { - impl := ¬ifyAdminWatchImpl{defaultGracePeriodDays: 30} + impl := newTestImpl() link := &repository.LinkCLA{ Id: "link1", @@ -69,7 +80,7 @@ func TestHandleCorpSigningNotifyWithin7Days(t *testing.T) { } func TestHandleIndividualSigningAlreadyLatest(t *testing.T) { - impl := ¬ifyAdminWatchImpl{defaultGracePeriodDays: 30} + impl := newTestImpl() link := &repository.LinkCLA{ Id: "link1", @@ -86,7 +97,7 @@ func TestHandleIndividualSigningAlreadyLatest(t *testing.T) { } func TestHandleIndividualSigningNoLatestCLA(t *testing.T) { - impl := ¬ifyAdminWatchImpl{defaultGracePeriodDays: 30} + impl := newTestImpl() link := &repository.LinkCLA{ Id: "link1", From 3a87b69572d805f9ad1dc9548e1bbcf983ca014a Mon Sep 17 00:00:00 2001 From: JavaPythonAIForBAT Date: Wed, 15 Jul 2026 15:57:04 +0800 Subject: [PATCH 17/19] =?UTF-8?q?feat:=20=E9=80=9A=E7=9F=A5=E6=89=AB?= =?UTF-8?q?=E6=8F=8F=E6=94=AF=E6=8C=81=E5=8F=AF=E9=85=8D=E5=AE=9A=E6=97=B6?= =?UTF-8?q?=E9=97=B4=E9=9A=94=E4=B8=8E=E7=A4=BE=E5=8C=BA=E7=99=BD=E5=90=8D?= =?UTF-8?q?=E5=8D=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 新增 enabled_community_orgs 白名单配置,仅对名单内社区发送通知 列表为空时默认通知全部社区(逐渐开放,可控上线) - 定时扫描间隔变为可配置: 配置 notify_corp_admin_interval/notify_individual_interval != 86400 → 直接使用该间隔(测试环境可设为60秒快速验证) 配置 == 86400 → 生产模式:首次对齐中午/凌晨,后续每12小时一次 - handleNotifyJob/handleIndividualNotifyJob 增加社区过滤逻辑 --- signing/watch/config.go | 27 ++++++++++++++----- signing/watch/notify_corp_admin.go | 42 +++++++++++++++++++++++++----- 2 files changed, 57 insertions(+), 12 deletions(-) diff --git a/signing/watch/config.go b/signing/watch/config.go index 0cdcfb4f..b0f24f93 100644 --- a/signing/watch/config.go +++ b/signing/watch/config.go @@ -51,12 +51,13 @@ func (cfg *CLAUpdateConfig) genAllDiffInterval() time.Duration { } 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"` - NotifyBatchSize int `json:"notify_batch_size"` + 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"` + NotifyBatchSize int `json:"notify_batch_size"` + EnabledCommunityOrgs []string `json:"enabled_community_orgs"` } func (cfg *NotifyAdminConfig) SetDefault() { @@ -108,3 +109,17 @@ func (cfg *NotifyAdminConfig) genNotifyCorpAdminRemindDays() int { func (cfg *NotifyAdminConfig) genNotifyBatchSize() int { return cfg.NotifyBatchSize } + +func (cfg *NotifyAdminConfig) isCommunityEnabled(alias string) bool { + if len(cfg.EnabledCommunityOrgs) == 0 { + return true + } + + for _, org := range cfg.EnabledCommunityOrgs { + if org == alias { + return true + } + } + + return false +} diff --git a/signing/watch/notify_corp_admin.go b/signing/watch/notify_corp_admin.go index eac6a38b..239c92ea 100644 --- a/signing/watch/notify_corp_admin.go +++ b/signing/watch/notify_corp_admin.go @@ -98,7 +98,8 @@ func (impl *notifyAdminWatchImpl) exit() { } func (impl *notifyAdminWatchImpl) notifyCorpAdmin() { - timer := time.NewTimer(nextNoonOrMidnight()) + interval := impl.config.genNotifyCorpAdminInterval() + timer := time.NewTimer(impl.initialDelay(interval)) for { select { case <-impl.stop: @@ -107,10 +108,10 @@ func (impl *notifyAdminWatchImpl) notifyCorpAdmin() { return case <-timer.C: impl.handleNotifyJob() - timer.Reset(nextNoonOrMidnight()) + timer.Reset(impl.nextDelay(interval)) case <-impl.corpTrigger: impl.handleNotifyJob() - timer.Reset(nextNoonOrMidnight()) + timer.Reset(impl.nextDelay(interval)) } } } @@ -140,6 +141,11 @@ func (impl *notifyAdminWatchImpl) handleNotifyJob() { } link := &links[i] + + if !impl.config.isCommunityEnabled(link.Org.Alias) { + continue + } + corpsSummary, err := impl.corpSigningRepo.FindAll(link.Id) if err != nil { logs.Error("list corp signing failed in notify job:", link.Id, err) @@ -247,7 +253,8 @@ func (impl *notifyAdminWatchImpl) handleSendEmail(link *repository.LinkCLA, corp } func (impl *notifyAdminWatchImpl) notifyIndividualSigner() { - timer := time.NewTimer(nextNoonOrMidnight()) + interval := impl.config.genNotifyIndividualInterval() + timer := time.NewTimer(impl.initialDelay(interval)) for { select { case <-impl.stop: @@ -256,10 +263,10 @@ func (impl *notifyAdminWatchImpl) notifyIndividualSigner() { return case <-timer.C: impl.handleIndividualNotifyJob() - timer.Reset(nextNoonOrMidnight()) + timer.Reset(impl.nextDelay(interval)) case <-impl.individualTrigger: impl.handleIndividualNotifyJob() - timer.Reset(nextNoonOrMidnight()) + timer.Reset(impl.nextDelay(interval)) } } } @@ -289,6 +296,11 @@ func (impl *notifyAdminWatchImpl) handleIndividualNotifyJob() { } link := &links[i] + + if !impl.config.isCommunityEnabled(link.Org.Alias) { + continue + } + individuals, err := impl.individualRepo.FindAll(link.Id) if err != nil { logs.Error("list individual signing failed in notify job:", link.Id, err) @@ -447,3 +459,21 @@ func nextNoonOrMidnight() time.Duration { } return midnight.Sub(now) } + +// initialDelay 计算首次扫描的延迟。若 interval 为默认 86400(生产模式), +// 使用中午/凌晨对齐策略;否则直接使用配置的 interval(测试环境可配为短间隔)。 +func (impl *notifyAdminWatchImpl) initialDelay(interval time.Duration) time.Duration { + if interval >= 86400*time.Second { + return nextNoonOrMidnight() + } + return interval +} + +// nextDelay 计算后续扫描的间隔。生产模式用 12 小时(保证每日 2 次), +// 测试模式直接使用配置的 interval。 +func (impl *notifyAdminWatchImpl) nextDelay(interval time.Duration) time.Duration { + if interval >= 86400*time.Second { + return 12 * time.Hour + } + return interval +} From f61c16762f19e4ead2f8ef7164865c06a70dbf39 Mon Sep 17 00:00:00 2001 From: JavaPythonAIForBAT Date: Wed, 15 Jul 2026 16:32:50 +0800 Subject: [PATCH 18/19] =?UTF-8?q?feat:=20=E6=8F=90=E9=86=92=E9=97=B4?= =?UTF-8?q?=E9=9A=94=E6=94=AF=E6=8C=81=E7=A7=92=E7=BA=A7=E7=B2=BE=E5=BA=A6?= =?UTF-8?q?=EF=BC=8C=E6=96=B0=E5=A2=9E=20remind=5Fseconds=20=E5=8F=AF?= =?UTF-8?q?=E9=85=8D=E5=AD=97=E6=AE=B5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - notify_corp_admin_remind_seconds / notify_individual_remind_seconds 当设置为正数时覆盖 remind_days(秒级精度),方便测试验证 - 内部比较从按天改为按秒,配置为0时回退到 remind_days * 86400 换算 - 测试环境可配为300秒(5分钟),生产保持90天 --- signing/watch/config.go | 30 +++++++++++++++++++++++------- signing/watch/notify_corp_admin.go | 21 ++++++--------------- 2 files changed, 29 insertions(+), 22 deletions(-) diff --git a/signing/watch/config.go b/signing/watch/config.go index b0f24f93..d4718330 100644 --- a/signing/watch/config.go +++ b/signing/watch/config.go @@ -51,13 +51,15 @@ func (cfg *CLAUpdateConfig) genAllDiffInterval() time.Duration { } 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"` - NotifyBatchSize int `json:"notify_batch_size"` - EnabledCommunityOrgs []string `json:"enabled_community_orgs"` + 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"` + NotifyCorpAdminRemindSeconds int `json:"notify_corp_admin_remind_seconds"` + NotifyIndividualRemindSeconds int `json:"notify_individual_remind_seconds"` + NotifyBatchSize int `json:"notify_batch_size"` + EnabledCommunityOrgs []string `json:"enabled_community_orgs"` } func (cfg *NotifyAdminConfig) SetDefault() { @@ -106,6 +108,20 @@ func (cfg *NotifyAdminConfig) genNotifyCorpAdminRemindDays() int { return cfg.NotifyCorpAdminRemindDays } +func (cfg *NotifyAdminConfig) genNotifyCorpAdminRemindSeconds() int { + if cfg.NotifyCorpAdminRemindSeconds > 0 { + return cfg.NotifyCorpAdminRemindSeconds + } + return cfg.NotifyCorpAdminRemindDays * 86400 +} + +func (cfg *NotifyAdminConfig) genNotifyIndividualRemindSeconds() int { + if cfg.NotifyIndividualRemindSeconds > 0 { + return cfg.NotifyIndividualRemindSeconds + } + return cfg.NotifyIndividualRemindDays * 86400 +} + func (cfg *NotifyAdminConfig) genNotifyBatchSize() int { return cfg.NotifyBatchSize } diff --git a/signing/watch/notify_corp_admin.go b/signing/watch/notify_corp_admin.go index 239c92ea..86086c0e 100644 --- a/signing/watch/notify_corp_admin.go +++ b/signing/watch/notify_corp_admin.go @@ -178,7 +178,7 @@ func (impl *notifyAdminWatchImpl) handleCorpSigning(link *repository.LinkCLA, co return false } - remindDays := impl.config.genNotifyCorpAdminRemindDays() + remindSeconds := impl.config.genNotifyCorpAdminRemindSeconds() nowUnix := time.Now().Unix() if corp.CLANotify != latestCLA.Id { @@ -187,12 +187,8 @@ func (impl *notifyAdminWatchImpl) handleCorpSigning(link *repository.LinkCLA, co corp.ClaNotifyTime = 0 } - daysSinceLastNotify := 0 - if corp.ClaNotifyTime > 0 { - daysSinceLastNotify = int((nowUnix - corp.ClaNotifyTime) / 86400) - } - - if daysSinceLastNotify < remindDays && corp.ClaNotifyCount > 0 { + elapsed := nowUnix - corp.ClaNotifyTime + if elapsed < int64(remindSeconds) && corp.ClaNotifyCount > 0 { return false } @@ -335,7 +331,7 @@ func (impl *notifyAdminWatchImpl) handleIndividualSigning(link *repository.LinkC return false } - remindDays := impl.config.genNotifyIndividualRemindDays() + remindSeconds := impl.config.genNotifyIndividualRemindSeconds() nowUnix := time.Now().Unix() if is.ClaNotify != latestCLA.Id { @@ -344,13 +340,8 @@ func (impl *notifyAdminWatchImpl) handleIndividualSigning(link *repository.LinkC is.ClaNotifyTime = 0 } - daysSinceLastNotify := 0 - if is.ClaNotifyTime > 0 { - daysSinceLastNotify = int((nowUnix - is.ClaNotifyTime) / 86400) - } - - // 如果距上次通知不足 remindDays 天,跳过 - if daysSinceLastNotify < remindDays && is.ClaNotifyCount > 0 { + elapsed := nowUnix - is.ClaNotifyTime + if elapsed < int64(remindSeconds) && is.ClaNotifyCount > 0 { return false } From 32413ea56a78b62ee3545d0d935a939606ed0e2f Mon Sep 17 00:00:00 2001 From: JavaPythonAIForBAT Date: Wed, 15 Jul 2026 16:33:44 +0800 Subject: [PATCH 19/19] =?UTF-8?q?Revert=20"feat:=20=E6=8F=90=E9=86=92?= =?UTF-8?q?=E9=97=B4=E9=9A=94=E6=94=AF=E6=8C=81=E7=A7=92=E7=BA=A7=E7=B2=BE?= =?UTF-8?q?=E5=BA=A6=EF=BC=8C=E6=96=B0=E5=A2=9E=20remind=5Fseconds=20?= =?UTF-8?q?=E5=8F=AF=E9=85=8D=E5=AD=97=E6=AE=B5"?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This reverts commit f61c16762f19e4ead2f8ef7164865c06a70dbf39. --- signing/watch/config.go | 30 +++++++----------------------- signing/watch/notify_corp_admin.go | 21 +++++++++++++++------ 2 files changed, 22 insertions(+), 29 deletions(-) diff --git a/signing/watch/config.go b/signing/watch/config.go index d4718330..b0f24f93 100644 --- a/signing/watch/config.go +++ b/signing/watch/config.go @@ -51,15 +51,13 @@ func (cfg *CLAUpdateConfig) genAllDiffInterval() time.Duration { } 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"` - NotifyCorpAdminRemindSeconds int `json:"notify_corp_admin_remind_seconds"` - NotifyIndividualRemindSeconds int `json:"notify_individual_remind_seconds"` - NotifyBatchSize int `json:"notify_batch_size"` - EnabledCommunityOrgs []string `json:"enabled_community_orgs"` + 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"` + NotifyBatchSize int `json:"notify_batch_size"` + EnabledCommunityOrgs []string `json:"enabled_community_orgs"` } func (cfg *NotifyAdminConfig) SetDefault() { @@ -108,20 +106,6 @@ func (cfg *NotifyAdminConfig) genNotifyCorpAdminRemindDays() int { return cfg.NotifyCorpAdminRemindDays } -func (cfg *NotifyAdminConfig) genNotifyCorpAdminRemindSeconds() int { - if cfg.NotifyCorpAdminRemindSeconds > 0 { - return cfg.NotifyCorpAdminRemindSeconds - } - return cfg.NotifyCorpAdminRemindDays * 86400 -} - -func (cfg *NotifyAdminConfig) genNotifyIndividualRemindSeconds() int { - if cfg.NotifyIndividualRemindSeconds > 0 { - return cfg.NotifyIndividualRemindSeconds - } - return cfg.NotifyIndividualRemindDays * 86400 -} - func (cfg *NotifyAdminConfig) genNotifyBatchSize() int { return cfg.NotifyBatchSize } diff --git a/signing/watch/notify_corp_admin.go b/signing/watch/notify_corp_admin.go index 86086c0e..239c92ea 100644 --- a/signing/watch/notify_corp_admin.go +++ b/signing/watch/notify_corp_admin.go @@ -178,7 +178,7 @@ func (impl *notifyAdminWatchImpl) handleCorpSigning(link *repository.LinkCLA, co return false } - remindSeconds := impl.config.genNotifyCorpAdminRemindSeconds() + remindDays := impl.config.genNotifyCorpAdminRemindDays() nowUnix := time.Now().Unix() if corp.CLANotify != latestCLA.Id { @@ -187,8 +187,12 @@ func (impl *notifyAdminWatchImpl) handleCorpSigning(link *repository.LinkCLA, co corp.ClaNotifyTime = 0 } - elapsed := nowUnix - corp.ClaNotifyTime - if elapsed < int64(remindSeconds) && corp.ClaNotifyCount > 0 { + daysSinceLastNotify := 0 + if corp.ClaNotifyTime > 0 { + daysSinceLastNotify = int((nowUnix - corp.ClaNotifyTime) / 86400) + } + + if daysSinceLastNotify < remindDays && corp.ClaNotifyCount > 0 { return false } @@ -331,7 +335,7 @@ func (impl *notifyAdminWatchImpl) handleIndividualSigning(link *repository.LinkC return false } - remindSeconds := impl.config.genNotifyIndividualRemindSeconds() + remindDays := impl.config.genNotifyIndividualRemindDays() nowUnix := time.Now().Unix() if is.ClaNotify != latestCLA.Id { @@ -340,8 +344,13 @@ func (impl *notifyAdminWatchImpl) handleIndividualSigning(link *repository.LinkC is.ClaNotifyTime = 0 } - elapsed := nowUnix - is.ClaNotifyTime - if elapsed < int64(remindSeconds) && is.ClaNotifyCount > 0 { + daysSinceLastNotify := 0 + if is.ClaNotifyTime > 0 { + daysSinceLastNotify = int((nowUnix - is.ClaNotifyTime) / 86400) + } + + // 如果距上次通知不足 remindDays 天,跳过 + if daysSinceLastNotify < remindDays && is.ClaNotifyCount > 0 { return false }