增加招投标模块

This commit is contained in:
2026-09-24 17:06:46 +08:00
parent bc9e32d62a
commit eafa5e1cf4
51 changed files with 13453 additions and 5 deletions
@@ -0,0 +1,208 @@
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"
}