一.技术实现
项目演示地址:可私聊作者获取(演示地址不定时变化)
前端
- vue+elementui;
后端:
- SpringBoot
- OAuth2
- Spring Security
- Redis
- mybatis-plus
- mysql
- swagger
二.前言
淘柳职网站:淘柳职
本项目完全是模拟淘柳职,在此基础上进一步完善和改进,同时也增加了额外的功能,无论是前端功能和样式,除了仿照基本功能外,还在兼容性和样式、细节加以了更多的优化,所花费的时间也是非常大的,从数据库表设计、后端搭建、前端搭建等,足足花费了一周的时间。
可能细节无法一一细说,毕竟这是一个完整的项目,如需更多完整信息,可留言或私聊,马上回复!
三.成品效果对比
1.淘柳职的登录+注册:
2.作者的登录+注册:
主要是作者觉得他的登录样式有点老套洋气,所以作者自己设计了一个:
登录页源码:
<template><div id="body"><div style="display: flex;width: 100%;height: 100%;overflow: hidden;"><div class="login-modal"><div class="title">{{loginType === 'login'?'登录':(loginType==='forget'?'重置密码':'注册')}}</div><el-form class="login-form":rules="loginRules"ref="loginForm":model="loginForm"label-width="0"><el-form-item prop="phone"><el-inputplaceholder="请输入手机号"prefix-icon="el-icon-mobile-phone"v-model.number="loginForm.phone"clearable></el-input></el-form-item><el-form-item prop="password"><el-input:type="passwordType"placeholder="请输入密码"prefix-icon="el-icon-lock"v-model="loginForm.password"clearable></el-input></el-form-item><el-form-item prop="confirmPassword" v-show="loginType === 'register'"><el-input:type="passwordType"placeholder="请再次输入密码"prefix-icon="el-icon-lock"v-model="loginForm.confirmPassword"clearable></el-input></el-form-item><el-form-item><el-row :span="24"><el-col :span="12"><el-checkbox v-model="loginForm.rememberPwd">记住密码</el-checkbox></el-col><el-col :span="12"><el-popoverplacement="top-start"title=""width="200"trigger="hover"content="忘记密码请联系系统管理员"><span style="color: #1890ff;float: right;" slot="reference">忘记密码</span></el-popover></el-col></el-row></el-form-item><el-form-item><el-button :type="loginType === 'login'?'success':'danger'"style="width: 100%;"@click.native.prevent="handleLogin"class="login-submit">{{loginType === 'login'?'登录':(loginType==='forget'?'重置密码':'注册')}}</el-button></el-form-item><div v-if="loginType === 'login'" style="text-align: center;font-size: 14px;">没有账号?<span style="cursor: pointer;color: #df1f20;" @click="changeModalType('register')">免费注册</span></div><div v-if="loginType !== 'login'" style="text-align: center;font-size: 14px;">已有账号?<span style="cursor: pointer;color: #df1f20;" @click="changeModalType('login')">返回登录</span></div></el-form></div></div></div>
</template><script>import {userRegister} from '@/api/login';export default {name: "index",data() {return {loginType:'login',passwordType: "password",loginForm: {phone: "",password: "",confirmPassword: "",rememberPwd: false,},loginRules: {phone: [{required: true, message: "请输入手机号", trigger: "change"},{ type: 'number', message: '手机号格式错误',trigger: "blur"}],password: [{required: true, message: "请输入密码", trigger: "change"},{min: 6, message: "密码长度最少为6位", trigger: "blur"}],confirmPassword: [{required: false, message: "请再次输入密码", trigger: "change"},{min: 6, message: "密码长度最少为6位", trigger: "blur"}]},};},mounted() {},methods: {homePage(){this.$router.push({path: '/index'});},showPassword() {this.passwordType === ""? (this.passwordType = "password"): (this.passwordType = "");},changeModalType(type){this.loginType = type;this.$refs.loginForm.resetFields();if(type === 'login'){this.loginRules['confirmPassword'][0]['required'] = false;}else{this.loginRules['confirmPassword'][0]['required'] = true;}},handleLogin() {if(this.loginType === 'login'){this.login();}else if(this.loginType === 'register'){this.register();}},login() {//登录this.$refs.loginForm.validate(valid => {if (valid) {const loading = this.$loading({lock: true,text: '登录中,请稍后。。。',spinner: "el-icon-loading"});this.$store.dispatch('login',this.loginForm).then((res)=>{if(res.code === 200){this.$notify({title: '登录成功',message: res.data.nickname+',欢迎您!',type: 'success'});this.$router.push({path: '/'});}}).finally(() =>loading.close());}});},register() {//注册this.$refs.loginForm.validate(valid => {if (valid) {const loading = this.$loading({lock: true,text: '注册中,请稍后。。。',spinner: "el-icon-loading"});userRegister(this.loginForm).then(res => {if(res.code === 200){this.$notify({title: '注册成功',message: '请登录',type: 'success'});}}).finally(() =>loading.close())}});},}}
</script><style scoped>#body{margin: 0;padding: 0;width: 100%;height: 100%;background-size: 100% 100%;background-image: linear-gradient(to top, rgba(255, 95, 45, 0.27), rgba(211, 155, 5, 0.2)), url("../../../public/img/login-bg.png");background-repeat: no-repeat;}.name{line-height: 50px;font-size: 30px;font-weight: 700;color: #FFFFFF;margin-left: 10px;}.login-modal{position: relative;width: 420px;height: 450px;margin: 0 auto;top: 50%;margin-top: -225px;background-color: #FFFFFF;border-radius: 5px;}.title{height: 80px;line-height: 100px;font-weight: 600;text-align: center;font-size: 25px;}.login-form{margin: 20px 40px;}
</style>
3.首页帖子
与淘柳职相比,作者设计的增加了分页和底部子模块(没有分页怎么可以呢,性能无法把控~),其他几乎一样,对了,右上角也增加了昵称展示
4.详情
与淘柳职相比,大图展示尽量保持图片原有的分辨率,然后右边详细描述文字使用了textarea只读文本框,为了保持与发布帖子输入的内容样式一致,同时增加了关注发帖用户功能(如果是自己的帖子,关注按钮会屏蔽),同时也增加了浏览次数更新,停留页面2秒钟就会浏览次数+1;
5.校园分享
校园分享几个tab使用同一个页面,因为只是查询条件不一样,与淘柳职相比,作者这里也是增加了分页(往下滚动分页,技术采用 Element - The world's most popular Vue UI framework),这里滚动分页放在tab父级页面,作者也是花费了挺长时间来调试实现
tab源码:
<template><div class="body" v-infinite-scroll="loadFun"><div style="font-size: 14px;margin: 0 320px;"><div class="share-tab"><el-tabs v-model="activeName" @tab-click="handleClick"><el-tab-pane label="全部" name="first"><all ref="first"></all></el-tab-pane><el-tab-pane label="官塘校区" name="second"><all ref="second"></all></el-tab-pane><el-tab-pane label="社湾校区" name="third"><all ref="third"></all></el-tab-pane><el-tab-pane label="我的分享" name="fourth"><all ref="fourth"></all></el-tab-pane></el-tabs></div></div></div>
</template><script>import all from "./all.vue";export default {components: {all,},data() {return {activeName: 'first',school: null};},mounted() {this.$refs[this.activeName].init(this.school);},methods: {handleClick(tab, event) {if(tab.index !== '0'){this.school = tab.index;}else{this.school = null;}this.$refs[tab.name].init(this.school);console.log(tab, event);},loadFun() {this.$refs[this.activeName].load();},}};
</script><style>.body{margin: 0;padding: 20px 0 0 0;width: 100%;height: calc(100% - 80px);/*background-size: 100% 100%;*//*background-image: url("../../../public/img/background-detail.jpg");*//*background-repeat: no-repeat;*//*overflow: hidden;*/background-image: url("../../../public/img/background-detail.jpg");background-size: cover;background-attachment: fixed;overflow: scroll;}/*.share-tab{*//* overflow: scroll;*//*}*//*.share-tab .el-tabs__content{*//* overflow: scroll!important;*//*}*/.share-tab .el-tabs__header{background-color: #ffffff!important;padding: 0px 20px!important;}.share-tab .el-tabs__nav{height: 60px!important;line-height: 60px!important;}
</style>
6.关于我们,自由发挥,不重要
7.发布帖子
样式也是与淘柳职几乎一样,作者没有使用淘柳职发布过,但是作者这里有一点是特别实现的,就是图片上传功能,一般偷懒的做法就是选择图片后马上就会上传到服务器,然后服务器返回图片路径,但是作者很抗拒这种做法,所以作者是选择图片后不会立马提交到服务器,只有点击“立即发布”最后一步才会一起提交到服务器(这样做的好处是不会乱上传图片文件,不会乱占用服务器资源和造成过多垃圾图片)
后端指定上传目录映射:
8.个人中心
与淘柳职几乎一样,增加了关注数量:
信息修改,回显头像及其他信息:
9.我的收藏
淘柳职的 - 我的收藏
作者设计的 - 我的收藏(作者当然是觉得自己的更好看一点,哈哈~),同时增加点击跳转详情功能
10.我的关注
与淘柳职相比,这是作者自己额外设计实现的功能
11.我的粉丝
与淘柳职相比,这也是作者自己额外设计实现的功能
四.数据表mysql
-- 2022-10-19 用户信息
CREATE TABLE `user_info`
(`id` bigint NOT NULL COMMENT '主键',`nickname` varchar(10) NOT NULL COMMENT '用户名称',`phone` varchar(20) NOT NULL COMMENT '手机号',`password` varchar(255) NOT NULL COMMENT '登录密码 加密',`original_password` varchar(255) NOT NULL COMMENT '登录密码 明文密码',`avatar` varchar(225) DEFAULT NULL COMMENT '头像',`gender` TINYINT(1) DEFAULT 0 COMMENT '性别 0保密 1男 2女',`status` TINYINT(1) DEFAULT 0 COMMENT '是否禁用 0否 1是',`hobby` varchar(225) DEFAULT NULL COMMENT '爱好',`remark` varchar(225) DEFAULT NULL COMMENT '备注',`deleted` tinyint(1) DEFAULT '0' COMMENT '逻辑删除标记 是否已删除: 0否 1是',`create_time` datetime(0) COMMENT '创建时间',`update_time` datetime(0) COMMENT '更新时间',PRIMARY KEY (`id`) USING BTREE,UNIQUE KEY `phone` (`phone`) USING BTREE
) ENGINE = InnoDBDEFAULT CHARSET = utf8mb4 COMMENT ='用户信息';-- 首页轮播图CREATE TABLE `banner`
(`id` bigint NOT NULL COMMENT '主键',`img_path` varchar(500) DEFAULT NULL COMMENT '图片路径',`sort` INT(11) NOT NULL DEFAULT 0 COMMENT '排序',`remark` varchar(225) DEFAULT NULL COMMENT '备注',`status` TINYINT(1) NOT NULL DEFAULT 0 COMMENT '是否禁用 0否 1是',`deleted` tinyint(1) NOT NULL DEFAULT 0 COMMENT '逻辑删除标记 是否已删除: 0否 1是',`create_time` datetime(0) COMMENT '创建时间',`update_time` datetime(0) COMMENT '更新时间',PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDBDEFAULT CHARSET = utf8mb4 COMMENT ='首页轮播图';-- 用户关注CREATE TABLE `follow`
(`id` bigint NOT NULL COMMENT '主键',`user_id` bigint NOT NULL COMMENT '用户主键',`be_followed_user_id` bigint NOT NULL COMMENT '被关注用户主键',`status` tinyint(1) DEFAULT 0 COMMENT '是否已读 0否 1是',`deleted` tinyint(1) DEFAULT 0 COMMENT '逻辑删除标记 是否已删除: 0否 1是',`create_time` datetime(0) COMMENT '创建时间',`update_time` datetime(0) COMMENT '更新时间',PRIMARY KEY (`id`) USING BTREE,UNIQUE KEY `user_id_be_followed_user_id` (`user_id`,`be_followed_user_id`) USING BTREE,KEY `user_id` (`user_id`) USING BTREE,KEY `be_followed_user_id` (`be_followed_user_id`) USING BTREE
) ENGINE = InnoDBDEFAULT CHARSET = utf8mb4 COMMENT ='用户关注';-- 帖子CREATE TABLE `posts`
(`id` bigint NOT NULL COMMENT '主键',`user_id` bigint NOT NULL COMMENT '帖子所属用户主键',`posts_type` TINYINT(1) NOT NULL COMMENT '帖子类型 1闲置帖 2校园帖',`title` varchar(225) NOT NULL COMMENT '标题',`content` varchar(1000) DEFAULT NULL COMMENT '内容',`school` TINYINT(1) NOT NULL COMMENT '校区 1官塘校区 2社湾校区',`price` decimal(10, 2) NOT NULL DEFAULT 0 COMMENT '单价',`cover_path` varchar(500) DEFAULT NULL COMMENT '封面图片',`img_path` varchar(2000) DEFAULT NULL COMMENT '图片,多张英文逗号分割',`browse_num` int(11) NOT NULL DEFAULT 0 COMMENT '浏览数量',`collect_num` int(11) NOT NULL DEFAULT 0 COMMENT '收藏数量',`like_num` int(11) NOT NULL DEFAULT 0 COMMENT '点赞数量',`comment_num` int(11) NOT NULL DEFAULT 0 COMMENT '评论数量',`version` int(11) NOT NULL DEFAULT 0 COMMENT '版本号',`status` TINYINT(1) NOT NULL DEFAULT 0 COMMENT '是否禁用 0否 1是',`deleted` tinyint(1) NOT NULL DEFAULT 0 COMMENT '逻辑删除标记 是否已删除: 0否 1是',`create_time` datetime(0) COMMENT '创建时间',`update_time` datetime(0) COMMENT '更新时间',PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDBDEFAULT CHARSET = utf8mb4 COMMENT ='帖子';-- 用户收藏CREATE TABLE `collect`
(`id` bigint NOT NULL COMMENT '主键',`user_id` bigint NOT NULL COMMENT '用户主键',`posts_id` bigint NOT NULL COMMENT '帖子主键',`posts_user_id` bigint NOT NULL COMMENT '帖子所属用户主键',`status` tinyint(1) DEFAULT 0 COMMENT '是否已读 0否 1是',`deleted` tinyint(1) DEFAULT 0 COMMENT '逻辑删除标记 是否已删除: 0否 1是',`create_time` datetime(0) COMMENT '创建时间',`update_time` datetime(0) COMMENT '更新时间',PRIMARY KEY (`id`) USING BTREE,UNIQUE KEY `user_id_posts_id` (`user_id`,`posts_id`) USING BTREE,KEY `user_id` (`user_id`) USING BTREE,KEY `posts_id` (`posts_id`) USING BTREE
) ENGINE = InnoDBDEFAULT CHARSET = utf8mb4 COMMENT ='用户收藏';-- 帖子点赞CREATE TABLE `posts_like`
(`id` bigint NOT NULL COMMENT '主键',`user_id` bigint NOT NULL COMMENT '用户主键',`posts_id` bigint NOT NULL COMMENT '帖子主键',`posts_user_id` bigint NOT NULL COMMENT '帖子所属用户主键',`status` tinyint(1) DEFAULT 0 COMMENT '是否已读 0否 1是',`deleted` tinyint(1) DEFAULT 0 COMMENT '逻辑删除标记 是否已删除: 0否 1是',`create_time` datetime(0) COMMENT '创建时间',`update_time` datetime(0) COMMENT '更新时间',PRIMARY KEY (`id`) USING BTREE,UNIQUE KEY `user_id_posts_id` (`user_id`,`posts_id`) USING BTREE,KEY `user_id` (`user_id`) USING BTREE,KEY `posts_id` (`posts_id`) USING BTREE
) ENGINE = InnoDBDEFAULT CHARSET = utf8mb4 COMMENT ='帖子点赞';-- 帖子评论CREATE TABLE `posts_comment`
(`id` bigint NOT NULL COMMENT '主键',`parent_id` bigint DEFAULT NULL COMMENT '上级评论主键',`parent_user_id` bigint DEFAULT NULL COMMENT '上级评论用户主键',`user_id` bigint NOT NULL COMMENT '用户主键',`posts_id` bigint NOT NULL COMMENT '帖子主键',`posts_user_id` bigint NOT NULL COMMENT '帖子所属用户主键',`content` varchar(1000) DEFAULT NULL COMMENT '评论内容',`status` tinyint(1) DEFAULT 0 COMMENT '是否已读 0否 1是',`deleted` tinyint(1) DEFAULT 0 COMMENT '逻辑删除标记 是否已删除: 0否 1是',`create_time` datetime(0) COMMENT '创建时间',`update_time` datetime(0) COMMENT '更新时间',PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDBDEFAULT CHARSET = utf8mb4 COMMENT ='帖子评论';
五.帖子发布相关后端源码
PostsController:
package com.love.product.controller;import com.love.product.entity.Posts;
import com.love.product.entity.base.Result;
import com.love.product.entity.base.ResultPage;
import com.love.product.entity.req.PostsPageReq;
import com.love.product.entity.req.PostsReq;
import com.love.product.entity.vo.PostsVO;
import com.love.product.service.PostsService;
import com.love.product.util.JwtUtil;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiImplicitParam;
import io.swagger.annotations.ApiImplicitParams;
import io.swagger.annotations.ApiOperation;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;import javax.annotation.Resource;
import java.math.BigDecimal;/*** @author hjf* @date 2022-10-19 10:26* @describe 帖子controller*/
@Api(tags = "帖子")
@Slf4j
@RestController
@RequestMapping("/posts")
public class PostsController {@Resourceprivate PostsService postsService;@PostMapping("/add")@ApiOperation(value = "添加", notes = "添加")@ApiImplicitParams({@ApiImplicitParam(name = "postsType", value = "发布类型", required = true, dataType = "Integer", paramType = "query"),@ApiImplicitParam(name = "title", value = "标题", required = true, dataType = "String", paramType = "query"),@ApiImplicitParam(name = "content", value = "内容", required = true, dataType = "String", paramType = "query"),@ApiImplicitParam(name = "school", value = "校区", required = true, dataType = "Integer", paramType = "query"),@ApiImplicitParam(name = "price", value = "校区", required = false, dataType = "BigDecimal", paramType = "query"),@ApiImplicitParam(name = "files", value = "上传图片列表", required = false, dataType = "MultipartFile[]", paramType = "query")})public Result<Posts> add(@RequestParam("postsType") Integer postsType,@RequestParam("title") String title,@RequestParam("content") String content,@RequestParam("school") Integer school,@RequestParam(value = "price",required = false) BigDecimal price,@RequestParam(value = "files",required = false) MultipartFile[] files) {PostsReq postsReq = new PostsReq();postsReq.setPostsType(postsType);postsReq.setTitle(title);postsReq.setContent(content);postsReq.setSchool(school);postsReq.setPrice(price);postsReq.setFiles(files);return postsService.add(JwtUtil.getUserId(),postsReq);}@ApiOperation("分页")@PostMapping("/getPage")public ResultPage<PostsVO> getPage(@RequestBody PostsPageReq postsPageReq) {return postsService.getPage(JwtUtil.getUserId(),postsPageReq);}@ApiOperation("详情")@GetMapping("/getDetail")public Result<PostsVO> getDetail(@RequestParam("id") Long id) {return postsService.getDetail(JwtUtil.getUserId(),id);}@ApiOperation("浏览")@GetMapping("/browse")public Result<?> browse(@RequestParam("id") Long id) {return postsService.browse(id);}
}
PostsService:
package com.love.product.service;import com.baomidou.mybatisplus.extension.service.IService;
import com.love.product.entity.Posts;
import com.love.product.entity.base.Result;
import com.love.product.entity.base.ResultPage;
import com.love.product.entity.req.PostsPageReq;
import com.love.product.entity.req.PostsReq;
import com.love.product.entity.vo.PostsVO;import java.util.List;
import java.util.Map;/*** @author hjf* @date 2022-10-19 10:26*/
public interface PostsService extends IService<Posts> {Result<Posts> add(Long userId,PostsReq postsReq);ResultPage<PostsVO> getPage(Long userId,PostsPageReq postsPageReq);Result<PostsVO> getDetail(Long userId,Long id);Result<?> browse(Long id);Map<Long, PostsVO> listByIds(List<Long> postsIds);
}
PostsServiceImpl:
package com.love.product.service.impl;import cn.hutool.core.bean.BeanUtil;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.toolkit.IdWorker;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.love.product.entity.Posts;
import com.love.product.entity.PostsLike;
import com.love.product.entity.base.Result;
import com.love.product.entity.base.ResultPage;
import com.love.product.entity.req.PostsPageReq;
import com.love.product.entity.req.PostsReq;
import com.love.product.entity.vo.PostsVO;
import com.love.product.entity.vo.UserInfoVO;
import com.love.product.enumerate.PostsType;
import com.love.product.enumerate.School;
import com.love.product.enumerate.YesOrNo;
import com.love.product.mapper.PostsMapper;
import com.love.product.service.*;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.stereotype.Service;
import org.springframework.web.multipart.MultipartFile;import javax.annotation.Resource;
import java.time.LocalDateTime;
import java.util.*;
import java.util.stream.Collectors;/*** @author hjf* @date 2022-10-19 10:26*/
@Slf4j
@Service
public class PostsServiceImpl extends ServiceImpl<PostsMapper, Posts> implements PostsService {@Resourceprivate FileUploadService fileUploadService;@Resourceprivate UserInfoService userInfoService;@Resourceprivate CollectService collectService;@Resourceprivate FollowService followService;@Resourceprivate PostsLikeService postsLikeService;/*** 发布帖子*/@Overridepublic Result<Posts> add(Long userId,PostsReq postsReq) {PostsType postsType = PostsType.valueOf(postsReq.getPostsType());School school = School.valueOf(postsReq.getSchool());if(postsType == null){return Result.failMsg("请选择帖子类型");}if(StringUtils.isBlank(postsReq.getTitle())){return Result.failMsg("标题不能为空");}if(StringUtils.isBlank(postsReq.getContent())){return Result.failMsg("内容不能为空");}if(school == null){return Result.failMsg("请选择校区");}List<String> imgPathList = new ArrayList<>();if(postsType.equals(PostsType.LEAVE)){//闲置帖if(postsReq.getPrice() == null || postsReq.getPrice().doubleValue() <= 0){return Result.failMsg("请输入价格");}if(postsReq.getFiles() == null || postsReq.getFiles().length == 0){return Result.failMsg("请至少上传一张图片");}}if(postsReq.getFiles() != null){if(postsReq.getFiles().length > 9){return Result.failMsg("最多可上传9张图片");}//上传图片for(MultipartFile multipartFile : postsReq.getFiles()){String imgPath = fileUploadService.uploadImage(multipartFile);imgPathList.add(imgPath);}}LocalDateTime now = LocalDateTime.now();Posts posts = new Posts();BeanUtil.copyProperties(postsReq,posts);posts.setId(IdWorker.getId());posts.setUserId(userId);posts.setCreateTime(now);posts.setUpdateTime(now);if(imgPathList.size() > 0){posts.setCoverPath(imgPathList.get(0));posts.setImgPath(imgPathList.stream().map(String::valueOf).collect(Collectors.joining(",")));}boolean flag = save(posts);if(flag){return Result.OK("发布成功",posts);}return Result.failMsg("发布失败,请重试");}/*** 分页*/@Overridepublic ResultPage<PostsVO> getPage(Long userId,PostsPageReq postsPageReq) {if(userId == null && Objects.equals(postsPageReq.getSchool(),3)){return ResultPage.FAIL(403,"请登录");}LambdaQueryWrapper<Posts> queryWrapper = new LambdaQueryWrapper<>();queryWrapper.eq(postsPageReq.getPostsType()!=null,Posts::getPostsType, postsPageReq.getPostsType());queryWrapper.eq(postsPageReq.getSchool()!=null&&postsPageReq.getSchool()!=3,Posts::getSchool, postsPageReq.getSchool());queryWrapper.eq(Objects.equals(postsPageReq.getSchool(),3),Posts::getUserId, userId);queryWrapper.eq(Posts::getStatus, YesOrNo.NO.getValue());queryWrapper.orderByDesc(Posts::getCreateTime);Page<Posts> page = page(postsPageReq.build(), queryWrapper);List<PostsVO> list = new ArrayList<>();List<Long> userIds = new ArrayList<>();List<Long> postsIds = new ArrayList<>();if (page.getTotal() > 0) {list = page.getRecords().stream().map(posts -> {PostsVO postsVO = BeanUtil.copyProperties(posts, PostsVO.class);School school = School.valueOf(postsVO.getSchool());postsVO.setSchoolName(school!=null?school.getText():"");initImgPath(postsVO);userIds.add(postsVO.getUserId());postsIds.add(postsVO.getId());return postsVO;}).collect(Collectors.toList());}Map<Long, UserInfoVO> userInfoVOMap;Map<Long, PostsLike> postsLikeHashMap;if(Objects.equals(postsPageReq.getPostsType(),PostsType.SCHOOL.getValue()) && list.size() > 0){userInfoVOMap = userInfoService.listByIds(userIds);postsLikeHashMap = postsLikeService.listByUserId(userId,postsIds);Map<Long, UserInfoVO> finalUserInfoVOMap = userInfoVOMap;Map<Long, PostsLike> finalPostsLikeHashMap = postsLikeHashMap;list.forEach(item -> {UserInfoVO userInfoVO = finalUserInfoVOMap.get(item.getUserId());item.setUserInfo(userInfoVO);item.setLike(false);PostsLike postsLike = finalPostsLikeHashMap.get(item.getId());if(postsLike != null){item.setLike(true);}});}return ResultPage.OK(page.getTotal(), page.getCurrent(), page.getSize(), list);}/*** 详情**/@Overridepublic Result<PostsVO> getDetail(Long userId,Long id) {Posts posts = getById(id);if(posts == null || posts.getStatus().equals(YesOrNo.YES.getValue())){return Result.failMsg("帖子不存在或已下架");}PostsVO postsVO = BeanUtil.copyProperties(posts, PostsVO.class);School school = School.valueOf(postsVO.getSchool());postsVO.setSchoolName(school!=null?school.getText():"");UserInfoVO userInfoVO = userInfoService.getUserInfoById(posts.getUserId());postsVO.setUserInfo(userInfoVO);initImgPath(postsVO);postsVO.setCollect(false);postsVO.setFollow(false);if(userId != null && collectService.getDetail(userId,posts.getId()) != null){postsVO.setCollect(true);}if(userId != null && followService.getDetail(userId,posts.getUserId()) != null){postsVO.setFollow(true);}return Result.OK(postsVO);}/*** 更新浏览次数*/@Overridepublic Result<?> browse(Long id) {Posts posts = getById(id);if(posts != null){posts.setBrowseNum(posts.getBrowseNum() + 1);saveOrUpdate(posts);}return Result.OK();}/*** 拼接图片获取绝对路径*/private void initImgPath(PostsVO postsVO){postsVO.setCoverPath(fileUploadService.getImgPath(postsVO.getCoverPath()));if(StringUtils.isNotEmpty(postsVO.getImgPath())){String[] arr = postsVO.getImgPath().split(",");List<String> list = Arrays.asList(arr);List<String> imgPathList = new ArrayList<>();list.forEach(item-> {imgPathList.add(fileUploadService.getImgPath(item));});postsVO.setImgPath(imgPathList.stream().map(String::valueOf).collect(Collectors.joining(",")));}}/*** 批量获取*/@Overridepublic Map<Long, PostsVO> listByIds(List<Long> postsIds){Map<Long, PostsVO> postsHashMap = new HashMap<>();LambdaQueryWrapper<Posts> queryWrapper = new LambdaQueryWrapper<>();queryWrapper.in(Posts::getId,postsIds);List<Posts> postsList = list(queryWrapper);postsList.forEach(item -> {PostsVO postsVO = BeanUtil.copyProperties(item, PostsVO.class);initImgPath(postsVO);postsHashMap.put(postsVO.getId(), postsVO);});return postsHashMap;}
}
文件图片上传,存到上面说的路径(D:\school\image\),返回的是相对路径,存储到数据库:
@Overridepublic String uploadImage(MultipartFile file) {if (file == null)throw new BizException("图片不能为空");//得到上传文件的文件名String fileName = file.getOriginalFilename();//以传入的字符串开头,到该字符串的结尾,前开后闭String suffixName = fileName.substring(fileName.lastIndexOf("."));long size = file.getSize();double mul = NumberUtil.div(size, (1024 * 1024), 2);// 自定义异常if (mul > 2)throw new BizException("图片大小不能大于2M");if (!isImage(suffixName))throw new BizException("不是图片格式");// 这里可以用uuid等 拼接新图片名String newFileName = UUID.randomUUID().toString().replace("-", "") + suffixName;// 创建路径String destFileName = fileUploadConfig.getImageRealPath() + File.separator + newFileName;File destFile = new File(destFileName);if (!destFile.getParentFile().exists())destFile.getParentFile().mkdirs();try {//将图片保存到文件夹里file.transferTo(new File(destFileName));} catch (IOException e) {e.printStackTrace();throw new BizException("图片上传错误");}//返回相对路径存储return fileUploadConfig.getImageMapperPath() + newFileName;}
文件在电脑本地D盘,default-avatar: /image/default-avatar.png是用户注册默认的图片,预先放到该目录下:
重点是存放在D盘文件图片是如何获取路径让前端显示呢,其实是通过配置映射路径,数据库/image/映射成D:\school\image\,就是以下这一句代码即可,作者放在了swagger配置里面:
swagger开发文档:
redis下载压缩包解压,启动 redis-server.exe redis.windows.conf: