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));
+ group.setTemplateThumbnailUrl(getUrl.apply(templateFile));
+
+ return group;
+ }
+}
diff --git a/src/main/java/eu/org/biwin/screen/controller/GalleryController.java b/src/main/java/eu/org/biwin/screen/controller/GalleryController.java
index 606a616..e106a6f 100644
--- a/src/main/java/eu/org/biwin/screen/controller/GalleryController.java
+++ b/src/main/java/eu/org/biwin/screen/controller/GalleryController.java
@@ -1,12 +1,13 @@
package eu.org.biwin.screen.controller;
import cn.dev33.satoken.stp.StpUtil;
-import cn.hutool.core.io.FileUtil;
-import cn.hutool.crypto.SecureUtil;
-import cn.hutool.crypto.symmetric.AES;
+import eu.org.biwin.screen.model.ImageGroup;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
+import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
+import org.springframework.dao.EmptyResultDataAccessException;
+import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
@@ -15,181 +16,134 @@ import org.springframework.web.bind.annotation.ResponseBody;
import java.io.File;
import java.io.IOException;
-import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
+import java.time.LocalDateTime;
+import java.time.format.DateTimeFormatter;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Comparator;
+import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
+import java.util.stream.Collectors;
-/**
- * 主控制器,包含登录,图片加载等功能
- *
- * @author wangli
- * @since 2025-12-20 19:57
- */
@Controller
public class GalleryController {
- /**
- * A robust, natural-order comparator that mimics modern file managers (like macOS Finder).
- * This is essential because `File.listFiles()` provides no order guarantee,
- * and an unstable sort leads to random-seeming results on each refresh.
- * This comparator fixes that by enforcing a stable and predictable sort order.
- *
- * Sorting Rules:
- * 1. Directories are always sorted before files.
- * 2. Filenames are chunked into numbers ("num") and text ("str").
- * 3. Number chunks are compared numerically (2 before 10).
- * 4. Text chunks are compared case-insensitively.
- * 5. The sort is stable, preventing random reordering on refresh.
- */
private static final Comparator FILE_MANAGER_COMPARATOR = (f1, f2) -> {
- // Rule 1: Directories first
if (f1.isDirectory() && !f2.isDirectory()) return -1;
if (!f1.isDirectory() && f2.isDirectory()) return 1;
-
String s1 = f1.getName();
String s2 = f2.getName();
-
Pattern pattern = Pattern.compile("(?\\d+)|(?\\D+)");
Matcher m1 = pattern.matcher(s1);
Matcher m2 = pattern.matcher(s2);
-
while (m1.find() && m2.find()) {
boolean isNum1 = m1.group("num") != null;
boolean isNum2 = m2.group("num") != null;
-
- // Case 1: Both chunks are numbers
if (isNum1 && isNum2) {
String numStr1 = m1.group("num");
String numStr2 = m2.group("num");
try {
long val1 = Long.parseLong(numStr1);
long val2 = Long.parseLong(numStr2);
- if (val1 != val2) {
- return Long.compare(val1, val2);
- }
- // If values are equal (e.g., 1 and 01), the one with more leading zeros comes later.
+ if (val1 != val2) return Long.compare(val1, val2);
int lenCompare = numStr1.length() - numStr2.length();
- if (lenCompare != 0) {
- return lenCompare;
- }
- continue; // Identical numbers, move to next chunk
+ if (lenCompare != 0) return lenCompare;
+ continue;
} catch (NumberFormatException e) {
- // Fallback to text compare for extremely large numbers
}
}
-
- // Case 2: One is a number, the other is text. Number comes first.
if (isNum1) return -1;
if (isNum2) return 1;
-
- // Case 3: Both are text. Compare case-insensitively.
int res = m1.group("str").compareToIgnoreCase(m2.group("str"));
- if (res != 0) {
- return res;
- }
+ if (res != 0) return res;
}
-
- // If one name is a prefix of the other, the shorter one comes first.
- if (m1.find()) return 1; // s1 is longer
- if (m2.find()) return -1; // s2 is longer
-
+ if (m1.find()) return 1;
+ if (m2.find()) return -1;
return 0;
};
@Value("${file.path}")
private String rootPath;
- @Value("${file.auth-code-file}")
- private String authCodeFile;
@Value("${app.secure-password}")
private String securePassword;
- @Value("${app.secure-key}")
- private String secureKey;
- // 登录页面
+ @Autowired
+ private JdbcTemplate jdbcTemplate;
+
+ private static final DateTimeFormatter formatter = DateTimeFormatter.ISO_LOCAL_DATE_TIME;
+
@GetMapping("/login")
public String loginPage() {
return "login";
}
- // 登录认证逻辑
- @PostMapping("/doLogin")
- @ResponseBody
- public String doLogin(String password) {
- // 1. 首先检查是否匹配固定密码
- if (securePassword.equals(password)) {
- StpUtil.login("admin"); // 登录成功
- return "ok";
- }
-
- // 2. 如果不是固定密码,再尝试作为动态码解密
- try {
- AES aes = SecureUtil.aes(secureKey.getBytes(StandardCharsets.UTF_8));
- String decrypted = aes.decryptStr(password);
- long expireTime = Long.parseLong(decrypted);
-
- // 3. 检查时间是否超过 12 小时
- if (System.currentTimeMillis() > expireTime) {
- return "该登录码已过期";
- }
-
- // 4. 状态文件校验 (控制手动失效)
- File file = new File(rootPath, authCodeFile);
- if (file.exists()) {
- List lines = FileUtil.readUtf8Lines(file);
- boolean isValid = lines.stream().anyMatch(line -> line.equals(password + ",1"));
- if (!isValid) {
- return "该授权码已被管理员作废";
- }
- }
-
- // 5. 验证通过,执行 Sa-Token 登录 (维持 30 分钟会话)
- StpUtil.login("admin");
- return "ok";
- } catch (Exception e) {
- // 解密失败或格式错误,说明是无效的授权码
- return "无效的授权码或密码";
- }
- }
-
- // 1. 仅负责跳转到页面
@GetMapping("/")
public String index() {
return "index";
}
- private boolean isImage(String name) {
- String n = name.toLowerCase();
- return n.endsWith(".jpg") || n.endsWith(".jpeg") || n.endsWith(".png") || n.endsWith(".gif");
+ /**
+ * [NEW] Serves the cart page, consistent with other page-serving methods.
+ */
+ @GetMapping("/cart")
+ public String cartPage() {
+ return "cart";
}
- // 在 GalleryController 或单独的类中添加
- @GetMapping("/raw/**") // 我们统一用 /raw 作为原图前缀
+ @PostMapping("/doLogin")
+ @ResponseBody
+ public String doLogin(String password) {
+ if (securePassword.equals(password)) {
+ StpUtil.login("admin-root");
+ return "ok";
+ }
+ try {
+ String sql = "SELECT * FROM auth_codes WHERE code = ?";
+ Map codeDetails = jdbcTemplate.queryForMap(sql, password);
+ if ((int) codeDetails.get("status") != 1) return "该授权码已被停用";
+ if (LocalDateTime.now().isAfter(LocalDateTime.parse((String) codeDetails.get("expires_at"), formatter)))
+ return "该授权码已过期";
+ if ((int) codeDetails.get("login_limit") > 0) {
+ if ((int) codeDetails.get("login_count") >= (int) codeDetails.get("login_limit"))
+ return "该授权码已达到最大登录次数";
+ }
+
+ jdbcTemplate.update("UPDATE auth_codes SET login_count = login_count + 1 WHERE code = ?", password);
+ StpUtil.login("user-" + password);
+ return "ok";
+ } catch (EmptyResultDataAccessException e) {
+ return "无效的授权码或密码";
+ } catch (Exception e) {
+ e.printStackTrace();
+ return "服务器内部错误";
+ }
+ }
+
+ private boolean isImage(String name) {
+ String n = name.toLowerCase();
+ return n.endsWith(".jpg") || n.endsWith(".JPG") || n.endsWith(".png") || n.endsWith(".PNG");
+ }
+
+ @GetMapping("/raw/**")
public void getRawImage(HttpServletRequest request, HttpServletResponse response) throws IOException {
-
- // 获取 /raw/ 之后的所有路径
String servletPath = request.getServletPath();
- String relativePath = servletPath.substring(5); // 去掉 "/raw/"
-
+ String relativePath = servletPath.substring(servletPath.indexOf('/', 1));
File file = new File(rootPath, relativePath);
if (!file.exists()) {
response.sendError(404);
return;
}
-
- // 设置内容类型(简单处理)
String contentType = Files.probeContentType(file.toPath());
response.setContentType(contentType != null ? contentType : "image/jpeg");
-
- // 输出文件流
Files.copy(file.toPath(), response.getOutputStream());
}
+ private static final Pattern IMAGE_GROUP_PATTERN = Pattern.compile("^(.*?)-(effect|template)\\.(jpg|JPG|png|PNG)$", Pattern.CASE_INSENSITIVE);
+
@GetMapping("/api/images/list")
@ResponseBody
public Map getLevelContent(
@@ -197,43 +151,69 @@ public class GalleryController {
@RequestParam(defaultValue = "1") int page,
@RequestParam(defaultValue = "20") int size) {
File currentFolder = new File(rootPath, path);
+ String rootAbsolutePath = new File(rootPath).getAbsolutePath();
List