ソースを参照

refactor: 数据库初始化从代码迁移到 sql/init.sql 脚本

新增 sql/init.sql:一次性建表 + 灌入角色/权限/菜单种子数据
- 含 8 张表 DDL、ADMIN/USER 角色、14 个权限点、20 个菜单
- 含默认管理员 admin/admin123(BCrypt 哈希)
- DDL/DML 使用 IF NOT EXISTS / INSERT OR IGNORE,幂等可重复执行

删除代码中的初始化逻辑:
- SqliteDataSourceClient.initDemoTable 及 init-demo-table 配置
- UserService.initSchema
- RbacService.init(含 ensureRole/Permission/AdminUser 等辅助方法)
- MenuService.init(含 seedMenus/seedResearchTopDirs/seedFundamentalChildren)
- AuthAutoConfiguration / RbacAutoConfiguration 中的 @PostConstruct 钩子
- AuthProperties.Admin 嵌套类(管理员由脚本维护)

更新 README/SCHEMA/API.md 指向 sql/init.sql
启动流程:先执行 sqlite3 data/ir.db < sql/init.sql,再启动服务
ligb 3 週間 前
親
コミット
5c7c61e760

+ 1 - 1
API.md

@@ -23,7 +23,7 @@
 
 
 角色/权限随 JWT 下发,**修改角色或权限后需重新登录** 获取新 Token 生效。
 角色/权限随 JWT 下发,**修改角色或权限后需重新登录** 获取新 Token 生效。
 
 
-默认管理员:`admin / admin123`(配置于 `application.yml` 的 `mf.auth.admin`)。
+默认管理员:`admin / admin123`(由 `sql/init.sql` 一次性灌入)。
 
 
 ---
 ---
 
 

+ 6 - 1
README.md

@@ -15,11 +15,16 @@ Spring Boot 2.7.x + Java 8 + WAR(兼容东方通 TongWeb 7/8 部署)。
 ## 启动(本地)
 ## 启动(本地)
 
 
 ```bash
 ```bash
+# 1. 先初始化数据库(执行一次,建表 + 灌入种子数据 + 创建默认管理员 admin/admin123)
+sqlite3 data/ir.db < sql/init.sql
+
+# 2. 启动服务
 mvn spring-boot:run       # 或 mvn package 后 java -jar target/ir-backend.war
 mvn spring-boot:run       # 或 mvn package 后 java -jar target/ir-backend.war
 ```
 ```
 
 
-验证:`curl http://localhost:8080/api/hello`
+验证:`curl http://localhost:8080/api/hello`,登录:`curl -X POST http://localhost:8080/api/auth/login -H "Content-Type: application/json" -d '{"username":"admin","password":"admin123"}'`
 
 
+> 数据库初始化已从代码中剥离,统一由 `sql/init.sql` 脚本负责;脚本使用 `IF NOT EXISTS` / `INSERT OR IGNORE`,重复执行安全。
 > 本机未安装 Maven 时,可先下载并配置 Maven,或改用 IDEA 内置 Maven 运行。
 > 本机未安装 Maven 时,可先下载并配置 Maven,或改用 IDEA 内置 Maven 运行。
 
 
 ## 远端
 ## 远端

+ 12 - 13
SCHEMA.md

@@ -1,19 +1,18 @@
 # 数据库表结构
 # 数据库表结构
 
 
-投研平台后端当前使用 SQLite(`data/ir.db`)。表结构在启动时通过代码自动创建:
+投研平台后端当前使用 SQLite(`data/ir.db`)。表结构与种子数据由 **`sql/init.sql`** 一次性脚本初始化(应用代码不再做建表/灌种子):
 
 
-- 数据源客户端:`SqliteDataSourceClient#initDemoTable` → `demo_user`
-- 用户表:`UserService#initSchema` → `sys_user`
-- RBAC 表:`RbacService#init` → `sys_role` / `sys_permission` / `sys_user_role` / `sys_role_permission`
-- 菜单表:`MenuService#init` → `sys_menu` / `sys_role_menu`(独立菜单权限体系,与接口权限解耦)
+```
+sqlite3 data/ir.db < sql/init.sql
+```
 
 
-所有 DDL 均为 `CREATE TABLE IF NOT EXISTS`(幂等,重复启动不会重建已存在的表)。
+脚本中所有 DDL/DML 使用 `IF NOT EXISTS` / `INSERT OR IGNORE`,可重复执行。
 
 
 ---
 ---
 
 
 ## 1. demo_user — 演示用户表
 ## 1. demo_user — 演示用户表
 
 
-> 由 `init-demo-table` 配置控制是否创建,仅供数据源演示接口使用。
+> 供数据源演示接口 `/api/datasource/*` 使用。
 
 
 | 字段 | 类型 | 约束 | 说明 |
 | 字段 | 类型 | 约束 | 说明 |
 |---|---|---|---|
 |---|---|---|---|
@@ -25,7 +24,7 @@
 
 
 ## 2. sys_user — 系统用户表
 ## 2. sys_user — 系统用户表
 
 
-> 由 `UserService#initSchema` 创建;`RbacService` 在启动时校验并创建内置管理员。
+> 默认管理员 `admin / admin123` 由 `sql/init.sql` 灌入(BCrypt 哈希)。
 
 
 | 字段 | 类型 | 约束 | 说明 |
 | 字段 | 类型 | 约束 | 说明 |
 |---|---|---|---|
 |---|---|---|---|
@@ -90,7 +89,7 @@
 
 
 ## 7. sys_menu — 菜单表(独立菜单权限体系)
 ## 7. sys_menu — 菜单表(独立菜单权限体系)
 
 
-> 由 `MenuService#init` 创建。菜单与接口权限(`sys_permission`)**完全解耦**:菜单自身是一棵树,通过 `sys_role_menu` 直接授予角色,不读取任何接口权限编码。
+> 菜单与接口权限(`sys_permission`)**完全解耦**:菜单自身是一棵树,通过 `sys_role_menu` 直接授予角色,不读取任何接口权限编码。
 
 
 | 字段 | 类型 | 约束 | 说明 |
 | 字段 | 类型 | 约束 | 说明 |
 |---|---|---|---|
 |---|---|---|---|
