Compare commits

...

13 Commits

Author SHA1 Message Date
otto 0cbd73f03b Merge pull request '通过定时任务定时统计专区数据,优化统计接口加载较慢的问题' (#13) from otto/microservices:ccf_tmp into ccf_tmp 2026-07-29 16:03:53 +08:00
otto 73b42af29f Merge branch 'master' of https://gitlink.org.cn/Gitlink/microservices into ccf_tmp 2026-07-29 16:03:03 +08:00
otto 5dbe659c51 Merge pull request '修复获取热门文章时搜索条件中不存在publishYear条件导致查询失败' (#11) from otto/microservices:ccf_tmp into ccf_tmp 2026-07-29 14:48:12 +08:00
otto a4f8a37a59 fix(特色专区): 文章打卡功能开发
修复获取热门文章时搜索条件中不存在publishYear条件导致查询失败
2026-07-29 14:47:47 +08:00
otto f3f1061250 Merge pull request '文章打卡功能开发以及基于字典改造专区支持通过字典快速拓展属性' (#10) from otto/microservices:ccf_tmp into ccf_tmp 2026-07-29 13:52:41 +08:00
otto db32b93ae7 Merge branch 'master' of https://gitlink.org.cn/Gitlink/microservices into ccf_tmp 2026-07-29 13:49:26 +08:00
otto 9603d42295 feat(特色专区): 文章打卡功能开发
校验上传照片是否存在,校验文章是否存在
2026-07-29 13:48:39 +08:00
otto 24b5f3ea5d feat(特色专区): 文章打卡功能开发
校验上传照片是否存在,校验文章是否存在
2026-07-29 13:48:12 +08:00
otto a1dd6c7545 feat(特色专区): 文章打卡功能开发 2026-07-29 10:51:33 +08:00
otto 92610d6cd9 feat(特色专区): 专区资讯文章列表查询新增按发布时间筛选能力 2026-07-29 09:09:06 +08:00
otto 699fb21398 feat(特色专区): 基于字典改造专区,支持通过字典快速拓展属性
优化基于字典的文章筛选逻辑
2026-07-28 15:55:02 +08:00
otto 99d9e46835 feat(特色专区): 文章字典属性按需返回功能开发
为解决文章关联的字典属性需前端逐篇单独查询、体验差且性能低的问题,在文章列表和详情查询接口新增按需返回字典关联属性能力,前端传参控制是否加载,默认不加载。
2026-07-28 14:39:46 +08:00
otto 9eb885c7b9 feat(特色专区): 基于字典改造专区,支持通过字典快速拓展属性 2026-07-28 09:25:35 +08:00
34 changed files with 2018 additions and 20 deletions

View File

@ -5,6 +5,8 @@ import com.microservices.common.core.constant.ServiceNameConstants;
import com.microservices.common.core.domain.R;
import com.microservices.system.api.domain.SysDept;
import com.microservices.system.api.factory.RemoteZoneFallbackFactory;
import com.alibaba.fastjson2.JSONArray;
import com.alibaba.fastjson2.JSONObject;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.*;
@ -121,4 +123,31 @@ public interface RemoteZoneService {
public R<Boolean> lockCmsByDeptId(@PathVariable(value = "deptId") Long deptId
, @PathVariable(value = "lockStatus") Integer lockStatus
, @RequestHeader(SecurityConstants.FROM_SOURCE) String source);
/**
* 根据多个字典条件查询文章主键列表AND 逻辑
*
* @param objectType 对象类型CMS_DOC
* @param dictFiltersJson 字典筛选条件JSON数组
* @param source 来源
* @return 文章主键列表
*/
@PostMapping("/objectDict/objectIdsByFilters")
R<List<Long>> getArticleIdsByDictFilters(@RequestParam("objectType") String objectType,
@RequestBody JSONArray dictFiltersJson,
@RequestHeader(SecurityConstants.FROM_SOURCE) String source);
/**
* 批量查询多个对象的字典属性(含标签解析)
* 返回 List<JSONObject>每条含 objectId/dictType/dictCode/dictLabel/dictValue
*
* @param objectType 对象类型( ARTICLE)
* @param objectIds 对象主键数组
* @param source 来源
* @return 字典属性列表
*/
@PostMapping("/objectDict/dictsByObjectIds")
R<List<JSONObject>> getDictsByObjectIds(@RequestParam("objectType") String objectType,
@RequestBody Long[] objectIds,
@RequestHeader(SecurityConstants.FROM_SOURCE) String source);
}

View File

@ -3,6 +3,8 @@ package com.microservices.system.api.factory;
import com.microservices.common.core.domain.R;
import com.microservices.system.api.RemoteZoneService;
import com.microservices.system.api.domain.SysDept;
import com.alibaba.fastjson2.JSONArray;
import com.alibaba.fastjson2.JSONObject;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.cloud.openfeign.FallbackFactory;
@ -68,6 +70,16 @@ public class RemoteZoneFallbackFactory implements FallbackFactory<RemoteZoneServ
public R<Boolean> lockCmsByDeptId(Long deptId, Integer lockStatus, String source) {
return R.fail("通过组织id锁定/解锁专区资讯失败:" + throwable.getMessage());
}
@Override
public R<List<Long>> getArticleIdsByDictFilters(String objectType, JSONArray dictFiltersJson, String source) {
return R.fail("根据字典条件查询文章ID失败:" + throwable.getMessage());
}
@Override
public R<List<JSONObject>> getDictsByObjectIds(String objectType, Long[] objectIds, String source) {
return R.fail("批量查询对象字典属性失败:" + throwable.getMessage());
}
};
}
}

View File

@ -0,0 +1,64 @@
package com.microservices.cms.checkin.controller;
import com.microservices.cms.checkin.domain.CmsDocCheckinInputDto;
import com.microservices.cms.checkin.domain.vo.CmsDocCheckinQueryVo;
import com.microservices.cms.checkin.domain.vo.CmsDocCheckinVo;
import com.microservices.cms.checkin.service.ICmsDocCheckinService;
import com.microservices.common.core.web.controller.BaseController;
import com.microservices.common.core.web.domain.AjaxResult;
import com.microservices.common.core.web.page.TableDataInfo;
import com.microservices.common.log.annotation.Log;
import com.microservices.common.log.enums.BusinessType;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import io.swagger.annotations.ApiParam;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
import java.util.Arrays;
import java.util.List;
/**
* 文章打卡Controller
*
* @author otto
*/
@Api(tags = "文章打卡相关接口")
@RestController
@RequestMapping("/docCheckin")
public class CmsDocCheckinController extends BaseController {
@Autowired
private ICmsDocCheckinService cmsDocCheckinService;
/**
* 文章打卡(一人一文一次,重复打卡将被拦截)
*/
@ApiOperation("文章打卡")
@Log(title = "文章打卡", businessType = BusinessType.INSERT)
@PostMapping("/{docId}")
public AjaxResult checkin(@ApiParam(name = "docId", value = "文章Id", required = true) @PathVariable("docId") Long docId,
@RequestBody(required = false) CmsDocCheckinInputDto dto) {
return AjaxResult.success(cmsDocCheckinService.checkin(docId, dto));
}
/**
* 查询本人打卡记录(分页)
*/
@ApiOperation("查询本人打卡记录")
@GetMapping("/myList")
public TableDataInfo myList(CmsDocCheckinQueryVo query) {
startPage();
List<CmsDocCheckinVo> list = cmsDocCheckinService.selectMyCheckinList(query);
return getDataTable(list);
}
/**
* 按文章ID列表批量查询当前用户的打卡记录(用于标记"我已打卡")
*/
@ApiOperation("按文章ID列表批量查询当前用户已打卡文章")
@PostMapping("/byDocIds")
public AjaxResult byDocIds(@RequestBody Long[] docIds) {
return AjaxResult.success(cmsDocCheckinService.selectCheckinByDocIds(Arrays.asList(docIds)));
}
}

View File

@ -0,0 +1,39 @@
package com.microservices.cms.checkin.domain;
import com.fasterxml.jackson.annotation.JsonFormat;
import com.microservices.common.core.web.domain.BaseEntity;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import java.util.Date;
/**
* 文章打卡记录对象 cms_doc_checkin
*
* @author otto
*/
@Data
@ApiModel("文章打卡记录")
public class CmsDocCheckin extends BaseEntity {
private static final long serialVersionUID = 1L;
@ApiModelProperty("主键")
private Long id;
@ApiModelProperty("文章ID")
private Long docId;
@ApiModelProperty("打卡用户ID")
private Long userId;
@ApiModelProperty("照片文件标识(逗号分隔)")
private String fileIdentifier;
@ApiModelProperty("打卡备注")
private String notes;
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
@ApiModelProperty("打卡时间(服务端记录)")
private Date checkinTime;
}

View File

@ -0,0 +1,23 @@
package com.microservices.cms.checkin.domain;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import java.util.List;
/**
* 文章打卡输入对象
*
* @author otto
*/
@Data
@ApiModel("文章打卡输入对象")
public class CmsDocCheckinInputDto {
@ApiModelProperty("照片文件标识列表(选填)")
private List<String> fileIdentifier;
@ApiModelProperty("打卡备注(选填)")
private String notes;
}

View File

@ -0,0 +1,19 @@
package com.microservices.cms.checkin.domain.vo;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
/**
* 文章打卡记录查询条件(本人打卡记录分页查询)
*
* @author otto
*/
@Data
public class CmsDocCheckinQueryVo {
@ApiModelProperty("文章标题(模糊)")
private String docName;
@ApiModelProperty("打卡备注(模糊)")
private String notes;
}

View File

@ -0,0 +1,46 @@
package com.microservices.cms.checkin.domain.vo;
import com.fasterxml.jackson.annotation.JsonFormat;
import com.microservices.system.api.domain.SysFileInfo;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import java.util.Date;
import java.util.List;
/**
* 文章打卡展示对象
*
* @author otto
*/
@Data
@ApiModel("文章打卡展示对象")
public class CmsDocCheckinVo {
@ApiModelProperty("打卡记录主键")
private Long id;
@ApiModelProperty("文章ID")
private Long docId;
@ApiModelProperty("文章标题")
private String docName;
@ApiModelProperty("照片文件标识(逗号分隔)")
private String fileIdentifier;
@ApiModelProperty("文件信息列表(按 fileIdentifier 解析填充)")
private List<SysFileInfo> fileList;
@ApiModelProperty("打卡备注")
private String notes;
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
@ApiModelProperty("打卡时间")
private Date checkinTime;
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
@ApiModelProperty("打卡记录创建时间")
private Date createTime;
}

View File

