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/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/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/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/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..618e4bf4 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) @@ -46,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 @@ -103,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/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/cla.go b/signing/app/cla.go index dd3742bc..1e41b4a6 100644 --- a/signing/app/cla.go +++ b/signing/app/cla.go @@ -1,10 +1,13 @@ 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" "github.com/opensourceways/app-cla-server/signing/domain/repository" + "github.com/opensourceways/app-cla-server/signing/watch" ) func NewCLAService( @@ -55,7 +58,17 @@ 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 + } + + if err = s.cs.SetPendingCLAForLink(cmd.LinkId, cla.Id); err != nil { + logs.Error("set pending CLA for link failed: %s, err: %v", cmd.LinkId, err) + } + + watch.TriggerNotify() + + return nil } func (s *claService) Remove(cmd *CmdToRemoveCLA) 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..2f42342b 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 @@ -188,15 +181,48 @@ func (s *claService) AddLink(link *domain.Link) error { } func (s *claService) ContainsCla(linkId, claId string) bool { + if s.linkCache.contains(linkId, claId) { + return true + } + + // 缓存未命中时兜底查询数据库,防止缓存因服务未重启等原因落后于数据库 + 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) GetClaId(linkId string, claType dp.CLAType, language dp.Language) string { - return s.linkCache.getClaId(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) { 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..45e3401f 100644 --- a/signing/domain/repository/corp_signing.go +++ b/signing/domain/repository/corp_signing.go @@ -6,18 +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 - - PendingCLAId string + 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 { @@ -71,7 +69,6 @@ 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) + SetPendingCLAForLink(linkId, newClaId string) 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..4c494c0c 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) { @@ -70,7 +66,9 @@ func (impl *link) UpdateCLA(link *domain.Link, newCla *domain.CLA) error { update := bson.M{ fieldId: newCla.Id, - fieldUrl: newCla.URL, + fieldUrl: newCla.URL.URL(), + fieldLang: newCla.Language.Language(), + fieldType: newCla.Type.CLAType(), fieldClaUpdatedAt: newCla.UpdatedAt, } diff --git a/signing/infrastructure/repositoryimpl/corp_signing.go b/signing/infrastructure/repositoryimpl/corp_signing.go index 6517a825..26118121 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 @@ -166,9 +165,8 @@ func (impl *corpSigning) FindAll(linkId string) ([]repository.CorpSigningSummary fieldLinkId: 1, fieldHasPDF: 1, fieldCLANotify: 1, - fieldPendingCLAId: 1, - fieldCLANotifyCount: 1, - fieldCLANotifyTime: 1, + fieldClaNotifyCount: 1, + fieldClaNotifyTime: 1, } var dos []corpSigningDO @@ -205,9 +203,8 @@ func (impl *corpSigning) FindAllWithPagination(linkId string, offset, limit int) fieldLinkId: 1, fieldHasPDF: 1, fieldCLANotify: 1, - fieldPendingCLAId: 1, - fieldCLANotifyCount: 1, - fieldCLANotifyTime: 1, + fieldClaNotifyCount: 1, + fieldClaNotifyTime: 1, } var dos []corpSigningDO @@ -267,9 +264,8 @@ func (impl *corpSigning) FindPage(linkId string, intPage, intPageSize int, admin fieldLinkId: 1, fieldHasPDF: 1, fieldCLANotify: 1, - fieldPendingCLAId: 1, - fieldCLANotifyCount: 1, - fieldCLANotifyTime: 1, + fieldClaNotifyCount: 1, + fieldClaNotifyTime: 1, } // Single aggregation round-trip: $facet returns both total count and the @@ -367,20 +363,12 @@ func (impl *corpSigning) UpdateClaId(cs *domain.CorpSigning) error { return err } - setFields := bson.M{ + return impl.dao.UpdateDoc(filter, 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) + fieldCLANotify: "", + fieldClaNotifyCount: 0, + fieldClaNotifyTime: 0, + }, cs.Version) } func (impl *corpSigning) UpdateCLANotify(summary *repository.CorpSigningSummary) error { @@ -389,13 +377,11 @@ func (impl *corpSigning) UpdateCLANotify(summary *repository.CorpSigningSummary) return err } - update := bson.M{ + return impl.dao.UpdateDocsWithoutVersion(filter, bson.M{ fieldCLANotify: summary.CLANotify, - fieldCLANotifyCount: summary.ClaNotifyCount, - fieldCLANotifyTime: summary.ClaNotifyTime, - } - - return impl.dao.UpdateDocsWithoutVersion(filter, update) + fieldClaNotifyCount: summary.ClaNotifyCount, + fieldClaNotifyTime: summary.ClaNotifyTime, + }) } func (impl *corpSigning) Update(cs *domain.CorpSigning) error { @@ -404,54 +390,49 @@ 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, + fieldClaNotifyCount: 0, + fieldClaNotifyTime: 0, + }) } 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_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{ diff --git a/signing/infrastructure/repositoryimpl/corp_signing_do.go b/signing/infrastructure/repositoryimpl/corp_signing_do.go index 3f662c8c..b6562d63 100644 --- a/signing/infrastructure/repositoryimpl/corp_signing_do.go +++ b/signing/infrastructure/repositoryimpl/corp_signing_do.go @@ -10,42 +10,42 @@ 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" + 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, - 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, + ClaNotifyCount: v.ClaNotifyCount, + ClaNotifyTime: v.ClaNotifyTime, } } @@ -53,17 +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), - 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), + ClaNotifyCount: v.ClaNotifyCount, + ClaNotifyTime: v.ClaNotifyTime, } } @@ -85,14 +86,12 @@ 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"` - - 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"` + 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. Triggered bool `bson:"triggered" json:"triggered,omitempty"` } @@ -120,7 +119,6 @@ func (do *corpSigningDO) toCorpSigningSummary() repository.CorpSigningSummary { Admin: do.Admin.toManager(), HasPDF: do.HasPDF, CLANotify: do.ClaNotify, - PendingCLAId: do.PendingCLAId, ClaNotifyCount: do.ClaNotifyCount, ClaNotifyTime: do.ClaNotifyTime, } @@ -154,10 +152,8 @@ func (do *corpSigningDO) toCorpSigning() domain.CorpSigning { Managers: do.toManagers(), Employees: do.toEmployeeSignings(), Version: do.Version, - PendingCLAId: do.PendingCLAId, ClaNotifyCount: do.ClaNotifyCount, ClaNotifyTime: do.ClaNotifyTime, - Logs: do.toCorpSigningLogs(), } } @@ -257,7 +253,7 @@ func toCorpDO(v *domain.Corporation) corpDO { } domain := "" - if v.PrimaryEmailDomain != "" { + if v.PrimaryEmailDomain != "" { // 如果PrimaryEmailDomain是string,零值就是空字符串 domain = v.PrimaryEmailDomain } @@ -272,45 +268,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..b0f24f93 100644 --- a/signing/watch/config.go +++ b/signing/watch/config.go @@ -51,8 +51,13 @@ 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"` + NotifyCorpAdminRemindDays int `json:"notify_corp_admin_remind_days"` + NotifyBatchSize int `json:"notify_batch_size"` + EnabledCommunityOrgs []string `json:"enabled_community_orgs"` } func (cfg *NotifyAdminConfig) SetDefault() { @@ -61,7 +66,23 @@ func (cfg *NotifyAdminConfig) SetDefault() { } if cfg.NotifyCorpAdminInterval <= 0 { - cfg.NotifyCorpAdminInterval = 1200 + cfg.NotifyCorpAdminInterval = 86400 + } + + if cfg.NotifyIndividualInterval <= 0 { + cfg.NotifyIndividualInterval = 86400 + } + + if cfg.NotifyIndividualRemindDays <= 0 { + cfg.NotifyIndividualRemindDays = 90 + } + + if cfg.NotifyCorpAdminRemindDays <= 0 { + cfg.NotifyCorpAdminRemindDays = 90 + } + + if cfg.NotifyBatchSize <= 0 { + cfg.NotifyBatchSize = 500 } } @@ -72,3 +93,33 @@ 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 +} + +func (cfg *NotifyAdminConfig) genNotifyCorpAdminRemindDays() int { + return cfg.NotifyCorpAdminRemindDays +} + +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/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..239c92ea 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,12 +19,15 @@ 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, + stop: make(chan struct{}), + corpTrigger: make(chan struct{}, 1), + individualTrigger: make(chan struct{}, 1), } notifyAdminWatchInstance.start() @@ -37,32 +41,54 @@ 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 } 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 - stop chan struct{} + wg sync.WaitGroup + stop chan struct{} + corpTrigger chan struct{} + individualTrigger chan struct{} } func (impl *notifyAdminWatchImpl) start() { impl.wg.Add(1) go impl.notifyCorpAdmin() + + impl.wg.Add(1) + go impl.notifyIndividualSigner() } func (impl *notifyAdminWatchImpl) exit() { @@ -73,7 +99,7 @@ func (impl *notifyAdminWatchImpl) exit() { func (impl *notifyAdminWatchImpl) notifyCorpAdmin() { interval := impl.config.genNotifyCorpAdminInterval() - timer := time.NewTimer(interval) + timer := time.NewTimer(impl.initialDelay(interval)) for { select { case <-impl.stop: @@ -82,7 +108,10 @@ func (impl *notifyAdminWatchImpl) notifyCorpAdmin() { return case <-timer.C: impl.handleNotifyJob() - timer.Reset(interval) + timer.Reset(impl.nextDelay(interval)) + case <-impl.corpTrigger: + impl.handleNotifyJob() + timer.Reset(impl.nextDelay(interval)) } } } @@ -97,6 +126,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) @@ -109,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) @@ -119,45 +156,58 @@ 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++ + } } - - impl.handleIndividualSignings(link, needStop) } } -func (impl *notifyAdminWatchImpl) handleCorpSigning(link *repository.LinkCLA, corp *repository.CorpSigningSummary) { - if !corp.HasPDF { - return +func (impl *notifyAdminWatchImpl) handleCorpSigning(link *repository.LinkCLA, corp *repository.CorpSigningSummary) bool { + if impl.isCorpSigningLatest(link.Clas, corp.Link.CLAInfo) { + return false } - if impl.isCorpSigningLatest(link.Clas, corp.Link.CLAInfo) { - return + latestCLA := impl.getLatestCorpCLA(link.Clas, corp.Link.Language) + if latestCLA == nil { + return false } - latestClaId := impl.getLatestCorpClaId(link, corp.Link.Language) - if latestClaId == "" { - return + remindDays := impl.config.genNotifyCorpAdminRemindDays() + nowUnix := time.Now().Unix() + + if corp.CLANotify != latestCLA.Id { + corp.CLANotify = latestCLA.Id + corp.ClaNotifyCount = 0 + corp.ClaNotifyTime = 0 } - if corp.CLANotify == latestClaId { - if time.Since(time.Unix(corp.ClaNotifyTime, 0)) < 7*24*time.Hour { - return - } + daysSinceLastNotify := 0 + if corp.ClaNotifyTime > 0 { + daysSinceLastNotify = int((nowUnix - corp.ClaNotifyTime) / 86400) + } + + if daysSinceLastNotify < remindDays && corp.ClaNotifyCount > 0 { + return false } if err := impl.handleSendEmail(link, corp); err != nil { logs.Error("send cla notify email failed:", corp.Id, err) - return + return false } - corp.CLANotify = latestClaId - corp.ClaNotifyCount += 1 - corp.ClaNotifyTime = time.Now().Unix() + corp.ClaNotifyCount++ + corp.ClaNotifyTime = nowUnix 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 { @@ -178,15 +228,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() + "/corporation-manager-login/" + link.Id, } emailMsg, err := builder.GenEmailMsg() if err != nil { @@ -195,105 +241,184 @@ 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(impl.initialDelay(interval)) + for { + select { + case <-impl.stop: + timer.Stop() + impl.wg.Done() + return + case <-timer.C: + impl.handleIndividualNotifyJob() + timer.Reset(impl.nextDelay(interval)) + case <-impl.individualTrigger: + impl.handleIndividualNotifyJob() + timer.Reset(impl.nextDelay(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 { + 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) + return + } + + for i := range links { if needStop() { return } - individuals, err := impl.individualSigningRepo.FindAllWithPagination(link.Id, offset, limit) - if err != nil { - logs.Error("list individual signing failed in notify job:", link.Id, err) - return + link := &links[i] + + if !impl.config.isCommunityEnabled(link.Org.Alias) { + continue } - if len(individuals) == 0 { - break + individuals, err := impl.individualRepo.FindAll(link.Id) + if err != nil { + logs.Error("list individual signing failed in notify job:", link.Id, err) + continue } - for i := range individuals { + for j := range individuals { if needStop() { return } + if sendCount >= batchSize { + logs.Info("individual notify job reached batch limit: %d", batchSize) + return + } - impl.handleIndividualSigning(link, &individuals[i]) + if impl.handleIndividualSigning(link, &individuals[j]) { + sendCount++ + } } - - offset += limit } } -func (impl *notifyAdminWatchImpl) handleIndividualSigning(link *repository.LinkCLA, is *domain.IndividualSigning) { - latestClaId := impl.getLatestIndividualClaId(link, is.Link.Language) - if latestClaId == "" { - return +func (impl *notifyAdminWatchImpl) handleIndividualSigning(link *repository.LinkCLA, is *domain.IndividualSigning) bool { + // 检查是否签署了最新版本 + if impl.isIndividualSigningLatest(link.Clas, is.Link.CLAInfo) { + return false } - if is.HasSignedCLA(latestClaId) { - return + // 获取当前 CLA 的更新时间 + latestCLA := impl.getLatestIndividualCLA(link.Clas, is.Link.Language) + if latestCLA == nil { + return false } - 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() + + if is.ClaNotify != latestCLA.Id { + is.ClaNotify = latestCLA.Id + is.ClaNotifyCount = 0 + is.ClaNotifyTime = 0 + } + + daysSinceLastNotify := 0 + if is.ClaNotifyTime > 0 { + daysSinceLastNotify = int((nowUnix - is.ClaNotifyTime) / 86400) } - if err := impl.handleSendIndividualEmail(link, is); err != nil { + // 如果距上次通知不足 remindDays 天,跳过 + if daysSinceLastNotify < remindDays && is.ClaNotifyCount > 0 { + 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.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) } + + return true } -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 { + var matchedCLA *domain.CLA + for i := range latestCLAs { + if latestCLAs[i].Type == dp.CLATypeIndividual && + latestCLAs[i].Language == signedInfo.Language { + matchedCLA = &latestCLAs[i] + break + } } - 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 matchedCLA != nil && matchedCLA.Id == signedInfo.CLAId +} + +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) 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 { + name = is.Rep.Name.Name() + } + + // 格式化 CLA 更新时间 + updateDate := time.Unix(latestCLA.UpdatedAt, 0).Format("2006-01-02") + + // 构造个人签署 URL:从 claPlatformURL 提取 scheme+host,避免 /sign/sign-cla 双前缀 + // 最终格式: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{ + 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 +427,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 +436,44 @@ 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 +// 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 impl.defaultGracePeriodDays + return fmt.Sprintf("%s://%s", u.Scheme, u.Host) } -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 - } +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) +} + +// 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 "" + return interval } diff --git a/signing/watch/notify_corp_admin_test.go b/signing/watch/notify_corp_admin_test.go index f9dcd3fd..27e9476e 100644 --- a/signing/watch/notify_corp_admin_test.go +++ b/signing/watch/notify_corp_admin_test.go @@ -2,15 +2,25 @@ package watch import ( "testing" - "time" "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" ) +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} @@ -19,7 +29,7 @@ func TestHandleCorpSigningNoPDF(t *testing.T) { } func TestHandleCorpSigningAlreadyLatest(t *testing.T) { - impl := ¬ifyAdminWatchImpl{defaultGracePeriodDays: 30} + impl := newTestImpl() link := &repository.LinkCLA{ Id: "link1", @@ -36,7 +46,7 @@ func TestHandleCorpSigningAlreadyLatest(t *testing.T) { } func TestHandleCorpSigningNoLatestCorpCLA(t *testing.T) { - impl := ¬ifyAdminWatchImpl{defaultGracePeriodDays: 30} + impl := newTestImpl() link := &repository.LinkCLA{ Id: "link1", @@ -51,7 +61,7 @@ func TestHandleCorpSigningNoLatestCorpCLA(t *testing.T) { } func TestHandleCorpSigningNotifyWithin7Days(t *testing.T) { - impl := ¬ifyAdminWatchImpl{defaultGracePeriodDays: 30} + impl := newTestImpl() link := &repository.LinkCLA{ Id: "link1", @@ -61,17 +71,16 @@ 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) } func TestHandleIndividualSigningAlreadyLatest(t *testing.T) { - impl := ¬ifyAdminWatchImpl{defaultGracePeriodDays: 30} + impl := newTestImpl() link := &repository.LinkCLA{ Id: "link1", @@ -88,7 +97,7 @@ func TestHandleIndividualSigningAlreadyLatest(t *testing.T) { } func TestHandleIndividualSigningNoLatestCLA(t *testing.T) { - impl := ¬ifyAdminWatchImpl{defaultGracePeriodDays: 30} + impl := newTestImpl() link := &repository.LinkCLA{ Id: "link1",