怎样在PHP项目中实现用户自定义签名?

wen java案例 4

本文目录导读:

怎样在PHP项目中实现用户自定义签名?

  1. 功能设计思路
  2. 数据库设计
  3. 后端PHP接口实现
  4. 前端HTML/CSS/JS实现
  5. 安全与优化建议
  6. 扩展功能

在PHP项目中实现用户自定义签名功能,通常涉及前端编辑、后端存储和前端展示三个环节,以下是一个完整的实现方案,涵盖常见需求(如文本签名、手写签名、图片上传签名)。


功能设计思路

功能点 技术方案
文本签名 用户输入文本,存储为字符串
手写签名 使用Canvas绘制,生成Base64图片或Blob
图片上传签名 上传图片文件,存储路径
签名存储 数据库字段存储JSON或图片路径
签名展示 根据类型动态渲染

数据库设计

CREATE TABLE user_signatures (
    id INT PRIMARY KEY AUTO_INCREMENT,
    user_id INT NOT NULL,
    signature_type ENUM('text', 'draw', 'image') DEFAULT 'text',
    signature_data TEXT,  -- 文本内容或Base64或图片路径
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
    FOREIGN KEY (user_id) REFERENCES users(id)
);

字段说明

  • signature_type:签名类型
  • signature_data:根据类型存储不同数据(文本签名存字符串,手写签名存Base64或路径,图片签名存路径)

后端PHP接口实现

1 保存签名(文本/手写/图片)

