From b55b56c9cb627ac3defb2c7e62c9ca5f245c8eee Mon Sep 17 00:00:00 2001 From: zpc Date: Sun, 29 Mar 2026 01:11:22 +0800 Subject: [PATCH] Add notification template configuration management --- README.md | 137 ++++++++++++++++++ admin/src/api/config.ts | 24 +++ admin/src/views/system/config.vue | 106 +++++++++++++- .../Controllers/AdminConfigController.cs | 20 +++ .../Interfaces/ISystemConfigService.cs | 10 ++ .../Services/NotificationService.cs | 28 +++- .../Services/SystemConfigService.cs | 65 +++++++++ 7 files changed, 384 insertions(+), 6 deletions(-) create mode 100644 README.md diff --git a/README.md b/README.md new file mode 100644 index 0000000..8c87f77 --- /dev/null +++ b/README.md @@ -0,0 +1,137 @@ +# 相宜相亲 + +一站式相亲交友平台,包含微信小程序、后台管理系统、官方网站三端,采用 .NET 8 + Vue 3 + uni-app 技术栈。 + +## 项目结构 + +``` +xiangyixiangqin/ +├── server/ # 后端服务(.NET 8) +│ ├── src/ +│ │ ├── XiangYi.Core/ # 核心层:实体、枚举、常量、接口 +│ │ ├── XiangYi.Application/ # 应用层:业务服务、DTO、校验、定时任务 +│ │ ├── XiangYi.Infrastructure/ # 基础设施层:数据库、缓存、微信、支付、存储、短信 +│ │ ├── XiangYi.AppApi/ # 小程序端 API + SignalR 聊天 +│ │ └── XiangYi.AdminApi/ # 后台管理端 API +│ └── tests/ # 单元测试 & 集成测试 +├── admin/ # 后台管理前端(Vue 3 + Element Plus) +├── miniapp/ # 微信小程序(uni-app) +├── website/ # 官方网站(静态页面) +├── deploy/ # 生产部署配置 +├── docs/ # 需求文档、协议文本 +└── scripts/ # 运维脚本 +``` + +## 技术栈 + +### 后端 + +| 类别 | 技术 | 说明 | +|------|------|------| +| 运行时 | .NET 8 | LTS 版本 | +| Web 框架 | ASP.NET Core WebAPI | RESTful API,双 API 独立部署 | +| ORM | FreeSql | CodeFirst,支持迁移 | +| 数据库 | SQL Server | 主数据库 | +| 缓存 | Redis | 验证码、Token、热点数据 | +| 认证 | JWT | 身份认证 | +| 实时通信 | SignalR | 聊天消息推送 | +| 后台任务 | Hangfire | 每日推荐刷新、通知推送等 | +| 日志 | Serilog | 结构化日志 | +| 对象映射 | Mapster | 轻量级映射 | +| 参数校验 | FluentValidation | 请求验证 | + +### 后台管理前端 + +| 类别 | 技术 | +|------|------| +| 框架 | Vue 3 + TypeScript | +| 构建 | Vite | +| UI | Element Plus | +| 状态管理 | Pinia | +| 图表 | ECharts | + +### 小程序端 + +| 类别 | 技术 | +|------|------| +| 框架 | uni-app (Vue 3) | +| UI | uView Plus | +| 状态管理 | Pinia | + +### 第三方服务 + +| 服务 | 方案 | +|------|------| +| 文件存储 | 腾讯云 COS(可切换阿里云 OSS) | +| 短信 | 阿里云 SMS(可切换腾讯云 SMS) | +| 实名认证 | 阿里云实人认证(可切换腾讯云) | +| 支付 | 微信支付 V3 | + +所有第三方云服务采用接口抽象 + 依赖注入模式,可通过配置切换服务商。 + +## 核心功能 + +- **用户体系** — 微信登录、手机绑定、实名认证、相亲编号 +- **相亲资料** — 资料提交/编辑、照片管理、择偶条件 +- **智能推荐** — 基于择偶条件的每日推荐匹配 +- **即时聊天** — 基于 SignalR 的实时消息、在线状态 +- **会员服务** — 会员等级、联系方式解锁、微信支付 +- **互动功能** — 浏览、收藏、点赞、送花、关注 +- **内容管理** — Banner 轮播、首页弹窗、系统通知 +- **后台管理** — 用户审核、数据统计、系统配置、操作日志 + +## 本地开发 + +### 后端 + +```bash +cd server +dotnet restore +dotnet run --project src/XiangYi.AppApi # 小程序 API,默认 http://localhost:5000 +dotnet run --project src/XiangYi.AdminApi # 管理端 API,默认 http://localhost:5001 +``` + +### 后台管理前端 + +```bash +cd admin +npm install +npm run dev # 默认 http://localhost:5173 +``` + +### 小程序 + +```bash +cd miniapp +npm install +npm run dev:mp-weixin # 生成到 unpackage/dist/dev/mp-weixin +``` + +用微信开发者工具打开 `unpackage/dist/dev/mp-weixin` 目录进行调试。 + +## 部署 + +项目使用 **Drone CI + Harbor + Docker Compose** 进行自动化部署。 + +推送代码到 `master` 分支后,Drone 自动执行: + +1. 并行构建 3 个 Docker 镜像(admin-api、app-api、admin-web) +2. 推送到内网 Harbor 镜像仓库 +3. SSH 到目标服务器执行 `docker compose pull && docker compose up -d` + +| 服务 | 默认端口 | +|------|---------| +| Admin API | 2801 | +| App API | 2802 | +| Admin Web | 2803 | + +详见 [CI-CD部署文档.md](CI-CD部署文档.md)。 + +## 相关文档 + +| 文档 | 说明 | +|------|------| +| [技术栈与开发规范](server/技术栈与开发规范.md) | 技术选型、项目结构、API 设计、安全规范 | +| [数据库设计](server/数据库设计.md) | 表结构、字段说明、索引设计 | +| [CI-CD部署文档](CI-CD部署文档.md) | Drone CI 流水线、Harbor 镜像管理、部署步骤 | +| [需求文档](docs/需求文档.md) | 产品需求说明 | diff --git a/admin/src/api/config.ts b/admin/src/api/config.ts index fcc3693..924ce2d 100644 --- a/admin/src/api/config.ts +++ b/admin/src/api/config.ts @@ -184,3 +184,27 @@ export function getRealNamePrice() { export function setRealNamePrice(price: number) { return request.post('/admin/config/realNamePrice', { price }) } + +/** + * 服务号通知模板配置 + */ +export interface NotificationTemplatesConfig { + unlockTemplateId?: string + favoriteTemplateId?: string + messageTemplateId?: string + dailyRecommendTemplateId?: string +} + +/** + * 获取服务号通知模板配置 + */ +export function getNotificationTemplates() { + return request.get('/admin/config/notificationTemplates') +} + +/** + * 设置服务号通知模板配置 + */ +export function setNotificationTemplates(templates: NotificationTemplatesConfig) { + return request.post('/admin/config/notificationTemplates', templates) +} diff --git a/admin/src/views/system/config.vue b/admin/src/views/system/config.vue index 4957d19..faf3f3d 100644 --- a/admin/src/views/system/config.vue +++ b/admin/src/views/system/config.vue @@ -301,6 +301,61 @@ /> + + + + + + + + +
有用户解锁我时,服务号发送相应通知
+
+ + + +
有用户收藏我时,服务号发送相应通知
+
+ + + +
首次沟通通知、5分钟未回复提醒共用此模板
+
+ + + +
每天早上8~10点随机时间发送推荐更新通知
+
+ + + + 保存模板配置 + + +
+
@@ -330,7 +385,9 @@ import { getMemberEntryImage, setMemberEntryImage, getRealNamePrice, - setRealNamePrice + setRealNamePrice, + getNotificationTemplates, + setNotificationTemplates } from '@/api/config' import { useUserStore } from '@/stores/user' @@ -359,9 +416,17 @@ const agreementForm = ref({ privacyPolicy: '' }) +const templateForm = ref({ + unlockTemplateId: '', + favoriteTemplateId: '', + messageTemplateId: '', + dailyRecommendTemplateId: '' +}) + const saving = ref(false) const savingAgreement = ref(false) const savingPolicy = ref(false) +const savingTemplates = ref(false) const uploadUrl = computed(() => `${apiBaseUrl}/admin/upload`) @@ -626,9 +691,41 @@ const savePrivacyPolicy = async () => { } } +const loadNotificationTemplates = async () => { + try { + const res = await getNotificationTemplates() + if (res) { + templateForm.value.unlockTemplateId = res.unlockTemplateId || '' + templateForm.value.favoriteTemplateId = res.favoriteTemplateId || '' + templateForm.value.messageTemplateId = res.messageTemplateId || '' + templateForm.value.dailyRecommendTemplateId = res.dailyRecommendTemplateId || '' + } + } catch (error) { + console.error('加载模板配置失败:', error) + } +} + +const saveNotificationTemplates = async () => { + savingTemplates.value = true + try { + await setNotificationTemplates({ + unlockTemplateId: templateForm.value.unlockTemplateId || undefined, + favoriteTemplateId: templateForm.value.favoriteTemplateId || undefined, + messageTemplateId: templateForm.value.messageTemplateId || undefined, + dailyRecommendTemplateId: templateForm.value.dailyRecommendTemplateId || undefined + }) + ElMessage.success('模板配置保存成功') + } catch (error) { + console.error('保存模板配置失败:', error) + } finally { + savingTemplates.value = false + } +} + onMounted(() => { loadConfig() loadAgreements() + loadNotificationTemplates() }) @@ -863,4 +960,11 @@ onMounted(() => { color: #606266; font-size: 14px; } + +.template-tip { + color: #909399; + font-size: 12px; + margin-top: 4px; + line-height: 1.6; +} diff --git a/server/src/XiangYi.AdminApi/Controllers/AdminConfigController.cs b/server/src/XiangYi.AdminApi/Controllers/AdminConfigController.cs index a65085e..5474b09 100644 --- a/server/src/XiangYi.AdminApi/Controllers/AdminConfigController.cs +++ b/server/src/XiangYi.AdminApi/Controllers/AdminConfigController.cs @@ -362,6 +362,26 @@ public class AdminConfigController : ControllerBase var result = await _configService.SetRealNamePriceAsync(request.Price); return result ? ApiResponse.Success("设置成功") : ApiResponse.Error(40001, "设置失败"); } + + /// + /// 获取服务号通知模板配置 + /// + [HttpGet("notificationTemplates")] + public async Task> GetNotificationTemplates() + { + var templates = await _configService.GetNotificationTemplatesAsync(); + return ApiResponse.Success(templates); + } + + /// + /// 设置服务号通知模板配置 + /// + [HttpPost("notificationTemplates")] + public async Task SetNotificationTemplates([FromBody] NotificationTemplatesDto request) + { + var result = await _configService.SetNotificationTemplatesAsync(request); + return result ? ApiResponse.Success("设置成功") : ApiResponse.Error(40001, "设置失败"); + } } /// diff --git a/server/src/XiangYi.Application/Interfaces/ISystemConfigService.cs b/server/src/XiangYi.Application/Interfaces/ISystemConfigService.cs index 39bf38b..c0195ba 100644 --- a/server/src/XiangYi.Application/Interfaces/ISystemConfigService.cs +++ b/server/src/XiangYi.Application/Interfaces/ISystemConfigService.cs @@ -141,4 +141,14 @@ public interface ISystemConfigService /// 设置实名认证费用 /// Task SetRealNamePriceAsync(decimal price); + + /// + /// 获取服务号通知模板配置 + /// + Task GetNotificationTemplatesAsync(); + + /// + /// 设置服务号通知模板配置 + /// + Task SetNotificationTemplatesAsync(NotificationTemplatesDto templates); } diff --git a/server/src/XiangYi.Application/Services/NotificationService.cs b/server/src/XiangYi.Application/Services/NotificationService.cs index 5e60e30..753487c 100644 --- a/server/src/XiangYi.Application/Services/NotificationService.cs +++ b/server/src/XiangYi.Application/Services/NotificationService.cs @@ -19,6 +19,7 @@ public class NotificationService : INotificationService private readonly IRepository _userRepository; private readonly IWeChatService _weChatService; private readonly WeChatOptions _weChatOptions; + private readonly ISystemConfigService _configService; private readonly ILogger _logger; /// @@ -47,6 +48,7 @@ public class NotificationService : INotificationService IRepository userRepository, IWeChatService weChatService, IOptions weChatOptions, + ISystemConfigService configService, ILogger logger) { _notificationRepository = notificationRepository; @@ -54,6 +56,7 @@ public class NotificationService : INotificationService _userRepository = userRepository; _weChatService = weChatService; _weChatOptions = weChatOptions.Value; + _configService = configService; _logger = logger; } @@ -534,7 +537,7 @@ public class NotificationService : INotificationService { try { - var templateId = GetServiceAccountTemplateId(templateType); + var templateId = await GetServiceAccountTemplateIdAsync(templateType); if (string.IsNullOrEmpty(templateId)) { _logger.LogWarning("服务号模板ID未配置: TemplateType={TemplateType}", templateType); @@ -648,12 +651,27 @@ public class NotificationService : INotificationService } /// - /// 获取服务号模板ID + /// 获取服务号模板ID(优先从数据库配置读取,回退到appsettings.json) /// - /// 模板类型 - /// 模板ID - private string GetServiceAccountTemplateId(NotificationTemplateType templateType) + private async Task GetServiceAccountTemplateIdAsync(NotificationTemplateType templateType) { + string? configKey = templateType switch + { + NotificationTemplateType.Unlock => SystemConfigService.SaUnlockTemplateIdKey, + NotificationTemplateType.Favorite => SystemConfigService.SaFavoriteTemplateIdKey, + NotificationTemplateType.FirstMessage => SystemConfigService.SaMessageTemplateIdKey, + NotificationTemplateType.MessageReminder => SystemConfigService.SaMessageTemplateIdKey, + NotificationTemplateType.DailyRecommend => SystemConfigService.SaDailyRecommendTemplateIdKey, + _ => null + }; + + if (configKey != null) + { + var dbValue = await _configService.GetConfigValueAsync(configKey); + if (!string.IsNullOrEmpty(dbValue)) + return dbValue; + } + return templateType switch { NotificationTemplateType.Unlock => _weChatOptions.ServiceAccount.UnlockTemplateId, diff --git a/server/src/XiangYi.Application/Services/SystemConfigService.cs b/server/src/XiangYi.Application/Services/SystemConfigService.cs index 27ad45a..64f72ee 100644 --- a/server/src/XiangYi.Application/Services/SystemConfigService.cs +++ b/server/src/XiangYi.Application/Services/SystemConfigService.cs @@ -98,6 +98,11 @@ public class SystemConfigService : ISystemConfigService /// public const decimal DefaultRealNamePrice = 88m; + public const string SaUnlockTemplateIdKey = "sa_unlock_template_id"; + public const string SaFavoriteTemplateIdKey = "sa_favorite_template_id"; + public const string SaMessageTemplateIdKey = "sa_message_template_id"; + public const string SaDailyRecommendTemplateIdKey = "sa_daily_recommend_template_id"; + public SystemConfigService( IRepository configRepository, ILogger logger) @@ -344,6 +349,40 @@ public class SystemConfigService : ISystemConfigService { return await SetConfigValueAsync(RealNamePriceKey, price.ToString(), "实名认证费用(元)"); } + + /// + public async Task GetNotificationTemplatesAsync() + { + return new NotificationTemplatesDto + { + UnlockTemplateId = await GetConfigValueAsync(SaUnlockTemplateIdKey), + FavoriteTemplateId = await GetConfigValueAsync(SaFavoriteTemplateIdKey), + MessageTemplateId = await GetConfigValueAsync(SaMessageTemplateIdKey), + DailyRecommendTemplateId = await GetConfigValueAsync(SaDailyRecommendTemplateIdKey) + }; + } + + /// + public async Task SetNotificationTemplatesAsync(NotificationTemplatesDto templates) + { + try + { + if (!string.IsNullOrEmpty(templates.UnlockTemplateId)) + await SetConfigValueAsync(SaUnlockTemplateIdKey, templates.UnlockTemplateId, "服务号解锁通知模板ID"); + if (!string.IsNullOrEmpty(templates.FavoriteTemplateId)) + await SetConfigValueAsync(SaFavoriteTemplateIdKey, templates.FavoriteTemplateId, "服务号收藏通知模板ID"); + if (!string.IsNullOrEmpty(templates.MessageTemplateId)) + await SetConfigValueAsync(SaMessageTemplateIdKey, templates.MessageTemplateId, "服务号消息通知模板ID"); + if (!string.IsNullOrEmpty(templates.DailyRecommendTemplateId)) + await SetConfigValueAsync(SaDailyRecommendTemplateIdKey, templates.DailyRecommendTemplateId, "服务号每日推荐通知模板ID"); + return true; + } + catch (Exception ex) + { + _logger.LogError(ex, "设置通知模板配置失败"); + return false; + } + } } /// @@ -371,3 +410,29 @@ public class MemberIconsDto /// public string? TimeLimitedMemberIcon { get; set; } } + +/// +/// 服务号通知模板配置DTO +/// +public class NotificationTemplatesDto +{ + /// + /// 解锁通知模板ID + /// + public string? UnlockTemplateId { get; set; } + + /// + /// 收藏通知模板ID + /// + public string? FavoriteTemplateId { get; set; } + + /// + /// 消息通知模板ID(首次消息/未回复提醒共用) + /// + public string? MessageTemplateId { get; set; } + + /// + /// 每日推荐通知模板ID + /// + public string? DailyRecommendTemplateId { get; set; } +}