feat: 会员区域字段、管理后台区域编辑与列表展示;分公司账号密码重置脚本;业务按钮权限修复方案

This commit is contained in:
神码-方晓辉 2026-02-05 15:11:24 +08:00
parent add00c9992
commit e04cf47bdf
15 changed files with 1155 additions and 5 deletions

View File

@ -0,0 +1,13 @@
-- ============================================================
-- 重置分公司用户 wuhan 的登录密码为 admin123
-- 使用方式:在对应库中执行本脚本后,用 wuhan / admin123 登录
-- ============================================================
USE `pguser-db`;
-- 与 init 脚本中相同的 BCrypt 哈希admin123
UPDATE sys_user
SET password = '$2a$10$7JB720yubVSZvUI0rEqK/.VqGOZTH.ulu33dHOiBE8ByOhJIrdAu2'
WHERE user_name = 'wuhan';
-- 若上面影响行数为 0说明 wuhan 用户不存在,请先执行 system_init_data.sql 初始化

View File

@ -22,4 +22,7 @@ public class H5MemberUpdateDto {
@Schema(description = "生日", example = "1990-01-15")
private Date birthday;
@Schema(description = "所在区域ID", example = "420102")
private Long regionId;
}

View File

@ -36,4 +36,7 @@ public class H5RegisterDto {
@NotBlank(message = "密码不能为空")
@Size(min = 6, message = "密码至少6位")
private String password;
@Schema(description = "所在区域ID", requiredMode = Schema.RequiredMode.REQUIRED, example = "420102")
private Long regionId;
}

View File

