Преглед на файлове

知识库 查看兑换以及积分情况数据信息

徐滕 преди 2 седмици
родител
ревизия
180fb44edd

+ 33 - 0
src/main/java/com/jeeplus/modules/WorkKnowledgeBase/entity/WorkKnowledgeBaseAuditRecord.java

@@ -33,6 +33,15 @@ public class WorkKnowledgeBaseAuditRecord extends DataEntity<WorkKnowledgeBaseAu
     /** 评分分数(仅技术总结/培训心得分类需要) */
     private Integer score;
 
+    /** 审核页面停留时长(秒) */
+    private Integer viewDurationSeconds;
+
+    /** 附件查看行为明细(JSON数组) */
+    private String attachmentViewDetail;
+
+    /** 是否查看过附件 0-否 1-是 */
+    private Integer isViewedAttachment;
+
     public String getFileId() {
         return fileId;
     }
@@ -89,4 +98,28 @@ public class WorkKnowledgeBaseAuditRecord extends DataEntity<WorkKnowledgeBaseAu
     public void setScore(Integer score) {
         this.score = score;
     }
+
+    public Integer getViewDurationSeconds() {
+        return viewDurationSeconds;
+    }
+
+    public void setViewDurationSeconds(Integer viewDurationSeconds) {
+        this.viewDurationSeconds = viewDurationSeconds;
+    }
+
+    public String getAttachmentViewDetail() {
+        return attachmentViewDetail;
+    }
+
+    public void setAttachmentViewDetail(String attachmentViewDetail) {
+        this.attachmentViewDetail = attachmentViewDetail;
+    }
+
+    public Integer getIsViewedAttachment() {
+        return isViewedAttachment;
+    }
+
+    public void setIsViewedAttachment(Integer isViewedAttachment) {
+        this.isViewedAttachment = isViewedAttachment;
+    }
 }

+ 44 - 2
src/main/java/com/jeeplus/modules/WorkKnowledgeBase/service/WorkKnowledgeBaseShareService.java

@@ -1015,7 +1015,8 @@ public class WorkKnowledgeBaseShareService extends CrudService<WorkKnowledgeBase
      * 规则:本轮三人通过即成功(仅统计 audit_start_date 之后的记录,隔离历史数据)
      */
     @Transactional(readOnly = false)
-    public void auditPass(String fileId, String auditUserId, String auditOpinion, Integer score) {
+    public void auditPass(String fileId, String auditUserId, String auditOpinion, Integer score,
+                          Integer viewDurationSeconds, String attachmentViewDetail, Integer isViewedAttachment) {
         // 使用 SELECT ... FOR UPDATE 加行锁,防止并发审核
         WorkKnowledgeBaseShareInfo entity = dao.getForUpdate(fileId);
         if (entity == null) {
@@ -1028,6 +1029,22 @@ public class WorkKnowledgeBaseShareService extends CrudService<WorkKnowledgeBase
         if (auditUserId.equals(entity.getSubmitAuditUserId())) {
             throw new RuntimeException("不允许审核自己提交的文件");
         }
+        
+        // ========== 审核浏览行为校验(字典配置最低停留时间 + 必须查看过附件) ==========
+        String minStayStr = com.jeeplus.modules.sys.utils.DictUtils.getDictLabel("1", "audit_min_stay_seconds", "30");
+        int minStaySeconds = 30; // 默认30秒
+        try {
+            minStaySeconds = Integer.parseInt(minStayStr);
+        } catch (NumberFormatException e) {
+            minStaySeconds = 30;
+        }
+        if (isViewedAttachment == null || isViewedAttachment != 1) {
+            throw new RuntimeException("您还没有查看附件信息,请认真查看附件内容至少" + minStaySeconds + "秒后再审核");
+        }
+        if (viewDurationSeconds != null && viewDurationSeconds < minStaySeconds) {
+            throw new RuntimeException("您查看附件仅" + viewDurationSeconds + "秒,低于最低要求" + minStaySeconds + "秒,请认真查看附件内容后再审核");
+        }
+        
         // 检查本轮是否已审核过(按 audit_start_date 过滤,避免历史记录干扰)
         Date startTime = entity.getAuditStartDate();
         if (auditRecordDao.countByFileIdAndUserIdSinceTime(fileId, auditUserId, startTime) > 0) {
@@ -1120,6 +1137,18 @@ public class WorkKnowledgeBaseShareService extends CrudService<WorkKnowledgeBase
         record.setAuditResult(1); // 1:通过
         record.setAuditOpinion(auditOpinion);
         record.setScore(score); // 设置评分分数
+        record.setViewDurationSeconds(viewDurationSeconds);
+        // 对 attachmentViewDetail 进行 URL 解码
+        String decodedViewDetail = attachmentViewDetail;
+        if (StringUtils.isNotBlank(attachmentViewDetail)) {
+            try {
+                decodedViewDetail = java.net.URLDecoder.decode(attachmentViewDetail, "UTF-8");
+            } catch (Exception e) {
+                decodedViewDetail = attachmentViewDetail;
+            }
+        }
+        record.setAttachmentViewDetail(decodedViewDetail);
+        record.setIsViewedAttachment(isViewedAttachment);
         record.setId(IdGen.uuid());
         record.setAuditTime(new java.util.Date());
         record.preInsert();
@@ -1224,7 +1253,8 @@ public class WorkKnowledgeBaseShareService extends CrudService<WorkKnowledgeBase
      * 规则:一票驳回直接审核失败;历史记录依靠 audit_start_date 隔离,无需逻辑删除
      */
     @Transactional(readOnly = false)
-    public void auditReject(String fileId, String auditUserId, String auditOpinion) {
+    public void auditReject(String fileId, String auditUserId, String auditOpinion,
+                            Integer viewDurationSeconds, String attachmentViewDetail, Integer isViewedAttachment) {
         // 使用 SELECT ... FOR UPDATE 加行锁,防止并发审核
         WorkKnowledgeBaseShareInfo entity = dao.getForUpdate(fileId);
         if (entity == null) {
@@ -1293,6 +1323,18 @@ public class WorkKnowledgeBaseShareService extends CrudService<WorkKnowledgeBase
         record.setAuditUserId(auditUserId);
         record.setAuditResult(2); // 2:驳回
         record.setAuditOpinion(auditOpinion);
+        record.setViewDurationSeconds(viewDurationSeconds);
+        // 对 attachmentViewDetail 进行 URL 解码
+        String decodedViewDetail = attachmentViewDetail;
+        if (StringUtils.isNotBlank(attachmentViewDetail)) {
+            try {
+                decodedViewDetail = java.net.URLDecoder.decode(attachmentViewDetail, "UTF-8");
+            } catch (Exception e) {
+                decodedViewDetail = attachmentViewDetail;
+            }
+        }
+        record.setAttachmentViewDetail(decodedViewDetail);
+        record.setIsViewedAttachment(isViewedAttachment);
         record.setId(IdGen.uuid());
         record.setAuditTime(new java.util.Date());
         record.preInsert();

+ 26 - 7
src/main/java/com/jeeplus/modules/WorkKnowledgeBase/web/WorkKnowledgeBaseShareController.java

@@ -387,6 +387,16 @@ public class WorkKnowledgeBaseShareController extends BaseController {
             }
         }
 
+        // 传递审核最低停留时间字典值(用于前端校验)
+        String minStayStr = com.jeeplus.modules.sys.utils.DictUtils.getDictLabel("1", "audit_min_stay_seconds", "30");
+        int minStaySeconds = 30;
+        try {
+            minStaySeconds = Integer.parseInt(minStayStr);
+        } catch (NumberFormatException e) {
+            minStaySeconds = 30;
+        }
+        model.addAttribute("minStaySeconds", minStaySeconds);
+
         return "modules/WorkKnowledgeBase/workKnowledgeBaseShareDetail";
     }
 
@@ -1482,11 +1492,14 @@ public class WorkKnowledgeBaseShareController extends BaseController {
     @ResponseBody
     public Map<String, Object> auditPass(@RequestParam String id,
                                          @RequestParam(required = false) String auditOpinion,
-                                         @RequestParam(required = false) Integer score) {
+                                         @RequestParam(required = false) Integer score,
+                                         @RequestParam(required = false) Integer viewDurationSeconds,
+                                         @RequestParam(required = false) String attachmentViewDetail,
+                                         @RequestParam(required = false) Integer isViewedAttachment) {
         Map<String, Object> result = new HashMap<>();
         try {
             String currentUserId = UserUtils.getUser().getId();
-            shareService.auditPass(id, currentUserId, auditOpinion, score);
+            shareService.auditPass(id, currentUserId, auditOpinion, score, viewDurationSeconds, attachmentViewDetail, isViewedAttachment);
             result.put("success", true);
             result.put("message", "审核通过操作成功");
         } catch (Exception e) {
@@ -1506,17 +1519,20 @@ public class WorkKnowledgeBaseShareController extends BaseController {
                                 @RequestParam(required = false) String auditOpinion,
                                 @RequestParam(required = false) Integer score,
                                 @RequestParam(required = false) String home,
-                                @RequestParam(required = false) Integer result) {
+                                @RequestParam(required = false) Integer result,
+                                @RequestParam(required = false) Integer viewDurationSeconds,
+                                @RequestParam(required = false) String attachmentViewDetail,
+                                @RequestParam(required = false) Integer isViewedAttachment) {
         try {
             String currentUserId = UserUtils.getUser().getId();
             
             // 判断是通过还是驳回(result=1或null为通过,result=2为驳回)
             if (result != null && result == 2) {
                 // 驳回
-                shareService.auditReject(id, currentUserId, auditOpinion);
+                shareService.auditReject(id, currentUserId, auditOpinion, viewDurationSeconds, attachmentViewDetail, isViewedAttachment);
             } else {
                 // 通过
-                shareService.auditPass(id, currentUserId, auditOpinion, score);
+                shareService.auditPass(id, currentUserId, auditOpinion, score, viewDurationSeconds, attachmentViewDetail, isViewedAttachment);
             }
             
             // 根据home参数决定重定向到哪个页面
@@ -1547,11 +1563,14 @@ public class WorkKnowledgeBaseShareController extends BaseController {
     @RequestMapping(value = "auditReject")
     @ResponseBody
     public Map<String, Object> auditReject(@RequestParam String id,
-                                           @RequestParam(required = false) String auditOpinion) {
+                                           @RequestParam(required = false) String auditOpinion,
+                                           @RequestParam(required = false) Integer viewDurationSeconds,
+                                           @RequestParam(required = false) String attachmentViewDetail,
+                                           @RequestParam(required = false) Integer isViewedAttachment) {
         Map<String, Object> result = new HashMap<>();
         try {
             String currentUserId = UserUtils.getUser().getId();
-            shareService.auditReject(id, currentUserId, auditOpinion);
+            shareService.auditReject(id, currentUserId, auditOpinion, viewDurationSeconds, attachmentViewDetail, isViewedAttachment);
             result.put("success", true);
             result.put("message", "审核驳回操作成功");
         } catch (Exception e) {

+ 2 - 2
src/main/resources/jeeplus.properties

@@ -159,9 +159,9 @@ endpoint=http://oss-cn-hangzhou.aliyuncs.com
 accessKeyId=LTAI5tKa6kzGr5EyPWJB4EcD
 accessKeySecret=arHxB7ZPhizrBYf4844TtyaRctPMgW
 #\u5F00\u53D1\u73AF\u5883
-bucketName=xgxm-test
+#bucketName=xgxm-test
 #\u751F\u4EA7\u73AF\u5883
-#bucketName=xg-ccpm
+bucketName=xg-ccpm
 #\u01E9\uFFFD\uFFFDbucketName
 qzBucketName=xg-qz
 avatarDir=app-img/avatar/

+ 21 - 13
src/main/resources/mappings/modules/WorkKnowledgeBase/WorkKnowledgeBaseAuditRecordDao.xml

@@ -3,15 +3,18 @@
 <mapper namespace="com.jeeplus.modules.WorkKnowledgeBase.dao.WorkKnowledgeBaseAuditRecordDao">
 
     <sql id="auditRecordColumns">
-        a.id              AS "id",
-        a.file_id         AS "fileId",
-        a.audit_user_id   AS "auditUserId",
-        a.audit_result    AS "auditResult",
-        a.audit_opinion   AS "auditOpinion",
-        a.audit_time      AS "auditTime",
-        a.score           AS "score",
-        a.create_date     AS "createDate",
-        a.del_flag        AS "delFlag"
+        a.id                    AS "id",
+        a.file_id               AS "fileId",
+        a.audit_user_id         AS "auditUserId",
+        a.audit_result          AS "auditResult",
+        a.audit_opinion         AS "auditOpinion",
+        a.audit_time            AS "auditTime",
+        a.score                 AS "score",
+        a.view_duration_seconds AS "viewDurationSeconds",
+        a.attachment_view_detail AS "attachmentViewDetail",
+        a.is_viewed_attachment  AS "isViewedAttachment",
+        a.create_date           AS "createDate",
+        a.del_flag              AS "delFlag"
     </sql>
 
     <!-- 单条查询 -->
@@ -147,9 +150,11 @@
     <insert id="insert">
         INSERT INTO work_knowledge_base_audit_record (
             id, file_id, audit_user_id, audit_result, audit_opinion, audit_time, score,
+            view_duration_seconds, attachment_view_detail, is_viewed_attachment,
             create_date, del_flag
         ) VALUES (
             #{id}, #{fileId}, #{auditUserId}, #{auditResult}, #{auditOpinion}, #{auditTime}, #{score},
+            #{viewDurationSeconds}, #{attachmentViewDetail}, #{isViewedAttachment},
             #{createDate}, #{delFlag}
         )
     </insert>
@@ -157,10 +162,13 @@
     <!-- 更新 -->
     <update id="update">
         UPDATE work_knowledge_base_audit_record SET
-            audit_result  = #{auditResult},
-            audit_opinion = #{auditOpinion},
-            audit_time    = #{auditTime},
-            score         = #{score}
+            audit_result           = #{auditResult},
+            audit_opinion          = #{auditOpinion},
+            audit_time             = #{auditTime},
+            score                  = #{score},
+            view_duration_seconds  = #{viewDurationSeconds},
+            attachment_view_detail = #{attachmentViewDetail},
+            is_viewed_attachment   = #{isViewedAttachment}
         WHERE id = #{id}
     </update>
 

+ 16 - 0
src/main/resources/mysqlDateBase/alter_work_knowledge_base_audit_record_add_view_behavior.sql

@@ -0,0 +1,16 @@
+-- 审核记录表新增浏览行为追踪字段
+-- 用于记录审核人在审核详情页的停留时长和附件查看行为
+ALTER TABLE work_knowledge_base_audit_record
+  ADD COLUMN view_duration_seconds INT DEFAULT NULL COMMENT '审核页面停留时长(秒)',
+  ADD COLUMN attachment_view_detail TEXT DEFAULT NULL COMMENT '附件查看行为明细(JSON数组)',
+  ADD COLUMN is_viewed_attachment TINYINT DEFAULT 0 COMMENT '是否查看过附件 0-否 1-是';
+
+-- =====================================================
+-- 字典数据:审核最低停留时间配置(audit_min_stay_seconds)
+-- 用于控制审核通过时,审核人在详情页的最低停留时间(秒)
+-- 领导可通过系统字典管理直接调整该值
+-- Java: DictUtils.getDictLabel("1", "audit_min_stay_seconds", "30")
+-- =====================================================
+INSERT INTO `sys_dict` (`id`, `value`, `label`, `type`, `description`, `sort`, `parent_id`, `create_by`, `create_date`, `update_by`, `update_date`, `remarks`, `del_flag`)
+VALUES
+  ('audit_mss_001', '1', '30', 'audit_min_stay_seconds', '审核最低停留时间(秒)', 10, '0', '1', NOW(), '1', NOW(), '审核通过时审核人在详情页的最低停留时间,单位秒', '0');

Файловите разлики са ограничени, защото са твърде много
+ 2308 - 0
src/main/resources/mysqlDateBase/backfill_audit_view_behavior_updates.sql


+ 117 - 7
src/main/webapp/webpage/modules/WorkKnowledgeBase/workKnowledgeBaseShareDetail.jsp

@@ -21,6 +21,87 @@
             homeParam = urlParams.get('home') || '';
         })();
         
+        // ========== 审核浏览行为追踪 ==========
+        var entryTime = null; // 首次查看附件时开始计时(null表示尚未查看附件)
+        var attachmentViewLog = []; // 附件查看行为日志
+        
+        // 拦截 openPreview 函数,记录预览行为
+        var originalOpenPreview = window.openPreview;
+        window.openPreview = function(url, type) {
+            // 首次查看附件时开始计时
+            if (entryTime === null) {
+                entryTime = Date.now();
+            }
+            // 记录预览行为
+            var attachName = '';
+            // 尝试从 URL 中提取文件名
+            try {
+                var urlObj = new URL(url);
+                var pathname = urlObj.pathname;
+                attachName = decodeURIComponent(pathname.split('/').pop() || '');
+            } catch(e) {
+                attachName = url.substring(0, 50);
+            }
+            attachmentViewLog.push({
+                name: attachName,
+                action: 'preview',
+                time: new Date().toLocaleString('zh-CN', {hour12: false})
+            });
+            // 调用原始函数
+            if (originalOpenPreview) {
+                originalOpenPreview(url, type);
+            }
+        };
+        
+        // 监听下载按钮点击(强制下载,阻止浏览器直接打开预览)
+        $(document).on('click', '.op-btn-download', function(e) {
+            e.preventDefault(); // 阻止默认的链接行为
+                    
+            // 首次查看附件时开始计时
+            if (entryTime === null) {
+                entryTime = Date.now();
+            }
+                    
+            var $btn = $(this);
+            var href = $btn.attr('href') || '';
+            var attachName = $btn.attr('download') || href.substring(href.lastIndexOf('/') + 1) || 'file';
+            // 对文件名进行 URL 解码
+            try {
+                attachName = decodeURIComponent(attachName);
+            } catch(e) {
+                // 解码失败,使用原始值
+            }
+                    
+            // 记录行为日志
+            attachmentViewLog.push({
+                name: attachName,
+                action: 'download',
+                time: new Date().toLocaleString('zh-CN', {hour12: false})
+            });
+                    
+            // 通过 fetch 获取文件并强制下载
+            if (href) {
+                fetch(href)
+                    .then(function(response) {
+                        return response.blob();
+                    })
+                    .then(function(blob) {
+                        var blobUrl = URL.createObjectURL(blob);
+                        var a = document.createElement('a');
+                        a.href = blobUrl;
+                        a.download = attachName;
+                        document.body.appendChild(a);
+                        a.click();
+                        document.body.removeChild(a);
+                        URL.revokeObjectURL(blobUrl);
+                    })
+                    .catch(function(err) {
+                        // 如果 fetch 失败(如跨域),回退到直接打开
+                        window.location.href = href;
+                    });
+            }
+        });
+        
         function doSubmit(obj){
             console.log('doSubmit called, obj=', obj);
             if (!canAuditFlag) {
@@ -36,9 +117,30 @@
                 $('#inputForm input[name="auditOpinion"]').remove();
                 $('#inputForm input[name="score"]').remove();
                 $('#inputForm input[name="result"]').remove();
+                $('#inputForm input[name="viewDurationSeconds"]').remove();
+                $('#inputForm input[name="attachmentViewDetail"]').remove();
+                $('#inputForm input[name="isViewedAttachment"]').remove();
+                
+                // 计算附件查看时长(秒):从首次查看附件开始计时,未查看过则为0
+                var viewDurationSeconds = entryTime !== null ? Math.floor((Date.now() - entryTime) / 1000) : 0;
+                // 是否查看过附件
+                var isViewedAttachment = attachmentViewLog.length > 0 ? 1 : 0;
+                // 附件查看行为 JSON
+                var attachmentViewDetail = JSON.stringify(attachmentViewLog);
                 
                 if (obj == 1) {
-                    // 通过:审核意见为空时默认"同意"
+                    // ========== 前端校验:停留时长和附件查看(仅“通过”操作) ==========
+                    var minStaySeconds = ${minStaySeconds != null ? minStaySeconds : 30};
+                    if (isViewedAttachment != 1) {
+                        parent.layer.msg('您还没有查看附件信息,请认真查看附件内容至少' + minStaySeconds + '秒后再审核', {icon: 5, time: 5000});
+                        return false;
+                    }
+                    if (viewDurationSeconds < minStaySeconds) {
+                        parent.layer.msg('您查看附件仅' + viewDurationSeconds + '秒,低于最低要求' + minStaySeconds + '秒,请认真查看附件内容后再审核', {icon: 5, time: 5000});
+                        return false;
+                    }
+                                    
+                    // 通过:审核意见为空时默认“同意”
                     if (!opinion || opinion.trim() === '') {
                         opinion = '同意';
                     }
@@ -83,25 +185,33 @@
                     <c:if test="${treeName == '技术总结' || treeName == '培训心得' || (treeName == '典型案例' && entity.contentAttribute == '0')}">
                     $('#inputForm').append('<input type="hidden" name="score" value="' + $('#scoreValue').val() + '" />');
                     </c:if>
-                    
+                    // 添加浏览行为追踪字段
+                    $('#inputForm').append('<input type="hidden" name="viewDurationSeconds" value="' + viewDurationSeconds + '" />');
+                    $('#inputForm').append('<input type="hidden" name="attachmentViewDetail" value="' + encodeURIComponent(attachmentViewDetail) + '" />');
+                    $('#inputForm').append('<input type="hidden" name="isViewedAttachment" value="' + isViewedAttachment + '" />');
+                                    
                     // 提交表单
                     $("#inputForm").submit();
                     return true;
                 } else {
-                    // 驳回:审核意见为空时默认"驳回"
+                    // 驳回:审核意见为空时默认“驳回”
                     if (!opinion || opinion.trim() === '') {
                         opinion = '驳回';
                     }
-                    
+                                    
                     // 设置表单action为新的auditPassForm方法(驳回也走同一个接口,只是result不同)
                     var formAction = '${ctx}/workKnowledgeBase/share/auditPassForm?id=${entity.id}&home=' + encodeURIComponent(homeParam);
                     $("#inputForm").attr("action", formAction);
                     console.log('Form action set to:', formAction);
-                    
+                                    
                     // 添加隐藏字段传递审核意见(驳回时不传score)
                     $('#inputForm').append('<input type="hidden" name="auditOpinion" value="' + escapeHtml(opinion) + '" />');
                     $('#inputForm').append('<input type="hidden" name="result" value="2" />');
-                    
+                    // 添加浏览行为追踪字段(驳回也记录)
+                    $('#inputForm').append('<input type="hidden" name="viewDurationSeconds" value="' + viewDurationSeconds + '" />');
+                    $('#inputForm').append('<input type="hidden" name="attachmentViewDetail" value="' + encodeURIComponent(attachmentViewDetail) + '" />');
+                    $('#inputForm').append('<input type="hidden" name="isViewedAttachment" value="' + isViewedAttachment + '" />');
+                                    
                     // 提交表单
                     $("#inputForm").submit();
                     return true;
@@ -435,7 +545,7 @@
                                 <td><fmt:formatDate value="${attach.createDate}" type="both"/></td>
                                 <td>${attach.fileSize}</td>
                                 <td>
-                                    <a href="${attach.temporaryUrl}" class="op-btn op-btn-download">
+                                    <a href="${attach.temporaryUrl}" download="${attach.attachmentName}" class="op-btn op-btn-download">
                                         <i class="fa fa-download"></i>&nbsp;下载
                                     </a>
                                 </td>