feat(interactive): add participatory verification lab for debugging and repair validation
This commit is contained in:
@@ -0,0 +1,125 @@
|
||||
package com.example.scaffold.controller;
|
||||
|
||||
import com.example.scaffold.mapper.ProductMapper;
|
||||
import com.example.scaffold.service.UserService;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.jdbc.core.JdbcTemplate;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
import java.time.Instant;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.Map;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/api/verify")
|
||||
public class VerificationController {
|
||||
|
||||
private final JdbcTemplate jdbcTemplate;
|
||||
private final UserService userService;
|
||||
private final ProductMapper productMapper;
|
||||
|
||||
@Value("${spring.profiles.active:learn}")
|
||||
private String activeProfile;
|
||||
|
||||
@Value("${auth.type:none}")
|
||||
private String authType;
|
||||
|
||||
@Value("${spring.datasource.url:unknown}")
|
||||
private String datasourceUrl;
|
||||
|
||||
public VerificationController(JdbcTemplate jdbcTemplate, UserService userService, ProductMapper productMapper) {
|
||||
this.jdbcTemplate = jdbcTemplate;
|
||||
this.userService = userService;
|
||||
this.productMapper = productMapper;
|
||||
}
|
||||
|
||||
@GetMapping("/overview")
|
||||
public Map<String, Object> overview() {
|
||||
Map<String, Object> result = new LinkedHashMap<>();
|
||||
result.put("timestamp", Instant.now().toString());
|
||||
result.put("profile", activeProfile);
|
||||
result.put("authType", authType);
|
||||
result.put("datasourceUrl", datasourceUrl);
|
||||
result.put("checks", Map.of(
|
||||
"database", checkDatabase(),
|
||||
"users", checkUsers(),
|
||||
"products", checkProducts(),
|
||||
"h2Console", checkH2ConsoleHint(),
|
||||
"auth", checkAuthMode()
|
||||
));
|
||||
return result;
|
||||
}
|
||||
|
||||
@GetMapping("/database")
|
||||
public Map<String, Object> checkDatabase() {
|
||||
Map<String, Object> r = new LinkedHashMap<>();
|
||||
try {
|
||||
Integer one = jdbcTemplate.queryForObject("SELECT 1", Integer.class);
|
||||
r.put("ok", one != null && one == 1);
|
||||
r.put("message", "数据库连接正常,SELECT 1 返回成功");
|
||||
} catch (Exception e) {
|
||||
r.put("ok", false);
|
||||
r.put("message", "数据库连接失败: " + e.getMessage());
|
||||
}
|
||||
r.put("hint", "如果这里失败,优先检查 DataSource、H2 文件锁或 profile 配置");
|
||||
return r;
|
||||
}
|
||||
|
||||
@GetMapping("/users")
|
||||
public Map<String, Object> checkUsers() {
|
||||
Map<String, Object> r = new LinkedHashMap<>();
|
||||
try {
|
||||
long count = userService.count();
|
||||
r.put("ok", true);
|
||||
r.put("count", count);
|
||||
r.put("message", "用户服务可用");
|
||||
} catch (Exception e) {
|
||||
r.put("ok", false);
|
||||
r.put("message", "用户服务异常: " + e.getMessage());
|
||||
}
|
||||
r.put("hint", "如果用户接口异常,说明 Service / JPA / 数据源链路可能有问题");
|
||||
return r;
|
||||
}
|
||||
|
||||
@GetMapping("/products")
|
||||
public Map<String, Object> checkProducts() {
|
||||
Map<String, Object> r = new LinkedHashMap<>();
|
||||
try {
|
||||
int size = productMapper.findAll().size();
|
||||
r.put("ok", true);
|
||||
r.put("count", size);
|
||||
r.put("message", "MyBatis 产品查询可用");
|
||||
} catch (Exception e) {
|
||||
r.put("ok", false);
|
||||
r.put("message", "MyBatis 查询异常: " + e.getMessage());
|
||||
}
|
||||
r.put("hint", "如果这里失败但 users 正常,优先排查 Mapper / SQL / MyBatis 配置");
|
||||
return r;
|
||||
}
|
||||
|
||||
@GetMapping("/h2")
|
||||
public Map<String, Object> checkH2ConsoleHint() {
|
||||
Map<String, Object> r = new LinkedHashMap<>();
|
||||
r.put("ok", true);
|
||||
r.put("consolePath", "/h2-console");
|
||||
r.put("expectedJdbc", datasourceUrl);
|
||||
r.put("message", "H2 控制台入口已暴露;若跳转异常,优先检查 X-Forwarded-Proto / forward-headers 配置");
|
||||
return r;
|
||||
}
|
||||
|
||||
@GetMapping("/auth")
|
||||
public Map<String, Object> checkAuthMode() {
|
||||
Map<String, Object> r = new LinkedHashMap<>();
|
||||
r.put("ok", true);
|
||||
r.put("authType", authType);
|
||||
r.put("profile", activeProfile);
|
||||
r.put("message", switch (authType) {
|
||||
case "jwt" -> "当前是 JWT 模式,更适合验证 token / 401 / 403 链路";
|
||||
case "satoken" -> "当前是 Sa-Token 模式,可测试登录态与会话行为";
|
||||
default -> "当前是 none 模式,适合先学 Spring 核心功能";
|
||||
});
|
||||
return r;
|
||||
}
|
||||
}
|
||||
@@ -73,6 +73,7 @@
|
||||
<div id="profileBanner" class="profile-banner">正在读取 profile...</div>
|
||||
<div class="tools">
|
||||
<button class="btn btn-primary" onclick="copyCurl()">复制当前示例 cURL(GET /api/users)</button>
|
||||
<a class="btn btn-primary" href="verify-lab.html">进入修复验证实验室</a>
|
||||
</div>
|
||||
|
||||
<div class="tabs">
|
||||
|
||||
@@ -33,6 +33,7 @@
|
||||
<div class="nav">
|
||||
<a href="index.html">🏠 首页</a>
|
||||
<a href="advanced.html">🚀 高级功能</a>
|
||||
<a href="verify-lab.html">🩺 修复验证实验室</a>
|
||||
<a href="api.html">🔌 API 测试</a>
|
||||
<a href="auth-lab.html" class="active">🔐 鉴权实验室</a>
|
||||
</div>
|
||||
|
||||
@@ -99,6 +99,7 @@
|
||||
<a href="users.html">👥 用户管理</a>
|
||||
<a href="advanced.html">🚀 高级功能</a>
|
||||
<a href="auth-lab.html">🔐 鉴权实验室</a>
|
||||
<a href="verify-lab.html">🩺 修复验证实验室</a>
|
||||
<a href="api.html">🔌 API 测试</a>
|
||||
</div>
|
||||
|
||||
@@ -188,6 +189,18 @@
|
||||
<a href="auth-lab.html" class="btn">开始实验 →</a>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<h3>🩺 修复验证实验室</h3>
|
||||
<p>自己点检查,不靠口头确认。直接验证 profile、H2、用户服务、MyBatis 和鉴权模式。</p>
|
||||
<ul class="feature-list">
|
||||
<li><span class="icon">✅</span>一键总览检查</li>
|
||||
<li><span class="icon">✅</span>数据库链路验证</li>
|
||||
<li><span class="icon">✅</span>用户服务验证</li>
|
||||
<li><span class="icon">✅</span>MyBatis 查询验证</li>
|
||||
</ul>
|
||||
<a href="verify-lab.html" class="btn">开始验证 →</a>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<h3>🔌 API 测试面板</h3>
|
||||
<p>在线测试所有 API 接口,查看请求响应,理解 RESTful API 工作原理。</p>
|
||||
|
||||
120
src/main/resources/static/verify-lab.html
Normal file
120
src/main/resources/static/verify-lab.html
Normal file
@@ -0,0 +1,120 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>修复验证实验室 - Spring Boot</title>
|
||||
<style>
|
||||
* { margin:0; padding:0; box-sizing:border-box; }
|
||||
body { font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,sans-serif; background:#f5f5f5; }
|
||||
.container { max-width:1200px; margin:0 auto; padding:20px; }
|
||||
.header { background:linear-gradient(135deg,#667eea,#764ba2); color:#fff; padding:28px 20px; border-radius:12px; text-align:center; margin-bottom:20px; }
|
||||
.nav { display:flex; gap:10px; flex-wrap:wrap; justify-content:center; margin-bottom:20px; }
|
||||
.nav a { padding:10px 18px; background:#fff; border-radius:20px; text-decoration:none; color:#333; }
|
||||
.nav a.active { background:#667eea; color:#fff; }
|
||||
.lab { background:#fff7e6; border-left:4px solid #fa8c16; padding:16px; border-radius:8px; margin-bottom:20px; }
|
||||
.grid { display:grid; grid-template-columns: repeat(auto-fit, minmax(320px, 1fr)); gap:16px; }
|
||||
.card { background:#fff; border-radius:12px; padding:18px; box-shadow:0 2px 10px rgba(0,0,0,.08); }
|
||||
.card h3 { margin-bottom:12px; color:#333; }
|
||||
.result { background:#111827; color:#d1d5db; padding:12px; border-radius:8px; min-height:160px; white-space:pre-wrap; font-family:monospace; margin-top:10px; }
|
||||
button { padding:10px 14px; background:#667eea; color:#fff; border:none; border-radius:6px; cursor:pointer; margin-right:8px; margin-bottom:8px; }
|
||||
.status-pass { color:#16a34a; font-weight:700; }
|
||||
.status-fail { color:#dc2626; font-weight:700; }
|
||||
.hint { color:#666; line-height:1.7; margin-top:8px; }
|
||||
.summary { background:#fff; border-radius:12px; padding:18px; margin-bottom:20px; box-shadow:0 2px 10px rgba(0,0,0,.08); }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<div class="header">
|
||||
<h1>🩺 修复验证实验室</h1>
|
||||
<p>不是“我说修好了”,而是你自己点检查,看到哪里坏、为什么坏、修没修好。</p>
|
||||
</div>
|
||||
|
||||
<div class="nav">
|
||||
<a href="index.html">🏠 首页</a>
|
||||
<a href="auth-lab.html">🔐 鉴权实验室</a>
|
||||
<a href="api.html">🔌 API 测试</a>
|
||||
<a href="verify-lab.html" class="active">🩺 修复验证实验室</a>
|
||||
</div>
|
||||
|
||||
<div class="lab">
|
||||
<strong>参与式验证任务卡</strong>
|
||||
<ul style="padding-left:20px;line-height:1.8;margin-top:8px;">
|
||||
<li>先点“一键跑总览”,确认当前 profile / auth / datasource 状态</li>
|
||||
<li>再分别点数据库、用户服务、MyBatis 产品查询,感受“哪个环节坏了就在哪一步暴露”</li>
|
||||
<li>最后打开 H2 Console,验证修复后跳转是不是仍保持 https</li>
|
||||
<li>如果某一步失败,不要只看失败,要看返回里的 hint —— 那是排查方向</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="summary">
|
||||
<button onclick="runOverview()">一键跑总览</button>
|
||||
<button onclick="openH2()">打开 H2 Console</button>
|
||||
<div id="summaryResult" class="result">点击按钮开始验证...</div>
|
||||
</div>
|
||||
|
||||
<div class="grid">
|
||||
<div class="card">
|
||||
<h3>数据库链路</h3>
|
||||
<button onclick="runCheck('database','dbResult')">检查数据库</button>
|
||||
<div class="hint">验证 DataSource / JDBC / H2 是否可用。</div>
|
||||
<div id="dbResult" class="result"></div>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<h3>用户服务链路</h3>
|
||||
<button onclick="runCheck('users','userResult')">检查用户服务</button>
|
||||
<div class="hint">验证 JPA / Service / 用户数据查询是否正常。</div>
|
||||
<div id="userResult" class="result"></div>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<h3>MyBatis 链路</h3>
|
||||
<button onclick="runCheck('products','productResult')">检查产品查询</button>
|
||||
<div class="hint">验证 MyBatis Mapper / SQL / 数据库查询是否正常。</div>
|
||||
<div id="productResult" class="result"></div>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<h3>鉴权模式</h3>
|
||||
<button onclick="runCheck('auth','authResult')">检查鉴权模式</button>
|
||||
<div class="hint">对照当前 profile,理解 why learn=none / advanced=jwt。</div>
|
||||
<div id="authResult" class="result"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<script>
|
||||
async function runOverview() {
|
||||
const el = document.getElementById('summaryResult');
|
||||
el.textContent = '检查中...';
|
||||
try {
|
||||
const res = await fetch('/api/verify/overview');
|
||||
const data = await res.json();
|
||||
el.textContent = JSON.stringify(data, null, 2);
|
||||
} catch (e) {
|
||||
el.textContent = '总览检查失败: ' + e.message;
|
||||
}
|
||||
}
|
||||
|
||||
async function runCheck(name, resultId) {
|
||||
const el = document.getElementById(resultId);
|
||||
el.textContent = '检查中...';
|
||||
try {
|
||||
const res = await fetch('/api/verify/' + name);
|
||||
const data = await res.json();
|
||||
const badge = data.ok ? '✅ PASS' : '❌ FAIL';
|
||||
el.textContent = badge + '\n\n' + JSON.stringify(data, null, 2);
|
||||
} catch (e) {
|
||||
el.textContent = '检查失败: ' + e.message;
|
||||
}
|
||||
}
|
||||
|
||||
function openH2() {
|
||||
window.open('/h2-console', '_blank');
|
||||
}
|
||||
|
||||
runOverview();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
Reference in New Issue
Block a user