2026-01-31 17:55:58 +08:00
|
|
|
|
# 会员管理模块 - 后端详细设计
|
|
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
|
|
|
|
|
| 文档信息 | 内容 |
|
|
|
|
|
|
|---------|------|
|
|
|
|
|
|
| **文档版本** | 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;
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 会员实体
|
2026-01-31 23:14:11 +08:00
|
|
|
|
* @author pangu
|
2026-01-31 17:55:58 +08:00
|
|
|
|
*/
|
|
|
|
|
|
@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;
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 会员数据传输对象
|
2026-01-31 23:14:11 +08:00
|
|
|
|
* @author pangu
|
2026-01-31 17:55:58 +08:00
|
|
|
|
*/
|
|
|
|
|
|
@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;
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 会员视图对象
|
2026-01-31 23:14:11 +08:00
|
|
|
|
* @author pangu
|
2026-01-31 17:55:58 +08:00
|
|
|
|
*/
|
|
|
|
|
|
@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;
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 身份类型枚举
|
2026-01-31 23:14:11 +08:00
|
|
|
|
* @author pangu
|
2026-01-31 17:55:58 +08:00
|
|
|
|
*/
|
|
|
|
|
|
@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;
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 注册来源枚举
|
2026-01-31 23:14:11 +08:00
|
|
|
|
* @author pangu
|
2026-01-31 17:55:58 +08:00
|
|
|
|
*/
|
|
|
|
|
|
@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;
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 会员数据访问接口
|
2026-01-31 23:14:11 +08:00
|
|
|
|
* @author pangu
|
2026-01-31 17:55:58 +08:00
|
|
|
|
*/
|
|
|
|
|
|
@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;
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 会员服务接口
|
2026-01-31 23:14:11 +08:00
|
|
|
|
* @author pangu
|
2026-01-31 17:55:58 +08:00
|
|
|
|
*/
|
|
|
|
|
|
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;
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 会员服务实现
|
2026-01-31 23:14:11 +08:00
|
|
|
|
* @author pangu
|
2026-01-31 17:55:58 +08:00
|
|
|
|
*/
|
|
|
|
|
|
@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.*;
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 会员管理控制器
|
2026-01-31 23:14:11 +08:00
|
|
|
|
* @author pangu
|
2026-01-31 17:55:58 +08:00
|
|
|
|
*/
|
|
|
|
|
|
@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记录异常信息
|
|
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
|
|
|
|
|
*文档结束*
|