feat(public-todo): 新增免登录公开待办看板后端
CI/CD Deploy (Admin) / 构建后端镜像并部署 (push) Successful in 14s

- 新增表 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(幂等建表 + 示例数据)
This commit is contained in:
张家训
2026-10-19 18:45:49 +08:00
parent c0da4c4583
commit 02dda2d597
7 changed files with 413 additions and 0 deletions
+40
View File
@@ -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`);
@@ -84,6 +84,9 @@ public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
// 待办清单:多人数据隔离,必须登录后访问
.antMatchers("/todo/**")
.authenticated()
// 公开待办看板:对外分享,无需登录
.antMatchers("/public-todo/**")
.permitAll()
.and()
.formLogin()
.loginProcessingUrl("/login")
@@ -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<PublicTodo>> list(Integer status, Integer priority, String category,
String keyword, String sort) {
try {
QueryWrapper<PublicTodo> 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<PublicTodo> 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<PublicTodo> 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<PublicTodo> 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<Map<String, Object>> stats() {
try {
QueryWrapper<PublicTodo> queryWrapper = new QueryWrapper<>();
queryWrapper.eq("del_flag", 0);
List<PublicTodo> list = publicTodoService.list(queryWrapper);
int total = list.size();
int done = 0;
int overdue = 0;
Map<String, Integer> 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<String, Object> 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("统计失败");
}
}
}
@@ -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;
}
@@ -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<PublicTodo> {
}
@@ -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<PublicTodo> {
}
@@ -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<PublicTodoMapper, PublicTodo> implements PublicTodoService {
}