# 会员管理模块技术方案

---

| 文档信息 | 内容 |
|---------|------|
| **文档版本** | 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;

/**
 * 会员实体
 * @author pangu
 */
@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;

/**
 * 会员视图对象
 * @author pangu
 */
@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;

/**
 * 会员服务接口
 * @author pangu
 */
public interface IMemberService extends IService<Member> {
    
    /**
     * 查询会员列表
     * @param memberDTO 查询条件
     * @return 会员列表
     */
    TableDataInfo<MemberVO> selectMemberList(MemberDTO memberDTO);
    
    /**
     * 根据ID获取会员详情
     * @param memberId 会员ID
     * @return 会员详情
     */
    MemberVO getMemberById(Long memberId);
    
    /**
     * 新增会员
     * @param memberDTO 会员信息
     * @return 结果
     */
    int insertMember(MemberDTO memberDTO);
    
    /**
     * 修改会员
     * @param memberDTO 会员信息
     * @return 结果
     */
    int updateMember(MemberDTO memberDTO);
    
    /**
     * 删除会员
     * @param memberId 会员ID
     * @return 结果
     */
    int deleteMember(Long memberId);
    
    /**
     * 重置密码
     * @param memberId 会员ID
     * @return 新密码
     */
    String resetPassword(Long memberId);
    
    /**
     * 修改会员状态
     * @param memberId 会员ID
     * @param status 状态
     * @return 结果
     */
    int changeStatus(Long memberId, String status);
    
    /**
     * 绑定学生
     * @param memberId 会员ID
     * @param studentId 学生ID
     * @return 结果
     */
    int bindStudent(Long memberId, Long studentId);
    
    /**
     * 解绑学生
     * @param memberId 会员ID
     * @param studentId 学生ID
     * @return 结果
     */
    int unbindStudent(Long memberId, Long studentId);
    
    /**
     * 根据手机号查询会员
     * @param phone 手机号
     * @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;

/**
 * 会员服务实现
 * @author pangu
 */
@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.*;

/**
 * 会员管理控制器
 * @author pangu
 */
@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;

/**
 * 会员数据访问
 * @author pangu
 */
@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 &gt;= #{dto.beginTime}
        </if>
        <if test="dto.endTime != null">
            AND m.register_time &lt;= #{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 业务风险

| 风险项 | 风险等级 | 应对措施 |
|-------|:-------:|---------|
| 教师绑定错误学生 | 中 | 严格校验本校学生 |
| 误删除会员 | 低 | 软删除机制，删除前校验 |
| 密码泄露 | 高 | 重置密码后一次性显示，建议用户修改 |

---

## 审核签字

| 角色 | 姓名 | 日期 | 签字 |
|-----|------|------|------|
| 技术负责人 | | | |
| 前端负责人 | | | |
| 后端负责人 | | | |
| 测试负责人 | | | |

---

*文档结束*
