本文目录导读:

我来详细说明PHP项目中短信模板的后端配置管理方案。
数据库表设计
短信模板表
CREATE TABLE `sms_templates` ( `id` int(11) NOT NULL AUTO_INCREMENT, `template_code` varchar(50) NOT NULL COMMENT '模板编码', `template_name` varchar(100) NOT NULL COMMENT '模板名称', `template_content` text NOT NULL COMMENT '模板内容', `template_type` tinyint(1) DEFAULT '1' COMMENT '模板类型 1:验证码 2:通知 3:营销', `channel` varchar(50) DEFAULT 'default' COMMENT '短信通道', `params` json DEFAULT NULL COMMENT '参数定义', `status` tinyint(1) DEFAULT '1' COMMENT '状态 1:启用 0:禁用', `remark` varchar(500) DEFAULT NULL COMMENT '备注', `created_at` datetime DEFAULT CURRENT_TIMESTAMP, `updated_at` datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, PRIMARY KEY (`id`), UNIQUE KEY `uk_template_code` (`template_code`), KEY `idx_status` (`status`), KEY `idx_type` (`template_type`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='短信模板表';
配置管理接口
模板管理控制器
<?php
namespace App\Http\Controllers\Admin;
use App\Models\SmsTemplate;
use Illuminate\Http\Request;
use App\Http\Controllers\Controller;
class SmsTemplateController extends Controller
{
/**
* 模板列表
*/
public function index(Request $request)
{
$templates = SmsTemplate::where(function($query) use ($request) {
// 按状态筛选
if ($request->has('status')) {
$query->where('status', $request->status);
}
// 按类型筛选
if ($request->has('type')) {
$query->where('template_type', $request->type);
}
// 关键词搜索
if ($request->has('keyword')) {
$query->where(function($q) use ($request) {
$q->where('template_name', 'like', "%{$request->keyword}%")
->orWhere('template_code', 'like', "%{$request->keyword}%");
});
}
})->paginate(15);
return response()->json([
'code' => 200,
'data' => $templates
]);
}
/**
* 创建模板
*/
public function store(Request $request)
{
$validated = $request->validate([
'template_code' => 'required|unique:sms_templates|max:50',
'template_name' => 'required|max:100',
'template_content' => 'required',
'template_type' => 'required|in:1,2,3',
'params' => 'nullable|json',
'channel' => 'nullable|max:50',
'remark' => 'nullable|max:500'
]);
// 解析参数占位符
$params = $this->parseTemplateParams($validated['template_content']);
$validated['params'] = json_encode($params);
$template = SmsTemplate::create($validated);
// 清除缓存
$this->clearTemplateCache();
return response()->json([
'code' => 200,
'message' => '创建成功',
'data' => $template
]);
}
/**
* 更新模板
*/
public function update(Request $request, $id)
{
$template = SmsTemplate::findOrFail($id);
$validated = $request->validate([
'template_code' => 'required|max:50|unique:sms_templates,template_code,'.$id,
'template_name' => 'required|max:100',
'template_content' => 'required',
'template_type' => 'required|in:1,2,3',
'params' => 'nullable|json',
'channel' => 'nullable|max:50',
'status' => 'nullable|in:0,1',
'remark' => 'nullable|max:500'
]);
// 重新解析参数
$params = $this->parseTemplateParams($validated['template_content']);
$validated['params'] = json_encode($params);
$template->update($validated);
// 清除缓存
$this->clearTemplateCache();
return response()->json([
'code' => 200,
'message' => '更新成功',
'data' => $template
]);
}
/**
* 解析模板参数
*/
private function parseTemplateParams($content)
{
preg_match_all('/\{(\w+)\}/', $content, $matches);
return $matches[1] ?? [];
}
/**
* 清除模板缓存
*/
private function clearTemplateCache()
{
\Cache::forget('sms_templates_all');
\Cache::forget('sms_templates_active');
}
}
模板渲染服务
短信发送服务
<?php
namespace App\Services;
use App\Models\SmsTemplate;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\Log;
class SmsService
{
/**
* 获取模板并渲染
*/
public function renderTemplate($templateCode, $params = [])
{
// 从缓存获取模板
$template = $this->getTemplate($templateCode);
if (!$template) {
throw new \Exception("短信模板不存在: {$templateCode}");
}
// 替换参数
$content = $template->template_content;
foreach ($params as $key => $value) {
$content = str_replace("{{$key}}", $value, $content);
}
// 检查未替换的参数
preg_match_all('/\{(\w+)\}/', $content, $unreplaced);
if (!empty($unreplaced[0])) {
throw new \Exception("缺少必要参数: " . implode(', ', $unreplaced[0]));
}
return [
'content' => $content,
'template' => $template,
'channel' => $template->channel
];
}
/**
* 获取模板(带缓存)
*/
private function getTemplate($templateCode)
{
$templates = Cache::remember('sms_templates_active', 3600, function() {
return SmsTemplate::where('status', 1)
->get()
->keyBy('template_code');
});
return $templates->get($templateCode);
}
/**
* 发送短信
*/
public function send($phone, $templateCode, $params = [])
{
try {
// 渲染模板
$rendered = $this->renderTemplate($templateCode, $params);
// 根据通道发送
$result = $this->sendByChannel($phone, $rendered);
// 记录日志
Log::info('短信发送', [
'phone' => $phone,
'template' => $templateCode,
'params' => $params,
'result' => $result
]);
return $result;
} catch (\Exception $e) {
Log::error('短信发送失败', [
'phone' => $phone,
'template' => $templateCode,
'error' => $e->getMessage()
]);
throw $e;
}
}
/**
* 按通道发送
*/
private function sendByChannel($phone, $rendered)
{
$channel = $rendered['channel'] ?: config('sms.default_channel', 'aliyun');
switch ($channel) {
case 'aliyun':
return $this->sendByAliyun($phone, $rendered);
case 'tencent':
return $this->sendByTencent($phone, $rendered);
case 'yunpian':
return $this->sendByYunpian($phone, $rendered);
default:
throw new \Exception("不支持的短信通道: {$channel}");
}
}
}
配置管理页面
管理员操作接口
<?php
namespace App\Http\Controllers\Admin;
use App\Models\SmsConfig;
use Illuminate\Http\Request;
class SmsConfigController extends Controller
{
/**
* 短信全局配置
*/
public function config(Request $request)
{
$config = [
'default_channel' => config('sms.default_channel'),
'channels' => [
'aliyun' => [
'access_key_id' => config('sms.aliyun.access_key_id'),
'access_key_secret' => config('sms.aliyun.access_key_secret'),
'sign_name' => config('sms.aliyun.sign_name'),
],
'tencent' => [
'app_id' => config('sms.tencent.app_id'),
'app_key' => config('sms.tencent.app_key'),
'sign_name' => config('sms.tencent.sign_name'),
]
],
'limits' => [
'daily_limit' => config('sms.limits.daily_limit', 1000),
'verify_code_limit' => config('sms.limits.verify_code_limit', 10),
'interval_seconds' => config('sms.limits.interval_seconds', 60)
],
'templates' => SmsTemplate::select('id', 'template_code', 'template_name', 'status')
->get()
];
return response()->json([
'code' => 200,
'data' => $config
]);
}
/**
* 更新配置
*/
public function updateConfig(Request $request)
{
$validated = $request->validate([
'default_channel' => 'required|in:aliyun,tencent,yunpian',
'channels' => 'required|array',
'limits' => 'required|array',
]);
// 保存到数据库或配置文件
$this->saveConfig($validated);
// 清除配置缓存
\Cache::forget('sms_config');
return response()->json([
'code' => 200,
'message' => '配置更新成功'
]);
}
/**
* 测试发送
*/
public function testSend(Request $request)
{
$validated = $request->validate([
'phone' => 'required|phone',
'template_code' => 'required|exists:sms_templates,template_code',
'params' => 'required|array'
]);
try {
$smsService = app(SmsService::class);
$result = $smsService->send(
$validated['phone'],
$validated['template_code'],
$validated['params']
);
return response()->json([
'code' => 200,
'message' => '发送成功',
'data' => $result
]);
} catch (\Exception $e) {
return response()->json([
'code' => 400,
'message' => $e->getMessage()
], 400);
}
}
}
前端管理界面(Vue示例)
模板管理组件
<template>
<div class="sms-template-manager">
<!-- 模板列表 -->
<el-table :data="templates" v-loading="loading">
<el-table-column prop="template_code" label="模板编码" width="150" />
<el-table-column prop="template_name" label="模板名称" width="200" />
<el-table-column prop="template_content" label="模板内容" min-width="300">
<template slot-scope="scope">
<div class="template-preview"
v-html="highlightParams(scope.row.template_content)">
</div>
</template>
</el-table-column>
<el-table-column prop="template_type" label="类型" width="100">
<template slot-scope="scope">
<el-tag :type="typeMap[scope.row.template_type].type">
{{ typeMap[scope.row.template_type].label }}
</el-tag>
</template>
</el-table-column>
<el-table-column prop="status" label="状态" width="100">
<template slot-scope="scope">
<el-switch
v-model="scope.row.status"
@change="toggleStatus(scope.row)"
active-color="#13ce66"
inactive-color="#ff4949">
</el-switch>
</template>
</el-table-column>
<el-table-column label="操作" width="200" fixed="right">
<template slot-scope="scope">
<el-button size="mini" @click="editTemplate(scope.row)">
编辑
</el-button>
<el-button size="mini" type="danger" @click="deleteTemplate(scope.row)">
删除
</el-button>
<el-button size="mini" type="success" @click="testSend(scope.row)">
测试
</el-button>
</template>
</el-table-column>
</el-table>
<!-- 编辑/创建弹窗 -->
<el-dialog :title="dialogTitle" :visible.sync="dialogVisible" width="600px">
<el-form :model="templateForm" :rules="formRules" ref="templateForm">
<el-form-item label="模板编码" prop="template_code">
<el-input v-model="templateForm.template_code" />
</el-form-item>
<el-form-item label="模板名称" prop="template_name">
<el-input v-model="templateForm.template_name" />
</el-form-item>
<el-form-item label="模板内容" prop="template_content">
<el-input
type="textarea"
:rows="4"
v-model="templateForm.template_content"
placeholder="使用 {param_name} 作为参数占位符">
</el-input>
<div class="params-tip">
可用参数:
<el-tag
v-for="param in detectedParams"
:key="param"
size="mini"
type="warning">
{{ param }}
</el-tag>
</div>
</el-form-item>
<el-form-item label="短信类型" prop="template_type">
<el-select v-model="templateForm.template_type">
<el-option label="验证码" :value="1" />
<el-option label="通知" :value="2" />
<el-option label="营销" :value="3" />
</el-select>
</el-form-item>
<el-form-item label="短信通道" prop="channel">
<el-select v-model="templateForm.channel">
<el-option label="默认" value="default" />
<el-option label="阿里云" value="aliyun" />
<el-option label="腾讯云" value="tencent" />
</el-select>
</el-form-item>
<el-form-item label="备注" prop="remark">
<el-input type="textarea" v-model="templateForm.remark" />
</el-form-item>
</el-form>
<span slot="footer">
<el-button @click="dialogVisible = false">取消</el-button>
<el-button type="primary" @click="submitForm">保存</el-button>
</span>
</el-dialog>
<!-- 测试发送弹窗 -->
<el-dialog title="测试发送" :visible.sync="testDialogVisible" width="400px">
<el-form :model="testForm">
<el-form-item label="手机号" prop="phone">
<el-input v-model="testForm.phone" />
</el-form-item>
<el-form-item label="参数" prop="params">
<div v-for="(param, index) in testForm.params" :key="index">
<el-input
:placeholder="param"
v-model="testForm.paramValues[index]"
style="margin-bottom: 10px">
<template slot="prepend">{{ param }} =</template>
</el-input>
</div>
</el-form-item>
</el-form>
<span slot="footer">
<el-button @click="testDialogVisible = false">取消</el-button>
<el-button type="primary" @click="doTestSend">发送测试</el-button>
</span>
</el-dialog>
</div>
</template>
<script>
export default {
data() {
return {
loading: false,
templates: [],
dialogVisible: false,
testDialogVisible: false,
isEdit: false,
templateForm: {
template_code: '',
template_name: '',
template_content: '',
template_type: 1,
channel: 'default',
remark: ''
},
testForm: {
phone: '',
params: [],
paramValues: []
},
formRules: {
template_code: [
{ required: true, message: '请输入模板编码', trigger: 'blur' },
{ min: 2, max: 50, message: '长度在 2 到 50 个字符', trigger: 'blur' }
],
template_name: [
{ required: true, message: '请输入模板名称', trigger: 'blur' }
],
template_content: [
{ required: true, message: '请输入模板内容', trigger: 'blur' }
],
template_type: [
{ required: true, message: '请选择短信类型', trigger: 'change' }
]
},
typeMap: {
1: { label: '验证码', type: 'warning' },
2: { label: '通知', type: 'info' },
3: { label: '营销', type: 'success' }
}
}
},
computed: {
dialogTitle() {
return this.isEdit ? '编辑短信模板' : '创建短信模板'
},
detectedParams() {
const matches = this.templateForm.template_content.match(/\{(\w+)\}/g) || []
return matches.map(m => m.replace(/[{}]/g, ''))
}
},
methods: {
async fetchTemplates() {
this.loading = true
try {
const { data } = await axios.get('/api/admin/sms/templates')
this.templates = data.data
} finally {
this.loading = false
}
},
highlightParams(content) {
return content.replace(/\{(\w+)\}/g, '<span class="highlight-param">{$1}</span>')
},
async submitForm() {
const isValid = await this.$refs.templateForm.validate()
if (!isValid) return
try {
if (this.isEdit) {
await axios.put(`/api/admin/sms/templates/${this.editId}`, this.templateForm)
} else {
await axios.post('/api/admin/sms/templates', this.templateForm)
}
this.$message.success('保存成功')
this.dialogVisible = false
this.fetchTemplates()
} catch (error) {
this.$message.error(error.response?.data?.message || '保存失败')
}
},
editTemplate(template) {
this.isEdit = true
this.editId = template.id
this.templateForm = { ...template }
this.dialogVisible = true
},
testSend(template) {
this.testForm.phone = ''
this.testForm.params = this.detectedParams
this.testForm.paramValues = []
this.testTemplateCode = template.template_code
this.testDialogVisible = true
},
async doTestSend() {
const params = {}
this.testForm.params.forEach((param, index) => {
params[param] = this.testForm.paramValues[index]
})
try {
await axios.post('/api/admin/sms/test-send', {
phone: this.testForm.phone,
template_code: this.testTemplateCode,
params
})
this.$message.success('测试发送成功')
this.testDialogVisible = false
} catch (error) {
this.$message.error(error.response?.data?.message || '发送失败')
}
}
},
mounted() {
this.fetchTemplates()
}
}
</script>
<style>
.highlight-param {
color: #e6a23c;
background: #fdf6ec;
padding: 2px 4px;
border-radius: 4px;
font-weight: bold;
}
.params-tip {
margin-top: 10px;
}
</style>
配置缓存优化
缓存管理
<?php
namespace App\Services;
use App\Models\SmsTemplate;
use Illuminate\Support\Facades\Cache;
class SmsCacheService
{
/**
* 获取所有活跃模板
*/
public function getActiveTemplates()
{
return Cache::remember('sms_active_templates', 3600, function () {
return SmsTemplate::where('status', 1)
->get(['template_code', 'template_content', 'channel', 'template_type'])
->keyBy('template_code')
->toArray();
});
}
/**
* 获取单个模板
*/
public function getTemplate($code)
{
$templates = $this->getActiveTemplates();
return $templates[$code] ?? null;
}
/**
* 清除缓存
*/
public function clearCache()
{
Cache::forget('sms_active_templates');
Cache::forget('sms_config');
}
/**
* 模板变更时事件
*/
public function onTemplateChanged()
{
$this->clearCache();
// 可以记录操作日志
Log::info('短信模板缓存已清除');
}
}
安全建议
参数验证中间件
<?php
namespace App\Http\Middleware;
use Closure;
use Illuminate\Http\Request;
class ValidateSmsParams
{
public function handle(Request $request, Closure $next)
{
// 验证手机号格式
if ($request->has('phone')) {
if (!preg_match('/^1[3-9]\d{9}$/', $request->phone)) {
return response()->json([
'code' => 400,
'message' => '手机号格式不正确'
], 400);
}
}
// 验证发送频率
$lastSent = cache('sms_last_sent_' . $request->phone);
if ($lastSent && time() - $lastSent < 60) {
return response()->json([
'code' => 429,
'message' => '发送频率过快,请稍后再试'
], 429);
}
// 验证每日发送上限
$todayCount = cache('sms_daily_count_' . $request->phone, 0);
if ($todayCount >= 10) {
return response()->json([
'code' => 429,
'message' => '今日发送次数已达上限'
], 429);
}
return $next($request);
}
}
部署建议
-
环境配置
- 使用环境变量管理敏感配置
- 配置不同的短信通道备用
- 设置合理的超时和重试机制
-
监控告警
- 监控短信发送成功率
- 设置余额告警
- 记录发送失败日志
-
性能优化
- 使用Redis缓存模板
- 批量发送时使用队列
- 限制单个IP的请求频率
-
安全性
- 参数过滤和验证
- 防止模板注入
- 敏感信息脱敏
这样完整的配置管理方案可以支持灵活管理短信模板,方便运维和业务调整。
版权声明:NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播