fuyu 5 年 前
コミット
a066376b38
39 ファイル変更1541 行追加0 行削除
  1. 1 0
      server/.babelrc
  2. 30 0
      server/.editorconfig
  3. 5 0
      server/.eslintrc.json
  4. 62 0
      server/bin/server.js
  5. BIN
      server/common/53919977a32cd.01b68f7f18373ca0e3000bedaa8dcec6
  6. BIN
      server/common/5add3b5229f07.01b68f7f18373ca0e3000bedaa8dcec6
  7. BIN
      server/common/6b847905ba8bd.01b68f7f18373ca0e3000bedaa8dcec6
  8. BIN
      server/common/94a446f4d3bad.01b68f7f18373ca0e3000bedaa8dcec6
  9. BIN
      server/common/952849f8ef396.dcm
  10. BIN
      server/common/a1768214d2b45.01b68f7f18373ca0e3000bedaa8dcec6 (1)
  11. BIN
      server/common/b0e078726f7f7.dcm
  12. 3 0
      server/config/env/common.js
  13. 50 0
      server/config/env/development.js
  14. 26 0
      server/config/env/production.js
  15. 6 0
      server/config/index.js
  16. 43 0
      server/config/passport.js
  17. 20 0
      server/gulpfile.js
  18. 4 0
      server/index.js
  19. 40 0
      server/package.json
  20. 22 0
      server/pacs.config.js
  21. 26 0
      server/src/dao/db/sequelize.js
  22. 25 0
      server/src/dao/exam.js
  23. 39 0
      server/src/dao/images.js
  24. 12 0
      server/src/dao/index.js
  25. 26 0
      server/src/dao/patient.js
  26. 24 0
      server/src/dao/series.js
  27. 30 0
      server/src/dao/study.js
  28. 12 0
      server/src/middleware/index.js
  29. 28 0
      server/src/middleware/validators.js
  30. 31 0
      server/src/modules/index.js
  31. 60 0
      server/src/modules/upload/controller.js
  32. 16 0
      server/src/modules/upload/router.js
  33. 323 0
      server/src/service/dicomTag.js
  34. 127 0
      server/src/service/parseDicomService.js
  35. 14 0
      server/src/service/saveDicom.js
  36. 222 0
      server/src/service/saveDicom2PacsonlineByFileUpload.js
  37. 176 0
      server/src/utils/common.js
  38. 19 0
      server/src/utils/dicomCharsetFormat.js
  39. 19 0
      server/src/utils/dicomParse.js

+ 1 - 0
server/.babelrc

@@ -0,0 +1 @@
+{"presets": ["es2015-node5", "stage-0"]}

+ 30 - 0
server/.editorconfig

@@ -0,0 +1,30 @@
+# http://editorconfig.org
+
+# A special property that should be specified at the top of the file outside of
+# any sections. Set to true to stop .editor config file search on current file
+root = true
+
+[*]
+# Indentation style
+# Possible values - tab, space
+indent_style = space
+
+# Indentation size in single-spaced characters
+# Possible values - an integer, tab
+indent_size = 2
+
+# Line ending file format
+# Possible values - lf, crlf, cr
+end_of_line = lf
+
+# File character encoding
+# Possible values - latin1, utf-8, utf-16be, utf-16le
+charset = utf-8
+
+# Denotes whether to trim whitespace at the end of lines
+# Possible values - true, false
+trim_trailing_whitespace = true
+
+# Denotes whether file should end with a newline
+# Possible values - true, false
+insert_final_newline = true

+ 5 - 0
server/.eslintrc.json

@@ -0,0 +1,5 @@
+{
+  "parser" : "babel-eslint",
+  "extends": "standard",
+  "env": {"node" : true, "mocha": true}
+}

+ 62 - 0
server/bin/server.js

@@ -0,0 +1,62 @@
+// import mongoose   from 'mongoose'
+import Koa from 'koa'
+// import mount      from 'koa-mount'
+// import serve      from 'koa-static'
+import logger from 'koa-logger'
+import convert from 'koa-convert'
+// import passport   from 'koa-passport'
+import bodyParser from 'koa-bodyparser'
+// import session    from 'koa-generic-session'
+const cors = require('koa-cors');
+
+import config from '../config'
+
+//错误处理中间件
+import { errorMiddleware } from '../src/middleware'
+
+//建立mongoose数据库链接
+/**
+mongoose.Promise = global.Promise
+mongoose.connect(config.mongodb)
+*/
+
+//建立MYSQL连接
+require('../src/dao');
+//创建Koa对象
+const app = new Koa()
+  // app.keys  = [config.session]
+
+//加载中间件
+app.use(convert(logger()))
+  // app.use(convert(session()))
+app.use(convert(bodyParser()))
+app.use(convert(errorMiddleware()))
+
+//本地访问跨域
+app.use(convert(cors()));
+
+//加载docs
+// app.use(convert(mount('/docs', serve(`${process.cwd()}/docs`))))
+
+//加载passport
+// require('../config/passport')
+// app.use(passport.initialize())
+// app.use(passport.session())
+
+//加载路由
+require('../src/modules')(app)
+
+//接入Socket.IO
+const server = require('http').createServer(app.callback());
+// const io = require('socket.io')(server);
+// io.on('connection', (socket) => {
+//   console.log('connection');
+// });
+
+console.log('config', config)
+  //启动服务
+server.listen(config.port, () => {
+  console.log(`Server started on ${config.port}`)
+});
+
+export default app

BIN
server/common/53919977a32cd.01b68f7f18373ca0e3000bedaa8dcec6


BIN
server/common/5add3b5229f07.01b68f7f18373ca0e3000bedaa8dcec6


BIN
server/common/6b847905ba8bd.01b68f7f18373ca0e3000bedaa8dcec6


BIN
server/common/94a446f4d3bad.01b68f7f18373ca0e3000bedaa8dcec6


BIN
server/common/952849f8ef396.dcm


BIN
server/common/a1768214d2b45.01b68f7f18373ca0e3000bedaa8dcec6 (1)


BIN
server/common/b0e078726f7f7.dcm


+ 3 - 0
server/config/env/common.js

@@ -0,0 +1,3 @@
+export default {
+  port: process.env.PORT || 9999
+}

+ 50 - 0
server/config/env/development.js

@@ -0,0 +1,50 @@
+export default {
+  token: 'secret-jwt-token',
+  session: 'secret-boilerplate-token',
+  mongodb: 'mongodb://localhost:27017/koa2-boilerplate-dev',
+
+  //mysql配置
+  db_type: 'mysql', //数据库类型
+
+  // dbconfig: {
+  //   host    : 'localhost',    //服务器地址
+  //   port    : 3306,           //数据库端口号
+  //   username: 'root',         //数据库用户名
+  //   password: 'admin123',    //数据库密码
+  //   database: 'pacs',         //数据库名称
+  // },
+
+  //mysql配置
+  // dbconfig: {
+  //   host    : 'localhost',    //服务器地址
+  //   port    : 3306,           //数据库端口号
+  //   username: 'root',         //数据库用户名
+  //   password: 'admin123',    //数据库密码
+  //   database: 'pacs',         //数据库名称
+  // },
+  dbconfig: {
+    host: '47.104.6.21',
+    port: '3306',
+    username: 'pacs',
+    password: 'ZSKK@2017~!@#',
+    database: 'pacsonline',
+  },
+  // dbconfig: {
+  //   host    : '127.0.0.1',  //服务器地址
+  //   port    : 3306,             //数据库端口号
+  //   username: 'root',           //数据库用户名
+  //   password: 'admin123',           //数据库密码
+  //   database: 'pacs_test_local',           //数据库名称
+  // },
+
+
+  //fdfs 配置
+  fdfs: {
+    trackers: [{ host: '47.104.6.21', port: 22122 }],
+    timeout: 10000000,
+    charset: 'utf8'
+  },
+
+  //上传文件配置
+  upload: {}
+}

+ 26 - 0
server/config/env/production.js

