ChatGLM关于DDD的理解

ChatGLM

前端react,后端spring boot DDD

DDD架构

应用程序层

应用程序层主要存放配置文件,redis配置,线程池配置,支付宝支付配置。

除此之外还有项目主启动类,以及yml文件。

基础/仓储设施层

该层主要构建数据库所需的各种接口查询实体类等。在这个GLM项目中,将DAO层和数据库对应的实体类PO,以及仓储实现类,以及redis实现类和接口。

就最后的仓储和redis来解释,首先各个仓储的实现类放在这里集中管理,而仓储的对外接口则放在domain的对应领域下,基础设施层只负责实现。其次,redis不属于任何领域中,并且是通用的服务,所以需要定义在基础设施层。

值对象层

这里面定义了各个领域,也可以说整个项目所需要的基础类,比如response,ChatGLMModel,Exception等等。

领域层

这里是DDD架构的主要部分,各个功能分别在各个领域中,方便后续项目添加功能,也方便查找某一个功能对其进行修改。

比如在openAi领域中,我们需要定义自定义注解,他所用到的实体类,对于这个实体类来讲,因为各个领域之间是独立的,所以我们不能在一个领域中调用另一个领域中的实体类,当有一个实体类在两个领域中都会多次使用,建议把这个实体类创建到type,即值对象层。

除此之外,定义的实体类中要有值对象,值对象其实就是枚举类,里面有需要的枚举类型,以及状态码和信息。

然后就是基础的实体类的定义,其次就是聚合对象的定义,这个聚合对象,里面是某个对象的聚合,比如用户向GLM询问问题,一般用户的问题不是一次就问完的,而是一问一答的形式,我们将这些用户信息聚合起来,为其分配id和其他信息。

然后就是仓储接口的定义,这是这个领域下,可以调用这些接口对数据库进行操作,而其实现类则在基础设施层,是唯一一个向外暴露的点。

最后就是这个领域的服务实现和接口定义了。

可以在这个编写各种服务所需的代码,在openAi这个领域下,先实现了工厂类,这个是规则工厂,里面是各种规则的配置使用,我们还定义了一个过滤器接口,然后定义各种实现类,都继承这个过滤器接口,不同的实现类里面可以写属于自己的过滤规则实现代码,最后规则工厂统一使用。

然后是服务接口,触发器层可以调用这个接口,然后定义抽象类去实现这个接口,这里就是抽象逻辑的编写了,我们可以在这里面写出大概的逻辑框架也可以在这个里面调用仓储接口实现数据的更改,但将具体的主要的实现细节需要封装起来到另外一个类中。

这时候,我们再写一个实现类去扩展(extends)这个抽象类,把里面所写的抽象方法重写一遍,把服务的具体实现细节都写在这个的里面,我们可以在这时候调用我们定义的规则工厂,把这个过滤认证什么的规则全部加进来。

触发器层

这里首先需要定义我们需要的DTO对象,分别对应着各个领域,然后就是Controller的编写了,这里的controller要相互分清各自的层次,不要全部都写一起。每个不同的controller不是只可以调用属于自己的那个领域的服务,可以调用其他的。

但是啊,这里面传给领域服务的对象,只能是自己定义的DTO对象。

对于这个GLM项目,我在触发器层还定义了各种定时任务,比如检测未接收到或未正确处理的支付回调通知、订单补货任务、超时关单任务。

最后,还定义了监听订单支付成功失败的订阅事件。

依赖

根依赖就是项目所需要的全部依赖。

对于值对象层,只定义这个层里面需要的依赖即可。

对于触发器层,因为触发器层需要用到领域domain和值对象type层,所以这一层中,除了自己需要的一些基本依赖外,还要引入domain和type。

对于基础设施层,这一层被domain调用,所以要用到domain的依赖,把domain的依赖导入。

对于领域层,这一层主要调用了type,所以把type和自己需要的引入即可,但是,我们可以看到domain也有关于基础设施层的接口,为什么不引入基础设施层的依赖呢?这里就用到了依赖倒置,domain里面只是写了仓储接口,但具体实现类还在基础设施层中......(只可意会不可言传)。

对于应用程序层,这一层的依赖中,把自己在配置文件中需要的依赖都导入,然后把触发器层和基础设施层导入即可。

SDK

主要包含拦截器、请求信息封装实体类、会话工厂、配置信息、创建接口。

在SDK中定义了ChatCompletionRequest(请求参数)、ChatCompletionResponse(返回结果)、EventType(消息类型)、Model(会话模型)、Role(角色)。

配置信息中设置url请求地址、密钥,还有okHttp配置信息:连接时间、读写时间。以及常量的定义。

