You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
718 lines
25 KiB
718 lines
25 KiB
'use strict';
|
|
const superagent = require('superagent');
|
|
const { spawn } = require('child_process');
|
|
const stream = require('stream');
|
|
const path = require('path');
|
|
const moment = require('moment');
|
|
|
|
module.exports.getTenderList = async (ctx, next) => {
|
|
try {
|
|
const { models } = ctx.app.fs.dc;
|
|
const { page, pageSize, userId } = ctx.request.query;
|
|
const where = {};
|
|
if (userId) {
|
|
where.userId = userId;
|
|
}
|
|
|
|
// 处理分页
|
|
const options = {
|
|
where,
|
|
order: [['updateAt', 'DESC']],
|
|
raw: true,
|
|
};
|
|
|
|
if (page && pageSize) {
|
|
options.offset = (page - 1) * pageSize;
|
|
options.limit = parseInt(pageSize);
|
|
}
|
|
|
|
const tenderList = await models.Tender.findAndCountAll(options);
|
|
ctx.body = tenderList;
|
|
ctx.status = 200;
|
|
} catch (error) {
|
|
ctx.logger.log(error);
|
|
ctx.status = 400;
|
|
ctx.body = {
|
|
message: typeof error == 'string' ? error : '获取标书列表失败'
|
|
};
|
|
}
|
|
}
|
|
|
|
module.exports.getTenderDetail = async (ctx, next) => {
|
|
try {
|
|
const { models } = ctx.app.fs.dc;
|
|
const tenderId = ctx.params.tenderId;
|
|
if (!tenderId) { throw '缺少参数' };
|
|
const tender = await models.Tender.findOne({
|
|
where: { id: tenderId },
|
|
include: [
|
|
{
|
|
model: models.TenderDirectory,
|
|
}
|
|
],
|
|
});
|
|
ctx.body = tender;
|
|
ctx.status = 200;
|
|
} catch (error) {
|
|
ctx.logger.log(error);
|
|
ctx.status = 400;
|
|
ctx.body = {
|
|
message: typeof error == 'string' ? error : '获取标书详情失败'
|
|
};
|
|
}
|
|
}
|
|
|
|
module.exports.addTender = async (ctx, next) => {
|
|
try {
|
|
const { models } = ctx.app.fs.dc;
|
|
const { name, fileUrl, onlyTechnicalPapers, userId, curIp } = ctx.request.body;
|
|
if (!name || !fileUrl) { throw '缺少参数' };
|
|
|
|
const tender = await models.Tender.create(
|
|
{ name, fileUrl, status: 'pending', userId },
|
|
{ returning: true }
|
|
);
|
|
|
|
await models.AiQueryRecord.create({
|
|
userId: userId,
|
|
ipAddress: curIp,
|
|
clientId: 'pep-feixiaoshang',
|
|
feature: '标书生成',
|
|
time: moment(),
|
|
});
|
|
|
|
ctx.body = tender;
|
|
ctx.status = 200;
|
|
|
|
// 执行生成目录任务,不影响此接口返回
|
|
generationDirectory(ctx, tender.id, fileUrl, onlyTechnicalPapers);
|
|
} catch (error) {
|
|
ctx.logger.log(error);
|
|
ctx.status = 400;
|
|
ctx.body = {
|
|
message: typeof error == 'string' ? error : '新增标书失败'
|
|
};
|
|
}
|
|
}
|
|
|
|
module.exports.retryGenerationDirectory = async (ctx, next) => {
|
|
try {
|
|
const { models } = ctx.app.fs.dc;
|
|
const id = ctx.params.tenderId;
|
|
if (!id) { throw '缺少参数' };
|
|
|
|
const tender = await models.Tender.update(
|
|
{ status: 'pending' },
|
|
{ where: { id }, returning: true }
|
|
);
|
|
|
|
ctx.body = tender;
|
|
ctx.status = 200;
|
|
|
|
// 执行生成目录任务,不影响此接口返回
|
|
generationDirectory(ctx, id, tender[1][0].fileUrl, tender[1][0].onlyTechnicalPapers);
|
|
} catch (error) {
|
|
ctx.logger.log(error);
|
|
ctx.status = 400;
|
|
ctx.body = {
|
|
message: typeof error == 'string' ? error : '重新生成目录失败'
|
|
};
|
|
}
|
|
}
|
|
|
|
async function generationDirectory(ctx, tenderId, fileUrl, onlyTechnicalPapers = false) {
|
|
const transaction = await ctx.app.fs.dc.orm.transaction();
|
|
const { models } = ctx.app.fs.dc;
|
|
try {
|
|
const { tenderAppKey } = ctx.app.fs.config.fastGpt;
|
|
const res = await superagent
|
|
.post(`${ctx.app.fs.config.fastGpt.apiUrl}/api/v1/chat/completions`)
|
|
.send({
|
|
"stream": false,
|
|
"detail": false,
|
|
"variables": { "generation_type": onlyTechnicalPapers ? '技术文件目录生成' : '目录生成' },
|
|
"messages": [
|
|
{
|
|
"role": "user",
|
|
"content": [
|
|
{
|
|
"type": "file_url",
|
|
"name": "文件",
|
|
"url": fileUrl
|
|
}
|
|
]
|
|
}
|
|
]
|
|
})
|
|
.set({
|
|
Authorization: `Bearer ${tenderAppKey}`,
|
|
"Content-Type": "application/json",
|
|
})
|
|
// .timeout(1000 * 60 * 24)
|
|
.timeout({
|
|
response: 1000 * 60 * 24, // 等待服务器发送数据的超时时间(毫秒)
|
|
deadline: 1000 * 60 * 24, // 整个请求完成的最大时间(毫秒)
|
|
}).then(async res => {
|
|
const directoryMd = res.body.choices[0].message.content;
|
|
await models.Tender.update(
|
|
{ status: 'success' },
|
|
{ where: { id: tenderId }, transaction }
|
|
);
|
|
await parseAndInsertDirectories(directoryMd, models, transaction, tenderId);
|
|
transaction.commit();
|
|
}, async error => {
|
|
await models.Tender.update(
|
|
{ status: 'fail' },
|
|
{ where: { id: tenderId } }
|
|
);
|
|
ctx.logger.log(error);
|
|
await transaction.rollback();
|
|
})
|
|
|
|
// const directoryMd = res.body.choices[0].message.content;
|
|
|
|
// await new Promise((resolve) => { setTimeout(resolve, 5000) });
|
|
// const directoryMd = "\n# 1. 投标书 \n# 2. 开标一览表 \n# 3. 分项报价表 \n# 4. 开标一览明细表 \n# 5. 服务标准及要求响应/偏离表 \n# 6. 商务条款响应/偏离表 \n# 7. 投标人应当提交的资格证明文件 \n## 7.1 江西省政府采购供应商资格信用承诺函 \n## 7.2 法定代表人授权书 \n## 7.3 投标人的资格声明 \n## 7.4 投标保证金凭证 \n## 7.5 制造商出具的授权函 \n## 7.6 联合体协议 \n## 7.7 本项目的特定资格证明材料 \n# 8. 为落实政府采购政策投标人须提供的证明材料 \n## 8.1 中小企业声明(服务) \n## 8.2 省级以上监狱管理局、戒毒管理局出具的属于监狱企业证明文件 \n## 8.3 残疾人福利性单位声明函 \n## 8.4 节能产品认证证书 \n# 9. 技术文件 \n## 9.1 对项目的理解 \n### 9.1.1 采购需求及项目背景 \n#### 9.1.1.1 采购需求 \n#### 9.1.1.2 建设背景 \n#### 9.1.1.3 建设目标 \n### 9.1.2 实施内容理解 \n#### 9.1.2.1 升级数据资源中心 \n##### 9.1.2.1.1 数据目录系统升级 \n##### 9.1.2.1.2 数据库管控 \n##### 9.1.2.1.3 数据治理系统升级 \n###### 9.1.2.1.3.1 数据处理系统 \n###### 9.1.2.1.3.2 数据标准管理 \n###### 9.1.2.1.3.3 数据质量管理 \n###### 9.1.2.1.3.4 元数据管理 \n###### 9.1.2.1.3.5 数据建模管理 \n###### 9.1.2.1.3.6 标签管理 \n###### 9.1.2.1.3.7 算法开发 \n#### 9.1.2.1.4 数据服务平台 \n#### 9.1.2.1.5 数据技术规范 \n#### 9.1.2.1.6 数据库能力 \n#### 9.1.2.2 技术支撑中心 \n##### 9.1.2.2.1 能力组件中心 \n##### 9.1.2.2.2 能力开放中心 \n##### 9.1.2.2.3 安全管控中心 \n##### 9.1.2.2.4 能力技术规范 \n#### 9.1.2.3 一体化服务门户 \n##### 9.1.2.3.1 工作门户 \n#### 9.1.2.4 国产化适配 \n### 9.1.3 满足采购需求外的技术功能和软件产品能力 \n#### 9.1.3.1 技术功能 \n#### 9.1.3.2 软件产品能力 \n## 9.2 工作规划描述 \n### 9.2.1 需求分析 \n### 9.2.2 总体设计 \n### 9.2.3 详细设计 \n### 9.2.4 软件实现 \n### 9.2.5 软件测试 \n### 9.2.6 用户培训 \n### 9.2.7 系统上线 \n### 9.2.8 项目验收 \n## 9.3 项目测试方案 \n### 9.3.1 测试目标和原则 \n### 9.3.2 测试内容 \n### 9.3.3 测试程序 \n## 9.4 质量保障 \n### 9.4.1 质量保障体系 \n### 9.4.2 质量保障措施 \n### 9.4.3 阶段评审 \n### 9.4.4 日常检查 \n### 9.4.5 安装维护检查 \n### 9.4.6 文档 \n### 9.4.7 软件系统、设备的集成、调试质量控制 \n## 9.5 项目管理机构及人员 \n### 9.5.1 项目管理组织机构 \n### 9.5.2 项目人员配置及分工 \n### 9.5.3 岗位职责及工作界面划分 \n## 9.6 培训计划和培训方案 \n### 9.6.1 培训计划 \n### 9.6.2 培训方案 \n#### 9.6.2.1 培训目标 \n#### 9.6.2.2 培训内容 \n#### 9.6.2.3 培训质量保证措施 \n## 9.7 售后服务与支撑 \n### 9.7.1 售后服务承诺 \n### 9.7.2 运维服务承诺 \n### 9.7.3 服务人员承诺 \n## 9.8 知识产权 \n## 9.9 项目保密 \n# 10. 其他资料"
|
|
} catch (error) {
|
|
// await transaction.rollback();
|
|
ctx.logger.log(error);
|
|
try {
|
|
await models.Tender.update(
|
|
{ status: 'fail' },
|
|
{ where: { id: tenderId } }
|
|
);
|
|
} catch (err) {
|
|
console.log(`修改状态失败,tender.id=${tenderId}`);
|
|
ctx.logger.log(err);
|
|
}
|
|
}
|
|
}
|
|
// 解析并插入目录树
|
|
async function parseAndInsertDirectories(input, models, transaction, tenderId) {
|
|
// 删除原目录
|
|
await models.TenderDirectory.destroy({ where: { tenderId }, transaction });
|
|
|
|
// 1. 解析输入字符串为树形结构
|
|
const lines = input.split('\n').filter(line => line.trim() !== '');
|
|
const tree = [];
|
|
const stack = [];
|
|
|
|
for (const line of lines) {
|
|
// 提取编号和名称
|
|
const match = line.match(/^(#+)\s+([\d.]+\s+.*)/);
|
|
if (!match) continue;
|
|
|
|
const level = match[1].length; // 计算层级
|
|
const fullName = match[2].trim(); // 获取完整名称(包括编号)
|
|
|
|
// 构建当前节点
|
|
const node = { level, fullName, children: [] };
|
|
|
|
// 根据层级插入到树中
|
|
while (stack.length > 0 && stack[stack.length - 1].level >= level) {
|
|
stack.pop(); // 回退到上一级
|
|
}
|
|
|
|
if (stack.length === 0) {
|
|
tree.push(node); // 如果栈为空,说明是根节点
|
|
} else {
|
|
stack[stack.length - 1].children.push(node); // 否则作为子节点添加
|
|
}
|
|
|
|
stack.push(node); // 当前节点入栈
|
|
}
|
|
|
|
// 2. 递归插入目录树到数据库
|
|
async function insertNode(node, parentId = null, order = 1) {
|
|
// 插入当前节点
|
|
const directory = await models.TenderDirectory.create({
|
|
tenderId,
|
|
name: node.fullName, // 使用完整名称(包含编号)
|
|
parentId,
|
|
order,
|
|
}, { transaction });
|
|
|
|
// 插入子节点
|
|
for (let i = 0; i < node.children.length; i++) {
|
|
await insertNode(node.children[i], directory.id, i + 1); // 子节点的 order 从 1 开始
|
|
}
|
|
}
|
|
|
|
// 遍历树并插入所有节点
|
|
for (let i = 0; i < tree.length; i++) {
|
|
await insertNode(tree[i], null, i + 1); // 根节点的 order 从 1 开始
|
|
}
|
|
}
|
|
|
|
module.exports.putTender = async (ctx, next) => {
|
|
try {
|
|
const { models } = ctx.app.fs.dc;
|
|
const tenderId = ctx.params.tenderId;
|
|
const body = ctx.request.body;
|
|
if (!tenderId) { throw '缺少参数' };
|
|
await models.Tender.update(
|
|
{ ...body },
|
|
{ where: { id: tenderId } }
|
|
);
|
|
ctx.status = 204;
|
|
} catch (error) {
|
|
ctx.logger.log(error);
|
|
ctx.status = 400;
|
|
ctx.body = {
|
|
message: typeof error == 'string' ? error : '修改标书失败'
|
|
};
|
|
}
|
|
}
|
|
|
|
module.exports.delTender = async (ctx, next) => {
|
|
const transaction = await ctx.app.fs.dc.orm.transaction();
|
|
try {
|
|
const { models } = ctx.app.fs.dc;
|
|
const tenderId = ctx.params.tenderId;
|
|
if (!tenderId) { throw '缺少参数' };
|
|
await models.TenderDirectory.destroy({
|
|
where: { tenderId },
|
|
transaction
|
|
});
|
|
await models.Tender.destroy({
|
|
where: { id: tenderId },
|
|
transaction
|
|
});
|
|
ctx.status = 204;
|
|
await transaction.commit();
|
|
} catch (error) {
|
|
await transaction.rollback();
|
|
ctx.logger.log(error);
|
|
ctx.status = 400;
|
|
ctx.body = {
|
|
message: typeof error == 'string' ? error : '删除标书失败'
|
|
};
|
|
}
|
|
}
|
|
|
|
module.exports.exportTender = async (ctx, next) => {
|
|
try {
|
|
const { models } = ctx.app.fs.dc;
|
|
const tenderId = ctx.params.tenderId;
|
|
if (!tenderId) { throw '缺少参数' };
|
|
const tender = await models.Tender.findOne({
|
|
where: { id: tenderId },
|
|
attributes: ['id', 'name'],
|
|
raw: true,
|
|
});
|
|
const tenderDirectories = await models.TenderDirectory.findAll({
|
|
where: { tenderId },
|
|
attributes: ['id', 'name', 'content', 'parentId', 'order'],
|
|
raw: true,
|
|
});
|
|
const tree = buildTree(tenderDirectories);
|
|
const mdContent = generateMdFromTree(tree);
|
|
|
|
// 调用 Pandoc 将 Markdown 字符串转换为 DOCX
|
|
const pandoc = spawn('pandoc', [
|
|
'-f', 'markdown', // -f markdown: 指定输入格式为 markdown
|
|
'-t', 'docx', // 指定输出格式为 docx
|
|
'--toc', // 生成目录
|
|
'--metadata', 'toc-title=目录', // 元数据:目录名
|
|
'--reference-doc', path.resolve(__dirname, '../static/custom-reference.docx'), // 格式参考文档
|
|
'-o', '-' // 指定输出到 stdout (标准输出)
|
|
]);
|
|
|
|
// 创建一个可写流来捕获 Pandoc 的 stdout
|
|
let docxBuffers = [];
|
|
pandoc.stdout.on('data', (data) => {
|
|
docxBuffers.push(data);
|
|
});
|
|
|
|
// 处理 Pandoc 进程的错误
|
|
let pandocError = '';
|
|
pandoc.stderr.on('data', (data) => {
|
|
pandocError += data.toString();
|
|
});
|
|
|
|
// 将 Markdown 字符串写入 Pandoc 的 stdin
|
|
const stdinStream = new stream.Readable();
|
|
stdinStream.push(mdContent);
|
|
stdinStream.push(null); // 表示输入结束
|
|
stdinStream.pipe(pandoc.stdin);
|
|
|
|
// 等待 Pandoc 进程结束
|
|
await new Promise((resolve, reject) => {
|
|
pandoc.on('close', (code) => {
|
|
if (code !== 0) {
|
|
console.error('Pandoc stderr:', pandocError);
|
|
reject(new Error(`Pandoc exited with code ${code}. Error: ${pandocError}`));
|
|
} else {
|
|
resolve();
|
|
}
|
|
});
|
|
|
|
pandoc.on('error', (err) => {
|
|
console.error('Failed to start Pandoc process:', err);
|
|
reject(err);
|
|
});
|
|
});
|
|
|
|
const docxBuffer = Buffer.concat(docxBuffers);
|
|
|
|
// 设置响应头,告诉浏览器这是一个 DOCX 文件并应该下载
|
|
const fileName = encodeURI(`${tender.name}.docx`);
|
|
ctx.set('Content-Type', 'application/vnd.openxmlformats-officedocument.wordprocessingml.document');
|
|
ctx.set('Content-Disposition', `attachment; filename="${fileName}"`);
|
|
ctx.set('Content-Length', docxBuffer.length.toString());
|
|
|
|
ctx.body = docxBuffer;
|
|
ctx.status = 200;
|
|
} catch (error) {
|
|
ctx.logger.log(error);
|
|
ctx.status = 400;
|
|
ctx.body = {
|
|
message: typeof error == 'string' ? error : '导出标书失败'
|
|
};
|
|
}
|
|
}
|
|
|
|
function buildTree(items) {
|
|
const map = {};
|
|
const tree = [];
|
|
|
|
// 构建 ID 到节点的映射
|
|
items.forEach(item => {
|
|
map[item.id] = { ...item, children: [] };
|
|
});
|
|
|
|
// 构建树结构
|
|
items.forEach(item => {
|
|
const node = map[item.id];
|
|
if (item.parentId && map[item.parentId]) {
|
|
map[item.parentId].children.push(node);
|
|
} else if (!item.parentId) {
|
|
tree.push(node);
|
|
}
|
|
});
|
|
|
|
// 按 order 排序
|
|
function sortNode(node) {
|
|
node.children.sort((a, b) => a.order - b.order);
|
|
node.children.forEach(sortNode);
|
|
}
|
|
|
|
tree.sort((a, b) => a.order - b.order);
|
|
tree.forEach(sortNode);
|
|
|
|
return tree;
|
|
}
|
|
function generateMdFromTree(tree, level = 1) {
|
|
let mdContent = '';
|
|
|
|
function traverse(node, currentLevel) {
|
|
const heading = '#'.repeat(currentLevel);
|
|
mdContent += `${heading} ${node.name}\n${node.content || ''}\n\n`;
|
|
|
|
if (node.children.length > 0) {
|
|
node.children.forEach(child => traverse(child, currentLevel + 1));
|
|
}
|
|
}
|
|
|
|
tree.forEach(node => traverse(node, level));
|
|
|
|
return mdContent.trim();
|
|
}
|
|
|
|
module.exports.contentGenerate = async (ctx, next) => {
|
|
try {
|
|
const { tenderAppKey } = ctx.app.fs.config.fastGpt;
|
|
const { models, orm: sequelize } = ctx.app.fs.dc;
|
|
const { tenderId } = ctx.params;
|
|
const { directoryId, fileUrl } = ctx.request.body;
|
|
if (!tenderId || !directoryId || !fileUrl) { throw '缺少请求参数' };
|
|
let text = '';
|
|
const result = await sequelize.query(`
|
|
WITH RECURSIVE parent_tree AS (
|
|
SELECT id, name, parent_id
|
|
FROM tender_directory
|
|
WHERE id = ${directoryId}
|
|
UNION ALL
|
|
SELECT d.id, d.name, d.parent_id
|
|
FROM tender_directory d
|
|
INNER JOIN parent_tree pt ON d.id = pt.parent_id
|
|
)
|
|
SELECT string_agg(name, '-' ORDER BY id ASC) AS path
|
|
FROM parent_tree;
|
|
`, { type: sequelize.QueryTypes.SELECT });
|
|
if (result.length > 0) {
|
|
text = '章节:' + result[0].path;
|
|
} else {
|
|
throw '章节查询错误';
|
|
}
|
|
const curDirectory = await models.TenderDirectory.findOne({
|
|
where: {
|
|
id: directoryId,
|
|
tenderId
|
|
},
|
|
attributes: ['chapterRequirements', 'chapterWords'],
|
|
raw: true
|
|
});
|
|
let variables = {};
|
|
if (curDirectory?.chapterRequirements) {
|
|
text = text + '\n章节需求:' + curDirectory.chapterRequirements;
|
|
}
|
|
if (typeof curDirectory?.chapterWords === 'number') {
|
|
variables = { word_count_requirement: curDirectory.chapterWords };
|
|
}
|
|
const res = await superagent
|
|
.post(`${ctx.app.fs.config.fastGpt.apiUrl}/api/v1/chat/completions`)
|
|
.send({
|
|
"stream": false,
|
|
"detail": false,
|
|
"variables": variables,
|
|
"messages": [
|
|
{
|
|
"role": "user",
|
|
"content": [
|
|
{
|
|
"type": "file_url",
|
|
"name": "文件",
|
|
"url": fileUrl
|
|
},
|
|
{
|
|
"type": "text",
|
|
"text": text
|
|
}
|
|
]
|
|
}
|
|
]
|
|
})
|
|
.set({
|
|
Authorization: `Bearer ${tenderAppKey}`,
|
|
"Content-Type": "application/json",
|
|
})
|
|
const contentMd = res.body.choices[0].message.content;
|
|
await models.TenderDirectory.update(
|
|
{ content: contentMd },
|
|
{ where: { id: directoryId } }
|
|
);
|
|
ctx.body = contentMd;
|
|
ctx.status = 200;
|
|
} catch (error) {
|
|
ctx.logger.log(error);
|
|
ctx.status = 400;
|
|
ctx.body = {
|
|
message: typeof error == 'string' ? error : '章节内容生成失败'
|
|
};
|
|
}
|
|
}
|
|
|
|
// 内容润色
|
|
module.exports.contentEmbellish = async (ctx, next) => {
|
|
try {
|
|
const { tenderAppKey } = ctx.app.fs.config.fastGpt;
|
|
const { tenderId } = ctx.params;
|
|
const { directoryId, fileUrl, selectedText, embellishDemand } = ctx.request.body;
|
|
if (!tenderId || !directoryId || !fileUrl || !selectedText || !embellishDemand) {
|
|
throw '缺少请求参数'
|
|
};
|
|
const res = await superagent
|
|
.post(`${ctx.app.fs.config.fastGpt.apiUrl}/api/v1/chat/completions`)
|
|
.send({
|
|
"stream": false,
|
|
"detail": false,
|
|
"variables": { "generation_type": '章节内容修改' },
|
|
"messages": [
|
|
{
|
|
"role": "user",
|
|
"content": [
|
|
{
|
|
"type": "file_url",
|
|
"name": "文件",
|
|
"url": fileUrl
|
|
},
|
|
{
|
|
"type": "text",
|
|
"text": `待优化段落:\n${selectedText}\n\n用户需求:\n${embellishDemand}\n`
|
|
}
|
|
]
|
|
}
|
|
]
|
|
})
|
|
.set({
|
|
Authorization: `Bearer ${tenderAppKey}`,
|
|
"Content-Type": "application/json",
|
|
})
|
|
|
|
const contentMd = res.body.choices[0].message.content;
|
|
ctx.body = contentMd;
|
|
ctx.status = 200;
|
|
} catch (error) {
|
|
ctx.logger.log(error);
|
|
ctx.status = 400;
|
|
ctx.body = {
|
|
message: typeof error == 'string' ? error : '章节文字润色失败'
|
|
};
|
|
}
|
|
}
|
|
|
|
module.exports.addTenderDirectory = async (ctx, next) => {
|
|
try {
|
|
const { models } = ctx.app.fs.dc;
|
|
const { tenderId } = ctx.params;
|
|
const body = ctx.request.body;
|
|
if (!tenderId || !body) { throw '缺少参数' };
|
|
|
|
await models.TenderDirectory.create(body);
|
|
|
|
ctx.status = 204;
|
|
} catch (error) {
|
|
ctx.logger.log(error);
|
|
ctx.status = 400;
|
|
ctx.body = {
|
|
message: typeof error == 'string' ? error : '添加章节失败'
|
|
};
|
|
}
|
|
}
|
|
|
|
// 修改节点顺序
|
|
async function moveNode(ctx, tenderId, id, direction, transaction) {
|
|
const { models } = ctx.app.fs.dc;
|
|
// 1. 获取目标节点的父节点 ID 和当前顺序
|
|
const targetNode = await models.TenderDirectory.findByPk(id, {
|
|
attributes: ["parentId", "order"],
|
|
});
|
|
|
|
if (!targetNode) {
|
|
throw new Error(`Node with id ${id} not found`);
|
|
}
|
|
|
|
const { parentId, order: currentOrder } = targetNode;
|
|
|
|
// 2. 获取同级节点(根据 parentId 分组)
|
|
const siblings = await models.TenderDirectory.findAll({
|
|
attributes: ["id", "order"],
|
|
where: {
|
|
parentId,
|
|
tenderId,
|
|
},
|
|
order: [["order", "ASC"]],
|
|
raw: true,
|
|
});
|
|
|
|
// 找到目标节点在同级节点中的索引
|
|
const targetIndex = siblings.findIndex(node => node.id == id);
|
|
if (targetIndex === -1) {
|
|
throw new Error(`Node with id ${id} not found in siblings`);
|
|
}
|
|
|
|
// 3. 根据方向计算交换的目标节点
|
|
let swapIndex;
|
|
if (direction === "up") {
|
|
if (targetIndex === 0) {
|
|
console.log("Already at the top, cannot move up further.");
|
|
return; // 已经是第一个节点,无法继续上移
|
|
}
|
|
swapIndex = targetIndex - 1; // 上移:与前一个节点交换顺序
|
|
} else if (direction === "down") {
|
|
if (targetIndex === siblings.length - 1) {
|
|
console.log("Already at the bottom, cannot move down further.");
|
|
return; // 已经是最后一个节点,无法继续下移
|
|
}
|
|
swapIndex = targetIndex + 1; // 下移:与后一个节点交换顺序
|
|
} else {
|
|
throw new Error("Invalid direction. Use 'up' or 'down'.");
|
|
}
|
|
|
|
// 4. 获取需要交换顺序的节点
|
|
const swapNode = siblings[swapIndex];
|
|
|
|
// 5. 交换两个节点的顺序值
|
|
await models.TenderDirectory.update(
|
|
{ order: swapNode.order },
|
|
{ where: { id }, transaction }
|
|
);
|
|
await models.TenderDirectory.update(
|
|
{ order: currentOrder },
|
|
{ where: { id: swapNode.id }, transaction }
|
|
);
|
|
|
|
console.log(`Moved node ${id} ${direction}`);
|
|
}
|
|
|
|
module.exports.putTenderDirectory = async (ctx, next) => {
|
|
const transaction = await ctx.app.fs.dc.orm.transaction();
|
|
try {
|
|
const { models } = ctx.app.fs.dc;
|
|
const { tenderId, directoryId } = ctx.params;
|
|
const body = ctx.request.body;
|
|
if (!tenderId || !directoryId) { throw '缺少参数' };
|
|
|
|
if (body.direction) {
|
|
await moveNode(ctx, tenderId, directoryId, body.direction, transaction);
|
|
} else {
|
|
await models.TenderDirectory.update(
|
|
{ ...body },
|
|
{
|
|
where: {
|
|
id: directoryId,
|
|
tenderId
|
|
},
|
|
transaction
|
|
}
|
|
);
|
|
}
|
|
|
|
ctx.status = 204;
|
|
await transaction.commit();
|
|
} catch (error) {
|
|
await transaction.rollback();
|
|
ctx.logger.log(error);
|
|
ctx.status = 400;
|
|
ctx.body = {
|
|
message: typeof error == 'string' ? error : '修改章节失败'
|
|
};
|
|
}
|
|
}
|
|
|
|
module.exports.delTenderDirectory = async (ctx, next) => {
|
|
try {
|
|
const { models } = ctx.app.fs.dc;
|
|
const Op = ctx.app.fs.dc.ORM.Op;
|
|
const { tenderId, directoryId } = ctx.params;
|
|
if (!tenderId || !directoryId) { throw '缺少参数' };
|
|
// 递归删除所有子章节
|
|
async function getChildIds(parentId, allChildIds = []) {
|
|
const children = await models.TenderDirectory.findAll({
|
|
attributes: ["id"], // 只查询 id 字段
|
|
where: { parentId },
|
|
});
|
|
if (children.length > 0) {
|
|
const childIds = children.map(child => child.id);
|
|
allChildIds.push(...childIds);
|
|
|
|
// 对每个子节点递归查找其子节点
|
|
for (const childId of childIds) {
|
|
await getChildIds(childId, allChildIds);
|
|
}
|
|
}
|
|
return allChildIds;
|
|
}
|
|
// 获取目标节点及其所有子节点的 ID
|
|
const allIdsToDelete = [Number(directoryId)]; // 包含目标节点本身
|
|
const childIds = await getChildIds(directoryId);
|
|
allIdsToDelete.push(...childIds);
|
|
|
|
console.log(allIdsToDelete, 'allIdsToDelete')
|
|
|
|
await models.TenderDirectory.destroy({
|
|
where: {
|
|
id: { [Op.in]: allIdsToDelete },
|
|
tenderId
|
|
},
|
|
});
|
|
ctx.status = 204;
|
|
} catch (error) {
|
|
ctx.logger.log(error);
|
|
ctx.status = 400;
|
|
ctx.body = {
|
|
message: typeof error == 'string' ? error : '删除章节失败'
|
|
};
|
|
}
|
|
}
|