登录、申报表信息填报、申报表详情页、审核状态的修改功能初步完成
一、需求
- 登录:用户的登录、信息拉取、退出。
- 申报表填报:基层负责人负责申报表信息的填写,初期未加入权限控制的话可以直接登录用户填报
- 申报表详情页:可以看到填报的申报表列表,点击可进入申报表详情页
- 审核状态的修改:在申报表详情页尾可以进行审核状态的改变
二、分析实现
2.1登录
登录大致逻辑见往期博客:基层教学组织评估系统1_SpringBoot、Shiro、前后端权限和登录初探
初期可以使用username + password
登录、后期使用userNumber
进一步完善
- 对应文档和实体
- 对应UserController
package henu.soft.xiaosi.controller;
import henu.soft.xiaosi.pojo.user.User;
import henu.soft.xiaosi.service.UserService;
import henu.soft.xiaosi.utils.JwtUtil;
import henu.soft.xiaosi.vo.ResultResponse;
import henu.soft.xiaosi.vo.VoUser;
import org.apache.shiro.SecurityUtils;
import org.apache.shiro.authz.annotation.RequiresAuthentication;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
@RestController
@RequestMapping("/user")
public class UserController {
@Autowired
UserService userService;
/***
* 1. 用户登录
* @param voUser
* @param
* @param httpServletResponse
* @return
*/
@PostMapping("/login")
public ResultResponse login(@RequestBody VoUser voUser, HttpServletResponse httpServletResponse){
String currentUsername = voUser.getUsername();
String currentUserPassword = voUser.getPassword();
System.out.println("debug=>"+ currentUsername + currentUserPassword);
// 用户不存在
User findUser = userService.findUserByUsername(currentUsername);
System.out.println(findUser);
if(findUser == null){
return ResultResponse.fail(401,"用户不存在!",null);
}
// 密码不正确
if(!findUser.getPassword().equals(currentUserPassword)){
return ResultResponse.fail(401,"用户密码错误!",null);
}
// 生成token,返回前端
String token = JwtUtil.createToken(currentUsername);
httpServletResponse.setHeader("Access-Control-Expose-Headers","token");
httpServletResponse.setHeader("token",token);
voUser.setToken(token);
return ResultResponse.success(200,"正在登录...!",voUser);
}
/***
* 2. 获取用户信息
* @param httpServletRequest
* @return
*/
@GetMapping("/info")
public ResultResponse userInfo(HttpServletRequest httpServletRequest){
String token = httpServletRequest.getHeader("token");
if(token == null) {
return ResultResponse.success(200,"请重新登录!",null);
}
String username = JwtUtil.getUserNumber(token);
User currentUser = userService.findUserByUsername(username);
return ResultResponse.success(200,"获取用户信息成功!",currentUser);
}
/**
* 退出功能
* @return
*/
@RequiresAuthentication
@PostMapping("/logout")
public ResultResponse logout(){
SecurityUtils.getSubject().logout();
return ResultResponse.success(200,"退出成功!",null);
}
}
- 对应UserService
package henu.soft.xiaosi.service;
import henu.soft.xiaosi.pojo.user.User;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.mongodb.core.MongoTemplate;
import org.springframework.data.mongodb.core.query.Criteria;
import org.springframework.data.mongodb.core.query.Query;
import org.springframework.stereotype.Service;
@Service
public class UserService {
@Autowired
MongoTemplate mongoTemplate;
/**
* 1. 根据学工号查询用户
*/
public User findUserByUserNumber(String currentUserNumber) {
Query query=new Query(Criteria.where("usernumber").is(currentUserNumber));
User currentUser = mongoTemplate.findOne(query, User.class);
return currentUser;
}
/**
* 2. 根据用户名查询用户
*/
public User findUserByUsername(String currentUsername){
Query query=new Query(Criteria.where("username").is(currentUsername));
User currentUser = mongoTemplate.findOne(query, User.class);
return currentUser;
}
}
2.1实现过程中遇到的问题及解决
-
前端封装axios的request配置的response拦截器和后端Response的成功状态码不一致导致登录页面不跳转
解决:将request.js中的请求成功的状态码改成200和后端返回成功状态吗一致
import axios from 'axios'
import {
MessageBox, Message } from 'element-ui'
import store from '@/store'
import {
getToken } from '@/utils/auth'
// create an axios instance
const service = axios.create({
baseURL: process.env.VUE_APP_BASE_API, // url = base url + request url
// withCredentials: true, // send cookies when cross-domain requests
timeout: 5000 // request timeout
})
// request interceptor
service.interceptors.request.use(
config => {
// do something before request is sent
if (store.getters.token) {
// let each request carry token
// ['X-Token'] is a custom headers key
// please modify it according to the actual situation
//config.headers['X-Token'] = getToken()
config.headers['token'] = getToken()
}
return config
},
error => {
// do something with request error
console.log(error) // for debug
return Promise.reject(error)
}
)
// response interceptor
service.interceptors.response.use(
/**
* If you want to get http information such as headers or status
* Please return response => response
*/
/**
* Determine the request status by custom code
* Here is just an example
* You can also judge the status by HTTP Status Code
*/
response => {
const res = response.data
//if the custom code is not 20000, it is judged as an error.
if (res.code !== 200) {
Message({
message: res.message || 'Error',
type: 'error',
duration: 5 * 1000
})
// 50008: Illegal token; 50012: Other clients logged in; 50014: Token expired;
if (res.code === 50008 || res.code === 50012 || res.code === 50014) {
// to re-login
MessageBox.confirm('You have been logged out, you can cancel to stay on this page, or log in again', 'Confirm logout', {
confirmButtonText: 'Re-Login',
cancelButtonText: 'Cancel',
type: 'warning'
}).then(() => {
store.dispatch('user/resetToken').then(() => {
location.reload()
})
})
}
return Promise.reject(new Error(res.message || 'Error'))
}else {
return res
}
return response
},
error => {
console.log('err' + error) // for debug
Message({
message: error.message,
type: 'error',
duration: 5 * 1000
})
return Promise.reject(error)
}
)
export default service
2.2 申报表填报
因为申报表的信息量偏大,原始的MySQL也被换成了MongoDB
为了提升用户体验,节省表单提交的时间,将整个申报表表单拆成了大约13个子组件,对应着14个数据库collection
使用declaration_form来存储另外13个分collection的id将整个申报表信息关联起来
后端具体源码见:https://github.com/GitHubSi/base_education_system/tree/main/src/main/java/henu/soft/xiaosi
2.2实现过程中遇到的问题及解决
-
前后端数据格式不匹配报错
报错1:
err:JSON parse error: Cannot construct instance of ``ava.util.ArrayList
(although at least one Creator exists): no String-argument constructor/factory method to deserialize from String value (''); nested exception is com.fasterxml.jackson.databind.exc.MismatchedInputException: Cannot construct instance of ``java.util.ArrayList
(although at least one Creator exists): no String-argument constructor/factory method to deserialize from String value ('') at [Source: (PushbackInputStream); line: 1, column: 324] (through reference chain: henu.soft.xiaosi.pojo.declarationform.form1_principal.Principal["teachingCoursesInRecentTwoAcademicYears"])
报错2:
err:JSON parse error: Cannot deserialize instance of `henu.soft.xiaosi.pojo.declarationform.form2_teachers.Teachers` out of START_ARRAY token; nested exception is com.fasterxml.jackson.databind.exc.MismatchedInputException: Cannot deserialize instance of `henu.soft.xiaosi.pojo.declarationform.form2_teachers.Teachers` out of START_ARRAY token at [Source: (PushbackInputStream); line: 1, column: 1]
报错的原因是,前后端为不同人开发,后端实体包括数据库数据类型为 对象数组、对象
而前段传递到后端的数据是字符串,导致报错
解决办法是,修改前、后端的数据格式类型使对应起来
2.3申报表详情页
申报表详情页 需要使用另外一个collection来存储所有填写申报表及审核状态的信息
当点击某个申报表的时候,会携带该申报表的formID来发起请求,先请求declaration_form表,获得各个分表ID,
通过父、子组件传值的方式传递给各个分表对应的子组件,
然后子组件带着xxxID中发起异步请求,获得各个分表的信息,渲染到前端页面展示
<template>
<!-- eslint-disable -->
<div class="app-container">
<el-card>
<div slot="header" class="card-header">
<span>申报表详情</span>
</div>
<principal header="负责人概况" :principalID="this.form.principalID" />
<member header="成员概况" :membersID="this.form.membersID" />
<rules
header="规章制度"
:ruleAndRegulationID="this.form.ruleAndRegulationID"
/>
<teambuilding
header="队伍建设"
:teamBuildingID="this.form.teamBuildingID"
/>
<organization
header="教学组织"
:teachingOrganizationID="this.form.teachingOrganizationID"
/>
<coursematerial
header="课程教材概述"
:courseMaterialID="this.form.courseMaterialID"
/>
<teachingresearch
header="教学研究"
:teachingResearchID="this.form.teachingResearchID"
/>
<specialtyconstruction
header="专业建设"
:specialtyConstructionID="this.form.specialtyConstructionID"
/>
<practicalteaching
header="实践教学"
:practicalTeachingID="this.form.practicalTeachingID"
/>
<conditionguarantee
header="条件保障"
:conditionGuaranteeID="this.form.conditionGuaranteeID"
/>
<talentcultivationability
header="人才培养能力"
:talentCultivationAbilityID="this.form.talentCultivationAbilityID"
/>
<futureconstructionplan
header="今后计划建设"
:futureConstructionPlanID="this.form.futureConstructionPlanID"
/>
<opinionfeedback
header="教务处意见反馈"
:opinionFeedbackID="this.form.opinionFeedbackID"
v-on:getStatus="getStatus($event)"
/>
</el-card>
</div>
</template>
<script>
// import { getForm } from '../../api/form'
import principal from "./components/principal/index";
import member from "./components/member/index";
import rules from "./components/rules-troops/index";
import organization from "./components/organization/index";
import teambuilding from "./components/team-building/index";
import coursematerial from "./components/course-material/index";
import teachingresearch from "./components/teaching-research/index";
import specialtyconstruction from "./components/specialty-construction/index";
import practicalteaching from "./components/practical-teaching/index";
import conditionguarantee from "./components/condition-guarantee/index";
import talentcultivationability from "./components/talent-cultivation-ability/index";
import futureconstructionplan from "./components/future-construction-plan/index";
import opinionfeedback from "./components/opinion-feedback/index";
import {
getForm } from "@/api/detail-page/index";
import {
updateInfo } from "@/api/detail-page/index";
export default {
components: {
principal,
member,
rules,
organization,
teambuilding,
teachingresearch,
practicalteaching,
conditionguarantee,
talentcultivationability,
futureconstructionplan,
opinionfeedback,
coursematerial,
specialtyconstruction,
},
data() {
return {
form: {
info: {
organization: "",
college: "",
principal: "",
professionalTitle: "",
status: "等待审核",
reviewer: "",
},
principalID: "",
membersID: "",
ruleAndRegulationID: "",
teamBuildingID: "",
teachingOrganizationID: "",
courseMaterialID: "",
teachingResearchID: "",
specialtyConstructionID: "",
practicalTeachingID: "",
conditionGuaranteeID: "",
talentCultivationAbilityID: "",
futureConstructionPlanID: "",
opinionFeedbackID: "",
},
formID: "",
};
},
created() {
this.getFormID();
this.getInfo();
},
methods: {
// 1. 获取当前申报表的formID
getFormID() {
this.formID = this.$route.query.formID;
console.log(this.formID);
},
// 2. 数据各表ID渲染
getInfo() {
getForm(this.formID)
.then((result) => {
console.log(result.data);
const data = result.data;
for (const item in data) {
if (typeof data[item] === "function") {
continue;
}
this.form[item] = data[item];
}
})
.catch((err) => {
console.log(err);
});
},
// 3. 更新教务处审核状态和信息,由子组件调用
updateForm() {
updateInfo(this.formID, this.form)
.then(() => {
})
.catch((err) => {
console.log(err);
});
},
getStatus(val) {
console.log(val);
this.form.info.status = val;
},
},
};
</script>
<style lang="scss" scoped>
.el-card {
margin-bottom: 10px;
}
.card-header span {
padding: 3px 0;
border-bottom: 3px solid #409eff;
}
.form-header {
margin-bottom: 20px;
margin-top: 10px;
span {
padding: 3px 0;
border-bottom: 3px solid #409eff;
}
}
.cell-content span {
padding: 1px 10px;
border-bottom: 1px solid #000000;
}
.section-content {
margin-top: -20px;
p {
min-height: 110px;
padding: 10px;
border-radius: 5px;
border: 1px solid rgba(0, 0, 0, 0.4);
}
}
</style>
2.3实现过程中遇到的问题及解决
- vue组件拆分_父组件传递值给子组件undefined,需要watch监听:Vue解决报错4_父组件传递给子组件值,子组件使用插值表达式可以渲染,放在created函数中提示undefined