499 lines
19 KiB
JavaScript
499 lines
19 KiB
JavaScript
const Service = require("egg").Service;
|
||
const crypto = require('crypto');
|
||
const zlib = require('zlib');
|
||
const fs = require('fs');
|
||
const path = require('path');
|
||
const net = require('net');
|
||
const md5 = require('js-md5')
|
||
|
||
const {
|
||
spawn,
|
||
execSync
|
||
} = require('child_process');
|
||
const dicomParser = require('dicom-parser'); //dicom解析
|
||
class UtilService extends Service {
|
||
async initData() {
|
||
let today = this.app.moment(from).subtract(1, "day");
|
||
for (let i = 0; i < 30; i++) {
|
||
let date = today.format("YYYY-MM-DD");
|
||
try {
|
||
console.log('查询成功:', date)
|
||
} catch (error) {
|
||
console.error(error, date)
|
||
}
|
||
today.subtract(1, "day")
|
||
}
|
||
}
|
||
|
||
|
||
|
||
|
||
enCode(password) {
|
||
let date = Date.now()
|
||
.toString()
|
||
.slice(2, 8);
|
||
let salt =
|
||
password +
|
||
date +
|
||
Math.random()
|
||
.toString("10")
|
||
.slice(2, 6);
|
||
return salt;
|
||
}
|
||
|
||
random(min, max) {
|
||
return Math.floor(Math.random() * (max - min + 1) + min);
|
||
}
|
||
|
||
getGuid (text) {
|
||
text = md5(text)
|
||
let t1, t2, t3, t4, t5, t6, t7, t8, t9, t10
|
||
t1 = text.substr(0, 2)
|
||
t2 = text.substr(2, 2)
|
||
t3 = text.substr(4, 2)
|
||
t4 = text.substr(6, 2)
|
||
t5 = text.substr(8, 2)
|
||
t6 = text.substr(10, 2)
|
||
t7 = text.substr(12, 2)
|
||
t8 = text.substr(14, 2)
|
||
t9 = text.substr(16, 4)
|
||
t10 = text.substr(20, 12)
|
||
return `${t4+t3+t2+t1}-${t6+t5}-${t8+t7}-${t9}-${t10}`
|
||
}
|
||
|
||
readDicomToJsonJSON(param) {
|
||
try {
|
||
if (typeof param === 'string' || typeof param === 'number') {
|
||
return param;
|
||
} else {
|
||
return ""
|
||
}
|
||
} catch (error) {
|
||
return ""
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 读取dicom文件返回结果
|
||
* @param {fileData} fileData dicom文件
|
||
*/
|
||
readDicomToJson(fileData) {
|
||
let dataSet = dicomParser.parseDicom(fileData);
|
||
let json = dicomParser.explicitDataSetToJS(dataSet);
|
||
let data = {
|
||
study: {
|
||
PatientID: this.readDicomToJsonJSON(json["x00100020"]), //患者ID
|
||
PatientName: this.readDicomToJsonJSON(decodeURIComponent(escape(json["x00100010"]))), //患者姓名
|
||
patientBirthDate: this.readDicomToJsonJSON(json["x00100030"]), //患者出生日期
|
||
PatientBirthTime: this.readDicomToJsonJSON(json["x00100032"]), //患者出生时间
|
||
PatientSex: this.readDicomToJsonJSON(json["x00100040"]), //患者性别
|
||
PatientAge: this.readDicomToJsonJSON(json["x00101010"]), //患者年龄
|
||
PatientWeight: this.readDicomToJsonJSON(json["x00101030"]), //
|
||
|
||
StudyInstanceUID: this.readDicomToJsonJSON(json["x0020000d"]), // 检查实例号
|
||
StudyDate: this.readDicomToJsonJSON(json["x00080020"]), // 检查开始的日期.
|
||
StudyTime: this.readDicomToJsonJSON(json["x00080030"]), //检查开始的时间.
|
||
StudyDescription: this.readDicomToJsonJSON(json["x00081030"]), //检查的描述
|
||
BodyPartExamined: this.readDicomToJsonJSON(json["x00180015"]), //身体部位
|
||
InstitutionName: this.readDicomToJsonJSON(json["x00080080"]), // 机构名称
|
||
ModalitiesInStudy: this.readDicomToJsonJSON(json["x00080061"]), //模态
|
||
OperatorsName: this.readDicomToJsonJSON(json["x00081070"]), //技师名称
|
||
},
|
||
serie: {
|
||
SeriesInstanceUID: this.readDicomToJsonJSON(json["x0020000e"]), //唯一标记不同序列的号码.
|
||
SeriesDescription: this.readDicomToJsonJSON(json["x0008103e"]), //检查描述和说明
|
||
SeriesDate: this.readDicomToJsonJSON(json["x00080021"]), //检查日期
|
||
SeriesTime: this.readDicomToJsonJSON(json["x00080031"]), //检查时间
|
||
Modality: this.readDicomToJsonJSON(json["x00080060"]), //序列的检测模态
|
||
AcquisitionTime: this.readDicomToJsonJSON(json["x00080032"]), //获得时间
|
||
SliceThickness: this.readDicomToJsonJSON(json["x00180050"]), //层厚
|
||
SpacingBetweenSlices: this.readDicomToJsonJSON(json["x00180088"]), //层与层之间的间距,单位为mm
|
||
Manufacturer: this.readDicomToJsonJSON(json["x00080070"]), // 制造商
|
||
ManufacturerModelName: this.readDicomToJsonJSON(json["x00081090"]), // 制造商模态名称
|
||
RadionuclideTotalDose: this.readDicomToJsonJSON(json["x00181074"]), //modify 19.2.22
|
||
DoseCalibrationFactor: this.readDicomToJsonJSON(json["x00541322"]), //modify 19.2.22
|
||
},
|
||
image: {
|
||
StudyInstanceUID: this.readDicomToJsonJSON(json["x0020000d"]), // StudyInstanceUID
|
||
SeriesInstanceUID: this.readDicomToJsonJSON(json["x0020000e"]), //SeriesInstanceUID
|
||
SOPInstanceUID: this.readDicomToJsonJSON(json["x00080018"]), // 4-11新增
|
||
InstanceNumber: this.readDicomToJsonJSON(json["x00200013"]), // 实例number
|
||
PixelSpacing: this.readDicomToJsonJSON(json["x00280030"]) ? this.readDicomToJsonJSON(json["x00280030"]) : this.readDicomToJsonJSON(json["x00181164"]), //像素
|
||
SamplesPerPixel: this.readDicomToJsonJSON(json['x00280002']), //采样率
|
||
PixelPaddingValue: this.readDicomToJsonJSON(json['x00280120']), // 像素填充值
|
||
Rows: this.readDicomToJsonJSON(json["x00280010"]), //图像行数
|
||
Columns: this.readDicomToJsonJSON(json["x00280011"]), //图像列数
|
||
WindowWidth: this.readDicomToJsonJSON(json["x00281051"]), //窗宽
|
||
WindowCenter: this.readDicomToJsonJSON(json["x00281050"]), //窗位
|
||
RescaleIntercept: this.readDicomToJsonJSON(json["x00281052"]), //截距
|
||
RescaleSlope: this.readDicomToJsonJSON(json["x00281053"]), //斜率
|
||
RescaleType: this.readDicomToJsonJSON(json["x00281054"]), //斜率截距换算类型
|
||
ImagePosition: this.readDicomToJsonJSON(json["x00200030"]), //图像位置
|
||
ImagePositionPatient: this.readDicomToJsonJSON(json["x00200032"]), //图像相对病人的位置
|
||
imageOrientation: this.readDicomToJsonJSON(json["x00200035"]), //图像方位
|
||
ImageOrientationPatient: this.readDicomToJsonJSON(json["x00200037"]), //200||'',037 图像相对于病人的方位
|
||
SliceLocation: this.readDicomToJsonJSON(json["x00201041"]), //实际的相对位置,单位为mm.
|
||
BitsAllocated: this.readDicomToJsonJSON(json["x00280100"]), //分配的位数
|
||
BitsStored: this.readDicomToJsonJSON(json["x00280101"]), //存储的位数
|
||
HighBit: this.readDicomToJsonJSON(json["x00280102"]), //符号型二进制整数,长度 16 比特
|
||
PixelRepresentation: this.readDicomToJsonJSON(json['x00280103']), //像素数据的表现类型
|
||
}
|
||
}
|
||
return data;
|
||
}
|
||
//当没有InstanceNumber时根据SOPInstanceUID不同生成
|
||
async createNewInstanceNumber(dicomInfo) {
|
||
let SeriesInstanceUID = dicomInfo.image.SeriesInstanceUID;
|
||
let SOPInstanceUID = dicomInfo.image.SOPInstanceUID;
|
||
//获取该series下的images
|
||
let serie = await this.service.series.getSerieBySUID(SeriesInstanceUID);
|
||
let num = 1;
|
||
if (serie && serie.images_ids) {
|
||
for (let i = 0; i < serie.images_ids.length; i++) {
|
||
let image = serie.images_ids[i];
|
||
if (image.SOPInstanceUID == SOPInstanceUID) {
|
||
num = image.InstanceNumber;
|
||
break;
|
||
}
|
||
//如果num小于当前image.InstanceNumber则image.InstanceNumber+1赋给num
|
||
num = num < image.InstanceNumber ? image.InstanceNumber + 1 : num;
|
||
}
|
||
}
|
||
return num;
|
||
}
|
||
readDicom(fileData) {
|
||
let dataSet = dicomParser.parseDicom(fileData);
|
||
let data = {
|
||
study: {
|
||
PatientID: dataSet.string("x00100020") || '', //患者ID
|
||
PatientName: decodeURIComponent(escape(dataSet.string("x00100010"))) || '', //患者姓名
|
||
patientBirthDate: dataSet.string("x00100030") || '', //患者出生日期
|
||
PatientBirthTime: dataSet.string("x00100032") || '', //患者出生时间
|
||
PatientSex: dataSet.string("x00100040") || '', //患者性别
|
||
PatientAge: dataSet.string("x00101010") || '', //患者年龄
|
||
PatientWeight: dataSet.string("x00101030") || '', //
|
||
|
||
StudyInstanceUID: dataSet.string("x0020000d") || '', // 检查实例号
|
||
StudyDate: dataSet.string("x00080020") || '', // 检查开始的日期.
|
||
StudyTime: dataSet.string("x00080030") || '', //检查开始的时间.
|
||
StudyDescription: dataSet.string("x00081030") || '', //检查的描述
|
||
BodyPartExamined: dataSet.string("x00180015") || '', //身体部位
|
||
InstitutionName: dataSet.string("x00080080") || '', //
|
||
ModalitiesInStudy: dataSet.string("x00080060") || '', //模态
|
||
OperatorsName: dataSet.string("x00081070") || '', //技师名称
|
||
},
|
||
serie: {
|
||
SeriesInstanceUID: dataSet.string("x0020000e") || '', //唯一标记不同序列的号码.
|
||
SeriesDescription: dataSet.string("x0008103e") || '', //检查描述和说明
|
||
SeriesDate: dataSet.string("x00080021") || '', //检查日期
|
||
SeriesTime: dataSet.string("x00080031") || '', //检查时间
|
||
Modality: dataSet.string("x00080060") || '', //序列的检测模态
|
||
BodyPartExamined: dataSet.string("x00180015") || '', //身体部位
|
||
AcquisitionTime: dataSet.string("x00080031") || '',
|
||
SliceThickness: dataSet.floatString("x00180050") || '', //层厚
|
||
SpacingBetweenSlices: dataSet.floatString("x00180088") || '', //层与层之间的间距,单位为mm
|
||
Manufacturer: dataSet.string("x00080070") || '',
|
||
ManufacturerModelName: dataSet.string("x00081090") || '',
|
||
RadionuclideTotalDose: dataSet.string("x00181074") || '', //modify 19.2.22
|
||
DoseCalibrationFactor: dataSet.string("x00541322") || '', //modify 19.2.22
|
||
},
|
||
image: {
|
||
StudyInstanceUID: dataSet.string("x0020000d") || '', // StudyInstanceUID
|
||
SeriesInstanceUID: dataSet.string("x0020000e") || '', //SeriesInstanceUID
|
||
SOPInstanceUID: dataSet.string("x00080018"), // 4-11新增
|
||
InstanceNumber: dataSet.string("x00200013") || '',
|
||
PixelSpacing: dataSet.string("x00280030") || '', //像素
|
||
SamplesPerPixel: dataSet.uint16('x00280002') || '', //采样率
|
||
PixelPaddingValue: dataSet.int16('x00280120') || '', //
|
||
Columns: dataSet.int16("x00280011") || '', //图像列数
|
||
Rows: dataSet.int16("x00280010") || '', //图像行数
|
||
WindowWidth: dataSet.floatString("x00281051") || '', //窗宽
|
||
WindowCenter: dataSet.floatString("x00281050") || '', //窗位
|
||
RescaleType: dataSet.string("x00281054") || '', //斜率截距换算类型
|
||
RescaleSlope: dataSet.floatString("x00281053") || '', //斜率
|
||
RescaleIntercept: dataSet.string("x00281052") || '', //截距
|
||
ImagePosition: dataSet.string("x00200030") || '', //图像位置
|
||
ImagePositionPatient: dataSet.string("x00200032") || '', //图像相对病人的位置
|
||
imageOrientation: dataSet.string("x00200035") || '', //图像方位
|
||
ImageOrientationPatient: dataSet.string("x00200037") || '', //200||'',037 图像相对于病人的方位
|
||
SliceLocation: dataSet.string("x00201041") || '', //实际的相对位置,单位为mm.
|
||
BitsAllocated: dataSet.uint16("x00280100") || '', //分配的位数
|
||
BitsStored: dataSet.uint16("x00280101") || '', //存储的位数
|
||
HighBit: dataSet.uint16("x00280102") || '', //符号型二进制整数,长度 16 比特
|
||
PixelRepresentation: dataSet.uint16("x00280103") || '', //像素数据的表现类型
|
||
}
|
||
}
|
||
return data;
|
||
}
|
||
|
||
getDateRange(date) {
|
||
try {
|
||
console.log(date)
|
||
return {
|
||
//今天
|
||
day: this.app.moment(date).endOf('day'),
|
||
//日 中间
|
||
mid_day: this.app.moment(date).startOf('day'),
|
||
//昨日
|
||
pre_day: this.app.moment(date).subtract(1, 'day').startOf('day'),
|
||
//上周末
|
||
week: this.app.moment(date).subtract(1, 'week').endOf('week').endOf('day'),
|
||
//周 中间
|
||
mid_week: this.app.moment(date).subtract(1, 'week').startOf('week').startOf('day'),
|
||
//上上周
|
||
pre_week: this.app.moment(date).subtract(2, 'week').startOf('week').startOf('day'),
|
||
//上个月末
|
||
month: this.app.moment(date).subtract(1, 'month').endOf('month').endOf('day'),
|
||
//月中间
|
||
mid_month: this.app.moment(date).subtract(1, 'month').startOf('month').startOf('day'),
|
||
//上上个月
|
||
pre_month: this.app.moment(date).subtract(2, 'month').startOf('month').startOf('day'),
|
||
}
|
||
} catch (error) {
|
||
throw "参数错误"
|
||
}
|
||
}
|
||
|
||
parseResponse(buf) {
|
||
let unzipData;
|
||
try {
|
||
unzipData = this.gunzip(buf).toString();
|
||
} catch (error) {
|
||
unzipData = buf.toString();
|
||
}
|
||
let res;
|
||
try {
|
||
res = JSON.parse(this.aesDecrypt(unzipData, this.getkey()))
|
||
} catch (error) {
|
||
res = JSON.parse(unzipData);
|
||
}
|
||
return res;
|
||
}
|
||
|
||
//获取秘钥
|
||
getkey() {
|
||
let time = this.app.moment().format("YYYY-MM-DD");
|
||
return 'rayplus_miyao_' + this.app.moment(time).valueOf();
|
||
}
|
||
|
||
aesEncrypt(data, key) {
|
||
const cipher = crypto.createCipher('aes192', key);
|
||
var crypted = cipher.update(data, 'utf8', 'hex');
|
||
crypted += cipher.final('hex');
|
||
return crypted;
|
||
}
|
||
|
||
aesDecrypt(encrypted, key) {
|
||
const decipher = crypto.createDecipher('aes192', key);
|
||
var decrypted = decipher.update(encrypted, 'hex', 'utf8');
|
||
decrypted += decipher.final('utf8');
|
||
return decrypted;
|
||
}
|
||
|
||
gzip(data) {
|
||
return zlib.gzipSync(buf);
|
||
}
|
||
|
||
gunzip(buf) {
|
||
return zlib.gunzipSync(buf);
|
||
}
|
||
|
||
/**
|
||
* 生成操作锁
|
||
* @param {*} name 锁名字
|
||
* @param {*} data 锁内容
|
||
* @param {*} flag 是否强制生成锁
|
||
*/
|
||
upsertLock(name, data, flag = false) {
|
||
let filePath = path.join(__dirname, "../public", `${name}.json`);
|
||
let fileExist = fs.existsSync(filePath);
|
||
if (fileExist) {
|
||
if (flag) {
|
||
this.deleteLock(name);
|
||
} else {
|
||
let fileData = fs.readFileSync(filePath);
|
||
let time = Number(JSON.parse(fileData).time);
|
||
//超过规定时间就删除锁文件
|
||
if (Date.now() - time > this.config.site.MOVE.LOCKTIME) {
|
||
this.deleteLock(name);
|
||
} else {
|
||
//返回正在被锁
|
||
return fileData;
|
||
}
|
||
}
|
||
}
|
||
let fileData = JSON.stringify({
|
||
time: Date.now(),
|
||
data
|
||
})
|
||
fs.writeFileSync(
|
||
filePath,
|
||
fileData
|
||
);
|
||
return fileData;
|
||
}
|
||
|
||
//获取lock文件内容
|
||
setLockData(name, data) {
|
||
try {
|
||
let filePath = path.join(__dirname, "../public", `${name}.json`);
|
||
let fd = fs.openSync(filePath);
|
||
if (!fd) throw "读取文件失败";
|
||
data = JSON.stringify(data);
|
||
fs.writeSync(fd, data);
|
||
fs.closeSync(fd);
|
||
} catch (error) {
|
||
console.error(error);
|
||
}
|
||
}
|
||
|
||
//删除操作锁
|
||
deleteLock(name) {
|
||
try {
|
||
let filePath = path.join(__dirname, "../public", `${name}.json`);
|
||
fs.unlinkSync(filePath);
|
||
} catch (error) {}
|
||
}
|
||
|
||
/**
|
||
* 清空文件夹
|
||
* @param {*} PATH 文件夹地址
|
||
* @param {*} flag 是否删除根文件
|
||
*/
|
||
async delFolder(PATH, flag = false) {
|
||
return new Promise((resolve, reject) => {
|
||
if (fs.existsSync(PATH)) {
|
||
fs.readdirSync(PATH).forEach((file) => {
|
||
var filePATH = path.join(PATH, file);
|
||
if (fs.statSync(filePATH).isDirectory()) {
|
||
this.delFolder(filePATH, flag)
|
||
} else {
|
||
try {
|
||
fs.unlinkSync(filePATH);
|
||
} catch (error) {
|
||
this.logger.error('文件删除失败', error)
|
||
}
|
||
}
|
||
});
|
||
if (flag) {
|
||
try {
|
||
fs.rmdirSync(PATH);
|
||
} catch (error) {
|
||
this.logger.error('文件夹删除失败', error)
|
||
}
|
||
}
|
||
}
|
||
return resolve();
|
||
})
|
||
}
|
||
|
||
//创建文件夹
|
||
createDirectory(directoryPath) {
|
||
if (!fs.existsSync(directoryPath)) {
|
||
fs.mkdirSync(directoryPath);
|
||
}
|
||
}
|
||
|
||
//检测端口是否被占用
|
||
portIsOccupied(port) {
|
||
const server = net.createServer().listen(port)
|
||
return new Promise((resolve, reject) => {
|
||
server.on('listening', () => {
|
||
server.close()
|
||
resolve(port)
|
||
})
|
||
server.on('error', (err) => {
|
||
if (err.code === 'EADDRINUSE') {
|
||
resolve(this.portIsOccupied(port + 1)) //注意这句,如占用端口号+1
|
||
console.log(`this port ${port} is occupied.try another.`)
|
||
} else {
|
||
reject(err)
|
||
}
|
||
})
|
||
})
|
||
}
|
||
|
||
//关闭某个端口的进程
|
||
closePort(port) {
|
||
let pidQuery, cmd;
|
||
if (process.platform == 'win32') {
|
||
pidQuery = execSync(`netstat -aon|findstr "${port}"`)
|
||
} else {
|
||
pidQuery = execSync(`lsof -i:${port}`)
|
||
}
|
||
var p = pidQuery.toString().trim().split(/\s+/);
|
||
console.log(pidQuery.toString(), p)
|
||
}
|
||
|
||
storesPython() {
|
||
return new Promise((resolve, reject) => {
|
||
try {
|
||
console.log('python storycp exec',`python this.config.site.STORYESCPPATH -dicompath=${this.config.site.CACHE_SAVE_PATH}`)
|
||
spawn('python', [this.config.site.STORYESCPPATH, `-dicompath=${this.config.site.CACHE_SAVE_PATH}`], {
|
||
cwd: this.config.site.PUBLIC,
|
||
detached: false,
|
||
stdio: ['ignore'],
|
||
windowsHide: true
|
||
})
|
||
// subprocess.on('close', (err) => {
|
||
// console.log('python storycp exit')
|
||
// setTimeout(() => {
|
||
// console.log('python storycp start')
|
||
// this.storesPython();
|
||
// }, 5e3);
|
||
// })
|
||
resolve("storescp启动成功");
|
||
} catch (e) {
|
||
reject("storescp启动失败", e);
|
||
}
|
||
});
|
||
}
|
||
|
||
/**
|
||
* 存储dicom数据
|
||
* @param {*} file 文件
|
||
* @param {*} dicomInfo 文件名
|
||
* @param {*} series_id series_id
|
||
*/
|
||
dicomSave(file, dicomInfo, series_id) {
|
||
series_id = series_id.toString()
|
||
// dicom存储主路径
|
||
let dicomSavePath = this.config.site.DICOM_SAVE_PATH;
|
||
// dicom展示主路径
|
||
let dicomShowPath = this.config.site.DICOM_SHOW_PATH;
|
||
//文件夹地址 使用series_id作为文件夹名称
|
||
let dirPath = path.join(dicomSavePath, series_id);
|
||
// 文件夹不存在就创建
|
||
if (!fs.existsSync(dirPath)) {
|
||
fs.mkdirSync(dirPath);
|
||
}
|
||
let fileName = `${dicomInfo.InstanceNumber}.dcm`;
|
||
let showPath = path.join(dicomShowPath, series_id, fileName);
|
||
let savePath = path.join(dirPath, fileName);
|
||
return new Promise((resolve, reject) => {
|
||
fs.writeFile(savePath, file, (err) => {
|
||
if (err) reject(err);
|
||
resolve(showPath);
|
||
})
|
||
})
|
||
}
|
||
|
||
//生成PNG
|
||
createPng(serie, image) {
|
||
return new Promise(async (resolve, reject) => {
|
||
let pngPath = this.app.config.site.PNG_SAVE_PATH + '/' + serie._id.toString() + '/'
|
||
let dicomFile = `${image.InstanceNumber}.dcm`
|
||
let dicomPath = path.join(this.app.config.site.DICOM_SAVE_PATH, serie._id.toString(), dicomFile)
|
||
let msg = image.id.toString() + ' ' + dicomPath + ' ' + pngPath + ' ' + image.InstanceNumber + '\n'
|
||
try {
|
||
await this.app.config.png16.spawn[this.app.config.png16.cur].sendMsg(msg, image)
|
||
this.app.config.png16.cur = (this.app.config.png16.cur + 1) % this.config.png16.num >> 0;
|
||
} catch (error) {
|
||
return reject(`createPng:${dicomPath} 失败`)
|
||
}
|
||
return resolve();
|
||
})
|
||
}
|
||
}
|
||
module.exports = UtilService
|