1350 lines
38 KiB
Markdown
1350 lines
38 KiB
Markdown
# 会员管理模块 - 后端详细设计
|
||
|
||
---
|
||
|
||
| 文档信息 | 内容 |
|
||
|---------|------|
|
||
| **文档版本** | 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 会员DTO(MemberDTO.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 会员VO(MemberVO.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 XML(MemberMapper.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 >= 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)
|
||
|
||
```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记录异常信息
|
||
|
||
---
|
||
|
||
*文档结束*
|