diff --git a/backend/design/db_schema.md b/backend/design/db_schema.md index 5db62cb..802da1a 100644 --- a/backend/design/db_schema.md +++ b/backend/design/db_schema.md @@ -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` + diff --git a/backend/design/db_schema_pgsql.sql b/backend/design/db_schema_pgsql.sql index 5b5f1d6..516e915 100644 --- a/backend/design/db_schema_pgsql.sql +++ b/backend/design/db_schema_pgsql.sql @@ -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; + diff --git a/backend/src/main/java/com/imeeting/controller/android/AndroidScreenSaverController.java b/backend/src/main/java/com/imeeting/controller/android/AndroidScreenSaverController.java index 11808db..042bd6a 100644 --- a/backend/src/main/java/com/imeeting/controller/android/AndroidScreenSaverController.java +++ b/backend/src/main/java/com/imeeting/controller/android/AndroidScreenSaverController.java @@ -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; diff --git a/backend/src/main/java/com/imeeting/controller/android/legacy/LegacyScreenSaverController.java b/backend/src/main/java/com/imeeting/controller/android/legacy/LegacyScreenSaverController.java index f6b47cc..902f4ff 100644 --- a/backend/src/main/java/com/imeeting/controller/android/legacy/LegacyScreenSaverController.java +++ b/backend/src/main/java/com/imeeting/controller/android/legacy/LegacyScreenSaverController.java @@ -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> active(HttpServletRequest request) { + public LegacyApiResponse active(HttpServletRequest request) { LoginUser loginUser = resolveLoginUser(request); return LegacyApiResponse.ok(queryActive(loginUser)); } - private List 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()) ); } diff --git a/backend/src/main/java/com/imeeting/controller/biz/ScreenSaverController.java b/backend/src/main/java/com/imeeting/controller/biz/ScreenSaverController.java index a37e6a0..b57e9b2 100644 --- a/backend/src/main/java/com/imeeting/controller/biz/ScreenSaverController.java +++ b/backend/src/main/java/com/imeeting/controller/biz/ScreenSaverController.java @@ -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 getMySettings() { + return ApiResponse.ok(screenSaverService.getMySettings(currentLoginUser())); + } + + @Operation(summary = "更新当前用户屏保播放设置") + @PutMapping("/my-settings") + @PreAuthorize("isAuthenticated()") + public ApiResponse updateMySettings(@RequestBody ScreenSaverUserSettingsDTO dto) { + return ApiResponse.ok(screenSaverService.updateMySettings(dto, currentLoginUser())); + } + @Operation(summary = "新增屏保") @PostMapping @PreAuthorize("isAuthenticated()") diff --git a/backend/src/main/java/com/imeeting/dto/android/AndroidScreenSaverCatalogVO.java b/backend/src/main/java/com/imeeting/dto/android/AndroidScreenSaverCatalogVO.java index 26b2d88..e064ae0 100644 --- a/backend/src/main/java/com/imeeting/dto/android/AndroidScreenSaverCatalogVO.java +++ b/backend/src/main/java/com/imeeting/dto/android/AndroidScreenSaverCatalogVO.java @@ -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 = "屏保图片项列表") diff --git a/backend/src/main/java/com/imeeting/dto/android/AndroidScreenSaverItemVO.java b/backend/src/main/java/com/imeeting/dto/android/AndroidScreenSaverItemVO.java index 22f37e2..a6117c8 100644 --- a/backend/src/main/java/com/imeeting/dto/android/AndroidScreenSaverItemVO.java +++ b/backend/src/main/java/com/imeeting/dto/android/AndroidScreenSaverItemVO.java @@ -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 = "最近更新时间") diff --git a/backend/src/main/java/com/imeeting/dto/android/legacy/LegacyScreenSaverItemResponse.java b/backend/src/main/java/com/imeeting/dto/android/legacy/LegacyScreenSaverItemResponse.java index faeff45..fd7e1c9 100644 --- a/backend/src/main/java/com/imeeting/dto/android/legacy/LegacyScreenSaverItemResponse.java +++ b/backend/src/main/java/com/imeeting/dto/android/legacy/LegacyScreenSaverItemResponse.java @@ -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()); diff --git a/backend/src/main/java/com/imeeting/dto/biz/ScreenSaverAdminVO.java b/backend/src/main/java/com/imeeting/dto/biz/ScreenSaverAdminVO.java index 12a6566..f9bfafd 100644 --- a/backend/src/main/java/com/imeeting/dto/biz/ScreenSaverAdminVO.java +++ b/backend/src/main/java/com/imeeting/dto/biz/ScreenSaverAdminVO.java @@ -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()); diff --git a/backend/src/main/java/com/imeeting/dto/biz/ScreenSaverDTO.java b/backend/src/main/java/com/imeeting/dto/biz/ScreenSaverDTO.java index f6fcb1b..5bbff42 100644 --- a/backend/src/main/java/com/imeeting/dto/biz/ScreenSaverDTO.java +++ b/backend/src/main/java/com/imeeting/dto/biz/ScreenSaverDTO.java @@ -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; diff --git a/backend/src/main/java/com/imeeting/dto/biz/ScreenSaverSelectionResult.java b/backend/src/main/java/com/imeeting/dto/biz/ScreenSaverSelectionResult.java index e96593a..47cc8ee 100644 --- a/backend/src/main/java/com/imeeting/dto/biz/ScreenSaverSelectionResult.java +++ b/backend/src/main/java/com/imeeting/dto/biz/ScreenSaverSelectionResult.java @@ -9,5 +9,6 @@ import java.util.List; @AllArgsConstructor public class ScreenSaverSelectionResult { private String sourceScope; + private Integer displayDurationSec; private List items; } diff --git a/backend/src/main/java/com/imeeting/entity/biz/ScreenSaver.java b/backend/src/main/java/com/imeeting/entity/biz/ScreenSaver.java index 6614ca6..752ce76 100644 --- a/backend/src/main/java/com/imeeting/entity/biz/ScreenSaver.java +++ b/backend/src/main/java/com/imeeting/entity/biz/ScreenSaver.java @@ -32,9 +32,6 @@ public class ScreenSaver extends BaseEntity { @Schema(description = "屏保描述") private String description; - @Schema(description = "展示时长,单位秒") - private Integer displayDurationSec; - @Schema(description = "图片宽度") private Integer imageWidth; diff --git a/backend/src/main/java/com/imeeting/service/android/legacy/LegacyScreenSaverAdapterService.java b/backend/src/main/java/com/imeeting/service/android/legacy/LegacyScreenSaverAdapterService.java index 32939cc..aac37dd 100644 --- a/backend/src/main/java/com/imeeting/service/android/legacy/LegacyScreenSaverAdapterService.java +++ b/backend/src/main/java/com/imeeting/service/android/legacy/LegacyScreenSaverAdapterService.java @@ -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 listActiveScreenSavers(Long userId); + LegacyScreenSaverCatalogResponse getActiveScreenSavers(Long userId); } diff --git a/backend/src/main/java/com/imeeting/service/android/legacy/impl/LegacyScreenSaverAdapterServiceImpl.java b/backend/src/main/java/com/imeeting/service/android/legacy/impl/LegacyScreenSaverAdapterServiceImpl.java index b8ebc67..226e87e 100644 --- a/backend/src/main/java/com/imeeting/service/android/legacy/impl/LegacyScreenSaverAdapterServiceImpl.java +++ b/backend/src/main/java/com/imeeting/service/android/legacy/impl/LegacyScreenSaverAdapterServiceImpl.java @@ -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 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; } } diff --git a/backend/src/main/java/com/imeeting/service/biz/ScreenSaverService.java b/backend/src/main/java/com/imeeting/service/biz/ScreenSaverService.java index d33fa94..d05d511 100644 --- a/backend/src/main/java/com/imeeting/service/biz/ScreenSaverService.java +++ b/backend/src/main/java/com/imeeting/service/biz/ScreenSaverService.java @@ -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 { ScreenSaverImageUploadVO uploadImage(MultipartFile file) throws IOException; ScreenSaverSelectionResult getActiveSelection(Long userId); + + ScreenSaverUserSettingsVO getMySettings(LoginUser loginUser); + + ScreenSaverUserSettingsVO updateMySettings(ScreenSaverUserSettingsDTO dto, LoginUser loginUser); } diff --git a/backend/src/main/java/com/imeeting/service/biz/impl/ScreenSaverServiceImpl.java b/backend/src/main/java/com/imeeting/service/biz/impl/ScreenSaverServiceImpl.java index efeb92a..30f7ce0 100644 --- a/backend/src/main/java/com/imeeting/service/biz/impl/ScreenSaverServiceImpl.java +++ b/backend/src/main/java/com/imeeting/service/biz/impl/ScreenSaverServiceImpl.java @@ -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 ALLOWED_FORMATS = Set.of("jpg", "jpeg", "png"); @@ -54,6 +63,7 @@ public class ScreenSaverServiceImpl extends ServiceImpl records = this.list(wrapper); - Map 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 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 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() + .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 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 platformItems = listActiveByScope(SCOPE_PLATFORM, null); + if (userId == null) { + return new ScreenSaverSelectionResult(SCOPE_PLATFORM, displayDurationSec, toAdminVOs(platformItems, Map.of(), displayDurationSec)); + } + + Map userStatusMap = queryUserStatusMap(tenantId, userId, extractPlatformIds(platformItems)); List effectivePlatformItems = platformItems.stream() .filter(item -> effectiveStatus(item, userStatusMap.get(item.getId())) == 1) .toList(); @@ -196,7 +250,11 @@ public class ScreenSaverServiceImpl extends ServiceImpl 0; } - private Map queryUserStatusMap(Long userId, List screenSaverIds) { + private Map queryUserStatusMap(Long tenantId, Long userId, List screenSaverIds) { if (userId == null || screenSaverIds == null || screenSaverIds.isEmpty()) { return Map.of(); } - List configs = userConfigMapper.selectList(new LambdaQueryWrapper() + LambdaQueryWrapper wrapper = new LambdaQueryWrapper() .eq(ScreenSaverUserConfig::getUserId, userId) - .in(ScreenSaverUserConfig::getScreenSaverId, screenSaverIds)); + .in(ScreenSaverUserConfig::getScreenSaverId, screenSaverIds); + if (tenantId != null) { + wrapper.eq(ScreenSaverUserConfig::getTenantId, tenantId); + } + List configs = userConfigMapper.selectList(wrapper); Map statusMap = new HashMap<>(); for (ScreenSaverUserConfig config : configs) { @@ -299,10 +361,10 @@ public class ScreenSaverServiceImpl extends ServiceImpl toAdminVOs(List entities) { - return toAdminVOs(entities, Map.of()); + return toAdminVOs(entities, Map.of(), DEFAULT_DISPLAY_DURATION_SEC); } - private List toAdminVOs(List entities, Map userStatusMap) { + private List toAdminVOs(List entities, Map userStatusMap, Integer displayDurationSec) { if (entities == null || entities.isEmpty()) { return List.of(); } @@ -312,6 +374,7 @@ public class ScreenSaverServiceImpl extends ServiceImpl 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 wrapper = new LambdaQueryWrapper() + .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)) { diff --git a/backend/src/test/java/com/imeeting/controller/android/AndroidScreenSaverControllerTest.java b/backend/src/test/java/com/imeeting/controller/android/AndroidScreenSaverControllerTest.java index 520e9b7..13c1920 100644 --- a/backend/src/test/java/com/imeeting/controller/android/AndroidScreenSaverControllerTest.java +++ b/backend/src/test/java/com/imeeting/controller/android/AndroidScreenSaverControllerTest.java @@ -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()); } } diff --git a/backend/src/test/java/com/imeeting/controller/android/legacy/LegacyScreenSaverControllerTest.java b/backend/src/test/java/com/imeeting/controller/android/legacy/LegacyScreenSaverControllerTest.java index 0a494d1..bb84a86 100644 --- a/backend/src/test/java/com/imeeting/controller/android/legacy/LegacyScreenSaverControllerTest.java +++ b/backend/src/test/java/com/imeeting/controller/android/legacy/LegacyScreenSaverControllerTest.java @@ -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> response = controller.active(request); + LegacyApiResponse 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> response = controller.active(request); + LegacyApiResponse 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()); } } diff --git a/backend/src/test/java/com/imeeting/service/android/legacy/LegacyScreenSaverAdapterServiceImplTest.java b/backend/src/test/java/com/imeeting/service/android/legacy/LegacyScreenSaverAdapterServiceImplTest.java index d8facc7..0813079 100644 --- a/backend/src/test/java/com/imeeting/service/android/legacy/LegacyScreenSaverAdapterServiceImplTest.java +++ b/backend/src/test/java/com/imeeting/service/android/legacy/LegacyScreenSaverAdapterServiceImplTest.java @@ -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 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()); } } diff --git a/backend/src/test/java/com/imeeting/service/biz/impl/ScreenSaverServiceImplTest.java b/backend/src/test/java/com/imeeting/service/biz/impl/ScreenSaverServiceImplTest.java index fb1a081..d9051a2 100644 --- a/backend/src/test/java/com/imeeting/service/biz/impl/ScreenSaverServiceImplTest.java +++ b/backend/src/test/java/com/imeeting/service/biz/impl/ScreenSaverServiceImplTest.java @@ -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 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 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); diff --git a/frontend/src/api/business/screenSaver.ts b/frontend/src/api/business/screenSaver.ts index b0cbdf8..8a46ad9 100644 --- a/frontend/src/api/business/screenSaver.ts +++ b/frontend/src/api/business/screenSaver.ts @@ -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) { const resp = await http.put(`/api/screen-savers/${id}`, payload); return resp.data.data as ScreenSaverVO; diff --git a/frontend/src/pages/business/ScreenSaverManagement.tsx b/frontend/src/pages/business/ScreenSaverManagement.tsx index aae4f09..345cc94 100644 --- a/frontend/src/pages/business/ScreenSaverManagement.tsx +++ b/frontend/src/pages/business/ScreenSaverManagement.tsx @@ -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(); + const [settingsForm] = Form.useForm(); 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(null); const [records, setRecords] = useState([]); const [users, setUsers] = useState([]); + const [mySettings, setMySettings] = useState({ 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 = [ { title: "屏保画面", @@ -614,12 +647,11 @@ export default function ScreenSaverManagement() { ), }, { - title: "播放与状态", + title: "排序与状态", key: "status", width: 210, render: (_, record) => ( - {record.displayDurationSec} 秒 / 张 排序值:{record.sortOrder ?? 0} void handleToggleStatus(record, checked)} /> @@ -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() { + @@ -758,16 +793,11 @@ export default function ScreenSaverManagement() { >
- + - - - - - {isAdmin ? ( @@ -887,13 +917,34 @@ export default function ScreenSaverManagement() { {(isAdmin || currentScopeType === "USER") ? ( - + ) : null} + setSettingsOpen(false)} + onOk={() => void handleSaveSettings()} + okText="保存" + confirmLoading={settingsSaving} + destroyOnHidden + > +
+ + + +
+
+ setCropState((prev) => ({ ...prev, open: false, src: "" }))}