From fee64cb1aca5eb94d2f4cade38e19596a401a16c Mon Sep 17 00:00:00 2001 From: winter <2436197699@qq.com> Date: Tue, 19 Dec 2023 09:41:35 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E5=88=9D=E6=AD=A5=E8=B5=B0=E9=80=9A=20?= =?UTF-8?q?spark=20Gpt=20=E7=9A=84=E5=AF=B9=E6=8E=A5=E6=B5=81=E7=A8=8B,?= =?UTF-8?q?=E7=95=99=E4=B8=8B=E6=89=A9=E5=B1=95=E5=85=B6=E4=BB=96=20AI=20?= =?UTF-8?q?=E7=9A=84=E7=A9=BA=E9=97=B4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- pom.xml | 10 ++ .../java/cn/teammodel/ai/SparkGptClient.java | 115 ++++++++++++++++++ .../cn/teammodel/ai/SparkGptProperties.java | 20 +++ src/main/java/cn/teammodel/ai/SseHelper.java | 30 +++++ .../ai/domain/SparkChatRequestParam.java | 83 +++++++++++++ .../ai/domain/SparkChatResponse.java | 77 ++++++++++++ .../ai/listener/SparkGptStreamListener.java | 96 +++++++++++++++ .../cn/teammodel/common/CommonConstant.java | 1 + .../cn/teammodel/common/FiveEducations.java | 13 ++ .../controller/AdminAppraiseController.java | 32 +++++ .../admin/service/AdminAppraiseService.java | 4 + .../impl/AdminAppraiseServiceImpl.java | 61 +++++++++- .../controller/frontend/AiController.java | 28 +++++ .../{ => frontend}/AppraiseController.java | 12 +- .../{ => frontend}/HelloController.java | 62 +++++----- .../dao/AppraiseRecordRepository.java | 1 + .../cn/teammodel/dao/AppraiseRepository.java | 3 +- .../cn/teammodel/dao/StudentRepository.java | 2 + .../dto/admin/UpdateAchievementRuleDto.java | 45 +++++++ .../model/dto/ai/ChatCompletionReqDto.java | 16 +++ .../entity/appraise/AchievementRule.java | 38 ++++++ .../model/entity/appraise/Appraise.java | 9 +- .../model/entity/school/SchoolConfig.java | 16 +++ .../model/vo/appraise/StudentReportVo.java | 20 +++ .../teammodel/service/ChatMessageService.java | 15 +++ .../teammodel/service/EvaluationService.java | 7 ++ .../service/impl/ChatMessageServiceImpl.java | 69 +++++++++++ .../service/impl/EvaluationServiceImpl.java | 88 ++++++++++++-- src/main/resources/application.yml | 10 +- .../TeamModelExtensionApplicationTests.java | 22 ++-- 30 files changed, 945 insertions(+), 60 deletions(-) create mode 100644 src/main/java/cn/teammodel/ai/SparkGptClient.java create mode 100644 src/main/java/cn/teammodel/ai/SparkGptProperties.java create mode 100644 src/main/java/cn/teammodel/ai/SseHelper.java create mode 100644 src/main/java/cn/teammodel/ai/domain/SparkChatRequestParam.java create mode 100644 src/main/java/cn/teammodel/ai/domain/SparkChatResponse.java create mode 100644 src/main/java/cn/teammodel/ai/listener/SparkGptStreamListener.java create mode 100644 src/main/java/cn/teammodel/common/FiveEducations.java create mode 100644 src/main/java/cn/teammodel/controller/admin/controller/AdminAppraiseController.java create mode 100644 src/main/java/cn/teammodel/controller/frontend/AiController.java rename src/main/java/cn/teammodel/controller/{ => frontend}/AppraiseController.java (86%) rename src/main/java/cn/teammodel/controller/{ => frontend}/HelloController.java (93%) create mode 100644 src/main/java/cn/teammodel/model/dto/admin/UpdateAchievementRuleDto.java create mode 100644 src/main/java/cn/teammodel/model/dto/ai/ChatCompletionReqDto.java create mode 100644 src/main/java/cn/teammodel/model/entity/appraise/AchievementRule.java create mode 100644 src/main/java/cn/teammodel/model/entity/school/SchoolConfig.java create mode 100644 src/main/java/cn/teammodel/model/vo/appraise/StudentReportVo.java create mode 100644 src/main/java/cn/teammodel/service/ChatMessageService.java create mode 100644 src/main/java/cn/teammodel/service/impl/ChatMessageServiceImpl.java diff --git a/pom.xml b/pom.xml index 0ddb111..8ec0e1d 100644 --- a/pom.xml +++ b/pom.xml @@ -37,6 +37,16 @@ org.springframework.boot spring-boot-starter-oauth2-resource-server + + org.springframework.boot + spring-boot-configuration-processor + + + + com.squareup.okhttp3 + okhttp + 3.14.9 + diff --git a/src/main/java/cn/teammodel/ai/SparkGptClient.java b/src/main/java/cn/teammodel/ai/SparkGptClient.java new file mode 100644 index 0000000..ff21814 --- /dev/null +++ b/src/main/java/cn/teammodel/ai/SparkGptClient.java @@ -0,0 +1,115 @@ +package cn.teammodel.ai; + +import cn.hutool.json.JSONUtil; +import cn.teammodel.ai.domain.SparkChatRequestParam; +import cn.teammodel.ai.listener.SparkGptStreamListener; +import lombok.Data; +import lombok.extern.slf4j.Slf4j; +import okhttp3.HttpUrl; +import okhttp3.OkHttpClient; +import okhttp3.Request; +import org.springframework.beans.factory.InitializingBean; +import org.springframework.stereotype.Component; + +import javax.annotation.Resource; +import javax.crypto.Mac; +import javax.crypto.spec.SecretKeySpec; +import java.net.URL; +import java.nio.charset.StandardCharsets; +import java.text.SimpleDateFormat; +import java.util.*; +import java.util.concurrent.TimeUnit; + +/** + * spark Gpt client + * @author winter + * @create 2023-12-15 14:29 + */ +@Component +@Data +@Slf4j +public class SparkGptClient implements InitializingBean { + @Resource + private SparkGptProperties sparkGptProperties; + private OkHttpClient okHttpClient; + private String authUrl; + + /** + * 静态构造对象方法 + */ + public void init() { + String authUrl = genAuthUrl(sparkGptProperties.getEndpoint(), sparkGptProperties.getApiKey(), sparkGptProperties.getApiSecret()); + this.authUrl = authUrl.replace("http://", "ws://").replace("https://", "wss://"); + log.info("鉴权 url: {}", this.authUrl); + + this.okHttpClient = new OkHttpClient() + .newBuilder() + .connectTimeout(90, TimeUnit.SECONDS) + .readTimeout(90, TimeUnit.SECONDS) + .writeTimeout(90, TimeUnit.SECONDS) + .build(); + } + + /** + * 流式的生成结果,以 sse 的方式向客户端推送 + */ + public void streamChatCompletion(SparkChatRequestParam param, SparkGptStreamListener listener) { + try { + param.setAppId(sparkGptProperties.getAppId()); + Request request = new Request.Builder().url(authUrl).build(); + // 设置请求参数 + listener.setRequestJson(param.toJsonParams()); + log.info("请求参数 {}", JSONUtil.parseObj(param.toJsonParams()).toStringPretty()); + okHttpClient.newWebSocket(request, listener); + } catch (Exception e) { + log.error("Spark AI 请求异常: {}", e.getMessage()); + e.printStackTrace(); + } + } + + /** + * 生成鉴权URL + */ + public static String genAuthUrl(String endpoint, String apiKey, String apiSecret) { + URL url = null; + String date = null; + String preStr = null; + Mac mac = null; + try { + url = new URL(endpoint); + // 时间 + SimpleDateFormat format = new SimpleDateFormat("EEE, dd MMM yyyy HH:mm:ss z", Locale.US); + format.setTimeZone(TimeZone.getTimeZone("GMT")); + date = format.format(new Date()); + // 拼接 + preStr = "host: " + url.getHost() + "\n" + + "date: " + date + "\n" + + "GET " + url.getPath() + " HTTP/1.1"; + // SHA256加密 + mac = Mac.getInstance("hmacsha256"); + SecretKeySpec spec = new SecretKeySpec(apiSecret.getBytes(StandardCharsets.UTF_8), "hmacsha256"); + mac.init(spec); + } catch (Exception e) { + log.error("生成鉴权URL失败, endpoint: {}, apiKey: {}, apiSecret: {}", endpoint, apiKey, apiSecret); + throw new RuntimeException(e); + } + + byte[] hexDigits = mac.doFinal(preStr.getBytes(StandardCharsets.UTF_8)); + // Base64加密 + String sha = Base64.getEncoder().encodeToString(hexDigits); + // 拼接 + String authorization = String.format("api_key=\"%s\", algorithm=\"%s\", headers=\"%s\", signature=\"%s\"", apiKey, "hmac-sha256", "host date request-line", sha); + // 拼接地址 + HttpUrl httpUrl = Objects.requireNonNull(HttpUrl.parse("https://" + url.getHost() + url.getPath())).newBuilder(). + addQueryParameter("authorization", Base64.getEncoder().encodeToString(authorization.getBytes(StandardCharsets.UTF_8))). + addQueryParameter("date", date).// + addQueryParameter("host", url.getHost()).// + build(); + return httpUrl.toString(); + } + + @Override + public void afterPropertiesSet() throws Exception { + init(); + } +} diff --git a/src/main/java/cn/teammodel/ai/SparkGptProperties.java b/src/main/java/cn/teammodel/ai/SparkGptProperties.java new file mode 100644 index 0000000..7d82632 --- /dev/null +++ b/src/main/java/cn/teammodel/ai/SparkGptProperties.java @@ -0,0 +1,20 @@ +package cn.teammodel.ai; + +import lombok.Data; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.context.annotation.Configuration; + +/** + * @author winter + * @create 2023-12-15 14:29 + */ + +@Data +@Configuration +@ConfigurationProperties(prefix = "spark.gpt") +public class SparkGptProperties { + private String endpoint; + private String appId; + private String apiKey; + private String apiSecret; +} diff --git a/src/main/java/cn/teammodel/ai/SseHelper.java b/src/main/java/cn/teammodel/ai/SseHelper.java new file mode 100644 index 0000000..9dbe5ba --- /dev/null +++ b/src/main/java/cn/teammodel/ai/SseHelper.java @@ -0,0 +1,30 @@ +package cn.teammodel.ai; + +import lombok.experimental.UtilityClass; +import lombok.extern.slf4j.Slf4j; +import org.springframework.web.servlet.mvc.method.annotation.SseEmitter; + +@UtilityClass +@Slf4j +public class SseHelper { + /** + * 断开 server 与 client 的 sse 连接 + */ + public void complete(SseEmitter sseEmitter) { + try { + sseEmitter.complete(); + } catch (Exception e) { + } + } + + /** + * 向 client 发送消息 + */ + public void send(SseEmitter sseEmitter, Object data) { + try { + sseEmitter.send(data); + } catch (Exception e) { + log.error("sseEmitter send error", e); + } + } +} diff --git a/src/main/java/cn/teammodel/ai/domain/SparkChatRequestParam.java b/src/main/java/cn/teammodel/ai/domain/SparkChatRequestParam.java new file mode 100644 index 0000000..45aa1c8 --- /dev/null +++ b/src/main/java/cn/teammodel/ai/domain/SparkChatRequestParam.java @@ -0,0 +1,83 @@ +package cn.teammodel.ai.domain; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.node.ObjectNode; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; + +import java.util.List; + +/** + * 调用星火大模型需要向其 endpoint 传递的参数 + * @author winter + * @create 2023-12-15 16:04 + */ +@Data +@Builder +// 注意这两个注解一起使用会让无参构造丢失 +public class SparkChatRequestParam { + //应用appid,从开放平台控制台创建的应用中获取 + private String appId; + //每个用户的id,用于区分不同用户 + private String uid; + //指定访问的领域,general指向V1.5版本 generalv2指向V2版本。注意:不同的取值对应的url也不一样! + @Builder.Default + private String domain = "generalv3"; + //核采样阈值。用于决定结果随机性,取值越高随机性越强即相同的问题得到的不同答案的可能性越高 + @Builder.Default + private Float temperature = 0.5F; + //模型回答的tokens的最大长度 + @Builder.Default + private Integer maxTokens = 2048; + //从k个候选中随机选择⼀个(⾮等概率) + @Builder.Default + private Integer top_k = 4; + //用于关联用户会话 + private String chatId; + private List messageList; + + + @Data + @AllArgsConstructor + public static class Message { + private String role; + private String content; + + /** + * 一个类下面有两种类型的对象,使用静态的对象产生方法,好思路 + */ + public static Message ofUser(String content){ + return new Message("user",content); + } + + public static Message ofAssistant(String content){ + return new Message("assistant",content); + } + } + + public String toJsonParams(){ + ObjectMapper om = new ObjectMapper(); + ObjectNode root = om.createObjectNode(); + ObjectNode header = om.createObjectNode(); + header.put("app_id",appId); + header.put("uid",uid); + ObjectNode parameter = om.createObjectNode(); + ObjectNode chat = om.createObjectNode(); + chat.put("domain", domain); + chat.put("temperature", temperature); + chat.put("max_tokens", maxTokens); + chat.put("top_k", top_k); + chat.put("chat_id", chatId); + parameter.set("chat", chat); + + ObjectNode payload = om.createObjectNode(); + payload.set("message", om.createObjectNode().putPOJO("text", messageList)); + + root.set("header", header); + root.set("parameter", parameter); + root.set("payload", payload); + return root.toString(); + } + +} diff --git a/src/main/java/cn/teammodel/ai/domain/SparkChatResponse.java b/src/main/java/cn/teammodel/ai/domain/SparkChatResponse.java new file mode 100644 index 0000000..39f634c --- /dev/null +++ b/src/main/java/cn/teammodel/ai/domain/SparkChatResponse.java @@ -0,0 +1,77 @@ +package cn.teammodel.ai.domain; + +import lombok.Data; + +import java.util.List; + +/** + * spark ws 的响应实体 + * @Author: winter + */ +@Data +public class SparkChatResponse { + private Header header; + private Payload payload; + + @Data + public static class Header{ + /** + * 错误码,0表示正常,非0表示出错 + */ + private Integer code; + private String message; + private String sid; + /** + * 会话状态,取值为[0,1,2];0代表首次结果;1代表中间结果;2代表最后一个结果 + */ + private Integer status; + } + + @Data + public static class Payload{ + private Choices choices; + private Usage usage; + } + + @Data + public static class Choices{ + private Integer status; + private Integer seq; + private List text; + } + + @Data + public static class Usage{ + private UsageText text; + } + @Data + public static class UsageText{ + private Integer question_tokens; + /** + * 包含历史问题的总tokens大小 + */ + private Integer prompt_tokens; + /** + * 回答的tokens大小 + */ + private Integer completion_tokens; + /** + * prompt_tokens和completion_tokens的和,也是本次交互计费的tokens大小 + */ + private Integer total_tokens; + } + + @Data + public static class Text{ + /** + * AI的回答内容 + */ + private String content; + /** + * 角色标识,固定为assistant,标识角色为AI + */ + private String role; + private Integer index; + } + +} diff --git a/src/main/java/cn/teammodel/ai/listener/SparkGptStreamListener.java b/src/main/java/cn/teammodel/ai/listener/SparkGptStreamListener.java new file mode 100644 index 0000000..013deae --- /dev/null +++ b/src/main/java/cn/teammodel/ai/listener/SparkGptStreamListener.java @@ -0,0 +1,96 @@ +package cn.teammodel.ai.listener; + +import cn.hutool.core.util.StrUtil; +import cn.hutool.json.JSONUtil; +import cn.teammodel.ai.SseHelper; +import cn.teammodel.ai.domain.SparkChatResponse; +import cn.teammodel.common.ErrorCode; +import cn.teammodel.config.exception.ServiceException; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import okhttp3.Response; +import okhttp3.WebSocket; +import okhttp3.WebSocketListener; +import org.springframework.web.servlet.mvc.method.annotation.SseEmitter; + +import javax.annotation.Nullable; +import javax.validation.constraints.NotNull; +import java.util.function.Consumer; + +/** + * okhttp 调用 ws 接口时的 listner + * @author winter + * @create 2023-12-15 16:17 + */ +@EqualsAndHashCode(callSuper = true) +@Data +@Slf4j +@RequiredArgsConstructor +public class SparkGptStreamListener extends WebSocketListener { + private final SseEmitter sseEmitter; + private String requestJson; + /** + * 模型响应的完整回复 + */ + private String answer = ""; + + @Override + public void onOpen(WebSocket webSocket, @NotNull Response response) { + // ws 建立连接后发送请求的数据, 在 onMessage 中接受 server 的响应数据 + webSocket.send(requestJson); + // 执行成功回调 + try { + onOpen.accept(webSocket); + } catch (Exception e) { + // todo: 这儿不应该直接调她 + this.onFailure(webSocket, e, response); + } + } + + @Override + public void onMessage(WebSocket webSocket, String text) { + // 向 sse 推送消息(一次 onMessage 事件推送一次) + SparkChatResponse sparkChatResponse = JSONUtil.toBean(text, SparkChatResponse.class); + // 请求是否异常 + if (sparkChatResponse.getHeader().getCode() != 0) { + this.onFailure( + webSocket, + new ServiceException(ErrorCode.SYSTEM_ERROR.getCode(), + sparkChatResponse.getHeader().getMessage()), + null + ); + } + // 推送回答 segment + String msgSegment = sparkChatResponse.getPayload().getChoices().getText().get(0).getContent(); + // 处理消息格式(空格和换行符) + msgSegment = StrUtil.replace(msgSegment, " ", " ").replaceAll("\n", "\n"); + answer += msgSegment; + SseHelper.send(sseEmitter, msgSegment); + + // 处理模型的最终响应 + if (sparkChatResponse.getHeader().getStatus() == 2) { + // 其实 spark 会主动断开连接 + webSocket.close(1000, "done"); + onComplete.accept(answer); + SseHelper.complete(sseEmitter); + } + + } + + @Override + public void onFailure(WebSocket webSocket, Throwable t, @Nullable Response response) { + webSocket.close(1000, t.getMessage()); + this.onError.accept(t); + // 失败时结束 sse 连接 + SseHelper.send(sseEmitter,t.getMessage() + "[DONE]"); + SseHelper.complete(sseEmitter); + } + + + // 这几个 function 可以在 listener 被调用时设置, 实现类似事件的回调 + protected Consumer onOpen = (s) -> {}; + protected Consumer onError = (s) -> {}; + protected Consumer onComplete = (s) -> {}; +} diff --git a/src/main/java/cn/teammodel/common/CommonConstant.java b/src/main/java/cn/teammodel/common/CommonConstant.java index e5b6698..b2666ad 100644 --- a/src/main/java/cn/teammodel/common/CommonConstant.java +++ b/src/main/java/cn/teammodel/common/CommonConstant.java @@ -6,4 +6,5 @@ package cn.teammodel.common; */ public interface CommonConstant { String DASH = "-"; + } diff --git a/src/main/java/cn/teammodel/common/FiveEducations.java b/src/main/java/cn/teammodel/common/FiveEducations.java new file mode 100644 index 0000000..2efd603 --- /dev/null +++ b/src/main/java/cn/teammodel/common/FiveEducations.java @@ -0,0 +1,13 @@ +package cn.teammodel.common; + +/** + * @author winter + * @create 2023-12-14 12:19 + */ +public interface FiveEducations { + String MORAL = "美德"; + String INTELLECTUAL = "美智"; + String PHYSICAL = "体育"; + String AESTHETIC = "美艺"; + String LABOUR = "劳动"; +} diff --git a/src/main/java/cn/teammodel/controller/admin/controller/AdminAppraiseController.java b/src/main/java/cn/teammodel/controller/admin/controller/AdminAppraiseController.java new file mode 100644 index 0000000..e8803d8 --- /dev/null +++ b/src/main/java/cn/teammodel/controller/admin/controller/AdminAppraiseController.java @@ -0,0 +1,32 @@ +package cn.teammodel.controller.admin.controller; + +import cn.teammodel.common.R; +import cn.teammodel.controller.admin.service.AdminAppraiseService; +import cn.teammodel.model.dto.admin.UpdateAchievementRuleDto; +import cn.teammodel.model.entity.appraise.AchievementRule; +import io.swagger.annotations.ApiOperation; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import javax.annotation.Resource; +import java.util.List; + +/** + * @author winter + * @create 2023-12-13 16:07 + */ +@RestController +@RequestMapping("admin/appraise") +public class AdminAppraiseController { + @Resource + private AdminAppraiseService adminAppraiseService; + + @PostMapping("updateAchieveRule") + @ApiOperation("更新的 rule 节点将会直接覆盖老节点") + public R> updateAchieveRule(@RequestBody UpdateAchievementRuleDto ruleDto) { + List res = adminAppraiseService.updateAchieveRule(ruleDto); + return R.success(res); + } +} diff --git a/src/main/java/cn/teammodel/controller/admin/service/AdminAppraiseService.java b/src/main/java/cn/teammodel/controller/admin/service/AdminAppraiseService.java index dacec0f..e5862b9 100644 --- a/src/main/java/cn/teammodel/controller/admin/service/AdminAppraiseService.java +++ b/src/main/java/cn/teammodel/controller/admin/service/AdminAppraiseService.java @@ -1,6 +1,8 @@ package cn.teammodel.controller.admin.service; import cn.teammodel.model.dto.admin.TimeRangeDto; +import cn.teammodel.model.dto.admin.UpdateAchievementRuleDto; +import cn.teammodel.model.entity.appraise.AchievementRule; import cn.teammodel.model.vo.admin.IndexData; import cn.teammodel.model.vo.admin.RankPo; import cn.teammodel.model.vo.admin.RankVo; @@ -28,4 +30,6 @@ public interface AdminAppraiseService { List appraiseNodeRank(TimeRangeDto timeRangeDto); List studentRank(TimeRangeDto timeRangeDto); + + List updateAchieveRule(UpdateAchievementRuleDto ruleDto); } diff --git a/src/main/java/cn/teammodel/controller/admin/service/impl/AdminAppraiseServiceImpl.java b/src/main/java/cn/teammodel/controller/admin/service/impl/AdminAppraiseServiceImpl.java index d490a98..b4a9ddf 100644 --- a/src/main/java/cn/teammodel/controller/admin/service/impl/AdminAppraiseServiceImpl.java +++ b/src/main/java/cn/teammodel/controller/admin/service/impl/AdminAppraiseServiceImpl.java @@ -7,7 +7,10 @@ import cn.teammodel.config.exception.ServiceException; import cn.teammodel.controller.admin.service.AdminAppraiseService; import cn.teammodel.dao.*; import cn.teammodel.model.dto.admin.TimeRangeDto; +import cn.teammodel.model.dto.admin.UpdateAchievementRuleDto; import cn.teammodel.model.entity.User; +import cn.teammodel.model.entity.appraise.AchievementRule; +import cn.teammodel.model.entity.appraise.Appraise; import cn.teammodel.model.entity.school.ClassInfo; import cn.teammodel.model.entity.school.School; import cn.teammodel.model.entity.school.Student; @@ -18,10 +21,14 @@ import cn.teammodel.model.vo.admin.RankVo; import cn.teammodel.model.vo.admin.StudentRankVo; import cn.teammodel.model.vo.appraise.RecordVo; import cn.teammodel.security.utils.SecurityUtil; +import cn.teammodel.utils.RepositoryUtil; import cn.teammodel.utils.SchoolDateUtil; +import com.azure.cosmos.models.CosmosPatchOperations; +import com.azure.cosmos.models.PartitionKey; import com.azure.spring.data.cosmos.core.query.CosmosPageRequest; import org.apache.commons.lang3.ObjectUtils; import org.apache.commons.lang3.StringUtils; +import org.springframework.beans.BeanUtils; import org.springframework.data.domain.Slice; import org.springframework.stereotype.Service; @@ -46,10 +53,9 @@ public class AdminAppraiseServiceImpl implements AdminAppraiseService { @Resource private TeacherRepository teacherRepository; @Resource - private StudentRepository studentRepository; - @Resource private AppraiseRepository appraiseRepository; - + @Resource + private StudentRepository studentRepository; @Resource private AppraiseRecordRepository appraiseRecordRepository; @@ -130,7 +136,7 @@ public class AdminAppraiseServiceImpl implements AdminAppraiseService { String academicYearId = timeRangeDto.getAcademicYearId(); String schoolId = SecurityUtil.getLoginUser().getSchoolId(); - // fixme: 是否对时间范围做一些限制 + // fixme: 是否对时间范围做一些限制(不能确保当前周有数据) // 无参默认当前周 if (startTime == null || endTime == null) { // 将时间范围调整为当前周的周一到当前时间 @@ -150,6 +156,10 @@ public class AdminAppraiseServiceImpl implements AdminAppraiseService { endTime ); + if (res != null) { + res = res.stream().sorted((o1, o2) -> o2.getCreateTime().compareTo(o1.getCreateTime())).collect(Collectors.toList()); + } + return res; } @@ -167,6 +177,7 @@ public class AdminAppraiseServiceImpl implements AdminAppraiseService { startTime, endTime ); + if (ObjectUtils.isEmpty(rankPoList)) return null; Set classIdSet = rankPoList.stream().map(RankPo::getId).collect(Collectors.toSet()); // 注意: 如果查询 in 的查询集在数据库中不存在,则在结果集也不会为 null. @@ -307,4 +318,46 @@ public class AdminAppraiseServiceImpl implements AdminAppraiseService { return res; } + @Override + public List updateAchieveRule(UpdateAchievementRuleDto ruleDto) { + String periodId = ruleDto.getPeriodId(); + UpdateAchievementRuleDto.UpdateRule updateRule = ruleDto.getUpdateRule(); + // fixme: 判断 参数 + if (ObjectUtils.isEmpty(updateRule) || StringUtils.isBlank(updateRule.getId())) { + throw new ServiceException(ErrorCode.PARAMS_ERROR.getCode(), "rule id 不能为空"); + } + User user = SecurityUtil.getLoginUser(); + String schoolId = user.getSchoolId(); +// String schoolId = "template"; + + Appraise appraise = RepositoryUtil.findOne(appraiseRepository.findRulesById(schoolId, periodId), "参数错误,找不到该学段下的评价规则"); + List rules = appraise.getAchievementRules(); + if (ObjectUtils.isEmpty(rules)) { + throw new ServiceException(ErrorCode.OPERATION_ERROR.getCode(), "该学段暂无没有成就规则"); + } + // sort rules by level + rules = rules.stream().sorted(Comparator.comparing(AchievementRule::getLevel)).collect(Collectors.toList()); + boolean flag = false; + int lastPromotionCount = 0; + for (int i = 0, rulesSize = rules.size(); i < rulesSize; i++) { + AchievementRule rule = rules.get(i); + if (updateRule.getId().equals(rule.getId())) { + BeanUtils.copyProperties(updateRule, rule); + lastPromotionCount = rule.getLevelCount() * rule.getPromotionLevel(); + rule.setPromotionCount(lastPromotionCount); + flag = true; + continue; + } + // 处理后面的节点,将 promotionCount 依次修改 + if (flag) { + lastPromotionCount = lastPromotionCount + rule.getLevelCount() * rule.getPromotionLevel(); + rule.setPromotionCount(lastPromotionCount); + } + } + + CosmosPatchOperations operations = CosmosPatchOperations.create().replace("/achievementRules", rules); + Appraise saved = appraiseRepository.save(appraise.getId(), new PartitionKey(PK.PK_APPRAISE), Appraise.class, operations); + return saved.getAchievementRules(); + } + } diff --git a/src/main/java/cn/teammodel/controller/frontend/AiController.java b/src/main/java/cn/teammodel/controller/frontend/AiController.java new file mode 100644 index 0000000..45e05c6 --- /dev/null +++ b/src/main/java/cn/teammodel/controller/frontend/AiController.java @@ -0,0 +1,28 @@ +package cn.teammodel.controller.frontend; + +import cn.teammodel.model.dto.ai.ChatCompletionReqDto; +import cn.teammodel.service.ChatMessageService; +import io.swagger.annotations.ApiOperation; +import org.springframework.web.bind.annotation.*; +import org.springframework.web.servlet.mvc.method.annotation.SseEmitter; + +import javax.annotation.Resource; +import javax.validation.Valid; + +@RestController +@RequestMapping("/public/ai") +public class AiController { + @Resource + private ChatMessageService chatMessageService; + + @PostMapping("chat/completion") + @ApiOperation("与 spark 的流式对话") + public SseEmitter chatCompletion(@RequestBody @Valid ChatCompletionReqDto chatCompletionReqDto) { + return chatMessageService.chatCompletion(chatCompletionReqDto); + } + @GetMapping("test/completion") + @ApiOperation("与 spark 的流式对话") + public SseEmitter testChatCompletion() { + return chatMessageService.chatCompletion(null); + } +} \ No newline at end of file diff --git a/src/main/java/cn/teammodel/controller/AppraiseController.java b/src/main/java/cn/teammodel/controller/frontend/AppraiseController.java similarity index 86% rename from src/main/java/cn/teammodel/controller/AppraiseController.java rename to src/main/java/cn/teammodel/controller/frontend/AppraiseController.java index 310ae12..8d9ef45 100644 --- a/src/main/java/cn/teammodel/controller/AppraiseController.java +++ b/src/main/java/cn/teammodel/controller/frontend/AppraiseController.java @@ -1,9 +1,11 @@ -package cn.teammodel.controller; +package cn.teammodel.controller.frontend; +import cn.teammodel.common.IdRequest; import cn.teammodel.common.R; import cn.teammodel.model.dto.Appraise.*; import cn.teammodel.model.entity.appraise.Appraise; import cn.teammodel.model.vo.appraise.AppraiseRecordVo; +import cn.teammodel.model.vo.appraise.StudentReportVo; import cn.teammodel.service.EvaluationService; import io.swagger.annotations.ApiOperation; import org.springframework.web.bind.annotation.PostMapping; @@ -71,5 +73,13 @@ public class AppraiseController { return R.success(res); } + @PostMapping("studentReport") + @ApiOperation(value = "查看学生当前的学期的实时评价报告") + public R studentReport(@Valid @RequestBody IdRequest idRequest) { + StudentReportVo res = evaluationService.studentReport(idRequest); + return R.success(res); + } + + } diff --git a/src/main/java/cn/teammodel/controller/HelloController.java b/src/main/java/cn/teammodel/controller/frontend/HelloController.java similarity index 93% rename from src/main/java/cn/teammodel/controller/HelloController.java rename to src/main/java/cn/teammodel/controller/frontend/HelloController.java index c47665c..7e12a1c 100644 --- a/src/main/java/cn/teammodel/controller/HelloController.java +++ b/src/main/java/cn/teammodel/controller/frontend/HelloController.java @@ -1,32 +1,32 @@ -package cn.teammodel.controller; - -import cn.teammodel.common.R; -import cn.teammodel.dao.AppraiseRepository; -import org.springframework.security.access.prepost.PreAuthorize; -import org.springframework.security.core.context.SecurityContextHolder; -import org.springframework.web.bind.annotation.GetMapping; -import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.RestController; - -import javax.annotation.Resource; - -@RestController -@RequestMapping("/") -public class HelloController { - - @Resource - private AppraiseRepository appraiseRepository; - - @GetMapping("hello") - @PreAuthorize("@ss.hasRole('admin')") - public R hello() { - System.out.println(SecurityContextHolder.getContext().getAuthentication()); - - return new R(200, "success","hello world"); - } - @GetMapping("public/free") - @PreAuthorize("permitAll()") - public R free() { - return new R(200, "success","hello world"); - } +package cn.teammodel.controller.frontend; + +import cn.teammodel.common.R; +import cn.teammodel.dao.AppraiseRepository; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import javax.annotation.Resource; + +@RestController +@RequestMapping("/") +public class HelloController { + + @Resource + private AppraiseRepository appraiseRepository; + + @GetMapping("hello") + @PreAuthorize("@ss.hasRole('admin')") + public R hello() { + System.out.println(SecurityContextHolder.getContext().getAuthentication()); + + return new R(200, "success","hello world"); + } + @GetMapping("public/free") + @PreAuthorize("permitAll()") + public R free() { + return new R(200, "success","hello world"); + } } \ No newline at end of file diff --git a/src/main/java/cn/teammodel/dao/AppraiseRecordRepository.java b/src/main/java/cn/teammodel/dao/AppraiseRecordRepository.java index bf3fddd..2b16fb5 100644 --- a/src/main/java/cn/teammodel/dao/AppraiseRecordRepository.java +++ b/src/main/java/cn/teammodel/dao/AppraiseRecordRepository.java @@ -111,6 +111,7 @@ public interface AppraiseRecordRepository extends CosmosRepository studentRank(String code, String academicYearId, Long startTime, Long endTime); + // test script @Query("select * from Student as c where c.code = @code") List findByCode(String code); } diff --git a/src/main/java/cn/teammodel/dao/AppraiseRepository.java b/src/main/java/cn/teammodel/dao/AppraiseRepository.java index 388c73e..9fe115a 100644 --- a/src/main/java/cn/teammodel/dao/AppraiseRepository.java +++ b/src/main/java/cn/teammodel/dao/AppraiseRepository.java @@ -33,7 +33,8 @@ public interface AppraiseRepository extends CosmosRepository { List findTemplateTree(); @Query("SELECT value n FROM School AS s join n in s.nodes where s.code = @code and n.id = @nodeId") List findNodeById(@Param("code") String code, @Param("nodeId") String nodeId); - @Query("select n.id, n.name from School as c join n in c.nodes where c.code = @code and n.id in (@ids)") List findAllByCodeAndIdIn(String code, Set ids); + @Query("SELECT c.id, c.achievementRules FROM School AS c where c.code = 'Appraise' and c.schoolId = @schoolId and c.periodId = @periodId") + List findRulesById(String schoolId, String periodId); } diff --git a/src/main/java/cn/teammodel/dao/StudentRepository.java b/src/main/java/cn/teammodel/dao/StudentRepository.java index 82154e2..279a9b7 100644 --- a/src/main/java/cn/teammodel/dao/StudentRepository.java +++ b/src/main/java/cn/teammodel/dao/StudentRepository.java @@ -11,6 +11,8 @@ import java.util.Set; @Repository public interface StudentRepository extends CosmosRepository { + @Deprecated + // 似乎会出现 irs 的重复问题,建议使用下面的 findByIdAndCode Student findStudentByIdAndCode(String id, String code); @Query("select c.pk, c.code, c.id, c.name, c.gender, c.schoolId, c.periodId, c.year, c.createTime, c.picture, c.mail, c.mobile, c.country, c.classId, c.no, c.groupId, c.groupName, c.guardians, c.irs, c.salt from Student as c where c.id = @id and c.code = @code") diff --git a/src/main/java/cn/teammodel/model/dto/admin/UpdateAchievementRuleDto.java b/src/main/java/cn/teammodel/model/dto/admin/UpdateAchievementRuleDto.java new file mode 100644 index 0000000..fc78663 --- /dev/null +++ b/src/main/java/cn/teammodel/model/dto/admin/UpdateAchievementRuleDto.java @@ -0,0 +1,45 @@ +package cn.teammodel.model.dto.admin; + +import io.swagger.annotations.ApiModelProperty; +import lombok.Data; + +import javax.validation.constraints.NotNull; + +/** + * @author winter + * @create 2023-12-13 17:34 + */ +@Data +public class UpdateAchievementRuleDto { + @NotNull + @ApiModelProperty("学段 id") + private String periodId; + @ApiModelProperty("更新的 rule 节点: 将会直接覆盖老节点") + private UpdateRule updateRule; + + @Data + public static class UpdateRule { + @NotNull + private String id; + /** + * 等级名称 + */ + @NotNull + private String name; + /** + * 等级 logo + */ + @NotNull + private String logo; + /** + * 每次所需表扬数 + */ + @NotNull + private Integer levelCount; + /** + * 晋级所需下一等级所需当前等级次数 + */ + @NotNull + private Integer promotionLevel; + } +} diff --git a/src/main/java/cn/teammodel/model/dto/ai/ChatCompletionReqDto.java b/src/main/java/cn/teammodel/model/dto/ai/ChatCompletionReqDto.java new file mode 100644 index 0000000..c96d4fe --- /dev/null +++ b/src/main/java/cn/teammodel/model/dto/ai/ChatCompletionReqDto.java @@ -0,0 +1,16 @@ +package cn.teammodel.model.dto.ai; + +import lombok.Data; + +import javax.validation.constraints.NotBlank; + +@Data +public class ChatCompletionReqDto { + private Long sessionId; + /** + * 预设的会话面具 + */ + private Long appId; + @NotBlank(message = "请输入消息内容") + private String text; +} \ No newline at end of file diff --git a/src/main/java/cn/teammodel/model/entity/appraise/AchievementRule.java b/src/main/java/cn/teammodel/model/entity/appraise/AchievementRule.java new file mode 100644 index 0000000..11c7da5 --- /dev/null +++ b/src/main/java/cn/teammodel/model/entity/appraise/AchievementRule.java @@ -0,0 +1,38 @@ +package cn.teammodel.model.entity.appraise; + +import lombok.Data; + +/** + * 成就的晋级规则 + * @author winter + * @create 2023-12-13 15:23 + */ +@Data +public class AchievementRule { + private String id; + /** + * 等级名称 + */ + private String name; + /** + * 等级 logo + */ + private String logo; + /** + * 等级顺序 + */ + private Integer level; + /** + * 每次所需表扬数 + */ + private Integer levelCount; + /** + * 晋级所需下一等级所需当前等级次数 + */ + private Integer promotionLevel; + /** + * 晋升到下一等级所需总表扬树 + */ + private Integer promotionCount; + +} diff --git a/src/main/java/cn/teammodel/model/entity/appraise/Appraise.java b/src/main/java/cn/teammodel/model/entity/appraise/Appraise.java index 5396291..b6e812c 100644 --- a/src/main/java/cn/teammodel/model/entity/appraise/Appraise.java +++ b/src/main/java/cn/teammodel/model/entity/appraise/Appraise.java @@ -26,7 +26,12 @@ public class Appraise extends BaseItem { * 学段 id (默认 default, 模板为 template) */ private String periodId; - + /** + * 评价树的扁平化节点 + */ private List nodes; - + /** + * 成就等级节点 + */ + private List achievementRules; } diff --git a/src/main/java/cn/teammodel/model/entity/school/SchoolConfig.java b/src/main/java/cn/teammodel/model/entity/school/SchoolConfig.java new file mode 100644 index 0000000..fbd9de8 --- /dev/null +++ b/src/main/java/cn/teammodel/model/entity/school/SchoolConfig.java @@ -0,0 +1,16 @@ +package cn.teammodel.model.entity.school; + +import lombok.Data; + +/** + * 学校的配置项 + * @author winter + * @create 2023-12-13 11:44 + */ +@Data +public class SchoolConfig { + /** + * 学校积分名字 + */ + private String scoreName = "醍摩豆"; +} diff --git a/src/main/java/cn/teammodel/model/vo/appraise/StudentReportVo.java b/src/main/java/cn/teammodel/model/vo/appraise/StudentReportVo.java new file mode 100644 index 0000000..269f155 --- /dev/null +++ b/src/main/java/cn/teammodel/model/vo/appraise/StudentReportVo.java @@ -0,0 +1,20 @@ +package cn.teammodel.model.vo.appraise; + +import cn.teammodel.model.entity.appraise.AchievementRule; +import lombok.Data; + +import java.util.Map; + +/** + * 学生个人评价 vo + * @author winter + * @create 2023-12-04 15:26 + */ +@Data +public class StudentReportVo { + private Integer praiseCount; + private Integer score; + private Map praiseDistribution; + private Map criticalDistribution; + private AchievementRule curAchievement; +} diff --git a/src/main/java/cn/teammodel/service/ChatMessageService.java b/src/main/java/cn/teammodel/service/ChatMessageService.java new file mode 100644 index 0000000..6cc867e --- /dev/null +++ b/src/main/java/cn/teammodel/service/ChatMessageService.java @@ -0,0 +1,15 @@ +package cn.teammodel.service; + +import cn.teammodel.model.dto.ai.ChatCompletionReqDto; +import org.springframework.web.servlet.mvc.method.annotation.SseEmitter; + +/** + * @author winter + * @create 2023-12-18 15:20 + */ +public interface ChatMessageService { + /** + * AI 聊天 + */ + SseEmitter chatCompletion(ChatCompletionReqDto chatCompletionReqDto); +} diff --git a/src/main/java/cn/teammodel/service/EvaluationService.java b/src/main/java/cn/teammodel/service/EvaluationService.java index 97936a1..8df8500 100644 --- a/src/main/java/cn/teammodel/service/EvaluationService.java +++ b/src/main/java/cn/teammodel/service/EvaluationService.java @@ -1,9 +1,11 @@ package cn.teammodel.service; +import cn.teammodel.common.IdRequest; import cn.teammodel.model.dto.Appraise.*; import cn.teammodel.model.entity.appraise.Appraise; import cn.teammodel.model.entity.appraise.AppraiseTreeNode; import cn.teammodel.model.vo.appraise.AppraiseRecordVo; +import cn.teammodel.model.vo.appraise.StudentReportVo; import java.util.List; @@ -52,4 +54,9 @@ public interface EvaluationService { List findVoteRecord(FindVoteRecordDto findVoteRecordDto); void recallVote(RecallVoteDto recallVoteDto); + + /** + * 学生评价报告 + */ + StudentReportVo studentReport(IdRequest idRequest); } diff --git a/src/main/java/cn/teammodel/service/impl/ChatMessageServiceImpl.java b/src/main/java/cn/teammodel/service/impl/ChatMessageServiceImpl.java new file mode 100644 index 0000000..d695fc7 --- /dev/null +++ b/src/main/java/cn/teammodel/service/impl/ChatMessageServiceImpl.java @@ -0,0 +1,69 @@ +package cn.teammodel.service.impl; + +import cn.teammodel.ai.SparkGptClient; +import cn.teammodel.ai.SseHelper; +import cn.teammodel.ai.domain.SparkChatRequestParam; +import cn.teammodel.ai.listener.SparkGptStreamListener; +import cn.teammodel.model.dto.ai.ChatCompletionReqDto; +import cn.teammodel.model.entity.User; +import cn.teammodel.security.utils.SecurityUtil; +import cn.teammodel.service.ChatMessageService; +import com.google.common.collect.Lists; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; +import org.springframework.web.servlet.mvc.method.annotation.SseEmitter; + +import javax.annotation.Resource; +import java.util.List; + +/** + * @author winter + * @create 2023-12-18 15:20 + */ +@Service +@Slf4j +public class ChatMessageServiceImpl implements ChatMessageService { + @Resource + private SparkGptClient sparkGptClient; + + @Override + public SseEmitter chatCompletion(ChatCompletionReqDto chatCompletionReqDto) { + // 目前仅使用讯飞星火大模型 + User user = SecurityUtil.getLoginUser(); + String userId = user.getId(); + String text = chatCompletionReqDto.getText(); +// String userId = "123"; +// String text = "hello, how should I call you?"; + SseEmitter sseEmitter = new SseEmitter(-1L); + SparkGptStreamListener listener = new SparkGptStreamListener(sseEmitter); + + // open 回调 + listener.setOnOpen((s) -> { + // 敏感词检查,计费 + log.info("callback: ws open event emmit"); + }); + // 对话完成的回调 + listener.setOnComplete((s) -> { + log.info("callback: ws complete event emmit"); + SseHelper.send(sseEmitter, "[DONE]"); + // 处理完成后的事件: 保存消息记录 + }); + // 错误的回调 + listener.setOnError((s) -> { + log.error("callback: ws error" ); + // 返还积分 + }); + // todo: 拉取对话上下文 + List messageList = Lists.newArrayList(); + messageList.add(SparkChatRequestParam.Message.ofUser(text)); + // todo: sessionId + SparkChatRequestParam requestParam = SparkChatRequestParam + .builder() + .uid(userId) + .chatId("123") + .messageList(messageList) + .build(); + sparkGptClient.streamChatCompletion(requestParam, listener); + return sseEmitter; + } +} diff --git a/src/main/java/cn/teammodel/service/impl/EvaluationServiceImpl.java b/src/main/java/cn/teammodel/service/impl/EvaluationServiceImpl.java index 92b7aa4..db26386 100644 --- a/src/main/java/cn/teammodel/service/impl/EvaluationServiceImpl.java +++ b/src/main/java/cn/teammodel/service/impl/EvaluationServiceImpl.java @@ -2,19 +2,18 @@ package cn.teammodel.service.impl; import cn.hutool.core.lang.UUID; import cn.teammodel.common.ErrorCode; +import cn.teammodel.common.IdRequest; import cn.teammodel.common.PK; import cn.teammodel.config.exception.ServiceException; import cn.teammodel.dao.*; import cn.teammodel.model.dto.Appraise.*; import cn.teammodel.model.entity.User; -import cn.teammodel.model.entity.appraise.Appraise; -import cn.teammodel.model.entity.appraise.AppraiseRecord; -import cn.teammodel.model.entity.appraise.AppraiseRecordItem; -import cn.teammodel.model.entity.appraise.AppraiseTreeNode; +import cn.teammodel.model.entity.appraise.*; import cn.teammodel.model.entity.school.ClassInfo; import cn.teammodel.model.entity.school.School; import cn.teammodel.model.entity.school.Student; import cn.teammodel.model.vo.appraise.AppraiseRecordVo; +import cn.teammodel.model.vo.appraise.StudentReportVo; import cn.teammodel.security.utils.SecurityUtil; import cn.teammodel.service.EvaluationService; import cn.teammodel.utils.RepositoryUtil; @@ -82,7 +81,6 @@ public class EvaluationServiceImpl implements EvaluationService { User loginUser = SecurityUtil.getLoginUser(); String schoolId = loginUser.getSchoolId(); Appraise appraise = appraiseRepository.findAppraiseBySchoolIdAndPeriodIdAndCode(schoolId, periodId, PK.PK_APPRAISE); - // todo: 是否要对学段进行鉴权 if (appraise != null) { return this.buildTree(appraise); } @@ -91,7 +89,8 @@ public class EvaluationServiceImpl implements EvaluationService { if (appraise == null) { throw new ServiceException(); } - refreshAppraiseTree(appraise.getNodes()); + // refresh + refreshAppraiseTree(appraise); appraise.setPeriodId(periodId); appraise.setSchoolId(schoolId); appraise.setId(null); @@ -99,10 +98,13 @@ public class EvaluationServiceImpl implements EvaluationService { return this.buildTree(appraise); } + /** - * 将树的节点的 id 进行替换(天才) + * 将 appraise 进行替换(天才) */ - private void refreshAppraiseTree(List nodes) { + private void refreshAppraiseTree(Appraise appraise) { + List nodes = appraise.getNodes(); + List rules = appraise.getAchievementRules(); List children = nodes.stream().filter(item -> item.getPid() != null).collect(Collectors.toList()); // 将非 root 的 nodes 通过 pid 收集成 list, 再遍历 nodes, 将其每一个 id 对应 map 中的 list 的 pid修改成新的 id 即可 Map> pidNodeMap = children.stream().collect(Collectors.groupingBy(AppraiseTreeNode::getPid)); @@ -118,6 +120,8 @@ public class EvaluationServiceImpl implements EvaluationService { item.setCreator("template"); }); + // refresh rules + rules.forEach(item -> item.setId(UUID.randomUUID().toString())); } @Override @@ -328,9 +332,10 @@ public class EvaluationServiceImpl implements EvaluationService { } else { CosmosPatchOperations operations = CosmosPatchOperations.create(); operations.add("/nodes/0", item); - // 表扬 - long praise = appraiseTreeNode.isPraise() ? 1 : -1; - operations.increment("/praiseCount", praise); + // 表扬 (待改进不会减少表扬数) + if (appraiseTreeNode.isPraise()) { + operations.increment("/praiseCount", 1); + } // 加分 int scoreToPlus = ObjectUtils.isEmpty(appraiseTreeNode.getScore()) ? 0 : appraiseTreeNode.getScore(); operations.increment("/score", scoreToPlus); @@ -401,6 +406,67 @@ public class EvaluationServiceImpl implements EvaluationService { appraiseRecordRepository.save(appraiseRecord); } + @Override + public StudentReportVo studentReport(IdRequest idRequest) { + String studentId = idRequest.getId(); + User user = SecurityUtil.getLoginUser(); + String schoolId = user.getSchoolId(); + // 查询学生信息,学段等 + Student student = RepositoryUtil.findOne(studentRepository.findByIdAndCode(studentId, String.format(PK.STUDENT, schoolId)), "当前学生不存在"); + String periodId = student.getPeriodId(); + String classId = student.getClassId(); + List semesters = schoolRepository.findSemestersById(schoolId, periodId); + // 生成学年 id + String academicYearId = SchoolDateUtil.calculateAcademicYearId(semesters, LocalDate.now()); + // 查询评价文档 + AppraiseRecord appraiseRecord = appraiseRecordRepository.findAppraiseRecordByTargetIdAndClassIdAndAcademicYearIdAndCode( + studentId, + classId, + academicYearId, + String.format(PK.PK_APPRAISE_RECORD, schoolId) + ); + if (appraiseRecord == null || appraiseRecord.getNodes() == null) { + return null; + } + List records = appraiseRecord.getNodes(); + // 查询成就规则 + Appraise appraise = RepositoryUtil.findOne(appraiseRepository.findRulesById(schoolId, periodId), "当前成就规则还未创建"); + List rules = appraise.getAchievementRules(); + StudentReportVo reportVo = new StudentReportVo(); + // 计算雷达图 + Map praiseDistribution = new HashMap<>(); + Map criticalDistribution = new HashMap<>(); + for (AppraiseRecordItem record : records) { + AppraiseTreeNode appraiseNode = record.getAppraiseNode(); + String[] path = appraiseNode.getPath(); + String root = path[0]; + if (appraiseNode.isPraise()) { + praiseDistribution.put(root, praiseDistribution.getOrDefault(root, 0) + 1); + } else { + criticalDistribution.put(root, criticalDistribution.getOrDefault(root, 0) + 1); + } + } + // 计算成就项 (排序 + rules = rules.stream().sorted(Comparator.comparing(AchievementRule::getLevel).reversed()).collect(Collectors.toList()); + Integer praiseCount = appraiseRecord.getPraiseCount(); + AchievementRule curAchievement = rules.get(0); + for (AchievementRule rule : rules) { + Integer promotionCount = rule.getPromotionCount(); + int flag = praiseCount / promotionCount; + // 说明当前规则成就匹配 + if (flag >= 1) { + curAchievement = rule; + break; + } + } + reportVo.setPraiseCount(appraiseRecord.getPraiseCount()); + reportVo.setScore(appraiseRecord.getScore()); + reportVo.setPraiseDistribution(praiseDistribution); + reportVo.setCriticalDistribution(criticalDistribution); + reportVo.setCurAchievement(curAchievement); + return reportVo; + } + /** * 递归收集 id 的节点及 id 节点的孩子节点 (迭代器删除居然也报错) */ diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index f22fa9e..7ab4ee1 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -17,8 +17,6 @@ spring: key: JTUVk92Gjsx17L0xqxn0X4wX2thDPMKiw4daeTyV1HzPb6JmBeHdtFY1MF1jdctW1ofgzqkDMFOtcqS46by31A== populate-query-metrics: true - - security: oauth2: resourceserver: @@ -26,6 +24,14 @@ spring: issuer-uri: https://login.partner.microsoftonline.cn/4807e9cf-87b8-4174-aa5b-e76497d7392b/v2.0 audiences: 72643704-b2e7-4b26-b881-bd5865e7a7a5 +spark: + gpt: + endpoint: https://spark-api.xf-yun.com/v3.1/chat + appId: c49d1e24 + apiKey: 6c586e7dd1721ed1bb19bdb573b4ad34 + apiSecret: MDU1MTU1Nzg4MDg2ZTJjZWU3MmI4ZGU1 + + jwt: secret: fXO6ko/qyXeYrkecPeKdgXnuLXf9vMEtnBC9OB3s+aA= diff --git a/src/test/java/cn/teammodel/TeamModelExtensionApplicationTests.java b/src/test/java/cn/teammodel/TeamModelExtensionApplicationTests.java index 7062405..064994d 100644 --- a/src/test/java/cn/teammodel/TeamModelExtensionApplicationTests.java +++ b/src/test/java/cn/teammodel/TeamModelExtensionApplicationTests.java @@ -8,10 +8,8 @@ import cn.teammodel.dao.SchoolRepository; import cn.teammodel.dao.StudentRepository; import cn.teammodel.manager.DingAlertNotifier; import cn.teammodel.model.dto.admin.TimeRangeDto; -import cn.teammodel.model.entity.appraise.Appraise; -import cn.teammodel.model.entity.appraise.AppraiseRecord; -import cn.teammodel.model.entity.appraise.AppraiseRecordItem; -import cn.teammodel.model.entity.appraise.AppraiseTreeNode; +import cn.teammodel.model.dto.admin.UpdateAchievementRuleDto; +import cn.teammodel.model.entity.appraise.*; import cn.teammodel.model.entity.school.School; import cn.teammodel.model.vo.admin.RankPo; import cn.teammodel.service.EvaluationService; @@ -207,8 +205,16 @@ class TeamModelExtensionApplicationTests { } } -// @Test -// public void batchUpdateTimeFormat() { -// -// } + @Test + public void batchAppraiseSelect() { + UpdateAchievementRuleDto ruleDto = new UpdateAchievementRuleDto(); + ruleDto.setPeriodId("template"); + AchievementRule rule = new AchievementRule(); + rule.setId("1"); + rule.setLevelCount(10); + rule.setPromotionLevel(5); + rule.setLogo("https://www.baidu.com"); +// ruleDto.setUpdateRule(rule); + System.out.println(adminAppraiseService.updateAchieveRule(ruleDto)); + } }