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 招投标管理 - 投标方(Bid) // ============================================================= // // 状态机:0草稿 -> 1立项审批中 -> 2投标中 -> 3已开标 -> (4中标 | 5落标) -> (6已转合同 | 7已归档) // 关联:商机(1)/项目(2)/独立(0) -> 中标 -> 合同/项目 // ============================================================= // BackendCrmBiddingBidController 投标方 type BackendCrmBiddingBidController struct { beego.Controller } // bidPayload 创建/更新请求体 type bidPayload struct { BidNo string `json:"bid_no"` Title string `json:"title"` BidType string `json:"bid_type"` SourceType int8 `json:"source_type"` SourceID uint64 `json:"source_id"` BusinessID uint64 `json:"business_id"` BusinessName string `json:"business_name"` ProjectID uint64 `json:"project_id"` ProjectName string `json:"project_name"` TenderID uint64 `json:"tender_id"` TenderNo string `json:"tender_no"` CustomerID uint64 `json:"customer_id"` CustomerName string `json:"customer_name"` PublishOrg string `json:"publish_org"` BidAmount float64 `json:"bid_amount"` BidDeposit float64 `json:"bid_deposit"` PublishDate string `json:"publish_date"` RegisterEndDate string `json:"register_end_date"` BidEndDate string `json:"bid_end_date"` OpenBidDate string `json:"open_bid_date"` ContactPerson string `json:"contact_person"` ContactPhone string `json:"contact_phone"` BidAddress string `json:"bid_address"` OpenAddress string `json:"open_address"` OwnerUserID string `json:"owner_user_id"` OwnerUserName string `json:"owner_user_name"` Description string `json:"description"` Remark string `json:"remark"` } // List GET /backend/crm/bidding/bid/list func (c *BackendCrmBiddingBidController) 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")) winStatus := strings.TrimSpace(c.GetString("win_status")) sourceType := strings.TrimSpace(c.GetString("source_type")) 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("bid_no__contains", keyword). Or("customer_name__contains", keyword). Or("business_name__contains", keyword). Or("contact_person__contains", keyword) cond = cond.AndCond(kw) } if status != "" { cond = cond.And("status", status) } if winStatus != "" { cond = cond.And("win_status", winStatus) } if sourceType != "" { if v, errParse := strconv.Atoi(sourceType); errParse == nil { cond = cond.And("source_type", int8(v)) } } if ownerID != "" { cond = cond.And("owner_user_id", ownerID) } qs := models.Orm.QueryTable(new(models.TenantCrmBiddingBid)).SetCond(cond) total, _ := qs.Count() var list []models.TenantCrmBiddingBid 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/bid/detail/:id func (c *BackendCrmBiddingBidController) 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.TenantCrmBiddingBid err = models.Orm.QueryTable(new(models.TenantCrmBiddingBid)). Filter("id", id).Filter("tenant_id", pipelineTenantID(claims)). Filter("delete_time__isnull", true).One(&row) if err != nil { pipelineErr(&c.Controller, 404, 404, "投标记录未找到") return } if !canAccessCrmRecord(claims, row.OwnerUserID, row.CreateUserID) { pipelineErr(&c.Controller, 403, 403, "无权访问该投标数据") return } pipelineOk(&c.Controller, row) } // Create POST /backend/crm/bidding/bid func (c *BackendCrmBiddingBidController) 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 bidPayload 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 } if strings.TrimSpace(p.CustomerName) == "" { pipelineErr(&c.Controller, 400, 400, "请填写招标方名称") return } tenantID := pipelineTenantID(claims) now := time.Now() row := models.TenantCrmBiddingBid{ TenantID: tenantID, BidNo: strings.TrimSpace(p.BidNo), Title: strings.TrimSpace(p.Title), BidType: firstNonEmpty(p.BidType, "1"), Status: 0, SourceType: p.SourceType, BusinessID: nilOrID(p.BusinessID), BusinessName: strings.TrimSpace(p.BusinessName), ProjectID: nilOrID(p.ProjectID), ProjectName: strings.TrimSpace(p.ProjectName), TenderID: nilOrID(p.TenderID), TenderNo: strings.TrimSpace(p.TenderNo), CustomerID: nilOrID(p.CustomerID), CustomerName: strings.TrimSpace(p.CustomerName), PublishOrg: strings.TrimSpace(p.PublishOrg), BidAmount: p.BidAmount, BidDeposit: p.BidDeposit, PublishDate: parsePipelineDate(p.PublishDate), RegisterEndDate: parsePipelineDateTime(p.RegisterEndDate), BidEndDate: parsePipelineDateTime(p.BidEndDate), OpenBidDate: parsePipelineDateTime(p.OpenBidDate), ContactPerson: strings.TrimSpace(p.ContactPerson), ContactPhone: strings.TrimSpace(p.ContactPhone), BidAddress: strings.TrimSpace(p.BidAddress), OpenAddress: strings.TrimSpace(p.OpenAddress), OwnerUserID: firstNonEmpty(p.OwnerUserID, pipelineUID(claims)), OwnerUserName: firstNonEmpty(p.OwnerUserName, resolveUserName(claims)), Description: p.Description, Remark: p.Remark, CreateUserID: pipelineUID(claims), CreateUserName: resolveUserName(claims), CreateTime: now, UpdateTime: now, } // 唯一性校验:同租户内不允许重复创建同名投标 if crmNameDuplicated(new(models.TenantCrmBiddingBid), 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, 4, uint64(id), "create", "创建投标:"+row.Title, claims) pipelineOk(&c.Controller, map[string]interface{}{"id": id}) } // Update PUT /backend/crm/bidding/bid/:id func (c *BackendCrmBiddingBidController) 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 bidPayload if err := json.Unmarshal(raw, &p); err != nil { pipelineErr(&c.Controller, 400, 400, "参数错误") return } tenantID := pipelineTenantID(claims) var row models.TenantCrmBiddingBid if err := models.Orm.QueryTable(new(models.TenantCrmBiddingBid)). 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.BidNo = strings.TrimSpace(p.BidNo) row.Title = strings.TrimSpace(p.Title) if strings.TrimSpace(p.BidType) != "" { row.BidType = p.BidType } if p.SourceType > 0 { row.SourceType = p.SourceType } row.BusinessID = nilOrID(p.BusinessID) row.BusinessName = strings.TrimSpace(p.BusinessName) row.ProjectID = nilOrID(p.ProjectID) row.ProjectName = strings.TrimSpace(p.ProjectName) row.TenderID = nilOrID(p.TenderID) row.TenderNo = strings.TrimSpace(p.TenderNo) row.CustomerID = nilOrID(p.CustomerID) row.CustomerName = strings.TrimSpace(p.CustomerName) row.PublishOrg = strings.TrimSpace(p.PublishOrg) row.BidAmount = p.BidAmount row.BidDeposit = p.BidDeposit row.PublishDate = parsePipelineDate(p.PublishDate) row.RegisterEndDate = parsePipelineDateTime(p.RegisterEndDate) row.BidEndDate = parsePipelineDateTime(p.BidEndDate) row.OpenBidDate = parsePipelineDateTime(p.OpenBidDate) row.ContactPerson = strings.TrimSpace(p.ContactPerson) row.ContactPhone = strings.TrimSpace(p.ContactPhone) row.BidAddress = strings.TrimSpace(p.BidAddress) row.OpenAddress = strings.TrimSpace(p.OpenAddress) if strings.TrimSpace(p.OwnerUserID) != "" { row.OwnerUserID = strings.TrimSpace(p.OwnerUserID) } if strings.TrimSpace(p.OwnerUserName) != "" { row.OwnerUserName = strings.TrimSpace(p.OwnerUserName) } row.Description = p.Description 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, 4, row.ID, "update", "更新投标:"+row.Title, claims) pipelineOk(&c.Controller, map[string]interface{}{"id": row.ID}) } // Delete DELETE /backend/crm/bidding/bid/:id func (c *BackendCrmBiddingBidController) 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.TenantCrmBiddingBid if err := models.Orm.QueryTable(new(models.TenantCrmBiddingBid)). 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.TenantCrmBiddingBid)). 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) } // ConvertContract POST /backend/crm/bidding/bid/:id/contract 中标转合同 func (c *BackendCrmBiddingBidController) ConvertContract() { 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 bid models.TenantCrmBiddingBid if err := models.Orm.QueryTable(new(models.TenantCrmBiddingBid)). Filter("id", id).Filter("tenant_id", tenantID). Filter("delete_time__isnull", true).One(&bid); err != nil { pipelineErr(&c.Controller, 404, 404, "投标记录未找到") return } if !canAccessCrmRecord(claims, bid.OwnerUserID, bid.CreateUserID) { pipelineErr(&c.Controller, 401, 401, "无权操作该投标数据") return } if bid.WinStatus != 1 { pipelineErr(&c.Controller, 400, 400, "只有中标的投标才能转合同") return } if bid.ContractID != nil && *bid.ContractID > 0 { pipelineErr(&c.Controller, 400, 400, "该投标已转合同,不能重复转") return } contractName := strings.TrimSpace(p.ContractName) if contractName == "" { contractName = bid.Title } category := strings.TrimSpace(p.ContractCategory) if category == "" { category = "2" // 默认:服务类 } now := time.Now() bidIDVal := bid.ID contract := models.TenantCrmContract{ TenantID: tenantID, ContractNo: strings.TrimSpace(p.ContractNo), ContractName: contractName, ContractCategory: category, OurRole: 2, // 我方为乙方 PartyCount: 2, ProjectID: bid.ProjectID, // 中标项目关联 ProjectName: bid.ProjectName, OwnerUserID: bid.OwnerUserID, OwnerUserName: bid.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 } // 更新投标状态:状态置6已转合同,存 contract_id,加锁 cid := uint64(contractID) bid.ContractID = &cid bid.Status = 6 bid.Locked = 1 bidIDForLog := bidIDVal bid.UpdateTime = now if _, err := models.Orm.Update(&bid, "contract_id", "status", "locked", "update_time"); err != nil { pipelineErr(&c.Controller, 500, 500, "更新投标状态失败: "+err.Error()) return } crmWriteLog(tenantID, 4, bidIDForLog, "convert-contract", "中标转合同:"+contractName, claims) crmWriteLog(tenantID, 1, cid, "create", "由投标「"+bid.Title+"」中标转化生成", claims) pipelineOk(&c.Controller, map[string]interface{}{"contract_id": contractID}) } // ConvertProject POST /backend/crm/bidding/bid/:id/project 中标转项目 func (c *BackendCrmBiddingBidController) ConvertProject() { 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 { ProjectName string `json:"project_name"` ProjectNo string `json:"project_no"` Amount float64 `json:"amount"` StartDate string `json:"start_date"` EndDate string `json:"end_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 bid models.TenantCrmBiddingBid if err := models.Orm.QueryTable(new(models.TenantCrmBiddingBid)). Filter("id", id).Filter("tenant_id", tenantID). Filter("delete_time__isnull", true).One(&bid); err != nil { pipelineErr(&c.Controller, 404, 404, "投标记录未找到") return } if !canAccessCrmRecord(claims, bid.OwnerUserID, bid.CreateUserID) { pipelineErr(&c.Controller, 401, 401, "无权操作该投标数据") return } if bid.WinStatus != 1 { pipelineErr(&c.Controller, 400, 400, "只有中标的投标才能转项目") return } if bid.NewProjectID != nil && *bid.NewProjectID > 0 { pipelineErr(&c.Controller, 400, 400, "该投标已转项目,不能重复转") return } projectName := strings.TrimSpace(p.ProjectName) if projectName == "" { projectName = bid.Title } amount := p.Amount if amount == 0 { amount = bid.BidAmount } now := time.Now() bidIDVal := bid.ID proj := models.TenantCrmProject{ TenantID: tenantID, ProjectName: projectName, ProjectNo: strings.TrimSpace(p.ProjectNo), ProjectType: 1, // 单一项目 BusinessID: bid.BusinessID, CustomerID: bid.CustomerID, CustomerName: bid.CustomerName, OwnerUserID: bid.OwnerUserID, OwnerUserName: bid.OwnerUserName, UpstreamAmount: amount, Amount: amount, Status: 1, StartDate: parsePipelineDate(p.StartDate), EndDate: parsePipelineDate(p.EndDate), ContactPerson: bid.ContactPerson, ContactPhone: bid.ContactPhone, Remark: p.Remark, CreateUserID: pipelineUID(claims), CreateTime: now, UpdateTime: now, } projID, err := models.Orm.Insert(&proj) if err != nil { pipelineErr(&c.Controller, 500, 500, "创建项目失败: "+err.Error()) return } pid := uint64(projID) bid.NewProjectID = &pid bid.ProjectID = &pid bid.ProjectName = projectName bid.UpdateTime = now if _, err := models.Orm.Update(&bid, "new_project_id", "project_id", "project_name", "update_time"); err != nil { pipelineErr(&c.Controller, 500, 500, "更新投标状态失败: "+err.Error()) return } crmWriteLog(tenantID, 4, bidIDVal, "convert-project", "中标转项目:"+projectName, claims) crmWriteLog(tenantID, 3, pid, "create", "由投标「"+bid.Title+"」中标转化生成", claims) pipelineOk(&c.Controller, map[string]interface{}{"project_id": projID}) } // SubmitResult POST /backend/crm/bidding/bid/:id/result 提交开标结果 func (c *BackendCrmBiddingBidController) SubmitResult() { 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 { ResultType int8 `json:"result_type"` AnnounceDate string `json:"announce_date"` AnnounceURL string `json:"announce_url"` TotalScore float64 `json:"total_score"` OurRanking uint32 `json:"our_ranking"` CompetitorCount uint32 `json:"competitor_count"` Reason string `json:"reason"` Lessons string `json:"lessons"` NextStep string `json:"next_step"` } 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 bid models.TenantCrmBiddingBid if err := models.Orm.QueryTable(new(models.TenantCrmBiddingBid)). Filter("id", id).Filter("tenant_id", tenantID). Filter("delete_time__isnull", true).One(&bid); err != nil { pipelineErr(&c.Controller, 404, 404, "投标记录未找到") return } if !canAccessCrmRecord(claims, bid.OwnerUserID, bid.CreateUserID) { pipelineErr(&c.Controller, 401, 401, "无权操作该投标数据") return } now := time.Now() result := models.TenantCrmBiddingBidResult{ TenantID: tenantID, BidID: bid.ID, ResultType: p.ResultType, AnnounceDate: parsePipelineDateTime(p.AnnounceDate), AnnounceURL: strings.TrimSpace(p.AnnounceURL), TotalScore: p.TotalScore, OurRanking: p.OurRanking, CompetitorCount: p.CompetitorCount, Reason: p.Reason, Lessons: p.Lessons, NextStep: strings.TrimSpace(p.NextStep), CreateUserID: pipelineUID(claims), CreateUserName: resolveUserName(claims), CreateTime: now, UpdateTime: now, } resultID, err := models.Orm.Insert(&result) if err != nil { pipelineErr(&c.Controller, 500, 500, "保存投标结果失败: "+err.Error()) return } // 同步投标主表状态/得分 bid.WinStatus = p.ResultType bid.WinDate = parsePipelineDateTime(p.AnnounceDate) bid.Status = 3 // 已开标 bid.TotalScore = p.TotalScore bid.OurRanking = p.OurRanking bid.CompetitorCount = p.CompetitorCount if p.ResultType == 1 { bid.Status = 4 // 中标 } else if p.ResultType == 2 { bid.Status = 5 // 落标 } bid.UpdateTime = now if _, err := models.Orm.Update(&bid, "win_status", "win_date", "status", "total_score", "our_ranking", "competitor_count", "update_time"); err != nil { pipelineErr(&c.Controller, 500, 500, "更新投标状态失败: "+err.Error()) return } crmWriteLog(tenantID, 4, bid.ID, "submit-result", fmt.Sprintf("提交开标结果:%s(%s)", resultTypeText(p.ResultType), bid.Title), claims) pipelineOk(&c.Controller, map[string]interface{}{"id": resultID}) } // FromBusiness POST /backend/crm/bidding/bid/from-business 从商机创建投标 func (c *BackendCrmBiddingBidController) FromBusiness() { claims, err := pipelineClaims(&c.Controller) if err != nil { pipelineErr(&c.Controller, 401, 401, err.Error()) return } var p struct { BusinessID uint64 `json:"business_id"` Title string `json:"title"` CustomerID uint64 `json:"customer_id"` CustomerName string `json:"customer_name"` PublishOrg string `json:"publish_org"` BidType string `json:"bid_type"` BidAmount float64 `json:"bid_amount"` BidEndDate string `json:"bid_end_date"` OpenBidDate string `json:"open_bid_date"` OwnerUserID string `json:"owner_user_id"` OwnerUserName string `json:"owner_user_name"` Description string `json:"description"` 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 } if p.BusinessID == 0 { pipelineErr(&c.Controller, 400, 400, "请选择商机") return } tenantID := pipelineTenantID(claims) var biz models.TenantCrmBusiness if err := models.Orm.QueryTable(new(models.TenantCrmBusiness)). Filter("id", p.BusinessID).Filter("tenant_id", tenantID). Filter("delete_time__isnull", true).One(&biz); err != nil { pipelineErr(&c.Controller, 404, 404, "商机未找到") return } if biz.Locked == 1 { pipelineErr(&c.Controller, 400, 400, "商机已转化并锁定,不能再发起投标") return } now := time.Now() title := strings.TrimSpace(p.Title) if title == "" { title = biz.BusinessName } bizID := biz.ID row := models.TenantCrmBiddingBid{ TenantID: tenantID, Title: title, BidType: firstNonEmpty(p.BidType, "1"), Status: 0, SourceType: 1, // 1=商机 SourceID: &bizID, BusinessID: &bizID, BusinessName: biz.BusinessName, CustomerID: biz.CustomerID, CustomerName: firstNonEmpty(p.CustomerName, biz.CustomerName), PublishOrg: firstNonEmpty(p.PublishOrg, biz.CustomerName), BidAmount: p.BidAmount, BidEndDate: parsePipelineDateTime(p.BidEndDate), OpenBidDate: parsePipelineDateTime(p.OpenBidDate), ContactPerson: biz.ContactPerson, ContactPhone: biz.ContactPhone, OwnerUserID: firstNonEmpty(p.OwnerUserID, biz.OwnerUserID, pipelineUID(claims)), OwnerUserName: firstNonEmpty(p.OwnerUserName, biz.OwnerUserName, resolveUserName(claims)), Description: p.Description, Remark: p.Remark, CreateUserID: pipelineUID(claims), CreateUserName: resolveUserName(claims), CreateTime: now, UpdateTime: now, } if crmNameDuplicated(new(models.TenantCrmBiddingBid), 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, 4, uint64(id), "from-business", "从商机「"+biz.BusinessName+"」创建投标", claims) pipelineOk(&c.Controller, map[string]interface{}{"id": id, "business": biz}) } // FromProject POST /backend/crm/bidding/bid/from-project 从项目创建投标 func (c *BackendCrmBiddingBidController) FromProject() { claims, err := pipelineClaims(&c.Controller) if err != nil { pipelineErr(&c.Controller, 401, 401, err.Error()) return } var p struct { ProjectID uint64 `json:"project_id"` Title string `json:"title"` BidType string `json:"bid_type"` BidAmount float64 `json:"bid_amount"` BidEndDate string `json:"bid_end_date"` OpenBidDate string `json:"open_bid_date"` OwnerUserID string `json:"owner_user_id"` OwnerUserName string `json:"owner_user_name"` Description string `json:"description"` 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 } if p.ProjectID == 0 { pipelineErr(&c.Controller, 400, 400, "请选择项目") return } tenantID := pipelineTenantID(claims) var proj models.TenantCrmProject if err := models.Orm.QueryTable(new(models.TenantCrmProject)). Filter("id", p.ProjectID).Filter("tenant_id", tenantID). Filter("delete_time__isnull", true).One(&proj); err != nil { pipelineErr(&c.Controller, 404, 404, "项目未找到") return } now := time.Now() title := strings.TrimSpace(p.Title) if title == "" { title = proj.ProjectName } projID := proj.ID row := models.TenantCrmBiddingBid{ TenantID: tenantID, Title: title, BidType: firstNonEmpty(p.BidType, "1"), Status: 0, SourceType: 2, // 2=项目 SourceID: &projID, ProjectID: &projID, ProjectName: proj.ProjectName, BusinessID: proj.BusinessID, BusinessName: "", // 需要回查商机名(不阻塞,简化处理) CustomerID: proj.CustomerID, CustomerName: proj.CustomerName, BidAmount: p.BidAmount, BidEndDate: parsePipelineDateTime(p.BidEndDate), OpenBidDate: parsePipelineDateTime(p.OpenBidDate), OwnerUserID: firstNonEmpty(p.OwnerUserID, proj.OwnerUserID, pipelineUID(claims)), OwnerUserName: firstNonEmpty(p.OwnerUserName, proj.OwnerUserName, resolveUserName(claims)), Description: p.Description, Remark: p.Remark, CreateUserID: pipelineUID(claims), CreateUserName: resolveUserName(claims), CreateTime: now, UpdateTime: now, } if crmNameDuplicated(new(models.TenantCrmBiddingBid), 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, 4, uint64(id), "from-project", "从项目「"+proj.ProjectName+"」创建投标", claims) pipelineOk(&c.Controller, map[string]interface{}{"id": id, "project": proj}) } // ===================================================================== // 辅助函数 // ===================================================================== // nilOrID 当 id>0 时返回指针,否则返回 nil func nilOrID(id uint64) *uint64 { if id == 0 { return nil } v := id return &v } // resultTypeText 投标结果文本 func resultTypeText(t int8) string { switch t { case 1: return "中标" case 2: return "未中标" case 3: return "废标" case 4: return "流标" default: return "未知" } }