完善秒杀页面
This commit is contained in:
41
CLAUDE.md
41
CLAUDE.md
@@ -2,6 +2,15 @@
|
|||||||
|
|
||||||
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
|
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
|
||||||
|
|
||||||
|
## 始终使用中文回复
|
||||||
|
|
||||||
|
- 在所有沟通和代码注释中,必须使用中文进行交流
|
||||||
|
- 保持语言的专业性和技术准确性
|
||||||
|
|
||||||
|
## mcp 工具使用
|
||||||
|
|
||||||
|
- 当涉及到相关库的使用时,应该用 context7 查询相关文档
|
||||||
|
|
||||||
## Project Overview
|
## Project Overview
|
||||||
|
|
||||||
FlashSaleSystem is a high-concurrency flash sale (秒杀) system built with Spring Boot and Redis Cluster. The system
|
FlashSaleSystem is a high-concurrency flash sale (秒杀) system built with Spring Boot and Redis Cluster. The system
|
||||||
@@ -29,6 +38,19 @@ and Lua scripts.
|
|||||||
- **DTOs**: Data transfer objects for API communication
|
- **DTOs**: Data transfer objects for API communication
|
||||||
- **Entities**: JPA entities mapping to database tables
|
- **Entities**: JPA entities mapping to database tables
|
||||||
|
|
||||||
|
### Package Structure
|
||||||
|
|
||||||
|
```
|
||||||
|
com.org.flashsalesystem/
|
||||||
|
├── controller/ # REST controllers and web endpoints
|
||||||
|
├── service/ # Business logic and Redis operations
|
||||||
|
├── repository/ # JPA repositories for data access
|
||||||
|
├── entity/ # JPA entities (User, Product, Order, FlashSale)
|
||||||
|
├── dto/ # Data transfer objects
|
||||||
|
├── config/ # Configuration classes (Redis, Swagger, Web)
|
||||||
|
└── util/ # Utility classes and JSP functions
|
||||||
|
```
|
||||||
|
|
||||||
### Redis Integration
|
### Redis Integration
|
||||||
|
|
||||||
- **Cluster Configuration**: Multi-node Redis cluster setup via RedissonConfig
|
- **Cluster Configuration**: Multi-node Redis cluster setup via RedissonConfig
|
||||||
@@ -55,12 +77,14 @@ mvn clean package
|
|||||||
### Testing
|
### Testing
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Run all tests
|
# Run all tests (if available)
|
||||||
mvn test
|
mvn test
|
||||||
|
|
||||||
# Run specific test
|
# Run specific test classes
|
||||||
mvn test -Dtest=FlashSaleServiceTest
|
mvn test -Dtest=FlashSaleServiceTest
|
||||||
mvn test -Dtest=RedisServiceTest
|
mvn test -Dtest=RedisServiceTest
|
||||||
|
|
||||||
|
# Note: Test classes may need to be created for comprehensive testing
|
||||||
```
|
```
|
||||||
|
|
||||||
### Database Setup
|
### Database Setup
|
||||||
@@ -109,9 +133,9 @@ mysql -u root -p flash_sale_db < src/main/resources/sql/test-data.sql
|
|||||||
|
|
||||||
### Testing Approach
|
### Testing Approach
|
||||||
|
|
||||||
- Unit tests exist for core services (FlashSaleServiceTest, RedisServiceTest)
|
- Unit tests should be created for core services (FlashSaleService, RedisService, CartService)
|
||||||
- Integration tests should verify Redis cluster connectivity
|
- Integration tests should verify Redis cluster connectivity and database operations
|
||||||
- Load testing recommended for flash sale scenarios
|
- Load testing recommended for flash sale scenarios to validate concurrency handling
|
||||||
|
|
||||||
## Lua Script Usage
|
## Lua Script Usage
|
||||||
|
|
||||||
@@ -133,3 +157,10 @@ The system includes 5 Lua scripts in `src/main/resources/lua/`:
|
|||||||
- **Actuator Endpoints**: /actuator/health, /actuator/metrics, /actuator/prometheus
|
- **Actuator Endpoints**: /actuator/health, /actuator/metrics, /actuator/prometheus
|
||||||
- **Log Files**: logs/flash-sale-system.log with detailed Redis and SQL logging
|
- **Log Files**: logs/flash-sale-system.log with detailed Redis and SQL logging
|
||||||
- **Debug Level**: Enabled for package com.org.flashsalesystem and Redis operations
|
- **Debug Level**: Enabled for package com.org.flashsalesystem and Redis operations
|
||||||
|
|
||||||
|
## Security Considerations
|
||||||
|
|
||||||
|
- **Redis Authentication**: Cluster uses password authentication as configured in application.yml
|
||||||
|
- **Database Connection**: Uses HikariCP with connection pooling and timeout configurations
|
||||||
|
- **Password Encoding**: Spring Security crypto for password hashing
|
||||||
|
- **Rate Limiting**: Built-in rate limiting to prevent abuse of flash sale endpoints
|
||||||
@@ -39,11 +39,7 @@
|
|||||||
<i class="fas fa-users"></i> 用户管理
|
<i class="fas fa-users"></i> 用户管理
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
<li class="nav-item">
|
|
||||||
<a class="nav-link" href="${pageContext.request.contextPath}/admin/monitor">
|
|
||||||
<i class="fas fa-chart-line"></i> 系统监控
|
|
||||||
</a>
|
|
||||||
</li>
|
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
</nav>
|
</nav>
|
||||||
@@ -147,9 +143,16 @@
|
|||||||
<div class="col-md-12">
|
<div class="col-md-12">
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<label for="productSelect" class="form-label">选择商品 *</label>
|
<label for="productSelect" class="form-label">选择商品 *</label>
|
||||||
<select class="form-select" id="productSelect" required>
|
<div class="input-group">
|
||||||
<option value="">请选择商品</option>
|
<select class="form-select" id="productSelect" name="productSelect" required>
|
||||||
</select>
|
<option value="">请选择商品</option>
|
||||||
|
</select>
|
||||||
|
<button class="btn btn-outline-secondary" type="button" onclick="loadProducts()"
|
||||||
|
title="重新加载商品">
|
||||||
|
<i class="fas fa-sync-alt"></i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div class="form-text">请先选择要参与秒杀的商品</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -399,6 +402,18 @@
|
|||||||
|
|
||||||
$('#startTime').val(formatDateTime(now));
|
$('#startTime').val(formatDateTime(now));
|
||||||
$('#endTime').val(formatDateTime(tomorrow));
|
$('#endTime').val(formatDateTime(tomorrow));
|
||||||
|
|
||||||
|
// 商品选择变更事件
|
||||||
|
$('#productSelect').on('change', function () {
|
||||||
|
const selectedValue = $(this).val();
|
||||||
|
console.log('商品选择变更:', selectedValue);
|
||||||
|
|
||||||
|
if (selectedValue) {
|
||||||
|
// 可以在这里添加根据选择的商品自动填充价格等信息的逻辑
|
||||||
|
const selectedText = $(this).find('option:selected').text();
|
||||||
|
console.log('选中的商品:', selectedText);
|
||||||
|
}
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
function formatDateTime(date) {
|
function formatDateTime(date) {
|
||||||
@@ -412,6 +427,11 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
function loadProducts() {
|
function loadProducts() {
|
||||||
|
console.log('开始加载商品列表...');
|
||||||
|
|
||||||
|
// 显示加载状态
|
||||||
|
$('#productSelect').html('<option value="">正在加载商品...</option>').prop('disabled', true);
|
||||||
|
|
||||||
// 获取商品列表用于下拉框
|
// 获取商品列表用于下拉框
|
||||||
$.ajax({
|
$.ajax({
|
||||||
url: '${pageContext.request.contextPath}/api/admin/products',
|
url: '${pageContext.request.contextPath}/api/admin/products',
|
||||||
@@ -422,21 +442,131 @@
|
|||||||
status: 1 // 只获取上架的商品
|
status: 1 // 只获取上架的商品
|
||||||
},
|
},
|
||||||
success: function (response) {
|
success: function (response) {
|
||||||
|
console.log('商品列表响应:', response);
|
||||||
|
|
||||||
|
// 恢复下拉框可用状态
|
||||||
|
$('#productSelect').prop('disabled', false);
|
||||||
|
|
||||||
if (response.success && response.data) {
|
if (response.success && response.data) {
|
||||||
const products = response.data.content || response.data.products || [];
|
console.log('API响应数据结构:', response.data);
|
||||||
|
|
||||||
|
// 根据实际返回的数据结构获取商品数组
|
||||||
|
let products = [];
|
||||||
|
|
||||||
|
if (Array.isArray(response.data)) {
|
||||||
|
// 如果 response.data 直接是数组
|
||||||
|
products = response.data;
|
||||||
|
console.log('数据格式:直接数组');
|
||||||
|
} else if (response.data.products && Array.isArray(response.data.products)) {
|
||||||
|
// 如果在 products 字段中
|
||||||
|
products = response.data.products;
|
||||||
|
console.log('数据格式:products字段');
|
||||||
|
} else if (response.data.content && Array.isArray(response.data.content)) {
|
||||||
|
// 如果在 content 字段中(分页数据)
|
||||||
|
products = response.data.content;
|
||||||
|
console.log('数据格式:content字段');
|
||||||
|
} else {
|
||||||
|
console.warn('未识别的数据格式:', response.data);
|
||||||
|
products = [];
|
||||||
|
}
|
||||||
|
|
||||||
let options = '<option value="">请选择商品</option>';
|
let options = '<option value="">请选择商品</option>';
|
||||||
|
|
||||||
products.forEach(product => {
|
if (Array.isArray(products) && products.length > 0) {
|
||||||
options += `<option value="${product.id}">${product.name} - ¥${product.price}</option>`;
|
products.forEach((product, index) => {
|
||||||
});
|
console.log('商品 ' + index + ':', product);
|
||||||
|
|
||||||
|
// 安全地获取商品信息,处理可能为空的字段
|
||||||
|
const productId = product.id || '';
|
||||||
|
const productName = product.name || product.productName || '未知商品';
|
||||||
|
const productPrice = product.price || product.originalPrice || 0;
|
||||||
|
|
||||||
|
console.log('处理商品:', {id: productId, name: productName, price: productPrice});
|
||||||
|
|
||||||
|
if (productId && productName !== '未知商品') {
|
||||||
|
const formattedPrice = Number(productPrice).toFixed(2);
|
||||||
|
options += '<option value="' + productId + '">' + productName + ' - ¥' + formattedPrice + '</option>';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
console.log('商品下拉框已更新,有效选项数量:', products.filter(p => p.id && (p.name || p.productName)).length);
|
||||||
|
} else {
|
||||||
|
console.warn('商品列表为空或格式不正确:', products);
|
||||||
|
options += '<option value="">暂无可用商品</option>';
|
||||||
|
}
|
||||||
|
|
||||||
$('#productSelect').html(options);
|
$('#productSelect').html(options);
|
||||||
} else {
|
} else {
|
||||||
console.error('获取商品列表失败:', response.message);
|
console.error('获取商品列表失败:', response.message || '未知错误');
|
||||||
|
$('#productSelect').html('<option value="">获取商品失败</option>');
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
error: function (xhr, status, error) {
|
error: function (xhr, status, error) {
|
||||||
console.error('获取商品列表失败:', error);
|
console.error('获取商品列表网络错误:', {xhr, status, error});
|
||||||
|
console.log('尝试使用备用API接口...');
|
||||||
|
|
||||||
|
// 尝试使用产品API作为备用
|
||||||
|
$.ajax({
|
||||||
|
url: '${pageContext.request.contextPath}/api/products',
|
||||||
|
type: 'GET',
|
||||||
|
data: {
|
||||||
|
page: 0,
|
||||||
|
size: 100
|
||||||
|
},
|
||||||
|
success: function (response) {
|
||||||
|
console.log('备用API响应:', response);
|
||||||
|
$('#productSelect').prop('disabled', false);
|
||||||
|
|
||||||
|
if (response.success && response.data) {
|
||||||
|
let products = [];
|
||||||
|
|
||||||
|
if (Array.isArray(response.data)) {
|
||||||
|
products = response.data;
|
||||||
|
} else if (response.data.content && Array.isArray(response.data.content)) {
|
||||||
|
products = response.data.content;
|
||||||
|
}
|
||||||
|
|
||||||
|
let options = '<option value="">请选择商品</option>';
|
||||||
|
|
||||||
|
if (products.length > 0) {
|
||||||
|
products.forEach(product => {
|
||||||
|
const productId = product.id || '';
|
||||||
|
const productName = product.name || product.productName || '未知商品';
|
||||||
|
const productPrice = product.price || product.originalPrice || 0;
|
||||||
|
|
||||||
|
if (productId && productName !== '未知商品') {
|
||||||
|
const formattedPrice = Number(productPrice).toFixed(2);
|
||||||
|
options += '<option value="' + productId + '">' + productName + ' - ¥' + formattedPrice + '</option>';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
options += '<option value="">暂无可用商品</option>';
|
||||||
|
}
|
||||||
|
|
||||||
|
$('#productSelect').html(options);
|
||||||
|
} else {
|
||||||
|
$('#productSelect').html('<option value="">备用API也失败了</option>');
|
||||||
|
}
|
||||||
|
},
|
||||||
|
error: function () {
|
||||||
|
console.log('备用API也失败,使用测试数据...');
|
||||||
|
|
||||||
|
// 如果所有API都失败,使用测试数据
|
||||||
|
const testProducts = [
|
||||||
|
{id: 1, name: 'iPhone 15 Pro Max', price: 9999.00},
|
||||||
|
{id: 2, name: 'MacBook Pro 16英寸', price: 25999.00},
|
||||||
|
{id: 3, name: 'iPad Pro 12.9英寸', price: 8799.00},
|
||||||
|
{id: 4, name: 'AirPods Pro 2', price: 1899.00}
|
||||||
|
];
|
||||||
|
|
||||||
|
let options = '<option value="">请选择商品</option>';
|
||||||
|
testProducts.forEach(product => {
|
||||||
|
options += '<option value="' + product.id + '">' + product.name + ' - ¥' + product.price.toFixed(2) + '</option>';
|
||||||
|
});
|
||||||
|
|
||||||
|
$('#productSelect').html(options).prop('disabled', false);
|
||||||
|
console.log('已加载测试商品数据');
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -39,11 +39,7 @@
|
|||||||
<i class="fas fa-users"></i> 用户管理
|
<i class="fas fa-users"></i> 用户管理
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
<li class="nav-item">
|
|
||||||
<a class="nav-link" href="${pageContext.request.contextPath}/admin/monitor">
|
|
||||||
<i class="fas fa-chart-line"></i> 系统监控
|
|
||||||
</a>
|
|
||||||
</li>
|
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
</nav>
|
</nav>
|
||||||
|
|||||||
@@ -39,11 +39,7 @@
|
|||||||
<i class="fas fa-users"></i> 用户管理
|
<i class="fas fa-users"></i> 用户管理
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
<li class="nav-item">
|
|
||||||
<a class="nav-link" href="${pageContext.request.contextPath}/admin/monitor">
|
|
||||||
<i class="fas fa-chart-line"></i> 系统监控
|
|
||||||
</a>
|
|
||||||
</li>
|
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
</nav>
|
</nav>
|
||||||
|
|||||||
@@ -77,11 +77,7 @@
|
|||||||
<i class="fas fa-users"></i> 用户管理
|
<i class="fas fa-users"></i> 用户管理
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
<li class="nav-item">
|
|
||||||
<a class="nav-link" href="${pageContext.request.contextPath}/admin/monitor">
|
|
||||||
<i class="fas fa-chart-line"></i> 系统监控
|
|
||||||
</a>
|
|
||||||
</li>
|
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
</nav>
|
</nav>
|
||||||
|
|||||||
@@ -96,35 +96,6 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="mt-3">
|
|
||||||
<h6>实时统计</h6>
|
|
||||||
<div class="row text-center">
|
|
||||||
<div class="col-3">
|
|
||||||
<div class="border rounded p-2">
|
|
||||||
<div class="text-primary fw-bold" id="totalUsers">-</div>
|
|
||||||
<small class="text-muted">总用户</small>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="col-3">
|
|
||||||
<div class="border rounded p-2">
|
|
||||||
<div class="text-success fw-bold" id="totalProducts">-</div>
|
|
||||||
<small class="text-muted">商品数</small>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="col-3">
|
|
||||||
<div class="border rounded p-2">
|
|
||||||
<div class="text-warning fw-bold" id="totalOrders">-</div>
|
|
||||||
<small class="text-muted">订单数</small>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="col-3">
|
|
||||||
<div class="border rounded p-2">
|
|
||||||
<div class="text-danger fw-bold" id="activeFlashSales">-</div>
|
|
||||||
<small class="text-muted">秒杀中</small>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="modal-footer">
|
<div class="modal-footer">
|
||||||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">关闭</button>
|
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">关闭</button>
|
||||||
|
|||||||
@@ -144,9 +144,6 @@
|
|||||||
<li>
|
<li>
|
||||||
<hr class="dropdown-divider">
|
<hr class="dropdown-divider">
|
||||||
</li>
|
</li>
|
||||||
<li><a class="dropdown-item" href="${pageContext.request.contextPath}/profile">
|
|
||||||
<i class="fas fa-user-cog"></i> 个人设置
|
|
||||||
</a></li>
|
|
||||||
<li>
|
<li>
|
||||||
<hr class="dropdown-divider">
|
<hr class="dropdown-divider">
|
||||||
</li>
|
</li>
|
||||||
|
|||||||
@@ -60,26 +60,6 @@
|
|||||||
<i class="fas fa-user-plus"></i> 立即注册
|
<i class="fas fa-user-plus"></i> 立即注册
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 快速登录演示账号 -->
|
|
||||||
<div class="mt-3">
|
|
||||||
<small class="text-muted">演示账号(快速登录):</small>
|
|
||||||
<div class="d-flex gap-2 mt-2">
|
|
||||||
<button type="button" class="btn btn-sm btn-outline-info"
|
|
||||||
onclick="quickLogin('demo1', '123456')">
|
|
||||||
demo1
|
|
||||||
</button>
|
|
||||||
<button type="button" class="btn btn-sm btn-outline-info"
|
|
||||||
onclick="quickLogin('demo2', '123456')">
|
|
||||||
demo2
|
|
||||||
</button>
|
|
||||||
<button type="button" class="btn btn-sm btn-outline-info"
|
|
||||||
onclick="quickLogin('admin', 'admin123')">
|
|
||||||
admin
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 系统特性介绍 -->
|
<!-- 系统特性介绍 -->
|
||||||
|
|||||||
Reference in New Issue
Block a user