feat: 添加授权码管理和设备自注册功能
- 新增 `AndroidTenantProviderConfig` 配置类,提供租户解析逻辑 - 更新 `AndroidAuthService` 接口,添加 `authenticateHttp` 方法的可选参数 - 新增前端授权码管理页面 `LicenseManagement`,支持授权码列表展示和导入 - 新增 `AndroidDeviceRegistrationServiceImpl` 的 `register` 方法,支持设备自注册并验证租户代码 - 更新 `AndroidAuthServiceImpl`,在认证过程中应用授权信息 - 更新相关 DTO 和接口,支持新的授权和设备注册逻辑dev_na
parent
d47a66febd
commit
1cce0aeabb
|
|
@ -0,0 +1,9 @@
|
||||||
|
package com.imeeting.common;
|
||||||
|
|
||||||
|
public final class LicenseConstants {
|
||||||
|
private LicenseConstants() {
|
||||||
|
}
|
||||||
|
|
||||||
|
public static final String TEMP_SERIAL_PREFIX = "SN";
|
||||||
|
public static final String IMPORT_BATCH_PREFIX = "LIC";
|
||||||
|
}
|
||||||
|
|
@ -123,6 +123,10 @@ public final class RedisKeys {
|
||||||
return "biz:meeting:android:draft:" + meetingId;
|
return "biz:meeting:android:draft:" + meetingId;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static String androidDeviceWeatherKey(String cityName) {
|
||||||
|
return "biz:android:device:weather:" + cityName;
|
||||||
|
}
|
||||||
|
|
||||||
public static final String CACHE_EMPTY_MARKER = "EMPTY_MARKER";
|
public static final String CACHE_EMPTY_MARKER = "EMPTY_MARKER";
|
||||||
public static final String SYS_PARAM_FIELD_VALUE = "value";
|
public static final String SYS_PARAM_FIELD_VALUE = "value";
|
||||||
public static final String SYS_PARAM_FIELD_TYPE = "type";
|
public static final String SYS_PARAM_FIELD_TYPE = "type";
|
||||||
|
|
|
||||||
|
|
@ -21,4 +21,9 @@ public final class SysParamKeys {
|
||||||
public static final String MEETING_POINTS_LLM_RATIO = "meeting.points.llm_ratio";
|
public static final String MEETING_POINTS_LLM_RATIO = "meeting.points.llm_ratio";
|
||||||
public static final String MEETING_POINTS_INITIAL_BALANCE = "meeting.points.initial_balance";
|
public static final String MEETING_POINTS_INITIAL_BALANCE = "meeting.points.initial_balance";
|
||||||
public static final String MEETING_POINTS_ACCOUNT_MODE = "meeting.points.account_mode";
|
public static final String MEETING_POINTS_ACCOUNT_MODE = "meeting.points.account_mode";
|
||||||
|
public static final String LICENSE_TEMP_DEFAULT_COUNT = "license.temp.default.count";
|
||||||
|
public static final String LICENSE_TEMP_DEFAULT_EXPIRE_MONTHS = "license.temp.default.expire.months";
|
||||||
|
public static final String LICENSE_DEFAULT_PRODUCT_CODE = "license.default.product.code";
|
||||||
|
public static final String DEVICE_WEATHER_QWEATHER_BASE_URL = "device.weather.qweather.base_url";
|
||||||
|
public static final String DEVICE_WEATHER_QWEATHER_KEY = "device.weather.qweather.key";
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,116 @@
|
||||||
|
package com.imeeting.config;
|
||||||
|
|
||||||
|
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||||
|
import com.unisbase.config.properties.UnisBaseProperties;
|
||||||
|
import com.unisbase.entity.SysTenant;
|
||||||
|
import com.unisbase.mapper.SysTenantMapper;
|
||||||
|
import com.unisbase.security.SpringSecurityTenantProvider;
|
||||||
|
import com.unisbase.spi.UnisBaseTenantProvider;
|
||||||
|
import jakarta.servlet.http.HttpServletRequest;
|
||||||
|
import org.springframework.beans.factory.ObjectProvider;
|
||||||
|
import org.springframework.context.annotation.Bean;
|
||||||
|
import org.springframework.context.annotation.Configuration;
|
||||||
|
import org.springframework.security.core.Authentication;
|
||||||
|
import org.springframework.security.core.context.SecurityContextHolder;
|
||||||
|
import org.springframework.util.StringUtils;
|
||||||
|
import org.springframework.web.context.request.RequestContextHolder;
|
||||||
|
import org.springframework.web.context.request.ServletRequestAttributes;
|
||||||
|
|
||||||
|
@Configuration
|
||||||
|
public class AndroidTenantProviderConfig {
|
||||||
|
|
||||||
|
private static final String TENANT_ID_HEADER = "X-Tenant-Id";
|
||||||
|
private static final String TENANT_CODE_HEADER = "X-Tenant-Code";
|
||||||
|
|
||||||
|
@Bean
|
||||||
|
public UnisBaseTenantProvider unisBaseTenantProvider(UnisBaseProperties properties,
|
||||||
|
ObjectProvider<SysTenantMapper> sysTenantMapperProvider) {
|
||||||
|
return new AndroidTenantProvider(properties, sysTenantMapperProvider);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static final class AndroidTenantProvider extends SpringSecurityTenantProvider {
|
||||||
|
private final UnisBaseProperties properties;
|
||||||
|
private final ObjectProvider<SysTenantMapper> sysTenantMapperProvider;
|
||||||
|
|
||||||
|
private AndroidTenantProvider(UnisBaseProperties properties, ObjectProvider<SysTenantMapper> sysTenantMapperProvider) {
|
||||||
|
super(properties);
|
||||||
|
this.properties = properties;
|
||||||
|
this.sysTenantMapperProvider = sysTenantMapperProvider;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Long getCurrentTenantId() {
|
||||||
|
Authentication auth = SecurityContextHolder.getContext().getAuthentication();
|
||||||
|
if (auth != null && auth.getPrincipal() instanceof com.unisbase.security.LoginUser user && user.getTenantId() != null) {
|
||||||
|
return user.getTenantId();
|
||||||
|
}
|
||||||
|
|
||||||
|
Long tenantId = parseTenantIdHeader();
|
||||||
|
if (tenantId != null) {
|
||||||
|
return tenantId;
|
||||||
|
}
|
||||||
|
|
||||||
|
String tenantCode = header(TENANT_CODE_HEADER);
|
||||||
|
if (StringUtils.hasText(tenantCode)) {
|
||||||
|
Long resolvedTenantId = resolveTenantIdByCode(tenantCode.trim());
|
||||||
|
if (resolvedTenantId != null) {
|
||||||
|
return resolvedTenantId;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isSingleTenantMode()) {
|
||||||
|
Long defaultTenantId = properties.getTenant().getDefaultTenantId();
|
||||||
|
return defaultTenantId == null || defaultTenantId <= 0 ? 1L : defaultTenantId;
|
||||||
|
}
|
||||||
|
return 0L;
|
||||||
|
}
|
||||||
|
|
||||||
|
private Long parseTenantIdHeader() {
|
||||||
|
String value = header(TENANT_ID_HEADER);
|
||||||
|
if (!StringUtils.hasText(value)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
return Long.valueOf(value.trim());
|
||||||
|
} catch (NumberFormatException ex) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private Long resolveTenantIdByCode(String tenantCode) {
|
||||||
|
SysTenantMapper sysTenantMapper = sysTenantMapperProvider.getIfAvailable();
|
||||||
|
if (sysTenantMapper == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
SysTenant tenant = sysTenantMapper.selectOne(new LambdaQueryWrapper<SysTenant>()
|
||||||
|
.eq(SysTenant::getTenantCode, tenantCode)
|
||||||
|
.eq(SysTenant::getIsDeleted, 0)
|
||||||
|
.last("LIMIT 1"));
|
||||||
|
return tenant == null ? null : tenant.getId();
|
||||||
|
} catch (Exception ex) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private String header(String name) {
|
||||||
|
ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
|
||||||
|
if (attributes == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
HttpServletRequest request = attributes.getRequest();
|
||||||
|
return request == null ? null : request.getHeader(name);
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean isSingleTenantMode() {
|
||||||
|
if (properties == null || properties.getTenant() == null) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
String mode = properties.getTenant().getMode();
|
||||||
|
if (mode != null && !mode.isBlank()) {
|
||||||
|
return "single".equalsIgnoreCase(mode.trim());
|
||||||
|
}
|
||||||
|
return !properties.getTenant().isEnabled();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -67,6 +67,11 @@ public class AndroidAuthController {
|
||||||
appVersion,
|
appVersion,
|
||||||
platform
|
platform
|
||||||
);
|
);
|
||||||
|
androidDeviceBindingService.recordLogin(
|
||||||
|
deviceId.trim(),
|
||||||
|
response.getCurrentTenantId(),
|
||||||
|
response.getUser().getUserId()
|
||||||
|
);
|
||||||
}
|
}
|
||||||
return ApiResponse.ok(response);
|
return ApiResponse.ok(response);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,7 @@ import com.imeeting.support.AndroidRequestLogHelper;
|
||||||
import com.imeeting.entity.biz.ClientDownload;
|
import com.imeeting.entity.biz.ClientDownload;
|
||||||
import com.imeeting.service.android.AndroidAuthService;
|
import com.imeeting.service.android.AndroidAuthService;
|
||||||
import com.imeeting.service.biz.ClientDownloadService;
|
import com.imeeting.service.biz.ClientDownloadService;
|
||||||
|
import com.unisbase.annotation.Anonymous;
|
||||||
import com.unisbase.common.ApiResponse;
|
import com.unisbase.common.ApiResponse;
|
||||||
import io.swagger.v3.oas.annotations.media.Content;
|
import io.swagger.v3.oas.annotations.media.Content;
|
||||||
import io.swagger.v3.oas.annotations.media.Schema;
|
import io.swagger.v3.oas.annotations.media.Schema;
|
||||||
|
|
@ -38,6 +39,7 @@ public class AndroidClientController {
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
@GetMapping("/latest/by-platform")
|
@GetMapping("/latest/by-platform")
|
||||||
|
@Anonymous
|
||||||
public ApiResponse<ClientDownload> latestByPlatform(HttpServletRequest request,
|
public ApiResponse<ClientDownload> latestByPlatform(HttpServletRequest request,
|
||||||
@RequestParam(value = "platform_code", required = false) String platformCode,
|
@RequestParam(value = "platform_code", required = false) String platformCode,
|
||||||
@RequestParam(value = "platform_type", required = false) String platformType,
|
@RequestParam(value = "platform_type", required = false) String platformType,
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,11 @@
|
||||||
package com.imeeting.controller.android;
|
package com.imeeting.controller.android;
|
||||||
|
|
||||||
import com.imeeting.dto.android.AndroidAuthContext;
|
import com.imeeting.dto.android.AndroidAuthContext;
|
||||||
|
import com.imeeting.dto.android.AndroidDeviceHomeStatsVO;
|
||||||
import com.imeeting.dto.android.AndroidDeviceRegisterRequest;
|
import com.imeeting.dto.android.AndroidDeviceRegisterRequest;
|
||||||
import com.imeeting.dto.android.AndroidDeviceRegisterResponse;
|
import com.imeeting.dto.android.AndroidDeviceRegisterResponse;
|
||||||
import com.imeeting.service.android.AndroidAuthService;
|
import com.imeeting.service.android.AndroidAuthService;
|
||||||
|
import com.imeeting.service.android.AndroidDeviceHomeService;
|
||||||
import com.imeeting.service.android.AndroidDeviceRegistrationService;
|
import com.imeeting.service.android.AndroidDeviceRegistrationService;
|
||||||
import com.imeeting.support.AndroidRequestLogHelper;
|
import com.imeeting.support.AndroidRequestLogHelper;
|
||||||
import com.unisbase.annotation.Anonymous;
|
import com.unisbase.annotation.Anonymous;
|
||||||
|
|
@ -16,6 +18,8 @@ import io.swagger.v3.oas.annotations.tags.Tag;
|
||||||
import jakarta.servlet.http.HttpServletRequest;
|
import jakarta.servlet.http.HttpServletRequest;
|
||||||
import lombok.RequiredArgsConstructor;
|
import lombok.RequiredArgsConstructor;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.springframework.util.StringUtils;
|
||||||
|
import org.springframework.web.bind.annotation.GetMapping;
|
||||||
import org.springframework.web.bind.annotation.PostMapping;
|
import org.springframework.web.bind.annotation.PostMapping;
|
||||||
import org.springframework.web.bind.annotation.RequestBody;
|
import org.springframework.web.bind.annotation.RequestBody;
|
||||||
import org.springframework.web.bind.annotation.RequestMapping;
|
import org.springframework.web.bind.annotation.RequestMapping;
|
||||||
|
|
@ -27,8 +31,11 @@ import org.springframework.web.bind.annotation.RestController;
|
||||||
@RequiredArgsConstructor
|
@RequiredArgsConstructor
|
||||||
@Slf4j
|
@Slf4j
|
||||||
public class AndroidDeviceController {
|
public class AndroidDeviceController {
|
||||||
|
private static final String TENANT_CODE_HEADER = "X-Tenant-Code";
|
||||||
|
|
||||||
private final AndroidAuthService androidAuthService;
|
private final AndroidAuthService androidAuthService;
|
||||||
private final AndroidDeviceRegistrationService androidDeviceRegistrationService;
|
private final AndroidDeviceRegistrationService androidDeviceRegistrationService;
|
||||||
|
private final AndroidDeviceHomeService androidDeviceHomeService;
|
||||||
|
|
||||||
@Operation(summary = "设备自注册")
|
@Operation(summary = "设备自注册")
|
||||||
@ApiResponses({
|
@ApiResponses({
|
||||||
|
|
@ -42,14 +49,46 @@ public class AndroidDeviceController {
|
||||||
@Anonymous
|
@Anonymous
|
||||||
public ApiResponse<AndroidDeviceRegisterResponse> register(HttpServletRequest request,
|
public ApiResponse<AndroidDeviceRegisterResponse> register(HttpServletRequest request,
|
||||||
@RequestBody(required = false) AndroidDeviceRegisterRequest command) {
|
@RequestBody(required = false) AndroidDeviceRegisterRequest command) {
|
||||||
AndroidRequestLogHelper.logRequest(log, "Android设备", "设备自注册", "request", command);
|
if (command == null) {
|
||||||
|
throw new IllegalArgumentException("注册请求不能为空");
|
||||||
|
}
|
||||||
|
String tenantCode = resolveTenantCode(request, command);
|
||||||
|
AndroidRequestLogHelper.logRequest(log, "Android设备", "设备自注册", "request", command, "tenantCode", tenantCode);
|
||||||
AndroidAuthContext authContext = androidAuthService.authenticateHttp(request, false);
|
AndroidAuthContext authContext = androidAuthService.authenticateHttp(request, false);
|
||||||
AndroidDeviceRegisterResponse response = androidDeviceRegistrationService.register(
|
AndroidDeviceRegisterResponse response = androidDeviceRegistrationService.register(
|
||||||
|
tenantCode,
|
||||||
authContext.getDeviceId(),
|
authContext.getDeviceId(),
|
||||||
command == null ? null : command.getDeviceName(),
|
command.getDeviceName(),
|
||||||
command == null ? authContext.getPlatform() : command.getTerminalType(),
|
command.getTerminalType() == null ? authContext.getPlatform() : command.getTerminalType(),
|
||||||
command == null ? authContext.getAppVersion() : command.getTerminalVersion()
|
command.getTerminalVersion() == null ? authContext.getAppVersion() : command.getTerminalVersion()
|
||||||
);
|
);
|
||||||
return ApiResponse.ok(response);
|
return ApiResponse.ok(response);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Operation(summary = "查询设备首页统计")
|
||||||
|
@ApiResponses({
|
||||||
|
@io.swagger.v3.oas.annotations.responses.ApiResponse(
|
||||||
|
responseCode = "200",
|
||||||
|
description = "返回设备首页统计信息",
|
||||||
|
content = @Content(schema = @Schema(implementation = AndroidDeviceHomeStatsVO.class))
|
||||||
|
)
|
||||||
|
})
|
||||||
|
@GetMapping("/home")
|
||||||
|
@Anonymous
|
||||||
|
public ApiResponse<AndroidDeviceHomeStatsVO> home(HttpServletRequest request) {
|
||||||
|
AndroidRequestLogHelper.logRequest(log, "Android设备", "查询设备首页统计");
|
||||||
|
AndroidAuthContext authContext = androidAuthService.authenticateHttp(request, true, true);
|
||||||
|
return ApiResponse.ok(androidDeviceHomeService.getHomeStats(authContext));
|
||||||
|
}
|
||||||
|
|
||||||
|
private String resolveTenantCode(HttpServletRequest request, AndroidDeviceRegisterRequest command) {
|
||||||
|
if (command != null && StringUtils.hasText(command.getTenantCode())) {
|
||||||
|
return command.getTenantCode().trim();
|
||||||
|
}
|
||||||
|
String tenantCodeFromHeader = request == null ? null : request.getHeader(TENANT_CODE_HEADER);
|
||||||
|
if (StringUtils.hasText(tenantCodeFromHeader)) {
|
||||||
|
return tenantCodeFromHeader.trim();
|
||||||
|
}
|
||||||
|
throw new IllegalArgumentException("tenantCode不能为空");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -14,6 +14,7 @@ import io.swagger.v3.oas.annotations.responses.ApiResponses;
|
||||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||||
import lombok.RequiredArgsConstructor;
|
import lombok.RequiredArgsConstructor;
|
||||||
import org.springframework.security.core.context.SecurityContextHolder;
|
import org.springframework.security.core.context.SecurityContextHolder;
|
||||||
|
import org.springframework.web.bind.annotation.DeleteMapping;
|
||||||
import org.springframework.web.bind.annotation.GetMapping;
|
import org.springframework.web.bind.annotation.GetMapping;
|
||||||
import org.springframework.web.bind.annotation.PathVariable;
|
import org.springframework.web.bind.annotation.PathVariable;
|
||||||
import org.springframework.web.bind.annotation.PostMapping;
|
import org.springframework.web.bind.annotation.PostMapping;
|
||||||
|
|
@ -58,6 +59,18 @@ public class DeviceManagementController {
|
||||||
return ApiResponse.ok(deviceOnlineManagementService.kick(id, currentLoginUser()));
|
return ApiResponse.ok(deviceOnlineManagementService.kick(id, currentLoginUser()));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Operation(summary = "删除设备并解绑授权")
|
||||||
|
@DeleteMapping("/{id}")
|
||||||
|
public ApiResponse<Boolean> delete(@PathVariable Long id) {
|
||||||
|
return ApiResponse.ok(deviceOnlineManagementService.delete(id, currentLoginUser()));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Operation(summary = "重置设备首页统计")
|
||||||
|
@PostMapping("/{id}/reset")
|
||||||
|
public ApiResponse<Boolean> reset(@PathVariable Long id) {
|
||||||
|
return ApiResponse.ok(deviceOnlineManagementService.resetStats(id, currentLoginUser()));
|
||||||
|
}
|
||||||
|
|
||||||
private LoginUser currentLoginUser() {
|
private LoginUser currentLoginUser() {
|
||||||
return (LoginUser) SecurityContextHolder.getContext().getAuthentication().getPrincipal();
|
return (LoginUser) SecurityContextHolder.getContext().getAuthentication().getPrincipal();
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,45 @@
|
||||||
|
package com.imeeting.controller.biz;
|
||||||
|
|
||||||
|
import com.imeeting.dto.biz.LicenseImportResultVO;
|
||||||
|
import com.imeeting.dto.biz.LicenseVO;
|
||||||
|
import com.imeeting.service.biz.LicenseService;
|
||||||
|
import com.unisbase.common.ApiResponse;
|
||||||
|
import com.unisbase.security.LoginUser;
|
||||||
|
import io.swagger.v3.oas.annotations.Operation;
|
||||||
|
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import org.springframework.security.core.context.SecurityContextHolder;
|
||||||
|
import org.springframework.web.bind.annotation.GetMapping;
|
||||||
|
import org.springframework.web.bind.annotation.PostMapping;
|
||||||
|
import org.springframework.web.bind.annotation.RequestMapping;
|
||||||
|
import org.springframework.web.bind.annotation.RequestParam;
|
||||||
|
import org.springframework.web.bind.annotation.RestController;
|
||||||
|
import org.springframework.web.multipart.MultipartFile;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
@Tag(name = "租户授权管理")
|
||||||
|
@RestController
|
||||||
|
@RequestMapping("/api/admin/licenses")
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class LicenseManagementController {
|
||||||
|
|
||||||
|
private final LicenseService licenseService;
|
||||||
|
|
||||||
|
@Operation(summary = "查询当前租户授权列表")
|
||||||
|
@GetMapping
|
||||||
|
public ApiResponse<List<LicenseVO>> list() {
|
||||||
|
return ApiResponse.ok(licenseService.listCurrentTenantLicenses(currentLoginUser()));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Operation(summary = "导入当前租户正式授权")
|
||||||
|
@PostMapping("/import")
|
||||||
|
public ApiResponse<LicenseImportResultVO> importLicenses(@RequestParam("file") MultipartFile file) throws IOException {
|
||||||
|
return ApiResponse.ok(licenseService.importFormalLicenses(file, currentLoginUser()));
|
||||||
|
}
|
||||||
|
|
||||||
|
private LoginUser currentLoginUser() {
|
||||||
|
return (LoginUser) SecurityContextHolder.getContext().getAuthentication().getPrincipal();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,36 @@
|
||||||
|
package com.imeeting.dto.android;
|
||||||
|
|
||||||
|
import io.swagger.v3.oas.annotations.media.Schema;
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
@Data
|
||||||
|
@Schema(description = "Android 设备首页统计响应")
|
||||||
|
public class AndroidDeviceHomeStatsVO {
|
||||||
|
|
||||||
|
@Schema(description = "设备名称")
|
||||||
|
private String deviceName;
|
||||||
|
|
||||||
|
@Schema(description = "授权类型:1-临时,2-正式")
|
||||||
|
private Integer licenseType;
|
||||||
|
|
||||||
|
@Schema(description = "剩余分钟数")
|
||||||
|
private Long remainingMinutes;
|
||||||
|
|
||||||
|
@Schema(description = "会议数量")
|
||||||
|
private Long meetingCount;
|
||||||
|
|
||||||
|
@Schema(description = "会议总时长,单位分钟")
|
||||||
|
private Long meetingDurationMinutes;
|
||||||
|
|
||||||
|
@Schema(description = "登录用户数")
|
||||||
|
private Long loginUserCount;
|
||||||
|
|
||||||
|
@Schema(description = "H5 地址")
|
||||||
|
private String h5BaseUrl;
|
||||||
|
|
||||||
|
@Schema(description = "天气信息")
|
||||||
|
private AndroidDeviceHomeWeatherVO weather;
|
||||||
|
|
||||||
|
@Schema(description = "是否已登录")
|
||||||
|
private Boolean loggedIn;
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,18 @@
|
||||||
|
package com.imeeting.dto.android;
|
||||||
|
|
||||||
|
import io.swagger.v3.oas.annotations.media.Schema;
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
@Data
|
||||||
|
@Schema(description = "Android 设备首页天气信息")
|
||||||
|
public class AndroidDeviceHomeWeatherVO {
|
||||||
|
|
||||||
|
@Schema(description = "城市名称")
|
||||||
|
private String cityName;
|
||||||
|
|
||||||
|
@Schema(description = "天气文本")
|
||||||
|
private String text;
|
||||||
|
|
||||||
|
@Schema(description = "温度,单位摄氏度")
|
||||||
|
private String temperature;
|
||||||
|
}
|
||||||
|
|
@ -6,6 +6,9 @@ import lombok.Data;
|
||||||
@Data
|
@Data
|
||||||
@Schema(description = "Android设备注册请求")
|
@Schema(description = "Android设备注册请求")
|
||||||
public class AndroidDeviceRegisterRequest {
|
public class AndroidDeviceRegisterRequest {
|
||||||
|
@Schema(description = "租户编码", requiredMode = Schema.RequiredMode.REQUIRED)
|
||||||
|
private String tenantCode;
|
||||||
|
|
||||||
@Schema(description = "设备名称")
|
@Schema(description = "设备名称")
|
||||||
private String deviceName;
|
private String deviceName;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -20,4 +20,7 @@ public class AndroidDeviceRegisterResponse {
|
||||||
|
|
||||||
@Schema(description = "是否已被用户占用")
|
@Schema(description = "是否已被用户占用")
|
||||||
private Boolean occupied;
|
private Boolean occupied;
|
||||||
|
|
||||||
|
@Schema(description = "授权类型:1-临时,2-正式")
|
||||||
|
private Integer licenseType;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,10 @@
|
||||||
|
package com.imeeting.dto.android;
|
||||||
|
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
@Data
|
||||||
|
public class AndroidDeviceWeatherCacheValue {
|
||||||
|
private String cityName;
|
||||||
|
private String text;
|
||||||
|
private String temperature;
|
||||||
|
}
|
||||||
|
|
@ -12,4 +12,7 @@ public class DeviceAdminUpdateCommand {
|
||||||
|
|
||||||
@Schema(description = "状态:1启用,0停用")
|
@Schema(description = "状态:1启用,0停用")
|
||||||
private Integer status;
|
private Integer status;
|
||||||
|
|
||||||
|
@Schema(description = "设备所在天气城市")
|
||||||
|
private String weatherCityName;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -39,6 +39,12 @@ public class DeviceOnlineAdminVO {
|
||||||
@Schema(description = "最后一次在线时间")
|
@Schema(description = "最后一次在线时间")
|
||||||
private LocalDateTime lastOnlineAt;
|
private LocalDateTime lastOnlineAt;
|
||||||
|
|
||||||
|
@Schema(description = "统计重置时间")
|
||||||
|
private LocalDateTime statsResetAt;
|
||||||
|
|
||||||
|
@Schema(description = "设备天气城市")
|
||||||
|
private String weatherCityName;
|
||||||
|
|
||||||
@Schema(description = "状态:1启用,0停用")
|
@Schema(description = "状态:1启用,0停用")
|
||||||
private Integer status;
|
private Integer status;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,23 @@
|
||||||
|
package com.imeeting.dto.biz;
|
||||||
|
|
||||||
|
import io.swagger.v3.oas.annotations.media.Schema;
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
@Data
|
||||||
|
@Schema(description = "正式授权导入结果")
|
||||||
|
public class LicenseImportResultVO {
|
||||||
|
@Schema(description = "导入批次号")
|
||||||
|
private String importBatchNo;
|
||||||
|
|
||||||
|
@Schema(description = "导入总数")
|
||||||
|
private Integer totalCount;
|
||||||
|
|
||||||
|
@Schema(description = "替换中的设备数量")
|
||||||
|
private Integer replacedCount;
|
||||||
|
|
||||||
|
@Schema(description = "新增未使用正式授权数量")
|
||||||
|
private Integer unusedFormalCount;
|
||||||
|
|
||||||
|
@Schema(description = "失效的临时授权数量")
|
||||||
|
private Integer invalidatedTempCount;
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,20 @@
|
||||||
|
package com.imeeting.dto.biz;
|
||||||
|
|
||||||
|
import io.swagger.v3.oas.annotations.media.Schema;
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
@Data
|
||||||
|
@Schema(description = "正式授权导入行")
|
||||||
|
public class LicenseImportRow {
|
||||||
|
@Schema(description = "授权序列号")
|
||||||
|
private String licenseSerial;
|
||||||
|
|
||||||
|
@Schema(description = "授权码")
|
||||||
|
private String licenseCode;
|
||||||
|
|
||||||
|
@Schema(description = "产品编码")
|
||||||
|
private String productCode;
|
||||||
|
|
||||||
|
@Schema(description = "备注")
|
||||||
|
private String remark;
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,49 @@
|
||||||
|
package com.imeeting.dto.biz;
|
||||||
|
|
||||||
|
import io.swagger.v3.oas.annotations.media.Schema;
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
|
||||||
|
@Data
|
||||||
|
@Schema(description = "授权列表视图")
|
||||||
|
public class LicenseVO {
|
||||||
|
@Schema(description = "主键ID")
|
||||||
|
private Long id;
|
||||||
|
|
||||||
|
@Schema(description = "租户ID")
|
||||||
|
private Long tenantId;
|
||||||
|
|
||||||
|
@Schema(description = "授权序列号")
|
||||||
|
private String licenseSerial;
|
||||||
|
|
||||||
|
@Schema(description = "授权码")
|
||||||
|
private String licenseCode;
|
||||||
|
|
||||||
|
@Schema(description = "授权类型")
|
||||||
|
private Integer licenseType;
|
||||||
|
|
||||||
|
@Schema(description = "授权状态")
|
||||||
|
private Integer licenseStatus;
|
||||||
|
|
||||||
|
@Schema(description = "产品编码")
|
||||||
|
private String productCode;
|
||||||
|
|
||||||
|
@Schema(description = "绑定设备编码")
|
||||||
|
private String deviceCode;
|
||||||
|
|
||||||
|
@Schema(description = "绑定时间")
|
||||||
|
private LocalDateTime bindTime;
|
||||||
|
|
||||||
|
@Schema(description = "过期时间")
|
||||||
|
private LocalDateTime expireTime;
|
||||||
|
|
||||||
|
@Schema(description = "导入批次号")
|
||||||
|
private String importBatchNo;
|
||||||
|
|
||||||
|
@Schema(description = "导入时间")
|
||||||
|
private LocalDateTime importTime;
|
||||||
|
|
||||||
|
@Schema(description = "备注")
|
||||||
|
private String remark;
|
||||||
|
}
|
||||||
|
|
@ -28,4 +28,8 @@ public class DeviceInfoEntity extends BaseEntity {
|
||||||
private String terminalVersion;
|
private String terminalVersion;
|
||||||
|
|
||||||
private LocalDateTime lastOnlineAt;
|
private LocalDateTime lastOnlineAt;
|
||||||
|
|
||||||
|
private LocalDateTime statsResetAt;
|
||||||
|
|
||||||
|
private String weatherCityName;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,31 @@
|
||||||
|
package com.imeeting.entity.biz;
|
||||||
|
|
||||||
|
import com.baomidou.mybatisplus.annotation.IdType;
|
||||||
|
import com.baomidou.mybatisplus.annotation.TableId;
|
||||||
|
import com.baomidou.mybatisplus.annotation.TableName;
|
||||||
|
import com.unisbase.entity.BaseEntity;
|
||||||
|
import io.swagger.v3.oas.annotations.media.Schema;
|
||||||
|
import lombok.Data;
|
||||||
|
import lombok.EqualsAndHashCode;
|
||||||
|
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
|
||||||
|
@Data
|
||||||
|
@EqualsAndHashCode(callSuper = true)
|
||||||
|
@TableName("biz_device_login_log")
|
||||||
|
@Schema(description = "设备登录日志实体")
|
||||||
|
public class DeviceLoginLogEntity extends BaseEntity {
|
||||||
|
|
||||||
|
@TableId(value = "id", type = IdType.AUTO)
|
||||||
|
@Schema(description = "主键ID")
|
||||||
|
private Long id;
|
||||||
|
|
||||||
|
@Schema(description = "设备编码")
|
||||||
|
private String deviceCode;
|
||||||
|
|
||||||
|
@Schema(description = "登录用户ID")
|
||||||
|
private Long userId;
|
||||||
|
|
||||||
|
@Schema(description = "登录时间")
|
||||||
|
private LocalDateTime loginAt;
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,55 @@
|
||||||
|
package com.imeeting.entity.biz;
|
||||||
|
|
||||||
|
import com.baomidou.mybatisplus.annotation.IdType;
|
||||||
|
import com.baomidou.mybatisplus.annotation.TableId;
|
||||||
|
import com.baomidou.mybatisplus.annotation.TableName;
|
||||||
|
import com.unisbase.entity.BaseEntity;
|
||||||
|
import io.swagger.v3.oas.annotations.media.Schema;
|
||||||
|
import lombok.Data;
|
||||||
|
import lombok.EqualsAndHashCode;
|
||||||
|
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
|
||||||
|
@Data
|
||||||
|
@EqualsAndHashCode(callSuper = true)
|
||||||
|
@TableName("biz_license")
|
||||||
|
@Schema(description = "设备授权实体")
|
||||||
|
public class LicenseEntity extends BaseEntity {
|
||||||
|
|
||||||
|
@TableId(value = "id", type = IdType.AUTO)
|
||||||
|
@Schema(description = "主键ID")
|
||||||
|
private Long id;
|
||||||
|
|
||||||
|
@Schema(description = "授权序列号")
|
||||||
|
private String licenseSerial;
|
||||||
|
|
||||||
|
@Schema(description = "授权码")
|
||||||
|
private String licenseCode;
|
||||||
|
|
||||||
|
@Schema(description = "授权类型:1-临时,2-正式")
|
||||||
|
private Integer licenseType;
|
||||||
|
|
||||||
|
@Schema(description = "授权状态:1-未使用,2-使用中,3-已过期,4-已失效")
|
||||||
|
private Integer licenseStatus;
|
||||||
|
|
||||||
|
@Schema(description = "产品BOM编码")
|
||||||
|
private String productCode;
|
||||||
|
|
||||||
|
@Schema(description = "绑定设备编码")
|
||||||
|
private String deviceCode;
|
||||||
|
|
||||||
|
@Schema(description = "绑定时间")
|
||||||
|
private LocalDateTime bindTime;
|
||||||
|
|
||||||
|
@Schema(description = "过期时间")
|
||||||
|
private LocalDateTime expireTime;
|
||||||
|
|
||||||
|
@Schema(description = "导入批次号")
|
||||||
|
private String importBatchNo;
|
||||||
|
|
||||||
|
@Schema(description = "导入时间")
|
||||||
|
private LocalDateTime importTime;
|
||||||
|
|
||||||
|
@Schema(description = "备注")
|
||||||
|
private String remark;
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,19 @@
|
||||||
|
package com.imeeting.enums;
|
||||||
|
|
||||||
|
import lombok.Getter;
|
||||||
|
|
||||||
|
@Getter
|
||||||
|
public enum LicenseStatusEnum {
|
||||||
|
UNUSED(1, "未使用"),
|
||||||
|
IN_USE(2, "使用中"),
|
||||||
|
EXPIRED(3, "已过期"),
|
||||||
|
INVALID(4, "已失效");
|
||||||
|
|
||||||
|
private final int code;
|
||||||
|
private final String desc;
|
||||||
|
|
||||||
|
LicenseStatusEnum(int code, String desc) {
|
||||||
|
this.code = code;
|
||||||
|
this.desc = desc;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,17 @@
|
||||||
|
package com.imeeting.enums;
|
||||||
|
|
||||||
|
import lombok.Getter;
|
||||||
|
|
||||||
|
@Getter
|
||||||
|
public enum LicenseTypeEnum {
|
||||||
|
TEMPORARY(1, "临时"),
|
||||||
|
FORMAL(2, "正式");
|
||||||
|
|
||||||
|
private final int code;
|
||||||
|
private final String desc;
|
||||||
|
|
||||||
|
LicenseTypeEnum(int code, String desc) {
|
||||||
|
this.code = code;
|
||||||
|
this.desc = desc;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -3,9 +3,9 @@ package com.imeeting.grpc.push;
|
||||||
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.service.android.AndroidAuthService;
|
import com.imeeting.service.android.AndroidAuthService;
|
||||||
import com.imeeting.service.android.AndroidPushMessageService;
|
|
||||||
import com.imeeting.service.android.AndroidDeviceSessionService;
|
import com.imeeting.service.android.AndroidDeviceSessionService;
|
||||||
import com.imeeting.service.android.AndroidGatewayPushService;
|
import com.imeeting.service.android.AndroidGatewayPushService;
|
||||||
|
import com.imeeting.service.android.AndroidPushMessageService;
|
||||||
import com.imeeting.service.biz.DeviceOnlineManagementService;
|
import com.imeeting.service.biz.DeviceOnlineManagementService;
|
||||||
import com.unisbase.common.exception.BusinessException;
|
import com.unisbase.common.exception.BusinessException;
|
||||||
import io.grpc.BindableService;
|
import io.grpc.BindableService;
|
||||||
|
|
@ -116,8 +116,6 @@ public class AndroidPushGrpcService extends PushServiceGrpc.PushServiceImplBase
|
||||||
request.getAppVersion(),
|
request.getAppVersion(),
|
||||||
resolvePlatform(request.getPlatform())
|
resolvePlatform(request.getPlatform())
|
||||||
);
|
);
|
||||||
authContext.setUserId(parseNullableLong(request.getUserId(), "user_id"));
|
|
||||||
authContext.setTenantId(parseNullableLong(request.getTenantId(), "tenant_id"));
|
|
||||||
AndroidDeviceSessionState sessionState = androidDeviceSessionService.openSession(authContext, request.getConnectionId());
|
AndroidDeviceSessionState sessionState = androidDeviceSessionService.openSession(authContext, request.getConnectionId());
|
||||||
connectionId = sessionState.getConnectionId();
|
connectionId = sessionState.getConnectionId();
|
||||||
deviceId = sessionState.getDeviceId();
|
deviceId = sessionState.getDeviceId();
|
||||||
|
|
@ -181,30 +179,15 @@ public class AndroidPushGrpcService extends PushServiceGrpc.PushServiceImplBase
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (!request.getConnectionId().isBlank() && !request.getConnectionId().equals(connectionId)) {
|
if (!request.getConnectionId().isBlank() && !request.getConnectionId().equals(connectionId)) {
|
||||||
log.info(buildLog("gRPC确认拒绝",
|
|
||||||
"ACK连接ID与当前活动连接不一致,请求连接ID=" + request.getConnectionId() + ",当前连接ID=" + connectionId,
|
|
||||||
deviceId,
|
|
||||||
appVersion,
|
|
||||||
platform));
|
|
||||||
sendError(responseObserver, "PUSH_CONNECTION_MISMATCH", "Connection id does not match active session", false,
|
sendError(responseObserver, "PUSH_CONNECTION_MISMATCH", "Connection id does not match active session", false,
|
||||||
deviceId, appVersion, platform);
|
deviceId, appVersion, platform);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (!request.getDeviceId().isBlank() && !request.getDeviceId().equals(deviceId)) {
|
if (!request.getDeviceId().isBlank() && !request.getDeviceId().equals(deviceId)) {
|
||||||
log.info(buildLog("gRPC确认拒绝",
|
|
||||||
"ACK设备ID与当前活动设备不一致,请求设备ID=" + request.getDeviceId() + ",当前设备ID=" + deviceId,
|
|
||||||
deviceId,
|
|
||||||
appVersion,
|
|
||||||
platform));
|
|
||||||
sendError(responseObserver, "PUSH_DEVICE_MISMATCH", "Device id does not match active session", false,
|
sendError(responseObserver, "PUSH_DEVICE_MISMATCH", "Device id does not match active session", false,
|
||||||
deviceId, appVersion, platform);
|
deviceId, appVersion, platform);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
log.info(buildLog("gRPC消息确认",
|
|
||||||
"收到客户端ACK确认,消息ID=" + safe(request.getMessageId()) + ",连接ID=" + connectionId,
|
|
||||||
deviceId,
|
|
||||||
appVersion,
|
|
||||||
platform));
|
|
||||||
androidPushMessageService.ack(request.getMessageId(), deviceId);
|
androidPushMessageService.ack(request.getMessageId(), deviceId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -212,11 +195,6 @@ public class AndroidPushGrpcService extends PushServiceGrpc.PushServiceImplBase
|
||||||
if (connected) {
|
if (connected) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
log.info(buildLog("gRPC请求拒绝",
|
|
||||||
"连接尚未建立即发送后续消息",
|
|
||||||
deviceId,
|
|
||||||
appVersion,
|
|
||||||
platform));
|
|
||||||
sendError(responseObserver, "PUSH_NOT_CONNECTED", "Push connection has not been established", false,
|
sendError(responseObserver, "PUSH_NOT_CONNECTED", "Push connection has not been established", false,
|
||||||
deviceId, appVersion, platform);
|
deviceId, appVersion, platform);
|
||||||
return false;
|
return false;
|
||||||
|
|
@ -230,11 +208,6 @@ public class AndroidPushGrpcService extends PushServiceGrpc.PushServiceImplBase
|
||||||
androidGatewayPushService.unregister(connectionId);
|
androidGatewayPushService.unregister(connectionId);
|
||||||
androidDeviceSessionService.closeSession(connectionId);
|
androidDeviceSessionService.closeSession(connectionId);
|
||||||
deviceOnlineManagementService.recordDisconnected(deviceId, state == null ? null : state.getLastSeenAt());
|
deviceOnlineManagementService.recordDisconnected(deviceId, state == null ? null : state.getLastSeenAt());
|
||||||
log.info(buildLog("gRPC连接关闭",
|
|
||||||
"Android推送连接已关闭,连接ID=" + connectionId,
|
|
||||||
deviceId,
|
|
||||||
state == null ? appVersion : state.getAppVersion(),
|
|
||||||
state == null ? platform : state.getPlatform()));
|
|
||||||
connectionId = null;
|
connectionId = null;
|
||||||
deviceId = null;
|
deviceId = null;
|
||||||
appVersion = null;
|
appVersion = null;
|
||||||
|
|
@ -251,11 +224,6 @@ public class AndroidPushGrpcService extends PushServiceGrpc.PushServiceImplBase
|
||||||
String deviceId,
|
String deviceId,
|
||||||
String appVersion,
|
String appVersion,
|
||||||
String platform) {
|
String platform) {
|
||||||
log.info(buildLog("gRPC错误响应",
|
|
||||||
"向客户端返回错误,错误码=" + safe(code) + ",原因=" + safe(message),
|
|
||||||
deviceId,
|
|
||||||
appVersion,
|
|
||||||
platform));
|
|
||||||
responseObserver.onNext(ServerMessage.newBuilder()
|
responseObserver.onNext(ServerMessage.newBuilder()
|
||||||
.setError(ErrorEvent.newBuilder()
|
.setError(ErrorEvent.newBuilder()
|
||||||
.setCode(code)
|
.setCode(code)
|
||||||
|
|
@ -286,15 +254,4 @@ public class AndroidPushGrpcService extends PushServiceGrpc.PushServiceImplBase
|
||||||
case PLATFORM_UNKNOWN, UNRECOGNIZED -> "android";
|
case PLATFORM_UNKNOWN, UNRECOGNIZED -> "android";
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
private Long parseNullableLong(String value, String fieldName) {
|
|
||||||
if (value == null || value.isBlank()) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
return Long.parseLong(value.trim());
|
|
||||||
} catch (NumberFormatException ex) {
|
|
||||||
throw new RuntimeException("Invalid " + fieldName);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -87,6 +87,8 @@ public interface DeviceInfoMapper extends BaseMapper<DeviceInfoEntity> {
|
||||||
d.terminal_type AS terminalType,
|
d.terminal_type AS terminalType,
|
||||||
d.terminal_version AS terminalVersion,
|
d.terminal_version AS terminalVersion,
|
||||||
d.last_online_at AS lastOnlineAt,
|
d.last_online_at AS lastOnlineAt,
|
||||||
|
d.stats_reset_at AS statsResetAt,
|
||||||
|
d.weather_city_name AS weatherCityName,
|
||||||
d.status AS status,
|
d.status AS status,
|
||||||
d.created_at AS createdAt,
|
d.created_at AS createdAt,
|
||||||
d.updated_at AS updatedAt,
|
d.updated_at AS updatedAt,
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,31 @@
|
||||||
|
package com.imeeting.mapper;
|
||||||
|
|
||||||
|
import com.baomidou.mybatisplus.annotation.InterceptorIgnore;
|
||||||
|
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||||
|
import com.imeeting.entity.biz.DeviceLoginLogEntity;
|
||||||
|
import org.apache.ibatis.annotations.Mapper;
|
||||||
|
import org.apache.ibatis.annotations.Param;
|
||||||
|
import org.apache.ibatis.annotations.Select;
|
||||||
|
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
|
||||||
|
@Mapper
|
||||||
|
public interface DeviceLoginLogMapper extends BaseMapper<DeviceLoginLogEntity> {
|
||||||
|
|
||||||
|
@InterceptorIgnore(tenantLine = "true")
|
||||||
|
@Select("""
|
||||||
|
<script>
|
||||||
|
SELECT COUNT(DISTINCT user_id)
|
||||||
|
FROM biz_device_login_log
|
||||||
|
WHERE tenant_id = #{tenantId}
|
||||||
|
AND device_code = #{deviceCode}
|
||||||
|
AND is_deleted = 0
|
||||||
|
<if test="resetAt != null">
|
||||||
|
AND login_at >= #{resetAt}
|
||||||
|
</if>
|
||||||
|
</script>
|
||||||
|
""")
|
||||||
|
Long countDistinctUsersSince(@Param("tenantId") Long tenantId,
|
||||||
|
@Param("deviceCode") String deviceCode,
|
||||||
|
@Param("resetAt") LocalDateTime resetAt);
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,223 @@
|
||||||
|
package com.imeeting.mapper;
|
||||||
|
|
||||||
|
import com.baomidou.mybatisplus.annotation.InterceptorIgnore;
|
||||||
|
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||||
|
import com.imeeting.dto.biz.LicenseVO;
|
||||||
|
import com.imeeting.entity.biz.LicenseEntity;
|
||||||
|
import org.apache.ibatis.annotations.Mapper;
|
||||||
|
import org.apache.ibatis.annotations.Options;
|
||||||
|
import org.apache.ibatis.annotations.Param;
|
||||||
|
import org.apache.ibatis.annotations.Select;
|
||||||
|
import org.apache.ibatis.annotations.Update;
|
||||||
|
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
@Mapper
|
||||||
|
public interface LicenseMapper extends BaseMapper<LicenseEntity> {
|
||||||
|
|
||||||
|
@InterceptorIgnore(tenantLine = "true")
|
||||||
|
@Select("""
|
||||||
|
SELECT *
|
||||||
|
FROM biz_license
|
||||||
|
WHERE device_code = #{deviceCode}
|
||||||
|
AND is_deleted = 0
|
||||||
|
ORDER BY updated_at DESC, id DESC
|
||||||
|
LIMIT 1
|
||||||
|
""")
|
||||||
|
LicenseEntity selectByDeviceCodeIgnoreTenant(@Param("deviceCode") String deviceCode);
|
||||||
|
|
||||||
|
@InterceptorIgnore(tenantLine = "true")
|
||||||
|
@Select("""
|
||||||
|
SELECT *
|
||||||
|
FROM biz_license
|
||||||
|
WHERE tenant_id = #{tenantId}
|
||||||
|
AND device_code = #{deviceCode}
|
||||||
|
AND license_status = 2
|
||||||
|
AND is_deleted = 0
|
||||||
|
AND (expire_time IS NULL OR expire_time > CURRENT_TIMESTAMP)
|
||||||
|
ORDER BY updated_at DESC, id DESC
|
||||||
|
LIMIT 1
|
||||||
|
""")
|
||||||
|
LicenseEntity selectValidBoundByTenantAndDeviceIgnoreTenant(@Param("tenantId") Long tenantId,
|
||||||
|
@Param("deviceCode") String deviceCode);
|
||||||
|
|
||||||
|
@InterceptorIgnore(tenantLine = "true")
|
||||||
|
@Select("""
|
||||||
|
SELECT *
|
||||||
|
FROM biz_license
|
||||||
|
WHERE tenant_id = #{tenantId}
|
||||||
|
AND license_code = #{licenseCode}
|
||||||
|
AND is_deleted = 0
|
||||||
|
LIMIT 1
|
||||||
|
""")
|
||||||
|
LicenseEntity selectByTenantAndCodeIgnoreTenant(@Param("tenantId") Long tenantId, @Param("licenseCode") String licenseCode);
|
||||||
|
|
||||||
|
@InterceptorIgnore(tenantLine = "true")
|
||||||
|
@Select("""
|
||||||
|
SELECT *
|
||||||
|
FROM biz_license
|
||||||
|
WHERE license_serial = #{licenseSerial}
|
||||||
|
AND is_deleted = 0
|
||||||
|
LIMIT 1
|
||||||
|
""")
|
||||||
|
LicenseEntity selectBySerialIgnoreTenant(@Param("licenseSerial") String licenseSerial);
|
||||||
|
|
||||||
|
@InterceptorIgnore(tenantLine = "true")
|
||||||
|
@Options(useCache = false, flushCache = Options.FlushCachePolicy.TRUE)
|
||||||
|
@Select("SELECT nextval('biz_license_temp_serial_seq')")
|
||||||
|
Long nextTempSerialValue();
|
||||||
|
|
||||||
|
@InterceptorIgnore(tenantLine = "true")
|
||||||
|
@Select("""
|
||||||
|
SELECT *
|
||||||
|
FROM biz_license
|
||||||
|
WHERE tenant_id = #{tenantId}
|
||||||
|
AND license_status = 2
|
||||||
|
AND device_code = #{deviceCode}
|
||||||
|
AND is_deleted = 0
|
||||||
|
LIMIT 1
|
||||||
|
""")
|
||||||
|
LicenseEntity selectActiveByTenantAndDeviceIgnoreTenant(@Param("tenantId") Long tenantId, @Param("deviceCode") String deviceCode);
|
||||||
|
|
||||||
|
@InterceptorIgnore(tenantLine = "true")
|
||||||
|
@Select("""
|
||||||
|
SELECT *
|
||||||
|
FROM biz_license
|
||||||
|
WHERE tenant_id = #{tenantId}
|
||||||
|
AND license_status = 2
|
||||||
|
AND device_code IS NOT NULL
|
||||||
|
AND license_type = 1
|
||||||
|
AND is_deleted = 0
|
||||||
|
ORDER BY bind_time ASC NULLS LAST, id ASC
|
||||||
|
""")
|
||||||
|
List<LicenseEntity> selectBoundTemporaryLicensesForReplace(@Param("tenantId") Long tenantId);
|
||||||
|
|
||||||
|
@InterceptorIgnore(tenantLine = "true")
|
||||||
|
@Select("""
|
||||||
|
SELECT *
|
||||||
|
FROM biz_license
|
||||||
|
WHERE tenant_id = #{tenantId}
|
||||||
|
AND license_status = 1
|
||||||
|
AND license_type = 2
|
||||||
|
AND import_batch_no = #{importBatchNo}
|
||||||
|
AND is_deleted = 0
|
||||||
|
ORDER BY created_at ASC, id ASC
|
||||||
|
""")
|
||||||
|
List<LicenseEntity> selectUnusedFormalLicenses(@Param("tenantId") Long tenantId, @Param("importBatchNo") String importBatchNo);
|
||||||
|
|
||||||
|
@InterceptorIgnore(tenantLine = "true")
|
||||||
|
@Select("""
|
||||||
|
SELECT *
|
||||||
|
FROM biz_license
|
||||||
|
WHERE tenant_id = #{tenantId}
|
||||||
|
AND license_status = 1
|
||||||
|
AND is_deleted = 0
|
||||||
|
AND (expire_time IS NULL OR expire_time > CURRENT_TIMESTAMP)
|
||||||
|
ORDER BY license_type ASC, created_at ASC, id ASC
|
||||||
|
LIMIT 1
|
||||||
|
""")
|
||||||
|
LicenseEntity selectFirstAssignableLicense(@Param("tenantId") Long tenantId);
|
||||||
|
|
||||||
|
@InterceptorIgnore(tenantLine = "true")
|
||||||
|
@Select("""
|
||||||
|
SELECT *
|
||||||
|
FROM biz_license
|
||||||
|
WHERE tenant_id = #{tenantId}
|
||||||
|
AND license_status = 2
|
||||||
|
AND device_code IS NOT NULL
|
||||||
|
AND is_deleted = 0
|
||||||
|
ORDER BY bind_time ASC NULLS LAST, id ASC
|
||||||
|
""")
|
||||||
|
List<LicenseEntity> selectBoundLicenses(@Param("tenantId") Long tenantId);
|
||||||
|
|
||||||
|
@InterceptorIgnore(tenantLine = "true")
|
||||||
|
@Select("""
|
||||||
|
SELECT
|
||||||
|
id,
|
||||||
|
tenant_id AS tenantId,
|
||||||
|
license_serial AS licenseSerial,
|
||||||
|
license_code AS licenseCode,
|
||||||
|
license_type AS licenseType,
|
||||||
|
license_status AS licenseStatus,
|
||||||
|
product_code AS productCode,
|
||||||
|
device_code AS deviceCode,
|
||||||
|
bind_time AS bindTime,
|
||||||
|
expire_time AS expireTime,
|
||||||
|
import_batch_no AS importBatchNo,
|
||||||
|
import_time AS importTime,
|
||||||
|
remark
|
||||||
|
FROM biz_license
|
||||||
|
WHERE tenant_id = #{tenantId}
|
||||||
|
AND is_deleted = 0
|
||||||
|
ORDER BY updated_at DESC, id DESC
|
||||||
|
""")
|
||||||
|
List<LicenseVO> selectListByTenant(@Param("tenantId") Long tenantId);
|
||||||
|
|
||||||
|
@InterceptorIgnore(tenantLine = "true")
|
||||||
|
@Update("""
|
||||||
|
UPDATE biz_license
|
||||||
|
SET license_status = 3,
|
||||||
|
device_code = NULL,
|
||||||
|
bind_time = NULL,
|
||||||
|
updated_at = CURRENT_TIMESTAMP
|
||||||
|
WHERE is_deleted = 0
|
||||||
|
AND license_status IN (1, 2)
|
||||||
|
AND expire_time IS NOT NULL
|
||||||
|
AND expire_time <= CURRENT_TIMESTAMP
|
||||||
|
""")
|
||||||
|
int expireDueLicenses();
|
||||||
|
|
||||||
|
@InterceptorIgnore(tenantLine = "true")
|
||||||
|
@Update("""
|
||||||
|
UPDATE biz_license
|
||||||
|
SET device_code = NULL,
|
||||||
|
bind_time = NULL,
|
||||||
|
license_status = #{licenseStatus},
|
||||||
|
updated_at = CURRENT_TIMESTAMP
|
||||||
|
WHERE id = #{id}
|
||||||
|
AND is_deleted = 0
|
||||||
|
""")
|
||||||
|
int clearBindingById(@Param("id") Long id, @Param("licenseStatus") Integer licenseStatus);
|
||||||
|
|
||||||
|
@InterceptorIgnore(tenantLine = "true")
|
||||||
|
@Update("""
|
||||||
|
UPDATE biz_license
|
||||||
|
SET device_code = #{deviceCode},
|
||||||
|
bind_time = #{bindTime},
|
||||||
|
license_status = #{licenseStatus},
|
||||||
|
updated_at = CURRENT_TIMESTAMP
|
||||||
|
WHERE id = #{id}
|
||||||
|
AND is_deleted = 0
|
||||||
|
""")
|
||||||
|
int bindLicenseById(@Param("id") Long id,
|
||||||
|
@Param("deviceCode") String deviceCode,
|
||||||
|
@Param("bindTime") LocalDateTime bindTime,
|
||||||
|
@Param("licenseStatus") Integer licenseStatus);
|
||||||
|
|
||||||
|
@InterceptorIgnore(tenantLine = "true")
|
||||||
|
@Update("""
|
||||||
|
UPDATE biz_license
|
||||||
|
SET license_status = 4,
|
||||||
|
device_code = NULL,
|
||||||
|
bind_time = NULL,
|
||||||
|
updated_at = CURRENT_TIMESTAMP
|
||||||
|
WHERE tenant_id = #{tenantId}
|
||||||
|
AND license_type = 1
|
||||||
|
AND is_deleted = 0
|
||||||
|
AND license_status IN (1, 2, 3)
|
||||||
|
""")
|
||||||
|
int invalidateAllTemporaryLicenses(@Param("tenantId") Long tenantId);
|
||||||
|
|
||||||
|
@InterceptorIgnore(tenantLine = "true")
|
||||||
|
@Update("""
|
||||||
|
UPDATE biz_license
|
||||||
|
SET license_status = 4,
|
||||||
|
device_code = NULL,
|
||||||
|
bind_time = NULL,
|
||||||
|
updated_at = CURRENT_TIMESTAMP
|
||||||
|
WHERE id = #{id}
|
||||||
|
AND is_deleted = 0
|
||||||
|
""")
|
||||||
|
int invalidateById(@Param("id") Long id);
|
||||||
|
}
|
||||||
|
|
@ -7,9 +7,45 @@ import org.apache.ibatis.annotations.Mapper;
|
||||||
import org.apache.ibatis.annotations.Param;
|
import org.apache.ibatis.annotations.Param;
|
||||||
import org.apache.ibatis.annotations.Select;
|
import org.apache.ibatis.annotations.Select;
|
||||||
|
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
|
||||||
@Mapper
|
@Mapper
|
||||||
public interface MeetingMapper extends BaseMapper<Meeting> {
|
public interface MeetingMapper extends BaseMapper<Meeting> {
|
||||||
@InterceptorIgnore(tenantLine = "true")
|
@InterceptorIgnore(tenantLine = "true")
|
||||||
@Select("SELECT * FROM biz_meetings WHERE id = #{id} AND is_deleted = 0")
|
@Select("SELECT * FROM biz_meetings WHERE id = #{id} AND is_deleted = 0")
|
||||||
Meeting selectByIdIgnoreTenant(@Param("id") Long id);
|
Meeting selectByIdIgnoreTenant(@Param("id") Long id);
|
||||||
|
|
||||||
|
@InterceptorIgnore(tenantLine = "true")
|
||||||
|
@Select("""
|
||||||
|
<script>
|
||||||
|
SELECT COUNT(1)
|
||||||
|
FROM biz_meetings
|
||||||
|
WHERE tenant_id = #{tenantId}
|
||||||
|
AND source_device_code = #{deviceCode}
|
||||||
|
AND is_deleted = 0
|
||||||
|
<if test="resetAt != null">
|
||||||
|
AND created_at >= #{resetAt}
|
||||||
|
</if>
|
||||||
|
</script>
|
||||||
|
""")
|
||||||
|
Long countByDeviceSince(@Param("tenantId") Long tenantId,
|
||||||
|
@Param("deviceCode") String deviceCode,
|
||||||
|
@Param("resetAt") LocalDateTime resetAt);
|
||||||
|
|
||||||
|
@InterceptorIgnore(tenantLine = "true")
|
||||||
|
@Select("""
|
||||||
|
<script>
|
||||||
|
SELECT COALESCE(SUM(COALESCE(effective_audio_duration_seconds, 0)), 0)
|
||||||
|
FROM biz_meetings
|
||||||
|
WHERE tenant_id = #{tenantId}
|
||||||
|
AND source_device_code = #{deviceCode}
|
||||||
|
AND is_deleted = 0
|
||||||
|
<if test="resetAt != null">
|
||||||
|
AND created_at >= #{resetAt}
|
||||||
|
</if>
|
||||||
|
</script>
|
||||||
|
""")
|
||||||
|
Long sumMeetingDurationSecondsByDeviceSince(@Param("tenantId") Long tenantId,
|
||||||
|
@Param("deviceCode") String deviceCode,
|
||||||
|
@Param("resetAt") LocalDateTime resetAt);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,27 @@
|
||||||
package com.imeeting.mapper.biz;
|
package com.imeeting.mapper.biz;
|
||||||
|
|
||||||
|
import com.baomidou.mybatisplus.annotation.InterceptorIgnore;
|
||||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||||
import com.imeeting.entity.biz.ScreenSaver;
|
import com.imeeting.entity.biz.ScreenSaver;
|
||||||
import org.apache.ibatis.annotations.Mapper;
|
import org.apache.ibatis.annotations.Mapper;
|
||||||
|
import org.apache.ibatis.annotations.Param;
|
||||||
|
import org.apache.ibatis.annotations.Select;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
@Mapper
|
@Mapper
|
||||||
public interface ScreenSaverMapper extends BaseMapper<ScreenSaver> {
|
public interface ScreenSaverMapper extends BaseMapper<ScreenSaver> {
|
||||||
|
|
||||||
|
@InterceptorIgnore(tenantLine = "true")
|
||||||
|
@Select("""
|
||||||
|
SELECT *
|
||||||
|
FROM biz_screen_savers
|
||||||
|
WHERE tenant_id = #{tenantId}
|
||||||
|
AND scope_type = 'PLATFORM'
|
||||||
|
AND status = 1
|
||||||
|
AND owner_user_id IS NULL
|
||||||
|
AND is_deleted = 0
|
||||||
|
ORDER BY sort_order ASC, id DESC
|
||||||
|
""")
|
||||||
|
List<ScreenSaver> selectActivePlatformByTenantIgnoreTenant(@Param("tenantId") Long tenantId);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -9,4 +9,6 @@ public interface AndroidAuthService {
|
||||||
AndroidAuthContext authenticateHttp(HttpServletRequest request);
|
AndroidAuthContext authenticateHttp(HttpServletRequest request);
|
||||||
|
|
||||||
AndroidAuthContext authenticateHttp(HttpServletRequest request, boolean requireRegistered);
|
AndroidAuthContext authenticateHttp(HttpServletRequest request, boolean requireRegistered);
|
||||||
|
|
||||||
|
AndroidAuthContext authenticateHttp(HttpServletRequest request, boolean requireRegistered, boolean allowOptionalToken);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -6,4 +6,6 @@ public interface AndroidDeviceBindingService {
|
||||||
void validatePrivateDeviceAccess(String deviceCode, Long tenantId, Long userId);
|
void validatePrivateDeviceAccess(String deviceCode, Long tenantId, Long userId);
|
||||||
|
|
||||||
void unbindPrivateDevice(String deviceCode);
|
void unbindPrivateDevice(String deviceCode);
|
||||||
|
|
||||||
|
void recordLogin(String deviceCode, Long tenantId, Long userId);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,9 @@
|
||||||
|
package com.imeeting.service.android;
|
||||||
|
|
||||||
|
import com.imeeting.dto.android.AndroidAuthContext;
|
||||||
|
import com.imeeting.dto.android.AndroidDeviceHomeStatsVO;
|
||||||
|
|
||||||
|
public interface AndroidDeviceHomeService {
|
||||||
|
|
||||||
|
AndroidDeviceHomeStatsVO getHomeStats(AndroidAuthContext authContext);
|
||||||
|
}
|
||||||
|
|
@ -3,7 +3,7 @@ package com.imeeting.service.android;
|
||||||
import com.imeeting.dto.android.AndroidDeviceRegisterResponse;
|
import com.imeeting.dto.android.AndroidDeviceRegisterResponse;
|
||||||
|
|
||||||
public interface AndroidDeviceRegistrationService {
|
public interface AndroidDeviceRegistrationService {
|
||||||
AndroidDeviceRegisterResponse register(String deviceCode, String deviceName, String terminalType, String terminalVersion);
|
AndroidDeviceRegisterResponse register(String tenantCode, String deviceCode, String deviceName, String terminalType, String terminalVersion);
|
||||||
|
|
||||||
void requireRegistered(String deviceCode);
|
void requireRegistered(String deviceCode);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -3,9 +3,11 @@ package com.imeeting.service.android.impl;
|
||||||
import com.imeeting.config.grpc.AndroidGrpcAuthProperties;
|
import com.imeeting.config.grpc.AndroidGrpcAuthProperties;
|
||||||
import com.imeeting.dto.android.AndroidAuthContext;
|
import com.imeeting.dto.android.AndroidAuthContext;
|
||||||
import com.imeeting.entity.biz.DeviceInfoEntity;
|
import com.imeeting.entity.biz.DeviceInfoEntity;
|
||||||
|
import com.imeeting.entity.biz.LicenseEntity;
|
||||||
import com.imeeting.mapper.DeviceInfoMapper;
|
import com.imeeting.mapper.DeviceInfoMapper;
|
||||||
import com.imeeting.service.android.AndroidAuthService;
|
import com.imeeting.service.android.AndroidAuthService;
|
||||||
import com.imeeting.service.android.AndroidDeviceBindingService;
|
import com.imeeting.service.android.AndroidDeviceBindingService;
|
||||||
|
import com.imeeting.service.biz.LicenseService;
|
||||||
import com.unisbase.common.exception.BusinessException;
|
import com.unisbase.common.exception.BusinessException;
|
||||||
import com.unisbase.dto.InternalAuthCheckResponse;
|
import com.unisbase.dto.InternalAuthCheckResponse;
|
||||||
import com.unisbase.security.LoginUser;
|
import com.unisbase.security.LoginUser;
|
||||||
|
|
@ -34,27 +36,34 @@ public class AndroidAuthServiceImpl implements AndroidAuthService {
|
||||||
private final TokenValidationService tokenValidationService;
|
private final TokenValidationService tokenValidationService;
|
||||||
private final DeviceInfoMapper deviceInfoMapper;
|
private final DeviceInfoMapper deviceInfoMapper;
|
||||||
private final AndroidDeviceBindingService androidDeviceBindingService;
|
private final AndroidDeviceBindingService androidDeviceBindingService;
|
||||||
|
private final LicenseService licenseService;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public AndroidAuthContext authenticateGrpc(String deviceId, String appVersion, String platform) {
|
public AndroidAuthContext authenticateGrpc(String deviceId, String appVersion, String platform) {
|
||||||
if (properties.isEnabled() && !properties.isAllowAnonymous()) {
|
if (properties.isEnabled() && !properties.isAllowAnonymous()) {
|
||||||
throw new RuntimeException("Android gRPC push does not allow anonymous access");
|
throw new RuntimeException("Android gRPC push does not allow anonymous access");
|
||||||
}
|
}
|
||||||
|
LicenseEntity license = licenseService.requireValidBoundLicense(deviceId);
|
||||||
DeviceInfoEntity device = requireRegisteredDevice(deviceId);
|
DeviceInfoEntity device = requireRegisteredDevice(deviceId);
|
||||||
assertDeviceEnabled(device);
|
assertDeviceEnabled(device);
|
||||||
AndroidAuthContext context = buildContext("NONE", true, deviceId, null, appVersion, platform, null, null, null, null);
|
AndroidAuthContext context = buildContext("NONE", true, deviceId, null, appVersion, platform, null, null, null, null);
|
||||||
context.setUserId(device.getUserId());
|
context.setUserId(device.getUserId());
|
||||||
context.setTenantId(device.getTenantId());
|
context.setTenantId(license.getTenantId());
|
||||||
return context;
|
return context;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public AndroidAuthContext authenticateHttp(HttpServletRequest request) {
|
public AndroidAuthContext authenticateHttp(HttpServletRequest request) {
|
||||||
return authenticateHttp(request, true);
|
return authenticateHttp(request, true, false);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public AndroidAuthContext authenticateHttp(HttpServletRequest request, boolean requireRegistered) {
|
public AndroidAuthContext authenticateHttp(HttpServletRequest request, boolean requireRegistered) {
|
||||||
|
return authenticateHttp(request, requireRegistered, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public AndroidAuthContext authenticateHttp(HttpServletRequest request, boolean requireRegistered, boolean allowOptionalToken) {
|
||||||
LoginUser loginUser = currentLoginUser();
|
LoginUser loginUser = currentLoginUser();
|
||||||
String resolvedToken = resolveHttpToken(request);
|
String resolvedToken = resolveHttpToken(request);
|
||||||
String deviceId = firstHeader(request, HEADER_DEVICE_ID);
|
String deviceId = firstHeader(request, HEADER_DEVICE_ID);
|
||||||
|
|
@ -63,12 +72,17 @@ public class AndroidAuthServiceImpl implements AndroidAuthService {
|
||||||
String platform = request.getHeader(HEADER_PLATFORM);
|
String platform = request.getHeader(HEADER_PLATFORM);
|
||||||
|
|
||||||
requireAndroidHttpHeaders(deviceId, appVersion, platform);
|
requireAndroidHttpHeaders(deviceId, appVersion, platform);
|
||||||
log.info("[安卓接口访问]X-Android-Device-Id={},X-Android-App-Version={},X-Android-Platform={}",deviceId,appVersion,platform);
|
log.info("[安卓接口访问]X-Android-Device-Id={},X-Android-App-Version={},X-Android-Platform={}", deviceId, appVersion, platform);
|
||||||
|
|
||||||
DeviceInfoEntity device = requireRegistered ? requireRegisteredDevice(deviceId) : findDevice(deviceId);
|
DeviceInfoEntity device = requireRegistered ? requireRegisteredDevice(deviceId) : findDevice(deviceId);
|
||||||
assertDeviceEnabled(device);
|
assertDeviceEnabled(device);
|
||||||
|
LicenseEntity license = requireRegistered ? licenseService.requireValidBoundLicense(deviceId) : null;
|
||||||
|
|
||||||
if (loginUser != null) {
|
if (loginUser != null) {
|
||||||
androidDeviceBindingService.validatePrivateDeviceAccess(deviceId, loginUser.getTenantId(), loginUser.getUserId());
|
if (!allowOptionalToken) {
|
||||||
return buildContext("USER_JWT", false,
|
androidDeviceBindingService.validatePrivateDeviceAccess(deviceId, loginUser.getTenantId(), loginUser.getUserId());
|
||||||
|
}
|
||||||
|
AndroidAuthContext context = buildContext("USER_JWT", false,
|
||||||
deviceId,
|
deviceId,
|
||||||
appId,
|
appId,
|
||||||
appVersion,
|
appVersion,
|
||||||
|
|
@ -77,12 +91,15 @@ public class AndroidAuthServiceImpl implements AndroidAuthService {
|
||||||
null,
|
null,
|
||||||
null,
|
null,
|
||||||
loginUser);
|
loginUser);
|
||||||
|
return applyLicenseContext(context, license, allowOptionalToken);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (StringUtils.hasText(resolvedToken)) {
|
if (StringUtils.hasText(resolvedToken)) {
|
||||||
InternalAuthCheckResponse authResult = validateToken(resolvedToken);
|
InternalAuthCheckResponse authResult = validateToken(resolvedToken);
|
||||||
androidDeviceBindingService.validatePrivateDeviceAccess(deviceId, authResult.getTenantId(), authResult.getUserId());
|
if (requireRegistered && !allowOptionalToken) {
|
||||||
return buildContext("USER_JWT", false,
|
androidDeviceBindingService.validatePrivateDeviceAccess(deviceId, authResult.getTenantId(), authResult.getUserId());
|
||||||
|
}
|
||||||
|
AndroidAuthContext context = buildContext("USER_JWT", false,
|
||||||
deviceId,
|
deviceId,
|
||||||
appId,
|
appId,
|
||||||
appVersion,
|
appVersion,
|
||||||
|
|
@ -91,9 +108,11 @@ public class AndroidAuthServiceImpl implements AndroidAuthService {
|
||||||
null,
|
null,
|
||||||
authResult,
|
authResult,
|
||||||
null);
|
null);
|
||||||
|
return applyLicenseContext(context, license, allowOptionalToken);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (properties.isAllowAnonymous()) {
|
if (properties.isAllowAnonymous()) {
|
||||||
return buildContext("NONE", true,
|
AndroidAuthContext context = buildContext("NONE", true,
|
||||||
deviceId,
|
deviceId,
|
||||||
appId,
|
appId,
|
||||||
appVersion,
|
appVersion,
|
||||||
|
|
@ -102,10 +121,33 @@ public class AndroidAuthServiceImpl implements AndroidAuthService {
|
||||||
null,
|
null,
|
||||||
null,
|
null,
|
||||||
null);
|
null);
|
||||||
|
return applyLicenseContext(context, license, allowOptionalToken);
|
||||||
}
|
}
|
||||||
throw new RuntimeException("Missing Android HTTP access token");
|
throw new RuntimeException("Missing Android HTTP access token");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private AndroidAuthContext applyLicenseContext(AndroidAuthContext context, LicenseEntity license, boolean allowOptionalToken) {
|
||||||
|
if (context == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
if (license == null) {
|
||||||
|
return context;
|
||||||
|
}
|
||||||
|
Long currentTenantId = context.getTenantId();
|
||||||
|
context.setTenantId(license.getTenantId());
|
||||||
|
if (allowOptionalToken && context.getUserId() != null && currentTenantId != null && !currentTenantId.equals(license.getTenantId())) {
|
||||||
|
context.setAnonymous(true);
|
||||||
|
context.setAuthMode("NONE");
|
||||||
|
context.setUserId(null);
|
||||||
|
context.setUsername(null);
|
||||||
|
context.setDisplayName(null);
|
||||||
|
context.setPlatformAdmin(null);
|
||||||
|
context.setTenantAdmin(null);
|
||||||
|
context.setPermissions(null);
|
||||||
|
}
|
||||||
|
return context;
|
||||||
|
}
|
||||||
|
|
||||||
private AndroidAuthContext buildContext(String authMode, boolean anonymous, String deviceId,
|
private AndroidAuthContext buildContext(String authMode, boolean anonymous, String deviceId,
|
||||||
String appId, String appVersion, String platform, String accessToken,
|
String appId, String appVersion, String platform, String accessToken,
|
||||||
String fallbackDeviceId, InternalAuthCheckResponse authResult, LoginUser loginUser) {
|
String fallbackDeviceId, InternalAuthCheckResponse authResult, LoginUser loginUser) {
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,9 @@
|
||||||
package com.imeeting.service.android.impl;
|
package com.imeeting.service.android.impl;
|
||||||
|
|
||||||
import com.imeeting.mapper.DeviceInfoMapper;
|
import com.imeeting.mapper.DeviceInfoMapper;
|
||||||
|
import com.imeeting.mapper.DeviceLoginLogMapper;
|
||||||
import com.imeeting.entity.biz.DeviceInfoEntity;
|
import com.imeeting.entity.biz.DeviceInfoEntity;
|
||||||
|
import com.imeeting.entity.biz.DeviceLoginLogEntity;
|
||||||
import com.imeeting.service.android.AndroidDeviceBindingService;
|
import com.imeeting.service.android.AndroidDeviceBindingService;
|
||||||
import lombok.RequiredArgsConstructor;
|
import lombok.RequiredArgsConstructor;
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
|
|
@ -14,6 +16,7 @@ import java.util.Objects;
|
||||||
@RequiredArgsConstructor
|
@RequiredArgsConstructor
|
||||||
public class AndroidDeviceBindingServiceImpl implements AndroidDeviceBindingService {
|
public class AndroidDeviceBindingServiceImpl implements AndroidDeviceBindingService {
|
||||||
private final DeviceInfoMapper deviceInfoMapper;
|
private final DeviceInfoMapper deviceInfoMapper;
|
||||||
|
private final DeviceLoginLogMapper deviceLoginLogMapper;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void bindPrivateDevice(String deviceCode, Long tenantId, Long userId, String appVersion, String platform) {
|
public void bindPrivateDevice(String deviceCode, Long tenantId, Long userId, String appVersion, String platform) {
|
||||||
|
|
@ -61,6 +64,20 @@ public class AndroidDeviceBindingServiceImpl implements AndroidDeviceBindingServ
|
||||||
deviceInfoMapper.updateConnectionInfoByIdIgnoreTenant(existing);
|
deviceInfoMapper.updateConnectionInfoByIdIgnoreTenant(existing);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void recordLogin(String deviceCode, Long tenantId, Long userId) {
|
||||||
|
if (!StringUtils.hasText(deviceCode) || tenantId == null || userId == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
DeviceLoginLogEntity entity = new DeviceLoginLogEntity();
|
||||||
|
entity.setTenantId(tenantId);
|
||||||
|
entity.setStatus(1);
|
||||||
|
entity.setDeviceCode(deviceCode.trim());
|
||||||
|
entity.setUserId(userId);
|
||||||
|
entity.setLoginAt(LocalDateTime.now());
|
||||||
|
deviceLoginLogMapper.insert(entity);
|
||||||
|
}
|
||||||
|
|
||||||
private String normalize(String value) {
|
private String normalize(String value) {
|
||||||
if (!StringUtils.hasText(value)) {
|
if (!StringUtils.hasText(value)) {
|
||||||
return null;
|
return null;
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,246 @@
|
||||||
|
package com.imeeting.service.android.impl;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.databind.JsonNode;
|
||||||
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||||
|
import com.imeeting.common.RedisKeys;
|
||||||
|
import com.imeeting.common.SysParamKeys;
|
||||||
|
import com.imeeting.dto.android.AndroidAuthContext;
|
||||||
|
import com.imeeting.dto.android.AndroidDeviceHomeStatsVO;
|
||||||
|
import com.imeeting.dto.android.AndroidDeviceHomeWeatherVO;
|
||||||
|
import com.imeeting.dto.android.AndroidDeviceWeatherCacheValue;
|
||||||
|
import com.imeeting.dto.biz.MeetingPointsBalanceVO;
|
||||||
|
import com.imeeting.entity.biz.DeviceInfoEntity;
|
||||||
|
import com.imeeting.entity.biz.LicenseEntity;
|
||||||
|
import com.imeeting.mapper.DeviceInfoMapper;
|
||||||
|
import com.imeeting.mapper.DeviceLoginLogMapper;
|
||||||
|
import com.imeeting.mapper.LicenseMapper;
|
||||||
|
import com.imeeting.mapper.biz.MeetingMapper;
|
||||||
|
import com.imeeting.service.android.AndroidDeviceHomeService;
|
||||||
|
import com.imeeting.service.biz.MeetingPointsService;
|
||||||
|
import com.imeeting.support.RedisSupport;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.springframework.beans.factory.annotation.Value;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
import org.springframework.util.StringUtils;
|
||||||
|
|
||||||
|
import java.net.URI;
|
||||||
|
import java.net.URLEncoder;
|
||||||
|
import java.net.http.HttpClient;
|
||||||
|
import java.net.http.HttpRequest;
|
||||||
|
import java.net.http.HttpResponse;
|
||||||
|
import java.io.ByteArrayInputStream;
|
||||||
|
import java.io.InputStream;
|
||||||
|
import java.nio.charset.StandardCharsets;
|
||||||
|
import java.time.Duration;
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
import java.util.Locale;
|
||||||
|
import java.util.zip.GZIPInputStream;
|
||||||
|
|
||||||
|
@Service
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
@Slf4j
|
||||||
|
public class AndroidDeviceHomeServiceImpl implements AndroidDeviceHomeService {
|
||||||
|
|
||||||
|
private static final Duration WEATHER_CACHE_TTL = Duration.ofHours(1);
|
||||||
|
private static final Duration WEATHER_CONNECT_TIMEOUT = Duration.ofSeconds(5);
|
||||||
|
private static final Duration WEATHER_READ_TIMEOUT = Duration.ofSeconds(8);
|
||||||
|
private static final String DEFAULT_QWEATHER_BASE_URL = "https://devapi.qweather.com";
|
||||||
|
|
||||||
|
private final DeviceInfoMapper deviceInfoMapper;
|
||||||
|
private final LicenseMapper licenseMapper;
|
||||||
|
private final MeetingMapper meetingMapper;
|
||||||
|
private final DeviceLoginLogMapper deviceLoginLogMapper;
|
||||||
|
private final MeetingPointsService meetingPointsService;
|
||||||
|
private final RedisSupport redisSupport;
|
||||||
|
private final com.unisbase.service.SysParamService sysParamService;
|
||||||
|
private final ObjectMapper objectMapper;
|
||||||
|
|
||||||
|
@Value("${imeeting.h5.base-url:}")
|
||||||
|
private String h5BaseUrl;
|
||||||
|
|
||||||
|
private final HttpClient httpClient = HttpClient.newBuilder()
|
||||||
|
.connectTimeout(WEATHER_CONNECT_TIMEOUT)
|
||||||
|
.version(HttpClient.Version.HTTP_1_1)
|
||||||
|
.build();
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public AndroidDeviceHomeStatsVO getHomeStats(AndroidAuthContext authContext) {
|
||||||
|
if (authContext == null || !StringUtils.hasText(authContext.getDeviceId())) {
|
||||||
|
throw new RuntimeException("设备上下文不存在");
|
||||||
|
}
|
||||||
|
DeviceInfoEntity device = deviceInfoMapper.selectByDeviceCodeIgnoreTenant(authContext.getDeviceId());
|
||||||
|
if (device == null) {
|
||||||
|
throw new RuntimeException("设备未注册,请先完成设备注册");
|
||||||
|
}
|
||||||
|
|
||||||
|
Long tenantId = authContext.getTenantId();
|
||||||
|
LicenseEntity license = licenseMapper.selectValidBoundByTenantAndDeviceIgnoreTenant(tenantId, authContext.getDeviceId());
|
||||||
|
if (license == null) {
|
||||||
|
throw new RuntimeException("设备未绑定有效授权");
|
||||||
|
}
|
||||||
|
|
||||||
|
LocalDateTime resetAt = device.getStatsResetAt();
|
||||||
|
Long meetingCount = defaultLong(meetingMapper.countByDeviceSince(tenantId, authContext.getDeviceId(), resetAt));
|
||||||
|
Long totalSeconds = defaultLong(meetingMapper.sumMeetingDurationSecondsByDeviceSince(tenantId, authContext.getDeviceId(), resetAt));
|
||||||
|
Long loginUserCount = defaultLong(deviceLoginLogMapper.countDistinctUsersSince(tenantId, authContext.getDeviceId(), resetAt));
|
||||||
|
|
||||||
|
AndroidDeviceHomeStatsVO vo = new AndroidDeviceHomeStatsVO();
|
||||||
|
vo.setDeviceName(device.getDeviceName());
|
||||||
|
vo.setLicenseType(license.getLicenseType());
|
||||||
|
vo.setMeetingCount(meetingCount);
|
||||||
|
vo.setMeetingDurationMinutes(toCeilMinutes(totalSeconds));
|
||||||
|
vo.setLoginUserCount(loginUserCount);
|
||||||
|
vo.setH5BaseUrl(trimToNull(h5BaseUrl));
|
||||||
|
vo.setRemainingMinutes(calculateRemainingMinutes(tenantId, authContext.getUserId(), authContext.isAnonymous()));
|
||||||
|
vo.setWeather(resolveWeather(device.getWeatherCityName()));
|
||||||
|
vo.setLoggedIn(!authContext.isAnonymous() && authContext.getUserId() != null);
|
||||||
|
return vo;
|
||||||
|
}
|
||||||
|
|
||||||
|
private Long calculateRemainingMinutes(Long tenantId, Long userId, boolean anonymous) {
|
||||||
|
if (tenantId == null) {
|
||||||
|
return 0L;
|
||||||
|
}
|
||||||
|
MeetingPointsBalanceVO balance = meetingPointsService.getBalanceView(tenantId, anonymous ? null : userId);
|
||||||
|
long totalPoints = defaultLong(balance.getPublicBalance());
|
||||||
|
if (!anonymous && userId != null) {
|
||||||
|
totalPoints += defaultLong(balance.getPersonalBalance());
|
||||||
|
}
|
||||||
|
int unitMinutes = positiveInt(sysParamService.getCachedParamValue(SysParamKeys.MEETING_POINTS_UNIT_MINUTES, "1"), 1);
|
||||||
|
int costPerUnit = positiveInt(sysParamService.getCachedParamValue(SysParamKeys.MEETING_POINTS_COST_PER_UNIT, "1"), 1);
|
||||||
|
return costPerUnit <= 0 ? 0L : (totalPoints * unitMinutes) / costPerUnit;
|
||||||
|
}
|
||||||
|
|
||||||
|
private AndroidDeviceHomeWeatherVO resolveWeather(String cityName) {
|
||||||
|
if (!StringUtils.hasText(cityName)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
String normalizedCity = cityName.trim();
|
||||||
|
AndroidDeviceWeatherCacheValue cached = redisSupport.getJsonQuietly(RedisKeys.androidDeviceWeatherKey(normalizedCity), AndroidDeviceWeatherCacheValue.class);
|
||||||
|
if (cached != null) {
|
||||||
|
return toWeatherVO(cached);
|
||||||
|
}
|
||||||
|
|
||||||
|
String apiKey = sysParamService.getCachedParamValue(SysParamKeys.DEVICE_WEATHER_QWEATHER_KEY, "");
|
||||||
|
String baseUrl = normalizeBaseUrl(sysParamService.getCachedParamValue(
|
||||||
|
SysParamKeys.DEVICE_WEATHER_QWEATHER_BASE_URL,
|
||||||
|
DEFAULT_QWEATHER_BASE_URL
|
||||||
|
));
|
||||||
|
if (!StringUtils.hasText(apiKey) || !StringUtils.hasText(baseUrl)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
String location = URLEncoder.encode(normalizedCity, StandardCharsets.UTF_8);
|
||||||
|
String url = baseUrl + "/geo/v2/city/lookup?location=" + location + "&key=" + apiKey.trim();
|
||||||
|
HttpRequest cityRequest = HttpRequest.newBuilder(URI.create(url))
|
||||||
|
.timeout(WEATHER_READ_TIMEOUT)
|
||||||
|
.header("Accept-Encoding", "gzip")
|
||||||
|
.GET()
|
||||||
|
.build();
|
||||||
|
HttpResponse<byte[]> cityResponse = httpClient.send(cityRequest, HttpResponse.BodyHandlers.ofByteArray());
|
||||||
|
String locationId = parseLocationId(readResponseBody(cityResponse));
|
||||||
|
if (!StringUtils.hasText(locationId)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
String weatherUrl = baseUrl + "/v7/weather/now?location=" + locationId + "&key=" + apiKey.trim();
|
||||||
|
HttpRequest weatherRequest = HttpRequest.newBuilder(URI.create(weatherUrl))
|
||||||
|
.timeout(WEATHER_READ_TIMEOUT)
|
||||||
|
.header("Accept-Encoding", "gzip")
|
||||||
|
.GET()
|
||||||
|
.build();
|
||||||
|
HttpResponse<byte[]> weatherResponse = httpClient.send(weatherRequest, HttpResponse.BodyHandlers.ofByteArray());
|
||||||
|
AndroidDeviceWeatherCacheValue value = parseWeatherValue(normalizedCity, readResponseBody(weatherResponse));
|
||||||
|
if (value == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
redisSupport.setJson(RedisKeys.androidDeviceWeatherKey(normalizedCity), value, WEATHER_CACHE_TTL);
|
||||||
|
return toWeatherVO(value);
|
||||||
|
} catch (Exception ex) {
|
||||||
|
log.warn("查询设备天气失败, cityName={}", normalizedCity, ex);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private String parseLocationId(String body) throws Exception {
|
||||||
|
JsonNode root = objectMapper.readTree(body);
|
||||||
|
JsonNode location = root.path("location");
|
||||||
|
if (!location.isArray() || location.isEmpty()) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
JsonNode first = location.get(0);
|
||||||
|
String id = first.path("id").asText(null);
|
||||||
|
return StringUtils.hasText(id) ? id.trim() : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private AndroidDeviceWeatherCacheValue parseWeatherValue(String cityName, String body) throws Exception {
|
||||||
|
JsonNode root = objectMapper.readTree(body);
|
||||||
|
JsonNode now = root.path("now");
|
||||||
|
if (now.isMissingNode() || now.isNull()) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
AndroidDeviceWeatherCacheValue value = new AndroidDeviceWeatherCacheValue();
|
||||||
|
value.setCityName(cityName);
|
||||||
|
value.setText(trimToNull(now.path("text").asText(null)));
|
||||||
|
value.setTemperature(trimToNull(now.path("temp").asText(null)));
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
private AndroidDeviceHomeWeatherVO toWeatherVO(AndroidDeviceWeatherCacheValue cached) {
|
||||||
|
AndroidDeviceHomeWeatherVO vo = new AndroidDeviceHomeWeatherVO();
|
||||||
|
vo.setCityName(cached.getCityName());
|
||||||
|
vo.setText(cached.getText());
|
||||||
|
vo.setTemperature(cached.getTemperature());
|
||||||
|
return vo;
|
||||||
|
}
|
||||||
|
|
||||||
|
private long defaultLong(Long value) {
|
||||||
|
return value == null ? 0L : value;
|
||||||
|
}
|
||||||
|
|
||||||
|
private long toCeilMinutes(long durationSeconds) {
|
||||||
|
if (durationSeconds <= 0L) {
|
||||||
|
return 0L;
|
||||||
|
}
|
||||||
|
return (long) Math.ceil(durationSeconds / 60.0d);
|
||||||
|
}
|
||||||
|
|
||||||
|
private int positiveInt(String value, int defaultValue) {
|
||||||
|
try {
|
||||||
|
int resolved = Integer.parseInt(String.valueOf(value).trim());
|
||||||
|
return resolved > 0 ? resolved : defaultValue;
|
||||||
|
} catch (Exception ex) {
|
||||||
|
return defaultValue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private String trimToNull(String value) {
|
||||||
|
return StringUtils.hasText(value) ? value.trim() : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private String normalizeBaseUrl(String value) {
|
||||||
|
if (!StringUtils.hasText(value)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
String normalized = value.trim();
|
||||||
|
while (normalized.endsWith("/")) {
|
||||||
|
normalized = normalized.substring(0, normalized.length() - 1);
|
||||||
|
}
|
||||||
|
return normalized;
|
||||||
|
}
|
||||||
|
|
||||||
|
private String readResponseBody(HttpResponse<byte[]> response) throws Exception {
|
||||||
|
if (response == null || response.body() == null) {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
String contentEncoding = response.headers().firstValue("Content-Encoding").orElse("");
|
||||||
|
byte[] payload = response.body();
|
||||||
|
if (!StringUtils.hasText(contentEncoding) || !contentEncoding.toLowerCase(Locale.ROOT).contains("gzip")) {
|
||||||
|
return new String(payload, StandardCharsets.UTF_8);
|
||||||
|
}
|
||||||
|
try (InputStream inputStream = new GZIPInputStream(new ByteArrayInputStream(payload))) {
|
||||||
|
return new String(inputStream.readAllBytes(), StandardCharsets.UTF_8);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,11 +1,18 @@
|
||||||
package com.imeeting.service.android.impl;
|
package com.imeeting.service.android.impl;
|
||||||
|
|
||||||
|
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||||
import com.imeeting.dto.android.AndroidDeviceRegisterResponse;
|
import com.imeeting.dto.android.AndroidDeviceRegisterResponse;
|
||||||
import com.imeeting.entity.biz.DeviceInfoEntity;
|
import com.imeeting.entity.biz.DeviceInfoEntity;
|
||||||
|
import com.imeeting.entity.biz.LicenseEntity;
|
||||||
import com.imeeting.mapper.DeviceInfoMapper;
|
import com.imeeting.mapper.DeviceInfoMapper;
|
||||||
import com.imeeting.service.android.AndroidDeviceRegistrationService;
|
import com.imeeting.service.android.AndroidDeviceRegistrationService;
|
||||||
|
import com.imeeting.service.biz.LicenseService;
|
||||||
|
import com.unisbase.common.exception.BusinessException;
|
||||||
|
import com.unisbase.entity.SysTenant;
|
||||||
|
import com.unisbase.mapper.SysTenantMapper;
|
||||||
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.util.StringUtils;
|
import org.springframework.util.StringUtils;
|
||||||
|
|
||||||
import java.time.LocalDateTime;
|
import java.time.LocalDateTime;
|
||||||
|
|
@ -14,35 +21,49 @@ import java.time.LocalDateTime;
|
||||||
@RequiredArgsConstructor
|
@RequiredArgsConstructor
|
||||||
public class AndroidDeviceRegistrationServiceImpl implements AndroidDeviceRegistrationService {
|
public class AndroidDeviceRegistrationServiceImpl implements AndroidDeviceRegistrationService {
|
||||||
private final DeviceInfoMapper deviceInfoMapper;
|
private final DeviceInfoMapper deviceInfoMapper;
|
||||||
|
private final SysTenantMapper sysTenantMapper;
|
||||||
|
private final LicenseService licenseService;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public AndroidDeviceRegisterResponse register(String deviceCode, String deviceName, String terminalType, String terminalVersion) {
|
@Transactional(rollbackFor = Exception.class)
|
||||||
if (!StringUtils.hasText(deviceCode)) {
|
public AndroidDeviceRegisterResponse register(String tenantCode, String deviceCode, String deviceName, String terminalType, String terminalVersion) {
|
||||||
throw new RuntimeException("deviceId不能为空");
|
if (!StringUtils.hasText(tenantCode)) {
|
||||||
|
throw new BusinessException("400", "tenantCode不能为空");
|
||||||
}
|
}
|
||||||
|
if (!StringUtils.hasText(deviceCode)) {
|
||||||
|
throw new BusinessException("400", "deviceId不能为空");
|
||||||
|
}
|
||||||
|
SysTenant tenant = requireTenant(tenantCode.trim());
|
||||||
String normalizedDeviceCode = deviceCode.trim();
|
String normalizedDeviceCode = deviceCode.trim();
|
||||||
|
licenseService.validateDeviceCanRegisterToTenant(normalizedDeviceCode, tenant.getId());
|
||||||
|
|
||||||
DeviceInfoEntity existing = deviceInfoMapper.selectByDeviceCodeIgnoreTenant(normalizedDeviceCode);
|
DeviceInfoEntity existing = deviceInfoMapper.selectByDeviceCodeIgnoreTenant(normalizedDeviceCode);
|
||||||
if (existing == null) {
|
if (existing == null) {
|
||||||
existing = new DeviceInfoEntity();
|
existing = new DeviceInfoEntity();
|
||||||
existing.setDeviceCode(normalizedDeviceCode);
|
existing.setDeviceCode(normalizedDeviceCode);
|
||||||
existing.setDeviceName(normalize(deviceName));
|
|
||||||
existing.setTerminalType(normalizeTerminalType(terminalType));
|
|
||||||
existing.setTerminalVersion(normalize(terminalVersion));
|
|
||||||
existing.setLastOnlineAt(LocalDateTime.now());
|
|
||||||
existing.setStatus(1);
|
existing.setStatus(1);
|
||||||
|
}
|
||||||
|
existing.setTenantId(tenant.getId());
|
||||||
|
existing.setDeviceName(normalize(deviceName));
|
||||||
|
existing.setTerminalType(normalizeTerminalType(terminalType));
|
||||||
|
existing.setTerminalVersion(normalize(terminalVersion));
|
||||||
|
existing.setLastOnlineAt(LocalDateTime.now());
|
||||||
|
|
||||||
|
if (existing.getDeviceId() == null) {
|
||||||
deviceInfoMapper.insert(existing);
|
deviceInfoMapper.insert(existing);
|
||||||
} else {
|
} else {
|
||||||
existing.setDeviceName(normalize(deviceName));
|
deviceInfoMapper.updateById(existing);
|
||||||
existing.setTerminalType(normalizeTerminalType(terminalType));
|
|
||||||
existing.setTerminalVersion(normalize(terminalVersion));
|
|
||||||
deviceInfoMapper.updateBaseInfoByIdIgnoreTenant(existing);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
LicenseEntity license = licenseService.allocateForDeviceRegistration(tenant.getId(), tenant.getTenantCode(), normalizedDeviceCode);
|
||||||
|
|
||||||
AndroidDeviceRegisterResponse response = new AndroidDeviceRegisterResponse();
|
AndroidDeviceRegisterResponse response = new AndroidDeviceRegisterResponse();
|
||||||
response.setDeviceCode(existing.getDeviceCode());
|
response.setDeviceCode(existing.getDeviceCode());
|
||||||
response.setDeviceName(existing.getDeviceName());
|
response.setDeviceName(existing.getDeviceName());
|
||||||
response.setTerminalType(existing.getTerminalType());
|
response.setTerminalType(existing.getTerminalType());
|
||||||
response.setTerminalVersion(existing.getTerminalVersion());
|
response.setTerminalVersion(existing.getTerminalVersion());
|
||||||
response.setOccupied(existing.getUserId() != null);
|
response.setOccupied(existing.getUserId() != null);
|
||||||
|
response.setLicenseType(license.getLicenseType());
|
||||||
return response;
|
return response;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -53,6 +74,17 @@ public class AndroidDeviceRegistrationServiceImpl implements AndroidDeviceRegist
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private SysTenant requireTenant(String tenantCode) {
|
||||||
|
SysTenant tenant = sysTenantMapper.selectOne(new LambdaQueryWrapper<SysTenant>()
|
||||||
|
.eq(SysTenant::getTenantCode, tenantCode)
|
||||||
|
.eq(SysTenant::getIsDeleted, 0)
|
||||||
|
.last("LIMIT 1"));
|
||||||
|
if (tenant == null || tenant.getId() == null) {
|
||||||
|
throw new BusinessException("400", "租户不存在");
|
||||||
|
}
|
||||||
|
return tenant;
|
||||||
|
}
|
||||||
|
|
||||||
private String normalize(String value) {
|
private String normalize(String value) {
|
||||||
if (!StringUtils.hasText(value)) {
|
if (!StringUtils.hasText(value)) {
|
||||||
return null;
|
return null;
|
||||||
|
|
|
||||||
|
|
@ -18,4 +18,8 @@ public interface DeviceOnlineManagementService {
|
||||||
DeviceOnlineAdminVO update(Long id, DeviceAdminUpdateCommand command, LoginUser loginUser);
|
DeviceOnlineAdminVO update(Long id, DeviceAdminUpdateCommand command, LoginUser loginUser);
|
||||||
|
|
||||||
boolean kick(Long id, LoginUser loginUser);
|
boolean kick(Long id, LoginUser loginUser);
|
||||||
|
|
||||||
|
boolean delete(Long id, LoginUser loginUser);
|
||||||
|
|
||||||
|
boolean resetStats(Long id, LoginUser loginUser);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,26 @@
|
||||||
|
package com.imeeting.service.biz;
|
||||||
|
|
||||||
|
import com.imeeting.dto.biz.LicenseImportResultVO;
|
||||||
|
import com.imeeting.dto.biz.LicenseVO;
|
||||||
|
import com.imeeting.entity.biz.LicenseEntity;
|
||||||
|
import com.unisbase.security.LoginUser;
|
||||||
|
import org.springframework.web.multipart.MultipartFile;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
public interface LicenseService {
|
||||||
|
void initializeTemporaryLicenses(Long tenantId);
|
||||||
|
|
||||||
|
LicenseEntity allocateForDeviceRegistration(Long tenantId, String tenantCode, String deviceCode);
|
||||||
|
|
||||||
|
LicenseEntity requireValidBoundLicense(String deviceCode);
|
||||||
|
|
||||||
|
void validateDeviceCanRegisterToTenant(String deviceCode, Long tenantId);
|
||||||
|
|
||||||
|
void unbindDeviceLicense(String deviceCode);
|
||||||
|
|
||||||
|
LicenseImportResultVO importFormalLicenses(MultipartFile file, LoginUser loginUser) throws IOException;
|
||||||
|
|
||||||
|
List<LicenseVO> listCurrentTenantLicenses(LoginUser loginUser);
|
||||||
|
}
|
||||||
|
|
@ -159,7 +159,10 @@ public class AiTaskServiceImpl extends ServiceImpl<AiTaskMapper, AiTask> impleme
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void triggerQueuedAsrScheduling() {
|
public void triggerQueuedAsrScheduling() {
|
||||||
scheduleQueuedAsrTasks();
|
taskSecurityContextRunner.callAsPlatformAdmin(() -> {
|
||||||
|
scheduleQueuedAsrTasks();
|
||||||
|
return null;
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|
|
||||||
|
|
@ -9,9 +9,11 @@ import com.imeeting.mapper.DeviceInfoMapper;
|
||||||
import com.imeeting.service.android.AndroidDeviceSessionService;
|
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.unisbase.security.LoginUser;
|
import com.unisbase.security.LoginUser;
|
||||||
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.util.StringUtils;
|
import org.springframework.util.StringUtils;
|
||||||
|
|
||||||
import java.time.Instant;
|
import java.time.Instant;
|
||||||
|
|
@ -26,6 +28,7 @@ public class DeviceOnlineManagementServiceImpl implements DeviceOnlineManagement
|
||||||
private final DeviceInfoMapper deviceInfoMapper;
|
private final DeviceInfoMapper deviceInfoMapper;
|
||||||
private final AndroidDeviceSessionService androidDeviceSessionService;
|
private final AndroidDeviceSessionService androidDeviceSessionService;
|
||||||
private final AndroidGatewayPushService androidGatewayPushService;
|
private final AndroidGatewayPushService androidGatewayPushService;
|
||||||
|
private final LicenseService licenseService;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void recordConnected(AndroidAuthContext authContext) {
|
public void recordConnected(AndroidAuthContext authContext) {
|
||||||
|
|
@ -43,7 +46,7 @@ public class DeviceOnlineManagementServiceImpl implements DeviceOnlineManagement
|
||||||
existing.setLastOnlineAt(now);
|
existing.setLastOnlineAt(now);
|
||||||
existing.setUserId(authContext.getUserId());
|
existing.setUserId(authContext.getUserId());
|
||||||
existing.setTenantId(authContext.getTenantId());
|
existing.setTenantId(authContext.getTenantId());
|
||||||
deviceInfoMapper.updateConnectionInfoByIdIgnoreTenant(existing);
|
deviceInfoMapper.updateById(existing);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|
@ -78,6 +81,7 @@ public class DeviceOnlineManagementServiceImpl implements DeviceOnlineManagement
|
||||||
public DeviceOnlineAdminVO update(Long id, DeviceAdminUpdateCommand command, LoginUser loginUser) {
|
public DeviceOnlineAdminVO update(Long id, DeviceAdminUpdateCommand command, LoginUser loginUser) {
|
||||||
DeviceInfoEntity existing = requireVisibleDevice(id, loginUser);
|
DeviceInfoEntity existing = requireVisibleDevice(id, loginUser);
|
||||||
existing.setDeviceName(normalize(command.getDeviceName()));
|
existing.setDeviceName(normalize(command.getDeviceName()));
|
||||||
|
existing.setWeatherCityName(normalize(command.getWeatherCityName()));
|
||||||
boolean disableAfterUpdate = command.getStatus() != null && command.getStatus() == 0;
|
boolean disableAfterUpdate = command.getStatus() != null && command.getStatus() == 0;
|
||||||
if (command.getStatus() != null) {
|
if (command.getStatus() != null) {
|
||||||
existing.setStatus(command.getStatus());
|
existing.setStatus(command.getStatus());
|
||||||
|
|
@ -99,6 +103,25 @@ public class DeviceOnlineManagementServiceImpl implements DeviceOnlineManagement
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
@Transactional(rollbackFor = Exception.class)
|
||||||
|
public boolean delete(Long id, LoginUser loginUser) {
|
||||||
|
DeviceInfoEntity existing = requireVisibleDevice(id, loginUser);
|
||||||
|
disconnectDevice(existing.getDeviceCode());
|
||||||
|
licenseService.unbindDeviceLicense(existing.getDeviceCode());
|
||||||
|
existing.setTenantId(null);
|
||||||
|
existing.setUserId(null);
|
||||||
|
deviceInfoMapper.updateById(existing);
|
||||||
|
return deviceInfoMapper.deleteById(existing.getDeviceId()) > 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean resetStats(Long id, LoginUser loginUser) {
|
||||||
|
DeviceInfoEntity existing = requireVisibleDevice(id, loginUser);
|
||||||
|
existing.setStatsResetAt(LocalDateTime.now());
|
||||||
|
return deviceInfoMapper.updateById(existing) > 0;
|
||||||
|
}
|
||||||
|
|
||||||
private DeviceInfoEntity requireVisibleDevice(Long id, LoginUser loginUser) {
|
private DeviceInfoEntity requireVisibleDevice(Long id, LoginUser loginUser) {
|
||||||
DeviceInfoEntity existing = deviceInfoMapper.selectByIdIgnoreTenant(id);
|
DeviceInfoEntity existing = deviceInfoMapper.selectByIdIgnoreTenant(id);
|
||||||
if (existing == null) {
|
if (existing == null) {
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,285 @@
|
||||||
|
package com.imeeting.service.biz.impl;
|
||||||
|
|
||||||
|
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
|
||||||
|
import com.imeeting.common.LicenseConstants;
|
||||||
|
import com.imeeting.common.SysParamKeys;
|
||||||
|
import com.imeeting.dto.biz.LicenseImportResultVO;
|
||||||
|
import com.imeeting.dto.biz.LicenseImportRow;
|
||||||
|
import com.imeeting.dto.biz.LicenseVO;
|
||||||
|
import com.imeeting.entity.biz.LicenseEntity;
|
||||||
|
import com.imeeting.enums.LicenseStatusEnum;
|
||||||
|
import com.imeeting.enums.LicenseTypeEnum;
|
||||||
|
import com.imeeting.mapper.LicenseMapper;
|
||||||
|
import com.imeeting.service.biz.LicenseService;
|
||||||
|
import com.unisbase.common.exception.BusinessException;
|
||||||
|
import com.unisbase.security.LoginUser;
|
||||||
|
import com.unisbase.service.SysParamService;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
|
import org.springframework.util.StringUtils;
|
||||||
|
import org.springframework.web.multipart.MultipartFile;
|
||||||
|
|
||||||
|
import java.io.BufferedReader;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.InputStreamReader;
|
||||||
|
import java.nio.charset.StandardCharsets;
|
||||||
|
import java.time.LocalDate;
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
import java.time.format.DateTimeFormatter;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Locale;
|
||||||
|
|
||||||
|
@Service
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class LicenseServiceImpl extends ServiceImpl<LicenseMapper, LicenseEntity> implements LicenseService {
|
||||||
|
|
||||||
|
private static final DateTimeFormatter TEMP_SERIAL_DATE = DateTimeFormatter.ofPattern("yyyyMMdd");
|
||||||
|
private static final DateTimeFormatter IMPORT_BATCH_TIME = DateTimeFormatter.ofPattern("yyyyMMddHHmmss");
|
||||||
|
|
||||||
|
private final SysParamService sysParamService;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
@Transactional(rollbackFor = Exception.class)
|
||||||
|
public void initializeTemporaryLicenses(Long tenantId) {
|
||||||
|
if (tenantId == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
int count = parsePositiveInt(sysParamService.getParamValue(SysParamKeys.LICENSE_TEMP_DEFAULT_COUNT, "0"));
|
||||||
|
if (count <= 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
int expireMonths = Math.max(1, parsePositiveInt(sysParamService.getParamValue(SysParamKeys.LICENSE_TEMP_DEFAULT_EXPIRE_MONTHS, "3")));
|
||||||
|
String productCode = normalize(sysParamService.getParamValue(SysParamKeys.LICENSE_DEFAULT_PRODUCT_CODE, ""));
|
||||||
|
if (!StringUtils.hasText(productCode)) {
|
||||||
|
throw new BusinessException("500", "未配置临时授权产品编码");
|
||||||
|
}
|
||||||
|
LocalDateTime expireTime = LocalDateTime.now().plusMonths(expireMonths);
|
||||||
|
List<LicenseEntity> entities = new ArrayList<>(count);
|
||||||
|
for (int i = 0; i < count; i++) {
|
||||||
|
String serial = buildTemporarySerial(productCode);
|
||||||
|
LicenseEntity entity = new LicenseEntity();
|
||||||
|
entity.setTenantId(tenantId);
|
||||||
|
entity.setLicenseSerial(serial);
|
||||||
|
entity.setLicenseCode(serial);
|
||||||
|
entity.setLicenseType(LicenseTypeEnum.TEMPORARY.getCode());
|
||||||
|
entity.setLicenseStatus(LicenseStatusEnum.UNUSED.getCode());
|
||||||
|
entity.setProductCode(productCode);
|
||||||
|
entity.setExpireTime(expireTime);
|
||||||
|
entities.add(entity);
|
||||||
|
}
|
||||||
|
saveBatch(entities);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
@Transactional(rollbackFor = Exception.class)
|
||||||
|
public LicenseEntity allocateForDeviceRegistration(Long tenantId, String tenantCode, String deviceCode) {
|
||||||
|
expireDueLicenses();
|
||||||
|
LicenseEntity bound = baseMapper.selectActiveByTenantAndDeviceIgnoreTenant(tenantId, deviceCode);
|
||||||
|
if (bound != null) {
|
||||||
|
return bound;
|
||||||
|
}
|
||||||
|
LicenseEntity license = baseMapper.selectFirstAssignableLicense(tenantId);
|
||||||
|
if (license == null) {
|
||||||
|
throw new BusinessException("402", "无可用授权码");
|
||||||
|
}
|
||||||
|
LocalDateTime now = LocalDateTime.now();
|
||||||
|
baseMapper.bindLicenseById(license.getId(), deviceCode, now, LicenseStatusEnum.IN_USE.getCode());
|
||||||
|
license.setDeviceCode(deviceCode);
|
||||||
|
license.setBindTime(now);
|
||||||
|
license.setLicenseStatus(LicenseStatusEnum.IN_USE.getCode());
|
||||||
|
return license;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public LicenseEntity requireValidBoundLicense(String deviceCode) {
|
||||||
|
expireDueLicenses();
|
||||||
|
LicenseEntity license = baseMapper.selectByDeviceCodeIgnoreTenant(deviceCode);
|
||||||
|
if (license == null || license.getLicenseStatus() == null || license.getLicenseStatus() != LicenseStatusEnum.IN_USE.getCode()) {
|
||||||
|
throw new BusinessException("403", "设备未绑定有效授权");
|
||||||
|
}
|
||||||
|
if (license.getExpireTime() != null && !license.getExpireTime().isAfter(LocalDateTime.now())) {
|
||||||
|
baseMapper.clearBindingById(license.getId(), LicenseStatusEnum.EXPIRED.getCode());
|
||||||
|
throw new BusinessException("403", "设备授权已过期");
|
||||||
|
}
|
||||||
|
return license;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void validateDeviceCanRegisterToTenant(String deviceCode, Long tenantId) {
|
||||||
|
if (!StringUtils.hasText(deviceCode) || tenantId == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
LicenseEntity existing = baseMapper.selectByDeviceCodeIgnoreTenant(deviceCode.trim());
|
||||||
|
if (existing == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!tenantId.equals(existing.getTenantId())) {
|
||||||
|
throw new BusinessException("403", "该设备已绑定,无法重复绑定");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
@Transactional(rollbackFor = Exception.class)
|
||||||
|
public void unbindDeviceLicense(String deviceCode) {
|
||||||
|
if (!StringUtils.hasText(deviceCode)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
expireDueLicenses();
|
||||||
|
LicenseEntity license = baseMapper.selectByDeviceCodeIgnoreTenant(deviceCode.trim());
|
||||||
|
if (license == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
int targetStatus = isExpired(license) ? LicenseStatusEnum.EXPIRED.getCode() : LicenseStatusEnum.UNUSED.getCode();
|
||||||
|
baseMapper.clearBindingById(license.getId(), targetStatus);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
@Transactional(rollbackFor = Exception.class)
|
||||||
|
public LicenseImportResultVO importFormalLicenses(MultipartFile file, LoginUser loginUser) throws IOException {
|
||||||
|
Long tenantId = currentTenantId(loginUser);
|
||||||
|
List<LicenseImportRow> rows = parseImportRows(file);
|
||||||
|
if (rows.isEmpty()) {
|
||||||
|
throw new BusinessException("400", "导入文件不能为空");
|
||||||
|
}
|
||||||
|
expireDueLicenses();
|
||||||
|
|
||||||
|
List<LicenseEntity> boundTempLicenses = baseMapper.selectBoundTemporaryLicensesForReplace(tenantId);
|
||||||
|
if (rows.size() < boundTempLicenses.size()) {
|
||||||
|
throw new BusinessException("400", "正式授权数量不足,无法完成替换");
|
||||||
|
}
|
||||||
|
validateImportRows(tenantId, rows);
|
||||||
|
|
||||||
|
String importBatchNo = buildImportBatchNo();
|
||||||
|
LocalDateTime now = LocalDateTime.now();
|
||||||
|
List<LicenseEntity> imported = new ArrayList<>(rows.size());
|
||||||
|
for (LicenseImportRow row : rows) {
|
||||||
|
LicenseEntity entity = new LicenseEntity();
|
||||||
|
entity.setTenantId(tenantId);
|
||||||
|
entity.setLicenseSerial(row.getLicenseSerial());
|
||||||
|
entity.setLicenseCode(row.getLicenseCode());
|
||||||
|
entity.setProductCode(normalize(row.getProductCode()));
|
||||||
|
entity.setRemark(normalize(row.getRemark()));
|
||||||
|
entity.setLicenseType(LicenseTypeEnum.FORMAL.getCode());
|
||||||
|
entity.setLicenseStatus(LicenseStatusEnum.UNUSED.getCode());
|
||||||
|
entity.setImportBatchNo(importBatchNo);
|
||||||
|
entity.setImportTime(now);
|
||||||
|
imported.add(entity);
|
||||||
|
}
|
||||||
|
saveBatch(imported);
|
||||||
|
|
||||||
|
List<LicenseEntity> unusedFormalLicenses = baseMapper.selectUnusedFormalLicenses(tenantId, importBatchNo);
|
||||||
|
for (int i = 0; i < boundTempLicenses.size(); i++) {
|
||||||
|
LicenseEntity temp = boundTempLicenses.get(i);
|
||||||
|
LicenseEntity formal = unusedFormalLicenses.get(i);
|
||||||
|
baseMapper.bindLicenseById(formal.getId(), temp.getDeviceCode(), now, LicenseStatusEnum.IN_USE.getCode());
|
||||||
|
baseMapper.invalidateById(temp.getId());
|
||||||
|
}
|
||||||
|
int invalidatedTempCount = baseMapper.invalidateAllTemporaryLicenses(tenantId);
|
||||||
|
|
||||||
|
LicenseImportResultVO result = new LicenseImportResultVO();
|
||||||
|
result.setImportBatchNo(importBatchNo);
|
||||||
|
result.setTotalCount(imported.size());
|
||||||
|
result.setReplacedCount(boundTempLicenses.size());
|
||||||
|
result.setUnusedFormalCount(imported.size() - boundTempLicenses.size());
|
||||||
|
result.setInvalidatedTempCount(invalidatedTempCount);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<LicenseVO> listCurrentTenantLicenses(LoginUser loginUser) {
|
||||||
|
return baseMapper.selectListByTenant(currentTenantId(loginUser));
|
||||||
|
}
|
||||||
|
|
||||||
|
private void validateImportRows(Long tenantId, List<LicenseImportRow> rows) {
|
||||||
|
for (LicenseImportRow row : rows) {
|
||||||
|
if (!StringUtils.hasText(row.getLicenseSerial()) || !StringUtils.hasText(row.getLicenseCode())) {
|
||||||
|
throw new BusinessException("400", "导入文件缺少授权序列号或授权码");
|
||||||
|
}
|
||||||
|
if (baseMapper.selectBySerialIgnoreTenant(row.getLicenseSerial()) != null) {
|
||||||
|
throw new BusinessException("400", "授权序列号已存在:" + row.getLicenseSerial());
|
||||||
|
}
|
||||||
|
if (baseMapper.selectByTenantAndCodeIgnoreTenant(tenantId, row.getLicenseCode()) != null) {
|
||||||
|
throw new BusinessException("400", "授权码已存在:" + row.getLicenseCode());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<LicenseImportRow> parseImportRows(MultipartFile file) throws IOException {
|
||||||
|
if (file == null || file.isEmpty()) {
|
||||||
|
throw new BusinessException("400", "导入文件不能为空");
|
||||||
|
}
|
||||||
|
List<LicenseImportRow> rows = new ArrayList<>();
|
||||||
|
try (BufferedReader reader = new BufferedReader(new InputStreamReader(file.getInputStream(), StandardCharsets.UTF_8))) {
|
||||||
|
String line;
|
||||||
|
int lineNo = 0;
|
||||||
|
while ((line = reader.readLine()) != null) {
|
||||||
|
lineNo++;
|
||||||
|
String trimmed = line.trim();
|
||||||
|
if (trimmed.isEmpty()) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (lineNo == 1 && trimmed.toLowerCase(Locale.ROOT).startsWith("license_serial")) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
String[] parts = trimmed.split(",", -1);
|
||||||
|
if (parts.length < 2) {
|
||||||
|
throw new BusinessException("400", "第" + lineNo + "行格式错误");
|
||||||
|
}
|
||||||
|
LicenseImportRow row = new LicenseImportRow();
|
||||||
|
row.setLicenseSerial(normalize(parts[0]));
|
||||||
|
row.setLicenseCode(normalize(parts[1]));
|
||||||
|
row.setProductCode(parts.length > 2 ? normalize(parts[2]) : null);
|
||||||
|
row.setRemark(parts.length > 3 ? normalize(parts[3]) : null);
|
||||||
|
rows.add(row);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return rows;
|
||||||
|
}
|
||||||
|
|
||||||
|
private String buildTemporarySerial(String productCode) {
|
||||||
|
Long sequence = baseMapper.nextTempSerialValue();
|
||||||
|
if (sequence == null) {
|
||||||
|
throw new BusinessException("500", "临时授权序列号生成失败");
|
||||||
|
}
|
||||||
|
return LicenseConstants.TEMP_SERIAL_PREFIX
|
||||||
|
+ productCode
|
||||||
|
+ LocalDate.now().format(TEMP_SERIAL_DATE)
|
||||||
|
+ String.format("%08d", sequence);
|
||||||
|
}
|
||||||
|
|
||||||
|
private String buildImportBatchNo() {
|
||||||
|
return LicenseConstants.IMPORT_BATCH_PREFIX + LocalDateTime.now().format(IMPORT_BATCH_TIME);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void expireDueLicenses() {
|
||||||
|
baseMapper.expireDueLicenses();
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean isExpired(LicenseEntity license) {
|
||||||
|
return license.getExpireTime() != null && !license.getExpireTime().isAfter(LocalDateTime.now());
|
||||||
|
}
|
||||||
|
|
||||||
|
private String normalize(String value) {
|
||||||
|
return StringUtils.hasText(value) ? value.trim() : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private int parsePositiveInt(String value) {
|
||||||
|
if (!StringUtils.hasText(value)) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
return Integer.parseInt(value.trim());
|
||||||
|
} catch (NumberFormatException ex) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private Long currentTenantId(LoginUser loginUser) {
|
||||||
|
if (loginUser == null || loginUser.getTenantId() == null) {
|
||||||
|
throw new BusinessException("403", "缺少租户上下文");
|
||||||
|
}
|
||||||
|
return loginUser.getTenantId();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -52,6 +52,7 @@ public class ScreenSaverServiceImpl extends ServiceImpl<ScreenSaverMapper, Scree
|
||||||
private static final String SCOPE_PLATFORM = "PLATFORM";
|
private static final String SCOPE_PLATFORM = "PLATFORM";
|
||||||
private static final String SCOPE_USER = "USER";
|
private static final String SCOPE_USER = "USER";
|
||||||
private static final String SCOPE_MIXED = "MIXED";
|
private static final String SCOPE_MIXED = "MIXED";
|
||||||
|
private static final long PLATFORM_TENANT_ID = 0L;
|
||||||
private static final int DEFAULT_DISPLAY_DURATION_SEC = 15;
|
private static final int DEFAULT_DISPLAY_DURATION_SEC = 15;
|
||||||
private static final int MIN_DISPLAY_DURATION_SEC = 3;
|
private static final int MIN_DISPLAY_DURATION_SEC = 3;
|
||||||
private static final int MAX_DISPLAY_DURATION_SEC = 3600;
|
private static final int MAX_DISPLAY_DURATION_SEC = 3600;
|
||||||
|
|
@ -236,7 +237,8 @@ public class ScreenSaverServiceImpl extends ServiceImpl<ScreenSaverMapper, Scree
|
||||||
Integer displayDurationSec = resolveDisplayDurationSec(tenantId, userId);
|
Integer displayDurationSec = resolveDisplayDurationSec(tenantId, userId);
|
||||||
List<ScreenSaver> platformItems = listActiveByScope(SCOPE_PLATFORM, null);
|
List<ScreenSaver> platformItems = listActiveByScope(SCOPE_PLATFORM, null);
|
||||||
if (userId == null) {
|
if (userId == null) {
|
||||||
return new ScreenSaverSelectionResult(SCOPE_PLATFORM, displayDurationSec, toAdminVOs(platformItems, Map.of(), displayDurationSec));
|
List<ScreenSaver> selectedPlatformItems = platformItems.isEmpty() ? listGlobalFallbackPlatformItems() : platformItems;
|
||||||
|
return new ScreenSaverSelectionResult(SCOPE_PLATFORM, displayDurationSec, toAdminVOs(selectedPlatformItems, Map.of(), displayDurationSec));
|
||||||
}
|
}
|
||||||
|
|
||||||
Map<Long, Integer> userStatusMap = queryUserStatusMap(tenantId, userId, extractPlatformIds(platformItems));
|
Map<Long, Integer> userStatusMap = queryUserStatusMap(tenantId, userId, extractPlatformIds(platformItems));
|
||||||
|
|
@ -249,6 +251,16 @@ public class ScreenSaverServiceImpl extends ServiceImpl<ScreenSaverMapper, Scree
|
||||||
selected.addAll(effectivePlatformItems);
|
selected.addAll(effectivePlatformItems);
|
||||||
selected.addAll(userItems);
|
selected.addAll(userItems);
|
||||||
selected.sort(SCREEN_SAVER_ORDER);
|
selected.sort(SCREEN_SAVER_ORDER);
|
||||||
|
if (selected.isEmpty()) {
|
||||||
|
List<ScreenSaver> fallbackPlatformItems = listGlobalFallbackPlatformItems();
|
||||||
|
if (!fallbackPlatformItems.isEmpty()) {
|
||||||
|
return new ScreenSaverSelectionResult(
|
||||||
|
SCOPE_PLATFORM,
|
||||||
|
displayDurationSec,
|
||||||
|
toAdminVOs(fallbackPlatformItems, Map.of(), displayDurationSec)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return new ScreenSaverSelectionResult(
|
return new ScreenSaverSelectionResult(
|
||||||
resolveSourceScope(effectivePlatformItems, userItems),
|
resolveSourceScope(effectivePlatformItems, userItems),
|
||||||
|
|
@ -257,6 +269,13 @@ public class ScreenSaverServiceImpl extends ServiceImpl<ScreenSaverMapper, Scree
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected List<ScreenSaver> listGlobalFallbackPlatformItems() {
|
||||||
|
if (baseMapper == null) {
|
||||||
|
return List.of();
|
||||||
|
}
|
||||||
|
return baseMapper.selectActivePlatformByTenantIgnoreTenant(PLATFORM_TENANT_ID);
|
||||||
|
}
|
||||||
|
|
||||||
private ScreenSaverDTO normalizeCreateDto(ScreenSaverDTO dto, LoginUser loginUser) {
|
private ScreenSaverDTO normalizeCreateDto(ScreenSaverDTO dto, LoginUser loginUser) {
|
||||||
if (dto == null) {
|
if (dto == null) {
|
||||||
return null;
|
return null;
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,73 @@
|
||||||
|
package com.imeeting.service.biz.impl;
|
||||||
|
|
||||||
|
import com.unisbase.dto.CreateTenantDTO;
|
||||||
|
import com.unisbase.dto.PageResult;
|
||||||
|
import com.unisbase.dto.SysTenantDTO;
|
||||||
|
import com.unisbase.service.SysTenantService;
|
||||||
|
import com.unisbase.service.TenantManagementService;
|
||||||
|
import com.unisbase.service.TenantModeService;
|
||||||
|
import com.imeeting.service.biz.LicenseService;
|
||||||
|
import org.springframework.context.annotation.Primary;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
@Service
|
||||||
|
@Primary
|
||||||
|
public class TenantManagementServicePrimaryImpl implements TenantManagementService {
|
||||||
|
|
||||||
|
private final SysTenantService sysTenantService;
|
||||||
|
private final TenantModeService tenantModeService;
|
||||||
|
private final LicenseService licenseService;
|
||||||
|
|
||||||
|
public TenantManagementServicePrimaryImpl(SysTenantService sysTenantService,
|
||||||
|
TenantModeService tenantModeService,
|
||||||
|
LicenseService licenseService) {
|
||||||
|
this.sysTenantService = sysTenantService;
|
||||||
|
this.tenantModeService = tenantModeService;
|
||||||
|
this.licenseService = licenseService;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public PageResult<List<SysTenantDTO>> listTenants(Integer current, Integer size, String name, String code) {
|
||||||
|
if (tenantModeService.isSingleTenantMode()) {
|
||||||
|
SysTenantDTO defaultTenant = sysTenantService.findById(tenantModeService.getDefaultTenantId());
|
||||||
|
PageResult<List<SysTenantDTO>> result = new PageResult<>();
|
||||||
|
result.setRecords(defaultTenant == null ? List.of() : List.of(defaultTenant));
|
||||||
|
result.setTotal(defaultTenant == null ? 0 : 1);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
return sysTenantService.page(current, size, name, code);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public SysTenantDTO getTenant(Long tenantId) {
|
||||||
|
return sysTenantService.findById(tenantId);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
@Transactional(rollbackFor = Exception.class)
|
||||||
|
public Long createTenant(CreateTenantDTO tenant) {
|
||||||
|
tenantModeService.assertTenantLifecycleAllowed();
|
||||||
|
Long tenantId = sysTenantService.createTenantWithAdmin(tenant);
|
||||||
|
licenseService.initializeTemporaryLicenses(tenantId);
|
||||||
|
return tenantId;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean updateTenant(Long tenantId, SysTenantDTO tenant) {
|
||||||
|
tenantModeService.assertDefaultTenantCanBeUpdated(tenantId, tenant == null ? null : tenant.getStatus());
|
||||||
|
if (tenant == null) {
|
||||||
|
throw new IllegalArgumentException("租户信息不能为空");
|
||||||
|
}
|
||||||
|
tenant.setId(tenantId);
|
||||||
|
return sysTenantService.update(tenant);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean deleteTenant(Long tenantId) {
|
||||||
|
tenantModeService.assertTenantLifecycleAllowed();
|
||||||
|
return sysTenantService.deleteById(tenantId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -4,6 +4,7 @@ import com.imeeting.entity.biz.AndroidPushMessage;
|
||||||
import com.imeeting.grpc.push.PushMessage;
|
import com.imeeting.grpc.push.PushMessage;
|
||||||
import com.imeeting.service.android.AndroidGatewayPushService;
|
import com.imeeting.service.android.AndroidGatewayPushService;
|
||||||
import com.imeeting.service.android.AndroidPushMessageService;
|
import com.imeeting.service.android.AndroidPushMessageService;
|
||||||
|
import com.imeeting.support.TaskSecurityContextRunner;
|
||||||
import lombok.RequiredArgsConstructor;
|
import lombok.RequiredArgsConstructor;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
import org.springframework.scheduling.annotation.Scheduled;
|
import org.springframework.scheduling.annotation.Scheduled;
|
||||||
|
|
@ -18,30 +19,34 @@ import java.util.List;
|
||||||
public class AndroidPushMessageRetryTask {
|
public class AndroidPushMessageRetryTask {
|
||||||
private final AndroidPushMessageService androidPushMessageService;
|
private final AndroidPushMessageService androidPushMessageService;
|
||||||
private final AndroidGatewayPushService androidGatewayPushService;
|
private final AndroidGatewayPushService androidGatewayPushService;
|
||||||
|
private final TaskSecurityContextRunner taskSecurityContextRunner;
|
||||||
|
|
||||||
@Scheduled(fixedDelayString = "${imeeting.android.push.retry-interval-ms:15000}")
|
@Scheduled(fixedDelayString = "${imeeting.android.push.retry-interval-ms:15000}")
|
||||||
public void retryPendingMessages() {
|
public void retryPendingMessages() {
|
||||||
List<AndroidPushMessage> pendingMessages = androidPushMessageService.listPendingMeetingPushMessages();
|
taskSecurityContextRunner.callAsPlatformAdmin(() -> {
|
||||||
for (AndroidPushMessage message : pendingMessages) {
|
List<AndroidPushMessage> pendingMessages = androidPushMessageService.listPendingMeetingPushMessages();
|
||||||
if (message.getExpireAt() != null && message.getExpireAt().isBefore(LocalDateTime.now())) {
|
for (AndroidPushMessage message : pendingMessages) {
|
||||||
androidPushMessageService.markExpired(message.getId());
|
if (message.getExpireAt() != null && message.getExpireAt().isBefore(LocalDateTime.now())) {
|
||||||
continue;
|
androidPushMessageService.markExpired(message.getId());
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
PushMessage pushMessage = PushMessage.newBuilder()
|
||||||
|
.setMessageId(message.getMessageId())
|
||||||
|
.setTimestamp(System.currentTimeMillis())
|
||||||
|
.setType(message.getMessageType())
|
||||||
|
.setTitle(resolveMessageTitle(message))
|
||||||
|
.setContent(message.getPayload() == null ? "" : message.getPayload())
|
||||||
|
.setNeedAck(true)
|
||||||
|
.build();
|
||||||
|
int pushed = androidGatewayPushService.pushToDevice(message.getDeviceCode(), pushMessage);
|
||||||
|
if (pushed > 0) {
|
||||||
|
androidPushMessageService.markPushed(message.getId());
|
||||||
|
log.info("Retried android push message, messageId={}, deviceCode={}, pushCountIncreased=true",
|
||||||
|
message.getMessageId(), message.getDeviceCode());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
PushMessage pushMessage = PushMessage.newBuilder()
|
return null;
|
||||||
.setMessageId(message.getMessageId())
|
});
|
||||||
.setTimestamp(System.currentTimeMillis())
|
|
||||||
.setType(message.getMessageType())
|
|
||||||
.setTitle(resolveMessageTitle(message))
|
|
||||||
.setContent(message.getPayload() == null ? "" : message.getPayload())
|
|
||||||
.setNeedAck(true)
|
|
||||||
.build();
|
|
||||||
int pushed = androidGatewayPushService.pushToDevice(message.getDeviceCode(), pushMessage);
|
|
||||||
if (pushed > 0) {
|
|
||||||
androidPushMessageService.markPushed(message.getId());
|
|
||||||
log.info("Retried android push message, messageId={}, deviceCode={}, pushCountIncreased=true",
|
|
||||||
message.getMessageId(), message.getDeviceCode());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private String resolveMessageTitle(AndroidPushMessage message) {
|
private String resolveMessageTitle(AndroidPushMessage message) {
|
||||||
|
|
|
||||||
|
|
@ -11,14 +11,14 @@ logging:
|
||||||
com.imeeting.service.realtime.impl.AsrUpstreamBridgeServiceImpl: debug
|
com.imeeting.service.realtime.impl.AsrUpstreamBridgeServiceImpl: debug
|
||||||
spring:
|
spring:
|
||||||
datasource:
|
datasource:
|
||||||
url: ${SPRING_DATASOURCE_URL:jdbc:postgresql://127.0.0.1:5432/imeeting_db}
|
url: ${SPRING_DATASOURCE_URL:jdbc:postgresql://10.100.53.199:5432/imeeting_dev}
|
||||||
username: ${SPRING_DATASOURCE_USERNAME:postgres}
|
username: ${SPRING_DATASOURCE_USERNAME:postgres}
|
||||||
password: ${SPRING_DATASOURCE_PASSWORD:postgres}
|
password: ${SPRING_DATASOURCE_PASSWORD:postgres}
|
||||||
data:
|
data:
|
||||||
redis:
|
redis:
|
||||||
host: ${SPRING_DATA_REDIS_HOST:127.0.0.1}
|
host: ${SPRING_DATA_REDIS_HOST:10.100.53.199}
|
||||||
port: ${SPRING_DATA_REDIS_PORT:6379}
|
port: ${SPRING_DATA_REDIS_PORT:6379}
|
||||||
password: ${SPRING_DATA_REDIS_PASSWORD:}
|
password: ${SPRING_DATA_REDIS_PASSWORD:unis@123}
|
||||||
database: ${SPRING_DATA_REDIS_DATABASE:15}
|
database: ${SPRING_DATA_REDIS_DATABASE:15}
|
||||||
|
|
||||||
mybatis-plus:
|
mybatis-plus:
|
||||||
|
|
@ -31,10 +31,10 @@ unisbase:
|
||||||
internal-auth:
|
internal-auth:
|
||||||
secret: ${INTERNAL_AUTH_SECRET:change-me-dev-internal-secret}
|
secret: ${INTERNAL_AUTH_SECRET:change-me-dev-internal-secret}
|
||||||
app:
|
app:
|
||||||
server-base-url: ${APP_SERVER_BASE_URL:http://127.0.0.1:${server.port}}
|
server-base-url: ${APP_SERVER_BASE_URL:http://10.100.52.13:${server.port}}
|
||||||
upload-path: ${APP_UPLOAD_PATH:D:/data/imeeting/uploads/}
|
upload-path: ${APP_UPLOAD_PATH:D:/data/imeeting/uploads/}
|
||||||
imeeting:
|
imeeting:
|
||||||
h5:
|
h5:
|
||||||
base-url: ${IMEETING_H5_BASE_URL:http://127.0.0.1:3000}
|
base-url: ${IMEETING_H5_BASE_URL:http://10.100.52.13:3000}
|
||||||
audio:
|
audio:
|
||||||
ffmpeg-path: D:\tools\exe\ffmpeg-master-latest-win64-gpl-shared\bin\ffmpeg.exe
|
ffmpeg-path: D:\tools\exe\ffmpeg-master-latest-win64-gpl-shared\bin\ffmpeg.exe
|
||||||
|
|
|
||||||
|
|
@ -63,6 +63,7 @@ unisbase:
|
||||||
- /api/auth/**
|
- /api/auth/**
|
||||||
- /api/static/**
|
- /api/static/**
|
||||||
- /api/public/meetings/**
|
- /api/public/meetings/**
|
||||||
|
- /api/android/devices/home
|
||||||
- /api/android/auth/login
|
- /api/android/auth/login
|
||||||
- /api/android/auth/refresh
|
- /api/android/auth/refresh
|
||||||
- /api/clients/latest/by-platform
|
- /api/clients/latest/by-platform
|
||||||
|
|
|
||||||
|
|
@ -1,43 +1,20 @@
|
||||||
//package com.imeeting.service;
|
package com.imeeting.service;
|
||||||
//
|
|
||||||
//import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
import com.imeeting.service.biz.LicenseService;
|
||||||
//
|
import org.junit.jupiter.api.Test;
|
||||||
//import org.junit.jupiter.api.Test;
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
//import org.junit.jupiter.api.extension.ExtendWith;
|
import org.springframework.boot.test.context.SpringBootTest;
|
||||||
//import org.mockito.InjectMocks;
|
import org.springframework.test.context.ActiveProfiles;
|
||||||
//import org.mockito.Mock;
|
|
||||||
//import org.mockito.junit.jupiter.MockitoExtension;
|
@SpringBootTest
|
||||||
//
|
@ActiveProfiles("dev")
|
||||||
//import java.util.Collections;
|
|
||||||
//import java.util.List;
|
public class DictItemServiceTest {
|
||||||
//
|
|
||||||
//import static org.junit.jupiter.api.Assertions.assertEquals;
|
@Autowired
|
||||||
//import static org.mockito.ArgumentMatchers.any;
|
private LicenseService licenseService;
|
||||||
//import static org.mockito.Mockito.when;
|
@Test
|
||||||
//
|
public void main(){
|
||||||
//@ExtendWith(MockitoExtension.class)
|
licenseService.initializeTemporaryLicenses(1L);
|
||||||
//public class DictItemServiceTest {
|
}
|
||||||
//
|
}
|
||||||
// @Mock
|
|
||||||
// private SysDictItemMapper dictItemMapper;
|
|
||||||
//
|
|
||||||
// @InjectMocks
|
|
||||||
// private SysDictItemServiceImpl dictItemService;
|
|
||||||
//
|
|
||||||
// @Test
|
|
||||||
// void testGetItemsByTypeCode() {
|
|
||||||
// String typeCode = "gender";
|
|
||||||
// SysDictItem item = new SysDictItem();
|
|
||||||
// item.setTypeCode(typeCode);
|
|
||||||
// item.setItemLabel("Male");
|
|
||||||
// item.setItemValue("1");
|
|
||||||
// item.setStatus(1);
|
|
||||||
// item.setSortOrder(1);
|
|
||||||
//
|
|
||||||
// when(dictItemMapper.selectList(any(LambdaQueryWrapper.class))).thenReturn(Collections.singletonList(item));
|
|
||||||
//
|
|
||||||
// List<SysDictItem> result = dictItemService.getItemsByTypeCode(typeCode);
|
|
||||||
// assertEquals(1, result.size());
|
|
||||||
// assertEquals("Male", result.get(0).getItemLabel());
|
|
||||||
// }
|
|
||||||
//}
|
|
||||||
|
|
|
||||||
|
|
@ -61,6 +61,30 @@ class ScreenSaverServiceImplTest {
|
||||||
assertEquals(List.of(22, 22), result.getItems().stream().map(ScreenSaverAdminVO::getDisplayDurationSec).toList());
|
assertEquals(List.of(22, 22), result.getItems().stream().map(ScreenSaverAdminVO::getDisplayDurationSec).toList());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void getActiveSelectionShouldFallbackToPlatformTenantItemsWhenTenantSelectionIsEmpty() {
|
||||||
|
ScreenSaverUserConfigMapper userConfigMapper = mock(ScreenSaverUserConfigMapper.class);
|
||||||
|
ScreenSaverUserSettingsMapper userSettingsMapper = mock(ScreenSaverUserSettingsMapper.class);
|
||||||
|
SysUserMapper sysUserMapper = mock(SysUserMapper.class);
|
||||||
|
when(userConfigMapper.selectList(any())).thenReturn(List.of());
|
||||||
|
when(userSettingsMapper.selectOne(any())).thenReturn(userSettings(77L, 22));
|
||||||
|
when(sysUserMapper.selectBatchIds(any())).thenReturn(List.of());
|
||||||
|
|
||||||
|
ScreenSaverServiceImpl service = spy(new ScreenSaverServiceImpl(userConfigMapper, userSettingsMapper, sysUserMapper));
|
||||||
|
doReturn(List.of())
|
||||||
|
.doReturn(List.of())
|
||||||
|
.when(service).list(any(LambdaQueryWrapper.class));
|
||||||
|
doReturn(List.of(screenSaver(301L, "PLATFORM", null, 1, 1)))
|
||||||
|
.when(service).listGlobalFallbackPlatformItems();
|
||||||
|
|
||||||
|
ScreenSaverSelectionResult result = service.getActiveSelection(77L);
|
||||||
|
|
||||||
|
assertEquals("PLATFORM", result.getSourceScope());
|
||||||
|
assertEquals(22, result.getDisplayDurationSec());
|
||||||
|
assertEquals(List.of(301L), result.getItems().stream().map(ScreenSaverAdminVO::getId).toList());
|
||||||
|
assertEquals(List.of(1), result.getItems().stream().map(ScreenSaverAdminVO::getStatus).toList());
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void listForAdminShouldApplyCurrentUserStatusFilter() {
|
void listForAdminShouldApplyCurrentUserStatusFilter() {
|
||||||
ScreenSaverUserConfigMapper userConfigMapper = mock(ScreenSaverUserConfigMapper.class);
|
ScreenSaverUserConfigMapper userConfigMapper = mock(ScreenSaverUserConfigMapper.class);
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,40 @@
|
||||||
|
import http from "../http";
|
||||||
|
|
||||||
|
export interface LicenseVO {
|
||||||
|
id: number;
|
||||||
|
tenantId: number;
|
||||||
|
licenseSerial: string;
|
||||||
|
licenseCode: string;
|
||||||
|
licenseType: number;
|
||||||
|
licenseStatus: number;
|
||||||
|
productCode?: string;
|
||||||
|
deviceCode?: string;
|
||||||
|
bindTime?: string;
|
||||||
|
expireTime?: string;
|
||||||
|
importBatchNo?: string;
|
||||||
|
importTime?: string;
|
||||||
|
remark?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface LicenseImportResultVO {
|
||||||
|
importBatchNo: string;
|
||||||
|
totalCount: number;
|
||||||
|
replacedCount: number;
|
||||||
|
unusedFormalCount: number;
|
||||||
|
invalidatedTempCount: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function listLicenses() {
|
||||||
|
const resp = await http.get("/api/admin/licenses");
|
||||||
|
return resp.data.data as LicenseVO[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function importLicenses(file: File) {
|
||||||
|
const formData = new FormData();
|
||||||
|
formData.append("file", file);
|
||||||
|
const resp = await http.post("/api/admin/licenses/import", formData, {
|
||||||
|
headers: { "Content-Type": "multipart/form-data" },
|
||||||
|
timeout: 600000
|
||||||
|
});
|
||||||
|
return resp.data.data as LicenseImportResultVO;
|
||||||
|
}
|
||||||
|
|
@ -174,6 +174,16 @@ export async function kickManagedDevice(id: number) {
|
||||||
return resp.data.data as boolean;
|
return resp.data.data as boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function deleteManagedDevice(id: number) {
|
||||||
|
const resp = await http.delete(`/api/admin/devices/${id}`);
|
||||||
|
return resp.data.data as boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function resetManagedDeviceStats(id: number) {
|
||||||
|
const resp = await http.post(`/api/admin/devices/${id}/reset`);
|
||||||
|
return resp.data.data as boolean;
|
||||||
|
}
|
||||||
|
|
||||||
export async function listUserRoles(userId: number) {
|
export async function listUserRoles(userId: number) {
|
||||||
const resp = await http.get(`/sys/api/users/${userId}/roles`);
|
const resp = await http.get(`/sys/api/users/${userId}/roles`);
|
||||||
return resp.data.data as number[];
|
return resp.data.data as number[];
|
||||||
|
|
|
||||||
|
|
@ -350,7 +350,12 @@
|
||||||
"kickDevice": "Kick device",
|
"kickDevice": "Kick device",
|
||||||
"kickDeviceConfirm": "Kick this device offline?",
|
"kickDeviceConfirm": "Kick this device offline?",
|
||||||
"kickSucceeded": "Device has been kicked offline",
|
"kickSucceeded": "Device has been kicked offline",
|
||||||
|
"resetStats": "Reset stats",
|
||||||
|
"resetStatsConfirm": "Reset this device's homepage statistics?",
|
||||||
|
"resetStatsSucceeded": "Device statistics reset",
|
||||||
"deleteDevice": "Delete this device?",
|
"deleteDevice": "Delete this device?",
|
||||||
|
"weatherCityName": "Weather City",
|
||||||
|
"statsResetAt": "Stats Reset At",
|
||||||
"drawerTitleCreate": "New Device",
|
"drawerTitleCreate": "New Device",
|
||||||
"drawerTitleEdit": "Edit Device",
|
"drawerTitleEdit": "Edit Device",
|
||||||
"owner": "Owner",
|
"owner": "Owner",
|
||||||
|
|
@ -358,7 +363,8 @@
|
||||||
"searchSelectUser": "Search and select a user",
|
"searchSelectUser": "Search and select a user",
|
||||||
"deviceCodeRequired": "Enter the device code",
|
"deviceCodeRequired": "Enter the device code",
|
||||||
"deviceCodePlaceholder": "Enter a unique device code",
|
"deviceCodePlaceholder": "Enter a unique device code",
|
||||||
"deviceNamePlaceholder": "Example: Meeting Room A Recorder"
|
"deviceNamePlaceholder": "Example: Meeting Room A Recorder",
|
||||||
|
"weatherCityNamePlaceholder": "Example: Shenzhen"
|
||||||
},
|
},
|
||||||
"dashboardExt": {
|
"dashboardExt": {
|
||||||
"processing": "Processing",
|
"processing": "Processing",
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,243 @@
|
||||||
|
import { App, Button, Card, Col, Empty, Input, Row, Space, Table, Tag, Typography, Upload } from "antd";
|
||||||
|
import type { ColumnsType } from "antd/es/table";
|
||||||
|
import { CheckCircleOutlined, ClockCircleOutlined, KeyOutlined, LinkOutlined, ReloadOutlined, SearchOutlined, UploadOutlined } from "@ant-design/icons";
|
||||||
|
import { useCallback, useEffect, useMemo, useState } from "react";
|
||||||
|
import PageContainer from "@/components/shared/PageContainer";
|
||||||
|
import AppPagination from "@/components/shared/AppPagination";
|
||||||
|
import { importLicenses, listLicenses, type LicenseImportResultVO, type LicenseVO } from "@/api/business/license";
|
||||||
|
|
||||||
|
const { Text } = Typography;
|
||||||
|
|
||||||
|
function resolveLicenseTypeTag(type?: number) {
|
||||||
|
if (type === 1) return <Tag color="gold">临时</Tag>;
|
||||||
|
if (type === 2) return <Tag color="blue">正式</Tag>;
|
||||||
|
return <Tag>未知</Tag>;
|
||||||
|
}
|
||||||
|
|
||||||
|
function resolveLicenseStatusTag(status?: number) {
|
||||||
|
switch (status) {
|
||||||
|
case 1:
|
||||||
|
return <Tag>未使用</Tag>;
|
||||||
|
case 2:
|
||||||
|
return <Tag color="green">使用中</Tag>;
|
||||||
|
case 3:
|
||||||
|
return <Tag color="red">已过期</Tag>;
|
||||||
|
case 4:
|
||||||
|
return <Tag>已失效</Tag>;
|
||||||
|
default:
|
||||||
|
return <Tag>未知</Tag>;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatDateTime(value?: string) {
|
||||||
|
return value ? value.replace("T", " ").substring(0, 19) : "-";
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function LicenseManagement() {
|
||||||
|
const { message } = App.useApp();
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
const [uploading, setUploading] = useState(false);
|
||||||
|
const [records, setRecords] = useState<LicenseVO[]>([]);
|
||||||
|
const [searchValue, setSearchValue] = useState("");
|
||||||
|
const [page, setPage] = useState(1);
|
||||||
|
const [pageSize, setPageSize] = useState(10);
|
||||||
|
|
||||||
|
const loadData = useCallback(async () => {
|
||||||
|
setLoading(true);
|
||||||
|
try {
|
||||||
|
const result = await listLicenses();
|
||||||
|
setRecords(result || []);
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
void loadData();
|
||||||
|
}, [loadData]);
|
||||||
|
|
||||||
|
const filteredRecords = useMemo(() => {
|
||||||
|
const keyword = searchValue.trim().toLowerCase();
|
||||||
|
if (!keyword) {
|
||||||
|
return records;
|
||||||
|
}
|
||||||
|
return records.filter((item) =>
|
||||||
|
[
|
||||||
|
item.licenseSerial,
|
||||||
|
item.licenseCode,
|
||||||
|
item.productCode,
|
||||||
|
item.deviceCode,
|
||||||
|
item.importBatchNo,
|
||||||
|
item.remark,
|
||||||
|
].some((field) => String(field || "").toLowerCase().includes(keyword))
|
||||||
|
);
|
||||||
|
}, [records, searchValue]);
|
||||||
|
|
||||||
|
const pagedRecords = useMemo(() => {
|
||||||
|
const start = (page - 1) * pageSize;
|
||||||
|
return filteredRecords.slice(start, start + pageSize);
|
||||||
|
}, [filteredRecords, page, pageSize]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setPage(1);
|
||||||
|
}, [searchValue]);
|
||||||
|
|
||||||
|
const stats = useMemo(() => ({
|
||||||
|
total: records.length,
|
||||||
|
using: records.filter((item) => item.licenseStatus === 2).length,
|
||||||
|
formal: records.filter((item) => item.licenseType === 2).length,
|
||||||
|
available: records.filter((item) => item.licenseStatus === 1).length,
|
||||||
|
}), [records]);
|
||||||
|
|
||||||
|
const handleImport = async (file: File) => {
|
||||||
|
setUploading(true);
|
||||||
|
try {
|
||||||
|
const result: LicenseImportResultVO = await importLicenses(file);
|
||||||
|
message.success(`导入完成:${result.totalCount} 条,替换 ${result.replacedCount} 条`);
|
||||||
|
await loadData();
|
||||||
|
} finally {
|
||||||
|
setUploading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const columns: ColumnsType<LicenseVO> = [
|
||||||
|
{
|
||||||
|
title: "授权标识",
|
||||||
|
key: "license",
|
||||||
|
width: 320,
|
||||||
|
render: (_, record) => (
|
||||||
|
<Space direction="vertical" size={0}>
|
||||||
|
<Text strong className="tabular-nums">{record.licenseSerial}</Text>
|
||||||
|
{/*<Text type="secondary" className="tabular-nums">{record.licenseCode}</Text>*/}
|
||||||
|
</Space>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "类型",
|
||||||
|
dataIndex: "licenseType",
|
||||||
|
key: "licenseType",
|
||||||
|
width: 100,
|
||||||
|
render: (value) => resolveLicenseTypeTag(value),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "状态",
|
||||||
|
dataIndex: "licenseStatus",
|
||||||
|
key: "licenseStatus",
|
||||||
|
width: 100,
|
||||||
|
render: (value) => resolveLicenseStatusTag(value),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "绑定设备",
|
||||||
|
dataIndex: "deviceCode",
|
||||||
|
key: "deviceCode",
|
||||||
|
width: 240,
|
||||||
|
render: (value) => <Text className="tabular-nums">{value || "-"}</Text>,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "过期时间",
|
||||||
|
dataIndex: "expireTime",
|
||||||
|
key: "expireTime",
|
||||||
|
width: 200,
|
||||||
|
render: (value) => <Text type="secondary">{formatDateTime(value)}</Text>,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<PageContainer
|
||||||
|
title="授权码管理"
|
||||||
|
subtitle="查看当前租户授权池"
|
||||||
|
headerExtra={
|
||||||
|
<Space size={12}>
|
||||||
|
<Button icon={<ReloadOutlined />} onClick={() => void loadData()} loading={loading}>
|
||||||
|
刷新
|
||||||
|
</Button>
|
||||||
|
{/*<Upload showUploadList={false} beforeUpload={(file) => { void handleImport(file as File); return Upload.LIST_IGNORE; }}>*/}
|
||||||
|
{/* <Button type="primary" icon={<UploadOutlined />} loading={uploading}>*/}
|
||||||
|
{/* 导入正式授权*/}
|
||||||
|
{/* </Button>*/}
|
||||||
|
{/*</Upload>*/}
|
||||||
|
</Space>
|
||||||
|
}
|
||||||
|
toolbar={
|
||||||
|
<Input
|
||||||
|
placeholder="搜索序列号、授权码、设备编码"
|
||||||
|
prefix={<SearchOutlined />}
|
||||||
|
allowClear
|
||||||
|
style={{ width: 360 }}
|
||||||
|
value={searchValue}
|
||||||
|
onChange={(event) => setSearchValue(event.target.value)}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<Row gutter={24} style={{ marginBottom: 24 }}>
|
||||||
|
<Col span={6}>
|
||||||
|
<Card bordered={false} style={{ borderRadius: 16 }}>
|
||||||
|
<Space size={16}>
|
||||||
|
<KeyOutlined style={{ color: "#1677ff", fontSize: 24 }} />
|
||||||
|
<div>
|
||||||
|
<div style={{ color: "var(--app-text-secondary)", fontSize: 13 }}>授权总数</div>
|
||||||
|
<div style={{ fontSize: 30, fontWeight: 800 }}>{stats.total}</div>
|
||||||
|
</div>
|
||||||
|
</Space>
|
||||||
|
</Card>
|
||||||
|
</Col>
|
||||||
|
<Col span={6}>
|
||||||
|
<Card bordered={false} style={{ borderRadius: 16 }}>
|
||||||
|
<Space size={16}>
|
||||||
|
<LinkOutlined style={{ color: "#52c41a", fontSize: 24 }} />
|
||||||
|
<div>
|
||||||
|
<div style={{ color: "var(--app-text-secondary)", fontSize: 13 }}>使用中</div>
|
||||||
|
<div style={{ fontSize: 30, fontWeight: 800 }}>{stats.using}</div>
|
||||||
|
</div>
|
||||||
|
</Space>
|
||||||
|
</Card>
|
||||||
|
</Col>
|
||||||
|
<Col span={6}>
|
||||||
|
<Card bordered={false} style={{ borderRadius: 16 }}>
|
||||||
|
<Space size={16}>
|
||||||
|
<CheckCircleOutlined style={{ color: "#722ed1", fontSize: 24 }} />
|
||||||
|
<div>
|
||||||
|
<div style={{ color: "var(--app-text-secondary)", fontSize: 13 }}>正式授权</div>
|
||||||
|
<div style={{ fontSize: 30, fontWeight: 800 }}>{stats.formal}</div>
|
||||||
|
</div>
|
||||||
|
</Space>
|
||||||
|
</Card>
|
||||||
|
</Col>
|
||||||
|
<Col span={6}>
|
||||||
|
<Card bordered={false} style={{ borderRadius: 16 }}>
|
||||||
|
<Space size={16}>
|
||||||
|
<ClockCircleOutlined style={{ color: "#fa8c16", fontSize: 24 }} />
|
||||||
|
<div>
|
||||||
|
<div style={{ color: "var(--app-text-secondary)", fontSize: 13 }}>可分配</div>
|
||||||
|
<div style={{ fontSize: 30, fontWeight: 800 }}>{stats.available}</div>
|
||||||
|
</div>
|
||||||
|
</Space>
|
||||||
|
</Card>
|
||||||
|
</Col>
|
||||||
|
</Row>
|
||||||
|
|
||||||
|
<Card className="app-page__content-card flex-1 flex flex-col overflow-hidden" styles={{ body: { padding: 0, flex: 1, display: "flex", flexDirection: "column", overflow: "hidden" } }}>
|
||||||
|
<div className="app-page__table-wrap flex-1 min-h-0" style={{ padding: "0 24px", overflow: "auto" }}>
|
||||||
|
<Table
|
||||||
|
rowKey="id"
|
||||||
|
columns={columns}
|
||||||
|
dataSource={pagedRecords}
|
||||||
|
loading={loading}
|
||||||
|
pagination={false}
|
||||||
|
scroll={{ x: 1000, y: "100%" }}
|
||||||
|
locale={{ emptyText: <Empty description="暂无授权数据" /> }}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<AppPagination
|
||||||
|
current={page}
|
||||||
|
pageSize={pageSize}
|
||||||
|
total={filteredRecords.length}
|
||||||
|
onChange={(nextPage, nextSize) => {
|
||||||
|
setPage(nextPage);
|
||||||
|
setPageSize(nextSize);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Card>
|
||||||
|
</PageContainer>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -1,8 +1,8 @@
|
||||||
import { Button, Card, Col, Drawer, Form, Input, Popconfirm, Row, Select, Space, Tag, Typography, message } from "antd";
|
import { Button, Card, Col, Drawer, Form, Input, Popconfirm, Row, Select, Space, Tag, Typography, message } from "antd";
|
||||||
import { CheckCircleOutlined, DesktopOutlined, DisconnectOutlined, EditOutlined, ReloadOutlined, SearchOutlined, ThunderboltOutlined, UserOutlined } from "@ant-design/icons";
|
import { CheckCircleOutlined, DeleteOutlined, DesktopOutlined, DisconnectOutlined, EditOutlined, ReloadOutlined, SearchOutlined, ThunderboltOutlined, UndoOutlined, UserOutlined } from "@ant-design/icons";
|
||||||
import { useEffect, useMemo, useState } from "react";
|
import { useEffect, useMemo, useState } from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { kickManagedDevice, listManagedDevices, updateManagedDevice } from "@/api";
|
import { deleteManagedDevice, kickManagedDevice, listManagedDevices, resetManagedDeviceStats, updateManagedDevice } from "@/api";
|
||||||
import PageHeader from "@/components/shared/PageHeader";
|
import PageHeader from "@/components/shared/PageHeader";
|
||||||
import PageContainer from "@/components/shared/PageContainer";
|
import PageContainer from "@/components/shared/PageContainer";
|
||||||
import ListTable from "@/components/shared/ListTable/ListTable";
|
import ListTable from "@/components/shared/ListTable/ListTable";
|
||||||
|
|
@ -17,6 +17,7 @@ const { Text } = Typography;
|
||||||
type DeviceFormValues = {
|
type DeviceFormValues = {
|
||||||
deviceName?: string;
|
deviceName?: string;
|
||||||
status: number;
|
status: number;
|
||||||
|
weatherCityName?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export default function Devices() {
|
export default function Devices() {
|
||||||
|
|
@ -82,7 +83,8 @@ export default function Devices() {
|
||||||
setEditing(record);
|
setEditing(record);
|
||||||
form.setFieldsValue({
|
form.setFieldsValue({
|
||||||
deviceName: record.deviceName,
|
deviceName: record.deviceName,
|
||||||
status: record.status ?? 1
|
status: record.status ?? 1,
|
||||||
|
weatherCityName: record.weatherCityName
|
||||||
});
|
});
|
||||||
setOpen(true);
|
setOpen(true);
|
||||||
};
|
};
|
||||||
|
|
@ -96,7 +98,8 @@ export default function Devices() {
|
||||||
try {
|
try {
|
||||||
await updateManagedDevice(editing.deviceId, {
|
await updateManagedDevice(editing.deviceId, {
|
||||||
deviceName: values.deviceName,
|
deviceName: values.deviceName,
|
||||||
status: values.status
|
status: values.status,
|
||||||
|
weatherCityName: values.weatherCityName
|
||||||
});
|
});
|
||||||
message.success(t("devicesExt.operationSucceeded"));
|
message.success(t("devicesExt.operationSucceeded"));
|
||||||
setOpen(false);
|
setOpen(false);
|
||||||
|
|
@ -112,6 +115,18 @@ export default function Devices() {
|
||||||
await loadData();
|
await loadData();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const remove = async (record: DeviceInfo) => {
|
||||||
|
await deleteManagedDevice(record.deviceId);
|
||||||
|
message.success(t("devicesExt.deleteSucceeded"));
|
||||||
|
await loadData();
|
||||||
|
};
|
||||||
|
|
||||||
|
const resetStats = async (record: DeviceInfo) => {
|
||||||
|
await resetManagedDeviceStats(record.deviceId);
|
||||||
|
message.success(t("devicesExt.resetStatsSucceeded"));
|
||||||
|
await loadData();
|
||||||
|
};
|
||||||
|
|
||||||
const handlePageChange = (page: number, pageSize: number) => {
|
const handlePageChange = (page: number, pageSize: number) => {
|
||||||
setPagination({ current: page, pageSize });
|
setPagination({ current: page, pageSize });
|
||||||
};
|
};
|
||||||
|
|
@ -187,6 +202,22 @@ export default function Devices() {
|
||||||
</Text>
|
</Text>
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
title: t("devicesExt.statsResetAt"),
|
||||||
|
dataIndex: "statsResetAt",
|
||||||
|
width: 180,
|
||||||
|
render: (text: string) => (
|
||||||
|
<Text type="secondary" className="tabular-nums">
|
||||||
|
{text ? text.replace("T", " ").substring(0, 19) : "-"}
|
||||||
|
</Text>
|
||||||
|
)
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: t("devicesExt.weatherCityName"),
|
||||||
|
dataIndex: "weatherCityName",
|
||||||
|
width: 140,
|
||||||
|
render: (text: string) => <Text>{text || "-"}</Text>
|
||||||
|
},
|
||||||
{
|
{
|
||||||
title: t("common.status"),
|
title: t("common.status"),
|
||||||
dataIndex: "status",
|
dataIndex: "status",
|
||||||
|
|
@ -209,7 +240,7 @@ export default function Devices() {
|
||||||
{
|
{
|
||||||
title: t("common.action"),
|
title: t("common.action"),
|
||||||
key: "action",
|
key: "action",
|
||||||
width: 140,
|
width: 220,
|
||||||
fixed: "right" as const,
|
fixed: "right" as const,
|
||||||
render: (_value: unknown, record: DeviceInfo) => (
|
render: (_value: unknown, record: DeviceInfo) => (
|
||||||
<Space>
|
<Space>
|
||||||
|
|
@ -226,6 +257,25 @@ export default function Devices() {
|
||||||
/>
|
/>
|
||||||
</Popconfirm>
|
</Popconfirm>
|
||||||
) : null}
|
) : null}
|
||||||
|
{can("device:update") ? (
|
||||||
|
<Popconfirm title={t("devicesExt.resetStatsConfirm")} onConfirm={() => resetStats(record)}>
|
||||||
|
<Button
|
||||||
|
type="text"
|
||||||
|
icon={<UndoOutlined aria-hidden="true" />}
|
||||||
|
aria-label={t("devicesExt.resetStats")}
|
||||||
|
/>
|
||||||
|
</Popconfirm>
|
||||||
|
) : null}
|
||||||
|
{can("device:update") ? (
|
||||||
|
<Popconfirm title={t("devicesExt.deleteDevice")} onConfirm={() => remove(record)}>
|
||||||
|
<Button
|
||||||
|
type="text"
|
||||||
|
danger
|
||||||
|
icon={<DeleteOutlined aria-hidden="true" />}
|
||||||
|
aria-label={t("devicesExt.deleteDevice")}
|
||||||
|
/>
|
||||||
|
</Popconfirm>
|
||||||
|
) : null}
|
||||||
</Space>
|
</Space>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
@ -334,6 +384,9 @@ export default function Devices() {
|
||||||
<Form.Item label={t("devices.deviceName")} name="deviceName">
|
<Form.Item label={t("devices.deviceName")} name="deviceName">
|
||||||
<Input placeholder={t("devicesExt.deviceNamePlaceholder")} />
|
<Input placeholder={t("devicesExt.deviceNamePlaceholder")} />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
|
<Form.Item label={t("devicesExt.weatherCityName")} name="weatherCityName">
|
||||||
|
<Input placeholder={t("devicesExt.weatherCityNamePlaceholder")} />
|
||||||
|
</Form.Item>
|
||||||
<Form.Item label={t("common.status")} name="status" initialValue={1}>
|
<Form.Item label={t("common.status")} name="status" initialValue={1}>
|
||||||
<Select options={statusDict.map((item) => ({ value: Number(item.itemValue), label: item.itemLabel }))} />
|
<Select options={statusDict.map((item) => ({ value: Number(item.itemValue), label: item.itemLabel }))} />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
|
|
|
||||||
|
|
@ -21,6 +21,7 @@ const ClientManagement = lazy(() => import("@/pages/business/ClientManagement"))
|
||||||
const ExternalAppManagement = lazy(() => import("@/pages/business/ExternalAppManagement"));
|
const ExternalAppManagement = lazy(() => import("@/pages/business/ExternalAppManagement"));
|
||||||
const ScreenSaverManagement = lazy(() => import("@/pages/business/ScreenSaverManagement"));
|
const ScreenSaverManagement = lazy(() => import("@/pages/business/ScreenSaverManagement"));
|
||||||
const MeetingPointsManagement = lazy(() => import("@/pages/business/MeetingPointsManagement"));
|
const MeetingPointsManagement = lazy(() => import("@/pages/business/MeetingPointsManagement"));
|
||||||
|
const LicenseManagement = lazy(() => import("@/pages/business/LicenseManagement"));
|
||||||
|
|
||||||
import SpeakerReg from "../pages/business/SpeakerReg";
|
import SpeakerReg from "../pages/business/SpeakerReg";
|
||||||
const RealtimeAsrSession = lazy(async () => {
|
const RealtimeAsrSession = lazy(async () => {
|
||||||
|
|
@ -65,6 +66,7 @@ export const menuRoutes: MenuRoute[] = [
|
||||||
{ path: "/prompts", label: "总结模板", element: <LazyPage><PromptTemplates /></LazyPage>, perm: "menu:prompt" },
|
{ path: "/prompts", label: "总结模板", element: <LazyPage><PromptTemplates /></LazyPage>, perm: "menu:prompt" },
|
||||||
{ path: "/aimodels", label: "模型配置", element: <LazyPage><AiModels /></LazyPage>, perm: "menu:aimodel" },
|
{ path: "/aimodels", label: "模型配置", element: <LazyPage><AiModels /></LazyPage>, perm: "menu:aimodel" },
|
||||||
{ path: "/clients", label: "客户端管理", element: <LazyPage><ClientManagement /></LazyPage>, perm: "menu:clients" },
|
{ path: "/clients", label: "客户端管理", element: <LazyPage><ClientManagement /></LazyPage>, perm: "menu:clients" },
|
||||||
|
{ path: "/licenses", label: "授权码管理", element: <LazyPage><LicenseManagement /></LazyPage>, perm: "menu:licenses" },
|
||||||
{ path: "/external-apps", label: "外部应用管理", element: <LazyPage><ExternalAppManagement /></LazyPage>, perm: "menu:external-apps" },
|
{ path: "/external-apps", label: "外部应用管理", element: <LazyPage><ExternalAppManagement /></LazyPage>, perm: "menu:external-apps" },
|
||||||
{ path: "/screen-savers", label: "屏保管理", element: <LazyPage><ScreenSaverManagement /></LazyPage>, perm: "menu:screen-savers" },
|
{ path: "/screen-savers", label: "屏保管理", element: <LazyPage><ScreenSaverManagement /></LazyPage>, perm: "menu:screen-savers" },
|
||||||
{ path: "/meeting-points", label: "积分管理", element: <LazyPage><MeetingPointsManagement /></LazyPage>, perm: "menu:meeting-points" },
|
{ path: "/meeting-points", label: "积分管理", element: <LazyPage><MeetingPointsManagement /></LazyPage>, perm: "menu:meeting-points" },
|
||||||
|
|
|
||||||
|
|
@ -92,6 +92,10 @@ export interface DeviceInfo extends BaseEntity {
|
||||||
terminalVersion?: string;
|
terminalVersion?: string;
|
||||||
online?: boolean;
|
online?: boolean;
|
||||||
lastOnlineAt?: string;
|
lastOnlineAt?: string;
|
||||||
|
statsResetAt?: string;
|
||||||
|
weatherCityName?: string;
|
||||||
|
username?: string;
|
||||||
|
displayName?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface SysDictType extends BaseEntity {
|
export interface SysDictType extends BaseEntity {
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue