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; }
+}