diff --git a/services/delivery-platform-api/src/main/java/cn/craftlabs/platform/api/audit/AuditController.java b/services/delivery-platform-api/src/main/java/cn/craftlabs/platform/api/audit/AuditController.java index 0dcfdc9..34e93b6 100644 --- a/services/delivery-platform-api/src/main/java/cn/craftlabs/platform/api/audit/AuditController.java +++ b/services/delivery-platform-api/src/main/java/cn/craftlabs/platform/api/audit/AuditController.java @@ -33,9 +33,9 @@ public class AuditController { @GetMapping public PageResponse list( - @RequestParam(required = false) String entityType, - @RequestParam(required = false) Long entityId, - @RequestParam(required = false) String userId, + @RequestParam(value = "entityType", required = false) String entityType, + @RequestParam(value = "entityId", required = false) Long entityId, + @RequestParam(value = "userId", required = false) String userId, @RequestParam(value = "page", defaultValue = "0") @Min(0) int page, @RequestParam(value = "size", defaultValue = "20") @Min(1) @Max(200) int size) { return auditService.page(entityType, entityId, userId, page, size); @@ -43,11 +43,11 @@ public class AuditController { @GetMapping("/export") public ResponseEntity exportAuditEvents( - @RequestParam(required = false) String entityType, - @RequestParam(required = false) Long entityId, - @RequestParam(required = false) String userId, - @RequestParam(required = false) String from, - @RequestParam(required = false) String to) { + @RequestParam(value = "entityType", required = false) String entityType, + @RequestParam(value = "entityId", required = false) Long entityId, + @RequestParam(value = "userId", required = false) String userId, + @RequestParam(value = "from", required = false) String from, + @RequestParam(value = "to", required = false) String to) { List events = auditService.searchAuditEvents(entityType, entityId, from, to, userId); diff --git a/services/delivery-platform-api/src/main/java/cn/craftlabs/platform/api/auth/UserAdminController.java b/services/delivery-platform-api/src/main/java/cn/craftlabs/platform/api/auth/UserAdminController.java new file mode 100644 index 0000000..3707020 --- /dev/null +++ b/services/delivery-platform-api/src/main/java/cn/craftlabs/platform/api/auth/UserAdminController.java @@ -0,0 +1,108 @@ +package cn.craftlabs.platform.api.auth; + +import cn.craftlabs.platform.api.persistence.auth.PlatformUser; +import cn.craftlabs.platform.api.persistence.auth.PlatformUserMapper; +import com.baomidou.mybatisplus.core.toolkit.Wrappers; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.security.crypto.password.PasswordEncoder; +import org.springframework.web.bind.annotation.*; +import org.springframework.web.server.ResponseStatusException; + +import java.time.OffsetDateTime; +import java.time.ZoneOffset; +import java.util.List; +import java.util.Map; + +@RestController +@RequestMapping("/api/v1/admin/users") +public class UserAdminController { + + private final PlatformUserMapper userMapper; + private final PasswordEncoder passwordEncoder; + + public UserAdminController(PlatformUserMapper userMapper, PasswordEncoder passwordEncoder) { + this.userMapper = userMapper; + this.passwordEncoder = passwordEncoder; + } + + @GetMapping + public List list() { + return userMapper.selectList(Wrappers.lambdaQuery(PlatformUser.class) + .orderByAsc(PlatformUser::getId)); + } + + @PostMapping + public ResponseEntity create(@RequestBody Map body) { + String username = body.get("username"); + String password = body.get("password"); + String displayName = body.get("displayName"); + String role = body.get("role"); + + if (username == null || username.trim().isEmpty()) { + throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "用户名不能为空"); + } + if (password == null || password.length() < 6) { + throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "密码至少6位"); + } + + var existing = userMapper.selectOne(Wrappers.lambdaQuery(PlatformUser.class) + .eq(PlatformUser::getUsername, username.trim().toLowerCase())); + if (existing != null) { + throw new ResponseStatusException(HttpStatus.CONFLICT, "用户名已存在"); + } + + PlatformUser user = new PlatformUser(); + user.setUsername(username.trim().toLowerCase()); + user.setDisplayName(displayName != null ? displayName.trim() : username.trim()); + user.setPasswordHash(passwordEncoder.encode(password)); + user.setRole(role != null ? role.trim().toUpperCase() : "SALES"); + user.setStatus("ACTIVE"); + user.setCreatedAt(OffsetDateTime.now(ZoneOffset.UTC)); + user.setUpdatedAt(OffsetDateTime.now(ZoneOffset.UTC)); + userMapper.insert(user); + return ResponseEntity.status(HttpStatus.CREATED).body(user); + } + + @PutMapping("/{id}") + public ResponseEntity update(@PathVariable("id") Long id, @RequestBody Map body) { + PlatformUser user = userMapper.selectById(id); + if (user == null) { + throw new ResponseStatusException(HttpStatus.NOT_FOUND, "用户不存在"); + } + + String displayName = body.get("displayName"); + String role = body.get("role"); + String password = body.get("password"); + + if (displayName != null) user.setDisplayName(displayName.trim()); + if (role != null) user.setRole(role.trim().toUpperCase()); + if (password != null && !password.isEmpty()) { + if (password.length() < 6) { + throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "密码至少6位"); + } + user.setPasswordHash(passwordEncoder.encode(password)); + } + user.setUpdatedAt(OffsetDateTime.now(ZoneOffset.UTC)); + userMapper.updateById(user); + return ResponseEntity.ok(user); + } + + @PatchMapping("/{id}/status") + public ResponseEntity toggleStatus(@PathVariable("id") Long id, @RequestBody Map body) { + PlatformUser user = userMapper.selectById(id); + if (user == null) { + throw new ResponseStatusException(HttpStatus.NOT_FOUND, "用户不存在"); + } + + String newStatus = body.get("status"); + if (!List.of("ACTIVE", "DISABLED").contains(newStatus)) { + throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "状态值无效 (ACTIVE/DISABLED)"); + } + + user.setStatus(newStatus); + user.setUpdatedAt(OffsetDateTime.now(ZoneOffset.UTC)); + userMapper.updateById(user); + return ResponseEntity.ok().build(); + } +} diff --git a/services/delivery-platform-api/src/main/java/cn/craftlabs/platform/api/config/SecurityConfig.java b/services/delivery-platform-api/src/main/java/cn/craftlabs/platform/api/config/SecurityConfig.java index 05c12de..a36399c 100644 --- a/services/delivery-platform-api/src/main/java/cn/craftlabs/platform/api/config/SecurityConfig.java +++ b/services/delivery-platform-api/src/main/java/cn/craftlabs/platform/api/config/SecurityConfig.java @@ -17,6 +17,7 @@ import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; import org.springframework.security.web.header.writers.ReferrerPolicyHeaderWriter.ReferrerPolicy; +import com.fasterxml.jackson.databind.ObjectMapper; /** * I1:JWT(Bearer)保护业务 API;I5:{@code /internal/**} 使用内部共享 Token,与 JWT 分离;I6:统一安全响应头。 @@ -79,6 +80,11 @@ public class SecurityConfig { return new BCryptPasswordEncoder(); } + @Bean + public ObjectMapper objectMapper() { + return new ObjectMapper(); + } + /** I6:API 最小安全头;HSTS 由边缘 HTTPS 终止(Nginx/Caddy)配置。 */ private void apiHeaders(HeadersConfigurer headers) { headers diff --git a/services/delivery-platform-api/src/main/java/cn/craftlabs/platform/api/customer/CustomerController.java b/services/delivery-platform-api/src/main/java/cn/craftlabs/platform/api/customer/CustomerController.java index c781bd4..1b52acb 100644 --- a/services/delivery-platform-api/src/main/java/cn/craftlabs/platform/api/customer/CustomerController.java +++ b/services/delivery-platform-api/src/main/java/cn/craftlabs/platform/api/customer/CustomerController.java @@ -58,7 +58,7 @@ public class CustomerController { } @GetMapping("/{id}/summary") - public ResponseEntity> getSummary(@PathVariable Long id) { + public ResponseEntity> getSummary(@PathVariable("id") Long id) { return ResponseEntity.ok(customerService.getCustomerSummary(id)); } diff --git a/services/delivery-platform-api/src/main/java/cn/craftlabs/platform/api/integration/IntegrationCatalogController.java b/services/delivery-platform-api/src/main/java/cn/craftlabs/platform/api/integration/IntegrationCatalogController.java index 7a06a1c..3d45f09 100644 --- a/services/delivery-platform-api/src/main/java/cn/craftlabs/platform/api/integration/IntegrationCatalogController.java +++ b/services/delivery-platform-api/src/main/java/cn/craftlabs/platform/api/integration/IntegrationCatalogController.java @@ -98,8 +98,8 @@ public class IntegrationCatalogController { @GetMapping("/id-mappings") public ResponseEntity> listIdMappings( - @RequestParam(required = false) Long productLineId, - @RequestParam(required = false) Long environmentId) { + @RequestParam(value = "productLineId", required = false) Long productLineId, + @RequestParam(value = "environmentId", required = false) Long environmentId) { return ResponseEntity.ok(integrationCatalogService.listIdMappings(productLineId, environmentId)); } @@ -152,13 +152,13 @@ public class IntegrationCatalogController { @GetMapping("/feature-mappings") public ResponseEntity> listFeatureMappings( - @RequestParam(required = false) Long productLineId) { + @RequestParam(value = "productLineId", required = false) Long productLineId) { return ResponseEntity.ok(integrationCatalogService.listFeatureMappings(productLineId)); } @GetMapping("/sku-mappings") public ResponseEntity> listSkuMappings( - @RequestParam(required = false) Long contractLineId) { + @RequestParam(value = "contractLineId", required = false) Long contractLineId) { return ResponseEntity.ok(integrationCatalogService.listSkuMappings(contractLineId)); } diff --git a/services/delivery-platform-api/src/main/java/cn/craftlabs/platform/api/report/ReportController.java b/services/delivery-platform-api/src/main/java/cn/craftlabs/platform/api/report/ReportController.java index 357686c..a50801c 100644 --- a/services/delivery-platform-api/src/main/java/cn/craftlabs/platform/api/report/ReportController.java +++ b/services/delivery-platform-api/src/main/java/cn/craftlabs/platform/api/report/ReportController.java @@ -17,6 +17,7 @@ import org.springframework.web.bind.annotation.RestController; import java.nio.charset.StandardCharsets; import java.time.LocalDate; import java.util.List; +import java.util.Map; @RestController @RequestMapping("/api/v1/reports") @@ -29,40 +30,37 @@ public class ReportController { } @GetMapping("/contract-sn") - public List getContractSnReport( + public ResponseEntity> getContractSnReport( @RequestParam(value = "projectId", required = false) Long projectId, @RequestParam(value = "contractId", required = false) Long contractId) { - return reportService.getContractSnReport(projectId, contractId); + List rows = reportService.getContractSnReport(projectId, contractId); + return ResponseEntity.ok(rows); + } + + @GetMapping("/sn-stats") + public ResponseEntity> getSnStats() { + return ResponseEntity.ok(reportService.getSnStats()); } @GetMapping("/callback-stats") - public CallbackStatsResponse getCallbackStats( + public ResponseEntity getCallbackStats( @RequestParam(value = "from", required = false) String from, @RequestParam(value = "to", required = false) String to) { - return reportService.getCallbackStats(from, to); - } - - @GetMapping("/project-health") - public List getProjectHealth() { - return reportService.getProjectHealth(); + return ResponseEntity.ok(reportService.getCallbackStats(from, to)); } @GetMapping("/export") public ResponseEntity exportReport( - @RequestParam String type, - @RequestParam(required = false) Long projectId, - @RequestParam(required = false) Long contractId) { - + @RequestParam(value = "type", defaultValue = "contract-sn") String type, + @RequestParam(value = "projectId", required = false) Long projectId, + @RequestParam(value = "contractId", required = false) Long contractId) { String csv = reportService.exportReport(type, projectId, contractId); - byte[] bytes = csv.getBytes(StandardCharsets.UTF_8); byte[] bom = {(byte) 0xEF, (byte) 0xBB, (byte) 0xBF}; byte[] withBom = new byte[bom.length + bytes.length]; System.arraycopy(bom, 0, withBom, 0, bom.length); System.arraycopy(bytes, 0, withBom, bom.length, bytes.length); - ByteArrayResource resource = new ByteArrayResource(withBom); - return ResponseEntity.ok() .header(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=report-" + type + "-" + LocalDate.now() + ".csv")