aiWithWechat代码讲解(Java端)
作者:企起期 阅读次数:

演示视频

介绍

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函数用来处理登录请求。

captcha函数

@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中获取验证码,并在获取后删除该验证码(为了保证安全)。否则从本地缓存中获取验证码,并在获取后从缓存中删除。

login函数

@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语句进行保存或者修改。

  1. 获得对应的向量数据(getQuestionEmbedding)

函数的主体是首先通过配置类得到对应的embeddingUrl,其次组织对应的http请求头相关信息,设置对应的模型为"text-embedding-ada-002",通过HuTools的HttpRequet进行post请求,得到对应的响应。通过fastjson的JSON把响应字符串转换成JSONObject对象,再取出对应的embedding向量数据以及total_tokens,封装到返回的HashMap中。

  1. 保存包括向量数据的实体(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>
  1. 修改包括向量数据的实体(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

企起期科技 qiqiqi

联系电话:400-8049-474

联系电话:187-0363-0315

联系电话:199-3777-5101