| = $r['id'] ?> |
- = formatDateTime($r['created_at']) ?> |
= h($r['code_name']) ?> |
= h($r['batch_no']) ?> |
= h($r['code_type']) ?> |
@@ -199,6 +200,7 @@ $records = $stmt->fetchAll();
= $r['price_tier1'] !== null ? h($r['price_tier1']) : '-' ?> |
= $r['price_tier2'] !== null ? h($r['price_tier2']) : '-' ?> |
+ = formatDateTime($r['updated_at'] ?? '') ?> |
= h($r['remark'] ?? '') ?> |
diff --git a/clear_data.sql b/clear_data.sql
new file mode 100644
index 0000000..49d239e
--- /dev/null
+++ b/clear_data.sql
@@ -0,0 +1,7 @@
+-- 清除数据(按外键依赖顺序)
+-- 注意:请先备份数据!生产环境谨慎操作。
+
+DELETE FROM work_orders;
+DELETE FROM claim_records;
+DELETE FROM redemption_codes;
+DELETE FROM bill_status;
diff --git a/coupon_sync_rules.md b/coupon_sync_rules.md
new file mode 100644
index 0000000..ba9ff12
--- /dev/null
+++ b/coupon_sync_rules.md
@@ -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`
diff --git a/cron_sync.php b/cron_sync.php
new file mode 100644
index 0000000..a5208f2
--- /dev/null
+++ b/cron_sync.php
@@ -0,0 +1,30 @@
+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";
+ }
+}
diff --git a/includes/coupon_sync.php b/includes/coupon_sync.php
new file mode 100644
index 0000000..0b30f49
--- /dev/null
+++ b/includes/coupon_sync.php
@@ -0,0 +1,92 @@
+ $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,
+ ];
+}
diff --git a/includes/header.php b/includes/header.php
index d1f4be6..fa03381 100755
--- a/includes/header.php
+++ b/includes/header.php
@@ -13,7 +13,7 @@
兑换码管理系统
0): $pid = getCurrentProductId(); ?>
- |