feat: 认证管理页面

This commit is contained in:
zheng020 2026-03-19 19:36:09 +08:00
parent 52367cb030
commit c5e49b0bbc
45 changed files with 3471 additions and 152 deletions

View File

@ -359,14 +359,14 @@
- _Requirements: 3.2, 9.5_
- [ ] 18. 前端Vue3界面实现 - 企业认证管理页面
- [ ] 18.1 创建企业认证管理页面
- [x] 18.1 创建企业认证管理页面
- 创建企业认证列表组件
- 创建认证筛选组件
- 创建企业认证详情对话框(显示审核历史和审核人角色)
- 创建批量审核对话框
- _Requirements: 6.1, 6.2, 6.3, 6.6, 9.6, 10.6_
- [ ] 18.2 实现企业认证管理功能
- [x] 18.2 实现企业认证管理功能
- 实现企业认证列表查询和分页
- 实现认证状态筛选
- 实现企业认证搜索(按用户姓名、企业名称、企业代码)
@ -386,8 +386,8 @@
- [ ] 19. Checkpoint - 确保前端所有功能正常
- 确保所有前端功能正常,如有问题请向用户询问
- [ ] 20. 数据安全和权限加固
- [ ] 20.1 实现数据访问权限控制
- [x] 20. 数据安全和权限加固
- [x] 20.1 实现数据访问权限控制
- 在Service层添加权限检查
- 记录敏感数据访问日志
- 验证用户只能访问自己的认证数据(管理员除外)
@ -403,22 +403,22 @@
- 测试审核日志记录角色信息
- _Requirements: 10.4, 10.5, 10.6_
- [ ] 20.4 配置HTTPS和安全头
- [x] 20.4 配置HTTPS和安全头
- 配置HTTPS证书
- 配置安全响应头HSTS, CSP等
- _Requirements: 7.2_
- [ ] 21. 性能优化
- [ ] 21.1 实现认证状态缓存
- [x] 21. 性能优化
- [x] 21.1 实现认证状态缓存
- 使用Redis缓存用户认证状态
- 设置合理的缓存过期时间
- 认证状态变更时清除缓存
- [ ] 21.2 实现二维码图片缓存
- [x] 21.2 实现二维码图片缓存
- 缓存生成的二维码图片
- 设置合理的缓存过期时间
- [ ] 21.3 优化数据库查询
- [x] 21.3 优化数据库查询
- 添加必要的索引
- 优化复杂查询的SQL

View File

@ -0,0 +1,68 @@
import request from '@/utils/request'
// 查询租户列表
export function listTenant(query) {
return request({
url: '/system/tenant/list',
method: 'get',
params: query
})
}
// 查询租户详细
export function getTenant(tenantId) {
return request({
url: '/system/tenant/info/' + tenantId,
method: 'get'
})
}
// 根据租户编码查询租户
export function getTenantByCode(tenantCode) {
return request({
url: '/system/tenant/code/' + tenantCode,
method: 'get'
})
}
// 新增租户
export function addTenant(data) {
return request({
url: '/system/tenant',
method: 'post',
data: data
})
}
// 修改租户
export function updateTenant(data) {
return request({
url: '/system/tenant',
method: 'put',
data: data
})
}
// 删除租户
export function delTenant(tenantId) {
return request({
url: '/system/tenant/' + tenantId,
method: 'delete'
})
}
// 冻结租户
export function freezeTenant(tenantId) {
return request({
url: '/system/tenant/freeze/' + tenantId,
method: 'put'
})
}
// 激活租户
export function activeTenant(tenantId) {
return request({
url: '/system/tenant/active/' + tenantId,
method: 'put'
})
}

View File

@ -65,3 +65,96 @@ export function getUserVerificationStatus() {
method: 'get'
})
}
// ===== 企业认证管理接口(管理员使用)=====
/**
* 查询企业认证列表管理员
* @param {Object} params - 查询参数
* @param {string} params.userName - 用户姓名
* @param {string} params.enterpriseName - 企业名称
* @param {string} params.enterpriseCode - 企业代码
* @param {string} params.verificationStatus - 认证状态
* @param {number} params.pageNum - 页码
* @param {number} params.pageSize - 每页数量
* @returns {Promise}
*/
export function listEnterpriseVerifications(params) {
return request({
url: '/system/user/verification/enterprise/list',
method: 'get',
params
})
}
/**
* 查询企业认证详情含审核历史
* @param {number} verificationId - 认证ID
* @returns {Promise}
*/
export function getEnterpriseVerificationDetail(verificationId) {
return request({
url: `/system/user/verification/enterprise/${verificationId}`,
method: 'get'
})
}
/**
* 导出企业认证数据
* @param {Object} params - 查询参数
* @returns {Promise}
*/
export function exportEnterpriseVerifications(params) {
return request({
url: '/system/user/verification/enterprise/export',
method: 'get',
params,
responseType: 'blob'
})
}
// ===== 身份认证管理接口(管理员使用)=====
/**
* 查询身份认证列表管理员
* @param {Object} params - 查询参数
* @param {string} params.userName - 用户姓名
* @param {string} params.realName - 真实姓名
* @param {string} params.verificationStatus - 认证状态
* @param {number} params.pageNum - 页码
* @param {number} params.pageSize - 每页数量
* @returns {Promise}
*/
export function listIdentityVerifications(params) {
return request({
url: '/system/user/verification/identity/list',
method: 'get',
params
})
}
/**
* 查询身份认证详情含审核历史
* @param {number} verificationId - 认证ID
* @returns {Promise}
*/
export function getIdentityVerificationDetail(verificationId) {
return request({
url: `/system/user/verification/identity/${verificationId}`,
method: 'get'
})
}
/**
* 导出身份认证数据
* @param {Object} params - 查询参数
* @returns {Promise}
*/
export function exportIdentityVerifications(params) {
return request({
url: '/system/user/verification/identity/export',
method: 'get',
params,
responseType: 'blob'
})
}

View File

