优化细节
This commit is contained in:
parent
5fbbbc8804
commit
0c0aa6d0a9
|
|
@ -373,6 +373,62 @@ while ($row = $stmt->fetch()) {
|
||||||
</div>
|
</div>
|
||||||
</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" id="addUserModal">
|
||||||
<div class="modal-content">
|
<div class="modal-content">
|
||||||
|
|
|
||||||
|
|
@ -68,6 +68,8 @@ if ($isAdmin && $_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['action'])
|
||||||
if ($usedAt !== '') { $update[] = 'used_at = ?'; $params[] = $usedAt; }
|
if ($usedAt !== '') { $update[] = 'used_at = ?'; $params[] = $usedAt; }
|
||||||
if ($userId > 0) { $update[] = 'user_id = ?'; $params[] = $userId; }
|
if ($userId > 0) { $update[] = 'user_id = ?'; $params[] = $userId; }
|
||||||
if ($usedUserId !== '') { $update[] = 'used_user_id = ?'; $params[] = $usedUserId; }
|
if ($usedUserId !== '') { $update[] = 'used_user_id = ?'; $params[] = $usedUserId; }
|
||||||
|
$update[] = "remark = '手动修改'";
|
||||||
|
$update[] = 'updated_at = NOW()';
|
||||||
$params[] = $id;
|
$params[] = $id;
|
||||||
$stmt = $pdo->prepare('UPDATE claim_records SET ' . implode(', ', $update) . ' WHERE id = ?');
|
$stmt = $pdo->prepare('UPDATE claim_records SET ' . implode(', ', $update) . ' WHERE id = ?');
|
||||||
$stmt->execute($params);
|
$stmt->execute($params);
|
||||||
|
|
@ -159,7 +161,6 @@ $records = $stmt->fetchAll();
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th>ID</th>
|
<th>ID</th>
|
||||||
<th>创建时间</th>
|
|
||||||
<th>兑换码名称</th>
|
<th>兑换码名称</th>
|
||||||
<th>批次号</th>
|
<th>批次号</th>
|
||||||
<th>类型</th>
|
<th>类型</th>
|
||||||
|
|
@ -175,6 +176,7 @@ $records = $stmt->fetchAll();
|
||||||
<th>出货价一档</th>
|
<th>出货价一档</th>
|
||||||
<th>出货价二挡</th>
|
<th>出货价二挡</th>
|
||||||
<?php endif; ?>
|
<?php endif; ?>
|
||||||
|
<th>更新时间</th>
|
||||||
<th>备注</th>
|
<th>备注</th>
|
||||||
<?php if ($isAdmin): ?><th>操作</th><?php endif; ?>
|
<?php if ($isAdmin): ?><th>操作</th><?php endif; ?>
|
||||||
</tr>
|
</tr>
|
||||||
|
|
@ -183,7 +185,6 @@ $records = $stmt->fetchAll();
|
||||||
<?php foreach ($records as $r): ?>
|
<?php foreach ($records as $r): ?>
|
||||||
<tr>
|
<tr>
|
||||||
<td><?= $r['id'] ?></td>
|
<td><?= $r['id'] ?></td>
|
||||||
<td><?= formatDateTime($r['created_at']) ?></td>
|
|
||||||
<td><?= h($r['code_name']) ?></td>
|
<td><?= h($r['code_name']) ?></td>
|
||||||
<td><?= h($r['batch_no']) ?></td>
|
<td><?= h($r['batch_no']) ?></td>
|
||||||
<td><?= h($r['code_type']) ?></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_tier1'] !== null ? h($r['price_tier1']) : '-' ?></td>
|
||||||
<td><?= $r['price_tier2'] !== null ? h($r['price_tier2']) : '-' ?></td>
|
<td><?= $r['price_tier2'] !== null ? h($r['price_tier2']) : '-' ?></td>
|
||||||
<?php endif; ?>
|
<?php endif; ?>
|
||||||
|
<td><?= formatDateTime($r['updated_at'] ?? '') ?></td>
|
||||||
<td><?= h($r['remark'] ?? '') ?></td>
|
<td><?= h($r['remark'] ?? '') ?></td>
|
||||||
<?php if ($isAdmin): ?>
|
<?php if ($isAdmin): ?>
|
||||||
<td>
|
<td>
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,7 @@
|
||||||
|
-- 清除数据(按外键依赖顺序)
|
||||||
|
-- 注意:请先备份数据!生产环境谨慎操作。
|
||||||
|
|
||||||
|
DELETE FROM work_orders;
|
||||||
|
DELETE FROM claim_records;
|
||||||
|
DELETE FROM redemption_codes;
|
||||||
|
DELETE FROM bill_status;
|
||||||
|
|
@ -0,0 +1,45 @@
|
||||||
|
# 兑换码状态同步规则
|
||||||
|
|
||||||
|
## 数据流
|
||||||
|
|
||||||
|
```
|
||||||
|
远程API(根据产品配置的接口地址)
|
||||||
|
│
|
||||||
|
▼ 请求参数:start_time, end_time, app_id(产品code)
|
||||||
|
▼ Header:Authorization = 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`
|
||||||
|
|
@ -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";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
@ -13,7 +13,7 @@
|
||||||
<a href="index.php" class="nav-brand">兑换码管理系统</a>
|
<a href="index.php" class="nav-brand">兑换码管理系统</a>
|
||||||
<?php if (count($enabledProducts) > 0): $pid = getCurrentProductId(); ?>
|
<?php if (count($enabledProducts) > 0): $pid = getCurrentProductId(); ?>
|
||||||
<div class="product-switcher">
|
<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): ?>
|
<?php foreach ($enabledProducts as $p): ?>
|
||||||
<option value="<?= $p['id'] ?>" <?= $pid == $p['id'] ? 'selected' : '' ?>><?= h($p['name']) ?></option>
|
<option value="<?= $p['id'] ?>" <?= $pid == $p['id'] ? 'selected' : '' ?>><?= h($p['name']) ?></option>
|
||||||
<?php endforeach; ?>
|
<?php endforeach; ?>
|
||||||
|
|
|
||||||
|
|
@ -95,6 +95,7 @@ CREATE TABLE IF NOT EXISTS claim_records (
|
||||||
status TINYINT NOT NULL DEFAULT 1 COMMENT '1未使用 2已使用 3过期',
|
status TINYINT NOT NULL DEFAULT 1 COMMENT '1未使用 2已使用 3过期',
|
||||||
expired_at DATETIME DEFAULT NULL COMMENT '过期时间',
|
expired_at DATETIME DEFAULT NULL COMMENT '过期时间',
|
||||||
used_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_tier1 DECIMAL(10,2) DEFAULT NULL COMMENT '出货价一档',
|
||||||
price_tier2 DECIMAL(10,2) DEFAULT NULL COMMENT '出货价二挡',
|
price_tier2 DECIMAL(10,2) DEFAULT NULL COMMENT '出货价二挡',
|
||||||
claim_user VARCHAR(50) DEFAULT NULL COMMENT '领取人(用户名)',
|
claim_user VARCHAR(50) DEFAULT NULL COMMENT '领取人(用户名)',
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue