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
相关产品推荐
相关产品推荐

