" 常在河边走,哪能不湿鞋。" ——若发现文章内容有误,敬请指正,望不吝赐教,感谢!
以往记录
Web项目实战 | 购物系统v2.0 | 开发记录(一)需求分析 | 技术选型 | 系统设计 | 数据表设计 | SpringBoot、SSM、Thymeleaf、Bootstrap…
Web项目实战 | 购物系统v2.0 | 开发记录(二)搭建SpringBoot+SSM框架环境 | 配置Druid+MyBatis | 基于Bootstrap实现登陆页面| 图片验证码接口
Web项目实战 | 购物系统v2.0 | 开发记录(四)单个页面单个请求解决根据商品类型进行分页查询 | 使用省市区三级联动 | 使用JQuery 插件实现图片上传
Web项目实战 | 购物系统v2.0 | 开发记录(五)| 使用base64编码实现头像修改 | 用户个人信息修改 | JQuery动态提示
Web项目实战 | 购物系统v2.0 | 开发记录(六)商品详情页面 | 游客访问主页 | 运用Bootstrap4轻量级弹窗实现提示
Web项目实战 | 购物系统v2.0 | 开发记录(七)SpringBoot整合Shiro框架进行身份认证 | Shiro 加盐(MD5+Salt)验证登陆 | 数据表结构优化避免外键+设置中间表
一、使用JQuery设计工具函数处理URL
URL的作用非同小可,比如在百度搜索CSDN,观察地址栏可以发现有许多参数。
这些参数可以传输到SpringMVC的Controller层
,后期有许多的用途,比如查询一个用户的映射地址为user/query
,这时可以给它添加一个参数表示查询的用户信息,比如user/query?id=1
,这时则表示查询id为1的用户,笔者通过URL的处理,实现了后台管理系统中用户信息多个条件搜索的功能。
JS实现代码:urlUtil.js
// 添加 或者 修改 url中参数的值
function clearURIExceptPageNow(){
return document.location.href.split('&')[0]
}
function getUrlParam(thisURL, name) {
let array = new RegExp(name + '=([^&]*)').exec(thisURL);
return (array == null || array.length < 1)? '' : decodeURI(array[1]);
}
function updateUrlParam(thisURL, name, val) {
thisURL = decodeURI(thisURL)
// 如果 url中包含这个参数 则修改
if (thisURL.indexOf(name+'=') > 0) {
var v = getUrlParam(thisURL, name);
if (v != null) {
// 是否包含参数
thisURL = thisURL.replace(name + '=' + v, name + '=' + val);
}
else {
thisURL = thisURL.replace(name + '=', name + '=' + val);
}
} // 不包含这个参数 则添加
else {
if (thisURL.indexOf("?") > 0) {
thisURL = thisURL + "&" + name + "=" + val;
}
else {
thisURL = thisURL + "?" + name + "=" + val;
}
}
return thisURL;
}
function updateCurrentUrlParam(name, val) {
return updateUrlParam(document.location.href, name, val)
}
function getUrlParamToMap() {
const params = window.location.toString().split('\?')[1]
let map = new Map();
if(params == null || params === '') return ''
params.split('&').forEach(param =>{
let kv = param.split('=');
let k = kv[0]
let v = kv[1]
map.set(k,v)
})
return map
}
function getUrlParamToJSON() {
let obj= Object.create(null)
let map = getUrlParamToMap();
if(map == null || map === '') return;
for (let[k,v] of map) {
k = decodeURI(k)
v = decodeURI(v)
obj[k] = v;
}
return JSON.stringify(obj)
}
功能一:直接获取替换参数值后的URL
比如当前地址栏的URL为:
user/query?id=1&roleName=普通用户
此时将查询用户ID为1,身份是普通用户的所有用户,当要切换查询的用户身份时,可以通过这个函数直接获取对应的URL
updateCurrentUrlParam('roleName', '管理员')
那么URL就会变成以下内容(其他的参数不会受影响)
user/query?id=1&roleName=管理员
功能二:清空URL除第一个以外的参数
比如当前地址栏请求的URL为:
user/query?pageNow=1&username=尤你&roleName=管理员
由于笔者固定第一个参数表示页数(分页查询),所以这里第一个参数不能清除,在clearURIExceptPageNow() 函数里保留了第一个参数,执行后的结果:
user/query?pageNow=1
功能三:封装URL的参数为Map/JSON类型
之前的函数都是基于URL内容上的修改,本质上都是字符串类型,主要用于修改当前的地址栏信息。
接下来是对其类型的封装,这样有助于通过AJAX请求向后台传递数据,直接将URL的参数传递到后台进行处理。
getUrlParamToJSON() // 将当前地址栏的参数值封装成JSON字符串(可传递到后台)
getUrlParamToMap() // 将当前地址栏的参数封装成map类型的对象
二、JQuery AJAX 通过Map传输数据
有了之前的工具函数后,现在操作URL的参数变得特别灵活,以获取用户信息为例:
function loadData(){
$.ajax({
url: '/admin/user/query',
type: 'POST',
data: getUrlParamToJSON(),
dataType: 'JSON',
contentType: 'application/json;charset=UTF-8',
error: (err) =>{
console.error(err)
},
success: (data) =>{
console.error(data)
//...
}
})
}
Controller层:
package com.uni.controller.admin;
import com.github.pagehelper.PageHelper;
import com.github.pagehelper.PageInfo;
import com.uni.base.RestResponse;
import com.uni.pojo.User;
import com.uni.service.UserService;
import com.uni.utils.PageUtil;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
import java.io.UnsupportedEncodingException;
import java.net.URLDecoder;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
@RestController
@RequestMapping("/admin/user")
public class UserAdminController {
@Autowired
UserService userService;
@PostMapping("/query")
public String toAdminUserByPage(@RequestBody Map<String, String> paramMap) throws UnsupportedEncodingException {
Map<String, String> map = new HashMap<>();
for (String param : paramMap.keySet()) {
paramMap.put(URLDecoder.decode(param, "utf-8"),
URLDecoder.decode(paramMap.get(param), "utf-8"));
}
// 获取请求的参数
Integer pageNow = Integer.parseInt(map.getOrDefault("pageNow", "1"));
PageHelper.startPage(pageNow, 5);
List<User> usersInfo = userService.getUserByMapPojos(paramMap);
PageInfo pageInfo = PageUtil.setPageInfo(pageNow, 5, usersInfo);
return RestResponse.ok(new HashMap<String, Object>(){{
put("usersInfo", usersInfo);
put("pageInfo", pageInfo);
}}).toJSONString();
}
}
这里需要注意,在传递中文参数的时候,URL对其进行了转码,所以在后台接收到的数据默认是转码后的结果,比如在地址栏输入
http://localhost:8080/测试
然后手动复制这个地址,再粘贴后
http://localhost:8080/%E6%B5%8B%E8%AF%95
所以在Controller层接收的参数值就会是这个%E6%B5%8B%E8%AF%95,可以通过JavaEE原生的包java.net.URLDecoder
里提供的decode方法进行解码,比如:(这里笔者将Map的KV都进行了转码):
paramMap.put(URLDecoder.decode(param, "utf-8"), URLDecoder.decode(paramMap.get(param), "utf-8"));
同样地,之前在前端的JS文件中也进行了解码(防止中文乱码)
thisURL = decodeURI(thisURL)
三、前后端分离思路:动态渲染Ajax请求返回的数据
在这个项目里,笔者一开始是使用Thymeleaf模板引擎渲染数据的,之前要么通过session获取数据,要么通过Controller层在Model对象里写入的数据,而现在则是通过JSON数据,关于数据库的页面就通过JS来渲染,比如渲染用户的信息:
function renderRoleInfo(roleInfo){
$('#tbody-role').html('') // 先清空内容
$.each(roleInfo, (index, user) =>{
$('#tbody-role').append(`
<tr valign="baseline">
<td><input type="checkbox" class="cb_role" data-code="${user['code']}"></td>
<td>${user['code']}</td>
<td>${user['role'][0]['roleName']}</td>
<td>
<button class="btn btn-primary" onclick="updateRole('${user['code']}', 'up')">晋升</button>
<button class="btn btn-warning" onclick="updateRole('${user['code']}', 'down')">降级</button>
<button class="btn btn-danger">禁用</button>
</td>
</tr>`)
})
}
不难发现,这个和ThymeLeaf类似,使用 ` 符号标记HTML代码,然后可以在里面通过${}取JS文件里的变量值。
还有个问题就是它在什么时候调用?笔者采取的是在AJAX请求获取到数据的时候进行渲染
function loadData(){
$.ajax({
url: '/admin/user/query',
type: 'POST',
data: getUrlParamToJSON(),
dataType: 'JSON',
contentType: 'application/json;charset=UTF-8',
error: (err) =>{
console.error(err)
open_toast('fail', "获取用户数据失败!")
},
success: (data) =>{
console.log(data)
if(data['code'] === 1){
$('#userTbody').html('')
// 渲染搜索的标签
loadSearchTag()
// 设置渲染延迟
setTimeout(function (){
// 渲染用户信息
loadUserInfo(data['response']['usersInfo'])
// 渲染分页信息
loadPageInfo(data['response']['pageInfo'])
// 渲染完毕后关闭加载
$('#searchSpinnerPlaceholder').html('')
// 设置结果显示
$('#TextOfShowResultSum').text(`共有${data['response']['pageInfo'].total}条结果`)
}, 200)
}
}
})
}
四、两种方式实现筛选查询
思路一:代码限制查询的筛选条件
数据是通过AJAX获取的,而筛选的条件则显示在URL里,所以接下来可以有两种方式实现筛选的功能,一是通过代码修改Service层业务逻辑处理(繁琐),二是通过改写Mapper映射文件SQL语句(推荐),由于笔者是通过MyBatis逆向工程生成的DAO层接口以及实现的映射文件,它提供了原生的一些增删改查接口,所以没有自己写多表查询的SQL(使用的方法1),在Service层通过简单的SQL语句实现查询的条件限制,之后就用方法2进行了优化,这里都做一个记录吧…
实现效果:
JS关键代码:
/* 选择搜索的条件触发函数 */
$('.nav-search ul li a').click(function () {
$wrapper = $('#searchWrapper')
$wrapper.html('')
const searchType = $(this).text();
/* 添加地址 */
if(searchType === '详细地址'){
$wrapper.html(`<div class="pt-2">
省份
<select name="searchProv">
<option value="*" data-code="*">请选择</option>
</select>
市区
<select name="searchCity">
<option value="*" data-code="*">请选择</option>
</select>
地区
<select name="searchArea">
<option value="*" data-code="*">请选择</option>
</select>
详细地址
<input name="searchDetail">
</div>`)
loadSearchAddress()
} else if(searchType === '名称'){
$wrapper.html('<input id="input-search-text" type="text" name="searchUserName"' +
' class="bg-light w-auto mx-1" placeholder="支持正则表达式">')
} else if(searchType === '性别'){
$wrapper.html('<select name="searchGender">\n' +
'<option value="男">男</option>\n' +
'<option value="女">女</option>\n' +
'</select>')
} else if(searchType === '生日'){
$wrapper.html('从 ' +
'<input type="date" name="searchBirthdayBegin" value="2022-02-14">' +
' 至 ' +
'<input type="date" name="searchBirthdayEnd" value="2022-02-14">')
} else if(searchType === '电话'){
$wrapper.html('<input id="input-search-text" type="text" name="searchPhone"' +
' class="bg-light w-auto mx-1" placeholder="支持正则表达式">')
} else if(searchType === 'ID'){
$wrapper.html('范围 ' +
'<input type="number" name="searchIDBegin" value="1">' +
' — </div>' +
'<input type="number" name="searchIDEnd" value="1">\n')
}
$('#label-search-type').text(searchType)
})
/* 动态渲染搜索条件的标签 */
function ualert(searchType, searchText, alertStyle) {
let wrapper = $('<div class="div-search-condition mx-3 col-md-3 pt-2 pb-1 fade show alert alert-' + alertStyle
+ ' alert-dismissible" role="alert" data-type="' + searchType + '" data-text="' + searchText +'">' +
searchType + ' :' + searchText + '<button type="button" class="btn-close btn-sm pt-3 pb-1" data-bs-dismiss="alert" aria-label="Close">' +
'</button></div>')
$('#searchAlertPlaceholder').append(wrapper)
}
/* 添加筛选标签 */
$('#btn-add-search').click(function(){
let count = $('.div-search-condition').length
// 限制1: 筛选标签数量 <=3
if(count >= 3) {
open_toast('fail', '目前最多支持添加三个标签')
return ;
}
let searchType = $('#label-search-type').text()
let searchText = ''
if(searchType === '名称'){
searchText = $('input[name="searchUserName"]').val()
} else if(searchType === '性别'){
searchText = $('select[name="searchGender"]').val()
} else if(searchType === '生日'){
searchText = $('input[name="searchBirthdayBegin"]').val() + '到'
$('input[name="searchBirthdayEnd"]').val()
} else if(searchType === '详细地址'){
searchText = $selectProv.val() + '-' + $selectCity.val() + '-'
+ $selectArea.val() + '-' + ($inputDetail.val() === '' ? '*' : $inputDetail.val())
} else if(searchType === 'ID'){
searchText = $('input[name="searchIDBegin"]').val() + '到' +
$('input[name="searchIDEnd"]').val()
} else if(searchType === '电话'){
searchText = $('input[name="searchPhone"]').val()
}
// 限制2: 筛选内容不为空
if(searchText == null || searchText === '')
open_toast('warning', '请输入筛选的条件')
// 限制3: 筛选类型不能重复
else if(hasSearchType(searchType)) {
open_toast('warning', '当前筛选的类型已存在!')
} else
ualert(searchType, searchText, 'success')
})
// 搜索功能
$('#btn-start-search').click(function(){
// 显示加载图标
let reqUrl = clearURIExceptPageNow()
// 封装表单信息
let reqData = []
$('.div-search-condition').each(function(){
reqData.push({
name: $(this).data('type'),
value: $(this).data('text')
})
reqUrl = updateUrlParam(reqUrl, $(this).data('type'), $(this).data('text'))
})
// 将页数默认为第一页
reqUrl = updateUrlParam(reqUrl, 'pageNow', '1')
// 更换地址
window.location = reqUrl
$.ajax({
url: '/admin/user/search',
type: 'POST',
data: JSON.stringify(reqData),
dataType: 'JSON',
contentType: 'application/json;charset=UTF-8',
error: (err) => {
open_toast('fail', "请求失败!")
},
success: (data) =>{
if(data['code'] === 1) {
loadUserInfo(data['response']['usersInfo'])
loadPageInfo(data['response']['pageInfo'])
$('#searchSpinnerPlaceholder').html('')
}
}
})
})
Controller层:
public String toAdminUserByPageBySome(@RequestBody Map<String, String> paramMap){
int pageNow = Integer.parseInt(paramMap.getOrDefault("pageNow", "1"));
PageHelper.startPage(pageNow, 5);
List<User> usersInfo = userService.getUserByMapPojos(paramMap);
PageInfo pageInfo = PageUtil.setPageInfo(pageNow, 5, usersInfo);
return RestResponse.ok(new HashMap<String, Object>(){{
put("usersInfo", usersInfo);
put("pageInfo", pageInfo);
}}).toJSONString();
}
Service层
public List<User> getUserByMapPojos(Map<String, String> paramMap) {
UserExample userExample = new UserExample();
UserExample.Criteria criteria = userExample.createCriteria();
boolean up = true;
for (String param : paramMap.keySet()) {
String value = paramMap.get(param);
if("orderById".equals(param))
up = "up".equals(value);
if ("名称".equals(param)) {
criteria.andUsernameLike("%" + value + "%");
}
else if ("生日".equals(param)) {
System.out.println(value);
}
else if ("性别".equals(param)) {
if ("男".equals(value))
criteria.andGenderEqualTo(2);
else if ("女".equals(value))
criteria.andGenderEqualTo(1);
}
else if ("电话".equals(param)) {
criteria.andPhoneLike("%" + value + "%");
}
else if ("详细地址".equals(param)) {
criteria.andAddressLike("%" + value + "%");
}
else if ("ID".equals(param)) {
String[] v = value.split("到");
int begin = Integer.parseInt(v[0]);
int end = Integer.parseInt(v[1]);
criteria.andIdBetween((long) Math.max(1, begin), (long) Math.max(0, end));
}
}
List<User> users = userMapper.selectByExampleWithBLOBs(userExample);
if(!up){
users.sort(new Comparator<User>() {
@Override
public int compare(User o1, User o2) {
if(o1.getId() < o2.getId())
return 1;
else if(o1.getId() > o2.getId())
return -1;
else
return 0;
}
});
}
return users;
}
DAO层就不放代码了,是MyBatis逆向工程插件的原生代码,想了解其基本使用可参考文章【点击查看】
思路二:通过改写Mapper映射文件实现多表查询+条件限制
实现效果:
前端部分就不放了,和之前一样是通过AJAX请求数据的,搜索则是直接修改地址栏的URL
Controller层:
package com.uni.controller.admin;
import com.github.pagehelper.PageHelper;
import com.github.pagehelper.PageInfo;
import com.uni.base.RestResponse;
import com.uni.pojo.Role;
import com.uni.pojo.User;
import com.uni.service.RoleService;
import com.uni.service.UserService;
import com.uni.utils.PageUtil;
import com.uni.utils.ParamUtil;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
import java.io.UnsupportedEncodingException;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
@RestController
@RequestMapping("/admin/role")
public class RoleAdminController {
@Autowired
UserService userService;
@Autowired
RoleService roleService;
@PostMapping("/query/all")
public String getRoleInfo(@RequestBody(required = false) Map<String, String> paramMap) {
ParamUtil.translate(paramMap);
int pageNum = Integer.parseInt(paramMap.getOrDefault("pageNow", "1"));
PageHelper.startPage(pageNum, 5);
List<User> users = userService.getUserRoleByMapPojos(paramMap);
List<Role> roles = roleService.queryAllRole();
PageInfo pageInfo = PageUtil.setPageInfo(pageNum, 5, users);
return RestResponse.ok(new HashMap<String, Object>(){{
put("pageInfo", pageInfo);
put("roleInfo", roles);
}}).toJSONString();
}
}
上述代码使用的 ParamUtil工具类是自定义的,就是之前URL编码的问题,内容如下:
package com.uni.utils;
import java.io.UnsupportedEncodingException;
import java.net.URLDecoder;
import java.util.Map;
public class ParamUtil {
public static Map<String, String> translate(Map<String, String> map) {
try {
for (String key : map.keySet()) {
map.put(URLDecoder.decode(key, "utf-8"),
URLDecoder.decode(map.get(key), "utf-8"));
}
}catch(UnsupportedEncodingException e){
e.printStackTrace();
}
return map;
}
}
Service层: 直接调用Mapper接口即可,相比之前的代码简化了许多
@Override
public List<User> getUserRoleByMapPojos(Map<String, String> paramMap) {
return userMapper.selectCodeWithRole(paramMap);
}
DAO层:UserMapper.xml
目前笔者对角色只做了角色名称和用户账号进行了筛选
<select id="selectCodeWithRole" parameterType="Map" resultMap="userWithRole">
SELECT user.code, r.id, r.roleName
FROM v2_user AS user
LEFT JOIN v2_user_role AS ur
ON user.id = ur.userId
LEFT JOIN v2_role AS r
ON ur.roleId = r.id
<where>
<foreach collection="map" item="value" index="key" separator="AND">
<if test="key == 'roleName'">
r.roleName = #{value}
</if>
<if test="key == 'code'">
user.code LIKE CONCAT('%',#{value}, '%')
</if>
</foreach>
</where>
</select>
<resultMap id="userWithRole" type="com.uni.pojo.User">
<result column="code" property="code"/>
<collection property="role" ofType="com.uni.pojo.Role">
<result column="id" property="id"/>
<result column="roleName" property="roleName"/>
</collection>
</resultMap>
这里用到了用户角色表(中间表)进行角色的查询,三张表的结构如下: