9 Commits

Author SHA1 Message Date
zhangjiaxun 264e60392d feat(tools): 新增 JSON 格式化菜单 SQL
CI/CD Deploy (Admin) / 构建后端镜像并部署 (push) Successful in 1m7s
沿用已验证的自增版式:
- parent_id 用子查询按 permission_code 定位「工具列表」,不硬编码 id
- 菜单 id 由 LAST_INSERT_ID() 经 SET 变量传给授权语句
- 授权用 INSERT...SELECT 覆盖全部有效角色
- 清理语句用 JOIN 精确定位,不误删他人菜单

已实测执行并校验:菜单树与授权均正确,超级管理员无遗漏项。
2026-10-20 23:00:38 +08:00
zhangjiaxun bf3939c415 fix(sql): 菜单SQL 改用自增分配 id,修复主键冲突
CI/CD Deploy (Admin) / 构建后端镜像并部署 (push) Successful in 1m6s
实测执行时发现硬编码 id 会撞主键:
- t_sys_permission 自增起点 1000,但 t_sys_role_permission 已单独用到 1005
- 写死 1005/1006 插入关联表时触发 Duplicate entry '1005'

改动:
- add_tools_text_diff.sql 改用 LAST_INSERT_ID() 取自增 id
- 授权改为 INSERT...SELECT,覆盖全部有效角色,不再写死 role_id=1
- 清理语句用 JOIN 按 permission_code 定位,避免误删他人菜单
- init.sql 同步修正 id,并补上遗漏的 user:getUserInfo 权限点(id=8)
- 两文件均已实测通过(含连续执行两次的幂等验证)
2026-10-20 22:40:36 +08:00
zhangjiaxun 4aba70b047 feat(tools): 补充工具列表菜单 SQL 并修正 init.sql 过时数据
CI/CD Deploy (Admin) / 构建后端镜像并部署 (push) Successful in 1m19s
- 新增 sql/add_tools_text_diff.sql:工具列表 + 文本比对菜单初始化脚本(幂等)
- 修正 init.sql 中已过时的 component 取值
  - 原值 sys/user/user-list 等与实际文件 sys/sys-user.vue 不符
  - 按线上真实数据更正为 /sys/sys-user、/sys/sys-role 等,并补齐待办菜单