<?php
// save_signature.php
session_start();
require_once 'db.php'; // 数据库连接
$user_id = $_SESSION['user_id'];
$type = $_POST['type'] ?? 'text';
$data = $_POST['data'] ?? '';
if (!$user_id) {
    die(json_encode(['error' => '用户未登录']));
}
// 1. 文本签名
if ($type === 'text') {
    $data = htmlspecialchars(trim($data), ENT_QUOTES, 'UTF-8');
    if (mb_strlen($data) > 50) {
        die(json_encode(['error' => '签名文本过长']));
    }
}
// 2. 手写签名(Base64图片)
elseif ($type === 'draw') {
    // 可选:将Base64保存为文件,减少数据库压力
    $data = save_base64_image($data, $user_id);
}
// 3. 图片上传签名
elseif ($type === 'image') {
    if (!isset($_FILES['file'])) {
        die(json_encode(['error' => '未上传文件']));
    }
    $data = handle_upload($_FILES['file'], $user_id);
}
// 存储或更新签名
$stmt = $db->prepare("
    INSERT INTO user_signatures (user_id, signature_type, signature_data)
    VALUES (?, ?, ?)
    ON DUPLICATE KEY UPDATE signature_type=VALUES(signature_type), signature_data=VALUES(signature_data)
");
$stmt->execute([$user_id, $type, $data]);
echo json_encode(['success' => true, 'data' => $data]);

2 辅助函数

// 保存Base64图片为文件
function save_base64_image($base64_string, $user_id) {
    // 去除前缀 data:image/png;base64,
    $image_parts = explode(";base64,", $base64_string);
    if (count($image_parts) != 2) return false;
    $image_base64 = base64_decode($image_parts[1]);
    $filename = "signatures/user_{$user_id}_" . time() . ".png";
    $filepath = __DIR__ . "/uploads/" . $filename;
    if (!file_exists(dirname($filepath))) {
        mkdir(dirname($filepath), 0755, true);
    }
    file_put_contents($filepath, $image_base64);
    return $filename;
}
// 处理上传的签名图片
function handle_upload($file, $user_id) {
    $allowed_types = ['image/png', 'image/jpeg', 'image/gif'];
    if (!in_array($file['type'], $allowed_types)) {
        die(json_encode(['error' => '仅支持PNG/JPG/GIF图片']));
    }
    $ext = pathinfo($file['name'], PATHINFO_EXTENSION);
    $filename = "signatures/user_{$user_id}_" . time() . "." . $ext;
    $filepath = __DIR__ . "/uploads/" . $filename;
    if (!move_uploaded_file($file['tmp_name'], $filepath)) {
        die(json_encode(['error' => '文件上传失败']));
    }
    return $filename;
}

前端HTML/CSS/JS实现

1 签名选择与编辑界面

<div class="signature-container">
    <h3>设置签名</h3>
    <!-- 签名类型切换 -->
    <div class="type-selector">
        <label><input type="radio" name="sig_type" value="text" checked> 文字签名</label>
        <label><input type="radio" name="sig_type" value="draw"> 手写签名</label>
        <label><input type="radio" name="sig_type" value="image"> 上传图片</label>
    </div>
    <!-- 文本签名编辑 -->
    <div id="text-signature" class="sig-edit-area">
        <input type="text" id="sig-text" placeholder="输入签名文字" maxlength="50">
        <button onclick="previewTextSig()">预览</button>
    </div>
    <!-- 手写签名画布 -->
    <div id="draw-signature" class="sig-edit-area" style="display:none;">
        <canvas id="sig-canvas" width="300" height="150" style="border:1px solid #ccc;"></canvas>
        <div>
            <button onclick="clearCanvas()">清除</button>
            <button onclick="saveDrawSig()">保存手写签名</button>
        </div>
    </div>
    <!-- 图片上传 -->
    <div id="image-signature" class="sig-edit-area" style="display:none;">
        <input type="file" id="sig-image" accept="image/png,image/jpeg,image/gif">
    </div>
    <!-- 签名预览 -->
    <div id="signature-preview" class="sig-preview">
        <h4>当前签名</h4>
        <div id="sig-display"></div>
    </div>
    <button onclick="saveSignature()">保存签名设置</button>
</div>

2 JavaScript核心逻辑

// 签名类型切换
document.querySelectorAll('input[name="sig_type"]').forEach(radio => {
    radio.addEventListener('change', function() {
        document.querySelectorAll('.sig-edit-area').forEach(el => el.style.display = 'none');
        document.getElementById(this.value + '-signature').style.display = 'block';
    });
});
// ----- 手写签名Canvas -----
const canvas = document.getElementById('sig-canvas');
const ctx = canvas.getContext('2d');
let drawing = false;
canvas.addEventListener('mousedown', startDraw);
canvas.addEventListener('mousemove', draw);
canvas.addEventListener('mouseup', stopDraw);
canvas.addEventListener('mouseout', stopDraw);
// 触屏支持
canvas.addEventListener('touchstart', e => { e.preventDefault(); startDraw(e); });
canvas.addEventListener('touchmove', e => { e.preventDefault(); draw(e); });
canvas.addEventListener('touchend', stopDraw);
function startDraw(e) {
    drawing = true;
    ctx.beginPath();
    const pos = getPos(e);
    ctx.moveTo(pos.x, pos.y);
}
function draw(e) {
    if (!drawing) return;
    const pos = getPos(e);
    ctx.lineWidth = 3;
    ctx.lineCap = 'round';
    ctx.strokeStyle = '#000';
    ctx.lineTo(pos.x, pos.y);
    ctx.stroke();
}
function stopDraw() { drawing = false; }
function getPos(e) {
    const rect = canvas.getBoundingClientRect();
    const clientX = e.touches ? e.touches[0].clientX : e.clientX;
    const clientY = e.touches ? e.touches[0].clientY : e.clientY;
    return { x: clientX - rect.left, y: clientY - rect.top };
}
function clearCanvas() {
    ctx.clearRect(0, 0, canvas.width, canvas.height);
}
// ----- 保存签名到后端 -----
function saveSignature() {
    const type = document.querySelector('input[name="sig_type"]:checked').value;
    let data = '';
    if (type === 'text') {
        data = document.getElementById('sig-text').value;
        if (!data.trim()) { alert('请输入签名文本'); return; }
    } else if (type === 'draw') {
        if (isCanvasBlank(canvas)) { alert('请先绘制签名'); return; }
        data = canvas.toDataURL('image/png'); // Base64
    } else if (type === 'image') {
        const fileInput = document.getElementById('sig-image');
        if (!fileInput.files[0]) { alert('请选择签名图片'); return; }
        // 使用FormData上传
        const formData = new FormData();
        formData.append('file', fileInput.files[0]);
        formData.append('type', 'image');
        formData.append('data', '');
        uploadViaFormData(formData);
        return;
    }
    // 文本或手写签名使用JSON提交
    fetch('save_signature.php', {
        method: 'POST',
        headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
        body: `type=${type}&data=${encodeURIComponent(data)}`
    })
    .then(res => res.json())
    .then(result => {
        if (result.success) {
            alert('签名保存成功');
            updatePreview(type, data);
        } else {
            alert('保存失败:' + result.error);
        }
    });
}
function uploadViaFormData(formData) {
    formData.append('type', 'image');
    formData.append('data', '');
    fetch('save_signature.php', { method: 'POST', body: formData })
    .then(res => res.json())
    .then(result => {
        if (result.success) {
            alert('签名图片上传成功');
            updatePreview('image', result.data);
        } else {
            alert('上传失败:' + result.error);
        }
    });
}
function isCanvasBlank(canvas) {
    const pixelData = canvas.getContext('2d').getImageData(0, 0, canvas.width, canvas.height).data;
    return !pixelData.some(channel => channel !== 0);
}
// ----- 预览签名 -----
function updatePreview(type, data) {
    const display = document.getElementById('sig-display');
    if (type === 'text') {
        display.innerHTML = `<span style="font-family: '华文行楷', cursive; font-size: 24px;">${data}</span>`;
    } else if (type === 'draw' || type === 'image') {
        display.innerHTML = `<img src="${type === 'draw' ? data : 'uploads/' + data}" style="max-height:100px;">`;
    }
}
// 从数据库加载已有签名(页面初始化时调用)
function loadExistingSignature() {
    fetch('get_signature.php')
    .then(res => res.json())
    .then(data => {
        if (data && data.signature_data) {
            document.querySelector(`input[value="${data.signature_type}"]`).checked = true;
            // 触发切换显示
            document.querySelector(`input[value="${data.signature_type}"]`).dispatchEvent(new Event('change'));
            updatePreview(data.signature_type, data.signature_data);
        }
    });
}
loadExistingSignature();

安全与优化建议

  1. 输入验证:文本签名限制长度,过滤XSS(如 htmlspecialchars
  2. 图片处理
    • 验证文件类型(MIME + 扩展名)
    • 限制文件大小(建议最大2MB)
    • 使用 getimagesize() 确认图片合法性
    • 对上传图片重命名防路径穿越
  3. 存储优化
    • 手写签名Base64数据较长(约10-50KB),建议存为文件而非数据库
    • 可定期清理未使用的签名图片
  4. 权限控制:确保用户只能修改自己的签名
  5. 图片缩放:前端Canvas可设置最大尺寸,避免生成超大图片

扩展功能

  • 签名水印:在手写签名Canvas上叠加用户名、日期
  • 字体选择器:文本签名支持多种字体(引入Google Fonts)
  • 坐标存储:手写签名存储为坐标数组,可无损缩放和重绘
  • 签名验证:在重要操作前要求用户重新绘制签名确认身份

这个方案覆盖了主流的用户签名需求,你可以根据项目复杂度选择实现全部或部分功能。

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