Files
par/par-api/src/main/java/com/par/api/controller/AdminController.java

461 lines
18 KiB
Java

package com.par.api.controller;
import com.par.core.dto.AccountDTO;
import com.par.core.dto.ApiResponse;
import com.par.core.dto.SiteConfigDTO;
import com.par.core.entity.Account;
import com.par.core.entity.SiteConfig;
import com.par.core.enums.ConfigStatus;
import com.par.core.enums.TrustLevel;
import com.par.core.mapper.AccountMapper;
import com.par.core.mapper.ConfigReviewMapper;
import com.par.core.mapper.SiteConfigMapper;
import com.par.core.mapper.SiteMapper;
import com.par.core.entity.Psk;
import com.par.core.service.AccountService;
import com.par.core.service.ConfigService;
import com.par.core.service.PskService;
import com.par.core.storage.CachedStorageService;
import com.par.common.util.HmacUtil;
import jakarta.servlet.http.HttpServletRequest;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.*;
import java.nio.charset.StandardCharsets;
import java.time.LocalDateTime;
import java.util.List;
import java.util.Map;
/**
* 管理员控制器
* 所有接口需鉴权 + 管理员权限
*/
@Slf4j
@RestController
@RequestMapping("/api/v1/admin")
@RequiredArgsConstructor
public class AdminController {
private final SiteConfigMapper siteConfigMapper;
private final ConfigReviewMapper configReviewMapper;
private final AccountMapper accountMapper;
private final SiteMapper siteMapper;
private final AccountService accountService;
private final PskService pskService;
private final ConfigService configService;
private final CachedStorageService storageService;
private final com.par.core.service.SiteService siteService;
/**
* 获取所有用户列表
*/
@GetMapping("/accounts")
public ApiResponse<List<AccountDTO>> listAccounts(HttpServletRequest request) {
checkAdmin(request);
List<Account> accounts = accountMapper.selectList(null);
List<AccountDTO> dtos = accounts.stream()
.map(accountService::toDTO)
.toList();
return ApiResponse.success(dtos);
}
/**
* 审核配置
*/
@PostMapping("/reviews/{reviewId}/approve")
public ApiResponse<Void> approveConfig(
@PathVariable Long reviewId,
@RequestParam(value = "comment", required = false) String comment,
HttpServletRequest request) {
checkAdmin(request);
// Phase 1: 简化实现,直接更新配置状态
SiteConfig config = siteConfigMapper.selectById(reviewId);
if (config == null) {
return ApiResponse.error(404, "Config not found");
}
Long reviewerId = (Long) request.getAttribute("accountId");
// 自动分配版本号:取该站点已审批的最大版本号 + 1
var latestApproved = siteConfigMapper.selectList(
new com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper<SiteConfig>()
.eq(SiteConfig::getSiteId, config.getSiteId())
.eq(SiteConfig::getStatus, ConfigStatus.APPROVED)
.orderByDesc(SiteConfig::getVersion));
int nextVersion = 1;
if (!latestApproved.isEmpty()) {
try { nextVersion = Integer.parseInt(latestApproved.get(0).getVersion()) + 1; }
catch (NumberFormatException ignored) {}
}
config.setVersion(String.valueOf(nextVersion));
// 文件从 pending 移到 approved 路径
String approvedPath = config.getStoragePath().replace("/pending/", "/approved/");
String content = storageService.read(config.getStoragePath());
if (content != null) {
storageService.store(approvedPath, content);
config.setStoragePath(approvedPath);
}
// Groovy 脚本也移到 approved
if (config.getScriptStoragePath() != null && !config.getScriptStoragePath().isBlank()) {
String approvedScriptPath = config.getScriptStoragePath().replace("/pending/", "/approved/");
String script = storageService.read(config.getScriptStoragePath());
if (script != null) {
storageService.store(approvedScriptPath, script);
config.setScriptStoragePath(approvedScriptPath);
}
}
config.setStatus(ConfigStatus.APPROVED);
config.setReviewerId(reviewerId);
config.setReviewedAt(LocalDateTime.now());
config.setReviewComment(comment);
// 更新 is_latest 标记
siteConfigMapper.clearLatestFlag(config.getSiteId());
config.setIsLatest(true);
siteConfigMapper.updateById(config);
// 更新 sites 表的 current_config_id
var site = siteMapper.selectBySiteId(config.getSiteId());
if (site != null) {
site.setCurrentConfigId(config.getId());
site.setSchemaVersion(config.getSchemaVersion());
siteMapper.updateById(site);
}
log.info("Config approved: id={}, reviewer={}", reviewId, reviewerId);
return ApiResponse.success();
}
/**
* 拒绝配置
*/
@PostMapping("/reviews/{reviewId}/reject")
public ApiResponse<Void> rejectConfig(
@PathVariable Long reviewId,
@RequestParam(value = "comment", required = false, defaultValue = "") String comment,
HttpServletRequest request) {
checkAdmin(request);
SiteConfig config = siteConfigMapper.selectById(reviewId);
if (config == null) {
return ApiResponse.error(404, "Config not found");
}
Long reviewerId = (Long) request.getAttribute("accountId");
// 删除 pending 文件
storageService.delete(config.getStoragePath());
config.setStoragePath(null);
config.setStatus(ConfigStatus.REJECTED);
config.setReviewerId(reviewerId);
config.setReviewedAt(LocalDateTime.now());
config.setReviewComment(comment);
siteConfigMapper.updateById(config);
log.info("Config rejected: id={}, reviewer={}", reviewId, reviewerId);
return ApiResponse.success();
}
/**
* 获取指定站点的待审核配置列表
*/
@GetMapping("/configs/pending/{siteId}")
public ApiResponse<List<SiteConfigDTO>> listPendingConfigsBySite(
@PathVariable String siteId, HttpServletRequest request) {
checkAdmin(request);
List<SiteConfig> configs = siteConfigMapper.selectList(
new com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper<SiteConfig>()
.eq(SiteConfig::getSiteId, siteId)
.eq(SiteConfig::getStatus, ConfigStatus.PENDING)
.eq(SiteConfig::getDeleted, 0));
return ApiResponse.success(configs.stream().map(configService::toDTO).toList());
}
/**
* 查看指定版本配置内容
*/
@GetMapping("/configs/{configId}/content")
public ApiResponse<Map<String, Object>> getConfigContent(
@PathVariable Long configId, HttpServletRequest request) {
checkAdmin(request);
SiteConfig config = siteConfigMapper.selectById(configId);
if (config == null) return ApiResponse.error(404, "Config not found");
String content = null;
try { content = storageService.read(config.getStoragePath()); } catch (Exception e) {
return ApiResponse.error(500, "读取配置失败: " + e.getMessage());
}
return ApiResponse.success(Map.of("id", config.getId(), "content", content != null ? content : ""));
}
/**
* 编辑待审核配置内容
*/
@PutMapping("/configs/{configId}/content")
public ApiResponse<SiteConfigDTO> updateConfigContent(
@PathVariable Long configId,
@RequestBody Map<String, String> payload,
HttpServletRequest request) {
checkAdmin(request);
SiteConfig config = siteConfigMapper.selectById(configId);
if (config == null) return ApiResponse.error(404, "Config not found");
if (config.getStatus() != ConfigStatus.PENDING) {
return ApiResponse.error(400, "只能编辑待审核的配置");
}
String content = payload.get("content");
if (content == null || content.isBlank()) {
return ApiResponse.error(400, "content is required");
}
// 更新存储文件
storageService.store(config.getStoragePath(), content);
// 更新 hash 和文件大小
config.setConfigHash(HmacUtil.sha256(content));
config.setFileSize((long) content.getBytes(StandardCharsets.UTF_8).length);
siteConfigMapper.updateById(config);
log.info("Config content updated: id={}, siteId={}", configId, config.getSiteId());
return ApiResponse.success(configService.toDTO(config));
}
/**
* 查看指定配置的 Groovy 脚本
*/
@GetMapping("/configs/{configId}/script")
public ApiResponse<Map<String, Object>> getConfigScript(
@PathVariable Long configId, HttpServletRequest request) {
checkAdmin(request);
SiteConfig config = siteConfigMapper.selectById(configId);
if (config == null) return ApiResponse.error(404, "Config not found");
String script = null;
if (config.getScriptStoragePath() != null) {
try { script = storageService.read(config.getScriptStoragePath()); } catch (Exception ignored) {}
}
return ApiResponse.success(Map.of("id", config.getId(), "script", script != null ? script : ""));
}
/**
* 编辑待审核配置的 Groovy 脚本
*/
@PutMapping("/configs/{configId}/script")
public ApiResponse<SiteConfigDTO> updateConfigScript(
@PathVariable Long configId,
@RequestBody Map<String, String> payload,
HttpServletRequest request) {
checkAdmin(request);
SiteConfig config = siteConfigMapper.selectById(configId);
if (config == null) return ApiResponse.error(404, "Config not found");
if (config.getStatus() != ConfigStatus.PENDING) {
return ApiResponse.error(400, "只能编辑待审核的配置");
}
String script = payload.get("script");
if (script == null) return ApiResponse.error(400, "script is required");
String scriptPath = config.getScriptStoragePath();
if (scriptPath == null || scriptPath.isBlank()) {
scriptPath = String.format("configs/%s/pending/%s.groovy", config.getSiteId(), config.getConfigHash().substring(0, 8));
config.setScriptStoragePath(scriptPath);
}
storageService.store(scriptPath, script);
siteConfigMapper.updateById(config);
log.info("Config script updated: id={}, siteId={}", configId, config.getSiteId());
return ApiResponse.success(configService.toDTO(config));
}
/**
* 设置用户信任等级(禁止修改管理员、禁止提升为管理员)
*/
@PostMapping("/accounts/{accountId}/trust-level")
public ApiResponse<Void> setTrustLevel(
@PathVariable Long accountId,
@RequestParam("level") String level,
HttpServletRequest request) {
checkAdmin(request);
var target = accountMapper.selectById(accountId);
if (target == null) {
return ApiResponse.error(404, "Account not found");
}
// 禁止修改管理员
if (target.getTrustLevel() == TrustLevel.ADMIN) {
return ApiResponse.error(403, "不能修改管理员账户的信任等级");
}
// 禁止将用户设置为管理员
TrustLevel newLevel = TrustLevel.fromValue(level);
if (newLevel == TrustLevel.ADMIN) {
return ApiResponse.error(403, "不能通过此接口提升为管理员,请直接操作数据库");
}
target.setTrustLevel(newLevel);
accountMapper.updateById(target);
log.info("Trust level updated: accountId={}, level={}", accountId, level);
return ApiResponse.success();
}
/**
* 绑定额外邮箱
*/
@PostMapping("/accounts/{accountId}/emails")
public ApiResponse<Void> bindEmail(
@PathVariable Long accountId,
@RequestBody Map<String, String> payload,
HttpServletRequest request) {
checkAdmin(request);
String email = payload.get("email");
if (email == null || email.isBlank()) {
return ApiResponse.error(400, "email is required");
}
accountService.bindEmail(accountId, email);
log.info("Email bound to account: accountId={}, email={}", accountId, email);
return ApiResponse.success();
}
/**
* 解绑邮箱
*/
@DeleteMapping("/accounts/{accountId}/emails/{emailId}")
public ApiResponse<Void> unbindEmail(
@PathVariable Long accountId,
@PathVariable Long emailId,
HttpServletRequest request) {
checkAdmin(request);
accountService.unbindEmail(emailId);
log.info("Email unbound: accountId={}, emailId={}", accountId, emailId);
return ApiResponse.success();
}
/**
* 获取账户的绑定邮箱列表
*/
@GetMapping("/accounts/{accountId}/emails")
public ApiResponse<List<String>> listEmails(
@PathVariable Long accountId,
HttpServletRequest request) {
checkAdmin(request);
return ApiResponse.success(accountService.listEmails(accountId));
}
// ===================== PSK 管理 =====================
@GetMapping("/psks")
public ApiResponse<List<Psk>> listPsks(HttpServletRequest request) {
checkAdmin(request);
return ApiResponse.success(pskService.listAll());
}
@PostMapping("/psks")
public ApiResponse<Psk> addPsk(@RequestBody Map<String, String> body, HttpServletRequest request) {
checkAdmin(request);
String version = body.get("version");
String secret = body.get("secret");
String description = body.get("description");
if (version == null || secret == null) {
return ApiResponse.error(400, "version and secret are required");
}
return ApiResponse.success(pskService.add(version, secret, description));
}
@PostMapping("/psks/{id}/toggle")
public ApiResponse<Void> togglePsk(@PathVariable Long id, @RequestParam("active") boolean active,
HttpServletRequest request) {
checkAdmin(request);
pskService.toggle(id, active);
return ApiResponse.success();
}
// ===================== 站点审核 =====================
@GetMapping("/sites")
public ApiResponse<List<com.par.core.dto.SiteDTO>> listAllSites(HttpServletRequest request) {
checkAdmin(request);
return ApiResponse.success(siteService.listAllSites().stream()
.map(siteService::toDTO).toList());
}
@GetMapping("/sites/pending")
public ApiResponse<List<com.par.core.dto.SiteChangeDTO>> listPendingChanges(HttpServletRequest request) {
checkAdmin(request);
return ApiResponse.success(siteService.listPendingChanges());
}
@PostMapping("/changes/{id}/approve")
public ApiResponse<Void> approveChange(@PathVariable Long id,
@RequestBody(required = false) Map<String, Object> body,
HttpServletRequest request) {
checkAdmin(request);
Long reviewerId = (Long) request.getAttribute("accountId");
@SuppressWarnings("unchecked")
List<String> names = body != null ? (List<String>) body.get("names") : null;
@SuppressWarnings("unchecked")
List<String> urls = body != null ? (List<String>) body.get("urls") : null;
String siteType = body != null ? (String) body.get("siteType") : null;
String status = body != null ? (String) body.get("status") : null;
String notes = body != null ? (String) body.get("notes") : null;
siteService.approveChange(id, reviewerId, names, urls, siteType, status, notes);
log.info("Change approved: id={}, reviewer={}", id, reviewerId);
return ApiResponse.success();
}
@PostMapping("/changes/{id}/reject")
public ApiResponse<Void> rejectChange(@PathVariable Long id, HttpServletRequest request) {
checkAdmin(request);
Long reviewerId = (Long) request.getAttribute("accountId");
siteService.rejectChange(id, reviewerId);
log.info("Change rejected: id={}", id);
return ApiResponse.success();
}
@GetMapping("/sites/{siteId}/changes")
public ApiResponse<List<com.par.core.dto.SiteChangeDTO>> listSiteChanges(
@PathVariable("siteId") String siteId, HttpServletRequest request) {
checkAdmin(request);
return ApiResponse.success(siteService.listChangesBySite(siteId));
}
@GetMapping("/sites/search")
public ApiResponse<List<com.par.core.dto.SiteDTO>> searchSites(
@RequestParam("q") String q, HttpServletRequest request) {
checkAdmin(request);
return ApiResponse.success(siteService.searchSites(q));
}
@PostMapping("/changes/{id}/link")
public ApiResponse<Void> linkChangeToSite(
@PathVariable Long id,
@RequestBody Map<String, String> payload,
HttpServletRequest request) {
checkAdmin(request);
String targetSiteId = payload.get("siteId");
if (targetSiteId == null || targetSiteId.isBlank()) {
return ApiResponse.error(400, "siteId is required");
}
siteService.linkChangeToSite(id, targetSiteId);
log.info("Change linked: changeId={}, siteId={}", id, targetSiteId);
return ApiResponse.success();
}
private void checkAdmin(HttpServletRequest request) {
TrustLevel level = (TrustLevel) request.getAttribute("trustLevel");
if (level == null || level != TrustLevel.ADMIN) {
throw new IllegalArgumentException("Admin permission required");
}
}
}