ソースを参照

feature:
1. 添加了对文档的改名、复制和归档功能
2. 对文档的上传、下载、删除等添加了权限控制

eyes4 5 ヶ月 前
コミット
fed5b0fa08

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

@@ -201,6 +201,8 @@ export class AcsFunction extends Model {
             (2161, 309, 'upload', ' 添加项目文档', 'prj.doc.upload', true, 1),
             (2162, 309, 'download', '下载项目文档', 'prj.doc.download', true, 0),
             (2163, 309, 'remove', '删除项目文档', 'prj.doc.remove', true, 1),
+            (2164, 309, 'detail', '获取项目文档详情', 'prj.doc.detail', true, 0),
+            (2165, 309, 'rename', '修改项目文档名', 'prj.doc.rename', true, 1),
             
             (2180, 310, 'milestone', '查看项目里程碑', 'prj.kanban.milestone', true, 0),
             

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

@@ -180,6 +180,8 @@ export class AcsRoleFunction extends Model {
             ('admin', 2161),
             ('admin', 2162),
             ('admin', 2163),
+            ('admin', 2164),
+            ('admin', 2165),
             
             ('admin', 2180),
             

+ 10 - 1
base-lib/src/core-models/PrjFile.ts

@@ -15,8 +15,11 @@ export class PrjFile extends Model {
     declare filename: string;
     declare uploaded_at: string;
     declare dependent_id: string;
+    declare creator_id: string;
     declare uploaded: boolean;
     declare size: number;
+    declare archived: boolean;
+    declare req_upload_time: string;
 
     static table_name = 'tb_prj_file';
 
@@ -89,6 +92,12 @@ export class PrjFile extends Model {
             allowNull: true,
             comment: '文件大小(Bytes)',
             defaultValue: 0
+        },
+        archived: {
+            type: DataTypes.BOOLEAN,
+            allowNull: false,
+            defaultValue: false,
+            comment: '是否已归档'
         }
     }
 
@@ -96,7 +105,7 @@ export class PrjFile extends Model {
 
     ]
 
-    static associate(sequelize: Sequelize) {
+    static associate(_sequelize: Sequelize) {
 
     }
 

+ 20 - 2
base-lib/src/util/AliOss.ts

@@ -117,6 +117,24 @@ export class AliOss implements IOss {
         })
     }
 
+    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
+
+                });
+                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 {
@@ -155,13 +173,13 @@ export class AliOss implements IOss {
     }
 
     setBucketLifecycle(bucket: string, id: string, enabled: boolean, expiration: number): Promise<void>{
-        return new Promise<void>(async (resolve, reject) => {
+        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 new Promise<IObjectStat | undefined>(async (resolve, _reject) => {
             return resolve(undefined);
         });
     }

+ 16 - 3
base-lib/src/util/Minio.ts

@@ -79,10 +79,23 @@ export class MinioClient implements IOss {
         })
     }
 
+    copy_object(bucket: string, src_object_name: string, dest_object_name: string): Promise<void> {
+        return new Promise<void>(async (resolve, reject) => {
+            try {
+                const conds = new Minio.CopyConditions();
+                // conds.setMatchETag(IdGen.id());
+                await this.client.copyObject(bucket, dest_object_name, `${bucket}/${src_object_name}`, conds);
+                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 reqParams;
+                // let reqParams;
                 let url;
                 // if (filename) {
                 //     reqParams = {
@@ -101,7 +114,7 @@ export class MinioClient implements IOss {
     }
 
     presignedPostPolicy(bucket: string, objectName: string, expiry: number): Promise<any> {
-        return new Promise<any>(async (resolve, reject) => {
+        return new Promise<any>(async (resolve, _reject) => {
             let policy = this.client.newPostPolicy();
             policy.setBucket(bucket);
             policy.setKey(objectName);
@@ -157,7 +170,7 @@ export class MinioClient implements IOss {
     }
 
     stat_object(bucket: string, object_name: string): Promise<IObjectStat | undefined> {
-        return new Promise<IObjectStat | undefined>(async (resolve, reject) => {
+        return new Promise<IObjectStat | undefined>(async (resolve, _reject) => {
             try {
                 let stat = await this.client.statObject(bucket, object_name);
                 return resolve(stat);

+ 1 - 0
base-lib/src/util/Oss.ts

@@ -57,6 +57,7 @@ export interface IOss {
     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>;
+    copy_object(bucket: string, src_object_name: string, dest_object_name: string): Promise<void>;
     presigned_url(bucket: string, object_name: string, expiry: number, filename?: string): Promise<string>;
     presignedPostPolicy(bucket: string, objectName: string, expiry: number): Promise<any>;
     listenBucketNotification(bucket: string, prefix: string, suffix: string, events: Array<string>, cb: any): void;

+ 108 - 0
pmr-biz-manager/src/routes/api/prj/doc/archive.ts

@@ -0,0 +1,108 @@
+import {IApiProcessor, ICachedData, IMethodParams, IRequest} from "@core/Defined";
+import {Resp} from "@util/Resp";
+import {PrjFile} from "@core-models/PrjFile";
+import {is_project_document_archivable} from "@src/utils/prj_premission_helper";
+
+interface IData {
+    /**
+     * 文档id
+     */
+    id: string;
+    /**
+     * 是否归档
+     */
+    archived: boolean;
+}
+
+
+function archive(json: IRequest, params: IMethodParams, cached_data: ICachedData): Promise<any> {
+    return new Promise(async (resolve, reject) => {
+        try {
+            let data = <IData>json.data;
+            let file = await PrjFile.findOne({where: {id: data.id}, raw: true});
+            if (!file) throw Resp.gen_err(Resp.ResourceNotFound, `文档(id:${data.id})不存在。`);
+
+            // 检查权限
+            if (!await is_project_document_archivable(cached_data.user_id, file.prj_id)) {
+                return reject(Resp.gen_err(Resp.Forbidden, '您没有权限对此项目的文档进行归档,请确认您是项目负责人或特权人员。'));
+            }
+            await PrjFile.update({archived: data.archived}, {where: {id: data.id}, returning: false});
+            resolve({});
+        } catch (e) {
+            reject(Resp.throw_err(e));
+        }
+    });
+}
+
+const v1_0: IApiProcessor = {
+    schema: {
+        "type": "object",
+        "properties": {
+            "data": {
+                "type": "object",
+                "properties": {
+                    "id": {
+                        "type": "string",
+                        "title": "文档id"
+                    },
+                    "archive": {
+                        "type": "boolean",
+                        "title": "是否归档"
+                    }
+                },
+                "x-apifox-orders": [
+                    "id",
+                    "archive"
+                ],
+                "title": "请求参数内容",
+                "required": [
+                    "id",
+                    "archive"
+                ]
+            },
+            "ver": {
+                "type": "string",
+                "title": "版本号",
+                "description": "文档中没有说明时,默认为1.0",
+                "default": "1.0"
+            },
+            "app_id": {
+                "type": "string",
+                "title": "应用id",
+                "description": "平台分配给客户端的app id。一个应用一个id。"
+            },
+            "timestamp": {
+                "type": "integer",
+                "title": "UTC时间戳",
+                "description": "UTC时间戳,精确到毫秒。平台对超过五分钟的消息,将做忽略处理"
+            },
+            "token": {
+                "type": "string",
+                "title": "身份认证token",
+                "description": "平台登录后分配的token"
+            }
+        },
+        "x-apifox-orders": [
+            "ver",
+            "app_id",
+            "timestamp",
+            "token",
+            "data"
+        ],
+        "required": [
+            "data",
+            "ver",
+            "app_id",
+            "timestamp",
+            "token"
+        ]
+    },
+    method: archive,
+    method_params: {},
+    guards: []
+};
+
+module.exports = {
+    default: v1_0,
+    v1_0: v1_0
+};

+ 129 - 0
pmr-biz-manager/src/routes/api/prj/doc/copy.ts

@@ -0,0 +1,129 @@
+import {IApiProcessor, ICachedData, IMethodParams, IRequest} from "@core/Defined";
+import {Resp} from "@util/Resp";
+import {PrjFile} from "@core-models/PrjFile";
+import {is_project_document_modifiable} from "@src/utils/prj_premission_helper";
+import {PrjFileCategory} from "@core-models/PrjFileCategory";
+import {Oss} from "@util/Oss";
+
+interface IData {
+    /**
+     * 文档id
+     */
+    id: string;
+    /**
+     * 目标分类id,注意分类必须是可上传的
+     */
+    target: string;
+}
+
+function copy(json: IRequest, params: IMethodParams, cached_data: ICachedData): Promise<any> {
+    return new Promise(async (resolve, reject) => {
+        try {
+            let data = <IData>json.data;
+            let file = await PrjFile.findOne({where: {id: data.id}, raw: true});
+            if (!file) throw Resp.gen_err(Resp.ResourceNotFound, `文档(id:${data.id})不存在。`);
+
+            let category = await PrjFileCategory.findOne({where: {id: data.target}, raw: true});
+            if (!category) return reject(Resp.gen_err(Resp.ResourceNotFound, "文档类型不存在,id: "+ data.target));
+            if (!category.allow_upload) return reject(Resp.gen_err(Resp.Forbidden, '目标文档类型不允许自由复制文件。'))
+            // 检查权限
+            if (!await is_project_document_modifiable(cached_data.user_id, file.prj_id, file.id)) {
+                return reject(Resp.gen_err(Resp.Forbidden, '您没有权限复制此文档。'));
+            }
+            let oss = Oss.get_instance('pmr-doc');
+            let new_id = file.id + '_c';
+            await oss.copy_object(oss.bucket, file.id, new_id);
+            await PrjFile.create({
+                id: new_id,
+                category_id: data.target,
+                prj_id: file.prj_id,
+                dependent_id: file.dependent_id,
+                uploaded: file.uploaded,
+                req_upload_time: file.req_upload_time,
+                uploaded_at: new Date(),
+                creator_id: cached_data.user_id,
+                filename: file.filename,
+                size: file.size,
+                archived: file.archived
+
+            });
+            resolve({});
+        } catch (e) {
+            reject(Resp.throw_err(e));
+        }
+    });
+}
+
+const v1_0: IApiProcessor = {
+    schema: {
+        "type": "object",
+        "properties": {
+            "data": {
+                "type": "object",
+                "properties": {
+                    "id": {
+                        "type": "string",
+                        "title": "文档id"
+                    },
+                    "target": {
+                        "type": "string",
+                        "title": "目标分类id",
+                        "description": "注意分类必须是可上传的"
+                    }
+                },
+                "x-apifox-orders": [
+                    "id",
+                    "target"
+                ],
+                "title": "请求参数内容",
+                "required": [
+                    "id",
+                    "target"
+                ]
+            },
+            "ver": {
+                "type": "string",
+                "title": "版本号",
+                "description": "文档中没有说明时,默认为1.0",
+                "default": "1.0"
+            },
+            "app_id": {
+                "type": "string",
+                "title": "应用id",
+                "description": "平台分配给客户端的app id。一个应用一个id。"
+            },
+            "timestamp": {
+                "type": "integer",
+                "title": "UTC时间戳",
+                "description": "UTC时间戳,精确到毫秒。平台对超过五分钟的消息,将做忽略处理"
+            },
+            "token": {
+                "type": "string",
+                "title": "身份认证token",
+                "description": "平台登录后分配的token"
+            }
+        },
+        "x-apifox-orders": [
+            "ver",
+            "app_id",
+            "timestamp",
+            "token",
+            "data"
+        ],
+        "required": [
+            "data",
+            "ver",
+            "app_id",
+            "timestamp",
+            "token"
+        ]
+    },
+    method: copy,
+    method_params: {},
+    guards: []
+};
+
+module.exports = {
+    default: v1_0,
+    v1_0: v1_0
+};

+ 163 - 0
pmr-biz-manager/src/routes/api/prj/doc/detail.ts

@@ -0,0 +1,163 @@
+import {IApiProcessor, ICachedData, IMethodParams, IRequest} from "@core/Defined";
+import {Resp} from "@util/Resp";
+import {QueryTypes} from "sequelize";
+import {PrjInfo} from "@core-models/PrjInfo";
+import {AcsUserInfo} from "@core-models/AcsUserInfo";
+import DataCURD from "@core/DataCURD";
+import {PrjFile} from "@core-models/PrjFile";
+import {PrjFileCategory} from "@core-models/PrjFileCategory";
+
+
+interface IData {
+    /**
+     * 文档id
+     */
+    id: string;
+}
+
+
+interface IResult {
+    /**
+     * 文档类型id
+     */
+    category_id: string;
+    /**
+     * 文档类型名
+     */
+    category_name: string;
+    /**
+     * 上传者账号
+     */
+    creator_id: string;
+    /**
+     * 上传者名称
+     */
+    creator_name: string;
+    /**
+     * 文件扩展名
+     */
+    extension: string;
+    /**
+     * 文件名
+     */
+    filename: string;
+    /**
+     * 文档id
+     */
+    id: string;
+    /**
+     * 项目id
+     */
+    prj_id: string;
+    /**
+     * 项目名称
+     */
+    prj_name: string;
+    /**
+     * 文件大小,单位为字节
+     */
+    size: number;
+    /**
+     * 上传时间(YYYY-MM-DD HH:mm:ss)
+     */
+    uploaded_at: Date;
+}
+
+
+function detail(json: IRequest, _params: IMethodParams, _cached_data: ICachedData): Promise<IResult> {
+    return new Promise<IResult>(async (resolve, reject) => {
+        try {
+            let data = <IData>json.data;
+            let doc_info = await PrjFile.sequelize!.query(`
+                select prj_file.id, prj_file.filename,
+                    SPLIT_PART(prj_file.filename, '.', -1) AS extension,
+                    prj_file.category_id, category.name as category_name,
+                    prj_file.prj_id, prj.name as prj_name,
+                    prj_file.creator_id, creator.name as creator_name,
+                    prj_file.size,
+                    prj_file.uploaded,
+                    TO_CHAR(prj_file.uploaded_at, 'yyyy-MM-dd HH24' || ':' || 'MI' || ':' || 'ss') as uploaded_at,
+                    prj_file.archived
+                from ${PrjFile.table_name} prj_file
+                left join ${PrjFileCategory.table_name} category on prj_file.category_id = category.id
+                left join ${PrjInfo.table_name} prj on prj_file.prj_id = prj.id
+                left join ${AcsUserInfo.table_name} creator on creator.id = prj_file.creator_id
+                where prj_file.id = :file_id
+            `, {type: QueryTypes.SELECT, raw: true, replacements: {
+                    file_id: data.id,
+                }})
+            if(!doc_info || doc_info.length === 0) return reject(Resp.gen_err(Resp.ResourceNotFound));
+            resolve(DataCURD.filter_null(doc_info[0]));
+        } catch (e) {
+            reject(Resp.throw_err(e));
+        }
+    })
+}
+
+const v1_0: IApiProcessor = {
+    schema: {
+        "type": "object",
+        "properties": {
+            "data": {
+                "type": "object",
+                "properties": {
+                    "id": {
+                        "type": "string",
+                        "title": "文档id"
+                    }
+                },
+                "x-apifox-orders": [
+                    "id"
+                ],
+                "title": "请求参数内容",
+                "required": [
+                    "id"
+                ]
+            },
+            "ver": {
+                "type": "string",
+                "title": "版本号",
+                "description": "文档中没有说明时,默认为1.0",
+                "default": "1.0"
+            },
+            "app_id": {
+                "type": "string",
+                "title": "应用id",
+                "description": "平台分配给客户端的app id。一个应用一个id。"
+            },
+            "timestamp": {
+                "type": "integer",
+                "title": "UTC时间戳",
+                "description": "UTC时间戳,精确到毫秒。平台对超过五分钟的消息,将做忽略处理"
+            },
+            "token": {
+                "type": "string",
+                "title": "身份认证token",
+                "description": "平台登录后分配的token"
+            }
+        },
+        "x-apifox-orders": [
+            "ver",
+            "app_id",
+            "timestamp",
+            "token",
+            "data"
+        ],
+        "required": [
+            "data",
+            "ver",
+            "app_id",
+            "timestamp",
+            "token"
+        ]
+    },
+    method: detail,
+    method_params: {},
+    guards: []
+}
+
+
+module.exports = {
+    default: v1_0,
+    v1_0: v1_0
+}

+ 8 - 3
pmr-biz-manager/src/routes/api/prj/doc/download.ts

@@ -2,20 +2,25 @@ import {IApiProcessor, ICachedData, IMethodParams, IRequest} from "@core/Defined
 import {Resp} from "@util/Resp";
 import {Oss} from "@util/Oss";
 import {PrjFile} from "@core-models/PrjFile";
+import {is_project_document_downloadable} from "@src/utils/prj_premission_helper";
 
 interface IData {
     id: string;
 }
 
-// TODO: 检查文档下载权限
-
 function download(json: IRequest, params: IMethodParams, cached_data: ICachedData): Promise<any> {
     return new Promise(async (resolve, reject) => {
         try {
             let data = <IData>json.data;
+
             let file = await PrjFile.findOne({where: {id: data.id}, raw: true});
             if (!file) throw Resp.gen_err(Resp.ResourceNotFound, `文档(id:${data.id})不存在。`);
-            if (file.uploaded === false) throw Resp.gen_err(Resp.ResourceNotFound, '文档尚未上传。')
+            if (!file.uploaded) throw Resp.gen_err(Resp.ResourceNotFound, '文档尚未上传。')
+
+            if (!await is_project_document_downloadable(cached_data.user_id, file.prj_id, file.id)) {
+                throw Resp.gen_err(Resp.Forbidden, '您没有权限下载该文档。');
+            }
+
             let object_name = file.id;
             let oss = Oss.get_instance('pmr-doc');
             let result = await oss.presigned_url(oss.bucket, object_name, 300, file.filename);

+ 2 - 1
pmr-biz-manager/src/routes/api/prj/doc/get_list.ts

@@ -62,7 +62,8 @@ function get_list(json: IRequest, _params: IMethodParams, _cached_data: ICachedD
                     prj_file.creator_id, creator.name as creator_name,
                     prj_file.size,
                     prj_file.uploaded,
-                    TO_CHAR(prj_file.uploaded_at, 'yyyy-MM-dd HH24' || ':' || 'MI' || ':' || 'ss') as uploaded_at
+                    TO_CHAR(prj_file.uploaded_at, 'yyyy-MM-dd HH24' || ':' || 'MI' || ':' || 'ss') as uploaded_at,
+                    prj_file.archived
             `;
             let condition = `
                 from ${PrjFile.table_name} prj_file

+ 8 - 4
pmr-biz-manager/src/routes/api/prj/doc/remove.ts

@@ -3,22 +3,26 @@ import {Resp} from "@util/Resp";
 import {Oss} from "@util/Oss";
 import {PrjFile} from "@core-models/PrjFile";
 import {PrjFileCategory} from "@core-models/PrjFileCategory";
+import {is_project_document_deletable} from "@src/utils/prj_premission_helper";
 interface IData {
     id: string;
 }
 
-// TODO: 部分文档不允许删除,或删除时需要与其它信息同步
-
-
 function remove(json: IRequest, params: IMethodParams, cached_data: ICachedData): Promise<any> {
     return new Promise(async (resolve, reject) => {
         try {
             let data = <IData>json.data;
             let file = await PrjFile.findOne({where: {id: data.id}, raw: true});
             if (!file) throw Resp.gen_err(Resp.ResourceNotFound, `文档(id:${data.id})不存在。`);
+            if (file.archived) {
+                return reject(Resp.gen_err(Resp.Forbidden, '文档已被归档,无法删除。'));
+            }
+            if (!await is_project_document_deletable(cached_data.user_id, file.prj_id, file.id)){
+                return reject(Resp.gen_err(Resp.Forbidden, '您没有权限删除此文档。'));
+            }
             let category = await PrjFileCategory.findOne({where: {id: file.category_id}, raw: true});
             if (!category) return reject(Resp.gen_err(Resp.ResourceNotFound, "文档类型不存在,id: "+ file.category_id));
-            if (category.allow_upload === false) return reject(Resp.gen_err(Resp.Forbidden, '此文档类型不允许自由删除。'))
+            if (!category.allow_upload) return reject(Resp.gen_err(Resp.Forbidden, '此文档类型不允许自由删除。'))
             await PrjFile.destroy({where: {id: data.id}});
             let object_name = file.id;
             let oss = Oss.get_instance('pmr-doc');

+ 111 - 0
pmr-biz-manager/src/routes/api/prj/doc/rename.ts

@@ -0,0 +1,111 @@
+import {IApiProcessor, ICachedData, IMethodParams, IRequest} from "@core/Defined";
+import {Resp} from "@util/Resp";
+import {PrjFile} from "@core-models/PrjFile";
+import {PrjFileCategory} from "@core-models/PrjFileCategory";
+import {is_project_document_modifiable} from "@src/utils/prj_premission_helper";
+interface IData {
+    /**
+     * 新文件名
+     */
+    filename: string;
+    /**
+     * 文档id
+     */
+    id: string;
+}
+
+
+function rename(json: IRequest, params: IMethodParams, cached_data: ICachedData): Promise<any> {
+    return new Promise(async (resolve, reject) => {
+        try {
+            let data = <IData>json.data;
+            let file = await PrjFile.findOne({where: {id: data.id}, raw: true});
+            if (!file) throw Resp.gen_err(Resp.ResourceNotFound, `文档(id:${data.id})不存在。`);
+
+            let category = await PrjFileCategory.findOne({where: {id: file.category_id}, raw: true});
+            if (!category) return reject(Resp.gen_err(Resp.ResourceNotFound, "文档类型不存在,id: "+ file.category_id));
+            if (!category.allow_upload) return reject(Resp.gen_err(Resp.Forbidden, '此文档类型不允许自由修改文件名。'))
+            // 检查权限
+            if (!await is_project_document_modifiable(cached_data.user_id, file.prj_id, file.id)) {
+                return reject(Resp.gen_err(Resp.Forbidden, '您没有权限修改此文档的文件名。'));
+            }
+            await PrjFile.update({filename: data.filename}, {where: {id: data.id}, returning: false});
+            resolve({});
+        } catch (e) {
+            reject(Resp.throw_err(e));
+        }
+    });
+}
+
+const v1_0: IApiProcessor = {
+    schema: {
+        "type": "object",
+        "properties": {
+            "data": {
+                "type": "object",
+                "properties": {
+                    "id": {
+                        "type": "string",
+                        "title": "文档id"
+                    },
+                    "filename": {
+                        "type": "string",
+                        "title": "新文件名"
+                    }
+                },
+                "x-apifox-orders": [
+                    "id",
+                    "filename"
+                ],
+                "title": "请求参数内容",
+                "required": [
+                    "id",
+                    "filename"
+                ]
+            },
+            "ver": {
+                "type": "string",
+                "title": "版本号",
+                "description": "文档中没有说明时,默认为1.0",
+                "default": "1.0"
+            },
+            "app_id": {
+                "type": "string",
+                "title": "应用id",
+                "description": "平台分配给客户端的app id。一个应用一个id。"
+            },
+            "timestamp": {
+                "type": "integer",
+                "title": "UTC时间戳",
+                "description": "UTC时间戳,精确到毫秒。平台对超过五分钟的消息,将做忽略处理"
+            },
+            "token": {
+                "type": "string",
+                "title": "身份认证token",
+                "description": "平台登录后分配的token"
+            }
+        },
+        "x-apifox-orders": [
+            "ver",
+            "app_id",
+            "timestamp",
+            "token",
+            "data"
+        ],
+        "required": [
+            "data",
+            "ver",
+            "app_id",
+            "timestamp",
+            "token"
+        ]
+    },
+    method: rename,
+    method_params: {},
+    guards: []
+};
+
+module.exports = {
+    default: v1_0,
+    v1_0: v1_0
+};

+ 9 - 12
pmr-biz-manager/src/routes/api/prj/doc/upload.ts

@@ -2,15 +2,11 @@ import {IApiProcessor, ICachedData, IMethodParams, IRequest} from "@core/Defined
 import {Resp} from "@util/Resp";
 import {Oss} from "@util/Oss";
 import {IdGen} from "@util/IdGen";
-import {PrjTaskOutcome} from "@core-models/PrjTaskOutcome";
 import {PrjInfo} from "@core-models/PrjInfo";
-import {PrjPlanTask} from "@core-models/PrjPlanTask";
-import {QueryTypes} from "sequelize";
-import {BizContractInfo} from "@core-models/BizContractInfo";
 import dayjs from "dayjs";
-import {PrjWeekReport} from "@core-models/PrjWeekReport";
 import {PrjFile} from "@core-models/PrjFile";
 import {PrjFileCategory} from "@core-models/PrjFileCategory";
+import {is_project_document_uploadable} from "@src/utils/prj_premission_helper";
 
 interface IData {
     /**
@@ -25,17 +21,18 @@ interface IData {
      * 项目id
      */
     prj_id: string;
-    [property: string]: any;
 }
 
-// TODO: 所有文档都放在一个表中,记录项目id和文档类型
-
 function get_upload_url(json: IRequest, params: IMethodParams, cached_data: ICachedData): Promise<any> {
     return new Promise(async (resolve, reject) => {
+        let data = <IData>json.data;
+        if (!await is_project_document_uploadable(cached_data.user_id, data.prj_id)) {
+            return reject(Resp.gen_err(Resp.Forbidden, "没有权限上传文档"));
+        }
         let t = await PrjFile.sequelize!.transaction();
         try {
             let id = IdGen.id();
-            let data = <IData>json.data;
+
             let object_name = `project/${data.prj_id}/${data.category_id}/${id}`;
 
             await PrjFile.create({
@@ -59,7 +56,7 @@ function get_upload_url(json: IRequest, params: IMethodParams, cached_data: ICac
 }
 
 /// 检查项目是否存在
-function existsGuard(json: IRequest, cached_data: ICachedData): Promise<void> {
+function existsGuard(json: IRequest, _cached_data: ICachedData): Promise<void> {
     return new Promise<void>(async (resolve, reject) => {
         let data = <IData>json.data;
         let prj = await PrjInfo.findOne({where: {id: data.prj_id}, raw: true});
@@ -69,12 +66,12 @@ function existsGuard(json: IRequest, cached_data: ICachedData): Promise<void> {
 }
 
 /// 检查文档类型是否允许自由上传
-function allowUploadGuard(json: IRequest, cached_data: ICachedData): Promise<void> {
+function allowUploadGuard(json: IRequest, _cached_data: ICachedData): Promise<void> {
     return new Promise<void>(async (resolve, reject) => {
         let data = <IData>json.data;
         let category = await PrjFileCategory.findOne({where: {id: data.category_id}, raw: true});
         if (!category) return reject(Resp.gen_err(Resp.ResourceNotFound, "文档类型不存在,id: "+ data.category_id));
-        if (category.allow_upload === false) return reject(Resp.gen_err(Resp.Forbidden, '此文档类型不允许自由上传文件。'))
+        if (!category.allow_upload) return reject(Resp.gen_err(Resp.Forbidden, '此文档类型不允许自由上传文件。'))
         resolve();
     });
 }

+ 4 - 4
pmr-biz-manager/src/routes/api/prj/week_report/add.ts

@@ -1,4 +1,4 @@
-import {ADMINISTRATOR, IApiProcessor, ICachedData, IMethodParams, IRequest} from "@core/Defined";
+import {IApiProcessor, ICachedData, IMethodParams, IRequest} from "@core/Defined";
 import {Resp} from "@util/Resp";
 import {WhereOptions} from "sequelize";
 import {IdGen} from "@util/IdGen";
@@ -34,7 +34,7 @@ interface IData {
 }
 
 /// 检查项目是否存在
-function existsGuard(json: IRequest, cached_data: ICachedData): Promise<void> {
+function existsGuard(json: IRequest, _cached_data: ICachedData): Promise<void> {
     return new Promise<void>(async (resolve, reject) => {
         let data = <IData>json.data;
         let prj = await PrjInfo.findOne({where: {id: data.prj_id}, raw: true});
@@ -50,8 +50,8 @@ function add(json: IRequest, params: IMethodParams, cached_data: ICachedData): P
         try {
             let data = <IData>json.data;
             let week = dayjs().week();
-            let report = await PrjWeekReport.findOne({where: {prj_id: data.prj_id, week: week}, transaction: t});
-            if (report) throw Resp.gen_err(Resp.DataExists, '本周周报已创建,不可重复创建。');
+            let report = await PrjWeekReport.findOne({where: {prj_id: data.prj_id, week: week, reporter_id: cached_data.user_id}, transaction: t});
+            if (report) throw Resp.gen_err(Resp.DataExists, '您的本周周报已创建,不可重复创建。');
             let id = IdGen.id();
             let value: any = {
                 id: id,

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

@@ -7,6 +7,7 @@ import {Logger} from "@util/Logger";
 import {PrjPlanTaskDraft} from "@core-models/PrjPlanTaskDraft";
 import {PrjPhaseDefine} from "@core-models/PrjPhaseDefine";
 import {Resp} from "@util/Resp";
+import {PrjFile} from "@core-models/PrjFile";
 
 /// 是否是项目特权账户
 export async function is_project_privileged_account(account_id: string): Promise<boolean> {
@@ -110,6 +111,18 @@ export async function is_project_accessible(account_id: string, project_id: stri
     return false;
 }
 
+/// 检查账号是否是项目文档的创建者
+export async function is_project_document_creator(account_id: string, document_id: string): Promise<boolean> {
+    let document = await PrjFile.findOne({where: {id: document_id}, raw: true});
+    if (document && document.creator_id === account_id) {
+        Logger.trace(`${account_id} 是项目文档 ${document_id} 的创建者。`);
+        return true;
+    }
+
+    Logger.trace(`${account_id} 不是项目文档 ${document_id} 的创建者。`);
+    return false;
+}
+
 /// 是否有修改项目信息的权限
 export async function is_project_modifiable(account_id: string, project_id: string): Promise<boolean> {
     if (
@@ -191,6 +204,7 @@ export async function is_project_task_outcome_modifiable(account_id: string, tas
 }
 
 
+
 export interface ITaskManageableCheckResult {
     manageable: boolean;
     error: any;
@@ -236,3 +250,43 @@ export async function if_project_task_manageable_in_current_phase(draft: boolean
     result.manageable = true;
     return result;
 }
+
+// 检查是否对项目文档有取列表查看权限
+export async function is_project_document_listable(account_id: string, prj_id: string): Promise<boolean> {
+    return await is_project_accessible(account_id, prj_id);
+}
+
+/// 检查是否对项目文档有上传权限
+export async function is_project_document_uploadable(account_id: string, prj_id: string): Promise<boolean> {
+    // 项目成员、特权人员、项目负责人、商务负责人、审核人员都有权限
+    return await is_project_privileged_account(account_id) ||
+        await is_project_leader(account_id, prj_id) ||
+        await is_project_business_leader(account_id, prj_id) ||
+        await is_project_member(account_id, prj_id);
+}
+
+// 检查是否对项目文档有下载权限
+export async function is_project_document_downloadable(account_id: string, prj_id: string, document_id: string): Promise<boolean> {
+    return await is_project_accessible(account_id, prj_id) ||
+        await is_project_document_creator(account_id, document_id);
+}
+
+// 检查是否对项目文档和修改文件名的权限
+export async function is_project_document_modifiable(account_id: string, prj_id: string, document_id: string): Promise<boolean> {
+    return await is_project_privileged_account(account_id) ||
+        await is_project_leader(account_id, prj_id) ||
+        await is_project_document_creator(account_id, document_id);
+}
+
+// 检查是否对项目文档有删除权限
+export async function is_project_document_deletable(account_id: string, prj_id: string, document_id: string): Promise<boolean> {
+    return await is_project_privileged_account(account_id) ||
+        await is_project_leader(account_id, prj_id) ||
+        await is_project_document_creator(account_id, document_id);
+}
+
+// 检查是否对项目文档有归档权限
+export async function is_project_document_archivable(account_id: string, prj_id: string): Promise<boolean> {
+    return await is_project_privileged_account(account_id) ||
+        await is_project_leader(account_id, prj_id);
+}