# 罗邓自定义报表管理系统 **Repository Path**: elfbobo_admin_admin/LuodengReportingSystem ## Basic Information - **Project Name**: 罗邓自定义报表管理系统 - **Description**: No description available - **Primary Language**: Unknown - **License**: Not specified - **Default Branch**: main - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 0 - **Forks**: 1 - **Created**: 2026-10-30 - **Last Updated**: 2026-11-05 ## Categories & Tags **Categories**: Uncategorized **Tags**: None ## README # 自定义报表系统 把一段 SQL 或者一个存储过程贴进来,系统自动生成查询条件、执行查询、导出 Excel。 不用写代码、不用改配置,适合日常给科室/领导临时出数。 --- ## 一、功能清单 | 功能 | 说明 | |------|------| | 用户登录 | 多用户,密码加盐 SHA-256 存储;禁用账号立即无法登录 | | 用户管理 | 管理员专属:新增/编辑/删除用户、重置密码(统一 123456)、启用/禁用、查看登录记录 | | 角色与权限 | 一个用户可挂多个角色,能看的报表 = 各角色权限**叠加(并集)**;角色可勾选具体报表,也可一键「全部报表」 | | **角色操作权限** | 角色上还能单独开关三项:**查看查询日志**、**查看执行 SQL**、**导出 Excel**;关掉后按钮隐藏、接口也拒绝,多角色按并集算 | | **报表批量维护** | 报表管理页勾选多张报表,一次搞定「批量设置分组」「批量设置数据源」(换库、整理左侧菜单时用),SQL 与查询条件不动 | | **报表导出 / 导入** | 报表可以导出成 `.json` 报表包(含条件、下拉 SQL,不含数据库密码),给别的医院直接导入;导入前有预检清单,数据源按名称匹配、同名报表可跳过或另存,还能顺便授权给角色 | | **用户批量导入** | 下载 Excel 模板填好直接导入,支持一次挂多个角色;角色名对不上、用户名已存在的行不会导入,逐行给原因,失败明细可导出成 Excel 改完再导 | | 主题切换 | 红 / 橙 / 绿 / 黑 / 蓝 五套主题,一键切换,跟着账号保存 | | 数据源管理 | 保存多台 SQL Server 的连接信息,支持命名实例(`IP\实例名`),可一键测试连接(仅管理员) | | 报表定义 | 贴「整段 SQL」或「存储过程定义」即可,自动识别类型与查询条件(仅管理员维护) | | **固定参数** | `exec usp_x @kjqj,-1,-1,@kfdm` 里只有 `@变量` 生成查询条件,`-1`、`'2026-01-01'` 这类写死的实参按位置原样传给过程,用户不用填 | | **多标签页** | 点报表就在**新标签页**里打开,可同时看多张;**同一张报表也能开多个**;标签可单个关 / 关闭其他 / 关闭全部;切换不重新查库,条件、结果、页码、滚动位置都留着 | | 查询条件 | 自动生成:文本框 / 数字框 / 日期 / 日期时间 / 下拉框 / 勾选框,可手工改标签、默认值、必填 | | **下拉选项库** | 常用的下拉框 SQL 存成一条(名称 + SQL + 执行库),多个报表条件共用,改一处全部跟着变;报表条件里选「用已有:科室字典」即可,也保留就地写「自定义 SQL」。维护时能**试运行**(返回行数/列名/前 10 行 + 下拉框实际样式),被引用的选项不许删 | | 结果展示 | 表格展示,数字右对齐、日期格式化、固定表头、前端分页(50/100/200 行每页) | | 表格筛选 | 点工具条「筛选」按钮才展开每列的筛选框(默认收起),数据已在前端,**不再请求后端**,支持 `>` `<` `>=` `<=` `=` `!=`,5000 行约 20 ms | | **表格排序** | **每一列都能点表头排序**:升序 → 降序 → 取消;数字按数值、日期按时间、中文按拼音;空值永远排最后;纯前端排,不重新查库,和筛选、分页叠加使用 | | 表格样式 | 简约 / 斑马纹 / 网格线 / 浅彩头 / 深色表头 五种,随主题配色,选择记在浏览器 | | **小数精度** | 存储过程里 `float`/`real` 累加出来的二进制尾差(`934650.5899999999`)自动收成 `934650.59`,真实小数位不砍,导出 Excel 同样干净 | | 结果概况 | 行数、耗时用色块芯片显示,超过行数上限时给出橙色「已截断」提醒 | | 报表说明 | 可写多行口径说明,**由使用者自己点右上角【报表说明】按钮显示或隐藏,默认不显示**,选择记在浏览器 | | 左侧菜单收起 | 侧栏右上角「«」收起,给表格腾出 264px;收起后左上角「☰ 报表列表」再展开,状态记在浏览器 | | 导出 Excel | 后端 POI 生成 xlsx,带表头样式、冻结首行、列宽自适应;**每次导出单独留一条日志**(谁、什么时候、什么参数、导了多少行、文件名) | | 查询日志 | 查询和导出都记,每个查询条件的**参数名 + 实际值**分块显示(留空的标「(空)」),**记录客户端 IP**,点「SQL」能看**当时实际执行的 SQL**(参数已代入,可复制),可按「全部 / 查询 / 导出」筛选;谁能看由角色的操作权限决定 | | **日志汇总** | **系统管理 → 查询日志**:所有报表的查询/导出记录汇总在一起,按报表名、用户、IP、参数搜索,可按操作/结果/报表/日期区间过滤,带「总数 / 查询 / 导出 / 失败 / 用户数 / IP 数」汇总与分页,仅管理员 | | 登录记录 | 记录每次登录的时间、用户名、结果(成功/失败原因)、IP,可搜索、可清空 | | 分组管理 | 报表按分组显示在左侧菜单,支持搜索;分组标题用**主题色小字 + 数量徽标 + 右侧细线**,和下面的报表条目一眼分得开(跟随主题换色) | --- ## 二、技术栈 | 层 | 技术 | |----|------| | 前端 | **Vue 2.7 + Vite 5 + vue-router 3(标准脚手架工程,`frontend/`)**,单文件组件 + 手写 CSS 主题变量 | | 后端 | Spring Boot 3.5.16 / JDK 17 | | 元数据库 | SQLite(`./data/report.db`,首次启动自动建表) | | 业务数据 | SQL Server(mssql-jdbc 12.10.2) | | 导出 | Apache POI 5.2.3 | --- ## 三、快速开始 1. 环境要求:**JDK 17 或以上**(`java -version` 能看到 17+ 即可) 2. 双击 **`start.bat`**(会自动打开浏览器 http://localhost:8090/ ) 3. 登录:**admin / 123456**(管理员)、**user / 123456**(普通用户,能看到全部报表) 4. 首次使用先到「系统管理 → 数据源管理」把数据源密码补全,点「测试」确认能连上 5. 回到「报表中心 → 新建报表」,贴 SQL,点「解析查询条件」,保存,然后就能查了 6. 要给别人用:先到「角色管理」建角色并勾选报表,再到「用户管理」建账号、勾上角色 > **只运行**不需要 Node.js:jar 里已经带着打包好的前端。 > **改前端或重新打包**才需要 Node.js 18+:双击 `build.bat`(会先 `npm run build` 再 `mvn clean package`)。 > 只改后端也可以直接 `mvnw.cmd -DskipTests package`(前端产物已经在 `src/main/resources/static` 里)。 **本机已经附带一套演示数据**:SQL Server 实例 `LUODENG` 下的 `ReportDemo` 库 (12 条门诊收入样例 + 存储过程 `dbo.usp_rpt_income`),数据源叫「本机演示库(SQL2019)」, 登录系统后直接点报表就能看到效果。不需要了可以删库删登录: ```sql ALTER DATABASE ReportDemo SET SINGLE_USER WITH ROLLBACK IMMEDIATE; DROP DATABASE ReportDemo; DROP LOGIN rpt_demo; ``` --- ## 四、日常使用流程 ``` 新建报表 ↓ ① 选数据源 ② 贴 SQL / 存储过程 ③ 点「解析查询条件」 ↓ ④ 调整条件(改中文标签、设默认值、勾必填) 保存 ↓ 左侧点开报表 → 填条件 → 查询 → 看结果 → 导出 Excel ``` ### 多标签页:同时看几张报表 报表中心做成了浏览器那种标签页,**点左侧任意一张报表就在新标签页里打开**,可以同时开好几张对着看: | 操作 | 效果 | |------|------| | 点左侧报表 | 新开一个标签页(**同一张报表也能开多个**,方便用不同条件对比) | | 点标签页 | 切过去;**已经查过的条件、结果、翻到第几页、滚动位置都还在**,不会重新查库 | | 标签上的 ✕ | 关掉这一个 | | 「关闭其他」 | 只留当前这个,其余全关(标签页 ≥2 时出现) | | 「关闭全部」 | 全部关掉,回到空白页 | - 标签页**数量不限制**,多了就在标签栏里横向滚动;切换/新开时会自动把当前标签滚进可视区。 - 同一个报表开了多个时,标题自动带序号:`招聘公告查询 (1)`、`招聘公告查询 (2)`。 - 标签名左边的小圆点:灰色=还没查过,实心主题色=这个标签已经有查询结果。 - 每个标签页的「查询/导出日志」「报表说明」「编辑报表」都是各自独立的,互不影响。 - 编辑并保存某张报表后,已经打开它的标签页会自动重新拉一次定义(条件、列可能变了), 没开过的就直接新开一个。 - 关掉标签页不会丢数据,但**刷新页面会清空**(标签页状态不落盘,避免打开时自动跑一堆查询)。 ### 新建 / 编辑报表:弹窗还是整页,自己选 报表编辑器有两种外壳,内容完全一样,区别只在显示方式: | 方式 | 怎么进 | 适合 | |------|--------|------| | 弹窗 | 左侧「+ 新建报表」/「编辑报表」 | 改一两个字段、快速看一下 | | 整页 | 弹窗右下角「⤢ 整页编辑」,或把左侧底部的「打开方式」切到**整页** | 贴长 SQL、条件多、要来回对照 | - 「打开方式」的选择记在浏览器里(`localStorage`),下次还是这个方式; - 弹窗切整页时**已经填的内容会带过去**,不会白填; - 整页模式地址是 `/#/report/editor`(新建)、`/#/report/editor/5`(编辑 5 号), 保存后自动回到报表中心并打开刚保存的那张; - 整页模式下 SQL 框高度按窗口自适应,表单内容(解析结果、查询条件表、各项提示) 在面板内部滚动,顶部的「返回 / 保存」始终固定,不会被长内容顶出屏幕外。 ### 左侧报表菜单可以收起 报表中心左侧列出所有能看的报表。字段多、要横着看表格时,点侧栏右上角的「**«**」把它收起来, 内容区就多出 264px;收起后标签栏最左边出现「**☰**」,点一下展开回来。 状态记在浏览器里(`localStorage: report_sidebar_collapsed`),刷新、重开都还是上次的样子。 ### 批量维护报表:一次改多张的分组 / 数据源 报表多了以后,「这张报表归到哪个分组」「这批报表是不是都要换到新库」这类改动一张张点太慢。 「系统管理 → 报表管理」支持勾选后批量改: | 步骤 | 说明 | |------|------| | ① 勾选 | 每行最左边的勾选框;表头那个勾选框是**全选 / 取消全选**(选了一部分时显示成半选) | | ② 批量操作条 | 勾上之后表格上方出现「已选 N 个报表」,里面有【批量设置分组】【批量设置数据源】【取消选择】 | | ③ 批量设置分组 | 填分组名(会列出**已有分组**,点一下就填进去),确定后这些报表一起换成该分组,左侧菜单立刻按新分组排列 | | ④ 批量设置数据源 | 下拉选一个数据源,确定后这些报表的 `conn_id` 一起改;会有确认提示 | 几个要点: - **只改分组和数据源**,报表的 SQL、查询条件、行数上限、说明、角色授权都不动; - 改数据源前会校验数据源是否存在,选了已删掉的连接会直接报错,不会把报表改坏; - 换库之后若新库里表名/字段不一致,查询会报错——SQL 不会自动改写,需要进去看那张报表; - 批量操作只有管理员能做(接口 `POST /api/report/batch`,前端按钮也只在管理页出现)。 ### 报表导出 / 导入:给别的医院一份,导进去就能用 一张报表在一家医院配好,另一家医院往往也想要同样的。不用照着重配一遍,导出成报表包,对面导入即可。 **导出**(系统管理 → 报表管理 → 右上角「导出」) | 项 | 说明 | |----|------| | 范围 | 勾了报表就导勾选的,没勾选导全部(会先确认一次) | | 内容 | 报表定义、查询条件(含下拉框 SQL)、备注与报表说明、引用到的下拉选项库条目 | | 不含 | 数据源的主机、账号、**密码**;只记数据源名称和库名 | | 格式 | `.json`,文件名带时间戳 | **导入**(同一页「导入报表」) | 步骤 | 说明 | |------|------| | ① 选文件 | 出现**预检清单**(只读,不写库):几张报表、每张的分组与条件数、数据源在本机能不能对上、同名报表是否会冲突 | | ② 选设置 | 「数据源对不上时导入到」哪个库;「同名报表」跳过还是另存为 `名称(导入)` | | ③ 授权角色 | 可不选;勾了「全部报表」的角色不用选,本来就能看 | | ④ 开始导入 | 结果页给出 成功 / 跳过 / 失败 三个数字,失败逐条给原因 | 几个要点: - 数据源先按**名称**找本机同名的,找不到才落到勾选的那个库;两边库名不同时,导入后建议进报表把数据源确认一遍; - 包里带的下拉选项按**名称**复用:本机已有同名选项就直接引用,没有就自动建一条; - 同名报表默认**跳过**,不覆盖已有报表;要保留两份就选「另存为 名称(导入)」; - 报表里带 SQL,属于实现细节,所以导出、导入接口都要求管理员。 --- ## 五、用户 / 角色 / 权限(重要) ### 三条权限线 | 权限线 | 管什么 | 由谁决定 | |--------|--------|----------| | **管理员身份** | 能不能进「系统管理」:用户、角色、数据源、报表维护、批量改报表、**日志汇总**;以及**报表是 SQL 还是存储过程、过程名、原始脚本**这类实现细节 | `sys_user.is_admin` | | **角色 → 报表** | 能看哪几张报表(能查、能导出哪些数据) | 角色 → 报表 的关联表 | | **角色 → 操作** | 在能看的报表上**还能做什么**:看查询日志、看执行 SQL、导出 Excel | `sys_role.perm_log / perm_sql / perm_export` | - 管理员**不受角色限制**,所有报表都能看,所有功能都能用; - 普通用户**只能看角色授权过的报表**,界面上连「系统管理」入口都没有; - **全站日志汇总(系统管理 → 查询日志)只有管理员能进**:它把所有报表的日志摊在一起, 普通角色即使开了「查看查询日志」,也只在自己打开的报表里看得到那一张的日志; - 后端接口也做了校验:就算有人直接调接口,看不到的报表照样返回「没有访问权限」, 普通用户调 `/api/logs` 返回「只有管理员可以执行这个操作」。 - **报表的实现细节只对管理员可见**:报表标题下那行「SQL 查询 / 存储过程」标签、后面的存储过程名 (`dbo.usp_xxx`),普通用户看不到;接口层面同步做了屏蔽——`GET /api/report`、 `GET /api/report/{id}` 返回的 `sqlType`、`procName`、`sqlText` 对非管理员一律置 `null`, 所以从浏览器控制台也翻不出来。判断依据是 `AuthContext.isAdmin()`,与角色勾选无关(管理员恒可见)。 ### 细分操作权限:角色能做什么 「角色管理 → 新增/编辑角色」里,除了勾报表,还有一组**操作权限**: | 权限 | 打开后能做 | 关掉后 | |------|-----------|--------| | 查看查询 / 导出日志 | 报表右上角出现【查询/导出日志】,能看到谁在什么时候查了、用了什么参数、查到多少行 | 按钮不显示;直接调接口返回「你的角色没有查看查询日志的权限」 | | 查看实际执行的 SQL | 结果页工具条出现【SQL】按钮、日志里每行有【SQL】可展开(参数已代入,可复制) | 按钮不显示;**查询接口返回包里也不带 SQL**,日志接口把 `sql_text` 字段摘掉 | | 导出 Excel | 【导出 Excel】按钮可用 | 按钮不显示;导出接口直接拒绝 | 「执行 SQL」建议只给管理员和排障的人:SQL 里带着库名、表名、字段名,等于把取数逻辑全暴露了。 角色列表里这三项用绿色小标签显示,关掉的会灰掉加删除线,一眼能看出这个角色能干什么。 ``` 例:给「科室查询员」角色勾上 查看日志 + 导出,不勾 执行 SQL → 他能导出、能看别人查过什么,但看不到 SQL 本身 ``` ### 多角色叠加 一个用户可以挂多个角色,能看到的是**并集**: ``` 张三 = 角色A(门诊报表、住院报表) + 角色B(绩效报表) → 能看:门诊报表、住院报表、绩效报表 ``` 操作权限同样是并集:角色A 只给「查看日志」,角色B 给了「导出」,那张三两项都能用。 只要挂的角色里有一个勾了「拥有全部报表」,他就能看到所有报表,**包括以后新增的**—— 适合给科室主任、院领导这种要全看的账号。 ### 配一遍就懂的流程 ``` ① 角色管理 → 新增角色「门诊组」→ 勾选门诊相关的报表 → 保存 ② 用户管理 → 新增用户 zhangsan / 123456 → 勾上角色「门诊组」→ 保存 ③ 张三登录 → 左侧只有门诊那几张报表,没有系统管理 ``` ### 用户管理的几个约定 | 操作 | 行为 | |------|------| | 重置密码 | 统一重置为 **123456**,并把该用户踢下线 | | 禁用用户 | 立即无法登录,正在用的会话也立刻失效 | | 改角色 | 在线用户的权限(包括报表范围和三项操作权限)**实时生效**,不用重新登录 | | 删自己 / 禁用自己 / 取消自己的管理员 | 不允许(防止把自己锁在外面) | | 删掉或禁用最后一个管理员 | 不允许,系统至少保留一个启用状态的管理员 | ### 用户批量导入(Excel) 用户多的时候一个个建太慢。「系统管理 → 用户管理 → 导入用户」: | 步骤 | 说明 | |------|------| | ① 下载模板 | 点「下载模板」,得到一个 `.xlsx`:第一行是表头,附「填写说明」页 | | ② 填写 | 从第 2 行开始一行一个用户;管理员、状态两列是下拉,选就行 | | ③ 选文件并导入 | 一次上传,直接出结果:成功几个、失败几行、每行为什么失败 | | ④ 下载失败明细 | 有失败行时可以导成 Excel(行号 / 用户名 / 姓名 / 角色 / 失败原因),改完再导一次 | 模板列: | 列 | 是否必填 | 说明 | |----|----------|------| | 用户名 | 必填 | 登录账号,不能有空格;**已存在的用户名会跳过**并记进失败明细 | | 姓名 | 可空 | 界面上显示的中文名 | | 密码 | 可空 | 不填就是 `123456` | | 角色 | 可空 | 填「角色管理」里已有的**角色名称**,多个用 `、` 分隔 | | 管理员 | 可空 | 是 / 否,不填按否 | | 状态 | 可空 | 启用 / 停用,不填按启用 | 整行失败、不会建出半个账号的情况: - **角色名称对不上**——这一行不导入,并单独列出「哪些角色不存在」,去「角色管理」建好再导; - 用户名为空、含空格、已存在; - 密码不足 4 位; - 管理员列 / 状态列填了认不出来的词(比如「也许」)。 > 为什么角色对不上就整行不导:账号建好了却看不到任何报表,现场排查起来比不建更麻烦。 > 需要先有角色的,可以先把角色建好(或让角色勾上「全部报表」)再导用户。 ### 数据源密码:不下发前端、加密落库 数据源里带着数据库账号密码,这块按「密码只存在于服务端」来设计: | 环节 | 做法 | |------|------| | **接口返回** | `GET /api/conn` 把 `password` 一律置 `null`,只回一个 `hasPassword` 布尔值。**改 F12、把输入框 type 改成 text 都看不到密码**,因为浏览器里根本没有这个值 | | **编辑保存** | 密码框不预填,占位符写着「留空 = 保持原密码」。留空保存时 SQL 里**根本不带 password 列**,不会把已存的密码冲成空 | | **测试连接** | 前端只传 `id` + 主机/库名/账号,密码留空;后端发现密码为空且有 `id` 时,自己从库里取了去连。密码全程没离开服务端 | | **落库加密** | `SecretBox` 用 **AES-256-GCM**(随机 12 字节 IV、带认证标签),密文形如 `GCM:base64(iv+密文+tag)`。同一条密码每次保存的密文都不同,改库里的密文一位就解不开,不会静默解出乱码 | | **密钥放哪** | **不写在代码里**。优先读环境变量 `REPORT_SECRET_KEY`;没有就用 `data/.secret.key`(首次启动自动生成 32 字节随机密钥);都没有才退化到进程内临时密钥(会打 ERROR 提示) | 密钥怎么给(三选一,优先级从上到下): ```bash # 1. 环境变量:Base64 的 32 字节密钥(openssl rand -base64 32 生成),适合容器/k8s export REPORT_SECRET_KEY='xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx=' # 2. 环境变量也可以直接给口令,内部按 SHA-256 派生密钥(省事但别用过短的) export REPORT_SECRET_KEY='WiNEX-MY-2026' # 3. 什么都不配:首次启动自动生成 data/.secret.key(默认方式,够用) ``` > ⚠️ **备份要拷两个文件**:`data/report.db` 和 `data/.secret.key`。 > 只拷库不拷密钥文件,换机器后所有数据源密码都解不开,只能在界面上重新填一遍。 > 反过来,密钥文件泄露 + 库泄露 = 密码泄露,所以两个文件不要放在同一个共享目录里。 **传输层怎么办**:上面解决的是「密码存在哪、会不会被前端看到」。如果担心网络抓包, 正解是给站点上 **HTTPS**(Nginx 加 `listen 443 ssl` + 证书,反代到 8090 不用改代码), 而不是在前端做一层加密——前端代码本身就是从服务器取的,能被注入 JS 的攻击者同样能拿到明文输入, 只有 TLS 才能保证「链路上看不到、也改不了」。 ### 主题切换 顶栏右上角一排色块:**红 / 橙 / 绿 / 黑 / 蓝**,点一下立即生效,选择会存到账号上, 换电脑登录还是同一个主题。黑色主题是暗色,晚上看表格不刺眼。 --- ## 六、SQL 的三种写法(核心) ### 写法 1:存储过程定义(推荐) 直接把库里 `CREATE PROCEDURE` 的完整内容贴进来,系统会自动认出参数并生成条件: ```sql CREATE PROCEDURE dbo.usp_rpt_income @StartDate datetime, @EndDate datetime = null, @DeptId varchar(20) = '', @OnlyValid bit = 1 AS BEGIN SET NOCOUNT ON; SELECT ... WHERE 日期 >= @StartDate ... END ``` 执行时系统发的是:`EXEC dbo.usp_rpt_income @StartDate=?, @EndDate=?, ...` (贴的是「过程定义」时按参数名传;贴的是「EXEC 调用」时按 SQL 里的位置传,见下面写法 2。) **注意:系统只调用库里已有的存储过程,不会自动创建或修改过程本身。** 过程要先在数据库里建好。 各控件类型对应的 SQL 参数类型: | 界面控件 | SQL 类型 | |----------|----------| | 文本框 | varchar / nvarchar / char | | 数字框 | int / bigint / decimal / numeric / float / money | | 日期 | date | | 日期时间 | datetime / datetime2 / smalldatetime | | 勾选框 | bit | ### 写法 2:EXEC 调用 ```sql EXEC dbo.usp_rpt_income @StartDate='2024-01-01', @DeptId='01' ``` 会把 `@参数=值` 识别成查询条件(默认值就是这里写的值)。 #### 位置传参:一部分给用户填,一部分写死 真实的过程常常是「4 个参数里只有 2 个要用户填」,比如: ```sql exec usp_luodeng_入库明细查询 @kjqj,-1,-1,@kfdm ``` 系统按**位置**判断每个实参: | 写法 | 认成什么 | 界面上 | |---|---|---| | `@kjqj` | 变量参数 | 一个输入框 | | `-1`、`0`、`'2026-01-01'`、`NULL`、`GETDATE()` | **固定值** | 不出现,执行时原样传给过程 | | `@a='值'` | 变量参数,并把这个值当默认值 | 一个输入框,默认值已填 | | `?` | 认不出名字,执行时明确报错 | —— | 所以上面这句只生成 **2 个查询条件**(`@kjqj`、`@kfdm`),中间两个 `-1` 永远跟着传过去。 执行时系统发的是: ```sql DECLARE @kjqj nvarchar(400) = ?; DECLARE @kfdm nvarchar(400) = ?; EXEC usp_luodeng_入库明细查询 @kjqj, -1, -1, @kfdm ``` 几点约定: - **固定值改不了界面上的**,要改就改 SQL 文本里那个值(改完重新点「解析查询条件」即可,条件不会变); - DECLARE 的类型取自该条件的「控件类型」(文本 → `nvarchar(400)`、日期 → `date`/`datetime`、 数字 → `decimal(18,4)`),所以日期参数请把控件类型选成日期,不要在文本框里手打日期; - 条件留空时:文本传 `''`,日期/数字传 `NULL`; - SQL 文本里**自己 `DECLARE` 过**的变量(例如先 `DECLARE @d datetime = GETDATE()` 再 `EXEC p @d, -1`) 不当查询条件,那段 SQL 会原样带在 `EXEC` 前面一起执行。 ### 写法 3:普通 SELECT 支持两种占位符,写法随意混用: ```sql -- ① 冒号占位符(推荐,干净) SELECT * FROM v_income WHERE biz_date >= :StartDate AND biz_date <= :EndDate AND (:DeptId = '' OR dept_code = :DeptId) -- ② @变量(没 DECLARE 时系统会自动补 DECLARE + SET) SELECT * FROM v_income WHERE biz_date >= @StartDate AND dept_code = @DeptId ``` - 名字里带 `date / time / 日期 / 时间 / start / end` 的参数,默认按日期时间处理(界面给日期选择器);其余按文本 - 已经在 SQL 里 `DECLARE` 过的变量不会被当成查询条件(由你的 SQL 自己维护) --- ## 七、参数留空时传什么值、动态默认值、报表说明 ### 7.1 留空规则(重要) | 控件类型 | 留空时 | |----------|--------| | 文本框 / 下拉框 | 传 **空字符串 `''`** —— 所以 SQL 里写 `(@DeptId = '' OR 列 = @DeptId)` 可以查全部 | | 日期 / 日期时间 / 数字 / 勾选框 | 传 **NULL** —— 所以 SQL 里写 `(@EndDate IS NULL OR 日期 <= @EndDate)` | 存储过程参数如果**在定义里写了默认值**(`@EndDate datetime = null`),界面留空时 系统会**直接不传这个参数**,让存储过程用自己的默认值,行为和直接 EXEC 完全一致。 如果存储过程参数的默认值不是写死的日期,而是希望"每次打开自动算"(比如本月), 不要指望过程里的默认值——在报表的「默认值」里写占位符(见 7.2),系统会把算好的真实日期传进去。 ### 7.2 动态默认值(日期不用每月手工改) 报表编辑里「默认值」一栏可以直接写占位符,每次打开报表按当天计算: | 写法 | 含义 | 日期控件得到 | 日期时间控件得到 | |------|------|--------------|------------------| | `{today}` | 今天 | 2026-10-29 | 2026-10-29 00:00:00 | | `{today_end}` | 今天末尾 | 2026-10-29 | 2026-10-29 23:59:59 | | `{yesterday}` | 昨天 | 2026-10-28 | 2026-10-28 00:00:00 | | `{tomorrow}` | 明天 | 2026-10-30 | — | | `{month_start}` | 本月 1 号 | 2026-10-01 | 2026-10-01 00:00:00 | | `{month_end}` | 本月最后一天 | 2026-10-31 | 2026-10-31 23:59:59 | | `{last_month_start}` | 上月 1 号 | 2026-09-01 | — | | `{last_month_end}` | 上月最后一天 | 2026-09-30 | 2026-09-30 23:59:59 | | `{year_start}` | 本年 1 月 1 日 | 2026-01-01 | — | | `{year_end}` | 本年 12 月 31 日 | 2026-12-31 | 2026-12-31 23:59:59 | | `{now}` | 当前时间 | 2026-10-29 | 2026-10-29 14:00:00 | 几个约定: - 大小写都行,`{MONTH_END}` 一样认;认不出来的写法(比如 `{moth_end}`)不会报错, 只是按"没有默认值"处理,界面留空。 - 结束类占位符在**日期时间**控件下自动补 `23:59:59`。不补的话 `日期 <= @End` 会漏掉最后一天的数据。 - 占位符只影响**展示和执行**,库里存的还是 `{month_start}` 原文。所以每月不用改报表, 编辑界面回显的也是 `{month_start}`,不会被固化成具体某天。 - 前端没传值(比如直接调接口)时,后端执行前也会自己展开,不依赖界面。 ### 7.3 下拉框:显示名称+编码、支持搜索 下拉条件的 `optionsSql` 约定**第一列是编码(code)、第二列是名称(name)**: ```sql SELECT dept_code, dept_name FROM t_dept ORDER BY dept_code ``` 界面上是一个可搜索下拉:收起时显示「名称 + 编码」,展开后可按编码或名称模糊过滤, 支持 ↑↓ 选择、回车确认、Esc 关闭,右侧 `×` 一键清空(清空 = 全部)。 只查一列也能用(编码和名称相同,此时不重复显示): ```sql SELECT DISTINCT 科室代码 FROM t_收费明细 ``` #### 复用:把常用 SQL 存进「下拉选项库」 同一个字典往往好几张报表都要用(科室、收费项目、病区…)。与其每个条件各抄一遍 `optionsSql`, 不如在 **系统管理 → 下拉选项** 里存一条: | 字段 | 说明 | |---|---| | 名称 | 报表编辑器里选的就是它,如「科室字典」,**不能重名**(重名会提示) | | 执行的数据源 | 一般选「跟随报表的数据源」;字典在另一个库时才指定 | | 取选项的 SQL | 同样是第 1 列 = 值、第 2 列 = 显示文字 | | 备注 | 给同事看的一句话 | 写的时候可以点 **试运行**:返回多少行多少列、前 10 行数据,以及**下拉框实际会变成什么样** (`01 · 内科` 这种)都直接显示,不用保存了再回报表里试。选项设成「跟随报表」时, 试运行会让你临时选一个库来跑(不影响保存)。 然后回到报表编辑器的查询条件表,「下拉选项」这一列选 **用已有:科室字典** 就行 (选了之后下面灰着显示那段 SQL 备查,控件类型自动切成「下拉框」);想临时写一段就保持「自定义 SQL」。 维护上两个约定: - **改一处,所有引用它的报表一起变** —— 这就是复用的意义,反过来说改之前要想清楚影响面; - **被引用的选项不允许删除**。列表「被引用」列显示用了几个条件,点删除会告诉你还有几个在用, 先把那些条件改成别的选项或自定义 SQL 再删(否则那些报表的下拉框会直接空掉)。 ### 7.4 报表说明(写口径给使用者看) 「报表说明」在报表编辑弹窗里填,支持多行(换行会原样保留)。 **显示与否由使用者自己决定,管理员不用配开关**:查询页标题右边有【报表说明】按钮,点一下就在标题下方展开, 再点一下(或点说明框右上角的 ✕)就收起。默认不显示,不会挡住查询条件; 开关状态记在浏览器里(`localStorage: report_desc_show`),下次打开还是上次的选择。 说明框用主题色高亮、左侧竖条强调;超过 4 行自动折叠,点「展开全部」看完整内容。 建议按这个结构写: ``` 【统计口径】 1. 只统计未作废(is_void = 0)的明细。 2. 金额 = SUM(amount),不含税。 【数据来源】 ReportDemo.t_income 【使用说明】 日期默认本月,科室留空为全部。 ``` 列表页的搜索框也会搜说明内容,方便按口径关键字找报表。 ### 7.5 结果表格:前端筛选、排序与样式 **① 每列都能再筛(不请求后端)** 结果工具条上有【筛选】按钮,**点了才展开表头下面那排输入框,默认收起**,表格更清爽; 展开后查询回来的数据直接在浏览器里过滤,不会重新查库: | 输入 | 含义 | 例子 | |------|------|------| | 普通文字 | 包含匹配,不区分大小写 | `门诊`、`D0`、`2026-10` | | `>` `<` `>=` `<=` | 数值/日期比较 | `>=1000`、`<2026-09-25` | | `=` `!=` | 精确等于 / 不等于 | `=门诊`、`!=作废` | 多列可以叠加,是「与」的关系;工具条会显示「筛选后 246 / 5000 行」,点「清除筛选」一键还原。 有筛选生效时,就算把筛选行收起来,【筛选】按钮也会变成主题色并带一个数字角标(生效的列数),不会忘记。 数字两边都是数字时按数值比,否则按字符串比(`yyyy-MM-dd` 的日期正好能这么比)。 输入有 140 ms 防抖,5000 行实测 16~27 ms 出结果。 ⚠️ 「导出 Excel」是后端重新查一遍再生成文件,拿的是**全量**结果,界面上的筛选和排序都不会带进 Excel (导出前会提示一句)。要小批量数据请改查询条件再导。 **② 五种表格样式,随主题配色** 结果区右上角「表格样式」下拉,选完记在浏览器里(`localStorage: report_table_style`): | 样式 | 效果 | |------|------| | 简约(白底) | 原来的样子,只有行分隔线 | | 斑马纹(隔行浅色) | **默认样式**,隔行浅主题色底,长表格不容易看串行 | | 网格线(全框线) | 每格都有框,像 Excel | | 浅彩头(表头淡主题色) | 表头用淡主题色,行保持白色 | | 深色表头(主题色) | 表头主题色打底白字,视觉冲击最强,适合投屏/汇报 | 五种样式都用 CSS 变量,切主题(红橙绿黑蓝)时配色会自动跟着变。 **③ 行数 / 耗时 / 截断提醒** 查询按钮右侧用色块芯片显示:`5000 行`(主题色芯片)、`耗时 223 ms`(灰芯片); 超过报表设置的最大返回行数时,多出一个橙色芯片 `⚠ 已截断:只显示前 1000 行`,不容易被忽略。 **④ 查看本次执行的 SQL** 工具条上还有个【SQL】按钮,点开就地展开**这一次查询实际执行的 SQL**(参数已代入值,不用再翻日志), 右上角「复制 SQL」一键拷走,直接粘到 SSMS 里就能复现。排查「同样的条件为什么线上查得不一样」时最省事。 和日志里那份是同一套拼装逻辑,区别只是日志会留历史,这里是当前这一次。 **⑤ 每一列都能点表头排序(不请求后端)** 点表头就排,三态循环:**升序 → 降序 → 取消**(取消即回到数据库给的原顺序)。 没排序的列右侧有个淡灰 `⇅` 提示这列能点;正在排的那列名字变主题色、底下多一条主题色边线、箭头变成 `▲` / `▼`。 工具条同时显示「已按 金额 降序」和【清除排序】,随时能看出现在按什么排、一键还原。 比较规则按列的**类型**走,不是一刀切按字符串比: | 列类型 | 怎么比 | 例子 | |--------|--------|------| | 数字(金额、笔数、数量) | 按数值大小 | `860.5 < 1250 < 7200`(不会出现 "1250" 排在 "860" 前面) | | 日期 / 日期时间 | 按时间先后 | `2024-01-05 < 2024-03-15`(`yyyy-MM-dd` 的字典序正好是时间序) | | 文字 | 中文按**拼音**、数字串按数值 | `儿童保健 < 换药费 < 门诊挂号费`;`第2项` 在 `第10项` 前面 | 空值(NULL / 空字符串,表格里显示成 `—`)**升序降序都沉到最后**,跟 Excel 一个习惯,不会一排序满屏都是 `—`。 排序是纯前端的(数据已经全在浏览器里),不重新查库,可以和「筛选」叠加: **先筛选 → 再排序 → 最后分页**,所以 5000 行以内点一下就是瞬时出结果。 重新点【查询】会把排序清掉——换了一批数据,列可能都变了,接着按上一列的序号排会错位。 **⑥ 金额显示成 `934650.5899999999` 是怎么回事** 这不是程序算错,是 SQL Server 的 `float` / `real` **二进制浮点**本身存不准十进制小数。 SSMS 里看着是 `934650.59`,是因为客户端按「最短可还原写法」显示;实际那个 double 是 `934650.59000000002…`,存储过程里再经过 `SUM`、乘法、`/100` 之类的运算,尾差还会继续累积, JDBC 把这个原始值原样传出来就露馅了。 程序的做法:**浮点值按可靠位数收尾**(double 取 15 位有效数字、real 取 7 位),把这种二进制尾差收掉: | 数据库里的类型 | 值 | 前台显示 | |----------------|-----|----------| | `float` | `934650.5899999999`(SUM 累积出来的) | `934650.59` | | `float` | `0.1 + 0.2` → `0.30000000000000004` | `0.3` | | `float` | `1234.5678` | `1234.5678`(**真实小数位不砍**,不会硬留 2 位) | | `float` | `27500.000000` | `27500` | | `decimal(18,2)` | `934650.59` | `934650.59`(精确类型本来就没问题) | 收尾只影响**显示和导出**,不动数据库里的值,也不做业务四舍五入(金额想固定两位小数请在自己 SQL 里 `CAST(... AS decimal(18,2))`,这样连数据库侧都是精确的,最稳)。 如果改完还看到长尾,按下面顺序排查: 1. 报表 SQL 里那一列是不是被 `CAST(... AS varchar)` 转过——转成字符串后程序无从判断,只能原样显示; 2. 值是不是 `decimal` 但精度写得太小又被放大(如 `decimal(18,2)` 乘除后再拼);这种请在 SQL 里显式 `CAST` 定小数位; 3. 确认服务换的是新 jar(老版本没有这个处理)。 ### 7.6 纯 SELECT 怎么带参数(和存储过程一样有查询条件) **支持。** 普通 `SELECT` 不需要包成存储过程,直接在 SQL 里写占位符,点「解析查询条件」就能像存储过程一样 生成界面上的查询框、按条件查库。两种占位符写法都认: | 写法 | 例子 | 执行时怎么处理 | 推荐度 | |------|------|----------------|--------| | 冒号参数 | `WHERE a.biz_date >= :开始日期` | 换成 JDBC 的 `?` 绑定,类型由驱动决定 | **推荐**,最稳 | | at 参数 | `WHERE (@科室 = '' OR a.dept_code = @科室)` | 自动在语句前拼 `DECLARE @科室 nvarchar(400); SET @科室 = ?;` | 适合条件里要多次复用的写法 | 一段完整的例子(带空值放行、模糊匹配): ```sql SELECT a.dept_code AS 科室, a.biz_date AS 日期, a.item_name AS 项目, a.amount AS 金额 FROM t_income a WHERE a.biz_date >= :开始日期 AND a.biz_date <= :结束日期 AND (@科室 = '' OR a.dept_code = @科室) AND (@项目 = '' OR a.item_name LIKE '%' + @项目 + '%') AND a.is_void = 0 ORDER BY a.biz_date ``` 解析出来的条件就是:开始日期(日期)、结束日期(日期)、科室(文本框)、项目(文本框), 点查询就按填的值过滤,留空的按 7.1 的规则放行。 **几个必须知道的点:** 1. **类型靠参数名猜**:纯 SELECT 没法从 SQL 里读出参数类型,参数名里带 `date` / `time` / `日期` / `时间` / `start` / `end` / `begin` 的默认给**日期时间控件**,其余给文本框。名字对不上就解析后手工把「控件类型」改成 数字 / 日期 / 下拉 / 勾选,改一次就跟着报表存下去。 2. **`@变量` 自己 DECLARE 过就不再自动声明**:SQL 里已经写了 `DECLARE @科室 nvarchar(50)`,系统认为这个变量由 SQL 自己维护,**不会**把界面填的值塞进去,那个条件框填了也没用。要么删掉自己的 DECLARE,要么改用 `:科室`。 3. **自动补的 DECLARE 类型跟控件类型走**:文本框 → `nvarchar(400)`、数字 → `decimal(18,4)`、日期 → `date`、 日期时间 → `datetime`、勾选 → `bit`。拿不准就用 `:参数`,让驱动去推断,不会踩隐式转换。 4. **同名参数出现多次没问题**:`:科室` 出现 3 次就绑 3 个 `?`,`@科室` 出现多次只 `SET` 一次、复用变量。 5. **注释和字符串里的 `@xx` / `:xx` 不会被当参数**(解析前先把注释和字符串内容清掉)。 6. **中文参数名可以用**:`@科室`、`:开始日期` 都行;存储过程名同理(中文过程名不会被截断)。 > 想快速确认识别结果:报表编辑器里点「解析查询条件」,下面的提示会列出「识别到 N 个查询参数:…」以及哪些 > `@变量` 会在执行时自动补 `DECLARE`。 ### 7.7 查询 / 导出日志(参数值可追溯) 报表右上角「查询/导出日志」打开,里面记着这张报表最近 100 次操作: | 列 | 说明 | |----|------| | 时间 / 用户 | 谁、什么时候 | | IP 地址 | 从哪台机器操作的(多个客户端共用账号时用来区分是谁;Nginx 反代下取的是真实客户端 IP,老日志没有这一列显示「—」) | | 操作 | **查询**(蓝)还是 **导出**(橙) | | 查询参数 | 一个条件一个小块:左边参数名、右边**实际执行时用的值**(已按报表默认值补齐,`{today}` 这类占位符也展开成真实日期);留空的显示灰色「(空)」 | | 行数 / 耗时 | 查询返回或导出的行数、这条操作花了多久 | | 结果 | 成功 / 失败,失败会在下面一行显示原因;导出成功时显示「导出 N 行 → 文件名」 | | SQL | 点这一行的「SQL」,就地展开**当时实际执行的 SQL**(参数已代入值),带「复制 SQL」按钮 | 顶部三段「全部 / 查询 / 导出」可直接过滤,条数就标在按钮上。 #### 全站日志汇总(管理员,不用一张张报表点进去) 单报表的日志只看得到那一张。要看「今天谁都在查什么、有没有人在导数据」, 去 **系统管理 → 查询日志**:所有报表的查询和导出记录按时间倒序排在一起。 | 能力 | 说明 | |------|------| | 关键词 | 一个框搜**报表名 / 用户 / IP / 参数值 / 说明**,模糊匹配 | | 操作 | 全部 / 查询 / 导出 | | 结果 | 全部 / 成功 / **只看失败**(排查报错最快) | | 报表 | 只看某一张报表(下拉列出全部报表) | | 日期区间 | 起始~结束,只传日期时结束日按当天 23:59:59 算 | | 汇总数字 | 当前筛选条件下的 `总条数 · 查询 N · 导出 N · 失败 N · 涉及 N 个用户 / N 个 IP`,失败数不为 0 时标红 | | 分页 | 每页 20 / 50 / 100 / 200,服务端分页(日志再多也不会一次全塞给浏览器) | | SQL | 和单报表弹窗一样,点「SQL」展开当时执行的语句,可复制 | 这个页面**只有管理员能进**(「系统管理」本来就只有管理员看得到,接口上也校验了, 普通用户直接调 `/api/logs` 会返回「只有管理员可以执行这个操作」)。 普通用户在报表里看单张报表的日志仍然由角色的「查看查询日志」权限控制。 日志一直留着,不会自动清理。数据量大想瘦身时,可以直接在 SQLite 里按时间删: ```sql DELETE FROM report_log WHERE create_time < '2025-01-01 00:00:00'; ``` **SQL 里长什么样**:`?` 占位符已经按当时的参数值填好,字符串加引号、数字不加、空条件写 `NULL`, 所以复制出来就能直接粘到 SSMS 里跑: ```sql -- 存储过程(参数按名称平铺) EXEC dbo.usp_AllUserGetXtgg @QYDM=1, @JUSTONE=0, @GGID=0 -- 普通 SELECT(自动补的 DECLARE 也在里面) DECLARE @StartDate datetime; SET @StartDate = '2026-10-01'; SELECT * FROM v_income WHERE 日期 >= @StartDate AND 科室 = '01' ``` - 只用于**展示**:真正发往数据库的永远是 `PreparedStatement` + `?`,不存在拼字符串注入的问题; - 字符串里的问号、注释里的问号不会被误当占位符(`WHERE remark = '为什么?'` 原样保留); - 存进 `report_log.sql_text`,单条最长 20000 字符,超了截断; - 查询失败时**也记 SQL** —— 报错时最需要看的就是那条语句;参数没填全、SQL 拼不出来时才留空; - 升级前的老日志没有 SQL,展开会提示「当时还没有记录 SQL」。 - 能不能看 SQL 由角色的「查看实际执行的 SQL」权限决定(见第五章):关掉后按钮不出现, 查询接口返回的 `sqlUsed`、日志接口返回的 `sql_text` 都会摘掉,不是只藏个按钮。 **导出为什么单独记一条**:点「导出 Excel」时后端会重新查一遍数据再生成文件,这次查询**不重复记成「查询」**, 而是在 Excel 生成完之后记一条「导出」记录 —— 这样「查了多少次」和「导了多少次」分得清, 导出失败(查询出错或生成文件出错)也记成失败的导出记录,原因写在说明里。 参数值在 `report_log.param_values` 里存成 JSON: ```json {"QYDM":"1","JUSTONE":"0","GGID":"0"} ``` 老版本存的是 `QYDM=1; JUSTONE=0; ` 这种纯文本,界面两种都能解析显示,老日志不用迁移。 --- ## 八、前端工程(脚手架)与目录结构 前端是标准 Vue 2 脚手架工程,在 `frontend/` 里,**由后端托管构建产物**(不用单独部署、不用跨域)。 ### 两种跑法 | 场景 | 命令 | 说明 | |------|------|------| | 日常使用 / 部署 | `start.bat` | 只跑 jar,前端产物已打进 jar | | 改前端代码 | `dev.bat` | 后端 8090 + 前端 5173,前端热更新,`/api` 由 Vite 代理到 8090 | | 出包 | `build.bat` | `npm run build` → 输出到 `src/main/resources/static` → `mvn clean package` | 单独执行的话就是: ```bash cd frontend npm install # 首次 npm run dev # http://localhost:5173 npm run build # 产物直接落到 ../src/main/resources/static ``` > 路由用的是 **hash 模式**(`/#/report`),刷新不会 404,后端也不用配 fallback。 ### 目录结构 ``` LuodengReportingSystem/ ├── start.bat / dev.bat / build.bat 启动 / 开发模式 / 打包 ├── data/report.db SQLite 元数据库(用户/数据源/报表定义/日志) ├── pom.xml ├── frontend/ ★ 前端脚手架 │ ├── package.json vue 2.7 / vue-router 3.6 / vite 5 │ ├── vite.config.js dev 代理 /api、build 输出到后端 static │ ├── index.html 唯一页面入口(无第三方 CDN) │ └── src/ │ ├── main.js 挂载 + 主题预热 + 401 处理 │ ├── App.vue 根组件 │ ├── api/ │ │ ├── request.js fetch 封装(X-Token / 统一错误 / 文件下载) │ │ └── index.js 全部接口按模块列出 │ ├── store/index.js Vue.observable 全局状态(用户/主题/Toast) │ ├── router/index.js 路由表 + 登录守卫 + 管理员守卫 │ ├── assets/app.css 五套主题的 CSS 变量 + 全部样式 │ ├── utils/clipboard.js 复制到剪贴板(内网 http 环境自动降级) │ ├── components/ AppModal / ToastBox / ThemeSwitch / PasswordDialog / SearchSelect │ └── views/ │ ├── Login.vue 登录页 │ ├── Layout.vue 顶栏(导航/主题/用户菜单) │ ├── report/ │ │ ├── ReportCenter.vue 报表中心外壳(左侧报表树 + 标签页管理) │ │ ├── ReportTabPanel.vue ★ 单个标签页的内容(条件 + 结果 + 日志),一页一个实例 │ │ ├── ReportEditor.vue 报表编辑表单(只有内容,无外壳) │ │ ├── ReportEditorDialog.vue 弹窗外壳(保存按钮在弹窗底部) │ │ ├── ReportEditorPage.vue 整页外壳(路由 /report/editor/:id?) │ │ └── LogsDialog.vue 查询 / 导出日志 │ └── admin/ │ ├── Admin.vue 页签外壳(子路由) │ ├── ConnTab.vue 数据源管理 │ ├── ReportTab.vue 报表管理 │ ├── UserTab.vue 用户管理 │ ├── RoleTab.vue 角色管理(勾报表权限) │ ├── OptionTab.vue ★ 下拉选项库(可复用的下拉框 SQL) │ ├── LogTab.vue 查询日志汇总(所有报表的日志) │ └── LoginLogTab.vue 登录记录 └── src/main/ ├── java/com/luodeng/report/ │ ├── LuodengReportingSystemApplication.java 启动类 │ ├── common/ 统一返回体、业务异常、全局异常处理 │ ├── config/ 登录拦截器、Token 上下文、管理员/报表权限判断 │ ├── controller/ Auth(登录/主题/登录记录)/ User / Role / Conn / Report / OptionSql(下拉选项)/ Log(日志汇总)/ Page │ ├── db/ │ │ ├── DataInitializer.java 建表 + 老库自动补字段 + 默认账号/角色/示例数据 │ │ ├── SqlServerExecutor.java 执行 SQL、连接串拼装、结果集转换 │ │ └── RowMappers.java 行 -> 对象映射 │ ├── model/ User / Role / DbConn / Report / ReportParam / OptionSql / QueryResult │ ├── service/ │ │ ├── AuthService.java 登录、会话、登录记录 │ │ ├── UserService.java 用户增删改、重置密码、启用禁用 │ │ ├── RoleService.java ★ 角色 + 多角色报表权限叠加算法 │ │ ├── ConnService.java 数据源 │ │ ├── ReportService.java 报表定义与执行 │ │ ├── OptionSqlService.java ★ 下拉选项库(引用校验、试运行) │ │ └── ExcelService.java 导出 xlsx │ └── util/ │ ├── SqlParser.java ★ SQL 解析(识别参数、类型、过程名) │ ├── SqlRender.java ★ 把 ? 换成参数值(日志/界面上展示实际执行的 SQL) │ ├── DateExpr.java {today}/{month_end} 这类日期占位符展开 │ ├── SecretBox.java ★ 数据源密码加密(AES-GCM,密钥外置) │ └── CryptoUtil.java 用户密码摘要 + 老格式密文兼容解密 └── resources/ ├── application.properties 端口、超时、Token 有效期 └── static/ ★ npm run build 的产物(不要手改) ├── index.html └── assets/index-*.js、index-*.css ``` --- ## 九、元数据库表结构(SQLite) | 表 | 用途 | 关键字段 | |----|------|----------| | `sys_user` | 用户 | username / password(盐+SHA256) / **is_admin** / **status**(1正常0禁用) / **theme** | | `sys_role` | 角色 | role_name / role_code / all_reports(1=全部报表) / **perm_log** / **perm_sql** / **perm_export**(1=允许) | | `sys_user_role` | 用户-角色 | user_id / role_id(多对多,权限叠加) | | `sys_role_report` | 角色-报表 | role_id / report_id(这个角色能看哪些报表) | | `db_conn` | 数据源 | host / port / db_name / username / password(**AES-256-GCM**,形如 `GCM:...`,密钥不在库里) | | `report` | 报表定义 | conn_id / sql_text / sql_type(SQL·PROC) / proc_name / max_rows / remark / desc_show | | `report_param` | 查询条件 | param_key / param_label / param_type / default_value / required / options_sql(就地写的选项 SQL) / **option_id**(引用下拉选项库) | | `option_sql` | **下拉选项库** | name(唯一) / conn_id(空=跟随报表的数据源) / sql_text / remark / create_by / update_time | | `report_log` | 查询 / 导出日志 | username / **param_values(JSON)** / **sql_text(实际执行的 SQL)** / **client_ip(客户端 IP)** / row_count / cost_ms / success / message / **action(QUERY·EXPORT)** | | `login_log` | 登录记录 | username / ip / user_agent / success / message | 备份要拷 **两个**文件:`data/report.db`(数据)和 `data/.secret.key`(数据源密码的密钥)。 老版本的库启动时会自动补列 (`sys_user.is_admin` / `status` / `theme`、`report.desc_show`、`report_log.action` / `sql_text` / `client_ip`、 `sys_role.perm_log` / `perm_sql` / `perm_export`、`report_param.option_id`),不用删库重建;升级上来的普通用户会自动挂到「全部报表」角色, 权限行为和老版本一致。新增的 `option_sql`(下拉选项库)表也会自动建好,空表不影响使用。 补上的 `report_log.action` 老记录一律算 `QUERY`(查询),参数值格式老的是纯文本、新的是 JSON,界面都认。 补上的 `report_log.sql_text` 老记录为空,日志里展开会提示「当时还没有记录 SQL」,从下一次查询起就有。 补上的 `report_log.client_ip` 老记录为空,界面上显示「—」,从下一次查询/导出起就会记录, 按 IP 搜索也只搜得到新记录。 补上的三个 `sys_role.perm_*` 一律默认 **1(允许)**,也就是升级后大家照旧能看日志、能看 SQL、能导出, 再按角色逐个收紧。 **数据源密码格式升级(一次性,自动)**:老版本用写死的密钥做 AES/ECB(`AES:` 前缀),新版本换成 AES-256-GCM(`GCM:` 前缀,密钥在 `data/.secret.key` 或环境变量里)。启动时会把库里所有 `AES:` 前缀的密码 解密后重新加密,日志里打印一行「已把 N 个数据源的密码升级为 AES-GCM 加密」。注意两点: - 升级前先备份 `data/report.db`;升级后**不要回退到旧 jar**,旧版本读不懂 `GCM:` 前缀,会把它当明文密码用,连不上库; - 解不开的老密码(比如手工改过库)不会被覆盖,保持原值并打 WARN,界面上重新填一次密码即可。 > `report.desc_show` 是早期「管理员控制说明是否显示」留下的列,现在显示与否由使用者自己点按钮决定, > 这一列只做兼容保留(升级时自动补列,不必手工处理,也不会影响显示)。 --- ## 十、Linux 部署(Nginx 反代)与清库 `deploy/` 目录放了 5 个文件,拷到服务器就能用: | 文件 | 用途 | |------|------| | `nginx-luodeng-report.conf` | Nginx server 配置(反代 8090) | | `restart.sh` | Linux 启停脚本(重启 / 停止 / 启动 / 状态) | | `reset-db.sh` | Linux 清库脚本 | | `reset-db.bat` | Windows 清库脚本 | | `reset-db-data.sql` | 只清报表/日志、保留账号的 SQL | ### 1. 程序目录结构 ``` /opt/luodeng-report/ ├── LuodengReportingSystem.jar ├── config/application.properties ← 外置配置,改端口改这里 ├── data/report.db ← SQLite 元数据库,首次启动自动生成 ├── data/.secret.key ← 数据源密码的加密密钥,首次启动自动生成(务必备份) └── static/ ← 前端构建产物(可选,给 Nginx 直发用) ``` ### 2. systemd 托管 ```ini # /etc/systemd/system/luodeng-report.service [Unit] Description=Luodeng Reporting System After=network.target [Service] Type=simple WorkingDirectory=/opt/luodeng-report ExecStart=/usr/bin/java -jar /opt/luodeng-report/LuodengReportingSystem.jar Restart=on-failure RestartSec=5 SuccessExitStatus=143 [Install] WantedBy=multi-user.target ``` ```bash systemctl daemon-reload && systemctl enable --now luodeng-report curl http://127.0.0.1:8090/api/health # 应返回 {"code":0,"msg":"ok",...} ``` ### 3. Nginx ```bash cp deploy/nginx-luodeng-report.conf /etc/nginx/conf.d/ vi /etc/nginx/conf.d/nginx-luodeng-report.conf # 改 server_name 为你的域名 nginx -t && nginx -s reload ``` 配置要点(改的时候别删): - `X-Real-IP` / `X-Forwarded-For` 两个头必须透传,否则登录记录和查询日志里的 IP 全是网关地址(`127.0.0.1`); - `proxy_read_timeout 300s`:后端单次查询超时是 180 秒,Nginx 必须更长,否则慢报表会被 Nginx 掐断; - 导出接口单独一个 location:关 `gzip`、关 `proxy_buffering`,大文件直接流给浏览器; - 文件里还留了两个可选段落:静态资源交给 Nginx 直发、IP 白名单(`allow/deny`)。 ### 4. 启停脚本(nohup 方式) 没上 systemd、直接用 `nohup java -jar ... &` 跑的,用 `deploy/restart.sh`: ```bash cp deploy/restart.sh /opt/luodeng-report/ && chmod +x /opt/luodeng-report/restart.sh cd /opt/luodeng-report ./restart.sh # 重启:停 → 起 → 等健康检查通过才返回 ./restart.sh --status # 看 PID / 已运行时长 / 内存 / 健康检查 ./restart.sh --stop # 只停 ./restart.sh --start # 只起(已有进程会跳过,不会重复拉) ./restart.sh -f # 20 秒不退出就 kill -9 ./restart.sh --no-wait # 启动后不等健康检查,立即返回 ``` 可用环境变量:`PORT=8090`、`JAVA_OPTS="-Xms256m -Xmx512m"`、`WAIT_SECONDS=60`。 脚本做的事: 1. 自动识别程序目录(脚本放程序目录或 `deploy/` 子目录都行); 2. 找旧进程:先读 `app.pid` 并核对命令行(防 PID 复用),再按 `-jar xxx.jar` 匹配; 3. 发 `SIGTERM`,最多等 20 秒内核态退出,超时按 `-f` 决定是否 `kill -9`; 4. 启动前检查 jar、java 命令、端口是否被别的程序占用; 5. 启动后轮询 `/api/health`,通过才打印成功;进程提前死掉会直接把本次新增日志打出来。 与原手工命令的差别只有一处:日志用 `>>` **追加**而不是 `>` 覆盖,同一天多次重启不会把 之前的日志冲掉。想恢复覆盖行为,把脚本里 `>> "$LOG_FILE"` 改成 `> "$LOG_FILE"` 即可。 > 用 systemd 托管的**不要**用这个脚本(会和 systemd 抢进程),改走 `systemctl restart luodeng-report`。 ### 5. 清库(恢复到出厂状态) **Windows**:双击 `deploy/reset-db.bat`,输入大写 `YES` 确认。 **Linux**: ```bash ./reset-db.sh # 交互确认;自动备份到 data/backup/ ./reset-db.sh -y # 跳过确认 ./reset-db.sh -y --force # 服务在跑时先结束进程再清 ./reset-db.sh --keep-users # 只清报表/数据源/日志,账号密码不变(需要 sqlite3) ``` 清库做的事:**先备份** `data/backup/report.db.时间戳` → 删除 `data/report.db` → 重启服务后自动重建表, 并重新生成默认账号 `admin / 123456`、`user / 123456`、默认角色「全部报表」、示例数据源和示例报表。 > 脚本会先检查端口是否还在监听,服务没停就不会动手(SQLite 文件被占用时删除会失败)。 > 只想清掉某几张表、保留账号:用 `reset-db-data.sql`,`sqlite3 ./data/report.db < reset-db-data.sql`。 --- ## 十一、常见问题 **1. 启动报 Port xxx was already in use** 本机 8080 被卫宁 OA 的 IIS 占了,所以默认用 **8090**。要换端口就改 `application.properties` 里的 `server.port`,或改 `start.bat` 里的 `PORT`。 **2. 测试连接提示「连接不上服务器」** 按顺序查:IP 能不能 ping 通 → 1433 端口通不通 → 实例名对不对(`IP\实例名` 反斜杠后面是实例名) → SQL Server 是否开了「TCP/IP 协议」和「SQL Server Browser」服务 → 防火墙。 **3. 报「从数据类型 nvarchar 转换为 datetime 时出错」** 某个月的日期条件被当成文本传了。把这个条件在报表编辑里的「控件类型」改成**日期**或**日期时间**。 **4. 查出来是 0 行,但直接跑 SQL 有数据** 多半是条件留空时传值不对: - 文本条件留空传的是 `''`,SQL 要写成 `(@p = '' OR 列 = @p)` - 日期条件留空传的是 `NULL`,SQL 要写成 `(@p IS NULL OR 日期 <= @p)` 先点「重置条件」,只留一个必填条件再试。 **5. 结果行数被截断** 每个报表有「最大返回行数」(默认 5000)。要全量就调大,或者导出 Excel(导出也受同样限制)。 建议加日期范围等条件,别一次拉几十万行。 **6. 导出 Excel 中文乱码?** 不会。Excel 是后端 POI 生成的,中文没问题。如果是打开 CSV 才乱码,那是 Excel 的老毛病,和本系统无关。 **7. 忘记密码** 普通用户忘了密码,让管理员在「用户管理」里点一下「重置密码」即可,统一重置为 **123456**。 管理员自己忘了密码:先停掉服务,用 SQLite 工具(DB Browser / sqlite3)打开 `data/report.db` 执行下面一句,再启动服务,`admin / 123456` 就恢复了(角色、报表、数据源都不受影响): ```sql DELETE FROM sys_user WHERE username = 'admin'; ``` **8. 普通用户登录后左侧一片空白** 说明他挂的角色里没有勾任何报表。到「用户管理」看他的角色,「角色管理」里看这个角色勾了哪些报表; 或者直接给他挂一个勾了「全部报表」的角色。 **9. 想让新报表自动对所有人可见** 在「角色管理」里把角色勾上「拥有全部报表」,以后新增报表不用再逐个授权; 只想精确授权的,就按报表逐个勾。 **10. 日志里一直刷 `NoResourceFoundException: No static resource favicon.ico`** 浏览器打开任何页面都会自动请求 `/favicon.ico`。以前没放这个图标文件,Spring 6 会把「静态资源找不到」 当成异常抛出来,被全局异常处理器按系统故障打了 ERROR 堆栈——**功能完全不受影响,纯日志噪音**。 现在两处都处理了: - 内置了图标 `static/favicon.ico`(源文件在 `frontend/public/`,构建时自动拷到 static 根目录), `index.html` 里也加了 ``; - `GlobalExceptionHandler` 单独接住 `NoResourceFoundException` / `NoHandlerFoundException`, 只返回 `404 {"code":404,"msg":"资源不存在:xxx"}` 并打一行 debug,不再打 ERROR 堆栈。 升级后如果还在刷,说明 jar 没换或服务没重启:确认 `static/favicon.ico` 或 jar 内的 `BOOT-INF/classes/static/favicon.ico` 存在,然后重启服务。 **11. 某个用户看不到「导出 Excel」/「查询日志」/「SQL」按钮** 不是 bug,是这个角色被关掉了对应权限。到「系统管理 → 角色管理」看这个角色「操作权限」那三个绿标签: - 想放开就编辑角色把它勾上,保存后**在线用户立刻生效**,不用重新登录; - 用户挂了多个角色时按并集算,只要有一个角色勾了就能用; - 管理员不受这个限制,永远都有。 **12. 批量改分组 / 数据源时提示「请先勾选要修改的报表」** 勾选框在每行最左边(表头那个是全选)。勾上后表格上方会出现「已选 N 个报表」的操作条, 这时两个批量按钮才起作用;改完会自动取消选择。 **13. 点了表头排序,导出的 Excel 怎么还是原来的顺序** 排序和筛选一样只作用在**界面上**,导出是后端重新查一遍全量数据再生成 xlsx,顺序跟数据库一致。 需要按某个顺序出 Excel,最稳的是在报表 SQL 里写 `ORDER BY`(存过定义里也行)。 另外**翻页不会打乱排序**——排序在前、分页在后,第 2 页接的是第 1 页的下一段。 **14. 数据库里明明是 934650.59,前台显示 934650.5899999999** `float` / `real` 是二进制浮点,存不住十进制小数,存储过程里 `SUM` 之后尾差会累积;SSMS 按最短写法显示 所以看着正常,JDBC 传出来的原始值就带尾巴。现在程序会按 double 15 位 / real 7 位有效数字收尾, `934650.5899999999` → `934650.59`,真实小数位不砍(`1234.5678` 仍是四位),导出 Excel 一并生效。 如果还长,检查这一列是不是被 `CAST(... AS varchar)` 转成了字符串(转成文本程序就只能原样显示), 或者确认服务已换成新 jar。详见 7.5 第 ⑥ 节。 **15. 日志里的 IP 地址是 172.x(网关地址)或者显示「—」** 两种情况: - **显示「—」**:那条是升级前的老日志,当时还没有记录 IP。从下一次查询/导出起就会有了。 - **是内网/网关地址**:说明请求经过 Nginx 但代理头没透传。程序优先读 `X-Forwarded-For` / `X-Real-IP`, Nginx 配置里要有这两行(`location / { proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; }`),否则只能拿到 Nginx 的地址。 `X-Forwarded-For` 是一串时会取**第一个**(真实客户端),IPv6 回环 `::1` 会写成 `127.0.0.1`。 **16. 普通用户看不到报表是「SQL 查询」还是「存储过程」,也看不到过程名** 这是刻意做的:报表标题下面那行类型标签和 `dbo.usp_xxx` 过程名属于实现细节,只给管理员看。 后端 `GET /api/report`、`GET /api/report/{id}` 对非管理员会把 `sqlType` / `procName` / `sqlText` 直接置空,所以就算有人按 F12 抄接口也拿不到;管理员打开同一张报表照旧能看到标签和过程名。 想让使用者也知道口径,用「报表说明」写人话,别把过程名写进报表名称(名称是所有人都能看到的)。 **17. 编辑数据源时密码框是空的,是不是密码丢了?** 没丢,是故意的:接口不再把密码发给浏览器(以前虽然显示成圆点,但 F12 把 `type` 改成 `text` 就能看明文, 现在浏览器里压根没有这个值,列表里也只显示 `••••••`)。密码框留空保存 = **沿用原密码**,不会覆盖; 要换密码就重新输入一遍。想确认密码对不对,直接点「测试连接」——后端会用库里已存的密码去连。 **18. 换机器 / 重装后,所有数据源都提示「密码解密失败」** 只拷了 `data/report.db`,没拷 `data/.secret.key`。数据源密码是用这个密钥加密的,密钥没了就解不开。 两条路:① 把旧机器的 `data/.secret.key` 拷过来;② 在界面上把每个数据源的密码重新填一遍(会自动用新密钥加密)。 另外如果用了环境变量 `REPORT_SECRET_KEY`,换机器后要保证值一致,否则同样解不开。 **19. 条件的下拉框是空的,或者提示「下拉选项加载失败」** 三种常见原因:① 条件引用的下拉选项被删了(正常删不掉,但可以手工改库);② 下拉选项的 SQL 报错, 比如表名写错、字段被改名——这类错误只会发生在点开下拉的那一刻,去「系统管理 → 下拉选项」点「试运行」能直接看到报错原文; ③ 选项设了「指定数据源」而那台库连不上,改成「跟随报表的数据源」或者修好那台库即可。 补一句:普通用户看不到选项里的 SQL(实现细节仅管理员可见),但下拉框照常能加载,这是后端算好 `hasOptions` 后单独放行的。 **20. 改了下拉选项库里的 SQL,报表里没生效?** 下拉选项是**每次点开下拉框时实时查**的,没有缓存,改完保存下一次点开就是新的。 如果看起来没变,基本是「那个条件用的是自定义 SQL,不是引用的选项」——去报表编辑器看「下拉选项」这一列, 显示「用已有:XXX」才算引用上;显示「自定义 SQL」则是条件里自己那段,选项库改了和它无关。 **21. 存储过程报表报「过程或函数 'xxx' 需要参数 '@yyy',但未提供该参数」** 说明 SQL 文本里有几个位置**没有对应上查询条件**。老版本遇到 `exec usp_x @kjqj,-1,-1,@kfdm` 这种写法会把过程的所有参数都列成条件,删掉中间两个之后,那两个位置就没人传,数据库就报这个错。 现在的处理是**按位置传参**:`@kjqj`、`@kfdm` 是条件,`-1` 是固定值,跟着一起传,不会再缺参数。 已经建好的老报表按下面做一次就行(不用重建): 1. 打开「报表管理」→ 编辑该报表 → 点 **解析查询条件**,条件会自动变成只有该填的那几个; 2. 保存。之后执行、导出都按 SQL 里的位置传值。 如果解析出来仍然不对,检查三点:SQL 里是不是写成了 `EXEC proc ?, ?`(`?` 认不出名字,要改成 `@参数名`); 过程名后面有没有漏逗号;`EXEC` 是不是写在注释或字符串里。 --- ## 十二、二次开发提示 - **加字段**:`DataInitializer.createTables()` 里改表结构 → 对应 model 加 public 字段 → 前端加输入框。 model 用的是 public 字段(没有 getter/setter),所以读库统一走 `RowMappers.of(Xxx.class)`, 不要用 Spring 的 `BeanPropertyRowMapper`(它只认 getter/setter,会映射成 null)。 - **加页面**:`frontend/src/views/` 新建 `.vue`,在 `router/index.js` 的 `children` 里挂一条路由; 需要管理员才能进的加 `meta: { admin: true }`,守卫会自动拦。 - **加接口**:`frontend/src/api/index.js` 里按模块加一个方法,页面里 import 直接用, 不要在组件里写裸路径(换部署前缀时只改一处)。 - **加控件类型**:`ParamDef.mapUiType()` 决定 SQL 类型→控件类型的映射; `ReportCenter.vue` 的查询条件区按 `paramType` 分支渲染,`ReportService.sqlTypeFor()` 决定自动 DECLARE 的类型。 - **改主题**:`frontend/src/assets/app.css` 顶部的 CSS 变量块,加一段 `:root[data-theme="xxx"]`, 再到 `store/index.js` 的 `THEMES` 里加一个色块即可(登录页和顶栏会自动出现)。 - **加数据源类型**(比如 MySQL):`SqlServerExecutor` 里加连接串拼装即可, 其余逻辑(结果集转 JSON、导出)是通用的。 - **数值显示**:结果集取值在 `SqlServerExecutor.readCell()`,浮点(`float`/`real`)走 `NumFormat.tidy()` 按有效位数收尾,精确十进制(`decimal`/`money`)保持 `BigDecimal` 原样——别在这里把 BigDecimal 转 double, 那样等于自己把精度丢了。 - **单元测试**:`mvnw.cmd test` 覆盖了 SQL 解析、SQL 拼装、Excel 导出、浮点数收尾、密码加解密、下拉选项校验、EXEC 位置传参(54 个用例)。 - **加权限点**:现在是三条线:管理员(`sys_user.is_admin`)、按角色看哪些报表(`sys_role_report`)、 角色的操作权限(`sys_role.perm_log / perm_sql / perm_export`)。要再加一个权限点,照这个套路: ① `sys_role` 加字段(`DataInitializer.migrateRoleTable()` 里补 `addColumnIfMissing`,默认值取 1 保证升级即兼容) → ② `Role` 加 public 字段、`RoleService.save()` 带上它、`RoleService.permOf()` 里算并集 → ③ `AuthService.Session` 加字段、`applyPerm()` 里赋值 → ④ `AuthContext` 加 `canXxx()/requireXxx()` → ⑤ Controller 里校验 → ⑥ 前端 `store.hasPerm('xxx')` 控制按钮显隐、角色弹窗加勾选框。 > 注意:**别手改 `src/main/resources/static/`**,那是 `npm run build` 的输出, > 下次构建会被清空。所有前端改动都写在 `frontend/src/` 里。 --- _作者:罗邓 | 版本 2.0.0(前端改为 Vue 脚手架工程)| 2026-10_