拦截器把调用的api请求拦截,在这个请求中构建Authorization以及一些基本信息,执行,返回执行结果。

@Override
    public @NotNull Response intercept(Chain chain) throws IOException {
        // 1. 获取原始 Request
        Request original = chain.request();
​
        // 2. 构建请求
        Request request = original.newBuilder()
                .url(original.url())
                .header("Authorization", "Bearer " + BearerTokenUtils.getToken(configuration.getApiKey(), configuration.getApiSecret()))
                .header("Content-Type", Configuration.JSON_CONTENT_TYPE)
                .header("User-Agent", Configuration.DEFAULT_USER_AGENT)
                .header("Accept", Configuration.SSE_CONTENT_TYPE)
                .method(original.method(), original.body())
                .build();
​
        // 3. 返回执行结果
        return chain.proceed(request);
    }

会话工厂中配置日志、开启http客户端、创建API服务。封装工厂,提供接口。

@Override
    public OpenAiSession openSession() {
        // 1. 日志配置
        HttpLoggingInterceptor httpLoggingInterceptor = new HttpLoggingInterceptor();
        httpLoggingInterceptor.setLevel(configuration.getLevel());
​
        // 2. 开启 Http 客户端
        OkHttpClient okHttpClient = new OkHttpClient
                .Builder()
                .addInterceptor(httpLoggingInterceptor)
                .addInterceptor(new OpenAiHTTPInterceptor(configuration))
                .connectTimeout(configuration.getConnectTimeout(), TimeUnit.SECONDS)
                .writeTimeout(configuration.getWriteTimeout(), TimeUnit.SECONDS)
                .readTimeout(configuration.getReadTimeout(), TimeUnit.SECONDS)
                .build();
​
        configuration.setOkHttpClient(okHttpClient);
​
        // 3. 创建 API 服务
        IOpenAiApi openAiApi = new Retrofit.Builder()
                .baseUrl(configuration.getApiHost())
                .client(okHttpClient)
                .addCallAdapterFactory(RxJava2CallAdapterFactory.create())
                .addConverterFactory(JacksonConverterFactory.create())
                .build().create(IOpenAiApi.class);
​
        configuration.setOpenAiApi(openAiApi);
​
        return new DefaultOpenAiSession(configuration);
    }

会话服务中构建请求信息,返回时间结果。封装,提供接口。

    public DefaultOpenAiSession(Configuration configuration) {
        this.configuration = configuration;
        this.openAiApi = configuration.getOpenAiApi();
        this.factory = configuration.createRequestFactory();
    }
​
    @Override
    public EventSource completions(ChatCompletionRequest chatCompletionRequest, EventSourceListener eventSourceListener) throws JsonProcessingException {
        // 构建请求信息
        Request request = new Request.Builder()
                .url(configuration.getApiHost().concat(IOpenAiApi.v3_completions).replace("{model}", chatCompletionRequest.getModel().getCode()))
                .post(RequestBody.create(MediaType.parse("application/json"), chatCompletionRequest.toString()))
                .build();
​
        // 返回事件结果
        return factory.newEventSource(request, eventSourceListener);
    }

签名工具包,过期时间30分钟,缓存29分钟,登录创建apikey,用户登录时从缓存中查找token,如果有就直接返回,如果没有就创建token,使用HMAC256算法,根据apisecret创建,最后JWT工具类创建token并存入缓存。

项目流程

用户登录

  1. 用户关注公众号获取验证码,这时候要配置微信公众号,提供get、post方法,配置内网穿透,配置基本信息。

  2. 后端生成验证码并存入redis缓存中,然后根据openId+验证码绑定进行登录操作,为用户创建一个有效期为30分钟的Token,用户开始对话

开始对话

  1. 用户登录之后,选择模型开始对话,这时候要判断用户的账户额度,在这里配置了规则工厂,在调用模型应答接口的时候进行权限判断,然后是次数校验。

  2. 没额度了就去买额度,用户对额度商品进行下单操作,这里使用的是支付宝SDK,用户进行扫码支付,充值余额。

  3. 用户冲完钱了,再次选择模型对话,complaintions接口将再次使用规则工厂进行过滤校验等操作

  4. 校验失败:模型开始对话,不过是说你没钱了,或者封装错误信息进行回答。

  5. 校验成功:模型开始对话,这里可以优化为多渠道AI,但目前只有GLM(因为api接口最容易访问),并且,现在只有文生文,文生图还未开发。

应用程序层

配置ChatGLM的基本信息

  1. 调用chatglmSDK,设置apihost,secretKey。

  2. 创建会话工厂,开启会话

创建配置实体类

  1. apiHost\apisecretKey\enable

  2. 从配置文件中获取值

缓存配置

