This commit is contained in:
@@ -0,0 +1,30 @@
|
||||
package com.kaidi.finance.dashboard.api;
|
||||
|
||||
import com.kaidi.finance.dashboard.application.DashboardApplicationService;
|
||||
import com.kaidi.finance.shared.api.ApiResponse;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import org.springframework.security.access.prepost.PreAuthorize;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RequestParam;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/api/v1/dashboard")
|
||||
public class DashboardController {
|
||||
|
||||
private final DashboardApplicationService dashboardService;
|
||||
|
||||
public DashboardController(DashboardApplicationService dashboardService) {
|
||||
this.dashboardService = dashboardService;
|
||||
}
|
||||
|
||||
@GetMapping("/overview")
|
||||
@Operation(operationId = "getDashboardOverview", summary = "查询当前身份可见的系统仪表盘")
|
||||
@PreAuthorize("@authorizationService.hasPermission('dashboard:overview:view')")
|
||||
public ApiResponse<DashboardOverviewView> overview(
|
||||
@RequestParam(defaultValue = "CNY") String currency
|
||||
) {
|
||||
return ApiResponse.ok(dashboardService.overview(currency));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
package com.kaidi.finance.dashboard.api;
|
||||
|
||||
public record DashboardDistributionView(
|
||||
String code,
|
||||
String label,
|
||||
long value,
|
||||
boolean available,
|
||||
String targetRoute
|
||||
) {
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
package com.kaidi.finance.dashboard.api;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public record DashboardGeographyView(
|
||||
boolean available,
|
||||
String message,
|
||||
List<DashboardRegionView> regions
|
||||
) {
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
package com.kaidi.finance.dashboard.api;
|
||||
|
||||
public record DashboardMetricView(
|
||||
String code,
|
||||
String label,
|
||||
String kind,
|
||||
String value,
|
||||
String unit,
|
||||
boolean available,
|
||||
String targetRoute
|
||||
) {
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
package com.kaidi.finance.dashboard.api;
|
||||
|
||||
import com.kaidi.finance.workbench.api.WorkbenchActivityView;
|
||||
import java.time.Instant;
|
||||
import java.util.List;
|
||||
|
||||
public record DashboardOverviewView(
|
||||
String title,
|
||||
Instant refreshedAt,
|
||||
String currency,
|
||||
DashboardSystemView system,
|
||||
List<DashboardMetricView> metrics,
|
||||
DashboardTrendView trend,
|
||||
List<DashboardDistributionView> lifecycle,
|
||||
List<DashboardRiskView> risks,
|
||||
DashboardGeographyView geography,
|
||||
List<WorkbenchActivityView> activities
|
||||
) {
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
package com.kaidi.finance.dashboard.api;
|
||||
|
||||
public record DashboardRegionView(
|
||||
String regionCode,
|
||||
String regionName,
|
||||
long projectCount,
|
||||
String amount,
|
||||
String currency
|
||||
) {
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
package com.kaidi.finance.dashboard.api;
|
||||
|
||||
public record DashboardRiskView(
|
||||
String code,
|
||||
String label,
|
||||
String severity,
|
||||
long count,
|
||||
String detail,
|
||||
boolean available,
|
||||
String targetRoute
|
||||
) {
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
package com.kaidi.finance.dashboard.api;
|
||||
|
||||
public record DashboardServiceStatusView(
|
||||
String code,
|
||||
String label,
|
||||
String status,
|
||||
String detail
|
||||
) {
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
package com.kaidi.finance.dashboard.api;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public record DashboardSystemView(
|
||||
String overallStatus,
|
||||
String currentVersion,
|
||||
long uptimeSeconds,
|
||||
List<DashboardServiceStatusView> services
|
||||
) {
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
package com.kaidi.finance.dashboard.api;
|
||||
|
||||
public record DashboardTrendPointView(
|
||||
String period,
|
||||
String receivedAmount,
|
||||
String paidAmount,
|
||||
String invoicedAmount
|
||||
) {
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
package com.kaidi.finance.dashboard.api;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public record DashboardTrendView(
|
||||
String currency,
|
||||
boolean receiptsAvailable,
|
||||
boolean paymentsAvailable,
|
||||
boolean invoicesAvailable,
|
||||
List<DashboardTrendPointView> points
|
||||
) {
|
||||
}
|
||||
+278
@@ -0,0 +1,278 @@
|
||||
package com.kaidi.finance.dashboard.application;
|
||||
|
||||
import com.kaidi.finance.dashboard.api.DashboardDistributionView;
|
||||
import com.kaidi.finance.dashboard.api.DashboardGeographyView;
|
||||
import com.kaidi.finance.dashboard.api.DashboardMetricView;
|
||||
import com.kaidi.finance.dashboard.api.DashboardOverviewView;
|
||||
import com.kaidi.finance.dashboard.api.DashboardRiskView;
|
||||
import com.kaidi.finance.dashboard.api.DashboardServiceStatusView;
|
||||
import com.kaidi.finance.dashboard.api.DashboardSystemView;
|
||||
import com.kaidi.finance.dashboard.api.DashboardTrendPointView;
|
||||
import com.kaidi.finance.dashboard.api.DashboardTrendView;
|
||||
import com.kaidi.finance.dashboard.infrastructure.DashboardMapper;
|
||||
import com.kaidi.finance.iam.domain.FinancePrincipal;
|
||||
import com.kaidi.finance.shared.api.BusinessException;
|
||||
import com.kaidi.finance.shared.api.ErrorCode;
|
||||
import com.kaidi.finance.shared.file.FileStorageProperties;
|
||||
import com.kaidi.finance.shared.security.AuthorizationService;
|
||||
import com.kaidi.finance.shared.security.IdentityContext;
|
||||
import com.kaidi.finance.update.application.SystemUpdateProperties;
|
||||
import com.kaidi.finance.workbench.api.WorkbenchActivityView;
|
||||
import com.kaidi.finance.workbench.infrastructure.WorkbenchMapper;
|
||||
import java.lang.management.ManagementFactory;
|
||||
import java.math.BigDecimal;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.InvalidPathException;
|
||||
import java.nio.file.Path;
|
||||
import java.time.Instant;
|
||||
import java.time.LocalDate;
|
||||
import java.time.YearMonth;
|
||||
import java.time.ZoneOffset;
|
||||
import java.util.ArrayList;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.function.Supplier;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
@Service
|
||||
public class DashboardApplicationService {
|
||||
|
||||
private static final Set<String> SUPPORTED_CURRENCIES = Set.of("CNY", "USD", "EUR", "HKD", "JPY", "GBP");
|
||||
private static final List<String> STAGES = List.of(
|
||||
"INITIATION", "CONTRACTING", "EXECUTION", "ACCEPTANCE", "SETTLEMENT", "CLOSED"
|
||||
);
|
||||
|
||||
private final DashboardMapper mapper;
|
||||
private final WorkbenchMapper workbenchMapper;
|
||||
private final IdentityContext identityContext;
|
||||
private final AuthorizationService authorizationService;
|
||||
private final SystemUpdateProperties updateProperties;
|
||||
private final FileStorageProperties storageProperties;
|
||||
|
||||
public DashboardApplicationService(
|
||||
DashboardMapper mapper,
|
||||
WorkbenchMapper workbenchMapper,
|
||||
IdentityContext identityContext,
|
||||
AuthorizationService authorizationService,
|
||||
SystemUpdateProperties updateProperties,
|
||||
FileStorageProperties storageProperties
|
||||
) {
|
||||
this.mapper = mapper;
|
||||
this.workbenchMapper = workbenchMapper;
|
||||
this.identityContext = identityContext;
|
||||
this.authorizationService = authorizationService;
|
||||
this.updateProperties = updateProperties;
|
||||
this.storageProperties = storageProperties;
|
||||
}
|
||||
|
||||
@Transactional(readOnly = true)
|
||||
public DashboardOverviewView overview(String requestedCurrency) {
|
||||
authorizationService.requirePermission("dashboard:overview:view");
|
||||
FinancePrincipal actor = identityContext.requirePrincipal();
|
||||
String role = identityContext.requireActiveRole();
|
||||
String currency = normalizeCurrency(requestedCurrency);
|
||||
|
||||
boolean canProjects = authorizationService.hasPermission("project:project:view");
|
||||
boolean canContracts = authorizationService.hasPermission("masterdata:contract:view");
|
||||
boolean canReceipts = authorizationService.hasPermission("receivable:receipt:view");
|
||||
boolean canPayments = authorizationService.hasPermission("payment:request:view");
|
||||
boolean canInvoices = authorizationService.hasPermission("receivable:invoice:view");
|
||||
boolean canWorkflow = authorizationService.hasPermission("workflow:task:view");
|
||||
boolean canRisks = authorizationService.hasPermission("project:risk-flag:view");
|
||||
boolean canArchives = authorizationService.hasPermission("archive:package:view");
|
||||
boolean canFiles = authorizationService.hasPermission("archive:file:view");
|
||||
|
||||
long projectCount = count(canProjects, () -> mapper.countProjects(actor.userId(), role));
|
||||
BigDecimal contractAmount = amount(canContracts,
|
||||
() -> mapper.sumContracts(actor.userId(), role, currency));
|
||||
BigDecimal receiptAmount = amount(canReceipts,
|
||||
() -> mapper.sumReceipts(actor.userId(), role, currency));
|
||||
BigDecimal paymentAmount = amount(canPayments,
|
||||
() -> mapper.sumPayments(actor.userId(), role, currency));
|
||||
long pendingTasks = count(canWorkflow, () -> workbenchMapper.countPendingTasks(actor.userId(), role));
|
||||
long activeRisks = count(canRisks, () -> mapper.countActiveRisks(actor.userId(), role));
|
||||
|
||||
List<DashboardMetricView> metrics = List.of(
|
||||
countMetric("ACTIVE_PROJECTS", "进行中项目", projectCount, canProjects, "/projects"),
|
||||
moneyMetric("CONTRACT_AMOUNT", "合同总额", contractAmount, currency, canContracts,
|
||||
"/finance/contracts-costs"),
|
||||
moneyMetric("RECEIPT_AMOUNT", "累计收款", receiptAmount, currency, canReceipts,
|
||||
"/finance/receipts-invoices?resource=receipts"),
|
||||
moneyMetric("PAYMENT_AMOUNT", "累计付款", paymentAmount, currency, canPayments,
|
||||
"/finance/payments"),
|
||||
countMetric("PENDING_TASKS", "待办审批", pendingTasks, canWorkflow, "/tasks?view=TODO"),
|
||||
countMetric("ACTIVE_RISKS", "风险事项", activeRisks, canRisks, "/projects?riskStatus=ACTIVE")
|
||||
);
|
||||
|
||||
List<DashboardRiskView> risks = List.of(
|
||||
risk("PROJECT_RISK", "项目风险", "DANGER", activeRisks, "当前权限范围内的有效风险标记",
|
||||
canRisks, "/projects?riskStatus=ACTIVE"),
|
||||
risk("OVERDUE_TASK", "审批超时", "DANGER",
|
||||
count(canWorkflow, () -> workbenchMapper.countOverdueTasks(actor.userId(), role)),
|
||||
"超过处理时限的审批任务", canWorkflow, "/tasks?view=TODO"),
|
||||
risk("PAYMENT_BLOCK", "付款阻断", "WARNING",
|
||||
count(canPayments, () -> mapper.countPaymentBlocks(actor.userId(), role)),
|
||||
"付款检查中仍未解除的阻断项", canPayments,
|
||||
"/finance/payments?tab=finance-check&riskCode=BLOCK"),
|
||||
risk("ARCHIVE_MISSING", "档案缺件", "WARNING",
|
||||
count(canArchives, () -> workbenchMapper.countArchiveMissing(actor.userId(), role)),
|
||||
"待补充的归档材料", canArchives, "/archives/projects?completeness=INCOMPLETE"),
|
||||
risk("QUARANTINED_FILE", "隔离文件", "INFO",
|
||||
count(canFiles, () -> workbenchMapper.countFiles("QUARANTINED", actor.userId(), role)),
|
||||
"等待处理的隔离文件", canFiles, "/archives/files?scanStatus=QUARANTINED")
|
||||
);
|
||||
|
||||
List<WorkbenchActivityView> activities = workbenchMapper.listRecentActivities(actor.publicId(), 8).stream()
|
||||
.map(row -> new WorkbenchActivityView(row.publicId(), row.actionCode(), row.objectType(),
|
||||
row.objectPublicId(), row.title(), row.resultCode(), instant(row.occurredAt()),
|
||||
activityRoute(row.objectType(), row.objectPublicId())))
|
||||
.toList();
|
||||
|
||||
return new DashboardOverviewView(
|
||||
"系统经营仪表盘",
|
||||
Instant.now(),
|
||||
currency,
|
||||
systemView(),
|
||||
metrics,
|
||||
trend(actor.userId(), role, currency, canReceipts, canPayments, canInvoices),
|
||||
lifecycle(actor.userId(), role, canProjects),
|
||||
risks,
|
||||
new DashboardGeographyView(false, "项目尚未配置统一的省市维度,当前以项目阶段分布替代地图。", List.of()),
|
||||
activities
|
||||
);
|
||||
}
|
||||
|
||||
private DashboardTrendView trend(long userId, String role, String currency,
|
||||
boolean canReceipts, boolean canPayments, boolean canInvoices) {
|
||||
YearMonth firstMonth = YearMonth.now(ZoneOffset.UTC).minusMonths(5);
|
||||
LocalDate fromDate = firstMonth.atDay(1);
|
||||
Map<String, BigDecimal> receipts = amountMap(canReceipts
|
||||
? mapper.receiptTrend(userId, role, currency, fromDate) : List.of());
|
||||
Map<String, BigDecimal> payments = amountMap(canPayments
|
||||
? mapper.paymentTrend(userId, role, currency, fromDate) : List.of());
|
||||
Map<String, BigDecimal> invoices = amountMap(canInvoices
|
||||
? mapper.invoiceTrend(userId, role, currency, fromDate) : List.of());
|
||||
List<DashboardTrendPointView> points = new ArrayList<>();
|
||||
for (int index = 0; index < 6; index++) {
|
||||
String period = firstMonth.plusMonths(index).toString();
|
||||
points.add(new DashboardTrendPointView(period, plain(receipts.get(period)),
|
||||
plain(payments.get(period)), plain(invoices.get(period))));
|
||||
}
|
||||
return new DashboardTrendView(currency, canReceipts, canPayments, canInvoices, points);
|
||||
}
|
||||
|
||||
private List<DashboardDistributionView> lifecycle(long userId, String role, boolean available) {
|
||||
Map<String, Long> counts = new LinkedHashMap<>();
|
||||
if (available) {
|
||||
mapper.countProjectStages(userId, role).forEach(row -> counts.put(row.code(), row.itemCount()));
|
||||
}
|
||||
return STAGES.stream()
|
||||
.map(stage -> new DashboardDistributionView(stage, stageLabel(stage), counts.getOrDefault(stage, 0L),
|
||||
available, "/projects?stage=" + stage))
|
||||
.toList();
|
||||
}
|
||||
|
||||
private DashboardSystemView systemView() {
|
||||
List<DashboardServiceStatusView> services = new ArrayList<>();
|
||||
services.add(new DashboardServiceStatusView("APPLICATION", "应用服务", "UP", "服务运行正常"));
|
||||
services.add(new DashboardServiceStatusView("DATABASE", "数据库连接", "UP", "业务数据库连接正常"));
|
||||
String storageStatus = storageStatus();
|
||||
services.add(new DashboardServiceStatusView("FILE_STORAGE", "文件存储", storageStatus,
|
||||
"UP".equals(storageStatus) ? "存储目录可读写" : "存储目录待初始化或当前不可写"));
|
||||
services.add(new DashboardServiceStatusView("UPDATE", "更新服务", updateProperties.enabled() ? "UP" : "DISABLED",
|
||||
updateProperties.enabled() ? "在线更新服务已启用" : "在线更新服务未启用"));
|
||||
String overall = services.stream().anyMatch(item -> "DOWN".equals(item.status())) ? "DOWN"
|
||||
: services.stream().anyMatch(item -> "WARNING".equals(item.status())) ? "WARNING" : "UP";
|
||||
return new DashboardSystemView(overall, updateProperties.currentVersion(),
|
||||
ManagementFactory.getRuntimeMXBean().getUptime() / 1000, List.copyOf(services));
|
||||
}
|
||||
|
||||
private String storageStatus() {
|
||||
try {
|
||||
Path path = storageProperties.rootPath();
|
||||
return Files.isDirectory(path) && Files.isReadable(path) && Files.isWritable(path) ? "UP" : "WARNING";
|
||||
} catch (InvalidPathException | NullPointerException exception) {
|
||||
return "WARNING";
|
||||
}
|
||||
}
|
||||
|
||||
private Map<String, BigDecimal> amountMap(List<DashboardMapper.AmountRow> rows) {
|
||||
Map<String, BigDecimal> values = new LinkedHashMap<>();
|
||||
rows.forEach(row -> values.merge(row.period(), value(row.amount()), BigDecimal::add));
|
||||
return values;
|
||||
}
|
||||
|
||||
private DashboardMetricView countMetric(String code, String label, long value, boolean available,
|
||||
String route) {
|
||||
return new DashboardMetricView(code, label, "COUNT", available ? Long.toString(value) : "0", "项",
|
||||
available, available ? route : null);
|
||||
}
|
||||
|
||||
private DashboardMetricView moneyMetric(String code, String label, BigDecimal value, String currency,
|
||||
boolean available, String route) {
|
||||
return new DashboardMetricView(code, label, "MONEY", available ? plain(value) : "0", currency,
|
||||
available, available ? route : null);
|
||||
}
|
||||
|
||||
private DashboardRiskView risk(String code, String label, String severity, long count, String detail,
|
||||
boolean available, String route) {
|
||||
return new DashboardRiskView(code, label, severity, count, detail, available, available ? route : null);
|
||||
}
|
||||
|
||||
private long count(boolean available, Supplier<Long> supplier) {
|
||||
return available ? supplier.get() : 0;
|
||||
}
|
||||
|
||||
private BigDecimal amount(boolean available, Supplier<BigDecimal> supplier) {
|
||||
return available ? value(supplier.get()) : BigDecimal.ZERO;
|
||||
}
|
||||
|
||||
private BigDecimal value(BigDecimal value) {
|
||||
return value == null ? BigDecimal.ZERO : value;
|
||||
}
|
||||
|
||||
private String plain(BigDecimal value) {
|
||||
return value(value).stripTrailingZeros().toPlainString();
|
||||
}
|
||||
|
||||
private String normalizeCurrency(String requested) {
|
||||
String currency = requested == null ? "CNY" : requested.trim().toUpperCase(Locale.ROOT);
|
||||
if (!SUPPORTED_CURRENCIES.contains(currency)) {
|
||||
throw new BusinessException(HttpStatus.UNPROCESSABLE_ENTITY, ErrorCode.VALIDATION_FAILED,
|
||||
"币种仅支持 CNY、USD、EUR、HKD、JPY 或 GBP");
|
||||
}
|
||||
return currency;
|
||||
}
|
||||
|
||||
private String stageLabel(String stage) {
|
||||
return switch (stage) {
|
||||
case "INITIATION" -> "立项";
|
||||
case "CONTRACTING" -> "合同签订";
|
||||
case "EXECUTION" -> "执行中";
|
||||
case "ACCEPTANCE" -> "验收";
|
||||
case "SETTLEMENT" -> "结算";
|
||||
case "CLOSED" -> "已关闭";
|
||||
default -> stage;
|
||||
};
|
||||
}
|
||||
|
||||
private String activityRoute(String objectType, String objectPublicId) {
|
||||
if (objectPublicId == null || objectPublicId.isBlank()) return "/reports";
|
||||
return switch (objectType) {
|
||||
case "PROJECT" -> "/projects/%s".formatted(objectPublicId);
|
||||
case "PAYMENT", "PAYMENT_REQUEST" -> "/finance/payments?keyword=%s".formatted(objectPublicId);
|
||||
case "VOUCHER", "ACCOUNTING" -> "/finance/accounting?keyword=%s".formatted(objectPublicId);
|
||||
case "ARCHIVE_PACKAGE", "ARCHIVE" -> "/archives/projects";
|
||||
default -> "/reports";
|
||||
};
|
||||
}
|
||||
|
||||
private Instant instant(java.time.LocalDateTime value) {
|
||||
return value == null ? null : value.toInstant(ZoneOffset.UTC);
|
||||
}
|
||||
}
|
||||
+256
@@ -0,0 +1,256 @@
|
||||
package com.kaidi.finance.dashboard.infrastructure;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.time.LocalDate;
|
||||
import java.util.List;
|
||||
import org.apache.ibatis.annotations.Param;
|
||||
import org.apache.ibatis.annotations.Select;
|
||||
|
||||
@org.apache.ibatis.annotations.Mapper
|
||||
public interface DashboardMapper {
|
||||
|
||||
@Select("""
|
||||
SELECT COUNT(*)
|
||||
FROM md_project project
|
||||
JOIN md_company company ON company.id = project.company_id
|
||||
WHERE project.status IN ('ACTIVE', 'SUSPENDED')
|
||||
AND EXISTS (
|
||||
SELECT 1 FROM iam_scope scope
|
||||
JOIN iam_role role ON role.id = scope.role_id
|
||||
JOIN iam_permission permission ON permission.id = scope.permission_id
|
||||
WHERE scope.user_id = #{userId} AND role.code = #{roleCode}
|
||||
AND permission.code = 'project:project:view' AND scope.status = 'ACTIVE'
|
||||
AND scope.valid_from <= UTC_TIMESTAMP(3)
|
||||
AND (scope.valid_to IS NULL OR scope.valid_to >= UTC_TIMESTAMP(3))
|
||||
AND (scope.scope_type = 'GLOBAL'
|
||||
OR (scope.scope_type = 'COMPANY' AND scope.company_public_id = company.public_id)
|
||||
OR (scope.scope_type = 'PROJECT' AND scope.project_public_id = project.public_id))
|
||||
)
|
||||
""")
|
||||
long countProjects(@Param("userId") long userId, @Param("roleCode") String roleCode);
|
||||
|
||||
@Select("""
|
||||
SELECT COALESCE(SUM(contract.original_amount + contract.approved_change_amount), 0)
|
||||
FROM md_contract contract
|
||||
JOIN md_project project ON project.id = contract.project_id
|
||||
JOIN md_company company ON company.id = contract.company_id
|
||||
WHERE contract.status NOT IN ('VOID', 'DISABLED')
|
||||
AND contract.currency = #{currency}
|
||||
AND EXISTS (
|
||||
SELECT 1 FROM iam_scope scope
|
||||
JOIN iam_role role ON role.id = scope.role_id
|
||||
JOIN iam_permission permission ON permission.id = scope.permission_id
|
||||
WHERE scope.user_id = #{userId} AND role.code = #{roleCode}
|
||||
AND permission.code = 'masterdata:contract:view' AND scope.status = 'ACTIVE'
|
||||
AND scope.valid_from <= UTC_TIMESTAMP(3)
|
||||
AND (scope.valid_to IS NULL OR scope.valid_to >= UTC_TIMESTAMP(3))
|
||||
AND (scope.scope_type = 'GLOBAL'
|
||||
OR (scope.scope_type = 'COMPANY' AND scope.company_public_id = company.public_id)
|
||||
OR (scope.scope_type = 'PROJECT' AND scope.project_public_id = project.public_id))
|
||||
)
|
||||
""")
|
||||
BigDecimal sumContracts(@Param("userId") long userId, @Param("roleCode") String roleCode,
|
||||
@Param("currency") String currency);
|
||||
|
||||
@Select("""
|
||||
SELECT COALESCE(SUM(receipt.amount), 0)
|
||||
FROM fin_receipt receipt
|
||||
JOIN md_company company ON company.id = receipt.company_id
|
||||
LEFT JOIN md_project project ON project.id = receipt.project_id
|
||||
WHERE receipt.status NOT IN ('DRAFT', 'VOID')
|
||||
AND receipt.currency = #{currency}
|
||||
AND EXISTS (
|
||||
SELECT 1 FROM iam_scope scope
|
||||
JOIN iam_role role ON role.id = scope.role_id
|
||||
JOIN iam_permission permission ON permission.id = scope.permission_id
|
||||
WHERE scope.user_id = #{userId} AND role.code = #{roleCode}
|
||||
AND permission.code = 'receivable:receipt:view' AND scope.status = 'ACTIVE'
|
||||
AND scope.valid_from <= UTC_TIMESTAMP(3)
|
||||
AND (scope.valid_to IS NULL OR scope.valid_to >= UTC_TIMESTAMP(3))
|
||||
AND (scope.scope_type = 'GLOBAL'
|
||||
OR (scope.scope_type = 'COMPANY' AND scope.company_public_id = company.public_id)
|
||||
OR (scope.scope_type = 'PROJECT' AND scope.project_public_id = project.public_id))
|
||||
)
|
||||
""")
|
||||
BigDecimal sumReceipts(@Param("userId") long userId, @Param("roleCode") String roleCode,
|
||||
@Param("currency") String currency);
|
||||
|
||||
@Select("""
|
||||
SELECT COALESCE(SUM(COALESCE(payment.approved_amount, payment.requested_amount)), 0)
|
||||
FROM fin_payment_request payment
|
||||
JOIN md_company company ON company.id = payment.company_id
|
||||
JOIN md_project project ON project.id = payment.project_id
|
||||
WHERE payment.status = 'PAID'
|
||||
AND payment.currency = #{currency}
|
||||
AND EXISTS (
|
||||
SELECT 1 FROM iam_scope scope
|
||||
JOIN iam_role role ON role.id = scope.role_id
|
||||
JOIN iam_permission permission ON permission.id = scope.permission_id
|
||||
WHERE scope.user_id = #{userId} AND role.code = #{roleCode}
|
||||
AND permission.code = 'payment:request:view' AND scope.status = 'ACTIVE'
|
||||
AND scope.valid_from <= UTC_TIMESTAMP(3)
|
||||
AND (scope.valid_to IS NULL OR scope.valid_to >= UTC_TIMESTAMP(3))
|
||||
AND (scope.scope_type = 'GLOBAL'
|
||||
OR (scope.scope_type = 'COMPANY' AND scope.company_public_id = company.public_id)
|
||||
OR (scope.scope_type = 'PROJECT' AND scope.project_public_id = project.public_id))
|
||||
)
|
||||
""")
|
||||
BigDecimal sumPayments(@Param("userId") long userId, @Param("roleCode") String roleCode,
|
||||
@Param("currency") String currency);
|
||||
|
||||
@Select("""
|
||||
SELECT project.stage AS code, COUNT(*) AS item_count
|
||||
FROM md_project project
|
||||
JOIN md_company company ON company.id = project.company_id
|
||||
WHERE project.status IN ('ACTIVE', 'SUSPENDED', 'CLOSED', 'ARCHIVED')
|
||||
AND EXISTS (
|
||||
SELECT 1 FROM iam_scope scope
|
||||
JOIN iam_role role ON role.id = scope.role_id
|
||||
JOIN iam_permission permission ON permission.id = scope.permission_id
|
||||
WHERE scope.user_id = #{userId} AND role.code = #{roleCode}
|
||||
AND permission.code = 'project:project:view' AND scope.status = 'ACTIVE'
|
||||
AND scope.valid_from <= UTC_TIMESTAMP(3)
|
||||
AND (scope.valid_to IS NULL OR scope.valid_to >= UTC_TIMESTAMP(3))
|
||||
AND (scope.scope_type = 'GLOBAL'
|
||||
OR (scope.scope_type = 'COMPANY' AND scope.company_public_id = company.public_id)
|
||||
OR (scope.scope_type = 'PROJECT' AND scope.project_public_id = project.public_id))
|
||||
)
|
||||
GROUP BY project.stage
|
||||
ORDER BY FIELD(project.stage, 'INITIATION', 'CONTRACTING', 'EXECUTION',
|
||||
'ACCEPTANCE', 'SETTLEMENT', 'CLOSED')
|
||||
""")
|
||||
List<CountRow> countProjectStages(@Param("userId") long userId, @Param("roleCode") String roleCode);
|
||||
|
||||
@Select("""
|
||||
SELECT DATE_FORMAT(receipt.receipt_date, '%Y-%m') AS period,
|
||||
COALESCE(SUM(receipt.amount), 0) AS amount
|
||||
FROM fin_receipt receipt
|
||||
JOIN md_company company ON company.id = receipt.company_id
|
||||
LEFT JOIN md_project project ON project.id = receipt.project_id
|
||||
WHERE receipt.status NOT IN ('DRAFT', 'VOID')
|
||||
AND receipt.currency = #{currency}
|
||||
AND receipt.receipt_date >= #{fromDate}
|
||||
AND EXISTS (
|
||||
SELECT 1 FROM iam_scope scope
|
||||
JOIN iam_role role ON role.id = scope.role_id
|
||||
JOIN iam_permission permission ON permission.id = scope.permission_id
|
||||
WHERE scope.user_id = #{userId} AND role.code = #{roleCode}
|
||||
AND permission.code = 'receivable:receipt:view' AND scope.status = 'ACTIVE'
|
||||
AND scope.valid_from <= UTC_TIMESTAMP(3)
|
||||
AND (scope.valid_to IS NULL OR scope.valid_to >= UTC_TIMESTAMP(3))
|
||||
AND (scope.scope_type = 'GLOBAL'
|
||||
OR (scope.scope_type = 'COMPANY' AND scope.company_public_id = company.public_id)
|
||||
OR (scope.scope_type = 'PROJECT' AND scope.project_public_id = project.public_id))
|
||||
)
|
||||
GROUP BY DATE_FORMAT(receipt.receipt_date, '%Y-%m')
|
||||
ORDER BY period
|
||||
""")
|
||||
List<AmountRow> receiptTrend(@Param("userId") long userId, @Param("roleCode") String roleCode,
|
||||
@Param("currency") String currency, @Param("fromDate") LocalDate fromDate);
|
||||
|
||||
@Select("""
|
||||
SELECT DATE_FORMAT(payment.requested_date, '%Y-%m') AS period,
|
||||
COALESCE(SUM(COALESCE(payment.approved_amount, payment.requested_amount)), 0) AS amount
|
||||
FROM fin_payment_request payment
|
||||
JOIN md_company company ON company.id = payment.company_id
|
||||
JOIN md_project project ON project.id = payment.project_id
|
||||
WHERE payment.status = 'PAID'
|
||||
AND payment.currency = #{currency}
|
||||
AND payment.requested_date >= #{fromDate}
|
||||
AND EXISTS (
|
||||
SELECT 1 FROM iam_scope scope
|
||||
JOIN iam_role role ON role.id = scope.role_id
|
||||
JOIN iam_permission permission ON permission.id = scope.permission_id
|
||||
WHERE scope.user_id = #{userId} AND role.code = #{roleCode}
|
||||
AND permission.code = 'payment:request:view' AND scope.status = 'ACTIVE'
|
||||
AND scope.valid_from <= UTC_TIMESTAMP(3)
|
||||
AND (scope.valid_to IS NULL OR scope.valid_to >= UTC_TIMESTAMP(3))
|
||||
AND (scope.scope_type = 'GLOBAL'
|
||||
OR (scope.scope_type = 'COMPANY' AND scope.company_public_id = company.public_id)
|
||||
OR (scope.scope_type = 'PROJECT' AND scope.project_public_id = project.public_id))
|
||||
)
|
||||
GROUP BY DATE_FORMAT(payment.requested_date, '%Y-%m')
|
||||
ORDER BY period
|
||||
""")
|
||||
List<AmountRow> paymentTrend(@Param("userId") long userId, @Param("roleCode") String roleCode,
|
||||
@Param("currency") String currency, @Param("fromDate") LocalDate fromDate);
|
||||
|
||||
@Select("""
|
||||
SELECT DATE_FORMAT(COALESCE(invoice.issued_date, invoice.requested_date), '%Y-%m') AS period,
|
||||
COALESCE(SUM(invoice.amount), 0) AS amount
|
||||
FROM fin_invoice invoice
|
||||
JOIN md_company company ON company.id = invoice.company_id
|
||||
JOIN md_project project ON project.id = invoice.project_id
|
||||
WHERE invoice.status = 'ISSUED'
|
||||
AND project.currency = #{currency}
|
||||
AND COALESCE(invoice.issued_date, invoice.requested_date) >= #{fromDate}
|
||||
AND EXISTS (
|
||||
SELECT 1 FROM iam_scope scope
|
||||
JOIN iam_role role ON role.id = scope.role_id
|
||||
JOIN iam_permission permission ON permission.id = scope.permission_id
|
||||
WHERE scope.user_id = #{userId} AND role.code = #{roleCode}
|
||||
AND permission.code = 'receivable:invoice:view' AND scope.status = 'ACTIVE'
|
||||
AND scope.valid_from <= UTC_TIMESTAMP(3)
|
||||
AND (scope.valid_to IS NULL OR scope.valid_to >= UTC_TIMESTAMP(3))
|
||||
AND (scope.scope_type = 'GLOBAL'
|
||||
OR (scope.scope_type = 'COMPANY' AND scope.company_public_id = company.public_id)
|
||||
OR (scope.scope_type = 'PROJECT' AND scope.project_public_id = project.public_id))
|
||||
)
|
||||
GROUP BY DATE_FORMAT(COALESCE(invoice.issued_date, invoice.requested_date), '%Y-%m')
|
||||
ORDER BY period
|
||||
""")
|
||||
List<AmountRow> invoiceTrend(@Param("userId") long userId, @Param("roleCode") String roleCode,
|
||||
@Param("currency") String currency, @Param("fromDate") LocalDate fromDate);
|
||||
|
||||
@Select("""
|
||||
SELECT COUNT(*)
|
||||
FROM project_risk_flag risk
|
||||
JOIN md_project project ON project.id = risk.project_id
|
||||
JOIN md_company company ON company.id = project.company_id
|
||||
WHERE risk.status = 'ACTIVE'
|
||||
AND risk.effective_from <= UTC_TIMESTAMP(3)
|
||||
AND (risk.effective_until IS NULL OR risk.effective_until >= UTC_TIMESTAMP(3))
|
||||
AND EXISTS (
|
||||
SELECT 1 FROM iam_scope scope
|
||||
JOIN iam_role role ON role.id = scope.role_id
|
||||
JOIN iam_permission permission ON permission.id = scope.permission_id
|
||||
WHERE scope.user_id = #{userId} AND role.code = #{roleCode}
|
||||
AND permission.code = 'project:risk-flag:view' AND scope.status = 'ACTIVE'
|
||||
AND scope.valid_from <= UTC_TIMESTAMP(3)
|
||||
AND (scope.valid_to IS NULL OR scope.valid_to >= UTC_TIMESTAMP(3))
|
||||
AND (scope.scope_type = 'GLOBAL'
|
||||
OR (scope.scope_type = 'COMPANY' AND scope.company_public_id = company.public_id)
|
||||
OR (scope.scope_type = 'PROJECT' AND scope.project_public_id = project.public_id))
|
||||
)
|
||||
""")
|
||||
long countActiveRisks(@Param("userId") long userId, @Param("roleCode") String roleCode);
|
||||
|
||||
@Select("""
|
||||
SELECT COUNT(DISTINCT payment.id)
|
||||
FROM fin_payment_check payment_check
|
||||
JOIN fin_payment_request payment ON payment.id = payment_check.payment_id
|
||||
JOIN md_company company ON company.id = payment.company_id
|
||||
JOIN md_project project ON project.id = payment.project_id
|
||||
WHERE payment_check.result = 'BLOCK'
|
||||
AND payment.status NOT IN ('VOID', 'PAID', 'REFUNDED')
|
||||
AND EXISTS (
|
||||
SELECT 1 FROM iam_scope scope
|
||||
JOIN iam_role role ON role.id = scope.role_id
|
||||
JOIN iam_permission permission ON permission.id = scope.permission_id
|
||||
WHERE scope.user_id = #{userId} AND role.code = #{roleCode}
|
||||
AND permission.code = 'payment:request:view' AND scope.status = 'ACTIVE'
|
||||
AND scope.valid_from <= UTC_TIMESTAMP(3)
|
||||
AND (scope.valid_to IS NULL OR scope.valid_to >= UTC_TIMESTAMP(3))
|
||||
AND (scope.scope_type = 'GLOBAL'
|
||||
OR (scope.scope_type = 'COMPANY' AND scope.company_public_id = company.public_id)
|
||||
OR (scope.scope_type = 'PROJECT' AND scope.project_public_id = project.public_id))
|
||||
)
|
||||
""")
|
||||
long countPaymentBlocks(@Param("userId") long userId, @Param("roleCode") String roleCode);
|
||||
|
||||
record CountRow(String code, long itemCount) {
|
||||
}
|
||||
|
||||
record AmountRow(String period, BigDecimal amount) {
|
||||
}
|
||||
}
|
||||
@@ -376,7 +376,7 @@ public class AuthApplicationService {
|
||||
case "PROJECT_MANAGER" -> "/workbench/project";
|
||||
case "FINANCE_MANAGER" -> "/workbench/finance";
|
||||
case "ARCHIVE_MANAGER" -> "/workbench/archive";
|
||||
case "SYSTEM_ADMIN" -> "/governance/settings";
|
||||
case "SYSTEM_ADMIN" -> "/dashboard";
|
||||
default -> "/role-select";
|
||||
};
|
||||
}
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
INSERT INTO iam_permission (code, name)
|
||||
VALUES ('dashboard:overview:view', '查看系统仪表盘')
|
||||
ON DUPLICATE KEY UPDATE name = VALUES(name);
|
||||
|
||||
-- The isolated system administrator is the only role that receives the new
|
||||
-- entry automatically. Business roles can be granted the permission from
|
||||
-- "权限与配置" according to the customer's actual responsibility matrix.
|
||||
INSERT IGNORE INTO iam_role_permission (role_id, permission_id)
|
||||
SELECT role.id, permission.id
|
||||
FROM iam_role role
|
||||
JOIN iam_permission permission ON permission.code = 'dashboard:overview:view'
|
||||
WHERE role.code = 'SYSTEM_ADMIN'
|
||||
AND role.enabled = TRUE;
|
||||
|
||||
-- Keep the server-side permission and data-scope model aligned for existing
|
||||
-- administrator accounts. Front-end super-admin bypasses are not considered
|
||||
-- an authorization boundary.
|
||||
INSERT IGNORE INTO iam_scope (
|
||||
user_id,
|
||||
role_id,
|
||||
permission_id,
|
||||
scope_type,
|
||||
company_public_id,
|
||||
project_public_id,
|
||||
amount_limit,
|
||||
status
|
||||
)
|
||||
SELECT user_role.user_id,
|
||||
user_role.role_id,
|
||||
permission.id,
|
||||
'GLOBAL',
|
||||
NULL,
|
||||
NULL,
|
||||
NULL,
|
||||
'ACTIVE'
|
||||
FROM iam_user_role user_role
|
||||
JOIN iam_role role ON role.id = user_role.role_id
|
||||
JOIN iam_permission permission ON permission.code = 'dashboard:overview:view'
|
||||
WHERE role.code = 'SYSTEM_ADMIN'
|
||||
AND role.enabled = TRUE;
|
||||
+136
@@ -0,0 +1,136 @@
|
||||
package com.kaidi.finance.dashboard;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.ArgumentMatchers.anyString;
|
||||
import static org.mockito.ArgumentMatchers.anyLong;
|
||||
import static org.mockito.ArgumentMatchers.anyInt;
|
||||
import static org.mockito.Mockito.doThrow;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
import com.kaidi.finance.dashboard.api.DashboardMetricView;
|
||||
import com.kaidi.finance.dashboard.application.DashboardApplicationService;
|
||||
import com.kaidi.finance.dashboard.infrastructure.DashboardMapper;
|
||||
import com.kaidi.finance.iam.domain.FinancePrincipal;
|
||||
import com.kaidi.finance.iam.domain.RoleAssignment;
|
||||
import com.kaidi.finance.shared.api.BusinessException;
|
||||
import com.kaidi.finance.shared.file.FileStorageProperties;
|
||||
import com.kaidi.finance.shared.security.AuthorizationService;
|
||||
import com.kaidi.finance.shared.security.IdentityContext;
|
||||
import com.kaidi.finance.update.application.SystemUpdateProperties;
|
||||
import com.kaidi.finance.workbench.infrastructure.WorkbenchMapper;
|
||||
import java.math.BigDecimal;
|
||||
import java.nio.file.Path;
|
||||
import java.time.Duration;
|
||||
import java.util.List;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.io.TempDir;
|
||||
|
||||
class DashboardApplicationServiceTest {
|
||||
|
||||
@TempDir
|
||||
Path tempDir;
|
||||
|
||||
private DashboardMapper mapper;
|
||||
private WorkbenchMapper workbenchMapper;
|
||||
private IdentityContext identity;
|
||||
private AuthorizationService authorization;
|
||||
private DashboardApplicationService service;
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
mapper = mock(DashboardMapper.class);
|
||||
workbenchMapper = mock(WorkbenchMapper.class);
|
||||
identity = mock(IdentityContext.class);
|
||||
authorization = mock(AuthorizationService.class);
|
||||
when(identity.requirePrincipal()).thenReturn(new FinancePrincipal(
|
||||
7, "01J00000000000000000000007", "admin", "系统管理员", "财务部", false,
|
||||
List.of(new RoleAssignment(1, "SYSTEM_ADMIN", "超级管理员", "系统治理"))));
|
||||
when(identity.requireActiveRole()).thenReturn("SYSTEM_ADMIN");
|
||||
when(authorization.hasPermission(anyString())).thenReturn(true);
|
||||
when(workbenchMapper.countPendingTasks(anyLong(), anyString())).thenReturn(3L);
|
||||
when(workbenchMapper.countOverdueTasks(anyLong(), anyString())).thenReturn(1L);
|
||||
when(workbenchMapper.countArchiveMissing(anyLong(), anyString())).thenReturn(2L);
|
||||
when(workbenchMapper.countFiles(anyString(), anyLong(), anyString())).thenReturn(1L);
|
||||
when(workbenchMapper.listRecentActivities(anyString(), anyInt())).thenReturn(List.of());
|
||||
when(mapper.countProjectStages(anyLong(), anyString())).thenReturn(List.of(
|
||||
new DashboardMapper.CountRow("INITIATION", 2L),
|
||||
new DashboardMapper.CountRow("EXECUTION", 1L)));
|
||||
when(mapper.receiptTrend(anyLong(), anyString(), anyString(), any())).thenReturn(List.of());
|
||||
when(mapper.paymentTrend(anyLong(), anyString(), anyString(), any())).thenReturn(List.of());
|
||||
when(mapper.invoiceTrend(anyLong(), anyString(), anyString(), any())).thenReturn(List.of());
|
||||
when(mapper.countProjects(anyLong(), anyString())).thenReturn(3L);
|
||||
when(mapper.sumContracts(anyLong(), anyString(), anyString())).thenReturn(new BigDecimal("12345.67"));
|
||||
when(mapper.sumReceipts(anyLong(), anyString(), anyString())).thenReturn(new BigDecimal("2345.67"));
|
||||
when(mapper.sumPayments(anyLong(), anyString(), anyString())).thenReturn(new BigDecimal("345.67"));
|
||||
when(mapper.countActiveRisks(anyLong(), anyString())).thenReturn(4L);
|
||||
when(mapper.countPaymentBlocks(anyLong(), anyString())).thenReturn(1L);
|
||||
service = new DashboardApplicationService(
|
||||
mapper,
|
||||
workbenchMapper,
|
||||
identity,
|
||||
authorization,
|
||||
new SystemUpdateProperties(true, "1.0.0-preview.48", tempDir.resolve("VERSION"),
|
||||
"https://git.example.invalid/releases", "https://git.example.invalid/api/latest", "",
|
||||
tempDir.resolve("request.json"), tempDir.resolve("status.json"), Duration.ofSeconds(1),
|
||||
Duration.ofSeconds(1), false),
|
||||
new FileStorageProperties(tempDir.toString(), tempDir.resolve("tmp").toString(), 100_000L));
|
||||
}
|
||||
|
||||
@Test
|
||||
void buildsPermissionAwareOverviewWithMoneyAndLifecycleData() {
|
||||
var view = service.overview("usd");
|
||||
|
||||
assertEquals("USD", view.currency());
|
||||
assertEquals("1.0.0-preview.48", view.system().currentVersion());
|
||||
assertEquals("UP", view.system().overallStatus());
|
||||
assertEquals(6, view.trend().points().size());
|
||||
assertEquals(2L, view.lifecycle().stream()
|
||||
.filter(item -> item.code().equals("INITIATION"))
|
||||
.findFirst().orElseThrow().value());
|
||||
DashboardMetricView contract = view.metrics().stream()
|
||||
.filter(item -> item.code().equals("CONTRACT_AMOUNT"))
|
||||
.findFirst().orElseThrow();
|
||||
assertEquals("12345.67", contract.value());
|
||||
assertTrue(contract.available());
|
||||
assertFalse(view.geography().available());
|
||||
}
|
||||
|
||||
@Test
|
||||
void hidesBusinessWidgetsWhenTheirUnderlyingPermissionIsMissing() {
|
||||
when(authorization.hasPermission("project:project:view")).thenReturn(false);
|
||||
when(authorization.hasPermission("masterdata:contract:view")).thenReturn(false);
|
||||
when(authorization.hasPermission("receivable:receipt:view")).thenReturn(false);
|
||||
when(authorization.hasPermission("payment:request:view")).thenReturn(false);
|
||||
when(authorization.hasPermission("receivable:invoice:view")).thenReturn(false);
|
||||
when(authorization.hasPermission("workflow:task:view")).thenReturn(false);
|
||||
when(authorization.hasPermission("project:risk-flag:view")).thenReturn(false);
|
||||
when(authorization.hasPermission("archive:package:view")).thenReturn(false);
|
||||
when(authorization.hasPermission("archive:file:view")).thenReturn(false);
|
||||
|
||||
var view = service.overview("CNY");
|
||||
|
||||
assertTrue(view.metrics().stream().noneMatch(DashboardMetricView::available));
|
||||
assertTrue(view.lifecycle().stream().noneMatch(item -> item.available()));
|
||||
assertTrue(view.risks().stream().noneMatch(item -> item.available()));
|
||||
}
|
||||
|
||||
@Test
|
||||
void rejectsUnsupportedCurrencyBeforeQueryingBusinessData() {
|
||||
assertThrows(BusinessException.class, () -> service.overview("RMB"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void requiresTheDedicatedDashboardPermission() {
|
||||
doThrow(new BusinessException(org.springframework.http.HttpStatus.FORBIDDEN,
|
||||
com.kaidi.finance.shared.api.ErrorCode.PERMISSION_DENIED, "无仪表盘权限"))
|
||||
.when(authorization).requirePermission("dashboard:overview:view");
|
||||
|
||||
assertThrows(BusinessException.class, () -> service.overview("CNY"));
|
||||
}
|
||||
}
|
||||
@@ -179,7 +179,7 @@ class AuthIntegrationTest {
|
||||
.andExpect(jsonPath("$.data.roles.length()").value(1))
|
||||
.andExpect(jsonPath("$.data.roles[0].code").value("SYSTEM_ADMIN"))
|
||||
.andExpect(jsonPath("$.data.roles[0].name").value("超级管理员"))
|
||||
.andExpect(jsonPath("$.data.roles[0].workbenchRoute").value("/governance/settings"))
|
||||
.andExpect(jsonPath("$.data.roles[0].workbenchRoute").value("/dashboard"))
|
||||
.andReturn();
|
||||
|
||||
ClientSession session = clientSession(login);
|
||||
|
||||
Reference in New Issue
Block a user