变更名称: demo-environment-control
变更类型: 新功能开发
优先级: 高
影响范围: 全局API拦截、系统配置、前端交互
预计工作量: 1-2人日
Forge Admin系统需要对外提供演示环境,供潜在客户或用户试用体验。演示环境需要:
| 问题类别 | 具体问题 | 业务影响 |
|---|---|---|
| 数据安全 | 演示环境无保护,用户可随意修改/删除数据 | 演示环境数据混乱,影响其他用户体验 |
| 维护成本 | 需要定期手动恢复数据库 | 浪费人力,维护成本高 |
| 无法区分 | 系统无法识别是否为演示环境 | 无法实现环境级别的差异化控制 |
| 提示缺失 | 用户不知道是演示环境,操作失败无提示 | 用户体验差,误以为系统故障 |
需求描述:提供全局配置项,标识当前是否为演示环境。
业务价值:
功能规格:
forge.demo.enabled(默认false)forge.demo.message(自定义提示消息)forge.demo.whitelist-urls(白名单URL列表)需求描述:演示环境下拦截POST/PUT/DELETE请求,返回友好提示。
业务价值:
功能规格:
/auth/login(登录)/auth/logout(登出)/auth/online/**(在线用户查询)json
{
"code": 403,
"msg": "演示环境禁止修改操作",
"data": null
}
需求描述:前端在演示环境下显示明显的环境标识和提示。
业务价值:
功能规格:
需求描述:支持配置接口白名单,部分接口在演示环境也可写操作。
业务价值:
功能规格:
/auth/login/auth/logout/system/user/profile(个人设置)/system/user/updatePassword(修改密码)┌─────────────────────────────────────────────────────────┐
│ Demo Environment Control Architecture │
├─────────────────────────────────────────────────────────┤
│ │
│ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │
│ │ DemoConfig │ │ DemoIntercep-│ │ DemoAspect │ │
│ │ (配置类) │ │ tor(拦截器) │ │ (切面) │ │
│ └──────────────┘ └──────────────┘ └──────────────┘ │
│ │
│ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │
│ │ DemoAnnotat- │ │ DemoResponse │ │ Frontend │ │
│ │ ion(注解) │ │ Handler │ │ Indicator │ │
│ └──────────────┘ └──────────────┘ └──────────────┘ │
│ │
└─────────────────────────────────────────────────────────┘
@Data
@Component
@ConfigurationProperties(prefix = "forge.demo")
@RefreshScope
public class DemoProperties {
/**
* 是否启用演示模式
*/
private Boolean enabled = false;
/**
* 演示环境提示消息
*/
private String message = "演示环境禁止修改操作";
/**
* 白名单URL列表(支持通配符)
*/
private List<String> whitelistUrls = new ArrayList<>();
/**
* 拦截的HTTP方法
*/
private List<String> blockedMethods = Arrays.asList(
"POST", "PUT", "DELETE", "PATCH"
);
}
@Slf4j
@Component
@RequiredArgsConstructor
public class DemoInterceptor implements HandlerInterceptor {
private final DemoProperties demoProperties;
@Override
public boolean preHandle(HttpServletRequest request,
HttpServletResponse response,
Object handler) throws Exception {
// 未启用演示模式,放行
if (!demoProperties.getEnabled()) {
return true;
}
String method = request.getMethod();
String requestURI = request.getRequestURI();
// GET请求放行
if ("GET".equalsIgnoreCase(method)) {
return true;
}
// 白名单URL放行
if (isWhitelisted(requestURI)) {
return true;
}
// 拦截写操作
if (demoProperties.getBlockedMethods().contains(method.toUpperCase())) {
log.warn("演示环境拦截写操作: {} {}", method, requestURI);
response.setContentType("application/json;charset=UTF-8");
response.setStatus(HttpServletResponse.SC_FORBIDDEN);
RespInfo<Object> result = RespInfo.error(
HttpServletResponse.SC_FORBIDDEN,
demoProperties.getMessage()
);
response.getWriter().write(JSON.toJSONString(result));
return false;
}
return true;
}
private boolean isWhitelisted(String requestURI) {
return demoProperties.getWhitelistUrls().stream()
.anyMatch(pattern -> match(pattern, requestURI));
}
}
用于标记某个接口在演示环境也可访问:
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface IgnoreDemo {
/**
* 是否忽略演示环境限制
*/
boolean value() default true;
}
# application-demo.yml
forge:
demo:
# 启用演示模式
enabled: true
# 提示消息
message: "演示环境禁止修改操作,如需完整体验请联系管理员"
# 白名单URL(支持通配符)
whitelist-urls:
- /auth/login
- /auth/logout
- /auth/captcha/**
- /system/user/profile
- /system/user/updatePassword
- /system/config/getByKey/**
# 拦截的HTTP方法
blocked-methods:
- POST
- PUT
- DELETE
- PATCH
无需新增表,使用现有的 sys_config 表存储演示环境配置:
-- 演示环境配置
INSERT INTO sys_config (config_key, config_value, config_name, remark) VALUES
('demo.enabled', 'false', '演示环境开关', '是否启用演示环境模式'),
('demo.message', '演示环境禁止修改操作', '演示环境提示', '演示环境拦截提示消息'),
('demo.whitelist', '/auth/login,/auth/logout', '演示环境白名单', '演示环境白名单URL列表');
@GetMapping("/api/system/demo/status")
@SaIgnore
public RespInfo<Map<String, Object>> getDemoStatus() {
Map<String, Object> result = new HashMap<>();
result.put("enabled", demoProperties.getEnabled());
result.put("message", demoProperties.getMessage());
return RespInfo.success(result);
}
前端调用示例:
// 获取演示环境状态
const { data } = await request.get('/system/demo/status')
if (data.enabled) {
// 显示演示环境标识
showDemoIndicator(data.message)
// 禁用写操作按钮
disableWriteButtons()
}
在 App.vue 或 layout 中检测演示环境:
// stores/demo.js
export const useDemoStore = defineStore('demo', {
state: () => ({
enabled: false,
message: ''
}),
actions: {
async loadDemoStatus() {
try {
const { data } = await request.get('/system/demo/status')
this.enabled = data.enabled
this.message = data.message
} catch (error) {
console.error('获取演示环境状态失败', error)
}
}
}
})
<!-- Layout.vue -->
<template>
<div class="layout">
<!-- 演示环境提示条 -->
<n-alert
v-if="demoStore.enabled"
type="warning"
closable
class="demo-banner"
>
<template #header>
<n-icon><WarningOutlined /></n-icon>
演示环境
</template>
{{ demoStore.message }}
</n-alert>
<!-- 主内容 -->
<router-view />
</div>
</template>
<script setup>
import { useDemoStore } from '@/stores/demo'
const demoStore = useDemoStore()
onMounted(() => {
demoStore.loadDemoStatus()
})
</script>
<style scoped>
.demo-banner {
position: fixed;
top: 0;
left: 0;
right: 0;
z-index: 9999;
text-align: center;
background: #FFF7E6;
border-bottom: 1px solid #FFD591;
}
</style>
<!-- 使用自定义指令 -->
<template>
<n-button
v-demo-disabled
type="primary"
@click="handleAdd"
>
新增
</n-button>
</template>
<script setup>
// 自定义指令:演示环境禁用按钮
const vDemoDisabled = {
mounted(el, binding) {
const demoStore = useDemoStore()
if (demoStore.enabled) {
el.disabled = true
el.classList.add('demo-disabled')
el.title = demoStore.message
}
}
}
</script>
// utils/request.js
request.interceptors.response.use(
response => {
const { data } = response
// 演示环境拦截提示
if (data.code === 403 && data.msg?.includes('演示环境')) {
window.$message.warning(data.msg, { duration: 5000 })
}
return data
},
error => {
// ...
}
)
| 风险项 | 风险等级 | 影响分析 | 缓解措施 |
|---|---|---|---|
| 拦截器性能 | 低 | 每个请求都需要判断 | 简单判断逻辑,性能影响小 |
| 白名单配置错误 | 中 | 部分接口无法访问 | 提供默认白名单,文档说明 |
| 前端缓存 | 低 | 环境状态缓存导致不一致 | 定期刷新状态或监听配置变更 |
| 风险项 | 风险等级 | 影响分析 | 缓解措施 |
|---|---|---|---|
| 误拦截正常请求 | 中 | 影响用户体验 | 完善白名单配置,提供豁免机制 |
| 用户不知道是演示环境 | 低 | 用户困惑 | 前端明显标识,友好提示 |
| 配置泄露 | 中 | 生产环境误启用 | 配置项默认false,加强权限控制 |
| 风险项 | 风险等级 | 影响分析 | 缓解措施 |
|---|---|---|---|
| 现有接口无影响 | 低 | 仅新增功能 | 向后兼容 |
| 配置项迁移 | 低 | 需要配置演示环境 | 提供配置示例 |
测试用例清单:
| 测试类 | 核心测试方法 | 验证点 |
|---|---|---|
DemoInterceptorTest |
testBlockPostInDemoMode |
演示模式拦截POST请求 |
DemoInterceptorTest |
testAllowGetInDemoMode |
演示模式允许GET请求 |
DemoInterceptorTest |
testWhitelistInDemoMode |
白名单URL放行 |
DemoInterceptorTest |
testDisabledDemoMode |
关闭演示模式全放行 |
DemoPropertiesTest |
testDefaultConfig |
默认配置验证 |
测试场景:
演示环境拦截测试
生产环境放行测试
前端提示测试
| 任务ID | 任务描述 | 预估工时 | 优先级 | 涉及文件 |
|---|---|---|---|---|
| T1 | 创建DemoProperties配置类 | 0.5小时 | P0 | DemoProperties.java |
| T2 | 实现DemoInterceptor拦截器 | 1小时 | P0 | DemoInterceptor.java |
| T3 | 注册拦截器到WebMvcConfigurer | 0.5小时 | P0 | WebMvcConfig.java |
| T4 | 创建@IgnoreDemo注解(可选) | 0.5小时 | P1 | IgnoreDemo.java |
| T5 | 实现前端环境检测逻辑 | 1小时 | P0 | demo.js, Layout.vue |
| T6 | 实现前端UI提示组件 | 1小时 | P0 | DemoBanner.vue |
| T7 | 实现请求拦截和提示 | 0.5小时 | P0 | request.js |
| T8 | 编写单元测试 | 1小时 | P1 | 测试类 |
| T9 | 编写集成测试 | 0.5小时 | P2 | 测试类 |
| T10 | 编写配置文档 | 0.5小时 | P1 | README.md |
总工时: 7小时(约1人日)
建议团队: 1人
| 问题ID | 问题描述 | 影响范围 | 建议方案 |
|---|---|---|---|
| Q1 | 白名单配置放在配置文件还是数据库? | 灵活性 | 建议配置文件,支持动态刷新 |
| Q2 | 是否需要@IgnoreDemo注解? | 扩展性 | 建议实现,提高灵活性 |
| Q3 | 前端提示方式:全局提示条还是对话框? | 用户体验 | 建议全局提示条,不打断用户 |
| Q4 | 是否需要记录拦截日志? | 审计 | 建议记录,方便排查问题 |
| Q5 | 是否需要动态切换演示模式? | 运维 | 建议支持,通过配置中心 |
| 问题ID | 问题描述 | 影响范围 | 建议方案 |
|---|---|---|---|
| Q6 | 是否限制文件上传? | 安全性 | 建议拦截POST,自然限制上传 |
| Q7 | 是否限制导出功能? | 数据安全 | 建议不限制(GET请求) |
| Q8 | 个人设置是否允许修改? | 用户体验 | 建议加入白名单 |
| Q9 | 是否显示被拦截的次数统计? | 运营分析 | 可作为后续优化项 |
| 验收项 | 验收标准 | 验证方法 |
|---|---|---|
| 配置管理 | 演示模式开关正常 | 修改配置,验证生效 |
| GET请求 | 演示环境GET请求正常 | 访问查询接口 |
| POST拦截 | 演示环境POST请求被拦截 | 访问新增接口 |
| 白名单 | 白名单接口正常访问 | 访问登录接口 |
| 前端提示 | 显示演示环境标识 | 检查页面显示 |
| 友好提示 | 操作失败提示友好 | 触发拦截操作 |
文档版本: v1.0
编写日期: 2026-04-08
编写人: AI Assistant
审核状态: 待用户审核