const fs = require('fs'); const path = require('path'); const { categoryDir } = require('./output-paths'); function customersDir() { return categoryDir('customers'); } function brokersDir() { return categoryDir('brokers'); } function customerFilePath(customerId) { return path.join(customersDir(), `${customerId}.json`); } function brokerFilePath(brokerId) { return path.join(brokersDir(), `${brokerId}.json`); } function phoneIndexPath() { return path.join(customersDir(), 'index-phone.json'); } function externalUserIdIndexPath() { return path.join(customersDir(), 'index-external-user-id.json'); } function safeReadJson(filePath, fallback = null) { try { if (!filePath || !fs.existsSync(filePath)) return fallback; return JSON.parse(fs.readFileSync(filePath, 'utf8')); } catch { return fallback; } } function atomicWriteJson(filePath, data) { const dir = path.dirname(filePath); fs.mkdirSync(dir, { recursive: true }); const tmpPath = `${filePath}.tmp.${Date.now()}.${Math.random().toString(36).slice(2, 8)}`; try { fs.writeFileSync(tmpPath, JSON.stringify(data, null, 2), 'utf8'); fs.renameSync(tmpPath, filePath); } catch (err) { try { fs.unlinkSync(tmpPath); } catch {} throw err; } return filePath; } function normalizePhone(raw) { if (!raw) return ''; let value = String(raw).trim().replace(/[^\d]/g, ''); if (value.length >= 12 && /^(?:86)?1[3-9]\d{9}$/.test(value)) value = value.replace(/^86/, ''); return value; } function buildCustomerId(input = {}) { if (input.customerId) return String(input.customerId).trim(); if (input.externalUserId) return String(input.externalUserId).trim(); const phone = normalizePhone(input.phone); if (phone) return phone; return `cust-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`; } function readCustomerById(customerId) { if (!customerId) return null; return safeReadJson(customerFilePath(customerId)); } function writeCustomer(customer) { if (!customer || !customer.customerId) throw new Error('customer.customerId 必填'); const filePath = customerFilePath(customer.customerId); const existing = safeReadJson(filePath) || {}; const now = new Date().toISOString(); const record = { ...existing, ...customer, customerId: customer.customerId, createdAt: existing.createdAt || now, updatedAt: now }; atomicWriteJson(filePath, record); rebuildCustomerIndexIfNeeded(); return record; } function readPhoneIndex() { return safeReadJson(phoneIndexPath(), {}); } function readExternalUserIdIndex() { return safeReadJson(externalUserIdIndexPath(), {}); } function writePhoneIndex(index) { atomicWriteJson(phoneIndexPath(), index); } function writeExternalUserIdIndex(index) { atomicWriteJson(externalUserIdIndexPath(), index); } function rebuildCustomerIndexIfNeeded() { const dir = customersDir(); if (!fs.existsSync(dir)) return; const phoneIndex = {}; const externalUserIdIndex = {}; for (const file of fs.readdirSync(dir)) { if (!file.endsWith('.json') || file.startsWith('index-')) continue; const customerId = file.replace(/\.json$/, ''); const customer = safeReadJson(path.join(dir, file)); if (!customer) continue; const phone = normalizePhone(customer.phone); if (phone) phoneIndex[phone] = customer.customerId || customerId; if (customer.externalUserId) externalUserIdIndex[customer.externalUserId] = customer.customerId || customerId; } writePhoneIndex(phoneIndex); writeExternalUserIdIndex(externalUserIdIndex); } function readCustomerByExternalUserId(externalUserId) { if (!externalUserId) return null; const index = readExternalUserIdIndex(); const customerId = index[externalUserId]; if (customerId) { const customer = readCustomerById(customerId); if (customer) return customer; } // fallback: 扫描文件 const dir = customersDir(); if (!fs.existsSync(dir)) return null; for (const file of fs.readdirSync(dir)) { if (!file.endsWith('.json') || file.startsWith('index-')) continue; const customer = safeReadJson(path.join(dir, file)); if (customer && customer.externalUserId === externalUserId) return customer; } return null; } function readCustomerByPhone(phone) { const normalized = normalizePhone(phone); if (!normalized) return null; const index = readPhoneIndex(); const customerId = index[normalized]; if (customerId) { const customer = readCustomerById(customerId); if (customer) return customer; } // fallback: 扫描文件 const dir = customersDir(); if (!fs.existsSync(dir)) return null; for (const file of fs.readdirSync(dir)) { if (!file.endsWith('.json') || file.startsWith('index-')) continue; const customer = safeReadJson(path.join(dir, file)); if (customer && normalizePhone(customer.phone) === normalized) return customer; } return null; } function readBrokerById(brokerId) { if (!brokerId) return null; return safeReadJson(brokerFilePath(brokerId)); } function writeBroker(broker) { if (!broker || !broker.brokerId) throw new Error('broker.brokerId 必填'); const filePath = brokerFilePath(broker.brokerId); const existing = safeReadJson(filePath) || {}; const now = new Date().toISOString(); const guidList = Array.from(new Set([ ...(existing.guidList || []), ...(broker.guidList || []) ].filter(Boolean))); const record = { ...existing, ...broker, brokerId: broker.brokerId, guidList, createdAt: existing.createdAt || now, updatedAt: now }; atomicWriteJson(filePath, record); return record; } function readBrokerByWecomUserId(wecomUserId) { if (!wecomUserId) return null; const dir = brokersDir(); if (!fs.existsSync(dir)) return null; for (const file of fs.readdirSync(dir)) { if (!file.endsWith('.json')) continue; const broker = safeReadJson(path.join(dir, file)); if (broker && broker.wecomUserId === wecomUserId) return broker; } return null; } function bindBrokerToDevice(brokerId, guid) { if (!brokerId || !guid) return null; const broker = readBrokerById(brokerId) || { brokerId }; broker.guidList = Array.from(new Set([...(broker.guidList || []), guid].filter(Boolean))); return writeBroker(broker); } function buildCustomerPhoneIndex() { rebuildCustomerIndexIfNeeded(); return readPhoneIndex(); } function buildCustomerExternalUserIdIndex() { rebuildCustomerIndexIfNeeded(); return readExternalUserIdIndex(); } module.exports = { normalizePhone, buildCustomerId, readCustomerById, writeCustomer, readCustomerByExternalUserId, readCustomerByPhone, readBrokerById, writeBroker, readBrokerByWecomUserId, bindBrokerToDevice, buildCustomerPhoneIndex, buildCustomerExternalUserIdIndex, customerFilePath, brokerFilePath, phoneIndexPath, externalUserIdIndexPath };