会员管理模块技术方案
| 文档信息 |
内容 |
| 文档版本 |
V1.0 |
| 项目名称 |
盘古用户平台(Pangu User Platform) |
| 模块名称 |
会员管理模块 |
| 编写团队 |
pangu |
| 创建日期 |
2026-01-31 |
| 评审状态 |
待评审 |
修订记录
| 版本 |
日期 |
修订人 |
修订内容 |
| V1.0 |
2026-01-31 |
pangu |
初稿 |
目录
- 概述
- 需求分析
- 前端技术方案
- 后端技术方案
- 数据库设计
- 接口设计
- 开发阶段计划
- 测试方案
- 部署方案
- 风险评估
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 ... > │ │
│ └──────────────────────┘ │
└─────────────────────────────────────────────────────────────────┘
核心代码结构
<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)
页面布局
┌─────────────────────────────────────────────────────────────────┐
│ 页面标题:新增会员 / 编辑会员 │
├─────────────────────────────────────────────────────────────────┤
│ 基本信息 │
│ ┌──────────────────────────────────────────────────────────┐ │
│ │ 手机号 * [_______________] │ │
│ │ 昵称 [_______________] │ │
│ │ 性别 ○ 未知 ○ 男 ○ 女 │ │
│ │ 出生日期 [_______________] │ │
│ │ 身份类型 * [___家长▼_______] │ │
│ └──────────────────────────────────────────────────────────┘ │
│ │
│ 教师信息(身份类型为教师时显示) │
│ ┌──────────────────────────────────────────────────────────┐ │
│ │ 所属区域 * [___区域选择___] │ │
│ │ 所属学校 * [___学校选择___] │ │
│ │ 所属年级 * [___年级选择___] │ │
│ │ 所属班级 * [___班级选择___] │ │
│ └──────────────────────────────────────────────────────────┘ │
│ │
│ 绑定学生 │
│ ┌──────────────────────────────────────────────────────────┐ │
│ │ ┌────────┐ │ │
│ │ │ + 绑定 │ │ │
│ │ └────────┘ │ │
│ │ ┌──────┬────────┬────────┬────────┬────────┬────────┐ │ │
│ │ │ 姓名 │ 学号 │ 学校 │ 年级 │ 班级 │ 操作 │ │ │
│ │ ├──────┼────────┼────────┼────────┼────────┼────────┤ │ │
│ │ │ ... │ ... │ ... │ ... │ ... │ 解绑 │ │ │
│ │ └──────┴────────┴────────┴────────┴────────┴────────┘ │ │
│ └──────────────────────────────────────────────────────────┘ │
│ │
│ ┌────────┐ ┌────────┐ │
│ │ 保存 │ │ 取消 │ │
│ └────────┘ └────────┘ │
└─────────────────────────────────────────────────────────────────┘
核心代码结构
<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 状态管理
// 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 表单校验规则
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 工具函数
// 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)
package com.pangu.member.domain;
import com.baomidou.mybatisplus.annotation.*;
import lombok.Data;
import java.time.LocalDate;
import java.time.LocalDateTime;
/**
* 会员实体
* @author 湖北新华业务中台研发团队
*/
@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)
package com.pangu.member.domain;
import lombok.Data;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.util.List;
/**
* 会员视图对象
* @author 湖北新华业务中台研发团队
*/
@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)
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;
/**
* 会员服务接口
* @author 湖北新华业务中台研发团队
*/
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)
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;
/**
* 会员服务实现
* @author 湖北新华业务中台研发团队
*/
@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 控制器设计
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.*;
/**
* 会员管理控制器
* @author 湖北新华业务中台研发团队
*/
@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 数据访问层
package com.pangu.member.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.pangu.member.domain.Member;
import com.pangu.member.domain.MemberDTO;
import com.pangu.member.domain.MemberVO;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import java.util.List;
/**
* 会员数据访问
* @author 湖北新华业务中台研发团队
*/
@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 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 示例数据
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 单元测试
@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 配置项
# application-prod.yml
pangu:
member:
# 默认密码(批量导入时使用)
default-password: 123456
# 密码重置长度
reset-password-length: 8
# 会员编号前缀
code-prefix: JS
9.3 菜单配置
-- 会员管理菜单
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 业务风险
| 风险项 |
风险等级 |
应对措施 |
| 教师绑定错误学生 |
中 |
严格校验本校学生 |
| 误删除会员 |
低 |
软删除机制,删除前校验 |
| 密码泄露 |
高 |
重置密码后一次性显示,建议用户修改 |
审核签字
| 角色 |
姓名 |
日期 |
签字 |
| 技术负责人 |
|
|
|
| 前端负责人 |
|
|
|
| 后端负责人 |
|
|
|
| 测试负责人 |
|
|
|
文档结束