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));
+ }
}