一、注册-持久层
BUBA系统中只可以绑定唯一的超级管理员账号,所以用户输入了 000000 这个激活码的时候,后
端Java项目必须要判断是否可以绑定超级管理员。如果用户表中没有超级管理员记录,则可以绑
定。否则就不能绑定超级管理员。
我们通过SQL语句就能查询出来用户表是否存在超级管理员账号,只需要查询 root字段 值为1的
记录数量就可以了。
1、用户表设计

DROP TABLE IF EXISTS `tb_user`;
CREATE TABLE `tb_user` (
`id` INT(10) UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '主键',
`open_id` VARCHAR(200) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '长期授权字符串',
`nickname` VARCHAR(200) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '昵称',
`photo` VARCHAR(200) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '头像网址',
`name` VARCHAR(20) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '姓名',
`sex` ENUM('男','女') CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '性别',
`tel` CHAR(11) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '手机号码',
`email` VARCHAR(200) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '邮箱',
`hiredate` DATE NULL DEFAULT NULL COMMENT '入职日期',
`role` JSON NOT NULL COMMENT '角色',
`root` TINYINT(1) NOT NULL COMMENT '是否是超级管理员',
`dept_id` INT(10) UNSIGNED NULL DEFAULT NULL COMMENT '部门编号',
`status` TINYINT(4) NOT NULL COMMENT '状态',
`create_time` DATETIME(0) NOT NULL ON UPDATE CURRENT_TIMESTAMP(0) COMMENT '创建时间',
PRIMARY KEY (`id`) USING BTREE,
UNIQUE INDEX `unq_open_id`(`open_id`) USING BTREE,
INDEX `unq_email`(`email`) USING BTREE,
INDEX `idx_dept_id`(`dept_id`) USING BTREE,
INDEX `idx_status`(`status`) USING BTREE
) ENGINE = INNODB AUTO_INCREMENT = 3 CHARACTER SET = utf8 COLLATE = utf8_general_ci COMMENT = '用户表' ROW_FORMAT = DYNAMIC;
2、mapper.xml
//是否已经存在超级管理员用户
<select id="haveRootUser" resultType="boolean">
SELECT IF(COUNT(*),TRUE,FALSE) FROM tb_user WHERE root=1;
</select>
//插入新用户
<select id="adduser">
insert into user .......
</select>
//查询用户的Id用来生成token
<select id="selectOpenIdByUserName">
select id from user where openId = ? and status = 1
</select>
3、UserMapper.java
二、注册-业务层
上面,我们封装了注册用户的持久层代码,下面就应该编写业务层的代码可。比如保存用户
记录之前,我们要获得OpenId才行。获取微信用户的 OpenId ,需要后端程序向微信平台发出请求,并上传若干参数,最终才能得
到。
在 com.buba.wechatmini.service 中创建 UserService.java 接口
public interface UserService {
public int registerUser(String registerCode,String code,String nickname,String photo);
}
在com.buba.wechatmini.service.impl 中创建 UserServiceImpl.java 类
1、UserServiceImpl.java
@Service
@Slf4j
@Scope("prototype")
public class UserServiceImpl implements UserService {
@Value("${wx.app-id}")
private String appId;
@Value("${wx.app-secret}")
private String appSecret;
@Autowired
private UserMapper userMapper;
private String getOpenId(String code) {
String url = "https://api.weixin.qq.com/sns/jscode2session";
HashMap map = new HashMap();
map.put("appid", appId);
map.put("secret", appSecret);
map.put("js_code", code);
map.put("grant_type", "authorization_code");
String response = HttpUtil.post(url, map);
JSONObject json = JSONUtil.parseObj(response);
String openId = json.getStr("openid");
if (openId == null || openId.length() == 0) {
throw new RuntimeException("临时登陆凭证错误");
}
return openId;
}
/**
*写注册新用户的业务代码
*/
@Override
public int registerUser(String registerCode, String code, String nickname, String photo) {
//如果邀请码是000000,代表是超级管理员
if (registerCode.equals("000000")) {
//查询超级管理员帐户是否已经绑定
boolean bool = userDao.haveRootUser();
if (!bool) {
//把当前用户绑定到ROOT帐户
String openId = getOpenId(code);
HashMap param = new HashMap();
param.put("openId", openId);
param.put("nickname", nickname);
param.put("photo", photo);
param.put("role", "[0]");
param.put("status", 1);
param.put("createTime", new Date());
param.put("root", true);
userDao.insert(param);
int id = userDao.searchIdByOpenId(openId);
return id;
} else {
//如果root已经绑定了,就抛出异常
throw new EmosException("无法绑定超级管理员账号");
}
} else{
//TODO 此处还有其他判断内容
}
return 0;
}
}
三、注册-web层
1、请求参数对象 RegisterForm.java
接收移动端提交的注册请求,我们需要用表单类来封装数据,所以创建 RegisterForm.java 类。
import io.swagger.annotations.ApiModel;
import lombok.Data;
import javax.validation.constraints.NotBlank;
import javax.validation.constraints.Pattern;
@Data
@ApiModel
public class RegisterForm {
@NotBlank(message = "注册码不能为空")
@Pattern(regexp = "^[0-9]{6}$",message = "注册码必须是6位数字")
private String registerCode;
@NotBlank(message = "微信临时授权不能为空")
private String code;
@NotBlank(message = "昵称不能为空")
private String nickname;
@NotBlank(message = "头像不能为空")
private String photo;
}
2、UserController.java
处理移动端提交的请求,我们需要Controller类,所以创建 UserController.java 类。
问:业务层采用先定义接口,后声明实现类的做法,为什么Web层不这么做?
答:业务层的需求经常变化,所以应该先声明接口,然后再写实现类。Web层这里变 化并不大,可以直接定义具体类
@RestController
@RequestMapping("/user")
@Api("用户模块Web接口")
public class UserController {
@Autowired
private UserService userService;
@Autowired
private JwtUtil jwtUtil;
@Autowired
private RedisTemplate redisTemplate;
@Value("${WeChatMiNi.jwt.cache-expire}")
private int cacheExpire;
@PostMapping("/register")
@ApiOperation("注册用户")
public R register(@Valid @RequestBody RegisterForm form) {
int id = userService.registerUser(form.getRegisterCode(), form.getCode(),
form.getNickname(), form.getPhoto());
String token = jwtUtil.createToken(id);
Set<String> permsSet = userService.searchUserPermissions(id);
saveCacheToken(token, id);
return R.ok("用户注册成功").put("token", token).put("permission", permsSet);
}
private void saveCacheToken(String token, int userId) {
redisTemplate.opsForValue().set(token, userId + "", cacheExpire, TimeUnit.DAYS);
}
}
四、登录——流程分析
完成了超级管理员注册流程之后,用户表中就已经有了超级管理员记录,那么接下来我们可
以利用这个用户记录来完成小程序的微信登陆功能。
如何判定登陆
用户表中并没有密码字段,我们无法根据username和password来判定用户是否可以登录。因为
用户要拿着微信登陆BUBA小程序,在用户表中只有 openid 、 nickname 和 photo 跟微信账号相
关,我们应该如何判定用户登陆?
我们可以这样设计,用户在BUBA登陆页面点击登陆按钮,然后小程序把 临时授权字符串 提交给
后端Java系统。后端Java系统拿着临时授权字符串换取到 openid ,我们查询用户表中是否存在
这个 openid 。如果存在,意味着该用户是已注册用户,可以登录。如果不存在,说明该用户尚
未注册,目前还不是我们的员工,所以禁止登录。
五、登录——持久层代码
1、 UserMapper.xml
<select id="searchIdByOpenId" parameterType="String" resultType="Integer">
SELECT id FROM tb_user WHERE open_id=#{openId} AND status = 1
</select>
2、 UserMapper.java
public Integer searchIdByOpenId(String openId);
六、登录——业务层代码
1、在 UserService.java 中定义抽象方法
public Integer login(String code);
2、在 UserServiceImpl.java 中实现抽象方法
@Override
public Integer login(String code) {
String openId = getOpenId(code);
Integer id = userDao.searchIdByOpenId(openId);
if (id == null) {
throw new EmosException("帐户不存在");
}
//TODO 从消息队列中接收消息,转移到消息表
return id;
}
七、登陆——Web层
1、创建表单类
创建 LoginForm.java 类,封装客户端提交的数据。
@ApiModel
@Data
public class LoginForm {
@NotBlank(message = "临时授权不能为空")
private String code;
}
2、创建登陆Web方法
在 UserController.java 中创建 login() 方法。
@PostMapping("/login")
@ApiOperation("登陆系统")
public R login(@Valid @RequestBody LoginForm form) {
int id = userService.login(form.getCode());
String token = jwtUtil.createToken(id);
Set<String> permsSet = userService.searchUserPermissions(id);
saveCacheToken(token, id);
return R.ok("登陆成功").put("token", token).put("permission", permsSet);
}
判定用户登陆成功之后,向客户端返回权限列表和Token令牌。
八、普通用户注册(邮件发送邀请码)
维基百科邀请码定义:邀请码是网站为了防止网站恶意注册ID灌水,也可能是因为产品正式上线之前的内测而采取的一种好友邀请好友的注册制度,每个邀请码只能使用一次,邀请码制度类似于CLUB的会员推荐会员制度。
邀请码的特点:
每个邀请码仅限使用一次,它的有效期一般一天至几十天不等,失效后不能再用于注册,必须重新申请。邀请码一般由10位以上的数字和字母随机组成,无法破解。当别人用你的邀请码注册后,系统可能会给你加些论坛分(加贡献值),但当你邀请的人在论坛违规时,你也可能负连带责任。
为何需要:
有些网站的论坛是高手交流的地方,为避免一些对论坛提高整体水平没有帮助的人注册,提高注册会员的质量,在注册时设置邀请码。有些论坛会员人数人数已经够多,所以对注册进行了限制;也有的论坛为了防止恶意注册账号乱发广告,或者防止恶意灌水,同时希望会员可以珍惜自己的账号,故采用邀请注册。有些论坛的服务器承载能力有限,为减少服务器负担,以防支持不住,不得已而为之。比如悠悠鸟影视论坛。一定程度上,实行一段时间的邀请注册和开放注册结合,也是论坛欲擒故纵,吸引人气的策略之一。
1、开启邮件服务
这里以QQ邮箱为例。
首先登录QQ邮箱>>>登录成功后找到设置>>>然后找到邮箱设置>>>点击账户>>>找到POP3|SMTP服务>>>点击开启(开启需要验证,验证成功后会有一串授权码用于发送邮件使用)>>>验证成功
记下QQ邮箱提示的授权码,这个授权码,就是发送邮件时需要的密码。
2、引入依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-mail</artifactId>
</dependency>
3、application.yml配置
在配置文件application.yml文件中写入发送邮件的配置信息
spring:
#邮箱基本配置
mail:
#配置smtp服务主机地址
# qq邮箱为smtp.qq.com 端口号465或587
# sina smtp.sina.cn
# aliyun smtp.aliyun.com
# 163 smtp.163.com 端口号465或994
host: smtp.qq.com
#发送者邮箱
username: 1045906978@qq.com
#配置密码,注意不是真正的密码,而是刚刚申请到的授权码
password: fe456156fefefee
#端口号465或587
port: 587
#默认的邮件编码为UTF-8
default-encoding: UTF-8
#其他参数
properties:
mail:
#配置SSL 加密工厂
smtp:
ssl:
#本地测试,先放开ssl
enable: false
required: false
#开启debug模式,这样邮件发送过程的日志会在控制台打印出来,方便排查错误
debug: true
4、编写发送邮件方法
编写邮件业务类MailService,分三种发送邮件类型:纯文本邮件、html邮件和带附件的邮件。
主要通过MailService工具类就可以满足发送java邮件的需要。当我们进行好 yml 配置后,SpringBoot会帮助我们自动配置 JavaMailSender 我们通过这个java类就可以实现操作java来发送邮件。
import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.mail.javamail.JavaMailSenderImpl;
import org.springframework.mail.javamail.MimeMessageHelper;
import org.springframework.stereotype.Service;
import javax.mail.MessagingException;
import java.util.Date;
/**
* 邮件业务类
*/
@Service
public class MailService {
private static final Logger logger = LoggerFactory.getLogger(MailService.class);
/**
* 注入邮件工具类
*/
@Autowired
private JavaMailSenderImpl javaMailSender;
@Value("${spring.mail.username}")
private String sendMailer;
/**
* 检测邮件信息类
* @param to
* @param subject
* @param text
*/
private void checkMail(String to,String subject,String text){
if(StringUtils.isEmpty(to)){
throw new RuntimeException("邮件收信人不能为空");
}
if(StringUtils.isEmpty(subject)){
throw new RuntimeException("邮件主题不能为空");
}
if(StringUtils.isEmpty(text)){
throw new RuntimeException("邮件内容不能为空");
}
}
/**
* 发送纯文本邮件
* @param to
* @param subject
* @param text
*/
public void sendTextMailMessage(String to,String subject,String text){
try {
//true 代表支持复杂的类型
MimeMessageHelper mimeMessageHelper = new MimeMessageHelper(javaMailSender.createMimeMessage(),true);
//邮件发信人
mimeMessageHelper.setFrom(sendMailer);
//邮件收信人 1或多个
mimeMessageHelper.setTo(to.split(","));
//邮件主题
mimeMessageHelper.setSubject(subject);
//邮件内容
mimeMessageHelper.setText(text);
//邮件发送时间
mimeMessageHelper.setSentDate(new Date());
//发送邮件
javaMailSender.send(mimeMessageHelper.getMimeMessage());
System.out.println("发送邮件成功:"+sendMailer+"->"+to);
} catch (MessagingException e) {
e.printStackTrace();
System.out.println("发送邮件失败:"+e.getMessage());
}
}
}
5、生成邀请码工具类
package com.buba.wechatsk.utils;
import java.util.Random;
/**
* 邀请码生成器,算法原理:
* 1) 获取时间戳
* 2) 使用自定义进制转为:gpm6
* 3) 转为字符串,并在后面加'O'字符:gpm6o
* 4)在后面随机产生若干个随机数字字符:gpm6o7
* 转为自定义进制后就不会出现o这个字符,然后在后面加个'o',这样就能确定唯一性。最后在后面产生一些随机字符进行补全。
*/
public class ShareCodeUtil {
/** 自定义进制(0,1没有加入,容易与o,l混淆) */
private static final char[] r=new char[]{'Q', 'W', 'E', '8', 'A', 'S', '2', 'D', 'Z', 'X', '9', 'C', '7', 'P', '5', 'K', '3', 'M', 'J', 'U', 'F', 'R', '4', 'V', 'Y', 'T', 'N', '6', 'B', 'G', 'H'};
/** (不能与自定义进制有重复) */
private static final char b='I';
/** 进制长度 */
private static final int binLen=r.length;
/** 序列最小长度 */
private static final int s=6;
/**
* 根据ID生成六位随机码
* @param id ID
* @return 随机码
*/
public static String toSerialCode(long id) {
char[] buf=new char[32];
int charPos=32;
while((id / binLen) > 0) {
int ind=(int)(id % binLen);
buf[--charPos]=r[ind];
id /= binLen;
}
buf[--charPos]=r[(int)(id % binLen)];
String str=new String(buf, charPos, (32 - charPos));
// 不够长度的自动随机补全
if(str.length() < s) {
StringBuilder sb=new StringBuilder();
sb.append(b);
Random rnd=new Random();
for(int i=1; i < s - str.length(); i++) {
sb.append(r[rnd.nextInt(binLen)]);
}
str+=sb.toString();
}
return str;
}
public static long codeToId(String code) {
char chs[]=code.toCharArray();
long res=0L;
for(int i=0; i < chs.length; i++) {
int ind=0;
for(int j=0; j < binLen; j++) {
if(chs[i] == r[j]) {
ind=j;
break;
}
}
if(chs[i] == b) {
break;
}
if(i > 0) {
res=res * binLen + ind;
} else {
res=ind;
}
}
return res;
}
}
6、测试邀请码接口
@ApiOperation("生成邀请码")
@GetMapping("/getcode")
public Result getcode(){
// 生成邀请码
String code = ShareCodeUtil.toSerialCode(1);
// 将邀请码保存到redis有效期1分钟
redisTemplate.opsForValue().set(code, code,1,TimeUnit.MINUTES);
// 返回邀请码
return Result.ok().put("邀请码",code);
}
@ApiOperation("验证邀请码是否生效")
@GetMapping("/checkCode")
public Result checkCode(String code){
String recode = (String) redisTemplate.opsForValue().get(code);
return Result.ok(recode);
}
7、发送邀请码到用户邮箱
@ApiOperation("发送邮件")
@GetMapping("/sendMail")
public Result sendMail(String toMail){
// 生成邀请码
String code = ShareCodeUtil.toSerialCode(1);
mailUtils.sendTextMailMessage(toMail,"邀请码",code);
// 将邀请码保存到redis有效期1分钟
redisTemplate.opsForValue().set(code, code,1,TimeUnit.MINUTES);
return Result.ok();
}