优化细节

This commit is contained in:
zoulin 2026-06-03 19:17:32 +08:00
parent 5fbbbc8804
commit 0c0aa6d0a9
8 changed files with 236 additions and 3 deletions

View File

@ -373,6 +373,62 @@ while ($row = $stmt->fetch()) {
</div>
</div>
<div class="card" style="margin-bottom:20px;">
<h2>兑换码状态同步</h2>
<?php
require_once __DIR__ . '/includes/coupon_sync.php';
$syncResult = '';
if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['sync_coupon'])) {
if (!verifyCsrf($_POST['csrf_token'] ?? '')) { die('CSRF token无效'); }
$startTime = (int)($_POST['start_time'] ?? 0);
$endTime = (int)($_POST['end_time'] ?? 0);
if ($startTime <= 0 || $endTime <= 0 || $startTime > $endTime) {
$syncResult = '<div class="alert alert-danger">请选择有效的时间范围</div>';
} else {
$stmt = $pdo->query("SELECT id, code, name, api_url, token FROM products WHERE status = 1 AND api_url IS NOT NULL AND api_url != ''");
$products = $stmt->fetchAll();
if (empty($products)) {
$syncResult = '<div class="alert alert-warning">没有已配置接口地址的产品</div>';
} else {
$output = '';
foreach ($products as $p) {
$ret = syncCouponStatus($pdo, $p, $startTime, $endTime);
if ($ret['success']) {
$output .= "<strong>{$p['name']}</strong>: API返回{$ret['total']}条, 匹配{$ret['matched']}条, 更新{$ret['updated']}";
if (!empty($ret['matched_codes'])) {
$output .= '<br><span style="font-size:12px;color:#166534;">✓ 已匹配 (' . count($ret['matched_codes']) . '个): ' . h(implode(', ', $ret['matched_codes'])) . '</span>';
}
if (!empty($ret['unmatched_codes'])) {
$output .= '<br><span style="font-size:12px;color:#991b1b;">✗ 未匹配 (' . count($ret['unmatched_codes']) . '个): ' . h(implode(', ', array_slice($ret['unmatched_codes'], 0, 50))) . (count($ret['unmatched_codes']) > 50 ? '...' : '') . '</span>';
}
$output .= '<br>';
} else {
$output .= "<strong>{$p['name']}</strong>: 失败 - {$ret['message']}<br>";
}
}
$syncResult = '<div class="alert alert-success" style="font-size:13px;word-break:break-all;">' . $output . '</div>';
}
}
}
?>
<?= $syncResult ?>
<p style="font-size:14px;color:#666;margin-bottom:12px;">通过远程接口查询兑换码的使用状态,并将已使用的兑换码更新到本地。</p>
<form method="post" style="display:flex;gap:8px;align-items:flex-end;flex-wrap:wrap;" onsubmit="document.getElementById('sync_start').value=Math.floor(new Date(document.getElementById('sync_start_input').value).getTime()/1000);document.getElementById('sync_end').value=Math.floor(new Date(document.getElementById('sync_end_input').value).getTime()/1000);">
<input type="hidden" name="csrf_token" value="<?= csrfToken() ?>">
<div class="form-group" style="margin-bottom:0;">
<label>开始时间</label>
<input type="datetime-local" id="sync_start_input" class="form-control" required style="width:200px;">
<input type="hidden" name="start_time" id="sync_start" value="">
</div>
<div class="form-group" style="margin-bottom:0;">
<label>结束时间</label>
<input type="datetime-local" id="sync_end_input" class="form-control" required style="width:200px;">
<input type="hidden" name="end_time" id="sync_end" value="">
</div>
<button type="submit" name="sync_coupon" class="btn btn-primary">立即同步</button>
</form>
</div>
<!-- 新增用户弹窗 -->
<div class="modal" id="addUserModal">
<div class="modal-content">

View File

