1 Star 0 Fork 0

okaykai/iShare IT

加入 Gitee
与超过 1200万 开发者一起发现、参与优秀开源项目,私有仓库也完全免费 :)
免费加入
该仓库未声明开源许可证文件(LICENSE),使用请关注具体项目描述及其代码上游依赖。
克隆/下载
贡献代码
同步代码
取消
提示: 由于 Git 不支持空文件夾,创建文件夹后会生成空的 .keep 文件
Loading...
README
Apache-2.0
# IShareIT #### 系统说明[参考掘金](https://juejin.im/timeline) 系统说明,本系统是面向用户的IT社区系统,分为两个角色,后台管理员、用户 用户说明:用户拥有个人账号,在前台可以浏览、发布、评论、收藏、点赞文章,可以关注喜欢的作者,回复评论,可以对自己的个人信息修改, 同时也可以删除、编辑文章,删除评论等功能,用户不可以进入后台管理,只可以在前台对内容进行修改 管理员说明:管理员可访问后台,可查看系统数据,可对文章、评论进行删除,对分类与标签CURD,用户CURD,系统设置,发布公告等功能 ![](./imgs/main.png) #### 开发说明 ##### 所用技术栈 本次项目采用前后端分离模式,接口规范采用restful风格,前端**vue**全家桶,后台使用**springboot 2.3.1.RELEASE**+**maven**(项目管理)+**mybatis-plus**(数据库操作)+**redis**(缓存)+**shiro**+**jwt**(权限认证),接口文档使用**swagger2**,数据库采用**mysql5.7.29** ### 后端开发过程及注意事项 #### Restful风格api[参考这篇文章](https://www.cnblogs.com/yinzhengjie/p/12037939.html) #### 整合mybatis-plus 1. 添加依赖 ```xml <dependency> <groupId>com.baomidou</groupId> <artifactId>mybatis-plus-boot-starter</artifactId> <version>3.2.0</version> </dependency> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <scope>runtime</scope> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-freemarker</artifactId> </dependency> <!--mp代码生成器--> <dependency> <groupId>com.baomidou</groupId> <artifactId>mybatis-plus-generator</artifactId> <version>3.2.0</version> </dependency> ``` 2. 配置文件 ```yml mybatis-plus: mapper-locations: classpath*:/mapper/**Mapper.xml configuration: log-impl: org.apache.ibatis.logging.stdout.StdOutImpl type-aliases-package: com.rjpyy.ishareit.entity ``` 3. 配置类 ```java @Configuration @EnableTransactionManagement @MapperScan("com.rjpyy.ishareit.mapper") public class MybatisPlusConfig { @Bean public PaginationInterceptor paginationInterceptor() { return new PaginationInterceptor(); } } ``` 4. 代码生成 ```java public class CodeGenerator { /** * <p> * 读取控制台内容 * </p> */ public static String scanner(String tip) { Scanner scanner = new Scanner(System.in); StringBuilder help = new StringBuilder(); help.append("请输入" + tip + ":"); System.out.println(help.toString()); if (scanner.hasNext()) { String ipt = scanner.next(); if (StringUtils.isNotEmpty(ipt)) { return ipt; } } throw new MybatisPlusException("请输入正确的" + tip + "!"); } public static void main(String[] args) { // 代码生成器 AutoGenerator mpg = new AutoGenerator(); // 全局配置 GlobalConfig gc = new GlobalConfig(); String projectPath = System.getProperty("user.dir"); gc.setOutputDir(projectPath + "/src/main/java"); gc.setAuthor("gzhhh"); gc.setOpen(false); gc.setIdType(IdType.AUTO); gc.setDateType(DateType.ONLY_DATE); gc.setSwagger2(true);// 实体属性 Swagger2 注解 mpg.setGlobalConfig(gc); // 数据源配置 DataSourceConfig dsc = new DataSourceConfig(); dsc.setUrl("jdbc:mysql://localhost:3306/ishareit?useUnicode=true&useSSL=false&characterEncoding=utf8"); // dsc.setSchemaName("public"); dsc.setDbType(DbType.MYSQL); dsc.setDriverName("com.mysql.cj.jdbc.Driver"); dsc.setUsername("root"); dsc.setPassword("123456"); mpg.setDataSource(dsc); // 包配置 PackageConfig pc = new PackageConfig(); pc.setModuleName(scanner("模块名")); pc.setParent("com.rjpyy"); pc.setEntity("entity"); pc.setMapper("mapper"); pc.setService("service"); pc.setController("controller"); mpg.setPackageInfo(pc); // 自定义配置 InjectionConfig cfg = new InjectionConfig() { @Override public void initMap() { // to do nothing } }; // 如果模板引擎是 freemarker String templatePath = "/templates/mapper.xml.ftl"; // 如果模板引擎是 velocity // String templatePath = "/templates/mapper.xml.vm"; // 自定义输出配置 List<FileOutConfig> focList = new ArrayList<>(); // 自定义配置会被优先输出 focList.add(new FileOutConfig(templatePath) { @Override public String outputFile(TableInfo tableInfo) { // 自定义输出文件名 , 如果你 Entity 设置了前后缀、此处注意 xml 的名称会跟着发生变化!! return projectPath + "/src/main/resources/mapper/" + tableInfo.getEntityName() + "Mapper" + StringPool.DOT_XML; } }); cfg.setFileOutConfigList(focList); mpg.setCfg(cfg); // 配置模板 TemplateConfig templateConfig = new TemplateConfig(); // 配置自定义输出模板 //指定自定义模板路径,注意不要带上.ftl/.vm, 会根据使用的模板引擎自动识别 // templateConfig.setEntity("templates/entity2.java"); // templateConfig.setService(); // templateConfig.setController(); templateConfig.setXml(null); mpg.setTemplate(templateConfig); // 策略配置 StrategyConfig strategy = new StrategyConfig(); strategy.setNaming(NamingStrategy.underline_to_camel); strategy.setColumnNaming(NamingStrategy.underline_to_camel); strategy.setEntityLombokModel(true); strategy.setRestControllerStyle(true); // 自动填充更新时间,创建时间 TableFill gmtCreate = new TableFill("create_time", FieldFill.INSERT); TableFill gmtModified = new TableFill("update_time", FieldFill.INSERT_UPDATE); ArrayList<TableFill> tableFills = new ArrayList<>(); tableFills.add(gmtCreate); tableFills.add(gmtModified); strategy.setTableFillList(tableFills); strategy.setInclude(scanner("表名,多个英文逗号分割").split(",")); strategy.setControllerMappingHyphenStyle(true); strategy.setTablePrefix("t_"); mpg.setStrategy(strategy); mpg.setTemplateEngine(new FreemarkerTemplateEngine()); mpg.execute(); } } ``` #### 整合**swagger2** > 启动地址 localhost:8990/swagger-ui.html 1. 添加依赖 ```xml <dependency> <groupId>io.springfox</groupId> <artifactId>springfox-swagger2</artifactId> <version>2.9.2</version> </dependency> <dependency> <groupId>io.springfox</groupId> <artifactId>springfox-swagger-ui</artifactId> <version>2.9.2</version> </dependency> ``` 2. 配置类 ```java @Configuration @EnableSwagger2 public class SwaggerConfig { public static final String VERSION = "1.0.0"; public static final String SWAGGER_SCAN_BASE_PACKAGE = "com.rjpyy.ishareit.controller"; @Bean public Docket createRestApi() { return new Docket(DocumentationType.SWAGGER_2) .pathMapping("/") .select() .apis(RequestHandlerSelectors.basePackage(SWAGGER_SCAN_BASE_PACKAGE)) .paths(PathSelectors.any()) .build().apiInfo(new ApiInfoBuilder() .title("IShareIT IT社区") // 文档标题 .description("IShareIT IT接口文档") //文档描述 .version(VERSION) // api版本 .contact(new Contact("gzhhh",null,"972134688@qq.com")) //联系人 .license("The Apache License") // 所属版权 .licenseUrl("http://www.apache.org/licenses/LICENSE-2.0") .build()); } } ``` 3. 在控制器加注解 ```java @RestController @Api(tags="User") @RequestMapping("/users") public class UserController { @Autowired IUserService userService; @GetMapping("/{id}") @ApiOperation(value = "根据id获取user", notes = "前端传递id", httpMethod = "GET") @ApiImplicitParam(name = "id",value = "用户id",required = true,paramType = "path",dataType = "Integer") public User getById(@PathVariable("id") long id){ return userService.getById(id); } } ``` [这里有更多注解](https://blog.csdn.net/xiaojin21cen/article/details/78654652) #### 统一结果封装 1. 在bean包新建`ResponseBean`类 ```java @Data public class ResponseBean { Integer code; //响应码 private String msg; //响应消息 private Object data; //具体前端数据 //返回成功的消息 public static ResponseBean success(Object data) { return new ResponseBean(ResultCode.SUCCESS, data); } public static ResponseBean success() { return new ResponseBean(ResultCode.SUCCESS,null); } //返回失败的消息 public static ResponseBean fail() { return new ResponseBean(ResultCode.ERROR, null); } // 自定义失败消息 public static ResponseBean fail(String msg){ return new ResponseBean(msg); } // 自定义失败状态码与信息 public static ResponseBean fail(Integer code,String msg){ return new ResponseBean(code,msg); } //返回失败的消息 public static ResponseBean notFound() { return new ResponseBean(ResultCode.NOT_FOUND, null); } //返回参数异常、内部错误等其他情况,根据resultCode构造响应结果 public static ResponseBean setStatus(ResultCode resultCode){ return new ResponseBean(resultCode, null); } //构造函数 private ResponseBean(ResultCode resultCode, Object data) { this.code = resultCode.getCode(); this.msg = resultCode.getMsg(); this.data = data; } private ResponseBean(String msg) { this.code = 400; this.msg = msg; this.data = null; } private ResponseBean(Integer code,String msg) { this.code = code; this.msg = msg; this.data = null; } } ``` 2. 新建枚举类(用于枚举响应状态码) ```java package com.zsc.ticketsys.enums; import lombok.Getter; /** * @author Hevean * @description 响应码枚举 */ @Getter public enum ResultCode { SUCCESS(200, "success"), FAILED(400, "failed"), UNAUTHORIZED(401, "没有相应权限"), NOT_FOUND(404, "没有相应权限"), VALIDATE_FAILED(405, "参数校验失败"), ERROR(500, "服务器内部异常"); private int code; private String msg; ResultCode(int code, String msg) { this.code = code; this.msg = msg; } } ``` 3. 返回的时候返回 ```java return ResponseBean.fail() ``` 4. 返回结果 ![](./imgs/result.png) #### 整合shiro+jwt+redis ![](./imgs/shiro.png) 开发说明: > 需要开启redis服务器,会将sessionid存到redis里 在需要认证才能访问的接口加上`@RequiresAuthentication`注解 需要角色权限的加上`@RequiresAuthentication`和`@RequiresRoles({"ROLE_admin"})` #### 统一异常处理 > 使用@RestControllerAdvice注解,所有的异常都会捕获到,除了Filter里抛出的异常 ```java @Slf4j @RestControllerAdvice public class GlobalExceptionHandler { // 用户登录认证不通过异常 @ExceptionHandler(ShiroException.class) public ResponseBean handle401(ShiroException e){ log.error("认证异常:-------->{}",e.getMessage()); return ResponseBean.setStatus(ResultCode.UNAUTHORIZED); } } ``` 开发说明:如果有需要返回给前端的异常,捕获后返回响应的信息。 #### 参数校验 表单验证时加上必要的参数校验 [具体参数校验注解](https://blog.csdn.net/justry_deng/article/details/86571671) > 具体看代码... #### 整合aliyunoss 上传文件 1. 添加依赖 ```java <dependency> <groupId>com.aliyun.oss</groupId> <artifactId>aliyun-sdk-oss</artifactId> <version>2.8.3</version> </dependency> ``` 2. 在配置文件中添加 配置 ``` aliyun: bucketName: xxx accessKeyId: xxx accessKeySecret: xxx endpoint: xxx ``` 3. 编写ossUtil ```java @Component @Slf4j public class OssUtil { @Value("${aliyun.endpoint}") private String endpoint; @Value("${aliyun.accessKeyId}") private String accessKeyId; @Value("${aliyun.accessKeySecret}") private String accessKeySecret; @Value("${aliyun.bucketName}") private String bucketName; //文件储存前缀目录 private String prefiledir = "ishareit/"; // 文件储存路径 private String filedir = ""; /** * * 上传图片 * @param file * @return */ private String uploadImg2Oss(MultipartFile file) { // 图片最大20M if (file.getSize() > 1024 * 1024 *20) { return "图片太大";//RestResultGenerator.createErrorResult(ResponseEnum.PHOTO_TOO_MAX); } String originalFilename = file.getOriginalFilename(); // 获取文件后缀 String substring = originalFilename.substring(originalFilename.lastIndexOf(".")).toLowerCase(); //生成随机数 Random random = new Random(); // 生成文件名 String name = random.nextInt(10000) + System.currentTimeMillis() + substring; try { InputStream inputStream = file.getInputStream(); this.uploadFile2OSS(inputStream, name); return name;//RestResultGenerator.createSuccessResult(name); } catch (Exception e) { return "上传失败";//RestResultGenerator.createErrorResult(ResponseEnum.PHOTO_UPLOAD); } } /** * 上传图片获取fileUrl * @param instream * @param fileName * @return */ private String uploadFile2OSS(InputStream instream, String fileName) { String ret = ""; try { //创建上传Object的Metadata ObjectMetadata objectMetadata = new ObjectMetadata(); objectMetadata.setContentLength(instream.available()); objectMetadata.setCacheControl("no-cache"); objectMetadata.setHeader("Pragma", "no-cache"); objectMetadata.setContentType(getcontentType(fileName.substring(fileName.lastIndexOf(".")))); objectMetadata.setContentDisposition("inline;filename=" + fileName); //上传文件 // 核心 OSSClient ossClient = new OSSClient(endpoint, accessKeyId, accessKeySecret); PutObjectResult putResult = ossClient.putObject(bucketName, filedir + fileName, instream, objectMetadata); ret = putResult.getETag(); } catch (IOException e) { log.error(e.getMessage(), e); } finally { try { if (instream != null) { instream.close(); } } catch (IOException e) { e.printStackTrace(); } } return ret; } private static String getcontentType(String FilenameExtension) { if (FilenameExtension.equalsIgnoreCase(".bmp")) { return "image/bmp"; } if (FilenameExtension.equalsIgnoreCase(".gif")) { return "image/gif"; } if (FilenameExtension.equalsIgnoreCase(".jpeg") || FilenameExtension.equalsIgnoreCase(".jpg") || FilenameExtension.equalsIgnoreCase(".png")) { return "image/jpg"; } if (FilenameExtension.equalsIgnoreCase(".html")) { return "text/html"; } if (FilenameExtension.equalsIgnoreCase(".txt")) { return "text/plain"; } if (FilenameExtension.equalsIgnoreCase(".vsd")) { return "application/vnd.visio"; } if (FilenameExtension.equalsIgnoreCase(".pptx") || FilenameExtension.equalsIgnoreCase(".ppt")) { return "application/vnd.ms-powerpoint"; } if (FilenameExtension.equalsIgnoreCase(".docx") || FilenameExtension.equalsIgnoreCase(".doc")) { return "application/msword"; } if (FilenameExtension.equalsIgnoreCase(".xml")) { return "text/xml"; } return "image/jpg"; } /** * 获取图片路径 * @param fileUrl * @return */ private String getImgUrl(String fileUrl) { if (!StringUtils.isEmpty(fileUrl)) { String[] split = fileUrl.split("/"); String url = this.getUrl(this.filedir + split[split.length - 1]); return url; } return null; } /** * 获得url链接 * * @param key * @return */ private String getUrl(String key) { // 设置URL过期时间为10年 3600l* 1000*24*365*10 Date expiration = new Date(new Date().getTime() + 3600l * 1000 * 24 * 365 * 10); // 生成URL OSSClient ossClient = new OSSClient(endpoint, accessKeyId, accessKeySecret); URL url = ossClient.generatePresignedUrl(bucketName, key, expiration); log.warn("url+ossClient:"+url); if (url != null) { return url.toString(); } return null; } /** * 多图片上传 * @param fileList * @return */ public String uploadImageList(List<MultipartFile> fileList) { this.filedir = this.prefiledir+"muti/"; String fileUrl = ""; String str = ""; String photoUrl = ""; for(int i = 0;i< fileList.size();i++){ fileUrl = uploadImg2Oss(fileList.get(i)); str = getImgUrl(fileUrl); str = str.split("\\?")[0]; if(i == 0){ photoUrl = str; }else { photoUrl += "," + str; } } return photoUrl.trim(); } /** * 单个图片上传 * * @param file * @param type 1为头像文件夹 ,2位文章缩略图 * @return */ public String uploadOneImage(MultipartFile file,Integer type){ if(type==1){ this.filedir = this.prefiledir + "avatar/"; }else{ this.filedir = this.prefiledir + "article/"; } String fileUrl = uploadImg2Oss(file); log.warn("fileurl:"+fileUrl); String str = getImgUrl(fileUrl); return str.trim(); } } ``` #### 获取当前用户的信息 ```java AccountProfile accountProfile = ShiroUtil.getProfile(); ``` #### 后端遇到的坑 ##### 1. 跨域sessionid问题 使用cors跨域默认是不带cookies的,而后端获取不到cookies的SESSIONID就无法判断用户是哪个用户,导致每次请求都会使服务器增加一个session。也是就跨域sessionid问题。 解决办法:前端发送请求时加上 ```js //jQuery xhrFields: { withCredentials: true, } // axios axios.default.withCredentials=true ``` 后端也同时开启`httpServletResponse.setHeader("Access-Control-Allow-Credentials", "true");` 即可解决这个问题,后来发现,我他妈用jwt还要session干嘛,概念混乱.. [CORS跨域详解]([http://www.ruanyifeng.com/blog/2016/04/cors.html](http://www.ruanyifeng.com/blog/2016/04/cors.html)) 因为用json数据,所以请求都是application/json属于非简单请求,所以每次请求都需要发一次OPTION请求,也称预检,但我们不希望他每次都发,可以设置一段时间内发送一次,设置如下 服务器设置Max-Age`httpServletResponse.setHeader("Access-Control-Max-Age", "43200"); // 预检时间间隔为60 * 60 * 12 = 43200 (12小时)` ##### 2. 在控制器上加@Transactional导致该Controller无法访问 ##### 3. 插入数据时,数据库时间对应不上 时区问题,数据库连接url使用`serverTimezone=Asia/Shanghai`时区 ##### 4. 数据库字段命名为数据库关键字导致的错误 article表有个文章描述的字段,开始用desc命名,但是插入的时候报错 ```java bad SQL grammar []; nested exception is java.sql.SQLSyntaxErrorException: You have an error in your SQL syntax; check the manual that corresponds to your MySQL server version for the right syntax to use near 'desc ) VALUES ( '', ``` 后来网上查了才知道是命名错误

简介

暂无描述 展开 收起
Apache-2.0
取消

发行版

暂无发行版

贡献者

全部

近期动态

加载更多
不能加载更多了
马建仓 AI 助手
尝试更多
代码解读
代码找茬
代码优化
Java
1
https://gitee.com/okaykai/iShare_IT.git
git@gitee.com:okaykai/iShare_IT.git
okaykai
iShare_IT
iShare IT
master

搜索帮助