@@ -0,0 +1,26 @@
+export default {
+  token: 'secret-jwt-token',
+  session: 'secret-boilerplate-token',
+  mongodb: 'mongodb://localhost:27017/koa2-boilerplate-dev',
+
+  //mysql配置
+  db_type: 'mysql', //数据库类型
+
+  //mysql配置
+  dbconfig: {
+    host: '127.0.0.1',
+    port: '3306',
+    username: 'pacs',
+    password: 'ZSKK@2017~!@#',
+    database: 'pacsonline',
+  },
+  //fdfs 配置
+  fdfs: {
+    trackers: [{ host: '127.0.0.1', port: 22122 }],
+    timeout: 10000000,
+    charset: 'utf8'
+  },
+
+  //上传文件配置
+  upload: {}
+}

+ 6 - 0
server/config/index.js

@@ -0,0 +1,6 @@
+import common from './env/common'
+
+//默认无开发环境
+const env = process.env.NODE_ENV || 'development'
+const config = require(`./env/${env}`).default
+export default Object.assign({}, common, config)

+ 43 - 0
server/config/passport.js

@@ -0,0 +1,43 @@
+// import passport   from 'koa-passport'
+// import {Strategy} from 'passport-local'
+
+// import User from '../src/models/users'
+
+// passport.serializeUser((user, done) => {
+//   done(null, user.id)
+// })
+
+// passport.deserializeUser(async (id, done) => {
+//   try {
+//     const user = await User.findById(id, '-password')
+//     done(null, user)
+//   } catch (err) {
+//     done(err)
+//   }
+// })
+
+// passport.use('local', new Strategy({usernameField: 'username', passwordField: 'password'}, 
+//   async (username, password, done) => {
+//     try {
+//       const user = await User.findOne({username})
+//       if (!user) {
+//         return done(null, false)
+//       }
+
+//       try {
+//         const isMatch = await user.validatePassword(password)
+//         if (!isMatch) {
+//           return done(null, false) 
+//         }
+
+//         done(null, user)
+//       } 
+//       catch (err) {
+//         done(err)
+//       }
+//     } 
+//     catch (err) {
+//       return done(err)
+//     }
+//   }
+// ))

+ 20 - 0
server/gulpfile.js

@@ -0,0 +1,20 @@
+const gulp    = require('gulp')
+const nodemon = require('gulp-nodemon')
+
+var jsScript = 'node';
+if (process.env.npm_config_argv.indexOf('debug') > 0) {  
+  jsScript = 'node debug';
+}
+
+gulp.task('nodemon', function (){
+  return nodemon({
+    script : 'index.js',
+    ext    : 'js json',
+    execMap: {js: jsScript},
+    env    : {NODE_ENV: 'development'},
+    verbose: true,
+    ignore : ['build/*.js', 'dist/*.js', 'nodemon.json', '.git', 'node_modules/**/node_modules', 'gulpfile.js'],
+  });
+});
+
+gulp.task('default', ['nodemon'], function () {});

+ 4 - 0
server/index.js

@@ -0,0 +1,4 @@
+require('babel-polyfill')
+require('babel-core/register')()
+
+require('./bin/server.js')

+ 40 - 0
server/package.json

@@ -0,0 +1,40 @@
+{
+  "name": "zskk-dicom-parse",
+  "main": "index.js",
+  "version": "1.0.0",
+  "keywords": [],
+  "apidoc": {
+    "title": "mysails"
+  },
+  "scripts": {
+    "start": "node index.js",
+    "build": "babel src -d dist",
+    "dev": "./node_modules/.bin/nodemon index.js",
+    "docs": "./node_modules/.bin/apidoc -i src/ -o docs",
+    "test": "NODE_ENV=test ./node_modules/.bin/mocha --compilers js:babel-register --require babel-polyfill"
+  },
+  "dependencies": {
+    "ali-oss": "^6.2.0",
+    "babel-core": "^6.26.3",
+    "babel-polyfill": "^6.26.0",
+    "babel-preset-es2015-node5": "^1.2.0",
+    "babel-preset-stage-0": "^6.24.1",
+    "busboy": "^0.3.1",
+    "dicom-parser": "^1.8.3",
+    "fs-promise": "^2.0.3",
+    "glob": "^7.1.6",
+    "iconv-lite": "^0.5.0",
+    "koa": "^2.11.0",
+    "koa-bodyparser": "^4.2.1",
+    "koa-convert": "^1.2.0",
+    "koa-cors": "0.0.16",
+    "koa-logger": "^3.2.1",
+    "koa-router": "^7.4.0",
+    "mysql2": "^2.0.0",
+    "path": "^0.12.7",
+    "sequelize": "^5.21.2"
+  },
+  "devDependencies": {
+    "nodemon": "^1.19.4"
+  }
+}

+ 22 - 0
server/pacs.config.js

@@ -0,0 +1,22 @@
+/**
+ * PM2启动配置
+ */
+module.exports = {
+  apps: [{
+    name: "zskk-dicom-parse",
+    script: 'index.js',
+
+    watch: ['src'],
+    ignore_watch: ['node_modules', 'dist', 'logs'],
+
+    out_file: '/data/logs/pm2/zskk-dicom-parse/out.log',
+    error_file: '/data/logs/pm2/zskk-dicom-parse/error.log',
+
+    max_memory_restart: '2G', //超过多大内存自动重启,仅防止内存泄露有意义,需要根据自己的业务设置
+    exec_mode: 'cluster', //开启多线程模式,用于负载均衡
+    instances: '2', //启用多少个实例,可用于负载均衡
+    autorestart: true, //程序崩溃后自动重启
+
+    env: { NODE_ENV: 'production' },
+  }]
+}

+ 26 - 0
server/src/dao/db/sequelize.js

@@ -0,0 +1,26 @@
+import Sequelize from 'sequelize'
+
+import config from '../../../config'
+
+const DB = config.dbconfig
+console.log(DB);
+
+export default new Sequelize(DB.database, DB.username, DB.password, {
+  host   : DB.host,
+  port   : DB.port,
+  dialect: config.db_type,
+
+  dialectOptions: {
+    charset: 'utf8mb4',
+    // collate: 'utf8mb4_unicode_520_ci',
+    bigNumberStrings : true,
+    supportBigNumbers: true
+  },
+
+  pool: {
+    max : 50,
+    min : 10,
+    idle: 10000
+  }
+  ,timezone:'+08:00'//修改时区为东八区
+})

+ 25 - 0
server/src/dao/exam.js

@@ -0,0 +1,25 @@
+const Sequelize = require('sequelize');
+
+import sequelize from './db/sequelize';
+
+/**
+ * 患者信息
+ */
+const Exam = sequelize.define('exams', {
+  id: { type: Sequelize.BIGINT, allowNull: false, primaryKey: true }, // uuid 16
+  patient_id: { type: Sequelize.STRING, allowNull: true }, //patient_info 的id
+  patient_num: { type: Sequelize.STRING, allowNull: true },
+  accession_num: { type: Sequelize.STRING, allowNull: true },
+  exam_datetime: { type: Sequelize.STRING, allowNull: true },
+  exam_class: { type: Sequelize.STRING, allowNull: true },
+  institution_id: { type: Sequelize.STRING, allowNull: true },
+  study_id: { type: Sequelize.STRING, allowNull: true },
+  exam_status: { type: Sequelize.INTEGER, allowNull: true },
+}, {
+  charset: 'utf8',
+  indexes: [
+    { fields: ["study_id"] }
+  ]
+});
+
+export default Exam;

+ 39 - 0
server/src/dao/images.js

