aiWithWechat后台部分是采用目前非常流程的SpringBoot框架实现,其中数据库访问采用mybatis-plus相关技术实现。aiWithWechat使用国内java最流行的技术栈,提供各类经过各种项目验证的组件方便在业务开发时的调用,例如HuTool、easyPOI、fastJson、freemarker等等。aiWithWechat的核心业务功能是设置问答,并把问题进行向量化存储到向量数据库中,然后在微信中进行使用。
本文档的作用只是起到抛砖引玉的作用,供广大爱好者或者相关行业工作者学习或借鉴。欢迎任何企业或个人联系企起期,我们的网址是www.iqiqiqi.cn。
aiwithwechat └── src #源码目录 | └── main | | ├── java | | | └── com | | | └── iqiqiqi #公司名 | | | └── aiwithdoc #项目名 | | | ├── common #通用模块部分 | | | | ├── annotation #自定义注解 | | | | ├── aspect #切面 | | | | ├── config #项目配置 | | | | ├── constant #常量 | | | | ├── dao #BaseDao | | | | ├── entity #BaseEntity | | | | ├── exception #统一异常处理 | | | | ├── interceptor #自定义拦截器 | | | | ├── page #通用分页处理 | | | | ├── redis #Redis相关封装 | | | | ├── service #BaseService | | | | ├── utils #各种小组件,例如文件处理,字典处理等等 | | | | ├── validator #后端校验 | | | | └── xss #防xss攻击 | | | ├── modules #功能模块 | | | | ├── log #日志管理 | | | | ├── qamgt #问答管理 | | | | ├── oss #对象存储服务 | | | | ├── security #安全相关 | | | | └── sys #系统管理 | | | ├── PlatformApplication.java #SpringBoot入口类 | | └── resources | | ├── application-dev.yml #开发环境 | | ├── application-prod.yml #生产环境 | | ├── application-test.yml #测试环境 | | ├── application.yml #公共配置 | | ├── banner.txt #启动时显示的文字 | | ├── dev | | | └── application.yml #maven打包时用的文件 | | ├── i18n #多语言 | | | ├── messages.properties | | | ├── messages_en_US.properties | | | ├── messages_zh_CN.properties | | ├── logback-spring.xml #日志配置 | | ├── mapper #mybatisplus mapper文件 | | | ├── log | | | ├── qamgt | | ├── prod | | | └── application.yml #maven打包时用的文件
一个成熟的后台项目包含很多技术框架类的内容,例如组件库,安全处理,统一日志处理,多语言,统一异常处理,excel导入导出等等。由于此aiWithWechat是一个完整的项目,虽然业务功能点不多,但技术框架该有的基础功能已经基本涵盖,以下从二个功能点代码进行讲解,起到抛砖引玉的作用,完整的代码学习还需要自行在项目中或者自行debug进行学习。
对应《aiWithWechat代码讲解(前端部分)》,此处解析java端对应的后台部分。
/** * 登录 * * @author chenyu 2020-07-27 */ @RestController public class LoginController { @Autowired private SysUserService sysUserService; @Autowired private SysUserTokenService sysUserTokenService; @Autowired private CaptchaService captchaService; @Autowired private SysLogLoginService sysLogLoginService; @GetMapping("captcha") public void captcha(HttpServletResponse response, String uuid)throws IOException { //uuid不能为空 AssertUtils.isBlank(uuid, ErrorCode.IDENTIFIER_NOT_NULL); //生成验证码 captchaService.create(response, uuid); } @PostMapping("login") public Result login(HttpServletRequest request, @RequestBody LoginDTO login) { //效验数据 ValidatorUtils.validateEntity(login); //验证码是否正确 boolean flag = captchaService.validate(login.getUuid(), login.getCaptcha()); if(!flag){ return new Result().error(ErrorCode.CAPTCHA_ERROR); } //用户信息 SysUserDTO user = sysUserService.getByUsername(login.getUsername()); SysLogLoginEntity log = new SysLogLoginEntity(); log.setOperation(LoginOperationEnum.LOGIN.value()); log.setCreateDate(new Date()); log.setIp(IpUtils.getIpAddr(request)); log.setUserAgent(request.getHeader(HttpHeaders.USER_AGENT)); log.setIp(IpUtils.getIpAddr(request)); //用户不存在 if(user == null){ log.setStatus(LoginStatusEnum.FAIL.value()); log.setCreatorName(login.getUsername()); sysLogLoginService.save(log); throw new MyException(ErrorCode.ACCOUNT_PASSWORD_ERROR); } //密码错误 if(!PasswordUtils.matches(login.getPassword(), user.getPassword())){ log.setStatus(LoginStatusEnum.FAIL.value()); log.setCreator(user.getId()); log.setCreatorName(user.getUsername()); sysLogLoginService.save(log); throw new MyException(ErrorCode.ACCOUNT_PASSWORD_ERROR); } //账号停用 if(user.getStatus() == UserStatusEnum.DISABLE.value()){ log.setStatus(LoginStatusEnum.LOCK.value()); log.setCreator(user.getId()); log.setCreatorName(user.getUsername()); sysLogLoginService.save(log); throw new MyException(ErrorCode.ACCOUNT_DISABLE); } //登录成功 log.setStatus(LoginStatusEnum.SUCCESS.value()); log.setCreator(user.getId()); log.setCreatorName(user.getUsername()); sysLogLoginService.save(log); return sysUserTokenService.createToken(user.getId()); } }
以上代码包括二个函数,一个是captcha,另一个是login函数。其中captcha用来生成验证码,login函数用来处理登录请求。
@GetMapping("captcha") 声明该restful接口为get请求,captchaService.create(response, uuid);代码表示调用captchaService的create方法生成验证码,参数uuid为前端传过来的唯一码。
@Service public class CaptchaServiceImpl implements CaptchaService { @Autowired private RedisUtils redisUtils; @Value("${redisopen: false}") private boolean open; /** * Local Cache 5分钟过期 */ Cache<String, String> localCache = CacheBuilder.newBuilder().maximumSize(1000).expireAfterAccess(5, TimeUnit.MINUTES).build(); @Override public void create(HttpServletResponse response, String uuid) throws IOException { response.setContentType("image/gif"); response.setHeader("Pragma", "No-cache"); response.setHeader("Cache-Control", "no-cache"); response.setDateHeader("Expires", 0); //生成验证码 ArithmeticCaptcha captcha = new ArithmeticCaptcha(150, 40); captcha.setLen(2); captcha.getArithmeticString(); captcha.out(response.getOutputStream()); //保存到缓存 setCache(uuid, captcha.text()); } @Override public boolean validate(String uuid, String code) { //获取验证码 String captcha = getCache(uuid); //效验成功 if(code.equalsIgnoreCase(captcha)){ return true; } return false; } private void setCache(String key, String value){ if(open){ key = RedisKeys.getCaptchaKey(key); redisUtils.set(key, value, 300); }else{ localCache.put(key, value); } } private String getCache(String key){ if(open){ key = RedisKeys.getCaptchaKey(key); String captcha = (String)redisUtils.get(key); //删除验证码 if(captcha != null){ redisUtils.delete(key); } return captcha; } String captcha = localCache.getIfPresent(key); //删除验证码 if(captcha != null){ localCache.invalidate(key); } return captcha; } }
create方法,用于生成验证码并将其写入HTTP响应中。该方法首先设置响应的内容类型和缓存控制头,然后使用ArithmeticCaptcha生成一个算术验证码,并将其输出到响应的输出流中。最后,将验证码保存到缓存中。验证码的组件为EasyCaptcha,支持gif、中文、算术等类型,本例使用的是算术类型。
validate方法,用于验证用户输入的验证码是否正确。该方法首先从缓存中获取验证码,然后将用户输入的验证码与缓存中的验证码进行比较,如果相等则返回true,否则返回false。
私有方法setCache,用于将验证码保存到缓存中。如果开启了Redis缓存,则将验证码保存到Redis中,否则保存到本地缓存中。
私有方法getCache,用于从缓存中获取验证码。如果开启了Redis缓存,则从Redis中获取验证码,并在获取后删除该验证码(为了保证安全)。否则从本地缓存中获取验证码,并在获取后从缓存中删除。
@PostMapping注解表示该restful接口为POST请求。该方法首先验证请求数据的有效性,然后验证验证码是否正确。如果正确,则根据用户名获取用户信息,并进行密码验证、账号状态验证等逻辑处理。最后,调用sysUserTokenService.createToken方法创建用户的登录令牌,并返回结果。
public SysUserDTO getByUsername(String username) { SysUserEntity entity = baseDao.getByUsername(username); return ConvertUtils.sourceToTarget(entity, SysUserDTO.class); }
以上代码是根据用户名得到用户实体的service实现,调用的sql如下:
<select id="getByUsername" resultType="com.iqiqiqi.aiwithdoc.modules.sys.entity.SysUserEntity"> select * from sys_user where username = #{value} </select>
为实现在微信中的机器人按照预制的问题进行解答,对应的管理端就需要有问答数据设置的相关功能点。前端传过来问答数据后,java端调用OpenAI接口,把问题进行向量化,并把相关的向量化数据存入到向量数据库中(本项目用到的向量数据库为pgvector)。以下是controller代码:
@PostMapping @RequiresPermissions("qamgt:qa:save") public Result save(@RequestBody QaDTO dto) throws SQLException { //需要设置代理,否则openAI禁止访问 System.setProperty("java.net.useSystemProxies", openAiConfig.getUseSystemProxies()); System.setProperty("https.proxyHost", openAiConfig.getProxyHost()); System.setProperty("https.proxyPort", openAiConfig.getProxyPort()); Map<String,Object> embedObj = qaService.getQuestionEmbedding(dto.getQuestion()); dto.setTokens(Integer.valueOf(embedObj.get("tokens").toString())); dto.setEmbedding(new PGvector(embedObj.get("embedding").toString())); qaService.insertIncludeVector(dto); return new Result(); }
根据注解可以看出,此restful接口为post请求,另外一个注解是进行权限判断。函数体中是调用service层的接口得到问题对应的向量化数据,再调用service接口进行问答对象的保存,以下是service层代码:
@Override public Map<String, Object> getQuestionEmbedding(String q) { Map<String, Object> rtn = new HashMap<>(); String embeddingUrl = openAiConfig.getApi_base()+"/embeddings"; Map<String, String > heads = new HashMap<>(); heads.put("Content-Type", "application/json;charset=UTF-8"); heads.put("Authorization", "Bearer "+openAiConfig.getApiKey()); Map<String, Object> params = new HashMap<>(); params.put("input",q); params.put("model", "text-embedding-ada-002"); String result = HttpRequest.post(embeddingUrl).headerMap(heads, false).body(JSON.toJSONString(params)).timeout(120000).execute().body(); com.alibaba.fastjson.JSONObject json = JSON.parseObject(result); rtn.put("embedding",json.getJSONArray("data").getJSONObject(0).getString("embedding")); rtn.put("tokens",json.getJSONObject("usage").getString("total_tokens")); return rtn; } @Override public void insertIncludeVector(QaDTO dto) { QaEntity entity = ConvertUtils.sourceToTarget(dto,QaEntity.class); long id = IdWorker.getId(); long currentUserId = SecurityUser.getUserId(); entity.setCreateDate(new Date()); entity.setCreator(currentUserId); entity.setId(id); baseDao.insertIncludeVector(entity); } @Override public void updateIncludeVector(QaDTO dto) { QaEntity entity = ConvertUtils.sourceToTarget(dto,QaEntity.class); long currentUserId = SecurityUser.getUserId(); entity.setUpdateDate(new Date()); entity.setUpdater(currentUserId); baseDao.updateIncludeVector(entity); }
以上service的实现包括三个函数,一个为调用接口获得对应的向量数据,一个为调用接口增加对应的问答对象,另外一个为修改指定的问答对象。由于用到了向量数据,因此并不能用传统的方式进行对象的保存和修改,而是需要用显示的sql语句进行保存或者修改。
获得对应的向量数据(getQuestionEmbedding)
函数的主体是首先通过配置类得到对应的embeddingUrl,其次组织对应的http请求头相关信息,设置对应的模型为"text-embedding-ada-002",通过HuTools的HttpRequet进行post请求,得到对应的响应。通过fastjson的JSON把响应字符串转换成JSONObject对象,再取出对应的embedding向量数据以及total_tokens,封装到返回的HashMap中。
保存包括向量数据的实体(insertIncludeVector)
由于向量数据的特殊性,因此不能调用继承的service进行实体保存,而是调用特定的dao接口进行原生数据的插入。对应的xml mapper如下:
<insert id="insertIncludeVector"> insert into tb_qa (id,question,answer,tokens,embedding,creator,create_date) values (${id},'${question}','${answer}',${tokens},'${embedding}',${creator},'${createDate}') </insert>
修改包括向量数据的实体(updateIncludeVector)
由于向量数据的特殊性,因此不能调用继承的service进行实体修改,而是调用特定的dao接口进行原生数据的修改。对应的xml mapper如下:
<update id="updateIncludeVector"> UPDATE "tb_qa" SET "question" = '${question}', "answer" = '${answer}', "tokens" = ${tokens}, "embedding" = '${embedding}', "updater" = ${updater}, "update_date" = '${updateDate}' WHERE "id" = ${id} </update>
扫码关注不迷路!!!
郑州升龙商业广场B座25层
service@iqiqiqi.cn
联系电话:400-8049-474
联系电话:187-0363-0315
联系电话:199-3777-5101