feat(tenant):实现租户套餐功能及多租户架构完善
1. 多租户架构完善
- TenantSqlInterceptor支持INSERT自动填充tenant_id
- 修复isTenantTable()使用TenantConfig.needTenantFilter()
- 唯一索引改造为租户内唯一(tenant_id + column)
2. 租户套餐功能
- 新增sys_tenant_package、sys_package_menu表
- 实现套餐CRUD及菜单关联管理
- SysMenuService实现套餐菜单过滤(角色菜单∩套餐菜单)
- 租户管理页面增加套餐选择
- 新增套餐管理页面
pull/1126/head
parent
9a8ffdbb8f
commit
158e666ef2
|
|
@ -0,0 +1,22 @@
|
|||
# 开发文档索引
|
||||
|
||||
本目录包含项目的开发相关文档。
|
||||
|
||||
## 文档列表
|
||||
|
||||
### 架构设计文档
|
||||
- [TENANT_GUIDE.md](./TENANT_GUIDE.md) - 若依多租户架构实现指南
|
||||
- 多租户SaaS架构概述与方案对比
|
||||
- 核心组件实现原理(TenantContext、拦截器、SQL改写)
|
||||
- 快速集成指南(依赖配置、代码集成、数据库改造)
|
||||
- 使用指南与开发注意事项
|
||||
- 最佳实践与性能优化
|
||||
- 常见问题FAQ
|
||||
|
||||
## 文档规范
|
||||
|
||||
所有开发文档遵循以下规范:
|
||||
- 文档命名格式:`{分类}_{英文大写}.md`,单词用 `_` 隔开
|
||||
- 文档使用简体中文编写
|
||||
- 言简意赅,避免长篇大论
|
||||
- 及时更新本索引文件
|
||||
File diff suppressed because it is too large
Load Diff
|
|
@ -0,0 +1,101 @@
|
|||
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.validation.annotation.Validated;
|
||||
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.page.TableDataInfo;
|
||||
import com.ruoyi.common.enums.BusinessType;
|
||||
import com.ruoyi.system.domain.SysTenantPackage;
|
||||
import com.ruoyi.system.service.ISysTenantPackageService;
|
||||
|
||||
/**
|
||||
* 租户套餐Controller
|
||||
*
|
||||
* @author ruoyi
|
||||
* @date 2025-12-19
|
||||
*/
|
||||
@RestController
|
||||
@RequestMapping("/system/package")
|
||||
public class SysTenantPackageController extends BaseController
|
||||
{
|
||||
@Autowired
|
||||
private ISysTenantPackageService packageService;
|
||||
|
||||
/**
|
||||
* 查询租户套餐列表
|
||||
*/
|
||||
@PreAuthorize("@ss.hasPermi('system:package:list')")
|
||||
@GetMapping("/list")
|
||||
public TableDataInfo list(SysTenantPackage sysTenantPackage)
|
||||
{
|
||||
startPage();
|
||||
List<SysTenantPackage> list = packageService.selectPackageList(sysTenantPackage);
|
||||
return getDataTable(list);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取租户套餐详细信息
|
||||
*/
|
||||
@PreAuthorize("@ss.hasPermi('system:package:query')")
|
||||
@GetMapping(value = "/{packageId}")
|
||||
public AjaxResult getInfo(@PathVariable("packageId") Long packageId)
|
||||
{
|
||||
return success(packageService.selectPackageById(packageId));
|
||||
}
|
||||
|
||||
/**
|
||||
* 新增租户套餐
|
||||
*/
|
||||
@PreAuthorize("@ss.hasPermi('system:package:add')")
|
||||
@Log(title = "租户套餐", businessType = BusinessType.INSERT)
|
||||
@PostMapping
|
||||
public AjaxResult add(@Validated @RequestBody SysTenantPackage sysTenantPackage)
|
||||
{
|
||||
return toAjax(packageService.insertPackage(sysTenantPackage));
|
||||
}
|
||||
|
||||
/**
|
||||
* 修改租户套餐
|
||||
*/
|
||||
@PreAuthorize("@ss.hasPermi('system:package:edit')")
|
||||
@Log(title = "租户套餐", businessType = BusinessType.UPDATE)
|
||||
@PutMapping
|
||||
public AjaxResult edit(@Validated @RequestBody SysTenantPackage sysTenantPackage)
|
||||
{
|
||||
return toAjax(packageService.updatePackage(sysTenantPackage));
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除租户套餐
|
||||
*/
|
||||
@PreAuthorize("@ss.hasPermi('system:package:remove')")
|
||||
@Log(title = "租户套餐", businessType = BusinessType.DELETE)
|
||||
@DeleteMapping("/{packageIds}")
|
||||
public AjaxResult remove(@PathVariable Long[] packageIds)
|
||||
{
|
||||
return toAjax(packageService.deletePackageByIds(packageIds));
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询套餐关联的菜单ID列表
|
||||
*/
|
||||
@PreAuthorize("@ss.hasPermi('system:package:query')")
|
||||
@GetMapping(value = "/{packageId}/menus")
|
||||
public AjaxResult getPackageMenus(@PathVariable("packageId") Long packageId)
|
||||
{
|
||||
List<Long> menuIds = packageService.selectMenuIdsByPackageId(packageId);
|
||||
return success(menuIds);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,20 +1,26 @@
|
|||
package com.ruoyi.framework.interceptor;
|
||||
|
||||
import java.sql.Connection;
|
||||
import java.util.List;
|
||||
import java.util.Properties;
|
||||
|
||||
import com.ruoyi.common.exception.ServiceException;
|
||||
import net.sf.jsqlparser.expression.LongValue;
|
||||
import net.sf.jsqlparser.expression.operators.conditional.AndExpression;
|
||||
import net.sf.jsqlparser.expression.operators.relational.EqualsTo;
|
||||
import net.sf.jsqlparser.expression.operators.relational.ExpressionList;
|
||||
import net.sf.jsqlparser.expression.operators.relational.ItemsList;
|
||||
import net.sf.jsqlparser.expression.operators.relational.MultiExpressionList;
|
||||
import net.sf.jsqlparser.parser.CCJSqlParserUtil;
|
||||
import net.sf.jsqlparser.schema.Column;
|
||||
import net.sf.jsqlparser.schema.Table;
|
||||
import net.sf.jsqlparser.statement.Statement;
|
||||
import net.sf.jsqlparser.statement.delete.Delete;
|
||||
import net.sf.jsqlparser.statement.insert.Insert;
|
||||
import net.sf.jsqlparser.statement.select.FromItem;
|
||||
import net.sf.jsqlparser.statement.select.PlainSelect;
|
||||
import net.sf.jsqlparser.statement.select.Select;
|
||||
import net.sf.jsqlparser.statement.select.SubSelect;
|
||||
import net.sf.jsqlparser.statement.update.Update;
|
||||
import org.apache.ibatis.executor.statement.StatementHandler;
|
||||
import org.apache.ibatis.mapping.BoundSql;
|
||||
|
|
@ -114,10 +120,11 @@ public class TenantSqlInterceptor implements Interceptor {
|
|||
* @return true-需要拦截,false-不需要
|
||||
*/
|
||||
private boolean shouldIntercept(SqlCommandType sqlCommandType, String sql) {
|
||||
// 只拦截 SELECT、UPDATE、DELETE
|
||||
// 拦截 SELECT、UPDATE、DELETE、INSERT
|
||||
if (sqlCommandType != SqlCommandType.SELECT &&
|
||||
sqlCommandType != SqlCommandType.UPDATE &&
|
||||
sqlCommandType != SqlCommandType.DELETE) {
|
||||
sqlCommandType != SqlCommandType.DELETE &&
|
||||
sqlCommandType != SqlCommandType.INSERT) {
|
||||
return false;
|
||||
}
|
||||
|
||||
|
|
@ -152,6 +159,8 @@ public class TenantSqlInterceptor implements Interceptor {
|
|||
return ((Update) statement).getTable().getName();
|
||||
} else if (statement instanceof Delete) {
|
||||
return ((Delete) statement).getTable().getName();
|
||||
} else if (statement instanceof Insert) {
|
||||
return ((Insert) statement).getTable().getName();
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
|
@ -163,8 +172,7 @@ public class TenantSqlInterceptor implements Interceptor {
|
|||
* @return true-是租户表,false-不是
|
||||
*/
|
||||
private boolean isTenantTable(String tableName) {
|
||||
return TenantConfig.TENANT_TABLES.stream()
|
||||
.anyMatch(t -> t.equalsIgnoreCase(tableName));
|
||||
return TenantConfig.needTenantFilter(tableName);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -233,6 +241,10 @@ public class TenantSqlInterceptor implements Interceptor {
|
|||
|
||||
return deleteStatement.toString();
|
||||
}
|
||||
else if (statement instanceof Insert) {
|
||||
// 处理 INSERT 语句
|
||||
return processInsert((Insert) statement, tenantId);
|
||||
}
|
||||
|
||||
return sql;
|
||||
}
|
||||
|
|
@ -307,6 +319,63 @@ public class TenantSqlInterceptor implements Interceptor {
|
|||
return equalsTo;
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理INSERT语句 - 自动填充tenant_id
|
||||
*
|
||||
* Reason: 防止开发人员忘记手动设置tenant_id导致孤儿数据
|
||||
*
|
||||
* @param insertStatement INSERT语句
|
||||
* @param tenantId 租户ID
|
||||
* @return 改写后的SQL
|
||||
*/
|
||||
private String processInsert(Insert insertStatement, Long tenantId) {
|
||||
// 1. 获取表名
|
||||
String tableName = insertStatement.getTable().getName();
|
||||
|
||||
// 2. 获取列定义
|
||||
List<Column> columns = insertStatement.getColumns();
|
||||
if (columns == null || columns.isEmpty()) {
|
||||
// 无列名INSERT: INSERT INTO table VALUES (...)
|
||||
log.warn("【租户INSERT】无列名语句无法自动填充tenant_id: {}", tableName);
|
||||
return insertStatement.toString();
|
||||
}
|
||||
|
||||
// 3. 检查是否已有tenant_id
|
||||
boolean hasTenantId = columns.stream()
|
||||
.anyMatch(col -> "tenant_id".equalsIgnoreCase(col.getColumnName()));
|
||||
|
||||
if (hasTenantId) {
|
||||
log.debug("【租户INSERT】已包含tenant_id,跳过自动填充: {}", tableName);
|
||||
return insertStatement.toString();
|
||||
}
|
||||
|
||||
// 4. 自动添加tenant_id列
|
||||
columns.add(new Column("tenant_id"));
|
||||
|
||||
// 5. 处理VALUES
|
||||
ItemsList itemsList = insertStatement.getItemsList();
|
||||
if (itemsList instanceof ExpressionList) {
|
||||
// 单行INSERT
|
||||
ExpressionList expressionList = (ExpressionList) itemsList;
|
||||
expressionList.getExpressions().add(new LongValue(tenantId));
|
||||
}
|
||||
else if (itemsList instanceof MultiExpressionList) {
|
||||
// 批量INSERT
|
||||
MultiExpressionList multiList = (MultiExpressionList) itemsList;
|
||||
for (ExpressionList expList : multiList.getExpressionLists()) {
|
||||
expList.getExpressions().add(new LongValue(tenantId));
|
||||
}
|
||||
}
|
||||
else if (itemsList instanceof SubSelect) {
|
||||
// INSERT SELECT(暂不支持)
|
||||
log.warn("【租户INSERT】INSERT SELECT语句暂不支持自动填充: {}", tableName);
|
||||
return insertStatement.toString();
|
||||
}
|
||||
|
||||
log.info("【租户INSERT拦截】自动填充tenant_id={}: {}", tenantId, tableName);
|
||||
return insertStatement.toString();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object plugin(Object target) {
|
||||
return Plugin.wrap(target, this);
|
||||
|
|
|
|||
|
|
@ -0,0 +1,92 @@
|
|||
package com.ruoyi.system.domain;
|
||||
|
||||
import com.ruoyi.common.core.domain.BaseEntity;
|
||||
|
||||
/**
|
||||
* 租户套餐对象 sys_tenant_package
|
||||
*
|
||||
* @author ruoyi
|
||||
* @date 2025-12-19
|
||||
*/
|
||||
public class SysTenantPackage extends BaseEntity
|
||||
{
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
/** 套餐ID */
|
||||
private Long packageId;
|
||||
|
||||
/** 套餐名称 */
|
||||
private String packageName;
|
||||
|
||||
/** 套餐编码 */
|
||||
private String packageCode;
|
||||
|
||||
/** 状态(0正常 1停用) */
|
||||
private String status;
|
||||
|
||||
/** 删除标志(0存在 2删除) */
|
||||
private String delFlag;
|
||||
|
||||
/** 菜单ID列表(非数据库字段) */
|
||||
private Long[] menuIds;
|
||||
|
||||
public void setPackageId(Long packageId)
|
||||
{
|
||||
this.packageId = packageId;
|
||||
}
|
||||
|
||||
public Long getPackageId()
|
||||
{
|
||||
return packageId;
|
||||
}
|
||||
|
||||
public void setPackageName(String packageName)
|
||||
{
|
||||
this.packageName = packageName;
|
||||
}
|
||||
|
||||
public String getPackageName()
|
||||
{
|
||||
return packageName;
|
||||
}
|
||||
|
||||
public void setPackageCode(String packageCode)
|
||||
{
|
||||
this.packageCode = packageCode;
|
||||
}
|
||||
|
||||
public String getPackageCode()
|
||||
{
|
||||
return packageCode;
|
||||
}
|
||||
|
||||
public void setStatus(String status)
|
||||
{
|
||||
this.status = status;
|
||||
}
|
||||
|
||||
public String getStatus()
|
||||
{
|
||||
return status;
|
||||
}
|
||||
|
||||
public void setDelFlag(String delFlag)
|
||||
{
|
||||
this.delFlag = delFlag;
|
||||
}
|
||||
|
||||
public String getDelFlag()
|
||||
{
|
||||
return delFlag;
|
||||
}
|
||||
|
||||
public Long[] getMenuIds()
|
||||
{
|
||||
return menuIds;
|
||||
}
|
||||
|
||||
public void setMenuIds(Long[] menuIds)
|
||||
{
|
||||
this.menuIds = menuIds;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,87 @@
|
|||
package com.ruoyi.system.mapper;
|
||||
|
||||
import java.util.List;
|
||||
import com.ruoyi.system.domain.SysTenantPackage;
|
||||
import org.apache.ibatis.annotations.Param;
|
||||
|
||||
/**
|
||||
* 租户套餐Mapper接口
|
||||
*
|
||||
* @author ruoyi
|
||||
* @date 2025-12-19
|
||||
*/
|
||||
public interface SysTenantPackageMapper
|
||||
{
|
||||
/**
|
||||
* 查询租户套餐
|
||||
*
|
||||
* @param packageId 租户套餐主键
|
||||
* @return 租户套餐
|
||||
*/
|
||||
public SysTenantPackage selectPackageById(Long packageId);
|
||||
|
||||
/**
|
||||
* 查询租户套餐列表
|
||||
*
|
||||
* @param sysTenantPackage 租户套餐
|
||||
* @return 租户套餐集合
|
||||
*/
|
||||
public List<SysTenantPackage> selectPackageList(SysTenantPackage sysTenantPackage);
|
||||
|
||||
/**
|
||||
* 新增租户套餐
|
||||
*
|
||||
* @param sysTenantPackage 租户套餐
|
||||
* @return 结果
|
||||
*/
|
||||
public int insertPackage(SysTenantPackage sysTenantPackage);
|
||||
|
||||
/**
|
||||
* 修改租户套餐
|
||||
*
|
||||
* @param sysTenantPackage 租户套餐
|
||||
* @return 结果
|
||||
*/
|
||||
public int updatePackage(SysTenantPackage sysTenantPackage);
|
||||
|
||||
/**
|
||||
* 删除租户套餐
|
||||
*
|
||||
* @param packageId 租户套餐主键
|
||||
* @return 结果
|
||||
*/
|
||||
public int deletePackageById(Long packageId);
|
||||
|
||||
/**
|
||||
* 批量删除租户套餐
|
||||
*
|
||||
* @param packageIds 需要删除的数据主键集合
|
||||
* @return 结果
|
||||
*/
|
||||
public int deletePackageByIds(Long[] packageIds);
|
||||
|
||||
/**
|
||||
* 查询套餐包含的菜单ID列表
|
||||
*
|
||||
* @param packageId 套餐ID
|
||||
* @return 菜单ID列表
|
||||
*/
|
||||
public List<Long> selectMenuIdsByPackageId(Long packageId);
|
||||
|
||||
/**
|
||||
* 批量新增套餐菜单关联
|
||||
*
|
||||
* @param packageId 套餐ID
|
||||
* @param menuIds 菜单ID列表
|
||||
* @return 结果
|
||||
*/
|
||||
public int batchInsertPackageMenu(@Param("packageId") Long packageId, @Param("menuIds") Long[] menuIds);
|
||||
|
||||
/**
|
||||
* 删除套餐菜单关联
|
||||
*
|
||||
* @param packageId 套餐ID
|
||||
* @return 结果
|
||||
*/
|
||||
public int deletePackageMenuByPackageId(Long packageId);
|
||||
}
|
||||
|
|
@ -0,0 +1,71 @@
|
|||
package com.ruoyi.system.service;
|
||||
|
||||
import java.util.List;
|
||||
import com.ruoyi.system.domain.SysTenantPackage;
|
||||
|
||||
/**
|
||||
* 租户套餐Service接口
|
||||
*
|
||||
* @author ruoyi
|
||||
* @date 2025-12-19
|
||||
*/
|
||||
public interface ISysTenantPackageService
|
||||
{
|
||||
/**
|
||||
* 查询租户套餐
|
||||
*
|
||||
* @param packageId 租户套餐主键
|
||||
* @return 租户套餐
|
||||
*/
|
||||
public SysTenantPackage selectPackageById(Long packageId);
|
||||
|
||||
/**
|
||||
* 查询租户套餐列表
|
||||
*
|
||||
* @param sysTenantPackage 租户套餐
|
||||
* @return 租户套餐集合
|
||||
*/
|
||||
public List<SysTenantPackage> selectPackageList(SysTenantPackage sysTenantPackage);
|
||||
|
||||
/**
|
||||
* 新增租户套餐
|
||||
*
|
||||
* @param sysTenantPackage 租户套餐
|
||||
* @return 结果
|
||||
*/
|
||||
public int insertPackage(SysTenantPackage sysTenantPackage);
|
||||
|
||||
/**
|
||||
* 修改租户套餐
|
||||
*
|
||||
* @param sysTenantPackage 租户套餐
|
||||
* @return 结果
|
||||
*/
|
||||
public int updatePackage(SysTenantPackage sysTenantPackage);
|
||||
|
||||
/**
|
||||
* 批量删除租户套餐
|
||||
*
|
||||
* @param packageIds 需要删除的租户套餐主键集合
|
||||
* @return 结果
|
||||
*/
|
||||
public int deletePackageByIds(Long[] packageIds);
|
||||
|
||||
/**
|
||||
* 删除租户套餐信息
|
||||
*
|
||||
* @param packageId 租户套餐主键
|
||||
* @return 结果
|
||||
*/
|
||||
public int deletePackageById(Long packageId);
|
||||
|
||||
/**
|
||||
* 查询套餐包含的菜单ID列表
|
||||
*
|
||||
* Reason: 用于套餐菜单过滤,返回空列表表示不限制
|
||||
*
|
||||
* @param packageId 套餐ID
|
||||
* @return 菜单ID列表,空列表表示高级套餐(不限制)
|
||||
*/
|
||||
public List<Long> selectMenuIdsByPackageId(Long packageId);
|
||||
}
|
||||
|
|
@ -8,6 +8,8 @@ import java.util.LinkedList;
|
|||
import java.util.List;
|
||||
import java.util.Set;
|
||||
import java.util.stream.Collectors;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.stereotype.Service;
|
||||
import com.ruoyi.common.constant.Constants;
|
||||
|
|
@ -16,14 +18,18 @@ import com.ruoyi.common.core.domain.TreeSelect;
|
|||
import com.ruoyi.common.core.domain.entity.SysMenu;
|
||||
import com.ruoyi.common.core.domain.entity.SysRole;
|
||||
import com.ruoyi.common.core.domain.entity.SysUser;
|
||||
import com.ruoyi.common.core.domain.model.LoginUser;
|
||||
import com.ruoyi.common.utils.SecurityUtils;
|
||||
import com.ruoyi.common.utils.StringUtils;
|
||||
import com.ruoyi.system.domain.SysTenant;
|
||||
import com.ruoyi.system.domain.vo.MetaVo;
|
||||
import com.ruoyi.system.domain.vo.RouterVo;
|
||||
import com.ruoyi.system.mapper.SysMenuMapper;
|
||||
import com.ruoyi.system.mapper.SysRoleMapper;
|
||||
import com.ruoyi.system.mapper.SysRoleMenuMapper;
|
||||
import com.ruoyi.system.service.ISysMenuService;
|
||||
import com.ruoyi.system.service.ISysTenantService;
|
||||
import com.ruoyi.system.service.ISysTenantPackageService;
|
||||
|
||||
/**
|
||||
* 菜单 业务层处理
|
||||
|
|
@ -33,6 +39,8 @@ import com.ruoyi.system.service.ISysMenuService;
|
|||
@Service
|
||||
public class SysMenuServiceImpl implements ISysMenuService
|
||||
{
|
||||
private static final Logger log = LoggerFactory.getLogger(SysMenuServiceImpl.class);
|
||||
|
||||
public static final String PREMISSION_STRING = "perms[\"{0}\"]";
|
||||
|
||||
@Autowired
|
||||
|
|
@ -44,6 +52,12 @@ public class SysMenuServiceImpl implements ISysMenuService
|
|||
@Autowired
|
||||
private SysRoleMenuMapper roleMenuMapper;
|
||||
|
||||
@Autowired
|
||||
private ISysTenantService tenantService;
|
||||
|
||||
@Autowired
|
||||
private ISysTenantPackageService packageService;
|
||||
|
||||
/**
|
||||
* 根据用户查询系统菜单列表
|
||||
*
|
||||
|
|
@ -58,7 +72,7 @@ public class SysMenuServiceImpl implements ISysMenuService
|
|||
|
||||
/**
|
||||
* 查询系统菜单列表
|
||||
*
|
||||
*
|
||||
* @param menu 菜单信息
|
||||
* @return 菜单列表
|
||||
*/
|
||||
|
|
@ -75,6 +89,8 @@ public class SysMenuServiceImpl implements ISysMenuService
|
|||
{
|
||||
menu.getParams().put("userId", userId);
|
||||
menuList = menuMapper.selectMenuListByUserId(menu);
|
||||
// 应用租户套餐过滤
|
||||
menuList = applyPackageFilter(menuList);
|
||||
}
|
||||
return menuList;
|
||||
}
|
||||
|
|
@ -123,7 +139,7 @@ public class SysMenuServiceImpl implements ISysMenuService
|
|||
|
||||
/**
|
||||
* 根据用户ID查询菜单
|
||||
*
|
||||
*
|
||||
* @param userId 用户名称
|
||||
* @return 菜单列表
|
||||
*/
|
||||
|
|
@ -138,6 +154,8 @@ public class SysMenuServiceImpl implements ISysMenuService
|
|||
else
|
||||
{
|
||||
menus = menuMapper.selectMenuTreeByUserId(userId);
|
||||
// 应用租户套餐过滤
|
||||
menus = applyPackageFilter(menus);
|
||||
}
|
||||
return getChildPerms(menus, 0);
|
||||
}
|
||||
|
|
@ -532,7 +550,7 @@ public class SysMenuServiceImpl implements ISysMenuService
|
|||
|
||||
/**
|
||||
* 内链域名特殊字符替换
|
||||
*
|
||||
*
|
||||
* @return 替换后的内链域名
|
||||
*/
|
||||
public String innerLinkReplaceEach(String path)
|
||||
|
|
@ -540,4 +558,61 @@ public class SysMenuServiceImpl implements ISysMenuService
|
|||
return StringUtils.replaceEach(path, new String[] { Constants.HTTP, Constants.HTTPS, Constants.WWW, ".", ":" },
|
||||
new String[] { "", "", "", "/", "/" });
|
||||
}
|
||||
|
||||
/**
|
||||
* 应用租户套餐菜单过滤
|
||||
*
|
||||
* Reason: 实现租户套餐功能,通过套餐控制租户的菜单权限
|
||||
* 逻辑:角色菜单 ∩ 套餐菜单
|
||||
*
|
||||
* @param menus 角色菜单列表
|
||||
* @return 过滤后的菜单列表
|
||||
*/
|
||||
private List<SysMenu> applyPackageFilter(List<SysMenu> menus)
|
||||
{
|
||||
try
|
||||
{
|
||||
// 1. 获取当前租户
|
||||
LoginUser loginUser = SecurityUtils.getLoginUser();
|
||||
if (loginUser == null || loginUser.getTenantId() == null)
|
||||
{
|
||||
return menus; // 无租户不过滤
|
||||
}
|
||||
|
||||
// 2. 获取租户信息
|
||||
SysTenant tenant = tenantService.selectSysTenantByTenantId(loginUser.getTenantId());
|
||||
if (tenant == null || tenant.getPackageId() == null)
|
||||
{
|
||||
log.warn("租户 {} 未配置套餐,不限制菜单", loginUser.getTenantId());
|
||||
return menus;
|
||||
}
|
||||
|
||||
// 3. 获取套餐菜单ID集合
|
||||
List<Long> packageMenuIds = packageService.selectMenuIdsByPackageId(tenant.getPackageId());
|
||||
|
||||
// 4. 空列表表示高级套餐(不限制)
|
||||
if (packageMenuIds == null || packageMenuIds.isEmpty())
|
||||
{
|
||||
log.debug("租户 {} 使用高级套餐,不限制菜单", loginUser.getTenantId());
|
||||
return menus;
|
||||
}
|
||||
|
||||
// 5. 过滤:只保留套餐内菜单
|
||||
Set<Long> packageMenuIdSet = new HashSet<>(packageMenuIds);
|
||||
List<SysMenu> filteredMenus = menus.stream()
|
||||
.filter(menu -> packageMenuIdSet.contains(menu.getMenuId()))
|
||||
.collect(Collectors.toList());
|
||||
|
||||
log.debug("租户 {} 套餐过滤: {} → {} 个菜单",
|
||||
loginUser.getTenantId(), menus.size(), filteredMenus.size());
|
||||
|
||||
return filteredMenus;
|
||||
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
log.error("应用租户套餐过滤失败,降级为不过滤", e);
|
||||
return menus; // 容错:失败不影响正常功能
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,124 @@
|
|||
package com.ruoyi.system.service.impl;
|
||||
|
||||
import java.util.List;
|
||||
import com.ruoyi.common.utils.DateUtils;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
import com.ruoyi.system.mapper.SysTenantPackageMapper;
|
||||
import com.ruoyi.system.domain.SysTenantPackage;
|
||||
import com.ruoyi.system.service.ISysTenantPackageService;
|
||||
|
||||
/**
|
||||
* 租户套餐Service业务层处理
|
||||
*
|
||||
* @author ruoyi
|
||||
* @date 2025-12-19
|
||||
*/
|
||||
@Service
|
||||
public class SysTenantPackageServiceImpl implements ISysTenantPackageService
|
||||
{
|
||||
@Autowired
|
||||
private SysTenantPackageMapper tenantPackageMapper;
|
||||
|
||||
/**
|
||||
* 查询租户套餐
|
||||
*
|
||||
* @param packageId 租户套餐主键
|
||||
* @return 租户套餐
|
||||
*/
|
||||
@Override
|
||||
public SysTenantPackage selectPackageById(Long packageId)
|
||||
{
|
||||
return tenantPackageMapper.selectPackageById(packageId);
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询租户套餐列表
|
||||
*
|
||||
* @param sysTenantPackage 租户套餐
|
||||
* @return 租户套餐
|
||||
*/
|
||||
@Override
|
||||
public List<SysTenantPackage> selectPackageList(SysTenantPackage sysTenantPackage)
|
||||
{
|
||||
return tenantPackageMapper.selectPackageList(sysTenantPackage);
|
||||
}
|
||||
|
||||
/**
|
||||
* 新增租户套餐
|
||||
*
|
||||
* @param sysTenantPackage 租户套餐
|
||||
* @return 结果
|
||||
*/
|
||||
@Override
|
||||
@Transactional
|
||||
public int insertPackage(SysTenantPackage sysTenantPackage)
|
||||
{
|
||||
sysTenantPackage.setCreateTime(DateUtils.getNowDate());
|
||||
int rows = tenantPackageMapper.insertPackage(sysTenantPackage);
|
||||
// 保存菜单关联
|
||||
if (sysTenantPackage.getMenuIds() != null && sysTenantPackage.getMenuIds().length > 0)
|
||||
{
|
||||
tenantPackageMapper.batchInsertPackageMenu(sysTenantPackage.getPackageId(), sysTenantPackage.getMenuIds());
|
||||
}
|
||||
return rows;
|
||||
}
|
||||
|
||||
/**
|
||||
* 修改租户套餐
|
||||
*
|
||||
* @param sysTenantPackage 租户套餐
|
||||
* @return 结果
|
||||
*/
|
||||
@Override
|
||||
@Transactional
|
||||
public int updatePackage(SysTenantPackage sysTenantPackage)
|
||||
{
|
||||
sysTenantPackage.setUpdateTime(DateUtils.getNowDate());
|
||||
// 先删除旧的菜单关联
|
||||
tenantPackageMapper.deletePackageMenuByPackageId(sysTenantPackage.getPackageId());
|
||||
// 再插入新的菜单关联
|
||||
if (sysTenantPackage.getMenuIds() != null && sysTenantPackage.getMenuIds().length > 0)
|
||||
{
|
||||
tenantPackageMapper.batchInsertPackageMenu(sysTenantPackage.getPackageId(), sysTenantPackage.getMenuIds());
|
||||
}
|
||||
return tenantPackageMapper.updatePackage(sysTenantPackage);
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量删除租户套餐
|
||||
*
|
||||
* @param packageIds 需要删除的租户套餐主键
|
||||
* @return 结果
|
||||
*/
|
||||
@Override
|
||||
public int deletePackageByIds(Long[] packageIds)
|
||||
{
|
||||
return tenantPackageMapper.deletePackageByIds(packageIds);
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除租户套餐信息
|
||||
*
|
||||
* @param packageId 租户套餐主键
|
||||
* @return 结果
|
||||
*/
|
||||
@Override
|
||||
public int deletePackageById(Long packageId)
|
||||
{
|
||||
return tenantPackageMapper.deletePackageById(packageId);
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询套餐包含的菜单ID列表
|
||||
*
|
||||
* @param packageId 套餐ID
|
||||
* @return 菜单ID列表,空列表表示高级套餐(不限制)
|
||||
*/
|
||||
@Override
|
||||
public List<Long> selectMenuIdsByPackageId(Long packageId)
|
||||
{
|
||||
return tenantPackageMapper.selectMenuIdsByPackageId(packageId);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,110 @@
|
|||
<?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.SysTenantPackageMapper">
|
||||
|
||||
<resultMap type="SysTenantPackage" id="SysTenantPackageResult">
|
||||
<result property="packageId" column="package_id" />
|
||||
<result property="packageName" column="package_name" />
|
||||
<result property="packageCode" column="package_code" />
|
||||
<result property="status" column="status" />
|
||||
<result property="delFlag" column="del_flag" />
|
||||
<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="selectPackageVo">
|
||||
select package_id, package_name, package_code, status, del_flag, create_by, create_time, update_by, update_time, remark from sys_tenant_package
|
||||
</sql>
|
||||
|
||||
<select id="selectPackageList" parameterType="SysTenantPackage" resultMap="SysTenantPackageResult">
|
||||
<include refid="selectPackageVo"/>
|
||||
<where>
|
||||
del_flag = '0'
|
||||
<if test="packageName != null and packageName != ''"> and package_name like concat('%', #{packageName}, '%')</if>
|
||||
<if test="packageCode != null and packageCode != ''"> and package_code = #{packageCode}</if>
|
||||
<if test="status != null and status != ''"> and status = #{status}</if>
|
||||
</where>
|
||||
</select>
|
||||
|
||||
<select id="selectPackageById" parameterType="Long" resultMap="SysTenantPackageResult">
|
||||
<include refid="selectPackageVo"/>
|
||||
where package_id = #{packageId} and del_flag = '0'
|
||||
</select>
|
||||
|
||||
<insert id="insertPackage" parameterType="SysTenantPackage" useGeneratedKeys="true" keyProperty="packageId">
|
||||
insert into sys_tenant_package
|
||||
<trim prefix="(" suffix=")" suffixOverrides=",">
|
||||
<if test="packageName != null and packageName != ''">package_name,</if>
|
||||
<if test="packageCode != null and packageCode != ''">package_code,</if>
|
||||
<if test="status != null">status,</if>
|
||||
<if test="delFlag != null">del_flag,</if>
|
||||
<if test="createBy != null">create_by,</if>
|
||||
<if test="createTime != null">create_time,</if>
|
||||
<if test="updateBy != null">update_by,</if>
|
||||
<if test="updateTime != null">update_time,</if>
|
||||
<if test="remark != null">remark,</if>
|
||||
</trim>
|
||||
<trim prefix="values (" suffix=")" suffixOverrides=",">
|
||||
<if test="packageName != null and packageName != ''">#{packageName},</if>
|
||||
<if test="packageCode != null and packageCode != ''">#{packageCode},</if>
|
||||
<if test="status != null">#{status},</if>
|
||||
<if test="delFlag != null">#{delFlag},</if>
|
||||
<if test="createBy != null">#{createBy},</if>
|
||||
<if test="createTime != null">#{createTime},</if>
|
||||
<if test="updateBy != null">#{updateBy},</if>
|
||||
<if test="updateTime != null">#{updateTime},</if>
|
||||
<if test="remark != null">#{remark},</if>
|
||||
</trim>
|
||||
</insert>
|
||||
|
||||
<update id="updatePackage" parameterType="SysTenantPackage">
|
||||
update sys_tenant_package
|
||||
<trim prefix="SET" suffixOverrides=",">
|
||||
<if test="packageName != null and packageName != ''">package_name = #{packageName},</if>
|
||||
<if test="packageCode != null and packageCode != ''">package_code = #{packageCode},</if>
|
||||
<if test="status != null">status = #{status},</if>
|
||||
<if test="delFlag != null">del_flag = #{delFlag},</if>
|
||||
<if test="createBy != null">create_by = #{createBy},</if>
|
||||
<if test="createTime != null">create_time = #{createTime},</if>
|
||||
<if test="updateBy != null">update_by = #{updateBy},</if>
|
||||
<if test="updateTime != null">update_time = #{updateTime},</if>
|
||||
<if test="remark != null">remark = #{remark},</if>
|
||||
</trim>
|
||||
where package_id = #{packageId}
|
||||
</update>
|
||||
|
||||
<update id="deletePackageById" parameterType="Long">
|
||||
update sys_tenant_package set del_flag = '2' where package_id = #{packageId}
|
||||
</update>
|
||||
|
||||
<update id="deletePackageByIds" parameterType="Long">
|
||||
update sys_tenant_package set del_flag = '2' where package_id in
|
||||
<foreach item="packageId" collection="array" open="(" separator="," close=")">
|
||||
#{packageId}
|
||||
</foreach>
|
||||
</update>
|
||||
|
||||
<!-- 查询套餐包含的菜单ID列表 -->
|
||||
<select id="selectMenuIdsByPackageId" parameterType="Long" resultType="Long">
|
||||
select menu_id from sys_package_menu where package_id = #{packageId}
|
||||
</select>
|
||||
|
||||
<!-- 批量新增套餐菜单关联 -->
|
||||
<insert id="batchInsertPackageMenu">
|
||||
insert into sys_package_menu(package_id, menu_id) values
|
||||
<foreach item="menuId" collection="menuIds" separator=",">
|
||||
(#{packageId}, #{menuId})
|
||||
</foreach>
|
||||
</insert>
|
||||
|
||||
<!-- 删除套餐菜单关联 -->
|
||||
<delete id="deletePackageMenuByPackageId" parameterType="Long">
|
||||
delete from sys_package_menu where package_id = #{packageId}
|
||||
</delete>
|
||||
|
||||
</mapper>
|
||||
|
|
@ -0,0 +1,52 @@
|
|||
import request from '@/utils/request'
|
||||
|
||||
// 查询套餐列表
|
||||
export function listPackage(query) {
|
||||
return request({
|
||||
url: '/system/package/list',
|
||||
method: 'get',
|
||||
params: query
|
||||
})
|
||||
}
|
||||
|
||||
// 查询套餐详细
|
||||
export function getPackage(packageId) {
|
||||
return request({
|
||||
url: '/system/package/' + packageId,
|
||||
method: 'get'
|
||||
})
|
||||
}
|
||||
|
||||
// 新增套餐
|
||||
export function addPackage(data) {
|
||||
return request({
|
||||
url: '/system/package',
|
||||
method: 'post',
|
||||
data: data
|
||||
})
|
||||
}
|
||||
|
||||
// 修改套餐
|
||||
export function updatePackage(data) {
|
||||
return request({
|
||||
url: '/system/package',
|
||||
method: 'put',
|
||||
data: data
|
||||
})
|
||||
}
|
||||
|
||||
// 删除套餐
|
||||
export function delPackage(packageId) {
|
||||
return request({
|
||||
url: '/system/package/' + packageId,
|
||||
method: 'delete'
|
||||
})
|
||||
}
|
||||
|
||||
// 查询套餐关联的菜单ID列表
|
||||
export function getPackageMenus(packageId) {
|
||||
return request({
|
||||
url: '/system/package/' + packageId + '/menus',
|
||||
method: 'get'
|
||||
})
|
||||
}
|
||||
|
|
@ -0,0 +1,376 @@
|
|||
<template>
|
||||
<div class="app-container">
|
||||
<el-form :model="queryParams" ref="queryForm" size="small" :inline="true" v-show="showSearch" label-width="68px">
|
||||
<el-form-item label="套餐名称" prop="packageName">
|
||||
<el-input
|
||||
v-model="queryParams.packageName"
|
||||
placeholder="请输入套餐名称"
|
||||
clearable
|
||||
@keyup.enter.native="handleQuery"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="套餐编码" prop="packageCode">
|
||||
<el-input
|
||||
v-model="queryParams.packageCode"
|
||||
placeholder="请输入套餐编码"
|
||||
clearable
|
||||
@keyup.enter.native="handleQuery"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="状态" prop="status">
|
||||
<el-select v-model="queryParams.status" placeholder="请选择状态" clearable>
|
||||
<el-option label="正常" value="0" />
|
||||
<el-option label="停用" value="1" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button type="primary" icon="el-icon-search" size="mini" @click="handleQuery">搜索</el-button>
|
||||
<el-button icon="el-icon-refresh" size="mini" @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="el-icon-plus"
|
||||
size="mini"
|
||||
@click="handleAdd"
|
||||
v-hasPermi="['system:package:add']"
|
||||
>新增</el-button>
|
||||
</el-col>
|
||||
<el-col :span="1.5">
|
||||
<el-button
|
||||
type="success"
|
||||
plain
|
||||
icon="el-icon-edit"
|
||||
size="mini"
|
||||
:disabled="single"
|
||||
@click="handleUpdate"
|
||||
v-hasPermi="['system:package:edit']"
|
||||
>修改</el-button>
|
||||
</el-col>
|
||||
<el-col :span="1.5">
|
||||
<el-button
|
||||
type="danger"
|
||||
plain
|
||||
icon="el-icon-delete"
|
||||
size="mini"
|
||||
:disabled="multiple"
|
||||
@click="handleDelete"
|
||||
v-hasPermi="['system:package:remove']"
|
||||
>删除</el-button>
|
||||
</el-col>
|
||||
<right-toolbar :showSearch.sync="showSearch" @queryTable="getList"></right-toolbar>
|
||||
</el-row>
|
||||
|
||||
<el-table v-loading="loading" :data="packageList" @selection-change="handleSelectionChange">
|
||||
<el-table-column type="selection" width="55" align="center" />
|
||||
<el-table-column label="套餐ID" align="center" prop="packageId" width="80" />
|
||||
<el-table-column label="套餐名称" align="center" prop="packageName" :show-overflow-tooltip="true" />
|
||||
<el-table-column label="套餐编码" align="center" prop="packageCode" :show-overflow-tooltip="true" />
|
||||
<el-table-column label="状态" align="center" prop="status" width="100">
|
||||
<template slot-scope="scope">
|
||||
<el-tag :type="scope.row.status === '0' ? 'success' : 'danger'">
|
||||
{{ scope.row.status === '0' ? '正常' : '停用' }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="备注" align="center" prop="remark" :show-overflow-tooltip="true" />
|
||||
<el-table-column label="创建时间" align="center" prop="createTime" width="180">
|
||||
<template slot-scope="scope">
|
||||
<span>{{ parseTime(scope.row.createTime) }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" align="center" class-name="small-padding fixed-width" width="200">
|
||||
<template slot-scope="scope">
|
||||
<el-button
|
||||
size="mini"
|
||||
type="text"
|
||||
icon="el-icon-edit"
|
||||
@click="handleUpdate(scope.row)"
|
||||
v-hasPermi="['system:package:edit']"
|
||||
>修改</el-button>
|
||||
<el-button
|
||||
size="mini"
|
||||
type="text"
|
||||
icon="el-icon-delete"
|
||||
@click="handleDelete(scope.row)"
|
||||
v-hasPermi="['system:package:remove']"
|
||||
>删除</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
|
||||
<pagination
|
||||
v-show="total>0"
|
||||
:total="total"
|
||||
:page.sync="queryParams.pageNum"
|
||||
:limit.sync="queryParams.pageSize"
|
||||
@pagination="getList"
|
||||
/>
|
||||
|
||||
<!-- 添加或修改套餐对话框 -->
|
||||
<el-dialog :title="title" :visible.sync="open" width="600px" append-to-body>
|
||||
<el-form ref="form" :model="form" :rules="rules" label-width="100px">
|
||||
<el-form-item label="套餐名称" prop="packageName">
|
||||
<el-input v-model="form.packageName" placeholder="请输入套餐名称" />
|
||||
</el-form-item>
|
||||
<el-form-item label="套餐编码" prop="packageCode">
|
||||
<el-input v-model="form.packageCode" placeholder="请输入套餐编码" />
|
||||
</el-form-item>
|
||||
<el-form-item label="状态" prop="status">
|
||||
<el-radio-group v-model="form.status">
|
||||
<el-radio label="0">正常</el-radio>
|
||||
<el-radio label="1">停用</el-radio>
|
||||
</el-radio-group>
|
||||
</el-form-item>
|
||||
<el-form-item label="菜单权限">
|
||||
<el-checkbox v-model="menuExpand" @change="handleCheckedTreeExpand($event, 'menu')">展开/折叠</el-checkbox>
|
||||
<el-checkbox v-model="menuNodeAll" @change="handleCheckedTreeNodeAll($event, 'menu')">全选/全不选</el-checkbox>
|
||||
<el-checkbox v-model="form.menuCheckStrictly" @change="handleCheckedTreeConnect($event, 'menu')">父子联动</el-checkbox>
|
||||
<el-tree
|
||||
class="tree-border"
|
||||
:data="menuOptions"
|
||||
show-checkbox
|
||||
ref="menu"
|
||||
node-key="id"
|
||||
:check-strictly="!form.menuCheckStrictly"
|
||||
empty-text="加载中,请稍候"
|
||||
:props="defaultProps"
|
||||
></el-tree>
|
||||
</el-form-item>
|
||||
<el-form-item label="备注" prop="remark">
|
||||
<el-input v-model="form.remark" type="textarea" placeholder="请输入备注" />
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<div slot="footer" class="dialog-footer">
|
||||
<el-button type="primary" @click="submitForm">确 定</el-button>
|
||||
<el-button @click="cancel">取 消</el-button>
|
||||
</div>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { listPackage, getPackage, addPackage, updatePackage, delPackage, getPackageMenus } from "@/api/system/package"
|
||||
import { treeselect as menuTreeselect } from "@/api/system/menu"
|
||||
|
||||
export default {
|
||||
name: "Package",
|
||||
data() {
|
||||
return {
|
||||
// 遮罩层
|
||||
loading: true,
|
||||
// 选中数组
|
||||
ids: [],
|
||||
// 非单个禁用
|
||||
single: true,
|
||||
// 非多个禁用
|
||||
multiple: true,
|
||||
// 显示搜索条件
|
||||
showSearch: true,
|
||||
// 总条数
|
||||
total: 0,
|
||||
// 套餐表格数据
|
||||
packageList: [],
|
||||
// 弹出层标题
|
||||
title: "",
|
||||
// 是否显示弹出层
|
||||
open: false,
|
||||
// 菜单列表
|
||||
menuOptions: [],
|
||||
// 菜单树展开
|
||||
menuExpand: false,
|
||||
// 菜单树全选
|
||||
menuNodeAll: false,
|
||||
// 查询参数
|
||||
queryParams: {
|
||||
pageNum: 1,
|
||||
pageSize: 10,
|
||||
packageName: null,
|
||||
packageCode: null,
|
||||
status: null
|
||||
},
|
||||
// 表单参数
|
||||
form: {},
|
||||
// 默认树属性
|
||||
defaultProps: {
|
||||
children: "children",
|
||||
label: "label"
|
||||
},
|
||||
// 表单校验
|
||||
rules: {
|
||||
packageName: [
|
||||
{ required: true, message: "套餐名称不能为空", trigger: "blur" }
|
||||
],
|
||||
packageCode: [
|
||||
{ required: true, message: "套餐编码不能为空", trigger: "blur" }
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
created() {
|
||||
this.getList()
|
||||
},
|
||||
methods: {
|
||||
/** 查询套餐列表 */
|
||||
getList() {
|
||||
this.loading = true
|
||||
listPackage(this.queryParams).then(response => {
|
||||
this.packageList = response.rows
|
||||
this.total = response.total
|
||||
this.loading = false
|
||||
})
|
||||
},
|
||||
/** 查询菜单树结构 */
|
||||
getMenuTreeselect() {
|
||||
menuTreeselect().then(response => {
|
||||
this.menuOptions = response.data
|
||||
})
|
||||
},
|
||||
// 取消按钮
|
||||
cancel() {
|
||||
this.open = false
|
||||
this.reset()
|
||||
},
|
||||
// 表单重置
|
||||
reset() {
|
||||
if (this.$refs.menu != undefined) {
|
||||
this.$refs.menu.setCheckedKeys([])
|
||||
}
|
||||
this.menuExpand = false
|
||||
this.menuNodeAll = false
|
||||
this.form = {
|
||||
packageId: null,
|
||||
packageName: null,
|
||||
packageCode: null,
|
||||
status: "0",
|
||||
menuCheckStrictly: true,
|
||||
remark: null
|
||||
}
|
||||
this.resetForm("form")
|
||||
},
|
||||
/** 搜索按钮操作 */
|
||||
handleQuery() {
|
||||
this.queryParams.pageNum = 1
|
||||
this.getList()
|
||||
},
|
||||
/** 重置按钮操作 */
|
||||
resetQuery() {
|
||||
this.resetForm("queryForm")
|
||||
this.handleQuery()
|
||||
},
|
||||
// 多选框选中数据
|
||||
handleSelectionChange(selection) {
|
||||
this.ids = selection.map(item => item.packageId)
|
||||
this.single = selection.length !== 1
|
||||
this.multiple = !selection.length
|
||||
},
|
||||
// 树权限(展开/折叠)
|
||||
handleCheckedTreeExpand(value, type) {
|
||||
if (type == 'menu') {
|
||||
let treeList = this.menuOptions
|
||||
for (let i = 0; i < treeList.length; i++) {
|
||||
this.$refs.menu.store.nodesMap[treeList[i].id].expanded = value
|
||||
}
|
||||
}
|
||||
},
|
||||
// 树权限(全选/全不选)
|
||||
handleCheckedTreeNodeAll(value, type) {
|
||||
if (type == 'menu') {
|
||||
this.$refs.menu.setCheckedNodes(value ? this.menuOptions : [])
|
||||
}
|
||||
},
|
||||
// 树权限(父子联动)
|
||||
handleCheckedTreeConnect(value, type) {
|
||||
if (type == 'menu') {
|
||||
this.form.menuCheckStrictly = value ? true : false
|
||||
}
|
||||
},
|
||||
/** 新增按钮操作 */
|
||||
handleAdd() {
|
||||
this.reset()
|
||||
this.getMenuTreeselect()
|
||||
this.open = true
|
||||
this.title = "添加套餐"
|
||||
},
|
||||
/** 修改按钮操作 */
|
||||
handleUpdate(row) {
|
||||
this.reset()
|
||||
this.getMenuTreeselect()
|
||||
const packageId = row.packageId || this.ids
|
||||
getPackage(packageId).then(response => {
|
||||
this.form = response.data
|
||||
this.form.menuCheckStrictly = true
|
||||
this.open = true
|
||||
this.title = "修改套餐"
|
||||
// 获取套餐关联的菜单
|
||||
this.$nextTick(() => {
|
||||
getPackageMenus(packageId).then(res => {
|
||||
let checkedKeys = res.data
|
||||
checkedKeys.forEach((v) => {
|
||||
this.$nextTick(() => {
|
||||
this.$refs.menu.setChecked(v, true, false)
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
},
|
||||
/** 提交按钮 */
|
||||
submitForm() {
|
||||
this.$refs["form"].validate(valid => {
|
||||
if (valid) {
|
||||
// 获取选中的菜单ID
|
||||
this.form.menuIds = this.getMenuAllCheckedKeys()
|
||||
if (this.form.packageId != null) {
|
||||
updatePackage(this.form).then(response => {
|
||||
this.$modal.msgSuccess("修改成功")
|
||||
this.open = false
|
||||
this.getList()
|
||||
})
|
||||
} else {
|
||||
addPackage(this.form).then(response => {
|
||||
this.$modal.msgSuccess("新增成功")
|
||||
this.open = false
|
||||
this.getList()
|
||||
})
|
||||
}
|
||||
}
|
||||
})
|
||||
},
|
||||
/** 删除按钮操作 */
|
||||
handleDelete(row) {
|
||||
const packageIds = row.packageId || this.ids
|
||||
this.$modal.confirm('是否确认删除套餐编号为"' + packageIds + '"的数据项?').then(function() {
|
||||
return delPackage(packageIds)
|
||||
}).then(() => {
|
||||
this.getList()
|
||||
this.$modal.msgSuccess("删除成功")
|
||||
}).catch(() => {})
|
||||
},
|
||||
// 所有菜单节点数据
|
||||
getMenuAllCheckedKeys() {
|
||||
// 目前被选中的菜单节点
|
||||
let checkedKeys = this.$refs.menu.getCheckedKeys()
|
||||
// 半选中的菜单节点
|
||||
let halfCheckedKeys = this.$refs.menu.getHalfCheckedKeys()
|
||||
checkedKeys.unshift.apply(checkedKeys, halfCheckedKeys)
|
||||
return checkedKeys
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.tree-border {
|
||||
margin-top: 5px;
|
||||
border: 1px solid #e5e6e7;
|
||||
background: #FFFFFF none;
|
||||
border-radius: 4px;
|
||||
max-height: 300px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
</style>
|
||||
|
|
@ -41,13 +41,15 @@
|
|||
placeholder="请选择过期时间">
|
||||
</el-date-picker>
|
||||
</el-form-item>
|
||||
<el-form-item label="套餐ID" prop="packageId">
|
||||
<el-input
|
||||
v-model="queryParams.packageId"
|
||||
placeholder="请输入套餐ID"
|
||||
clearable
|
||||
@keyup.enter.native="handleQuery"
|
||||
/>
|
||||
<el-form-item label="租户套餐" prop="packageId">
|
||||
<el-select v-model="queryParams.packageId" placeholder="请选择套餐" clearable>
|
||||
<el-option
|
||||
v-for="pkg in packageList"
|
||||
:key="pkg.packageId"
|
||||
:label="pkg.packageName"
|
||||
:value="pkg.packageId"
|
||||
/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button type="primary" icon="el-icon-search" size="mini" @click="handleQuery">搜索</el-button>
|
||||
|
|
@ -113,7 +115,11 @@
|
|||
<span>{{ parseTime(scope.row.expireTime, '{y}-{m}-{d}') }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="套餐ID" align="center" prop="packageId" />
|
||||
<el-table-column label="租户套餐" align="center" prop="packageId">
|
||||
<template slot-scope="scope">
|
||||
<span>{{ getPackageName(scope.row.packageId) }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="状态" align="center" prop="status" />
|
||||
<el-table-column label="备注" align="center" prop="remark" />
|
||||
<el-table-column label="操作" align="center" class-name="small-padding fixed-width">
|
||||
|
|
@ -167,11 +173,18 @@
|
|||
placeholder="请选择过期时间">
|
||||
</el-date-picker>
|
||||
</el-form-item>
|
||||
<el-form-item label="套餐ID" prop="packageId">
|
||||
<el-input v-model="form.packageId" placeholder="请输入套餐ID" />
|
||||
</el-form-item>
|
||||
<el-form-item label="删除标志" prop="delFlag">
|
||||
<el-input v-model="form.delFlag" placeholder="请输入删除标志" />
|
||||
<el-form-item label="租户套餐" prop="packageId">
|
||||
<el-select v-model="form.packageId" placeholder="请选择套餐" clearable style="width: 100%">
|
||||
<el-option
|
||||
v-for="pkg in packageList"
|
||||
:key="pkg.packageId"
|
||||
:label="pkg.packageName"
|
||||
:value="pkg.packageId"
|
||||
>
|
||||
<span>{{ pkg.packageName }}</span>
|
||||
<span style="color: #8492a6; font-size: 12px; margin-left: 10px">{{ pkg.remark }}</span>
|
||||
</el-option>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="备注" prop="remark">
|
||||
<el-input v-model="form.remark" type="textarea" placeholder="请输入内容" />
|
||||
|
|
@ -187,6 +200,7 @@
|
|||
|
||||
<script>
|
||||
import { listTenant, getTenant, delTenant, addTenant, updateTenant } from "@/api/system/tenant"
|
||||
import { listPackage } from "@/api/system/package"
|
||||
|
||||
export default {
|
||||
name: "Tenant",
|
||||
|
|
@ -206,6 +220,8 @@ export default {
|
|||
total: 0,
|
||||
// 租户信息表格数据
|
||||
tenantList: [],
|
||||
// 套餐列表
|
||||
packageList: [],
|
||||
// 弹出层标题
|
||||
title: "",
|
||||
// 是否显示弹出层
|
||||
|
|
@ -237,6 +253,7 @@ export default {
|
|||
},
|
||||
created() {
|
||||
this.getList()
|
||||
this.getPackageList()
|
||||
},
|
||||
methods: {
|
||||
/** 查询租户信息列表 */
|
||||
|
|
@ -248,6 +265,18 @@ export default {
|
|||
this.loading = false
|
||||
})
|
||||
},
|
||||
/** 查询套餐列表 */
|
||||
getPackageList() {
|
||||
listPackage({ status: '0' }).then(response => {
|
||||
this.packageList = response.rows
|
||||
})
|
||||
},
|
||||
/** 根据套餐ID获取套餐名称 */
|
||||
getPackageName(packageId) {
|
||||
if (!packageId) return '-'
|
||||
const pkg = this.packageList.find(p => p.packageId === packageId)
|
||||
return pkg ? pkg.packageName : '-'
|
||||
},
|
||||
// 取消按钮
|
||||
cancel() {
|
||||
this.open = false
|
||||
|
|
|
|||
|
|
@ -0,0 +1,120 @@
|
|||
-- ============================================
|
||||
-- 若依多租户 - 唯一索引改造脚本
|
||||
-- 版本: 1.0
|
||||
-- 日期: 2025-12-19
|
||||
-- 警告: 执行前务必备份数据库!
|
||||
-- ============================================
|
||||
|
||||
-- ============================================
|
||||
-- 第一步:检查是否存在重复数据(必须先执行)
|
||||
-- ============================================
|
||||
|
||||
-- 检查 sys_user 表重复数据
|
||||
SELECT tenant_id, user_name, COUNT(*)
|
||||
FROM sys_user
|
||||
WHERE del_flag = '0'
|
||||
GROUP BY tenant_id, user_name
|
||||
HAVING COUNT(*) > 1;
|
||||
-- 如有输出,需先处理重复数据
|
||||
|
||||
-- 检查 sys_role 表重复数据
|
||||
SELECT tenant_id, role_key, COUNT(*)
|
||||
FROM sys_role
|
||||
WHERE del_flag = '0'
|
||||
GROUP BY tenant_id, role_key
|
||||
HAVING COUNT(*) > 1;
|
||||
-- 如有输出,需先处理重复数据
|
||||
|
||||
-- 检查 sys_post 表重复数据
|
||||
-- 注意:sys_post 表没有 del_flag 字段
|
||||
SELECT tenant_id, post_code, COUNT(*)
|
||||
FROM sys_post
|
||||
GROUP BY tenant_id, post_code
|
||||
HAVING COUNT(*) > 1;
|
||||
-- 如有输出,需先处理重复数据
|
||||
|
||||
-- ============================================
|
||||
-- 第二步:执行唯一索引改造
|
||||
-- ============================================
|
||||
|
||||
-- 1. sys_user表:用户名改为租户内唯一
|
||||
-- 说明:不同租户可以有同名用户(如都有 admin)
|
||||
-- 安全删除旧索引(兼容MySQL 5.7)
|
||||
SET @index_exists = (SELECT COUNT(*) FROM information_schema.statistics
|
||||
WHERE table_schema = DATABASE() AND table_name = 'sys_user' AND index_name = 'uk_user_name');
|
||||
SET @sql = IF(@index_exists > 0, 'ALTER TABLE sys_user DROP INDEX uk_user_name', 'SELECT 1');
|
||||
PREPARE stmt FROM @sql;
|
||||
EXECUTE stmt;
|
||||
DEALLOCATE PREPARE stmt;
|
||||
-- 创建新的联合唯一索引
|
||||
ALTER TABLE sys_user ADD UNIQUE KEY uk_tenant_user_name (tenant_id, user_name);
|
||||
|
||||
-- 2. sys_role表:角色标识改为租户内唯一
|
||||
-- 说明:不同租户可以有同标识角色(如都有 role_admin)
|
||||
SET @index_exists = (SELECT COUNT(*) FROM information_schema.statistics
|
||||
WHERE table_schema = DATABASE() AND table_name = 'sys_role' AND index_name = 'uk_role_key');
|
||||
SET @sql = IF(@index_exists > 0, 'ALTER TABLE sys_role DROP INDEX uk_role_key', 'SELECT 1');
|
||||
PREPARE stmt FROM @sql;
|
||||
EXECUTE stmt;
|
||||
DEALLOCATE PREPARE stmt;
|
||||
ALTER TABLE sys_role ADD UNIQUE KEY uk_tenant_role_key (tenant_id, role_key);
|
||||
|
||||
-- 3. sys_post表:岗位编码改为租户内唯一
|
||||
-- 说明:不同租户可以有同编码岗位(如都有 ceo)
|
||||
SET @index_exists = (SELECT COUNT(*) FROM information_schema.statistics
|
||||
WHERE table_schema = DATABASE() AND table_name = 'sys_post' AND index_name = 'uk_post_code');
|
||||
SET @sql = IF(@index_exists > 0, 'ALTER TABLE sys_post DROP INDEX uk_post_code', 'SELECT 1');
|
||||
PREPARE stmt FROM @sql;
|
||||
EXECUTE stmt;
|
||||
DEALLOCATE PREPARE stmt;
|
||||
ALTER TABLE sys_post ADD UNIQUE KEY uk_tenant_post_code (tenant_id, post_code);
|
||||
|
||||
-- ============================================
|
||||
-- 第三步:验证索引创建
|
||||
-- ============================================
|
||||
|
||||
-- 验证 sys_user 索引
|
||||
SHOW INDEX FROM sys_user WHERE Key_name = 'uk_tenant_user_name';
|
||||
|
||||
-- 验证 sys_role 索引
|
||||
SHOW INDEX FROM sys_role WHERE Key_name = 'uk_tenant_role_key';
|
||||
|
||||
-- 验证 sys_post 索引
|
||||
SHOW INDEX FROM sys_post WHERE Key_name = 'uk_tenant_post_code';
|
||||
|
||||
-- ============================================
|
||||
-- 执行结果说明
|
||||
-- ============================================
|
||||
-- 每个表应该显示索引的两条记录(tenant_id 和对应的业务字段)
|
||||
-- Seq_in_index = 1: tenant_id
|
||||
-- Seq_in_index = 2: user_name / role_key / post_code
|
||||
|
||||
-- ============================================
|
||||
-- 回滚脚本(仅供紧急情况使用)
|
||||
-- ============================================
|
||||
/*
|
||||
-- 警告:回滚将恢复到单租户模式,可能导致数据冲突!
|
||||
|
||||
-- 回滚 sys_user 索引
|
||||
ALTER TABLE sys_user DROP INDEX IF EXISTS uk_tenant_user_name;
|
||||
ALTER TABLE sys_user ADD UNIQUE KEY uk_user_name (user_name);
|
||||
|
||||
-- 回滚 sys_role 索引
|
||||
ALTER TABLE sys_role DROP INDEX IF EXISTS uk_tenant_role_key;
|
||||
ALTER TABLE sys_role ADD UNIQUE KEY uk_role_key (role_key);
|
||||
|
||||
-- 回滚 sys_post 索引
|
||||
ALTER TABLE sys_post DROP INDEX IF EXISTS uk_tenant_post_code;
|
||||
ALTER TABLE sys_post ADD UNIQUE KEY uk_post_code (post_code);
|
||||
*/
|
||||
|
||||
-- ============================================
|
||||
-- 执行注意事项
|
||||
-- ============================================
|
||||
-- 1. 选择业务低峰期执行(预计停机10-30分钟)
|
||||
-- 2. 执行前完整备份数据库:mysqldump -u root -p database_name > backup.sql
|
||||
-- 3. 务必先执行第一步的重复数据检查
|
||||
-- 4. 如有重复数据,需先清理或合并
|
||||
-- 5. 大表索引创建较慢,耐心等待不要中断
|
||||
-- 6. 准备回滚脚本以备不时之需
|
||||
-- 7. 执行完毕后验证应用功能正常
|
||||
|
|
@ -0,0 +1,204 @@
|
|||
-- ============================================
|
||||
-- 租户套餐功能 - 数据库脚本
|
||||
-- 版本: 1.0
|
||||
-- 日期: 2025-12-19
|
||||
-- 说明: 实现租户套餐功能,通过套餐控制租户的菜单权限
|
||||
-- ============================================
|
||||
|
||||
-- ============================================
|
||||
-- 第一步:创建租户套餐表
|
||||
-- ============================================
|
||||
|
||||
CREATE TABLE IF NOT EXISTS sys_tenant_package (
|
||||
package_id BIGINT(20) NOT NULL AUTO_INCREMENT COMMENT '套餐ID',
|
||||
package_name VARCHAR(50) NOT NULL COMMENT '套餐名称',
|
||||
package_code VARCHAR(50) NOT NULL COMMENT '套餐编码',
|
||||
status CHAR(1) DEFAULT '0' COMMENT '状态(0正常 1停用)',
|
||||
del_flag CHAR(1) DEFAULT '0' COMMENT '删除标志(0存在 2删除)',
|
||||
create_by VARCHAR(64) DEFAULT '' COMMENT '创建者',
|
||||
create_time DATETIME DEFAULT NULL COMMENT '创建时间',
|
||||
update_by VARCHAR(64) DEFAULT '' COMMENT '更新者',
|
||||
update_time DATETIME DEFAULT NULL COMMENT '更新时间',
|
||||
remark VARCHAR(500) DEFAULT NULL COMMENT '备注',
|
||||
PRIMARY KEY (package_id),
|
||||
UNIQUE KEY uk_package_code (package_code)
|
||||
) ENGINE=InnoDB AUTO_INCREMENT=1 COMMENT='租户套餐表';
|
||||
|
||||
-- ============================================
|
||||
-- 第二步:创建套餐菜单关联表
|
||||
-- ============================================
|
||||
|
||||
CREATE TABLE IF NOT EXISTS sys_package_menu (
|
||||
package_id BIGINT(20) NOT NULL COMMENT '套餐ID',
|
||||
menu_id BIGINT(20) NOT NULL COMMENT '菜单ID',
|
||||
PRIMARY KEY (package_id, menu_id),
|
||||
INDEX idx_package_id (package_id) -- 为package_id添加索引(用户要求)
|
||||
) ENGINE=InnoDB COMMENT='套餐菜单关联表';
|
||||
|
||||
-- ============================================
|
||||
-- 第三步:修改sys_tenant表,增加套餐字段
|
||||
-- ============================================
|
||||
|
||||
-- 检查列是否存在,避免重复执行报错
|
||||
SELECT COUNT(*) INTO @col_exists
|
||||
FROM information_schema.columns
|
||||
WHERE table_schema = DATABASE()
|
||||
AND table_name = 'sys_tenant'
|
||||
AND column_name = 'package_id';
|
||||
|
||||
-- 只有列不存在时才添加
|
||||
SET @sql = IF(@col_exists = 0,
|
||||
'ALTER TABLE sys_tenant ADD COLUMN package_id BIGINT(20) DEFAULT NULL COMMENT ''套餐ID'' AFTER tenant_code',
|
||||
'SELECT ''Column package_id already exists'' AS message');
|
||||
PREPARE stmt FROM @sql;
|
||||
EXECUTE stmt;
|
||||
DEALLOCATE PREPARE stmt;
|
||||
|
||||
-- 添加索引(忽略已存在的情况)
|
||||
-- 注意:如果索引已存在会报错,可忽略该错误继续执行
|
||||
SET @index_exists = (SELECT COUNT(*) FROM information_schema.statistics
|
||||
WHERE table_schema = DATABASE() AND table_name = 'sys_tenant' AND index_name = 'idx_package_id');
|
||||
SET @sql = IF(@index_exists = 0, 'ALTER TABLE sys_tenant ADD INDEX idx_package_id (package_id)', 'SELECT 1');
|
||||
PREPARE stmt FROM @sql;
|
||||
EXECUTE stmt;
|
||||
DEALLOCATE PREPARE stmt;
|
||||
|
||||
-- ============================================
|
||||
-- 第四步:初始化套餐数据
|
||||
-- ============================================
|
||||
|
||||
INSERT INTO sys_tenant_package (package_name, package_code, status, create_by, create_time, remark) VALUES
|
||||
('基础套餐', 'BASIC', '0', 'admin', NOW(), '用户、部门管理'),
|
||||
('标准套餐', 'STANDARD', '0', 'admin', NOW(), '增加角色、菜单、字典管理'),
|
||||
('高级套餐', 'PREMIUM', '0', 'admin', NOW(), '全部功能,不限制菜单');
|
||||
|
||||
-- ============================================
|
||||
-- 第五步:初始化套餐菜单关联(基础套餐)
|
||||
-- ============================================
|
||||
|
||||
-- 基础套餐:只包含用户、部门、岗位、字典管理
|
||||
INSERT INTO sys_package_menu (package_id, menu_id) VALUES
|
||||
-- 系统管理模块
|
||||
(1, 1), -- 系统管理
|
||||
-- 菜单项
|
||||
(1, 100), -- 用户管理
|
||||
(1, 101), -- 部门管理
|
||||
(1, 103), -- 岗位管理
|
||||
(1, 104), -- 字典管理
|
||||
-- 用户管理按钮
|
||||
(1, 1000), (1, 1001), (1, 1002), (1, 1003), (1, 1004), (1, 1005), (1, 1006),
|
||||
-- 部门管理按钮
|
||||
(1, 1016), (1, 1017), (1, 1018), (1, 1019),
|
||||
-- 岗位管理按钮
|
||||
(1, 1020), (1, 1021), (1, 1022), (1, 1023), (1, 1024),
|
||||
-- 字典管理按钮
|
||||
(1, 1025), (1, 1026), (1, 1027), (1, 1028), (1, 1029);
|
||||
|
||||
-- ============================================
|
||||
-- 第六步:初始化套餐菜单关联(标准套餐)
|
||||
-- ============================================
|
||||
|
||||
-- 标准套餐:增加角色、菜单管理
|
||||
INSERT INTO sys_package_menu (package_id, menu_id) VALUES
|
||||
-- 系统管理模块
|
||||
(2, 1), -- 系统管理
|
||||
-- 菜单项
|
||||
(2, 100), (2, 101), (2, 102), (2, 103), (2, 104), (2, 105), -- 用户/部门/角色/岗位/字典/菜单
|
||||
-- 用户管理按钮
|
||||
(2, 1000), (2, 1001), (2, 1002), (2, 1003), (2, 1004), (2, 1005), (2, 1006),
|
||||
-- 角色管理按钮
|
||||
(2, 1007), (2, 1008), (2, 1009), (2, 1010), (2, 1011),
|
||||
-- 菜单管理按钮
|
||||
(2, 1012), (2, 1013), (2, 1014), (2, 1015),
|
||||
-- 部门管理按钮
|
||||
(2, 1016), (2, 1017), (2, 1018), (2, 1019),
|
||||
-- 岗位管理按钮
|
||||
(2, 1020), (2, 1021), (2, 1022), (2, 1023), (2, 1024),
|
||||
-- 字典管理按钮
|
||||
(2, 1025), (2, 1026), (2, 1027), (2, 1028), (2, 1029);
|
||||
|
||||
-- ============================================
|
||||
-- 第七步:高级套餐不插入关联数据
|
||||
-- ============================================
|
||||
|
||||
-- 说明:高级套餐 (package_id=3) 不插入 sys_package_menu 数据
|
||||
-- 代码逻辑:查询结果为空列表时,表示"不限制菜单",用户可以看到所有菜单
|
||||
|
||||
-- ============================================
|
||||
-- 第八步:关联现有租户到套餐
|
||||
-- ============================================
|
||||
|
||||
-- 根据实际租户情况调整(使用安全的更新方式)
|
||||
UPDATE sys_tenant SET package_id = 1 WHERE tenant_code = 'DEFAULT' AND package_id IS NULL;
|
||||
UPDATE sys_tenant SET package_id = 2 WHERE tenant_code = 'TENANT_A' AND package_id IS NULL;
|
||||
UPDATE sys_tenant SET package_id = 3 WHERE tenant_code = 'TENANT_B' AND package_id IS NULL;
|
||||
|
||||
-- ============================================
|
||||
-- 验证数据
|
||||
-- ============================================
|
||||
|
||||
-- 验证套餐表
|
||||
SELECT * FROM sys_tenant_package;
|
||||
|
||||
-- 验证套餐菜单关联(基础套餐)
|
||||
SELECT COUNT(*) AS '基础套餐菜单数' FROM sys_package_menu WHERE package_id = 1;
|
||||
|
||||
-- 验证套餐菜单关联(标准套餐)
|
||||
SELECT COUNT(*) AS '标准套餐菜单数' FROM sys_package_menu WHERE package_id = 2;
|
||||
|
||||
-- 验证套餐菜单关联(高级套餐)
|
||||
SELECT COUNT(*) AS '高级套餐菜单数(应为0)' FROM sys_package_menu WHERE package_id = 3;
|
||||
|
||||
-- 验证租户套餐关联
|
||||
SELECT tenant_id, tenant_name, tenant_code, package_id
|
||||
FROM sys_tenant
|
||||
WHERE package_id IS NOT NULL;
|
||||
|
||||
-- ============================================
|
||||
-- 回滚脚本(仅供紧急情况使用)
|
||||
-- ============================================
|
||||
|
||||
/*
|
||||
-- 警告:回滚将删除所有套餐数据和配置!
|
||||
|
||||
-- 1. 清除租户的套餐关联
|
||||
UPDATE sys_tenant SET package_id = NULL;
|
||||
|
||||
-- 2. 删除套餐菜单关联表
|
||||
DROP TABLE IF EXISTS sys_package_menu;
|
||||
|
||||
-- 3. 删除套餐表
|
||||
DROP TABLE IF EXISTS sys_tenant_package;
|
||||
|
||||
-- 4. 删除租户表的套餐字段
|
||||
ALTER TABLE sys_tenant DROP COLUMN IF EXISTS package_id;
|
||||
ALTER TABLE sys_tenant DROP INDEX IF EXISTS idx_package_id;
|
||||
*/
|
||||
|
||||
-- ============================================
|
||||
-- 使用说明
|
||||
-- ============================================
|
||||
|
||||
/*
|
||||
套餐机制说明:
|
||||
|
||||
1. **基础套餐 (BASIC)**
|
||||
- 适用于小型租户
|
||||
- 只包含基础的用户、部门、岗位、字典管理
|
||||
- 不能分配角色和菜单权限
|
||||
|
||||
2. **标准套餐 (STANDARD)**
|
||||
- 适用于中型租户
|
||||
- 包含基础功能 + 角色管理 + 菜单管理
|
||||
- 可以自定义角色和权限
|
||||
|
||||
3. **高级套餐 (PREMIUM)**
|
||||
- 适用于大型租户
|
||||
- 不限制任何菜单功能
|
||||
- sys_package_menu 表中无记录表示"全部菜单可见"
|
||||
|
||||
菜单过滤逻辑:
|
||||
- 用户最终可见菜单 = 角色菜单 ∩ 套餐菜单
|
||||
- 超级管理员不受套餐限制
|
||||
- 未配置套餐的租户默认不限制菜单
|
||||
*/
|
||||
|
|
@ -0,0 +1,32 @@
|
|||
-- ============================================
|
||||
-- 套餐管理菜单 - 插入脚本
|
||||
-- 版本: 1.0
|
||||
-- 日期: 2025-12-20
|
||||
-- ============================================
|
||||
|
||||
-- 1. 套餐管理菜单(放在系统管理下,parent_id = 1)
|
||||
INSERT INTO sys_menu (menu_id, menu_name, parent_id, order_num, path, component, query, route_name, is_frame, is_cache, menu_type, visible, status, perms, icon, create_by, create_time, update_by, update_time, remark)
|
||||
VALUES (2006, '套餐管理', 1, 10, 'package', 'system/package/index', NULL, 'Package', 1, 0, 'C', '0', '0', 'system:package:list', 'component', 'admin', NOW(), '', NULL, '租户套餐管理菜单');
|
||||
|
||||
-- 2. 套餐管理按钮权限
|
||||
INSERT INTO sys_menu (menu_id, menu_name, parent_id, order_num, path, component, query, route_name, is_frame, is_cache, menu_type, visible, status, perms, icon, create_by, create_time, update_by, update_time, remark)
|
||||
VALUES (2007, '套餐查询', 2006, 1, '', NULL, NULL, '', 1, 0, 'F', '0', '0', 'system:package:query', '#', 'admin', NOW(), '', NULL, '');
|
||||
|
||||
INSERT INTO sys_menu (menu_id, menu_name, parent_id, order_num, path, component, query, route_name, is_frame, is_cache, menu_type, visible, status, perms, icon, create_by, create_time, update_by, update_time, remark)
|
||||
VALUES (2008, '套餐新增', 2006, 2, '', NULL, NULL, '', 1, 0, 'F', '0', '0', 'system:package:add', '#', 'admin', NOW(), '', NULL, '');
|
||||
|
||||
INSERT INTO sys_menu (menu_id, menu_name, parent_id, order_num, path, component, query, route_name, is_frame, is_cache, menu_type, visible, status, perms, icon, create_by, create_time, update_by, update_time, remark)
|
||||
VALUES (2009, '套餐修改', 2006, 3, '', NULL, NULL, '', 1, 0, 'F', '0', '0', 'system:package:edit', '#', 'admin', NOW(), '', NULL, '');
|
||||
|
||||
INSERT INTO sys_menu (menu_id, menu_name, parent_id, order_num, path, component, query, route_name, is_frame, is_cache, menu_type, visible, status, perms, icon, create_by, create_time, update_by, update_time, remark)
|
||||
VALUES (2010, '套餐删除', 2006, 4, '', NULL, NULL, '', 1, 0, 'F', '0', '0', 'system:package:remove', '#', 'admin', NOW(), '', NULL, '');
|
||||
|
||||
-- 3. 给超级管理员角色分配权限(role_id = 1)
|
||||
INSERT INTO sys_role_menu (role_id, menu_id) VALUES (1, 2006);
|
||||
INSERT INTO sys_role_menu (role_id, menu_id) VALUES (1, 2007);
|
||||
INSERT INTO sys_role_menu (role_id, menu_id) VALUES (1, 2008);
|
||||
INSERT INTO sys_role_menu (role_id, menu_id) VALUES (1, 2009);
|
||||
INSERT INTO sys_role_menu (role_id, menu_id) VALUES (1, 2010);
|
||||
|
||||
-- 验证
|
||||
SELECT * FROM sys_menu WHERE menu_id >= 2006;
|
||||
Loading…
Reference in New Issue