@@ -0,0 +1,39 @@
+const Sequelize = require('sequelize');
+
+import sequelize from './db/sequelize';
+
+/**
+ * 患者信息
+ */
+const Image = sequelize.define('images', {
+  id: { type: Sequelize.STRING, allowNull: false, primaryKey: true },
+  series_id: { type: Sequelize.STRING, allowNull: true },
+  image_id: { type: Sequelize.STRING, allowNull: false, defaultValue: true, unique: 'imageid_index' },
+  sop_uid: { type: Sequelize.STRING, allowNull: true },
+  image_number: { type: Sequelize.INTEGER, allowNull: true },
+  frame: { type: Sequelize.INTEGER, allowNull: false, defaultValue: 0 },
+  owner: { type: Sequelize.STRING, allowNull: true },
+  window_width: { type: Sequelize.STRING, allowNull: true },
+  windo_center: { type: Sequelize.STRING, allowNull: true },
+  rows: { type: Sequelize.STRING, allowNull: true },
+  columns: { type: Sequelize.STRING, allowNull: true },
+  pixe_spacing: { type: Sequelize.STRING, allowNull: true },
+  image_position: { type: Sequelize.STRING, allowNull: true },
+  image_orientation: { type: Sequelize.STRING, allowNull: true },
+  url: { type: Sequelize.STRING, allowNull: true },
+  metadata: { type: Sequelize.STRING, allowNull: true },
+  local_url: { type: Sequelize.STRING, allowNull: true },
+  remote_url: { type: Sequelize.STRING, allowNull: true },
+  size: { type: Sequelize.INTEGER, allowNull: true },
+  institution_id: { type: Sequelize.STRING, allowNull: true },
+  cineRate: { type: Sequelize.INTEGER, allowNull: true, defaultValue: 0 }
+}, {
+  charset: 'utf8',
+  indexes: [
+    { fields: ["series_id"] }
+  ]
+});
+
+Image.sync({ force: false }).then(function() {});
+
+export default Image;

+ 12 - 0
server/src/dao/index.js

@@ -0,0 +1,12 @@
+import Patient from './patient'
+import Study from './study'
+import Series from './series'
+import Image from './images'
+import Exam from './exam'
+export {
+  Patient,
+  Study,
+  Series,
+  Image,
+  Exam
+}

+ 26 - 0
server/src/dao/patient.js

@@ -0,0 +1,26 @@
+const Sequelize = require('sequelize');
+
+import sequelize from './db/sequelize';
+
+/**
+ * 患者信息
+ */
+const Patient = sequelize.define('patient_infos', {
+  id: { type: Sequelize.BIGINT, allowNull: false, primaryKey: true, autoIncrement: true },
+  name: { type: Sequelize.STRING, allowNull: false, defaultValue: true },
+  sex: { type: Sequelize.STRING, allowNull: true },
+  birthday: { type: Sequelize.STRING, allowNull: true },
+  age: { type: Sequelize.STRING, allowNull: true },
+  temp_patient_id: { type: Sequelize.STRING, allowNull: true },
+  institution_id: { type: Sequelize.STRING, allowNull: true },
+  status: { type: Sequelize.INTEGER, allowNull: true }
+}, {
+  charset: 'utf8',
+  indexes: [
+    { fields: ["institution_id"] },
+    { fields: ["temp_patient_id"] }
+  ]
+});
+
+Patient.sync({ force: false }).then(function() {});
+export default Patient;

+ 24 - 0
server/src/dao/series.js

@@ -0,0 +1,24 @@
+const Sequelize = require('sequelize');
+
+import sequelize from './db/sequelize';
+
+/**
+ * 患者信息
+ */
+const Series = sequelize.define('series', {
+  id: { type: Sequelize.STRING, allowNull: false, primaryKey: true },
+  study_id: { type: Sequelize.STRING, allowNull: false },
+  seriesuid: { type: Sequelize.STRING, allowNull: true },
+  series_num: { type: Sequelize.INTEGER, allowNull: true },
+  modality: { type: Sequelize.STRING, allowNull: true },
+  description: { type: Sequelize.STRING, allowNull: true },
+}, {
+  charset: 'utf8',
+  indexes: [
+    { fields: ["study_id"] },
+    { fields: ["seriesuid"] }
+  ]
+});
+
+Series.sync({ force: false }).then(function() {});
+export default Series;

+ 30 - 0
server/src/dao/study.js

@@ -0,0 +1,30 @@
+const Sequelize = require('sequelize');
+
+import sequelize from './db/sequelize';
+
+/**
+ * 患者信息
+ */
+const Study = sequelize.define('studies', {
+  id: { type: Sequelize.STRING, allowNull: false, primaryKey: true },
+  studyuid: { type: Sequelize.STRING, allowNull: true },
+  patient_id: { type: Sequelize.STRING, allowNull: true },
+  studyid: { type: Sequelize.STRING, allowNull: true },
+  accession_num: { type: Sequelize.STRING, allowNull: true },
+  studydate: { type: Sequelize.STRING, allowNull: true },
+  modality: { type: Sequelize.STRING, allowNull: true },
+  status: { type: Sequelize.STRING, allowNull: true },
+  patient_age: { type: Sequelize.STRING, allowNull: true },
+  institution_name: { type: Sequelize.STRING, allowNull: true },
+  institution_id: { type: Sequelize.STRING, allowNull: true },
+}, {
+  charset: 'utf8',
+  indexes: [
+    { fields: ["patient_id"] },
+    { fields: ["modality"] },
+    { fields: ["studyuid"] }
+  ]
+});
+
+Study.sync({ force: false }).then(function() {});
+export default Study;

+ 12 - 0
server/src/middleware/index.js

@@ -0,0 +1,12 @@
+export function errorMiddleware() {
+  return async (ctx, next) => {
+    try {
+      await next()
+    }
+    catch (err) {
+      ctx.status = err.status || 500
+      ctx.body   = err.message
+      ctx.app.emit('error', err, ctx)
+    }
+  }
+}

+ 28 - 0
server/src/middleware/validators.js

@@ -0,0 +1,28 @@
+// import { verify } from 'jsonwebtoken'
+
+// import config from '../../config'
+// import { getToken } from '../utils/auth'
+
+// import User from '../models/users'
+
+// export async function ensureUser(ctx, next) {
+//   const token = getToken(ctx)
+
+//   if (!token) {
+//     ctx.throw(401)
+//   }
+
+//   let decoded = null
+//   try {
+//     decoded = verify(token, config.token)
+//   } catch (err) {
+//     ctx.throw(401)
+//   }
+
+//   ctx.state.user = await User.findById(decoded.id, '-password')
+//   if (!ctx.state.user) {
+//     ctx.throw(401)
+//   }
+
+//   return next()
+// }

+ 31 - 0
server/src/modules/index.js

@@ -0,0 +1,31 @@
+import glob   from 'glob'
+import Router from 'koa-router'
+
+//挂载路由
+exports = module.exports = function initModules (app) {
+  glob(`${__dirname}/*`, {ignore: '**/index.js' }, (err, matches) => {
+    if (err) {
+      throw err
+    }
+
+    matches.forEach((mod) => {
+      const router = require(`${mod}/router`)
+
+      const routes   = router.default
+      const baseUrl  = router.baseUrl
+
+      const instance = new Router({prefix: baseUrl})
+
+      routes.forEach((config) => {
+        const {method = '', route = '', handlers = []} = config
+        const lastHandler = handlers.pop()
+
+        instance[method.toLowerCase()](route, ...handlers, async function(ctx){
+          return await lastHandler(ctx)
+        })
+
+        app.use(instance.routes()).use(instance.allowedMethods())
+      })
+    })
+  })
+}

+ 60 - 0
server/src/modules/upload/controller.js

@@ -0,0 +1,60 @@
+import { parseDicom, deteleFile, uploadFile2FastDfs, uploadFile2AliOss, getFileMD5, doUpload, charsetFormat } from '../../service/parseDicomService'
+import { saveDicom } from '../../service/saveDicom'
+import config from '../../../config'
+// import fdfs from '../../service/fastdfs'
+const options = config.upload || {}
+const path = require('path');
+/** 上传文件 */
+export async function uploadFile(ctx) {
+  try {
+    let results = await doUpload(ctx);
+    for (let i = 0; i < results.length; i++) {
+      results[i] = await doUploadSingle(results[i]);
+    }
+    ctx.body = results;
+  } catch (err) {
+    console.log(err)
+    ctx.body = { code: 1, err, msg: '上传出现错误' };
+  }
+}
+
+async function doUploadSingle(result) {
+  const filePath = path.join(options.path || '.', options.fileType || 'common', result.fileName);
+  const md5 = await getFileMD5(filePath, result);
+  // const fileId = await uploadFile2FastDfs(fdfs, filePath)
+  const { url, metadata } = await uploadFile2AliOss(filePath, md5, result.hospital);
+  const { dataSet, length } = await parseDicom(result)
+  const saveFlag = await saveDicom(dataSet, length, url, metadata, result.hospital);
+  const deleteFlag = await deteleFile(filePath)
+  return {
+    ...result,
+    saveFlag,
+    deleteFlag,
+    url,
+    metadata,
+    // fileId,
+    md5
+  }
+}
+/** 上传文件页面检查 */
+export async function showUpload(ctx) {
+  const html = `
+        <html lang="en">
+            <head>
+                <meta charset="UTF-8">
+                <title>Title</title>
+            </head>
+            <body>
+                <div>
+                    <h1 style='font-size: 40px;'>请上传pacs图</h1>
+                    <form method="POST" action="/upload" enctype="multipart/form-data">
+                        <input name="filesize" type="input" style='width: 100%;height: 50px;font-size: 30px;margin-top: 30px;'/><br/><br/>
+                        <input name="file" type="file" multiple="multiple" style='width: 100%;height: 50px;font-size: 40px';margin-top: 30px;/><br/><br/>
+                        <button type="submit" style='width: 100%;height: 50px;font-size: 100px;'>提交</button>
+                    </form>
+                </div>
+            </body>
+        </html>
+    `
+  ctx.body = html
+}

