Files
a273825743 706dcc24eb 备份:PWA配置前的完整版本
包含所有最新开发的功能和修复,作为PWA正确配置前的备份点。
2026-06-13 12:44:48 +08:00

204 lines
8.3 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
const express = require('express');
const db = require('../db');
const { authenticate, requireAdmin } = require('../middleware/auth');
const router = express.Router();
// 获取项目收款记录
router.get('/', async (req, res) => {
try {
const { projectId } = req.query;
if (!projectId) {
return res.status(400).json({ success: false, message: '缺少项目ID' });
}
const result = await db.query(
`SELECT r.*, m.milestone_name, m.percentage as milestone_percentage,
u.name as creator_name
FROM project_receipts r
LEFT JOIN project_milestones m ON r.milestone_id = m.id
LEFT JOIN users u ON r.created_by = u.id
WHERE r.project_id = $1
ORDER BY r.receipt_date DESC, r.created_at DESC`,
[projectId]
);
res.json({ success: true, data: result.rows });
} catch (error) {
console.error('获取收款记录失败:', error);
res.status(500).json({ success: false, message: '获取收款记录失败' });
}
});
// 获取项目付款节点列表(用于收款时选择)
router.get('/milestones', async (req, res) => {
try {
const { projectId } = req.query;
if (!projectId) {
return res.status(400).json({ success: false, message: '缺少项目ID' });
}
const result = await db.query(
`SELECT id, milestone_name, percentage, amount, status, completion_progress
FROM project_milestones WHERE project_id = $1 ORDER BY id`,
[projectId]
);
res.json({ success: true, data: result.rows });
} catch (error) {
console.error('获取付款节点失败:', error);
res.status(500).json({ success: false, message: '获取付款节点失败' });
}
});
// 新增收款记录(同时写入 financial_records 统一记账)
router.post('/', authenticate, async (req, res) => {
const client = await db.pool.connect();
try {
await client.query('BEGIN');
const { project_id, receipt_type, milestone_id, amount, currency, exchange_rate, receipt_date, payer_name, description, voucher_url, counterparty_id } = req.body;
const userId = req.user?.id || req.user?.userId;
if (!project_id) {
await client.query('ROLLBACK');
return res.status(400).json({ success: false, message: '缺少项目ID' });
}
const amt = parseFloat(amount) || 0;
const rate = parseFloat(exchange_rate) || 1;
const amountCny = parseFloat((amt * rate).toFixed(2));
// 1. 写入 financial_records(统一记账)
const today = new Date();
const dateStr = today.toISOString().slice(0, 10).replace(/-/g, '');
const codeResult = await client.query(
"SELECT COUNT(*) as cnt FROM financial_records WHERE record_code LIKE $1",
[`FIN-${dateStr}%`]
);
const seq = String(parseInt(codeResult.rows[0].cnt) + 1).padStart(4, '0');
const recordCode = `FIN-${dateStr}-${seq}`;
// 确定分类
let categoryLevel2 = 'contract_payment';
if (receipt_type === 'advance') {
categoryLevel2 = 'customer_advance';
} else if (receipt_type === 'other') {
categoryLevel2 = 'other_income';
}
const frResult = await client.query(
`INSERT INTO financial_records
(record_code, txn_type, category_level1, category_level2, project_id, user_id, user_name,
amount_original, currency, exchange_rate, amount_cny, record_date,
counterparty_name, counterparty_type, counterparty_id, source, source_code, description, status)
VALUES ($1, 'income', $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, 'customer', $13, 'receipt', $14, $15, 'confirmed')
RETURNING id`,
[
recordCode, 'income', categoryLevel2, project_id,
userId, req.user?.name || req.user?.username || '',
amt, currency || 'CNY', rate, amountCny,
receipt_date, payer_name || '',
counterparty_id || null,
recordCode, description || ''
]
);
const financialRecordId = frResult.rows[0].id;
// 2. 写入 project_receipts
const receiptResult = await client.query(
`INSERT INTO project_receipts
(project_id, receipt_type, milestone_id, amount, currency, exchange_rate, amount_cny,
receipt_date, payer_name, description, voucher_url, financial_record_id, created_by)
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13)
RETURNING *`,
[project_id, receipt_type || 'node', milestone_id || null, amt, currency || 'CNY', rate, amountCny,
receipt_date, payer_name || '', description || '', voucher_url || '', financialRecordId, userId]
);
// 3. 如果是节点收款,更新里程碑完成进度
if (receipt_type === 'node' && milestone_id) {
const msResult = await client.query('SELECT amount FROM project_milestones WHERE id = $1', [milestone_id]);
if (msResult.rows.length > 0 && parseFloat(msResult.rows[0].amount) > 0) {
const totalReceived = await client.query(
`SELECT COALESCE(SUM(amount), 0) as total FROM project_receipts
WHERE milestone_id = $1 AND receipt_type = 'node'`,
[milestone_id]
);
const progress = Math.min(100, Math.round((parseFloat(totalReceived.rows[0].total) / parseFloat(msResult.rows[0].amount)) * 100));
await client.query(
`UPDATE project_milestones SET completion_progress = $1::numeric, status = CASE WHEN $1::numeric >= 100 THEN 'completed' ELSE 'in_progress' END, actual_date = CASE WHEN $1::numeric >= 100 THEN $2 ELSE actual_date END WHERE id = $3`,
[progress, receipt_date, milestone_id]
);
}
}
await client.query('COMMIT');
// 返回带里程碑名称的完整记录
const fullResult = await db.query(
`SELECT r.*, m.milestone_name FROM project_receipts r LEFT JOIN project_milestones m ON r.milestone_id = m.id WHERE r.id = $1`,
[receiptResult.rows[0].id]
);
res.json({ success: true, data: fullResult.rows[0] });
} catch (error) {
await client.query('ROLLBACK');
console.error('新增收款记录失败:', error);
res.status(500).json({ success: false, message: '新增收款记录失败' });
} finally {
client.release();
}
});
// 删除收款记录(同时删除 financial_records
router.delete('/:receiptId', authenticate, async (req, res) => {
const client = await db.pool.connect();
try {
await client.query('BEGIN');
const { receiptId } = req.params;
const receiptResult = await client.query(
'SELECT * FROM project_receipts WHERE id = $1',
[receiptId]
);
if (receiptResult.rowCount === 0) {
await client.query('ROLLBACK');
return res.status(404).json({ success: false, message: '收款记录不存在' });
}
const receipt = receiptResult.rows[0];
const financialRecordId = receipt.financial_record_id;
// 先删除 project_receipts(引用方),再删除 financial_records(被引用方)
await client.query('DELETE FROM project_receipts WHERE id = $1', [receiptId]);
if (financialRecordId) {
await client.query('DELETE FROM financial_records WHERE id = $1', [financialRecordId]);
}
// 如果是节点收款,重新计算里程碑进度
if (receipt.receipt_type === 'node' && receipt.milestone_id) {
const msResult = await client.query('SELECT amount FROM project_milestones WHERE id = $1', [receipt.milestone_id]);
if (msResult.rows.length > 0 && parseFloat(msResult.rows[0].amount) > 0) {
const totalReceived = await client.query(
`SELECT COALESCE(SUM(amount), 0) as total FROM project_receipts
WHERE milestone_id = $1 AND receipt_type = 'node'`,
[receipt.milestone_id]
);
const progress = Math.min(100, Math.round((parseFloat(totalReceived.rows[0].total) / parseFloat(msResult.rows[0].amount)) * 100));
await client.query(
`UPDATE project_milestones SET completion_progress = $1::numeric, status = CASE WHEN $1::numeric >= 100 THEN 'completed' WHEN $1::numeric > 0 THEN 'in_progress' ELSE 'pending' END WHERE id = $2`,
[progress, receipt.milestone_id]
);
}
}
await client.query('COMMIT');
res.json({ success: true, message: '删除成功' });
} catch (error) {
await client.query('ROLLBACK');
console.error('删除收款记录失败:', error);
res.status(500).json({ success: false, message: '删除收款记录失败' });
} finally {
client.release();
}
});
module.exports = router;