feat: 添加屏保用户播放设置和相关功能

- 在 `AndroidScreenSaverCatalogVO` 中添加 `displayDurationSec` 字段
- 移除 `ScreenSaver` 和 `AndroidScreenSaverItemVO` 中的 `displayDurationSec` 字段
- 更新 `ScreenSaverServiceImpl` 以支持用户播放设置
- 添加 `ScreenSaverUserSettings` 实体类和 `ScreenSaverUserSettingsMapper` 映射器
- 更新 `ScreenSaverSelectionResult` 以包含 `displayDurationSec`
- 更新数据库表结构以支持新的字段和表
- 更新单元测试以验证新功能的正确性
dev_na
chenhao 2026-04-21 14:17:41 +08:00
parent ce4743c5ea
commit 940cc8a939
22 changed files with 510 additions and 95 deletions

View File

@ -365,4 +365,62 @@
| result_file_path | VARCHAR(500) | | 缁撴灉鏂囦欢鐩稿璺緞 (濡侻D鎬荤粨鏂囦欢) |
| status | SMALLINT | | 0:鎺掗槦, 1:澶勭悊涓? 2:鎴愬姛, 3:澶辫触 |
## 6. 屏保模块
### 6.1 `biz_screen_savers`(屏保素材表)
| 字段 | 类型 | 约束 | 说明 |
| --- | --- | --- | --- |
| id | BIGSERIAL | PK | 屏保ID |
| tenant_id | BIGINT | NOT NULL | 租户ID |
| scope_type | VARCHAR(32) | NOT NULL, DEFAULT 'PLATFORM' | 作用域PLATFORM / USER |
| owner_user_id | BIGINT | | 当作用域为 USER 时的归属用户 |
| name | VARCHAR(128) | NOT NULL | 屏保名称 |
| image_url | VARCHAR(512) | NOT NULL | 图片地址 |
| description | VARCHAR(255) | | 屏保描述 |
| display_duration_sec | INTEGER | NOT NULL, DEFAULT 15 | 旧版素材级展示时长,兼容期保留 |
| image_width | INTEGER | | 图片宽度 |
| image_height | INTEGER | | 图片高度 |
| image_format | VARCHAR(16) | | 图片格式 |
| sort_order | INTEGER | NOT NULL, DEFAULT 0 | 排序值 |
| created_by | BIGINT | | 创建人ID |
| status | SMALLINT | NOT NULL, DEFAULT 1 | 启用状态 |
| remark | VARCHAR(255) | | 备注 |
| created_at | TIMESTAMP(6) | NOT NULL | 创建时间 |
| updated_at | TIMESTAMP(6) | NOT NULL | 更新时间 |
| is_deleted | SMALLINT | DEFAULT 0 | 逻辑删除 |
索引:
- `idx_screen_savers_status_sort`: `(status, sort_order)`
- `idx_screen_savers_scope_owner_status_sort`: `(scope_type, owner_user_id, status, sort_order)`
### 6.2 `biz_screen_saver_user_config`(屏保用户状态覆盖表)
| 字段 | 类型 | 约束 | 说明 |
| --- | --- | --- | --- |
| id | BIGSERIAL | PK | 配置ID |
| tenant_id | BIGINT | NOT NULL | 租户ID |
| user_id | BIGINT | NOT NULL | 用户ID |
| screen_saver_id | BIGINT | NOT NULL | 屏保素材ID |
| status | SMALLINT | NOT NULL, DEFAULT 1 | 用户覆盖启停状态 |
| created_at | TIMESTAMP(6) | NOT NULL | 创建时间 |
| updated_at | TIMESTAMP(6) | NOT NULL | 更新时间 |
| is_deleted | SMALLINT | DEFAULT 0 | 逻辑删除 |
索引:
- `uk_screen_saver_user_cfg_user_item`: `UNIQUE (tenant_id, user_id, screen_saver_id) WHERE is_deleted = 0`
- `idx_screen_saver_user_cfg_item`: `(screen_saver_id) WHERE is_deleted = 0`
### 6.3 `biz_screen_saver_user_settings`(屏保用户播放设置表)
| 字段 | 类型 | 约束 | 说明 |
| --- | --- | --- | --- |
| id | BIGSERIAL | PK | 设置ID |
| tenant_id | BIGINT | NOT NULL | 租户ID |
| user_id | BIGINT | NOT NULL | 用户ID |
| display_duration_sec | INTEGER | NOT NULL, DEFAULT 15 | 当前用户统一屏保展示时长(秒) |
| created_at | TIMESTAMP(6) | NOT NULL | 创建时间 |
| updated_at | TIMESTAMP(6) | NOT NULL | 更新时间 |
| is_deleted | SMALLINT | DEFAULT 0 | 逻辑删除 |
索引:
- `uk_screen_saver_user_settings_user`: `UNIQUE (tenant_id, user_id) WHERE is_deleted = 0`

View File

