package controllers import ( "encoding/json" "io" "strconv" "strings" "time" "server/models" beego "github.com/beego/beego/v2/server/web" "github.com/beego/beego/v2/client/orm" ) // ============================================================= // CRM 招投标管理 - 补充接口 // ============================================================= // // 覆盖开发计划中尚未实现的接口: // - 招标公告 / 投标方 / 开标评标 / 定标结果(招标侧子表) // - 投标立项审批 / 标书版本(投标侧子表) // - 标书模板库 / 资质文件库(公共库) // - 招标统计 / 投标统计 // // 约定沿用既有实现:软删除(delete_time)、多租户(tenant_id)、 // 统一 pipelineClaims / pipelineOk / pipelineErr 鉴权与响应。 // ============================================================= // ------------------------------ 招标公告 ------------------------------ // BackendCrmBiddingTenderAnnounceController 招标公告 type BackendCrmBiddingTenderAnnounceController struct { beego.Controller } type tenderAnnouncePayload struct { TenderID uint64 `json:"tender_id"` AnnounceType string `json:"announce_type"` Title string `json:"title"` Content string `json:"content"` } // List GET /backend/crm/bidding/tender-announce/list?tender_id=1 func (c *BackendCrmBiddingTenderAnnounceController) List() { claims, err := pipelineClaims(&c.Controller) if err != nil { pipelineErr(&c.Controller, 401, 401, err.Error()) return } tenderID, _ := c.GetInt64("tender_id") if tenderID == 0 { pipelineErr(&c.Controller, 400, 400, "tender_id 必填") return } var list []models.TenantCrmBiddingTenderAnnounce _, _ = models.Orm.QueryTable(new(models.TenantCrmBiddingTenderAnnounce)). Filter("tenant_id", pipelineTenantID(claims)). Filter("tender_id", tenderID). Filter("delete_time__isnull", true). OrderBy("-id"). All(&list) pipelineOk(&c.Controller, map[string]interface{}{"list": list, "total": len(list)}) } // Create POST /backend/crm/bidding/tender-announce func (c *BackendCrmBiddingTenderAnnounceController) Create() { claims, err := pipelineClaims(&c.Controller) if err != nil { pipelineErr(&c.Controller, 401, 401, err.Error()) return } raw, _ := io.ReadAll(c.Ctx.Request.Body) var p tenderAnnouncePayload if err := json.Unmarshal(raw, &p); err != nil { pipelineErr(&c.Controller, 400, 400, "参数错误") return } if p.TenderID == 0 || strings.TrimSpace(p.Title) == "" { pipelineErr(&c.Controller, 400, 400, "公告信息不完整") return } now := time.Now() row := models.TenantCrmBiddingTenderAnnounce{ TenantID: pipelineTenantID(claims), TenderID: p.TenderID, AnnounceType: firstNonEmpty(p.AnnounceType, "1"), Title: strings.TrimSpace(p.Title), Content: p.Content, PublishStatus: 0, CreateUserID: pipelineUID(claims), CreateUserName: resolveUserName(claims), CreateTime: now, UpdateTime: now, } id, err := models.Orm.Insert(&row) if err != nil { pipelineErr(&c.Controller, 500, 500, "创建失败: "+err.Error()) return } crmWriteLog(pipelineTenantID(claims), 5, uint64(id), "create", "创建招标公告", claims) pipelineOk(&c.Controller, map[string]interface{}{"id": id}) } // Publish POST /backend/crm/bidding/tender-announce/:id/publish // 发布公告:置为已发布并记录发布时间,同时把招标主表推进到「公告中」。 func (c *BackendCrmBiddingTenderAnnounceController) Publish() { claims, err := pipelineClaims(&c.Controller) if err != nil { pipelineErr(&c.Controller, 401, 401, err.Error()) return } id, _ := strconv.ParseUint(c.Ctx.Input.Param(":id"), 10, 64) if id == 0 { pipelineErr(&c.Controller, 400, 400, "无效的ID") return } var row models.TenantCrmBiddingTenderAnnounce if err := models.Orm.QueryTable(new(models.TenantCrmBiddingTenderAnnounce)). Filter("id", id). Filter("tenant_id", pipelineTenantID(claims)). Filter("delete_time__isnull", true). One(&row); err != nil { pipelineErr(&c.Controller, 404, 404, "公告不存在") return } now := time.Now() row.PublishStatus = 1 row.PublishTime = &now row.UpdateTime = now _, _ = models.Orm.Update(&row, "publish_status", "publish_time", "update_time") // 主表状态推进:草稿(0) -> 公告中(1) if row.TenderID > 0 { var t models.TenantCrmBiddingTender if err := models.Orm.QueryTable(new(models.TenantCrmBiddingTender)). Filter("id", row.TenderID). Filter("tenant_id", pipelineTenantID(claims)). Filter("delete_time__isnull", true). One(&t); err == nil && t.Status == 0 { t.Status = 1 t.PublishDate = &now t.UpdateTime = now _, _ = models.Orm.Update(&t, "status", "publish_date", "update_time") } } crmWriteLog(pipelineTenantID(claims), 5, id, "publish", "发布招标公告", claims) pipelineOk(&c.Controller, map[string]interface{}{"id": id}) } // Delete DELETE /backend/crm/bidding/tender-announce/:id func (c *BackendCrmBiddingTenderAnnounceController) Delete() { claims, err := pipelineClaims(&c.Controller) if err != nil { pipelineErr(&c.Controller, 401, 401, err.Error()) return } id, _ := strconv.ParseUint(c.Ctx.Input.Param(":id"), 10, 64) now := time.Now() _, err = models.Orm.QueryTable(new(models.TenantCrmBiddingTenderAnnounce)). Filter("id", id). Filter("tenant_id", pipelineTenantID(claims)). Update(map[string]interface{}{"delete_time": now, "update_time": now}) if err != nil { pipelineErr(&c.Controller, 500, 500, "删除失败: "+err.Error()) return } crmWriteLog(pipelineTenantID(claims), 5, id, "delete", "删除招标公告", claims) pipelineOk(&c.Controller, nil) } // ------------------------------ 投标方/供应商 ------------------------------ // BackendCrmBiddingTenderSupplierController 投标方 type BackendCrmBiddingTenderSupplierController struct { beego.Controller } type tenderSupplierPayload struct { TenderID uint64 `json:"tender_id"` SupplierID *uint64 `json:"supplier_id"` SupplierName string `json:"supplier_name"` RegisterStatus int8 `json:"register_status"` BidAmount float64 `json:"bid_amount"` BidFiles string `json:"bid_files"` ContactPerson string `json:"contact_person"` ContactPhone string `json:"contact_phone"` IsWinner int8 `json:"is_winner"` Ranking uint32 `json:"ranking"` Remark string `json:"remark"` } // List GET /backend/crm/bidding/tender-supplier/list?tender_id=1 func (c *BackendCrmBiddingTenderSupplierController) List() { claims, err := pipelineClaims(&c.Controller) if err != nil { pipelineErr(&c.Controller, 401, 401, err.Error()) return } tenderID, _ := c.GetInt64("tender_id") if tenderID == 0 { pipelineErr(&c.Controller, 400, 400, "tender_id 必填") return } var list []models.TenantCrmBiddingTenderSupplier _, _ = models.Orm.QueryTable(new(models.TenantCrmBiddingTenderSupplier)). Filter("tenant_id", pipelineTenantID(claims)). Filter("tender_id", tenderID). Filter("delete_time__isnull", true). OrderBy("-id"). All(&list) pipelineOk(&c.Controller, map[string]interface{}{"list": list, "total": len(list)}) } // Create POST /backend/crm/bidding/tender-supplier func (c *BackendCrmBiddingTenderSupplierController) Create() { claims, err := pipelineClaims(&c.Controller) if err != nil { pipelineErr(&c.Controller, 401, 401, err.Error()) return } raw, _ := io.ReadAll(c.Ctx.Request.Body) var p tenderSupplierPayload if err := json.Unmarshal(raw, &p); err != nil { pipelineErr(&c.Controller, 400, 400, "参数错误") return } if p.TenderID == 0 || strings.TrimSpace(p.SupplierName) == "" { pipelineErr(&c.Controller, 400, 400, "投标方信息不完整") return } now := time.Now() row := models.TenantCrmBiddingTenderSupplier{ TenantID: pipelineTenantID(claims), TenderID: p.TenderID, SupplierID: p.SupplierID, SupplierName: strings.TrimSpace(p.SupplierName), RegisterStatus: p.RegisterStatus, RegisterTime: &now, BidAmount: p.BidAmount, BidFiles: p.BidFiles, ContactPerson: p.ContactPerson, ContactPhone: p.ContactPhone, Remark: p.Remark, CreateUserID: pipelineUID(claims), CreateUserName: resolveUserName(claims), CreateTime: now, UpdateTime: now, } id, err := models.Orm.Insert(&row) if err != nil { pipelineErr(&c.Controller, 500, 500, "创建失败: "+err.Error()) return } crmWriteLog(pipelineTenantID(claims), 5, uint64(id), "create", "登记投标方", claims) pipelineOk(&c.Controller, map[string]interface{}{"id": id}) } // Update PUT /backend/crm/bidding/tender-supplier/:id func (c *BackendCrmBiddingTenderSupplierController) Update() { claims, err := pipelineClaims(&c.Controller) if err != nil { pipelineErr(&c.Controller, 401, 401, err.Error()) return } id, _ := strconv.ParseUint(c.Ctx.Input.Param(":id"), 10, 64) if id == 0 { pipelineErr(&c.Controller, 400, 400, "无效的ID") return } raw, _ := io.ReadAll(c.Ctx.Request.Body) var p tenderSupplierPayload if err := json.Unmarshal(raw, &p); err != nil { pipelineErr(&c.Controller, 400, 400, "参数错误") return } var row models.TenantCrmBiddingTenderSupplier if err := models.Orm.QueryTable(new(models.TenantCrmBiddingTenderSupplier)). Filter("id", id). Filter("tenant_id", pipelineTenantID(claims)). Filter("delete_time__isnull", true). One(&row); err != nil { pipelineErr(&c.Controller, 404, 404, "记录不存在") return } if strings.TrimSpace(p.SupplierName) != "" { row.SupplierName = strings.TrimSpace(p.SupplierName) } row.SupplierID = p.SupplierID row.RegisterStatus = p.RegisterStatus row.BidAmount = p.BidAmount row.BidFiles = p.BidFiles row.ContactPerson = p.ContactPerson row.ContactPhone = p.ContactPhone row.Ranking = p.Ranking row.IsWinner = p.IsWinner row.Remark = p.Remark row.UpdateTime = time.Now() _, _ = models.Orm.Update(&row) crmWriteLog(pipelineTenantID(claims), 5, id, "update", "更新投标方信息", claims) pipelineOk(&c.Controller, map[string]interface{}{"id": id}) } // Delete DELETE /backend/crm/bidding/tender-supplier/:id func (c *BackendCrmBiddingTenderSupplierController) Delete() { claims, err := pipelineClaims(&c.Controller) if err != nil { pipelineErr(&c.Controller, 401, 401, err.Error()) return } id, _ := strconv.ParseUint(c.Ctx.Input.Param(":id"), 10, 64) now := time.Now() _, err = models.Orm.QueryTable(new(models.TenantCrmBiddingTenderSupplier)). Filter("id", id). Filter("tenant_id", pipelineTenantID(claims)). Update(map[string]interface{}{"delete_time": now, "update_time": now}) if err != nil { pipelineErr(&c.Controller, 500, 500, "删除失败: "+err.Error()) return } crmWriteLog(pipelineTenantID(claims), 5, id, "delete", "删除投标方", claims) pipelineOk(&c.Controller, nil) } // ------------------------------ 开标评标 ------------------------------ // BackendCrmBiddingTenderEvaluationController 开标评标 type BackendCrmBiddingTenderEvaluationController struct { beego.Controller } type tenderEvaluationPayload struct { TenderID uint64 `json:"tender_id"` SupplierID *uint64 `json:"supplier_id"` SupplierName string `json:"supplier_name"` TechnicalScore float64 `json:"technical_score"` BusinessScore float64 `json:"business_score"` PriceScore float64 `json:"price_score"` ServiceScore float64 `json:"service_score"` WeightConfig string `json:"weight_config"` Ranking uint32 `json:"ranking"` Comment string `json:"comment"` Result int8 `json:"result"` } // evaluationWeights 评标权重配置,缺省时四项等权(各项满分按 100 计)。 type evaluationWeights struct { Technical float64 `json:"technical"` Business float64 `json:"business"` Price float64 `json:"price"` Service float64 `json:"service"` } // calcTotalScore 按权重折算总分;权重解析失败或全为 0 时退化为四项直接求和。 func calcTotalScore(p tenderEvaluationPayload) float64 { var w evaluationWeights _ = json.Unmarshal([]byte(p.WeightConfig), &w) sum := w.Technical + w.Business + w.Price + w.Service if sum <= 0 { return p.TechnicalScore + p.BusinessScore + p.PriceScore + p.ServiceScore } return p.TechnicalScore*w.Technical + p.BusinessScore*w.Business + p.PriceScore*w.Price + p.ServiceScore*w.Service } // List GET /backend/crm/bidding/tender-evaluation/list?tender_id=1 func (c *BackendCrmBiddingTenderEvaluationController) List() { claims, err := pipelineClaims(&c.Controller) if err != nil { pipelineErr(&c.Controller, 401, 401, err.Error()) return } tenderID, _ := c.GetInt64("tender_id") if tenderID == 0 { pipelineErr(&c.Controller, 400, 400, "tender_id 必填") return } var list []models.TenantCrmBiddingTenderEvaluation _, _ = models.Orm.QueryTable(new(models.TenantCrmBiddingTenderEvaluation)). Filter("tenant_id", pipelineTenantID(claims)). Filter("tender_id", tenderID). Filter("delete_time__isnull", true). OrderBy("-total_score"). All(&list) pipelineOk(&c.Controller, map[string]interface{}{"list": list, "total": len(list)}) } // Create POST /backend/crm/bidding/tender-evaluation func (c *BackendCrmBiddingTenderEvaluationController) Create() { claims, err := pipelineClaims(&c.Controller) if err != nil { pipelineErr(&c.Controller, 401, 401, err.Error()) return } raw, _ := io.ReadAll(c.Ctx.Request.Body) var p tenderEvaluationPayload if err := json.Unmarshal(raw, &p); err != nil { pipelineErr(&c.Controller, 400, 400, "参数错误") return } if p.TenderID == 0 || strings.TrimSpace(p.SupplierName) == "" { pipelineErr(&c.Controller, 400, 400, "评标信息不完整") return } now := time.Now() row := models.TenantCrmBiddingTenderEvaluation{ TenantID: pipelineTenantID(claims), TenderID: p.TenderID, SupplierID: p.SupplierID, SupplierName: strings.TrimSpace(p.SupplierName), TechnicalScore: p.TechnicalScore, BusinessScore: p.BusinessScore, PriceScore: p.PriceScore, ServiceScore: p.ServiceScore, TotalScore: calcTotalScore(p), WeightConfig: p.WeightConfig, Ranking: p.Ranking, Comment: p.Comment, Result: p.Result, EvaluationUserID: pipelineUID(claims), EvaluationUserName: resolveUserName(claims), EvaluationTime: &now, CreateTime: now, UpdateTime: now, } id, err := models.Orm.Insert(&row) if err != nil { pipelineErr(&c.Controller, 500, 500, "创建失败: "+err.Error()) return } // 录入评标后重排该招标下所有供应商名次(按总分降序) resortEvaluationRanking(p.TenderID, pipelineTenantID(claims)) crmWriteLog(pipelineTenantID(claims), 5, uint64(id), "create", "新增评标打分", claims) pipelineOk(&c.Controller, map[string]interface{}{"id": id}) } // resortEvaluationRanking 按总分降序重写 ranking 字段。 func resortEvaluationRanking(tenderID uint64, tenantID string) { var list []models.TenantCrmBiddingTenderEvaluation _, _ = models.Orm.QueryTable(new(models.TenantCrmBiddingTenderEvaluation)). Filter("tenant_id", tenantID). Filter("tender_id", tenderID). Filter("delete_time__isnull", true). OrderBy("-total_score"). All(&list) for i := range list { list[i].Ranking = uint32(i + 1) _, _ = models.Orm.Update(&list[i], "ranking") } } // Delete DELETE /backend/crm/bidding/tender-evaluation/:id func (c *BackendCrmBiddingTenderEvaluationController) Delete() { claims, err := pipelineClaims(&c.Controller) if err != nil { pipelineErr(&c.Controller, 401, 401, err.Error()) return } id, _ := strconv.ParseUint(c.Ctx.Input.Param(":id"), 10, 64) now := time.Now() _, err = models.Orm.QueryTable(new(models.TenantCrmBiddingTenderEvaluation)). Filter("id", id). Filter("tenant_id", pipelineTenantID(claims)). Update(map[string]interface{}{"delete_time": now, "update_time": now}) if err != nil { pipelineErr(&c.Controller, 500, 500, "删除失败: "+err.Error()) return } crmWriteLog(pipelineTenantID(claims), 5, id, "delete", "删除评标记录", claims) pipelineOk(&c.Controller, nil) } // ------------------------------ 定标结果 ------------------------------ // BackendCrmBiddingTenderResultController 定标结果(只读,写入由 tender/:id/award 完成) type BackendCrmBiddingTenderResultController struct { beego.Controller } // List GET /backend/crm/bidding/tender-result/list?tender_id=1 func (c *BackendCrmBiddingTenderResultController) List() { claims, err := pipelineClaims(&c.Controller) if err != nil { pipelineErr(&c.Controller, 401, 401, err.Error()) return } tenderID, _ := c.GetInt64("tender_id") if tenderID == 0 { pipelineErr(&c.Controller, 400, 400, "tender_id 必填") return } var list []models.TenantCrmBiddingTenderResult _, _ = models.Orm.QueryTable(new(models.TenantCrmBiddingTenderResult)). Filter("tenant_id", pipelineTenantID(claims)). Filter("tender_id", tenderID). Filter("delete_time__isnull", true). OrderBy("-id"). All(&list) pipelineOk(&c.Controller, map[string]interface{}{"list": list, "total": len(list)}) } // Detail GET /backend/crm/bidding/tender-result/detail/:id func (c *BackendCrmBiddingTenderResultController) Detail() { claims, err := pipelineClaims(&c.Controller) if err != nil { pipelineErr(&c.Controller, 401, 401, err.Error()) return } id, _ := strconv.ParseUint(c.Ctx.Input.Param(":id"), 10, 64) var row models.TenantCrmBiddingTenderResult if err := models.Orm.QueryTable(new(models.TenantCrmBiddingTenderResult)). Filter("id", id). Filter("tenant_id", pipelineTenantID(claims)). Filter("delete_time__isnull", true). One(&row); err != nil { pipelineErr(&c.Controller, 404, 404, "定标结果不存在") return } pipelineOk(&c.Controller, row) } // ------------------------------ 投标立项审批 ------------------------------ // BackendCrmBiddingBidApprovalController 投标立项审批 type BackendCrmBiddingBidApprovalController struct { beego.Controller } type bidApprovalPayload struct { BidID uint64 `json:"bid_id"` ApprovalType string `json:"approval_type"` ApprovalStatus int8 `json:"approval_status"` ApprovalUsers string `json:"approval_users"` Comment string `json:"comment"` } // List GET /backend/crm/bidding/bid-approval/list?bid_id=1 func (c *BackendCrmBiddingBidApprovalController) List() { claims, err := pipelineClaims(&c.Controller) if err != nil { pipelineErr(&c.Controller, 401, 401, err.Error()) return } bidID, _ := c.GetInt64("bid_id") if bidID == 0 { pipelineErr(&c.Controller, 400, 400, "bid_id 必填") return } var list []models.TenantCrmBiddingBidApproval _, _ = models.Orm.QueryTable(new(models.TenantCrmBiddingBidApproval)). Filter("tenant_id", pipelineTenantID(claims)). Filter("bid_id", bidID). Filter("delete_time__isnull", true). OrderBy("-id"). All(&list) pipelineOk(&c.Controller, map[string]interface{}{"list": list, "total": len(list)}) } // Create POST /backend/crm/bidding/bid-approval 提交立项审批 func (c *BackendCrmBiddingBidApprovalController) Create() { claims, err := pipelineClaims(&c.Controller) if err != nil { pipelineErr(&c.Controller, 401, 401, err.Error()) return } raw, _ := io.ReadAll(c.Ctx.Request.Body) var p bidApprovalPayload if err := json.Unmarshal(raw, &p); err != nil { pipelineErr(&c.Controller, 400, 400, "参数错误") return } if p.BidID == 0 { pipelineErr(&c.Controller, 400, 400, "bid_id 必填") return } now := time.Now() row := models.TenantCrmBiddingBidApproval{ TenantID: pipelineTenantID(claims), BidID: p.BidID, ApprovalType: firstNonEmpty(p.ApprovalType, "1"), ApprovalStatus: 1, // 1审批中 ApprovalUsers: p.ApprovalUsers, SubmitUserID: pipelineUID(claims), SubmitUserName: resolveUserName(claims), SubmitTime: &now, Comment: p.Comment, CurrentUserID: pipelineUID(claims), CurrentUserName: resolveUserName(claims), CreateTime: now, UpdateTime: now, } id, err := models.Orm.Insert(&row) if err != nil { pipelineErr(&c.Controller, 500, 500, "提交失败: "+err.Error()) return } // 主表推进到「立项审批中」 var bid models.TenantCrmBiddingBid if err := models.Orm.QueryTable(new(models.TenantCrmBiddingBid)). Filter("id", p.BidID). Filter("tenant_id", pipelineTenantID(claims)). Filter("delete_time__isnull", true). One(&bid); err == nil && bid.Status == 0 { bid.Status = 1 bid.UpdateTime = now _, _ = models.Orm.Update(&bid, "status", "update_time") } crmWriteLog(pipelineTenantID(claims), 5, uint64(id), "create", "提交立项审批", claims) pipelineOk(&c.Controller, map[string]interface{}{"id": id}) } // Approve POST /backend/crm/bidding/bid-approval/:id/approve 审批通过/驳回 // body: { "pass": true, "comment": "..." } func (c *BackendCrmBiddingBidApprovalController) Approve() { claims, err := pipelineClaims(&c.Controller) if err != nil { pipelineErr(&c.Controller, 401, 401, err.Error()) return } id, _ := strconv.ParseUint(c.Ctx.Input.Param(":id"), 10, 64) raw, _ := io.ReadAll(c.Ctx.Request.Body) var body struct { Pass bool `json:"pass"` Comment string `json:"comment"` } _ = json.Unmarshal(raw, &body) var row models.TenantCrmBiddingBidApproval if err := models.Orm.QueryTable(new(models.TenantCrmBiddingBidApproval)). Filter("id", id). Filter("tenant_id", pipelineTenantID(claims)). Filter("delete_time__isnull", true). One(&row); err != nil { pipelineErr(&c.Controller, 404, 404, "审批记录不存在") return } now := time.Now() if body.Pass { row.ApprovalStatus = 2 // 2已通过 } else { row.ApprovalStatus = 3 // 3已驳回 } row.Comment = body.Comment row.CompleteTime = &now row.UpdateTime = now _, _ = models.Orm.Update(&row, "approval_status", "comment", "complete_time", "update_time") // 审批通过 -> 投标转为「投标中」;驳回 -> 退回草稿 var bid models.TenantCrmBiddingBid if err := models.Orm.QueryTable(new(models.TenantCrmBiddingBid)). Filter("id", row.BidID). Filter("tenant_id", pipelineTenantID(claims)). Filter("delete_time__isnull", true). One(&bid); err == nil { if body.Pass { bid.Status = 2 } else { bid.Status = 0 } bid.UpdateTime = now _, _ = models.Orm.Update(&bid, "status", "update_time") } crmWriteLog(pipelineTenantID(claims), 5, id, "approve", "立项审批处理", claims) pipelineOk(&c.Controller, map[string]interface{}{"id": id}) } // ------------------------------ 标书版本 ------------------------------ // BackendCrmBiddingBidDocumentController 标书版本 type BackendCrmBiddingBidDocumentController struct { beego.Controller } type bidDocumentPayload struct { BidID uint64 `json:"bid_id"` TemplateID *uint64 `json:"template_id"` Version string `json:"version"` Title string `json:"title"` Content string `json:"content"` FileURL string `json:"file_url"` Status int8 `json:"status"` ReviewComment string `json:"review_comment"` } // List GET /backend/crm/bidding/bid-document/list?bid_id=1 func (c *BackendCrmBiddingBidDocumentController) List() { claims, err := pipelineClaims(&c.Controller) if err != nil { pipelineErr(&c.Controller, 401, 401, err.Error()) return } bidID, _ := c.GetInt64("bid_id") if bidID == 0 { pipelineErr(&c.Controller, 400, 400, "bid_id 必填") return } var list []models.TenantCrmBiddingBidDocument _, _ = models.Orm.QueryTable(new(models.TenantCrmBiddingBidDocument)). Filter("tenant_id", pipelineTenantID(claims)). Filter("bid_id", bidID). Filter("delete_time__isnull", true). OrderBy("-id"). All(&list) pipelineOk(&c.Controller, map[string]interface{}{"list": list, "total": len(list)}) } // Create POST /backend/crm/bidding/bid-document func (c *BackendCrmBiddingBidDocumentController) Create() { claims, err := pipelineClaims(&c.Controller) if err != nil { pipelineErr(&c.Controller, 401, 401, err.Error()) return } raw, _ := io.ReadAll(c.Ctx.Request.Body) var p bidDocumentPayload if err := json.Unmarshal(raw, &p); err != nil { pipelineErr(&c.Controller, 400, 400, "参数错误") return } if p.BidID == 0 || strings.TrimSpace(p.Title) == "" { pipelineErr(&c.Controller, 400, 400, "标书信息不完整") return } now := time.Now() row := models.TenantCrmBiddingBidDocument{ TenantID: pipelineTenantID(claims), BidID: p.BidID, TemplateID: p.TemplateID, Version: firstNonEmpty(p.Version, "v1.0"), Title: strings.TrimSpace(p.Title), Content: p.Content, FileURL: p.FileURL, Status: p.Status, EditorUserID: pipelineUID(claims), EditorUserName: resolveUserName(claims), CreateTime: now, UpdateTime: now, } id, err := models.Orm.Insert(&row) if err != nil { pipelineErr(&c.Controller, 500, 500, "创建失败: "+err.Error()) return } crmWriteLog(pipelineTenantID(claims), 5, uint64(id), "create", "新建标书版本", claims) pipelineOk(&c.Controller, map[string]interface{}{"id": id}) } // Update PUT /backend/crm/bidding/bid-document/:id func (c *BackendCrmBiddingBidDocumentController) Update() { claims, err := pipelineClaims(&c.Controller) if err != nil { pipelineErr(&c.Controller, 401, 401, err.Error()) return } id, _ := strconv.ParseUint(c.Ctx.Input.Param(":id"), 10, 64) raw, _ := io.ReadAll(c.Ctx.Request.Body) var p bidDocumentPayload if err := json.Unmarshal(raw, &p); err != nil { pipelineErr(&c.Controller, 400, 400, "参数错误") return } var row models.TenantCrmBiddingBidDocument if err := models.Orm.QueryTable(new(models.TenantCrmBiddingBidDocument)). Filter("id", id). Filter("tenant_id", pipelineTenantID(claims)). Filter("delete_time__isnull", true). One(&row); err != nil { pipelineErr(&c.Controller, 404, 404, "标书不存在") return } if strings.TrimSpace(p.Title) != "" { row.Title = strings.TrimSpace(p.Title) } row.Version = firstNonEmpty(p.Version, row.Version) row.Content = p.Content row.FileURL = p.FileURL row.Status = p.Status row.ReviewComment = p.ReviewComment if p.Status == 3 || p.Status == 4 { row.ReviewUserID = pipelineUID(claims) row.ReviewUserName = resolveUserName(claims) now := time.Now() row.ReviewTime = &now if p.Status == 4 { row.SubmitTime = &now } } row.UpdateTime = time.Now() _, _ = models.Orm.Update(&row) crmWriteLog(pipelineTenantID(claims), 5, id, "update", "更新标书版本", claims) pipelineOk(&c.Controller, map[string]interface{}{"id": id}) } // Delete DELETE /backend/crm/bidding/bid-document/:id func (c *BackendCrmBiddingBidDocumentController) Delete() { claims, err := pipelineClaims(&c.Controller) if err != nil { pipelineErr(&c.Controller, 401, 401, err.Error()) return } id, _ := strconv.ParseUint(c.Ctx.Input.Param(":id"), 10, 64) now := time.Now() _, err = models.Orm.QueryTable(new(models.TenantCrmBiddingBidDocument)). Filter("id", id). Filter("tenant_id", pipelineTenantID(claims)). Update(map[string]interface{}{"delete_time": now, "update_time": now}) if err != nil { pipelineErr(&c.Controller, 500, 500, "删除失败: "+err.Error()) return } crmWriteLog(pipelineTenantID(claims), 5, id, "delete", "删除标书版本", claims) pipelineOk(&c.Controller, nil) } // ------------------------------ 标书模板库 ------------------------------ // BackendCrmBiddingBidTemplateController 标书模板库(公共库,租户内共享) type BackendCrmBiddingBidTemplateController struct { beego.Controller } type bidTemplatePayload struct { TemplateName string `json:"template_name"` TemplateType string `json:"template_type"` Industry string `json:"industry"` Description string `json:"description"` Content string `json:"content"` FileURL string `json:"file_url"` Status int8 `json:"status"` } // List GET /backend/crm/bidding/template/list?keyword=&template_type= func (c *BackendCrmBiddingBidTemplateController) List() { claims, err := pipelineClaims(&c.Controller) if err != nil { pipelineErr(&c.Controller, 401, 401, err.Error()) return } keyword := strings.TrimSpace(c.GetString("keyword")) templateType := strings.TrimSpace(c.GetString("template_type")) page, _ := c.GetInt("page", 1) pageSize, _ := c.GetInt("pageSize", 20) if page < 1 { page = 1 } if pageSize < 1 || pageSize > 200 { pageSize = 20 } qs := models.Orm.QueryTable(new(models.TenantCrmBiddingBidTemplate)). Filter("tenant_id", pipelineTenantID(claims)). Filter("delete_time__isnull", true) if keyword != "" { qs = qs.Filter("template_name__icontains", keyword) } if templateType != "" { qs = qs.Filter("template_type", templateType) } total, _ := qs.Count() var list []models.TenantCrmBiddingBidTemplate _, _ = qs.OrderBy("-id").Limit(pageSize).Offset((page - 1) * pageSize).All(&list) pipelineOk(&c.Controller, map[string]interface{}{ "list": list, "total": total, "page": page, "pageSize": pageSize, }) } // Create POST /backend/crm/bidding/template func (c *BackendCrmBiddingBidTemplateController) Create() { claims, err := pipelineClaims(&c.Controller) if err != nil { pipelineErr(&c.Controller, 401, 401, err.Error()) return } raw, _ := io.ReadAll(c.Ctx.Request.Body) var p bidTemplatePayload if err := json.Unmarshal(raw, &p); err != nil { pipelineErr(&c.Controller, 400, 400, "参数错误") return } if strings.TrimSpace(p.TemplateName) == "" { pipelineErr(&c.Controller, 400, 400, "模板名称必填") return } now := time.Now() row := models.TenantCrmBiddingBidTemplate{ TenantID: pipelineTenantID(claims), TemplateName: strings.TrimSpace(p.TemplateName), TemplateType: firstNonEmpty(p.TemplateType, "1"), Industry: p.Industry, Description: p.Description, Content: p.Content, FileURL: p.FileURL, Status: 1, OwnerUserID: pipelineUID(claims), OwnerUserName: resolveUserName(claims), CreateTime: now, UpdateTime: now, } id, err := models.Orm.Insert(&row) if err != nil { pipelineErr(&c.Controller, 500, 500, "创建失败: "+err.Error()) return } crmWriteLog(pipelineTenantID(claims), 5, uint64(id), "create", "新建标书模板", claims) pipelineOk(&c.Controller, map[string]interface{}{"id": id}) } // Update PUT /backend/crm/bidding/template/:id func (c *BackendCrmBiddingBidTemplateController) Update() { claims, err := pipelineClaims(&c.Controller) if err != nil { pipelineErr(&c.Controller, 401, 401, err.Error()) return } id, _ := strconv.ParseUint(c.Ctx.Input.Param(":id"), 10, 64) raw, _ := io.ReadAll(c.Ctx.Request.Body) var p bidTemplatePayload if err := json.Unmarshal(raw, &p); err != nil { pipelineErr(&c.Controller, 400, 400, "参数错误") return } var row models.TenantCrmBiddingBidTemplate if err := models.Orm.QueryTable(new(models.TenantCrmBiddingBidTemplate)). Filter("id", id). Filter("tenant_id", pipelineTenantID(claims)). Filter("delete_time__isnull", true). One(&row); err != nil { pipelineErr(&c.Controller, 404, 404, "模板不存在") return } if strings.TrimSpace(p.TemplateName) != "" { row.TemplateName = strings.TrimSpace(p.TemplateName) } row.TemplateType = firstNonEmpty(p.TemplateType, row.TemplateType) row.Industry = p.Industry row.Description = p.Description row.Content = p.Content row.FileURL = p.FileURL if p.Status == 0 || p.Status == 1 { row.Status = p.Status } row.UpdateTime = time.Now() _, _ = models.Orm.Update(&row) crmWriteLog(pipelineTenantID(claims), 5, id, "update", "更新标书模板", claims) pipelineOk(&c.Controller, map[string]interface{}{"id": id}) } // Delete DELETE /backend/crm/bidding/template/:id func (c *BackendCrmBiddingBidTemplateController) Delete() { claims, err := pipelineClaims(&c.Controller) if err != nil { pipelineErr(&c.Controller, 401, 401, err.Error()) return } id, _ := strconv.ParseUint(c.Ctx.Input.Param(":id"), 10, 64) now := time.Now() _, err = models.Orm.QueryTable(new(models.TenantCrmBiddingBidTemplate)). Filter("id", id). Filter("tenant_id", pipelineTenantID(claims)). Update(map[string]interface{}{"delete_time": now, "update_time": now}) if err != nil { pipelineErr(&c.Controller, 500, 500, "删除失败: "+err.Error()) return } crmWriteLog(pipelineTenantID(claims), 5, id, "delete", "删除标书模板", claims) pipelineOk(&c.Controller, nil) } // ------------------------------ 资质文件库 ------------------------------ // BackendCrmBiddingBidQualificationController 资质文件库 type BackendCrmBiddingBidQualificationController struct { beego.Controller } type bidQualificationPayload struct { QualificationName string `json:"qualification_name"` QualificationType string `json:"qualification_type"` QualificationNo string `json:"qualification_no"` IssuingAuthority string `json:"issuing_authority"` IssueDate string `json:"issue_date"` ExpireDate string `json:"expire_date"` AlertDays uint32 `json:"alert_days"` FileURL string `json:"file_url"` FileName string `json:"file_name"` FileSize uint64 `json:"file_size"` Description string `json:"description"` Status int8 `json:"status"` } // List GET /backend/crm/bidding/qualification/list?keyword=&status= // 返回时按到期日自动标注即将过期(不落库,避免定时任务未跑时状态陈旧)。 func (c *BackendCrmBiddingBidQualificationController) List() { claims, err := pipelineClaims(&c.Controller) if err != nil { pipelineErr(&c.Controller, 401, 401, err.Error()) return } keyword := strings.TrimSpace(c.GetString("keyword")) status := strings.TrimSpace(c.GetString("status")) page, _ := c.GetInt("page", 1) pageSize, _ := c.GetInt("pageSize", 20) if page < 1 { page = 1 } if pageSize < 1 || pageSize > 200 { pageSize = 20 } qs := models.Orm.QueryTable(new(models.TenantCrmBiddingBidQualification)). Filter("tenant_id", pipelineTenantID(claims)). Filter("delete_time__isnull", true) if keyword != "" { qs = qs.Filter("qualification_name__icontains", keyword) } if status != "" { qs = qs.Filter("status", status) } total, _ := qs.Count() var list []models.TenantCrmBiddingBidQualification _, _ = qs.OrderBy("expire_date").Limit(pageSize).Offset((page - 1) * pageSize).All(&list) // 临期提醒:距到期不足 alert_days 天标记为「即将过期」,已过期标记为「失效」 type qualItem struct { models.TenantCrmBiddingBidQualification ExpiringDays int `json:"expiring_days"` } out := make([]qualItem, 0, len(list)) now := time.Now() for _, q := range list { days := -1 if q.ExpireDate != nil { d := int(q.ExpireDate.Sub(now).Hours() / 24) days = d if d < 0 { q.Status = 0 } else if q.AlertDays > 0 && uint32(d) <= q.AlertDays { q.Status = 2 } } out = append(out, qualItem{TenantCrmBiddingBidQualification: q, ExpiringDays: days}) } pipelineOk(&c.Controller, map[string]interface{}{ "list": out, "total": total, "page": page, "pageSize": pageSize, }) } // Create POST /backend/crm/bidding/qualification func (c *BackendCrmBiddingBidQualificationController) Create() { claims, err := pipelineClaims(&c.Controller) if err != nil { pipelineErr(&c.Controller, 401, 401, err.Error()) return } raw, _ := io.ReadAll(c.Ctx.Request.Body) var p bidQualificationPayload if err := json.Unmarshal(raw, &p); err != nil { pipelineErr(&c.Controller, 400, 400, "参数错误") return } if strings.TrimSpace(p.QualificationName) == "" { pipelineErr(&c.Controller, 400, 400, "资质名称必填") return } now := time.Now() row := models.TenantCrmBiddingBidQualification{ TenantID: pipelineTenantID(claims), QualificationName: strings.TrimSpace(p.QualificationName), QualificationType: firstNonEmpty(p.QualificationType, "1"), QualificationNo: p.QualificationNo, IssuingAuthority: p.IssuingAuthority, IssueDate: parsePipelineDate(p.IssueDate), ExpireDate: parsePipelineDate(p.ExpireDate), AlertDays: p.AlertDays, FileURL: p.FileURL, FileName: p.FileName, FileSize: p.FileSize, Description: p.Description, Status: 1, OwnerUserID: pipelineUID(claims), OwnerUserName: resolveUserName(claims), CreateTime: now, UpdateTime: now, } id, err := models.Orm.Insert(&row) if err != nil { pipelineErr(&c.Controller, 500, 500, "创建失败: "+err.Error()) return } crmWriteLog(pipelineTenantID(claims), 5, uint64(id), "create", "新增企业资质", claims) pipelineOk(&c.Controller, map[string]interface{}{"id": id}) } // Update PUT /backend/crm/bidding/qualification/:id func (c *BackendCrmBiddingBidQualificationController) Update() { claims, err := pipelineClaims(&c.Controller) if err != nil { pipelineErr(&c.Controller, 401, 401, err.Error()) return } id, _ := strconv.ParseUint(c.Ctx.Input.Param(":id"), 10, 64) raw, _ := io.ReadAll(c.Ctx.Request.Body) var p bidQualificationPayload if err := json.Unmarshal(raw, &p); err != nil { pipelineErr(&c.Controller, 400, 400, "参数错误") return } var row models.TenantCrmBiddingBidQualification if err := models.Orm.QueryTable(new(models.TenantCrmBiddingBidQualification)). Filter("id", id). Filter("tenant_id", pipelineTenantID(claims)). Filter("delete_time__isnull", true). One(&row); err != nil { pipelineErr(&c.Controller, 404, 404, "资质不存在") return } if strings.TrimSpace(p.QualificationName) != "" { row.QualificationName = strings.TrimSpace(p.QualificationName) } row.QualificationType = firstNonEmpty(p.QualificationType, row.QualificationType) row.QualificationNo = p.QualificationNo row.IssuingAuthority = p.IssuingAuthority if p.IssueDate != "" { row.IssueDate = parsePipelineDate(p.IssueDate) } if p.ExpireDate != "" { row.ExpireDate = parsePipelineDate(p.ExpireDate) } if p.AlertDays > 0 { row.AlertDays = p.AlertDays } row.FileURL = p.FileURL row.FileName = p.FileName row.FileSize = p.FileSize row.Description = p.Description if p.Status == 0 || p.Status == 1 || p.Status == 2 { row.Status = p.Status } row.UpdateTime = time.Now() _, _ = models.Orm.Update(&row) crmWriteLog(pipelineTenantID(claims), 5, id, "update", "更新企业资质", claims) pipelineOk(&c.Controller, map[string]interface{}{"id": id}) } // Delete DELETE /backend/crm/bidding/qualification/:id func (c *BackendCrmBiddingBidQualificationController) Delete() { claims, err := pipelineClaims(&c.Controller) if err != nil { pipelineErr(&c.Controller, 401, 401, err.Error()) return } id, _ := strconv.ParseUint(c.Ctx.Input.Param(":id"), 10, 64) now := time.Now() _, err = models.Orm.QueryTable(new(models.TenantCrmBiddingBidQualification)). Filter("id", id). Filter("tenant_id", pipelineTenantID(claims)). Update(map[string]interface{}{"delete_time": now, "update_time": now}) if err != nil { pipelineErr(&c.Controller, 500, 500, "删除失败: "+err.Error()) return } crmWriteLog(pipelineTenantID(claims), 5, id, "delete", "删除企业资质", claims) pipelineOk(&c.Controller, nil) } // ------------------------------ 统计 ------------------------------ // BackendCrmBiddingStatisticsController 招投标统计 type BackendCrmBiddingStatisticsController struct { beego.Controller } // groupRows 执行分组聚合,返回 [{name, count}]。 func groupRows(table, field, tenantID string) []map[string]interface{} { var maps []orm.Params _, _ = models.Orm.Raw( "SELECT "+field+" AS name, COUNT(*) AS count FROM "+table+ " WHERE tenant_id=? AND delete_time IS NULL GROUP BY "+field, tenantID, ).Values(&maps) out := make([]map[string]interface{}, 0, len(maps)) for _, m := range maps { out = append(out, map[string]interface{}{"name": m["name"], "count": m["count"]}) } return out } // sumField 对某数值字段求和。 func sumField(table, field, tenantID string) float64 { var v float64 _ = models.Orm.Raw( "SELECT IFNULL(SUM("+field+"),0) FROM "+table+ " WHERE tenant_id=? AND delete_time IS NULL", tenantID, ).QueryRow(&v) return v } // Tender GET /backend/crm/bidding/statistics/tender func (c *BackendCrmBiddingStatisticsController) Tender() { claims, err := pipelineClaims(&c.Controller) if err != nil { pipelineErr(&c.Controller, 401, 401, err.Error()) return } tid := pipelineTenantID(claims) var total int64 _ = models.Orm.Raw( "SELECT COUNT(*) FROM yz_backend_crm_bidding_tender WHERE tenant_id=? AND delete_time IS NULL", tid, ).QueryRow(&total) pipelineOk(&c.Controller, map[string]interface{}{ "total": total, "by_status": groupRows("yz_backend_crm_bidding_tender", "status", tid), "by_type": groupRows("yz_backend_crm_bidding_tender", "tender_type", tid), "budget_total": sumField("yz_backend_crm_bidding_tender", "budget_amount", tid), "award_total": sumField("yz_backend_crm_bidding_tender", "winner_amount", tid), }) } // Bid GET /backend/crm/bidding/statistics/bid func (c *BackendCrmBiddingStatisticsController) Bid() { claims, err := pipelineClaims(&c.Controller) if err != nil { pipelineErr(&c.Controller, 401, 401, err.Error()) return } tid := pipelineTenantID(claims) var total int64 _ = models.Orm.Raw( "SELECT COUNT(*) FROM yz_backend_crm_bidding_bid WHERE tenant_id=? AND delete_time IS NULL", tid, ).QueryRow(&total) var won int64 _ = models.Orm.Raw( "SELECT COUNT(*) FROM yz_backend_crm_bidding_bid WHERE tenant_id=? AND delete_time IS NULL AND status IN (4,6)", tid, ).QueryRow(&won) // 按月统计近 12 个月立项数 var monthly []orm.Params _, _ = models.Orm.Raw( "SELECT DATE_FORMAT(create_time,'%Y-%m') AS month, COUNT(*) AS count "+ "FROM yz_backend_crm_bidding_bid "+ "WHERE tenant_id=? AND delete_time IS NULL AND create_time >= DATE_SUB(NOW(), INTERVAL 12 MONTH) "+ "GROUP BY month ORDER BY month", tid, ).Values(&monthly) winRate := 0.0 if total > 0 { winRate = float64(won) / float64(total) * 100 } pipelineOk(&c.Controller, map[string]interface{}{ "total": total, "won": won, "win_rate": winRate, "by_status": groupRows("yz_backend_crm_bidding_bid", "status", tid), "amount_won": sumField("yz_backend_crm_bidding_bid", "bid_amount", tid), "expense": sumField("yz_backend_crm_bidding_bid_expense", "amount", tid), "monthly": monthly, }) }