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) { private DeviceInfoEntity requireRegisteredDevice(String deviceId) {
DeviceInfoEntity device = findDevice(deviceId); DeviceInfoEntity device = findDevice(deviceId);
if (device == null) { if (device == null) {
throw new RuntimeException("设备未注册,请先调用设备注册接口"); throw new RuntimeException("设备未注册");
} }
return device; return device;
} }

View File

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

View File

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

View File

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

View File

@ -217,7 +217,7 @@ export const MeetingCreateDrawer: React.FC<MeetingCreateDrawerProps> = ({
hotWordGroupId: defaultPrompt?.hotWordGroupId ?? 0, hotWordGroupId: defaultPrompt?.hotWordGroupId ?? 0,
summaryDetailLevel: "STANDARD", summaryDetailLevel: "STANDARD",
useSpkId: 1, useSpkId: 1,
enableTextRefine: false, enableTextRefine: true,
mode: "2pass", mode: "2pass",
language: "auto", language: "auto",
enablePunctuation: true, 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 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 { CheckCircleOutlined, CloudUploadOutlined, DeleteOutlined, DownloadOutlined, EditOutlined, LaptopOutlined, MobileOutlined, PlusOutlined, ReloadOutlined, RocketOutlined, SearchOutlined, UploadOutlined, WindowsOutlined } from "@ant-design/icons";
import { useCallback, useEffect, useMemo, useState } from "react"; 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>, () => Object.fromEntries(platformGroups.flatMap((group) => group.options.map((option) => [option.value, option]))) as Record<string, ClientPlatformOption>,
[platformGroups] [platformGroups]
); );
const platformTypeOptions = useMemo(
() => [{label: "全部类型", value: "all"}, ...platformGroups.map((group) => ({
label: group.label,
value: group.key
}))],
[platformGroups]
);
const loadData = useCallback(async () => { const loadData = useCallback(async () => {
setLoading(true); setLoading(true);
@ -447,12 +473,20 @@ export default function ClientManagement() {
</Row> </Row>
<Card className="app-page__filter-card mb-4" styles={{ body: { padding: 16 } }}> <Card className="app-page__filter-card mb-4" styles={{ body: { padding: 16 } }}>
<Space wrap style={{ width: "100%", justifyContent: "space-between" }}> <Space wrap style={{width: "100%"}}>
<Space wrap> <Input placeholder="搜索平台、版本、系统要求或下载地址" prefix={<SearchOutlined/>} allowClear
<Input placeholder="搜索平台、版本、系统要求或下载地址" prefix={<SearchOutlined />} allowClear style={{ width: 320 }} value={searchValue} onChange={(event) => setSearchValue(event.target.value)} /> 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 style={{width: 150}} value={statusFilter}
</Space> options={STATUS_FILTER_OPTIONS as unknown as { label: string; value: string }[]}
<Tabs activeKey={activeTab} onChange={setActiveTab} items={[{ key: "all", label: "全部" }, ...platformGroups.map((group) => ({ key: group.key, label: group.label }))]} /> onChange={(value) => setStatusFilter(value as typeof statusFilter)}/>
<Select
showSearch
optionFilterProp="label"
style={{width: 180}}
value={activeTab}
options={platformTypeOptions}
onChange={setActiveTab}
/>
</Space> </Space>
</Card> </Card>

View File

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

View File

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

View File

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

View File

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