38 KiB
38 KiB
会员管理模块 - 后端详细设计
| 文档信息 | 内容 |
|---|---|
| 文档版本 | 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)
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 会员DTO(MemberDTO.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 会员VO(MemberVO.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)
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)
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)
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 XML(MemberMapper.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 >= CONCAT(#{dto.beginTime}, ' 00:00:00')
</if>
<if test="dto.endTime != null and dto.endTime != ''">
AND m.register_time <= 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)
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)
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)
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 数据权限注解
/**
* 会员列表查询需要配置数据权限
* 根据不同角色过滤数据
*/
@DataScope(deptAlias = "s", userAlias = "m")
public TableDataInfo<MemberVO> selectMemberList(MemberDTO memberDTO) {
// 自动拼接数据权限SQL
}
6.2 数据权限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
pangu:
member:
# 默认密码
default-password: 123456
# 重置密码长度
reset-password-length: 8
# 会员编号前缀
code-prefix: JS
7.2 菜单权限配置
-- 会员管理菜单
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 开发注意事项
- 手机号唯一性:使用数据库唯一索引 + 业务层校验双重保障
- 密码安全:使用BCrypt加密,不存储明文密码
- 软删除:所有删除操作使用逻辑删除
- 数据权限:根据用户角色过滤数据
- 事务控制:涉及多表操作需要添加事务注解
8.2 性能优化
- 索引优化:为常用查询字段添加索引
- 分页查询:列表查询必须分页
- 关联查询:使用LEFT JOIN避免N+1问题
- 缓存使用:高频数据可考虑Redis缓存
8.3 日志记录
- 操作日志:使用@Log注解记录关键操作
- 业务日志:使用log.info记录业务关键节点
- 异常日志:使用log.error记录异常信息
文档结束