mirror of
https://gitee.com/zhijiantianya/yudao-cloud.git
synced 2026-10-06 07:22:58 +08:00
feat:同步 hrm、fms 最新进度
This commit is contained in:
Binary file not shown.
|
After Width: | Height: | Size: 366 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 403 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 484 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 77 KiB |
@@ -0,0 +1,32 @@
|
||||
# LiveKit Server PoC
|
||||
|
||||
最小可用的 LiveKit Server 自部署验证环境,用于零期 PoC。
|
||||
|
||||
## 启动
|
||||
|
||||
```bash
|
||||
cd tools/livekit-poc
|
||||
docker compose up -d
|
||||
bash verify.sh
|
||||
```
|
||||
|
||||
## 端口
|
||||
|
||||
- 7880:HTTP / WebSocket 信令;
|
||||
- 7881:WebRTC TCP fallback;
|
||||
- 7882/UDP:WebRTC 媒体;
|
||||
- macOS / Windows:当前 `docker-compose.yml` 走端口映射模式,`rtc.node_ip` 使用 `127.0.0.1` 让浏览器访问映射后的媒体端口,webhook URL 用 `host.docker.internal:48080` 让容器访问到宿主机 yudao 后端;
|
||||
- macOS 上 host network(`network_mode: host`)需要 Docker Desktop 4.34+ 并在 Settings → Resources → Network 勾选「Enable host networking」,老版本静默失败(容器跑得起来但端口完全不通);
|
||||
- Linux:可以把 `docker-compose.yml` 改成 `network_mode: host` + 删 `ports:` 段,并把 `livekit.yaml` 的 webhook URL 改为 `http://127.0.0.1:48080/admin-api/im/livekit/webhook`。
|
||||
|
||||
## 凭据 (仅 PoC,勿用于生产)
|
||||
|
||||
- `LIVEKIT_KEYS=devkey: secret-poc-key-min-32-chars-required-here`
|
||||
- API Key:`devkey`
|
||||
- API Secret:`secret-poc-key-min-32-chars-required-here`
|
||||
|
||||
生产环境必须改用强随机 secret,并通过 `--config /etc/livekit.yaml` 加载。
|
||||
|
||||
## 浏览器联调
|
||||
|
||||
`verify.sh` 跑完会输出一个 `meet.livekit.io` 链接,用两个浏览器(或两台机器)打开同一链接即可看到对方画面。
|
||||
@@ -0,0 +1,20 @@
|
||||
services:
|
||||
livekit:
|
||||
image: docker.m.daocloud.io/livekit/livekit-server:latest
|
||||
container_name: yudao-livekit-dev
|
||||
restart: unless-stopped
|
||||
# 端口映射模式
|
||||
# macOS / Windows 必走这种方式:Docker Desktop 4.34 以下没有 host network
|
||||
# Linux 可以改 network_mode: host 省去映射,并把 livekit.yaml 的 webhook url 换成 127.0.0.1
|
||||
ports:
|
||||
- "7880:7880" # HTTP / WebSocket 信令
|
||||
- "7881:7881" # WebRTC TCP fallback
|
||||
- "7882:7882/udp" # WebRTC UDP (dev 模式 UDP mux 单端口)
|
||||
volumes:
|
||||
# 挂载 config 文件;webhook 配置在 livekit.yaml 里
|
||||
- ./livekit.yaml:/etc/livekit.yaml:ro
|
||||
command:
|
||||
- --config
|
||||
- /etc/livekit.yaml
|
||||
- --bind
|
||||
- 0.0.0.0
|
||||
Executable
+105
@@ -0,0 +1,105 @@
|
||||
#!/usr/bin/env bash
|
||||
# LiveKit Server PoC 验证脚本;
|
||||
# 用法: bash verify.sh
|
||||
set -e
|
||||
|
||||
API_KEY="${LIVEKIT_API_KEY:-devkey}"
|
||||
API_SECRET="${LIVEKIT_API_SECRET:-secret-poc-key-min-32-chars-required-here}"
|
||||
HOST="${LIVEKIT_HOST:-localhost:7880}"
|
||||
ROOM="${LIVEKIT_ROOM:-poc-room}"
|
||||
|
||||
ok() { printf "[OK] %s\n" "$1"; }
|
||||
fail() { printf "[FAIL] %s\n" "$1"; exit 1; }
|
||||
|
||||
echo "==> 1/5 等待 HTTP 端点就绪 (http://${HOST}/)"
|
||||
for i in $(seq 1 20); do
|
||||
code=$(curl -s -o /dev/null -w "%{http_code}" "http://${HOST}/" || echo "000")
|
||||
[ "$code" = "200" ] && { ok "HTTP 200"; break; }
|
||||
[ $i -eq 20 ] && fail "20 秒内未就绪 (last code=${code})"
|
||||
sleep 1
|
||||
done
|
||||
|
||||
echo "==> 2/5 签发管理 + 客户端权限 Token"
|
||||
TOKEN=$(API_KEY="$API_KEY" API_SECRET="$API_SECRET" ROOM="$ROOM" python3 - <<'PY'
|
||||
import json, time, hmac, hashlib, base64, os
|
||||
def b64u(b): return base64.urlsafe_b64encode(b).rstrip(b'=').decode()
|
||||
header = b64u(json.dumps({"alg":"HS256","typ":"JWT"}, separators=(',',':')).encode())
|
||||
payload = b64u(json.dumps({
|
||||
"iss": os.environ["API_KEY"],
|
||||
"sub": "poc-tester",
|
||||
"name": "PoC Tester",
|
||||
"video": {
|
||||
"roomJoin": True, "room": os.environ["ROOM"],
|
||||
"canPublish": True, "canSubscribe": True, "canPublishData": True,
|
||||
"roomCreate": True, "roomList": True, "roomAdmin": True
|
||||
},
|
||||
"exp": int(time.time()) + 3600,
|
||||
"nbf": int(time.time())
|
||||
}, separators=(',',':')).encode())
|
||||
sig = b64u(hmac.new(os.environ["API_SECRET"].encode(),
|
||||
f"{header}.{payload}".encode(),
|
||||
hashlib.sha256).digest())
|
||||
print(f"{header}.{payload}.{sig}")
|
||||
PY
|
||||
)
|
||||
[ -n "$TOKEN" ] || fail "Token 生成失败"
|
||||
ok "Token 已生成 (${#TOKEN} chars)"
|
||||
|
||||
echo "==> 3/5 创建房间 ${ROOM} (CreateRoom RPC)"
|
||||
create_resp=$(curl -s -X POST "http://${HOST}/twirp/livekit.RoomService/CreateRoom" \
|
||||
-H "Authorization: Bearer ${TOKEN}" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d "{\"name\":\"${ROOM}\",\"empty_timeout\":300,\"max_participants\":10}")
|
||||
echo " 响应: $create_resp"
|
||||
echo "$create_resp" | jq -e '.sid' >/dev/null 2>&1 \
|
||||
&& ok "房间已创建" \
|
||||
|| fail "CreateRoom 失败"
|
||||
|
||||
echo "==> 4/5 列出房间 (ListRooms RPC)"
|
||||
list_resp=$(curl -s -X POST "http://${HOST}/twirp/livekit.RoomService/ListRooms" \
|
||||
-H "Authorization: Bearer ${TOKEN}" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{}')
|
||||
room_count=$(echo "$list_resp" | jq '.rooms | length' 2>/dev/null || echo "0")
|
||||
ok "当前房间数: ${room_count}"
|
||||
echo "$list_resp" | jq '.'
|
||||
|
||||
echo "==> 5/5 删除房间 (DeleteRoom RPC) —— 清理"
|
||||
del_resp=$(curl -s -X POST "http://${HOST}/twirp/livekit.RoomService/DeleteRoom" \
|
||||
-H "Authorization: Bearer ${TOKEN}" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d "{\"room\":\"${ROOM}\"}")
|
||||
ok "删除响应: $del_resp"
|
||||
|
||||
# 重新签一个仅 client 权限的 token;用于浏览器进会
|
||||
CLIENT_TOKEN=$(API_KEY="$API_KEY" API_SECRET="$API_SECRET" ROOM="$ROOM" python3 - <<'PY'
|
||||
import json, time, hmac, hashlib, base64, os
|
||||
def b64u(b): return base64.urlsafe_b64encode(b).rstrip(b'=').decode()
|
||||
header = b64u(json.dumps({"alg":"HS256","typ":"JWT"}, separators=(',',':')).encode())
|
||||
payload = b64u(json.dumps({
|
||||
"iss": os.environ["API_KEY"],
|
||||
"sub": "browser-tester",
|
||||
"name": "Browser",
|
||||
"video": {
|
||||
"roomJoin": True, "room": os.environ["ROOM"],
|
||||
"canPublish": True, "canSubscribe": True, "canPublishData": True
|
||||
},
|
||||
"exp": int(time.time()) + 7200
|
||||
}, separators=(',',':')).encode())
|
||||
sig = b64u(hmac.new(os.environ["API_SECRET"].encode(),
|
||||
f"{header}.{payload}".encode(),
|
||||
hashlib.sha256).digest())
|
||||
print(f"{header}.{payload}.{sig}")
|
||||
PY
|
||||
)
|
||||
|
||||
echo ""
|
||||
echo "============================================================"
|
||||
echo " LiveKit Server 验证通过"
|
||||
echo "============================================================"
|
||||
echo " 浏览器测试 (开两个窗口能互通):"
|
||||
echo " https://meet.livekit.io/?liveKitUrl=ws%3A%2F%2F${HOST}&token=${CLIENT_TOKEN}"
|
||||
echo ""
|
||||
echo " 停止服务:"
|
||||
echo " docker compose -f tools/livekit-poc/docker-compose.yml down"
|
||||
echo "============================================================"
|
||||
+5273
-3637
File diff suppressed because it is too large
Load Diff
+1
-3
@@ -46,9 +46,7 @@ public class CollectionUtils {
|
||||
if (CollUtil.isEmpty(from)) {
|
||||
return BigDecimal.ZERO;
|
||||
}
|
||||
return from.stream().map(valueFunc)
|
||||
.map(value -> value == null ? BigDecimal.ZERO : value)
|
||||
.reduce(BigDecimal.ZERO, BigDecimal::add);
|
||||
return from.stream().map(valueFunc).map(NumberUtils::zeroIfNull).reduce(BigDecimal.ZERO, BigDecimal::add);
|
||||
}
|
||||
|
||||
public static <T> long count(Collection<T> from, Predicate<T> predicate) {
|
||||
|
||||
+34
@@ -6,7 +6,9 @@ import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.TreeSet;
|
||||
import java.util.function.BiFunction;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
@@ -26,6 +28,38 @@ public class CollectionUtilsTest {
|
||||
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSum() {
|
||||
assertEquals(6L, CollectionUtils.sum(Arrays.asList(1L, 2L, 3L), Long::longValue));
|
||||
assertEquals(0L, CollectionUtils.sum(Collections.<Long>emptyList(), Long::longValue));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testCount() {
|
||||
assertEquals(2L, CollectionUtils.count(Arrays.asList(1, 2, 3, 4), value -> value % 2 == 0));
|
||||
assertEquals(0L, CollectionUtils.count(Collections.emptyList(), value -> true));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testDistinctCount() {
|
||||
assertEquals(3L, CollectionUtils.distinctCount(Arrays.asList("a", "b", "a", "c"), value -> value));
|
||||
assertEquals(0L, CollectionUtils.distinctCount(Collections.emptyList(), value -> value));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testConvertSet_withSupplier() {
|
||||
// 调用,并断言:按指定 Set 类型完成转换、去重和排序
|
||||
TreeSet<Integer> result = CollectionUtils.convertSetBySupplier(
|
||||
Arrays.asList("3", "1", null, "3", "2"),
|
||||
value -> value == null ? null : Integer.valueOf(value), TreeSet::new);
|
||||
assertEquals(Arrays.asList(1, 2, 3), Arrays.asList(result.toArray(new Integer[0])));
|
||||
|
||||
// 调用,并断言:空集合仍返回指定 Set 类型
|
||||
result = CollectionUtils.convertSetBySupplier(Collections.<String>emptyList(),
|
||||
Integer::valueOf, TreeSet::new);
|
||||
assertEquals(TreeSet.class, result.getClass());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testDiffList() {
|
||||
// 准备参数
|
||||
|
||||
+342
@@ -0,0 +1,342 @@
|
||||
package cn.iocoder.yudao.framework.common.util.date;
|
||||
|
||||
import cn.hutool.core.collection.CollUtil;
|
||||
import cn.iocoder.yudao.framework.common.util.date.LocalDateTimeUtils.TimeRange;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.time.LocalDate;
|
||||
import java.time.LocalDateTime;
|
||||
import java.time.LocalTime;
|
||||
import java.time.YearMonth;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||
import static org.junit.jupiter.api.Assertions.assertNotNull;
|
||||
import static org.junit.jupiter.api.Assertions.assertNull;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
|
||||
/**
|
||||
* {@link LocalDateTimeUtils} 的单元测试
|
||||
*
|
||||
* @author 芋道源码
|
||||
*/
|
||||
public class LocalDateTimeUtilsTest {
|
||||
|
||||
@Test
|
||||
public void testDateCompare() {
|
||||
// 准备参数
|
||||
LocalDate firstDate = LocalDate.of(2026, 7, 23);
|
||||
LocalDate secondDate = LocalDate.of(2026, 7, 24);
|
||||
|
||||
// 调用,并断言
|
||||
assertTrue(LocalDateTimeUtils.isBeforeOrEqual(firstDate, secondDate));
|
||||
assertTrue(LocalDateTimeUtils.isBeforeOrEqual(secondDate, secondDate));
|
||||
assertFalse(LocalDateTimeUtils.isBeforeOrEqual(secondDate, firstDate));
|
||||
assertTrue(LocalDateTimeUtils.isAfterOrEqual(secondDate, firstDate));
|
||||
assertTrue(LocalDateTimeUtils.isAfterOrEqual(secondDate, secondDate));
|
||||
assertFalse(LocalDateTimeUtils.isAfterOrEqual(firstDate, secondDate));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testDateTimeCompare() {
|
||||
// 准备参数
|
||||
LocalDateTime firstTime = LocalDateTime.of(2026, 7, 24, 9, 0);
|
||||
LocalDateTime secondTime = LocalDateTime.of(2026, 7, 24, 18, 0);
|
||||
|
||||
// 调用,并断言
|
||||
assertTrue(LocalDateTimeUtils.isBeforeOrEqual(firstTime, secondTime));
|
||||
assertTrue(LocalDateTimeUtils.isBeforeOrEqual(secondTime, secondTime));
|
||||
assertFalse(LocalDateTimeUtils.isBeforeOrEqual(secondTime, firstTime));
|
||||
assertTrue(LocalDateTimeUtils.isAfterOrEqual(secondTime, firstTime));
|
||||
assertTrue(LocalDateTimeUtils.isAfterOrEqual(secondTime, secondTime));
|
||||
assertFalse(LocalDateTimeUtils.isAfterOrEqual(firstTime, secondTime));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testIsTimeRangeValid() {
|
||||
// 准备参数
|
||||
LocalDateTime beginTime = LocalDateTime.of(2026, 7, 24, 9, 0);
|
||||
LocalDateTime endTime = LocalDateTime.of(2026, 7, 24, 18, 0);
|
||||
|
||||
// 调用,并断言
|
||||
assertTrue(LocalDateTimeUtils.isTimeRangeValid(beginTime, endTime));
|
||||
assertFalse(LocalDateTimeUtils.isTimeRangeValid(beginTime, beginTime));
|
||||
assertFalse(LocalDateTimeUtils.isTimeRangeValid(endTime, beginTime));
|
||||
assertFalse(LocalDateTimeUtils.isTimeRangeValid(null, endTime));
|
||||
assertFalse(LocalDateTimeUtils.isTimeRangeValid(beginTime, null));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testIsTimeRangePresent() {
|
||||
// 准备参数
|
||||
LocalDateTime time = LocalDateTime.of(2026, 7, 24, 9, 0);
|
||||
|
||||
// 调用,并断言:空数组和双空边界都不是有效范围
|
||||
assertFalse(LocalDateTimeUtils.isTimeRangePresent(null));
|
||||
assertFalse(LocalDateTimeUtils.isTimeRangePresent(new LocalDateTime[0]));
|
||||
assertFalse(LocalDateTimeUtils.isTimeRangePresent(new LocalDateTime[]{null, null}));
|
||||
|
||||
// 调用,并断言:单边或双边存在即为有效范围
|
||||
assertTrue(LocalDateTimeUtils.isTimeRangePresent(new LocalDateTime[]{time}));
|
||||
assertTrue(LocalDateTimeUtils.isTimeRangePresent(new LocalDateTime[]{null, time}));
|
||||
assertTrue(LocalDateTimeUtils.isTimeRangePresent(new LocalDateTime[]{time, time.plusHours(1)}));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testGetDayTime() {
|
||||
// 准备参数
|
||||
LocalDate date = LocalDate.of(2026, 7, 24);
|
||||
LocalDateTime time = date.atTime(15, 30);
|
||||
|
||||
// 调用,并断言
|
||||
assertEquals(LocalDateTime.of(2026, 7, 24, 0, 0),
|
||||
LocalDateTimeUtils.getDayBeginTime(date));
|
||||
assertEquals(LocalDateTime.of(2026, 7, 24, 0, 0),
|
||||
LocalDateTimeUtils.getDayBeginTime(time));
|
||||
assertNull(LocalDateTimeUtils.getDayBeginTime((LocalDateTime) null));
|
||||
assertEquals(LocalDateTime.of(date, LocalTime.MAX),
|
||||
LocalDateTimeUtils.getDayEndTime(date));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testGetDateTimeRange() {
|
||||
// 准备参数
|
||||
LocalDate beginDate = LocalDate.of(2026, 7, 1);
|
||||
LocalDate endDate = LocalDate.of(2026, 7, 31);
|
||||
|
||||
// 调用,并断言:左右两端均为闭区间边界
|
||||
LocalDateTime[] result = LocalDateTimeUtils.getDateTimeRange(beginDate, endDate);
|
||||
assertEquals(beginDate.atStartOfDay(), result[0]);
|
||||
assertEquals(endDate.atTime(LocalTime.MAX), result[1]);
|
||||
|
||||
// 调用,并断言:支持单边范围
|
||||
result = LocalDateTimeUtils.getDateTimeRange(beginDate, null);
|
||||
assertEquals(beginDate.atStartOfDay(), result[0]);
|
||||
assertNull(result[1]);
|
||||
result = LocalDateTimeUtils.getDateTimeRange(null, endDate);
|
||||
assertNull(result[0]);
|
||||
assertEquals(endDate.atTime(LocalTime.MAX), result[1]);
|
||||
|
||||
// 调用,并断言:两个边界都为空时不生成范围
|
||||
assertNull(LocalDateTimeUtils.getDateTimeRange(null, null));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testGetMonthDateTimeRange() {
|
||||
// 调用:闰年二月
|
||||
LocalDateTime[] result = LocalDateTimeUtils.getMonthDateTimeRange(2024, 2);
|
||||
|
||||
// 断言:完整包含闰年二月的首尾时刻
|
||||
assertEquals(LocalDateTime.of(2024, 2, 1, 0, 0), result[0]);
|
||||
assertEquals(LocalDate.of(2024, 2, 29).atTime(LocalTime.MAX), result[1]);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testGetMonthBeginTime() {
|
||||
// 准备参数
|
||||
YearMonth month = YearMonth.of(2026, 12);
|
||||
|
||||
// 调用,并断言
|
||||
assertEquals(LocalDateTime.of(2026, 12, 1, 0, 0),
|
||||
LocalDateTimeUtils.getMonthBeginTime(month));
|
||||
assertEquals(LocalDateTime.of(2027, 1, 1, 0, 0),
|
||||
LocalDateTimeUtils.getNextMonthBeginTime(month));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testIsBetween() {
|
||||
// 准备参数
|
||||
LocalDate beginDate = LocalDate.of(2026, 7, 23);
|
||||
LocalDate endDate = LocalDate.of(2026, 7, 25);
|
||||
LocalDateTime beginTime = beginDate.atTime(9, 0);
|
||||
LocalDateTime endTime = beginDate.atTime(18, 0);
|
||||
|
||||
// 调用,并断言:日期闭区间
|
||||
assertTrue(LocalDateTimeUtils.isBetween(beginDate, endDate, beginDate));
|
||||
assertTrue(LocalDateTimeUtils.isBetween(beginDate, endDate, LocalDate.of(2026, 7, 24)));
|
||||
assertTrue(LocalDateTimeUtils.isBetween(beginDate, endDate, endDate));
|
||||
assertFalse(LocalDateTimeUtils.isBetween(beginDate, endDate, LocalDate.of(2026, 7, 26)));
|
||||
assertFalse(LocalDateTimeUtils.isBetween(beginDate, endDate, null));
|
||||
assertFalse(LocalDateTimeUtils.isNotBetween(beginDate, endDate, beginDate));
|
||||
assertTrue(LocalDateTimeUtils.isNotBetween(beginDate, endDate, LocalDate.of(2026, 7, 26)));
|
||||
assertTrue(LocalDateTimeUtils.isNotBetween(beginDate, endDate, null));
|
||||
|
||||
// 调用,并断言:时间闭区间
|
||||
assertTrue(LocalDateTimeUtils.isBetween(beginTime, endTime, beginTime));
|
||||
assertTrue(LocalDateTimeUtils.isBetween(beginTime, endTime, beginDate.atTime(12, 0)));
|
||||
assertTrue(LocalDateTimeUtils.isBetween(beginTime, endTime, endTime));
|
||||
assertFalse(LocalDateTimeUtils.isBetween(beginTime, endTime, beginDate.atTime(18, 1)));
|
||||
assertFalse(LocalDateTimeUtils.isBetween(beginTime, endTime, (LocalDateTime) null));
|
||||
assertFalse(LocalDateTimeUtils.isNotBetween(beginTime, endTime, endTime));
|
||||
assertTrue(LocalDateTimeUtils.isNotBetween(beginTime, endTime, beginDate.atTime(18, 1)));
|
||||
assertTrue(LocalDateTimeUtils.isNotBetween(beginTime, endTime, null));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testIsClosedRangeOverlap() {
|
||||
// 准备参数
|
||||
LocalDateTime firstBeginTime = LocalDateTime.of(2026, 7, 24, 9, 0);
|
||||
LocalDateTime firstEndTime = LocalDateTime.of(2026, 7, 24, 12, 0);
|
||||
|
||||
// 调用,并断言:首尾相接属于闭区间重叠
|
||||
assertTrue(LocalDateTimeUtils.isClosedRangeOverlap(
|
||||
firstBeginTime, firstEndTime, firstEndTime, firstEndTime.plusHours(1)));
|
||||
assertTrue(LocalDateTimeUtils.isClosedRangeOverlap(
|
||||
firstBeginTime, firstEndTime, firstBeginTime.minusHours(1), firstBeginTime));
|
||||
assertFalse(LocalDateTimeUtils.isClosedRangeOverlap(
|
||||
firstBeginTime, firstEndTime, firstEndTime.plusNanos(1), firstEndTime.plusHours(1)));
|
||||
assertFalse(LocalDateTimeUtils.isClosedRangeOverlap(
|
||||
firstBeginTime, firstEndTime, null, firstEndTime));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSubtractTimeRanges() {
|
||||
// 准备参数
|
||||
LocalDate date = LocalDate.of(2026, 7, 24);
|
||||
TimeRange sourceRange = new TimeRange(date.atTime(9, 0), date.atTime(18, 0));
|
||||
|
||||
// 调用:扣除原时段中间的一段
|
||||
List<TimeRange> result = LocalDateTimeUtils.subtractTimeRanges(
|
||||
Collections.singletonList(sourceRange),
|
||||
new TimeRange(date.atTime(12, 0), date.atTime(13, 0)));
|
||||
|
||||
// 断言:原时段被拆分为两段
|
||||
assertEquals(2, result.size());
|
||||
assertTimeRange(CollUtil.getFirst(result), date.atTime(9, 0), date.atTime(12, 0));
|
||||
assertTimeRange(result.get(1), date.atTime(13, 0), date.atTime(18, 0));
|
||||
|
||||
// 调用,并断言:扣除时段完整覆盖原时段
|
||||
result = LocalDateTimeUtils.subtractTimeRanges(
|
||||
Collections.singletonList(sourceRange),
|
||||
new TimeRange(date.atTime(8, 0), date.atTime(19, 0)));
|
||||
assertTrue(result.isEmpty());
|
||||
|
||||
// 调用:扣除时段与原时段首尾相接
|
||||
result = LocalDateTimeUtils.subtractTimeRanges(
|
||||
Collections.singletonList(sourceRange),
|
||||
new TimeRange(date.atTime(18, 0), date.atTime(19, 0)));
|
||||
// 断言:两个时段仅首尾相接,没有正时长交集,原时段保持不变
|
||||
assertEquals(1, result.size());
|
||||
assertTimeRange(CollUtil.getFirst(result), date.atTime(9, 0), date.atTime(18, 0));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testMergeTimeRanges() {
|
||||
// 准备参数
|
||||
LocalDate date = LocalDate.of(2026, 7, 24);
|
||||
List<TimeRange> timeRanges = Arrays.asList(
|
||||
new TimeRange(date.atTime(13, 0), date.atTime(14, 0)),
|
||||
new TimeRange(date.atTime(9, 0), date.atTime(11, 0)),
|
||||
new TimeRange(date.atTime(10, 0), date.atTime(13, 0)),
|
||||
new TimeRange(date.atTime(16, 0), date.atTime(17, 0)));
|
||||
|
||||
// 调用
|
||||
List<TimeRange> result = LocalDateTimeUtils.mergeTimeRanges(timeRanges);
|
||||
|
||||
// 断言:重叠或首尾相接的时段被合并,独立时段保持不变
|
||||
assertEquals(2, result.size());
|
||||
assertTimeRange(CollUtil.getFirst(result), date.atTime(9, 0), date.atTime(14, 0));
|
||||
assertTimeRange(result.get(1), date.atTime(16, 0), date.atTime(17, 0));
|
||||
// 调用,并断言:空列表
|
||||
assertTrue(LocalDateTimeUtils.mergeTimeRanges(Collections.emptyList()).isEmpty());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testIntersectTimeRanges() {
|
||||
// 准备参数
|
||||
LocalDate date = LocalDate.of(2026, 7, 24);
|
||||
List<TimeRange> firstRanges = Arrays.asList(
|
||||
new TimeRange(date.atTime(9, 0), date.atTime(12, 0)),
|
||||
new TimeRange(date.atTime(13, 0), date.atTime(18, 0)));
|
||||
List<TimeRange> secondRanges = Arrays.asList(
|
||||
new TimeRange(date.atTime(10, 0), date.atTime(14, 0)),
|
||||
new TimeRange(date.atTime(17, 0), date.atTime(19, 0)));
|
||||
|
||||
// 调用
|
||||
List<TimeRange> result = LocalDateTimeUtils.intersectTimeRanges(firstRanges, secondRanges);
|
||||
|
||||
// 断言
|
||||
assertEquals(3, result.size());
|
||||
assertTimeRange(CollUtil.getFirst(result), date.atTime(10, 0), date.atTime(12, 0));
|
||||
assertTimeRange(result.get(1), date.atTime(13, 0), date.atTime(14, 0));
|
||||
assertTimeRange(result.get(2), date.atTime(17, 0), date.atTime(18, 0));
|
||||
// 调用,并断言:两个时段仅首尾相接时不生成零时长交集
|
||||
assertNull(LocalDateTimeUtils.intersectTimeRange(
|
||||
new TimeRange(date.atTime(9, 0), date.atTime(10, 0)),
|
||||
new TimeRange(date.atTime(10, 0), date.atTime(11, 0))));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testCalculateDurationMinutes() {
|
||||
// 准备参数
|
||||
LocalDate date = LocalDate.of(2026, 7, 24);
|
||||
List<TimeRange> timeRanges = Arrays.asList(
|
||||
new TimeRange(date.atTime(9, 0), date.atTime(10, 0)),
|
||||
new TimeRange(date.atTime(13, 0), date.atTime(14, 30)));
|
||||
|
||||
// 调用,并断言
|
||||
assertEquals(150, LocalDateTimeUtils.calculateDurationMinutes(timeRanges));
|
||||
// 调用,并断言:空列表
|
||||
assertEquals(0, LocalDateTimeUtils.calculateDurationMinutes(Collections.emptyList()));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testBuildDailyTimeRanges() {
|
||||
// 准备参数
|
||||
LocalDate beginDate = LocalDate.of(2026, 7, 24);
|
||||
LocalDate endDate = LocalDate.of(2026, 7, 25);
|
||||
|
||||
// 调用:构建跨天时间范围
|
||||
List<TimeRange> result = LocalDateTimeUtils.buildDailyTimeRanges(
|
||||
beginDate, endDate, LocalTime.of(22, 0), LocalTime.of(6, 0));
|
||||
|
||||
// 断言
|
||||
assertEquals(2, result.size());
|
||||
assertTimeRange(CollUtil.getFirst(result), beginDate.atTime(22, 0), beginDate.plusDays(1).atTime(6, 0));
|
||||
assertTimeRange(result.get(1), endDate.atTime(22, 0), endDate.plusDays(1).atTime(6, 0));
|
||||
|
||||
// 调用:每日开始和结束时刻相同
|
||||
result = LocalDateTimeUtils.buildDailyTimeRanges(
|
||||
beginDate, beginDate, LocalTime.of(9, 0), LocalTime.of(9, 0));
|
||||
// 断言:按完整一天处理
|
||||
assertEquals(1, result.size());
|
||||
assertTimeRange(CollUtil.getFirst(result), beginDate.atTime(9, 0), beginDate.plusDays(1).atTime(9, 0));
|
||||
|
||||
// 调用,并断言:开始日期晚于结束日期
|
||||
assertTrue(LocalDateTimeUtils.buildDailyTimeRanges(
|
||||
endDate, beginDate, LocalTime.of(9, 0), LocalTime.of(18, 0)).isEmpty());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testFindDailyTimeRange() {
|
||||
// 准备参数
|
||||
LocalDate date = LocalDate.of(2026, 7, 8);
|
||||
|
||||
// 调用,并断言普通时间范围的左右边界
|
||||
TimeRange result = LocalDateTimeUtils.findDailyTimeRange(
|
||||
date, LocalTime.of(8, 0), LocalTime.of(10, 0), date.atTime(8, 0));
|
||||
assertEquals(date.atTime(8, 0), result.getStartTime());
|
||||
assertEquals(date.atTime(10, 0), result.getEndTime());
|
||||
assertNotNull(LocalDateTimeUtils.findDailyTimeRange(
|
||||
date, LocalTime.of(8, 0), LocalTime.of(10, 0), date.atTime(10, 0)));
|
||||
|
||||
// 调用,并断言跨日时间范围
|
||||
result = LocalDateTimeUtils.findDailyTimeRange(
|
||||
date, LocalTime.of(20, 0), LocalTime.of(6, 0), date.plusDays(1).atTime(5, 0));
|
||||
assertEquals(date.atTime(20, 0), result.getStartTime());
|
||||
assertEquals(date.plusDays(1).atTime(6, 0), result.getEndTime());
|
||||
assertNull(LocalDateTimeUtils.findDailyTimeRange(
|
||||
date, LocalTime.of(8, 0), LocalTime.of(10, 0), date.atTime(12, 0)));
|
||||
}
|
||||
|
||||
private static void assertTimeRange(TimeRange timeRange,
|
||||
LocalDateTime expectedStartTime,
|
||||
LocalDateTime expectedEndTime) {
|
||||
assertEquals(expectedStartTime, timeRange.getStartTime());
|
||||
assertEquals(expectedEndTime, timeRange.getEndTime());
|
||||
}
|
||||
|
||||
}
|
||||
+207
@@ -0,0 +1,207 @@
|
||||
package cn.iocoder.yudao.framework.excel.core.convert;
|
||||
|
||||
import cn.hutool.core.convert.Convert;
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
import cn.iocoder.yudao.framework.dict.core.DictFrameworkUtils;
|
||||
import cn.iocoder.yudao.framework.excel.core.annotations.DictFormat;
|
||||
import cn.idev.excel.converters.Converter;
|
||||
import cn.idev.excel.enums.CellDataTypeEnum;
|
||||
import cn.idev.excel.metadata.GlobalConfiguration;
|
||||
import cn.idev.excel.metadata.data.ReadCellData;
|
||||
import cn.idev.excel.metadata.data.WriteCellData;
|
||||
import cn.idev.excel.metadata.property.ExcelContentProperty;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
import java.lang.reflect.Array;
|
||||
import java.lang.reflect.Field;
|
||||
import java.lang.reflect.Modifier;
|
||||
import java.lang.reflect.ParameterizedType;
|
||||
import java.lang.reflect.Type;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.LinkedHashSet;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
|
||||
/**
|
||||
* Excel 多值数据字典转换器
|
||||
*
|
||||
* 数据库存储值使用半角逗号分隔,例如 {@code 1,2}
|
||||
* Excel 展示使用顿号分隔,例如 {@code 男、女}
|
||||
* 使用时,需要在字段上同时配置
|
||||
* {@code @ExcelProperty(converter = MultiDictConvert.class)} 和 {@link DictFormat}
|
||||
*
|
||||
* @author NaCl
|
||||
*/
|
||||
@Slf4j
|
||||
public class MultiDictConvert implements Converter<Object> {
|
||||
|
||||
private static final String EXCEL_SEPARATOR = "、";
|
||||
private static final String DB_SEPARATOR = ",";
|
||||
private static final String EXCEL_SEPARATOR_REGEX = "[、,,]";
|
||||
|
||||
@Override
|
||||
public Class<?> supportJavaTypeKey() {
|
||||
throw new UnsupportedOperationException("暂不支持,也不需要");
|
||||
}
|
||||
|
||||
@Override
|
||||
public CellDataTypeEnum supportExcelTypeKey() {
|
||||
throw new UnsupportedOperationException("暂不支持,也不需要");
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object convertToJavaData(ReadCellData readCellData, ExcelContentProperty contentProperty,
|
||||
GlobalConfiguration globalConfiguration) {
|
||||
// 空时,返回空值
|
||||
Field field = contentProperty.getField();
|
||||
String labels = readCellData.getStringValue();
|
||||
if (StrUtil.isBlank(labels)) {
|
||||
return convertToFieldValue(field, new ArrayList<>());
|
||||
}
|
||||
|
||||
// 使用字典解析
|
||||
String type = getType(contentProperty);
|
||||
String[] labelArray = labels.split(EXCEL_SEPARATOR_REGEX);
|
||||
List<String> values = new ArrayList<>(labelArray.length);
|
||||
for (String item : labelArray) {
|
||||
String label = item.trim();
|
||||
if (StrUtil.isBlank(label)) {
|
||||
continue;
|
||||
}
|
||||
String value = DictFrameworkUtils.parseDictDataValue(type, label);
|
||||
if (value == null) {
|
||||
log.error("[convertToJavaData][type({}) 解析不掉 label({})]", type, label);
|
||||
return null;
|
||||
}
|
||||
values.add(value);
|
||||
}
|
||||
// 将 String 的 value 转换成对应的属性
|
||||
return convertToFieldValue(field, values);
|
||||
}
|
||||
|
||||
@Override
|
||||
public WriteCellData<String> convertToExcelData(Object object, ExcelContentProperty contentProperty,
|
||||
GlobalConfiguration globalConfiguration) {
|
||||
// 空时,返回空
|
||||
if (object == null) {
|
||||
return new WriteCellData<>("");
|
||||
}
|
||||
|
||||
// 使用字典格式化
|
||||
String type = getType(contentProperty);
|
||||
List<String> values = convertToStringList(object);
|
||||
List<String> labels = new ArrayList<>(values.size());
|
||||
for (String value : values) {
|
||||
String label = DictFrameworkUtils.parseDictDataLabel(type, value);
|
||||
if (label == null) {
|
||||
log.error("[convertToExcelData][type({}) 转换不了 value({})]", type, value);
|
||||
return new WriteCellData<>("");
|
||||
}
|
||||
labels.add(label);
|
||||
}
|
||||
// 生成 Excel 小表格
|
||||
return new WriteCellData<>(String.join(EXCEL_SEPARATOR, labels));
|
||||
}
|
||||
|
||||
private static Object convertToFieldValue(Field field, List<String> values) {
|
||||
Class<?> fieldClazz = field.getType();
|
||||
if (String.class == fieldClazz) {
|
||||
return String.join(DB_SEPARATOR, values);
|
||||
}
|
||||
if (fieldClazz.isArray()) {
|
||||
return convertToArray(fieldClazz.getComponentType(), values);
|
||||
}
|
||||
if (Collection.class.isAssignableFrom(fieldClazz)) {
|
||||
return convertToCollection(field, values);
|
||||
}
|
||||
return Convert.convert(fieldClazz, String.join(DB_SEPARATOR, values));
|
||||
}
|
||||
|
||||
private static Object convertToArray(Class<?> componentType, List<String> values) {
|
||||
Object array = Array.newInstance(componentType, values.size());
|
||||
for (int i = 0; i < values.size(); i++) {
|
||||
Array.set(array, i, Convert.convert(componentType, values.get(i)));
|
||||
}
|
||||
return array;
|
||||
}
|
||||
|
||||
private static Collection<?> convertToCollection(Field field, List<String> values) {
|
||||
Class<?> elementClazz = getCollectionElementClazz(field);
|
||||
Collection<Object> result = createCollection(field.getType());
|
||||
for (String value : values) {
|
||||
result.add(Convert.convert(elementClazz, value));
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
private static Collection<Object> createCollection(Class<?> fieldClazz) {
|
||||
if (!fieldClazz.isInterface() && !Modifier.isAbstract(fieldClazz.getModifiers())) {
|
||||
try {
|
||||
return (Collection<Object>) fieldClazz.getDeclaredConstructor().newInstance();
|
||||
} catch (Exception ignored) {
|
||||
// 使用默认集合实现
|
||||
}
|
||||
}
|
||||
return Set.class.isAssignableFrom(fieldClazz) ? new LinkedHashSet<>() : new ArrayList<>();
|
||||
}
|
||||
|
||||
private static Class<?> getCollectionElementClazz(Field field) {
|
||||
Type genericType = field.getGenericType();
|
||||
if (!(genericType instanceof ParameterizedType)) {
|
||||
return String.class;
|
||||
}
|
||||
Type actualType = ((ParameterizedType) genericType).getActualTypeArguments()[0];
|
||||
if (actualType instanceof Class<?>) {
|
||||
return (Class<?>) actualType;
|
||||
}
|
||||
if (actualType instanceof ParameterizedType
|
||||
&& ((ParameterizedType) actualType).getRawType() instanceof Class<?>) {
|
||||
return (Class<?>) ((ParameterizedType) actualType).getRawType();
|
||||
}
|
||||
return String.class;
|
||||
}
|
||||
|
||||
private static List<String> convertToStringList(Object object) {
|
||||
List<String> values = new ArrayList<>();
|
||||
if (object instanceof String) {
|
||||
String[] valueArray = ((String) object).split(DB_SEPARATOR);
|
||||
for (String value : valueArray) {
|
||||
addStringValue(values, value);
|
||||
}
|
||||
return values;
|
||||
}
|
||||
if (object instanceof Collection<?>) {
|
||||
for (Object item : (Collection<?>) object) {
|
||||
addStringValue(values, item);
|
||||
}
|
||||
return values;
|
||||
}
|
||||
if (object.getClass().isArray()) {
|
||||
int length = Array.getLength(object);
|
||||
for (int i = 0; i < length; i++) {
|
||||
addStringValue(values, Array.get(object, i));
|
||||
}
|
||||
return values;
|
||||
}
|
||||
addStringValue(values, object);
|
||||
return values;
|
||||
}
|
||||
|
||||
private static void addStringValue(List<String> values, Object value) {
|
||||
if (value == null) {
|
||||
return;
|
||||
}
|
||||
String str = String.valueOf(value).trim();
|
||||
if (StrUtil.isBlank(str)) {
|
||||
return;
|
||||
}
|
||||
values.add(str);
|
||||
}
|
||||
|
||||
private static String getType(ExcelContentProperty contentProperty) {
|
||||
return contentProperty.getField().getAnnotation(DictFormat.class).value();
|
||||
}
|
||||
|
||||
}
|
||||
+296
@@ -0,0 +1,296 @@
|
||||
package cn.iocoder.yudao.framework.excel.core.convert;
|
||||
|
||||
import cn.hutool.core.collection.ListUtil;
|
||||
import cn.idev.excel.metadata.data.ReadCellData;
|
||||
import cn.idev.excel.metadata.data.WriteCellData;
|
||||
import cn.idev.excel.metadata.property.ExcelContentProperty;
|
||||
import cn.iocoder.yudao.framework.common.biz.system.dict.DictDataCommonApi;
|
||||
import cn.iocoder.yudao.framework.common.biz.system.dict.dto.DictDataRespDTO;
|
||||
import cn.iocoder.yudao.framework.common.pojo.CommonResult;
|
||||
import cn.iocoder.yudao.framework.dict.core.DictFrameworkUtils;
|
||||
import cn.iocoder.yudao.framework.excel.core.annotations.DictFormat;
|
||||
import cn.iocoder.yudao.framework.test.core.ut.BaseMockitoUnitTest;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.mockito.Mock;
|
||||
|
||||
import java.lang.reflect.Field;
|
||||
import java.util.Arrays;
|
||||
import java.util.HashSet;
|
||||
import java.util.LinkedHashSet;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
|
||||
import static cn.iocoder.yudao.framework.test.core.util.RandomUtils.randomPojo;
|
||||
import static org.junit.jupiter.api.Assertions.assertArrayEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertNull;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
import static org.mockito.ArgumentMatchers.eq;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
/**
|
||||
* {@link MultiDictConvert} 的单元测试
|
||||
*
|
||||
* @author NaCl
|
||||
*/
|
||||
public class MultiDictConvertTest extends BaseMockitoUnitTest {
|
||||
|
||||
private static final String DICT_TYPE = "test_dict";
|
||||
|
||||
@Mock
|
||||
private DictDataCommonApi dictDataApi;
|
||||
|
||||
private MultiDictConvert convert;
|
||||
|
||||
@BeforeEach
|
||||
public void setUp() {
|
||||
convert = new MultiDictConvert();
|
||||
DictFrameworkUtils.init(dictDataApi);
|
||||
DictFrameworkUtils.clearCache();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testConvertToExcelData_string() throws NoSuchFieldException {
|
||||
// mock 数据
|
||||
mockDictDatas();
|
||||
|
||||
// 调用
|
||||
WriteCellData<String> result = convert.convertToExcelData("1,2", mockContentProperty("stringValues"), null);
|
||||
// 断言
|
||||
assertEquals("男、女", result.getStringValue());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testConvertToExcelData_single() throws NoSuchFieldException {
|
||||
// mock 数据
|
||||
mockDictDatas();
|
||||
|
||||
// 调用
|
||||
WriteCellData<String> result = convert.convertToExcelData("1", mockContentProperty("stringValues"), null);
|
||||
// 断言
|
||||
assertEquals("男", result.getStringValue());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testConvertToExcelData_order() throws NoSuchFieldException {
|
||||
// mock 数据
|
||||
mockDictDatas();
|
||||
|
||||
// 调用
|
||||
WriteCellData<String> result = convert.convertToExcelData("2,1", mockContentProperty("stringValues"), null);
|
||||
// 断言
|
||||
assertEquals("女、男", result.getStringValue());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testConvertToExcelData_null() {
|
||||
// 调用
|
||||
WriteCellData<String> result = convert.convertToExcelData(null, null, null);
|
||||
// 断言
|
||||
assertEquals("", result.getStringValue());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testConvertToExcelData_collection() throws NoSuchFieldException {
|
||||
// mock 数据
|
||||
mockDictDatas();
|
||||
|
||||
// 调用
|
||||
WriteCellData<String> result = convert.convertToExcelData(ListUtil.of(1, 2),
|
||||
mockContentProperty("listValues"), null);
|
||||
// 断言
|
||||
assertEquals("男、女", result.getStringValue());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testConvertToExcelData_array() throws NoSuchFieldException {
|
||||
// mock 数据
|
||||
mockDictDatas();
|
||||
|
||||
// 调用
|
||||
WriteCellData<String> result = convert.convertToExcelData(new int[]{1, 2},
|
||||
mockContentProperty("stringValues"), null);
|
||||
// 断言
|
||||
assertEquals("男、女", result.getStringValue());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testConvertToExcelData_unknownValue() throws NoSuchFieldException {
|
||||
// mock 数据
|
||||
mockDictDatas();
|
||||
|
||||
// 调用
|
||||
WriteCellData<String> result = convert.convertToExcelData("1,3", mockContentProperty("stringValues"), null);
|
||||
// 断言
|
||||
assertEquals("", result.getStringValue());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testConvertToJavaData_string() throws NoSuchFieldException {
|
||||
// mock 数据
|
||||
mockDictDatas();
|
||||
|
||||
// 调用
|
||||
Object result = convert.convertToJavaData(mockReadCellData("男、女"),
|
||||
mockContentProperty("stringValues"), null);
|
||||
// 断言
|
||||
assertEquals("1,2", result);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testConvertToJavaData_single() throws NoSuchFieldException {
|
||||
// mock 数据
|
||||
mockDictDatas();
|
||||
|
||||
// 调用
|
||||
Object result = convert.convertToJavaData(mockReadCellData("男"),
|
||||
mockContentProperty("stringValues"), null);
|
||||
// 断言
|
||||
assertEquals("1", result);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testConvertToJavaData_blank() throws NoSuchFieldException {
|
||||
// 调用
|
||||
Object result = convert.convertToJavaData(mockReadCellData(""),
|
||||
mockContentProperty("stringValues"), null);
|
||||
// 断言
|
||||
assertEquals("", result);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testConvertToJavaData_blankList() throws NoSuchFieldException {
|
||||
// 调用
|
||||
Object result = convert.convertToJavaData(mockReadCellData(""),
|
||||
mockContentProperty("listValues"), null);
|
||||
// 断言
|
||||
assertTrue(result instanceof List);
|
||||
assertTrue(((List<?>) result).isEmpty());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testConvertToJavaData_blankSet() throws NoSuchFieldException {
|
||||
// 调用
|
||||
Object result = convert.convertToJavaData(mockReadCellData(""),
|
||||
mockContentProperty("setValues"), null);
|
||||
// 断言
|
||||
assertTrue(result instanceof LinkedHashSet);
|
||||
assertTrue(((Set<?>) result).isEmpty());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testConvertToJavaData_blankArray() throws NoSuchFieldException {
|
||||
// 调用
|
||||
Object result = convert.convertToJavaData(mockReadCellData(""),
|
||||
mockContentProperty("arrayValues"), null);
|
||||
// 断言
|
||||
assertArrayEquals(new Integer[0], (Integer[]) result);
|
||||
}
|
||||
|
||||
@Test
|
||||
@SuppressWarnings("unchecked")
|
||||
public void testConvertToJavaData_list() throws NoSuchFieldException {
|
||||
// mock 数据
|
||||
mockDictDatas();
|
||||
|
||||
// 调用
|
||||
Object result = convert.convertToJavaData(mockReadCellData("男、女"),
|
||||
mockContentProperty("listValues"), null);
|
||||
// 断言
|
||||
assertEquals(ListUtil.of(1, 2), (List<Integer>) result);
|
||||
}
|
||||
|
||||
@Test
|
||||
@SuppressWarnings("unchecked")
|
||||
public void testConvertToJavaData_set() throws NoSuchFieldException {
|
||||
// mock 数据
|
||||
mockDictDatas();
|
||||
|
||||
// 调用
|
||||
Object result = convert.convertToJavaData(mockReadCellData("男,女"),
|
||||
mockContentProperty("setValues"), null);
|
||||
// 断言
|
||||
assertEquals(new LinkedHashSet<>(Arrays.asList(1L, 2L)), (Set<Long>) result);
|
||||
}
|
||||
|
||||
@Test
|
||||
@SuppressWarnings("SimplifiableAssertion")
|
||||
public void testConvertToJavaData_hashSet() throws NoSuchFieldException {
|
||||
// mock 数据
|
||||
mockDictDatas();
|
||||
|
||||
// 调用
|
||||
Object result = convert.convertToJavaData(mockReadCellData("男,女"),
|
||||
mockContentProperty("hashSetValues"), null);
|
||||
// 断言
|
||||
assertTrue(result instanceof HashSet);
|
||||
assertEquals(new HashSet<>(Arrays.asList(1L, 2L)), result);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testConvertToJavaData_array() throws NoSuchFieldException {
|
||||
// mock 数据
|
||||
mockDictDatas();
|
||||
|
||||
// 调用
|
||||
Object result = convert.convertToJavaData(mockReadCellData("男,女"),
|
||||
mockContentProperty("arrayValues"), null);
|
||||
// 断言
|
||||
assertArrayEquals(new Integer[]{1, 2}, (Integer[]) result);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testConvertToJavaData_unknownLabel() throws NoSuchFieldException {
|
||||
// mock 数据
|
||||
mockDictDatas();
|
||||
|
||||
// 调用
|
||||
Object result = convert.convertToJavaData(mockReadCellData("男、未知"),
|
||||
mockContentProperty("stringValues"), null);
|
||||
// 断言
|
||||
assertNull(result);
|
||||
}
|
||||
|
||||
private void mockDictDatas() {
|
||||
List<DictDataRespDTO> dictDatas = ListUtil.of(
|
||||
randomPojo(DictDataRespDTO.class, o -> o.setDictType(DICT_TYPE).setValue("1").setLabel("男")),
|
||||
randomPojo(DictDataRespDTO.class, o -> o.setDictType(DICT_TYPE).setValue("2").setLabel("女"))
|
||||
);
|
||||
when(dictDataApi.getDictDataList(eq(DICT_TYPE))).thenReturn(CommonResult.success(dictDatas));
|
||||
}
|
||||
|
||||
private static ReadCellData<?> mockReadCellData(String value) {
|
||||
ReadCellData<?> readCellData = mock(ReadCellData.class);
|
||||
when(readCellData.getStringValue()).thenReturn(value);
|
||||
return readCellData;
|
||||
}
|
||||
|
||||
private static ExcelContentProperty mockContentProperty(String fieldName) throws NoSuchFieldException {
|
||||
Field field = TestExcelVO.class.getDeclaredField(fieldName);
|
||||
ExcelContentProperty contentProperty = mock(ExcelContentProperty.class);
|
||||
when(contentProperty.getField()).thenReturn(field);
|
||||
return contentProperty;
|
||||
}
|
||||
|
||||
static class TestExcelVO {
|
||||
|
||||
@DictFormat(DICT_TYPE)
|
||||
private String stringValues;
|
||||
|
||||
@DictFormat(DICT_TYPE)
|
||||
private List<Integer> listValues;
|
||||
|
||||
@DictFormat(DICT_TYPE)
|
||||
private Set<Long> setValues;
|
||||
|
||||
@DictFormat(DICT_TYPE)
|
||||
private HashSet<Long> hashSetValues;
|
||||
|
||||
@DictFormat(DICT_TYPE)
|
||||
private Integer[] arrayValues;
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
+61
@@ -0,0 +1,61 @@
|
||||
package cn.iocoder.yudao.framework.excel.core.util;
|
||||
|
||||
import cn.idev.excel.FastExcelFactory;
|
||||
import cn.idev.excel.annotation.ExcelProperty;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.mock.web.MockMultipartFile;
|
||||
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
|
||||
/**
|
||||
* {@link ExcelUtils} 的单元测试
|
||||
*
|
||||
* @author 芋道源码
|
||||
*/
|
||||
public class ExcelUtilsTest {
|
||||
|
||||
@Test
|
||||
public void testRead_maxRowCount() throws Exception {
|
||||
// mock 数据
|
||||
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
|
||||
FastExcelFactory.write(outputStream, TestRow.class).sheet().doWrite(Arrays.asList(
|
||||
new TestRow("员工一"), new TestRow("员工二"), new TestRow("员工三")));
|
||||
MockMultipartFile file = new MockMultipartFile(
|
||||
"file", "employees.xlsx", null, outputStream.toByteArray());
|
||||
|
||||
// 调用
|
||||
List<TestRow> rows = ExcelUtils.read(file, TestRow.class, 2);
|
||||
|
||||
// 断言
|
||||
assertEquals(2, rows.size());
|
||||
assertEquals("员工一", rows.get(0).getName());
|
||||
assertEquals("员工二", rows.get(1).getName());
|
||||
}
|
||||
|
||||
public static class TestRow {
|
||||
|
||||
@ExcelProperty("姓名")
|
||||
private String name;
|
||||
|
||||
public TestRow() {
|
||||
}
|
||||
|
||||
public TestRow(String name) {
|
||||
this.name = name;
|
||||
}
|
||||
|
||||
public String getName() {
|
||||
return name;
|
||||
}
|
||||
|
||||
public void setName(String name) {
|
||||
this.name = name;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
+3
-10
@@ -102,16 +102,9 @@ public class QueryWrapperX<T> extends QueryWrapper<T> {
|
||||
}
|
||||
|
||||
public QueryWrapperX<T> betweenIfPresent(String column, Object[] values) {
|
||||
if (values != null && values.length != 0 && values[0] != null && values[1] != null) {
|
||||
return (QueryWrapperX<T>) super.between(column, values[0], values[1]);
|
||||
}
|
||||
if (values != null && values.length != 0 && values[0] != null) {
|
||||
return (QueryWrapperX<T>) ge(column, values[0]);
|
||||
}
|
||||
if (values != null && values.length != 0 && values[1] != null) {
|
||||
return (QueryWrapperX<T>) le(column, values[1]);
|
||||
}
|
||||
return this;
|
||||
Object val1 = values != null && values.length > 0 ? values[0] : null;
|
||||
Object val2 = values != null && values.length > 1 ? values[1] : null;
|
||||
return betweenIfPresent(column, val1, val2);
|
||||
}
|
||||
|
||||
// ========== 重写父类方法,方便链式调用 ==========
|
||||
|
||||
+24
@@ -24,6 +24,8 @@ import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
import java.util.regex.Pattern;
|
||||
import java.util.stream.Collectors;
|
||||
import java.util.stream.IntStream;
|
||||
|
||||
/**
|
||||
* MyBatis 工具类
|
||||
@@ -179,6 +181,28 @@ public class MyBatisUtils {
|
||||
return findInSet(columnName, paramIndex);
|
||||
}
|
||||
|
||||
/**
|
||||
* 跨数据库的 find_in_set 实现,适用于同一字段匹配多个参数的场景
|
||||
*
|
||||
* 每个参数生成一个 find_in_set 条件,并使用 OR 连接。
|
||||
*
|
||||
* @param columnName 字段名称
|
||||
* @param values 参数集合
|
||||
* @return sql
|
||||
*/
|
||||
public static String findInSet(String columnName, Collection<?> values) {
|
||||
return findInSet(JdbcUtils.getDbType(), columnName, values);
|
||||
}
|
||||
|
||||
static String findInSet(DbType dbType, String columnName, Collection<?> values) {
|
||||
if (CollUtil.isEmpty(values)) {
|
||||
throw new IllegalArgumentException("Values cannot be empty");
|
||||
}
|
||||
return IntStream.range(0, values.size())
|
||||
.mapToObj(index -> findInSet(dbType, columnName, index))
|
||||
.collect(Collectors.joining(" OR "));
|
||||
}
|
||||
|
||||
private static String findInSet(String columnName, int paramIndex) {
|
||||
DbType dbType = JdbcUtils.getDbType();
|
||||
return findInSet(dbType, columnName, paramIndex);
|
||||
|
||||
+32
@@ -0,0 +1,32 @@
|
||||
package cn.iocoder.yudao.framework.mybatis.core.query;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
|
||||
/**
|
||||
* {@link QueryWrapperX} 的单元测试
|
||||
*
|
||||
* @author 芋道源码
|
||||
*/
|
||||
public class QueryWrapperXTest {
|
||||
|
||||
@Test
|
||||
public void testBetweenIfPresent_array() {
|
||||
// 调用,并断言:空范围不追加条件
|
||||
assertEquals("", new QueryWrapperX<>().betweenIfPresent("create_time", (Object[]) null).getSqlSegment());
|
||||
assertEquals("", new QueryWrapperX<>().betweenIfPresent("create_time", new Object[0]).getSqlSegment());
|
||||
|
||||
// 调用,并断言:单边范围安全降级为大于等于或小于等于
|
||||
assertTrue(new QueryWrapperX<>().betweenIfPresent("create_time", new Object[]{1})
|
||||
.getSqlSegment().contains("create_time >="));
|
||||
assertTrue(new QueryWrapperX<>().betweenIfPresent("create_time", new Object[]{null, 2})
|
||||
.getSqlSegment().contains("create_time <="));
|
||||
|
||||
// 调用,并断言:双边范围使用左右闭合的 BETWEEN
|
||||
assertTrue(new QueryWrapperX<>().betweenIfPresent("create_time", new Object[]{1, 2})
|
||||
.getSqlSegment().contains("create_time BETWEEN"));
|
||||
}
|
||||
|
||||
}
|
||||
+9
@@ -10,6 +10,7 @@ import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
@@ -128,6 +129,14 @@ public class MyBatisUtilsTest {
|
||||
() -> MyBatisUtils.findInSet(DbType.MYSQL, "websites", -1));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testFindInSet_multipleValues() {
|
||||
assertEquals("FIND_IN_SET({0}, tag_ids) <> 0 OR FIND_IN_SET({1}, tag_ids) <> 0",
|
||||
MyBatisUtils.findInSet(DbType.MYSQL, "tag_ids", Arrays.asList(1L, 2L)));
|
||||
assertThrows(IllegalArgumentException.class,
|
||||
() -> MyBatisUtils.findInSet(DbType.MYSQL, "tag_ids", Collections.emptyList()));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testFindInSet_applyBindsValue() {
|
||||
// 准备参数
|
||||
|
||||
+1
-1
@@ -15,7 +15,7 @@ public class FmsAccountSetRespVO {
|
||||
@Schema(description = "公司编码", requiredMode = Schema.RequiredMode.REQUIRED, example = "WK001")
|
||||
private String companyCode;
|
||||
|
||||
@Schema(description = "公司名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "悟空科技有限公司")
|
||||
@Schema(description = "公司名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "测试科技有限公司")
|
||||
private String companyName;
|
||||
|
||||
@Schema(description = "公司简介", example = "专注企业数字化服务")
|
||||
|
||||
+1
-1
@@ -20,7 +20,7 @@ public class FmsAccountSetSaveReqVO {
|
||||
@DiffLogField(name = "公司编码")
|
||||
private String companyCode;
|
||||
|
||||
@Schema(description = "公司名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "悟空科技有限公司")
|
||||
@Schema(description = "公司名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "测试科技有限公司")
|
||||
@NotBlank(message = "公司名称不能为空")
|
||||
@Size(max = 255, message = "公司名称不能超过 255 个字符")
|
||||
@DiffLogField(name = "公司名称")
|
||||
|
||||
+1
-1
@@ -24,7 +24,7 @@ public class FmsAuxiliaryItemPageReqVO extends PageParam {
|
||||
@NotNull(message = "辅助核算类别编号不能为空")
|
||||
private Long auxiliaryTypeId;
|
||||
|
||||
@Schema(description = "关键词,匹配编码或名称", example = "悟空")
|
||||
@Schema(description = "关键词,匹配编码或名称", example = "测试")
|
||||
private String search;
|
||||
|
||||
}
|
||||
|
||||
+1
-1
@@ -30,7 +30,7 @@ public class FmsAuxiliaryItemRespVO {
|
||||
@ExcelProperty("编码")
|
||||
private String code;
|
||||
|
||||
@Schema(description = "名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "上海悟空数字科技有限公司")
|
||||
@Schema(description = "名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "上海测试数字科技有限公司")
|
||||
@ExcelProperty("名称")
|
||||
private String name;
|
||||
|
||||
|
||||
+1
-1
@@ -31,7 +31,7 @@ public class FmsAuxiliaryItemSaveReqVO {
|
||||
@Size(max = 64, message = "编码长度不能超过 64 个字符")
|
||||
private String code;
|
||||
|
||||
@Schema(description = "名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "上海悟空数字科技有限公司")
|
||||
@Schema(description = "名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "上海测试数字科技有限公司")
|
||||
@NotBlank(message = "名称不能为空")
|
||||
@Size(max = 255, message = "名称长度不能超过 255 个字符")
|
||||
private String name;
|
||||
|
||||
+1
-1
@@ -180,7 +180,7 @@ public class FmsFinanceParameterServiceImpl implements FmsFinanceParameterServic
|
||||
/**
|
||||
* 校验财务参数只扩展科目层级和各级编码长度,避免已使用的科目编码失效
|
||||
*
|
||||
* 每级编码长度限制为 2 至 5 位,与悟空财务的科目编码输入范围保持一致
|
||||
* 每级编码长度限制为 2 至 5 位,与既有财务规则保持一致
|
||||
*
|
||||
* @param oldLevel 原科目层级
|
||||
* @param oldRules 原科目编码规则
|
||||
|
||||
+1
-1
@@ -569,7 +569,7 @@ public class FmsCashFlowStatementServiceImpl implements FmsCashFlowStatementServ
|
||||
/**
|
||||
* 获得现金流量表金额
|
||||
*
|
||||
* 悟空财务使用非零金额覆盖公式计算结果,保存零值时重新按公式计算
|
||||
* 既有财务规则使用非零金额覆盖公式计算结果,保存零值时重新按公式计算
|
||||
*
|
||||
* @param savedAmount 已保存金额
|
||||
* @param calculatedAmount 公式计算金额
|
||||
|
||||
+1
-1
@@ -151,7 +151,7 @@ public interface FmsReportCommonService {
|
||||
FmsLedgerSubjectBalanceRespVO balance, boolean current);
|
||||
|
||||
/**
|
||||
* 计算利润表发生额,正向公式项按悟空语义抵减反方向发生额
|
||||
* 计算利润表发生额,正向公式项按既有业务语义抵减反方向发生额
|
||||
*
|
||||
* @param rule 取数规则
|
||||
* @param subject 科目
|
||||
|
||||
+3
-1
@@ -58,6 +58,8 @@ public class FmsAccountSetServiceImplTest extends BaseDbUnitTest {
|
||||
@MockitoBean
|
||||
private FmsFinanceParameterService financeParameterService;
|
||||
@MockitoBean
|
||||
private FmsFinanceIndicatorService financeIndicatorService;
|
||||
@MockitoBean
|
||||
private FmsVoucherWordService voucherWordService;
|
||||
@MockitoBean
|
||||
private FmsAuxiliaryTypeService auxiliaryTypeService;
|
||||
@@ -72,7 +74,7 @@ public class FmsAccountSetServiceImplTest extends BaseDbUnitTest {
|
||||
public void testCreateAccountSet_success() {
|
||||
// 准备参数
|
||||
Long userId = randomLongId();
|
||||
FmsAccountSetSaveReqVO reqVO = buildAccountSetSaveReqVO("WK001", "悟空科技有限公司");
|
||||
FmsAccountSetSaveReqVO reqVO = buildAccountSetSaveReqVO("TEST001", "测试科技有限公司");
|
||||
|
||||
// 调用
|
||||
Long accountSetId = accountSetService.createAccountSet(reqVO, userId);
|
||||
|
||||
+8
-8
@@ -59,7 +59,7 @@ public class FmsAuxiliaryItemServiceImplTest extends BaseDbUnitTest {
|
||||
// mock 方法
|
||||
mockAuxiliaryType(1L, 11L);
|
||||
// 准备参数
|
||||
FmsAuxiliaryItemSaveReqVO reqVO = buildSaveReqVO(1L, 11L, "KH001", "悟空客户");
|
||||
FmsAuxiliaryItemSaveReqVO reqVO = buildSaveReqVO(1L, 11L, "KH001", "测试客户");
|
||||
|
||||
// 调用
|
||||
Long auxiliaryItemId = auxiliaryItemService.createAuxiliaryItem(reqVO, 10L);
|
||||
@@ -67,7 +67,7 @@ public class FmsAuxiliaryItemServiceImplTest extends BaseDbUnitTest {
|
||||
// 断言
|
||||
FmsAuxiliaryItemDO auxiliaryItem = auxiliaryItemMapper.selectById(auxiliaryItemId);
|
||||
assertEquals("KH001", auxiliaryItem.getCode());
|
||||
assertEquals("悟空客户", auxiliaryItem.getName());
|
||||
assertEquals("测试客户", auxiliaryItem.getName());
|
||||
assertEquals(CommonStatusEnum.ENABLE.getStatus(), auxiliaryItem.getStatus());
|
||||
assertEquals(11L, auxiliaryItem.getAuxiliaryTypeId());
|
||||
verify(accountSetService).validateAccountSetWritePermission(1L, 10L);
|
||||
@@ -89,7 +89,7 @@ public class FmsAuxiliaryItemServiceImplTest extends BaseDbUnitTest {
|
||||
@Test
|
||||
public void testDeleteAuxiliaryItemList_success() {
|
||||
// mock 数据
|
||||
FmsAuxiliaryItemDO auxiliaryItem = buildAuxiliaryItem(1L, 11L, "KH001", "悟空客户");
|
||||
FmsAuxiliaryItemDO auxiliaryItem = buildAuxiliaryItem(1L, 11L, "KH001", "测试客户");
|
||||
auxiliaryItemMapper.insert(auxiliaryItem);
|
||||
|
||||
// 调用
|
||||
@@ -105,7 +105,7 @@ public class FmsAuxiliaryItemServiceImplTest extends BaseDbUnitTest {
|
||||
@Test
|
||||
public void testDeleteAuxiliaryItemList_voucherTemplateInUse() {
|
||||
// mock 数据
|
||||
FmsAuxiliaryItemDO auxiliaryItem = buildAuxiliaryItem(1L, 11L, "KH001", "悟空客户");
|
||||
FmsAuxiliaryItemDO auxiliaryItem = buildAuxiliaryItem(1L, 11L, "KH001", "测试客户");
|
||||
auxiliaryItemMapper.insert(auxiliaryItem);
|
||||
// mock 方法
|
||||
when(voucherTemplateService.getVoucherTemplateCountByAuxiliaryItemIds(
|
||||
@@ -120,7 +120,7 @@ public class FmsAuxiliaryItemServiceImplTest extends BaseDbUnitTest {
|
||||
@Test
|
||||
public void testUpdateAuxiliaryItemStatus_success() {
|
||||
// mock 数据
|
||||
FmsAuxiliaryItemDO auxiliaryItem = buildAuxiliaryItem(1L, 11L, "KH001", "悟空客户");
|
||||
FmsAuxiliaryItemDO auxiliaryItem = buildAuxiliaryItem(1L, 11L, "KH001", "测试客户");
|
||||
auxiliaryItemMapper.insert(auxiliaryItem);
|
||||
|
||||
// 调用
|
||||
@@ -137,13 +137,13 @@ public class FmsAuxiliaryItemServiceImplTest extends BaseDbUnitTest {
|
||||
public void testGetAuxiliaryItemPage_search() {
|
||||
// mock 数据
|
||||
mockAuxiliaryType(1L, 11L);
|
||||
auxiliaryItemMapper.insert(buildAuxiliaryItem(1L, 11L, "KH001", "上海悟空"));
|
||||
auxiliaryItemMapper.insert(buildAuxiliaryItem(1L, 11L, "KH001", "上海测试"));
|
||||
auxiliaryItemMapper.insert(buildAuxiliaryItem(1L, 11L, "KH002", "北京云财务"));
|
||||
// 准备参数
|
||||
FmsAuxiliaryItemPageReqVO reqVO = new FmsAuxiliaryItemPageReqVO();
|
||||
reqVO.setAccountSetId(1L);
|
||||
reqVO.setAuxiliaryTypeId(11L);
|
||||
reqVO.setSearch("悟空");
|
||||
reqVO.setSearch("测试");
|
||||
|
||||
// 调用
|
||||
PageResult<FmsAuxiliaryItemDO> pageResult = auxiliaryItemService
|
||||
@@ -212,7 +212,7 @@ public class FmsAuxiliaryItemServiceImplTest extends BaseDbUnitTest {
|
||||
.thenReturn(Collections.singletonList(new FmsAuxiliaryTypeDO()
|
||||
.setId(16L).setAccountSetId(1L).setName("存货").setSystemPreset(true)
|
||||
.setType(FmsAuxiliaryTypeEnum.INVENTORY.getType())));
|
||||
FmsAuxiliaryItemImportExcelVO row = buildImportRow("SP001", "悟空商品");
|
||||
FmsAuxiliaryItemImportExcelVO row = buildImportRow("SP001", "测试商品");
|
||||
row.setRemark("库存备注");
|
||||
row.setSpecification("标准版");
|
||||
row.setUnit("台");
|
||||
|
||||
+1
-1
@@ -10,7 +10,7 @@ import org.springframework.stereotype.Component;
|
||||
*
|
||||
* @author 芋道源码
|
||||
*/
|
||||
@Component
|
||||
@Component("hrmAreaParseFunction")
|
||||
public class HrmAreaParseFunction implements IParseFunction {
|
||||
|
||||
public static final String NAME = "getArea";
|
||||
|
||||
+2
-8
@@ -328,9 +328,7 @@ public class HrmAttendanceGroupServiceImpl implements HrmAttendanceGroupService
|
||||
// 1. 计算当前组接管的部门及员工范围
|
||||
Set<Long> deptIds = new HashSet<>(reqVO.getDeptIds());
|
||||
Set<Long> deptScope = new HashSet<>(deptIds);
|
||||
for (Long deptId : deptIds) {
|
||||
deptScope.addAll(convertSet(deptApi.getChildDeptList(deptId).getCheckedData(), DeptRespDTO::getId));
|
||||
}
|
||||
deptScope.addAll(convertSet(deptApi.getChildDeptList(deptIds).getCheckedData(), DeptRespDTO::getId));
|
||||
Set<Long> employeeIds = new HashSet<>(reqVO.getEmployeeIds());
|
||||
if (CollUtil.isNotEmpty(deptScope)) {
|
||||
employeeIds.addAll(convertSet(
|
||||
@@ -414,11 +412,7 @@ public class HrmAttendanceGroupServiceImpl implements HrmAttendanceGroupService
|
||||
Map<Long, List<Long>> result = new HashMap<>();
|
||||
for (Long deptId : validDeptIds) {
|
||||
List<Long> hierarchy = new ArrayList<>(Collections.singletonList(deptId));
|
||||
DeptRespDTO dept = deptApi.getDept(deptId).getCheckedData();
|
||||
while (dept != null && dept.getParentId() != null && dept.getParentId() > 0) {
|
||||
hierarchy.add(dept.getParentId());
|
||||
dept = deptApi.getDept(dept.getParentId()).getCheckedData();
|
||||
}
|
||||
hierarchy.addAll(convertList(deptApi.getParentDeptList(deptId).getCheckedData(), DeptRespDTO::getId));
|
||||
result.put(deptId, hierarchy);
|
||||
}
|
||||
return result;
|
||||
|
||||
+3
-13
@@ -20,7 +20,6 @@ import org.springframework.stereotype.Service;
|
||||
import org.springframework.validation.annotation.Validated;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
@@ -180,15 +179,8 @@ public class HrmSalaryGroupServiceImpl implements HrmSalaryGroupService {
|
||||
if (salaryGroup != null) {
|
||||
return salaryGroup;
|
||||
}
|
||||
List<Long> parentDeptIds = parentDeptIdCache.computeIfAbsent(employeeDeptId, deptId -> {
|
||||
List<Long> result = new ArrayList<>();
|
||||
DeptRespDTO dept = deptApi.getDept(deptId).getCheckedData();
|
||||
while (dept != null && dept.getParentId() != null && dept.getParentId() > 0) {
|
||||
result.add(dept.getParentId());
|
||||
dept = deptApi.getDept(dept.getParentId()).getCheckedData();
|
||||
}
|
||||
return result;
|
||||
});
|
||||
List<Long> parentDeptIds = parentDeptIdCache.computeIfAbsent(employeeDeptId,
|
||||
deptId -> convertList(deptApi.getParentDeptList(deptId).getCheckedData(), DeptRespDTO::getId));
|
||||
for (Long parentDeptId : parentDeptIds) {
|
||||
salaryGroup = deptSalaryGroupMap.get(parentDeptId);
|
||||
if (salaryGroup != null) {
|
||||
@@ -257,9 +249,7 @@ public class HrmSalaryGroupServiceImpl implements HrmSalaryGroupService {
|
||||
return Collections.emptySet();
|
||||
}
|
||||
Set<Long> result = new HashSet<>(deptIds);
|
||||
for (Long deptId : deptIds) {
|
||||
result.addAll(convertSet(deptApi.getChildDeptList(deptId).getCheckedData(), DeptRespDTO::getId));
|
||||
}
|
||||
result.addAll(convertSet(deptApi.getChildDeptList(deptIds).getCheckedData(), DeptRespDTO::getId));
|
||||
return result;
|
||||
}
|
||||
|
||||
|
||||
+147
@@ -0,0 +1,147 @@
|
||||
package cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpclient.manager;
|
||||
|
||||
import cn.iocoder.yudao.module.iot.core.biz.dto.IotModbusDeviceConfigRespDTO;
|
||||
import cn.iocoder.yudao.module.iot.core.biz.dto.IotModbusPointRespDTO;
|
||||
import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpclient.manager.IotModbusTcpClientPollScheduler.ReadSegment;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
import static cn.iocoder.yudao.module.iot.gateway.protocol.modbus.common.utils.IotModbusCommonUtils.FC_READ_COILS;
|
||||
import static cn.iocoder.yudao.module.iot.gateway.protocol.modbus.common.utils.IotModbusCommonUtils.FC_READ_HOLDING_REGISTERS;
|
||||
import static cn.iocoder.yudao.module.iot.gateway.protocol.modbus.common.utils.IotModbusCommonUtils.FC_READ_INPUT_REGISTERS;
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
|
||||
/**
|
||||
* {@link IotModbusTcpClientPollScheduler} 的单元测试
|
||||
*
|
||||
* @author 芋道源码
|
||||
*/
|
||||
public class IotModbusTcpClientPollSchedulerTest {
|
||||
|
||||
@Test
|
||||
public void testBuildReadSegments_mergeContinuousPoints() {
|
||||
// 准备参数
|
||||
IotModbusPointRespDTO point01 = randomPoint(1L, FC_READ_HOLDING_REGISTERS, 0, 1, 1000);
|
||||
IotModbusPointRespDTO point02 = randomPoint(2L, FC_READ_HOLDING_REGISTERS, 1, 2, 1000);
|
||||
IotModbusPointRespDTO point03 = randomPoint(3L, FC_READ_HOLDING_REGISTERS, 4, 1, 1000);
|
||||
IotModbusDeviceConfigRespDTO config = randomConfig(point01, point02, point03);
|
||||
|
||||
// 调用
|
||||
List<ReadSegment> segments = IotModbusTcpClientPollScheduler.buildReadSegments(config);
|
||||
|
||||
// 断言
|
||||
assertEquals(2, segments.size());
|
||||
assertEquals(0, segments.get(0).getStartAddress());
|
||||
assertEquals(3, segments.get(0).getRegisterCount());
|
||||
assertIterableEquals(Arrays.asList(point01, point02), segments.get(0).getPoints());
|
||||
assertEquals(4, segments.get(1).getStartAddress());
|
||||
assertEquals(1, segments.get(1).getRegisterCount());
|
||||
assertIterableEquals(Collections.singletonList(point03), segments.get(1).getPoints());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testBuildReadSegments_mergeOverlappingPoints() {
|
||||
// 准备参数
|
||||
IotModbusPointRespDTO point01 = randomPoint(1L, FC_READ_HOLDING_REGISTERS, 0, 2, 1000);
|
||||
IotModbusPointRespDTO point02 = randomPoint(2L, FC_READ_HOLDING_REGISTERS, 1, 1, 1000);
|
||||
IotModbusDeviceConfigRespDTO config = randomConfig(point01, point02);
|
||||
|
||||
// 调用
|
||||
List<ReadSegment> segments = IotModbusTcpClientPollScheduler.buildReadSegments(config);
|
||||
|
||||
// 断言
|
||||
assertEquals(1, segments.size());
|
||||
assertEquals(0, segments.get(0).getStartAddress());
|
||||
assertEquals(2, segments.get(0).getRegisterCount());
|
||||
assertIterableEquals(Arrays.asList(point01, point02), segments.get(0).getPoints());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testBuildReadSegments_notMergeDifferentFunctionCodeOrPollInterval() {
|
||||
// 准备参数
|
||||
IotModbusPointRespDTO point01 = randomPoint(1L, FC_READ_HOLDING_REGISTERS, 0, 1, 1000);
|
||||
IotModbusPointRespDTO point02 = randomPoint(2L, FC_READ_INPUT_REGISTERS, 1, 1, 1000);
|
||||
IotModbusPointRespDTO point03 = randomPoint(3L, FC_READ_HOLDING_REGISTERS, 1, 1, 2000);
|
||||
IotModbusDeviceConfigRespDTO config = randomConfig(point01, point02, point03);
|
||||
|
||||
// 调用
|
||||
List<ReadSegment> segments = IotModbusTcpClientPollScheduler.buildReadSegments(config);
|
||||
|
||||
// 断言
|
||||
assertEquals(3, segments.size());
|
||||
assertEquals(FC_READ_HOLDING_REGISTERS, segments.get(0).getFunctionCode());
|
||||
assertEquals(1000, segments.get(0).getPollInterval());
|
||||
assertEquals(FC_READ_HOLDING_REGISTERS, segments.get(1).getFunctionCode());
|
||||
assertEquals(2000, segments.get(1).getPollInterval());
|
||||
assertEquals(FC_READ_INPUT_REGISTERS, segments.get(2).getFunctionCode());
|
||||
assertEquals(1000, segments.get(2).getPollInterval());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testBuildReadSegments_splitWhenExceedsMaxRegisterCount() {
|
||||
// 准备参数
|
||||
IotModbusPointRespDTO point01 = randomPoint(1L, FC_READ_HOLDING_REGISTERS, 0, 100, 1000);
|
||||
IotModbusPointRespDTO point02 = randomPoint(2L, FC_READ_HOLDING_REGISTERS, 100, 30, 1000);
|
||||
IotModbusDeviceConfigRespDTO config = randomConfig(point01, point02);
|
||||
|
||||
// 调用
|
||||
List<ReadSegment> segments = IotModbusTcpClientPollScheduler.buildReadSegments(config);
|
||||
|
||||
// 断言
|
||||
assertEquals(2, segments.size());
|
||||
assertEquals(0, segments.get(0).getStartAddress());
|
||||
assertEquals(100, segments.get(0).getRegisterCount());
|
||||
assertEquals(100, segments.get(1).getStartAddress());
|
||||
assertEquals(30, segments.get(1).getRegisterCount());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testExtractPointRawValues() {
|
||||
// 准备参数
|
||||
IotModbusPointRespDTO point = randomPoint(1L, FC_READ_HOLDING_REGISTERS, 12, 2, 1000);
|
||||
ReadSegment segment = new ReadSegment(FC_READ_HOLDING_REGISTERS, 1000, 10, 4, Collections.singletonList(point));
|
||||
int[] rawValues = new int[]{100, 200, 300, 400};
|
||||
|
||||
// 调用
|
||||
int[] pointRawValues = IotModbusTcpClientPollScheduler.extractPointRawValues(rawValues, segment, point);
|
||||
|
||||
// 断言
|
||||
assertArrayEquals(new int[]{300, 400}, pointRawValues);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testExtractPointRawValuesForCoils() {
|
||||
// 准备参数
|
||||
IotModbusPointRespDTO point = randomPoint(1L, FC_READ_COILS, 3, 2, 1000);
|
||||
ReadSegment segment = new ReadSegment(FC_READ_COILS, 1000, 0, 5, Collections.singletonList(point));
|
||||
int[] rawValues = new int[]{1, 0, 1, 1, 0, 0, 0, 0}; // 线圈响应可能按字节补齐
|
||||
|
||||
// 调用
|
||||
int[] pointRawValues = IotModbusTcpClientPollScheduler.extractPointRawValues(rawValues, segment, point);
|
||||
|
||||
// 断言
|
||||
assertArrayEquals(new int[]{1, 0}, pointRawValues);
|
||||
}
|
||||
|
||||
private static IotModbusDeviceConfigRespDTO randomConfig(IotModbusPointRespDTO... points) {
|
||||
IotModbusDeviceConfigRespDTO config = new IotModbusDeviceConfigRespDTO();
|
||||
config.setDeviceId(1L);
|
||||
config.setPoints(Arrays.asList(points));
|
||||
return config;
|
||||
}
|
||||
|
||||
private static IotModbusPointRespDTO randomPoint(Long id, Integer functionCode, Integer registerAddress,
|
||||
Integer registerCount, Integer pollInterval) {
|
||||
IotModbusPointRespDTO point = new IotModbusPointRespDTO();
|
||||
point.setId(id);
|
||||
point.setFunctionCode(functionCode);
|
||||
point.setRegisterAddress(registerAddress);
|
||||
point.setRegisterCount(registerCount);
|
||||
point.setPollInterval(pollInterval);
|
||||
return point;
|
||||
}
|
||||
|
||||
}
|
||||
+10
@@ -52,4 +52,14 @@ public interface DeptApi {
|
||||
@Parameter(name = "id", description = "部门编号", example = "1024", required = true)
|
||||
CommonResult<List<DeptRespDTO>> getChildDeptList(@RequestParam("id") Long id);
|
||||
|
||||
@GetMapping(PREFIX + "/list-child-by-ids")
|
||||
@Operation(summary = "获得指定部门的所有子部门")
|
||||
@Parameter(name = "ids", description = "部门编号数组", example = "1,2", required = true)
|
||||
CommonResult<List<DeptRespDTO>> getChildDeptList(@RequestParam("ids") Collection<Long> ids);
|
||||
|
||||
@GetMapping(PREFIX + "/list-parent")
|
||||
@Operation(summary = "获得指定部门的所有父部门")
|
||||
@Parameter(name = "id", description = "部门编号", example = "1024", required = true)
|
||||
CommonResult<List<DeptRespDTO>> getParentDeptList(@RequestParam("id") Long id);
|
||||
|
||||
}
|
||||
|
||||
+12
@@ -45,4 +45,16 @@ public class DeptApiImpl implements DeptApi {
|
||||
return success(BeanUtils.toBean(depts, DeptRespDTO.class));
|
||||
}
|
||||
|
||||
@Override
|
||||
public CommonResult<List<DeptRespDTO>> getChildDeptList(Collection<Long> ids) {
|
||||
List<DeptDO> depts = deptService.getChildDeptList(ids);
|
||||
return success(BeanUtils.toBean(depts, DeptRespDTO.class));
|
||||
}
|
||||
|
||||
@Override
|
||||
public CommonResult<List<DeptRespDTO>> getParentDeptList(Long id) {
|
||||
List<DeptDO> depts = deptService.getParentDeptList(id);
|
||||
return success(BeanUtils.toBean(depts, DeptRespDTO.class));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+10
@@ -96,6 +96,16 @@ public interface DeptService {
|
||||
*/
|
||||
List<DeptDO> getChildDeptList(Collection<Long> ids);
|
||||
|
||||
/**
|
||||
* 获得指定部门的所有父部门
|
||||
*
|
||||
* 按直属父部门到根部门的顺序返回,不包含指定部门自身。
|
||||
*
|
||||
* @param id 部门编号
|
||||
* @return 父部门列表
|
||||
*/
|
||||
List<DeptDO> getParentDeptList(Long id);
|
||||
|
||||
/**
|
||||
* 获得指定领导者的部门列表
|
||||
*
|
||||
|
||||
+23
@@ -203,6 +203,29 @@ public class DeptServiceImpl implements DeptService {
|
||||
return children;
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<DeptDO> getParentDeptList(Long id) {
|
||||
List<DeptDO> parents = new ArrayList<>();
|
||||
Set<Long> visitedDeptIds = new HashSet<>();
|
||||
visitedDeptIds.add(id);
|
||||
DeptDO dept = getDept(id);
|
||||
// TODO DONE @AI:使用 Short.MAX_VALUE 限制父链遍历次数,并通过已访问集合提前结束脏数据环路。
|
||||
for (int i = 0; i < Short.MAX_VALUE; i++) {
|
||||
if (dept == null || dept.getParentId() == null
|
||||
|| ObjectUtil.equal(dept.getParentId(), DeptDO.PARENT_ID_ROOT)
|
||||
|| visitedDeptIds.contains(dept.getParentId())) {
|
||||
break;
|
||||
}
|
||||
visitedDeptIds.add(dept.getParentId());
|
||||
dept = getDept(dept.getParentId());
|
||||
if (dept == null) {
|
||||
break;
|
||||
}
|
||||
parents.add(dept);
|
||||
}
|
||||
return parents;
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<DeptDO> getDeptListByLeaderUserId(Long id) {
|
||||
return deptMapper.selectListByLeaderUserId(id);
|
||||
|
||||
Reference in New Issue
Block a user