@@ -134,13 +133,13 @@ sys_user ──< sys_user_role >── sys_role ──< sys_role_menu >── sy
 - 用户的最终权限 = 其所有角色权限的并集(查询时已 `DISTINCT` 去重)。
 - 用户的最终权限 = 其所有角色权限的并集(查询时已 `DISTINCT` 去重)。
 - 用户的可见菜单 = 其所有角色被授予菜单的并集;下发时向上补全父级、剔除空目录。
 - 用户的可见菜单 = 其所有角色被授予菜单的并集;下发时向上补全父级、剔除空目录。
 
 
-## 种子数据(启动时自动灌入,幂等)
+## 种子数据(由 `sql/init.sql` 一次性灌入)
 
 
 | 类别 | 内容 |
 | 类别 | 内容 |
 |---|---|
 |---|---|
 | 角色 | `ADMIN`(超级管理员,拥有全部权限)、`USER`(普通用户,默认注册用户角色) |
 | 角色 | `ADMIN`(超级管理员,拥有全部权限)、`USER`(普通用户,默认注册用户角色) |
 | 权限 | `user:list` `user:create` `user:update` `user:delete`、`role:list` `role:create` `role:update` `role:delete` `role:assign`、`permission:list` `permission:create` `permission:delete`、`data:query` `data:write` |
 | 权限 | `user:list` `user:create` `user:update` `user:delete`、`role:list` `role:create` `role:update` `role:delete` `role:assign`、`permission:list` `permission:create` `permission:delete`、`data:query` `data:write` |
-| 菜单 | `工作台`、`系统管理(目录)→ 用户管理/角色管理/权限管理/菜单管理`、`数据源演示`;启动时全部授权给 `ADMIN` 角色,新建菜单也会自动授权给 `ADMIN` |
-| 管理员 | 由 `ir.auth.admin.username/password` 配置(默认 `admin / admin123`),创建并授予 `ADMIN` 角色 |
+| 菜单 | `工作台`、`系统管理(目录)→ 用户管理/角色管理/权限管理/菜单管理`、`数据源演示`、投研业务 6 个一级目录(`基本面 / 政策面 / 资金面 / 债市分析 / 权益分析 / 多资产`),其中 `基本面` 下含 7 个二级菜单(`GDP / 投资 / 出口 / 消费 / 通胀 / 供给 / 景气度`);脚本一次性把全部菜单授权给 `ADMIN`,新建菜单仍会运行时自动授权给 `ADMIN` |
+| 管理员 | `admin / admin123`(BCrypt 哈希写死在脚本首注,使用前请尽快改密) |
 
 
-新注册用户由 `UserService` 自动分配默认角色 `USER`。
+新注册用户由 `UserService` 在运行时自动分配默认角色 `USER`。

+ 176 - 0
sql/init.sql

