846 lines
28 KiB
Go
846 lines
28 KiB
Go
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 "未知"
|
||
}
|
||
} |