|
@@ -10,7 +10,9 @@
|
|
|
|
|
|
|
|
const archiver = require('archiver');
|
|
const archiver = require('archiver');
|
|
|
const path = require('path');
|
|
const path = require('path');
|
|
|
-const fs = require('fs');
|
|
|
|
|
|
|
+const fs = require('fs');
|
|
|
|
|
+const ZhDrawing = require('../lib/zh_drawing');
|
|
|
|
|
+const ledgerAudit = require('../const/audit').ledger;
|
|
|
module.exports = app => {
|
|
module.exports = app => {
|
|
|
class LedgerAtt extends app.BaseService {
|
|
class LedgerAtt extends app.BaseService {
|
|
|
/**
|
|
/**
|
|
@@ -49,14 +51,112 @@ module.exports = app => {
|
|
|
* @param {int} uid - 上传者id
|
|
* @param {int} uid - 上传者id
|
|
|
* @return {void}
|
|
* @return {void}
|
|
|
*/
|
|
*/
|
|
|
- async updateByID(postData, fileData) {
|
|
|
|
|
- delete postData.size;
|
|
|
|
|
- const data = {};
|
|
|
|
|
- Object.assign(data, fileData);
|
|
|
|
|
- Object.assign(data, postData);
|
|
|
|
|
- const result = await this.db.update(this.tableName, data);
|
|
|
|
|
- return result.affectedRows === 1;
|
|
|
|
|
- }
|
|
|
|
|
|
|
+ async updateByID(postData, fileData) {
|
|
|
|
|
+ delete postData.size;
|
|
|
|
|
+ // 图纸关联仅由专用接口写入;替换附件文件后清除旧关联。
|
|
|
|
|
+ delete postData.is_drawing;
|
|
|
|
|
+ delete postData.drawing_file_id;
|
|
|
|
|
+ delete postData.drawing_access;
|
|
|
|
|
+ const data = {};
|
|
|
|
|
+ Object.assign(data, fileData);
|
|
|
|
|
+ Object.assign(data, postData);
|
|
|
|
|
+ if (fileData.filepath) {
|
|
|
|
|
+ Object.assign(data, { is_drawing: 0, drawing_file_id: null, drawing_access: null });
|
|
|
|
|
+ }
|
|
|
|
|
+ const result = await this.db.update(this.tableName, data);
|
|
|
|
|
+ return result.affectedRows === 1;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 与附件“编辑”按钮相同:本人附件,审批完成后仅允许补传附件。
|
|
|
|
|
+ async createDrawingEditableAccess(id) {
|
|
|
|
|
+ const { ctx } = this;
|
|
|
|
|
+ id = Number(id);
|
|
|
|
|
+ if (!Number.isSafeInteger(id) || id <= 0 || ctx.tender.isTourist) throw new Error('无权编辑此附件');
|
|
|
|
|
+ const att = await this.db.get(this.tableName, { id, tid: ctx.tender.id, revising: 0, settle_id: -1 });
|
|
|
|
|
+ if (!att || Number(att.uid) !== Number(ctx.session.sessionUser.accountId) ||
|
|
|
|
|
+ (ctx.tender.data.ledger_status === ledgerAudit.status.checked && !att.extra_upload)) {
|
|
|
|
|
+ throw new Error('无权编辑此附件');
|
|
|
|
|
+ }
|
|
|
|
|
+ if (Number(att.is_drawing) !== 1 || !att.drawing_file_id) throw new Error('附件尚未关联图纸');
|
|
|
|
|
+ const client = new ZhDrawing(ctx.app.config.pdfDrawing);
|
|
|
|
|
+ const result = await client.createEditableAccess(att.drawing_file_id);
|
|
|
|
|
+ // 纵横图纸无时区的时间采用北京时间,与其服务端 now_local 保持一致。
|
|
|
|
|
+ const expiresAt = String(result.expires_at || '');
|
|
|
|
|
+ const expiry = Date.parse(/(?:Z|[+-]\d{2}:\d{2})$/i.test(expiresAt) ? expiresAt : expiresAt + '+08:00');
|
|
|
|
|
+ const remaining = expiry - Date.now();
|
|
|
|
|
+ if (result.permission !== 'editable' || typeof result.access !== 'string' || !result.access || !(remaining > 0)) {
|
|
|
|
|
+ throw new Error('未获取到有效的可编辑图纸凭证');
|
|
|
|
|
+ }
|
|
|
|
|
+ return {
|
|
|
|
|
+ access: result.access, expires_at: result.expires_at, expires_in_ms: remaining,
|
|
|
|
|
+ url: client.baseUrl + '/file?id=' + encodeURIComponent(att.drawing_file_id) + '&access=' + encodeURIComponent(result.access),
|
|
|
|
|
+ };
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ async findDrawing(drawingNumber) {
|
|
|
|
|
+ const attachments = await this.getDataByTenderId(this.ctx.tender.id);
|
|
|
|
|
+ const fileIds = [...new Set(attachments.filter(att => Number(att.is_drawing) === 1 && att.drawing_file_id)
|
|
|
|
|
+ .map(att => att.drawing_file_id))];
|
|
|
|
|
+ if (!fileIds.length) return { file_id: null };
|
|
|
|
|
+ const client = new ZhDrawing(this.ctx.app.config.pdfDrawing);
|
|
|
|
|
+ // 与全部附件显示顺序一致;超出单次接口上限时按顺序分批。
|
|
|
|
|
+ for (let i = 0; i < fileIds.length; i += 100) {
|
|
|
|
|
+ const result = await client.findDrawing(fileIds.slice(i, i + 100), drawingNumber);
|
|
|
|
|
+ if (result.file_id) return { file_id: result.file_id };
|
|
|
|
|
+ }
|
|
|
|
|
+ return { file_id: null };
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 附件已成功删除后尽力同步图纸软删除;远端失败不影响附件删除结果。
|
|
|
|
|
+ async syncDeletedDrawing(attachment) {
|
|
|
|
|
+ if (Number(attachment.is_drawing) !== 1 || !attachment.drawing_file_id) return;
|
|
|
|
|
+ try {
|
|
|
|
|
+ const config = Object.assign({}, this.ctx.app.config.pdfDrawing, { timeout: 3000 });
|
|
|
|
|
+ await new ZhDrawing(config).deleteFile(attachment.drawing_file_id);
|
|
|
|
|
+ } catch (err) {
|
|
|
|
|
+ // 不记录请求对象、Token 或 access,避免敏感凭证写入日志。
|
|
|
|
|
+ this.ctx.logger.warn('附件 %s 的图纸删除同步失败(状态 %s),附件删除结果不受影响',
|
|
|
|
|
+ attachment.id, err.status || err.code || 'unknown');
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 仅处理当前标段中本人上传的 PDF;行锁避免重复点击并发创建关联。
|
|
|
|
|
+ async createDrawing(id) {
|
|
|
|
|
+ const { ctx } = this;
|
|
|
|
|
+ id = Number(id);
|
|
|
|
|
+ if (!Number.isSafeInteger(id) || id <= 0 || ctx.tender.isTourist) {
|
|
|
|
|
+ throw new Error('无权操作此附件');
|
|
|
|
|
+ }
|
|
|
|
|
+ const conn = await this.db.beginTransaction();
|
|
|
|
|
+ try {
|
|
|
|
|
+ const rows = await conn.query(
|
|
|
|
|
+ 'SELECT * FROM ?? WHERE id = ? AND tid = ? AND revising = 0 AND settle_id = -1 FOR UPDATE',
|
|
|
|
|
+ [this.tableName, id, ctx.tender.id]
|
|
|
|
|
+ );
|
|
|
|
|
+ const att = rows[0];
|
|
|
|
|
+ if (!att || Number(att.uid) !== Number(ctx.session.sessionUser.accountId)) {
|
|
|
|
|
+ throw new Error('无权操作此附件');
|
|
|
|
|
+ }
|
|
|
|
|
+ if (String(att.fileext).toLowerCase() !== '.pdf') throw new Error('仅支持 PDF 附件');
|
|
|
|
|
+ if (!(Number(att.is_drawing) === 1 && att.drawing_file_id && att.drawing_access)) {
|
|
|
|
|
+ const file = (ctx.app.config.fujianOssPath + att.filepath).replace(/\\/g, '/');
|
|
|
|
|
+ if (!/^https?:\/\//i.test(file)) throw new Error('附件下载地址无效');
|
|
|
|
|
+ const result = await new ZhDrawing(ctx.app.config.pdfDrawing).createFile(file, att.filename + att.fileext);
|
|
|
|
|
+ if (!result || result.accepted !== true || typeof result.file_id !== 'string' || !result.file_id.trim() ||
|
|
|
|
|
+ typeof result.access !== 'string' || !result.access.trim() || result.permission !== 'readonly') {
|
|
|
|
|
+ throw new Error('图纸接口未返回有效的图纸 ID 和只读凭证');
|
|
|
|
|
+ }
|
|
|
|
|
+ await conn.update(this.tableName, {
|
|
|
|
|
+ id, is_drawing: 1, drawing_file_id: result.file_id, drawing_access: result.access,
|
|
|
|
|
+ });
|
|
|
|
|
+ }
|
|
|
|
|
+ await conn.commit();
|
|
|
|
|
+ } catch (err) {
|
|
|
|
|
+ await conn.rollback();
|
|
|
|
|
+ throw err;
|
|
|
|
|
+ }
|
|
|
|
|
+ return this.getDataByFid(id);
|
|
|
|
|
+ }
|
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
/**
|
|
@@ -65,7 +165,7 @@ module.exports = app => {
|
|
|
* @return {void}
|
|
* @return {void}
|
|
|
*/
|
|
*/
|
|
|
async getDataByTenderId(tid) {
|
|
async getDataByTenderId(tid) {
|
|
|
- const sql = 'SELECT att.id, att.lid, att.uid, att.filepath, att.filename, att.fileext, att.filesize, att.extra_upload, att.remark, att.in_time,' +
|
|
|
|
|
|
|
+ const sql = 'SELECT att.is_drawing, att.drawing_file_id, att.drawing_access, att.id, att.lid, att.uid, att.filepath, att.filename, att.fileext, att.filesize, att.extra_upload, att.remark, att.in_time,' +
|
|
|
' pa.name as `username`, leg.name as `lname`, leg.code as `code`, leg.ledger_id as `ledger_id`, leg.b_code as `b_code`' +
|
|
' pa.name as `username`, leg.name as `lname`, leg.code as `code`, leg.ledger_id as `ledger_id`, leg.b_code as `b_code`' +
|
|
|
' FROM ?? AS att ' +
|
|
' FROM ?? AS att ' +
|
|
|
' LEFT JOIN ?? AS pa ON att.uid = pa.id ' +
|
|
' LEFT JOIN ?? AS pa ON att.uid = pa.id ' +
|
|
@@ -84,7 +184,7 @@ module.exports = app => {
|
|
|
*/
|
|
*/
|
|
|
async getDataByFid(id) {
|
|
async getDataByFid(id) {
|
|
|
const { ctx } = this;
|
|
const { ctx } = this;
|
|
|
- const sql = 'SELECT att.id, att.lid, att.uid, att.filepath, att.filename, att.extra_upload, att.fileext, att.filesize, att.remark, att.in_time,' +
|
|
|
|
|
|
|
+ const sql = 'SELECT att.is_drawing, att.drawing_file_id, att.drawing_access, att.id, att.lid, att.uid, att.filepath, att.filename, att.extra_upload, att.fileext, att.filesize, att.remark, att.in_time,' +
|
|
|
' pa.name as `username`, leg.name as `lname`, leg.code as `code`, leg.ledger_id as `ledger_id`,leg.b_code as `b_code`' +
|
|
' pa.name as `username`, leg.name as `lname`, leg.code as `code`, leg.ledger_id as `ledger_id`,leg.b_code as `b_code`' +
|
|
|
' FROM ?? AS att,?? AS pa,?? AS leg' +
|
|
' FROM ?? AS att,?? AS pa,?? AS leg' +
|
|
|
' WHERE leg.id = att.lid AND pa.id = att.uid AND att.id = ? ORDER BY att.in_time DESC';
|
|
' WHERE leg.id = att.lid AND pa.id = att.uid AND att.id = ? ORDER BY att.in_time DESC';
|
|
@@ -144,11 +244,16 @@ module.exports = app => {
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
_analysisAtt(data) {
|
|
_analysisAtt(data) {
|
|
|
- const datas = data instanceof Array ? data : [data];
|
|
|
|
|
|
|
+ const datas = Array.isArray(data) ? data : [data];
|
|
|
for (const r of datas) {
|
|
for (const r of datas) {
|
|
|
r.orginpath = this.ctx.app.config.fujianOssPath + r.filepath;
|
|
r.orginpath = this.ctx.app.config.fujianOssPath + r.filepath;
|
|
|
r.filepath = this.ctx.app.config.fujianOssPath + r.filepath;
|
|
r.filepath = this.ctx.app.config.fujianOssPath + r.filepath;
|
|
|
- r.viewpath = this.ctx.helper.getPreviewPath(r.fileext, r.filepath);
|
|
|
|
|
|
|
+ r.viewpath = this.ctx.helper.getPreviewPath(r.fileext, r.filepath);
|
|
|
|
|
+ // 页面访问仅使用该文件的只读 access,不向浏览器传递 API token。
|
|
|
|
|
+ const baseUrl = String((this.ctx.app.config.pdfDrawing || {}).baseUrl || '').trim().replace(/\/+$/, '');
|
|
|
|
|
+ r.drawing_view_url = Number(r.is_drawing) === 1 && r.drawing_file_id && r.drawing_access && /^https?:\/\//i.test(baseUrl)
|
|
|
|
|
+ ? `${baseUrl}/file?id=${encodeURIComponent(r.drawing_file_id)}&access=${encodeURIComponent(r.drawing_access)}`
|
|
|
|
|
+ : '';
|
|
|
r.in_time = this.ctx.moment(r.in_time * 1000).format('YYYY-MM-DD');
|
|
r.in_time = this.ctx.moment(r.in_time * 1000).format('YYYY-MM-DD');
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
@@ -183,8 +288,8 @@ module.exports = app => {
|
|
|
return result;
|
|
return result;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- async getLedgerViewData(tid) {
|
|
|
|
|
- const sql = 'SELECT att.id, att.lid, att.uid, att.filepath, att.filename, att.fileext, att.filesize, att.extra_upload, att.remark, att.in_time,' +
|
|
|
|
|
|
|
+ async getLedgerViewData(tid) {
|
|
|
|
|
+ const sql = 'SELECT att.is_drawing, att.drawing_file_id, att.drawing_access, att.id, att.lid, att.uid, att.filepath, att.filename, att.fileext, att.filesize, att.extra_upload, att.remark, att.in_time,' +
|
|
|
' pa.name as `username`' +
|
|
' pa.name as `username`' +
|
|
|
' FROM ' + this.tableName + ' att Left Join ' + this.ctx.service.projectAccount.tableName + ' pa On pa.id = att.uid' +
|
|
' FROM ' + this.tableName + ' att Left Join ' + this.ctx.service.projectAccount.tableName + ' pa On pa.id = att.uid' +
|
|
|
' WHERE att.tid = ? AND att.revising = 0 AND att.settle_id = -1 ORDER BY att.id DESC';
|
|
' WHERE att.tid = ? AND att.revising = 0 AND att.settle_id = -1 ORDER BY att.id DESC';
|