+ 16 - 0
server/src/modules/upload/router.js

@@ -0,0 +1,16 @@
+import * as upload from './controller'
+
+export const baseUrl = '/upload'
+
+export default [
+  {
+    method: 'POST',
+    route : '/',
+    handlers: [upload.uploadFile]
+  },
+  {
+    method: 'GET',
+    route : '/',
+    handlers: [upload.showUpload]
+  }
+]

+ 323 - 0
server/src/service/dicomTag.js

@@ -0,0 +1,323 @@
+const patientTag = {
+  'patientName': 'x00100010', //患者姓名	   PN
+  'patientId': 'x00100020', //患者ID	    LO
+  'birthDate': 'x00100030', //患者出生日期	DA
+  'birthTime': 'x00100032', //患者出生时间	TM
+  'patientSex': 'x00100040', //患者性别	   CS
+  'pregnancy': 'x001021c0', //怀孕状态	   US
+  'weight': 'x00101030', //患者体重	   DS
+  'institutionName': 'x00080080', //生产厂家	   US
+  'institutionCode': 'x00080082', //生产厂家代码	   US
+  'patientAge': 'x00101010' //做检查时刻的患者年龄         AS
+
+};
+
+// 'patientName': {
+//   'tag': 'x00100010',
+//   'vr': 'PN',
+//   'description': 'Patient’s Name',
+//   'comment': '患者姓名'
+// },     //患者姓名	   PN
+const studyTag = {
+  'accessionNumber': 'x00080050', //检查号(RIS的生成序号)	      SH
+  'studyId': 'x00200010', //检查ID	                  SH
+  'studyUid': 'x0020000d', //检查实例号 唯一标记          UI
+  'studyDate': 'x00080020', //检查日期(检查开始的日期)	    DA
+  'studyTime': 'x00080030', //检查时间(检查开始的时间)	    TM
+  'modalities': 'x00080061', //一个检查中含有的不同检查类型	CS
+  'partExamined': 'x00080015', //检查的部位	                CS
+  'description': 'x00081030', //检查的描述	                LO
+  'patientAge': 'x00101010', //做检查时刻的患者年龄         AS
+  'institutionName': 'x00080080', //生产厂家	   US
+  'institutionCode': 'x00080082', //生产厂家代码	   US
+};
+
+const seriesTag = {
+  'seriesNumber': 'x00200011', //序列号:识别不同检查的号码.
+  'seriesUid': 'x0020000e', //序列实例号:唯一标记不同序列的号码.
+  'modality': 'x00080060', //检查模态(MRI/CT/CR/DR)
+  'description': 'x0008103e', //检查描述和说明
+  'seriesDate': 'x00080021', //检查日期
+  'seriesTime': 'x00080031', //检查时间
+  'imagePosition': 'x00200032', //图像位置:图像的左上角在空间坐标系中的x,y,z坐标,单位是毫米. 如果在检查中,则指该序列中第一张影像左上角的坐标.
+  'imageOrientation': 'x00200037', //图像方位:
+  'sliceThickness': 'x00180050', //层厚
+  'spacingSlices': 'x00180088', //层与层之间的间距,单位为mm
+  'sliceLocation': 'x00201041', //实际的相对位置,单位为mm.
+  'MRAcquisition': 'x00180023', //
+  'partExamined': 'x00180015', //身体部位.
+};
+
+const imageTag = {
+  'imageType': 'x00080008', //图片类型	                    CS
+  'imageId': 'x00080018', //实例UID
+  'contentDate': 'x00080023', //影像拍摄的日期	                DA
+  'contentTime': 'x00080033', //影像拍摄的时间	                TM
+  'imageNumber': 'x00200013', //图像码                       IS
+  'pixel': 'x00280002', //图像上的采样率.	             US
+  'rows': 'x00280010', //图像的总行数,行分辨率	 	        US
+  'columns': 'x00280011', //图像的总列数,列分辨率          US
+  'pixelSpacing': 'x00280030', //像素间距(像素中心之间的物理间距)	DS
+  'bitsStored': 'x00280101', //存储的位数:有12到16列举值.存储每一个像素用的位数.每一个样本应该有相同值.
+  'highBit': 'x00280102', //高位.	DS
+  'pixelRepresentation': 'x00280103', //像素数据的表现类型:这是一个枚举值,分别为十六进制数0000和0001.0000H = 无符号整数,0001H = 2的补码.	DS
+  'windowCenter': 'x00281050', //窗位	                        DS
+  'windowWidth': 'x00281051', //窗宽	                        DS
+  'rescaleIntercept': 'x00281052', //截距:如果表明不同模态的LUT颜色对应表不存在时,则使用方程Units = m*SV + b,计算真实的像素值到呈现像素值。其中这个值为表达式中的b
+  'rescaleSlope': 'x00281053', //斜率.这个值为表达式中的m。
+  'rescaleType': 'x00281054', //输出值的单位.这是一个枚举值,
+  'imagePosition': 'x00200032', //图像位置	                    DS
+  'imageOrientation': 'x00200037', //图像方位	                    DS
+  'SOPInstanceUID': 'x00080018', //图像方位	                    DS
+  'numberOfFrames': 'x00280008', //图像frame张数	                    DS
+  'cineRate': 'x00180040' //图像播放速率
+}
+
+/** Image Tag*/
+//00080008	Image Type	          图片类型	             CS
+//00080018	SOP Instance UID	    SOP实例UID
+//00080023	Content Date	        影像拍摄的日期	         DA
+//00080033	Content Time	        影像拍摄的时间.	       TM
+//00200013	Image/Instance Number	图像码(辨识图像的号码)	IS
+//00280002	Samples Per Pixel	    图像上的采样率.	        US
+/**
+ * MONOCHROME1,MONOCHROME2.
+ * 用来判断图像是否是彩色的,
+ * MONOCHROME1/2是灰度图,
+ * RGB则是真彩色图,还有其他.
+ */
+//00280004	Photometric Interpretation:	光度计的解释,对于CT图像,用两个枚举值	  CS
+//00280010	Rows	              图像的总行数,行分辨率.	               US
+//00280011	Columns             图像的总列数,列分辨率.	               US
+//00280030	Pixel Spacing	      像素间距(像素中心之间的物理间距)	        DS
+//00280100	Bits Allocated	    分配的位数(存储每一个像素值时分配的位数)	  US
+//00280101	Bits Stored:	      存储的位数(有12到16列举值  	            US
+//00280102	High Bit	          高位	                               US
+//00280103	Pixel Representation	像素数据的表现类型(枚举值,分别为十六进制数0000和0001)	US
+//00281050	Window Center	      窗位	       DS
+//00281051	Window Width	      窗宽	       DS
+//00281052	Rescale Intercept	  截距	       DS
+//00281053	Rescale Slope	      斜率	       DS
+//00281054	Rescale Type	      输出值的单位	LO
+
+/**图像的左上角在空间坐标系中的x,y,z坐标,单位是毫米. 如果在检查中,则指该序列中第一张影像左上角的坐标*/
+//00200032	Image Position 	        图像位置	               DS
+//00200037	Image Orientation       图像方位	               DS
+//00180050	Slice Thickness 	      层厚	                  DS
+//00180088	Spacing Between Slices	层与层之间的间距,单位为mm	DS
+//00201041	Slice Location	        实际的相对位置,单位为mm.	DS
+//00180023	MR Acquisition		                             CS
+//00180015	Body Part Examined	    身体部位	               CS
+const patientWADO = {
+  'patientName': ['00100010', 'PN'], //患者姓名	   PN
+  'patientId': ['00100020', 'LO'], //患者ID	    LO
+  'birthDate': ['00100030', 'DA'], //患者出生日期	DA
+  'birthTime': ['00100032', 'TM'], //患者出生时间	TM
+  'patientSex': ['00100040', 'CS'], //患者性别	   CS
+  'pregnancy': ['001021c0', 'US'], //怀孕状态	   US
+  'weight': ['00101030', 'DS'], //患者体重	   DS
+  'institutionName': ['00080080', 'US'], //生产厂家	   US
+  'institutionCode': ['00080082', 'US'], //生产厂家代码	   US
+  'patientAge': ['00101010', 'AS'] //做检查时刻的患者年龄         AS
+}
+
+const studyWADO = {
+  'accessionNumber': ['00080050', 'SH'], //检查号(RIS的生成序号)	      SH
+  'studyId': ['00200010', 'SH'], //检查ID	                    SH
+  'studyUid': ['0020000d', 'UI'], //检查实例号 唯一标记          UI
+  'studyDate': ['00080020', 'DA'], //检查日期(检查开始的日期)	    DA
+  'studyTime': ['00080030', 'TM'], //检查时间(检查开始的时间)	    TM
+  'modalities': ['00080061', 'CS'], //一个检查中含有的不同检查类型	  CS
+  'partExamined': ['00080015', 'CS'], //检查的部位	                CS
+  'description': ['00081030', 'LO'], //检查的描述	                LO
+  'patientAge': ['00101010', 'AS'] //做检查时刻的患者年龄          AS
+}
+
+const seriesWADO = {
+  'seriesNumber': ['00200011', 'IS'], //序列号:识别不同检查的号码.
+  'seriesUid': ['0020000e', 'UI'], //序列实例号:唯一标记不同序列的号码.
+  'modality': ['00080060', 'CS'], //检查模态(MRI/CT/CR/DR)
+  'description': ['0008103e', 'LO'], //检查描述和说明
+  'seriesDate': ['00080021', 'DA'], //检查日期
+  'seriesTime': ['00080031', 'DA'], //检查时间
+  'imagePosition': ['00200032', 'DS'], //图像位置:图像的左上角在空间坐标系中的x,y,z坐标,单位是毫米. 如果在检查中,则指该序列中第一张影像左上角的坐标.
+  'imageOrientation': ['00200037', 'DS'], //图像方位:
+  'sliceThickness': ['00180050', 'DS'], //层厚
+  'spacingSlices': ['00180088', 'DS'], //层与层之间的间距,单位为mm
+  'sliceLocation': ['00201041', 'DS'], //实际的相对位置,单位为mm.
+  'MRAcquisition': ['00180023', 'CS'], //
+  'partExamined': ['00180015', 'CS'], //身体部位.
+};
+
+const imageWADO = {
+  'imageType': ['00080008', 'CS'], //图片类型	                    CS
+  'imageId': ['00080018', 'UI'], //实例UID                      UI
+  'contentDate': ['00080023', 'DA'], //影像拍摄的日期	                DA
+  'contentTime': ['00080033', 'TM'], //影像拍摄的时间	                TM
+  'imageNumber': ['00200013', 'IS'], //图像码                       IS
+  'pixel': ['00280002', 'US'], //图像上的采样率.	             US
+  'rows': ['00280010', 'US'], //图像的总行数,行分辨率	 	        US
+  'columns': ['00280011', 'US'], //图像的总列数,列分辨率          US
+  'pixelSpacing': ['00280030', 'DS'], //像素间距(像素中心之间的物理间距)	DS
+  'bitsStored': ['00280101', 'US'], //存储的位数:有12到16列举值.存储每一个像素用的位数.每一个样本应该有相同值. US
+  'highBit': ['00280102', 'DS'], //高位.	DS
+  'pixelRepresentation': ['00280103', 'DS'], //像素数据的表现类型:这是一个枚举值,分别为十六进制数0000和0001.0000H = 无符号整数,0001H = 2的补码.	DS
+  'windowCenter': ['00281050', 'DS'], //窗位	                        DS
+  'windowWidth': ['00281051', 'DS'], //窗宽	                        DS
+  'rescaleIntercept': ['00281052', 'DS'], //截距:如果表明不同模态的LUT颜色对应表不存在时,则使用方程Units = m*SV + b,计算真实的像素值到呈现像素值。其中这个值为表达式中的b
+  'rescaleSlope': ['00281053', 'DS'], //斜率.这个值为表达式中的m。
+  'rescaleType': ['00281054', 'DS'], //输出值的单位.这是一个枚举值,
+  'imagePosition': ['00200032', 'DS'], //图像位置	                    DS
+  'imageOrientation': ['00200037', 'DS'], //图像方位	                    DS
+  'SOPInstanceUID': ['00080018', 'DS'], //图像方位
+  'numberOfFrames': ['00280008', 'IS'] //图像frame张数	                    DS
+}
+export {
+  patientTag,
+  studyTag,
+  seriesTag,
+  imageTag,
+  patientWADO,
+  studyWADO,
+  seriesWADO,
+  imageWADO
+}
+// tag                  VR                        VM                  Description
+// 00080005             CS                        1-n                 SpecificCharacterSet
+// 00080020             DA                        1-1                 StudyDate
+// 00080030             TM                        1-1                 StudyTime
+// 00080050             SH                        1-1                 Accession​Numbe
+// 00080056             CS                        1-1                 Instance​Availability
+// 00080061             CS                        1-n                 Modalities​In​Study
+// 00080090             PN                        1-1                 Referring​Physician​Name
+// 00081190             UR                        1-1                 Retrieve​URL
+// 00090010             LO                        1-1                 SpecificCharacterSet
+// 00091002             UN                        1-1                 SpecificCharacterSet
+// 00100010             PN                        1-1                 Patient​Name
+// 00100020             LO                        1-1                 Patient​Id
+// 00100021             LO                        1-1                 Issuer​Of​Patient​ID
+// 00100030             DT                        1-1                 Patient​Birth​Date
+// 00100040             CS                        1-1                 Patient​Sex
+// 00101002             SQ                        1-1                 Other​Patient​IDs​Sequence
+// 0020000D             UI                        1-1                 Study​Instance​UID
+// 00200010             SH                        1-1                 Study​ID
+// 00201206             IS                        1-1                 Number​Of​Study​Related​Series
+// 00201208             IS                        1-1                 Number​Of​Study​Related​Instances
+
+
+// {   // Result 1
+//         "00080005": {
+//             "vr": "CS",
+//             "Value": [ "ISO_IR 192" ]
+//         },
+//         "00080020": {
+//             "vr": "DT",
+//             "Value": [ "20130409" ]
+//         },
+//         "00080030": {
+//             "vr": "TM",
+//             "Value": [ "131600.0000" ]
+//         },
+//         "00080050": {
+//             "vr": "SH",
+//             "Value": [ "11235813" ]
+//         },
+//         "00080056": {
+//             "vr": "CS",
+//             "Value": [ "ONLINE" ]
+//         },
+//         "00080061": {
+//             "vr": "CS",
+//             "Value": [
+//                 "CT",
+//                 "PET"
+//             ]
+//         },
+//         "00080090": {
+//             "vr": "PN",
+//             "Value": [
+//               {
+//                 "Alphabetic": "^Bob^^Dr."
+//               }
+//             ]
+//         },
+//         "00081190": {
+//             "vr": "UR",
+//             "Value": [ "http://wado.nema.org/studies/
+//             1.2.392.200036.9116.2.2.2.1762893313.1029997326.945873" ]
+//         },
+//         "00090010": {
+//             "vr": "LO",
+//             "Value": [ "Vendor A" ]
+//         },
+//         "00091002": {
+//             "vr": "UN",
+//             "InlineBinary": [ "z0x9c8v7" ]
+//         },
+//         "00100010": {
+//             "vr": "PN",
+//             "Value": [
+//               {
+//                 "Alphabetic": "Wang^XiaoDong",
+//                 "Ideographic": "王^小東"
+//               }
+//             ]
+//         },
+//         "00100020": {
+//             "vr": "LO",
+//             "Value": [ "12345" ]
+//         },
+//         "00100021": {
+//             "vr": "LO",
+//             "Value": [ "Hospital A" ]
+//         },
+//         "00100030": {
+//             "vr": "DT",
+//             "Value": [ "19670701" ]
+//         },
+//         "00100040": {
+//             "vr": "CS",
+//             "Value": [ "M" ]
+//         },
+//         "00101002": {
+//             "vr": "SQ",
+//             "Value": [
+//                 {
+//                     "00100020": {
+//                         "vr": "LO",
+//                         "Value": [ "54321" ]
+//                     },
+//                     "00100021": {
+//                         "vr": "LO",
+//                         "Value": [ "Hospital B" ]
+//                     }
+//                 },
+//                 {
+//                     "00100020": {
+//                         "vr": "LO",
+//                         "Value": [ "24680" ]
+//                     },
+//                     "00100021": {
+//                         "vr": "LO",
+//                         "Value": [ "Hospital C" ]
+//                     }
+//                 }
+//             ]
+//         },
+//         "0020000D": {
+//             "vr": "UI",
+//             "Value": [ "1.2.392.200036.9116.2.2.2.1762893313.1029997326.945873" ]
+//         },
+//         "00200010": {
+//             "vr": "SH",
+//             "Value": [ "11235813" ]
+//         },
+//         "00201206": {
+//             "vr": "IS",
+//             "Value": [ 4 ]
+//         },
+//         "00201208": {
+//             "vr": "IS",
+//             "Value": [ 942 ]
+//         }
+//     }

