Add notification template configuration management
All checks were successful
continuous-integration/drone/push Build is passing
All checks were successful
continuous-integration/drone/push Build is passing
This commit is contained in:
parent
ecccecfd83
commit
b55b56c9cb
137
README.md
Normal file
137
README.md
Normal 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) | 产品需求说明 |
|
||||
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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; }
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user