Files
yunhaifinance/backend/api-complete.js
T

1174 lines
46 KiB
JavaScript
Raw Normal View History

const express = require('express');
const cors = require('cors');
const { body, param, query, validationResult } = require('express-validator');
const multer = require('multer');
const xlsx = require('xlsx');
require('dotenv').config();
const db = require('./db-sqlite');
const { verifyPassword, generateToken } = require('./utils/auth');
// 文件上传配置
const storage = multer.memoryStorage();
const upload = multer({ storage, limits: { fileSize: 10 * 1024 * 1024 } });
const imageFormats = ['jpg', 'jpeg', 'png', 'gif', 'webp', 'bmp', 'svg'];
const app = express();
const PORT = process.argv[2] || process.env.PORT || 3000;
// 中间件 - 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() });
});
app.get('/', (req, res) => {
res.json({
success: true,
message: '轻远电力老挝ERP API',
version: '2.0.0',
endpoints: {
health: '/api/health',
auth: '/api/auth',
users: '/api/users',
projects: '/api/projects',
products: '/api/products',
customers: '/api/customers',
suppliers: '/api/suppliers',
advances: '/api/advances',
reimbursements: '/api/reimbursements',
exchangeRates: '/api/exchange-rates'
}
});
});
// ==================== 用户认证 ====================
app.post('/api/auth/login', [
body('username').notEmpty(),
body('password').notEmpty()
], validate, async (req, res) => {
try {
const { username, password } = req.body;
const result = await db.query(
'SELECT id, username, role, name, password_hash FROM users WHERE username = ?',
[username]
);
if (result.rows.length === 0) {
return res.status(401).json({ success: false, message: '用户名或密码错误' });
}
const user = result.rows[0];
if (!user.password_hash || !verifyPassword(password, user.password_hash)) {
return res.status(401).json({ success: false, message: '用户名或密码错误' });
}
const token = generateToken({ id: user.id, username: user.username, role: user.role });
res.json({ success: true, data: { id: user.id, username: user.username, role: user.role, name: user.name, token } });
} catch (error) {
res.status(500).json({ success: false, message: '登录失败' });
}
});
// 兼容前端的登录路径
app.post('/api/users/login', [
body('username').notEmpty(),
body('password').notEmpty()
], validate, async (req, res) => {
try {
const { username, password } = req.body;
const result = await db.query(
'SELECT id, username, role, name, password_hash FROM users WHERE username = ?',
[username]
);
if (result.rows.length === 0) {
return res.status(401).json({ user: null, token: null, message: '用户名或密码错误' });
}
const user = result.rows[0];
if (!user.password_hash || !verifyPassword(password, user.password_hash)) {
return res.status(401).json({ user: null, token: null, message: '用户名或密码错误' });
}
const token = generateToken({ id: user.id, username: user.username, role: user.role });
res.json({ user: { id: user.id, username: user.username, role: user.role, name: user.name }, token });
} catch (error) {
res.status(500).json({ user: null, token: null, message: '登录失败' });
}
});
// ==================== 项目管理 ====================
app.get('/api/projects', async (req, res) => {
try {
const result = await db.query('SELECT * FROM projects ORDER BY created_at DESC');
res.json({ success: true, data: result.rows });
} catch (error) {
res.status(500).json({ success: false, message: error.message });
}
});
app.post('/api/projects', [
body('name').notEmpty(),
body('code').notEmpty()
], validate, async (req, res) => {
try {
const { name, code, customer_id, contract_amount, start_date, end_date, description } = req.body;
const result = await db.query(
`INSERT INTO projects (name, code, customer_id, contract_amount, start_date, end_date, description, status)
VALUES (?, ?, ?, ?, ?, ?, ?, 'active')`,
[name, code, customer_id, contract_amount, start_date, end_date, description]
);
// SQLite不支持RETURNING,所以需要查询刚插入的数据
const lastInsert = await db.query('SELECT * FROM projects ORDER BY id DESC LIMIT 1');
res.json({ success: true, data: lastInsert.rows[0] });
} catch (error) {
res.status(500).json({ success: false, message: error.message });
}
});
// ==================== 客户管理 ====================
app.get('/api/customers', async (req, res) => {
try {
const result = await db.query('SELECT * FROM customers ORDER BY created_at DESC');
res.json({ success: true, data: result.rows });
} catch (error) {
res.status(500).json({ success: false, message: error.message });
}
});
app.post('/api/customers', [
body('name').notEmpty()
], validate, async (req, res) => {
try {
const { name, contact, phone, email, address, type } = req.body;
const result = await db.query(
`INSERT INTO customers (name, contact, phone, email, address, type)
VALUES (?, ?, ?, ?, ?, ?)`,
[name, contact, phone, email, address, type || 'corporate']
);
// SQLite不支持RETURNING,所以需要查询刚插入的数据
const lastInsert = await db.query('SELECT * FROM customers ORDER BY id DESC LIMIT 1');
res.json({ success: true, data: lastInsert.rows[0] });
} catch (error) {
res.status(500).json({ success: false, message: error.message });
}
});
// ==================== 供应商管理 ====================
app.get('/api/suppliers', async (req, res) => {
try {
const result = await db.query('SELECT * FROM suppliers ORDER BY created_at DESC');
res.json({ success: true, data: result.rows });
} catch (error) {
res.status(500).json({ success: false, message: error.message });
}
});
// ==================== 产品管理 ====================
app.get('/api/products', async (req, res) => {
try {
// 获取所有分类信息
const categoriesResult = await db.query('SELECT * FROM categories');
const categoriesMap = new Map();
categoriesResult.rows.forEach(cat => {
categoriesMap.set(cat.id, cat);
});
// 获取所有商品
const result = await db.query('SELECT * FROM products ORDER BY created_at DESC');
// 转换数据结构以匹配前端期望
const products = result.rows.map((product, index) => {
let category_name = product.category;
let parent_category_name = null;
// 根据category_id获取分类信息
if (product.category_id && categoriesMap.has(product.category_id)) {
const category = categoriesMap.get(product.category_id);
category_name = category.name;
// 获取父分类信息
if (category.parent_id && categoriesMap.has(category.parent_id)) {
const parentCategory = categoriesMap.get(category.parent_id);
parent_category_name = parentCategory.name;
}
}
return {
id: product.id,
name: product.name,
model: product.model || product.code, // 如果model为空,使用code作为fallback
category_id: product.category_id || index + 1, // 使用实际的category_id
category_name: category_name,
parent_category_name: parent_category_name,
unit: '个', // 默认单位
quantity: 0, // 默认数量
cost_price: product.price,
thumbnail: null, // 默认无缩略图
specification: product.description, // 使用description作为specification
brand: null, // 默认无品牌
remark: null, // 默认无备注
source: product.source || '老挝', // 确保source字段存在
created_at: product.created_at,
updated_at: product.updated_at
};
});
res.json({ success: true, data: products, total: products.length });
} catch (error) {
console.error('获取商品失败:', error);
res.status(500).json({ success: false, message: error.message });
}
});
// ==================== 预支管理 ====================
app.get('/api/advances', async (req, res) => {
try {
const result = await db.query(`
SELECT a.*, u.name as user_name, p.name as project_name
FROM advances a
LEFT JOIN users u ON a.user_id = u.id
LEFT JOIN projects p ON a.project_id = p.id
ORDER BY a.created_at DESC
`);
res.json({ success: true, data: result.rows });
} catch (error) {
res.status(500).json({ success: false, message: error.message });
}
});
// ==================== 报销管理 ====================
app.get('/api/reimbursements', async (req, res) => {
try {
const result = await db.query(`
SELECT r.*, u.name as user_name, p.name as project_name
FROM reimbursements r
LEFT JOIN users u ON r.user_id = u.id
LEFT JOIN projects p ON r.project_id = p.id
ORDER BY r.created_at DESC
`);
res.json({ success: true, data: result.rows });
} catch (error) {
res.status(500).json({ success: false, message: error.message });
}
});
// ==================== 汇率管理 ====================
app.get('/api/exchange-rates', async (req, res) => {
try {
const result = await db.query(`
SELECT * FROM exchange_rates
WHERE is_active = true
ORDER BY effective_date DESC
LIMIT 10
`);
res.json({ success: true, data: result.rows });
} catch (error) {
res.status(500).json({ success: false, message: error.message });
}
});
// ==================== 预算报价管理 ====================
app.get('/api/budget-projects', async (req, res) => {
try {
const result = await db.query(`
SELECT b.*,
(SELECT json_group_array(json_object(
'id', q.id,
'version', q.version,
'quotation_date', q.quotation_date,
'amount', q.amount,
'currency', q.currency,
'status', q.status,
'file_url', q.file_url,
'remark', q.remark,
'created_at', q.created_at
)) FROM quotations q WHERE q.project_id = b.id) as quotations
FROM budget_projects b
ORDER BY b.created_at DESC
`);
res.json({ success: true, data: result.rows });
} catch (error) {
res.status(500).json({ success: false, message: error.message });
}
});
// ==================== 施工管理 ====================
app.get('/api/construction/my-projects', async (req, res) => {
try {
const result = await db.query(`
SELECT p.*,
c.name as customer_name,
(SELECT json_object(
'id', cl.id,
'log_date', cl.log_date,
'weather', cl.weather,
'work_content', cl.work_content
) FROM construction_logs cl WHERE cl.project_id = p.id ORDER BY cl.log_date DESC LIMIT 1) as latest_log
FROM projects p
LEFT JOIN customers c ON p.customer_id = c.id
WHERE p.status IN ('active', 'pending')
ORDER BY p.created_at DESC
`);
res.json({ success: true, data: result.rows });
} catch (error) {
res.status(500).json({ success: false, message: error.message });
}
});
// ==================== 文件上传API (模拟) ====================
app.post('/api/upload/single', upload.single('file'), async (req, res) => {
try {
if (!req.file) return res.status(400).json({ success: false, error: '没有上传文件' });
const ext = req.file.originalname.split('.').pop().toLowerCase();
const timestamp = Date.now();
const randomStr = Math.random().toString(36).substring(2, 8);
const filename = 'uploads/' + timestamp + '_' + randomStr + '.' + ext;
// 模拟文件上传,返回模拟的文件URL
const fileUrl = 'https://example.com/' + filename;
res.json({
success: true,
data: {
url: fileUrl,
name: req.file.originalname,
size: req.file.size,
type: req.file.mimetype,
isImage: imageFormats.includes(ext)
}
});
} catch (error) {
console.error('上传异常:', error);
res.status(500).json({ success: false, error: '上传失败' });
}
});
app.post('/api/upload/multiple', upload.array('files', 10), async (req, res) => {
try {
if (!req.files || req.files.length === 0) {
return res.status(400).json({ success: false, error: '没有上传文件' });
}
const results = req.files.map(file => {
const ext = file.originalname.split('.').pop().toLowerCase();
const filename = 'uploads/' + Date.now() + '_' + Math.random().toString(36).substring(2, 8) + '.' + ext;
const fileUrl = 'https://example.com/' + filename;
return {
url: fileUrl,
name: file.originalname,
size: file.size,
isImage: imageFormats.includes(ext)
};
});
res.json({ success: true, data: results });
} catch (error) {
console.error('批量上传失败:', error);
res.status(500).json({ success: false, error: '上传失败' });
}
});
// 处理分类的函数
async function handleCategory(firstCategory, secondCategory = null) {
try {
// 检查一级分类是否存在
let firstCategoryId;
const firstCategoryResult = await db.query(
'SELECT id FROM categories WHERE name = ? AND parent_id IS NULL',
[firstCategory]
);
if (firstCategoryResult.rows.length === 0) {
// 创建一级分类
await db.query(
'INSERT INTO categories (name, parent_id, level, created_at, updated_at) VALUES (?, NULL, 1, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP)',
[firstCategory]
);
// 获取刚创建的一级分类ID
const newFirstCategoryResult = await db.query(
'SELECT id FROM categories WHERE name = ? AND parent_id IS NULL ORDER BY id DESC LIMIT 1',
[firstCategory]
);
firstCategoryId = newFirstCategoryResult.rows[0].id;
} else {
firstCategoryId = firstCategoryResult.rows[0].id;
}
// 如果有二级分类,检查并创建
if (secondCategory) {
let secondCategoryId;
const secondCategoryResult = await db.query(
'SELECT id FROM categories WHERE name = ? AND parent_id = ?',
[secondCategory, firstCategoryId]
);
if (secondCategoryResult.rows.length === 0) {
// 创建二级分类
await db.query(
'INSERT INTO categories (name, parent_id, level, created_at, updated_at) VALUES (?, ?, 2, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP)',
[secondCategory, firstCategoryId]
);
// 获取刚创建的二级分类ID
const newSecondCategoryResult = await db.query(
'SELECT id FROM categories WHERE name = ? AND parent_id = ? ORDER BY id DESC LIMIT 1',
[secondCategory, firstCategoryId]
);
secondCategoryId = newSecondCategoryResult.rows[0].id;
} else {
secondCategoryId = secondCategoryResult.rows[0].id;
}
return secondCategoryId;
}
return firstCategoryId;
} catch (error) {
throw new Error('处理分类失败: ' + error.message);
}
}
// ==================== 商品批量上传API ====================
app.post('/api/products/batch-import', upload.single('file'), async (req, res) => {
try {
console.log('收到批量导入请求');
if (!req.file) {
console.error('没有上传文件');
return res.status(400).json({ success: false, error: '没有上传文件' });
}
console.log('文件信息:', req.file.originalname, req.file.size);
const ext = req.file.originalname.split('.').pop().toLowerCase();
if (!['xlsx', 'xls'].includes(ext)) {
console.error('文件类型不支持:', ext);
return res.status(400).json({ success: false, error: '只支持Excel文件' });
}
// 解析Excel文件
console.log('开始解析Excel文件');
const workbook = xlsx.read(req.file.buffer, { type: 'buffer' });
const sheetName = workbook.SheetNames[0];
console.log('工作表名称:', sheetName);
const worksheet = workbook.Sheets[sheetName];
const data = xlsx.utils.sheet_to_json(worksheet);
console.log('解析成功,数据条数:', data.length);
if (data.length === 0) {
console.error('Excel文件为空');
return res.status(400).json({ success: false, error: 'Excel文件为空' });
}
console.log('前5条数据:', data.slice(0, 5));
// 批量导入商品
let successCount = 0;
let errorCount = 0;
const errors = [];
for (const item of data) {
try {
// 验证必填字段
if (!item['商品名称']) {
throw new Error('商品名称不能为空');
}
if (!item['一级分类']) {
throw new Error('一级分类不能为空');
}
// 处理分类
const categoryId = await handleCategory(item['一级分类'], item['二级分类']);
// 处理来源字段,默认为老挝
const source = item['来源'] || '老挝';
// 生成唯一的code
const baseCode = item['型号'] || item['商品名称'];
const uniqueCode = baseCode + '_' + Date.now() + '_' + Math.floor(Math.random() * 1000);
// 确定分类名称
let categoryName = item['一级分类'];
if (item['二级分类']) {
categoryName = item['二级分类'];
}
// 插入商品
await db.query(
`INSERT INTO products (name, code, model, category_id, category, price, description, source, created_at, updated_at)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP)`,
[
item['商品名称'],
uniqueCode,
item['型号'],
categoryId,
categoryName,
item['成本单价'] || 0,
item['规格参数'] || item['备注'] || '',
source
]
);
successCount++;
} catch (error) {
errorCount++;
console.error('导入商品失败:', item['商品名称'], error.message);
errors.push({ item: item['商品名称'], error: error.message });
}
}
res.json({
success: true,
message: `导入完成,成功 ${successCount} 条,失败 ${errorCount} 条`,
data: {
successCount,
errorCount,
errors
}
});
} catch (error) {
console.error('批量导入失败:', error);
res.status(500).json({ success: false, error: '导入失败: ' + error.message });
}
});
// ==================== 下载商品模板 ====================
app.get('/api/products/template', async (req, res) => {
try {
// 创建模板数据
const templateData = [
{ '商品名称': '示例商品1', '型号': 'MODEL001', '一级分类': '电器', '二级分类': '家用电器', '单位': '个', '成本单价': 100.00, '品牌': '品牌A', '规格参数': '220V', '来源': '老挝', '备注': '示例商品' },
{ '商品名称': '示例商品2', '型号': 'MODEL002', '一级分类': '工具', '二级分类': '手动工具', '单位': '把', '成本单价': 50.00, '品牌': '品牌B', '规格参数': '标准型', '来源': '中国', '备注': '示例商品' }
];
// 创建工作簿和工作表
const workbook = xlsx.utils.book_new();
const worksheet = xlsx.utils.json_to_sheet(templateData);
xlsx.utils.book_append_sheet(workbook, worksheet, '商品模板');
// 设置响应头
res.setHeader('Content-Type', 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet');
res.setHeader('Content-Disposition', 'attachment; filename=' + encodeURIComponent('商品导入模板.xlsx'));
// 发送文件
const buffer = xlsx.write(workbook, { bookType: 'xlsx', type: 'buffer' });
res.send(buffer);
} catch (error) {
console.error('下载模板失败:', error);
res.status(500).json({ success: false, error: '下载模板失败' });
}
});
// ==================== 付款申请 ====================
app.get('/api/payment-requests', async (req, res) => {
try {
const result = await db.query('SELECT * FROM payment_requests ORDER BY created_at DESC');
res.json({ success: true, data: result.rows });
} catch (error) {
res.status(500).json({ success: false, message: error.message });
}
});
app.post('/api/payment-requests', async (req, res) => {
try {
const { applicant, payee, bank_account, bank_name, amount, amount_cny, currency, payment_date, reason, detail_items, attachments } = req.body;
const requestCode = 'PAY' + new Date().toISOString().slice(0, 10).replace(/-/g, '') + Math.floor(1000 + Math.random() * 9000);
await db.query(
'INSERT INTO payment_requests (request_code, applicant, payee, bank_account, bank_name, amount, amount_cny, currency, payment_date, reason, detail_items, attachments, status) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)',
[requestCode, applicant, payee, bank_account, bank_name, amount, amount_cny, currency, payment_date, reason, JSON.stringify(detail_items), JSON.stringify(attachments), 'pending']
);
const lastInsert = await db.query('SELECT * FROM payment_requests ORDER BY id DESC LIMIT 1');
res.json({ success: true, data: lastInsert.rows[0] });
} catch (error) {
res.status(500).json({ success: false, message: error.message });
}
});
app.put('/api/payment-requests/:id', async (req, res) => {
try {
const { id } = req.params;
const { applicant, payee, bank_account, bank_name, amount, amount_cny, currency, payment_date, reason, detail_items, attachments } = req.body;
await db.query(
'UPDATE payment_requests SET applicant = ?, payee = ?, bank_account = ?, bank_name = ?, amount = ?, amount_cny = ?, currency = ?, payment_date = ?, reason = ?, detail_items = ?, attachments = ? WHERE id = ?',
[applicant, payee, bank_account, bank_name, amount, amount_cny, currency, payment_date, reason, JSON.stringify(detail_items), JSON.stringify(attachments), id]
);
const updatedResult = await db.query('SELECT * FROM payment_requests WHERE id = ?', [id]);
res.json({ success: true, data: updatedResult.rows[0] });
} catch (error) {
res.status(500).json({ success: false, message: error.message });
}
});
app.delete('/api/payment-requests/:id', async (req, res) => {
try {
const { id } = req.params;
await db.query('DELETE FROM payment_requests WHERE id = ?', [id]);
res.json({ success: true, message: '删除成功' });
} catch (error) {
res.status(500).json({ success: false, message: error.message });
}
});
app.post('/api/payment-requests/:id/withdraw', async (req, res) => {
try {
const { id } = req.params;
await db.query('UPDATE payment_requests SET status = ? WHERE id = ?', ['withdrawn', id]);
res.json({ success: true, message: '已撤回' });
} catch (error) {
res.status(500).json({ success: false, message: error.message });
}
});
// ==================== 核销申请 ====================
app.get('/api/verifications', async (req, res) => {
try {
const result = await db.query('SELECT * FROM verifications ORDER BY created_at DESC');
res.json({ success: true, data: result.rows });
} catch (error) {
res.status(500).json({ success: false, message: error.message });
}
});
app.post('/api/verifications', async (req, res) => {
try {
const { applicant, advance_code, advance_amount, amount, amount_cny, currency, verification_date, reason, detail_items, attachments } = req.body;
const verificationCode = 'VER' + new Date().toISOString().slice(0, 10).replace(/-/g, '') + Math.floor(1000 + Math.random() * 9000);
await db.query(
'INSERT INTO verifications (verification_code, applicant, advance_code, advance_amount, amount, amount_cny, currency, verification_date, reason, detail_items, attachments, status) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)',
[verificationCode, applicant, advance_code, advance_amount, amount, amount_cny, currency, verification_date, reason, JSON.stringify(detail_items), JSON.stringify(attachments), 'pending']
);
const lastInsert = await db.query('SELECT * FROM verifications ORDER BY id DESC LIMIT 1');
res.json({ success: true, data: lastInsert.rows[0] });
} catch (error) {
res.status(500).json({ success: false, message: error.message });
}
});
app.put('/api/verifications/:id', async (req, res) => {
try {
const { id } = req.params;
const { applicant, advance_code, advance_amount, amount, amount_cny, currency, verification_date, reason, detail_items, attachments } = req.body;
await db.query(
'UPDATE verifications SET applicant = ?, advance_code = ?, advance_amount = ?, amount = ?, amount_cny = ?, currency = ?, verification_date = ?, reason = ?, detail_items = ?, attachments = ? WHERE id = ?',
[applicant, advance_code, advance_amount, amount, amount_cny, currency, verification_date, reason, JSON.stringify(detail_items), JSON.stringify(attachments), id]
);
const updatedResult = await db.query('SELECT * FROM verifications WHERE id = ?', [id]);
res.json({ success: true, data: updatedResult.rows[0] });
} catch (error) {
res.status(500).json({ success: false, message: error.message });
}
});
app.delete('/api/verifications/:id', async (req, res) => {
try {
const { id } = req.params;
await db.query('DELETE FROM verifications WHERE id = ?', [id]);
res.json({ success: true, message: '删除成功' });
} catch (error) {
res.status(500).json({ success: false, message: error.message });
}
});
app.post('/api/verifications/:id/withdraw', async (req, res) => {
try {
const { id } = req.params;
await db.query('UPDATE verifications SET status = ? WHERE id = ?', ['withdrawn', id]);
res.json({ success: true, message: '已撤回' });
} catch (error) {
res.status(500).json({ success: false, message: error.message });
}
});
// ==================== 分类管理API ====================
app.get('/api/categories', async (req, res) => {
try {
const result = await db.query('SELECT * FROM categories ORDER BY level, parent_id, name');
res.json({ success: true, data: result.rows });
} catch (error) {
res.status(500).json({ success: false, message: error.message });
}
});
app.post('/api/categories', async (req, res) => {
try {
const { name, parent_id, level } = req.body;
await db.query(
'INSERT INTO categories (name, parent_id, level, created_at, updated_at) VALUES (?, ?, ?, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP)',
[name, parent_id, level]
);
res.json({ success: true, message: '分类创建成功' });
} catch (error) {
res.status(500).json({ success: false, message: error.message });
}
});
app.put('/api/categories/:id', async (req, res) => {
try {
const { id } = req.params;
const { name, parent_id, level } = req.body;
await db.query(
'UPDATE categories SET name = ?, parent_id = ?, level = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ?',
[name, parent_id, level, id]
);
res.json({ success: true, message: '分类更新成功' });
} catch (error) {
res.status(500).json({ success: false, message: error.message });
}
});
app.delete('/api/categories/:id', async (req, res) => {
try {
const { id } = req.params;
// 先检查是否有子分类
const children = await db.query('SELECT id FROM categories WHERE parent_id = ?', [id]);
if (children.rows.length > 0) {
return res.status(400).json({ success: false, message: '该分类下有子分类,无法删除' });
}
// 再检查是否有商品使用该分类
const products = await db.query('SELECT id FROM products WHERE category_id = ?', [id]);
if (products.rows.length > 0) {
return res.status(400).json({ success: false, message: '该分类下有商品,无法删除' });
}
await db.query('DELETE FROM categories WHERE id = ?', [id]);
res.json({ success: true, message: '分类删除成功' });
} catch (error) {
res.status(500).json({ success: false, message: error.message });
}
});
// ==================== 汇率API ====================
app.get('/api/exchange-rates/latest', async (req, res) => {
try {
const result = await db.query('SELECT * FROM exchange_rates WHERE is_active = true ORDER BY effective_date DESC LIMIT 10');
const rates = {};
result.rows.forEach(row => {
rates[row.from_currency + '_' + row.to_currency] = row.rate;
});
res.json({ success: true, data: rates });
} catch (error) {
res.status(500).json({ success: false, message: error.message });
}
});
app.post('/api/exchange-rates', async (req, res) => {
try {
const { pair_key, rate, effective_date } = req.body;
// 解析货币对
const [from_currency, to_currency] = pair_key.split('_');
// 先将所有同货币对的汇率设置为非激活
await db.query(
'UPDATE exchange_rates SET is_active = 0 WHERE from_currency = ? AND to_currency = ?',
[from_currency, to_currency]
);
// 插入新的汇率记录
await db.query(
'INSERT INTO exchange_rates (from_currency, to_currency, rate, effective_date, is_active) VALUES (?, ?, ?, ?, ?)',
[from_currency, to_currency, rate, effective_date, 1]
);
res.json({ success: true, message: '汇率保存成功' });
} catch (error) {
console.error('保存汇率失败:', error);
res.status(500).json({ success: false, message: error.message });
}
});
app.get('/api/exchange-rates/history', async (req, res) => {
try {
const limit = req.query.limit || 20;
const result = await db.query(
'SELECT * FROM exchange_rates ORDER BY created_at DESC LIMIT ?',
[limit]
);
res.json({ success: true, data: result.rows });
} catch (error) {
res.status(500).json({ success: false, message: error.message });
}
});
// 启动服务器
app.listen(PORT, '0.0.0.0', () => {
console.log(`轻远电力老挝ERP API运行在端口 ${PORT}`);
});
// ==================== 项目详情 ====================
app.get('/api/projects/:id', async (req, res) => {
try {
const { id } = req.params;
const result = await db.query('SELECT * FROM projects WHERE id = ?', [id]);
if (result.rows.length === 0) {
return res.status(404).json({ success: false, message: '项目不存在' });
}
res.json({ success: true, data: result.rows[0] });
} catch (error) {
res.status(500).json({ success: false, message: error.message });
}
});
// ==================== 更新项目 ====================
app.put('/api/projects/:id', [
param('id').notEmpty()
], validate, async (req, res) => {
try {
const { id } = req.params;
const { name, manager_id, location, start_date, end_date, description, status, contract_amount } = req.body;
// SQLite不支持COALESCE和NOW()的组合使用,需要使用不同的方法
await db.query(
'UPDATE projects SET name = CASE WHEN ? IS NOT NULL THEN ? ELSE name END, manager_id = CASE WHEN ? IS NOT NULL THEN ? ELSE manager_id END, location = CASE WHEN ? IS NOT NULL THEN ? ELSE location END, start_date = CASE WHEN ? IS NOT NULL THEN ? ELSE start_date END, end_date = CASE WHEN ? IS NOT NULL THEN ? ELSE end_date END, description = CASE WHEN ? IS NOT NULL THEN ? ELSE description END, status = CASE WHEN ? IS NOT NULL THEN ? ELSE status END, contract_amount = CASE WHEN ? IS NOT NULL THEN ? ELSE contract_amount END, updated_at = CURRENT_TIMESTAMP WHERE id = ?',
[name, name, manager_id, manager_id, location, location, start_date, start_date, end_date, end_date, description, description, status, status, contract_amount, contract_amount, id]
);
// 查询更新后的数据
const updatedResult = await db.query('SELECT * FROM projects WHERE id = ?', [id]);
res.json({ success: true, data: updatedResult.rows[0] });
} catch (error) {
res.status(500).json({ success: false, message: error.message });
}
});
// ==================== 删除项目 ====================
app.delete('/api/projects/:id', async (req, res) => {
try {
const { id } = req.params;
await db.query('DELETE FROM projects WHERE id = ?', [id]);
res.json({ success: true, message: '项目已删除' });
} catch (error) {
res.status(500).json({ success: false, message: error.message });
}
});
// ==================== 预算项目管理API ====================
app.post('/api/budget-projects', async (req, res) => {
try {
const { name, customer_id, manager_id, location, survey_date, intermediary, intermediary_fee_type, intermediary_fee_value, customer_requirements, project_overview, attachments, survey_photos } = req.body;
await db.query(
`INSERT INTO budget_projects (name, customer_id, manager_id, location, survey_date, intermediary, intermediary_fee_type, intermediary_fee_value, customer_requirements, project_overview, attachments, survey_photos, status, days_in_status)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 'negotiating', 0)`,
[name, customer_id, manager_id, location, survey_date, intermediary, intermediary_fee_type, intermediary_fee_value, customer_requirements, project_overview, attachments, survey_photos]
);
res.json({ success: true, message: '预算项目创建成功' });
} catch (error) {
res.status(500).json({ success: false, message: error.message });
}
});
app.put('/api/budget-projects/:id/sign', async (req, res) => {
try {
const { id } = req.params;
await db.query('UPDATE budget_projects SET status = ? WHERE id = ?', ['signed', id]);
res.json({ success: true, message: '标记签约成功' });
} catch (error) {
res.status(500).json({ success: false, message: error.message });
}
});
app.put('/api/budget-projects/:id/unsigned', async (req, res) => {
try {
const { id } = req.params;
await db.query('UPDATE budget_projects SET status = ? WHERE id = ?', ['unsigned', id]);
res.json({ success: true, message: '标记未签约成功' });
} catch (error) {
res.status(500).json({ success: false, message: error.message });
}
});
// ==================== 报价管理API ====================
app.post('/api/budget-projects/:projectId/quotations', async (req, res) => {
try {
const { projectId } = req.params;
const { version, quotation_date, amount, currency, status, file_url, remark } = req.body;
await db.query(
`INSERT INTO quotations (project_id, version, quotation_date, amount, currency, status, file_url, remark)
VALUES (?, ?, ?, ?, ?, ?, ?, ?)`,
[projectId, version, quotation_date, amount, currency, status, file_url, remark]
);
res.json({ success: true, message: '报价版本创建成功' });
} catch (error) {
res.status(500).json({ success: false, message: error.message });
}
});
app.delete('/api/budget-projects/:projectId/quotations/:quotationId', async (req, res) => {
try {
const { quotationId } = req.params;
await db.query('DELETE FROM quotations WHERE id = ?', [quotationId]);
res.json({ success: true, message: '报价版本删除成功' });
} catch (error) {
res.status(500).json({ success: false, message: error.message });
}
});
// ==================== 项目合同API ====================
app.post('/api/projects/:projectId/contracts', async (req, res) => {
try {
const { projectId } = req.params;
const { contract_code, contract_amount, currency, settlement_method, contract_period, start_date, end_date, warranty_deposit_percentage, warranty_period, contract_file } = req.body;
await db.query(
`INSERT INTO project_contracts (project_id, contract_code, contract_amount, currency, settlement_method, contract_period, start_date, end_date, warranty_deposit_percentage, warranty_period, contract_file)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
[projectId, contract_code, contract_amount, currency, settlement_method, contract_period, start_date, end_date, warranty_deposit_percentage, warranty_period, contract_file]
);
res.json({ success: true, message: '合同创建成功' });
} catch (error) {
res.status(500).json({ success: false, message: error.message });
}
});
app.get('/api/projects/:projectId/contracts', async (req, res) => {
try {
const { projectId } = req.params;
const result = await db.query('SELECT * FROM project_contracts WHERE project_id = ?', [projectId]);
res.json({ success: true, data: result.rows });
} catch (error) {
res.status(500).json({ success: false, message: error.message });
}
});
// ==================== 分包管理API ====================
app.post('/api/projects/:projectId/subcontracts', async (req, res) => {
try {
const { projectId } = req.params;
const { subcontractor_name, contract_amount, start_date, end_date, contract_file } = req.body;
await db.query(
`INSERT INTO subcontracts (project_id, subcontractor_name, contract_amount, start_date, end_date, contract_file)
VALUES (?, ?, ?, ?, ?, ?)`,
[projectId, subcontractor_name, contract_amount, start_date, end_date, contract_file]
);
res.json({ success: true, message: '分包合同创建成功' });
} catch (error) {
res.status(500).json({ success: false, message: error.message });
}
});
app.get('/api/projects/:projectId/subcontracts', async (req, res) => {
try {
const { projectId } = req.params;
const result = await db.query('SELECT * FROM subcontracts WHERE project_id = ?', [projectId]);
res.json({ success: true, data: result.rows });
} catch (error) {
res.status(500).json({ success: false, message: error.message });
}
});
// ==================== 材料管理API ====================
app.post('/api/projects/:projectId/materials', async (req, res) => {
try {
const { projectId } = req.params;
const { product_id, product_name, unit, budget_quantity, purchase_quantity, used_quantity, average_price, total_amount } = req.body;
await db.query(
`INSERT INTO project_materials (project_id, product_id, product_name, unit, budget_quantity, purchase_quantity, used_quantity, average_price, total_amount)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`,
[projectId, product_id, product_name, unit, budget_quantity, purchase_quantity, used_quantity, average_price, total_amount]
);
res.json({ success: true, message: '材料添加成功' });
} catch (error) {
res.status(500).json({ success: false, message: error.message });
}
});
app.get('/api/projects/:projectId/materials', async (req, res) => {
try {
const { projectId } = req.params;
const result = await db.query('SELECT * FROM project_materials WHERE project_id = ?', [projectId]);
res.json({ success: true, data: result.rows });
} catch (error) {
res.status(500).json({ success: false, message: error.message });
}
});
// ==================== 施工节点API ====================
app.post('/api/projects/:projectId/milestones', async (req, res) => {
try {
const { projectId } = req.params;
const { milestone_name, percentage, amount, completion_progress, status, expected_date, voucher } = req.body;
await db.query(
`INSERT INTO project_milestones (project_id, milestone_name, percentage, amount, completion_progress, status, expected_date, voucher)
VALUES (?, ?, ?, ?, ?, ?, ?, ?)`,
[projectId, milestone_name, percentage, amount, completion_progress, status, expected_date, voucher]
);
res.json({ success: true, message: '节点添加成功' });
} catch (error) {
res.status(500).json({ success: false, message: error.message });
}
});
app.get('/api/projects/:projectId/milestones', async (req, res) => {
try {
const { projectId } = req.params;
const result = await db.query('SELECT * FROM project_milestones WHERE project_id = ?', [projectId]);
res.json({ success: true, data: result.rows });
} catch (error) {
res.status(500).json({ success: false, message: error.message });
}
});
// ==================== 财务信息API ====================
app.post('/api/projects/:projectId/finances', async (req, res) => {
try {
const { projectId } = req.params;
const { payment_type, amount, currency, payment_date, status, description } = req.body;
await db.query(
`INSERT INTO project_finances (project_id, payment_type, amount, currency, payment_date, status, description)
VALUES (?, ?, ?, ?, ?, ?, ?)`,
[projectId, payment_type, amount, currency, payment_date, status, description]
);
res.json({ success: true, message: '财务记录添加成功' });
} catch (error) {
res.status(500).json({ success: false, message: error.message });
}
});
app.get('/api/projects/:projectId/finances', async (req, res) => {
try {
const { projectId } = req.params;
const result = await db.query('SELECT * FROM project_finances WHERE project_id = ?', [projectId]);
res.json({ success: true, data: result.rows });
} catch (error) {
res.status(500).json({ success: false, message: error.message });
}
});
// ==================== 质保金API ====================
app.post('/api/projects/:projectId/warranty-deposits', async (req, res) => {
try {
const { projectId } = req.params;
const { amount, currency, warranty_period, start_date, end_date } = req.body;
await db.query(
`INSERT INTO warranty_deposits (project_id, amount, currency, warranty_period, start_date, end_date)
VALUES (?, ?, ?, ?, ?, ?)`,
[projectId, amount, currency, warranty_period, start_date, end_date]
);
res.json({ success: true, message: '质保金设置成功' });
} catch (error) {
res.status(500).json({ success: false, message: error.message });
}
});
app.get('/api/projects/:projectId/warranty-deposits', async (req, res) => {
try {
const { projectId } = req.params;
const result = await db.query('SELECT * FROM warranty_deposits WHERE project_id = ?', [projectId]);
res.json({ success: true, data: result.rows });
} catch (error) {
res.status(500).json({ success: false, message: error.message });
}
});
// ==================== 施工日志API ====================
app.post('/api/projects/:projectId/construction-logs', async (req, res) => {
try {
const { projectId } = req.params;
const { log_date, weather, work_content, photos } = req.body;
await db.query(
`INSERT INTO construction_logs (project_id, log_date, weather, work_content, photos)
VALUES (?, ?, ?, ?, ?)`,
[projectId, log_date, weather, work_content, photos]
);
res.json({ success: true, message: '施工日志添加成功' });
} catch (error) {
res.status(500).json({ success: false, message: error.message });
}
});
app.get('/api/projects/:projectId/construction-logs', async (req, res) => {
try {
const { projectId } = req.params;
const result = await db.query('SELECT * FROM construction_logs WHERE project_id = ? ORDER BY log_date DESC', [projectId]);
res.json({ success: true, data: result.rows });
} catch (error) {
res.status(500).json({ success: false, message: error.message });
}
});
app.delete('/api/construction-logs/:logId', async (req, res) => {
try {
const { logId } = req.params;
await db.query('DELETE FROM construction_logs WHERE id = ?', [logId]);
res.json({ success: true, message: '施工日志删除成功' });
} catch (error) {
res.status(500).json({ success: false, message: error.message });
}
});
// ==================== 创建预支申请 ====================
app.post('/api/advances', [
body('amount').isFloat({ min: 0.01 }),
body('reason').notEmpty()
], validate, async (req, res) => {
try {
const { amount, reason, project_id } = req.body;
const user_id = 1; // 临时使用admin用户
const result = await db.query(
'INSERT INTO advances (user_id, project_id, amount, reason, status) VALUES (?, ?, ?, ?, ?)',
[user_id, project_id, amount, reason, 'pending']
);
// SQLite不支持RETURNING,所以需要查询刚插入的数据
const lastInsert = await db.query('SELECT * FROM advances ORDER BY id DESC LIMIT 1');
res.json({ success: true, data: lastInsert.rows[0] });
} catch (error) {
res.status(500).json({ success: false, message: error.message });
}
});
// ==================== 创建报销申请 ====================
app.post('/api/reimbursements', [
body('amount').isFloat({ min: 0.01 }),
body('type').notEmpty()
], validate, async (req, res) => {
try {
const { amount, type, description, project_id } = req.body;
const user_id = 1; // 临时使用admin用户
const result = await db.query(
'INSERT INTO reimbursements (user_id, project_id, amount, type, description, status) VALUES (?, ?, ?, ?, ?, ?)',
[user_id, project_id, amount, type, description, 'pending']
);
// SQLite不支持RETURNING,所以需要查询刚插入的数据
const lastInsert = await db.query('SELECT * FROM reimbursements ORDER BY id DESC LIMIT 1');
res.json({ success: true, data: lastInsert.rows[0] });
} catch (error) {
res.status(500).json({ success: false, message: error.message });
}
});
console.log('轻远电力老挝ERP API服务已启动 - 完整版');