+ 127 - 0
server/src/service/parseDicomService.js

@@ -0,0 +1,127 @@
+import dicomParse from '../utils/dicomParse'
+import config from '../../config'
+const fs = require('fs')
+const path = require('path')
+const Busboy = require('busboy')
+const crypto = require('crypto')
+const inspect = require('util').inspect
+const options = config.upload || {}
+  /** 同步创建文件目录 */
+const _mkdirsSync = dirname => {
+    if (fs.existsSync(dirname)) {
+      return true
+    } else {
+      if (_mkdirsSync(path.dirname(dirname))) {
+        fs.mkdirSync(dirname);
+        return true
+      }
+    }
+  }
+  /** 获取上传文件的后缀名 */
+const _getSuffixName = fileName => {
+    const nameList = fileName.split('.');
+    return nameList[nameList.length - 1]
+  }
+  /** 获取上传文件的医院ID */
+const _getHospitalId = fileName => {
+    const nameList = fileName.split('-');
+    return nameList[0]
+  }
+  /**dicom解析 */
+const parseDicom = async result => new Promise((resolve, reject) => {
+    dicomParse(result.fileName).then((dataSet) => {
+      resolve(dataSet)
+    }).catch(err => {
+      reject(err)
+    })
+  })
+  /**上传文件到fastdfs */
+const uploadFile2FastDfs = async(fdfs, filePath) => new Promise((resolve, reject) => {
+  fdfs.upload(filePath).then(function(fileId) {
+    resolve(fileId)
+  }).catch(function(err) {
+    reject(err)
+  })
+})
+
+const OSS = require('ali-oss')
+const client = new OSS({
+  region: 'oss-cn-beijing',
+  accessKeyId: 'LTAImzRGKfWaL7Vi',
+  accessKeySecret: 'GjWdd2cdHtbQkhhnhSxNbw0QChKD98',
+  bucket: 'zskk-dcm',
+});
+const uploadFile2AliOss = async(filePath, md5, hospitalId) => {
+    let result = await client.put(hospitalId + '/' + md5, filePath);
+    return { url: result.url.replace('http', 'dicomweb'), metadata: result.name };
+  }
+  // dicomweb://zskk-dcm.oss-cn-beijing.aliyuncs.com/73090001/d10378a36815fe3babd9b1e814b68fe6
+  // new Promise((resolve, reject) => {
+  //     let result = await client.put(hospitalId + '/' + md5, filePath);
+  //     config.log(result, 'result', hospitalId + '/' + md5, filePath)
+  //   })
+  /**删除文件 */
+const deteleFile = async filePath => new Promise((resolve, reject) => {
+    fs.unlink(filePath, function() {
+      resolve(true)
+    })
+  })
+  /**获取文件md5 */
+const getFileMD5 = async filePath => new Promise((resolve, reject) => {
+    let rs = fs.createReadStream(filePath);
+    let hash = crypto.createHash('md5');
+    rs.on('data', hash.update.bind(hash));
+    rs.on('end', function() {
+      resolve(hash.digest('hex'))
+    });
+  })
+  /** 上传文件 */
+const doUpload = async ctx => new Promise((resolve, reject) => {
+  let req = ctx.req;
+  let res = ctx.res;
+
+  let busboy = new Busboy({ headers: req.headers });
+
+  //获取类型
+  let fileType = options.fileType || 'common';
+  let filePath = path.join(options.path || '.', fileType);
+  let mkdirResult = _mkdirsSync(filePath);
+  let results = []
+  let resultsLength = 0
+  let finishFlag = false
+  busboy.on('file', function(fieldname, file, filename, encoding, mimetype) {
+    let fileName = Math.random().toString(16).substr(2) + '.' + _getSuffixName(filename);
+    let _uploadFilePath = path.join(filePath, fileName);
+    let saveTo = path.join(_uploadFilePath);
+    resultsLength++;
+    file.pipe(fs.createWriteStream(saveTo));
+    file.on('end', function() {
+      let result = {
+        'success': true,
+        'fileName': fileName,
+        'hospital': _getHospitalId(filename)
+      };
+      results.push(result);
+      if (finishFlag && results.length == resultsLength) {
+        resolve(results)
+      }
+    })
+  });
+  busboy.on('field', function(fieldname, val, fieldnameTruncated, valTruncated) {
+    console.log(`Field [${fieldname}]: value: ${inspect(val)}`)
+  });
+  busboy.on('finish', function() {
+    if (resultsLength == results.length) {
+      resolve(results)
+    } else {
+      finishFlag = true
+      console.log(`busboy finish : error: results长度不对`)
+    }
+  });
+  busboy.on('error', function(err) {
+    console.log(`busboy error : value: ${err}`)
+    reject(err)
+  });
+  req.pipe(busboy)
+})
+export { parseDicom, uploadFile2FastDfs, deteleFile, getFileMD5, doUpload, uploadFile2AliOss }

