|
|
@@ -0,0 +1,376 @@
|
|
|
+package org.dromara.demo.service.impl;
|
|
|
+
|
|
|
+import com.fasterxml.jackson.databind.JsonNode;
|
|
|
+import com.fasterxml.jackson.databind.ObjectMapper;
|
|
|
+import io.jsonwebtoken.Jwts;
|
|
|
+import io.jsonwebtoken.SignatureAlgorithm;
|
|
|
+import lombok.extern.slf4j.Slf4j;
|
|
|
+import org.apache.http.HttpEntity;
|
|
|
+import org.apache.http.client.methods.CloseableHttpResponse;
|
|
|
+import org.apache.http.client.methods.HttpPost;
|
|
|
+import org.apache.http.entity.StringEntity;
|
|
|
+import org.apache.http.impl.client.CloseableHttpClient;
|
|
|
+import org.apache.http.impl.client.HttpClients;
|
|
|
+import org.apache.http.util.EntityUtils;
|
|
|
+import org.dromara.demo.domain.dto.*;
|
|
|
+import org.springframework.stereotype.Service;
|
|
|
+
|
|
|
+import java.io.UnsupportedEncodingException;
|
|
|
+import java.nio.charset.StandardCharsets;
|
|
|
+import java.util.HashMap;
|
|
|
+import java.util.Map;
|
|
|
+
|
|
|
+/**
|
|
|
+ * 考试星-试卷试题列表接口(action_id=603,完全对齐文档)
|
|
|
+ */
|
|
|
+@Slf4j
|
|
|
+@Service
|
|
|
+public class KaoshixingService {
|
|
|
+
|
|
|
+ // 文档指定的基础接口地址
|
|
|
+ private static final String BASE_API_URL = "https://api.kaoshixing.com/api/company/data/";
|
|
|
+ // 试卷试题列表固定action_id
|
|
|
+ private static final String ACTION_ID_QUESTIONS = "603";
|
|
|
+ // 考试列表 action_id
|
|
|
+ private static final String ACTION_ID_EXAMS = "601";
|
|
|
+ // 考生分数列表 action_id
|
|
|
+ private static final String ACTION_ID_SCORES = "602";
|
|
|
+ // 考生答案列表 action_id
|
|
|
+ private static final String ACTION_ID_ANSWERS = "604";
|
|
|
+ // 考生可见考试+结果列表 action_id
|
|
|
+ private static final String ACTION_ID_USER_EXAMS = "702";
|
|
|
+ // 静默登录 action_id
|
|
|
+ private static final String ACTION_ID_SILENT_LOGIN = "203";
|
|
|
+ // 考生登录(可注册) action_id
|
|
|
+ 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;
|
|
|
+
|
|
|
+ // 固定的 appId / appKey(按需求写死)
|
|
|
+ private static final String FIXED_APP_ID = "631088";
|
|
|
+ private static final String FIXED_APP_KEY = "605f5f51895f423195850957bb903a43";
|
|
|
+
|
|
|
+ private final ObjectMapper objectMapper = new ObjectMapper();
|
|
|
+ /**
|
|
|
+ * 调用试卷试题列表接口
|
|
|
+ * @param examInfoId 考试ID(示例:119551)
|
|
|
+ * @return 试题列表JSON字符串
|
|
|
+ */
|
|
|
+ public String fetchExamPaperQuestions(KaoshixingRequest request) {
|
|
|
+ Long examInfoId = request.getExamInfoId();
|
|
|
+
|
|
|
+ validateParams(FIXED_APP_ID, FIXED_APP_KEY, examInfoId);
|
|
|
+
|
|
|
+ String jwtInfo = generateJwt(FIXED_APP_KEY, ACTION_ID_QUESTIONS);
|
|
|
+ log.info("试卷试题列表接口JWT:{}", jwtInfo);
|
|
|
+
|
|
|
+ String requestUrl = String.format("%s%s/?jwt=%s", BASE_API_URL, FIXED_APP_ID, jwtInfo);
|
|
|
+ log.info("试卷试题列表接口请求地址:{}", requestUrl);
|
|
|
+
|
|
|
+ String requestBody = String.format("{\"examInfoId\":\"%s\"}", examInfoId);
|
|
|
+ log.info("试卷试题列表接口请求体:{}", requestBody);
|
|
|
+
|
|
|
+ return sendPostRequest(requestUrl, requestBody);
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 考生答案列表(action_id=604)
|
|
|
+ */
|
|
|
+ public String fetchAnswerList(KaoshixingAnswerListRequest req) {
|
|
|
+ validateAnswerListParams(req);
|
|
|
+
|
|
|
+ String jwtInfo = generateJwt(FIXED_APP_KEY, ACTION_ID_ANSWERS);
|
|
|
+ log.info("考生答案列表接口JWT:{}", jwtInfo);
|
|
|
+
|
|
|
+ String requestUrl = String.format("%s%s/?jwt=%s", BASE_API_URL, FIXED_APP_ID, jwtInfo);
|
|
|
+ log.info("考生答案列表接口请求地址:{}", requestUrl);
|
|
|
+
|
|
|
+ String requestBody = buildAnswerListBody(req);
|
|
|
+ log.info("考生答案列表接口请求体:{}", requestBody);
|
|
|
+
|
|
|
+ return sendPostRequest(requestUrl, requestBody);
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 考生分数列表(action_id=602)
|
|
|
+ */
|
|
|
+ public String fetchScoreList(KaoshixingScoreListRequest req) {
|
|
|
+ validateScoreListParams(req);
|
|
|
+
|
|
|
+ String jwtInfo = generateJwt(FIXED_APP_KEY, ACTION_ID_SCORES);
|
|
|
+ log.info("考生分数列表接口JWT:{}", jwtInfo);
|
|
|
+
|
|
|
+ String requestUrl = String.format("%s%s/?jwt=%s", BASE_API_URL, FIXED_APP_ID, jwtInfo);
|
|
|
+ log.info("考生分数列表接口请求地址:{}", requestUrl);
|
|
|
+
|
|
|
+ String requestBody = buildScoreListBody(req);
|
|
|
+ log.info("考生分数列表接口请求体:{}", requestBody);
|
|
|
+
|
|
|
+ return sendPostRequest(requestUrl, requestBody);
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 免注册登录:先静默登录(203),失败则注册并登录(201)
|
|
|
+ */
|
|
|
+ public String fetchAutoLogin(KaoshixingAutoLoginRequest req) {
|
|
|
+ // 0) 先查是否存在(action_id=209)
|
|
|
+ if (userExists(req.getUserId())) {
|
|
|
+ // 已存在 -> 静默登录
|
|
|
+ String silentJwt = generateJwt(FIXED_APP_KEY, ACTION_ID_SILENT_LOGIN);
|
|
|
+ String silentUrl = String.format("%s%s/?jwt=%s", BASE_API_URL, FIXED_APP_ID, silentJwt);
|
|
|
+ String silentBody = String.format("{\"user_id\":\"%s\"}", req.getUserId());
|
|
|
+ log.info("静默登录请求体:{}", silentBody);
|
|
|
+ return sendPostRequest(silentUrl, silentBody);
|
|
|
+ }
|
|
|
+
|
|
|
+ // 不存在 -> 注册并登录(action_id=201),支持自定义跳转
|
|
|
+ String loginJwt = generateJwt(FIXED_APP_KEY, ACTION_ID_LOGIN);
|
|
|
+ String loginUrl = String.format("%s%s/?jwt=%s", BASE_API_URL, FIXED_APP_ID, loginJwt);
|
|
|
+ String loginBody = buildLoginBody(req);
|
|
|
+ log.info("考生登录(可注册)请求体:{}", loginBody);
|
|
|
+ return sendPostRequest(loginUrl, loginBody);
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 获取考生可见考试和考试结果整合列表(action_id=702)
|
|
|
+ */
|
|
|
+ public String fetchUserExamList(KaoshixingUserExamListRequest req) {
|
|
|
+ validateUserExamListParams(req);
|
|
|
+
|
|
|
+ String jwtInfo = generateJwt(FIXED_APP_KEY, ACTION_ID_USER_EXAMS);
|
|
|
+ log.info("考生可见考试列表接口JWT:{}", jwtInfo);
|
|
|
+
|
|
|
+ String requestUrl = String.format("%s%s/?jwt=%s", BASE_API_URL, FIXED_APP_ID, jwtInfo);
|
|
|
+ log.info("考生可见考试列表接口请求地址:{}", requestUrl);
|
|
|
+
|
|
|
+ String requestBody = String.format("{\"user_id\":\"%s\",\"page\":%d}", req.getUserId(), req.getPage());
|
|
|
+ log.info("考生可见考试列表接口请求体:{}", requestBody);
|
|
|
+
|
|
|
+ return sendPostRequest(requestUrl, requestBody);
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 调用考试信息列表接口(action_id=601,支持分页及时间筛选)
|
|
|
+ */
|
|
|
+ public String fetchExamList(KaoshixingExamListRequest req) {
|
|
|
+ validateExamListParams(req);
|
|
|
+
|
|
|
+ String jwtInfo = generateJwt(FIXED_APP_KEY, ACTION_ID_EXAMS);
|
|
|
+ log.info("考试信息列表接口JWT:{}", jwtInfo);
|
|
|
+
|
|
|
+ String requestUrl = String.format("%s%s/?jwt=%s", BASE_API_URL, FIXED_APP_ID, jwtInfo);
|
|
|
+ log.info("考试信息列表接口请求地址:{}", requestUrl);
|
|
|
+
|
|
|
+ String requestBody = buildExamListBody(req);
|
|
|
+ log.info("考试信息列表接口请求体:{}", requestBody);
|
|
|
+
|
|
|
+ return sendPostRequest(requestUrl, requestBody);
|
|
|
+ }
|
|
|
+
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 生成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());
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ private boolean userExists(String userId) {
|
|
|
+ try {
|
|
|
+ String queryJwt = generateJwt(FIXED_APP_KEY, ACTION_ID_QUERY_USER);
|
|
|
+ String queryUrl = String.format("%s%s/?jwt=%s", BASE_API_URL, FIXED_APP_ID, queryJwt);
|
|
|
+ String queryBody = String.format("{\"user_id\":\"%s\"}", userId);
|
|
|
+ log.info("查询考生信息请求体:{}", queryBody);
|
|
|
+ String resp = sendPostRequest(queryUrl, queryBody);
|
|
|
+ JsonNode root = objectMapper.readTree(resp);
|
|
|
+ if (root.get("code") != null && root.get("code").asInt() == 10000) {
|
|
|
+ JsonNode rowCount = root.get("rowCount");
|
|
|
+ JsonNode total = root.get("total");
|
|
|
+ int count = 0;
|
|
|
+ if (rowCount != null) {
|
|
|
+ count = rowCount.asInt(0);
|
|
|
+ } else if (total != null) {
|
|
|
+ count = total.asInt(0);
|
|
|
+ }
|
|
|
+ return count > 0;
|
|
|
+ }
|
|
|
+ } catch (Exception e) {
|
|
|
+ log.warn("查询考生信息失败,视为不存在", e);
|
|
|
+ }
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 发送POST请求(对齐官方示例的HttpClient)
|
|
|
+ */
|
|
|
+ private String sendPostRequest(String url, String body) {
|
|
|
+ try (CloseableHttpClient httpClient = HttpClients.createDefault()) {
|
|
|
+ HttpPost httpPost = new HttpPost(url);
|
|
|
+ // 文档要求的Content-Type(JSON格式)
|
|
|
+ httpPost.addHeader("Content-Type", "application/json");
|
|
|
+ // 设置请求体(UTF-8编码)
|
|
|
+ httpPost.setEntity(new StringEntity(body, StandardCharsets.UTF_8.toString()));
|
|
|
+
|
|
|
+ // 执行请求
|
|
|
+ try (CloseableHttpResponse response = httpClient.execute(httpPost)) {
|
|
|
+ HttpEntity entity = response.getEntity();
|
|
|
+ String responseContent = EntityUtils.toString(entity, StandardCharsets.UTF_8.toString());
|
|
|
+
|
|
|
+ // 打印响应信息
|
|
|
+ log.info("接口响应码:{}", response.getStatusLine().getStatusCode());
|
|
|
+ log.info("接口响应体:{}", responseContent);
|
|
|
+
|
|
|
+ // 非200响应抛出异常
|
|
|
+ if (response.getStatusLine().getStatusCode() != 200) {
|
|
|
+ throw new RuntimeException(String.format(
|
|
|
+ "接口请求失败:状态码=%s,内容=%s",
|
|
|
+ response.getStatusLine().getStatusCode(),
|
|
|
+ responseContent
|
|
|
+ ));
|
|
|
+ }
|
|
|
+ return responseContent;
|
|
|
+ }
|
|
|
+ } catch (Exception e) {
|
|
|
+ log.error("调用试卷试题列表接口失败", e);
|
|
|
+ throw new RuntimeException("接口调用失败:" + e.getMessage());
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 校验必填参数
|
|
|
+ */
|
|
|
+ private void validateParams(String appId, String appKey, Long examInfoId) {
|
|
|
+ if (isBlank(appId)) {
|
|
|
+ throw new IllegalArgumentException("appId不能为空(来自考试星后台)");
|
|
|
+ }
|
|
|
+ if (isBlank(appKey)) {
|
|
|
+ throw new IllegalArgumentException("appKey不能为空(来自考试星后台)");
|
|
|
+ }
|
|
|
+ if (examInfoId == null) {
|
|
|
+ throw new IllegalArgumentException("examInfoId不能为空(考试ID)");
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ private void validateExamListParams(KaoshixingExamListRequest req) {
|
|
|
+ if (req.getPage() == null) {
|
|
|
+ throw new IllegalArgumentException("page不能为空");
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ private void validateScoreListParams(KaoshixingScoreListRequest req) {
|
|
|
+ if (req.getExamInfoId() == null) {
|
|
|
+ throw new IllegalArgumentException("examInfoId不能为空");
|
|
|
+ }
|
|
|
+ if (req.getPage() == null) {
|
|
|
+ throw new IllegalArgumentException("page不能为空");
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ private void validateAnswerListParams(KaoshixingAnswerListRequest req) {
|
|
|
+ if (req.getExamInfoId() == null) {
|
|
|
+ throw new IllegalArgumentException("examInfoId不能为空");
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ private void validateUserExamListParams(KaoshixingUserExamListRequest req) {
|
|
|
+ if (isBlank(req.getUserId())) {
|
|
|
+ throw new IllegalArgumentException("user_id不能为空");
|
|
|
+ }
|
|
|
+ if (req.getPage() == null) {
|
|
|
+ throw new IllegalArgumentException("page不能为空");
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ private String buildLoginBody(KaoshixingAutoLoginRequest req) {
|
|
|
+ try {
|
|
|
+ Map<String, Object> payload = new HashMap<>();
|
|
|
+ payload.put("user_id", req.getUserId());
|
|
|
+ payload.put("user_name", req.getUserName());
|
|
|
+ payload.put("department", req.getDepartment());
|
|
|
+ if (!isBlank(req.getCustomUrl())) {
|
|
|
+ payload.put("custom_url", req.getCustomUrl());
|
|
|
+ }
|
|
|
+ return objectMapper.writeValueAsString(payload);
|
|
|
+ } catch (Exception e) {
|
|
|
+ log.error("构造登录请求体失败", e);
|
|
|
+ throw new RuntimeException("构造请求体失败:" + e.getMessage());
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ private boolean isSuccessResponse(String resp) {
|
|
|
+ try {
|
|
|
+ JsonNode root = objectMapper.readTree(resp);
|
|
|
+ JsonNode codeNode = root.get("code");
|
|
|
+ return codeNode != null && codeNode.asInt() == 10000;
|
|
|
+ } catch (Exception e) {
|
|
|
+ log.warn("解析响应判断成功标识失败,默认视为失败", e);
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ private String buildExamListBody(KaoshixingExamListRequest req) {
|
|
|
+ try {
|
|
|
+ Map<String, Object> payload = new HashMap<>();
|
|
|
+ payload.put("page", req.getPage());
|
|
|
+ if (!isBlank(req.getExamStartTime())) {
|
|
|
+ payload.put("examStartTime", req.getExamStartTime());
|
|
|
+ }
|
|
|
+ if (!isBlank(req.getExamEndTime())) {
|
|
|
+ payload.put("examEndTime", req.getExamEndTime());
|
|
|
+ }
|
|
|
+ if (!isBlank(req.getCreateTime())) {
|
|
|
+ payload.put("createTime", req.getCreateTime());
|
|
|
+ }
|
|
|
+ if (!isBlank(req.getExamIds())) {
|
|
|
+ payload.put("examIds", req.getExamIds());
|
|
|
+ }
|
|
|
+ return objectMapper.writeValueAsString(payload);
|
|
|
+ } catch (Exception e) {
|
|
|
+ log.error("构造考试信息列表请求体失败", e);
|
|
|
+ throw new RuntimeException("构造请求体失败:" + e.getMessage());
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ private String buildScoreListBody(KaoshixingScoreListRequest req) {
|
|
|
+ try {
|
|
|
+ Map<String, Object> payload = new HashMap<>();
|
|
|
+ payload.put("examInfoId", req.getExamInfoId());
|
|
|
+ payload.put("page", req.getPage());
|
|
|
+ return objectMapper.writeValueAsString(payload);
|
|
|
+ } catch (Exception e) {
|
|
|
+ log.error("构造成绩列表请求体失败", e);
|
|
|
+ throw new RuntimeException("构造请求体失败:" + e.getMessage());
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ private String buildAnswerListBody(KaoshixingAnswerListRequest req) {
|
|
|
+ try {
|
|
|
+ Map<String, Object> payload = new HashMap<>();
|
|
|
+ payload.put("examInfoId", req.getExamInfoId());
|
|
|
+ return objectMapper.writeValueAsString(payload);
|
|
|
+ } catch (Exception e) {
|
|
|
+ log.error("构造答案列表请求体失败", e);
|
|
|
+ throw new RuntimeException("构造请求体失败:" + e.getMessage());
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ private boolean isBlank(String val) {
|
|
|
+ return val == null || val.trim().isEmpty();
|
|
|
+ }
|
|
|
+}
|