2026-04-19 19:15:01 +08:00
/**
* 付款执行统一路由
* 遵循设计方案:采购-付款-物流-退库一体化流程设计方案.md
* 章节:七、付款执行统一页面
*
* 功能:
* - 整合所有支付类型:材料采购、一次运费、二次运费、预支、报销等
* - 执行付款时必须上传付款凭证
* - 统一的付款执行列表和操作界面
*/
const express = require ( 'express' );
const db = require ( '../db' );
const router = express . Router ();
/**
* 获取待执行付款列表
* 整合所有支付类型
*/
router . get ( '/pending' , async ( req , res ) => {
try {
const { payment_type } = req . query ;
const results = [];
// 1. 材料采购付款(来自付款计划)
if ( ! payment_type || payment_type === 'material' ) {
const materialPayments = await db . query ( `
SELECT
'material' as payment_type,
pp.id as source_id,
pp.stage as description,
pp.planned_amount as amount,
po.currency,
pp.planned_date as due_date,
s.name as payee_name,
po.code as order_code,
pp.status,
'付款计划' as source_type
FROM payment_plans pp
LEFT JOIN purchase_orders po ON pp.purchase_order_id = po.id
LEFT JOIN suppliers s ON po.supplier_id = s.id
WHERE pp.status IN ('pending', 'requested')
` );
results . push (... materialPayments . rows );
}
// 2. 一次运费付款
if ( ! payment_type || payment_type === 'primary_freight' ) {
const primaryFreight = await db . query ( `
SELECT
'primary_freight' as payment_type,
lr.id as source_id,
'一次运费' as description,
lr.primary_freight as amount,
lr.primary_freight_currency as currency,
lr.ship_date as due_date,
lc.name as payee_name,
po.code as order_code,
lr.primary_freight_status as status,
'物流单' as source_type
FROM logistics_records lr
LEFT JOIN purchase_orders po ON lr.purchase_order_id = po.id
LEFT JOIN logistics_companies lc ON lr.logistics_company_id = lc.id
WHERE lr.primary_freight_status IN ('pending', 'requested')
AND lr.primary_freight > 0
` );
results . push (... primaryFreight . rows );
}
// 3. 二次运费付款
if ( ! payment_type || payment_type === 'secondary_freight' ) {
const secondaryFreight = await db . query ( `
SELECT
'secondary_freight' as payment_type,
lr.id as source_id,
'二次运费' as description,
lr.secondary_freight as amount,
lr.secondary_freight_currency as currency,
lr.second_ship_date as due_date,
lr.driver_phone as payee_name,
po.code as order_code,
lr.secondary_freight_status as status,
'物流单' as source_type
FROM logistics_records lr
LEFT JOIN purchase_orders po ON lr.purchase_order_id = po.id
WHERE lr.secondary_freight_status IN ('pending', 'requested')
AND lr.secondary_freight > 0
` );
results . push (... secondaryFreight . rows );
}
// 4. 预支款
if ( ! payment_type || payment_type === 'advance' ) {
const advances = await db . query ( `
SELECT
'advance' as payment_type,
a.id as source_id,
a.purpose as description,
a.amount,
a.currency,
a.request_date as due_date,
a.applicant as payee_name,
NULL as order_code,
a.status,
'预支申请' as source_type
FROM advances a
WHERE a.status = 'approved'
` );
results . push (... advances . rows );
}
// 5. 报销
if ( ! payment_type || payment_type === 'reimbursement' ) {
const reimbursements = await db . query ( `
SELECT
'reimbursement' as payment_type,
r.id as source_id,
r.description,
r.total_amount as amount,
r.currency,
r.request_date as due_date,
r.applicant as payee_name,
NULL as order_code,
r.status,
'报销申请' as source_type
FROM reimbursements r
WHERE r.status = 'approved'
` );
results . push (... reimbursements . rows );
}
// 按日期排序
results . sort (( a , b ) => {
if ( ! a . due_date ) return 1 ;
if ( ! b . due_date ) return - 1 ;
return new Date ( a . due_date ) - new Date ( b . due_date );
});
res . json ({
success : true ,
data : results ,
count : results . length
});
} catch ( error ) {
console . error ( '获取待执行付款列表失败:' , error );
res . status ( 500 ). json ({
success : false ,
message : '获取待执行付款列表失败' ,
2026-05-15 12:02:24 +08:00
error : process . env . NODE_ENV === 'development' ? error . message : '操作失败'
2026-04-19 19:15:01 +08:00
});
}
});
/**
* 获取已执行付款记录
*/
router . get ( '/executed' , async ( req , res ) => {
try {
const results = [];
// 1. 材料采购付款记录
const materialPayments = await db . query ( `
SELECT
'material' as payment_type,
pp.id as source_id,
pp.stage as description,
pp.actual_amount as amount,
po.currency,
pp.actual_date as payment_date,
s.name as payee_name,
po.code as order_code,
'已支付' as status,
'付款计划' as source_type
FROM payment_plans pp
LEFT JOIN purchase_orders po ON pp.purchase_order_id = po.id
LEFT JOIN suppliers s ON po.supplier_id = s.id
WHERE pp.status = 'paid'
` );
results . push (... materialPayments . rows );
// 2. 一次运费付款记录
const primaryFreight = await db . query ( `
SELECT
'primary_freight' as payment_type,
lr.id as source_id,
'一次运费' as description,
lr.primary_freight as amount,
lr.primary_freight_currency as currency,
lr.final_arrival_date as payment_date,
lc.name as payee_name,
po.code as order_code,
'已支付' as status,
'物流单' as source_type
FROM logistics_records lr
LEFT JOIN purchase_orders po ON lr.purchase_order_id = po.id
LEFT JOIN logistics_companies lc ON lr.logistics_company_id = lc.id
WHERE lr.primary_freight_status = 'paid'
` );
results . push (... primaryFreight . rows );
// 3. 二次运费付款记录
const secondaryFreight = await db . query ( `
SELECT
'secondary_freight' as payment_type,
lr.id as source_id,
'二次运费' as description,
lr.secondary_freight as amount,
lr.secondary_freight_currency as currency,
lr.final_arrival_date as payment_date,
lr.driver_phone as payee_name,
po.code as order_code,
'已支付' as status,
'物流单' as source_type
FROM logistics_records lr
LEFT JOIN purchase_orders po ON lr.purchase_order_id = po.id
WHERE lr.secondary_freight_status = 'paid'
` );
results . push (... secondaryFreight . rows );
// 按日期排序(最新的在前)
results . sort (( a , b ) => {
if ( ! a . payment_date ) return 1 ;
if ( ! b . payment_date ) return - 1 ;
return new Date ( b . payment_date ) - new Date ( a . payment_date );
});
res . json ({
success : true ,
data : results ,
count : results . length
});
} catch ( error ) {
console . error ( '获取已执行付款记录失败:' , error );
res . status ( 500 ). json ({
success : false ,
message : '获取已执行付款记录失败' ,
2026-05-15 12:02:24 +08:00
error : process . env . NODE_ENV === 'development' ? error . message : '操作失败'
2026-04-19 19:15:01 +08:00
});
}
});
/**
* 执行付款
* 遵循设计方案:必须上传付款凭证
*/
router . post ( '/execute' , async ( req , res ) => {
try {
const { payment_type , source_id , amount , payment_date , voucher_url , remark , payee_account } = req . body ;
if ( ! payment_type || ! source_id ) {
return res . status ( 400 ). json ({ success : false , message : '缺少付款类型或来源ID' });
}
if ( ! voucher_url ) {
return res . status ( 400 ). json ({ success : false , message : '执行付款必须上传付款凭证' });
}
await db . query ( 'BEGIN TRANSACTION' );
try {
const paymentDate = payment_date || new Date (). toISOString (). slice ( 0 , 10 );
switch ( payment_type ) {
case 'material' :
await db . query ( `
UPDATE payment_plans
SET status = 'paid', actual_amount = ?, actual_date = ?, updated_at = CURRENT_TIMESTAMP
WHERE id = ?
` , [ amount , paymentDate , source_id ]);
const planResult = await db . query ( 'SELECT purchase_order_id FROM payment_plans WHERE id = $1' , [ source_id ]);
if ( planResult . rows . length > 0 ) {
const orderId = planResult . rows [ 0 ]. purchase_order_id ;
const orderStats = await db . query ( `
SELECT
SUM(CASE WHEN status = 'paid' THEN COALESCE(actual_amount, 0) ELSE 0 END) as paid_amount,
SUM(planned_amount) as total_amount
FROM payment_plans WHERE purchase_order_id = ?
` , [ orderId ]);
const { paid_amount , total_amount } = orderStats . rows [ 0 ];
let newStatus = 'confirmed' ;
if ( paid_amount >= total_amount ) {
newStatus = 'paid' ;
} else if ( paid_amount > 0 ) {
newStatus = 'partial_paid' ;
}
await db . query ( `
UPDATE purchase_orders SET paid_amount = ?, status = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ?
` , [ paid_amount , newStatus , orderId ]);
}
break ;
case 'primary_freight' :
await db . query ( `
UPDATE logistics_records
SET primary_freight_status = 'paid', primary_freight_document = ?, updated_at = CURRENT_TIMESTAMP
WHERE id = ?
` , [ voucher_url , source_id ]);
break ;
case 'secondary_freight' :
await db . query ( `
UPDATE logistics_records
SET secondary_freight_status = 'paid', updated_at = CURRENT_TIMESTAMP
WHERE id = ?
` , [ source_id ]);
break ;
case 'advance' :
await db . query ( `
UPDATE advances SET status = 'paid', updated_at = CURRENT_TIMESTAMP WHERE id = ?
` , [ source_id ]);
break ;
case 'reimbursement' :
await db . query ( `
UPDATE reimbursements SET status = 'paid', updated_at = CURRENT_TIMESTAMP WHERE id = ?
` , [ source_id ]);
break ;
default :
throw new Error ( '未知的付款类型' );
}
const recordCode = 'PAY-REC-' + Date . now ();
await db . query ( `
INSERT INTO payment_records
(code, payment_type, source_id, amount, currency, payment_date, voucher_url, payee_account, remark, created_at)
2026-05-15 12:02:24 +08:00
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, CURRENT_TIMESTAMP)
RETURNING id` ,
[ recordCode , payment_type , source_id , amount , 'CNY' , paymentDate , voucher_url , payee_account , remark ]);
2026-05-16 03:04:48 +08:00
let finProjectId = null ;
let finUserId = null ;
let finUserName = null ;
let finCategory1 = 'company' ;
let finCategory2 = 'other_company' ;
let finSourceCode = recordCode ;
let finCounterpartyName = null ;
let finDescription = remark || '' ;
switch ( payment_type ) {
case 'material' : {
const planRow = await db . query ( 'SELECT purchase_order_id FROM payment_plans WHERE id = $1' , [ source_id ]);
if ( planRow . rows . length > 0 ) {
const poRow = await db . query ( 'SELECT project_id, code FROM purchase_orders WHERE id = $1' , [ planRow . rows [ 0 ]. purchase_order_id ]);
if ( poRow . rows . length > 0 ) {
finProjectId = poRow . rows [ 0 ]. project_id ;
finSourceCode = poRow . rows [ 0 ]. code ;
}
}
finCategory1 = 'project' ;
finCategory2 = 'material' ;
finDescription = finDescription || '材料采购付款' ;
break ;
}
case 'primary_freight' :
case 'secondary_freight' : {
const lrRow = await db . query ( 'SELECT purchase_order_id FROM logistics_records WHERE id = $1' , [ source_id ]);
if ( lrRow . rows . length > 0 ) {
const poRow = await db . query ( 'SELECT project_id, code FROM purchase_orders WHERE id = $1' , [ lrRow . rows [ 0 ]. purchase_order_id ]);
if ( poRow . rows . length > 0 ) {
finProjectId = poRow . rows [ 0 ]. project_id ;
finSourceCode = poRow . rows [ 0 ]. code ;
}
}
finCategory1 = 'project' ;
finCategory2 = 'freight' ;
finDescription = finDescription || ( payment_type === 'primary_freight' ? '一次运费付款' : '二次运费付款' );
break ;
}
case 'advance' : {
const advRow = await db . query ( 'SELECT project_id, applicant_id, applicant, advance_code, purpose FROM advances WHERE id = $1' , [ source_id ]);
if ( advRow . rows . length > 0 ) {
finProjectId = advRow . rows [ 0 ]. project_id ;
finUserId = advRow . rows [ 0 ]. applicant_id ;
finUserName = advRow . rows [ 0 ]. applicant ;
finSourceCode = advRow . rows [ 0 ]. advance_code ;
finDescription = finDescription || advRow . rows [ 0 ]. purpose || '预支款' ;
}
finCategory1 = finProjectId ? 'project' : 'company' ;
finCategory2 = finProjectId ? 'other_project' : 'other_company' ;
break ;
}
case 'reimbursement' : {
const reimbRow = await db . query ( 'SELECT project_id, applicant_id, applicant, reimbursement_code, expense_type, description as reimb_desc FROM reimbursements WHERE id = $1' , [ source_id ]);
if ( reimbRow . rows . length > 0 ) {
finProjectId = reimbRow . rows [ 0 ]. project_id ;
finUserId = reimbRow . rows [ 0 ]. applicant_id ;
finUserName = reimbRow . rows [ 0 ]. applicant ;
finSourceCode = reimbRow . rows [ 0 ]. reimbursement_code ;
finDescription = finDescription || reimbRow . rows [ 0 ]. reimb_desc || '报销' ;
if ( reimbRow . rows [ 0 ]. expense_type === 'project' ) {
finCategory1 = 'project' ;
finCategory2 = 'other_project' ;
} else {
finCategory1 = 'company' ;
finCategory2 = 'other_company' ;
}
}
break ;
}
}
const finCode = 'FIN-' + Date . now ();
const finAmountCny = parseFloat ( amount ) || 0 ;
await db . 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,
source, source_id, source_code,
description, voucher_no)
VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12,$13,$14,$15,$16,$17,$18)` ,
[ finCode , 'expense' , finCategory1 , finCategory2 ,
finProjectId , finUserId , finUserName ,
amount , 'CNY' , 1 , finAmountCny ,
paymentDate , finCounterpartyName ,
payment_type , source_id , finSourceCode ,
finDescription , voucher_url ]);
2026-04-19 19:15:01 +08:00
await db . query ( 'COMMIT' );
res . json ({
success : true ,
message : '付款执行成功' ,
data : { record_code : recordCode }
});
} catch ( innerError ) {
await db . query ( 'ROLLBACK' );
throw innerError ;
}
} catch ( error ) {
console . error ( '执行付款失败:' , error );
res . status ( 500 ). json ({
success : false ,
message : '执行付款失败' ,
2026-05-15 12:02:24 +08:00
error : process . env . NODE_ENV === 'development' ? error . message : '操作失败'
2026-04-19 19:15:01 +08:00
});
}
});
/**
* 获取付款详情
*/
router . get ( '/detail/:payment_type/:source_id' , async ( req , res ) => {
try {
const { payment_type , source_id } = req . params ;
let result ;
switch ( payment_type ) {
case 'material' :
result = await db . query ( `
SELECT pp.*, po.code as order_code, s.name as supplier_name, s.country as supplier_country
FROM payment_plans pp
LEFT JOIN purchase_orders po ON pp.purchase_order_id = po.id
LEFT JOIN suppliers s ON po.supplier_id = s.id
WHERE pp.id = ?
` , [ source_id ]);
break ;
case 'primary_freight' :
case 'secondary_freight' :
result = await db . query ( `
SELECT lr.*, po.code as order_code, lc.name as logistics_company_name
FROM logistics_records lr
LEFT JOIN purchase_orders po ON lr.purchase_order_id = po.id
LEFT JOIN logistics_companies lc ON lr.logistics_company_id = lc.id
WHERE lr.id = ?
` , [ source_id ]);
break ;
case 'advance' :
result = await db . query ( 'SELECT * FROM advances WHERE id = $1' , [ source_id ]);
break ;
case 'reimbursement' :
result = await db . query ( 'SELECT * FROM reimbursements WHERE id = $1' , [ source_id ]);
break ;
default :
return res . status ( 400 ). json ({ success : false , message : '未知的付款类型' });
}
if ( result . rows . length === 0 ) {
return res . status ( 404 ). json ({ success : false , message : '付款记录不存在' });
}
res . json ({
success : true ,
data : result . rows [ 0 ]
});
} catch ( error ) {
console . error ( '获取付款详情失败:' , error );
res . status ( 500 ). json ({
success : false ,
message : '获取付款详情失败' ,
2026-05-15 12:02:24 +08:00
error : process . env . NODE_ENV === 'development' ? error . message : '操作失败'
2026-04-19 19:15:01 +08:00
});
}
});
/**
* 获取付款统计
*/
router . get ( '/statistics' , async ( req , res ) => {
try {
const stats = {
pending_count : 0 ,
pending_amount : 0 ,
executed_count : 0 ,
executed_amount : 0 ,
by_type : {}
};
// 统计待执行付款
const pendingResult = await db . query ( `
SELECT payment_type, COUNT(*) as count, SUM(amount) as total_amount
FROM (
SELECT 'material' as payment_type, pp.planned_amount as amount
FROM payment_plans pp WHERE pp.status IN ('pending', 'requested')
UNION ALL
SELECT 'primary_freight', lr.primary_freight
FROM logistics_records lr WHERE lr.primary_freight_status IN ('pending', 'requested') AND lr.primary_freight > 0
UNION ALL
SELECT 'secondary_freight', lr.secondary_freight
FROM logistics_records lr WHERE lr.secondary_freight_status IN ('pending', 'requested') AND lr.secondary_freight > 0
UNION ALL
SELECT 'advance', a.amount
FROM advances a WHERE a.status = 'approved'
UNION ALL
SELECT 'reimbursement', r.total_amount
FROM reimbursements r WHERE r.status = 'approved'
)
GROUP BY payment_type
` );
for ( const row of pendingResult . rows ) {
stats . pending_count += row . count ;
stats . pending_amount += row . total_amount || 0 ;
stats . by_type [ row . payment_type ] = {
pending_count : row . count ,
pending_amount : row . total_amount || 0
};
}
// 统计已执行付款
const executedResult = await db . query ( `
SELECT payment_type, COUNT(*) as count, SUM(amount) as total_amount
FROM (
SELECT 'material' as payment_type, pp.actual_amount as amount
FROM payment_plans pp WHERE pp.status = 'paid'
UNION ALL
SELECT 'primary_freight', lr.primary_freight
FROM logistics_records lr WHERE lr.primary_freight_status = 'paid'
UNION ALL
SELECT 'secondary_freight', lr.secondary_freight
FROM logistics_records lr WHERE lr.secondary_freight_status = 'paid'
)
GROUP BY payment_type
` );
for ( const row of executedResult . rows ) {
stats . executed_count += row . count ;
stats . executed_amount += row . total_amount || 0 ;
if ( ! stats . by_type [ row . payment_type ]) {
stats . by_type [ row . payment_type ] = {};
}
stats . by_type [ row . payment_type ]. executed_count = row . count ;
stats . by_type [ row . payment_type ]. executed_amount = row . total_amount || 0 ;
}
res . json ({
success : true ,
data : stats
});
} catch ( error ) {
console . error ( '获取付款统计失败:' , error );
res . status ( 500 ). json ({
success : false ,
message : '获取付款统计失败' ,
2026-05-15 12:02:24 +08:00
error : process . env . NODE_ENV === 'development' ? error . message : '操作失败'
2026-04-19 19:15:01 +08:00
});
}
});
module . exports = router ;