使用GuavaCache,将缓存信息存入本地主机中,redis需要单独的1C1G,占用了太多服务器内存

  1. 创建缓存实例

  2. 设置过期时间为3分钟

  3. 返回缓存实例

支付宝配置

  1. 配置支付宝客户端信息

注册监听器enventBus

普罗米修斯(Prometheus)监控配置

Redis配置

敏感词配置

  1. Bean方法:sensitiveWords,配置基本信息

线程池配置

配置文件

server:
  port: 8091
​
# 应用配置
app:
  config:
    # 版本,方便通过接口版本升级
    api-version: v1
    # 跨域,开发阶段可以设置为 * 不限制
    cross-origin: '*'
    # 访问频次限制
    limit-count: 10
    # 白名单,不做频次拦截【微信的openai】oxfA9w8-23yvwTmo2ombz0E4zJv4
    white-list: ojbZUv18lbmriaTjcCWBYkOrSbHA
​
# 线程池配置
thread:
  pool:
    executor:
      config:
        core-pool-size: 20
        max-pool-size: 50
        keep-alive-time: 5000
        block-queue-size: 5000
        policy: CallerRunsPolicy
​
# ChatGLM SDK Config
chatglm:
  sdk:
    config:
      # 状态;true = 开启、false 关闭
      enabled: true
      # 官网地址
      api-host: https://open.bigmodel.cn/
      # 官网申请 https://open.bigmodel.cn/usercenter/apikeys - 自己可申请
      api-key: 8fb21e517a965b10cf87b7fdadf18a74.UVLbxTJXdL3YLxcT
​
# 微信公众号配置信息
# originalid:原始ID
# appid:个人AppID
# token:开通接口服务自定义设置
wx:
  config:
    originalid: gh_c5ce6e4a0e0e
    appid: wxad979c0307864a66
    token: b8b6
​
​
# 日志
logging:
  level:
    root: info
  config: classpath:logback-spring.xml

测试类

流式应答测试

  1. 注入openaiSession

  2. 构建参数

  3. 发起请求

  4. 获取响应结果

  5. 日志输出

基础设施层

这一层里面主要封装了需要的基础实体类和工具类

  1. 首先是告警信息Constans,这个里面封装了一个枚举类ResponseCode

 public final static String SPLIT = ",";
