Files
yunzerwebsiteallinone/go/controllers/backend_crm_bidding_more.go
T
2026-09-24 17:06:46 +08:00

1267 lines
42 KiB
Go

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,
})
}