@ -68,6 +68,8 @@ if ($isAdmin && $_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['action'])
if ($usedAt !== '') { $update[] = 'used_at = ?'; $params[] = $usedAt; }
if ($userId > 0) { $update[] = 'user_id = ?'; $params[] = $userId; }
if ($usedUserId !== '') { $update[] = 'used_user_id = ?'; $params[] = $usedUserId; }
$update[] = "remark = '手动修改'";
$update[] = 'updated_at = NOW()';
$params[] = $id;
$stmt = $pdo->prepare('UPDATE claim_records SET ' . implode(', ', $update) . ' WHERE id = ?');
$stmt->execute($params);
@ -159,7 +161,6 @@ $records = $stmt->fetchAll();
<thead>
<tr>
<th>ID</th>
<th>创建时间</th>
<th>兑换码名称</th>
<th>批次号</th>
<th>类型</th>
@ -175,6 +176,7 @@ $records = $stmt->fetchAll();
<th>出货价一档</th>
<th>出货价二挡</th>
<?php endif; ?>
<th>更新时间</th>
<th>备注</th>
<?php if ($isAdmin): ?><th>操作</th><?php endif; ?>
</tr>
@ -183,7 +185,6 @@ $records = $stmt->fetchAll();
<?php foreach ($records as $r): ?>
<tr>
<td><?= $r['id'] ?></td>
<td><?= formatDateTime($r['created_at']) ?></td>
<td><?= h($r['code_name']) ?></td>
<td><?= h($r['batch_no']) ?></td>
<td><?= h($r['code_type']) ?></td>
@ -199,6 +200,7 @@ $records = $stmt->fetchAll();
<td><?= $r['price_tier1'] !== null ? h($r['price_tier1']) : '-' ?></td>
<td><?= $r['price_tier2'] !== null ? h($r['price_tier2']) : '-' ?></td>
<?php endif; ?>
<td><?= formatDateTime($r['updated_at'] ?? '') ?></td>
<td><?= h($r['remark'] ?? '') ?></td>
<?php if ($isAdmin): ?>
<td>

7
clear_data.sql Normal file
View File

@ -0,0 +1,7 @@
-- 清除数据(按外键依赖顺序)
-- 注意:请先备份数据!生产环境谨慎操作。
DELETE FROM work_orders;
DELETE FROM claim_records;
DELETE FROM redemption_codes;
DELETE FROM bill_status;

45
coupon_sync_rules.md Normal file
View File

@ -0,0 +1,45 @@
# 兑换码状态同步规则
## 数据流
```
远程API根据产品配置的接口地址
▼ 请求参数start_time, end_time, app_id产品code
▼ HeaderAuthorization = md5(start_time + end_time)
▼ 返回数据status=2 已使用的兑换码列表)
▼ 逐条匹配本地 claim_records
▼ 匹配成功 → 更新状态
▼ 匹配失败 → 记录未匹配
```
## 匹配规则
| 步骤 | 说明 |
|------|------|
| 1. 取数据 | 调用产品配置的 API 接口,按使用时间范围查询已使用的兑换码 |
| 2. 过滤 | 只处理 `status=2`(已使用)的记录 |
| 3. 匹配 | 用 `coupon_code` + `product_id` 匹配本地 `claim_records` 表 |
| 4. 更新 | 匹配到的记录如果本地 `status != 2`则更新状态、使用时间、会员ID |
## 未匹配的常见原因
| 原因 | 说明 |
|------|------|
| 该兑换码不是通过本系统领取的 | API 返回的是该产品下所有已使用的码,包含其他渠道发放的 |
| 产品归属不对 | `claim_records.product_id` 与当前产品的 ID 不一致 |
| 领取记录已被删除 | 数据已被清理 |
## 同步方式
### 1. 手动同步
- 位置:后台设置 → 兑换码状态同步
- 选择时间范围,点击"立即同步"
- 结果显示API 总条数、匹配条数、更新条数、已匹配 code 列表、未匹配 code 列表
### 2. 定时任务
- 每天凌晨 2 点执行,同步前一天的数据
- crontab`0 2 * * * php /path/to/cron_sync.php`

30
cron_sync.php Normal file
View File

@ -0,0 +1,30 @@
<?php
/**
* 定时任务:同步兑换码使用状态
* crontab: 0 2 * * * php /path/to/cron_sync.php
*/
require_once __DIR__ . '/config/db.php';
require_once __DIR__ . '/includes/functions.php';
require_once __DIR__ . '/includes/coupon_sync.php';
$stmt = $pdo->query("SELECT id, code, name, api_url, token FROM products WHERE status = 1 AND api_url IS NOT NULL AND api_url != ''");
$products = $stmt->fetchAll();
if (empty($products)) {
echo date('Y-m-d H:i:s') . " 没有可同步的产品\n";
exit;
}
// 同步前一天的记录
$endTime = strtotime('today') - 1;
$startTime = strtotime('-1 day', $endTime);
foreach ($products as $p) {
echo date('Y-m-d H:i:s') . " 同步产品 [{$p['code']}] {$p['name']} ... ";
$ret = syncCouponStatus($pdo, $p, $startTime, $endTime);
if ($ret['success']) {
echo "API返回{$ret['total']}条, 匹配{$ret['matched']}条, 更新{$ret['updated']}\n";
} else {
echo "失败: {$ret['message']}\n";
}
}