@ -39,6 +39,9 @@ public class H5MemberInfoVo {
@Schema(description = "注册时间", example = "2024-01-01 12:00:00")
private Date registerTime;
@Schema(description = "所在区域ID", example = "420102")
private Long regionId;
@Schema(description = "教育身份列表")
private List<H5EducationVo> educations;

View File

@ -316,6 +316,7 @@ public class H5AuthServiceImpl implements H5AuthService {
member.setPassword(BCrypt.hashpw(dto.getPassword()));
member.setMemberCode(generateMemberCode());
member.setNickname("user_" + dto.getPhone().substring(7));
member.setRegionId(dto.getRegionId()); // 保存区域ID
member.setRegisterSource("2"); // H5注册
member.setRegisterTime(new Date());
member.setStatus("0");

View File

@ -89,6 +89,7 @@ public class H5MemberServiceImpl implements H5MemberService {
vo.setGender(member.getGender());
vo.setBirthday(member.getBirthday());
vo.setRegisterTime(member.getRegisterTime());
vo.setRegionId(member.getRegionId());
// 获取教育身份列表
vo.setEducations(getEducations());
@ -107,7 +108,7 @@ public class H5MemberServiceImpl implements H5MemberService {
throw new ServiceException("会员不存在");
}
// 只允许修改昵称性别生日
// 只允许修改昵称性别生日区域
if (StringUtils.isNotBlank(dto.getNickname())) {
member.setNickname(dto.getNickname());
}
@ -117,6 +118,9 @@ public class H5MemberServiceImpl implements H5MemberService {
if (dto.getBirthday() != null) {
member.setBirthday(dto.getBirthday());
}
if (dto.getRegionId() != null) {
member.setRegionId(dto.getRegionId());
}
memberMapper.updateById(member);
log.info("H5会员信息修改: memberId={}", memberId);

View File

@ -61,6 +61,11 @@ public class PgMember extends BaseEntity {
private String tenantId;
/**
* 区域ID
*/
private Long regionId;
@TableLogic
private String delFlag;

View File

@ -37,6 +37,9 @@ public class MemberSaveDto {
@Schema(description = "状态0正常 1停用")
private String status;
@Schema(description = "所在区域ID")
private Long regionId;
@Schema(description = "教育身份列表")
private List<EducationDto> educations;

View File

@ -114,6 +114,7 @@ public class PgMemberServiceImpl implements IPgMemberService {
member.setGender(dto.getGender());
member.setBirthday(dto.getBirthday());
member.setStatus(dto.getStatus());
member.setRegionId(dto.getRegionId());
insert(member);
Long memberId = member.getMemberId();
@ -148,6 +149,7 @@ public class PgMemberServiceImpl implements IPgMemberService {
member.setGender(dto.getGender());
member.setBirthday(dto.getBirthday());
member.setStatus(dto.getStatus());
member.setRegionId(dto.getRegionId());
update(member);
// 注意编辑时教育身份通过单独的接口管理这里不处理

View File

@ -0,0 +1,576 @@
# 会员区域字段需求技术方案
**版本**: v1.0
**编制人**: pangu
**编制日期**: 2026-02-05
**状态**: 待审核
---
## 一、需求概述
### 1.1 需求背景
当前会员管理系统中,会员数据缺少区域归属信息。在添加教育关系和亲子关系时,需要用户从头开始选择区域,用户体验不佳。
### 1.2 需求内容
| 序号 | 需求项 | 描述 |
|------|--------|------|
| 1 | 会员增加区域字段 | 在会员数据中记录所属区域 |
| 2 | H5注册时选择区域 | 用户注册时必须选择所在区域 |
| 3 | 弹窗区域默认值 | 添加教育关系/亲子关系时,区域默认选择会员所在区域,但允许修改 |
---
## 二、现状分析
### 2.1 数据库现状
```sql
-- pg_member 表已有 region_id 字段
CREATE TABLE `pg_member` (
...
`region_id` bigint DEFAULT NULL COMMENT '区域ID',
...
);
```
**结论**: 数据库表已具备 `region_id` 字段,无需修改表结构。
### 2.2 后端实体现状
```java
// PgMember.java 中缺少 regionId 字段
public class PgMember extends BaseEntity {
private Long memberId;
private String memberCode;
private String phone;
// ... 无 regionId 字段
}
```
**结论**: 实体类需要添加 `regionId` 字段。
### 2.3 H5注册现状
当前H5注册表单字段
- 手机号码 (phone)
- 图形验证码 (imageCaptcha)
- 短信验证码 (verificationCode)
- 密码 (password)
- 同意协议 (agreed)
**结论**: 注册流程无区域选择,需要新增。
### 2.4 弹窗组件现状
**管理后台前端**:
| 组件 | 区域选择 | 默认值 |
|------|---------|--------|
| EducationDialog (教育关系) | ✅ 有 | 无默认值 |
| StudentSelectDialog (亲子关系) | ❌ 无 | - |
**H5前端**:
| 组件 | 区域选择 | 默认值 |
|------|---------|--------|
| TeacherIdentityForm (教育身份) | ✅ 有 | 无默认值 |
| ParentChildrenForm (绑定学生) | ✅ 有 | 无默认值 |
**结论**:
- 管理后台EducationDialog 已有区域选择需添加默认值逻辑StudentSelectDialog 需添加区域筛选功能
- H5前端两个弹窗均已有区域选择只需添加默认区域逻辑
---
## 三、技术方案
### 3.1 总体架构
```
┌─────────────────────────────────────────────────────────────┐
│ H5前端 │
├─────────────────┬───────────────────────────────────────────┤
│ 注册页面 │ 添加区域选择器注册时提交regionId │
└────────┬────────┴───────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────┐
│ 后端API │
├─────────────────┬───────────────────────────────────────────┤
│ H5AuthController│ register接口接收regionId │
│ PgMember实体 │ 新增regionId字段 │
└────────┬────────┴───────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────┐
│ 管理后台前端 │
├─────────────────┬───────────────────────────────────────────┤
│ EducationDialog │ open时传入会员regionId作为默认值 │
│ StudentSelectDialog │ 新增区域筛选默认使用会员regionId │
│ MemberDialog │ 向子组件传递会员regionId │
└─────────────────┴───────────────────────────────────────────┘
```
### 3.2 详细设计
#### 3.2.1 后端修改
**文件**: `PgMember.java`
```java
// 新增字段
/**
* 区域ID
*/
private Long regionId;
```
**文件**: `H5AuthServiceImpl.java` - register方法
```java
// 修改注册逻辑接收并保存regionId
public R<LoginVo> register(RegisterDto dto) {
// ... 原有验证逻辑
PgMember member = new PgMember();
member.setPhone(dto.getPhone());
member.setPassword(encryptPassword(dto.getPassword()));
member.setRegionId(dto.getRegionId()); // 新增保存区域ID
member.setRegisterSource("2"); // H5注册
member.setRegisterTime(new Date());
// ... 保存会员
}
```
**文件**: `RegisterDto.java` (如不存在需创建)
```java
// 新增区域ID字段
private Long regionId;
```
#### 3.2.2 H5前端修改
##### 3.2.2.1 注册页面新增区域选择
**文件**: `user-front/src/views/register/index.vue`
```vue
<!-- 新增区域选择器使用el-cascader与现有弹窗风格一致 -->
<el-form-item label="所在区域" prop="regionPath" required>
<el-cascader
v-model="registerForm.regionPath"
:options="regionsData"
:props="{ value: 'regionId', label: 'regionName', children: 'children' }"
placeholder="请选择省市区"
clearable
style="width: 100%"
/>
</el-form-item>
```
```javascript
// 表单数据新增
registerForm = {
phone: '',
imageCaptcha: '',
verificationCode: '',
password: '',
agreed: false,
regionPath: [] // 新增:区域路径 [provinceId, cityId, districtId]
}
// 获取区域树(复用已有逻辑)
const regionsData = ref([])
const loadRegionsData = async () => {
const res = await request.get('/h5/region/tree')
regionsData.value = res.data || []
}
// 提交注册时传递regionId取数组最后一个元素
register({
// ... 原有字段
regionId: registerForm.regionPath[registerForm.regionPath.length - 1]
})
```
**文件**: `user-front/src/api/user.js`
```javascript
// 修改register函数添加regionId参数
export function register(data) {
return request({
url: '/h5/auth/register',
method: 'post',
data: {
phone: data.consumerName,
password: data.password,
smsCode: data.verifyCode,
captchaCode: data.captchaValue,
uuid: data.captchaUuid,
regionId: data.regionId // 新增
}
})
}
```
##### 3.2.2.2 弹窗默认区域
**文件**: `user-front/src/components/TeacherIdentityForm.vue`
```javascript
// 新增props接收默认区域
props: {
// ... 原有props
defaultRegionPath: { // 新增:默认区域路径
type: Array,
default: () => []
}
}
// 组件挂载或打开时设置默认值
watch(() => props.defaultRegionPath, (val) => {
if (val && val.length > 0 && formData.regionPath.length === 0) {
formData.regionPath = [...val]
// 触发学校加载
loadSchoolsByRegion(val[val.length - 1])
}
}, { immediate: true })
```
**文件**: `user-front/src/components/ParentChildrenForm.vue`
```javascript
// 新增props接收默认区域同上
props: {
// ... 原有props
defaultRegionPath: {
type: Array,
default: () => []
}
}
```
**文件**: `user-front/src/views/userCenter/index.vue`
```javascript
// 获取会员区域信息
const memberRegionPath = ref([])
// 加载会员信息时获取区域路径
const loadMemberInfo = async () => {
// ... 原有逻辑
if (memberInfo.regionId) {
memberRegionPath.value = await getRegionPath(memberInfo.regionId)
}
}
// 向弹窗组件传递默认区域
<teacher-identity-form
:default-region-path="memberRegionPath"
// ... 其他props
/>
<parent-children-form
:default-region-path="memberRegionPath"
// ... 其他props
/>
```
#### 3.2.3 管理后台前端修改
**文件**: `MemberDialog.vue`
```javascript
// 向子组件传递会员区域ID
const memberRegionId = ref(null)
// 编辑会员时获取区域ID
const open = async (row) => {
if (row) {
// ... 加载会员数据
memberRegionId.value = row.regionId
}
}
// 打开教育关系弹窗时传递默认区域
const handleAddEducation = () => {
educationDialogRef.value?.open(memberId.value, null, null, memberRegionId.value)
}
// 打开亲子关系弹窗时传递默认区域
const handleAddStudent = () => {
studentSelectDialogRef.value?.open({
memberId: memberId.value,
defaultRegionId: memberRegionId.value
})
}
```
**文件**: `EducationDialog.vue`
```javascript
// 修改open方法签名增加默认区域参数
const open = async (mId, row, index, defaultRegionId) => {
resetForm()
memberId.value = mId
isEdit.value = !!row
localIndex.value = index ?? null
visible.value = true
await Promise.all([loadRegionTree(), loadSubjectList()])
if (row) {
// 编辑模式:使用已有数据
// ... 原有逻辑
} else if (defaultRegionId) {
// 新增模式:使用会员默认区域
regionIds.value = await getRegionPath(defaultRegionId)
form.regionId = defaultRegionId
await loadSchoolList(defaultRegionId)
}
}
```
**文件**: `StudentSelectDialog.vue`
```vue
<!-- 新增区域筛选 -->
<el-form-item label="区域">
<el-cascader
v-model="queryParams.regionIds"
:options="regionTree"
:props="{ value: 'regionId', label: 'regionName', checkStrictly: true }"
placeholder="请选择区域"
clearable
style="width: 180px"
@change="handleRegionChange"
/>
</el-form-item>
```
```javascript
// 新增区域相关变量
const regionTree = ref([])
const defaultRegionId = ref(null)
// 修改查询参数
const queryParams = reactive({
pageNum: 1,
pageSize: 10,
studentName: '',
studentNo: '',
regionIds: [], // 新增
regionId: null // 新增
})
// 修改open方法
const open = async (options = {}) => {
memberId.value = options.memberId || null
defaultRegionId.value = options.defaultRegionId || null
excludeStudentIds.value = options.excludeIds || []
resetQuery()
// 加载区域树
await loadRegionTree()
// 设置默认区域
if (defaultRegionId.value) {
queryParams.regionIds = await getRegionPath(defaultRegionId.value)
queryParams.regionId = defaultRegionId.value
}
visible.value = true
getList()
}
// 区域变更处理
const handleRegionChange = (val) => {
queryParams.regionId = val && val.length ? val[val.length - 1] : null
handleQuery()
}
// 修改getList添加regionId参数
const getList = async () => {
const params = {
// ... 原有参数
regionId: queryParams.regionId || undefined // 新增
}
// ...
}
```
---
## 四、接口变更
### 4.1 H5注册接口
**路径**: `POST /h5/auth/register`
**请求体变更**:
| 字段 | 类型 | 必填 | 说明 |
|------|------|------|------|
| phone | String | 是 | 手机号 |
| password | String | 是 | 密码 |
| smsCode | String | 是 | 短信验证码 |
| captchaCode | String | 是 | 图形验证码 |
| uuid | String | 是 | 验证码UUID |
| **regionId** | **Long** | **是** | **区域ID新增** |
### 4.2 区域树接口H5端
**路径**: `GET /h5/region/tree`
**说明**: 如不存在需新增返回区域树结构供H5选择。
**响应体**:
```json
{
"code": 200,
"data": [
{
"regionId": 42,
"regionName": "湖北省",
"children": [
{
"regionId": 4201,
"regionName": "武汉市",
"children": [...]
}
]
}
]
}
```
### 4.3 学生列表接口
**路径**: `GET /business/student/available`
**请求参数变更**:
| 参数 | 类型 | 必填 | 说明 |
|------|------|------|------|
| pageNum | Integer | 否 | 页码 |
| pageSize | Integer | 否 | 每页数量 |
| studentName | String | 否 | 学生姓名 |
| studentNo | String | 否 | 学号 |
| **regionId** | **Long** | **否** | **区域ID新增** |
---
## 五、数据库变更
### 5.1 表结构
**无变更** - `pg_member` 表已有 `region_id` 字段。
### 5.2 数据迁移
**无需迁移** - 新注册用户将自动填充区域ID历史用户区域ID保持为NULL。
---
## 六、影响范围
### 6.1 后端文件清单
| 文件 | 修改类型 | 说明 |
|------|---------|------|
| PgMember.java | 修改 | 新增regionId字段 |
| H5AuthServiceImpl.java | 修改 | register方法接收regionId |
| RegisterDto.java | 新增/修改 | 新增regionId字段 |
| H5RegionController.java | 新增 | 提供区域树接口 |
| PgStudentServiceImpl.java | 修改 | available方法支持regionId筛选 |
### 6.2 H5前端文件清单
| 文件 | 修改类型 | 说明 |
|------|---------|------|
| views/register/index.vue | 修改 | 新增区域选择器 |
| api/user.js | 修改 | register函数添加regionId |
| components/TeacherIdentityForm.vue | 修改 | 新增defaultRegionPath prop |
| components/ParentChildrenForm.vue | 修改 | 新增defaultRegionPath prop |
| views/userCenter/index.vue | 修改 | 获取会员区域并传递给弹窗 |
### 6.3 管理后台前端文件清单
| 文件 | 修改类型 | 说明 |
|------|---------|------|
| MemberDialog.vue | 修改 | 传递memberRegionId给子组件 |
| EducationDialog.vue | 修改 | open方法增加defaultRegionId参数 |
| StudentSelectDialog.vue | 修改 | 新增区域筛选功能 |
---
## 七、测试要点
### 7.1 功能测试
| 测试项 | 测试步骤 | 预期结果 |
|--------|---------|---------|
| H5注册-区域必填 | 不选区域直接注册 | 提示"请选择所在区域" |
| H5注册-区域保存 | 选择区域后注册 | 会员数据包含正确的regionId |
| H5教育身份-默认区域 | 有区域的会员打开"添加教育身份"弹窗 | 区域字段自动选中会员所在区域 |
| H5教育身份-可修改 | 修改默认选中的区域 | 可正常切换到其他区域,学校列表刷新 |
| H5绑定学生-默认区域 | 有区域的会员打开"绑定学生"弹窗 | 区域字段自动选中会员所在区域 |
| H5绑定学生-可修改 | 修改默认选中的区域 | 可正常切换到其他区域,学校列表刷新 |
| 管理后台-教育关系默认区域 | 为有区域的会员添加任教信息 | 区域字段自动选中会员所在区域 |
| 管理后台-亲子关系区域筛选 | 为有区域的会员添加亲子关系 | 区域筛选默认选中会员所在区域 |
### 7.2 兼容性测试
| 测试项 | 测试步骤 | 预期结果 |
|--------|---------|---------|
| 历史会员-无区域(H5) | 无区域会员打开"添加教育身份"弹窗 | 区域字段为空,需手动选择 |
| 历史会员-无区域(管理后台) | 为无区域的会员添加任教信息 | 区域字段为空,需手动选择 |
| 历史会员-弹窗正常 | 打开历史会员的各类弹窗 | 弹窗正常显示,无报错 |
---
## 八、风险评估
| 风险项 | 风险等级 | 应对措施 |
|--------|---------|---------|
| H5注册流程变更 | 🟡 中 | 充分测试,确保注册流程顺畅 |
| 历史数据无区域 | 🟢 低 | 兼容处理,无区域时默认值为空 |
| 弹窗组件改动 | 🟢 低 | 保持向后兼容,参数可选 |
---
## 九、工作量估算
| 模块 | 工作项 | 估算 |
|------|--------|------|
| 后端 | PgMember实体修改 | 0.5h |
| 后端 | 注册接口修改 | 1h |
| 后端 | 区域树接口H5端如不存在 | 1h |
| 后端 | 学生列表接口修改 | 0.5h |
| H5前端 | 注册页面区域选择 | 1.5h |
| H5前端 | TeacherIdentityForm默认区域 | 1h |
| H5前端 | ParentChildrenForm默认区域 | 1h |
| H5前端 | userCenter传递区域 | 0.5h |
| 管理后台 | MemberDialog修改 | 0.5h |
| 管理后台 | EducationDialog修改 | 1h |
| 管理后台 | StudentSelectDialog修改 | 2h |
| 测试 | 功能测试 | 2h |
| **合计** | | **12.5h** |
---
## 十、审批记录
| 角色 | 姓名 | 意见 | 日期 |
|------|------|------|------|
| 技术负责人 | | | |
| 产品负责人 | | | |
---
*文档生成时间: 2026-02-05*

View File

@ -0,0 +1,112 @@
# 业务功能按钮权限统一修复方案
**问题**:角色管理中已取消「新增/修改/删除」等菜单权限后,学生管理、会员管理等自建业务页面上,对应按钮仍显示且可操作,与权限配置不一致。
**原因**:后端接口已使用 `@SaCheckPermission` 做权限校验(无权限会 403但前端列表/表格上的「新增」「编辑」「删除」「导入」等按钮**未做权限控制**,未使用项目已有的 `v-hasPermi` 指令,导致仅靠菜单可见性无法隐藏这些操作按钮。
**修复思路**:在**前端**为所有业务模块的增删改等操作按钮增加 `v-hasPermi="['权限字符']"`,与后端 `@SaCheckPermission` 使用的权限字符一致。不修改后端(后端已校验),不修改菜单/角色配置方式。
---
## 一、涉及范围
以下为**自建业务**页面,需统一加按钮权限控制(系统管理下用户/角色/部门等已有 `v-hasPermi`,不在此次范围):
| 模块 | 前端路径 | 后端权限前缀 |
|------------|----------|--------------|
| 学生管理 | `views/business/student/index.vue` | `business:student:*` |
| 会员管理 | `views/business/member/index.vue` | `business:member:*` |
| 学校管理 | `views/business/school/index.vue` | `business:school:*` |
| 应用管理 | `views/business/application/index.vue` | `business:application:*` |
| 接口字典 | `views/business/apiDict/index.vue` | `business:apiDict:*` |
| 基础数据-年级 | `views/business/base/grade/index.vue` | `business:grade:*` |
| 基础数据-班级 | `views/business/base/class/index.vue` | `business:class:*` |
| 基础数据-学科 | `views/business/base/subject/index.vue` | `business:subject:*` |
| 基础数据-区域 | `views/business/base/region/index.vue` | `business:region:*` |
---
## 二、各页面具体修改
### 1. 学生管理 `student/index.vue`
- 工具栏「新增」:`v-hasPermi="['business:student:add']"`
- 工具栏「导入」:`v-hasPermi="['business:student:import']"`
- 表格操作「编辑」:`v-hasPermi="['business:student:edit']"`
- 表格操作「删除」:`v-hasPermi="['business:student:remove']"`
### 2. 会员管理 `member/index.vue`
- 工具栏「新增」:`v-hasPermi="['business:member:add']"`
- 表格操作「编辑」:`v-hasPermi="['business:member:edit']"`
- 表格操作「重置密码」:`v-hasPermi="['business:member:resetPwd']"`
- 表格操作「删除」:`v-hasPermi="['business:member:remove']"`
### 3. 学校管理 `school/index.vue`
- 工具栏「新增」:`v-hasPermi="['business:school:add']"`
- 表格内「编辑」:`v-hasPermi="['business:school:edit']"`
- 表格内「新增年级」:`v-hasPermi="['business:school:edit']"`
- 表格内「新增班级」:`v-hasPermi="['business:school:edit']"`
- 表格内「删除」(学校/年级/班级):`v-hasPermi="['business:school:remove']"`
### 4. 应用管理 `application/index.vue`
- 工具栏「新增」:`v-hasPermi="['business:application:add']"`
- 表格操作「编辑」:`v-hasPermi="['business:application:edit']"`
- 表格操作「重置密钥」:`v-hasPermi="['business:application:edit']"`
- 表格操作「删除」:`v-hasPermi="['business:application:remove']"`
### 5. 接口字典 `apiDict/index.vue`
- 工具栏「新增」:`v-hasPermi="['business:apiDict:add']"`
- 工具栏「批量删除」:`v-hasPermi="['business:apiDict:remove']"`
- 表格操作「编辑」:`v-hasPermi="['business:apiDict:edit']"`
- 表格操作「删除」:`v-hasPermi="['business:apiDict:remove']"`
### 6. 基础数据 - 年级 `base/grade/index.vue`
- 工具栏「新增」:`v-hasPermi="['business:grade:add']"`
- 表格操作「编辑」:`v-hasPermi="['business:grade:edit']"`
- 表格操作「删除」:`v-hasPermi="['business:grade:remove']"`
### 7. 基础数据 - 班级 `base/class/index.vue`
- 工具栏「新增」:`v-hasPermi="['business:class:add']"`
- 表格操作「编辑」:`v-hasPermi="['business:class:edit']"`
- 表格操作「删除」:`v-hasPermi="['business:class:remove']"`
### 8. 基础数据 - 学科 `base/subject/index.vue`
- 工具栏「新增」:`v-hasPermi="['business:subject:add']"`
- 表格操作「编辑」:`v-hasPermi="['business:subject:edit']"`
- 表格操作「删除」:`v-hasPermi="['business:subject:remove']"`
### 9. 基础数据 - 区域 `base/region/index.vue`
- 工具栏「新增」:`v-hasPermi="['business:region:add']"`
- 表格操作「新增下级」:`v-hasPermi="['business:region:add']"`
- 表格操作「编辑」:`v-hasPermi="['business:region:edit']"`
- 表格操作「删除」:`v-hasPermi="['business:region:remove']"`
---
## 三、技术说明
- **指令**:使用现有 `v-hasPermi``directive/permission/hasPermi.js`),无权限时移除 DOM与系统管理页面用法一致。
- **权限字符**:与后端 `@SaCheckPermission` 完全一致,保证「角色未勾选该权限 → 前端不显示按钮、后端接口 403」。
- **后端**:不改动,已具备接口级校验。
- **菜单/角色**:不改动,仅依赖现有「菜单权限」配置(角色管理里勾选的子权限如「学生新增」「学生修改」等)。
---
## 四、验收方式
1. 使用**分公司用户**(如 wuhan登录在角色管理中仅保留「学生管理」下的「学生查询」去掉「学生新增」「学生修改」「学生删除」「学生导入」。
2. 进入学生管理页:不应出现「新增」「导入」按钮,表格操作列不应出现「编辑」「删除」。
3. 对会员、学校、应用、接口字典、年级/班级/学科/区域做同类配置,验证对应按钮按权限显隐。
4. 超级管理员或拥有全部权限的角色,所有按钮仍正常显示。
---
**请确认是否按此方案执行修改,同意后再进行代码改动。**

View File

@ -0,0 +1,290 @@
# 增量发布计划
**发布日期**: 2026-02-05
**目标服务器**: 192.168.71.56
**发布类型**: 增量发布
**编制人**: pangu
---
## 一、发布概览
| 项目 | 本地最新版本 | 服务器当前版本 | 待发布提交数 |
|------|-------------|---------------|-------------|
| pangu-user-platform (后端+管理后台) | add00c9 | 2026-02-03 22:29 部署 | 5 个 |
| user_authentication_center_front (H5前端) | 70fc1ad | 2026-02-03 22:04 部署 | 2 个 |
---
## 二、pangu-user-platform 发布内容
### 2.1 待发布提交清单
| 序号 | Commit ID | 提交说明 | 类型 |
|------|----------|---------|------|
| 1 | add00c9 | 新增学校自动添加年级 + 修复区域层级查询 + 清理区域数据 | feat |
| 2 | 72cb666 | 年级管理增加学段字段(小学/初中/高中/中专/大学) | feat |
| 3 | 80dd406 | 新增OpenApi基础数据接口 + 学生完整数据接口 + UI文案优化 | feat |
| 4 | 1a0b75e | 同步需求与技术方案文档 | docs |
| 5 | 6027a8c | 修改后端欢迎语为盘古后台管理系统 | refactor |
### 2.2 功能变更摘要
1. **学校管理优化**
- 新增学校时自动添加对应学段的年级
- 修复选择省/市时无法显示学校的Bug支持区域层级查询
- 区域树默认只展开湖北省,平行显示市级
2. **年级管理增强**
- 新增学段字段(小学/初中/高中/中专/大学)
- 支持按学段筛选年级
3. **OpenAPI接口扩展**
- 新增学校/年级/班级基础数据查询接口
- 新增学生完整数据接口(不脱敏,需特殊授权)
4. **UI文案优化**
- "教育身份" 改为 "任教信息"
- 后端欢迎语改为 "盘古后台管理系统"
### 2.3 数据库增量脚本
| 序号 | 脚本文件 | 说明 | 影响范围 | 风险等级 |
|------|---------|------|---------|---------|
| 1 | V1.0.3__open_api_dict.sql | OpenAPI接口字典数据 | pg_api_dict表 | 🟢 低 |
| 2 | V1.0.4__grade_add_stage.sql | 年级表增加学段字段 | pg_grade表结构 | 🟡 中 |
| 3 | V1.0.5__clean_region_data.sql | 清理非湖北省区域数据 | pg_region表 | 🔴 高 |
#### 脚本详细说明
**V1.0.3__open_api_dict.sql**
- 新增11条API字典记录
- 使用 `ON DUPLICATE KEY UPDATE` 幂等设计
- 可重复执行,无副作用
**V1.0.4__grade_add_stage.sql**
- 为 `pg_grade` 表新增 `stage` 字段
- 自动根据年级名称初始化学段数据
- ⚠️ 注意:执行前需确认 `pg_grade` 表无 `stage` 字段
**V1.0.5__clean_region_data.sql**
- ⚠️ **高风险脚本 - 物理删除操作**
- 删除所有非湖北省的区域数据约3309条
- 仅保留湖北省及其下级区域约120条
- ⚠️ **执行前必须备份 `pg_region` 表**
### 2.4 服务器部署路径
| 组件 | 路径 |
|------|------|
| 后端 JAR | /opt/pangu-user-platform/backend/pangu-admin.jar |
| 管理后台前端 | /opt/pangu-user-platform/frontend/admin-manage/ |
| 启动脚本 | /opt/pangu-user-platform/scripts/service.sh |
| 配置文件 | /opt/pangu-user-platform/backend/application-test.yml |
| 日志目录 | /opt/pangu-user-platform/backend/logs/ |
---
## 三、user_authentication_center_front 发布内容
### 3.1 待发布提交清单
| 序号 | Commit ID | 提交说明 | 类型 |
|------|----------|---------|------|
| 1 | 70fc1ad | 优化登录注册页面交互和弹窗内容 | feat |
| 2 | 842d64f | 允许通过IP地址访问H5前端 | fix |
### 3.2 功能变更摘要
1. **登录注册优化**
- 优化登录注册页面交互体验
- 改进弹窗内容展示
2. **访问限制修复**
- 允许通过IP地址直接访问H5前端
### 3.3 服务器部署路径
| 组件 | 路径 |
|------|------|
| H5前端 | /opt/pangu-user-platform/frontend/user-front/ |
---
## 四、发布步骤
### 4.1 发布前准备
```bash
# 1. 备份数据库
ssh root@192.168.71.56
mysqldump -h 8.148.25.55 -uroot -p pguser-db pg_region > /opt/backup/pg_region_$(date +%Y%m%d_%H%M%S).sql
mysqldump -h 8.148.25.55 -uroot -p pguser-db pg_grade > /opt/backup/pg_grade_$(date +%Y%m%d_%H%M%S).sql
mysqldump -h 8.148.25.55 -uroot -p pguser-db pg_api_dict > /opt/backup/pg_api_dict_$(date +%Y%m%d_%H%M%S).sql
# 2. 备份当前部署版本
cp -r /opt/pangu-user-platform/backend/pangu-admin.jar /opt/backup/pangu-admin.jar.bak
cp -r /opt/pangu-user-platform/frontend/admin-manage /opt/backup/admin-manage.bak
cp -r /opt/pangu-user-platform/frontend/user-front /opt/backup/user-front.bak
```
### 4.2 本地构建
```bash
# 1. 构建后端
cd /Users/felix/pgWorkSpace/pangu-user-platform/backend
./build.sh -q -f
# 2. 构建管理后台前端
cd /Users/felix/pgWorkSpace/pangu-user-platform/frontend
npm run build
# 3. 构建H5前端
cd /Users/felix/pgWorkSpace/user_authentication_center_front/user-front
npm run build
```
### 4.3 上传部署包
```bash
# 1. 上传后端JAR
scp /Users/felix/pgWorkSpace/pangu-user-platform/backend/pangu-admin/target/pangu-admin.jar root@192.168.71.56:/opt/pangu-user-platform/backend/
# 2. 上传管理后台前端
scp -r /Users/felix/pgWorkSpace/pangu-user-platform/frontend/dist/* root@192.168.71.56:/opt/pangu-user-platform/frontend/admin-manage/
# 3. 上传H5前端
scp -r /Users/felix/pgWorkSpace/user_authentication_center_front/user-front/dist/* root@192.168.71.56:/opt/pangu-user-platform/frontend/user-front/
# 4. 上传SQL脚本
scp /Users/felix/pgWorkSpace/pangu-user-platform/scripts/sql/V1.0.*.sql root@192.168.71.56:/opt/pangu-user-platform/scripts/
```
### 4.4 执行数据库脚本
```bash
ssh root@192.168.71.56
# 按顺序执行SQL脚本
mysql -h 8.148.25.55 -uroot -paly2024A pguser-db < /opt/pangu-user-platform/scripts/V1.0.3__open_api_dict.sql
mysql -h 8.148.25.55 -uroot -paly2024A pguser-db < /opt/pangu-user-platform/scripts/V1.0.4__grade_add_stage.sql
# ⚠️ V1.0.5 需要单独确认后执行(高风险)
# mysql -h 8.148.25.55 -uroot -paly2024A pguser-db < /opt/pangu-user-platform/scripts/V1.0.5__clean_region_data.sql
```
### 4.5 重启服务
```bash
ssh root@192.168.71.56
# 重启后端服务
cd /opt/pangu-user-platform/scripts
./service.sh restart
# 检查服务状态
./service.sh status
# 查看启动日志
./service.sh logs
```
### 4.6 验证测试
| 验证项 | 验证方法 | 预期结果 |
|--------|---------|---------|
| 后端服务 | `curl http://localhost:9083/actuator/health` | {"status":"UP"} |
| 管理后台登录 | 浏览器访问管理后台 | 正常登录 |
| 学校管理-区域树 | 查看区域树展开状态 | 默认展开湖北省 |
| 学校管理-层级查询 | 选择武汉市查看学校 | 显示武汉市下所有学校 |
| 年级管理-学段 | 查看年级列表 | 显示学段列 |
| OpenAPI接口 | 调用新增的接口 | 正常返回数据 |
| H5前端登录 | 浏览器访问H5 | 正常登录 |
---
## 五、回滚方案
### 5.1 后端回滚
```bash
ssh root@192.168.71.56
cd /opt/pangu-user-platform/scripts
# 停止服务
./service.sh stop
# 恢复JAR包
cp /opt/backup/pangu-admin.jar.bak /opt/pangu-user-platform/backend/pangu-admin.jar
# 启动服务
./service.sh start
```
### 5.2 前端回滚
```bash
# 恢复管理后台前端
rm -rf /opt/pangu-user-platform/frontend/admin-manage/*
cp -r /opt/backup/admin-manage.bak/* /opt/pangu-user-platform/frontend/admin-manage/
# 恢复H5前端
rm -rf /opt/pangu-user-platform/frontend/user-front/*
cp -r /opt/backup/user-front.bak/* /opt/pangu-user-platform/frontend/user-front/
```
### 5.3 数据库回滚
```bash
# 恢复pg_grade表如果执行了V1.0.4
mysql -h 8.148.25.55 -uroot -paly2024A pguser-db -e "ALTER TABLE pg_grade DROP COLUMN stage;"
# 恢复pg_region表如果执行了V1.0.5
mysql -h 8.148.25.55 -uroot -paly2024A pguser-db < /opt/backup/pg_region_YYYYMMDD_HHMMSS.sql
```
---
## 六、风险评估
| 风险项 | 风险等级 | 应对措施 |
|--------|---------|---------|
| 数据库结构变更 | 🟡 中 | 执行前备份相关表 |
| 区域数据物理删除 | 🔴 高 | 必须备份,可单独安排执行 |
| 后端服务中断 | 🟢 低 | 使用热部署,中断时间<30秒 |
| 前端静态资源更新 | 🟢 低 | nginx无需重启刷新即生效 |
---
## 七、审批确认
| 角色 | 姓名 | 确认签字 | 日期 |
|------|------|---------|------|
| 开发负责人 | | | |
| 测试负责人 | | | |
| 运维负责人 | | | |
| 项目经理 | | | |
---
## 八、特别说明
### 8.1 关于 V1.0.5__clean_region_data.sql
此脚本将**物理删除**所有非湖北省的区域数据约3309条仅保留湖北省及其下级区域约120条
**建议处理方式**
1. 此脚本可在本次发布中暂不执行
2. 单独安排时间,在业务低峰期执行
3. 执行前必须完成 `pg_region` 表的完整备份
4. 执行后需验证学校管理功能正常
### 8.2 注意事项
1. 发布前确保本地代码已全部提交并推送
2. 建议选择业务低峰期(如晚间或周末)发布
3. 发布后持续关注服务日志,确保无异常
---
*文档生成时间: 2026-02-05*

View File

@ -63,11 +63,19 @@
<script setup>
import request from '@/utils/request'
import { ElMessage } from 'element-plus'
import { reactive, ref } from 'vue'
import { reactive, ref, watch } from 'vue'
import useBaseDataStore from '@/store/modules/baseData'
const baseDataStore = useBaseDataStore()
const props = defineProps({
//
defaultRegionPath: {
type: Array,
default: () => []
}
})
const emit = defineEmits(['success', 'add', 'update'])
const visible = ref(false)
@ -148,6 +156,20 @@ const open = async (mId, row, index) => {
if (row.schoolGradeId) {
await loadClassList(row.schoolGradeId)
}
} else {
// 使
applyDefaultRegion()
}
}
/**
* 应用默认区域路径
*/
const applyDefaultRegion = async () => {
if (props.defaultRegionPath && props.defaultRegionPath.length > 0 && regionIds.value.length === 0) {
regionIds.value = [...props.defaultRegionPath]
form.regionId = props.defaultRegionPath[props.defaultRegionPath.length - 1]
await loadSchoolList(form.regionId)
}
}

View File

@ -64,6 +64,19 @@
/>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="所在区域" prop="regionId">
<el-cascader
v-model="regionIds"
:options="regionTree"
:props="{ value: 'regionId', label: 'regionName', checkStrictly: true }"
placeholder="请选择所在区域"
clearable
style="width: 100%"
@change="handleMemberRegionChange"
/>
</el-form-item>
</el-col>
</el-row>
<!-- 任教信息 -->
@ -131,7 +144,7 @@
<StudentSelectDialog ref="studentSelectRef" @success="loadBoundStudents" @add="handleLocalAddStudents" />
<!-- 任教信息编辑弹窗 -->
<EducationDialog ref="educationDialogRef" @success="loadEducations" @add="handleLocalAddEducation" @update="handleLocalUpdateEducation" />
<EducationDialog ref="educationDialogRef" :default-region-path="memberRegionPath" @success="loadEducations" @add="handleLocalAddEducation" @update="handleLocalUpdateEducation" />
</el-dialog>
</template>
@ -142,6 +155,9 @@ import { ElMessage } from 'element-plus'
import { reactive, ref } from 'vue'
import StudentSelectDialog from './StudentSelectDialog.vue'
import EducationDialog from './EducationDialog.vue'
import useBaseDataStore from '@/store/modules/baseData'
const baseDataStore = useBaseDataStore()
const emit = defineEmits(['success'])
@ -154,6 +170,8 @@ const educationDialogRef = ref(null)
//
const educations = ref([])
//
const memberRegionPath = ref([])
//
const form = reactive({
@ -163,9 +181,14 @@ const form = reactive({
gender: '0',
birthday: '',
status: '0',
regionId: null,
students: []
})
//
const regionIds = ref([])
const regionTree = ref([])
//
const rules = {
phone: [
@ -182,12 +205,20 @@ const open = async (row) => {
isEdit.value = !!row
visible.value = true
//
await loadRegionTree()
//
if (row && row.memberId) {
try {
const res = await request.get(`/business/member/${row.memberId}`)
if (res.code === 200 && res.data) {
Object.assign(form, res.data)
// regionIds
await computeMemberRegionPath(res.data.regionId)
if (res.data.regionId) {
regionIds.value = [...memberRegionPath.value]
}
//
await Promise.all([loadEducations(), loadBoundStudents()])
}
@ -197,6 +228,58 @@ const open = async (row) => {
}
}
/**
* 加载区域树
*/
const loadRegionTree = async () => {
try {
regionTree.value = await baseDataStore.fetchRegionTree()
} catch (e) {
regionTree.value = []
}
}
/**
* 会员区域变更
*/
const handleMemberRegionChange = (val) => {
form.regionId = val && val.length ? val[val.length - 1] : null
// memberRegionPath 使
memberRegionPath.value = val || []
}
/**
* 计算会员区域路径
*/
const computeMemberRegionPath = async (regionId) => {
if (!regionId) {
memberRegionPath.value = []
return
}
try {
//
const regionTree = await baseDataStore.fetchRegionTree()
//
const findPath = (nodes, targetId, path = []) => {
for (const node of nodes) {
const currentPath = [...path, node.regionId]
if (node.regionId === targetId) {
return currentPath
}
if (node.children && node.children.length > 0) {
const found = findPath(node.children, targetId, currentPath)
if (found) return found
}
}
return null
}
const path = findPath(regionTree, regionId)
memberRegionPath.value = path || []
} catch (e) {
memberRegionPath.value = []
}
}
/**
* 加载任教信息列表
*/
@ -239,8 +322,11 @@ const resetForm = () => {
form.gender = '0'
form.birthday = ''
form.status = '0'
form.regionId = null
form.students = []
educations.value = []
memberRegionPath.value = []
regionIds.value = []
}
/**
@ -408,7 +494,8 @@ const handleSubmit = async () => {
nickname: form.nickname,
gender: form.gender,
birthday: form.birthday,
status: form.status
status: form.status,
regionId: form.regionId
}
if (isEdit.value) {

View File

@ -56,6 +56,11 @@
</template>
</el-table-column>
<el-table-column prop="nickname" label="昵称" min-width="100" show-overflow-tooltip />
<el-table-column prop="regionId" label="所在区域" min-width="140" show-overflow-tooltip>
<template #default="{ row }">
{{ getRegionNameById(row.regionId) }}
</template>
</el-table-column>
<el-table-column prop="gender" label="性别" width="60" align="center">
<template #default="{ row }">
{{ row.gender === '1' ? '男' : row.gender === '2' ? '女' : '未知' }}
@ -131,9 +136,11 @@ import { Delete, Edit, Hide, Key, Plus, Refresh, Search, View } from '@element-p
import { ElMessage, ElMessageBox } from 'element-plus'
import { onMounted, ref } from 'vue'
import request from '@/utils/request'
import useBaseDataStore from '@/store/modules/baseData'
import MemberDialog from './components/MemberDialog.vue'
import ResetPwdDialog from './components/ResetPwdDialog.vue'
const baseDataStore = useBaseDataStore()
const loading = ref(false)
const tableData = ref([])
const total = ref(0)
@ -186,6 +193,24 @@ const formatBirthday = (birthday) => {
return `${year}-${month}-${day}`
}
// regionId " / / "
const getRegionNameById = (regionId) => {
if (regionId == null || regionId === '') return ''
const tree = baseDataStore.regionTree || []
const findPath = (nodes, targetId, pathNames = []) => {
for (const node of nodes) {
const names = [...pathNames, node.regionName]
if (node.regionId === targetId) return names.join(' / ')
if (node.children?.length) {
const found = findPath(node.children, targetId, names)
if (found) return found
}
}
return ''
}
return findPath(tree, regionId)
}
//
const getList = async () => {
loading.value = true
@ -284,7 +309,8 @@ const handleStatusChange = async (row) => {
}
}
onMounted(() => {
onMounted(async () => {
await baseDataStore.fetchRegionTree()
getList()
})
</script>