From eafa5e1cf428fc5f8a81a75b691a29451a89cba0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=9D=8E=E5=BF=97=E5=BC=BA?= <357099073@qq.com> Date: Thu, 24 Sep 2026 17:06:46 +0800 Subject: [PATCH] =?UTF-8?q?=E5=A2=9E=E5=8A=A0=E6=8B=9B=E6=8A=95=E6=A0=87?= =?UTF-8?q?=E6=A8=A1=E5=9D=97?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/components.d.ts | 1 + backend/src/api/crmBidding.js | 330 +++++ .../bidding/bid/components/BidApproval.vue | 226 +++ .../bidding/bid/components/BidBaseInfo.vue | 145 ++ .../bidding/bid/components/BidCompetitor.vue | 195 +++ .../bid/components/BidDocumentList.vue | 311 ++++ .../bidding/bid/components/BidExpenseList.vue | 287 ++++ .../bidding/bid/components/BidFileList.vue | 160 +++ .../crm/bidding/bid/components/BidQuote.vue | 389 +++++ .../crm/bidding/bid/components/BidResult.vue | 215 +++ .../bidding/bid/components/BidTaskList.vue | 257 ++++ .../bid/components/convert-contract.vue | 103 ++ .../bid/components/convert-project.vue | 107 ++ .../apps/crm/bidding/bid/components/edit.vue | 152 ++ .../src/views/apps/crm/bidding/bid/create.vue | 237 +++ .../src/views/apps/crm/bidding/bid/detail.vue | 207 +++ .../src/views/apps/crm/bidding/bid/index.vue | 350 +++++ .../crm/bidding/components/BidTimeline.vue | 95 ++ .../crm/bidding/components/SourceSelect.vue | 128 ++ .../apps/crm/bidding/components/StatusTag.vue | 34 + .../crm/bidding/components/TenderTypeTag.vue | 16 + .../apps/crm/bidding/components/index.vue | 5 - backend/src/views/apps/crm/bidding/index.vue | 371 +++++ .../crm/bidding/library/qualification.vue | 436 ++++++ .../apps/crm/bidding/library/template.vue | 303 ++++ .../views/apps/crm/bidding/statistics/bid.vue | 243 ++++ .../apps/crm/bidding/statistics/tender.vue | 192 +++ .../tender/components/TenderAnnounceList.vue | 260 ++++ .../tender/components/TenderBaseInfo.vue | 176 +++ .../tender/components/TenderEvaluation.vue | 346 +++++ .../tender/components/TenderFileList.vue | 200 +++ .../tender/components/TenderResult.vue | 305 ++++ .../tender/components/TenderSupplierList.vue | 292 ++++ .../crm/bidding/tender/components/edit.vue | 322 +++++ .../views/apps/crm/bidding/tender/create.vue | 292 ++++ .../views/apps/crm/bidding/tender/detail.vue | 188 +++ .../views/apps/crm/bidding/tender/index.vue | 274 ++++ backend/src/views/apps/crm/dict.js | 413 ++++++ go/controllers/backend_crm_bidding_bid.go | 846 +++++++++++ go/controllers/backend_crm_bidding_more.go | 1266 +++++++++++++++++ .../backend_crm_bidding_reminder.go | 208 +++ go/controllers/backend_crm_bidding_sub.go | 812 +++++++++++ go/controllers/backend_crm_bidding_tender.go | 501 +++++++ go/docs/sql/bidding_menus.sql | 103 ++ go/docs/sql/create_crm_bidding.sql | 526 +++++++ go/models/init.go | 18 + go/models/tenant_crm_bidding_bid.go | 143 ++ go/models/tenant_crm_bidding_library.go | 113 ++ go/models/tenant_crm_bidding_misc.go | 81 ++ go/models/tenant_crm_bidding_tender.go | 182 +++ go/routers/backend/backend.go | 96 ++ 51 files changed, 13453 insertions(+), 5 deletions(-) create mode 100644 backend/src/api/crmBidding.js create mode 100644 backend/src/views/apps/crm/bidding/bid/components/BidApproval.vue create mode 100644 backend/src/views/apps/crm/bidding/bid/components/BidBaseInfo.vue create mode 100644 backend/src/views/apps/crm/bidding/bid/components/BidCompetitor.vue create mode 100644 backend/src/views/apps/crm/bidding/bid/components/BidDocumentList.vue create mode 100644 backend/src/views/apps/crm/bidding/bid/components/BidExpenseList.vue create mode 100644 backend/src/views/apps/crm/bidding/bid/components/BidFileList.vue create mode 100644 backend/src/views/apps/crm/bidding/bid/components/BidQuote.vue create mode 100644 backend/src/views/apps/crm/bidding/bid/components/BidResult.vue create mode 100644 backend/src/views/apps/crm/bidding/bid/components/BidTaskList.vue create mode 100644 backend/src/views/apps/crm/bidding/bid/components/convert-contract.vue create mode 100644 backend/src/views/apps/crm/bidding/bid/components/convert-project.vue create mode 100644 backend/src/views/apps/crm/bidding/bid/components/edit.vue create mode 100644 backend/src/views/apps/crm/bidding/bid/create.vue create mode 100644 backend/src/views/apps/crm/bidding/bid/detail.vue create mode 100644 backend/src/views/apps/crm/bidding/bid/index.vue create mode 100644 backend/src/views/apps/crm/bidding/components/BidTimeline.vue create mode 100644 backend/src/views/apps/crm/bidding/components/SourceSelect.vue create mode 100644 backend/src/views/apps/crm/bidding/components/StatusTag.vue create mode 100644 backend/src/views/apps/crm/bidding/components/TenderTypeTag.vue delete mode 100644 backend/src/views/apps/crm/bidding/components/index.vue create mode 100644 backend/src/views/apps/crm/bidding/index.vue create mode 100644 backend/src/views/apps/crm/bidding/library/qualification.vue create mode 100644 backend/src/views/apps/crm/bidding/library/template.vue create mode 100644 backend/src/views/apps/crm/bidding/statistics/bid.vue create mode 100644 backend/src/views/apps/crm/bidding/statistics/tender.vue create mode 100644 backend/src/views/apps/crm/bidding/tender/components/TenderAnnounceList.vue create mode 100644 backend/src/views/apps/crm/bidding/tender/components/TenderBaseInfo.vue create mode 100644 backend/src/views/apps/crm/bidding/tender/components/TenderEvaluation.vue create mode 100644 backend/src/views/apps/crm/bidding/tender/components/TenderFileList.vue create mode 100644 backend/src/views/apps/crm/bidding/tender/components/TenderResult.vue create mode 100644 backend/src/views/apps/crm/bidding/tender/components/TenderSupplierList.vue create mode 100644 backend/src/views/apps/crm/bidding/tender/components/edit.vue create mode 100644 backend/src/views/apps/crm/bidding/tender/create.vue create mode 100644 backend/src/views/apps/crm/bidding/tender/detail.vue create mode 100644 backend/src/views/apps/crm/bidding/tender/index.vue create mode 100644 go/controllers/backend_crm_bidding_bid.go create mode 100644 go/controllers/backend_crm_bidding_more.go create mode 100644 go/controllers/backend_crm_bidding_reminder.go create mode 100644 go/controllers/backend_crm_bidding_sub.go create mode 100644 go/controllers/backend_crm_bidding_tender.go create mode 100644 go/docs/sql/bidding_menus.sql create mode 100644 go/docs/sql/create_crm_bidding.sql create mode 100644 go/models/tenant_crm_bidding_bid.go create mode 100644 go/models/tenant_crm_bidding_library.go create mode 100644 go/models/tenant_crm_bidding_misc.go create mode 100644 go/models/tenant_crm_bidding_tender.go diff --git a/backend/components.d.ts b/backend/components.d.ts index b2c4634..4e1b2cb 100644 --- a/backend/components.d.ts +++ b/backend/components.d.ts @@ -65,6 +65,7 @@ declare module 'vue' { ElScrollbar: typeof import('element-plus/es')['ElScrollbar'] ElSelect: typeof import('element-plus/es')['ElSelect'] ElSkeleton: typeof import('element-plus/es')['ElSkeleton'] + ElSlider: typeof import('element-plus/es')['ElSlider'] ElStep: typeof import('element-plus/es')['ElStep'] ElSteps: typeof import('element-plus/es')['ElSteps'] ElSubMenu: typeof import('element-plus/es')['ElSubMenu'] diff --git a/backend/src/api/crmBidding.js b/backend/src/api/crmBidding.js new file mode 100644 index 0000000..a0941a5 --- /dev/null +++ b/backend/src/api/crmBidding.js @@ -0,0 +1,330 @@ +import request from "@/utils/request"; + +/** + * CRM 招投标管理接口 + * + * 关联类型 related_type:4=投标 5=招标 + * 投标状态机:0草稿 -> 1立项审批中 -> 2投标中 -> 3已开标 -> (4中标 | 5落标) -> (6已转合同 | 7已归档) + * 招标状态机:0草稿 -> 1公告中 -> 2报名中 -> 3投标中 -> 4开标中 -> 5评标中 -> 6已定标 -> 7已归档 + */ + +/* ====================== 投标(Bid)主表 ====================== */ + +export function getBidList(params) { + return request({ url: "/backend/crm/bidding/bid/list", method: "get", params }); +} + +export function getBidDetail(id) { + return request({ url: `/backend/crm/bidding/bid/detail/${id}`, method: "get" }); +} + +export function createBid(data) { + return request({ url: "/backend/crm/bidding/bid", method: "post", data }); +} + +export function updateBid(id, data) { + return request({ url: `/backend/crm/bidding/bid/${id}`, method: "put", data }); +} + +export function deleteBid(id) { + return request({ url: `/backend/crm/bidding/bid/${id}`, method: "delete" }); +} + +/** 中标转合同 */ +export function convertBidToContract(id, data) { + return request({ url: `/backend/crm/bidding/bid/${id}/contract`, method: "post", data }); +} + +/** 中标转项目 */ +export function convertBidToProject(id, data) { + return request({ url: `/backend/crm/bidding/bid/${id}/project`, method: "post", data }); +} + +/** 提交开标结果 */ +export function submitBidResult(id, data) { + return request({ url: `/backend/crm/bidding/bid/${id}/result`, method: "post", data }); +} + +/** 从商机创建投标 */ +export function createBidFromBusiness(data) { + return request({ url: "/backend/crm/bidding/bid/from-business", method: "post", data }); +} + +/** 从项目创建投标 */ +export function createBidFromProject(data) { + return request({ url: "/backend/crm/bidding/bid/from-project", method: "post", data }); +} + +/* ====================== 投标文件 ====================== */ + +export function getBidFileList(params) { + return request({ url: "/backend/crm/bidding/bid-file/list", method: "get", params }); +} + +export function createBidFile(data) { + return request({ url: "/backend/crm/bidding/bid-file", method: "post", data }); +} + +export function deleteBidFile(id) { + return request({ url: `/backend/crm/bidding/bid-file/${id}`, method: "delete" }); +} + +/* ====================== 投标任务 ====================== */ + +export function getBidTaskList(params) { + return request({ url: "/backend/crm/bidding/bid-task/list", method: "get", params }); +} + +export function createBidTask(data) { + return request({ url: "/backend/crm/bidding/bid-task", method: "post", data }); +} + +export function updateBidTask(id, data) { + return request({ url: `/backend/crm/bidding/bid-task/${id}`, method: "put", data }); +} + +export function deleteBidTask(id) { + return request({ url: `/backend/crm/bidding/bid-task/${id}`, method: "delete" }); +} + +/* ====================== 投标结果 ====================== */ + +export function getBidResultList(params) { + return request({ url: "/backend/crm/bidding/bid-result/list", method: "get", params }); +} + +export function getBidResultDetail(id) { + return request({ url: `/backend/crm/bidding/bid-result/detail/${id}`, method: "get" }); +} + +/* ====================== 投标报价 ====================== */ + +export function getBidQuoteList(params) { + return request({ url: "/backend/crm/bidding/bid-quote/list", method: "get", params }); +} + +export function createBidQuote(data) { + return request({ url: "/backend/crm/bidding/bid-quote", method: "post", data }); +} + +export function deleteBidQuote(id) { + return request({ url: `/backend/crm/bidding/bid-quote/${id}`, method: "delete" }); +} + +/* ====================== 投标竞争对手 ====================== */ + +export function getBidCompetitorList(params) { + return request({ url: "/backend/crm/bidding/bid-competitor/list", method: "get", params }); +} + +export function createBidCompetitor(data) { + return request({ url: "/backend/crm/bidding/bid-competitor", method: "post", data }); +} + +export function deleteBidCompetitor(id) { + return request({ url: `/backend/crm/bidding/bid-competitor/${id}`, method: "delete" }); +} + +/* ====================== 投标费用 ====================== */ + +export function getBidExpenseList(params) { + return request({ url: "/backend/crm/bidding/bid-expense/list", method: "get", params }); +} + +export function createBidExpense(data) { + return request({ url: "/backend/crm/bidding/bid-expense", method: "post", data }); +} + +export function deleteBidExpense(id) { + return request({ url: `/backend/crm/bidding/bid-expense/${id}`, method: "delete" }); +} + +/* ====================== 招标(Tender)主表 ====================== */ + +export function getTenderList(params) { + return request({ url: "/backend/crm/bidding/tender/list", method: "get", params }); +} + +export function getTenderDetail(id) { + return request({ url: `/backend/crm/bidding/tender/detail/${id}`, method: "get" }); +} + +export function createTender(data) { + return request({ url: "/backend/crm/bidding/tender", method: "post", data }); +} + +export function updateTender(id, data) { + return request({ url: `/backend/crm/bidding/tender/${id}`, method: "put", data }); +} + +export function deleteTender(id) { + return request({ url: `/backend/crm/bidding/tender/${id}`, method: "delete" }); +} + +/** 定标 */ +export function awardTender(id, data) { + return request({ url: `/backend/crm/bidding/tender/${id}/award`, method: "post", data }); +} + +/** 定标转合同 */ +export function awardTenderToContract(id, data) { + return request({ url: `/backend/crm/bidding/tender/${id}/contract`, method: "post", data }); +} + +/* ====================== 招标文件 ====================== */ + +export function getTenderFileList(params) { + return request({ url: "/backend/crm/bidding/tender-file/list", method: "get", params }); +} + +export function createTenderFile(data) { + return request({ url: "/backend/crm/bidding/tender-file", method: "post", data }); +} + +export function deleteTenderFile(id) { + return request({ url: `/backend/crm/bidding/tender-file/${id}`, method: "delete" }); +} + +/* ====================== 招标公告 ====================== */ + +export function getTenderAnnounceList(params) { + return request({ url: "/backend/crm/bidding/tender-announce/list", method: "get", params }); +} + +export function createTenderAnnounce(data) { + return request({ url: "/backend/crm/bidding/tender-announce", method: "post", data }); +} + +export function publishTenderAnnounce(id) { + return request({ url: `/backend/crm/bidding/tender-announce/${id}/publish`, method: "post" }); +} + +export function deleteTenderAnnounce(id) { + return request({ url: `/backend/crm/bidding/tender-announce/${id}`, method: "delete" }); +} + +/* ====================== 投标方 / 供应商 ====================== */ + +export function getTenderSupplierList(params) { + return request({ url: "/backend/crm/bidding/tender-supplier/list", method: "get", params }); +} + +export function createTenderSupplier(data) { + return request({ url: "/backend/crm/bidding/tender-supplier", method: "post", data }); +} + +export function updateTenderSupplier(id, data) { + return request({ url: `/backend/crm/bidding/tender-supplier/${id}`, method: "put", data }); +} + +export function deleteTenderSupplier(id) { + return request({ url: `/backend/crm/bidding/tender-supplier/${id}`, method: "delete" }); +} + +/* ====================== 开标评标 ====================== */ + +export function getTenderEvaluationList(params) { + return request({ url: "/backend/crm/bidding/tender-evaluation/list", method: "get", params }); +} + +export function createTenderEvaluation(data) { + return request({ url: "/backend/crm/bidding/tender-evaluation", method: "post", data }); +} + +export function deleteTenderEvaluation(id) { + return request({ url: `/backend/crm/bidding/tender-evaluation/${id}`, method: "delete" }); +} + +/* ====================== 定标结果 ====================== */ + +export function getTenderResultList(params) { + return request({ url: "/backend/crm/bidding/tender-result/list", method: "get", params }); +} + +export function getTenderResultDetail(id) { + return request({ url: `/backend/crm/bidding/tender-result/detail/${id}`, method: "get" }); +} + +/* ====================== 投标立项审批 ====================== */ + +export function getBidApprovalList(params) { + return request({ url: "/backend/crm/bidding/bid-approval/list", method: "get", params }); +} + +export function createBidApproval(data) { + return request({ url: "/backend/crm/bidding/bid-approval", method: "post", data }); +} + +export function approveBidApproval(id, data) { + return request({ url: `/backend/crm/bidding/bid-approval/${id}/approve`, method: "post", data }); +} + +/* ====================== 标书版本 ====================== */ + +export function getBidDocumentList(params) { + return request({ url: "/backend/crm/bidding/bid-document/list", method: "get", params }); +} + +export function createBidDocument(data) { + return request({ url: "/backend/crm/bidding/bid-document", method: "post", data }); +} + +export function updateBidDocument(id, data) { + return request({ url: `/backend/crm/bidding/bid-document/${id}`, method: "put", data }); +} + +export function deleteBidDocument(id) { + return request({ url: `/backend/crm/bidding/bid-document/${id}`, method: "delete" }); +} + +/* ====================== 标书模板库 ====================== */ + +export function getTemplateList(params) { + return request({ url: "/backend/crm/bidding/template/list", method: "get", params }); +} + +export function createTemplate(data) { + return request({ url: "/backend/crm/bidding/template", method: "post", data }); +} + +export function updateTemplate(id, data) { + return request({ url: `/backend/crm/bidding/template/${id}`, method: "put", data }); +} + +export function deleteTemplate(id) { + return request({ url: `/backend/crm/bidding/template/${id}`, method: "delete" }); +} + +/* ====================== 资质文件库 ====================== */ + +export function getQualificationList(params) { + return request({ url: "/backend/crm/bidding/qualification/list", method: "get", params }); +} + +export function createQualification(data) { + return request({ url: "/backend/crm/bidding/qualification", method: "post", data }); +} + +export function updateQualification(id, data) { + return request({ url: `/backend/crm/bidding/qualification/${id}`, method: "put", data }); +} + +export function deleteQualification(id) { + return request({ url: `/backend/crm/bidding/qualification/${id}`, method: "delete" }); +} + +/* ====================== 统计 ====================== */ + +export function getTenderStatistics(params) { + return request({ url: "/backend/crm/bidding/statistics/tender", method: "get", params }); +} + +export function getBidStatistics(params) { + return request({ url: "/backend/crm/bidding/statistics/bid", method: "get", params }); +} + +/* ====================== 到期提醒(开发计划 4.1) ====================== */ + +export function getBiddingReminders(params) { + return request({ url: "/backend/crm/bidding/reminders", method: "get", params }); +} \ No newline at end of file diff --git a/backend/src/views/apps/crm/bidding/bid/components/BidApproval.vue b/backend/src/views/apps/crm/bidding/bid/components/BidApproval.vue new file mode 100644 index 0000000..cae58e3 --- /dev/null +++ b/backend/src/views/apps/crm/bidding/bid/components/BidApproval.vue @@ -0,0 +1,226 @@ + + + + + diff --git a/backend/src/views/apps/crm/bidding/bid/components/BidBaseInfo.vue b/backend/src/views/apps/crm/bidding/bid/components/BidBaseInfo.vue new file mode 100644 index 0000000..fc91b8a --- /dev/null +++ b/backend/src/views/apps/crm/bidding/bid/components/BidBaseInfo.vue @@ -0,0 +1,145 @@ + + + + + \ No newline at end of file diff --git a/backend/src/views/apps/crm/bidding/bid/components/BidCompetitor.vue b/backend/src/views/apps/crm/bidding/bid/components/BidCompetitor.vue new file mode 100644 index 0000000..a1f432e --- /dev/null +++ b/backend/src/views/apps/crm/bidding/bid/components/BidCompetitor.vue @@ -0,0 +1,195 @@ + + + + + diff --git a/backend/src/views/apps/crm/bidding/bid/components/BidDocumentList.vue b/backend/src/views/apps/crm/bidding/bid/components/BidDocumentList.vue new file mode 100644 index 0000000..2a67d8d --- /dev/null +++ b/backend/src/views/apps/crm/bidding/bid/components/BidDocumentList.vue @@ -0,0 +1,311 @@ + + + + + diff --git a/backend/src/views/apps/crm/bidding/bid/components/BidExpenseList.vue b/backend/src/views/apps/crm/bidding/bid/components/BidExpenseList.vue new file mode 100644 index 0000000..66cde82 --- /dev/null +++ b/backend/src/views/apps/crm/bidding/bid/components/BidExpenseList.vue @@ -0,0 +1,287 @@ + + + + + diff --git a/backend/src/views/apps/crm/bidding/bid/components/BidFileList.vue b/backend/src/views/apps/crm/bidding/bid/components/BidFileList.vue new file mode 100644 index 0000000..fbddf9d --- /dev/null +++ b/backend/src/views/apps/crm/bidding/bid/components/BidFileList.vue @@ -0,0 +1,160 @@ + + + + + \ No newline at end of file diff --git a/backend/src/views/apps/crm/bidding/bid/components/BidQuote.vue b/backend/src/views/apps/crm/bidding/bid/components/BidQuote.vue new file mode 100644 index 0000000..9a2e9fa --- /dev/null +++ b/backend/src/views/apps/crm/bidding/bid/components/BidQuote.vue @@ -0,0 +1,389 @@ + + + + + diff --git a/backend/src/views/apps/crm/bidding/bid/components/BidResult.vue b/backend/src/views/apps/crm/bidding/bid/components/BidResult.vue new file mode 100644 index 0000000..75f8e64 --- /dev/null +++ b/backend/src/views/apps/crm/bidding/bid/components/BidResult.vue @@ -0,0 +1,215 @@ + + + + + \ No newline at end of file diff --git a/backend/src/views/apps/crm/bidding/bid/components/BidTaskList.vue b/backend/src/views/apps/crm/bidding/bid/components/BidTaskList.vue new file mode 100644 index 0000000..1e88117 --- /dev/null +++ b/backend/src/views/apps/crm/bidding/bid/components/BidTaskList.vue @@ -0,0 +1,257 @@ + + + + + \ No newline at end of file diff --git a/backend/src/views/apps/crm/bidding/bid/components/convert-contract.vue b/backend/src/views/apps/crm/bidding/bid/components/convert-contract.vue new file mode 100644 index 0000000..365e79d --- /dev/null +++ b/backend/src/views/apps/crm/bidding/bid/components/convert-contract.vue @@ -0,0 +1,103 @@ + + + \ No newline at end of file diff --git a/backend/src/views/apps/crm/bidding/bid/components/convert-project.vue b/backend/src/views/apps/crm/bidding/bid/components/convert-project.vue new file mode 100644 index 0000000..0ef6e0f --- /dev/null +++ b/backend/src/views/apps/crm/bidding/bid/components/convert-project.vue @@ -0,0 +1,107 @@ + + + \ No newline at end of file diff --git a/backend/src/views/apps/crm/bidding/bid/components/edit.vue b/backend/src/views/apps/crm/bidding/bid/components/edit.vue new file mode 100644 index 0000000..8304cf2 --- /dev/null +++ b/backend/src/views/apps/crm/bidding/bid/components/edit.vue @@ -0,0 +1,152 @@ + + + \ No newline at end of file diff --git a/backend/src/views/apps/crm/bidding/bid/create.vue b/backend/src/views/apps/crm/bidding/bid/create.vue new file mode 100644 index 0000000..71f238b --- /dev/null +++ b/backend/src/views/apps/crm/bidding/bid/create.vue @@ -0,0 +1,237 @@ + + + + + \ No newline at end of file diff --git a/backend/src/views/apps/crm/bidding/bid/detail.vue b/backend/src/views/apps/crm/bidding/bid/detail.vue new file mode 100644 index 0000000..ed36af0 --- /dev/null +++ b/backend/src/views/apps/crm/bidding/bid/detail.vue @@ -0,0 +1,207 @@ + + + + + diff --git a/backend/src/views/apps/crm/bidding/bid/index.vue b/backend/src/views/apps/crm/bidding/bid/index.vue new file mode 100644 index 0000000..64d902e --- /dev/null +++ b/backend/src/views/apps/crm/bidding/bid/index.vue @@ -0,0 +1,350 @@ + + + + + \ No newline at end of file diff --git a/backend/src/views/apps/crm/bidding/components/BidTimeline.vue b/backend/src/views/apps/crm/bidding/components/BidTimeline.vue new file mode 100644 index 0000000..7d17338 --- /dev/null +++ b/backend/src/views/apps/crm/bidding/components/BidTimeline.vue @@ -0,0 +1,95 @@ + + + + + diff --git a/backend/src/views/apps/crm/bidding/components/SourceSelect.vue b/backend/src/views/apps/crm/bidding/components/SourceSelect.vue new file mode 100644 index 0000000..c4b8665 --- /dev/null +++ b/backend/src/views/apps/crm/bidding/components/SourceSelect.vue @@ -0,0 +1,128 @@ + + + + + diff --git a/backend/src/views/apps/crm/bidding/components/StatusTag.vue b/backend/src/views/apps/crm/bidding/components/StatusTag.vue new file mode 100644 index 0000000..f09a83f --- /dev/null +++ b/backend/src/views/apps/crm/bidding/components/StatusTag.vue @@ -0,0 +1,34 @@ + + + diff --git a/backend/src/views/apps/crm/bidding/components/TenderTypeTag.vue b/backend/src/views/apps/crm/bidding/components/TenderTypeTag.vue new file mode 100644 index 0000000..eca8334 --- /dev/null +++ b/backend/src/views/apps/crm/bidding/components/TenderTypeTag.vue @@ -0,0 +1,16 @@ + + + diff --git a/backend/src/views/apps/crm/bidding/components/index.vue b/backend/src/views/apps/crm/bidding/components/index.vue deleted file mode 100644 index 923a537..0000000 --- a/backend/src/views/apps/crm/bidding/components/index.vue +++ /dev/null @@ -1,5 +0,0 @@ - - - diff --git a/backend/src/views/apps/crm/bidding/index.vue b/backend/src/views/apps/crm/bidding/index.vue new file mode 100644 index 0000000..b90c3e6 --- /dev/null +++ b/backend/src/views/apps/crm/bidding/index.vue @@ -0,0 +1,371 @@ + + + + + \ No newline at end of file diff --git a/backend/src/views/apps/crm/bidding/library/qualification.vue b/backend/src/views/apps/crm/bidding/library/qualification.vue new file mode 100644 index 0000000..c4e1b36 --- /dev/null +++ b/backend/src/views/apps/crm/bidding/library/qualification.vue @@ -0,0 +1,436 @@ + + + + + + + diff --git a/backend/src/views/apps/crm/bidding/library/template.vue b/backend/src/views/apps/crm/bidding/library/template.vue new file mode 100644 index 0000000..e703df5 --- /dev/null +++ b/backend/src/views/apps/crm/bidding/library/template.vue @@ -0,0 +1,303 @@ + + + + + + + diff --git a/backend/src/views/apps/crm/bidding/statistics/bid.vue b/backend/src/views/apps/crm/bidding/statistics/bid.vue new file mode 100644 index 0000000..e6ffb90 --- /dev/null +++ b/backend/src/views/apps/crm/bidding/statistics/bid.vue @@ -0,0 +1,243 @@ + + + + + + + diff --git a/backend/src/views/apps/crm/bidding/statistics/tender.vue b/backend/src/views/apps/crm/bidding/statistics/tender.vue new file mode 100644 index 0000000..1c59905 --- /dev/null +++ b/backend/src/views/apps/crm/bidding/statistics/tender.vue @@ -0,0 +1,192 @@ + + + + + + + diff --git a/backend/src/views/apps/crm/bidding/tender/components/TenderAnnounceList.vue b/backend/src/views/apps/crm/bidding/tender/components/TenderAnnounceList.vue new file mode 100644 index 0000000..c729c8c --- /dev/null +++ b/backend/src/views/apps/crm/bidding/tender/components/TenderAnnounceList.vue @@ -0,0 +1,260 @@ + + + + + diff --git a/backend/src/views/apps/crm/bidding/tender/components/TenderBaseInfo.vue b/backend/src/views/apps/crm/bidding/tender/components/TenderBaseInfo.vue new file mode 100644 index 0000000..2b6cfcd --- /dev/null +++ b/backend/src/views/apps/crm/bidding/tender/components/TenderBaseInfo.vue @@ -0,0 +1,176 @@ + + + + + diff --git a/backend/src/views/apps/crm/bidding/tender/components/TenderEvaluation.vue b/backend/src/views/apps/crm/bidding/tender/components/TenderEvaluation.vue new file mode 100644 index 0000000..2a956f1 --- /dev/null +++ b/backend/src/views/apps/crm/bidding/tender/components/TenderEvaluation.vue @@ -0,0 +1,346 @@ + + + + + diff --git a/backend/src/views/apps/crm/bidding/tender/components/TenderFileList.vue b/backend/src/views/apps/crm/bidding/tender/components/TenderFileList.vue new file mode 100644 index 0000000..41b1151 --- /dev/null +++ b/backend/src/views/apps/crm/bidding/tender/components/TenderFileList.vue @@ -0,0 +1,200 @@ + + + + + diff --git a/backend/src/views/apps/crm/bidding/tender/components/TenderResult.vue b/backend/src/views/apps/crm/bidding/tender/components/TenderResult.vue new file mode 100644 index 0000000..1f29015 --- /dev/null +++ b/backend/src/views/apps/crm/bidding/tender/components/TenderResult.vue @@ -0,0 +1,305 @@ + + + + + diff --git a/backend/src/views/apps/crm/bidding/tender/components/TenderSupplierList.vue b/backend/src/views/apps/crm/bidding/tender/components/TenderSupplierList.vue new file mode 100644 index 0000000..5421cfc --- /dev/null +++ b/backend/src/views/apps/crm/bidding/tender/components/TenderSupplierList.vue @@ -0,0 +1,292 @@ + + + + + diff --git a/backend/src/views/apps/crm/bidding/tender/components/edit.vue b/backend/src/views/apps/crm/bidding/tender/components/edit.vue new file mode 100644 index 0000000..5ee4afb --- /dev/null +++ b/backend/src/views/apps/crm/bidding/tender/components/edit.vue @@ -0,0 +1,322 @@ + + + + + diff --git a/backend/src/views/apps/crm/bidding/tender/create.vue b/backend/src/views/apps/crm/bidding/tender/create.vue new file mode 100644 index 0000000..2267ae6 --- /dev/null +++ b/backend/src/views/apps/crm/bidding/tender/create.vue @@ -0,0 +1,292 @@ + + + + + diff --git a/backend/src/views/apps/crm/bidding/tender/detail.vue b/backend/src/views/apps/crm/bidding/tender/detail.vue new file mode 100644 index 0000000..9b43d11 --- /dev/null +++ b/backend/src/views/apps/crm/bidding/tender/detail.vue @@ -0,0 +1,188 @@ + + + + + diff --git a/backend/src/views/apps/crm/bidding/tender/index.vue b/backend/src/views/apps/crm/bidding/tender/index.vue new file mode 100644 index 0000000..a2c3ae4 --- /dev/null +++ b/backend/src/views/apps/crm/bidding/tender/index.vue @@ -0,0 +1,274 @@ + + + + + + + diff --git a/backend/src/views/apps/crm/dict.js b/backend/src/views/apps/crm/dict.js index 5575afd..6d75769 100644 --- a/backend/src/views/apps/crm/dict.js +++ b/backend/src/views/apps/crm/dict.js @@ -716,3 +716,416 @@ export const paybackStatusTag = (val) => PAYBACK_STATUS_TAG[normalize(val)] || " export const paybackItemStatusText = (val) => PAYBACK_ITEM_STATUS_MAP[normalize(val)] || normalize(val) || "-"; export const paybackItemStatusTag = (val) => PAYBACK_ITEM_STATUS_TAG[normalize(val)] || "info"; + +/* ===================================================================== + * 招投标管理(CRM-我要招标/我要投标) + * ===================================================================== */ + +/** 招标类型:1公开 2邀请 3竞争性谈判 4询比 5单一来源 */ +export const TENDER_TYPE_OPTIONS = [ + { label: "公开招标", value: "1" }, + { label: "邀请招标", value: "2" }, + { label: "竞争性谈判", value: "3" }, + { label: "询比采购", value: "4" }, + { label: "单一来源", value: "5" }, +]; + +/** 招标状态:0草稿 1公告中 2报名中 3投标中 4开标中 5评标中 6已定标 7已归档 */ +export const TENDER_STATUS_OPTIONS = [ + { label: "草稿", value: 0 }, + { label: "公告中", value: 1 }, + { label: "报名中", value: 2 }, + { label: "投标中", value: 3 }, + { label: "开标中", value: 4 }, + { label: "评标中", value: 5 }, + { label: "已定标", value: 6 }, + { label: "已归档", value: 7 }, +]; + +/** 评标方法:1综合评估 2最低价 3性价比 */ +export const EVALUATION_METHOD_OPTIONS = [ + { label: "综合评估法", value: "1" }, + { label: "最低价法", value: "2" }, + { label: "性价比法", value: "3" }, +]; + +/** 定标类型:1定标 2流标 3废标 */ +export const TENDER_RESULT_TYPE_OPTIONS = [ + { label: "定标", value: 1 }, + { label: "流标", value: 2 }, + { label: "废标", value: 3 }, +]; + +/** 投标类型(同招标类型) */ +export const BID_TYPE_OPTIONS = TENDER_TYPE_OPTIONS; + +/** 投标状态:0草稿 1立项审批中 2投标中 3已开标 4中标 5落标 6已转合同 7已归档 */ +export const BID_STATUS_OPTIONS = [ + { label: "草稿", value: 0 }, + { label: "立项审批中", value: 1 }, + { label: "投标中", value: 2 }, + { label: "已开标", value: 3 }, + { label: "中标", value: 4 }, + { label: "落标", value: 5 }, + { label: "已转合同", value: 6 }, + { label: "已归档", value: 7 }, +]; + +/** 投标来源:0独立 1商机 2项目 */ +export const BID_SOURCE_TYPE_OPTIONS = [ + { label: "独立创建", value: 0 }, + { label: "商机", value: 1 }, + { label: "项目", value: 2 }, +]; + +/** 投标结果:1中标 2未中标 3废标 4流标 */ +export const BID_RESULT_TYPE_OPTIONS = [ + { label: "中标", value: 1 }, + { label: "未中标", value: 2 }, + { label: "废标", value: 3 }, + { label: "流标", value: 4 }, +]; + +/** 投标方报名状态:1待审核 2已通过 3已拒绝 4已退标 */ +export const TENDER_SUPPLIER_STATUS_OPTIONS = [ + { label: "待审核", value: 1 }, + { label: "已通过", value: 2 }, + { label: "已拒绝", value: 3 }, + { label: "已退标", value: 4 }, +]; + +/** 招标公告类型:1招标公告 2变更公告 3中标公告 4终止公告 */ +export const TENDER_ANNOUNCE_TYPE_OPTIONS = [ + { label: "招标公告", value: "1" }, + { label: "变更公告", value: "2" }, + { label: "中标公告", value: "3" }, + { label: "终止公告", value: "4" }, +]; + +/** 评标结论:0待评 1合格 2不合格 3废标 */ +export const EVALUATION_RESULT_OPTIONS = [ + { label: "待评", value: 0 }, + { label: "合格", value: 1 }, + { label: "不合格", value: 2 }, + { label: "废标", value: 3 }, +]; + +/** 任务类型:1商务 2技术 3报价 4法务 5财务 6其他 */ +export const BID_TASK_TYPE_OPTIONS = [ + { label: "商务", value: "1" }, + { label: "技术", value: "2" }, + { label: "报价", value: "3" }, + { label: "法务", value: "4" }, + { label: "财务", value: "5" }, + { label: "其他", value: "6" }, +]; + +/** 任务状态:0未开始 1进行中 2已完成 3已逾期 4已取消 */ +export const BID_TASK_STATUS_OPTIONS = [ + { label: "未开始", value: 0 }, + { label: "进行中", value: 1 }, + { label: "已完成", value: 2 }, + { label: "已逾期", value: 3 }, + { label: "已取消", value: 4 }, +]; + +/** 标书模板类型:1技术标 2商务标 3综合标 */ +export const BID_TEMPLATE_TYPE_OPTIONS = [ + { label: "技术标", value: "1" }, + { label: "商务标", value: "2" }, + { label: "综合标", value: "3" }, +]; + +/** 资质类型:1营业执照 2资质证书 3业绩证明 4认证证书 5其他 */ +export const QUALIFICATION_TYPE_OPTIONS = [ + { label: "营业执照", value: "1" }, + { label: "资质证书", value: "2" }, + { label: "业绩证明", value: "3" }, + { label: "认证证书", value: "4" }, + { label: "其他", value: "5" }, +]; + +/** 资质状态:1有效 2即将过期 0失效 */ +export const QUALIFICATION_STATUS_OPTIONS = [ + { label: "有效", value: 1 }, + { label: "即将过期", value: 2 }, + { label: "失效", value: 0 }, +]; + +/** 报价单状态:1草稿 2待审批 3已审批 4已使用 */ +export const BID_QUOTE_STATUS_OPTIONS = [ + { label: "草稿", value: 1 }, + { label: "待审批", value: 2 }, + { label: "已审批", value: 3 }, + { label: "已使用", value: 4 }, +]; + +/** 竞争对手类型:1同类厂商 2关联企业 3其他 */ +export const COMPETITOR_TYPE_OPTIONS = [ + { label: "同类厂商", value: 1 }, + { label: "关联企业", value: 2 }, + { label: "其他", value: 3 }, +]; + +/** 投标费用类型:1标书费 2保证金 3差旅费 4咨询费 5代理费 6其他 */ +export const BID_EXPENSE_TYPE_OPTIONS = [ + { label: "标书费", value: 1 }, + { label: "保证金", value: 2 }, + { label: "差旅费", value: 3 }, + { label: "咨询费", value: 4 }, + { label: "代理费", value: 5 }, + { label: "其他", value: 6 }, +]; + +/** 费用支付状态:0未付 1已付 2已退还 3已抵扣 */ +export const PAYMENT_STATUS_OPTIONS = [ + { label: "未付", value: 0 }, + { label: "已付", value: 1 }, + { label: "已退还", value: 2 }, + { label: "已抵扣", value: 3 }, +]; + +/** 标书版本状态:1编辑中 2待审核 3已定稿 4已提交 */ +export const BID_DOCUMENT_STATUS_OPTIONS = [ + { label: "编辑中", value: 1 }, + { label: "待审核", value: 2 }, + { label: "已定稿", value: 3 }, + { label: "已提交", value: 4 }, +]; + +/** 立项审批状态:0待审批 1已通过 2已拒绝 3已撤回 */ +export const APPROVAL_STATUS_OPTIONS = [ + { label: "待审批", value: 0 }, + { label: "已通过", value: 1 }, + { label: "已拒绝", value: 2 }, + { label: "已撤回", value: 3 }, +]; + +/** 审批类型:1立项审批 2报价审批 3合同审批 */ +export const APPROVAL_TYPE_OPTIONS = [ + { label: "立项审批", value: "1" }, + { label: "报价审批", value: "2" }, + { label: "合同审批", value: "3" }, +]; + +/* ---------------- 派生 MAP / TAG / 工具函数 ---------------- */ + +const TENDER_STATUS_MAP = TENDER_STATUS_OPTIONS.reduce((m, i) => ((m[i.value] = i.label), m), {}); +const TENDER_STATUS_TAG = { + 0: "info", + 1: "primary", + 2: "primary", + 3: "warning", + 4: "warning", + 5: "warning", + 6: "success", + 7: "info", +}; + +const BID_STATUS_MAP = BID_STATUS_OPTIONS.reduce((m, i) => ((m[i.value] = i.label), m), {}); +const BID_STATUS_TAG = { + 0: "info", + 1: "primary", + 2: "warning", + 3: "warning", + 4: "success", + 5: "danger", + 6: "success", + 7: "info", +}; + +const BID_RESULT_TYPE_MAP = BID_RESULT_TYPE_OPTIONS.reduce((m, i) => ((m[i.value] = i.label), m), {}); +const BID_RESULT_TYPE_TAG = { + 1: "success", + 2: "danger", + 3: "warning", + 4: "info", +}; + +const BID_SOURCE_TYPE_MAP = BID_SOURCE_TYPE_OPTIONS.reduce((m, i) => ((m[i.value] = i.label), m), {}); + +const BID_TASK_STATUS_MAP = BID_TASK_STATUS_OPTIONS.reduce((m, i) => ((m[i.value] = i.label), m), {}); +const BID_TASK_STATUS_TAG = { + 0: "info", + 1: "primary", + 2: "success", + 3: "danger", + 4: "info", +}; + +const QUALIFICATION_STATUS_MAP = QUALIFICATION_STATUS_OPTIONS.reduce((m, i) => ((m[i.value] = i.label), m), {}); +const QUALIFICATION_STATUS_TAG = { + 1: "success", + 2: "warning", + 0: "info", +}; + +const BID_QUOTE_STATUS_MAP = BID_QUOTE_STATUS_OPTIONS.reduce((m, i) => ((m[i.value] = i.label), m), {}); +const BID_QUOTE_STATUS_TAG = { + 1: "info", + 2: "warning", + 3: "success", + 4: "primary", +}; + +const PAYMENT_STATUS_MAP = PAYMENT_STATUS_OPTIONS.reduce((m, i) => ((m[i.value] = i.label), m), {}); +const PAYMENT_STATUS_TAG = { + 0: "warning", + 1: "success", + 2: "primary", + 3: "info", +}; + +const APPROVAL_STATUS_MAP = APPROVAL_STATUS_OPTIONS.reduce((m, i) => ((m[i.value] = i.label), m), {}); +const APPROVAL_STATUS_TAG = { + 0: "warning", + 1: "success", + 2: "danger", + 3: "info", +}; + +/* 工具函数 */ +export const tenderStatusText = (val) => TENDER_STATUS_MAP[normalize(val)] || normalize(val) || "-"; +export const tenderStatusTag = (val) => TENDER_STATUS_TAG[normalize(val)] || "info"; + +export const bidStatusText = (val) => BID_STATUS_MAP[normalize(val)] || normalize(val) || "-"; +export const bidStatusTag = (val) => BID_STATUS_TAG[normalize(val)] || "info"; + +export const bidResultTypeText = (val) => BID_RESULT_TYPE_MAP[normalize(val)] || normalize(val) || "-"; +export const bidResultTypeTag = (val) => BID_RESULT_TYPE_TAG[normalize(val)] || "info"; + +export const bidSourceTypeText = (val) => BID_SOURCE_TYPE_MAP[normalize(val)] || normalize(val) || "-"; + +export const bidTaskStatusText = (val) => BID_TASK_STATUS_MAP[normalize(val)] || normalize(val) || "-"; +export const bidTaskStatusTag = (val) => BID_TASK_STATUS_TAG[normalize(val)] || "info"; + +export const qualificationStatusText = (val) => QUALIFICATION_STATUS_MAP[normalize(val)] || normalize(val) || "-"; +export const qualificationStatusTag = (val) => QUALIFICATION_STATUS_TAG[normalize(val)] || "info"; + +export const bidQuoteStatusText = (val) => BID_QUOTE_STATUS_MAP[normalize(val)] || normalize(val) || "-"; +export const bidQuoteStatusTag = (val) => BID_QUOTE_STATUS_TAG[normalize(val)] || "info"; + +export const paymentStatusText = (val) => PAYMENT_STATUS_MAP[normalize(val)] || normalize(val) || "-"; +export const paymentStatusTag = (val) => PAYMENT_STATUS_TAG[normalize(val)] || "info"; + +export const approvalStatusText = (val) => APPROVAL_STATUS_MAP[normalize(val)] || normalize(val) || "-"; +export const approvalStatusTag = (val) => APPROVAL_STATUS_TAG[normalize(val)] || "info"; + +/** 暴露给 el-select 遍历 */ +export const tenderTypeOptions = TENDER_TYPE_OPTIONS; +export const tenderStatusOptions = TENDER_STATUS_OPTIONS; +export const evaluationMethodOptions = EVALUATION_METHOD_OPTIONS; +export const tenderResultTypeOptions = TENDER_RESULT_TYPE_OPTIONS; +export const bidTypeOptions = BID_TYPE_OPTIONS; +export const bidStatusOptions = BID_STATUS_OPTIONS; +export const bidSourceTypeOptions = BID_SOURCE_TYPE_OPTIONS; +export const bidResultTypeOptions = BID_RESULT_TYPE_OPTIONS; +export const tenderSupplierStatusOptions = TENDER_SUPPLIER_STATUS_OPTIONS; +export const tenderAnnounceTypeOptions = TENDER_ANNOUNCE_TYPE_OPTIONS; +export const evaluationResultOptions = EVALUATION_RESULT_OPTIONS; +export const bidTaskTypeOptions = BID_TASK_TYPE_OPTIONS; +export const bidTaskStatusOptions = BID_TASK_STATUS_OPTIONS; +export const bidTemplateTypeOptions = BID_TEMPLATE_TYPE_OPTIONS; +export const qualificationTypeOptions = QUALIFICATION_TYPE_OPTIONS; +export const qualificationStatusOptions = QUALIFICATION_STATUS_OPTIONS; +export const bidQuoteStatusOptions = BID_QUOTE_STATUS_OPTIONS; +export const competitorTypeOptions = COMPETITOR_TYPE_OPTIONS; +export const bidExpenseTypeOptions = BID_EXPENSE_TYPE_OPTIONS; +export const paymentStatusOptions = PAYMENT_STATUS_OPTIONS; +export const bidDocumentStatusOptions = BID_DOCUMENT_STATUS_OPTIONS; +export const approvalStatusOptions = APPROVAL_STATUS_OPTIONS; +export const approvalTypeOptions = APPROVAL_TYPE_OPTIONS; + +/* ===================================================================== + * 招投标:补齐的取值工具 + * 上面的 *_OPTIONS 已定义,这里只补 "值 -> 文案 / tag 类型" 的映射, + * 与既有一 Style保持一致,供页面和 Tab 组件直接引用。 + * ===================================================================== */ + +const BIDDING_TEXT_MAP = { + tenderType: TENDER_TYPE_OPTIONS, + evaluationMethod: EVALUATION_METHOD_OPTIONS, + bidResult: BID_RESULT_TYPE_OPTIONS, + templateType: BID_TEMPLATE_TYPE_OPTIONS, + competitorType: COMPETITOR_TYPE_OPTIONS, + expenseType: BID_EXPENSE_TYPE_OPTIONS, + documentStatus: BID_DOCUMENT_STATUS_OPTIONS +}; + +const BIDDING_TAG_MAP = { + tenderType: { 1: "primary", 2: "success", 3: "warning", 4: "info", 5: "danger" }, + bidResult: { 1: "success", 2: "danger", 3: "warning", 4: "info" }, + documentStatus: { 1: "info", 2: "warning", 3: "success", 4: "primary" } +}; + +function biddingText(kind, val) { + const opt = BIDDING_TEXT_MAP[kind]; + if (!opt) return "-"; + const hit = opt.find(i => String(i.value) === normalize(val)); + return hit ? hit.label : normalize(val) || "-"; +} + +export const tenderTypeText = val => biddingText("tenderType", val); +export const tenderTypeTag = val => BIDDING_TAG_MAP.tenderType[normalize(val)] || "info"; + +// 投标类型与招标类型共用同一套字典,保留 bidType* 别名避免调用方改字典名 +export const bidTypeText = val => biddingText("tenderType", val); +export const bidTypeTag = val => BIDDING_TAG_MAP.tenderType[normalize(val)] || "info"; + +export const evaluationMethodText = val => biddingText("evaluationMethod", val); + +export const bidResultText = val => biddingText("bidResult", val); +export const bidResultTag = val => BIDDING_TAG_MAP.bidResult[normalize(val)] || "info"; + +export const templateTypeText = val => biddingText("templateType", val); + +export const competitorTypeText = val => biddingText("competitorType", val); + +export const expenseTypeText = val => biddingText("expenseType", val); + +export const documentStatusText = val => biddingText("documentStatus", val); +export const documentStatusTag = val => + BIDDING_TAG_MAP.documentStatus[normalize(val)] || "info"; + +/** + * 时间线/详情用的时间格式化:空值、零值时间统一返回空串而不是 "-", + * 避免时间线上出现一排 "-"。 + */ +export function formatBidTime(val) { + if (!val) return ""; + const s = String(val).trim(); + if (!s || s.startsWith("0001-")) return ""; + return s.length >= 16 ? s.slice(0, 16).replace("T", " ") : s.slice(0, 10); +} + +/* ====================== 状态流转合法性 ====================== */ + +// 允许流转到的下一状态,用于前端限制操作按钮(后端仍会二次校验) +export const BID_NEXT_STATUS = { + 0: [1, 2], // 草稿 -> 审批中 / 直接投标中 + 1: [2, 0], // 审批中 -> 投标中 / 驳回回草稿 + 2: [3], // 投标中 -> 已开标 + 3: [4, 5], // 已开标 -> 中标 / 落标 + 4: [6, 7], // 中标 -> 已转合同 / 归档 + 5: [7], + 6: [7], + 7: [] +}; + +export const TENDER_NEXT_STATUS = { + 0: [1], + 1: [2], + 2: [3], + 3: [4], + 4: [5], + 5: [6], + 6: [7], + 7: [] +}; + +/** 判断目标状态是否是合法流转 */ +export function canTransfer(kind, from, to) { + const table = kind === "tender" ? TENDER_NEXT_STATUS : BID_NEXT_STATUS; + const next = table[normalize(from)]; + return Array.isArray(next) && next.includes(Number(to)); +} diff --git a/go/controllers/backend_crm_bidding_bid.go b/go/controllers/backend_crm_bidding_bid.go new file mode 100644 index 0000000..abde62b --- /dev/null +++ b/go/controllers/backend_crm_bidding_bid.go @@ -0,0 +1,846 @@ +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 "未知" + } +} \ No newline at end of file diff --git a/go/controllers/backend_crm_bidding_more.go b/go/controllers/backend_crm_bidding_more.go new file mode 100644 index 0000000..36aabb2 --- /dev/null +++ b/go/controllers/backend_crm_bidding_more.go @@ -0,0 +1,1266 @@ +package controllers + +import ( + "encoding/json" + "io" + "strconv" + "strings" + "time" + + "server/models" + + beego "github.com/beego/beego/v2/server/web" + "github.com/beego/beego/v2/client/orm" +) + +// ============================================================= +// CRM 招投标管理 - 补充接口 +// ============================================================= +// +// 覆盖开发计划中尚未实现的接口: +// - 招标公告 / 投标方 / 开标评标 / 定标结果(招标侧子表) +// - 投标立项审批 / 标书版本(投标侧子表) +// - 标书模板库 / 资质文件库(公共库) +// - 招标统计 / 投标统计 +// +// 约定沿用既有实现:软删除(delete_time)、多租户(tenant_id)、 +// 统一 pipelineClaims / pipelineOk / pipelineErr 鉴权与响应。 +// ============================================================= + +// ------------------------------ 招标公告 ------------------------------ + +// BackendCrmBiddingTenderAnnounceController 招标公告 +type BackendCrmBiddingTenderAnnounceController struct { + beego.Controller +} + +type tenderAnnouncePayload struct { + TenderID uint64 `json:"tender_id"` + AnnounceType string `json:"announce_type"` + Title string `json:"title"` + Content string `json:"content"` +} + +// List GET /backend/crm/bidding/tender-announce/list?tender_id=1 +func (c *BackendCrmBiddingTenderAnnounceController) List() { + claims, err := pipelineClaims(&c.Controller) + if err != nil { + pipelineErr(&c.Controller, 401, 401, err.Error()) + return + } + tenderID, _ := c.GetInt64("tender_id") + if tenderID == 0 { + pipelineErr(&c.Controller, 400, 400, "tender_id 必填") + return + } + var list []models.TenantCrmBiddingTenderAnnounce + _, _ = models.Orm.QueryTable(new(models.TenantCrmBiddingTenderAnnounce)). + Filter("tenant_id", pipelineTenantID(claims)). + Filter("tender_id", tenderID). + Filter("delete_time__isnull", true). + OrderBy("-id"). + All(&list) + pipelineOk(&c.Controller, map[string]interface{}{"list": list, "total": len(list)}) +} + +// Create POST /backend/crm/bidding/tender-announce +func (c *BackendCrmBiddingTenderAnnounceController) 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 tenderAnnouncePayload + if err := json.Unmarshal(raw, &p); err != nil { + pipelineErr(&c.Controller, 400, 400, "参数错误") + return + } + if p.TenderID == 0 || strings.TrimSpace(p.Title) == "" { + pipelineErr(&c.Controller, 400, 400, "公告信息不完整") + return + } + now := time.Now() + row := models.TenantCrmBiddingTenderAnnounce{ + TenantID: pipelineTenantID(claims), + TenderID: p.TenderID, + AnnounceType: firstNonEmpty(p.AnnounceType, "1"), + Title: strings.TrimSpace(p.Title), + Content: p.Content, + PublishStatus: 0, + CreateUserID: pipelineUID(claims), + CreateUserName: resolveUserName(claims), + CreateTime: now, + UpdateTime: now, + } + id, err := models.Orm.Insert(&row) + if err != nil { + pipelineErr(&c.Controller, 500, 500, "创建失败: "+err.Error()) + return + } + crmWriteLog(pipelineTenantID(claims), 5, uint64(id), "create", "创建招标公告", claims) + pipelineOk(&c.Controller, map[string]interface{}{"id": id}) +} + +// Publish POST /backend/crm/bidding/tender-announce/:id/publish +// 发布公告:置为已发布并记录发布时间,同时把招标主表推进到「公告中」。 +func (c *BackendCrmBiddingTenderAnnounceController) Publish() { + 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.TenantCrmBiddingTenderAnnounce + if err := models.Orm.QueryTable(new(models.TenantCrmBiddingTenderAnnounce)). + Filter("id", id). + Filter("tenant_id", pipelineTenantID(claims)). + Filter("delete_time__isnull", true). + One(&row); err != nil { + pipelineErr(&c.Controller, 404, 404, "公告不存在") + return + } + now := time.Now() + row.PublishStatus = 1 + row.PublishTime = &now + row.UpdateTime = now + _, _ = models.Orm.Update(&row, "publish_status", "publish_time", "update_time") + + // 主表状态推进:草稿(0) -> 公告中(1) + if row.TenderID > 0 { + var t models.TenantCrmBiddingTender + if err := models.Orm.QueryTable(new(models.TenantCrmBiddingTender)). + Filter("id", row.TenderID). + Filter("tenant_id", pipelineTenantID(claims)). + Filter("delete_time__isnull", true). + One(&t); err == nil && t.Status == 0 { + t.Status = 1 + t.PublishDate = &now + t.UpdateTime = now + _, _ = models.Orm.Update(&t, "status", "publish_date", "update_time") + } + } + crmWriteLog(pipelineTenantID(claims), 5, id, "publish", "发布招标公告", claims) + pipelineOk(&c.Controller, map[string]interface{}{"id": id}) +} + +// Delete DELETE /backend/crm/bidding/tender-announce/:id +func (c *BackendCrmBiddingTenderAnnounceController) 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) + now := time.Now() + _, err = models.Orm.QueryTable(new(models.TenantCrmBiddingTenderAnnounce)). + Filter("id", id). + Filter("tenant_id", pipelineTenantID(claims)). + Update(map[string]interface{}{"delete_time": now, "update_time": now}) + if err != nil { + pipelineErr(&c.Controller, 500, 500, "删除失败: "+err.Error()) + return + } + crmWriteLog(pipelineTenantID(claims), 5, id, "delete", "删除招标公告", claims) + pipelineOk(&c.Controller, nil) +} + +// ------------------------------ 投标方/供应商 ------------------------------ + +// BackendCrmBiddingTenderSupplierController 投标方 +type BackendCrmBiddingTenderSupplierController struct { + beego.Controller +} + +type tenderSupplierPayload struct { + TenderID uint64 `json:"tender_id"` + SupplierID *uint64 `json:"supplier_id"` + SupplierName string `json:"supplier_name"` + RegisterStatus int8 `json:"register_status"` + BidAmount float64 `json:"bid_amount"` + BidFiles string `json:"bid_files"` + ContactPerson string `json:"contact_person"` + ContactPhone string `json:"contact_phone"` + IsWinner int8 `json:"is_winner"` + Ranking uint32 `json:"ranking"` + Remark string `json:"remark"` +} + +// List GET /backend/crm/bidding/tender-supplier/list?tender_id=1 +func (c *BackendCrmBiddingTenderSupplierController) List() { + claims, err := pipelineClaims(&c.Controller) + if err != nil { + pipelineErr(&c.Controller, 401, 401, err.Error()) + return + } + tenderID, _ := c.GetInt64("tender_id") + if tenderID == 0 { + pipelineErr(&c.Controller, 400, 400, "tender_id 必填") + return + } + var list []models.TenantCrmBiddingTenderSupplier + _, _ = models.Orm.QueryTable(new(models.TenantCrmBiddingTenderSupplier)). + Filter("tenant_id", pipelineTenantID(claims)). + Filter("tender_id", tenderID). + Filter("delete_time__isnull", true). + OrderBy("-id"). + All(&list) + pipelineOk(&c.Controller, map[string]interface{}{"list": list, "total": len(list)}) +} + +// Create POST /backend/crm/bidding/tender-supplier +func (c *BackendCrmBiddingTenderSupplierController) 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 tenderSupplierPayload + if err := json.Unmarshal(raw, &p); err != nil { + pipelineErr(&c.Controller, 400, 400, "参数错误") + return + } + if p.TenderID == 0 || strings.TrimSpace(p.SupplierName) == "" { + pipelineErr(&c.Controller, 400, 400, "投标方信息不完整") + return + } + now := time.Now() + row := models.TenantCrmBiddingTenderSupplier{ + TenantID: pipelineTenantID(claims), + TenderID: p.TenderID, + SupplierID: p.SupplierID, + SupplierName: strings.TrimSpace(p.SupplierName), + RegisterStatus: p.RegisterStatus, + RegisterTime: &now, + BidAmount: p.BidAmount, + BidFiles: p.BidFiles, + ContactPerson: p.ContactPerson, + ContactPhone: p.ContactPhone, + Remark: p.Remark, + CreateUserID: pipelineUID(claims), + CreateUserName: resolveUserName(claims), + CreateTime: now, + UpdateTime: now, + } + id, err := models.Orm.Insert(&row) + if err != nil { + pipelineErr(&c.Controller, 500, 500, "创建失败: "+err.Error()) + return + } + crmWriteLog(pipelineTenantID(claims), 5, uint64(id), "create", "登记投标方", claims) + pipelineOk(&c.Controller, map[string]interface{}{"id": id}) +} + +// Update PUT /backend/crm/bidding/tender-supplier/:id +func (c *BackendCrmBiddingTenderSupplierController) 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 tenderSupplierPayload + if err := json.Unmarshal(raw, &p); err != nil { + pipelineErr(&c.Controller, 400, 400, "参数错误") + return + } + var row models.TenantCrmBiddingTenderSupplier + if err := models.Orm.QueryTable(new(models.TenantCrmBiddingTenderSupplier)). + Filter("id", id). + Filter("tenant_id", pipelineTenantID(claims)). + Filter("delete_time__isnull", true). + One(&row); err != nil { + pipelineErr(&c.Controller, 404, 404, "记录不存在") + return + } + if strings.TrimSpace(p.SupplierName) != "" { + row.SupplierName = strings.TrimSpace(p.SupplierName) + } + row.SupplierID = p.SupplierID + row.RegisterStatus = p.RegisterStatus + row.BidAmount = p.BidAmount + row.BidFiles = p.BidFiles + row.ContactPerson = p.ContactPerson + row.ContactPhone = p.ContactPhone + row.Ranking = p.Ranking + row.IsWinner = p.IsWinner + row.Remark = p.Remark + row.UpdateTime = time.Now() + _, _ = models.Orm.Update(&row) + crmWriteLog(pipelineTenantID(claims), 5, id, "update", "更新投标方信息", claims) + pipelineOk(&c.Controller, map[string]interface{}{"id": id}) +} + +// Delete DELETE /backend/crm/bidding/tender-supplier/:id +func (c *BackendCrmBiddingTenderSupplierController) 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) + now := time.Now() + _, err = models.Orm.QueryTable(new(models.TenantCrmBiddingTenderSupplier)). + Filter("id", id). + Filter("tenant_id", pipelineTenantID(claims)). + Update(map[string]interface{}{"delete_time": now, "update_time": now}) + if err != nil { + pipelineErr(&c.Controller, 500, 500, "删除失败: "+err.Error()) + return + } + crmWriteLog(pipelineTenantID(claims), 5, id, "delete", "删除投标方", claims) + pipelineOk(&c.Controller, nil) +} + +// ------------------------------ 开标评标 ------------------------------ + +// BackendCrmBiddingTenderEvaluationController 开标评标 +type BackendCrmBiddingTenderEvaluationController struct { + beego.Controller +} + +type tenderEvaluationPayload struct { + TenderID uint64 `json:"tender_id"` + SupplierID *uint64 `json:"supplier_id"` + SupplierName string `json:"supplier_name"` + TechnicalScore float64 `json:"technical_score"` + BusinessScore float64 `json:"business_score"` + PriceScore float64 `json:"price_score"` + ServiceScore float64 `json:"service_score"` + WeightConfig string `json:"weight_config"` + Ranking uint32 `json:"ranking"` + Comment string `json:"comment"` + Result int8 `json:"result"` +} + +// evaluationWeights 评标权重配置,缺省时四项等权(各项满分按 100 计)。 +type evaluationWeights struct { + Technical float64 `json:"technical"` + Business float64 `json:"business"` + Price float64 `json:"price"` + Service float64 `json:"service"` +} + +// calcTotalScore 按权重折算总分;权重解析失败或全为 0 时退化为四项直接求和。 +func calcTotalScore(p tenderEvaluationPayload) float64 { + var w evaluationWeights + _ = json.Unmarshal([]byte(p.WeightConfig), &w) + sum := w.Technical + w.Business + w.Price + w.Service + if sum <= 0 { + return p.TechnicalScore + p.BusinessScore + p.PriceScore + p.ServiceScore + } + return p.TechnicalScore*w.Technical + + p.BusinessScore*w.Business + + p.PriceScore*w.Price + + p.ServiceScore*w.Service +} + +// List GET /backend/crm/bidding/tender-evaluation/list?tender_id=1 +func (c *BackendCrmBiddingTenderEvaluationController) List() { + claims, err := pipelineClaims(&c.Controller) + if err != nil { + pipelineErr(&c.Controller, 401, 401, err.Error()) + return + } + tenderID, _ := c.GetInt64("tender_id") + if tenderID == 0 { + pipelineErr(&c.Controller, 400, 400, "tender_id 必填") + return + } + var list []models.TenantCrmBiddingTenderEvaluation + _, _ = models.Orm.QueryTable(new(models.TenantCrmBiddingTenderEvaluation)). + Filter("tenant_id", pipelineTenantID(claims)). + Filter("tender_id", tenderID). + Filter("delete_time__isnull", true). + OrderBy("-total_score"). + All(&list) + pipelineOk(&c.Controller, map[string]interface{}{"list": list, "total": len(list)}) +} + +// Create POST /backend/crm/bidding/tender-evaluation +func (c *BackendCrmBiddingTenderEvaluationController) 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 tenderEvaluationPayload + if err := json.Unmarshal(raw, &p); err != nil { + pipelineErr(&c.Controller, 400, 400, "参数错误") + return + } + if p.TenderID == 0 || strings.TrimSpace(p.SupplierName) == "" { + pipelineErr(&c.Controller, 400, 400, "评标信息不完整") + return + } + now := time.Now() + row := models.TenantCrmBiddingTenderEvaluation{ + TenantID: pipelineTenantID(claims), + TenderID: p.TenderID, + SupplierID: p.SupplierID, + SupplierName: strings.TrimSpace(p.SupplierName), + TechnicalScore: p.TechnicalScore, + BusinessScore: p.BusinessScore, + PriceScore: p.PriceScore, + ServiceScore: p.ServiceScore, + TotalScore: calcTotalScore(p), + WeightConfig: p.WeightConfig, + Ranking: p.Ranking, + Comment: p.Comment, + Result: p.Result, + EvaluationUserID: pipelineUID(claims), + EvaluationUserName: resolveUserName(claims), + EvaluationTime: &now, + CreateTime: now, + UpdateTime: now, + } + id, err := models.Orm.Insert(&row) + if err != nil { + pipelineErr(&c.Controller, 500, 500, "创建失败: "+err.Error()) + return + } + // 录入评标后重排该招标下所有供应商名次(按总分降序) + resortEvaluationRanking(p.TenderID, pipelineTenantID(claims)) + crmWriteLog(pipelineTenantID(claims), 5, uint64(id), "create", "新增评标打分", claims) + pipelineOk(&c.Controller, map[string]interface{}{"id": id}) +} + +// resortEvaluationRanking 按总分降序重写 ranking 字段。 +func resortEvaluationRanking(tenderID uint64, tenantID string) { + var list []models.TenantCrmBiddingTenderEvaluation + _, _ = models.Orm.QueryTable(new(models.TenantCrmBiddingTenderEvaluation)). + Filter("tenant_id", tenantID). + Filter("tender_id", tenderID). + Filter("delete_time__isnull", true). + OrderBy("-total_score"). + All(&list) + for i := range list { + list[i].Ranking = uint32(i + 1) + _, _ = models.Orm.Update(&list[i], "ranking") + } +} + +// Delete DELETE /backend/crm/bidding/tender-evaluation/:id +func (c *BackendCrmBiddingTenderEvaluationController) 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) + now := time.Now() + _, err = models.Orm.QueryTable(new(models.TenantCrmBiddingTenderEvaluation)). + Filter("id", id). + Filter("tenant_id", pipelineTenantID(claims)). + Update(map[string]interface{}{"delete_time": now, "update_time": now}) + if err != nil { + pipelineErr(&c.Controller, 500, 500, "删除失败: "+err.Error()) + return + } + crmWriteLog(pipelineTenantID(claims), 5, id, "delete", "删除评标记录", claims) + pipelineOk(&c.Controller, nil) +} + +// ------------------------------ 定标结果 ------------------------------ + +// BackendCrmBiddingTenderResultController 定标结果(只读,写入由 tender/:id/award 完成) +type BackendCrmBiddingTenderResultController struct { + beego.Controller +} + +// List GET /backend/crm/bidding/tender-result/list?tender_id=1 +func (c *BackendCrmBiddingTenderResultController) List() { + claims, err := pipelineClaims(&c.Controller) + if err != nil { + pipelineErr(&c.Controller, 401, 401, err.Error()) + return + } + tenderID, _ := c.GetInt64("tender_id") + if tenderID == 0 { + pipelineErr(&c.Controller, 400, 400, "tender_id 必填") + return + } + var list []models.TenantCrmBiddingTenderResult + _, _ = models.Orm.QueryTable(new(models.TenantCrmBiddingTenderResult)). + Filter("tenant_id", pipelineTenantID(claims)). + Filter("tender_id", tenderID). + Filter("delete_time__isnull", true). + OrderBy("-id"). + All(&list) + pipelineOk(&c.Controller, map[string]interface{}{"list": list, "total": len(list)}) +} + +// Detail GET /backend/crm/bidding/tender-result/detail/:id +func (c *BackendCrmBiddingTenderResultController) 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) + var row models.TenantCrmBiddingTenderResult + if err := models.Orm.QueryTable(new(models.TenantCrmBiddingTenderResult)). + Filter("id", id). + Filter("tenant_id", pipelineTenantID(claims)). + Filter("delete_time__isnull", true). + One(&row); err != nil { + pipelineErr(&c.Controller, 404, 404, "定标结果不存在") + return + } + pipelineOk(&c.Controller, row) +} + +// ------------------------------ 投标立项审批 ------------------------------ + +// BackendCrmBiddingBidApprovalController 投标立项审批 +type BackendCrmBiddingBidApprovalController struct { + beego.Controller +} + +type bidApprovalPayload struct { + BidID uint64 `json:"bid_id"` + ApprovalType string `json:"approval_type"` + ApprovalStatus int8 `json:"approval_status"` + ApprovalUsers string `json:"approval_users"` + Comment string `json:"comment"` +} + +// List GET /backend/crm/bidding/bid-approval/list?bid_id=1 +func (c *BackendCrmBiddingBidApprovalController) List() { + claims, err := pipelineClaims(&c.Controller) + if err != nil { + pipelineErr(&c.Controller, 401, 401, err.Error()) + return + } + bidID, _ := c.GetInt64("bid_id") + if bidID == 0 { + pipelineErr(&c.Controller, 400, 400, "bid_id 必填") + return + } + var list []models.TenantCrmBiddingBidApproval + _, _ = models.Orm.QueryTable(new(models.TenantCrmBiddingBidApproval)). + Filter("tenant_id", pipelineTenantID(claims)). + Filter("bid_id", bidID). + Filter("delete_time__isnull", true). + OrderBy("-id"). + All(&list) + pipelineOk(&c.Controller, map[string]interface{}{"list": list, "total": len(list)}) +} + +// Create POST /backend/crm/bidding/bid-approval 提交立项审批 +func (c *BackendCrmBiddingBidApprovalController) 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 bidApprovalPayload + if err := json.Unmarshal(raw, &p); err != nil { + pipelineErr(&c.Controller, 400, 400, "参数错误") + return + } + if p.BidID == 0 { + pipelineErr(&c.Controller, 400, 400, "bid_id 必填") + return + } + now := time.Now() + row := models.TenantCrmBiddingBidApproval{ + TenantID: pipelineTenantID(claims), + BidID: p.BidID, + ApprovalType: firstNonEmpty(p.ApprovalType, "1"), + ApprovalStatus: 1, // 1审批中 + ApprovalUsers: p.ApprovalUsers, + SubmitUserID: pipelineUID(claims), + SubmitUserName: resolveUserName(claims), + SubmitTime: &now, + Comment: p.Comment, + CurrentUserID: pipelineUID(claims), + CurrentUserName: resolveUserName(claims), + CreateTime: now, + UpdateTime: now, + } + id, err := models.Orm.Insert(&row) + if err != nil { + pipelineErr(&c.Controller, 500, 500, "提交失败: "+err.Error()) + return + } + // 主表推进到「立项审批中」 + var bid models.TenantCrmBiddingBid + if err := models.Orm.QueryTable(new(models.TenantCrmBiddingBid)). + Filter("id", p.BidID). + Filter("tenant_id", pipelineTenantID(claims)). + Filter("delete_time__isnull", true). + One(&bid); err == nil && bid.Status == 0 { + bid.Status = 1 + bid.UpdateTime = now + _, _ = models.Orm.Update(&bid, "status", "update_time") + } + crmWriteLog(pipelineTenantID(claims), 5, uint64(id), "create", "提交立项审批", claims) + pipelineOk(&c.Controller, map[string]interface{}{"id": id}) +} + +// Approve POST /backend/crm/bidding/bid-approval/:id/approve 审批通过/驳回 +// body: { "pass": true, "comment": "..." } +func (c *BackendCrmBiddingBidApprovalController) Approve() { + 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) + raw, _ := io.ReadAll(c.Ctx.Request.Body) + var body struct { + Pass bool `json:"pass"` + Comment string `json:"comment"` + } + _ = json.Unmarshal(raw, &body) + + var row models.TenantCrmBiddingBidApproval + if err := models.Orm.QueryTable(new(models.TenantCrmBiddingBidApproval)). + Filter("id", id). + Filter("tenant_id", pipelineTenantID(claims)). + Filter("delete_time__isnull", true). + One(&row); err != nil { + pipelineErr(&c.Controller, 404, 404, "审批记录不存在") + return + } + now := time.Now() + if body.Pass { + row.ApprovalStatus = 2 // 2已通过 + } else { + row.ApprovalStatus = 3 // 3已驳回 + } + row.Comment = body.Comment + row.CompleteTime = &now + row.UpdateTime = now + _, _ = models.Orm.Update(&row, "approval_status", "comment", "complete_time", "update_time") + + // 审批通过 -> 投标转为「投标中」;驳回 -> 退回草稿 + var bid models.TenantCrmBiddingBid + if err := models.Orm.QueryTable(new(models.TenantCrmBiddingBid)). + Filter("id", row.BidID). + Filter("tenant_id", pipelineTenantID(claims)). + Filter("delete_time__isnull", true). + One(&bid); err == nil { + if body.Pass { + bid.Status = 2 + } else { + bid.Status = 0 + } + bid.UpdateTime = now + _, _ = models.Orm.Update(&bid, "status", "update_time") + } + crmWriteLog(pipelineTenantID(claims), 5, id, "approve", "立项审批处理", claims) + pipelineOk(&c.Controller, map[string]interface{}{"id": id}) +} + +// ------------------------------ 标书版本 ------------------------------ + +// BackendCrmBiddingBidDocumentController 标书版本 +type BackendCrmBiddingBidDocumentController struct { + beego.Controller +} + +type bidDocumentPayload struct { + BidID uint64 `json:"bid_id"` + TemplateID *uint64 `json:"template_id"` + Version string `json:"version"` + Title string `json:"title"` + Content string `json:"content"` + FileURL string `json:"file_url"` + Status int8 `json:"status"` + ReviewComment string `json:"review_comment"` +} + +// List GET /backend/crm/bidding/bid-document/list?bid_id=1 +func (c *BackendCrmBiddingBidDocumentController) List() { + claims, err := pipelineClaims(&c.Controller) + if err != nil { + pipelineErr(&c.Controller, 401, 401, err.Error()) + return + } + bidID, _ := c.GetInt64("bid_id") + if bidID == 0 { + pipelineErr(&c.Controller, 400, 400, "bid_id 必填") + return + } + var list []models.TenantCrmBiddingBidDocument + _, _ = models.Orm.QueryTable(new(models.TenantCrmBiddingBidDocument)). + Filter("tenant_id", pipelineTenantID(claims)). + Filter("bid_id", bidID). + Filter("delete_time__isnull", true). + OrderBy("-id"). + All(&list) + pipelineOk(&c.Controller, map[string]interface{}{"list": list, "total": len(list)}) +} + +// Create POST /backend/crm/bidding/bid-document +func (c *BackendCrmBiddingBidDocumentController) 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 bidDocumentPayload + if err := json.Unmarshal(raw, &p); err != nil { + pipelineErr(&c.Controller, 400, 400, "参数错误") + return + } + if p.BidID == 0 || strings.TrimSpace(p.Title) == "" { + pipelineErr(&c.Controller, 400, 400, "标书信息不完整") + return + } + now := time.Now() + row := models.TenantCrmBiddingBidDocument{ + TenantID: pipelineTenantID(claims), + BidID: p.BidID, + TemplateID: p.TemplateID, + Version: firstNonEmpty(p.Version, "v1.0"), + Title: strings.TrimSpace(p.Title), + Content: p.Content, + FileURL: p.FileURL, + Status: p.Status, + EditorUserID: pipelineUID(claims), + EditorUserName: resolveUserName(claims), + CreateTime: now, + UpdateTime: now, + } + id, err := models.Orm.Insert(&row) + if err != nil { + pipelineErr(&c.Controller, 500, 500, "创建失败: "+err.Error()) + return + } + crmWriteLog(pipelineTenantID(claims), 5, uint64(id), "create", "新建标书版本", claims) + pipelineOk(&c.Controller, map[string]interface{}{"id": id}) +} + +// Update PUT /backend/crm/bidding/bid-document/:id +func (c *BackendCrmBiddingBidDocumentController) 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) + raw, _ := io.ReadAll(c.Ctx.Request.Body) + var p bidDocumentPayload + if err := json.Unmarshal(raw, &p); err != nil { + pipelineErr(&c.Controller, 400, 400, "参数错误") + return + } + var row models.TenantCrmBiddingBidDocument + if err := models.Orm.QueryTable(new(models.TenantCrmBiddingBidDocument)). + Filter("id", id). + Filter("tenant_id", pipelineTenantID(claims)). + Filter("delete_time__isnull", true). + One(&row); err != nil { + pipelineErr(&c.Controller, 404, 404, "标书不存在") + return + } + if strings.TrimSpace(p.Title) != "" { + row.Title = strings.TrimSpace(p.Title) + } + row.Version = firstNonEmpty(p.Version, row.Version) + row.Content = p.Content + row.FileURL = p.FileURL + row.Status = p.Status + row.ReviewComment = p.ReviewComment + if p.Status == 3 || p.Status == 4 { + row.ReviewUserID = pipelineUID(claims) + row.ReviewUserName = resolveUserName(claims) + now := time.Now() + row.ReviewTime = &now + if p.Status == 4 { + row.SubmitTime = &now + } + } + row.UpdateTime = time.Now() + _, _ = models.Orm.Update(&row) + crmWriteLog(pipelineTenantID(claims), 5, id, "update", "更新标书版本", claims) + pipelineOk(&c.Controller, map[string]interface{}{"id": id}) +} + +// Delete DELETE /backend/crm/bidding/bid-document/:id +func (c *BackendCrmBiddingBidDocumentController) 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) + now := time.Now() + _, err = models.Orm.QueryTable(new(models.TenantCrmBiddingBidDocument)). + Filter("id", id). + Filter("tenant_id", pipelineTenantID(claims)). + Update(map[string]interface{}{"delete_time": now, "update_time": now}) + if err != nil { + pipelineErr(&c.Controller, 500, 500, "删除失败: "+err.Error()) + return + } + crmWriteLog(pipelineTenantID(claims), 5, id, "delete", "删除标书版本", claims) + pipelineOk(&c.Controller, nil) +} + +// ------------------------------ 标书模板库 ------------------------------ + +// BackendCrmBiddingBidTemplateController 标书模板库(公共库,租户内共享) +type BackendCrmBiddingBidTemplateController struct { + beego.Controller +} + +type bidTemplatePayload struct { + TemplateName string `json:"template_name"` + TemplateType string `json:"template_type"` + Industry string `json:"industry"` + Description string `json:"description"` + Content string `json:"content"` + FileURL string `json:"file_url"` + Status int8 `json:"status"` +} + +// List GET /backend/crm/bidding/template/list?keyword=&template_type= +func (c *BackendCrmBiddingBidTemplateController) List() { + claims, err := pipelineClaims(&c.Controller) + if err != nil { + pipelineErr(&c.Controller, 401, 401, err.Error()) + return + } + keyword := strings.TrimSpace(c.GetString("keyword")) + templateType := strings.TrimSpace(c.GetString("template_type")) + page, _ := c.GetInt("page", 1) + pageSize, _ := c.GetInt("pageSize", 20) + if page < 1 { + page = 1 + } + if pageSize < 1 || pageSize > 200 { + pageSize = 20 + } + qs := models.Orm.QueryTable(new(models.TenantCrmBiddingBidTemplate)). + Filter("tenant_id", pipelineTenantID(claims)). + Filter("delete_time__isnull", true) + if keyword != "" { + qs = qs.Filter("template_name__icontains", keyword) + } + if templateType != "" { + qs = qs.Filter("template_type", templateType) + } + total, _ := qs.Count() + var list []models.TenantCrmBiddingBidTemplate + _, _ = qs.OrderBy("-id").Limit(pageSize).Offset((page - 1) * pageSize).All(&list) + pipelineOk(&c.Controller, map[string]interface{}{ + "list": list, "total": total, "page": page, "pageSize": pageSize, + }) +} + +// Create POST /backend/crm/bidding/template +func (c *BackendCrmBiddingBidTemplateController) 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 bidTemplatePayload + if err := json.Unmarshal(raw, &p); err != nil { + pipelineErr(&c.Controller, 400, 400, "参数错误") + return + } + if strings.TrimSpace(p.TemplateName) == "" { + pipelineErr(&c.Controller, 400, 400, "模板名称必填") + return + } + now := time.Now() + row := models.TenantCrmBiddingBidTemplate{ + TenantID: pipelineTenantID(claims), + TemplateName: strings.TrimSpace(p.TemplateName), + TemplateType: firstNonEmpty(p.TemplateType, "1"), + Industry: p.Industry, + Description: p.Description, + Content: p.Content, + FileURL: p.FileURL, + Status: 1, + OwnerUserID: pipelineUID(claims), + OwnerUserName: resolveUserName(claims), + CreateTime: now, + UpdateTime: now, + } + id, err := models.Orm.Insert(&row) + if err != nil { + pipelineErr(&c.Controller, 500, 500, "创建失败: "+err.Error()) + return + } + crmWriteLog(pipelineTenantID(claims), 5, uint64(id), "create", "新建标书模板", claims) + pipelineOk(&c.Controller, map[string]interface{}{"id": id}) +} + +// Update PUT /backend/crm/bidding/template/:id +func (c *BackendCrmBiddingBidTemplateController) 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) + raw, _ := io.ReadAll(c.Ctx.Request.Body) + var p bidTemplatePayload + if err := json.Unmarshal(raw, &p); err != nil { + pipelineErr(&c.Controller, 400, 400, "参数错误") + return + } + var row models.TenantCrmBiddingBidTemplate + if err := models.Orm.QueryTable(new(models.TenantCrmBiddingBidTemplate)). + Filter("id", id). + Filter("tenant_id", pipelineTenantID(claims)). + Filter("delete_time__isnull", true). + One(&row); err != nil { + pipelineErr(&c.Controller, 404, 404, "模板不存在") + return + } + if strings.TrimSpace(p.TemplateName) != "" { + row.TemplateName = strings.TrimSpace(p.TemplateName) + } + row.TemplateType = firstNonEmpty(p.TemplateType, row.TemplateType) + row.Industry = p.Industry + row.Description = p.Description + row.Content = p.Content + row.FileURL = p.FileURL + if p.Status == 0 || p.Status == 1 { + row.Status = p.Status + } + row.UpdateTime = time.Now() + _, _ = models.Orm.Update(&row) + crmWriteLog(pipelineTenantID(claims), 5, id, "update", "更新标书模板", claims) + pipelineOk(&c.Controller, map[string]interface{}{"id": id}) +} + +// Delete DELETE /backend/crm/bidding/template/:id +func (c *BackendCrmBiddingBidTemplateController) 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) + now := time.Now() + _, err = models.Orm.QueryTable(new(models.TenantCrmBiddingBidTemplate)). + Filter("id", id). + Filter("tenant_id", pipelineTenantID(claims)). + Update(map[string]interface{}{"delete_time": now, "update_time": now}) + if err != nil { + pipelineErr(&c.Controller, 500, 500, "删除失败: "+err.Error()) + return + } + crmWriteLog(pipelineTenantID(claims), 5, id, "delete", "删除标书模板", claims) + pipelineOk(&c.Controller, nil) +} + +// ------------------------------ 资质文件库 ------------------------------ + +// BackendCrmBiddingBidQualificationController 资质文件库 +type BackendCrmBiddingBidQualificationController struct { + beego.Controller +} + +type bidQualificationPayload struct { + QualificationName string `json:"qualification_name"` + QualificationType string `json:"qualification_type"` + QualificationNo string `json:"qualification_no"` + IssuingAuthority string `json:"issuing_authority"` + IssueDate string `json:"issue_date"` + ExpireDate string `json:"expire_date"` + AlertDays uint32 `json:"alert_days"` + FileURL string `json:"file_url"` + FileName string `json:"file_name"` + FileSize uint64 `json:"file_size"` + Description string `json:"description"` + Status int8 `json:"status"` +} + +// List GET /backend/crm/bidding/qualification/list?keyword=&status= +// 返回时按到期日自动标注即将过期(不落库,避免定时任务未跑时状态陈旧)。 +func (c *BackendCrmBiddingBidQualificationController) List() { + claims, err := pipelineClaims(&c.Controller) + if err != nil { + pipelineErr(&c.Controller, 401, 401, err.Error()) + return + } + keyword := strings.TrimSpace(c.GetString("keyword")) + status := strings.TrimSpace(c.GetString("status")) + page, _ := c.GetInt("page", 1) + pageSize, _ := c.GetInt("pageSize", 20) + if page < 1 { + page = 1 + } + if pageSize < 1 || pageSize > 200 { + pageSize = 20 + } + qs := models.Orm.QueryTable(new(models.TenantCrmBiddingBidQualification)). + Filter("tenant_id", pipelineTenantID(claims)). + Filter("delete_time__isnull", true) + if keyword != "" { + qs = qs.Filter("qualification_name__icontains", keyword) + } + if status != "" { + qs = qs.Filter("status", status) + } + total, _ := qs.Count() + var list []models.TenantCrmBiddingBidQualification + _, _ = qs.OrderBy("expire_date").Limit(pageSize).Offset((page - 1) * pageSize).All(&list) + + // 临期提醒:距到期不足 alert_days 天标记为「即将过期」,已过期标记为「失效」 + type qualItem struct { + models.TenantCrmBiddingBidQualification + ExpiringDays int `json:"expiring_days"` + } + out := make([]qualItem, 0, len(list)) + now := time.Now() + for _, q := range list { + days := -1 + if q.ExpireDate != nil { + d := int(q.ExpireDate.Sub(now).Hours() / 24) + days = d + if d < 0 { + q.Status = 0 + } else if q.AlertDays > 0 && uint32(d) <= q.AlertDays { + q.Status = 2 + } + } + out = append(out, qualItem{TenantCrmBiddingBidQualification: q, ExpiringDays: days}) + } + pipelineOk(&c.Controller, map[string]interface{}{ + "list": out, "total": total, "page": page, "pageSize": pageSize, + }) +} + +// Create POST /backend/crm/bidding/qualification +func (c *BackendCrmBiddingBidQualificationController) 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 bidQualificationPayload + if err := json.Unmarshal(raw, &p); err != nil { + pipelineErr(&c.Controller, 400, 400, "参数错误") + return + } + if strings.TrimSpace(p.QualificationName) == "" { + pipelineErr(&c.Controller, 400, 400, "资质名称必填") + return + } + now := time.Now() + row := models.TenantCrmBiddingBidQualification{ + TenantID: pipelineTenantID(claims), + QualificationName: strings.TrimSpace(p.QualificationName), + QualificationType: firstNonEmpty(p.QualificationType, "1"), + QualificationNo: p.QualificationNo, + IssuingAuthority: p.IssuingAuthority, + IssueDate: parsePipelineDate(p.IssueDate), + ExpireDate: parsePipelineDate(p.ExpireDate), + AlertDays: p.AlertDays, + FileURL: p.FileURL, + FileName: p.FileName, + FileSize: p.FileSize, + Description: p.Description, + Status: 1, + OwnerUserID: pipelineUID(claims), + OwnerUserName: resolveUserName(claims), + CreateTime: now, + UpdateTime: now, + } + id, err := models.Orm.Insert(&row) + if err != nil { + pipelineErr(&c.Controller, 500, 500, "创建失败: "+err.Error()) + return + } + crmWriteLog(pipelineTenantID(claims), 5, uint64(id), "create", "新增企业资质", claims) + pipelineOk(&c.Controller, map[string]interface{}{"id": id}) +} + +// Update PUT /backend/crm/bidding/qualification/:id +func (c *BackendCrmBiddingBidQualificationController) 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) + raw, _ := io.ReadAll(c.Ctx.Request.Body) + var p bidQualificationPayload + if err := json.Unmarshal(raw, &p); err != nil { + pipelineErr(&c.Controller, 400, 400, "参数错误") + return + } + var row models.TenantCrmBiddingBidQualification + if err := models.Orm.QueryTable(new(models.TenantCrmBiddingBidQualification)). + Filter("id", id). + Filter("tenant_id", pipelineTenantID(claims)). + Filter("delete_time__isnull", true). + One(&row); err != nil { + pipelineErr(&c.Controller, 404, 404, "资质不存在") + return + } + if strings.TrimSpace(p.QualificationName) != "" { + row.QualificationName = strings.TrimSpace(p.QualificationName) + } + row.QualificationType = firstNonEmpty(p.QualificationType, row.QualificationType) + row.QualificationNo = p.QualificationNo + row.IssuingAuthority = p.IssuingAuthority + if p.IssueDate != "" { + row.IssueDate = parsePipelineDate(p.IssueDate) + } + if p.ExpireDate != "" { + row.ExpireDate = parsePipelineDate(p.ExpireDate) + } + if p.AlertDays > 0 { + row.AlertDays = p.AlertDays + } + row.FileURL = p.FileURL + row.FileName = p.FileName + row.FileSize = p.FileSize + row.Description = p.Description + if p.Status == 0 || p.Status == 1 || p.Status == 2 { + row.Status = p.Status + } + row.UpdateTime = time.Now() + _, _ = models.Orm.Update(&row) + crmWriteLog(pipelineTenantID(claims), 5, id, "update", "更新企业资质", claims) + pipelineOk(&c.Controller, map[string]interface{}{"id": id}) +} + +// Delete DELETE /backend/crm/bidding/qualification/:id +func (c *BackendCrmBiddingBidQualificationController) 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) + now := time.Now() + _, err = models.Orm.QueryTable(new(models.TenantCrmBiddingBidQualification)). + Filter("id", id). + Filter("tenant_id", pipelineTenantID(claims)). + Update(map[string]interface{}{"delete_time": now, "update_time": now}) + if err != nil { + pipelineErr(&c.Controller, 500, 500, "删除失败: "+err.Error()) + return + } + crmWriteLog(pipelineTenantID(claims), 5, id, "delete", "删除企业资质", claims) + pipelineOk(&c.Controller, nil) +} + +// ------------------------------ 统计 ------------------------------ + +// BackendCrmBiddingStatisticsController 招投标统计 +type BackendCrmBiddingStatisticsController struct { + beego.Controller +} + +// groupRows 执行分组聚合,返回 [{name, count}]。 +func groupRows(table, field, tenantID string) []map[string]interface{} { + var maps []orm.Params + _, _ = models.Orm.Raw( + "SELECT "+field+" AS name, COUNT(*) AS count FROM "+table+ + " WHERE tenant_id=? AND delete_time IS NULL GROUP BY "+field, + tenantID, + ).Values(&maps) + out := make([]map[string]interface{}, 0, len(maps)) + for _, m := range maps { + out = append(out, map[string]interface{}{"name": m["name"], "count": m["count"]}) + } + return out +} + +// sumField 对某数值字段求和。 +func sumField(table, field, tenantID string) float64 { + var v float64 + _ = models.Orm.Raw( + "SELECT IFNULL(SUM("+field+"),0) FROM "+table+ + " WHERE tenant_id=? AND delete_time IS NULL", + tenantID, + ).QueryRow(&v) + return v +} + +// Tender GET /backend/crm/bidding/statistics/tender +func (c *BackendCrmBiddingStatisticsController) Tender() { + claims, err := pipelineClaims(&c.Controller) + if err != nil { + pipelineErr(&c.Controller, 401, 401, err.Error()) + return + } + tid := pipelineTenantID(claims) + var total int64 + _ = models.Orm.Raw( + "SELECT COUNT(*) FROM yz_backend_crm_bidding_tender WHERE tenant_id=? AND delete_time IS NULL", + tid, + ).QueryRow(&total) + + pipelineOk(&c.Controller, map[string]interface{}{ + "total": total, + "by_status": groupRows("yz_backend_crm_bidding_tender", "status", tid), + "by_type": groupRows("yz_backend_crm_bidding_tender", "tender_type", tid), + "budget_total": sumField("yz_backend_crm_bidding_tender", "budget_amount", tid), + "award_total": sumField("yz_backend_crm_bidding_tender", "winner_amount", tid), + }) +} + +// Bid GET /backend/crm/bidding/statistics/bid +func (c *BackendCrmBiddingStatisticsController) Bid() { + claims, err := pipelineClaims(&c.Controller) + if err != nil { + pipelineErr(&c.Controller, 401, 401, err.Error()) + return + } + tid := pipelineTenantID(claims) + var total int64 + _ = models.Orm.Raw( + "SELECT COUNT(*) FROM yz_backend_crm_bidding_bid WHERE tenant_id=? AND delete_time IS NULL", + tid, + ).QueryRow(&total) + + var won int64 + _ = models.Orm.Raw( + "SELECT COUNT(*) FROM yz_backend_crm_bidding_bid WHERE tenant_id=? AND delete_time IS NULL AND status IN (4,6)", + tid, + ).QueryRow(&won) + + // 按月统计近 12 个月立项数 + var monthly []orm.Params + _, _ = models.Orm.Raw( + "SELECT DATE_FORMAT(create_time,'%Y-%m') AS month, COUNT(*) AS count "+ + "FROM yz_backend_crm_bidding_bid "+ + "WHERE tenant_id=? AND delete_time IS NULL AND create_time >= DATE_SUB(NOW(), INTERVAL 12 MONTH) "+ + "GROUP BY month ORDER BY month", + tid, + ).Values(&monthly) + + winRate := 0.0 + if total > 0 { + winRate = float64(won) / float64(total) * 100 + } + + pipelineOk(&c.Controller, map[string]interface{}{ + "total": total, + "won": won, + "win_rate": winRate, + "by_status": groupRows("yz_backend_crm_bidding_bid", "status", tid), + "amount_won": sumField("yz_backend_crm_bidding_bid", "bid_amount", tid), + "expense": sumField("yz_backend_crm_bidding_bid_expense", "amount", tid), + "monthly": monthly, + }) +} diff --git a/go/controllers/backend_crm_bidding_reminder.go b/go/controllers/backend_crm_bidding_reminder.go new file mode 100644 index 0000000..fe89342 --- /dev/null +++ b/go/controllers/backend_crm_bidding_reminder.go @@ -0,0 +1,208 @@ +package controllers + +import ( + "sort" + "time" + + "server/models" + + beego "github.com/beego/beego/v2/server/web" +) + +// ============================================================= +// CRM 招投标管理 - 到期提醒(开发计划 4.1) +// ============================================================= +// +// 不新增表、不引入定时任务:所有提醒都是「按当前时间实时计算」的, +// 避免定时任务没跑导致状态陈旧。前端在招投标首页拉取近 N 天的待办。 +// +// 覆盖四类到期: +// 1. 投标:报名截止 / 投标截止 / 开标时间 +// 2. 招标:报名截止 / 投标截止 / 开标时间 +// 3. 资质:证书到期 +// +// GET /backend/crm/bidding/reminders?days=7 +// ============================================================= + +// BackendCrmBiddingReminderController 招投标到期提醒 +type BackendCrmBiddingReminderController struct { + beego.Controller +} + +// biddingReminderItem 一条到期提醒 +type biddingReminderItem struct { + BizType string `json:"biz_type"` // bid | tender | qualification + BizID uint64 `json:"biz_id"` // 业务记录ID + Kind string `json:"kind"` // register_end | bid_end | open_bid | expire + KindName string `json:"kind_name"` // 提醒类型名称 + Title string `json:"title"` // 记录标题 + DueDate string `json:"due_date"` // 到期时间 yyyy-MM-dd HH:mm:ss + RemainDays int `json:"remain_days"` // 剩余天数,负值表示已逾期 + Level string `json:"level"` // danger(<=1天/已逾期) | warning(<=3天) | info + OwnerName string `json:"owner_user_name"` +} + +// List GET /backend/crm/bidding/reminders?days=7 +func (c *BackendCrmBiddingReminderController) List() { + claims, err := pipelineClaims(&c.Controller) + if err != nil { + pipelineErr(&c.Controller, 401, 401, err.Error()) + return + } + tid := pipelineTenantID(claims) + + days, _ := c.GetInt("days", 7) + if days <= 0 || days > 365 { + days = 7 + } + now := time.Now() + deadline := now.AddDate(0, 0, days) + + items := make([]biddingReminderItem, 0, 32) + + // ---------- 投标侧 ---------- + var bids []models.TenantCrmBiddingBid + _, _ = models.Orm.QueryTable(new(models.TenantCrmBiddingBid)). + Filter("tenant_id", tid). + Filter("delete_time__isnull", true). + Filter("status__in", 0, 1, 2, 3). + All(&bids) + for _, b := range bids { + if !canAccessCrmRecord(claims, b.OwnerUserID, b.CreateUserID) { + continue + } + items = append(items, pickBidReminders(b, now, deadline)...) + } + + // ---------- 招标侧 ---------- + var tenders []models.TenantCrmBiddingTender + _, _ = models.Orm.QueryTable(new(models.TenantCrmBiddingTender)). + Filter("tenant_id", tid). + Filter("delete_time__isnull", true). + Filter("status__in", 0, 1, 2, 3, 4, 5). + All(&tenders) + for _, t := range tenders { + if !canAccessCrmRecord(claims, t.OwnerUserID, t.CreateUserID) { + continue + } + items = append(items, pickTenderReminders(t, now, deadline)...) + } + + // ---------- 资质到期 ---------- + var quals []models.TenantCrmBiddingBidQualification + _, _ = models.Orm.QueryTable(new(models.TenantCrmBiddingBidQualification)). + Filter("tenant_id", tid). + Filter("delete_time__isnull", true). + All(&quals) + for _, q := range quals { + if q.ExpireDate == nil { + continue + } + due := *q.ExpireDate + // 资质到期包含「已过期」,便于用户发现失效证书 + if due.After(deadline) { + continue + } + items = append(items, biddingReminderItem{ + BizType: "qualification", + BizID: q.ID, + Kind: "expire", + KindName: "资质到期", + Title: q.QualificationName, + DueDate: due.Format("2006-01-02"), + RemainDays: daysBetween(now, due), + Level: levelOf(daysBetween(now, due)), + OwnerName: q.OwnerUserName, + }) + } + + // 按到期时间升序:最紧急的排在最前 + sort.SliceStable(items, func(i, j int) bool { + if items[i].RemainDays != items[j].RemainDays { + return items[i].RemainDays < items[j].RemainDays + } + return items[i].DueDate < items[j].DueDate + }) + + pipelineOk(&c.Controller, map[string]interface{}{ + "list": items, + "total": len(items), + "days": days, + }) +} + +// pickBidReminders 从一条投标记录中挑出落在窗口内的到期项 +func pickBidReminders(b models.TenantCrmBiddingBid, now, deadline time.Time) []biddingReminderItem { + out := make([]biddingReminderItem, 0, 3) + add := func(kind, kindName string, due *time.Time) { + if due == nil { + return + } + d := *due + if d.Before(now) || d.After(deadline) { + return + } + out = append(out, biddingReminderItem{ + BizType: "bid", + BizID: b.ID, + Kind: kind, + KindName: kindName, + Title: b.Title, + DueDate: d.Format("2006-01-02 15:04:05"), + RemainDays: daysBetween(now, d), + Level: levelOf(daysBetween(now, d)), + OwnerName: b.OwnerUserName, + }) + } + add("register_end", "报名截止", b.RegisterEndDate) + add("bid_end", "投标截止", b.BidEndDate) + add("open_bid", "开标时间", b.OpenBidDate) + return out +} + +// pickTenderReminders 从一条招标记录中挑出落在窗口内的到期项 +func pickTenderReminders(t models.TenantCrmBiddingTender, now, deadline time.Time) []biddingReminderItem { + out := make([]biddingReminderItem, 0, 3) + add := func(kind, kindName string, due *time.Time) { + if due == nil { + return + } + d := *due + if d.Before(now) || d.After(deadline) { + return + } + out = append(out, biddingReminderItem{ + BizType: "tender", + BizID: t.ID, + Kind: kind, + KindName: kindName, + Title: t.Title, + DueDate: d.Format("2006-01-02 15:04:05"), + RemainDays: daysBetween(now, d), + Level: levelOf(daysBetween(now, d)), + OwnerName: t.OwnerUserName, + }) + } + add("register_end", "报名截止", t.RegisterEndDate) + add("bid_end", "投标截止", t.BidEndDate) + add("open_bid", "开标时间", t.OpenBidDate) + return out +} + +// daysBetween 计算今天(0点)到目标时间的剩余天数 +func daysBetween(now, target time.Time) int { + n := time.Date(now.Year(), now.Month(), now.Day(), 0, 0, 0, 0, now.Location()) + t := time.Date(target.Year(), target.Month(), target.Day(), 0, 0, 0, 0, target.Location()) + return int(t.Sub(n).Hours() / 24) +} + +// levelOf 剩余天数 -> 提醒等级 +func levelOf(days int) string { + if days <= 1 { + return "danger" + } + if days <= 3 { + return "warning" + } + return "info" +} diff --git a/go/controllers/backend_crm_bidding_sub.go b/go/controllers/backend_crm_bidding_sub.go new file mode 100644 index 0000000..cba871a --- /dev/null +++ b/go/controllers/backend_crm_bidding_sub.go @@ -0,0 +1,812 @@ +package controllers + +import ( + "encoding/json" + "io" + "strconv" + "strings" + "time" + + "server/models" + + beego "github.com/beego/beego/v2/server/web" +) + +// ============================================================= +// CRM 招投标管理 - 子表 CRUD(统一接口) +// ============================================================= +// +// 每个子表控制器提供统一的接口: +// - List 按外键 bid_id / tender_id 列出 +// - Create 新增(关联外键由请求体传入) +// - Delete 软删除 +// - Update 更新(用于文件改名、任务进度等) +// ============================================================= + +// ------------------------------ 投标文件 ------------------------------ + +// BackendCrmBiddingBidFileController 投标文件 +type BackendCrmBiddingBidFileController struct { + beego.Controller +} + +// bidFilePayload +type bidFilePayload struct { + BidID uint64 `json:"bid_id"` + FileType string `json:"file_type"` + FileName string `json:"file_name"` + FileURL string `json:"file_url"` + FileSize uint64 `json:"file_size"` + FileExt string `json:"file_ext"` + Version string `json:"version"` + UploadUserID string `json:"upload_user_id"` + UploadUserName string `json:"upload_user_name"` + Remark string `json:"remark"` +} + +// List GET /backend/crm/bidding/bid-file/list?bid_id=1 +func (c *BackendCrmBiddingBidFileController) List() { + claims, err := pipelineClaims(&c.Controller) + if err != nil { + pipelineErr(&c.Controller, 401, 401, err.Error()) + return + } + bidID, _ := c.GetInt64("bid_id") + if bidID == 0 { + pipelineErr(&c.Controller, 400, 400, "bid_id 必填") + return + } + var list []models.TenantCrmBiddingBidFile + _, _ = models.Orm.QueryTable(new(models.TenantCrmBiddingBidFile)). + Filter("tenant_id", pipelineTenantID(claims)). + Filter("bid_id", bidID). + Filter("delete_time__isnull", true). + OrderBy("-id").All(&list) + pipelineOk(&c.Controller, map[string]interface{}{"list": list}) +} + +// Create POST /backend/crm/bidding/bid-file +func (c *BackendCrmBiddingBidFileController) 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 bidFilePayload + if err := json.Unmarshal(raw, &p); err != nil { + pipelineErr(&c.Controller, 400, 400, "参数错误") + return + } + if p.BidID == 0 { + pipelineErr(&c.Controller, 400, 400, "bid_id 必填") + return + } + if strings.TrimSpace(p.FileName) == "" || strings.TrimSpace(p.FileURL) == "" { + pipelineErr(&c.Controller, 400, 400, "文件信息不完整") + return + } + now := time.Now() + row := models.TenantCrmBiddingBidFile{ + TenantID: pipelineTenantID(claims), + BidID: p.BidID, + FileType: firstNonEmpty(p.FileType, "1"), + FileName: strings.TrimSpace(p.FileName), + FileURL: strings.TrimSpace(p.FileURL), + FileSize: p.FileSize, + FileExt: strings.TrimSpace(p.FileExt), + Version: firstNonEmpty(p.Version, "1.0"), + UploadUserID: firstNonEmpty(p.UploadUserID, pipelineUID(claims)), + UploadUserName: firstNonEmpty(p.UploadUserName, resolveUserName(claims)), + Remark: p.Remark, + CreateTime: now, + UpdateTime: now, + } + id, err := models.Orm.Insert(&row) + if err != nil { + pipelineErr(&c.Controller, 500, 500, "上传失败: "+err.Error()) + return + } + crmWriteLog(pipelineTenantID(claims), 5, p.BidID, "create", "上传投标文件", claims) + pipelineOk(&c.Controller, map[string]interface{}{"id": id}) +} + +// Delete DELETE /backend/crm/bidding/bid-file/:id +func (c *BackendCrmBiddingBidFileController) 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 + } + now := time.Now() + _, err = models.Orm.QueryTable(new(models.TenantCrmBiddingBidFile)). + Filter("id", id). + Filter("tenant_id", pipelineTenantID(claims)). + Update(map[string]interface{}{"delete_time": now, "update_time": now}) + if err != nil { + pipelineErr(&c.Controller, 500, 500, "删除失败: "+err.Error()) + return + } + var delRow models.TenantCrmBiddingBidFile + _ = models.Orm.QueryTable(new(models.TenantCrmBiddingBidFile)).Filter("id", id).One(&delRow) + crmWriteLog(pipelineTenantID(claims), 5, delRow.BidID, "delete", "删除投标文件", claims) + pipelineOk(&c.Controller, nil) +} + +// ------------------------------ 投标任务 ------------------------------ + +// BackendCrmBiddingBidTaskController 投标任务 +type BackendCrmBiddingBidTaskController struct { + beego.Controller +} + +type bidTaskPayload struct { + BidID uint64 `json:"bid_id"` + TaskName string `json:"task_name"` + TaskType string `json:"task_type"` + Content string `json:"content"` + OwnerUserID string `json:"owner_user_id"` + OwnerUserName string `json:"owner_user_name"` + StartDate string `json:"start_date"` + EndDate string `json:"end_date"` + Status int8 `json:"status"` + Progress int8 `json:"progress"` + Files string `json:"files"` + Remark string `json:"remark"` +} + +// List +func (c *BackendCrmBiddingBidTaskController) List() { + claims, err := pipelineClaims(&c.Controller) + if err != nil { + pipelineErr(&c.Controller, 401, 401, err.Error()) + return + } + bidID, _ := c.GetInt64("bid_id") + if bidID == 0 { + pipelineErr(&c.Controller, 400, 400, "bid_id 必填") + return + } + var list []models.TenantCrmBiddingBidTask + _, _ = models.Orm.QueryTable(new(models.TenantCrmBiddingBidTask)). + Filter("tenant_id", pipelineTenantID(claims)). + Filter("bid_id", bidID). + Filter("delete_time__isnull", true). + OrderBy("-id").All(&list) + pipelineOk(&c.Controller, map[string]interface{}{"list": list}) +} + +// Create +func (c *BackendCrmBiddingBidTaskController) 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 bidTaskPayload + if err := json.Unmarshal(raw, &p); err != nil { + pipelineErr(&c.Controller, 400, 400, "参数错误") + return + } + if p.BidID == 0 || strings.TrimSpace(p.TaskName) == "" { + pipelineErr(&c.Controller, 400, 400, "任务信息不完整") + return + } + now := time.Now() + row := models.TenantCrmBiddingBidTask{ + TenantID: pipelineTenantID(claims), + BidID: p.BidID, + TaskName: strings.TrimSpace(p.TaskName), + TaskType: firstNonEmpty(p.TaskType, "1"), + Content: p.Content, + OwnerUserID: firstNonEmpty(p.OwnerUserID, pipelineUID(claims)), + OwnerUserName: firstNonEmpty(p.OwnerUserName, resolveUserName(claims)), + StartDate: parsePipelineDate(p.StartDate), + EndDate: parsePipelineDate(p.EndDate), + Status: p.Status, + Progress: p.Progress, + Files: p.Files, + Remark: p.Remark, + CreateUserID: pipelineUID(claims), + CreateUserName: resolveUserName(claims), + CreateTime: now, + UpdateTime: now, + } + id, err := models.Orm.Insert(&row) + if err != nil { + pipelineErr(&c.Controller, 500, 500, "创建失败: "+err.Error()) + return + } + crmWriteLog(pipelineTenantID(claims), 5, p.BidID, "create", "新建投标任务", claims) + pipelineOk(&c.Controller, map[string]interface{}{"id": id}) +} + +// Update +func (c *BackendCrmBiddingBidTaskController) 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 bidTaskPayload + if err := json.Unmarshal(raw, &p); err != nil { + pipelineErr(&c.Controller, 400, 400, "参数错误") + return + } + var row models.TenantCrmBiddingBidTask + if err := models.Orm.QueryTable(new(models.TenantCrmBiddingBidTask)). + Filter("id", id). + Filter("tenant_id", pipelineTenantID(claims)). + Filter("delete_time__isnull", true).One(&row); err != nil { + pipelineErr(&c.Controller, 404, 404, "任务未找到") + return + } + if strings.TrimSpace(p.TaskName) != "" { + row.TaskName = strings.TrimSpace(p.TaskName) + } + if strings.TrimSpace(p.TaskType) != "" { + row.TaskType = p.TaskType + } + row.Content = p.Content + if strings.TrimSpace(p.OwnerUserID) != "" { + row.OwnerUserID = strings.TrimSpace(p.OwnerUserID) + row.OwnerUserName = strings.TrimSpace(p.OwnerUserName) + } + row.StartDate = parsePipelineDate(p.StartDate) + row.EndDate = parsePipelineDate(p.EndDate) + row.Status = p.Status + row.Progress = p.Progress + row.Files = p.Files + 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(pipelineTenantID(claims), 5, row.BidID, "update", "更新投标任务", claims) + pipelineOk(&c.Controller, map[string]interface{}{"id": row.ID}) +} + +// Delete +func (c *BackendCrmBiddingBidTaskController) 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 + } + now := time.Now() + _, err = models.Orm.QueryTable(new(models.TenantCrmBiddingBidTask)). + Filter("id", id). + Filter("tenant_id", pipelineTenantID(claims)). + Update(map[string]interface{}{"delete_time": now, "update_time": now}) + if err != nil { + pipelineErr(&c.Controller, 500, 500, "删除失败: "+err.Error()) + return + } + var delRow models.TenantCrmBiddingBidTask + _ = models.Orm.QueryTable(new(models.TenantCrmBiddingBidTask)).Filter("id", id).One(&delRow) + crmWriteLog(pipelineTenantID(claims), 5, delRow.BidID, "delete", "删除投标任务", claims) + pipelineOk(&c.Controller, nil) +} + +// ------------------------------ 投标结果 ------------------------------ + +// BackendCrmBiddingBidResultController 投标结果 +type BackendCrmBiddingBidResultController struct { + beego.Controller +} + +// List +func (c *BackendCrmBiddingBidResultController) List() { + claims, err := pipelineClaims(&c.Controller) + if err != nil { + pipelineErr(&c.Controller, 401, 401, err.Error()) + return + } + bidID, _ := c.GetInt64("bid_id") + if bidID == 0 { + pipelineErr(&c.Controller, 400, 400, "bid_id 必填") + return + } + var list []models.TenantCrmBiddingBidResult + _, _ = models.Orm.QueryTable(new(models.TenantCrmBiddingBidResult)). + Filter("tenant_id", pipelineTenantID(claims)). + Filter("bid_id", bidID). + Filter("delete_time__isnull", true). + OrderBy("-id").All(&list) + pipelineOk(&c.Controller, map[string]interface{}{"list": list}) +} + +// Detail +func (c *BackendCrmBiddingBidResultController) 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.TenantCrmBiddingBidResult + err = models.Orm.QueryTable(new(models.TenantCrmBiddingBidResult)). + 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) +} + +// ------------------------------ 投标报价 ------------------------------ + +// BackendCrmBiddingBidQuoteController 投标报价 +type BackendCrmBiddingBidQuoteController struct { + beego.Controller +} + +type bidQuotePayload struct { + BidID uint64 `json:"bid_id"` + QuoteName string `json:"quote_name"` + TotalAmount float64 `json:"total_amount"` + Currency string `json:"currency"` + TaxRate float64 `json:"tax_rate"` + ValidDate string `json:"valid_date"` + Products string `json:"products"` + CostAnalysis string `json:"cost_analysis"` + ProfitRate float64 `json:"profit_rate"` + Status int8 `json:"status"` + Remark string `json:"remark"` +} + +// List +func (c *BackendCrmBiddingBidQuoteController) List() { + claims, err := pipelineClaims(&c.Controller) + if err != nil { + pipelineErr(&c.Controller, 401, 401, err.Error()) + return + } + bidID, _ := c.GetInt64("bid_id") + if bidID == 0 { + pipelineErr(&c.Controller, 400, 400, "bid_id 必填") + return + } + var list []models.TenantCrmBiddingBidQuote + _, _ = models.Orm.QueryTable(new(models.TenantCrmBiddingBidQuote)). + Filter("tenant_id", pipelineTenantID(claims)). + Filter("bid_id", bidID). + Filter("delete_time__isnull", true). + OrderBy("-id").All(&list) + pipelineOk(&c.Controller, map[string]interface{}{"list": list}) +} + +// Create +func (c *BackendCrmBiddingBidQuoteController) 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 bidQuotePayload + if err := json.Unmarshal(raw, &p); err != nil { + pipelineErr(&c.Controller, 400, 400, "参数错误") + return + } + if p.BidID == 0 || strings.TrimSpace(p.QuoteName) == "" { + pipelineErr(&c.Controller, 400, 400, "报价信息不完整") + return + } + now := time.Now() + row := models.TenantCrmBiddingBidQuote{ + TenantID: pipelineTenantID(claims), + BidID: p.BidID, + QuoteName: strings.TrimSpace(p.QuoteName), + TotalAmount: p.TotalAmount, + Currency: firstNonEmpty(p.Currency, "CNY"), + TaxRate: p.TaxRate, + ValidDate: parsePipelineDate(p.ValidDate), + Products: p.Products, + CostAnalysis: p.CostAnalysis, + ProfitRate: p.ProfitRate, + Status: 1, + SubmitUserID: pipelineUID(claims), + SubmitUserName: resolveUserName(claims), + SubmitTime: &now, + Remark: p.Remark, + CreateTime: now, + UpdateTime: now, + } + id, err := models.Orm.Insert(&row) + if err != nil { + pipelineErr(&c.Controller, 500, 500, "创建失败: "+err.Error()) + return + } + crmWriteLog(pipelineTenantID(claims), 5, p.BidID, "create", "新建投标报价", claims) + pipelineOk(&c.Controller, map[string]interface{}{"id": id}) +} + +// Delete +func (c *BackendCrmBiddingBidQuoteController) 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 + } + now := time.Now() + _, err = models.Orm.QueryTable(new(models.TenantCrmBiddingBidQuote)). + Filter("id", id). + Filter("tenant_id", pipelineTenantID(claims)). + Update(map[string]interface{}{"delete_time": now, "update_time": now}) + if err != nil { + pipelineErr(&c.Controller, 500, 500, "删除失败: "+err.Error()) + return + } + var delRow models.TenantCrmBiddingBidQuote + _ = models.Orm.QueryTable(new(models.TenantCrmBiddingBidQuote)).Filter("id", id).One(&delRow) + crmWriteLog(pipelineTenantID(claims), 5, delRow.BidID, "delete", "删除投标报价", claims) + pipelineOk(&c.Controller, nil) +} + +// ------------------------------ 投标竞争对手 ------------------------------ + +// BackendCrmBiddingBidCompetitorController +type BackendCrmBiddingBidCompetitorController struct { + beego.Controller +} + +type bidCompetitorPayload struct { + BidID uint64 `json:"bid_id"` + CompetitorName string `json:"competitor_name"` + CompetitorType int8 `json:"competitor_type"` + QuoteAmount float64 `json:"quote_amount"` + Advantages string `json:"advantages"` + Weaknesses string `json:"weaknesses"` + Source string `json:"source"` + Remark string `json:"remark"` +} + +// List +func (c *BackendCrmBiddingBidCompetitorController) List() { + claims, err := pipelineClaims(&c.Controller) + if err != nil { + pipelineErr(&c.Controller, 401, 401, err.Error()) + return + } + bidID, _ := c.GetInt64("bid_id") + if bidID == 0 { + pipelineErr(&c.Controller, 400, 400, "bid_id 必填") + return + } + var list []models.TenantCrmBiddingBidCompetitor + _, _ = models.Orm.QueryTable(new(models.TenantCrmBiddingBidCompetitor)). + Filter("tenant_id", pipelineTenantID(claims)). + Filter("bid_id", bidID). + Filter("delete_time__isnull", true). + OrderBy("-id").All(&list) + pipelineOk(&c.Controller, map[string]interface{}{"list": list}) +} + +// Create +func (c *BackendCrmBiddingBidCompetitorController) 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 bidCompetitorPayload + if err := json.Unmarshal(raw, &p); err != nil { + pipelineErr(&c.Controller, 400, 400, "参数错误") + return + } + if p.BidID == 0 || strings.TrimSpace(p.CompetitorName) == "" { + pipelineErr(&c.Controller, 400, 400, "竞争对手信息不完整") + return + } + now := time.Now() + row := models.TenantCrmBiddingBidCompetitor{ + TenantID: pipelineTenantID(claims), + BidID: p.BidID, + CompetitorName: strings.TrimSpace(p.CompetitorName), + CompetitorType: firstNonEmptyInt8(p.CompetitorType, 1), + QuoteAmount: p.QuoteAmount, + Advantages: p.Advantages, + Weaknesses: p.Weaknesses, + Source: strings.TrimSpace(p.Source), + Remark: p.Remark, + CreateUserID: pipelineUID(claims), + CreateUserName: resolveUserName(claims), + CreateTime: now, + UpdateTime: now, + } + id, err := models.Orm.Insert(&row) + if err != nil { + pipelineErr(&c.Controller, 500, 500, "创建失败: "+err.Error()) + return + } + crmWriteLog(pipelineTenantID(claims), 5, p.BidID, "create", "新增竞争对手", claims) + pipelineOk(&c.Controller, map[string]interface{}{"id": id}) +} + +// Delete +func (c *BackendCrmBiddingBidCompetitorController) 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 + } + now := time.Now() + _, err = models.Orm.QueryTable(new(models.TenantCrmBiddingBidCompetitor)). + Filter("id", id). + Filter("tenant_id", pipelineTenantID(claims)). + Update(map[string]interface{}{"delete_time": now, "update_time": now}) + if err != nil { + pipelineErr(&c.Controller, 500, 500, "删除失败: "+err.Error()) + return + } + var delRow models.TenantCrmBiddingBidCompetitor + _ = models.Orm.QueryTable(new(models.TenantCrmBiddingBidCompetitor)).Filter("id", id).One(&delRow) + crmWriteLog(pipelineTenantID(claims), 5, delRow.BidID, "delete", "删除竞争对手", claims) + pipelineOk(&c.Controller, nil) +} + +// ------------------------------ 投标费用 ------------------------------ + +// BackendCrmBiddingBidExpenseController +type BackendCrmBiddingBidExpenseController struct { + beego.Controller +} + +type bidExpensePayload struct { + BidID uint64 `json:"bid_id"` + ExpenseType int8 `json:"expense_type"` + ExpenseName string `json:"expense_name"` + Amount float64 `json:"amount"` + Currency string `json:"currency"` + PaymentStatus int8 `json:"payment_status"` + PaymentDate string `json:"payment_date"` + Description string `json:"description"` + VoucherURL string `json:"voucher_url"` +} + +// List +func (c *BackendCrmBiddingBidExpenseController) List() { + claims, err := pipelineClaims(&c.Controller) + if err != nil { + pipelineErr(&c.Controller, 401, 401, err.Error()) + return + } + bidID, _ := c.GetInt64("bid_id") + if bidID == 0 { + pipelineErr(&c.Controller, 400, 400, "bid_id 必填") + return + } + var list []models.TenantCrmBiddingBidExpense + _, _ = models.Orm.QueryTable(new(models.TenantCrmBiddingBidExpense)). + Filter("tenant_id", pipelineTenantID(claims)). + Filter("bid_id", bidID). + Filter("delete_time__isnull", true). + OrderBy("-id").All(&list) + pipelineOk(&c.Controller, map[string]interface{}{"list": list}) +} + +// Create +func (c *BackendCrmBiddingBidExpenseController) 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 bidExpensePayload + if err := json.Unmarshal(raw, &p); err != nil { + pipelineErr(&c.Controller, 400, 400, "参数错误") + return + } + if p.BidID == 0 || strings.TrimSpace(p.ExpenseName) == "" { + pipelineErr(&c.Controller, 400, 400, "费用信息不完整") + return + } + now := time.Now() + row := models.TenantCrmBiddingBidExpense{ + TenantID: pipelineTenantID(claims), + BidID: p.BidID, + ExpenseType: firstNonEmptyInt8(p.ExpenseType, 1), + ExpenseName: strings.TrimSpace(p.ExpenseName), + Amount: p.Amount, + Currency: firstNonEmpty(p.Currency, "CNY"), + PaymentStatus: p.PaymentStatus, + PaymentDate: parsePipelineDateTime(p.PaymentDate), + Description: p.Description, + VoucherURL: strings.TrimSpace(p.VoucherURL), + CreateUserID: pipelineUID(claims), + CreateUserName: resolveUserName(claims), + CreateTime: now, + UpdateTime: now, + } + id, err := models.Orm.Insert(&row) + if err != nil { + pipelineErr(&c.Controller, 500, 500, "创建失败: "+err.Error()) + return + } + crmWriteLog(pipelineTenantID(claims), 5, p.BidID, "create", "登记投标费用", claims) + pipelineOk(&c.Controller, map[string]interface{}{"id": id}) +} + +// Delete +func (c *BackendCrmBiddingBidExpenseController) 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 + } + now := time.Now() + _, err = models.Orm.QueryTable(new(models.TenantCrmBiddingBidExpense)). + Filter("id", id). + Filter("tenant_id", pipelineTenantID(claims)). + Update(map[string]interface{}{"delete_time": now, "update_time": now}) + if err != nil { + pipelineErr(&c.Controller, 500, 500, "删除失败: "+err.Error()) + return + } + var delRow models.TenantCrmBiddingBidExpense + _ = models.Orm.QueryTable(new(models.TenantCrmBiddingBidExpense)).Filter("id", id).One(&delRow) + crmWriteLog(pipelineTenantID(claims), 5, delRow.BidID, "delete", "删除投标费用", claims) + pipelineOk(&c.Controller, nil) +} + +// ------------------------------ 招标文件 ------------------------------ + +// BackendCrmBiddingTenderFileController 招标文件 +type BackendCrmBiddingTenderFileController struct { + beego.Controller +} + +type tenderFilePayload struct { + TenderID uint64 `json:"tender_id"` + FileType string `json:"file_type"` + FileName string `json:"file_name"` + FileURL string `json:"file_url"` + FileSize uint64 `json:"file_size"` + FileExt string `json:"file_ext"` + UploadUserID string `json:"upload_user_id"` + UploadUserName string `json:"upload_user_name"` + Remark string `json:"remark"` +} + +// List +func (c *BackendCrmBiddingTenderFileController) List() { + claims, err := pipelineClaims(&c.Controller) + if err != nil { + pipelineErr(&c.Controller, 401, 401, err.Error()) + return + } + tenderID, _ := c.GetInt64("tender_id") + if tenderID == 0 { + pipelineErr(&c.Controller, 400, 400, "tender_id 必填") + return + } + var list []models.TenantCrmBiddingTenderFile + _, _ = models.Orm.QueryTable(new(models.TenantCrmBiddingTenderFile)). + Filter("tenant_id", pipelineTenantID(claims)). + Filter("tender_id", tenderID). + Filter("delete_time__isnull", true). + OrderBy("-id").All(&list) + pipelineOk(&c.Controller, map[string]interface{}{"list": list}) +} + +// Create +func (c *BackendCrmBiddingTenderFileController) 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 tenderFilePayload + if err := json.Unmarshal(raw, &p); err != nil { + pipelineErr(&c.Controller, 400, 400, "参数错误") + return + } + if p.TenderID == 0 || strings.TrimSpace(p.FileName) == "" || strings.TrimSpace(p.FileURL) == "" { + pipelineErr(&c.Controller, 400, 400, "文件信息不完整") + return + } + now := time.Now() + row := models.TenantCrmBiddingTenderFile{ + TenantID: pipelineTenantID(claims), + TenderID: p.TenderID, + FileType: firstNonEmpty(p.FileType, "1"), + FileName: strings.TrimSpace(p.FileName), + FileURL: strings.TrimSpace(p.FileURL), + FileSize: p.FileSize, + FileExt: strings.TrimSpace(p.FileExt), + UploadUserID: firstNonEmpty(p.UploadUserID, pipelineUID(claims)), + UploadUserName: firstNonEmpty(p.UploadUserName, resolveUserName(claims)), + Remark: p.Remark, + CreateTime: now, + UpdateTime: now, + } + id, err := models.Orm.Insert(&row) + if err != nil { + pipelineErr(&c.Controller, 500, 500, "上传失败: "+err.Error()) + return + } + crmWriteLog(pipelineTenantID(claims), 5, p.TenderID, "create", "上传招标文件", claims) + pipelineOk(&c.Controller, map[string]interface{}{"id": id}) +} + +// Delete +func (c *BackendCrmBiddingTenderFileController) 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 + } + now := time.Now() + _, err = models.Orm.QueryTable(new(models.TenantCrmBiddingTenderFile)). + Filter("id", id). + Filter("tenant_id", pipelineTenantID(claims)). + Update(map[string]interface{}{"delete_time": now, "update_time": now}) + if err != nil { + pipelineErr(&c.Controller, 500, 500, "删除失败: "+err.Error()) + return + } + var delRow models.TenantCrmBiddingTenderFile + _ = models.Orm.QueryTable(new(models.TenantCrmBiddingTenderFile)).Filter("id", id).One(&delRow) + crmWriteLog(pipelineTenantID(claims), 5, delRow.TenderID, "delete", "删除招标文件", claims) + pipelineOk(&c.Controller, nil) +} + +// ===================================================================== +// 工具函数 +// ===================================================================== + +// firstNonEmptyInt8 返回第一个非零的 int8 +func firstNonEmptyInt8(v, def int8) int8 { + if v == 0 { + return def + } + return v +} \ No newline at end of file diff --git a/go/controllers/backend_crm_bidding_tender.go b/go/controllers/backend_crm_bidding_tender.go new file mode 100644 index 0000000..d76a963 --- /dev/null +++ b/go/controllers/backend_crm_bidding_tender.go @@ -0,0 +1,501 @@ +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}) +} \ No newline at end of file diff --git a/go/docs/sql/bidding_menus.sql b/go/docs/sql/bidding_menus.sql new file mode 100644 index 0000000..1edc6b1 --- /dev/null +++ b/go/docs/sql/bidding_menus.sql @@ -0,0 +1,103 @@ +-- ============================================================ +-- CRM 招投标管理 —— 菜单配置(yz_system_menu) +-- 幂等脚本:按 path 判重,重复执行不会产生重复菜单 +-- 执行:mysql -h -P -u -p < bidding_menus.sql +-- ============================================================ + +-- 菜单表字段说明(yz_system_menu) +-- type: 1=目录菜单,2=页面菜单 +-- pid : 父菜单ID +-- views: 可见端标识,沿用父级 '[2]'(后台) +-- is_visible / status: 1 表示显示并启用 + +-- 顶层入口「招投标」已存在:path = '/apps/crm/bidding' +SET @bidding_id := ( + SELECT id FROM yz_system_menu + WHERE path = '/apps/crm/bidding' AND delete_time IS NULL + LIMIT 1 +); + +-- ---------- 一级子页面 ---------- +INSERT INTO yz_system_menu + (pid, title, path, component_path, icon, sort, status, is_visible, views, type, creater, remark) +SELECT @bidding_id, '我要投标', '/apps/crm/bidding/bid', 'apps/crm/bidding/bid/index.vue', + 'Trophy', 1, 1, 1, '[2]', 2, 'system', '投标方视角:从商机/项目发起投标,中标可转合同/项目' +FROM DUAL +WHERE @bidding_id IS NOT NULL + AND NOT EXISTS (SELECT 1 FROM (SELECT id FROM yz_system_menu WHERE path = '/apps/crm/bidding/bid' AND delete_time IS NULL) x); + +INSERT INTO yz_system_menu + (pid, title, path, component_path, icon, sort, status, is_visible, views, type, creater, remark) +SELECT @bidding_id, '我要招标', '/apps/crm/bidding/tender', 'apps/crm/bidding/tender/index.vue', + 'Document', 2, 1, 1, '[2]', 2, 'system', '招标方视角:发布公告、报名、评标、定标' +FROM DUAL +WHERE @bidding_id IS NOT NULL + AND NOT EXISTS (SELECT 1 FROM (SELECT id FROM yz_system_menu WHERE path = '/apps/crm/bidding/tender' AND delete_time IS NULL) x); + +-- ---------- 公共资料库(目录 + 两个页面) ---------- +INSERT INTO yz_system_menu + (pid, title, path, component_path, icon, sort, status, is_visible, views, type, creater, remark) +SELECT @bidding_id, '公共资料库', '/apps/crm/bidding/library', NULL, + 'Folder', 3, 1, 1, '[2]', 1, 'system', '标书模板 + 企业资质' +FROM DUAL +WHERE @bidding_id IS NOT NULL + AND NOT EXISTS (SELECT 1 FROM (SELECT id FROM yz_system_menu WHERE path = '/apps/crm/bidding/library' AND delete_time IS NULL) x); + +SET @library_id := ( + SELECT id FROM yz_system_menu + WHERE path = '/apps/crm/bidding/library' AND delete_time IS NULL + LIMIT 1 +); + +INSERT INTO yz_system_menu + (pid, title, path, component_path, icon, sort, status, is_visible, views, type, creater, remark) +SELECT @library_id, '标书模板', '/apps/crm/bidding/library/template', 'apps/crm/bidding/library/template.vue', + 'Files', 1, 1, 1, '[2]', 2, 'system', '技术标/商务标/综合标模板库' +FROM DUAL +WHERE @library_id IS NOT NULL + AND NOT EXISTS (SELECT 1 FROM (SELECT id FROM yz_system_menu WHERE path = '/apps/crm/bidding/library/template' AND delete_time IS NULL) x); + +INSERT INTO yz_system_menu + (pid, title, path, component_path, icon, sort, status, is_visible, views, type, creater, remark) +SELECT @library_id, '企业资质', '/apps/crm/bidding/library/qualification', 'apps/crm/bidding/library/qualification.vue', + 'Stamp', 2, 1, 1, '[2]', 2, 'system', '证照资质库,临期自动预警' +FROM DUAL +WHERE @library_id IS NOT NULL + AND NOT EXISTS (SELECT 1 FROM (SELECT id FROM yz_system_menu WHERE path = '/apps/crm/bidding/library/qualification' AND delete_time IS NULL) x); + +-- ---------- 统计报表(目录 + 两个页面) ---------- +INSERT INTO yz_system_menu + (pid, title, path, component_path, icon, sort, status, is_visible, views, type, creater, remark) +SELECT @bidding_id, '统计报表', '/apps/crm/bidding/statistics', NULL, + 'DataAnalysis', 4, 1, 1, '[2]', 1, 'system', '招标/投标数据统计' +FROM DUAL +WHERE @bidding_id IS NOT NULL + AND NOT EXISTS (SELECT 1 FROM (SELECT id FROM yz_system_menu WHERE path = '/apps/crm/bidding/statistics' AND delete_time IS NULL) x); + +SET @stats_id := ( + SELECT id FROM yz_system_menu + WHERE path = '/apps/crm/bidding/statistics' AND delete_time IS NULL + LIMIT 1 +); + +INSERT INTO yz_system_menu + (pid, title, path, component_path, icon, sort, status, is_visible, views, type, creater, remark) +SELECT @stats_id, '招标统计', '/apps/crm/bidding/statistics/tender', 'apps/crm/bidding/statistics/tender.vue', + 'PieChart', 1, 1, 1, '[2]', 2, 'system', '招标状态/类型分布、预算与中标金额' +FROM DUAL +WHERE @stats_id IS NOT NULL + AND NOT EXISTS (SELECT 1 FROM (SELECT id FROM yz_system_menu WHERE path = '/apps/crm/bidding/statistics/tender' AND delete_time IS NULL) x); + +INSERT INTO yz_system_menu + (pid, title, path, component_path, icon, sort, status, is_visible, views, type, creater, remark) +SELECT @stats_id, '投标统计', '/apps/crm/bidding/statistics/bid', 'apps/crm/bidding/statistics/bid.vue', + 'Histogram', 2, 1, 1, '[2]', 2, 'system', '投标数量、中标率、费用投入与月度趋势' +FROM DUAL +WHERE @stats_id IS NOT NULL + AND NOT EXISTS (SELECT 1 FROM (SELECT id FROM yz_system_menu WHERE path = '/apps/crm/bidding/statistics/bid' AND delete_time IS NULL) x); + +-- ---------- 结果校验 ---------- +SELECT id, pid, title, path, component_path, type, sort +FROM yz_system_menu +WHERE delete_time IS NULL AND path LIKE '/apps/crm/bidding%' +ORDER BY pid, sort; diff --git a/go/docs/sql/create_crm_bidding.sql b/go/docs/sql/create_crm_bidding.sql new file mode 100644 index 0000000..3163586 --- /dev/null +++ b/go/docs/sql/create_crm_bidding.sql @@ -0,0 +1,526 @@ +-- ============================================================= +-- CRM 招投标管理模块表结构(17 张) +-- +-- 模块说明:CRM 系统下的「招投标管理」子模块 +-- - 我要招标(甲方):6 张表 +-- - 我要投标(乙方):11 张表 +-- 设计原则: +-- 1. 命名统一以 yz_backend_crm_bidding_ 为前缀 +-- 2. 软删除:delete_time +-- 3. 多租户:tenant_id +-- 4. 与现有 CRM 模块(business/contract/project)字段风格一致 +-- 5. 中标/定标后只存 ID 引用(contract_id、project_id),不冗余数据 +-- +-- 状态机: +-- 招标:0草稿 -> 1公告中 -> 2报名中 -> 3投标中 -> 4开标中 -> 5评标中 -> 6已定标 -> 7已归档 +-- 投标:0草稿 -> 1立项审批中 -> 2投标中 -> 3已开标 -> (4中标 | 5落标) -> (6已转合同 | 7已归档) +-- ============================================================= + +SET NAMES utf8mb4; + +-- ============================================================ +-- 第一部分:我要招标(6 张) +-- ============================================================ + +-- ---------------- 1. 招标项目主表 ---------------- +CREATE TABLE IF NOT EXISTS `yz_backend_crm_bidding_tender` ( + `id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT, + `tenant_id` VARCHAR(64) NOT NULL DEFAULT '' COMMENT '租户ID', + `tender_no` VARCHAR(50) NOT NULL DEFAULT '' COMMENT '招标编号', + `title` VARCHAR(200) NOT NULL DEFAULT '' COMMENT '招标标题/项目名称', + `tender_type` VARCHAR(20) NOT NULL DEFAULT '1' COMMENT '招标类型: 1公开招标 2邀请招标 3竞争性谈判 4询比 5单一来源', + `status` TINYINT NOT NULL DEFAULT 0 COMMENT '0草稿 1公告中 2报名中 3投标中 4开标中 5评标中 6已定标 7已归档', + `customer_id` BIGINT UNSIGNED DEFAULT NULL COMMENT '招标方客户ID(我方客户库)', + `customer_name` VARCHAR(200) NOT NULL DEFAULT '' COMMENT '招标方名称', + `publish_org` VARCHAR(200) NOT NULL DEFAULT '' COMMENT '发布单位', + `budget_amount` DECIMAL(14,2) NOT NULL DEFAULT 0 COMMENT '预算金额', + `bid_deposit` DECIMAL(14,2) NOT NULL DEFAULT 0 COMMENT '保证金', + `publish_date` DATE DEFAULT NULL COMMENT '发布日期', + `register_start_date` DATETIME DEFAULT NULL COMMENT '报名开始', + `register_end_date` DATETIME DEFAULT NULL COMMENT '报名截止', + `bid_start_date` DATETIME DEFAULT NULL COMMENT '投标开始', + `bid_end_date` DATETIME DEFAULT NULL COMMENT '投标截止', + `open_bid_date` DATETIME DEFAULT NULL COMMENT '开标时间', + `contact_person` VARCHAR(50) NOT NULL DEFAULT '' COMMENT '联系人', + `contact_phone` VARCHAR(20) NOT NULL DEFAULT '' COMMENT '联系电话', + `contact_email` VARCHAR(100) NOT NULL DEFAULT '' COMMENT '联系邮箱', + `bid_address` VARCHAR(255) NOT NULL DEFAULT '' COMMENT '投标地址', + `open_address` VARCHAR(255) NOT NULL DEFAULT '' COMMENT '开标地址', + `description` TEXT COMMENT '项目描述', + `requirement` TEXT COMMENT '投标要求', + `evaluation_method` VARCHAR(50) NOT NULL DEFAULT '1' COMMENT '评标方法: 1综合评估 2最低价 3性价比', + `winner_supplier_id` BIGINT UNSIGNED DEFAULT NULL COMMENT '中标供应商ID', + `winner_supplier_name` VARCHAR(200) NOT NULL DEFAULT '' COMMENT '中标供应商名称', + `winner_amount` DECIMAL(14,2) NOT NULL DEFAULT 0 COMMENT '中标金额', + `winner_date` DATETIME DEFAULT NULL COMMENT '定标时间', + `contract_id` BIGINT UNSIGNED DEFAULT NULL COMMENT '转合同ID', + `project_id` BIGINT UNSIGNED DEFAULT NULL COMMENT '关联项目ID', + `owner_user_id` VARCHAR(64) NOT NULL DEFAULT '' COMMENT '负责人ID', + `owner_user_name` VARCHAR(128) NOT NULL DEFAULT '' COMMENT '负责人姓名', + `department_id` BIGINT UNSIGNED DEFAULT 0 COMMENT '部门ID', + `locked` TINYINT NOT NULL DEFAULT 0 COMMENT '0否 1是(定标后锁定)', + `remark` TEXT COMMENT '备注', + `create_user_id` VARCHAR(64) NOT NULL DEFAULT '', + `create_user_name` VARCHAR(128) NOT NULL DEFAULT '', + `create_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + `update_time` DATETIME DEFAULT NULL ON UPDATE CURRENT_TIMESTAMP, + `delete_time` DATETIME DEFAULT NULL, + PRIMARY KEY (`id`), + KEY `idx_tenant_status` (`tenant_id`, `status`), + KEY `idx_tender_no` (`tenant_id`, `tender_no`), + KEY `idx_owner` (`tenant_id`, `owner_user_id`), + KEY `idx_customer` (`tenant_id`, `customer_id`), + KEY `idx_delete_time` (`delete_time`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='CRM-招标项目主表'; + +-- ---------------- 2. 招标文件表 ---------------- +CREATE TABLE IF NOT EXISTS `yz_backend_crm_bidding_tender_file` ( + `id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT, + `tenant_id` VARCHAR(64) NOT NULL DEFAULT '', + `tender_id` BIGINT UNSIGNED NOT NULL DEFAULT 0 COMMENT '招标项目ID', + `file_type` VARCHAR(20) NOT NULL DEFAULT '1' COMMENT '1招标文件 2补充文件 3答疑文件 4其他', + `file_name` VARCHAR(255) NOT NULL DEFAULT '' COMMENT '文件名称', + `file_url` VARCHAR(500) NOT NULL DEFAULT '' COMMENT '文件URL', + `file_size` BIGINT UNSIGNED NOT NULL DEFAULT 0 COMMENT '文件大小(字节)', + `file_ext` VARCHAR(20) NOT NULL DEFAULT '' COMMENT '扩展名', + `upload_user_id` VARCHAR(64) NOT NULL DEFAULT '', + `upload_user_name` VARCHAR(128) NOT NULL DEFAULT '', + `remark` VARCHAR(500) NOT NULL DEFAULT '', + `create_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + `update_time` DATETIME DEFAULT NULL ON UPDATE CURRENT_TIMESTAMP, + `delete_time` DATETIME DEFAULT NULL, + PRIMARY KEY (`id`), + KEY `idx_tenant_tender` (`tenant_id`, `tender_id`), + KEY `idx_delete_time` (`delete_time`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='CRM-招标文件'; + +-- ---------------- 3. 招标公告表 ---------------- +CREATE TABLE IF NOT EXISTS `yz_backend_crm_bidding_tender_announce` ( + `id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT, + `tenant_id` VARCHAR(64) NOT NULL DEFAULT '', + `tender_id` BIGINT UNSIGNED NOT NULL DEFAULT 0 COMMENT '招标项目ID', + `announce_type` VARCHAR(20) NOT NULL DEFAULT '1' COMMENT '1招标公告 2变更公告 3中标公告 4终止公告', + `title` VARCHAR(255) NOT NULL DEFAULT '' COMMENT '公告标题', + `content` LONGTEXT COMMENT '公告内容(富文本)', + `publish_status` TINYINT NOT NULL DEFAULT 0 COMMENT '0草稿 1已发布', + `publish_time` DATETIME DEFAULT NULL COMMENT '发布时间', + `view_count` INT UNSIGNED NOT NULL DEFAULT 0 COMMENT '浏览次数', + `create_user_id` VARCHAR(64) NOT NULL DEFAULT '', + `create_user_name` VARCHAR(128) NOT NULL DEFAULT '', + `create_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + `update_time` DATETIME DEFAULT NULL ON UPDATE CURRENT_TIMESTAMP, + `delete_time` DATETIME DEFAULT NULL, + PRIMARY KEY (`id`), + KEY `idx_tenant_tender` (`tenant_id`, `tender_id`), + KEY `idx_announce_type` (`tenant_id`, `announce_type`), + KEY `idx_delete_time` (`delete_time`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='CRM-招标公告'; + +-- ---------------- 4. 投标方/供应商表 ---------------- +CREATE TABLE IF NOT EXISTS `yz_backend_crm_bidding_tender_supplier` ( + `id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT, + `tenant_id` VARCHAR(64) NOT NULL DEFAULT '', + `tender_id` BIGINT UNSIGNED NOT NULL DEFAULT 0 COMMENT '招标项目ID', + `supplier_id` BIGINT UNSIGNED DEFAULT NULL COMMENT '供应商ID(ERP/CRM共享)', + `supplier_name` VARCHAR(200) NOT NULL DEFAULT '' COMMENT '投标方/供应商名称', + `register_status` TINYINT NOT NULL DEFAULT 1 COMMENT '1待审核 2已通过 3已拒绝 4已退标', + `register_time` DATETIME DEFAULT NULL COMMENT '报名时间', + `bid_amount` DECIMAL(14,2) NOT NULL DEFAULT 0 COMMENT '报价金额', + `bid_files` TEXT COMMENT '投标文件JSON(多文件)', + `contact_person` VARCHAR(50) NOT NULL DEFAULT '', + `contact_phone` VARCHAR(20) NOT NULL DEFAULT '', + `is_winner` TINYINT NOT NULL DEFAULT 0 COMMENT '0否 1是中标方', + `ranking` INT UNSIGNED NOT NULL DEFAULT 0 COMMENT '排名', + `remark` VARCHAR(500) NOT NULL DEFAULT '', + `create_user_id` VARCHAR(64) NOT NULL DEFAULT '', + `create_user_name` VARCHAR(128) NOT NULL DEFAULT '', + `create_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + `update_time` DATETIME DEFAULT NULL ON UPDATE CURRENT_TIMESTAMP, + `delete_time` DATETIME DEFAULT NULL, + PRIMARY KEY (`id`), + KEY `idx_tenant_tender` (`tenant_id`, `tender_id`), + KEY `idx_supplier` (`tenant_id`, `supplier_id`), + KEY `idx_register_status` (`tenant_id`, `register_status`), + KEY `idx_delete_time` (`delete_time`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='CRM-投标方/供应商'; + +-- ---------------- 5. 开标评标表 ---------------- +CREATE TABLE IF NOT EXISTS `yz_backend_crm_bidding_tender_evaluation` ( + `id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT, + `tenant_id` VARCHAR(64) NOT NULL DEFAULT '', + `tender_id` BIGINT UNSIGNED NOT NULL DEFAULT 0 COMMENT '招标项目ID', + `supplier_id` BIGINT UNSIGNED DEFAULT NULL COMMENT '被评标供应商ID', + `supplier_name` VARCHAR(200) NOT NULL DEFAULT '' COMMENT '供应商名称', + `technical_score` DECIMAL(6,2) NOT NULL DEFAULT 0 COMMENT '技术分', + `business_score` DECIMAL(6,2) NOT NULL DEFAULT 0 COMMENT '商务分', + `price_score` DECIMAL(6,2) NOT NULL DEFAULT 0 COMMENT '价格分', + `service_score` DECIMAL(6,2) NOT NULL DEFAULT 0 COMMENT '服务分', + `total_score` DECIMAL(6,2) NOT NULL DEFAULT 0 COMMENT '总分', + `weight_config` VARCHAR(255) NOT NULL DEFAULT '' COMMENT '权重配置JSON', + `ranking` INT UNSIGNED NOT NULL DEFAULT 0 COMMENT '排名', + `evaluation_user_id` VARCHAR(64) NOT NULL DEFAULT '' COMMENT '评标人ID', + `evaluation_user_name` VARCHAR(128) NOT NULL DEFAULT '' COMMENT '评标人姓名', + `evaluation_time` DATETIME DEFAULT NULL COMMENT '评标时间', + `comment` TEXT COMMENT '评语', + `result` TINYINT NOT NULL DEFAULT 0 COMMENT '0待评 1合格 2不合格 3废标', + `create_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + `update_time` DATETIME DEFAULT NULL ON UPDATE CURRENT_TIMESTAMP, + `delete_time` DATETIME DEFAULT NULL, + PRIMARY KEY (`id`), + KEY `idx_tenant_tender` (`tenant_id`, `tender_id`), + KEY `idx_supplier` (`tenant_id`, `supplier_id`), + KEY `idx_delete_time` (`delete_time`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='CRM-开标评标'; + +-- ---------------- 6. 定标结果表 ---------------- +CREATE TABLE IF NOT EXISTS `yz_backend_crm_bidding_tender_result` ( + `id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT, + `tenant_id` VARCHAR(64) NOT NULL DEFAULT '', + `tender_id` BIGINT UNSIGNED NOT NULL DEFAULT 0 COMMENT '招标项目ID', + `result_type` TINYINT NOT NULL DEFAULT 1 COMMENT '1定标 2流标 3废标', + `winner_supplier_id` BIGINT UNSIGNED DEFAULT NULL COMMENT '中标供应商ID', + `winner_supplier_name` VARCHAR(200) NOT NULL DEFAULT '' COMMENT '中标供应商名称', + `winner_amount` DECIMAL(14,2) NOT NULL DEFAULT 0 COMMENT '中标金额', + `result_date` DATETIME DEFAULT NULL COMMENT '定标日期', + `announcement` TEXT COMMENT '定标公告', + `contract_id` BIGINT UNSIGNED DEFAULT NULL COMMENT '转合同ID', + `project_id` BIGINT UNSIGNED DEFAULT NULL COMMENT '关联项目ID', + `create_user_id` VARCHAR(64) NOT NULL DEFAULT '', + `create_user_name` VARCHAR(128) NOT NULL DEFAULT '', + `create_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + `update_time` DATETIME DEFAULT NULL ON UPDATE CURRENT_TIMESTAMP, + `delete_time` DATETIME DEFAULT NULL, + PRIMARY KEY (`id`), + KEY `idx_tenant_tender` (`tenant_id`, `tender_id`), + KEY `idx_result_type` (`tenant_id`, `result_type`), + KEY `idx_delete_time` (`delete_time`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='CRM-定标结果'; + + +-- ============================================================ +-- 第二部分:我要投标(11 张) +-- ============================================================ + +-- ---------------- 7. 投标项目主表 ---------------- +CREATE TABLE IF NOT EXISTS `yz_backend_crm_bidding_bid` ( + `id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT, + `tenant_id` VARCHAR(64) NOT NULL DEFAULT '', + `bid_no` VARCHAR(50) NOT NULL DEFAULT '' COMMENT '投标编号', + `title` VARCHAR(200) NOT NULL DEFAULT '' COMMENT '投标标题/项目名称', + `bid_type` VARCHAR(20) NOT NULL DEFAULT '1' COMMENT '投标类型: 1公开 2邀请 3竞争性谈判 4询比 5单一来源', + `status` TINYINT NOT NULL DEFAULT 0 COMMENT '0草稿 1立项审批中 2投标中 3已开标 4中标 5落标 6已转合同 7已归档', + `source_type` TINYINT NOT NULL DEFAULT 0 COMMENT '0独立 1商机 2项目', + `source_id` BIGINT UNSIGNED DEFAULT NULL COMMENT '来源ID(商机/项目)', + `business_id` BIGINT UNSIGNED DEFAULT NULL COMMENT '关联商机ID', + `business_name` VARCHAR(200) NOT NULL DEFAULT '' COMMENT '商机名称', + `project_id` BIGINT UNSIGNED DEFAULT NULL COMMENT '关联项目ID(中转)', + `project_name` VARCHAR(200) NOT NULL DEFAULT '' COMMENT '项目名称', + `tender_id` BIGINT UNSIGNED DEFAULT NULL COMMENT '关联招标项目ID(如果从招标信息创建)', + `tender_no` VARCHAR(50) NOT NULL DEFAULT '' COMMENT '招标编号', + `customer_id` BIGINT UNSIGNED DEFAULT NULL COMMENT '招标方ID', + `customer_name` VARCHAR(200) NOT NULL DEFAULT '' COMMENT '招标方名称', + `publish_org` VARCHAR(200) NOT NULL DEFAULT '' COMMENT '发布单位', + `bid_amount` DECIMAL(14,2) NOT NULL DEFAULT 0 COMMENT '投标报价', + `bid_deposit` DECIMAL(14,2) NOT NULL DEFAULT 0 COMMENT '保证金', + `publish_date` DATE DEFAULT NULL COMMENT '招标发布日期', + `register_end_date` DATETIME DEFAULT NULL COMMENT '报名截止', + `bid_end_date` DATETIME DEFAULT NULL COMMENT '投标截止', + `open_bid_date` DATETIME DEFAULT NULL COMMENT '开标时间', + `contact_person` VARCHAR(50) NOT NULL DEFAULT '' COMMENT '联系人', + `contact_phone` VARCHAR(20) NOT NULL DEFAULT '', + `bid_address` VARCHAR(255) NOT NULL DEFAULT '' COMMENT '投标地址', + `open_address` VARCHAR(255) NOT NULL DEFAULT '' COMMENT '开标地址', + `win_status` TINYINT NOT NULL DEFAULT 0 COMMENT '0未开标 1中标 2未中标 3废标 4流标', + `win_date` DATETIME DEFAULT NULL COMMENT '中标日期', + `our_ranking` INT UNSIGNED NOT NULL DEFAULT 0 COMMENT '我方排名', + `total_score` DECIMAL(6,2) NOT NULL DEFAULT 0 COMMENT '我方得分', + `competitor_count` INT UNSIGNED NOT NULL DEFAULT 0 COMMENT '竞争对手数量', + `contract_id` BIGINT UNSIGNED DEFAULT NULL COMMENT '转合同ID', + `new_project_id` BIGINT UNSIGNED DEFAULT NULL COMMENT '中标后新建项目ID', + `owner_user_id` VARCHAR(64) NOT NULL DEFAULT '', + `owner_user_name` VARCHAR(128) NOT NULL DEFAULT '', + `department_id` BIGINT UNSIGNED DEFAULT 0 COMMENT '部门ID', + `description` TEXT COMMENT '项目描述', + `remark` TEXT COMMENT '备注', + `locked` TINYINT NOT NULL DEFAULT 0 COMMENT '0否 1是', + `create_user_id` VARCHAR(64) NOT NULL DEFAULT '', + `create_user_name` VARCHAR(128) NOT NULL DEFAULT '', + `create_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + `update_time` DATETIME DEFAULT NULL ON UPDATE CURRENT_TIMESTAMP, + `delete_time` DATETIME DEFAULT NULL, + PRIMARY KEY (`id`), + KEY `idx_tenant_status` (`tenant_id`, `status`), + KEY `idx_bid_no` (`tenant_id`, `bid_no`), + KEY `idx_owner` (`tenant_id`, `owner_user_id`), + KEY `idx_business` (`tenant_id`, `business_id`), + KEY `idx_project` (`tenant_id`, `project_id`), + KEY `idx_tender` (`tenant_id`, `tender_id`), + KEY `idx_customer` (`tenant_id`, `customer_id`), + KEY `idx_win_status` (`tenant_id`, `win_status`), + KEY `idx_delete_time` (`delete_time`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='CRM-投标项目主表'; + +-- ---------------- 8. 投标文件表 ---------------- +CREATE TABLE IF NOT EXISTS `yz_backend_crm_bidding_bid_file` ( + `id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT, + `tenant_id` VARCHAR(64) NOT NULL DEFAULT '', + `bid_id` BIGINT UNSIGNED NOT NULL DEFAULT 0 COMMENT '投标项目ID', + `file_type` VARCHAR(20) NOT NULL DEFAULT '1' COMMENT '1投标文件 2技术方案 3商务方案 4报价单 5保证金 7其他', + `file_name` VARCHAR(255) NOT NULL DEFAULT '', + `file_url` VARCHAR(500) NOT NULL DEFAULT '', + `file_size` BIGINT UNSIGNED NOT NULL DEFAULT 0, + `file_ext` VARCHAR(20) NOT NULL DEFAULT '', + `version` VARCHAR(20) NOT NULL DEFAULT '1.0' COMMENT '版本号', + `upload_user_id` VARCHAR(64) NOT NULL DEFAULT '', + `upload_user_name` VARCHAR(128) NOT NULL DEFAULT '', + `remark` VARCHAR(500) NOT NULL DEFAULT '', + `create_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + `update_time` DATETIME DEFAULT NULL ON UPDATE CURRENT_TIMESTAMP, + `delete_time` DATETIME DEFAULT NULL, + PRIMARY KEY (`id`), + KEY `idx_tenant_bid` (`tenant_id`, `bid_id`), + KEY `idx_delete_time` (`delete_time`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='CRM-投标文件'; + +-- ---------------- 9. 投标立项审批表 ---------------- +CREATE TABLE IF NOT EXISTS `yz_backend_crm_bidding_bid_approval` ( + `id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT, + `tenant_id` VARCHAR(64) NOT NULL DEFAULT '', + `bid_id` BIGINT UNSIGNED NOT NULL DEFAULT 0 COMMENT '投标项目ID', + `approval_type` VARCHAR(20) NOT NULL DEFAULT '1' COMMENT '1立项审批 2报价审批 3合同审批', + `approval_status` TINYINT NOT NULL DEFAULT 0 COMMENT '0待审批 1已通过 2已拒绝 3已撤回', + `approval_flow_id` BIGINT UNSIGNED DEFAULT NULL COMMENT '审批流ID(复用现有引擎)', + `current_user_id` VARCHAR(64) NOT NULL DEFAULT '' COMMENT '当前审批人', + `current_user_name` VARCHAR(128) NOT NULL DEFAULT '', + `approval_users` TEXT COMMENT '审批人列表JSON', + `submit_user_id` VARCHAR(64) NOT NULL DEFAULT '', + `submit_user_name` VARCHAR(128) NOT NULL DEFAULT '', + `submit_time` DATETIME DEFAULT NULL, + `complete_time` DATETIME DEFAULT NULL, + `comment` TEXT COMMENT '审批意见', + `create_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + `update_time` DATETIME DEFAULT NULL ON UPDATE CURRENT_TIMESTAMP, + `delete_time` DATETIME DEFAULT NULL, + PRIMARY KEY (`id`), + KEY `idx_tenant_bid` (`tenant_id`, `bid_id`), + KEY `idx_flow` (`approval_flow_id`), + KEY `idx_status` (`tenant_id`, `approval_status`), + KEY `idx_delete_time` (`delete_time`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='CRM-投标立项审批'; + +-- ---------------- 10. 投标任务表 ---------------- +CREATE TABLE IF NOT EXISTS `yz_backend_crm_bidding_bid_task` ( + `id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT, + `tenant_id` VARCHAR(64) NOT NULL DEFAULT '', + `bid_id` BIGINT UNSIGNED NOT NULL DEFAULT 0 COMMENT '投标项目ID', + `task_name` VARCHAR(100) NOT NULL DEFAULT '' COMMENT '任务名称', + `task_type` VARCHAR(20) NOT NULL DEFAULT '1' COMMENT '1商务 2技术 3报价 4法务 5财务 6其他', + `content` TEXT COMMENT '任务内容', + `owner_user_id` VARCHAR(64) NOT NULL DEFAULT '' COMMENT '负责人', + `owner_user_name` VARCHAR(128) NOT NULL DEFAULT '', + `start_date` DATE DEFAULT NULL, + `end_date` DATE DEFAULT NULL, + `complete_time` DATETIME DEFAULT NULL, + `status` TINYINT NOT NULL DEFAULT 0 COMMENT '0未开始 1进行中 2已完成 3已逾期 4已取消', + `progress` TINYINT NOT NULL DEFAULT 0 COMMENT '进度 0-100', + `files` TEXT COMMENT '附件JSON', + `remark` VARCHAR(500) NOT NULL DEFAULT '', + `create_user_id` VARCHAR(64) NOT NULL DEFAULT '', + `create_user_name` VARCHAR(128) NOT NULL DEFAULT '', + `create_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + `update_time` DATETIME DEFAULT NULL ON UPDATE CURRENT_TIMESTAMP, + `delete_time` DATETIME DEFAULT NULL, + PRIMARY KEY (`id`), + KEY `idx_tenant_bid` (`tenant_id`, `bid_id`), + KEY `idx_owner` (`tenant_id`, `owner_user_id`), + KEY `idx_status` (`tenant_id`, `status`), + KEY `idx_delete_time` (`delete_time`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='CRM-投标任务'; + +-- ---------------- 11. 标书模板表 ---------------- +CREATE TABLE IF NOT EXISTS `yz_backend_crm_bidding_bid_template` ( + `id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT, + `tenant_id` VARCHAR(64) NOT NULL DEFAULT '', + `template_name` VARCHAR(100) NOT NULL DEFAULT '' COMMENT '模板名称', + `template_type` VARCHAR(20) NOT NULL DEFAULT '1' COMMENT '1技术标 2商务标 3综合标', + `industry` VARCHAR(50) NOT NULL DEFAULT '' COMMENT '适用行业', + `description` VARCHAR(500) NOT NULL DEFAULT '', + `content` LONGTEXT COMMENT '模板内容', + `file_url` VARCHAR(500) NOT NULL DEFAULT '' COMMENT '模板文件URL', + `use_count` INT UNSIGNED NOT NULL DEFAULT 0 COMMENT '使用次数', + `status` TINYINT NOT NULL DEFAULT 1 COMMENT '1启用 0禁用', + `owner_user_id` VARCHAR(64) NOT NULL DEFAULT '', + `owner_user_name` VARCHAR(128) NOT NULL DEFAULT '', + `create_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + `update_time` DATETIME DEFAULT NULL ON UPDATE CURRENT_TIMESTAMP, + `delete_time` DATETIME DEFAULT NULL, + PRIMARY KEY (`id`), + KEY `idx_tenant_type` (`tenant_id`, `template_type`), + KEY `idx_delete_time` (`delete_time`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='CRM-标书模板'; + +-- ---------------- 12. 标书版本表 ---------------- +CREATE TABLE IF NOT EXISTS `yz_backend_crm_bidding_bid_document` ( + `id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT, + `tenant_id` VARCHAR(64) NOT NULL DEFAULT '', + `bid_id` BIGINT UNSIGNED NOT NULL DEFAULT 0 COMMENT '投标项目ID', + `template_id` BIGINT UNSIGNED DEFAULT NULL COMMENT '基于模板', + `version` VARCHAR(20) NOT NULL DEFAULT 'v1.0' COMMENT '版本号', + `title` VARCHAR(200) NOT NULL DEFAULT '' COMMENT '版本标题', + `content` LONGTEXT COMMENT '标书内容', + `file_url` VARCHAR(500) NOT NULL DEFAULT '' COMMENT '标书文件URL', + `status` TINYINT NOT NULL DEFAULT 1 COMMENT '1编辑中 2待审核 3已定稿 4已提交', + `editor_user_id` VARCHAR(64) NOT NULL DEFAULT '', + `editor_user_name` VARCHAR(128) NOT NULL DEFAULT '', + `review_user_id` VARCHAR(64) NOT NULL DEFAULT '', + `review_user_name` VARCHAR(128) NOT NULL DEFAULT '', + `review_comment` TEXT COMMENT '审核意见', + `review_time` DATETIME DEFAULT NULL, + `submit_time` DATETIME DEFAULT NULL, + `create_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + `update_time` DATETIME DEFAULT NULL ON UPDATE CURRENT_TIMESTAMP, + `delete_time` DATETIME DEFAULT NULL, + PRIMARY KEY (`id`), + KEY `idx_tenant_bid` (`tenant_id`, `bid_id`), + KEY `idx_template` (`template_id`), + KEY `idx_status` (`tenant_id`, `status`), + KEY `idx_delete_time` (`delete_time`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='CRM-标书版本'; + +-- ---------------- 13. 资质文件库 ---------------- +CREATE TABLE IF NOT EXISTS `yz_backend_crm_bidding_bid_qualification` ( + `id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT, + `tenant_id` VARCHAR(64) NOT NULL DEFAULT '', + `qualification_name` VARCHAR(100) NOT NULL DEFAULT '' COMMENT '资质名称', + `qualification_type` VARCHAR(20) NOT NULL DEFAULT '1' COMMENT '1营业执照 2资质证书 3业绩证明 4认证证书 5其他', + `qualification_no` VARCHAR(100) NOT NULL DEFAULT '' COMMENT '证书编号', + `issuing_authority` VARCHAR(200) NOT NULL DEFAULT '' COMMENT '颁发机构', + `issue_date` DATE DEFAULT NULL COMMENT '颁发日期', + `expire_date` DATE DEFAULT NULL COMMENT '到期日期', + `alert_days` INT UNSIGNED NOT NULL DEFAULT 30 COMMENT '提前提醒天数', + `file_url` VARCHAR(500) NOT NULL DEFAULT '' COMMENT '文件URL', + `file_name` VARCHAR(255) NOT NULL DEFAULT '', + `file_size` BIGINT UNSIGNED NOT NULL DEFAULT 0, + `description` TEXT COMMENT '备注', + `status` TINYINT NOT NULL DEFAULT 1 COMMENT '1有效 2即将过期 0失效', + `owner_user_id` VARCHAR(64) NOT NULL DEFAULT '', + `owner_user_name` VARCHAR(128) NOT NULL DEFAULT '', + `create_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + `update_time` DATETIME DEFAULT NULL ON UPDATE CURRENT_TIMESTAMP, + `delete_time` DATETIME DEFAULT NULL, + PRIMARY KEY (`id`), + KEY `idx_tenant_type` (`tenant_id`, `qualification_type`), + KEY `idx_expire` (`tenant_id`, `expire_date`), + KEY `idx_status` (`tenant_id`, `status`), + KEY `idx_delete_time` (`delete_time`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='CRM-资质文件库'; + +-- ---------------- 14. 报价表 ---------------- +CREATE TABLE IF NOT EXISTS `yz_backend_crm_bidding_bid_quote` ( + `id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT, + `tenant_id` VARCHAR(64) NOT NULL DEFAULT '', + `bid_id` BIGINT UNSIGNED NOT NULL DEFAULT 0 COMMENT '投标项目ID', + `quote_name` VARCHAR(100) NOT NULL DEFAULT '' COMMENT '报价单名称', + `total_amount` DECIMAL(14,2) NOT NULL DEFAULT 0 COMMENT '总报价', + `currency` VARCHAR(10) NOT NULL DEFAULT 'CNY' COMMENT '币种', + `tax_rate` DECIMAL(6,4) NOT NULL DEFAULT 0 COMMENT '税率', + `valid_date` DATE DEFAULT NULL COMMENT '报价有效期', + `products` LONGTEXT COMMENT '产品清单JSON', + `cost_analysis` TEXT COMMENT '成本分析', + `profit_rate` DECIMAL(6,2) NOT NULL DEFAULT 0 COMMENT '利润率%', + `status` TINYINT NOT NULL DEFAULT 1 COMMENT '1草稿 2待审批 3已审批 4已使用', + `approval_flow_id` BIGINT UNSIGNED DEFAULT NULL, + `submit_user_id` VARCHAR(64) NOT NULL DEFAULT '', + `submit_user_name` VARCHAR(128) NOT NULL DEFAULT '', + `submit_time` DATETIME DEFAULT NULL, + `approval_time` DATETIME DEFAULT NULL, + `remark` TEXT COMMENT '备注', + `create_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + `update_time` DATETIME DEFAULT NULL ON UPDATE CURRENT_TIMESTAMP, + `delete_time` DATETIME DEFAULT NULL, + PRIMARY KEY (`id`), + KEY `idx_tenant_bid` (`tenant_id`, `bid_id`), + KEY `idx_status` (`tenant_id`, `status`), + KEY `idx_delete_time` (`delete_time`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='CRM-报价表'; + +-- ---------------- 15. 竞争对手报价表 ---------------- +CREATE TABLE IF NOT EXISTS `yz_backend_crm_bidding_bid_competitor` ( + `id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT, + `tenant_id` VARCHAR(64) NOT NULL DEFAULT '', + `bid_id` BIGINT UNSIGNED NOT NULL DEFAULT 0 COMMENT '投标项目ID', + `competitor_name` VARCHAR(200) NOT NULL DEFAULT '' COMMENT '竞争对手名称', + `competitor_type` TINYINT NOT NULL DEFAULT 1 COMMENT '1同类厂商 2关联企业 3其他', + `quote_amount` DECIMAL(14,2) NOT NULL DEFAULT 0 COMMENT '报价', + `advantages` TEXT COMMENT '优势', + `weaknesses` TEXT COMMENT '劣势', + `source` VARCHAR(200) NOT NULL DEFAULT '' COMMENT '信息来源', + `remark` VARCHAR(500) NOT NULL DEFAULT '', + `create_user_id` VARCHAR(64) NOT NULL DEFAULT '', + `create_user_name` VARCHAR(128) NOT NULL DEFAULT '', + `create_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + `update_time` DATETIME DEFAULT NULL ON UPDATE CURRENT_TIMESTAMP, + `delete_time` DATETIME DEFAULT NULL, + PRIMARY KEY (`id`), + KEY `idx_tenant_bid` (`tenant_id`, `bid_id`), + KEY `idx_delete_time` (`delete_time`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='CRM-竞争对手报价'; + +-- ---------------- 16. 投标结果表 ---------------- +CREATE TABLE IF NOT EXISTS `yz_backend_crm_bidding_bid_result` ( + `id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT, + `tenant_id` VARCHAR(64) NOT NULL DEFAULT '', + `bid_id` BIGINT UNSIGNED NOT NULL DEFAULT 0 COMMENT '投标项目ID', + `result_type` TINYINT NOT NULL DEFAULT 0 COMMENT '1中标 2未中标 3废标 4流标', + `announce_date` DATETIME DEFAULT NULL COMMENT '公告日期', + `announce_url` VARCHAR(500) NOT NULL DEFAULT '' COMMENT '公告URL', + `total_score` DECIMAL(6,2) NOT NULL DEFAULT 0 COMMENT '我方得分', + `our_ranking` INT UNSIGNED NOT NULL DEFAULT 0 COMMENT '我方排名', + `competitor_count` INT UNSIGNED NOT NULL DEFAULT 0 COMMENT '竞争对手数量', + `reason` TEXT COMMENT '原因分析', + `lessons` TEXT COMMENT '经验教训', + `next_step` VARCHAR(255) NOT NULL DEFAULT '' COMMENT '下一步行动', + `create_user_id` VARCHAR(64) NOT NULL DEFAULT '', + `create_user_name` VARCHAR(128) NOT NULL DEFAULT '', + `create_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + `update_time` DATETIME DEFAULT NULL ON UPDATE CURRENT_TIMESTAMP, + `delete_time` DATETIME DEFAULT NULL, + PRIMARY KEY (`id`), + KEY `idx_unique_bid` (`tenant_id`, `bid_id`), + KEY `idx_result_type` (`tenant_id`, `result_type`), + KEY `idx_delete_time` (`delete_time`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='CRM-投标结果'; + +-- ---------------- 17. 投标费用表 ---------------- +CREATE TABLE IF NOT EXISTS `yz_backend_crm_bidding_bid_expense` ( + `id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT, + `tenant_id` VARCHAR(64) NOT NULL DEFAULT '', + `bid_id` BIGINT UNSIGNED NOT NULL DEFAULT 0 COMMENT '投标项目ID', + `expense_type` TINYINT NOT NULL DEFAULT 1 COMMENT '1标书费 2保证金 3差旅费 4咨询费 5代理费 6其他', + `expense_name` VARCHAR(100) NOT NULL DEFAULT '' COMMENT '费用名称', + `amount` DECIMAL(12,2) NOT NULL DEFAULT 0 COMMENT '金额', + `currency` VARCHAR(10) NOT NULL DEFAULT 'CNY', + `payment_status` TINYINT NOT NULL DEFAULT 0 COMMENT '0未付 1已付 2已退还 3已抵扣', + `payment_date` DATETIME DEFAULT NULL COMMENT '支付日期', + `payment_user_id` VARCHAR(64) NOT NULL DEFAULT '', + `payment_user_name` VARCHAR(128) NOT NULL DEFAULT '', + `voucher_url` VARCHAR(500) NOT NULL DEFAULT '' COMMENT '凭证URL', + `description` TEXT COMMENT '说明', + `create_user_id` VARCHAR(64) NOT NULL DEFAULT '', + `create_user_name` VARCHAR(128) NOT NULL DEFAULT '', + `create_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + `update_time` DATETIME DEFAULT NULL ON UPDATE CURRENT_TIMESTAMP, + `delete_time` DATETIME DEFAULT NULL, + PRIMARY KEY (`id`), + KEY `idx_tenant_bid` (`tenant_id`, `bid_id`), + KEY `idx_payment_status` (`tenant_id`, `payment_status`), + KEY `idx_delete_time` (`delete_time`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='CRM-投标费用'; + + +-- ============================================================ +-- 菜单配置(投标管理) +-- ============================================================ +-- 招标方菜单:path = 'apps/crm/bidding/tender/index.vue' +-- 投标方菜单:path = 'apps/crm/bidding/bid/index.vue' +-- 注意:菜单通常通过平台端 UI 配置,详见 docs/bidding_menus.sql \ No newline at end of file diff --git a/go/models/init.go b/go/models/init.go index 31d7fc6..21bb297 100644 --- a/go/models/init.go +++ b/go/models/init.go @@ -97,6 +97,24 @@ func Init(_ string) { new(TenantCrmContract), new(TenantCrmPayback), new(TenantCrmPaybackRecord), + // 招投标管理(docs/sql/create_crm_bidding.sql) + new(TenantCrmBiddingTender), + new(TenantCrmBiddingTenderFile), + new(TenantCrmBiddingTenderAnnounce), + new(TenantCrmBiddingTenderSupplier), + new(TenantCrmBiddingTenderEvaluation), + new(TenantCrmBiddingTenderResult), + new(TenantCrmBiddingBid), + new(TenantCrmBiddingBidFile), + new(TenantCrmBiddingBidApproval), + new(TenantCrmBiddingBidTask), + new(TenantCrmBiddingBidTemplate), + new(TenantCrmBiddingBidDocument), + new(TenantCrmBiddingBidQualification), + new(TenantCrmBiddingBidQuote), + new(TenantCrmBiddingBidCompetitor), + new(TenantCrmBiddingBidResult), + new(TenantCrmBiddingBidExpense), // 平台更新通知(docs/sql/platform_upgrade.sql) new(PlatformUpgradeNotice), new(ErpAccountSet), diff --git a/go/models/tenant_crm_bidding_bid.go b/go/models/tenant_crm_bidding_bid.go new file mode 100644 index 0000000..1130695 --- /dev/null +++ b/go/models/tenant_crm_bidding_bid.go @@ -0,0 +1,143 @@ +package models + +import "time" + +// ============================================================= +// 投标方(我要投标):主表 + 文件 + 审批 + 任务(4 张) +// ============================================================= + +// TenantCrmBiddingBid 投标项目主表: yz_backend_crm_bidding_bid +// +// 状态机: +// 0草稿 -> 1立项审批中 -> 2投标中 -> 3已开标 -> (4中标 | 5落标) -> (6已转合同 | 7已归档) +// +// 来源关联: +// - source_type: 0独立 1商机 2项目 +// - source_id: 来源ID(商机/项目) +type TenantCrmBiddingBid struct { + ID uint64 `orm:"column(id);pk;auto" json:"id"` + TenantID string `orm:"column(tenant_id);size(64)" json:"tenant_id"` + BidNo string `orm:"column(bid_no);size(50)" json:"bid_no"` // 投标编号 + Title string `orm:"column(title);size(200)" json:"title"` // 投标标题 + BidType string `orm:"column(bid_type);size(20);default(1)" json:"bid_type"` + Status int8 `orm:"column(status);default(0)" json:"status"` // 状态 + SourceType int8 `orm:"column(source_type);default(0)" json:"source_type"` + SourceID *uint64 `orm:"column(source_id);null" json:"source_id"` + BusinessID *uint64 `orm:"column(business_id);null" json:"business_id"` // 关联商机 + BusinessName string `orm:"column(business_name);size(200)" json:"business_name"` + ProjectID *uint64 `orm:"column(project_id);null" json:"project_id"` // 关联项目(中转) + ProjectName string `orm:"column(project_name);size(200)" json:"project_name"` + TenderID *uint64 `orm:"column(tender_id);null" json:"tender_id"` // 关联招标项目 + TenderNo string `orm:"column(tender_no);size(50)" json:"tender_no"` + CustomerID *uint64 `orm:"column(customer_id);null" json:"customer_id"` // 招标方 + CustomerName string `orm:"column(customer_name);size(200)" json:"customer_name"` + PublishOrg string `orm:"column(publish_org);size(200)" json:"publish_org"` + BidAmount float64 `orm:"column(bid_amount);digits(14);decimals(2);default(0)" json:"bid_amount"` + BidDeposit float64 `orm:"column(bid_deposit);digits(14);decimals(2);default(0)" json:"bid_deposit"` + PublishDate *time.Time `orm:"column(publish_date);type(date);null" json:"publish_date"` + RegisterEndDate *time.Time `orm:"column(register_end_date);type(datetime);null" json:"register_end_date"` + BidEndDate *time.Time `orm:"column(bid_end_date);type(datetime);null" json:"bid_end_date"` + OpenBidDate *time.Time `orm:"column(open_bid_date);type(datetime);null" json:"open_bid_date"` + ContactPerson string `orm:"column(contact_person);size(50)" json:"contact_person"` + ContactPhone string `orm:"column(contact_phone);size(20)" json:"contact_phone"` + BidAddress string `orm:"column(bid_address);size(255)" json:"bid_address"` + OpenAddress string `orm:"column(open_address);size(255)" json:"open_address"` + WinStatus int8 `orm:"column(win_status);default(0)" json:"win_status"` // 0未开标 1中标 2未中标 3废标 4流标 + WinDate *time.Time `orm:"column(win_date);type(datetime);null" json:"win_date"` + OurRanking uint32 `orm:"column(our_ranking);default(0)" json:"our_ranking"` + TotalScore float64 `orm:"column(total_score);digits(6);decimals(2);default(0)" json:"total_score"` + CompetitorCount uint32 `orm:"column(competitor_count);default(0)" json:"competitor_count"` + ContractID *uint64 `orm:"column(contract_id);null" json:"contract_id"` // 转合同ID + NewProjectID *uint64 `orm:"column(new_project_id);null" json:"new_project_id"` // 中标后新建项目ID + OwnerUserID string `orm:"column(owner_user_id);size(64)" json:"owner_user_id"` + OwnerUserName string `orm:"column(owner_user_name);size(128)" json:"owner_user_name"` + DepartmentID uint64 `orm:"column(department_id);default(0)" json:"department_id"` + Description string `orm:"column(description);type(text);null" json:"description"` + Remark string `orm:"column(remark);type(text);null" json:"remark"` + Locked int8 `orm:"column(locked);default(0)" json:"locked"` + CreateUserID string `orm:"column(create_user_id);size(64)" json:"create_user_id"` + CreateUserName string `orm:"column(create_user_name);size(128)" json:"create_user_name"` + CreateTime time.Time `orm:"column(create_time);auto_now_add;type(datetime)" json:"create_time"` + UpdateTime time.Time `orm:"column(update_time);auto_now;type(datetime)" json:"update_time"` + DeleteTime *time.Time `orm:"column(delete_time);type(datetime);null" json:"delete_time"` +} + +func (m *TenantCrmBiddingBid) TableName() string { + return "yz_backend_crm_bidding_bid" +} + +// TenantCrmBiddingBidFile 投标文件: yz_backend_crm_bidding_bid_file +type TenantCrmBiddingBidFile struct { + ID uint64 `orm:"column(id);pk;auto" json:"id"` + TenantID string `orm:"column(tenant_id);size(64)" json:"tenant_id"` + BidID uint64 `orm:"column(bid_id);default(0)" json:"bid_id"` + FileType string `orm:"column(file_type);size(20);default(1)" json:"file_type"` + FileName string `orm:"column(file_name);size(255)" json:"file_name"` + FileURL string `orm:"column(file_url);size(500)" json:"file_url"` + FileSize uint64 `orm:"column(file_size);default(0)" json:"file_size"` + FileExt string `orm:"column(file_ext);size(20)" json:"file_ext"` + Version string `orm:"column(version);size(20);default(1.0)" json:"version"` + UploadUserID string `orm:"column(upload_user_id);size(64)" json:"upload_user_id"` + UploadUserName string `orm:"column(upload_user_name);size(128)" json:"upload_user_name"` + Remark string `orm:"column(remark);size(500)" json:"remark"` + CreateTime time.Time `orm:"column(create_time);auto_now_add;type(datetime)" json:"create_time"` + UpdateTime time.Time `orm:"column(update_time);auto_now;type(datetime)" json:"update_time"` + DeleteTime *time.Time `orm:"column(delete_time);type(datetime);null" json:"delete_time"` +} + +func (m *TenantCrmBiddingBidFile) TableName() string { + return "yz_backend_crm_bidding_bid_file" +} + +// TenantCrmBiddingBidApproval 投标立项审批: yz_backend_crm_bidding_bid_approval +type TenantCrmBiddingBidApproval struct { + ID uint64 `orm:"column(id);pk;auto" json:"id"` + TenantID string `orm:"column(tenant_id);size(64)" json:"tenant_id"` + BidID uint64 `orm:"column(bid_id);default(0)" json:"bid_id"` + ApprovalType string `orm:"column(approval_type);size(20);default(1)" json:"approval_type"` + ApprovalStatus int8 `orm:"column(approval_status);default(0)" json:"approval_status"` + ApprovalFlowID *uint64 `orm:"column(approval_flow_id);null" json:"approval_flow_id"` + CurrentUserID string `orm:"column(current_user_id);size(64)" json:"current_user_id"` + CurrentUserName string `orm:"column(current_user_name);size(128)" json:"current_user_name"` + ApprovalUsers string `orm:"column(approval_users);type(text);null" json:"approval_users"` + SubmitUserID string `orm:"column(submit_user_id);size(64)" json:"submit_user_id"` + SubmitUserName string `orm:"column(submit_user_name);size(128)" json:"submit_user_name"` + SubmitTime *time.Time `orm:"column(submit_time);type(datetime);null" json:"submit_time"` + CompleteTime *time.Time `orm:"column(complete_time);type(datetime);null" json:"complete_time"` + Comment string `orm:"column(comment);type(text);null" json:"comment"` + CreateTime time.Time `orm:"column(create_time);auto_now_add;type(datetime)" json:"create_time"` + UpdateTime time.Time `orm:"column(update_time);auto_now;type(datetime)" json:"update_time"` + DeleteTime *time.Time `orm:"column(delete_time);type(datetime);null" json:"delete_time"` +} + +func (m *TenantCrmBiddingBidApproval) TableName() string { + return "yz_backend_crm_bidding_bid_approval" +} + +// TenantCrmBiddingBidTask 投标任务: yz_backend_crm_bidding_bid_task +type TenantCrmBiddingBidTask struct { + ID uint64 `orm:"column(id);pk;auto" json:"id"` + TenantID string `orm:"column(tenant_id);size(64)" json:"tenant_id"` + BidID uint64 `orm:"column(bid_id);default(0)" json:"bid_id"` + TaskName string `orm:"column(task_name);size(100)" json:"task_name"` + TaskType string `orm:"column(task_type);size(20);default(1)" json:"task_type"` + Content string `orm:"column(content);type(text);null" json:"content"` + OwnerUserID string `orm:"column(owner_user_id);size(64)" json:"owner_user_id"` + OwnerUserName string `orm:"column(owner_user_name);size(128)" json:"owner_user_name"` + StartDate *time.Time `orm:"column(start_date);type(date);null" json:"start_date"` + EndDate *time.Time `orm:"column(end_date);type(date);null" json:"end_date"` + CompleteTime *time.Time `orm:"column(complete_time);type(datetime);null" json:"complete_time"` + Status int8 `orm:"column(status);default(0)" json:"status"` + Progress int8 `orm:"column(progress);default(0)" json:"progress"` + Files string `orm:"column(files);type(text);null" json:"files"` + Remark string `orm:"column(remark);size(500)" json:"remark"` + CreateUserID string `orm:"column(create_user_id);size(64)" json:"create_user_id"` + CreateUserName string `orm:"column(create_user_name);size(128)" json:"create_user_name"` + CreateTime time.Time `orm:"column(create_time);auto_now_add;type(datetime)" json:"create_time"` + UpdateTime time.Time `orm:"column(update_time);auto_now;type(datetime)" json:"update_time"` + DeleteTime *time.Time `orm:"column(delete_time);type(datetime);null" json:"delete_time"` +} + +func (m *TenantCrmBiddingBidTask) TableName() string { + return "yz_backend_crm_bidding_bid_task" +} \ No newline at end of file diff --git a/go/models/tenant_crm_bidding_library.go b/go/models/tenant_crm_bidding_library.go new file mode 100644 index 0000000..271092a --- /dev/null +++ b/go/models/tenant_crm_bidding_library.go @@ -0,0 +1,113 @@ +package models + +import "time" + +// ============================================================= +// 投标方 - 公共库:模板 + 标书版本 + 资质文件 + 报价(4 张) +// ============================================================= + +// TenantCrmBiddingBidTemplate 标书模板: yz_backend_crm_bidding_bid_template +type TenantCrmBiddingBidTemplate struct { + ID uint64 `orm:"column(id);pk;auto" json:"id"` + TenantID string `orm:"column(tenant_id);size(64)" json:"tenant_id"` + TemplateName string `orm:"column(template_name);size(100)" json:"template_name"` + TemplateType string `orm:"column(template_type);size(20);default(1)" json:"template_type"` // 1技术标 2商务标 3综合标 + Industry string `orm:"column(industry);size(50)" json:"industry"` + Description string `orm:"column(description);size(500)" json:"description"` + Content string `orm:"column(content);type(longtext);null" json:"content"` + FileURL string `orm:"column(file_url);size(500)" json:"file_url"` + UseCount uint32 `orm:"column(use_count);default(0)" json:"use_count"` + Status int8 `orm:"column(status);default(1)" json:"status"` // 1启用 0禁用 + OwnerUserID string `orm:"column(owner_user_id);size(64)" json:"owner_user_id"` + OwnerUserName string `orm:"column(owner_user_name);size(128)" json:"owner_user_name"` + CreateTime time.Time `orm:"column(create_time);auto_now_add;type(datetime)" json:"create_time"` + UpdateTime time.Time `orm:"column(update_time);auto_now;type(datetime)" json:"update_time"` + DeleteTime *time.Time `orm:"column(delete_time);type(datetime);null" json:"delete_time"` +} + +func (m *TenantCrmBiddingBidTemplate) TableName() string { + return "yz_backend_crm_bidding_bid_template" +} + +// TenantCrmBiddingBidDocument 标书版本: yz_backend_crm_bidding_bid_document +type TenantCrmBiddingBidDocument struct { + ID uint64 `orm:"column(id);pk;auto" json:"id"` + TenantID string `orm:"column(tenant_id);size(64)" json:"tenant_id"` + BidID uint64 `orm:"column(bid_id);default(0)" json:"bid_id"` + TemplateID *uint64 `orm:"column(template_id);null" json:"template_id"` + Version string `orm:"column(version);size(20);default(v1.0)" json:"version"` + Title string `orm:"column(title);size(200)" json:"title"` + Content string `orm:"column(content);type(longtext);null" json:"content"` + FileURL string `orm:"column(file_url);size(500)" json:"file_url"` + Status int8 `orm:"column(status);default(1)" json:"status"` // 1编辑中 2待审核 3已定稿 4已提交 + EditorUserID string `orm:"column(editor_user_id);size(64)" json:"editor_user_id"` + EditorUserName string `orm:"column(editor_user_name);size(128)" json:"editor_user_name"` + ReviewUserID string `orm:"column(review_user_id);size(64)" json:"review_user_id"` + ReviewUserName string `orm:"column(review_user_name);size(128)" json:"review_user_name"` + ReviewComment string `orm:"column(review_comment);type(text);null" json:"review_comment"` + ReviewTime *time.Time `orm:"column(review_time);type(datetime);null" json:"review_time"` + SubmitTime *time.Time `orm:"column(submit_time);type(datetime);null" json:"submit_time"` + CreateTime time.Time `orm:"column(create_time);auto_now_add;type(datetime)" json:"create_time"` + UpdateTime time.Time `orm:"column(update_time);auto_now;type(datetime)" json:"update_time"` + DeleteTime *time.Time `orm:"column(delete_time);type(datetime);null" json:"delete_time"` +} + +func (m *TenantCrmBiddingBidDocument) TableName() string { + return "yz_backend_crm_bidding_bid_document" +} + +// TenantCrmBiddingBidQualification 资质文件库: yz_backend_crm_bidding_bid_qualification +type TenantCrmBiddingBidQualification struct { + ID uint64 `orm:"column(id);pk;auto" json:"id"` + TenantID string `orm:"column(tenant_id);size(64)" json:"tenant_id"` + QualificationName string `orm:"column(qualification_name);size(100)" json:"qualification_name"` + QualificationType string `orm:"column(qualification_type);size(20);default(1)" json:"qualification_type"` + QualificationNo string `orm:"column(qualification_no);size(100)" json:"qualification_no"` + IssuingAuthority string `orm:"column(issuing_authority);size(200)" json:"issuing_authority"` + IssueDate *time.Time `orm:"column(issue_date);type(date);null" json:"issue_date"` + ExpireDate *time.Time `orm:"column(expire_date);type(date);null" json:"expire_date"` + AlertDays uint32 `orm:"column(alert_days);default(30)" json:"alert_days"` + FileURL string `orm:"column(file_url);size(500)" json:"file_url"` + FileName string `orm:"column(file_name);size(255)" json:"file_name"` + FileSize uint64 `orm:"column(file_size);default(0)" json:"file_size"` + Description string `orm:"column(description);type(text);null" json:"description"` + Status int8 `orm:"column(status);default(1)" json:"status"` // 1有效 2即将过期 0失效 + OwnerUserID string `orm:"column(owner_user_id);size(64)" json:"owner_user_id"` + OwnerUserName string `orm:"column(owner_user_name);size(128)" json:"owner_user_name"` + CreateTime time.Time `orm:"column(create_time);auto_now_add;type(datetime)" json:"create_time"` + UpdateTime time.Time `orm:"column(update_time);auto_now;type(datetime)" json:"update_time"` + DeleteTime *time.Time `orm:"column(delete_time);type(datetime);null" json:"delete_time"` +} + +func (m *TenantCrmBiddingBidQualification) TableName() string { + return "yz_backend_crm_bidding_bid_qualification" +} + +// TenantCrmBiddingBidQuote 报价表: yz_backend_crm_bidding_bid_quote +type TenantCrmBiddingBidQuote struct { + ID uint64 `orm:"column(id);pk;auto" json:"id"` + TenantID string `orm:"column(tenant_id);size(64)" json:"tenant_id"` + BidID uint64 `orm:"column(bid_id);default(0)" json:"bid_id"` + QuoteName string `orm:"column(quote_name);size(100)" json:"quote_name"` + TotalAmount float64 `orm:"column(total_amount);digits(14);decimals(2);default(0)" json:"total_amount"` + Currency string `orm:"column(currency);size(10);default(CNY)" json:"currency"` + TaxRate float64 `orm:"column(tax_rate);digits(6);decimals(4);default(0)" json:"tax_rate"` + ValidDate *time.Time `orm:"column(valid_date);type(date);null" json:"valid_date"` + Products string `orm:"column(products);type(longtext);null" json:"products"` // 产品清单JSON + CostAnalysis string `orm:"column(cost_analysis);type(text);null" json:"cost_analysis"` + ProfitRate float64 `orm:"column(profit_rate);digits(6);decimals(2);default(0)" json:"profit_rate"` + Status int8 `orm:"column(status);default(1)" json:"status"` + ApprovalFlowID *uint64 `orm:"column(approval_flow_id);null" json:"approval_flow_id"` + SubmitUserID string `orm:"column(submit_user_id);size(64)" json:"submit_user_id"` + SubmitUserName string `orm:"column(submit_user_name);size(128)" json:"submit_user_name"` + SubmitTime *time.Time `orm:"column(submit_time);type(datetime);null" json:"submit_time"` + ApprovalTime *time.Time `orm:"column(approval_time);type(datetime);null" json:"approval_time"` + Remark string `orm:"column(remark);type(text);null" json:"remark"` + CreateTime time.Time `orm:"column(create_time);auto_now_add;type(datetime)" json:"create_time"` + UpdateTime time.Time `orm:"column(update_time);auto_now;type(datetime)" json:"update_time"` + DeleteTime *time.Time `orm:"column(delete_time);type(datetime);null" json:"delete_time"` +} + +func (m *TenantCrmBiddingBidQuote) TableName() string { + return "yz_backend_crm_bidding_bid_quote" +} \ No newline at end of file diff --git a/go/models/tenant_crm_bidding_misc.go b/go/models/tenant_crm_bidding_misc.go new file mode 100644 index 0000000..b700927 --- /dev/null +++ b/go/models/tenant_crm_bidding_misc.go @@ -0,0 +1,81 @@ +package models + +import "time" + +// ============================================================= +// 投标方 - 杂项:竞争对手 + 投标结果 + 投标费用(3 张) +// ============================================================= + +// TenantCrmBiddingBidCompetitor 竞争对手报价: yz_backend_crm_bidding_bid_competitor +type TenantCrmBiddingBidCompetitor struct { + ID uint64 `orm:"column(id);pk;auto" json:"id"` + TenantID string `orm:"column(tenant_id);size(64)" json:"tenant_id"` + BidID uint64 `orm:"column(bid_id);default(0)" json:"bid_id"` + CompetitorName string `orm:"column(competitor_name);size(200)" json:"competitor_name"` + CompetitorType int8 `orm:"column(competitor_type);default(1)" json:"competitor_type"` // 1同类厂商 2关联企业 3其他 + QuoteAmount float64 `orm:"column(quote_amount);digits(14);decimals(2);default(0)" json:"quote_amount"` + Advantages string `orm:"column(advantages);type(text);null" json:"advantages"` + Weaknesses string `orm:"column(weaknesses);type(text);null" json:"weaknesses"` + Source string `orm:"column(source);size(200)" json:"source"` + Remark string `orm:"column(remark);size(500)" json:"remark"` + CreateUserID string `orm:"column(create_user_id);size(64)" json:"create_user_id"` + CreateUserName string `orm:"column(create_user_name);size(128)" json:"create_user_name"` + CreateTime time.Time `orm:"column(create_time);auto_now_add;type(datetime)" json:"create_time"` + UpdateTime time.Time `orm:"column(update_time);auto_now;type(datetime)" json:"update_time"` + DeleteTime *time.Time `orm:"column(delete_time);type(datetime);null" json:"delete_time"` +} + +func (m *TenantCrmBiddingBidCompetitor) TableName() string { + return "yz_backend_crm_bidding_bid_competitor" +} + +// TenantCrmBiddingBidResult 投标结果: yz_backend_crm_bidding_bid_result +type TenantCrmBiddingBidResult struct { + ID uint64 `orm:"column(id);pk;auto" json:"id"` + TenantID string `orm:"column(tenant_id);size(64)" json:"tenant_id"` + BidID uint64 `orm:"column(bid_id);default(0)" json:"bid_id"` + ResultType int8 `orm:"column(result_type);default(0)" json:"result_type"` // 1中标 2未中标 3废标 4流标 + AnnounceDate *time.Time `orm:"column(announce_date);type(datetime);null" json:"announce_date"` + AnnounceURL string `orm:"column(announce_url);size(500)" json:"announce_url"` + TotalScore float64 `orm:"column(total_score);digits(6);decimals(2);default(0)" json:"total_score"` + OurRanking uint32 `orm:"column(our_ranking);default(0)" json:"our_ranking"` + CompetitorCount uint32 `orm:"column(competitor_count);default(0)" json:"competitor_count"` + Reason string `orm:"column(reason);type(text);null" json:"reason"` + Lessons string `orm:"column(lessons);type(text);null" json:"lessons"` + NextStep string `orm:"column(next_step);size(255)" json:"next_step"` + CreateUserID string `orm:"column(create_user_id);size(64)" json:"create_user_id"` + CreateUserName string `orm:"column(create_user_name);size(128)" json:"create_user_name"` + CreateTime time.Time `orm:"column(create_time);auto_now_add;type(datetime)" json:"create_time"` + UpdateTime time.Time `orm:"column(update_time);auto_now;type(datetime)" json:"update_time"` + DeleteTime *time.Time `orm:"column(delete_time);type(datetime);null" json:"delete_time"` +} + +func (m *TenantCrmBiddingBidResult) TableName() string { + return "yz_backend_crm_bidding_bid_result" +} + +// TenantCrmBiddingBidExpense 投标费用: yz_backend_crm_bidding_bid_expense +type TenantCrmBiddingBidExpense struct { + ID uint64 `orm:"column(id);pk;auto" json:"id"` + TenantID string `orm:"column(tenant_id);size(64)" json:"tenant_id"` + BidID uint64 `orm:"column(bid_id);default(0)" json:"bid_id"` + ExpenseType int8 `orm:"column(expense_type);default(1)" json:"expense_type"` // 1标书费 2保证金 3差旅费 4咨询费 5代理费 6其他 + ExpenseName string `orm:"column(expense_name);size(100)" json:"expense_name"` + Amount float64 `orm:"column(amount);digits(12);decimals(2);default(0)" json:"amount"` + Currency string `orm:"column(currency);size(10);default(CNY)" json:"currency"` + PaymentStatus int8 `orm:"column(payment_status);default(0)" json:"payment_status"` // 0未付 1已付 2已退还 3已抵扣 + PaymentDate *time.Time `orm:"column(payment_date);type(datetime);null" json:"payment_date"` + PaymentUserID string `orm:"column(payment_user_id);size(64)" json:"payment_user_id"` + PaymentUserName string `orm:"column(payment_user_name);size(128)" json:"payment_user_name"` + VoucherURL string `orm:"column(voucher_url);size(500)" json:"voucher_url"` + Description string `orm:"column(description);type(text);null" json:"description"` + CreateUserID string `orm:"column(create_user_id);size(64)" json:"create_user_id"` + CreateUserName string `orm:"column(create_user_name);size(128)" json:"create_user_name"` + CreateTime time.Time `orm:"column(create_time);auto_now_add;type(datetime)" json:"create_time"` + UpdateTime time.Time `orm:"column(update_time);auto_now;type(datetime)" json:"update_time"` + DeleteTime *time.Time `orm:"column(delete_time);type(datetime);null" json:"delete_time"` +} + +func (m *TenantCrmBiddingBidExpense) TableName() string { + return "yz_backend_crm_bidding_bid_expense" +} \ No newline at end of file diff --git a/go/models/tenant_crm_bidding_tender.go b/go/models/tenant_crm_bidding_tender.go new file mode 100644 index 0000000..04529c1 --- /dev/null +++ b/go/models/tenant_crm_bidding_tender.go @@ -0,0 +1,182 @@ +package models + +import "time" + +// ============================================================= +// 招标方(我要招标):6 张表 +// ============================================================= + +// TenantCrmBiddingTender 招标项目主表: yz_backend_crm_bidding_tender +// +// 状态机: +// 0草稿 -> 1公告中 -> 2报名中 -> 3投标中 -> 4开标中 -> 5评标中 -> 6已定标 -> 7已归档 +type TenantCrmBiddingTender struct { + ID uint64 `orm:"column(id);pk;auto" json:"id"` + TenantID string `orm:"column(tenant_id);size(64)" json:"tenant_id"` + TenderNo string `orm:"column(tender_no);size(50)" json:"tender_no"` // 招标编号 + Title string `orm:"column(title);size(200)" json:"title"` // 招标标题 + TenderType string `orm:"column(tender_type);size(20);default(1)" json:"tender_type"` // 招标类型 + Status int8 `orm:"column(status);default(0)" json:"status"` // 状态 + CustomerID *uint64 `orm:"column(customer_id);null" json:"customer_id"` // 招标方客户ID + CustomerName string `orm:"column(customer_name);size(200)" json:"customer_name"` + PublishOrg string `orm:"column(publish_org);size(200)" json:"publish_org"` + BudgetAmount float64 `orm:"column(budget_amount);digits(14);decimals(2);default(0)" json:"budget_amount"` + BidDeposit float64 `orm:"column(bid_deposit);digits(14);decimals(2);default(0)" json:"bid_deposit"` + PublishDate *time.Time `orm:"column(publish_date);type(date);null" json:"publish_date"` + RegisterStartDate *time.Time `orm:"column(register_start_date);type(datetime);null" json:"register_start_date"` + RegisterEndDate *time.Time `orm:"column(register_end_date);type(datetime);null" json:"register_end_date"` + BidStartDate *time.Time `orm:"column(bid_start_date);type(datetime);null" json:"bid_start_date"` + BidEndDate *time.Time `orm:"column(bid_end_date);type(datetime);null" json:"bid_end_date"` + OpenBidDate *time.Time `orm:"column(open_bid_date);type(datetime);null" json:"open_bid_date"` + ContactPerson string `orm:"column(contact_person);size(50)" json:"contact_person"` + ContactPhone string `orm:"column(contact_phone);size(20)" json:"contact_phone"` + ContactEmail string `orm:"column(contact_email);size(100)" json:"contact_email"` + BidAddress string `orm:"column(bid_address);size(255)" json:"bid_address"` + OpenAddress string `orm:"column(open_address);size(255)" json:"open_address"` + Description string `orm:"column(description);type(text);null" json:"description"` + Requirement string `orm:"column(requirement);type(text);null" json:"requirement"` + EvaluationMethod string `orm:"column(evaluation_method);size(50);default(1)" json:"evaluation_method"` + WinnerSupplierID *uint64 `orm:"column(winner_supplier_id);null" json:"winner_supplier_id"` + WinnerSupplierName string `orm:"column(winner_supplier_name);size(200)" json:"winner_supplier_name"` + WinnerAmount float64 `orm:"column(winner_amount);digits(14);decimals(2);default(0)" json:"winner_amount"` + WinnerDate *time.Time `orm:"column(winner_date);type(datetime);null" json:"winner_date"` + ContractID *uint64 `orm:"column(contract_id);null" json:"contract_id"` // 转合同ID + ProjectID *uint64 `orm:"column(project_id);null" json:"project_id"` // 关联项目ID + OwnerUserID string `orm:"column(owner_user_id);size(64)" json:"owner_user_id"` + OwnerUserName string `orm:"column(owner_user_name);size(128)" json:"owner_user_name"` + DepartmentID uint64 `orm:"column(department_id);default(0)" json:"department_id"` + Locked int8 `orm:"column(locked);default(0)" json:"locked"` + Remark string `orm:"column(remark);type(text);null" json:"remark"` + CreateUserID string `orm:"column(create_user_id);size(64)" json:"create_user_id"` + CreateUserName string `orm:"column(create_user_name);size(128)" json:"create_user_name"` + CreateTime time.Time `orm:"column(create_time);auto_now_add;type(datetime)" json:"create_time"` + UpdateTime time.Time `orm:"column(update_time);auto_now;type(datetime)" json:"update_time"` + DeleteTime *time.Time `orm:"column(delete_time);type(datetime);null" json:"delete_time"` +} + +func (m *TenantCrmBiddingTender) TableName() string { + return "yz_backend_crm_bidding_tender" +} + +// TenantCrmBiddingTenderFile 招标文件: yz_backend_crm_bidding_tender_file +type TenantCrmBiddingTenderFile struct { + ID uint64 `orm:"column(id);pk;auto" json:"id"` + TenantID string `orm:"column(tenant_id);size(64)" json:"tenant_id"` + TenderID uint64 `orm:"column(tender_id);default(0)" json:"tender_id"` + FileType string `orm:"column(file_type);size(20);default(1)" json:"file_type"` + FileName string `orm:"column(file_name);size(255)" json:"file_name"` + FileURL string `orm:"column(file_url);size(500)" json:"file_url"` + FileSize uint64 `orm:"column(file_size);default(0)" json:"file_size"` + FileExt string `orm:"column(file_ext);size(20)" json:"file_ext"` + UploadUserID string `orm:"column(upload_user_id);size(64)" json:"upload_user_id"` + UploadUserName string `orm:"column(upload_user_name);size(128)" json:"upload_user_name"` + Remark string `orm:"column(remark);size(500)" json:"remark"` + CreateTime time.Time `orm:"column(create_time);auto_now_add;type(datetime)" json:"create_time"` + UpdateTime time.Time `orm:"column(update_time);auto_now;type(datetime)" json:"update_time"` + DeleteTime *time.Time `orm:"column(delete_time);type(datetime);null" json:"delete_time"` +} + +func (m *TenantCrmBiddingTenderFile) TableName() string { + return "yz_backend_crm_bidding_tender_file" +} + +// TenantCrmBiddingTenderAnnounce 招标公告: yz_backend_crm_bidding_tender_announce +type TenantCrmBiddingTenderAnnounce struct { + ID uint64 `orm:"column(id);pk;auto" json:"id"` + TenantID string `orm:"column(tenant_id);size(64)" json:"tenant_id"` + TenderID uint64 `orm:"column(tender_id);default(0)" json:"tender_id"` + AnnounceType string `orm:"column(announce_type);size(20);default(1)" json:"announce_type"` + Title string `orm:"column(title);size(255)" json:"title"` + Content string `orm:"column(content);type(longtext);null" json:"content"` + PublishStatus int8 `orm:"column(publish_status);default(0)" json:"publish_status"` + PublishTime *time.Time `orm:"column(publish_time);type(datetime);null" json:"publish_time"` + ViewCount uint32 `orm:"column(view_count);default(0)" json:"view_count"` + CreateUserID string `orm:"column(create_user_id);size(64)" json:"create_user_id"` + CreateUserName string `orm:"column(create_user_name);size(128)" json:"create_user_name"` + CreateTime time.Time `orm:"column(create_time);auto_now_add;type(datetime)" json:"create_time"` + UpdateTime time.Time `orm:"column(update_time);auto_now;type(datetime)" json:"update_time"` + DeleteTime *time.Time `orm:"column(delete_time);type(datetime);null" json:"delete_time"` +} + +func (m *TenantCrmBiddingTenderAnnounce) TableName() string { + return "yz_backend_crm_bidding_tender_announce" +} + +// TenantCrmBiddingTenderSupplier 投标方/供应商: yz_backend_crm_bidding_tender_supplier +type TenantCrmBiddingTenderSupplier struct { + ID uint64 `orm:"column(id);pk;auto" json:"id"` + TenantID string `orm:"column(tenant_id);size(64)" json:"tenant_id"` + TenderID uint64 `orm:"column(tender_id);default(0)" json:"tender_id"` + SupplierID *uint64 `orm:"column(supplier_id);null" json:"supplier_id"` + SupplierName string `orm:"column(supplier_name);size(200)" json:"supplier_name"` + RegisterStatus int8 `orm:"column(register_status);default(1)" json:"register_status"` + RegisterTime *time.Time `orm:"column(register_time);type(datetime);null" json:"register_time"` + BidAmount float64 `orm:"column(bid_amount);digits(14);decimals(2);default(0)" json:"bid_amount"` + BidFiles string `orm:"column(bid_files);type(text);null" json:"bid_files"` + ContactPerson string `orm:"column(contact_person);size(50)" json:"contact_person"` + ContactPhone string `orm:"column(contact_phone);size(20)" json:"contact_phone"` + IsWinner int8 `orm:"column(is_winner);default(0)" json:"is_winner"` + Ranking uint32 `orm:"column(ranking);default(0)" json:"ranking"` + Remark string `orm:"column(remark);size(500)" json:"remark"` + CreateUserID string `orm:"column(create_user_id);size(64)" json:"create_user_id"` + CreateUserName string `orm:"column(create_user_name);size(128)" json:"create_user_name"` + CreateTime time.Time `orm:"column(create_time);auto_now_add;type(datetime)" json:"create_time"` + UpdateTime time.Time `orm:"column(update_time);auto_now;type(datetime)" json:"update_time"` + DeleteTime *time.Time `orm:"column(delete_time);type(datetime);null" json:"delete_time"` +} + +func (m *TenantCrmBiddingTenderSupplier) TableName() string { + return "yz_backend_crm_bidding_tender_supplier" +} + +// TenantCrmBiddingTenderEvaluation 开标评标: yz_backend_crm_bidding_tender_evaluation +type TenantCrmBiddingTenderEvaluation struct { + ID uint64 `orm:"column(id);pk;auto" json:"id"` + TenantID string `orm:"column(tenant_id);size(64)" json:"tenant_id"` + TenderID uint64 `orm:"column(tender_id);default(0)" json:"tender_id"` + SupplierID *uint64 `orm:"column(supplier_id);null" json:"supplier_id"` + SupplierName string `orm:"column(supplier_name);size(200)" json:"supplier_name"` + TechnicalScore float64 `orm:"column(technical_score);digits(6);decimals(2);default(0)" json:"technical_score"` + BusinessScore float64 `orm:"column(business_score);digits(6);decimals(2);default(0)" json:"business_score"` + PriceScore float64 `orm:"column(price_score);digits(6);decimals(2);default(0)" json:"price_score"` + ServiceScore float64 `orm:"column(service_score);digits(6);decimals(2);default(0)" json:"service_score"` + TotalScore float64 `orm:"column(total_score);digits(6);decimals(2);default(0)" json:"total_score"` + WeightConfig string `orm:"column(weight_config);size(255)" json:"weight_config"` + Ranking uint32 `orm:"column(ranking);default(0)" json:"ranking"` + EvaluationUserID string `orm:"column(evaluation_user_id);size(64)" json:"evaluation_user_id"` + EvaluationUserName string `orm:"column(evaluation_user_name);size(128)" json:"evaluation_user_name"` + EvaluationTime *time.Time `orm:"column(evaluation_time);type(datetime);null" json:"evaluation_time"` + Comment string `orm:"column(comment);type(text);null" json:"comment"` + Result int8 `orm:"column(result);default(0)" json:"result"` + CreateTime time.Time `orm:"column(create_time);auto_now_add;type(datetime)" json:"create_time"` + UpdateTime time.Time `orm:"column(update_time);auto_now;type(datetime)" json:"update_time"` + DeleteTime *time.Time `orm:"column(delete_time);type(datetime);null" json:"delete_time"` +} + +func (m *TenantCrmBiddingTenderEvaluation) TableName() string { + return "yz_backend_crm_bidding_tender_evaluation" +} + +// TenantCrmBiddingTenderResult 定标结果: yz_backend_crm_bidding_tender_result +type TenantCrmBiddingTenderResult struct { + ID uint64 `orm:"column(id);pk;auto" json:"id"` + TenantID string `orm:"column(tenant_id);size(64)" json:"tenant_id"` + TenderID uint64 `orm:"column(tender_id);default(0)" json:"tender_id"` + ResultType int8 `orm:"column(result_type);default(1)" json:"result_type"` + WinnerSupplierID *uint64 `orm:"column(winner_supplier_id);null" json:"winner_supplier_id"` + WinnerSupplierName string `orm:"column(winner_supplier_name);size(200)" json:"winner_supplier_name"` + WinnerAmount float64 `orm:"column(winner_amount);digits(14);decimals(2);default(0)" json:"winner_amount"` + ResultDate *time.Time `orm:"column(result_date);type(datetime);null" json:"result_date"` + Announcement string `orm:"column(announcement);type(text);null" json:"announcement"` + ContractID *uint64 `orm:"column(contract_id);null" json:"contract_id"` + ProjectID *uint64 `orm:"column(project_id);null" json:"project_id"` + CreateUserID string `orm:"column(create_user_id);size(64)" json:"create_user_id"` + CreateUserName string `orm:"column(create_user_name);size(128)" json:"create_user_name"` + CreateTime time.Time `orm:"column(create_time);auto_now_add;type(datetime)" json:"create_time"` + UpdateTime time.Time `orm:"column(update_time);auto_now;type(datetime)" json:"update_time"` + DeleteTime *time.Time `orm:"column(delete_time);type(datetime);null" json:"delete_time"` +} + +func (m *TenantCrmBiddingTenderResult) TableName() string { + return "yz_backend_crm_bidding_tender_result" +} \ No newline at end of file diff --git a/go/routers/backend/backend.go b/go/routers/backend/backend.go index dc138c9..7b5c640 100644 --- a/go/routers/backend/backend.go +++ b/go/routers/backend/backend.go @@ -503,6 +503,102 @@ func registerOrganizationRoutes(module string) { // CRM操作日志 beego.Router("/backend/crm/oplog/list", &controllers.BackendCrmOperateLogController{}, "get:List") + // ============================================================ + // CRM 招投标管理(我要招标 / 我要投标) + // ============================================================ + // 投标方(Bid):从商机/项目发起 -> 投标 -> 中标 -> 转合同/项目 + beego.Router("/backend/crm/bidding/bid/list", &controllers.BackendCrmBiddingBidController{}, "get:List") + beego.Router("/backend/crm/bidding/bid/detail/:id", &controllers.BackendCrmBiddingBidController{}, "get:Detail") + beego.Router("/backend/crm/bidding/bid", &controllers.BackendCrmBiddingBidController{}, "post:Create") + beego.Router("/backend/crm/bidding/bid/:id", &controllers.BackendCrmBiddingBidController{}, "put:Update;delete:Delete") + beego.Router("/backend/crm/bidding/bid/:id/contract", &controllers.BackendCrmBiddingBidController{}, "post:ConvertContract") + beego.Router("/backend/crm/bidding/bid/:id/project", &controllers.BackendCrmBiddingBidController{}, "post:ConvertProject") + beego.Router("/backend/crm/bidding/bid/:id/result", &controllers.BackendCrmBiddingBidController{}, "post:SubmitResult") + beego.Router("/backend/crm/bidding/bid/from-business", &controllers.BackendCrmBiddingBidController{}, "post:FromBusiness") + beego.Router("/backend/crm/bidding/bid/from-project", &controllers.BackendCrmBiddingBidController{}, "post:FromProject") + // 投标文件 + beego.Router("/backend/crm/bidding/bid-file/list", &controllers.BackendCrmBiddingBidFileController{}, "get:List") + beego.Router("/backend/crm/bidding/bid-file", &controllers.BackendCrmBiddingBidFileController{}, "post:Create") + beego.Router("/backend/crm/bidding/bid-file/:id", &controllers.BackendCrmBiddingBidFileController{}, "delete:Delete") + // 投标任务 + beego.Router("/backend/crm/bidding/bid-task/list", &controllers.BackendCrmBiddingBidTaskController{}, "get:List") + beego.Router("/backend/crm/bidding/bid-task", &controllers.BackendCrmBiddingBidTaskController{}, "post:Create") + beego.Router("/backend/crm/bidding/bid-task/:id", &controllers.BackendCrmBiddingBidTaskController{}, "put:Update;delete:Delete") + // 投标结果 + beego.Router("/backend/crm/bidding/bid-result/list", &controllers.BackendCrmBiddingBidResultController{}, "get:List") + beego.Router("/backend/crm/bidding/bid-result/detail/:id", &controllers.BackendCrmBiddingBidResultController{}, "get:Detail") + // 投标报价 + beego.Router("/backend/crm/bidding/bid-quote/list", &controllers.BackendCrmBiddingBidQuoteController{}, "get:List") + beego.Router("/backend/crm/bidding/bid-quote", &controllers.BackendCrmBiddingBidQuoteController{}, "post:Create") + beego.Router("/backend/crm/bidding/bid-quote/:id", &controllers.BackendCrmBiddingBidQuoteController{}, "delete:Delete") + // 投标竞争对手 + beego.Router("/backend/crm/bidding/bid-competitor/list", &controllers.BackendCrmBiddingBidCompetitorController{}, "get:List") + beego.Router("/backend/crm/bidding/bid-competitor", &controllers.BackendCrmBiddingBidCompetitorController{}, "post:Create") + beego.Router("/backend/crm/bidding/bid-competitor/:id", &controllers.BackendCrmBiddingBidCompetitorController{}, "delete:Delete") + // 投标费用 + beego.Router("/backend/crm/bidding/bid-expense/list", &controllers.BackendCrmBiddingBidExpenseController{}, "get:List") + beego.Router("/backend/crm/bidding/bid-expense", &controllers.BackendCrmBiddingBidExpenseController{}, "post:Create") + beego.Router("/backend/crm/bidding/bid-expense/:id", &controllers.BackendCrmBiddingBidExpenseController{}, "delete:Delete") + + // 招标方(Tender):发布招标 -> 投标方报名 -> 评标 -> 定标 -> 转合同 + beego.Router("/backend/crm/bidding/tender/list", &controllers.BackendCrmBiddingTenderController{}, "get:List") + beego.Router("/backend/crm/bidding/tender/detail/:id", &controllers.BackendCrmBiddingTenderController{}, "get:Detail") + beego.Router("/backend/crm/bidding/tender", &controllers.BackendCrmBiddingTenderController{}, "post:Create") + beego.Router("/backend/crm/bidding/tender/:id", &controllers.BackendCrmBiddingTenderController{}, "put:Update;delete:Delete") + beego.Router("/backend/crm/bidding/tender/:id/award", &controllers.BackendCrmBiddingTenderController{}, "post:Award") + beego.Router("/backend/crm/bidding/tender/:id/contract", &controllers.BackendCrmBiddingTenderController{}, "post:AwardToContract") + // 招标文件 + beego.Router("/backend/crm/bidding/tender-file/list", &controllers.BackendCrmBiddingTenderFileController{}, "get:List") + beego.Router("/backend/crm/bidding/tender-file", &controllers.BackendCrmBiddingTenderFileController{}, "post:Create") + beego.Router("/backend/crm/bidding/tender-file/:id", &controllers.BackendCrmBiddingTenderFileController{}, "delete:Delete") + + // 招标公告 + beego.Router("/backend/crm/bidding/tender-announce/list", &controllers.BackendCrmBiddingTenderAnnounceController{}, "get:List") + beego.Router("/backend/crm/bidding/tender-announce", &controllers.BackendCrmBiddingTenderAnnounceController{}, "post:Create") + beego.Router("/backend/crm/bidding/tender-announce/:id/publish", &controllers.BackendCrmBiddingTenderAnnounceController{}, "post:Publish") + beego.Router("/backend/crm/bidding/tender-announce/:id", &controllers.BackendCrmBiddingTenderAnnounceController{}, "delete:Delete") + + // 投标方/供应商 + beego.Router("/backend/crm/bidding/tender-supplier/list", &controllers.BackendCrmBiddingTenderSupplierController{}, "get:List") + beego.Router("/backend/crm/bidding/tender-supplier", &controllers.BackendCrmBiddingTenderSupplierController{}, "post:Create") + beego.Router("/backend/crm/bidding/tender-supplier/:id", &controllers.BackendCrmBiddingTenderSupplierController{}, "put:Update;delete:Delete") + + // 开标评标 + beego.Router("/backend/crm/bidding/tender-evaluation/list", &controllers.BackendCrmBiddingTenderEvaluationController{}, "get:List") + beego.Router("/backend/crm/bidding/tender-evaluation", &controllers.BackendCrmBiddingTenderEvaluationController{}, "post:Create") + beego.Router("/backend/crm/bidding/tender-evaluation/:id", &controllers.BackendCrmBiddingTenderEvaluationController{}, "delete:Delete") + + // 定标结果 + beego.Router("/backend/crm/bidding/tender-result/list", &controllers.BackendCrmBiddingTenderResultController{}, "get:List") + beego.Router("/backend/crm/bidding/tender-result/detail/:id", &controllers.BackendCrmBiddingTenderResultController{}, "get:Detail") + + // 投标立项审批 + beego.Router("/backend/crm/bidding/bid-approval/list", &controllers.BackendCrmBiddingBidApprovalController{}, "get:List") + beego.Router("/backend/crm/bidding/bid-approval", &controllers.BackendCrmBiddingBidApprovalController{}, "post:Create") + beego.Router("/backend/crm/bidding/bid-approval/:id/approve", &controllers.BackendCrmBiddingBidApprovalController{}, "post:Approve") + + // 标书版本 + beego.Router("/backend/crm/bidding/bid-document/list", &controllers.BackendCrmBiddingBidDocumentController{}, "get:List") + beego.Router("/backend/crm/bidding/bid-document", &controllers.BackendCrmBiddingBidDocumentController{}, "post:Create") + beego.Router("/backend/crm/bidding/bid-document/:id", &controllers.BackendCrmBiddingBidDocumentController{}, "put:Update;delete:Delete") + + // 标书模板库 + beego.Router("/backend/crm/bidding/template/list", &controllers.BackendCrmBiddingBidTemplateController{}, "get:List") + beego.Router("/backend/crm/bidding/template", &controllers.BackendCrmBiddingBidTemplateController{}, "post:Create") + beego.Router("/backend/crm/bidding/template/:id", &controllers.BackendCrmBiddingBidTemplateController{}, "put:Update;delete:Delete") + + // 资质文件库 + beego.Router("/backend/crm/bidding/qualification/list", &controllers.BackendCrmBiddingBidQualificationController{}, "get:List") + beego.Router("/backend/crm/bidding/qualification", &controllers.BackendCrmBiddingBidQualificationController{}, "post:Create") + beego.Router("/backend/crm/bidding/qualification/:id", &controllers.BackendCrmBiddingBidQualificationController{}, "put:Update;delete:Delete") + + // 招投标统计 + beego.Router("/backend/crm/bidding/statistics/tender", &controllers.BackendCrmBiddingStatisticsController{}, "get:Tender") + beego.Router("/backend/crm/bidding/statistics/bid", &controllers.BackendCrmBiddingStatisticsController{}, "get:Bid") + + // 招投标到期提醒(开发计划 4.1,实时计算,无定时任务) + beego.Router("/backend/crm/bidding/reminders", &controllers.BackendCrmBiddingReminderController{}, "get:List") + // ERP供应商管理 beego.Router("/backend/erp/supplier/list", &controllers.BackendErpSupplierController{}, "get:List") beego.Router("/backend/erp/supplier", &controllers.BackendErpSupplierController{}, "post:Create")