Compare commits

...

8 Commits

Author SHA1 Message Date
d498f9f3cd fix(cms): 修复网站设置查询自愈和孤儿记录复活问题
- 去除无用的 LambdaQueryWrapper 导入,改用自定义 Mapper 方法查询
- 新增 CmsWebsiteSettingMapper 的 selectByWebsiteIdIgnoreLogic 方法,忽略逻辑删除过滤查询
- 新增 reviveById 方法,实现孤儿记录(deleted 非0或null)复活为正常状态
- 修改控制器 get 方法,优先使用自定义查询,避免因逻辑删除导致查询不到
- 对查询到的已删除或孤儿记录进行复活处理,保持数据唯一约束不冲突
- 新建记录时显式设置 deleted=0,保证逻辑删除查询正常返回
2026-08-04 23:54:07 +08:00
a2f432c70c fix(cms): 修复根据ID查询网站设置时未存在记录问题
- 查询网站设置记录时,若不存在则自动创建一条空记录
- 新建记录时显式设置 deleted 字段为 0,确保逻辑删除功能正常
- 便于前端传入 ID 后直接调用更新接口保存数据
- 保持查询接口返回一致的结构和数据格式
2026-08-04 21:03:16 +08:00
75fd0077cb feat(entity): 添加首页优势模块配置字段
- 在 CmsWebsiteSetting 实体中新增 features 字段
- features 字段用于存储首页优势模块配置信息,格式为 JSON
- 添加了对应的 Schema 注解及描述说明
2026-08-04 20:46:01 +08:00
4e20c2df62 fix(website): 防止同一租户重复创建站点,设置默认状态
- CmsWebsiteController中新增校验,若当前租户已有站点则拒绝新增
- 避免并发和重复请求导致多条孤儿记录产生
- CmsWebsiteServiceImpl中设置默认运行状态,防止状态字段为空显示异常
- 保持后端枚举status与前端展示running状态一致
- 调用方仍可覆盖默认状态值
2026-08-04 12:33:15 +08:00
4478f5f23c feat(cms): 添加虚拟阅读量展示与实际阅读量原子递增功能
- 在CmsArticle实体中新增views字段,表示实际阅读量与虚拟阅读量之和,供展示使用
- 实现views字段的getter方法,动态计算实际阅读量和虚拟阅读量的总和
- 修改CmsArticleServiceImpl中阅读量更新逻辑,采用SQL原子递增实际阅读量,防止并发覆盖
- 更新内存中article的实际阅读量值,保持与数据库同步
2026-08-04 11:13:08 +08:00
b007b17e8d fix(auth): 修正网站设置权限控制表达式
- 将保存权限由 cms:cmsWebsiteSetting:save 更新为 cms:website:save
- 将删除权限由 cms:cmsWebsiteSetting:remove 更新为 cms:website:remove
- 将批量保存权限由 cms:cmsWebsiteSetting:save 更新为 cms:website:save
- 将批量更新权限由 cms:cmsWebsiteSetting:update 更新为 cms:website:update
- 将批量删除权限由 cms:cmsWebsiteSetting:remove 更新为 cms:website:remove
2026-08-03 23:24:41 +08:00
f472933846 feat(entity): 新增网站图标属性
- 在 CmsWebsite 实体中添加 websiteAvatar 字段
- 字段描述为网站图标/正方形Logo,适用于后台应用卡、分享图标及浏览器标签页
- 保持现有网站LOGO字段不变,增强网站数据模型完整性
2026-08-03 20:35:26 +08:00
ff432b82c0 fix(cmsNavigation): 规范code字段并添加唯一性校验
- 规范化code字段,trim并转为小写以保证唯一性校验一致
- 添加栏目code租户内唯一性校验逻辑,避免重复冲突
- 保留空或空串code时不进行唯一性校验
- 修改与新增操作均调用唯一性校验函数
- 避免触发数据库唯一索引导致操作失败异常
2026-08-03 20:29:33 +08:00
9 changed files with 119 additions and 14 deletions

View File

