第三阶段实战(六)—— 用户管理功能设计与实现

1 用户管理设计说明

1.1 业务设计说明

本模块主要是实现对用户信息的管理,包括用户查询,保存,更新,禁用启用等操作,其 业务分析如下图所示:图
在这里插入图片描述
基于对表的设计,其数据逻辑关系的展示,如图
在这里插入图片描述
用户表设计的脚本如下:

DROP TABLE IF EXISTS `sys_users`;
/*!40101 SET @saved_cs_client     = @@character_set_client */;
/*!40101 SET character_set_client = utf8 */;
CREATE TABLE `sys_users` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `username` varchar(50) NOT NULL COMMENT '用户名',
  `password` varchar(100) DEFAULT NULL COMMENT '密码',
  `salt` varchar(50) DEFAULT NULL COMMENT '盐  密码加密时前缀,使加密后的值不同',
  `email` varchar(100) DEFAULT NULL COMMENT '邮箱',
  `mobile` varchar(100) DEFAULT NULL COMMENT '手机号',
  `valid` tinyint(4) DEFAULT NULL COMMENT '状态  0:禁用   1:正常  默认值 :1',
  `deptId` int(11) DEFAULT NULL,
  `createdTime` datetime DEFAULT NULL COMMENT '创建时间',
  `modifiedTime` datetime DEFAULT NULL COMMENT '修改时间',
  `createdUser` varchar(20) DEFAULT NULL COMMENT '创建用户',
  `modifiedUser` varchar(20) DEFAULT NULL COMMENT '修改用户',
  PRIMARY KEY (`id`),
  UNIQUE KEY `username` (`username`)
) ENGINE=InnoDB AUTO_INCREMENT=21 DEFAULT CHARSET=utf8 COMMENT='系统用户';
/*!40101 SET character_set_client = @saved_cs_client */;

用户与角色的关系表脚本设计如下:

DROP TABLE IF EXISTS `sys_user_roles`;
/*!40101 SET @saved_cs_client     = @@character_set_client */;
/*!40101 SET character_set_client = utf8 */;
CREATE TABLE `sys_user_roles` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `user_id` int(11) DEFAULT NULL COMMENT '用户ID',
  `role_id` int(11) DEFAULT NULL COMMENT '角色ID',
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=77 DEFAULT CHARSET=utf8 COMMENT='用户与角色对应关系';
/*!40101 SET character_set_client = @saved_cs_client */;

1.2 原型设计说明

基于用户需求,通过静态页面为用户呈现用户模块的基本需求。 当在主页点击用户管理时,呈现用户列表页面,如图:
在这里插入图片描述
在列表页面点击添加按钮时,呈现用户编辑页面, 如图:
在这里插入图片描述
在编辑页面点击所属部门时,呈现部门树结构信息, 如图:
在这里插入图片描述
说明:假如客户对此原型进行了确认,后续则可以基于此原型进行研发。

1.3 API 设计说明

用户管理业务后台 API 分层架构及调用关系如图
在这里插入图片描述
说明:分层目的主要将复杂问题简单化,实现各司其职,各尽所能。

2 用户管理列表页面呈现

2.1 业务时序分析

用户列表页面,其加载时序分析,如图
在这里插入图片描述

2.2 服务端实现

2.2.1 Controller 实现

业务描述与设计实现
基于用户管理的请求业务,在 PageController 中添加返回用户页面相关方法。
关键代码设计与实现
检查 PageController 中是否有返回 UI 页面的方法,有则无需添加。例如:

@RequestMapping("{module}/{moduleUI}")
public String doMouduleUI(@PathVariable String module){
	return "sys/"+moduleUI;
}

2.3 客户端实现

2.3.1 首页菜单事件处理

业务描述与设计实现
首先准备用户列表页面(/templates/pages/sys/user_list.html),然后在 starter.html 页面中点击用户管理时异步加载用户列表页面。
关键代码设计与实现
找到项目中的 starter.html 页面,页面加载完成以后,注册用户管理项的点击事件, 当点击用户管理时,执行事件处理函数。关键代码如下:

$(function(){      
	doLoadUI("load-user-id","user/user_list") }) 
function doLoadUI(id,url){   
	$("#"+id).click(function(){       
		$("#mainContentId").load(url);     
	}); 
}

其中,load 函数为 jquery 中的 ajax 异步请求函数。

2.3.2 用户列表页面

业务描述与设计实现
本页面呈现用户信息时要以分页形式进行呈现。
▪ 关键代码设计与实现:
参考 sys_user.html 文件内容

3 用户管理列表数据呈现

3.1 数据架构分析

用户列表页面加载完成,启动用户数据的异步加载操作,本次列表页面要以分页形式 呈现用户信息,其数据查询时,数据的封装及传递过程,如图
在这里插入图片描述
说明:本模块将从数据库查询到的用户及相关数据封装到SysUserDeptVo 对象,一行 记录一个SysUserDeptVo 对象。 用户列表数据加载,其时序分析,如图
在这里插入图片描述

3.2 服务端关键业务及代码实现

3.2.1 pojo 类实现

业务描述及设计实现
构建pojo对象,封装从数据库查询到的用户以及用户对应的部门信息,一行记录映射 为内存中一个的这样的对象。
关键代码分析及实现
定义SysUserDept类,基于此类对象属性封装数据。关键代码如下:

package com.cy.pj.sys.pojo;
import com.fasterxml.jackson.annotation.JsonFormat;
import lombok.Data;
import java.io.Serializable;
import java.util.Date;

@Data
@AllArgsConstructor
@NoArgsConstructor
public class SysUserDept implements Serializable {
    private static final long serialVersionUID = 3776304095517793129L;
    private Integer id;
    private String username;
    private String password;//md5
    private String salt;//加密盐
    private String email;
    private String mobile;
    private Integer valid=1;
    private SysDept sysDept; //private Integer deptId;
    @JsonFormat(pattern = "yyyy/MM/dd HH:mm")
    private Date createdTime;

    @JsonFormat(pattern = "yyyy/MM/dd HH:mm")
    private Date modifiedTime;
    private String createdUser;
    private String modifiedUser;

}

说明:通过此对象除了可以封装从数据库查询的数据,还可以封装客户端请求数据, 实现层与层之间数据的传递。

