西格玛许 vor 5 Tagen
Ursprung
Commit
ecbfd4cb04

+ 31 - 17
ruoyi-modules/ruoyi-demo/src/main/java/org/dromara/demo/service/impl/KaoshixingService.java

@@ -47,8 +47,8 @@ public class KaoshixingService {
     private static final String ACTION_ID_LOGIN = "201";
     // 查询考生信息 action_id
     private static final String ACTION_ID_QUERY_USER = "209";
-    // 文档指定的JWT过期时间(10秒
-    private static final long EXPIRATION_MILLIS = 10 * 1000L;
+    // JWT过期时间(30秒,考虑网络延迟
+    private static final long EXPIRATION_SECONDS = 30L;
 
     // 固定的 appId / appKey(按需求写死)
     private static final String FIXED_APP_ID = "631088";
@@ -159,7 +159,9 @@ public class KaoshixingService {
     public String fetchExamList(KaoshixingExamListRequest req) {
         validateExamListParams(req);
 
-        String jwtInfo = generateJwt(FIXED_APP_KEY, ACTION_ID_EXAMS);
+        // 使用请求中的过期时间,如果没有则使用默认值
+        Integer expSeconds = req.getExpSeconds() != null ? req.getExpSeconds() : 300;
+        String jwtInfo = generateJwt(FIXED_APP_KEY, ACTION_ID_EXAMS, expSeconds);
         log.info("考试信息列表接口JWT:{}", jwtInfo);
 
         String requestUrl = String.format("%s%s/?jwt=%s", BASE_API_URL, FIXED_APP_ID, jwtInfo);
@@ -192,22 +194,34 @@ public class KaoshixingService {
 
 
     /**
-     * 生成JWT(完全按文档示例代码实现
+     * 生成JWT(使用默认过期时间
      */
     private String generateJwt(String appKey, String actionId) {
-        try {
-            return Jwts.builder()
-                .claim("exp", System.currentTimeMillis() + EXPIRATION_MILLIS) // 文档要求的过期时间
-                .claim("action_id", actionId)
-                .signWith(
-                    SignatureAlgorithm.HS256,
-                    appKey.getBytes(StandardCharsets.UTF_8.toString()) // 文档要求的UTF-8编码
-                )
-                .compact();
-        } catch (UnsupportedEncodingException e) {
-            log.error("生成JWT失败", e);
-            throw new RuntimeException("JWT生成失败:" + e.getMessage());
-        }
+        return generateJwt(appKey, actionId, EXPIRATION_SECONDS);
+    }
+
+    /**
+     * 生成JWT(自定义过期时间)
+     */
+    private String generateJwt(String appKey, String actionId, long expirationSeconds) {
+        // 按照考试星官方文档要求:使用毫秒级时间戳
+        long currentTimeMillis = System.currentTimeMillis();
+        long expTimeMillis = currentTimeMillis + expirationSeconds * 1000;
+
+        log.info("JWT生成参数 - appKey: {}, actionId: {}, 当前时间(毫秒): {}, 过期时间(毫秒): {}, 有效期: {}秒",
+                appKey, actionId, currentTimeMillis, expTimeMillis, expirationSeconds);
+
+        String jwt = Jwts.builder()
+            .claim("exp", expTimeMillis) // 考试星要求毫秒级时间戳,不是JWT标准的秒级
+            .claim("action_id", actionId)
+            .signWith(
+                SignatureAlgorithm.HS256,
+                appKey.getBytes(StandardCharsets.UTF_8)
+            )
+            .compact();
+        
+        log.info("生成的JWT: {}", jwt);
+        return jwt;
     }
 
     private boolean userExists(String userId) {

+ 74 - 0
ruoyi-modules/ruoyi-main/src/main/java/org/dromara/main/controller/MainExamEvaluationController.java

@@ -5,6 +5,7 @@ import cn.dev33.satoken.annotation.SaCheckPermission;
 import jakarta.validation.constraints.NotEmpty;
 import jakarta.validation.constraints.NotNull;
 import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
 import org.dromara.common.core.domain.R;
 import org.dromara.common.core.validate.AddGroup;
 import org.dromara.common.core.validate.EditGroup;
@@ -15,8 +16,10 @@ import org.dromara.common.mybatis.core.page.PageQuery;
 import org.dromara.common.mybatis.core.page.TableDataInfo;
 import org.dromara.common.web.core.BaseController;
 import org.dromara.demo.domain.dto.KaoshixingAnswerListRequest;
+import org.dromara.demo.domain.dto.KaoshixingAutoLoginRequest;
 import org.dromara.demo.domain.dto.KaoshixingExamListRequest;
 import org.dromara.demo.domain.dto.KaoshixingRequest;
+import org.dromara.demo.domain.dto.KaoshixingScoreListRequest;
 import org.dromara.demo.service.impl.KaoshixingService;
 import org.dromara.main.domain.bo.MainExamEvaluationBo;
 import org.dromara.main.domain.bo.MainExamEvaluationSyncBo;
@@ -32,6 +35,7 @@ import java.util.Collection;
 import java.util.List;
 import java.util.Map;
 
+@Slf4j
 @Validated
 @RequiredArgsConstructor
 @RestController
@@ -206,6 +210,21 @@ public class MainExamEvaluationController extends BaseController {
         return R.ok(result);
     }
 
+
+//    @SaCheckPermission("main:evaluation:list")
+    @PostMapping("/score-list")
+    @Log(title = "获取考生分数列表", businessType = BusinessType.OTHER)
+    public R<String> getScoreList(@RequestBody Map<String, Object> params) {
+        KaoshixingScoreListRequest request = new KaoshixingScoreListRequest();
+        Object examInfoIdObj = params.get("examInfoId");
+        if (examInfoIdObj == null) {
+            return R.fail("examInfoId不能为空");
+        }
+        request.setExamInfoId(Long.valueOf(examInfoIdObj.toString()));
+        request.setPage((Integer) params.getOrDefault("page", 1));
+        return R.ok(kaoshixingService.fetchScoreList(request));
+    }
+
     /**
      * 获取试卷试题列表 (603)
      */
@@ -238,4 +257,59 @@ public class MainExamEvaluationController extends BaseController {
         request.setPage((Integer) params.getOrDefault("page", 1));
         return R.ok(kaoshixingService.fetchLearningContents(request));
     }
+
+    /**
+     * 小程序专用:考生静默登录(action_id=203)
+     * 流程:先尝试203静默登录;若用户不存在,自动以201注册并登录。
+     * 成功后返回考试星跳转url,前端用web-view打开即可完成登录进入考试。
+     *
+     * @param params user_id      考生唯一标识(必填,通常用 studentId)
+     *               user_name    考生姓名(首次注册时必填)
+     *               department   部门(首次注册时必填,缺省传 "学员")
+     *               custom_url   登录后跳转地址(可选,*.kaoshixing.com 域名)
+     */
+    @PostMapping("/silent-login")
+    @Log(title = "考试星静默登录", businessType = BusinessType.OTHER)
+    public R<Map<String, Object>> silentLogin(@RequestBody Map<String, Object> params) {
+        String userId = (String) params.get("user_id");
+        if (userId == null || userId.isBlank()) {
+            return R.fail("user_id不能为空");
+        }
+        String userName  = (String) params.getOrDefault("user_name",  "学员");
+        String department = (String) params.getOrDefault("department", "学员");
+        String customUrl  = (String) params.get("custom_url");
+
+        KaoshixingAutoLoginRequest req = new KaoshixingAutoLoginRequest();
+        req.setUserId(userId);
+        req.setUserName(userName);
+        req.setDepartment(department);
+        req.setCustomUrl(customUrl);
+
+        try {
+            String resp = kaoshixingService.fetchAutoLogin(req);
+            // 解析考试星返回的url字段
+            com.fasterxml.jackson.databind.ObjectMapper mapper =
+                new com.fasterxml.jackson.databind.ObjectMapper();
+            com.fasterxml.jackson.databind.JsonNode root = mapper.readTree(resp);
+
+            int code = root.path("code").asInt(0);
+            if (code != 10000) {
+                String msg = root.path("msg").asText("登录失败");
+                return R.fail(msg);
+            }
+
+            String url = root.path("url").asText(null);
+            if (url == null || url.isBlank()) {
+                return R.fail("考试星未返回跳转链接");
+            }
+
+            Map<String, Object> data = new java.util.HashMap<>();
+            data.put("url", url);
+            return R.ok(data);
+        } catch (Exception e) {
+            log.error("考试星静默登录失败", e);
+            return R.fail("登录失败:" + e.getMessage());
+        }
+    }
 }
+

+ 1 - 0
ruoyi-modules/ruoyi-main/src/main/java/org/dromara/main/domain/MainAbilityConfig.java

@@ -25,6 +25,7 @@ public class MainAbilityConfig implements Serializable {
     private Integer thirdExamTime;
     private BigDecimal thirdExamPassMark;
     private BigDecimal thirdExamTotalScore;
+    private String thirdExamLink;
     private Integer sortOrder;
     private Long createDept;
     private Long createBy;

+ 2 - 1
ruoyi-modules/ruoyi-main/src/main/java/org/dromara/main/service/ICsOrderCardService.java

@@ -26,7 +26,8 @@ public interface ICsOrderCardService {
     Boolean cancelOrderCard(Long orderCardId);
 
     /**
-     * 检查并更新过期结算单
+     * 定时校验并过期超时未支付的结算单
      */
     void checkExpiredOrderCards();
+
 }

+ 9 - 9
ruoyi-modules/ruoyi-main/src/main/java/org/dromara/main/service/impl/CsOrderCardServiceImpl.java

@@ -118,16 +118,16 @@ public class CsOrderCardServiceImpl implements ICsOrderCardService {
 
     @Override
     public void checkExpiredOrderCards() {
-        List<CsOrderCard> expiredCards = baseMapper.selectList(
-            Wrappers.lambdaQuery(CsOrderCard.class)
+        List<CsOrderCard> expiredCards = baseMapper.selectList(Wrappers.<CsOrderCard>lambdaQuery()
                 .eq(CsOrderCard::getStatus, "pending")
-                .lt(CsOrderCard::getExpireTime, LocalDateTime.now())
-        );
-
-        for (CsOrderCard card : expiredCards) {
-            card.setStatus("expired");
-            baseMapper.updateById(card);
-            log.info("结算单已过期: orderCardId={}", card.getId());
+                .le(CsOrderCard::getExpireTime, LocalDateTime.now()));
+        if (!expiredCards.isEmpty()) {
+            for (CsOrderCard card : expiredCards) {
+                card.setStatus("expired");
+                baseMapper.updateById(card);
+                // 这里可以根据情况扩展同步改变消息 payload 状状态
+            }
         }
     }
+
 }

+ 68 - 5
ruoyi-modules/ruoyi-main/src/main/java/org/dromara/main/service/impl/MainExamEvaluationServiceImpl.java

@@ -40,6 +40,7 @@ import java.util.Comparator;
 import java.util.Date;
 import java.util.HashSet;
 import java.util.HashMap;
+import java.util.LinkedHashMap;
 import java.util.List;
 import java.util.Map;
 import java.util.Set;
@@ -101,7 +102,7 @@ public class MainExamEvaluationServiceImpl implements IMainExamEvaluationService
         boolean flag = baseMapper.insert(add) > 0;
         if (flag) {
             bo.setId(add.getId());
-            saveMainAbilityConfigs(add.getId(), bo.getAbilityConfigs());
+            saveMainAbilityConfigs(bo.getId(), bo.getAbilityConfigs());
         }
         return flag;
     }
@@ -123,12 +124,23 @@ public class MainExamEvaluationServiceImpl implements IMainExamEvaluationService
 
     private void saveMainAbilityConfigs(Long evaluationId, List<MainAbilityConfig> configs) {
         if (!CollectionUtils.isEmpty(configs)) {
-            for (int i = 0; i < configs.size(); i++) {
-                MainAbilityConfig config = configs.get(i);
+            Map<String, MainAbilityConfig> distinctConfigMap = new LinkedHashMap<>();
+            for (MainAbilityConfig config : configs) {
+                if (config == null || StringUtils.isBlank(config.getAbilityName()) || config.getThirdExamInfoId() == null) {
+                    continue;
+                }
+                String distinctKey = config.getAbilityName().trim() + "_" + config.getThirdExamInfoId();
+                distinctConfigMap.putIfAbsent(distinctKey, config);
+            }
+            List<MainAbilityConfig> distinctConfigs = distinctConfigMap.values().stream().toList();
+            for (int i = 0; i < distinctConfigs.size(); i++) {
+                MainAbilityConfig config = distinctConfigs.get(i);
                 config.setEvaluationId(evaluationId);
                 config.setSortOrder(i);
             }
-            mainAbilityConfigMapper.insertBatch(configs);
+            if (!distinctConfigs.isEmpty()) {
+                mainAbilityConfigMapper.insertBatch(distinctConfigs);
+            }
         }
     }
 
@@ -174,9 +186,60 @@ public class MainExamEvaluationServiceImpl implements IMainExamEvaluationService
     }
 
     @Override
+    @Transactional(rollbackFor = Exception.class)
     public Boolean syncThirdPartyData() {
         log.info("开始同步第三方测评数据");
-        return true;
+        try {
+            // 查询所有绑定了第三方考试的能力配置
+            List<MainAbilityConfig> configs = mainAbilityConfigMapper.selectList(new LambdaQueryWrapper<MainAbilityConfig>()
+                .isNotNull(MainAbilityConfig::getThirdExamInfoId));
+
+            if (configs.isEmpty()) {
+                log.info("没有需要同步的能力配置");
+                return true;
+            }
+
+            // 获取考试列表(可以通过考试星接口查询)
+            org.dromara.demo.domain.dto.KaoshixingExamListRequest request = new org.dromara.demo.domain.dto.KaoshixingExamListRequest();
+            request.setPage(1);
+            request.setExpSeconds(300);
+
+            String response = org.dromara.common.core.utils.SpringUtils.getBean(org.dromara.demo.service.impl.KaoshixingService.class)
+                    .fetchExamList(request);
+
+            com.fasterxml.jackson.databind.ObjectMapper objectMapper = new com.fasterxml.jackson.databind.ObjectMapper();
+            com.fasterxml.jackson.databind.JsonNode responseNode = objectMapper.readTree(response);
+
+            if (!responseNode.get("success").asBoolean()) {
+                log.error("调用考试星接口获取考试列表失败: {}", response);
+                return false;
+            }
+
+            com.fasterxml.jackson.databind.JsonNode rows = responseNode.get("bizContent").get("rows");
+
+            int updateCount = 0;
+            // 遍历配置更新信息
+            for (MainAbilityConfig config : configs) {
+                for (com.fasterxml.jackson.databind.JsonNode row : rows) {
+                    if (row.get("examInfoId").asLong() == config.getThirdExamInfoId()) {
+                        String examLink = row.has("examLink") ? row.get("examLink").asText() : null;
+                        if (org.dromara.common.core.utils.StringUtils.isNotBlank(examLink) &&
+                            !examLink.equals(config.getThirdExamLink())) {
+                            config.setThirdExamLink(examLink);
+                            mainAbilityConfigMapper.updateById(config);
+                            updateCount++;
+                        }
+                        break;
+                    }
+                }
+            }
+
+            log.info("同步第三方测评数据完成,共更新了 {} 条记录", updateCount);
+            return true;
+        } catch (Exception e) {
+            log.error("同步第三方数据异常", e);
+            throw new org.dromara.common.core.exception.ServiceException("同步第三方数据失败: " + e.getMessage());
+        }
     }
 
     @Override

+ 3 - 2
ruoyi-modules/ruoyi-main/src/main/resources/mapper/MainAbilityConfigMapper.xml

@@ -13,6 +13,7 @@
         <result property="thirdExamTime" column="third_exam_time"/>
         <result property="thirdExamPassMark" column="third_exam_pass_mark"/>
         <result property="thirdExamTotalScore" column="third_exam_total_score"/>
+        <result property="thirdExamLink" column="third_exam_link"/>
         <result property="sortOrder" column="sort_order"/>
         <result property="createDept" column="create_dept"/>
         <result property="createBy" column="create_by"/>
@@ -36,13 +37,13 @@
     <insert id="insertBatch" parameterType="java.util.List">
         INSERT INTO main_ability_config (
         evaluation_id, ability_name, third_exam_info_id, third_exam_name,
-        third_exam_time, third_exam_pass_mark, third_exam_total_score,
+        third_exam_time, third_exam_pass_mark, third_exam_total_score, third_exam_link,
         sort_order, create_dept, create_by, create_time, del_flag
         ) VALUES
         <foreach collection="configs" item="item" separator=",">
             (#{item.evaluationId}, #{item.abilityName}, #{item.thirdExamInfoId},
             #{item.thirdExamName}, #{item.thirdExamTime}, #{item.thirdExamPassMark},
-            #{item.thirdExamTotalScore}, #{item.sortOrder}, #{item.createDept},
+            #{item.thirdExamTotalScore}, #{item.thirdExamLink}, #{item.sortOrder}, #{item.createDept},
             #{item.createBy}, NOW(), '0')
         </foreach>
     </insert>