feat: Spring Boot 学习脚手架 v2.0

- 新增 IoC 容器学习模块
- 新增 AOP 切面编程学习模块
- 新增 MyBatis 集成学习模块
- 新增事务管理学习模块
- 新增用户/产品/订单 CRUD
- 新增 7 个交互式学习页面
- 集成性能监控切面
This commit is contained in:
likingcode
2026-03-07 08:37:40 +00:00
commit c04235c655
73 changed files with 4978 additions and 0 deletions

View File

@@ -0,0 +1,22 @@
spring.application.name=springboot-scaffold
server.port=8082
# H2 Database
spring.h2.console.enabled=true
spring.datasource.url=jdbc:h2:file:~/h2/springboot_scaffold
spring.datasource.driverClassName=org.h2.Driver
spring.datasource.username=sa
spring.datasource.password=
# JPA
spring.jpa.hibernate.ddl-auto=update
spring.jpa.show-sql=true
spring.jpa.properties.hibernate.format_sql=true
# MyBatis
mybatis.configuration.map-underscore-to-camel-case=true
mybatis.configuration.log-impl=org.apache.ibatis.logging.stdout.StdOutImpl
# Logging
logging.level.com.example.scaffold=DEBUG
logging.level.org.springframework.transaction.interceptor=TRACE

View File

@@ -0,0 +1,159 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>AOP 切面学习 - 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; }
.advice-box { background: #f8f9fa; border-radius: 8px; padding: 15px; margin: 10px 0; border-left: 4px solid; }
.advice-before { border-color: #28a745; }
.advice-after { border-color: #6c757d; }
.advice-returning { border-color: #17a2b8; }
.advice-throwing { border-color: #dc3545; }
.advice-around { border-color: #ffc107; }
.btn { padding: 10px 20px; background: #f5576c; color: white; border: none; border-radius: 5px; cursor: pointer; margin: 5px; }
.btn:hover { background: #e0465b; }
.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; }
.code-block { background: #f4f4f4; padding: 15px; border-radius: 5px; font-family: monospace; font-size: 0.9em; overflow-x: auto; margin: 10px 0; }
.flow-diagram { display: flex; align-items: center; justify-content: center; flex-wrap: wrap; gap: 10px; padding: 20px; background: #f8f9fa; border-radius: 10px; margin: 15px 0; }
.flow-step { padding: 10px 20px; background: white; border-radius: 20px; border: 2px solid #ddd; }
.flow-arrow { font-size: 1.5em; color: #999; }
</style>
</head>
<body>
<div class="container">
<div class="header">
<h1>🔪 AOP 切面编程</h1>
<p>Aspect Oriented Programming - 面向切面编程</p>
</div>
<div class="nav">
<a href="index.html">🏠 首页</a>
<a href="ioc.html">📦 IoC</a>
<a href="aop.html" class="active">🔪 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>
<p><strong>AOP (面向切面编程)</strong>:将横切关注点(日志、权限、事务等)从业务逻辑中分离出来,实现模块化。</p>
<div class="flow-diagram">
<div class="flow-step">目标方法</div>
<div class="flow-arrow"></div>
<div class="flow-step">@Before</div>
<div class="flow-arrow"></div>
<div class="flow-step">@Around (前)</div>
<div class="flow-arrow"></div>
<div class="flow-step">方法执行</div>
<div class="flow-arrow"></div>
<div class="flow-step">@Around (后)</div>
<div class="flow-arrow"></div>
<div class="flow-step">@AfterReturning</div>
<div class="flow-arrow"></div>
<div class="flow-step">@After</div>
</div>
</div>
<div class="card">
<h3>🔔 五种通知类型</h3>
<div class="advice-box advice-before">
<h4>@Before - 前置通知</h4>
<p>方法执行前触发,可用于参数校验、日志记录</p>
<div class="code-block">@Before("execution(* com.example.service..*.*(..))")<br>public void before(JoinPoint jp) {<br> log.info("即将执行: " + jp.getSignature().getName());<br>}</div>
</div>
<div class="advice-box advice-after">
<h4>@After - 后置通知</h4>
<p>方法执行后触发(无论是否异常),可用于资源释放</p>
<div class="code-block">@After("execution(* com.example.service..*.*(..))")<br>public void after(JoinPoint jp) {<br> log.info("执行完成: " + jp.getSignature().getName());<br>}</div>
</div>
<div class="advice-box advice-returning">
<h4>@AfterReturning - 返回通知</h4>
<p>方法成功返回后触发,可获取返回值</p>
<div class="code-block">@AfterReturning(pointcut="...", returning="result")<br>public void afterReturning(Object result) {<br> log.info("返回结果: " + result);<br>}</div>
</div>
<div class="advice-box advice-throwing">
<h4>@AfterThrowing - 异常通知</h4>
<p>方法抛出异常后触发,可用于异常处理</p>
<div class="code-block">@AfterThrowing(pointcut="...", throwing="ex")<br>public void afterThrowing(Exception ex) {<br> log.error("发生异常: " + ex.getMessage());<br>}</div>
</div>
<div class="advice-box advice-around">
<h4>@Around - 环绕通知</h4>
<p>完全控制方法执行,可决定是否执行目标方法</p>
<div class="code-block">@Around("execution(* com.example.controller..*.*(..))")<br>public Object around(ProceedingJoinPoint pjp) throws Throwable {<br> long start = System.currentTimeMillis();<br> Object result = pjp.proceed(); // 执行目标方法<br> long cost = System.currentTimeMillis() - start;<br> log.info("耗时: " + cost + "ms");<br> return result;<br>}</div>
</div>
</div>
<div class="card">
<h3>🧪 在线测试</h3>
<p>调用下面的 API然后查看控制台日志观察 AOP 执行顺序</p>
<button class="btn" onclick="testAop()">测试正常执行</button>
<button class="btn" onclick="testAopError()">测试异常通知</button>
<div id="testResult" class="result"></div>
</div>
<div class="card">
<h3>📝 切入点表达式</h3>
<table style="width:100%;border-collapse:collapse;">
<tr style="background:#f8f9fa;"><th style="padding:10px;text-align:left;">表达式</th><th style="padding:10px;text-align:left;">含义</th></tr>
<tr><td style="padding:10px;border-bottom:1px solid #eee;">execution(* *(..))</td><td style="padding:10px;border-bottom:1px solid #eee;">匹配所有方法</td></tr>
<tr><td style="padding:10px;border-bottom:1px solid #eee;">execution(* com.example.service.*.*(..))</td><td style="padding:10px;border-bottom:1px solid #eee;">service包下所有方法</td></tr>
<tr><td style="padding:10px;border-bottom:1px solid #eee;">execution(* com.example.service..*.*(..))</td><td style="padding:10px;border-bottom:1px solid #eee;">service包及子包所有方法</td></tr>
<tr><td style="padding:10px;border-bottom:1px solid #eee;">execution(public * *(..))</td><td style="padding:10px;border-bottom:1px solid #eee;">所有public方法</td></tr>
<tr><td style="padding:10px;border-bottom:1px solid #eee;">@annotation(org.springframework.transaction.annotation.Transactional)</td><td style="padding:10px;border-bottom:1px solid #eee;">带@Transactional的方法</td></tr>
</table>
</div>
</div>
<script>
async function testAop() {
const result = document.getElementById('testResult');
result.textContent = '测试中...';
try {
const res = await fetch('/api/learning/aop/test?message=HelloAOP');
const data = await res.json();
result.innerHTML = `<strong>✅ 测试成功</strong>\n\n${JSON.stringify(data, null, 2)}\n\n💡 提示: 查看服务器控制台,观察 AOP 通知执行顺序`;
} catch (e) {
result.textContent = '测试失败: ' + e.message;
}
}
async function testAopError() {
const result = document.getElementById('testResult');
result.textContent = '测试中...';
try {
const res = await fetch('/api/learning/aop/test-error?error=true');
const data = await res.json();
result.innerHTML = `<strong>✅ 正常返回</strong>\n\n${JSON.stringify(data, null, 2)}`;
} catch (e) {
result.innerHTML = `<strong>❌ 触发异常(预期行为)</strong>\n\n异常信息: ${e.message}\n\n💡 提示: 查看服务器控制台,观察 @AfterThrowing 通知`;
}
}
</script>
</body>
</html>

View File

@@ -0,0 +1,280 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>API 测试面板 - 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; }
.api-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(350px, 1fr)); gap: 15px; }
.api-item { background: #f8f9fa; border-radius: 8px; padding: 15px; border-left: 4px solid; }
.api-item.get { border-color: #28a745; }
.api-item.post { border-color: #007bff; }
.api-item.put { border-color: #ffc107; }
.api-item.delete { border-color: #dc3545; }
.method { display: inline-block; padding: 3px 8px; border-radius: 4px; font-size: 0.75em; font-weight: bold; margin-right: 8px; }
.method.get { background: #28a745; color: white; }
.method.post { background: #007bff; color: white; }
.method.put { background: #ffc107; color: #333; }
.method.delete { background: #dc3545; color: white; }
.btn { padding: 8px 16px; border: none; border-radius: 5px; cursor: pointer; font-size: 0.9em; margin-top: 10px; }
.btn-primary { background: #667eea; color: white; }
.btn-primary:hover { background: #5a6fd6; }
.result { background: #1e1e1e; color: #d4d4d4; padding: 15px; border-radius: 5px; margin-top: 10px; font-family: monospace; font-size: 0.85em; overflow-x: auto; max-height: 200px; overflow-y: auto; display: none; }
.result.show { display: block; }
.url { font-family: monospace; color: #666; font-size: 0.9em; word-break: break-all; }
.tabs { display: flex; gap: 5px; margin-bottom: 20px; border-bottom: 2px solid #eee; }
.tab { padding: 10px 20px; cursor: pointer; border-bottom: 2px solid transparent; }
.tab.active { border-bottom-color: #667eea; color: #667eea; font-weight: 600; }
.tab-content { display: none; }
.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; }
</style>
</head>
<body>
<div class="container">
<div class="header">
<h1>🔌 API 测试面板</h1>
<p>在线测试所有 RESTful API</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="api.html" class="active">🔌 API</a>
</div>
<div class="tabs">
<div class="tab active" onclick="switchTab('user')">👥 用户 API</div>
<div class="tab" onclick="switchTab('product')">📦 产品 API</div>
<div class="tab" onclick="switchTab('order')">🛒 订单 API</div>
<div class="tab" onclick="switchTab('learning')">📚 学习 API</div>
</div>
<!-- 用户 API -->
<div id="userTab" class="tab-content active">
<div class="card">
<h3>用户管理 API</h3>
<div class="api-grid">
<div class="api-item get">
<span class="method get">GET</span>
<strong>获取所有用户</strong>
<div class="url">/api/users</div>
<button class="btn btn-primary" onclick="testApi('GET', '/api/users', null, 'userResult1')">测试</button>
<div id="userResult1" class="result"></div>
</div>
<div class="api-item get">
<span class="method get">GET</span>
<strong>获取单个用户</strong>
<div class="url">/api/users/{id}</div>
<input type="number" id="userId" placeholder="用户ID" style="width:80px;padding:5px;margin-top:5px;">
<button class="btn btn-primary" onclick="testApi('GET', '/api/users/' + document.getElementById('userId').value, null, 'userResult2')">测试</button>
<div id="userResult2" class="result"></div>
</div>
<div class="api-item post">
<span class="method post">POST</span>
<strong>创建用户</strong>
<div class="url">/api/users</div>
<textarea class="json-input" id="createUserJson">{
"username": "testuser",
"email": "test@example.com",
"phone": "13800138000",
"bio": "测试用户"
}</textarea>
<button class="btn btn-primary" onclick="testApi('POST', '/api/users', document.getElementById('createUserJson').value, 'userResult3')">测试</button>
<div id="userResult3" class="result"></div>
</div>
<div class="api-item get">
<span class="method get">GET</span>
<strong>搜索用户</strong>
<div class="url">/api/users/search?username={name}</div>
<input type="text" id="searchName" placeholder="用户名关键词" style="padding:5px;margin-top:5px;">
<button class="btn btn-primary" onclick="testApi('GET', '/api/users/search?username=' + encodeURIComponent(document.getElementById('searchName').value), null, 'userResult4')">测试</button>
<div id="userResult4" class="result"></div>
</div>
</div>
</div>
</div>
<!-- 产品 API -->
<div id="productTab" class="tab-content">
<div class="card">
<h3>产品管理 API</h3>
<div class="api-grid">
<div class="api-item get">
<span class="method get">GET</span>
<strong>获取所有产品</strong>
<div class="url">/api/products</div>
<button class="btn btn-primary" onclick="testApi('GET', '/api/products', null, 'productResult1')">测试</button>
<div id="productResult1" class="result"></div>
</div>
<div class="api-item post">
<span class="method post">POST</span>
<strong>创建产品</strong>
<div class="url">/api/products</div>
<textarea class="json-input" id="createProductJson">{
"name": "iPhone 15",
"description": "最新款苹果手机",
"price": 5999.00,
"stockQuantity": 100,
"category": "手机"
}</textarea>
<button class="btn btn-primary" onclick="testApi('POST', '/api/products', document.getElementById('createProductJson').value, 'productResult2')">测试</button>
<div id="productResult2" class="result"></div>
</div>
</div>
</div>
</div>
<!-- 订单 API -->
<div id="orderTab" class="tab-content">
<div class="card">
<h3>订单管理 API (演示事务)</h3>
<div class="api-grid">
<div class="api-item get">
<span class="method get">GET</span>
<strong>获取所有订单</strong>
<div class="url">/api/orders</div>
<button class="btn btn-primary" onclick="testApi('GET', '/api/orders', null, 'orderResult1')">测试</button>
<div id="orderResult1" class="result"></div>
</div>
<div class="api-item post">
<span class="method post">POST</span>
<strong>创建订单</strong>
<div class="url">/api/orders</div>
<textarea class="json-input" id="createOrderJson">{
"userId": 1,
"productId": 1,
"quantity": 1,
"rollback": false
}</textarea>
<button class="btn btn-primary" onclick="testApi('POST', '/api/orders', document.getElementById('createOrderJson').value, 'orderResult2')">测试</button>
<div id="orderResult2" class="result"></div>
</div>
<div class="api-item post">
<span class="method post">POST</span>
<strong>创建订单(触发回滚)</strong>
<div class="url">/api/orders (rollback=true)</div>
<textarea class="json-input" id="rollbackOrderJson">{
"userId": 1,
"productId": 1,
"quantity": 1,
"rollback": true
}</textarea>
<button class="btn btn-primary" onclick="testApi('POST', '/api/orders', document.getElementById('rollbackOrderJson').value, 'orderResult3')">测试</button>
<div id="orderResult3" class="result"></div>
</div>
</div>
</div>
</div>
<!-- 学习 API -->
<div id="learningTab" class="tab-content">
<div class="card">
<h3>学习 API</h3>
<div class="api-grid">
<div class="api-item get">
<span class="method get">GET</span>
<strong>IoC - 查看所有 Bean</strong>
<div class="url">/api/learning/ioc/beans</div>
<button class="btn btn-primary" onclick="testApi('GET', '/api/learning/ioc/beans', null, 'learnResult1')">测试</button>
<div id="learnResult1" class="result"></div>
</div>
<div class="api-item get">
<span class="method get">GET</span>
<strong>AOP - 概念</strong>
<div class="url">/api/learning/aop/concepts</div>
<button class="btn btn-primary" onclick="testApi('GET', '/api/learning/aop/concepts', null, 'learnResult2')">测试</button>
<div id="learnResult2" class="result"></div>
</div>
<div class="api-item get">
<span class="method get">GET</span>
<strong>MyBatis - 配置</strong>
<div class="url">/api/learning/mybatis/config</div>
<button class="btn btn-primary" onclick="testApi('GET', '/api/learning/mybatis/config', null, 'learnResult3')">测试</button>
<div id="learnResult3" class="result"></div>
</div>
<div class="api-item get">
<span class="method get">GET</span>
<strong>事务 - 传播行为</strong>
<div class="url">/api/learning/transaction/propagation</div>
<button class="btn btn-primary" onclick="testApi('GET', '/api/learning/transaction/propagation', null, 'learnResult4')">测试</button>
<div id="learnResult4" class="result"></div>
</div>
</div>
</div>
</div>
</div>
<script>
function switchTab(tab) {
document.querySelectorAll('.tab').forEach(t => t.classList.remove('active'));
document.querySelectorAll('.tab-content').forEach(t => t.classList.remove('active'));
event.target.classList.add('active');
document.getElementById(tab + 'Tab').classList.add('active');
}
async function testApi(method, url, body, resultId) {
const resultDiv = document.getElementById(resultId);
resultDiv.classList.add('show');
resultDiv.textContent = '请求中...';
try {
const options = {
method: method,
headers: {}
};
if (body) {
options.headers['Content-Type'] = 'application/json';
options.body = body;
}
const startTime = Date.now();
const res = await fetch(url, options);
const duration = Date.now() - startTime;
const data = await res.json();
resultDiv.innerHTML = `<strong>${res.status} ${res.statusText}</strong> (${duration}ms)\n\n${JSON.stringify(data, null, 2)}`;
} catch (e) {
resultDiv.innerHTML = `<strong style="color:#ff6b6b;">Error</strong>\n\n${e.message}`;
}
}
</script>
</body>
</html>

View File

@@ -0,0 +1,203 @@
<!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, #667eea 0%, #764ba2 100%); color: white; padding: 40px 20px; text-align: center; margin-bottom: 30px; border-radius: 10px; }
.header h1 { font-size: 2.5em; margin-bottom: 10px; }
.header p { opacity: 0.9; font-size: 1.1em; }
.nav { display: flex; gap: 10px; margin-bottom: 30px; flex-wrap: wrap; justify-content: center; }
.nav a { padding: 12px 24px; background: white; border-radius: 25px; text-decoration: none; color: #333; font-weight: 500; transition: all 0.3s; border: 2px solid #e0e0e0; }
.nav a:hover, .nav a.active { background: #667eea; color: white; border-color: #667eea; }
.grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(300px, 1fr)); gap: 20px; }
.card { background: white; border-radius: 15px; padding: 25px; box-shadow: 0 5px 20px rgba(0,0,0,0.08); transition: transform 0.3s; }
.card:hover { transform: translateY(-5px); }
.card h3 { color: #333; margin-bottom: 15px; font-size: 1.3em; }
.card p { color: #666; line-height: 1.6; margin-bottom: 15px; }
.card .btn { display: inline-block; padding: 10px 20px; background: #667eea; color: white; text-decoration: none; border-radius: 20px; font-size: 0.9em; }
.card .btn:hover { background: #5a6fd6; }
.feature-list { list-style: none; }
.feature-list li { padding: 8px 0; border-bottom: 1px solid #eee; }
.feature-list li:last-child { border-bottom: none; }
.feature-list .icon { margin-right: 8px; }
.api-test { background: #1e1e1e; border-radius: 10px; padding: 20px; margin-top: 20px; }
.api-test h4 { color: #4ec9b0; margin-bottom: 15px; }
.api-test pre { color: #d4d4d4; overflow-x: auto; font-size: 0.85em; }
.api-test .method { color: #569cd6; }
.api-test .url { color: #4ec9b0; }
.status { display: flex; gap: 20px; flex-wrap: wrap; margin-bottom: 30px; }
.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; }
footer { text-align: center; padding: 30px; color: #666; margin-top: 40px; }
</style>
</head>
<body>
<div class="container">
<div class="header">
<h1>🍃 Spring Boot 学习中心</h1>
<p>交互式学习 Spring 核心功能 | IoC · AOP · MyBatis · 事务</p>
</div>
<div class="status">
<div class="status-item">
<div class="value" id="beanCount">-</div>
<div class="label">已加载 Bean</div>
</div>
<div class="status-item">
<div class="value" id="userCount">-</div>
<div class="label">用户数量</div>
</div>
<div class="status-item">
<div class="value" id="productCount">-</div>
<div class="label">产品数量</div>
</div>
<div class="status-item">
<div class="value" id="orderCount">-</div>
<div class="label">订单数量</div>
</div>
</div>
<div class="nav">
<a href="index.html" class="active">🏠 首页</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="api.html">🔌 API 测试</a>
</div>
<div class="grid">
<div class="card">
<h3>📦 IoC 容器</h3>
<p>理解 Spring 的核心:控制反转和依赖注入。学习 Bean 的生命周期、作用域和各种注入方式。</p>
<ul class="feature-list">
<li><span class="icon"></span>Bean 生命周期演示</li>
<li><span class="icon"></span>依赖注入方式对比</li>
<li><span class="icon"></span>Bean 作用域详解</li>
<li><span class="icon"></span>性能统计面板</li>
</ul>
<a href="ioc.html" class="btn">开始学习 →</a>
</div>
<div class="card">
<h3>🔪 AOP 切面编程</h3>
<p>掌握面向切面编程,实现日志、性能监控、事务等横切关注点的模块化管理。</p>
<ul class="feature-list">
<li><span class="icon"></span>5 种通知类型演示</li>
<li><span class="icon"></span>切入点表达式语法</li>
<li><span class="icon"></span>实时日志展示</li>
<li><span class="icon"></span>性能监控切面</li>
</ul>
<a href="aop.html" class="btn">开始学习 →</a>
</div>
<div class="card">
<h3>💾 MyBatis 集成</h3>
<p>学习 MyBatis 与 Spring Boot 的整合,对比 JPA掌握动态 SQL 和缓存机制。</p>
<ul class="feature-list">
<li><span class="icon"></span>MyBatis vs JPA 对比</li>
<li><span class="icon"></span>动态 SQL 语法</li>
<li><span class="icon"></span>一级/二级缓存演示</li>
<li><span class="icon"></span>批量操作示例</li>
</ul>
<a href="mybatis.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="transaction.html" class="btn">开始学习 →</a>
</div>
<div class="card">
<h3>👥 用户管理 CRUD</h3>
<p>完整的 RESTful API 示例,演示增删改查操作和参数验证。</p>
<ul class="feature-list">
<li><span class="icon"></span>RESTful 设计规范</li>
<li><span class="icon"></span>参数验证</li>
<li><span class="icon"></span>异常处理</li>
<li><span class="icon"></span>交互式测试</li>
</ul>
<a href="users.html" class="btn">开始学习 →</a>
</div>
<div class="card">
<h3>🔌 API 测试面板</h3>
<p>在线测试所有 API 接口,查看请求响应,理解 RESTful API 工作原理。</p>
<ul class="feature-list">
<li><span class="icon"></span>用户 API</li>
<li><span class="icon"></span>产品 API</li>
<li><span class="icon"></span>订单 API</li>
<li><span class="icon"></span>学习 API</li>
</ul>
<a href="api.html" class="btn">开始测试 →</a>
</div>
</div>
<div class="api-test">
<h4>🚀 快速开始 - API 示例</h4>
<pre>
<span class="method">GET</span> <span class="url">/api/users</span> # 获取所有用户
<span class="method">GET</span> <span class="url">/api/users/{id}</span> # 获取单个用户
<span class="method">POST</span> <span class="url">/api/users</span> # 创建用户
<span class="method">PUT</span> <span class="url">/api/users/{id}</span> # 更新用户
<span class="method">DEL</span> <span class="url">/api/users/{id}</span> # 删除用户
<span class="method">GET</span> <span class="url">/api/learning/ioc/beans</span> # 查看所有 Bean
<span class="method">GET</span> <span class="url">/api/learning/aop/concepts</span> # AOP 概念
<span class="method">GET</span> <span class="url">/api/learning/mybatis/cache</span> # 缓存机制
<span class="method">GET</span> <span class="url">/api/learning/transaction/propagation</span> # 传播行为
</pre>
</div>
<footer>
<p>🍃 Spring Boot 学习脚手架 | <a href="/h2-console" target="_blank">H2 控制台</a> (JDBC: jdbc:h2:file:~/h2/springboot_scaffold, 用户: sa)</p>
</footer>
</div>
<script>
// 加载状态数据
async function loadStatus() {
try {
const [beans, users, products, orders] = 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())
]);
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;
} catch (e) {
console.error('加载状态失败:', e);
}
}
loadStatus();
setInterval(loadStatus, 30000);
</script>
</body>
</html>

View File

@@ -0,0 +1,175 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<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; }
.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; }
.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; }
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; }
</style>
</head>
<body>
<div class="container">
<div class="header">
<h1>📦 IoC 容器学习</h1>
<p>控制反转 (Inversion of Control) 与依赖注入 (Dependency Injection)</p>
</div>
<div class="nav">
<a href="index.html">🏠 首页</a>
<a href="ioc.html" class="active">📦 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="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>
<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>
</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 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')}`;
} 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();
</script>
</body>
</html>

View File

@@ -0,0 +1,176 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>MyBatis 学习 - 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, #4facfe 0%, #00f2fe 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: #4facfe; 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: #4facfe; margin-bottom: 15px; border-bottom: 2px solid #eee; padding-bottom: 10px; }
.comparison { display: grid; grid-template-columns: 1fr 1fr; gap: 20px; }
.compare-box { padding: 20px; border-radius: 10px; }
.mybatis-box { background: #e3f2fd; border: 2px solid #4facfe; }
.jpa-box { background: #f3e5f5; border: 2px solid #9c27b0; }
.btn { padding: 10px 20px; background: #4facfe; color: white; border: none; border-radius: 5px; cursor: pointer; margin: 5px; }
.btn:hover { background: #3d9be8; }
.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; }
.code-block { background: #f4f4f4; padding: 15px; border-radius: 5px; font-family: monospace; font-size: 0.9em; overflow-x: auto; margin: 10px 0; }
.cache-box { background: #fff3e0; border-left: 4px solid #ff9800; padding: 15px; margin: 10px 0; }
</style>
</head>
<body>
<div class="container">
<div class="header">
<h1>💾 MyBatis 学习</h1>
<p>半自动 ORM 框架 - SQL 与 Java 对象的映射</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" class="active">💾 MyBatis</a>
<a href="transaction.html">🔄 事务</a>
<a href="users.html">👥 用户</a>
<a href="api.html">🔌 API</a>
</div>
<div class="card">
<h3>📊 MyBatis vs JPA 对比</h3>
<div class="comparison">
<div class="compare-box mybatis-box">
<h4>MyBatis</h4>
<p><strong>优点:</strong></p>
<ul>
<li>✅ SQL 灵活可控</li>
<li>✅ 性能优化方便</li>
<li>✅ 复杂查询友好</li>
<li>✅ 易于 DBA 协作</li>
</ul>
<p><strong>缺点:</strong></p>
<ul>
<li>❌ SQL 与代码耦合</li>
<li>❌ 数据库迁移成本高</li>
<li>❌ 需要手写 SQL</li>
</ul>
<p><strong>适用:</strong>复杂查询、性能要求高</p>
</div>
<div class="compare-box jpa-box">
<h4>JPA/Hibernate</h4>
<p><strong>优点:</strong></p>
<ul>
<li>✅ 面向对象</li>
<li>✅ 数据库无关</li>
<li>✅ 开发效率高</li>
<li>✅ 自动维护</li>
</ul>
<p><strong>缺点:</strong></p>
<ul>
<li>❌ 复杂查询困难</li>
<li>❌ 性能调优复杂</li>
<li>❌ 学习曲线陡</li>
</ul>
<p><strong>适用:</strong>标准 CRUD、快速开发</p>
</div>
</div>
</div>
<div class="card">
<h3>🚀 快速体验</h3>
<button class="btn" onclick="loadConfig()">查看 MyBatis 配置</button>
<button class="btn" onclick="loadConcepts()">核心概念</button>
<button class="btn" onclick="loadVsJpa()">详细对比</button>
<div id="result" class="result"></div>
</div>
<div class="card">
<h3>💡 缓存机制</h3>
<div class="cache-box">
<h4>一级缓存 (SqlSession 级别)</h4>
<p>默认开启,同一 SqlSession 内相同查询只执行一次 SQL</p>
<div class="code-block">// 第一次查询 - 执行 SQL<br>User u1 = mapper.findById(1L);<br>// 第二次查询 - 命中缓存,不执行 SQL<br>User u2 = mapper.findById(1L);</div>
</div>
<div class="cache-box">
<h4>二级缓存 (Mapper 级别)</h4>
<p>需配置 @CacheNamespace多个 SqlSession 共享</p>
<div class="code-block">@Mapper<br>@CacheNamespace<br>public interface UserMapper { ... }</div>
</div>
<p>💡 <strong>验证方式:</strong>查看控制台 SQL 日志,缓存命中时不打印 SQL</p>
</div>
<div class="card">
<h3>📝 常用注解</h3>
<table style="width:100%;border-collapse:collapse;">
<tr style="background:#f8f9fa;"><th style="padding:10px;text-align:left;">注解</th><th style="padding:10px;text-align:left;">用途</th></tr>
<tr><td style="padding:10px;border-bottom:1px solid #eee;">@Mapper</td><td style="padding:10px;border-bottom:1px solid #eee;">标记为 MyBatis Mapper 接口</td></tr>
<tr><td style="padding:10px;border-bottom:1px solid #eee;">@Select</td><td style="padding:10px;border-bottom:1px solid #eee;">查询 SQL</td></tr>
<tr><td style="padding:10px;border-bottom:1px solid #eee;">@Insert</td><td style="padding:10px;border-bottom:1px solid #eee;">插入 SQL</td></tr>
<tr><td style="padding:10px;border-bottom:1px solid #eee;">@Update</td><td style="padding:10px;border-bottom:1px solid #eee;">更新 SQL</td></tr>
<tr><td style="padding:10px;border-bottom:1px solid #eee;">@Delete</td><td style="padding:10px;border-bottom:1px solid #eee;">删除 SQL</td></tr>
<tr><td style="padding:10px;border-bottom:1px solid #eee;">@Param</td><td style="padding:10px;border-bottom:1px solid #eee;">参数命名</td></tr>
<tr><td style="padding:10px;border-bottom:1px solid #eee;">@Options</td><td style="padding:10px;border-bottom:1px solid #eee;">额外选项如返回自增ID</td></tr>
<tr><td style="padding:10px;border-bottom:1px solid #eee;">@CacheNamespace</td><td style="padding:10px;border-bottom:1px solid #eee;">启用二级缓存</td></tr>
</table>
</div>
<div class="card">
<h3>🔧 动态 SQL 示例</h3>
<div class="code-block">&lt;select id="findUsers"&gt;<br> SELECT * FROM users<br> &lt;where&gt;<br> &lt;if test="name != null"&gt;<br> AND name LIKE CONCAT('%', #{name}, '%')<br> &lt;/if&gt;<br> &lt;if test="email != null"&gt;<br> AND email = #{email}<br> &lt;/if&gt;<br> &lt;/where&gt;<br>&lt;/select&gt;</div>
</div>
</div>
<script>
async function loadConfig() {
const result = document.getElementById('result');
result.textContent = '加载中...';
try {
const res = await fetch('/api/learning/mybatis/config');
const data = await res.json();
result.innerHTML = `<strong>MyBatis 配置</strong>\n\n${JSON.stringify(data, null, 2)}`;
} catch (e) {
result.textContent = '加载失败: ' + e.message;
}
}
async function loadConcepts() {
const result = document.getElementById('result');
result.textContent = '加载中...';
try {
const res = await fetch('/api/learning/mybatis/concepts');
const data = await res.json();
result.innerHTML = `<strong>核心概念</strong>\n\n${JSON.stringify(data, null, 2)}`;
} catch (e) {
result.textContent = '加载失败: ' + e.message;
}
}
async function loadVsJpa() {
const result = document.getElementById('result');
result.textContent = '加载中...';
try {
const res = await fetch('/api/learning/mybatis/vs-jpa');
const data = await res.json();
result.innerHTML = `<strong>MyBatis vs JPA</strong>\n\n${JSON.stringify(data, null, 2)}`;
} catch (e) {
result.textContent = '加载失败: ' + e.message;
}
}
</script>
</body>
</html>

View File

@@ -0,0 +1,191 @@
<!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, #fa709a 0%, #fee140 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: #fa709a; 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: #fa709a; margin-bottom: 15px; border-bottom: 2px solid #eee; padding-bottom: 10px; }
.acid-box { display: grid; grid-template-columns: repeat(4, 1fr); gap: 15px; margin: 20px 0; }
.acid-item { background: #f8f9fa; padding: 20px; border-radius: 10px; text-align: center; border-top: 4px solid; }
.acid-a { border-color: #e74c3c; }
.acid-c { border-color: #3498db; }
.acid-i { border-color: #2ecc71; }
.acid-d { border-color: #9b59b6; }
.acid-item h4 { font-size: 2em; margin-bottom: 10px; }
.btn { padding: 10px 20px; background: #fa709a; color: white; border: none; border-radius: 5px; cursor: pointer; margin: 5px; }
.btn:hover { background: #e85a8a; }
.btn-danger { background: #e74c3c; }
.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; }
.propagation-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(300px, 1fr)); gap: 15px; }
.prop-box { background: #f8f9fa; padding: 15px; border-radius: 8px; border-left: 4px solid #fa709a; }
.prop-box h4 { color: #fa709a; margin-bottom: 8px; }
table { width: 100%; border-collapse: collapse; margin: 15px 0; }
th, td { padding: 12px; text-align: left; border-bottom: 1px solid #eee; }
th { background: #f8f9fa; font-weight: 600; }
tr:hover { background: #f8f9fa; }
</style>
</head>
<body>
<div class="container">
<div class="header">
<h1>🔄 事务管理</h1>
<p>声明式事务 - @Transactional</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" class="active">🔄 事务</a>
<a href="users.html">👥 用户</a>
<a href="api.html">🔌 API</a>
</div>
<div class="card">
<h3>📚 ACID 特性</h3>
<div class="acid-box">
<div class="acid-item acid-a">
<h4>A</h4>
<p><strong>Atomicity</strong></p>
<p>原子性</p>
<p style="font-size:0.9em;color:#666;">事务是不可分割的工作单位</p>
</div>
<div class="acid-item acid-c">
<h4>C</h4>
<p><strong>Consistency</strong></p>
<p>一致性</p>
<p style="font-size:0.9em;color:#666;">数据库状态保持一致</p>
</div>
<div class="acid-item acid-i">
<h4>I</h4>
<p><strong>Isolation</strong></p>
<p>隔离性</p>
<p style="font-size:0.9em;color:#666;">事务之间相互隔离</p>
</div>
<div class="acid-item acid-d">
<h4>D</h4>
<p><strong>Durability</strong></p>
<p>持久性</p>
<p style="font-size:0.9em;color:#666;">提交后永久保存</p>
</div>
</div>
</div>
<div class="card">
<h3>🚀 传播行为 (Propagation)</h3>
<div class="propagation-grid">
<div class="prop-box">
<h4>REQUIRED (默认)</h4>
<p>有事务则加入,无则新建</p>
<p style="font-size:0.85em;color:#666;">最常用,适合大多数业务方法</p>
</div>
<div class="prop-box">
<h4>REQUIRES_NEW</h4>
<p>总是新建事务,挂起当前事务</p>
<p style="font-size:0.85em;color:#666;">适合日志记录、独立子任务</p>
</div>
<div class="prop-box">
<h4>SUPPORTS</h4>
<p>有事务则加入,无则以非事务运行</p>
<p style="font-size:0.85em;color:#666;">适合查询方法</p>
</div>
<div class="prop-box">
<h4>NOT_SUPPORTED</h4>
<p>以非事务运行,挂起当前事务</p>
<p style="font-size:0.85em;color:#666;">不需要事务的操作</p>
</div>
<div class="prop-box">
<h4>MANDATORY</h4>
<p>必须在事务中运行,否则抛异常</p>
<p style="font-size:0.85em;color:#666;">强制要求事务</p>
</div>
<div class="prop-box">
<h4>NEVER</h4>
<p>不能在事务中运行,否则抛异常</p>
<p style="font-size:0.85em;color:#666;">确保无事务</p>
</div>
</div>
<button class="btn" onclick="loadPropagation()">查看详细说明</button>
<div id="propResult" class="result"></div>
</div>
<div class="card">
<h3>🔒 隔离级别 (Isolation)</h3>
<table>
<tr><th>级别</th><th>脏读</th><th>不可重复读</th><th>幻读</th><th>说明</th></tr>
<tr><td>READ_UNCOMMITTED</td><td></td><td></td><td></td><td>读未提交,性能最高</td></tr>
<tr><td>READ_COMMITTED</td><td></td><td></td><td></td><td>读已提交Oracle默认</td></tr>
<tr><td>REPEATABLE_READ</td><td></td><td></td><td></td><td>可重复读MySQL默认</td></tr>
<tr><td>SERIALIZABLE</td><td></td><td></td><td></td><td>串行化,性能最低</td></tr>
</table>
<p style="margin-top:10px;color:#666;">✅ = 防止该问题 | ❌ = 可能出现该问题</p>
<button class="btn" onclick="loadIsolation()">查看详细说明</button>
<div id="isoResult" class="result"></div>
</div>
<div class="card">
<h3>🧪 事务回滚演示</h3>
<p>创建订单时触发异常,观察事务回滚效果</p>
<button class="btn" onclick="loadRollback()">查看回滚规则</button>
<div id="rollbackResult" class="result"></div>
</div>
</div>
<script>
async function loadPropagation() {
const result = document.getElementById('propResult');
result.textContent = '加载中...';
try {
const res = await fetch('/api/learning/transaction/propagation');
const data = await res.json();
result.innerHTML = `<strong>传播行为详解</strong>\n\n${JSON.stringify(data, null, 2)}`;
} catch (e) {
result.textContent = '加载失败: ' + e.message;
}
}
async function loadIsolation() {
const result = document.getElementById('isoResult');
result.textContent = '加载中...';
try {
const res = await fetch('/api/learning/transaction/isolation');
const data = await res.json();
result.innerHTML = `<strong>隔离级别详解</strong>\n\n${JSON.stringify(data, null, 2)}`;
} catch (e) {
result.textContent = '加载失败: ' + e.message;
}
}
async function loadRollback() {
const result = document.getElementById('rollbackResult');
result.textContent = '加载中...';
try {
const res = await fetch('/api/learning/transaction/rollback');
const data = await res.json();
result.innerHTML = `<strong>回滚规则</strong>\n\n${JSON.stringify(data, null, 2)}`;
} catch (e) {
result.textContent = '加载失败: ' + e.message;
}
}
</script>
</body>
</html>

View File

@@ -0,0 +1,278 @@
<!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 CRUD</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, #11998e 0%, #38ef7d 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: #11998e; 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: #11998e; margin-bottom: 15px; border-bottom: 2px solid #eee; padding-bottom: 10px; }
.form-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(250px, 1fr)); gap: 15px; margin-bottom: 20px; }
.form-group { display: flex; flex-direction: column; }
.form-group label { margin-bottom: 5px; color: #666; font-size: 0.9em; }
.form-group input, .form-group textarea { padding: 10px; border: 1px solid #ddd; border-radius: 5px; font-size: 1em; }
.form-group input:focus, .form-group textarea:focus { outline: none; border-color: #11998e; }
.btn { padding: 10px 20px; border: none; border-radius: 5px; cursor: pointer; font-size: 1em; margin: 5px; }
.btn-primary { background: #11998e; color: white; }
.btn-primary:hover { background: #0d7a6e; }
.btn-secondary { background: #6c757d; color: white; }
.btn-danger { background: #dc3545; color: white; }
.btn-warning { background: #ffc107; color: #333; }
table { width: 100%; border-collapse: collapse; margin-top: 20px; }
th, td { padding: 12px; text-align: left; border-bottom: 1px solid #eee; }
th { background: #f8f9fa; font-weight: 600; }
tr:hover { background: #f8f9fa; }
.actions { display: flex; gap: 5px; }
.actions button { padding: 5px 10px; font-size: 0.85em; }
.badge { padding: 4px 8px; border-radius: 4px; font-size: 0.8em; }
.badge-active { background: #28a745; color: white; }
.badge-inactive { background: #6c757d; color: white; }
.search-box { display: flex; gap: 10px; margin-bottom: 20px; }
.search-box input { flex: 1; padding: 10px; border: 1px solid #ddd; border-radius: 5px; }
.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: 300px; overflow-y: auto; }
.result.success { background: #d4edda; color: #155724; }
.result.error { background: #f8d7da; color: #721c24; }
.modal { display: none; position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: rgba(0,0,0,0.5); z-index: 1000; }
.modal-content { background: white; margin: 50px auto; padding: 30px; width: 90%; max-width: 600px; border-radius: 10px; }
.modal-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 20px; }
.modal-header h3 { margin: 0; border: none; }
.close { font-size: 1.5em; cursor: pointer; color: #999; }
.close:hover { color: #333; }
</style>
</head>
<body>
<div class="container">
<div class="header">
<h1>👥 用户管理</h1>
<p>RESTful CRUD 操作演示</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" class="active">👥 用户</a>
<a href="api.html">🔌 API</a>
</div>
<div class="card">
<h3> 创建/编辑用户</h3>
<form id="userForm">
<input type="hidden" id="userId">
<div class="form-grid">
<div class="form-group">
<label>用户名 *</label>
<input type="text" id="username" required placeholder="输入用户名">
</div>
<div class="form-group">
<label>邮箱 *</label>
<input type="email" id="email" required placeholder="输入邮箱">
</div>
<div class="form-group">
<label>手机号</label>
<input type="text" id="phone" placeholder="输入手机号">
</div>
<div class="form-group">
<label>状态</label>
<select id="active" style="padding:10px;border:1px solid #ddd;border-radius:5px;">
<option value="true">启用</option>
<option value="false">禁用</option>
</select>
</div>
</div>
<div class="form-group" style="margin-bottom:15px;">
<label>简介</label>
<textarea id="bio" rows="3" placeholder="输入个人简介"></textarea>
</div>
<button type="submit" class="btn btn-primary">💾 保存</button>
<button type="button" class="btn btn-secondary" onclick="resetForm()">🔄 重置</button>
</form>
<div id="formResult"></div>
</div>
<div class="card">
<h3>📋 用户列表</h3>
<div class="search-box">
<input type="text" id="searchInput" placeholder="搜索用户名...">
<button class="btn btn-primary" onclick="searchUsers()">🔍 搜索</button>
<button class="btn btn-secondary" onclick="loadUsers()">🔄 刷新</button>
</div>
<table>
<thead>
<tr>
<th>ID</th>
<th>用户名</th>
<th>邮箱</th>
<th>手机号</th>
<th>状态</th>
<th>创建时间</th>
<th>操作</th>
</tr>
</thead>
<tbody id="userTable">
<tr><td colspan="7" style="text-align:center;color:#999;">加载中...</td></tr>
</tbody>
</table>
</div>
</div>
<script>
let users = [];
async function loadUsers() {
const tbody = document.getElementById('userTable');
tbody.innerHTML = '<tr><td colspan="7" style="text-align:center;color:#999;">加载中...</td></tr>';
try {
const res = await fetch('/api/users');
users = await res.json();
renderUsers(users);
} catch (e) {
tbody.innerHTML = `<tr><td colspan="7" style="text-align:center;color:#dc3545;">加载失败: ${e.message}</td></tr>`;
}
}
function renderUsers(data) {
const tbody = document.getElementById('userTable');
if (data.length === 0) {
tbody.innerHTML = '<tr><td colspan="7" style="text-align:center;color:#999;">暂无数据</td></tr>';
return;
}
tbody.innerHTML = data.map(u => `
<tr>
<td>${u.id}</td>
<td>${u.username}</td>
<td>${u.email}</td>
<td>${u.phone || '-'}</td>
<td><span class="badge ${u.active ? 'badge-active' : 'badge-inactive'}">${u.active ? '启用' : '禁用'}</span></td>
<td>${u.createdAt ? new Date(u.createdAt).toLocaleString() : '-'}</td>
<td class="actions">
<button class="btn btn-warning" onclick="editUser(${u.id})">✏️ 编辑</button>
<button class="btn btn-danger" onclick="deleteUser(${u.id})">🗑️ 删除</button>
</td>
</tr>
`).join('');
}
async function searchUsers() {
const keyword = document.getElementById('searchInput').value.trim();
if (!keyword) {
loadUsers();
return;
}
try {
const res = await fetch(`/api/users/search?username=${encodeURIComponent(keyword)}`);
const data = await res.json();
renderUsers(data);
} catch (e) {
alert('搜索失败: ' + e.message);
}
}
document.getElementById('userForm').addEventListener('submit', async (e) => {
e.preventDefault();
const userId = document.getElementById('userId').value;
const user = {
username: document.getElementById('username').value,
email: document.getElementById('email').value,
phone: document.getElementById('phone').value,
bio: document.getElementById('bio').value,
active: document.getElementById('active').value === 'true'
};
const resultDiv = document.getElementById('formResult');
resultDiv.className = 'result';
resultDiv.textContent = '保存中...';
try {
const url = userId ? `/api/users/${userId}` : '/api/users';
const method = userId ? 'PUT' : 'POST';
const res = await fetch(url, {
method: method,
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(user)
});
const data = await res.json();
if (res.ok) {
resultDiv.className = 'result success';
resultDiv.innerHTML = `<strong>✅ 保存成功!</strong><br>ID: ${data.id}, 用户名: ${data.username}`;
resetForm();
loadUsers();
} else {
resultDiv.className = 'result error';
resultDiv.innerHTML = `<strong>❌ 保存失败</strong><br>${JSON.stringify(data)}`;
}
} catch (e) {
resultDiv.className = 'result error';
resultDiv.innerHTML = `<strong>❌ 错误</strong><br>${e.message}`;
}
});
function editUser(id) {
const user = users.find(u => u.id === id);
if (!user) return;
document.getElementById('userId').value = user.id;
document.getElementById('username').value = user.username;
document.getElementById('email').value = user.email;
document.getElementById('phone').value = user.phone || '';
document.getElementById('bio').value = user.bio || '';
document.getElementById('active').value = user.active.toString();
document.getElementById('formResult').innerHTML = '<span style="color:#11998e;">✏️ 正在编辑用户 #' + id + '</span>';
window.scrollTo(0, 0);
}
async function deleteUser(id) {
if (!confirm(`确定要删除用户 #${id} 吗?`)) return;
try {
const res = await fetch(`/api/users/${id}`, { method: 'DELETE' });
if (res.ok) {
alert('✅ 删除成功');
loadUsers();
} else {
alert('❌ 删除失败');
}
} catch (e) {
alert('删除失败: ' + e.message);
}
}
function resetForm() {
document.getElementById('userForm').reset();
document.getElementById('userId').value = '';
document.getElementById('formResult').innerHTML = '';
}
// 初始化
loadUsers();
</script>
</body>
</html>