+ 14 - 0
server/src/service/saveDicom.js

@@ -0,0 +1,14 @@
+import { saveDicomInfo2PacsonlineByFileUpload } from './saveDicom2PacsonlineByFileUpload'
+import charsetFormat from '../utils/dicomCharsetFormat'
+/** 存储dicom数据 */
+const saveDicom = async(dataSet, size, url, metadata, hospitalId, isWAN = true) => {
+
+    charsetFormat(dataSet, hospitalId === '73090026' ? 'UTF-8' : 'GB18030')
+    return await _saveDicomByFileUpload(dataSet, size, url, metadata, hospitalId, isWAN)
+  }
+  /** 存储通过upload接口上传的dicom数据 */
+const _saveDicomByFileUpload = async(dataSet, size, url, metadata, hospitalId, isWAN) => {
+  const pacsonlineFlag = await saveDicomInfo2PacsonlineByFileUpload(dataSet, size, url, metadata, hospitalId, isWAN)
+  return { pacsonlineFlag }
+}
+export { saveDicom }

+ 222 - 0
server/src/service/saveDicom2PacsonlineByFileUpload.js

@@ -0,0 +1,222 @@
+import { Patient, Study, Series, Image, Exam } from '../dao'
+import { patientTag, studyTag, seriesTag, imageTag } from '../service/dicomTag'
+import { uuid16, formatTime } from '../utils/common'
+/**
+ * 保存dicom信息到pacsonline数据库
+ * @param {object} dataSet dicom数据
+ * @param {integer} size 影像大小
+ * @param {string} fileId fastdfs存储路径
+ * @param {string} hospitalId 医院id
+ * @param {boolean} isWAN 是否外网路径
+ */
+const saveDicomInfo2PacsonlineByFileUpload = async(dataSet, size, url, metadata, hospitalId, isWAN) => {
+    // console.log(dataSet.string(imageTag.imageId), 'dataSet.string(imageTag.imageId)')
+    // return
+    const patient = await _savePatient(dataSet, hospitalId) || {}
+    const study = await _saveStudy(dataSet, hospitalId, patient.id) || {}
+    const serie = await _saveSeries(dataSet, study.id) || {}
+    const image = await _saveImage(dataSet, hospitalId, serie.id, url, metadata, size, isWAN) || {}
+    const partExamined = dataSet.string(studyTag.partExamined)
+    const exam = await _saveExam(hospitalId, patient.id, study.id, patient.temp_patient_id, study.accession_num, study.studydate, study.modality, patient.institution_name, partExamined) || {}
+    return {
+      patient: patient.id ? true : false,
+      study: study.id ? true : false,
+      serie: serie.id ? true : false,
+      image: image.id ? true : false,
+      exam: exam.id ? true : false,
+    }
+  }
+  /**
+   * 保存患者信息
+   * @param {object} dataSet dicom数据
+   * @param {string} hospitalId 医院id
+   * */
+const _savePatient = async(dataSet, hospitalId) => {
+    const temp_patient_id = dataSet.string(patientTag.patientId)
+    const where = { temp_patient_id }
+      // const where = {temp_patient_id, 'institution_id': hospitalId}
+    console.log('where', where)
+    let old = await Patient.findOne({ where })
+    if (old) {
+      return old
+    }
+    const name = dataSet.string(patientTag.patientName)
+    const sex = dataSet.string(patientTag.patientSex)
+    const birthday = dataSet.string(patientTag.birthDate)
+    const age = dataSet.string(patientTag.patientAge)
+    const patient = { 'id': uuid16(), 'institution_id': hospitalId, 'status': 1, temp_patient_id, name, sex, birthday, age }
+    try {
+      old = await Patient.create(patient);
+    } catch (e) {
+      old = await Patient.findOne({ where });
+    }
+    console.log('old', old)
+    return old
+  }
+  /** 
+   * 保存检查信息 
+   * @param {object} dataSet dicom数据
+   * @param {string} hospitalId 医院id
+   * @param {string} patient_key 患者key
+   * */
+const _saveStudy = async(dataSet, hospitalId, patient_key) => {
+  const ignoreModalities = ['SR', 'PR']
+  const studyuid = dataSet.string(studyTag.studyUid)
+  const modality = dataSet.string(studyTag.modalities) || dataSet.string(seriesTag.modality)
+  const where = { studyuid, 'patient_id': patient_key }
+    // const where             = {studyuid, 'patient_id': patient_key, 'institution_id': hospitalId}
+  let old = await Study.findOne({ where })
+  if (old) {
+    if (modality && ignoreModalities.indexOf(old.modality) != -1 && ignoreModalities.indexOf(modality) == -1) {
+      old.modality = modality
+      await old.save()
+    }
+    return old;
+  }
+  let studydate = dataSet.string(studyTag.studyDate)
+  if (!studydate || (studydate + '').length < 8) {
+    studydate = formatTime()
+  }
+  const studyid = dataSet.string(studyTag.studyId)
+  const accession_num = dataSet.string(studyTag.accessionNumber)
+  const patient_age = dataSet.string(studyTag.patientAge)
+  const institution_name = dataSet.string(studyTag.institutionName)
+  const study = { 'id': uuid16(), 'patient_id': patient_key, 'status': 1, 'institution_id': hospitalId, studyuid, studyid, studydate, accession_num, modality, patient_age, institution_name }
+  try {
+    old = await Study.create(study)
+  } catch (err) {
+    old = await Study.findOne({ where })
+    console.error("_saveStudy", err, "study", old)
+  }
+  return old
+}
+
+/** 
+ * 保存序列信息 
+ * @param {object} dataSet dicom数据
+ * @param {string} study_key 检查key
+ * */
+const _saveSeries = async(dataSet, study_key) => {
+  const ignoreModalities = ['SR', 'PR']
+  const seriesuid = dataSet.string(seriesTag.seriesUid)
+  const modality = dataSet.string(seriesTag.modality)
+  const where = { seriesuid, modality, 'study_id': study_key }
+  let old = await Series.findOne({ where })
+  if (old) {
+    if (modality && ignoreModalities.indexOf(old.modality) != -1 && ignoreModalities.indexOf(modality) == -1) {
+      old.modality = modality
+      await old.save()
+    }
+    return old;
+  }
+  const series_num = dataSet.intString(seriesTag.seriesNumber)
+  const description = dataSet.string(seriesTag.description)
+  const serie = { 'id': uuid16(), 'study_id': study_key, seriesuid, modality, series_num, description }
+  try {
+    old = await Series.create(serie);
+  } catch (err) {
+    old = await Series.findOne({ where })
+    console.error("_saveSeries", err, "series", old)
+  }
+  return old
+}
+const IMAGE_URL_STATUS = {
+  LOCAL: 0b01,
+  REMOTE: 0b10
+}
+
+/** 
+ * 保存图像信息 
+ * @param {object} dataSet dicom数据
+ * @param {string} hospitalId 医院id
+ * @param {string} series_key 序列key
+ * @param {string} url 存储地址
+ * @param {integer} size 文件大小
+ * @param {boolean} isWAN 是否是外网地址
+ * */
+const _saveImage = async(dataSet, hospitalId, series_key, url, metadata, size, isWAN) => {
+    const image_id = dataSet.string(imageTag.imageId)
+    const where = { image_id, 'series_id': series_key }
+    let old = await Image.findOne({ where })
+    if (old) {
+      return old
+    }
+    const local_url = isWAN ? null : url
+    const remote_url = isWAN ? url : null
+    const status = isWAN ? IMAGE_URL_STATUS.REMOTE : IMAGE_URL_STATUS.LOCAL
+    const sop_uid = dataSet.string(imageTag.SOPInstanceUID)
+    const image_number = dataSet.intString(imageTag.imageNumber)
+    const pixe_spacing = dataSet.string(imageTag.pixelSpacing)
+    const window_width = dataSet.string(imageTag.windowWidth)
+    const windo_center = dataSet.string(imageTag.windowCenter)
+    const rows = dataSet.int16(imageTag.rows)
+    const columns = dataSet.int16(imageTag.columns)
+    const image_position = dataSet.string(imageTag.imagePosition)
+    const image_orientation = dataSet.string(imageTag.imageOrientation)
+    const owner = dataSet.string(patientTag.patientName)
+    const frame = dataSet.string(imageTag.numberOfFrames)
+    const cineRate = dataSet.string(imageTag.cineRate)
+    const image = { 'id': uuid16(), 'series_id': series_key, 'institution_id': hospitalId, 'size': size, 'url': url, 'metadata': metadata, status, image_id, sop_uid, image_number, pixe_spacing, window_width, windo_center, rows, columns, image_position, image_orientation, owner, frame, local_url, remote_url, cineRate }
+    try {
+      old = await Image.create(image)
+    } catch (err) {
+      old = await Image.findOne({ where })
+      console.error("_saveImage", err)
+    }
+    return old
+  }
+  /** 
+   * 保存检查信息 
+   * @param {string} hospitalId 医院id
+   * @param {string} patient_key 患者key
+   * @param {string} study_key 检查key
+   * @param {string} temp_patient_id 患者id
+   * @param {string} accession_num 检查号病历号
+   * @param {string} studydate 检查时间
+   * @param {string} modality 检查类型
+   * @param {string} institution_name 设备名称
+   * @param {string} partExamined 检查部位
+   * */
+const _saveExam = async(hospitalId, patient_key, study_key, temp_patient_id, accession_num, studydate, modality, institution_name, partExamined) => {
+  const where = { study_id: study_key }
+  const exam = {
+    id: uuid16(),
+    patient_id: patient_key,
+    study_id: study_key,
+    exam_status: 3,
+    patient_num: temp_patient_id,
+    accession_num: accession_num,
+    exam_datetime: studydate,
+    exam_class: modality,
+    institution_id: hospitalId,
+    device: institution_name,
+    body_part: partExamined
+      // exam_method
+  }
+  let old = await Exam.findOne({ where })
+  if (old) {
+    if (old.exam_status < exam.exam_status) {
+      old.exam_status = exam.exam_status
+    }
+    if (old.exam_class != exam.exam_class) {
+      old.exam_class = temp.exam_class
+    }
+    // old.patient_id      = temp.patient_id
+    // old.study_id        = temp.study_id
+    // old.exam_status     = exam.exam_status
+    // old.patient_num     = temp.patient_num
+    // old.accession_num   = temp.accession_num
+    // old.exam_datetime   = temp.exam_datetime
+    // old.exam_class      = temp.exam_class
+    // old.institution_id  = temp.institution_id
+    await old.save()
+    return old
+  }
+  try {
+    old = await Exam.create(exam);
+  } catch (e) {
+    old = await Exam.findOne({ where })
+  }
+  return old
+}
+export { saveDicomInfo2PacsonlineByFileUpload }

