admin 页面增加统计功能
This commit is contained in:
parent
707b8c2551
commit
9025507bf3
@ -6,23 +6,25 @@ import cn.hutool.crypto.symmetric.AES;
|
||||
import cn.hutool.json.JSONUtil;
|
||||
import eu.org.biwin.screen.model.GenerateCodeRequest;
|
||||
import eu.org.biwin.screen.model.ImageGroup;
|
||||
import jakarta.annotation.PostConstruct;
|
||||
import jakarta.servlet.http.HttpServletResponse;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.dao.DataAccessException;
|
||||
import org.springframework.jdbc.core.JdbcTemplate;
|
||||
import org.springframework.stereotype.Controller;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
import org.springframework.util.StringUtils;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.PathVariable;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.RequestBody;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.ResponseBody;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import javax.sql.DataSource;
|
||||
import java.io.File;
|
||||
import java.io.FileInputStream;
|
||||
import java.io.IOException;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.sql.Connection;
|
||||
import java.sql.SQLException;
|
||||
import java.sql.Statement;
|
||||
import java.time.LocalDateTime;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.util.HashMap;
|
||||
@ -35,12 +37,14 @@ import java.util.zip.ZipOutputStream;
|
||||
|
||||
@Controller
|
||||
@RequestMapping("/api/admin")
|
||||
// **[REMOVED]** Removed @SaCheckRole("admin") from the class level
|
||||
public class AdminController {
|
||||
|
||||
@Autowired
|
||||
private JdbcTemplate jdbcTemplate;
|
||||
|
||||
@Autowired
|
||||
private DataSource dataSource;
|
||||
|
||||
@Value("${file.path}")
|
||||
private String rootPath;
|
||||
@Value("${app.secure-key}")
|
||||
@ -50,27 +54,42 @@ public class AdminController {
|
||||
|
||||
private static final DateTimeFormatter formatter = DateTimeFormatter.ISO_LOCAL_DATE_TIME;
|
||||
|
||||
// This method is now PUBLIC and requires NO LOGIN
|
||||
@PostConstruct
|
||||
public void init() {
|
||||
try (Connection conn = dataSource.getConnection(); Statement stmt = conn.createStatement()) {
|
||||
// Create delivered auth codes table
|
||||
stmt.execute("CREATE TABLE IF NOT EXISTS delivered_auth_codes (" +
|
||||
"id INTEGER PRIMARY KEY AUTOINCREMENT," +
|
||||
"code TEXT NOT NULL UNIQUE," +
|
||||
"order_no TEXT," +
|
||||
"login_limit INTEGER DEFAULT 1," +
|
||||
"login_count INTEGER DEFAULT 0," +
|
||||
"created_at TEXT," +
|
||||
"expires_at TEXT," +
|
||||
"delivered_at TEXT," +
|
||||
"status INTEGER DEFAULT 2)"); // 2 for delivered
|
||||
|
||||
// Create delivered cart items table
|
||||
stmt.execute("CREATE TABLE IF NOT EXISTS delivered_cart_items (" +
|
||||
"id INTEGER PRIMARY KEY AUTOINCREMENT," +
|
||||
"auth_code TEXT NOT NULL," +
|
||||
"item_id TEXT NOT NULL)");
|
||||
} catch (SQLException e) {
|
||||
throw new RuntimeException("Failed to create delivered tables", e);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@PostMapping("/generate-code")
|
||||
@ResponseBody
|
||||
public String generateCode(@RequestBody GenerateCodeRequest request) {
|
||||
if (StringUtils.hasText(request.getAuthCode()) && Objects.equals(request.getAuthCode(), secureAuth)) {
|
||||
|
||||
|
||||
String orderNo = request.getOrderNo();
|
||||
Integer loginLimit = request.getLoginLimit();
|
||||
Long expireSeconds = request.getExpireSeconds();
|
||||
|
||||
// 1. 设置 12 小时后的过期时间戳
|
||||
long expireTime = System.currentTimeMillis() + (Objects.isNull(expireSeconds) ? 12 * 60 * 60 * 1000 : expireSeconds);
|
||||
|
||||
// 2. 创建 AES 实例(Hutool 会自动根据 16 字节 key 选择 AES-128)
|
||||
AES aes = SecureUtil.aes(secureKey.getBytes(StandardCharsets.UTF_8));
|
||||
|
||||
// 3. 加密时间戳并返回
|
||||
String loginCode = aes.encryptHex(String.valueOf(expireTime));
|
||||
|
||||
|
||||
LocalDateTime now = LocalDateTime.now();
|
||||
LocalDateTime expiresAt = now.plusSeconds(expireSeconds);
|
||||
String sql = "INSERT INTO auth_codes (code, order_no, login_limit, created_at, expires_at, status) VALUES (?, ?, ?, ?, ?, ?)";
|
||||
@ -85,31 +104,64 @@ public class AdminController {
|
||||
|
||||
@GetMapping("/codes")
|
||||
@ResponseBody
|
||||
@SaCheckRole("admin") // Add annotation to each protected method
|
||||
@SaCheckRole("admin")
|
||||
public List<Map<String, Object>> getAuthCodes() {
|
||||
return jdbcTemplate.queryForList("SELECT code, order_no, login_limit, login_count, expires_at FROM auth_codes ORDER BY created_at DESC");
|
||||
return jdbcTemplate.queryForList("SELECT code, order_no, login_limit, login_count, expires_at FROM auth_codes WHERE status = 1 ORDER BY created_at DESC");
|
||||
}
|
||||
|
||||
@GetMapping("/cart/{code}")
|
||||
@ResponseBody
|
||||
@SaCheckRole("admin") // Add annotation to each protected method
|
||||
@SaCheckRole("admin")
|
||||
public List<ImageGroup> getCartForCode(@PathVariable String code) {
|
||||
List<String> itemIds = jdbcTemplate.queryForList("SELECT item_id FROM cart_items WHERE auth_code = ?", String.class, code);
|
||||
return itemIds.stream().map(this::createImageGroupFromId).filter(Objects::nonNull).collect(Collectors.toList());
|
||||
}
|
||||
|
||||
@GetMapping("/download-cart/{code}")
|
||||
@SaCheckRole("admin") // Add annotation to each protected method
|
||||
public void downloadCartAsZip(@PathVariable String code, HttpServletResponse response) throws IOException {
|
||||
@PostMapping("/deliver/{code}")
|
||||
@ResponseBody
|
||||
@SaCheckRole("admin")
|
||||
@Transactional
|
||||
public void deliverCode(@PathVariable String code) {
|
||||
// 1. Get the auth_code details
|
||||
Map<String, Object> authCode = jdbcTemplate.queryForMap("SELECT * FROM auth_codes WHERE code = ?", code);
|
||||
|
||||
// 2. Insert into delivered_auth_codes
|
||||
jdbcTemplate.update("INSERT INTO delivered_auth_codes (code, order_no, login_limit, login_count, created_at, expires_at, delivered_at, status) VALUES (?, ?, ?, ?, ?, ?, ?, ?)",
|
||||
authCode.get("code"), authCode.get("order_no"), authCode.get("login_limit"), authCode.get("login_count"),
|
||||
authCode.get("created_at"), authCode.get("expires_at"), formatter.format(LocalDateTime.now()), 2);
|
||||
|
||||
// 3. Get cart items
|
||||
List<String> itemIds = jdbcTemplate.queryForList("SELECT item_id FROM cart_items WHERE auth_code = ?", String.class, code);
|
||||
|
||||
// 4. Insert into delivered_cart_items
|
||||
for (String itemId : itemIds) {
|
||||
jdbcTemplate.update("INSERT INTO delivered_cart_items (auth_code, item_id) VALUES (?, ?)", code, itemId);
|
||||
}
|
||||
|
||||
// 5. Delete from original tables
|
||||
jdbcTemplate.update("DELETE FROM cart_items WHERE auth_code = ?", code);
|
||||
jdbcTemplate.update("DELETE FROM auth_codes WHERE code = ?", code);
|
||||
}
|
||||
|
||||
@PostMapping("/cancel/{code}")
|
||||
@ResponseBody
|
||||
@SaCheckRole("admin")
|
||||
@Transactional
|
||||
public void cancelCode(@PathVariable String code) {
|
||||
jdbcTemplate.update("DELETE FROM cart_items WHERE auth_code = ?", code);
|
||||
jdbcTemplate.update("DELETE FROM auth_codes WHERE code = ?", code);
|
||||
}
|
||||
|
||||
|
||||
@GetMapping("/download-cart/{code}")
|
||||
@SaCheckRole("admin")
|
||||
public void downloadCartAsZip(@PathVariable String code, HttpServletResponse response) throws IOException {
|
||||
List<String> itemIds = jdbcTemplate.queryForList("SELECT item_id FROM cart_items WHERE auth_code = ?", String.class, code);
|
||||
Map<String, Object> codeDetails = jdbcTemplate.queryForMap("SELECT order_no FROM auth_codes WHERE code = ?", code);
|
||||
String orderId = (String) codeDetails.get("order_no");
|
||||
String zipFileName = (orderId != null ? orderId.replaceAll("[^a-zA-Z0-9]", "_") : code) + "_templates.zip";
|
||||
|
||||
response.setContentType("application/zip");
|
||||
response.setHeader("Content-Disposition", "attachment; filename=\"" + zipFileName + "\"");
|
||||
|
||||
try (ZipOutputStream zos = new ZipOutputStream(response.getOutputStream())) {
|
||||
for (String itemId : itemIds) {
|
||||
File templateFile = findTemplateFile(itemId);
|
||||
@ -129,20 +181,17 @@ public class AdminController {
|
||||
}
|
||||
}
|
||||
|
||||
// ... (Helper methods remain the same)
|
||||
private ImageGroup createImageGroupFromId(String itemId) {
|
||||
String displayName = new File(itemId).getName();
|
||||
ImageGroup group = new ImageGroup(itemId, displayName);
|
||||
File effectFile = findFile(itemId, "effect");
|
||||
File templateFile = findFile(itemId, "template");
|
||||
if (effectFile == null) return null;
|
||||
|
||||
String rootAbsolutePath = new File(rootPath).getAbsolutePath();
|
||||
java.util.function.Function<File, String> getUrl = (file) -> {
|
||||
if (file == null) return null;
|
||||
return file.getAbsolutePath().substring(rootAbsolutePath.length()).replace("\\", "/");
|
||||
};
|
||||
|
||||
group.setEffectImageUrl(getUrl.apply(effectFile));
|
||||
group.setEffectThumbnailUrl(getUrl.apply(effectFile));
|
||||
group.setTemplateImageUrl(getUrl.apply(templateFile));
|
||||
|
||||
@ -53,12 +53,58 @@
|
||||
margin-bottom: 10px;
|
||||
border-radius: 4px;
|
||||
border: 1px solid #ccc;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.code-list-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.code-list-header h3 {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.code-list-header #code-count {
|
||||
font-size: 14px;
|
||||
font-weight: bold;
|
||||
color: var(--primary-color);
|
||||
}
|
||||
|
||||
.cart-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.cart-header h2 {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.cart-header #cart-item-count {
|
||||
font-size: 16px;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.code-list-item { padding: 12px; border-bottom: 1px solid #eee; cursor: pointer; border-radius: 6px; }
|
||||
.code-list-item:hover, .code-list-item.active { background-color: #d1e7fd; }
|
||||
.code-list-item strong { font-size: 14px; color: #333; }
|
||||
.code-list-item span { font-size: 12px; color: #777; display: block; margin-top: 4px; }
|
||||
.code-list-item .actions { margin-top: 8px; display: flex; gap: 8px; }
|
||||
.code-list-item .actions button {
|
||||
padding: 4px 8px;
|
||||
font-size: 10px;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
}
|
||||
.code-list-item .actions .deliver-btn { background-color: #28a745; color: white; }
|
||||
.code-list-item .actions .cancel-btn { background-color: #dc3545; color: white; }
|
||||
|
||||
|
||||
.grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(200px, 1fr)); gap: 15px; margin-top: 20px; }
|
||||
.grid-item { background: #fff; border-radius: 10px; overflow: hidden; box-shadow: var(--card-shadow); }
|
||||
.grid-item img { width: 100%; height: auto; display: block; }
|
||||
@ -73,13 +119,19 @@
|
||||
<div id="particles-js"></div>
|
||||
|
||||
<div class="sidebar" id="sidebar">
|
||||
<h3>授权码列表</h3>
|
||||
<div class="code-list-header">
|
||||
<h3>授权码列表</h3>
|
||||
<div id="code-count"></div>
|
||||
</div>
|
||||
<input type="text" id="search-input" placeholder="搜索授权码...">
|
||||
<div id="code-list"></div>
|
||||
</div>
|
||||
|
||||
<div class="main-content">
|
||||
<h2>购物车详情</h2>
|
||||
<div class="cart-header">
|
||||
<h2>购物车详情</h2>
|
||||
<div id="cart-item-count"></div>
|
||||
</div>
|
||||
<button id="download-btn">打包下载模板原图</button>
|
||||
<div class="grid" id="cart-grid"></div>
|
||||
<div id="loader">请从左侧选择一个授权码查看</div>
|
||||
@ -91,6 +143,8 @@
|
||||
const loaderEl = document.getElementById('loader');
|
||||
const downloadBtn = document.getElementById('download-btn');
|
||||
const searchInput = document.getElementById('search-input');
|
||||
const codeCountEl = document.getElementById('code-count');
|
||||
const cartItemCountEl = document.getElementById('cart-item-count');
|
||||
let activeCode = null;
|
||||
let allCodes = [];
|
||||
|
||||
@ -108,17 +162,26 @@
|
||||
function renderCodeList() {
|
||||
const searchTerm = searchInput.value.toLowerCase();
|
||||
const filteredCodes = allCodes.filter(code =>
|
||||
(code.order_id && code.order_id.toLowerCase().includes(searchTerm)) ||
|
||||
code.code.toLowerCase().includes(searchTerm)
|
||||
(code.order_no && code.order_no.toLowerCase().includes(searchTerm)) ||
|
||||
code.code.toLowerCase().includes(searchTerm)
|
||||
);
|
||||
|
||||
codeCountEl.innerText = `待处理: ${allCodes.length}`;
|
||||
codeListEl.innerHTML = '';
|
||||
filteredCodes.forEach(code => {
|
||||
const item = document.createElement('div');
|
||||
item.className = 'code-list-item';
|
||||
item.dataset.code = code.code;
|
||||
item.innerHTML = `<strong>${code.order_id || code.code}</strong><span>登录/上限:${code.login_count}/${code.login_limit} | 失效: ${new Date(code.expires_at).toLocaleString()}</span>`;
|
||||
item.onclick = () => loadCartForCode(code.code);
|
||||
item.innerHTML = `
|
||||
<div onclick="loadCartForCode('${code.code}')">
|
||||
<strong>${code.order_no || code.code}</strong>
|
||||
<span>登录/上限:${code.login_count}/${code.login_limit} | 失效: ${new Date(code.expires_at).toLocaleString()}</span>
|
||||
</div>
|
||||
<div class="actions">
|
||||
<button class="deliver-btn" onclick="deliverCodeAction('${code.code}', event)">已交付</button>
|
||||
<button class="cancel-btn" onclick="cancelCodeAction('${code.code}', event)">已取消</button>
|
||||
</div>
|
||||
`;
|
||||
codeListEl.appendChild(item);
|
||||
});
|
||||
|
||||
@ -130,11 +193,38 @@
|
||||
}
|
||||
}
|
||||
|
||||
async function deliverCodeAction(code, event) {
|
||||
event.stopPropagation();
|
||||
if (!confirm(`确定要将授权码 ${code} 标记为已交付吗?`)) return;
|
||||
|
||||
try {
|
||||
const response = await fetch(`/api/admin/deliver/${code}`, { method: 'POST' });
|
||||
if (!response.ok) throw new Error('操作失败');
|
||||
fetchAuthCodes(); // Refresh the list
|
||||
} catch (e) {
|
||||
alert(e.message);
|
||||
}
|
||||
}
|
||||
|
||||
async function cancelCodeAction(code, event) {
|
||||
event.stopPropagation();
|
||||
if (!confirm(`确定要取消授权码 ${code} 吗?此操作不可逆!`)) return;
|
||||
|
||||
try {
|
||||
const response = await fetch(`/api/admin/cancel/${code}`, { method: 'POST' });
|
||||
if (!response.ok) throw new Error('操作失败');
|
||||
fetchAuthCodes(); // Refresh the list
|
||||
} catch (e) {
|
||||
alert(e.message);
|
||||
}
|
||||
}
|
||||
|
||||
async function loadCartForCode(code) {
|
||||
activeCode = code;
|
||||
renderCodeList(); // Re-render to highlight the active item
|
||||
|
||||
cartGridEl.innerHTML = '';
|
||||
cartItemCountEl.innerText = '';
|
||||
loaderEl.innerText = '正在加载...';
|
||||
loaderEl.style.display = 'block';
|
||||
downloadBtn.style.display = 'none';
|
||||
@ -144,6 +234,8 @@
|
||||
if (!response.ok) throw new Error('Failed to load cart');
|
||||
const items = await response.json();
|
||||
|
||||
cartItemCountEl.innerText = `总数: ${items.length}`;
|
||||
|
||||
if (items.length === 0) {
|
||||
loaderEl.innerText = '这个购物车是空的';
|
||||
return;
|
||||
@ -177,9 +269,8 @@
|
||||
const a = document.createElement('a');
|
||||
a.style.display = 'none';
|
||||
a.href = url;
|
||||
// Use order_id for filename if available
|
||||
const activeItem = document.querySelector(`[data-code="${activeCode}"] strong`);
|
||||
const filename = `${activeItem.innerText.replace(/\\s+/g, '_')}_templates.zip`;
|
||||
const filename = `${activeItem.innerText.replace(/\s+/g, '_')}_templates.zip`;
|
||||
a.download = filename;
|
||||
document.body.appendChild(a);
|
||||
a.click();
|
||||
|
||||
Loading…
Reference in New Issue
Block a user