@ -0,0 +1,36 @@
package com.microservices.cms.checkin.mapper;
import com.microservices.cms.checkin.domain.CmsDocCheckin;
import com.microservices.cms.checkin.domain.vo.CmsDocCheckinQueryVo;
import com.microservices.cms.checkin.domain.vo.CmsDocCheckinVo;
import org.apache.ibatis.annotations.Param;
import java.util.List;
/**
* 文章打卡记录Mapper接口
*
* @author otto
*/
public interface CmsDocCheckinMapper {
/**
* 唯一性检查查询某用户对某文章的打卡记录
*/
CmsDocCheckin selectByDocIdAndUserId(@Param("docId") Long docId, @Param("userId") Long userId);
/**
* 新增打卡记录
*/
int insertCmsDocCheckin(CmsDocCheckin cmsDocCheckin);
/**
* 查询本人打卡记录(JOIN cms_doc 取文章标题)
*/
List<CmsDocCheckinVo> selectMyList(@Param("userId") Long userId, @Param("query") CmsDocCheckinQueryVo query);
/**
* 按文章ID列表批量查询当前用户的打卡记录
*/
List<CmsDocCheckinVo> selectByDocIdsAndUserId(@Param("docIds") List<Long> docIds, @Param("userId") Long userId);
}

View File

@ -0,0 +1,40 @@
package com.microservices.cms.checkin.service;
import com.microservices.cms.checkin.domain.CmsDocCheckinInputDto;
import com.microservices.cms.checkin.domain.vo.CmsDocCheckinQueryVo;
import com.microservices.cms.checkin.domain.vo.CmsDocCheckinVo;
import java.util.List;
/**
* 文章打卡记录Service接口
*
* @author otto
*/
public interface ICmsDocCheckinService {
/**
* 文章打卡(一人一文一次,重复打卡抛异常)
*
* @param docId 文章ID
* @param dto 打卡入参(照片文件标识列表备注)
* @return 打卡展示对象
*/
CmsDocCheckinVo checkin(Long docId, CmsDocCheckinInputDto dto);
/**
* 查询本人打卡记录(JOIN 文章标题)
*
* @param query 查询条件(文章标题/备注模糊)
* @return 打卡记录列表
*/
List<CmsDocCheckinVo> selectMyCheckinList(CmsDocCheckinQueryVo query);
/**
* 按文章ID列表批量查询当前用户的打卡记录
*
* @param docIds 文章ID列表
* @return 当前用户已打卡的文章及打卡详情
*/
List<CmsDocCheckinVo> selectCheckinByDocIds(List<Long> docIds);
}

View File

@ -0,0 +1,180 @@
package com.microservices.cms.checkin.service.impl;
import com.microservices.cms.checkin.domain.CmsDocCheckin;
import com.microservices.cms.checkin.domain.CmsDocCheckinInputDto;
import com.microservices.cms.checkin.domain.vo.CmsDocCheckinQueryVo;
import com.microservices.cms.checkin.domain.vo.CmsDocCheckinVo;
import com.microservices.cms.checkin.mapper.CmsDocCheckinMapper;
import com.microservices.cms.checkin.service.ICmsDocCheckinService;
import com.microservices.cms.doc.service.ICmsDocService;
import com.microservices.common.core.exception.ServiceException;
import com.microservices.common.core.utils.DateUtils;
import com.microservices.common.core.utils.StringUtils;
import com.microservices.common.security.utils.SecurityUtils;
import com.microservices.system.api.RemoteFileService;
import com.microservices.system.api.domain.SysFileInfo;
import com.microservices.system.api.utils.FeignUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.stream.Collectors;
/**
* 文章打卡记录Service业务层处理
*
* @author otto
*/
@Service
public class CmsDocCheckinServiceImpl implements ICmsDocCheckinService {
private static final Logger logger = LoggerFactory.getLogger(CmsDocCheckinServiceImpl.class);
@Autowired
private CmsDocCheckinMapper cmsDocCheckinMapper;
@Autowired
private ICmsDocService cmsDocService;
@Autowired
private RemoteFileService remoteFileService;
@Override
public CmsDocCheckinVo checkin(Long docId, CmsDocCheckinInputDto dto) {
if (docId == null) {
throw new ServiceException("文章ID不能为空");
}
Long userId = SecurityUtils.getUserId();
if (userId == null) {
throw new ServiceException("未获取到登录用户信息,请重新登录");
}
// 校验文章存在(不存在或为栏目即抛异常)
cmsDocService.selectBaseCmsDocById(docId);
// 校验文件标识存在性(每个 fileIdentifier 必须对应真实文件)
List<String> identifiers = (dto != null) ? dto.getFileIdentifier() : null;
if (identifiers != null && !identifiers.isEmpty()) {
checkFileIdentifiersExist(identifiers);
}
// 唯一性检查:一人一文只能打卡一次
CmsDocCheckin exist = cmsDocCheckinMapper.selectByDocIdAndUserId(docId, userId);
if (exist != null) {
throw new ServiceException("您已打卡过该文章");
}
// 组装打卡记录(fileIdentifier 由前端传入的文件标识列表以逗号拼接)
CmsDocCheckin checkin = new CmsDocCheckin();
checkin.setDocId(docId);
checkin.setUserId(userId);
if (identifiers != null && !identifiers.isEmpty()) {
checkin.setFileIdentifier(String.join(",", identifiers));
}
if (dto != null) {
checkin.setNotes(dto.getNotes());
}
checkin.setCheckinTime(DateUtils.getNowDate());
checkin.setCreateAndUpdate(SecurityUtils.getUsername());
cmsDocCheckinMapper.insertCmsDocCheckin(checkin);
// 返回打卡结果(不含文章标题,前端已有文章上下文)
CmsDocCheckinVo vo = new CmsDocCheckinVo();
vo.setId(checkin.getId());
vo.setDocId(checkin.getDocId());
vo.setFileIdentifier(checkin.getFileIdentifier());
vo.setNotes(checkin.getNotes());
vo.setCheckinTime(checkin.getCheckinTime());
vo.setCreateTime(checkin.getCreateTime());
return vo;
}
@Override
public List<CmsDocCheckinVo> selectMyCheckinList(CmsDocCheckinQueryVo query) {
Long userId = SecurityUtils.getUserId();
if (userId == null) {
throw new ServiceException("未获取到登录用户信息,请重新登录");
}
List<CmsDocCheckinVo> list = cmsDocCheckinMapper.selectMyList(userId, query);
fillFileInfo(list);
return list;
}
@Override
public List<CmsDocCheckinVo> selectCheckinByDocIds(List<Long> docIds) {
if (docIds == null || docIds.isEmpty()) {
return Collections.emptyList();
}
Long userId = SecurityUtils.getUserId();
if (userId == null) {
throw new ServiceException("未获取到登录用户信息,请重新登录");
}
List<CmsDocCheckinVo> list = cmsDocCheckinMapper.selectByDocIdsAndUserId(docIds, userId);
fillFileInfo(list);
return list;
}
/**
* 校验文件标识列表是否都对应真实文件
* (getFileListByIdentifier 对不存在的标识会静默跳过,故需显式比对覆盖)
*/
private void checkFileIdentifiersExist(List<String> identifiers) {
List<SysFileInfo> files = FeignUtils.getReturnData(
remoteFileService.getFileListByIdentifier(String.join(",", identifiers)));
Set<String> found = (files == null) ? Collections.emptySet()
: files.stream().map(SysFileInfo::getFileIdentifier).collect(Collectors.toSet());
for (String fid : identifiers) {
if (!found.contains(fid)) {
throw new ServiceException("文件不存在(文件标识[%s])", fid);
}
}
}
/**
* 批量填充打卡记录的文件信息(一次 Feign 查询,避免 N+1)
* 文件信息为增强展示,文件服务异常时降级为不填充,不阻断主查询
*/
private void fillFileInfo(List<CmsDocCheckinVo> list) {
if (list == null || list.isEmpty()) {
return;
}
// 收集所有 fileIdentifier(逗号分隔)去重合并
Set<String> allIds = list.stream()
.map(CmsDocCheckinVo::getFileIdentifier)
.filter(StringUtils::isNotEmpty)
.flatMap(s -> Arrays.stream(s.split(",")))
.collect(Collectors.toSet());
if (allIds.isEmpty()) {
return;
}
try {
List<SysFileInfo> files = FeignUtils.getReturnData(
remoteFileService.getFileListByIdentifier(String.join(",", allIds)));
if (files == null || files.isEmpty()) {
return;
}
Map<String, SysFileInfo> map = files.stream()
.collect(Collectors.toMap(SysFileInfo::getFileIdentifier, f -> f, (a, b) -> a));
for (CmsDocCheckinVo vo : list) {
if (StringUtils.isEmpty(vo.getFileIdentifier())) {
continue;
}
List<SysFileInfo> voFiles = Arrays.stream(vo.getFileIdentifier().split(","))
.map(map::get)
.filter(Objects::nonNull)
.collect(Collectors.toList());
vo.setFileList(voFiles);
}
} catch (Exception e) {
logger.warn("填充打卡文件信息失败,降级为不填充", e);
}
}
}

View File

