# 会员管理模块 - 后端详细设计

---

| 文档信息 | 内容 |
|---------|------|
| **文档版本** | 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 &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记录异常信息

---

*文档结束*
