2026-05-15 12:25:08 +08:00
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 result = await db . query ( `
SELECT
p.id,
p.name,
p.customer_id,
p.project_manager_id,
p.contract_amount,
p.start_date,
p.end_date,
p.description,
p.status,
p.location,
c.name as customer_name,
2026-06-13 12:44:48 +08:00
u.name as manager_name,
COALESCE(expense_summary.total_expense, 0) as total_expense
2026-05-15 12:25:08 +08:00
FROM projects p
LEFT JOIN customers c ON p.customer_id = c.id
LEFT JOIN users u ON p.project_manager_id = u.id
2026-06-13 12:44:48 +08:00
LEFT JOIN (
SELECT project_id, SUM(amount_cny::numeric) as total_expense
FROM financial_records
WHERE txn_type = 'expense' AND status = 'confirmed'
GROUP BY project_id
) expense_summary ON p.id = expense_summary.project_id
2026-05-15 12:25:08 +08:00
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 : process . env . NODE_ENV === 'development' ? error . message : '操作失败'
});
}
});
router . get ( '/:id' , async ( req , res ) => {
try {
const { id } = req . params ;
// 获取项目基本信息
const projectResult = await db . query ( `
SELECT
p.*,
c.name as customer_name,
u.name as manager_name
FROM projects p
LEFT JOIN customers c ON p.customer_id = c.id
LEFT JOIN users u ON p.project_manager_id = u.id
WHERE p.id = $1
` , [ id ]);
if ( projectResult . rows . length > 0 ) {
const project = projectResult . rows [ 0 ];
// 获取项目合同信息
const contractResult = await db . query ( `
SELECT * FROM project_contracts
WHERE project_id = $1
ORDER BY created_at DESC
LIMIT 1
` , [ id ]);
const contract = contractResult . rows [ 0 ];
// 从合同表读取质保金数据,如果没有则使用默认值
const warrantyPercent = contract ? . warranty_deposit_percentage || 5 ;
const warrantyMonths = contract ? . warranty_period || 12 ;
const contractAmount = parseFloat ( project . contract_amount || 0 );
// 计算质保金金额:合同金额 * 质保比例 / 100
const warrantyAmount = Math . round ( contractAmount * warrantyPercent / 100 );
// 计算质保期结束日期
const warrantyStartDate = project . end_date ;
const warrantyEndDate = warrantyStartDate
? new Date ( new Date ( warrantyStartDate ). getTime () + warrantyMonths * 30 * 24 * 60 * 60 * 1000 ). toISOString ()
: null ;
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 : contract ? . contract_period || 180 ,
project_manager_id : project . project_manager_id ,
manager_id : project . project_manager_id ,
manager_name : project . manager_name || '未知经理' ,
location : project . location || '' ,
work_quantity : '' ,
project_situation : project . description || '' ,
settlement_type : contract ? . settlement_method || 'lump_sum' ,
has_warranty : true ,
warranty_amount : warrantyAmount . toString (),
warranty_percent : warrantyPercent . toString (),
warranty_months : warrantyMonths ,
warranty_start_date : warrantyStartDate ,
warranty_end_date : warrantyEndDate ,
warranty_status : 'pending'
}
});
} else {
res . status ( 404 ). json ({
success : false ,
message : '项目不存在'
});
}
} catch ( error ) {
console . error ( '获取项目详情失败:' , error );
res . status ( 500 ). json ({
success : false ,
message : '获取项目详情失败' ,
error : process . env . NODE_ENV === 'development' ? error . message : '操作失败'
});
}
});
router . get ( '/:id/contracts' , async ( req , res ) => {
try {
const { id } = req . params ;
const result = await db . query ( `
SELECT * FROM project_contracts
WHERE project_id = $1
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 : process . env . NODE_ENV === 'development' ? error . message : '操作失败'
});
}
});
router . get ( '/:id/subcontracts' , async ( req , res ) => {
try {
const { id } = req . params ;
const result = await db . query ( `
SELECT * FROM subcontracts
WHERE project_id = $1
ORDER BY created_at DESC
` , [ id ]);
// 解析unit_price_items字段
const subcontracts = result . rows . map ( subcontract => {
if ( subcontract . unit_price_items ) {
try {
subcontract . unit_price_items = JSON . parse ( subcontract . unit_price_items );
} catch ( error ) {
subcontract . unit_price_items = [];
}
} else {
subcontract . unit_price_items = [];
}
return subcontract ;
});
res . json ({
success : true ,
data : subcontracts
});
} catch ( error ) {
console . error ( '获取项目分包失败:' , error );
res . status ( 500 ). json ({
success : false ,
message : '获取项目分包失败' ,
error : process . env . NODE_ENV === 'development' ? error . message : '操作失败'
});
}
});
router . post ( '/:id/subcontracts' , async ( req , res ) => {
try {
const { id } = req . params ;
const { subcontractor_id , subcontractor_name , contract_amount , currency , settlement_type , other_terms , payment_description , unit_price_items , start_date , end_date , work_days , status } = req . body ;
const unitPriceItemsJson = unit_price_items ? JSON . stringify ( unit_price_items ) : null ;
2026-05-15 12:02:24 +08:00
const result = await db . query (
2026-05-15 12:25:08 +08:00
`INSERT INTO subcontracts (project_id, subcontractor_id, subcontractor_name, contract_amount, currency, settlement_type, other_terms, payment_description, unit_price_items, start_date, end_date, work_days, paid_amount, status, created_at, updated_at)
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP)
2026-05-15 12:02:24 +08:00
RETURNING id` ,
2026-05-15 12:25:08 +08:00
[ id , subcontractor_id , subcontractor_name , contract_amount , currency || 'CNY' , settlement_type || 'lump_sum' , other_terms , payment_description , unitPriceItemsJson , start_date , end_date , work_days , 0 , status || 'active' ]
);
const subcontractId = ( result . rows [ 0 ] ? . id || result . rows ? .[ 0 ] ? . id );
res . json ({
success : true ,
message : '新增分包成功' ,
data : {
id : subcontractId ,
project_id : id ,
subcontractor_id ,
subcontractor_name ,
contract_amount ,
currency : currency || 'CNY' ,
settlement_type : settlement_type || 'lump_sum' ,
other_terms ,
payment_description ,
unit_price_items ,
start_date ,
end_date ,
work_days ,
paid_amount : 0 ,
status : status || 'active' ,
created_at : new Date (). toISOString ()
}
});
} catch ( error ) {
console . error ( '新增项目分包失败:' , error );
res . status ( 500 ). json ({
success : false ,
message : '新增项目分包失败' ,
error : process . env . NODE_ENV === 'development' ? error . message : '操作失败'
});
}
});
router . get ( '/:id/materials' , async ( req , res ) => {
try {
const { id } = req . params ;
const result = await db . query ( `
SELECT * FROM project_materials
WHERE project_id = $1
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 : process . env . NODE_ENV === 'development' ? error . message : '操作失败'
});
}
});
router . get ( '/:id/milestones' , async ( req , res ) => {
try {
const { id } = req . params ;
const result = await db . query ( `
SELECT * FROM project_milestones
WHERE project_id = $1
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 : process . env . NODE_ENV === 'development' ? error . message : '操作失败'
});
}
});
router . get ( '/:id/finances' , async ( req , res ) => {
try {
const { id } = req . params ;
const result = await db . query ( `
SELECT * FROM project_finances
WHERE project_id = $1
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 : process . env . NODE_ENV === 'development' ? error . message : '操作失败'
});
}
});
router . get ( '/:id/warranty-deposits' , async ( req , res ) => {
try {
const { id } = req . params ;
const result = await db . query ( `
SELECT * FROM warranty_deposits
WHERE project_id = $1
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 : process . env . NODE_ENV === 'development' ? error . message : '操作失败'
});
}
});
router . get ( '/: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 : process . env . NODE_ENV === 'development' ? error . message : '操作失败'
});
}
});
router . post ( '/' , async ( req , res ) => {
try {
const { name , code , customer_id , manager_id , contract_amount , start_date , end_date , description , status , location , type_template_id } = req . body ;
const projectCode = code || 'PROJ' + new Date (). toISOString (). slice ( 0 , 10 ). replace ( /-/g , '' ) +
String ( Math . floor ( Math . random () * 10000 )). padStart ( 4 , '0' );
const result = await db . query (
2026-05-17 01:10:24 +08:00
`INSERT INTO projects (name, project_code, customer_id, project_manager_id, contract_amount, start_date, end_date, description, status, location, type_template_id, created_at, updated_at)
2026-05-15 12:25:08 +08:00
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP)
RETURNING id` ,
[ name , projectCode , customer_id || null , manager_id || null , contract_amount || 0 , start_date || '' , end_date || '' , description || '' , status || 'planning' , location || '' , type_template_id || null ]
);
const projectId = result . rows [ 0 ] ? . id || result . rows ? .[ 0 ] ? . id ;
if ( type_template_id && projectId ) {
const tplResult = await db . query ( 'SELECT phases FROM project_type_templates WHERE id = $1' , [ type_template_id ]);
if ( tplResult . rows . length > 0 ) {
const phases = tplResult . rows [ 0 ]. phases ;
if ( Array . isArray ( phases )) {
for ( const phase of phases ) {
const subItems = ( phase . sub_items || []). map ( item => ({ name : item , completed : false }));
await db . query (
`INSERT INTO project_phases (project_id, template_key, phase_name, phase_order, phase_type, depends_on, sub_items, status) VALUES ($1, $2, $3, $4, $5, $6, $7, $8)` ,
[ projectId , phase . key || `phase_ ${ phase . order } ` , phase . name , phase . order , phase . type || 'serial' , phase . depends || [], JSON . stringify ( subItems ), phase . order === 1 ? 'in_progress' : 'pending' ]
);
}
const firstPhase = phases . find ( p => p . order === 1 );
await db . query ( 'UPDATE projects SET current_phase = $1 WHERE id = $2' , [ firstPhase ? firstPhase . name : '' , projectId ]);
}
}
}
res . json ({
success : true ,
message : '项目创建成功' ,
data : { id : projectId , code : projectCode }
});
} catch ( error ) {
console . error ( '创建项目失败:' , error );
res . status ( 500 ). json ({
success : false ,
message : '创建项目失败' ,
error : process . env . NODE_ENV === 'development' ? error . message : '操作失败'
});
}
});
router . delete ( '/:id' , authenticate , requireAdmin , async ( req , res ) => {
try {
const { id } = req . params ;
await db . query ( 'DELETE FROM projects WHERE id = $1' , [ id ]);
res . json ({ success : true , message : '项目已删除' });
} catch ( error ) {
console . error ( '删除项目失败:' , error );
res . status ( 500 ). json ({ success : false , message : '操作失败' });
}
});
router . put ( '/:id' , async ( req , res ) => {
try {
const { id } = req . params ;
const { name , manager_id , location , start_date , end_date , description , status , contract_amount } = req . body ;
const updates = [];
const values = [];
let paramIndex = 1 ;
if ( name !== undefined && name !== null ) {
updates . push ( `name = $ ${ paramIndex ++ } ` );
values . push ( name );
}
if ( manager_id !== undefined && manager_id !== null ) {
updates . push ( `project_manager_id = $ ${ paramIndex ++ } ` );
values . push ( manager_id );
}
if ( location !== undefined && location !== null ) {
updates . push ( `location = $ ${ paramIndex ++ } ` );
values . push ( location );
}
if ( start_date !== undefined && start_date !== null ) {
updates . push ( `start_date = $ ${ paramIndex ++ } ` );
values . push ( start_date );
}
if ( end_date !== undefined && end_date !== null ) {
updates . push ( `end_date = $ ${ paramIndex ++ } ` );
values . push ( end_date );
}
if ( description !== undefined && description !== null ) {
updates . push ( `description = $ ${ paramIndex ++ } ` );
values . push ( description );
}
if ( status !== undefined && status !== null ) {
updates . push ( `status = $ ${ paramIndex ++ } ` );
values . push ( status );
}
if ( contract_amount !== undefined && contract_amount !== null ) {
updates . push ( `contract_amount = $ ${ paramIndex ++ } ` );
values . push ( contract_amount );
}
updates . push ( `updated_at = CURRENT_TIMESTAMP` );
if ( updates . length > 1 ) {
values . push ( id );
await db . query (
`UPDATE projects SET ${ updates . join ( ', ' ) } WHERE id = $ ${ paramIndex } ` ,
values
);
}
2026-06-13 12:44:48 +08:00
// 合同金额变更时,自动按比例更新付款节点金额
if ( contract_amount !== undefined && contract_amount !== null ) {
await db . query (
`UPDATE project_milestones SET amount = ROUND($1 * percentage / 100, 2) WHERE project_id = $2` ,
[ contract_amount , id ]
);
}
2026-05-15 12:25:08 +08:00
if ( start_date && end_date ) {
const start = new Date ( start_date );
const end = new Date ( end_date );
const contractPeriod = Math . floor (( end . getTime () - start . getTime ()) / ( 1000 * 60 * 60 * 24 )) + 1 ;
await db . query (
'UPDATE project_contracts SET start_date = $1, end_date = $2, contract_period = $3 WHERE project_id = $4' ,
[ start_date , end_date , contractPeriod , id ]
);
}
const updatedResult = await db . query ( 'SELECT * FROM projects WHERE id = $1' , [ id ]);
res . json ({ success : true , data : updatedResult . rows [ 0 ] });
} catch ( error ) {
console . error ( '更新项目失败:' , error );
res . status ( 500 ). json ({ success : false , message : '操作失败' });
}
});
router . put ( '/:id/contract' , async ( req , res ) => {
try {
const { id } = req . params ;
const {
project_overview ,
settlement_type ,
contract_total ,
tax_included ,
unit_price_items ,
payment_nodes ,
other_info ,
contract_file
} = req . body ;
// 1. 更新项目基本信息
await db . query (
`UPDATE projects
SET description = $1, contract_amount = $2
WHERE id = $3` ,
[ project_overview , contract_total , id ]
);
// 2. 更新或创建项目合同
const contractResult = await db . query (
`SELECT * FROM project_contracts WHERE project_id = $1` ,
[ id ]
);
if ( contractResult . rows . length > 0 ) {
// 更新现有合同
await db . query (
`UPDATE project_contracts
SET settlement_method = $1, contract_amount = $2, contract_file = $3, other_info = $4, tax_included = $5
WHERE project_id = $6` ,
[ settlement_type , contract_total , contract_file , other_info , tax_included , id ]
);
} else {
// 创建新合同
const contractCode = `CONTRACT- ${ Date . now () } ` ;
await db . query (
`INSERT INTO project_contracts (project_id, contract_code, contract_amount, settlement_method, contract_file, other_info, tax_included, created_at, updated_at)
2026-05-15 12:02:24 +08:00
VALUES ($1, $2, $3, $4, $5, $6, $7, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP)
2026-05-15 12:25:08 +08:00
RETURNING id` ,
[ id , contractCode , contract_total , settlement_type , contract_file , other_info , tax_included ]
);
}
// 3. 处理付款节点
if ( payment_nodes && Array . isArray ( payment_nodes )) {
// 删除旧的付款节点
await db . query ( `DELETE FROM project_milestones WHERE project_id = $1` , [ id ]);
// 创建新的付款节点
for ( const node of payment_nodes ) {
await db . query (
`INSERT INTO project_milestones (project_id, milestone_name, condition, percentage, amount, status, created_at, updated_at)
2026-05-15 12:02:24 +08:00
VALUES ($1, $2, $3, $4, $5, $6, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP)
2026-05-15 12:25:08 +08:00
RETURNING id` ,
[ id , node . name , node . condition || '' , node . percentage , node . amount , 'pending' ]
);
}
2026-06-13 12:44:48 +08:00
} else if ( contract_total ) {
// 没有传付款节点但合同金额变了,按比例更新现有里程碑金额
await db . query (
`UPDATE project_milestones SET amount = ROUND($1 * percentage / 100, 2) WHERE project_id = $2` ,
[ contract_total , id ]
);
2026-05-15 12:25:08 +08:00
}
// 4. 处理单价项
if ( unit_price_items && Array . isArray ( unit_price_items ) && settlement_type === 'unit_price' ) {
// 删除旧的材料项
await db . query ( `DELETE FROM project_materials WHERE project_id = $1` , [ id ]);
// 创建新的材料项
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)
2026-05-15 12:02:24 +08:00
VALUES ($1, $2, $3, $4, $5, $6, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP)
2026-05-15 12:25:08 +08:00
RETURNING id` ,
[ id , item . name , item . unit , item . quantity , item . price , item . total ]
);
}
}
res . json ({
success : true ,
message : '合同细节保存成功'
});
} catch ( error ) {
console . error ( '保存合同细节失败:' , error );
res . status ( 500 ). json ({ success : false , message : '操作失败' });
}
});
2026-06-13 12:44:48 +08:00
router . get ( '/:id/financial-details' , async ( req , res ) => {
try {
const { id } = req . params ;
const { txn_type , category_level1 , category_level2 , date_from , date_to , page = 1 , pageSize = 20 } = req . query ;
let sql = `SELECT fr.*, p.name as project_name FROM financial_records fr LEFT JOIN projects p ON fr.project_id = p.id WHERE fr.project_id = $1 AND fr.status != 'voided'` ;
const params = [ id ];
let idx = 2 ;
if ( txn_type ) { params . push ( txn_type ); sql += ` AND fr.txn_type = $ ${ idx ++ } ` ; }
if ( category_level1 ) { params . push ( category_level1 ); sql += ` AND fr.category_level1 = $ ${ idx ++ } ` ; }
if ( category_level2 ) { params . push ( category_level2 ); sql += ` AND fr.category_level2 = $ ${ idx ++ } ` ; }
if ( date_from ) { params . push ( date_from ); sql += ` AND fr.record_date >= $ ${ idx ++ } ` ; }
if ( date_to ) { params . push ( date_to ); sql += ` AND fr.record_date <= $ ${ idx ++ } ` ; }
const countResult = await db . query ( `SELECT COUNT(*) as total FROM ( ${ sql } ) sub` , params );
const total = parseInt ( countResult . rows [ 0 ]. total );
sql += ' ORDER BY fr.record_date DESC, fr.created_at DESC' ;
const offset = ( parseInt ( page ) - 1 ) * parseInt ( pageSize );
params . push ( parseInt ( pageSize ));
sql += ` LIMIT $ ${ idx ++ } ` ;
params . push ( offset );
sql += ` OFFSET $ ${ idx ++ } ` ;
const result = await db . query ( sql , params );
res . json ({
success : true ,
data : result . rows ,
pagination : { page : parseInt ( page ), pageSize : parseInt ( pageSize ), total , totalPages : Math . ceil ( total / parseInt ( pageSize )) }
});
} catch ( error ) {
console . error ( '获取项目财务明细失败:' , error );
res . status ( 500 ). json ({ success : false , message : '获取项目财务明细失败' });
}
});
2026-05-15 12:25:08 +08:00
router . get ( '/:id/cost-summary' , async ( req , res ) => {
try {
const { id } = req . params ;
2026-06-13 12:44:48 +08:00
2026-05-15 12:25:08 +08:00
const projectResult = await db . query ( 'SELECT * FROM projects WHERE id = $1' , [ id ]);
if ( projectResult . rows . length === 0 ) {
return res . status ( 404 ). json ({ success : false , message : '项目不存在' });
}
const project = projectResult . rows [ 0 ];
2026-06-13 12:44:48 +08:00
const incomeResult = await db . query ( `
SELECT category_level2, COALESCE(SUM(amount_cny), 0) as total_amount
FROM financial_records
WHERE project_id = $1 AND txn_type = 'income' AND status != 'voided'
GROUP BY category_level2
` , [ id ]);
const expenseResult = await db . query ( `
SELECT category_level1, category_level2, COALESCE(SUM(amount_cny), 0) as total_amount
FROM financial_records
WHERE project_id = $1 AND txn_type = 'expense' AND status != 'voided'
GROUP BY category_level1, category_level2
` , [ id ]);
const totalIncomeRow = await db . query ( `
SELECT COALESCE(SUM(amount_cny), 0) as total FROM financial_records
WHERE project_id = $1 AND txn_type = 'income' AND status != 'voided'
` , [ id ]);
const totalExpenseRow = await db . query ( `
SELECT COALESCE(SUM(amount_cny), 0) as total FROM financial_records
WHERE project_id = $1 AND txn_type = 'expense' AND status != 'voided'
` , [ id ]);
const incomeByCategory = {};
let totalIncome = parseFloat ( totalIncomeRow . rows [ 0 ]. total );
incomeResult . rows . forEach ( row => {
incomeByCategory [ row . category_level2 ] = parseFloat ( row . total_amount );
2026-05-15 12:25:08 +08:00
});
2026-06-13 12:44:48 +08:00
const expenseByCategory = {};
const expenseByLevel1 = {};
let totalExpense = parseFloat ( totalExpenseRow . rows [ 0 ]. total );
expenseResult . rows . forEach ( row => {
expenseByCategory [ row . category_level2 ] = parseFloat ( row . total_amount );
if ( ! expenseByLevel1 [ row . category_level1 ]) expenseByLevel1 [ row . category_level1 ] = 0 ;
expenseByLevel1 [ row . category_level1 ] += parseFloat ( row . total_amount );
});
2026-05-15 12:25:08 +08:00
res . json ({
success : true ,
data : {
project_name : project . name ,
contract_amount : project . contract_amount || 0 ,
2026-06-13 12:44:48 +08:00
income : {
total : totalIncome ,
by_category : incomeByCategory
2026-05-15 12:25:08 +08:00
},
2026-06-13 12:44:48 +08:00
expense : {
total : totalExpense ,
by_category : expenseByCategory ,
by_level1 : expenseByLevel1
},
total_cost : totalExpense ,
profit : totalIncome - totalExpense
2026-05-15 12:25:08 +08:00
}
});
} catch ( error ) {
console . error ( '获取项目成本统计失败:' , error );
res . status ( 500 ). json ({
success : false ,
message : '获取项目成本统计失败' ,
error : process . env . NODE_ENV === 'development' ? error . message : '操作失败'
});
}
});
module . exports = router ;
router . get ( '/:id/phases' , async ( req , res ) => {
try {
const { id } = req . params ;
const result = await db . query (
'SELECT * FROM project_phases WHERE project_id = $1 ORDER BY phase_order' ,
[ id ]
);
res . json ({ success : true , data : result . rows });
} catch ( error ) {
console . error ( '获取项目阶段失败:' , error );
res . status ( 500 ). json ({ success : false , message : '获取项目阶段失败' });
}
});
router . post ( '/:id/phases/init' , async ( req , res ) => {
try {
const { id } = req . params ;
const { template_id } = req . body ;
const templateResult = await db . query ( 'SELECT phases FROM project_type_templates WHERE id = $1' , [ template_id ]);
if ( templateResult . rows . length === 0 ) {
return res . status ( 404 ). json ({ success : false , message : '模板不存在' });
}
const phases = templateResult . rows [ 0 ]. phases ;
if ( ! Array . isArray ( phases )) {
return res . status ( 400 ). json ({ success : false , message : '模板阶段数据格式错误' });
}
const existing = await db . query ( 'SELECT id FROM project_phases WHERE project_id = $1' , [ id ]);
if ( existing . rows . length > 0 ) {
return res . status ( 400 ). json ({ success : false , message : '该项目已有阶段数据' });
}
for ( const phase of phases ) {
const subItems = ( phase . sub_items || []). map ( item => ({ name : item , completed : false }));
await db . query (
`INSERT INTO project_phases (project_id, template_key, phase_name, phase_order, phase_type, depends_on, sub_items, status)
VALUES ($1, $2, $3, $4, $5, $6, $7, $8)` ,
[ id , phase . key || `phase_ ${ phase . order } ` , phase . name , phase . order , phase . type || 'serial' , phase . depends || [], JSON . stringify ( subItems ), phase . order === 1 ? 'in_progress' : 'pending' ]
);
}
const firstPhase = phases . find ( p => p . order === 1 );
await db . query (
'UPDATE projects SET type_template_id = $1, current_phase = $2, phase_progress = 0, updated_at = CURRENT_TIMESTAMP WHERE id = $3' ,
[ template_id , firstPhase ? firstPhase . name : '' , id ]
);
res . json ({ success : true , message : '阶段初始化成功' });
} catch ( error ) {
console . error ( '初始化项目阶段失败:' , error );
res . status ( 500 ). json ({ success : false , message : '初始化项目阶段失败' });
}
});
router . put ( '/:id/phases/:phaseId/complete' , async ( req , res ) => {
try {
const { id , phaseId } = req . params ;
2026-05-17 10:01:43 +08:00
const { remark , photos , completed_by , attachments } = req . body ;
2026-05-15 12:25:08 +08:00
const phaseResult = await db . query ( 'SELECT * FROM project_phases WHERE id = $1 AND project_id = $2' , [ phaseId , id ]);
if ( phaseResult . rows . length === 0 ) {
return res . status ( 404 ). json ({ success : false , message : '阶段不存在' });
}
const phase = phaseResult . rows [ 0 ];
2026-05-17 10:01:43 +08:00
const allAttachments = attachments || photos || phase . photos || [];
2026-05-15 12:25:08 +08:00
await db . query (
`UPDATE project_phases SET status = 'completed', completed_at = NOW(), completed_by = $1, remark = $2, photos = $3 WHERE id = $4` ,
2026-05-17 10:01:43 +08:00
[ completed_by || null , remark || phase . remark , JSON . stringify ( allAttachments ), phaseId ]
2026-05-15 12:25:08 +08:00
);
const allPhases = await db . query ( 'SELECT * FROM project_phases WHERE project_id = $1 ORDER BY phase_order' , [ id ]);
const completedCount = allPhases . rows . filter ( p => p . status === 'completed' ). length ;
const totalCount = allPhases . rows . length ;
const progress = totalCount > 0 ? Math . round (( completedCount / totalCount ) * 100 ) : 0 ;
let nextPhase = null ;
for ( const p of allPhases . rows ) {
if ( p . status === 'pending' ) {
const deps = p . depends_on || [];
const depsMet = deps . length === 0 || deps . every ( depId => {
const depPhase = allPhases . rows . find ( ap => ap . id === depId || ap . phase_order === depId );
return depPhase && depPhase . status === 'completed' ;
});
if ( depsMet ) {
nextPhase = p ;
break ;
}
}
}
if ( nextPhase ) {
await db . query ( "UPDATE project_phases SET status = 'in_progress', started_at = NOW() WHERE id = $1" , [ nextPhase . id ]);
const newStatus = progress === 100 ? 'completed' : 'active' ;
await db . query (
'UPDATE projects SET current_phase = $1, phase_progress = $2, status = $3, updated_at = CURRENT_TIMESTAMP WHERE id = $4' ,
[ nextPhase . phase_name , progress , newStatus , id ]
);
} else {
const newStatus = progress === 100 ? 'completed' : 'active' ;
await db . query (
'UPDATE projects SET current_phase = $1, phase_progress = $2, status = $3, updated_at = CURRENT_TIMESTAMP WHERE id = $4' ,
[ '' , progress , newStatus , id ]
);
}
res . json ({ success : true , message : '阶段完成' , data : { progress , nextPhase : nextPhase ? nextPhase . phase_name : null } });
} catch ( error ) {
console . error ( '完成阶段失败:' , error );
res . status ( 500 ). json ({ success : false , message : '完成阶段失败' });
}
});
router . put ( '/:id/phases/:phaseId/reopen' , async ( req , res ) => {
try {
const { id , phaseId } = req . params ;
await db . query (
"UPDATE project_phases SET status = 'in_progress', completed_at = NULL, completed_by = NULL WHERE id = $1 AND project_id = $2" ,
[ phaseId , id ]
);
const allPhases = await db . query ( 'SELECT * FROM project_phases WHERE project_id = $1 ORDER BY phase_order' , [ id ]);
const completedCount = allPhases . rows . filter ( p => p . status === 'completed' ). length ;
const totalCount = allPhases . rows . length ;
const progress = totalCount > 0 ? Math . round (( completedCount / totalCount ) * 100 ) : 0 ;
const reopenedPhase = allPhases . rows . find ( p => p . id === parseInt ( phaseId ));
await db . query (
'UPDATE projects SET current_phase = $1, phase_progress = $2, status = $3, updated_at = CURRENT_TIMESTAMP WHERE id = $4' ,
[ reopenedPhase ? reopenedPhase . phase_name : '' , progress , 'active' , id ]
);
res . json ({ success : true , message : '阶段已重新打开' , data : { progress } });
} catch ( error ) {
console . error ( '重新打开阶段失败:' , error );
res . status ( 500 ). json ({ success : false , message : '重新打开阶段失败' });
}
});
router . put ( '/:id/phases/:phaseId/sub-item' , async ( req , res ) => {
try {
const { id , phaseId } = req . params ;
const { sub_item_index , completed } = req . body ;
const phaseResult = await db . query ( 'SELECT sub_items FROM project_phases WHERE id = $1 AND project_id = $2' , [ phaseId , id ]);
if ( phaseResult . rows . length === 0 ) {
return res . status ( 404 ). json ({ success : false , message : '阶段不存在' });
}
const subItems = phaseResult . rows [ 0 ]. sub_items || [];
if ( sub_item_index >= 0 && sub_item_index < subItems . length ) {
subItems [ sub_item_index ]. completed = completed ;
await db . query ( 'UPDATE project_phases SET sub_items = $1 WHERE id = $2' , [ JSON . stringify ( subItems ), phaseId ]);
}
res . json ({ success : true , message : '子项状态更新成功' });
} catch ( error ) {
console . error ( '更新子项状态失败:' , error );
res . status ( 500 ). json ({ success : false , message : '更新子项状态失败' });
}
});