@ -149,8 +149,9 @@ public class CmsDocController extends BaseController {
@RequiresPermissions("cms:doc:query")
@ApiOperation("获取文章详细信息")
@GetMapping(value = "/{id}")
public AjaxResult getInfo(@ApiParam(name = "id", value = "文章Id") @PathVariable("id") Long id) {
return success(cmsDocService.selectCmsDocById(id, false, null));
public AjaxResult getInfo(@ApiParam(name = "id", value = "文章Id") @PathVariable("id") Long id,
@RequestParam(value = "withDict", required = false, defaultValue = "false") Boolean withDict) {
return success(cmsDocService.selectCmsDocById(id, false, null, withDict));
}
/**

View File

@ -179,8 +179,10 @@ public class OpenController extends BaseController {
*/
@ApiOperation("获取文章详细信息")
@GetMapping(value = "/{id}")
public GenericsAjaxResult<CmsDocDetailDto> getInfo(@ApiParam(name = "id", value = "文章Id") @PathVariable("id") Long id, HttpServletRequest request) {
return genericsSuccess(cmsDocService.selectCmsDocById(id, true, request));
public GenericsAjaxResult<CmsDocDetailDto> getInfo(@ApiParam(name = "id", value = "文章Id") @PathVariable("id") Long id,
@RequestParam(value = "withDict", required = false, defaultValue = "false") Boolean withDict,
HttpServletRequest request) {
return genericsSuccess(cmsDocService.selectCmsDocById(id, true, request, withDict));
}
/**

View File

@ -1,11 +1,14 @@
package com.microservices.cms.doc.domain;
import com.fasterxml.jackson.annotation.JsonInclude;
import com.microservices.cms.doc.domain.vo.DictAttributeVo;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.List;
/**
* @author otto
@ -106,6 +109,13 @@ public class CmsDocBaseDto {
*/
private String remark;
/**
* 字典扩展属性列表(withDict=true 时返回)
*/
@JsonInclude(JsonInclude.Include.NON_NULL)
@ApiModelProperty("字典扩展属性列表withDict=true 时返回)")
private List<DictAttributeVo> dictAttributes;
public void setPublishTime(Date publishTime) {
if (publishTime != null) {
SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy/MM/dd");

View File

@ -1,10 +1,12 @@
package com.microservices.cms.doc.domain;
import com.fasterxml.jackson.annotation.JsonFormat;
import com.microservices.cms.doc.domain.vo.DictFilterVo;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import java.util.Date;
import java.util.List;
/**
* @author otto
@ -43,6 +45,18 @@ public class CmsDocSearchVo {
@ApiModelProperty(value = "发布时间",hidden = true)
private Date publishTime;
/**
* 发布年份(:2026)
*/
@ApiModelProperty(value = "发布年份(如:2026)")
private Integer publishYear;
/**
* 发布月份(0或空代表查询全年1-12查询对应月份)
*/
@ApiModelProperty(value = "发布月份(0或空代表查询全年1-12查询对应月份)")
private Integer publishMonth;
/**
* 是否为栏目
*/
@ -89,4 +103,16 @@ public class CmsDocSearchVo {
*/
@ApiModelProperty(value = "排除文章Id", hidden = true)
private Long excludeId;
/**
* 字典筛选条件列表transient不参与数据库查询
*/
@ApiModelProperty(value = "字典筛选条件列表")
private List<DictFilterVo> dictFilters;
/**
* 是否按需加载字典扩展属性(默认 false不加载)
*/
@ApiModelProperty(value = "是否加载字典扩展属性")
private Boolean withDict;
}

View File

@ -0,0 +1,27 @@
package com.microservices.cms.doc.domain.vo;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
/**
* 文章字典属性(按需加载扁平结构)
*
* @author otto
*/
@Data
@ApiModel("文章字典属性")
public class DictAttributeVo {
@ApiModelProperty("字典类型Key")
private String dictType;
@ApiModelProperty("字典类型名称")
private String dictTypeName;
@ApiModelProperty("字典数据标签")
private String dictLabel;
@ApiModelProperty("字典数据值")
private String dictValue;
}

View File

@ -0,0 +1,23 @@
package com.microservices.cms.doc.domain.vo;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import javax.validation.constraints.NotBlank;
import java.io.Serializable;
/**
* 字典筛选条件VO
*
* @author otto
*/
@Data
public class DictFilterVo implements Serializable {
private static final long serialVersionUID = 1L;
@ApiModelProperty(value = "字典类型")
private String dictType;
@ApiModelProperty(value = "字典键值")
private String dictValue;
}

View File

@ -40,6 +40,16 @@ public interface CmsDocMapper {
*/
public List<CmsDoc> selectCmsDocList(CmsDocSearchVo cmsDocSearchVo);
/**
* 查询文章信息列表支持字典筛选
*
* @param cmsDocSearchVo 文章信息
* @param articleIds 字典筛选后的文章ID列表
* @return 文章信息集合
*/
public List<CmsDoc> selectCmsDocList(@Param("searchVo") CmsDocSearchVo cmsDocSearchVo,
@Param("articleIds") List<Long> articleIds);
/**
@ -96,7 +106,7 @@ public interface CmsDocMapper {
*/
CmsDoc selectCmsDocByNameAndDeptIdIgnoreDirName(@Param("name") String name, @Param("deptId") Long deptId);
List<CmsDoc> selectHotCmsDocList(CmsDoc cmsDocInput);
List<CmsDoc> selectHotCmsDocList(CmsDocSearchVo cmsDocSearchVo);
List<CmsDoc> selectCmsDocDirByDeptIdAndInName(@Param("deptId") Long deptId, @Param("cmsDirNameList") List<String> cmsDirNameList);

View File

@ -4,6 +4,7 @@ import com.alibaba.fastjson2.JSONObject;
import com.microservices.cms.doc.domain.*;
import com.microservices.cms.doc.domain.gitlink.EntryDto;
import com.microservices.cms.doc.domain.gitlink.FileEntryDto;
import com.microservices.cms.doc.domain.vo.DictFilterVo;
import com.microservices.cms.project.domain.CmsProject;
import com.microservices.common.core.web.page.GenericsTableDataInfo;
@ -26,6 +27,17 @@ public interface ICmsDocService {
*/
public CmsDocDetailDto selectCmsDocById(Long id, boolean isOpen, HttpServletRequest request);
/**
* 查询文章信息(可按需加载字典扩展属性)
*
* @param id 文章信息主键
* @param isOpen 是否公开接口
* @param request 请求对象
* @param withDict 是否按需加载字典扩展属性
* @return 文章信息
*/
public CmsDocDetailDto selectCmsDocById(Long id, boolean isOpen, HttpServletRequest request, boolean withDict);
/**
* 查询文章基础信息
*
@ -217,6 +229,14 @@ public interface ICmsDocService {
* @param cmsDocSearchVo
* @return
*/
/**
* 根据多个字典条件查询文章主键列表
*
* @param dictFilters 字典筛选条件
* @return 文章主键列表
*/
List<Long> selectArticleIdsByDictFilters(List<DictFilterVo> dictFilters);
int selectCmsDocCountByDept(Long deptId, CmsDocSearchVo cmsDocSearchVo);
/**

View File

@ -1,9 +1,12 @@
package com.microservices.cms.doc.service.impl;
import com.alibaba.fastjson2.JSONArray;
import com.alibaba.fastjson2.JSONObject;
import com.microservices.cms.doc.domain.*;
import com.microservices.cms.doc.domain.gitlink.EntryDto;
import com.microservices.cms.doc.domain.gitlink.FileEntryDto;
import com.microservices.cms.doc.domain.vo.DictAttributeVo;
import com.microservices.cms.doc.domain.vo.DictFilterVo;
import com.microservices.cms.doc.mapper.CmsDocMapper;
import com.microservices.cms.doc.service.ICmsAsyncService;
import com.microservices.cms.doc.service.ICmsDocCommentService;
@ -16,6 +19,7 @@ import com.microservices.common.core.constant.CacheConstants;
import com.microservices.common.core.constant.HttpStatus;
import com.microservices.common.core.constant.SecurityConstants;
import com.microservices.common.core.constant.UserConstants;
import com.microservices.common.core.domain.R;
import com.microservices.common.core.enums.DocAndZoneOperation;
import com.microservices.common.core.enums.DocAuditStatus;
import com.microservices.common.core.enums.SystemRole;
@ -102,6 +106,11 @@ public class CmsDocServiceImpl implements ICmsDocService {
*/
@Override
public CmsDocDetailDto selectCmsDocById(Long id, boolean isOpen, HttpServletRequest request) {
return selectCmsDocById(id, isOpen, request, false);
}
@Override
public CmsDocDetailDto selectCmsDocById(Long id, boolean isOpen, HttpServletRequest request, boolean withDict) {
CmsDoc cmsDoc = cmsDocMapper.selectCmsDocById(id);
if (cmsDoc == null || !canVisitNotPassedDoc(cmsDoc, isOpen, request)) {
throw new ServiceException(HttpStatus.NOT_FOUND, "该文章不存在(文章Id[" + id + "])");
@ -133,9 +142,61 @@ public class CmsDocServiceImpl implements ICmsDocService {
if (isOpen && DocAuditStatus.AUDIT_PASS.getAuditCode().equals(cmsDoc.getAuditStatus())) {
updateVisits(cmsDocDto.getId());
}
if (withDict) {
fillDictAttributes(Collections.singletonList(cmsDocDto));
}
return cmsDocDto;
}
/**
* 按需批量填充字典扩展属性
* 收集所有文章 ID 一次 Feign 取字典 objectId 分组 填充到每个 DTO
* Feign 降级或异常时返回空列表不影响主查询
*/
private void fillDictAttributes(List<? extends CmsDocBaseDto> dtoList) {
if (dtoList == null || dtoList.isEmpty()) {
return;
}
List<Long> articleIds = dtoList.stream()
.map(CmsDocBaseDto::getId)
.filter(id -> id != null)
.distinct()
.collect(Collectors.toList());
if (articleIds.isEmpty()) {
return;
}
try {
R<List<JSONObject>> result = remoteZoneService.getDictsByObjectIds(
"CMS_DOC", articleIds.toArray(new Long[0]), SecurityConstants.INNER);
if (result == null || !R.isSuccess(result) || result.getData() == null) {
logger.warn("按需加载文章字典属性降级返回空(原因: {})",
result != null ? result.getMsg() : "null");
dtoList.forEach(dto -> dto.setDictAttributes(Collections.emptyList()));
return;
}
Map<Long, List<JSONObject>> dictsByObjectId = result.getData().stream()
.collect(Collectors.groupingBy(j -> j.getLong("objectId")));
for (CmsDocBaseDto dto : dtoList) {
List<JSONObject> dicts = dictsByObjectId.get(dto.getId());
if (dicts == null || dicts.isEmpty()) {
dto.setDictAttributes(Collections.emptyList());
} else {
dto.setDictAttributes(dicts.stream().map(j -> {
DictAttributeVo vo = new DictAttributeVo();
vo.setDictType(j.getString("dictType"));
vo.setDictTypeName(j.getString("dictTypeName"));
vo.setDictLabel(j.getString("dictLabel"));
vo.setDictValue(j.getString("dictValue"));
return vo;
}).collect(Collectors.toList()));
}
}
} catch (Exception e) {
logger.warn("按需加载文章字典属性异常,降级返回空列表", e);
dtoList.forEach(dto -> dto.setDictAttributes(Collections.emptyList()));
}
}
@Override
public CmsDoc selectBaseCmsDocById(Long id) {
CmsDoc cmsDoc = cmsDocMapper.selectCmsDocById(id);
@ -182,8 +243,24 @@ public class CmsDocServiceImpl implements ICmsDocService {
public List<CmsDocAllDto> selectCmsDocListByDept(Long deptId, CmsDocSearchVo cmsDocSearchVo) {
cmsDocSearchVo.setDeptId(deptId);
cmsDocSearchVo.setIsDir(false);
List<CmsDoc> cmsDocList = cmsDocMapper.selectCmsDocList(cmsDocSearchVo);
return cmsDocList.stream().map(CmsDoc::copyToCmsDocAllDto).collect(Collectors.toList());
// 如果有字典筛选条件先获取符合条件的文章 ID
List<Long> articleIds = null;
if (cmsDocSearchVo.getDictFilters() != null && !cmsDocSearchVo.getDictFilters().isEmpty()) {
articleIds = selectArticleIdsByDictFilters(cmsDocSearchVo.getDictFilters());
// 如果没有符合条件的文章直接返回空列表
if (articleIds.isEmpty()) {
return Collections.emptyList();
}
}
List<CmsDoc> cmsDocList = cmsDocMapper.selectCmsDocList(cmsDocSearchVo, articleIds);
List<CmsDocAllDto> dtoList = cmsDocList.stream().map(CmsDoc::copyToCmsDocAllDto).collect(Collectors.toList());
if (Boolean.TRUE.equals(cmsDocSearchVo.getWithDict())) {
fillDictAttributes(dtoList);
}
return dtoList;
}
@Override
@ -339,7 +416,11 @@ public class CmsDocServiceImpl implements ICmsDocService {
if (cmsDocList == null || cmsDocList.isEmpty()) {
return new ArrayList<>();
}
return cmsDocList.stream().map(CmsDoc::copyToCmsDocBaseDto).collect(Collectors.toList());
List<CmsDocBaseDto> dtoList = cmsDocList.stream().map(CmsDoc::copyToCmsDocBaseDto).collect(Collectors.toList());
if (Boolean.TRUE.equals(cmsDocSearchVo.getWithDict())) {
fillDictAttributes(dtoList);
}
return dtoList;
}
/**
@ -354,11 +435,46 @@ public class CmsDocServiceImpl implements ICmsDocService {
cmsDocSearchVo.setDeptId(cmsDocDir.getDeptId());
cmsDocSearchVo.setDirName(cmsDocDir.getName());
// 参数验证发布月份0或空代表查询全年1-12查询对应月份
Integer publishMonth = cmsDocSearchVo.getPublishMonth();
if (publishMonth != null && (publishMonth < 0 || publishMonth > 12)) {
throw new ServiceException("月份必须在0-12之间(0或空代表全年)");
}
// 处理字典筛选条件如果存在字典筛选条件先获取符合条件的文章ID列表
List<Long> dictFilteredIds = null;
if (cmsDocSearchVo.getDictFilters() != null && !cmsDocSearchVo.getDictFilters().isEmpty()) {
dictFilteredIds = selectArticleIdsByDictFilters(cmsDocSearchVo.getDictFilters());
// 如果没有符合条件的文章直接返回空列表
if (dictFilteredIds == null || dictFilteredIds.isEmpty()) {
return new ArrayList<>();
}
}
List<CmsDoc> cmsDocList = cmsDocMapper.selectCmsDocListSetDefaultHeadImg(cmsDocSearchVo);
if (cmsDocList == null || cmsDocList.isEmpty()) {
return new ArrayList<>();
}
return cmsDocList.stream().map(CmsDoc::copyToCmsDocBaseDto).collect(Collectors.toList());
// 如果有字典筛选条件在内存中过滤结果只保留符合字典条件的文章
List<CmsDoc> filteredList = cmsDocList;
if (dictFilteredIds != null && !dictFilteredIds.isEmpty()) {
Set<Long> dictFilteredIdSet = new HashSet<>(dictFilteredIds);
filteredList = cmsDocList.stream()
.filter(doc -> dictFilteredIdSet.contains(doc.getId()))
.collect(Collectors.toList());
// 如果过滤后为空直接返回空列表
if (filteredList.isEmpty()) {
return new ArrayList<>();
}
}
List<CmsDocBaseDto> dtoList = filteredList.stream().map(CmsDoc::copyToCmsDocBaseDto).collect(Collectors.toList());
if (Boolean.TRUE.equals(cmsDocSearchVo.getWithDict())) {
fillDictAttributes(dtoList);
}
return dtoList;
}
/**
@ -1142,12 +1258,12 @@ public class CmsDocServiceImpl implements ICmsDocService {
@Override
public List<HotCmsDocDto> selectHotCmsDocList(Long dirId) {
CmsDoc cmsDocInput = new CmsDoc();
CmsDocSearchVo cmsDocSearchVo = new CmsDocSearchVo();
CmsDoc cmsDir = selectCmsDir(dirId);
cmsDocInput.setDeptId(cmsDir.getDeptId());
cmsDocInput.setIsDir(false);
cmsDocInput.setDirName(cmsDir.getName());
List<CmsDoc> cmsDocList = cmsDocMapper.selectHotCmsDocList(cmsDocInput);
cmsDocSearchVo.setDeptId(cmsDir.getDeptId());
cmsDocSearchVo.setIsDir(false);
cmsDocSearchVo.setDirName(cmsDir.getName());
List<CmsDoc> cmsDocList = cmsDocMapper.selectHotCmsDocList(cmsDocSearchVo);
if (cmsDocList != null) {
//todo:需检查该文章是否在GitLink中存在
return cmsDocList.stream().map(CmsDoc::copyToHotCmsDocDto).collect(Collectors.toList());
@ -1178,10 +1294,10 @@ public class CmsDocServiceImpl implements ICmsDocService {
@Override
public List<HotCmsDocDto> selectHotCmsDocListByDept(Long deptId) {
CmsDoc cmsDocInput = new CmsDoc();
cmsDocInput.setDeptId(deptId);
cmsDocInput.setIsDir(false);
List<CmsDoc> cmsDocList = cmsDocMapper.selectHotCmsDocList(cmsDocInput);
CmsDocSearchVo cmsDocSearchVo = new CmsDocSearchVo();
cmsDocSearchVo.setDeptId(deptId);
cmsDocSearchVo.setIsDir(false);
List<CmsDoc> cmsDocList = cmsDocMapper.selectHotCmsDocList(cmsDocSearchVo);
if (cmsDocList != null) {
//todo:需检查该文章是否在GitLink中存在
return cmsDocList.stream().map(CmsDoc::copyToHotCmsDocDto).collect(Collectors.toList());
@ -1597,4 +1713,26 @@ public class CmsDocServiceImpl implements ICmsDocService {
}
return success;
}
@Override
public List<Long> selectArticleIdsByDictFilters(List<DictFilterVo> dictFilters) {
if (dictFilters == null || dictFilters.isEmpty()) {
return Collections.emptyList();
}
try {
// 调用 Zone 服务获取符合条件的文章 ID
R<List<Long>> result = remoteZoneService.getArticleIdsByDictFilters(
"CMS_DOC", JSONArray.from(dictFilters), SecurityConstants.INNER);
if (result == null || !R.isSuccess(result)) {
throw new ServiceException("获取字典筛选文章ID失败" + (result != null ? result.getMsg() : "未知错误"));
}
return result.getData() != null ? result.getData() : Collections.emptyList();
} catch (Exception e) {
logger.error("调用Zone服务获取字典筛选文章ID失败", e);
throw new ServiceException("字典筛选文章查询失败:" + e.getMessage());
}
}
}

View File

@ -0,0 +1,95 @@
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.microservices.cms.checkin.mapper.CmsDocCheckinMapper">
<resultMap type="com.microservices.cms.checkin.domain.CmsDocCheckin" id="CmsDocCheckinResult">
<id property="id" column="id"/>
<result property="docId" column="doc_id"/>
<result property="userId" column="user_id"/>
<result property="fileIdentifier" column="file_identifier"/>
<result property="notes" column="notes"/>
<result property="checkinTime" column="checkin_time"/>
<result property="createBy" column="create_by"/>
<result property="createTime" column="create_time"/>
<result property="updateBy" column="update_by"/>
<result property="updateTime" column="update_time"/>
</resultMap>
<resultMap type="com.microservices.cms.checkin.domain.vo.CmsDocCheckinVo" id="CmsDocCheckinVoResult">
<id property="id" column="id"/>
<result property="docId" column="doc_id"/>
<result property="docName" column="doc_name"/>
<result property="fileIdentifier" column="file_identifier"/>
<result property="notes" column="notes"/>
<result property="checkinTime" column="checkin_time"/>
<result property="createTime" column="create_time"/>
</resultMap>
<sql id="selectCmsDocCheckinVo">
select id, doc_id, user_id, file_identifier, notes, checkin_time,
create_by, create_time, update_by, update_time
from cms_doc_checkin
</sql>
<select id="selectByDocIdAndUserId" resultMap="CmsDocCheckinResult">
<include refid="selectCmsDocCheckinVo"/>
where doc_id = #{docId} and user_id = #{userId}
</select>
<insert id="insertCmsDocCheckin"
parameterType="com.microservices.cms.checkin.domain.CmsDocCheckin"
useGeneratedKeys="true"
keyProperty="id">
insert into cms_doc_checkin
<trim prefix="(" suffix=")" suffixOverrides=",">
<if test="docId != null">doc_id,</if>
<if test="userId != null">user_id,</if>
<if test="fileIdentifier != null and fileIdentifier != ''">file_identifier,</if>
<if test="notes != null and notes != ''">notes,</if>
<if test="checkinTime != null">checkin_time,</if>
<if test="createBy != null">create_by,</if>
<if test="createTime != null">create_time,</if>
<if test="updateBy != null">update_by,</if>
<if test="updateTime != null">update_time,</if>
</trim>
<trim prefix="values (" suffix=")" suffixOverrides=",">
<if test="docId != null">#{docId},</if>
<if test="userId != null">#{userId},</if>
<if test="fileIdentifier != null and fileIdentifier != ''">#{fileIdentifier},</if>
<if test="notes != null and notes != ''">#{notes},</if>
<if test="checkinTime != null">#{checkinTime},</if>
<if test="createBy != null">#{createBy},</if>
<if test="createTime != null">#{createTime},</if>
<if test="updateBy != null">#{updateBy},</if>
<if test="updateTime != null">#{updateTime},</if>
</trim>
</insert>
<select id="selectMyList" resultMap="CmsDocCheckinVoResult">
select c.id, c.doc_id, d.name as doc_name, c.file_identifier, c.notes, c.checkin_time, c.create_time
from cms_doc_checkin c
left join cms_doc d on c.doc_id = d.id
where c.user_id = #{userId}
<if test="query != null and query.docName != null and query.docName != ''">
and d.name like concat('%', #{query.docName}, '%')
</if>
<if test="query != null and query.notes != null and query.notes != ''">
and c.notes like concat('%', #{query.notes}, '%')
</if>
order by c.checkin_time desc
</select>
<select id="selectByDocIdsAndUserId" resultMap="CmsDocCheckinVoResult">
select c.id, c.doc_id, d.name as doc_name, c.file_identifier, c.notes, c.checkin_time, c.create_time
from cms_doc_checkin c
left join cms_doc d on c.doc_id = d.id
where c.user_id = #{userId}
and c.doc_id in
<foreach collection="docIds" item="docId" open="(" separator="," close=")">
#{docId}
</foreach>
</select>
</mapper>

View File

@ -202,7 +202,7 @@
where dept_id=#{deptId}
</select>
<select id="selectCmsDocList" parameterType="com.microservices.cms.doc.domain.CmsDocSearchVo"
<select id="selectCmsDocListSimple" parameterType="com.microservices.cms.doc.domain.CmsDocSearchVo"
resultMap="CmsDocResult">
<include refid="selectCmsDocVo"/>
<include refid="selectListWhere"/>
@ -212,6 +212,35 @@
create_time DESC
</select>
<select id="selectCmsDocList" resultMap="CmsDocResult">
<include refid="selectCmsDocVo"/>
<where>
<if test="searchVo.name != null and searchVo.name != ''">and name like concat('%', #{searchVo.name}, '%')</if>
<if test="searchVo.keywords != null and searchVo.keywords != ''">and keywords like concat('%', #{searchVo.keywords}, '%')</if>
<if test="searchVo.fileName != null and searchVo.fileName != ''">and file_name like concat('%', #{searchVo.fileName}, '%')</if>
<if test="searchVo.filePath != null and searchVo.filePath != ''">and file_path = #{searchVo.filePath}</if>
<if test="searchVo.publishTime != null ">and publish_time = #{searchVo.publishTime}</if>
<if test="searchVo.isDir != null ">and is_dir = #{searchVo.isDir}</if>
<if test="searchVo.dirName != null ">and dir_name = #{searchVo.dirName}</if>
<if test="searchVo.deptId != null ">and dept_id = #{searchVo.deptId}</if>
<if test="searchVo.isHomepage != null ">and is_homepage = #{searchVo.isHomepage}</if>
<if test="searchVo.auditStatus != null and searchVo.auditStatus != '-1' ">and audit_status = #{searchVo.auditStatus}</if>
<if test="searchVo.auditStatus == null ">and audit_status = '1'</if>
<if test="searchVo.createBy != null and searchVo.createBy != '' ">and create_by = #{searchVo.createBy}</if>
<if test="searchVo.excludeId != null ">and id != #{searchVo.excludeId}</if>
<if test="articleIds != null and articleIds.size() > 0">
AND id IN
<foreach collection="articleIds" item="id" open="(" separator="," close=")">
#{id}
</foreach>
</if>
</where>
ORDER BY
sort DESC,
update_time DESC,
create_time DESC
</select>
<select id="selectCmsDocById" parameterType="Long" resultMap="CmsDocResult">
<include refid="selectCmsDocVo"/>
where id = #{id}
@ -257,7 +286,7 @@
<include refid="selectCmsDocVo"/>
where file_path = #{filePath} and dept_id = #{deptId} and is_dir = 0
</select>
<select id="selectHotCmsDocList" parameterType="com.microservices.cms.doc.domain.CmsDoc" resultMap="CmsDocResult">
<select id="selectHotCmsDocList" parameterType="com.microservices.cms.doc.domain.CmsDocSearchVo" resultMap="CmsDocResult">
<include refid="selectCmsDocAliasSetDefaultHeadImgVo"/>
<include refid="selectListWhereAlias"/>
ORDER BY
@ -278,6 +307,18 @@
<if test="fileName != null and fileName != ''">and file_name like concat('%', #{fileName}, '%')</if>
<if test="filePath != null and filePath != ''">and file_path = #{filePath}</if>
<if test="publishTime != null ">and publish_time = #{publishTime}</if>
<if test="publishYear != null">
<choose>
<when test="publishMonth != null and publishMonth > 0">
and publish_time &gt;= STR_TO_DATE(CONCAT(#{publishYear}, '-', LPAD(#{publishMonth}, 2, '0'), '-01'), '%Y-%m-%d')
and publish_time &lt; DATE_ADD(STR_TO_DATE(CONCAT(#{publishYear}, '-', LPAD(#{publishMonth}, 2, '0'), '-01'), '%Y-%m-%d'), INTERVAL 1 MONTH)
</when>
<otherwise>
and publish_time &gt;= STR_TO_DATE(CONCAT(#{publishYear}, '-01-01'), '%Y-%m-%d')
and publish_time &lt; DATE_ADD(STR_TO_DATE(CONCAT(#{publishYear}, '-01-01'), '%Y-%m-%d'), INTERVAL 1 YEAR)
</otherwise>
</choose>
</if>
<if test="isDir != null ">and is_dir = #{isDir}</if>
<if test="dirName != null ">and dir_name = #{dirName}</if>
<if test="deptId != null ">and dept_id = #{deptId}</if>
@ -296,6 +337,18 @@
<if test="fileName != null and fileName != ''">and cd.file_name like concat('%', #{fileName}, '%')</if>
<if test="filePath != null and filePath != ''">and cd.file_path = #{filePath}</if>
<if test="publishTime != null ">and cd.publish_time = #{publishTime}</if>
<if test="publishYear != null">
<choose>
<when test="publishMonth != null and publishMonth > 0">
and cd.publish_time &gt;= STR_TO_DATE(CONCAT(#{publishYear}, '-', LPAD(#{publishMonth}, 2, '0'), '-01'), '%Y-%m-%d')
and cd.publish_time &lt; DATE_ADD(STR_TO_DATE(CONCAT(#{publishYear}, '-', LPAD(#{publishMonth}, 2, '0'), '-01'), '%Y-%m-%d'), INTERVAL 1 MONTH)
</when>
<otherwise>
and cd.publish_time &gt;= STR_TO_DATE(CONCAT(#{publishYear}, '-01-01'), '%Y-%m-%d')
and cd.publish_time &lt; DATE_ADD(STR_TO_DATE(CONCAT(#{publishYear}, '-01-01'), '%Y-%m-%d'), INTERVAL 1 YEAR)
</otherwise>
</choose>
</if>
<if test="isDir != null ">and cd.is_dir = #{isDir}</if>
<if test="dirName != null ">and cd.dir_name = #{dirName}</if>
<if test="deptId != null ">and cd.dept_id = #{deptId}</if>

View File

@ -0,0 +1,19 @@
-- 文章打卡记录表
-- 说明CMS 模块无 DDL 版本管理(flyway),此脚本需手动在目标库执行。
-- 唯一索引 uk_doc_user 保证一人对一篇文章只能打卡一次(与 Service 层先查后插双保险)。
CREATE TABLE cms_doc_checkin (
id BIGINT NOT NULL AUTO_INCREMENT COMMENT '主键',
doc_id BIGINT NOT NULL COMMENT '文章ID',
user_id BIGINT NOT NULL COMMENT '打卡用户ID',
file_identifier VARCHAR(500) COMMENT '照片文件标识(逗号分隔)',
notes VARCHAR(500) COMMENT '打卡备注',
checkin_time DATETIME NOT NULL COMMENT '打卡时间(服务端记录)',
create_by VARCHAR(64) DEFAULT '' COMMENT '创建者',
create_time DATETIME COMMENT '创建时间',
update_by VARCHAR(64) DEFAULT '' COMMENT '更新者',
update_time DATETIME COMMENT '更新时间',
PRIMARY KEY (id),
UNIQUE KEY uk_doc_user (doc_id, user_id) COMMENT '一人一文一次打卡唯一约束',
KEY idx_user_id (user_id),
KEY idx_doc_id (doc_id)
) COMMENT='文章打卡记录';

View File

@ -0,0 +1,142 @@
package com.microservices.zone.extension.controller;
import com.microservices.common.core.web.controller.BaseController;
import com.microservices.common.core.web.domain.AjaxResult;
import com.microservices.common.core.web.domain.GenericsAjaxResult;
import com.microservices.common.core.web.page.GenericsTableDataInfo;
import com.microservices.common.log.annotation.Log;
import com.microservices.common.log.enums.BusinessType;
import com.microservices.common.security.annotation.RequiresPermissions;
import com.microservices.zone.extension.domain.ZoneObjectDict;
import com.microservices.zone.extension.domain.vo.DictFilterVo;
import com.microservices.zone.extension.domain.vo.ZoneObjectDictInputVo;
import com.microservices.zone.extension.domain.vo.ZoneObjectDictSearchVo;
import com.microservices.zone.extension.service.IZoneObjectDictService;
import com.microservices.zone.specialProject.domain.vo.CommonTypeVo;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
import com.alibaba.fastjson2.JSONArray;
import com.alibaba.fastjson2.JSONObject;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
/**
* 特色专区对象-字典扩展Controller
*
* @author otto
*/
@RestController
@RequestMapping("/objectDict")
@Api(tags = "特色专区对象-字典扩展接口")
public class ZoneObjectDictController extends BaseController {
@Autowired
private IZoneObjectDictService zoneObjectDictService;
/**
* 查询对象-字典绑定列表
*/
// @RequiresPermissions("zone:objectDict:list")
@GetMapping("/list")
@ApiOperation("查询对象-字典绑定列表")
public GenericsTableDataInfo<ZoneObjectDict> list(@Validated ZoneObjectDictSearchVo searchVo) {
startPage();
List<ZoneObjectDict> list = zoneObjectDictService.selectZoneObjectDictList(searchVo.toZoneObjectDict());
return getGenericsDataTable(list);
}
/**
* 获取对象-字典绑定详细信息
*/
// @RequiresPermissions("zone:objectDict:query")
@GetMapping("/{id}")
@ApiOperation("获取对象-字典绑定详细信息")
public GenericsAjaxResult<ZoneObjectDict> getInfo(@PathVariable("id") Long id) {
return genericsSuccess(zoneObjectDictService.selectZoneObjectDictById(id));
}
/**
* 查询某对象的全部字典绑定(含字典标签)
*/
// @RequiresPermissions("zone:objectDict:query")
@GetMapping("/object/{objectType}/{objectId}")
@ApiOperation("查询某对象的全部字典绑定(含字典标签)")
public GenericsAjaxResult<List<ZoneObjectDict>> getByObject(@PathVariable("objectType") String objectType,
@PathVariable("objectId") Long objectId) {
return genericsSuccess(zoneObjectDictService.selectByObject(objectType, objectId));
}
/**
* 反查某字典数据关联的对象主键列表
*/
// @RequiresPermissions("zone:objectDict:query")
@GetMapping("/objectIds")
@ApiOperation("反查:某字典数据关联的对象主键列表")
public GenericsAjaxResult<List<Long>> objectIds(@RequestParam("objectType") String objectType,
@RequestParam("dictType") String dictType,
@RequestParam("dictValue") String dictValue) {
return genericsSuccess(zoneObjectDictService.selectObjectIdsByDict(objectType, dictType, dictValue));
}
/**
* 对象类型列表
*/
@GetMapping("/objectType/list")
@ApiOperation("对象类型列表")
public List<CommonTypeVo> objectTypeList() {
return zoneObjectDictService.selectObjectTypeList();
}
/**
* 全量保存某对象的字典绑定(传空列表清空)
*/
// @RequiresPermissions("zone:objectDict:add")
@Log(title = "专区对象-字典扩展", businessType = BusinessType.INSERT)
@PostMapping
@ApiOperation("全量保存某对象的字典绑定(传空列表清空)")
public AjaxResult save(@RequestBody @Validated ZoneObjectDictInputVo inputVo) {
zoneObjectDictService.saveObjectDicts(inputVo);
return AjaxResult.success();
}
/**
* 根据多个字典条件查询对象主键列表AND 逻辑
*/
// @RequiresPermissions("zone:objectDict:query")
@PostMapping("/objectIdsByFilters")
@ApiOperation("根据多个字典条件查询对象主键列表")
public GenericsAjaxResult<List<Long>> objectIdsByFilters(@RequestParam("objectType") String objectType,
@RequestBody JSONArray dictFiltersJson) {
List<DictFilterVo> dictFilters = dictFiltersJson.toJavaList(DictFilterVo.class);
return genericsSuccess(zoneObjectDictService.selectObjectIdsByDictFilters(objectType, dictFilters));
}
/**
* 批量查询多个对象的字典属性(含标签用于跨服务按需加载)
*/
// @RequiresPermissions("zone:objectDict:query")
@PostMapping("/dictsByObjectIds")
@ApiOperation("批量查询多个对象的字典属性")
public GenericsAjaxResult<List<JSONObject>> dictsByObjectIds(@RequestParam("objectType") String objectType,
@RequestBody Long[] objectIds) {
return genericsSuccess(
zoneObjectDictService.selectDictAttributesByObjectIds(objectType, Arrays.asList(objectIds)));
}
/**
* 批量删除对象-字典绑定
*/
// @RequiresPermissions("zone:objectDict:remove")
@Log(title = "专区对象-字典扩展", businessType = BusinessType.DELETE)
@DeleteMapping("/{ids}")
@ApiOperation("批量删除对象-字典绑定")
public AjaxResult remove(@PathVariable Long[] ids) {
return toAjax(zoneObjectDictService.deleteZoneObjectDictByIds(ids));
}
}

View File

@ -0,0 +1,62 @@
package com.microservices.zone.extension.domain;
import com.microservices.zone.detail.domain.ZoneSort;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
/**
* 特色专区对象-字典扩展关联对象 zone_object_dict
* <p>
* 用于将专区对象(文章/资源/会员/项目)与系统字典(sys_dict_type/sys_dict_data)绑定
* 从而为专区对象扩展任意数量的自定义属性
*
* @author otto
*/
@Data
@ApiModel("专区对象-字典扩展关联对象")
public class ZoneObjectDict extends ZoneSort {
public static final String table_name = "zone_object_dict";
/**
* 主键
*/
@ApiModelProperty(value = "主键")
private Long id;
/**
* 对象类型(CMS_DOC文章/RESOURCE资源/MEMBER会员/PROJECT项目)
*/
@ApiModelProperty(value = "对象类型(CMS_DOC文章/RESOURCE资源/MEMBER会员/PROJECT项目)")
private String objectType;
/**
* 对象主键( cms_doc.id / zone_resource.id )
*/
@ApiModelProperty(value = "对象主键")
private Long objectId;
/**
* 关联字典类型Key(sys_dict_type.dict_type)
*/
@ApiModelProperty(value = "关联字典类型Key")
private String dictType;
/**
* 关联字典数据键值(sys_dict_data.dict_value)
*/
@ApiModelProperty(value = "关联字典数据键值")
private String dictValue;
/**
* 字典数据标签(非持久化服务端经 DictUtils 缓存解析填充)
*/
@ApiModelProperty(value = "字典数据标签(非持久化,服务端解析填充)")
private String dictLabel;
/**
* 字典类型名称(非持久化服务端解析填充来自 sys_dict_type.dict_name)
*/
@ApiModelProperty(value = "字典类型名称(非持久化,服务端解析填充)")
private String dictTypeName;
}

View File

@ -0,0 +1,62 @@
package com.microservices.zone.extension.domain;
import com.microservices.common.core.exception.ServiceException;
import com.microservices.common.core.utils.StringUtils;
import com.microservices.zone.specialProject.domain.vo.CommonTypeVo;
import io.swagger.annotations.ApiModel;
import lombok.Getter;
import java.util.ArrayList;
import java.util.List;
/**
* 专区对象字典扩展-对象类型枚举
*
* @author otto
*/
@Getter
@ApiModel("专区对象字典扩展-对象类型")
public enum ZoneObjectDictTypeEnum {
CMS_DOC("CMS_DOC", "文章"),
RESOURCE("RESOURCE", "资源"),
MEMBER("MEMBER", "会员"),
PROJECT("PROJECT", "项目");
private final String key;
private final String name;
ZoneObjectDictTypeEnum(String key, String name) {
this.key = key;
this.name = name;
}
public static List<CommonTypeVo> getAllToList() {
List<CommonTypeVo> list = new ArrayList<>();
for (ZoneObjectDictTypeEnum e : values()) {
list.add(new CommonTypeVo(e.key, e.name));
}
return list;
}
public static String getNameByKey(String key) {
if (StringUtils.isNotEmpty(key)) {
for (ZoneObjectDictTypeEnum e : values()) {
if (e.getKey().equalsIgnoreCase(key)) {
return e.getName();
}
}
}
throw new ServiceException("该对象类型不存在(对象类型[%s])", key);
}
public static boolean isValid(String key) {
if (StringUtils.isNotEmpty(key)) {
for (ZoneObjectDictTypeEnum e : values()) {
if (e.getKey().equalsIgnoreCase(key)) {
return true;
}
}
}
return false;
}
}

View File

@ -0,0 +1,35 @@
package com.microservices.zone.extension.domain.vo;
import javax.validation.constraints.NotBlank;
import java.io.Serializable;
/**
* 字典筛选条件VO
*
* @author otto
*/
public class DictFilterVo implements Serializable {
private static final long serialVersionUID = 1L;
@NotBlank(message = "字典类型不能为空")
private String dictType;
@NotBlank(message = "字典键值不能为空")
private String dictValue;
public String getDictType() {
return dictType;
}
public void setDictType(String dictType) {
this.dictType = dictType;
}
public String getDictValue() {
return dictValue;
}
public void setDictValue(String dictValue) {
this.dictValue = dictValue;
}
}

View File

@ -0,0 +1,25 @@
package com.microservices.zone.extension.domain.vo;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import javax.validation.constraints.NotBlank;
/**
* 专区对象字典扩展-字典绑定项
*
* @author otto
*/
@Data
@ApiModel("专区对象字典扩展-字典绑定项")
public class ZoneObjectDictBindingVo {
@ApiModelProperty(value = "关联字典类型Key", required = true)
@NotBlank
private String dictType;
@ApiModelProperty(value = "关联字典数据键值", required = true)
@NotBlank
private String dictValue;
}

View File

@ -0,0 +1,45 @@
package com.microservices.zone.extension.domain.vo;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import javax.validation.Valid;
import javax.validation.constraints.NotBlank;
import javax.validation.constraints.NotNull;
import java.util.List;
/**
* 专区对象字典扩展-保存输入对象
* <p>
* 全量保存某对象的字典绑定服务端会先删除该对象已有的全部绑定再按 bindings 批量插入
* bindings 为空列表表示清空该对象的全部绑定
*
* @author otto
*/
@Data
@ApiModel("专区对象字典扩展-保存输入对象")
public class ZoneObjectDictInputVo {
@ApiModelProperty(value = "对象类型(CMS_DOC/RESOURCE/MEMBER/PROJECT)", required = true)
@NotBlank
private String objectType;
@ApiModelProperty(value = "对象主键", required = true)
@NotNull
private Long objectId;
@ApiModelProperty(value = "专区标识")
private Long zoneId;
@ApiModelProperty(value = "所属组织Id")
private Long deptId;
@ApiModelProperty(value = "备注")
private String remark;
@ApiModelProperty(value = "字典绑定列表(传空列表表示清空该对象的所有绑定)", required = true)
@NotNull
@Valid
private List<ZoneObjectDictBindingVo> bindings;
}

View File

@ -0,0 +1,41 @@
package com.microservices.zone.extension.domain.vo;
import com.microservices.common.core.utils.bean.BeanUtils;
import com.microservices.zone.extension.domain.ZoneObjectDict;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
/**
* 专区对象字典扩展-搜索对象
*
* @author otto
*/
@Data
@ApiModel("专区对象字典扩展-搜索对象")
public class ZoneObjectDictSearchVo {
@ApiModelProperty(value = "对象类型")
private String objectType;
@ApiModelProperty(value = "对象主键")
private Long objectId;
@ApiModelProperty(value = "关联字典类型Key")
private String dictType;
@ApiModelProperty(value = "关联字典数据键值")
private String dictValue;
@ApiModelProperty(value = "专区标识")
private Long zoneId;
@ApiModelProperty(value = "所属组织Id")
private Long deptId;
public ZoneObjectDict toZoneObjectDict() {
ZoneObjectDict target = new ZoneObjectDict();
BeanUtils.copyProperties(this, target);
return target;
}
}

View File

@ -0,0 +1,87 @@
package com.microservices.zone.extension.mapper;
import com.microservices.zone.extension.domain.ZoneObjectDict;
import com.microservices.zone.extension.domain.vo.DictFilterVo;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import java.util.List;
import java.util.Map;
/**
* 专区对象-字典扩展关联Mapper接口
*
* @author otto
*/
@Mapper
public interface ZoneObjectDictMapper {
/**
* 按主键查询
*/
ZoneObjectDict selectZoneObjectDictById(Long id);
/**
* 条件查询列表
*/
List<ZoneObjectDict> selectZoneObjectDictList(ZoneObjectDict zoneObjectDict);
/**
* 查询某对象的全部字典绑定
*/
List<ZoneObjectDict> selectByObject(@Param("objectType") String objectType, @Param("objectId") Long objectId);
/**
* 批量查询多个对象的全部字典绑定
*/
List<ZoneObjectDict> selectByObjectIds(@Param("objectType") String objectType,
@Param("objectIds") List<Long> objectIds);
/**
* 反查某字典数据关联的对象主键列表
*/
List<Long> selectObjectIdsByDict(@Param("objectType") String objectType,
@Param("dictType") String dictType,
@Param("dictValue") String dictValue);
/**
* 新增单条
*/
int insertZoneObjectDict(ZoneObjectDict zoneObjectDict);
/**
* 批量新增
*/
int batchInsertZoneObjectDict(@Param("list") List<ZoneObjectDict> list);
/**
* 修改
*/
int updateZoneObjectDict(ZoneObjectDict zoneObjectDict);
/**
* 按主键删除
*/
int deleteZoneObjectDictById(Long id);
/**
* 批量删除
*/
int deleteZoneObjectDictByIds(Long[] ids);
/**
* 删除某对象的全部字典绑定(全量替换时使用)
*/
int deleteByObject(@Param("objectType") String objectType, @Param("objectId") Long objectId);
/**
* 根据多个字典条件查询对象主键列表AND 逻辑
* 返回同时满足所有字典条件的对象 ID
*/
List<Long> selectObjectIdsByDictFilters(@Param("objectType") String objectType,
@Param("dictFilters") List<DictFilterVo> dictFilters);
/**
* 批量查询字典类型名称( sys_dict_type)返回 dict_type / dict_name
*/
List<Map<String, String>> selectDictTypeNamesByTypes(@Param("dictTypes") List<String> dictTypes);
}

View File

@ -0,0 +1,69 @@
package com.microservices.zone.extension.service;
import com.alibaba.fastjson2.JSONObject;
import com.microservices.zone.extension.domain.ZoneObjectDict;
import com.microservices.zone.extension.domain.vo.DictFilterVo;
import com.microservices.zone.extension.domain.vo.ZoneObjectDictInputVo;
import com.microservices.zone.specialProject.domain.vo.CommonTypeVo;
import java.util.List;
/**
* 专区对象-字典扩展关联Service接口
*
* @author otto
*/
public interface IZoneObjectDictService {
/**
* 按主键查询(含字典标签解析)
*/
ZoneObjectDict selectZoneObjectDictById(Long id);
/**
* 条件查询列表(含字典标签解析)
*/
List<ZoneObjectDict> selectZoneObjectDictList(ZoneObjectDict zoneObjectDict);
/**
* 查询某对象的全部字典绑定(含字典标签解析)
*/
List<ZoneObjectDict> selectByObject(String objectType, Long objectId);
/**
* 反查某字典数据关联的对象主键列表
*/
List<Long> selectObjectIdsByDict(String objectType, String dictType, String dictValue);
/**
* 全量保存某对象的字典绑定(先删后插事务)bindings 为空表示清空
*
* @return 实际写入的绑定数量
*/
int saveObjectDicts(ZoneObjectDictInputVo inputVo);
/**
* 批量删除
*/
int deleteZoneObjectDictByIds(Long[] ids);
/**
* 按主键删除
*/
int deleteZoneObjectDictById(Long id);
/**
* 对象类型列表
*/
List<CommonTypeVo> selectObjectTypeList();
/**
* 根据多个字典条件查询对象主键列表AND 逻辑
*/
List<Long> selectObjectIdsByDictFilters(String objectType, List<DictFilterVo> dictFilters);
/**
* 批量查询多个对象的字典属性(含标签解析)返回精简 JSONObject 列表
* 每条含objectId / dictType / dictValue / dictLabel
*/
List<JSONObject> selectDictAttributesByObjectIds(String objectType, List<Long> objectIds);
}

View File

@ -0,0 +1,300 @@
package com.microservices.zone.extension.service.impl;
import com.alibaba.fastjson2.JSONObject;
import com.microservices.common.core.exception.ServiceException;
import com.microservices.common.core.utils.DateUtils;
import com.microservices.common.core.utils.StringUtils;
import com.microservices.common.security.utils.DictUtils;
import com.microservices.common.security.utils.SecurityUtils;
import com.microservices.system.api.domain.SysDictData;
import com.microservices.zone.extension.domain.ZoneObjectDict;
import com.microservices.zone.extension.domain.ZoneObjectDictTypeEnum;
import com.microservices.zone.extension.domain.vo.DictFilterVo;
import com.microservices.zone.extension.domain.vo.ZoneObjectDictBindingVo;
import com.microservices.zone.extension.domain.vo.ZoneObjectDictInputVo;
import com.microservices.zone.extension.mapper.ZoneObjectDictMapper;
import com.microservices.zone.extension.service.IZoneObjectDictService;
import com.microservices.zone.specialProject.domain.vo.CommonTypeVo;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors;
/**
* 专区对象-字典扩展关联Service业务层处理
*
* @author otto
*/
@Service
public class ZoneObjectDictServiceImpl implements IZoneObjectDictService {
@Autowired
private ZoneObjectDictMapper zoneObjectDictMapper;
@Override
public ZoneObjectDict selectZoneObjectDictById(Long id) {
ZoneObjectDict zoneObjectDict = zoneObjectDictMapper.selectZoneObjectDictById(id);
if (zoneObjectDict == null) {
throw new ServiceException("该对象字典绑定不存在或无权限访问(主键id[" + id + "])");
}
resolveDictLabel(zoneObjectDict);
return zoneObjectDict;
}
@Override
public List<ZoneObjectDict> selectZoneObjectDictList(ZoneObjectDict zoneObjectDict) {
List<ZoneObjectDict> list = zoneObjectDictMapper.selectZoneObjectDictList(zoneObjectDict);
resolveDictLabels(list);
return list;
}
@Override
public List<ZoneObjectDict> selectByObject(String objectType, Long objectId) {
checkObjectType(objectType);
if (objectId == null) {
throw new ServiceException("对象主键不能为空");
}
List<ZoneObjectDict> list = zoneObjectDictMapper.selectByObject(objectType, objectId);
resolveDictLabels(list);
return list;
}
@Override
public List<Long> selectObjectIdsByDict(String objectType, String dictType, String dictValue) {
checkObjectType(objectType);
if (StringUtils.isEmpty(dictType) || StringUtils.isEmpty(dictValue)) {
throw new ServiceException("字典类型Key与字典数据键值不能为空");
}
return zoneObjectDictMapper.selectObjectIdsByDict(objectType, dictType, dictValue);
}
@Override
@Transactional(rollbackFor = Exception.class)
public int saveObjectDicts(ZoneObjectDictInputVo inputVo) {
String objectType = inputVo.getObjectType();
Long objectId = inputVo.getObjectId();
checkObjectType(objectType);
if (objectId == null) {
throw new ServiceException("对象主键不能为空");
}
List<ZoneObjectDictBindingVo> bindings = inputVo.getBindings();
Set<String> seen = new HashSet<>();
List<ZoneObjectDict> toInsert = new ArrayList<>();
if (bindings != null) {
for (ZoneObjectDictBindingVo binding : bindings) {
String dictType = binding.getDictType();
String dictValue = binding.getDictValue();
if (StringUtils.isEmpty(dictType) || StringUtils.isEmpty(dictValue)) {
throw new ServiceException("字典绑定项的字典类型Key与字典数据键值不能为空");
}
// 忽略同一字典数据的重复绑定
if (!seen.add(dictType + ":" + dictValue)) {
continue;
}
// 校验字典数据在缓存中真实存在
checkDictData(dictType, dictValue);
ZoneObjectDict row = new ZoneObjectDict();
row.setObjectType(objectType);
row.setObjectId(objectId);
row.setDictType(dictType);
row.setDictValue(dictValue);
row.setZoneId(inputVo.getZoneId());
row.setDeptId(inputVo.getDeptId());
row.setRemark(inputVo.getRemark());
row.setDelFlag("0");
row.setCreateBy(SecurityUtils.getUsername());
row.setCreateTime(DateUtils.getNowDate());
toInsert.add(row);
}
}
// 全量替换先删除该对象的全部绑定再批量插入
zoneObjectDictMapper.deleteByObject(objectType, objectId);
if (!toInsert.isEmpty()) {
zoneObjectDictMapper.batchInsertZoneObjectDict(toInsert);
}
return toInsert.size();
}
@Override
public int deleteZoneObjectDictByIds(Long[] ids) {
return zoneObjectDictMapper.deleteZoneObjectDictByIds(ids);
}
@Override
public int deleteZoneObjectDictById(Long id) {
return zoneObjectDictMapper.deleteZoneObjectDictById(id);
}
@Override
public List<CommonTypeVo> selectObjectTypeList() {
return ZoneObjectDictTypeEnum.getAllToList();
}
@Override
public List<Long> selectObjectIdsByDictFilters(String objectType, List<DictFilterVo> dictFilters) {
checkObjectType(objectType);
if (dictFilters == null || dictFilters.isEmpty()) {
return Collections.emptyList();
}
// 过滤掉字典数据不存在的筛选条件静默跳过不抛出异常
List<DictFilterVo> validFilters = new ArrayList<>();
for (DictFilterVo filter : dictFilters) {
if (isDictDataExists(filter.getDictType(), filter.getDictValue())) {
validFilters.add(filter);
}
}
// 如果所有字典条件都不存在返回空列表
if (validFilters.isEmpty()) {
return Collections.emptyList();
}
return zoneObjectDictMapper.selectObjectIdsByDictFilters(objectType, validFilters);
}
@Override
public List<JSONObject> selectDictAttributesByObjectIds(String objectType, List<Long> objectIds) {
checkObjectType(objectType);
if (objectIds == null || objectIds.isEmpty()) {
return new ArrayList<>();
}
List<ZoneObjectDict> list = zoneObjectDictMapper.selectByObjectIds(objectType, objectIds);
resolveDictLabels(list);
return toDictAttributeJsonList(list);
}
/**
* ZoneObjectDict 转为精简 JSONObject(只含 objectId/dictType/dictValue/dictLabel/dictTypeName
* 不暴露 deptId/zoneId/createBy 等内部字段)
*/
private List<JSONObject> toDictAttributeJsonList(List<ZoneObjectDict> list) {
List<JSONObject> result = new ArrayList<>(list.size());
for (ZoneObjectDict zod : list) {
JSONObject j = new JSONObject();
j.put("objectId", zod.getObjectId());
j.put("dictType", zod.getDictType());
j.put("dictLabel", zod.getDictLabel());
j.put("dictValue", zod.getDictValue());
j.put("dictTypeName", zod.getDictTypeName());
result.add(j);
}
return result;
}
// ===== private helpers =====
private void checkObjectType(String objectType) {
if (!ZoneObjectDictTypeEnum.isValid(objectType)) {
throw new ServiceException("不支持的对象类型(对象类型[%s])", objectType);
}
}
/**
* 校验字典数据在 DictUtils 缓存中真实存在( dictType 取列表再按 dictValue 过滤)
*/
private void checkDictData(String dictType, String dictValue) {
List<SysDictData> dictDataList = DictUtils.getDictCache(dictType);
if (dictDataList == null || dictDataList.isEmpty()) {
throw new ServiceException("字典类型[%s]的字典数据缓存不存在,请先在系统字典中维护并刷新缓存", dictType);
}
boolean exists = dictDataList.stream().anyMatch(x -> dictValue.equals(x.getDictValue()));
if (!exists) {
throw new ServiceException("字典数据不存在(字典类型[%s],字典键值[%s])", dictType, dictValue);
}
}
/**
* 检查字典数据是否存在不抛出异常
* @param dictType 字典类型Key
* @param dictValue 字典数据键值
* @return true-存在false-不存在
*/
private boolean isDictDataExists(String dictType, String dictValue) {
if (StringUtils.isEmpty(dictType) || StringUtils.isEmpty(dictValue)) {
return false;
}
List<SysDictData> dictDataList = DictUtils.getDictCache(dictType);
if (dictDataList == null || dictDataList.isEmpty()) {
return false;
}
return dictDataList.stream().anyMatch(x -> dictValue.equals(x.getDictValue()));
}
/**
* 解析单条绑定的字典标签( DictUtils 缓存 dictValue 过滤)
*/
private void resolveDictLabel(ZoneObjectDict zoneObjectDict) {
if (zoneObjectDict == null
|| StringUtils.isEmpty(zoneObjectDict.getDictType())
|| StringUtils.isEmpty(zoneObjectDict.getDictValue())) {
return;
}
List<SysDictData> dictDataList = DictUtils.getDictCache(zoneObjectDict.getDictType());
if (dictDataList == null) {
return;
}
dictDataList.stream()
.filter(x -> zoneObjectDict.getDictValue().equals(x.getDictValue()))
.findFirst()
.ifPresent(d -> zoneObjectDict.setDictLabel(d.getDictLabel()));
Map<String, String> typeNameMap = loadDictTypeNames(Collections.singleton(zoneObjectDict.getDictType()));
zoneObjectDict.setDictTypeName(typeNameMap.get(zoneObjectDict.getDictType()));
}
private void resolveDictLabels(List<ZoneObjectDict> list) {
if (list == null || list.isEmpty()) {
return;
}
// dictType 去重每种只取一次缓存构建 (dictType|dictValue) -> SysDictData 查表
Set<String> dictTypes = list.stream()
.map(ZoneObjectDict::getDictType)
.filter(StringUtils::isNotEmpty)
.collect(Collectors.toSet());
Map<String, SysDictData> lookup = new HashMap<>();
for (String dictType : dictTypes) {
List<SysDictData> dictDataList = DictUtils.getDictCache(dictType);
if (dictDataList == null) {
continue;
}
for (SysDictData d : dictDataList) {
lookup.put(dictType + "|" + d.getDictValue(), d);
}
}
// 批量加载字典类型名称
Map<String, String> typeNameMap = loadDictTypeNames(dictTypes);
for (ZoneObjectDict zod : list) {
if (zod.getDictType() == null || StringUtils.isEmpty(zod.getDictValue())) {
continue;
}
SysDictData d = lookup.get(zod.getDictType() + "|" + zod.getDictValue());
if (d != null) {
zod.setDictLabel(d.getDictLabel());
}
zod.setDictTypeName(typeNameMap.get(zod.getDictType()));
}
}
/**
* 批量加载字典类型名称( sys_dict_type)返回 dictType -> dictName
*/
private Map<String, String> loadDictTypeNames(Set<String> dictTypes) {
if (dictTypes == null || dictTypes.isEmpty()) {
return Collections.emptyMap();
}
List<Map<String, String>> rows = zoneObjectDictMapper.selectDictTypeNamesByTypes(new ArrayList<>(dictTypes));
Map<String, String> map = new HashMap<>();
for (Map<String, String> row : rows) {
map.put(row.get("dict_type"), row.get("dict_name"));
}
return map;
}
}

View File

@ -0,0 +1,192 @@
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.microservices.zone.extension.mapper.ZoneObjectDictMapper">
<resultMap type="com.microservices.zone.extension.domain.ZoneObjectDict" id="ZoneObjectDictResult">
<result property="id" column="id"/>
<result property="objectType" column="object_type"/>
<result property="objectId" column="object_id"/>
<result property="dictType" column="dict_type"/>
<result property="dictValue" column="dict_value"/>
<result property="sort" column="sort"/>
<result property="zoneId" column="zone_id"/>
<result property="deptId" column="dept_id"/>
<result property="delFlag" column="del_flag"/>
<result property="createBy" column="create_by"/>
<result property="createTime" column="create_time"/>
<result property="updateBy" column="update_by"/>
<result property="updateTime" column="update_time"/>
<result property="remark" column="remark"/>
</resultMap>
<sql id="selectZoneObjectDictVo">
select id, object_type, object_id, dict_type, dict_value,
sort, zone_id, dept_id, del_flag,
create_by, create_time, update_by, update_time, remark
from zone_object_dict
</sql>
<select id="selectZoneObjectDictList"
parameterType="com.microservices.zone.extension.domain.ZoneObjectDict"
resultMap="ZoneObjectDictResult">
<include refid="selectZoneObjectDictVo"/>
<where>
<if test="objectType != null and objectType != ''">and object_type = #{objectType}</if>
<if test="objectId != null">and object_id = #{objectId}</if>
<if test="dictType != null and dictType != ''">and dict_type = #{dictType}</if>
<if test="dictValue != null and dictValue != ''">and dict_value = #{dictValue}</if>
<if test="zoneId != null">and zone_id = #{zoneId}</if>
<if test="deptId != null">and dept_id = #{deptId}</if>
</where>
order by id desc
</select>
<select id="selectZoneObjectDictById" parameterType="Long" resultMap="ZoneObjectDictResult">
<include refid="selectZoneObjectDictVo"/>
where id = #{id}
</select>
<select id="selectByObject" resultMap="ZoneObjectDictResult">
<include refid="selectZoneObjectDictVo"/>
where object_type = #{objectType} and object_id = #{objectId}
order by dict_type asc, id asc
</select>
<select id="selectByObjectIds" resultMap="ZoneObjectDictResult">
<include refid="selectZoneObjectDictVo"/>
where object_type = #{objectType} and object_id in
<foreach collection="objectIds" item="objectId" open="(" separator="," close=")">
#{objectId}
</foreach>
order by object_id asc, dict_type asc, id asc
</select>
<select id="selectObjectIdsByDict" resultType="long">
select object_id
from zone_object_dict
where object_type = #{objectType}
and dict_type = #{dictType}
and dict_value = #{dictValue}
order by object_id asc
</select>
<insert id="insertZoneObjectDict"
parameterType="com.microservices.zone.extension.domain.ZoneObjectDict"
useGeneratedKeys="true"
keyProperty="id">
insert into zone_object_dict
<trim prefix="(" suffix=")" suffixOverrides=",">
<if test="objectType != null and objectType != ''">object_type,</if>
<if test="objectId != null">object_id,</if>
<if test="dictType != null and dictType != ''">dict_type,</if>
<if test="dictValue != null and dictValue != ''">dict_value,</if>
<if test="sort != null">sort,</if>
<if test="zoneId != null">zone_id,</if>
<if test="deptId != null">dept_id,</if>
<if test="delFlag != null">del_flag,</if>
<if test="createBy != null">create_by,</if>
<if test="createTime != null">create_time,</if>
<if test="updateBy != null">update_by,</if>
<if test="updateTime != null">update_time,</if>
<if test="remark != null and remark != ''">remark,</if>
</trim>
<trim prefix="values (" suffix=")" suffixOverrides=",">
<if test="objectType != null and objectType != ''">#{objectType},</if>
<if test="objectId != null">#{objectId},</if>
<if test="dictType != null and dictType != ''">#{dictType},</if>
<if test="dictValue != null and dictValue != ''">#{dictValue},</if>
<if test="sort != null">#{sort},</if>
<if test="zoneId != null">#{zoneId},</if>
<if test="deptId != null">#{deptId},</if>
<if test="delFlag != null">#{delFlag},</if>
<if test="createBy != null">#{createBy},</if>
<if test="createTime != null">#{createTime},</if>
<if test="updateBy != null">#{updateBy},</if>
<if test="updateTime != null">#{updateTime},</if>
<if test="remark != null and remark != ''">#{remark},</if>
</trim>
</insert>
<insert id="batchInsertZoneObjectDict" parameterType="java.util.List">
insert into zone_object_dict
(object_type, object_id, dict_type, dict_value, sort, zone_id, dept_id, del_flag, create_by, create_time,
update_by, update_time, remark)
values
<foreach collection="list" item="item" separator=",">
(#{item.objectType}, #{item.objectId}, #{item.dictType}, #{item.dictValue}, #{item.sort}, #{item.zoneId},
#{item.deptId}, #{item.delFlag}, #{item.createBy}, #{item.createTime}, #{item.updateBy},
#{item.updateTime}, #{item.remark})
</foreach>
</insert>
<update id="updateZoneObjectDict"
parameterType="com.microservices.zone.extension.domain.ZoneObjectDict">
update zone_object_dict
<trim prefix="SET" suffixOverrides=",">
<if test="objectType != null and objectType != ''">object_type = #{objectType},</if>
<if test="objectId != null">object_id = #{objectId},</if>
<if test="dictType != null and dictType != ''">dict_type = #{dictType},</if>
<if test="dictValue != null and dictValue != ''">dict_value = #{dictValue},</if>
<if test="sort != null">sort = #{sort},</if>
<if test="zoneId != null">zone_id = #{zoneId},</if>
<if test="deptId != null">dept_id = #{deptId},</if>
<if test="delFlag != null">del_flag = #{delFlag},</if>
<if test="createBy != null">create_by = #{createBy},</if>
<if test="createTime != null">create_time = #{createTime},</if>
<if test="updateBy != null">update_by = #{updateBy},</if>
<if test="updateTime != null">update_time = #{updateTime},</if>
<if test="remark != null">remark = #{remark},</if>
</trim>
where id = #{id}
</update>
<delete id="deleteZoneObjectDictById" parameterType="Long">
delete
from zone_object_dict
where id = #{id}
</delete>
<delete id="deleteZoneObjectDictByIds" parameterType="String">
delete from zone_object_dict where id in
<foreach item="id" collection="array" open="(" separator="," close=")">
#{id}
</foreach>
</delete>
<delete id="deleteByObject">
delete
from zone_object_dict
where object_type = #{objectType}
and object_id = #{objectId}
</delete>
<select id="selectObjectIdsByDictFilters" resultType="long">
SELECT DISTINCT object_id
FROM zone_object_dict
WHERE object_type = #{objectType}
AND del_flag = '0'
<foreach collection="dictFilters" item="filter" separator="">
AND EXISTS (
SELECT 1 FROM zone_object_dict zod2
WHERE zod2.object_type = #{objectType}
AND zod2.object_id = zone_object_dict.object_id
AND zod2.dict_type = #{filter.dictType}
AND zod2.dict_value = #{filter.dictValue}
AND zod2.del_flag = '0'
)
</foreach>
ORDER BY object_id ASC
</select>
<select id="selectDictTypeNamesByTypes" resultType="java.util.Map">
select dict_type, dict_name
from sys_dict_type
where dict_type in
<foreach collection="dictTypes" item="dt" open="(" separator="," close=")">
#{dt}
</foreach>
</select>
</mapper>

View File

@ -0,0 +1,26 @@
-- 特色专区对象-字典扩展关联表
-- 说明zone 模块未集成数据库迁移框架,此文件仅作建表参考,需在目标库手动执行。
-- 字典类型/数据来源于 system 微服务的 sys_dict_type / sys_dict_data
-- 本表通过 dict_type(String键) + dict_value(sys_dict_data.dict_value) 关联,解析标签时复用 DictUtils 的 Redis 缓存。
CREATE TABLE IF NOT EXISTS zone_object_dict
(
id BIGINT NOT NULL AUTO_INCREMENT COMMENT '主键',
object_type VARCHAR(32) NOT NULL COMMENT '对象类型(CMS_DOC文章/RESOURCE资源/MEMBER会员/PROJECT项目)',
object_id BIGINT NOT NULL COMMENT '对象主键(如 cms_doc.id / zone_resource.id 等)',
dict_type VARCHAR(100) NOT NULL COMMENT '关联字典类型Key(sys_dict_type.dict_type)',
dict_value VARCHAR(100) NOT NULL COMMENT '关联字典数据键值(sys_dict_data.dict_value)',
sort INT DEFAULT NULL COMMENT '排序',
zone_id BIGINT DEFAULT NULL COMMENT '专区标识',
dept_id BIGINT DEFAULT NULL COMMENT '所属组织Id',
del_flag CHAR(1) DEFAULT '0' COMMENT '删除标志(0存在 2删除)',
create_by VARCHAR(64) DEFAULT '' COMMENT '创建者',
create_time DATETIME DEFAULT NULL COMMENT '创建时间',
update_by VARCHAR(64) DEFAULT '' COMMENT '更新者',
update_time DATETIME DEFAULT NULL COMMENT '更新时间',
remark VARCHAR(500) DEFAULT NULL COMMENT '备注',
PRIMARY KEY (id),
UNIQUE KEY uk_object_dict (object_type, object_id, dict_type, dict_value),
KEY idx_object (object_type, object_id),
KEY idx_dict (dict_type, dict_value)
) ENGINE = InnoDB
DEFAULT CHARSET = utf8mb4 COMMENT ='特色专区对象-字典扩展关联表';