From 02dda2d5975f628e48b0f05eb84bde5d4425220f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=BC=A0=E5=AE=B6=E8=AE=AD?= Date: Mon, 19 Oct 2026 18:45:49 +0800 Subject: [PATCH] =?UTF-8?q?feat(public-todo):=20=E6=96=B0=E5=A2=9E?= =?UTF-8?q?=E5=85=8D=E7=99=BB=E5=BD=95=E5=85=AC=E5=BC=80=E5=BE=85=E5=8A=9E?= =?UTF-8?q?=E7=9C=8B=E6=9D=BF=E5=90=8E=E7=AB=AF?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 新增表 t_public_todo 及 Entity/Mapper/Service/Controller 四个类 - 对外接口 7 个:list/add/update/updateStatus/remove/removeDone/stats - Controller 不继承 BaseController,不依赖登录态 - WebSecurityConfig 放行 /public-todo/**,与个人待办 /todo/** 隔离 - 落库脚本 sql/public_todo.sql(幂等建表 + 示例数据) --- sql/public_todo.sql | 40 +++ .../admin/config/WebSecurityConfig.java | 3 + .../controller/PublicTodoController.java | 235 ++++++++++++++++++ .../com/freedom/admin/entity/PublicTodo.java | 90 +++++++ .../admin/mapper/PublicTodoMapper.java | 13 + .../admin/service/PublicTodoService.java | 13 + .../service/impl/PublicTodoServiceImpl.java | 19 ++ 7 files changed, 413 insertions(+) create mode 100644 sql/public_todo.sql create mode 100644 src/main/java/com/freedom/admin/controller/PublicTodoController.java create mode 100644 src/main/java/com/freedom/admin/entity/PublicTodo.java create mode 100644 src/main/java/com/freedom/admin/mapper/PublicTodoMapper.java create mode 100644 src/main/java/com/freedom/admin/service/PublicTodoService.java create mode 100644 src/main/java/com/freedom/admin/service/impl/PublicTodoServiceImpl.java diff --git a/sql/public_todo.sql b/sql/public_todo.sql new file mode 100644 index 0000000..4bb361a --- /dev/null +++ b/sql/public_todo.sql @@ -0,0 +1,40 @@ +-- ============================================================= +-- 公开待办看板(无需登录)增量脚本 +-- 用途: 对外分享的待办清单页面数据源,独立于个人待办表 t_todo +-- 特性: 幂等,可重复执行 +-- 执行: freedom_db +-- ============================================================= + +-- 1. 公开待办表 +CREATE TABLE IF NOT EXISTS `t_public_todo` ( + `id` BIGINT NOT NULL AUTO_INCREMENT COMMENT '主键', + `title` VARCHAR(200) NOT NULL COMMENT '标题', + `remark` VARCHAR(1000) DEFAULT NULL COMMENT '备注', + `priority` TINYINT DEFAULT 2 COMMENT '优先级:1低 2中 3高', + `category` VARCHAR(50) DEFAULT NULL COMMENT '分类', + `due_date` DATE DEFAULT NULL COMMENT '截止日期', + `status` TINYINT DEFAULT 0 COMMENT '状态:0未完成 1已完成', + `create_by` VARCHAR(50) DEFAULT NULL COMMENT '创建者', + `create_date` DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', + `update_by` VARCHAR(50) DEFAULT NULL COMMENT '更新者', + `update_date` DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', + `del_flag` TINYINT DEFAULT 0 COMMENT '删除标记:0正常 1已删除', + PRIMARY KEY (`id`), + KEY `idx_public_todo_status` (`status`, `del_flag`), + KEY `idx_public_todo_due` (`due_date`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='公开待办看板表'; + +-- 2. 示例数据(仅当表为空时插入,便于首次打开不空白) +INSERT INTO `t_public_todo` (`title`, `remark`, `priority`, `category`, `due_date`, `status`, `create_by`) +SELECT * FROM ( + SELECT '欢迎使用公开待办看板' AS title, + '这个页面无需登录,链接发给谁都能用。点左侧圆圈可以勾选完成。' AS remark, + 2 AS priority, '想法' AS category, NULL AS due_date, 0 AS status, 'system' AS create_by + UNION ALL + SELECT '试试按 N 快速新建、按 / 搜索', '快捷键在底部有提示,手机端会自动隐藏。', + 1, '想法', NULL, 0, 'system' + UNION ALL + SELECT '数据保存在服务器数据库', '所有人看到的是同一份清单,支持导出/导入 JSON 备份。', + 3, '工作', DATE_ADD(CURDATE(), INTERVAL 3 DAY), 0, 'system' +) t +WHERE NOT EXISTS (SELECT 1 FROM `t_public_todo`); diff --git a/src/main/java/com/freedom/admin/config/WebSecurityConfig.java b/src/main/java/com/freedom/admin/config/WebSecurityConfig.java index b00e6c0..7043969 100644 --- a/src/main/java/com/freedom/admin/config/WebSecurityConfig.java +++ b/src/main/java/com/freedom/admin/config/WebSecurityConfig.java @@ -84,6 +84,9 @@ public class WebSecurityConfig extends WebSecurityConfigurerAdapter { // 待办清单:多人数据隔离,必须登录后访问 .antMatchers("/todo/**") .authenticated() + // 公开待办看板:对外分享,无需登录 + .antMatchers("/public-todo/**") + .permitAll() .and() .formLogin() .loginProcessingUrl("/login") diff --git a/src/main/java/com/freedom/admin/controller/PublicTodoController.java b/src/main/java/com/freedom/admin/controller/PublicTodoController.java new file mode 100644 index 0000000..c561ac0 --- /dev/null +++ b/src/main/java/com/freedom/admin/controller/PublicTodoController.java @@ -0,0 +1,235 @@ +package com.freedom.admin.controller; + +import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; +import com.baomidou.mybatisplus.core.conditions.update.UpdateWrapper; +import com.freedom.admin.common.Result; +import com.freedom.admin.entity.PublicTodo; +import com.freedom.admin.service.PublicTodoService; +import lombok.extern.log4j.Log4j2; +import org.apache.commons.lang3.StringUtils; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import java.util.Date; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +/** + * 控制层-公开待办看板 + * + * @author zhangjiaxun + * @description 对外分享的待办清单,无需登录即可访问,全体访问者共享同一份清单。 + * 与个人待办 t_todo 完全独立,互不影响。 + */ +@RestController +@RequestMapping("public-todo") +@Log4j2 +public class PublicTodoController { + + /** + * 操作人标识(公开页面无登录态,统一记录为 guest) + */ + private static final String OPERATOR = "guest"; + + /** + * 单次返回上限,防止数据量过大拖慢页面 + */ + private static final int MAX_ROWS = 500; + + @Autowired + private PublicTodoService publicTodoService; + + /** + * 查询公开看板全部待办 + * + * @param status 状态筛选:0未完成 1已完成,null 为全部 + * @param priority 优先级筛选:1低 2中 3高 + * @param category 分类筛选 + * @param keyword 标题/备注关键字 + * @param sort 排序:created 最新创建 / priority 优先级 / due 截止日期 / title 标题 + */ + @RequestMapping("list") + public Result> list(Integer status, Integer priority, String category, + String keyword, String sort) { + try { + QueryWrapper queryWrapper = new QueryWrapper<>(); + queryWrapper.eq("del_flag", 0); + queryWrapper.eq(status != null, "status", status); + queryWrapper.eq(priority != null, "priority", priority); + queryWrapper.eq(StringUtils.isNotBlank(category), "category", category); + if (StringUtils.isNotBlank(keyword)) { + String kw = keyword.trim(); + queryWrapper.and(w -> w.like("title", kw).or().like("remark", kw)); + } + // 排序:由于列表合并了未完成/已完成,统一在服务端排序,前端不再二次处理 + if ("priority".equals(sort)) { + queryWrapper.orderByDesc("priority").orderByAsc("status").orderByDesc("create_date"); + } else if ("due".equals(sort)) { + // 无截止日期的排在最后 + queryWrapper.orderByAsc("due_date IS NULL").orderByAsc("due_date").orderByAsc("status"); + } else if ("title".equals(sort)) { + queryWrapper.orderByAsc("title"); + } else { + queryWrapper.orderByAsc("status").orderByDesc("create_date"); + } + queryWrapper.last("LIMIT " + MAX_ROWS); + List list = publicTodoService.list(queryWrapper); + return Result.ok().setData(list); + } catch (Exception e) { + log.error("查询公开待办失败", e); + return Result.error().message("查询失败"); + } + } + + /** + * 新增公开待办 + */ + @RequestMapping("add") + public Result add(PublicTodo publicTodo) { + if (StringUtils.isBlank(publicTodo.getTitle())) { + return Result.error().message("标题不能为空"); + } + try { + publicTodo.setId(null); + if (publicTodo.getStatus() == null) { + publicTodo.setStatus(0); + } + if (publicTodo.getPriority() == null) { + publicTodo.setPriority(2); + } + publicTodo.setCreateBy(OPERATOR); + publicTodo.setCreateDate(new Date()); + publicTodo.setUpdateBy(OPERATOR); + publicTodo.setUpdateDate(new Date()); + boolean save = publicTodoService.save(publicTodo); + return Result.ok().setData(save); + } catch (Exception e) { + log.error("新增公开待办失败", e); + return Result.error().message("新增失败"); + } + } + + /** + * 更新公开待办(标题 / 备注 / 优先级 / 分类 / 截止日期 / 状态) + */ + @RequestMapping("update") + public Result update(PublicTodo publicTodo) { + if (publicTodo.getId() == null) { + return Result.error().message("id 不能为空"); + } + if (StringUtils.isBlank(publicTodo.getTitle())) { + return Result.error().message("标题不能为空"); + } + try { + publicTodo.setUpdateBy(OPERATOR); + publicTodo.setUpdateDate(new Date()); + publicTodo.setCreateBy(null); + publicTodo.setCreateDate(null); + boolean update = publicTodoService.updateById(publicTodo); + return Result.ok().setData(update); + } catch (Exception e) { + log.error("更新公开待办失败", e); + return Result.error().message("更新失败"); + } + } + + /** + * 切换完成状态 + */ + @RequestMapping("updateStatus") + public Result updateStatus(Long id, Integer status) { + if (id == null) { + return Result.error().message("id 不能为空"); + } + if (status == null) { + return Result.error().message("status 不能为空"); + } + try { + UpdateWrapper updateWrapper = new UpdateWrapper<>(); + updateWrapper.eq("id", id).eq("del_flag", 0); + updateWrapper.set("status", status); + updateWrapper.set("update_by", OPERATOR); + updateWrapper.set("update_date", new Date()); + boolean update = publicTodoService.update(updateWrapper); + return Result.ok().setData(update); + } catch (Exception e) { + log.error("更新公开待办状态失败", e); + return Result.error().message("更新失败"); + } + } + + /** + * 删除公开待办(逻辑删除) + */ + @RequestMapping("remove") + public Result remove(Long id) { + if (id == null) { + return Result.error().message("id 不能为空"); + } + try { + boolean remove = publicTodoService.removeById(id); + return Result.ok().setData(remove); + } catch (Exception e) { + log.error("删除公开待办失败", e); + return Result.error().message("删除失败"); + } + } + + /** + * 批量删除已完成的待办(逻辑删除) + */ + @RequestMapping("removeDone") + public Result removeDone() { + try { + UpdateWrapper updateWrapper = new UpdateWrapper<>(); + updateWrapper.eq("status", 1).eq("del_flag", 0); + updateWrapper.set("del_flag", 1); + updateWrapper.set("update_by", OPERATOR); + updateWrapper.set("update_date", new Date()); + boolean update = publicTodoService.update(updateWrapper); + return Result.ok().setData(update); + } catch (Exception e) { + log.error("批量删除已完成待办失败", e); + return Result.error().message("清理失败"); + } + } + + /** + * 看板统计(总数 / 未完成 / 已完成 / 已逾期 / 分类分布) + */ + @RequestMapping("stats") + public Result> stats() { + try { + QueryWrapper queryWrapper = new QueryWrapper<>(); + queryWrapper.eq("del_flag", 0); + List list = publicTodoService.list(queryWrapper); + + int total = list.size(); + int done = 0; + int overdue = 0; + Map byCategory = new HashMap<>(); + Date today = new Date(); + for (PublicTodo item : list) { + if (item.getStatus() != null && item.getStatus() == 1) { + done++; + } else if (item.getDueDate() != null && item.getDueDate().before(today)) { + overdue++; + } + String cat = StringUtils.isBlank(item.getCategory()) ? "未分类" : item.getCategory(); + byCategory.merge(cat, 1, Integer::sum); + } + Map data = new HashMap<>(); + data.put("total", total); + data.put("done", done); + data.put("active", total - done); + data.put("overdue", overdue); + data.put("byCategory", byCategory); + return Result.ok().setData(data); + } catch (Exception e) { + log.error("公开待办统计失败", e); + return Result.error().message("统计失败"); + } + } +} diff --git a/src/main/java/com/freedom/admin/entity/PublicTodo.java b/src/main/java/com/freedom/admin/entity/PublicTodo.java new file mode 100644 index 0000000..e322279 --- /dev/null +++ b/src/main/java/com/freedom/admin/entity/PublicTodo.java @@ -0,0 +1,90 @@ +package com.freedom.admin.entity; + +import com.baomidou.mybatisplus.annotation.IdType; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import com.fasterxml.jackson.annotation.JsonFormat; +import lombok.Data; +import org.springframework.format.annotation.DateTimeFormat; + +import java.io.Serializable; +import java.util.Date; + +/** + * t_public_todo + * 公开待办看板(无需登录即可访问,全体访问者共享同一份清单) + * + * @author zhangjiaxun + */ +@Data +@TableName("t_public_todo") +public class PublicTodo implements Serializable { + + private static final long serialVersionUID = 1L; + + /** + * 主键 + */ + @TableId(type = IdType.AUTO) + private Long id; + + /** + * 标题 + */ + private String title; + + /** + * 备注 + */ + private String remark; + + /** + * 优先级:1低 2中 3高 + */ + private Integer priority; + + /** + * 分类 + */ + private String category; + + /** + * 截止日期 + */ + @DateTimeFormat(pattern = "yyyy-MM-dd") + @JsonFormat(pattern = "yyyy-MM-dd", timezone = "GMT+8") + private Date dueDate; + + /** + * 状态:0未完成 1已完成 + */ + private Integer status; + + /** + * 创建者 + */ + private String createBy; + + /** + * 创建时间 + */ + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8") + private Date createDate; + + /** + * 更新者 + */ + private String updateBy; + + /** + * 更新时间 + */ + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8") + private Date updateDate; + + /** + * 删除标记 + */ + private Boolean delFlag; + +} diff --git a/src/main/java/com/freedom/admin/mapper/PublicTodoMapper.java b/src/main/java/com/freedom/admin/mapper/PublicTodoMapper.java new file mode 100644 index 0000000..ce5aea2 --- /dev/null +++ b/src/main/java/com/freedom/admin/mapper/PublicTodoMapper.java @@ -0,0 +1,13 @@ +package com.freedom.admin.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.freedom.admin.entity.PublicTodo; + +/** + * 数据访问层-公开待办看板表 + * + * @author zhangjiaxun + */ +public interface PublicTodoMapper extends BaseMapper { + +} diff --git a/src/main/java/com/freedom/admin/service/PublicTodoService.java b/src/main/java/com/freedom/admin/service/PublicTodoService.java new file mode 100644 index 0000000..6c1d486 --- /dev/null +++ b/src/main/java/com/freedom/admin/service/PublicTodoService.java @@ -0,0 +1,13 @@ +package com.freedom.admin.service; + +import com.baomidou.mybatisplus.extension.service.IService; +import com.freedom.admin.entity.PublicTodo; + +/** + * 业务层-公开待办看板表 + * + * @author zhangjiaxun + */ +public interface PublicTodoService extends IService { + +} diff --git a/src/main/java/com/freedom/admin/service/impl/PublicTodoServiceImpl.java b/src/main/java/com/freedom/admin/service/impl/PublicTodoServiceImpl.java new file mode 100644 index 0000000..470a48c --- /dev/null +++ b/src/main/java/com/freedom/admin/service/impl/PublicTodoServiceImpl.java @@ -0,0 +1,19 @@ +package com.freedom.admin.service.impl; + +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import com.freedom.admin.entity.PublicTodo; +import com.freedom.admin.mapper.PublicTodoMapper; +import com.freedom.admin.service.PublicTodoService; +import lombok.extern.log4j.Log4j2; +import org.springframework.stereotype.Service; + +/** + * 业务层实现-公开待办看板表 + * + * @author zhangjiaxun + */ +@Service +@Log4j2 +public class PublicTodoServiceImpl extends ServiceImpl implements PublicTodoService { + +}