- component 字段必须带前导斜杠,前端会拼成 import('@/view' + component + '.vue')
2026-10-20 22:28:32 +08:00
张家训 634ce4b2e4 chore(cleanup): 移除三角洲改枪码模块全套代码
CI/CD Deploy (Admin) / 构建后端镜像并部署 (push) Successful in 9s
- 删除 DeltaGunCodeController/Entity/Mapper/Service/Impl 与建表脚本
- WebSecurityConfig 移除 /delta-gun-code/** 白名单
- 配套数据库清理(已执行):菜单 1000/1001/1002、角色关联 3 行、
  delta_gun_code 表;备份在服务器 /opt/freedom/backup/*-20261020.sql
- 项目保留范围:系统管理 + 待办清单(含公开看板)
2026-10-20 17:21:53 +08:00
张家训 8d97510bc9 fix(public-todo): dueDate 改 String 映射,修复截止日期差一天
CI/CD Deploy (Admin) / 构建后端镜像并部署 (push) Successful in 23s
- 根因:Date 类型经 @DateTimeFormat 按本地时区解析为当日 00:00,
  再经 JDBC(serverTimezone=UTC)与 MySQL 时区换算后漂移一天
- dueDate 改为 String(yyyy-MM-dd),字符串往返不参与任何时区换算
- add/update 对空串统一转 null(DATE 列不接受空串)
- stats 逾期判断改字符串比较,与前端口径一致
- 实测:提交 2026-10-27 → 库存 2026-10-27 → 返回 2026-10-27,完全一致
2026-10-20 15:09:31 +08:00
张家训 6076895959 chore(public-todo): remark 字段升级为 TEXT 支持富文本
CI/CD Deploy (Admin) / 构建后端镜像并部署 (push) Successful in 7s
2026-10-20 09:03:40 +08:00
张家训 02dda2d597 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(幂等建表 + 示例数据)
2026-10-19 18:45:49 +08:00
zhangjiaxun c0da4c4583 fix(mp): 注册 MyBatis-Plus 分页拦截器,修复 page() 返回 total=0
CI/CD Deploy (Admin) / 构建后端镜像并部署 (push) Successful in 1m25s
- 新增 MybatisPlusConfig 注册 PaginationInnerInterceptor
- 修复分页查询只填充 records、total/pages 恒为 0 的问题
2026-10-16 23:56:09 +08:00
zhangjiaxun f07c1ae6b8 perf(ci): 后端镜像构建优化——腾讯云Maven镜像源+BuildKit缓存
CI/CD Deploy (Admin) / 构建后端镜像并部署 (push) Successful in 4m23s
- 镜像源从阿里云换成腾讯云(内网直连,解决依赖下载仅几十KB/s的问题)
- RUN 加 BuildKit cache mount 持久化 /root/.m2/repository,跨构建复用
- CI 构建步骤显式启用 DOCKER_BUILDKIT=1
2026-10-16 23:53:55 +08:00
18 changed files with 549 additions and 313 deletions
+1
View File
@@ -47,6 +47,7 @@ jobs:
- name: 构建后端镜像
run: |
set -euo pipefail
export DOCKER_BUILDKIT=1
docker build -t freedom-admin:latest /opt/freedom/workspace-admin
- name: 部署后端服务
+6 -6
View File
@@ -6,7 +6,7 @@
FROM maven:3.6.3-jdk-8 AS builder
WORKDIR /build
# 配置 Maven 国内镜像源,加速依赖下载
# 配置腾讯云镜像源(腾讯云服务器内网直连,速度远快于公网阿里云)
RUN mkdir -p /root/.m2 && \
echo '<?xml version="1.0" encoding="UTF-8"?>' > /root/.m2/settings.xml && \
echo '<settings xmlns="http://maven.apache.org/SETTINGS/1.0.0"' >> /root/.m2/settings.xml && \
@@ -14,10 +14,10 @@ RUN mkdir -p /root/.m2 && \
echo ' xsi:schemaLocation="http://maven.apache.org/SETTINGS/1.0.0 http://maven.apache.org/xsd/settings-1.0.0.xsd">' >> /root/.m2/settings.xml && \
echo ' <mirrors>' >> /root/.m2/settings.xml && \
echo ' <mirror>' >> /root/.m2/settings.xml && \
echo ' <id>aliyun</id>' >> /root/.m2/settings.xml && \
echo ' <id>tencent</id>' >> /root/.m2/settings.xml && \
echo ' <mirrorOf>central</mirrorOf>' >> /root/.m2/settings.xml && \
echo ' <name>Aliyun Maven</name>' >> /root/.m2/settings.xml && \
echo ' <url>https://maven.aliyun.com/repository/central</url>' >> /root/.m2/settings.xml && \
echo ' <name>Tencent Cloud Maven</name>' >> /root/.m2/settings.xml && \
echo ' <url>https://mirrors.cloud.tencent.com/nexus/repository/maven-public/</url>' >> /root/.m2/settings.xml && \
echo ' </mirror>' >> /root/.m2/settings.xml && \
echo ' </mirrors>' >> /root/.m2/settings.xml && \
echo '</settings>' >> /root/.m2/settings.xml
@@ -26,8 +26,8 @@ RUN mkdir -p /root/.m2 && \
COPY pom.xml .
COPY src ./src
# 打包(跳过测试),产物 target/freedom-admin-1.0.1.jar
RUN mvn -B clean package -DskipTests
# 打包(跳过测试),使用 BuildKit cache mount 持久化 Maven 仓库,跨构建复用
RUN --mount=type=cache,target=/root/.m2/repository mvn -B clean package -DskipTests
# ---------- 运行阶段 ----------
FROM openjdk:8-jre-slim
+64
View File
@@ -0,0 +1,64 @@
-- ============================================================
-- 「工具列表」下新增「JSON 格式化」菜单
--
-- 菜单由 t_sys_permission 表驱动,前端 formatMenuList() 会把 component
-- 拼成 import('@/view' + component + '.vue'),因此 component 必须带前导斜杠,
-- 且要与磁盘文件 src/view/tools/json-format.vue 对应
--
-- 幂等:可重复执行,已存在则先清理再插入
-- 执行库:freedom_db
--
-- 注意:id 由数据库自增分配,父菜单通过 permission_code 子查询定位,
-- 两者都不硬编码——避免与 t_sys_role_permission 的独立自增序列撞主键。
-- ============================================================
-- ---------- 1. 清理历史遗留 ----------
DELETE rp
FROM `t_sys_role_permission` rp
JOIN `t_sys_permission` p ON p.id = rp.permission_id
WHERE p.permission_code = 'tools:json-format';
DELETE FROM `t_sys_permission` WHERE `permission_code` = 'tools:json-format';
-- ---------- 2. 插入子菜单「JSON 格式化」 ----------
-- parent_id 用子查询取「工具列表」的 id,不写死数字
-- sort=2 排在「文本比对」(sort=1) 之后
-- icon md-brush 已从线上 CSS 校验存在
INSERT INTO `t_sys_permission`
(`parent_id`, `permission_code`, `permission_name`, `permission_desc`, `level`,
`icon`, `url`, `component`, `sort`, `create_by`, `create_date`, `del_flag`)
SELECT p.`id`, 'tools:json-format', 'JSON 格式化',
'JSON 美化、压缩与语法错误定位,支持超长整数原样保留',
2, 'md-brush', '/tools/json-format', '/tools/json-format',
2, 'admin', NOW(), 0
FROM `t_sys_permission` p
WHERE p.permission_code = 'tools';
-- ---------- 3. 授权给全部有效角色 ----------
-- 菜单 id 由上一条语句的插入值取得
SET @json_menu_id = (
SELECT `id` FROM `t_sys_permission` WHERE `permission_code` = 'tools:json-format' LIMIT 1
);
INSERT INTO `t_sys_role_permission` (`role_id`, `permission_id`, `create_by`, `create_date`)
SELECT r.`id`, @json_menu_id, 'admin', NOW()
FROM `t_sys_role` r
WHERE r.`del_flag` = 0
AND @json_menu_id IS NOT NULL;
-- ---------- 4. 校验 ----------
-- 预期 2 行:工具列表 + 文本比对 + JSON 格式化(level=2 的两条子菜单)
SELECT p.`id`, p.`parent_id`, p.`permission_code`, p.`permission_name`,
p.`level`, p.`icon`, p.`url`, p.`component`, p.`sort`
FROM `t_sys_permission` p
WHERE p.permission_code = 'tools'
OR p.parent_id IN (SELECT `id` FROM (SELECT `id` FROM `t_sys_permission` WHERE `permission_code` = 'tools') x)
ORDER BY p.level, p.sort;
-- 预期:每个有效角色 1 条新关联
SELECT rp.`id`, rp.`role_id`, r.`role_name`, rp.`permission_id`, p.`permission_name`
FROM `t_sys_role_permission` rp
JOIN `t_sys_role` r ON r.`id` = rp.`role_id`
JOIN `t_sys_permission` p ON p.`id` = rp.`permission_id`
WHERE p.permission_code = 'tools:json-format'
ORDER BY rp.`role_id`;
+72
View File
@@ -0,0 +1,72 @@
-- ============================================================
-- 工具列表菜单 + 文本比对页面
--
-- 与「系统管理」同级的顶级菜单,下挂「文本比对」
-- 菜单由 t_sys_permission 表驱动,前端 formatMenuList() 会把 component
-- 拼成 import('@/view' + component + '.vue'),因此 component 必须带前导斜杠,
-- 且要与 src/view/tools/text-diff.vue 对应
--
-- 幂等:可重复执行,已存在则先清理再插入
-- 执行库:freedom_db
--
-- 注意:id 一律由数据库自增分配,不要硬编码。
-- t_sys_permission 自增起点为 1000,且t_sys_role_permission 已单独用到 1005,
-- 硬编码 1005/1006 会直接撞主键(实测已踩坑,事务回滚后才改用自增)。
-- ============================================================
-- ---------- 1. 清理历史遗留 ----------
-- 用 JOIN 按 permission_code 精确定位,避免误删他人菜单
DELETE rp
FROM `t_sys_role_permission` rp
JOIN `t_sys_permission` p ON p.id = rp.permission_id
WHERE p.permission_code IN ('tools', 'tools:text-diff');
DELETE FROM `t_sys_permission` WHERE `permission_code` IN ('tools', 'tools:text-diff');
-- ---------- 2. 插入顶级菜单「工具列表」 ----------
-- url 与 component 留空,前端会挂载 Main 布局作为容器
-- icon 必须在 view-design 图标清单内(ios-apps 已从线上 CSS 校验存在)
INSERT INTO `t_sys_permission`
(`parent_id`, `permission_code`, `permission_name`, `permission_desc`, `level`,
`icon`, `url`, `component`, `sort`, `create_by`, `create_date`, `del_flag`)
VALUES (0, 'tools', '工具列表', '与系统管理同级的工具集合',
1, 'ios-apps', '', '', 20, 'admin', NOW(), 0);
SET @tools_id = LAST_INSERT_ID();
-- ---------- 3. 插入子菜单「文本比对」 ----------
-- component 必须带前导斜杠且与磁盘文件对应:src/view/tools/text-diff.vue
INSERT INTO `t_sys_permission`
(`parent_id`, `permission_code`, `permission_name`, `permission_desc`, `level`,
`icon`, `url`, `component`, `sort`, `create_by`, `create_date`, `del_flag`)
VALUES (@tools_id, 'tools:text-diff', '文本比对',
'左右文本并排比对,支持字符级高亮、忽略空白与结果导出',
2, 'md-git-compare', '/tools/text-diff', '/tools/text-diff', 1, 'admin', NOW(), 0);
SET @diff_id = LAST_INSERT_ID();
-- ---------- 4. 授权给全部有效角色 ----------
-- 不写死 role_id,将来新增角色后重跑本脚本即可自动补齐授权
INSERT INTO `t_sys_role_permission` (`role_id`, `permission_id`, `create_by`, `create_date`)
SELECT r.`id`, p.`pid`, 'admin', NOW()
FROM `t_sys_role` r
CROSS JOIN (SELECT @tools_id AS `pid`
UNION ALL
SELECT @diff_id) p
WHERE r.`del_flag` = 0;
-- ---------- 5. 校验 ----------
-- 预期 2 行:工具列表(parent_id=0, level=1) 与 文本比对(parent_id=工具列表id, level=2)
SELECT p.`id`, p.`parent_id`, p.`permission_code`, p.`permission_name`,
p.`level`, p.`icon`, p.`url`, p.`component`, p.`sort`
FROM `t_sys_permission` p
WHERE p.`permission_code` IN ('tools', 'tools:text-diff')
ORDER BY p.`level`, p.`sort`;
-- 预期:每个有效角色 2 条关联
SELECT rp.`id`, rp.`role_id`, r.`role_name`, rp.`permission_id`, p.`permission_name`
FROM `t_sys_role_permission` rp
JOIN `t_sys_role` r ON r.`id` = rp.`role_id`
JOIN `t_sys_permission` p ON p.`id` = rp.`permission_id`
WHERE p.`permission_code` IN ('tools', 'tools:text-diff')
ORDER BY rp.`role_id`, rp.`permission_id`;
-76
View File
@@ -1,76 +0,0 @@
-- ============================================================
-- 三角洲行动 · 改枪码模块(delta_gun_code)
-- 数据库: freedom_db
-- 用途: 后台维护改枪码 + 无需鉴权的公开复制页面数据源
-- 说明: 可重复执行(CREATE TABLE IF NOT EXISTS / 幂等 INSERT 判断)
-- 执行后需重新登录系统(或退出登录)刷新菜单缓存方可看到新菜单
-- ============================================================
USE `freedom_db`;
-- 1. 改枪码数据表
CREATE TABLE IF NOT EXISTS `delta_gun_code` (
`id` BIGINT NOT NULL AUTO_INCREMENT COMMENT '主键',
`name` VARCHAR(100) NOT NULL COMMENT '配装名称',
`gun_name` VARCHAR(100) DEFAULT NULL COMMENT '枪械/载具名称',
`category` VARCHAR(50) DEFAULT NULL COMMENT '分类标签(主武器/副武器/载具配置等)',
`code` TEXT NOT NULL COMMENT '改枪码内容',
`remark` VARCHAR(500) DEFAULT NULL COMMENT '备注说明',
`sort` INT DEFAULT 0 COMMENT '排序',
`status` TINYINT DEFAULT 1 COMMENT '状态:1显示 0隐藏',
`copy_count` INT DEFAULT 0 COMMENT '被复制次数',
`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 '删除标记',
PRIMARY KEY (`id`),
KEY `idx_delta_code_status` (`status`, `del_flag`)
) ENGINE=InnoDB AUTO_INCREMENT=1000 DEFAULT CHARSET=utf8mb4 COMMENT='三角洲改枪码表';
-- 2. 菜单权限:一级菜单「三角洲」
INSERT INTO `t_sys_permission`
(`parent_id`, `permission_code`, `permission_name`, `permission_desc`, `level`, `icon`, `url`, `component`, `sort`, `create_by`, `create_date`, `del_flag`)
SELECT 0, 'delta', '三角洲', '三角洲行动相关功能', 1, 'md-game-controller-a', '', '', 9, 'admin', NOW(), 0
FROM DUAL
WHERE NOT EXISTS (SELECT 1 FROM `t_sys_permission` WHERE `permission_code` = 'delta' AND `del_flag` = 0);
-- 3. 菜单权限:二级菜单「改枪码管理」
SET @delta_parent_id = (SELECT `id` FROM `t_sys_permission` WHERE `permission_code` = 'delta' AND `del_flag` = 0 ORDER BY `id` LIMIT 1);
INSERT INTO `t_sys_permission`
(`parent_id`, `permission_code`, `permission_name`, `permission_desc`, `level`, `icon`, `url`, `component`, `sort`, `create_by`, `create_date`, `del_flag`)
SELECT @delta_parent_id, 'delta:gun-code', '改枪码管理', '三角洲改枪码维护与分享', 2, 'md-code-working', '/delta/gun-code', '/delta/delta-gun-code', 1, 'admin', NOW(), 0
FROM DUAL
WHERE NOT EXISTS (SELECT 1 FROM `t_sys_permission` WHERE `permission_code` = 'delta:gun-code' AND `del_flag` = 0);
-- 4. 角色授权:给所有现有角色挂上新菜单(幂等)
INSERT INTO `t_sys_role_permission` (`role_id`, `permission_id`, `create_by`, `create_date`)
SELECT r.`id`, p.`id`, 'admin', NOW()
FROM `t_sys_role` r
JOIN `t_sys_permission` p ON p.`permission_code` IN ('delta', 'delta:gun-code') AND p.`del_flag` = 0
WHERE r.`del_flag` = 0
AND NOT EXISTS (
SELECT 1 FROM `t_sys_role_permission` rp
WHERE rp.`role_id` = r.`id` AND rp.`permission_id` = p.`id`
);
-- 5. 菜单权限:二级菜单「改枪码录入」
SET @delta_parent_id = (SELECT `id` FROM `t_sys_permission` WHERE `permission_code` = 'delta' AND `del_flag` = 0 ORDER BY `id` LIMIT 1);
INSERT INTO `t_sys_permission`
(`parent_id`, `permission_code`, `permission_name`, `permission_desc`, `level`, `icon`, `url`, `component`, `sort`, `create_by`, `create_date`, `del_flag`)
SELECT @delta_parent_id, 'delta:gun-code-entry', '改枪码录入', '快速录入三角洲改枪码', 2, 'md-create', '/delta/gun-code-entry', '/delta/delta-gun-code-entry', 0, 'admin', NOW(), 0
FROM DUAL
WHERE NOT EXISTS (SELECT 1 FROM `t_sys_permission` WHERE `permission_code` = 'delta:gun-code-entry' AND `del_flag` = 0);
-- 6. 角色授权:给所有现有角色挂上「改枪码录入」菜单(幂等)
INSERT INTO `t_sys_role_permission` (`role_id`, `permission_id`, `create_by`, `create_date`)
SELECT r.`id`, p.`id`, 'admin', NOW()
FROM `t_sys_role` r
JOIN `t_sys_permission` p ON p.`permission_code` = 'delta:gun-code-entry' AND p.`del_flag` = 0
WHERE r.`del_flag` = 0
AND NOT EXISTS (
SELECT 1 FROM `t_sys_role_permission` rp
WHERE rp.`role_id` = r.`id` AND rp.`permission_id` = p.`id`
);
+20 -5
View File
@@ -152,23 +152,38 @@ VALUES
(1, '超级管理员', 'admin', '超级管理员,拥有所有权限', '1', 1, 1, 'admin', NOW(), 0);
-- 权限菜单
-- component 必须带前导斜杠且与 src/view 下的实际文件名一致:
-- 前端 formatMenuList() 会拼成 import('@/view' + component + '.vue')
-- 说明:此处 id 硬编码仅适用于全新建库(此时两张表都为空)。
-- 已有数据的库请用 sql/add_tools_text_diff.sql,它改用自增避免撞主键。
INSERT INTO `t_sys_permission` (`id`, `parent_id`, `permission_code`, `permission_name`, `permission_desc`, `level`, `icon`, `url`, `component`, `sort`, `create_by`, `create_date`, `del_flag`)
VALUES
(1, 0, 'system', '系统管理', '系统管理菜单', 1, 'ios-settings', '', '', 1, 'admin', NOW(), 0),
(2, 1, 'sys:user', '用户管理', '用户管理', 2, 'ios-person', '/sys/user', 'sys/user/user-list', 1, 'admin', NOW(), 0),
(3, 1, 'sys:role', '角色管理', '角色管理', 2, 'ios-people', '/sys/role', 'sys/role/role-list', 2, 'admin', NOW(), 0),
(4, 1, 'sys:dept', '部门管理', '部门管理', 2, 'ios-home', '/sys/dept', 'sys/dept/dept-list', 3, 'admin', NOW(), 0),
(5, 1, 'sys:permission', '权限管理', '权限管理', 2, 'ios-key', '/sys/permission', 'sys/permission/permission-list', 4, 'admin', NOW(), 0);
(2, 1, 'sys:user', '用户管理', '用户管理', 2, 'ios-person', '/sys/user', '/sys/sys-user', 1, 'admin', NOW(), 0),
(3, 1, 'sys:role', '角色管理', '角色管理', 2, 'ios-people', '/sys/role', '/sys/sys-role', 2, 'admin', NOW(), 0),
(4, 1, 'sys:dept', '部门管理', '部门管理', 2, 'ios-home', '/sys/dept', '/sys/sys-dept', 3, 'admin', NOW(), 0),
(5, 1, 'sys:permission', '权限管理', '权限管理', 2, 'ios-key', '/sys/permission', '/sys/sys-permission', 4, 'admin', NOW(), 0),
(6, 0, 'user:getUserInfo', '获取用户信息', '登录后获取当前用户信息的前端权限点', 1, '', '', '', 99, 'admin', NOW(), 0),
(1003, 0, 'todo', '待办', '个人与公开待办', 1, 'md-checkmark-circle', '', '', 10, 'admin', NOW(), 0),
(1004, 1003, 'todo:list', '待办清单', '个人待办清单', 2, 'md-list-box', '/todo/list', '/todo/todo-list', 1, 'admin', NOW(), 0),
(1007, 0, 'tools', '工具列表', '与系统管理同级的工具集合', 1, 'ios-apps', '', '', 20, 'admin', NOW(), 0),
(1008, 1007, 'tools:text-diff', '文本比对', '左右文本并排比对,支持字符级高亮、忽略空白与结果导出', 2, 'md-git-compare', '/tools/text-diff', '/tools/text-diff', 1, 'admin', NOW(), 0);
-- 用户-角色关联:admin -> 超级管理员
INSERT INTO `t_sys_user_role` (`id`, `user_id`, `role_id`, `create_by`, `create_time`)
VALUES (1, 1, 1, 'admin', NOW());
-- 角色-权限关联:超级管理员 -> 所有权限
-- 关联表自增起点同为 1000,这里也按实际 id 顺排,避免与 t_sys_permission 混淆
INSERT INTO `t_sys_role_permission` (`id`, `role_id`, `permission_id`, `create_by`, `create_date`)
VALUES
(1, 1, 1, 'admin', NOW()),
(2, 1, 2, 'admin', NOW()),
(3, 1, 3, 'admin', NOW()),
(4, 1, 4, 'admin', NOW()),
(5, 1, 5, 'admin', NOW());
(5, 1, 5, 'admin', NOW()),
(8, 1, 6, 'admin', NOW()),
(1003, 1, 1003, 'admin', NOW()),
(1004, 1, 1004, 'admin', NOW()),
(1005, 1, 1007, 'admin', NOW()),
(1006, 1, 1008, 'admin', NOW());
+43
View File
@@ -0,0 +1,43 @@
-- =============================================================
-- 公开待办看板(无需登录)增量脚本
-- 用途: 对外分享的待办清单页面数据源,独立于个人待办表 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` TEXT COMMENT '备注(富文本 HTML)',
`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`);
-- 3. 兼容已存在的旧表:remark 由 VARCHAR(1000) 升级为 TEXT(重复执行无害)
ALTER TABLE `t_public_todo` MODIFY COLUMN `remark` TEXT COMMENT '备注(富文本 HTML)';
@@ -0,0 +1,26 @@
package com.freedom.admin.config;
import com.baomidou.mybatisplus.annotation.DbType;
import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor;
import com.baomidou.mybatisplus.extension.plugins.inner.PaginationInnerInterceptor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
/**
* MyBatis-Plus 分页插件配置
*
* @author zhangjiaxun
* @description 注册分页拦截器后,Service.page() 才会执行 count 并返回正确的 total/pages,
* 否则仅填充 records 而 total 恒为 0
*/
@Configuration
public class MybatisPlusConfig {
@Bean
public MybatisPlusInterceptor mybatisPlusInterceptor() {
MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL));
return interceptor;
}
}
@@ -76,14 +76,12 @@ public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
.authorizeRequests()// 验证码允许匿名访问
.antMatchers("/email-sender/sendEmailVerificationCode","/sys-user/register")
.anonymous()
// 三角洲改枪码:公开复制接口无需登录,后台管理接口需登录
.antMatchers("/delta-gun-code/public/**")
.permitAll()
.antMatchers("/delta-gun-code/**")
.authenticated()
// 待办清单:多人数据隔离,必须登录后访问
.antMatchers("/todo/**")
.authenticated()
// 公开待办看板:对外分享,无需登录
.antMatchers("/public-todo/**")
.permitAll()
.and()
.formLogin()
.loginProcessingUrl("/login")
@@ -1,145 +0,0 @@
package com.freedom.admin.controller;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.core.conditions.update.UpdateWrapper;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.freedom.admin.base.BaseController;
import com.freedom.admin.common.Result;
import com.freedom.admin.entity.DeltaGunCode;
import com.freedom.admin.service.DeltaGunCodeService;
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.List;
/**
* 控制层-三角洲改枪码表
*
* @author zhangjiaxun
* @description 后台维护改枪码 + 公开复制接口(无需登录)
*/
@RestController
@RequestMapping("delta-gun-code")
@Log4j2
public class DeltaGunCodeController extends BaseController {
@Autowired
private DeltaGunCodeService deltaGunCodeService;
/**
* 后台管理-分页查询改枪码列表
*/
@RequestMapping("findPageList")
public Result<DeltaGunCode> findPageList(long pageNum, long pageSize, DeltaGunCode deltaGunCode) {
QueryWrapper<DeltaGunCode> queryWrapper = new QueryWrapper<>();
queryWrapper.eq("del_flag", 0);
queryWrapper.like(StringUtils.isNotBlank(deltaGunCode.getName()), "name", deltaGunCode.getName());
queryWrapper.like(StringUtils.isNotBlank(deltaGunCode.getGunName()), "gun_name", deltaGunCode.getGunName());
queryWrapper.like(StringUtils.isNotBlank(deltaGunCode.getCategory()), "category", deltaGunCode.getCategory());
queryWrapper.eq(deltaGunCode.getStatus() != null, "status", deltaGunCode.getStatus());
queryWrapper.orderByAsc("sort").orderByDesc("id");
Page<DeltaGunCode> page = deltaGunCodeService.page(new Page<>(pageNum, pageSize), queryWrapper);
return Result.ok().setData(page);
}
/**
* 公开页面-查询所有启用的改枪码(无需登录)
*/
@RequestMapping("public/list")
public Result<DeltaGunCode> publicList() {
QueryWrapper<DeltaGunCode> queryWrapper = new QueryWrapper<>();
queryWrapper.eq("del_flag", 0);
queryWrapper.eq("status", 1);
queryWrapper.orderByAsc("sort").orderByDesc("id");
List<DeltaGunCode> list = deltaGunCodeService.list(queryWrapper);
return Result.ok().setData(list);
}
/**
* 公开页面-复制成功后增加复制次数(无需登录)
*/
@RequestMapping("public/addCopyCount")
public Result addCopyCount(Long id) {
if (id == null) {
return Result.error().message("参数缺失");
}
UpdateWrapper<DeltaGunCode> updateWrapper = new UpdateWrapper<>();
updateWrapper.eq("id", id).eq("del_flag", 0)
.setSql("copy_count = copy_count + 1");
deltaGunCodeService.update(updateWrapper);
return Result.ok();
}
/**
* 后台管理-新增改枪码
*/
@RequestMapping("add")
public Result add(DeltaGunCode deltaGunCode) {
if (StringUtils.isBlank(deltaGunCode.getName())) {
return Result.error().message("配装名称不能为空");
}
if (StringUtils.isBlank(deltaGunCode.getCode())) {
return Result.error().message("改枪码不能为空");
}
try {
if (deltaGunCode.getStatus() == null) {
deltaGunCode.setStatus(1);
}
if (deltaGunCode.getSort() == null) {
deltaGunCode.setSort(0);
}
deltaGunCode.setCopyCount(0);
deltaGunCode.setCreateBy(getUsername());
deltaGunCode.setCreateDate(new Date());
boolean save = deltaGunCodeService.save(deltaGunCode);
return Result.ok().setData(save);
} catch (Exception e) {
log.error("新增改枪码失败", e);
return Result.error().message("新增失败");
}
}
/**
* 后台管理-更新改枪码
*/
@RequestMapping("update")
public Result update(DeltaGunCode deltaGunCode) {
if (deltaGunCode.getId() == null) {
return Result.error().message("参数缺失");
}
if (StringUtils.isBlank(deltaGunCode.getName())) {
return Result.error().message("配装名称不能为空");
}
if (StringUtils.isBlank(deltaGunCode.getCode())) {
return Result.error().message("改枪码不能为空");
}
try {
deltaGunCode.setUpdateBy(getUsername());
deltaGunCode.setUpdateDate(new Date());
boolean update = deltaGunCodeService.updateById(deltaGunCode);
return Result.ok().setData(update);
} catch (Exception e) {
log.error("更新改枪码失败", e);
return Result.error().message("更新失败");
}
}
/**
* 后台管理-删除改枪码(逻辑删除)
*/
@RequestMapping("remove")
public Result remove(DeltaGunCode deltaGunCode) {
try {
boolean remove = deltaGunCodeService.removeById(deltaGunCode.getId());
return Result.ok().setData(remove);
} catch (Exception e) {
log.error("删除改枪码失败", e);
return Result.error().message("删除失败");
}
}
}
@@ -0,0 +1,244 @@
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);
// DATE 列不接受空串,空值统一转 null
if (StringUtils.isBlank(publicTodo.getDueDate())) {
publicTodo.setDueDate(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 {
// DATE 列不接受空串,空值统一转 null
if (StringUtils.isBlank(publicTodo.getDueDate())) {
publicTodo.setDueDate(null);
}
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<>();
// 逾期按 yyyy-MM-dd 字符串比较,与前端口径一致,不经时区换算
String today = new java.text.SimpleDateFormat("yyyy-MM-dd").format(new Date());
for (PublicTodo item : list) {
if (item.getStatus() != null && item.getStatus() == 1) {
done++;
} else if (item.getDueDate() != null && item.getDueDate().compareTo(today) < 0) {
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("统计失败");
}
}
}
@@ -3,20 +3,21 @@ 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 java.io.Serializable;
import java.util.Date;
/**
* delta_gun_code
* 三角洲行动-改枪码
* t_public_todo
* 公开待办看板(无需登录即可访问,全体访问者共享同一份清单)
*
* @author zhangjiaxun
*/
@Data
@TableName("delta_gun_code")
public class DeltaGunCode implements Serializable {
@TableName("t_public_todo")
public class PublicTodo implements Serializable {
private static final long serialVersionUID = 1L;
@@ -27,45 +28,36 @@ public class DeltaGunCode implements Serializable {
private Long id;
/**
* 配装名称
* 标题
*/
private String name;
private String title;
/**
* 枪械/载具名称
*/
private String gunName;
/**
* 分类标签(如:主武器/副武器/载具配置)
*/
private String category;
/**
* 改枪码内容
*/
private String code;
/**
* 备注说明
* 备注
*/
private String remark;
/**
* 排序
* 优先级:1低 2中 3高
*/
private Integer sort;
private Integer priority;
/**
* 状态:1显示 0隐藏
* 分类
*/
private String category;
/**
* 截止日期(yyyy-MM-dd)
* 用 String 映射 DATE 列:字符串往返不经 JDBC 时区换算,避免差一天
*/
private String dueDate;
/**
* 状态:0未完成 1已完成
*/
private Integer status;
/**
* 被复制次数
*/
private Integer copyCount;
/**
* 创建者
*/
@@ -74,6 +66,7 @@ public class DeltaGunCode implements Serializable {
/**
* 创建时间
*/
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
private Date createDate;
/**
@@ -84,6 +77,7 @@ public class DeltaGunCode implements Serializable {
/**
* 更新时间
*/
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
private Date updateDate;
/**
@@ -1,13 +0,0 @@
package com.freedom.admin.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.freedom.admin.entity.DeltaGunCode;
/**
* 数据访问层-三角洲改枪码表
*
* @author zhangjiaxun
*/
public interface DeltaGunCodeMapper extends BaseMapper<DeltaGunCode> {
}
@@ -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> {
}
@@ -1,13 +0,0 @@
package com.freedom.admin.service;
import com.baomidou.mybatisplus.extension.service.IService;
import com.freedom.admin.entity.DeltaGunCode;
/**
* 业务层-三角洲改枪码表
*
* @author zhangjiaxun
*/
public interface DeltaGunCodeService extends IService<DeltaGunCode> {
}
@@ -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> {
}
@@ -1,19 +0,0 @@
package com.freedom.admin.service.impl;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.freedom.admin.entity.DeltaGunCode;
import com.freedom.admin.mapper.DeltaGunCodeMapper;
import com.freedom.admin.service.DeltaGunCodeService;
import lombok.extern.log4j.Log4j2;
import org.springframework.stereotype.Service;
/**
* 业务层实现-三角洲改枪码表
*
* @author zhangjiaxun
*/
@Service
@Log4j2
public class DeltaGunCodeServiceImpl extends ServiceImpl<DeltaGunCodeMapper, DeltaGunCode> implements DeltaGunCodeService {
}
@@ -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 {
}