diff --git a/server/HoneyBox/scripts/create_user_poster_cache.sql b/server/HoneyBox/scripts/create_user_poster_cache.sql new file mode 100644 index 00000000..5f73de1e --- /dev/null +++ b/server/HoneyBox/scripts/create_user_poster_cache.sql @@ -0,0 +1,35 @@ +-- 创建用户海报缓存表 +-- 用于存储用户生成的推广海报,避免重复生成 + +IF NOT EXISTS (SELECT * FROM sys.objects WHERE object_id = OBJECT_ID(N'[dbo].[user_poster_cache]') AND type in (N'U')) +BEGIN + CREATE TABLE [dbo].[user_poster_cache] ( + [id] INT IDENTITY(1,1) NOT NULL, + [user_id] INT NOT NULL, + [app_id] NVARCHAR(100) NULL, + [template_hash] NVARCHAR(64) NOT NULL, + [cos_url] NVARCHAR(500) NOT NULL, + [file_size] BIGINT NOT NULL DEFAULT 0, + [mime_type] NVARCHAR(50) NOT NULL DEFAULT 'image/png', + [status] INT NOT NULL DEFAULT 1, + [expires_at] DATETIME2 NOT NULL, + [platform] NVARCHAR(50) NOT NULL DEFAULT 'MP-WEIXIN', + [created_at] DATETIME2 NOT NULL DEFAULT GETDATE(), + [updated_at] DATETIME2 NOT NULL DEFAULT GETDATE(), + CONSTRAINT [pk_user_poster_cache] PRIMARY KEY CLUSTERED ([id] ASC) + ); + + -- 创建索引 + CREATE INDEX [ix_user_poster_cache_user_id] ON [dbo].[user_poster_cache] ([user_id]); + CREATE INDEX [ix_user_poster_cache_template_hash] ON [dbo].[user_poster_cache] ([template_hash]); + CREATE INDEX [ix_user_poster_cache_platform] ON [dbo].[user_poster_cache] ([platform]); + CREATE INDEX [ix_user_poster_cache_status] ON [dbo].[user_poster_cache] ([status]); + CREATE INDEX [ix_user_poster_cache_expires_at] ON [dbo].[user_poster_cache] ([expires_at]); + + PRINT '表 user_poster_cache 创建成功'; +END +ELSE +BEGIN + PRINT '表 user_poster_cache 已存在'; +END +GO diff --git a/server/HoneyBox/src/HoneyBox.Admin.Business/Models/Config/ConfigModels.cs b/server/HoneyBox/src/HoneyBox.Admin.Business/Models/Config/ConfigModels.cs index 52bcea77..078cb678 100644 --- a/server/HoneyBox/src/HoneyBox.Admin.Business/Models/Config/ConfigModels.cs +++ b/server/HoneyBox/src/HoneyBox.Admin.Business/Models/Config/ConfigModels.cs @@ -427,6 +427,36 @@ public class BaseSetting [JsonPropertyName("share_image")] public string? ShareImage { get; set; } + /// + /// 海报模板图片URL + /// + [JsonPropertyName("poster_template")] + public string? PosterTemplate { get; set; } + + /// + /// 海报二维码X坐标 + /// + [JsonPropertyName("poster_qr_x")] + public string? PosterQrX { get; set; } + + /// + /// 海报二维码Y坐标 + /// + [JsonPropertyName("poster_qr_y")] + public string? PosterQrY { get; set; } + + /// + /// 海报二维码大小 + /// + [JsonPropertyName("poster_qr_size")] + public string? PosterQrSize { get; set; } + + /// + /// 站点URL + /// + [JsonPropertyName("site_url")] + public string? SiteUrl { get; set; } + /// /// 抽奖券拉人上限 /// diff --git a/server/HoneyBox/src/HoneyBox.Admin/admin-web/src/api/business/config.ts b/server/HoneyBox/src/HoneyBox.Admin/admin-web/src/api/business/config.ts index 472321d8..02ed6ff0 100644 --- a/server/HoneyBox/src/HoneyBox.Admin/admin-web/src/api/business/config.ts +++ b/server/HoneyBox/src/HoneyBox.Admin/admin-web/src/api/business/config.ts @@ -73,6 +73,16 @@ export interface BaseSetting { share_title?: string /** 分享图片 */ share_image?: string + /** 海报模板图片URL */ + poster_template?: string + /** 海报二维码X坐标 */ + poster_qr_x?: string + /** 海报二维码Y坐标 */ + poster_qr_y?: string + /** 海报二维码大小 */ + poster_qr_size?: string + /** 站点URL */ + site_url?: string /** 抽奖券拉人上限 */ draw_people_num?: string /** 首页是否弹窗 0关闭 1开启 */ diff --git a/server/HoneyBox/src/HoneyBox.Admin/admin-web/src/views/business/config/base.vue b/server/HoneyBox/src/HoneyBox.Admin/admin-web/src/views/business/config/base.vue index ffafcc2c..4972de38 100644 --- a/server/HoneyBox/src/HoneyBox.Admin/admin-web/src/views/business/config/base.vue +++ b/server/HoneyBox/src/HoneyBox.Admin/admin-web/src/views/business/config/base.vue @@ -237,6 +237,70 @@ + + 海报配置 + + + + + + + + + + +
用于生成海报二维码的推广链接
+
+
+
+ + + + + +
二维码在海报上的X坐标(像素)
+
+
+ + + +
二维码在海报上的Y坐标(像素)
+
+
+ + + +
二维码宽高(像素)
+
+
+
+ 开关配置 @@ -303,6 +367,11 @@ interface FormDataType { erweima: string share_title: string share_image: string + poster_template: string + poster_qr_x: number + poster_qr_y: number + poster_qr_size: number + site_url: string draw_people_num: number is_shou_tan: number is_exchange: number @@ -325,6 +394,11 @@ const formData = reactive({ erweima: '', share_title: '', share_image: '', + poster_template: '', + poster_qr_x: 104, + poster_qr_y: 1180, + poster_qr_size: 200, + site_url: '', draw_people_num: 10, is_shou_tan: 0, is_exchange: 1 @@ -372,6 +446,11 @@ const loadData = async () => { erweima: data.erweima || '', share_title: data.share_title || '', share_image: data.share_image || '', + poster_template: data.poster_template || '', + poster_qr_x: Number(data.poster_qr_x) || 104, + poster_qr_y: Number(data.poster_qr_y) || 1180, + poster_qr_size: Number(data.poster_qr_size) || 200, + site_url: data.site_url || '', draw_people_num: Number(data.draw_people_num) || 10, is_shou_tan: Number(data.is_shou_tan) || 0, is_exchange: Number(data.is_exchange) || 1 @@ -416,6 +495,11 @@ const handleSave = async () => { erweima: formData.erweima, share_title: formData.share_title, share_image: formData.share_image, + poster_template: formData.poster_template, + poster_qr_x: String(formData.poster_qr_x), + poster_qr_y: String(formData.poster_qr_y), + poster_qr_size: String(formData.poster_qr_size), + site_url: formData.site_url, draw_people_num: String(formData.draw_people_num), is_shou_tan: String(formData.is_shou_tan), is_exchange: String(formData.is_exchange) diff --git a/server/HoneyBox/src/HoneyBox.Api/Controllers/InvitationController.cs b/server/HoneyBox/src/HoneyBox.Api/Controllers/InvitationController.cs index bb2982c7..4ccf2586 100644 --- a/server/HoneyBox/src/HoneyBox.Api/Controllers/InvitationController.cs +++ b/server/HoneyBox/src/HoneyBox.Api/Controllers/InvitationController.cs @@ -30,12 +30,12 @@ public class InvitationController : ControllerBase /// /// 获取推荐信息 - /// POST /api/invitation + /// GET /api/invitation /// Requirements: 9.1-9.2 /// [HttpGet("invitation")] [Authorize] - public async Task> GetInvitationInfo([FromBody] InvitationInfoRequest? request) + public async Task> GetInvitationInfo([FromQuery] int page = 1, [FromQuery] int pageSize = 10) { var userId = GetCurrentUserId(); if (userId == null) @@ -45,10 +45,16 @@ public class InvitationController : ControllerBase try { - var page = request?.Page ?? 1; if (page < 1) page = 1; - var result = await _invitationService.GetInvitationInfoAsync(userId.Value, page); + // 从请求头获取平台类型 + var platform = Request.Headers["client"].FirstOrDefault(); + if (string.IsNullOrEmpty(platform)) + { + platform = Request.Headers["platform"].FirstOrDefault() ?? "H5"; + } + + var result = await _invitationService.GetInvitationInfoAsync(userId.Value, page, platform); return ApiResponse.Success(result); } catch (Exception ex) diff --git a/server/HoneyBox/src/HoneyBox.Core/Interfaces/IInvitationService.cs b/server/HoneyBox/src/HoneyBox.Core/Interfaces/IInvitationService.cs index 107da9ff..e9b35a9e 100644 --- a/server/HoneyBox/src/HoneyBox.Core/Interfaces/IInvitationService.cs +++ b/server/HoneyBox/src/HoneyBox.Core/Interfaces/IInvitationService.cs @@ -12,8 +12,9 @@ public interface IInvitationService /// /// 用户ID /// 页码 + /// 平台类型 /// 推荐信息响应 - Task GetInvitationInfoAsync(int userId, int page); + Task GetInvitationInfoAsync(int userId, int page, string? platform = null); /// /// 绑定邀请码 diff --git a/server/HoneyBox/src/HoneyBox.Core/Interfaces/IPosterService.cs b/server/HoneyBox/src/HoneyBox.Core/Interfaces/IPosterService.cs new file mode 100644 index 00000000..88980635 --- /dev/null +++ b/server/HoneyBox/src/HoneyBox.Core/Interfaces/IPosterService.cs @@ -0,0 +1,36 @@ +namespace HoneyBox.Core.Interfaces; + +/// +/// 海报生成服务接口 +/// +public interface IPosterService +{ + /// + /// 获取或生成用户推广海报 + /// + /// 用户ID + /// 平台类型(MP-WEIXIN/H5/APP等) + /// 海报生成结果 + Task GetUserPosterAsync(int userId, string? platform = null); +} + +/// +/// 海报生成结果 +/// +public class PosterResult +{ + /// + /// 是否成功 + /// + public bool Success { get; set; } + + /// + /// 消息 + /// + public string Message { get; set; } = string.Empty; + + /// + /// 海报图片URL + /// + public string? ImageUrl { get; set; } +} diff --git a/server/HoneyBox/src/HoneyBox.Core/Services/InvitationService.cs b/server/HoneyBox/src/HoneyBox.Core/Services/InvitationService.cs index 0874830d..a8675253 100644 --- a/server/HoneyBox/src/HoneyBox.Core/Services/InvitationService.cs +++ b/server/HoneyBox/src/HoneyBox.Core/Services/InvitationService.cs @@ -13,17 +13,22 @@ namespace HoneyBox.Core.Services; public class InvitationService : IInvitationService { private readonly HoneyBoxDbContext _dbContext; + private readonly IPosterService _posterService; private readonly ILogger _logger; private const int PageSize = 15; - public InvitationService(HoneyBoxDbContext dbContext, ILogger logger) + public InvitationService( + HoneyBoxDbContext dbContext, + IPosterService posterService, + ILogger logger) { _dbContext = dbContext; + _posterService = posterService; _logger = logger; } /// - public async Task GetInvitationInfoAsync(int userId, int page) + public async Task GetInvitationInfoAsync(int userId, int page, string? platform = null) { // 获取被邀请用户列表(pid = userId 且 status = 1) var query = _dbContext.Users @@ -72,7 +77,20 @@ public class InvitationService : IInvitationService // 获取分享配置 var shareTitle = await GetShareTitleAsync(); - var shareImage = await GetShareImageAsync(); + + // 生成用户专属海报 + var shareImage = string.Empty; + var posterResult = await _posterService.GetUserPosterAsync(userId, platform); + if (posterResult.Success && !string.IsNullOrEmpty(posterResult.ImageUrl)) + { + shareImage = posterResult.ImageUrl; + } + else + { + // 如果海报生成失败,使用静态分享图片作为备选 + shareImage = await GetShareImageAsync(); + _logger.LogWarning("用户 {UserId} 海报生成失败: {Message},使用静态分享图片", userId, posterResult.Message); + } return new InvitationInfoResponse { diff --git a/server/HoneyBox/src/HoneyBox.Infrastructure/External/Poster/PosterService.cs b/server/HoneyBox/src/HoneyBox.Infrastructure/External/Poster/PosterService.cs new file mode 100644 index 00000000..8ea9350f --- /dev/null +++ b/server/HoneyBox/src/HoneyBox.Infrastructure/External/Poster/PosterService.cs @@ -0,0 +1,454 @@ +using System.Security.Cryptography; +using System.Text.Json; +using HoneyBox.Core.Interfaces; +using HoneyBox.Model.Data; +using HoneyBox.Model.Entities; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Logging; +using QRCoder; +using SkiaSharp; + +namespace HoneyBox.Infrastructure.External.Poster; + +/// +/// 海报生成服务实现 +/// +public class PosterService : IPosterService +{ + private readonly HoneyBoxDbContext _dbContext; + private readonly IImageUploadService _imageUploadService; + private readonly ILogger _logger; + private readonly HttpClient _httpClient; + + // 默认缓存100天 + private const int DefaultCacheExpireDays = 100; + // 二维码在海报上的位置(可通过配置调整) + private const int DefaultQrCodeX = 104; + private const int DefaultQrCodeY = 1180; + private const int DefaultQrCodeSize = 200; + + public PosterService( + HoneyBoxDbContext dbContext, + IImageUploadService imageUploadService, + IHttpClientFactory httpClientFactory, + ILogger logger) + { + _dbContext = dbContext; + _imageUploadService = imageUploadService; + _httpClient = httpClientFactory.CreateClient(); + _logger = logger; + } + + /// + public async Task GetUserPosterAsync(int userId, string? platform = null) + { + try + { + // 默认平台 + platform ??= "H5"; + + // 1. 验证用户ID + if (userId <= 0) + { + return new PosterResult { Success = false, Message = "无效的用户ID" }; + } + + // 2. 获取海报配置 + var posterConfig = await GetPosterConfigAsync(); + if (posterConfig == null || string.IsNullOrEmpty(posterConfig.TemplateUrl)) + { + return new PosterResult { Success = false, Message = "海报模板未配置" }; + } + + // 3. 计算模板哈希(用于缓存失效判断) + var templateHash = ComputeHash(posterConfig.TemplateUrl); + + // 4. 获取小程序配置(用于生成小程序码) + var appId = await GetDefaultAppIdAsync(); + + // 5. 查询缓存记录 + var cacheRecord = await FindValidCacheAsync(userId, templateHash, appId, platform); + if (cacheRecord != null) + { + _logger.LogInformation("用户 {UserId} 海报缓存命中", userId); + return new PosterResult + { + Success = true, + Message = "海报获取成功", + ImageUrl = cacheRecord.CosUrl + }; + } + + // 6. 生成推广链接 + var qrContent = await GeneratePromotionUrlAsync(userId, platform); + if (string.IsNullOrEmpty(qrContent)) + { + return new PosterResult { Success = false, Message = "生成推广链接失败" }; + } + + // 7. 下载海报模板 + var templateBytes = await DownloadImageAsync(posterConfig.TemplateUrl); + if (templateBytes == null) + { + return new PosterResult { Success = false, Message = "下载海报模板失败" }; + } + + // 8. 生成带二维码的海报 + var posterBytes = GeneratePosterWithQrCode( + templateBytes, + qrContent, + posterConfig.QrCodeX ?? DefaultQrCodeX, + posterConfig.QrCodeY ?? DefaultQrCodeY, + posterConfig.QrCodeSize ?? DefaultQrCodeSize); + + if (posterBytes == null) + { + return new PosterResult { Success = false, Message = "海报生成失败" }; + } + + // 9. 上传到COS + using var stream = new MemoryStream(posterBytes); + var cosUrl = await _imageUploadService.UploadStreamAsync( + stream, + $"poster_{userId}_{platform}.png", + "image/png", + "poster"); + + if (string.IsNullOrEmpty(cosUrl)) + { + return new PosterResult { Success = false, Message = "海报上传失败" }; + } + + // 10. 失效旧缓存 + await InvalidateOldCachesAsync(userId, templateHash, platform); + + // 11. 保存新缓存记录 + var newCache = new UserPosterCache + { + UserId = userId, + AppId = appId, + TemplateHash = templateHash, + CosUrl = cosUrl, + FileSize = posterBytes.Length, + MimeType = "image/png", + Status = 1, + ExpiresAt = DateTime.Now.AddDays(DefaultCacheExpireDays), + Platform = platform, + CreatedAt = DateTime.Now, + UpdatedAt = DateTime.Now + }; + _dbContext.Set().Add(newCache); + await _dbContext.SaveChangesAsync(); + + // 12. 随机清理过期缓存(5%概率) + if (Random.Shared.Next(100) < 5) + { + _ = CleanExpiredCachesAsync(); + } + + _logger.LogInformation("用户 {UserId} 海报生成成功: {Url}", userId, cosUrl); + return new PosterResult + { + Success = true, + Message = "海报生成成功", + ImageUrl = cosUrl + }; + } + catch (Exception ex) + { + _logger.LogError(ex, "获取用户海报异常: UserId={UserId}", userId); + return new PosterResult { Success = false, Message = "系统异常,请稍后重试" }; + } + } + + #region 私有方法 + + /// + /// 获取海报配置 + /// + private async Task GetPosterConfigAsync() + { + var config = await _dbContext.Configs + .FirstOrDefaultAsync(c => c.ConfigKey == "base"); + + if (config?.ConfigValue == null) + { + return null; + } + + try + { + var jsonDoc = JsonSerializer.Deserialize(config.ConfigValue); + return new PosterConfig + { + TemplateUrl = GetJsonString(jsonDoc, "poster_template"), + QrCodeX = GetJsonInt(jsonDoc, "poster_qr_x"), + QrCodeY = GetJsonInt(jsonDoc, "poster_qr_y"), + QrCodeSize = GetJsonInt(jsonDoc, "poster_qr_size"), + SiteUrl = GetJsonString(jsonDoc, "site_url") + }; + } + catch (Exception ex) + { + _logger.LogWarning(ex, "解析海报配置失败"); + return null; + } + } + + /// + /// 获取默认小程序AppId + /// + private async Task GetDefaultAppIdAsync() + { + var config = await _dbContext.Configs + .FirstOrDefaultAsync(c => c.ConfigKey == "miniprogram_setting"); + + if (config?.ConfigValue == null) + { + return string.Empty; + } + + try + { + var jsonDoc = JsonSerializer.Deserialize(config.ConfigValue); + if (jsonDoc.TryGetProperty("miniprograms", out var miniprograms) && + miniprograms.ValueKind == JsonValueKind.Array) + { + foreach (var mp in miniprograms.EnumerateArray()) + { + if (mp.TryGetProperty("is_default", out var isDefault) && + isDefault.GetInt32() == 1 && + mp.TryGetProperty("appid", out var appid)) + { + return appid.GetString() ?? string.Empty; + } + } + } + } + catch (Exception ex) + { + _logger.LogWarning(ex, "解析小程序配置失败"); + } + + return string.Empty; + } + + /// + /// 生成推广链接 + /// + private async Task GeneratePromotionUrlAsync(int userId, string platform) + { + // 获取站点URL配置 + var config = await _dbContext.Configs + .FirstOrDefaultAsync(c => c.ConfigKey == "base"); + + var siteUrl = "https://zfunbox.cn"; + if (config?.ConfigValue != null) + { + try + { + var jsonDoc = JsonSerializer.Deserialize(config.ConfigValue); + if (jsonDoc.TryGetProperty("site_url", out var siteUrlProp)) + { + var url = siteUrlProp.GetString(); + if (!string.IsNullOrEmpty(url)) + { + siteUrl = url; + } + } + } + catch { } + } + + // H5和APP使用URL链接 + return $"{siteUrl}?pid={userId}"; + } + + /// + /// 下载图片 + /// + private async Task DownloadImageAsync(string url) + { + try + { + var response = await _httpClient.GetAsync(url); + if (response.IsSuccessStatusCode) + { + return await response.Content.ReadAsByteArrayAsync(); + } + _logger.LogWarning("下载图片失败: {Url}, StatusCode: {StatusCode}", url, response.StatusCode); + return null; + } + catch (Exception ex) + { + _logger.LogError(ex, "下载图片异常: {Url}", url); + return null; + } + } + + /// + /// 生成带二维码的海报 + /// + private byte[]? GeneratePosterWithQrCode(byte[] templateBytes, string qrContent, int qrX, int qrY, int qrSize) + { + try + { + // 加载模板图片 + using var templateBitmap = SKBitmap.Decode(templateBytes); + if (templateBitmap == null) + { + _logger.LogWarning("无法解码海报模板图片"); + return null; + } + + // 生成二维码 + using var qrGenerator = new QRCodeGenerator(); + var qrCodeData = qrGenerator.CreateQrCode(qrContent, QRCodeGenerator.ECCLevel.M); + using var qrCode = new PngByteQRCode(qrCodeData); + var qrCodeBytes = qrCode.GetGraphic(20); + + // 解码二维码图片 + using var qrBitmap = SKBitmap.Decode(qrCodeBytes); + if (qrBitmap == null) + { + _logger.LogWarning("无法解码二维码图片"); + return null; + } + + // 缩放二维码到指定大小 + using var scaledQrBitmap = qrBitmap.Resize(new SKImageInfo(qrSize, qrSize), SKFilterQuality.High); + + // 创建画布 + using var surface = SKSurface.Create(new SKImageInfo(templateBitmap.Width, templateBitmap.Height)); + var canvas = surface.Canvas; + + // 绘制模板 + canvas.DrawBitmap(templateBitmap, 0, 0); + + // 绘制二维码 + canvas.DrawBitmap(scaledQrBitmap, qrX, qrY); + + // 导出为PNG + using var image = surface.Snapshot(); + using var data = image.Encode(SKEncodedImageFormat.Png, 90); + return data.ToArray(); + } + catch (Exception ex) + { + _logger.LogError(ex, "生成海报失败"); + return null; + } + } + + /// + /// 查找有效缓存 + /// + private async Task FindValidCacheAsync(int userId, string templateHash, string appId, string platform) + { + return await _dbContext.Set() + .Where(c => c.UserId == userId) + .Where(c => c.TemplateHash == templateHash) + .Where(c => c.Platform == platform) + .Where(c => c.Status == 1) + .Where(c => c.ExpiresAt > DateTime.Now) + .OrderByDescending(c => c.Id) + .FirstOrDefaultAsync(); + } + + /// + /// 失效旧缓存 + /// + private async Task InvalidateOldCachesAsync(int userId, string templateHash, string platform) + { + var oldCaches = await _dbContext.Set() + .Where(c => c.UserId == userId) + .Where(c => c.Platform == platform) + .Where(c => c.TemplateHash != templateHash || c.Status == 1) + .ToListAsync(); + + foreach (var cache in oldCaches) + { + cache.Status = 0; + cache.UpdatedAt = DateTime.Now; + } + + if (oldCaches.Count > 0) + { + await _dbContext.SaveChangesAsync(); + } + } + + /// + /// 清理过期缓存 + /// + private async Task CleanExpiredCachesAsync() + { + try + { + var expiredCaches = await _dbContext.Set() + .Where(c => c.ExpiresAt < DateTime.Now || c.Status == 0) + .Take(100) + .ToListAsync(); + + if (expiredCaches.Count > 0) + { + _dbContext.Set().RemoveRange(expiredCaches); + await _dbContext.SaveChangesAsync(); + _logger.LogInformation("清理了 {Count} 条过期海报缓存", expiredCaches.Count); + } + } + catch (Exception ex) + { + _logger.LogWarning(ex, "清理过期海报缓存失败"); + } + } + + /// + /// 计算字符串哈希 + /// + private static string ComputeHash(string input) + { + var bytes = System.Text.Encoding.UTF8.GetBytes(input); + var hash = MD5.HashData(bytes); + return Convert.ToHexString(hash).ToLowerInvariant(); + } + + private static string? GetJsonString(JsonElement element, string propertyName) + { + if (element.TryGetProperty(propertyName, out var prop) && prop.ValueKind == JsonValueKind.String) + { + return prop.GetString(); + } + return null; + } + + private static int? GetJsonInt(JsonElement element, string propertyName) + { + if (element.TryGetProperty(propertyName, out var prop)) + { + if (prop.ValueKind == JsonValueKind.Number) + { + return prop.GetInt32(); + } + if (prop.ValueKind == JsonValueKind.String && int.TryParse(prop.GetString(), out var val)) + { + return val; + } + } + return null; + } + + #endregion + + /// + /// 海报配置 + /// + private class PosterConfig + { + public string? TemplateUrl { get; set; } + public int? QrCodeX { get; set; } + public int? QrCodeY { get; set; } + public int? QrCodeSize { get; set; } + public string? SiteUrl { get; set; } + } +} diff --git a/server/HoneyBox/src/HoneyBox.Infrastructure/HoneyBox.Infrastructure.csproj b/server/HoneyBox/src/HoneyBox.Infrastructure/HoneyBox.Infrastructure.csproj index 8f756a29..bef544a6 100644 --- a/server/HoneyBox/src/HoneyBox.Infrastructure/HoneyBox.Infrastructure.csproj +++ b/server/HoneyBox/src/HoneyBox.Infrastructure/HoneyBox.Infrastructure.csproj @@ -15,6 +15,8 @@ + + diff --git a/server/HoneyBox/src/HoneyBox.Infrastructure/Modules/InfrastructureModule.cs b/server/HoneyBox/src/HoneyBox.Infrastructure/Modules/InfrastructureModule.cs index b08a8c77..b0d3a59c 100644 --- a/server/HoneyBox/src/HoneyBox.Infrastructure/Modules/InfrastructureModule.cs +++ b/server/HoneyBox/src/HoneyBox.Infrastructure/Modules/InfrastructureModule.cs @@ -1,6 +1,7 @@ using Autofac; using HoneyBox.Core.Interfaces; using HoneyBox.Infrastructure.Cache; +using HoneyBox.Infrastructure.External.Poster; using HoneyBox.Infrastructure.External.Storage; namespace HoneyBox.Infrastructure.Modules; @@ -27,6 +28,11 @@ public class InfrastructureModule : Module .As() .InstancePerLifetimeScope(); + // 注册海报生成服务 + builder.RegisterType() + .As() + .InstancePerLifetimeScope(); + // 后续可在此注册其他基础设施服务 // 如: 外部服务客户端、消息队列等 } diff --git a/server/HoneyBox/src/HoneyBox.Infrastructure/Modules/ServiceModule.cs b/server/HoneyBox/src/HoneyBox.Infrastructure/Modules/ServiceModule.cs index 22516bc0..83420546 100644 --- a/server/HoneyBox/src/HoneyBox.Infrastructure/Modules/ServiceModule.cs +++ b/server/HoneyBox/src/HoneyBox.Infrastructure/Modules/ServiceModule.cs @@ -101,8 +101,9 @@ public class ServiceModule : Module builder.Register(c => { var dbContext = c.Resolve(); + var posterService = c.Resolve(); var logger = c.Resolve>(); - return new InvitationService(dbContext, logger); + return new InvitationService(dbContext, posterService, logger); }).As().InstancePerLifetimeScope(); // 注册排行榜服务 diff --git a/server/HoneyBox/src/HoneyBox.Model/Data/HoneyBoxDbContext.cs b/server/HoneyBox/src/HoneyBox.Model/Data/HoneyBoxDbContext.cs index b4d34008..244b4dd7 100644 --- a/server/HoneyBox/src/HoneyBox.Model/Data/HoneyBoxDbContext.cs +++ b/server/HoneyBox/src/HoneyBox.Model/Data/HoneyBoxDbContext.cs @@ -126,6 +126,8 @@ public partial class HoneyBoxDbContext : DbContext public virtual DbSet PrizeAnnouncements { get; set; } + public virtual DbSet UserPosterCaches { get; set; } + protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) { // Connection string is configured in Program.cs via dependency injection diff --git a/server/HoneyBox/src/HoneyBox.Model/Entities/UserPosterCache.cs b/server/HoneyBox/src/HoneyBox.Model/Entities/UserPosterCache.cs new file mode 100644 index 00000000..2ea817d1 --- /dev/null +++ b/server/HoneyBox/src/HoneyBox.Model/Entities/UserPosterCache.cs @@ -0,0 +1,86 @@ +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; + +namespace HoneyBox.Model.Entities; + +/// +/// 用户海报缓存实体 +/// +[Table("user_poster_cache")] +public class UserPosterCache +{ + [Key] + [Column("id")] + public int Id { get; set; } + + /// + /// 用户ID + /// + [Column("user_id")] + public int UserId { get; set; } + + /// + /// 小程序AppId + /// + [Column("app_id")] + [StringLength(100)] + public string? AppId { get; set; } + + /// + /// 模板文件哈希值 + /// + [Column("template_hash")] + [StringLength(64)] + public string TemplateHash { get; set; } = string.Empty; + + /// + /// COS存储URL + /// + [Column("cos_url")] + [StringLength(500)] + public string CosUrl { get; set; } = string.Empty; + + /// + /// 文件大小(字节) + /// + [Column("file_size")] + public long FileSize { get; set; } + + /// + /// MIME类型 + /// + [Column("mime_type")] + [StringLength(50)] + public string MimeType { get; set; } = "image/png"; + + /// + /// 状态:1有效 0无效 + /// + [Column("status")] + public int Status { get; set; } = 1; + + /// + /// 过期时间 + /// + [Column("expires_at")] + public DateTime ExpiresAt { get; set; } + + /// + /// 平台类型:MP-WEIXIN/H5/APP等 + /// + [Column("platform")] + [StringLength(50)] + public string Platform { get; set; } = "MP-WEIXIN"; + + /// + /// 创建时间 + /// + [Column("created_at")] + public DateTime CreatedAt { get; set; } = DateTime.Now; + + /// + /// 更新时间 + /// + [Column("updated_at")] + public DateTime UpdatedAt { get; set; } = DateTime.Now; +}