PHP项目短信模板如何后端配置管理

wen PHP项目 30

本文目录导读:

PHP项目短信模板如何后端配置管理

  1. 数据库表设计
  2. 配置管理接口
  3. 模板渲染服务
  4. 配置管理页面
  5. 前端管理界面(Vue示例)
  6. 配置缓存优化
  7. 安全建议
  8. 部署建议

我来详细说明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);
    }
}

部署建议

  1. 环境配置

    • 使用环境变量管理敏感配置
    • 配置不同的短信通道备用
    • 设置合理的超时和重试机制
  2. 监控告警

    • 监控短信发送成功率
    • 设置余额告警
    • 记录发送失败日志
  3. 性能优化

    • 使用Redis缓存模板
    • 批量发送时使用队列
    • 限制单个IP的请求频率
  4. 安全性

    • 参数过滤和验证
    • 防止模板注入
    • 敏感信息脱敏

这样完整的配置管理方案可以支持灵活管理短信模板,方便运维和业务调整。

抱歉,评论功能暂时关闭!