feat: Spring Boot 学习脚手架 v2.0
- 新增 IoC 容器学习模块 - 新增 AOP 切面编程学习模块 - 新增 MyBatis 集成学习模块 - 新增事务管理学习模块 - 新增用户/产品/订单 CRUD - 新增 7 个交互式学习页面 - 集成性能监控切面
This commit is contained in:
22
target/classes/application.properties
Normal file
22
target/classes/application.properties
Normal 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
|
||||
Binary file not shown.
BIN
target/classes/com/example/scaffold/aop/LearningAspect.class
Normal file
BIN
target/classes/com/example/scaffold/aop/LearningAspect.class
Normal file
Binary file not shown.
Binary file not shown.
BIN
target/classes/com/example/scaffold/aop/PerformanceAspect.class
Normal file
BIN
target/classes/com/example/scaffold/aop/PerformanceAspect.class
Normal file
Binary file not shown.
BIN
target/classes/com/example/scaffold/config/AppConfig.class
Normal file
BIN
target/classes/com/example/scaffold/config/AppConfig.class
Normal file
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
target/classes/com/example/scaffold/dto/OrderCreateRequest.class
Normal file
BIN
target/classes/com/example/scaffold/dto/OrderCreateRequest.class
Normal file
Binary file not shown.
Binary file not shown.
BIN
target/classes/com/example/scaffold/dto/UserCreateRequest.class
Normal file
BIN
target/classes/com/example/scaffold/dto/UserCreateRequest.class
Normal file
Binary file not shown.
BIN
target/classes/com/example/scaffold/entity/Order.class
Normal file
BIN
target/classes/com/example/scaffold/entity/Order.class
Normal file
Binary file not shown.
BIN
target/classes/com/example/scaffold/entity/Product.class
Normal file
BIN
target/classes/com/example/scaffold/entity/Product.class
Normal file
Binary file not shown.
BIN
target/classes/com/example/scaffold/entity/User.class
Normal file
BIN
target/classes/com/example/scaffold/entity/User.class
Normal file
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
target/classes/com/example/scaffold/mapper/OrderMapper.class
Normal file
BIN
target/classes/com/example/scaffold/mapper/OrderMapper.class
Normal file
Binary file not shown.
BIN
target/classes/com/example/scaffold/mapper/ProductMapper.class
Normal file
BIN
target/classes/com/example/scaffold/mapper/ProductMapper.class
Normal file
Binary file not shown.
BIN
target/classes/com/example/scaffold/mapper/UserMapper.class
Normal file
BIN
target/classes/com/example/scaffold/mapper/UserMapper.class
Normal file
Binary file not shown.
BIN
target/classes/com/example/scaffold/service/UserService.class
Normal file
BIN
target/classes/com/example/scaffold/service/UserService.class
Normal file
Binary file not shown.
Binary file not shown.
Binary file not shown.
159
target/classes/static/aop.html
Normal file
159
target/classes/static/aop.html
Normal 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>
|
||||
280
target/classes/static/api.html
Normal file
280
target/classes/static/api.html
Normal 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>
|
||||
203
target/classes/static/index.html
Normal file
203
target/classes/static/index.html
Normal 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>
|
||||
175
target/classes/static/ioc.html
Normal file
175
target/classes/static/ioc.html
Normal 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>
|
||||
176
target/classes/static/mybatis.html
Normal file
176
target/classes/static/mybatis.html
Normal 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"><select id="findUsers"><br> SELECT * FROM users<br> <where><br> <if test="name != null"><br> AND name LIKE CONCAT('%', #{name}, '%')<br> </if><br> <if test="email != null"><br> AND email = #{email}<br> </if><br> </where><br></select></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>
|
||||
191
target/classes/static/transaction.html
Normal file
191
target/classes/static/transaction.html
Normal 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>
|
||||
278
target/classes/static/users.html
Normal file
278
target/classes/static/users.html
Normal 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>
|
||||
3
target/maven-archiver/pom.properties
Normal file
3
target/maven-archiver/pom.properties
Normal file
@@ -0,0 +1,3 @@
|
||||
artifactId=springboot-scaffold
|
||||
groupId=com.example
|
||||
version=1.0.0
|
||||
@@ -0,0 +1,26 @@
|
||||
com/example/scaffold/dto/OrderCreateRequest.class
|
||||
com/example/scaffold/learning/MyBatisLearningController.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/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/mapper/OrderMapper.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/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/entity/Product.class
|
||||
@@ -0,0 +1,24 @@
|
||||
/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/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/aop/PerformanceAspect.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/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/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
|
||||
BIN
target/springboot-scaffold-1.0.0.jar
Normal file
BIN
target/springboot-scaffold-1.0.0.jar
Normal file
Binary file not shown.
BIN
target/springboot-scaffold-1.0.0.jar.original
Normal file
BIN
target/springboot-scaffold-1.0.0.jar.original
Normal file
Binary file not shown.
Reference in New Issue
Block a user