浏览代码

feature:
1. oss切换到ali-oss
2. 项目立项前,项目owner改为商务负责人,原为项目负责人

eyes4 4 月之前
父节点
当前提交
1e32796c2e
共有 38 个文件被更改,包括 4352 次插入2578 次删除
  1. 3295 2425
      base-lib/package-lock.json
  2. 7 3
      base-lib/package.json
  3. 1 1
      base-lib/src/core-guards/apiGuards.ts
  4. 2 0
      base-lib/src/core-models/AcsApplication.ts
  5. 2 0
      base-lib/src/core-models/AcsFunction.ts
  6. 4 1
      base-lib/src/core-models/AcsModule.ts
  7. 7 1
      base-lib/src/core-models/AcsRole.ts
  8. 193 6
      base-lib/src/core-models/BpmnForm.ts
  9. 1 1
      base-lib/src/core-models/PrjInfo.ts
  10. 1 0
      base-lib/src/core/Defined.ts
  11. 8 2
      base-lib/src/core/Router.ts
  12. 138 18
      base-lib/src/util/AliOss.ts
  13. 247 0
      base-lib/src/util/AmazonS3.ts
  14. 3 1
      base-lib/src/util/Config.ts
  15. 10 2
      base-lib/src/util/Executor.ts
  16. 33 12
      base-lib/src/util/Minio.ts
  17. 13 3
      base-lib/src/util/Oss.ts
  18. 8 2
      base-lib/src/util/Resp.ts
  19. 26 26
      deploy/docker-compose.yml
  20. 1 0
      pmr-access-control/package.json
  21. 0 1
      pmr-api-gateway/config.json
  22. 1 1
      pmr-api-gateway/proxy.json
  23. 1 1
      pmr-api-gateway/proxy_dev.json
  24. 56 21
      pmr-biz-manager/bpmn/立项流程.bpmn
  25. 8 7
      pmr-biz-manager/config.json
  26. 25 1
      pmr-biz-manager/config_dev.json
  27. 3 0
      pmr-biz-manager/package.json
  28. 16 3
      pmr-biz-manager/src/app.ts
  29. 52 1
      pmr-biz-manager/src/bpmn/flow_engine.ts
  30. 36 0
      pmr-biz-manager/src/routes/api/callback/oss/upload_file.ts
  31. 6 1
      pmr-biz-manager/src/routes/api/prj/contract/upload_doc.ts
  32. 18 32
      pmr-biz-manager/src/routes/api/prj/info/add.ts
  33. 2 2
      pmr-biz-manager/src/routes/api/prj/info/remove.ts
  34. 2 1
      pmr-biz-manager/src/routes/api/prj/plan/add_task.ts
  35. 5 1
      pmr-biz-manager/src/routes/api/prj/week_report/upload_file.ts
  36. 3 1
      pmr-biz-manager/src/routes/api/prj/work/upload_file.ts
  37. 102 0
      pmr-biz-manager/src/utils/file_upload_helper.ts
  38. 16 0
      pmr-biz-manager/src/utils/prj_premission_helper.ts

文件差异内容过多而无法显示
+ 3295 - 2425
base-lib/package-lock.json


+ 7 - 3
base-lib/package.json

