refactor:优化设备列表样式和更新 Redis 支持

- 在 `devices/index.less` 中注释掉不必要的 CSS 规则
- 更新 `AndroidAuthServiceImpl` 和 `AndroidDeviceRegistrationServiceImpl` 中的异常信息和方法简化
- 在 `MeetingCreateDrawer.tsx` 中启用文本精炼功能
- 在 `devices/index.tsx` 中使用通用成功消息
- 在 `DeviceOnlineManagementServiceImpl` 中添加对终端类型的映射
- 更新 `ClientManagement.tsx` 中的平台类型选项
- 在 `MeetingPointsManagement.tsx` 中注释掉当前可用额度显示
- 在 `scan-confirm/index.tsx` 中更新登录确认消息
- 更新 `RedisSupport` 以使用 Lettuce 库并调整相关方法
dev_na
chenhao 2026-06-17 15:16:08 +08:00
parent f787f867bb
commit 7233f13598
10 changed files with 125 additions and 58 deletions

View File

@ -272,7 +272,7 @@ public class AndroidAuthServiceImpl implements AndroidAuthService {
private DeviceInfoEntity requireRegisteredDevice(String deviceId) {
DeviceInfoEntity device = findDevice(deviceId);
if (device == null) {
throw new RuntimeException("设备未注册,请先调用设备注册接口");
throw new RuntimeException("设备未注册");
}
return device;
}

View File

@ -94,7 +94,6 @@ public class AndroidDeviceRegistrationServiceImpl implements AndroidDeviceRegist
}
private String normalizeTerminalType(String value) {
String normalized = normalize(value);
return normalized == null ? null : normalized.toLowerCase();
return normalize(value);
}
}

View File

