You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

LEMP票务系统中如何区分下拉选框选中项与手动输入数据?

实现基于LEMP的票务系统电话号码联想填充功能

1. 数据库查询(INNER JOIN关联多表)

通过INNER JOIN关联Customers、PhoneNumbers、CreditCards三张表,获取联想所需的完整数据。使用参数化查询避免SQL注入,同时支持模糊匹配实现type-ahead:

SELECT 
    c.CustomerID,
    c.FullName,
    p.PhoneNumber,
    cc.CardType,
    RIGHT(cc.CardNumber, 4) AS LastFourDigits,
    cc.ExpirationDate
FROM Customers c
INNER JOIN PhoneNumbers p ON c.CustomerID = p.CustomerID
INNER JOIN CreditCards cc ON c.CustomerID = cc.CustomerID
WHERE p.PhoneNumber LIKE CONCAT('%', :phone, '%')
LIMIT 10;

该查询会返回所有匹配电话号码的客户记录,包括同一客户关联多张信用卡的情况(对应场景A中Tom Smith的两条记录)。

2. 前端实现type-ahead与自动填充

HTML结构

搭建包含电话号码输入框、datalist及表单字段的基础结构:

<form id="ticket-form" method="POST" action="/submit-ticket">
  <div>
    <label for="phone">电话号码:</label>
    <input type="tel" id="phone" list="phone-suggestions" autocomplete="off" name="phone">
    <datalist id="phone-suggestions"></datalist>
    <!-- 隐藏字段存储CustomerID,用于场景A判断 -->
    <input type="hidden" id="customer_id" name="customer_id">
  </div>

  <div>
    <label for="fullname">客户姓名:</label>
    <input type="text" id="fullname" name="fullname">
  </div>

  <div>
    <label for="card-type">信用卡类型:</label>
    <input type="text" id="card-type" name="card_type">
  </div>

  <div>
    <label for="card-number">完整卡号:</label>
    <input type="text" id="card-number" name="card_number">
  </div>

  <div>
    <label for="exp-date">有效期(MM/YY):</label>
    <input type="text" id="exp-date" name="exp_date">
  </div>

  <div>
    <label for="event">活动选择:</label>
    <select id="event" name="event_id">
      <option value="1">春季音乐节</option>
      <option value="2">夏季戏剧展</option>
    </select>
  </div>

  <button type="submit" name="submit-ticket">提交订单</button>
</form>

JavaScript逻辑

监听电话号码输入事件,触发AJAX请求获取联想数据,选中选项后自动填充表单并切换焦点:

const phoneInput = document.getElementById('phone');
const datalist = document.getElementById('phone-suggestions');
const customerIdInput = document.getElementById('customer_id');
const formFields = {
  fullname: document.getElementById('fullname'),
  cardType: document.getElementById('card-type'),
  cardNumber: document.getElementById('card-number'),
  expDate: document.getElementById('exp-date'),
  event: document.getElementById('event')
};

// 输入到第7位时触发联想
phoneInput.addEventListener('input', async () => {
  const cleanPhone = phoneInput.value.replace(/\D/g, '');
  if (cleanPhone.length < 7) {
    datalist.innerHTML = '';
    return;
  }

  try {
    const res = await fetch('/api/get-customer-suggestions', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ phone: cleanPhone })
    });
    const suggestions = await res.json();

    datalist.innerHTML = '';
    suggestions.forEach(item => {
      const option = document.createElement('option');
      option.value = item.PhoneNumber;
      // 显示清晰的提示文本,包含客户、信用卡信息
      option.textContent = `${item.PhoneNumber} | ${item.FullName} | ${item.CardType} ****${item.LastFourDigits} | 有效期: ${item.ExpirationDate}`;
      // 存储关联数据到data属性
      option.dataset.customerId = item.CustomerID;
      option.dataset.fullname = item.FullName;
      option.dataset.cardType = item.CardType;
      option.dataset.lastFour = item.LastFourDigits;
      option.dataset.expDate = item.ExpirationDate;
      datalist.appendChild(option);
    });
  } catch (err) {
    console.error('获取联想数据失败:', err);
  }
});

// 选中选项后自动填充表单
phoneInput.addEventListener('change', () => {
  const selectedOpt = [...datalist.options].find(opt => opt.value === phoneInput.value);
  if (!selectedOpt) return;

  customerIdInput.value = selectedOpt.dataset.customerId;
  formFields.fullname.value = selectedOpt.dataset.fullname;
  formFields.cardType.value = selectedOpt.dataset.cardType;
  // 仅显示后4位,设置为只读
  formFields.cardNumber.value = `****${selectedOpt.dataset.lastFour}`;
  formFields.cardNumber.setAttribute('readonly', true);
  formFields.expDate.value = selectedOpt.dataset.expDate;
  // 自动切换焦点到活动选择框
  formFields.event.focus();
});

// 用户手动修改姓名时,切换到场景B逻辑
formFields.fullname.addEventListener('input', () => {
  customerIdInput.value = '';
  formFields.cardNumber.removeAttribute('readonly');
});

3. 后端接口与表单提交处理

联想数据接口(PHP)

