2342 lines
71 KiB
JavaScript
2342 lines
71 KiB
JavaScript
const express = require('express');
|
|||
|
|
const cors = require('cors');
|
||
|
|
const path = require('path');
|
||
|
|
const dotenv = require('dotenv');
|
||
|
|
const db = require('./db-sqlite');
|
||
|
|
|
||
|
|
// 加载环境变量
|
||
|
|
dotenv.config();
|
||
|
|
|
||
|
|
const app = express();
|
||
|
|
const PORT = 3004;
|
||
|
|
|
||
|
|
// 中间件
|
||
|
|
app.use(cors());
|
||
|
|
app.use(express.json());
|
||
|
|
app.use(express.urlencoded({ extended: true }));
|
||
|
|
|
||
|
|
// 静态文件服务 - 前端应用
|
||
|
|
app.use(express.static(path.join(__dirname, '../frontend/dist')));
|
||
|
|
|
||
|
|
// ==================== 健康检查 ====================
|
||
|
|
app.get('/api/health', (req, res) => {
|
||
|
|
res.json({
|
||
|
|
success: true,
|
||
|
|
message: '公司财务管理系统 API',
|
||
|
|
version: '1.0.0',
|
||
|
|
timestamp: new Date().toISOString(),
|
||
|
|
endpoints: {
|
||
|
|
upload: "/api/upload",
|
||
|
|
health: '/api/health',
|
||
|
|
auth: '/api/auth',
|
||
|
|
customers: '/api/customers',
|
||
|
|
suppliers: '/api/suppliers',
|
||
|
|
projects: '/api/projects',
|
||
|
|
products: '/api/products',
|
||
|
|
payment_nodes: '/api/payment-nodes',
|
||
|
|
payment_records: '/api/payment-records',
|
||
|
|
exchange_rates: '/api/exchange-rates',
|
||
|
|
advances: '/api/advances',
|
||
|
|
reimbursements: '/api/reimbursements',
|
||
|
|
finance_stats: '/api/finance-stats'
|
||
|
|
}
|
||
|
|
});
|
||
|
|
});
|
||
|
|
|
||
|
|
// ==================== 认证API ====================
|
||
|
|
app.post('/api/auth/login', async (req, res) => {
|
||
|
|
try {
|
||
|
|
const { username, password } = req.body;
|
||
|
|
|
||
|
|
// 简单认证逻辑(生产环境应使用JWT和密码哈希)
|
||
|
|
if (username === 'admin' && password === 'X123c321@') {
|
||
|
|
res.json({
|
||
|
|
success: true,
|
||
|
|
data: {
|
||
|
|
id: 1,
|
||
|
|
username: 'admin',
|
||
|
|
name: '系统管理员',
|
||
|
|
role: 'admin',
|
||
|
|
department: '管理部'
|
||
|
|
}
|
||
|
|
});
|
||
|
|
} else if (username === 'manager' && password === 'X123c321@') {
|
||
|
|
res.json({
|
||
|
|
success: true,
|
||
|
|
data: {
|
||
|
|
id: 2,
|
||
|
|
username: 'manager',
|
||
|
|
name: '罗仕林',
|
||
|
|
role: 'manager',
|
||
|
|
department: '业务部'
|
||
|
|
}
|
||
|
|
});
|
||
|
|
} else if (username === 'pm1' && password === 'X123c321@') {
|
||
|
|
res.json({
|
||
|
|
success: true,
|
||
|
|
data: {
|
||
|
|
id: 3,
|
||
|
|
username: 'pm1',
|
||
|
|
name: '张三',
|
||
|
|
role: 'user',
|
||
|
|
department: '项目部'
|
||
|
|
}
|
||
|
|
});
|
||
|
|
} else {
|
||
|
|
res.status(401).json({
|
||
|
|
success: false,
|
||
|
|
message: '用户名或密码错误'
|
||
|
|
});
|
||
|
|
}
|
||
|
|
} catch (error) {
|
||
|
|
console.error('登录失败:', error);
|
||
|
|
res.status(500).json({
|
||
|
|
success: false,
|
||
|
|
message: '登录失败',
|
||
|
|
error: error.message
|
||
|
|
});
|
||
|
|
}
|
||
|
|
});
|
||
|
|
|
||
|
|
// ==================== 用户管理API ====================
|
||
|
|
app.get('/api/users', async (req, res) => {
|
||
|
|
try {
|
||
|
|
// 模拟用户数据
|
||
|
|
const users = [
|
||
|
|
{
|
||
|
|
id: 1,
|
||
|
|
username: 'admin',
|
||
|
|
name: '系统管理员',
|
||
|
|
role: 'admin',
|
||
|
|
department: '管理部'
|
||
|
|
},
|
||
|
|
{
|
||
|
|
id: 2,
|
||
|
|
username: 'manager',
|
||
|
|
name: '罗仕林',
|
||
|
|
role: 'manager',
|
||
|
|
department: '业务部'
|
||
|
|
},
|
||
|
|
{
|
||
|
|
id: 3,
|
||
|
|
username: 'pm1',
|
||
|
|
name: '张三',
|
||
|
|
role: 'user',
|
||
|
|
department: '项目部'
|
||
|
|
},
|
||
|
|
{
|
||
|
|
id: 4,
|
||
|
|
username: 'pm2',
|
||
|
|
name: '李四',
|
||
|
|
role: 'user',
|
||
|
|
department: '项目部'
|
||
|
|
},
|
||
|
|
{
|
||
|
|
id: 5,
|
||
|
|
username: 'finance',
|
||
|
|
name: '王五',
|
||
|
|
role: 'user',
|
||
|
|
department: '财务部'
|
||
|
|
}
|
||
|
|
];
|
||
|
|
|
||
|
|
res.json({
|
||
|
|
success: true,
|
||
|
|
data: users
|
||
|
|
});
|
||
|
|
} catch (error) {
|
||
|
|
console.error('获取用户列表失败:', error);
|
||
|
|
res.status(500).json({
|
||
|
|
success: false,
|
||
|
|
message: '获取用户列表失败',
|
||
|
|
error: error.message
|
||
|
|
});
|
||
|
|
}
|
||
|
|
});
|
||
|
|
|
||
|
|
// ==================== 客户管理API ====================
|
||
|
|
app.get('/api/customers', async (req, res) => {
|
||
|
|
try {
|
||
|
|
const result = await db.query(`
|
||
|
|
SELECT * FROM customers
|
||
|
|
ORDER BY created_at DESC
|
||
|
|
LIMIT 50
|
||
|
|
`);
|
||
|
|
|
||
|
|
res.json({
|
||
|
|
success: true,
|
||
|
|
data: result.rows,
|
||
|
|
count: result.rows.length
|
||
|
|
});
|
||
|
|
} catch (error) {
|
||
|
|
console.error('获取客户失败:', error);
|
||
|
|
res.status(500).json({
|
||
|
|
success: false,
|
||
|
|
message: '获取客户失败',
|
||
|
|
error: error.message
|
||
|
|
});
|
||
|
|
}
|
||
|
|
});
|
||
|
|
|
||
|
|
// ==================== 客户管理API ====================
|
||
|
|
app.get('/api/customers', async (req, res) => {
|
||
|
|
try {
|
||
|
|
const result = await db.query(`
|
||
|
|
SELECT * FROM customers
|
||
|
|
ORDER BY created_at DESC
|
||
|
|
LIMIT 50
|
||
|
|
`);
|
||
|
|
|
||
|
|
res.json({
|
||
|
|
success: true,
|
||
|
|
data: result.rows,
|
||
|
|
count: result.rows.length
|
||
|
|
});
|
||
|
|
} catch (error) {
|
||
|
|
console.error('获取客户失败:', error);
|
||
|
|
res.status(500).json({
|
||
|
|
success: false,
|
||
|
|
message: '获取客户失败',
|
||
|
|
error: error.message
|
||
|
|
});
|
||
|
|
}
|
||
|
|
});
|
||
|
|
|
||
|
|
app.get('/api/customers/:id', async (req, res) => {
|
||
|
|
try {
|
||
|
|
const { id } = req.params;
|
||
|
|
|
||
|
|
// 获取客户基本信息
|
||
|
|
const customerResult = await db.query(`
|
||
|
|
SELECT * FROM customers
|
||
|
|
WHERE id = ?
|
||
|
|
`, [id]);
|
||
|
|
|
||
|
|
if (customerResult.rows.length > 0) {
|
||
|
|
const customer = customerResult.rows[0];
|
||
|
|
|
||
|
|
// 获取客户的所有联系人
|
||
|
|
const contactsResult = await db.query(`
|
||
|
|
SELECT * FROM contacts
|
||
|
|
WHERE entity_id = ? AND entity_type = 'customer'
|
||
|
|
ORDER BY is_primary DESC
|
||
|
|
`, [id]);
|
||
|
|
|
||
|
|
// 转换联系人数据结构
|
||
|
|
const contacts = contactsResult.rows.map(contact => ({
|
||
|
|
name: contact.name || '未命名',
|
||
|
|
position: contact.position || '',
|
||
|
|
phone: contact.phone || '',
|
||
|
|
is_primary: contact.is_primary === 1
|
||
|
|
}));
|
||
|
|
|
||
|
|
// 转换数据结构以匹配前端期望
|
||
|
|
const formattedCustomer = {
|
||
|
|
id: customer.id,
|
||
|
|
code: `C${String(customer.id).padStart(4, '0')}`, // 生成客户编号
|
||
|
|
name: customer.name,
|
||
|
|
address: customer.address,
|
||
|
|
contacts: contacts.length > 0 ? contacts : [], // 使用从联系人表获取的联系人
|
||
|
|
remark: customer.remark || '', // 默认为空
|
||
|
|
total_contract_amount: 0, // 默认为0
|
||
|
|
total_received: 0, // 默认为0
|
||
|
|
total_receivable: 0, // 默认为0
|
||
|
|
created_at: customer.created_at
|
||
|
|
};
|
||
|
|
|
||
|
|
res.json({
|
||
|
|
success: true,
|
||
|
|
data: formattedCustomer
|
||
|
|
});
|
||
|
|
} else {
|
||
|
|
res.status(404).json({
|
||
|
|
success: false,
|
||
|
|
message: '客户不存在'
|
||
|
|
});
|
||
|
|
}
|
||
|
|
} catch (error) {
|
||
|
|
console.error('获取客户详情失败:', error);
|
||
|
|
res.status(500).json({
|
||
|
|
success: false,
|
||
|
|
message: '获取客户详情失败',
|
||
|
|
error: error.message
|
||
|
|
});
|
||
|
|
}
|
||
|
|
});
|
||
|
|
|
||
|
|
app.post('/api/customers', async (req, res) => {
|
||
|
|
try {
|
||
|
|
const { name, address, remark, contacts } = req.body;
|
||
|
|
|
||
|
|
// 从contacts中获取主联系人信息
|
||
|
|
const primaryContact = contacts?.find(c => c.is_primary) || contacts?.[0];
|
||
|
|
const contact = primaryContact?.name || '';
|
||
|
|
const position = primaryContact?.position || '';
|
||
|
|
const phone = primaryContact?.phone || '';
|
||
|
|
const email = ''; // 前端没有email字段
|
||
|
|
|
||
|
|
const result = await db.query(
|
||
|
|
`INSERT INTO customers (name, address, contact, position, phone, email, remark, created_at, updated_at)
|
||
|
|
VALUES (?, ?, ?, ?, ?, ?, ?, datetime('now'), datetime('now'))`,
|
||
|
|
[name, address, contact, position, phone, email, remark]
|
||
|
|
);
|
||
|
|
|
||
|
|
const customerId = result.lastID;
|
||
|
|
|
||
|
|
// 插入联系人数据
|
||
|
|
if (contacts && contacts.length > 0) {
|
||
|
|
for (const contactItem of contacts) {
|
||
|
|
await db.query(
|
||
|
|
`INSERT INTO contacts (entity_id, entity_type, name, position, phone, is_primary, created_at, updated_at)
|
||
|
|
VALUES (?, ?, ?, ?, ?, ?, datetime('now'), datetime('now'))`,
|
||
|
|
[customerId, 'customer', contactItem.name, contactItem.position, contactItem.phone, contactItem.is_primary ? 1 : 0]
|
||
|
|
);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
res.json({
|
||
|
|
success: true,
|
||
|
|
message: '客户创建成功',
|
||
|
|
data: {
|
||
|
|
id: customerId,
|
||
|
|
code: `C${String(customerId).padStart(4, '0')}`,
|
||
|
|
name,
|
||
|
|
address,
|
||
|
|
contacts: contacts || [],
|
||
|
|
remark,
|
||
|
|
total_contract_amount: 0,
|
||
|
|
total_received: 0,
|
||
|
|
total_receivable: 0,
|
||
|
|
created_at: new Date().toISOString()
|
||
|
|
}
|
||
|
|
});
|
||
|
|
} catch (error) {
|
||
|
|
console.error('创建客户失败:', error);
|
||
|
|
res.status(500).json({
|
||
|
|
success: false,
|
||
|
|
message: '创建客户失败',
|
||
|
|
error: error.message
|
||
|
|
});
|
||
|
|
}
|
||
|
|
});
|
||
|
|
|
||
|
|
app.put('/api/customers/:id', async (req, res) => {
|
||
|
|
try {
|
||
|
|
const { id } = req.params;
|
||
|
|
const { name, address, remark, contacts } = req.body;
|
||
|
|
|
||
|
|
// 从contacts中获取主联系人信息
|
||
|
|
const primaryContact = contacts?.find(c => c.is_primary) || contacts?.[0];
|
||
|
|
const contact = primaryContact?.name || '';
|
||
|
|
const position = primaryContact?.position || '';
|
||
|
|
const phone = primaryContact?.phone || '';
|
||
|
|
const email = ''; // 前端没有email字段
|
||
|
|
|
||
|
|
await db.query(
|
||
|
|
`UPDATE customers
|
||
|
|
SET name = ?, address = ?, contact = ?, position = ?, phone = ?, email = ?, remark = ?, updated_at = datetime('now')
|
||
|
|
WHERE id = ?`,
|
||
|
|
[name, address, contact, position, phone, email, remark, id]
|
||
|
|
);
|
||
|
|
|
||
|
|
// 删除旧的联系人数据
|
||
|
|
await db.query(`DELETE FROM contacts WHERE entity_id = ? AND entity_type = 'customer'`, [id]);
|
||
|
|
|
||
|
|
// 插入新的联系人数据
|
||
|
|
if (contacts && contacts.length > 0) {
|
||
|
|
for (const contactItem of contacts) {
|
||
|
|
await db.query(
|
||
|
|
`INSERT INTO contacts (entity_id, entity_type, name, position, phone, is_primary, created_at, updated_at)
|
||
|
|
VALUES (?, ?, ?, ?, ?, ?, datetime('now'), datetime('now'))`,
|
||
|
|
[id, 'customer', contactItem.name, contactItem.position, contactItem.phone, contactItem.is_primary ? 1 : 0]
|
||
|
|
);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
res.json({
|
||
|
|
success: true,
|
||
|
|
message: '客户更新成功',
|
||
|
|
data: {
|
||
|
|
id,
|
||
|
|
code: `C${String(id).padStart(4, '0')}`,
|
||
|
|
name,
|
||
|
|
address,
|
||
|
|
contacts: contacts || [],
|
||
|
|
remark,
|
||
|
|
total_contract_amount: 0,
|
||
|
|
total_received: 0,
|
||
|
|
total_receivable: 0,
|
||
|
|
created_at: new Date().toISOString()
|
||
|
|
}
|
||
|
|
});
|
||
|
|
} catch (error) {
|
||
|
|
console.error('更新客户失败:', error);
|
||
|
|
res.status(500).json({
|
||
|
|
success: false,
|
||
|
|
message: '更新客户失败',
|
||
|
|
error: error.message
|
||
|
|
});
|
||
|
|
}
|
||
|
|
});
|
||
|
|
|
||
|
|
app.delete('/api/customers/:id', async (req, res) => {
|
||
|
|
try {
|
||
|
|
const { id } = req.params;
|
||
|
|
|
||
|
|
// 先删除关联的联系人数据
|
||
|
|
await db.query(`DELETE FROM contacts WHERE entity_id = ? AND entity_type = 'customer'`, [id]);
|
||
|
|
|
||
|
|
// 再删除客户数据
|
||
|
|
const result = await db.query(`DELETE FROM customers WHERE id = ?`, [id]);
|
||
|
|
|
||
|
|
if (result.changes > 0) {
|
||
|
|
res.json({
|
||
|
|
success: true,
|
||
|
|
message: '客户删除成功'
|
||
|
|
});
|
||
|
|
} else {
|
||
|
|
res.status(404).json({
|
||
|
|
success: false,
|
||
|
|
message: '客户不存在'
|
||
|
|
});
|
||
|
|
}
|
||
|
|
} catch (error) {
|
||
|
|
console.error('删除客户失败:', error);
|
||
|
|
res.status(500).json({
|
||
|
|
success: false,
|
||
|
|
message: '删除客户失败',
|
||
|
|
error: error.message
|
||
|
|
});
|
||
|
|
}
|
||
|
|
});
|
||
|
|
|
||
|
|
// ==================== 供应商管理API ====================
|
||
|
|
app.get('/api/suppliers', async (req, res) => {
|
||
|
|
try {
|
||
|
|
const result = await db.query(`
|
||
|
|
SELECT * FROM suppliers
|
||
|
|
ORDER BY created_at DESC
|
||
|
|
LIMIT 50
|
||
|
|
`);
|
||
|
|
|
||
|
|
res.json({
|
||
|
|
success: true,
|
||
|
|
data: result.rows,
|
||
|
|
count: result.rows.length
|
||
|
|
});
|
||
|
|
} catch (error) {
|
||
|
|
console.error('获取供应商失败:', error);
|
||
|
|
res.status(500).json({
|
||
|
|
success: false,
|
||
|
|
message: '获取供应商失败',
|
||
|
|
error: error.message
|
||
|
|
});
|
||
|
|
}
|
||
|
|
});
|
||
|
|
|
||
|
|
app.get('/api/suppliers/:id', async (req, res) => {
|
||
|
|
try {
|
||
|
|
const { id } = req.params;
|
||
|
|
|
||
|
|
// 获取供应商基本信息
|
||
|
|
const supplierResult = await db.query(`
|
||
|
|
SELECT * FROM suppliers
|
||
|
|
WHERE id = ?
|
||
|
|
`, [id]);
|
||
|
|
|
||
|
|
if (supplierResult.rows.length > 0) {
|
||
|
|
const supplier = supplierResult.rows[0];
|
||
|
|
|
||
|
|
// 获取供应商的所有联系人
|
||
|
|
const contactsResult = await db.query(`
|
||
|
|
SELECT * FROM contacts
|
||
|
|
WHERE entity_id = ? AND entity_type = 'supplier'
|
||
|
|
ORDER BY is_primary DESC
|
||
|
|
`, [id]);
|
||
|
|
|
||
|
|
// 转换联系人数据结构
|
||
|
|
const contacts = contactsResult.rows.map(contact => ({
|
||
|
|
name: contact.name || '未命名',
|
||
|
|
position: contact.position || '',
|
||
|
|
phone: contact.phone || '',
|
||
|
|
is_primary: contact.is_primary === 1
|
||
|
|
}));
|
||
|
|
|
||
|
|
// 转换数据结构以匹配前端期望
|
||
|
|
const formattedSupplier = {
|
||
|
|
id: supplier.id,
|
||
|
|
code: `S${String(supplier.id).padStart(4, '0')}`, // 生成供应商编号
|
||
|
|
name: supplier.name || '未命名',
|
||
|
|
supply_category: supplier.supply_category || '电力设备', // 默认为电力设备
|
||
|
|
country: supplier.country || 'Laos', // 默认为老挝
|
||
|
|
contacts: contacts.length > 0 ? contacts : [], // 使用从联系人表获取的联系人
|
||
|
|
remark: supplier.remark || '', // 默认为空
|
||
|
|
total_purchase_amount: 0, // 默认为0
|
||
|
|
total_paid: 0, // 默认为0
|
||
|
|
total_payable: 0, // 默认为0
|
||
|
|
created_at: supplier.created_at
|
||
|
|
};
|
||
|
|
|
||
|
|
// 设置响应头确保UTF-8编码
|
||
|
|
res.setHeader('Content-Type', 'application/json; charset=utf-8');
|
||
|
|
res.json({
|
||
|
|
success: true,
|
||
|
|
data: formattedSupplier
|
||
|
|
});
|
||
|
|
} else {
|
||
|
|
res.status(404).json({
|
||
|
|
success: false,
|
||
|
|
message: '供应商不存在'
|
||
|
|
});
|
||
|
|
}
|
||
|
|
} catch (error) {
|
||
|
|
console.error('获取供应商详情失败:', error);
|
||
|
|
res.status(500).json({
|
||
|
|
success: false,
|
||
|
|
message: '获取供应商详情失败',
|
||
|
|
error: error.message
|
||
|
|
});
|
||
|
|
}
|
||
|
|
});
|
||
|
|
|
||
|
|
app.post('/api/suppliers', async (req, res) => {
|
||
|
|
try {
|
||
|
|
const { name, supply_category, country, remark, contacts } = req.body;
|
||
|
|
|
||
|
|
// 从contacts中获取主联系人信息
|
||
|
|
const primaryContact = contacts?.find(c => c.is_primary) || contacts?.[0];
|
||
|
|
const contact = primaryContact?.name || '';
|
||
|
|
const position = primaryContact?.position || '';
|
||
|
|
const phone = primaryContact?.phone || '';
|
||
|
|
const email = ''; // 前端没有email字段
|
||
|
|
const address = ''; // 前端没有address字段
|
||
|
|
|
||
|
|
const result = await db.query(
|
||
|
|
`INSERT INTO suppliers (name, address, contact, position, phone, email, supply_category, country, remark, created_at, updated_at)
|
||
|
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, datetime('now'), datetime('now'))`,
|
||
|
|
[name, address, contact, position, phone, email, supply_category, country, remark]
|
||
|
|
);
|
||
|
|
|
||
|
|
const supplierId = result.lastID;
|
||
|
|
|
||
|
|
// 插入联系人数据
|
||
|
|
if (contacts && contacts.length > 0) {
|
||
|
|
for (const contactItem of contacts) {
|
||
|
|
await db.query(
|
||
|
|
`INSERT INTO contacts (entity_id, entity_type, name, position, phone, is_primary, created_at, updated_at)
|
||
|
|
VALUES (?, ?, ?, ?, ?, ?, datetime('now'), datetime('now'))`,
|
||
|
|
[supplierId, 'supplier', contactItem.name, contactItem.position, contactItem.phone, contactItem.is_primary ? 1 : 0]
|
||
|
|
);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
res.json({
|
||
|
|
success: true,
|
||
|
|
message: '供应商创建成功',
|
||
|
|
data: {
|
||
|
|
id: supplierId,
|
||
|
|
code: `S${String(supplierId).padStart(4, '0')}`,
|
||
|
|
name,
|
||
|
|
supply_category,
|
||
|
|
country,
|
||
|
|
contacts: contacts || [],
|
||
|
|
remark,
|
||
|
|
total_purchase_amount: 0,
|
||
|
|
total_paid: 0,
|
||
|
|
total_payable: 0,
|
||
|
|
created_at: new Date().toISOString()
|
||
|
|
}
|
||
|
|
});
|
||
|
|
} catch (error) {
|
||
|
|
console.error('创建供应商失败:', error);
|
||
|
|
res.status(500).json({
|
||
|
|
success: false,
|
||
|
|
message: '创建供应商失败',
|
||
|
|
error: error.message
|
||
|
|
});
|
||
|
|
}
|
||
|
|
});
|
||
|
|
|
||
|
|
app.put('/api/suppliers/:id', async (req, res) => {
|
||
|
|
try {
|
||
|
|
const { id } = req.params;
|
||
|
|
const { name, supply_category, country, remark, contacts } = req.body;
|
||
|
|
|
||
|
|
// 从contacts中获取主联系人信息
|
||
|
|
const primaryContact = contacts?.find(c => c.is_primary) || contacts?.[0];
|
||
|
|
const contact = primaryContact?.name || '';
|
||
|
|
const position = primaryContact?.position || '';
|
||
|
|
const phone = primaryContact?.phone || '';
|
||
|
|
const email = ''; // 前端没有email字段
|
||
|
|
const address = ''; // 前端没有address字段
|
||
|
|
|
||
|
|
await db.query(
|
||
|
|
`UPDATE suppliers
|
||
|
|
SET name = ?, address = ?, contact = ?, position = ?, phone = ?, email = ?, supply_category = ?, country = ?, remark = ?, updated_at = datetime('now')
|
||
|
|
WHERE id = ?`,
|
||
|
|
[name, address, contact, position, phone, email, supply_category, country, remark, id]
|
||
|
|
);
|
||
|
|
|
||
|
|
// 删除旧的联系人数据
|
||
|
|
await db.query(`DELETE FROM contacts WHERE entity_id = ? AND entity_type = 'supplier'`, [id]);
|
||
|
|
|
||
|
|
// 插入新的联系人数据
|
||
|
|
if (contacts && contacts.length > 0) {
|
||
|
|
for (const contactItem of contacts) {
|
||
|
|
await db.query(
|
||
|
|
`INSERT INTO contacts (entity_id, entity_type, name, position, phone, is_primary, created_at, updated_at)
|
||
|
|
VALUES (?, ?, ?, ?, ?, ?, datetime('now'), datetime('now'))`,
|
||
|
|
[id, 'supplier', contactItem.name, contactItem.position, contactItem.phone, contactItem.is_primary ? 1 : 0]
|
||
|
|
);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
res.json({
|
||
|
|
success: true,
|
||
|
|
message: '供应商更新成功',
|
||
|
|
data: {
|
||
|
|
id,
|
||
|
|
code: `S${String(id).padStart(4, '0')}`,
|
||
|
|
name,
|
||
|
|
supply_category,
|
||
|
|
country,
|
||
|
|
contacts: contacts || [],
|
||
|
|
remark,
|
||
|
|
total_purchase_amount: 0,
|
||
|
|
total_paid: 0,
|
||
|
|
total_payable: 0,
|
||
|
|
created_at: new Date().toISOString()
|
||
|
|
}
|
||
|
|
});
|
||
|
|
} catch (error) {
|
||
|
|
console.error('更新供应商失败:', error);
|
||
|
|
res.status(500).json({
|
||
|
|
success: false,
|
||
|
|
message: '更新供应商失败',
|
||
|
|
error: error.message
|
||
|
|
});
|
||
|
|
}
|
||
|
|
});
|
||
|
|
|
||
|
|
app.delete('/api/suppliers/:id', async (req, res) => {
|
||
|
|
try {
|
||
|
|
const { id } = req.params;
|
||
|
|
|
||
|
|
// 先删除关联的联系人数据
|
||
|
|
await db.query(`DELETE FROM contacts WHERE entity_id = ? AND entity_type = 'supplier'`, [id]);
|
||
|
|
|
||
|
|
// 再删除供应商数据
|
||
|
|
const result = await db.query(`DELETE FROM suppliers WHERE id = ?`, [id]);
|
||
|
|
|
||
|
|
if (result.changes > 0) {
|
||
|
|
res.json({
|
||
|
|
success: true,
|
||
|
|
message: '供应商删除成功'
|
||
|
|
});
|
||
|
|
} else {
|
||
|
|
res.status(404).json({
|
||
|
|
success: false,
|
||
|
|
message: '供应商不存在'
|
||
|
|
});
|
||
|
|
}
|
||
|
|
} catch (error) {
|
||
|
|
console.error('删除供应商失败:', error);
|
||
|
|
res.status(500).json({
|
||
|
|
success: false,
|
||
|
|
message: '删除供应商失败',
|
||
|
|
error: error.message
|
||
|
|
});
|
||
|
|
}
|
||
|
|
});
|
||
|
|
|
||
|
|
// ==================== 分包商管理API ====================
|
||
|
|
app.get('/api/subcontractors', async (req, res) => {
|
||
|
|
try {
|
||
|
|
const result = await db.query(`
|
||
|
|
SELECT * FROM subcontractors
|
||
|
|
ORDER BY created_at DESC
|
||
|
|
LIMIT 50
|
||
|
|
`);
|
||
|
|
|
||
|
|
res.json({
|
||
|
|
success: true,
|
||
|
|
data: result.rows,
|
||
|
|
count: result.rows.length
|
||
|
|
});
|
||
|
|
} catch (error) {
|
||
|
|
console.error('获取分包商失败:', error);
|
||
|
|
res.status(500).json({
|
||
|
|
success: false,
|
||
|
|
message: '获取分包商失败',
|
||
|
|
error: error.message
|
||
|
|
});
|
||
|
|
}
|
||
|
|
});
|
||
|
|
|
||
|
|
app.get('/api/subcontractors/:id', async (req, res) => {
|
||
|
|
try {
|
||
|
|
const { id } = req.params;
|
||
|
|
|
||
|
|
// 获取分包商基本信息
|
||
|
|
const subcontractorResult = await db.query(`
|
||
|
|
SELECT * FROM subcontractors
|
||
|
|
WHERE id = ?
|
||
|
|
`, [id]);
|
||
|
|
|
||
|
|
if (subcontractorResult.rows.length > 0) {
|
||
|
|
const subcontractor = subcontractorResult.rows[0];
|
||
|
|
|
||
|
|
// 获取分包商的所有联系人
|
||
|
|
const contactsResult = await db.query(`
|
||
|
|
SELECT * FROM contacts
|
||
|
|
WHERE entity_id = ? AND entity_type = 'subcontractor'
|
||
|
|
ORDER BY is_primary DESC
|
||
|
|
`, [id]);
|
||
|
|
|
||
|
|
// 转换联系人数据结构
|
||
|
|
const contacts = contactsResult.rows.map(contact => ({
|
||
|
|
name: contact.name || '未命名',
|
||
|
|
position: contact.position || '',
|
||
|
|
phone: contact.phone || '',
|
||
|
|
is_primary: contact.is_primary === 1
|
||
|
|
}));
|
||
|
|
|
||
|
|
// 转换数据结构以匹配前端期望
|
||
|
|
const formattedSubcontractor = {
|
||
|
|
id: subcontractor.id,
|
||
|
|
code: `SC${String(subcontractor.id).padStart(4, '0')}`, // 生成分包商编号
|
||
|
|
name: subcontractor.name,
|
||
|
|
scope: subcontractor.scope || '', // 默认为空
|
||
|
|
features: subcontractor.features || '', // 默认为空
|
||
|
|
country: subcontractor.country || '', // 默认为空
|
||
|
|
contacts: contacts.length > 0 ? contacts : [], // 使用从联系人表获取的联系人
|
||
|
|
remark: subcontractor.remark || '', // 默认为空
|
||
|
|
total_contract_amount: 0, // 默认为0
|
||
|
|
total_paid: 0, // 默认为0
|
||
|
|
total_payable: 0, // 默认为0
|
||
|
|
created_at: subcontractor.created_at
|
||
|
|
};
|
||
|
|
|
||
|
|
res.json({
|
||
|
|
success: true,
|
||
|
|
data: formattedSubcontractor
|
||
|
|
});
|
||
|
|
} else {
|
||
|
|
res.status(404).json({
|
||
|
|
success: false,
|
||
|
|
message: '分包商不存在'
|
||
|
|
});
|
||
|
|
}
|
||
|
|
} catch (error) {
|
||
|
|
console.error('获取分包商详情失败:', error);
|
||
|
|
res.status(500).json({
|
||
|
|
success: false,
|
||
|
|
message: '获取分包商详情失败',
|
||
|
|
error: error.message
|
||
|
|
});
|
||
|
|
}
|
||
|
|
});
|
||
|
|
|
||
|
|
app.post('/api/subcontractors', async (req, res) => {
|
||
|
|
try {
|
||
|
|
const { name, scope, features, country, remark, contacts } = req.body;
|
||
|
|
|
||
|
|
// 从contacts中获取主联系人信息
|
||
|
|
const primaryContact = contacts?.find(c => c.is_primary) || contacts?.[0];
|
||
|
|
const contact = primaryContact?.name || '';
|
||
|
|
const position = primaryContact?.position || '';
|
||
|
|
const phone = primaryContact?.phone || '';
|
||
|
|
const email = ''; // 前端没有email字段
|
||
|
|
const address = ''; // 前端没有address字段
|
||
|
|
|
||
|
|
const result = await db.query(
|
||
|
|
`INSERT INTO subcontractors (name, address, contact, position, phone, email, scope, features, country, remark, created_at, updated_at)
|
||
|
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, datetime('now'), datetime('now'))`,
|
||
|
|
[name, address, contact, position, phone, email, scope, features, country, remark]
|
||
|
|
);
|
||
|
|
|
||
|
|
const subcontractorId = result.lastID;
|
||
|
|
|
||
|
|
// 插入联系人数据
|
||
|
|
if (contacts && contacts.length > 0) {
|
||
|
|
for (const contactItem of contacts) {
|
||
|
|
await db.query(
|
||
|
|
`INSERT INTO contacts (entity_id, entity_type, name, position, phone, is_primary, created_at, updated_at)
|
||
|
|
VALUES (?, ?, ?, ?, ?, ?, datetime('now'), datetime('now'))`,
|
||
|
|
[subcontractorId, 'subcontractor', contactItem.name, contactItem.position, contactItem.phone, contactItem.is_primary ? 1 : 0]
|
||
|
|
);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
res.json({
|
||
|
|
success: true,
|
||
|
|
message: '分包商创建成功',
|
||
|
|
data: {
|
||
|
|
id: subcontractorId,
|
||
|
|
code: `SC${String(subcontractorId).padStart(4, '0')}`,
|
||
|
|
name,
|
||
|
|
scope,
|
||
|
|
features,
|
||
|
|
country,
|
||
|
|
contacts: contacts || [],
|
||
|
|
remark,
|
||
|
|
total_contract_amount: 0,
|
||
|
|
total_paid: 0,
|
||
|
|
total_payable: 0,
|
||
|
|
created_at: new Date().toISOString()
|
||
|
|
}
|
||
|
|
});
|
||
|
|
} catch (error) {
|
||
|
|
console.error('创建分包商失败:', error);
|
||
|
|
res.status(500).json({
|
||
|
|
success: false,
|
||
|
|
message: '创建分包商失败',
|
||
|
|
error: error.message
|
||
|
|
});
|
||
|
|
}
|
||
|
|
});
|
||
|
|
|
||
|
|
app.put('/api/subcontractors/:id', async (req, res) => {
|
||
|
|
try {
|
||
|
|
const { id } = req.params;
|
||
|
|
const { name, scope, features, country, remark, contacts } = req.body;
|
||
|
|
|
||
|
|
// 从contacts中获取主联系人信息
|
||
|
|
const primaryContact = contacts?.find(c => c.is_primary) || contacts?.[0];
|
||
|
|
const contact = primaryContact?.name || '';
|
||
|
|
const position = primaryContact?.position || '';
|
||
|
|
const phone = primaryContact?.phone || '';
|
||
|
|
const email = ''; // 前端没有email字段
|
||
|
|
const address = ''; // 前端没有address字段
|
||
|
|
|
||
|
|
await db.query(
|
||
|
|
`UPDATE subcontractors
|
||
|
|
SET name = ?, address = ?, contact = ?, position = ?, phone = ?, email = ?, scope = ?, features = ?, country = ?, remark = ?, updated_at = datetime('now')
|
||
|
|
WHERE id = ?`,
|
||
|
|
[name, address, contact, position, phone, email, scope, features, country, remark, id]
|
||
|
|
);
|
||
|
|
|
||
|
|
// 删除旧的联系人数据
|
||
|
|
await db.query(`DELETE FROM contacts WHERE entity_id = ? AND entity_type = 'subcontractor'`, [id]);
|
||
|
|
|
||
|
|
// 插入新的联系人数据
|
||
|
|
if (contacts && contacts.length > 0) {
|
||
|
|
for (const contactItem of contacts) {
|
||
|
|
await db.query(
|
||
|
|
`INSERT INTO contacts (entity_id, entity_type, name, position, phone, is_primary, created_at, updated_at)
|
||
|
|
VALUES (?, ?, ?, ?, ?, ?, datetime('now'), datetime('now'))`,
|
||
|
|
[id, 'subcontractor', contactItem.name, contactItem.position, contactItem.phone, contactItem.is_primary ? 1 : 0]
|
||
|
|
);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
res.json({
|
||
|
|
success: true,
|
||
|
|
message: '分包商更新成功',
|
||
|
|
data: {
|
||
|
|
id,
|
||
|
|
code: `SC${String(id).padStart(4, '0')}`,
|
||
|
|
name,
|
||
|
|
scope,
|
||
|
|
features,
|
||
|
|
country,
|
||
|
|
contacts: contacts || [],
|
||
|
|
remark,
|
||
|
|
total_contract_amount: 0,
|
||
|
|
total_paid: 0,
|
||
|
|
total_payable: 0,
|
||
|
|
created_at: new Date().toISOString()
|
||
|
|
}
|
||
|
|
});
|
||
|
|
} catch (error) {
|
||
|
|
console.error('更新分包商失败:', error);
|
||
|
|
res.status(500).json({
|
||
|
|
success: false,
|
||
|
|
message: '更新分包商失败',
|
||
|
|
error: error.message
|
||
|
|
});
|
||
|
|
}
|
||
|
|
});
|
||
|
|
|
||
|
|
app.delete('/api/subcontractors/:id', async (req, res) => {
|
||
|
|
try {
|
||
|
|
const { id } = req.params;
|
||
|
|
|
||
|
|
// 先删除关联的联系人数据
|
||
|
|
await db.query(`DELETE FROM contacts WHERE entity_id = ? AND entity_type = 'subcontractor'`, [id]);
|
||
|
|
|
||
|
|
// 再删除分包商数据
|
||
|
|
const result = await db.query(`DELETE FROM subcontractors WHERE id = ?`, [id]);
|
||
|
|
|
||
|
|
if (result.changes > 0) {
|
||
|
|
res.json({
|
||
|
|
success: true,
|
||
|
|
message: '分包商删除成功'
|
||
|
|
});
|
||
|
|
} else {
|
||
|
|
res.status(404).json({
|
||
|
|
success: false,
|
||
|
|
message: '分包商不存在'
|
||
|
|
});
|
||
|
|
}
|
||
|
|
} catch (error) {
|
||
|
|
console.error('删除分包商失败:', error);
|
||
|
|
res.status(500).json({
|
||
|
|
success: false,
|
||
|
|
message: '删除分包商失败',
|
||
|
|
error: error.message
|
||
|
|
});
|
||
|
|
}
|
||
|
|
});
|
||
|
|
|
||
|
|
// ==================== 项目管理API ====================
|
||
|
|
app.get('/api/projects', async (req, res) => {
|
||
|
|
try {
|
||
|
|
const result = await db.query(`
|
||
|
|
SELECT
|
||
|
|
p.*,
|
||
|
|
c.name as customer_name
|
||
|
|
FROM projects p
|
||
|
|
LEFT JOIN customers c ON p.customer_id = c.id
|
||
|
|
ORDER BY p.created_at DESC
|
||
|
|
LIMIT 50
|
||
|
|
`);
|
||
|
|
|
||
|
|
res.json({
|
||
|
|
success: true,
|
||
|
|
data: result.rows,
|
||
|
|
count: result.rows.length
|
||
|
|
});
|
||
|
|
} catch (error) {
|
||
|
|
console.error('获取项目失败:', error);
|
||
|
|
res.status(500).json({
|
||
|
|
success: false,
|
||
|
|
message: '获取项目失败',
|
||
|
|
error: error.message
|
||
|
|
});
|
||
|
|
}
|
||
|
|
});
|
||
|
|
|
||
|
|
// ==================== 项目详情API ====================
|
||
|
|
app.get('/api/projects/:id', async (req, res) => {
|
||
|
|
try {
|
||
|
|
const { id } = req.params;
|
||
|
|
|
||
|
|
const result = await db.query(`
|
||
|
|
SELECT
|
||
|
|
p.*,
|
||
|
|
c.name as customer_name
|
||
|
|
FROM projects p
|
||
|
|
LEFT JOIN customers c ON p.customer_id = c.id
|
||
|
|
WHERE p.id = ?
|
||
|
|
`, [id]);
|
||
|
|
|
||
|
|
if (result.rows.length > 0) {
|
||
|
|
const project = result.rows[0];
|
||
|
|
|
||
|
|
res.json({
|
||
|
|
success: true,
|
||
|
|
data: {
|
||
|
|
id: project.id,
|
||
|
|
project_code: project.code || `PROJ-${String(project.id).padStart(4, '0')}`,
|
||
|
|
name: project.name,
|
||
|
|
customer_id: project.customer_id,
|
||
|
|
customer_name: project.customer_name || '未知客户',
|
||
|
|
status: project.status || 'planning',
|
||
|
|
budget: '0',
|
||
|
|
spent: '0',
|
||
|
|
start_date: project.start_date,
|
||
|
|
end_date: project.end_date,
|
||
|
|
description: project.description,
|
||
|
|
contract_type: 'lump_sum',
|
||
|
|
contract_amount: project.contract_amount?.toString() || '0',
|
||
|
|
currency: 'CNY',
|
||
|
|
contract_days: 180,
|
||
|
|
project_manager_id: 1,
|
||
|
|
manager_name: '未知经理',
|
||
|
|
location: project.location || '',
|
||
|
|
work_quantity: '',
|
||
|
|
project_situation: project.description || '',
|
||
|
|
settlement_type: 'lump_sum',
|
||
|
|
has_warranty: true,
|
||
|
|
warranty_amount: (project.contract_amount * 0.05).toString(),
|
||
|
|
warranty_percent: '5',
|
||
|
|
warranty_months: 12,
|
||
|
|
warranty_start_date: project.end_date,
|
||
|
|
warranty_end_date: new Date(new Date(project.end_date).getTime() + 12 * 30 * 24 * 60 * 60 * 1000).toISOString(),
|
||
|
|
warranty_status: 'pending'
|
||
|
|
}
|
||
|
|
});
|
||
|
|
} else {
|
||
|
|
res.status(404).json({
|
||
|
|
success: false,
|
||
|
|
message: '项目不存在'
|
||
|
|
});
|
||
|
|
}
|
||
|
|
} catch (error) {
|
||
|
|
console.error('获取项目详情失败:', error);
|
||
|
|
res.status(500).json({
|
||
|
|
success: false,
|
||
|
|
message: '获取项目详情失败',
|
||
|
|
error: error.message
|
||
|
|
});
|
||
|
|
}
|
||
|
|
});
|
||
|
|
|
||
|
|
// ==================== 项目合同API ====================
|
||
|
|
app.get('/api/projects/:id/contracts', async (req, res) => {
|
||
|
|
try {
|
||
|
|
const { id } = req.params;
|
||
|
|
|
||
|
|
const result = await db.query(`
|
||
|
|
SELECT * FROM project_contracts
|
||
|
|
WHERE project_id = ?
|
||
|
|
ORDER BY created_at DESC
|
||
|
|
`, [id]);
|
||
|
|
|
||
|
|
res.json({
|
||
|
|
success: true,
|
||
|
|
data: result.rows
|
||
|
|
});
|
||
|
|
} catch (error) {
|
||
|
|
console.error('获取项目合同失败:', error);
|
||
|
|
res.status(500).json({
|
||
|
|
success: false,
|
||
|
|
message: '获取项目合同失败',
|
||
|
|
error: error.message
|
||
|
|
});
|
||
|
|
}
|
||
|
|
});
|
||
|
|
|
||
|
|
// ==================== 项目分包API ====================
|
||
|
|
app.get('/api/projects/:id/subcontracts', async (req, res) => {
|
||
|
|
try {
|
||
|
|
const { id } = req.params;
|
||
|
|
|
||
|
|
const result = await db.query(`
|
||
|
|
SELECT * FROM subcontracts
|
||
|
|
WHERE project_id = ?
|
||
|
|
ORDER BY created_at DESC
|
||
|
|
`, [id]);
|
||
|
|
|
||
|
|
res.json({
|
||
|
|
success: true,
|
||
|
|
data: result.rows
|
||
|
|
});
|
||
|
|
} catch (error) {
|
||
|
|
console.error('获取项目分包失败:', error);
|
||
|
|
res.status(500).json({
|
||
|
|
success: false,
|
||
|
|
message: '获取项目分包失败',
|
||
|
|
error: error.message
|
||
|
|
});
|
||
|
|
}
|
||
|
|
});
|
||
|
|
|
||
|
|
// ==================== 项目材料API ====================
|
||
|
|
app.get('/api/projects/:id/materials', async (req, res) => {
|
||
|
|
try {
|
||
|
|
const { id } = req.params;
|
||
|
|
|
||
|
|
const result = await db.query(`
|
||
|
|
SELECT * FROM project_materials
|
||
|
|
WHERE project_id = ?
|
||
|
|
ORDER BY created_at DESC
|
||
|
|
`, [id]);
|
||
|
|
|
||
|
|
res.json({
|
||
|
|
success: true,
|
||
|
|
data: result.rows
|
||
|
|
});
|
||
|
|
} catch (error) {
|
||
|
|
console.error('获取项目材料失败:', error);
|
||
|
|
res.status(500).json({
|
||
|
|
success: false,
|
||
|
|
message: '获取项目材料失败',
|
||
|
|
error: error.message
|
||
|
|
});
|
||
|
|
}
|
||
|
|
});
|
||
|
|
|
||
|
|
// ==================== 项目施工节点API ====================
|
||
|
|
app.get('/api/projects/:id/milestones', async (req, res) => {
|
||
|
|
try {
|
||
|
|
const { id } = req.params;
|
||
|
|
|
||
|
|
const result = await db.query(`
|
||
|
|
SELECT * FROM project_milestones
|
||
|
|
WHERE project_id = ?
|
||
|
|
ORDER BY expected_date ASC
|
||
|
|
`, [id]);
|
||
|
|
|
||
|
|
res.json({
|
||
|
|
success: true,
|
||
|
|
data: result.rows
|
||
|
|
});
|
||
|
|
} catch (error) {
|
||
|
|
console.error('获取项目施工节点失败:', error);
|
||
|
|
res.status(500).json({
|
||
|
|
success: false,
|
||
|
|
message: '获取项目施工节点失败',
|
||
|
|
error: error.message
|
||
|
|
});
|
||
|
|
}
|
||
|
|
});
|
||
|
|
|
||
|
|
// ==================== 项目财务API ====================
|
||
|
|
app.get('/api/projects/:id/finances', async (req, res) => {
|
||
|
|
try {
|
||
|
|
const { id } = req.params;
|
||
|
|
|
||
|
|
const result = await db.query(`
|
||
|
|
SELECT * FROM project_finances
|
||
|
|
WHERE project_id = ?
|
||
|
|
ORDER BY payment_date DESC
|
||
|
|
`, [id]);
|
||
|
|
|
||
|
|
res.json({
|
||
|
|
success: true,
|
||
|
|
data: result.rows
|
||
|
|
});
|
||
|
|
} catch (error) {
|
||
|
|
console.error('获取项目财务失败:', error);
|
||
|
|
res.status(500).json({
|
||
|
|
success: false,
|
||
|
|
message: '获取项目财务失败',
|
||
|
|
error: error.message
|
||
|
|
});
|
||
|
|
}
|
||
|
|
});
|
||
|
|
|
||
|
|
// ==================== 项目质保金API ====================
|
||
|
|
app.get('/api/projects/:id/warranty-deposits', async (req, res) => {
|
||
|
|
try {
|
||
|
|
const { id } = req.params;
|
||
|
|
|
||
|
|
const result = await db.query(`
|
||
|
|
SELECT * FROM warranty_deposits
|
||
|
|
WHERE project_id = ?
|
||
|
|
ORDER BY created_at DESC
|
||
|
|
`, [id]);
|
||
|
|
|
||
|
|
res.json({
|
||
|
|
success: true,
|
||
|
|
data: result.rows
|
||
|
|
});
|
||
|
|
} catch (error) {
|
||
|
|
console.error('获取项目质保金失败:', error);
|
||
|
|
res.status(500).json({
|
||
|
|
success: false,
|
||
|
|
message: '获取项目质保金失败',
|
||
|
|
error: error.message
|
||
|
|
});
|
||
|
|
}
|
||
|
|
});
|
||
|
|
|
||
|
|
// ==================== 项目施工日志API ====================
|
||
|
|
app.get('/api/projects/:id/construction-logs', async (req, res) => {
|
||
|
|
try {
|
||
|
|
const { id } = req.params;
|
||
|
|
|
||
|
|
// 由于施工日志表可能不存在,返回空数组
|
||
|
|
res.json({
|
||
|
|
success: true,
|
||
|
|
data: []
|
||
|
|
});
|
||
|
|
} catch (error) {
|
||
|
|
console.error('获取项目施工日志失败:', error);
|
||
|
|
res.status(500).json({
|
||
|
|
success: false,
|
||
|
|
message: '获取项目施工日志失败',
|
||
|
|
error: error.message
|
||
|
|
});
|
||
|
|
}
|
||
|
|
});
|
||
|
|
|
||
|
|
// ==================== 项目删除API ====================
|
||
|
|
app.delete('/api/projects/:id', checkAdmin, async (req, res) => {
|
||
|
|
try {
|
||
|
|
const { id } = req.params;
|
||
|
|
await db.query('DELETE FROM projects WHERE id = ?', [id]);
|
||
|
|
res.json({ success: true, message: '项目已删除' });
|
||
|
|
} catch (error) {
|
||
|
|
console.error('删除项目失败:', error);
|
||
|
|
res.status(500).json({ success: false, message: error.message });
|
||
|
|
}
|
||
|
|
});
|
||
|
|
|
||
|
|
// ==================== 预算报价管理 ====================
|
||
|
|
app.get('/api/budget-projects', async (req, res) => {
|
||
|
|
try {
|
||
|
|
const { customer_id } = req.query;
|
||
|
|
let 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 budget_quotations q WHERE q.project_id = b.id) as quotations
|
||
|
|
FROM budget_projects b
|
||
|
|
`;
|
||
|
|
|
||
|
|
if (customer_id) {
|
||
|
|
query += ` WHERE b.customer_id = ?`;
|
||
|
|
}
|
||
|
|
|
||
|
|
query += ` ORDER BY b.created_at DESC`;
|
||
|
|
|
||
|
|
const params = customer_id ? [customer_id] : [];
|
||
|
|
const result = await db.query(query, params);
|
||
|
|
|
||
|
|
// 解析每个项目的附件和照片数据
|
||
|
|
const projects = result.rows.map(project => {
|
||
|
|
try {
|
||
|
|
return {
|
||
|
|
...project,
|
||
|
|
attachments: project.attachments ? JSON.parse(project.attachments) : [],
|
||
|
|
survey_photos: project.survey_photos ? JSON.parse(project.survey_photos) : []
|
||
|
|
};
|
||
|
|
} catch (error) {
|
||
|
|
console.error('解析项目数据失败:', error);
|
||
|
|
// 如果解析失败,返回原始数据,避免整个应用崩溃
|
||
|
|
return {
|
||
|
|
...project,
|
||
|
|
attachments: [],
|
||
|
|
survey_photos: []
|
||
|
|
};
|
||
|
|
}
|
||
|
|
});
|
||
|
|
|
||
|
|
res.json({ success: true, data: projects });
|
||
|
|
} catch (error) {
|
||
|
|
console.error('获取预算项目失败:', error);
|
||
|
|
res.status(500).json({ success: false, message: error.message });
|
||
|
|
}
|
||
|
|
});
|
||
|
|
|
||
|
|
// 预算项目API已修改,支持按客户ID筛选
|
||
|
|
|
||
|
|
// ==================== 施工管理 ====================
|
||
|
|
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) {
|
||
|
|
console.error('获取施工项目失败:', error);
|
||
|
|
res.status(500).json({ success: false, message: error.message });
|
||
|
|
}
|
||
|
|
});
|
||
|
|
|
||
|
|
// ==================== 商品管理API ====================
|
||
|
|
app.get('/api/products', async (req, res) => {
|
||
|
|
try {
|
||
|
|
const result = await db.query(`
|
||
|
|
SELECT
|
||
|
|
p.*,
|
||
|
|
pc.category_name
|
||
|
|
FROM products p
|
||
|
|
LEFT JOIN product_categories pc ON p.category_id = pc.category_id
|
||
|
|
ORDER BY p.created_at DESC
|
||
|
|
LIMIT 50
|
||
|
|
`);
|
||
|
|
|
||
|
|
res.json({
|
||
|
|
success: true,
|
||
|
|
data: result.rows,
|
||
|
|
count: result.rows.length
|
||
|
|
});
|
||
|
|
} catch (error) {
|
||
|
|
console.error('获取商品失败:', error);
|
||
|
|
res.status(500).json({
|
||
|
|
success: false,
|
||
|
|
message: '获取商品失败',
|
||
|
|
error: error.message
|
||
|
|
});
|
||
|
|
}
|
||
|
|
});
|
||
|
|
|
||
|
|
// ==================== 付款节点API ====================
|
||
|
|
app.get('/api/payment-nodes', async (req, res) => {
|
||
|
|
try {
|
||
|
|
const result = await db.query(`
|
||
|
|
SELECT
|
||
|
|
pn.*,
|
||
|
|
p.project_name,
|
||
|
|
p.project_code
|
||
|
|
FROM payment_nodes pn
|
||
|
|
LEFT JOIN projects p ON pn.project_id = p.project_id
|
||
|
|
ORDER BY pn.due_date ASC
|
||
|
|
LIMIT 50
|
||
|
|
`);
|
||
|
|
|
||
|
|
res.json({
|
||
|
|
success: true,
|
||
|
|
data: result.rows,
|
||
|
|
count: result.rows.length
|
||
|
|
});
|
||
|
|
} catch (error) {
|
||
|
|
console.error('获取付款节点失败:', error);
|
||
|
|
res.status(500).json({
|
||
|
|
success: false,
|
||
|
|
message: '获取付款节点失败',
|
||
|
|
error: error.message
|
||
|
|
});
|
||
|
|
}
|
||
|
|
});
|
||
|
|
|
||
|
|
// ==================== 付款记录API ====================
|
||
|
|
app.get('/api/payment-records', async (req, res) => {
|
||
|
|
try {
|
||
|
|
const result = await db.query(`
|
||
|
|
SELECT
|
||
|
|
pr.*,
|
||
|
|
pn.node_name,
|
||
|
|
p.project_name
|
||
|
|
FROM payment_records pr
|
||
|
|
LEFT JOIN payment_nodes pn ON pr.node_id = pn.node_id
|
||
|
|
LEFT JOIN projects p ON pn.project_id = p.project_id
|
||
|
|
ORDER BY pr.payment_date DESC
|
||
|
|
LIMIT 50
|
||
|
|
`);
|
||
|
|
|
||
|
|
res.json({
|
||
|
|
success: true,
|
||
|
|
data: result.rows,
|
||
|
|
count: result.rows.length
|
||
|
|
});
|
||
|
|
} catch (error) {
|
||
|
|
console.error('获取付款记录失败:', error);
|
||
|
|
res.status(500).json({
|
||
|
|
success: false,
|
||
|
|
message: '获取付款记录失败',
|
||
|
|
error: error.message
|
||
|
|
});
|
||
|
|
}
|
||
|
|
});
|
||
|
|
|
||
|
|
// 权限检查中间件
|
||
|
|
function checkAdmin(req, res, next) {
|
||
|
|
// 简单的权限检查,实际项目中应该从token中解析用户信息
|
||
|
|
// 这里暂时假设只有管理员可以修改数据
|
||
|
|
const userRole = req.headers['x-user-role'] || 'employee';
|
||
|
|
if (userRole !== 'admin') {
|
||
|
|
return res.status(403).json({ success: false, message: '权限不足,仅管理员可操作' });
|
||
|
|
}
|
||
|
|
next();
|
||
|
|
}
|
||
|
|
|
||
|
|
// ==================== 预算项目API ====================
|
||
|
|
app.post('/api/budget-projects', checkAdmin, 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;
|
||
|
|
|
||
|
|
// 确保 attachments 和 survey_photos 是数组
|
||
|
|
const attachmentsArray = Array.isArray(attachments) ? attachments : [];
|
||
|
|
const surveyPhotosArray = Array.isArray(survey_photos) ? survey_photos : [];
|
||
|
|
|
||
|
|
const result = 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, created_at, updated_at)
|
||
|
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, datetime('now'), datetime('now'))`,
|
||
|
|
[name, customer_id, manager_id, location, survey_date, intermediary, intermediary_fee_type, intermediary_fee_value, customer_requirements, project_overview, JSON.stringify(attachmentsArray), JSON.stringify(surveyPhotosArray), 'negotiating']
|
||
|
|
);
|
||
|
|
|
||
|
|
const projectId = result.lastID;
|
||
|
|
|
||
|
|
res.json({
|
||
|
|
success: true,
|
||
|
|
message: '创建成功',
|
||
|
|
data: {
|
||
|
|
id: projectId,
|
||
|
|
name,
|
||
|
|
customer_id,
|
||
|
|
manager_id,
|
||
|
|
location,
|
||
|
|
survey_date,
|
||
|
|
intermediary,
|
||
|
|
intermediary_fee_type,
|
||
|
|
intermediary_fee_value,
|
||
|
|
customer_requirements,
|
||
|
|
project_overview,
|
||
|
|
attachments,
|
||
|
|
survey_photos,
|
||
|
|
status: 'negotiating',
|
||
|
|
created_at: new Date().toISOString()
|
||
|
|
}
|
||
|
|
});
|
||
|
|
} catch (error) {
|
||
|
|
console.error('创建预算项目失败:', error);
|
||
|
|
res.status(500).json({
|
||
|
|
success: false,
|
||
|
|
message: '创建失败',
|
||
|
|
error: error.message
|
||
|
|
});
|
||
|
|
}
|
||
|
|
});
|
||
|
|
|
||
|
|
// ==================== 预算项目详情API ====================
|
||
|
|
app.get('/api/budget-projects/:id', async (req, res) => {
|
||
|
|
try {
|
||
|
|
const { id } = req.params;
|
||
|
|
|
||
|
|
const result = await db.query(`
|
||
|
|
SELECT b.*,
|
||
|
|
c.name as customer_name,
|
||
|
|
u.name as manager_name,
|
||
|
|
(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 budget_quotations q WHERE q.project_id = b.id) as quotations
|
||
|
|
FROM budget_projects b
|
||
|
|
LEFT JOIN customers c ON b.customer_id = c.id
|
||
|
|
LEFT JOIN users u ON b.manager_id = u.id
|
||
|
|
WHERE b.id = ?
|
||
|
|
`, [id]);
|
||
|
|
|
||
|
|
if (result.rows.length > 0) {
|
||
|
|
const project = result.rows[0];
|
||
|
|
try {
|
||
|
|
// 解析JSON字符串为数组
|
||
|
|
project.attachments = project.attachments ? JSON.parse(project.attachments) : [];
|
||
|
|
project.survey_photos = project.survey_photos ? JSON.parse(project.survey_photos) : [];
|
||
|
|
} catch (error) {
|
||
|
|
console.error('解析项目数据失败:', error);
|
||
|
|
// 如果解析失败,设置默认值
|
||
|
|
project.attachments = [];
|
||
|
|
project.survey_photos = [];
|
||
|
|
}
|
||
|
|
res.json({ success: true, data: project });
|
||
|
|
} else {
|
||
|
|
res.status(404).json({ success: false, message: '项目不存在' });
|
||
|
|
}
|
||
|
|
} catch (error) {
|
||
|
|
console.error('获取预算项目详情失败:', error);
|
||
|
|
res.status(500).json({ success: false, message: error.message });
|
||
|
|
}
|
||
|
|
});
|
||
|
|
|
||
|
|
// ==================== 预算报价API ====================
|
||
|
|
app.post('/api/budget-projects/:projectId/quotations', checkAdmin, async (req, res) => {
|
||
|
|
try {
|
||
|
|
const { projectId } = req.params;
|
||
|
|
const { quotation_date, amount, currency, file_url, remark, version } = req.body;
|
||
|
|
|
||
|
|
const result = await db.query(
|
||
|
|
`INSERT INTO budget_quotations (project_id, version, quotation_date, amount, currency, status, file_url, remark, created_at, updated_at)
|
||
|
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?, datetime('now'), datetime('now'))`,
|
||
|
|
[projectId, version, quotation_date, amount, currency, 'draft', file_url, remark]
|
||
|
|
);
|
||
|
|
|
||
|
|
const quotationId = result.lastID;
|
||
|
|
|
||
|
|
res.json({
|
||
|
|
success: true,
|
||
|
|
message: '新增报价版本成功',
|
||
|
|
data: {
|
||
|
|
id: quotationId,
|
||
|
|
project_id: projectId,
|
||
|
|
version,
|
||
|
|
quotation_date,
|
||
|
|
amount,
|
||
|
|
currency,
|
||
|
|
status: 'draft',
|
||
|
|
file_url,
|
||
|
|
remark,
|
||
|
|
created_at: new Date().toISOString()
|
||
|
|
}
|
||
|
|
});
|
||
|
|
} catch (error) {
|
||
|
|
console.error('创建报价版本失败:', error);
|
||
|
|
res.status(500).json({
|
||
|
|
success: false,
|
||
|
|
message: '创建失败',
|
||
|
|
error: error.message
|
||
|
|
});
|
||
|
|
}
|
||
|
|
});
|
||
|
|
|
||
|
|
app.delete('/api/budget-projects/:projectId/quotations/:quotationId', checkAdmin, async (req, res) => {
|
||
|
|
try {
|
||
|
|
const { projectId, quotationId } = req.params;
|
||
|
|
|
||
|
|
const result = await db.query(
|
||
|
|
`DELETE FROM budget_quotations WHERE id = ? AND project_id = ?`,
|
||
|
|
[quotationId, projectId]
|
||
|
|
);
|
||
|
|
|
||
|
|
if (result.changes > 0) {
|
||
|
|
res.json({
|
||
|
|
success: true,
|
||
|
|
message: '删除成功'
|
||
|
|
});
|
||
|
|
} else {
|
||
|
|
res.status(404).json({
|
||
|
|
success: false,
|
||
|
|
message: '报价版本不存在'
|
||
|
|
});
|
||
|
|
}
|
||
|
|
} catch (error) {
|
||
|
|
console.error('删除报价版本失败:', error);
|
||
|
|
res.status(500).json({
|
||
|
|
success: false,
|
||
|
|
message: '删除失败',
|
||
|
|
error: error.message
|
||
|
|
});
|
||
|
|
}
|
||
|
|
});
|
||
|
|
|
||
|
|
// ==================== 预算项目状态更新API ====================
|
||
|
|
app.put('/api/budget-projects/:id/sign', checkAdmin, async (req, res) => {
|
||
|
|
try {
|
||
|
|
console.log('收到签约请求:', req.body);
|
||
|
|
const { id } = req.params;
|
||
|
|
const {
|
||
|
|
contract_code,
|
||
|
|
project_name,
|
||
|
|
contract_method,
|
||
|
|
currency,
|
||
|
|
contract_amount,
|
||
|
|
start_date,
|
||
|
|
end_date,
|
||
|
|
contract_period,
|
||
|
|
project_overview,
|
||
|
|
other_requirements,
|
||
|
|
warranty_deposit_percentage,
|
||
|
|
warranty_period,
|
||
|
|
contract_file,
|
||
|
|
payment_nodes,
|
||
|
|
unit_price_items
|
||
|
|
} = req.body;
|
||
|
|
|
||
|
|
console.log('解析请求参数成功:', {
|
||
|
|
id,
|
||
|
|
contract_code,
|
||
|
|
project_name,
|
||
|
|
contract_method,
|
||
|
|
currency,
|
||
|
|
contract_amount,
|
||
|
|
start_date,
|
||
|
|
end_date,
|
||
|
|
contract_period,
|
||
|
|
project_overview,
|
||
|
|
other_requirements,
|
||
|
|
warranty_deposit_percentage,
|
||
|
|
warranty_period,
|
||
|
|
contract_file,
|
||
|
|
payment_nodes: payment_nodes?.length,
|
||
|
|
unit_price_items: unit_price_items?.length
|
||
|
|
});
|
||
|
|
|
||
|
|
// 1. 获取预算项目详细信息
|
||
|
|
const budgetProjectResult = await db.query(
|
||
|
|
`SELECT b.*,
|
||
|
|
c.name as customer_name,
|
||
|
|
(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 budget_quotations q WHERE q.project_id = b.id ORDER BY q.version DESC LIMIT 1) as latest_quotation
|
||
|
|
FROM budget_projects b
|
||
|
|
LEFT JOIN customers c ON b.customer_id = c.id
|
||
|
|
WHERE b.id = ?`,
|
||
|
|
[id]
|
||
|
|
);
|
||
|
|
|
||
|
|
if (budgetProjectResult.rows.length === 0) {
|
||
|
|
return res.status(404).json({ success: false, message: '预算项目不存在' });
|
||
|
|
}
|
||
|
|
|
||
|
|
const budgetProject = budgetProjectResult.rows[0];
|
||
|
|
|
||
|
|
// 2. 获取最新报价信息
|
||
|
|
let latestQuotation = null;
|
||
|
|
let defaultContractAmount = 0;
|
||
|
|
if (budgetProject.latest_quotation) {
|
||
|
|
try {
|
||
|
|
const quotations = JSON.parse(budgetProject.latest_quotation);
|
||
|
|
if (quotations && quotations.length > 0) {
|
||
|
|
latestQuotation = quotations[0];
|
||
|
|
defaultContractAmount = parseFloat(latestQuotation.amount) || 0;
|
||
|
|
}
|
||
|
|
} catch (e) {
|
||
|
|
console.error('解析报价信息失败:', e);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// 3. 生成项目代码
|
||
|
|
const today = new Date();
|
||
|
|
const dateStr = today.toISOString().split('T')[0].replace(/-/g, '');
|
||
|
|
|
||
|
|
// 获取当天项目数量,生成序号
|
||
|
|
const projectCountResult = await db.query(
|
||
|
|
`SELECT COUNT(*) as count FROM projects WHERE DATE(created_at) = DATE('now')`
|
||
|
|
);
|
||
|
|
|
||
|
|
const projectCount = parseInt(projectCountResult.rows[0].count) || 0;
|
||
|
|
const sequence = String(projectCount + 1).padStart(3, '0');
|
||
|
|
const projectCode = `PROJ-${dateStr}-${sequence}`;
|
||
|
|
|
||
|
|
// 4. 计算项目时间
|
||
|
|
const startDate = today.toISOString();
|
||
|
|
const endDate = new Date(today.getTime() + 6 * 30 * 24 * 60 * 60 * 1000).toISOString();
|
||
|
|
|
||
|
|
// 5. 创建项目
|
||
|
|
const finalContractAmount = contract_amount || defaultContractAmount;
|
||
|
|
const projectResult = await db.query(
|
||
|
|
`INSERT INTO projects (code, name, customer_id, status, contract_amount, start_date, end_date, description, created_at, updated_at)
|
||
|
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?, datetime('now'), datetime('now'))`,
|
||
|
|
[
|
||
|
|
projectCode,
|
||
|
|
project_name || budgetProject.name,
|
||
|
|
budgetProject.customer_id,
|
||
|
|
'active',
|
||
|
|
finalContractAmount,
|
||
|
|
start_date || startDate,
|
||
|
|
end_date || endDate,
|
||
|
|
project_overview || budgetProject.project_overview || ''
|
||
|
|
]
|
||
|
|
);
|
||
|
|
|
||
|
|
const newProjectId = projectResult.lastID;
|
||
|
|
|
||
|
|
// 6. 创建项目合同
|
||
|
|
const contractCode = contract_code || `CONTRACT-${dateStr}-${sequence}`;
|
||
|
|
const finalContractMethod = contract_method || 'lump_sum';
|
||
|
|
const finalContractPeriod = contract_period || (end_date && start_date ? Math.floor((new Date(end_date).getTime() - new Date(start_date).getTime()) / (1000 * 60 * 60 * 24)) : 180);
|
||
|
|
const finalWarrantyPercentage = warranty_deposit_percentage || 5;
|
||
|
|
const finalWarrantyPeriod = warranty_period || 12;
|
||
|
|
|
||
|
|
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, created_at, updated_at)
|
||
|
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, datetime('now'), datetime('now'))`,
|
||
|
|
[
|
||
|
|
newProjectId,
|
||
|
|
contractCode,
|
||
|
|
finalContractAmount,
|
||
|
|
currency || 'CNY',
|
||
|
|
finalContractMethod,
|
||
|
|
finalContractPeriod,
|
||
|
|
start_date || startDate,
|
||
|
|
end_date || endDate,
|
||
|
|
finalWarrantyPercentage,
|
||
|
|
finalWarrantyPeriod,
|
||
|
|
contract_file || null
|
||
|
|
]
|
||
|
|
);
|
||
|
|
|
||
|
|
// 7. 创建付款节点
|
||
|
|
if (payment_nodes && Array.isArray(payment_nodes)) {
|
||
|
|
for (const node of payment_nodes) {
|
||
|
|
await db.query(
|
||
|
|
`INSERT INTO project_milestones (project_id, milestone_name, percentage, amount, expected_date, status, created_at, updated_at)
|
||
|
|
VALUES (?, ?, ?, ?, ?, ?, datetime('now'), datetime('now'))`,
|
||
|
|
[
|
||
|
|
newProjectId,
|
||
|
|
node.node_name || `节点${node.id}`,
|
||
|
|
node.percentage || 0,
|
||
|
|
node.amount || 0,
|
||
|
|
start_date || startDate,
|
||
|
|
'pending'
|
||
|
|
]
|
||
|
|
);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// 8. 创建单价项(如果是单价结算)
|
||
|
|
if (unit_price_items && Array.isArray(unit_price_items)) {
|
||
|
|
for (const item of unit_price_items) {
|
||
|
|
await db.query(
|
||
|
|
`INSERT INTO project_materials (project_id, product_name, unit, budget_quantity, average_price, total_amount, created_at, updated_at)
|
||
|
|
VALUES (?, ?, ?, ?, ?, ?, datetime('now'), datetime('now'))`,
|
||
|
|
[
|
||
|
|
newProjectId,
|
||
|
|
item.name || `单项${item.id}`,
|
||
|
|
item.unit || '个',
|
||
|
|
item.quantity || 0,
|
||
|
|
item.price || 0,
|
||
|
|
item.total || 0
|
||
|
|
]
|
||
|
|
);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// 9. 更新预算项目状态
|
||
|
|
await db.query(
|
||
|
|
`UPDATE budget_projects SET status = 'signed', updated_at = datetime('now') WHERE id = ?`,
|
||
|
|
[id]
|
||
|
|
);
|
||
|
|
|
||
|
|
res.json({
|
||
|
|
success: true,
|
||
|
|
message: '标记签约成功,项目已自动创建',
|
||
|
|
data: {
|
||
|
|
project_id: newProjectId,
|
||
|
|
project_code: projectCode,
|
||
|
|
contract_code: contractCode
|
||
|
|
}
|
||
|
|
});
|
||
|
|
} catch (error) {
|
||
|
|
console.error('标记签约失败:', error);
|
||
|
|
console.error('错误堆栈:', error.stack);
|
||
|
|
res.status(500).json({
|
||
|
|
success: false,
|
||
|
|
message: '操作失败',
|
||
|
|
error: error.message,
|
||
|
|
stack: error.stack
|
||
|
|
});
|
||
|
|
}
|
||
|
|
});
|
||
|
|
|
||
|
|
app.put('/api/budget-projects/:id/unsigned', checkAdmin, async (req, res) => {
|
||
|
|
try {
|
||
|
|
const { id } = req.params;
|
||
|
|
|
||
|
|
await db.query(
|
||
|
|
`UPDATE budget_projects SET status = 'unsigned', updated_at = datetime('now') WHERE id = ?`,
|
||
|
|
[id]
|
||
|
|
);
|
||
|
|
|
||
|
|
res.json({
|
||
|
|
success: true,
|
||
|
|
message: '标记未签约成功'
|
||
|
|
});
|
||
|
|
} catch (error) {
|
||
|
|
console.error('标记未签约失败:', error);
|
||
|
|
res.status(500).json({
|
||
|
|
success: false,
|
||
|
|
message: '操作失败',
|
||
|
|
error: error.message
|
||
|
|
});
|
||
|
|
}
|
||
|
|
});
|
||
|
|
|
||
|
|
// ==================== 删除预算项目API ====================
|
||
|
|
app.delete('/api/budget-projects/:id', checkAdmin, async (req, res) => {
|
||
|
|
try {
|
||
|
|
const { id } = req.params;
|
||
|
|
|
||
|
|
// 先删除关联的报价
|
||
|
|
await db.query(`DELETE FROM budget_quotations WHERE project_id = ?`, [id]);
|
||
|
|
|
||
|
|
// 再删除预算项目
|
||
|
|
const result = await db.query(`DELETE FROM budget_projects WHERE id = ?`, [id]);
|
||
|
|
|
||
|
|
if (result.changes > 0) {
|
||
|
|
res.json({
|
||
|
|
success: true,
|
||
|
|
message: '删除成功'
|
||
|
|
});
|
||
|
|
} else {
|
||
|
|
res.status(404).json({
|
||
|
|
success: false,
|
||
|
|
message: '项目不存在'
|
||
|
|
});
|
||
|
|
}
|
||
|
|
} catch (error) {
|
||
|
|
console.error('删除预算项目失败:', error);
|
||
|
|
res.status(500).json({
|
||
|
|
success: false,
|
||
|
|
message: '删除失败',
|
||
|
|
error: error.message
|
||
|
|
});
|
||
|
|
}
|
||
|
|
});
|
||
|
|
|
||
|
|
// ==================== 汇率API ====================
|
||
|
|
app.get('/api/exchange-rates/latest', async (req, res) => {
|
||
|
|
try {
|
||
|
|
const result = await db.query(`
|
||
|
|
SELECT pair_key, rate
|
||
|
|
FROM exchange_rates
|
||
|
|
WHERE effective_date <= DATE('now')
|
||
|
|
GROUP BY pair_key
|
||
|
|
ORDER BY effective_date DESC
|
||
|
|
`);
|
||
|
|
|
||
|
|
const data = {};
|
||
|
|
result.rows.forEach(row => {
|
||
|
|
data[row.pair_key] = row.rate;
|
||
|
|
});
|
||
|
|
|
||
|
|
// 如果没有数据,使用默认值
|
||
|
|
if (Object.keys(data).length === 0) {
|
||
|
|
data.CNY_LAK = 2900;
|
||
|
|
data.CNY_USD = 0.143;
|
||
|
|
data.CNY_THB = 4.8;
|
||
|
|
data.USD_LAK = 20300;
|
||
|
|
data.THB_LAK = 604;
|
||
|
|
}
|
||
|
|
|
||
|
|
res.json({
|
||
|
|
success: true,
|
||
|
|
data: data,
|
||
|
|
updated_at: new Date().toISOString(),
|
||
|
|
date: new Date().toISOString().split('T')[0]
|
||
|
|
});
|
||
|
|
} catch (error) {
|
||
|
|
console.error('获取汇率失败:', error);
|
||
|
|
res.status(500).json({ success: false, message: '获取汇率失败', error: error.message });
|
||
|
|
}
|
||
|
|
});
|
||
|
|
|
||
|
|
app.get('/api/exchange-rates', async (req, res) => {
|
||
|
|
try {
|
||
|
|
const result = await db.query(`
|
||
|
|
SELECT * FROM exchange_rates
|
||
|
|
ORDER BY effective_date DESC
|
||
|
|
LIMIT 20
|
||
|
|
`);
|
||
|
|
|
||
|
|
res.json({
|
||
|
|
success: true,
|
||
|
|
data: result.rows,
|
||
|
|
count: result.rows.length
|
||
|
|
});
|
||
|
|
} catch (error) {
|
||
|
|
console.error('获取汇率失败:', error);
|
||
|
|
res.status(500).json({
|
||
|
|
success: false,
|
||
|
|
message: '获取汇率失败',
|
||
|
|
error: 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]);
|
||
|
|
|
||
|
|
// 转换数据格式以匹配前端期望
|
||
|
|
const formattedData = result.rows.map(row => {
|
||
|
|
const [from_currency, to_currency] = row.pair_key.split('_');
|
||
|
|
return {
|
||
|
|
...row,
|
||
|
|
from_currency,
|
||
|
|
to_currency
|
||
|
|
};
|
||
|
|
});
|
||
|
|
|
||
|
|
res.json({
|
||
|
|
success: true,
|
||
|
|
data: formattedData
|
||
|
|
});
|
||
|
|
} catch (error) {
|
||
|
|
console.error('获取历史汇率失败:', error);
|
||
|
|
res.status(500).json({
|
||
|
|
success: false,
|
||
|
|
message: '获取历史汇率失败',
|
||
|
|
error: error.message
|
||
|
|
});
|
||
|
|
}
|
||
|
|
});
|
||
|
|
|
||
|
|
app.post('/api/exchange-rates', async (req, res) => {
|
||
|
|
try {
|
||
|
|
const { pair_key, rate, effective_date } = req.body;
|
||
|
|
|
||
|
|
if (!pair_key || rate === undefined || !effective_date) {
|
||
|
|
return res.status(400).json({ success: false, message: '缺少必要参数' });
|
||
|
|
}
|
||
|
|
|
||
|
|
const result = await db.query(
|
||
|
|
`INSERT INTO exchange_rates (pair_key, rate, effective_date, created_at, updated_at)
|
||
|
|
VALUES (?, ?, ?, datetime('now'), datetime('now'))`,
|
||
|
|
[pair_key, rate, effective_date]
|
||
|
|
);
|
||
|
|
|
||
|
|
res.json({
|
||
|
|
success: true,
|
||
|
|
message: '汇率保存成功',
|
||
|
|
data: {
|
||
|
|
id: result.lastID,
|
||
|
|
pair_key,
|
||
|
|
rate,
|
||
|
|
effective_date,
|
||
|
|
created_at: new Date().toISOString()
|
||
|
|
}
|
||
|
|
});
|
||
|
|
} catch (error) {
|
||
|
|
console.error('保存汇率失败:', error);
|
||
|
|
res.status(500).json({
|
||
|
|
success: false,
|
||
|
|
message: '保存汇率失败',
|
||
|
|
error: error.message
|
||
|
|
});
|
||
|
|
}
|
||
|
|
});
|
||
|
|
|
||
|
|
// ==================== 预支款API ====================
|
||
|
|
app.get('/api/advances', async (req, res) => {
|
||
|
|
try {
|
||
|
|
// 模拟数据 - 实际应从数据库获取
|
||
|
|
res.json({
|
||
|
|
success: true,
|
||
|
|
data: [],
|
||
|
|
count: 2
|
||
|
|
});
|
||
|
|
} catch (error) {
|
||
|
|
res.status(500).json({
|
||
|
|
success: false,
|
||
|
|
message: '获取预支款失败',
|
||
|
|
error: error.message
|
||
|
|
});
|
||
|
|
}
|
||
|
|
});
|
||
|
|
|
||
|
|
// ==================== 报销API ====================
|
||
|
|
// ==================== 付款申请API ====================
|
||
|
|
// ==================== 核销申请API ====================
|
||
|
|
app.get('/api/verifications', async (req, res) => {
|
||
|
|
try {
|
||
|
|
res.json({
|
||
|
|
success: true,
|
||
|
|
data: []
|
||
|
|
});
|
||
|
|
} catch (error) {
|
||
|
|
res.status(500).json({ success: false, message: '获取核销记录失败', error: error.message });
|
||
|
|
}
|
||
|
|
});
|
||
|
|
|
||
|
|
app.get('/api/payment-requests', async (req, res) => {
|
||
|
|
try {
|
||
|
|
res.json({
|
||
|
|
success: true,
|
||
|
|
data: []
|
||
|
|
});
|
||
|
|
} catch (error) {
|
||
|
|
res.status(500).json({ success: false, message: '获取付款申请失败', error: error.message });
|
||
|
|
}
|
||
|
|
});
|
||
|
|
|
||
|
|
app.get('/api/reimbursements', async (req, res) => {
|
||
|
|
try {
|
||
|
|
// 模拟数据
|
||
|
|
res.json({
|
||
|
|
success: true,
|
||
|
|
data: [],
|
||
|
|
count: 1
|
||
|
|
});
|
||
|
|
} catch (error) {
|
||
|
|
res.status(500).json({
|
||
|
|
success: false,
|
||
|
|
message: '获取报销记录失败',
|
||
|
|
error: error.message
|
||
|
|
});
|
||
|
|
}
|
||
|
|
});
|
||
|
|
|
||
|
|
// ==================== 财务统计API ====================
|
||
|
|
app.get('/api/finance-stats', async (req, res) => {
|
||
|
|
try {
|
||
|
|
// 获取统计数据
|
||
|
|
const [customers, suppliers, projects, paymentNodes, paymentRecords] = await Promise.all([
|
||
|
|
db.query('SELECT COUNT(*) as count FROM customers'),
|
||
|
|
db.query('SELECT COUNT(*) as count FROM suppliers'),
|
||
|
|
db.query('SELECT COUNT(*) as count FROM projects'),
|
||
|
|
db.query('SELECT COUNT(*) as count FROM payment_nodes'),
|
||
|
|
db.query('SELECT COUNT(*) as count FROM payment_records')
|
||
|
|
]);
|
||
|
|
|
||
|
|
res.json({
|
||
|
|
success: true,
|
||
|
|
data: {
|
||
|
|
summary: {
|
||
|
|
customers: parseInt(customers.rows[0].count) || 0,
|
||
|
|
suppliers: parseInt(suppliers.rows[0].count) || 0,
|
||
|
|
projects: parseInt(projects.rows[0].count) || 0,
|
||
|
|
payment_nodes: parseInt(paymentNodes.rows[0].count) || 0,
|
||
|
|
payment_records: parseInt(paymentRecords.rows[0].count) || 0
|
||
|
|
},
|
||
|
|
timestamp: new Date().toISOString()
|
||
|
|
}
|
||
|
|
});
|
||
|
|
} catch (error) {
|
||
|
|
res.json({
|
||
|
|
success: false,
|
||
|
|
message: '获取财务统计失败',
|
||
|
|
error: error.message
|
||
|
|
});
|
||
|
|
}
|
||
|
|
});
|
||
|
|
|
||
|
|
// ==================== 系统状态页面 ====================
|
||
|
|
app.get('/status', (req, res) => {
|
||
|
|
res.send(`
|
||
|
|
<!DOCTYPE html>
|
||
|
|
<html>
|
||
|
|
<head>
|
||
|
|
<title>系统状态 - 公司财务管理系统</title>
|
||
|
|
<meta charset="utf-8">
|
||
|
|
<style>
|
||
|
|
body { font-family: Arial; margin: 40px; background: #f5f5f5; }
|
||
|
|
.container { max-width: 1000px; margin: 0 auto; background: white; padding: 30px; border-radius: 15px; box-shadow: 0 5px 20px rgba(0,0,0,0.1); }
|
||
|
|
h1 { color: #1890ff; border-bottom: 3px solid #1890ff; padding-bottom: 15px; }
|
||
|
|
.status-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 20px; margin: 30px 0; }
|
||
|
|
.status-card { padding: 20px; border-radius: 10px; text-align: center; }
|
||
|
|
.status-ok { background: linear-gradient(135deg, #52c41a 0%, #73d13d 100%); color: white; }
|
||
|
|
.status-error { background: linear-gradient(135deg, #ff4d4f 0%, #ff7875 100%); color: white; }
|
||
|
|
.btn { display: inline-block; padding: 12px 24px; background: #1890ff; color: white; text-decoration: none; border-radius: 8px; margin: 10px 5px; }
|
||
|
|
.info-box { background: #e6f7ff; padding: 20px; border-radius: 10px; margin: 20px 0; }
|
||
|
|
</style>
|
||
|
|
</head>
|
||
|
|
<body>
|
||
|
|
<div class="container">
|
||
|
|
<h1>🏢 公司财务管理系统 - 生产环境状态</h1>
|
||
|
|
<p>服务器: <strong>43.161.248.209:3000</strong> | 时间: ${new Date().toLocaleString()}</p>
|
||
|
|
|
||
|
|
<div class="status-grid">
|
||
|
|
<div class="status-card status-ok">
|
||
|
|
<div style="font-size: 36px;">✅</div>
|
||
|
|
<div style="font-size: 18px; font-weight: bold;">前端服务</div>
|
||
|
|
<div>端口: 3000</div>
|
||
|
|
<div>状态: 正常</div>
|
||
|
|
</div>
|
||
|
|
<div class="status-card status-ok">
|
||
|
|
<div style="font-size: 36px;">✅</div>
|
||
|
|
<div style="font-size: 18px; font-weight: bold;">后端API</div>
|
||
|
|
<div>12个端点</div>
|
||
|
|
<div>状态: 正常</div>
|
||
|
|
</div>
|
||
|
|
<div class="status-card status-ok">
|
||
|
|
<div style="font-size: 36px;">✅</div>
|
||
|
|
<div style="font-size: 18px; font-weight: bold;">数据库</div>
|
||
|
|
<div>PostgreSQL</div>
|
||
|
|
<div>状态: 已连接</div>
|
||
|
|
</div>
|
||
|
|
<div class="status-card status-ok">
|
||
|
|
<div style="font-size: 36px;">✅</div>
|
||
|
|
<div style="font-size: 18px; font-weight: bold;">网络访问</div>
|
||
|
|
<div>绑定: 0.0.0.0</div>
|
||
|
|
<div>状态: 已验证</div>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<div class="info-box">
|
||
|
|
<h2>🔧 端口访问说明</h2>
|
||
|
|
<p><strong>✅ 端口3000</strong>: 已验证可外部访问,所有服务运行正常</p>
|
||
|
|
<p><strong>⚠️ 端口5000</strong>: 安全组已开放,但可能存在网络路由问题</p>
|
||
|
|
<p><strong>🎯 解决方案</strong>: 使用已验证的3000端口作为生产环境</p>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<div style="text-align: center; margin-top: 30px;">
|
||
|
|
<a href="/" class="btn">进入系统</a>
|
||
|
|
<a href="/api/health" class="btn" target="_blank">API健康检查</a>
|
||
|
|
<a href="http://43.161.248.209:3000/api/customers" class="btn" target="_blank">测试客户API</a>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
</body>
|
||
|
|
</html>
|
||
|
|
`);
|
||
|
|
});
|
||
|
|
|
||
|
|
// ==================== 欢迎页面 ====================
|
||
|
|
app.get('/welcome', (req, res) => {
|
||
|
|
res.send(`
|
||
|
|
<!DOCTYPE html>
|
||
|
|
<html>
|
||
|
|
<head>
|
||
|
|
<title>欢迎 - 公司财务管理系统</title>
|
||
|
|
<meta charset="utf-8">
|
||
|
|
<style>
|
||
|
|
body { font-family: Arial; margin: 0; padding: 0; background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); min-height: 100vh; }
|
||
|
|
.container { max-width: 1200px; margin: 0 auto; padding: 40px 20px; }
|
||
|
|
.header { text-align: center; color: white; margin-bottom: 50px; }
|
||
|
|
h1 { font-size: 48px; margin-bottom: 20px; text-shadow: 0 2px 10px rgba(0,0,0,0.3); }
|
||
|
|
.subtitle { font-size: 20px; opacity: 0.9; }
|
||
|
|
.dashboard { display: grid; grid-template-columns: repeat(auto-fit, minmax(300px, 1fr)); gap: 30px; }
|
||
|
|
.card { background: white; border-radius: 20px; padding: 30px; box-shadow: 0 15px 35px rgba(0,0,0,0.2); transition: transform 0.3s; }
|
||
|
|
.card:hover { transform: translateY(-10px); }
|
||
|
|
.card h2 { color: #1890ff; margin-bottom: 20px; border-bottom: 3px solid #1890ff; padding-bottom: 10px; }
|
||
|
|
.btn { display: inline-block; background: linear-gradient(135deg, #1890ff 0%, #36cfc9 100%); color: white; padding: 15px 30px; border-radius: 10px; text-decoration: none; font-size: 18px; font-weight: bold; margin: 10px; }
|
||
|
|
.btn:hover { opacity: 0.9; }
|
||
|
|
.feature-list { list-style: none; padding: 0; }
|
||
|
|
.feature-list li { padding: 10px 0; border-bottom: 1px solid #eee; }
|
||
|
|
.feature-list li:before { content: "✅ "; color: #52c41a; }
|
||
|
|
.stats { display: flex; justify-content: space-around; margin: 30px 0; }
|
||
|
|
.stat-item { text-align: center; color: white; }
|
||
|
|
.stat-value { font-size: 36px; font-weight: bold; }
|
||
|
|
.stat-label { font-size: 14px; opacity: 0.8; }
|
||
|
|
</style>
|
||
|
|
</head>
|
||
|
|
<body>
|
||
|
|
<div class="container">
|
||
|
|
<div class="header">
|
||
|
|
<h1>🏢 公司财务管理系统</h1>
|
||
|
|
<div class="subtitle">生产环境 v1.0.0 | 专为老挝电力公司定制</div>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<div class="stats">
|
||
|
|
<div class="stat-item">
|
||
|
|
<div class="stat-value">12</div>
|
||
|
|
<div class="stat-label">功能模块</div>
|
||
|
|
</div>
|
||
|
|
<div class="stat-item">
|
||
|
|
<div class="stat-value">4</div>
|
||
|
|
<div class="stat-label">多币种支持</div>
|
||
|
|
</div>
|
||
|
|
<div class="stat-item">
|
||
|
|
<div class="stat-value">100%</div>
|
||
|
|
<div class="stat-label">响应式设计</div>
|
||
|
|
</div>
|
||
|
|
<div class="stat-item">
|
||
|
|
<div class="stat-value">24/7</div>
|
||
|
|
<div class="stat-label">服务可用</div>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<div class="dashboard">
|
||
|
|
<div class="card">
|
||
|
|
<h2>🚀 立即开始</h2>
|
||
|
|
<p>点击下方按钮进入系统,开始管理您的财务业务。</p>
|
||
|
|
<a href="/" class="btn">进入系统主界面</a>
|
||
|
|
<a href="/status" class="btn">查看系统状态</a>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<div class="card">
|
||
|
|
<h2>📊 核心功能</h2>
|
||
|
|
<ul class="feature-list">
|
||
|
|
<li>客户与供应商管理</li>
|
||
|
|
<li>项目与合同管理</li>
|
||
|
|
<li>付款节点与记录</li>
|
||
|
|
<li>多币种汇率管理</li>
|
||
|
|
<li>预支款与报销流程</li>
|
||
|
|
<li>财务统计与报表</li>
|
||
|
|
<li>移动端适配</li>
|
||
|
|
<li>多语言支持</li>
|
||
|
|
</ul>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<div class="card">
|
||
|
|
<h2>🔧 系统信息</h2>
|
||
|
|
<p><strong>服务器</strong>: 43.161.248.209:3000</p>
|
||
|
|
<p><strong>技术栈</strong>: React + Node.js + PostgreSQL</p>
|
||
|
|
<p><strong>部署时间</strong>: 2026-03-09</p>
|
||
|
|
<p><strong>测试账号</strong>: admin / password</p>
|
||
|
|
<div style="margin-top: 20px;">
|
||
|
|
<a href="/api/health" class="btn" style="padding: 10px 20px; font-size: 14px;">API健康检查</a>
|
||
|
|
<a href="/api/customers" class="btn" style="padding: 10px 20px; font-size: 14px;">客户API</a>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<div style="text-align: center; margin-top: 50px; color: white; opacity: 0.8;">
|
||
|
|
<p>© 2026 老挝电力公司财务管理系统 | 技术支持: OpenClaw AI Assistant</p>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
</body>
|
||
|
|
</html>
|
||
|
|
`);
|
||
|
|
});
|
||
|
|
|
||
|
|
// ==================== 默认路由 ====================
|
||
|
|
app.get('/', (req, res) => {
|
||
|
|
res.sendFile(path.join(__dirname, '../frontend/dist/index.html'));
|
||
|
|
});
|
||
|
|
|
||
|
|
// ==================== API文档页面 ====================
|
||
|
|
app.get('/api-docs', (req, res) => {
|
||
|
|
res.send(`
|
||
|
|
<!DOCTYPE html>
|
||
|
|
<html>
|
||
|
|
<head><title>API文档</title><meta charset="utf-8"></head>
|
||
|
|
<body style="font-family: Arial; padding: 40px;">
|
||
|
|
<h1>📚 API文档</h1>
|
||
|
|
<p>这是API端点文档页面。如果您想使用业务界面,请访问:</p>
|
||
|
|
<p><a href="/" style="font-size: 18px; color: #1890ff;">👉 点击这里进入业务系统</a></p>
|
||
|
|
<p>或访问:<a href="/welcome">欢迎页面</a></p>
|
||
|
|
</body>
|
||
|
|
</html>
|
||
|
|
`);
|
||
|
|
});
|
||
|
|
|
||
|
|
// ==================== 文件上传API (腾讯云COS) ====================
|
||
|
|
const multer = require('multer');
|
||
|
|
const COS = require('cos-nodejs-sdk-v5');
|
||
|
|
const storage = multer.memoryStorage();
|
||
|
|
const upload = multer({ storage, limits: { fileSize: 10 * 1024 * 1024 } });
|
||
|
|
|
||
|
|
const cosConfig = {
|
||
|
|
SecretId: process.env.TENCENT_SECRET_ID || '',
|
||
|
|
SecretKey: process.env.TENCENT_SECRET_KEY || '',
|
||
|
|
Bucket: 'qingyuan-erp-files-1310040146',
|
||
|
|
Region: 'ap-hongkong'
|
||
|
|
};
|
||
|
|
const cos = new COS(cosConfig);
|
||
|
|
const imageFormats = ['jpg', 'jpeg', 'png', 'gif', 'webp', 'bmp', 'svg'];
|
||
|
|
|
||
|
|
app.post('/api/upload/single', upload.single('file'), async (req, res) => {
|
||
|
|
try {
|
||
|
|
if (!req.file) return res.status(400).json({ success: false, error: '没有上传文件' });
|
||
|
|
|
||
|
|
console.log('接收到文件:', req.file.originalname);
|
||
|
|
|
||
|
|
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;
|
||
|
|
|
||
|
|
console.log('准备上传到COS:', filename);
|
||
|
|
|
||
|
|
cos.putObject({
|
||
|
|
Bucket: cosConfig.Bucket,
|
||
|
|
Region: cosConfig.Region,
|
||
|
|
Key: filename,
|
||
|
|
Body: req.file.buffer,
|
||
|
|
ContentType: req.file.mimetype
|
||
|
|
}, (err, data) => {
|
||
|
|
if (err) {
|
||
|
|
console.error('COS上传失败:', err);
|
||
|
|
return res.status(500).json({ success: false, error: '上传失败' });
|
||
|
|
}
|
||
|
|
|
||
|
|
console.log('COS上传成功:', data);
|
||
|
|
|
||
|
|
const fileUrl = 'https://' + cosConfig.Bucket + '.cos.' + cosConfig.Region + '.myqcloud.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 uploadPromises = req.files.map(file => {
|
||
|
|
return new Promise((resolve, reject) => {
|
||
|
|
const ext = file.originalname.split('.').pop().toLowerCase();
|
||
|
|
const filename = 'uploads/' + Date.now() + '_' + Math.random().toString(36).substring(2, 8) + '.' + ext;
|
||
|
|
|
||
|
|
cos.putObject({
|
||
|
|
Bucket: cosConfig.Bucket,
|
||
|
|
Region: cosConfig.Region,
|
||
|
|
Key: filename,
|
||
|
|
Body: file.buffer,
|
||
|
|
ContentType: file.mimetype
|
||
|
|
}, (err, data) => {
|
||
|
|
if (err) reject(err);
|
||
|
|
else {
|
||
|
|
const fileUrl = 'https://' + cosConfig.Bucket + '.cos.' + cosConfig.Region + '.myqcloud.com/' + filename;
|
||
|
|
resolve({
|
||
|
|
url: fileUrl,
|
||
|
|
name: file.originalname,
|
||
|
|
size: file.size,
|
||
|
|
isImage: imageFormats.includes(ext)
|
||
|
|
});
|
||
|
|
}
|
||
|
|
});
|
||
|
|
});
|
||
|
|
});
|
||
|
|
|
||
|
|
const results = await Promise.all(uploadPromises);
|
||
|
|
res.json({ success: true, data: results });
|
||
|
|
} catch (error) {
|
||
|
|
console.error('批量上传失败:', error);
|
||
|
|
res.status(500).json({ success: false, error: '上传失败' });
|
||
|
|
}
|
||
|
|
});
|
||
|
|
|
||
|
|
// ==================== 404处理 ====================
|
||
|
|
app.use((req, res) => {
|
||
|
|
res.status(404).json({
|
||
|
|
success: false,
|
||
|
|
message: '端点未找到',
|
||
|
|
requested_url: req.originalUrl
|
||
|
|
});
|
||
|
|
});
|
||
|
|
|
||
|
|
// ==================== 错误处理 ====================
|
||
|
|
app.use((err, req, res, next) => {
|
||
|
|
console.error('服务器错误:', err.stack);
|
||
|
|
res.status(500).json({
|
||
|
|
success: false,
|
||
|
|
message: '服务器内部错误',
|
||
|
|
error: process.env.NODE_ENV === 'development' ? err.message : undefined
|
||
|
|
});
|
||
|
|
});
|
||
|
|
|
||
|
|
// ==================== 启动服务器 ====================
|
||
|
|
|
||
|
|
app.listen(PORT, '0.0.0.0', () => {
|
||
|
|
console.log(`
|
||
|
|
🚀 公司财务管理系统 - 最终生产后端
|
||
|
|
===========================================
|
||
|
|
📍 服务器地址: http://0.0.0.0:${PORT}
|
||
|
|
🌐 外部访问: http://43.161.248.209:${PORT}
|
||
|
|
|
||
|
|
🔗 核心API端点:
|
||
|
|
- 健康检查: /api/health
|
||
|
|
- 客户管理: /api/customers
|
||
|
|
- 供应商管理: /api/suppliers
|
||
|
|
- 项目管理: /api/projects
|
||
|
|
- 商品管理: /api/products
|
||
|
|
- 付款节点: /api/payment-nodes
|
||
|
|
- 付款记录: /api/payment-records
|
||
|
|
- 汇率管理: /api/exchange-rates
|
||
|
|
- 预支款管理: /api/advances
|
||
|
|
- 报销管理: /api/reimbursements
|
||
|
|
- 财务统计: /api/finance-stats
|
||
|
|
|
||
|
|
👤 测试账号:
|
||
|
|
- 用户名: admin
|
||
|
|
- 密码: X123c321@
|
||
|
|
|
||
|
|
✅ 所有API已就绪
|
||
|
|
✅ 前端应用已集成
|
||
|
|
✅ 数据库已连接
|
||
|
|
✅ 等待用户访问
|
||
|
|
|
||
|
|
⏰ 启动时间: ${new Date().toISOString()}
|
||
|
|
===========================================
|
||
|
|
`);
|
||
|
|
});
|