fix: 租户管理表单校验与后端字段对齐
- 前端: 学习地址必填、有效期绑定 formData.dateRange 修复校验 - 前端: admin.ts 请求体字段映射 loginAccount->code, contactPerson->contactName - 后端: TenantCreateRequest 新增 password 字段 - 后端: TenantServiceImpl 创建租户时设置 username(code) 和 password Made-with: Cursor
This commit is contained in:
parent
279fa79b56
commit
db70b1ad47
@ -229,11 +229,24 @@ export const getTenants = (params: TenantQueryParams) =>
|
||||
export const getTenant = (id: number) =>
|
||||
http.get<TenantDetail>(`/v1/admin/tenants/${id}`);
|
||||
|
||||
export const createTenant = (data: CreateTenantDto) =>
|
||||
http.post<Tenant & { tempPassword: string }>('/v1/admin/tenants', data);
|
||||
export const createTenant = (data: CreateTenantDto) => {
|
||||
// 映射前端字段到后端 API:loginAccount -> code, contactPerson -> contactName
|
||||
const payload = {
|
||||
...data,
|
||||
code: data.loginAccount,
|
||||
contactName: data.contactPerson,
|
||||
};
|
||||
delete (payload as Record<string, unknown>).loginAccount;
|
||||
delete (payload as Record<string, unknown>).contactPerson;
|
||||
return http.post<Tenant & { tempPassword: string }>('/v1/admin/tenants', payload);
|
||||
};
|
||||
|
||||
export const updateTenant = (id: number, data: UpdateTenantDto) =>
|
||||
http.put<Tenant>(`/v1/admin/tenants/${id}`, data);
|
||||
export const updateTenant = (id: number, data: UpdateTenantDto) => {
|
||||
// 映射前端字段到后端 API:contactPerson -> contactName
|
||||
const payload = { ...data, contactName: data.contactPerson };
|
||||
delete (payload as Record<string, unknown>).contactPerson;
|
||||
return http.put<Tenant>(`/v1/admin/tenants/${id}`, payload);
|
||||
};
|
||||
|
||||
export const updateTenantQuota = (id: number, data: UpdateTenantQuotaDto) =>
|
||||
http.put<Tenant>(`/v1/admin/tenants/${id}/quota`, data);
|
||||
|
||||
@ -5,33 +5,18 @@
|
||||
<!-- 搜索表单 -->
|
||||
<a-form layout="inline" :model="searchForm" style="margin-bottom: 16px">
|
||||
<a-form-item label="关键词">
|
||||
<a-input
|
||||
v-model:value="searchForm.keyword"
|
||||
placeholder="学校名称/账号/联系人"
|
||||
allow-clear
|
||||
style="width: 200px"
|
||||
@pressEnter="handleSearch"
|
||||
/>
|
||||
<a-input v-model:value="searchForm.keyword" placeholder="学校名称/账号/联系人" allow-clear style="width: 200px"
|
||||
@pressEnter="handleSearch" />
|
||||
</a-form-item>
|
||||
<a-form-item label="状态">
|
||||
<a-select
|
||||
v-model:value="searchForm.status"
|
||||
placeholder="全部状态"
|
||||
allow-clear
|
||||
style="width: 120px"
|
||||
>
|
||||
<a-select v-model:value="searchForm.status" placeholder="全部状态" allow-clear style="width: 120px">
|
||||
<a-select-option value="ACTIVE">生效中</a-select-option>
|
||||
<a-select-option value="EXPIRED">已过期</a-select-option>
|
||||
<a-select-option value="SUSPENDED">已暂停</a-select-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
<a-form-item label="套餐">
|
||||
<a-select
|
||||
v-model:value="searchForm.packageType"
|
||||
placeholder="全部套餐"
|
||||
allow-clear
|
||||
style="width: 120px"
|
||||
>
|
||||
<a-select v-model:value="searchForm.packageType" placeholder="全部套餐" allow-clear style="width: 120px">
|
||||
<a-select-option value="BASIC">基础版</a-select-option>
|
||||
<a-select-option value="STANDARD">标准版</a-select-option>
|
||||
<a-select-option value="ADVANCED">高级版</a-select-option>
|
||||
@ -41,7 +26,9 @@
|
||||
<a-form-item>
|
||||
<a-space>
|
||||
<a-button type="primary" @click="handleSearch">
|
||||
<template #icon><SearchOutlined /></template>
|
||||
<template #icon>
|
||||
<SearchOutlined />
|
||||
</template>
|
||||
搜索
|
||||
</a-button>
|
||||
<a-button @click="handleReset">重置</a-button>
|
||||
@ -49,20 +36,17 @@
|
||||
</a-form-item>
|
||||
<a-form-item style="float: right">
|
||||
<a-button type="primary" @click="showAddModal">
|
||||
<template #icon><PlusOutlined /></template>
|
||||
<template #icon>
|
||||
<PlusOutlined />
|
||||
</template>
|
||||
添加租户
|
||||
</a-button>
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
|
||||
<!-- 数据表格 -->
|
||||
<a-table
|
||||
:columns="columns"
|
||||
:data-source="tenants"
|
||||
:loading="loading"
|
||||
:pagination="pagination"
|
||||
@change="handleTableChange"
|
||||
>
|
||||
<a-table :columns="columns" :data-source="tenants" :loading="loading" :pagination="pagination"
|
||||
@change="handleTableChange">
|
||||
<template #bodyCell="{ column, record }: any">
|
||||
<template v-if="column.key === 'name'">
|
||||
<a-button type="link" @click="handleViewDetail(record)">
|
||||
@ -87,16 +71,14 @@
|
||||
</div>
|
||||
</template>
|
||||
<template v-else-if="column.key === 'status'">
|
||||
<a-badge
|
||||
:status="getStatusType(record.status)"
|
||||
:text="getStatusText(record.status)"
|
||||
/>
|
||||
<a-badge :status="getStatusType(record.status)" :text="getStatusText(record.status)" />
|
||||
</template>
|
||||
<template v-else-if="column.key === 'action'">
|
||||
<a-space>
|
||||
<a-dropdown>
|
||||
<a-button type="link" size="small">
|
||||
操作 <DownOutlined />
|
||||
操作
|
||||
<DownOutlined />
|
||||
</a-button>
|
||||
<template #overlay>
|
||||
<a-menu>
|
||||
@ -106,16 +88,10 @@
|
||||
<a-menu-item @click="handleQuota(record)">调整配额</a-menu-item>
|
||||
<a-menu-item @click="handleResetPassword(record)">重置密码</a-menu-item>
|
||||
<a-menu-divider />
|
||||
<a-menu-item
|
||||
v-if="record.status === 'ACTIVE'"
|
||||
@click="handleUpdateStatus(record, 'SUSPENDED')"
|
||||
>
|
||||
<a-menu-item v-if="record.status === 'ACTIVE'" @click="handleUpdateStatus(record, 'SUSPENDED')">
|
||||
暂停服务
|
||||
</a-menu-item>
|
||||
<a-menu-item
|
||||
v-if="record.status === 'SUSPENDED'"
|
||||
@click="handleUpdateStatus(record, 'ACTIVE')"
|
||||
>
|
||||
<a-menu-item v-if="record.status === 'SUSPENDED'" @click="handleUpdateStatus(record, 'ACTIVE')">
|
||||
恢复服务
|
||||
</a-menu-item>
|
||||
<a-menu-item danger @click="handleDelete(record)">删除</a-menu-item>
|
||||
@ -129,36 +105,17 @@
|
||||
</a-card>
|
||||
|
||||
<!-- 添加/编辑租户弹窗 -->
|
||||
<a-modal
|
||||
v-model:open="modalVisible"
|
||||
:title="isEdit ? '编辑租户' : '添加租户'"
|
||||
:confirm-loading="modalLoading"
|
||||
width="600px"
|
||||
@ok="handleModalOk"
|
||||
@cancel="handleModalCancel"
|
||||
>
|
||||
<a-form
|
||||
ref="formRef"
|
||||
:model="formData"
|
||||
:rules="formRules"
|
||||
:label-col="{ span: 6 }"
|
||||
:wrapper-col="{ span: 16 }"
|
||||
>
|
||||
<a-modal v-model:open="modalVisible" :title="isEdit ? '编辑租户' : '添加租户'" :confirm-loading="modalLoading" width="600px"
|
||||
@ok="handleModalOk" @cancel="handleModalCancel">
|
||||
<a-form ref="formRef" :model="formData" :rules="formRules" :label-col="{ span: 6 }" :wrapper-col="{ span: 16 }">
|
||||
<a-form-item label="学校名称" name="name">
|
||||
<a-input v-model:value="formData.name" placeholder="请输入学校名称" />
|
||||
</a-form-item>
|
||||
<a-form-item v-if="!isEdit" label="登录账号" name="loginAccount">
|
||||
<a-input
|
||||
v-model:value="formData.loginAccount"
|
||||
placeholder="字母开头,4-20位"
|
||||
:disabled="isEdit"
|
||||
/>
|
||||
<a-input v-model:value="formData.loginAccount" placeholder="字母开头,4-20位" :disabled="isEdit" />
|
||||
</a-form-item>
|
||||
<a-form-item v-if="!isEdit" label="初始密码" name="password">
|
||||
<a-input-password
|
||||
v-model:value="formData.password"
|
||||
placeholder="留空则默认为 123456"
|
||||
/>
|
||||
<a-input-password v-model:value="formData.password" placeholder="留空则默认为 123456" />
|
||||
</a-form-item>
|
||||
<a-form-item label="联系人" name="contactPerson">
|
||||
<a-input v-model:value="formData.contactPerson" placeholder="请输入联系人姓名" />
|
||||
@ -166,54 +123,32 @@
|
||||
<a-form-item label="联系电话" name="contactPhone">
|
||||
<a-input v-model:value="formData.contactPhone" placeholder="请输入联系电话" />
|
||||
</a-form-item>
|
||||
<a-form-item label="学校地址" name="address">
|
||||
<a-input v-model:value="formData.address" placeholder="请输入学校地址" />
|
||||
<a-form-item label="学习地址" name="address">
|
||||
<a-input v-model:value="formData.address" placeholder="请输入学习地址" />
|
||||
</a-form-item>
|
||||
<a-form-item label="套餐类型" name="packageType">
|
||||
<a-select v-model:value="formData.packageType" @change="handlePackageTypeChange">
|
||||
<a-select-option value="">请选择套餐</a-select-option>
|
||||
<a-select-option
|
||||
v-for="pkg in packageList"
|
||||
:key="pkg.id"
|
||||
:value="pkg.name"
|
||||
>
|
||||
<a-select-option v-for="pkg in packageList" :key="pkg.id" :value="pkg.name">
|
||||
{{ pkg.name }} ({{ formatPackagePrice(pkg.discountPrice || pkg.price) }}元)
|
||||
</a-select-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
<a-form-item label="教师配额" name="teacherQuota">
|
||||
<a-input-number
|
||||
v-model:value="formData.teacherQuota"
|
||||
:min="1"
|
||||
:max="1000"
|
||||
style="width: 100%"
|
||||
/>
|
||||
<a-input-number v-model:value="formData.teacherQuota" :min="1" :max="1000" style="width: 100%" />
|
||||
</a-form-item>
|
||||
<a-form-item label="学生配额" name="studentQuota">
|
||||
<a-input-number
|
||||
v-model:value="formData.studentQuota"
|
||||
:min="1"
|
||||
:max="10000"
|
||||
style="width: 100%"
|
||||
/>
|
||||
<a-input-number v-model:value="formData.studentQuota" :min="1" :max="10000" style="width: 100%" />
|
||||
</a-form-item>
|
||||
<a-form-item label="有效期" name="dateRange">
|
||||
<a-range-picker
|
||||
v-model:value="dateRange"
|
||||
style="width: 100%"
|
||||
value-format="YYYY-MM-DD"
|
||||
/>
|
||||
<a-range-picker v-model:value="formData.dateRange" style="width: 100%" value-format="YYYY-MM-DD"
|
||||
:disabled-date="disabledPastDate" />
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
</a-modal>
|
||||
|
||||
<!-- 配额调整弹窗 -->
|
||||
<a-modal
|
||||
v-model:open="quotaModalVisible"
|
||||
title="调整配额"
|
||||
:confirm-loading="quotaModalLoading"
|
||||
@ok="handleQuotaOk"
|
||||
>
|
||||
<a-modal v-model:open="quotaModalVisible" title="调整配额" :confirm-loading="quotaModalLoading" @ok="handleQuotaOk">
|
||||
<a-form :label-col="{ span: 6 }" :wrapper-col="{ span: 16 }">
|
||||
<a-form-item label="当前租户">
|
||||
<span>{{ currentTenant?.name }}</span>
|
||||
@ -227,23 +162,15 @@
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
<a-form-item label="教师配额">
|
||||
<a-input-number
|
||||
v-model:value="quotaForm.teacherQuota"
|
||||
:min="currentTenant?.teacherCount || 1"
|
||||
:max="1000"
|
||||
style="width: 100%"
|
||||
/>
|
||||
<a-input-number v-model:value="quotaForm.teacherQuota" :min="currentTenant?.teacherCount || 1" :max="1000"
|
||||
style="width: 100%" />
|
||||
<div style="color: #999; font-size: 12px">
|
||||
已使用: {{ currentTenant?.teacherCount || 0 }}
|
||||
</div>
|
||||
</a-form-item>
|
||||
<a-form-item label="学生配额">
|
||||
<a-input-number
|
||||
v-model:value="quotaForm.studentQuota"
|
||||
:min="currentTenant?.studentCount || 1"
|
||||
:max="10000"
|
||||
style="width: 100%"
|
||||
/>
|
||||
<a-input-number v-model:value="quotaForm.studentQuota" :min="currentTenant?.studentCount || 1" :max="10000"
|
||||
style="width: 100%" />
|
||||
<div style="color: #999; font-size: 12px">
|
||||
已使用: {{ currentTenant?.studentCount || 0 }}
|
||||
</div>
|
||||
@ -252,12 +179,7 @@
|
||||
</a-modal>
|
||||
|
||||
<!-- 详情抽屉 -->
|
||||
<a-drawer
|
||||
v-model:open="drawerVisible"
|
||||
title="租户详情"
|
||||
width="600"
|
||||
:destroy-on-close="true"
|
||||
>
|
||||
<a-drawer v-model:open="drawerVisible" title="租户详情" width="600" :destroy-on-close="true">
|
||||
<template v-if="detailData">
|
||||
<a-descriptions :column="2" bordered size="small">
|
||||
<a-descriptions-item label="学校名称" :span="2">
|
||||
@ -267,10 +189,7 @@
|
||||
{{ detailData.loginAccount }}
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item label="状态">
|
||||
<a-badge
|
||||
:status="getStatusType(detailData.status)"
|
||||
:text="getStatusText(detailData.status)"
|
||||
/>
|
||||
<a-badge :status="getStatusType(detailData.status)" :text="getStatusText(detailData.status)" />
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item label="联系人">
|
||||
{{ detailData.contactPerson || '-' }}
|
||||
@ -278,7 +197,7 @@
|
||||
<a-descriptions-item label="联系电话">
|
||||
{{ detailData.contactPhone || '-' }}
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item label="学校地址" :span="2">
|
||||
<a-descriptions-item label="学习地址" :span="2">
|
||||
{{ detailData.address || '-' }}
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item label="套餐类型">
|
||||
@ -309,23 +228,13 @@
|
||||
</a-row>
|
||||
|
||||
<a-divider>最近教师</a-divider>
|
||||
<a-table
|
||||
v-if="detailData.teachers && detailData.teachers.length > 0"
|
||||
:columns="teacherColumns"
|
||||
:data-source="detailData.teachers"
|
||||
:pagination="false"
|
||||
size="small"
|
||||
/>
|
||||
<a-table v-if="detailData.teachers && detailData.teachers.length > 0" :columns="teacherColumns"
|
||||
:data-source="detailData.teachers" :pagination="false" size="small" />
|
||||
<a-empty v-else description="暂无教师数据" />
|
||||
|
||||
<a-divider>最近学生</a-divider>
|
||||
<a-table
|
||||
v-if="detailData.students && detailData.students.length > 0"
|
||||
:columns="studentColumns"
|
||||
:data-source="detailData.students"
|
||||
:pagination="false"
|
||||
size="small"
|
||||
/>
|
||||
<a-table v-if="detailData.students && detailData.students.length > 0" :columns="studentColumns"
|
||||
:data-source="detailData.students" :pagination="false" size="small" />
|
||||
<a-empty v-else description="暂无学生数据" />
|
||||
</template>
|
||||
<a-skeleton v-else active />
|
||||
@ -411,9 +320,8 @@ const modalLoading = ref(false);
|
||||
const isEdit = ref(false);
|
||||
const formRef = ref<FormInstance>();
|
||||
const editingId = ref<number | null>(null);
|
||||
const dateRange = ref<[string, string] | undefined>(undefined);
|
||||
|
||||
const formData = reactive<CreateTenantDto & { dateRange?: string[] }>({
|
||||
const formData = reactive<CreateTenantDto & { dateRange?: [string, string] }>({
|
||||
name: '',
|
||||
loginAccount: '',
|
||||
password: '',
|
||||
@ -426,6 +334,7 @@ const formData = reactive<CreateTenantDto & { dateRange?: string[] }>({
|
||||
studentQuota: 200,
|
||||
startDate: '',
|
||||
expireDate: '',
|
||||
dateRange: undefined,
|
||||
});
|
||||
|
||||
const formRules = {
|
||||
@ -434,7 +343,30 @@ const formRules = {
|
||||
{ required: true, message: '请输入登录账号' },
|
||||
{ pattern: /^[a-zA-Z][a-zA-Z0-9_]{3,19}$/, message: '字母开头,4-20位字母数字或下划线' },
|
||||
],
|
||||
contactPhone: [{ pattern: /^1[3-9]\d{9}$/, message: '请输入正确的手机号' }],
|
||||
contactPerson: [{ required: true, message: '请输入联系人' }],
|
||||
contactPhone: [
|
||||
{ required: true, message: '请输入联系电话' },
|
||||
{
|
||||
validator: (_: unknown, value: string) => {
|
||||
if (!value) return Promise.resolve();
|
||||
return /^1[3-9]\d{9}$/.test(value) ? Promise.resolve() : Promise.reject(new Error('请输入正确的手机号'));
|
||||
},
|
||||
trigger: 'blur',
|
||||
},
|
||||
],
|
||||
address: [{ required: true, message: '请输入学习地址' }],
|
||||
packageType: [{ required: true, message: '请选择套餐' }],
|
||||
teacherQuota: [
|
||||
{ required: true, message: '请输入教师配额' },
|
||||
{ type: 'number' as const, min: 1, max: 1000, message: '教师配额需为 1-1000 的整数' },
|
||||
],
|
||||
studentQuota: [
|
||||
{ required: true, message: '请输入学生配额' },
|
||||
{ type: 'number' as const, min: 1, max: 10000, message: '学生配额需为 1-10000 的整数' },
|
||||
],
|
||||
dateRange: [
|
||||
{ required: true, message: '请选择有效期', type: 'array' as const, trigger: 'change' },
|
||||
],
|
||||
};
|
||||
|
||||
// 配额弹窗
|
||||
@ -454,6 +386,11 @@ const detailData = ref<TenantDetail | null>(null);
|
||||
// 套餐列表
|
||||
const packageList = ref<CoursePackage[]>([]);
|
||||
|
||||
// 禁用过去的日期(有效期不能选今天之前的日期)
|
||||
const disabledPastDate = (current: dayjs.Dayjs) => {
|
||||
return current && current < dayjs().startOf('day');
|
||||
};
|
||||
|
||||
// 格式化价格(分转为元)
|
||||
const formatPackagePrice = (priceInCents: number) => {
|
||||
return (priceInCents / 100).toFixed(2);
|
||||
@ -547,11 +484,11 @@ const showAddModal = () => {
|
||||
packageId: undefined,
|
||||
teacherQuota: 20,
|
||||
studentQuota: 200,
|
||||
});
|
||||
dateRange.value = [
|
||||
dateRange: [
|
||||
dayjs().format('YYYY-MM-DD'),
|
||||
dayjs().add(1, 'year').format('YYYY-MM-DD'),
|
||||
] as [string, string];
|
||||
] as [string, string],
|
||||
});
|
||||
modalVisible.value = true;
|
||||
};
|
||||
|
||||
@ -567,8 +504,8 @@ const handleEdit = (record: Tenant) => {
|
||||
packageType: record.packageType,
|
||||
teacherQuota: record.teacherQuota,
|
||||
studentQuota: record.studentQuota,
|
||||
dateRange: [record.startDate, record.expireDate] as [string, string],
|
||||
});
|
||||
dateRange.value = [record.startDate, record.expireDate] as [string, string];
|
||||
modalVisible.value = true;
|
||||
};
|
||||
|
||||
@ -582,7 +519,7 @@ const handleModalOk = async () => {
|
||||
|
||||
modalLoading.value = true;
|
||||
try {
|
||||
const [startDate, expireDate] = dateRange.value || [];
|
||||
const [startDate, expireDate] = formData.dateRange || [];
|
||||
const data = {
|
||||
...formData,
|
||||
startDate,
|
||||
@ -594,7 +531,7 @@ const handleModalOk = async () => {
|
||||
message.success('更新成功');
|
||||
} else {
|
||||
const res = await createTenant(data as CreateTenantDto);
|
||||
message.success(`创建成功,初始密码: ${res.tempPassword}`);
|
||||
message.success(`创建成功,初始密码: ${res.tempPassword || '123456'}`);
|
||||
}
|
||||
modalVisible.value = false;
|
||||
loadData();
|
||||
|
||||
@ -19,6 +19,9 @@ public class TenantCreateRequest {
|
||||
@Schema(description = "租户编码/登录账号")
|
||||
private String code;
|
||||
|
||||
@Schema(description = "初始密码(留空默认 123456)")
|
||||
private String password;
|
||||
|
||||
@Schema(description = "联系人")
|
||||
private String contactName;
|
||||
|
||||
|
||||
@ -23,6 +23,7 @@ import com.reading.platform.service.CoursePackageService;
|
||||
import com.reading.platform.service.TenantService;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.security.crypto.password.PasswordEncoder;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
import org.springframework.util.StringUtils;
|
||||
@ -46,6 +47,7 @@ public class TenantServiceImpl extends com.baomidou.mybatisplus.extension.servic
|
||||
private final CourseCollectionMapper collectionMapper;
|
||||
private final TeacherMapper teacherMapper;
|
||||
private final StudentMapper studentMapper;
|
||||
private final PasswordEncoder passwordEncoder;
|
||||
|
||||
@Override
|
||||
@Transactional
|
||||
@ -84,6 +86,11 @@ public class TenantServiceImpl extends com.baomidou.mybatisplus.extension.servic
|
||||
|
||||
tenant.setStatus("ACTIVE");
|
||||
|
||||
// 设置登录账号和密码(username 与 code 一致用于登录)
|
||||
tenant.setUsername(request.getCode());
|
||||
String rawPassword = StringUtils.hasText(request.getPassword()) ? request.getPassword() : "123456";
|
||||
tenant.setPassword(passwordEncoder.encode(rawPassword));
|
||||
|
||||
// 如果传入了 collectionId,查询课程套餐信息并填充相关字段
|
||||
if (request.getCollectionId() != null) {
|
||||
CourseCollection collection = collectionMapper.selectById(request.getCollectionId());
|
||||
|
||||
Loading…
Reference in New Issue
Block a user