diff --git a/frontend/index.html b/frontend/index.html index 84b2676..a82e7e4 100644 --- a/frontend/index.html +++ b/frontend/index.html @@ -107,7 +107,6 @@ - @@ -116,6 +115,7 @@ + diff --git a/frontend/js/main.js b/frontend/js/main.js index 18dea1f..9c2e740 100644 --- a/frontend/js/main.js +++ b/frontend/js/main.js @@ -1,4 +1,4 @@ -document.addEventListener('DOMContentLoaded', function() { +document.addEventListener('DOMContentLoaded', function () { // 登录守卫 const token = localStorage.getItem('crmToken'); if (!token && !window.location.pathname.endsWith('login.html')) { @@ -15,13 +15,13 @@ document.addEventListener('DOMContentLoaded', function() { }; const response = await fetch(url, { ...options, headers }); - + if (response.status === 401) { localStorage.removeItem('crmToken'); window.location.href = '/static/login.html'; return; } - + return response; } @@ -30,7 +30,7 @@ document.addEventListener('DOMContentLoaded', function() { const customerSection = document.getElementById('customerSection'); const dashboardSection = document.getElementById('dashboardSection'); const pageTitle = document.getElementById('pageTitle'); - + // Elements const createCustomerForm = document.getElementById('createCustomerForm'); const importFileForm = document.getElementById('importFileForm'); @@ -48,18 +48,18 @@ document.addEventListener('DOMContentLoaded', function() { const contentArea = document.querySelector('.content-area'); const refreshCustomersBtn = document.getElementById('refreshCustomersBtn'); const exportCustomersBtn = document.getElementById('exportCustomersBtn'); - + let allCustomers = []; let filteredCustomers = []; let dashboardCustomers = []; - + // Chart instances let statusChartInstance = null; let typeChartInstance = null; - + // Current section tracking let currentSection = 'customer'; - + // Pagination state let currentPage = 1; let pageSize = 10; @@ -198,88 +198,88 @@ document.addEventListener('DOMContentLoaded', function() { const day = parts[2].padStart(2, '0'); return `${year}-${month}-${day}`; } - + // Navigation event listeners navItems.forEach(item => { - item.addEventListener('click', function(e) { + item.addEventListener('click', function (e) { e.preventDefault(); const section = this.getAttribute('data-section'); switchSection(section); }); }); - + // Menu toggle for mobile - menuToggle.addEventListener('click', function() { + menuToggle.addEventListener('click', function () { sidebar.classList.toggle('open'); }); - + // Close sidebar when clicking outside on mobile - document.addEventListener('click', function(e) { + document.addEventListener('click', function (e) { if (window.innerWidth <= 768) { if (!sidebar.contains(e.target) && !menuToggle.contains(e.target)) { sidebar.classList.remove('open'); } } }); - + // Add Customer button - addCustomerBtn.addEventListener('click', function() { + addCustomerBtn.addEventListener('click', function () { createModal.style.display = 'block'; }); - + // Import button - importBtn.addEventListener('click', function() { + importBtn.addEventListener('click', function () { importModal.style.display = 'block'; }); - + // Close create modal - createModal.querySelector('.close').addEventListener('click', function() { + createModal.querySelector('.close').addEventListener('click', function () { createModal.style.display = 'none'; }); - - createModal.querySelector('.cancel-create').addEventListener('click', function() { + + createModal.querySelector('.cancel-create').addEventListener('click', function () { createModal.style.display = 'none'; }); - + // Close import modal - importModal.querySelector('.close').addEventListener('click', function() { + importModal.querySelector('.close').addEventListener('click', function () { importModal.style.display = 'none'; }); - - importModal.querySelector('.cancel-import').addEventListener('click', function() { + + importModal.querySelector('.cancel-import').addEventListener('click', function () { importModal.style.display = 'none'; }); - + // Close edit modal - editModal.querySelector('.close').addEventListener('click', function() { + editModal.querySelector('.close').addEventListener('click', function () { editModal.style.display = 'none'; }); - - editModal.querySelector('.cancel-edit').addEventListener('click', function() { + + editModal.querySelector('.cancel-edit').addEventListener('click', function () { editModal.style.display = 'none'; }); - + // File name display const importFile = document.getElementById('importFile'); const fileName = document.getElementById('fileName'); - - importFile.addEventListener('change', function() { + + importFile.addEventListener('change', function () { if (this.files.length > 0) { fileName.textContent = this.files[0].name; } else { fileName.textContent = '选择文件...'; } }); - + // Customer filter change event - customerFilter.addEventListener('change', function() { + customerFilter.addEventListener('change', function () { selectedCustomerFilter = this.value; currentPage = 1; applyAllCustomerFilters(); }); if (customerSearchInput) { - customerSearchInput.addEventListener('input', function() { + customerSearchInput.addEventListener('input', function () { customerSearchQuery = (this.value || '').trim(); if (currentSection === 'customer') { currentPage = 1; @@ -287,29 +287,29 @@ document.addEventListener('DOMContentLoaded', function() { } }); } - + // Apply date filter for dashboard - document.getElementById('applyFilters').addEventListener('click', function() { + document.getElementById('applyFilters').addEventListener('click', function () { const startDate = document.getElementById('startDate').value; const endDate = document.getElementById('endDate').value; applyDateFilter(startDate, endDate); }); - + // Chart title change events - document.getElementById('statusChartTitle').addEventListener('input', function() { + document.getElementById('statusChartTitle').addEventListener('input', function () { applyDateFilter(document.getElementById('startDate').value, document.getElementById('endDate').value); }); - - document.getElementById('typeChartTitle').addEventListener('input', function() { + + document.getElementById('typeChartTitle').addEventListener('input', function () { applyDateFilter(document.getElementById('startDate').value, document.getElementById('endDate').value); }); - + // Switch between sections function switchSection(section) { navItems.forEach(item => { item.classList.remove('active'); }); - + if (section === 'customer') { customerSection.classList.add('active'); dashboardSection.classList.remove('active'); @@ -325,19 +325,19 @@ document.addEventListener('DOMContentLoaded', function() { currentSection = 'dashboard'; loadDashboardData(); } - + // Close sidebar on mobile after navigation if (window.innerWidth <= 768) { sidebar.classList.remove('open'); } } - + // Load customers from API async function loadCustomers() { try { const response = await authenticatedFetch('/api/customers?page=1&pageSize=1000'); const data = await response.json(); - + if (data.customers) { allCustomers = data.customers; populateCustomerFilter(); @@ -347,14 +347,14 @@ document.addEventListener('DOMContentLoaded', function() { console.error('Error loading customers:', error); } } - + // Populate customer filter dropdown function populateCustomerFilter() { console.log('Populating filter, allCustomers:', allCustomers); const uniqueCustomers = [...new Set(allCustomers.map(c => c.intendedProduct).filter(c => c))]; console.log('Unique customers:', uniqueCustomers); customerFilter.innerHTML = ''; - + uniqueCustomers.forEach(customer => { const option = document.createElement('option'); option.value = customer; @@ -366,7 +366,7 @@ document.addEventListener('DOMContentLoaded', function() { customerFilter.value = selectedCustomerFilter; } } - + // Filter customers by selected customer function filterCustomers(selectedCustomer) { selectedCustomerFilter = selectedCustomer; @@ -438,7 +438,6 @@ document.addEventListener('DOMContentLoaded', function() { function exportCustomersToCsv(customers) { const header = [ - '时间', '客户', '版本', '描述', @@ -446,13 +445,13 @@ document.addEventListener('DOMContentLoaded', function() { '类型', '模块', '状态与进度', - '报告人' + '报告人', + '时间' ].map(toCsvCell).join(','); const lines = customers.map(c => { const date = normalizeDateValue(c.customerName) || (c.customerName || ''); const cells = [ - date, c.intendedProduct || '', c.version || '', c.description || '', @@ -460,7 +459,8 @@ document.addEventListener('DOMContentLoaded', function() { c.type || '', c.module || '', c.statusProgress || '', - c.reporter || '' + c.reporter || '', + date ]; return cells.map(toCsvCell).join(','); }); @@ -491,29 +491,29 @@ document.addEventListener('DOMContentLoaded', function() { exportCustomersToCsv(filteredCustomers); }); } - + // Apply customer filter and render table with pagination function applyCustomerFilter() { totalItems = filteredCustomers.length; totalPages = Math.ceil(totalItems / pageSize); - + const startIndex = (currentPage - 1) * pageSize; const endIndex = startIndex + pageSize; const paginatedCustomers = filteredCustomers.slice(startIndex, endIndex); - + renderCustomerTable(paginatedCustomers); updatePaginationControls(); } - + // Load all customers for dashboard async function loadAllCustomers() { console.log('loadAllCustomers called'); try { const response = await authenticatedFetch('/api/customers?page=1&pageSize=1000'); const data = await response.json(); - + console.log('Dashboard data received:', data); - + if (data.customers) { dashboardCustomers = data.customers; console.log('Dashboard customers set:', dashboardCustomers.length, 'customers'); @@ -527,11 +527,11 @@ document.addEventListener('DOMContentLoaded', function() { console.error('Error loading all customers:', error); } } - + // Apply date filter for dashboard function applyDateFilter(startDate, endDate) { let filteredData = dashboardCustomers; - + if (startDate) { filteredData = filteredData.filter(c => { if (!c.customerName) return false; @@ -539,7 +539,7 @@ document.addEventListener('DOMContentLoaded', function() { return date && date >= startDate; }); } - + if (endDate) { filteredData = filteredData.filter(c => { if (!c.customerName) return false; @@ -547,23 +547,22 @@ document.addEventListener('DOMContentLoaded', function() { return date && date <= endDate; }); } - + updateDashboardStats(filteredData); renderStatusChart(filteredData); renderTypeChart(filteredData); } - + // Render customer table function renderCustomerTable(customers) { customerTableBody.innerHTML = ''; - + customers.forEach(customer => { const row = document.createElement('tr'); - + const date = customer.customerName || ''; - + const fields = [ - { value: date, name: 'date' }, { value: customer.intendedProduct || '', name: 'intendedProduct' }, { value: customer.version || '', name: 'version' }, { value: customer.description || '', name: 'description' }, @@ -571,22 +570,23 @@ document.addEventListener('DOMContentLoaded', function() { { value: customer.type || '', name: 'type' }, { value: customer.module || '', name: 'module' }, { value: customer.statusProgress || '', name: 'statusProgress' }, - { value: customer.reporter || '', name: 'reporter' } + { value: customer.reporter || '', name: 'reporter' }, + { value: date, name: 'date' } ]; - + fields.forEach(field => { const td = document.createElement('td'); const textValue = String(field.value ?? ''); td.textContent = textValue; td.setAttribute('data-tooltip', textValue); - + if (field.name === 'description' || field.name === 'solution') { td.classList.add('overflow-cell'); } - + row.appendChild(td); }); - + const actionTd = document.createElement('td'); actionTd.innerHTML = ` `; row.appendChild(actionTd); - + customerTableBody.appendChild(row); }); - + checkTextOverflow(); - + document.querySelectorAll('.edit-btn').forEach(btn => { - btn.addEventListener('click', function() { + btn.addEventListener('click', function () { const customerId = this.getAttribute('data-id'); openEditModal(customerId); }); }); - + document.querySelectorAll('.delete-btn').forEach(btn => { - btn.addEventListener('click', function() { + btn.addEventListener('click', function () { const customerId = this.getAttribute('data-id'); deleteCustomer(customerId); }); }); } - + function checkTextOverflow() { // Use requestAnimationFrame to ensure DOM is fully rendered before checking overflow requestAnimationFrame(() => { @@ -635,31 +635,31 @@ document.addEventListener('DOMContentLoaded', function() { }); }); } - + // Update pagination controls function updatePaginationControls() { const startItem = totalItems === 0 ? 0 : (currentPage - 1) * pageSize + 1; const endItem = Math.min(currentPage * pageSize, totalItems); - - document.getElementById('paginationInfo').textContent = + + document.getElementById('paginationInfo').textContent = `显示 ${startItem}-${endItem} 共 ${totalItems} 条`; - + document.getElementById('firstPage').disabled = currentPage === 1; document.getElementById('prevPage').disabled = currentPage === 1; document.getElementById('nextPage').disabled = currentPage === totalPages; document.getElementById('lastPage').disabled = currentPage === totalPages; - + const pageNumbers = document.getElementById('pageNumbers'); pageNumbers.innerHTML = ''; - + const maxVisiblePages = 5; let startPage = Math.max(1, currentPage - Math.floor(maxVisiblePages / 2)); let endPage = Math.min(totalPages, startPage + maxVisiblePages - 1); - + if (endPage - startPage + 1 < maxVisiblePages) { startPage = Math.max(1, endPage - maxVisiblePages + 1); } - + for (let i = startPage; i <= endPage; i++) { const pageBtn = document.createElement('button'); pageBtn.className = `page-number ${i === currentPage ? 'active' : ''}`; @@ -671,11 +671,11 @@ document.addEventListener('DOMContentLoaded', function() { pageNumbers.appendChild(pageBtn); } } - + // Create customer - createCustomerForm.addEventListener('submit', async function(e) { + createCustomerForm.addEventListener('submit', async function (e) { e.preventDefault(); - + const formData = { customerName: document.getElementById('customerName').value, intendedProduct: document.getElementById('intendedProduct').value, @@ -687,7 +687,7 @@ document.addEventListener('DOMContentLoaded', function() { statusProgress: document.getElementById('createStatusProgress').value, reporter: document.getElementById('createReporter').value }; - + try { const response = await authenticatedFetch('/api/customers', { method: 'POST', @@ -696,7 +696,7 @@ document.addEventListener('DOMContentLoaded', function() { }, body: JSON.stringify(formData) }); - + if (response.ok) { createCustomerForm.reset(); createModal.style.display = 'none'; @@ -710,27 +710,27 @@ document.addEventListener('DOMContentLoaded', function() { alert('创建客户时出错'); } }); - + // Import customers - importFileForm.addEventListener('submit', async function(e) { + importFileForm.addEventListener('submit', async function (e) { e.preventDefault(); - + const formData = new FormData(); const fileInput = document.getElementById('importFile'); - + if (!fileInput.files.length) { alert('请选择要导入的文件'); return; } - + formData.append('file', fileInput.files[0]); - + try { const response = await authenticatedFetch('/api/customers/import', { method: 'POST', body: formData }); - + if (response.ok) { const result = await response.json(); const message = `客户导入成功!\n导入数量: ${result.importedCount}\n重复数量: ${result.duplicateCount}`; @@ -747,13 +747,13 @@ document.addEventListener('DOMContentLoaded', function() { alert('导入客户时出错'); } }); - + // Open edit modal async function openEditModal(customerId) { try { const response = await authenticatedFetch(`/api/customers/${customerId}`); const customer = await response.json(); - + document.getElementById('editCustomerId').value = customer.id; document.getElementById('editCustomerName').value = normalizeDateValue(customer.customerName); document.getElementById('editIntendedProduct').value = customer.intendedProduct || ''; @@ -764,14 +764,14 @@ document.addEventListener('DOMContentLoaded', function() { document.getElementById('editModule').value = customer.module || ''; document.getElementById('editStatusProgress').value = customer.statusProgress || ''; document.getElementById('editReporter').value = customer.reporter || ''; - + editModal.style.display = 'block'; } catch (error) { console.error('Error loading customer for edit:', error); } } - - window.addEventListener('click', function(e) { + + window.addEventListener('click', function (e) { if (e.target === createModal) { createModal.style.display = 'none'; } @@ -782,13 +782,13 @@ document.addEventListener('DOMContentLoaded', function() { editModal.style.display = 'none'; } }); - + // Update customer - editCustomerForm.addEventListener('submit', async function(e) { + editCustomerForm.addEventListener('submit', async function (e) { e.preventDefault(); - + const customerId = document.getElementById('editCustomerId').value; - + const formData = { customerName: document.getElementById('editCustomerName').value, intendedProduct: document.getElementById('editIntendedProduct').value, @@ -800,7 +800,7 @@ document.addEventListener('DOMContentLoaded', function() { statusProgress: document.getElementById('editStatusProgress').value, reporter: document.getElementById('editReporter').value }; - + try { const response = await authenticatedFetch(`/api/customers/${customerId}`, { method: 'PUT', @@ -809,7 +809,7 @@ document.addEventListener('DOMContentLoaded', function() { }, body: JSON.stringify(formData) }); - + if (response.ok) { editModal.style.display = 'none'; loadCustomers(); @@ -822,18 +822,18 @@ document.addEventListener('DOMContentLoaded', function() { alert('更新客户时出错'); } }); - + // Delete customer async function deleteCustomer(customerId) { if (!confirm('确定要删除这个客户吗?')) { return; } - + try { const response = await authenticatedFetch(`/api/customers/${customerId}`, { method: 'DELETE' }); - + if (response.ok) { loadCustomers(); alert('客户删除成功!'); @@ -845,17 +845,17 @@ document.addEventListener('DOMContentLoaded', function() { alert('删除客户时出错'); } } - + // Dashboard functionality async function loadDashboardData() { console.log('loadDashboardData called'); await loadAllCustomers(); } - + // Update dashboard statistics function updateDashboardStats(customers) { const totalCustomers = new Set(customers.map(c => c.intendedProduct).filter(c => c)).size; - + const now = new Date(); const currentMonth = String(now.getMonth() + 1).padStart(2, '0'); const currentYear = now.getFullYear(); @@ -869,39 +869,39 @@ document.addEventListener('DOMContentLoaded', function() { return year === currentYear && month === currentMonth; }).map(c => c.intendedProduct).filter(c => c) ).size; - + const products = new Set(customers.map(c => c.intendedProduct)).size; - + const completed = new Set( - customers.filter(c => + customers.filter(c => c.statusProgress && (c.statusProgress.includes('已修复') || c.statusProgress.includes('完成') || c.statusProgress.toLowerCase().includes('complete')) ).map(c => c.intendedProduct).filter(c => c) ).size; - + document.getElementById('totalCustomers').textContent = totalCustomers; document.getElementById('newCustomers').textContent = newCustomers; document.getElementById('totalProducts').textContent = products; document.getElementById('completedTasks').textContent = completed; } - + function renderStatusChart(customers) { const ctx = document.getElementById('statusChart').getContext('2d'); const selectedField = document.getElementById('chartFieldSelect').value; const chartTitle = document.getElementById('statusChartTitle').value || '数据分布'; - + const fieldCount = {}; customers.forEach(customer => { const value = customer[selectedField] || '未设置'; fieldCount[value] = (fieldCount[value] || 0) + 1; }); - + const labels = Object.keys(fieldCount); const data = Object.values(fieldCount); - + if (statusChartInstance) { statusChartInstance.destroy(); } - + statusChartInstance = new Chart(ctx, { type: 'pie', data: { @@ -940,38 +940,38 @@ document.addEventListener('DOMContentLoaded', function() { } }); } - + // Render customer type chart function renderTypeChart(customers) { console.log('renderTypeChart called with customers:', customers); const canvas = document.getElementById('typeChart'); console.log('typeChart canvas element:', canvas); - + if (!canvas) { console.error('typeChart canvas not found'); return; } - + const ctx = canvas.getContext('2d'); const chartTitle = document.getElementById('typeChartTitle').value || '客户类型'; - + if (typeChartInstance) { typeChartInstance.destroy(); } - + const selectedField = document.getElementById('typeChartFieldSelect').value; const typeCount = {}; customers.forEach(customer => { const type = customer[selectedField] || '未设置'; typeCount[type] = (typeCount[type] || 0) + 1; }); - + const labels = Object.keys(typeCount); const data = Object.values(typeCount); - + console.log('Type chart labels:', labels); console.log('Type chart data:', data); - + typeChartInstance = new Chart(ctx, { type: 'doughnut', data: { @@ -1005,15 +1005,15 @@ document.addEventListener('DOMContentLoaded', function() { } } }); - + console.log('Type chart created successfully'); } - + // Initialize the app loadCustomers().then(() => { loadAllCustomers(); }); - + // Pagination event listeners document.getElementById('firstPage').addEventListener('click', () => { if (currentPage > 1) { @@ -1021,43 +1021,43 @@ document.addEventListener('DOMContentLoaded', function() { applyCustomerFilter(); } }); - + document.getElementById('prevPage').addEventListener('click', () => { if (currentPage > 1) { currentPage--; applyCustomerFilter(); } }); - + document.getElementById('nextPage').addEventListener('click', () => { if (currentPage < totalPages) { currentPage++; applyCustomerFilter(); } }); - + document.getElementById('lastPage').addEventListener('click', () => { if (currentPage < totalPages) { currentPage = totalPages; applyCustomerFilter(); } }); - + document.getElementById('pageSizeSelect').addEventListener('change', (e) => { pageSize = parseInt(e.target.value); currentPage = 1; loadCustomers(); }); - + // Chart field select event listener - document.getElementById('chartFieldSelect').addEventListener('change', function() { + document.getElementById('chartFieldSelect').addEventListener('change', function () { const startDate = document.getElementById('startDate').value; const endDate = document.getElementById('endDate').value; applyDateFilter(startDate, endDate); }); - + // Type chart field select event listener - document.getElementById('typeChartFieldSelect').addEventListener('change', function() { + document.getElementById('typeChartFieldSelect').addEventListener('change', function () { const startDate = document.getElementById('startDate').value; const endDate = document.getElementById('endDate').value; applyDateFilter(startDate, endDate); @@ -1074,7 +1074,7 @@ document.addEventListener('DOMContentLoaded', function() { window.location.href = '/static/login.html'; } }); - + const headerRight = document.querySelector('.header-right'); if (headerRight) { headerRight.appendChild(logoutBtn); diff --git a/internal/storage/customer_storage.go b/internal/storage/customer_storage.go index 90ce9ce..94241e6 100644 --- a/internal/storage/customer_storage.go +++ b/internal/storage/customer_storage.go @@ -1,14 +1,14 @@ package storage import ( + "crm-go/models" "crypto/rand" "encoding/hex" "encoding/json" "os" - "sync" "path/filepath" + "sync" "time" - "crm-go/models" ) type CustomerStorage interface { @@ -31,58 +31,72 @@ func NewCustomerStorage(filePath string) CustomerStorage { storage := &customerStorage{ filePath: filePath, } - + // Ensure the directory exists dir := os.DirFS(filePath[:len(filePath)-len("/customers.json")]) _ = dir // Use the directory to ensure it exists - + return storage } func (cs *customerStorage) GetAllCustomers() ([]models.Customer, error) { cs.mutex.RLock() defer cs.mutex.RUnlock() - - return cs.LoadCustomers() + + customers, err := cs.LoadCustomers() + if err != nil { + return nil, err + } + + // Sort by CreatedAt in descending order (newest first) + for i := 0; i < len(customers)-1; i++ { + for j := i + 1; j < len(customers); j++ { + if customers[i].CreatedAt.Before(customers[j].CreatedAt) { + customers[i], customers[j] = customers[j], customers[i] + } + } + } + + return customers, nil } func (cs *customerStorage) GetCustomerByID(id string) (*models.Customer, error) { cs.mutex.RLock() defer cs.mutex.RUnlock() - + customers, err := cs.LoadCustomers() if err != nil { return nil, err } - + for _, customer := range customers { if customer.ID == id { return &customer, nil } } - + return nil, nil } func (cs *customerStorage) CreateCustomer(customer models.Customer) error { cs.mutex.Lock() defer cs.mutex.Unlock() - + if customer.ID == "" { customer.ID = generateUUID() } - + if customer.CreatedAt.IsZero() { customer.CreatedAt = time.Now() } - + customers, err := cs.LoadCustomers() if err != nil { return err } - + customers = append(customers, customer) - + return cs.SaveCustomers(customers) } @@ -91,19 +105,19 @@ func generateUUID() string { rand.Read(bytes) bytes[6] = (bytes[6] & 0x0f) | 0x40 // Version 4 bytes[8] = (bytes[8] & 0x3f) | 0x80 // Variant - + return hex.EncodeToString(bytes) } func (cs *customerStorage) UpdateCustomer(id string, updates models.UpdateCustomerRequest) error { cs.mutex.Lock() defer cs.mutex.Unlock() - + customers, err := cs.LoadCustomers() if err != nil { return err } - + for i, customer := range customers { if customer.ID == id { if updates.CustomerName != nil { @@ -133,30 +147,30 @@ func (cs *customerStorage) UpdateCustomer(id string, updates models.UpdateCustom if updates.Reporter != nil { customers[i].Reporter = *updates.Reporter } - + return cs.SaveCustomers(customers) } } - + return nil // Customer not found, but not an error } func (cs *customerStorage) DeleteCustomer(id string) error { cs.mutex.Lock() defer cs.mutex.Unlock() - + customers, err := cs.LoadCustomers() if err != nil { return err } - + for i, customer := range customers { if customer.ID == id { customers = append(customers[:i], customers[i+1:]...) return cs.SaveCustomers(customers) } } - + return nil // Customer not found, but not an error } @@ -166,12 +180,12 @@ func (cs *customerStorage) SaveCustomers(customers []models.Customer) error { if err := os.MkdirAll(dir, 0755); err != nil { return err } - + data, err := json.MarshalIndent(customers, "", " ") if err != nil { return err } - + return os.WriteFile(cs.filePath, data, 0644) } @@ -181,34 +195,34 @@ func (cs *customerStorage) LoadCustomers() ([]models.Customer, error) { // Return empty slice if file doesn't exist return []models.Customer{}, nil } - + data, err := os.ReadFile(cs.filePath) if err != nil { return nil, err } - + var customers []models.Customer if err := json.Unmarshal(data, &customers); err != nil { return nil, err } - + return customers, nil } func (cs *customerStorage) CustomerExists(customer models.Customer) (bool, error) { cs.mutex.RLock() defer cs.mutex.RUnlock() - + customers, err := cs.LoadCustomers() if err != nil { return false, err } - + for _, existingCustomer := range customers { if existingCustomer.Description == customer.Description { return true, nil } } - + return false, nil -} \ No newline at end of file +}
时间 客户 版本 描述模块 状态与进度 报告人时间 操作