PHP Symfony框架下客户拜访多签名采集存储的技术咨询
我之前在Symfony项目里做过几乎一模一样的客户拜访签名记录功能,分享下实际落地的方案,帮你搞定多签名采集和存储的问题:
一、前端签名采集实现
核心是用Canvas做签名画板,不用依赖第三方外链库,轻量还能适配移动端:
- 基于Canvas实现手写轨迹:监听鼠标/触摸事件,绘制用户签名轨迹,完成后通过
toDataURL('image/png')把签名转成base64字符串 - 表单里加一个隐藏字段,把base64签名值存进去,提交拜访记录时一起传给后端
二、Symfony实体设计(适配多签名场景)
因为同一客户会有多条拜访记录,每条记录对应一个签名,实体关系要这样设计:
Customer实体:存储客户基础信息(名称、联系方式等)Visit实体:通过ManyToOne关联Customer,包含拜访时间、拜访内容等字段,同时用text类型字段存储签名(或者存文件路径,后面说优化方案)
示例Visit实体代码:
// src/Entity/Visit.php namespace App\Entity; use Doctrine\ORM\Mapping as ORM; use App\Entity\Customer; #[ORM\Entity] class Visit { #[ORM\Id] #[ORM\GeneratedValue] #[ORM\Column(type: 'integer')] private $id; #[ORM\ManyToOne(targetEntity: Customer::class, inversedBy: 'visits')] #[ORM\JoinColumn(nullable: false)] private $customer; #[ORM\Column(type: 'datetime_immutable')] private $visitAt; #[ORM\Column(type: 'text', nullable: true)] private $notes; // 这里先存base64,后面可以优化成文件路径 #[ORM\Column(type: 'text', nullable: false)] private $signature; // 自动生成的getter和setter... }
三、Symfony表单集成
创建VisitType表单,把签名字段设为HiddenType,方便前端赋值:
// src/Form/VisitType.php namespace App\Form; use App\Entity\Visit; use Symfony\Component\Form\AbstractType; use Symfony\Component\Form\FormBuilderInterface; use Symfony\Component\OptionsResolver\OptionsResolver; use Symfony\Component\Form\Extension\Core\Type\HiddenType; use Symfony\Component\Form\Extension\Core\Type\TextareaType; use Symfony\Component\Form\Extension\Core\Type\DateTimeType; use Symfony\Bridge\Doctrine\Form\Type\EntityType; class VisitType extends AbstractType { public function buildForm(FormBuilderInterface $builder, array $options): void { $builder ->add('customer', EntityType::class, [ 'class' => Customer::class, 'choice_label' => 'name', // 显示客户名称 ]) ->add('visitAt', DateTimeType::class, [ 'widget' => 'single_text', 'data' => new \DateTimeImmutable(), // 默认当前时间 ]) ->add('notes', TextareaType::class) ->add('signature', HiddenType::class); } public function configureOptions(OptionsResolver $resolver): void { $resolver->setDefaults([ 'data_class' => Visit::class, ]); } }
四、签名存储优化(从base64转文件)
直接存base64会占用大量数据库空间,建议转成图片文件存到服务器,数据库只存文件路径:
在控制器里处理表单提交时,把base64转成png文件:
// src/Controller/VisitController.php namespace App\Controller; use App\Entity\Visit; use App\Form\VisitType; use Doctrine\ORM\EntityManagerInterface; use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; class VisitController extends AbstractController { public function new(Request $request, EntityManagerInterface $em): Response { $visit = new Visit(); $form = $this->createForm(VisitType::class, $visit); $form->handleRequest($request); if ($form->isSubmitted() && $form->isValid()) { $signatureBase64 = $form->get('signature')->getData(); if ($signatureBase64) { // 去掉base64前缀,解码成二进制内容 $signatureContent = base64_decode(str_replace('data:image/png;base64,', '', $signatureBase64)); // 生成唯一文件名 $fileName = uniqid('signature_', true) . '.png'; // 存到public/uploads/signatures目录(要提前创建并设置权限) $savePath = $this->getParameter('kernel.project_dir') . '/public/uploads/signatures/' . $fileName; file_put_contents($savePath, $signatureContent); // 存储相对路径,方便前端访问 $visit->setSignature('/uploads/signatures/' . $fileName); } $em->persist($visit); $em->flush(); return $this->redirectToRoute('visit_index'); } return $this->render('visit/new.html.twig', [ 'form' => $form->createView(), ]); } }
五、前端模板整合签名画板
在Twig模板里把Canvas画板和表单绑定,实现实时签名和清除功能:
{# templates/visit/new.html.twig #} {% extends 'base.html.twig' %} {% block body %} <h1>记录客户拜访</h1> {{ form_start(form) }} {{ form_row(form.customer) }} {{ form_row(form.visitAt) }} {{ form_row(form.notes) }} <div class="signature-section"> <h3>请客户在此签名</h3> <canvas id="signatureCanvas" width="400" height="200" style="border:1px solid #ddd;"></canvas> <div class="mt-2"> <button type="button" id="clearBtn" class="btn btn-secondary">清除签名</button> </div> {{ form_widget(form.signature) }} </div> <button type="submit" class="btn btn-primary mt-4">保存拜访记录</button> {{ form_end(form) }} <script> const canvas = document.getElementById('signatureCanvas'); const ctx = canvas.getContext('2d'); const signatureInput = document.getElementById('{{ form.signature.vars.id }}'); let isDrawing = false; // 鼠标事件处理 canvas.addEventListener('mousedown', startDraw); canvas.addEventListener('mousemove', draw); canvas.addEventListener('mouseup', stopDraw); canvas.addEventListener('mouseout', stopDraw); // 触摸事件适配移动端 canvas.addEventListener('touchstart', (e) => { e.preventDefault(); const touch = e.touches[0]; const mouseEvent = new MouseEvent('mousedown', { clientX: touch.clientX, clientY: touch.clientY }); canvas.dispatchEvent(mouseEvent); }); function startDraw(e) { isDrawing = true; ctx.beginPath(); ctx.moveTo(e.offsetX, e.offsetY); ctx.lineWidth = 2; ctx.strokeStyle = '#000'; } function draw(e) { if (!isDrawing) return; ctx.lineTo(e.offsetX, e.offsetY); ctx.stroke(); } function stopDraw() { isDrawing = false; // 把签名转成base64存到隐藏字段 signatureInput.value = canvas.toDataURL('image/png'); } document.getElementById('clearBtn').addEventListener('click', () => { ctx.clearRect(0, 0, canvas.width, canvas.height); signatureInput.value = ''; }); </script> {% endblock %}
内容的提问来源于stack exchange,提问作者Mgulizia
相关产品推荐
相关产品推荐

