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

141 lines
5.9 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 Microsoft.Extensions.Options;
using MyHomePage.Api.Common;
using MyHomePage.Api.Infrastructure.Configuration;
using MyHomePage.Api.Infrastructure.Database;
using MyHomePage.Api.Services;
using SqlSugar;
var builder = WebApplication.CreateBuilder(args);
// ===== 配置节点绑定 =====
builder.Services.Configure<DatabaseOptions>(builder.Configuration.GetSection(DatabaseOptions.SectionName));
builder.Services.Configure<UploadOptions>(builder.Configuration.GetSection(UploadOptions.SectionName));
builder.Services.Configure<CorsOptions>(builder.Configuration.GetSection(CorsOptions.SectionName));
// ===== 控制器 =====
builder.Services.AddControllers()
.AddJsonOptions(o =>
{
// 前端约定使用 camelCase
o.JsonSerializerOptions.PropertyNamingPolicy = System.Text.Json.JsonNamingPolicy.CamelCase;
o.JsonSerializerOptions.DictionaryKeyPolicy = System.Text.Json.JsonNamingPolicy.CamelCase;
});
// ===== Swagger / OpenAPI =====
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen(c =>
{
c.SwaggerDoc("v1", new() { Title = "MyHomePage API", Version = "v1" });
// 注意:API 全部返回 ApiResponse<T>,类型签名会进 swagger
c.CustomOperationIds(apiDesc =>
{
// Controller Action 有 action 键,minimal APIMapGet / MapPost 等)没有
// 用 TryGetValue 防御,避免 KeyNotFoundException 启动崩
var routeValues = apiDesc.ActionDescriptor.RouteValues;
if (routeValues.TryGetValue("action", out var action)
&& !string.IsNullOrEmpty(action)
&& routeValues.TryGetValue("controller", out var controller)
&& !string.IsNullOrEmpty(controller))
{
return $"{controller}_{action}";
}
// minimal API 兜底:用路由模板做 operationId(去掉 / 和特殊字符)
// 例:/health → health/api/wallpaper/random → api_wallpaper_random
return apiDesc.RelativePath?
.Replace("/", "_", StringComparison.Ordinal)
.Trim('_')
?? "UnknownEndpoint";
});
});
// ===== CORS =====
var corsOrigins = builder.Configuration.GetSection("Cors:Origins").Get<string[]>() ?? Array.Empty<string>();
builder.Services.AddCors(o => o.AddDefaultPolicy(p =>
{
if (corsOrigins.Length > 0)
p.WithOrigins(corsOrigins).AllowAnyHeader().AllowAnyMethod().AllowCredentials();
else
p.AllowAnyOrigin().AllowAnyHeader().AllowAnyMethod();
}));
// ===== SqlSugar 单例 =====
builder.Services.AddSingleton<SqlSugarContext>();
// ===== 服务依赖注入 =====
builder.Services.AddScoped<SyncLogHelper>();
builder.Services.AddScoped<ISqlSugarClient>(sp => sp.GetRequiredService<SqlSugarContext>().Db);
builder.Services.AddScoped<ICategoryService, CategoryService>();
builder.Services.AddScoped<IBookmarkService, BookmarkService>();
builder.Services.AddScoped<ISearchEngineService, SearchEngineService>();
builder.Services.AddScoped<ISettingService, SettingService>();
builder.Services.AddScoped<IUploadService, UploadService>();
builder.Services.AddScoped<ISyncService, SyncService>();
builder.Services.AddScoped<DatabaseInitializer>();
// ===== P31favicon 自动抓取 =====
builder.Services.AddMemoryCache(); // IMemoryCache24h 缓存已抓 faviconSingleton
builder.Services.AddHttpClient(nameof(FaviconService), c => // 命名 HttpClientIHttpClientFactory 管理生命周期)
{
c.Timeout = TimeSpan.FromSeconds(5); // 全局 5s 超时(个别 GET 还会二次限制)
c.DefaultRequestHeaders.Add("User-Agent",
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.0.0 Safari/537.36");
});
builder.Services.AddScoped<FaviconService>(); // 注入 IHttpClientFactory + IMemoryCache
// ===== P34360 在线壁纸代理 =====
builder.Services.AddHttpClient(nameof(WallpaperService), c =>
{
c.Timeout = TimeSpan.FromSeconds(10); // 拉分类 / 200 张池子给 10s 充裕
c.DefaultRequestHeaders.Add("User-Agent",
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.0.0 Safari/537.36");
});
builder.Services.AddScoped<WallpaperService>();
// ===== 上传文件大小限制 =====
var maxUpload = builder.Configuration.GetValue<long?>("Upload:MaxSizeBytes") ?? 10L * 1024 * 1024;
builder.Services.Configure<Microsoft.AspNetCore.Http.Features.FormOptions>(o =>
{
o.MultipartBodyLengthLimit = maxUpload;
});
builder.WebHost.ConfigureKestrel(o => o.Limits.MaxRequestBodySize = maxUpload);
var app = builder.Build();
// ===== 启动时初始化数据库(CodeFirst + 种子) =====
using (var scope = app.Services.CreateScope())
{
var initializer = scope.ServiceProvider.GetRequiredService<DatabaseInitializer>();
await initializer.InitializeAsync();
}
// ===== HTTP Pipeline =====
app.UseMiddleware<ExceptionHandlingMiddleware>();
// 静态文件:暴露 Upload 目录
var uploadPath = Path.IsPathRooted(app.Configuration["Upload:Path"] ?? "Uploads")
? app.Configuration["Upload:Path"]!
: Path.Combine(app.Environment.ContentRootPath, app.Configuration["Upload:Path"] ?? "Uploads");
Directory.CreateDirectory(uploadPath);
app.UseStaticFiles(new Microsoft.AspNetCore.Builder.StaticFileOptions
{
FileProvider = new Microsoft.Extensions.FileProviders.PhysicalFileProvider(uploadPath),
RequestPath = app.Configuration["Upload:BaseUrl"] ?? "/uploads",
ServeUnknownFileTypes = true
});
if (app.Environment.IsDevelopment())
{
app.UseSwagger();
app.UseSwaggerUI();
}
app.UseCors();
app.UseAuthorization();
app.MapControllers();
// 根路径健康检查
app.MapGet("/", () => Results.Ok(new { name = "MyHomePage API", version = "1.0.0", status = "ok" }));
app.MapGet("/health", () => Results.Ok(new { status = "ok", time = DateTimeOffset.UtcNow }));
app.Run();