2026-01-31 17:55:58 +08:00
|
|
|
|
# 会员管理模块技术方案
|
|
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
|
|
|
|
|
| 文档信息 | 内容 |
|
|
|
|
|
|
|---------|------|
|
|
|
|
|
|
| **文档版本** | V1.0 |
|
|
|
|
|
|
| **项目名称** | 盘古用户平台(Pangu User Platform) |
|
|
|
|
|
|
| **模块名称** | 会员管理模块 |
|
|
|
|
|
|
| **编写团队** | pangu |
|
|
|
|
|
|
| **创建日期** | 2026-01-31 |
|
|
|
|
|
|
| **评审状态** | 待评审 |
|
|
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
|
|
|
|
|
## 修订记录
|
|
|
|
|
|
|
|
|
|
|
|
| 版本 | 日期 | 修订人 | 修订内容 |
|
|
|
|
|
|
|-----|------|-------|---------|
|
|
|
|
|
|
| V1.0 | 2026-01-31 | pangu | 初稿 |
|
|
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
|
|
|
|
|
## 目录
|
|
|
|
|
|
|
|
|
|
|
|
1. [概述](#1-概述)
|
|
|
|
|
|
2. [需求分析](#2-需求分析)
|
|
|
|
|
|
3. [前端技术方案](#3-前端技术方案)
|
|
|
|
|
|
4. [后端技术方案](#4-后端技术方案)
|
|
|
|
|
|
5. [数据库设计](#5-数据库设计)
|
|
|
|
|
|
6. [接口设计](#6-接口设计)
|
|
|
|
|
|
7. [开发阶段计划](#7-开发阶段计划)
|
|
|
|
|
|
8. [测试方案](#8-测试方案)
|
|
|
|
|
|
9. [部署方案](#9-部署方案)
|
|
|
|
|
|
10. [风险评估](#10-风险评估)
|
|
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
|
|
|
|
|
## 1. 概述
|
|
|
|
|
|
|
|
|
|
|
|
### 1.1 模块简介
|
|
|
|
|
|
|
|
|
|
|
|
会员管理模块是盘古用户平台的核心业务模块之一,主要负责管理通过小程序/H5端注册的前端用户(家长/教师),支持用户信息维护、登录认证、学生绑定等功能。
|
|
|
|
|
|
|
|
|
|
|
|
### 1.2 模块边界
|
|
|
|
|
|
|
|
|
|
|
|
```
|
|
|
|
|
|
┌─────────────────────────────────────────────────────────────┐
|
|
|
|
|
|
│ 会员管理模块 │
|
|
|
|
|
|
├─────────────────────────────────────────────────────────────┤
|
|
|
|
|
|
│ │
|
|
|
|
|
|
│ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │
|
|
|
|
|
|
│ │ 会员信息管理 │ │ 登录认证 │ │ 学生绑定 │ │
|
|
|
|
|
|
│ │ - 新增会员 │ │ - 验证码登录 │ │ - 绑定学生 │ │
|
|
|
|
|
|
│ │ - 编辑会员 │ │ - 密码登录 │ │ - 解绑学生 │ │
|
|
|
|
|
|
│ │ - 删除会员 │ │ - 微信登录 │ │ - 绑定规则 │ │
|
|
|
|
|
|
│ │ - 列表查询 │ │ - Token管理 │ │ │ │
|
|
|
|
|
|
│ │ - 重置密码 │ │ │ │ │ │
|
|
|
|
|
|
│ │ - 状态控制 │ │ │ │ │ │
|
|
|
|
|
|
│ └──────────────┘ └──────────────┘ └──────────────┘ │
|
|
|
|
|
|
│ │
|
|
|
|
|
|
│ 依赖模块:学校管理、区域管理、年级班级管理、学生管理 │
|
|
|
|
|
|
└─────────────────────────────────────────────────────────────┘
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
### 1.3 用户角色
|
|
|
|
|
|
|
|
|
|
|
|
| 角色 | 权限范围 | 说明 |
|
|
|
|
|
|
|-----|---------|------|
|
|
|
|
|
|
| 超级管理员 | 全部会员数据 | 可管理所有会员 |
|
|
|
|
|
|
| 分公司用户 | 所属区域会员 | 只能管理所属区域的会员 |
|
|
|
|
|
|
| 学校用户 | 本校教师 | 只能查看本校教师会员 |
|
|
|
|
|
|
|
|
|
|
|
|
### 1.4 术语定义
|
|
|
|
|
|
|
|
|
|
|
|
| 术语 | 定义 |
|
|
|
|
|
|
|-----|------|
|
|
|
|
|
|
| 会员 | 通过小程序/H5端注册的前端用户,包括家长和教师 |
|
|
|
|
|
|
| 家长会员 | 身份类型为"家长"的会员,可绑定任意学校的学生 |
|
|
|
|
|
|
| 教师会员 | 身份类型为"教师"的会员,需绑定学校信息,只能绑定本校学生 |
|
|
|
|
|
|
| 学生绑定 | 会员与学生之间的关联关系 |
|
|
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
|
|
|
|
|
## 2. 需求分析
|
|
|
|
|
|
|
|
|
|
|
|
### 2.1 功能需求清单
|
|
|
|
|
|
|
|
|
|
|
|
| 功能编号 | 功能名称 | 功能描述 | 优先级 |
|
|
|
|
|
|
|---------|---------|---------|:-----:|
|
|
|
|
|
|
| MEM-001 | 会员列表查询 | 按手机号、昵称、状态、注册时间、身份类型筛选 | P0 |
|
|
|
|
|
|
| MEM-002 | 新增会员 | 后台手动创建会员账号 | P0 |
|
|
|
|
|
|
| MEM-003 | 编辑会员 | 修改会员基本信息和学生绑定关系 | P0 |
|
|
|
|
|
|
| MEM-004 | 删除会员 | 软删除会员(需检查学生绑定) | P1 |
|
|
|
|
|
|
| MEM-005 | 重置密码 | 重置会员登录密码并显示新密码 | P0 |
|
|
|
|
|
|
| MEM-006 | 禁用/启用会员 | 控制会员登录权限 | P0 |
|
|
|
|
|
|
| MEM-007 | 绑定学生 | 为会员绑定学生信息 | P0 |
|
|
|
|
|
|
| MEM-008 | 解绑学生 | 移除会员与学生的绑定关系 | P0 |
|
|
|
|
|
|
|
|
|
|
|
|
### 2.2 业务规则
|
|
|
|
|
|
|
|
|
|
|
|
| 规则编号 | 规则描述 | 校验时机 |
|
|
|
|
|
|
|---------|---------|---------|
|
|
|
|
|
|
| MEM-R01 | 会员编号由系统自动生成,格式:JS + 时间戳 | 新增时 |
|
|
|
|
|
|
| MEM-R02 | 昵称未填写时,系统自动生成默认昵称 | 新增时 |
|
|
|
|
|
|
| MEM-R03 | 手机号为必填项,需验证格式有效性和唯一性 | 新增/编辑时 |
|
|
|
|
|
|
| MEM-R04 | 出生日期和性别为选填项 | - |
|
|
|
|
|
|
| MEM-R05 | 身份类型为"教师"时,必须选择所属区域/学校/年级/班级 | 新增/编辑时 |
|
|
|
|
|
|
| MEM-R06 | 身份类型为"教师"时,只能绑定同校学生 | 绑定学生时 |
|
|
|
|
|
|
| MEM-R07 | 身份类型为"家长"时,不显示区域信息,可绑定任意学生 | 绑定学生时 |
|
|
|
|
|
|
| MEM-R08 | 删除会员前需检查是否绑定学生,有则不允许删除 | 删除时 |
|
|
|
|
|
|
| MEM-R09 | 重置密码后,需弹窗显示新密码并提供复制功能 | 重置密码后 |
|
|
|
|
|
|
| MEM-R10 | 禁用会员后,该用户无法登录任何端 | 禁用时 |
|
|
|
|
|
|
| MEM-R11 | 使用RuoYi鉴权体系,区分后台用户和会员信息 | 登录时 |
|
|
|
|
|
|
|
|
|
|
|
|
### 2.3 数据权限
|
|
|
|
|
|
|
|
|
|
|
|
```
|
|
|
|
|
|
┌────────────────────────────────────────────────────────────┐
|
|
|
|
|
|
│ 会员数据权限控制 │
|
|
|
|
|
|
├────────────────────────────────────────────────────────────┤
|
|
|
|
|
|
│ │
|
|
|
|
|
|
│ 超级管理员 ────────────────────────────────► 全部会员数据 │
|
|
|
|
|
|
│ │
|
|
|
|
|
|
│ 分公司用户 ─────► 所属区域 ────► 区域下学校 ──► 学校相关会员 │
|
|
|
|
|
|
│ │
|
|
|
|
|
|
│ 学校用户 ────────────────────────────────► 本校教师会员 │
|
|
|
|
|
|
│ │
|
|
|
|
|
|
└────────────────────────────────────────────────────────────┘
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
|
|
|
|
|
## 3. 前端技术方案
|
|
|
|
|
|
|
|
|
|
|
|
### 3.1 技术栈
|
|
|
|
|
|
|
|
|
|
|
|
| 技术 | 版本 | 说明 |
|
|
|
|
|
|
|-----|------|------|
|
|
|
|
|
|
| Vue | 3.5.x | 前端框架 |
|
|
|
|
|
|
| Element Plus | 2.13.x | UI组件库 |
|
|
|
|
|
|
| Pinia | 3.0.x | 状态管理 |
|
|
|
|
|
|
| Axios | 1.13.x | HTTP客户端 |
|
|
|
|
|
|
| Vue Router | 4.6.x | 路由管理 |
|
|
|
|
|
|
|
|
|
|
|
|
### 3.2 目录结构
|
|
|
|
|
|
|
|
|
|
|
|
```
|
|
|
|
|
|
pangu-ui/src/
|
|
|
|
|
|
├── api/
|
|
|
|
|
|
│ └── member.js # 会员管理API接口
|
|
|
|
|
|
├── views/
|
|
|
|
|
|
│ └── member/
|
|
|
|
|
|
│ ├── index.vue # 会员列表页
|
|
|
|
|
|
│ ├── form.vue # 会员新增/编辑页
|
|
|
|
|
|
│ └── components/
|
|
|
|
|
|
│ ├── MemberSearch.vue # 搜索条件组件
|
|
|
|
|
|
│ ├── MemberTable.vue # 列表表格组件
|
|
|
|
|
|
│ ├── MemberForm.vue # 表单组件
|
|
|
|
|
|
│ ├── StudentBind.vue # 学生绑定组件
|
|
|
|
|
|
│ └── PasswordDialog.vue # 密码弹窗组件
|
|
|
|
|
|
├── mock/
|
|
|
|
|
|
│ └── member.js # Mock数据
|
|
|
|
|
|
└── utils/
|
|
|
|
|
|
└── member.js # 会员相关工具函数
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
### 3.3 页面设计
|
|
|
|
|
|
|
|
|
|
|
|
#### 3.3.1 会员列表页(index.vue)
|
|
|
|
|
|
|
|
|
|
|
|
**页面布局**
|
|
|
|
|
|
|
|
|
|
|
|
```
|
|
|
|
|
|
┌─────────────────────────────────────────────────────────────────┐
|
|
|
|
|
|
│ 搜索区域 │
|
|
|
|
|
|
│ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐ │
|
|
|
|
|
|
│ │ 手机号 │ │ 昵称 │ │ 身份类型 │ │ 状态 │ │
|
|
|
|
|
|
│ └──────────┘ └──────────┘ └──────────┘ └──────────┘ │
|
|
|
|
|
|
│ ┌──────────────────────┐ ┌────────┐ ┌────────┐ │
|
|
|
|
|
|
│ │ 注册时间区间 │ │ 搜索 │ │ 重置 │ │
|
|
|
|
|
|
│ └──────────────────────┘ └────────┘ └────────┘ │
|
|
|
|
|
|
├─────────────────────────────────────────────────────────────────┤
|
|
|
|
|
|
│ 操作按钮 │
|
|
|
|
|
|
│ ┌────────┐ │
|
|
|
|
|
|
│ │ 新增 │ │
|
|
|
|
|
|
│ └────────┘ │
|
|
|
|
|
|
├─────────────────────────────────────────────────────────────────┤
|
|
|
|
|
|
│ 列表区域 │
|
|
|
|
|
|
│ ┌──────┬─────────┬──────┬──────┬──────┬──────┬──────┬────────┐ │
|
|
|
|
|
|
│ │会员编号│ 手机号 │ 昵称 │ 性别 │身份类型│注册时间│ 状态 │ 操作 │ │
|
|
|
|
|
|
│ ├──────┼─────────┼──────┼──────┼──────┼──────┼──────┼────────┤ │
|
|
|
|
|
|
│ │ ... │ ... │ ... │ ... │ ... │ ... │ ... │编辑/重置│ │
|
|
|
|
|
|
│ └──────┴─────────┴──────┴──────┴──────┴──────┴──────┴────────┘ │
|
|
|
|
|
|
├─────────────────────────────────────────────────────────────────┤
|
|
|
|
|
|
│ 分页区域 │
|
|
|
|
|
|
│ ┌──────────────────────┐ │
|
|
|
|
|
|
│ │ < 1 2 3 4 5 ... > │ │
|
|
|
|
|
|
│ └──────────────────────┘ │
|
|
|
|
|
|
└─────────────────────────────────────────────────────────────────┘
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
**核心代码结构**
|
|
|
|
|
|
|
|
|
|
|
|
```vue
|
|
|
|
|
|
<template>
|
|
|
|
|
|
<div class="member-container">
|
|
|
|
|
|
<!-- 搜索区域 -->
|
|
|
|
|
|
<el-form :model="queryParams" ref="queryRef" :inline="true">
|
|
|
|
|
|
<el-form-item label="手机号" prop="phone">
|
|
|
|
|
|
<el-input v-model="queryParams.phone" placeholder="请输入手机号" clearable />
|
|
|
|
|
|
</el-form-item>
|
|
|
|
|
|
<el-form-item label="昵称" prop="nickname">
|
|
|
|
|
|
<el-input v-model="queryParams.nickname" placeholder="请输入昵称" clearable />
|
|
|
|
|
|
</el-form-item>
|
|
|
|
|
|
<el-form-item label="身份类型" prop="identityType">
|
|
|
|
|
|
<el-select v-model="queryParams.identityType" placeholder="请选择" clearable>
|
|
|
|
|
|
<el-option label="家长" value="1" />
|
|
|
|
|
|
<el-option label="教师" value="2" />
|
|
|
|
|
|
</el-select>
|
|
|
|
|
|
</el-form-item>
|
|
|
|
|
|
<el-form-item label="状态" prop="status">
|
|
|
|
|
|
<el-select v-model="queryParams.status" placeholder="请选择" clearable>
|
|
|
|
|
|
<el-option label="正常" value="0" />
|
|
|
|
|
|
<el-option label="停用" value="1" />
|
|
|
|
|
|
</el-select>
|
|
|
|
|
|
</el-form-item>
|
|
|
|
|
|
<el-form-item label="注册时间">
|
|
|
|
|
|
<el-date-picker
|
|
|
|
|
|
v-model="dateRange"
|
|
|
|
|
|
type="daterange"
|
|
|
|
|
|
range-separator="-"
|
|
|
|
|
|
start-placeholder="开始日期"
|
|
|
|
|
|
end-placeholder="结束日期"
|
|
|
|
|
|
/>
|
|
|
|
|
|
</el-form-item>
|
|
|
|
|
|
<el-form-item>
|
|
|
|
|
|
<el-button type="primary" @click="handleQuery">搜索</el-button>
|
|
|
|
|
|
<el-button @click="resetQuery">重置</el-button>
|
|
|
|
|
|
</el-form-item>
|
|
|
|
|
|
</el-form>
|
|
|
|
|
|
|
|
|
|
|
|
<!-- 操作按钮 -->
|
|
|
|
|
|
<el-row :gutter="10" class="mb8">
|
|
|
|
|
|
<el-col :span="1.5">
|
|
|
|
|
|
<el-button type="primary" @click="handleAdd">新增</el-button>
|
|
|
|
|
|
</el-col>
|
|
|
|
|
|
</el-row>
|
|
|
|
|
|
|
|
|
|
|
|
<!-- 列表表格 -->
|
|
|
|
|
|
<el-table v-loading="loading" :data="memberList">
|
|
|
|
|
|
<el-table-column label="会员编号" prop="memberCode" width="150" />
|
|
|
|
|
|
<el-table-column label="手机号" prop="phone" width="120" />
|
|
|
|
|
|
<el-table-column label="昵称" prop="nickname" />
|
|
|
|
|
|
<el-table-column label="性别" prop="gender" width="80">
|
|
|
|
|
|
<template #default="scope">
|
|
|
|
|
|
{{ genderFormat(scope.row.gender) }}
|
|
|
|
|
|
</template>
|
|
|
|
|
|
</el-table-column>
|
|
|
|
|
|
<el-table-column label="身份类型" prop="identityType" width="100">
|
|
|
|
|
|
<template #default="scope">
|
|
|
|
|
|
<el-tag :type="scope.row.identityType === '1' ? '' : 'success'">
|
|
|
|
|
|
{{ scope.row.identityType === '1' ? '家长' : '教师' }}
|
|
|
|
|
|
</el-tag>
|
|
|
|
|
|
</template>
|
|
|
|
|
|
</el-table-column>
|
|
|
|
|
|
<el-table-column label="注册时间" prop="registerTime" width="160" />
|
|
|
|
|
|
<el-table-column label="状态" prop="status" width="80">
|
|
|
|
|
|
<template #default="scope">
|
|
|
|
|
|
<el-switch
|
|
|
|
|
|
v-model="scope.row.status"
|
|
|
|
|
|
active-value="0"
|
|
|
|
|
|
inactive-value="1"
|
|
|
|
|
|
@change="handleStatusChange(scope.row)"
|
|
|
|
|
|
/>
|
|
|
|
|
|
</template>
|
|
|
|
|
|
</el-table-column>
|
|
|
|
|
|
<el-table-column label="操作" width="200" fixed="right">
|
|
|
|
|
|
<template #default="scope">
|
|
|
|
|
|
<el-button link type="primary" @click="handleEdit(scope.row)">编辑</el-button>
|
|
|
|
|
|
<el-button link type="primary" @click="handleResetPwd(scope.row)">重置密码</el-button>
|
|
|
|
|
|
<el-popconfirm
|
|
|
|
|
|
title="确认删除该会员?"
|
|
|
|
|
|
@confirm="handleDelete(scope.row)"
|
|
|
|
|
|
>
|
|
|
|
|
|
<template #reference>
|
|
|
|
|
|
<el-button link type="danger">删除</el-button>
|
|
|
|
|
|
</template>
|
|
|
|
|
|
</el-popconfirm>
|
|
|
|
|
|
</template>
|
|
|
|
|
|
</el-table-column>
|
|
|
|
|
|
</el-table>
|
|
|
|
|
|
|
|
|
|
|
|
<!-- 分页 -->
|
|
|
|
|
|
<el-pagination
|
|
|
|
|
|
v-model:current-page="queryParams.pageNum"
|
|
|
|
|
|
v-model:page-size="queryParams.pageSize"
|
|
|
|
|
|
:total="total"
|
|
|
|
|
|
@size-change="handleSizeChange"
|
|
|
|
|
|
@current-change="handleCurrentChange"
|
|
|
|
|
|
/>
|
|
|
|
|
|
|
|
|
|
|
|
<!-- 密码重置弹窗 -->
|
|
|
|
|
|
<el-dialog v-model="passwordDialogVisible" title="密码重置成功" width="400px">
|
|
|
|
|
|
<div class="password-display">
|
|
|
|
|
|
<span>新密码:</span>
|
|
|
|
|
|
<span class="password-text">{{ newPassword }}</span>
|
|
|
|
|
|
<el-button type="primary" link @click="copyPassword">复制</el-button>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</el-dialog>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</template>
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
#### 3.3.2 会员编辑页(form.vue)
|
|
|
|
|
|
|
|
|
|
|
|
**页面布局**
|
|
|
|
|
|
|
|
|
|
|
|
```
|
|
|
|
|
|
┌─────────────────────────────────────────────────────────────────┐
|
|
|
|
|
|
│ 页面标题:新增会员 / 编辑会员 │
|
|
|
|
|
|
├─────────────────────────────────────────────────────────────────┤
|
|
|
|
|
|
│ 基本信息 │
|
|
|
|
|
|
│ ┌──────────────────────────────────────────────────────────┐ │
|
|
|
|
|
|
│ │ 手机号 * [_______________] │ │
|
|
|
|
|
|
│ │ 昵称 [_______________] │ │
|
|
|
|
|
|
│ │ 性别 ○ 未知 ○ 男 ○ 女 │ │
|
|
|
|
|
|
│ │ 出生日期 [_______________] │ │
|
|
|
|
|
|
│ │ 身份类型 * [___家长▼_______] │ │
|
|
|
|
|
|
│ └──────────────────────────────────────────────────────────┘ │
|
|
|
|
|
|
│ │
|
|
|
|
|
|
│ 教师信息(身份类型为教师时显示) │
|
|
|
|
|
|
│ ┌──────────────────────────────────────────────────────────┐ │
|
|
|
|
|
|
│ │ 所属区域 * [___区域选择___] │ │
|
|
|
|
|
|
│ │ 所属学校 * [___学校选择___] │ │
|
|
|
|
|
|
│ │ 所属年级 * [___年级选择___] │ │
|
|
|
|
|
|
│ │ 所属班级 * [___班级选择___] │ │
|
|
|
|
|
|
│ └──────────────────────────────────────────────────────────┘ │
|
|
|
|
|
|
│ │
|
|
|
|
|
|
│ 绑定学生 │
|
|
|
|
|
|
│ ┌──────────────────────────────────────────────────────────┐ │
|
|
|
|
|
|
│ │ ┌────────┐ │ │
|
|
|
|
|
|
│ │ │ + 绑定 │ │ │
|
|
|
|
|
|
│ │ └────────┘ │ │
|
|
|
|
|
|
│ │ ┌──────┬────────┬────────┬────────┬────────┬────────┐ │ │
|
|
|
|
|
|
│ │ │ 姓名 │ 学号 │ 学校 │ 年级 │ 班级 │ 操作 │ │ │
|
|
|
|
|
|
│ │ ├──────┼────────┼────────┼────────┼────────┼────────┤ │ │
|
|
|
|
|
|
│ │ │ ... │ ... │ ... │ ... │ ... │ 解绑 │ │ │
|
|
|
|
|
|
│ │ └──────┴────────┴────────┴────────┴────────┴────────┘ │ │
|
|
|
|
|
|
│ └──────────────────────────────────────────────────────────┘ │
|
|
|
|
|
|
│ │
|
|
|
|
|
|
│ ┌────────┐ ┌────────┐ │
|
|
|
|
|
|
│ │ 保存 │ │ 取消 │ │
|
|
|
|
|
|
│ └────────┘ └────────┘ │
|
|
|
|
|
|
└─────────────────────────────────────────────────────────────────┘
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
**核心代码结构**
|
|
|
|
|
|
|
|
|
|
|
|
```vue
|
|
|
|
|
|
<template>
|
|
|
|
|
|
<div class="member-form-container">
|
|
|
|
|
|
<el-form ref="formRef" :model="form" :rules="rules" label-width="100px">
|
|
|
|
|
|
<!-- 基本信息 -->
|
|
|
|
|
|
<el-divider content-position="left">基本信息</el-divider>
|
|
|
|
|
|
|
|
|
|
|
|
<el-row :gutter="20">
|
|
|
|
|
|
<el-col :span="12">
|
|
|
|
|
|
<el-form-item label="手机号" prop="phone">
|
|
|
|
|
|
<el-input v-model="form.phone" placeholder="请输入手机号" maxlength="11" />
|
|
|
|
|
|
</el-form-item>
|
|
|
|
|
|
</el-col>
|
|
|
|
|
|
<el-col :span="12">
|
|
|
|
|
|
<el-form-item label="昵称" prop="nickname">
|
|
|
|
|
|
<el-input v-model="form.nickname" placeholder="请输入昵称(不填则自动生成)" />
|
|
|
|
|
|
</el-form-item>
|
|
|
|
|
|
</el-col>
|
|
|
|
|
|
</el-row>
|
|
|
|
|
|
|
|
|
|
|
|
<el-row :gutter="20">
|
|
|
|
|
|
<el-col :span="12">
|
|
|
|
|
|
<el-form-item label="性别" prop="gender">
|
|
|
|
|
|
<el-radio-group v-model="form.gender">
|
|
|
|
|
|
<el-radio value="0">未知</el-radio>
|
|
|
|
|
|
<el-radio value="1">男</el-radio>
|
|
|
|
|
|
<el-radio value="2">女</el-radio>
|
|
|
|
|
|
</el-radio-group>
|
|
|
|
|
|
</el-form-item>
|
|
|
|
|
|
</el-col>
|
|
|
|
|
|
<el-col :span="12">
|
|
|
|
|
|
<el-form-item label="出生日期" prop="birthday">
|
|
|
|
|
|
<el-date-picker v-model="form.birthday" type="date" placeholder="请选择出生日期" />
|
|
|
|
|
|
</el-form-item>
|
|
|
|
|
|
</el-col>
|
|
|
|
|
|
</el-row>
|
|
|
|
|
|
|
|
|
|
|
|
<el-row :gutter="20">
|
|
|
|
|
|
<el-col :span="12">
|
|
|
|
|
|
<el-form-item label="身份类型" prop="identityType">
|
|
|
|
|
|
<el-select v-model="form.identityType" placeholder="请选择身份类型" @change="handleIdentityChange">
|
|
|
|
|
|
<el-option label="家长" value="1" />
|
|
|
|
|
|
<el-option label="教师" value="2" />
|
|
|
|
|
|
</el-select>
|
|
|
|
|
|
</el-form-item>
|
|
|
|
|
|
</el-col>
|
|
|
|
|
|
<el-col :span="12">
|
|
|
|
|
|
<el-form-item label="状态" prop="status">
|
|
|
|
|
|
<el-switch v-model="form.status" active-value="0" inactive-value="1" />
|
|
|
|
|
|
</el-form-item>
|
|
|
|
|
|
</el-col>
|
|
|
|
|
|
</el-row>
|
|
|
|
|
|
|
|
|
|
|
|
<!-- 教师信息(身份类型为教师时显示) -->
|
|
|
|
|
|
<template v-if="form.identityType === '2'">
|
|
|
|
|
|
<el-divider content-position="left">教师信息</el-divider>
|
|
|
|
|
|
|
|
|
|
|
|
<el-row :gutter="20">
|
|
|
|
|
|
<el-col :span="12">
|
|
|
|
|
|
<el-form-item label="所属区域" prop="regionId">
|
|
|
|
|
|
<el-cascader
|
|
|
|
|
|
v-model="form.regionIds"
|
|
|
|
|
|
:options="regionTree"
|
|
|
|
|
|
:props="{ value: 'id', label: 'label', children: 'children' }"
|
|
|
|
|
|
placeholder="请选择区域"
|
|
|
|
|
|
@change="handleRegionChange"
|
|
|
|
|
|
/>
|
|
|
|
|
|
</el-form-item>
|
|
|
|
|
|
</el-col>
|
|
|
|
|
|
<el-col :span="12">
|
|
|
|
|
|
<el-form-item label="所属学校" prop="schoolId">
|
|
|
|
|
|
<el-select v-model="form.schoolId" placeholder="请选择学校" @change="handleSchoolChange">
|
|
|
|
|
|
<el-option
|
|
|
|
|
|
v-for="item in schoolList"
|
|
|
|
|
|
:key="item.id"
|
|
|
|
|
|
:label="item.name"
|
|
|
|
|
|
:value="item.id"
|
|
|
|
|
|
/>
|
|
|
|
|
|
</el-select>
|
|
|
|
|
|
</el-form-item>
|
|
|
|
|
|
</el-col>
|
|
|
|
|
|
</el-row>
|
|
|
|
|
|
|
|
|
|
|
|
<el-row :gutter="20">
|
|
|
|
|
|
<el-col :span="12">
|
|
|
|
|
|
<el-form-item label="所属年级" prop="schoolGradeId">
|
|
|
|
|
|
<el-select v-model="form.schoolGradeId" placeholder="请选择年级" @change="handleGradeChange">
|
|
|
|
|
|
<el-option
|
|
|
|
|
|
v-for="item in gradeList"
|
|
|
|
|
|
:key="item.id"
|
|
|
|
|
|
:label="item.name"
|
|
|
|
|
|
:value="item.id"
|
|
|
|
|
|
/>
|
|
|
|
|
|
</el-select>
|
|
|
|
|
|
</el-form-item>
|
|
|
|
|
|
</el-col>
|
|
|
|
|
|
<el-col :span="12">
|
|
|
|
|
|
<el-form-item label="所属班级" prop="schoolClassId">
|
|
|
|
|
|
<el-select v-model="form.schoolClassId" placeholder="请选择班级">
|
|
|
|
|
|
<el-option
|
|
|
|
|
|
v-for="item in classList"
|
|
|
|
|
|
:key="item.id"
|
|
|
|
|
|
:label="item.name"
|
|
|
|
|
|
:value="item.id"
|
|
|
|
|
|
/>
|
|
|
|
|
|
</el-select>
|
|
|
|
|
|
</el-form-item>
|
|
|
|
|
|
</el-col>
|
|
|
|
|
|
</el-row>
|
|
|
|
|
|
</template>
|
|
|
|
|
|
|
|
|
|
|
|
<!-- 绑定学生 -->
|
|
|
|
|
|
<el-divider content-position="left">绑定学生</el-divider>
|
|
|
|
|
|
|
|
|
|
|
|
<el-button type="primary" @click="openStudentDialog">+ 绑定学生</el-button>
|
|
|
|
|
|
|
|
|
|
|
|
<el-table :data="form.students" style="margin-top: 16px">
|
|
|
|
|
|
<el-table-column label="姓名" prop="studentName" />
|
|
|
|
|
|
<el-table-column label="学号" prop="studentNo" />
|
|
|
|
|
|
<el-table-column label="学校" prop="schoolName" />
|
|
|
|
|
|
<el-table-column label="年级" prop="gradeName" />
|
|
|
|
|
|
<el-table-column label="班级" prop="className" />
|
|
|
|
|
|
<el-table-column label="操作" width="100">
|
|
|
|
|
|
<template #default="scope">
|
|
|
|
|
|
<el-popconfirm title="确认解绑该学生?" @confirm="handleUnbind(scope.row)">
|
|
|
|
|
|
<template #reference>
|
|
|
|
|
|
<el-button link type="danger">解绑</el-button>
|
|
|
|
|
|
</template>
|
|
|
|
|
|
</el-popconfirm>
|
|
|
|
|
|
</template>
|
|
|
|
|
|
</el-table-column>
|
|
|
|
|
|
</el-table>
|
|
|
|
|
|
|
|
|
|
|
|
<!-- 按钮 -->
|
|
|
|
|
|
<el-form-item style="margin-top: 24px">
|
|
|
|
|
|
<el-button type="primary" @click="submitForm">保存</el-button>
|
|
|
|
|
|
<el-button @click="cancel">取消</el-button>
|
|
|
|
|
|
</el-form-item>
|
|
|
|
|
|
</el-form>
|
|
|
|
|
|
|
|
|
|
|
|
<!-- 学生选择弹窗 -->
|
|
|
|
|
|
<student-select-dialog
|
|
|
|
|
|
v-model:visible="studentDialogVisible"
|
|
|
|
|
|
:identity-type="form.identityType"
|
|
|
|
|
|
:school-id="form.schoolId"
|
|
|
|
|
|
:exclude-ids="form.students.map(s => s.studentId)"
|
|
|
|
|
|
@confirm="handleStudentSelected"
|
|
|
|
|
|
/>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</template>
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
### 3.4 组件设计
|
|
|
|
|
|
|
|
|
|
|
|
#### 3.4.1 学生选择弹窗(StudentSelectDialog.vue)
|
|
|
|
|
|
|
|
|
|
|
|
| 属性 | 类型 | 说明 |
|
|
|
|
|
|
|-----|------|------|
|
|
|
|
|
|
| visible | Boolean | 弹窗显示状态 |
|
|
|
|
|
|
| identityType | String | 身份类型,用于控制可选范围 |
|
|
|
|
|
|
| schoolId | Number | 学校ID,教师身份时限制只能选本校学生 |
|
|
|
|
|
|
| excludeIds | Array | 已绑定学生ID列表,排除这些学生 |
|
|
|
|
|
|
|
|
|
|
|
|
| 事件 | 参数 | 说明 |
|
|
|
|
|
|
|-----|------|------|
|
|
|
|
|
|
| confirm | studentList | 选中的学生列表 |
|
|
|
|
|
|
| update:visible | Boolean | 更新显示状态 |
|
|
|
|
|
|
|
|
|
|
|
|
#### 3.4.2 密码显示弹窗(PasswordDialog.vue)
|
|
|
|
|
|
|
|
|
|
|
|
| 属性 | 类型 | 说明 |
|
|
|
|
|
|
|-----|------|------|
|
|
|
|
|
|
| visible | Boolean | 弹窗显示状态 |
|
|
|
|
|
|
| password | String | 新密码 |
|
|
|
|
|
|
|
|
|
|
|
|
| 事件 | 参数 | 说明 |
|
|
|
|
|
|
|-----|------|------|
|
|
|
|
|
|
| copy | - | 复制密码 |
|
|
|
|
|
|
| update:visible | Boolean | 更新显示状态 |
|
|
|
|
|
|
|
|
|
|
|
|
### 3.5 状态管理
|
|
|
|
|
|
|
|
|
|
|
|
```javascript
|
|
|
|
|
|
// store/member.js
|
|
|
|
|
|
import { defineStore } from 'pinia'
|
|
|
|
|
|
|
|
|
|
|
|
export const useMemberStore = defineStore('member', {
|
|
|
|
|
|
state: () => ({
|
|
|
|
|
|
// 区域树缓存
|
|
|
|
|
|
regionTree: [],
|
|
|
|
|
|
// 当前编辑的会员
|
|
|
|
|
|
currentMember: null
|
|
|
|
|
|
}),
|
|
|
|
|
|
|
|
|
|
|
|
actions: {
|
|
|
|
|
|
// 获取区域树(带缓存)
|
|
|
|
|
|
async fetchRegionTree() {
|
|
|
|
|
|
if (this.regionTree.length > 0) {
|
|
|
|
|
|
return this.regionTree
|
|
|
|
|
|
}
|
|
|
|
|
|
const res = await getRegionTree()
|
|
|
|
|
|
if (res.code === 200) {
|
|
|
|
|
|
this.regionTree = res.data
|
|
|
|
|
|
}
|
|
|
|
|
|
return this.regionTree
|
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
|
|
// 设置当前会员
|
|
|
|
|
|
setCurrentMember(member) {
|
|
|
|
|
|
this.currentMember = member
|
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
|
|
// 清空当前会员
|
|
|
|
|
|
clearCurrentMember() {
|
|
|
|
|
|
this.currentMember = null
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
})
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
### 3.6 表单校验规则
|
|
|
|
|
|
|
|
|
|
|
|
```javascript
|
|
|
|
|
|
const rules = reactive({
|
|
|
|
|
|
phone: [
|
|
|
|
|
|
{ required: true, message: '请输入手机号', trigger: 'blur' },
|
|
|
|
|
|
{ pattern: /^1[3-9]\d{9}$/, message: '请输入正确的手机号', trigger: 'blur' }
|
|
|
|
|
|
],
|
|
|
|
|
|
identityType: [
|
|
|
|
|
|
{ required: true, message: '请选择身份类型', trigger: 'change' }
|
|
|
|
|
|
],
|
|
|
|
|
|
regionId: [
|
|
|
|
|
|
{ required: true, message: '请选择所属区域', trigger: 'change' }
|
|
|
|
|
|
],
|
|
|
|
|
|
schoolId: [
|
|
|
|
|
|
{ required: true, message: '请选择所属学校', trigger: 'change' }
|
|
|
|
|
|
],
|
|
|
|
|
|
schoolGradeId: [
|
|
|
|
|
|
{ required: true, message: '请选择所属年级', trigger: 'change' }
|
|
|
|
|
|
],
|
|
|
|
|
|
schoolClassId: [
|
|
|
|
|
|
{ required: true, message: '请选择所属班级', trigger: 'change' }
|
|
|
|
|
|
]
|
|
|
|
|
|
})
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
### 3.7 工具函数
|
|
|
|
|
|
|
|
|
|
|
|
```javascript
|
|
|
|
|
|
// utils/member.js
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 生成默认昵称
|
|
|
|
|
|
*/
|
|
|
|
|
|
export function generateNickname(phone) {
|
|
|
|
|
|
return `用户${phone.slice(-4)}`
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 手机号脱敏
|
|
|
|
|
|
*/
|
|
|
|
|
|
export function maskPhone(phone) {
|
|
|
|
|
|
if (!phone || phone.length !== 11) return phone
|
|
|
|
|
|
return phone.replace(/(\d{3})\d{4}(\d{4})/, '$1****$2')
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 性别格式化
|
|
|
|
|
|
*/
|
|
|
|
|
|
export function formatGender(gender) {
|
|
|
|
|
|
const map = { '0': '未知', '1': '男', '2': '女' }
|
|
|
|
|
|
return map[gender] || '未知'
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 身份类型格式化
|
|
|
|
|
|
*/
|
|
|
|
|
|
export function formatIdentityType(type) {
|
|
|
|
|
|
return type === '1' ? '家长' : '教师'
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 复制文本到剪贴板
|
|
|
|
|
|
*/
|
|
|
|
|
|
export async function copyToClipboard(text) {
|
|
|
|
|
|
try {
|
|
|
|
|
|
await navigator.clipboard.writeText(text)
|
|
|
|
|
|
ElMessage.success('复制成功')
|
|
|
|
|
|
} catch (err) {
|
|
|
|
|
|
ElMessage.error('复制失败')
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
|
|
|
|
|
## 4. 后端技术方案
|
|
|
|
|
|
|
|
|
|
|
|
### 4.1 技术栈
|
|
|
|
|
|
|
|
|
|
|
|
| 技术 | 版本 | 说明 |
|
|
|
|
|
|
|-----|------|------|
|
|
|
|
|
|
| Spring Boot | 3.3.x | 应用框架 |
|
|
|
|
|
|
| Spring Security | 6.x | 安全框架 |
|
|
|
|
|
|
| MyBatis Plus | 3.5.x | ORM框架 |
|
|
|
|
|
|
| JWT | 0.12.x | Token认证 |
|
|
|
|
|
|
| Hutool | 5.x | 工具库 |
|
|
|
|
|
|
| JDK | 17+ | 运行环境 |
|
|
|
|
|
|
|
|
|
|
|
|
### 4.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 # 注册来源枚举
|
|
|
|
|
|
└── common/
|
|
|
|
|
|
└── exception/
|
|
|
|
|
|
└── MemberException.java # 会员模块异常
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
### 4.3 实体设计
|
|
|
|
|
|
|
|
|
|
|
|
#### 4.3.1 会员实体(Member.java)
|
|
|
|
|
|
|
|
|
|
|
|
```java
|
|
|
|
|
|
package com.pangu.member.domain;
|
|
|
|
|
|
|
|
|
|
|
|
import com.baomidou.mybatisplus.annotation.*;
|
|
|
|
|
|
import lombok.Data;
|
|
|
|
|
|
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
|
|
|
|
|
|
@TableName("pg_member")
|
|
|
|
|
|
public class Member {
|
|
|
|
|
|
|
|
|
|
|
|
/** 会员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;
|
|
|
|
|
|
|
|
|
|
|
|
/** 创建者 */
|
|
|
|
|
|
@TableField(fill = FieldFill.INSERT)
|
|
|
|
|
|
private String createBy;
|
|
|
|
|
|
|
|
|
|
|
|
/** 创建时间 */
|
|
|
|
|
|
@TableField(fill = FieldFill.INSERT)
|
|
|
|
|
|
private LocalDateTime createTime;
|
|
|
|
|
|
|
|
|
|
|
|
/** 更新者 */
|
|
|
|
|
|
@TableField(fill = FieldFill.INSERT_UPDATE)
|
|
|
|
|
|
private String updateBy;
|
|
|
|
|
|
|
|
|
|
|
|
/** 更新时间 */
|
|
|
|
|
|
@TableField(fill = FieldFill.INSERT_UPDATE)
|
|
|
|
|
|
private LocalDateTime updateTime;
|
|
|
|
|
|
|
|
|
|
|
|
/** 删除标志(0存在 1删除) */
|
|
|
|
|
|
@TableLogic
|
|
|
|
|
|
private String delFlag;
|
|
|
|
|
|
|
|
|
|
|
|
/** 备注 */
|
|
|
|
|
|
private String remark;
|
|
|
|
|
|
}
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
#### 4.3.2 会员VO(MemberVO.java)
|
|
|
|
|
|
|
|
|
|
|
|
```java
|
|
|
|
|
|
package com.pangu.member.domain;
|
|
|
|
|
|
|
|
|
|
|
|
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 {
|
|
|
|
|
|
|
|
|
|
|
|
private Long memberId;
|
|
|
|
|
|
private String memberCode;
|
|
|
|
|
|
private String phone; // 脱敏显示
|
|
|
|
|
|
private String phoneFull; // 完整手机号(编辑时使用)
|
|
|
|
|
|
private String nickname;
|
|
|
|
|
|
private String avatar;
|
|
|
|
|
|
private String gender;
|
|
|
|
|
|
private String genderName;
|
|
|
|
|
|
private LocalDate birthday;
|
|
|
|
|
|
private String identityType;
|
|
|
|
|
|
private String identityTypeName;
|
|
|
|
|
|
private String openId;
|
|
|
|
|
|
private Long regionId;
|
|
|
|
|
|
private String regionPath; // 区域路径
|
|
|
|
|
|
private Long schoolId;
|
|
|
|
|
|
private String schoolName;
|
|
|
|
|
|
private Long schoolGradeId;
|
|
|
|
|
|
private String gradeName;
|
|
|
|
|
|
private Long schoolClassId;
|
|
|
|
|
|
private String className;
|
|
|
|
|
|
private String registerSource;
|
|
|
|
|
|
private String registerSourceName;
|
|
|
|
|
|
private LocalDateTime registerTime;
|
|
|
|
|
|
private String status;
|
|
|
|
|
|
|
|
|
|
|
|
/** 绑定的学生列表 */
|
|
|
|
|
|
private List<StudentVO> students;
|
|
|
|
|
|
|
|
|
|
|
|
@Data
|
|
|
|
|
|
public static class StudentVO {
|
|
|
|
|
|
private Long studentId;
|
|
|
|
|
|
private String studentName;
|
|
|
|
|
|
private String studentNo;
|
|
|
|
|
|
private String schoolName;
|
|
|
|
|
|
private String gradeName;
|
|
|
|
|
|
private String className;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
### 4.4 服务层设计
|
|
|
|
|
|
|
|
|
|
|
|
#### 4.4.1 会员服务接口(IMemberService.java)
|
|
|
|
|
|
|
|
|
|
|
|
```java
|
|
|
|
|
|
package com.pangu.member.service;
|
|
|
|
|
|
|
|
|
|
|
|
import com.baomidou.mybatisplus.extension.service.IService;
|
|
|
|
|
|
import com.pangu.member.domain.Member;
|
|
|
|
|
|
import com.pangu.member.domain.MemberDTO;
|
|
|
|
|
|
import com.pangu.member.domain.MemberVO;
|
|
|
|
|
|
import com.pangu.common.core.page.TableDataInfo;
|
|
|
|
|
|
|
|
|
|
|
|
import java.util.List;
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 会员服务接口
|
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 手机号
|
|
|
|
|
|
* @return 会员信息
|
|
|
|
|
|
*/
|
|
|
|
|
|
Member getMemberByPhone(String phone);
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 检查手机号是否唯一
|
|
|
|
|
|
* @param phone 手机号
|
|
|
|
|
|
* @param memberId 会员ID(编辑时排除自己)
|
|
|
|
|
|
* @return 是否唯一
|
|
|
|
|
|
*/
|
|
|
|
|
|
boolean checkPhoneUnique(String phone, Long memberId);
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 校验会员是否可删除
|
|
|
|
|
|
* @param memberId 会员ID
|
|
|
|
|
|
* @return 校验结果
|
|
|
|
|
|
*/
|
|
|
|
|
|
boolean checkCanDelete(Long memberId);
|
|
|
|
|
|
}
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
#### 4.4.2 会员服务实现(MemberServiceImpl.java)
|
|
|
|
|
|
|
|
|
|
|
|
```java
|
|
|
|
|
|
package com.pangu.member.service.impl;
|
|
|
|
|
|
|
|
|
|
|
|
import cn.hutool.core.util.IdUtil;
|
|
|
|
|
|
import cn.hutool.core.util.RandomUtil;
|
|
|
|
|
|
import cn.hutool.core.util.StrUtil;
|
|
|
|
|
|
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
|
|
|
|
|
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.member.domain.Member;
|
|
|
|
|
|
import com.pangu.member.domain.MemberDTO;
|
|
|
|
|
|
import com.pangu.member.domain.MemberVO;
|
|
|
|
|
|
import com.pangu.member.mapper.MemberMapper;
|
|
|
|
|
|
import com.pangu.member.service.IMemberService;
|
|
|
|
|
|
import com.pangu.student.service.IStudentService;
|
|
|
|
|
|
import lombok.RequiredArgsConstructor;
|
|
|
|
|
|
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
|
|
|
|
|
|
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
|
|
|
|
*/
|
|
|
|
|
|
@Service
|
|
|
|
|
|
@RequiredArgsConstructor
|
|
|
|
|
|
public class MemberServiceImpl extends ServiceImpl<MemberMapper, Member> implements IMemberService {
|
|
|
|
|
|
|
|
|
|
|
|
private final MemberMapper memberMapper;
|
|
|
|
|
|
private final IStudentService studentService;
|
|
|
|
|
|
private final BCryptPasswordEncoder passwordEncoder;
|
|
|
|
|
|
|
|
|
|
|
|
@Override
|
|
|
|
|
|
public TableDataInfo<MemberVO> selectMemberList(MemberDTO memberDTO) {
|
|
|
|
|
|
Page<MemberVO> page = new Page<>(memberDTO.getPageNum(), memberDTO.getPageSize());
|
|
|
|
|
|
List<MemberVO> list = memberMapper.selectMemberVOList(page, memberDTO);
|
|
|
|
|
|
return TableDataInfo.build(list, page.getTotal());
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
@Override
|
|
|
|
|
|
public MemberVO getMemberById(Long memberId) {
|
|
|
|
|
|
MemberVO memberVO = memberMapper.selectMemberVOById(memberId);
|
|
|
|
|
|
if (memberVO == null) {
|
|
|
|
|
|
throw new ServiceException("会员不存在");
|
|
|
|
|
|
}
|
|
|
|
|
|
// 查询绑定的学生
|
|
|
|
|
|
memberVO.setStudents(studentService.selectStudentsByMemberId(memberId));
|
|
|
|
|
|
return memberVO;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
@Override
|
|
|
|
|
|
@Transactional(rollbackFor = Exception.class)
|
|
|
|
|
|
public int insertMember(MemberDTO memberDTO) {
|
|
|
|
|
|
// 校验手机号唯一性
|
|
|
|
|
|
if (!checkPhoneUnique(memberDTO.getPhone(), null)) {
|
|
|
|
|
|
throw new ServiceException("手机号已存在");
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
Member member = new Member();
|
|
|
|
|
|
// 生成会员编号
|
|
|
|
|
|
member.setMemberCode("JS" + System.currentTimeMillis());
|
|
|
|
|
|
member.setPhone(memberDTO.getPhone());
|
|
|
|
|
|
// 默认密码
|
|
|
|
|
|
member.setPassword(passwordEncoder.encode("123456"));
|
|
|
|
|
|
// 昵称自动生成
|
|
|
|
|
|
member.setNickname(StrUtil.isBlank(memberDTO.getNickname())
|
|
|
|
|
|
? "用户" + memberDTO.getPhone().substring(7)
|
|
|
|
|
|
: memberDTO.getNickname());
|
|
|
|
|
|
member.setGender(memberDTO.getGender());
|
|
|
|
|
|
member.setBirthday(memberDTO.getBirthday());
|
|
|
|
|
|
member.setIdentityType(memberDTO.getIdentityType());
|
|
|
|
|
|
|
|
|
|
|
|
// 教师身份必须填写学校信息
|
|
|
|
|
|
if ("2".equals(memberDTO.getIdentityType())) {
|
|
|
|
|
|
validateTeacherInfo(memberDTO);
|
|
|
|
|
|
member.setRegionId(memberDTO.getRegionId());
|
|
|
|
|
|
member.setSchoolId(memberDTO.getSchoolId());
|
|
|
|
|
|
member.setSchoolGradeId(memberDTO.getSchoolGradeId());
|
|
|
|
|
|
member.setSchoolClassId(memberDTO.getSchoolClassId());
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
member.setRegisterSource("3"); // 后台新增
|
|
|
|
|
|
member.setRegisterTime(LocalDateTime.now());
|
|
|
|
|
|
member.setStatus("0");
|
|
|
|
|
|
|
|
|
|
|
|
return memberMapper.insert(member);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
@Override
|
|
|
|
|
|
@Transactional(rollbackFor = Exception.class)
|
|
|
|
|
|
public int updateMember(MemberDTO memberDTO) {
|
|
|
|
|
|
// 校验手机号唯一性
|
|
|
|
|
|
if (!checkPhoneUnique(memberDTO.getPhone(), memberDTO.getMemberId())) {
|
|
|
|
|
|
throw new ServiceException("手机号已存在");
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
Member member = memberMapper.selectById(memberDTO.getMemberId());
|
|
|
|
|
|
if (member == null) {
|
|
|
|
|
|
throw new ServiceException("会员不存在");
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
member.setPhone(memberDTO.getPhone());
|
|
|
|
|
|
member.setNickname(memberDTO.getNickname());
|
|
|
|
|
|
member.setGender(memberDTO.getGender());
|
|
|
|
|
|
member.setBirthday(memberDTO.getBirthday());
|
|
|
|
|
|
member.setIdentityType(memberDTO.getIdentityType());
|
|
|
|
|
|
|
|
|
|
|
|
// 教师身份必须填写学校信息
|
|
|
|
|
|
if ("2".equals(memberDTO.getIdentityType())) {
|
|
|
|
|
|
validateTeacherInfo(memberDTO);
|
|
|
|
|
|
member.setRegionId(memberDTO.getRegionId());
|
|
|
|
|
|
member.setSchoolId(memberDTO.getSchoolId());
|
|
|
|
|
|
member.setSchoolGradeId(memberDTO.getSchoolGradeId());
|
|
|
|
|
|
member.setSchoolClassId(memberDTO.getSchoolClassId());
|
|
|
|
|
|
} else {
|
|
|
|
|
|
// 家长不需要学校信息
|
|
|
|
|
|
member.setRegionId(null);
|
|
|
|
|
|
member.setSchoolId(null);
|
|
|
|
|
|
member.setSchoolGradeId(null);
|
|
|
|
|
|
member.setSchoolClassId(null);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return memberMapper.updateById(member);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
@Override
|
|
|
|
|
|
@Transactional(rollbackFor = Exception.class)
|
|
|
|
|
|
public int deleteMember(Long memberId) {
|
|
|
|
|
|
// 检查是否可删除
|
|
|
|
|
|
if (!checkCanDelete(memberId)) {
|
|
|
|
|
|
throw new ServiceException("该会员已绑定学生,请先解绑学生后再删除");
|
|
|
|
|
|
}
|
|
|
|
|
|
return memberMapper.deleteById(memberId);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
@Override
|
|
|
|
|
|
public String resetPassword(Long memberId) {
|
|
|
|
|
|
Member member = memberMapper.selectById(memberId);
|
|
|
|
|
|
if (member == null) {
|
|
|
|
|
|
throw new ServiceException("会员不存在");
|
|
|
|
|
|
}
|
|
|
|
|
|
// 生成8位随机密码
|
|
|
|
|
|
String newPassword = RandomUtil.randomString(8);
|
|
|
|
|
|
member.setPassword(passwordEncoder.encode(newPassword));
|
|
|
|
|
|
memberMapper.updateById(member);
|
|
|
|
|
|
return newPassword;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
@Override
|
|
|
|
|
|
public int changeStatus(Long memberId, String status) {
|
|
|
|
|
|
Member member = new Member();
|
|
|
|
|
|
member.setMemberId(memberId);
|
|
|
|
|
|
member.setStatus(status);
|
|
|
|
|
|
return memberMapper.updateById(member);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
@Override
|
|
|
|
|
|
@Transactional(rollbackFor = Exception.class)
|
|
|
|
|
|
public int bindStudent(Long memberId, Long studentId) {
|
|
|
|
|
|
Member member = memberMapper.selectById(memberId);
|
|
|
|
|
|
if (member == null) {
|
|
|
|
|
|
throw new ServiceException("会员不存在");
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 教师只能绑定本校学生
|
|
|
|
|
|
if ("2".equals(member.getIdentityType())) {
|
|
|
|
|
|
if (!studentService.isStudentInSchool(studentId, member.getSchoolId())) {
|
|
|
|
|
|
throw new ServiceException("教师只能绑定本校学生");
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return studentService.updateStudentMember(studentId, memberId);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
@Override
|
|
|
|
|
|
@Transactional(rollbackFor = Exception.class)
|
|
|
|
|
|
public int unbindStudent(Long memberId, Long studentId) {
|
|
|
|
|
|
// 解绑时将学生的memberId置空或设置为默认值
|
|
|
|
|
|
return studentService.unbindStudent(studentId, memberId);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
@Override
|
|
|
|
|
|
public Member getMemberByPhone(String phone) {
|
|
|
|
|
|
return memberMapper.selectOne(
|
|
|
|
|
|
new LambdaQueryWrapper<Member>().eq(Member::getPhone, phone)
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
@Override
|
|
|
|
|
|
public boolean checkPhoneUnique(String phone, Long memberId) {
|
|
|
|
|
|
LambdaQueryWrapper<Member> wrapper = new LambdaQueryWrapper<>();
|
|
|
|
|
|
wrapper.eq(Member::getPhone, phone);
|
|
|
|
|
|
if (memberId != null) {
|
|
|
|
|
|
wrapper.ne(Member::getMemberId, memberId);
|
|
|
|
|
|
}
|
|
|
|
|
|
return memberMapper.selectCount(wrapper) == 0;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
@Override
|
|
|
|
|
|
public boolean checkCanDelete(Long memberId) {
|
|
|
|
|
|
// 检查是否有绑定的学生
|
|
|
|
|
|
return studentService.countByMemberId(memberId) == 0;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 校验教师信息完整性
|
|
|
|
|
|
*/
|
|
|
|
|
|
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("请选择所属班级");
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
### 4.5 控制器设计
|
|
|
|
|
|
|
|
|
|
|
|
```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 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
|
|
|
|
*/
|
|
|
|
|
|
@RestController
|
|
|
|
|
|
@RequestMapping("/member")
|
|
|
|
|
|
@RequiredArgsConstructor
|
|
|
|
|
|
public class MemberController extends BaseController {
|
|
|
|
|
|
|
|
|
|
|
|
private final IMemberService memberService;
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 查询会员列表
|
|
|
|
|
|
*/
|
|
|
|
|
|
@PreAuthorize("@ss.hasPermi('user:member:list')")
|
|
|
|
|
|
@GetMapping("/list")
|
|
|
|
|
|
public TableDataInfo<MemberVO> list(MemberDTO memberDTO) {
|
|
|
|
|
|
return memberService.selectMemberList(memberDTO);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 获取会员详情
|
|
|
|
|
|
*/
|
|
|
|
|
|
@PreAuthorize("@ss.hasPermi('user:member:query')")
|
|
|
|
|
|
@GetMapping("/{memberId}")
|
|
|
|
|
|
public AjaxResult getInfo(@PathVariable Long memberId) {
|
|
|
|
|
|
return success(memberService.getMemberById(memberId));
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 新增会员
|
|
|
|
|
|
*/
|
|
|
|
|
|
@PreAuthorize("@ss.hasPermi('user:member:add')")
|
|
|
|
|
|
@Log(title = "会员管理", businessType = BusinessType.INSERT)
|
|
|
|
|
|
@PostMapping
|
|
|
|
|
|
public AjaxResult add(@Validated @RequestBody MemberDTO memberDTO) {
|
|
|
|
|
|
return toAjax(memberService.insertMember(memberDTO));
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 修改会员
|
|
|
|
|
|
*/
|
|
|
|
|
|
@PreAuthorize("@ss.hasPermi('user:member:edit')")
|
|
|
|
|
|
@Log(title = "会员管理", businessType = BusinessType.UPDATE)
|
|
|
|
|
|
@PutMapping
|
|
|
|
|
|
public AjaxResult edit(@Validated @RequestBody MemberDTO memberDTO) {
|
|
|
|
|
|
return toAjax(memberService.updateMember(memberDTO));
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 删除会员
|
|
|
|
|
|
*/
|
|
|
|
|
|
@PreAuthorize("@ss.hasPermi('user:member:remove')")
|
|
|
|
|
|
@Log(title = "会员管理", businessType = BusinessType.DELETE)
|
|
|
|
|
|
@DeleteMapping("/{memberId}")
|
|
|
|
|
|
public AjaxResult remove(@PathVariable Long memberId) {
|
|
|
|
|
|
return toAjax(memberService.deleteMember(memberId));
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 重置密码
|
|
|
|
|
|
*/
|
|
|
|
|
|
@PreAuthorize("@ss.hasPermi('user:member:resetPwd')")
|
|
|
|
|
|
@Log(title = "会员管理", businessType = BusinessType.UPDATE)
|
|
|
|
|
|
@PutMapping("/resetPwd/{memberId}")
|
|
|
|
|
|
public AjaxResult resetPwd(@PathVariable Long memberId) {
|
|
|
|
|
|
String newPassword = memberService.resetPassword(memberId);
|
|
|
|
|
|
return AjaxResult.success("密码重置成功").put("password", newPassword);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 修改状态
|
|
|
|
|
|
*/
|
|
|
|
|
|
@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()));
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 绑定学生
|
|
|
|
|
|
*/
|
|
|
|
|
|
@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()));
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 解绑学生
|
|
|
|
|
|
*/
|
|
|
|
|
|
@PreAuthorize("@ss.hasPermi('user:member:edit')")
|
|
|
|
|
|
@Log(title = "会员管理", businessType = BusinessType.UPDATE)
|
|
|
|
|
|
@DeleteMapping("/unbindStudent/{memberId}/{studentId}")
|
|
|
|
|
|
public AjaxResult unbindStudent(@PathVariable Long memberId, @PathVariable Long studentId) {
|
|
|
|
|
|
return toAjax(memberService.unbindStudent(memberId, studentId));
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
### 4.6 数据访问层
|
|
|
|
|
|
|
|
|
|
|
|
```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> {
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 查询会员列表(带关联信息)
|
|
|
|
|
|
*/
|
|
|
|
|
|
List<MemberVO> selectMemberVOList(Page<MemberVO> page, @Param("dto") MemberDTO dto);
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 根据ID查询会员详情(带关联信息)
|
|
|
|
|
|
*/
|
|
|
|
|
|
MemberVO selectMemberVOById(@Param("memberId") Long memberId);
|
|
|
|
|
|
}
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
**Mapper 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="nickname" column="nickname"/>
|
|
|
|
|
|
<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, m.phone, m.nickname, 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
|
|
|
|
|
|
LEFT JOIN pg_school_grade sg ON m.school_grade_id = sg.id
|
|
|
|
|
|
LEFT JOIN pg_grade g ON sg.grade_id = g.grade_id
|
|
|
|
|
|
LEFT JOIN pg_school_class sc ON m.school_class_id = sc.id
|
|
|
|
|
|
LEFT JOIN pg_class c ON sc.class_id = c.class_id
|
|
|
|
|
|
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 m.register_time >= #{dto.beginTime}
|
|
|
|
|
|
</if>
|
|
|
|
|
|
<if test="dto.endTime != null">
|
|
|
|
|
|
AND m.register_time <= #{dto.endTime}
|
|
|
|
|
|
</if>
|
|
|
|
|
|
<!-- 数据权限 -->
|
|
|
|
|
|
${dto.params.dataScope}
|
|
|
|
|
|
ORDER BY m.register_time DESC
|
|
|
|
|
|
</select>
|
|
|
|
|
|
|
|
|
|
|
|
<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
|
|
|
|
|
|
LEFT JOIN pg_school_grade sg ON m.school_grade_id = sg.id
|
|
|
|
|
|
LEFT JOIN pg_grade g ON sg.grade_id = g.grade_id
|
|
|
|
|
|
LEFT JOIN pg_school_class sc ON m.school_class_id = sc.id
|
|
|
|
|
|
LEFT JOIN pg_class c ON sc.class_id = c.class_id
|
|
|
|
|
|
WHERE m.member_id = #{memberId} AND m.del_flag = '0'
|
|
|
|
|
|
</select>
|
|
|
|
|
|
|
|
|
|
|
|
</mapper>
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
|
|
|
|
|
## 5. 数据库设计
|
|
|
|
|
|
|
|
|
|
|
|
### 5.1 表结构
|
|
|
|
|
|
|
|
|
|
|
|
详见《数据库设计文档_v1.0.md》第3.8节"会员表(pg_member)"。
|
|
|
|
|
|
|
|
|
|
|
|
### 5.2 索引设计
|
|
|
|
|
|
|
|
|
|
|
|
| 索引名 | 索引类型 | 索引字段 | 说明 |
|
|
|
|
|
|
|-------|---------|---------|------|
|
|
|
|
|
|
| uk_member_code | UNIQUE | member_code | 会员编号唯一 |
|
|
|
|
|
|
| uk_phone | UNIQUE | phone | 手机号唯一 |
|
|
|
|
|
|
| idx_open_id | INDEX | open_id | 微信登录查询 |
|
|
|
|
|
|
| idx_school_id | INDEX | school_id | 按学校查询 |
|
|
|
|
|
|
| idx_identity_type | INDEX | identity_type | 按身份类型查询 |
|
|
|
|
|
|
| idx_register_time | INDEX | register_time | 按注册时间排序 |
|
|
|
|
|
|
|
|
|
|
|
|
### 5.3 示例数据
|
|
|
|
|
|
|
|
|
|
|
|
```sql
|
|
|
|
|
|
INSERT INTO pg_member (member_id, member_code, phone, password, nickname, gender, identity_type, region_id, school_id, register_source, register_time, status) VALUES
|
|
|
|
|
|
(1, 'JS123123123', '13207166213', '$2a$10$7JB720yubVSZvUI0rEqK/.VqGOZTH.ulu33dHOiBE8ByOhJIrdAu2', '张三家长', '1', '1', NULL, NULL, '1', NOW(), '0'),
|
|
|
|
|
|
(2, 'JS123123124', '13807166214', '$2a$10$7JB720yubVSZvUI0rEqK/.VqGOZTH.ulu33dHOiBE8ByOhJIrdAu2', '李老师', '2', '2', 111, 1, '1', NOW(), '0');
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
|
|
|
|
|
## 6. 接口设计
|
|
|
|
|
|
|
|
|
|
|
|
详见《接口设计文档_v1.0.md》第4章"会员管理接口"。
|
|
|
|
|
|
|
|
|
|
|
|
### 6.1 接口清单
|
|
|
|
|
|
|
|
|
|
|
|
| 接口路径 | 方法 | 说明 | 权限 |
|
|
|
|
|
|
|---------|------|------|------|
|
|
|
|
|
|
| GET /api/member/list | GET | 查询会员列表 | user:member:list |
|
|
|
|
|
|
| GET /api/member/{id} | GET | 获取会员详情 | user:member:query |
|
|
|
|
|
|
| POST /api/member | POST | 新增会员 | user:member:add |
|
|
|
|
|
|
| PUT /api/member | PUT | 修改会员 | user:member:edit |
|
|
|
|
|
|
| DELETE /api/member/{id} | DELETE | 删除会员 | user:member:remove |
|
|
|
|
|
|
| PUT /api/member/resetPwd/{id} | PUT | 重置密码 | user:member:resetPwd |
|
|
|
|
|
|
| PUT /api/member/changeStatus | PUT | 修改状态 | user:member:edit |
|
|
|
|
|
|
| POST /api/member/bindStudent | POST | 绑定学生 | user:member:edit |
|
|
|
|
|
|
| DELETE /api/member/unbindStudent/{memberId}/{studentId} | DELETE | 解绑学生 | user:member:edit |
|
|
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
|
|
|
|
|
## 7. 开发阶段计划
|
|
|
|
|
|
|
|
|
|
|
|
### 7.1 阶段划分
|
|
|
|
|
|
|
|
|
|
|
|
| 阶段 | 任务 | 交付物 |
|
|
|
|
|
|
|-----|------|-------|
|
|
|
|
|
|
| **阶段一:后端开发** | 数据库表创建、实体类、Mapper、Service、Controller | 接口可调用 |
|
|
|
|
|
|
| **阶段二:前端开发** | 页面组件开发、API对接、功能联调 | 页面可操作 |
|
|
|
|
|
|
| **阶段三:功能测试** | 单元测试、接口测试、功能测试 | 测试报告 |
|
|
|
|
|
|
| **阶段四:集成部署** | 代码合并、环境部署、验收测试 | 上线部署 |
|
|
|
|
|
|
|
|
|
|
|
|
### 7.2 详细任务分解
|
|
|
|
|
|
|
|
|
|
|
|
#### 阶段一:后端开发
|
|
|
|
|
|
|
|
|
|
|
|
| 序号 | 任务 | 负责人 | 备注 |
|
|
|
|
|
|
|:---:|-----|-------|------|
|
|
|
|
|
|
| 1.1 | 创建pg_member表 | - | 包含索引 |
|
|
|
|
|
|
| 1.2 | 创建Member实体类 | - | 含VO/DTO |
|
|
|
|
|
|
| 1.3 | 开发MemberMapper | - | 含XML映射 |
|
|
|
|
|
|
| 1.4 | 开发IMemberService | - | 接口定义 |
|
|
|
|
|
|
| 1.5 | 开发MemberServiceImpl | - | 业务逻辑 |
|
|
|
|
|
|
| 1.6 | 开发MemberController | - | REST接口 |
|
|
|
|
|
|
| 1.7 | 配置权限菜单 | - | 菜单和按钮 |
|
|
|
|
|
|
| 1.8 | 接口测试 | - | Postman测试 |
|
|
|
|
|
|
|
|
|
|
|
|
#### 阶段二:前端开发
|
|
|
|
|
|
|
|
|
|
|
|
| 序号 | 任务 | 负责人 | 备注 |
|
|
|
|
|
|
|:---:|-----|-------|------|
|
|
|
|
|
|
| 2.1 | 开发member.js API | - | 接口封装 |
|
|
|
|
|
|
| 2.2 | 开发会员列表页 | - | index.vue |
|
|
|
|
|
|
| 2.3 | 开发会员编辑页 | - | form.vue |
|
|
|
|
|
|
| 2.4 | 开发学生选择弹窗 | - | 组件 |
|
|
|
|
|
|
| 2.5 | 开发密码显示弹窗 | - | 组件 |
|
|
|
|
|
|
| 2.6 | 配置路由 | - | router配置 |
|
|
|
|
|
|
| 2.7 | 前后端联调 | - | 功能验证 |
|
|
|
|
|
|
|
|
|
|
|
|
#### 阶段三:功能测试
|
|
|
|
|
|
|
|
|
|
|
|
| 序号 | 任务 | 负责人 | 备注 |
|
|
|
|
|
|
|:---:|-----|-------|------|
|
|
|
|
|
|
| 3.1 | 编写单元测试 | - | JUnit |
|
|
|
|
|
|
| 3.2 | 接口测试 | - | API测试 |
|
|
|
|
|
|
| 3.3 | 功能测试 | - | 页面操作 |
|
|
|
|
|
|
| 3.4 | 业务规则验证 | - | 规则校验 |
|
|
|
|
|
|
| 3.5 | 权限测试 | - | 角色权限 |
|
|
|
|
|
|
| 3.6 | 问题修复 | - | Bug修复 |
|
|
|
|
|
|
|
|
|
|
|
|
#### 阶段四:集成部署
|
|
|
|
|
|
|
|
|
|
|
|
| 序号 | 任务 | 负责人 | 备注 |
|
|
|
|
|
|
|:---:|-----|-------|------|
|
|
|
|
|
|
| 4.1 | 代码评审 | - | Code Review |
|
|
|
|
|
|
| 4.2 | 合并代码 | - | Git操作 |
|
|
|
|
|
|
| 4.3 | 测试环境部署 | - | 环境配置 |
|
|
|
|
|
|
| 4.4 | UAT验收测试 | - | 用户验收 |
|
|
|
|
|
|
| 4.5 | 生产环境部署 | - | 上线 |
|
|
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
|
|
|
|
|
## 8. 测试方案
|
|
|
|
|
|
|
|
|
|
|
|
### 8.1 单元测试
|
|
|
|
|
|
|
|
|
|
|
|
```java
|
|
|
|
|
|
@SpringBootTest
|
|
|
|
|
|
class MemberServiceTest {
|
|
|
|
|
|
|
|
|
|
|
|
@Autowired
|
|
|
|
|
|
private IMemberService memberService;
|
|
|
|
|
|
|
|
|
|
|
|
@Test
|
|
|
|
|
|
void testInsertMember() {
|
|
|
|
|
|
MemberDTO dto = new MemberDTO();
|
|
|
|
|
|
dto.setPhone("13812345678");
|
|
|
|
|
|
dto.setIdentityType("1");
|
|
|
|
|
|
int result = memberService.insertMember(dto);
|
|
|
|
|
|
assertEquals(1, result);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
@Test
|
|
|
|
|
|
void testCheckPhoneUnique() {
|
|
|
|
|
|
boolean unique = memberService.checkPhoneUnique("13812345678", null);
|
|
|
|
|
|
// 根据数据库状态断言
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
@Test
|
|
|
|
|
|
void testResetPassword() {
|
|
|
|
|
|
String newPwd = memberService.resetPassword(1L);
|
|
|
|
|
|
assertNotNull(newPwd);
|
|
|
|
|
|
assertEquals(8, newPwd.length());
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
### 8.2 功能测试用例
|
|
|
|
|
|
|
|
|
|
|
|
| 用例编号 | 用例名称 | 前置条件 | 测试步骤 | 预期结果 |
|
|
|
|
|
|
|---------|---------|---------|---------|---------|
|
|
|
|
|
|
| TC-001 | 会员列表查询 | 已登录 | 1.进入会员管理页面 2.输入搜索条件 3.点击搜索 | 列表显示符合条件的数据 |
|
|
|
|
|
|
| TC-002 | 新增家长会员 | 已登录 | 1.点击新增 2.填写手机号 3.选择家长身份 4.点击保存 | 新增成功,列表显示新数据 |
|
|
|
|
|
|
| TC-003 | 新增教师会员 | 已登录 | 1.点击新增 2.填写手机号 3.选择教师身份 4.选择学校信息 5.保存 | 新增成功 |
|
|
|
|
|
|
| TC-004 | 手机号重复校验 | 已存在会员 | 1.新增会员 2.输入已存在的手机号 3.保存 | 提示"手机号已存在" |
|
|
|
|
|
|
| TC-005 | 教师信息必填校验 | 已登录 | 1.新增会员 2.选择教师身份 3.不填学校信息 4.保存 | 提示相关必填项 |
|
|
|
|
|
|
| TC-006 | 重置密码 | 已存在会员 | 1.点击重置密码 2.确认操作 | 弹窗显示新密码,可复制 |
|
|
|
|
|
|
| TC-007 | 删除会员(无绑定) | 会员无绑定学生 | 1.点击删除 2.确认 | 删除成功 |
|
|
|
|
|
|
| TC-008 | 删除会员(有绑定) | 会员已绑定学生 | 1.点击删除 2.确认 | 提示"请先解绑学生" |
|
|
|
|
|
|
| TC-009 | 绑定学生-家长 | 家长会员 | 1.编辑会员 2.点击绑定学生 3.选择任意学生 | 绑定成功 |
|
|
|
|
|
|
| TC-010 | 绑定学生-教师 | 教师会员 | 1.编辑会员 2.点击绑定学生 3.选择非本校学生 | 提示"只能绑定本校学生" |
|
|
|
|
|
|
| TC-011 | 禁用会员 | 已存在会员 | 1.切换状态开关为禁用 | 状态变更成功 |
|
|
|
|
|
|
| TC-012 | 数据权限-分公司 | 分公司用户登录 | 1.进入会员列表 | 只显示所属区域会员 |
|
|
|
|
|
|
|
|
|
|
|
|
### 8.3 性能测试
|
|
|
|
|
|
|
|
|
|
|
|
| 测试项 | 测试场景 | 性能指标 |
|
|
|
|
|
|
|-------|---------|---------|
|
|
|
|
|
|
| 列表查询 | 10万条数据分页查询 | 响应时间 ≤ 500ms |
|
|
|
|
|
|
| 新增会员 | 并发100用户新增 | 成功率 ≥ 99% |
|
|
|
|
|
|
| 重置密码 | 并发50用户操作 | 响应时间 ≤ 300ms |
|
|
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
|
|
|
|
|
## 9. 部署方案
|
|
|
|
|
|
|
|
|
|
|
|
### 9.1 环境配置
|
|
|
|
|
|
|
|
|
|
|
|
| 环境 | 用途 | 配置 |
|
|
|
|
|
|
|-----|------|------|
|
|
|
|
|
|
| 开发环境 | 开发调试 | 本地MySQL、Redis |
|
|
|
|
|
|
| 测试环境 | 功能测试 | 测试服务器 |
|
|
|
|
|
|
| 生产环境 | 正式运行 | 生产服务器集群 |
|
|
|
|
|
|
|
|
|
|
|
|
### 9.2 配置项
|
|
|
|
|
|
|
|
|
|
|
|
```yaml
|
|
|
|
|
|
# application-prod.yml
|
|
|
|
|
|
pangu:
|
|
|
|
|
|
member:
|
|
|
|
|
|
# 默认密码(批量导入时使用)
|
|
|
|
|
|
default-password: 123456
|
|
|
|
|
|
# 密码重置长度
|
|
|
|
|
|
reset-password-length: 8
|
|
|
|
|
|
# 会员编号前缀
|
|
|
|
|
|
code-prefix: JS
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
### 9.3 菜单配置
|
|
|
|
|
|
|
|
|
|
|
|
```sql
|
|
|
|
|
|
-- 会员管理菜单
|
|
|
|
|
|
INSERT INTO sys_menu (menu_id, menu_name, parent_id, order_num, path, component, menu_type, perms, icon) VALUES
|
|
|
|
|
|
(2000, '用户管理', 0, 2, 'user', NULL, 'M', NULL, 'user'),
|
|
|
|
|
|
(2010, '会员管理', 2000, 1, 'member', 'user/member/index', 'C', 'user:member:list', 'peoples'),
|
|
|
|
|
|
(2011, '会员查询', 2010, 1, '#', '', 'F', 'user:member:query', '#'),
|
|
|
|
|
|
(2012, '会员新增', 2010, 2, '#', '', 'F', 'user:member:add', '#'),
|
|
|
|
|
|
(2013, '会员修改', 2010, 3, '#', '', 'F', 'user:member:edit', '#'),
|
|
|
|
|
|
(2014, '会员删除', 2010, 4, '#', '', 'F', 'user:member:remove', '#'),
|
|
|
|
|
|
(2015, '重置密码', 2010, 5, '#', '', 'F', 'user:member:resetPwd', '#');
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
|
|
|
|
|
## 10. 风险评估
|
|
|
|
|
|
|
|
|
|
|
|
### 10.1 技术风险
|
|
|
|
|
|
|
|
|
|
|
|
| 风险项 | 风险等级 | 应对措施 |
|
|
|
|
|
|
|-------|:-------:|---------|
|
|
|
|
|
|
| 手机号唯一性并发问题 | 中 | 使用数据库唯一索引 + 业务层校验 |
|
|
|
|
|
|
| 密码安全性 | 高 | BCrypt加密,密码复杂度校验 |
|
|
|
|
|
|
| 数据权限泄露 | 高 | 严格的数据权限控制 |
|
|
|
|
|
|
| 接口性能问题 | 中 | 合理的索引设计,分页查询 |
|
|
|
|
|
|
|
|
|
|
|
|
### 10.2 业务风险
|
|
|
|
|
|
|
|
|
|
|
|
| 风险项 | 风险等级 | 应对措施 |
|
|
|
|
|
|
|-------|:-------:|---------|
|
|
|
|
|
|
| 教师绑定错误学生 | 中 | 严格校验本校学生 |
|
|
|
|
|
|
| 误删除会员 | 低 | 软删除机制,删除前校验 |
|
|
|
|
|
|
| 密码泄露 | 高 | 重置密码后一次性显示,建议用户修改 |
|
|
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
|
|
|
|
|
## 审核签字
|
|
|
|
|
|
|
|
|
|
|
|
| 角色 | 姓名 | 日期 | 签字 |
|
|
|
|
|
|
|-----|------|------|------|
|
|
|
|
|
|
| 技术负责人 | | | |
|
|
|
|
|
|
| 前端负责人 | | | |
|
|
|
|
|
|
| 后端负责人 | | | |
|
|
|
|
|
|
| 测试负责人 | | | |
|
|
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
|
|
|
|
|
*文档结束*
|