501 lines
17 KiB
Go
501 lines
17 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 招投标管理 - 招标方(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})
|
|
} |