处理前端AJAX请求,执行数据库查询并返回JSON数据:

<?php
// 初始化PDO连接(根据实际配置修改)
$pdo = new PDO(
  'mysql:host=localhost;dbname=ticket_system;charset=utf8mb4',
  'db_user',
  'db_pass',
  [PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION]
);

if ($_SERVER['REQUEST_METHOD'] !== 'POST') exit;

$input = json_decode(file_get_contents('php://input'), true);
$phone = $input['phone'] ?? '';

$stmt = $pdo->prepare("
  SELECT 
    c.CustomerID,
    c.FullName,
    p.PhoneNumber,
    cc.CardType,
    RIGHT(cc.CardNumber, 4) AS LastFourDigits,
    cc.ExpirationDate
  FROM Customers c
  INNER JOIN PhoneNumbers p ON c.CustomerID = p.CustomerID
  INNER JOIN CreditCards cc ON c.CustomerID = cc.CustomerID
  WHERE p.PhoneNumber LIKE CONCAT('%', :phone, '%')
  LIMIT 10;
");
$stmt->bindParam(':phone', $phone, PDO::PARAM_STR);
$stmt->execute();

header('Content-Type: application/json');
echo json_encode($stmt->fetchAll(PDO::FETCH_ASSOC));
?>

表单提交处理(PHP)

区分场景A和场景B,使用事务保证场景B的数据一致性:

<?php
// 初始化PDO连接
$pdo = new PDO(
  'mysql:host=localhost;dbname=ticket_system;charset=utf8mb4',
  'db_user',
  'db_pass',
  [PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION]
);

if ($_SERVER['REQUEST_METHOD'] !== 'POST' || !isset($_POST['submit-ticket'])) exit;

$customerId = $_POST['customer_id'] ?? '';
$eventId = $_POST['event_id'] ?? '';

try {
  if (!empty($customerId)) {
    // 场景A:仅插入票务记录
    $stmt = $pdo->prepare("
      INSERT INTO Tickets (CustomerID, EventID, CreatedAt)
      VALUES (:customer_id, :event_id, NOW())
    ");
    $stmt->bindParam(':customer_id', $customerId, PDO::PARAM_INT);
    $stmt->bindParam(':event_id', $eventId, PDO::PARAM_INT);
    $stmt->execute();
    echo '订单提交成功(场景A)';
  } else {
    // 场景B:插入客户、电话、信用卡、票务记录,开启事务
    $pdo->beginTransaction();

    // 插入客户
    $stmt = $pdo->prepare("
      INSERT INTO Customers (FullName, CreatedAt)
      VALUES (:fullname, NOW())
    ");
    $stmt->bindParam(':fullname', $_POST['fullname'], PDO::PARAM_STR);
    $stmt->execute();
    $newCustomerId = $pdo->lastInsertId();

    // 插入电话号码
    $stmt = $pdo->prepare("
      INSERT INTO PhoneNumbers (CustomerID, PhoneNumber)
      VALUES (:customer_id, :phone)
    ");
    $stmt->bindParam(':customer_id', $newCustomerId, PDO::PARAM_INT);
    $stmt->bindParam(':phone', $_POST['phone'], PDO::PARAM_STR);
    $stmt->execute();

    // 插入信用卡(生产环境需加密存储卡号)
    $stmt = $pdo->prepare("
      INSERT INTO CreditCards (CustomerID, CardType, CardNumber, ExpirationDate)
      VALUES (:customer_id, :card_type, :card_number, :exp_date)
    ");
    $stmt->bindParam(':customer_id', $newCustomerId, PDO::PARAM_INT);
    $stmt->bindParam(':card_type', $_POST['card_type'], PDO::PARAM_STR);
    $stmt->bindParam(':card_number', $_POST['card_number'], PDO::PARAM_STR);
    $stmt->bindParam(':exp_date', $_POST['exp_date'], PDO::PARAM_STR);
    $stmt->execute();

    // 插入票务记录
    $stmt = $pdo->prepare("
      INSERT INTO Tickets (CustomerID, EventID, CreatedAt)
      VALUES (:customer_id, :event_id, NOW())
    ");
    $stmt->bindParam(':customer_id', $newCustomerId, PDO::PARAM_INT);
    $stmt->bindParam(':event_id', $eventId, PDO::PARAM_INT);
    $stmt->execute();

    $pdo->commit();
    echo '订单提交成功(场景B)';
  }
} catch (Exception $e) {
  if ($pdo->inTransaction()) $pdo->rollBack();
  echo '提交失败:' . $e->getMessage();
}
?>

关键注意事项

  • 信用卡安全:绝对不能明文存储完整信用卡号,建议使用AES加密或第三方支付服务的令牌存储。
  • 数据一致性:场景B必须使用数据库事务,避免部分数据插入失败导致的脏数据。
  • 用户体验:联想列表的提示文本要简洁清晰,让操作员能快速区分同一客户的不同信用卡。
  • 输入验证:前端和后端都要添加输入验证(比如电话号码格式、有效期格式),避免无效数据入库。

内容的提问来源于stack exchange,提问作者Octavio Masomenos

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.08 22:05:05