Files
g82tt 68be41e7a2 初始提交:浏览器首页 MyHomePage 全栈项目
# 项目概述
个人浏览器首页导航应用,支持书签分类管理、搜索引擎快捷搜索、
必应每日壁纸轮播、前后端分离部署,适配 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 模式)
2026-07-05 05:09:56 +08:00

246 lines
11 KiB
C#
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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 InitTablesSqlite 不支持 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("已写入分类 / 链接种子数据");
}
}
}