微服务的编程测评系统14-C端题目列表功能-个人中心
提示:文章写完后,目录可以自动生成,如何生成可参考右边的帮助文档
文章目录
- 前言
- 1. C端题目列表功能
- 1.1 功能开发1
- 1.2 功能开发2
- 1.3 前端开发
- 2. 个人中心
- 2.1 用户详情
- 2.2 用户编辑-基本信息
- 总结
前言
1. C端题目列表功能
1.1 功能开发1
我们就是把redis换成了es
先查es,查到了就直接返回,没有查到的话,就查mysql,然后同步数据给es,mysql没有查到的话,直接返回null
@RestController
@RequestMapping("/question")
@Tag(name = "题目接口")
@Slf4j
public class QuestionController extends BaseController {@Autowiredprivate IQuestionService questionService;@GetMapping("/list")public TableDataInfo list(QuestionQueryDTO questionQueryDTO){return getTableDataInfo(questionService.list(questionQueryDTO));}}
@Data
public class QuestionQueryDTO extends PageQueryDTO {private String keyword;private Integer difficultly;
}
怎么操作es呢—》需要一个类
专门放在question里面es包下
@Getter
@Setter
@Document(indexName = "idx_question")
public class QuestionES {@Id@Field(type = FieldType.Long)private Long questionId;@Field(type = FieldType.Text, analyzer = "ik_max_word", searchAnalyzer = "ik_max_word")private String title;@Field(type = FieldType.Byte)private Integer difficulty;@Field(type = FieldType.Long)private Long timeLimit;@Field(type = FieldType.Long)private Long spaceLimit;@Field(type = FieldType.Text, analyzer = "ik_max_word", searchAnalyzer = "ik_max_word")private String content;@Field(type = FieldType.Text)private String questionCase;@Field(type = FieldType.Text)private String mainFuc;@Field(type = FieldType.Text)private String defaultCode;@Field(type = FieldType.Date, format = DateFormat.date_hour_minute_second)private LocalDateTime createTime;
}
这个就相当于mysql的实体类,这个就是es的实体类
@Document(indexName = “idx_question”)就相当于指定表名
@Id说明这个字段是唯一标识
searchAnalyzer = "ik_max_word"是ik配置
然后我们创建一个类似操作mysql的mapper
@Repository
public interface QuestionRepository extends ElasticsearchRepository<QuestionES, Long> {Page<QuestionES> findQuestionByDifficulty(Integer difficulty, Pageable pageable);//select * from tb_question where (title like '%aaa%' or content like '%bbb%') and difficulty = 1@Query("{\"bool\": {\"should\": [{ \"match\": { \"title\": \"?0\" } }, { \"match\": { \"content\": \"?1\" } }], \"minimum_should_match\": 1, \"must\": [{\"term\": {\"difficulty\": \"?2\"}}]}}")Page<QuestionES> findByTitleOrContentAndDifficulty(String keywordTitle, String keywordContent,Integer difficulty, Pageable pageable);@Query("{\"bool\": {\"should\": [{ \"match\": { \"title\": \"?0\" } }, { \"match\": { \"content\": \"?1\" } }], \"minimum_should_match\": 1}}")Page<QuestionES> findByTitleOrContent(String keywordTitle, String keywordContent, Pageable pageable);}
<QuestionES, Long>中Long是主键的类型
这个就类似于mapper操作数据库,这个接口是用来操作es的
注意我们引入的elasticsearch是spring-data的,这个非常方便,比如它适用于es,mysql,意思就是我们修改引入依赖
将 <artifactId>spring-boot-starter-data-elasticsearch</artifactId>
修改为 <artifactId>spring-boot-starter-data-mysql</artifactId>
代码都可能不会进行改变
所以说代码通用性很高
然后就是
我们定义的这个方法 Page<QuestionES> findQuestionByDifficulty(Integer difficulty, Pageable pageable);
并没有去实现,但是spring-data就可以根据我们写的这个方法名,自动进行代码查询的实现
然后复杂的,比如findByTitleOrContentAndDifficulty,这个是模糊查询就需要我们自己写了
@Query("{\"bool\": {\"should\": [{ \"match\": { \"title\": \"?0\" } }, { \"match\": { \"content\": \"?1\" } }], \"minimum_should_match\": 1, \"must\": [{\"term\": {\"difficulty\": \"?2\"}}]}}")
这个就是查询的语句
和这个一样的
dificultly是必须加的,所以是must,term意思就是精确匹配,必须等于2
should指的是,这个条件可以不满足,只需要满足一个,或者0个或者多个
minimum_should_match指的是should中的条件title和content至少要成立一个
match是模糊匹配
form和size就是分页,query代码里面的0,1,2就是参数的位置,分别是keywordTitle,keywordContent,difficulty
findByTitleOrContent就是只根据关键字搜索
如果都没有条件的话,我们可以使用自带的方法,findAll
@NoRepositoryBean
public interface PagingAndSortingRepository<T, ID> extends Repository<T, ID> {Iterable<T> findAll(Sort sort);Page<T> findAll(Pageable pageable);
}
一直往上一级就可以找到方法,也是有分页参数的
所以这个就是相当于具有redis查询速度的数据库
1.2 功能开发2
现在开始实际写代码了
@GetMapping("/semiLogin/list")public TableDataInfo list(QuestionQueryDTO questionQueryDTO){return questionService.list(questionQueryDTO);}
@Data
public class QuestionVO {@JsonSerialize(using = ToStringSerializer.class)private Long questionId;private String title;private Integer difficulty;
}
@Data
public class QuestionQueryDTO extends PageQueryDTO {private String keyword;private Integer difficultly;
}
@Overridepublic TableDataInfo list(QuestionQueryDTO questionQueryDTO) {long count = questionRepository.count();//这个就是相当于查询一个表的数量if(count<=0){refreshQuestionEs();}Sort sort = Sort.by(Sort.Direction.DESC, "createTime");//因为es是从第0页开始的,前端是从第一页开始的,所以要减一,这些函数都可以去查看源码得到Pageable pageable = PageRequest.of(questionQueryDTO.getPageNum()-1,questionQueryDTO.getPageSize(),sort);String keyword = questionQueryDTO.getKeyword();Integer difficultly = questionQueryDTO.getDifficultly();Page<QuestionES> questionESPage;if(difficultly == null && StrUtil.isEmpty(keyword)){questionESPage = questionRepository.findAll(pageable);}else if(difficultly != null && StrUtil.isEmpty(keyword)){questionESPage = questionRepository.findQuestionByDifficulty(difficultly,pageable);}else if(difficultly == null && StrUtil.isNotEmpty((keyword))){questionESPage = questionRepository.findByTitleOrContent(keyword,keyword,pageable);}else{questionESPage = questionRepository.findByTitleOrContentAndDifficulty(keyword,keyword,difficultly,pageable);}long total = questionESPage.getTotalElements();//所有页数的元素之和if(total<=0){return TableDataInfo.empty();}List<QuestionES> questionESList = questionESPage.getContent();List<QuestionVO> questionVOList = BeanUtil.copyToList(questionESList, QuestionVO.class);return TableDataInfo.success(questionVOList,total);}private void refreshQuestionEs() {List<Question> questionList = questionMapper.selectList(new LambdaQueryWrapper<Question>());if(CollectionUtil.isEmpty(questionList)){return;}//同步数据到esList<QuestionES> questionESList = BeanUtil.copyToList(questionList, QuestionES.class);questionRepository.saveAll(questionESList);//批量插入}
这样就成功了
如果没有索引,插入数据索引的时候是会自动创建索引的
然后就是服务启动的时候是会自动创建索引的,如果没有这个索引的话,就是根据我们创建的那个questionEs来对应创建的,就是相当于创建表了
GET /idx_question/_mapping
这个就可以查看索引的结构了(表的结构)
GET /idx_question/_search
这个就是查看所有数据,但是默认只能看十条
GET /idx_question/_search
{"size":100, // 指定返回20条数据"query": {"match_all": {}}
}
这个可以限制看的条数
然后就是还有题目的新增,删除,修改,对应es都要修改的
add
int insert = questionMapper.insert(question);QuestionES questionES = new QuestionES();BeanUtil.copyProperties(question,questionES);questionRepository.save(questionES);return insert;
因为只有数据库insert之后才有id,所以这样搞
edit
QuestionES questionES = new QuestionES();BeanUtil.copyProperties(question,questionES);questionRepository.save(questionES);
save方法就是新增,如果id重复了,就是覆盖
delete
questionRepository.deleteById(questionId);
还有要注意的是,这里的关键词搜索最好是词语形式的,这样子可能好搜一点,并不是每个单字都是可以搜到的
1.3 前端开发
export function getQuestionListService(params) {return service({url: "/question/semiLogin/list",method: "get",params,});
}
然后是question.vue
<template><div class="index-question"><div class="index-question-table-box"><page-container class="left"><el-form inline="true" ref="formModel" :model="form"><el-form-item style="max-width:864px"><el-input v-model="params.keyword" placeholder="请您输入题目标题或内容"style="width: 100%; margin: 20px 0 20px 10px;" /></el-form-item class="question-header"><el-form-item style="width: 180px;"><selector v-model="params.difficulty"></selector></el-form-item><el-form-item><el-button type="primary" plain @click="onSearch">搜索</el-button><el-button type="info" plain @click="onReset">重置</el-button></el-form-item></el-form><!-- 表格 --><el-table :data="questionList" height="580px" style="width: 100%"><el-table-column align="center" width="100px" label="序号"><template #default="{ $index }">{{ (params.pageNum - 1) * params.pageSize + $index + 1 }}</template></el-table-column><el-table-column align="left" prop="title" :show-overflow-tooltip="true" label="题目标题" /><el-table-column align="center" width="180px" prop="difficulty" label="题目难度"><template #default="{ row }"><div style="color:#3EC8FF;" v-if="row.difficulty === 1">简单</div><div style="color:#FE7909;" v-if="row.difficulty === 2">中等</div><div style="color:#FD4C40;" v-if="row.difficulty === 3">困难</div></template></el-table-column><el-table-column label="操作" align="center" width="180px"><template #default="{ row }"><el-button type="text" plain v-if="isLogin" @click="goQuestTest(row.questionId)">开始答题</el-button><span style="color:#9E9E9E;" v-else>请登录后参与答题</span></template></el-table-column></el-table><!-- 分页区域 --><el-pagination background layout="total, sizes, prev, pager, next, jumper" :total="total"v-model:current-page="params.pageNum" v-model:page-size="params.pageSize" :page-sizes="[5, 10, 15, 20]"@size-change="handleSizeChange" @current-change="handleCurrentChange"style="margin:20px 100px 20px 0; justify-content: flex-end" /></page-container><div class="right"><div class="top-box"><el-calendar v-model="today"> </el-calendar></div><div class="bot-box"><div class="title"><img width="96px" height="24px" src="@/assets/rebang.png" alt=""></div><div class="hot-list"><div class="list-item" v-for="(item, index) in hotQuestionList" :key="'hot_' + index"><img class="index-box" v-if="index == 0" src="@/assets/images/icon_1.png" alt=""><img class="index-box" v-if="index == 1" src="@/assets/images/icon_2.png" alt=""><img class="index-box" v-if="index == 2" src="@/assets/images/icon_3.png" alt=""><span class="index-box" v-if="index > 2">{{ index + 1 }}</span><span class="txt" :title="item.title">{{ item.title }}</span></div></div></div></div></div></div>
</template><script setup>
import { reactive, ref } from "vue"
import Selector from "@/components/QuestionSelector.vue"
import { getQuestionListService } from '@/apis/question'
import { getToken } from "@/utils/cookie"
import router from "@/router"
import { getUserInfoService } from "@/apis/user"const params = reactive({pageNum: 1,pageSize: 10,difficulty: '',keyword: ''
})
let today = ref(new Date()) //日历默认今天
const questionList = ref([]) //题目列表
const total = ref(0)
const isLogin = ref(false)async function checkLogin() {if (getToken()) {await getUserInfoService()isLogin.value = true}
}
checkLogin()async function getQuestionList() {const result = await getQuestionListService(params)questionList.value = result.rowstotal.value = result.total
}
getQuestionList()const hotQuestionList = ref([])// 搜索/重置
function onSearch() {params.pageNum = 1getQuestionList()
}function onReset() {params.pageNum = 1params.pageSize = 9params.difficulty = ''params.keyword = ''getQuestionList()
}// 分页
function handleSizeChange(newSize) {console.log(params.pageSize)params.pageNum = 1getQuestionList()
}function handleCurrentChange(newPage) {getQuestionList()
}function goQuestTest(questionId) {router.push(`/c-oj/anwser?questionId=${questionId}`)
}</script><style lang="scss" scoped>
.index-question-table-box {display: flex;max-width: 1520px;width: 100%;justify-content: space-between;:deep(.el-pagination) {.el-select .el-select__wrapper,.el-input .el-input__wrapper {height: 24px;}}.right {width: 340px;:deep(.top-box) {width: 100%;height: 345px;padding-bottom: 20px;margin-bottom: 20px;border-radius: 16px;background: linear-gradient(164deg, #E2F7FF 0%, #FFFFFF 50%);.el-calendar,.el-calendar__body {background: transparent;}.el-calendar {position: relative;.el-calendar__title {font-weight: bold;}.el-calendar__header {border: none;padding-bottom: 0;}.el-calendar__button-group {text-align: center;background: transparent;.el-button-group>.el-button {color: #1f2122;border: none;background: rgba(255, 255, 255, .1);}}.el-calendar-table {thead th {color: #32C5FF;font-size: 12px;font-weight: bold;}.el-calendar-day {height: 40px;text-align: center;}td {border: none;}}}}.bot-box {width: 340px;border-radius: 16px;padding-top: 0;box-sizing: border-box;background: #fff;position: relative;overflow: hidden;.title {font-weight: bold;color: #32C5FF;position: relative;width: 100%;height: 60px;padding: 20px;padding-bottom: 0;background: linear-gradient(169deg, #E2F7FF 0%, #FFFFFF 100%);span {position: absolute;display: flex;right: 20px;font-family: PingFangSC, PingFang SC;font-weight: 400;font-size: 14px;color: #32C5FF;bottom: 20px;cursor: pointer;}}.hot-list {width: calc(100% - 40px);margin: 0 auto;padding-top: 20px;padding-bottom: 20px;.list-item {margin-bottom: 18px;cursor: pointer;display: flex;align-items: center;.index-box {display: inline-block;text-align: center;width: 20px;font-family: Tensentype-RuiHeiJ, Tensentype-RuiHeiJ;font-weight: normal;font-size: 18px;color: #999999;font-weight: bold;}.txt {max-width: calc(100% - 34px);overflow: hidden;text-overflow: ellipsis;white-space: nowrap;margin-left: 10px;font-family: PingFangSC, PingFang SC;font-weight: 400;font-size: 16px;color: #222222;line-height: 22px;text-align: left;font-style: normal;}&:hover {.txt {color: #32C5FF;}}}}}}:deep(.left) {width: calc(100% - 360px);border: none;border-radius: 16px;overflow: hidden;background: linear-gradient(177deg, #f3fcff 0%, #FFFFFF 10%);.el-table {th.el-table__cell {background-color: rgba(50, 197, 255, 0.08) !important;font-size: 13px;}td {height: 54px;}}.el-card__header {font-family: PingFangSC, PingFang SC;font-weight: 600;font-size: 20px;color: #222222;line-height: 28px;text-align: left;font-style: normal;border: none;padding-bottom: 0;}}
}.el-input__wrapper {background-color: #F8F8F8;
}.index-question {background-color: rgba(247, 247, 247, 1);display: flex;justify-content: center;margin-top: 20px;.no-border {border: none;}
}.el-table {th {word-break: break-word;background-color: #033e7c !important;color: #ffff;height: 40px;font-weight: 700;font-size: 14px;}
}
</style><!-- npm install ace-builds@1.4.13 -->
这样就成功了
2. 个人中心
2.1 用户详情
头像上传我们用的是el-upload
但是怎么存储头像呢–》用的是oss
@Getter
@Setter
public class UserVO {@JsonSerialize(using = ToStringSerializer.class)private Long userId;private String nickName;private String headImage;private Integer sex;private String phone;private String code;private String email;private String wechat;private String schoolName;private String majorName;private String introduce;private Integer status;
}
@GetMapping("/detail")public R<UserVO> detail() {return R.ok(userService.detail());}
@Overridepublic UserVO detail() {Long userId = ThreadLocalUtil.get(Constants.USER_ID, Long.class);if (userId == null) {throw new ServiceException(ResultCode.FAILED_USER_NOT_EXISTS);}UserVO userVO = userCacheManager.getUserById(userId);if (userVO == null) {throw new ServiceException(ResultCode.FAILED_USER_NOT_EXISTS);}return userVO;}
然后是userCacheManager
public final static String USER_DETAIL_USERID = "user:detail:"; //用户详情信息public final static long USER_DETAIL_EXP = 10;//用户详细信息存储时间
@Component
public class UserCacheManager {@Autowiredprivate RedisService redisService;@Autowiredprivate UserMapper userMapper;public UserVO getUserById(Long userId) {String userKey = getUserKey(userId);UserVO userVO = redisService.getCacheObject(userKey, UserVO.class);if (userVO != null) {//将缓存延长10minredisService.expire(userKey, CacheConstants.USER_DETAIL_EXP, TimeUnit.MINUTES);return userVO;}User user = userMapper.selectOne(new LambdaQueryWrapper<User>().select(User::getUserId,User::getNickName,User::getHeadImage,User::getSex,User::getEmail,User::getPhone,User::getWechat,User::getIntroduce,User::getSchoolName,User::getMajorName,User::getStatus).eq(User::getUserId, userId));if (user == null) {return null;}refreshUser(user);userVO = new UserVO();BeanUtil.copyProperties(user, userVO);return userVO;}public void refreshUser(User user) {//刷新用户缓存String userKey = getUserKey(user.getUserId());redisService.setCacheObject(userKey, user);//设置用户缓存有效期为10分钟redisService.expire(userKey, CacheConstants.USER_DETAIL_EXP, TimeUnit.MINUTES);}//u:d:用户idprivate String getUserKey(Long userId) {return CacheConstants.USER_DETAIL_USERID + userId;}
}
然后就是前端的代码了
export function getUserDetailService(userId) {return service({url: "/user/detail",method: "get",params: { userId },});
}
<template><div class="user-info-page"><div class="oj-user-info oj-user-info-page"><div class="oj-user-info-body"><el-form :model="userDetailForm" label-position="top" status-icon label-width="100px" class="demo-ruleForm"><div class="oj-user-info-subtitle-box"><div class="oj-user-info-subtitle"><span class="oj-user-info-dot">基本信息</span><div class="opt-box"><el-button class="user-info-edit" v-if="isDisabled()" type="text" @click="changeState('updating')">编辑</el-button><el-button class="user-info-edit" v-if="!isDisabled()" type="text" @click="changeState('normal')">取消</el-button><el-button class="user-info-back" type="text" @click="goBack()">返回</el-button></div></div></div><div class="ku-user-section"><el-form-item prop="nickName"><div class="photo-image-box"><div class="photo-image"><div class="left"><img width="80px" v-if="!userDetailForm.headImage" height="80px" src="@/assets/user/head_image.png"><img width="80px" v-else height="80px" :src="userDetailForm.headImage"></div><div class="right" v-if="!userDetailForm.headImage"><div class="bold">上传你的个人头像</div><div class="normal">上传你的个性头像展示你的个人风格</div></div></div><div v-if="!isDisabled()"><el-upload :action="uploadUrl" :headers="headers":on-error="handleUploadError" :on-success="handleUploadSuccess" :show-file-list="false"><el-button type="info">{{ userDetailForm.headImage ? '更换头像' : '上传头像' }}</el-button><template #tip> </template></el-upload></div></div></el-form-item><el-form-item label="昵称" prop="nickName"><el-input v-if="!isDisabled()" type="text" v-model="userDetailForm.nickName" autocomplete="off"placeholder="请填写您的昵称" maxlength="8"></el-input><span :class="userDetailForm.nickName ? 'right-info' : 'right-info gray'" v-else>{{ userDetailForm.nickName || "请填写您的昵称" }}</span></el-form-item><el-form-item label="性别" prop="sex"><el-radio-group v-model="userDetailForm.sex" v-if="!isDisabled()"><el-radio :label="1">男</el-radio><el-radio :label="2">女</el-radio></el-radio-group><span :class="userDetailForm.sex ? 'right-info' : 'right-info gray'" v-else>{{ userDetailForm.sex == null ? "请选择您的性别" : +userDetailForm.sex == 1 ? "男" : "女" }}</span></el-form-item><el-form-item label="学校" prop="schoolName"><el-input v-if="!isDisabled()" type="text" v-model="userDetailForm.schoolName" autocomplete="off"placeholder="请填写您的学校名称"></el-input><span :class="userDetailForm.schoolName ? 'right-info' : 'right-info gray'" v-else>{{ userDetailForm.schoolName || "请填写您的学校名称" }}</span></el-form-item><el-form-item label="专业" prop="majorName"><el-input v-if="!isDisabled()" type="text" v-model="userDetailForm.majorName" autocomplete="off"placeholder="请填写您的专业名称"></el-input><span :class="userDetailForm.majorName ? 'right-info' : 'right-info gray'" v-else>{{ userDetailForm.majorName || "请填写您的专业名称" }}</span></el-form-item><el-form-item label="手机" prop="phone"><el-input v-if="!isDisabled()" type="text" v-model="userDetailForm.phone" autocomplete="off"placeholder="请填写常用手机号"></el-input><span :class="userDetailForm.phone ? 'right-info' : 'right-info gray'" v-else>{{ userDetailForm.phone || "请填写常用手机号" }}</span></el-form-item><el-form-item label="常用邮箱" prop="email"><el-input v-if="!isDisabled()" type="text" v-model="userDetailForm.email" autocomplete="off"placeholder="请填写常用邮箱"></el-input><span :class="userDetailForm.email ? 'right-info' : 'right-info gray'" v-else>{{ userDetailForm.email || "请填写常用邮箱" }}</span></el-form-item><el-form-item label="微信号码" prop="wechat"><el-input v-if="!isDisabled()" type="text" v-model="userDetailForm.wechat" autocomplete="off"placeholder="请填写微信号码"></el-input><span :class="userDetailForm.wechat ? 'right-info' : 'right-info gray'" v-else>{{ userDetailForm.wechat || "请填写微信号" }}</span></el-form-item><el-form-item label="个人介绍" prop="introduce"><el-input v-if="!isDisabled()" :rows="5" type="textarea" v-model="userDetailForm.introduce"autocomplete="off" placeholder="请填写个人介绍" maxlength="80" show-word-limit></el-input><span :class="userDetailForm.introduce ? 'right-info intro' : 'right-info intro gray'" v-else>{{ userDetailForm.introduce || "请用一段话介绍自己,该简介将会在您的个人主页展示" }}</span></el-form-item></div><div class="ku-login-footer" v-if="!isDisabled()"><el-button class="ku-login-footer-btn" type="info" plain @click="changeState('normal')">取消</el-button><el-button class="ku-login-footer-btn blue" type="primary" plain @click="editUser()">保存</el-button></div></el-form></div></div></div>
</template><script setup>
import { getUserDetailService} from '@/apis/user'
import { getToken } from '@/utils/cookie'import router from '@/router'
import { ElMessage } from 'element-plus';
import { reactive, ref } from "vue"const userDetailForm = reactive({})
const currentState = ref('normal')async function getUserDetail() {const userRef = await getUserDetailService()currentState.value = "normal"Object.assign(userDetailForm, userRef.data)
}
getUserDetail()function goBack() {router.go(-1)
}function changeState(state) {currentState.value = state
}function isDisabled() {return currentState.value === "normal"
}// async function editUser() {
// await editUserService(userDetailForm)
// getUserDetail()
// }</script><style lang="scss" scoped>
.back-btn-box {background: #fff;width: 100%;height: 48px;margin-bottom: 16px;.back-btn {height: 100%;margin: 0 auto;display: flex;align-items: center;font-size: 14px;font-weight: 400;color: #999999;cursor: pointer;line-height: unset;i {margin-right: 6px;font-size: 16px;margin-top: 2.5px;}}
}.user-info-page {background-color: rgba(247, 247, 247, 1);position: relative;overflow: hidden;:deep(.el-form) {.el-button--info {background: #f2f3f5;border: 1px solid #f2f3f5;color: #222;&:hover {background: #32C5FF;border: 1px solid #32C5FF;color: #fff;}}.el-form-item {max-width: 1120px;width: 100%;.el-form-item__label {font-family: PingFangSC, PingFang SC;font-weight: 600;font-size: 18px;color: #222222;margin-bottom: 16px;}.photo-image-box {display: flex;align-items: center;justify-content: space-between;width: 100%;padding-bottom: 20px;border-bottom: 1px solid #ebebeb;.photo-image {display: flex;flex: 1;margin-right: 20px;img {border-radius: 50%;}.right {flex: 1;margin-left: 20px;display: flex;flex-direction: column;justify-content: center;.bold {height: 25px;font-family: PingFangSC, PingFang SC;font-weight: 600;font-size: 18px;color: #222222;line-height: 25px;margin-bottom: 10px;}.normal {height: 22px;font-family: PingFangSC, PingFang SC;font-weight: 400;font-size: 16px;color: #999999;line-height: 22px;}}}}.el-input,.el-select,.el-radio-group {width: 100%;height: 44px;border-radius: 4px;outline-style: none;border: none;font-size: 16px;input {color: #222;}}.el-radio-group {border-bottom: 1px solid #ebebeb;}.el-input__wrapper {border: none;box-shadow: none;border-radius: 0;padding-left: 0;color: #222;border-bottom: 1px solid #ebebeb;&:hover {border-bottom: 1px solid #32c5ff;}}}}.oj-user-info {margin: 0 auto;max-width: 1520px;width: 100%;&.oj-user-info-page {background: #f7f7f7;margin-bottom: 30px;.user-info-back {cursor: pointer;color: #999999;font-size: 15px;}.user-info-edit {cursor: pointer;font-size: 15px;margin-left: auto;}.ku-user-section {background: #fff;}.oj-user-info-body {padding: 0;.oj-user-info-subtitle-box {padding: 30px 20px;padding-bottom: 0;.oj-user-info-subtitle {display: flex;align-items: center;height: 33px;font-family: PingFangSC, PingFang SC;font-weight: 600;font-size: 24px;color: #222222;line-height: 33px;}}}.ku-user-section {padding: 30px 24px;padding-bottom: 0;}.ku-user-real-info {margin-top: 0;margin-bottom: 0;padding-left: 24px;box-sizing: border-box;height: 60px;line-height: 60px;border-bottom: 1px solid #f2f2f2;}.el-form-item__content {margin-left: 110px;}.ku-login-footer {text-align: left;padding-left: 24px;padding-bottom: 24px;}}.oj-user-info-title {background-color: rgba(255, 255, 255, 1);border-radius: 10px;width: 100%;/* 设置宽度为100%以确保水平居中 */height: 60px;font-size: 25px;text-indent: 20px;display: flex;align-items: center;margin: 20px 0 20px 0;}.oj-user-info-body {background-color: #fff;padding: 20px 20px 40px;border-radius: 8px;}.oj-user-info-subtitle {font-size: 16px;font-weight: 500;color: #222222;line-height: 25px;display: flex;justify-content: space-between;align-items: center;}.ku-login-footer {text-align: center;margin-top: 30px;}.ku-login-footer-btn {width: 84px;height: 44px;border-radius: 3px;font-size: 15px;font-weight: 400;&.blue {background: #32C5FF !important;color: #fff;}}.ku-user-section {background-color: #f7f7f7;padding: 30px 38px;border-radius: 10px;}.oj-user-info-action {position: relative;height: 0;}.ku-user-validate-phone {display: inline-block;height: 40px;margin-left: 8px;color: #24c68b;font-weight: 700;cursor: pointer;user-select: none;}.ku-user-real-info {margin-top: 47px;}}
}
</style><style lang="scss">
.oj-user-info-page {.el-form-item__label {padding-right: 30px !important;color: #666;}.el-form {padding-bottom: 24px;}.right-info {display: inline-block;min-width: 90px;color: #222;font-family: PingFangSC, PingFang SC;font-weight: 400;font-size: 16px;width: 100%;border-bottom: 1px solid #f2f2f2;height: 44px;line-height: 44px;&.gray {color: #999;}}
}
</style>
2.2 用户编辑-基本信息
更新用户信息—.要改缓存(用户详情和用户登录信息的缓存)和数据库
先是后端
@Getter
@Setter
public class UserUpdateDTO {private String headImage;private String nickName;private Integer sex;private String schoolName;private String majorName;private String phone;private String email;private String wechat;private String introduce;
}
@PutMapping("/edit")public R<Void> edit(@RequestBody UserUpdateDTO userUpdateDTO) {return toR(userService.edit(userUpdateDTO));}
public static final String USER_KEY = "userKey";
@Overridepublic int edit(UserUpdateDTO userUpdateDTO) {Long userId = ThreadLocalUtil.get(Constants.USER_ID, Long.class);if (userId == null) {throw new ServiceException(ResultCode.FAILED_USER_NOT_EXISTS);}User user = userMapper.selectById(userId);if (user == null) {throw new ServiceException(ResultCode.FAILED_USER_NOT_EXISTS);}user.setNickName(userUpdateDTO.getNickName());user.setSex(userUpdateDTO.getSex());user.setSchoolName(userUpdateDTO.getSchoolName());user.setMajorName(userUpdateDTO.getMajorName());user.setPhone(userUpdateDTO.getPhone());user.setEmail(userUpdateDTO.getEmail());user.setWechat(userUpdateDTO.getWechat());user.setIntroduce(userUpdateDTO.getIntroduce());//更新用户缓存userCacheManager.refreshUser(user);tokenService.refreshLoginUser(user.getNickName(),user.getHeadImage(),ThreadLocalUtil.get(Constants.USER_KEY, String.class));return userMapper.updateById(user);}
在拦截器中存入userKey
String userKey = tokenService.getUserKey(token, secret);ThreadLocalUtil.set(Constants.USER_KEY,userKey);
public void refreshLoginUser(String nickName, String headImage, String userKey) {String tokenKey = getTokenKey(userKey);LoginUser loginUser = redisService.getCacheObject(tokenKey, LoginUser.class);loginUser.setHeadImage(headImage);loginUser.setNickName(nickName);redisService.setCacheObject(tokenKey,loginUser);}
这样就成功了,然后是前端的开发
export function editUserService(params = {}) {return service({url: "/user/edit",method: "put",data: params,});
}
async function editUser() {await editUserService(userDetailForm)getUserDetail()
}