@ -161,6 +161,14 @@
<el-tag v-else type="warning">其他</el-tag>
</template>
</el-table-column>
<el-table-column label="实名认证" align="center" prop="verificationStatus" width="100">
<template #default="scope">
<el-tag v-if="scope.row.verificationStatus === 'APPROVED'" type="success">已认证</el-tag>
<el-tag v-else-if="scope.row.verificationStatus === 'REJECTED'" type="danger">认证失败</el-tag>
<el-tag v-else-if="scope.row.verificationStatus === 'PENDING'" type="warning">待认证</el-tag>
<el-tag v-else type="info">未认证</el-tag>
</template>
</el-table-column>
<el-table-column label="创建时间" align="center" prop="createTime" width="160" />
<el-table-column label="更新时间" align="center" prop="updateTime" width="160" />
<el-table-column label="操作" align="center" class-name="small-padding fixed-width" width="300" fixed="right">
@ -451,6 +459,12 @@
<el-tag v-if="detailData.isTemporary" type="warning"></el-tag>
<el-tag v-else type="success"></el-tag>
</el-descriptions-item>
<el-descriptions-item label="实名认证状态">
<el-tag v-if="detailData.verificationStatus === 'APPROVED'" type="success">已认证</el-tag>
<el-tag v-else-if="detailData.verificationStatus === 'REJECTED'" type="danger">认证失败</el-tag>
<el-tag v-else-if="detailData.verificationStatus === 'PENDING'" type="warning">待认证</el-tag>
<el-tag v-else type="info">未认证</el-tag>
</el-descriptions-item>
<el-descriptions-item label="员工库">{{ detailData.libraryName || '暂无' }}</el-descriptions-item>
<el-descriptions-item label="创建时间">{{ formatDateTime(detailData.createTime) }}</el-descriptions-item>
<el-descriptions-item label="更新时间">{{ formatDateTime(detailData.updateTime) }}</el-descriptions-item>
@ -942,7 +956,8 @@ function handleView(row) {
isTemporary: employeeData.isTemporary || false,
hireDate: employeeData.hireDate,
createTime: employeeData.createTime,
updateTime: employeeData.updateTime
updateTime: employeeData.updateTime,
verificationStatus: employeeData.verificationStatus || null
};
detailOpen.value = true;
@ -1573,6 +1588,9 @@ function handleQRCodeDialogClose() {
//
qrCodeData.value = {};
currentEmployeeForQRCode.value = null;
//
getList();
}
onMounted(() => {

View File

@ -0,0 +1,327 @@
<template>
<div class="app-container">
<el-form :model="queryParams" ref="queryRef" :inline="true" v-show="showSearch" label-width="68px">
<el-form-item label="租户名称" prop="tenantName">
<el-input
v-model="queryParams.tenantName"
placeholder="请输入租户名称"
clearable
style="width: 200px"
@keyup.enter="handleQuery"
/>
</el-form-item>
<el-form-item label="租户编码" prop="tenantCode">
<el-input
v-model="queryParams.tenantCode"
placeholder="请输入租户编码"
clearable
style="width: 200px"
@keyup.enter="handleQuery"
/>
</el-form-item>
<el-form-item label="公司类型" prop="companyType">
<el-select v-model="queryParams.companyType" placeholder="请选择公司类型" clearable style="width: 200px">
<el-option label="银行" value="BANK" />
<el-option label="甲方" value="CLIENT" />
<el-option label="劳务公司" value="LABOR" />
</el-select>
</el-form-item>
<el-form-item label="状态" prop="status">
<el-select v-model="queryParams.status" placeholder="租户状态" clearable style="width: 200px">
<el-option label="正常" value="ACTIVE" />
<el-option label="冻结" value="FROZEN" />
<el-option label="已删除" value="DELETED" />
</el-select>
</el-form-item>
<el-form-item>
<el-button type="primary" icon="Search" @click="handleQuery">搜索</el-button>
<el-button icon="Refresh" @click="resetQuery">重置</el-button>
</el-form-item>
</el-form>
<el-row :gutter="10" class="mb8">
<el-col :span="1.5">
<el-button
type="primary"
plain
icon="Plus"
@click="handleAdd"
v-hasPermi="['system:tenant:add']"
>新增</el-button>
</el-col>
<el-col :span="1.5">
<el-button
type="danger"
plain
icon="Delete"
:disabled="multiple"
@click="handleDelete"
v-hasPermi="['system:tenant:remove']"
>删除</el-button>
</el-col>
<right-toolbar v-model:showSearch="showSearch" @queryTable="getList"></right-toolbar>
</el-row>
<el-table v-loading="loading" :data="tenantList" @selection-change="handleSelectionChange">
<el-table-column type="selection" width="55" align="center" />
<el-table-column label="租户ID" align="center" prop="tenantId" width="80" />
<el-table-column label="租户编码" align="center" prop="tenantCode" width="150" />
<el-table-column label="租户名称" align="center" prop="tenantName" min-width="150" />
<el-table-column label="公司类型" align="center" prop="companyType" width="120">
<template #default="scope">
<el-tag v-if="scope.row.companyType === 'BANK'" type="success">银行</el-tag>
<el-tag v-else-if="scope.row.companyType === 'CLIENT'" type="warning">甲方</el-tag>
<el-tag v-else-if="scope.row.companyType === 'LABOR'" type="primary">劳务公司</el-tag>
<span v-else>-</span>
</template>
</el-table-column>
<el-table-column label="联系人" align="center" prop="contactPerson" width="100" />
<el-table-column label="联系电话" align="center" prop="contactPhone" width="130" />
<el-table-column label="状态" align="center" prop="status" width="80">
<template #default="scope">
<el-tag v-if="scope.row.status === 'ACTIVE'" type="success">正常</el-tag>
<el-tag v-else-if="scope.row.status === 'FROZEN'" type="warning">冻结</el-tag>
<el-tag v-else-if="scope.row.status === 'DELETED'" type="danger">已删除</el-tag>
</template>
</el-table-column>
<el-table-column label="最大用户数" align="center" prop="maxUsers" width="100" />
<el-table-column label="创建时间" align="center" prop="createTime" width="180">
<template #default="scope">
<span>{{ parseTime(scope.row.createTime) }}</span>
</template>
</el-table-column>
<el-table-column label="操作" align="center" class-name="small-padding fixed-width" width="180">
<template #default="scope">
<el-button link type="primary" icon="Edit" @click="handleUpdate(scope.row)" v-hasPermi="['system:tenant:edit']">修改</el-button>
<el-button v-if="scope.row.tenantId !== 1" link type="primary" icon="Lock" @click="handleFreeze(scope.row)" v-hasPermi="['system:tenant:edit']">
{{ scope.row.status === 'ACTIVE' ? '冻结' : '激活' }}
</el-button>
<el-button v-if="scope.row.tenantId !== 1" link type="primary" icon="Delete" @click="handleDelete(scope.row)" v-hasPermi="['system:tenant:remove']">删除</el-button>
</template>
</el-table-column>
</el-table>
<pagination
v-show="total > 0"
:total="total"
v-model:page="queryParams.pageNum"
v-model:limit="queryParams.pageSize"
@pagination="getList"
/>
<!-- 添加或修改租户对话框 -->
<el-dialog :title="title" v-model="open" width="600px" append-to-body>
<el-form ref="formRef" :model="form" :rules="rules" label-width="100px">
<el-form-item label="租户编码" prop="tenantCode">
<el-input v-model="form.tenantCode" placeholder="请输入租户编码" :disabled="form.tenantId !== undefined" />
</el-form-item>
<el-form-item label="租户名称" prop="tenantName">
<el-input v-model="form.tenantName" placeholder="请输入租户名称" />
</el-form-item>
<el-form-item label="公司类型" prop="companyType">
<el-select v-model="form.companyType" placeholder="请选择公司类型" style="width: 100%">
<el-option label="银行" value="BANK" />
<el-option label="甲方" value="CLIENT" />
<el-option label="劳务公司" value="LABOR" />
</el-select>
</el-form-item>
<el-form-item label="联系人" prop="contactPerson">
<el-input v-model="form.contactPerson" placeholder="请输入联系人" />
</el-form-item>
<el-form-item label="联系电话" prop="contactPhone">
<el-input v-model="form.contactPhone" placeholder="请输入联系电话" />
</el-form-item>
<el-form-item label="联系邮箱" prop="contactEmail">
<el-input v-model="form.contactEmail" placeholder="请输入联系邮箱" />
</el-form-item>
<el-form-item label="最大用户数" prop="maxUsers">
<el-input-number v-model="form.maxUsers" :min="1" :max="10000" style="width: 100%" />
</el-form-item>
<el-form-item label="最大公司数" prop="maxCompanies">
<el-input-number v-model="form.maxCompanies" :min="1" :max="1000" style="width: 100%" />
</el-form-item>
<el-form-item label="过期日期" prop="expireDate">
<el-date-picker
v-model="form.expireDate"
type="date"
placeholder="选择过期日期"
style="width: 100%"
value-format="YYYY-MM-DD"
/>
</el-form-item>
<el-form-item label="状态" prop="status">
<el-radio-group v-model="form.status">
<el-radio label="ACTIVE">正常</el-radio>
<el-radio label="FROZEN">冻结</el-radio>
</el-radio-group>
</el-form-item>
<el-form-item label="备注" prop="remark">
<el-input v-model="form.remark" type="textarea" placeholder="请输入备注" />
</el-form-item>
</el-form>
<template #footer>
<div class="dialog-footer">
<el-button type="primary" @click="submitForm"> </el-button>
<el-button @click="cancel"> </el-button>
</div>
</template>
</el-dialog>
</div>
</template>
<script setup name="Tenant">
import { listTenant, getTenant, addTenant, updateTenant, delTenant, freezeTenant, activeTenant } from '@/api/system/tenant'
const { proxy } = getCurrentInstance()
const loading = ref(true)
const showSearch = ref(true)
const tenantList = ref([])
const total = ref(0)
const multiple = ref(true)
const ids = ref([])
const title = ref('')
const open = ref(false)
const formRef = ref()
const form = ref({})
const rules = ref({
tenantCode: [{ required: true, message: '租户编码不能为空', trigger: 'blur' }],
tenantName: [{ required: true, message: '租户名称不能为空', trigger: 'blur' }],
companyType: [{ required: true, message: '请选择公司类型', trigger: 'change' }],
status: [{ required: true, message: '请选择状态', trigger: 'change' }]
})
const queryParams = ref({
pageNum: 1,
pageSize: 10,
tenantCode: undefined,
tenantName: undefined,
companyType: undefined,
status: undefined
})
/** 查询租户列表 */
function getList() {
loading.value = true
listTenant(queryParams.value).then(response => {
tenantList.value = response.rows
total.value = response.total
loading.value = false
})
}
/** 取消按钮 */
function cancel() {
open.value = false
reset()
}
/** 表单重置 */
function reset() {
form.value = {
tenantId: undefined,
tenantCode: undefined,
tenantName: undefined,
tenantType: 'COMPANY',
companyType: undefined,
contactPerson: undefined,
contactPhone: undefined,
contactEmail: undefined,
maxUsers: 10,
maxCompanies: 5,
expireDate: undefined,
status: 'ACTIVE',
remark: undefined
}
if (formRef.value) {
formRef.value.resetFields()
}
}
/** 搜索按钮操作 */
function handleQuery() {
queryParams.value.pageNum = 1
getList()
}
/** 重置按钮操作 */
function resetQuery() {
proxy.$refs.queryRef.resetFields()
handleQuery()
}
/** 多选框选中数据 */
function handleSelectionChange(selection) {
ids.value = selection.map(item => item.tenantId)
multiple.value = !selection.length
}
/** 新增按钮操作 */
function handleAdd() {
reset()
open.value = true
title.value = '添加租户'
}
/** 修改按钮操作 */
function handleUpdate(row) {
reset()
const tenantId = row.tenantId || ids.value
getTenant(tenantId).then(response => {
form.value = response.data
open.value = true
title.value = '修改租户'
})
}
/** 冻结/激活按钮操作 */
function handleFreeze(row) {
const tenantId = row.tenantId
const status = row.status === 'ACTIVE' ? '冻结' : '激活'
proxy.$modal.confirm('确认要' + status + '租户【' + row.tenantName + '】吗?').then(() => {
if (row.status === 'ACTIVE') {
return freezeTenant(tenantId)
} else {
return activeTenant(tenantId)
}
}).then(() => {
getList()
proxy.$modal.msgSuccess(status + '成功')
}).catch(() => {})
}
/** 删除按钮操作 */
function handleDelete(row) {
const tenantIds = row.tenantId || ids.value
proxy.$modal.confirm('是否确认删除租户编号为"' + tenantIds + '"的数据项?').then(() => {
return delTenant(tenantIds)
}).then(() => {
getList()
proxy.$modal.msgSuccess('删除成功')
}).catch(() => {})
}
/** 提交按钮 */
function submitForm() {
proxy.$refs.formRef.validate(valid => {
if (valid) {
if (form.value.tenantId !== undefined) {
updateTenant(form.value).then(response => {
proxy.$modal.msgSuccess('修改成功')
open.value = false
getList()
})
} else {
addTenant(form.value).then(response => {
proxy.$modal.msgSuccess('新增成功')
open.value = false
getList()
})
}
}
})
}
getList()
</script>

View File

@ -0,0 +1,192 @@
<template>
<div class="app-container">
<!-- 搜索筛选区 -->
<el-form :model="queryParams" ref="queryRef" :inline="true" v-show="showSearch" label-width="80px">
<el-form-item label="用户姓名" prop="userName">
<el-input v-model="queryParams.userName" placeholder="请输入用户姓名" clearable style="width: 200px" @keyup.enter="handleQuery" />
</el-form-item>
<el-form-item label="企业名称" prop="enterpriseName">
<el-input v-model="queryParams.enterpriseName" placeholder="请输入企业名称" clearable style="width: 200px" @keyup.enter="handleQuery" />
</el-form-item>
<el-form-item label="企业代码" prop="enterpriseCode">
<el-input v-model="queryParams.enterpriseCode" placeholder="请输入统一社会信用代码" clearable style="width: 220px" @keyup.enter="handleQuery" />
</el-form-item>
<el-form-item label="认证状态" prop="verificationStatus">
<el-select v-model="queryParams.verificationStatus" placeholder="请选择认证状态" clearable style="width: 160px">
<el-option label="待认证" value="PENDING" />
<el-option label="已认证" value="APPROVED" />
<el-option label="认证失败" value="REJECTED" />
</el-select>
</el-form-item>
<el-form-item>
<el-button type="primary" icon="Search" @click="handleQuery">搜索</el-button>
<el-button icon="Refresh" @click="resetQuery">重置</el-button>
</el-form-item>
</el-form>
<!-- 操作按钮区 -->
<el-row :gutter="10" class="mb8">
<el-col :span="1.5">
<el-button type="warning" plain icon="Download" @click="handleExport" v-hasPermi="['system:verification:export']">导出</el-button>
</el-col>
<right-toolbar v-model:showSearch="showSearch" @queryTable="getList"></right-toolbar>
</el-row>
<!-- 数据表格 -->
<el-table v-loading="loading" :data="verificationList">
<el-table-column label="认证ID" align="center" prop="verificationId" width="80" />
<el-table-column label="用户姓名" align="center" prop="userName" min-width="100" :show-overflow-tooltip="true" />
<el-table-column label="企业名称" align="center" prop="enterpriseName" min-width="160" :show-overflow-tooltip="true" />
<el-table-column label="统一社会信用代码" align="center" prop="enterpriseCode" min-width="180" :show-overflow-tooltip="true" />
<el-table-column label="法人代表" align="center" prop="legalPerson" min-width="100" />
<el-table-column label="认证状态" align="center" prop="verificationStatus" width="110">
<template #default="scope">
<el-tag :type="getStatusTagType(scope.row.verificationStatus)">
{{ getStatusText(scope.row.verificationStatus) }}
</el-tag>
</template>
</el-table-column>
<el-table-column label="CA认证时间" align="center" prop="caVerificationTime" min-width="160">
<template #default="scope">
<span>{{ scope.row.caVerificationTime ? parseTime(scope.row.caVerificationTime) : '-' }}</span>
</template>
</el-table-column>
<el-table-column label="提交时间" align="center" prop="createTime" min-width="160">
<template #default="scope">
<span>{{ parseTime(scope.row.createTime) }}</span>
</template>
</el-table-column>
<el-table-column label="操作" align="center" width="100" fixed="right">
<template #default="scope">
<el-tooltip content="查看详情" placement="top">
<el-button link type="primary" icon="View" @click="handleDetail(scope.row)" v-hasPermi="['system:verification:view:all']" />
</el-tooltip>
</template>
</el-table-column>
</el-table>
<pagination v-show="total > 0" :total="total" v-model:page="queryParams.pageNum" v-model:limit="queryParams.pageSize" @pagination="getList" />
<!-- 企业认证详情对话框 -->
<el-dialog title="企业认证详情" v-model="detailOpen" width="700px" append-to-body>
<el-descriptions :column="2" border v-if="currentDetail">
<el-descriptions-item label="认证ID">{{ currentDetail.verificationId }}</el-descriptions-item>
<el-descriptions-item label="用户姓名">{{ currentDetail.userName }}</el-descriptions-item>
<el-descriptions-item label="企业名称" :span="2">{{ currentDetail.enterpriseName }}</el-descriptions-item>
<el-descriptions-item label="统一社会信用代码" :span="2">{{ currentDetail.enterpriseCode }}</el-descriptions-item>
<el-descriptions-item label="法人代表">{{ currentDetail.legalPerson || '-' }}</el-descriptions-item>
<el-descriptions-item label="认证状态">
<el-tag :type="getStatusTagType(currentDetail.verificationStatus)">
{{ getStatusText(currentDetail.verificationStatus) }}
</el-tag>
</el-descriptions-item>
<el-descriptions-item label="CA认证ID" :span="2">{{ currentDetail.caVerificationId || '-' }}</el-descriptions-item>
<el-descriptions-item label="CA认证时间" :span="2">{{ currentDetail.caVerificationTime ? parseTime(currentDetail.caVerificationTime) : '-' }}</el-descriptions-item>
<el-descriptions-item label="备注" :span="2">{{ currentDetail.auditRemark || '-' }}</el-descriptions-item>
<el-descriptions-item label="提交时间">{{ parseTime(currentDetail.createTime) }}</el-descriptions-item>
<el-descriptions-item label="更新时间">{{ parseTime(currentDetail.updateTime) }}</el-descriptions-item>
</el-descriptions>
<!-- 审核历史 -->
<div v-if="currentDetail && currentDetail.auditLogs && currentDetail.auditLogs.length > 0" style="margin-top: 20px">
<div style="font-weight: bold; margin-bottom: 10px; color: #606266;">审核历史</div>
<el-timeline>
<el-timeline-item
v-for="log in currentDetail.auditLogs"
:key="log.logId"
:timestamp="parseTime(log.operationTime)"
placement="top"
>
<el-card shadow="never" style="padding: 8px 12px;">
<div>
<span>状态变更</span>
<el-tag size="small" :type="getStatusTagType(log.oldStatus)" style="margin-right: 4px">{{ getStatusText(log.oldStatus) }}</el-tag>
<el-icon><ArrowRight /></el-icon>
<el-tag size="small" :type="getStatusTagType(log.newStatus)" style="margin-left: 4px">{{ getStatusText(log.newStatus) }}</el-tag>
</div>
<div style="margin-top: 6px; color: #909399; font-size: 13px;">
操作人{{ log.operatorName }}
<span v-if="log.operatorRoles" style="margin-left: 8px">{{ log.operatorRoles }}</span>
</div>
<div v-if="log.auditRemark" style="margin-top: 4px; color: #606266; font-size: 13px;">备注{{ log.auditRemark }}</div>
</el-card>
</el-timeline-item>
</el-timeline>
</div>
<template #footer>
<el-button @click="detailOpen = false"> </el-button>
</template>
</el-dialog>
</div>
</template>
<script setup name="EnterpriseVerification">
import { listEnterpriseVerifications, getEnterpriseVerificationDetail, exportEnterpriseVerifications } from '@/api/system/verification'
const { proxy } = getCurrentInstance()
const verificationList = ref([])
const loading = ref(true)
const showSearch = ref(true)
const total = ref(0)
const detailOpen = ref(false)
const currentDetail = ref(null)
const data = reactive({
queryParams: {
pageNum: 1,
pageSize: 10,
userName: undefined,
enterpriseName: undefined,
enterpriseCode: undefined,
verificationStatus: undefined
}
})
const { queryParams } = toRefs(data)
function getStatusText(status) {
const map = { PENDING: '待认证', APPROVED: '已认证', REJECTED: '认证失败' }
return map[status] || status || '-'
}
function getStatusTagType(status) {
const map = { PENDING: 'warning', APPROVED: 'success', REJECTED: 'danger' }
return map[status] || 'info'
}
function getList() {
loading.value = true
listEnterpriseVerifications(queryParams.value).then(res => {
verificationList.value = res.rows
total.value = res.total
loading.value = false
}).catch(() => { loading.value = false })
}
function handleQuery() {
queryParams.value.pageNum = 1
getList()
}
function resetQuery() {
proxy.resetForm('queryRef')
handleQuery()
}
function handleDetail(row) {
getEnterpriseVerificationDetail(row.verificationId).then(res => {
currentDetail.value = res.data
detailOpen.value = true
})
}
function handleExport() {
proxy.download('system/user/verification/enterprise/export', { ...queryParams.value }, `enterprise_verification_${new Date().getTime()}.xlsx`)
}
onMounted(() => {
getList()
})
</script>

View File

@ -0,0 +1,194 @@
<template>
<div class="app-container">
<!-- 搜索筛选区 -->
<el-form :model="queryParams" ref="queryRef" :inline="true" v-show="showSearch" label-width="80px">
<el-form-item label="用户姓名" prop="userName">
<el-input v-model="queryParams.userName" placeholder="请输入用户姓名" clearable style="width: 200px" @keyup.enter="handleQuery" />
</el-form-item>
<el-form-item label="真实姓名" prop="realName">
<el-input v-model="queryParams.realName" placeholder="请输入真实姓名" clearable style="width: 200px" @keyup.enter="handleQuery" />
</el-form-item>
<el-form-item label="认证状态" prop="verificationStatus">
<el-select v-model="queryParams.verificationStatus" placeholder="请选择认证状态" clearable style="width: 160px">
<el-option label="待认证" value="PENDING" />
<el-option label="已认证" value="APPROVED" />
<el-option label="认证失败" value="REJECTED" />
</el-select>
</el-form-item>
<el-form-item>
<el-button type="primary" icon="Search" @click="handleQuery">搜索</el-button>
<el-button icon="Refresh" @click="resetQuery">重置</el-button>
</el-form-item>
</el-form>
<!-- 操作按钮区 -->
<el-row :gutter="10" class="mb8">
<el-col :span="1.5">
<el-button type="warning" plain icon="Download" @click="handleExport" v-hasPermi="['system:verification:export']">导出</el-button>
</el-col>
<right-toolbar v-model:showSearch="showSearch" @queryTable="getList"></right-toolbar>
</el-row>
<!-- 数据表格 -->
<el-table v-loading="loading" :data="verificationList">
<el-table-column label="认证ID" align="center" prop="verificationId" width="80" />
<el-table-column label="用户姓名" align="center" prop="userName" min-width="100" :show-overflow-tooltip="true" />
<el-table-column label="真实姓名" align="center" prop="realName" min-width="100" />
<el-table-column label="认证状态" align="center" prop="verificationStatus" width="110">
<template #default="scope">
<el-tag :type="getStatusTagType(scope.row.verificationStatus)">
{{ getStatusText(scope.row.verificationStatus) }}
</el-tag>
</template>
</el-table-column>
<el-table-column label="CA认证ID" align="center" prop="caVerificationId" min-width="200" :show-overflow-tooltip="true">
<template #default="scope">
<span>{{ scope.row.caVerificationId || '-' }}</span>
</template>
</el-table-column>
<el-table-column label="认证时间" align="center" prop="verificationTime" min-width="160">
<template #default="scope">
<span>{{ scope.row.verificationTime ? parseTime(scope.row.verificationTime) : '-' }}</span>
</template>
</el-table-column>
<el-table-column label="拒绝原因" align="center" prop="rejectReason" min-width="160" :show-overflow-tooltip="true">
<template #default="scope">
<span>{{ scope.row.rejectReason || '-' }}</span>
</template>
</el-table-column>
<el-table-column label="提交时间" align="center" prop="createTime" min-width="160">
<template #default="scope">
<span>{{ parseTime(scope.row.createTime) }}</span>
</template>
</el-table-column>
<el-table-column label="操作" align="center" width="100" fixed="right">
<template #default="scope">
<el-tooltip content="查看详情" placement="top">
<el-button link type="primary" icon="View" @click="handleDetail(scope.row)" v-hasPermi="['system:verification:view:all']" />
</el-tooltip>
</template>
</el-table-column>
</el-table>
<pagination v-show="total > 0" :total="total" v-model:page="queryParams.pageNum" v-model:limit="queryParams.pageSize" @pagination="getList" />
<!-- 身份认证详情对话框 -->
<el-dialog title="身份认证详情" v-model="detailOpen" width="650px" append-to-body>
<el-descriptions :column="2" border v-if="currentDetail">
<el-descriptions-item label="认证ID">{{ currentDetail.verificationId }}</el-descriptions-item>
<el-descriptions-item label="用户姓名">{{ currentDetail.userName || '-' }}</el-descriptions-item>
<el-descriptions-item label="真实姓名" :span="2">{{ currentDetail.realName }}</el-descriptions-item>
<el-descriptions-item label="认证状态" :span="2">
<el-tag :type="getStatusTagType(currentDetail.verificationStatus)">
{{ getStatusText(currentDetail.verificationStatus) }}
</el-tag>
</el-descriptions-item>
<el-descriptions-item label="CA认证ID" :span="2">{{ currentDetail.caVerificationId || '-' }}</el-descriptions-item>
<el-descriptions-item label="认证时间" :span="2">{{ currentDetail.verificationTime ? parseTime(currentDetail.verificationTime) : '-' }}</el-descriptions-item>
<el-descriptions-item label="拒绝原因" :span="2">{{ currentDetail.rejectReason || '-' }}</el-descriptions-item>
<el-descriptions-item label="提交时间">{{ parseTime(currentDetail.createTime) }}</el-descriptions-item>
<el-descriptions-item label="更新时间">{{ parseTime(currentDetail.updateTime) }}</el-descriptions-item>
</el-descriptions>
<!-- 审核历史 -->
<div v-if="currentDetail && currentDetail.auditLogs && currentDetail.auditLogs.length > 0" style="margin-top: 20px">
<div style="font-weight: bold; margin-bottom: 10px; color: #606266;">审核历史</div>
<el-timeline>
<el-timeline-item
v-for="log in currentDetail.auditLogs"
:key="log.logId"
:timestamp="parseTime(log.operationTime)"
placement="top"
>
<el-card shadow="never" style="padding: 8px 12px;">
<div>
<span>状态变更</span>
<el-tag size="small" :type="getStatusTagType(log.oldStatus)" style="margin-right: 4px">{{ getStatusText(log.oldStatus) }}</el-tag>
<el-icon><ArrowRight /></el-icon>
<el-tag size="small" :type="getStatusTagType(log.newStatus)" style="margin-left: 4px">{{ getStatusText(log.newStatus) }}</el-tag>
</div>
<div style="margin-top: 6px; color: #909399; font-size: 13px;">
操作人{{ log.operatorName }}
<span v-if="log.operatorRoles" style="margin-left: 8px">{{ log.operatorRoles }}</span>
</div>
<div v-if="log.auditRemark" style="margin-top: 4px; color: #606266; font-size: 13px;">备注{{ log.auditRemark }}</div>
</el-card>
</el-timeline-item>
</el-timeline>
</div>
<template #footer>
<el-button @click="detailOpen = false"> </el-button>
</template>
</el-dialog>
</div>
</template>
<script setup name="IdentityVerification">
import { listIdentityVerifications, getIdentityVerificationDetail, exportIdentityVerifications } from '@/api/system/verification'
const { proxy } = getCurrentInstance()
const verificationList = ref([])
const loading = ref(true)
const showSearch = ref(true)
const total = ref(0)
const detailOpen = ref(false)
const currentDetail = ref(null)
const data = reactive({
queryParams: {
pageNum: 1,
pageSize: 10,
userName: undefined,
realName: undefined,
verificationStatus: undefined
}
})
const { queryParams } = toRefs(data)
function getStatusText(status) {
const map = { PENDING: '待认证', APPROVED: '已认证', REJECTED: '认证失败' }
return map[status] || status || '-'
}
function getStatusTagType(status) {
const map = { PENDING: 'warning', APPROVED: 'success', REJECTED: 'danger' }
return map[status] || 'info'
}
function getList() {
loading.value = true
listIdentityVerifications(queryParams.value).then(res => {
verificationList.value = res.rows
total.value = res.total
loading.value = false
}).catch(() => { loading.value = false })
}
function handleQuery() {
queryParams.value.pageNum = 1
getList()
}
function resetQuery() {
proxy.resetForm('queryRef')
handleQuery()
}
function handleDetail(row) {
getIdentityVerificationDetail(row.verificationId).then(res => {
currentDetail.value = res.data
detailOpen.value = true
})
}
function handleExport() {
proxy.download('system/user/verification/identity/export', { ...queryParams.value }, `identity_verification_${new Date().getTime()}.xlsx`)
}
onMounted(() => {
getList()
})
</script>

View File

@ -1,20 +1,76 @@
# Nginx配置文件 - 生产环境 (HTTP版本)
# Nginx配置文件 - 生产环境 (HTTPS版本)
# 用于Vue3前端静态文件服务和API代理
#
# HTTPS证书配置说明:
# 1. 将SSL证书文件放置在 /etc/nginx/ssl/ 目录下
# 2. 证书文件: /etc/nginx/ssl/server.crt
# 3. 私钥文件: /etc/nginx/ssl/server.key
# 4. 如使用Let's Encrypt路径为 /etc/letsencrypt/live/<domain>/
# HTTP -> HTTPS 重定向
server {
listen 80;
server_name _;
# 健康检查端点(不重定向,供负载均衡器使用)
location /health {
access_log off;
return 200 "healthy\n";
add_header Content-Type text/plain;
}
# 所有其他HTTP请求重定向到HTTPS
location / {
return 301 https://$host$request_uri;
}
}
# HTTPS主服务
server {
listen 443 ssl http2;
server_name _;
# SSL证书配置
ssl_certificate /etc/nginx/ssl/server.crt;
ssl_certificate_key /etc/nginx/ssl/server.key;
# TLS协议版本仅允许TLS 1.2和1.3
ssl_protocols TLSv1.2 TLSv1.3;
# 强密码套件
ssl_ciphers 'ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256';
ssl_prefer_server_ciphers on;
# SSL会话缓存
ssl_session_cache shared:SSL:10m;
ssl_session_timeout 10m;
ssl_session_tickets off;
# OCSP Stapling需要CA证书链
# ssl_stapling on;
# ssl_stapling_verify on;
# ssl_trusted_certificate /etc/nginx/ssl/chain.crt;
# 生产环境日志配置
access_log /var/log/nginx/access.log;
error_log /var/log/nginx/error.log error;
# 安全头配置
add_header X-Frame-Options DENY;
add_header X-Content-Type-Options nosniff;
add_header X-XSS-Protection "1; mode=block";
add_header Referrer-Policy "strict-origin-when-cross-origin";
# 安全响应头
# HTTP Strict Transport Security (HSTS) - 强制HTTPS有效期1年包含子域名
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains; preload" always;
# 禁止在iframe中嵌入防止点击劫持
add_header X-Frame-Options "SAMEORIGIN" always;
# 禁止MIME类型嗅探
add_header X-Content-Type-Options "nosniff" always;
# XSS保护
add_header X-XSS-Protection "1; mode=block" always;
# Referrer策略
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
# 内容安全策略
add_header Content-Security-Policy "default-src 'self'; script-src 'self' 'unsafe-inline' 'unsafe-eval'; style-src 'self' 'unsafe-inline'; img-src 'self' data: blob:; font-src 'self' data:; connect-src 'self'; frame-ancestors 'self'" always;
# 权限策略(禁用不需要的浏览器功能)
add_header Permissions-Policy "camera=(), microphone=(), geolocation=(), payment=()" always;
# Gzip压缩配置
gzip on;
gzip_vary on;
@ -31,26 +87,26 @@ server {
application/xml+rss
application/atom+xml
image/svg+xml;
# 静态文件服务
location / {
root /usr/share/nginx/html;
index index.html index.htm;
try_files $uri $uri/ /index.html;
# 生产环境缓存配置
location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|woff|woff2|ttf|eot)$ {
expires 1y;
add_header Cache-Control "public, immutable";
access_log off;
}
location ~* \.(html)$ {
expires 1h;
add_header Cache-Control "public";
}
}
# API代理到后端服务
location /prod-api/ {
proxy_pass http://anxin-backend:8080/;
@ -58,7 +114,7 @@ server {
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
# 生产环境代理配置
proxy_connect_timeout 10s;
proxy_send_timeout 10s;
@ -66,33 +122,33 @@ server {
proxy_buffering on;
proxy_buffer_size 4k;
proxy_buffers 8 4k;
# 限制请求大小
client_max_body_size 5m;
# 代理缓存配置
proxy_cache_bypass $http_upgrade;
proxy_set_header Connection 'upgrade';
proxy_set_header Upgrade $http_upgrade;
}
# 健康检查端点
location /health {
access_log off;
return 200 "healthy\n";
add_header Content-Type text/plain;
}
# 拒绝访问敏感文件
location ~ /\. {
deny all;
access_log off;
log_not_found off;
}
location ~ ~$ {
deny all;
access_log off;
log_not_found off;
}
}
}

View File

@ -9,11 +9,13 @@ server {
access_log /var/log/nginx/access.log;
error_log /var/log/nginx/error.log warn;
# 安全头配置
add_header X-Frame-Options DENY;
add_header X-Content-Type-Options nosniff;
add_header X-XSS-Protection "1; mode=block";
add_header Referrer-Policy "strict-origin-when-cross-origin";
# 安全响应头
add_header X-Frame-Options "SAMEORIGIN" always;
add_header X-Content-Type-Options "nosniff" always;
add_header X-XSS-Protection "1; mode=block" always;
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
add_header Content-Security-Policy "default-src 'self'; script-src 'self' 'unsafe-inline' 'unsafe-eval'; style-src 'self' 'unsafe-inline'; img-src 'self' data: blob:; font-src 'self' data:; connect-src 'self'; frame-ancestors 'self'" always;
add_header Permissions-Policy "camera=(), microphone=(), geolocation=(), payment=()" always;
# 静态文件服务
location / {

View File

@ -0,0 +1,63 @@
-- ============================================================================
-- 性能优化:认证相关表索引优化
-- 创建日期: 2026-03-19
-- 描述: 为认证相关表添加复合索引,优化高频查询性能
-- ============================================================================
SET NAMES utf8mb4;
-- ============================================================================
-- 1. sys_user_enterprise_verification 索引优化
-- ============================================================================
-- 复合索引用户ID + 认证状态isUserFullyVerified 和 getUserVerificationStatus 的核心查询)
-- 查询: WHERE user_id = ? ORDER BY create_time DESC LIMIT 1
-- 查询: WHERE user_id = ? AND verification_status = 'APPROVED'
ALTER TABLE `sys_user_enterprise_verification`
ADD INDEX `idx_user_status` (`user_id`, `verification_status`) USING BTREE COMMENT '用户ID+认证状态复合索引,优化认证状态查询';
-- 创建时间索引(管理列表按时间排序)
ALTER TABLE `sys_user_enterprise_verification`
ADD INDEX `idx_create_time` (`create_time`) USING BTREE COMMENT '创建时间索引,优化时间范围查询';
-- 企业名称索引(管理页面按企业名称搜索)
ALTER TABLE `sys_user_enterprise_verification`
ADD INDEX `idx_enterprise_name` (`enterprise_name`(50)) USING BTREE COMMENT '企业名称前缀索引,优化模糊搜索';
-- ============================================================================
-- 2. sys_user_identity_verification 索引优化
-- ============================================================================
-- 复合索引用户ID + 认证状态isUserFullyVerified 的核心查询)
ALTER TABLE `sys_user_identity_verification`
ADD INDEX `idx_user_status` (`user_id`, `verification_status`) USING BTREE COMMENT '用户ID+认证状态复合索引,优化认证状态查询';
-- 创建时间索引(管理列表按时间排序)
ALTER TABLE `sys_user_identity_verification`
ADD INDEX `idx_create_time` (`create_time`) USING BTREE COMMENT '创建时间索引,优化时间范围查询';
-- ============================================================================
-- 3. sys_verification_audit_log 索引优化
-- ============================================================================
-- 复合索引用户ID + 操作时间(查询某用户的审核历史,按时间排序)
ALTER TABLE `sys_verification_audit_log`
ADD INDEX `idx_user_time` (`user_id`, `operation_time`) USING BTREE COMMENT '用户ID+操作时间复合索引,优化用户审核历史查询';
-- ============================================================================
-- 4. dc_employee_qr_code 索引优化
-- ============================================================================
-- 复合索引员工ID + 二维码状态(查询员工有效二维码)
ALTER TABLE `dc_employee_qr_code`
ADD INDEX `idx_employee_status` (`employee_id`, `qr_code_status`) USING BTREE COMMENT '员工ID+状态复合索引,优化员工有效二维码查询';
-- 复合索引:二维码状态 + 过期时间(定时清理过期二维码)
ALTER TABLE `dc_employee_qr_code`
ADD INDEX `idx_status_expiry` (`qr_code_status`, `expiry_time`) USING BTREE COMMENT '状态+过期时间复合索引,优化过期二维码清理查询';
-- ============================================================================
-- 完成
-- ============================================================================
SELECT '认证相关表性能索引优化完成!' AS message;

View File

@ -20,6 +20,7 @@ import com.ruoyi.common.core.text.Convert;
import com.ruoyi.common.utils.DateUtils;
import com.ruoyi.common.utils.SecurityUtils;
import com.ruoyi.common.utils.StringUtils;
import com.ruoyi.framework.security.context.TenantContextHolder;
import com.ruoyi.framework.web.service.SysLoginService;
import com.ruoyi.framework.web.service.SysPermissionService;
import com.ruoyi.framework.web.service.TokenService;
@ -56,7 +57,7 @@ public class SysLoginController
/**
* 登录方法
*
*
* @param loginBody 登录信息
* @return 结果
*/
@ -67,6 +68,10 @@ public class SysLoginController
// 生成令牌
String token = loginService.login(loginBody.getUsername(), loginBody.getPassword(), loginBody.getCode(),
loginBody.getUuid());
// 登录成功后设置租户上下文
TenantContextHolder.setCurrentTenantId(TenantContextHolder.DEFAULT_TENANT_ID);
ajax.put(Constants.TOKEN, token);
return ajax;
}

View File

@ -0,0 +1,123 @@
package com.ruoyi.web.controller.system;
import java.util.List;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.PutMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import com.ruoyi.common.annotation.Log;
import com.ruoyi.common.core.controller.BaseController;
import com.ruoyi.common.core.domain.AjaxResult;
import com.ruoyi.common.core.domain.entity.SysTenant;
import com.ruoyi.common.enums.BusinessType;
import com.ruoyi.system.service.ISysTenantService;
/**
* 租户信息
*
* @author ruoyi
*/
@RestController
@RequestMapping("/system/tenant")
public class SysTenantController extends BaseController
{
@Autowired
private ISysTenantService tenantService;
/**
* 获取租户列表
*/
@PreAuthorize("@ss.hasPermi('system:tenant:list')")
@GetMapping("/list")
public AjaxResult list(SysTenant tenant)
{
List<SysTenant> list = tenantService.selectTenantList(tenant);
return success(list);
}
/**
* 获取租户详情
*/
@PreAuthorize("@ss.hasPermi('system:tenant:query')")
@GetMapping("/info/{tenantId}")
public AjaxResult getInfo(@PathVariable Long tenantId)
{
return success(tenantService.selectTenantById(tenantId));
}
/**
* 根据租户编码获取租户信息
*/
@GetMapping("/code/{tenantCode}")
public AjaxResult getByCode(@PathVariable String tenantCode)
{
return success(tenantService.selectTenantByCode(tenantCode));
}
/**
* 新增租户
*/
@PreAuthorize("@ss.hasPermi('system:tenant:add')")
@Log(title = "租户管理", businessType = BusinessType.INSERT)
@PostMapping
public AjaxResult add(@RequestBody SysTenant tenant)
{
return toAjax(tenantService.insertTenant(tenant));
}
/**
* 修改租户
*/
@PreAuthorize("@ss.hasPermi('system:tenant:edit')")
@Log(title = "租户管理", businessType = BusinessType.UPDATE)
@PutMapping
public AjaxResult edit(@RequestBody SysTenant tenant)
{
return toAjax(tenantService.updateTenant(tenant));
}
/**
* 删除租户
*/
@PreAuthorize("@ss.hasPermi('system:tenant:remove')")
@Log(title = "租户管理", businessType = BusinessType.DELETE)
@DeleteMapping("/{tenantId}")
public AjaxResult remove(@PathVariable Long tenantId)
{
return toAjax(tenantService.deleteTenantById(tenantId));
}
/**
* 冻结租户
*/
@PreAuthorize("@ss.hasPermi('system:tenant:edit')")
@Log(title = "租户管理", businessType = BusinessType.UPDATE)
@PutMapping("/freeze/{tenantId}")
public AjaxResult freeze(@PathVariable Long tenantId)
{
SysTenant tenant = new SysTenant();
tenant.setTenantId(tenantId);
tenant.setStatus("FROZEN");
return toAjax(tenantService.updateTenant(tenant));
}
/**
* 激活租户
*/
@PreAuthorize("@ss.hasPermi('system:tenant:edit')")
@Log(title = "租户管理", businessType = BusinessType.UPDATE)
@PutMapping("/active/{tenantId}")
public AjaxResult active(@PathVariable Long tenantId)
{
SysTenant tenant = new SysTenant();
tenant.setTenantId(tenantId);
tenant.setStatus("ACTIVE");
return toAjax(tenantService.updateTenant(tenant));
}
}

View File

@ -1,7 +1,9 @@
package com.ruoyi.web.controller.system;
import java.util.List;
import javax.servlet.http.HttpServletResponse;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.security.access.prepost.PreAuthorize;
@ -13,6 +15,7 @@ import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import com.ruoyi.common.annotation.Log;
import com.ruoyi.common.core.controller.BaseController;
import com.ruoyi.common.core.domain.AjaxResult;
@ -75,7 +78,7 @@ public class UserVerificationController extends BaseController
/**
* 查询企业认证列表
*/
@PreAuthorize("hasAnyAuthority('system:verification:view:all', 'system:verification:manage')")
@PreAuthorize("@ss.hasAnyPermi('system:verification:view:all,system:verification:manage')")
@GetMapping("/enterprise/list")
public TableDataInfo listEnterpriseVerifications(UserEnterpriseVerification verification)
{
@ -100,7 +103,7 @@ public class UserVerificationController extends BaseController
/**
* 查询企业认证详情
*/
@PreAuthorize("hasAnyAuthority('system:verification:view:all', 'system:verification:manage')")
@PreAuthorize("@ss.hasAnyPermi('system:verification:view:all,system:verification:manage')")
@GetMapping("/enterprise/{verificationId}")
public AjaxResult getEnterpriseVerificationDetail(@PathVariable Long verificationId)
{
@ -135,7 +138,7 @@ public class UserVerificationController extends BaseController
/**
* 导出企业认证数据
*/
@PreAuthorize("hasAnyAuthority('system:verification:export', 'system:verification:manage')")
@PreAuthorize("@ss.hasAnyPermi('system:verification:export,system:verification:manage')")
@Log(title = "企业认证", businessType = BusinessType.EXPORT)
@PostMapping("/enterprise/export")
public void exportEnterpriseVerifications(HttpServletResponse response, UserEnterpriseVerification verification)
@ -200,22 +203,87 @@ public class UserVerificationController extends BaseController
/**
* 查询身份认证列表
*/
@PreAuthorize("@ss.hasAnyPermi('system:verification:view:all,system:verification:manage') or #userId == null or #userId == authentication.principal.userId")
@GetMapping("/identity/list")
public TableDataInfo listIdentityVerifications(
@RequestParam(required = false) Long userId,
@RequestParam(required = false) String realName,
@RequestParam(required = false) String verificationStatus)
{
try
{
startPage();
List<com.ruoyi.system.domain.UserIdentityVerification> list =
verificationService.listIdentityVerifications(userId, verificationStatus);
List<com.ruoyi.system.domain.UserIdentityVerification> list =
verificationService.listIdentityVerifications(userId, realName, verificationStatus);
return getDataTable(list);
}
catch (AccessDeniedException e)
{
logger.error("权限不足", e);
throw e;
}
catch (Exception e)
{
logger.error("查询身份认证列表失败", e);
return getDataTable(null);
}
}
/**
* 查询身份认证详情
*/
@PreAuthorize("@ss.hasAnyPermi('system:verification:view:all,system:verification:manage')")
@GetMapping("/identity/{verificationId}")
public AjaxResult getIdentityVerificationDetail(@PathVariable Long verificationId)
{
try
{
com.ruoyi.system.domain.UserIdentityVerification detail =
verificationService.getIdentityVerificationDetail(verificationId);
if (detail == null)
{
return AjaxResult.error("认证记录不存在");
}
return AjaxResult.success(detail);
}
catch (AccessDeniedException e)
{
logger.error("权限不足", e);
throw e;
}
catch (Exception e)
{
logger.error("查询身份认证详情失败", e);
return AjaxResult.error("查询身份认证详情失败:" + e.getMessage());
}
}
/**
* 导出身份认证数据
*/
@PreAuthorize("@ss.hasAnyPermi('system:verification:export,system:verification:manage')")
@Log(title = "身份认证", businessType = BusinessType.EXPORT)
@PostMapping("/identity/export")
public void exportIdentityVerifications(HttpServletResponse response,
@RequestParam(required = false) Long userId,
@RequestParam(required = false) String realName,
@RequestParam(required = false) String verificationStatus)
{
try
{
List<com.ruoyi.system.domain.UserIdentityVerification> list =
verificationService.listIdentityVerifications(userId, realName, verificationStatus);
ExcelUtil<com.ruoyi.system.domain.UserIdentityVerification> util =
new ExcelUtil<>(com.ruoyi.system.domain.UserIdentityVerification.class);
util.exportExcel(response, list, "身份认证数据");
}
catch (AccessDeniedException e)
{
logger.error("权限不足", e);
}
catch (Exception e)
{
logger.error("导出身份认证数据失败", e);
}
}
}

View File

@ -0,0 +1,40 @@
# HTTPS配置文件
# 生产环境启用HTTPS时在spring.profiles.active中添加 https
# 示例: spring.profiles.active: druid,api-security,https
#
# 使用前提:
# 1. 准备PKCS12格式的SSL证书 (keystore.p12)
# 2. 将证书文件放置在 src/main/resources/ 目录下,或指定绝对路径
# 3. 通过环境变量 SSL_KEYSTORE_PASSWORD 设置证书密码
#
# 生成自签名证书示例 (仅用于测试):
# keytool -genkeypair -alias ruoyi -keyalg RSA -keysize 2048 \
# -storetype PKCS12 -keystore keystore.p12 -validity 3650 \
# -storepass changeit -dname "CN=localhost, OU=RuoYi, O=RuoYi, L=Beijing, ST=Beijing, C=CN"
server:
# HTTPS端口
port: 8443
ssl:
enabled: true
# 证书路径支持classpath:或绝对路径)
key-store: classpath:keystore.p12
# 证书密码(通过环境变量注入,避免明文存储)
key-store-password: ${SSL_KEYSTORE_PASSWORD:changeit}
key-store-type: PKCS12
key-alias: ruoyi
# TLS协议版本仅允许TLS 1.2和1.3
protocol: TLS
enabled-protocols:
- TLSv1.2
- TLSv1.3
# 强密码套件(禁用弱加密算法)
ciphers:
- TLS_AES_256_GCM_SHA384
- TLS_AES_128_GCM_SHA256
- TLS_CHACHA20_POLY1305_SHA256
- TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384
- TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256
# HTTP重定向到HTTPS的配置需要同时启用http-redirect profile
# 如需同时监听HTTP(80)并重定向到HTTPS(8443),请参考 HttpsRedirectConfig

View File

@ -54,6 +54,13 @@ server:
max: 800
# Tomcat启动初始化的线程数默认值10
min-spare: 100
# HTTPS配置生产环境启用取消注释并配置证书路径
# ssl:
# enabled: true
# key-store: classpath:keystore.p12
# key-store-password: ${SSL_KEYSTORE_PASSWORD}
# key-store-type: PKCS12
# key-alias: ruoyi
# 日志配置
logging:

View File

@ -51,4 +51,38 @@ public class CacheConstants
* 验证统计信息 redis key
*/
public static final String VALIDATION_STATS_KEY = "validation_stats:";
/**
* 用户认证状态缓存 redis key
* 格式: verification_status:{userId}
*/
public static final String VERIFICATION_STATUS_KEY = "verification_status:";
/**
* 用户企业认证缓存 redis key
* 格式: enterprise_verification:{userId}
*/
public static final String ENTERPRISE_VERIFICATION_KEY = "enterprise_verification:";
/**
* 用户身份认证缓存 redis key
* 格式: identity_verification:{userId}
*/
public static final String IDENTITY_VERIFICATION_KEY = "identity_verification:";
/**
* 员工二维码图片缓存 redis key
* 格式: qrcode_image:{qrCodeId}
*/
public static final String QRCODE_IMAGE_KEY = "qrcode_image:";
/**
* 认证状态缓存过期时间分钟
*/
public static final int VERIFICATION_STATUS_EXPIRE_MINUTES = 30;
/**
* 二维码图片缓存过期时间分钟
*/
public static final int QRCODE_IMAGE_EXPIRE_MINUTES = 60;
}

View File

@ -0,0 +1,263 @@
package com.ruoyi.common.core.domain.entity;
import java.util.Date;
import org.apache.commons.lang3.builder.ToStringBuilder;
import org.apache.commons.lang3.builder.ToStringStyle;
import com.ruoyi.common.annotation.Excel;
import com.ruoyi.common.core.domain.BaseEntity;
/**
* 租户表 sys_tenant
*
* @author ruoyi
*/
public class SysTenant extends BaseEntity
{
private static final long serialVersionUID = 1L;
/** 租户ID */
@Excel(name = "租户序号")
private Long tenantId;
/** 租户编码 */
@Excel(name = "租户编码")
private String tenantCode;
/** 租户名称 */
@Excel(name = "租户名称")
private String tenantName;
/** 租户类型 */
@Excel(name = "租户类型")
private String tenantType;
/** 公司类型 */
@Excel(name = "公司类型", readConverterExp = "BANK=银行,CLIENT=甲方,LABOR=劳务公司")
private String companyType;
/** 联系人 */
@Excel(name = "联系人")
private String contactPerson;
/** 联系电话 */
@Excel(name = "联系电话")
private String contactPhone;
/** 联系邮箱 */
@Excel(name = "联系邮箱")
private String contactEmail;
/** 租户域名 */
private String domain;
/** Logo URL */
private String logoUrl;
/** 状态 */
@Excel(name = "状态", readConverterExp = "ACTIVE=正常,FROZEN=冻结,DELETED=已删除")
private String status;
/** 最大用户数 */
private Integer maxUsers;
/** 最大公司数 */
private Integer maxCompanies;
/** 过期日期 */
private Date expireDate;
/** 套餐ID */
private Long packageId;
/** 备注 */
private String remark;
public Long getTenantId()
{
return tenantId;
}
public void setTenantId(Long tenantId)
{
this.tenantId = tenantId;
}
public String getTenantCode()
{
return tenantCode;
}
public void setTenantCode(String tenantCode)
{
this.tenantCode = tenantCode;
}
public String getTenantName()
{
return tenantName;
}
public void setTenantName(String tenantName)
{
this.tenantName = tenantName;
}
public String getTenantType()
{
return tenantType;
}
public void setTenantType(String tenantType)
{
this.tenantType = tenantType;
}
public String getCompanyType()
{
return companyType;
}
public void setCompanyType(String companyType)
{
this.companyType = companyType;
}
public String getContactPerson()
{
return contactPerson;
}
public void setContactPerson(String contactPerson)
{
this.contactPerson = contactPerson;
}
public String getContactPhone()
{
return contactPhone;
}
public void setContactPhone(String contactPhone)
{
this.contactPhone = contactPhone;
}
public String getContactEmail()
{
return contactEmail;
}
public void setContactEmail(String contactEmail)
{
this.contactEmail = contactEmail;
}
public String getDomain()
{
return domain;
}
public void setDomain(String domain)
{
this.domain = domain;
}
public String getLogoUrl()
{
return logoUrl;
}
public void setLogoUrl(String logoUrl)
{
this.logoUrl = logoUrl;
}
public String getStatus()
{
return status;
}
public void setStatus(String status)
{
this.status = status;
}
public Integer getMaxUsers()
{
return maxUsers;
}
public void setMaxUsers(Integer maxUsers)
{
this.maxUsers = maxUsers;
}
public Integer getMaxCompanies()
{
return maxCompanies;
}
public void setMaxCompanies(Integer maxCompanies)
{
this.maxCompanies = maxCompanies;
}
public Date getExpireDate()
{
return expireDate;
}
public void setExpireDate(Date expireDate)
{
this.expireDate = expireDate;
}
public Long getPackageId()
{
return packageId;
}
public void setPackageId(Long packageId)
{
this.packageId = packageId;
}
@Override
public String getRemark()
{
return remark;
}
@Override
public void setRemark(String remark)
{
this.remark = remark;
}
@Override
public String toString()
{
return new ToStringBuilder(this, ToStringStyle.MULTI_LINE_STYLE)
.append("tenantId", getTenantId())
.append("tenantCode", getTenantCode())
.append("tenantName", getTenantName())
.append("tenantType", getTenantType())
.append("companyType", getCompanyType())
.append("contactPerson", getContactPerson())
.append("contactPhone", getContactPhone())
.append("contactEmail", getContactEmail())
.append("domain", getDomain())
.append("logoUrl", getLogoUrl())
.append("status", getStatus())
.append("maxUsers", getMaxUsers())
.append("maxCompanies", getMaxCompanies())
.append("expireDate", getExpireDate())
.append("packageId", getPackageId())
.append("createBy", getCreateBy())
.append("createTime", getCreateTime())
.append("updateBy", getUpdateBy())
.append("updateTime", getUpdateTime())
.append("remark", getRemark())
.toString();
}
}

View File

@ -68,6 +68,9 @@ public class SysUser extends BaseEntity
/** 删除标志0代表存在 2代表删除 */
private String delFlag;
/** 租户ID */
private Long tenantId;
/** 最后登录IP */
@Excel(name = "最后登录IP", type = Type.EXPORT)
private String loginIp;
@ -246,6 +249,16 @@ public class SysUser extends BaseEntity
this.delFlag = delFlag;
}
public Long getTenantId()
{
return tenantId;
}
public void setTenantId(Long tenantId)
{
this.tenantId = tenantId;
}
public String getLoginIp()
{
return loginIp;

View File

@ -1,13 +1,32 @@
package com.ruoyi.credit.config;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.ruoyi.credit.domain.ValidationRule;
import com.ruoyi.credit.mapper.ValidationRuleMapper;
import java.io.IOException;
import java.io.InputStream;
import java.nio.file.FileSystems;
import java.nio.file.Path;
import java.nio.file.StandardWatchEventKinds;
import java.nio.file.WatchEvent;
import java.nio.file.WatchKey;
import java.nio.file.WatchService;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;
import javax.annotation.PostConstruct;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.context.event.ApplicationReadyEvent;
import org.springframework.context.event.EventListener;
import org.springframework.core.io.Resource;
@ -15,16 +34,9 @@ import org.springframework.core.io.ResourceLoader;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
import javax.annotation.PostConstruct;
import java.io.IOException;
import java.io.InputStream;
import java.nio.file.*;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.ruoyi.credit.domain.ValidationRule;
import com.ruoyi.credit.mapper.ValidationRuleMapper;
/**
* 验证规则配置管理类
@ -91,14 +103,32 @@ public class ValidationRuleConfig {
private volatile String configVersion = "1.0.0";
/**
* 初始化配置
* 初始化配置仅加载配置文件不访问数据库
*/
@PostConstruct
public void init() {
log.info("初始化验证规则配置管理器...");
// 加载初始配置
loadValidationRules();
// 仅从配置文件加载不访问数据库数据库在 ApplicationReadyEvent 后才安全访问
loadRulesFromConfigFile();
rebuildCacheIndexes();
log.info("验证规则配置管理器初始化完成(配置文件),当前加载规则数量: {}", ruleCache.size());
}
/**
* 应用启动完成后加载数据库规则并启动热更新
*/
@EventListener(ApplicationReadyEvent.class)
public void onApplicationReady() {
log.info("应用启动完成,开始从数据库加载验证规则...");
// 数据库连接池就绪后再加载
loadRulesFromDatabase();
rebuildCacheIndexes();
// 同步配置文件规则到数据库
syncDatabaseRules();
// 启动热更新机制
if (properties.getHotReload().isEnabled()) {
@ -110,16 +140,7 @@ public class ValidationRuleConfig {
startFileWatch();
}
log.info("验证规则配置管理器初始化完成,当前加载规则数量: {}", ruleCache.size());
}
/**
* 应用启动完成后加载数据库中的规则
*/
@EventListener(ApplicationReadyEvent.class)
public void onApplicationReady() {
log.info("应用启动完成,开始同步数据库中的验证规则...");
syncDatabaseRules();
log.info("验证规则配置管理器完全初始化,当前加载规则数量: {}", ruleCache.size());
}
/**

View File

@ -89,6 +89,9 @@ public class EmployeeInfo extends BaseEntity
@Excel(name = "部门ID")
private Long deptId;
/** 实名认证状态(来自最新二维码记录,非数据库字段) */
private String verificationStatus;
// 薪资单位常量
public static final String SALARY_UNIT_HOURLY = "HOURLY";
public static final String SALARY_UNIT_DAILY = "DAILY";
@ -260,6 +263,16 @@ public class EmployeeInfo extends BaseEntity
return deptId;
}
public void setVerificationStatus(String verificationStatus)
{
this.verificationStatus = verificationStatus;
}
public String getVerificationStatus()
{
return verificationStatus;
}
@Override
public String toString() {
return new ToStringBuilder(this,ToStringStyle.MULTI_LINE_STYLE)

View File

@ -3,6 +3,7 @@ package com.ruoyi.credit.dto;
import java.math.BigDecimal;
import java.util.Date;
import java.util.List;
import com.fasterxml.jackson.annotation.JsonFormat;
/**
@ -71,6 +72,9 @@ public class EmployeeRequestDTO {
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private Date updateTime;
/** 实名认证状态: PENDING-待认证, APPROVED-已认证, REJECTED-认证失败, null-未认证 */
private String verificationStatus;
// Getters and Setters
public Long getEmployeeId() {
return employeeId;
@ -216,6 +220,14 @@ public class EmployeeRequestDTO {
this.updateTime = updateTime;
}
public String getVerificationStatus() {
return verificationStatus;
}
public void setVerificationStatus(String verificationStatus) {
this.verificationStatus = verificationStatus;
}
@Override
public String toString() {
return "EmployeeRequestDTO{" +

View File

@ -3,6 +3,7 @@ package com.ruoyi.credit.service.impl;
import java.util.Calendar;
import java.util.Date;
import java.util.UUID;
import java.util.concurrent.TimeUnit;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@ -13,7 +14,9 @@ import org.springframework.transaction.annotation.Transactional;
import com.alibaba.fastjson2.JSON;
import com.alibaba.fastjson2.JSONObject;
import com.google.zxing.WriterException;
import com.ruoyi.common.constant.CacheConstants;
import com.ruoyi.common.core.domain.AjaxResult;
import com.ruoyi.common.core.redis.RedisCache;
import com.ruoyi.common.utils.QRCodeGenerator;
import com.ruoyi.credit.ca.CAServiceInvoker;
import com.ruoyi.credit.ca.config.CAServiceConfig;
@ -48,6 +51,9 @@ public class EmployeeQRCodeServiceImpl implements IEmployeeQRCodeService
@Autowired
private CAServiceConfig caServiceConfig;
@Autowired
private RedisCache redisCache;
/**
* 为员工生成实名认证二维码
*
@ -144,6 +150,15 @@ public class EmployeeQRCodeServiceImpl implements IEmployeeQRCodeService
{
log.info("获取二维码图片: qrCodeId={}", qrCodeId);
// 尝试从缓存获取
String cacheKey = CacheConstants.QRCODE_IMAGE_KEY + qrCodeId;
byte[] cachedImage = redisCache.getCacheObject(cacheKey);
if (cachedImage != null)
{
log.debug("从缓存获取二维码图片: qrCodeId={}, size={}bytes", qrCodeId, cachedImage.length);
return cachedImage;
}
try {
// 1. 查询二维码记录
EmployeeQRCode qrCode = employeeQRCodeMapper.selectEmployeeQRCodeByQrCodeId(qrCodeId);
@ -163,6 +178,14 @@ public class EmployeeQRCodeServiceImpl implements IEmployeeQRCodeService
// 3. 生成二维码图片
byte[] imageBytes = QRCodeGenerator.generateQRCode(qrCodeData.toJSONString());
// 4. 缓存图片使用二维码剩余有效期作为缓存时间最多缓存QRCODE_IMAGE_EXPIRE_MINUTES分钟
long remainingMinutes = (qrCode.getExpiryTime().getTime() - System.currentTimeMillis()) / 60000;
if (remainingMinutes > 0)
{
long cacheMinutes = Math.min(remainingMinutes, CacheConstants.QRCODE_IMAGE_EXPIRE_MINUTES);
redisCache.setCacheObject(cacheKey, imageBytes, (int) cacheMinutes, TimeUnit.MINUTES);
}
log.info("成功生成二维码图片: qrCodeId={}, size={}bytes", qrCodeId, imageBytes.length);
return imageBytes;
@ -220,6 +243,8 @@ public class EmployeeQRCodeServiceImpl implements IEmployeeQRCodeService
// 更新二维码状态为已过期
qrCode.setQrCodeStatus(EmployeeQRCode.QR_CODE_STATUS_EXPIRED);
employeeQRCodeMapper.updateEmployeeQRCode(qrCode);
// 清除图片缓存
redisCache.deleteObject(CacheConstants.QRCODE_IMAGE_KEY + qrCodeId);
return AjaxResult.error("二维码已过期,请重新生成");
}
@ -246,6 +271,8 @@ public class EmployeeQRCodeServiceImpl implements IEmployeeQRCodeService
qrCode.setCaVerificationId(response.getVerificationId());
qrCode.setVerificationTime(response.getVerificationTime());
employeeQRCodeMapper.updateEmployeeQRCode(qrCode);
// 清除图片缓存二维码已使用
redisCache.deleteObject(CacheConstants.QRCODE_IMAGE_KEY + qrCodeId);
AjaxResult result = AjaxResult.success("员工实名认证成功");
result.put("verificationId", response.getVerificationId());

View File

@ -1,11 +1,11 @@
package com.ruoyi.credit.util;
import com.ruoyi.common.utils.StringUtils;
import com.ruoyi.common.utils.spring.SpringUtils;
import com.ruoyi.credit.domain.EmployeeInfo;
import com.ruoyi.credit.domain.EmployeeLibrary;
import com.ruoyi.credit.dto.EmployeeRequestDTO;
import com.ruoyi.credit.service.IEmployeeManagerService;
import com.ruoyi.common.utils.StringUtils;
import com.ruoyi.common.utils.spring.SpringUtils;
/**
* 员工信息转换工具类
@ -116,6 +116,9 @@ public class EmployeeConverter {
} else {
dto.setSalaryUnits(new java.util.ArrayList<>());
}
// 映射实名认证状态
dto.setVerificationStatus(entity.getVerificationStatus());
return dto;
}

View File

@ -2,13 +2,13 @@
api:
# 限流配置
rateLimit:
enabled: true
enabled: false
maxRequests: 100 # 每个时间窗口最大请求数
timeWindow: 60 # 时间窗口(秒)
# 签名验证配置
signature:
enabled: true
enabled: false
secretKey: ${API_SECRET_KEY:digital_credit_secret_key_2025}
timestampTolerance: 300 # 时间戳容忍度(秒)
@ -54,7 +54,7 @@ spring:
- OPTIONS
allowed-headers:
- "*"
allow-credentials: true
allow-credentials: false
max-age: 3600
# 监控和度量配置
@ -69,6 +69,6 @@ management:
metrics:
export:
prometheus:
enabled: true
enabled: false
tags:
application: digital-credit-service

View File

@ -5,76 +5,89 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
<mapper namespace="com.ruoyi.credit.mapper.EmployeeInfoMapper">
<resultMap type="EmployeeInfo" id="EmployeeInfoResult">
<result property="employeeId" column="employee_id" />
<result property="employeeNumber" column="employee_number" />
<result property="employeeName" column="employee_name" />
<result property="idCardNumber" column="id_card_number" />
<result property="position" column="position" />
<result property="department" column="department" />
<result property="salaryUnit" column="salary_unit" />
<result property="hourlySalary" column="hourly_salary" />
<result property="dailySalary" column="daily_salary" />
<result property="monthlySalary" column="monthly_salary" />
<result property="employeeStatus" column="employee_status" />
<result property="hireDate" column="hire_date" />
<result property="contactPhone" column="contact_phone" />
<result property="libraryId" column="library_id" />
<result property="isTemporary" column="is_temporary" />
<result property="createTime" column="create_time" />
<result property="updateTime" column="update_time" />
<result property="createBy" column="created_by" />
<result property="updateBy" column="updated_by" />
<result property="remark" column="remark" />
<result property="deptId" column="dept_id" />
<result property="employeeId" column="employee_id" />
<result property="employeeNumber" column="employee_number" />
<result property="employeeName" column="employee_name" />
<result property="idCardNumber" column="id_card_number" />
<result property="position" column="position" />
<result property="department" column="department" />
<result property="salaryUnit" column="salary_unit" />
<result property="hourlySalary" column="hourly_salary" />
<result property="dailySalary" column="daily_salary" />
<result property="monthlySalary" column="monthly_salary" />
<result property="employeeStatus" column="employee_status" />
<result property="hireDate" column="hire_date" />
<result property="contactPhone" column="contact_phone" />
<result property="libraryId" column="library_id" />
<result property="isTemporary" column="is_temporary" />
<result property="createTime" column="create_time" />
<result property="updateTime" column="update_time" />
<result property="createBy" column="created_by" />
<result property="updateBy" column="updated_by" />
<result property="remark" column="remark" />
<result property="deptId" column="dept_id" />
<result property="verificationStatus" column="verification_status"/>
</resultMap>
<sql id="selectEmployeeInfoVo">
select employee_id, employee_number, employee_name, id_card_number, position, department, salary_unit, hourly_salary, daily_salary, monthly_salary, employee_status, hire_date, contact_phone, library_id, is_temporary, create_time, update_time, created_by, updated_by, remark, dept_id from dc_employee_info
select e.employee_id, e.employee_number, e.employee_name, e.id_card_number, e.position, e.department,
e.salary_unit, e.hourly_salary, e.daily_salary, e.monthly_salary, e.employee_status,
e.hire_date, e.contact_phone, e.library_id, e.is_temporary, e.create_time, e.update_time,
e.created_by, e.updated_by, e.remark, e.dept_id,
qr.verification_status
from dc_employee_info e
left join (
select employee_id, verification_status
from dc_employee_qr_code
where (employee_id, generate_time) in (
select employee_id, max(generate_time) from dc_employee_qr_code group by employee_id
)
) qr on e.employee_id = qr.employee_id
</sql>
<select id="selectEmployeeInfoList" parameterType="EmployeeInfo" resultMap="EmployeeInfoResult">
<include refid="selectEmployeeInfoVo"/>
<where>
<if test="employeeNumber != null and employeeNumber != ''"> and employee_number like concat('%', #{employeeNumber}, '%')</if>
<if test="employeeName != null and employeeName != ''"> and employee_name like concat('%', #{employeeName}, '%')</if>
<if test="idCardNumber != null and idCardNumber != ''"> and id_card_number like concat('%', #{idCardNumber}, '%')</if>
<if test="position != null and position != ''"> and position = #{position}</if>
<if test="department != null and department != ''"> and department = #{department}</if>
<if test="salaryUnit != null and salaryUnit != ''"> and salary_unit = #{salaryUnit}</if>
<if test="employeeStatus != null and employeeStatus != ''"> and employee_status = #{employeeStatus}</if>
<if test="hireDate != null "> and hire_date = #{hireDate}</if>
<if test="libraryId != null "> and library_id = #{libraryId}</if>
<if test="isTemporary != null "> and is_temporary = #{isTemporary}</if>
<if test="params.beginTime != null and params.beginTime != ''"><!-- 开始时间检索 -->
and date_format(create_time,'%y%m%d') &gt;= date_format(#{params.beginTime},'%y%m%d')
<where>
<if test="employeeNumber != null and employeeNumber != ''"> and e.employee_number like concat('%', #{employeeNumber}, '%')</if>
<if test="employeeName != null and employeeName != ''"> and e.employee_name like concat('%', #{employeeName}, '%')</if>
<if test="idCardNumber != null and idCardNumber != ''"> and e.id_card_number like concat('%', #{idCardNumber}, '%')</if>
<if test="position != null and position != ''"> and e.position = #{position}</if>
<if test="department != null and department != ''"> and e.department = #{department}</if>
<if test="salaryUnit != null and salaryUnit != ''"> and e.salary_unit = #{salaryUnit}</if>
<if test="employeeStatus != null and employeeStatus != ''"> and e.employee_status = #{employeeStatus}</if>
<if test="hireDate != null "> and e.hire_date = #{hireDate}</if>
<if test="libraryId != null "> and e.library_id = #{libraryId}</if>
<if test="isTemporary != null "> and e.is_temporary = #{isTemporary}</if>
<if test="params.beginTime != null and params.beginTime != ''">
and date_format(e.create_time,'%y%m%d') &gt;= date_format(#{params.beginTime},'%y%m%d')
</if>
<if test="params.endTime != null and params.endTime != ''"><!-- 结束时间检索 -->
and date_format(create_time,'%y%m%d') &lt;= date_format(#{params.endTime},'%y%m%d')
<if test="params.endTime != null and params.endTime != ''">
and date_format(e.create_time,'%y%m%d') &lt;= date_format(#{params.endTime},'%y%m%d')
</if>
</where>
order by create_time desc
order by e.create_time desc
</select>
<select id="selectEmployeeInfoByEmployeeId" parameterType="Long" resultMap="EmployeeInfoResult">
<include refid="selectEmployeeInfoVo"/>
where employee_id = #{employeeId}
where e.employee_id = #{employeeId}
</select>
<select id="selectEmployeeInfoByEmployeeNumber" parameterType="String" resultMap="EmployeeInfoResult">
<include refid="selectEmployeeInfoVo"/>
where employee_number = #{employeeNumber}
where e.employee_number = #{employeeNumber}
</select>
<select id="selectEmployeeInfosByLibraryId" parameterType="Long" resultMap="EmployeeInfoResult">
<include refid="selectEmployeeInfoVo"/>
where library_id = #{libraryId}
order by create_time desc
where e.library_id = #{libraryId}
order by e.create_time desc
</select>
<select id="selectEmployeeInfosByPosition" parameterType="String" resultMap="EmployeeInfoResult">
<include refid="selectEmployeeInfoVo"/>
where position = #{position}
order by create_time desc
where e.position = #{position}
order by e.create_time desc
</select>
<select id="countEmployeesByLibraryId" parameterType="Long" resultType="int">

View File

@ -0,0 +1,79 @@
package com.ruoyi.framework.config;
import org.apache.catalina.Context;
import org.apache.catalina.connector.Connector;
import org.apache.tomcat.util.descriptor.web.SecurityCollection;
import org.apache.tomcat.util.descriptor.web.SecurityConstraint;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.web.embedded.tomcat.TomcatServletWebServerFactory;
import org.springframework.boot.web.servlet.server.ServletWebServerFactory;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
/**
* HTTPS重定向配置
* <p>
* 当启用HTTPS时同时开启HTTP监听并将所有HTTP请求重定向到HTTPS
* 通过配置项 security.https.redirect.enabled=true 启用
* </p>
*
* 使用方式 application-https.yml 中添加
* <pre>
* security:
* https:
* redirect:
* enabled: true
* http-port: 8080
* https-port: 8443
* </pre>
*
* @author ruoyi
*/
@Configuration
@ConditionalOnProperty(value = "security.https.redirect.enabled", havingValue = "true")
public class HttpsRedirectConfig
{
@Value("${security.https.redirect.http-port:8080}")
private int httpPort;
@Value("${security.https.redirect.https-port:8443}")
private int httpsPort;
/**
* 配置额外的HTTP连接器将HTTP请求重定向到HTTPS
*/
@Bean
public ServletWebServerFactory servletContainer()
{
TomcatServletWebServerFactory tomcat = new TomcatServletWebServerFactory()
{
@Override
protected void postProcessContext(Context context)
{
// 强制所有HTTP请求使用HTTPS
SecurityConstraint securityConstraint = new SecurityConstraint();
securityConstraint.setUserConstraint("CONFIDENTIAL");
SecurityCollection collection = new SecurityCollection();
collection.addPattern("/*");
securityConstraint.addCollection(collection);
context.addConstraint(securityConstraint);
}
};
tomcat.addAdditionalTomcatConnectors(createHttpConnector());
return tomcat;
}
/**
* 创建HTTP连接器监听HTTP端口并重定向到HTTPS
*/
private Connector createHttpConnector()
{
Connector connector = new Connector(TomcatServletWebServerFactory.DEFAULT_PROTOCOL);
connector.setScheme("http");
connector.setPort(httpPort);
connector.setSecure(false);
connector.setRedirectPort(httpsPort);
return connector;
}
}

View File

@ -26,7 +26,7 @@ import org.springframework.util.ClassUtils;
import org.apache.ibatis.plugin.Interceptor;
import com.ruoyi.common.utils.StringUtils;
import com.ruoyi.framework.interceptor.DepartmentDataInterceptor;
import com.ruoyi.framework.interceptor.CompanyScopeDataInterceptor;
import com.ruoyi.framework.interceptor.TenantDataInterceptor;
import com.ruoyi.framework.service.IDepartmentDataPerformanceService;
import com.ruoyi.framework.service.impl.DepartmentDataPerformanceServiceImpl;
@ -45,7 +45,7 @@ public class MyBatisConfig
private DepartmentDataInterceptor departmentDataInterceptor;
@Autowired
private CompanyScopeDataInterceptor companyScopeDataInterceptor;
private TenantDataInterceptor tenantDataInterceptor;
/**
* 部门数据隔离性能监控服务Bean
@ -150,12 +150,9 @@ public class MyBatisConfig
sessionFactory.setMapperLocations(resolveMapperLocations(StringUtils.split(mapperLocations, ",")));
sessionFactory.setConfigLocation(new DefaultResourceLoader().getResource(configLocation));
// 注册数据隔离拦截器
// 配置拦截器执行顺序公司数据隔离拦截器优先级最高然后是部门数据隔离拦截器
sessionFactory.setPlugins(new Interceptor[]{
companyScopeDataInterceptor, // 新的公司数据隔离拦截器
departmentDataInterceptor // 原有的部门数据隔离拦截器向后兼容
});
// 注册部门数据隔离拦截器和租户数据隔离拦截器
// 配置拦截器执行顺序租户隔离 -> 部门隔离
sessionFactory.setPlugins(new Interceptor[]{tenantDataInterceptor, departmentDataInterceptor});
return sessionFactory.getObject();
}

View File

@ -15,7 +15,9 @@ import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import org.springframework.security.web.authentication.logout.LogoutFilter;
import org.springframework.security.web.header.writers.ReferrerPolicyHeaderWriter;
import org.springframework.web.filter.CorsFilter;
import com.ruoyi.framework.config.properties.PermitAllUrlProperties;
import com.ruoyi.framework.security.filter.JwtAuthenticationTokenFilter;
import com.ruoyi.framework.security.handle.AuthenticationEntryPointImpl;
@ -99,9 +101,25 @@ public class SecurityConfig
return httpSecurity
// CSRF禁用因为不使用session
.csrf(csrf -> csrf.disable())
// 禁用HTTP响应标
// 配置安全响应
.headers((headersCustomizer) -> {
headersCustomizer.cacheControl(cache -> cache.disable()).frameOptions(options -> options.sameOrigin());
headersCustomizer
.cacheControl(cache -> cache.disable())
.frameOptions(options -> options.sameOrigin())
// HTTP Strict Transport Security (HSTS)
.httpStrictTransportSecurity(hsts -> hsts
.includeSubDomains(true)
.maxAgeInSeconds(31536000))
// X-Content-Type-Options: nosniff
.contentTypeOptions(contentType -> {})
// X-XSS-Protection
.xssProtection(xss -> xss.block(true))
// Referrer-Policy
.referrerPolicy(referrer -> referrer
.policy(ReferrerPolicyHeaderWriter.ReferrerPolicy.STRICT_ORIGIN_WHEN_CROSS_ORIGIN))
// Content-Security-Policy
.contentSecurityPolicy(csp -> csp
.policyDirectives("default-src 'self'; script-src 'self' 'unsafe-inline' 'unsafe-eval'; style-src 'self' 'unsafe-inline'; img-src 'self' data: blob:; font-src 'self' data:; connect-src 'self'; frame-ancestors 'self'"));
})
// 认证失败处理类
.exceptionHandling(exception -> exception.authenticationEntryPoint(unauthorizedHandler))

View File

@ -0,0 +1,504 @@
package com.ruoyi.framework.interceptor;
import java.sql.Connection;
import java.util.Arrays;
import java.util.HashSet;
import java.util.Map;
import java.util.Properties;
import java.util.Set;
import java.util.regex.Pattern;
import org.apache.ibatis.executor.Executor;
import org.apache.ibatis.executor.statement.StatementHandler;
import org.apache.ibatis.mapping.BoundSql;
import org.apache.ibatis.mapping.MappedStatement;
import org.apache.ibatis.mapping.SqlCommandType;
import org.apache.ibatis.plugin.Interceptor;
import org.apache.ibatis.plugin.Intercepts;
import org.apache.ibatis.plugin.Invocation;
import org.apache.ibatis.plugin.Plugin;
import org.apache.ibatis.plugin.Signature;
import org.apache.ibatis.session.ResultHandler;
import org.apache.ibatis.session.RowBounds;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;
import com.ruoyi.framework.security.context.TenantContextHolder;
/**
* 租户数据隔离拦截器
*
* 自动为涉及租户隔离的表添加租户过滤条件确保用户只能访问自己租户的数据
* 超级管理员不受此限制可以访问所有租户的数据
*
* @author ruoyi
*/
@Component
@Intercepts({
@Signature(type = Executor.class, method = "query", args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class}),
@Signature(type = Executor.class, method = "update", args = {MappedStatement.class, Object.class}),
@Signature(type = StatementHandler.class, method = "prepare", args = {Connection.class, Integer.class})
})
public class TenantDataInterceptor implements Interceptor
{
private static final Logger log = LoggerFactory.getLogger(TenantDataInterceptor.class);
/**
* 需要进行租户隔离的表名集合
*/
private static final Set<String> TARGET_TABLES = new HashSet<>(Arrays.asList(
"sys_user",
"sys_dept",
"dc_contract",
"dc_service_contract",
"dc_service_period",
"dc_service_period_loan",
"dc_employee_info",
"dc_employee_library",
"dc_credit",
"dc_bank_institution",
"dc_financing",
"dc_company_relationship"
));
/**
* 忽略租户隔离的系统表
*/
private static final Set<String> IGNORE_TABLES = new HashSet<>(Arrays.asList(
"sys_tenant",
"sys_tenant_package",
"sys_role",
"sys_menu",
"sys_dict_data",
"sys_dict_type",
"sys_config",
"sys_notice",
"sys_post",
"sys_login_log",
"sys_oper_log"
));
/**
* SQL注入检测模式
*/
private static final Pattern SQL_INJECTION_PATTERN = Pattern.compile(
"('.+(\\s)*(or|and)(\\s)+.+=')|('.+(\\s)*(or|and)(\\s)+.+\\s*=\\s*.+)",
Pattern.CASE_INSENSITIVE
);
/**
* 是否启用租户隔离可通过配置覆盖
*/
private boolean enabled = true;
@Override
public Object intercept(Invocation invocation) throws Throwable
{
try
{
// 检查功能是否启用
if (!enabled)
{
log.debug("租户数据隔离功能已禁用,跳过拦截");
return invocation.proceed();
}
// 超级管理员跳过租户隔离
if (TenantContextHolder.isSuperAdmin())
{
log.debug("超级管理员访问,跳过租户数据隔离");
return invocation.proceed();
}
// 获取当前租户ID
Long currentTenantId = TenantContextHolder.getCurrentTenantId();
if (currentTenantId == null)
{
log.warn("无法获取当前租户ID跳过租户数据隔离");
return invocation.proceed();
}
Object target = invocation.getTarget();
Object[] args = invocation.getArgs();
if (target instanceof Executor)
{
return handleExecutorIntercept(invocation, args, currentTenantId);
}
else if (target instanceof StatementHandler)
{
return handleStatementHandlerIntercept(invocation, args, currentTenantId);
}
return invocation.proceed();
}
catch (Exception e)
{
log.error("租户数据隔离拦截器执行异常", e);
// 发生异常时继续执行原始操作避免影响业务
return invocation.proceed();
}
}
/**
* 处理Executor拦截
*/
private Object handleExecutorIntercept(Invocation invocation, Object[] args, Long currentTenantId) throws Throwable
{
MappedStatement mappedStatement = (MappedStatement) args[0];
Object parameter = args[1];
// 检查是否需要添加租户过滤
if (needTenantFilter(mappedStatement))
{
log.debug("为SQL操作添加租户过滤条件租户ID: {}", currentTenantId);
addTenantFilter(parameter, currentTenantId);
}
return invocation.proceed();
}
/**
* 处理StatementHandler拦截
*/
private Object handleStatementHandlerIntercept(Invocation invocation, Object[] args, Long currentTenantId) throws Throwable
{
StatementHandler statementHandler = (StatementHandler) invocation.getTarget();
BoundSql boundSql = statementHandler.getBoundSql();
String originalSql = boundSql.getSql();
// 检查SQL是否涉及目标表
if (containsTargetTables(originalSql))
{
String modifiedSql = addTenantConditionToSql(originalSql, currentTenantId);
if (!modifiedSql.equals(originalSql))
{
log.debug("修改SQL添加租户过滤条件: {}", modifiedSql);
// 通过反射修改BoundSql中的SQL
java.lang.reflect.Field sqlField = BoundSql.class.getDeclaredField("sql");
sqlField.setAccessible(true);
sqlField.set(boundSql, modifiedSql);
}
}
return invocation.proceed();
}
/**
* 检查是否需要添加租户过滤
*/
private boolean needTenantFilter(MappedStatement mappedStatement)
{
// 获取SQL命令类型
SqlCommandType sqlCommandType = mappedStatement.getSqlCommandType();
// 只对SELECTUPDATEDELETE操作进行过滤
if (sqlCommandType != SqlCommandType.SELECT &&
sqlCommandType != SqlCommandType.UPDATE &&
sqlCommandType != SqlCommandType.DELETE)
{
return false;
}
// 检查SQL是否涉及目标表
BoundSql boundSql = mappedStatement.getBoundSql(null);
if (boundSql == null)
{
return false;
}
String sql = boundSql.getSql();
return containsTargetTables(sql);
}
/**
* 检查SQL是否包含目标表
*/
private boolean containsTargetTables(String sql)
{
if (sql == null || sql.trim().isEmpty())
{
return false;
}
String lowerSql = sql.toLowerCase();
for (String table : TARGET_TABLES)
{
// 使用正则表达式匹配表名确保是完整的表名而不是子字符串
Pattern pattern = Pattern.compile("\\b" + table + "\\b", Pattern.CASE_INSENSITIVE);
if (pattern.matcher(lowerSql).find())
{
return true;
}
}
return false;
}
/**
* 为参数添加租户过滤条件
*/
private void addTenantFilter(Object parameter, Long currentTenantId)
{
if (parameter instanceof Map)
{
@SuppressWarnings("unchecked")
Map<String, Object> paramMap = (Map<String, Object>) parameter;
// 如果参数中还没有tenantId则添加
if (!paramMap.containsKey("tenantId"))
{
paramMap.put("tenantId", currentTenantId);
log.debug("添加租户过滤参数: tenantId = {}", currentTenantId);
}
}
}
/**
* 为SQL添加租户条件
*/
private String addTenantConditionToSql(String originalSql, Long currentTenantId)
{
if (originalSql == null || originalSql.trim().isEmpty())
{
return originalSql;
}
// 防止SQL注入
if (SQL_INJECTION_PATTERN.matcher(String.valueOf(currentTenantId)).find())
{
log.warn("检测到潜在的SQL注入风险跳过租户条件添加");
return originalSql;
}
String sql = originalSql.trim();
String lowerSql = sql.toLowerCase();
// 检查是否需要忽略此表
for (String ignoreTable : IGNORE_TABLES)
{
if (lowerSql.contains(ignoreTable))
{
return originalSql;
}
}
try
{
// 处理SELECT语句
if (lowerSql.startsWith("select"))
{
return addTenantConditionToSelect(sql, currentTenantId);
}
// 处理UPDATE语句
else if (lowerSql.startsWith("update"))
{
return addTenantConditionToUpdate(sql, currentTenantId);
}
// 处理DELETE语句
else if (lowerSql.startsWith("delete"))
{
return addTenantConditionToDelete(sql, currentTenantId);
}
}
catch (Exception e)
{
log.warn("添加租户条件时发生异常使用原始SQL: {}", e.getMessage());
return originalSql;
}
return originalSql;
}
/**
* 为SELECT语句添加租户条件
*/
private String addTenantConditionToSelect(String sql, Long currentTenantId)
{
String lowerSql = sql.toLowerCase();
// 查找WHERE子句的位置
int whereIndex = lowerSql.indexOf(" where ");
if (whereIndex != -1)
{
// 已有WHERE子句添加AND条件
StringBuilder sb = new StringBuilder(sql);
int insertIndex = whereIndex + 7; // " where ".length()
// 为每个目标表添加租户条件
String tableAlias = findTableAlias(lowerSql, TARGET_TABLES);
if (tableAlias != null)
{
String condition = tableAlias + ".tenant_id = " + currentTenantId + " AND ";
sb.insert(insertIndex, condition);
log.debug("添加租户过滤条件到现有WHERE子句: {}", condition);
}
return sb.toString();
}
else
{
// 没有WHERE子句添加WHERE条件
// 查找ORDER BY, GROUP BY, HAVING, LIMIT等子句的位置
String[] keywords = {" order by ", " group by ", " having ", " limit ", " offset "};
int insertIndex = sql.length();
for (String keyword : keywords)
{
int keywordIndex = lowerSql.indexOf(keyword);
if (keywordIndex != -1 && keywordIndex < insertIndex)
{
insertIndex = keywordIndex;
}
}
// 为第一个找到的目标表添加WHERE条件
String tableAlias = findTableAlias(lowerSql, TARGET_TABLES);
if (tableAlias != null)
{
String whereClause = " WHERE " + tableAlias + ".tenant_id = " + currentTenantId;
String modifiedSql = sql.substring(0, insertIndex) + whereClause + sql.substring(insertIndex);
log.debug("添加新的WHERE子句: {}", whereClause);
return modifiedSql;
}
}
log.warn("无法为SQL添加租户过滤条件未找到目标表");
return sql;
}
/**
* 查找表的别名
*/
private String findTableAlias(String lowerSql, Set<String> targetTables)
{
for (String table : targetTables)
{
if (lowerSql.contains(table))
{
// 对于UPDATE语句表名在UPDATE关键字之后
if (lowerSql.trim().startsWith("update"))
{
String updatePattern = "update\\s+" + table + "\\s+(as\\s+)?(\\w+)\\s+set";
java.util.regex.Pattern p = java.util.regex.Pattern.compile(updatePattern, java.util.regex.Pattern.CASE_INSENSITIVE);
java.util.regex.Matcher m = p.matcher(lowerSql);
if (m.find())
{
String alias = m.group(2);
if (alias != null && !isReservedKeyword(alias))
{
return alias;
}
}
return table;
}
else
{
// 对于SELECT语句查找表名后的别名
String pattern = "\\b" + table + "\\s+(as\\s+)?(\\w+)";
java.util.regex.Pattern p = java.util.regex.Pattern.compile(pattern, java.util.regex.Pattern.CASE_INSENSITIVE);
java.util.regex.Matcher m = p.matcher(lowerSql);
if (m.find())
{
String alias = m.group(2);
if (!isReservedKeyword(alias))
{
return alias;
}
}
return table;
}
}
}
return null;
}
/**
* 检查是否为SQL保留关键字
*/
private boolean isReservedKeyword(String word)
{
String[] keywords = {"inner", "left", "right", "join", "on", "where", "and", "or", "order", "by", "group", "having", "limit", "offset", "set", "from", "into", "values"};
for (String keyword : keywords)
{
if (keyword.equalsIgnoreCase(word))
{
return true;
}
}
return false;
}
/**
* 为UPDATE语句添加租户条件
*/
private String addTenantConditionToUpdate(String sql, Long currentTenantId)
{
String lowerSql = sql.toLowerCase();
int whereIndex = lowerSql.indexOf(" where ");
if (whereIndex != -1)
{
// 已有WHERE子句添加AND条件
String tableAlias = findTableAlias(lowerSql, TARGET_TABLES);
if (tableAlias != null)
{
String condition = " AND " + tableAlias + ".tenant_id = " + currentTenantId;
return sql + condition;
}
}
else
{
// 没有WHERE子句添加WHERE条件
String tableAlias = findTableAlias(lowerSql, TARGET_TABLES);
if (tableAlias != null)
{
String whereClause = " WHERE " + tableAlias + ".tenant_id = " + currentTenantId;
return sql + whereClause;
}
}
return sql;
}
/**
* 为DELETE语句添加租户条件
*/
private String addTenantConditionToDelete(String sql, Long currentTenantId)
{
return addTenantConditionToUpdate(sql, currentTenantId);
}
@Override
public Object plugin(Object target)
{
if (target instanceof Executor || target instanceof StatementHandler)
{
return Plugin.wrap(target, this);
}
return target;
}
@Override
public void setProperties(Properties properties)
{
// 可以通过properties配置拦截器参数
log.info("租户数据隔离拦截器初始化完成");
}
/**
* 设置是否启用租户隔离
*/
public void setEnabled(boolean enabled)
{
this.enabled = enabled;
}
}

View File

@ -0,0 +1,172 @@
package com.ruoyi.framework.security.context;
import com.ruoyi.common.core.domain.model.LoginUser;
import com.ruoyi.common.utils.SecurityUtils;
/**
* 租户上下文管理器
*
* 提供线程安全的租户上下文管理支持当前租户ID的获取和设置
* 以及超级管理员权限判断功能
*
* @author ruoyi
*/
public class TenantContextHolder
{
/**
* 默认租户ID兼容历史数据
*/
public static final Long DEFAULT_TENANT_ID = 1L;
/**
* 租户上下文ThreadLocal存储
*/
private static final ThreadLocal<Long> TENANT_CONTEXT = new ThreadLocal<>();
/**
* 设置当前线程的租户ID
*
* @param tenantId 租户ID
*/
public static void setCurrentTenantId(Long tenantId)
{
TENANT_CONTEXT.set(tenantId);
}
/**
* 获取当前线程的租户ID
*
* 优先从ThreadLocal获取如果没有则返回默认租户ID
*
* @return 租户ID
*/
public static Long getCurrentTenantId()
{
Long tenantId = TENANT_CONTEXT.get();
if (tenantId != null)
{
return tenantId;
}
// 如果ThreadLocal中没有则从SecurityUtils获取当前用户租户ID
try
{
tenantId = getTenantIdFromUser();
if (tenantId != null)
{
return tenantId;
}
}
catch (Exception e)
{
// 忽略异常
}
// 返回默认租户ID兼容历史数据
return DEFAULT_TENANT_ID;
}
/**
* 从当前用户获取租户ID
*
* @return 租户ID如果无法获取则返回null
*/
private static Long getTenantIdFromUser()
{
try
{
LoginUser loginUser = SecurityUtils.getLoginUser();
if (loginUser != null && loginUser.getUser() != null)
{
return loginUser.getUser().getTenantId();
}
}
catch (Exception e)
{
// 忽略异常
}
return null;
}
/**
* 清除当前线程的租户上下文
*/
public static void clear()
{
TENANT_CONTEXT.remove();
}
/**
* 判断当前用户是否为超级管理员
*
* 超级管理员可以切换到任意租户不受租户隔离限制
*
* @return true-超级管理员false-普通用户
*/
public static boolean isSuperAdmin()
{
try
{
LoginUser loginUser = SecurityUtils.getLoginUser();
if (loginUser == null || loginUser.getUser() == null)
{
return false;
}
// 使用SysUser的isAdmin方法判断是否为超级管理员
return loginUser.getUser().isAdmin();
}
catch (Exception e)
{
// 如果获取用户信息失败默认不是超级管理员
return false;
}
}
/**
* 判断是否启用租户隔离
*
* 超级管理员可以不受租户隔离限制
*
* @return true-启用租户隔离false-禁用租户隔离
*/
public static boolean isTenantEnabled()
{
// 非超级管理员都启用租户隔离
return !isSuperAdmin();
}
/**
* 获取当前用户ID
*
* @return 用户ID如果无法获取则返回null
*/
public static Long getCurrentUserId()
{
try
{
return SecurityUtils.getUserId();
}
catch (Exception e)
{
return null;
}
}
/**
* 获取当前用户名
*
* @return 用户名如果无法获取则返回null
*/
public static String getCurrentUsername()
{
try
{
return SecurityUtils.getUsername();
}
catch (Exception e)
{
return null;
}
}
}

View File

@ -16,6 +16,7 @@ import com.ruoyi.common.core.domain.model.LoginUser;
import com.ruoyi.common.event.UserAuthenticationEvent;
import com.ruoyi.common.utils.SecurityUtils;
import com.ruoyi.common.utils.StringUtils;
import com.ruoyi.framework.security.context.TenantContextHolder;
import com.ruoyi.framework.web.service.TokenService;
/**
@ -40,6 +41,14 @@ public class JwtAuthenticationTokenFilter extends OncePerRequestFilter
if (StringUtils.isNotNull(loginUser) && StringUtils.isNull(SecurityUtils.getAuthentication()))
{
tokenService.verifyToken(loginUser);
// 设置租户上下文
Long tenantId = loginUser.getUser().getTenantId();
if (tenantId != null)
{
TenantContextHolder.setCurrentTenantId(tenantId);
}
UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(loginUser, null, loginUser.getAuthorities());
authenticationToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
SecurityContextHolder.getContext().setAuthentication(authenticationToken);

View File

@ -1,6 +1,7 @@
package com.ruoyi.system.domain;
import java.util.Date;
import java.util.List;
import javax.validation.constraints.NotBlank;
import javax.validation.constraints.NotNull;
@ -15,7 +16,7 @@ import com.ruoyi.common.core.domain.BaseEntity;
/**
* 用户身份认证表 sys_user_identity_verification
*
*
* @author ruoyi
*/
public class UserIdentityVerification extends BaseEntity
@ -30,6 +31,10 @@ public class UserIdentityVerification extends BaseEntity
@NotNull(message = "用户ID不能为空")
private Long userId;
/** 用户名(关联查询,非数据库字段) */
@Excel(name = "用户名")
private String userName;
/** 真实姓名 */
@Excel(name = "真实姓名")
@NotBlank(message = "真实姓名不能为空")
@ -61,6 +66,9 @@ public class UserIdentityVerification extends BaseEntity
@Excel(name = "CA认证时间", width = 30, dateFormat = "yyyy-MM-dd HH:mm:ss")
private Date verificationTime;
/** 审核日志列表(非数据库字段,详情查询时附加) */
private List<VerificationAuditLog> auditLogs;
public Long getVerificationId()
{
return verificationId;
@ -81,6 +89,16 @@ public class UserIdentityVerification extends BaseEntity
this.userId = userId;
}
public String getUserName()
{
return userName;
}
public void setUserName(String userName)
{
this.userName = userName;
}
public String getRealName()
{
return realName;
@ -141,12 +159,23 @@ public class UserIdentityVerification extends BaseEntity
this.verificationTime = verificationTime;
}
public List<VerificationAuditLog> getAuditLogs()
{
return auditLogs;
}
public void setAuditLogs(List<VerificationAuditLog> auditLogs)
{
this.auditLogs = auditLogs;
}
@Override
public String toString()
{
return new ToStringBuilder(this, ToStringStyle.MULTI_LINE_STYLE)
.append("verificationId", getVerificationId())
.append("userId", getUserId())
.append("userName", getUserName())
.append("realName", getRealName())
.append("idCardNumber", "[MASKED]")
.append("verificationStatus", getVerificationStatus())

View File

@ -0,0 +1,76 @@
package com.ruoyi.system.mapper;
import java.util.List;
import com.ruoyi.common.core.domain.entity.SysTenant;
/**
* 租户管理 数据层
*
* @author ruoyi
*/
public interface SysTenantMapper
{
/**
* 查询租户管理数据
*
* @param tenant 租户信息
* @return 租户信息集合
*/
public List<SysTenant> selectTenantList(SysTenant tenant);
/**
* 根据租户ID查询信息
*
* @param tenantId 租户ID
* @return 租户信息
*/
public SysTenant selectTenantById(Long tenantId);
/**
* 根据租户编码查询信息
*
* @param tenantCode 租户编码
* @return 租户信息
*/
public SysTenant selectTenantByCode(String tenantCode);
/**
* 校验租户编码是否唯一
*
* @param tenantCode 租户编码
* @return 结果
*/
public int checkTenantCodeUnique(String tenantCode);
/**
* 新增租户信息
*
* @param tenant 租户信息
* @return 结果
*/
public int insertTenant(SysTenant tenant);
/**
* 修改租户信息
*
* @param tenant 租户信息
* @return 结果
*/
public int updateTenant(SysTenant tenant);
/**
* 删除租户管理信息
*
* @param tenantId 租户ID
* @return 结果
*/
public int deleteTenantById(Long tenantId);
/**
* 根据用户ID查询租户信息
*
* @param userId 用户ID
* @return 租户信息
*/
public SysTenant selectTenantByUserId(Long userId);
}

View File

@ -37,6 +37,14 @@ public interface UserEnterpriseVerificationMapper
*/
public UserEnterpriseVerification selectVerificationByUserId(Long userId);
/**
* 根据用户ID查询企业认证状态轻量级查询用于权限检查
*
* @param userId 用户ID
* @return 认证状态字符串
*/
public String selectVerificationStatusByUserId(Long userId);
/**
* 根据 sys_user_company.company_id 查询企业认证信息
*

View File

@ -35,6 +35,14 @@ public interface UserIdentityVerificationMapper
*/
public UserIdentityVerification selectVerificationByUserId(Long userId);
/**
* 根据用户ID查询身份认证状态轻量级查询用于权限检查
*
* @param userId 用户ID
* @return 认证状态字符串
*/
public String selectVerificationStatusByUserId(Long userId);
/**
* 新增用户身份认证
*

View File

@ -0,0 +1,83 @@
package com.ruoyi.system.service;
import java.util.List;
import com.ruoyi.common.core.domain.entity.SysTenant;
/**
* 租户管理 服务层
*
* @author ruoyi
*/
public interface ISysTenantService
{
/**
* 查询租户管理数据
*
* @param tenant 租户信息
* @return 租户信息集合
*/
public List<SysTenant> selectTenantList(SysTenant tenant);
/**
* 根据租户ID查询信息
*
* @param tenantId 租户ID
* @return 租户信息
*/
public SysTenant selectTenantById(Long tenantId);
/**
* 根据租户编码查询信息
*
* @param tenantCode 租户编码
* @return 租户信息
*/
public SysTenant selectTenantByCode(String tenantCode);
/**
* 根据用户ID查询租户信息
*
* @param userId 用户ID
* @return 租户信息
*/
public SysTenant selectTenantByUserId(Long userId);
/**
* 校验租户编码是否唯一
*
* @param tenant 租户信息
* @return 结果
*/
public boolean checkTenantCodeUnique(SysTenant tenant);
/**
* 新增保存租户信息
*
* @param tenant 租户信息
* @return 结果
*/
public int insertTenant(SysTenant tenant);
/**
* 修改保存租户信息
*
* @param tenant 租户信息
* @return 结果
*/
public int updateTenant(SysTenant tenant);
/**
* 删除租户管理信息
*
* @param tenantId 租户ID
* @return 结果
*/
public int deleteTenantById(Long tenantId);
/**
* 校验租户是否允许删除
*
* @param tenantId 租户ID
*/
public void checkTenantCanDelete(Long tenantId);
}

View File

@ -78,10 +78,19 @@ public interface IUserVerificationService
* 查询身份认证列表
*
* @param userId 用户ID可选
* @param realName 真实姓名可选
* @param verificationStatus 认证状态可选
* @return 身份认证集合
*/
public List<com.ruoyi.system.domain.UserIdentityVerification> listIdentityVerifications(Long userId, String verificationStatus);
public List<com.ruoyi.system.domain.UserIdentityVerification> listIdentityVerifications(Long userId, String realName, String verificationStatus);
/**
* 查询身份认证详情含审核历史
*
* @param verificationId 认证ID
* @return 身份认证详情
*/
public com.ruoyi.system.domain.UserIdentityVerification getIdentityVerificationDetail(Long verificationId);
// ==================== 认证状态查询方法 ====================

View File

@ -0,0 +1,150 @@
package com.ruoyi.system.service.impl;
import java.util.List;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import com.ruoyi.common.core.domain.entity.SysTenant;
import com.ruoyi.common.exception.ServiceException;
import com.ruoyi.system.mapper.SysTenantMapper;
import com.ruoyi.system.service.ISysTenantService;
/**
* 租户管理 服务实现
*
* @author ruoyi
*/
@Service
public class SysTenantServiceImpl implements ISysTenantService
{
@Autowired
private SysTenantMapper tenantMapper;
/**
* 查询租户管理数据
*
* @param tenant 租户信息
* @return 租户信息集合
*/
@Override
public List<SysTenant> selectTenantList(SysTenant tenant)
{
return tenantMapper.selectTenantList(tenant);
}
/**
* 根据租户ID查询信息
*
* @param tenantId 租户ID
* @return 租户信息
*/
@Override
public SysTenant selectTenantById(Long tenantId)
{
return tenantMapper.selectTenantById(tenantId);
}
/**
* 根据租户编码查询信息
*
* @param tenantCode 租户编码
* @return 租户信息
*/
@Override
public SysTenant selectTenantByCode(String tenantCode)
{
return tenantMapper.selectTenantByCode(tenantCode);
}
/**
* 根据用户ID查询租户信息
*
* @param userId 用户ID
* @return 租户信息
*/
@Override
public SysTenant selectTenantByUserId(Long userId)
{
return tenantMapper.selectTenantByUserId(userId);
}
/**
* 校验租户编码是否唯一
*
* @param tenant 租户信息
* @return 结果
*/
@Override
public boolean checkTenantCodeUnique(SysTenant tenant)
{
Long tenantId = tenant.getTenantId() == null ? -1L : tenant.getTenantId();
SysTenant info = tenantMapper.selectTenantByCode(tenant.getTenantCode());
if (info != null && info.getTenantId().longValue() != tenantId.longValue())
{
return false;
}
return true;
}
/**
* 新增保存租户信息
*
* @param tenant 租户信息
* @return 结果
*/
@Override
public int insertTenant(SysTenant tenant)
{
// 校验租户编码唯一性
if (!checkTenantCodeUnique(tenant))
{
throw new ServiceException("租户编码已存在");
}
return tenantMapper.insertTenant(tenant);
}
/**
* 修改保存租户信息
*
* @param tenant 租户信息
* @return 结果
*/
@Override
public int updateTenant(SysTenant tenant)
{
// 校验租户编码唯一性
if (!checkTenantCodeUnique(tenant))
{
throw new ServiceException("租户编码已存在");
}
return tenantMapper.updateTenant(tenant);
}
/**
* 删除租户管理信息
*
* @param tenantId 租户ID
* @return 结果
*/
@Override
public int deleteTenantById(Long tenantId)
{
// 校验租户是否允许删除
checkTenantCanDelete(tenantId);
return tenantMapper.deleteTenantById(tenantId);
}
/**
* 校验租户是否允许删除
*
* @param tenantId 租户ID
*/
@Override
public void checkTenantCanDelete(Long tenantId)
{
// 默认租户不允许删除
if (tenantId != null && tenantId == 1L)
{
throw new ServiceException("默认租户不允许删除");
}
}
}

View File

@ -2,6 +2,7 @@ package com.ruoyi.system.service.impl;
import java.util.Date;
import java.util.List;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;
import org.slf4j.Logger;
@ -12,9 +13,13 @@ import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import com.ruoyi.common.config.EncryptionConfig;
import com.ruoyi.common.constant.CacheConstants;
import com.ruoyi.common.constant.VerificationPermissions;
import com.ruoyi.common.core.domain.AjaxResult;
import com.ruoyi.common.core.domain.entity.SysRole;
import com.ruoyi.common.core.domain.model.LoginUser;
import com.ruoyi.common.core.redis.RedisCache;
import com.ruoyi.common.exception.ServiceException;
import com.ruoyi.common.utils.EncryptionUtil;
import com.ruoyi.common.utils.IdCardValidator;
import com.ruoyi.common.utils.SecurityUtils;
@ -56,6 +61,9 @@ public class UserVerificationServiceImpl implements IUserVerificationService
@Autowired
private VerificationNotificationService notificationService;
@Autowired
private RedisCache redisCache;
/**
* 提交企业法人CA认证申请
*
@ -170,11 +178,15 @@ public class UserVerificationServiceImpl implements IUserVerificationService
if ("APPROVED".equals(verification.getVerificationStatus()))
{
notificationService.sendEnterpriseApprovalNotification(verification);
// 清除用户认证状态缓存
evictVerificationCache(request.getUserId());
return AjaxResult.success("法人CA认证成功企业认证已自动完成");
}
else
{
notificationService.sendEnterpriseRejectionNotification(verification);
// 清除用户认证状态缓存
evictVerificationCache(request.getUserId());
return AjaxResult.error("法人CA认证失败: " + caResponse.getRejectReason());
}
}
@ -222,12 +234,27 @@ public class UserVerificationServiceImpl implements IUserVerificationService
@Override
public AjaxResult getEnterpriseVerificationStatus(Long userId)
{
// 检查数据访问权限用户只能查询自己的认证状态
checkDataAccessPermission(userId);
logSensitiveDataAccess("查询企业认证状态", userId);
// 尝试从缓存获取
String cacheKey = CacheConstants.ENTERPRISE_VERIFICATION_KEY + userId;
UserEnterpriseVerification cached = redisCache.getCacheObject(cacheKey);
if (cached != null)
{
return AjaxResult.success(cached);
}
UserEnterpriseVerification verification = enterpriseVerificationMapper.selectVerificationByUserId(userId);
if (verification == null)
{
return AjaxResult.success("未认证", null);
}
// 缓存结果
redisCache.setCacheObject(cacheKey, verification, CacheConstants.VERIFICATION_STATUS_EXPIRE_MINUTES, TimeUnit.MINUTES);
return AjaxResult.success(verification);
}
@ -277,6 +304,69 @@ public class UserVerificationServiceImpl implements IUserVerificationService
return auditLogMapper.selectAuditLogByVerificationId(verificationType, verificationId);
}
/**
* 检查数据访问权限用户只能访问自己的认证数据管理员可访问所有数据
*
* @param targetUserId 目标用户ID
* @throws ServiceException 权限不足时抛出异常
*/
private void checkDataAccessPermission(Long targetUserId)
{
try
{
LoginUser loginUser = SecurityUtils.getLoginUser();
Long currentUserId = loginUser.getUserId();
// 超级管理员可访问所有数据
if (SecurityUtils.isAdmin(currentUserId))
{
return;
}
// 具有查看所有认证信息权限的用户可访问所有数据
if (SecurityUtils.hasPermi(VerificationPermissions.VIEW_ALL_VERIFICATION)
|| SecurityUtils.hasPermi(VerificationPermissions.MANAGE_VERIFICATION))
{
log.info("管理员用户ID: {} 访问用户ID: {} 的认证数据", currentUserId, targetUserId);
return;
}
// 普通用户只能访问自己的数据
if (!currentUserId.equals(targetUserId))
{
log.warn("用户ID: {} 尝试访问用户ID: {} 的认证数据,权限不足", currentUserId, targetUserId);
throw new ServiceException("无权访问其他用户的认证数据");
}
}
catch (ServiceException e)
{
throw e;
}
catch (Exception e)
{
log.error("检查数据访问权限时发生异常", e);
}
}
/**
* 记录敏感数据访问日志
*
* @param operation 操作描述
* @param targetUserId 目标用户ID
*/
private void logSensitiveDataAccess(String operation, Long targetUserId)
{
try
{
Long currentUserId = SecurityUtils.getUserId();
log.info("[敏感数据访问] 操作: {}, 操作人ID: {}, 目标用户ID: {}", operation, currentUserId, targetUserId);
}
catch (Exception e)
{
// 日志记录失败不影响主流程
}
}
/**
* 记录审核日志
*
@ -427,11 +517,15 @@ public class UserVerificationServiceImpl implements IUserVerificationService
if ("APPROVED".equals(verification.getVerificationStatus()))
{
notificationService.sendIdentityApprovalNotification(verification);
// 清除用户认证状态缓存
evictVerificationCache(userId);
return AjaxResult.success("身份认证成功");
}
else
{
notificationService.sendIdentityRejectionNotification(verification);
// 清除用户认证状态缓存
evictVerificationCache(userId);
return AjaxResult.error("身份认证失败: " + verification.getRejectReason());
}
}
@ -476,6 +570,18 @@ public class UserVerificationServiceImpl implements IUserVerificationService
@Override
public AjaxResult getIdentityVerificationStatus(Long userId)
{
// 检查数据访问权限用户只能查询自己的认证状态
checkDataAccessPermission(userId);
logSensitiveDataAccess("查询身份认证状态", userId);
// 尝试从缓存获取不含身份证号
String cacheKey = CacheConstants.IDENTITY_VERIFICATION_KEY + userId;
UserIdentityVerification cached = redisCache.getCacheObject(cacheKey);
if (cached != null)
{
return AjaxResult.success(cached);
}
UserIdentityVerification verification = identityVerificationMapper.selectVerificationByUserId(userId);
if (verification == null)
@ -485,6 +591,9 @@ public class UserVerificationServiceImpl implements IUserVerificationService
// 不返回加密的身份证号
verification.setIdCardNumber(null);
// 缓存结果
redisCache.setCacheObject(cacheKey, verification, CacheConstants.VERIFICATION_STATUS_EXPIRE_MINUTES, TimeUnit.MINUTES);
return AjaxResult.success(verification);
}
@ -493,14 +602,34 @@ public class UserVerificationServiceImpl implements IUserVerificationService
* 查询身份认证列表
*
* @param userId 用户ID可选
* @param realName 真实姓名可选
* @param verificationStatus 认证状态可选
* @return 身份认证集合
*/
@Override
public List<UserIdentityVerification> listIdentityVerifications(Long userId, String verificationStatus)
public List<UserIdentityVerification> listIdentityVerifications(Long userId, String realName, String verificationStatus)
{
// 如果指定了userId检查数据访问权限
if (userId != null)
{
checkDataAccessPermission(userId);
}
else
{
// 查询所有用户数据需要管理员权限
if (!SecurityUtils.isAdmin(SecurityUtils.getUserId())
&& !SecurityUtils.hasPermi(VerificationPermissions.VIEW_ALL_VERIFICATION)
&& !SecurityUtils.hasPermi(VerificationPermissions.MANAGE_VERIFICATION))
{
// 普通用户只能查询自己的数据
userId = SecurityUtils.getUserId();
}
}
logSensitiveDataAccess("查询身份认证列表", userId);
UserIdentityVerification query = new UserIdentityVerification();
query.setUserId(userId);
query.setRealName(realName);
query.setVerificationStatus(verificationStatus);
List<UserIdentityVerification> list = identityVerificationMapper.selectVerificationList(query);
@ -511,6 +640,28 @@ public class UserVerificationServiceImpl implements IUserVerificationService
return list;
}
/**
* 查询身份认证详情含审核历史
*
* @param verificationId 认证ID
* @return 身份认证详情
*/
@Override
public UserIdentityVerification getIdentityVerificationDetail(Long verificationId)
{
UserIdentityVerification verification = identityVerificationMapper.selectVerificationById(verificationId);
if (verification == null)
{
return null;
}
// 不返回加密的身份证号
verification.setIdCardNumber(null);
// 附加审核历史
List<VerificationAuditLog> auditLogs = auditLogMapper.selectAuditLogByVerificationId("IDENTITY", verificationId);
verification.setAuditLogs(auditLogs);
return verification;
}
// ==================== 认证状态查询方法实现 ====================
/**
@ -528,20 +679,31 @@ public class UserVerificationServiceImpl implements IUserVerificationService
return true;
}
// 查询企业认证状态
UserEnterpriseVerification enterpriseVerification = enterpriseVerificationMapper.selectVerificationByUserId(userId);
if (enterpriseVerification == null || !"APPROVED".equals(enterpriseVerification.getVerificationStatus()))
// 尝试从缓存获取认证状态
String cacheKey = CacheConstants.VERIFICATION_STATUS_KEY + userId;
Boolean cachedStatus = redisCache.getCacheObject(cacheKey);
if (cachedStatus != null)
{
return cachedStatus;
}
// 查询企业认证状态使用轻量级状态查询
String enterpriseStatus = enterpriseVerificationMapper.selectVerificationStatusByUserId(userId);
if (!"APPROVED".equals(enterpriseStatus))
{
redisCache.setCacheObject(cacheKey, Boolean.FALSE, CacheConstants.VERIFICATION_STATUS_EXPIRE_MINUTES, TimeUnit.MINUTES);
return false;
}
// 查询身份认证状态
UserIdentityVerification identityVerification = identityVerificationMapper.selectVerificationByUserId(userId);
if (identityVerification == null || !"APPROVED".equals(identityVerification.getVerificationStatus()))
// 查询身份认证状态使用轻量级状态查询
String identityStatus = identityVerificationMapper.selectVerificationStatusByUserId(userId);
if (!"APPROVED".equals(identityStatus))
{
redisCache.setCacheObject(cacheKey, Boolean.FALSE, CacheConstants.VERIFICATION_STATUS_EXPIRE_MINUTES, TimeUnit.MINUTES);
return false;
}
redisCache.setCacheObject(cacheKey, Boolean.TRUE, CacheConstants.VERIFICATION_STATUS_EXPIRE_MINUTES, TimeUnit.MINUTES);
return true;
}
@ -557,6 +719,14 @@ public class UserVerificationServiceImpl implements IUserVerificationService
// 检查是否为超级管理员
boolean isSuperAdmin = (userId != null && userId == 1L);
// 尝试从缓存获取完整状态DTO
String cacheKey = CacheConstants.VERIFICATION_STATUS_KEY + "dto:" + userId;
com.ruoyi.system.domain.dto.VerificationStatusDTO cachedDto = redisCache.getCacheObject(cacheKey);
if (cachedDto != null)
{
return cachedDto;
}
// 查询企业认证状态
UserEnterpriseVerification enterpriseVerification = enterpriseVerificationMapper.selectVerificationByUserId(userId);
String enterpriseStatus = enterpriseVerification != null ? enterpriseVerification.getVerificationStatus() : "PENDING";
@ -569,12 +739,35 @@ public class UserVerificationServiceImpl implements IUserVerificationService
boolean fullyVerified = isSuperAdmin ||
("APPROVED".equals(enterpriseStatus) && "APPROVED".equals(identityStatus));
return new com.ruoyi.system.domain.dto.VerificationStatusDTO(
com.ruoyi.system.domain.dto.VerificationStatusDTO dto = new com.ruoyi.system.domain.dto.VerificationStatusDTO(
userId,
enterpriseStatus,
identityStatus,
fullyVerified,
isSuperAdmin
);
// 缓存结果
redisCache.setCacheObject(cacheKey, dto, CacheConstants.VERIFICATION_STATUS_EXPIRE_MINUTES, TimeUnit.MINUTES);
return dto;
}
/**
* 清除用户认证状态相关缓存
*
* @param userId 用户ID
*/
private void evictVerificationCache(Long userId)
{
if (userId == null)
{
return;
}
redisCache.deleteObject(CacheConstants.VERIFICATION_STATUS_KEY + userId);
redisCache.deleteObject(CacheConstants.VERIFICATION_STATUS_KEY + "dto:" + userId);
redisCache.deleteObject(CacheConstants.ENTERPRISE_VERIFICATION_KEY + userId);
redisCache.deleteObject(CacheConstants.IDENTITY_VERIFICATION_KEY + userId);
log.debug("已清除用户认证状态缓存: userId={}", userId);
}
}

View File

@ -0,0 +1,145 @@
<?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.ruoyi.system.mapper.SysTenantMapper">
<resultMap type="SysTenant" id="SysTenantResult">
<id property="tenantId" column="tenant_id" />
<result property="tenantCode" column="tenant_code" />
<result property="tenantName" column="tenant_name" />
<result property="tenantType" column="tenant_type" />
<result property="companyType" column="company_type" />
<result property="contactPerson" column="contact_person" />
<result property="contactPhone" column="contact_phone" />
<result property="contactEmail" column="contact_email" />
<result property="domain" column="domain" />
<result property="logoUrl" column="logo_url" />
<result property="status" column="status" />
<result property="maxUsers" column="max_users" />
<result property="maxCompanies" column="max_companies" />
<result property="expireDate" column="expire_date" />
<result property="packageId" column="package_id" />
<result property="createBy" column="create_by" />
<result property="createTime" column="create_time" />
<result property="updateBy" column="update_by" />
<result property="updateTime" column="update_time" />
<result property="remark" column="remark" />
</resultMap>
<sql id="selectTenantVo">
select tenant_id, tenant_code, tenant_name, tenant_type, company_type, contact_person, contact_phone, contact_email,
domain, logo_url, status, max_users, max_companies, expire_date, package_id,
create_by, create_time, update_by, update_time, remark
from sys_tenant
</sql>
<select id="selectTenantList" parameterType="SysTenant" resultMap="SysTenantResult">
<include refid="selectTenantVo"/>
<where>
<if test="tenantCode != null and tenantCode != ''">
AND tenant_code like concat('%', #{tenantCode}, '%')
</if>
<if test="tenantName != null and tenantName != ''">
AND tenant_name like concat('%', #{tenantName}, '%')
</if>
<if test="tenantType != null and tenantType != ''">
AND tenant_type = #{tenantType}
</if>
<if test="companyType != null and companyType != ''">
AND company_type = #{companyType}
</if>
<if test="status != null and status != ''">
AND status = #{status}
</if>
</where>
order by tenant_id
</select>
<select id="selectTenantById" parameterType="Long" resultMap="SysTenantResult">
<include refid="selectTenantVo"/>
where tenant_id = #{tenantId}
</select>
<select id="selectTenantByCode" parameterType="String" resultMap="SysTenantResult">
<include refid="selectTenantVo"/>
where tenant_code = #{tenantCode}
</select>
<select id="checkTenantCodeUnique" parameterType="String" resultType="int">
select count(1) from sys_tenant where tenant_code = #{tenantCode}
</select>
<select id="selectTenantByUserId" parameterType="Long" resultMap="SysTenantResult">
<include refid="selectTenantVo"/>
where tenant_id = (select tenant_id from sys_user where user_id = #{userId} limit 1)
</select>
<insert id="insertTenant" parameterType="SysTenant" useGeneratedKeys="true" keyProperty="tenantId">
insert into sys_tenant
<trim prefix="(" suffix=")" suffixOverrides=",">
<if test="tenantCode != null and tenantCode != ''">tenant_code,</if>
<if test="tenantName != null and tenantName != ''">tenant_name,</if>
<if test="tenantType != null">tenant_type,</if>
<if test="companyType != null">company_type,</if>
<if test="contactPerson != null">contact_person,</if>
<if test="contactPhone != null">contact_phone,</if>
<if test="contactEmail != null">contact_email,</if>
<if test="domain != null">domain,</if>
<if test="logoUrl != null">logo_url,</if>
<if test="status != null">status,</if>
<if test="maxUsers != null">max_users,</if>
<if test="maxCompanies != null">max_companies,</if>
<if test="expireDate != null">expire_date,</if>
<if test="packageId != null">package_id,</if>
<if test="createBy != null">create_by,</if>
<if test="remark != null">remark,</if>
</trim>
<trim prefix="values (" suffix=")" suffixOverrides=",">
<if test="tenantCode != null and tenantCode != ''">#{tenantCode},</if>
<if test="tenantName != null and tenantName != ''">#{tenantName},</if>
<if test="tenantType != null">#{tenantType},</if>
<if test="companyType != null">#{companyType},</if>
<if test="contactPerson != null">#{contactPerson},</if>
<if test="contactPhone != null">#{contactPhone},</if>
<if test="contactEmail != null">#{contactEmail},</if>
<if test="domain != null">#{domain},</if>
<if test="logoUrl != null">#{logoUrl},</if>
<if test="status != null">#{status},</if>
<if test="maxUsers != null">#{maxUsers},</if>
<if test="maxCompanies != null">#{maxCompanies},</if>
<if test="expireDate != null">#{expireDate},</if>
<if test="packageId != null">#{packageId},</if>
<if test="createBy != null">#{createBy},</if>
<if test="remark != null">#{remark},</if>
</trim>
</insert>
<update id="updateTenant" parameterType="SysTenant">
update sys_tenant
<trim prefix="SET" suffixOverrides=",">
<if test="tenantCode != null and tenantCode != ''">tenant_code = #{tenantCode},</if>
<if test="tenantName != null and tenantName != ''">tenant_name = #{tenantName},</if>
<if test="tenantType != null">tenant_type = #{tenantType},</if>
<if test="companyType != null">company_type = #{companyType},</if>
<if test="contactPerson != null">contact_person = #{contactPerson},</if>
<if test="contactPhone != null">contact_phone = #{contactPhone},</if>
<if test="contactEmail != null">contact_email = #{contactEmail},</if>
<if test="domain != null">domain = #{domain},</if>
<if test="logoUrl != null">logo_url = #{logoUrl},</if>
<if test="status != null">status = #{status},</if>
<if test="maxUsers != null">max_users = #{maxUsers},</if>
<if test="maxCompanies != null">max_companies = #{maxCompanies},</if>
<if test="expireDate != null">expire_date = #{expireDate},</if>
<if test="packageId != null">package_id = #{packageId},</if>
<if test="updateBy != null">update_by = #{updateBy},</if>
<if test="remark != null">remark = #{remark},</if>
</trim>
where tenant_id = #{tenantId}
</update>
<delete id="deleteTenantById" parameterType="Long">
update sys_tenant set status = 'DELETED' where tenant_id = #{tenantId}
</delete>
</mapper>

View File

@ -68,6 +68,15 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
limit 1
</select>
<!-- 仅查询认证状态(轻量级查询,用于权限检查) -->
<select id="selectVerificationStatusByUserId" parameterType="Long" resultType="String">
select verification_status
from sys_user_enterprise_verification
where user_id = #{userId}
order by create_time desc
limit 1
</select>
<select id="selectApprovedVerifications" resultMap="UserEnterpriseVerificationResult">
<include refid="selectVerificationVo"/>
where verification_status = 'APPROVED'

View File

@ -7,6 +7,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
<resultMap type="UserIdentityVerification" id="UserIdentityVerificationResult">
<id property="verificationId" column="verification_id" />
<result property="userId" column="user_id" />
<result property="userName" column="user_name" />
<result property="realName" column="real_name" />
<result property="idCardNumber" column="id_card_number" />
<result property="verificationStatus" column="verification_status" />
@ -20,44 +21,54 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
</resultMap>
<sql id="selectVerificationVo">
select verification_id, user_id, real_name, id_card_number, verification_status,
ca_verification_id, reject_reason, verification_time,
create_by, create_time, update_by, update_time
from sys_user_identity_verification
select v.verification_id, v.user_id, u.user_name, v.real_name, v.id_card_number,
v.verification_status, v.ca_verification_id, v.reject_reason, v.verification_time,
v.create_by, v.create_time, v.update_by, v.update_time
from sys_user_identity_verification v
left join sys_user u on u.user_id = v.user_id
</sql>
<select id="selectVerificationList" parameterType="UserIdentityVerification" resultMap="UserIdentityVerificationResult">
<include refid="selectVerificationVo"/>
<where>
<if test="userId != null">
AND user_id = #{userId}
AND v.user_id = #{userId}
</if>
<if test="realName != null and realName != ''">
AND real_name like concat('%', #{realName}, '%')
AND v.real_name like concat('%', #{realName}, '%')
</if>
<if test="verificationStatus != null and verificationStatus != ''">
AND verification_status = #{verificationStatus}
AND v.verification_status = #{verificationStatus}
</if>
<if test="caVerificationId != null and caVerificationId != ''">
AND ca_verification_id = #{caVerificationId}
AND v.ca_verification_id = #{caVerificationId}
</if>
<if test="params.beginTime != null and params.beginTime != ''">
AND date_format(create_time,'%Y%m%d') &gt;= date_format(#{params.beginTime},'%Y%m%d')
AND date_format(v.create_time,'%Y%m%d') &gt;= date_format(#{params.beginTime},'%Y%m%d')
</if>
<if test="params.endTime != null and params.endTime != ''">
AND date_format(create_time,'%Y%m%d') &lt;= date_format(#{params.endTime},'%Y%m%d')
AND date_format(v.create_time,'%Y%m%d') &lt;= date_format(#{params.endTime},'%Y%m%d')
</if>
</where>
order by create_time desc
order by v.create_time desc
</select>
<select id="selectVerificationById" parameterType="Long" resultMap="UserIdentityVerificationResult">
<include refid="selectVerificationVo"/>
where verification_id = #{verificationId}
where v.verification_id = #{verificationId}
</select>
<select id="selectVerificationByUserId" parameterType="Long" resultMap="UserIdentityVerificationResult">
<include refid="selectVerificationVo"/>
where v.user_id = #{userId}
order by v.create_time desc
limit 1
</select>
<!-- 仅查询认证状态(轻量级查询,用于权限检查) -->
<select id="selectVerificationStatusByUserId" parameterType="Long" resultType="String">
select verification_status
from sys_user_identity_verification
where user_id = #{userId}
order by create_time desc
limit 1

View File

@ -3850,6 +3850,11 @@ INSERT INTO `sys_menu` VALUES (6134, '关系删除', 6130, 4, '', '', '', '', 1,
INSERT INTO `sys_menu` VALUES (6135, '关系导出', 6130, 5, '', '', '', '', 1, 0, 'F', '0', '0', 'credit:relationship:export', '#', 'admin', '2026-03-16 14:59:18', '', NULL, '');
INSERT INTO `sys_menu` VALUES (6155, '认证管理', 0, 5, 'verification', NULL, NULL, '', 1, 0, 'M', '0', '0', '', 'documentation', 'admin', '2026-03-05 18:23:25', 'admin', '2026-03-05 18:24:19', '认证管理目录');
INSERT INTO `sys_menu` VALUES (6156, '认证信息管理', 6155, 1, 'manage', 'system/verification/manage/index', NULL, '', 1, 0, 'C', '0', '0', 'system:verification:view:all', 'list', 'admin', '2026-03-05 18:23:25', '', NULL, '认证信息管理菜单');
INSERT INTO `sys_menu` VALUES (6157, '企业认证管理', 6155, 2, 'enterprise', 'system/verification/enterprise', NULL, '', 1, 0, 'C', '0', '0', 'system:verification:view:all', 'office', 'admin', '2026-03-05 18:23:25', '', NULL, '企业认证管理菜单');
INSERT INTO `sys_menu` VALUES (6158, '企业认证查询', 6157, 1, '', '', '', '', 1, 0, 'F', '0', '0', 'system:verification:view:all', '#', 'admin', '2026-03-05 18:23:25', '', NULL, '企业认证列表查询权限');
INSERT INTO `sys_menu` VALUES (6159, '企业认证详情', 6157, 2, '', '', '', '', 1, 0, 'F', '0', '0', 'system:verification:view:all', '#', 'admin', '2026-03-05 18:23:25', '', NULL, '企业认证详情查看权限');
INSERT INTO `sys_menu` VALUES (6160, '企业认证导出', 6157, 3, '', '', '', '', 1, 0, 'F', '0', '0', 'system:verification:export', '#', 'admin', '2026-03-05 18:23:25', '', NULL, '企业认证数据导出权限');
INSERT INTO `sys_menu` VALUES (6161, '认证管理权限', 6157, 4, '', '', '', '', 1, 0, 'F', '0', '0', 'system:verification:manage', '#', 'admin', '2026-03-05 18:23:25', '', NULL, '企业认证管理操作权限');
-- ----------------------------
-- Table structure for sys_notice

34
sql/tenant-menu.sql Normal file
View File

@ -0,0 +1,34 @@
-- =============================================
-- 租户管理菜单权限SQL
-- =============================================
-- ----------------------------
-- 1. 插入租户管理菜单
-- ----------------------------
-- 菜单 SQL
INSERT INTO sys_menu (menu_id, menu_name, parent_id, order_num, path, component, is_frame, is_cache, menu_type, visible, status, perms, icon, create_by, create_time, update_by, update_time, remark)
VALUES (2000, '租户管理', 0, 6, 'tenant', 'system/tenant/index', 1, 0, 'C', '0', '0', 'system:tenant:list', 'peoples', 'admin', sysdate(), '', '', '租户管理菜单');
-- 按钮 SQL
INSERT INTO sys_menu (menu_name, parent_id, order_num, perms, menu_type, visible, status, create_by, create_time)
VALUES ('租户查询', 2000, 1, 'system:tenant:query', 'F', '0', '0', 'admin', sysdate());
INSERT INTO sys_menu (menu_name, parent_id, order_num, perms, menu_type, visible, status, create_by, create_time)
VALUES ('租户新增', 2000, 2, 'system:tenant:add', 'F', '0', '0', 'admin', sysdate());
INSERT INTO sys_menu (menu_name, parent_id, order_num, perms, menu_type, visible, status, create_by, create_time)
VALUES ('租户修改', 2000, 3, 'system:tenant:edit', 'F', '0', '0', 'admin', sysdate());
INSERT INTO sys_menu (menu_name, parent_id, order_num, perms, menu_type, visible, status, create_by, create_time)
VALUES ('租户删除', 2000, 4, 'system:tenant:remove', 'F', '0', '0', 'admin', sysdate());
-- ----------------------------
-- 2. 为超级管理员角色分配租户管理权限
-- ----------------------------
INSERT INTO sys_role_menu (role_id, menu_id)
SELECT 1, menu_id FROM sys_menu WHERE perms IN ('system:tenant:list', 'system:tenant:query', 'system:tenant:add', 'system:tenant:edit', 'system:tenant:remove');
-- ----------------------------
-- 3. 查看菜单是否插入成功
-- ----------------------------
-- SELECT * FROM sys_menu WHERE menu_id = 2000 OR parent_id = 2000;

125
sql/tenant-migration.sql Normal file
View File

@ -0,0 +1,125 @@
-- =============================================
-- 租户相关数据库变更脚本
-- 用于若依框架 SAAS 化迁移
-- =============================================
-- ----------------------------
-- 1. 创建租户表
-- ----------------------------
DROP TABLE IF EXISTS `sys_tenant`;
CREATE TABLE `sys_tenant` (
`tenant_id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '租户ID',
`tenant_code` varchar(50) NOT NULL COMMENT '租户编码(唯一)',
`tenant_name` varchar(100) NOT NULL COMMENT '租户名称',
`tenant_type` varchar(20) NOT NULL DEFAULT 'COMPANY' COMMENT '租户类型: COMPANY-企业',
`company_type` varchar(20) NULL COMMENT '公司类型: BANK-银行, CLIENT-甲方, LABOR-劳务公司',
`contact_person` varchar(50) NULL COMMENT '联系人',
`contact_phone` varchar(20) NULL COMMENT '联系电话',
`contact_email` varchar(100) NULL COMMENT '联系邮箱',
`domain` varchar(100) NULL COMMENT '租户域名',
`logo_url` varchar(500) NULL COMMENT 'Logo URL',
`status` varchar(10) NOT NULL DEFAULT 'ACTIVE' COMMENT '状态: ACTIVE-正常, FROZEN-冻结, DELETED-已删除',
`max_users` int(11) NULL DEFAULT 10 COMMENT '最大用户数',
`max_companies` int(11) NULL DEFAULT 5 COMMENT '最大公司数',
`expire_date` date NULL COMMENT '过期日期',
`package_id` bigint(20) NULL COMMENT '套餐ID',
`create_by` varchar(64) NULL DEFAULT '' COMMENT '创建者',
`create_time` datetime NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`update_by` varchar(64) NULL DEFAULT '' COMMENT '更新者',
`update_time` datetime NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
`remark` varchar(500) NULL COMMENT '备注',
PRIMARY KEY (`tenant_id`),
UNIQUE INDEX `uk_tenant_code`(`tenant_code` ASC),
INDEX `idx_status`(`status` ASC),
INDEX `idx_company_type`(`company_type` ASC)
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8mb4 COMMENT='租户表';
-- ----------------------------
-- 2. 初始化默认租户 (兼容现有数据)
-- ----------------------------
INSERT INTO `sys_tenant` (`tenant_id`, `tenant_code`, `tenant_name`, `tenant_type`, `company_type`, `status`, `max_users`, `max_companies`, `remark`)
VALUES (1, 'DEFAULT', '默认租户', 'COMPANY', NULL, 'ACTIVE', 1000, 100, '系统默认租户,兼容历史数据');
-- ----------------------------
-- 3. 为 sys_user 表添加 tenant_id 字段
-- ----------------------------
ALTER TABLE `sys_user` ADD COLUMN `tenant_id` bigint(20) NULL DEFAULT 1 COMMENT '租户ID' AFTER `del_flag`;
ALTER TABLE `sys_user` ADD INDEX `idx_user_tenant_id`(`tenant_id` ASC);
-- ----------------------------
-- 4. 为 sys_dept 表添加 tenant_id 字段
-- ----------------------------
ALTER TABLE `sys_dept` ADD COLUMN `tenant_id` bigint(20) NULL DEFAULT 1 COMMENT '租户ID' AFTER `dept_id`;
ALTER TABLE `sys_dept` ADD INDEX `idx_dept_tenant_id`(`tenant_id` ASC);
-- ----------------------------
-- 5. 为 dc_contract 表添加 tenant_id 字段
-- ----------------------------
ALTER TABLE `dc_contract` ADD COLUMN `tenant_id` bigint(20) NULL DEFAULT 1 COMMENT '租户ID' AFTER `contract_id`;
ALTER TABLE `dc_contract` ADD INDEX `idx_contract_tenant_id`(`tenant_id` ASC);
-- ----------------------------
-- 6. 为 dc_service_contract 表添加 tenant_id 字段
-- ----------------------------
ALTER TABLE `dc_service_contract` ADD COLUMN `tenant_id` bigint(20) NULL DEFAULT 1 COMMENT '租户ID' AFTER `service_contract_id`;
ALTER TABLE `dc_service_contract` ADD INDEX `idx_service_contract_tenant_id`(`tenant_id` ASC);
-- ----------------------------
-- 7. 为 dc_employee_info 表添加 tenant_id 字段
-- ----------------------------
ALTER TABLE `dc_employee_info` ADD COLUMN `tenant_id` bigint(20) NULL DEFAULT 1 COMMENT '租户ID' AFTER `employee_id`;
ALTER TABLE `dc_employee_info` ADD INDEX `idx_employee_tenant_id`(`tenant_id` ASC);
-- ----------------------------
-- 8. 为 dc_employee_library 表添加 tenant_id 字段
-- ----------------------------
ALTER TABLE `dc_employee_library` ADD COLUMN `tenant_id` bigint(20) NULL DEFAULT 1 COMMENT '租户ID' AFTER `library_id`;
ALTER TABLE `dc_employee_library` ADD INDEX `idx_library_tenant_id`(`tenant_id` ASC);
-- ----------------------------
-- 9. 为 dc_credit 表添加 tenant_id 字段
-- ----------------------------
ALTER TABLE `dc_credit` ADD COLUMN `tenant_id` bigint(20) NULL DEFAULT 1 COMMENT '租户ID' AFTER `credit_id`;
ALTER TABLE `dc_credit` ADD INDEX `idx_credit_tenant_id`(`tenant_id` ASC);
-- ----------------------------
-- 10. 为 dc_financing 表添加 tenant_id 字段
-- ----------------------------
ALTER TABLE `dc_financing` ADD COLUMN `tenant_id` bigint(20) NULL DEFAULT 1 COMMENT '租户ID' AFTER `financing_id`;
ALTER TABLE `dc_financing` ADD INDEX `idx_financing_tenant_id`(`tenant_id` ASC);
-- ----------------------------
-- 11. 为 dc_company_relationship 表添加 tenant_id 字段
-- ----------------------------
ALTER TABLE `dc_company_relationship` ADD COLUMN `tenant_id` bigint(20) NULL DEFAULT 1 COMMENT '租户ID' AFTER `relationship_id`;
ALTER TABLE `dc_company_relationship` ADD INDEX `idx_relationship_tenant_id`(`tenant_id` ASC);
-- ----------------------------
-- 12. 为 dc_bank_institution 表添加 tenant_id 字段
-- ----------------------------
ALTER TABLE `dc_bank_institution` ADD COLUMN `tenant_id` bigint(20) NULL DEFAULT 1 COMMENT '租户ID' AFTER `bank_id`;
ALTER TABLE `dc_bank_institution` ADD INDEX `idx_bank_tenant_id`(`tenant_id` ASC);
-- ----------------------------
-- 13. 为 dc_service_period 表添加 tenant_id 字段
-- ----------------------------
ALTER TABLE `dc_service_period` ADD COLUMN `tenant_id` bigint(20) NULL DEFAULT 1 COMMENT '租户ID' AFTER `service_period_id`;
ALTER TABLE `dc_service_period` ADD INDEX `idx_service_period_tenant_id`(`tenant_id` ASC);
-- ----------------------------
-- 14. 为 dc_service_period_loan 表添加 tenant_id 字段
-- ----------------------------
ALTER TABLE `dc_service_period_loan` ADD COLUMN `tenant_id` bigint(20) NULL DEFAULT 1 COMMENT '租户ID' AFTER `link_id`;
ALTER TABLE `dc_service_period_loan` ADD INDEX `idx_loan_tenant_id`(`tenant_id` ASC);
-- ----------------------------
-- 15. 更新现有数据的 tenant_id (根据 company_id 关联)
-- ----------------------------
-- 注意:此脚本需要在确认现有公司数据后再执行
-- 以下为示例逻辑,实际执行时需要根据具体业务逻辑调整
-- 更新用户的 tenant_id 为 1 (默认租户)
UPDATE sys_user SET tenant_id = 1 WHERE tenant_id IS NULL;
-- 更新部门的 tenant_id 为 1 (默认租户)
UPDATE sys_dept SET tenant_id = 1 WHERE tenant_id IS NULL;