SystemDatabase.cs 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500
  1. using System;
  2. using System.Collections.Generic;
  3. using System.Data;
  4. using System.IO;
  5. using System.Security.Cryptography;
  6. using System.Text;
  7. using TeamAAS.Database.Interfaces;
  8. using TeamAAS.Database.Models;
  9. using TeamAAS.Database.Providers;
  10. namespace TeamAAS.Database.Services
  11. {
  12. /// <summary>
  13. /// 系统数据库(用户权限 + 操作审计)。
  14. /// 固定 SQLite 文件,首次运行自动建表并创建默认管理员。
  15. /// </summary>
  16. public class SystemDatabase : IDisposable
  17. {
  18. private readonly IDatabase _db;
  19. private readonly string _dbPath;
  20. /// <summary>
  21. /// 数据库实例
  22. /// </summary>
  23. public IDatabase Database => _db;
  24. /// <summary>
  25. /// 是否连接成功
  26. /// </summary>
  27. public bool IsConnected => _db?.IsConnected == true;
  28. /// <summary>
  29. /// 构造系统数据库
  30. /// </summary>
  31. /// <param name="configDir">配置目录(如 Config\),数据库文件放这里</param>
  32. public SystemDatabase(string configDir)
  33. {
  34. if (string.IsNullOrWhiteSpace(configDir))
  35. throw new ArgumentNullException(nameof(configDir));
  36. _dbPath = Path.Combine(configDir, "system.db");
  37. var config = new DatabaseConfig
  38. {
  39. Id = Guid.Parse("11111111-1111-1111-1111-111111111111"),
  40. Name = "SystemDB",
  41. ProviderType = "sqlite",
  42. Server = _dbPath
  43. };
  44. _db = new SqliteDatabase();
  45. _db.Configure(config);
  46. }
  47. /// <summary>
  48. /// 初始化:打开连接 + 自动建表 + 确保默认管理员存在
  49. /// </summary>
  50. public (bool Success, string Message) Initialize()
  51. {
  52. try
  53. {
  54. var dir = Path.GetDirectoryName(_dbPath);
  55. if (!string.IsNullOrWhiteSpace(dir) && !Directory.Exists(dir))
  56. Directory.CreateDirectory(dir);
  57. if (!_db.Open())
  58. return (false, "系统数据库打开失败");
  59. CreateTables();
  60. EnsureDefaultAdmin();
  61. return (true, "系统数据库初始化成功");
  62. }
  63. catch (Exception ex)
  64. {
  65. return (false, $"系统数据库初始化失败:{ex.Message}");
  66. }
  67. }
  68. #region 建表
  69. private void CreateTables()
  70. {
  71. var sqls = new[]
  72. {
  73. // 角色表
  74. @"CREATE TABLE IF NOT EXISTS sys_roles (
  75. id INTEGER PRIMARY KEY AUTOINCREMENT,
  76. role_name TEXT NOT NULL UNIQUE,
  77. display_name TEXT,
  78. description TEXT,
  79. is_enabled INTEGER DEFAULT 1,
  80. created_at TEXT DEFAULT (datetime('now','localtime')),
  81. updated_at TEXT DEFAULT (datetime('now','localtime'))
  82. )",
  83. // 权限表
  84. @"CREATE TABLE IF NOT EXISTS sys_permissions (
  85. id INTEGER PRIMARY KEY AUTOINCREMENT,
  86. permission_key TEXT NOT NULL UNIQUE,
  87. display_name TEXT,
  88. category TEXT,
  89. description TEXT,
  90. created_at TEXT DEFAULT (datetime('now','localtime'))
  91. )",
  92. // 用户表
  93. @"CREATE TABLE IF NOT EXISTS sys_users (
  94. id INTEGER PRIMARY KEY AUTOINCREMENT,
  95. username TEXT NOT NULL UNIQUE,
  96. password_hash TEXT NOT NULL,
  97. salt TEXT NOT NULL,
  98. display_name TEXT,
  99. email TEXT,
  100. is_enabled INTEGER DEFAULT 1,
  101. is_locked INTEGER DEFAULT 0,
  102. login_failed_count INTEGER DEFAULT 0,
  103. last_login_at TEXT,
  104. created_at TEXT DEFAULT (datetime('now','localtime')),
  105. updated_at TEXT DEFAULT (datetime('now','localtime'))
  106. )",
  107. // 用户-角色关联
  108. @"CREATE TABLE IF NOT EXISTS sys_user_roles (
  109. id INTEGER PRIMARY KEY AUTOINCREMENT,
  110. user_id INTEGER NOT NULL,
  111. role_id INTEGER NOT NULL,
  112. created_at TEXT DEFAULT (datetime('now','localtime')),
  113. UNIQUE(user_id, role_id)
  114. )",
  115. // 角色-权限关联
  116. @"CREATE TABLE IF NOT EXISTS sys_role_permissions (
  117. id INTEGER PRIMARY KEY AUTOINCREMENT,
  118. role_id INTEGER NOT NULL,
  119. permission_id INTEGER NOT NULL,
  120. created_at TEXT DEFAULT (datetime('now','localtime')),
  121. UNIQUE(role_id, permission_id)
  122. )",
  123. // 审计日志
  124. @"CREATE TABLE IF NOT EXISTS sys_audit_logs (
  125. id INTEGER PRIMARY KEY AUTOINCREMENT,
  126. user_id INTEGER,
  127. username TEXT,
  128. action_type TEXT NOT NULL,
  129. action_detail TEXT,
  130. module TEXT,
  131. ip_address TEXT,
  132. result INTEGER DEFAULT 1,
  133. error_message TEXT,
  134. created_at TEXT DEFAULT (datetime('now','localtime'))
  135. )",
  136. // 索引
  137. "CREATE INDEX IF NOT EXISTS idx_audit_user ON sys_audit_logs(username)",
  138. "CREATE INDEX IF NOT EXISTS idx_audit_action ON sys_audit_logs(action_type)",
  139. "CREATE INDEX IF NOT EXISTS idx_audit_time ON sys_audit_logs(created_at)",
  140. "CREATE INDEX IF NOT EXISTS idx_user_roles_uid ON sys_user_roles(user_id)",
  141. "CREATE INDEX IF NOT EXISTS idx_role_perm_rid ON sys_role_permissions(role_id)"
  142. };
  143. _db.ExecuteTransaction(sqls);
  144. }
  145. #endregion
  146. #region 默认数据
  147. /// <summary>
  148. /// 确保默认管理员账号存在(admin / admin123)
  149. /// </summary>
  150. private void EnsureDefaultAdmin()
  151. {
  152. var count = Convert.ToInt32(_db.ExecuteScalar(
  153. "SELECT COUNT(*) FROM sys_users WHERE username = 'admin'"));
  154. if (count > 0) return;
  155. // 默认角色
  156. var roleId = Convert.ToInt32(_db.ExecuteScalar(
  157. "SELECT id FROM sys_roles WHERE role_name = 'admin'"));
  158. if (roleId == 0)
  159. {
  160. _db.ExecuteNonQuery(
  161. "INSERT INTO sys_roles (role_name, display_name, description) VALUES ('admin', '系统管理员', '拥有全部权限')");
  162. roleId = Convert.ToInt32(_db.ExecuteScalar("SELECT last_insert_rowid()"));
  163. }
  164. // 默认管理员
  165. var salt = GenerateSalt();
  166. var hash = HashPassword("admin123", salt);
  167. var userParams = new Dictionary<string, object>
  168. {
  169. {"@username", "admin"},
  170. {"@password_hash", hash},
  171. {"@salt", salt},
  172. {"@display_name", "系统管理员"}
  173. };
  174. _db.ExecuteNonQuery(
  175. "INSERT INTO sys_users (username, password_hash, salt, display_name) VALUES (@username, @password_hash, @salt, @display_name)",
  176. userParams);
  177. var userId = Convert.ToInt32(_db.ExecuteScalar("SELECT last_insert_rowid()"));
  178. // 关联角色
  179. var relateParams = new Dictionary<string, object>
  180. {
  181. {"@user_id", userId},
  182. {"@role_id", roleId}
  183. };
  184. _db.ExecuteNonQuery(
  185. "INSERT OR IGNORE INTO sys_user_roles (user_id, role_id) VALUES (@user_id, @role_id)",
  186. relateParams);
  187. // 默认权限(种子数据)
  188. SeedPermissions();
  189. }
  190. private void SeedPermissions()
  191. {
  192. var perms = new (string Key, string Name, string Category)[]
  193. {
  194. ("system.user.view", "查看用户", "用户管理"),
  195. ("system.user.manage", "管理用户", "用户管理"),
  196. ("system.role.view", "查看角色", "角色权限"),
  197. ("system.role.manage", "管理角色", "角色权限"),
  198. ("system.audit.view", "查看审计日志", "系统审计"),
  199. ("product.view", "查看产品", "产品管理"),
  200. ("product.manage", "管理产品", "产品管理"),
  201. ("recipe.view", "查看配方", "配方管理"),
  202. ("recipe.manage", "管理配方", "配方管理"),
  203. ("device.control", "设备控制", "设备操作"),
  204. ("flow.edit", "编辑流程", "流程编辑"),
  205. ("flow.run", "运行流程", "流程运行"),
  206. ("system.config", "系统配置", "系统设置")
  207. };
  208. // 管理员角色ID
  209. var roleId = Convert.ToInt32(_db.ExecuteScalar(
  210. "SELECT id FROM sys_roles WHERE role_name = 'admin'"));
  211. foreach (var p in perms)
  212. {
  213. var pms = new Dictionary<string, object>
  214. {
  215. {"@k", p.Key}, {"@n", p.Name}, {"@c", p.Category}
  216. };
  217. _db.ExecuteNonQuery(
  218. "INSERT OR IGNORE INTO sys_permissions (permission_key, display_name, category) VALUES (@k, @n, @c)",
  219. pms);
  220. var permId = Convert.ToInt32(_db.ExecuteScalar(
  221. "SELECT id FROM sys_permissions WHERE permission_key = @k",
  222. new Dictionary<string, object> {{"@k", p.Key}}));
  223. var rpm = new Dictionary<string, object>
  224. {
  225. {"@rid", roleId}, {"@pid", permId}
  226. };
  227. _db.ExecuteNonQuery(
  228. "INSERT OR IGNORE INTO sys_role_permissions (role_id, permission_id) VALUES (@rid, @pid)",
  229. rpm);
  230. }
  231. }
  232. #endregion
  233. #region 用户认证
  234. /// <summary>
  235. /// 验证用户名密码
  236. /// </summary>
  237. public (bool Success, int UserId, string Message) ValidateUser(string username, string password)
  238. {
  239. if (string.IsNullOrWhiteSpace(username) || string.IsNullOrWhiteSpace(password))
  240. return (false, 0, "用户名或密码不能为空");
  241. var dt = _db.ExecuteQuery(
  242. "SELECT id, password_hash, salt, is_enabled, is_locked FROM sys_users WHERE username = @u",
  243. new Dictionary<string, object> {{"@u", username}});
  244. if (dt.Rows.Count == 0)
  245. return (false, 0, "用户不存在");
  246. var row = dt.Rows[0];
  247. var userId = Convert.ToInt32(row["id"]);
  248. var hash = row["password_hash"].ToString();
  249. var salt = row["salt"].ToString();
  250. var enabled = Convert.ToInt32(row["is_enabled"]) == 1;
  251. var locked = Convert.ToInt32(row["is_locked"]) == 1;
  252. if (!enabled) return (false, userId, "账号已禁用");
  253. if (locked) return (false, userId, "账号已锁定");
  254. var inputHash = HashPassword(password, salt);
  255. if (inputHash != hash)
  256. {
  257. // 失败次数+1
  258. _db.ExecuteNonQuery(
  259. "UPDATE sys_users SET login_failed_count = login_failed_count + 1 WHERE id = @id",
  260. new Dictionary<string, object> {{"@id", userId}});
  261. return (false, userId, "密码错误");
  262. }
  263. // 登录成功,清失败计数,写登录时间
  264. var up = new Dictionary<string, object> {{"@id", userId}};
  265. _db.ExecuteNonQuery(
  266. "UPDATE sys_users SET login_failed_count = 0, last_login_at = datetime('now','localtime') WHERE id = @id",
  267. up);
  268. return (true, userId, "登录成功");
  269. }
  270. /// <summary>
  271. /// 获取用户的所有权限 key
  272. /// </summary>
  273. public List<string> GetUserPermissions(int userId)
  274. {
  275. var result = new List<string>();
  276. var sql = @"
  277. SELECT DISTINCT p.permission_key
  278. FROM sys_permissions p
  279. INNER JOIN sys_role_permissions rp ON p.id = rp.permission_id
  280. INNER JOIN sys_user_roles ur ON rp.role_id = ur.role_id
  281. WHERE ur.user_id = @uid";
  282. var dt = _db.ExecuteQuery(sql, new Dictionary<string, object> {{"@uid", userId}});
  283. foreach (DataRow row in dt.Rows)
  284. result.Add(row["permission_key"].ToString());
  285. return result;
  286. }
  287. /// <summary>
  288. /// 检查用户是否有指定权限
  289. /// </summary>
  290. public bool HasPermission(int userId, string permissionKey)
  291. {
  292. var sql = @"
  293. SELECT COUNT(*)
  294. FROM sys_permissions p
  295. INNER JOIN sys_role_permissions rp ON p.id = rp.permission_id
  296. INNER JOIN sys_user_roles ur ON rp.role_id = ur.role_id
  297. WHERE ur.user_id = @uid AND p.permission_key = @pk";
  298. var count = Convert.ToInt32(_db.ExecuteQuery(sql,
  299. new Dictionary<string, object> {{"@uid", userId}, {"@pk", permissionKey}}).Rows[0][0]);
  300. return count > 0;
  301. }
  302. #endregion
  303. #region 审计日志
  304. /// <summary>
  305. /// 写入审计日志
  306. /// </summary>
  307. public void WriteAuditLog(string username, string actionType, string actionDetail,
  308. string module = null, string ipAddress = null, bool success = true, string errorMessage = null)
  309. {
  310. try
  311. {
  312. var sql = @"INSERT INTO sys_audit_logs
  313. (username, action_type, action_detail, module, ip_address, result, error_message)
  314. VALUES (@u, @a, @d, @m, @ip, @r, @e)";
  315. var p = new Dictionary<string, object>
  316. {
  317. {"@u", (object)username ?? DBNull.Value},
  318. {"@a", actionType},
  319. {"@d", (object)actionDetail ?? DBNull.Value},
  320. {"@m", (object)module ?? DBNull.Value},
  321. {"@ip", (object)ipAddress ?? DBNull.Value},
  322. {"@r", success ? 1 : 0},
  323. {"@e", (object)errorMessage ?? DBNull.Value}
  324. };
  325. _db.ExecuteNonQuery(sql, p);
  326. }
  327. catch
  328. {
  329. // 审计日志写入失败不影响主流程
  330. }
  331. }
  332. /// <summary>
  333. /// 查询审计日志(分页)
  334. /// </summary>
  335. public (DataTable Rows, int TotalCount) QueryAuditLogs(
  336. string username = null, string actionType = null, string module = null,
  337. DateTime? startTime = null, DateTime? endTime = null,
  338. int pageIndex = 1, int pageSize = 50)
  339. {
  340. var conditions = new List<string>();
  341. var parameters = new Dictionary<string, object>();
  342. if (!string.IsNullOrWhiteSpace(username))
  343. {
  344. conditions.Add("username LIKE @u");
  345. parameters["@u"] = $"%{username}%";
  346. }
  347. if (!string.IsNullOrWhiteSpace(actionType))
  348. {
  349. conditions.Add("action_type = @a");
  350. parameters["@a"] = actionType;
  351. }
  352. if (!string.IsNullOrWhiteSpace(module))
  353. {
  354. conditions.Add("module = @m");
  355. parameters["@m"] = module;
  356. }
  357. if (startTime.HasValue)
  358. {
  359. conditions.Add("created_at >= @st");
  360. parameters["@st"] = startTime.Value.ToString("yyyy-MM-dd HH:mm:ss");
  361. }
  362. if (endTime.HasValue)
  363. {
  364. conditions.Add("created_at <= @et");
  365. parameters["@et"] = endTime.Value.ToString("yyyy-MM-dd HH:mm:ss");
  366. }
  367. var where = conditions.Count > 0 ? "WHERE " + string.Join(" AND ", conditions) : "";
  368. var totalCount = Convert.ToInt32(
  369. _db.ExecuteScalar($"SELECT COUNT(*) FROM sys_audit_logs {where}", parameters));
  370. var offset = (pageIndex - 1) * pageSize;
  371. var dataSql = $@"
  372. SELECT * FROM sys_audit_logs {where}
  373. ORDER BY id DESC
  374. LIMIT @limit OFFSET @offset";
  375. parameters["@limit"] = pageSize;
  376. parameters["@offset"] = offset;
  377. var dt = _db.ExecuteQuery(dataSql, parameters);
  378. return (dt, totalCount);
  379. }
  380. #endregion
  381. #region 密码工具
  382. private static string GenerateSalt()
  383. {
  384. using (var rng = new RNGCryptoServiceProvider())
  385. {
  386. var bytes = new byte[16];
  387. rng.GetBytes(bytes);
  388. return Convert.ToBase64String(bytes);
  389. }
  390. }
  391. private static string HashPassword(string password, string salt)
  392. {
  393. using (var sha256 = SHA256.Create())
  394. {
  395. var combined = Encoding.UTF8.GetBytes(password + salt);
  396. var hash = sha256.ComputeHash(combined);
  397. var sb = new StringBuilder();
  398. foreach (var b in hash)
  399. sb.Append(b.ToString("x2"));
  400. return sb.ToString();
  401. }
  402. }
  403. #endregion
  404. #region IDisposable
  405. private bool _disposed;
  406. public void Dispose()
  407. {
  408. Dispose(true);
  409. GC.SuppressFinalize(this);
  410. }
  411. protected virtual void Dispose(bool disposing)
  412. {
  413. if (_disposed) return;
  414. if (disposing)
  415. {
  416. try { _db?.Dispose(); } catch { }
  417. }
  418. _disposed = true;
  419. }
  420. ~SystemDatabase()
  421. {
  422. Dispose(false);
  423. }
  424. #endregion
  425. }
  426. }