Update: cache module, security modules and build

This commit is contained in:
likingcode
2026-03-18 15:18:41 +08:00
parent 4a0737ddeb
commit d257e3595d
48 changed files with 2076 additions and 822 deletions

View File

@@ -0,0 +1,46 @@
# 高级配置 - 可插拔组件
spring:
config:
activate:
on-profile: advanced
# 数据库选择: h2 / mysql / postgresql
datasource:
driver-class-name: ${DB_DRIVER:org.h2.Driver}
url: ${DB_URL:jdbc:h2:file:~/h2/springboot_scaffold}
username: ${DB_USER:sa}
password: ${DB_PASS:}
# Redis 缓存
redis:
host: ${REDIS_HOST:localhost}
port: ${REDIS_PORT:6379}
password: ${REDIS_PASS:}
database: ${REDIS_DB:0}
timeout: 3000ms
lettuce:
pool:
max-active: 8
max-idle: 8
min-idle: 0
# 鉴权方案选择: none / jwt / satoken
auth:
type: ${AUTH_TYPE:jwt} # none | jwt | satoken
jwt:
secret: ${JWT_SECRET:your-secret-key}
expiration: ${JWT_EXPIRATION:86400000} # 24小时
satoken:
timeout: ${SA_TIMEOUT:86400} # 24小时
activity-timeout: ${SA_ACTIVITY_TIMEOUT:1800} # 30分钟
# 缓存配置
cache:
type: ${CACHE_TYPE:caffeine} # caffeine | redis
redis:
time-to-live: 600000 # 10分钟
# MyBatis 多数据库适配
mybatis:
configuration:
database-id: ${DB_TYPE:h2} # h2 | mysql | postgresql

View File

@@ -0,0 +1,29 @@
# Learn profile: keep dependencies and runtime simple for first-round learning
spring:
config:
activate:
on-profile: learn
datasource:
url: jdbc:h2:mem:springboot_scaffold_learn;MODE=MYSQL;DB_CLOSE_DELAY=-1;DB_CLOSE_ON_EXIT=FALSE
driver-class-name: org.h2.Driver
username: sa
password:
h2:
console:
enabled: true
app:
profile: learn
enabled-modules:
- ioc
- aop
- mybatis
- transaction
- users
auth:
type: none
learning-note: "learn 模式默认不开启鉴权,专注 Spring 核心学习"
cache:
type: caffeine

View File

@@ -1,7 +1,10 @@
spring.application.name=springboot-scaffold
server.port=8082
server.port=8083
server.forward-headers-strategy=framework
# H2 Database
spring.profiles.active=${APP_PROFILE:learn}
# H2 Database (default baseline; learn profile overrides to in-memory)
spring.h2.console.enabled=true
spring.datasource.url=jdbc:h2:file:~/h2/springboot_scaffold
spring.datasource.driverClassName=org.h2.Driver

Binary file not shown.

Binary file not shown.

View File

