Add notification template configuration management
All checks were successful
continuous-integration/drone/push Build is passing

This commit is contained in:
zpc 2026-03-29 01:11:22 +08:00
parent ecccecfd83
commit b55b56c9cb
7 changed files with 384 additions and 6 deletions

137
README.md Normal file
View File

@ -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) | 产品需求说明 |

View File

@ -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)
}

View File

@ -301,6 +301,61 @@
/>
</div>
</el-tab-pane>
<!-- 消息模板配置 -->
<el-tab-pane label="消息模板" name="notificationTemplates">
<el-form :model="templateForm" label-width="160px" class="config-form">
<el-alert
title="服务号模板消息配置"
description="在微信公众平台 → 模板消息 中获取模板ID填入下方对应字段。模板ID格式类似1WwIIY4NoPWE972HfSgjm..."
type="info"
:closable="false"
style="margin-bottom: 24px;"
/>
<el-form-item label="解锁通知模板ID">
<el-input
v-model="templateForm.unlockTemplateId"
placeholder="用户解锁我时发送的通知模板ID"
clearable
/>
<div class="template-tip">有用户解锁我时服务号发送相应通知</div>
</el-form-item>
<el-form-item label="收藏通知模板ID">
<el-input
v-model="templateForm.favoriteTemplateId"
placeholder="用户收藏我时发送的通知模板ID"
clearable
/>
<div class="template-tip">有用户收藏我时服务号发送相应通知</div>
</el-form-item>
<el-form-item label="消息通知模板ID">
<el-input
v-model="templateForm.messageTemplateId"
placeholder="首次聊天 / 5分钟未回复时发送的通知模板ID"
clearable
/>
<div class="template-tip">首次沟通通知5分钟未回复提醒共用此模板</div>
</el-form-item>
<el-form-item label="每日推荐通知模板ID">
<el-input
v-model="templateForm.dailyRecommendTemplateId"
placeholder="每日推荐列表更新时发送的通知模板ID"
clearable
/>
<div class="template-tip">每天早上8~10点随机时间发送推荐更新通知</div>
</el-form-item>
<el-form-item>
<el-button type="primary" @click="saveNotificationTemplates" :loading="savingTemplates">
保存模板配置
</el-button>
</el-form-item>
</el-form>
</el-tab-pane>
</el-tabs>
</el-card>
</div>
@ -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()
})
</script>
@ -863,4 +960,11 @@ onMounted(() => {
color: #606266;
font-size: 14px;
}
.template-tip {
color: #909399;
font-size: 12px;
margin-top: 4px;
line-height: 1.6;
}
</style>

View File

@ -362,6 +362,26 @@ public class AdminConfigController : ControllerBase
var result = await _configService.SetRealNamePriceAsync(request.Price);
return result ? ApiResponse.Success("设置成功") : ApiResponse.Error(40001, "设置失败");
}
/// <summary>
/// 获取服务号通知模板配置
/// </summary>
[HttpGet("notificationTemplates")]
public async Task<ApiResponse<NotificationTemplatesDto>> GetNotificationTemplates()
{
var templates = await _configService.GetNotificationTemplatesAsync();
return ApiResponse<NotificationTemplatesDto>.Success(templates);
}
/// <summary>
/// 设置服务号通知模板配置
/// </summary>
[HttpPost("notificationTemplates")]
public async Task<ApiResponse> SetNotificationTemplates([FromBody] NotificationTemplatesDto request)
{
var result = await _configService.SetNotificationTemplatesAsync(request);
return result ? ApiResponse.Success("设置成功") : ApiResponse.Error(40001, "设置失败");
}
}
/// <summary>

View File

@ -141,4 +141,14 @@ public interface ISystemConfigService
/// 设置实名认证费用
/// </summary>
Task<bool> SetRealNamePriceAsync(decimal price);
/// <summary>
/// 获取服务号通知模板配置
/// </summary>
Task<NotificationTemplatesDto> GetNotificationTemplatesAsync();
/// <summary>
/// 设置服务号通知模板配置
/// </summary>
Task<bool> SetNotificationTemplatesAsync(NotificationTemplatesDto templates);
}

View File

@ -19,6 +19,7 @@ public class NotificationService : INotificationService
private readonly IRepository<User> _userRepository;
private readonly IWeChatService _weChatService;
private readonly WeChatOptions _weChatOptions;
private readonly ISystemConfigService _configService;
private readonly ILogger<NotificationService> _logger;
/// <summary>
@ -47,6 +48,7 @@ public class NotificationService : INotificationService
IRepository<User> userRepository,
IWeChatService weChatService,
IOptions<WeChatOptions> weChatOptions,
ISystemConfigService configService,
ILogger<NotificationService> 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
}
/// <summary>
/// 获取服务号模板ID
/// 获取服务号模板ID优先从数据库配置读取回退到appsettings.json
/// </summary>
/// <param name="templateType">模板类型</param>
/// <returns>模板ID</returns>
private string GetServiceAccountTemplateId(NotificationTemplateType templateType)
private async Task<string> 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,

View File

@ -98,6 +98,11 @@ public class SystemConfigService : ISystemConfigService
/// </summary>
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<SystemConfig> configRepository,
ILogger<SystemConfigService> logger)
@ -344,6 +349,40 @@ public class SystemConfigService : ISystemConfigService
{
return await SetConfigValueAsync(RealNamePriceKey, price.ToString(), "实名认证费用(元)");
}
/// <inheritdoc />
public async Task<NotificationTemplatesDto> GetNotificationTemplatesAsync()
{
return new NotificationTemplatesDto
{
UnlockTemplateId = await GetConfigValueAsync(SaUnlockTemplateIdKey),
FavoriteTemplateId = await GetConfigValueAsync(SaFavoriteTemplateIdKey),
MessageTemplateId = await GetConfigValueAsync(SaMessageTemplateIdKey),
DailyRecommendTemplateId = await GetConfigValueAsync(SaDailyRecommendTemplateIdKey)
};
}
/// <inheritdoc />
public async Task<bool> 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;
}
}
}
/// <summary>
@ -371,3 +410,29 @@ public class MemberIconsDto
/// </summary>
public string? TimeLimitedMemberIcon { get; set; }
}
/// <summary>
/// 服务号通知模板配置DTO
/// </summary>
public class NotificationTemplatesDto
{
/// <summary>
/// 解锁通知模板ID
/// </summary>
public string? UnlockTemplateId { get; set; }
/// <summary>
/// 收藏通知模板ID
/// </summary>
public string? FavoriteTemplateId { get; set; }
/// <summary>
/// 消息通知模板ID首次消息/未回复提醒共用)
/// </summary>
public string? MessageTemplateId { get; set; }
/// <summary>
/// 每日推荐通知模板ID
/// </summary>
public string? DailyRecommendTemplateId { get; set; }
}