+ 176 - 0
server/src/utils/common.js

@@ -0,0 +1,176 @@
+//截取字符串,多余的部分用...代替
+export let setString = (str, len) => {
+  let StrLen = 0
+  let s = ''
+  for (let i = 0; i < str.length; i++) {
+    if (str.charCodeAt(i) > 128) {
+      StrLen += 2
+    } else {
+      StrLen++
+    }
+
+    s += str.charAt(i)
+    if (StrLen >= len) {
+      return s + '...'
+    }
+  }
+
+  return s
+}
+
+//格式化设置
+export let OptionFormat = (GetOptions) => {
+  let options = '{'
+  for (let n = 0; n < GetOptions.length; n++) {
+    options = options + '\'' + GetOptions[n].option_name + '\':\'' + GetOptions[n].option_value + '\''
+    if (n < GetOptions.length - 1) {
+      options = options + ','
+    }
+  }
+
+  return JSON.parse(options + '}')
+}
+
+//数组去重
+export let HovercUnique = (arr) => {
+  let n = {}
+  let r = []
+  for (var i = 0; i < arr.length; i++) {
+    if (!n[arr[i]]) {
+      n[arr[i]] = true
+      r.push(arr[i])
+    }
+  }
+
+  return r
+}
+
+//获取json长度
+export let getJsonLength = (jsonData) => {
+  var arr = []
+  for (var item in jsonData) {
+    arr.push(jsonData[item])
+  }
+
+  return arr.length
+}
+
+export let getRandomInt = (mix, max) => {
+  var num = Math.random() * (max - mix) + mix;
+  num = parseInt(num, 10);
+  return num;
+}
+
+export let getTimeSecond = (s) => {
+  let date, year, month, day = new Date()
+  if (s) {
+    date = new Date(s)
+  } else {
+    date = new Date()
+  }
+
+  return Math.round(date.getTime() / 1000)
+
+}
+
+export let formatTime = (s) => {
+
+  let getNum = (num) => {
+    if (num > 9) {
+      return num
+    } else {
+      return "0" + num
+    }
+  }
+
+  let date, year, month, day
+  if (s) {
+    date = new Date(s)
+  } else {
+    date = new Date()
+  }
+  year = date.getFullYear()
+  month = getNum(date.getMonth() + 1)
+  day = getNum(date.getDate())
+  return year + '' + month + '' + day + '';
+}
+
+export let formatSqlTime = (s) => {
+  let getNum = (num) => {
+    if (num > 9) {
+      return num
+    } else {
+      return "0" + num
+    }
+  }
+  let date, year, month, day, hour, minute, seconds
+  if (s) {
+    date = new Date(s * 1000)
+  } else {
+    date = new Date()
+  }
+  year = date.getFullYear()
+  month = getNum(date.getMonth() + 1)
+  day = getNum(date.getDate())
+  hour = getNum(date.getHours()); //获取当前小时数(0-23)  
+  minute = getNum(date.getMinutes()); //获取当前分钟数(0-59)  
+  seconds = getNum(date.getSeconds()); //获取当前秒数(0-59)
+
+
+  return `${year}-${month}-${day} ${hour}:${minute}:${seconds}`;
+}
+
+export let uuid = () => {
+  function S4() {
+    return (((1 + Math.random()) * 0x10000) | 0).toString(16).substring(1);
+  }
+  return (S4() + S4() + "-" + S4() + "-" + S4() + "-" + S4() + "-" + S4() + S4() + S4());
+}
+
+// export let uuid16 = () => {
+//     function S4() {
+//        return (((1+Math.random())*0x10000)|0).toString(16).substring(1);
+//     }
+//     return (S4()+ S4() + S4() + S4());
+// }
+
+
+export let uuid16 = () => {
+  function S4() {
+    return (((1 + Math.random()) * 0x10000) | 0).toString(16).substring(1);
+  }
+  return ('1' + new Date().getTime().toString(16) + S4());
+}
+export const TOKEN_VALIDITY_TIME = 3600 * 24
+
+const WeQrCodeObj = {
+  "11000001": "pacsln",
+  "11000002": "pacsln",
+  "13000001": "pacsjl",
+  "22100001": "pacsjs",
+  "22100002": "pacsjs",
+  "22100003": "pacsjs",
+  "40000001": "pacscq",
+  "6100001": "pacshb",
+  "15200001": "pacshlj"
+}
+
+export const getWxQrCodeByHospital = (hid) => {
+  return WeQrCodeObj[hid || '15200001'] || 'pacshlj'
+}
+
+Date.prototype.Format = function(fmt) { //author: meizz
+  var o = {
+    "M+": this.getMonth() + 1, //月份
+    "d+": this.getDate(), //日
+    "h+": this.getHours(), //小时
+    "m+": this.getMinutes(), //分
+    "s+": this.getSeconds(), //秒
+    "q+": Math.floor((this.getMonth() + 3) / 3), //季度
+    "S": this.getMilliseconds() //毫秒
+  };
+  if (/(y+)/.test(fmt)) fmt = fmt.replace(RegExp.$1, (this.getFullYear() + "").substr(4 - RegExp.$1.length));
+  for (var k in o)
+    if (new RegExp("(" + k + ")").test(fmt)) fmt = fmt.replace(RegExp.$1, (RegExp.$1.length == 1) ? (o[k]) : (("00" + o[k]).substr(("" + o[k]).length)));
+  return fmt;
+}

