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

846 lines
28 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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 "未知"
}
}