@ -1,5 +1,7 @@
package com.imeeting.service.biz.impl;
import cn.hutool.core.collection.CollectionUtil;
import cn.hutool.core.collection.ListUtil;
import com.imeeting.dto.android.AndroidAuthContext;
import com.imeeting.dto.android.AndroidDeviceSessionState;
import com.imeeting.dto.biz.DeviceAdminUpdateCommand;
@ -11,7 +13,9 @@ import com.imeeting.service.android.AndroidDeviceSessionService;
import com.imeeting.service.android.AndroidGatewayPushService;
import com.imeeting.service.biz.DeviceOnlineManagementService;
import com.imeeting.service.biz.LicenseService;
import com.unisbase.dto.SysDictItemDTO;
import com.unisbase.security.LoginUser;
import com.unisbase.service.SysDictItemService;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@ -20,7 +24,10 @@ import org.springframework.util.StringUtils;
import java.time.Instant;
import java.time.LocalDateTime;
import java.time.ZoneId;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
@Service
@RequiredArgsConstructor
@ -31,8 +38,9 @@ public class DeviceOnlineManagementServiceImpl implements DeviceOnlineManagement
private final AndroidGatewayPushService androidGatewayPushService;
private final AndroidDeviceBindingService androidDeviceBindingService;
private final LicenseService licenseService;
private final SysDictItemService sysDictItemService;
@Override
@Override
public void recordConnected(AndroidAuthContext authContext) {
if (authContext == null || !StringUtils.hasText(authContext.getDeviceId())) {
return;
@ -82,8 +90,19 @@ public class DeviceOnlineManagementServiceImpl implements DeviceOnlineManagement
@Override
public List<DeviceOnlineAdminVO> listForAdmin(LoginUser loginUser) {
List<DeviceOnlineAdminVO> devices = deviceInfoMapper.selectAdminList(loginUser == null ? null : loginUser.getTenantId(), isPlatformAdmin(loginUser));
for (DeviceOnlineAdminVO device : devices) {
List<SysDictItemDTO> clientPlatform = sysDictItemService.getItemsByTypeCode("client_platform");
Map<String, String> typeMap = new HashMap<>();
for (SysDictItemDTO sysDictItemDTO : clientPlatform) {
List<SysDictItemDTO> itemsByTypeCode = sysDictItemService.getItemsByTypeCode(sysDictItemDTO.getItemValue());
if (CollectionUtil.isNotEmpty(itemsByTypeCode)) {
typeMap.putAll(itemsByTypeCode.stream().collect(Collectors.toMap(SysDictItemDTO::getItemValue, SysDictItemDTO::getItemLabel)));
}
}
for (DeviceOnlineAdminVO device : devices) {
AndroidDeviceSessionState state = androidDeviceSessionService.getByDeviceId(device.getDeviceCode());
device.setTerminalType(typeMap.getOrDefault(device.getTerminalType(), device.getTerminalType()));
if (state != null) {
device.setOnline(true);
device.setLastOnlineAt(toLocalDateTime(state.getLastSeenAt()));

View File

@ -1,9 +1,11 @@
package com.imeeting.support;
import com.fasterxml.jackson.databind.ObjectMapper;
import io.lettuce.core.SetArgs;
import io.lettuce.core.api.StatefulRedisConnection;
import io.lettuce.core.api.sync.RedisCommands;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Component;
import java.time.Duration;
@ -14,12 +16,12 @@ import java.util.Collection;
@RequiredArgsConstructor
public class RedisSupport {
private final StringRedisTemplate redisTemplate;
private final StatefulRedisConnection<String, String> redisConnection;
private final ObjectMapper objectMapper;
public String getStringQuietly(String key) {
try {
return redisTemplate.opsForValue().get(key);
return commands().get(key);
} catch (Exception ex) {
log.warn("读取 Redis 字符串失败, key={}", key, ex);
return null;
@ -41,7 +43,7 @@ public class RedisSupport {
public void setString(String key, String value) {
try {
redisTemplate.opsForValue().set(key, value);
commands().set(key, value);
} catch (Exception ex) {
throw new RuntimeException("写入 Redis 字符串失败, key=" + key, ex);
}
@ -49,7 +51,7 @@ public class RedisSupport {
public void setString(String key, String value, Duration ttl) {
try {
redisTemplate.opsForValue().set(key, value, ttl);
commands().psetex(key, ttl.toMillis(), value);
} catch (Exception ex) {
throw new RuntimeException("写入 Redis 字符串失败, key=" + key, ex);
}
@ -65,8 +67,8 @@ public class RedisSupport {
public boolean setIfAbsentQuietly(String key, String value, Duration ttl) {
try {
Boolean success = redisTemplate.opsForValue().setIfAbsent(key, value, ttl);
return Boolean.TRUE.equals(success);
String result = commands().set(key, value, buildNxPxArgs(ttl));
return isOk(result);
} catch (Exception ex) {
log.warn("写入 Redis 锁失败, key={}", key, ex);
return false;
@ -75,8 +77,8 @@ public class RedisSupport {
public boolean setIfAbsentOrThrow(String key, String value, Duration ttl) {
try {
Boolean success = redisTemplate.opsForValue().setIfAbsent(key, value, ttl);
return Boolean.TRUE.equals(success);
String result = commands().set(key, value, buildNxPxArgs(ttl));
return isOk(result);
} catch (Exception ex) {
throw new RuntimeException("写入 Redis 锁失败, key=" + key, ex);
}
@ -84,15 +86,18 @@ public class RedisSupport {
public void deleteQuietly(String key) {
try {
redisTemplate.delete(key);
commands().del(key);
} catch (Exception ex) {
log.warn("删除 Redis Key 失败, key={}", key, ex);
}
}
public void deleteQuietly(Collection<String> keys) {
if (keys == null || keys.isEmpty()) {
return;
}
try {
redisTemplate.delete(keys);
commands().del(keys.toArray(String[]::new));
} catch (Exception ex) {
log.warn("批量删除 Redis Key 失败, keys={}", keys, ex);
}
@ -103,7 +108,7 @@ public class RedisSupport {
return;
}
try {
redisTemplate.opsForSet().remove(key, (Object[]) members);
commands().srem(key, members);
} catch (Exception ex) {
log.warn("从 Redis Set 删除成员失败, key={}", key, ex);
}
@ -114,8 +119,7 @@ public class RedisSupport {
return false;
}
try {
Long added = redisTemplate.opsForSet().add(key, member);
return added != null && added > 0;
return commands().sadd(key, member) > 0;
} catch (Exception ex) {
log.warn("add Redis set member failed, key={}", key, ex);
return false;
@ -127,8 +131,7 @@ public class RedisSupport {
return false;
}
try {
Boolean memberPresent = redisTemplate.opsForSet().isMember(key, member);
return Boolean.TRUE.equals(memberPresent);
return Boolean.TRUE.equals(commands().sismember(key, member));
} catch (Exception ex) {
log.warn("check Redis set member failed, key={}", key, ex);
return false;
@ -137,7 +140,7 @@ public class RedisSupport {
public long getSetSizeQuietly(String key) {
try {
Long size = redisTemplate.opsForSet().size(key);
Long size = commands().scard(key);
return size == null ? 0L : size;
} catch (Exception ex) {
log.warn("read Redis set size failed, key={}", key, ex);
@ -145,6 +148,18 @@ public class RedisSupport {
}
}
private RedisCommands<String, String> commands() {
return redisConnection.sync();
}
private SetArgs buildNxPxArgs(Duration ttl) {
return SetArgs.Builder.nx().px(ttl.toMillis());
}
private boolean isOk(String result) {
return "OK".equalsIgnoreCase(result);
}
private String writeJson(Object value) {
try {
return objectMapper.writeValueAsString(value);

View File

@ -217,7 +217,7 @@ export const MeetingCreateDrawer: React.FC<MeetingCreateDrawerProps> = ({
hotWordGroupId: defaultPrompt?.hotWordGroupId ?? 0,
summaryDetailLevel: "STANDARD",
useSpkId: 1,
enableTextRefine: false,
enableTextRefine: true,
mode: "2pass",
language: "auto",
enablePunctuation: true,

View File

@ -1,4 +1,23 @@
import { App, Button, Card, Col, Drawer, Empty, Form, Input, InputNumber, Popconfirm, Row, Select, Space, Switch, Table, Tabs, Tag, Typography, Upload } from "antd";
import {
App,
Button,
Card,
Col,
Drawer,
Empty,
Form,
Input,
InputNumber,
Popconfirm,
Row,
Select,
Space,
Switch,
Table,
Tag,
Typography,
Upload
} from "antd";
import type { ColumnsType } from "antd/es/table";
import { CheckCircleOutlined, CloudUploadOutlined, DeleteOutlined, DownloadOutlined, EditOutlined, LaptopOutlined, MobileOutlined, PlusOutlined, ReloadOutlined, RocketOutlined, SearchOutlined, UploadOutlined, WindowsOutlined } from "@ant-design/icons";
import { useCallback, useEffect, useMemo, useState } from "react";
@ -155,6 +174,13 @@ export default function ClientManagement() {
() => Object.fromEntries(platformGroups.flatMap((group) => group.options.map((option) => [option.value, option]))) as Record<string, ClientPlatformOption>,
[platformGroups]
);
const platformTypeOptions = useMemo(
() => [{label: "全部类型", value: "all"}, ...platformGroups.map((group) => ({
label: group.label,
value: group.key
}))],
[platformGroups]
);
const loadData = useCallback(async () => {
setLoading(true);
@ -447,12 +473,20 @@ export default function ClientManagement() {
</Row>
<Card className="app-page__filter-card mb-4" styles={{ body: { padding: 16 } }}>
<Space wrap style={{ width: "100%", justifyContent: "space-between" }}>
<Space wrap>
<Input placeholder="搜索平台、版本、系统要求或下载地址" prefix={<SearchOutlined />} allowClear style={{ width: 320 }} value={searchValue} onChange={(event) => setSearchValue(event.target.value)} />
<Select style={{ width: 150 }} value={statusFilter} options={STATUS_FILTER_OPTIONS as unknown as { label: string; value: string }[]} onChange={(value) => setStatusFilter(value as typeof statusFilter)} />
</Space>
<Tabs activeKey={activeTab} onChange={setActiveTab} items={[{ key: "all", label: "全部" }, ...platformGroups.map((group) => ({ key: group.key, label: group.label }))]} />
<Space wrap style={{width: "100%"}}>
<Input placeholder="搜索平台、版本、系统要求或下载地址" prefix={<SearchOutlined/>} allowClear
style={{width: 320}} value={searchValue} onChange={(event) => setSearchValue(event.target.value)}/>
<Select style={{width: 150}} value={statusFilter}
options={STATUS_FILTER_OPTIONS as unknown as { label: string; value: string }[]}
onChange={(value) => setStatusFilter(value as typeof statusFilter)}/>
<Select
showSearch
optionFilterProp="label"
style={{width: 180}}
value={activeTab}
options={platformTypeOptions}
onChange={setActiveTab}
/>
</Space>
</Card>

View File

@ -101,12 +101,12 @@ function buildSummaryCards(overview: MeetingPointsOverviewVO | null) {
value: number | string;
note: string;
}> = [
{
key: "available-balance",
title: "当前可用额度",
value: isUnlimitedBalanceMode ? "无限" : (overview.totalAvailableBalance ?? 0),
note: isUnlimitedBalanceMode ? "关闭余额校验后只记录消耗和流水" : "按当前账户模式计算的可用额度",
},
// {
// key: "available-balance",
// title: "当前可用额度",
// value: isUnlimitedBalanceMode ? "无限" : (overview.totalAvailableBalance ?? 0),
// note: isUnlimitedBalanceMode ? "关闭余额校验后只记录消耗和流水" : "按当前账户模式计算的可用额度",
// },
{
key: "charge-count",
title: "累计消耗次数",
@ -493,20 +493,20 @@ export default function MeetingPointsManagement() {
<Text strong style={{ fontSize: 16 }}>
</Text>
<Space wrap>
<Tag color="processing" bordered={false}>
{getAccountModeLabel(overview?.accountMode)}
</Tag>
<Tag color="blue" bordered={false}>
{getChargePriorityLabel(overview?.chargePriority)}
</Tag>
<Tag color={isUnlimitedBalanceMode ? "volcano" : "green"} bordered={false}>
{isUnlimitedBalanceMode ? "无限余额模式" : "校验余额模式"}
</Tag>
<Tag color={isAdmin ? "gold" : "default"} bordered={false}>
{isAdmin ? "管理员视角" : "当前用户视角"}
</Tag>
</Space>
{/*<Space wrap>*/}
{/* <Tag color="processing" bordered={false}>*/}
{/* 模式:{getAccountModeLabel(overview?.accountMode)}*/}
{/* </Tag>*/}
{/* <Tag color="blue" bordered={false}>*/}
{/* 优先级:{getChargePriorityLabel(overview?.chargePriority)}*/}
{/* </Tag>*/}
{/* <Tag color={isUnlimitedBalanceMode ? "volcano" : "green"} bordered={false}>*/}
{/* {isUnlimitedBalanceMode ? "无限余额模式" : "校验余额模式"}*/}
{/* </Tag>*/}
{/* <Tag color={isAdmin ? "gold" : "default"} bordered={false}>*/}
{/* {isAdmin ? "管理员视角" : "当前用户视角"}*/}
{/* </Tag>*/}
{/*</Space>*/}
</div>

View File

@ -175,10 +175,10 @@
font-variant-numeric: tabular-nums;
}
.app-page__table-wrap .ant-table-wrapper .ant-table-cell-fix-right-first,
.app-page__table-wrap .ant-table-wrapper .ant-table-cell-fix-right-last {
right: 0 !important;
}
//.app-page__table-wrap .ant-table-wrapper .ant-table-cell-fix-right-first,
//.app-page__table-wrap .ant-table-wrapper .ant-table-cell-fix-right-last {
// right: 0 !important;
//}
@media (max-width: 768px) {
.devices-page {

View File

@ -117,13 +117,13 @@ export default function Devices() {
const remove = async (record: DeviceInfo) => {
await deleteManagedDevice(record.deviceId);
message.success(t("devicesExt.deleteSucceeded"));
message.success(t("common.success"));
await loadData();
};
const resetStats = async (record: DeviceInfo) => {
await resetManagedDeviceStats(record.deviceId);
message.success(t("devicesExt.resetStatsSucceeded"));
message.success(t("common.success"));
await loadData();
};

View File

@ -52,7 +52,7 @@ export default function ScanConfirmPage() {
<Result
status="success"
title="登录确认已发送"
subTitle="安卓设备收到确认消息后,会继续按离线发会流程处理后续动作。"
subTitle="设备收到确认消息后,会开启会议。"
extra={
<Button type="primary" onClick={() => navigate("/meetings")}>