2026-04-21 22:07:29 +08:00
|
|
|
package com.xuqm.tenant.controller;
|
|
|
|
|
|
|
|
|
|
import com.xuqm.common.model.ApiResponse;
|
2026-05-01 21:27:39 +08:00
|
|
|
import com.xuqm.tenant.entity.AppEntity;
|
|
|
|
|
import com.xuqm.tenant.entity.FeatureServiceEntity;
|
|
|
|
|
import com.xuqm.tenant.entity.OperationLogEntity;
|
2026-04-25 06:40:27 +08:00
|
|
|
import com.xuqm.tenant.entity.ServiceActivationRequestEntity;
|
2026-05-08 12:00:33 +08:00
|
|
|
import com.xuqm.tenant.dto.ServiceRequestView;
|
2026-04-21 22:07:29 +08:00
|
|
|
import com.xuqm.tenant.entity.TenantEntity;
|
2026-04-25 06:40:27 +08:00
|
|
|
import com.xuqm.tenant.service.FeatureServiceManager;
|
2026-05-02 22:57:55 +08:00
|
|
|
import com.xuqm.tenant.entity.RiskConfigEntity;
|
|
|
|
|
import com.xuqm.tenant.entity.SensitiveWordEntity;
|
2026-04-21 22:07:29 +08:00
|
|
|
import com.xuqm.tenant.service.OpsService;
|
2026-05-05 17:54:59 +08:00
|
|
|
import com.xuqm.tenant.service.OpsPushDiagnosticsClient;
|
2026-05-15 22:38:46 +08:00
|
|
|
import com.xuqm.tenant.service.LicenseServiceClient;
|
2026-05-02 22:57:55 +08:00
|
|
|
import com.xuqm.tenant.service.RiskControlService;
|
2026-04-21 22:07:29 +08:00
|
|
|
import org.springframework.data.domain.Page;
|
|
|
|
|
import org.springframework.http.ResponseEntity;
|
|
|
|
|
import org.springframework.security.access.prepost.PreAuthorize;
|
2026-05-02 22:57:55 +08:00
|
|
|
import org.springframework.web.bind.annotation.DeleteMapping;
|
2026-04-21 22:07:29 +08:00
|
|
|
import org.springframework.web.bind.annotation.GetMapping;
|
2026-05-02 22:57:55 +08:00
|
|
|
import org.springframework.web.bind.annotation.PatchMapping;
|
2026-04-21 22:07:29 +08:00
|
|
|
import org.springframework.web.bind.annotation.PathVariable;
|
|
|
|
|
import org.springframework.web.bind.annotation.PostMapping;
|
2026-05-02 22:57:55 +08:00
|
|
|
import org.springframework.web.bind.annotation.PutMapping;
|
2026-04-21 22:07:29 +08:00
|
|
|
import org.springframework.web.bind.annotation.RequestBody;
|
|
|
|
|
import org.springframework.web.bind.annotation.RequestMapping;
|
|
|
|
|
import org.springframework.web.bind.annotation.RequestParam;
|
|
|
|
|
import org.springframework.web.bind.annotation.RestController;
|
|
|
|
|
|
|
|
|
|
import java.util.Map;
|
|
|
|
|
|
|
|
|
|
@RestController
|
|
|
|
|
@RequestMapping
|
|
|
|
|
public class OpsController {
|
|
|
|
|
|
|
|
|
|
private final OpsService opsService;
|
2026-04-25 06:40:27 +08:00
|
|
|
private final FeatureServiceManager featureServiceManager;
|
2026-05-02 22:57:55 +08:00
|
|
|
private final RiskControlService riskControlService;
|
2026-05-05 17:54:59 +08:00
|
|
|
private final OpsPushDiagnosticsClient pushDiagnosticsClient;
|
2026-05-15 22:38:46 +08:00
|
|
|
private final LicenseServiceClient licenseServiceClient;
|
2026-04-21 22:07:29 +08:00
|
|
|
|
2026-05-02 22:57:55 +08:00
|
|
|
public OpsController(OpsService opsService, FeatureServiceManager featureServiceManager,
|
2026-05-05 17:54:59 +08:00
|
|
|
RiskControlService riskControlService,
|
2026-05-15 22:38:46 +08:00
|
|
|
OpsPushDiagnosticsClient pushDiagnosticsClient,
|
|
|
|
|
LicenseServiceClient licenseServiceClient) {
|
2026-04-21 22:07:29 +08:00
|
|
|
this.opsService = opsService;
|
2026-04-25 06:40:27 +08:00
|
|
|
this.featureServiceManager = featureServiceManager;
|
2026-05-02 22:57:55 +08:00
|
|
|
this.riskControlService = riskControlService;
|
2026-05-05 17:54:59 +08:00
|
|
|
this.pushDiagnosticsClient = pushDiagnosticsClient;
|
2026-05-15 22:38:46 +08:00
|
|
|
this.licenseServiceClient = licenseServiceClient;
|
2026-04-21 22:07:29 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@PostMapping("/api/auth/ops/login")
|
|
|
|
|
public ResponseEntity<ApiResponse<Map<String, String>>> opsLogin(@RequestBody Map<String, String> body) {
|
|
|
|
|
String token = opsService.login(body.get("username"), body.get("password"));
|
|
|
|
|
return ResponseEntity.ok(ApiResponse.success(Map.of("token", token)));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@GetMapping("/api/ops/tenants")
|
|
|
|
|
@PreAuthorize("hasAuthority('ROLE_OPS')")
|
|
|
|
|
public ResponseEntity<ApiResponse<Map<String, Object>>> listTenants(
|
|
|
|
|
@RequestParam(defaultValue = "") String keyword,
|
|
|
|
|
@RequestParam(defaultValue = "0") int page,
|
|
|
|
|
@RequestParam(defaultValue = "20") int size) {
|
|
|
|
|
Page<TenantEntity> result = opsService.listTenants(keyword, page, size);
|
|
|
|
|
return ResponseEntity.ok(ApiResponse.success(Map.of(
|
|
|
|
|
"content", result.getContent(),
|
|
|
|
|
"total", result.getTotalElements(),
|
|
|
|
|
"totalPages", result.getTotalPages()
|
|
|
|
|
)));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@PostMapping("/api/ops/tenants/{id}/toggle-status")
|
|
|
|
|
@PreAuthorize("hasAuthority('ROLE_OPS')")
|
|
|
|
|
public ResponseEntity<ApiResponse<Void>> toggleStatus(@PathVariable String id) {
|
|
|
|
|
opsService.toggleStatus(id);
|
|
|
|
|
return ResponseEntity.ok(ApiResponse.ok());
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-01 21:27:39 +08:00
|
|
|
@GetMapping("/api/ops/tenants/{id}")
|
|
|
|
|
@PreAuthorize("hasAuthority('ROLE_OPS')")
|
|
|
|
|
public ResponseEntity<ApiResponse<Map<String, Object>>> getTenantDetail(@PathVariable String id) {
|
|
|
|
|
return ResponseEntity.ok(ApiResponse.success(opsService.getTenantDetail(id)));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@GetMapping("/api/ops/tenants/{id}/apps")
|
|
|
|
|
@PreAuthorize("hasAuthority('ROLE_OPS')")
|
|
|
|
|
public ResponseEntity<ApiResponse<java.util.List<AppEntity>>> listTenantApps(@PathVariable String id) {
|
|
|
|
|
return ResponseEntity.ok(ApiResponse.success(opsService.listTenantApps(id)));
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-21 22:07:29 +08:00
|
|
|
@GetMapping("/api/ops/statistics")
|
|
|
|
|
@PreAuthorize("hasAuthority('ROLE_OPS')")
|
|
|
|
|
public ResponseEntity<ApiResponse<Map<String, Object>>> statistics() {
|
|
|
|
|
return ResponseEntity.ok(ApiResponse.success(opsService.statistics()));
|
|
|
|
|
}
|
2026-04-25 06:40:27 +08:00
|
|
|
|
|
|
|
|
@GetMapping("/api/ops/service-requests")
|
|
|
|
|
@PreAuthorize("hasAuthority('ROLE_OPS')")
|
|
|
|
|
public ResponseEntity<ApiResponse<Map<String, Object>>> listServiceRequests(
|
|
|
|
|
@RequestParam(required = false) String status,
|
|
|
|
|
@RequestParam(defaultValue = "0") int page,
|
|
|
|
|
@RequestParam(defaultValue = "20") int size) {
|
2026-05-08 12:00:33 +08:00
|
|
|
Page<ServiceRequestView> result = opsService.listServiceRequests(status, page, size);
|
2026-04-25 06:40:27 +08:00
|
|
|
return ResponseEntity.ok(ApiResponse.success(Map.of(
|
|
|
|
|
"content", result.getContent(),
|
|
|
|
|
"total", result.getTotalElements(),
|
|
|
|
|
"totalPages", result.getTotalPages()
|
|
|
|
|
)));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@PostMapping("/api/ops/service-requests/{requestId}/approve")
|
|
|
|
|
@PreAuthorize("hasAuthority('ROLE_OPS')")
|
|
|
|
|
public ResponseEntity<ApiResponse<ServiceActivationRequestEntity>> approveRequest(
|
|
|
|
|
@PathVariable String requestId,
|
|
|
|
|
@RequestBody(required = false) Map<String, String> body) {
|
|
|
|
|
String reviewNote = body != null ? body.getOrDefault("reviewNote", "") : "";
|
2026-05-15 22:38:46 +08:00
|
|
|
String expiresAt = body != null ? body.get("expiresAt") : null;
|
2026-04-25 06:40:27 +08:00
|
|
|
return ResponseEntity.ok(ApiResponse.success(
|
2026-05-15 22:38:46 +08:00
|
|
|
featureServiceManager.approveRequest(requestId, reviewNote, expiresAt)));
|
2026-04-25 06:40:27 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@PostMapping("/api/ops/service-requests/{requestId}/reject")
|
|
|
|
|
@PreAuthorize("hasAuthority('ROLE_OPS')")
|
|
|
|
|
public ResponseEntity<ApiResponse<ServiceActivationRequestEntity>> rejectRequest(
|
|
|
|
|
@PathVariable String requestId,
|
|
|
|
|
@RequestBody(required = false) Map<String, String> body) {
|
|
|
|
|
String reviewNote = body != null ? body.getOrDefault("reviewNote", "") : "";
|
|
|
|
|
return ResponseEntity.ok(ApiResponse.success(
|
|
|
|
|
featureServiceManager.rejectRequest(requestId, reviewNote)));
|
|
|
|
|
}
|
2026-05-01 21:27:39 +08:00
|
|
|
|
|
|
|
|
@GetMapping("/api/ops/apps")
|
|
|
|
|
@PreAuthorize("hasAuthority('ROLE_OPS')")
|
|
|
|
|
public ResponseEntity<ApiResponse<Map<String, Object>>> listApps(
|
|
|
|
|
@RequestParam(defaultValue = "") String keyword,
|
2026-05-16 00:24:10 +08:00
|
|
|
@RequestParam(required = false) String tenantId,
|
2026-05-01 21:27:39 +08:00
|
|
|
@RequestParam(defaultValue = "0") int page,
|
|
|
|
|
@RequestParam(defaultValue = "20") int size) {
|
2026-05-16 00:24:10 +08:00
|
|
|
Page<Map<String, Object>> result = opsService.listApps(keyword, tenantId, page, size);
|
2026-05-01 21:27:39 +08:00
|
|
|
return ResponseEntity.ok(ApiResponse.success(Map.of(
|
|
|
|
|
"content", result.getContent(),
|
|
|
|
|
"total", result.getTotalElements(),
|
|
|
|
|
"totalPages", result.getTotalPages()
|
|
|
|
|
)));
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-08 10:09:22 +08:00
|
|
|
@GetMapping("/api/ops/apps/{appKey}")
|
2026-05-01 21:27:39 +08:00
|
|
|
@PreAuthorize("hasAuthority('ROLE_OPS')")
|
2026-05-08 10:09:22 +08:00
|
|
|
public ResponseEntity<ApiResponse<Map<String, Object>>> getAppDetail(@PathVariable String appKey) {
|
|
|
|
|
return ResponseEntity.ok(ApiResponse.success(opsService.getAppDetail(appKey)));
|
2026-05-01 21:27:39 +08:00
|
|
|
}
|
|
|
|
|
|
2026-05-08 10:09:22 +08:00
|
|
|
@GetMapping("/api/ops/apps/{appKey}/services")
|
2026-05-01 21:27:39 +08:00
|
|
|
@PreAuthorize("hasAuthority('ROLE_OPS')")
|
2026-05-08 10:09:22 +08:00
|
|
|
public ResponseEntity<ApiResponse<java.util.List<FeatureServiceEntity>>> listAppServices(@PathVariable String appKey) {
|
|
|
|
|
return ResponseEntity.ok(ApiResponse.success(opsService.listAppServices(appKey)));
|
2026-05-01 21:27:39 +08:00
|
|
|
}
|
|
|
|
|
|
2026-05-16 15:34:59 +08:00
|
|
|
@PostMapping("/api/ops/apps/{appKey}/transfer")
|
|
|
|
|
@PreAuthorize("hasAuthority('ROLE_OPS')")
|
|
|
|
|
public ResponseEntity<ApiResponse<Void>> transferApp(
|
|
|
|
|
@PathVariable String appKey,
|
|
|
|
|
@RequestBody Map<String, String> body) {
|
|
|
|
|
String targetTenantId = body.get("targetTenantId");
|
|
|
|
|
if (targetTenantId == null || targetTenantId.isBlank()) {
|
|
|
|
|
return ResponseEntity.badRequest().body(ApiResponse.badRequest("targetTenantId is required"));
|
|
|
|
|
}
|
|
|
|
|
opsService.transferApp(appKey, targetTenantId);
|
|
|
|
|
return ResponseEntity.ok(ApiResponse.ok());
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-01 21:27:39 +08:00
|
|
|
@GetMapping("/api/ops/operation-logs")
|
|
|
|
|
@PreAuthorize("hasAuthority('ROLE_OPS')")
|
|
|
|
|
public ResponseEntity<ApiResponse<Map<String, Object>>> listOperationLogs(
|
|
|
|
|
@RequestParam(defaultValue = "0") int page,
|
|
|
|
|
@RequestParam(defaultValue = "20") int size) {
|
|
|
|
|
Page<OperationLogEntity> result = opsService.listOperationLogs(page, size);
|
|
|
|
|
return ResponseEntity.ok(ApiResponse.success(Map.of(
|
|
|
|
|
"content", result.getContent(),
|
|
|
|
|
"total", result.getTotalElements(),
|
|
|
|
|
"totalPages", result.getTotalPages()
|
|
|
|
|
)));
|
|
|
|
|
}
|
2026-05-02 22:57:55 +08:00
|
|
|
|
2026-05-05 17:54:59 +08:00
|
|
|
@GetMapping("/api/ops/push/search")
|
|
|
|
|
@PreAuthorize("hasAuthority('ROLE_OPS')")
|
|
|
|
|
public ResponseEntity<ApiResponse<Map<String, Object>>> searchPushByToken(
|
|
|
|
|
@RequestParam String token,
|
2026-05-07 19:39:42 +08:00
|
|
|
@RequestParam(required = false) String appKey) {
|
|
|
|
|
return ResponseEntity.ok(ApiResponse.success(pushDiagnosticsClient.searchByToken(token, appKey)));
|
2026-05-05 17:54:59 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@GetMapping("/api/ops/push/device-logs")
|
|
|
|
|
@PreAuthorize("hasAuthority('ROLE_OPS')")
|
|
|
|
|
public ResponseEntity<ApiResponse<Map<String, Object>>> pushDeviceLogs(
|
2026-05-07 19:39:42 +08:00
|
|
|
@RequestParam String appKey,
|
2026-05-05 17:54:59 +08:00
|
|
|
@RequestParam String userId,
|
|
|
|
|
@RequestParam(defaultValue = "0") int page,
|
|
|
|
|
@RequestParam(defaultValue = "20") int size) {
|
2026-05-07 19:39:42 +08:00
|
|
|
return ResponseEntity.ok(ApiResponse.success(pushDiagnosticsClient.deviceLogs(appKey, userId, page, size)));
|
2026-05-05 17:54:59 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@PostMapping("/api/ops/push/test-offline")
|
|
|
|
|
@PreAuthorize("hasAuthority('ROLE_OPS')")
|
|
|
|
|
public ResponseEntity<ApiResponse<Map<String, Object>>> sendPushTestOffline(@RequestBody Map<String, String> body) {
|
|
|
|
|
return ResponseEntity.ok(ApiResponse.success(pushDiagnosticsClient.sendTestOffline(
|
2026-05-07 19:39:42 +08:00
|
|
|
body.get("appKey"),
|
2026-05-05 17:54:59 +08:00
|
|
|
body.get("userId"),
|
|
|
|
|
body.getOrDefault("title", "XuqmGroup Push 测试"),
|
|
|
|
|
body.getOrDefault("body", "这是一条离线推送测试消息"),
|
|
|
|
|
body.getOrDefault("payload", "{}"))));
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-15 22:38:46 +08:00
|
|
|
/* ---------- License 管理 ---------- */
|
|
|
|
|
@GetMapping("/api/ops/apps/{appKey}/license")
|
|
|
|
|
@PreAuthorize("hasAuthority('ROLE_OPS')")
|
|
|
|
|
public ResponseEntity<ApiResponse<Map<String, Object>>> getAppLicense(@PathVariable String appKey) {
|
|
|
|
|
return ResponseEntity.ok(ApiResponse.success(licenseServiceClient.getAppLicenseStatus(appKey)));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@PutMapping("/api/ops/apps/{appKey}/license/max-devices")
|
|
|
|
|
@PreAuthorize("hasAuthority('ROLE_OPS')")
|
|
|
|
|
public ResponseEntity<ApiResponse<Void>> updateMaxDevices(
|
|
|
|
|
@PathVariable String appKey,
|
|
|
|
|
@RequestBody Map<String, Object> body) {
|
|
|
|
|
Object maxDevicesObj = body.get("maxDevices");
|
|
|
|
|
if (maxDevicesObj == null) {
|
|
|
|
|
throw new com.xuqm.common.exception.BusinessException(400, "最大设备数不能为空");
|
|
|
|
|
}
|
|
|
|
|
int maxDevices;
|
|
|
|
|
if (maxDevicesObj instanceof Number n) {
|
|
|
|
|
maxDevices = n.intValue();
|
|
|
|
|
} else {
|
|
|
|
|
maxDevices = Integer.parseInt(maxDevicesObj.toString());
|
|
|
|
|
}
|
|
|
|
|
if (maxDevices < 1) {
|
|
|
|
|
throw new com.xuqm.common.exception.BusinessException(400, "最大设备数必须大于0");
|
|
|
|
|
}
|
|
|
|
|
licenseServiceClient.updateMaxDevices(appKey, maxDevices);
|
|
|
|
|
return ResponseEntity.ok(ApiResponse.ok());
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-02 22:57:55 +08:00
|
|
|
/* ---------- 风控配置 ---------- */
|
|
|
|
|
@GetMapping("/api/ops/risk/rules")
|
|
|
|
|
@PreAuthorize("hasAuthority('ROLE_OPS')")
|
|
|
|
|
public ResponseEntity<ApiResponse<RiskConfigEntity>> getRiskConfig() {
|
|
|
|
|
return ResponseEntity.ok(ApiResponse.success(riskControlService.getConfig()));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@PostMapping("/api/ops/risk/rules")
|
|
|
|
|
@PreAuthorize("hasAuthority('ROLE_OPS')")
|
|
|
|
|
public ResponseEntity<ApiResponse<RiskConfigEntity>> saveRiskConfig(@RequestBody RiskConfigEntity req) {
|
|
|
|
|
return ResponseEntity.ok(ApiResponse.success(riskControlService.saveConfig(req)));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/* ---------- 敏感词 ---------- */
|
|
|
|
|
@GetMapping("/api/ops/risk/sensitive-words")
|
|
|
|
|
@PreAuthorize("hasAuthority('ROLE_OPS')")
|
|
|
|
|
public ResponseEntity<ApiResponse<Map<String, Object>>> listSensitiveWords(
|
|
|
|
|
@RequestParam(defaultValue = "0") int page,
|
|
|
|
|
@RequestParam(defaultValue = "20") int size) {
|
|
|
|
|
Page<SensitiveWordEntity> result = riskControlService.listWords(page, size);
|
|
|
|
|
return ResponseEntity.ok(ApiResponse.success(Map.of(
|
|
|
|
|
"content", result.getContent(),
|
|
|
|
|
"total", result.getTotalElements(),
|
|
|
|
|
"totalPages", result.getTotalPages()
|
|
|
|
|
)));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@PostMapping("/api/ops/risk/sensitive-words")
|
|
|
|
|
@PreAuthorize("hasAuthority('ROLE_OPS')")
|
|
|
|
|
public ResponseEntity<ApiResponse<SensitiveWordEntity>> createSensitiveWord(@RequestBody SensitiveWordEntity req) {
|
|
|
|
|
return ResponseEntity.ok(ApiResponse.success(riskControlService.createWord(req)));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@PutMapping("/api/ops/risk/sensitive-words/{id}")
|
|
|
|
|
@PreAuthorize("hasAuthority('ROLE_OPS')")
|
|
|
|
|
public ResponseEntity<ApiResponse<SensitiveWordEntity>> updateSensitiveWord(
|
|
|
|
|
@PathVariable String id, @RequestBody SensitiveWordEntity req) {
|
|
|
|
|
return ResponseEntity.ok(ApiResponse.success(riskControlService.updateWord(id, req)));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@PatchMapping("/api/ops/risk/sensitive-words/{id}/toggle")
|
|
|
|
|
@PreAuthorize("hasAuthority('ROLE_OPS')")
|
|
|
|
|
public ResponseEntity<ApiResponse<Void>> toggleSensitiveWord(
|
|
|
|
|
@PathVariable String id, @RequestParam boolean enabled) {
|
|
|
|
|
riskControlService.toggleWord(id, enabled);
|
|
|
|
|
return ResponseEntity.ok(ApiResponse.ok());
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@DeleteMapping("/api/ops/risk/sensitive-words/{id}")
|
|
|
|
|
@PreAuthorize("hasAuthority('ROLE_OPS')")
|
|
|
|
|
public ResponseEntity<ApiResponse<Void>> deleteSensitiveWord(@PathVariable String id) {
|
|
|
|
|
riskControlService.deleteWord(id);
|
|
|
|
|
return ResponseEntity.ok(ApiResponse.ok());
|
|
|
|
|
}
|
2026-04-21 22:07:29 +08:00
|
|
|
}
|