​
    @AllArgsConstructor
    @NoArgsConstructor
    @Getter
    public enum ResponseCode {
        SUCCESS("0000", "成功"),
        UN_ERROR("0001", "未知失败"),
        ILLEGAL_PARAMETER("0002", "非法参数"),
        TOKEN_ERROR("0003", "权限拦截"),
        ORDER_PRODUCT_ERR("OE001", "所购商品已下线,请重新选择下单商品"),
        ;
​
        private String code;
        private String info;
  1. 然后是枚举类

    1. 第一个是模型

      CHATGLM_6B_SSE("chatGLM_6b_SSE"),
          CHATGLM_LITE("chatglm_lite"),
          CHATGLM_LITE_32K("chatglm_lite_32k"),
          CHATGLM_STD("chatglm_std"),
          CHATGLM_PRO("chatglm_pro"),
      ​
          ;
          private final String code;
      ​
          public static ChatGLMModel get(String code){
              switch (code){
                  case "chatGLM_6b_SSE":
                      return ChatGLMModel.CHATGLM_6B_SSE;
                  case "chatglm_lite":
                      return ChatGLMModel.CHATGLM_LITE;
                  case "chatglm_lite_32k":
                      return ChatGLMModel.CHATGLM_LITE_32K;
                  case "chatglm_std":
                      return ChatGLMModel.CHATGLM_STD;
                  case "chatglm_pro":
                      return ChatGLMModel.CHATGLM_PRO;
                  default:
                      return ChatGLMModel.CHATGLM_6B_SSE;
              }
    2. openAi的渠道

          ChatGLM("ChatGLM"),
          ChatGPT("ChatGPT"),
      ​
          ;
          private final String code;
      ​
          public static OpenAiChannel getChannel(String model) {
              if (model.toLowerCase().contains("gpt")) return OpenAiChannel.ChatGPT;
              if (model.toLowerCase().contains("glm")) return OpenAiChannel.ChatGLM;
              return null;
          }
    3. 模型启用信息

      CLOSE(0, "无效,已关闭"),
          OPEN(1,"有效,使用中"),
          ;
      ​
          private final Integer code;
      ​
          private final String info;
      ​
          public static OpenAIProductEnableModel get(Integer code){
              switch (code){
                  case 0:
                      return OpenAIProductEnableModel.CLOSE;
                  case 1:
                      return OpenAIProductEnableModel.OPEN;
                  default:
                      return OpenAIProductEnableModel.CLOSE;
              }
          }
  2. 自定义异常

     /**
         * 异常码
         */
        private String code;
    ​
        /**
         * 异常信息
         */
        private String message;
  3. 模型

    private String code;
        private String info;
        private T data;
  4. 微信实体

    1. 文章类

      private String title;
          private String description;
          private String picUrl;
          private String url;
    2. 签名工具类

      在微信服务中,就是调用了这里的check方法实现验证签名

      public static boolean check(String token, String signature, String timestamp, String nonce) {
              String[] arr = new String[]{token, timestamp, nonce};
              // 将token、timestamp、nonce三个参数进行字典序排序
              sort(arr);
              StringBuilder content = new StringBuilder();
              for (String s : arr) {
                  content.append(s);
              }
              MessageDigest md;
              String tmpStr = null;
              try {
                  md = MessageDigest.getInstance("SHA-1");
                  // 将三个参数字符串拼接成一个字符串进行sha1加密
                  byte[] digest = md.digest(content.toString().getBytes());
                  tmpStr = byteToStr(digest);
              } catch (NoSuchAlgorithmException e) {
                  e.printStackTrace();
              }
              // 将sha1加密后的字符串可与signature对比,标识该请求来源于微信
              return tmpStr != null && tmpStr.equals(signature.toUpperCase());
          }
    3. 微信数据解析工具类

      这里主要对微信发来的和发给微信的信息进行xml解析和构建

      /**
           * bean转成微信的xml消息格式
           */
          public static String beanToXml(Object object) {
              XStream xStream = getMyXStream();
              xStream.alias("xml", object.getClass());
              xStream.processAnnotations(object.getClass());
              String xml = xStream.toXML(object);
              if (!StringUtils.isEmpty(xml)) {
                  return xml;
              } else {
                  return null;
              }
          }
      ​
          /**
           * xml转成bean泛型方法
           */
          public static <T> T xmlToBean(String resultXml, Class clazz) {
              // XStream对象设置默认安全防护,同时设置允许的类
              XStream stream = new XStream(new DomDriver());
              XStream.setupDefaultSecurity(stream);
              stream.allowTypes(new Class[]{clazz});
              stream.processAnnotations(new Class[]{clazz});
              stream.setMode(XStream.NO_REFERENCES);
              stream.alias("xml", clazz);
              return (T) stream.fromXML(resultXml);
          }

领域层

用户领域层

实体类
  1. AuthSateEntity 鉴权结果

    • code 四位验证码(接入微信公众号获取的)

    • info 信息

    • openId 用户代码

    • token 用户访问携带token

  2. AuthTypeVO

    • A0000 验证成功

    • A0001 验证码不存在

    • A0002 验证码无效

服务

主要是两个接口的实现:doLogin接口,用户的登录验证。checkToken接口,检测token是否合法,是否过期。

  1. IAuthService

        AuthStateEntity doLogin(String code);
        boolean checkToken(String token);
  2. AbstractAuthService

    在这个实现方法中,定义三个不可更改的量

    1. defaultBase64EncodedSecretKey = "B*B^D%fe";这个密钥最好通过配置的方式改为自己的。

    2. base64EncodedSecretKey = Base64.encodeBase64String(defaultBase64EncodedSecretKey.getBytes());

    3. Algorithm algorithm = Algorithm.HMAC256(Base64.decodeBase64(Base64.encodeBase64String(defaultBase64EncodedSecretKey.getBytes())));

      对于doLogin实现方法首先需要验证传过来的code(四位验证码)

    //如果不是4位有效数字字符串,则返回验证码无效
            if (!code.matches("\\d{4}")){
                log.info("鉴权,用户收入的验证码无效 {}", code);
                return AuthStateEntity.builder()
                        .code(AuthTypeVO.A0002.getCode())
                        .info(AuthTypeVO.A0002.getInfo())
                        .build();
            }

    ​ 使用checkCode()方法对code进行校验,失败就返回状态。

    ​ 获取用户token并返回,这个token,我们将用openid和时间来生成

    我们写一个方法encode来生成它

    /**
         * 生成jwt字符串
         * @param issuer 签发人
         * @param ttlMillis 生存时间
         * @param claims 需要在jwt中存储的一些非隐私信息
         * @return
         */
        private String encode(String issuer, long ttlMillis, Map<String, Object> claims) {
            if (claims==null){
                claims = new HashMap<>();
            }
            long nowMillis = System.currentTimeMillis();
    ​
            JwtBuilder builder = Jwts.builder()
                    //荷载部分
                    .setClaims(claims)
                    //这个是JWT唯一标识,一般设置成唯一的
                    .setId(UUID.randomUUID().toString())
                    //签发时间
                    .setIssuedAt(new Date(nowMillis))
                    //签发人
                    .setSubject(issuer)
                    .signWith(SignatureAlgorithm.HS256,base64EncodedSecretKey);//生成jwt使用的算法和密钥
            if (ttlMillis>=0){
                long expMillis = nowMillis+ ttlMillis;
                Date exp = new Date(expMillis);
                builder.setExpiration(exp);//设置过期时间
            }
            return builder.compact();
        }

    再写一个方法用于判断token是否合法

     //判断token是否合法
        protected  boolean isVerify(String jwtToken){
            try{
                JWTVerifier verifier = JWT.require(algorithm).build();
                verifier.verify(jwtToken);
                //校验不通过会抛出异常
                //判断合法的标准:1.头部和荷载部分没有篡改过 2.没有过期
                return true;
            }catch (Exception e){
                log.error("jwt isVerify err ",e);
                return false;
            }
        }
  1. AuthService

    此服务中主要实现了对用户输入的code进行校验,对传进来的code判断,缓存中是否有这个值。

    如果有就移除缓存的key值(key:openId&&code)。

    在用户领域层中,我们主要实现两个接口,一个是doLogin,另一个是checkToken。

    在这个AuthService中重写这个接口,让他调用isVerify方法。

模型领域层

实体类
  1. MessageEntity

    • role:权限,对模型接口发起请求时设置,调用chatGlMSDK中的枚举类型Role

    • content:请求设置的消息主题

    • name:不知道~

    以下是一个例子

     //构建参数
                ChatProcessAggregate chatProcessAggregate = ChatProcessAggregate.builder()
                        .token(token)
                        .model(request.getModel())
                        .messages(request.getMessages().stream()
                                .map(entity-> MessageEntity.builder()
                                        .role(entity.getRole())
                                        .content(entity.getContent())
                                        .name(entity.getName())
                                        .build())
                                .collect(Collectors.toList()))
                        .build();
  2. ChoiceEntity

    • 封装MessageEntity

       /** stream = true 请求参数里返回的属性是 delta */
        private MessageEntity delta;
        /** stream = false 请求参数里返回的属性是 delta */
        private MessageEntity message;
  3. ChatProcessAggregate聚合

    • token

    • model:默认的模型(调用SDK中的Model)

      private String model = Model.CHATGLM_TURBO.getCode();
    • message: 用户发送的消息,这里用的List类型,因为用户和模型的问答一般有上下文,用户没有结束此次会话的时候,每次进行一次新的提问就会连带着上次的提问内容一起发送给模型。

      List<MessageEntity> messages;
服务

主要实现了一个流式应答的接口

ResponseBodyEmitter completions(ResponseBodyEmitter emitter,ChatProcessAggregate chatProcess);

写一个抽象类来实现这个接口

调用ChatGLMSDK的openAiSession接口。在这个实现方法里面,我们封装了一个外部实现类doMessageResponse实现应答处理,这个类里面主要是处理信息的,最后,我们返回emitter。在这里,emitter是一个ResponseBodyEmitter对象,它的作用是用于异步的相应数据给前端,它允许你将数据以流的形式发送,而不是一次性的全部发送完成的数据。emitter.send()方法将数据发送给前端。

@Override
    public ResponseBodyEmitter completions(ResponseBodyEmitter emitter, ChatProcessAggregate chatProcess) {
        // 1. 请求应答
        emitter.onCompletion(() -> {
            log.info("流式问答请求完成,使用模型:{}", chatProcess.getModel());
        });
        emitter.onError(throwable -> log.error("流式问答请求疫情,使用模型:{}", chatProcess.getModel(), throwable));
​
        // 2. 应答处理
        try {
            this.doMessageResponse(chatProcess, emitter);
        } catch (Exception e) {
            throw new ChatGLMException(Constants.ResponseCode.UN_ERROR.getCode(), Constants.ResponseCode.UN_ERROR.getInfo());
        }
​
        // 3. 返回结果
        return emitter;
    }

再看doMessageResponse方法,该方法接收一个聚合对象ChatProcessAggregate,这个里面包含了聊天的数据,具体见Controller层。还有一个emitter用于发送数据。

首先需要构建请求信息

这个请求信息是一个类型为Prompt类型的List集合,这个prompt里面有role(用户权限)和content(用户输入信息)

// 1. 请求消息
        List<ChatCompletionRequest.Prompt> prompts = chatProcess.getMessages().stream()
                .map(entity -> ChatCompletionRequest.Prompt.builder()
                        .role(Role.user.getCode())
                        .content(entity.getContent())
                        .build())
                .collect(Collectors.toList());

下一步封装参数

构建ChatCompletionRequest对象,设置所用的模型和基本信息prompt。

ChatCompletionRequest request = new ChatCompletionRequest();
        request.setModel(Model.valueOf(ChatGLMModel.get(chatProcess.getModel()).name())); // chatGLM_6b_SSE、chatglm_lite、chatglm_lite_32k、chatglm_std、chatglm_pro
        request.setPrompt(prompts);

现在可以调用封装好的GLM的SDK里面的completions方法:去请求GLM的API并处理数据。

订阅这个SDK里面的completions方法,在这个订阅里面,设置了onEvent和onClosed方法,对于onEvent方法,这个方法在收到聊天完成事件时会被调用,先将收到的数据解析为ChatCompletionResponse对象,然后根据事件类型type进行不同的处理。

如果事件类型是add,表示收到了增量的聊天响应,通过response.getData()获取响应的数据,并通过emitter发送给客户端。

如果事件类型是finish,表示聊天完成,解析数据后输出相关信息。

chatGlMOpenAiSession.completions(request, new EventSourceListener() {
            @Override
            public void onEvent(EventSource eventSource, @Nullable String id, @Nullable String type, String data) {
                log.info(data);
                ChatCompletionResponse response = JSON.parseObject(data, ChatCompletionResponse.class);
​
                // 发送信息
                if (EventType.add.getCode().equals(type)){
                    try {
                        responseBodyEmitter.send(response.getData());
                    } catch (Exception e) {
                        throw new ChatGLMException(e.getMessage());
                    }
                }
​
                // type 消息类型,add 增量,finish 结束,error 错误,interrupted 中断
                if (EventType.finish.getCode().equals(type)) {
                    ChatCompletionResponse.Meta meta = JSON.parseObject(response.getMeta(), ChatCompletionResponse.Meta.class);
                    log.info("[输出结束] Tokens {}", JSON.toJSONString(meta));
                }
            }
​
            @Override
            public void onClosed(EventSource eventSource) {
                responseBodyEmitter.complete();
            }
​
        });

微信领域层

实体类
  1. 消息实体类

        @XStreamAlias("MsgId")
        private String msgId;
        @XStreamAlias("ToUserName")
        private String toUserName;
        @XStreamAlias("FromUserName")
        private String fromUserName;
        @XStreamAlias("CreateTime")
        private String createTime;
        @XStreamAlias("MsgType")
        private String msgType;
        @XStreamAlias("Content")
        private String content;
        @XStreamAlias("Event")
        private String event;
        @XStreamAlias("EventKey")
        private String eventKey;
  2. 用户行为信息

        private String openId;
        private String fromUserName;
        private String msgType;
        private String content;
        private String event;
        private Date createTime;
  3. 微信公众号消息类型值对象,用于描述对象属性的值,为值对象

        EVENT("event","事件消息"),
        TEXT("text","文本消息");
    ​
        private String code;
        private String desc;
服务

首先是接口,一共有两个接口方法

在验签接口中创建了checkSign(String signature,String timestamp,String nonce)接口。

对于他的实现,我们调用了工具类中的check方法对其实现。

另一个接口中,我们定义了acceptUserBehavior(UserBehaviorMessageEntity userBehaviorMessageEntity)方法,这个方法用于受理用户行为。

先不管事件类型,主要处理用户的文本事件

  1. 缓存验证码

  2. 判断验证码-不考虑验证码的重复问题

  3. 如果判断验证码为空就创建一个四位的验证码,然后存入缓存中。将第一步缓存的验证码更换为当前创建的

  4. 反馈文本信息,构建MessageTextEntity实体类,设置基本信息,内容里面提醒用户验证码的内容和过期时间

  5. 调用Xml解析工具类对刚刚构建的信息进行处理

触发器层

首先定义DTO(Data Transfer Object)是一种用于数据传输的对象,它在应用程序的不同层之间传递数据。DTO的主要目的是在不同的层之间进行数据交换,将数据从一个层传递到另一个层,同时隐藏底层的实现细节。

  1. MessageEntity

    消息实体类

        private String role;
        private String content;
        private String name;

    不要忘记加注解

    @Data
    @Builder
    @AllArgsConstructor
    @NoArgsConstructor
  2. 封装消息实体类的ChoiceEntity

    @Data
    public class ChoiceEntity {
    ​
        /** stream = true 请求参数里返回的属性是 delta */
        private MessageEntity delta;
        /** stream = false 请求参数里返回的属性是 delta */
        private MessageEntity message;
    ​
    }
  3. 模型请求实体类

    /** 默认模型 */
        private String model = ChatGLMModel.CHATGLM_6B_SSE.getCode();
    ​
        /** 问题描述 */
        private List<MessageEntity> messages;

然后就是Controller控制层

首先定义用户Controller层

@RequestMapping(value = "login", method = RequestMethod.POST)
    public Response<String> doLogin(@RequestParam String code) {
        log.info("鉴权登录校验开始,验证码: {}", code);
        try {
            AuthStateEntity authStateEntity = authService.doLogin(code);
            log.info("鉴权登录校验完成,验证码: {} 结果: {}", code, JSON.toJSONString(authStateEntity));
            // 拦截,鉴权失败
            if (!AuthTypeVO.A0000.getCode().equals(authStateEntity.getCode())) {
                return Response.<String>builder()
                        .code(Constants.ResponseCode.TOKEN_ERROR.getCode())
                        .info(Constants.ResponseCode.TOKEN_ERROR.getInfo())
                        .build();
            }
​
            // 放行,鉴权成功
            return Response.<String>builder()
                    .code(Constants.ResponseCode.SUCCESS.getCode())
                    .info(Constants.ResponseCode.SUCCESS.getInfo())
                    .data(authStateEntity.getToken())
                    .build();
​
        } catch (Exception e) {
            log.error("鉴权登录校验失败,验证码: {}", code);
            return Response.<String>builder()
                    .code(Constants.ResponseCode.UN_ERROR.getCode())
                    .info(Constants.ResponseCode.UN_ERROR.getInfo())
                    .build();
        }
    }

这段代码调用了用户领域层的dologin方法,对于方法返回过来的数据进行处理,判断是否拦截。

然后是微信Controller层

这里面主要提供get和post方法用于微信服务器发送和接收信息,处理微信公众号,验证和请求应答

@GetMapping(produces = "text/plain;charset=utf-8")
    public String validate(@PathVariable String appid,
                           @RequestParam(value = "signature", required = false) String signature,
                           @RequestParam(value = "timestamp", required = false) String timestamp,
                           @RequestParam(value = "nonce", required = false) String nonce,
                           @RequestParam(value = "echostr", required = false) String echostr) {
        try {
            log.info("微信公众号验签信息{}开始 [{}, {}, {}, {}]", appid, signature, timestamp, nonce, echostr);
            if (StringUtils.isAnyBlank(signature, timestamp, nonce, echostr)) {
                throw new IllegalArgumentException("请求参数非法,请核实!");
            }
            boolean check = weiXinValidateService.checkSign(signature, timestamp, nonce);
            log.info("微信公众号验签信息{}完成 check:{}", appid, check);
            if (!check) {
                return null;
            }
            return echostr;
        } catch (Exception e) {
            log.error("微信公众号验签信息{}失败 [{}, {}, {}, {}]", appid, signature, timestamp, nonce, echostr, e);
            return null;
        }
    }
​
    @PostMapping(produces = "application/xml; charset=UTF-8")
    public String post(@PathVariable String appid,
                       @RequestBody String requestBody,
                       @RequestParam("signature") String signature,
                       @RequestParam("timestamp") String timestamp,
                       @RequestParam("nonce") String nonce,
                       @RequestParam("openid") String openid,
                       @RequestParam(name = "encrypt_type", required = false) String encType,
                       @RequestParam(name = "msg_signature", required = false) String msgSignature) {
        try {
            log.info("接收微信公众号信息请求{}开始 {}", openid, requestBody);
            // 消息转换
            MessageTextEntity message = XmlUtil.xmlToBean(requestBody, MessageTextEntity.class);
​
            // 构建实体
            UserBehaviorMessageEntity entity = UserBehaviorMessageEntity.builder()
                    .openId(openid)
                    .fromUserName(message.getFromUserName())
                    .msgType(message.getMsgType())
                    .content(StringUtils.isBlank(message.getContent()) ? null : message.getContent().trim())
                    .event(message.getEvent())
                    .createTime(new Date(Long.parseLong(message.getCreateTime()) * 1000L))
                    .build();
​
            // 受理消息
            String result = weiXinBehaviorService.acceptUserBehavior(entity);
            log.info("接收微信公众号信息请求{}完成 {}", openid, result);
            return result;
        } catch (Exception e) {
            log.error("接收微信公众号信息请求{}失败 {}", openid, requestBody, e);
            return "";
        }
    }

现在是最重要的模型Controller层~!

注意模型的请求的地址,如果直接请求官网地址,是不需要加token即可访问的,我的意思是:项目里面调用了自定义的chatGLM的SDK,如果这个SDK里面openAiSession接口里面的conpletions方法中url的组合方式是直接调用官网地址,那就不需要token认证。

这里用了需要加token的api地址。

首先需要在请求头里面多加一个Authorization

 // 1. 基础配置;流式输出、编码、禁用缓存
            response.setContentType("text/event-stream");
            response.setCharacterEncoding("UTF-8");
            response.setHeader("Cache-Control", "no-cache");
            response.setHeader("Authorization",token);

然后,开始进行是否拦截的判断处理

 // 2. 构建异步响应对象【对 Token 过期拦截】
            ResponseBodyEmitter emitter = new ResponseBodyEmitter(3 * 60 * 1000L);
            boolean success = authService.checkToken(token);
​
            if (!success) {
                try {
                    emitter.send(Constants.ResponseCode.TOKEN_ERROR.getCode());
                } catch (IOException e) {
                    throw new RuntimeException(e);
                }
                emitter.complete();
                return emitter;
            }

然后获取openid,这一步可有可无,只是日志记录需要

// 3. 获取 OpenID
            String openid = authService.openid(token);
            log.info("流式问答请求处理,openid:{} 请求模型:{}", openid, request.getModel());

然后构建参数

注意,构建参数的时候需要把自己的token一并构建进去

// 4. 构建参数
            ChatProcessAggregate chatProcessAggregate = ChatProcessAggregate.builder()
                    .token(token)
                    .model(request.getModel())
                    .messages(request.getMessages().stream()
                            .map(entity -> MessageEntity.builder()
                                    .role(entity.getRole())
                                    .content(entity.getContent())
                                    .name(entity.getName())
                                    .build())
                            .collect(Collectors.toList()))
                    .build();

最后调用chatService中的completions方法,传入emitter(当前会话),chatProcessAggregate(消息聚合类,里面是上下文消息)。

// 5. 请求结果&返回
            return chatService.completions(emitter, chatProcessAggregate);

注意事项

后端

首先是项目的jdk版本,最好使用jdk8,如果你使用了jdk8+

  1. 第一个可能出现的错误是

Servlet.service() for servlet [dispatcherServlet] in context with path [] threw exception [Handler dispatch failed; nested exception is java.lang.NoClassDefFoundError: javax/xml/bind/DatatypeConverter] with root cause
java.lang.ClassNotFoundException: javax.xml.bind.DatatypeConverter
	at java.base/jdk.internal.loader.BuiltinClassLoader.loadClass(BuiltinClassLoader.java:581)
	at java.base/jdk.internal.loader.ClassLoaders$AppClassLoader.loadClass(ClassLoaders.java:178)Parameter 0 of constructor in com.xfusion.chatglm.data.domain.openai.service.rule.factory.DefaultLogicFactory required a bean of type 'com.google.common.cache.Cache' that could not be found.

解决方法

这个错误是由于缺少 `javax.xml.bind.DatatypeConverter` 类所在的类库引起的。在Java 9及以上版本中,`javax.xml.bind` 包已经被标记为过时,并且在标准库中被移除。

针对你的问题,可以尝试以下解决方案:

1. 如果你使用的是Java 9及以上版本,可以尝试添加以下依赖来引入 `javax.xml.bind` 类库:

​```xml
<dependency>
    <groupId>javax.xml.bind</groupId>
    <artifactId>jaxb-api</artifactId>
    <version>2.3.1</version>
</dependency>
​```

这个依赖将提供 `javax.xml.bind.DatatypeConverter` 类所在的类库。

2. 如果你使用的是Java 11及以上版本,可以尝试添加以下依赖来引入 `javax.xml.bind` 类库:

​```xml
<dependency>
    <groupId>jakarta.xml.bind</groupId>
    <artifactId>jakarta.xml.bind-api</artifactId>
    <version>3.0.0</version>
</dependency>
​```

这个依赖将提供 `javax.xml.bind.DatatypeConverter` 类所在的类库。请注意,这里使用的是 `jakarta.xml.bind` 包,而不是 `javax.xml.bind` 包。

请根据你的Java版本和项目需求选择适合的依赖。如果问题仍然存在,请提供更多的错误信息和项目配置,以便我更好地帮助你解决问题。

  1. 第二个问题

Parameter 0 of constructor in com.xfusion.chatglm.data.domain.openai.service.rule.factory.DefaultLogicFactory required a bean of type 'com.google.common.cache.Cache' that could not be found.

忘记怎么解决的了。。。。。。。。

前端

这部分目前没有什么问题

注意好api包下的index.tsx里面fetch方法请求的地址对不对应就好

微信公众号

这里面需要先申请好自己的originid和appID,然后就是配置本地服务的信息,你需要自己弄一个内网穿透。

这里需要注意的是,你本地服务需要先跑起来,然后微信公众号配置服务器那个部分要填写自己写的controller方法的地址,因为配置的时候,微信服务器会给你的程序发送并请求信息,所以自己的项目需要先跑起来。

令牌(Token)随便写就好了。

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

HalukiSan

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值