68be41e7a2
# 项目概述 个人浏览器首页导航应用,支持书签分类管理、搜索引擎快捷搜索、 必应每日壁纸轮播、前后端分离部署,适配 1Panel 服务器(Docker 模式)。 # 技术栈 - 前端:Vue 3 + TypeScript + Vite + Pinia + Capacitor(Android 打包) - 后端:.NET 8 + SqlSugar(多数据库) + SQLite/MySQL + Swashbuckle - 部署:1Panel 应用商店自定义应用(Docker Compose 模式) # 项目结构 - backend/ .NET 8 API 后端(8 个 Controller + 15 个 Service) - frontend/ Vue 3 前端(19 个组件 + 9 个 API 模块 + 5 个 Store) - docker/ Docker 部署文件(后端镜像 + Nginx 反代) - docs/ 部署手册(1Panel 实战版) - scripts/ E2E 测试脚本 # 已实现功能 - 书签管理:增删改查 + 树形分类 + 拖拽排序 + 主色自适应 - 搜索引擎:8 个内置引擎 + 自定义引擎 + favicon 自动抓取 - 必应壁纸:每日轮播 + 多分辨率自动选择 + 1.6MP 质量优先 - 全局设置:主题/行为/数据/工具 4 分类 + 跨设备同步 - 文件上传:图标/书签/通用(容器持久化 + 跨域 URL 拼接) - 同步:基于变更日志的设备间数据同步 - 跨域部署:前后端分离 + runtime config.json 无需重新编译 # 进度记录 - 已完成 P0~P52 共 53 个开发节点(详细见 说明文档.md) - 当前版本:v1.0 部署就绪 # 部署文档 - README.md:项目说明 + 快速开始 - 说明文档.md:完整开发进度(中文) - docs/DEPLOY.md:1Panel 部署手册(Docker 模式)
246 lines
11 KiB
C#
246 lines
11 KiB
C#
using MyHomePage.Api.Models.Entities;
|
||
using SqlSugar;
|
||
|
||
namespace MyHomePage.Api.Infrastructure.Database;
|
||
|
||
/// <summary>
|
||
/// 数据库初始化器:CodeFirst 建表 + 种子数据。
|
||
/// 应用启动时调用一次。
|
||
/// </summary>
|
||
public class DatabaseInitializer
|
||
{
|
||
private readonly SqlSugarContext _ctx;
|
||
private readonly ILogger<DatabaseInitializer> _logger;
|
||
|
||
public DatabaseInitializer(SqlSugarContext ctx, ILogger<DatabaseInitializer> logger)
|
||
{
|
||
_ctx = ctx;
|
||
_logger = logger;
|
||
}
|
||
|
||
/// <summary>建表 + 种子数据</summary>
|
||
public async Task InitializeAsync()
|
||
{
|
||
try
|
||
{
|
||
_logger.LogInformation("开始 CodeFirst 建表...");
|
||
|
||
// ===== 兼容老库:表已存在则跳过 CodeFirst InitTables(Sqlite 不支持 alter column primary key,触发表结构不一致会抛)=====
|
||
// 后续靠 MigrateSettingColumns / MigrateBookmarkColumns 给老库补列。
|
||
const string settingsTable = "settings";
|
||
if (_ctx.Db.DbMaintenance.IsAnyTable(settingsTable))
|
||
{
|
||
_logger.LogInformation("检测到 settings 表已存在,跳过 CodeFirst(已通过轻量迁移补齐列)");
|
||
}
|
||
else
|
||
{
|
||
_ctx.Db.CodeFirst.InitTables(
|
||
typeof(Category),
|
||
typeof(Bookmark),
|
||
typeof(SearchEngine),
|
||
typeof(Setting),
|
||
typeof(SyncLog)
|
||
);
|
||
}
|
||
|
||
// 对已存在数据库做轻量迁移:给 settings 表补上新增列(CodeFirst InitTables 不会自动 ALTER 老库)
|
||
MigrateSettingColumns();
|
||
// 给 bookmarks 表补充 ColorBg 列(P28 链接 logo 背景色)
|
||
MigrateBookmarkColumns();
|
||
// 给 search_engines 表补充 IconType / IconUrl / ColorBg 列(P37 引擎图标逻辑对齐链接)
|
||
MigrateSearchEngineColumns();
|
||
// 给 settings 表补充 OpenSearchInNewTab 列(P46 搜索框行为开关)
|
||
MigrateSettingColumnsV2();
|
||
|
||
await SeedAsync();
|
||
_logger.LogInformation("数据库初始化完成");
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
_logger.LogError(ex, "数据库初始化失败");
|
||
throw;
|
||
}
|
||
}
|
||
|
||
/// <summary>为 settings 表补充新列(已存在则跳过)。</summary>
|
||
private void MigrateSettingColumns()
|
||
{
|
||
const string tableName = "settings";
|
||
// P26.2
|
||
if (!_ctx.Db.DbMaintenance.IsAnyColumn(tableName, "OpenLinksInNewTab"))
|
||
{
|
||
_ctx.Db.DbMaintenance.AddColumn(tableName, new DbColumnInfo
|
||
{
|
||
DbColumnName = "OpenLinksInNewTab",
|
||
DataType = "INTEGER",
|
||
IsNullable = false,
|
||
DefaultValue = "1"
|
||
});
|
||
_logger.LogInformation("已为 settings 表补充列 {Column}", "OpenLinksInNewTab");
|
||
}
|
||
// P34 360 壁纸模式
|
||
if (!_ctx.Db.DbMaintenance.IsAnyColumn(tableName, "WallpaperEnabled"))
|
||
{
|
||
_ctx.Db.DbMaintenance.AddColumn(tableName, new DbColumnInfo
|
||
{
|
||
DbColumnName = "WallpaperEnabled",
|
||
DataType = "INTEGER",
|
||
IsNullable = false,
|
||
DefaultValue = "0"
|
||
});
|
||
_logger.LogInformation("已为 settings 表补充列 {Column}", "WallpaperEnabled");
|
||
}
|
||
if (!_ctx.Db.DbMaintenance.IsAnyColumn(tableName, "WallpaperCategoryId"))
|
||
{
|
||
_ctx.Db.DbMaintenance.AddColumn(tableName, new DbColumnInfo
|
||
{
|
||
DbColumnName = "WallpaperCategoryId",
|
||
DataType = "varchar(32)",
|
||
IsNullable = true,
|
||
DefaultValue = ""
|
||
});
|
||
_logger.LogInformation("已为 settings 表补充列 {Column}", "WallpaperCategoryId");
|
||
}
|
||
if (!_ctx.Db.DbMaintenance.IsAnyColumn(tableName, "WallpaperInterval"))
|
||
{
|
||
_ctx.Db.DbMaintenance.AddColumn(tableName, new DbColumnInfo
|
||
{
|
||
DbColumnName = "WallpaperInterval",
|
||
DataType = "INTEGER",
|
||
IsNullable = false,
|
||
DefaultValue = "30"
|
||
});
|
||
_logger.LogInformation("已为 settings 表补充列 {Column}", "WallpaperInterval");
|
||
}
|
||
}
|
||
|
||
/// <summary>为 bookmarks 表补充 ColorBg 列(已存在则跳过)。</summary>
|
||
private void MigrateBookmarkColumns()
|
||
{
|
||
const string tableName = "bookmarks";
|
||
const string newColumn = "ColorBg";
|
||
if (!_ctx.Db.DbMaintenance.IsAnyColumn(tableName, newColumn))
|
||
{
|
||
_ctx.Db.DbMaintenance.AddColumn(tableName, new DbColumnInfo
|
||
{
|
||
DbColumnName = newColumn,
|
||
DataType = "varchar(32)",
|
||
IsNullable = true
|
||
});
|
||
_logger.LogInformation("已为 bookmarks 表补充列 {Column}", newColumn);
|
||
}
|
||
}
|
||
|
||
/// <summary>为 search_engines 表补充 IconType / IconUrl / ColorBg 列(已存在则跳过,P37 引擎图标逻辑对齐链接)。</summary>
|
||
private void MigrateSearchEngineColumns()
|
||
{
|
||
const string tableName = "search_engines";
|
||
AddColumnIfMissing(tableName, "IconType", "varchar(16)", isNullable: false, defaultValue: "lucide");
|
||
AddColumnIfMissing(tableName, "IconUrl", "varchar(512)", isNullable: true);
|
||
AddColumnIfMissing(tableName, "ColorBg", "varchar(32)", isNullable: true);
|
||
}
|
||
|
||
/// <summary>P46:给 settings 表补 OpenSearchInNewTab 列(int default 1)—— 复刻 P37/P42 的「轻量迁移」模式</summary>
|
||
private void MigrateSettingColumnsV2()
|
||
{
|
||
const string tableName = "settings";
|
||
// 注意:int 类型的 column 在 SqlSugar + SQLite 下要显式声明 DataType
|
||
if (!_ctx.Db.DbMaintenance.IsAnyColumn(tableName, "OpenSearchInNewTab"))
|
||
{
|
||
_ctx.Db.DbMaintenance.AddColumn(tableName, new DbColumnInfo
|
||
{
|
||
DbColumnName = "OpenSearchInNewTab",
|
||
DataType = "int",
|
||
IsNullable = false,
|
||
DefaultValue = "1"
|
||
});
|
||
}
|
||
}
|
||
|
||
private void AddColumnIfMissing(string tableName, string columnName, string dataType, bool isNullable, string? defaultValue = null)
|
||
{
|
||
if (_ctx.Db.DbMaintenance.IsAnyColumn(tableName, columnName)) return;
|
||
_ctx.Db.DbMaintenance.AddColumn(tableName, new DbColumnInfo
|
||
{
|
||
DbColumnName = columnName,
|
||
DataType = dataType,
|
||
IsNullable = isNullable,
|
||
DefaultValue = defaultValue
|
||
});
|
||
_logger.LogInformation("已为 {Table} 表补充列 {Column}", tableName, columnName);
|
||
}
|
||
|
||
/// <summary>写入种子数据(仅当表为空时执行)</summary>
|
||
private async Task SeedAsync()
|
||
{
|
||
var db = _ctx.Db;
|
||
|
||
// 搜索引擎种子
|
||
if (!db.Queryable<SearchEngine>().Any())
|
||
{
|
||
var engines = new List<SearchEngine>
|
||
{
|
||
new() { Name = "百度", UrlTemplate = "https://www.baidu.com/s?wd={q}", Icon = "search", Sort = 0, IsDefault = true, CreatedAt = DateTime.UtcNow, UpdatedAt = DateTime.UtcNow },
|
||
new() { Name = "Google", UrlTemplate = "https://www.google.com/search?q={q}", Icon = "search", Sort = 1, IsDefault = false, CreatedAt = DateTime.UtcNow, UpdatedAt = DateTime.UtcNow },
|
||
new() { Name = "Bing", UrlTemplate = "https://www.bing.com/search?q={q}", Icon = "search", Sort = 2, IsDefault = false, CreatedAt = DateTime.UtcNow, UpdatedAt = DateTime.UtcNow }
|
||
};
|
||
await db.Insertable(engines).ExecuteCommandAsync();
|
||
_logger.LogInformation("已写入搜索引擎种子数据 ({Count} 条)", engines.Count);
|
||
}
|
||
|
||
// 设置种子(单行)
|
||
if (!db.Queryable<Setting>().Any())
|
||
{
|
||
var setting = new Setting
|
||
{
|
||
Id = 1,
|
||
ThemeMode = "dark",
|
||
AccentColor = "#6c5ce7",
|
||
BackgroundImage = "wp1",
|
||
BackgroundType = "preset",
|
||
OpenLinksInNewTab = 1,
|
||
UpdatedAt = DateTime.UtcNow
|
||
};
|
||
await db.Insertable(setting).ExecuteCommandAsync();
|
||
_logger.LogInformation("已写入默认设置");
|
||
}
|
||
|
||
// 分类 + 链接种子
|
||
if (!db.Queryable<Category>().Any())
|
||
{
|
||
var now = DateTime.UtcNow;
|
||
// 一级:常用工具
|
||
var catTools = new Category
|
||
{
|
||
ParentId = 0,
|
||
Name = "常用工具",
|
||
Icon = "wrench",
|
||
Sort = 0,
|
||
CreatedAt = now,
|
||
UpdatedAt = now
|
||
};
|
||
catTools.Id = await db.Insertable(catTools).ExecuteReturnIdentityAsync();
|
||
var catToolsId = catTools.Id;
|
||
|
||
// 二级分类(单独插入回填 Id,方便后续链接绑定)
|
||
var subAi = new Category { ParentId = catToolsId, Name = "AI 工具", Icon = "bot", Sort = 1, CreatedAt = now, UpdatedAt = now };
|
||
var subDev = new Category { ParentId = catToolsId, Name = "开发工具", Icon = "code-2", Sort = 2, CreatedAt = now, UpdatedAt = now };
|
||
subAi.Id = await db.Insertable(subAi).ExecuteReturnIdentityAsync();
|
||
subDev.Id = await db.Insertable(subDev).ExecuteReturnIdentityAsync();
|
||
|
||
// 链接示例
|
||
var bookmarks = new List<Bookmark>
|
||
{
|
||
new() { CategoryId = subAi.Id, Title = "ChatGPT", Url = "https://chat.openai.com", Description = "AI 对话助手,智能问答", Icon = "bot", IconType = "lucide", Sort = 0, CreatedAt = now, UpdatedAt = now },
|
||
new() { CategoryId = subAi.Id, Title = "Claude", Url = "https://claude.ai", Description = "Anthropic 推出的 AI 助手", Icon = "bot", IconType = "lucide", Sort = 1, CreatedAt = now, UpdatedAt = now },
|
||
new() { CategoryId = subDev.Id, Title = "GitHub", Url = "https://github.com", Description = "代码托管与协作平台", Icon = "github", IconType = "lucide", Sort = 0, CreatedAt = now, UpdatedAt = now },
|
||
new() { CategoryId = subDev.Id, Title = "MDN", Url = "https://developer.mozilla.org", Description = "Web 技术文档参考", Icon = "book", IconType = "lucide", Sort = 1, CreatedAt = now, UpdatedAt = now },
|
||
new() { CategoryId = subDev.Id, Title = "Stack Overflow", Url = "https://stackoverflow.com", Description = "开发者问答社区", Icon = "message-circle", IconType = "lucide", Sort = 2, CreatedAt = now, UpdatedAt = now },
|
||
new() { CategoryId = subDev.Id, Title = "VS Code", Url = "https://code.visualstudio.com", Description = "轻量级代码编辑器", Icon = "code-2", IconType = "lucide", Sort = 3, CreatedAt = now, UpdatedAt = now }
|
||
};
|
||
await db.Insertable(bookmarks).ExecuteCommandAsync();
|
||
_logger.LogInformation("已写入分类 / 链接种子数据");
|
||
}
|
||
}
|
||
}
|