diff --git a/cmd/server/main.go b/cmd/server/main.go index b55b935..c9fed82 100644 --- a/cmd/server/main.go +++ b/cmd/server/main.go @@ -50,7 +50,7 @@ func main() { // Initialize handlers customerHandler := handlers.NewCustomerHandler(customerStorage, feishuWebhook) - followUpHandler := handlers.NewFollowUpHandler(followUpStorage, customerStorage, feishuWebhook) + followUpHandler := handlers.NewFollowUpHandler(followUpStorage, customerStorage, trialPeriodStorage, feishuWebhook) trialPeriodHandler := handlers.NewTrialPeriodHandler(trialPeriodStorage, customerStorage, feishuWebhook) authHandler := handlers.NewAuthHandler() diff --git a/frontend/css/style.css b/frontend/css/style.css index 6d6e4dc..6158ae8 100644 --- a/frontend/css/style.css +++ b/frontend/css/style.css @@ -1580,6 +1580,52 @@ td.overflow-cell { border-color: #ffd591; } +/* Deal Status Badges */ +.deal-badge { + display: inline-flex; + align-items: center; + gap: 4px; + padding: 3px 10px; + border-radius: 12px; + font-size: 0.75rem; + font-weight: 600; + white-space: nowrap; +} + +.deal-badge i { + font-size: 0.7rem; +} + +.deal-prospect { + background-color: #f0f0f0; + color: #595959; + border: 1px solid #d9d9d9; +} + +.deal-trial { + background-color: #e6f7ff; + color: #1890ff; + border: 1px solid #91d5ff; +} + +.deal-negotiation { + background-color: #fff7e6; + color: #fa8c16; + border: 1px solid #ffd591; +} + +.deal-closed-won { + background-color: #f6ffed; + color: #52c41a; + border: 1px solid #b7eb8f; +} + +.deal-lost { + background-color: #fff2f0; + color: #ff4d4f; + border: 1px solid #ffccc7; +} + .status-active { background-color: #d4edda; color: #155724; diff --git a/frontend/index.html b/frontend/index.html index 3ebe1fe..256f817 100644 --- a/frontend/index.html +++ b/frontend/index.html @@ -341,7 +341,8 @@ 客户名称 来源 意向产品 - 状态 + 跟进状态 + 试用状态 开始时间 结束时间 创建时间 @@ -409,7 +410,7 @@
- +

0

@@ -473,6 +474,25 @@
+
+
+
+

意向产品分布

+
+
+ +
+
+
+
+

跟进状态分布

+
+
+ +
+
+
+
@@ -1032,6 +1052,16 @@
+
+ + +
@@ -1113,6 +1143,16 @@
+
+ + +
@@ -1188,9 +1228,9 @@
- - - + + + \ No newline at end of file diff --git a/frontend/js/main.js b/frontend/js/main.js index 0936859..6911e34 100644 --- a/frontend/js/main.js +++ b/frontend/js/main.js @@ -101,6 +101,8 @@ document.addEventListener("DOMContentLoaded", function () { let statusChartInstance = null; let typeChartInstance = null; let trendChartInstance = null; + let productChartInstance = null; + let followupStatusChartInstance = null; // Current section tracking let currentSection = "dashboard"; @@ -1360,6 +1362,9 @@ document.addEventListener("DOMContentLoaded", function () { // Update total customers display (数据仪表盘的总客户数从每周进度获取) // This function will be called after loading dashboard customers updateTotalCustomersWithTrialData(trialCustomerNames); + + // Render trial period analysis charts + renderTrialAnalysisCharts(trialPeriods); } catch (error) { console.error("Error loading trial customers for dashboard:", error); } @@ -1416,23 +1421,41 @@ document.addEventListener("DOMContentLoaded", function () { if (data.followUps) { const followUpCount = data.followUps.length; document.getElementById("followUpCount").textContent = followUpCount; + } else { + document.getElementById("followUpCount").textContent = "0"; + } + } catch (error) { + console.error("Error loading follow-up count:", error); + document.getElementById("followUpCount").textContent = "0"; + } + // 已成交数量从客户信息表(trial_periods)统计 + await loadCompletedDealsCount(); + } + + // Load completed deals count from trial periods + async function loadCompletedDealsCount() { + try { + const response = await authenticatedFetch( + "/api/trial-periods/all", + ); + const data = await response.json(); + + if (data.trialPeriods) { // 计算已成交数量:统计 dealStatus 为"已成交"的唯一客户数 const completedCustomers = new Set( - data.followUps - .filter((f) => f.dealStatus === "已成交") - .map((f) => f.customerName) + data.trialPeriods + .filter((tp) => tp.dealStatus === "已成交") + .map((tp) => tp.customerName) .filter((name) => name), ).size; document.getElementById("completedTasks").textContent = completedCustomers; } else { - document.getElementById("followUpCount").textContent = "0"; document.getElementById("completedTasks").textContent = "0"; } } catch (error) { - console.error("Error loading follow-up count:", error); - document.getElementById("followUpCount").textContent = "0"; + console.error("Error loading completed deals count:", error); document.getElementById("completedTasks").textContent = "0"; } } @@ -1643,6 +1666,155 @@ document.addEventListener("DOMContentLoaded", function () { }); } + // Render trial analysis charts (Intended Product & Follow-up Status) + function renderTrialAnalysisCharts(trialPeriods) { + renderProductChart(trialPeriods); + renderFollowupStatusChart(trialPeriods); + } + + function renderProductChart(trialPeriods) { + const canvas = document.getElementById("productChart"); + if (!canvas) return; + const ctx = canvas.getContext("2d"); + + const productCount = {}; + trialPeriods.forEach((tp) => { + if (!tp.intendedProduct) return; + // Split by comma if multiple products selected + const products = tp.intendedProduct.split(",").map((p) => p.trim()); + products.forEach((p) => { + if (p) { + productCount[p] = (productCount[p] || 0) + 1; + } + }); + }); + + const labels = Object.keys(productCount); + const data = Object.values(productCount); + + if (productChartInstance) { + productChartInstance.destroy(); + } + + productChartInstance = new Chart(ctx, { + type: "bar", + data: { + labels: labels, + datasets: [ + { + label: "产品数量", + data: data, + backgroundColor: "#3b82f6", + borderRadius: 6, + maxBarThickness: 40, + }, + ], + }, + options: { + responsive: true, + plugins: { + legend: { display: false }, + tooltip: { + callbacks: { + label: (context) => `数量: ${context.parsed.y}`, + }, + }, + datalabels: { + anchor: "end", + align: "top", + color: "#666", + font: { weight: "bold" }, + formatter: (value) => value, + }, + }, + scales: { + y: { + beginAtZero: true, + ticks: { stepSize: 1 }, + }, + }, + }, + plugins: [ChartDataLabels], + }); + } + + function renderFollowupStatusChart(trialPeriods) { + const canvas = document.getElementById("followupStatusChart"); + if (!canvas) return; + const ctx = canvas.getContext("2d"); + + const statusCount = {}; + trialPeriods.forEach((tp) => { + let status = tp.dealStatus || "初步接触"; + + // 数据清洗:统一新旧名称 + if (status === "潜在客户") status = "初步接触"; + if (status === "试用中") status = "需求确认"; + if (status === "") status = "初步接触"; + + statusCount[status] = (statusCount[status] || 0) + 1; + }); + + const labels = Object.keys(statusCount); + const data = Object.values(statusCount); + + if (followupStatusChartInstance) { + followupStatusChartInstance.destroy(); + } + + const colors = { + "已成交": "#10b981", // Emerald 500 + "商务洽谈": "#f59e0b", // Amber 500 + "需求确认": "#6366f1", // Indigo 500 + "初步接触": "#94a3b8", // Slate 400 + "已流失": "#ef4444", // Red 500 + "未设置": "#cbd5e1", // Slate 300 + }; + + const backgroundColors = labels.map((l) => colors[l] || "#9E9E9E"); + + followupStatusChartInstance = new Chart(ctx, { + type: "doughnut", + data: { + labels: labels, + datasets: [ + { + data: data, + backgroundColor: backgroundColors, + }, + ], + }, + options: { + responsive: true, + plugins: { + legend: { + position: "right", + labels: { + generateLabels: (chart) => { + const data = chart.data; + return data.labels.map((label, i) => ({ + text: `${label}: ${data.datasets[0].data[i]}`, + fillStyle: data.datasets[0].backgroundColor[i], + index: i, + })); + }, + }, + }, + datalabels: { + formatter: (value, ctx) => { + const total = ctx.dataset.data.reduce((a, b) => a + b, 0); + const percentage = ((value / total) * 100).toFixed(1); + return `${value}\n(${percentage}%)`; + }, + color: "#fff", + font: { weight: "bold", size: 10 }, + }, + }, + }, + plugins: [ChartDataLabels], + }); + } + // Render trend line chart function renderTrendChart(customers) { const canvas = document.getElementById("trendChart"); diff --git a/frontend/js/trial-periods-page.js b/frontend/js/trial-periods-page.js index 83ad789..197b055 100644 --- a/frontend/js/trial-periods-page.js +++ b/frontend/js/trial-periods-page.js @@ -553,10 +553,26 @@ function renderTrialPeriodsTable() { return `${p}`; }).join(''); + // 生成跟进状态标签 + const dealStatus = period.dealStatus || '初步接触'; + let dealStatusBadge = ''; + if (dealStatus === '已成交') { + dealStatusBadge = ' 已成交'; + } else if (dealStatus === '需求确认') { + dealStatusBadge = ' 需求确认'; + } else if (dealStatus === '商务洽谈') { + dealStatusBadge = ' 商务洽谈'; + } else if (dealStatus === '已流失') { + dealStatusBadge = ' 已流失'; + } else { + dealStatusBadge = ' 初步接触'; + } + row.innerHTML = ` ${customerName} ${source} ${productBadges} + ${dealStatusBadge} ${statusCell} ${startTimeCell} ${endTimeCell} @@ -641,6 +657,10 @@ function openAddTrialModal() { // Reset intended product setIntendedProductValue('trialIntendedProduct', 'trialIntendedProductOther', ''); + // Reset deal status + const dealStatusEl = document.getElementById('trialDealStatus'); + if (dealStatusEl) dealStatusEl.value = '初步接触'; + document.querySelector('input[name="isTrial"][value="true"]').checked = true; document.getElementById('trialStartTime').value = ''; document.getElementById('trialEndTime').value = ''; @@ -661,6 +681,10 @@ function openEditTrialModal(periodId) { // Set intended product setIntendedProductValue('editTrialIntendedProduct', 'editTrialIntendedProductOther', period.intendedProduct || ''); + // Set deal status + const dealStatusEl = document.getElementById('editTrialDealStatus'); + if (dealStatusEl) dealStatusEl.value = period.dealStatus || '初步接触'; + const startDate = new Date(period.startTime); const endDate = new Date(period.endTime); @@ -740,6 +764,7 @@ async function createTrialPeriodFromPage() { const isTrial = isTrialValue === 'true'; const startTime = document.getElementById('trialStartTime').value; const endTime = document.getElementById('trialEndTime').value; + const dealStatus = document.getElementById('trialDealStatus').value; if (!customerName) { alert('请选择或输入客户名称'); @@ -758,6 +783,7 @@ async function createTrialPeriodFromPage() { customerName: customerName, source: source, intendedProduct: intendedProduct, + dealStatus: dealStatus, startTime: startTime ? new Date(startTime).toISOString() : '', endTime: endTime ? new Date(endTime).toISOString() : '', isTrial: isTrial @@ -777,11 +803,12 @@ async function createTrialPeriodFromPage() { document.getElementById('addTrialPeriodForm').reset(); await loadAllTrialPeriods(); } else { - alert('添加试用时间时出错'); + const errorText = await response.text(); + alert(errorText || '添加试用时间时出错'); } } catch (error) { console.error('Error creating trial period:', error); - alert('添加试用时间时出错'); + alert('网络错误,请稍后再试'); } } diff --git a/frontend/js/trial-periods.js b/frontend/js/trial-periods.js index f4cb869..6b6241b 100644 --- a/frontend/js/trial-periods.js +++ b/frontend/js/trial-periods.js @@ -187,13 +187,14 @@ async function createTrialPeriod() { if (response.ok) { document.getElementById('addTrialPeriodModal').style.display = 'none'; document.getElementById('addTrialPeriodForm').reset(); - await loadTrialPeriods(customerId); + loadTrialPeriods(currentCustomerId); // Reload trial periods for the current customer } else { - alert('添加试用时间时出错'); + const errorText = await response.text(); + alert(errorText || '创建试用期时出错'); } } catch (error) { console.error('Error creating trial period:', error); - alert('添加试用时间时出错'); + alert('网络错误,请稍后再试'); } } @@ -260,7 +261,13 @@ async function updateTrialPeriod() { const isTrialRadio = document.querySelector('input[name="editIsTrial"]:checked'); const isTrial = isTrialRadio ? isTrialRadio.value === 'true' : true; - if (!startTime || !endTime) { + // Get deal status value + const dealStatusEl = document.getElementById('editTrialDealStatus'); + const dealStatus = dealStatusEl ? dealStatusEl.value : '初步接触'; + + // Only require start/end time if "数据闭环" is selected + const requiresTimeFields = intendedProduct.includes('数据闭环'); + if (requiresTimeFields && (!startTime || !endTime)) { alert('请填写开始时间和结束时间'); return; } @@ -268,11 +275,18 @@ async function updateTrialPeriod() { const formData = { source: source, intendedProduct: intendedProduct, - startTime: new Date(startTime).toISOString(), - endTime: new Date(endTime).toISOString(), + dealStatus: dealStatus, isTrial: isTrial }; + // Only include time fields if they have values + if (startTime) { + formData.startTime = new Date(startTime).toISOString(); + } + if (endTime) { + formData.endTime = new Date(endTime).toISOString(); + } + try { const response = await authenticatedFetch(`/api/trial-periods/${periodId}`, { method: 'PUT', diff --git a/internal/handlers/followup_handler.go b/internal/handlers/followup_handler.go index acf1a8f..85a6c23 100644 --- a/internal/handlers/followup_handler.go +++ b/internal/handlers/followup_handler.go @@ -14,16 +14,18 @@ import ( ) type FollowUpHandler struct { - storage storage.FollowUpStorage - customerStorage storage.CustomerStorage - feishuWebhook string + storage storage.FollowUpStorage + customerStorage storage.CustomerStorage + trialPeriodStorage storage.TrialPeriodStorage + feishuWebhook string } -func NewFollowUpHandler(storage storage.FollowUpStorage, customerStorage storage.CustomerStorage, feishuWebhook string) *FollowUpHandler { +func NewFollowUpHandler(storage storage.FollowUpStorage, customerStorage storage.CustomerStorage, trialPeriodStorage storage.TrialPeriodStorage, feishuWebhook string) *FollowUpHandler { return &FollowUpHandler{ - storage: storage, - customerStorage: customerStorage, - feishuWebhook: feishuWebhook, + storage: storage, + customerStorage: customerStorage, + trialPeriodStorage: trialPeriodStorage, + feishuWebhook: feishuWebhook, } } @@ -134,6 +136,11 @@ func (h *FollowUpHandler) CreateFollowUp(w http.ResponseWriter, r *http.Request) return } + // 联动更新: 当跟进记录状态为"已成交"时,自动更新客户信息表的成交状态 + if req.DealStatus == "已成交" { + h.updateCustomerDealStatus(req.CustomerName, "已成交") + } + w.Header().Set("Content-Type", "application/json") w.WriteHeader(http.StatusCreated) json.NewEncoder(w).Encode(followUp) @@ -160,11 +167,32 @@ func (h *FollowUpHandler) UpdateFollowUp(w http.ResponseWriter, r *http.Request) return } + // 先获取原始跟进记录,用于获取客户名称 + existingFollowUp, err := h.storage.GetFollowUpByID(id) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + if existingFollowUp == nil { + http.Error(w, "Follow-up not found", http.StatusNotFound) + return + } + if err := h.storage.UpdateFollowUp(id, req); err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return } + // 联动更新: 当跟进记录状态更新为"已成交"时,自动更新客户信息表的成交状态 + if req.DealStatus != nil && *req.DealStatus == "已成交" { + // 优先使用请求中的客户名称,如果没有则使用原记录的客户名称 + customerName := existingFollowUp.CustomerName + if req.CustomerName != nil { + customerName = *req.CustomerName + } + h.updateCustomerDealStatus(customerName, "已成交") + } + w.WriteHeader(http.StatusOK) } @@ -303,3 +331,26 @@ func (h *FollowUpHandler) GetCustomerList(w http.ResponseWriter, r *http.Request "customerMap": customerMap, // Complete ID->Name mapping for display }) } + +// updateCustomerDealStatus 联动更新客户信息表的成交状态 +func (h *FollowUpHandler) updateCustomerDealStatus(customerName string, dealStatus string) { + if h.trialPeriodStorage == nil { + return + } + + // 获取所有试用记录,找到匹配客户名称的记录 + trialPeriods, err := h.trialPeriodStorage.GetAllTrialPeriods() + if err != nil { + return + } + + // 更新所有匹配客户名称的记录的成交状态 + for _, period := range trialPeriods { + if period.CustomerName == customerName { + updateReq := models.UpdateTrialPeriodRequest{ + DealStatus: &dealStatus, + } + h.trialPeriodStorage.UpdateTrialPeriod(period.ID, updateReq) + } + } +} diff --git a/internal/handlers/trial_period_handler.go b/internal/handlers/trial_period_handler.go index 0f94385..f208bf3 100644 --- a/internal/handlers/trial_period_handler.go +++ b/internal/handlers/trial_period_handler.go @@ -93,12 +93,20 @@ func (h *TrialPeriodHandler) CreateTrialPeriod(w http.ResponseWriter, r *http.Re CustomerName: req.CustomerName, Source: req.Source, IntendedProduct: req.IntendedProduct, + DealStatus: req.DealStatus, StartTime: startTime, EndTime: endTime, IsTrial: req.IsTrial, CreatedAt: time.Now(), } + // Check if customer name already exists + existing, err := h.storage.GetTrialPeriodsByCustomerID(req.CustomerName) + if err == nil && len(existing) > 0 { + http.Error(w, fmt.Sprintf("客户名称 '%s' 已存在,请勿重复添加", req.CustomerName), http.StatusConflict) + return + } + createdPeriod, err := h.storage.CreateTrialPeriod(trialPeriod) if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) diff --git a/internal/storage/db.go b/internal/storage/db.go index e9fa622..0fccf76 100644 --- a/internal/storage/db.go +++ b/internal/storage/db.go @@ -147,6 +147,31 @@ func autoMigrate() error { log.Printf("✅ Modified screenshots column type from %s to LONGTEXT\n", columnType.String) } + // 检查并添加 deal_status 列到 trial_periods 表 + var dealStatusColumn sql.NullString + err = db.QueryRow(` + SELECT COLUMN_NAME + FROM INFORMATION_SCHEMA.COLUMNS + WHERE TABLE_SCHEMA = DATABASE() + AND TABLE_NAME = 'trial_periods' + AND COLUMN_NAME = 'deal_status' + `).Scan(&dealStatusColumn) + + if err == sql.ErrNoRows { + // deal_status 列不存在,添加新列 + _, err = db.Exec("ALTER TABLE trial_periods ADD COLUMN deal_status VARCHAR(50) DEFAULT '初步接触'") + if err != nil { + return fmt.Errorf("failed to add deal_status column: %v", err) + } + log.Println("✅ Added deal_status column to trial_periods table") + } else if err != nil { + return fmt.Errorf("failed to check deal_status column: %v", err) + } + + // 数据清理:将旧的状态名称统一更新为新名称 + _, _ = db.Exec("UPDATE trial_periods SET deal_status = '初步接触' WHERE deal_status = '潜在客户' OR deal_status = '' OR deal_status IS NULL") + _, _ = db.Exec("UPDATE trial_periods SET deal_status = '需求确认' WHERE deal_status = '试用中'") + log.Println("✅ Database tables migrated successfully") return nil } diff --git a/internal/storage/mysql_trial_period_storage.go b/internal/storage/mysql_trial_period_storage.go index 8949219..1f28075 100644 --- a/internal/storage/mysql_trial_period_storage.go +++ b/internal/storage/mysql_trial_period_storage.go @@ -23,7 +23,8 @@ func NewMySQLTrialPeriodStorage() TrialPeriodStorage { func (ts *mysqlTrialPeriodStorage) GetAllTrialPeriods() ([]models.TrialPeriod, error) { query := ` - SELECT id, customer_name, COALESCE(source, '') as source, COALESCE(intended_product, '') as intended_product, start_time, end_time, is_trial, created_at + SELECT id, customer_name, COALESCE(source, '') as source, COALESCE(intended_product, '') as intended_product, + COALESCE(deal_status, '') as deal_status, start_time, end_time, is_trial, created_at FROM trial_periods ORDER BY created_at DESC ` @@ -40,8 +41,8 @@ func (ts *mysqlTrialPeriodStorage) GetAllTrialPeriods() ([]models.TrialPeriod, e var isTrial int err := rows.Scan( - &tp.ID, &tp.CustomerName, &tp.Source, &tp.IntendedProduct, &tp.StartTime, - &tp.EndTime, &isTrial, &tp.CreatedAt, + &tp.ID, &tp.CustomerName, &tp.Source, &tp.IntendedProduct, &tp.DealStatus, + &tp.StartTime, &tp.EndTime, &isTrial, &tp.CreatedAt, ) if err != nil { return nil, err @@ -56,7 +57,8 @@ func (ts *mysqlTrialPeriodStorage) GetAllTrialPeriods() ([]models.TrialPeriod, e func (ts *mysqlTrialPeriodStorage) GetTrialPeriodsByCustomerID(customerID string) ([]models.TrialPeriod, error) { query := ` - SELECT id, customer_name, COALESCE(source, '') as source, COALESCE(intended_product, '') as intended_product, start_time, end_time, is_trial, created_at + SELECT id, customer_name, COALESCE(source, '') as source, COALESCE(intended_product, '') as intended_product, + COALESCE(deal_status, '') as deal_status, start_time, end_time, is_trial, created_at FROM trial_periods WHERE customer_name = ? ORDER BY end_time DESC @@ -74,8 +76,8 @@ func (ts *mysqlTrialPeriodStorage) GetTrialPeriodsByCustomerID(customerID string var isTrial int err := rows.Scan( - &tp.ID, &tp.CustomerName, &tp.Source, &tp.IntendedProduct, &tp.StartTime, - &tp.EndTime, &isTrial, &tp.CreatedAt, + &tp.ID, &tp.CustomerName, &tp.Source, &tp.IntendedProduct, &tp.DealStatus, + &tp.StartTime, &tp.EndTime, &isTrial, &tp.CreatedAt, ) if err != nil { return nil, err @@ -90,7 +92,8 @@ func (ts *mysqlTrialPeriodStorage) GetTrialPeriodsByCustomerID(customerID string func (ts *mysqlTrialPeriodStorage) GetTrialPeriodByID(id string) (*models.TrialPeriod, error) { query := ` - SELECT id, customer_name, COALESCE(source, '') as source, COALESCE(intended_product, '') as intended_product, start_time, end_time, is_trial, created_at + SELECT id, customer_name, COALESCE(source, '') as source, COALESCE(intended_product, '') as intended_product, + COALESCE(deal_status, '') as deal_status, start_time, end_time, is_trial, created_at FROM trial_periods WHERE id = ? ` @@ -99,8 +102,8 @@ func (ts *mysqlTrialPeriodStorage) GetTrialPeriodByID(id string) (*models.TrialP var isTrial int err := ts.db.QueryRow(query, id).Scan( - &tp.ID, &tp.CustomerName, &tp.Source, &tp.IntendedProduct, &tp.StartTime, - &tp.EndTime, &isTrial, &tp.CreatedAt, + &tp.ID, &tp.CustomerName, &tp.Source, &tp.IntendedProduct, &tp.DealStatus, + &tp.StartTime, &tp.EndTime, &isTrial, &tp.CreatedAt, ) if err == sql.ErrNoRows { @@ -123,8 +126,8 @@ func (ts *mysqlTrialPeriodStorage) CreateTrialPeriod(trialPeriod models.TrialPer } query := ` - INSERT INTO trial_periods (id, customer_name, source, intended_product, start_time, end_time, is_trial, created_at) - VALUES (?, ?, ?, ?, ?, ?, ?, ?) + INSERT INTO trial_periods (id, customer_name, source, intended_product, deal_status, start_time, end_time, is_trial, created_at) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?) ` isTrial := 0 @@ -134,7 +137,7 @@ func (ts *mysqlTrialPeriodStorage) CreateTrialPeriod(trialPeriod models.TrialPer _, err := ts.db.Exec(query, trialPeriod.ID, trialPeriod.CustomerName, trialPeriod.Source, trialPeriod.IntendedProduct, - trialPeriod.StartTime, trialPeriod.EndTime, isTrial, trialPeriod.CreatedAt, + trialPeriod.DealStatus, trialPeriod.StartTime, trialPeriod.EndTime, isTrial, trialPeriod.CreatedAt, ) if err != nil { @@ -161,6 +164,9 @@ func (ts *mysqlTrialPeriodStorage) UpdateTrialPeriod(id string, updates models.U if updates.IntendedProduct != nil { existing.IntendedProduct = *updates.IntendedProduct } + if updates.DealStatus != nil { + existing.DealStatus = *updates.DealStatus + } if updates.StartTime != nil { startTime, err := time.Parse(time.RFC3339, *updates.StartTime) if err == nil { @@ -179,7 +185,7 @@ func (ts *mysqlTrialPeriodStorage) UpdateTrialPeriod(id string, updates models.U query := ` UPDATE trial_periods - SET customer_name = ?, source = ?, intended_product = ?, start_time = ?, end_time = ?, is_trial = ? + SET customer_name = ?, source = ?, intended_product = ?, deal_status = ?, start_time = ?, end_time = ?, is_trial = ? WHERE id = ? ` @@ -189,7 +195,7 @@ func (ts *mysqlTrialPeriodStorage) UpdateTrialPeriod(id string, updates models.U } _, err = ts.db.Exec(query, - existing.CustomerName, existing.Source, existing.IntendedProduct, + existing.CustomerName, existing.Source, existing.IntendedProduct, existing.DealStatus, existing.StartTime, existing.EndTime, isTrial, id, ) diff --git a/internal/storage/trial_period_storage.go b/internal/storage/trial_period_storage.go index 92419c5..cc78a7d 100644 --- a/internal/storage/trial_period_storage.go +++ b/internal/storage/trial_period_storage.go @@ -139,6 +139,12 @@ func (ts *trialPeriodStorage) UpdateTrialPeriod(id string, updates models.Update if updates.Source != nil { trialPeriods[i].Source = *updates.Source } + if updates.IntendedProduct != nil { + trialPeriods[i].IntendedProduct = *updates.IntendedProduct + } + if updates.DealStatus != nil { + trialPeriods[i].DealStatus = *updates.DealStatus + } if updates.StartTime != nil { startTime, err := time.Parse(time.RFC3339, *updates.StartTime) if err == nil { diff --git a/models/trial_period.go b/models/trial_period.go index 8661802..fbbbe58 100644 --- a/models/trial_period.go +++ b/models/trial_period.go @@ -8,6 +8,7 @@ type TrialPeriod struct { CustomerName string `json:"customerName"` // 直接存储客户名称 Source string `json:"source"` // 客户来源 IntendedProduct string `json:"intendedProduct"` // 意向产品 + DealStatus string `json:"dealStatus"` // 成交状态 StartTime time.Time `json:"startTime"` EndTime time.Time `json:"endTime"` IsTrial bool `json:"isTrial"` @@ -19,6 +20,7 @@ type CreateTrialPeriodRequest struct { CustomerName string `json:"customerName"` // 直接使用客户名称 Source string `json:"source"` // 客户来源 IntendedProduct string `json:"intendedProduct"` // 意向产品 + DealStatus string `json:"dealStatus"` // 成交状态 StartTime string `json:"startTime"` EndTime string `json:"endTime"` IsTrial bool `json:"isTrial"` @@ -29,6 +31,7 @@ type UpdateTrialPeriodRequest struct { CustomerName *string `json:"customerName,omitempty"` Source *string `json:"source,omitempty"` // 客户来源 IntendedProduct *string `json:"intendedProduct,omitempty"` // 意向产品 + DealStatus *string `json:"dealStatus,omitempty"` // 成交状态 StartTime *string `json:"startTime,omitempty"` EndTime *string `json:"endTime,omitempty"` IsTrial *bool `json:"isTrial,omitempty"`