@@ -15,7 +15,6 @@
   "license": "ISC",
   "devDependencies": {
     "@types/ajv": "^1.0.0",
-    "@types/ali-oss": "^6.16.4",
     "@types/axios": "^0.14.0",
     "@types/compression": "^1.7.0",
     "@types/express": "^4.17.11",
@@ -34,9 +33,12 @@
     "typescript": "^4.2.4"
   },
   "dependencies": {
+    "@aws-sdk/client-s3": "^3.608.0",
+    "@aws-sdk/s3-request-presigner": "^3.608.0",
+    "@types/ali-oss": "^6.16.11",
     "@types/body-parser": "^1.19.5",
     "ajv": "^7.0.3",
-    "ali-oss": "^6.17.1",
+    "ali-oss": "^6.20.0",
     "ast-to-markdown": "^1.0.0",
     "axios": "^0.21.1",
     "biguint-format": "^1.0.2",
@@ -48,11 +50,12 @@
     "json-stringify-safe": "^5.0.1",
     "kafka-node": "^5.0.0",
     "mdast-builder": "^1.1.1",
-    "minio": "^7.0.32",
+    "minio": "^7.1.0",
     "mongoose": "5.13.14",
     "path-to-regexp": "^1.7.0",
     "pg": "^8.8.0",
     "pg-hstore": "^2.3.4",
+    "proxy-agent": "^5.0.0",
     "querystring": "^0.2.1",
     "redis": "^4.3.0",
     "reflect-metadata": "^0.1.13",
@@ -62,6 +65,7 @@
     "sequelize": "^6.6.2",
     "string-random": "^0.1.3",
     "unified": "^10.1.2",
+    "urllib": "^4.1.0",
     "uuid": "^8.3.2",
     "webpack-env": "^0.8.0",
     "websocket": "^1.0.34",

+ 1 - 1
base-lib/src/core-guards/apiGuards.ts

@@ -14,7 +14,7 @@ import {bizTokenGuard} from "@core-guards/rest/bizTokenGuard";
 
 export const apiGuards: Array<IGuard> = [
     {
-        path: ['/api/mp/login', '/api/client/login', '/api/client/get_vcode'],
+        path: ['/api/mp/login', '/api/client/login', '/api/client/get_vcode', '/api/callback.*'],
         guards: []
     },
     {

+ 2 - 0
base-lib/src/core-models/AcsApplication.ts

@@ -79,5 +79,7 @@ export class AcsApplication extends Model {
     ]
     static init_sqls = [
         `insert into tb_acs_application (app_id, app_key, name, app_type, auth_type, status) values('f2741721-1c07-430e-9f01-5529692340f9', 'UA3u7eX9BtHNtWitnbvbWJxQK6RepbqZ', 'web客户端', 3, 1, 1) ON CONFLICT DO NOTHING`,
+        `insert into tb_acs_application (app_id, app_key, name, app_type, auth_type, status) values('wx78e79f8a976d3952', 'c0d7b98c4088dbf877be4cfa24c152cc', '牧云项目管理小程序', 1, 1, 1) ON CONFLICT DO NOTHING`,
+
     ];
 }

+ 2 - 0
base-lib/src/core-models/AcsFunction.ts

@@ -106,6 +106,8 @@ export class AcsFunction extends Model {
             (65, 8, 'modify_pass', '修改密码', 'acs.self.modify_pass', false, 1),
             (66, 8, 'reset_pass', '重置密码', 'acs.self.reset_pass', false, 1),
             
+            (90, 21, 'upload_file', 'OSS上传回调', 'callback.oss.upload_file', false, 0),
+            
             (100, 101, 'get_list', '获取客户级别', 'cfg.customer_level.get_list', true, 0),
             (110, 102, 'get_list', '获取文档类型', 'cfg.doc_type.get_list', true, 0),
             (120, 103, 'get_list', '获取行业类型', 'cfg.industry_type.get_list', true, 0),

+ 4 - 1
base-lib/src/core-models/AcsModule.ts

@@ -78,7 +78,10 @@ export class AcsModule extends Model {
             (7, 'user', '系统用户管理', 1, text2ltree('1.7'), 'acs.user'),
             (8, 'self', '账号自助', 1, text2ltree('1.8'), 'acs.self'),
 
-
+            (20, 'callback', '回调接口', null, text2ltree('20'), 'callback'),
+            (21, 'oss', 'oss回调', 20, text2ltree('20.21'), 'callback.oss'),
+            
+            
             (100, 'cfg', '配置接口', null, text2ltree('100'), 'cfg'),
             (101, 'customer_level', '客户级别', 100, text2ltree('100.101'), 'cfg.customer_level'),
             (102, 'doc_type', '文档类型', 100, text2ltree('100.102'), 'cfg.doc_type'),

+ 7 - 1
base-lib/src/core-models/AcsRole.ts

@@ -32,10 +32,16 @@ export class AcsRole extends Model {
             ('cto', '技术总监', 'sys')
             ON CONFLICT DO NOTHING `,
         `insert into tb_acs_role (id, name, tag) values
-            ('prj_leader', '项目组长', 'prj')
+            ('leader', '技术组长', 'sys')
+            ON CONFLICT DO NOTHING `,
+        `insert into tb_acs_role (id, name, tag) values
+            ('prj_leader', '项目负责人', 'prj')
             ON CONFLICT DO NOTHING `,
         `insert into tb_acs_role (id, name, tag) values
             ('prj_member', '项目成员', 'prj')
+            ON CONFLICT DO NOTHING `,
+        `insert into tb_acs_role (id, name, tag) values
+            ('prj_bizman', '商务负责人', 'prj')
             ON CONFLICT DO NOTHING `
     ]
 }

+ 193 - 6
base-lib/src/core-models/BpmnForm.ts

@@ -1,31 +1,203 @@
 import {Model} from "sequelize";
 import { Sequelize, DataTypes }  from 'sequelize';
 
+const form_prj_create_req = `
+{
+  "type": "object",
+  "title": "立项申请",
+  "required": [
+    "memo"
+  ],
+  "ui:order": [
+    "memo",
+    "files"
+  ],
+  "properties": {
+    "memo": {
+      "type": "string",
+      "title": "立项申请理由",
+      "format": "textarea",
+      "minRows": 5,
+      "description": "请输入立项申请的相关说明"
+    },
+    "files": {
+      "type": "upload",
+      "title": "立项文件",
+      "ui:options": {
+        "type": "upload",
+        "accept": ".doc,.docx,application/msword",
+        "limitCount": 1
+      },
+      "description": "上传立项相关文件,如可行性报告等"
+    }
+  }
+}
+`;
 const form_prj_approve = `
 {
   "type": "object",
-  "title": "条件",
+  "title": "立项审核",
+  "required": [
+    "pass"
+  ],
+  "ui:order": [
+    "memo",
+    "files",
+    "pass",
+    "opinion"
+  ],
   "properties": {
+    "memo": {
+      "type": "string",
+      "title": "申请理由",
+      "format": "textarea",
+      "minRows": 5,
+      "default": "\${environment.output.task_request.memo}",
+      "readonly": true
+    },
+    "files": {
+      "type": "download",
+      "title": "立项文件",
+      "default": \${JSON.stringify(environment.output.task_request.files)}
+    },
     "pass": {
-      "description": "是否通过",
-      "type": "boolean"
+      "type": "boolean",
+      "title": "是否通过审核?",
+      "default": false
     },
     "opinion": {
       "type": "string",
-      "description": "审核意见",
+      "title": "审核意见",
+      "format": "textarea",
+      "default": "",
+      "minRows": 6,
       "minLength": 10,
       "ui:options": {
+        "rows": 10,
         "type": "textarea"
       }
     }
-  },
+  }
+}
+`;
+const form_plan_create_approve = `
+{
+  "type": "object",
+  "title": "计划审核",
+  "required": [
+    "pass"
+  ],
+  "ui:order": [
+    "pass",
+    "opinion"
+  ],
+  "properties": {
+    "pass": {
+      "type": "boolean",
+      "title": "是否通过审核?",
+      "default": false
+    },
+    "opinion": {
+      "type": "string",
+      "title": "审核意见",
+      "format": "textarea",
+      "default": "",
+      "minRows": 6
+    }
+  }
+}
+`;
+const form_plan_alt_approve = `
+{
+  "type": "object",
+  "title": "计划变更审核",
+  "required": [
+    "pass"
+  ],
   "ui:order": [
     "pass",
     "opinion"
   ],
+  "properties": {
+    "pass": {
+      "type": "boolean",
+      "title": "是否通过审核?",
+      "default": false
+    },
+    "opinion": {
+      "type": "string",
+      "title": "审核意见",
+      "format": "textarea",
+      "default": "",
+      "minRows": 6
+    }
+  }
+}
+`;
+const form_task_approve = `
+{
+  "type": "object",
+  "title": "任务审核",
   "required": [
     "pass"
-  ]
+  ],
+  "ui:order": [
+    "pass",
+    "opinion",
+    "annotated_draft"
+  ],
+  "properties": {
+    "pass": {
+      "type": "boolean",
+      "title": "是否通过?",
+      "default": false
+    },
+    "opinion": {
+      "type": "upload",
+      "title": "审核意见",
+      "ui:options": {
+        "type": "upload",
+        "accept": ".doc,.docx,application/msword",
+        "limitCount": 1
+      }
+    },
+    "annotated_draft": {
+      "type": "upload",
+      "title": "批注稿",
+      "ui:options": {
+        "type": "upload",
+        "accept": ".doc,.docx,application/msword",
+        "limitCount": 1
+      }
+    }
+  }
+}
+`;
+const form_select_approve_type = `
+{
+  "type": "object",
+  "title": "选择任务审核方式",
+  "required": [
+    "check_type"
+  ],
+  "ui:order": [
+    "check_type"
+  ],
+  "properties": {
+    "check_type": {
+      "enum": [
+        1,
+        2
+      ],
+      "type": "integer",
+      "title": "审核方式",
+      "enumNames": [
+        "顺序审核",
+        "并行审核"
+      ],
+      "description": "审核方式,目前是并行和串行"
+    }
+  }
 }
 `;
 
@@ -61,9 +233,24 @@ export class BpmnForm extends Model {
     static associate(sequelize: Sequelize) {}
 
     static init_sqls = [
+        `insert into ${BpmnForm.table_name} (id, title, schema) values
+            ('prj_create_req', '立项申请', '${form_prj_create_req}')
+            ON CONFLICT DO NOTHING `,
         `insert into ${BpmnForm.table_name} (id, title, schema) values
             ('prj_approve', '立项审核', '${form_prj_approve}')
             ON CONFLICT DO NOTHING `,
+        `insert into ${BpmnForm.table_name} (id, title, schema) values
+            ('plan_create_approve', '计划审核', '${form_plan_create_approve}')
+            ON CONFLICT DO NOTHING `,
+        `insert into ${BpmnForm.table_name} (id, title, schema) values
+            ('plan_alt_approve', '计划变更审核', '${form_plan_alt_approve}')
+            ON CONFLICT DO NOTHING `,
+        `insert into ${BpmnForm.table_name} (id, title, schema) values
+            ('task_approve', '任务审核', '${form_task_approve}')
+            ON CONFLICT DO NOTHING `,
+        `insert into ${BpmnForm.table_name} (id, title, schema) values
+            ('select_approve_type', '选择任务审核方式', '${form_select_approve_type}')
+            ON CONFLICT DO NOTHING `
 
     ]
 }

+ 1 - 1
base-lib/src/core-models/PrjInfo.ts

@@ -97,7 +97,7 @@ export class PrjInfo extends Model {
         },
         leader_id: {
             type: DataTypes.STRING,
-            allowNull: false,
+            allowNull: true,
             comment: '项目负责人账号id',
             references: {
                 model: AcsUserInfo,

+ 1 - 0
base-lib/src/core/Defined.ts

@@ -109,6 +109,7 @@ export interface IRequest {
     sign?: string;
     token?: string;
     data: unknown;          // 请求参数
+    [property: string]: any;
 }
 
 export interface IPageRequestData {

+ 8 - 2
base-lib/src/core/Router.ts

@@ -3,7 +3,7 @@ import {Logger} from "@util/Logger";
 import {Resp} from "@util/Resp";
 import {IApiProcessor, IGuard, IMethodModule, IMethodParams, ICachedData, IApiInfo} from "@core/Defined";
 import {Auditor} from '@core/Auditor';
-import {extend} from "@util/JsonToolkit";
+import {clone, extend} from "@util/JsonToolkit";
 
 export interface IRouteAliases {
     full_alias: any;
@@ -145,7 +145,13 @@ export class Router extends RouterBase {
                     let r = await module.processor.on_before_respond(content, module.processor.method_params, cached_data, result);
                     if (r) result = r;
                 }
-                Resp.respond(module.api, res, result);
+                let info;
+                if (result && result.__info) {
+                    info = clone(result.__info)
+                    delete result.__info;
+                }
+
+                Resp.respond(module.api, res, result, info);
                 if (module.processor.on_completed) await module.processor.on_completed(content, module.processor.method_params, cached_data, result);
             } catch (e: any) {
                 Logger.error(e);

+ 138 - 18
base-lib/src/util/AliOss.ts

@@ -3,9 +3,10 @@ import {Resp} from "@util/Resp";
 import {Stream} from "stream";
 import {Logger} from "@util/Logger";
 import {IOssConfig} from "@util/Config";
-
+const { default: axios } = require("axios");
+const fs = require("fs");
 const OSS = require('ali-oss');
-
+const { STS } = require("ali-oss");
 export type AliOssTag = string;
 
 
@@ -21,7 +22,10 @@ export class AliOss implements IOss {
         this.client = new OSS({
             endpoint: config.end_point,
             accessKeyId: config.access_key,
-            accessKeySecret: config.secret_key
+            accessKeySecret: config.secret_key,
+            region: config.region,
+            authorizationV4: true,
+            secure: this.config.use_ssl
         })
     }
 
@@ -50,6 +54,31 @@ export class AliOss implements IOss {
         });
     }
 
+    object_exists(bucket: string, name: string): Promise<boolean> {
+        return new Promise<boolean>(async (resolve, reject) => {
+            try {
+                let client = new OSS({
+                    bucket: bucket,
+                    endpoint: this.config.end_point,
+                    accessKeyId: this.config.access_key,
+                    accessKeySecret: this.config.secret_key,
+                    authorizationV4: true,
+                    secure: this.config.use_ssl
+                });
+                await client.head(name, {});
+                resolve(true);
+            } catch (error: any) {
+                Logger.error(error);
+                // 指定的存储空间不存在。
+                if (error.name === 'NoSuchKey') {
+                    resolve(false);
+                } else {
+                    reject(Resp.gen_err(Resp.InternalServerError, error.message));
+                }
+            }
+        });
+    }
+
     make_bucket(bucket: string): Promise<void> {
         return new Promise<void>(async (resolve, reject) => {
             try {
@@ -83,7 +112,9 @@ export class AliOss implements IOss {
                     bucket: bucket,
                     endpoint: this.config.end_point,
                     accessKeyId: this.config.access_key,
-                    accessKeySecret: this.config.secret_key
+                    accessKeySecret: this.config.secret_key,
+                    authorizationV4: true,
+                    secure: this.config.use_ssl
                 });
                 Logger.info({
                     bucket: bucket,
@@ -104,10 +135,13 @@ export class AliOss implements IOss {
         return new Promise<void>(async (resolve, reject) => {
             try {
                 let client = new OSS({
+                    region: this.config.region,
                     bucket: bucket,
-                    endpoint: this.config.end_point,
+                    // endpoint: this.config.end_point,
                     accessKeyId: this.config.access_key,
-                    accessKeySecret: this.config.secret_key
+                    accessKeySecret: this.config.secret_key,
+                    authorizationV4: true,
+                    secure: this.config.use_ssl
                 });
                 await client.delete(objectName);
                 resolve();
@@ -121,10 +155,13 @@ export class AliOss implements IOss {
         return new Promise<void>(async (resolve, reject) => {
             try {
                 let client = new OSS({
+                    region: this.config.region,
                     bucket: bucket,
-                    endpoint: this.config.end_point,
+                    // endpoint: this.config.end_point,
                     accessKeyId: this.config.access_key,
-                    accessKeySecret: this.config.secret_key
+                    accessKeySecret: this.config.secret_key,
+                    authorizationV4: true,
+                    secure: this.config.use_ssl
 
                 });
                 await client.copy(dest_object_name, src_object_name);
@@ -139,12 +176,16 @@ export class AliOss implements IOss {
         return new Promise<string>(async (resolve, reject) => {
             try {
                 let client = new OSS({
+                    region: this.config.region,
                     bucket: bucket,
-                    endpoint: this.config.end_point,
+                    // endpoint: this.config.end_point,
                     accessKeyId: this.config.access_key,
-                    accessKeySecret: this.config.secret_key
+                    accessKeySecret: this.config.secret_key,
+                    authorizationV4: true,
+                    secure: this.config.use_ssl
                 });
                 let url = client.signatureUrl(objectName, {expires: expiry});
+
                 resolve(url);
             } catch (e: any) {
                 reject(Resp.gen_err(Resp.InternalServerError, `获取图片url出错, ${e.message}`));
@@ -152,19 +193,63 @@ export class AliOss implements IOss {
         })
     }
 
+    //https://help.aliyun.com/zh/oss/use-cases/node-js?spm=a2c4g.11186623.0.0.53324cf5Ov5aix
+    //https://help.aliyun.com/zh/oss/use-cases/uploading-objects-to-oss-directly-from-clients/?spm=a2c4g.11186623.0.i1#36c322a137x9q
     presignedPostPolicy(bucket: string, objectName: string, expiry: number, prefix?: string): Promise<any> {
-        return new Promise<string>(async (resolve, reject) => {
+        return new Promise<any>(async (resolve, reject) => {
             try {
-                let client = new OSS({
+                /*let client = new OSS({
+                    region: this.config.region,
                     bucket: bucket,
-                    endpoint: this.config.end_point,
+                    // endpoint: this.config.end_point,
                     accessKeyId: this.config.access_key,
-                    accessKeySecret: this.config.secret_key
+                    accessKeySecret: this.config.secret_key,
+                    authorizationV4: true,
+                    secure: this.config.use_ssl
+                    // cname: true
+                });
+
+                let url = await client.signatureUrlV4('POST', expiry, {
+                    method: 'POST',
+                    headers: {
+                        "Content-Type": "multipart/form-data",
+                    }
+                }, objectName);
+
+                const file = fs.readFileSync("/Users/chengang/Downloads/test.docx");
+                axios({
+                    url,
+                    headers: {
+                        "Content-Type": "multipart/form-data",
+                    },
+                    method: "POST",
+                    data: file,
+                })
+                    .then((r: any) => console.log(r))
+                    .catch((e: any) => console.log(e));*/
+                let result = await this.get_token(bucket, expiry);
+                result.formData.key = objectName;
+                if (this.config.callback_url) {
+                    const callback = {
+                        callbackUrl: this.config.callback_url,
+                        callbackBody:// 设置回调的内容,例如文件ETag、资源类型mimeType等。
+                            "filename=${object}&size=${size}&mimeType=${mimeType}",
+                        callbackBodyType: "application/x-www-form-urlencoded",
+                    }
+                    result.formData.callback = Buffer.from(JSON.stringify(callback)).toString("base64");
+                }
+
+                resolve({
+                    // postURL: url,
+                    // headers: {
+                    //     method: 'POST',
+                    //     "Content-Type": "multipart/form-data",
+                    // },
+                    // formData
+                    ...result
                 });
-                let url = client.signatureUrl(objectName, {expires: expiry, method: 'POST'});
-                resolve(url);
             } catch (e: any) {
-                reject(Resp.gen_err(Resp.InternalServerError, `获取图片url出错, ${e.message}`));
+                reject(Resp.gen_err(Resp.InternalServerError, `获取oss上传url出错, ${e.message}`));
             }
         });
     }
@@ -180,7 +265,42 @@ export class AliOss implements IOss {
 
     stat_object(bucket: string, object_name: string): Promise<IObjectStat | undefined> {
         return new Promise<IObjectStat | undefined>(async (resolve, _reject) => {
-            return resolve(undefined);
+            try {
+                let client = new OSS({
+                    bucket: bucket,
+                    endpoint: this.config.end_point,
+                    accessKeyId: this.config.access_key,
+                    accessKeySecret: this.config.secret_key,
+                    authorizationV4: true,
+                    secure: this.config.use_ssl
+                });
+                let head = await client.head(object_name, {});
+                resolve({
+                    size: head.res.headers['content-length'],
+                    etag: head.res.headers['etag']
+                });
+            } catch (error: any) {
+                // Logger.error(error);
+                resolve(undefined);
+            }
         });
     }
+
+    private async get_token(bucket: string, expiry: number) {
+        const date = new Date();
+        date.setSeconds(date.getSeconds() + expiry);
+        const policy = {
+            expiration: date.toISOString(),
+            conditions: [
+                {bucket}
+            ]
+        }
+        // 调用SDK获取签名。
+        const formData = await this.client.calculatePostSignature(policy);
+        const host = `https://${bucket}.${this.config.region}.aliyuncs.com`.toString();
+        return {
+            postURL: host,
+            formData
+        }
+    }
 }

+ 247 - 0
base-lib/src/util/AmazonS3.ts

@@ -0,0 +1,247 @@
+import {IObjectStat, IOss} from "@util/Oss";
+import {Resp} from "@util/Resp";
+import {Stream} from "stream";
+import {Logger} from "@util/Logger";
+import {IOssConfig} from "@util/Config";
+import {CreateBucketCommand} from "@aws-sdk/client-s3";
+import {CreateBucketCommandInput} from "@aws-sdk/client-s3/dist-types/commands/CreateBucketCommand";
+const { default: axios } = require("axios");
+const fs = require("fs");
+const { getSignedUrl } = require("@aws-sdk/s3-request-presigner");
+const { S3Client, GetObjectCommand, HeadBucketCommand } = require("@aws-sdk/client-s3");
+
+export type AmazonS3Tag = string;
+
+//https://docs.aws.amazon.com/AWSJavaScriptSDK/v3/latest/client/s3/
+//https://docs.aws.amazon.com/AWSJavaScriptSDK/v3/latest/Package/-aws-sdk-s3-presigned-post/
+export class AmazonS3 implements IOss {
+
+    private client;
+    private tag: string;
+    private config: IOssConfig;
+
+    constructor(tag: AmazonS3Tag, config: IOssConfig) {
+        this.tag = tag;
+        this.config = config;
+        // this.client = new OSS({
+        //     endpoint: config.end_point,
+        //     accessKeyId: config.access_key,
+        //     accessKeySecret: config.secret_key,
+        //     region: config.region,
+        //     authorizationV4: true,
+        // })
+        this.client = new S3Client({
+            // region: this.config.region,
+            endpoint: this.config.end_point,
+            // credentials: {
+            //     accessKeyId: this.config.access_key,
+            //     secretAccessKey: this.config.secret_key,
+            // },
+            region: this.config.region,
+            credentials:{
+                accessKeyId: this.config.access_key,
+                secretAccessKey: this.config.secret_key
+            }
+        })
+    }
+
+    get bucket(): string {
+        return this.config.bucket;
+    }
+
+    get url(): string {
+        return `${this.config.use_ssl ? 'https' : 'http'}://${this.config.end_point}:${this.config.port}/`;
+    }
+
+    bucket_exists(bucket: string): Promise<boolean> {
+        return new Promise<boolean>(async (resolve, reject) => {
+            try {
+                const input = {
+                    Bucket: bucket,
+                }
+                const command = new HeadBucketCommand(input);
+                const response = await this.client.send(command);
+                resolve(true);
+            } catch (error: any) {
+                Logger.error(error);
+                // 指定的存储空间不存在。
+                if (error.name === 'NotFound') {
+                    resolve(false);
+                } else {
+                    reject(Resp.gen_err(Resp.InternalServerError, error.message));
+                }
+            }
+        });
+    }
+
+    object_exists(bucket: string, name: string): Promise<boolean> {
+        return new Promise<boolean>(async (resolve, reject) => {
+            try {
+                // const input = {
+                //     Bucket: bucket,
+                // }
+                // const command = new HeadBucketCommand(input);
+                // const response = await this.client.send(command);
+                resolve(true);
+            } catch (error: any) {
+                Logger.error(error);
+                // 指定的存储空间不存在。
+                if (error.name === 'NotFound') {
+                    resolve(false);
+                } else {
+                    reject(Resp.gen_err(Resp.InternalServerError, error.message));
+                }
+            }
+        });
+    }
+
+    make_bucket(bucket: string): Promise<void> {
+        return new Promise<void>(async (resolve, reject) => {
+            try {
+                const input: CreateBucketCommandInput = {
+                    ACL: 'private',
+                    Bucket: bucket,
+                    // CreateBucketConfiguration: {
+                    //     LocationConstraint: this.config.region,
+                    //     // Location: {
+                    //     //
+                    //     // }
+                    // }
+
+                }
+                // await this.client.putBucket(bucket, options);
+                const command = new CreateBucketCommand(input);
+                const response = await this.client.send(command);
+                Logger.debug(response);
+                resolve();
+            } catch (err: any) {
+                Logger.error(err);
+                reject(Resp.gen_err(Resp.InternalServerError, err.message));
+            }
+        });
+    }
+
+    put_object(bucket: string, objectName: string, buffer: Stream | Buffer, size: number, contentType?: string): Promise<string> {
+        return new Promise<string>(async (resolve, reject) => {
+            try {
+                let options;
+                if (contentType) {
+                    options = {
+                        headers: {
+                            'content-type': contentType
+                        }
+                    }
+                }
+                // let client = new OSS({
+                //     bucket: bucket,
+                //     endpoint: this.config.end_point,
+                //     accessKeyId: this.config.access_key,
+                //     accessKeySecret: this.config.secret_key,
+                //     authorizationV4: true,
+                // });
+                // Logger.info({
+                //     bucket: bucket,
+                //     endpoint: this.config.end_point,
+                //     accessKeyId: this.config.access_key,
+                //     accessKeySecret: this.config.secret_key
+                // });
+                // let etag = await client.put(objectName, buffer, options);
+                resolve('etag');
+            } catch (e: any) {
+                Logger.error(e);
+                reject(Resp.gen_err(Resp.InternalServerError, `上传图片失败, ${e.message}`));
+            }
+        });
+    }
+
+    remove_object(bucket: string, objectName: string): Promise<void> {
+        return new Promise<void>(async (resolve, reject) => {
+            try {
+                // let client = new OSS({
+                //     bucket: bucket,
+                //     endpoint: this.config.end_point,
+                //     accessKeyId: this.config.access_key,
+                //     accessKeySecret: this.config.secret_key,
+                //     authorizationV4: true,
+                // });
+                // await client.delete(objectName);
+                resolve();
+            } catch (e: any) {
+                reject(Resp.gen_err(Resp.InternalServerError, `删除对象出错, ${e.message}`));
+            }
+        })
+    }
+
+    copy_object(bucket: string, src_object_name: string, dest_object_name: string): Promise<void> {
+        return new Promise<void>(async (resolve, reject) => {
+            try {
+                // let client = new OSS({
+                //     bucket: bucket,
+                //     endpoint: this.config.end_point,
+                //     accessKeyId: this.config.access_key,
+                //     accessKeySecret: this.config.secret_key,
+                //     authorizationV4: true,
+                //
+                // });
+                // await client.copy(dest_object_name, src_object_name);
+                resolve();
+            } catch (e: any) {
+                reject(Resp.gen_err(Resp.InternalServerError, `复制对象出错, ${e.message}`));
+            }
+        })
+    }
+
+    presigned_url(bucket: string, objectName: string, expiry: number, filename?: string): Promise<string> {
+        return new Promise<string>(async (resolve, reject) => {
+            try {
+                // let client = new OSS({
+                //     bucket: bucket,
+                //     endpoint: this.config.end_point,
+                //     accessKeyId: this.config.access_key,
+                //     accessKeySecret: this.config.secret_key,
+                //     authorizationV4: true,
+                // });
+                // let url = client.signatureUrl(objectName, {expires: expiry});
+
+                resolve('url');
+            } catch (e: any) {
+                reject(Resp.gen_err(Resp.InternalServerError, `获取图片url出错, ${e.message}`));
+            }
+        })
+    }
+
+    //https://help.aliyun.com/zh/oss/use-cases/node-js?spm=a2c4g.11186623.0.0.53324cf5Ov5aix
+    //https://help.aliyun.com/zh/oss/use-cases/uploading-objects-to-oss-directly-from-clients/?spm=a2c4g.11186623.0.i1#36c322a137x9q
+    presignedPostPolicy(bucket: string, objectName: string, expiry: number, prefix?: string): Promise<any> {
+        return new Promise<any>(async (resolve, reject) => {
+            try {
+
+                resolve({
+                    postURL: 'url',
+                    headers: {
+                        method: 'PUT',
+                        "Content-Type": "application/octet-stream",
+                    },
+                    // formData
+                });
+            } catch (e: any) {
+                reject(Resp.gen_err(Resp.InternalServerError, `获取oss上传url出错, ${e.message}`));
+            }
+        });
+    }
+
+    listenBucketNotification(bucket: string, prefix: string, suffix: string, events: Array<string>, cb: any) {
+    }
+
+    setBucketLifecycle(bucket: string, id: string, enabled: boolean, expiration: number): Promise<void>{
+        return new Promise<void>(async (resolve, _reject) => {
+            return resolve();
+        });
+    }
+
+    stat_object(bucket: string, object_name: string): Promise<IObjectStat | undefined> {
+        return new Promise<IObjectStat | undefined>(async (resolve, _reject) => {
+            return resolve(undefined);
+        });
+    }
+}

+ 3 - 1
base-lib/src/util/Config.ts

@@ -75,17 +75,19 @@ export interface IMqttConfig {
     password: string;
 }
 
-export type OssType = 'Minio' | 'AliOss';
+export type OssType = 'Minio' | 'AliOss' | 'AmazonS3';
 
 export interface IOssConfig {
     id: string;
     type: OssType;
+    region?: string;
     end_point: string;
     port?: number | undefined;
     use_ssl?: boolean;
     bucket: string;
     access_key: string;
     secret_key: string;
+    callback_url?: string;
 }
 
 // websocket服务配置

+ 10 - 2
base-lib/src/util/Executor.ts

@@ -104,7 +104,7 @@ export interface ISandboxData {
 
 export function evaluate(code: string, sandbox_data?: ISandboxData): any {
     let sandbox = SharedContext.data;
-    const fn = new Function('sandbox', `with(sandbox){ \n return eval(\`(()=>{let __$$ = ${code}; \n return __$$;})()\`); \n}`);
+    const fn = new Function('sandbox', `with(sandbox){debugger; \n return  eval(\`(async ()=>{let __$$ = ${code}; \n return __$$;})()\`); \n}`);
     // const fn = new Function('sandbox', `with(sandbox){let xxxx = \`(()=>{let x = ${code}; \n return x;})()\`;\n debugger; \n return eval(\`(()=>{\n debugger; \nlet x = ${code}; \n console.log(typeof ${code});\n debugger; \n return x;})()\`); \n}`);
     const proxy = new Proxy(sandbox, {
         has(_target, _key) {
@@ -131,6 +131,10 @@ export function evaluate(code: string, sandbox_data?: ISandboxData): any {
                     } else if (typeof result === 'function') {
                         result = result();
                     }
+
+                    // if (result instanceof Promise) {
+                    //     result = await result;
+                    // }
                 }
                 return result;
             }
@@ -140,7 +144,7 @@ export function evaluate(code: string, sandbox_data?: ISandboxData): any {
 }
 
 
-export function executor(expression: any, data?: ISandboxData): any {
+export async function executor(expression: any, data?: ISandboxData): Promise<any> {
     let result;
     if (typeof expression === 'string') {
         result = evaluate(expression, data);
@@ -151,5 +155,9 @@ export function executor(expression: any, data?: ISandboxData): any {
     } else {
         result = evaluate(JSON.stringify(_.cloneDeep(expression)), data);
     }
+
+    if (result instanceof Promise) {
+        result = await result;
+    }
     return result;
 }

+ 33 - 12
base-lib/src/util/Minio.ts

@@ -21,6 +21,7 @@ export class MinioClient implements IOss {
             accessKey: config.access_key,
             secretKey: config.secret_key,
             useSSL: config.use_ssl,
+            region: config.region
         });
     }
 
@@ -45,11 +46,25 @@ export class MinioClient implements IOss {
         })
     }
 
+    object_exists(bucket: string, name: string): Promise<boolean> {
+        return new Promise<boolean>(async (resolve, reject) => {
+            //TODO: 修改
+            this.client.bucketExists(bucket, (err: Error | null, exists: boolean) => {
+                if (err) {
+                    Logger.error(err);
+                    return reject(Resp.gen_err(Resp.InternalServerError, 'bucketExists error: ' + err.message))
+                }
+
+                return resolve(exists);
+            })
+        })
+    }
+
     make_bucket(bucket: string): Promise<void> {
         return new Promise<void>(async (resolve, reject) => {
-            this.client.makeBucket(bucket, 'us-east-1', (err: Error | null) => {
+            this.client.makeBucket(bucket, this.config.region? this.config.region: 'us-east-1', (err) => {
                 if (err) {
-                    return reject(Resp.gen_err(Resp.InternalServerError, 'makeBucket error: ' + err.message))
+                    return reject(Resp.gen_err(Resp.InternalServerError, 'makeBucket error: ' + err))
                 }
                 resolve();
             })
@@ -115,21 +130,27 @@ export class MinioClient implements IOss {
 
     presignedPostPolicy(bucket: string, objectName: string, expiry: number): Promise<any> {
         return new Promise<any>(async (resolve, _reject) => {
-            let policy = this.client.newPostPolicy();
-            policy.setBucket(bucket);
-            policy.setKey(objectName);
-            let expires = new Date;
-            expires.setSeconds(expiry);
-            policy.setExpires(expires);
-            let result = await this.client.presignedPostPolicy(policy);
-            console.log(result);
-            resolve(result);
+            try{
+                let policy = this.client.newPostPolicy();
+                policy.setBucket(bucket);
+                policy.setKey(objectName);
+                let expires = new Date;
+                expires.setSeconds(expiry);
+                policy.setExpires(expires);
+                let result = await this.client.presignedPostPolicy(policy);
+                console.log(result);
+                resolve(result);
+            } catch (e) {
+                resolve(e);
+            }
+
+
         })
     }
 
 
     listenBucketNotification(bucket: string, prefix: string, suffix: string, events: Array<string>, cb: any) {
-        let listener = this.client.listenBucketNotification(bucket, prefix, suffix, events);
+        let listener: any = this.client.listenBucketNotification(bucket, prefix, suffix, events);
         listener.on('notification', function (record: any) {
             console.log(record);
             cb({

+ 13 - 3
base-lib/src/util/Oss.ts

@@ -4,6 +4,7 @@ import {MinioClient} from "@util/Minio";
 import {AliOss} from "@util/AliOss";
 import {IOssConfig} from "@util/Config";
 import {Logger} from "@util/Logger";
+import {AmazonS3} from "@util/AmazonS3";
 
 export interface IObjectStat {
     size: number;
@@ -21,6 +22,9 @@ export class Oss {
                     case "AliOss":
                         instance = new AliOss(id, config);
                         break;
+                    case "AmazonS3":
+                        instance = new AmazonS3(id, config);
+                        break;
                     case "Minio":
                     default:
                         instance = new MinioClient(id, config);
@@ -28,10 +32,15 @@ export class Oss {
                 }
                 Oss.instance_map.set(id, instance);
                 let bucket = instance.bucket;
-                let exists = await instance.bucket_exists(bucket);
-                if (!exists) {
-                    await instance.make_bucket(bucket);
+                try{
+                    let exists = await instance.bucket_exists(bucket);
+                    if (!exists) {
+                        await instance.make_bucket(bucket);
+                    }
+                } catch (e) {
+                    console.log(e);
                 }
+
                 return instance;
             }
             throw Resp.gen_err(Resp.InternalServerError,'oss初始化失败');
@@ -54,6 +63,7 @@ export interface IOss {
     bucket: string;
     url: string;
     bucket_exists(bucket: string): Promise<boolean>;
+    object_exists(bucket: string, name: string): Promise<boolean>;
     make_bucket(bucket: string): Promise<void>;
     put_object(bucket: string, object_name: string, buffer: Stream | Buffer, size: number, content_type?: string): Promise<string>;
     remove_object(bucket: string, object_name: string): Promise<void>;

+ 8 - 2
base-lib/src/util/Resp.ts

@@ -80,8 +80,8 @@ export class Resp {
         return des;
     }
 
-    static respond(api: string, res: Response, result?: any, code?: number) {
-        let json: any = {};
+    static respond(api: string, res: Response, result?: any, code?: number, info?: any) {
+        let json: any = {...info};
         json.api = api;
         json.code = code ? code : Resp.OK.code;
         json.msg = Resp.OK.msg;
@@ -90,6 +90,12 @@ export class Resp {
             json.data = result;
             // Resp.extend(json, result, true);
         }
+        if (info) {
+            json = {
+                ...json,
+                ...info
+            }
+        }
         Logger.info(json);
         res.json(json);
     }

+ 26 - 26
deploy/docker-compose.yml

@@ -10,15 +10,15 @@ services:
       POSTGRES_DB: pmr
       PGDATA: /var/lib/postgresql/data/pgdata
     volumes:
-      - /home/pmr/tsdata:/var/lib/postgresql/data/pgdata
+      - /home/pmr/pmr-backend/tsdata:/var/lib/postgresql/data/pgdata
       - /etc/localtime:/etc/localtime:ro
-      - /home/pmr/share:/mnt/share/
+      - /home/pmr/pmr-backend/share:/mnt/share/
     logging:
       driver: "json-file"
       options:
         max-size: "500m"
     ports:
-      - 25432:5432
+      - 15432:5432
   redis:
     restart: always
     container_name: pmr-redis
@@ -30,23 +30,23 @@ services:
       options:
         max-size: "100m"
     command: redis-server --requirepass hM3QmYwfGFr6Hh3RYY7kMqgW6mJAEde7 --notify-keyspace-events Ex
-  minio:
-    restart: always
-    container_name: pmr-minio
-    image: minio/minio:RELEASE.2021-03-17T02-33-02Z
-    ports:
-      - 29000:9000
-    environment:
-      - MINIO_ROOT_USER=pmr
-      - MINIO_ROOT_PASSWORD=8ufhAdw2WQTBpus8vku6CkZDkncV9x9d
-    volumes:
-      - /home/pmr/minio/minio_data:/data
-      - /home/pmr/minio/certs:/root/.minio/certs
-    logging:
-      driver: "json-file"
-      options:
-        max-size: "500m"
-    command: server /data
+#  minio:
+#    restart: always
+#    container_name: pmr-minio
+#    image: minio/minio:RELEASE.2021-03-17T02-33-02Z
+#    ports:
+#      - 29000:9000
+#    environment:
+#      - MINIO_ROOT_USER=pmr
+#      - MINIO_ROOT_PASSWORD=8ufhAdw2WQTBpus8vku6CkZDkncV9x9d
+#    volumes:
+#      - /home/pmr/minio/minio_data:/data
+#      - /home/pmr/minio/certs:/root/.minio/certs
+#    logging:
+#      driver: "json-file"
+#      options:
+#        max-size: "500m"
+#    command: server /data
   pmr-api-gateway:
     restart: always
     container_name: pmr-api-gateway
@@ -54,9 +54,9 @@ services:
     environment:
       TZ: Asia/Shanghai
     ports:
-      - 21443:443
+      - 1443:443
     volumes:
-      - /home/pmr/pmr-api-gateway:/usr/src/app
+      - /home/pmr/pmr-backend/pmr-api-gateway:/usr/src/app
       - /etc/localtime:/etc/localtime:ro
     logging:
       driver: "json-file"
@@ -73,8 +73,8 @@ services:
     ports:
       - 23443:443
     volumes:
-      - /home/pmr/pmr-access-control:/usr/src/app
-      - /home/pmr/pmr-access-control/fonts:/usr/src/fonts
+      - /home/pmr/pmr-backend/pmr-access-control:/usr/src/app
+      - /home/pmr/pmr-backend/pmr-access-control/fonts:/usr/src/fonts
       - /etc/localtime:/etc/localtime:ro
     logging:
       driver: "json-file"
@@ -85,13 +85,13 @@ services:
   pmr-biz-manager:
     restart: always
     container_name: pmr-biz-manager
-    image: node:14.16.0
+    image: node:21.6.0
     environment:
       TZ: Asia/Shanghai
     ports:
       - 24443:443
     volumes:
-      - /home/pmr/pmr-biz-manager:/usr/src/app
+      - /home/pmr/pmr-backend/pmr-biz-manager:/usr/src/app
       - /etc/localtime:/etc/localtime:ro
     logging:
       driver: "json-file"

+ 1 - 0
pmr-access-control/package.json

@@ -51,6 +51,7 @@
     "path-to-regexp": "^1.7.0",
     "pg": "^8.5.1",
     "pg-hstore": "^2.3.3",
+    "proxy-agent": "^6.4.0",
     "redis": "^3.0.2",
     "sequelize": "^6.21.4",
     "svg-captcha": "^1.4.0",

文件差异内容过多而无法显示
+ 0 - 1
pmr-api-gateway/config.json


+ 1 - 1
pmr-api-gateway/proxy.json

@@ -8,7 +8,7 @@
   },
   {
     "serviceName": "pmr-biz-manager",
-    "path": ["/api/report.*", "/api/prj.*", "/api/biz.*", "/api/cfg.*"],
+    "path": ["/api/report.*", "/api/prj.*", "/api/biz.*", "/api/cfg.*", "/api/callback.*"],
     "protocol": "https",
     "host": "pmr-biz-manager",
     "port": 443

+ 1 - 1
pmr-api-gateway/proxy_dev.json

@@ -8,7 +8,7 @@
   },
   {
     "serviceName": "pmr-biz-manager",
-    "path": ["/api/report.*","/api/prj.*", "/api/biz.*", "/api/cfg.*"],
+    "path": ["/api/report.*","/api/prj.*", "/api/biz.*", "/api/cfg.*", "/api/callback.*"],
     "protocol": "https",
     "host": "localhost",
     "port": 3443

+ 56 - 21
pmr-biz-manager/bpmn/立项流程.bpmn

@@ -47,7 +47,18 @@
 (async function () {
     try {
         let checkers = await this.environment.services.get_handlers('project_checker');
-         this.environment.Logger('work').error(checkers);
+         this.environment.Logger('work').debug(checkers);
+
+        let staffs = await this.environment.services.get_staffs();
+            this.environment.staff_ids = [];
+            this.environment.staff_names = [];
+        if (staffs) {
+            for (let staff of staffs) {
+                this.environment.staff_ids.push(staff.id);
+                this.environment.staff_names.push(staff.name);
+            }
+        }
+        
 
         let result = checkers.length &gt; 0 ? true: false; 
         if (!result) {
@@ -187,7 +198,7 @@
       <bpmn2:incoming>Flow_0tj9nd6</bpmn2:incoming>
       <bpmn2:signalEventDefinition id="SignalEventDefinition_1tt7p06" signalRef="Signal_0hhmd7l" />
     </bpmn2:intermediateThrowEvent>
-    <bpmn2:sequenceFlow id="Flow_02taytj" sourceRef="event_throw_approved" targetRef="Activity_10dmh6u">
+    <bpmn2:sequenceFlow id="Flow_02taytj" sourceRef="event_throw_approved" targetRef="task_set_owner">
       <bpmn2:extensionElements />
     </bpmn2:sequenceFlow>
     <bpmn2:intermediateThrowEvent id="event_throw_approved">
@@ -306,7 +317,7 @@ next();
     </bpmn2:scriptTask>
     <bpmn2:sequenceFlow id="Flow_0if8for" sourceRef="Activity_10dmh6u" targetRef="Event_end" />
     <bpmn2:scriptTask id="Activity_10dmh6u" name="归档审核通过的立项文件" scriptFormat="JavaScript">
-      <bpmn2:incoming>Flow_02taytj</bpmn2:incoming>
+      <bpmn2:incoming>Flow_07g8vvu</bpmn2:incoming>
       <bpmn2:outgoing>Flow_0if8for</bpmn2:outgoing>
       <bpmn2:script>try{
 let files = this.environment.output.task_request.files;
@@ -336,6 +347,23 @@ this.environment.Logger('work').error(e);
 next();      
 }</bpmn2:script>
     </bpmn2:scriptTask>
+    <bpmn2:sequenceFlow id="Flow_07g8vvu" sourceRef="task_set_owner" targetRef="Activity_10dmh6u" />
+    <bpmn2:scriptTask id="task_set_owner" scriptFormat="JavaScript">
+      <bpmn2:documentation>设置项目负责人</bpmn2:documentation>
+      <bpmn2:incoming>Flow_02taytj</bpmn2:incoming>
+      <bpmn2:outgoing>Flow_07g8vvu</bpmn2:outgoing>
+      <bpmn2:script>
+(async function () {
+        // 从审核时提交的表单中得到项目负责人,并设置
+    try {
+        await this.environment.services.set_leader(this.environment.variables.leader_id); 
+    } catch (e) {
+        this.environment.Logger('work').error(e);
+    }
+    next();
+})(); 
+</bpmn2:script>
+    </bpmn2:scriptTask>
   </bpmn2:process>
   <bpmn2:signal id="Signal_0hhmd7l" name="cancel_sign" />
   <bpmn2:message id="Message_0fh9t3e" name="msg_cancel" />
@@ -361,8 +389,14 @@ next();
         <dc:Bounds x="870" y="80" width="100" height="80" />
         <bpmndi:BPMNLabel />
       </bpmndi:BPMNShape>
+      <bpmndi:BPMNShape id="Event_0mceau4_di" bpmnElement="Event_throw_withdraw">
+        <dc:Bounds x="1112" y="102" width="36" height="36" />
+        <bpmndi:BPMNLabel>
+          <dc:Bounds x="1108" y="78" width="44" height="14" />
+        </bpmndi:BPMNLabel>
+      </bpmndi:BPMNShape>
       <bpmndi:BPMNShape id="Event_0tntlum_di" bpmnElement="event_throw_approved">
-        <dc:Bounds x="1242" y="302" width="36" height="36" />
+        <dc:Bounds x="1202" y="302" width="36" height="36" />
         <bpmndi:BPMNLabel>
           <dc:Bounds x="1233" y="235" width="55" height="14" />
         </bpmndi:BPMNLabel>
@@ -381,7 +415,7 @@ next();
         <dc:Bounds x="490" y="410" width="100" height="80" />
       </bpmndi:BPMNShape>
       <bpmndi:BPMNShape id="Event_1jljt88_di" bpmnElement="Event_end">
-        <dc:Bounds x="1442" y="302" width="36" height="36" />
+        <dc:Bounds x="1632" y="302" width="36" height="36" />
       </bpmndi:BPMNShape>
       <bpmndi:BPMNShape id="Activity_0kc4efu_di" bpmnElement="Activity_16s6waw">
         <dc:Bounds x="510" y="190" width="100" height="80" />
@@ -400,18 +434,15 @@ next();
         <bpmndi:BPMNLabel />
       </bpmndi:BPMNShape>
       <bpmndi:BPMNShape id="Activity_04s4p12_di" bpmnElement="Activity_10dmh6u">
-        <dc:Bounds x="1310" y="280" width="100" height="80" />
-      </bpmndi:BPMNShape>
-      <bpmndi:BPMNShape id="Event_0mceau4_di" bpmnElement="Event_throw_withdraw">
-        <dc:Bounds x="1112" y="102" width="36" height="36" />
-        <bpmndi:BPMNLabel>
-          <dc:Bounds x="1108" y="78" width="44" height="14" />
-        </bpmndi:BPMNLabel>
+        <dc:Bounds x="1500" y="280" width="100" height="80" />
       </bpmndi:BPMNShape>
       <bpmndi:BPMNShape id="Activity_1k1mf96_di" bpmnElement="Activity_1ga5o6o">
         <dc:Bounds x="770" y="400" width="100" height="80" />
         <bpmndi:BPMNLabel />
       </bpmndi:BPMNShape>
+      <bpmndi:BPMNShape id="Activity_1cu0e92_di" bpmnElement="task_set_owner">
+        <dc:Bounds x="1300" y="280" width="100" height="80" />
+      </bpmndi:BPMNShape>
       <bpmndi:BPMNShape id="Event_06bs6js_di" bpmnElement="event_catch_approved">
         <dc:Bounds x="912" y="142" width="36" height="36" />
         <bpmndi:BPMNLabel>
@@ -438,9 +469,9 @@ next();
       </bpmndi:BPMNEdge>
       <bpmndi:BPMNEdge id="Flow_0zubkyw_di" bpmnElement="Flow_0zubkyw">
         <di:waypoint x="1165" y="320" />
-        <di:waypoint x="1242" y="320" />
+        <di:waypoint x="1202" y="320" />
         <bpmndi:BPMNLabel>
-          <dc:Bounds x="1193" y="302" width="22" height="14" />
+          <dc:Bounds x="1173" y="302" width="22" height="14" />
         </bpmndi:BPMNLabel>
       </bpmndi:BPMNEdge>
       <bpmndi:BPMNEdge id="Flow_0nwaga3_di" bpmnElement="Flow_0nwaga3">
@@ -455,14 +486,14 @@ next();
         <di:waypoint x="1112" y="120" />
       </bpmndi:BPMNEdge>
       <bpmndi:BPMNEdge id="Flow_02taytj_di" bpmnElement="Flow_02taytj">
-        <di:waypoint x="1278" y="320" />
-        <di:waypoint x="1310" y="320" />
+        <di:waypoint x="1238" y="320" />
+        <di:waypoint x="1300" y="320" />
       </bpmndi:BPMNEdge>
       <bpmndi:BPMNEdge id="Flow_1ucg773_di" bpmnElement="Flow_1ucg773">
         <di:waypoint x="930" y="178" />
         <di:waypoint x="930" y="240" />
-        <di:waypoint x="1460" y="240" />
-        <di:waypoint x="1460" y="302" />
+        <di:waypoint x="1650" y="240" />
+        <di:waypoint x="1650" y="302" />
       </bpmndi:BPMNEdge>
       <bpmndi:BPMNEdge id="Flow_1t45qnv_di" bpmnElement="Flow_1t45qnv">
         <di:waypoint x="540" y="410" />
@@ -522,13 +553,17 @@ next();
         <di:waypoint x="590" y="450" />
       </bpmndi:BPMNEdge>
       <bpmndi:BPMNEdge id="Flow_0if8for_di" bpmnElement="Flow_0if8for">
-        <di:waypoint x="1410" y="320" />
-        <di:waypoint x="1442" y="320" />
+        <di:waypoint x="1600" y="320" />
+        <di:waypoint x="1632" y="320" />
       </bpmndi:BPMNEdge>
       <bpmndi:BPMNEdge id="Flow_0ipuk5f_di" bpmnElement="Flow_0ipuk5f">
         <di:waypoint x="770" y="440" />
         <di:waypoint x="590" y="440" />
       </bpmndi:BPMNEdge>
+      <bpmndi:BPMNEdge id="Flow_07g8vvu_di" bpmnElement="Flow_07g8vvu">
+        <di:waypoint x="1400" y="320" />
+        <di:waypoint x="1500" y="320" />
+      </bpmndi:BPMNEdge>
     </bpmndi:BPMNPlane>
   </bpmndi:BPMNDiagram>
-</bpmn2:definitions>
+</bpmn2:definitions>

+ 8 - 7
pmr-biz-manager/config.json

@@ -35,13 +35,14 @@
     "oss": [
       {
         "id": "pmr-doc",
-        "type": "Minio",
-        "bucket": "pmr-doc",
-        "end_point": "siot.surkw.com",
-        "port": 29000,
-        "access_key": "pmr",
-        "secret_key": "8ufhAdw2WQTBpus8vku6CkZDkncV9x9d",
-        "use_ssl": true
+        "type": "AliOss",
+        "region": "oss-cn-hangzhou",
+        "bucket": "my-prj",
+        "end_point": "oss-cn-hangzhou.aliyuncs.com",
+        "access_key": "LTAI5tAFC78NqRpj3JEiWQ1M",
+        "secret_key": "2B9Ibh6uq7MVO7f6vhHDqIBmSF26Oe",
+        "use_ssl": true,
+        "callback_url": "https://pmr.surkw.com:1443/api/callback.oss.upload_file"
       }
     ]
   }

+ 25 - 1
pmr-biz-manager/config_dev.json

@@ -31,7 +31,7 @@
     ],
     "oss": [
       {
-        "id": "pmr-doc",
+        "id": "pmr-doc-1",
         "type": "Minio",
         "bucket": "pmr-doc",
         "end_point": "siot.surkw.com",
@@ -39,6 +39,30 @@
         "access_key": "siot",
         "secret_key": "Vnk5eLVAucDAD9xXpJQCLyJF6fq33epa",
         "use_ssl": true
+      },
+      {
+        "id": "pmr-doc",
+        "type": "AliOss",
+        "region": "oss-cn-hangzhou",
+        "bucket1": "myoo-pmr",
+        "bucket": "my-prj",
+        "end_point": "oss-cn-hangzhou.aliyuncs.com",
+        "access_key1": "LTAI5tEN7eu28Sawg6NVgEJr",
+        "secret_key1": "JY9Co7yD18Ew54jPtesk8urFaKoHZ5",
+        "access_key": "LTAI5tAFC78NqRpj3JEiWQ1M",
+        "secret_key": "2B9Ibh6uq7MVO7f6vhHDqIBmSF26Oe",
+        "use_ssl": true,
+        "callback_url": "https://pmr.surkw.com:1443/api/callback.oss.upload_file"
+      },
+      {
+        "id": "pmr-doc22",
+        "type": "AmazonS3",
+        "region": "oss-cn-hangzhou",
+        "bucket": "my-prj",
+        "end_point": "http://oss-cn-hangzhou.aliyuncs.com",
+        "access_key": "LTAI5tAFC78NqRpj3JEiWQ1M",
+        "secret_key": "2B9Ibh6uq7MVO7f6vhHDqIBmSF26Oe",
+        "use_ssl": false
       }
     ]
   }

+ 3 - 0
pmr-biz-manager/package.json

@@ -31,7 +31,9 @@
   },
   "dependencies": {
     "@aircall/expression-parser": "^1.0.4",
+    "@aws-sdk/s3-request-presigner": "^3.608.0",
     "@types/ajv": "^1.0.0",
+    "@types/ali-oss": "^6.16.11",
     "@types/axios": "^0.14.0",
     "@types/compression": "^1.7.0",
     "@types/express": "^4.17.11",
@@ -43,6 +45,7 @@
     "@types/sequelize": "^4.28.20",
     "@types/uuid": "^8.3.0",
     "ajv": "^7.0.3",
+    "ali-oss": "^6.20.0",
     "axios": "^0.21.1",
     "body-parser": "^1.19.0",
     "bpmn-engine": "^19.0.1",

+ 16 - 3
pmr-biz-manager/src/app.ts

@@ -20,6 +20,7 @@ import {PrjPlanTask} from "@core-models/PrjPlanTask";
 import {Op} from "sequelize";
 import {BpmnWork} from "@core-models/BpmnWork";
 import {Config} from "@core-models/Config";
+import {check_uploaded_files_status} from "@src/utils/file_upload_helper";
 
 const schedule = require('node-schedule');
 
@@ -33,6 +34,14 @@ new ServiceApp().start(new MyRouter('@src/routes', guards), Models).then(async (
         if (!await oss.bucket_exists(oss.bucket)) {
             await oss.make_bucket(oss.bucket);
         }
+
+        oss.listenBucketNotification(oss.bucket, '', '*', ['*'],
+            async function (record: any) {
+                Logger.info(record);
+            }
+
+        );
+
         // 侦听oss上传消息,更新喊话日志表中语音是否已上传字段
         oss.listenBucketNotification(oss.bucket, '', '*', ['s3:ObjectCreated:*'],
             async function (record: any) {
@@ -130,6 +139,10 @@ new ServiceApp().start(new MyRouter('@src/routes', guards), Models).then(async (
         }, bpmn_flow_on_end).run(flow.state);
     }
 
+    schedule.scheduleJob('* * * * * *', async function () {
+        let oss = Oss.get_instance('pmr-doc');
+        await check_uploaded_files_status(oss.bucket);
+    });
     // 项目数统计
     schedule.scheduleJob('50 23 * * *', async function () {
         await prj_stat_monthly();
@@ -138,13 +151,13 @@ new ServiceApp().start(new MyRouter('@src/routes', guards), Models).then(async (
     });
 
 
-    schedule.scheduleJob('0 * * * *', async function () {
+    schedule.scheduleJob('*/5 * * * *', async function () {
         let oss = Oss.get_instance('pmr-doc');
         // 清除项目文档记录表中,超过时间仍未上传完成的文档记录
         let records = await PrjFile.findAll({
             where: {
                 req_upload_time: {
-                    [Op.lt]: dayjs().subtract(2, 'hour').format('YYYY-MM-DD HH:mm:ss')
+                    [Op.lt]: dayjs().subtract(1, 'hour').format('YYYY-MM-DD HH:mm:ss')
                 },
                 uploaded: false
             },
@@ -180,7 +193,7 @@ new ServiceApp().start(new MyRouter('@src/routes', guards), Models).then(async (
     });
 
     // 每天8:30检查任务和工作项是否有临近过期的,有的话生成提醒工作
-    schedule.scheduleJob('30 8 * * *', async function () {
+    schedule.scheduleJob('30 7 * * *', async function () {
         let config = await Config.findOne({where: {id: '1'}});
         let task_remind_day_in_advance = config?.config.remind?.task ? config.config.remind.task : 2;
         let work_remind_day_in_advance = config?.config.remind?.work ? config.config.remind.work : 1;

+ 52 - 1
pmr-biz-manager/src/bpmn/flow_engine.ts

@@ -197,6 +197,25 @@ export class FlowEngine extends EventEmitter {
                         name: owner.name
                     };
                 },
+                set_leader: async (value: string) => {
+                    let leader = await AcsUserInfo.findOne({where: {id: value}, raw: true});
+                    if (!leader) return;
+                    let t = await PrjInfo.sequelize!.transaction();
+                    try {
+                        await PrjInfo.update({leader_id: value}, {where: {id: this._prj_id}, returning: false, transaction: t});
+                        await PrjMembers.create({
+                            prj_id: this._prj_id,
+                            member_id: value,
+                            role_id: 'prj_leader',
+                            created_at: dayjs().format('YYYY-MM-DD HH:mm:ss'),
+
+                        }, {transaction: t});
+                        await t.commit();
+                    } catch (e) {
+                        await t.rollback();
+                        Logger.error(e);
+                    }
+                },
                 get_datetime(): string {
                     return dayjs().format('YYYY-MM-DD HH:mm:ss');
                 },
@@ -212,6 +231,35 @@ export class FlowEngine extends EventEmitter {
                 get_handlers: (tag: string) => {
                     return this.get_handlers(tag);
                 },
+                // get_staff_ids: async () => {
+                //     let staffs: Array<AcsUserInfo> = await AcsUserInfo.sequelize!.query(`
+                //     select
+                //         staff.id
+                //     from ${AcsUserInfo.table_name} staff,  ${AcsDomain.table_name} domain, ${AcsUserDomain.table_name} user_domain
+                //     where  staff.id = user_domain.user_id and
+                //             domain.id = user_domain.domain_id and domain.path <@ '1.3.1000'
+                //     order by staff.id
+                // `, {type: QueryTypes.SELECT, raw: true});
+                //     let ids: Array<string> = [];
+                //     if (staffs) {
+                //         for (let staff of staffs) {
+                //             ids.push(staff.id);
+                //         }
+                //     }
+                //
+                //     return ids;
+                // },
+                get_staffs: async () => {
+                    let staffs: any = await AcsUserInfo.sequelize!.query(`
+                    select 
+                        distinct staff.id, staff.name
+                    from ${AcsUserInfo.table_name} staff,  ${AcsDomain.table_name} domain, ${AcsUserDomain.table_name} user_domain
+                    where  staff.id = user_domain.user_id and 
+                            domain.id = user_domain.domain_id and domain.path <@ '1.3.1000' 
+                    order by staff.id
+                `, {type: QueryTypes.SELECT, raw: true});
+                    return staffs;
+                },
                 gen_remind_task: async (assigned_to: string, name: string, detail: string, execution_id?: string) => {
                     await BpmnWork.upsert({
                         id: execution_id ? execution_id : IdGen.id(),
@@ -502,6 +550,9 @@ export class FlowEngine extends EventEmitter {
         }
 
         if (!msg.content || !msg.content.output || !msg.content.output[msg.content.index]) return;
+        for (let key in msg.content.output[msg.content.index]) {
+            activity.environment.variables[key] = msg.content.output[msg.content.index][key];
+        }
         await BpmnWork.update({
                 completed_at: dayjs(),
                 handler: msg.content.output[msg.content.index].user_id,
@@ -592,7 +643,7 @@ export class FlowEngine extends EventEmitter {
                     {
                         id: activity.content.form.id,
                         title: activity.content.form.title,
-                        schema: executor(activity.content.form.schema, {
+                        schema: await executor(activity.content.form.schema, {
                             global: {
                                 ...activity
                             }

+ 36 - 0
pmr-biz-manager/src/routes/api/callback/oss/upload_file.ts

@@ -0,0 +1,36 @@
+import {IApiProcessor, ICachedData, IMethodParams, IRequest} from "@core/Defined";
+import {Logger} from "@util/Logger";
+import {update_file_status} from "@src/utils/file_upload_helper";
+
+
+/*
+{
+    filename: 'project/641f01cf24000000/intermediate/641f4e97d6800000',
+    size: '1004774',
+    mimeType: 'application/pdf'
+}
+ */
+
+function response(json: IRequest, params: IMethodParams, cached_data: ICachedData): Promise<any> {
+    return new Promise<any>(async (resolve, reject) => {
+        Logger.info(json);
+        // let data = <IData>json;
+        await update_file_status(json.filename, Number(json.size));
+        resolve({__info: {Status: 'Ok'}});
+    });
+}
+
+const v1_0: IApiProcessor = {
+    schema: {
+        "type": "object",
+    },
+    method: response,
+    method_params: {},
+    guards: []
+}
+
+
+module.exports = {
+    default: v1_0,
+    v1_0: v1_0
+}

+ 6 - 1
pmr-biz-manager/src/routes/api/prj/contract/upload_doc.ts

@@ -56,7 +56,12 @@ function get_upload_url(json: IRequest, params: IMethodParams, cached_data: ICac
             let oss = Oss.get_instance('pmr-doc');
             let result = await oss.presignedPostPolicy(oss.bucket, object_name, 300);
             await t.commit();
-            resolve({object_name: object_name, url: result.postURL, form_data: result.formData});
+            resolve({
+                object_name: object_name,
+                url: result.postURL,
+                form_data: result.formData,
+                // ...result,
+            });
         } catch (e) {
             await t.rollback();
             reject(Resp.throw_err(e));

+ 18 - 32
pmr-biz-manager/src/routes/api/prj/info/add.ts

@@ -18,7 +18,7 @@ interface IData {
     /**
      * 商务负责人,商务负责人id
      */
-    bizman_id: string;
+    // bizman_id: string;
     /**
      * 审核组成员列表,必须有至少一个
      */
@@ -42,7 +42,8 @@ interface IData {
     /**
      * 项目负责人,项目负责人账号id
      */
-    leader_id: string;
+    // leader_id: string;
+    /**
     /**
      * 项目名称,项目名称
      */
@@ -61,9 +62,6 @@ interface IData {
     type_id: number;
 }
 
-
-
-
 function add(json: IRequest, params: IMethodParams, cached_data: ICachedData): Promise<WhereOptions> {
     return new Promise<WhereOptions>(async (resolve, reject) => {
         let t = await PrjInfo.sequelize!.transaction();
@@ -71,12 +69,12 @@ function add(json: IRequest, params: IMethodParams, cached_data: ICachedData): P
             let data = <IData>json.data;
             if (data.checker_ids.length === 0)
                 throw Resp.gen_err(Resp.ParamsError, '审核组成员列表不能为空');
-            let leader = await AcsUserInfo.findOne({where: {id: data.leader_id}, transaction: t});
-            if (!leader) throw Resp.gen_err(Resp.InternalServerError, '项目负责人数据缺失!');
-            // 如果创建人不是特权人员,也不是项目负责人,则不允许创建
-            if (leader.id !== cached_data.user_id && !await is_project_privileged_account(cached_data.user_id)) {
-                throw Resp.gen_err(Resp.Forbidden, '非特权账号只能创建项目负责人是自己的项目!');
-            }
+            let bizman = await AcsUserInfo.findOne({where: {id: cached_data.user_id}, transaction: t});
+            if (!bizman) throw Resp.gen_err(Resp.InternalServerError, '项目商务负责人数据缺失!');
+            // // 如果创建人不是特权人员,也不是项目负责人,则不允许创建
+            // if (leader.id !== cached_data.user_id && !await is_project_privileged_account(cached_data.user_id)) {
+            //     throw Resp.gen_err(Resp.Forbidden, '非特权账号只能创建项目负责人是自己的项目!');
+            // }
 
             let checkers: IHandler[] = [];
             for (let checker of data.checker_ids) {
@@ -98,7 +96,8 @@ function add(json: IRequest, params: IMethodParams, cached_data: ICachedData): P
                 flow_case_id: flow_case_id,
                 ...data,
                 checker_ids: undefined,
-                checkers: checkers
+                checkers: checkers,
+                bizman_id: cached_data.user_id,
             }
             value = DataCURD.filter_null(value);
             // 创建项目
@@ -106,8 +105,8 @@ function add(json: IRequest, params: IMethodParams, cached_data: ICachedData): P
             // 创建项目leader
             await PrjMembers.create({
                 prj_id: id,
-                member_id: data.leader_id,
-                role_id: 'prj_leader',
+                member_id: cached_data.user_id,
+                role_id: 'prj_bizman',
                 created_at: dayjs().format('YYYY-MM-DD HH:mm:ss'),
 
             }, {transaction: t});
@@ -116,20 +115,21 @@ function add(json: IRequest, params: IMethodParams, cached_data: ICachedData): P
             // 启动工作流
             let bpmn = await BpmnModel.findOne({where: {id: 'prj'}, transaction: t});
             if (!bpmn) throw Resp.gen_err(Resp.InternalServerError, '项目流程模型数据缺失!');
-            let flow = new FlowEngine(bpmn.id, bpmn.content, {prj_id: id, owner: data.leader_id}, bpmn_flow_on_end);
+            let flow = new FlowEngine(bpmn.id, bpmn.content, {prj_id: id, owner: cached_data.user_id}, bpmn_flow_on_end);
 
             await BpmnCase.create({
                 id: flow.id,
                 model_id: bpmn.id,
                 model: bpmn.content,
                 started_at: dayjs(),
-                creator_id: data.leader_id,
+                creator_id: cached_data.user_id,
+
                 prj_id: id,
                 // state: await flow.state(),
             }, {transaction: t});
             await PrjLogger.created({
-                creator_id: data.leader_id,
-                creator_name: leader.name,
+                creator_id: cached_data.user_id,
+                creator_name: bizman.name,
                 content: `项目`,
                 prj_id: id,
                 t: t as Transaction
@@ -161,11 +161,6 @@ const v1_0: IApiProcessor = {
                         "title": "项目类型",
                         "description": "项目类型id"
                     },
-                    "leader_id": {
-                        "type": "string",
-                        "title": "项目负责人",
-                        "description": "项目负责人账号id"
-                    },
                     "region_id": {
                         "type": "integer",
                         "title": "所在地区",
@@ -186,11 +181,6 @@ const v1_0: IApiProcessor = {
                         "description": "预计交付时间(YYYY-MM-DD)",
                         "title": "预计交付时间"
                     },
-                    "bizman_id": {
-                        "type": "string",
-                        "description": "商务负责人id",
-                        "title": "商务负责人"
-                    },
                     "intro": {
                         "type": "string",
                         "description": "项目简介",
@@ -218,8 +208,6 @@ const v1_0: IApiProcessor = {
                     "contract_id",
                     "template_id",
                     "deliver_at",
-                    "leader_id",
-                    "bizman_id",
                     "checker_ids",
                     "intro"
                 ],
@@ -227,8 +215,6 @@ const v1_0: IApiProcessor = {
                 "required": [
                     "name",
                     "region_id",
-                    "leader_id",
-                    "bizman_id",
                     "type_id",
                     "deliver_at",
                     "checker_ids"

+ 2 - 2
pmr-biz-manager/src/routes/api/prj/info/remove.ts

@@ -6,7 +6,7 @@ import {PrjPhaseDefine} from "@core-models/PrjPhaseDefine";
 import {BpmnCase} from "@core-models/BpmnCase";
 import {BpmnWork} from "@core-models/BpmnWork";
 import {FlowEngine} from "@src/bpmn/flow_engine";
-import {is_project_modifiable} from "@src/utils/prj_premission_helper";
+import {is_project_removable} from "@src/utils/prj_premission_helper";
 import {PrjFile} from "@core-models/PrjFile";
 
 interface IData {
@@ -22,7 +22,7 @@ export function statusGuard(json: IRequest, cached_data: ICachedData): Promise<v
         if (user === ADMINISTRATOR) return resolve();
         let prj_info = await PrjInfo.findOne({where: {id: data.id}, raw: true});
         if (!prj_info) return reject(Resp.gen_err(Resp.ResourceNotFound));
-        if (!await is_project_modifiable(cached_data.user_id, prj_info.id))
+        if (!await is_project_removable(cached_data.user_id, prj_info.id))
             return reject(Resp.gen_err(Resp.Forbidden, '您没有权限删除该项目,请确认您是项目负责人或特权人员。'));
         let phase = await PrjPhaseDefine.findOne({where: {id: prj_info.phase_id}, raw: true});
         if (!phase) return reject(Resp.gen_err(Resp.ResourceNotFound, '项目状态信息出错,id: ' + prj_info.id));

+ 2 - 1
pmr-biz-manager/src/routes/api/prj/plan/add_task.ts

@@ -60,6 +60,7 @@ function guard(json: IRequest, cached_data: ICachedData): Promise<void> {
 
         let prj_info = await PrjInfo.findOne({where: {id: data.prj_id}, raw: true});
         if (!prj_info) return reject(Resp.gen_err(Resp.ResourceNotFound));
+        cached_data.prj_leader_id = prj_info.leader_id;
 
         data.begin_at += ' 00:00:00';
         data.end_at += ' 23:59:59';
@@ -174,7 +175,7 @@ function add(json: IRequest, params: IMethodParams, cached_data: ICachedData): P
                 if (!bpmn) throw Resp.gen_err(Resp.InternalServerError, '项目流程模型数据缺失!');
                 let flow = new FlowEngine(bpmn.id, bpmn.content, {
                     prj_id: data.prj_id,
-                    owner: cached_data.user_id
+                    owner: cached_data.prj_leader_id
                 }, bpmn_flow_on_end);
                 await flow.run();
                 await BpmnCase.create({

+ 5 - 1
pmr-biz-manager/src/routes/api/prj/week_report/upload_file.ts

@@ -44,7 +44,11 @@ function get_upload_url(json: IRequest, params: IMethodParams, cached_data: ICac
             let oss = Oss.get_instance('pmr-doc');
             let result = await oss.presignedPostPolicy(oss.bucket, object_name, 300);
             await t.commit();
-            resolve({object_name: object_name, url: result.postURL, form_data: result.formData});
+            resolve({
+                object_name: object_name,
+                url: result.postURL,
+                // ...result,
+                form_data: result.formData});
         } catch (e) {
             await t.rollback();
             reject(Resp.throw_err(e));

+ 3 - 1
pmr-biz-manager/src/routes/api/prj/work/upload_file.ts

@@ -46,7 +46,9 @@ function get_upload_url(json: IRequest, params: IMethodParams, cached_data: ICac
             let oss = Oss.get_instance('pmr-doc');
             let result = await oss.presignedPostPolicy(oss.bucket, object_name, 300);
             await t.commit();
-            resolve({object_name: object_name, url: result.postURL, form_data: result.formData});
+            resolve({object_name: object_name, url: result.postURL, form_data: result.formData,
+                // ...result
+            });
         } catch (e) {
             await t.rollback();
             reject(Resp.throw_err(e));

+ 102 - 0
pmr-biz-manager/src/utils/file_upload_helper.ts

@@ -0,0 +1,102 @@
+import {Logger} from "@util/Logger";
+import {PrjFile} from "@core-models/PrjFile";
+import {PrjTaskOutcome} from "@core-models/PrjTaskOutcome";
+import {OutcomeStatus} from "@src/utils/define";
+import dayjs from "dayjs";
+import {BizContractInfo} from "@core-models/BizContractInfo";
+import {Op} from "sequelize";
+import {Oss} from "@util/Oss";
+
+
+export async function check_uploaded_files_status(bucket: string) {
+    let records = await PrjFile.findAll({
+        where: {
+            uploaded: false
+        },
+        raw: true
+    });
+    let oss = Oss.get_instance('pmr-doc');
+    for (let record of records) {
+        try {
+            let file_stat = await oss.stat_object(bucket, record.id);
+            if (file_stat)
+                await update_file_status(record.id, file_stat.size);
+            // Logger.debug(file_stat);
+        } catch (e) {
+            // Logger.error(e);
+        }
+
+    }
+}
+
+export async function update_file_status(object_name: string, size: number) {
+    // Logger.info(record);
+    let path = object_name.split('/');
+    let t = await PrjFile.sequelize!.transaction();
+    try {
+        switch (path[0]) {
+            case 'project':
+                switch (path[2]) {
+                    case 'outcome':
+                        let outcome_id = path[3];
+                        if (outcome_id) {
+                            await PrjTaskOutcome.update({status: OutcomeStatus.Uploaded, uploaded: true}, {
+                                where: {id: outcome_id},
+                                returning: false,
+                                transaction: t
+                            });
+                        }
+                        break;
+                    case 'week_report':
+                        break;
+                }
+                await PrjFile.update({
+                    uploaded_at: dayjs().format('YYYY-MM-DD HH:mm:ss'),
+                    uploaded: true,
+                    size: size
+                }, {where: {id: object_name}, transaction: t, returning: false})
+                break;
+            case 'contract':
+                let contract_id = path[1];
+                if (!contract_id) break;
+                switch (path[2]) { // 附件类型
+                    case '10': //打印件
+                        await BizContractInfo.update({
+                            doc_uploaded: true,
+                            doc_uploaded_at: dayjs(),
+                            doc_size: size
+                        }, {
+                            where: {id: contract_id},
+                            transaction: t,
+                            returning: false
+                        })
+                        break;
+                    case '20':  // 扫描件
+                        await BizContractInfo.update({
+                            pdf_uploaded: true,
+                            pdf_uploaded_at: dayjs(),
+                            pdf_size: size
+                        }, {
+                            where: {id: contract_id},
+                            transaction: t,
+                            returning: false
+                        })
+                        break;
+                }
+                break;
+            default:
+
+                break;
+        }
+        // await PrjFile.update({
+        //     uploaded_at: dayjs(),
+        //     uploaded: true,
+        //     size: record.size
+        // }, {where: {id: record.object}})
+        await t.commit();
+    } catch (e) {
+        await t.rollback();
+        Logger.error(e);
+    }
+
+}

+ 16 - 0
pmr-biz-manager/src/utils/prj_premission_helper.ts

@@ -123,6 +123,21 @@ export async function is_project_document_creator(account_id: string, document_i
     return false;
 }
 
+/// 是否有删除项目信息的权限,注意这里没有检查项目阶段,由其它地方检查。
+export async function is_project_removable(account_id: string, project_id: string): Promise<boolean> {
+    if (
+        await is_project_business_leader(account_id, project_id) ||
+        await is_project_privileged_account(account_id)
+    ) {
+        Logger.trace(`${account_id} 是权修改项目 ${project_id} 的信息。`);
+        return true;
+    }
+
+
+    Logger.trace(`${account_id} 没有权限修改项目 ${project_id} 的信息。`);
+    return false;
+}
+
 /// 是否有修改项目信息的权限
 export async function is_project_modifiable(account_id: string, project_id: string): Promise<boolean> {
     if (
@@ -133,6 +148,7 @@ export async function is_project_modifiable(account_id: string, project_id: stri
         return true;
     }
 
+
     Logger.trace(`${account_id} 没有权限修改项目 ${project_id} 的信息。`);
     return false;
 }

部分文件因为文件数量过多而无法显示