一.技术实现

项目演示地址:可私聊作者获取(演示地址不定时变化)

前端

  1. vue+elementui;

后端:

  1. SpringBoot
  2. OAuth2
  3. Spring Security
  4. Redis
  5. mybatis-plus
  6. mysql
  7. 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:

六.感谢看到这里,还有很多细节无法一一说,毕竟这是一个完整的项目,如需帮助可留言或私聊~,马上回复!

vue+elementui+springboot前后端分离实现学校帖子网站,模拟“淘柳职”学校大作业相关推荐

  1. 基于Vue+ElementUI+Springboot前后端分离网约车预约系统设计

    开发技术:Vue + ElementUI + Springboot  开发软件:Idea 为了用户搭乘车子更方便,我们设计了一个网约车预约系统,项目一共有3个身份包括乘客,司机和管理员.乘客在前台注册 ...

  2. 悟空CRM(基于jfinal+vue+ElementUI的前后端分离的开源CRM系统)

    https://www.jfinal.com/share/1591 官网:http://www.5kcrm.com 官网:http://www.72crm.com 论坛:http://bbs.72cr ...

  3. 悟空CRM java版(基于jfinal+vue+ElementUI的前后端分离CRM系统)

    CRM9.0(JAVA版) 软件长期为企业提供企业管理软件(CRM/HRM/OA/ERP等)的研发.实施.营销.咨询.培训.服务于一体的信息化服务.悟空软件以高科技为起点,以技术为核心.以完善的售后服 ...

  4. Vue+ElementUI+.netcore前后端分离框架开发项目实战

    点击上方 "程序员小乐"关注公众号, 星标或置顶一起成长 每天凌晨00点00分, 第一时间与你相约 每日英文 Smile and stop complaining about th ...

  5. Django+vue+ElementUi 实现前后端分离项目

    目录 一. 环境搭建 二. Django 2.1 创建项目 2.2 数据库配置 2.3 创建模型并且设计数据库表 Django app :myApp 2.4 创建新增和查询的接口 三.Vue 3.1. ...

  6. 悟空CRM(基于TP5.0+vue+ElementUI的前后端分离CRM系统)

    悟空CRM(9.0版本)(PHP版本) 悟空软件长期为企业提供企业管理软件(CRM/HRM/OA/ERP等)的研发.实施.营销.咨询.培训.服务于一体的信息化服务.悟空软件以高科技为起点,以技术为核心 ...

  7. 悟空CRM (基于jfinal+vue+ElementUI的前后端分离CRM系统)

    悟空CRM9.0(JAVA版) 悟空软件长期为企业提供企业管理软件(CRM/HRM/OA/ERP等)的研发.实施.营销.咨询.培训.服务于一体的信息化服务.悟空软件以高科技为起点,以技术为核心.以完善 ...

  8. 悟空CRM JAVA版 (基于jfinal+vue+ElementUI的前后端分离CRM系统)

    悟空CRM9.0(JAVA版) 悟空软件长期为企业提供企业管理软件(CRM/HRM/OA/ERP等)的研发.实施.营销.咨询.培训.服务于一体的信息化服务.悟空软件以高科技为起点,以技术为核心.以完善 ...

  9. springboot+vue+elementui实现前后端分离的网上商城购物系统

    文末获取源码 开发语言:Java 框架:springboot JDK版本:JDK1.8 服务器:tomcat7 数据库:mysql 5.7/8.0 数据库工具:Navicat11 开发软件:eclip ...

最新文章

  1. Func vs. Action vs. Predicate [复制]
  2. 用鸿蒙跑了个 hello world
  3. 回调函数自定义传参_10分钟教你手写8个常用的自定义hooks
  4. linux终端安装cuda,Ubuntu下安装CUDA10.0以及问题
  5. datastage配置oracle,IBM Datastage8.5配置问题
  6. 牛客网 2018年全国多校算法寒假训练营练习比赛(第五场) H.Tree Recovery-完全版线段树(区间更新、区间求和)...
  7. malloc动态内存分配
  8. 怎样成为一个合格的程序员
  9. 用javascript实现以下功能!_JavaScript实现汉字转拼音功能
  10. Qt XDF阅读管理
  11. 啦啦外卖独立版41.7全开源小程序app全套源码带vue源码
  12. 模拟银行叫号系统(c代码)
  13. 熵权法求权重python代码
  14. 江苏省计算机二级c语言备考,江苏省计算机二级C语言考试备考指南
  15. android 混淆报错记录
  16. android图片和文字设计,如何为图片加上精美文字?
  17. Python文本文件的输入输出
  18. word格式化字符技巧
  19. 4.页面评价算法 Heritrix应用与扩展
  20. 海量数据处理问题汇总

热门文章

  1. Ubuntu_部分键盘F1~F12强制为功能键
  2. PIC16F877A与Proteus仿真-ADC
  3. 华为云计算HCIE之oceanstor仿真器的使用操作
  4. [Unity3D]Unity官方认证考试模拟题
  5. 服务器监控-prometheus使用(4):收集器搭建篇
  6. 蓝松视频编辑SDK使用说明
  7. “贫困县电商特产”大数据地图:一定有你青睐的特色产品
  8. R+中文︱中文文本处理杂货柜——chinese.misc
  9. pinpoint2.3.3安装和部署
  10. 阿里云 oss多文件上传