3.2.2 Dao 接口实现

业务描述及设计实现
通过数据层对象,基于业务层的参数数据,查询用户记录总数以及当前页面要呈现的 用户基本信息。
关键代码分析及实现:
第一步:定义用户数据层接口对象,通过此对象实现用户业务的数据操作。
第二步:在 SysUserDao 接口中添加 getRowCount 方法用于按条件统计记录总数。
第三步:在SysUserDao 接口中添加findPageObjects 方法,基于此方法实现当前页 记录的数据查询操作。代码如下:

package com.cy.pj.sys.dao;

import com.cy.pj.sys.pojo.SysUser;
import com.cy.pj.sys.pojo.SysUserDept;
import java.util.List;

/**通过此Dao操作用户模块的数据*/
@Mapper
public interface SysUserDao {
    int getRowcount(String username);
    List<SysUserDept> findPageObject(String username,Integer startIndedx,Integer pageSize);
}

3.2.3 Mapper 文件实现

业务描述及设计实现
基于 Dao 接口创建映射文件,在此文件中通过相关元素(例如 select)描述要执行的 数据操作。
关键代码设计及实现
第一步:在映射文件的设计目录中添加 SysUserMapper.xml 映射文件,代码如下:

<?xml version="1.0" encoding="UTF-8"?> 
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"   
"http://mybatis.org/dtd/mybatis-3-mapper.dtd"> 
<mapper namespace="com.cy.pj.sys.dao.SysUserDao"> 
</mapper>

第二步:在映射文件中添加 sql 元素实现,SQL 中的共性操作,代码如下:

 <sql id="queryWhereId"> 
 	from sys_users           
 	<where>             
 		<if test="username!=null and username!=''">                
 			username like concat("%",#{username},"%")             
 		</if>           
 	</where>     
 </sql> 

第三步:在映射文件中添加 id 为 getRowCount 元素,按条件统计记录总数,代码如 下:

<select id="getRowCount" resultType="int">           
	select count(*)            
	<include refid="queryWhereId"/>     
</select>

第四步:在映射文件中添加 id 为 findPageObjects 元素,实现分页查询。代码如下:

<select id="findPageObjects" resultMap="sysUserDept">            
	select *            
	<include refid="queryWhereId"/>            
	order by createdTime desc            
	limit #{startIndex},#{pageSize}    
</select>

第五步:定义查询结果映射元素。代码如下:

<resultMap type="com.cy.pj.sys.vo.SysUserDeptVo" id="sysUserDept">             
	<!-- 一般应用于 many2one 或 one2one 做关联查询                   
	在当前应用是基于 deptId 查询部门信息并将其                  
	存储到 SysUserDept 对象的 sysDept 属性中。-->             
	<association property="sysDept" column="deptId" select="com.cy.pj.sys.dao.SysDeptDao.findById">             
	</association>      
</resultMap>

思考:

  1. resultMap 用户进行自定义结果映射。
  2. association 元素用于定义关联数据的查询。

3.2.4 Service 接口及实现类

业务描述与设计实现
在用户分页查询中,业务层对象主要负责对业务数据进行校验,并借助数据层对象完 成数据的分页查询操作。
关键代码设计及实现
第一步:定义用户业务接口及方法,暴露外界对用户业务数据的访问,其代码参考如下:

package com.cy.pj.sys.service; 
public interface SysUserService {   
	PageObject<SysUserDept> findPageObjects(String username,Integer pageCurrent); 
}

第二步:定义用户业务接口实现类,并添加用户业务数据分页查询操作的具体实现,其 代码参考如下:

package com.cy.pj.sys.service.impl; import java.util.List; 
import org.springframework.beans.factory.annotation.Autowired; 
import org.springframework.stereotype.Service; 
import org.springframework.util.StringUtils;  
@Service  
public class SysUserServiceImpl implements SysUserService {
	@Autowired
	private SysUserDao sysUserDao;  
	@Override  
	public PageObject<SysUserDept> findPageObjects(    String username,Integer pageCurrent) {   
		//1.对参数进行校验   
		if(pageCurrent==null||pageCurrent<1) throw new IllegalArgumentException("当前页码值无效");   
		//2.查询总记录数并进行校验   
		int rowCount=sysUserDao.getRowCount(username);   
		if(rowCount==0) throw new ServiceException("没有找到对应记录");   
		//3.查询当前页记录   
		int pageSize=2;   
		int startIndex=(pageCurrent-1)*pageSize;   
		List<SysUserDept> records= sysUserDao.findPageObjects(username,startIndex,pageSize);   
		//4.对查询结果进行封装并返回   
		return new PageObject<>(pageCurrent, pageSize, rowCount, records);  
	} 
}

3.2.5 Controller 类实现

业务描述与设计实现
控制层对象主要负责请求和响应数据的处理,例如,本模块通过业务层对象执行业务 逻辑,再通过VO对象封装响应结果(主要对业务层数据添加状态信息),最后将响应结果转 换为 JSON 格式的字符串响应到客户端。
关键代码设计与实现
定义 Controller 类,并将此类对象使用 Spring 框架中的@RestController 注解进 行标识,表示此类对象要交给 Spring 管理。然后基于@RequestMapping 注解为此类定义 根路径映射,添加菜单查询处理方法,代码参考如下:

package com.cy.pj.sys.controller; 
 
@RequestMapping("/user/") 
@RestController 
public class SysUserController {    
	@Autowired  
	private SysUserService sysUserService; 
	@RequestMapping("doFindPageObjects") 
	public JsonResult doFindPageObjects(String username,Integer pageCurrent) {    
		return new JsonResult(sysUserService.findPageObjects(username,pageCurrent));   
	}
}

3.3 客户端关键业务及代码实现

3.3.1 菜单列表信息呈现

业务描述与设计实现
角色分页页面加载完成以后,向服务端发起异步请求加载角色信息,当角色信息加载 完成需要将角色信息、分页信息呈现到列表页面上。
关键代码设计与实现
异步请求处理函数,关键代码如下: 第一步:分页页面加载完成,向服务端发起异步请求,代码参考如下:

$(function(){     
	//为什么要将 doGetObjects 函数写到 load 函数对应的回调内部。     
	$("#pageId").load("doPageUI",function(){      
		doGetObjects();     
	}); 
}

第二步:定义异步请求处理函数,代码参考如下:

function doGetObjects(){     
	//debugger;//断点调试     
	//1.定义 url 和参数     
	var url="user/doFindPageObjects"     
	var params={"pageCurrent":1};//pageCurrent=2     
	//2.发起异步请求     
	//请问如下 ajax 请求的回调函数参数名可以是任意吗?可以,必须符合标识符 的规范        
	$.getJSON(url,params,function(result){      
		//请问 result 是一个字符串还是 json 格式的 js 对象?对象              
		doHandleResponseResult(result);    
	} );//特殊的 ajax 函数    
}

第三步:定义回调函数,处理服务端的响应结果。代码如下:

function doHandleResponseResult (result){ //JsonResult     
	if(result.state==1){//ok   
		//更新 table 中 tbody 内部的数据   
		doSetTableBodyRows(result.data.records);//将数据呈现在页面上    
		//更新页面 page.html 分页数据   
		doSetPagination(result.data); //此方法写到 page.html 中      
	}else{   
		alert(result.msg);      
	}    
}

第四步:将异步响应结果呈现在 table 的 tbody 位置。代码参考如下:

function doSetTableBodyRows(records){     
	//1.获取 tbody 对象,并清空对象     
	var tBody=$("#tbodyId");     
	tBody.empty();     
	//2.迭代 records 记录,并将其内容追加到 tbody     
	for(var i in records){      
		//2.1 构建 tr 对象      
		var tr=$("<tr></tr>");      
		//2.2 构建 tds 对象      
		var tds=doCreateTds(records[i]);      
		//2.3 将 tds 追加到 tr 中      
		tr.append(tds);      
		//2.4 将 tr 追加到 tbody 中      
		tBody.append(tr);     
	}    
}

第五步:创建每行中的 td 元素,并填充具体业务数据。代码参考如下:

function doCreateTds(row){     
	console.log(row);     
	var tds="<td><input type='radio' name='radioId' value='"+row.id+"' ></td>"+       
	"<td>"+row.username+"</td>"+       
	"<td>"+(row.sysDept?row.sysDept.name:'未分配')+"</td>"+       
	"<td>"+row.email+"</td>"+       
	"<td>"+row.mobile+"</td>"+       
	"<td>"+(row.valid?"启用":"禁用")+"</td>"+       
	"<td>"+new Date(row.createdTime).toLocaleString()+"</td>"+       
	"<td>"+new Date(row.modifiedTime).toLocaleString()+"</td>"+       
	"<td><button type='button' class='btn btn-default btnvalid'>"+(row.valid?"禁用":"启用")+"</button></td>";         
	return tds;    
}

4 用户管理禁用操作实现

4.1 核心业务分析

基于用户在列表页面上选择的的用户记录 ID,执行禁用或启用操作,后续业务中被禁 用的用户不允许登陆系统。 其时序分析,如图:
在这里插入图片描述

4.2 服务端关键业务及代码实现

4.2.1 Dao 接口实现

业务描述及设计实现
基于用户 id,修改用户状态信息,对此用户进行禁用或启用。
关键代码设计及实现:
在创建 SysUserDao 中添加修改用户状态信息的方法。关键代码如下:

int validById(@Param("id") Integer id,
			@Param("valid") Integer valid,
			@Param("modifiedUser") String modifiedUser); 

4.2.2 Mapper 文件实现

业务描述及设计实现
在 SysUserDao 接口对应的映射文件中添加修改用户状态信息的元素,然后在元素内 部定义具体的 SQL 实现。
关键代码设计与实现
在 SysUserMapper.xml 文件,添加修改用户状态的 SQL 元素定义,关键代码如下:

<update id="validById">        
	update sys_users        
	set 
	valid=#{valid},modifiedUser=#{modifiedUser},modifiedTime=now()        
	where id=#{id}    
</update>

4.2.3 Service 接口及实现类

业务描述与设计实现
在用户业务层,添加用于完成禁用或启用用户状态的相关业务方法及实现。
关键代码设计与实现
第一步:在 SysUserService 接口中,添加修改用户装填的方法。关键代码如下:

int validById(Integer id,Integer valid) 

第三步:在 SysUserServiceImpl 实现类中添加禁用,启用业务的具体实现。关键代 码如下:

@Override  
public int validById(Integer id,Integer valid) {   
	//1.合法性验证   
	if(id==null||id<=0) throw new ServiceException("参数不合法,id="+id);   
	if(valid==null||(valid!=1&&valid!=0)) throw new ServiceException("参数不合法,valid="+valid);   
	//2.执行禁用或启用操作(admin 为后续登陆用户)   
	int rows=sysUserDao.validById(id, valid,"admin");   
	//3.判定结果,并返回   
	if(rows==0) throw new ServiceException("此记录可能已经不存在");   
	return rows;  
}

4.2.4 Controller 类实现

业务描述与设计实现
在用户控制层对象中,添加用于处理禁用启用业务的控制层方法。首先在此方法中通 过形参接收客户端提交的数据,然后调用业务层对象执行禁用,启用操作,最后封装执行 结果,并在运行时将响应对象转换为 JSON 格式的字符串,响应到客户端。
关键代码设计与实现
第一步:在 SysUserController 中添加用于执行删除业务的方法。代码如下:

@RequestMapping("doValidById")
public JsonResult doValidById(Integer id,Integer valid){
   sysUserService.validById(id,valid);
   return new JsonResult("update ok");
}

第二步:启动 tomcat 进行访问测试,打开浏览器输入如下网址:
http://localhost/user/doValidById?id=10

4.3 客户端关键业务及代码实现

4.3.1 用户列表页面事件处理

业务描述及设计实现
用户在用户列表页面上点击禁用或按钮,将用户记录 id 异步提交到服务端,最后在服 务端执行用户的禁用、启用动作。
关键代码设计与实现
第一步:用户列表页面加载完成以后,在删除按钮上进行点击事件注册。关键代码如 下:

$(".input-group-btn").on("click",".btn-valid",doValidById) 

第二步:定义禁用、启用操作对应的事件处理函数。关键代码如下:

function doValidById(){     
	//1.获取对象 id,validId 的值     
	var btn=$(this);//this 执行调用 dom 对象     
	var user=btn.parents("tr").data("rowData");     
	var id=user.id;     
	var valid=user.valid;     
	//2.构建 url,参数对象     
	var url="user/doValidById";     
	var params={"id":id,"valid":valid?0:1}     
	//3.发送异步请求,更新数据     
	$.post(url,params,function(result){      
		if(result.state==1){       
			alert(result.message);       
			//doGetObjects();//一种刷新方式       
			doEditRow(btn,valid?0:1);//一种局部刷新方式      
		}else{       
			alert(result.message);      
		}     
	})    
}

第三步:定义禁用、启用操作对应的局部刷新处理函数。关键代码如下:

function doEditRow(btn,valid){        
	//1.修改按钮上内容        
	btn.html(valid?"禁用":"启用");        
	//2.修改 td 中元素内容  
	var tr=btn.parents("tr");        
	tr.find("td:eq(5)").html(valid?"启用":"禁用");       
	//3.修改并重新绑定数据       
	var sysUser=tr.data("sysUser");       
	sysUser.valid=valid;       
	tr.data("sysUser",sysUser); 
}

5 用户添加页面呈现

5.1 业务时序分析

在用户列表页面,点击添加按钮时加载编辑页面,其加载时序分析,如图:
在这里插入图片描述

5.2 准备用户编辑页面

准备用户编辑页面(/templates/pages/sys/user_edit.html)

5.3 用户编辑页面呈现

业务描述与设计实现
在用户列表页面中点击添加按钮时,呈现用户编辑页面。
关键代码设计与实现
第一步:用户列表事件注册,关键代码如下:

$(document).ready(function(){      
	$(".input-group-btn").on("click",".btn-add",doLoadEditUI); 
});

第二步:定义用户列表页面添加按钮的事件处理函数,关键代码如下:

function doLoadEditUI(){    
	//1.定义标题(添加或修改)    
	var title;    
	if($(this).hasClass("btn-add")){     
		title="用户添加";    
	}else{     
		title="用户修改";    
	}    
	//2.异步加载页面    
	var url="user/user_edit";    
	$("#mainContentId").load(url,function(){     
		$(".box-title").html(title);    
	});  
};

6 用户页面角色呈现

6.1 核心业务分析

用户编辑页面呈现以后,发起异步任务从服务端获取角色信息然后呈现在页面上,其时 序分析如图:
在这里插入图片描述

6.2 服务端关键业务及代码实现

6.2.1 pojo 类定义

业务描述与设计实现
定义 pojo对象,基于此对象封装用户角色信息。
关键代码设计与实现
创建 CheckBox 类,基于此类对象封装角色 id,角色名信息。关键代码如下:

package com.cy.pj.common.pojo;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

import java.io.Serializable;
/**通过此对象封装类似CheckBox的结构的数据*/
@Data
@AllArgsConstructor
@NoArgsConstructor
public class CheckBox implements Serializable {
    private static final long serialVersionUID = -3930756932197466333L;
    private Integer id;
    private String name;
}

6.2.2 Dao 接口定义

业务描述与设计实现
在角色数据层接口中添加查询角色相关信息的方法,并对查询结果进行封装。
关键代码设计与实现
在 SysRole 添加查询角色 id,name 的相关方法。关键代码如下:

List<CheckBox> findObjects(); 

6.2.3 Mapper 文件实现

业务描述与设计实现
在角色数据层接口对应的映射文件中,定义角色查询对应的映射元素。
关键代码设计与实现
在 SysRoleMapper.xml 中添加 findObjects 元素。关键代码如下:

<select id="findObjects" resultType="com.cy.pj.common.pojo.CheckBox">            
	select id,name            
	from sys_roles   
</select>

6.2.4 Service 接口及实现类

业务描述与设计实现
在角色业务层接口中添加查询角色 id,name 相关信息的业务方法。
关键代码设计与实现
第一步:在 SysRoleService 接口中添加 findObjects 方法。关键代码如下:

 List<CheckBox> findObjects(); 

第二步:在 SysRoleService 接口中添加 findObjects 方法实现。关键代码如下:

List<CheckBox> findObjects(); 

第二步:在 SysRoleService 接口中添加 findObjects 方法实现。关键代码如下:

@Override     
public List<CheckBox> findObjects() {       
	return sysRoleDao.findObjects();     
}

6.2.5 Controller 类实现

业务描述与设计实现
在角色控制层对象中添加查询角色 id,name 相关信息的业务方法。
关键代码设计与实现
在 SysRoleController 中添加 doFindRoles 方法。关键代码如下:

@RequestMapping("doFindRoles")   
public JsonResult doFindRoles() {    
	return new JsonResult(sysRoleService.findObjects());   
}

6.3 客户端关键业务及代码实现

6.3.1 用户编辑页面实现

业务描述与设计实现
在用户编辑页面加载完成以后,异步加载角色信息,并在页面上进行呈现。
关键代码设计与实现
第一步:页面加载完成以后,异步加载角色相关信息。关键代码如下:

$(document).ready(function(){doLoadSysRoles(); }

第二步:定义异步加载角色信息的方法。关键代码如下:

function doLoadSysRoles(){   
	var url="role/doFindObjects";   
	$.getJSON(url,function(result){    
		if(result.state==1){     
			//初始化角色信息    
			doInitDivSysRoles(result.data);    
		}else{    
			alert(result.message);    
		}   
	})  
};

第三步:定义页面初始化方法,完成页面角色信息的初始化操作。关键代码如下:

//初始化表单角色数据  
function doInitDivSysRoles(data){   
	var div=$("#rolesId");   
	var checkBox="<input type='checkbox' name='roleItem' value='[id]'>[name]";   
	for(var i in data){    
		div.append(    
		checkBox.replace("[id]",data[i].id).replace("[name]",data[i].name));   
	}  
}

7 用户数据添加实现

7.1 核心业务分析

用户在编辑页面点击保存按钮时,获取编辑页面用户输入的基本信息异步提交到服务 端,实现用户数据的持久化操作。其时序分析,如图:
在这里插入图片描述

7.2 服务端关键业务及代码实现

7.2.1 pojo 类定义

业务描述与设计实现
负责封装用户的基本信息,然后由数据层持久化到数据库。
关键代码设计与实现
定义 SysUser 类,并通过相关数据封装用户基本信息,关键代码如下:

package com.cy.pj.sys.pojo;

import com.fasterxml.jackson.annotation.JsonFormat;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

import java.io.Serializable;
import java.util.Date;

@Data
@AllArgsConstructor
@NoArgsConstructor
public class SysUser implements Serializable {
    private static final long serialVersionUID = 3776304095517793129L;
    private Integer id;
    private String username;
    private String password;//md5
    private String salt;//加密盐
    private String email;
    private String mobile;
    private Integer valid=1;
    private Integer deptId;
    @JsonFormat(pattern = "yyyy/MM/dd HH:mm")
    private Date createdTime;
    @JsonFormat(pattern = "yyyy/MM/dd HH:mm")
    private Date modifiedTime;
    private String createdUser;
    private String modifiedUser;
}

7.2.2 DAO 接口定义

业务描述与设计实现
负责将用户提交的用户基本信息,持久化到数据库。
关键代码设计与实现
在 SysUserDao 接口中定义数据持久化方法:

int insertObject(SysUser entity); 

在 SysUserRoleDao 接口中方法定义(不存在则创建)

int insertObjects(@Param("userId")Integer userId, @Param("roleIds")Integer[] roleIds); 

7.2.3 Mapper 映射文件定义

业务描述与设计实现
基于 SysUserDao 中方法的定义,在对应的映射文件中添加的对应 SQL 元素。用于将 用户信息添加到数据库。
关键代码设计与实现
第一步:在 SysUserMapper.xml 中添加 insertObject 元素,用于写入用户信息。关 键代码如下:

<insert id="insertObject"
             parameterType="com.cy.pj.sys.pojo.SysUser"
             useGeneratedKeys="true"
             keyProperty="id">
   insert into sys_users
   (username,password,deptId,email,mobile,salt,valid,
   createdTime,modifiedTime,createdUser,modifiedUser)
   values
   (#{username},#{password},#{deptId},#{email},#{mobile},#{salt},#{valid},
   now(),now(),#{createdUser},#{modifiedUser})
</insert>

第二步:在 SysUserRoleMapper 中元素定义,关键代码如下:

<insert id="insertObjects">
    insert into sys_user_roles
    (user_id,role_id)
    values <!--(1,2),(1,3),(1,4)-->
    <foreach collection="roleIds" separator="," item="roleId">
        (#{userId},#{roleId})
    </foreach>
</insert>

7.2.4 Service 接口定义及实现

业务描述与设计实现
基于控制层请求,调用数据层对象将用户以及对应的角色信息写入到数据库中。
关键代码设计与实现
第一步:在 SysUserService 接口中,添加用于保存用户对象的方法。关键代码如下:

int saveObject(SysUser entity,Integer[]roleIds); 

第二步:在 SysUserServiceImpl 类中,实现用户保存操作。关键代码如下:

 @Override     
 public int saveObject(SysUser entity, Integer[] roleIds) {      
 	long start=System.currentTimeMillis();      
 	log.info("start:"+start);      
 	//1.参数校验      
 	if(entity==null) throw new IllegalArgumentException("保存对象不能为空");
 	if(StringUtils.isEmpty(entity.getUsername())) throw new IllegalArgumentException("用户名不能为空");      
 	if(StringUtils.isEmpty(entity.getPassword())) throw new IllegalArgumentException("密码不能为空");      
 	if(roleIds==null || roleIds.length==0) throw new IllegalArgumentException("至少要为用户分配角色");      
 	//2.保存用户自身信息         
	//2.1 对密码进行加密      
 	String source=entity.getPassword();      
 	String salt=UUID.randomUUID().toString();      
 	SimpleHash sh=new SimpleHash(//Shiro 框架        
 								"MD5",//algorithmName 算法         
 								source,//原密码         
 								salt, //盐值         
 								1);//hashIterations 表示加密次数 
 	entity.setSalt(salt);      
 	entity.setPassword(sh.toHex());//将加密结果转换为16进制      
 	int rows=sysUserDao.insertObject(entity);      
 	//3.保存用户角色关系数据      
 	sysUserRoleDao.insertObjects(entity.getId(), roleIds);      
 	long end=System.currentTimeMillis();      
 	log.info("end:"+end);      
 	log.info("total time :"+(end-start));      
 	//4.返回结果      
 	return rows; 
 }

说明:使用 SimpleHash 时,要添加一个 shiro 框架依赖

<dependency>    
	<groupId>org.apache.shiro</groupId>    
	<artifactId>shiro-spring</artifactId>    
	<version>1.4.1</version>   
</dependency>

7.2.5 Controller 类定义

业务描述与设计实现
接收客户端提交的用户相关数据,并对其进行封装,然后调用业务层对象进行业务处 理,最后将业务层处理结果响应到客户端。
关键代码设计与实现
定义 Controller 方法,借助此方法处理保存用户数据的请求和响应逻辑。关键代码 如下:

@RequestMapping("doSaveObject")  
public JsonResult doSaveObject(SysUser entity,Integer[] roleIds){
	sysUserService.saveObject(entity,roleIds);   
	return new JsonResult("save ok");  
}

7.3 客户端关键业务及代码实现

7.3.1 用户编辑页面部门信息呈现

业务描述与设计实现
用户点击所属部门时,异步加载部门信息并以 zTree 结构进行呈现,然后用户可以选 择对应的部门,将部门相关信息更新到页面上。
关键代码设计与实现
第一步:在所有部门相关 dom 元素上进行事件注册,关键代码如下:

$(".form-horizontal").on("click",".load-sys-dept",doLoadZTreeNodes);

第二步:定义事件处理函数,用户呈现部门信息,关键代码如下:

function doLoadZTreeNodes(){    
	var url="dept/doFindZTreeNodes";    
	$("#treeLayer").css("display","block");    
	$.getJSON(url,function(result){     
		if(result.state==1){      
			zTree = $.fn.zTree.init($("#zTreeId"),setting,result.data);     
		}else{      
			alert(result.message);     
		}    
	});  
}

第三步:部门 div 容器中注册,确定和取消事件,关键代码如下: 页面加载完成以后,进行事件注册:

$("#treeLayer").on("click",".btn-cancel",doHideTree).on("click",".btn-confirm",doConfirm); 

确定按钮事件处理函数定义:

function doConfirm(){    
	//1.获取选中的记录(id,name);    
	var selectedNodes=zTree.getSelectedNodes();    
	var node=selectedNodes[0];    
	//2.将 id 和 name 填写或绑定在具体对象上    
	$("#deptId").val(node.name);    
	$("#deptId").data("deptId",node.id)    
	//3.隐藏 zTree 对应的 Div    
	doHideTree();  
}

取消按钮事件处理函数定义:

function doHideTree(){    $("#treeLayer").css("display","none");  }

7.3.2 页面 cancel 按钮事件处理

业务描述与设计实现
点击页面 cancel 按钮时,加载菜单那列表页面。
关键代码设计与实现
第一步:事件注册(页面加载完成以后)

 $(".box-footer").on("click",".btn-cancel",doCancel) 

第二步:事件处理函数定义

function doCancel(){  
	$("#mainContentId").removeData("rowData");  
	$("#mainContentId").load("user/user_list");
} 

7.3.3 页面 Save 按钮事件处理

业务描述与设计实现
点击页面 save 按钮时,将页面上输入的菜单信息提交到服务端。
关键代码设计与实现
第一步:事件注册(页面加载完成以后)。

$(".box-footer").on("click",".btn-save",doSaveOrUpdate) 

第二步:Save 按钮事件处理函数定义。关键代码如下:

function doSaveOrUpdate(){   
	//1.url   
	var insertUrl="user/doSaveObject";   
	//2.获取表单数据   
	var params=doGetEditFormData();   
	//3.发起异步请求   
	$.post(insertUrl,params,function(result){    
		if(result.state==1){     
			alert(result.message);     
			doCancel();    
		}else{     
			alert(result.message);    
		}   
	})  
}

第三步:表单数据获取及封装。关键代码如下:

function doGetEditFormData(){   
	var params={        
		"username":$("#usernameId").val(),        
		"password":$("#passwordId").val(),        
		"email":$("#emailId").val(),        
		"mobile":$("#phoneId").val(),           
		"deptId":$("#deptId").data("deptId"),   
	}   
	var roleIds=new Array();   
	$("#rolesId input[type='checkbox']").each(function(){    
		if($(this).prop("checked")){     
		roleIds.push($(this).val())    
		}   
	});   
	params.roleIds=roleIds.toString();   
	console.log(params);   
	return params;     
}

8 用户修改页面数据呈现

8.1 核心业务分析

基于用户 id 查询用户以及用户对应的部门和角色信息,并将其对应的信息呈现在用户 编辑页面上,其时序图分析如图:
在这里插入图片描述

8.2 服务端关键业务及代码实现

8.2.1 DAO 接口定义

业务描述与设计实现
负责基于 id 执行用户和角色信息数据的查询操作。
关键代码设计与实现
在 SysUserDao 接口中定义基于用户 id 查询用户相关信息的方法,关键代码如下:

SysUserDept findObjectById(Integer id); 

在 SysUserRoleDao 接口中定义基于用户 id 查询角色 id 信息的方法,关键代码如下:

List<Integer> findRoleIdsByUserId(Integer id); 

8.2.2 Mapper 文件定义

业务描述与设计实现
基于SysUserDao,SysUserRoleDao中方法的定义,在映射文件中添加对应的用户查 询元素。
关键代码设计与实现
第一步:在 SysUserMapper.xml 中添加 id 为 findObjectById 的 select 元素,关 键代码如下:

<select id="findObjectById" parameterType="int" resultMap="sysUserDept">
	select * from sys_users               
	where id=#{id}         
</select>

第二步:在SysUserRoleMapper.xml中添加id为findRoleIdsByUserId的select 元素,关键代码如下:

<select id="findRoleIdsByUserId" resultType="int">         
	select role_id  from sys_user_roles         
	where user_id=#{id} 
</select>

8.2.3 Service 接口定义及实现

业务描述与设计实现
基于控制层请求,调用数据层方法,查询对应的用户及相关信息。
关键代码设计与实现
第一步:在 SysUserService 接口中,添加基于 id 查询用户及相关信息的方法。关 键代码如下:

Map<String,Object> findObjectById(Integer userId) ;

第二步:在 SysUserService 接口对应的实现类 SysUserServiceImpl 中添加 findObjectById 的具体实现。关键代码如下:

@Override 
public Map<String, Object> findObjectById(Integer userId) {   
	//1.合法性验证   
	if(userId==null||userId<=0) throw new IllegalArgumentException("参数数据不合法,userId="+userId);   
	//2.业务查询   
	SysUserDept user = sysUserDao.findObjectById(userId);   
	if(user==null) throw new ServiceException("此用户已经不存在");   
	List<Integer> roleIds= sysUserRoleDao.findRoleIdsByUserId(userId);   
	//3.数据封装   
	Map<String,Object> map=new HashMap<>(); 
	map.put("user", user);   
	map.put("roleIds", roleIds);   
	return map;  
}

8.2.4 Controller 类定义

业务描述与设计实现
基于客户端请求,调用业务层方法,查询对应的用户及相关信息。
关键代码设计与实现
在 SysUserController 类中定义基于用户 ID 查询用户的相关方法。关键代码如下:

@RequestMapping("doFindObjectById")  
public JsonResult doFindObjectById(Integer id){   
	Map<String,Object> map= sysUserService.findObjectById(id);   
	return new JsonResult(map);  
}

8.3 客户端关键业务及代码实现

8.3.1 列表页面修改按钮事件处理

业务描述与设计实现
在用户列表修改按钮上进行事件注册,点击页面修改按钮时,基于用户 id 向服务端发 起异步请求获取用户相关数据,然后加载修改页面。
关键代码设计与实现
第一步:页面加载完成,进行修改按钮事件注册,关键代码如下:

$(function(){         
	//假如是修改   
	$(".input-group-btn") .on("click","btn-update",doLoadEditUI);   
});

第二步:修改按钮事件处理函数定义或修改,关键代码如下:

function doLoadEditUI(){     
	//1.判定点击的对象     
	var title;     
	if($(this).hasClass("btn-add")){      
		title="添加用户";      
		doLoadPage(title); 
	}else if($(this).hasClass("btn-update")){      
		title="修改用户";      
		var id=$("tbody input[name='radioId']:checked").val();      
		console.log("id="+id)      
		if(!id){      
			alert("请先选择");      
			return;      
		}      
		//基于 id 进行查询并加载编辑页面      
		doFindObjectById(id,title);     
	}    
}

第三步:定义或修改加载编辑页面的方法。关键代码如下:

function doLoadPage(title){     
	var url="user/user_edit"     
	$("#mainContentId").load(url,function(){      
		$(".box-title").html(title);     
	})     
}

第四步:定义基于 id 查询用户信息的方法。关键代码如下:

function doFindObjectById(id,title){     
	//1.params     
	var params={"id":id};     
	//2.url     
	var url="user/doFindObjectById";     
	//3.ajax request     
	$.getJSON(url,params,function(result){//JsonResult      
		if(result.state==1){      
			$("#mainContentId").data("rowData",result.data);             
			doLoadPage(title);      
		}else{      
			alert(result.message);      
		}     
	});    
}

8.3.2 编辑页面角色数据呈现

业务描述与设计实现
页面加载完成,获取编辑页面数据,然后在页面指定位置进行数据呈现数据。
关键代码设计与实现
第一步:在用户编辑页面中,角色数据加载完成以后,获取用户编辑页面中需要的表 单数据,然后进行页面数据初始化。关键代码如下:

function doLoadRoles(){   
	var url="role/doFindRoles";   
	$.getJSON(url,function(result){    
		if(result.state==1){     
			doInitPageRoles(result.data);  
			doInitFormData();//修改时    
		}else{     
			alert(result.message);    
		}   
	})  
}

第三步:定义编辑页面数据初始化方法。关键代码如下:

function doInitFormData(){      
	var data=$("#mainContentId").data("rowData");      
	if(!data)return;      
	$("#pwdDiv").remove();   
	console.log(data);   
	//初始化用户信息    
	$("#usernameId").val(data.user.username);   
	$("#deptId").val(data.user.sysDept?data.user.sysDept.name:'');   
	$("#deptId").data("deptId",data.user.sysDept?data.user.sysDept.id:'');   
	$("#emailId").val(data.user.email);   
	$("#phoneId").val(data.user.mobile);   
	//初始化用户角色信息   
	var ids=data.roleIds;   
	for(var i in ids){    
		$("#rolesId input[value='"+ids[i]+"']").prop("checked",true);   
	}  
}

9 用户数据更新实现

9.1 核心业务分析

在用户编辑页面点击更新按钮时,异步提交数据到服务端,服务端要更新用户自身信 息以及用户和角色关系数据,其时序图分析,如图:
在这里插入图片描述

9.2 服务端关键业务及代码实现

9.2.1 DAO 接口实现

业务描述与设计实现
获取用户编辑页面数据,然后异步提交到服务端,将用户信息以及用户对应的角色关 系数据更新到数据库。
关键代码设计与实现
第一步:在 SysUserDao 接口中添加数据更新方法,关键代码如下:

int updateObject(SysUser entity); 

第二步:在 SysUserRoleDao 接口中添加基于用户 id 删除关系数据的方法,关键代 码如下:

int deleteObjectsByUserId(Integer userId); 

9.2.2 Mapper 文件定义

业务描述与设计实现
基于SysUserDao,SysUserRoleDao中方法的定义,编写用于实现用户更新的SQL元 素。
关键代码设计与实现
第一步:在 SysUserMapper.xml 中添加 updateObject 元素,用于更新菜单信息。 关键代码如下:

<update id="updateObject" parameterType="com.cy.pj.sys.pojo.SysUser">
   update sys_users
   set username=#{username},
       mobile=#{mobile},
       email=#{email},
       deptId=#{deptId},
       modifiedTime=now(),
       modifiedUser=#{modifiedUser}
   where id=#{id}
</update>

第二步:在 SysUserRoleMapper.xml 文件中添加基于用户 id 删除关系数据的元素, 关键代码如下:

<delete id="deleteObjectsByUserId" parameterType="int">          
	delete from sys_user_roles          
	where user_id=#{userId}              
</delete>

9.2.3 Service 接口及实现

业务描述与设计实现
基于控制层请求,对数据进行校验并调用数据层对象将角色信息以及角色菜单关系数 据更新到数据库中。
关键代码设计与实现
第一步:在 SysUserService 接口中,添加用于更新角色对象的方法。关键代码如下:

int updateObject(SysUser entity,Integer[] roleIds) 

第二步:在 SysUserServiceImpl 类中,实现更新角色操作。关键代码如下:

@Override
    public int updateObject(SysUser entity, Integer[] roleIds) {
        //1.参数校验
        if(entity==null)
            throw new IllegalArgumentException("保存对象不能为空");
        if(StringUtils.isEmpty(entity.getUsername()))
            throw new IllegalArgumentException("用户名不能为空");
        if(roleIds==null||roleIds.length==0)
            throw new IllegalArgumentException("必须为用户分配角色");
        //3.保存用户自身信息
        int rows=sysUserDao.updateObject(entity);
        //4.保存用户和角色关系数据
        sysUserRoleDao.deleteObjectsByUserId(entity.getId());
        sysUserRoleDao.insertObjects(entity.getId(),roleIds);
        return rows;
    }

9.2.4 Controller 类定义

业务描述与设计实现
首先接收客户端提交的用户数据,并对其进行封装,然后调用业务层对象对角色信息 进行更行更新,最后将业务层处理结果响应到客户端。
关键代码设计与实现
在 SysUserController 类中定义更新角色的方法。关键代码如下:

@RequestMapping("doUpdateObject")  
public JsonResult doUpdateObject(SysUser entity,Integer[] roleIds){   
	sysUserService.updateObject(entity,roleIds);   
	return new JsonResult("update ok");  
}

9.3 客户端关键业务及代码实现

9.3.1 编辑页面更新按钮事件处理

业务描述与设计实现
点击页面 save 按钮时,将页面上输入的用户编辑信息异步提交到服务端进行更新。
关键代码设计与实现
修改用户编辑页面中保存表单数据的 JS 函数,关键代码如下:

function doSaveOrUpdate(){   
	//1.params   
	var rowData=$("#mainContentId").data("rowData");   
	var params=doGetEditFormData();   
	if(rowData){      
		params.id=rowData.user.id;   
	}   
	//1.url   
	var insertUrl="user/doSaveObject";   
	var updateUrl="user/doUpdateObject";   
	var url=rowData?updateUrl:insertUrl;   
	//2.获取表单数据   
	//3.发起异步请求   
	$.post(url,params,function(result){    
		if(result.state==1){     
			alert(result.message);     
			doCancel();    
		}else{     
			alert(result.message);    
		}   
	})  
} 

10 修改密码页面呈现

10.1 服务端关键业务设计及实现

检查 PageController 中是否有返回 UI 页面的方法,有则无需添加。例如:

@RequestMapping("{module}/{moduleUI}") 
public String doModuleUI(@PathVariable String moduleUI) {   
	return "sys/"+moduleUI; 
}

10.2 客户端关键业务设计及实现

10.2.1 准备密码编辑页面

准备密码编辑页面(/templates/pages/sys/pwd_edit.html)

10.2.2 密码编辑页面呈现

业务描述与设计实现
在系统首页左侧操作菜单中点击修改密码时,呈现密码编辑页面。
关键代码设计与实现
在 starter.html 页面尾部的页面加载完成的事件处理函数中添加事件处理,关键代 码如下:

$(function(){           
	doLoadUI("load-user-id","user/user_list") 
}) 
function doLoadUI(id,url){   
	$("#"+id).click(function(){       
		$("#mainContentId").load(url);     
	}); 
} 

11 密码修改页面数据持久化实现

11.1 服务端关键业务设计及实现

11.1.1 DAO 接口定义

业务描述及设计实现
基于用户 id,修改用户密码和盐值。
关键代码设计及实现:
在创建 SysUserDao 中添加修改用户密码信息的方法。关键代码如下:

int updatePassword(@Param("password")String password,@Param("salt")String salt,@Param("id")Integer id); 

11.1.2 Mapper 映射文件定义

业务描述及设计实现
基于用户 SysUserDao 中修改密码方法的定义,在映射文件中定义映射元素。
关键代码设计及实现:
在创建 SysUserMapper.xml 中定义修改密码对应的映射元素。关键代码如下:

<update id="updatePassword">          
	update sys_users          
	set password=#{password},              
		salt=#{salt},              
		modifiedTime=now()          
	where id=#{id} 
</update>

11.1.3 Service 接口定义及实现

业务描述及设计实现
基于控制层提交的用户相关信息,实现修改密码业务。
关键代码设计及实现:
第一步:在 SysUserService 接口中添加,用于实现密码修改业务的方法(原密码,新密码,确认密码)。关键代码 如下:

 int updatePassword(String password,String newPassword,String cfgPassword); 

第二步:在 SysUserService 接口的实现类 SysUserServiceImpl 中添加密码修改业 务的具体实现。关键代码如下:

@Override  
public int updatePassword(String password, String newPassword,  String cfgPassword) {   
	//1.判定新密码与密码确认是否相同   
	if(StringUtils.isEmpty(newPassword)) throw new IllegalArgumentException("新密码不能为空");   
	if(StringUtils.isEmpty(cfgPassword)) throw new IllegalArgumentException("确认密码不能为空");   
	if(!newPassword.equals(cfgPassword)) throw new IllegalArgumentException("两次输入的密码不相等");   
	//2.判定原密码是否正确   
	if(StringUtils.isEmpty(password)) throw new IllegalArgumentException("原密码不能为空");   
	//获取登陆用户   
	SysUser user=(SysUser)SecurityUtils.getSubject().getPrincipal();   
	SimpleHash sh=new SimpleHash("MD5",   password, user.getSalt(), 1);   
	if(!user.getPassword().equals(sh.toHex())) throw new IllegalArgumentException("原密码不正确");   
	//3.对新密码进行加密   
	String salt=UUID.randomUUID().toString();   
	sh=new SimpleHash("MD5",newPassword,salt, 1);   
	//4.将新密码加密以后的结果更新到数据库   
	int rows=sysUserDao.updatePassword(sh.toHex(), salt,user.getId());   
	if(rows==0) throw new ServiceException("修改失败");   
	return rows;  
}

11.1.4 Controller 类定义

业务描述及设计实现
基于客户端提交的修改密码请求,定义处理请求的相关方法及映射。
关键代码设计及实现:
在 SysUserController 类中添加用于实现密码修改的控制层方法。关键代码如下:

@RequestMapping("doUpdatePassword") 
public JsonResult doUpdatePassword(String pwd,String newPwd, String cfgPwd) {    
	sysUserService.updatePassword(pwd, newPwd, cfgPwd);    
	return new JsonResult("update ok"); 
}

11.2 客户端关键业务设计及实现

业务描述及设计实现
获取修改页面表单中用户填写的数据,然后向服务端发起异步请求进行数据更新。
关键代码设计及实现:
第一步:对保存和取消按钮进行事件注册,关键代码如下:

$(function(){     
	$(".box-footer")
		.on("click",".btn-cancel",doCancel)
		.on("click",".btn-save",doUpdateObject)    
});

第二步:定义保存和取消事件处理函数,关键代码如下:

function doCancel(){     
	$("#mainContentId").html("");    
}
function doUpdateObject(){     
	//1.获取用户提交数据     
	var params={pwd:$("#pwdId").val(),                
	newPwd:$("d").val(),                
	cfgPwd:$("#cfgPwdId")#newPwdI.val()         
	//2.定义请求的 url     
	var url="user/doUpdatePassword"     
	//3.异步提交请求,执行更新操作     
	$.post(url,params,function(result){    
		alert(result.message);     
	})    
}

12 总结

12.1 重难点分析

▪ 一对多数据的保存?(保存用户信息,保存用户和角色的关系数据)
▪ 多对一数据的查询映射?(查询用户时查询用户对应的部门信息)。
▪ 用户密码的加密实现?(对用户密码进行MD5盐值加密)

12.2 FAQ 分析

▪ 用户与角色之间是什么关系?(Many2Many)
▪ 描述一下用户查询业务的实现?
▪ 描述一下用户添加业务的实现?
▪ 描述一下用户更新业务的实现?
▪ resultMap元素应用场景?
1)表中字段名与内存中的映射对象属性不一致(set方法不匹配)
2)表关联查询映射,表嵌套查询映射。

  • 3
    点赞
  • 11
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值