coupon/code_manage.php

373 lines
16 KiB
PHP
Executable File
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<?php
require_once __DIR__ . '/includes/auth.php';
require_once __DIR__ . '/includes/functions.php';
requireAdmin();
$pageTitle = '库存管理';
require __DIR__ . '/includes/header.php';
$msg = '';
$pid = getCurrentProductId();
// Excel导入 (CSV格式)
if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['action']) && $_POST['action'] === 'import') {
if (!verifyCsrf($_POST['csrf_token'] ?? '')) { die('CSRF token无效'); }
if (isset($_FILES['file']) && $_FILES['file']['error'] === UPLOAD_ERR_OK) {
$ext = strtolower(pathinfo($_FILES['file']['name'], PATHINFO_EXTENSION));
if (!in_array($ext, ['csv', 'xls', 'xlsx'])) {
$msg = '<div class="alert alert-danger">仅支持 CSV、XLS、XLSX 格式</div>';
} else {
$imported = 0;
$errors = [];
if ($ext === 'csv') {
$handle = fopen($_FILES['file']['tmp_name'], 'r');
if ($handle) {
$lineNo = 0;
while (($row = fgetcsv($handle)) !== false) {
$lineNo++;
if ($lineNo === 1) continue; // 跳过表头
if (count($row) < 5) {
$errors[] = "{$lineNo}行: 列数不足至少需要5列";
continue;
}
$name = trim($row[0]);
$batchNo = trim($row[1]);
$type = (int)trim($row[2]);
$code = trim($row[3]);
$value = trim($row[4]);
$expiredAt = isset($row[5]) ? trim($row[5]) : '';
$price1 = isset($row[6]) && $row[6] !== '' ? (float)trim($row[6]) : null;
$price2 = isset($row[7]) && $row[7] !== '' ? (float)trim($row[7]) : null;
if (!$name || !$batchNo || !$code) {
$errors[] = "{$lineNo}行: 兑换码名称、批次号、兑换码为必填项";
continue;
}
if ($type !== 1) {
$errors[] = "{$lineNo}行: 兑换码类型必须为1当前为{$type}";
continue;
}
$stmt = $pdo->prepare('INSERT INTO redemption_codes (product_id, created_at, user_id, name, batch_no, type, code, value, expired_at, price_tier1, price_tier2) VALUES (?, NOW(), ?, ?, ?, ?, ?, ?, ?, ?, ?) ON DUPLICATE KEY UPDATE name = VALUES(name), batch_no = VALUES(batch_no), type = VALUES(type), value = VALUES(value), expired_at = VALUES(expired_at), price_tier1 = VALUES(price_tier1), price_tier2 = VALUES(price_tier2)');
$stmt->execute([$pid, getCurrentUserId(), $name, $batchNo, $type, $code, $value, $expiredAt ?: null, $price1, $price2]);
$imported++;
}
fclose($handle);
}
} elseif ($ext === 'xlsx') {
$rows = readXlsx($_FILES['file']['tmp_name']);
if ($rows === false) {
$errors[] = '无法解析 XLSX 文件,请确认文件未损坏';
} else {
foreach ($rows as $lineNo => $row) {
if ($lineNo === 0) continue; // 跳过表头
$ln = $lineNo + 1;
if (count($row) < 5) {
$errors[] = "{$ln}行: 列数不足至少需要5列";
continue;
}
$name = trim($row[0] ?? '');
$batchNo = trim($row[1] ?? '');
$type = (int)($row[2] ?? 0);
$code = trim($row[3] ?? '');
$value = trim($row[4] ?? '');
$expiredAt = isset($row[5]) ? trim($row[5]) : '';
$price1 = isset($row[6]) && $row[6] !== '' ? (float)$row[6] : null;
$price2 = isset($row[7]) && $row[7] !== '' ? (float)$row[7] : null;
if (!$name || !$batchNo || !$code) {
$errors[] = "{$ln}行: 兑换码名称、批次号、兑换码为必填项";
continue;
}
if ($type !== 1) {
$errors[] = "{$ln}行: 兑换码类型必须为1当前为{$type}";
continue;
}
$stmt = $pdo->prepare('INSERT INTO redemption_codes (product_id, created_at, user_id, name, batch_no, type, code, value, expired_at, price_tier1, price_tier2) VALUES (?, NOW(), ?, ?, ?, ?, ?, ?, ?, ?, ?) ON DUPLICATE KEY UPDATE name = VALUES(name), batch_no = VALUES(batch_no), type = VALUES(type), value = VALUES(value), expired_at = VALUES(expired_at), price_tier1 = VALUES(price_tier1), price_tier2 = VALUES(price_tier2)');
$stmt->execute([$pid, getCurrentUserId(), $name, $batchNo, $type, $code, $value, $expiredAt ?: null, $price1, $price2]);
$imported++;
}
}
} else {
$errors[] = 'XLS旧版Excel格式不支持请另存为 XLSX 或 CSV 格式后导入';
}
$flashMsg = '';
if ($imported > 0) {
$flashMsg = '成功导入 ' . $imported . ' 条记录';
if (!empty($errors)) $flashMsg .= '' . count($errors) . ' 条失败';
}
if (!empty($errors) && $imported > 0) {
$flashMsg .= "\n" . implode("\n", array_slice($errors, 0, 20));
$_SESSION['flash_msg'] = $flashMsg;
$_SESSION['flash_type'] = 'success';
header('Location: code_manage.php');
exit;
} elseif ($imported > 0) {
$_SESSION['flash_msg'] = $flashMsg;
$_SESSION['flash_type'] = 'success';
header('Location: code_manage.php');
exit;
} elseif (!empty($errors)) {
$msg = '<div class="alert alert-warning"><pre>' . h(implode("\n", array_slice($errors, 0, 20))) . '</pre></div>';
}
}
} else {
$msg = '<div class="alert alert-danger">请选择文件</div>';
}
}
// 删除单个兑换码
if (isset($_GET['del']) && is_numeric($_GET['del'])) {
if (!verifyCsrf($_GET['csrf_token'] ?? '')) { $_SESSION['flash_msg'] = 'CSRF token无效'; $_SESSION['flash_type'] = 'danger'; header('Location: code_manage.php'); exit; }
$id = (int)$_GET['del'];
$pdo->prepare('DELETE FROM redemption_codes WHERE id = ?')->execute([$id]);
$_SESSION['flash_msg'] = '兑换码已删除';
$_SESSION['flash_type'] = 'success';
header('Location: code_manage.php');
exit;
}
// 搜索条件
$where = 'WHERE c.product_id = ?';
$searchParams = [$pid];
$joinUser = false;
if (!empty($_GET['claim_user'])) {
$where .= ' AND u.username LIKE ?';
$searchParams[] = '%' . $_GET['claim_user'] . '%';
$joinUser = true;
}
foreach (['code' => 'c.code', 'name' => 'c.name', 'batch_no' => 'c.batch_no'] as $key => $col) {
if (!empty($_GET[$key])) {
$where .= " AND $col LIKE ?";
$searchParams[] = '%' . $_GET[$key] . '%';
}
}
// 分页
$page = max(1, (int)($_GET['p'] ?? 1));
$perPage = 20;
$offset = ($page - 1) * $perPage;
$join = 'LEFT JOIN users u ON c.claim_user_id = u.id';
$stmt = $pdo->prepare("SELECT COUNT(*) FROM redemption_codes c $join $where");
$stmt->execute($searchParams);
$total = (int)$stmt->fetchColumn();
$totalPages = max(1, ceil($total / $perPage));
$stmt = $pdo->prepare("SELECT c.*, u.username AS claim_username FROM redemption_codes c $join $where ORDER BY c.id DESC LIMIT ? OFFSET ?");
$stmt->execute(array_merge($searchParams, [$perPage, $offset]));
$codes = $stmt->fetchAll();
?>
<div class="card">
<h2>库存管理</h2>
<?= $msg ?>
<?php if (isset($_SESSION['flash_msg'])): ?>
<div class="alert alert-<?= h($_SESSION['flash_type'] ?? 'success') ?>"><?= h($_SESSION['flash_msg']) ?></div>
<?php unset($_SESSION['flash_msg'], $_SESSION['flash_type']); ?>
<?php endif; ?>
<form method="get" style="display:flex;flex-wrap:wrap;gap:8px;margin-bottom:12px;align-items:flex-end;">
<div class="form-group" style="margin-bottom:0;">
<label style="font-size:12px;">用户</label>
<input type="text" name="claim_user" class="form-control" value="<?= h($_GET['claim_user'] ?? '') ?>" placeholder="用户名" style="width:110px;">
</div>
<div class="form-group" style="margin-bottom:0;">
<label style="font-size:12px;">兑换码</label>
<input type="text" name="code" class="form-control" value="<?= h($_GET['code'] ?? '') ?>" style="width:130px;">
</div>
<div class="form-group" style="margin-bottom:0;">
<label style="font-size:12px;">名称</label>
<input type="text" name="name" class="form-control" value="<?= h($_GET['name'] ?? '') ?>" style="width:100px;">
</div>
<div class="form-group" style="margin-bottom:0;">
<label style="font-size:12px;">批次号</label>
<input type="text" name="batch_no" class="form-control" value="<?= h($_GET['batch_no'] ?? '') ?>" style="width:120px;">
</div>
<button type="submit" class="btn btn-primary btn-sm">搜索</button>
<?php if (!empty($_GET['claim_user']) || !empty($_GET['code']) || !empty($_GET['name']) || !empty($_GET['batch_no'])): ?>
<a href="code_manage.php" class="btn btn-sm" style="background:#999;color:#fff;">清空</a>
<?php endif; ?>
</form>
<div style="margin-bottom:16px;">
<a href="javascript:void(0)" class="btn btn-success" onclick="showModal('importModal')">+ Excel导入</a>
<a href="export_template.php" class="btn btn-info">下载导入模板</a>
</div>
<div class="table-wrapper">
<table>
<thead>
<tr>
<th>ID</th>
<th>创建时间</th>
<th>名称</th>
<th>批次号</th>
<th>类型</th>
<th>兑换码</th>
<th>面值</th>
<th>状态</th>
<th>过期时间</th>
<th>用户</th>
<th>领取时间</th>
<th>出货价一档</th>
<th>出货价二挡</th>
<th>操作</th>
</tr>
</thead>
<tbody>
<?php foreach ($codes as $c): ?>
<tr>
<td><?= $c['id'] ?></td>
<td><?= formatDateTime($c['created_at']) ?></td>
<td><?= h($c['name']) ?></td>
<td><?= h($c['batch_no']) ?></td>
<td><?= h($c['type']) ?></td>
<td><?= h($c['code']) ?></td>
<td><?= h($c['value']) ?></td>
<td><?= claimStatusBadge((int)$c['status']) ?></td>
<td><?= formatDateTime($c['expired_at']) ?></td>
<td><?= h($c['claim_username'] ?? ($c['claim_user_id'] ? 'ID:' . $c['claim_user_id'] : '-')) ?></td>
<td><?= formatDateTime($c['claimed_at']) ?></td>
<td><?= $c['price_tier1'] !== null ? h($c['price_tier1']) : '-' ?></td>
<td><?= $c['price_tier2'] !== null ? h($c['price_tier2']) : '-' ?></td>
<td><a href="?del=<?= $c['id'] ?>&csrf_token=<?= csrfToken() ?>" class="btn btn-danger btn-sm" onclick="return confirm('确定删除此兑换码?')">删除</a></td>
</tr>
<?php endforeach; ?>
<?php if (empty($codes)): ?>
<tr><td colspan="14" style="text-align:center;color:#999;">暂无数据</td></tr>
<?php endif; ?>
</tbody>
</table>
</div>
<?php
$extra = [];
foreach (['claim_user', 'code', 'name', 'batch_no'] as $k) {
if (!empty($_GET[$k])) $extra[$k] = $_GET[$k];
}
renderPagination($page, $totalPages, $extra);
?>
</div>
<!-- 导入弹窗 -->
<div class="modal" id="importModal">
<div class="modal-content">
<span class="modal-close" onclick="hideModal('importModal')">&times;</span>
<h3>Excel导入兑换码</h3>
<div class="alert alert-info">
<strong>导入说明:</strong><br>
1. 支持 CSV 和 XLSX 格式<br>
2. 文件必须包含表头,列顺序为:<br>
<code>兑换码名称, 批次号, 类型, 兑换码, 面值, 过期时间, 出货价一档, 出货价二挡</code><br>
3. 类型必须为 1<br>
4. 过期时间格式2027-06-01 12:26:53可选<br>
5. 出货价字段可选<br>
6. 推荐先<a href="export_template.php">下载模板</a>
</div>
<form method="post" enctype="multipart/form-data">
<input type="hidden" name="action" value="import">
<input type="hidden" name="csrf_token" value="<?= csrfToken() ?>">
<div class="form-group">
<label>选择文件</label>
<input type="file" name="file" class="form-control-file" accept=".csv,.xls,.xlsx" required>
</div>
<button type="submit" class="btn btn-success">开始导入</button>
</form>
</div>
</div>
<script>
function showModal(id) { document.getElementById(id).classList.add('active'); }
function hideModal(id) { document.getElementById(id).classList.remove('active'); }
document.querySelectorAll('.modal').forEach(m => {
m.addEventListener('click', function(e) { if (e.target === this) this.classList.remove('active'); });
});
</script>
<?php
/**
* 使用内置 ZipArchive + SimpleXML 解析 XLSX 文件,无需第三方库
*/
function readXlsx($file) {
$zip = new ZipArchive;
if ($zip->open($file) !== true) return false;
// 读取共享字符串表
$sharedStrings = [];
$ssXml = $zip->getFromName('xl/sharedStrings.xml');
if ($ssXml !== false) {
$ss = simplexml_load_string($ssXml);
if ($ss) {
foreach ($ss->si as $si) {
$t = $si->t;
if ($t) {
$sharedStrings[] = (string)$t;
} else {
// 富文本,拼接所有 t 子元素
$parts = [];
foreach ($si->r->t as $rT) {
$parts[] = (string)$rT;
}
$sharedStrings[] = implode('', $parts);
}
}
}
}
// 读取第一个工作表
$sheetXml = $zip->getFromName('xl/worksheets/sheet1.xml');
$zip->close();
if ($sheetXml === false) return false;
$sheet = simplexml_load_string($sheetXml);
if (!$sheet) return false;
$ns = $sheet->getNamespaces(true);
$sheet->registerXPathNamespace('s', $ns[''] ?? '');
$rows = [];
$maxCol = 0;
$cells = $sheet->xpath('//s:sheetData/s:row/s:c') ?: [];
// 第一遍:收集所有数据
$data = [];
foreach ($cells as $c) {
$ref = (string)$c['r'];
if (!preg_match('/^([A-Z]+)(\d+)$/', $ref, $m)) continue;
$colStr = $m[1];
$rowNum = (int)$m[2];
// 列字母转数字
$colNum = 0;
for ($i = 0; $i < strlen($colStr); $i++) {
$colNum = $colNum * 26 + (ord($colStr[$i]) - 64);
}
$type = (string)$c['t'];
$value = (string)$c->v;
if ($type === 's') {
$idx = (int)$value;
$cellValue = $sharedStrings[$idx] ?? '';
} else {
$cellValue = $value;
}
if (!isset($data[$rowNum])) $data[$rowNum] = [];
$data[$rowNum][$colNum] = $cellValue;
if ($colNum > $maxCol) $maxCol = $colNum;
}
// 按行排序输出
ksort($data);
foreach ($data as $rowNum => $cols) {
$row = [];
for ($c = 1; $c <= $maxCol; $c++) {
$row[] = $cols[$c] ?? '';
}
$rows[] = $row;
}
return $rows;
}
require __DIR__ . '/includes/footer.php'; ?>