@@ -0,0 +1,260 @@
<!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: 1400px; margin: 0 auto; padding: 20px; }
.header { background: linear-gradient(135deg, #f093fb 0%, #f5576c 100%); color: white; padding: 30px 20px; text-align: center; margin-bottom: 20px; border-radius: 10px; }
.header h1 { font-size: 2em; }
.nav { display: flex; gap: 10px; margin-bottom: 20px; flex-wrap: wrap; justify-content: center; }
.nav a { padding: 10px 20px; background: white; border-radius: 20px; text-decoration: none; color: #333; font-size: 0.9em; }
.nav a:hover, .nav a.active { background: #f5576c; color: white; }
.card { background: white; border-radius: 10px; padding: 20px; margin-bottom: 20px; box-shadow: 0 2px 10px rgba(0,0,0,0.08); }
.card h3 { color: #f5576c; margin-bottom: 15px; border-bottom: 2px solid #eee; padding-bottom: 10px; }
.config-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(250px, 1fr)); gap: 15px; }
.config-item { background: #f8f9fa; padding: 15px; border-radius: 8px; border-left: 4px solid #f5576c; }
.config-item h4 { margin-bottom: 10px; }
.config-item .current { color: #28a745; font-weight: bold; }
.btn { padding: 10px 20px; background: #f5576c; color: white; border: none; border-radius: 5px; cursor: pointer; margin: 5px; }
.btn:hover { background: #e0465b; }
.btn-secondary { background: #6c757d; }
.result { background: #1e1e1e; color: #d4d4d4; padding: 15px; border-radius: 5px; margin-top: 10px; font-family: monospace; font-size: 0.9em; overflow-x: auto; max-height: 400px; overflow-y: auto; }
.compare-table { width: 100%; border-collapse: collapse; margin: 15px 0; }
.compare-table th, .compare-table td { padding: 12px; text-align: left; border-bottom: 1px solid #eee; }
.compare-table th { background: #f8f9fa; }
.compare-table tr:hover { background: #f8f9fa; }
.problem-box { background: #fff3cd; border-left: 4px solid #ffc107; padding: 15px; margin: 10px 0; border-radius: 5px; }
.problem-box h4 { color: #856404; margin-bottom: 8px; }
.redis-types { display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 10px; }
.redis-type { background: #e3f2fd; padding: 15px; border-radius: 8px; }
.redis-type h4 { color: #1976d2; margin-bottom: 5px; }
.tipbox { background:#fff7e6;border-left:4px solid #fa8c16;padding:15px;border-radius:8px;margin-bottom:20px; }
.tipbox h4 { color:#ad6800;margin-bottom:8px; }
</style>
</head>
<body>
<div class="container">
<div class="header">
<h1>🚀 高级功能学习</h1>
<p>Redis 缓存 | 分布式锁 | 多数据库 | 认证方案</p>
</div>
<div class="nav">
<a href="index.html">🏠 首页</a>
<a href="ioc.html">📦 IoC</a>
<a href="aop.html">🔪 AOP</a>
<a href="mybatis.html">💾 MyBatis</a>
<a href="transaction.html">🔄 事务</a>
<a href="users.html">👥 用户</a>
<a href="advanced.html" class="active">🚀 高级</a>
<a href="auth-lab.html">🔐 鉴权实验室</a>
<a href="api.html">🔌 API</a>
</div>
<div class="tipbox">
<h4>🧪 实验任务卡(高级模块)</h4>
<label style="display:block;margin-bottom:8px;"><input id="advancedTaskDone" type="checkbox" onchange="toggleAdvancedTaskDone(this)"> 本任务我已经完成</label>
<ul style="padding-left:20px;line-height:1.8;">
<li>目标:比较 learn/advanced profile 下可用能力差异</li>
<li>步骤1先查看“系统配置”和“认证方案对比”</li>
<li>步骤2执行 Redis SET/GET + 分布式锁接口</li>
<li>预期advanced 模式下功能更完整,返回字段更丰富</li>
<li>常见坑:本机无 Redis 导致接口失败(属于环境问题)</li>
</ul>
</div>
<div class="card">
<h3>⚙️ 系统配置</h3>
<button class="btn" onclick="loadConfig()">查看当前配置</button>
<div id="configResult" class="result"></div>
</div>
<div class="card">
<h3>🔐 认证方案对比</h3>
<button class="btn" onclick="loadAuthCompare()">JWT vs Sa-Token</button>
<a class="btn btn-secondary" href="auth-lab.html">进入鉴权实验室</a>
<div id="authResult" class="result"></div>
</div>
<div class="card">
<h3>💾 Redis 数据类型</h3>
<div class="redis-types">
<div class="redis-type"><h4>String</h4><p>字符串,最基本类型</p></div>
<div class="redis-type"><h4>Hash</h4><p>哈希,存储对象</p></div>
<div class="redis-type"><h4>List</h4><p>列表,队列/栈</p></div>
<div class="redis-type"><h4>Set</h4><p>集合,去重运算</p></div>
<div class="redis-type"><h4>SortedSet</h4><p>有序集合,排名</p></div>
</div>
<button class="btn" onclick="loadRedisTypes()">查看详细说明</button>
</div>
<div class="card">
<h3>🧪 Redis 操作测试</h3>
<div style="display:flex;gap:10px;margin-bottom:15px;">
<input type="text" id="redisKey" placeholder="Key" style="flex:1;padding:10px;border:1px solid #ddd;border-radius:5px;">
<input type="text" id="redisValue" placeholder="Value" style="flex:1;padding:10px;border:1px solid #ddd;border-radius:5px;">
<input type="number" id="redisTtl" placeholder="TTL(秒)" value="60" style="width:100px;padding:10px;border:1px solid #ddd;border-radius:5px;">
</div>
<button class="btn" onclick="setRedisString()">SET</button>
<button class="btn btn-secondary" onclick="getRedisString()">GET</button>
<div id="redisResult" class="result"></div>
</div>
<div class="card">
<h3>🔒 分布式锁演示</h3>
<p>模拟多个请求竞争同一资源</p>
<input type="text" id="lockResource" placeholder="资源名称" value="order:1001" style="padding:10px;border:1px solid #ddd;border-radius:5px;margin-right:10px;">
<button class="btn" onclick="testDistributedLock()">获取分布式锁</button>
<div id="lockResult" class="result"></div>
</div>
<div class="card">
<h3>⚠️ 缓存三大问题</h3>
<div class="problem-box">
<h4>缓存穿透</h4>
<p><strong>问题:</strong>查询不存在的数据,每次都打到数据库</p>
<p><strong>解决:</strong>布隆过滤器 | 缓存空值</p>
</div>
<div class="problem-box">
<h4>缓存击穿</h4>
<p><strong>问题:</strong>热点key过期大量请求打到数据库</p>
<p><strong>解决:</strong>互斥锁 | 逻辑过期</p>
</div>
<div class="problem-box">
<h4>缓存雪崩</h4>
<p><strong>问题:</strong>大量key同时过期数据库压力激增</p>
<p><strong>解决:</strong>随机过期时间 | 多级缓存</p>
</div>
<button class="btn" onclick="loadCacheProblems()">查看详细方案</button>
<div id="cacheResult" class="result"></div>
</div>
</div>
<script>
const ADV_TASK_KEY = 'task.advanced.done';
function toggleAdvancedTaskDone(el) {
localStorage.setItem(ADV_TASK_KEY, el.checked ? '1' : '0');
}
function initAdvancedTaskState() {
const done = localStorage.getItem(ADV_TASK_KEY) === '1';
const checkbox = document.getElementById('advancedTaskDone');
if (checkbox) checkbox.checked = done;
}
async function loadConfig() {
const result = document.getElementById('configResult');
result.textContent = '加载中...';
try {
const res = await fetch('/api/learning/advanced/config');
const data = await res.json();
result.innerHTML = `<strong>系统配置</strong>\n\n${JSON.stringify(data, null, 2)}`;
} catch (e) {
result.textContent = '加载失败: ' + e.message;
}
}
async function loadAuthCompare() {
const result = document.getElementById('authResult');
result.textContent = '加载中...';
try {
const res = await fetch('/api/learning/advanced/auth/compare');
const data = await res.json();
result.innerHTML = `<strong>认证方案对比</strong>\n\n${JSON.stringify(data, null, 2)}`;
} catch (e) {
result.textContent = '加载失败: ' + e.message;
}
}
async function loadRedisTypes() {
const result = document.getElementById('redisResult');
try {
const res = await fetch('/api/learning/advanced/redis/types');
const data = await res.json();
result.innerHTML = `<strong>Redis 数据类型</strong>\n\n${JSON.stringify(data, null, 2)}`;
} catch (e) {
result.textContent = '加载失败: ' + e.message;
}
}
async function setRedisString() {
const key = document.getElementById('redisKey').value;
const value = document.getElementById('redisValue').value;
const ttl = document.getElementById('redisTtl').value;
if (!key || !value) {
alert('请输入 Key 和 Value');
return;
}
const result = document.getElementById('redisResult');
try {
const res = await fetch(`/api/learning/advanced/redis/string?key=${encodeURIComponent(key)}&value=${encodeURIComponent(value)}&ttl=${ttl}`, { method: 'POST' });
const data = await res.json();
result.innerHTML = `<strong>SET 结果</strong>\n\n${JSON.stringify(data, null, 2)}`;
} catch (e) {
result.textContent = '操作失败: ' + e.message;
}
}
async function getRedisString() {
const key = document.getElementById('redisKey').value;
if (!key) {
alert('请输入 Key');
return;
}
const result = document.getElementById('redisResult');
try {
const res = await fetch(`/api/learning/advanced/redis/string?key=${encodeURIComponent(key)}`);
const data = await res.json();
result.innerHTML = `<strong>GET 结果</strong>\n\n${JSON.stringify(data, null, 2)}`;
} catch (e) {
result.textContent = '操作失败: ' + e.message;
}
}
async function testDistributedLock() {
const resource = document.getElementById('lockResource').value;
const result = document.getElementById('lockResult');
result.textContent = '获取锁中...';
try {
const res = await fetch(`/api/learning/advanced/redis/lock?resource=${encodeURIComponent(resource)}`, { method: 'POST' });
const data = await res.json();
result.innerHTML = `<strong>分布式锁结果</strong>\n\n${JSON.stringify(data, null, 2)}`;
} catch (e) {
result.textContent = '操作失败: ' + e.message;
}
}
async function loadCacheProblems() {
const result = document.getElementById('cacheResult');
try {
const res = await fetch('/api/learning/advanced/cache/problems');
const data = await res.json();
result.innerHTML = `<strong>缓存问题解决方案</strong>\n\n${JSON.stringify(data, null, 2)}`;
} catch (e) {
result.textContent = '加载失败: ' + e.message;
}
}
loadConfig();
initAdvancedTaskState();
</script>
</body>
</html>

View File

@@ -49,6 +49,8 @@
.tab-content.active { display: block; }
.json-input { width: 100%; min-height: 100px; padding: 10px; border: 1px solid #ddd; border-radius: 5px; font-family: monospace; font-size: 0.9em; }
.profile-banner { background:#fff7e6;border-left:4px solid #fa8c16;padding:12px 14px;border-radius:8px;margin-bottom:18px; color:#874d00; }
.tools { margin: 10px 0 16px; }
</style>
</head>
<body>
@@ -68,6 +70,12 @@
<a href="api.html" class="active">🔌 API</a>
</div>
<div id="profileBanner" class="profile-banner">正在读取 profile...</div>
<div class="tools">
<button class="btn btn-primary" onclick="copyCurl()">复制当前示例 cURLGET /api/users</button>
<a class="btn btn-primary" href="verify-lab.html">进入修复验证实验室</a>
</div>
<div class="tabs">
<div class="tab active" onclick="switchTab('user')">👥 用户 API</div>
<div class="tab" onclick="switchTab('product')">📦 产品 API</div>
@@ -248,6 +256,27 @@
document.getElementById(tab + 'Tab').classList.add('active');
}
async function loadProfileBanner() {
try {
const res = await fetch('/api/profile');
const data = await res.json();
const enabled = (data.enabledModules || []).join(', ');
document.getElementById('profileBanner').textContent = `当前 profile: ${data.profile} | 鉴权模式: ${data.authType || 'none'} | 已启用模块: ${enabled}`;
} catch (e) {
document.getElementById('profileBanner').textContent = '当前 profile 读取失败,请检查 /api/profile';
}
}
async function copyCurl() {
const curl = `curl -X GET "${window.location.origin}/api/users"`;
try {
await navigator.clipboard.writeText(curl);
alert('已复制: ' + curl);
} catch (e) {
alert('复制失败,请手动复制: ' + curl);
}
}
async function testApi(method, url, body, resultId) {
const resultDiv = document.getElementById(resultId);
resultDiv.classList.add('show');
@@ -275,6 +304,8 @@
resultDiv.innerHTML = `<strong style="color:#ff6b6b;">Error</strong>\n\n${e.message}`;
}
}
loadProfileBanner();
</script>
</body>
</html>

View File

@@ -0,0 +1,142 @@
<!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: 1100px; margin: 0 auto; padding: 20px; }
.header { background: linear-gradient(135deg,#4facfe,#00f2fe); color:#fff; padding:28px 20px; border-radius:12px; margin-bottom:20px; text-align:center; }
.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:#4facfe; color:#fff; }
.card { background:#fff; border-radius:12px; padding:20px; margin-bottom:20px; box-shadow:0 2px 10px rgba(0,0,0,.08); }
.card h3 { color:#1890ff; margin-bottom:12px; }
.lab { background:#fff7e6; border-left:4px solid #fa8c16; padding:15px; border-radius:8px; margin-bottom:20px; }
.row { display:flex; gap:10px; flex-wrap:wrap; margin:10px 0; }
input, select { padding:10px; border:1px solid #ddd; border-radius:6px; min-width:200px; }
button { padding:10px 16px; background:#1890ff; color:#fff; border:none; border-radius:6px; cursor:pointer; }
button.secondary { background:#6c757d; }
.result { background:#111827; color:#d1d5db; padding:14px; border-radius:8px; min-height:120px; white-space:pre-wrap; font-family:monospace; }
.hint { color:#666; margin-top:8px; line-height:1.7; }
</style>
</head>
<body>
<div class="container">
<div class="header">
<h1>🔐 Spring 鉴权实验室</h1>
<p>登录 → 取 Token → 访问受保护接口 → 对比 learn/advanced 模式</p>
</div>
<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>
<div class="lab">
<strong>实验任务卡</strong>
<ul style="padding-left:20px; line-height:1.8; margin-top:8px;">
<li>步骤1点击“读取当前模式”确认现在是 learn / advanced</li>
<li>步骤2用 admin/admin123 登录,拿到 token</li>
<li>步骤3带上 token 调用当前实验接口,观察是否放行或返回 503/401</li>
<li>步骤4切换 auth.type 后对比 none/jwt/satoken 行为差异</li>
</ul>
</div>
<div class="card">
<h3>当前模式</h3>
<div class="row">
<button onclick="loadProfile()">读取当前模式</button>
</div>
<div id="profileResult" class="result">点击按钮读取...</div>
</div>
<div class="card">
<h3>登录拿 Token</h3>
<div class="row">
<input id="username" value="admin" placeholder="用户名">
<input id="password" value="admin123" placeholder="密码" type="password">
<button onclick="login()">登录</button>
</div>
<div class="hint">演示账号admin/admin123 或 user/user123</div>
<div id="loginResult" class="result">点击登录后显示响应...</div>
</div>
<div class="card">
<h3>接口访问实验</h3>
<div class="row">
<button onclick="callSecureApi()">访问 /api/auth/info</button>
<button class="secondary" onclick="clearToken()">清空本地 Token</button>
</div>
<div class="hint">当前页面会优先从 localStorage 读取 token 并自动加到 Authorization 头里。learn 模式下接口通常直接放行advanced+jwt 下更适合观察真实鉴权链路。</div>
<div id="secureResult" class="result">点击按钮后显示接口结果...</div>
</div>
</div>
<script>
let cachedToken = localStorage.getItem('spring.scaffold.token') || '';
async function loadProfile() {
const el = document.getElementById('profileResult');
el.textContent = '读取中...';
try {
const res = await fetch('/api/profile');
const data = await res.json();
el.textContent = JSON.stringify(data, null, 2);
} catch (e) {
el.textContent = '读取失败: ' + e.message;
}
}
async function login() {
const el = document.getElementById('loginResult');
el.textContent = '登录中...';
try {
const res = await fetch('/api/auth/login', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
username: document.getElementById('username').value,
password: document.getElementById('password').value,
})
});
const data = await res.json();
const token = data.token || data.data?.token || '';
if (token) {
cachedToken = token;
localStorage.setItem('spring.scaffold.token', token);
}
el.textContent = JSON.stringify(data, null, 2);
} catch (e) {
el.textContent = '登录失败: ' + e.message;
}
}
async function callSecureApi() {
const el = document.getElementById('secureResult');
el.textContent = '请求中...';
try {
const headers = {};
if (cachedToken) headers['Authorization'] = 'Bearer ' + cachedToken;
const res = await fetch('/api/auth/info', { headers });
const text = await res.text();
el.textContent = `HTTP ${res.status}\n\n${text}`;
} catch (e) {
el.textContent = '请求失败: ' + e.message;
}
}
function clearToken() {
cachedToken = '';
localStorage.removeItem('spring.scaffold.token');
document.getElementById('secureResult').textContent = '本地 token 已清空';
}
loadProfile();
</script>
</body>
</html>

View File

@@ -41,6 +41,9 @@
.status-item { background: white; padding: 20px 30px; border-radius: 10px; text-align: center; }
.status-item .value { font-size: 2em; font-weight: bold; color: #667eea; }
.status-item .label { color: #666; margin-top: 5px; }
.lab { background:#fff7e6; border:1px solid #ffe58f; border-radius:10px; padding:16px; margin-bottom:20px; }
.lab h4 { margin-bottom:8px; color:#ad6800; }
.lab ul { margin-left:18px; color:#444; line-height:1.7; }
footer { text-align: center; padding: 30px; color: #666; margin-top: 40px; }
</style>
@@ -69,6 +72,22 @@
<div class="value" id="orderCount">-</div>
<div class="label">订单数量</div>
</div>
<div class="status-item">
<div class="value" id="activeProfile">-</div>
<div class="label">当前 Profile</div>
</div>
</div>
<div class="lab">
<h4>🧪 实验任务卡(事务模块)</h4>
<p style="margin-bottom:8px;color:#8c8c8c;">鉴权学习建议learn=none先学核心advanced=jwt/satoken再学安全链路</p>
<ul>
<li>目标:理解事务回滚与 REQUIRES_NEW 差异</li>
<li>步骤1到 transaction.html 创建普通订单rollback=false</li>
<li>步骤2再创建模拟失败订单rollback=true</li>
<li>预期:主事务回滚,但独立事务可保留日志/部分数据(取决于实现)</li>
<li>观察点查看控制台事务日志TransactionInterceptor TRACE</li>
</ul>
</div>
<div class="nav">
@@ -78,6 +97,10 @@
<a href="mybatis.html">💾 MyBatis</a>
<a href="transaction.html">🔄 事务管理</a>
<a href="users.html">👥 用户管理</a>
<a href="advanced.html">🚀 高级功能</a>
<a href="reflection.html">🪞 反射实验室</a>
<a href="auth-lab.html">🔐 鉴权实验室</a>
<a href="verify-lab.html">🩺 修复验证实验室</a>
<a href="api.html">🔌 API 测试</a>
</div>
@@ -142,6 +165,55 @@
<a href="users.html" class="btn">开始学习 →</a>
</div>
<div class="card">
<h3>🚀 高级功能</h3>
<p>Redis 缓存、分布式锁、多数据库、认证方案对比,从小白到高手的进阶之路。</p>
<ul class="feature-list">
<li><span class="icon"></span>Redis 数据类型操作</li>
<li><span class="icon"></span>缓存穿透/击穿/雪崩</li>
<li><span class="icon"></span>分布式锁实现</li>
<li><span class="icon"></span>JWT vs Sa-Token</li>
<li><span class="icon"></span>多数据库切换</li>
</ul>
<a href="advanced.html" class="btn">进阶学习 →</a>
</div>
<div class="card">
<h3>🪞 反射实验室</h3>
<p>动态查看类结构、通过构造器创建对象、修改私有字段、调用私有方法,理解 Spring 等框架底层为什么依赖反射。</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>公开/私有/静态方法调用</li>
</ul>
<a href="reflection.html" class="btn">开始实验 →</a>
</div>
<div class="card">
<h3>🔐 鉴权实验室</h3>
<p>一步步体验登录、带 Token 请求、鉴权放行/拦截,对比 learn / advanced 模式差异。</p>
<ul class="feature-list">
<li><span class="icon"></span>登录拿 Token</li>
<li><span class="icon"></span>自动携带 Authorization</li>
<li><span class="icon"></span>当前模式读取</li>
<li><span class="icon"></span>交互式错误观察</li>
</ul>
<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>
@@ -172,7 +244,7 @@
</div>
<footer>
<p>🍃 Spring Boot 学习脚手架 | <a href="/h2-console" target="_blank">H2 控制台</a> (JDBC: jdbc:h2:file:~/h2/springboot_scaffold, 用户: sa)</p>
<p>🍃 Spring Boot 学习脚手架 | <a href="/h2-console" target="_blank">H2 控制台</a> (learn 模式 JDBC: jdbc:h2:mem:springboot_scaffold_learn, 用户: sa)</p>
</footer>
</div>
@@ -180,17 +252,19 @@
// 加载状态数据
async function loadStatus() {
try {
const [beans, users, products, orders] = await Promise.all([
const [beans, users, products, orders, profile] = await Promise.all([
fetch('/api/learning/ioc/beans').then(r => r.json()),
fetch('/api/users/count').then(r => r.json()),
fetch('/api/products').then(r => r.json()),
fetch('/api/orders').then(r => r.json())
fetch('/api/orders').then(r => r.json()),
fetch('/api/profile').then(r => r.json())
]);
document.getElementById('beanCount').textContent = beans.total || '-';
document.getElementById('userCount').textContent = users.count || 0;
document.getElementById('productCount').textContent = products.length || 0;
document.getElementById('orderCount').textContent = orders.length || 0;
document.getElementById('activeProfile').textContent = profile.profile || '-';
} catch (e) {
console.error('加载状态失败:', e);
}

View File

@@ -3,173 +3,200 @@
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>IoC 容器学习 - Spring Boot</title>
<title>IoC 生命周期可视化实验室 - 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: 1400px; margin: 0 auto; padding: 20px; }
.header { background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); color: white; padding: 30px 20px; text-align: center; margin-bottom: 20px; border-radius: 10px; }
.header h1 { font-size: 2em; }
.nav { display: flex; gap: 10px; margin-bottom: 20px; flex-wrap: wrap; justify-content: center; }
.nav a { padding: 10px 20px; background: white; border-radius: 20px; text-decoration: none; color: #333; font-size: 0.9em; }
.nav a:hover, .nav a.active { background: #667eea; color: white; }
.card { background: white; border-radius: 10px; padding: 20px; margin-bottom: 20px; box-shadow: 0 2px 10px rgba(0,0,0,0.08); }
.card h3 { color: #667eea; margin-bottom: 15px; border-bottom: 2px solid #eee; padding-bottom: 10px; }
.grid { display:grid; grid-template-columns:repeat(auto-fit,minmax(320px,1fr)); gap:16px; }
.concept-box { background: #f8f9fa; border-left: 4px solid #667eea; padding: 15px; margin: 10px 0; border-radius: 5px; }
.concept-box h4 { color: #333; margin-bottom: 10px; }
.lab { background:#fff7e6; border-left:4px solid #fa8c16; padding:15px; border-radius:8px; margin-bottom:20px; }
.btn { padding: 10px 20px; background: #667eea; color: white; border: none; border-radius: 5px; cursor: pointer; margin: 5px; }
.btn:hover { background: #5a6fd6; }
.btn-secondary { background: #6c757d; }
.result { background: #1e1e1e; color: #d4d4d4; padding: 15px; border-radius: 5px; margin-top: 10px; font-family: monospace; font-size: 0.9em; overflow-x: auto; max-height: 400px; overflow-y: auto; }
.btn-purple { background:#8e44ad; }
.result { background: #1e1e1e; color: #d4d4d4; padding: 15px; border-radius: 5px; margin-top: 10px; font-family: monospace; font-size: 0.9em; overflow-x: auto; max-height: 420px; overflow-y: auto; white-space: pre-wrap; }
table { width: 100%; border-collapse: collapse; margin: 10px 0; }
th, td { padding: 10px; text-align: left; border-bottom: 1px solid #eee; }
th { background: #f8f9fa; font-weight: 600; }
tr:hover { background: #f8f9fa; }
.badge { padding: 4px 8px; border-radius: 4px; font-size: 0.8em; }
.badge-primary { background: #667eea; color: white; }
.badge-success { background: #28a745; color: white; }
.badge-warning { background: #ffc107; color: #333; }
.timeline { background:#0f172a; color:#e2e8f0; padding:14px; border-radius:8px; max-height:420px; overflow:auto; }
.timeline-item { padding:10px; border-left:3px solid #60a5fa; margin:10px 0; background:rgba(255,255,255,0.04); }
.small { color:#666; font-size:0.9em; line-height:1.7; }
</style>
</head>
<body>
<div class="container">
<div class="header">
<h1>📦 IoC 容器学习</h1>
<p>控制反转 (Inversion of Control) 与依赖注入 (Dependency Injection)</p>
<h1>📦 IoC 生命周期可视化实验室</h1>
<p>把“类加载”“Bean 创建”“单例/多例/懒加载”拆开看,自己动手触发、自己观察结果。</p>
</div>
<div class="nav">
<a href="index.html">🏠 首页</a>
<a href="ioc.html" class="active">📦 IoC</a>
<a href="verify-lab.html">🩺 修复验证</a>
<a href="aop.html">🔪 AOP</a>
<a href="mybatis.html">💾 MyBatis</a>
<a href="transaction.html">🔄 事务</a>
<a href="users.html">👥 用户</a>
<a href="api.html">🔌 API</a>
</div>
<div class="card">
<h3>📚 核心概念</h3>
<div class="concept-box">
<h4>什么是 IoC</h4>
<p><strong>控制反转 (Inversion of Control)</strong>:将对象的创建和管理交给 Spring 容器,而不是由开发者手动创建。</p>
<p><strong>依赖注入 (Dependency Injection)</strong>IoC 的一种实现方式通过构造器、Setter 或字段将依赖注入到对象中。</p>
<div class="lab">
<strong>🧪 实验任务卡</strong>
<ul style="padding-left:20px; line-height:1.8; margin-top:8px;">
<li>先点“加载总览”,搞清楚 JVM 类加载 和 Spring Bean 生命周期不是一回事</li>
<li>再点“比较三种作用域”,对比 singleton / prototype / lazy singleton 的实例 ID 和 hashCode</li>
<li>然后分别连续点击“获取单例 / 获取多例 / 获取懒加载单例”,观察时间线变化</li>
<li>最后重置时间线,再重新做一遍,看看第一次触发和第二次触发的区别</li>
</ul>
</div>
<div class="grid">
<div class="card">
<h3>📚 核心概念总览</h3>
<button class="btn" onclick="loadOverview()">加载总览</button>
<div id="overviewResult" class="result">点击按钮查看“类加载 vs Bean 生命周期”...</div>
</div>
<div class="concept-box">
<h4>为什么用 IoC</h4>
<ul>
<li><strong>解耦</strong>:对象之间不直接依赖,通过接口交互</li>
<li><strong>可测试</strong>:方便使用 Mock 对象进行单元测试</li>
<li><strong>可维护</strong>:集中管理对象生命周期</li>
<li><strong>AOP 支持</strong>:便于实现切面编程</li>
</ul>
<div class="card">
<h3>🆚 作用域对比实验</h3>
<button class="btn" onclick="compareScopes()">比较三种作用域</button>
<button class="btn btn-secondary" onclick="resetTimeline()">重置时间线</button>
<div class="small">观察重点singleton 两次获取是否同一实例prototype 为什么每次都变lazy singleton 第一次获取前是否就已经创建?</div>
<div id="compareResult" class="result"></div>
</div>
</div>
<div class="card">
<h3>🎯 单点触发实验</h3>
<button class="btn" onclick="inspect('singleton')">获取单例 Bean</button>
<button class="btn btn-purple" onclick="inspect('prototype')">获取多例 Bean</button>
<button class="btn btn-secondary" onclick="inspect('lazy')">获取懒加载单例</button>
<div id="inspectResult" class="result">点击上面的按钮,观察实例 ID / hashCode / 访问次数如何变化...</div>
</div>
<div class="grid">
<div class="card">
<h3>🕰️ 生命周期时间线</h3>
<button class="btn" onclick="loadTimeline()">刷新时间线</button>
<div id="timelineResult" class="timeline">时间线加载中...</div>
</div>
<div class="card">
<h3>📊 Bean 作用域解释</h3>
<table>
<tr><th>作用域</th><th>创建时机</th><th>实例特点</th></tr>
<tr><td><span class="badge badge-primary">singleton</span></td><td>通常容器启动时</td><td>全局一个实例,反复获取同一个对象</td></tr>
<tr><td><span class="badge badge-success">prototype</span></td><td>每次 getBean 时</td><td>每次都是新实例,不进单例池</td></tr>
<tr><td><span class="badge badge-warning">lazy singleton</span></td><td>第一次真正使用时</td><td>启动不创建,首次获取才创建,之后复用</td></tr>
</table>
<div class="concept-box">
<h4>记住这句话</h4>
<p><strong>类加载 ≠ Bean 创建Bean 创建 ≠ 每次请求都 new。</strong></p>
</div>
</div>
</div>
<div class="card">
<h3>🔍 查看所有 Bean</h3>
<p>Spring 容器中管理的所有 Bean 对象</p>
<button class="btn" onclick="loadBeans()">刷新 Bean 列表</button>
<button class="btn btn-secondary" onclick="document.getElementById('beansResult').innerHTML=''">清空</button>
<div id="beansResult" class="result"></div>
</div>
<div class="card">
<h3>📊 Bean 作用域</h3>
<table>
<tr><th>作用域</th><th>说明</th><th>使用场景</th></tr>
<tr><td><span class="badge badge-primary">singleton</span></td><td>默认,整个应用只有一个实例</td><td>无状态的服务、配置类</td></tr>
<tr><td><span class="badge badge-success">prototype</span></td><td>每次请求都创建新实例</td><td>有状态的对象</td></tr>
<tr><td><span class="badge badge-warning">request</span></td><td>每个 HTTP 请求一个实例</td><td>Web 应用</td></tr>
<tr><td><span class="badge badge-warning">session</span></td><td>每个 HTTP 会话一个实例</td><td>用户会话数据</td></tr>
</table>
<button class="btn" onclick="testScopes()">测试作用域</button>
<div id="scopesResult" class="result"></div>
</div>
<div class="card">
<h3>⚡ 性能统计</h3>
<p>实时监控方法执行时间和调用次数</p>
<button class="btn" onclick="loadPerformance()">刷新统计</button>
<button class="btn btn-secondary" onclick="resetPerformance()">重置统计</button>
<div id="performanceResult" class="result"></div>
</div>
<div class="card">
<h3>💉 依赖注入方式对比</h3>
<table>
<tr><th>方式</th><th>优点</th><th>缺点</th><th>推荐度</th></tr>
<tr><td>构造器注入</td><td>明确依赖、不可变、易测试</td><td>参数多时代码长</td><td>⭐⭐⭐⭐⭐</td></tr>
<tr><td>Setter 注入</td><td>可选依赖、灵活</td><td>可能为 null</td><td>⭐⭐⭐</td></tr>
<tr><td>字段注入</td><td>代码简洁</td><td>隐藏依赖、难测试</td><td></td></tr>
</table>
</div>
</div>
<script>
async function loadOverview() {
const result = document.getElementById('overviewResult');
result.textContent = '加载中...';
try {
const res = await fetch('/api/learning/ioc/lifecycle/overview');
const data = await res.json();
result.textContent = JSON.stringify(data, null, 2);
} catch (e) {
result.textContent = '加载失败: ' + e.message;
}
}
async function inspect(scope) {
const result = document.getElementById('inspectResult');
result.textContent = '触发中...';
try {
const res = await fetch(`/api/learning/ioc/lifecycle/inspect/${scope}?trigger=ui-click`);
const data = await res.json();
result.textContent = JSON.stringify(data, null, 2);
loadTimeline();
} catch (e) {
result.textContent = '触发失败: ' + e.message;
}
}
async function compareScopes() {
const result = document.getElementById('compareResult');
result.textContent = '对比中...';
try {
const res = await fetch('/api/learning/ioc/lifecycle/compare');
const data = await res.json();
result.textContent = JSON.stringify(data, null, 2);
loadTimeline();
} catch (e) {
result.textContent = '对比失败: ' + e.message;
}
}
async function loadTimeline() {
const result = document.getElementById('timelineResult');
result.textContent = '加载中...';
try {
const res = await fetch('/api/learning/ioc/lifecycle/timeline');
const data = await res.json();
if (!data.length) {
result.textContent = '暂无时间线事件,先点上面的实验按钮。';
return;
}
result.innerHTML = data.map(item => `
<div class="timeline-item">
<div><strong>#${item.seq}</strong> [${item.scope}] ${item.beanName}</div>
<div>${item.phase}</div>
<div style="color:#94a3b8; margin-top:6px;">${item.detail}</div>
</div>
`).join('');
} catch (e) {
result.textContent = '加载失败: ' + e.message;
}
}
async function resetTimeline() {
await fetch('/api/learning/ioc/lifecycle/reset', { method: 'POST' });
loadTimeline();
}
async function loadBeans() {
const result = document.getElementById('beansResult');
result.textContent = '加载中...';
try {
const res = await fetch('/api/learning/ioc/beans');
const data = await res.json();
result.innerHTML = `<strong>总 Bean 数: ${data.total}</strong>\n\n用户相关 Bean:\n${data.userBeans.map(b => ' - ' + b).join('\n')}`;
result.innerHTML = `<strong>总 Bean 数: ${data.total}</strong>\n\nIoC 学习相关 Bean:\n${data.userBeans.map(b => ' - ' + b).join('\n')}`;
} catch (e) {
result.textContent = '加载失败: ' + e.message;
}
}
async function testScopes() {
const result = document.getElementById('scopesResult');
try {
const res = await fetch('/api/learning/ioc/scopes');
const data = await res.json();
result.innerHTML = JSON.stringify(data, null, 2);
} catch (e) {
result.textContent = '测试失败: ' + e.message;
}
}
async function loadPerformance() {
const result = document.getElementById('performanceResult');
result.textContent = '加载中...';
try {
const res = await fetch('/api/learning/ioc/performance');
const data = await res.json();
if (Object.keys(data).length === 0) {
result.textContent = '暂无性能数据,请先调用一些 API';
return;
}
let html = '<table><tr><th>方法</th><th>调用次数</th><th>错误数</th><th>平均耗时(ms)</th><th>最大耗时(ms)</th></tr>';
for (const [key, val] of Object.entries(data)) {
html += `<tr><td>${key}</td><td>${val.count}</td><td>${val.errors}</td><td>${val.avgMs}</td><td>${val.maxMs}</td></tr>`;
}
html += '</table>';
result.innerHTML = html;
} catch (e) {
result.textContent = '加载失败: ' + e.message;
}
}
async function resetPerformance() {
try {
await fetch('/api/learning/ioc/performance/reset', { method: 'POST' });
loadPerformance();
} catch (e) {
alert('重置失败: ' + e.message);
}
}
loadBeans();
loadPerformance();
loadOverview();
loadTimeline();
</script>
</body>
</html>
</html>

View File

@@ -0,0 +1,119 @@
<!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,#0f766e,#14b8a6); 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:#0f766e; color:#fff; }
.lab { background:#fff7e6; border-left:4px solid #fa8c16; padding:15px; 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 { color:#0f766e; margin-bottom:10px; }
.result { background:#111827; color:#d1d5db; padding:12px; border-radius:8px; min-height:180px; white-space:pre-wrap; font-family:monospace; margin-top:10px; overflow:auto; }
button { padding:10px 14px; background:#0f766e; color:#fff; border:none; border-radius:6px; cursor:pointer; margin-right:8px; margin-bottom:8px; }
input { padding:10px; border:1px solid #ddd; border-radius:6px; margin-right:8px; margin-bottom:8px; }
</style>
</head>
<body>
<div class="container">
<div class="header">
<h1>🪞 反射可视化实验室</h1>
<p>不只说“反射很重要”,而是让你自己查看类信息、动态构造对象、读写字段、调用私有方法。</p>
</div>
<div class="nav">
<a href="index.html">🏠 首页</a>
<a href="ioc.html">📦 IoC</a>
<a href="reflection.html" class="active">🪞 反射</a>
<a href="verify-lab.html">🩺 修复验证</a>
</div>
<div class="lab">
<strong>实验任务卡</strong>
<ul style="padding-left:20px; line-height:1.8; margin-top:8px;">
<li>先看类信息,理解反射能拿到哪些元数据</li>
<li>再用构造器动态创建对象,观察实例内容</li>
<li>再读写私有字段,理解为什么框架可以“跳过表面上的访问限制”</li>
<li>最后调用私有方法和静态方法,感受反射为什么是框架底层利器</li>
</ul>
</div>
<div class="grid">
<div class="card">
<h3>概念总览</h3>
<button onclick="callApi('/api/learning/reflection/overview','overview')">加载总览</button>
<div id="overview" class="result"></div>
</div>
<div class="card">
<h3>类元信息</h3>
<button onclick="callApi('/api/learning/reflection/class-info','classInfo')">查看类信息</button>
<div id="classInfo" class="result"></div>
</div>
</div>
<div class="grid">
<div class="card">
<h3>动态构造对象</h3>
<input id="ctorName" value="ref-user" placeholder="name">
<input id="ctorCount" value="5" placeholder="count">
<button onclick="instantiate()">反射创建对象</button>
<div id="ctorResult" class="result"></div>
</div>
<div class="card">
<h3>私有字段读写</h3>
<input id="fieldValue" value="changed-by-reflection" placeholder="new field value">
<button onclick="fieldAccess()">修改私有字段</button>
<div id="fieldResult" class="result"></div>
</div>
</div>
<div class="grid">
<div class="card">
<h3>方法调用实验</h3>
<input id="prefix" value="你好" placeholder="prefix">
<button onclick="methodCall()">调用公开/私有/静态方法</button>
<div id="methodResult" class="result"></div>
</div>
<div class="card">
<h3>框架为什么依赖反射</h3>
<button onclick="callApi('/api/learning/reflection/why-frameworks-use-it','frameworkResult')">查看框架视角</button>
<div id="frameworkResult" class="result"></div>
</div>
</div>
</div>
<script>
async function callApi(url, id) {
const el = document.getElementById(id);
el.textContent = '加载中...';
try {
const res = await fetch(url);
const data = await res.json();
el.textContent = JSON.stringify(data, null, 2);
} catch (e) {
el.textContent = '失败: ' + e.message;
}
}
async function instantiate() {
const name = encodeURIComponent(document.getElementById('ctorName').value);
const count = encodeURIComponent(document.getElementById('ctorCount').value);
callApi(`/api/learning/reflection/instantiate?name=${name}&count=${count}`, 'ctorResult');
}
async function fieldAccess() {
const value = encodeURIComponent(document.getElementById('fieldValue').value);
callApi(`/api/learning/reflection/field-access?value=${value}`, 'fieldResult');
}
async function methodCall() {
const prefix = encodeURIComponent(document.getElementById('prefix').value);
callApi(`/api/learning/reflection/method-call?prefix=${prefix}`, 'methodResult');
}
callApi('/api/learning/reflection/overview','overview');
</script>
</body>
</html>

View 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>

View File

@@ -1,26 +1,47 @@
com/example/scaffold/dto/OrderCreateRequest.class
com/example/scaffold/learning/MyBatisLearningController.class
com/example/scaffold/learning/reflection/ReflectionLabTarget.class
com/example/scaffold/config/database/DatabaseConfig.class
com/example/scaffold/SpringbootScaffoldApplication.class
com/example/scaffold/learning/lifecycle/IocLifecycleTracker.class
com/example/scaffold/controller/AuthController.class
com/example/scaffold/security/LearningPermitAllSecurityConfig.class
com/example/scaffold/learning/IocLearningController.class
com/example/scaffold/learning/lifecycle/PrototypeLifecycleBean.class
com/example/scaffold/learning/lifecycle/LifecycleDemoBean.class
com/example/scaffold/controller/VerificationController.class
com/example/scaffold/dto/ProductCreateRequest.class
com/example/scaffold/learning/lifecycle/SingletonLifecycleBean.class
com/example/scaffold/learning/IocLearningController$LearningBean.class
com/example/scaffold/controller/RootController.class
com/example/scaffold/learning/AopLearningController.class
com/example/scaffold/security/jwt/JwtUtil.class
com/example/scaffold/cache/CacheConfig.class
com/example/scaffold/config/LearningProfileInfo.class
com/example/scaffold/controller/HelloController.class
com/example/scaffold/service/UserService.class
com/example/scaffold/SpringbootScaffoldApplication.class
com/example/scaffold/service/impl/UserServiceImpl.class
com/example/scaffold/controller/UserController.class
com/example/scaffold/learning/IocLearningController.class
com/example/scaffold/learning/ReflectionLearningController.class
com/example/scaffold/security/jwt/JwtSecurityConfig.class
com/example/scaffold/aop/LearningAspect.class
com/example/scaffold/service/impl/OrderService.class
com/example/scaffold/controller/ProductOrderController.class
com/example/scaffold/mapper/ProductMapper.class
com/example/scaffold/security/jwt/JwtAuthenticationFilter$JwtUserDetails.class
com/example/scaffold/cache/CacheService.class
com/example/scaffold/mapper/OrderMapper.class
com/example/scaffold/security/jwt/JwtAuthenticationFilter.class
com/example/scaffold/aop/PerformanceAspect.class
com/example/scaffold/entity/User.class
com/example/scaffold/dto/UserCreateRequest.class
com/example/scaffold/dto/ProductCreateRequest.class
com/example/scaffold/learning/lifecycle/LazySingletonLifecycleBean.class
com/example/scaffold/learning/AdvancedLearningController.class
com/example/scaffold/security/satoken/SaTokenConfig.class
com/example/scaffold/entity/Order.class
com/example/scaffold/mapper/UserMapper.class
com/example/scaffold/learning/TransactionLearningController.class
com/example/scaffold/learning/IocLearningController$LearningBean.class
com/example/scaffold/controller/RootController.class
com/example/scaffold/learning/AopLearningController.class
com/example/scaffold/aop/PerformanceAspect$MethodStats.class
com/example/scaffold/config/AppConfig.class
com/example/scaffold/controller/AuthController$LoginRequest.class
com/example/scaffold/entity/Product.class

View File

@@ -1,24 +1,43 @@
/home/llm/Projects/springboot-scaffold/src/main/java/com/example/scaffold/mapper/ProductMapper.java
/home/llm/Projects/springboot-scaffold/src/main/java/com/example/scaffold/controller/UserController.java
/home/llm/Projects/springboot-scaffold/src/main/java/com/example/scaffold/learning/AopLearningController.java
/home/llm/Projects/springboot-scaffold/src/main/java/com/example/scaffold/entity/Product.java
/home/llm/Projects/springboot-scaffold/src/main/java/com/example/scaffold/entity/Order.java
/home/llm/Projects/springboot-scaffold/src/main/java/com/example/scaffold/mapper/UserMapper.java
/home/llm/Projects/springboot-scaffold/src/main/java/com/example/scaffold/dto/ProductCreateRequest.java
/home/llm/Projects/springboot-scaffold/src/main/java/com/example/scaffold/security/jwt/JwtAuthenticationFilter.java
/home/llm/Projects/springboot-scaffold/src/main/java/com/example/scaffold/security/jwt/JwtUtil.java
/home/llm/Projects/springboot-scaffold/src/main/java/com/example/scaffold/cache/CacheConfig.java
/home/llm/Projects/springboot-scaffold/src/main/java/com/example/scaffold/learning/lifecycle/SingletonLifecycleBean.java
/home/llm/Projects/springboot-scaffold/src/main/java/com/example/scaffold/controller/HelloController.java
/home/llm/Projects/springboot-scaffold/src/main/java/com/example/scaffold/service/impl/UserServiceImpl.java
/home/llm/Projects/springboot-scaffold/src/main/java/com/example/scaffold/dto/OrderCreateRequest.java
/home/llm/Projects/springboot-scaffold/src/main/java/com/example/scaffold/learning/TransactionLearningController.java
/home/llm/Projects/springboot-scaffold/src/main/java/com/example/scaffold/security/satoken/SaTokenConfig.java
/home/llm/Projects/springboot-scaffold/src/main/java/com/example/scaffold/aop/PerformanceAspect.java
/home/llm/Projects/springboot-scaffold/src/main/java/com/example/scaffold/learning/ReflectionLearningController.java
/home/llm/Projects/springboot-scaffold/src/main/java/com/example/scaffold/entity/User.java
/home/llm/Projects/springboot-scaffold/src/main/java/com/example/scaffold/learning/IocLearningController.java
/home/llm/Projects/springboot-scaffold/src/main/java/com/example/scaffold/controller/ProductOrderController.java
/home/llm/Projects/springboot-scaffold/src/main/java/com/example/scaffold/mapper/OrderMapper.java
/home/llm/Projects/springboot-scaffold/src/main/java/com/example/scaffold/config/AppConfig.java
/home/llm/Projects/springboot-scaffold/src/main/java/com/example/scaffold/learning/lifecycle/LifecycleDemoBean.java
/home/llm/Projects/springboot-scaffold/src/main/java/com/example/scaffold/security/LearningPermitAllSecurityConfig.java
/home/llm/Projects/springboot-scaffold/src/main/java/com/example/scaffold/controller/RootController.java
/home/llm/Projects/springboot-scaffold/src/main/java/com/example/scaffold/service/UserService.java
/home/llm/Projects/springboot-scaffold/src/main/java/com/example/scaffold/cache/CacheService.java
/home/llm/Projects/springboot-scaffold/src/main/java/com/example/scaffold/config/database/DatabaseConfig.java
/home/llm/Projects/springboot-scaffold/src/main/java/com/example/scaffold/SpringbootScaffoldApplication.java
/home/llm/Projects/springboot-scaffold/src/main/java/com/example/scaffold/controller/VerificationController.java
/home/llm/Projects/springboot-scaffold/src/main/java/com/example/scaffold/dto/UserCreateRequest.java
/home/llm/Projects/springboot-scaffold/src/main/java/com/example/scaffold/mapper/ProductMapper.java
/home/llm/Projects/springboot-scaffold/src/main/java/com/example/scaffold/config/LearningProfileInfo.java
/home/llm/Projects/springboot-scaffold/src/main/java/com/example/scaffold/controller/UserController.java
/home/llm/Projects/springboot-scaffold/src/main/java/com/example/scaffold/entity/Product.java
/home/llm/Projects/springboot-scaffold/src/main/java/com/example/scaffold/learning/AdvancedLearningController.java
/home/llm/Projects/springboot-scaffold/src/main/java/com/example/scaffold/mapper/UserMapper.java
/home/llm/Projects/springboot-scaffold/src/main/java/com/example/scaffold/learning/lifecycle/IocLifecycleTracker.java
/home/llm/Projects/springboot-scaffold/src/main/java/com/example/scaffold/learning/reflection/ReflectionLabTarget.java
/home/llm/Projects/springboot-scaffold/src/main/java/com/example/scaffold/learning/TransactionLearningController.java
/home/llm/Projects/springboot-scaffold/src/main/java/com/example/scaffold/learning/lifecycle/PrototypeLifecycleBean.java
/home/llm/Projects/springboot-scaffold/src/main/java/com/example/scaffold/learning/lifecycle/LazySingletonLifecycleBean.java
/home/llm/Projects/springboot-scaffold/src/main/java/com/example/scaffold/controller/AuthController.java
/home/llm/Projects/springboot-scaffold/src/main/java/com/example/scaffold/security/jwt/JwtSecurityConfig.java
/home/llm/Projects/springboot-scaffold/src/main/java/com/example/scaffold/learning/MyBatisLearningController.java
/home/llm/Projects/springboot-scaffold/src/main/java/com/example/scaffold/service/impl/OrderService.java
/home/llm/Projects/springboot-scaffold/src/main/java/com/example/scaffold/SpringbootScaffoldApplication.java
/home/llm/Projects/springboot-scaffold/src/main/java/com/example/scaffold/dto/UserCreateRequest.java
/home/llm/Projects/springboot-scaffold/src/main/java/com/example/scaffold/aop/LearningAspect.java

Binary file not shown.