OA系统后台部分是采用目前非常流程的SpringBoot框架实现,其中数据库访问采用mybatis-plus相关技术实现。aiWithDoc使用国内java最流行的技术栈,提供各类经过各种项目验证的组件方便在业务开发时的调用,例如HuTool、easyPOI、fastJson、freemarker等等。aiWithDoc的核心业务功能是与文档聊天,感兴趣或者有相关技术能力的公司或个人可以基于此项目开发出适合自己的衍生项目。
本文档的作用只是起到抛砖引玉的作用,供广大爱好者或者相关行业工作者学习或借鉴。
sosoa-platform
└── src #源码目录
| └── main
| | ├── java
| | | └── com
| | | └── iqiqiqi #公司名
| | | └── sosoa #项目名
| | | ├── common #通用模块部分
| | | | ├── annotation #自定义注解
| | | | ├── aspect #切面
| | | | ├── config #项目配置
| | | | ├── constant #常量
| | | | ├── dao #BaseDao
| | | | ├── entity #BaseEntity
| | | | ├── exception #统一异常处理
| | | | ├── interceptor #自定义拦截器
| | | | ├── page #通用分页处理
| | | | ├── redis #Redis相关封装
| | | | ├── service #BaseService
| | | | ├── utils #各种小组件,例如文件处理,字典处理等等
| | | | ├── validator #后端校验
| | | | └── xss #防xss攻击
| | | ├── modules #功能模块
| | | | ├── flowable #流程管理
| | | | ├── job #定时任务管理
| | | | ├── message #消息管理
| | | | └── notice #通知管理
| | | | ├── oaassets #资产管理
| | | | ├── oacar #车辆管理
| | | | └── oacontract #合同管理
| | | | ├── oadocument #文档管理
| | | | ├── oalicence #证照管理
| | | | └── oameeting #会议管理
| | | | ├── oaofficesupplies #办公用品
| | | | ├── oaproject #项目管理
| | | | └── oareimburse #报销管理
| | | | ├── oaschedule #日程管理
| | | | ├── oaseal #安全相关
| | | | ├── oss #对象存储服务
| | | | ├── security #安全相关
| | | | └── sys #系统管理
| | | ├── PlatformApplication.java #SpringBoot入口类
| | └── resources
| | ├── application-dev.yml #开发环境
| | ├── application-prod.yml #生产环境
| | ├── application-test.yml #测试环境
| | ├── application.yml #公共配置
| | ├── banner.txt #启动时显示的文字
| | ├── dev
| | | └── application.yml #maven打包时用的文件
| | ├── excel #excel导出模版
| | ├── i18n #多语言
| | ├── logback-spring.xml #日志配置
| | ├── mapper #mybatisplus mapper文件
| | ├── prod
| | | └── application.yml #maven打包时用的文件
一个成熟的后台项目包含很多技术框架类的内容,例如组件库,安全处理,统一日志处理,多语言,统一异常处理,excel导入导出等等。由于此sosoa是一个完整的OA项目,包含业务功能点丰富,技术框架该有的基础功能已经基本涵盖(工作流相关没有包括进来),以下从几个功能点代码进行讲解,起到抛砖引玉的作用,完整的代码学习还需要自行在项目中或者自行debug进行学习。
BaseEntity是一个所有实体都需要自动继承的父类,该类定义了公共部分的Id、creator、createDate等字段,并且重写了equals方法和hashCode方法,便于我们在开发过程当中更便捷的使用
public abstract class BaseEntity implements Serializable { @TableId private Long id; @TableField(fill = FieldFill.INSERT) private Long creator; @TableField(fill = FieldFill.INSERT) @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") private Date createDate; public void setId(Long id) { this.id = id; } public void setCreator(Long creator) { this.creator = creator; } public void setCreateDate(Date createDate) { this.createDate = createDate; } public boolean equals(Object o) { if (o == this) { return true; } if (!(o instanceof BaseEntity)) { return false; } BaseEntity other = (BaseEntity) o; if (!other.canEqual(this)) { return false; } Object this$id = getId(); Object other$id = other.getId(); if (this$id == null ? other$id != null : !this$id.equals(other$id)) { return false; } Object this$creator = getCreator(); Object other$creator = other.getCreator(); if (this$creator == null ? other$creator != null : !this$creator.equals(other$creator)) { return false; } Object this$createDate = getCreateDate(); Object other$createDate = other.getCreateDate(); return this$createDate == null ? other$createDate == null : this$createDate.equals(other$createDate); } protected boolean canEqual(Object other) { return other instanceof BaseEntity; } public int hashCode() { int PRIME = 59; int result = 1; Object $id = getId(); result = result * 59 + ($id == null ? 43 : $id.hashCode()); Object $creator = getCreator(); result = result * 59 + ($creator == null ? 43 : $creator.hashCode()); Object $createDate = getCreateDate(); result = result * 59 + ($createDate == null ? 43 : $createDate.hashCode()); return result; } public String toString() { return "BaseEntity(id=" + getId() + ", creator=" + getCreator() + ", createDate=" + getCreateDate() + ")"; } public Long getId() { return this.id; } public Long getCreator() { return this.creator; } public Date getCreateDate() { return this.createDate; } }
BaseService是一个所以service都需要自动继承的父类,该类定义了公用的insert、update、selectById、deleteById等公共的接口,使我们在日常开发过程当中减少代码量
public abstract interface BaseService<T> { public abstract boolean insert(T paramT); public abstract boolean insertBatch(Collection<T> paramCollection); public abstract boolean insertBatch(Collection<T> paramCollection, int paramInt); public abstract boolean updateById(T paramT); public abstract boolean update(T paramT, Wrapper<T> paramWrapper); public abstract boolean updateBatchById(Collection<T> paramCollection); public abstract boolean updateBatchById(Collection<T> paramCollection, int paramInt); public abstract T selectById(Serializable paramSerializable); public abstract boolean deleteById(Serializable paramSerializable); public abstract boolean deleteBatchIds(Collection<? extends Serializable> paramCollection); }
BaseDao是一个所有DAO层都需要自动继承的父类,该类本身又继承了mybaits的BaseMapper类,使我们在日常开发过程当中减少代码量的同时,更方便于代码的统一管理
public abstract interface BaseDao<T> extends BaseMapper<T> {}
Result是一个统一的返回实体,包含了消息码、消息内容以及相应数据。这样所有API接口遵循同一种返回数据格式,使得代码更加整洁,也方便了客户端对数据的处理,通过返回码和返回消息,可以清晰地告诉客户端本次请求是成功还是失败,如果失败还提供了失败的原因。返回数据字段可以根据需要返回任意类型的数据,非常灵活。
@ApiModel("响应") public class Result<T> implements Serializable { private static final long serialVersionUID = 1L; @ApiModelProperty("编码0表示成功,其它值表示失败") private int code = 0; @ApiModelProperty("消息内容") private String msg = "success"; @ApiModelProperty("响应数据") private T data; public Result<T> ok(T data) { setData(data); return this; } public boolean success() { return this.code == 0; } public Result<T> error() { this.code = 500; this.msg = MessageUtils.getMessage(this.code); return this; } public Result<T> error(int code) { this.code = code; this.msg = MessageUtils.getMessage(this.code); return this; } public Result<T> error(int code, String msg) { this.code = code; this.msg = msg; return this; } public Result<T> error(String msg) { this.code = 500; this.msg = msg; return this; } public int getCode() { return this.code; } public void setCode(int code) { this.code = code; } public String getMsg() { return this.msg; } public void setMsg(String msg) { this.msg = msg; } public T getData() { return (T) this.data; } public void setData(T data) { this.data = data; } }
由于管理后台多是由表格和统计组成,故在此统一讲解部分高度重合代码。一下讲解以证照管理为示例,下面是java代码
@RestController @RequestMapping("oalicence/licence") @Api(tags="证照管理") public class LicenceController { @Autowired private LicenceService licenceService; @GetMapping("page") @ApiOperation("分页") @ApiImplicitParams({ @ApiImplicitParam(name = Constant.PAGE, value = "当前页码,从1开始", paramType = "query", required = true, dataType="int") , @ApiImplicitParam(name = Constant.LIMIT, value = "每页显示记录数", paramType = "query",required = true, dataType="int") , @ApiImplicitParam(name = Constant.ORDER_FIELD, value = "排序字段", paramType = "query", dataType="String") , @ApiImplicitParam(name = Constant.ORDER, value = "排序方式,可选值(asc、desc)", paramType = "query", dataType="String") }) @RequiresPermissions("oalicence:licence:page") public Result<PageData<LicenceDTO>> page(@ApiIgnore @RequestParam Map<String, Object> params){ PageData<LicenceDTO> page = licenceService.page(params); return new Result<PageData<LicenceDTO>>().ok(page); } @GetMapping("getList") @ApiOperation("信息") public Result<List<LicenceDTO>> getList() { List<LicenceDTO> list = licenceService.list(new HashMap<>()); return new Result<List<LicenceDTO>>().ok(list); } @GetMapping("{id}") @ApiOperation("信息") @RequiresPermissions("oalicence:licence:info") public Result<LicenceDTO> get(@PathVariable("id") Long id){ LicenceDTO data = licenceService.get(id); return new Result<LicenceDTO>().ok(data); } @PostMapping @ApiOperation("保存") @LogOperation("保存") @RequiresPermissions("oalicence:licence:save") public Result save(@RequestBody LicenceDTO dto){ //效验数据 ValidatorUtils.validateEntity(dto, AddGroup.class, DefaultGroup.class); licenceService.save(dto); return new Result(); } @PutMapping @ApiOperation("修改") @LogOperation("修改") @RequiresPermissions("oalicence:licence:update") public Result update(@RequestBody LicenceDTO dto){ //效验数据 ValidatorUtils.validateEntity(dto, UpdateGroup.class, DefaultGroup.class); licenceService.update(dto); return new Result(); } @DeleteMapping @ApiOperation("删除") @LogOperation("删除") @RequiresPermissions("oalicence:licence:delete") public Result delete(@RequestBody Long[] ids){ //效验数据 AssertUtils.isArrayEmpty(ids, "id"); licenceService.delete(ids); return new Result(); } @GetMapping("export") @ApiOperation("导出") @LogOperation("导出") @RequiresPermissions("oalicence:licence:export") public void export(@ApiIgnore @RequestParam Map<String, Object> params, HttpServletResponse response) throws Exception { List<LicenceDTO> list = licenceService.list(params); ExcelUtils.exportExcelToTarget(response, null, list, LicenceExcel.class); } }
用于处理请求,通常作用在控制层中的类之上,等同于@Controller和@ResponseBody;
通过该注解就可以使用配置的URL来进行访问,方式可以是post或者get请求
请求类的功能说明
通过类型注入属性
通过该注解就可以使用配置的URL来进行访问,但是限定了get请求
通过该注解就可以使用配置的URL来进行访问,但是限定了post请求
通过该注解可以将请求参数区域的数据映射到控制层方法的参数上
通过该注解可以对应路径上的变量,用在参数前,路径上的变量名需和参数名称一致
是指方法参数被绑定到HTTP请求Body上,前端就不能用表单的方式提交了,需要用json的方式提交。
方法的功能说明
方法入参的说明
系统内部开发的用于鉴权操作的注解。
系统内部开发的用于日志保存的切面的注解
该方法主要用于前端表格部分,通过分页参数信息和其他用户定义的参数向数据库获取带有分页新的的列表,然后以json的形式进行返回数据。一下为示例代码:
@GetMapping("page") @ApiOperation("分页") @ApiImplicitParams({ @ApiImplicitParam(name = Constant.PAGE, value = "当前页码,从1开始", paramType = "query", required = true, dataType="int") , @ApiImplicitParam(name = Constant.LIMIT, value = "每页显示记录数", paramType = "query",required = true, dataType="int") , @ApiImplicitParam(name = Constant.ORDER_FIELD, value = "排序字段", paramType = "query", dataType="String") , @ApiImplicitParam(name = Constant.ORDER, value = "排序方式,可选值(asc、desc)", paramType = "query", dataType="String") }) @RequiresPermissions("oalicence:licence:page") public Result<PageData<LicenceDTO>> page(@ApiIgnore @RequestParam Map<String, Object> params){ PageData<LicenceDTO> page = licenceService.page(params); return new Result<PageData<LicenceDTO>>().ok(page); }
该方法主要用于通过表单ID获取表单的详细信息,然后以json的形式进行返回数据。以下为示例代码:
@GetMapping("{id}") @ApiOperation("信息") @RequiresPermissions("oalicence:licence:info") public Result<LicenceDTO> get(@PathVariable("id") Long id){ LicenceDTO data = licenceService.get(id); return new Result<LicenceDTO>().ok(data); }
该方法用于表单的保存操作,通过用户在前端页面输入表单信息,点击保存操作之后,后台会进行数据库持久化操作,之后返回操作成功或者失败的信息,以下为示例代码:
@PostMapping @ApiOperation("保存") @LogOperation("保存") @RequiresPermissions("oalicence:licence:save") public Result save(@RequestBody LicenceDTO dto){ //效验数据 ValidatorUtils.validateEntity(dto, AddGroup.class, DefaultGroup.class); licenceService.save(dto); return new Result(); }
该方法用于表单的修改操作,用户在前端页面指定需要修改的数据列表的信息,点击修改操作之后,会先获取历史信息,然后用户在历史表单上进行修改,之后点击确认操作,后台会进行数据库持久化更新的操作,之后返回操作成功或者失败的信息,以下为示例代码:
@PutMapping @ApiOperation("修改") @LogOperation("修改") @RequiresPermissions("oalicence:licence:update") public Result update(@RequestBody LicenceDTO dto){ //效验数据 ValidatorUtils.validateEntity(dto, UpdateGroup.class, DefaultGroup.class); licenceService.update(dto); return new Result(); }
该方法用户表单的删除操作,当用户不需要某一条数据的时候,用户在前端页面指定需要删除的数据列表的信息,点击删除操作之后,后台会进行数据库持久化删除的操作,之后返回操作成功或者失败的信息,以下为示例代码:
@DeleteMapping @ApiOperation("删除") @LogOperation("删除") @RequiresPermissions("oalicence:licence:delete") public Result delete(@RequestBody Long[] ids){ //效验数据 AssertUtils.isArrayEmpty(ids, "id"); licenceService.delete(ids); return new Result(); }
该方法用于数据列表的导出操作,在系统使用过程当中,不免有些数据需要进行数据导出的操作,选中需要导出的数据之后,点击导出操作,之后返回导出的excel文件,可以进行下载,以下为示例代码:
@GetMapping("export") @ApiOperation("导出") @LogOperation("导出") @RequiresPermissions("oalicence:licence:export") public void export(@ApiIgnore @RequestParam Map<String, Object> params, HttpServletResponse response) throws Exception { List<LicenceDTO> list = licenceService.list(params); ExcelUtils.exportExcelToTarget(response, null, list, LicenceExcel.class); }
对应《sosoa代码讲解(前端部分)》,此处解析java端对应的后台部分。
/** * 登录 */ @RestController @Api(tags="登录管理") public class LoginController { @Autowired private SysUserService sysUserService; @Autowired private SysUserTokenService sysUserTokenService; @Autowired private CaptchaService captchaService; @Autowired private SysLogLoginService sysLogLoginService; @GetMapping("captcha") @ApiOperation(value = "验证码", produces="application/octet-stream") @ApiImplicitParam(paramType = "query", dataType="string", name = "uuid", required = true) public void captcha(HttpServletResponse response, String uuid)throws IOException { //uuid不能为空 AssertUtils.isBlank(uuid, ErrorCode.IDENTIFIER_NOT_NULL); //生成验证码 captchaService.create(response, uuid); } @PostMapping("login") @ApiOperation(value = "登录") 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()); } @PostMapping("logout") @ApiOperation(value = "退出") public Result logout(HttpServletRequest request) { UserDetail user = SecurityUser.getUser(); //退出 sysUserTokenService.logout(user.getId()); //用户信息 SysLogLoginEntity log = new SysLogLoginEntity(); log.setOperation(LoginOperationEnum.LOGOUT.value()); log.setIp(IpUtils.getIpAddr(request)); log.setUserAgent(request.getHeader(HttpHeaders.USER_AGENT)); log.setIp(IpUtils.getIpAddr(request)); log.setStatus(LoginStatusEnum.SUCCESS.value()); log.setCreator(user.getId()); log.setCreatorName(user.getUsername()); log.setCreateDate(new Date()); sysLogLoginService.save(log); return new Result(); } }
以上代码包括二个函数,一个是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>
扫码关注不迷路!!!
郑州升龙商业广场B座25层
service@iqiqiqi.cn
联系电话:400-8049-474
联系电话:187-0363-0315
联系电话:199-3777-5101