feat(wechat): Add template field mapping configuration for notification templates
All checks were successful
continuous-integration/drone/push Build is passing

- Add field mapping properties to NotificationTemplatesConfig interface for unlock, favorite, message, and daily recommend templates
- Add UI form fields in system config view for configuring JSON-based field mappings with helper text
- Implement dynamic template data construction using configured field mappings in NotificationService
- Add fallback to traditional first/keyword format when field mapping is not configured
- Support flexible field mapping with value types: title, name, content, time, remark
- Update SystemConfigService to persist and retrieve field mapping configurations
- Enable backend-driven template field customization without code changes
This commit is contained in:
zpc 2026-03-29 21:22:01 +08:00
parent 125ff21b77
commit 3764b6ef57
4 changed files with 160 additions and 11 deletions

View File

@ -192,9 +192,13 @@ export interface NotificationTemplatesConfig {
token?: string
encodingAESKey?: string
unlockTemplateId?: string
unlockFieldMapping?: string
favoriteTemplateId?: string
favoriteFieldMapping?: string
messageTemplateId?: string
messageFieldMapping?: string
dailyRecommendTemplateId?: string
dailyRecommendFieldMapping?: string
}
/**

View File

@ -340,6 +340,15 @@
<div class="template-tip">有用户解锁我时服务号发送相应通知</div>
</el-form-item>
<el-form-item label="解锁通知字段映射">
<el-input
v-model="templateForm.unlockFieldMapping"
placeholder='例: {"thing1":"title","thing2":"name","time3":"time"}'
clearable
/>
<div class="template-tip">JSON格式key为模板字段名value为数据类型title=标题, name=名称, content=内容, time=时间, remark=备注</div>
</el-form-item>
<el-form-item label="收藏通知模板ID">
<el-input
v-model="templateForm.favoriteTemplateId"
@ -349,6 +358,15 @@
<div class="template-tip">有用户收藏我时服务号发送相应通知</div>
</el-form-item>
<el-form-item label="收藏通知字段映射">
<el-input
v-model="templateForm.favoriteFieldMapping"
placeholder='例: {"thing1":"title","thing2":"name","time3":"time"}'
clearable
/>
<div class="template-tip">JSON格式key为模板字段名value为数据类型title=标题, name=名称, content=内容, time=时间, remark=备注</div>
</el-form-item>
<el-form-item label="消息通知模板ID">
<el-input
v-model="templateForm.messageTemplateId"
@ -358,6 +376,15 @@
<div class="template-tip">首次沟通通知5分钟未回复提醒共用此模板</div>
</el-form-item>
<el-form-item label="消息通知字段映射">
<el-input
v-model="templateForm.messageFieldMapping"
placeholder='例: {"thing1":"title","thing2":"name","time3":"time"}'
clearable
/>
<div class="template-tip">JSON格式key为模板字段名value为数据类型title=标题, name=名称, content=内容, time=时间, remark=备注</div>
</el-form-item>
<el-form-item label="每日推荐通知模板ID">
<el-input
v-model="templateForm.dailyRecommendTemplateId"
@ -367,6 +394,15 @@
<div class="template-tip">每天早上8~10点随机时间发送推荐更新通知</div>
</el-form-item>
<el-form-item label="每日推荐字段映射">
<el-input
v-model="templateForm.dailyRecommendFieldMapping"
placeholder='例: {"thing1":"title","thing2":"content","time3":"time"}'
clearable
/>
<div class="template-tip">JSON格式key为模板字段名value为数据类型title=标题, name=名称, content=内容, time=时间, remark=备注</div>
</el-form-item>
<el-form-item>
<el-button type="primary" @click="saveNotificationTemplates" :loading="savingTemplates">
保存模板配置
@ -438,9 +474,13 @@ const templateForm = ref({
token: '',
encodingAESKey: '',
unlockTemplateId: '',
unlockFieldMapping: '',
favoriteTemplateId: '',
favoriteFieldMapping: '',
messageTemplateId: '',
dailyRecommendTemplateId: ''
messageFieldMapping: '',
dailyRecommendTemplateId: '',
dailyRecommendFieldMapping: ''
})
const saving = ref(false)
@ -718,9 +758,13 @@ const loadNotificationTemplates = async () => {
templateForm.value.token = res.token || ''
templateForm.value.encodingAESKey = res.encodingAESKey || ''
templateForm.value.unlockTemplateId = res.unlockTemplateId || ''
templateForm.value.unlockFieldMapping = res.unlockFieldMapping || ''
templateForm.value.favoriteTemplateId = res.favoriteTemplateId || ''
templateForm.value.favoriteFieldMapping = res.favoriteFieldMapping || ''
templateForm.value.messageTemplateId = res.messageTemplateId || ''
templateForm.value.messageFieldMapping = res.messageFieldMapping || ''
templateForm.value.dailyRecommendTemplateId = res.dailyRecommendTemplateId || ''
templateForm.value.dailyRecommendFieldMapping = res.dailyRecommendFieldMapping || ''
}
} catch (error) {
console.error('加载模板配置失败:', error)
@ -734,9 +778,13 @@ const saveNotificationTemplates = async () => {
token: templateForm.value.token || undefined,
encodingAESKey: templateForm.value.encodingAESKey || undefined,
unlockTemplateId: templateForm.value.unlockTemplateId || undefined,
unlockFieldMapping: templateForm.value.unlockFieldMapping || undefined,
favoriteTemplateId: templateForm.value.favoriteTemplateId || undefined,
favoriteFieldMapping: templateForm.value.favoriteFieldMapping || undefined,
messageTemplateId: templateForm.value.messageTemplateId || undefined,
dailyRecommendTemplateId: templateForm.value.dailyRecommendTemplateId || undefined
messageFieldMapping: templateForm.value.messageFieldMapping || undefined,
dailyRecommendTemplateId: templateForm.value.dailyRecommendTemplateId || undefined,
dailyRecommendFieldMapping: templateForm.value.dailyRecommendFieldMapping || undefined
})
ElMessage.success('模板配置保存成功')
} catch (error) {

View File

@ -544,6 +544,43 @@ public class NotificationService : INotificationService
return false;
}
// 从数据库读取字段映射
var fieldMapping = await GetServiceAccountFieldMappingAsync(templateType);
Dictionary<string, TemplateDataItem> data;
if (fieldMapping != null && fieldMapping.Count > 0)
{
// 使用后台配置的字段映射动态构建
// 映射格式: {"thing1":"title","thing2":"name","time3":"time","thing4":"content","thing5":"remark"}
// 值映射: title->标题, name->名称, content->内容, time->时间, remark->备注
data = new Dictionary<string, TemplateDataItem>();
foreach (var (fieldName, valueKey) in fieldMapping)
{
var value = valueKey?.ToLower() switch
{
"title" => title,
"name" => name,
"content" => content,
"time" => time,
"remark" => "点击查看详情",
_ => valueKey ?? ""
};
data[fieldName] = new TemplateDataItem { Value = value };
}
}
else
{
// 兜底:使用传统的 first/keyword 格式
data = new Dictionary<string, TemplateDataItem>
{
["first"] = new TemplateDataItem { Value = title },
["keyword1"] = new TemplateDataItem { Value = name },
["keyword2"] = new TemplateDataItem { Value = content },
["keyword3"] = new TemplateDataItem { Value = time },
["remark"] = new TemplateDataItem { Value = "点击查看详情" }
};
}
var request = new ServiceAccountTemplateMessageRequest
{
ToUser = serviceAccountOpenId,
@ -553,14 +590,7 @@ public class NotificationService : INotificationService
AppId = _weChatOptions.MiniProgram.AppId,
PagePath = page
},
Data = new Dictionary<string, TemplateDataItem>
{
["first"] = new TemplateDataItem { Value = title },
["keyword1"] = new TemplateDataItem { Value = name },
["keyword2"] = new TemplateDataItem { Value = content },
["keyword3"] = new TemplateDataItem { Value = time },
["remark"] = new TemplateDataItem { Value = "点击查看详情" }
}
Data = data
};
var success = await _weChatService.SendServiceAccountTemplateMessageAsync(request);
@ -576,6 +606,37 @@ public class NotificationService : INotificationService
}
}
/// <summary>
/// 获取服务号模板字段映射
/// </summary>
private async Task<Dictionary<string, string>?> GetServiceAccountFieldMappingAsync(NotificationTemplateType templateType)
{
string? configKey = templateType switch
{
NotificationTemplateType.Unlock => SystemConfigService.SaUnlockFieldMappingKey,
NotificationTemplateType.Favorite => SystemConfigService.SaFavoriteFieldMappingKey,
NotificationTemplateType.FirstMessage => SystemConfigService.SaMessageFieldMappingKey,
NotificationTemplateType.MessageReminder => SystemConfigService.SaMessageFieldMappingKey,
NotificationTemplateType.DailyRecommend => SystemConfigService.SaDailyRecommendFieldMappingKey,
_ => null
};
if (configKey == null) return null;
var json = await _configService.GetConfigValueAsync(configKey);
if (string.IsNullOrEmpty(json)) return null;
try
{
return System.Text.Json.JsonSerializer.Deserialize<Dictionary<string, string>>(json);
}
catch
{
_logger.LogWarning("解析字段映射失败: Key={Key}, Value={Value}", configKey, json);
return null;
}
}
#endregion

View File

@ -101,9 +101,13 @@ public class SystemConfigService : ISystemConfigService
public const string SaTokenKey = "sa_token";
public const string SaEncodingAesKeyKey = "sa_encoding_aes_key";
public const string SaUnlockTemplateIdKey = "sa_unlock_template_id";
public const string SaUnlockFieldMappingKey = "sa_unlock_field_mapping";
public const string SaFavoriteTemplateIdKey = "sa_favorite_template_id";
public const string SaFavoriteFieldMappingKey = "sa_favorite_field_mapping";
public const string SaMessageTemplateIdKey = "sa_message_template_id";
public const string SaMessageFieldMappingKey = "sa_message_field_mapping";
public const string SaDailyRecommendTemplateIdKey = "sa_daily_recommend_template_id";
public const string SaDailyRecommendFieldMappingKey = "sa_daily_recommend_field_mapping";
public SystemConfigService(
IRepository<SystemConfig> configRepository,
@ -360,9 +364,13 @@ public class SystemConfigService : ISystemConfigService
Token = await GetConfigValueAsync(SaTokenKey),
EncodingAESKey = await GetConfigValueAsync(SaEncodingAesKeyKey),
UnlockTemplateId = await GetConfigValueAsync(SaUnlockTemplateIdKey),
UnlockFieldMapping = await GetConfigValueAsync(SaUnlockFieldMappingKey),
FavoriteTemplateId = await GetConfigValueAsync(SaFavoriteTemplateIdKey),
FavoriteFieldMapping = await GetConfigValueAsync(SaFavoriteFieldMappingKey),
MessageTemplateId = await GetConfigValueAsync(SaMessageTemplateIdKey),
DailyRecommendTemplateId = await GetConfigValueAsync(SaDailyRecommendTemplateIdKey)
MessageFieldMapping = await GetConfigValueAsync(SaMessageFieldMappingKey),
DailyRecommendTemplateId = await GetConfigValueAsync(SaDailyRecommendTemplateIdKey),
DailyRecommendFieldMapping = await GetConfigValueAsync(SaDailyRecommendFieldMappingKey)
};
}
@ -377,12 +385,20 @@ public class SystemConfigService : ISystemConfigService
await SetConfigValueAsync(SaEncodingAesKeyKey, templates.EncodingAESKey, "服务号消息加解密密钥");
if (!string.IsNullOrEmpty(templates.UnlockTemplateId))
await SetConfigValueAsync(SaUnlockTemplateIdKey, templates.UnlockTemplateId, "服务号解锁通知模板ID");
if (!string.IsNullOrEmpty(templates.UnlockFieldMapping))
await SetConfigValueAsync(SaUnlockFieldMappingKey, templates.UnlockFieldMapping, "服务号解锁通知字段映射");
if (!string.IsNullOrEmpty(templates.FavoriteTemplateId))
await SetConfigValueAsync(SaFavoriteTemplateIdKey, templates.FavoriteTemplateId, "服务号收藏通知模板ID");
if (!string.IsNullOrEmpty(templates.FavoriteFieldMapping))
await SetConfigValueAsync(SaFavoriteFieldMappingKey, templates.FavoriteFieldMapping, "服务号收藏通知字段映射");
if (!string.IsNullOrEmpty(templates.MessageTemplateId))
await SetConfigValueAsync(SaMessageTemplateIdKey, templates.MessageTemplateId, "服务号消息通知模板ID");
if (!string.IsNullOrEmpty(templates.MessageFieldMapping))
await SetConfigValueAsync(SaMessageFieldMappingKey, templates.MessageFieldMapping, "服务号消息通知字段映射");
if (!string.IsNullOrEmpty(templates.DailyRecommendTemplateId))
await SetConfigValueAsync(SaDailyRecommendTemplateIdKey, templates.DailyRecommendTemplateId, "服务号每日推荐通知模板ID");
if (!string.IsNullOrEmpty(templates.DailyRecommendFieldMapping))
await SetConfigValueAsync(SaDailyRecommendFieldMappingKey, templates.DailyRecommendFieldMapping, "服务号每日推荐通知字段映射");
return true;
}
catch (Exception ex)
@ -439,18 +455,38 @@ public class NotificationTemplatesDto
/// </summary>
public string? UnlockTemplateId { get; set; }
/// <summary>
/// 解锁通知字段映射JSON如 {"thing1":"title","thing2":"name","time3":"time"}
/// </summary>
public string? UnlockFieldMapping { get; set; }
/// <summary>
/// 收藏通知模板ID
/// </summary>
public string? FavoriteTemplateId { get; set; }
/// <summary>
/// 收藏通知字段映射JSON
/// </summary>
public string? FavoriteFieldMapping { get; set; }
/// <summary>
/// 消息通知模板ID首次消息/未回复提醒共用)
/// </summary>
public string? MessageTemplateId { get; set; }
/// <summary>
/// 消息通知字段映射JSON
/// </summary>
public string? MessageFieldMapping { get; set; }
/// <summary>
/// 每日推荐通知模板ID
/// </summary>
public string? DailyRecommendTemplateId { get; set; }
/// <summary>
/// 每日推荐通知字段映射JSON
/// </summary>
public string? DailyRecommendFieldMapping { get; set; }
}