package controllers import ( "sort" "time" "server/models" beego "github.com/beego/beego/v2/server/web" ) // ============================================================= // CRM 招投标管理 - 到期提醒(开发计划 4.1) // ============================================================= // // 不新增表、不引入定时任务:所有提醒都是「按当前时间实时计算」的, // 避免定时任务没跑导致状态陈旧。前端在招投标首页拉取近 N 天的待办。 // // 覆盖四类到期: // 1. 投标:报名截止 / 投标截止 / 开标时间 // 2. 招标:报名截止 / 投标截止 / 开标时间 // 3. 资质:证书到期 // // GET /backend/crm/bidding/reminders?days=7 // ============================================================= // BackendCrmBiddingReminderController 招投标到期提醒 type BackendCrmBiddingReminderController struct { beego.Controller } // biddingReminderItem 一条到期提醒 type biddingReminderItem struct { BizType string `json:"biz_type"` // bid | tender | qualification BizID uint64 `json:"biz_id"` // 业务记录ID Kind string `json:"kind"` // register_end | bid_end | open_bid | expire KindName string `json:"kind_name"` // 提醒类型名称 Title string `json:"title"` // 记录标题 DueDate string `json:"due_date"` // 到期时间 yyyy-MM-dd HH:mm:ss RemainDays int `json:"remain_days"` // 剩余天数,负值表示已逾期 Level string `json:"level"` // danger(<=1天/已逾期) | warning(<=3天) | info OwnerName string `json:"owner_user_name"` } // List GET /backend/crm/bidding/reminders?days=7 func (c *BackendCrmBiddingReminderController) List() { claims, err := pipelineClaims(&c.Controller) if err != nil { pipelineErr(&c.Controller, 401, 401, err.Error()) return } tid := pipelineTenantID(claims) days, _ := c.GetInt("days", 7) if days <= 0 || days > 365 { days = 7 } now := time.Now() deadline := now.AddDate(0, 0, days) items := make([]biddingReminderItem, 0, 32) // ---------- 投标侧 ---------- var bids []models.TenantCrmBiddingBid _, _ = models.Orm.QueryTable(new(models.TenantCrmBiddingBid)). Filter("tenant_id", tid). Filter("delete_time__isnull", true). Filter("status__in", 0, 1, 2, 3). All(&bids) for _, b := range bids { if !canAccessCrmRecord(claims, b.OwnerUserID, b.CreateUserID) { continue } items = append(items, pickBidReminders(b, now, deadline)...) } // ---------- 招标侧 ---------- var tenders []models.TenantCrmBiddingTender _, _ = models.Orm.QueryTable(new(models.TenantCrmBiddingTender)). Filter("tenant_id", tid). Filter("delete_time__isnull", true). Filter("status__in", 0, 1, 2, 3, 4, 5). All(&tenders) for _, t := range tenders { if !canAccessCrmRecord(claims, t.OwnerUserID, t.CreateUserID) { continue } items = append(items, pickTenderReminders(t, now, deadline)...) } // ---------- 资质到期 ---------- var quals []models.TenantCrmBiddingBidQualification _, _ = models.Orm.QueryTable(new(models.TenantCrmBiddingBidQualification)). Filter("tenant_id", tid). Filter("delete_time__isnull", true). All(&quals) for _, q := range quals { if q.ExpireDate == nil { continue } due := *q.ExpireDate // 资质到期包含「已过期」,便于用户发现失效证书 if due.After(deadline) { continue } items = append(items, biddingReminderItem{ BizType: "qualification", BizID: q.ID, Kind: "expire", KindName: "资质到期", Title: q.QualificationName, DueDate: due.Format("2006-01-02"), RemainDays: daysBetween(now, due), Level: levelOf(daysBetween(now, due)), OwnerName: q.OwnerUserName, }) } // 按到期时间升序:最紧急的排在最前 sort.SliceStable(items, func(i, j int) bool { if items[i].RemainDays != items[j].RemainDays { return items[i].RemainDays < items[j].RemainDays } return items[i].DueDate < items[j].DueDate }) pipelineOk(&c.Controller, map[string]interface{}{ "list": items, "total": len(items), "days": days, }) } // pickBidReminders 从一条投标记录中挑出落在窗口内的到期项 func pickBidReminders(b models.TenantCrmBiddingBid, now, deadline time.Time) []biddingReminderItem { out := make([]biddingReminderItem, 0, 3) add := func(kind, kindName string, due *time.Time) { if due == nil { return } d := *due if d.Before(now) || d.After(deadline) { return } out = append(out, biddingReminderItem{ BizType: "bid", BizID: b.ID, Kind: kind, KindName: kindName, Title: b.Title, DueDate: d.Format("2006-01-02 15:04:05"), RemainDays: daysBetween(now, d), Level: levelOf(daysBetween(now, d)), OwnerName: b.OwnerUserName, }) } add("register_end", "报名截止", b.RegisterEndDate) add("bid_end", "投标截止", b.BidEndDate) add("open_bid", "开标时间", b.OpenBidDate) return out } // pickTenderReminders 从一条招标记录中挑出落在窗口内的到期项 func pickTenderReminders(t models.TenantCrmBiddingTender, now, deadline time.Time) []biddingReminderItem { out := make([]biddingReminderItem, 0, 3) add := func(kind, kindName string, due *time.Time) { if due == nil { return } d := *due if d.Before(now) || d.After(deadline) { return } out = append(out, biddingReminderItem{ BizType: "tender", BizID: t.ID, Kind: kind, KindName: kindName, Title: t.Title, DueDate: d.Format("2006-01-02 15:04:05"), RemainDays: daysBetween(now, d), Level: levelOf(daysBetween(now, d)), OwnerName: t.OwnerUserName, }) } add("register_end", "报名截止", t.RegisterEndDate) add("bid_end", "投标截止", t.BidEndDate) add("open_bid", "开标时间", t.OpenBidDate) return out } // daysBetween 计算今天(0点)到目标时间的剩余天数 func daysBetween(now, target time.Time) int { n := time.Date(now.Year(), now.Month(), now.Day(), 0, 0, 0, 0, now.Location()) t := time.Date(target.Year(), target.Month(), target.Day(), 0, 0, 0, 0, target.Location()) return int(t.Sub(n).Hours() / 24) } // levelOf 剩余天数 -> 提醒等级 func levelOf(days int) string { if days <= 1 { return "danger" } if days <= 3 { return "warning" } return "info" }