+ 19 - 0
server/src/utils/dicomCharsetFormat.js

@@ -0,0 +1,19 @@
+const iconv = require("iconv-lite")
+iconv.skipDecodeWarning = true;
+/** 格式化dicom解析出来的数据的编码 */
+const charsetFormat = (dataSet, charSet = 'GB18030') => {
+  const originalString = dataSet.string;
+  dataSet.string = (tag, index) => {
+    let value = originalString.call(dataSet, tag, index);
+    if (value != null && value != 'undefined') {
+      let _value = iconv.decode(value, charSet);
+      if (_value != null && _value != 'undefined') {
+        return _value.replace(/['^']+/g, '')
+      }
+      return _value;
+    } else {
+      return value;
+    }
+  };
+}
+export default charsetFormat

+ 19 - 0
server/src/utils/dicomParse.js

@@ -0,0 +1,19 @@
+import config from '../../config'
+const path = require('path');
+const fsp = require('fs-promise');
+const parser = require('dicom-parser');
+
+let options = config.upload || {};
+const dicomParse = async fileName => {
+  const filePath = path.join(options.path || '.', options.fileType || 'common', fileName);
+  const fileBuffer = await fsp.readFile(filePath);
+  try {
+    return {
+      "dataSet": parser.parseDicom(fileBuffer),
+      "length": fileBuffer.length
+    };
+  } catch (err) {
+    return null
+  }
+}
+export default dicomParse