登录实现 和 添加配说明文档
This commit is contained in:
@@ -0,0 +1,38 @@
|
||||
package org.jiayunet.controller;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import org.jiayunet.pojo.dto.SmsLoginDto;
|
||||
import org.jiayunet.pojo.vo.LoginVo;
|
||||
import org.jiayunet.server.LoginServer;
|
||||
import org.springframework.validation.annotation.Validated;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import javax.servlet.http.HttpServletResponse;
|
||||
import javax.validation.constraints.Pattern;
|
||||
|
||||
/**
|
||||
* 登录控制类
|
||||
*
|
||||
* @author zk
|
||||
*/
|
||||
@RestController
|
||||
@RequestMapping("/public")
|
||||
@AllArgsConstructor
|
||||
@Validated
|
||||
public class LoginController {
|
||||
|
||||
private LoginServer loginServer;
|
||||
|
||||
@PostMapping("/sms/sendCode")
|
||||
public boolean sendCode(@RequestParam @Pattern(regexp = "^1[3-9]\\d{9}$", message = "手机号格式不正确") String mobileNumber) {
|
||||
return loginServer.sendCode(mobileNumber);
|
||||
}
|
||||
|
||||
@PostMapping("/login/smsLogin")
|
||||
public LoginVo smsLogin(@Validated @RequestBody SmsLoginDto dto,
|
||||
HttpServletRequest request,
|
||||
HttpServletResponse response) {
|
||||
return loginServer.smsLogin(dto.getMobileNumber(), dto.getCode(), request, response);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
package org.jiayunet.pojo.dto;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
import javax.validation.constraints.NotBlank;
|
||||
|
||||
/**
|
||||
* 短信验证码登录入参
|
||||
*
|
||||
* @author zk
|
||||
*/
|
||||
@Data
|
||||
public class SmsLoginDto {
|
||||
|
||||
@NotBlank(message = "手机号不能为空")
|
||||
private String mobileNumber;
|
||||
|
||||
@NotBlank(message = "验证码不能为空")
|
||||
private String code;
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
package org.jiayunet.pojo.vo;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
/**
|
||||
* 登录返回
|
||||
*
|
||||
* @author zk
|
||||
*/
|
||||
@Data
|
||||
@AllArgsConstructor
|
||||
@NoArgsConstructor
|
||||
public class LoginVo {
|
||||
|
||||
private Long userId;
|
||||
|
||||
private String nick;
|
||||
}
|
||||
@@ -0,0 +1,164 @@
|
||||
package org.jiayunet.server;
|
||||
|
||||
import com.auth0.jwt.JWT;
|
||||
import com.auth0.jwt.algorithms.Algorithm;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.jiayunet.constant.PreRedisKeyName;
|
||||
import org.jiayunet.constant.SmsTemplateEnum;
|
||||
import org.jiayunet.constant.sms.UniversalSmsVariable;
|
||||
import org.jiayunet.exception.BusinessException;
|
||||
import org.jiayunet.exception.BusinessExpCodeEnum;
|
||||
import org.jiayunet.mapper.UserMapper;
|
||||
import org.jiayunet.pojo.login.RedisLoginTokenInfo;
|
||||
import org.jiayunet.pojo.po.User;
|
||||
import org.jiayunet.pojo.vo.LoginVo;
|
||||
import org.jiayunet.tool.HttpIpTool;
|
||||
import org.jiayunet.tool.server.RedisServerTool;
|
||||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
import org.springframework.util.Assert;
|
||||
|
||||
import javax.servlet.http.Cookie;
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import javax.servlet.http.HttpServletResponse;
|
||||
import java.time.Instant;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Comparator;
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* 登录服务
|
||||
*
|
||||
* @author zk
|
||||
*/
|
||||
@Service
|
||||
@Slf4j
|
||||
public class LoginServer {
|
||||
|
||||
@Value("${app.secret.token:youweiqingnian123}")
|
||||
private String secret;
|
||||
|
||||
@Value("${app.login.token.exceed_time:43200}")
|
||||
private int tokenExceedTime;
|
||||
|
||||
@Value("${app.login.device_online_quantity:5}")
|
||||
private int deviceOnlineQuantity;
|
||||
|
||||
@Autowired
|
||||
private SmsServer smsServer;
|
||||
|
||||
@Autowired
|
||||
private UserMapper userMapper;
|
||||
|
||||
@Autowired
|
||||
private RedisServerTool redisServerTool;
|
||||
|
||||
/**
|
||||
* 发送短信验证码
|
||||
*/
|
||||
public boolean sendCode(String mobileNumber) {
|
||||
Assert.hasText(mobileNumber, "手机号不能为空");
|
||||
|
||||
UniversalSmsVariable variable = new UniversalSmsVariable();
|
||||
String code = String.valueOf((int) ((Math.random() * 9 + 1) * 100000));
|
||||
variable.setCode(code);
|
||||
|
||||
return smsServer.send(mobileNumber, SmsTemplateEnum.UNIVERSAL, variable);
|
||||
}
|
||||
|
||||
/**
|
||||
* 短信验证码登录
|
||||
*/
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public LoginVo smsLogin(String mobileNumber, String code, HttpServletRequest request, HttpServletResponse response) {
|
||||
Assert.hasText(mobileNumber, "手机号不能为空");
|
||||
Assert.hasText(code, "验证码不能为空");
|
||||
|
||||
// 校验验证码
|
||||
boolean verified = smsServer.verify(mobileNumber, SmsTemplateEnum.UNIVERSAL, code);
|
||||
if (!verified) {
|
||||
throw new BusinessException(BusinessExpCodeEnum.UNKNOWN_ERROR, "验证码错误或已过期");
|
||||
}
|
||||
|
||||
// 查询用户
|
||||
User user = userMapper.selectOne(new LambdaQueryWrapper<User>().eq(User::getMobileNumber, mobileNumber));
|
||||
|
||||
// 用户不存在则自动注册
|
||||
if (user == null) {
|
||||
user = new User();
|
||||
user.setMobileNumber(mobileNumber);
|
||||
user.setNick("用户" + mobileNumber.substring(mobileNumber.length() - 4));
|
||||
user.setStatus(0);
|
||||
user.setIsDelete(0L);
|
||||
user.setCreateTime(Instant.now());
|
||||
userMapper.insert(user);
|
||||
}
|
||||
|
||||
// 检查用户状态
|
||||
if (user.getStatus() != null && user.getStatus() == 1) {
|
||||
throw new BusinessException(BusinessExpCodeEnum.UNKNOWN_ERROR, "账号已被禁用");
|
||||
}
|
||||
|
||||
// 生成JWT
|
||||
String uuId = UUID.randomUUID().toString().replace("-", "");
|
||||
Algorithm algorithm = Algorithm.HMAC256(secret);
|
||||
String token = JWT.create()
|
||||
.withClaim("userId", user.getId())
|
||||
.withClaim("uuId", uuId)
|
||||
.sign(algorithm);
|
||||
|
||||
// 构建Redis登录信息
|
||||
String redisKey = PreRedisKeyName.LOGIN_TOKEN + user.getId();
|
||||
RedisLoginTokenInfo info = redisServerTool.get(redisKey, RedisLoginTokenInfo.class);
|
||||
if (info == null) {
|
||||
info = new RedisLoginTokenInfo();
|
||||
info.setUserId(user.getId());
|
||||
info.setAuthority(new ArrayList<>());
|
||||
info.setRole(new ArrayList<>());
|
||||
}
|
||||
|
||||
// 过滤过期设备
|
||||
List<RedisLoginTokenInfo.LoginDevice> devices = info.getLoginDevices();
|
||||
if (devices == null) {
|
||||
devices = new ArrayList<>();
|
||||
}
|
||||
long expireMillis = System.currentTimeMillis() - tokenExceedTime * 1000L;
|
||||
devices = devices.stream()
|
||||
.filter(d -> d.getLastLoginTime().isAfter(Instant.ofEpochMilli(expireMillis)))
|
||||
.collect(Collectors.toList());
|
||||
|
||||
// 超过设备上限,移除最早的
|
||||
while (devices.size() >= deviceOnlineQuantity) {
|
||||
devices.stream().min(Comparator.comparing(RedisLoginTokenInfo.LoginDevice::getLastLoginTime))
|
||||
.ifPresent(devices::remove);
|
||||
}
|
||||
|
||||
// 添加当前设备
|
||||
RedisLoginTokenInfo.LoginDevice device = new RedisLoginTokenInfo.LoginDevice();
|
||||
device.setUuId(uuId);
|
||||
device.setLastLoginTime(Instant.now());
|
||||
device.setLoginIp(HttpIpTool.gteRealIP(request));
|
||||
devices.add(device);
|
||||
|
||||
info.setLoginDevices(devices);
|
||||
redisServerTool.set(redisKey, info, tokenExceedTime, TimeUnit.SECONDS);
|
||||
|
||||
// 清除验证码
|
||||
smsServer.clear(mobileNumber, SmsTemplateEnum.UNIVERSAL);
|
||||
|
||||
// 设置Cookie
|
||||
Cookie cookie = new Cookie("Token", token);
|
||||
cookie.setHttpOnly(true);
|
||||
cookie.setPath("/");
|
||||
cookie.setMaxAge(tokenExceedTime);
|
||||
response.addCookie(cookie);
|
||||
|
||||
return new LoginVo(user.getId(), user.getNick());
|
||||
}
|
||||
}
|
||||
@@ -17,8 +17,8 @@ spring:
|
||||
# 电子邮箱
|
||||
email:
|
||||
status: close
|
||||
account: ${EMAIL_ACCOUNT:sim18502043706@163.com}
|
||||
authorization: ${EMAIL_AUTHORIZATION:CHBCVPYGFZCSUNCP}
|
||||
account: ${EMAIL_ACCOUNT:xxx@163.com}
|
||||
authorization: ${EMAIL_AUTHORIZATION:123456}
|
||||
|
||||
# 微信支付
|
||||
wx_pay:
|
||||
@@ -44,20 +44,20 @@ app:
|
||||
token:
|
||||
exceed_time: 129600
|
||||
#设备在线数量
|
||||
device_online_quantity: 10
|
||||
device_online_quantity: 2
|
||||
|
||||
# 短信
|
||||
sms:
|
||||
service_provider: aliyun
|
||||
aliyun:
|
||||
access_key_id: LTAI5tGRFuJmt6nrxRqZYC7z
|
||||
access_key_secret: xLC3kNSmCvEtsc9j4O8g3rOs70QjZQ
|
||||
access_key_id: LTAI5tJBVUUJhB7yp14UDzVf
|
||||
access_key_secret: Opf0iO5FKNrdwI63DPhXazW7utAGTj
|
||||
|
||||
oss:
|
||||
service_provider: aliyun
|
||||
aliyun:
|
||||
access_key_id: LTAI5tGRFuJmt6nrxRqZYC7z
|
||||
access_key_secret: xLC3kNSmCvEtsc9j4O8g3rOs70QjZQ
|
||||
access_key_id: LTAI5tEdLKKQUKhTyUpfH5Mk
|
||||
access_key_secret: RjUdTrq0V5qA4b3BUElNhXqs3ZLp5k
|
||||
|
||||
# 防刷配置 20秒 20次
|
||||
prevent_replay:
|
||||
|
||||
@@ -15,7 +15,7 @@ public enum SmsTemplateEnum {
|
||||
/**
|
||||
* 通用验证码
|
||||
*/
|
||||
UNIVERSAL("加鱼宠物", "SMS_480245067", 5, UniversalSmsVariable.class);
|
||||
UNIVERSAL("油梨科技", "SMS_501981064", 5, UniversalSmsVariable.class);
|
||||
|
||||
/**
|
||||
* 模板签名
|
||||
|
||||
@@ -23,55 +23,67 @@ public class User {
|
||||
* 手机号
|
||||
*/
|
||||
private String mobileNumber;
|
||||
|
||||
/**
|
||||
* 邮箱
|
||||
*/
|
||||
private String email;
|
||||
|
||||
/**
|
||||
* 登陆密码
|
||||
*/
|
||||
private String loginPassword;
|
||||
|
||||
/**
|
||||
* 昵称
|
||||
*/
|
||||
private String nick;
|
||||
|
||||
/**
|
||||
* 真实名字
|
||||
*/
|
||||
private String realName;
|
||||
|
||||
/**
|
||||
* 头像
|
||||
*/
|
||||
private String picture;
|
||||
|
||||
/**
|
||||
* 生日
|
||||
*/
|
||||
private Instant birthday;
|
||||
|
||||
/**
|
||||
* 性别 1男 2女
|
||||
*/
|
||||
private Integer sex;
|
||||
|
||||
/**
|
||||
* 微信头像
|
||||
*/
|
||||
private String wechatAvatar;
|
||||
|
||||
/**
|
||||
* 学员的微信昵称
|
||||
* 微信昵称
|
||||
*/
|
||||
private String wechatNick;
|
||||
|
||||
/**
|
||||
* 微信openid
|
||||
*/
|
||||
private String wechatOpenid;
|
||||
|
||||
/**
|
||||
* 微信UnionId
|
||||
*/
|
||||
private String wechatUnionId;
|
||||
|
||||
/**
|
||||
* 状态 0 正常 1 禁用
|
||||
*/
|
||||
private Integer status;
|
||||
|
||||
|
||||
/**
|
||||
* 添加时间
|
||||
*/
|
||||
|
||||
@@ -0,0 +1,81 @@
|
||||
# OfferPie Back‑End 项目结构说明
|
||||
|
||||
## 1️⃣ 项目整体层次
|
||||
```
|
||||
offerpie/back-end
|
||||
│
|
||||
├─ pom.xml # 父 Maven 项目,统一管理依赖、插件、属性
|
||||
│
|
||||
├─ client‑api/ # **C 端**(面向用户)API 服务,包含业务控制器(如 LoginController)、DTO/VO(如 LoginVo、SmsLoginDto)以及服务器实现(LoginServer 等)
|
||||
│ ├─ pom.xml
|
||||
│ └─ src/main/java
|
||||
│ └─ org.jiayunet
|
||||
│ └─ ClientApplication.java # Spring Boot 主入口
|
||||
│
|
||||
├─ common/ # **共享层**:被 C 端和 B 端共同使用的代码库
|
||||
│ ├─ pom.xml
|
||||
│ └─ src/main/java
|
||||
│ └─ org.jiayunet
|
||||
│ ├─ config/ # OSS、Redis、Security、WxPay、Sms 等统一配置
|
||||
│ ├─ tool/ # Http、IP、Redis、认证、验证码等工具类
|
||||
│ ├─ interceptor/ # 全局拦截器(日志、TraceId、黑名单、SQL 统计等)
|
||||
│ ├─ aop/ # AOP 日志切面
|
||||
│ ├─ exception/ # 业务异常统一处理
|
||||
│ ├─ email/ # 邮件发送抽象(EmailAbility)
|
||||
│ ├─ wxPay/ # 微信支付相关能力(Js、Native、Transfer 等)
|
||||
│ ├─ pojo/ # 公共 POJO(统一响应、登录/防重放 token 等)
|
||||
│ └─ web/ # Spring MVC 全局响应体 advice
|
||||
│
|
||||
└─ manager/ # **B 端 + C 端共享** 的业务实现(尚未搭建完整的 B 端 UI)
|
||||
├─ pom.xml
|
||||
└─ src/main/java
|
||||
└─ org.jiayunet
|
||||
├─ controller/ # 对外 REST 接口(HealthCheck、Oss 等)
|
||||
├─ mapper/ # MyBatis Mapper(UserMapper、OssFileMapper)
|
||||
├─ pojo/
|
||||
│ ├─ po/ # 持久化实体(User、OssFile 等)
|
||||
│ └─ vo/ # ViewObject(OssUrlVo 等)
|
||||
└─ … (业务 Service、Utils 等)
|
||||
```
|
||||
> **设计理念** – `manager` 模块把 **C 端** 与 **B 端** 共用的代码(如实体、Mapper、统一响应、拦截器、配置)集中放在 `common`,从而避免在两个子项目之间出现重复实现。`client‑api` 只负责面向用户的 API,`manager` 提供后台管理相关的功能;两者均通过 `common` 中的工具、配置、统一异常处理等实现一致的技术栈。
|
||||
|
||||
## 2️⃣ 各层模块职责
|
||||
| 层级 | 主要职责 | 关键类/包 |
|
||||
|------|----------|-----------|
|
||||
| **client‑api** | - 面向终端用户的 REST API <br> - 启动 Spring Boot 应用 | `ClientApplication`、`controller/*`、`application.yml` |
|
||||
| **common** | - **统一配置**:OSS、Redis、Security、WxPay、Sms 等 <br> - **跨层工具**:HTTP、IP、认证、验证码、Redis Server 等 <br> - **全局拦截/切面**:日志、TraceId、黑名单、SQL 打印 <br> - **统一异常/响应**:`GlobalExceptionAdvice`、`UnifiedResponse` <br> - **业务抽象**:邮件发送、微信支付(Native/JS/Transfer) <br> - **公共 POJO**:登录令牌、防重放信息等 | `config/`, `tool/`, `interceptor/`, `aop/`, `exception/`, `email/`, `wxPay/`, `pojo/` |
|
||||
| **manager** | - **业务实体**(`User`、`OssFile` 等) <br> - **MyBatis Mapper**(`UserMapper`、`OssFileMapper`) <br> - **业务 API**:文件上传/下载、健康检查等 <br> - **业务逻辑**:服务层、工具类等 <br> - **既供 B 端 UI(待实现)使用,也供 C 端 业务直接调用** | `controller/`, `mapper/`, `pojo/po/`, `pojo/vo/` |
|
||||
|
||||
## 3️⃣ 关键业务实体(示例)
|
||||
| 实体 | 所属路径 | 作用概述 |
|
||||
|------|----------|----------|
|
||||
| `User` | `manager/src/main/java/.../pojo/po/User.java` | 记录用户基础信息(手机号、邮箱、密码、昵称、微信绑定等),配合 `UserMapper` 完成持久化。 |
|
||||
| `OssFile` | `manager/src/main/java/.../pojo/po/OssFile.java` | 描述 OSS(对象存储)中文件的元数据(路径、大小、标签等),通过 `OssFileMapper` 进行增删改查。 |
|
||||
| `OssUrlVo` | `manager/src/main/java/.../pojo/vo/OssUrlVo.java` | 对外返回的 OSS 访问 URL 结构体,供前端直接使用。 |
|
||||
> 这些实体位于 **manager**,因为它们属于业务模型,`manager` 负责提供完整的业务实现;同时它们可以被 **C 端**(如 `client‑api`)直接调用,体现了 B 端 + C 端共享的设计初衷。
|
||||
|
||||
## 4️⃣ 共享技术栈(位于 `common`)
|
||||
| 类别 | 关键实现 | 位置 |
|
||||
|------|----------|------|
|
||||
| **配置** | `OssConfig`, `RedissonConf`, `SecurityConfig`, `WxPayConfig`, `SmsConfig` | `common/config` |
|
||||
| **安全** | JWT 过滤器、登录令牌 (`RedisLoginTokenInfo`)、防重放 (`RedisPreventReplayInfo`) | `common/interceptor`、`common/pojo/interceptor` |
|
||||
| **邮件** | `EmailAbility`(封装邮件发送) | `common/email` |
|
||||
| **微信支付** | `WxJsPayAbility`, `WxNativePayAbility`, `WxTransferPayAbility`, `WxPayNotifyController` | `common/wxPay` |
|
||||
| **全局异常** | `GlobalExceptionAdvice`, `BusinessException`, `BusinessExpCodeEnum` | `common/exception` |
|
||||
| **日志 & AOP** | `ControllerLogAspect`, `LoggingOriginalRequestFilter`, `SqlLoggerInterceptor` | `common/aop`, `common/interceptor` |
|
||||
| **工具类** | `HttpTool`, `HttpIpTool`, `AuthenticTool`, `ObjectTool`, `VerifyImageCodeUtils` | `common/tool` |
|
||||
| **统一返回体** | `UnifiedResponse`, `UnifiedResponseBodyAdvice` | `common/pojo`, `common/web` |
|
||||
| **批量/更新** | `UpdateBatchMethod`(批量更新策略) | `common/config` |
|
||||
这些设施在 **C 端** 与 **B 端** 中统一使用,确保技术栈、日志、异常、配置保持一致。
|
||||
|
||||
## 5️⃣ 构建与运行
|
||||
- **父 POM**(`back-end/pom.xml`)统一管理子模块的依赖与插件。
|
||||
- 子模块 (`client‑api`, `common`, `manager`) 均可单独 `mvn clean install`,生成各自的 jar 包。
|
||||
- **启动入口**:运行 `client‑api` 中的 `ClientApplication`,Spring Boot 会自动扫描并加载 `common`(配置、拦截、工具)以及 `manager` 中声明的 Mapper 与 Service。
|
||||
|
||||
## 6️⃣ 小结
|
||||
- 项目采用 **三层结构**:
|
||||
1. **client‑api** → 只负责 C 端 REST 接口。
|
||||
2. **manager** → 包含业务实体、Mapper 与业务 API,既供 B 端(后台)使用,也供 C 端直接调用;因此是 **B 端 + C 端共享层**。
|
||||
3. **common** → 所有层共同依赖的底层设施(配置、工具、拦截、异常、支付、邮件等),实现“一次实现、全局共享”。
|
||||
- 这种布局把 **业务实现** 与 **技术支撑** 明确分离,后续在 C 端或 B 端新增功能时,只需在 `manager` 中编写业务代码,`common` 负责统一的技术支撑,避免代码重复、提高维护效率。
|
||||
Reference in New Issue
Block a user