本文目录导读:

我来详细说明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的请求频率
-
安全性
- 参数过滤和验证
- 防止模板注入
- 敏感信息脱敏
这样完整的配置管理方案可以支持灵活管理短信模板,方便运维和业务调整。