@ -563,3 +563,67 @@ INSERT INTO "sys_dict_item" ("dict_item_id", "type_code", "item_label", "item_va
INSERT INTO "sys_dict_item" ("dict_item_id", "type_code", "item_label", "item_value", "sort_order", "status", "remark", "created_at", "updated_at") VALUES (40, 'biz_prompt_level', '预置模板', '1', 1, 1, '平台系统预置或租户共享预置', '2026-03-04 10:55:42.163768', '2026-03-04 10:55:42.163768');
INSERT INTO "sys_dict_item" ("dict_item_id", "type_code", "item_label", "item_value", "sort_order", "status", "remark", "created_at", "updated_at") VALUES (41, 'biz_prompt_level', '个人模板', '0', 2, 1, '个人私有模板', '2026-03-04 10:55:42.175269', '2026-03-04 10:55:42.175269');
-- ----------------------------
-- 6. 屏保模块
-- ----------------------------
CREATE TABLE biz_screen_savers (
id BIGSERIAL PRIMARY KEY,
tenant_id BIGINT NOT NULL DEFAULT 0,
scope_type VARCHAR(32) NOT NULL DEFAULT 'PLATFORM',
owner_user_id BIGINT,
name VARCHAR(128) NOT NULL,
image_url VARCHAR(512) NOT NULL,
description VARCHAR(255),
display_duration_sec INTEGER NOT NULL DEFAULT 15,
image_width INTEGER,
image_height INTEGER,
image_format VARCHAR(16),
sort_order INTEGER NOT NULL DEFAULT 0,
created_by BIGINT,
status SMALLINT NOT NULL DEFAULT 1,
remark VARCHAR(255),
created_at TIMESTAMP(6) NOT NULL DEFAULT now(),
updated_at TIMESTAMP(6) NOT NULL DEFAULT now(),
is_deleted SMALLINT NOT NULL DEFAULT 0
);
CREATE INDEX idx_screen_savers_status_sort
ON biz_screen_savers (status, sort_order);
CREATE INDEX idx_screen_savers_scope_owner_status_sort
ON biz_screen_savers (scope_type, owner_user_id, status, sort_order);
CREATE TABLE biz_screen_saver_user_config (
id BIGSERIAL PRIMARY KEY,
tenant_id BIGINT NOT NULL DEFAULT 0,
user_id BIGINT NOT NULL,
screen_saver_id BIGINT NOT NULL,
status SMALLINT NOT NULL DEFAULT 1,
created_at TIMESTAMP(6) NOT NULL DEFAULT now(),
updated_at TIMESTAMP(6) NOT NULL DEFAULT now(),
is_deleted SMALLINT NOT NULL DEFAULT 0
);
CREATE UNIQUE INDEX uk_screen_saver_user_cfg_user_item
ON biz_screen_saver_user_config (tenant_id, user_id, screen_saver_id)
WHERE is_deleted = 0;
CREATE INDEX idx_screen_saver_user_cfg_item
ON biz_screen_saver_user_config (screen_saver_id)
WHERE is_deleted = 0;
CREATE TABLE biz_screen_saver_user_settings (
id BIGSERIAL PRIMARY KEY,
tenant_id BIGINT NOT NULL DEFAULT 0,
user_id BIGINT NOT NULL,
display_duration_sec INTEGER NOT NULL DEFAULT 15,
created_at TIMESTAMP(6) NOT NULL DEFAULT now(),
updated_at TIMESTAMP(6) NOT NULL DEFAULT now(),
is_deleted SMALLINT NOT NULL DEFAULT 0
);
CREATE UNIQUE INDEX uk_screen_saver_user_settings_user
ON biz_screen_saver_user_settings (tenant_id, user_id)
WHERE is_deleted = 0;

View File

@ -44,6 +44,7 @@ public class AndroidScreenSaverController {
AndroidScreenSaverCatalogVO vo = new AndroidScreenSaverCatalogVO();
vo.setRefreshIntervalSec(300);
vo.setPlayMode("SEQUENTIAL");
vo.setDisplayDurationSec(selection.getDisplayDurationSec());
vo.setSourceScope(selection.getSourceScope());
vo.setItems(selection.getItems().stream().map(item -> {
AndroidScreenSaverItemVO child = new AndroidScreenSaverItemVO();
@ -51,7 +52,6 @@ public class AndroidScreenSaverController {
child.setName(item.getName());
child.setImageUrl(item.getImageUrl());
child.setDescription(item.getDescription());
child.setDisplayDurationSec(item.getDisplayDurationSec());
child.setSortOrder(item.getSortOrder());
child.setUpdatedAt(item.getUpdatedAt());
return child;

View File

@ -1,7 +1,7 @@
package com.imeeting.controller.android.legacy;
import com.imeeting.dto.android.legacy.LegacyApiResponse;
import com.imeeting.dto.android.legacy.LegacyScreenSaverItemResponse;
import com.imeeting.dto.android.legacy.LegacyScreenSaverCatalogResponse;
import com.imeeting.service.android.legacy.LegacyScreenSaverAdapterService;
import com.imeeting.support.TaskSecurityContextRunner;
import com.unisbase.dto.InternalAuthCheckResponse;
@ -17,8 +17,6 @@ import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.List;
@Tag(name = "兼容屏保接口")
@RestController
@RequestMapping("/api/screensavers")
@ -34,19 +32,19 @@ public class LegacyScreenSaverController {
@Operation(summary = "查询启用的屏保列表")
@GetMapping("/active")
public LegacyApiResponse<List<LegacyScreenSaverItemResponse>> active(HttpServletRequest request) {
public LegacyApiResponse<LegacyScreenSaverCatalogResponse> active(HttpServletRequest request) {
LoginUser loginUser = resolveLoginUser(request);
return LegacyApiResponse.ok(queryActive(loginUser));
}
private List<LegacyScreenSaverItemResponse> queryActive(LoginUser loginUser) {
private LegacyScreenSaverCatalogResponse queryActive(LoginUser loginUser) {
if (loginUser == null || loginUser.getUserId() == null || loginUser.getTenantId() == null) {
return legacyScreenSaverAdapterService.listActiveScreenSavers(null);
return legacyScreenSaverAdapterService.getActiveScreenSavers(null);
}
return taskSecurityContextRunner.callAsTenantUser(
loginUser.getTenantId(),
loginUser.getUserId(),
() -> legacyScreenSaverAdapterService.listActiveScreenSavers(loginUser.getUserId())
() -> legacyScreenSaverAdapterService.getActiveScreenSavers(loginUser.getUserId())
);
}

View File

@ -3,6 +3,8 @@ package com.imeeting.controller.biz;
import com.imeeting.dto.biz.ScreenSaverAdminVO;
import com.imeeting.dto.biz.ScreenSaverDTO;
import com.imeeting.dto.biz.ScreenSaverImageUploadVO;
import com.imeeting.dto.biz.ScreenSaverUserSettingsDTO;
import com.imeeting.dto.biz.ScreenSaverUserSettingsVO;
import com.imeeting.entity.biz.ScreenSaver;
import com.imeeting.service.biz.ScreenSaverService;
import com.unisbase.common.ApiResponse;
@ -44,6 +46,20 @@ public class ScreenSaverController {
return ApiResponse.ok(screenSaverService.listForAdmin(currentLoginUser(), keyword, status, scopeType, ownerUserId));
}
@Operation(summary = "查询当前用户屏保播放设置")
@GetMapping("/my-settings")
@PreAuthorize("isAuthenticated()")
public ApiResponse<ScreenSaverUserSettingsVO> getMySettings() {
return ApiResponse.ok(screenSaverService.getMySettings(currentLoginUser()));
}
@Operation(summary = "更新当前用户屏保播放设置")
@PutMapping("/my-settings")
@PreAuthorize("isAuthenticated()")
public ApiResponse<ScreenSaverUserSettingsVO> updateMySettings(@RequestBody ScreenSaverUserSettingsDTO dto) {
return ApiResponse.ok(screenSaverService.updateMySettings(dto, currentLoginUser()));
}
@Operation(summary = "新增屏保")
@PostMapping
@PreAuthorize("isAuthenticated()")

View File

@ -12,6 +12,8 @@ public class AndroidScreenSaverCatalogVO {
private Integer refreshIntervalSec;
@Schema(description = "播放模式")
private String playMode;
@Schema(description = "当前用户统一屏保展示时长(秒)")
private Integer displayDurationSec;
@Schema(description = "当前屏保来源范围")
private String sourceScope;
@Schema(description = "屏保图片项列表")

View File

@ -14,8 +14,6 @@ public class AndroidScreenSaverItemVO {
private String imageUrl;
@Schema(description = "屏保描述")
private String description;
@Schema(description = "单张展示时长,单位秒")
private Integer displayDurationSec;
@Schema(description = "排序值")
private Integer sortOrder;
@Schema(description = "最近更新时间")

View File

@ -15,9 +15,6 @@ public class LegacyScreenSaverItemResponse {
private String description;
@JsonProperty("display_duration_sec")
private Integer displayDurationSec;
@JsonProperty("sort_order")
private Integer sortOrder;
@ -42,7 +39,6 @@ public class LegacyScreenSaverItemResponse {
response.setName(source.getName());
response.setImageUrl(source.getImageUrl());
response.setDescription(source.getDescription());
response.setDisplayDurationSec(source.getDisplayDurationSec());
response.setSortOrder(source.getSortOrder());
response.setIsActive(Integer.valueOf(1).equals(source.getStatus()) ? 1 : 0);
response.setCreatedAt(source.getCreatedAt());

View File

@ -33,7 +33,6 @@ public class ScreenSaverAdminVO {
vo.setName(entity.getName());
vo.setImageUrl(entity.getImageUrl());
vo.setDescription(entity.getDescription());
vo.setDisplayDurationSec(entity.getDisplayDurationSec());
vo.setImageWidth(entity.getImageWidth());
vo.setImageHeight(entity.getImageHeight());
vo.setImageFormat(entity.getImageFormat());

View File

@ -9,7 +9,6 @@ public class ScreenSaverDTO {
private String name;
private String imageUrl;
private String description;
private Integer displayDurationSec;
private Integer imageWidth;
private Integer imageHeight;
private String imageFormat;

View File

@ -9,5 +9,6 @@ import java.util.List;
@AllArgsConstructor
public class ScreenSaverSelectionResult {
private String sourceScope;
private Integer displayDurationSec;
private List<ScreenSaverAdminVO> items;
}

View File

@ -32,9 +32,6 @@ public class ScreenSaver extends BaseEntity {
@Schema(description = "屏保描述")
private String description;
@Schema(description = "展示时长,单位秒")
private Integer displayDurationSec;
@Schema(description = "图片宽度")
private Integer imageWidth;

View File

@ -1,9 +1,7 @@
package com.imeeting.service.android.legacy;
import com.imeeting.dto.android.legacy.LegacyScreenSaverItemResponse;
import java.util.List;
import com.imeeting.dto.android.legacy.LegacyScreenSaverCatalogResponse;
public interface LegacyScreenSaverAdapterService {
List<LegacyScreenSaverItemResponse> listActiveScreenSavers(Long userId);
LegacyScreenSaverCatalogResponse getActiveScreenSavers(Long userId);
}

View File

@ -1,5 +1,6 @@
package com.imeeting.service.android.legacy.impl;
import com.imeeting.dto.android.legacy.LegacyScreenSaverCatalogResponse;
import com.imeeting.dto.android.legacy.LegacyScreenSaverItemResponse;
import com.imeeting.dto.biz.ScreenSaverSelectionResult;
import com.imeeting.service.android.legacy.LegacyScreenSaverAdapterService;
@ -7,8 +8,6 @@ import com.imeeting.service.biz.ScreenSaverService;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import java.util.List;
@Service
@RequiredArgsConstructor
public class LegacyScreenSaverAdapterServiceImpl implements LegacyScreenSaverAdapterService {
@ -16,13 +15,24 @@ public class LegacyScreenSaverAdapterServiceImpl implements LegacyScreenSaverAda
private final ScreenSaverService screenSaverService;
@Override
public List<LegacyScreenSaverItemResponse> listActiveScreenSavers(Long userId) {
public LegacyScreenSaverCatalogResponse getActiveScreenSavers(Long userId) {
ScreenSaverSelectionResult selection = screenSaverService.getActiveSelection(userId);
if (selection == null || selection.getItems() == null || selection.getItems().isEmpty()) {
return List.of();
LegacyScreenSaverCatalogResponse response = new LegacyScreenSaverCatalogResponse();
response.setRefreshIntervalSec(300);
response.setPlayMode("SEQUENTIAL");
if (selection == null) {
response.setSourceScope("PLATFORM");
response.setDisplayDurationSec(15);
response.setItems(java.util.List.of());
return response;
}
return selection.getItems().stream()
.map(LegacyScreenSaverItemResponse::from)
.toList();
response.setSourceScope(selection.getSourceScope());
response.setDisplayDurationSec(selection.getDisplayDurationSec());
response.setItems(selection.getItems() == null
? java.util.List.of()
: selection.getItems().stream()
.map(LegacyScreenSaverItemResponse::from)
.toList());
return response;
}
}

View File

@ -5,6 +5,8 @@ import com.imeeting.dto.biz.ScreenSaverAdminVO;
import com.imeeting.dto.biz.ScreenSaverDTO;
import com.imeeting.dto.biz.ScreenSaverImageUploadVO;
import com.imeeting.dto.biz.ScreenSaverSelectionResult;
import com.imeeting.dto.biz.ScreenSaverUserSettingsDTO;
import com.imeeting.dto.biz.ScreenSaverUserSettingsVO;
import com.imeeting.entity.biz.ScreenSaver;
import com.unisbase.security.LoginUser;
import org.springframework.web.multipart.MultipartFile;
@ -26,4 +28,8 @@ public interface ScreenSaverService extends IService<ScreenSaver> {
ScreenSaverImageUploadVO uploadImage(MultipartFile file) throws IOException;
ScreenSaverSelectionResult getActiveSelection(Long userId);
ScreenSaverUserSettingsVO getMySettings(LoginUser loginUser);
ScreenSaverUserSettingsVO updateMySettings(ScreenSaverUserSettingsDTO dto, LoginUser loginUser);
}

View File

@ -6,10 +6,14 @@ import com.imeeting.dto.biz.ScreenSaverAdminVO;
import com.imeeting.dto.biz.ScreenSaverDTO;
import com.imeeting.dto.biz.ScreenSaverImageUploadVO;
import com.imeeting.dto.biz.ScreenSaverSelectionResult;
import com.imeeting.dto.biz.ScreenSaverUserSettingsDTO;
import com.imeeting.dto.biz.ScreenSaverUserSettingsVO;
import com.imeeting.entity.biz.ScreenSaver;
import com.imeeting.entity.biz.ScreenSaverUserConfig;
import com.imeeting.entity.biz.ScreenSaverUserSettings;
import com.imeeting.mapper.biz.ScreenSaverMapper;
import com.imeeting.mapper.biz.ScreenSaverUserConfigMapper;
import com.imeeting.mapper.biz.ScreenSaverUserSettingsMapper;
import com.imeeting.service.biz.ScreenSaverService;
import com.unisbase.entity.SysUser;
import com.unisbase.mapper.SysUserMapper;
@ -20,6 +24,8 @@ import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.StringUtils;
import org.springframework.web.multipart.MultipartFile;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import javax.imageio.ImageIO;
import java.awt.image.BufferedImage;
@ -46,6 +52,9 @@ public class ScreenSaverServiceImpl extends ServiceImpl<ScreenSaverMapper, Scree
private static final String SCOPE_PLATFORM = "PLATFORM";
private static final String SCOPE_USER = "USER";
private static final String SCOPE_MIXED = "MIXED";
private static final int DEFAULT_DISPLAY_DURATION_SEC = 15;
private static final int MIN_DISPLAY_DURATION_SEC = 3;
private static final int MAX_DISPLAY_DURATION_SEC = 3600;
private static final int REQUIRED_WIDTH = 1280;
private static final int REQUIRED_HEIGHT = 800;
private static final Set<String> ALLOWED_FORMATS = Set.of("jpg", "jpeg", "png");
@ -54,6 +63,7 @@ public class ScreenSaverServiceImpl extends ServiceImpl<ScreenSaverMapper, Scree
.thenComparing(ScreenSaver::getId, Comparator.nullsLast(Comparator.reverseOrder()));
private final ScreenSaverUserConfigMapper userConfigMapper;
private final ScreenSaverUserSettingsMapper userSettingsMapper;
private final SysUserMapper sysUserMapper;
@Value("${unisbase.app.upload-path}")
@ -80,8 +90,11 @@ public class ScreenSaverServiceImpl extends ServiceImpl<ScreenSaverMapper, Scree
wrapper.eq(ScreenSaver::getOwnerUserId, ownerUserId);
}
List<ScreenSaver> records = this.list(wrapper);
Map<Long, Integer> userStatusMap = queryUserStatusMap(loginUser == null ? null : loginUser.getUserId(), extractPlatformIds(records));
return toAdminVOs(records, userStatusMap).stream()
Long tenantId = loginUser == null ? null : loginUser.getTenantId();
Long userId = loginUser == null ? null : loginUser.getUserId();
Map<Long, Integer> userStatusMap = queryUserStatusMap(tenantId, userId, extractPlatformIds(records));
Integer displayDurationSec = resolveDisplayDurationSec(tenantId, userId);
return toAdminVOs(records, userStatusMap, displayDurationSec).stream()
.filter(item -> status == null || Objects.equals(item.getStatus(), status))
.toList();
}
@ -131,6 +144,10 @@ public class ScreenSaverServiceImpl extends ServiceImpl<ScreenSaverMapper, Scree
}
if (isPlatformScope(entity)) {
if (isAdmin(loginUser)) {
entity.setStatus(status);
return this.updateById(entity);
}
return upsertUserStatusConfig(id, status, loginUser);
}
if (!canManageEntity(entity, loginUser)) {
@ -179,13 +196,50 @@ public class ScreenSaverServiceImpl extends ServiceImpl<ScreenSaverMapper, Scree
}
@Override
public ScreenSaverSelectionResult getActiveSelection(Long userId) {
List<ScreenSaver> platformItems = listActiveByScope(SCOPE_PLATFORM, null);
if (userId == null) {
return new ScreenSaverSelectionResult(SCOPE_PLATFORM, toAdminVOs(platformItems));
public ScreenSaverUserSettingsVO getMySettings(LoginUser loginUser) {
if (loginUser == null || loginUser.getUserId() == null) {
throw new RuntimeException("login user is required");
}
return buildUserSettingsVO(loginUser.getUserId(), resolveDisplayDurationSec(loginUser.getTenantId(), loginUser.getUserId()));
}
@Override
@Transactional(rollbackFor = Exception.class)
public ScreenSaverUserSettingsVO updateMySettings(ScreenSaverUserSettingsDTO dto, LoginUser loginUser) {
if (loginUser == null || loginUser.getUserId() == null || loginUser.getTenantId() == null) {
throw new RuntimeException("login user is required");
}
Integer displayDurationSec = dto == null ? null : dto.getDisplayDurationSec();
validateDisplayDurationSec(displayDurationSec);
ScreenSaverUserSettings existing = userSettingsMapper.selectOne(new LambdaQueryWrapper<ScreenSaverUserSettings>()
.eq(ScreenSaverUserSettings::getTenantId, loginUser.getTenantId())
.eq(ScreenSaverUserSettings::getUserId, loginUser.getUserId())
.last("LIMIT 1"));
if (existing != null) {
existing.setDisplayDurationSec(displayDurationSec);
userSettingsMapper.updateById(existing);
return buildUserSettingsVO(loginUser.getUserId(), existing.getDisplayDurationSec());
}
Map<Long, Integer> userStatusMap = queryUserStatusMap(userId, extractPlatformIds(platformItems));
ScreenSaverUserSettings entity = new ScreenSaverUserSettings();
entity.setTenantId(loginUser.getTenantId());
entity.setUserId(loginUser.getUserId());
entity.setDisplayDurationSec(displayDurationSec);
userSettingsMapper.insert(entity);
return buildUserSettingsVO(loginUser.getUserId(), entity.getDisplayDurationSec());
}
@Override
public ScreenSaverSelectionResult getActiveSelection(Long userId) {
Long tenantId = currentTenantId();
Integer displayDurationSec = resolveDisplayDurationSec(tenantId, userId);
List<ScreenSaver> platformItems = listActiveByScope(SCOPE_PLATFORM, null);
if (userId == null) {
return new ScreenSaverSelectionResult(SCOPE_PLATFORM, displayDurationSec, toAdminVOs(platformItems, Map.of(), displayDurationSec));
}
Map<Long, Integer> userStatusMap = queryUserStatusMap(tenantId, userId, extractPlatformIds(platformItems));
List<ScreenSaver> effectivePlatformItems = platformItems.stream()
.filter(item -> effectiveStatus(item, userStatusMap.get(item.getId())) == 1)
.toList();
@ -196,7 +250,11 @@ public class ScreenSaverServiceImpl extends ServiceImpl<ScreenSaverMapper, Scree
selected.addAll(userItems);
selected.sort(SCREEN_SAVER_ORDER);
return new ScreenSaverSelectionResult(resolveSourceScope(effectivePlatformItems, userItems), toAdminVOs(selected, userStatusMap));
return new ScreenSaverSelectionResult(
resolveSourceScope(effectivePlatformItems, userItems),
displayDurationSec,
toAdminVOs(selected, userStatusMap, displayDurationSec)
);
}
private ScreenSaverDTO normalizeCreateDto(ScreenSaverDTO dto, LoginUser loginUser) {
@ -258,13 +316,17 @@ public class ScreenSaverServiceImpl extends ServiceImpl<ScreenSaverMapper, Scree
return userConfigMapper.insert(entity) > 0;
}
private Map<Long, Integer> queryUserStatusMap(Long userId, List<Long> screenSaverIds) {
private Map<Long, Integer> queryUserStatusMap(Long tenantId, Long userId, List<Long> screenSaverIds) {
if (userId == null || screenSaverIds == null || screenSaverIds.isEmpty()) {
return Map.of();
}
List<ScreenSaverUserConfig> configs = userConfigMapper.selectList(new LambdaQueryWrapper<ScreenSaverUserConfig>()
LambdaQueryWrapper<ScreenSaverUserConfig> wrapper = new LambdaQueryWrapper<ScreenSaverUserConfig>()
.eq(ScreenSaverUserConfig::getUserId, userId)
.in(ScreenSaverUserConfig::getScreenSaverId, screenSaverIds));
.in(ScreenSaverUserConfig::getScreenSaverId, screenSaverIds);
if (tenantId != null) {
wrapper.eq(ScreenSaverUserConfig::getTenantId, tenantId);
}
List<ScreenSaverUserConfig> configs = userConfigMapper.selectList(wrapper);
Map<Long, Integer> statusMap = new HashMap<>();
for (ScreenSaverUserConfig config : configs) {
@ -299,10 +361,10 @@ public class ScreenSaverServiceImpl extends ServiceImpl<ScreenSaverMapper, Scree
}
private List<ScreenSaverAdminVO> toAdminVOs(List<ScreenSaver> entities) {
return toAdminVOs(entities, Map.of());
return toAdminVOs(entities, Map.of(), DEFAULT_DISPLAY_DURATION_SEC);
}
private List<ScreenSaverAdminVO> toAdminVOs(List<ScreenSaver> entities, Map<Long, Integer> userStatusMap) {
private List<ScreenSaverAdminVO> toAdminVOs(List<ScreenSaver> entities, Map<Long, Integer> userStatusMap, Integer displayDurationSec) {
if (entities == null || entities.isEmpty()) {
return List.of();
}
@ -312,6 +374,7 @@ public class ScreenSaverServiceImpl extends ServiceImpl<ScreenSaverMapper, Scree
String creatorName = item.getCreatedBy() == null ? null : creatorNames.get(item.getCreatedBy());
ScreenSaverAdminVO vo = ScreenSaverAdminVO.from(item, creatorName);
vo.setStatus(effectiveStatus(item, userStatusMap.get(item.getId())));
vo.setDisplayDurationSec(displayDurationSec);
return vo;
})
.toList();
@ -379,9 +442,6 @@ public class ScreenSaverServiceImpl extends ServiceImpl<ScreenSaverMapper, Scree
if (!SCOPE_PLATFORM.equals(resolvedScopeType) && !SCOPE_USER.equals(resolvedScopeType)) {
throw new RuntimeException("scopeType only supports PLATFORM or USER");
}
if (dto.getDisplayDurationSec() != null && (dto.getDisplayDurationSec() < 3 || dto.getDisplayDurationSec() > 3600)) {
throw new RuntimeException("displayDurationSec must be between 3 and 3600");
}
}
private void requireImageMetadata(ScreenSaverDTO dto) {
@ -427,9 +487,6 @@ public class ScreenSaverServiceImpl extends ServiceImpl<ScreenSaverMapper, Scree
if (!partial || dto.getDescription() != null) {
entity.setDescription(trimToNull(dto.getDescription()));
}
if (!partial || dto.getDisplayDurationSec() != null) {
entity.setDisplayDurationSec(dto.getDisplayDurationSec());
}
if (!partial || dto.getImageWidth() != null) {
entity.setImageWidth(dto.getImageWidth());
}
@ -504,12 +561,50 @@ public class ScreenSaverServiceImpl extends ServiceImpl<ScreenSaverMapper, Scree
return entity != null && SCOPE_USER.equals(normalizeScopeType(entity.getScopeType()));
}
private Integer resolveDisplayDurationSec(Long tenantId, Long userId) {
if (userId == null) {
return DEFAULT_DISPLAY_DURATION_SEC;
}
LambdaQueryWrapper<ScreenSaverUserSettings> wrapper = new LambdaQueryWrapper<ScreenSaverUserSettings>()
.eq(ScreenSaverUserSettings::getUserId, userId)
.last("LIMIT 1");
if (tenantId != null) {
wrapper.eq(ScreenSaverUserSettings::getTenantId, tenantId);
}
ScreenSaverUserSettings settings = userSettingsMapper.selectOne(wrapper);
if (settings == null || settings.getDisplayDurationSec() == null) {
return DEFAULT_DISPLAY_DURATION_SEC;
}
return settings.getDisplayDurationSec();
}
private ScreenSaverUserSettingsVO buildUserSettingsVO(Long userId, Integer displayDurationSec) {
ScreenSaverUserSettingsVO vo = new ScreenSaverUserSettingsVO();
vo.setUserId(userId);
vo.setDisplayDurationSec(displayDurationSec == null ? DEFAULT_DISPLAY_DURATION_SEC : displayDurationSec);
return vo;
}
private Long currentTenantId() {
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
if (authentication == null || !(authentication.getPrincipal() instanceof LoginUser loginUser)) {
return null;
}
return loginUser.getTenantId();
}
private void validateStatus(Integer status) {
if (status == null || (status != 0 && status != 1)) {
throw new RuntimeException("status only supports 0 or 1");
}
}
private void validateDisplayDurationSec(Integer displayDurationSec) {
if (displayDurationSec == null || displayDurationSec < MIN_DISPLAY_DURATION_SEC || displayDurationSec > MAX_DISPLAY_DURATION_SEC) {
throw new RuntimeException("displayDurationSec must be between 3 and 3600");
}
}
private String resolveAndValidateFormat(String fileName, String contentType) {
String extension = fileName.substring(fileName.lastIndexOf('.') + 1).toLowerCase();
if (!ALLOWED_FORMATS.contains(extension)) {

View File

@ -39,7 +39,7 @@ class AndroidScreenSaverControllerTest {
when(taskSecurityContextRunner.callAsTenantUser(eq(9L), eq(88L), any()))
.thenAnswer(invocation -> ((Supplier<?>) invocation.getArgument(2)).get());
when(screenSaverService.getActiveSelection(88L))
.thenReturn(new ScreenSaverSelectionResult("USER", List.of()));
.thenReturn(new ScreenSaverSelectionResult("USER", 18, List.of()));
AndroidScreenSaverController controller =
new AndroidScreenSaverController(androidAuthService, screenSaverService, taskSecurityContextRunner);
@ -49,6 +49,7 @@ class AndroidScreenSaverControllerTest {
verify(taskSecurityContextRunner).callAsTenantUser(eq(9L), eq(88L), any());
verify(screenSaverService).getActiveSelection(88L);
assertEquals("USER", response.getData().getSourceScope());
assertEquals(18, response.getData().getDisplayDurationSec());
}
@Test
@ -63,7 +64,7 @@ class AndroidScreenSaverControllerTest {
when(androidAuthService.authenticateHttp(request)).thenReturn(authContext);
when(screenSaverService.getActiveSelection(null))
.thenReturn(new ScreenSaverSelectionResult("PLATFORM", List.of()));
.thenReturn(new ScreenSaverSelectionResult("PLATFORM", 15, List.of()));
AndroidScreenSaverController controller =
new AndroidScreenSaverController(androidAuthService, screenSaverService, taskSecurityContextRunner);
@ -73,5 +74,6 @@ class AndroidScreenSaverControllerTest {
verify(taskSecurityContextRunner, never()).callAsTenantUser(any(), any(), any());
verify(screenSaverService).getActiveSelection(null);
assertEquals("PLATFORM", response.getData().getSourceScope());
assertEquals(15, response.getData().getDisplayDurationSec());
}
}

View File

@ -1,6 +1,7 @@
package com.imeeting.controller.android.legacy;
import com.imeeting.dto.android.legacy.LegacyApiResponse;
import com.imeeting.dto.android.legacy.LegacyScreenSaverCatalogResponse;
import com.imeeting.dto.android.legacy.LegacyScreenSaverItemResponse;
import com.imeeting.service.android.legacy.LegacyScreenSaverAdapterService;
import com.imeeting.support.TaskSecurityContextRunner;
@ -42,7 +43,10 @@ class LegacyScreenSaverControllerTest {
LegacyScreenSaverItemResponse item = new LegacyScreenSaverItemResponse();
item.setId(1L);
when(adapterService.listActiveScreenSavers(55L)).thenReturn(List.of(item));
LegacyScreenSaverCatalogResponse catalog = new LegacyScreenSaverCatalogResponse();
catalog.setDisplayDurationSec(18);
catalog.setItems(List.of(item));
when(adapterService.getActiveScreenSavers(55L)).thenReturn(catalog);
LegacyScreenSaverController controller = new LegacyScreenSaverController(
adapterService,
@ -50,11 +54,12 @@ class LegacyScreenSaverControllerTest {
taskSecurityContextRunner
);
LegacyApiResponse<List<LegacyScreenSaverItemResponse>> response = controller.active(request);
LegacyApiResponse<LegacyScreenSaverCatalogResponse> response = controller.active(request);
verify(taskSecurityContextRunner).callAsTenantUser(eq(7L), eq(55L), any());
verify(adapterService).listActiveScreenSavers(55L);
assertEquals(1, response.getData().size());
verify(adapterService).getActiveScreenSavers(55L);
assertEquals(18, response.getData().getDisplayDurationSec());
assertEquals(1, response.getData().getItems().size());
}
@Test
@ -67,7 +72,10 @@ class LegacyScreenSaverControllerTest {
when(request.getHeader("Authorization")).thenReturn(null);
LegacyScreenSaverItemResponse item = new LegacyScreenSaverItemResponse();
item.setId(2L);
when(adapterService.listActiveScreenSavers(null)).thenReturn(List.of(item));
LegacyScreenSaverCatalogResponse catalog = new LegacyScreenSaverCatalogResponse();
catalog.setDisplayDurationSec(15);
catalog.setItems(List.of(item));
when(adapterService.getActiveScreenSavers(null)).thenReturn(catalog);
LegacyScreenSaverController controller = new LegacyScreenSaverController(
adapterService,
@ -75,10 +83,11 @@ class LegacyScreenSaverControllerTest {
taskSecurityContextRunner
);
LegacyApiResponse<List<LegacyScreenSaverItemResponse>> response = controller.active(request);
LegacyApiResponse<LegacyScreenSaverCatalogResponse> response = controller.active(request);
verify(taskSecurityContextRunner, never()).callAsTenantUser(any(), any(), any());
verify(adapterService).listActiveScreenSavers(null);
assertEquals(1, response.getData().size());
verify(adapterService).getActiveScreenSavers(null);
assertEquals(15, response.getData().getDisplayDurationSec());
assertEquals(1, response.getData().getItems().size());
}
}

View File

@ -1,5 +1,6 @@
package com.imeeting.service.android.legacy;
import com.imeeting.dto.android.legacy.LegacyScreenSaverCatalogResponse;
import com.imeeting.dto.android.legacy.LegacyScreenSaverItemResponse;
import com.imeeting.dto.biz.ScreenSaverAdminVO;
import com.imeeting.dto.biz.ScreenSaverSelectionResult;
@ -16,7 +17,7 @@ import static org.mockito.Mockito.when;
class LegacyScreenSaverAdapterServiceImplTest {
@Test
void listActiveScreenSaversShouldMapLegacyFields() {
void getActiveScreenSaversShouldMapLegacyFields() {
ScreenSaverService screenSaverService = mock(ScreenSaverService.class);
ScreenSaverAdminVO item = new ScreenSaverAdminVO();
@ -24,7 +25,6 @@ class LegacyScreenSaverAdapterServiceImplTest {
item.setName("欢迎屏");
item.setImageUrl("/api/static/screen-savers/images/a.jpg");
item.setDescription("主大厅欢迎屏");
item.setDisplayDurationSec(12);
item.setSortOrder(3);
item.setStatus(1);
item.setCreatedAt("2026-04-17T16:00:00");
@ -33,18 +33,20 @@ class LegacyScreenSaverAdapterServiceImplTest {
item.setCreatorUsername("admin");
when(screenSaverService.getActiveSelection(55L))
.thenReturn(new ScreenSaverSelectionResult("PLATFORM", List.of(item)));
.thenReturn(new ScreenSaverSelectionResult("PLATFORM", 12, List.of(item)));
LegacyScreenSaverAdapterServiceImpl service = new LegacyScreenSaverAdapterServiceImpl(screenSaverService);
List<LegacyScreenSaverItemResponse> result = service.listActiveScreenSavers(55L);
LegacyScreenSaverCatalogResponse result = service.getActiveScreenSavers(55L);
assertEquals(1, result.size());
assertEquals(9L, result.get(0).getId());
assertEquals("欢迎屏", result.get(0).getName());
assertEquals("/api/static/screen-savers/images/a.jpg", result.get(0).getImageUrl());
assertEquals(12, result.get(0).getDisplayDurationSec());
assertEquals(1, result.get(0).getIsActive());
assertEquals("admin", result.get(0).getCreatorUsername());
assertEquals(12, result.getDisplayDurationSec());
assertEquals("PLATFORM", result.getSourceScope());
assertEquals(1, result.getItems().size());
LegacyScreenSaverItemResponse first = result.getItems().get(0);
assertEquals(9L, first.getId());
assertEquals("欢迎屏", first.getName());
assertEquals("/api/static/screen-savers/images/a.jpg", first.getImageUrl());
assertEquals(1, first.getIsActive());
assertEquals("admin", first.getCreatorUsername());
}
}

View File

@ -3,9 +3,13 @@ package com.imeeting.service.biz.impl;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.imeeting.dto.biz.ScreenSaverAdminVO;
import com.imeeting.dto.biz.ScreenSaverSelectionResult;
import com.imeeting.dto.biz.ScreenSaverUserSettingsDTO;
import com.imeeting.dto.biz.ScreenSaverUserSettingsVO;
import com.imeeting.entity.biz.ScreenSaver;
import com.imeeting.entity.biz.ScreenSaverUserConfig;
import com.imeeting.entity.biz.ScreenSaverUserSettings;
import com.imeeting.mapper.biz.ScreenSaverUserConfigMapper;
import com.imeeting.mapper.biz.ScreenSaverUserSettingsMapper;
import com.unisbase.mapper.SysUserMapper;
import com.unisbase.security.LoginUser;
import org.junit.jupiter.api.Test;
@ -20,6 +24,7 @@ import static org.mockito.Mockito.doReturn;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.spy;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
@ -28,11 +33,13 @@ class ScreenSaverServiceImplTest {
@Test
void getActiveSelectionShouldMergePlatformAndUserItems() {
ScreenSaverUserConfigMapper userConfigMapper = mock(ScreenSaverUserConfigMapper.class);
ScreenSaverUserSettingsMapper userSettingsMapper = mock(ScreenSaverUserSettingsMapper.class);
SysUserMapper sysUserMapper = mock(SysUserMapper.class);
when(userConfigMapper.selectList(any())).thenReturn(List.of(userConfig(101L, 77L, 0)));
when(userSettingsMapper.selectOne(any())).thenReturn(userSettings(77L, 22));
when(sysUserMapper.selectBatchIds(any())).thenReturn(List.of());
ScreenSaverServiceImpl service = spy(new ScreenSaverServiceImpl(userConfigMapper, sysUserMapper));
ScreenSaverServiceImpl service = spy(new ScreenSaverServiceImpl(userConfigMapper, userSettingsMapper, sysUserMapper));
doReturn(List.of(
screenSaver(101L, "PLATFORM", null, 1, 2),
screenSaver(102L, "PLATFORM", null, 1, 5)
@ -43,18 +50,22 @@ class ScreenSaverServiceImplTest {
ScreenSaverSelectionResult result = service.getActiveSelection(77L);
assertEquals("MIXED", result.getSourceScope());
assertEquals(22, result.getDisplayDurationSec());
assertEquals(List.of(201L, 102L), result.getItems().stream().map(ScreenSaverAdminVO::getId).toList());
assertEquals(List.of(1, 1), result.getItems().stream().map(ScreenSaverAdminVO::getStatus).toList());
assertEquals(List.of(22, 22), result.getItems().stream().map(ScreenSaverAdminVO::getDisplayDurationSec).toList());
}
@Test
void listForAdminShouldApplyCurrentUserStatusFilter() {
ScreenSaverUserConfigMapper userConfigMapper = mock(ScreenSaverUserConfigMapper.class);
ScreenSaverUserSettingsMapper userSettingsMapper = mock(ScreenSaverUserSettingsMapper.class);
SysUserMapper sysUserMapper = mock(SysUserMapper.class);
when(userConfigMapper.selectList(any())).thenReturn(List.of(userConfig(101L, 88L, 0)));
when(userSettingsMapper.selectOne(any())).thenReturn(userSettings(88L, 18));
when(sysUserMapper.selectBatchIds(any())).thenReturn(List.of());
ScreenSaverServiceImpl service = spy(new ScreenSaverServiceImpl(userConfigMapper, sysUserMapper));
ScreenSaverServiceImpl service = spy(new ScreenSaverServiceImpl(userConfigMapper, userSettingsMapper, sysUserMapper));
doReturn(List.of(
screenSaver(101L, "PLATFORM", null, 1, 1),
screenSaver(201L, "USER", 88L, 1, 2)
@ -65,16 +76,39 @@ class ScreenSaverServiceImplTest {
assertEquals(1, result.size());
assertEquals(101L, result.get(0).getId());
assertEquals(0, result.get(0).getStatus());
assertEquals(18, result.get(0).getDisplayDurationSec());
}
@Test
void listForAdminShouldFallbackToGlobalStatusWhenNoUserOverrideExists() {
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(null);
when(sysUserMapper.selectBatchIds(any())).thenReturn(List.of());
ScreenSaverServiceImpl service = spy(new ScreenSaverServiceImpl(userConfigMapper, userSettingsMapper, sysUserMapper));
doReturn(List.of(screenSaver(101L, "PLATFORM", null, 1, 1)))
.when(service).list(any(LambdaQueryWrapper.class));
List<ScreenSaverAdminVO> result = service.listForAdmin(loginUser(88L, 9L, false), null, null, null, null);
assertEquals(1, result.size());
assertEquals(101L, result.get(0).getId());
assertEquals(1, result.get(0).getStatus());
assertEquals(15, result.get(0).getDisplayDurationSec());
}
@Test
void updateStatusShouldStoreUserOverrideForPlatformItem() {
ScreenSaverUserConfigMapper userConfigMapper = mock(ScreenSaverUserConfigMapper.class);
ScreenSaverUserSettingsMapper userSettingsMapper = mock(ScreenSaverUserSettingsMapper.class);
SysUserMapper sysUserMapper = mock(SysUserMapper.class);
when(userConfigMapper.selectOne(any())).thenReturn(null);
when(userConfigMapper.insert(any(ScreenSaverUserConfig.class))).thenReturn(1);
ScreenSaverServiceImpl service = spy(new ScreenSaverServiceImpl(userConfigMapper, sysUserMapper));
ScreenSaverServiceImpl service = spy(new ScreenSaverServiceImpl(userConfigMapper, userSettingsMapper, sysUserMapper));
doReturn(screenSaver(101L, "PLATFORM", null, 1, 1)).when(service).getOne(any(LambdaQueryWrapper.class));
boolean success = service.updateStatus(101L, 0, loginUser(88L, 9L, false));
@ -89,6 +123,61 @@ class ScreenSaverServiceImplTest {
assertEquals(0, captor.getValue().getStatus());
}
@Test
void updateStatusShouldUpdateGlobalStatusForAdminPlatformItem() {
ScreenSaverUserConfigMapper userConfigMapper = mock(ScreenSaverUserConfigMapper.class);
ScreenSaverUserSettingsMapper userSettingsMapper = mock(ScreenSaverUserSettingsMapper.class);
SysUserMapper sysUserMapper = mock(SysUserMapper.class);
ScreenSaverServiceImpl service = spy(new ScreenSaverServiceImpl(userConfigMapper, userSettingsMapper, sysUserMapper));
doReturn(screenSaver(101L, "PLATFORM", null, 1, 1)).when(service).getOne(any(LambdaQueryWrapper.class));
doReturn(true).when(service).updateById(any(ScreenSaver.class));
boolean success = service.updateStatus(101L, 0, loginUser(88L, 9L, true));
assertTrue(success);
verify(service, times(1)).updateById(any(ScreenSaver.class));
verify(userConfigMapper, never()).selectOne(any());
verify(userConfigMapper, never()).insert(any(ScreenSaverUserConfig.class));
}
@Test
void getMySettingsShouldFallbackToDefaultDuration() {
ScreenSaverUserConfigMapper userConfigMapper = mock(ScreenSaverUserConfigMapper.class);
ScreenSaverUserSettingsMapper userSettingsMapper = mock(ScreenSaverUserSettingsMapper.class);
SysUserMapper sysUserMapper = mock(SysUserMapper.class);
when(userSettingsMapper.selectOne(any())).thenReturn(null);
ScreenSaverServiceImpl service = new ScreenSaverServiceImpl(userConfigMapper, userSettingsMapper, sysUserMapper);
ScreenSaverUserSettingsVO result = service.getMySettings(loginUser(88L, 9L, false));
assertEquals(88L, result.getUserId());
assertEquals(15, result.getDisplayDurationSec());
}
@Test
void updateMySettingsShouldInsertWhenMissing() {
ScreenSaverUserConfigMapper userConfigMapper = mock(ScreenSaverUserConfigMapper.class);
ScreenSaverUserSettingsMapper userSettingsMapper = mock(ScreenSaverUserSettingsMapper.class);
SysUserMapper sysUserMapper = mock(SysUserMapper.class);
when(userSettingsMapper.selectOne(any())).thenReturn(null);
when(userSettingsMapper.insert(any(ScreenSaverUserSettings.class))).thenReturn(1);
ScreenSaverServiceImpl service = new ScreenSaverServiceImpl(userConfigMapper, userSettingsMapper, sysUserMapper);
ScreenSaverUserSettingsDTO dto = new ScreenSaverUserSettingsDTO();
dto.setDisplayDurationSec(20);
ScreenSaverUserSettingsVO result = service.updateMySettings(dto, loginUser(88L, 9L, false));
ArgumentCaptor<ScreenSaverUserSettings> captor = ArgumentCaptor.forClass(ScreenSaverUserSettings.class);
verify(userSettingsMapper).insert(captor.capture());
assertEquals(9L, captor.getValue().getTenantId());
assertEquals(88L, captor.getValue().getUserId());
assertEquals(20, captor.getValue().getDisplayDurationSec());
assertEquals(20, result.getDisplayDurationSec());
}
private ScreenSaver screenSaver(Long id, String scopeType, Long ownerUserId, Integer status, Integer sortOrder) {
ScreenSaver entity = new ScreenSaver();
entity.setId(id);
@ -109,6 +198,13 @@ class ScreenSaverServiceImplTest {
return config;
}
private ScreenSaverUserSettings userSettings(Long userId, Integer displayDurationSec) {
ScreenSaverUserSettings settings = new ScreenSaverUserSettings();
settings.setUserId(userId);
settings.setDisplayDurationSec(displayDurationSec);
return settings;
}
private LoginUser loginUser(Long userId, Long tenantId, boolean admin) {
LoginUser loginUser = new LoginUser();
loginUser.setUserId(userId);

View File

@ -10,7 +10,7 @@ export interface ScreenSaverVO {
name: string;
imageUrl: string;
description?: string;
displayDurationSec: number;
displayDurationSec?: number;
imageWidth?: number;
imageHeight?: number;
imageFormat?: string;
@ -29,7 +29,6 @@ export interface ScreenSaverDTO {
name: string;
imageUrl: string;
description?: string;
displayDurationSec: number;
imageWidth: number;
imageHeight: number;
imageFormat: string;
@ -38,6 +37,15 @@ export interface ScreenSaverDTO {
remark?: string;
}
export interface ScreenSaverUserSettingsVO {
userId: number;
displayDurationSec: number;
}
export interface ScreenSaverUserSettingsDTO {
displayDurationSec: number;
}
export interface ScreenSaverUploadResult {
imageUrl: string;
fileSize: number;
@ -61,6 +69,16 @@ export async function createScreenSaver(payload: ScreenSaverDTO) {
return resp.data.data as ScreenSaverVO;
}
export async function getMyScreenSaverSettings() {
const resp = await http.get("/api/screen-savers/my-settings");
return resp.data.data as ScreenSaverUserSettingsVO;
}
export async function updateMyScreenSaverSettings(payload: ScreenSaverUserSettingsDTO) {
const resp = await http.put("/api/screen-savers/my-settings", payload);
return resp.data.data as ScreenSaverUserSettingsVO;
}
export async function updateScreenSaver(id: number, payload: Partial<ScreenSaverDTO>) {
const resp = await http.put(`/api/screen-savers/${id}`, payload);
return resp.data.data as ScreenSaverVO;

View File

@ -26,12 +26,12 @@ import type { ColumnsType } from "antd/es/table";
import {
DeleteOutlined,
EditOutlined,
PictureOutlined,
PlusOutlined,
ReloadOutlined,
SaveOutlined,
ScissorOutlined,
SearchOutlined,
SettingOutlined,
TeamOutlined,
UploadOutlined,
UserOutlined,
@ -42,11 +42,14 @@ import AppPagination from "@/components/shared/AppPagination";
import {
createScreenSaver,
deleteScreenSaver,
getMyScreenSaverSettings,
listScreenSavers,
type ScreenSaverDTO,
type ScreenSaverScopeType,
type ScreenSaverUserSettingsVO,
type ScreenSaverUploadResult,
type ScreenSaverVO,
updateMyScreenSaverSettings,
updateScreenSaver,
updateScreenSaverStatus,
uploadScreenSaverImage,
@ -74,7 +77,6 @@ type ScreenSaverFormValues = {
name: string;
imageUrl: string;
description?: string;
displayDurationSec: number;
imageWidth: number;
imageHeight: number;
imageFormat: string;
@ -83,6 +85,10 @@ type ScreenSaverFormValues = {
remark?: string;
};
type ScreenSaverSettingsFormValues = {
displayDurationSec: number;
};
type CropModalState = {
open: boolean;
src: string;
@ -365,12 +371,16 @@ function ScreenSaverCropDialog({ state, onCancel, onConfirm }: CropDialogProps)
export default function ScreenSaverManagement() {
const { message } = App.useApp();
const [form] = Form.useForm<ScreenSaverFormValues>();
const [settingsForm] = Form.useForm<ScreenSaverSettingsFormValues>();
const [loading, setLoading] = useState(false);
const [saving, setSaving] = useState(false);
const [settingsSaving, setSettingsSaving] = useState(false);
const [drawerOpen, setDrawerOpen] = useState(false);
const [settingsOpen, setSettingsOpen] = useState(false);
const [editing, setEditing] = useState<ScreenSaverVO | null>(null);
const [records, setRecords] = useState<ScreenSaverVO[]>([]);
const [users, setUsers] = useState<SysUser[]>([]);
const [mySettings, setMySettings] = useState<ScreenSaverUserSettingsVO>({ userId: 0, displayDurationSec: 15 });
const [searchValue, setSearchValue] = useState("");
const [statusFilter, setStatusFilter] = useState<"all" | "enabled" | "disabled">("all");
const [scopeFilter, setScopeFilter] = useState<"all" | ScreenSaverScopeType>("all");
@ -399,12 +409,14 @@ export default function ScreenSaverManagement() {
const loadData = async () => {
setLoading(true);
try {
const [screenSavers, userList] = await Promise.all([
const [screenSavers, userList, settings] = await Promise.all([
listScreenSavers(),
listUsers().catch(() => [] as SysUser[]),
getMyScreenSaverSettings().catch(() => ({ userId: currentUserId || 0, displayDurationSec: 15 })),
]);
setRecords(screenSavers || []);
setUsers(userList || []);
setMySettings(settings || { userId: currentUserId || 0, displayDurationSec: 15 });
} finally {
setLoading(false);
}
@ -450,7 +462,6 @@ export default function ScreenSaverManagement() {
form.setFieldsValue({
scopeType: "USER",
ownerUserId: currentUserId || undefined,
displayDurationSec: 15,
sortOrder: 0,
statusEnabled: true,
imageWidth: CROP_WIDTH,
@ -472,7 +483,6 @@ export default function ScreenSaverManagement() {
name: record.name,
imageUrl: record.imageUrl,
description: record.description,
displayDurationSec: record.displayDurationSec,
imageWidth: record.imageWidth || CROP_WIDTH,
imageHeight: record.imageHeight || CROP_HEIGHT,
imageFormat: record.imageFormat || "jpg",
@ -491,20 +501,20 @@ export default function ScreenSaverManagement() {
const handleSubmit = async () => {
const values = await form.validateFields();
const resolvedScopeType = isAdmin ? values.scopeType : "USER";
const resolvedScopeType: ScreenSaverScopeType = isAdmin ? values.scopeType : "USER";
const resolvedOwnerUserId = resolvedScopeType === "USER" ? currentUserId || null : null;
const resolvedStatusEnabled = values.statusEnabled ?? true;
const payload: ScreenSaverDTO = {
scopeType: resolvedScopeType,
ownerUserId: resolvedOwnerUserId,
name: values.name.trim(),
imageUrl: values.imageUrl.trim(),
description: values.description?.trim(),
displayDurationSec: values.displayDurationSec,
imageWidth: values.imageWidth,
imageHeight: values.imageHeight,
imageFormat: values.imageFormat.trim().toLowerCase(),
sortOrder: values.sortOrder,
status: values.statusEnabled ? 1 : 0,
status: resolvedStatusEnabled ? 1 : 0,
remark: values.remark?.trim(),
};
@ -573,6 +583,29 @@ export default function ScreenSaverManagement() {
await loadData();
};
const openSettingsModal = () => {
settingsForm.setFieldsValue({
displayDurationSec: mySettings.displayDurationSec,
});
setSettingsOpen(true);
};
const handleSaveSettings = async () => {
const values = await settingsForm.validateFields();
setSettingsSaving(true);
try {
const result = await updateMyScreenSaverSettings({
displayDurationSec: values.displayDurationSec,
});
setMySettings(result);
setSettingsOpen(false);
message.success("播放设置已更新");
await loadData();
} finally {
setSettingsSaving(false);
}
};
const columns: ColumnsType<ScreenSaverVO> = [
{
title: "屏保画面",
@ -614,12 +647,11 @@ export default function ScreenSaverManagement() {
),
},
{
title: "播放与状态",
title: "排序与状态",
key: "status",
width: 210,
render: (_, record) => (
<Space direction="vertical" size={6}>
<Text>{record.displayDurationSec} / </Text>
<Text type="secondary">{record.sortOrder ?? 0}</Text>
<Switch size="small" checked={record.status === 1} onChange={(checked) => void handleToggleStatus(record, checked)} />
</Space>
@ -663,8 +695,8 @@ export default function ScreenSaverManagement() {
},
];
const currentImageUrl = Form.useWatch("imageUrl", form);
const currentScopeType = Form.useWatch("scopeType", form) || "PLATFORM";
const currentOwnerUserId = Form.useWatch("ownerUserId", form);
const watchedScopeType = Form.useWatch("scopeType", form);
const currentScopeType: ScreenSaverScopeType = isAdmin ? (watchedScopeType || "PLATFORM") : "USER";
const currentWidth = Form.useWatch("imageWidth", form) || CROP_WIDTH;
const currentHeight = Form.useWatch("imageHeight", form) || CROP_HEIGHT;
@ -708,6 +740,9 @@ export default function ScreenSaverManagement() {
<Button icon={<ReloadOutlined />} onClick={() => void loadData()} loading={loading}>
</Button>
<Button icon={<SettingOutlined />} onClick={openSettingsModal}>
{mySettings.displayDurationSec} /
</Button>
<Button type="primary" icon={<PlusOutlined />} onClick={openCreate}>
</Button>
@ -758,16 +793,11 @@ export default function ScreenSaverManagement() {
>
<Form form={form} layout="vertical" className="screen-saver-drawer__form">
<Row gutter={16}>
<Col xs={24} md={14}>
<Col xs={24}>
<Form.Item name="name" label="屏保名称" rules={[{ required: true, message: "请输入屏保名称" }]}>
<Input placeholder="例如:大厅欢迎屏、品牌发布屏" />
</Form.Item>
</Col>
<Col xs={24} md={10}>
<Form.Item name="displayDurationSec" label="展示时长(秒)" rules={[{ required: true, message: "请输入展示时长" }]}>
<InputNumber min={3} max={3600} style={{ width: "100%" }} />
</Form.Item>
</Col>
</Row>
{isAdmin ? (
@ -887,13 +917,34 @@ export default function ScreenSaverManagement() {
</Row>
{(isAdmin || currentScopeType === "USER") ? (
<Form.Item name="statusEnabled" label="启用状态" valuePropName="checked">
<Form.Item name="statusEnabled" label="启用状态" valuePropName="checked" preserve>
<Switch />
</Form.Item>
) : null}
</Form>
</Drawer>
<Modal
title="我的屏保播放设置"
open={settingsOpen}
onCancel={() => setSettingsOpen(false)}
onOk={() => void handleSaveSettings()}
okText="保存"
confirmLoading={settingsSaving}
destroyOnHidden
>
<Form form={settingsForm} layout="vertical">
<Form.Item
name="displayDurationSec"
label="统一展示时长(秒)"
rules={[{ required: true, message: "请输入统一展示时长" }]}
extra="该时长会应用到你看到的所有屏保图片,包括平台级和个人级素材。"
>
<InputNumber min={3} max={3600} style={{ width: "100%" }} />
</Form.Item>
</Form>
</Modal>
<ScreenSaverCropDialog
state={cropState}
onCancel={() => setCropState((prev) => ({ ...prev, open: false, src: "" }))}