package controllers import ( "encoding/json" "fmt" "io" "strconv" "strings" "time" "server/models" "github.com/beego/beego/v2/client/orm" beego "github.com/beego/beego/v2/server/web" ) // ============================================================= // CRM 招投标管理 - 招标方(Tender) // ============================================================= // // 状态机:0草稿 -> 1公告中 -> 2报名中 -> 3投标中 -> 4开标中 -> 5评标中 -> 6已定标 -> 7已归档 // 关联:定标 -> 中标供应商 -> 转合同 // ============================================================= // BackendCrmBiddingTenderController 招标方 type BackendCrmBiddingTenderController struct { beego.Controller } // tenderPayload 创建/更新请求体 type tenderPayload struct { TenderNo string `json:"tender_no"` Title string `json:"title"` TenderType string `json:"tender_type"` CustomerID uint64 `json:"customer_id"` CustomerName string `json:"customer_name"` PublishOrg string `json:"publish_org"` BudgetAmount float64 `json:"budget_amount"` BidDeposit float64 `json:"bid_deposit"` PublishDate string `json:"publish_date"` RegisterStartDate string `json:"register_start_date"` RegisterEndDate string `json:"register_end_date"` BidStartDate string `json:"bid_start_date"` BidEndDate string `json:"bid_end_date"` OpenBidDate string `json:"open_bid_date"` ContactPerson string `json:"contact_person"` ContactPhone string `json:"contact_phone"` ContactEmail string `json:"contact_email"` BidAddress string `json:"bid_address"` OpenAddress string `json:"open_address"` Description string `json:"description"` Requirement string `json:"requirement"` EvaluationMethod string `json:"evaluation_method"` OwnerUserID string `json:"owner_user_id"` OwnerUserName string `json:"owner_user_name"` Remark string `json:"remark"` } // List GET /backend/crm/bidding/tender/list func (c *BackendCrmBiddingTenderController) List() { claims, err := pipelineClaims(&c.Controller) if err != nil { pipelineErr(&c.Controller, 401, 401, err.Error()) return } page, _ := c.GetInt("page", 1) pageSize, _ := c.GetInt("pageSize", 20) if page < 1 { page = 1 } if pageSize < 1 || pageSize > 100 { pageSize = 20 } keyword := strings.TrimSpace(c.GetString("keyword")) status := strings.TrimSpace(c.GetString("status")) ownerID := strings.TrimSpace(c.GetString("owner_user_id")) tenantID := pipelineTenantID(claims) cond := orm.NewCondition().And("tenant_id", tenantID).And("delete_time__isnull", true) cond = crmApplyOwnerScope(cond, claims) if keyword != "" { kw := orm.NewCondition(). Or("title__contains", keyword). Or("tender_no__contains", keyword). Or("customer_name__contains", keyword). Or("publish_org__contains", keyword) cond = cond.AndCond(kw) } if status != "" { if v, errParse := strconv.Atoi(status); errParse == nil { cond = cond.And("status", int8(v)) } } if ownerID != "" { cond = cond.And("owner_user_id", ownerID) } qs := models.Orm.QueryTable(new(models.TenantCrmBiddingTender)).SetCond(cond) total, _ := qs.Count() var list []models.TenantCrmBiddingTender if total > 0 { _, _ = qs.OrderBy("-id").Offset((page - 1) * pageSize).Limit(pageSize).All(&list) } pipelineOk(&c.Controller, map[string]interface{}{ "list": list, "total": total, "page": page, "pageSize": pageSize, }) } // Detail GET /backend/crm/bidding/tender/detail/:id func (c *BackendCrmBiddingTenderController) 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) if id == 0 { pipelineErr(&c.Controller, 400, 400, "无效的ID") return } var row models.TenantCrmBiddingTender err = models.Orm.QueryTable(new(models.TenantCrmBiddingTender)). Filter("id", id).Filter("tenant_id", pipelineTenantID(claims)). Filter("delete_time__isnull", true).One(&row) if err != nil { pipelineErr(&c.Controller, 404, 404, "招标记录未找到") return } pipelineOk(&c.Controller, row) } // Create POST /backend/crm/bidding/tender func (c *BackendCrmBiddingTenderController) 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 tenderPayload if err := json.Unmarshal(raw, &p); err != nil { pipelineErr(&c.Controller, 400, 400, "参数错误") return } if strings.TrimSpace(p.Title) == "" { pipelineErr(&c.Controller, 400, 400, "招标标题不能为空") return } tenantID := pipelineTenantID(claims) now := time.Now() row := models.TenantCrmBiddingTender{ TenantID: tenantID, TenderNo: strings.TrimSpace(p.TenderNo), Title: strings.TrimSpace(p.Title), TenderType: firstNonEmpty(p.TenderType, "1"), Status: 0, CustomerID: nilOrID(p.CustomerID), CustomerName: strings.TrimSpace(p.CustomerName), PublishOrg: firstNonEmpty(p.PublishOrg, p.CustomerName), BudgetAmount: p.BudgetAmount, BidDeposit: p.BidDeposit, PublishDate: parsePipelineDate(p.PublishDate), RegisterStartDate: parsePipelineDateTime(p.RegisterStartDate), RegisterEndDate: parsePipelineDateTime(p.RegisterEndDate), BidStartDate: parsePipelineDateTime(p.BidStartDate), BidEndDate: parsePipelineDateTime(p.BidEndDate), OpenBidDate: parsePipelineDateTime(p.OpenBidDate), ContactPerson: strings.TrimSpace(p.ContactPerson), ContactPhone: strings.TrimSpace(p.ContactPhone), ContactEmail: strings.TrimSpace(p.ContactEmail), BidAddress: strings.TrimSpace(p.BidAddress), OpenAddress: strings.TrimSpace(p.OpenAddress), Description: p.Description, Requirement: p.Requirement, EvaluationMethod: firstNonEmpty(p.EvaluationMethod, "1"), OwnerUserID: firstNonEmpty(p.OwnerUserID, pipelineUID(claims)), OwnerUserName: firstNonEmpty(p.OwnerUserName, resolveUserName(claims)), Remark: p.Remark, CreateUserID: pipelineUID(claims), CreateUserName: resolveUserName(claims), CreateTime: now, UpdateTime: now, } if crmNameDuplicated(new(models.TenantCrmBiddingTender), tenantID, "title", row.Title) { pipelineErr(&c.Controller, 409, 409, "该招标标题已存在") return } id, err := models.Orm.Insert(&row) if err != nil { pipelineErr(&c.Controller, 500, 500, "创建失败: "+err.Error()) return } crmWriteLog(tenantID, 5, uint64(id), "create", "创建招标:"+row.Title, claims) pipelineOk(&c.Controller, map[string]interface{}{"id": id}) } // Update PUT /backend/crm/bidding/tender/:id func (c *BackendCrmBiddingTenderController) 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 tenderPayload if err := json.Unmarshal(raw, &p); err != nil { pipelineErr(&c.Controller, 400, 400, "参数错误") return } tenantID := pipelineTenantID(claims) var row models.TenantCrmBiddingTender if err := models.Orm.QueryTable(new(models.TenantCrmBiddingTender)). Filter("id", id).Filter("tenant_id", tenantID). Filter("delete_time__isnull", true).One(&row); err != nil { pipelineErr(&c.Controller, 404, 404, "招标记录未找到") return } if !canAccessCrmRecord(claims, row.OwnerUserID, row.CreateUserID) { pipelineErr(&c.Controller, 401, 401, "无权操作该招标数据") return } if row.Locked == 1 || row.Status == 6 { pipelineErr(&c.Controller, 400, 400, "招标记录已定标锁定,不能编辑") return } if strings.TrimSpace(p.Title) == "" { pipelineErr(&c.Controller, 400, 400, "招标标题不能为空") return } row.TenderNo = strings.TrimSpace(p.TenderNo) row.Title = strings.TrimSpace(p.Title) if strings.TrimSpace(p.TenderType) != "" { row.TenderType = p.TenderType } row.CustomerID = nilOrID(p.CustomerID) row.CustomerName = strings.TrimSpace(p.CustomerName) row.PublishOrg = strings.TrimSpace(p.PublishOrg) row.BudgetAmount = p.BudgetAmount row.BidDeposit = p.BidDeposit row.PublishDate = parsePipelineDate(p.PublishDate) row.RegisterStartDate = parsePipelineDateTime(p.RegisterStartDate) row.RegisterEndDate = parsePipelineDateTime(p.RegisterEndDate) row.BidStartDate = parsePipelineDateTime(p.BidStartDate) row.BidEndDate = parsePipelineDateTime(p.BidEndDate) row.OpenBidDate = parsePipelineDateTime(p.OpenBidDate) row.ContactPerson = strings.TrimSpace(p.ContactPerson) row.ContactPhone = strings.TrimSpace(p.ContactPhone) row.ContactEmail = strings.TrimSpace(p.ContactEmail) row.BidAddress = strings.TrimSpace(p.BidAddress) row.OpenAddress = strings.TrimSpace(p.OpenAddress) row.Description = p.Description row.Requirement = p.Requirement if strings.TrimSpace(p.EvaluationMethod) != "" { row.EvaluationMethod = p.EvaluationMethod } if strings.TrimSpace(p.OwnerUserID) != "" { row.OwnerUserID = strings.TrimSpace(p.OwnerUserID) } if strings.TrimSpace(p.OwnerUserName) != "" { row.OwnerUserName = strings.TrimSpace(p.OwnerUserName) } row.Remark = p.Remark row.UpdateTime = time.Now() if _, err := models.Orm.Update(&row); err != nil { pipelineErr(&c.Controller, 500, 500, "更新失败: "+err.Error()) return } crmWriteLog(tenantID, 5, row.ID, "update", "更新招标:"+row.Title, claims) pipelineOk(&c.Controller, map[string]interface{}{"id": row.ID}) } // Delete DELETE /backend/crm/bidding/tender/:id func (c *BackendCrmBiddingTenderController) 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) if id == 0 { pipelineErr(&c.Controller, 400, 400, "无效的ID") return } tenantID := pipelineTenantID(claims) var row models.TenantCrmBiddingTender if err := models.Orm.QueryTable(new(models.TenantCrmBiddingTender)). Filter("id", id).Filter("tenant_id", tenantID). Filter("delete_time__isnull", true).One(&row); err != nil { pipelineErr(&c.Controller, 404, 404, "招标记录未找到") return } if !canAccessCrmRecord(claims, row.OwnerUserID, row.CreateUserID) { pipelineErr(&c.Controller, 401, 401, "无权操作该招标数据") return } if row.Locked == 1 || row.Status == 6 { pipelineErr(&c.Controller, 400, 400, "招标记录已定标锁定,不能删除") return } if !canDeleteCrmRecord(claims, row.CreateUserID) { pipelineErr(&c.Controller, 401, 401, "只有创建人、租户管理员或平台管理员可以删除该招标") return } now := time.Now() _, err = models.Orm.QueryTable(new(models.TenantCrmBiddingTender)). Filter("id", id).Filter("tenant_id", tenantID). Update(map[string]interface{}{"delete_time": now, "update_time": now}) if err != nil { pipelineErr(&c.Controller, 500, 500, "删除失败: "+err.Error()) return } pipelineOk(&c.Controller, nil) } // Award POST /backend/crm/bidding/tender/:id/award 定标 func (c *BackendCrmBiddingTenderController) Award() { 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 p struct { WinnerSupplierID uint64 `json:"winner_supplier_id"` WinnerSupplierName string `json:"winner_supplier_name"` WinnerAmount float64 `json:"winner_amount"` Announcement string `json:"announcement"` ResultType int8 `json:"result_type"` // 1定标 2流标 3废标 } raw, _ := io.ReadAll(c.Ctx.Request.Body) if err := json.Unmarshal(raw, &p); err != nil { pipelineErr(&c.Controller, 400, 400, "参数错误") return } tenantID := pipelineTenantID(claims) var row models.TenantCrmBiddingTender if err := models.Orm.QueryTable(new(models.TenantCrmBiddingTender)). Filter("id", id).Filter("tenant_id", tenantID). Filter("delete_time__isnull", true).One(&row); err != nil { pipelineErr(&c.Controller, 404, 404, "招标记录未找到") return } if !canAccessCrmRecord(claims, row.OwnerUserID, row.CreateUserID) { pipelineErr(&c.Controller, 401, 401, "无权操作该招标数据") return } if row.Locked == 1 { pipelineErr(&c.Controller, 400, 400, "招标记录已定标,不能重复定标") return } now := time.Now() resultType := p.ResultType if resultType == 0 { resultType = 1 } // 写定标结果 result := models.TenantCrmBiddingTenderResult{ TenantID: tenantID, TenderID: row.ID, ResultType: resultType, WinnerSupplierID: nilOrID(p.WinnerSupplierID), WinnerSupplierName: strings.TrimSpace(p.WinnerSupplierName), WinnerAmount: p.WinnerAmount, ResultDate: &now, Announcement: p.Announcement, CreateUserID: pipelineUID(claims), CreateUserName: resolveUserName(claims), CreateTime: now, UpdateTime: now, } if _, err := models.Orm.Insert(&result); err != nil { pipelineErr(&c.Controller, 500, 500, "写入定标结果失败: "+err.Error()) return } // 更新招标主表:状态 6已定标 + 锁定 row.Status = 6 row.Locked = 1 row.WinnerSupplierID = nilOrID(p.WinnerSupplierID) row.WinnerSupplierName = strings.TrimSpace(p.WinnerSupplierName) row.WinnerAmount = p.WinnerAmount row.WinnerDate = &now row.UpdateTime = now if _, err := models.Orm.Update(&row, "status", "locked", "winner_supplier_id", "winner_supplier_name", "winner_amount", "winner_date", "update_time"); err != nil { pipelineErr(&c.Controller, 500, 500, "更新招标状态失败: "+err.Error()) return } crmWriteLog(tenantID, 5, row.ID, "award", fmt.Sprintf("定标:%s 中标金额 %.2f", p.WinnerSupplierName, p.WinnerAmount), claims) pipelineOk(&c.Controller, map[string]interface{}{"result_id": result.ID}) } // AwardToContract POST /backend/crm/bidding/tender/:id/contract 定标转合同 func (c *BackendCrmBiddingTenderController) AwardToContract() { 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 p struct { ContractName string `json:"contract_name"` ContractNo string `json:"contract_no"` ContractCategory string `json:"contract_category"` SignDate string `json:"sign_date"` Remark string `json:"remark"` } raw, _ := io.ReadAll(c.Ctx.Request.Body) if err := json.Unmarshal(raw, &p); err != nil { pipelineErr(&c.Controller, 400, 400, "参数错误") return } tenantID := pipelineTenantID(claims) var row models.TenantCrmBiddingTender if err := models.Orm.QueryTable(new(models.TenantCrmBiddingTender)). Filter("id", id).Filter("tenant_id", tenantID). Filter("delete_time__isnull", true).One(&row); err != nil { pipelineErr(&c.Controller, 404, 404, "招标记录未找到") return } if row.Status != 6 || row.Locked != 1 { pipelineErr(&c.Controller, 400, 400, "招标未定标,无法转合同") return } if row.ContractID != nil && *row.ContractID > 0 { pipelineErr(&c.Controller, 400, 400, "该招标已转合同,不能重复转") return } contractName := strings.TrimSpace(p.ContractName) if contractName == "" { contractName = row.Title } category := strings.TrimSpace(p.ContractCategory) if category == "" { category = "2" // 服务类 } now := time.Now() tenderID := row.ID contract := models.TenantCrmContract{ TenantID: tenantID, ContractNo: strings.TrimSpace(p.ContractNo), ContractName: contractName, ContractCategory: category, OurRole: 1, // 我方为甲方 PartyCount: 2, OwnerUserID: row.OwnerUserID, OwnerUserName: row.OwnerUserName, SignDate: parsePipelineDate(p.SignDate), Step: 1, Status: 1, Remark: p.Remark, CreateUserID: pipelineUID(claims), CreateTime: now, UpdateTime: now, } contractID, err := models.Orm.Insert(&contract) if err != nil { pipelineErr(&c.Controller, 500, 500, "创建合同失败: "+err.Error()) return } cid := uint64(contractID) row.ContractID = &cid row.UpdateTime = now if _, err := models.Orm.Update(&row, "contract_id", "update_time"); err != nil { pipelineErr(&c.Controller, 500, 500, "更新招标合同ID失败: "+err.Error()) return } crmWriteLog(tenantID, 5, tenderID, "award-contract", "定标转合同:"+contractName, claims) crmWriteLog(tenantID, 1, cid, "create", "由招标「"+row.Title+"」定标转化生成", claims) pipelineOk(&c.Controller, map[string]interface{}{"contract_id": contractID}) }