92
includes/coupon_sync.php Normal file
View File

@ -0,0 +1,92 @@
<?php
/**
* 调用远程API同步兑换码使用状态
*/
function callCouponApi(string $apiUrl, string $appId, int $startTime, int $endTime, int $page = 1, int $size = 100): ?array {
$url = rtrim($apiUrl, '/') . "/admin/coupon_code?start_time={$startTime}&end_time={$endTime}&app_id={$appId}&page={$page}&size={$size}";
$authToken = md5($startTime . $endTime);
$ch = curl_init();
curl_setopt_array($ch, [
CURLOPT_URL => $url,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_TIMEOUT => 30,
CURLOPT_HTTPHEADER => ["Authorization: $authToken"],
CURLOPT_SSL_VERIFYPEER => false,
]);
$resp = curl_exec($ch);
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
$error = curl_error($ch);
curl_close($ch);
if ($error) {
return ['error' => "请求失败: $error"];
}
if ($httpCode !== 200) {
return ['error' => "HTTP $httpCode"];
}
$data = json_decode($resp, true);
if (!$data || !isset($data['code']) || $data['code'] !== 0) {
return ['error' => $data['message'] ?? '接口返回格式异常'];
}
return $data;
}
function syncCouponStatus(PDO $pdo, array $product, int $startTime, int $endTime): array {
$page = 1;
$size = 100;
$matched = 0;
$updated = 0;
$matchedCodes = [];
$unmatchedCodes = [];
$total = 0;
while (true) {
$result = callCouponApi($product['api_url'], $product['code'], $startTime, $endTime, $page, $size);
if (isset($result['error'])) {
return ['success' => false, 'message' => $result['error']];
}
$items = $result['data']['items'] ?? [];
$total = (int)$result['data']['total'];
foreach ($items as $item) {
if ((string)$item['status'] !== '2') continue;
$code = $item['coupon_code'];
$usedAt = $item['use_time'] ?? null;
$userId = $item['user_id'] ?? null;
$stmt = $pdo->prepare("SELECT id, status FROM claim_records WHERE code = ? AND product_id = ? LIMIT 1");
$stmt->execute([$code, $product['id']]);
$cr = $stmt->fetch();
if (!$cr) {
$unmatchedCodes[] = $code;
continue;
}
$matched++;
$matchedCodes[] = $code;
if ((int)$cr['status'] !== 2) {
$stmt = $pdo->prepare("UPDATE claim_records SET status = 2, used_at = ?, used_user_id = ?, remark = COALESCE(remark, 'API同步'), updated_at = NOW() WHERE id = ? AND status != 2");
$stmt->execute([$usedAt, $userId, $cr['id']]);
$updated += $stmt->rowCount();
}
}
if ($page * $size >= $total) break;
$page++;
}
return [
'success' => true,
'total' => $total,
'matched' => $matched,
'updated' => $updated,
'matched_codes' => $matchedCodes,
'unmatched_codes' => $unmatchedCodes,
];
}

View File

@ -13,7 +13,7 @@
<a href="index.php" class="nav-brand">兑换码管理系统</a>
<?php if (count($enabledProducts) > 0): $pid = getCurrentProductId(); ?>
<div class="product-switcher">
<select onchange="switchProduct(this)" style="padding:4px 8px;border:1px solid rgba(255,255,255,0.2);border-radius:4px;background:rgba(255,255,255,0.1);color:#fff;font-size:13px;cursor:pointer;">
<select onchange="switchProduct(this)" style="padding:4px 8px;border-radius:4px;font-size:12px;cursor:pointer;">
<?php foreach ($enabledProducts as $p): ?>
<option value="<?= $p['id'] ?>" <?= $pid == $p['id'] ? 'selected' : '' ?>><?= h($p['name']) ?></option>
<?php endforeach; ?>

View File

@ -95,6 +95,7 @@ CREATE TABLE IF NOT EXISTS claim_records (
status TINYINT NOT NULL DEFAULT 1 COMMENT '1未使用 2已使用 3过期',
expired_at DATETIME DEFAULT NULL COMMENT '过期时间',
used_at DATETIME DEFAULT NULL COMMENT '使用时间',
updated_at DATETIME DEFAULT NULL COMMENT '更新时间',
price_tier1 DECIMAL(10,2) DEFAULT NULL COMMENT '出货价一档',
price_tier2 DECIMAL(10,2) DEFAULT NULL COMMENT '出货价二挡',
claim_user VARCHAR(50) DEFAULT NULL COMMENT '领取人(用户名)',