@@ -0,0 +1,176 @@
+-- ============================================================
+-- 投研平台后端 · 数据库初始化脚本(SQLite)
+-- ============================================================
+-- 适用:Spring Boot 2.7 + Java 8 + SQLite (data/ir.db)
+-- 用法:执行一次本脚本即可完成建表 + 种子数据灌入。
+--       sqlite3 data/ir.db < sql/init.sql
+--       或:在 sqlite3 交互终端里 .read sql/init.sql
+-- 幂等:所有 DDL/DML 使用 IF NOT EXISTS / OR IGNORE,重复执行安全。
+-- 默认管理员:admin / admin123(BCrypt 哈希见 sys_user 插入行)
+-- ============================================================
+
+PRAGMA foreign_keys = ON;
+
+-- ============================================================
+-- 1. 建表
+-- ============================================================
+
+-- 演示用户表(供 /api/datasource/* 演示接口使用)
+CREATE TABLE IF NOT EXISTS demo_user (
+    id          INTEGER PRIMARY KEY AUTOINCREMENT,
+    name        TEXT    NOT NULL,
+    created_at  TIMESTAMP DEFAULT CURRENT_TIMESTAMP
+);
+
+-- 系统用户表
+CREATE TABLE IF NOT EXISTS sys_user (
+    id          INTEGER PRIMARY KEY AUTOINCREMENT,
+    username    TEXT    NOT NULL UNIQUE,
+    password    TEXT    NOT NULL,                -- BCrypt 哈希
+    nickname    TEXT,
+    enabled     INTEGER NOT NULL DEFAULT 1,
+    created_at  TIMESTAMP DEFAULT CURRENT_TIMESTAMP
+);
+
+-- 角色表
+CREATE TABLE IF NOT EXISTS sys_role (
+    id          INTEGER PRIMARY KEY AUTOINCREMENT,
+    code        TEXT    NOT NULL UNIQUE,
+    name        TEXT    NOT NULL,
+    description TEXT,
+    created_at  TIMESTAMP DEFAULT CURRENT_TIMESTAMP
+);
+
+-- 权限点表(接口权限,与菜单权限解耦)
+CREATE TABLE IF NOT EXISTS sys_permission (
+    id          INTEGER PRIMARY KEY AUTOINCREMENT,
+    code        TEXT    NOT NULL UNIQUE,
+    name        TEXT    NOT NULL,
+    description TEXT,
+    created_at  TIMESTAMP DEFAULT CURRENT_TIMESTAMP
+);
+
+-- 用户-角色关联
+CREATE TABLE IF NOT EXISTS sys_user_role (
+    user_id INTEGER NOT NULL,
+    role_id INTEGER NOT NULL,
+    PRIMARY KEY (user_id, role_id)
+);
+
+-- 角色-权限关联
+CREATE TABLE IF NOT EXISTS sys_role_permission (
+    role_id       INTEGER NOT NULL,
+    permission_id INTEGER NOT NULL,
+    PRIMARY KEY (role_id, permission_id)
+);
+
+-- 菜单表(独立菜单权限体系)
+CREATE TABLE IF NOT EXISTS sys_menu (
+    id          INTEGER PRIMARY KEY AUTOINCREMENT,
+    parent_id   INTEGER,                         -- NULL = 顶级目录
+    name        TEXT    NOT NULL,
+    type        TEXT    NOT NULL DEFAULT 'menu', -- dir=目录 / menu=菜单
+    route       TEXT,                            -- 前端路由 path
+    component   TEXT,                            -- 前端组件路径
+    icon        TEXT,                            -- 菜单图标(Element Plus)
+    sort        INTEGER DEFAULT 0,               -- 排序号(升序)
+    visible     INTEGER DEFAULT 1,               -- 是否可见
+    status      INTEGER DEFAULT 1,               -- 是否启用
+    created_at  TIMESTAMP DEFAULT CURRENT_TIMESTAMP
+);
+
+-- 角色-菜单关联
+CREATE TABLE IF NOT EXISTS sys_role_menu (
+    role_id INTEGER NOT NULL,
+    menu_id INTEGER NOT NULL,
+    PRIMARY KEY (role_id, menu_id)
+);
+
+-- ============================================================
+-- 2. 种子数据:角色
+-- ============================================================
+INSERT OR IGNORE INTO sys_role(id, code, name, description) VALUES
+    (1, 'ADMIN', '超级管理员', '系统内置,拥有全部权限'),
+    (2, 'USER',  '普通用户',   '系统内置,默认用户角色');
+
+-- ============================================================
+-- 3. 种子数据:权限点
+-- ============================================================
+INSERT OR IGNORE INTO sys_permission(id, code, name, description) VALUES
+    (1,  'user:list',        'user:list',        NULL),
+    (2,  'user:create',      'user:create',      NULL),
+    (3,  'user:update',      'user:update',      NULL),
+    (4,  'user:delete',      'user:delete',      NULL),
+    (5,  'role:list',        'role:list',        NULL),
+    (6,  'role:create',      'role:create',      NULL),
+    (7,  'role:update',      'role:update',      NULL),
+    (8,  'role:delete',      'role:delete',      NULL),
+    (9,  'role:assign',      'role:assign',      NULL),
+    (10, 'permission:list',  'permission:list',  NULL),
+    (11, 'permission:create','permission:create',NULL),
+    (12, 'permission:delete','permission:delete',NULL),
+    (13, 'data:query',       'data:query',       NULL),
+    (14, 'data:write',       'data:write',       NULL);
+
+-- ADMIN 拥有全部权限
+INSERT OR IGNORE INTO sys_role_permission(role_id, permission_id)
+SELECT 1, id FROM sys_permission;
+
+-- ============================================================
+-- 4. 种子数据:默认管理员账号
+-- ============================================================
+-- 密码:admin123(BCrypt $2a$10$W6qORgtPvZnIj5KMJj0GZu3fYtAqgOUjAl6vQy7wt4kFeYVPVkwti)
+-- 首次部署后请尽快通过 UPDATE sys_user SET password=? WHERE username='admin' 修改。
+INSERT OR IGNORE INTO sys_user(id, username, password, nickname, enabled) VALUES
+    (1, 'admin', '$2a$10$W6qORgtPvZnIj5KMJj0GZu3fYtAqgOUjAl6vQy7wt4kFeYVPVkwti', '管理员', 1);
+
+-- 管理员授予 ADMIN 角色
+INSERT OR IGNORE INTO sys_user_role(user_id, role_id) VALUES (1, 1);
+
+-- ============================================================
+-- 5. 种子数据:菜单(顶级 + 二级)
+-- ============================================================
+
+-- 5.1 通用顶级(工作台 / 系统管理 / 数据源演示)
+INSERT OR IGNORE INTO sys_menu(id, parent_id, name, type, route, component, icon, sort) VALUES
+    (1, NULL, '工作台',     'menu', '/dashboard',  'dashboard/index',  'HomeFilled', 1),
+    (2, NULL, '系统管理',   'dir',  NULL,           NULL,               'Setting',    1),
+    (7, NULL, '数据源演示', 'menu', '/datasource',  'datasource/index', 'Coin',       2);
+
+-- 5.2 系统管理 → 子菜单
+INSERT OR IGNORE INTO sys_menu(id, parent_id, name, type, route, component, icon, sort) VALUES
+    (3, 2, '用户管理', 'menu', '/system/user',       'system/user/index',       'User',   1),
+    (4, 2, '角色管理', 'menu', '/system/role',       'system/role/index',       'Avatar', 2),
+    (5, 2, '权限管理', 'menu', '/system/permission', 'system/permission/index', 'Key',    3),
+    (6, 2, '菜单管理', 'menu', '/system/menu',       'system/menu/index',       'Menu',   4);
+
+-- 5.3 投研业务 6 个一级目录
+INSERT OR IGNORE INTO sys_menu(id, parent_id, name, type, route, component, icon, sort) VALUES
+    (8,  NULL, '基本面',   'dir', NULL, NULL, 'TrendCharts', 3),
+    (16, NULL, '政策面',   'dir', NULL, NULL, 'Document',    4),
+    (17, NULL, '资金面',   'dir', NULL, NULL, 'Money',       5),
+    (18, NULL, '债市分析', 'dir', NULL, NULL, 'Tickets',     6),
+    (19, NULL, '权益分析', 'dir', NULL, NULL, 'DataLine',    7),
+    (20, NULL, '多资产',   'dir', NULL, NULL, 'Grid',        8);
+
+-- 5.4 基本面 → 7 个二级菜单
+INSERT OR IGNORE INTO sys_menu(id, parent_id, name, type, route, component, icon, sort) VALUES
+    (9,  8, 'GDP',     'menu', '/fundamental/gdp',         'fundamental/gdp/index',         'DataAnalysis',  1),
+    (10, 8, '投资',    'menu', '/fundamental/investment',  'fundamental/investment/index',  'Coin',          2),
+    (11, 8, '出口',    'menu', '/fundamental/export',      'fundamental/export/index',      'Promotion',     3),
+    (12, 8, '消费',    'menu', '/fundamental/consumption', 'fundamental/consumption/index', 'ShoppingCart',  4),
+    (13, 8, '通胀',    'menu', '/fundamental/inflation',   'fundamental/inflation/index',   'Warning',       5),
+    (14, 8, '供给',    'menu', '/fundamental/supply',      'fundamental/supply/index',      'Box',           6),
+    (15, 8, '景气度',  'menu', '/fundamental/prosperity',  'fundamental/prosperity/index',  'Odometer',      7);
+
+-- ============================================================
+-- 6. 菜单授权:ADMIN 拥有全部菜单
+-- ============================================================
+INSERT OR IGNORE INTO sys_role_menu(role_id, menu_id)
+SELECT 1, id FROM sys_menu;
+
+-- ============================================================
+-- 完成
+-- ============================================================
+-- 默认管理员:admin / admin123
+-- 默认用户:注册即可,自动授予 USER 角色

+ 3 - 16
src/main/java/com/nylc/touyan/auth/AuthAutoConfiguration.java

@@ -1,6 +1,5 @@
 package com.nylc.touyan.auth;
 package com.nylc.touyan.auth;
 
 
-import com.nylc.touyan.datasource.DataSourceClient;
 import org.springframework.boot.context.properties.EnableConfigurationProperties;
 import org.springframework.boot.context.properties.EnableConfigurationProperties;
 import org.springframework.boot.web.servlet.FilterRegistrationBean;
 import org.springframework.boot.web.servlet.FilterRegistrationBean;
 import org.springframework.context.annotation.Bean;
 import org.springframework.context.annotation.Bean;
@@ -9,30 +8,18 @@ import org.springframework.core.annotation.Order;
 import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
 import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
 import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
 import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
 
 
-import javax.annotation.PostConstruct;
-
 /**
 /**
  * 鉴权模块装配入口:
  * 鉴权模块装配入口:
  * - 注册 JWT 过滤器(作用于 /api/**,兼顾 CORS);
  * - 注册 JWT 过滤器(作用于 /api/**,兼顾 CORS);
- * - 注册 {@link RequireAuth} 拦截器;
- * - 启动时初始化 sys_user 表。
+ * - 注册 {@link RequireAuth} 拦截器。
+ *
+ * <p>数据库表结构与种子数据由 {@code sql/init.sql} 维护,不再在此处触发建表。</p>
  */
  */
 @Order(0)
 @Order(0)
 @Configuration
 @Configuration
 @EnableConfigurationProperties(AuthProperties.class)
 @EnableConfigurationProperties(AuthProperties.class)
 public class AuthAutoConfiguration implements WebMvcConfigurer {
 public class AuthAutoConfiguration implements WebMvcConfigurer {
 
 
-    private final UserService userService;
-
-    public AuthAutoConfiguration(UserService userService) {
-        this.userService = userService;
-    }
-
-    @PostConstruct
-    public void initSchema() {
-        userService.initSchema();
-    }
-
     @Bean
     @Bean
     public AuthFilter authFilter(JwtUtil jwtUtil, AuthProperties properties) {
     public AuthFilter authFilter(JwtUtil jwtUtil, AuthProperties properties) {
         return new AuthFilter(jwtUtil, properties);
         return new AuthFilter(jwtUtil, properties);

+ 3 - 16
src/main/java/com/nylc/touyan/auth/AuthProperties.java

@@ -16,13 +16,15 @@ import org.springframework.boot.context.properties.ConfigurationProperties;
  *       - /api/auth/login
  *       - /api/auth/login
  *       - /api/auth/register
  *       - /api/auth/register
  * </pre>
  * </pre>
+ *
+ * <p>默认管理员账号由 {@code sql/init.sql} 创建与维护,
+ * 不再通过本配置类注入。</p>
  */
  */
 @ConfigurationProperties(prefix = "ir.auth")
 @ConfigurationProperties(prefix = "ir.auth")
 public class AuthProperties {
 public class AuthProperties {
 
 
     private Jwt jwt = new Jwt();
     private Jwt jwt = new Jwt();
     private String[] skipPaths = new String[]{"/api/auth/login", "/api/auth/register"};
     private String[] skipPaths = new String[]{"/api/auth/login", "/api/auth/register"};
-    private Admin admin = new Admin();
 
 
     public Jwt getJwt() { return jwt; }
     public Jwt getJwt() { return jwt; }
     public void setJwt(Jwt jwt) { this.jwt = jwt; }
     public void setJwt(Jwt jwt) { this.jwt = jwt; }
@@ -30,21 +32,6 @@ public class AuthProperties {
     public String[] getSkipPaths() { return skipPaths; }
     public String[] getSkipPaths() { return skipPaths; }
     public void setSkipPaths(String[] skipPaths) { this.skipPaths = skipPaths; }
     public void setSkipPaths(String[] skipPaths) { this.skipPaths = skipPaths; }
 
 
-    public Admin getAdmin() { return admin; }
-    public void setAdmin(Admin admin) { this.admin = admin; }
-
-    /** 内置管理员账号(首次启动自动创建并授予 ADMIN 角色)。 */
-    public static class Admin {
-        private String username = "admin";
-        private String password = "admin123";
-
-        public String getUsername() { return username; }
-        public void setUsername(String username) { this.username = username; }
-
-        public String getPassword() { return password; }
-        public void setPassword(String password) { this.password = password; }
-    }
-
     public static class Jwt {
     public static class Jwt {
         /** HMAC 密钥(≥32 字节);生产请通过环境变量注入。 */
         /** HMAC 密钥(≥32 字节);生产请通过环境变量注入。 */
         private String secret = "touyan-default-secret-please-change-in-production-256bit";
         private String secret = "touyan-default-secret-please-change-in-production-256bit";

+ 2 - 12
src/main/java/com/nylc/touyan/auth/UserService.java

@@ -14,6 +14,8 @@ import java.util.Map;
  * 用户业务:注册、登录、按用户名查询。
  * 用户业务:注册、登录、按用户名查询。
  * 复用 {@link DataSourceClient} 访问底层数据源(当前为 SQLite),未来切换 MySQL 业务代码零改动。
  * 复用 {@link DataSourceClient} 访问底层数据源(当前为 SQLite),未来切换 MySQL 业务代码零改动。
  * 登录/注册时通过 {@link RbacService} 加载角色与权限并随 token(AuthUser)下发。
  * 登录/注册时通过 {@link RbacService} 加载角色与权限并随 token(AuthUser)下发。
+ *
+ * <p>数据库表结构与种子数据由 {@code sql/init.sql} 初始化,本类不再做建表/灌种子。</p>
  */
  */
 @Service
 @Service
 public class UserService {
 public class UserService {
@@ -29,18 +31,6 @@ public class UserService {
         this.rbacService = rbacService;
         this.rbacService = rbacService;
     }
     }
 
 
-    public void initSchema() {
-        dataSource.execute(
-                "CREATE TABLE IF NOT EXISTS sys_user (" +
-                        "id INTEGER PRIMARY KEY AUTOINCREMENT," +
-                        "username TEXT NOT NULL UNIQUE," +
-                        "password TEXT NOT NULL," +
-                        "nickname TEXT," +
-                        "enabled INTEGER NOT NULL DEFAULT 1," +
-                        "created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP)");
-        log.info("[auth] 用户表 sys_user 已就绪");
-    }
-
     public AuthUser register(String username, String rawPassword, String nickname) {
     public AuthUser register(String username, String rawPassword, String nickname) {
         if (username == null || username.trim().isEmpty()) {
         if (username == null || username.trim().isEmpty()) {
             throw new AuthException("BAD_REQUEST", "用户名不能为空");
             throw new AuthException("BAD_REQUEST", "用户名不能为空");

+ 0 - 5
src/main/java/com/nylc/touyan/datasource/DataSourceProperties.java

@@ -41,14 +41,9 @@ public class DataSourceProperties {
     public static class Sqlite {
     public static class Sqlite {
         /** SQLite 数据库文件路径(相对工作目录或绝对路径) */
         /** SQLite 数据库文件路径(相对工作目录或绝对路径) */
         private String path = "data/ir.db";
         private String path = "data/ir.db";
-        /** 启动时是否自动初始化演示表 */
-        private boolean initDemoTable = true;
 
 
         public String getPath() { return path; }
         public String getPath() { return path; }
         public void setPath(String path) { this.path = path; }
         public void setPath(String path) { this.path = path; }
-
-        public boolean isInitDemoTable() { return initDemoTable; }
-        public void setInitDemoTable(boolean initDemoTable) { this.initDemoTable = initDemoTable; }
     }
     }
 
 
     public static class Mysql {
     public static class Mysql {

+ 0 - 15
src/main/java/com/nylc/touyan/datasource/SqliteDataSourceClient.java

@@ -31,12 +31,10 @@ public class SqliteDataSourceClient implements DataSourceClient {
     private static final Logger log = LoggerFactory.getLogger(SqliteDataSourceClient.class);
     private static final Logger log = LoggerFactory.getLogger(SqliteDataSourceClient.class);
 
 
     private final String path;
     private final String path;
-    private final boolean initDemoTable;
     private Connection connection;
     private Connection connection;
 
 
     public SqliteDataSourceClient(DataSourceProperties.Sqlite cfg) {
     public SqliteDataSourceClient(DataSourceProperties.Sqlite cfg) {
         this.path = cfg.getPath();
         this.path = cfg.getPath();
-        this.initDemoTable = cfg.isInitDemoTable();
     }
     }
 
 
     @PostConstruct
     @PostConstruct
@@ -54,19 +52,6 @@ public class SqliteDataSourceClient implements DataSourceClient {
             st.execute("PRAGMA foreign_keys=ON");
             st.execute("PRAGMA foreign_keys=ON");
         }
         }
         log.info("[datasource:sqlite] 已连接 {}", dbFile.getAbsolutePath());
         log.info("[datasource:sqlite] 已连接 {}", dbFile.getAbsolutePath());
-        if (initDemoTable) {
-            initDemoTable();
-        }
-    }
-
-    private void initDemoTable() throws SQLException {
-        try (Statement st = connection.createStatement()) {
-            st.execute("CREATE TABLE IF NOT EXISTS demo_user (" +
-                    "id INTEGER PRIMARY KEY AUTOINCREMENT," +
-                    "name TEXT NOT NULL," +
-                    "created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP)");
-            log.info("[datasource:sqlite] 演示表 demo_user 已就绪");
-        }
     }
     }
 
 
     @PreDestroy
     @PreDestroy

+ 6 - 74
src/main/java/com/nylc/touyan/menu/MenuService.java

@@ -2,8 +2,6 @@ package com.nylc.touyan.menu;
 
 
 import com.nylc.touyan.auth.AuthException;
 import com.nylc.touyan.auth.AuthException;
 import com.nylc.touyan.datasource.DataSourceClient;
 import com.nylc.touyan.datasource.DataSourceClient;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
 import org.springframework.stereotype.Service;
 import org.springframework.stereotype.Service;
 
 
 import java.util.ArrayList;
 import java.util.ArrayList;
@@ -16,46 +14,20 @@ import java.util.Set;
 /**
 /**
  * 独立菜单权限服务:
  * 独立菜单权限服务:
  * - 数据模型:sys_menu(菜单树)/ sys_role_menu(角色-菜单授权);
  * - 数据模型:sys_menu(菜单树)/ sys_role_menu(角色-菜单授权);
- * - 与 sys_permission(接口权限)完全解耦:菜单可见性只由“用户的角色 → sys_role_menu”决定,
+ * - 与 sys_permission(接口权限)完全解耦:菜单可见性只由"用户的角色 → sys_role_menu"决定,
  *   不读取任何接口权限编码,接口鉴权也不会因菜单授权而放行。
  *   不读取任何接口权限编码,接口鉴权也不会因菜单授权而放行。
- * - ADMIN 角色通过 sys_role_menu 被授予全部菜单(种子 + 新建菜单自动授予),保证超管全量可见。
+ * - 表结构与种子数据由 {@code sql/init.sql} 初始化,本服务不再做建表/灌种子;
+ *   新建菜单仍会自动授权给 ADMIN(保证超级管理员始终可管理)。
  */
  */
 @Service
 @Service
 public class MenuService {
 public class MenuService {
 
 
-    private static final Logger log = LoggerFactory.getLogger(MenuService.class);
-
     private final DataSourceClient dataSource;
     private final DataSourceClient dataSource;
 
 
     public MenuService(DataSourceClient dataSource) {
     public MenuService(DataSourceClient dataSource) {
         this.dataSource = dataSource;
         this.dataSource = dataSource;
     }
     }
 
 
-    /** 初始化菜单表并灌入种子(幂等)。 */
-    public void init() {
-        dataSource.execute(
-                "CREATE TABLE IF NOT EXISTS sys_menu (" +
-                        "id INTEGER PRIMARY KEY AUTOINCREMENT," +
-                        "parent_id INTEGER," +
-                        "name TEXT NOT NULL," +
-                        "type TEXT NOT NULL DEFAULT 'menu'," +
-                        "route TEXT," +
-                        "component TEXT," +
-                        "icon TEXT," +
-                        "sort INTEGER DEFAULT 0," +
-                        "visible INTEGER DEFAULT 1," +
-                        "status INTEGER DEFAULT 1," +
-                        "created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP)");
-        dataSource.execute(
-                "CREATE TABLE IF NOT EXISTS sys_role_menu (" +
-                        "role_id INTEGER NOT NULL," +
-                        "menu_id INTEGER NOT NULL," +
-                        "PRIMARY KEY (role_id, menu_id))");
-        seedMenus();
-        grantAllToAdmin();
-        log.info("[menu] 初始化完成:sys_menu / sys_role_menu");
-    }
-
     // ==================== 菜单树(管理端) ====================
     // ==================== 菜单树(管理端) ====================
 
 
     /** 全量菜单树(管理端,含停用/隐藏项)。 */
     /** 全量菜单树(管理端,含停用/隐藏项)。 */
@@ -136,7 +108,7 @@ public class MenuService {
 
 
     /**
     /**
      * 某用户可见的菜单树:取其所有角色被授予的菜单,向上补齐父级后组装成树,
      * 某用户可见的菜单树:取其所有角色被授予的菜单,向上补齐父级后组装成树,
-     * 再剔除“自身未被授予且无可见子项”的目录(空目录不下发)。
+     * 再剔除"自身未被授予且无可见子项"的目录(空目录不下发)。
      */
      */
     public List<Map<String, Object>> visibleTree(Long userId) {
     public List<Map<String, Object>> visibleTree(Long userId) {
         List<Map<String, Object>> grantedRows = dataSource.queryForList(
         List<Map<String, Object>> grantedRows = dataSource.queryForList(
@@ -169,32 +141,6 @@ public class MenuService {
 
 
     // ==================== 内部 ====================
     // ==================== 内部 ====================
 
 
-    private void seedMenus() {
-        if (countMenus() > 0) {
-            return;
-        }
-        long dashboard = insertSeed("工作台", "menu", null, "/dashboard", "dashboard/index", "HomeFilled", 1);
-        long system = insertSeed("系统管理", "dir", null, null, null, "Setting", 1);
-        insertSeed("用户管理", "menu", system, "/system/user", "system/user/index", "User", 1);
-        insertSeed("角色管理", "menu", system, "/system/role", "system/role/index", "Avatar", 2);
-        insertSeed("权限管理", "menu", system, "/system/permission", "system/permission/index", "Key", 3);
-        insertSeed("菜单管理", "menu", system, "/system/menu", "system/menu/index", "Menu", 4);
-        insertSeed("数据源演示", "menu", null, "/datasource", "datasource/index", "Coin", 2);
-        log.info("[menu] 已灌入种子菜单(工作台/系统管理 等 {} 个)", countMenus());
-    }
-
-    private void grantAllToAdmin() {
-        Long adminRoleId = roleIdByCode("ADMIN");
-        if (adminRoleId == null) {
-            return;
-        }
-        List<Map<String, Object>> menus = dataSource.queryForList("SELECT id FROM sys_menu");
-        for (Map<String, Object> menu : menus) {
-            dataSource.update("INSERT OR IGNORE INTO sys_role_menu(role_id, menu_id) VALUES(?, ?)",
-                    adminRoleId, longValue(menu.get("id")));
-        }
-    }
-
     private long insertMenu(MenuRequest req) {
     private long insertMenu(MenuRequest req) {
         dataSource.update(
         dataSource.update(
                 "INSERT INTO sys_menu(parent_id, name, type, route, component, icon, sort, visible, status) " +
                 "INSERT INTO sys_menu(parent_id, name, type, route, component, icon, sort, visible, status) " +
@@ -211,20 +157,6 @@ public class MenuService {
         return ((Number) dataSource.queryForObject("SELECT last_insert_rowid()")).longValue();
         return ((Number) dataSource.queryForObject("SELECT last_insert_rowid()")).longValue();
     }
     }
 
 
-    private long insertSeed(String name, String type, Long parentId, String route,
-                            String component, String icon, int sort) {
-        dataSource.update(
-                "INSERT INTO sys_menu(parent_id, name, type, route, component, icon, sort, visible, status) " +
-                        "VALUES(?, ?, ?, ?, ?, ?, ?, 1, 1)",
-                parentId, name, type, route, component, icon, sort);
-        return ((Number) dataSource.queryForObject("SELECT last_insert_rowid()")).longValue();
-    }
-
-    private int countMenus() {
-        Object v = dataSource.queryForObject("SELECT COUNT(*) FROM sys_menu");
-        return v == null ? 0 : ((Number) v).intValue();
-    }
-
     private Long roleIdByCode(String code) {
     private Long roleIdByCode(String code) {
         Map<String, Object> row = dataSource.queryForMap(
         Map<String, Object> row = dataSource.queryForMap(
                 "SELECT id FROM sys_role WHERE code = ?", code);
                 "SELECT id FROM sys_role WHERE code = ?", code);
@@ -251,7 +183,7 @@ public class MenuService {
         return roots;
         return roots;
     }
     }
 
 
-    /** 向上补齐全部祖先,保证“子菜单可见时其父级目录也出现在树中”。 */
+    /** 向上补齐全部祖先,保证"子菜单可见时其父级目录也出现在树中"。 */
     private Set<Long> collectWithAncestors(Set<Long> grants, List<Map<String, Object>> all) {
     private Set<Long> collectWithAncestors(Set<Long> grants, List<Map<String, Object>> all) {
         Map<Long, Map<String, Object>> byId = new LinkedHashMap<>();
         Map<Long, Map<String, Object>> byId = new LinkedHashMap<>();
         for (Map<String, Object> row : all) {
         for (Map<String, Object> row : all) {
@@ -270,7 +202,7 @@ public class MenuService {
         return result;
         return result;
     }
     }
 
 
-    /** 剔除“自身未直接被授予且无可见子项”的目录。 */
+    /** 剔除"自身未直接被授予且无可见子项"的目录。 */
     private List<Map<String, Object>> pruneEmptyDirs(List<Map<String, Object>> nodes, Set<Long> grantSet) {
     private List<Map<String, Object>> pruneEmptyDirs(List<Map<String, Object>> nodes, Set<Long> grantSet) {
         List<Map<String, Object>> kept = new ArrayList<>();
         List<Map<String, Object>> kept = new ArrayList<>();
         for (Map<String, Object> node : nodes) {
         for (Map<String, Object> node : nodes) {

+ 2 - 25
src/main/java/com/nylc/touyan/rbac/RbacAutoConfiguration.java

@@ -1,43 +1,20 @@
 package com.nylc.touyan.rbac;
 package com.nylc.touyan.rbac;
 
 
-import com.nylc.touyan.auth.UserService;
-import com.nylc.touyan.menu.MenuService;
-import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.context.annotation.Configuration;
 import org.springframework.context.annotation.Configuration;
 import org.springframework.core.annotation.Order;
 import org.springframework.core.annotation.Order;
 import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
 import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
 import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
 import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
 
 
-import javax.annotation.PostConstruct;
-
 /**
 /**
  * RBAC + 菜单权限装配入口:
  * RBAC + 菜单权限装配入口:
- * — 启动时初始化 RBAC 表 + 种子(先建 sys_user,再建角色/权限相关表并灌入管理员);
- * — 随后初始化菜单表 sys_menu / sys_role_menu 并授权给 ADMIN(顺序在 RBAC 之后,确保角色已存在);
  * — 注册 {@link RbacInterceptor}(@Order 保证在 AuthInterceptor 之后执行)。
  * — 注册 {@link RbacInterceptor}(@Order 保证在 AuthInterceptor 之后执行)。
- * 说明:菜单权限体系独立于 sys_permission(接口权限),此处仅保证建表顺序,不做逻辑耦合。
+ *
+ * <p>数据库表结构与种子数据由 {@code sql/init.sql} 维护,不再在此处触发建表。</p>
  */
  */
 @Order(1)
 @Order(1)
 @Configuration
 @Configuration
 public class RbacAutoConfiguration implements WebMvcConfigurer {
 public class RbacAutoConfiguration implements WebMvcConfigurer {
 
 
-    @Autowired
-    private UserService userService;
-
-    @Autowired
-    private RbacService rbacService;
-
-    @Autowired
-    private MenuService menuService;
-
-    @PostConstruct
-    public void init() {
-        // 先建 sys_user,再建 RBAC 表并灌入种子/管理员(幂等,可重复启动),最后初始化菜单
-        userService.initSchema();
-        rbacService.init();
-        menuService.init();
-    }
-
     @Override
     @Override
     public void addInterceptors(InterceptorRegistry registry) {
     public void addInterceptors(InterceptorRegistry registry) {
         registry.addInterceptor(new RbacInterceptor()).addPathPatterns("/api/**");
         registry.addInterceptor(new RbacInterceptor()).addPathPatterns("/api/**");

+ 2 - 111
src/main/java/com/nylc/touyan/rbac/RbacService.java

@@ -2,13 +2,9 @@ package com.nylc.touyan.rbac;
 
 
 import com.nylc.touyan.auth.AuthException;
 import com.nylc.touyan.auth.AuthException;
 import com.nylc.touyan.datasource.DataSourceClient;
 import com.nylc.touyan.datasource.DataSourceClient;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
 import org.springframework.stereotype.Service;
 import org.springframework.stereotype.Service;
 
 
 import java.util.ArrayList;
 import java.util.ArrayList;
-import java.util.Arrays;
 import java.util.LinkedHashMap;
 import java.util.LinkedHashMap;
 import java.util.List;
 import java.util.List;
 import java.util.Map;
 import java.util.Map;
@@ -16,73 +12,16 @@ import java.util.Map;
 /**
 /**
  * RBAC(基于角色的访问控制)服务:
  * RBAC(基于角色的访问控制)服务:
  * - 数据模型:sys_role / sys_permission / sys_user_role / sys_role_permission;
  * - 数据模型:sys_role / sys_permission / sys_user_role / sys_role_permission;
- * - 启动时建表并灌入种子(内置 ADMIN/USER 角色、初始权限集、默认管理员);
+ * - 表结构与种子数据由 {@code sql/init.sql} 初始化,本服务不再做建表/灌种子;
  * - 提供角色/权限管理与用户角色授权能力;登录后据此解析用户角色与权限。
  * - 提供角色/权限管理与用户角色授权能力;登录后据此解析用户角色与权限。
  */
  */
 @Service
 @Service
 public class RbacService {
 public class RbacService {
 
 
-    private static final Logger log = LoggerFactory.getLogger(RbacService.class);
-
     private final DataSourceClient dataSource;
     private final DataSourceClient dataSource;
-    private final BCryptPasswordEncoder encoder = new BCryptPasswordEncoder();
-    private final String adminUsername;
-    private final String adminPassword;
 
 
-    public RbacService(DataSourceClient dataSource,
-                       com.nylc.touyan.auth.AuthProperties properties) {
+    public RbacService(DataSourceClient dataSource) {
         this.dataSource = dataSource;
         this.dataSource = dataSource;
-        this.adminUsername = properties.getAdmin().getUsername();
-        this.adminPassword = properties.getAdmin().getPassword();
-    }
-
-    /** 初始化 RBAC 表并灌入种子数据(幂等)。 */
-    public void init() {
-        dataSource.execute(
-                "CREATE TABLE IF NOT EXISTS sys_role (" +
-                        "id INTEGER PRIMARY KEY AUTOINCREMENT," +
-                        "code TEXT NOT NULL UNIQUE," +
-                        "name TEXT NOT NULL," +
-                        "description TEXT," +
-                        "created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP)");
-        dataSource.execute(
-                "CREATE TABLE IF NOT EXISTS sys_permission (" +
-                        "id INTEGER PRIMARY KEY AUTOINCREMENT," +
-                        "code TEXT NOT NULL UNIQUE," +
-                        "name TEXT NOT NULL," +
-                        "description TEXT," +
-                        "created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP)");
-        dataSource.execute(
-                "CREATE TABLE IF NOT EXISTS sys_user_role (" +
-                        "user_id INTEGER NOT NULL," +
-                        "role_id INTEGER NOT NULL," +
-                        "PRIMARY KEY (user_id, role_id))");
-        dataSource.execute(
-                "CREATE TABLE IF NOT EXISTS sys_role_permission (" +
-                        "role_id INTEGER NOT NULL," +
-                        "permission_id INTEGER NOT NULL," +
-                        "PRIMARY KEY (role_id, permission_id))");
-
-        ensureRole("ADMIN", "超级管理员", "系统内置,拥有全部权限");
-        ensureRole("USER", "普通用户", "系统内置,默认用户角色");
-
-        List<String> seedPerms = Arrays.asList(
-                "user:list", "user:create", "user:update", "user:delete",
-                "role:list", "role:create", "role:update", "role:delete",
-                "role:assign", "permission:list", "permission:create", "permission:delete",
-                "data:query", "data:write");
-        for (String code : seedPerms) {
-            ensurePermission(code, code, null);
-        }
-
-        Long adminRoleId = roleIdByCode("ADMIN");
-        Long userRoleId = roleIdByCode("USER");
-        List<Long> allPermIds = allPermissionIds();
-        assignRolePermissions(adminRoleId, allPermIds);
-
-        ensureAdminUser();
-
-        log.info("[rbac] 初始化完成:角色 ADMIN/USER,管理员 [{}]", adminUsername);
     }
     }
 
 
     // ==================== 角色 ====================
     // ==================== 角色 ====================
@@ -229,45 +168,6 @@ public class RbacService {
 
 
     // ==================== 内部 ====================
     // ==================== 内部 ====================
 
 
-    private void ensureRole(String code, String name, String description) {
-        if (roleIdByCode(code) == null) {
-            dataSource.update("INSERT INTO sys_role(code, name, description) VALUES(?, ?, ?)",
-                    code, name, description);
-        }
-    }
-
-    private void ensurePermission(String code, String name, String description) {
-        if (permissionIdByCode(code) == null) {
-            dataSource.update("INSERT INTO sys_permission(code, name, description) VALUES(?, ?, ?)",
-                    code, name, description);
-        }
-    }
-
-    private void ensureAdminUser() {
-        Map<String, Object> exists = dataSource.queryForMap(
-                "SELECT id FROM sys_user WHERE username = ?", adminUsername);
-        Long adminId;
-        if (exists == null) {
-            dataSource.update("INSERT INTO sys_user(username, password, nickname, enabled) VALUES(?, ?, ?, 1)",
-                    adminUsername, encoder.encode(adminPassword), "管理员");
-            adminId = ((Number) dataSource.queryForObject(
-                    "SELECT id FROM sys_user WHERE username = ?", adminUsername)).longValue();
-        } else {
-            adminId = ((Number) exists.get("id")).longValue();
-        }
-        Long adminRoleId = roleIdByCode("ADMIN");
-        if (adminRoleId != null && userHasRole(adminId, adminRoleId)) {
-            return;
-        }
-        assignUserRoles(adminId, java.util.Collections.singletonList(adminRoleId));
-    }
-
-    private boolean userHasRole(Long userId, Long roleId) {
-        return dataSource.queryForMap(
-                "SELECT 1 FROM sys_user_role WHERE user_id = ? AND role_id = ?",
-                userId, roleId) != null;
-    }
-
     private Long roleIdByCode(String code) {
     private Long roleIdByCode(String code) {
         Map<String, Object> row = dataSource.queryForMap(
         Map<String, Object> row = dataSource.queryForMap(
                 "SELECT id FROM sys_role WHERE code = ?", code);
                 "SELECT id FROM sys_role WHERE code = ?", code);
@@ -280,15 +180,6 @@ public class RbacService {
         return row == null ? null : ((Number) row.get("id")).longValue();
         return row == null ? null : ((Number) row.get("id")).longValue();
     }
     }
 
 
-    private List<Long> allPermissionIds() {
-        List<Map<String, Object>> rows = dataSource.queryForList("SELECT id FROM sys_permission");
-        List<Long> ids = new ArrayList<>();
-        for (Map<String, Object> row : rows) {
-            ids.add(((Number) row.get("id")).longValue());
-        }
-        return ids;
-    }
-
     /** 当前用户权限(供登录时嵌入 token)。 */
     /** 当前用户权限(供登录时嵌入 token)。 */
     public AuthSnapshot resolve(Long userId) {
     public AuthSnapshot resolve(Long userId) {
         return new AuthSnapshot(roleCodesOf(userId), permissionCodesOf(userId));
         return new AuthSnapshot(roleCodesOf(userId), permissionCodesOf(userId));

+ 0 - 6
src/main/resources/application.yml

@@ -20,8 +20,6 @@ ir:
     sqlite:
     sqlite:
       # 数据库文件路径,相对工作目录(也可写绝对路径)
       # 数据库文件路径,相对工作目录(也可写绝对路径)
       path: data/ir.db
       path: data/ir.db
-      # 启动时自动创建演示表 demo_user(生产可置 false,由 Flyway/Liquibase 接管)
-      init-demo-table: true
     mysql:
     mysql:
       url: jdbc:mysql://localhost:3306/ir?useUnicode=true&characterEncoding=utf8&useSSL=false
       url: jdbc:mysql://localhost:3306/ir?useUnicode=true&characterEncoding=utf8&useSSL=false
       username: root
       username: root
@@ -36,10 +34,6 @@ ir:
       expiration-seconds: 7200
       expiration-seconds: 7200
       header: Authorization
       header: Authorization
       prefix: "Bearer "
       prefix: "Bearer "
-    # 内置管理员:首次启动自动创建并授予 ADMIN 角色(含全部权限)
-    admin:
-      username: admin
-      password: admin123
     # 无需登录即可访问的路径
     # 无需登录即可访问的路径
     skip-paths:
     skip-paths:
       - /api/auth/login
       - /api/auth/login