@@ -104,8 +104,14 @@ public class CmsNavigationController extends BaseController {
} }
// 去除前面空格 // 去除前面空格
cmsNavigation.setTitle(StrUtil.trimStart(cmsNavigation.getTitle())); cmsNavigation.setTitle(StrUtil.trimStart(cmsNavigation.getTitle()));
// code 规范化trim + 转小写(唯一性以规范化后的值为准)
if (cmsNavigation.getCode() != null) {
cmsNavigation.setCode(cmsNavigation.getCode().trim().toLowerCase());
}
// 单页模型绑定唯一性校验:同一租户下同一个单页只能绑定一个栏目 // 单页模型绑定唯一性校验:同一租户下同一个单页只能绑定一个栏目
checkPageBindingUnique(cmsNavigation); checkPageBindingUnique(cmsNavigation);
// 栏目 code 租户内唯一性校验code 为空/空串时不校验)
checkCodeUnique(cmsNavigation);
if (cmsNavigationService.save(cmsNavigation)) { if (cmsNavigationService.save(cmsNavigation)) {
// 添加成功事务处理 // 添加成功事务处理
cmsNavigationService.saveAsync(cmsNavigation); cmsNavigationService.saveAsync(cmsNavigation);
@@ -119,8 +125,14 @@ public class CmsNavigationController extends BaseController {
@Operation(summary = "修改网站导航记录表") @Operation(summary = "修改网站导航记录表")
@PutMapping() @PutMapping()
public ApiResult<?> update(@RequestBody CmsNavigation cmsNavigation) { public ApiResult<?> update(@RequestBody CmsNavigation cmsNavigation) {
// code 规范化trim + 转小写(唯一性以规范化后的值为准)
if (cmsNavigation.getCode() != null) {
cmsNavigation.setCode(cmsNavigation.getCode().trim().toLowerCase());
}
// 单页模型绑定唯一性校验:同一租户下同一个单页只能绑定一个栏目 // 单页模型绑定唯一性校验:同一租户下同一个单页只能绑定一个栏目
checkPageBindingUnique(cmsNavigation); checkPageBindingUnique(cmsNavigation);
// 栏目 code 租户内唯一性校验code 为空/空串时不校验)
checkCodeUnique(cmsNavigation);
// 顶级栏目parentId=0的显示位置top/bottom发生变更时 // 顶级栏目parentId=0的显示位置top/bottom发生变更时
// 递归同步所有子级(含孙级)的 top/bottom保持层级一致。 // 递归同步所有子级(含孙级)的 top/bottom保持层级一致。
if (cmsNavigation.getParentId() != null && cmsNavigation.getParentId() == 0 if (cmsNavigation.getParentId() != null && cmsNavigation.getParentId() == 0
@@ -160,6 +172,27 @@ public class CmsNavigationController extends BaseController {
} }
@PreAuthorize("hasAuthority('cms:cmsNavigation:update')") @PreAuthorize("hasAuthority('cms:cmsNavigation:update')")
/**
* 校验栏目 code 租户内唯一:同一租户下 code 不可重复code 为空/空串时不校验)。
* 与单页绑定唯一性校验并列,避免触发 uk_navigation_tenant_code 唯一索引导致用户看到"操作失败"。
*/
private void checkCodeUnique(CmsNavigation cmsNavigation) {
String code = cmsNavigation.getCode();
if (code == null || code.trim().isEmpty()) {
return;
}
LambdaQueryWrapper<CmsNavigation> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(CmsNavigation::getTenantId, getTenantId())
.eq(CmsNavigation::getCode, code.trim())
.eq(CmsNavigation::getDeleted, 0);
if (cmsNavigation.getNavigationId() != null) {
wrapper.ne(CmsNavigation::getNavigationId, cmsNavigation.getNavigationId());
}
if (cmsNavigationService.count(wrapper) > 0) {
throw new BusinessException("栏目 code「" + code.trim() + "」已存在,请更换");
}
}
@Operation(summary = "修改栏目状态") @Operation(summary = "修改栏目状态")
@PutMapping("/status") @PutMapping("/status")
public ApiResult<?> updateStatus(@RequestBody Map<String, Object> body) { public ApiResult<?> updateStatus(@RequestBody Map<String, Object> body) {

View File

@@ -109,6 +109,18 @@ public class CmsWebsiteController extends BaseController {
// 记录当前登录用户id // 记录当前登录用户id
User loginUser = getLoginUser(); User loginUser = getLoginUser();
if (loginUser != null) { if (loginUser != null) {
// 防重复建站:同一租户已存在站点时拒绝新增,
// 避免并发请求/重复点击建出多条记录getSiteInfo 仅取 limit 1多余记录会成为孤儿数据
if (ObjectUtil.isNotEmpty(loginUser.getTenantId())) {
long exist = cmsWebsiteService.count(
new LambdaQueryWrapper<CmsWebsite>()
.eq(CmsWebsite::getTenantId, loginUser.getTenantId())
.eq(CmsWebsite::getDeleted, 0)
);
if (exist > 0) {
return fail("当前租户已存在站点,请勿重复创建");
}
}
cmsWebsite.setLoginUser(loginUser); cmsWebsite.setLoginUser(loginUser);
return success("创建成功", cmsWebsiteService.create(cmsWebsite)); return success("创建成功", cmsWebsiteService.create(cmsWebsite));
} }

View File

@@ -1,7 +1,6 @@
package com.gxwebsoft.cms.controller; package com.gxwebsoft.cms.controller;
import cn.hutool.core.util.ObjectUtil; import cn.hutool.core.util.ObjectUtil;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.gxwebsoft.common.core.web.BaseController; import com.gxwebsoft.common.core.web.BaseController;
import com.gxwebsoft.cms.service.CmsWebsiteSettingService; import com.gxwebsoft.cms.service.CmsWebsiteSettingService;
import com.gxwebsoft.cms.entity.CmsWebsiteSetting; import com.gxwebsoft.cms.entity.CmsWebsiteSetting;
@@ -9,6 +8,7 @@ import com.gxwebsoft.cms.param.CmsWebsiteSettingParam;
import com.gxwebsoft.common.core.web.ApiResult; import com.gxwebsoft.common.core.web.ApiResult;
import com.gxwebsoft.common.core.web.PageResult; import com.gxwebsoft.common.core.web.PageResult;
import com.gxwebsoft.common.core.web.BatchParam; import com.gxwebsoft.common.core.web.BatchParam;
import com.gxwebsoft.cms.mapper.CmsWebsiteSettingMapper;
import io.swagger.v3.oas.annotations.tags.Tag; import io.swagger.v3.oas.annotations.tags.Tag;
import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Operation;
import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.security.access.prepost.PreAuthorize;
@@ -30,6 +30,9 @@ public class CmsWebsiteSettingController extends BaseController {
@Resource @Resource
private CmsWebsiteSettingService cmsWebsiteSettingService; private CmsWebsiteSettingService cmsWebsiteSettingService;
@Resource
private CmsWebsiteSettingMapper cmsWebsiteSettingMapper;
@Operation(summary = "分页查询网站设置") @Operation(summary = "分页查询网站设置")
@GetMapping("/page") @GetMapping("/page")
public ApiResult<PageResult<CmsWebsiteSetting>> page(CmsWebsiteSettingParam param) { public ApiResult<PageResult<CmsWebsiteSetting>> page(CmsWebsiteSettingParam param) {
@@ -47,18 +50,26 @@ public class CmsWebsiteSettingController extends BaseController {
@Operation(summary = "根据id查询网站设置") @Operation(summary = "根据id查询网站设置")
@GetMapping("/{id}") @GetMapping("/{id}")
public ApiResult<CmsWebsiteSetting> get(@PathVariable("id") Integer id) { public ApiResult<CmsWebsiteSetting> get(@PathVariable("id") Integer id) {
// 使用关联查询 // 自定义查询忽略逻辑删除:能捞到 deleted IS NULL 的孤儿记录(旧逻辑遗留),
final CmsWebsiteSetting cmsWebsiteSetting = cmsWebsiteSettingService.getOne(new LambdaQueryWrapper<CmsWebsiteSetting>().eq(CmsWebsiteSetting::getWebsiteId, id)); // 避免“查不到→新建”时撞 website_id 唯一索引,导致前端报“未获取到网站设置记录”。
if(ObjectUtil.isEmpty(cmsWebsiteSetting)){ final CmsWebsiteSetting existing = cmsWebsiteSettingMapper.selectByWebsiteIdIgnoreLogic(id);
if (ObjectUtil.isEmpty(existing)) {
// 真正没有记录才新建,并显式 setDeleted(0),避免被 @TableLogic 查询过滤。
final CmsWebsiteSetting setting = new CmsWebsiteSetting(); final CmsWebsiteSetting setting = new CmsWebsiteSetting();
setting.setWebsiteId(id); setting.setWebsiteId(id);
setting.setDeleted(0);
cmsWebsiteSettingService.save(setting); cmsWebsiteSettingService.save(setting);
return success(cmsWebsiteSettingService.getOne(new LambdaQueryWrapper<CmsWebsiteSetting>().eq(CmsWebsiteSetting::getWebsiteId, id))); return success(setting);
} }
return success(cmsWebsiteSetting); // 查到的是孤儿/已删除记录复活为正常态deleted=0后返回不再重复插入。
if (existing.getDeleted() == null || existing.getDeleted() != 0) {
cmsWebsiteSettingMapper.reviveById(existing.getId());
existing.setDeleted(0);
}
return success(existing);
} }
@PreAuthorize("hasAuthority('cms:cmsWebsiteSetting:save')") @PreAuthorize("hasAuthority('cms:website:save')")
@Operation(summary = "添加网站设置") @Operation(summary = "添加网站设置")
@PostMapping() @PostMapping()
public ApiResult<?> save(@RequestBody CmsWebsiteSetting cmsWebsiteSetting) { public ApiResult<?> save(@RequestBody CmsWebsiteSetting cmsWebsiteSetting) {
@@ -78,7 +89,7 @@ public class CmsWebsiteSettingController extends BaseController {
return fail("修改失败"); return fail("修改失败");
} }
@PreAuthorize("hasAuthority('cms:cmsWebsiteSetting:remove')") @PreAuthorize("hasAuthority('cms:website:remove')")
@Operation(summary = "删除网站设置") @Operation(summary = "删除网站设置")
@DeleteMapping("/{id}") @DeleteMapping("/{id}")
public ApiResult<?> remove(@PathVariable("id") Integer id) { public ApiResult<?> remove(@PathVariable("id") Integer id) {
@@ -88,7 +99,7 @@ public class CmsWebsiteSettingController extends BaseController {
return fail("删除失败"); return fail("删除失败");
} }
@PreAuthorize("hasAuthority('cms:cmsWebsiteSetting:save')") @PreAuthorize("hasAuthority('cms:website:save')")
@Operation(summary = "批量添加网站设置") @Operation(summary = "批量添加网站设置")
@PostMapping("/batch") @PostMapping("/batch")
public ApiResult<?> saveBatch(@RequestBody List<CmsWebsiteSetting> list) { public ApiResult<?> saveBatch(@RequestBody List<CmsWebsiteSetting> list) {
@@ -98,7 +109,7 @@ public class CmsWebsiteSettingController extends BaseController {
return fail("添加失败"); return fail("添加失败");
} }
@PreAuthorize("hasAuthority('cms:cmsWebsiteSetting:update')") @PreAuthorize("hasAuthority('cms:website:update')")
@Operation(summary = "批量修改网站设置") @Operation(summary = "批量修改网站设置")
@PutMapping("/batch") @PutMapping("/batch")
public ApiResult<?> removeBatch(@RequestBody BatchParam<CmsWebsiteSetting> batchParam) { public ApiResult<?> removeBatch(@RequestBody BatchParam<CmsWebsiteSetting> batchParam) {
@@ -108,7 +119,7 @@ public class CmsWebsiteSettingController extends BaseController {
return fail("修改失败"); return fail("修改失败");
} }
@PreAuthorize("hasAuthority('cms:cmsWebsiteSetting:remove')") @PreAuthorize("hasAuthority('cms:website:remove')")
@Operation(summary = "批量删除网站设置") @Operation(summary = "批量删除网站设置")
@DeleteMapping("/batch") @DeleteMapping("/batch")
public ApiResult<?> removeBatch(@RequestBody List<Integer> ids) { public ApiResult<?> removeBatch(@RequestBody List<Integer> ids) {

View File

@@ -109,6 +109,16 @@ public class CmsArticle implements Serializable {
@Schema(description = "实际阅读量") @Schema(description = "实际阅读量")
private Integer actualViews; private Integer actualViews;
@Schema(description = "阅读量(实际阅读量 + 虚拟阅读量,仅用于展示)")
@TableField(exist = false)
private Integer views;
public Integer getViews() {
int actual = actualViews == null ? 0 : actualViews;
int virtual = virtualViews == null ? 0 : virtualViews;
return actual + virtual;
}
@Schema(description = "评分") @Schema(description = "评分")
private BigDecimal rate; private BigDecimal rate;

View File

@@ -52,6 +52,9 @@ public class CmsWebsite implements Serializable {
@Schema(description = "网站LOGO(深色模式)") @Schema(description = "网站LOGO(深色模式)")
private String websiteDarkLogo; private String websiteDarkLogo;
@Schema(description = "网站图标/正方形Logo(后台应用卡/分享图标/浏览器标签页图标)")
private String websiteAvatar;
@Schema(description = "网站类型") @Schema(description = "网站类型")
private String websiteType; private String websiteType;

View File

@@ -68,6 +68,9 @@ public class CmsWebsiteSetting implements Serializable {
@Schema(description = "导航栏最多显示数量") @Schema(description = "导航栏最多显示数量")
private Boolean maxMenuNum; private Boolean maxMenuNum;
@Schema(description = "首页优势模块配置(JSON)")
private String features;
@Schema(description = "排序号") @Schema(description = "排序号")
private Integer sortNumber; private Integer sortNumber;

View File

@@ -5,6 +5,8 @@ import com.baomidou.mybatisplus.core.metadata.IPage;
import com.gxwebsoft.cms.entity.CmsWebsiteSetting; import com.gxwebsoft.cms.entity.CmsWebsiteSetting;
import com.gxwebsoft.cms.param.CmsWebsiteSettingParam; import com.gxwebsoft.cms.param.CmsWebsiteSettingParam;
import org.apache.ibatis.annotations.Param; import org.apache.ibatis.annotations.Param;
import org.apache.ibatis.annotations.Select;
import org.apache.ibatis.annotations.Update;
import java.util.List; import java.util.List;
@@ -34,4 +36,20 @@ public interface CmsWebsiteSettingMapper extends BaseMapper<CmsWebsiteSetting> {
*/ */
List<CmsWebsiteSetting> selectListRel(@Param("param") CmsWebsiteSettingParam param); List<CmsWebsiteSetting> selectListRel(@Param("param") CmsWebsiteSettingParam param);
/**
* 忽略逻辑删除,按 websiteId 查最新一条记录。
* 用于「根据id查询」自愈能捞到 deleted IS NULL 的孤儿记录(旧逻辑遗留),
* 避免 MP 自带 getOne 因补 deleted=0 而查不到、继而新建时撞 website_id 唯一索引。
* 注意:自定义 @Select 不被 @TableLogic 自动追加 deleted 过滤(与本项目 XML 自定义查询一致)。
*/
@Select("SELECT * FROM cms_website_setting WHERE website_id = #{websiteId} ORDER BY id ASC LIMIT 1")
CmsWebsiteSetting selectByWebsiteIdIgnoreLogic(@Param("websiteId") Integer websiteId);
/**
* 将指定记录复活为正常态deleted=0
* 自定义 @Update 不受 @TableLogic 改写,可修正孤儿记录而不被 WHERE deleted=0 拦截。
*/
@Update("UPDATE cms_website_setting SET deleted = 0 WHERE id = #{id}")
void reviveById(@Param("id") Integer id);
} }

View File

@@ -112,9 +112,15 @@ public class CmsArticleServiceImpl extends ServiceImpl<CmsArticleMapper, CmsArti
param.setArticleId(articleId); param.setArticleId(articleId);
final CmsArticle article = param.getOne(baseMapper.selectListRel(param)); final CmsArticle article = param.getOne(baseMapper.selectListRel(param));
if (ObjectUtil.isNotEmpty(article)) { if (ObjectUtil.isNotEmpty(article)) {
// 更新阅读数量 // 原子递增实际阅读量COALESCE 兼容历史空值,避免并发覆盖)
article.setActualViews(article.getActualViews() + 1); boolean viewUpdated = lambdaUpdate()
updateById(article); .setSql("actual_views = COALESCE(actual_views, 0) + 1")
.eq(CmsArticle::getArticleId, article.getArticleId())
.update();
if (viewUpdated) {
int current = article.getActualViews() == null ? 0 : article.getActualViews();
article.setActualViews(current + 1);
}
// 读取Banner // 读取Banner
// final CmsModel model = cmsModelService.getOne(new LambdaQueryWrapper<CmsModel>().eq(CmsModel::getModel, article.getModel()).last("limit 1")); // final CmsModel model = cmsModelService.getOne(new LambdaQueryWrapper<CmsModel>().eq(CmsModel::getModel, article.getModel()).last("limit 1"));
// if (ObjectUtil.isNotEmpty(model)) { // if (ObjectUtil.isNotEmpty(model)) {

View File

@@ -193,6 +193,15 @@ public class CmsWebsiteServiceImpl extends ServiceImpl<CmsWebsiteMapper, CmsWebs
website.setTemplateId(loginUser.getTemplateId()); website.setTemplateId(loginUser.getTemplateId());
website.setCompanyId(loginUser.getCompanyId()); website.setCompanyId(loginUser.getCompanyId());
// 默认运行中status后台枚举与 runninggetSiteInfo 状态展示用)保持一致,
// 避免公开站读取到空值显示「状态未知」;调用方可覆盖
if (website.getStatus() == null) {
website.setStatus(1);
}
if (website.getRunning() == null) {
website.setRunning(1);
}
// 初始化数据 // 初始化数据
if(save(website)){ if(save(website)){
// 插入网站设置记录 // 插入网站设置记录