420 lines
12 KiB
JavaScript
420 lines
12 KiB
JavaScript
const express = require('express');
|
|
const cors = require('cors');
|
|
const { body, param, query, validationResult } = require('express-validator');
|
|
require('dotenv').config();
|
|
|
|
const db = require('./db');
|
|
|
|
const app = express();
|
|
const PORT = process.env.PORT || 3002;
|
|
|
|
// 中间件 - CORS配置
|
|
// 当前暂无域名,员工通过公网IP访问,暂时允许所有来源
|
|
// TODO: 申请域名后,在 .env 的 CORS_ORIGIN 中填写域名,并切换为严格模式
|
|
const corsWhitelist = process.env.CORS_ORIGIN ? process.env.CORS_ORIGIN.split(',') : [];
|
|
const corsOptions = {
|
|
origin: (origin, callback) => {
|
|
// 暂无域名阶段:允许所有来源访问(公网IP访问需要)
|
|
if (!origin || corsWhitelist.length === 0 || corsWhitelist.includes(origin)) {
|
|
callback(null, true);
|
|
} else {
|
|
// 有域名后可改为 callback(new Error('Not allowed'), false) 限制来源
|
|
callback(null, true);
|
|
}
|
|
},
|
|
credentials: true,
|
|
methods: ['GET', 'POST', 'PUT', 'DELETE', 'PATCH', 'OPTIONS'],
|
|
allowedHeaders: ['Content-Type', 'Authorization']
|
|
};
|
|
app.use(cors(corsOptions));
|
|
app.use(express.json());
|
|
app.use(express.urlencoded({ extended: true }));
|
|
|
|
// 验证错误处理中间件
|
|
const validate = (req, res, next) => {
|
|
const errors = validationResult(req);
|
|
if (!errors.isEmpty()) {
|
|
return res.status(400).json({
|
|
success: false,
|
|
errors: errors.array()
|
|
});
|
|
}
|
|
next();
|
|
};
|
|
|
|
// 健康检查端点
|
|
app.get('/health', (req, res) => {
|
|
res.json({
|
|
status: 'healthy',
|
|
timestamp: new Date().toISOString(),
|
|
service: 'Customer Management API'
|
|
});
|
|
});
|
|
|
|
// ==================== 客户管理 API ====================
|
|
|
|
// 1. GET /api/customers - 获取客户列表(分页、搜索)
|
|
app.get('/api/customers',
|
|
[
|
|
query('page').optional().isInt({ min: 1 }).toInt(),
|
|
query('limit').optional().isInt({ min: 1, max: 100 }).toInt(),
|
|
query('search').optional().trim(),
|
|
query('status').optional().trim()
|
|
],
|
|
validate,
|
|
async (req, res) => {
|
|
try {
|
|
const page = req.query.page || 1;
|
|
const limit = req.query.limit || 10;
|
|
const offset = (page - 1) * limit;
|
|
const search = req.query.search || '';
|
|
const status = req.query.status || '';
|
|
|
|
let query = 'SELECT * FROM customers WHERE 1=1';
|
|
let queryParams = [];
|
|
let paramCount = 1;
|
|
|
|
if (search) {
|
|
query += ` AND (name ILIKE $${paramCount} OR email ILIKE $${paramCount} OR company ILIKE $${paramCount})`;
|
|
queryParams.push(`%${search}%`);
|
|
paramCount++;
|
|
}
|
|
|
|
if (status) {
|
|
query += ` AND status = $${paramCount}`;
|
|
queryParams.push(status);
|
|
paramCount++;
|
|
}
|
|
|
|
// 获取总数
|
|
const countQuery = query.replace('SELECT *', 'SELECT COUNT(*) as total');
|
|
const countResult = await db.query(countQuery, queryParams);
|
|
const total = parseInt(countResult.rows[0].total);
|
|
|
|
// 获取分页数据
|
|
query += ` ORDER BY created_at DESC LIMIT $${paramCount} OFFSET $${paramCount + 1}`;
|
|
queryParams.push(limit, offset);
|
|
|
|
const result = await db.query(query, queryParams);
|
|
|
|
res.json({
|
|
success: true,
|
|
data: result.rows,
|
|
pagination: {
|
|
page: parseInt(page),
|
|
limit: parseInt(limit),
|
|
total,
|
|
totalPages: Math.ceil(total / limit)
|
|
}
|
|
});
|
|
} catch (error) {
|
|
console.error('Error fetching customers:', error);
|
|
res.status(500).json({
|
|
success: false,
|
|
message: 'Failed to fetch customers',
|
|
error: error.message
|
|
});
|
|
}
|
|
}
|
|
);
|
|
|
|
// 2. GET /api/customers/:id - 获取单个客户
|
|
app.get('/api/customers/:id',
|
|
[
|
|
param('id').isInt({ min: 1 })
|
|
],
|
|
validate,
|
|
async (req, res) => {
|
|
try {
|
|
const { id } = req.params;
|
|
const result = await db.query('SELECT * FROM customers WHERE id = $1', [id]);
|
|
|
|
if (result.rows.length === 0) {
|
|
return res.status(404).json({
|
|
success: false,
|
|
message: 'Customer not found'
|
|
});
|
|
}
|
|
|
|
res.json({
|
|
success: true,
|
|
data: result.rows[0]
|
|
});
|
|
} catch (error) {
|
|
console.error('Error fetching customer:', error);
|
|
res.status(500).json({
|
|
success: false,
|
|
message: 'Failed to fetch customer',
|
|
error: error.message
|
|
});
|
|
}
|
|
}
|
|
);
|
|
|
|
// 3. POST /api/customers - 创建客户
|
|
app.post('/api/customers',
|
|
[
|
|
body('name').notEmpty().trim().withMessage('Name is required'),
|
|
body('email').notEmpty().trim().isEmail().withMessage('Valid email is required'),
|
|
body('phone').optional().trim(),
|
|
body('address').optional().trim(),
|
|
body('company').optional().trim(),
|
|
body('tax_id').optional().trim(),
|
|
body('status').optional().isIn(['active', 'inactive']).withMessage('Status must be active or inactive')
|
|
],
|
|
validate,
|
|
async (req, res) => {
|
|
try {
|
|
const { name, email, phone, address, company, tax_id, status = 'active' } = req.body;
|
|
|
|
const result = await db.query(
|
|
`INSERT INTO customers (name, email, phone, address, company, tax_id, status)
|
|
VALUES ($1, $2, $3, $4, $5, $6, $7)
|
|
RETURNING *`,
|
|
[name, email, phone, address, company, tax_id, status]
|
|
);
|
|
|
|
res.status(201).json({
|
|
success: true,
|
|
message: 'Customer created successfully',
|
|
data: result.rows[0]
|
|
});
|
|
} catch (error) {
|
|
console.error('Error creating customer:', error);
|
|
|
|
// 处理唯一约束错误
|
|
if (error.code === '23505') { // unique_violation
|
|
return res.status(409).json({
|
|
success: false,
|
|
message: 'Email already exists'
|
|
});
|
|
}
|
|
|
|
res.status(500).json({
|
|
success: false,
|
|
message: 'Failed to create customer',
|
|
error: error.message
|
|
});
|
|
}
|
|
}
|
|
);
|
|
|
|
// 4. PUT /api/customers/:id - 更新客户
|
|
app.put('/api/customers/:id',
|
|
[
|
|
param('id').isInt({ min: 1 }),
|
|
body('name').optional().trim(),
|
|
body('email').optional().trim().isEmail().withMessage('Valid email is required if provided'),
|
|
body('phone').optional().trim(),
|
|
body('address').optional().trim(),
|
|
body('company').optional().trim(),
|
|
body('tax_id').optional().trim(),
|
|
body('status').optional().isIn(['active', 'inactive']).withMessage('Status must be active or inactive')
|
|
],
|
|
validate,
|
|
async (req, res) => {
|
|
try {
|
|
const { id } = req.params;
|
|
const { name, email, phone, address, company, tax_id, status } = req.body;
|
|
|
|
// 检查客户是否存在
|
|
const checkResult = await db.query('SELECT * FROM customers WHERE id = $1', [id]);
|
|
if (checkResult.rows.length === 0) {
|
|
return res.status(404).json({
|
|
success: false,
|
|
message: 'Customer not found'
|
|
});
|
|
}
|
|
|
|
// 构建更新字段
|
|
const updateFields = [];
|
|
const values = [];
|
|
let paramCount = 1;
|
|
|
|
if (name !== undefined) {
|
|
updateFields.push(`name = $${paramCount}`);
|
|
values.push(name);
|
|
paramCount++;
|
|
}
|
|
|
|
if (email !== undefined) {
|
|
updateFields.push(`email = $${paramCount}`);
|
|
values.push(email);
|
|
paramCount++;
|
|
}
|
|
|
|
if (phone !== undefined) {
|
|
updateFields.push(`phone = $${paramCount}`);
|
|
values.push(phone);
|
|
paramCount++;
|
|
}
|
|
|
|
if (address !== undefined) {
|
|
updateFields.push(`address = $${paramCount}`);
|
|
values.push(address);
|
|
paramCount++;
|
|
}
|
|
|
|
if (company !== undefined) {
|
|
updateFields.push(`company = $${paramCount}`);
|
|
values.push(company);
|
|
paramCount++;
|
|
}
|
|
|
|
if (tax_id !== undefined) {
|
|
updateFields.push(`tax_id = $${paramCount}`);
|
|
values.push(tax_id);
|
|
paramCount++;
|
|
}
|
|
|
|
if (status !== undefined) {
|
|
updateFields.push(`status = $${paramCount}`);
|
|
values.push(status);
|
|
paramCount++;
|
|
}
|
|
|
|
// 添加更新时间
|
|
updateFields.push(`updated_at = CURRENT_TIMESTAMP`);
|
|
|
|
if (updateFields.length === 1) { // 只有updated_at被更新
|
|
return res.status(400).json({
|
|
success: false,
|
|
message: 'No fields to update'
|
|
});
|
|
}
|
|
|
|
values.push(id);
|
|
const query = `UPDATE customers SET ${updateFields.join(', ')} WHERE id = $${paramCount} RETURNING *`;
|
|
|
|
const result = await db.query(query, values);
|
|
|
|
res.json({
|
|
success: true,
|
|
message: 'Customer updated successfully',
|
|
data: result.rows[0]
|
|
});
|
|
} catch (error) {
|
|
console.error('Error updating customer:', error);
|
|
|
|
// 处理唯一约束错误
|
|
if (error.code === '23505') { // unique_violation
|
|
return res.status(409).json({
|
|
success: false,
|
|
message: 'Email already exists'
|
|
});
|
|
}
|
|
|
|
res.status(500).json({
|
|
success: false,
|
|
message: 'Failed to update customer',
|
|
error: error.message
|
|
});
|
|
}
|
|
}
|
|
);
|
|
|
|
// 5. DELETE /api/customers/:id - 删除客户
|
|
app.delete('/api/customers/:id',
|
|
[
|
|
param('id').isInt({ min: 1 })
|
|
],
|
|
validate,
|
|
async (req, res) => {
|
|
try {
|
|
const { id } = req.params;
|
|
|
|
// 检查客户是否存在
|
|
const checkResult = await db.query('SELECT * FROM customers WHERE id = $1', [id]);
|
|
if (checkResult.rows.length === 0) {
|
|
return res.status(404).json({
|
|
success: false,
|
|
message: 'Customer not found'
|
|
});
|
|
}
|
|
|
|
await db.query('DELETE FROM customers WHERE id = $1', [id]);
|
|
|
|
res.json({
|
|
success: true,
|
|
message: 'Customer deleted successfully'
|
|
});
|
|
} catch (error) {
|
|
console.error('Error deleting customer:', error);
|
|
res.status(500).json({
|
|
success: false,
|
|
message: 'Failed to delete customer',
|
|
error: error.message
|
|
});
|
|
}
|
|
}
|
|
);
|
|
|
|
// 6. GET /api/customers/:id/contacts - 获取客户联系人
|
|
app.get('/api/customers/:id/contacts',
|
|
[
|
|
param('id').isInt({ min: 1 })
|
|
],
|
|
validate,
|
|
async (req, res) => {
|
|
try {
|
|
const { id } = req.params;
|
|
|
|
// 检查客户是否存在
|
|
const customerResult = await db.query('SELECT * FROM customers WHERE id = $1', [id]);
|
|
if (customerResult.rows.length === 0) {
|
|
return res.status(404).json({
|
|
success: false,
|
|
message: 'Customer not found'
|
|
});
|
|
}
|
|
|
|
const result = await db.query(
|
|
'SELECT * FROM contacts WHERE customer_id = $1 ORDER BY is_primary DESC, created_at DESC',
|
|
[id]
|
|
);
|
|
|
|
res.json({
|
|
success: true,
|
|
data: result.rows
|
|
});
|
|
} catch (error) {
|
|
console.error('Error fetching customer contacts:', error);
|
|
res.status(500).json({
|
|
success: false,
|
|
message: 'Failed to fetch customer contacts',
|
|
error: error.message
|
|
});
|
|
}
|
|
}
|
|
);
|
|
|
|
// 错误处理中间件
|
|
app.use((err, req, res, next) => {
|
|
console.error(err.stack);
|
|
res.status(500).json({
|
|
success: false,
|
|
message: 'Internal server error',
|
|
error: process.env.NODE_ENV === 'development' ? err.message : undefined
|
|
});
|
|
});
|
|
|
|
// 404处理
|
|
app.use((req, res) => {
|
|
res.status(404).json({
|
|
success: false,
|
|
message: 'Endpoint not found'
|
|
});
|
|
});
|
|
|
|
// 启动服务器
|
|
app.listen(PORT, () => {
|
|
console.log(`Customer Management API server running on port ${PORT}`);
|
|
console.log('Available endpoints:');
|
|
console.log(' GET /health');
|
|
console.log(' GET /api/customers');
|
|
console.log(' GET /api/customers/:id');
|
|
console.log(' POST /api/customers');
|
|
console.log(' PUT /api/customers/:id');
|
|
console.log(' DELETE /api/customers/:id');
|
|
console.log(' GET /api/customers/:id/contacts');
|
|
}); |