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

38 KiB
Raw Permalink Blame History

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


文档信息 内容
文档版本 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 会员DTOMemberDTO.java

package com.pangu.member.domain;

import com.pangu.common.core.domain.BaseDTO;
import lombok.Data;
import lombok.EqualsAndHashCode;

import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.Pattern;
import java.time.LocalDate;
import java.util.List;

/**
 * 会员数据传输对象
 * @author pangu
 */
@Data
@EqualsAndHashCode(callSuper = true)
public class MemberDTO extends BaseDTO {

    private static final long serialVersionUID = 1L;

    /** 会员ID */
    private Long memberId;

    /** 手机号 */
    @NotBlank(message = "手机号不能为空")
    @Pattern(regexp = "^1[3-9]\\d{9}$", message = "手机号格式不正确")
    private String phone;

    /** 昵称 */
    private String nickname;

    /** 性别0未知 1男 2女 */
    private String gender;

    /** 出生日期 */
    private LocalDate birthday;

    /** 身份类型1家长 2教师 */
    @NotBlank(message = "请选择身份类型")
    private String identityType;

    /** 所属区域ID教师必填 */
    private Long regionId;

    /** 所属学校ID教师必填 */
    private Long schoolId;

    /** 所属学校年级ID教师必填 */
    private Long schoolGradeId;

    /** 所属学校班级ID教师必填 */
    private Long schoolClassId;

    /** 状态0正常 1停用 */
    private String status;

    /** 绑定的学生ID列表新增时使用 */
    private List<Long> studentIds;

    // ========== 查询条件 ==========

    /** 注册开始时间 */
    private String beginTime;

    /** 注册结束时间 */
    private String endTime;
}

2.3 会员VOMemberVO.java

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 XMLMemberMapper.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

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 开发注意事项

  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记录异常信息

文档结束