From 7100c8a4f7dd3fc7d53ffead4475d7a8abc51c12 Mon Sep 17 00:00:00 2001
From: yeg <1347747024@qq.com>
Date: 星期三, 09 九月 2026 02:32:44 +0800
Subject: [PATCH] 完善循环拼接与首页待办真实数据接口
---
ruoyi-admin/src/test/java/com/battery/business/report/CompareMergedSourcesTest.java | 33 ++++
battery-business/src/main/java/com/battery/business/report/CompareReportService.java | 10 +
ruoyi-admin/src/test/java/com/battery/business/report/ReportTestDetailPersistenceTest.java | 2
battery-business/src/main/java/com/battery/business/service/TodoService.java | 69 +++++++-
ruoyi-admin/src/main/java/com/battery/business/web/TodoController.java | 25 ++
ruoyi-admin/src/test/java/com/battery/business/report/SpliceProjectCyclesTest.java | 46 +++++
battery-business/src/main/java/com/battery/business/service/DashboardService.java | 91 +++++++++++
ruoyi-admin/src/test/java/com/battery/business/service/TodoServiceTest.java | 39 ++++
ruoyi-admin/src/main/java/com/battery/business/web/DashboardController.java | 19 ++
ruoyi-admin/src/main/java/com/ruoyi/web/controller/monitor/SysOperlogController.java | 2
ruoyi-admin/src/main/resources/db/migration-final/V21__dashboard_and_todo_permissions.sql | 35 ++++
ruoyi-admin/src/test/java/com/battery/business/service/DashboardServiceTest.java | 57 +++++++
battery-business/src/main/java/com/battery/business/report/SpliceReportService.java | 47 +++--
13 files changed, 438 insertions(+), 37 deletions(-)
diff --git a/battery-business/src/main/java/com/battery/business/report/CompareReportService.java b/battery-business/src/main/java/com/battery/business/report/CompareReportService.java
index d14ac6e..f3cc618 100644
--- a/battery-business/src/main/java/com/battery/business/report/CompareReportService.java
+++ b/battery-business/src/main/java/com/battery/business/report/CompareReportService.java
@@ -49,7 +49,13 @@
{
TTest test = tests.selectTestById(summary.getTestId());
if (!"COMPLETED".equals(test.getTestStatus())) continue;
- result.addAll(candidates(test, data.records(test.getTestId())));
+ var available = candidates(test, data.records(test.getTestId()));
+ Set<String> mapped = new HashSet<>();
+ for (var item : available) if (item.selection().logicalNo() != null)
+ mapped.add(item.selection().testBatteryId() + "|" + item.selection().cycleNo());
+ // 与控制台一致:有保存的逻辑工步时,新选入口展示合并结果;旧快照仍保留原选择。
+ result.addAll(available.stream().filter(item -> item.selection().logicalNo() != null
+ || !mapped.contains(item.selection().testBatteryId() + "|" + item.selection().cycleNo())).toList());
}
return result;
}
@@ -57,7 +63,7 @@
/** 预览所选当前有效记录;相同电池的不同循环或工步是独立比对项。 */
public List<Item> preview(List<Selection> selections)
{
- if (selections == null || selections.isEmpty() || selections.contains(null)
+ if (selections == null || selections.isEmpty() || selections.stream().anyMatch(Objects::isNull)
|| new HashSet<>(selections).size() != selections.size())
throw new ServiceException("请选择不重复的比对项");
Map<Long, TTest> loadedTests = new HashMap<>();
diff --git a/battery-business/src/main/java/com/battery/business/report/SpliceReportService.java b/battery-business/src/main/java/com/battery/business/report/SpliceReportService.java
index 841f84d..1506ecc 100644
--- a/battery-business/src/main/java/com/battery/business/report/SpliceReportService.java
+++ b/battery-business/src/main/java/com/battery/business/report/SpliceReportService.java
@@ -9,6 +9,7 @@
import com.battery.business.domain.TTest;
import com.battery.business.domain.TTestBattery;
import com.battery.business.service.ITTestService;
+import com.battery.business.service.ITBatteryGroupService;
import com.battery.business.service.ITestDataQueryService;
import com.ruoyi.common.exception.ServiceException;
import org.springframework.stereotype.Service;
@@ -21,8 +22,9 @@
private final ITestDataQueryService data;
private final SpliceReportRepository reports;
private final SpliceFieldTemplateService templates;
- public SpliceReportService(ITTestService tests, ITestDataQueryService data, SpliceReportRepository reports, SpliceFieldTemplateService templates) {
- this.tests = tests; this.data = data; this.reports = reports; this.templates = templates;
+ private final ITBatteryGroupService groups;
+ public SpliceReportService(ITTestService tests, ITestDataQueryService data, SpliceReportRepository reports, SpliceFieldTemplateService templates, ITBatteryGroupService groups) {
+ this.tests = tests; this.data = data; this.reports = reports; this.templates = templates; this.groups = groups;
}
public record Candidate(SpliceReportRequest.Selection selection,String name,String type,long groupId,String group,String batteryNo,long startedAt){}
public record Matrix(List<Map<String,Object>> segments,List<Integer> cycleNos){}
@@ -39,7 +41,7 @@
}
/** 校验每个测试电池来源并按提交顺序逐段接续真实循环。 */
public Matrix preview(List<SpliceReportRequest.Selection> selections) {
- if (selections == null || selections.size() < 2 || selections.contains(null)
+ if (selections == null || selections.size() < 2 || selections.stream().anyMatch(Objects::isNull)
|| new HashSet<>(selections).size() != selections.size())
throw new ServiceException("请选择至少两个不重复的检测完成测试电池");
Map<Long, TTest> loaded = new LinkedHashMap<>();
@@ -54,14 +56,14 @@
}
List<Map<String, Object>> segments = new ArrayList<>();
Set<Integer> mappedCycles = new TreeSet<>();
- Integer previousEnd = null;
+ int position = 0;
for (var entry : selected.entrySet()) {
long testId = entry.getKey().testId();
- List<Map<String, Object>> cycles = data.cycles(testId), steps = data.steps(testId), records = data.records(testId);
+ List<Map<String, Object>> cycles = data.cycles(testId), steps = data.steps(testId);
TTestBattery battery = entry.getValue();
var rows = batteryRows(cycles, battery);
var stepRows = batteryRows(steps, battery);
- var nos = java.util.stream.Stream.of(rows, stepRows, batteryRows(records, battery)).flatMap(List::stream)
+ var nos = java.util.stream.Stream.of(rows, stepRows).flatMap(List::stream)
.map(this::cycle).filter(no -> no > 0).distinct().sorted().toList();
Map<String, Object> segment = new LinkedHashMap<>();
segment.put("source", candidate(loaded.get(testId), battery));
@@ -79,12 +81,14 @@
});
frozen.put("fieldValues", values); segment.put("consoleDetail", frozen);
}
- segment.put("originalCycleNos", nos); segment.put("statistics", statistics(rows, stepRows, nos));
- // 每个测试电池独立计算偏移;空段不占用循环,各指标共用同一定位。
- int offset = previousEnd == null || nos.isEmpty() ? 0 : previousEnd + 1 - nos.get(0);
- segment.put("offset", offset);
- for (int no : nos) mappedCycles.add(no + offset);
- if (!nos.isEmpty()) previousEnd = nos.get(nos.size() - 1) + offset;
+ BigDecimal factor = capacityFactor(battery);
+ segment.put("capacityFactor", factor);
+ segment.put("originalCycleNos", nos); segment.put("statistics", statistics(rows, stepRows, nos, factor));
+ // 累计位置只计实际循环点;原循环号单独冻结,不能用数值偏移保留缺号空位。
+ Map<String,Integer> positions = new LinkedHashMap<>();
+ segment.put("offset", position);
+ for (int no : nos) { positions.put(Integer.toString(no), ++position); mappedCycles.add(position); }
+ segment.put("cyclePositions", positions);
segments.add(segment);
}
return new Matrix(segments, new ArrayList<>(mappedCycles));
@@ -92,7 +96,13 @@
private List<Map<String, Object>> batteryRows(List<Map<String, Object>> rows, TTestBattery battery) {
return rows.stream().filter(row -> Objects.equals(row.get("batteryNo"), battery.getBatteryNo())).toList();
}
- private Map<String,Object> statistics(List<Map<String,Object>> rows, List<Map<String,Object>> stepRows, List<Integer> nos) {
+ private BigDecimal capacityFactor(TTestBattery battery) {
+ var group = groups.selectBatteryGroupById(battery.getGroupId());
+ if (group == null || group.getF() == null) return null;
+ try { var value = new BigDecimal(group.getF().trim()); return value.signum() == 0 ? null : value; }
+ catch (NumberFormatException error) { return null; }
+ }
+ private Map<String,Object> statistics(List<Map<String,Object>> rows, List<Map<String,Object>> stepRows, List<Integer> nos, BigDecimal factor) {
Map<String,Object> result = new LinkedHashMap<>();
for (int no : nos) {
Map<String,Object> values = new LinkedHashMap<>();
@@ -105,9 +115,14 @@
if (charge != null && discharge != null && charge.doubleValue() > 0)
values.put("energyEfficiency", new BigDecimal(discharge.toString()).multiply(BigDecimal.valueOf(100))
.divide(new BigDecimal(charge.toString()), MathContext.DECIMAL128));
+ // 与项目图表一致:Ah 转 mAh 后除以来源电池组 F,缺失或零分母不补值。
+ Number capacity = num(values.get("dischargeCapacity"));
+ if (capacity != null && factor != null)
+ values.put("dischargeCapacityF", new BigDecimal(capacity.toString()).multiply(BigDecimal.valueOf(1000)).divide(factor, MathContext.DECIMAL128));
}
// 查询接口按实际发生顺序返回工步;先选末记录再取值,空值不得向前回退。
- var actualSteps = stepRows.stream().filter(row -> cycle(row) == no).toList();
+ var actualSteps = stepRows.stream().filter(row -> cycle(row) == no)
+ .sorted(Comparator.comparingInt(row -> row.get("stepNo") instanceof Number value ? value.intValue() : 0)).toList();
if (!actualSteps.isEmpty()) values.put("finalVoltage1", actualSteps.get(actualSteps.size() - 1).get("endVoltage"));
result.put(Integer.toString(no), values);
}
@@ -124,7 +139,7 @@
validateStatistics(settings, matrix.segments());
validateSourceDetails(settings, matrix.segments());
Map<String,Object> snapshot = new LinkedHashMap<>();
- snapshot.put("spliceMode", "battery-segments");
+ snapshot.put("spliceMode", "project-cycles");
snapshot.put("name", request.name().trim()); snapshot.put("description", request.description());
snapshot.put("selections", request.selections()); snapshot.put("segments", matrix.segments());
snapshot.put("cycleNos", matrix.cycleNos()); snapshot.put("settings", settings);
@@ -179,7 +194,7 @@
overrides.keySet().retainAll(statisticKeys(matrix.segments()));
settings.put("statisticValues", overrides);
snapshot.put("settings", settings); snapshot.put("segments", matrix.segments()); snapshot.put("cycleNos", matrix.cycleNos());
- snapshot.put("spliceMode", "battery-segments");
+ snapshot.put("spliceMode", "project-cycles");
reports.append(id, base + 1, snapshot.getString("name"), snapshot.toJSONString(), uid, user, "按最新来源生成:" + summary.trim());
return get(id, null);
}
diff --git a/battery-business/src/main/java/com/battery/business/service/DashboardService.java b/battery-business/src/main/java/com/battery/business/service/DashboardService.java
new file mode 100644
index 0000000..d8e0f29
--- /dev/null
+++ b/battery-business/src/main/java/com/battery/business/service/DashboardService.java
@@ -0,0 +1,91 @@
+package com.battery.business.service;
+
+import java.math.BigDecimal;
+import java.time.*;
+import java.util.*;
+import javax.sql.DataSource;
+import com.battery.business.domain.*;
+import com.battery.business.mapper.TTestMapper;
+import org.springframework.jdbc.core.JdbcTemplate;
+import org.springframework.stereotype.Service;
+
+/** 首页只读聚合已有事实,不发起设备控制或为展示新增采样。 */
+@Service
+public class DashboardService {
+ private static final ZoneId BUSINESS_ZONE = ZoneId.of("Asia/Hong_Kong");
+ private final ITTestService tests;
+ private final ITDetectionCabinetService cabinets;
+ private final TTestMapper testMapper;
+ private final JdbcTemplate jdbc;
+ public DashboardService(ITTestService tests, ITDetectionCabinetService cabinets, TTestMapper testMapper, DataSource source) {
+ this.tests = tests; this.cabinets = cabinets; this.testMapper = testMapper; jdbc = new JdbcTemplate(source);
+ }
+ public record Summary(long testCount, long batteryGroupCount, long todayCount, long monthCount) {}
+ public record Trend(List<String> labels, List<Long> values) {}
+ public record TestRow(long id, String name, String type, String status, String receptionStatus, String step, long warning) {}
+ public record CabinetRow(long id, String name, BigDecimal capacity, int channelCount, long usedChannels, long testCount, long batteryCount, String onlineStatus) {}
+ public record Warning(long id, long testId, String testName, String item, long occurredAt) {}
+ public record Snapshot(Summary summary, Map<String, Trend> trends, List<TestRow> tests, List<CabinetRow> cabinets, List<Warning> warnings) {}
+
+ /** 使用全部未删除测试计算总量及香港自然日/月新增数,统计不受页面分页限制。 */
+ public Snapshot read(Instant now) {
+ var allTests = tests.selectTestList(new TTest());
+ var today = now.atZone(BUSINESS_ZONE).toLocalDate();
+ Map<LocalDate, Long> dates = new HashMap<>();
+ allTests.stream().filter(test -> test.getCreateTime() != null).forEach(test ->
+ dates.merge(test.getCreateTime().toInstant().atZone(BUSINESS_ZONE).toLocalDate(), 1L, Long::sum));
+ var summary = new Summary(allTests.size(), jdbc.queryForObject("select count(*) from t_battery_group where del_flag=0", Long.class),
+ dates.getOrDefault(today, 0L), dates.entrySet().stream().filter(entry -> YearMonth.from(entry.getKey()).equals(YearMonth.from(today))).mapToLong(Map.Entry::getValue).sum());
+ var trends = new LinkedHashMap<String, Trend>();
+ trends.put("week", days(today.minusDays(6), today, dates));
+ trends.put("month", days(today.withDayOfMonth(1), today, dates));
+ List<String> months = new ArrayList<>(); List<Long> counts = new ArrayList<>();
+ for (int month = 1; month <= today.getMonthValue(); month++) {
+ final var key = YearMonth.of(today.getYear(), month); months.add(month + "月");
+ counts.add(dates.entrySet().stream().filter(entry -> YearMonth.from(entry.getKey()).equals(key)).mapToLong(Map.Entry::getValue).sum());
+ }
+ trends.put("year", new Trend(months, counts));
+ var open = testMapper.selectWarningExceptions(null, "OPEN");
+ Map<Long, Long> warningCounts = new HashMap<>();
+ List<Warning> warnings = open.stream().map(item -> {
+ warningCounts.merge(item.getTestId(), 1L, Long::sum);
+ return new Warning(item.getExceptionId(), item.getTestId(), item.getTestName(),
+ "DEVICE_OFFLINE".equals(item.getExceptionType()) ? "设备离线" : metricName(item.getMetricCode()),
+ item.getLastOccurredAtUtc() == null ? 0 : item.getLastOccurredAtUtc().getTime());
+ }).sorted(Comparator.comparingLong(Warning::occurredAt).reversed()).toList();
+ // 首页明确展示“最近记录”,不把持久化历史冒充设备当前工步;多电池分别保留记录身份。
+ Map<Long, List<String>> latest = new HashMap<>();
+ jdbc.query("select r.test_id,b.battery_no,r.step_no,r.cycle_no from t_test_realtime_record r join t_test_battery b on b.test_battery_id=r.test_battery_id and b.del_flag=0 join (select test_battery_id,max(record_id) record_id from t_test_realtime_record group by test_battery_id) last_record on last_record.record_id=r.record_id",
+ (org.springframework.jdbc.core.RowCallbackHandler) rs -> {
+ if (rs.getObject(3) != null && rs.getObject(4) != null)
+ latest.computeIfAbsent(rs.getLong(1), id -> new ArrayList<>()).add(rs.getString(2) + ":工步" + rs.getInt(3) + " / 循环" + rs.getInt(4));
+ });
+ var rows = allTests.stream().map(test -> new TestRow(test.getTestId(), test.getTestName(), test.getTestTypeName(), test.getTestStatus(),
+ test.getReceptionStatus(), String.join(";", latest.getOrDefault(test.getTestId(), List.of())), warningCounts.getOrDefault(test.getTestId(), 0L))).toList();
+ Map<Long, long[]> usage = new HashMap<>();
+ // 端口、电池和关联测试按未完成测试去重统计;在线事实仍由设备服务独立提供。
+ jdbc.query("select c.cabinet_id,count(distinct binding.channel_id),count(distinct t.test_id),count(distinct b.test_battery_id) from t_test_channel_binding binding join t_detection_channel c on c.channel_id=binding.channel_id and c.del_flag=0 join t_test t on t.test_id=binding.test_id and t.del_flag=0 and t.test_status<>'COMPLETED' join t_test_battery b on b.test_battery_id=binding.test_battery_id and b.del_flag=0 group by c.cabinet_id",
+ (org.springframework.jdbc.core.RowCallbackHandler) rs -> usage.put(rs.getLong(1), new long[] { rs.getLong(2), rs.getLong(3), rs.getLong(4) }));
+ var cabinetRows = cabinets.selectDetectionCabinetList(new TDetectionCabinet()).stream().map(cabinet -> {
+ var values = usage.getOrDefault(cabinet.getCabinetId(), new long[3]);
+ return new CabinetRow(cabinet.getCabinetId(), cabinet.getCabinetName(), cabinet.getCapacity(),
+ cabinet.getChannelCount() == null ? 0 : cabinet.getChannelCount(), values[0], values[1], values[2],
+ "0".equals(cabinet.getOnlineStatus()) ? "在线" : "离线");
+ }).toList();
+ return new Snapshot(summary, trends, rows, cabinetRows, warnings);
+ }
+
+ private Trend days(LocalDate begin, LocalDate end, Map<LocalDate, Long> dates) {
+ List<String> labels = new ArrayList<>(); List<Long> values = new ArrayList<>();
+ for (LocalDate day = begin; !day.isAfter(end); day = day.plusDays(1)) {
+ labels.add(day.getMonthValue() + "/" + day.getDayOfMonth()); values.add(dates.getOrDefault(day, 0L));
+ }
+ return new Trend(labels, values);
+ }
+
+ private String metricName(String code) {
+ if (code == null) return "阈值异常";
+ return Map.of("VOLTAGE", "电压", "CURRENT", "电流", "TEMPERATURE", "温度",
+ "POSITIVE_POTENTIAL", "正电位", "NEGATIVE_POTENTIAL", "负电位").getOrDefault(code, code);
+ }
+}
diff --git a/battery-business/src/main/java/com/battery/business/service/TodoService.java b/battery-business/src/main/java/com/battery/business/service/TodoService.java
index f215b5d..b7d36e6 100644
--- a/battery-business/src/main/java/com/battery/business/service/TodoService.java
+++ b/battery-business/src/main/java/com/battery/business/service/TodoService.java
@@ -1,25 +1,74 @@
package com.battery.business.service;
import java.sql.Timestamp;
+import java.sql.Statement;
import java.util.List;
import javax.sql.DataSource;
+import com.ruoyi.common.exception.ServiceException;
import org.springframework.jdbc.core.JdbcTemplate;
+import org.springframework.jdbc.core.RowMapper;
+import org.springframework.jdbc.support.GeneratedKeyHolder;
import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
-/** 待办真实存储;所有查询和写操作都按当前负责人身份约束。 */
+/** 独立待办持久化,负责人限制始终使用登录用户 ID,不依赖姓名或管理员身份。 */
@Service
public class TodoService {
private final JdbcTemplate jdbc;
- public TodoService(DataSource source) { this.jdbc = new JdbcTemplate(source); }
- public record Todo(long id, String content, long ownerId, String owner, String status, String creator, long createdAt, long operatedAt, String handleRemark) {}
+ public TodoService(DataSource source) { jdbc = new JdbcTemplate(source); }
+ public record Todo(long id, String content, long ownerId, String owner, String status,
+ String creator, long createdAt, long operatedAt, String handleRemark) {}
+ public record UserOption(long id, String name) {}
+ private static final String COLUMNS = "select todo_id,content,owner_id,owner_name,status,creator_name,created_at_utc,operated_at_utc,handle_remark from t_todo";
+ private static final RowMapper<Todo> ROW = (rs, index) -> new Todo(rs.getLong(1), rs.getString(2), rs.getLong(3), rs.getString(4),
+ rs.getString(5), rs.getString(6), rs.getTimestamp(7).getTime(), rs.getTimestamp(8).getTime(), rs.getString(9));
+
+ /** 返回当前负责人自己的全部待办;包括已处理项,不向创建人或管理员额外开放。 */
public List<Todo> list(long ownerId) {
- return jdbc.query("select todo_id,content,owner_id,owner_name,status,creator_name,created_at_utc,operated_at_utc,coalesce(handle_remark,'') from t_todo where owner_id=? order by operated_at_utc desc,todo_id desc", (rs, i) -> new Todo(rs.getLong(1),rs.getString(2),rs.getLong(3),rs.getString(4),rs.getString(5),rs.getString(6),rs.getTimestamp(7).getTime(),rs.getTimestamp(8).getTime(),rs.getString(9)), ownerId);
+ return jdbc.query(COLUMNS + " where owner_id=? order by operated_at_utc desc,todo_id desc", ROW, ownerId);
}
- public Todo create(String content, long ownerId, String owner, long creatorId, String creator) {
- Timestamp now = new Timestamp(System.currentTimeMillis());
- jdbc.update("insert into t_todo(content,owner_id,owner_name,creator_id,creator_name,status,created_at_utc,operated_at_utc,handle_remark) values(?,?,?,?,?,'待处理',?,?,null)", content.trim(), ownerId, owner, creatorId, creator, now, now);
- return jdbc.queryForObject("select todo_id,content,owner_id,owner_name,status,creator_name,created_at_utc,operated_at_utc,coalesce(handle_remark,'') from t_todo where todo_id=last_insert_id()", (rs,i)->new Todo(rs.getLong(1),rs.getString(2),rs.getLong(3),rs.getString(4),rs.getString(5),rs.getString(6),rs.getTimestamp(7).getTime(),rs.getTimestamp(8).getTime(),rs.getString(9)));
+
+ /** 供新增表单选择启用用户,不要求用户管理模块权限。 */
+ public List<UserOption> owners() {
+ return jdbc.query("select user_id,coalesce(nullif(nick_name,''),user_name) from sys_user where status='0' and del_flag='0' order by user_id",
+ (rs, index) -> new UserOption(rs.getLong(1), rs.getString(2)));
}
- public boolean handle(long id,long ownerId,String remark) { return jdbc.update("update t_todo set status='已处理',handle_remark=?,operated_at_utc=? where todo_id=? and owner_id=? and status='待处理'",remark.trim(),new Timestamp(System.currentTimeMillis()),id,ownerId)==1; }
- public boolean remove(long id,long ownerId) { return jdbc.update("delete from t_todo where todo_id=? and owner_id=?",id,ownerId)==1; }
+
+ /** 新增后仅返回确认主键,分配给别人时不把该待办回传给非负责人。 */
+ @Transactional
+ public long create(String content, long ownerId, long creatorId, String creator) {
+ String value = required(content, 200, "待办内容");
+ var owner = owners().stream().filter(user -> user.id() == ownerId).findFirst()
+ .orElseThrow(() -> new ServiceException("请选择启用的负责人"));
+ var now = new Timestamp(System.currentTimeMillis());
+ var key = new GeneratedKeyHolder();
+ // 主键从本次 INSERT 的 JDBC 结果取得,不跨连接使用会话相关的 last_insert_id。
+ jdbc.update(connection -> {
+ var statement = connection.prepareStatement("insert into t_todo(content,owner_id,owner_name,creator_id,creator_name,status,created_at_utc,operated_at_utc,handle_remark) values(?,?,?,?,?,'待处理',?,?,'')", Statement.RETURN_GENERATED_KEYS);
+ statement.setString(1, value); statement.setLong(2, owner.id()); statement.setString(3, owner.name());
+ statement.setLong(4, creatorId); statement.setString(5, creator); statement.setTimestamp(6, now); statement.setTimestamp(7, now);
+ return statement;
+ }, key);
+ return key.getKey().longValue();
+ }
+
+ /** 负责人只能将自己的待处理事项完成一次;写入条件同时校验身份和当前状态。 */
+ public void handle(long id, long ownerId, String remark) {
+ String value = required(remark, 500, "处理说明");
+ if (jdbc.update("update t_todo set status='已处理',handle_remark=?,operated_at_utc=? where todo_id=? and owner_id=? and status='待处理'",
+ value, new Timestamp(System.currentTimeMillis()), id, ownerId) != 1)
+ throw new ServiceException("待办不存在、已处理或无权处理");
+ }
+
+ /** 按现有删除交互物理删除自己的待办,不涉及测试或异常事实。 */
+ public void remove(long id, long ownerId) {
+ if (jdbc.update("delete from t_todo where todo_id=? and owner_id=?", id, ownerId) != 1)
+ throw new ServiceException("待办不存在或无权删除");
+ }
+
+ private String required(String text, int max, String name) {
+ if (text == null || text.trim().isEmpty() || text.trim().length() > max)
+ throw new ServiceException(name + "不能为空且长度不能超过" + max);
+ return text.trim();
+ }
}
diff --git a/ruoyi-admin/src/main/java/com/battery/business/web/DashboardController.java b/ruoyi-admin/src/main/java/com/battery/business/web/DashboardController.java
new file mode 100644
index 0000000..6d8f70d
--- /dev/null
+++ b/ruoyi-admin/src/main/java/com/battery/business/web/DashboardController.java
@@ -0,0 +1,19 @@
+package com.battery.business.web;
+
+import java.time.Instant;
+import com.battery.business.service.DashboardService;
+import com.ruoyi.common.core.controller.BaseController;
+import com.ruoyi.common.core.domain.AjaxResult;
+import org.springframework.security.access.prepost.PreAuthorize;
+import org.springframework.web.bind.annotation.*;
+
+/** 首页查询使用独立查看权限,不借用用户管理和测试编辑权限。 */
+@RestController
+@RequestMapping("/system/dashboard")
+public class DashboardController extends BaseController {
+ private final DashboardService service;
+ public DashboardController(DashboardService service) { this.service = service; }
+ /** 汇总当前测试、检测柜和开放异常;个人待办通过自己的负责人接口读取。 */
+ @GetMapping @PreAuthorize("@ss.hasAnyPermi('dashboard:workbench:list,dashboard:workbench:query')")
+ public AjaxResult get() { return success(service.read(Instant.now())); }
+}
diff --git a/ruoyi-admin/src/main/java/com/battery/business/web/TodoController.java b/ruoyi-admin/src/main/java/com/battery/business/web/TodoController.java
index 481fb79..339f6b0 100644
--- a/ruoyi-admin/src/main/java/com/battery/business/web/TodoController.java
+++ b/ruoyi-admin/src/main/java/com/battery/business/web/TodoController.java
@@ -1,26 +1,37 @@
package com.battery.business.web;
-import java.util.List;
import com.battery.business.service.TodoService;
import com.ruoyi.common.core.controller.BaseController;
import com.ruoyi.common.core.domain.AjaxResult;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.*;
-/** 待办列表接口;负责人身份由服务端从当前登录会话取得。 */
+/** 待办动作权限与负责人身份分别校验,创建身份只取当前登录会话。 */
@RestController
@RequestMapping("/system/todo")
public class TodoController extends BaseController {
private final TodoService service;
public TodoController(TodoService service) { this.service = service; }
- @GetMapping("/list") @PreAuthorize("@ss.hasPermi('todo:list:query')")
+ /** 仅查询自己的待办,不开放按其他负责人 ID 查询。 */
+ @GetMapping("/list") @PreAuthorize("@ss.hasAnyPermi('todo:list:list,todo:list:query')")
public AjaxResult list() { return success().put("rows", service.list(getUserId())); }
+ /** 返回新增待办可选的启用用户。 */
+ @GetMapping("/owners") @PreAuthorize("@ss.hasPermi('todo:list:add')")
+ public AjaxResult owners() { return success(service.owners()); }
+ /** 校验负责人后创建,返回主键确认,不泄露别人待办的详情。 */
@PostMapping @PreAuthorize("@ss.hasPermi('todo:list:add')")
- public AjaxResult create(@RequestBody CreateRequest request) { if (request == null || request.content() == null || request.content().trim().isEmpty() || request.ownerId() == null || request.ownerName() == null) return error("待办内容和负责人不能为空"); return success(service.create(request.content(), request.ownerId(), request.ownerName(), getUserId(), getUsername())); }
+ public AjaxResult create(@RequestBody CreateRequest request) {
+ if (request == null || request.ownerId() == null) return error("负责人不能为空");
+ return success(service.create(request.content(), request.ownerId(), getUserId(), getUsername()));
+ }
+ /** 仅负责人可完成自己的待办,已处理不能再次修改。 */
@PutMapping("/{id}/handle") @PreAuthorize("@ss.hasPermi('todo:list:handle')")
- public AjaxResult handle(@PathVariable long id,@RequestBody HandleRequest request) { if (request == null || request.remark() == null || request.remark().trim().isEmpty()) return error("处理说明不能为空"); return service.handle(id,getUserId(),request.remark()) ? success() : error("待办不存在、已处理或无权处理"); }
+ public AjaxResult handle(@PathVariable long id, @RequestBody HandleRequest request) {
+ service.handle(id, getUserId(), request == null ? null : request.remark()); return success();
+ }
+ /** 删除只作用于当前负责人自己的记录。 */
@DeleteMapping("/{id}") @PreAuthorize("@ss.hasPermi('todo:list:remove')")
- public AjaxResult remove(@PathVariable long id) { return service.remove(id,getUserId()) ? success() : error("待办不存在或无权删除"); }
- public record CreateRequest(String content, Long ownerId, String ownerName) {}
+ public AjaxResult remove(@PathVariable long id) { service.remove(id, getUserId()); return success(); }
+ public record CreateRequest(String content, Long ownerId) {}
public record HandleRequest(String remark) {}
}
diff --git a/ruoyi-admin/src/main/java/com/ruoyi/web/controller/monitor/SysOperlogController.java b/ruoyi-admin/src/main/java/com/ruoyi/web/controller/monitor/SysOperlogController.java
index f056d65..0db0745 100644
--- a/ruoyi-admin/src/main/java/com/ruoyi/web/controller/monitor/SysOperlogController.java
+++ b/ruoyi-admin/src/main/java/com/ruoyi/web/controller/monitor/SysOperlogController.java
@@ -31,7 +31,7 @@
@Autowired
private ISysOperLogService operLogService;
- @PreAuthorize("@ss.hasPermi('monitor:operlog:list')")
+ @PreAuthorize("@ss.hasAnyPermi('monitor:operlog:list,monitor:operlog:query')")
@GetMapping("/list")
public TableDataInfo list(SysOperLog operLog)
{
diff --git a/ruoyi-admin/src/main/resources/db/migration-final/V21__dashboard_and_todo_permissions.sql b/ruoyi-admin/src/main/resources/db/migration-final/V21__dashboard_and_todo_permissions.sql
new file mode 100644
index 0000000..bba0073
--- /dev/null
+++ b/ruoyi-admin/src/main/resources/db/migration-final/V21__dashboard_and_todo_permissions.sql
@@ -0,0 +1,35 @@
+-- 最终库补齐首页及待办权限;只登记权限,不为任何角色自动增加授权。
+INSERT INTO sys_menu(menu_name,parent_id,order_num,path,menu_type,visible,status,perms,create_by,create_time)
+SELECT '首页',0,0,'dashboard','M','0','0','','system',UTC_TIMESTAMP(3)
+WHERE NOT EXISTS (SELECT 1 FROM sys_menu WHERE path='dashboard' AND menu_type='M');
+SET @dashboard_module = (SELECT menu_id FROM sys_menu WHERE path='dashboard' AND menu_type='M' LIMIT 1);
+INSERT INTO sys_menu(menu_name,parent_id,order_num,path,menu_type,visible,status,perms,create_by,create_time)
+SELECT '工作台',@dashboard_module,1,'workbench','C','0','0','dashboard:workbench:list','system',UTC_TIMESTAMP(3)
+WHERE NOT EXISTS (SELECT 1 FROM sys_menu WHERE perms='dashboard:workbench:list');
+SET @dashboard_page = (SELECT menu_id FROM sys_menu WHERE perms='dashboard:workbench:list' LIMIT 1);
+INSERT INTO sys_menu(menu_name,parent_id,order_num,path,menu_type,visible,status,perms,create_by,create_time)
+SELECT '查看',@dashboard_page,1,'#','F','0','0','dashboard:workbench:query','system',UTC_TIMESTAMP(3)
+WHERE NOT EXISTS (SELECT 1 FROM sys_menu WHERE perms='dashboard:workbench:query');
+INSERT INTO sys_menu(menu_name,parent_id,order_num,path,menu_type,visible,status,perms,create_by,create_time)
+SELECT '待办管理',0,5,'todo','M','0','0','','system',UTC_TIMESTAMP(3)
+WHERE NOT EXISTS (SELECT 1 FROM sys_menu WHERE path='todo' AND menu_type='M');
+SET @todo_module = (SELECT menu_id FROM sys_menu WHERE path='todo' AND menu_type='M' LIMIT 1);
+INSERT INTO sys_menu(menu_name,parent_id,order_num,path,menu_type,visible,status,perms,create_by,create_time)
+SELECT '待办列表',@todo_module,1,'todo-list','C','0','0','todo:list:list','system',UTC_TIMESTAMP(3)
+WHERE NOT EXISTS (SELECT 1 FROM sys_menu WHERE perms='todo:list:list');
+SET @todo_menu = (SELECT menu_id FROM sys_menu WHERE perms='todo:list:list' LIMIT 1);
+INSERT INTO sys_menu(menu_name,parent_id,order_num,path,menu_type,visible,status,perms,create_by,create_time)
+SELECT '查看',@todo_menu,1,'#','F','0','0','todo:list:query','system',UTC_TIMESTAMP(3)
+WHERE NOT EXISTS (SELECT 1 FROM sys_menu WHERE perms='todo:list:query');
+INSERT INTO sys_menu(menu_name,parent_id,order_num,path,menu_type,visible,status,perms,create_by,create_time)
+SELECT '新增待办',@todo_menu,1,'#','F','0','0','todo:list:add','system',UTC_TIMESTAMP(3)
+WHERE NOT EXISTS (SELECT 1 FROM sys_menu WHERE perms='todo:list:add');
+INSERT INTO sys_menu(menu_name,parent_id,order_num,path,menu_type,visible,status,perms,create_by,create_time)
+SELECT '处理待办',@todo_menu,2,'#','F','0','0','todo:list:handle','system',UTC_TIMESTAMP(3)
+WHERE NOT EXISTS (SELECT 1 FROM sys_menu WHERE perms='todo:list:handle');
+INSERT INTO sys_menu(menu_name,parent_id,order_num,path,menu_type,visible,status,perms,create_by,create_time)
+SELECT '删除待办',@todo_menu,3,'#','F','0','0','todo:list:remove','system',UTC_TIMESTAMP(3)
+WHERE NOT EXISTS (SELECT 1 FROM sys_menu WHERE perms='todo:list:remove');
+
+-- 权限表仅展示三级结构,将既有操作日志放到系统管理页面层,不新增日志修改能力。
+UPDATE sys_menu SET parent_id=1 WHERE menu_id=500 AND parent_id=108 AND perms='monitor:operlog:list';
diff --git a/ruoyi-admin/src/test/java/com/battery/business/report/CompareMergedSourcesTest.java b/ruoyi-admin/src/test/java/com/battery/business/report/CompareMergedSourcesTest.java
new file mode 100644
index 0000000..c6b5faf
--- /dev/null
+++ b/ruoyi-admin/src/test/java/com/battery/business/report/CompareMergedSourcesTest.java
@@ -0,0 +1,33 @@
+package com.battery.business.report;
+
+import static org.junit.jupiter.api.Assertions.*;
+import static org.mockito.Mockito.*;
+import java.util.*;
+import com.battery.business.domain.*;
+import com.battery.business.service.*;
+import org.junit.jupiter.api.Test;
+
+class CompareMergedSourcesTest {
+ @Test
+ void selectingSourcesUsesSavedMergeAndPreviewIncludesItsActualSteps() {
+ var tests = mock(ITTestService.class); var data = mock(ITestDataQueryService.class);
+ var battery = new TTestBattery(); battery.setTestBatteryId(5L); battery.setGroupId(9L); battery.setBatteryNo("1号"); battery.setFields(List.of());
+ var test = new TTest(); test.setTestId(1L); test.setTestStatus("COMPLETED"); test.setBatteries(List.of(battery)); test.setLogicStepMappings(List.of());
+ when(tests.selectTestList(any())).thenReturn(List.of(test)); when(tests.selectTestById(1L)).thenReturn(test);
+ var records = List.of(Map.<String,Object>of("batteryNo", "1号", "cycleNo", 3, "stepNo", 10, "deviceTime", 1000L),
+ Map.<String,Object>of("batteryNo", "1号", "cycleNo", 3, "stepNo", 11, "deviceTime", 2000L));
+ when(data.records(1L)).thenReturn(records);
+ var service = new CompareReportService(tests, data, null);
+ var actual = service.sources(); assertEquals(2, actual.size());
+ var mapping = new TTestLogicStepMapping(); mapping.setLogicalNo(1); mapping.setLogicalName("合并充电"); mapping.setStartStepNo(1); mapping.setEndStepNo(2);
+ test.setLogicStepMappings(List.of(mapping));
+ var merged = service.sources(); assertEquals(1, merged.size());
+ assertEquals(1, merged.get(0).selection().logicalNo());
+ assertNull(merged.get(0).selection().stepNo());
+ assertTrue(merged.get(0).process().contains("合并充电"));
+ var preview = service.preview(List.of(merged.get(0).selection()));
+ assertEquals(records, preview.get(0).records());
+ assertEquals(1000, preview.get(0).zeroTime());
+ assertEquals(1, service.preview(List.of(actual.get(0).selection())).get(0).records().size(), "旧版来源仍可明确刷新原实际工步");
+ }
+}
diff --git a/ruoyi-admin/src/test/java/com/battery/business/report/ReportTestDetailPersistenceTest.java b/ruoyi-admin/src/test/java/com/battery/business/report/ReportTestDetailPersistenceTest.java
index 28fc6fe..5db5d8a 100644
--- a/ruoyi-admin/src/test/java/com/battery/business/report/ReportTestDetailPersistenceTest.java
+++ b/ruoyi-admin/src/test/java/com/battery/business/report/ReportTestDetailPersistenceTest.java
@@ -50,7 +50,7 @@
}
var compare = new CompareReportService(tests, data, new CompareReportRepository(database));
var formation = new FormationReportService(tests, data, new FormationReportRepository(database), definitions, value -> StepDirectionResolver.Direction.UNKNOWN);
- var splice = new SpliceReportService(tests, data, new SpliceReportRepository(database), new SpliceFieldTemplateService(database, definitions));
+ var splice = new SpliceReportService(tests, data, new SpliceReportRepository(database), new SpliceFieldTemplateService(database, definitions), mock(ITBatteryGroupService.class));
var settings = JSON.parseObject(switch (type) {
case "compare" -> "{\"metrics\":[],\"seriesColors\":[],\"batteryTemplateFields\":[],\"specialFields\":[],\"fieldStructure\":[],\"detailFieldValues\":{}}";
case "formation" -> "{\"fields\":[{\"key\":\"chargeCapacity\",\"scope\":\"cycle\",\"kind\":\"metric\",\"displayName\":\"充电容量\",\"unit\":\"mAh\"}],\"values\":{},\"chargeStepSelections\":{},\"metrics\":[],\"seriesColors\":{}}";
diff --git a/ruoyi-admin/src/test/java/com/battery/business/report/SpliceProjectCyclesTest.java b/ruoyi-admin/src/test/java/com/battery/business/report/SpliceProjectCyclesTest.java
new file mode 100644
index 0000000..e1de86d
--- /dev/null
+++ b/ruoyi-admin/src/test/java/com/battery/business/report/SpliceProjectCyclesTest.java
@@ -0,0 +1,46 @@
+package com.battery.business.report;
+
+import static org.junit.jupiter.api.Assertions.*;
+import static org.mockito.Mockito.*;
+import java.math.BigDecimal;
+import java.util.*;
+import com.alibaba.fastjson2.JSON;
+import com.battery.business.domain.*;
+import com.battery.business.service.*;
+import org.junit.jupiter.api.Test;
+
+class SpliceProjectCyclesTest {
+ @Test
+ void actualCyclesBecomeConsecutivePositionsWithFrozenProjectStatistics() {
+ var tests = mock(ITTestService.class);
+ var data = mock(ITestDataQueryService.class);
+ var groups = mock(ITBatteryGroupService.class);
+ var group = new TBatteryGroup(); group.setF("2");
+ when(groups.selectBatteryGroupById(9L)).thenReturn(group);
+ for (long id : List.of(1L, 2L)) {
+ var battery = new TTestBattery(); battery.setTestBatteryId(id); battery.setGroupId(9L);
+ battery.setBatteryNo("电池1"); battery.setFields(List.of());
+ var test = new TTest(); test.setTestId(id); test.setTestStatus("COMPLETED"); test.setBatteries(List.of(battery));
+ when(tests.selectTestById(id)).thenReturn(test);
+ var nos = id == 1 ? List.of(1, 3) : List.of(10, 11);
+ when(data.cycles(id)).thenReturn(nos.stream().map(no -> Map.<String,Object>of("batteryNo", "电池1", "cycleNo", no, "dischargeCapacity", new BigDecimal("0.004"))).toList());
+ when(data.steps(id)).thenReturn(nos.stream().flatMap(no -> List.of(
+ Map.<String,Object>of("batteryNo", "电池1", "cycleNo", no, "stepNo", 8),
+ Map.<String,Object>of("batteryNo", "电池1", "cycleNo", no, "stepNo", 1, "endVoltage", 4)).stream()).toList());
+ }
+ var service = new SpliceReportService(tests, data, null, null, groups);
+ var result = service.preview(List.of(new SpliceReportRequest.Selection(1L, 1L), new SpliceReportRequest.Selection(2L, 2L)));
+ assertEquals(List.of(1, 2, 3, 4), result.cycleNos());
+ assertEquals(Map.of("1", 1, "3", 2), result.segments().get(0).get("cyclePositions"));
+ assertEquals(Map.of("10", 3, "11", 4), result.segments().get(1).get("cyclePositions"));
+ assertEquals(List.of(10, 11), result.segments().get(1).get("originalCycleNos"));
+ var stat = JSON.parseObject(JSON.toJSONString(result.segments().get(0).get("statistics"))).getJSONObject("1");
+ assertEquals(0, new BigDecimal("2").compareTo(stat.getBigDecimal("dischargeCapacityF")));
+ assertNull(stat.get("finalVoltage1"), "末工步缺失不能回退到前一工步电压");
+ assertNull(stat.get("finalVoltage2"), "未定义公式不能补值");
+ verify(data, never()).records(anyLong());
+ group.setF("0");
+ var unavailable = service.preview(List.of(new SpliceReportRequest.Selection(1L, 1L), new SpliceReportRequest.Selection(2L, 2L)));
+ assertNull(JSON.parseObject(JSON.toJSONString(unavailable.segments().get(0).get("statistics"))).getJSONObject("1").get("dischargeCapacityF"));
+ }
+}
diff --git a/ruoyi-admin/src/test/java/com/battery/business/service/DashboardServiceTest.java b/ruoyi-admin/src/test/java/com/battery/business/service/DashboardServiceTest.java
new file mode 100644
index 0000000..20c9332
--- /dev/null
+++ b/ruoyi-admin/src/test/java/com/battery/business/service/DashboardServiceTest.java
@@ -0,0 +1,57 @@
+package com.battery.business.service;
+
+import static org.junit.jupiter.api.Assertions.*;
+import static org.mockito.Mockito.*;
+import java.time.Instant;
+import java.util.*;
+import com.battery.business.domain.*;
+import com.battery.business.mapper.TTestMapper;
+import org.h2.jdbcx.JdbcDataSource;
+import org.junit.jupiter.api.Test;
+import org.springframework.jdbc.core.JdbcTemplate;
+
+class DashboardServiceTest {
+ @Test
+ void aggregatesAllTestsAtHongKongMidnightAndKeepsDeviceFactsSeparate() {
+ var database = new JdbcDataSource(); database.setURL("jdbc:h2:mem:dashboard_" + UUID.randomUUID() + ";MODE=MySQL;DB_CLOSE_DELAY=-1");
+ var jdbc = new JdbcTemplate(database);
+ jdbc.execute("create table t_battery_group(group_id bigint, del_flag int)");
+ jdbc.execute("insert into t_battery_group values(1,0),(2,1)");
+ jdbc.execute("create table t_test_realtime_record(record_id bigint,test_id bigint,test_battery_id bigint,step_no int,cycle_no int)");
+ jdbc.execute("create table t_test_battery(test_battery_id bigint,battery_no varchar(20),del_flag int)");
+ jdbc.execute("create table t_test_channel_binding(channel_id bigint,test_id bigint,test_battery_id bigint)");
+ jdbc.execute("create table t_detection_channel(channel_id bigint,cabinet_id bigint,del_flag int)");
+ jdbc.execute("create table t_test(test_id bigint,test_status varchar(20),del_flag int)");
+ jdbc.execute("insert into t_test values(1,'RUNNING',0),(2,'COMPLETED',0)");
+ jdbc.execute("insert into t_test_battery values(1,'电池1',0),(2,'电池2',0)");
+ jdbc.execute("insert into t_detection_channel values(1,7,0)");
+ jdbc.execute("insert into t_test_channel_binding values(1,1,1),(1,2,2)");
+ var tests = mock(ITTestService.class);
+ var cabinets = mock(ITDetectionCabinetService.class);
+ var mapper = mock(TTestMapper.class);
+ var rows = new ArrayList<TTest>();
+ for (int index = 0; index < 1001; index++) {
+ var test = new TTest(); test.setTestId((long) index + 1); test.setTestStatus("RUNNING"); test.setReceptionStatus("PAUSED");
+ test.setCreateTime(Date.from(Instant.parse(index == 0 ? "2026-08-31T15:59:59Z" : "2026-08-31T16:00:00Z"))); rows.add(test);
+ }
+ when(tests.selectTestList(any())).thenReturn(rows);
+ var cabinet = new TDetectionCabinet(); cabinet.setCabinetId(7L); cabinet.setChannelCount(8); cabinet.setOnlineStatus("0");
+ when(cabinets.selectDetectionCabinetList(any())).thenReturn(List.of(cabinet));
+ var warning = new TTestWarningException(); warning.setExceptionId(3L); warning.setTestId(1L); warning.setExceptionType("DEVICE_OFFLINE");
+ when(mapper.selectWarningExceptions(null, "OPEN")).thenReturn(List.of(warning));
+ var result = new DashboardService(tests, cabinets, mapper, database).read(Instant.parse("2026-09-01T00:00:00Z"));
+ assertEquals(1001, result.summary().testCount());
+ assertEquals(1, result.summary().batteryGroupCount());
+ assertEquals(1000, result.summary().todayCount());
+ assertEquals(1000, result.summary().monthCount());
+ assertEquals(List.of(1000L), result.trends().get("month").values());
+ assertEquals(1, result.trends().get("year").values().get(7));
+ assertEquals(1000, result.trends().get("year").values().get(8));
+ assertEquals("在线", result.cabinets().get(0).onlineStatus());
+ assertEquals(1, result.cabinets().get(0).usedChannels());
+ assertEquals(1, result.cabinets().get(0).testCount());
+ assertEquals("RUNNING", result.tests().get(0).status());
+ assertEquals("PAUSED", result.tests().get(0).receptionStatus());
+ assertEquals("设备离线", result.warnings().get(0).item());
+ }
+}
diff --git a/ruoyi-admin/src/test/java/com/battery/business/service/TodoServiceTest.java b/ruoyi-admin/src/test/java/com/battery/business/service/TodoServiceTest.java
new file mode 100644
index 0000000..594f717
--- /dev/null
+++ b/ruoyi-admin/src/test/java/com/battery/business/service/TodoServiceTest.java
@@ -0,0 +1,39 @@
+package com.battery.business.service;
+
+import static org.junit.jupiter.api.Assertions.*;
+import java.nio.file.*;
+import java.util.UUID;
+import com.ruoyi.common.exception.ServiceException;
+import org.h2.jdbcx.JdbcDataSource;
+import org.junit.jupiter.api.Test;
+import org.springframework.jdbc.core.JdbcTemplate;
+
+class TodoServiceTest {
+ @Test
+ void onlyOwnerCanReadHandleAndRemoveEvenWhenNamesMatch() throws Exception {
+ var database = new JdbcDataSource();
+ database.setURL("jdbc:h2:mem:todo_" + UUID.randomUUID() + ";MODE=MySQL;DB_CLOSE_DELAY=-1");
+ var jdbc = new JdbcTemplate(database);
+ jdbc.execute("CREATE TABLE sys_user(user_id BIGINT, user_name VARCHAR(100), nick_name VARCHAR(100), status CHAR(1), del_flag CHAR(1))");
+ jdbc.update("INSERT INTO sys_user VALUES(1,'creator','同名','0','0'),(2,'owner','同名','0','0'),(3,'disabled','停用','1','0')");
+ var ddl = Files.readString(Path.of("src/main/resources/db/migration-final/V20__todo_records.sql"));
+ jdbc.execute(ddl.substring(0, ddl.indexOf("ENGINE=InnoDB")));
+ var service = new TodoService(database);
+ long id = service.create(" 工作事项 ", 2, 1, "创建人");
+ assertTrue(service.list(1).isEmpty());
+ assertEquals("工作事项", service.list(2).get(0).content());
+ assertEquals(id, service.list(2).get(0).id());
+ assertEquals(2, service.owners().size());
+ assertThrows(ServiceException.class, () -> service.create("事项", 3, 1, "创建人"));
+ assertThrows(ServiceException.class, () -> service.handle(id, 1, "管理员或创建人不可越权"));
+ assertThrows(ServiceException.class, () -> service.remove(id, 1));
+ assertThrows(ServiceException.class, () -> service.handle(id, 2, " "));
+ service.handle(id, 2, "已完成");
+ var reopened = new TodoService(database).list(2).get(0);
+ assertEquals("已处理", reopened.status());
+ assertEquals("已完成", reopened.handleRemark());
+ assertThrows(ServiceException.class, () -> service.handle(id, 2, "再次处理"));
+ service.remove(id, 2);
+ assertTrue(service.list(2).isEmpty());
+ }
+}
--
Gitblit v1.7.1