pangu-user-platform/docs/05-模块技术方案/会员管理/会员管理后端详细设计_v1.0.md

1350 lines
38 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 会员管理模块 - 后端详细设计
---
| 文档信息 | 内容 |
|---------|------|
| **文档版本** | V1.0 |
| **模块名称** | 会员管理模块 - 后端 |
| **编写团队** | pangu |
| **创建日期** | 2026-01-31 |
---
## 1. 设计概述
### 1.1 技术选型
| 技术 | 版本 | 说明 |
|-----|------|------|
| Spring Boot | 3.3.x | 应用框架LTS版本 |
| Spring Security | 6.x | 安全框架 |
| MyBatis Plus | 3.5.x | ORM框架 |
| JWT | 0.12.x | Token认证 |
| Hutool | 5.x | 工具库 |
| Lombok | - | 简化代码 |
| JDK | 17+ | 运行环境LTS |
| MySQL | 8.0+ | 数据库 |
| Redis | 7.x | 缓存 |
### 1.2 模块结构
```
pangu-admin/
└── src/main/java/com/pangu/
└── member/
├── controller/
│ └── MemberController.java # 会员管理控制器
├── service/
│ ├── IMemberService.java # 会员服务接口
│ └── impl/
│ └── MemberServiceImpl.java # 会员服务实现
├── mapper/
│ └── MemberMapper.java # 会员数据访问
├── domain/
│ ├── Member.java # 会员实体
│ ├── MemberVO.java # 会员视图对象
│ └── MemberDTO.java # 会员数据传输对象
└── enums/
├── IdentityTypeEnum.java # 身份类型枚举
└── RegisterSourceEnum.java # 注册来源枚举
pangu-admin/
└── src/main/resources/
└── mapper/
└── member/
└── MemberMapper.xml # Mapper映射文件
```
---
## 2. 实体设计
### 2.1 会员实体Member.java
```java
package com.pangu.member.domain;
import com.baomidou.mybatisplus.annotation.*;
import lombok.Data;
import lombok.EqualsAndHashCode;
import com.pangu.common.core.domain.BaseEntity;
import java.time.LocalDate;
import java.time.LocalDateTime;
/**
* 会员实体
* @author pangu
*/
@Data
@EqualsAndHashCode(callSuper = true)
@TableName("pg_member")
public class Member extends BaseEntity {
private static final long serialVersionUID = 1L;
/** 会员ID */
@TableId(type = IdType.AUTO)
private Long memberId;
/** 会员编号 */
private String memberCode;
/** 手机号 */
private String phone;
/** 密码 */
private String password;
/** 昵称 */
private String nickname;
/** 头像URL */
private String avatar;
/** 性别0未知 1男 2女 */
private String gender;
/** 出生日期 */
private LocalDate birthday;
/** 身份类型1家长 2教师 */
private String identityType;
/** 微信OpenID */
private String openId;
/** 微信UnionID */
private String unionId;
/** 所属区域ID教师必填 */
private Long regionId;
/** 所属学校ID教师必填 */
private Long schoolId;
/** 所属学校年级ID教师必填 */
private Long schoolGradeId;
/** 所属学校班级ID教师必填 */
private Long schoolClassId;
/** 注册来源1小程序 2H5 3后台 4导入 */
private String registerSource;
/** 注册时间 */
private LocalDateTime registerTime;
/** 最后登录时间 */
private LocalDateTime lastLoginTime;
/** 最后登录IP */
private String lastLoginIp;
/** 登录次数 */
private Integer loginCount;
/** 状态0正常 1停用 */
private String status;
/** 删除标志0存在 1删除 */
@TableLogic
private String delFlag;
}
```
### 2.2 会员DTOMemberDTO.java
```java
package com.pangu.member.domain;
import com.pangu.common.core.domain.BaseDTO;
import lombok.Data;
import lombok.EqualsAndHashCode;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.Pattern;
import java.time.LocalDate;
import java.util.List;
/**
* 会员数据传输对象
* @author pangu
*/
@Data
@EqualsAndHashCode(callSuper = true)
public class MemberDTO extends BaseDTO {
private static final long serialVersionUID = 1L;
/** 会员ID */
private Long memberId;
/** 手机号 */
@NotBlank(message = "手机号不能为空")
@Pattern(regexp = "^1[3-9]\\d{9}$", message = "手机号格式不正确")
private String phone;
/** 昵称 */
private String nickname;
/** 性别0未知 1男 2女 */
private String gender;
/** 出生日期 */
private LocalDate birthday;
/** 身份类型1家长 2教师 */
@NotBlank(message = "请选择身份类型")
private String identityType;
/** 所属区域ID教师必填 */
private Long regionId;
/** 所属学校ID教师必填 */
private Long schoolId;
/** 所属学校年级ID教师必填 */
private Long schoolGradeId;
/** 所属学校班级ID教师必填 */
private Long schoolClassId;
/** 状态0正常 1停用 */
private String status;
/** 绑定的学生ID列表新增时使用 */
private List<Long> studentIds;
// ========== 查询条件 ==========
/** 注册开始时间 */
private String beginTime;
/** 注册结束时间 */
private String endTime;
}
```
### 2.3 会员VOMemberVO.java
```java
package com.pangu.member.domain;
import com.fasterxml.jackson.annotation.JsonFormat;
import lombok.Data;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.util.List;
/**
* 会员视图对象
* @author pangu
*/
@Data
public class MemberVO {
/** 会员ID */
private Long memberId;
/** 会员编号 */
private String memberCode;
/** 手机号(脱敏) */
private String phone;
/** 手机号(完整,编辑时使用) */
private String phoneFull;
/** 昵称 */
private String nickname;
/** 头像URL */
private String avatar;
/** 性别代码 */
private String gender;
/** 性别名称 */
private String genderName;
/** 出生日期 */
@JsonFormat(pattern = "yyyy-MM-dd")
private LocalDate birthday;
/** 身份类型代码 */
private String identityType;
/** 身份类型名称 */
private String identityTypeName;
/** 微信OpenID */
private String openId;
/** 所属区域ID */
private Long regionId;
/** 区域路径(如:湖北省-武汉市-武昌区) */
private String regionPath;
/** 所属学校ID */
private Long schoolId;
/** 学校名称 */
private String schoolName;
/** 所属学校年级ID */
private Long schoolGradeId;
/** 年级名称 */
private String gradeName;
/** 所属学校班级ID */
private Long schoolClassId;
/** 班级名称 */
private String className;
/** 注册来源代码 */
private String registerSource;
/** 注册来源名称 */
private String registerSourceName;
/** 注册时间 */
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private LocalDateTime registerTime;
/** 状态 */
private String status;
/** 绑定的学生列表 */
private List<StudentVO> students;
/**
* 学生视图对象
*/
@Data
public static class StudentVO {
/** 学生ID */
private Long studentId;
/** 学生姓名 */
private String studentName;
/** 学号 */
private String studentNo;
/** 学校名称 */
private String schoolName;
/** 年级名称 */
private String gradeName;
/** 班级名称 */
private String className;
}
}
```
### 2.4 枚举类
#### 身份类型枚举IdentityTypeEnum.java
```java
package com.pangu.member.enums;
import lombok.AllArgsConstructor;
import lombok.Getter;
/**
* 身份类型枚举
* @author pangu
*/
@Getter
@AllArgsConstructor
public enum IdentityTypeEnum {
PARENT("1", "家长"),
TEACHER("2", "教师");
private final String code;
private final String name;
public static String getNameByCode(String code) {
for (IdentityTypeEnum type : values()) {
if (type.getCode().equals(code)) {
return type.getName();
}
}
return "";
}
public static boolean isTeacher(String code) {
return TEACHER.getCode().equals(code);
}
public static boolean isParent(String code) {
return PARENT.getCode().equals(code);
}
}
```
#### 注册来源枚举RegisterSourceEnum.java
```java
package com.pangu.member.enums;
import lombok.AllArgsConstructor;
import lombok.Getter;
/**
* 注册来源枚举
* @author pangu
*/
@Getter
@AllArgsConstructor
public enum RegisterSourceEnum {
MINI_PROGRAM("1", "小程序"),
H5("2", "H5"),
BACKEND("3", "后台新增"),
IMPORT("4", "批量导入");
private final String code;
private final String name;
public static String getNameByCode(String code) {
for (RegisterSourceEnum source : values()) {
if (source.getCode().equals(code)) {
return source.getName();
}
}
return "";
}
}
```
---
## 3. 数据访问层
### 3.1 Mapper接口MemberMapper.java
```java
package com.pangu.member.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.pangu.member.domain.Member;
import com.pangu.member.domain.MemberDTO;
import com.pangu.member.domain.MemberVO;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import java.util.List;
/**
* 会员数据访问接口
* @author pangu
*/
@Mapper
public interface MemberMapper extends BaseMapper<Member> {
/**
* 查询会员列表(带关联信息)
* @param page 分页对象
* @param dto 查询条件
* @return 会员列表
*/
List<MemberVO> selectMemberVOList(Page<MemberVO> page, @Param("dto") MemberDTO dto);
/**
* 根据ID查询会员详情带关联信息
* @param memberId 会员ID
* @return 会员详情
*/
MemberVO selectMemberVOById(@Param("memberId") Long memberId);
/**
* 根据手机号查询会员数量排除指定ID
* @param phone 手机号
* @param memberId 排除的会员ID可为空
* @return 数量
*/
int countByPhone(@Param("phone") String phone, @Param("memberId") Long memberId);
/**
* 根据手机号查询会员
* @param phone 手机号
* @return 会员信息
*/
Member selectByPhone(@Param("phone") String phone);
/**
* 根据OpenID查询会员
* @param openId 微信OpenID
* @return 会员信息
*/
Member selectByOpenId(@Param("openId") String openId);
}
```
### 3.2 Mapper XMLMemberMapper.xml
```xml
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.pangu.member.mapper.MemberMapper">
<resultMap id="MemberVOResult" type="com.pangu.member.domain.MemberVO">
<id property="memberId" column="member_id"/>
<result property="memberCode" column="member_code"/>
<result property="phone" column="phone"/>
<result property="phoneFull" column="phone_full"/>
<result property="nickname" column="nickname"/>
<result property="avatar" column="avatar"/>
<result property="gender" column="gender"/>
<result property="birthday" column="birthday"/>
<result property="identityType" column="identity_type"/>
<result property="openId" column="open_id"/>
<result property="regionId" column="region_id"/>
<result property="regionPath" column="region_path"/>
<result property="schoolId" column="school_id"/>
<result property="schoolName" column="school_name"/>
<result property="schoolGradeId" column="school_grade_id"/>
<result property="gradeName" column="grade_name"/>
<result property="schoolClassId" column="school_class_id"/>
<result property="className" column="class_name"/>
<result property="registerSource" column="register_source"/>
<result property="registerTime" column="register_time"/>
<result property="status" column="status"/>
</resultMap>
<sql id="selectMemberVOColumns">
m.member_id,
m.member_code,
CONCAT(LEFT(m.phone, 3), '****', RIGHT(m.phone, 4)) AS phone,
m.phone AS phone_full,
m.nickname,
m.avatar,
m.gender,
m.birthday,
m.identity_type,
m.open_id,
m.region_id,
m.school_id,
m.school_grade_id,
m.school_class_id,
m.register_source,
m.register_time,
m.status,
s.school_name,
s.region_path,
g.grade_name,
c.class_name
</sql>
<!-- 查询会员列表 -->
<select id="selectMemberVOList" resultMap="MemberVOResult">
SELECT <include refid="selectMemberVOColumns"/>
FROM pg_member m
LEFT JOIN pg_school s ON m.school_id = s.school_id AND s.del_flag = '0'
LEFT JOIN pg_school_grade sg ON m.school_grade_id = sg.id AND sg.del_flag = '0'
LEFT JOIN pg_grade g ON sg.grade_id = g.grade_id AND g.del_flag = '0'
LEFT JOIN pg_school_class sc ON m.school_class_id = sc.id AND sc.del_flag = '0'
LEFT JOIN pg_class c ON sc.class_id = c.class_id AND c.del_flag = '0'
WHERE m.del_flag = '0'
<if test="dto.phone != null and dto.phone != ''">
AND m.phone LIKE CONCAT('%', #{dto.phone}, '%')
</if>
<if test="dto.nickname != null and dto.nickname != ''">
AND m.nickname LIKE CONCAT('%', #{dto.nickname}, '%')
</if>
<if test="dto.identityType != null and dto.identityType != ''">
AND m.identity_type = #{dto.identityType}
</if>
<if test="dto.status != null and dto.status != ''">
AND m.status = #{dto.status}
</if>
<if test="dto.beginTime != null and dto.beginTime != ''">
AND m.register_time &gt;= CONCAT(#{dto.beginTime}, ' 00:00:00')
</if>
<if test="dto.endTime != null and dto.endTime != ''">
AND m.register_time &lt;= CONCAT(#{dto.endTime}, ' 23:59:59')
</if>
<if test="dto.regionId != null">
AND m.region_id = #{dto.regionId}
</if>
<if test="dto.schoolId != null">
AND m.school_id = #{dto.schoolId}
</if>
<!-- 数据权限过滤 -->
${dto.params.dataScope}
ORDER BY m.register_time DESC
</select>
<!-- 根据ID查询会员详情 -->
<select id="selectMemberVOById" resultMap="MemberVOResult">
SELECT <include refid="selectMemberVOColumns"/>
FROM pg_member m
LEFT JOIN pg_school s ON m.school_id = s.school_id AND s.del_flag = '0'
LEFT JOIN pg_school_grade sg ON m.school_grade_id = sg.id AND sg.del_flag = '0'
LEFT JOIN pg_grade g ON sg.grade_id = g.grade_id AND g.del_flag = '0'
LEFT JOIN pg_school_class sc ON m.school_class_id = sc.id AND sc.del_flag = '0'
LEFT JOIN pg_class c ON sc.class_id = c.class_id AND c.del_flag = '0'
WHERE m.member_id = #{memberId}
AND m.del_flag = '0'
</select>
<!-- 根据手机号查询数量 -->
<select id="countByPhone" resultType="int">
SELECT COUNT(1)
FROM pg_member
WHERE phone = #{phone}
AND del_flag = '0'
<if test="memberId != null">
AND member_id != #{memberId}
</if>
</select>
<!-- 根据手机号查询会员 -->
<select id="selectByPhone" resultType="com.pangu.member.domain.Member">
SELECT * FROM pg_member
WHERE phone = #{phone}
AND del_flag = '0'
LIMIT 1
</select>
<!-- 根据OpenID查询会员 -->
<select id="selectByOpenId" resultType="com.pangu.member.domain.Member">
SELECT * FROM pg_member
WHERE open_id = #{openId}
AND del_flag = '0'
LIMIT 1
</select>
</mapper>
```
---
## 4. 服务层
### 4.1 服务接口IMemberService.java
```java
package com.pangu.member.service;
import com.baomidou.mybatisplus.extension.service.IService;
import com.pangu.common.core.page.TableDataInfo;
import com.pangu.member.domain.Member;
import com.pangu.member.domain.MemberDTO;
import com.pangu.member.domain.MemberVO;
/**
* 会员服务接口
* @author pangu
*/
public interface IMemberService extends IService<Member> {
/**
* 查询会员列表
* @param memberDTO 查询条件
* @return 分页结果
*/
TableDataInfo<MemberVO> selectMemberList(MemberDTO memberDTO);
/**
* 根据ID获取会员详情
* @param memberId 会员ID
* @return 会员详情
*/
MemberVO getMemberById(Long memberId);
/**
* 新增会员
* @param memberDTO 会员信息
* @return 结果
*/
int insertMember(MemberDTO memberDTO);
/**
* 修改会员
* @param memberDTO 会员信息
* @return 结果
*/
int updateMember(MemberDTO memberDTO);
/**
* 删除会员
* @param memberId 会员ID
* @return 结果
*/
int deleteMember(Long memberId);
/**
* 重置密码
* @param memberId 会员ID
* @return 新密码
*/
String resetPassword(Long memberId);
/**
* 修改会员状态
* @param memberId 会员ID
* @param status 状态
* @return 结果
*/
int changeStatus(Long memberId, String status);
/**
* 绑定学生
* @param memberId 会员ID
* @param studentId 学生ID
* @return 结果
*/
int bindStudent(Long memberId, Long studentId);
/**
* 解绑学生
* @param memberId 会员ID
* @param studentId 学生ID
* @return 结果
*/
int unbindStudent(Long memberId, Long studentId);
/**
* 检查手机号是否唯一
* @param phone 手机号
* @param memberId 会员ID编辑时排除自己
* @return 是否唯一
*/
boolean checkPhoneUnique(String phone, Long memberId);
/**
* 校验会员是否可删除
* @param memberId 会员ID
* @return 是否可删除
*/
boolean checkCanDelete(Long memberId);
/**
* 根据手机号查询会员
* @param phone 手机号
* @return 会员信息
*/
Member getMemberByPhone(String phone);
/**
* 根据OpenID查询会员
* @param openId 微信OpenID
* @return 会员信息
*/
Member getMemberByOpenId(String openId);
/**
* 创建会员(批量导入时使用)
* @param phone 手机号
* @param identityType 身份类型
* @return 会员ID
*/
Long createMemberForImport(String phone, String identityType);
}
```
### 4.2 服务实现MemberServiceImpl.java
```java
package com.pangu.member.service.impl;
import cn.hutool.core.util.RandomUtil;
import cn.hutool.core.util.StrUtil;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.pangu.common.core.page.TableDataInfo;
import com.pangu.common.exception.ServiceException;
import com.pangu.common.utils.SecurityUtils;
import com.pangu.member.domain.Member;
import com.pangu.member.domain.MemberDTO;
import com.pangu.member.domain.MemberVO;
import com.pangu.member.enums.IdentityTypeEnum;
import com.pangu.member.enums.RegisterSourceEnum;
import com.pangu.member.mapper.MemberMapper;
import com.pangu.member.service.IMemberService;
import com.pangu.student.service.IStudentService;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.time.LocalDateTime;
import java.util.List;
/**
* 会员服务实现
* @author pangu
*/
@Slf4j
@Service
@RequiredArgsConstructor
public class MemberServiceImpl extends ServiceImpl<MemberMapper, Member> implements IMemberService {
private final MemberMapper memberMapper;
private final IStudentService studentService;
/** 默认密码 */
private static final String DEFAULT_PASSWORD = "123456";
/** 重置密码长度 */
private static final int RESET_PASSWORD_LENGTH = 8;
@Override
public TableDataInfo<MemberVO> selectMemberList(MemberDTO memberDTO) {
// 创建分页对象
Page<MemberVO> page = new Page<>(memberDTO.getPageNum(), memberDTO.getPageSize());
// 查询数据
List<MemberVO> list = memberMapper.selectMemberVOList(page, memberDTO);
// 填充名称字段
list.forEach(this::fillMemberVONames);
return TableDataInfo.build(list, page.getTotal());
}
@Override
public MemberVO getMemberById(Long memberId) {
MemberVO memberVO = memberMapper.selectMemberVOById(memberId);
if (memberVO == null) {
throw new ServiceException("会员不存在");
}
// 填充名称
fillMemberVONames(memberVO);
// 查询绑定的学生
memberVO.setStudents(studentService.selectStudentVOsByMemberId(memberId));
return memberVO;
}
@Override
@Transactional(rollbackFor = Exception.class)
public int insertMember(MemberDTO memberDTO) {
// 校验手机号唯一性
if (!checkPhoneUnique(memberDTO.getPhone(), null)) {
throw new ServiceException("手机号已存在");
}
// 校验教师信息完整性
if (IdentityTypeEnum.isTeacher(memberDTO.getIdentityType())) {
validateTeacherInfo(memberDTO);
}
// 构建会员对象
Member member = buildMember(memberDTO);
member.setMemberCode(generateMemberCode());
member.setPassword(SecurityUtils.encryptPassword(DEFAULT_PASSWORD));
member.setRegisterSource(RegisterSourceEnum.BACKEND.getCode());
member.setRegisterTime(LocalDateTime.now());
member.setLoginCount(0);
member.setStatus("0");
// 自动生成昵称
if (StrUtil.isBlank(member.getNickname())) {
member.setNickname(generateNickname(member.getPhone()));
}
int result = memberMapper.insert(member);
// 绑定学生(如果有)
if (memberDTO.getStudentIds() != null && !memberDTO.getStudentIds().isEmpty()) {
for (Long studentId : memberDTO.getStudentIds()) {
studentService.updateStudentMember(studentId, member.getMemberId());
}
}
log.info("新增会员成功, memberId={}, phone={}", member.getMemberId(), member.getPhone());
return result;
}
@Override
@Transactional(rollbackFor = Exception.class)
public int updateMember(MemberDTO memberDTO) {
Member existMember = memberMapper.selectById(memberDTO.getMemberId());
if (existMember == null) {
throw new ServiceException("会员不存在");
}
// 校验手机号唯一性
if (!checkPhoneUnique(memberDTO.getPhone(), memberDTO.getMemberId())) {
throw new ServiceException("手机号已存在");
}
// 校验教师信息完整性
if (IdentityTypeEnum.isTeacher(memberDTO.getIdentityType())) {
validateTeacherInfo(memberDTO);
}
// 更新会员信息
Member member = buildMember(memberDTO);
member.setMemberId(memberDTO.getMemberId());
// 如果从教师改为家长,清空学校信息
if (IdentityTypeEnum.isParent(memberDTO.getIdentityType())) {
member.setRegionId(null);
member.setSchoolId(null);
member.setSchoolGradeId(null);
member.setSchoolClassId(null);
}
int result = memberMapper.updateById(member);
log.info("更新会员成功, memberId={}", member.getMemberId());
return result;
}
@Override
@Transactional(rollbackFor = Exception.class)
public int deleteMember(Long memberId) {
// 检查是否可删除
if (!checkCanDelete(memberId)) {
throw new ServiceException("该会员已绑定学生,请先解绑学生后再删除");
}
int result = memberMapper.deleteById(memberId);
log.info("删除会员成功, memberId={}", memberId);
return result;
}
@Override
public String resetPassword(Long memberId) {
Member member = memberMapper.selectById(memberId);
if (member == null) {
throw new ServiceException("会员不存在");
}
// 生成随机密码
String newPassword = RandomUtil.randomString(RESET_PASSWORD_LENGTH);
// 更新密码
Member updateMember = new Member();
updateMember.setMemberId(memberId);
updateMember.setPassword(SecurityUtils.encryptPassword(newPassword));
memberMapper.updateById(updateMember);
log.info("重置会员密码成功, memberId={}", memberId);
return newPassword;
}
@Override
public int changeStatus(Long memberId, String status) {
Member member = new Member();
member.setMemberId(memberId);
member.setStatus(status);
int result = memberMapper.updateById(member);
log.info("修改会员状态成功, memberId={}, status={}", memberId, status);
return result;
}
@Override
@Transactional(rollbackFor = Exception.class)
public int bindStudent(Long memberId, Long studentId) {
Member member = memberMapper.selectById(memberId);
if (member == null) {
throw new ServiceException("会员不存在");
}
// 教师只能绑定本校学生
if (IdentityTypeEnum.isTeacher(member.getIdentityType())) {
if (!studentService.isStudentInSchool(studentId, member.getSchoolId())) {
throw new ServiceException("教师只能绑定本校学生");
}
}
int result = studentService.updateStudentMember(studentId, memberId);
log.info("绑定学生成功, memberId={}, studentId={}", memberId, studentId);
return result;
}
@Override
@Transactional(rollbackFor = Exception.class)
public int unbindStudent(Long memberId, Long studentId) {
int result = studentService.unbindStudent(studentId, memberId);
log.info("解绑学生成功, memberId={}, studentId={}", memberId, studentId);
return result;
}
@Override
public boolean checkPhoneUnique(String phone, Long memberId) {
return memberMapper.countByPhone(phone, memberId) == 0;
}
@Override
public boolean checkCanDelete(Long memberId) {
// 检查是否有绑定的学生
return studentService.countByMemberId(memberId) == 0;
}
@Override
public Member getMemberByPhone(String phone) {
return memberMapper.selectByPhone(phone);
}
@Override
public Member getMemberByOpenId(String openId) {
return memberMapper.selectByOpenId(openId);
}
@Override
@Transactional(rollbackFor = Exception.class)
public Long createMemberForImport(String phone, String identityType) {
// 检查手机号是否已存在
Member existMember = getMemberByPhone(phone);
if (existMember != null) {
return existMember.getMemberId();
}
// 创建新会员
Member member = new Member();
member.setMemberCode(generateMemberCode());
member.setPhone(phone);
member.setPassword(SecurityUtils.encryptPassword(DEFAULT_PASSWORD));
member.setNickname(generateNickname(phone));
member.setGender("0");
member.setIdentityType(identityType);
member.setRegisterSource(RegisterSourceEnum.IMPORT.getCode());
member.setRegisterTime(LocalDateTime.now());
member.setLoginCount(0);
member.setStatus("0");
memberMapper.insert(member);
log.info("批量导入创建会员, memberId={}, phone={}", member.getMemberId(), phone);
return member.getMemberId();
}
/**
* 构建会员对象
*/
private Member buildMember(MemberDTO dto) {
Member member = new Member();
member.setPhone(dto.getPhone());
member.setNickname(dto.getNickname());
member.setGender(dto.getGender());
member.setBirthday(dto.getBirthday());
member.setIdentityType(dto.getIdentityType());
member.setRegionId(dto.getRegionId());
member.setSchoolId(dto.getSchoolId());
member.setSchoolGradeId(dto.getSchoolGradeId());
member.setSchoolClassId(dto.getSchoolClassId());
member.setStatus(dto.getStatus());
return member;
}
/**
* 校验教师信息完整性
*/
private void validateTeacherInfo(MemberDTO memberDTO) {
if (memberDTO.getRegionId() == null) {
throw new ServiceException("请选择所属区域");
}
if (memberDTO.getSchoolId() == null) {
throw new ServiceException("请选择所属学校");
}
if (memberDTO.getSchoolGradeId() == null) {
throw new ServiceException("请选择所属年级");
}
if (memberDTO.getSchoolClassId() == null) {
throw new ServiceException("请选择所属班级");
}
}
/**
* 生成会员编号
* 格式JS + 时间戳
*/
private String generateMemberCode() {
return "JS" + System.currentTimeMillis();
}
/**
* 生成默认昵称
*/
private String generateNickname(String phone) {
if (StrUtil.isBlank(phone) || phone.length() < 4) {
return "用户" + RandomUtil.randomNumbers(4);
}
return "用户" + phone.substring(phone.length() - 4);
}
/**
* 填充VO的名称字段
*/
private void fillMemberVONames(MemberVO vo) {
// 性别名称
vo.setGenderName(getGenderName(vo.getGender()));
// 身份类型名称
vo.setIdentityTypeName(IdentityTypeEnum.getNameByCode(vo.getIdentityType()));
// 注册来源名称
vo.setRegisterSourceName(RegisterSourceEnum.getNameByCode(vo.getRegisterSource()));
}
/**
* 获取性别名称
*/
private String getGenderName(String gender) {
return switch (gender) {
case "1" -> "男";
case "2" -> "女";
default -> "未知";
};
}
}
```
---
## 5. 控制器层
### 5.1 会员控制器MemberController.java
```java
package com.pangu.member.controller;
import com.pangu.common.annotation.Log;
import com.pangu.common.core.controller.BaseController;
import com.pangu.common.core.domain.AjaxResult;
import com.pangu.common.core.page.TableDataInfo;
import com.pangu.common.enums.BusinessType;
import com.pangu.member.domain.MemberDTO;
import com.pangu.member.domain.MemberVO;
import com.pangu.member.service.IMemberService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.RequiredArgsConstructor;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
/**
* 会员管理控制器
* @author pangu
*/
@Tag(name = "会员管理")
@RestController
@RequestMapping("/member")
@RequiredArgsConstructor
public class MemberController extends BaseController {
private final IMemberService memberService;
/**
* 查询会员列表
*/
@Operation(summary = "查询会员列表")
@PreAuthorize("@ss.hasPermi('user:member:list')")
@GetMapping("/list")
public TableDataInfo<MemberVO> list(MemberDTO memberDTO) {
startPage();
return memberService.selectMemberList(memberDTO);
}
/**
* 获取会员详情
*/
@Operation(summary = "获取会员详情")
@PreAuthorize("@ss.hasPermi('user:member:query')")
@GetMapping("/{memberId}")
public AjaxResult getInfo(
@Parameter(description = "会员ID") @PathVariable Long memberId) {
return success(memberService.getMemberById(memberId));
}
/**
* 新增会员
*/
@Operation(summary = "新增会员")
@PreAuthorize("@ss.hasPermi('user:member:add')")
@Log(title = "会员管理", businessType = BusinessType.INSERT)
@PostMapping
public AjaxResult add(@Validated @RequestBody MemberDTO memberDTO) {
return toAjax(memberService.insertMember(memberDTO));
}
/**
* 修改会员
*/
@Operation(summary = "修改会员")
@PreAuthorize("@ss.hasPermi('user:member:edit')")
@Log(title = "会员管理", businessType = BusinessType.UPDATE)
@PutMapping
public AjaxResult edit(@Validated @RequestBody MemberDTO memberDTO) {
return toAjax(memberService.updateMember(memberDTO));
}
/**
* 删除会员
*/
@Operation(summary = "删除会员")
@PreAuthorize("@ss.hasPermi('user:member:remove')")
@Log(title = "会员管理", businessType = BusinessType.DELETE)
@DeleteMapping("/{memberId}")
public AjaxResult remove(
@Parameter(description = "会员ID") @PathVariable Long memberId) {
return toAjax(memberService.deleteMember(memberId));
}
/**
* 重置密码
*/
@Operation(summary = "重置密码")
@PreAuthorize("@ss.hasPermi('user:member:resetPwd')")
@Log(title = "会员管理", businessType = BusinessType.UPDATE)
@PutMapping("/resetPwd/{memberId}")
public AjaxResult resetPwd(
@Parameter(description = "会员ID") @PathVariable Long memberId) {
String newPassword = memberService.resetPassword(memberId);
return AjaxResult.success("密码重置成功").put("password", newPassword);
}
/**
* 修改状态
*/
@Operation(summary = "修改状态")
@PreAuthorize("@ss.hasPermi('user:member:edit')")
@Log(title = "会员管理", businessType = BusinessType.UPDATE)
@PutMapping("/changeStatus")
public AjaxResult changeStatus(@RequestBody MemberDTO memberDTO) {
return toAjax(memberService.changeStatus(memberDTO.getMemberId(), memberDTO.getStatus()));
}
/**
* 绑定学生
*/
@Operation(summary = "绑定学生")
@PreAuthorize("@ss.hasPermi('user:member:edit')")
@Log(title = "会员管理", businessType = BusinessType.UPDATE)
@PostMapping("/bindStudent")
public AjaxResult bindStudent(@RequestBody MemberDTO memberDTO) {
return toAjax(memberService.bindStudent(memberDTO.getMemberId(), memberDTO.getStudentId()));
}
/**
* 解绑学生
*/
@Operation(summary = "解绑学生")
@PreAuthorize("@ss.hasPermi('user:member:edit')")
@Log(title = "会员管理", businessType = BusinessType.UPDATE)
@DeleteMapping("/unbindStudent/{memberId}/{studentId}")
public AjaxResult unbindStudent(
@Parameter(description = "会员ID") @PathVariable Long memberId,
@Parameter(description = "学生ID") @PathVariable Long studentId) {
return toAjax(memberService.unbindStudent(memberId, studentId));
}
/**
* 检查手机号是否唯一
*/
@Operation(summary = "检查手机号唯一性")
@GetMapping("/checkPhone")
public AjaxResult checkPhoneUnique(
@Parameter(description = "手机号") @RequestParam String phone,
@Parameter(description = "会员ID") @RequestParam(required = false) Long memberId) {
boolean unique = memberService.checkPhoneUnique(phone, memberId);
return AjaxResult.success().put("unique", unique);
}
}
```
---
## 6. 数据权限配置
### 6.1 数据权限注解
```java
/**
* 会员列表查询需要配置数据权限
* 根据不同角色过滤数据
*/
@DataScope(deptAlias = "s", userAlias = "m")
public TableDataInfo<MemberVO> selectMemberList(MemberDTO memberDTO) {
// 自动拼接数据权限SQL
}
```
### 6.2 数据权限SQL
```sql
-- 超级管理员:无限制
-- 分公司用户:按区域过滤
AND m.region_id IN (SELECT region_id FROM sys_user_region WHERE user_id = #{userId})
-- 学校用户:按学校过滤
AND m.school_id = #{userSchoolId} AND m.identity_type = '2'
```
---
## 7. 配置项
### 7.1 application.yml
```yaml
pangu:
member:
# 默认密码
default-password: 123456
# 重置密码长度
reset-password-length: 8
# 会员编号前缀
code-prefix: JS
```
### 7.2 菜单权限配置
```sql
-- 会员管理菜单
INSERT INTO sys_menu (menu_name, parent_id, order_num, path, component, menu_type, perms, icon) VALUES
('会员管理', 2000, 1, 'member', 'user/member/index', 'C', 'user:member:list', 'peoples');
-- 按钮权限
INSERT INTO sys_menu (menu_name, parent_id, order_num, perms, menu_type) VALUES
('会员查询', 2010, 1, 'user:member:query', 'F'),
('会员新增', 2010, 2, 'user:member:add', 'F'),
('会员修改', 2010, 3, 'user:member:edit', 'F'),
('会员删除', 2010, 4, 'user:member:remove', 'F'),
('重置密码', 2010, 5, 'user:member:resetPwd', 'F');
```
---
## 8. 注意事项
### 8.1 开发注意事项
1. **手机号唯一性**:使用数据库唯一索引 + 业务层校验双重保障
2. **密码安全**使用BCrypt加密不存储明文密码
3. **软删除**:所有删除操作使用逻辑删除
4. **数据权限**:根据用户角色过滤数据
5. **事务控制**:涉及多表操作需要添加事务注解
### 8.2 性能优化
1. **索引优化**:为常用查询字段添加索引
2. **分页查询**:列表查询必须分页
3. **关联查询**使用LEFT JOIN避免N+1问题
4. **缓存使用**高频数据可考虑Redis缓存
### 8.3 日志记录
1. **操作日志**:使用@Log注解记录关键操作
2. **业务日志**使用log.info记录业务关键节点
3. **异常日志**使用log.error记录异常信息
---
*文档结束*