diff --git a/fastbee-admin/src/main/java/com/fastbee/web/controller/system/SysUserSettingController.java b/fastbee-admin/src/main/java/com/fastbee/web/controller/system/SysUserSettingController.java index f2dd1edf..e4f4b7eb 100644 --- a/fastbee-admin/src/main/java/com/fastbee/web/controller/system/SysUserSettingController.java +++ b/fastbee-admin/src/main/java/com/fastbee/web/controller/system/SysUserSettingController.java @@ -1,12 +1,10 @@ package com.fastbee.web.controller.system; import com.fastbee.common.annotation.Log; -import com.fastbee.common.constant.UserSettingConstants; import com.fastbee.common.core.controller.BaseController; import com.fastbee.common.core.domain.AjaxResult; import com.fastbee.common.enums.BusinessType; import com.fastbee.common.utils.SecurityUtils; -import com.fastbee.common.utils.StringUtils; import com.fastbee.system.domain.LoginRedirectOption; import com.fastbee.system.domain.SysUserSetting; import com.fastbee.system.service.ISysUserSettingService; @@ -15,8 +13,8 @@ import io.swagger.annotations.ApiOperation; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.web.bind.annotation.DeleteMapping; import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PatchMapping; import org.springframework.web.bind.annotation.PostMapping; -import org.springframework.web.bind.annotation.PutMapping; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestParam; @@ -80,25 +78,30 @@ public class SysUserSettingController extends BaseController { return AjaxResult.success(); } - @ApiOperation("保存当前用户的设置") + /** + * 部分更新:只提交、也只写「用户改动过的那几项」 + * + *
刻意不是整体覆盖写(PUT):设置页有多个互相独立的配置项, + * 整体覆盖会让「保存 A 配置」把 B 配置冲回默认值。 + * 这里按入参中非 null 的字段逐列更新,未传的字段在 SQL 层面就碰不到。 + *
用 PATCH 而非 PUT:PUT 的语义是整体替换,与部分更新名实不符。 + */ + @ApiOperation("部分更新当前用户的设置") @Log(title = "个人设置", businessType = BusinessType.UPDATE) - @PutMapping() - public AjaxResult save(@RequestBody SysUserSetting setting) { - Long userId = SecurityUtils.getUserId(); - String redirect = setting.getLoginRedirect(); - if (StringUtils.isEmpty(redirect)) { - // 空值不覆盖,交由默认值兜底,避免把用户显式配置冲掉 - redirect = UserSettingConstants.DEFAULT_LOGIN_REDIRECT; + @PatchMapping() + public AjaxResult update(@RequestBody SysUserSetting setting) { + if (setting.getLoginRedirect() == null + && setting.getQueryGatewayChild() == null + && setting.getShowChild() == null) { + return AjaxResult.error("没有需要保存的配置项"); } - if (!isValidLoginRedirect(redirect)) { + // 只有在本次确实要改「登录后显示页面」时才做白名单校验; + // 未传(null)表示本次不改它,不能顺手用默认值把它覆盖掉 + if (setting.getLoginRedirect() != null && !isValidLoginRedirect(setting.getLoginRedirect())) { return AjaxResult.error("登录后跳转页面不合法,请从系统菜单中选择"); } - setting.setLoginRedirect(redirect); // 审计字段由服务端填充,不接受前端传入,避免越权写入他人操作名 - String username = SecurityUtils.getUsername(); - setting.setCreateBy(username); - setting.setUpdateBy(username); - userSettingService.save(userId, setting); + userSettingService.updatePartial(SecurityUtils.getUserId(), setting, SecurityUtils.getUsername()); return AjaxResult.success(); } diff --git a/fastbee-common/src/main/java/com/fastbee/common/constant/UserSettingConstants.java b/fastbee-common/src/main/java/com/fastbee/common/constant/UserSettingConstants.java index d7675767..5bc2c663 100644 --- a/fastbee-common/src/main/java/com/fastbee/common/constant/UserSettingConstants.java +++ b/fastbee-common/src/main/java/com/fastbee/common/constant/UserSettingConstants.java @@ -69,6 +69,17 @@ public class UserSettingConstants { */ public static final boolean DEFAULT_QUERY_GATEWAY_CHILD = false; + /** + * 设备管理列表是否包含下级机构数据的默认值 + * + *
语义对应 {@code Device.showChild}:仅在机构用户({@code deptId} 非空)时参与 SQL, + * 开启时 {@code tenant_id} 落在「本机构及全部下级机构用户」集合内,关闭时仅本机构。 + * 原实现是设备管理页的勾选框 + 浏览器 localStorage(key {@code device-showChild}), + * 换浏览器/换电脑就丢,现改为落库、由「系统管理 → 系统设置」页维护。 + *
默认 false = 与引入本开关前完全一致(默认列表不含下级机构数据)。 + */ + public static final boolean DEFAULT_SHOW_CHILD = false; + private UserSettingConstants() { } diff --git a/fastbee-service/fastbee-system-service/src/main/java/com/fastbee/system/domain/SysUserSetting.java b/fastbee-service/fastbee-system-service/src/main/java/com/fastbee/system/domain/SysUserSetting.java index 09843d7b..0bad61af 100644 --- a/fastbee-service/fastbee-system-service/src/main/java/com/fastbee/system/domain/SysUserSetting.java +++ b/fastbee-service/fastbee-system-service/src/main/java/com/fastbee/system/domain/SysUserSetting.java @@ -60,6 +60,17 @@ public class SysUserSetting extends BaseEntity { @ApiModelProperty(value = "是否查询网关子设备(按编号查时换算网关并放开子设备类产品过滤)") private Boolean queryGatewayChild; + /** + * 设备管理列表是否包含下级机构数据 + * + *
对应 {@code Device.showChild}:仅在机构用户(deptId 非空)时参与 SQL, + * 开启时 {@code tenant_id} 落在「本机构及全部下级机构用户」集合内,关闭时仅本机构。 + * 原为设备管理页勾选框 + 浏览器 localStorage,现改为落库、由系统设置页维护; + * 设备管理页只读取该值作为查询参数下发,页面内不再可就地修改。 + */ + @ApiModelProperty(value = "设备管理列表是否包含下级机构数据") + private Boolean showChild; + public SysUserSetting() { } @@ -75,6 +86,7 @@ public class SysUserSetting extends BaseEntity { setting.setLoginRedirect(UserSettingConstants.DEFAULT_LOGIN_REDIRECT); setting.setPinnedTags(UserSettingConstants.EMPTY_PINNED_TAGS); setting.setQueryGatewayChild(UserSettingConstants.DEFAULT_QUERY_GATEWAY_CHILD); + setting.setShowChild(UserSettingConstants.DEFAULT_SHOW_CHILD); return setting; } @@ -118,6 +130,14 @@ public class SysUserSetting extends BaseEntity { this.queryGatewayChild = queryGatewayChild; } + public Boolean getShowChild() { + return showChild; + } + + public void setShowChild(Boolean showChild) { + this.showChild = showChild; + } + @Override public String toString() { return new ToStringBuilder(this, ToStringStyle.MULTI_LINE_STYLE) @@ -126,6 +146,7 @@ public class SysUserSetting extends BaseEntity { .append("loginRedirect", getLoginRedirect()) .append("pinnedTags", getPinnedTags()) .append("queryGatewayChild", getQueryGatewayChild()) + .append("showChild", getShowChild()) .append("createBy", getCreateBy()) .append("createTime", getCreateTime()) .append("updateBy", getUpdateBy()) diff --git a/fastbee-service/fastbee-system-service/src/main/java/com/fastbee/system/mapper/SysUserSettingMapper.java b/fastbee-service/fastbee-system-service/src/main/java/com/fastbee/system/mapper/SysUserSettingMapper.java index 4bc6ef2b..6af3d64c 100644 --- a/fastbee-service/fastbee-system-service/src/main/java/com/fastbee/system/mapper/SysUserSettingMapper.java +++ b/fastbee-service/fastbee-system-service/src/main/java/com/fastbee/system/mapper/SysUserSettingMapper.java @@ -23,12 +23,16 @@ public interface SysUserSettingMapper { SysUserSetting selectByUserId(Long userId); /** - * 保存设置(存在则更新,不存在则插入) + * 部分更新设置:只更新入参中**非 null** 的列,未传的列保持库中原值 * - * @param setting 设置 + *
调用方先 {@link #insertDefaultIfAbsent} 保证行存在,否则 UPDATE 0 行会静默丢改动。 + * 刻意不提供「整体覆盖写」的入口:设置页每次只提交用户改动过的那几项, + * 整体覆盖会让「保存 A 配置」把 B 配置冲回默认值。 + * + * @param setting 设置,userId 必填,其余字段只填本次要改的 * @return 影响行数 */ - int upsert(SysUserSetting setting); + int updateSetting(SysUserSetting setting); /** * 仅更新固定标签列(整列覆盖) @@ -46,12 +50,13 @@ public interface SysUserSettingMapper { * 确保用户设置行存在:不存在则插入默认行,已存在则不做任何修改 * *
固定标签的增量写入要在一行上做「锁定读 → 改 → 写」,行必须先存在。 - * 刻意不用 {@link #upsert}:upsert 的 ON DUPLICATE 子句会覆盖 - * login_redirect / query_gateway_child,把用户已保存的设置冲回默认值。 + * 刻意不用 ON DUPLICATE 写其它列:那会覆盖 + * login_redirect / query_gateway_child / show_child,把用户已保存的设置冲回默认值。 * * @param userId 用户ID * @param loginRedirect 新行使用的默认登录跳转 * @param queryGatewayChild 新行使用的默认开关值 + * @param showChild 新行使用的默认「包含下级机构数据」值 * @param operator 操作人用户名 * @param now 当前时间 * @return 影响行数(插入 1 / 已存在 0) @@ -59,6 +64,7 @@ public interface SysUserSettingMapper { int insertDefaultIfAbsent(@Param("userId") Long userId, @Param("loginRedirect") String loginRedirect, @Param("queryGatewayChild") Boolean queryGatewayChild, + @Param("showChild") Boolean showChild, @Param("operator") String operator, @Param("now") Date now); diff --git a/fastbee-service/fastbee-system-service/src/main/java/com/fastbee/system/service/ISysUserSettingService.java b/fastbee-service/fastbee-system-service/src/main/java/com/fastbee/system/service/ISysUserSettingService.java index b03426ec..a676e9c1 100644 --- a/fastbee-service/fastbee-system-service/src/main/java/com/fastbee/system/service/ISysUserSettingService.java +++ b/fastbee-service/fastbee-system-service/src/main/java/com/fastbee/system/service/ISysUserSettingService.java @@ -25,13 +25,17 @@ public interface ISysUserSettingService { SysUserSetting selectByUserId(Long userId); /** - * 保存用户设置(存在则更新,不存在则插入) + * 部分更新用户设置:只写 {@code patch} 中非 null 的字段,未传的字段保持库中原值 * - * @param userId 用户ID - * @param setting 设置内容,userId 字段忽略,以入参 userId 为准 + *
调用方每次只提交「用户真正改动过」的配置项,因此保存某一项不会把其它项冲成默认值。 + * 用户还没有设置行时先落一行默认值再更新,避免 UPDATE 0 行静默丢改动。 + * + * @param userId 用户ID + * @param patch 只填本次要改的字段,userId 字段忽略,以入参 userId 为准 + * @param operator 操作人用户名,写入 update_by * @return 影响行数 */ - int save(Long userId, SysUserSetting setting); + int updatePartial(Long userId, SysUserSetting patch, String operator); /** * 查询登录落地页候选项 diff --git a/fastbee-service/fastbee-system-service/src/main/java/com/fastbee/system/service/impl/SysUserSettingServiceImpl.java b/fastbee-service/fastbee-system-service/src/main/java/com/fastbee/system/service/impl/SysUserSettingServiceImpl.java index 35b519cd..c11f370a 100644 --- a/fastbee-service/fastbee-system-service/src/main/java/com/fastbee/system/service/impl/SysUserSettingServiceImpl.java +++ b/fastbee-service/fastbee-system-service/src/main/java/com/fastbee/system/service/impl/SysUserSettingServiceImpl.java @@ -37,7 +37,9 @@ public class SysUserSettingServiceImpl implements ISysUserSettingService { * *
用 \n 而不是常见的逗号/竖线:loginRedirect 是路由 path 不含换行, * pinnedTags 是 JSON 数组(元素为带引号的字符串)也不含裸换行, - * queryGatewayChild 是布尔字面量,三段互不冲突。 + * showChild 与 queryGatewayChild 是布尔字面量,四段互不冲突。 + *
字段顺序固定为:loginRedirect | pinnedTags | showChild | queryGatewayChild, + * 与 {@link #parseCached} 一一对应,增删字段必须同步改这两处。 */ private static final String CACHE_SEPARATOR = "\n"; @@ -80,15 +82,17 @@ public class SysUserSettingServiceImpl implements ISysUserSettingService { redisCache.setStrCacheObject(cacheKey, setting.getLoginRedirect() + CACHE_SEPARATOR + setting.getPinnedTags() + CACHE_SEPARATOR + + setting.getShowChild() + CACHE_SEPARATOR + setting.getQueryGatewayChild()); } /** * 解析缓存值,格式非法时返回 null 走查库 * - *
段数必须严格等于 3:旧版本只缓存了 2 段(loginRedirect + pinnedTags), - * 若按缺省值补齐会读到错误的 queryGatewayChild,故旧格式一律判为未命中, - * 由查库回填新格式。缓存无 TTL,但每个用户最多回填一次。 + *
段数必须严格等于 4:历史版本只缓存了 2 段(loginRedirect + pinnedTags)、
+ * 后又扩到 3 段(追加 queryGatewayChild)。若按缺省值补齐,会把旧格式里
+ * 那个位置的字段当成 showChild 读出来,故段数不等一律判为未命中,由查库回填新格式。
+ * 缓存无 TTL,但每个用户最多回填一次。
*
* @param cached 缓存字符串
* @param userId 用户ID
@@ -96,38 +100,33 @@ public class SysUserSettingServiceImpl implements ISysUserSettingService {
*/
private SysUserSetting parseCached(String cached, Long userId) {
String[] parts = cached.split(CACHE_SEPARATOR, -1);
- if (parts.length != 3) {
+ if (parts.length != 4) {
return null;
}
SysUserSetting setting = SysUserSetting.defaultOf(userId);
setting.setLoginRedirect(parts[0]);
setting.setPinnedTags(parts[1]);
- setting.setQueryGatewayChild(Boolean.parseBoolean(parts[2]));
+ setting.setShowChild(Boolean.parseBoolean(parts[2]));
+ setting.setQueryGatewayChild(Boolean.parseBoolean(parts[3]));
return setting;
}
@Override
@DS("master")
- public int save(Long userId, SysUserSetting setting) {
- SysUserSetting target = SysUserSetting.defaultOf(userId);
- target.setLoginRedirect(setting.getLoginRedirect());
- // 未传时落到默认值:列约束为 NOT NULL,直接写 null 会在 STRICT 模式下报错
- target.setQueryGatewayChild(setting.getQueryGatewayChild() == null
- ? UserSettingConstants.DEFAULT_QUERY_GATEWAY_CHILD
- : setting.getQueryGatewayChild());
- target.setRemark(setting.getRemark());
+ @Transactional(rollbackFor = Exception.class)
+ public int updatePartial(Long userId, SysUserSetting patch, String operator) {
Date now = new Date();
- target.setUpdateBy(setting.getUpdateBy());
- target.setUpdateTime(now);
- // create_by/create_time 仅首次写入,由 ON DUPLICATE KEY UPDATE 的列清单保证不被覆盖
- target.setCreateBy(setting.getUpdateBy());
- target.setCreateTime(now);
-
- int rows = userSettingMapper.upsert(target);
- // 写库成功后立即刷缓存。必须重查库再回填:upsert 的 ON DUPLICATE 子句不更新
- // pinned_tags,而内存对象 target 的 pinnedTags 是默认空数组,
- // 直接回填会把用户的固定标签"洗"成空。
- refreshCache(userId, target);
+ // 行必须先存在,否则后面的部分更新会 UPDATE 0 行、静默丢掉本次改动
+ userSettingMapper.insertDefaultIfAbsent(userId, UserSettingConstants.DEFAULT_LOGIN_REDIRECT,
+ UserSettingConstants.DEFAULT_QUERY_GATEWAY_CHILD, UserSettingConstants.DEFAULT_SHOW_CHILD,
+ operator, now);
+ patch.setUserId(userId);
+ patch.setUpdateBy(operator);
+ patch.setUpdateTime(now);
+ int rows = userSettingMapper.updateSetting(patch);
+ // 写库成功后重查库再回填缓存:patch 里只有本次改动的字段,
+ // 直接拿它刷缓存会把未改动字段写成 null。
+ refreshCache(userId, patch);
return rows;
}
@@ -141,7 +140,8 @@ public class SysUserSettingServiceImpl implements ISysUserSettingService {
Date now = new Date();
// 行必须先存在,后面的锁定读才有东西可锁
userSettingMapper.insertDefaultIfAbsent(userId, UserSettingConstants.DEFAULT_LOGIN_REDIRECT,
- UserSettingConstants.DEFAULT_QUERY_GATEWAY_CHILD, operator, now);
+ UserSettingConstants.DEFAULT_QUERY_GATEWAY_CHILD, UserSettingConstants.DEFAULT_SHOW_CHILD,
+ operator, now);
List