feat: 新增H5会员管理接口模块
需求文档: - 新增H5会员接口需求与技术方案文档 认证模块(/h5/auth): - 图形验证码获取 - 阿里云短信验证码发送 - 密码登录 - 短信验证码登录 - 会员注册 - Token刷新 - 退出登录 会员模块(/h5/member): - 获取会员信息 - 修改会员信息(昵称/性别/生日) - 修改密码 - 教育身份管理(教师) - 学生绑定管理(家长/教师均可) 基础数据模块(/h5/base): - 区域树查询 - 学校列表查询 - 年级列表查询 - 班级列表查询 - 学科列表查询 安全配置: - 放行H5公开接口(/h5/auth/**、/h5/base/**)
This commit is contained in:
parent
e8c4f3f568
commit
905e263ca8
|
|
@ -116,6 +116,9 @@ security:
|
|||
- /*/api-docs
|
||||
- /*/api-docs/**
|
||||
- /warm-flow-ui/config
|
||||
# H5公开接口
|
||||
- /h5/auth/**
|
||||
- /h5/base/**
|
||||
|
||||
# 多租户配置
|
||||
tenant:
|
||||
|
|
|
|||
|
|
@ -0,0 +1,88 @@
|
|||
package org.dromara.pangu.h5.controller;
|
||||
|
||||
import cn.dev33.satoken.annotation.SaIgnore;
|
||||
import jakarta.validation.Valid;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.dromara.common.core.domain.R;
|
||||
import org.dromara.pangu.h5.domain.dto.H5PasswordLoginDto;
|
||||
import org.dromara.pangu.h5.domain.dto.H5RegisterDto;
|
||||
import org.dromara.pangu.h5.domain.dto.H5SmsLoginDto;
|
||||
import org.dromara.pangu.h5.domain.dto.H5SmsSendDto;
|
||||
import org.dromara.pangu.h5.domain.vo.H5CaptchaVo;
|
||||
import org.dromara.pangu.h5.domain.vo.H5LoginVo;
|
||||
import org.dromara.pangu.h5.service.H5AuthService;
|
||||
import org.springframework.validation.annotation.Validated;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
/**
|
||||
* H5认证接口
|
||||
*
|
||||
* @author 湖北新华业务中台研发团队
|
||||
*/
|
||||
@SaIgnore
|
||||
@Validated
|
||||
@RestController
|
||||
@RequestMapping("/h5/auth")
|
||||
@RequiredArgsConstructor
|
||||
public class H5AuthController {
|
||||
|
||||
private final H5AuthService authService;
|
||||
|
||||
/**
|
||||
* 获取图形验证码
|
||||
*/
|
||||
@GetMapping("/captcha")
|
||||
public R<H5CaptchaVo> getCaptcha() {
|
||||
return R.ok(authService.getCaptcha());
|
||||
}
|
||||
|
||||
/**
|
||||
* 发送短信验证码
|
||||
*/
|
||||
@PostMapping("/sms/send")
|
||||
public R<Void> sendSmsCode(@Valid @RequestBody H5SmsSendDto dto) {
|
||||
authService.sendSmsCode(dto);
|
||||
return R.ok();
|
||||
}
|
||||
|
||||
/**
|
||||
* 密码登录
|
||||
*/
|
||||
@PostMapping("/login/password")
|
||||
public R<H5LoginVo> loginByPassword(@Valid @RequestBody H5PasswordLoginDto dto) {
|
||||
return R.ok(authService.loginByPassword(dto));
|
||||
}
|
||||
|
||||
/**
|
||||
* 短信验证码登录
|
||||
*/
|
||||
@PostMapping("/login/sms")
|
||||
public R<H5LoginVo> loginBySms(@Valid @RequestBody H5SmsLoginDto dto) {
|
||||
return R.ok(authService.loginBySms(dto));
|
||||
}
|
||||
|
||||
/**
|
||||
* 注册
|
||||
*/
|
||||
@PostMapping("/register")
|
||||
public R<H5LoginVo> register(@Valid @RequestBody H5RegisterDto dto) {
|
||||
return R.ok(authService.register(dto));
|
||||
}
|
||||
|
||||
/**
|
||||
* 刷新Token
|
||||
*/
|
||||
@PostMapping("/refresh")
|
||||
public R<H5LoginVo> refreshToken(@RequestParam String refreshToken) {
|
||||
return R.ok(authService.refreshToken(refreshToken));
|
||||
}
|
||||
|
||||
/**
|
||||
* 退出登录
|
||||
*/
|
||||
@PostMapping("/logout")
|
||||
public R<Void> logout() {
|
||||
authService.logout();
|
||||
return R.ok();
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,200 @@
|
|||
package org.dromara.pangu.h5.controller;
|
||||
|
||||
import cn.dev33.satoken.annotation.SaIgnore;
|
||||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.dromara.common.core.domain.R;
|
||||
import org.dromara.pangu.base.domain.PgRegion;
|
||||
import org.dromara.pangu.base.domain.PgSubject;
|
||||
import org.dromara.pangu.base.mapper.PgRegionMapper;
|
||||
import org.dromara.pangu.base.mapper.PgSubjectMapper;
|
||||
import org.dromara.pangu.base.domain.PgClass;
|
||||
import org.dromara.pangu.base.domain.PgGrade;
|
||||
import org.dromara.pangu.base.mapper.PgClassMapper;
|
||||
import org.dromara.pangu.base.mapper.PgGradeMapper;
|
||||
import org.dromara.pangu.school.domain.PgSchool;
|
||||
import org.dromara.pangu.school.domain.PgSchoolClass;
|
||||
import org.dromara.pangu.school.domain.PgSchoolGrade;
|
||||
import org.dromara.pangu.school.mapper.PgSchoolClassMapper;
|
||||
import org.dromara.pangu.school.mapper.PgSchoolGradeMapper;
|
||||
import org.dromara.pangu.school.mapper.PgSchoolMapper;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RequestParam;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
import java.util.*;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* H5基础数据接口
|
||||
*
|
||||
* @author 湖北新华业务中台研发团队
|
||||
*/
|
||||
@SaIgnore
|
||||
@RestController
|
||||
@RequestMapping("/h5/base")
|
||||
@RequiredArgsConstructor
|
||||
public class H5BaseDataController {
|
||||
|
||||
private final PgRegionMapper regionMapper;
|
||||
private final PgSchoolMapper schoolMapper;
|
||||
private final PgSchoolGradeMapper schoolGradeMapper;
|
||||
private final PgSchoolClassMapper schoolClassMapper;
|
||||
private final PgGradeMapper gradeMapper;
|
||||
private final PgClassMapper classMapper;
|
||||
private final PgSubjectMapper subjectMapper;
|
||||
|
||||
/**
|
||||
* 获取区域树
|
||||
*/
|
||||
@GetMapping("/regions")
|
||||
public R<List<Map<String, Object>>> getRegions() {
|
||||
List<PgRegion> regions = regionMapper.selectList(
|
||||
new LambdaQueryWrapper<PgRegion>()
|
||||
.eq(PgRegion::getStatus, "0")
|
||||
.orderByAsc(PgRegion::getOrderNum)
|
||||
);
|
||||
|
||||
// 构建树形结构
|
||||
List<Map<String, Object>> tree = buildRegionTree(regions, 0L);
|
||||
return R.ok(tree);
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据区域获取学校列表
|
||||
*/
|
||||
@GetMapping("/schools")
|
||||
public R<List<Map<String, Object>>> getSchools(@RequestParam Long regionId) {
|
||||
List<PgSchool> schools = schoolMapper.selectList(
|
||||
new LambdaQueryWrapper<PgSchool>()
|
||||
.eq(PgSchool::getRegionId, regionId)
|
||||
.eq(PgSchool::getStatus, "0")
|
||||
.eq(PgSchool::getDelFlag, "0")
|
||||
);
|
||||
|
||||
List<Map<String, Object>> result = schools.stream().map(school -> {
|
||||
Map<String, Object> map = new HashMap<>();
|
||||
map.put("schoolId", school.getSchoolId());
|
||||
map.put("schoolName", school.getSchoolName());
|
||||
map.put("schoolCode", school.getSchoolCode());
|
||||
return map;
|
||||
}).collect(Collectors.toList());
|
||||
|
||||
return R.ok(result);
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据学校获取年级列表
|
||||
*/
|
||||
@GetMapping("/grades")
|
||||
public R<List<Map<String, Object>>> getGrades(@RequestParam Long schoolId) {
|
||||
List<PgSchoolGrade> schoolGrades = schoolGradeMapper.selectList(
|
||||
new LambdaQueryWrapper<PgSchoolGrade>()
|
||||
.eq(PgSchoolGrade::getSchoolId, schoolId)
|
||||
);
|
||||
|
||||
// 获取基础年级信息
|
||||
Set<Long> gradeIds = schoolGrades.stream()
|
||||
.map(PgSchoolGrade::getGradeId)
|
||||
.filter(Objects::nonNull)
|
||||
.collect(Collectors.toSet());
|
||||
|
||||
Map<Long, String> gradeNameMap = new HashMap<>();
|
||||
if (!gradeIds.isEmpty()) {
|
||||
List<PgGrade> grades = gradeMapper.selectByIds(gradeIds);
|
||||
gradeNameMap = grades.stream()
|
||||
.collect(Collectors.toMap(PgGrade::getGradeId, PgGrade::getGradeName));
|
||||
}
|
||||
|
||||
Map<Long, String> finalGradeNameMap = gradeNameMap;
|
||||
List<Map<String, Object>> result = schoolGrades.stream().map(sg -> {
|
||||
Map<String, Object> map = new HashMap<>();
|
||||
map.put("schoolGradeId", sg.getId());
|
||||
map.put("gradeId", sg.getGradeId());
|
||||
map.put("gradeName", finalGradeNameMap.getOrDefault(sg.getGradeId(), ""));
|
||||
return map;
|
||||
}).collect(Collectors.toList());
|
||||
|
||||
return R.ok(result);
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据年级获取班级列表
|
||||
*/
|
||||
@GetMapping("/classes")
|
||||
public R<List<Map<String, Object>>> getClasses(@RequestParam Long schoolGradeId) {
|
||||
List<PgSchoolClass> schoolClasses = schoolClassMapper.selectList(
|
||||
new LambdaQueryWrapper<PgSchoolClass>()
|
||||
.eq(PgSchoolClass::getSchoolGradeId, schoolGradeId)
|
||||
);
|
||||
|
||||
// 获取基础班级信息
|
||||
Set<Long> classIds = schoolClasses.stream()
|
||||
.map(PgSchoolClass::getClassId)
|
||||
.filter(Objects::nonNull)
|
||||
.collect(Collectors.toSet());
|
||||
|
||||
Map<Long, String> classNameMap = new HashMap<>();
|
||||
if (!classIds.isEmpty()) {
|
||||
List<PgClass> classes = classMapper.selectByIds(classIds);
|
||||
classNameMap = classes.stream()
|
||||
.collect(Collectors.toMap(PgClass::getClassId, PgClass::getClassName));
|
||||
}
|
||||
|
||||
Map<Long, String> finalClassNameMap = classNameMap;
|
||||
List<Map<String, Object>> result = schoolClasses.stream().map(sc -> {
|
||||
Map<String, Object> map = new HashMap<>();
|
||||
map.put("schoolClassId", sc.getId());
|
||||
map.put("classId", sc.getClassId());
|
||||
map.put("className", finalClassNameMap.getOrDefault(sc.getClassId(), ""));
|
||||
return map;
|
||||
}).collect(Collectors.toList());
|
||||
|
||||
return R.ok(result);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取学科列表
|
||||
*/
|
||||
@GetMapping("/subjects")
|
||||
public R<List<Map<String, Object>>> getSubjects() {
|
||||
List<PgSubject> subjects = subjectMapper.selectList(
|
||||
new LambdaQueryWrapper<PgSubject>()
|
||||
.eq(PgSubject::getStatus, "0")
|
||||
.orderByAsc(PgSubject::getOrderNum)
|
||||
);
|
||||
|
||||
List<Map<String, Object>> result = subjects.stream().map(subject -> {
|
||||
Map<String, Object> map = new HashMap<>();
|
||||
map.put("subjectId", subject.getSubjectId());
|
||||
map.put("subjectName", subject.getSubjectName());
|
||||
map.put("subjectCode", subject.getSubjectCode());
|
||||
return map;
|
||||
}).collect(Collectors.toList());
|
||||
|
||||
return R.ok(result);
|
||||
}
|
||||
|
||||
/**
|
||||
* 构建区域树
|
||||
*/
|
||||
private List<Map<String, Object>> buildRegionTree(List<PgRegion> regions, Long parentId) {
|
||||
return regions.stream()
|
||||
.filter(r -> parentId.equals(r.getParentId()))
|
||||
.map(region -> {
|
||||
Map<String, Object> map = new HashMap<>();
|
||||
map.put("regionId", region.getRegionId());
|
||||
map.put("regionName", region.getRegionName());
|
||||
map.put("regionCode", region.getRegionCode());
|
||||
map.put("level", region.getLevel());
|
||||
|
||||
List<Map<String, Object>> children = buildRegionTree(regions, region.getRegionId());
|
||||
if (!children.isEmpty()) {
|
||||
map.put("children", children);
|
||||
}
|
||||
return map;
|
||||
})
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,118 @@
|
|||
package org.dromara.pangu.h5.controller;
|
||||
|
||||
import jakarta.validation.Valid;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.dromara.common.core.domain.R;
|
||||
import org.dromara.pangu.h5.domain.dto.H5EducationDto;
|
||||
import org.dromara.pangu.h5.domain.dto.H5MemberUpdateDto;
|
||||
import org.dromara.pangu.h5.domain.dto.H5PasswordUpdateDto;
|
||||
import org.dromara.pangu.h5.domain.dto.H5StudentBindDto;
|
||||
import org.dromara.pangu.h5.domain.vo.H5EducationVo;
|
||||
import org.dromara.pangu.h5.domain.vo.H5MemberInfoVo;
|
||||
import org.dromara.pangu.h5.domain.vo.H5StudentVo;
|
||||
import org.dromara.pangu.h5.service.H5MemberService;
|
||||
import org.springframework.validation.annotation.Validated;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* H5会员接口
|
||||
*
|
||||
* @author 湖北新华业务中台研发团队
|
||||
*/
|
||||
@Validated
|
||||
@RestController
|
||||
@RequestMapping("/h5/member")
|
||||
@RequiredArgsConstructor
|
||||
public class H5MemberController {
|
||||
|
||||
private final H5MemberService memberService;
|
||||
|
||||
/**
|
||||
* 获取当前会员信息
|
||||
*/
|
||||
@GetMapping("/info")
|
||||
public R<H5MemberInfoVo> getInfo() {
|
||||
return R.ok(memberService.getMemberInfo());
|
||||
}
|
||||
|
||||
/**
|
||||
* 修改会员信息(昵称/性别/生日)
|
||||
*/
|
||||
@PutMapping("/info")
|
||||
public R<Void> updateInfo(@RequestBody H5MemberUpdateDto dto) {
|
||||
memberService.updateMemberInfo(dto);
|
||||
return R.ok();
|
||||
}
|
||||
|
||||
/**
|
||||
* 修改密码
|
||||
*/
|
||||
@PutMapping("/password")
|
||||
public R<Void> updatePassword(@Valid @RequestBody H5PasswordUpdateDto dto) {
|
||||
memberService.updatePassword(dto);
|
||||
return R.ok();
|
||||
}
|
||||
|
||||
/**
|
||||
* 添加/修改教育身份
|
||||
*/
|
||||
@PostMapping("/education")
|
||||
public R<Void> saveEducation(@Valid @RequestBody H5EducationDto dto) {
|
||||
memberService.saveEducation(dto);
|
||||
return R.ok();
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取教育身份
|
||||
*/
|
||||
@GetMapping("/education")
|
||||
public R<H5EducationVo> getEducation() {
|
||||
return R.ok(memberService.getEducation());
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除教育身份
|
||||
*/
|
||||
@DeleteMapping("/education")
|
||||
public R<Void> deleteEducation() {
|
||||
memberService.deleteEducation();
|
||||
return R.ok();
|
||||
}
|
||||
|
||||
/**
|
||||
* 绑定学生
|
||||
*/
|
||||
@PostMapping("/student")
|
||||
public R<Void> bindStudent(@Valid @RequestBody H5StudentBindDto dto) {
|
||||
memberService.bindStudent(dto);
|
||||
return R.ok();
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取绑定的学生列表
|
||||
*/
|
||||
@GetMapping("/students")
|
||||
public R<List<H5StudentVo>> getStudents() {
|
||||
return R.ok(memberService.getStudents());
|
||||
}
|
||||
|
||||
/**
|
||||
* 修改学生信息
|
||||
*/
|
||||
@PutMapping("/student/{studentId}")
|
||||
public R<Void> updateStudent(@PathVariable Long studentId, @Valid @RequestBody H5StudentBindDto dto) {
|
||||
memberService.updateStudent(studentId, dto);
|
||||
return R.ok();
|
||||
}
|
||||
|
||||
/**
|
||||
* 解绑学生
|
||||
*/
|
||||
@DeleteMapping("/student/{studentId}")
|
||||
public R<Void> unbindStudent(@PathVariable Long studentId) {
|
||||
memberService.unbindStudent(studentId);
|
||||
return R.ok();
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,37 @@
|
|||
package org.dromara.pangu.h5.domain.dto;
|
||||
|
||||
import jakarta.validation.constraints.NotNull;
|
||||
import lombok.Data;
|
||||
|
||||
/**
|
||||
* H5教育身份请求DTO
|
||||
*
|
||||
* @author 湖北新华业务中台研发团队
|
||||
*/
|
||||
@Data
|
||||
public class H5EducationDto {
|
||||
|
||||
/**
|
||||
* 学校ID
|
||||
*/
|
||||
@NotNull(message = "请选择学校")
|
||||
private Long schoolId;
|
||||
|
||||
/**
|
||||
* 年级ID
|
||||
*/
|
||||
@NotNull(message = "请选择年级")
|
||||
private Long schoolGradeId;
|
||||
|
||||
/**
|
||||
* 班级ID
|
||||
*/
|
||||
@NotNull(message = "请选择班级")
|
||||
private Long schoolClassId;
|
||||
|
||||
/**
|
||||
* 学科ID
|
||||
*/
|
||||
@NotNull(message = "请选择学科")
|
||||
private Long subjectId;
|
||||
}
|
||||
|
|
@ -0,0 +1,29 @@
|
|||
package org.dromara.pangu.h5.domain.dto;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
import java.util.Date;
|
||||
|
||||
/**
|
||||
* H5会员信息修改请求DTO
|
||||
*
|
||||
* @author 湖北新华业务中台研发团队
|
||||
*/
|
||||
@Data
|
||||
public class H5MemberUpdateDto {
|
||||
|
||||
/**
|
||||
* 昵称
|
||||
*/
|
||||
private String nickname;
|
||||
|
||||
/**
|
||||
* 性别(0未知 1男 2女)
|
||||
*/
|
||||
private String gender;
|
||||
|
||||
/**
|
||||
* 生日
|
||||
*/
|
||||
private Date birthday;
|
||||
}
|
||||
|
|
@ -0,0 +1,44 @@
|
|||
package org.dromara.pangu.h5.domain.dto;
|
||||
|
||||
import jakarta.validation.constraints.NotBlank;
|
||||
import jakarta.validation.constraints.Pattern;
|
||||
import lombok.Data;
|
||||
|
||||
/**
|
||||
* H5密码登录请求DTO
|
||||
*
|
||||
* @author 湖北新华业务中台研发团队
|
||||
*/
|
||||
@Data
|
||||
public class H5PasswordLoginDto {
|
||||
|
||||
/**
|
||||
* 手机号
|
||||
*/
|
||||
@NotBlank(message = "手机号不能为空")
|
||||
@Pattern(regexp = "^1[3-9]\\d{9}$", message = "手机号格式不正确")
|
||||
private String phone;
|
||||
|
||||
/**
|
||||
* 密码
|
||||
*/
|
||||
@NotBlank(message = "密码不能为空")
|
||||
private String password;
|
||||
|
||||
/**
|
||||
* 图形验证码
|
||||
*/
|
||||
@NotBlank(message = "验证码不能为空")
|
||||
private String captchaCode;
|
||||
|
||||
/**
|
||||
* 验证码标识
|
||||
*/
|
||||
@NotBlank(message = "验证码标识不能为空")
|
||||
private String uuid;
|
||||
|
||||
/**
|
||||
* 记住我
|
||||
*/
|
||||
private Boolean rememberMe = false;
|
||||
}
|
||||
|
|
@ -0,0 +1,33 @@
|
|||
package org.dromara.pangu.h5.domain.dto;
|
||||
|
||||
import jakarta.validation.constraints.NotBlank;
|
||||
import jakarta.validation.constraints.Size;
|
||||
import lombok.Data;
|
||||
|
||||
/**
|
||||
* H5修改密码请求DTO
|
||||
*
|
||||
* @author 湖北新华业务中台研发团队
|
||||
*/
|
||||
@Data
|
||||
public class H5PasswordUpdateDto {
|
||||
|
||||
/**
|
||||
* 当前密码
|
||||
*/
|
||||
@NotBlank(message = "当前密码不能为空")
|
||||
private String oldPassword;
|
||||
|
||||
/**
|
||||
* 新密码
|
||||
*/
|
||||
@NotBlank(message = "新密码不能为空")
|
||||
@Size(min = 6, message = "新密码至少6位")
|
||||
private String newPassword;
|
||||
|
||||
/**
|
||||
* 确认新密码
|
||||
*/
|
||||
@NotBlank(message = "确认密码不能为空")
|
||||
private String confirmPassword;
|
||||
}
|
||||
|
|
@ -0,0 +1,47 @@
|
|||
package org.dromara.pangu.h5.domain.dto;
|
||||
|
||||
import jakarta.validation.constraints.NotBlank;
|
||||
import jakarta.validation.constraints.Pattern;
|
||||
import jakarta.validation.constraints.Size;
|
||||
import lombok.Data;
|
||||
|
||||
/**
|
||||
* H5注册请求DTO
|
||||
*
|
||||
* @author 湖北新华业务中台研发团队
|
||||
*/
|
||||
@Data
|
||||
public class H5RegisterDto {
|
||||
|
||||
/**
|
||||
* 手机号
|
||||
*/
|
||||
@NotBlank(message = "手机号不能为空")
|
||||
@Pattern(regexp = "^1[3-9]\\d{9}$", message = "手机号格式不正确")
|
||||
private String phone;
|
||||
|
||||
/**
|
||||
* 短信验证码
|
||||
*/
|
||||
@NotBlank(message = "短信验证码不能为空")
|
||||
private String smsCode;
|
||||
|
||||
/**
|
||||
* 图形验证码
|
||||
*/
|
||||
@NotBlank(message = "图形验证码不能为空")
|
||||
private String captchaCode;
|
||||
|
||||
/**
|
||||
* 验证码标识
|
||||
*/
|
||||
@NotBlank(message = "验证码标识不能为空")
|
||||
private String uuid;
|
||||
|
||||
/**
|
||||
* 密码
|
||||
*/
|
||||
@NotBlank(message = "密码不能为空")
|
||||
@Size(min = 6, message = "密码至少6位")
|
||||
private String password;
|
||||
}
|
||||
|
|
@ -0,0 +1,39 @@
|
|||
package org.dromara.pangu.h5.domain.dto;
|
||||
|
||||
import jakarta.validation.constraints.NotBlank;
|
||||
import jakarta.validation.constraints.Pattern;
|
||||
import lombok.Data;
|
||||
|
||||
/**
|
||||
* H5短信验证码登录请求DTO
|
||||
*
|
||||
* @author 湖北新华业务中台研发团队
|
||||
*/
|
||||
@Data
|
||||
public class H5SmsLoginDto {
|
||||
|
||||
/**
|
||||
* 手机号
|
||||
*/
|
||||
@NotBlank(message = "手机号不能为空")
|
||||
@Pattern(regexp = "^1[3-9]\\d{9}$", message = "手机号格式不正确")
|
||||
private String phone;
|
||||
|
||||
/**
|
||||
* 短信验证码
|
||||
*/
|
||||
@NotBlank(message = "短信验证码不能为空")
|
||||
private String smsCode;
|
||||
|
||||
/**
|
||||
* 图形验证码
|
||||
*/
|
||||
@NotBlank(message = "图形验证码不能为空")
|
||||
private String captchaCode;
|
||||
|
||||
/**
|
||||
* 验证码标识
|
||||
*/
|
||||
@NotBlank(message = "验证码标识不能为空")
|
||||
private String uuid;
|
||||
}
|
||||
|
|
@ -0,0 +1,39 @@
|
|||
package org.dromara.pangu.h5.domain.dto;
|
||||
|
||||
import jakarta.validation.constraints.NotBlank;
|
||||
import jakarta.validation.constraints.Pattern;
|
||||
import lombok.Data;
|
||||
|
||||
/**
|
||||
* H5发送短信验证码请求DTO
|
||||
*
|
||||
* @author 湖北新华业务中台研发团队
|
||||
*/
|
||||
@Data
|
||||
public class H5SmsSendDto {
|
||||
|
||||
/**
|
||||
* 手机号
|
||||
*/
|
||||
@NotBlank(message = "手机号不能为空")
|
||||
@Pattern(regexp = "^1[3-9]\\d{9}$", message = "手机号格式不正确")
|
||||
private String phone;
|
||||
|
||||
/**
|
||||
* 图形验证码
|
||||
*/
|
||||
@NotBlank(message = "图形验证码不能为空")
|
||||
private String captchaCode;
|
||||
|
||||
/**
|
||||
* 验证码标识
|
||||
*/
|
||||
@NotBlank(message = "验证码标识不能为空")
|
||||
private String uuid;
|
||||
|
||||
/**
|
||||
* 类型:login-登录,register-注册
|
||||
*/
|
||||
@NotBlank(message = "类型不能为空")
|
||||
private String type;
|
||||
}
|
||||
|
|
@ -0,0 +1,64 @@
|
|||
package org.dromara.pangu.h5.domain.dto;
|
||||
|
||||
import jakarta.validation.constraints.NotBlank;
|
||||
import jakarta.validation.constraints.NotNull;
|
||||
import lombok.Data;
|
||||
|
||||
import java.util.Date;
|
||||
|
||||
/**
|
||||
* H5绑定学生请求DTO
|
||||
*
|
||||
* @author 湖北新华业务中台研发团队
|
||||
*/
|
||||
@Data
|
||||
public class H5StudentBindDto {
|
||||
|
||||
/**
|
||||
* 学生姓名
|
||||
*/
|
||||
@NotBlank(message = "学生姓名不能为空")
|
||||
private String studentName;
|
||||
|
||||
/**
|
||||
* 学号
|
||||
*/
|
||||
@NotBlank(message = "学号不能为空")
|
||||
private String studentNo;
|
||||
|
||||
/**
|
||||
* 出生日期
|
||||
*/
|
||||
@NotNull(message = "出生日期不能为空")
|
||||
private Date birthday;
|
||||
|
||||
/**
|
||||
* 性别(0未知 1男 2女)
|
||||
*/
|
||||
@NotBlank(message = "性别不能为空")
|
||||
private String gender;
|
||||
|
||||
/**
|
||||
* 区域ID
|
||||
*/
|
||||
@NotNull(message = "请选择所在地区")
|
||||
private Long regionId;
|
||||
|
||||
/**
|
||||
* 学校ID
|
||||
*/
|
||||
@NotNull(message = "请选择学校")
|
||||
private Long schoolId;
|
||||
|
||||
/**
|
||||
* 年级ID
|
||||
*/
|
||||
@NotNull(message = "请选择年级")
|
||||
private Long schoolGradeId;
|
||||
|
||||
/**
|
||||
* 班级ID
|
||||
*/
|
||||
@NotNull(message = "请选择班级")
|
||||
private Long schoolClassId;
|
||||
}
|
||||
|
|
@ -0,0 +1,22 @@
|
|||
package org.dromara.pangu.h5.domain.vo;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
/**
|
||||
* H5图形验证码响应VO
|
||||
*
|
||||
* @author 湖北新华业务中台研发团队
|
||||
*/
|
||||
@Data
|
||||
public class H5CaptchaVo {
|
||||
|
||||
/**
|
||||
* 验证码标识
|
||||
*/
|
||||
private String uuid;
|
||||
|
||||
/**
|
||||
* 验证码图片(Base64)
|
||||
*/
|
||||
private String captchaImg;
|
||||
}
|
||||
|
|
@ -0,0 +1,52 @@
|
|||
package org.dromara.pangu.h5.domain.vo;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
/**
|
||||
* H5教育身份响应VO
|
||||
*
|
||||
* @author 湖北新华业务中台研发团队
|
||||
*/
|
||||
@Data
|
||||
public class H5EducationVo {
|
||||
|
||||
/**
|
||||
* 学校ID
|
||||
*/
|
||||
private Long schoolId;
|
||||
|
||||
/**
|
||||
* 学校名称
|
||||
*/
|
||||
private String schoolName;
|
||||
|
||||
/**
|
||||
* 年级ID
|
||||
*/
|
||||
private Long schoolGradeId;
|
||||
|
||||
/**
|
||||
* 年级名称
|
||||
*/
|
||||
private String gradeName;
|
||||
|
||||
/**
|
||||
* 班级ID
|
||||
*/
|
||||
private Long schoolClassId;
|
||||
|
||||
/**
|
||||
* 班级名称
|
||||
*/
|
||||
private String className;
|
||||
|
||||
/**
|
||||
* 学科ID
|
||||
*/
|
||||
private Long subjectId;
|
||||
|
||||
/**
|
||||
* 学科名称
|
||||
*/
|
||||
private String subjectName;
|
||||
}
|
||||
|
|
@ -0,0 +1,52 @@
|
|||
package org.dromara.pangu.h5.domain.vo;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
/**
|
||||
* H5登录响应VO
|
||||
*
|
||||
* @author 湖北新华业务中台研发团队
|
||||
*/
|
||||
@Data
|
||||
public class H5LoginVo {
|
||||
|
||||
/**
|
||||
* 访问令牌
|
||||
*/
|
||||
private String accessToken;
|
||||
|
||||
/**
|
||||
* 刷新令牌
|
||||
*/
|
||||
private String refreshToken;
|
||||
|
||||
/**
|
||||
* 过期时间(秒)
|
||||
*/
|
||||
private Long expiresIn;
|
||||
|
||||
/**
|
||||
* 会员ID
|
||||
*/
|
||||
private Long memberId;
|
||||
|
||||
/**
|
||||
* 会员编号
|
||||
*/
|
||||
private String memberCode;
|
||||
|
||||
/**
|
||||
* 手机号(脱敏)
|
||||
*/
|
||||
private String phone;
|
||||
|
||||
/**
|
||||
* 昵称
|
||||
*/
|
||||
private String nickname;
|
||||
|
||||
/**
|
||||
* 身份类型
|
||||
*/
|
||||
private String identityType;
|
||||
}
|
||||
|
|
@ -0,0 +1,70 @@
|
|||
package org.dromara.pangu.h5.domain.vo;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* H5会员信息响应VO
|
||||
*
|
||||
* @author 湖北新华业务中台研发团队
|
||||
*/
|
||||
@Data
|
||||
public class H5MemberInfoVo {
|
||||
|
||||
/**
|
||||
* 会员ID
|
||||
*/
|
||||
private Long memberId;
|
||||
|
||||
/**
|
||||
* 会员编号
|
||||
*/
|
||||
private String memberCode;
|
||||
|
||||
/**
|
||||
* 手机号(脱敏)
|
||||
*/
|
||||
private String phone;
|
||||
|
||||
/**
|
||||
* 昵称
|
||||
*/
|
||||
private String nickname;
|
||||
|
||||
/**
|
||||
* 头像
|
||||
*/
|
||||
private String avatar;
|
||||
|
||||
/**
|
||||
* 性别(0未知 1男 2女)
|
||||
*/
|
||||
private String gender;
|
||||
|
||||
/**
|
||||
* 生日
|
||||
*/
|
||||
private Date birthday;
|
||||
|
||||
/**
|
||||
* 注册时间
|
||||
*/
|
||||
private Date registerTime;
|
||||
|
||||
/**
|
||||
* 身份类型(parent/teacher)
|
||||
*/
|
||||
private String identityType;
|
||||
|
||||
/**
|
||||
* 教育信息(教师身份)
|
||||
*/
|
||||
private H5EducationVo education;
|
||||
|
||||
/**
|
||||
* 绑定的学生列表
|
||||
*/
|
||||
private List<H5StudentVo> students;
|
||||
}
|
||||
|
|
@ -0,0 +1,79 @@
|
|||
package org.dromara.pangu.h5.domain.vo;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
import java.util.Date;
|
||||
|
||||
/**
|
||||
* H5学生信息响应VO
|
||||
*
|
||||
* @author 湖北新华业务中台研发团队
|
||||
*/
|
||||
@Data
|
||||
public class H5StudentVo {
|
||||
|
||||
/**
|
||||
* 学生ID
|
||||
*/
|
||||
private Long studentId;
|
||||
|
||||
/**
|
||||
* 学生姓名
|
||||
*/
|
||||
private String studentName;
|
||||
|
||||
/**
|
||||
* 学号
|
||||
*/
|
||||
private String studentNo;
|
||||
|
||||
/**
|
||||
* 性别
|
||||
*/
|
||||
private String gender;
|
||||
|
||||
/**
|
||||
* 出生日期
|
||||
*/
|
||||
private Date birthday;
|
||||
|
||||
/**
|
||||
* 区域ID
|
||||
*/
|
||||
private Long regionId;
|
||||
|
||||
/**
|
||||
* 区域名称
|
||||
*/
|
||||
private String regionName;
|
||||
|
||||
/**
|
||||
* 学校ID
|
||||
*/
|
||||
private Long schoolId;
|
||||
|
||||
/**
|
||||
* 学校名称
|
||||
*/
|
||||
private String schoolName;
|
||||
|
||||
/**
|
||||
* 年级ID
|
||||
*/
|
||||
private Long schoolGradeId;
|
||||
|
||||
/**
|
||||
* 年级名称
|
||||
*/
|
||||
private String gradeName;
|
||||
|
||||
/**
|
||||
* 班级ID
|
||||
*/
|
||||
private Long schoolClassId;
|
||||
|
||||
/**
|
||||
* 班级名称
|
||||
*/
|
||||
private String className;
|
||||
}
|
||||
|
|
@ -0,0 +1,51 @@
|
|||
package org.dromara.pangu.h5.service;
|
||||
|
||||
import org.dromara.pangu.h5.domain.dto.H5PasswordLoginDto;
|
||||
import org.dromara.pangu.h5.domain.dto.H5RegisterDto;
|
||||
import org.dromara.pangu.h5.domain.dto.H5SmsLoginDto;
|
||||
import org.dromara.pangu.h5.domain.dto.H5SmsSendDto;
|
||||
import org.dromara.pangu.h5.domain.vo.H5CaptchaVo;
|
||||
import org.dromara.pangu.h5.domain.vo.H5LoginVo;
|
||||
|
||||
/**
|
||||
* H5认证服务接口
|
||||
*
|
||||
* @author 湖北新华业务中台研发团队
|
||||
*/
|
||||
public interface H5AuthService {
|
||||
|
||||
/**
|
||||
* 获取图形验证码
|
||||
*/
|
||||
H5CaptchaVo getCaptcha();
|
||||
|
||||
/**
|
||||
* 发送短信验证码
|
||||
*/
|
||||
void sendSmsCode(H5SmsSendDto dto);
|
||||
|
||||
/**
|
||||
* 密码登录
|
||||
*/
|
||||
H5LoginVo loginByPassword(H5PasswordLoginDto dto);
|
||||
|
||||
/**
|
||||
* 短信验证码登录
|
||||
*/
|
||||
H5LoginVo loginBySms(H5SmsLoginDto dto);
|
||||
|
||||
/**
|
||||
* 注册
|
||||
*/
|
||||
H5LoginVo register(H5RegisterDto dto);
|
||||
|
||||
/**
|
||||
* 刷新Token
|
||||
*/
|
||||
H5LoginVo refreshToken(String refreshToken);
|
||||
|
||||
/**
|
||||
* 退出登录
|
||||
*/
|
||||
void logout();
|
||||
}
|
||||
|
|
@ -0,0 +1,69 @@
|
|||
package org.dromara.pangu.h5.service;
|
||||
|
||||
import org.dromara.pangu.h5.domain.dto.H5EducationDto;
|
||||
import org.dromara.pangu.h5.domain.dto.H5MemberUpdateDto;
|
||||
import org.dromara.pangu.h5.domain.dto.H5PasswordUpdateDto;
|
||||
import org.dromara.pangu.h5.domain.dto.H5StudentBindDto;
|
||||
import org.dromara.pangu.h5.domain.vo.H5EducationVo;
|
||||
import org.dromara.pangu.h5.domain.vo.H5MemberInfoVo;
|
||||
import org.dromara.pangu.h5.domain.vo.H5StudentVo;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* H5会员服务接口
|
||||
*
|
||||
* @author 湖北新华业务中台研发团队
|
||||
*/
|
||||
public interface H5MemberService {
|
||||
|
||||
/**
|
||||
* 获取当前会员信息
|
||||
*/
|
||||
H5MemberInfoVo getMemberInfo();
|
||||
|
||||
/**
|
||||
* 修改会员信息
|
||||
*/
|
||||
void updateMemberInfo(H5MemberUpdateDto dto);
|
||||
|
||||
/**
|
||||
* 修改密码
|
||||
*/
|
||||
void updatePassword(H5PasswordUpdateDto dto);
|
||||
|
||||
/**
|
||||
* 添加/修改教育身份
|
||||
*/
|
||||
void saveEducation(H5EducationDto dto);
|
||||
|
||||
/**
|
||||
* 获取教育身份
|
||||
*/
|
||||
H5EducationVo getEducation();
|
||||
|
||||
/**
|
||||
* 删除教育身份
|
||||
*/
|
||||
void deleteEducation();
|
||||
|
||||
/**
|
||||
* 绑定学生
|
||||
*/
|
||||
void bindStudent(H5StudentBindDto dto);
|
||||
|
||||
/**
|
||||
* 获取绑定的学生列表
|
||||
*/
|
||||
List<H5StudentVo> getStudents();
|
||||
|
||||
/**
|
||||
* 修改学生信息
|
||||
*/
|
||||
void updateStudent(Long studentId, H5StudentBindDto dto);
|
||||
|
||||
/**
|
||||
* 解绑学生
|
||||
*/
|
||||
void unbindStudent(Long studentId);
|
||||
}
|
||||
|
|
@ -0,0 +1,387 @@
|
|||
package org.dromara.pangu.h5.service.impl;
|
||||
|
||||
import cn.dev33.satoken.stp.SaLoginModel;
|
||||
import cn.dev33.satoken.stp.StpUtil;
|
||||
import cn.hutool.captcha.generator.MathGenerator;
|
||||
import cn.hutool.core.util.IdUtil;
|
||||
import cn.hutool.core.util.RandomUtil;
|
||||
import cn.hutool.crypto.digest.BCrypt;
|
||||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.dromara.common.core.constant.Constants;
|
||||
import org.dromara.common.core.constant.GlobalConstants;
|
||||
import org.dromara.common.core.exception.ServiceException;
|
||||
import org.dromara.common.core.utils.StringUtils;
|
||||
import org.dromara.common.redis.utils.RedisUtils;
|
||||
import org.dromara.common.web.core.WaveAndCircleCaptcha;
|
||||
import org.dromara.pangu.h5.domain.dto.H5PasswordLoginDto;
|
||||
import org.dromara.pangu.h5.domain.dto.H5RegisterDto;
|
||||
import org.dromara.pangu.h5.domain.dto.H5SmsLoginDto;
|
||||
import org.dromara.pangu.h5.domain.dto.H5SmsSendDto;
|
||||
import org.dromara.pangu.h5.domain.vo.H5CaptchaVo;
|
||||
import org.dromara.pangu.h5.domain.vo.H5LoginVo;
|
||||
import org.dromara.pangu.h5.service.H5AuthService;
|
||||
import org.dromara.pangu.member.domain.PgMember;
|
||||
import org.dromara.pangu.member.mapper.PgMemberMapper;
|
||||
import org.dromara.sms4j.api.SmsBlend;
|
||||
import org.dromara.sms4j.api.entity.SmsResponse;
|
||||
import org.dromara.sms4j.core.factory.SmsFactory;
|
||||
import org.springframework.expression.Expression;
|
||||
import org.springframework.expression.ExpressionParser;
|
||||
import org.springframework.expression.spel.standard.SpelExpressionParser;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.awt.*;
|
||||
import java.time.Duration;
|
||||
import java.util.Date;
|
||||
import java.util.LinkedHashMap;
|
||||
|
||||
/**
|
||||
* H5认证服务实现
|
||||
*
|
||||
* @author 湖北新华业务中台研发团队
|
||||
*/
|
||||
@Slf4j
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class H5AuthServiceImpl implements H5AuthService {
|
||||
|
||||
private final PgMemberMapper memberMapper;
|
||||
|
||||
/**
|
||||
* H5会员登录设备标识
|
||||
*/
|
||||
private static final String H5_DEVICE = "h5";
|
||||
|
||||
/**
|
||||
* 短信验证码Redis前缀
|
||||
*/
|
||||
private static final String H5_SMS_CODE_KEY = "h5:sms:code:";
|
||||
|
||||
/**
|
||||
* RefreshToken Redis前缀
|
||||
*/
|
||||
private static final String H5_REFRESH_TOKEN_KEY = "h5:refresh:token:";
|
||||
|
||||
/**
|
||||
* accessToken有效期(秒)- 2小时
|
||||
*/
|
||||
private static final long ACCESS_TOKEN_EXPIRE = 2 * 60 * 60;
|
||||
|
||||
/**
|
||||
* refreshToken有效期(秒)- 7天
|
||||
*/
|
||||
private static final long REFRESH_TOKEN_EXPIRE = 7 * 24 * 60 * 60;
|
||||
|
||||
/**
|
||||
* refreshToken有效期(秒)- 记住我30天
|
||||
*/
|
||||
private static final long REFRESH_TOKEN_EXPIRE_REMEMBER = 30 * 24 * 60 * 60;
|
||||
|
||||
@Override
|
||||
public H5CaptchaVo getCaptcha() {
|
||||
String uuid = IdUtil.simpleUUID();
|
||||
String verifyKey = GlobalConstants.CAPTCHA_CODE_KEY + uuid;
|
||||
|
||||
// 生成数学验证码
|
||||
MathGenerator codeGenerator = new MathGenerator(1, false);
|
||||
WaveAndCircleCaptcha captcha = new WaveAndCircleCaptcha(160, 60);
|
||||
captcha.setFont(new Font("Arial", Font.BOLD, 45));
|
||||
captcha.setGenerator(codeGenerator);
|
||||
captcha.createCode();
|
||||
|
||||
// 计算验证码结果
|
||||
String code = captcha.getCode();
|
||||
ExpressionParser parser = new SpelExpressionParser();
|
||||
Expression exp = parser.parseExpression(StringUtils.remove(code, "="));
|
||||
code = exp.getValue(String.class);
|
||||
|
||||
// 存入Redis,5分钟有效
|
||||
RedisUtils.setCacheObject(verifyKey, code, Duration.ofMinutes(Constants.CAPTCHA_EXPIRATION));
|
||||
|
||||
H5CaptchaVo vo = new H5CaptchaVo();
|
||||
vo.setUuid(uuid);
|
||||
vo.setCaptchaImg(captcha.getImageBase64());
|
||||
return vo;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void sendSmsCode(H5SmsSendDto dto) {
|
||||
// 校验图形验证码
|
||||
validateCaptcha(dto.getUuid(), dto.getCaptchaCode());
|
||||
|
||||
String phone = dto.getPhone();
|
||||
String type = dto.getType();
|
||||
|
||||
// 校验类型
|
||||
if (!"login".equals(type) && !"register".equals(type)) {
|
||||
throw new ServiceException("验证码类型错误");
|
||||
}
|
||||
|
||||
// 注册时校验手机号是否已注册
|
||||
if ("register".equals(type)) {
|
||||
PgMember existMember = memberMapper.selectOne(
|
||||
new LambdaQueryWrapper<PgMember>().eq(PgMember::getPhone, phone)
|
||||
);
|
||||
if (existMember != null) {
|
||||
throw new ServiceException("该手机号已注册");
|
||||
}
|
||||
}
|
||||
|
||||
// 登录时校验手机号是否存在
|
||||
if ("login".equals(type)) {
|
||||
PgMember existMember = memberMapper.selectOne(
|
||||
new LambdaQueryWrapper<PgMember>().eq(PgMember::getPhone, phone)
|
||||
);
|
||||
if (existMember == null) {
|
||||
throw new ServiceException("该手机号未注册");
|
||||
}
|
||||
}
|
||||
|
||||
// 生成6位验证码
|
||||
String code = RandomUtil.randomNumbers(6);
|
||||
String codeKey = H5_SMS_CODE_KEY + type + ":" + phone;
|
||||
|
||||
// 检查是否在60秒内重复发送
|
||||
if (RedisUtils.hasKey(codeKey)) {
|
||||
Long ttl = RedisUtils.getTimeToLive(codeKey);
|
||||
if (ttl != null && ttl > 240) {
|
||||
throw new ServiceException("验证码发送过于频繁,请稍后再试");
|
||||
}
|
||||
}
|
||||
|
||||
// 存入Redis,5分钟有效
|
||||
RedisUtils.setCacheObject(codeKey, code, Duration.ofMinutes(5));
|
||||
|
||||
// 调用阿里云短信发送
|
||||
try {
|
||||
LinkedHashMap<String, String> map = new LinkedHashMap<>(1);
|
||||
map.put("code", code);
|
||||
// 使用配置的阿里云短信
|
||||
SmsBlend smsBlend = SmsFactory.getSmsBlend("alibaba");
|
||||
// 模板ID需要在配置文件中设置
|
||||
SmsResponse smsResponse = smsBlend.sendMessage(phone, map);
|
||||
if (!smsResponse.isSuccess()) {
|
||||
log.error("短信发送失败: phone={}, response={}", phone, smsResponse);
|
||||
throw new ServiceException("短信发送失败,请稍后重试");
|
||||
}
|
||||
log.info("短信发送成功: phone={}, type={}", phone, type);
|
||||
} catch (ServiceException e) {
|
||||
throw e;
|
||||
} catch (Exception e) {
|
||||
log.error("短信发送异常: phone={}, error={}", phone, e.getMessage());
|
||||
throw new ServiceException("短信发送失败,请稍后重试");
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public H5LoginVo loginByPassword(H5PasswordLoginDto dto) {
|
||||
// 校验图形验证码
|
||||
validateCaptcha(dto.getUuid(), dto.getCaptchaCode());
|
||||
|
||||
// 查询会员
|
||||
PgMember member = memberMapper.selectOne(
|
||||
new LambdaQueryWrapper<PgMember>().eq(PgMember::getPhone, dto.getPhone())
|
||||
);
|
||||
if (member == null) {
|
||||
throw new ServiceException("手机号或密码错误");
|
||||
}
|
||||
|
||||
// 校验密码
|
||||
if (!BCrypt.checkpw(dto.getPassword(), member.getPassword())) {
|
||||
throw new ServiceException("手机号或密码错误");
|
||||
}
|
||||
|
||||
// 校验状态
|
||||
if ("1".equals(member.getStatus())) {
|
||||
throw new ServiceException("账号已被禁用");
|
||||
}
|
||||
|
||||
// 执行登录
|
||||
return doLogin(member, dto.getRememberMe());
|
||||
}
|
||||
|
||||
@Override
|
||||
public H5LoginVo loginBySms(H5SmsLoginDto dto) {
|
||||
// 校验图形验证码
|
||||
validateCaptcha(dto.getUuid(), dto.getCaptchaCode());
|
||||
|
||||
// 校验短信验证码
|
||||
validateSmsCode(dto.getPhone(), dto.getSmsCode(), "login");
|
||||
|
||||
// 查询会员
|
||||
PgMember member = memberMapper.selectOne(
|
||||
new LambdaQueryWrapper<PgMember>().eq(PgMember::getPhone, dto.getPhone())
|
||||
);
|
||||
if (member == null) {
|
||||
throw new ServiceException("该手机号未注册");
|
||||
}
|
||||
|
||||
// 校验状态
|
||||
if ("1".equals(member.getStatus())) {
|
||||
throw new ServiceException("账号已被禁用");
|
||||
}
|
||||
|
||||
// 执行登录
|
||||
return doLogin(member, false);
|
||||
}
|
||||
|
||||
@Override
|
||||
public H5LoginVo register(H5RegisterDto dto) {
|
||||
// 校验图形验证码
|
||||
validateCaptcha(dto.getUuid(), dto.getCaptchaCode());
|
||||
|
||||
// 校验短信验证码
|
||||
validateSmsCode(dto.getPhone(), dto.getSmsCode(), "register");
|
||||
|
||||
// 校验手机号是否已注册
|
||||
PgMember existMember = memberMapper.selectOne(
|
||||
new LambdaQueryWrapper<PgMember>().eq(PgMember::getPhone, dto.getPhone())
|
||||
);
|
||||
if (existMember != null) {
|
||||
throw new ServiceException("该手机号已注册");
|
||||
}
|
||||
|
||||
// 创建会员
|
||||
PgMember member = new PgMember();
|
||||
member.setPhone(dto.getPhone());
|
||||
member.setPassword(BCrypt.hashpw(dto.getPassword()));
|
||||
member.setMemberCode(generateMemberCode());
|
||||
member.setNickname("user_" + dto.getPhone().substring(7));
|
||||
member.setRegisterSource("2"); // H5注册
|
||||
member.setRegisterTime(new Date());
|
||||
member.setStatus("0");
|
||||
member.setLoginCount(0);
|
||||
memberMapper.insert(member);
|
||||
|
||||
log.info("H5新会员注册: phone={}, memberId={}", dto.getPhone(), member.getMemberId());
|
||||
|
||||
// 自动登录
|
||||
return doLogin(member, false);
|
||||
}
|
||||
|
||||
@Override
|
||||
public H5LoginVo refreshToken(String refreshToken) {
|
||||
if (StringUtils.isBlank(refreshToken)) {
|
||||
throw new ServiceException("refreshToken不能为空");
|
||||
}
|
||||
|
||||
// 从Redis获取memberId
|
||||
String memberId = RedisUtils.getCacheObject(H5_REFRESH_TOKEN_KEY + refreshToken);
|
||||
if (StringUtils.isBlank(memberId)) {
|
||||
throw new ServiceException("refreshToken已过期,请重新登录");
|
||||
}
|
||||
|
||||
// 查询会员
|
||||
PgMember member = memberMapper.selectById(Long.parseLong(memberId));
|
||||
if (member == null) {
|
||||
throw new ServiceException("会员不存在");
|
||||
}
|
||||
|
||||
// 校验状态
|
||||
if ("1".equals(member.getStatus())) {
|
||||
throw new ServiceException("账号已被禁用");
|
||||
}
|
||||
|
||||
// 删除旧的refreshToken
|
||||
RedisUtils.deleteObject(H5_REFRESH_TOKEN_KEY + refreshToken);
|
||||
|
||||
// 重新登录
|
||||
return doLogin(member, false);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void logout() {
|
||||
if (StpUtil.isLogin()) {
|
||||
StpUtil.logout();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 执行登录
|
||||
*/
|
||||
private H5LoginVo doLogin(PgMember member, Boolean rememberMe) {
|
||||
// Sa-Token登录,指定设备类型为h5
|
||||
long timeout = rememberMe != null && rememberMe ? REFRESH_TOKEN_EXPIRE_REMEMBER : REFRESH_TOKEN_EXPIRE;
|
||||
StpUtil.login(member.getMemberId(), new SaLoginModel()
|
||||
.setDevice(H5_DEVICE)
|
||||
.setTimeout(ACCESS_TOKEN_EXPIRE)
|
||||
);
|
||||
|
||||
// 生成refreshToken
|
||||
String refreshToken = IdUtil.fastSimpleUUID();
|
||||
RedisUtils.setCacheObject(
|
||||
H5_REFRESH_TOKEN_KEY + refreshToken,
|
||||
member.getMemberId().toString(),
|
||||
Duration.ofSeconds(timeout)
|
||||
);
|
||||
|
||||
// 更新登录信息
|
||||
member.setLastLoginTime(new Date());
|
||||
member.setLoginCount(member.getLoginCount() == null ? 1 : member.getLoginCount() + 1);
|
||||
memberMapper.updateById(member);
|
||||
|
||||
// 构建返回结果
|
||||
H5LoginVo vo = new H5LoginVo();
|
||||
vo.setAccessToken(StpUtil.getTokenValue());
|
||||
vo.setRefreshToken(refreshToken);
|
||||
vo.setExpiresIn(ACCESS_TOKEN_EXPIRE);
|
||||
vo.setMemberId(member.getMemberId());
|
||||
vo.setMemberCode(member.getMemberCode());
|
||||
vo.setPhone(maskPhone(member.getPhone()));
|
||||
vo.setNickname(member.getNickname());
|
||||
vo.setIdentityType(member.getIdentityType());
|
||||
|
||||
return vo;
|
||||
}
|
||||
|
||||
/**
|
||||
* 校验图形验证码
|
||||
*/
|
||||
private void validateCaptcha(String uuid, String code) {
|
||||
String verifyKey = GlobalConstants.CAPTCHA_CODE_KEY + uuid;
|
||||
String captcha = RedisUtils.getCacheObject(verifyKey);
|
||||
if (captcha == null) {
|
||||
throw new ServiceException("验证码已过期");
|
||||
}
|
||||
if (!captcha.equalsIgnoreCase(code)) {
|
||||
throw new ServiceException("验证码错误");
|
||||
}
|
||||
// 验证成功后删除
|
||||
RedisUtils.deleteObject(verifyKey);
|
||||
}
|
||||
|
||||
/**
|
||||
* 校验短信验证码
|
||||
*/
|
||||
private void validateSmsCode(String phone, String code, String type) {
|
||||
String codeKey = H5_SMS_CODE_KEY + type + ":" + phone;
|
||||
String smsCode = RedisUtils.getCacheObject(codeKey);
|
||||
if (smsCode == null) {
|
||||
throw new ServiceException("短信验证码已过期");
|
||||
}
|
||||
if (!smsCode.equals(code)) {
|
||||
throw new ServiceException("短信验证码错误");
|
||||
}
|
||||
// 验证成功后删除
|
||||
RedisUtils.deleteObject(codeKey);
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成会员编号
|
||||
*/
|
||||
private String generateMemberCode() {
|
||||
return "M" + System.currentTimeMillis() + RandomUtil.randomNumbers(4);
|
||||
}
|
||||
|
||||
/**
|
||||
* 手机号脱敏
|
||||
*/
|
||||
private String maskPhone(String phone) {
|
||||
if (StringUtils.isBlank(phone) || phone.length() != 11) {
|
||||
return phone;
|
||||
}
|
||||
return phone.substring(0, 3) + "****" + phone.substring(7);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,442 @@
|
|||
package org.dromara.pangu.h5.service.impl;
|
||||
|
||||
import cn.dev33.satoken.stp.StpUtil;
|
||||
import cn.hutool.core.bean.BeanUtil;
|
||||
import cn.hutool.crypto.digest.BCrypt;
|
||||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.dromara.common.core.exception.ServiceException;
|
||||
import org.dromara.common.core.utils.StringUtils;
|
||||
import org.dromara.pangu.base.domain.PgClass;
|
||||
import org.dromara.pangu.base.domain.PgGrade;
|
||||
import org.dromara.pangu.base.domain.PgSubject;
|
||||
import org.dromara.pangu.base.mapper.PgClassMapper;
|
||||
import org.dromara.pangu.base.mapper.PgGradeMapper;
|
||||
import org.dromara.pangu.base.mapper.PgSubjectMapper;
|
||||
import org.dromara.pangu.h5.domain.dto.H5EducationDto;
|
||||
import org.dromara.pangu.h5.domain.dto.H5MemberUpdateDto;
|
||||
import org.dromara.pangu.h5.domain.dto.H5PasswordUpdateDto;
|
||||
import org.dromara.pangu.h5.domain.dto.H5StudentBindDto;
|
||||
import org.dromara.pangu.h5.domain.vo.H5EducationVo;
|
||||
import org.dromara.pangu.h5.domain.vo.H5MemberInfoVo;
|
||||
import org.dromara.pangu.h5.domain.vo.H5StudentVo;
|
||||
import org.dromara.pangu.h5.service.H5MemberService;
|
||||
import org.dromara.pangu.member.domain.PgMember;
|
||||
import org.dromara.pangu.member.mapper.PgMemberMapper;
|
||||
import org.dromara.pangu.school.domain.PgSchool;
|
||||
import org.dromara.pangu.school.domain.PgSchoolClass;
|
||||
import org.dromara.pangu.school.domain.PgSchoolGrade;
|
||||
import org.dromara.pangu.school.mapper.PgSchoolClassMapper;
|
||||
import org.dromara.pangu.school.mapper.PgSchoolGradeMapper;
|
||||
import org.dromara.pangu.school.mapper.PgSchoolMapper;
|
||||
import org.dromara.pangu.student.domain.PgStudent;
|
||||
import org.dromara.pangu.student.mapper.PgStudentMapper;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* H5会员服务实现
|
||||
*
|
||||
* @author 湖北新华业务中台研发团队
|
||||
*/
|
||||
@Slf4j
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class H5MemberServiceImpl implements H5MemberService {
|
||||
|
||||
private final PgMemberMapper memberMapper;
|
||||
private final PgStudentMapper studentMapper;
|
||||
private final PgSchoolMapper schoolMapper;
|
||||
private final PgSchoolGradeMapper schoolGradeMapper;
|
||||
private final PgSchoolClassMapper schoolClassMapper;
|
||||
private final PgGradeMapper gradeMapper;
|
||||
private final PgClassMapper classMapper;
|
||||
private final PgSubjectMapper subjectMapper;
|
||||
|
||||
@Override
|
||||
public H5MemberInfoVo getMemberInfo() {
|
||||
Long memberId = getCurrentMemberId();
|
||||
PgMember member = memberMapper.selectById(memberId);
|
||||
if (member == null) {
|
||||
throw new ServiceException("会员不存在");
|
||||
}
|
||||
|
||||
H5MemberInfoVo vo = new H5MemberInfoVo();
|
||||
vo.setMemberId(member.getMemberId());
|
||||
vo.setMemberCode(member.getMemberCode());
|
||||
vo.setPhone(maskPhone(member.getPhone()));
|
||||
vo.setNickname(member.getNickname());
|
||||
vo.setAvatar(member.getAvatar());
|
||||
vo.setGender(member.getGender());
|
||||
vo.setBirthday(member.getBirthday());
|
||||
vo.setRegisterTime(member.getRegisterTime());
|
||||
vo.setIdentityType(member.getIdentityType());
|
||||
|
||||
// 获取教育身份信息
|
||||
if ("2".equals(member.getIdentityType()) && member.getSchoolId() != null) {
|
||||
vo.setEducation(buildEducationVo(member));
|
||||
}
|
||||
|
||||
// 获取绑定的学生列表
|
||||
vo.setStudents(getStudents());
|
||||
|
||||
return vo;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void updateMemberInfo(H5MemberUpdateDto dto) {
|
||||
Long memberId = getCurrentMemberId();
|
||||
PgMember member = memberMapper.selectById(memberId);
|
||||
if (member == null) {
|
||||
throw new ServiceException("会员不存在");
|
||||
}
|
||||
|
||||
// 只允许修改昵称、性别、生日
|
||||
if (StringUtils.isNotBlank(dto.getNickname())) {
|
||||
member.setNickname(dto.getNickname());
|
||||
}
|
||||
if (StringUtils.isNotBlank(dto.getGender())) {
|
||||
member.setGender(dto.getGender());
|
||||
}
|
||||
if (dto.getBirthday() != null) {
|
||||
member.setBirthday(dto.getBirthday());
|
||||
}
|
||||
|
||||
memberMapper.updateById(member);
|
||||
log.info("H5会员信息修改: memberId={}", memberId);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void updatePassword(H5PasswordUpdateDto dto) {
|
||||
// 校验新密码与确认密码
|
||||
if (!dto.getNewPassword().equals(dto.getConfirmPassword())) {
|
||||
throw new ServiceException("两次输入的密码不一致");
|
||||
}
|
||||
|
||||
Long memberId = getCurrentMemberId();
|
||||
PgMember member = memberMapper.selectById(memberId);
|
||||
if (member == null) {
|
||||
throw new ServiceException("会员不存在");
|
||||
}
|
||||
|
||||
// 校验旧密码
|
||||
if (!BCrypt.checkpw(dto.getOldPassword(), member.getPassword())) {
|
||||
throw new ServiceException("当前密码错误");
|
||||
}
|
||||
|
||||
// 更新密码
|
||||
member.setPassword(BCrypt.hashpw(dto.getNewPassword()));
|
||||
memberMapper.updateById(member);
|
||||
log.info("H5会员密码修改: memberId={}", memberId);
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public void saveEducation(H5EducationDto dto) {
|
||||
Long memberId = getCurrentMemberId();
|
||||
PgMember member = memberMapper.selectById(memberId);
|
||||
if (member == null) {
|
||||
throw new ServiceException("会员不存在");
|
||||
}
|
||||
|
||||
// 校验学校、年级、班级、学科是否存在
|
||||
PgSchool school = schoolMapper.selectById(dto.getSchoolId());
|
||||
if (school == null) {
|
||||
throw new ServiceException("学校不存在");
|
||||
}
|
||||
|
||||
PgSchoolGrade schoolGrade = schoolGradeMapper.selectById(dto.getSchoolGradeId());
|
||||
if (schoolGrade == null || !schoolGrade.getSchoolId().equals(dto.getSchoolId())) {
|
||||
throw new ServiceException("年级不存在或不属于该学校");
|
||||
}
|
||||
|
||||
PgSchoolClass schoolClass = schoolClassMapper.selectById(dto.getSchoolClassId());
|
||||
if (schoolClass == null || !schoolClass.getSchoolGradeId().equals(dto.getSchoolGradeId())) {
|
||||
throw new ServiceException("班级不存在或不属于该年级");
|
||||
}
|
||||
|
||||
PgSubject subject = subjectMapper.selectById(dto.getSubjectId());
|
||||
if (subject == null) {
|
||||
throw new ServiceException("学科不存在");
|
||||
}
|
||||
|
||||
// 更新会员教育信息
|
||||
member.setIdentityType("2"); // 教师
|
||||
member.setSchoolId(dto.getSchoolId());
|
||||
member.setSchoolGradeId(dto.getSchoolGradeId());
|
||||
member.setSchoolClassId(dto.getSchoolClassId());
|
||||
// 学科信息需要在member表添加字段,这里先用remark暂存
|
||||
member.setRemark("subjectId:" + dto.getSubjectId());
|
||||
memberMapper.updateById(member);
|
||||
|
||||
log.info("H5教育身份保存: memberId={}, schoolId={}", memberId, dto.getSchoolId());
|
||||
}
|
||||
|
||||
@Override
|
||||
public H5EducationVo getEducation() {
|
||||
Long memberId = getCurrentMemberId();
|
||||
PgMember member = memberMapper.selectById(memberId);
|
||||
if (member == null) {
|
||||
throw new ServiceException("会员不存在");
|
||||
}
|
||||
|
||||
if (!"2".equals(member.getIdentityType()) || member.getSchoolId() == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return buildEducationVo(member);
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public void deleteEducation() {
|
||||
Long memberId = getCurrentMemberId();
|
||||
PgMember member = memberMapper.selectById(memberId);
|
||||
if (member == null) {
|
||||
throw new ServiceException("会员不存在");
|
||||
}
|
||||
|
||||
// 清除教育信息
|
||||
member.setIdentityType(null);
|
||||
member.setSchoolId(null);
|
||||
member.setSchoolGradeId(null);
|
||||
member.setSchoolClassId(null);
|
||||
member.setRemark(null);
|
||||
memberMapper.updateById(member);
|
||||
|
||||
log.info("H5教育身份删除: memberId={}", memberId);
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public void bindStudent(H5StudentBindDto dto) {
|
||||
Long memberId = getCurrentMemberId();
|
||||
PgMember member = memberMapper.selectById(memberId);
|
||||
if (member == null) {
|
||||
throw new ServiceException("会员不存在");
|
||||
}
|
||||
|
||||
// 校验学校、年级、班级是否存在
|
||||
validateSchoolInfo(dto.getSchoolId(), dto.getSchoolGradeId(), dto.getSchoolClassId());
|
||||
|
||||
// 创建学生
|
||||
PgStudent student = new PgStudent();
|
||||
student.setStudentName(dto.getStudentName());
|
||||
student.setStudentNo(dto.getStudentNo());
|
||||
student.setBirthday(dto.getBirthday());
|
||||
student.setGender(dto.getGender());
|
||||
student.setRegionId(dto.getRegionId());
|
||||
student.setSchoolId(dto.getSchoolId());
|
||||
student.setSchoolGradeId(dto.getSchoolGradeId());
|
||||
student.setSchoolClassId(dto.getSchoolClassId());
|
||||
student.setMemberId(memberId);
|
||||
student.setStatus("0");
|
||||
studentMapper.insert(student);
|
||||
|
||||
// 如果会员没有身份,默认设为家长
|
||||
if (StringUtils.isBlank(member.getIdentityType())) {
|
||||
member.setIdentityType("1"); // 家长
|
||||
memberMapper.updateById(member);
|
||||
}
|
||||
|
||||
log.info("H5绑定学生: memberId={}, studentId={}", memberId, student.getStudentId());
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<H5StudentVo> getStudents() {
|
||||
Long memberId = getCurrentMemberId();
|
||||
|
||||
List<PgStudent> students = studentMapper.selectList(
|
||||
new LambdaQueryWrapper<PgStudent>()
|
||||
.eq(PgStudent::getMemberId, memberId)
|
||||
.eq(PgStudent::getDelFlag, "0")
|
||||
);
|
||||
|
||||
List<H5StudentVo> voList = new ArrayList<>();
|
||||
for (PgStudent student : students) {
|
||||
H5StudentVo vo = new H5StudentVo();
|
||||
BeanUtil.copyProperties(student, vo);
|
||||
|
||||
// 填充学校、年级、班级名称
|
||||
fillStudentNames(vo, student);
|
||||
voList.add(vo);
|
||||
}
|
||||
|
||||
return voList;
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public void updateStudent(Long studentId, H5StudentBindDto dto) {
|
||||
Long memberId = getCurrentMemberId();
|
||||
|
||||
// 查询学生并校验归属
|
||||
PgStudent student = studentMapper.selectById(studentId);
|
||||
if (student == null || !memberId.equals(student.getMemberId())) {
|
||||
throw new ServiceException("学生不存在或无权限修改");
|
||||
}
|
||||
|
||||
// 校验学校、年级、班级是否存在
|
||||
validateSchoolInfo(dto.getSchoolId(), dto.getSchoolGradeId(), dto.getSchoolClassId());
|
||||
|
||||
// 更新学生信息
|
||||
student.setStudentName(dto.getStudentName());
|
||||
student.setStudentNo(dto.getStudentNo());
|
||||
student.setBirthday(dto.getBirthday());
|
||||
student.setGender(dto.getGender());
|
||||
student.setRegionId(dto.getRegionId());
|
||||
student.setSchoolId(dto.getSchoolId());
|
||||
student.setSchoolGradeId(dto.getSchoolGradeId());
|
||||
student.setSchoolClassId(dto.getSchoolClassId());
|
||||
studentMapper.updateById(student);
|
||||
|
||||
log.info("H5修改学生: memberId={}, studentId={}", memberId, studentId);
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public void unbindStudent(Long studentId) {
|
||||
Long memberId = getCurrentMemberId();
|
||||
|
||||
// 查询学生并校验归属
|
||||
PgStudent student = studentMapper.selectById(studentId);
|
||||
if (student == null || !memberId.equals(student.getMemberId())) {
|
||||
throw new ServiceException("学生不存在或无权限操作");
|
||||
}
|
||||
|
||||
// 解绑(清除会员关联)
|
||||
student.setMemberId(null);
|
||||
studentMapper.updateById(student);
|
||||
|
||||
log.info("H5解绑学生: memberId={}, studentId={}", memberId, studentId);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取当前登录会员ID
|
||||
*/
|
||||
private Long getCurrentMemberId() {
|
||||
if (!StpUtil.isLogin()) {
|
||||
throw new ServiceException("请先登录");
|
||||
}
|
||||
return StpUtil.getLoginIdAsLong();
|
||||
}
|
||||
|
||||
/**
|
||||
* 手机号脱敏
|
||||
*/
|
||||
private String maskPhone(String phone) {
|
||||
if (StringUtils.isBlank(phone) || phone.length() != 11) {
|
||||
return phone;
|
||||
}
|
||||
return phone.substring(0, 3) + "****" + phone.substring(7);
|
||||
}
|
||||
|
||||
/**
|
||||
* 构建教育身份VO
|
||||
*/
|
||||
private H5EducationVo buildEducationVo(PgMember member) {
|
||||
H5EducationVo vo = new H5EducationVo();
|
||||
vo.setSchoolId(member.getSchoolId());
|
||||
vo.setSchoolGradeId(member.getSchoolGradeId());
|
||||
vo.setSchoolClassId(member.getSchoolClassId());
|
||||
|
||||
// 获取学校名称
|
||||
PgSchool school = schoolMapper.selectById(member.getSchoolId());
|
||||
if (school != null) {
|
||||
vo.setSchoolName(school.getSchoolName());
|
||||
}
|
||||
|
||||
// 获取年级名称
|
||||
PgSchoolGrade schoolGrade = schoolGradeMapper.selectById(member.getSchoolGradeId());
|
||||
if (schoolGrade != null && schoolGrade.getGradeId() != null) {
|
||||
PgGrade grade = gradeMapper.selectById(schoolGrade.getGradeId());
|
||||
if (grade != null) {
|
||||
vo.setGradeName(grade.getGradeName());
|
||||
}
|
||||
}
|
||||
|
||||
// 获取班级名称
|
||||
PgSchoolClass schoolClass = schoolClassMapper.selectById(member.getSchoolClassId());
|
||||
if (schoolClass != null && schoolClass.getClassId() != null) {
|
||||
PgClass cls = classMapper.selectById(schoolClass.getClassId());
|
||||
if (cls != null) {
|
||||
vo.setClassName(cls.getClassName());
|
||||
}
|
||||
}
|
||||
|
||||
// 获取学科信息(从remark解析)
|
||||
if (StringUtils.isNotBlank(member.getRemark()) && member.getRemark().startsWith("subjectId:")) {
|
||||
try {
|
||||
Long subjectId = Long.parseLong(member.getRemark().replace("subjectId:", ""));
|
||||
vo.setSubjectId(subjectId);
|
||||
PgSubject subject = subjectMapper.selectById(subjectId);
|
||||
if (subject != null) {
|
||||
vo.setSubjectName(subject.getSubjectName());
|
||||
}
|
||||
} catch (Exception e) {
|
||||
// 忽略解析错误
|
||||
}
|
||||
}
|
||||
|
||||
return vo;
|
||||
}
|
||||
|
||||
/**
|
||||
* 校验学校信息
|
||||
*/
|
||||
private void validateSchoolInfo(Long schoolId, Long schoolGradeId, Long schoolClassId) {
|
||||
PgSchool school = schoolMapper.selectById(schoolId);
|
||||
if (school == null) {
|
||||
throw new ServiceException("学校不存在");
|
||||
}
|
||||
|
||||
PgSchoolGrade schoolGrade = schoolGradeMapper.selectById(schoolGradeId);
|
||||
if (schoolGrade == null || !schoolGrade.getSchoolId().equals(schoolId)) {
|
||||
throw new ServiceException("年级不存在或不属于该学校");
|
||||
}
|
||||
|
||||
PgSchoolClass schoolClass = schoolClassMapper.selectById(schoolClassId);
|
||||
if (schoolClass == null || !schoolClass.getSchoolGradeId().equals(schoolGradeId)) {
|
||||
throw new ServiceException("班级不存在或不属于该年级");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 填充学生名称信息
|
||||
*/
|
||||
private void fillStudentNames(H5StudentVo vo, PgStudent student) {
|
||||
// 学校名称
|
||||
if (student.getSchoolId() != null) {
|
||||
PgSchool school = schoolMapper.selectById(student.getSchoolId());
|
||||
if (school != null) {
|
||||
vo.setSchoolName(school.getSchoolName());
|
||||
}
|
||||
}
|
||||
|
||||
// 年级名称
|
||||
if (student.getSchoolGradeId() != null) {
|
||||
PgSchoolGrade schoolGrade = schoolGradeMapper.selectById(student.getSchoolGradeId());
|
||||
if (schoolGrade != null && schoolGrade.getGradeId() != null) {
|
||||
PgGrade grade = gradeMapper.selectById(schoolGrade.getGradeId());
|
||||
if (grade != null) {
|
||||
vo.setGradeName(grade.getGradeName());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 班级名称
|
||||
if (student.getSchoolClassId() != null) {
|
||||
PgSchoolClass schoolClass = schoolClassMapper.selectById(student.getSchoolClassId());
|
||||
if (schoolClass != null && schoolClass.getClassId() != null) {
|
||||
PgClass cls = classMapper.selectById(schoolClass.getClassId());
|
||||
if (cls != null) {
|
||||
vo.setClassName(cls.getClassName());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,314 @@
|
|||
# H5 会员管理接口需求与技术方案
|
||||
|
||||
> 作者:湖北新华业务中台研发团队
|
||||
> 创建时间:2026-02-02
|
||||
> 版本:v1.0
|
||||
|
||||
---
|
||||
|
||||
## 一、需求概述
|
||||
|
||||
为H5前端提供会员管理相关接口,包括认证、注册、个人信息管理、教育身份管理、学生绑定等功能。
|
||||
|
||||
---
|
||||
|
||||
## 二、接口需求设计
|
||||
|
||||
### 2.1 认证模块(OAuth 2.0)
|
||||
|
||||
#### 2.1.1 密码登录
|
||||
| 项目 | 说明 |
|
||||
|------|------|
|
||||
| **接口** | `POST /h5/auth/login/password` |
|
||||
| **请求参数** | phone(手机号)、password(密码)、captchaCode(图形验证码)、uuid(验证码标识)、rememberMe(记住我) |
|
||||
| **返回** | accessToken、refreshToken、expiresIn、会员基本信息 |
|
||||
|
||||
#### 2.1.2 短信验证码登录
|
||||
| 项目 | 说明 |
|
||||
|------|------|
|
||||
| **接口** | `POST /h5/auth/login/sms` |
|
||||
| **请求参数** | phone(手机号)、smsCode(短信验证码)、captchaCode(图形验证码)、uuid |
|
||||
| **短信平台** | **阿里云短信服务** |
|
||||
| **返回** | accessToken、refreshToken、expiresIn、会员基本信息 |
|
||||
|
||||
#### 2.1.3 获取图形验证码
|
||||
| 项目 | 说明 |
|
||||
|------|------|
|
||||
| **接口** | `GET /h5/auth/captcha` |
|
||||
| **返回** | uuid、captchaImg(Base64图片) |
|
||||
|
||||
#### 2.1.4 发送短信验证码(阿里云)
|
||||
| 项目 | 说明 |
|
||||
|------|------|
|
||||
| **接口** | `POST /h5/auth/sms/send` |
|
||||
| **请求参数** | phone、captchaCode、uuid、type(login/register) |
|
||||
| **短信平台** | **阿里云短信服务** |
|
||||
| **返回** | 发送结果、倒计时秒数(60秒) |
|
||||
|
||||
#### 2.1.5 刷新Token
|
||||
| 项目 | 说明 |
|
||||
|------|------|
|
||||
| **接口** | `POST /h5/auth/refresh` |
|
||||
| **请求参数** | refreshToken |
|
||||
| **返回** | 新的 accessToken、refreshToken |
|
||||
|
||||
#### 2.1.6 退出登录
|
||||
| 项目 | 说明 |
|
||||
|------|------|
|
||||
| **接口** | `POST /h5/auth/logout` |
|
||||
|
||||
---
|
||||
|
||||
### 2.2 注册模块
|
||||
|
||||
#### 2.2.1 注册 - 提交基本信息
|
||||
| 项目 | 说明 |
|
||||
|------|------|
|
||||
| **接口** | `POST /h5/auth/register` |
|
||||
| **请求参数** | phone、smsCode、captchaCode、uuid、password |
|
||||
| **短信平台** | **阿里云短信服务** |
|
||||
| **业务逻辑** | 1. 校验手机号未注册<br>2. 校验验证码<br>3. 创建会员账号<br>4. 自动登录返回Token |
|
||||
| **返回** | accessToken、refreshToken、memberId |
|
||||
|
||||
#### 2.2.2 完善身份信息(可选)
|
||||
| 项目 | 说明 |
|
||||
|------|------|
|
||||
| **接口** | `POST /h5/member/identity` |
|
||||
| **请求参数** | identityType(parent/teacher) |
|
||||
|
||||
---
|
||||
|
||||
### 2.3 会员信息模块
|
||||
|
||||
#### 2.3.1 获取当前会员信息
|
||||
| 项目 | 说明 |
|
||||
|------|------|
|
||||
| **接口** | `GET /h5/member/info` |
|
||||
| **返回** | memberId、phone(脱敏)、nickname、avatar、gender、birthday、registerTime、identityType、教育信息、绑定学生列表 |
|
||||
|
||||
#### 2.3.2 修改会员基本信息
|
||||
| 项目 | 说明 |
|
||||
|------|------|
|
||||
| **接口** | `PUT /h5/member/info` |
|
||||
| **可修改字段** | nickname(昵称)、gender(性别)、birthday(生日) |
|
||||
| **备注** | 手机号不可修改 |
|
||||
|
||||
---
|
||||
|
||||
### 2.4 密码管理
|
||||
|
||||
#### 2.4.1 修改密码
|
||||
| 项目 | 说明 |
|
||||
|------|------|
|
||||
| **接口** | `PUT /h5/member/password` |
|
||||
| **请求参数** | oldPassword(当前密码)、newPassword(新密码)、confirmPassword(确认新密码) |
|
||||
| **校验** | 新密码至少6位,包含字母、数字或符号 |
|
||||
|
||||
---
|
||||
|
||||
### 2.5 教育身份管理(教师身份)
|
||||
|
||||
#### 2.5.1 添加/修改教育身份
|
||||
| 项目 | 说明 |
|
||||
|------|------|
|
||||
| **接口** | `POST /h5/member/education` |
|
||||
| **请求参数** | schoolId(学校)、schoolGradeId(年级)、schoolClassId(班级)、subjectId(学科) |
|
||||
| **业务逻辑** | 设置会员身份为"教师",关联学校班级学科信息 |
|
||||
|
||||
#### 2.5.2 获取教育身份信息
|
||||
| 项目 | 说明 |
|
||||
|------|------|
|
||||
| **接口** | `GET /h5/member/education` |
|
||||
| **返回** | 学校、年级、班级、学科名称及ID |
|
||||
|
||||
#### 2.5.3 删除教育身份
|
||||
| 项目 | 说明 |
|
||||
|------|------|
|
||||
| **接口** | `DELETE /h5/member/education` |
|
||||
|
||||
---
|
||||
|
||||
### 2.6 绑定学生管理(家长/教师均可)
|
||||
|
||||
#### 2.6.1 绑定学生
|
||||
| 项目 | 说明 |
|
||||
|------|------|
|
||||
| **接口** | `POST /h5/member/student` |
|
||||
| **请求参数** | studentName(学生姓名)、studentNo(学号)、birthday(出生日期)、gender(性别)、regionId(地区)、schoolId、schoolGradeId、schoolClassId |
|
||||
| **业务逻辑** | 1. 创建学生记录<br>2. 关联当前会员<br>3. **家长和教师身份均可绑定学生** |
|
||||
|
||||
#### 2.6.2 获取绑定的学生列表
|
||||
| 项目 | 说明 |
|
||||
|------|------|
|
||||
| **接口** | `GET /h5/member/students` |
|
||||
| **返回** | 学生列表(含学校、年级、班级名称) |
|
||||
|
||||
#### 2.6.3 修改学生信息
|
||||
| 项目 | 说明 |
|
||||
|------|------|
|
||||
| **接口** | `PUT /h5/member/student/{studentId}` |
|
||||
| **请求参数** | 同绑定学生 |
|
||||
|
||||
#### 2.6.4 解绑学生
|
||||
| 项目 | 说明 |
|
||||
|------|------|
|
||||
| **接口** | `DELETE /h5/member/student/{studentId}` |
|
||||
|
||||
---
|
||||
|
||||
### 2.7 基础数据接口(公开)
|
||||
|
||||
| 接口 | 说明 |
|
||||
|------|------|
|
||||
| `GET /h5/base/regions` | 获取区域树(省市区) |
|
||||
| `GET /h5/base/schools?regionId=xxx` | 根据区域获取学校列表 |
|
||||
| `GET /h5/base/grades?schoolId=xxx` | 根据学校获取年级列表 |
|
||||
| `GET /h5/base/classes?schoolGradeId=xxx` | 根据年级获取班级列表 |
|
||||
| `GET /h5/base/subjects` | 获取学科列表 |
|
||||
|
||||
---
|
||||
|
||||
### 2.8 接口规范
|
||||
|
||||
| 规范项 | 说明 |
|
||||
|------|------|
|
||||
| **认证方式** | OAuth 2.0 Bearer Token,Header: `Authorization: Bearer {accessToken}` |
|
||||
| **Token有效期** | accessToken: 2小时,refreshToken: 7天(记住我30天) |
|
||||
| **响应格式** | `{ code: 200, msg: "success", data: {...} }` |
|
||||
| **错误码** | 401-未认证,403-无权限,10001-验证码错误,10002-密码错误,10003-手机号已注册 |
|
||||
| **短信服务** | 阿里云短信 |
|
||||
|
||||
---
|
||||
|
||||
## 三、技术方案
|
||||
|
||||
### 3.1 项目结构设计
|
||||
|
||||
在 `pangu-business` 模块下新建 `h5` 包:
|
||||
|
||||
```
|
||||
backend/ruoyi-modules/pangu-business/src/main/java/org/dromara/pangu/
|
||||
├── h5/ # H5接口模块
|
||||
│ ├── controller/
|
||||
│ │ ├── H5AuthController.java # 认证接口
|
||||
│ │ ├── H5MemberController.java # 会员信息接口
|
||||
│ │ └── H5BaseDataController.java # 基础数据接口
|
||||
│ ├── service/
|
||||
│ │ ├── H5AuthService.java # 认证服务接口
|
||||
│ │ ├── impl/
|
||||
│ │ │ └── H5AuthServiceImpl.java # 认证服务实现
|
||||
│ │ ├── H5MemberService.java # 会员服务接口
|
||||
│ │ └── impl/
|
||||
│ │ └── H5MemberServiceImpl.java # 会员服务实现
|
||||
│ └── domain/
|
||||
│ ├── dto/ # 请求DTO
|
||||
│ │ ├── H5PasswordLoginDto.java # 密码登录
|
||||
│ │ ├── H5SmsLoginDto.java # 短信登录
|
||||
│ │ ├── H5RegisterDto.java # 注册
|
||||
│ │ ├── H5SmsSendDto.java # 发送短信
|
||||
│ │ ├── H5MemberUpdateDto.java # 会员信息修改
|
||||
│ │ ├── H5PasswordUpdateDto.java # 密码修改
|
||||
│ │ ├── H5EducationDto.java # 教育身份
|
||||
│ │ └── H5StudentBindDto.java # 绑定学生
|
||||
│ └── vo/ # 响应VO
|
||||
│ ├── H5LoginVo.java # 登录响应
|
||||
│ ├── H5CaptchaVo.java # 验证码响应
|
||||
│ ├── H5MemberInfoVo.java # 会员信息
|
||||
│ ├── H5EducationVo.java # 教育身份
|
||||
│ └── H5StudentVo.java # 学生信息
|
||||
```
|
||||
|
||||
### 3.2 接口路径规划
|
||||
|
||||
| 模块 | 路径前缀 | 认证要求 |
|
||||
|------|---------|---------|
|
||||
| 认证接口 | `/h5/auth/*` | 无需认证 |
|
||||
| 会员接口 | `/h5/member/*` | 需要Token |
|
||||
| 基础数据 | `/h5/base/*` | 无需认证 |
|
||||
|
||||
### 3.3 认证方案(Sa-Token)
|
||||
|
||||
#### Token 策略
|
||||
|
||||
| 配置项 | 值 | 说明 |
|
||||
|--------|-----|------|
|
||||
| Token 存储 | Redis | 支持分布式 |
|
||||
| accessToken 有效期 | 2小时 | 常规访问 |
|
||||
| refreshToken 有效期 | 7天/30天 | 记住我时30天 |
|
||||
| Token 传递方式 | Header | `Authorization: Bearer {token}` |
|
||||
| 设备类型 | h5 | 区分多端登录 |
|
||||
|
||||
#### 安全配置
|
||||
|
||||
在 `SecurityConfig.java` 中添加H5接口放行规则:
|
||||
|
||||
```java
|
||||
// H5公开接口(无需认证)
|
||||
.excludePathPatterns(
|
||||
"/h5/auth/**", // 登录/注册/验证码
|
||||
"/h5/base/**" // 基础数据
|
||||
)
|
||||
```
|
||||
|
||||
### 3.4 短信服务(阿里云)
|
||||
|
||||
复用现有 `sms4j` 模块,配置阿里云:
|
||||
|
||||
```yaml
|
||||
sms:
|
||||
blends:
|
||||
alibaba:
|
||||
supplier: alibaba
|
||||
access-key-id: ${ALIYUN_SMS_ACCESS_KEY}
|
||||
access-key-secret: ${ALIYUN_SMS_ACCESS_SECRET}
|
||||
signature: 盘古教育
|
||||
template-id: SMS_XXXXXX
|
||||
```
|
||||
|
||||
短信验证码逻辑:
|
||||
1. 校验图形验证码
|
||||
2. 频率限制(60秒内不可重发)
|
||||
3. 生成6位数字验证码,5分钟有效
|
||||
4. 存入Redis:`h5:sms:{phone}:{type}`
|
||||
5. 调用阿里云API发送
|
||||
|
||||
### 3.5 数据库设计
|
||||
|
||||
复用现有表结构,无需新增表:
|
||||
- `pg_member` - 会员表
|
||||
- `pg_student` - 学生表
|
||||
- `pg_school` / `pg_school_grade` / `pg_school_class` - 学校相关
|
||||
- `pg_region` - 区域表
|
||||
- `pg_subject` - 学科表
|
||||
|
||||
### 3.6 依赖关系
|
||||
|
||||
```
|
||||
H5 Controller
|
||||
↓
|
||||
H5 Service(新建)
|
||||
↓
|
||||
复用现有 Service/Mapper
|
||||
├── IPgMemberService
|
||||
├── IPgStudentService
|
||||
├── IPgSchoolService
|
||||
└── 基础数据 Service
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 四、开发清单
|
||||
|
||||
| 序号 | 内容 | 文件 |
|
||||
|------|------|------|
|
||||
| 1 | DTO类 | H5PasswordLoginDto, H5SmsLoginDto, H5RegisterDto, H5SmsSendDto, H5MemberUpdateDto, H5PasswordUpdateDto, H5EducationDto, H5StudentBindDto |
|
||||
| 2 | VO类 | H5LoginVo, H5CaptchaVo, H5MemberInfoVo, H5EducationVo, H5StudentVo |
|
||||
| 3 | 认证服务 | H5AuthService, H5AuthServiceImpl |
|
||||
| 4 | 会员服务 | H5MemberService, H5MemberServiceImpl |
|
||||
| 5 | Controller | H5AuthController, H5MemberController, H5BaseDataController |
|
||||
| 6 | 安全配置 | SecurityConfig(添加放行规则) |
|
||||
| 7 | 短信配置 | application-dev.yml |
|
||||
|
||||
---
|
||||
|
||||
*最后更新: 2026-02-02*
|
||||
Loading…
Reference in New Issue