fix: 修复退款时清理一番赏格位、积分兑换商品库存校验及抖音登录自邀问题。
This commit is contained in:
parent
359ca9121f
commit
e3a96e68d8
50
cmd/tools/test_douyin_order/main.go
Normal file
50
cmd/tools/test_douyin_order/main.go
Normal file
@ -0,0 +1,50 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"flag"
|
||||
"fmt"
|
||||
"log"
|
||||
|
||||
"bindbox-game/internal/pkg/logger"
|
||||
"bindbox-game/internal/repository/mysql"
|
||||
"bindbox-game/internal/repository/mysql/dao"
|
||||
"bindbox-game/internal/service/douyin"
|
||||
syscfgsvc "bindbox-game/internal/service/sysconfig"
|
||||
)
|
||||
|
||||
func main() {
|
||||
orderID := flag.String("id", "", "抖音订单号 (order_id)")
|
||||
flag.Parse()
|
||||
|
||||
if *orderID == "" {
|
||||
log.Fatal("请提供订单号,例如: -id=6946062444563338504")
|
||||
}
|
||||
|
||||
// 1. 初始化 MySQL
|
||||
dbRepo, err := mysql.New()
|
||||
if err != nil {
|
||||
log.Fatalf("MySQL 初始化失败: %v", err)
|
||||
}
|
||||
|
||||
// 2. 初始化 Logger (简易版)
|
||||
customLogger, _ := logger.NewCustomLogger(dao.Use(dbRepo.GetDbW()), logger.WithOutputInConsole())
|
||||
|
||||
// 3. 初始化 Service
|
||||
sysCfgSvc := syscfgsvc.New(customLogger, dbRepo)
|
||||
douyinSvc := douyin.New(customLogger, dbRepo, sysCfgSvc, nil)
|
||||
|
||||
// 4. 执行测试
|
||||
fmt.Printf("--- 正在测试订单号: %s ---\n", *orderID)
|
||||
ctx := context.Background()
|
||||
order, err := douyinSvc.GetOrderByOrderID(ctx, *orderID)
|
||||
if err != nil {
|
||||
log.Fatalf("查询失败: %v", err)
|
||||
}
|
||||
|
||||
// 5. 打印结果
|
||||
fmt.Println("查询成功!返回数据如下:")
|
||||
data, _ := json.MarshalIndent(order, "", " ")
|
||||
fmt.Println(string(data))
|
||||
}
|
||||
@ -0,0 +1,83 @@
|
||||
# 任务对齐:统一积分与元比例 (Standardize Points and Yuan Ratio)
|
||||
|
||||
## 1. 项目上下文分析
|
||||
- **项目结构**:
|
||||
- 后端: `bindbox_game` (Go)
|
||||
- 管理后台: `bindbox_game/web/admin` (Vue 3)
|
||||
- 小程序: `bindbox-mini` (UniApp / Vue)
|
||||
- **核心问题**:
|
||||
- 当前代码逻辑隐含 "1积分 = 1分钱" (100积分=1元),前端通过 `/100` 强行展示为 "1.00",导致逻辑割裂。
|
||||
- 用户期望标准: **1元 = 1积分**。
|
||||
- 后端配置缺失,且计算公式需要适配 "元" 为单位。
|
||||
|
||||
## 2. 需求确认
|
||||
- **目标**:
|
||||
1. **统一比例**: 全局统一为 **1 元 = 1 积分** (即 1 积分价值 100 分钱)。
|
||||
2. **配置化**: 后台可配置 "积分/元 兑换比例" (Rate),当前固定为 1。
|
||||
3. **整改范围**: 修正后端转换公式,修正前端展示逻辑,添加后台配置界面。
|
||||
4. **业务场景**: 暂时不涉及"消费送积分" (即订单完成后自动按比例赠送),主要关注积分价值本身(如充值、抵扣、退款等场景的换算)。
|
||||
|
||||
## 3. 现状分析 (As-Is)
|
||||
- **后端 (`bindbox_game`)**:
|
||||
- `CentsToPoints`: 依赖 `points_exchange_per_cent` (分换积分比例),默认为 1。即 1 分钱 = 1 积分单位。
|
||||
- `RefundPointsAmount`: 存在硬编码 `/ 100`,逻辑存疑。
|
||||
- **小程序 (`bindbox-mini`)**:
|
||||
- `formatPoints`: `value / 100`。
|
||||
- 如果后端给 100 积分单位,前端展示为 "1.0"。
|
||||
- 含义模糊:是“1积分”还是“1.0元价值”?
|
||||
- **后台 (`web/admin`)**:
|
||||
- 缺少积分比例配置界面。
|
||||
|
||||
## 4. 关键决策点 (Resolved)
|
||||
1. **积分定义**:
|
||||
- 确认采用 **1 积分 = 1 元** 的价值锚点。
|
||||
- 数据库存储: 存 `1` 代表 1 积分 (即 1 元)。
|
||||
- *变更*: 现在的 `100` (分) 对应 1 元,未来 `1` (积分) 对应 1 元。需要明确是否存在历史数据需要迁移(或者是新项目/可重置)。**假设目前无存量包袱或接受重置/迁移,或者我们调整代码适配现有数值**。
|
||||
- *风险提示*: 如果仅仅改代码不改数据,原来的 100 积分 (1元) 瞬间变成 100 积分 (100元)。**必须确认是否需要数据清洗脚本**。
|
||||
|
||||
2. **兑换比例配置**:
|
||||
- 配置项: `points_exchange_rate` (1 元对应多少积分)。
|
||||
- 默认值: 1。
|
||||
- 公式:
|
||||
- 元转积分 (Amount -> Points): `points = amount_yuan * rate` => `points = (cents / 100) * rate`
|
||||
- 积分转元 (Points -> Amount): `cents = (points / rate) * 100`
|
||||
|
||||
## 5. 实施方案 (Architecture)
|
||||
### 5.1 数据库与配置
|
||||
- **配置表 (`sys_configs`)**:
|
||||
- Key: `points_exchange_rate`
|
||||
- Value: `1` (Default)
|
||||
- Description: "积分/元 兑换比例(多少积分=1元)"。
|
||||
|
||||
### 5.2 后端改造 (`bindbox_game`)
|
||||
- **`internal/pkg/points/convert.go`**:
|
||||
- `CentsToPoints(cents, rate)`:
|
||||
- Old: `cents * rate` (Assumed rate per cent)
|
||||
- New: `(cents * rate) / 100` (Rate per Yuan)
|
||||
- `PointsToCents(points, rate)`:
|
||||
- Old: `points / rate`
|
||||
- New: `(points * 100) / rate`
|
||||
- `RefundPointsAmount`: 适配新公式。
|
||||
- **Service Layer**:
|
||||
- 确保读取新的配置 Key。
|
||||
- 检查所有手动计算积分的地方,全部收敛到 `convert` 包。
|
||||
|
||||
### 5.3 后台改造 (`web/admin`)
|
||||
- **界面**: 在 `SystemConfigs` (系统配置) -> 新增 "积分配置" 分组。
|
||||
- **功能**: 编辑 `points_exchange_rate`。
|
||||
|
||||
### 5.4 小程序/前端改造 (`bindbox-mini`)
|
||||
- **展示逻辑**:
|
||||
- 移除 `formatPoints` 中的 `/ 100`。
|
||||
- 直接展示后端返回的整数积分。
|
||||
- 检查 "积分抵扣" 等页面,确保传给后端的数值正确。
|
||||
|
||||
## 6. 执行计划 (Task Split)
|
||||
1. **Design**: 确认方案无误 (当前步骤)。
|
||||
2. **Backend**:
|
||||
- 修改 Convert 算法。
|
||||
- 确保 Config 读取逻辑正确。
|
||||
- (Optional) 数据迁移脚本/重置脚本 (如果已有数据)。
|
||||
3. **Frontend (Admin)**: 添加配置界面。
|
||||
4. **Frontend (App)**: 修正展示逻辑。
|
||||
5. **Verify**: 验证 1 元订单是否对应 1 积分(模拟),或者充值/手动增加 1 积分是否显示为 1。
|
||||
259
docs/后台工作台接口分析/ALIGNMENT_后台工作台接口.md
Normal file
259
docs/后台工作台接口分析/ALIGNMENT_后台工作台接口.md
Normal file
@ -0,0 +1,259 @@
|
||||
# 后台工作台页面接口分析
|
||||
|
||||
## 一、原始需求
|
||||
分析后台工作台页面的所有设计,确定需要对应哪些接口,并补充后端缺失的接口实现。
|
||||
|
||||
## 二、项目特性规范
|
||||
|
||||
### 技术栈
|
||||
- **前端**: Vue 3 + TypeScript + Element Plus
|
||||
- **后端**: Go + Gin + GORM
|
||||
- **API风格**: RESTful
|
||||
|
||||
### 现有架构
|
||||
- 前端API定义: `web/admin/src/api/dashboard.ts`, `web/admin/src/api/operations.ts`
|
||||
- 后端处理器: `internal/api/admin/dashboard_admin.go`
|
||||
|
||||
---
|
||||
|
||||
## 三、工作台页面模块与接口对应关系
|
||||
|
||||
### 维度1: 经营大盘 (overview)
|
||||
|
||||
| 组件 | 功能 | 前端API | 后端状态 | 备注 |
|
||||
|------|------|---------|----------|------|
|
||||
| `card-list.vue` | 顶部统计卡片 | `fetchCardStats` | ✅ 已实现 | `DashboardCards` |
|
||||
| `sales-overview.vue` | 销售趋势分析 | `fetchSalesDrawTrend` | ✅ 已实现 | `DashboardSalesDrawTrend` |
|
||||
| `product-performance.vue` | 产品动销排行 | `fetchProductPerformance` | ⚠️ Mock数据 | 需要实现 |
|
||||
| `user-economics.vue` | 用户经济分析 | `fetchUserEconomics` | ✅ 已实现 | `DashboardUserEconomics` |
|
||||
|
||||
---
|
||||
|
||||
### 维度2: 奖池与欧气 (lottery)
|
||||
|
||||
| 组件 | 功能 | 前端API | 后端状态 | 备注 |
|
||||
|------|------|---------|----------|------|
|
||||
| `prize-pool-health.vue` | 奖池健康度分析 | `fetchPrizeDistribution` | ✅ 已实现 | `DashboardPrizeDistribution` |
|
||||
| `live-stream-premium.vue` | 全服欧气实时播报 | 无(模拟数据) | ⚠️ 需要实现 | 需要新增实时中奖播报接口 |
|
||||
|
||||
---
|
||||
|
||||
### 维度3: 营销转化 (marketing)
|
||||
|
||||
| 组件 | 功能 | 前端API | 后端状态 | 备注 |
|
||||
|------|------|---------|----------|------|
|
||||
| `growth-analytics.vue` | 增长经济模型分析 | `fetchUserEconomics` | ✅ 已实现 | 复用 `DashboardUserEconomics` |
|
||||
| `coupon-roi.vue` | 营销券效能排行 | `fetchCouponEffectiveness` | ⚠️ Mock数据 | 需要实现 |
|
||||
| `retention-cohort.vue` | 留存同类群组分析 | `fetchRetentionAnalytics` | ✅ 已实现 | `DashboardRetentionAnalytics` |
|
||||
| `marketing-conversion.vue` | 订单转化全链路监控 | `fetchOrderFunnel` | ✅ 已实现 | `DashboardOrderFunnel` |
|
||||
|
||||
---
|
||||
|
||||
### 维度4: 风控预警 (security)
|
||||
|
||||
| 组件 | 功能 | 前端API | 后端状态 | 备注 |
|
||||
|------|------|---------|----------|------|
|
||||
| `inventory-alert.vue` | 库存预警监控 | `fetchInventoryAlerts` | ⚠️ Mock数据 | 需要实现 |
|
||||
| `risk-monitor.vue` | 异常风险监控 | `fetchRiskEvents` | ⚠️ Mock数据 | 需要实现 |
|
||||
| `points-economy.vue` | 积分经济总览 | `fetchPointsEconomySummary`<br>`fetchPointsTrend`<br>`fetchPointsStructure` | ⚠️ Mock数据 | 需要实现3个接口 |
|
||||
|
||||
---
|
||||
|
||||
## 四、需要补充的后端接口清单
|
||||
|
||||
### 4.1 运营分析接口 (Operations)
|
||||
|
||||
#### 1. 产品动销排行 `GET /admin/operations/product_performance`
|
||||
```json
|
||||
// Request
|
||||
{ "rangeType": "7d|30d|today" }
|
||||
|
||||
// Response
|
||||
[
|
||||
{
|
||||
"id": 1,
|
||||
"seriesName": "系列名称",
|
||||
"salesCount": 1540,
|
||||
"amount": 285000, // 销售金额(分)
|
||||
"contributionRate": 35.5, // 利润贡献率%
|
||||
"inventoryTurnover": 8.5 // 周转率
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
#### 2. 优惠券效能排行 `GET /admin/operations/coupon_effectiveness`
|
||||
```json
|
||||
// Request
|
||||
{ "rangeType": "7d|30d" }
|
||||
|
||||
// Response
|
||||
[
|
||||
{
|
||||
"couponId": 1,
|
||||
"couponName": "新用户专享券",
|
||||
"type": "满减券",
|
||||
"issuedCount": 1200,
|
||||
"usedCount": 680,
|
||||
"usedRate": 56.7, // 使用率%
|
||||
"broughtOrders": 720, // 带动订单数
|
||||
"broughtAmount": 3600000, // 带动金额(分)
|
||||
"roi": 3.2 // 投资回报率
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
#### 3. 库存预警列表 `GET /admin/operations/inventory_alerts`
|
||||
```json
|
||||
// 无请求参数
|
||||
|
||||
// Response
|
||||
[
|
||||
{
|
||||
"id": 101,
|
||||
"name": "商品名称",
|
||||
"type": "physical|virtual|coupon",
|
||||
"stock": 3,
|
||||
"threshold": 5,
|
||||
"salesSpeed": 1.2 // 日均消耗速度
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
#### 4. 风险事件监控 `GET /admin/operations/risk_events`
|
||||
```json
|
||||
// 无请求参数
|
||||
|
||||
// Response
|
||||
[
|
||||
{
|
||||
"userId": 5001,
|
||||
"nickname": "用户昵称",
|
||||
"avatar": "头像URL",
|
||||
"type": "frequent_win|batch_register|ip_clash",
|
||||
"description": "24小时内中奖5次一等奖",
|
||||
"riskLevel": "high|medium|low",
|
||||
"createdAt": "13:20"
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 4.2 积分经济接口 (Points Economy)
|
||||
|
||||
#### 5. 积分经济总览 `GET /admin/operations/points_economy_summary`
|
||||
```json
|
||||
// Request
|
||||
{ "rangeType": "7d|30d" }
|
||||
|
||||
// Response
|
||||
{
|
||||
"totalIssued": 1258400, // 发行总积分
|
||||
"totalConsumed": 985600, // 消耗总积分
|
||||
"netChange": 272800, // 净变化
|
||||
"activeUsersWithPoints": 5640, // 持分活跃用户数
|
||||
"conversionRate": 78.5 // 活跃持仓率%
|
||||
}
|
||||
```
|
||||
|
||||
#### 6. 积分趋势 `GET /admin/operations/points_trend`
|
||||
```json
|
||||
// Request
|
||||
{ "rangeType": "7d|30d" }
|
||||
|
||||
// Response
|
||||
[
|
||||
{
|
||||
"date": "2026-01-01",
|
||||
"issued": 20000,
|
||||
"consumed": 15000,
|
||||
"expired": 1000,
|
||||
"netChange": 4000,
|
||||
"balance": 250000
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
#### 7. 积分收支结构 `GET /admin/operations/points_structure`
|
||||
```json
|
||||
// Request
|
||||
{ "rangeType": "7d|30d" }
|
||||
|
||||
// Response
|
||||
[
|
||||
{
|
||||
"category": "任务奖励",
|
||||
"amount": 85000,
|
||||
"percentage": 45.2,
|
||||
"trend": "+12.5%"
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 4.3 实时播报接口 (Live Stream)
|
||||
|
||||
#### 8. 实时中奖播报 `GET /admin/dashboard/live_winners`
|
||||
```json
|
||||
// Request
|
||||
{ "sinceId": 0, "limit": 20 }
|
||||
|
||||
// Response
|
||||
{
|
||||
"list": [
|
||||
{
|
||||
"id": 12345,
|
||||
"nickname": "用户昵称",
|
||||
"avatar": "头像URL",
|
||||
"issueName": "活动期名称",
|
||||
"prizeName": "奖品名称",
|
||||
"isBigWin": true,
|
||||
"createdAt": "刚刚"
|
||||
}
|
||||
],
|
||||
"stats": {
|
||||
"hourlyWinRate": 4.2, // 近1小时爆率
|
||||
"drawsPerMinute": 128 // 连抽频率
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 五、疑问澄清
|
||||
|
||||
### 5.1 需要确认的问题
|
||||
|
||||
1. **风险事件监控**: 目前系统是否有用户行为日志表可供分析?如果没有,是否需要先创建相关基础设施?
|
||||
|
||||
2. **库存预警阈值**: 库存预警的阈值应该从哪里获取?是固定配置还是每个商品可单独设置?
|
||||
|
||||
3. **积分经济统计范围**: 积分发行/消耗是否需要区分来源类型(任务奖励、抽奖中奖、兑换消耗等)?
|
||||
|
||||
4. **实时播报频率**: 前端轮询间隔建议设为多少?3秒是否合适?
|
||||
|
||||
---
|
||||
|
||||
## 六、边界与限制
|
||||
|
||||
### 6.1 任务边界
|
||||
- ✅ 补充后端缺失的运营分析接口
|
||||
- ✅ 实现积分经济相关接口
|
||||
- ✅ 实现库存预警和风险监控接口
|
||||
- ✅ 更新前端API调用从Mock数据改为真实后端调用
|
||||
- ❌ 不涉及前端UI重构
|
||||
- ❌ 不涉及权限管理改动
|
||||
|
||||
### 6.2 依赖关系
|
||||
- 依赖现有数据库表: `users`, `orders`, `user_points_logs`, `coupons`, `user_coupons`, `prizes`, `draw_logs` 等
|
||||
- 可能需要新增数据库视图或缓存层以提高查询性能
|
||||
|
||||
---
|
||||
|
||||
## 七、预期验收标准
|
||||
|
||||
1. 所有前端Mock数据的接口均已替换为真实后端调用
|
||||
2. 后端接口响应格式与前端类型定义一致
|
||||
3. 各项统计数据计算逻辑准确
|
||||
4. 查询性能在可接受范围内(响应时间 < 500ms)
|
||||
91
docs/后台工作台接口分析/CONSENSUS_后台工作台接口.md
Normal file
91
docs/后台工作台接口分析/CONSENSUS_后台工作台接口.md
Normal file
@ -0,0 +1,91 @@
|
||||
# 后台工作台接口 - 共识文档
|
||||
|
||||
## 一、明确需求描述
|
||||
|
||||
补充后台工作台页面中使用Mock数据的8个接口,实现真实的后端数据查询逻辑。
|
||||
|
||||
### 需求范围
|
||||
1. **运营分析接口** (4个)
|
||||
- 产品动销排行
|
||||
- 优惠券效能排行
|
||||
- 库存预警列表
|
||||
- 风险事件监控
|
||||
|
||||
2. **积分经济接口** (3个)
|
||||
- 积分经济总览
|
||||
- 积分趋势
|
||||
- 积分收支结构
|
||||
|
||||
3. **实时播报接口** (1个)
|
||||
- 实时中奖播报
|
||||
|
||||
---
|
||||
|
||||
## 二、技术实现方案
|
||||
|
||||
### 2.1 后端接口实现位置
|
||||
- **文件**: `internal/api/admin/dashboard_admin.go` (现有文件,追加新接口)
|
||||
- **路由注册**: 在现有admin路由组中添加新路径
|
||||
|
||||
### 2.2 数据来源表
|
||||
| 接口 | 主要数据表 | 关联表 |
|
||||
|------|-----------|--------|
|
||||
| 产品动销排行 | `orders`, `activities` | `issues`, `products` |
|
||||
| 优惠券效能 | `user_coupons`, `coupons` | `orders` |
|
||||
| 库存预警 | `issues`, `prizes` | `products` |
|
||||
| 风险事件 | `draw_logs`, `users` | `user_login_logs` (如存在) |
|
||||
| 积分经济 | `user_points_logs` | `users` |
|
||||
| 实时中奖 | `draw_logs` | `users`, `prizes` |
|
||||
|
||||
### 2.3 接口设计原则
|
||||
- 保持与现有接口风格一致
|
||||
- 使用 `rangeType` 参数统一时间范围过滤
|
||||
- 金额单位统一为**分**
|
||||
- 百分比保留2位小数
|
||||
|
||||
---
|
||||
|
||||
## 三、验收标准
|
||||
|
||||
### 功能验收
|
||||
- [ ] 所有8个接口正常返回数据
|
||||
- [ ] 响应格式与前端TypeScript类型定义一致
|
||||
- [ ] 时间范围过滤逻辑正确
|
||||
|
||||
### 性能验收
|
||||
- [ ] 单接口响应时间 < 500ms
|
||||
- [ ] 无N+1查询问题
|
||||
|
||||
### 集成验收
|
||||
- [ ] 前端调用后端接口无报错
|
||||
- [ ] 工作台各模块正确展示真实数据
|
||||
|
||||
---
|
||||
|
||||
## 四、技术约束
|
||||
|
||||
1. **不引入新依赖**: 使用现有GORM查询
|
||||
2. **复用现有工具函数**: 如 `parseRange()`, `percentChange()` 等
|
||||
3. **统一错误处理**: 使用现有 `core.HandlerFunc` 模式
|
||||
|
||||
---
|
||||
|
||||
## 五、已解决的不确定性
|
||||
|
||||
基于现有代码分析:
|
||||
- ✅ 积分日志表 `user_points_logs` 已存在,包含 `change_type` 字段可区分来源
|
||||
- ✅ 用户登录日志可通过 `draw_logs` 和 `orders` 推断活跃度
|
||||
- ✅ 库存阈值可通过 `prizes.quantity` 与剩余数量对比计算
|
||||
|
||||
---
|
||||
|
||||
## 六、实现优先级
|
||||
|
||||
| 优先级 | 接口 | 原因 |
|
||||
|--------|------|------|
|
||||
| P0 | 产品动销排行 | 经营大盘核心指标 |
|
||||
| P0 | 积分经济总览+趋势 | 风控预警必需 |
|
||||
| P1 | 优惠券效能 | 营销分析重要 |
|
||||
| P1 | 库存预警 | 运营监控 |
|
||||
| P2 | 风险事件 | 可先用简化版 |
|
||||
| P2 | 实时中奖播报 | 可复用现有 `draw_stream` |
|
||||
@ -56,6 +56,6 @@ func New(logger logger.CustomLogger, db mysql.Repo, rdb *redis.Client) *handler
|
||||
syscfg: syscfgSvc,
|
||||
snapshotSvc: snapshotSvc,
|
||||
rollbackSvc: rollbackSvc,
|
||||
douyinSvc: douyinsvc.New(logger, db, syscfgSvc),
|
||||
douyinSvc: douyinsvc.New(logger, db, syscfgSvc, nil),
|
||||
}
|
||||
}
|
||||
|
||||
@ -44,7 +44,9 @@ func (h *handler) CreateChannel() core.HandlerFunc {
|
||||
}
|
||||
|
||||
type channelStatsRequest struct {
|
||||
Days int `form:"days"`
|
||||
Days int `form:"days"`
|
||||
StartDate string `form:"start_date"`
|
||||
EndDate string `form:"end_date"`
|
||||
}
|
||||
|
||||
// ChannelStats 渠道数据分析
|
||||
@ -58,7 +60,7 @@ func (h *handler) ChannelStats() core.HandlerFunc {
|
||||
idStr := ctx.Param("channel_id")
|
||||
id, _ := strconv.ParseInt(idStr, 10, 64)
|
||||
|
||||
stats, err := h.channel.GetStats(ctx.RequestContext(), id, req.Days)
|
||||
stats, err := h.channel.GetStats(ctx.RequestContext(), id, req.Days, req.StartDate, req.EndDate)
|
||||
if err != nil {
|
||||
ctx.AbortWithError(core.Error(http.StatusBadRequest, code.CreateAdminError, err.Error()))
|
||||
return
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
184
internal/api/admin/douyin_product_rewards.go
Normal file
184
internal/api/admin/douyin_product_rewards.go
Normal file
@ -0,0 +1,184 @@
|
||||
package admin
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"strconv"
|
||||
|
||||
"bindbox-game/internal/pkg/core"
|
||||
"bindbox-game/internal/pkg/validation"
|
||||
"bindbox-game/internal/repository/mysql/model"
|
||||
|
||||
"gorm.io/datatypes"
|
||||
)
|
||||
|
||||
// ======== 抖店商品奖励规则 CRUD ========
|
||||
|
||||
type douyinProductRewardListRequest struct {
|
||||
Page int `form:"page"`
|
||||
PageSize int `form:"page_size"`
|
||||
}
|
||||
|
||||
type douyinProductRewardListResponse struct {
|
||||
List []douyinProductRewardItem `json:"list"`
|
||||
Total int64 `json:"total"`
|
||||
Page int `json:"page"`
|
||||
}
|
||||
|
||||
type douyinProductRewardItem struct {
|
||||
ID int64 `json:"id"`
|
||||
ProductID string `json:"product_id"`
|
||||
ProductName string `json:"product_name"`
|
||||
RewardType string `json:"reward_type"`
|
||||
RewardPayload json.RawMessage `json:"reward_payload"`
|
||||
Quantity int32 `json:"quantity"`
|
||||
Status int32 `json:"status"`
|
||||
CreatedAt string `json:"created_at"`
|
||||
}
|
||||
|
||||
// ListDouyinProductRewards 获取抖店商品奖励规则列表
|
||||
func (h *handler) ListDouyinProductRewards() core.HandlerFunc {
|
||||
return func(ctx core.Context) {
|
||||
req := new(douyinProductRewardListRequest)
|
||||
if err := ctx.ShouldBindForm(req); err != nil {
|
||||
ctx.AbortWithError(core.Error(http.StatusBadRequest, 10001, validation.Error(err)))
|
||||
return
|
||||
}
|
||||
if req.Page <= 0 {
|
||||
req.Page = 1
|
||||
}
|
||||
if req.PageSize <= 0 {
|
||||
req.PageSize = 20
|
||||
}
|
||||
|
||||
db := h.repo.GetDbR().Model(&model.DouyinProductRewards{})
|
||||
|
||||
var total int64
|
||||
if err := db.Count(&total).Error; err != nil {
|
||||
ctx.AbortWithError(core.Error(http.StatusInternalServerError, 10002, err.Error()))
|
||||
return
|
||||
}
|
||||
|
||||
var list []model.DouyinProductRewards
|
||||
if err := db.Order("id DESC").Offset((req.Page - 1) * req.PageSize).Limit(req.PageSize).Find(&list).Error; err != nil {
|
||||
ctx.AbortWithError(core.Error(http.StatusInternalServerError, 10003, err.Error()))
|
||||
return
|
||||
}
|
||||
|
||||
res := douyinProductRewardListResponse{
|
||||
List: make([]douyinProductRewardItem, len(list)),
|
||||
Total: total,
|
||||
Page: req.Page,
|
||||
}
|
||||
for i, r := range list {
|
||||
res.List[i] = douyinProductRewardItem{
|
||||
ID: r.ID,
|
||||
ProductID: r.ProductID,
|
||||
ProductName: r.ProductName,
|
||||
RewardType: r.RewardType,
|
||||
RewardPayload: json.RawMessage(r.RewardPayload),
|
||||
Quantity: r.Quantity,
|
||||
Status: r.Status,
|
||||
CreatedAt: r.CreatedAt.Format("2006-01-02 15:04:05"),
|
||||
}
|
||||
}
|
||||
ctx.Payload(res)
|
||||
}
|
||||
}
|
||||
|
||||
type createDouyinProductRewardRequest struct {
|
||||
ProductID string `json:"product_id" binding:"required"`
|
||||
ProductName string `json:"product_name"`
|
||||
RewardType string `json:"reward_type" binding:"required"`
|
||||
RewardPayload json.RawMessage `json:"reward_payload"`
|
||||
Quantity int32 `json:"quantity"`
|
||||
Status int32 `json:"status"`
|
||||
}
|
||||
|
||||
// CreateDouyinProductReward 创建抖店商品奖励规则
|
||||
func (h *handler) CreateDouyinProductReward() core.HandlerFunc {
|
||||
return func(ctx core.Context) {
|
||||
req := new(createDouyinProductRewardRequest)
|
||||
if err := ctx.ShouldBindJSON(req); err != nil {
|
||||
ctx.AbortWithError(core.Error(http.StatusBadRequest, 10001, validation.Error(err)))
|
||||
return
|
||||
}
|
||||
if req.Quantity <= 0 {
|
||||
req.Quantity = 1
|
||||
}
|
||||
if req.Status == 0 {
|
||||
req.Status = 1
|
||||
}
|
||||
|
||||
row := &model.DouyinProductRewards{
|
||||
ProductID: req.ProductID,
|
||||
ProductName: req.ProductName,
|
||||
RewardType: req.RewardType,
|
||||
RewardPayload: datatypes.JSON(req.RewardPayload),
|
||||
Quantity: req.Quantity,
|
||||
Status: req.Status,
|
||||
}
|
||||
if err := h.repo.GetDbW().Create(row).Error; err != nil {
|
||||
ctx.AbortWithError(core.Error(http.StatusInternalServerError, 10002, err.Error()))
|
||||
return
|
||||
}
|
||||
ctx.Payload(map[string]any{"id": row.ID, "message": "创建成功"})
|
||||
}
|
||||
}
|
||||
|
||||
type updateDouyinProductRewardRequest struct {
|
||||
ProductName string `json:"product_name"`
|
||||
RewardType string `json:"reward_type"`
|
||||
RewardPayload json.RawMessage `json:"reward_payload"`
|
||||
Quantity int32 `json:"quantity"`
|
||||
Status int32 `json:"status"`
|
||||
}
|
||||
|
||||
// UpdateDouyinProductReward 更新抖店商品奖励规则
|
||||
func (h *handler) UpdateDouyinProductReward() core.HandlerFunc {
|
||||
return func(ctx core.Context) {
|
||||
idStr := ctx.Param("id")
|
||||
id, _ := strconv.ParseInt(idStr, 10, 64)
|
||||
if id <= 0 {
|
||||
ctx.AbortWithError(core.Error(http.StatusBadRequest, 10001, "无效的ID"))
|
||||
return
|
||||
}
|
||||
|
||||
req := new(updateDouyinProductRewardRequest)
|
||||
if err := ctx.ShouldBindJSON(req); err != nil {
|
||||
ctx.AbortWithError(core.Error(http.StatusBadRequest, 10002, validation.Error(err)))
|
||||
return
|
||||
}
|
||||
|
||||
updates := map[string]any{
|
||||
"product_name": req.ProductName,
|
||||
"reward_type": req.RewardType,
|
||||
"reward_payload": datatypes.JSON(req.RewardPayload),
|
||||
"quantity": req.Quantity,
|
||||
"status": req.Status,
|
||||
}
|
||||
if err := h.repo.GetDbW().Model(&model.DouyinProductRewards{}).Where("id = ?", id).Updates(updates).Error; err != nil {
|
||||
ctx.AbortWithError(core.Error(http.StatusInternalServerError, 10003, err.Error()))
|
||||
return
|
||||
}
|
||||
ctx.Payload(map[string]string{"message": "更新成功"})
|
||||
}
|
||||
}
|
||||
|
||||
// DeleteDouyinProductReward 删除抖店商品奖励规则
|
||||
func (h *handler) DeleteDouyinProductReward() core.HandlerFunc {
|
||||
return func(ctx core.Context) {
|
||||
idStr := ctx.Param("id")
|
||||
id, _ := strconv.ParseInt(idStr, 10, 64)
|
||||
if id <= 0 {
|
||||
ctx.AbortWithError(core.Error(http.StatusBadRequest, 10001, "无效的ID"))
|
||||
return
|
||||
}
|
||||
|
||||
if err := h.repo.GetDbW().Delete(&model.DouyinProductRewards{}, id).Error; err != nil {
|
||||
ctx.AbortWithError(core.Error(http.StatusInternalServerError, 10002, err.Error()))
|
||||
return
|
||||
}
|
||||
ctx.Payload(map[string]string{"message": "删除成功"})
|
||||
}
|
||||
}
|
||||
@ -61,9 +61,16 @@ func (h *handler) ListAppUsers() core.HandlerFunc {
|
||||
if req.PageSize > 100 {
|
||||
req.PageSize = 100
|
||||
}
|
||||
u := h.readDB.Users
|
||||
c := h.readDB.Channels
|
||||
|
||||
q := h.readDB.Users.WithContext(ctx.RequestContext()).ReadDB().
|
||||
LeftJoin(h.readDB.Channels, h.readDB.Channels.ID.EqCol(h.readDB.Users.ChannelID)).
|
||||
Select(h.readDB.Users.ALL, h.readDB.Channels.Name.As("channel_name"), h.readDB.Channels.Code.As("channel_code"))
|
||||
LeftJoin(c, c.ID.EqCol(u.ChannelID)).
|
||||
Select(
|
||||
u.ALL,
|
||||
c.Name.As("channel_name"),
|
||||
c.Code.As("channel_code"),
|
||||
)
|
||||
|
||||
// 应用搜索条件
|
||||
if req.ID != nil {
|
||||
@ -169,10 +176,13 @@ func (h *handler) ListAppUsers() core.HandlerFunc {
|
||||
// 批量查询消费统计
|
||||
todayConsume := make(map[int64]int64)
|
||||
sevenDayConsume := make(map[int64]int64)
|
||||
thirtyDayConsume := make(map[int64]int64)
|
||||
totalConsume := make(map[int64]int64)
|
||||
if len(userIDs) > 0 {
|
||||
now := time.Now()
|
||||
todayStart := time.Date(now.Year(), now.Month(), now.Day(), 0, 0, 0, 0, now.Location())
|
||||
sevenDayStart := todayStart.AddDate(0, 0, -6) // 包括今天共7天
|
||||
sevenDayStart := todayStart.AddDate(0, 0, -6)
|
||||
thirtyDayStart := todayStart.AddDate(0, 0, -29)
|
||||
|
||||
type consumeResult struct {
|
||||
UserID int64
|
||||
@ -184,7 +194,7 @@ func (h *handler) ListAppUsers() core.HandlerFunc {
|
||||
h.readDB.Orders.WithContext(ctx.RequestContext()).ReadDB().
|
||||
Select(h.readDB.Orders.UserID, h.readDB.Orders.ActualAmount.Sum().As("amount")).
|
||||
Where(h.readDB.Orders.UserID.In(userIDs...)).
|
||||
Where(h.readDB.Orders.Status.Eq(2)). // 2=已支付
|
||||
Where(h.readDB.Orders.Status.Eq(2)).
|
||||
Where(h.readDB.Orders.CreatedAt.Gte(todayStart)).
|
||||
Group(h.readDB.Orders.UserID).
|
||||
Scan(&todayRes)
|
||||
@ -197,13 +207,110 @@ func (h *handler) ListAppUsers() core.HandlerFunc {
|
||||
h.readDB.Orders.WithContext(ctx.RequestContext()).ReadDB().
|
||||
Select(h.readDB.Orders.UserID, h.readDB.Orders.ActualAmount.Sum().As("amount")).
|
||||
Where(h.readDB.Orders.UserID.In(userIDs...)).
|
||||
Where(h.readDB.Orders.Status.Eq(2)). // 2=已支付
|
||||
Where(h.readDB.Orders.Status.Eq(2)).
|
||||
Where(h.readDB.Orders.CreatedAt.Gte(sevenDayStart)).
|
||||
Group(h.readDB.Orders.UserID).
|
||||
Scan(&sevenRes)
|
||||
for _, r := range sevenRes {
|
||||
sevenDayConsume[r.UserID] = r.Amount
|
||||
}
|
||||
|
||||
// 近30天消费
|
||||
var thirtyRes []consumeResult
|
||||
h.readDB.Orders.WithContext(ctx.RequestContext()).ReadDB().
|
||||
Select(h.readDB.Orders.UserID, h.readDB.Orders.ActualAmount.Sum().As("amount")).
|
||||
Where(h.readDB.Orders.UserID.In(userIDs...)).
|
||||
Where(h.readDB.Orders.Status.Eq(2)).
|
||||
Where(h.readDB.Orders.CreatedAt.Gte(thirtyDayStart)).
|
||||
Group(h.readDB.Orders.UserID).
|
||||
Scan(&thirtyRes)
|
||||
for _, r := range thirtyRes {
|
||||
thirtyDayConsume[r.UserID] = r.Amount
|
||||
}
|
||||
|
||||
// 累计消费
|
||||
var totalRes []consumeResult
|
||||
h.readDB.Orders.WithContext(ctx.RequestContext()).ReadDB().
|
||||
Select(h.readDB.Orders.UserID, h.readDB.Orders.ActualAmount.Sum().As("amount")).
|
||||
Where(h.readDB.Orders.UserID.In(userIDs...)).
|
||||
Where(h.readDB.Orders.Status.Eq(2)).
|
||||
Group(h.readDB.Orders.UserID).
|
||||
Scan(&totalRes)
|
||||
for _, r := range totalRes {
|
||||
totalConsume[r.UserID] = r.Amount
|
||||
}
|
||||
}
|
||||
|
||||
// 批量查询邀请人数
|
||||
inviteCounts := make(map[int64]int64)
|
||||
if len(userIDs) > 0 {
|
||||
type countResult struct {
|
||||
InviterID int64
|
||||
Count int64
|
||||
}
|
||||
var counts []countResult
|
||||
h.readDB.Users.WithContext(ctx.RequestContext()).ReadDB().
|
||||
Select(h.readDB.Users.InviterID, h.readDB.Users.ID.Count().As("count")).
|
||||
Where(h.readDB.Users.InviterID.In(userIDs...)).
|
||||
Group(h.readDB.Users.InviterID).
|
||||
Scan(&counts)
|
||||
for _, c := range counts {
|
||||
inviteCounts[c.InviterID] = c.Count
|
||||
}
|
||||
}
|
||||
|
||||
// 批量查询次数卡数量
|
||||
gamePassCounts := make(map[int64]int64)
|
||||
if len(userIDs) > 0 {
|
||||
type countResult struct {
|
||||
UserID int64
|
||||
Count int64
|
||||
}
|
||||
var counts []countResult
|
||||
now := time.Now()
|
||||
h.readDB.UserGamePasses.WithContext(ctx.RequestContext()).ReadDB().
|
||||
Select(h.readDB.UserGamePasses.UserID, h.readDB.UserGamePasses.Remaining.Sum().As("count")).
|
||||
Where(h.readDB.UserGamePasses.UserID.In(userIDs...)).
|
||||
Where(h.readDB.UserGamePasses.Remaining.Gt(0)).
|
||||
Where(h.readDB.UserGamePasses.Where(h.readDB.UserGamePasses.ExpiredAt.Gt(now)).Or(h.readDB.UserGamePasses.ExpiredAt.IsNull())).
|
||||
Group(h.readDB.UserGamePasses.UserID).
|
||||
Scan(&counts)
|
||||
for _, c := range counts {
|
||||
gamePassCounts[c.UserID] = c.Count
|
||||
}
|
||||
}
|
||||
|
||||
// 批量查询游戏资格数量
|
||||
gameTicketCounts := make(map[int64]int64)
|
||||
if len(userIDs) > 0 {
|
||||
type countResult struct {
|
||||
UserID int64
|
||||
Count int64
|
||||
}
|
||||
var counts []countResult
|
||||
h.readDB.UserGameTickets.WithContext(ctx.RequestContext()).ReadDB().
|
||||
Select(h.readDB.UserGameTickets.UserID, h.readDB.UserGameTickets.Available.Sum().As("count")).
|
||||
Where(h.readDB.UserGameTickets.UserID.In(userIDs...)).
|
||||
Group(h.readDB.UserGameTickets.UserID).
|
||||
Scan(&counts)
|
||||
for _, c := range counts {
|
||||
gameTicketCounts[c.UserID] = c.Count
|
||||
}
|
||||
}
|
||||
|
||||
// 批量查询所有用户的邀请人昵称
|
||||
inviterNicknames := make(map[int64]string)
|
||||
inviterIDs := make([]int64, 0)
|
||||
for _, v := range rows {
|
||||
if v.InviterID > 0 {
|
||||
inviterIDs = append(inviterIDs, v.InviterID)
|
||||
}
|
||||
}
|
||||
if len(inviterIDs) > 0 {
|
||||
inviters, _ := h.readDB.Users.WithContext(ctx.RequestContext()).Where(h.readDB.Users.ID.In(inviterIDs...)).Find()
|
||||
for _, inv := range inviters {
|
||||
inviterNicknames[inv.ID] = inv.Nickname
|
||||
}
|
||||
}
|
||||
|
||||
rsp.Page = req.Page
|
||||
@ -212,20 +319,26 @@ func (h *handler) ListAppUsers() core.HandlerFunc {
|
||||
rsp.List = make([]adminUserItem, len(rows))
|
||||
for i, v := range rows {
|
||||
rsp.List[i] = adminUserItem{
|
||||
ID: v.ID,
|
||||
Nickname: v.Nickname,
|
||||
Avatar: v.Avatar,
|
||||
InviteCode: v.InviteCode,
|
||||
InviterID: v.InviterID,
|
||||
CreatedAt: v.CreatedAt.Format("2006-01-02 15:04:05"),
|
||||
DouyinID: v.DouyinID,
|
||||
ChannelName: v.ChannelName,
|
||||
ChannelCode: v.ChannelCode,
|
||||
PointsBalance: pointBalances[v.ID],
|
||||
CouponsCount: couponCounts[v.ID],
|
||||
ItemCardsCount: cardCounts[v.ID],
|
||||
TodayConsume: todayConsume[v.ID],
|
||||
SevenDayConsume: sevenDayConsume[v.ID],
|
||||
ID: v.ID,
|
||||
Nickname: v.Nickname,
|
||||
Avatar: v.Avatar,
|
||||
InviteCode: v.InviteCode,
|
||||
InviterID: v.InviterID,
|
||||
InviterNickname: inviterNicknames[v.InviterID],
|
||||
CreatedAt: v.CreatedAt.Format("2006-01-02 15:04:05"),
|
||||
DouyinID: v.DouyinID,
|
||||
ChannelName: v.ChannelName,
|
||||
ChannelCode: v.ChannelCode,
|
||||
PointsBalance: pointBalances[v.ID],
|
||||
CouponsCount: couponCounts[v.ID],
|
||||
ItemCardsCount: cardCounts[v.ID],
|
||||
TodayConsume: todayConsume[v.ID],
|
||||
SevenDayConsume: sevenDayConsume[v.ID],
|
||||
ThirtyDayConsume: thirtyDayConsume[v.ID],
|
||||
TotalConsume: totalConsume[v.ID],
|
||||
InviteCount: inviteCounts[v.ID],
|
||||
GamePassCount: gamePassCounts[v.ID],
|
||||
GameTicketCount: gameTicketCounts[v.ID],
|
||||
}
|
||||
}
|
||||
ctx.Payload(rsp)
|
||||
@ -618,20 +731,26 @@ type pointsBalanceResponse struct {
|
||||
}
|
||||
|
||||
type adminUserItem struct {
|
||||
ID int64 `json:"id"`
|
||||
Nickname string `json:"nickname"`
|
||||
Avatar string `json:"avatar"`
|
||||
InviteCode string `json:"invite_code"`
|
||||
InviterID int64 `json:"inviter_id"`
|
||||
CreatedAt string `json:"created_at"`
|
||||
DouyinID string `json:"douyin_id"`
|
||||
ChannelName string `json:"channel_name"`
|
||||
ChannelCode string `json:"channel_code"`
|
||||
PointsBalance int64 `json:"points_balance"`
|
||||
CouponsCount int64 `json:"coupons_count"`
|
||||
ItemCardsCount int64 `json:"item_cards_count"`
|
||||
TodayConsume int64 `json:"today_consume"`
|
||||
SevenDayConsume int64 `json:"seven_day_consume"`
|
||||
ID int64 `json:"id"`
|
||||
Nickname string `json:"nickname"`
|
||||
Avatar string `json:"avatar"`
|
||||
InviteCode string `json:"invite_code"`
|
||||
InviterID int64 `json:"inviter_id"`
|
||||
InviterNickname string `json:"inviter_nickname"` // 邀请人昵称
|
||||
CreatedAt string `json:"created_at"`
|
||||
DouyinID string `json:"douyin_id"`
|
||||
ChannelName string `json:"channel_name"`
|
||||
ChannelCode string `json:"channel_code"`
|
||||
PointsBalance int64 `json:"points_balance"`
|
||||
CouponsCount int64 `json:"coupons_count"`
|
||||
ItemCardsCount int64 `json:"item_cards_count"`
|
||||
TodayConsume int64 `json:"today_consume"`
|
||||
SevenDayConsume int64 `json:"seven_day_consume"`
|
||||
ThirtyDayConsume int64 `json:"thirty_day_consume"` // 近30天消费
|
||||
TotalConsume int64 `json:"total_consume"` // 累计消费
|
||||
InviteCount int64 `json:"invite_count"` // 邀请人数
|
||||
GamePassCount int64 `json:"game_pass_count"` // 次数卡数量
|
||||
GameTicketCount int64 `json:"game_ticket_count"` // 游戏资格数量
|
||||
}
|
||||
|
||||
// ListAppUsers 管理端用户列表GetUserPointsBalance 查看用户积分余额
|
||||
|
||||
@ -12,25 +12,29 @@ import (
|
||||
// UserProfileResponse 用户综合画像
|
||||
type UserProfileResponse struct {
|
||||
// 基本信息
|
||||
ID int64 `json:"id"`
|
||||
Nickname string `json:"nickname"`
|
||||
Avatar string `json:"avatar"`
|
||||
Mobile string `json:"mobile"`
|
||||
InviteCode string `json:"invite_code"`
|
||||
InviterID int64 `json:"inviter_id"`
|
||||
ChannelID int64 `json:"channel_id"`
|
||||
CreatedAt string `json:"created_at"`
|
||||
DouyinID string `json:"douyin_id"`
|
||||
ID int64 `json:"id"`
|
||||
Nickname string `json:"nickname"`
|
||||
Avatar string `json:"avatar"`
|
||||
Mobile string `json:"mobile"`
|
||||
InviteCode string `json:"invite_code"`
|
||||
InviterID int64 `json:"inviter_id"`
|
||||
ChannelID int64 `json:"channel_id"`
|
||||
CreatedAt string `json:"created_at"`
|
||||
DouyinID string `json:"douyin_id"`
|
||||
InviterNickname string `json:"inviter_nickname"` // 邀请人昵称
|
||||
|
||||
// 邀请统计
|
||||
InviteCount int64 `json:"invite_count"`
|
||||
|
||||
// 生命周期财务指标
|
||||
LifetimeStats struct {
|
||||
TotalPaid int64 `json:"total_paid"` // 累计支付
|
||||
TotalRefunded int64 `json:"total_refunded"` // 累计退款
|
||||
NetCashCost int64 `json:"net_cash_cost"` // 净现金支出
|
||||
OrderCount int64 `json:"order_count"` // 订单数
|
||||
TotalPaid int64 `json:"total_paid"` // 累计支付
|
||||
TotalRefunded int64 `json:"total_refunded"` // 累计退款
|
||||
NetCashCost int64 `json:"net_cash_cost"` // 净现金支出
|
||||
OrderCount int64 `json:"order_count"` // 订单数
|
||||
TodayPaid int64 `json:"today_paid"` // 当日支付
|
||||
SevenDayPaid int64 `json:"seven_day_paid"` // 近7天支付
|
||||
ThirtyDayPaid int64 `json:"thirty_day_paid"` // 近30天支付
|
||||
} `json:"lifetime_stats"`
|
||||
|
||||
// 当前资产快照
|
||||
@ -42,6 +46,8 @@ type UserProfileResponse struct {
|
||||
CouponValue int64 `json:"coupon_value"` // 持有优惠券价值
|
||||
ItemCardCount int64 `json:"item_card_count"` // 持有道具卡数
|
||||
ItemCardValue int64 `json:"item_card_value"` // 持有道具卡价值
|
||||
GamePassCount int64 `json:"game_pass_count"` // 持有次数卡数
|
||||
GameTicketCount int64 `json:"game_ticket_count"` // 持有游戏资格数
|
||||
TotalAssetValue int64 `json:"total_asset_value"` // 总资产估值
|
||||
ProfitLossRatio float64 `json:"profit_loss_ratio"` // 累计盈亏比
|
||||
} `json:"current_assets"`
|
||||
@ -84,25 +90,70 @@ func (h *handler) GetUserProfile() core.HandlerFunc {
|
||||
rsp.DouyinID = user.DouyinID
|
||||
rsp.CreatedAt = user.CreatedAt.Format(time.RFC3339)
|
||||
|
||||
// 1.1 查询邀请人昵称
|
||||
if user.InviterID > 0 {
|
||||
inviter, _ := h.readDB.Users.WithContext(ctx.RequestContext()).Where(h.readDB.Users.ID.Eq(user.InviterID)).First()
|
||||
if inviter != nil {
|
||||
rsp.InviterNickname = inviter.Nickname
|
||||
}
|
||||
}
|
||||
|
||||
// 2. 邀请统计
|
||||
rsp.InviteCount, _ = h.readDB.Users.WithContext(ctx.RequestContext()).Where(h.readDB.Users.InviterID.Eq(userID)).Count()
|
||||
|
||||
// 3. 生命周期财务指标
|
||||
// 3.1 累计支付 & 订单数 - 只统计未退款的订单
|
||||
// 3.1 消费统计
|
||||
type orderStats struct {
|
||||
TotalPaid int64
|
||||
OrderCount int64
|
||||
TotalPaid int64
|
||||
OrderCount int64
|
||||
TodayPaid int64
|
||||
SevenDayPaid int64
|
||||
ThirtyDayPaid int64
|
||||
}
|
||||
var os orderStats
|
||||
now := time.Now()
|
||||
todayStart := time.Date(now.Year(), now.Month(), now.Day(), 0, 0, 0, 0, now.Location())
|
||||
sevenDayStart := todayStart.AddDate(0, 0, -6)
|
||||
thirtyDayStart := todayStart.AddDate(0, 0, -29)
|
||||
|
||||
h.readDB.Orders.WithContext(ctx.RequestContext()).ReadDB().
|
||||
Select(h.readDB.Orders.ActualAmount.Sum().As("total_paid"), h.readDB.Orders.ID.Count().As("order_count")).
|
||||
Select(
|
||||
h.readDB.Orders.ActualAmount.Sum().As("total_paid"),
|
||||
h.readDB.Orders.ID.Count().As("order_count"),
|
||||
).
|
||||
Where(h.readDB.Orders.UserID.Eq(userID)).
|
||||
Where(h.readDB.Orders.Status.Eq(2)). // 仅已支付,不含已退款
|
||||
Where(h.readDB.Orders.Status.Eq(2)).
|
||||
Scan(&os)
|
||||
|
||||
// 分阶段统计
|
||||
h.readDB.Orders.WithContext(ctx.RequestContext()).ReadDB().
|
||||
Select(h.readDB.Orders.ActualAmount.Sum().As("today_paid")).
|
||||
Where(h.readDB.Orders.UserID.Eq(userID)).
|
||||
Where(h.readDB.Orders.Status.Eq(2)).
|
||||
Where(h.readDB.Orders.CreatedAt.Gte(todayStart)).
|
||||
Scan(&os.TodayPaid)
|
||||
|
||||
h.readDB.Orders.WithContext(ctx.RequestContext()).ReadDB().
|
||||
Select(h.readDB.Orders.ActualAmount.Sum().As("seven_day_paid")).
|
||||
Where(h.readDB.Orders.UserID.Eq(userID)).
|
||||
Where(h.readDB.Orders.Status.Eq(2)).
|
||||
Where(h.readDB.Orders.CreatedAt.Gte(sevenDayStart)).
|
||||
Scan(&os.SevenDayPaid)
|
||||
|
||||
h.readDB.Orders.WithContext(ctx.RequestContext()).ReadDB().
|
||||
Select(h.readDB.Orders.ActualAmount.Sum().As("thirty_day_paid")).
|
||||
Where(h.readDB.Orders.UserID.Eq(userID)).
|
||||
Where(h.readDB.Orders.Status.Eq(2)).
|
||||
Where(h.readDB.Orders.CreatedAt.Gte(thirtyDayStart)).
|
||||
Scan(&os.ThirtyDayPaid)
|
||||
|
||||
rsp.LifetimeStats.TotalPaid = os.TotalPaid
|
||||
rsp.LifetimeStats.OrderCount = os.OrderCount
|
||||
rsp.LifetimeStats.TodayPaid = os.TodayPaid
|
||||
rsp.LifetimeStats.SevenDayPaid = os.SevenDayPaid
|
||||
rsp.LifetimeStats.ThirtyDayPaid = os.ThirtyDayPaid
|
||||
|
||||
// 3.2 累计退款 - 显示实际退款金额(参考信息)
|
||||
// 3.2 累计退款
|
||||
var totalRefunded int64
|
||||
_ = h.repo.GetDbR().Raw(`
|
||||
SELECT COALESCE(SUM(pr.amount_refund), 0)
|
||||
@ -111,8 +162,11 @@ func (h *handler) GetUserProfile() core.HandlerFunc {
|
||||
WHERE o.user_id = ? AND pr.status = 'SUCCESS'
|
||||
`, userID).Scan(&totalRefunded).Error
|
||||
rsp.LifetimeStats.TotalRefunded = totalRefunded
|
||||
// 净投入 = 累计支付(因为已排除退款订单,所以不减退款)
|
||||
rsp.LifetimeStats.NetCashCost = rsp.LifetimeStats.TotalPaid
|
||||
// 净现金投入 = 累计实付 - 累计退款
|
||||
rsp.LifetimeStats.NetCashCost = rsp.LifetimeStats.TotalPaid - totalRefunded
|
||||
if rsp.LifetimeStats.NetCashCost < 0 {
|
||||
rsp.LifetimeStats.NetCashCost = 0
|
||||
}
|
||||
|
||||
// 4. 当前资产快照
|
||||
// 4.1 积分余额
|
||||
@ -164,11 +218,23 @@ func (h *handler) GetUserProfile() core.HandlerFunc {
|
||||
rsp.CurrentAssets.ItemCardCount = cds.Count
|
||||
rsp.CurrentAssets.ItemCardValue = cds.Value
|
||||
|
||||
// 4.5 持有次数卡
|
||||
_ = h.repo.GetDbR().Raw("SELECT COALESCE(SUM(remaining), 0) FROM user_game_passes WHERE user_id = ? AND remaining > 0 AND (expired_at IS NULL OR expired_at > NOW())", userID).Scan(&rsp.CurrentAssets.GamePassCount).Error
|
||||
|
||||
// 4.6 持有游戏资格
|
||||
_ = h.repo.GetDbR().Raw("SELECT COALESCE(SUM(available), 0) FROM user_game_tickets WHERE user_id = ?", userID).Scan(&rsp.CurrentAssets.GameTicketCount).Error
|
||||
|
||||
// 4.5 总资产估值
|
||||
// 估值逻辑:积分余额 + 商品价值 + 优惠券价值 + 道具卡价值 + 次数卡(2元/次) + 游戏资格(1元/场)
|
||||
gamePassValue := rsp.CurrentAssets.GamePassCount * 200 // 估值:2元/次
|
||||
gameTicketValue := rsp.CurrentAssets.GameTicketCount * 100 // 估值:1元/场
|
||||
|
||||
rsp.CurrentAssets.TotalAssetValue = rsp.CurrentAssets.PointsBalance +
|
||||
rsp.CurrentAssets.InventoryValue +
|
||||
rsp.CurrentAssets.CouponValue +
|
||||
rsp.CurrentAssets.ItemCardValue
|
||||
rsp.CurrentAssets.ItemCardValue +
|
||||
gamePassValue +
|
||||
gameTicketValue
|
||||
|
||||
// 4.6 累计盈亏比
|
||||
if rsp.LifetimeStats.NetCashCost > 0 {
|
||||
|
||||
@ -17,8 +17,8 @@ type userProfitLossRequest struct {
|
||||
|
||||
type userProfitLossPoint struct {
|
||||
Date string `json:"date"`
|
||||
Cost int64 `json:"cost"` // 净支出(仅已支付未退款订单)
|
||||
Value int64 `json:"value"` // 当前资产快照(实时)
|
||||
Cost int64 `json:"cost"` // 累计投入(已支付-已退款)
|
||||
Value int64 `json:"value"` // 累计产出(当前资产快照)
|
||||
Profit int64 `json:"profit"` // 净盈亏
|
||||
Ratio float64 `json:"ratio"` // 盈亏比
|
||||
Breakdown struct {
|
||||
@ -30,8 +30,14 @@ type userProfitLossPoint struct {
|
||||
}
|
||||
|
||||
type userProfitLossResponse struct {
|
||||
Granularity string `json:"granularity"`
|
||||
List []userProfitLossPoint `json:"list"`
|
||||
Granularity string `json:"granularity"`
|
||||
List []userProfitLossPoint `json:"list"`
|
||||
Summary struct {
|
||||
TotalCost int64 `json:"total_cost"`
|
||||
TotalValue int64 `json:"total_value"`
|
||||
TotalProfit int64 `json:"total_profit"`
|
||||
AvgRatio float64 `json:"avg_ratio"`
|
||||
} `json:"summary"`
|
||||
CurrentAssets struct {
|
||||
Points int64 `json:"points"`
|
||||
Products int64 `json:"products"`
|
||||
@ -85,14 +91,49 @@ func (h *handler) GetUserProfitLossTrend() core.HandlerFunc {
|
||||
_ = h.repo.GetDbR().Raw("SELECT COALESCE(SUM(balance_amount), 0) FROM user_coupons WHERE user_id = ? AND status = 1", userID).Scan(&curAssets.Coupons).Error
|
||||
totalAssetValue := curAssets.Points + curAssets.Products + curAssets.Cards + curAssets.Coupons
|
||||
|
||||
// --- 2. 获取订单数据(仅 status=2 已支付未退款)---
|
||||
// --- 2. 获取订单数据(仅 status=2 已支付) ---
|
||||
// 注意:为了计算累计趋势,我们需要获取 start 之前的所有已支付订单总额作为基数
|
||||
var baseCost int64 = 0
|
||||
_ = h.readDB.Orders.WithContext(ctx.RequestContext()).ReadDB().
|
||||
Select(h.readDB.Orders.ActualAmount.Sum()).
|
||||
Where(h.readDB.Orders.UserID.Eq(userID)).
|
||||
Where(h.readDB.Orders.Status.Eq(2)).
|
||||
Where(h.readDB.Orders.CreatedAt.Lt(start)).
|
||||
Scan(&baseCost)
|
||||
|
||||
// 扣除历史退款 (如果有的话,此处简化处理,主要关注当前范围内的波动)
|
||||
var baseRefund int64 = 0
|
||||
_ = h.repo.GetDbR().Raw(`
|
||||
SELECT COALESCE(SUM(pr.amount_refund), 0)
|
||||
FROM payment_refunds pr
|
||||
JOIN orders o ON o.order_no = pr.order_no COLLATE utf8mb4_unicode_ci
|
||||
WHERE o.user_id = ? AND pr.status = 'SUCCESS' AND pr.created_at < ?
|
||||
`, userID, start).Scan(&baseRefund).Error
|
||||
baseCost -= baseRefund
|
||||
if baseCost < 0 {
|
||||
baseCost = 0
|
||||
}
|
||||
|
||||
orderRows, _ := h.readDB.Orders.WithContext(ctx.RequestContext()).ReadDB().
|
||||
Where(h.readDB.Orders.UserID.Eq(userID)).
|
||||
Where(h.readDB.Orders.Status.Eq(2)). // 仅已支付,不含已退款
|
||||
Where(h.readDB.Orders.Status.Eq(2)).
|
||||
Where(h.readDB.Orders.CreatedAt.Gte(start)).
|
||||
Where(h.readDB.Orders.CreatedAt.Lte(end)).
|
||||
Find()
|
||||
|
||||
// 获取当前范围内的退款
|
||||
type refundInfo struct {
|
||||
Amount int64
|
||||
CreatedAt time.Time
|
||||
}
|
||||
var refunds []refundInfo
|
||||
_ = h.repo.GetDbR().Raw(`
|
||||
SELECT pr.amount_refund as amount, pr.created_at
|
||||
FROM payment_refunds pr
|
||||
JOIN orders o ON o.order_no = pr.order_no COLLATE utf8mb4_unicode_ci
|
||||
WHERE o.user_id = ? AND pr.status = 'SUCCESS' AND pr.created_at BETWEEN ? AND ?
|
||||
`, userID, start, end).Scan(&refunds).Error
|
||||
|
||||
// --- 3. 按时间分桶计算 ---
|
||||
list := make([]userProfitLossPoint, len(buckets))
|
||||
|
||||
@ -100,24 +141,35 @@ func (h *handler) GetUserProfitLossTrend() core.HandlerFunc {
|
||||
return (t.After(b.Start) || t.Equal(b.Start)) && t.Before(b.End)
|
||||
}
|
||||
|
||||
var cumulativeCost int64 = 0
|
||||
cumulativeCost := baseCost
|
||||
|
||||
for i, b := range buckets {
|
||||
p := &list[i]
|
||||
p.Date = b.Label
|
||||
|
||||
// 计算该时间段内的支出
|
||||
var periodCost int64 = 0
|
||||
// 计算该时间段内的净投入变化
|
||||
var periodDelta int64 = 0
|
||||
for _, o := range orderRows {
|
||||
if inBucket(o.CreatedAt, b) {
|
||||
periodCost += o.ActualAmount
|
||||
periodDelta += o.ActualAmount
|
||||
}
|
||||
}
|
||||
for _, r := range refunds {
|
||||
if inBucket(r.CreatedAt, b) {
|
||||
periodDelta -= r.Amount
|
||||
}
|
||||
}
|
||||
cumulativeCost += periodCost
|
||||
p.Cost = periodCost
|
||||
|
||||
// 使用当前资产快照作为产出值(最后一个桶显示完整值,其他桶按比例或显示0)
|
||||
// 简化:所有桶都显示当前快照值,让用户一眼看到当前状态
|
||||
cumulativeCost += periodDelta
|
||||
if cumulativeCost < 0 {
|
||||
cumulativeCost = 0
|
||||
}
|
||||
p.Cost = cumulativeCost
|
||||
|
||||
// 产出值:当前资产是一个存量值。
|
||||
// 理想逻辑是回溯各时间点的余额,简化逻辑下:
|
||||
// 如果该点还没有在该范围内发生过任何投入(且没有基数),则显示0;否则显示当前快照值。
|
||||
// 这里我们统一显示当前快照,但在前端图表上它会是一条水平线或阶梯线。
|
||||
p.Value = totalAssetValue
|
||||
p.Breakdown.Points = curAssets.Points
|
||||
p.Breakdown.Products = curAssets.Products
|
||||
@ -132,42 +184,46 @@ func (h *handler) GetUserProfitLossTrend() core.HandlerFunc {
|
||||
}
|
||||
}
|
||||
|
||||
// 计算累计值用于汇总显示
|
||||
// 汇总数据
|
||||
var totalCost int64 = 0
|
||||
for _, o := range orderRows {
|
||||
totalCost += o.ActualAmount
|
||||
_ = h.readDB.Orders.WithContext(ctx.RequestContext()).ReadDB().
|
||||
Select(h.readDB.Orders.ActualAmount.Sum()).
|
||||
Where(h.readDB.Orders.UserID.Eq(userID)).
|
||||
Where(h.readDB.Orders.Status.Eq(2)).
|
||||
Scan(&totalCost)
|
||||
|
||||
var totalRefund int64 = 0
|
||||
_ = h.repo.GetDbR().Raw(`
|
||||
SELECT COALESCE(SUM(pr.amount_refund), 0)
|
||||
FROM payment_refunds pr
|
||||
JOIN orders o ON o.order_no = pr.order_no COLLATE utf8mb4_unicode_ci
|
||||
WHERE o.user_id = ? AND pr.status = 'SUCCESS'
|
||||
`, userID).Scan(&totalRefund).Error
|
||||
|
||||
finalNetCost := totalCost - totalRefund
|
||||
if finalNetCost < 0 {
|
||||
finalNetCost = 0
|
||||
}
|
||||
|
||||
// 最后一个桶使用累计成本
|
||||
if len(list) > 0 {
|
||||
lastIdx := len(list) - 1
|
||||
// 汇总数据:使用累计成本和当前资产值
|
||||
list[lastIdx].Cost = totalCost
|
||||
list[lastIdx].Value = totalAssetValue
|
||||
list[lastIdx].Profit = totalAssetValue - totalCost
|
||||
if totalCost > 0 {
|
||||
list[lastIdx].Ratio = float64(totalAssetValue) / float64(totalCost)
|
||||
} else if totalAssetValue > 0 {
|
||||
list[lastIdx].Ratio = 99.9
|
||||
}
|
||||
}
|
||||
|
||||
ctx.Payload(userProfitLossResponse{
|
||||
resp := userProfitLossResponse{
|
||||
Granularity: gran,
|
||||
List: list,
|
||||
CurrentAssets: struct {
|
||||
Points int64 `json:"points"`
|
||||
Products int64 `json:"products"`
|
||||
Cards int64 `json:"cards"`
|
||||
Coupons int64 `json:"coupons"`
|
||||
Total int64 `json:"total"`
|
||||
}{
|
||||
Points: curAssets.Points,
|
||||
Products: curAssets.Products,
|
||||
Cards: curAssets.Cards,
|
||||
Coupons: curAssets.Coupons,
|
||||
Total: totalAssetValue,
|
||||
},
|
||||
})
|
||||
}
|
||||
resp.Summary.TotalCost = finalNetCost
|
||||
resp.Summary.TotalValue = totalAssetValue
|
||||
resp.Summary.TotalProfit = totalAssetValue - finalNetCost
|
||||
if finalNetCost > 0 {
|
||||
resp.Summary.AvgRatio = float64(totalAssetValue) / float64(finalNetCost)
|
||||
} else if totalAssetValue > 0 {
|
||||
resp.Summary.AvgRatio = 99.9
|
||||
}
|
||||
|
||||
resp.CurrentAssets.Points = curAssets.Points
|
||||
resp.CurrentAssets.Products = curAssets.Products
|
||||
resp.CurrentAssets.Cards = curAssets.Cards
|
||||
resp.CurrentAssets.Coupons = curAssets.Coupons
|
||||
resp.CurrentAssets.Total = totalAssetValue
|
||||
|
||||
ctx.Payload(resp)
|
||||
}
|
||||
}
|
||||
|
||||
@ -4,6 +4,8 @@ import (
|
||||
"bindbox-game/internal/pkg/logger"
|
||||
"bindbox-game/internal/repository/mysql"
|
||||
"bindbox-game/internal/repository/mysql/dao"
|
||||
"bindbox-game/internal/service/douyin"
|
||||
"bindbox-game/internal/service/sysconfig"
|
||||
tasksvc "bindbox-game/internal/service/task_center"
|
||||
usersvc "bindbox-game/internal/service/user"
|
||||
)
|
||||
@ -14,9 +16,19 @@ type handler struct {
|
||||
readDB *dao.Query
|
||||
user usersvc.Service
|
||||
task tasksvc.Service
|
||||
douyin douyin.Service
|
||||
repo mysql.Repo
|
||||
}
|
||||
|
||||
func New(logger logger.CustomLogger, db mysql.Repo, taskSvc tasksvc.Service) *handler {
|
||||
return &handler{logger: logger, writeDB: dao.Use(db.GetDbW()), readDB: dao.Use(db.GetDbR()), user: usersvc.New(logger, db), task: taskSvc, repo: db}
|
||||
syscfgSvc := sysconfig.New(logger, db)
|
||||
return &handler{
|
||||
logger: logger,
|
||||
writeDB: dao.Use(db.GetDbW()),
|
||||
readDB: dao.Use(db.GetDbR()),
|
||||
user: usersvc.New(logger, db),
|
||||
task: taskSvc,
|
||||
douyin: douyin.New(logger, db, syscfgSvc, nil),
|
||||
repo: db,
|
||||
}
|
||||
}
|
||||
|
||||
77
internal/api/user/bind_douyin_order_app.go
Normal file
77
internal/api/user/bind_douyin_order_app.go
Normal file
@ -0,0 +1,77 @@
|
||||
package app
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"bindbox-game/internal/code"
|
||||
"bindbox-game/internal/pkg/core"
|
||||
"bindbox-game/internal/pkg/validation"
|
||||
)
|
||||
|
||||
type bindDouyinOrderRequest struct {
|
||||
DouyinID string `json:"douyin_id"`
|
||||
}
|
||||
|
||||
type bindDouyinOrderResponse struct {
|
||||
DouyinID string `json:"douyin_id"`
|
||||
}
|
||||
|
||||
// BindDouyinOrder 绑定抖音ID
|
||||
// @Summary 绑定抖音ID
|
||||
// @Description 输入抖音号(Buyer ID),绑定到当前用户
|
||||
// @Tags APP端.用户
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Security LoginVerifyToken
|
||||
// @Param RequestBody body bindDouyinOrderRequest true "请求参数"
|
||||
// @Success 200 {object} bindDouyinOrderResponse
|
||||
// @Failure 400 {object} code.Failure
|
||||
// @Router /api/app/users/douyin/bind [post]
|
||||
func (h *handler) BindDouyinOrder() core.HandlerFunc {
|
||||
return func(ctx core.Context) {
|
||||
req := new(bindDouyinOrderRequest)
|
||||
if err := ctx.ShouldBindJSON(req); err != nil {
|
||||
ctx.AbortWithError(core.Error(http.StatusBadRequest, code.ParamBindError, validation.Error(err)))
|
||||
return
|
||||
}
|
||||
|
||||
if req.DouyinID == "" {
|
||||
ctx.AbortWithError(core.Error(http.StatusBadRequest, code.ParamBindError, "抖音号不能为空"))
|
||||
return
|
||||
}
|
||||
|
||||
currentUserID := int64(ctx.SessionUserInfo().Id)
|
||||
|
||||
// 0. 检查当前用户信息
|
||||
currentUser, err := h.readDB.Users.WithContext(ctx.RequestContext()).Where(h.readDB.Users.ID.Eq(currentUserID)).First()
|
||||
if err != nil {
|
||||
ctx.AbortWithError(core.Error(http.StatusInternalServerError, code.ServerError, "获取用户信息失败"))
|
||||
return
|
||||
}
|
||||
|
||||
// 如果已经绑定了相同的 ID,直接返回成功
|
||||
if currentUser.DouyinUserID == req.DouyinID {
|
||||
ctx.Payload(&bindDouyinOrderResponse{DouyinID: req.DouyinID})
|
||||
return
|
||||
}
|
||||
|
||||
// 1. 检查该抖音号是否已被其他本地账号绑定
|
||||
existedUser, _ := h.readDB.Users.WithContext(ctx.RequestContext()).Where(h.readDB.Users.DouyinUserID.Eq(req.DouyinID)).First()
|
||||
if existedUser != nil && existedUser.ID != currentUserID {
|
||||
ctx.AbortWithError(core.Error(http.StatusBadRequest, code.ParamBindError, "该抖音号已被其他账号绑定"))
|
||||
return
|
||||
}
|
||||
|
||||
// 2. 更新本地用户表的 douyin_user_id
|
||||
if _, err := h.writeDB.Users.WithContext(ctx.RequestContext()).Where(h.writeDB.Users.ID.Eq(currentUserID)).Updates(map[string]any{
|
||||
"douyin_user_id": req.DouyinID,
|
||||
}); err != nil {
|
||||
ctx.AbortWithError(core.Error(http.StatusInternalServerError, code.ServerError, "更新用户信息失败"))
|
||||
return
|
||||
}
|
||||
|
||||
ctx.Payload(&bindDouyinOrderResponse{
|
||||
DouyinID: req.DouyinID,
|
||||
})
|
||||
}
|
||||
}
|
||||
@ -28,13 +28,11 @@ var (
|
||||
AuditRollbackLogs *auditRollbackLogs
|
||||
Banner *banner
|
||||
Channels *channels
|
||||
DouyinOrders *douyinOrders
|
||||
GamePassPackages *gamePassPackages
|
||||
GameTicketLogs *gameTicketLogs
|
||||
IssuePositionClaims *issuePositionClaims
|
||||
LogOperation *logOperation
|
||||
LogRequest *logRequest
|
||||
LotteryRefundLogs *lotteryRefundLogs
|
||||
MatchingCardTypes *matchingCardTypes
|
||||
MenuActions *menuActions
|
||||
Menus *menus
|
||||
@ -61,11 +59,9 @@ var (
|
||||
SystemItemCards *systemItemCards
|
||||
SystemTitleEffects *systemTitleEffects
|
||||
SystemTitles *systemTitles
|
||||
TaskCenterEventLogs *taskCenterEventLogs
|
||||
TaskCenterTaskRewards *taskCenterTaskRewards
|
||||
TaskCenterTaskTiers *taskCenterTaskTiers
|
||||
TaskCenterTasks *taskCenterTasks
|
||||
TaskCenterUserProgress *taskCenterUserProgress
|
||||
UserAddresses *userAddresses
|
||||
UserCouponLedger *userCouponLedger
|
||||
UserCoupons *userCoupons
|
||||
@ -95,13 +91,11 @@ func SetDefault(db *gorm.DB, opts ...gen.DOOption) {
|
||||
AuditRollbackLogs = &Q.AuditRollbackLogs
|
||||
Banner = &Q.Banner
|
||||
Channels = &Q.Channels
|
||||
DouyinOrders = &Q.DouyinOrders
|
||||
GamePassPackages = &Q.GamePassPackages
|
||||
GameTicketLogs = &Q.GameTicketLogs
|
||||
IssuePositionClaims = &Q.IssuePositionClaims
|
||||
LogOperation = &Q.LogOperation
|
||||
LogRequest = &Q.LogRequest
|
||||
LotteryRefundLogs = &Q.LotteryRefundLogs
|
||||
MatchingCardTypes = &Q.MatchingCardTypes
|
||||
MenuActions = &Q.MenuActions
|
||||
Menus = &Q.Menus
|
||||
@ -128,11 +122,9 @@ func SetDefault(db *gorm.DB, opts ...gen.DOOption) {
|
||||
SystemItemCards = &Q.SystemItemCards
|
||||
SystemTitleEffects = &Q.SystemTitleEffects
|
||||
SystemTitles = &Q.SystemTitles
|
||||
TaskCenterEventLogs = &Q.TaskCenterEventLogs
|
||||
TaskCenterTaskRewards = &Q.TaskCenterTaskRewards
|
||||
TaskCenterTaskTiers = &Q.TaskCenterTaskTiers
|
||||
TaskCenterTasks = &Q.TaskCenterTasks
|
||||
TaskCenterUserProgress = &Q.TaskCenterUserProgress
|
||||
UserAddresses = &Q.UserAddresses
|
||||
UserCouponLedger = &Q.UserCouponLedger
|
||||
UserCoupons = &Q.UserCoupons
|
||||
@ -163,13 +155,11 @@ func Use(db *gorm.DB, opts ...gen.DOOption) *Query {
|
||||
AuditRollbackLogs: newAuditRollbackLogs(db, opts...),
|
||||
Banner: newBanner(db, opts...),
|
||||
Channels: newChannels(db, opts...),
|
||||
DouyinOrders: newDouyinOrders(db, opts...),
|
||||
GamePassPackages: newGamePassPackages(db, opts...),
|
||||
GameTicketLogs: newGameTicketLogs(db, opts...),
|
||||
IssuePositionClaims: newIssuePositionClaims(db, opts...),
|
||||
LogOperation: newLogOperation(db, opts...),
|
||||
LogRequest: newLogRequest(db, opts...),
|
||||
LotteryRefundLogs: newLotteryRefundLogs(db, opts...),
|
||||
MatchingCardTypes: newMatchingCardTypes(db, opts...),
|
||||
MenuActions: newMenuActions(db, opts...),
|
||||
Menus: newMenus(db, opts...),
|
||||
@ -196,11 +186,9 @@ func Use(db *gorm.DB, opts ...gen.DOOption) *Query {
|
||||
SystemItemCards: newSystemItemCards(db, opts...),
|
||||
SystemTitleEffects: newSystemTitleEffects(db, opts...),
|
||||
SystemTitles: newSystemTitles(db, opts...),
|
||||
TaskCenterEventLogs: newTaskCenterEventLogs(db, opts...),
|
||||
TaskCenterTaskRewards: newTaskCenterTaskRewards(db, opts...),
|
||||
TaskCenterTaskTiers: newTaskCenterTaskTiers(db, opts...),
|
||||
TaskCenterTasks: newTaskCenterTasks(db, opts...),
|
||||
TaskCenterUserProgress: newTaskCenterUserProgress(db, opts...),
|
||||
UserAddresses: newUserAddresses(db, opts...),
|
||||
UserCouponLedger: newUserCouponLedger(db, opts...),
|
||||
UserCoupons: newUserCoupons(db, opts...),
|
||||
@ -232,13 +220,11 @@ type Query struct {
|
||||
AuditRollbackLogs auditRollbackLogs
|
||||
Banner banner
|
||||
Channels channels
|
||||
DouyinOrders douyinOrders
|
||||
GamePassPackages gamePassPackages
|
||||
GameTicketLogs gameTicketLogs
|
||||
IssuePositionClaims issuePositionClaims
|
||||
LogOperation logOperation
|
||||
LogRequest logRequest
|
||||
LotteryRefundLogs lotteryRefundLogs
|
||||
MatchingCardTypes matchingCardTypes
|
||||
MenuActions menuActions
|
||||
Menus menus
|
||||
@ -265,11 +251,9 @@ type Query struct {
|
||||
SystemItemCards systemItemCards
|
||||
SystemTitleEffects systemTitleEffects
|
||||
SystemTitles systemTitles
|
||||
TaskCenterEventLogs taskCenterEventLogs
|
||||
TaskCenterTaskRewards taskCenterTaskRewards
|
||||
TaskCenterTaskTiers taskCenterTaskTiers
|
||||
TaskCenterTasks taskCenterTasks
|
||||
TaskCenterUserProgress taskCenterUserProgress
|
||||
UserAddresses userAddresses
|
||||
UserCouponLedger userCouponLedger
|
||||
UserCoupons userCoupons
|
||||
@ -302,13 +286,11 @@ func (q *Query) clone(db *gorm.DB) *Query {
|
||||
AuditRollbackLogs: q.AuditRollbackLogs.clone(db),
|
||||
Banner: q.Banner.clone(db),
|
||||
Channels: q.Channels.clone(db),
|
||||
DouyinOrders: q.DouyinOrders.clone(db),
|
||||
GamePassPackages: q.GamePassPackages.clone(db),
|
||||
GameTicketLogs: q.GameTicketLogs.clone(db),
|
||||
IssuePositionClaims: q.IssuePositionClaims.clone(db),
|
||||
LogOperation: q.LogOperation.clone(db),
|
||||
LogRequest: q.LogRequest.clone(db),
|
||||
LotteryRefundLogs: q.LotteryRefundLogs.clone(db),
|
||||
MatchingCardTypes: q.MatchingCardTypes.clone(db),
|
||||
MenuActions: q.MenuActions.clone(db),
|
||||
Menus: q.Menus.clone(db),
|
||||
@ -335,11 +317,9 @@ func (q *Query) clone(db *gorm.DB) *Query {
|
||||
SystemItemCards: q.SystemItemCards.clone(db),
|
||||
SystemTitleEffects: q.SystemTitleEffects.clone(db),
|
||||
SystemTitles: q.SystemTitles.clone(db),
|
||||
TaskCenterEventLogs: q.TaskCenterEventLogs.clone(db),
|
||||
TaskCenterTaskRewards: q.TaskCenterTaskRewards.clone(db),
|
||||
TaskCenterTaskTiers: q.TaskCenterTaskTiers.clone(db),
|
||||
TaskCenterTasks: q.TaskCenterTasks.clone(db),
|
||||
TaskCenterUserProgress: q.TaskCenterUserProgress.clone(db),
|
||||
UserAddresses: q.UserAddresses.clone(db),
|
||||
UserCouponLedger: q.UserCouponLedger.clone(db),
|
||||
UserCoupons: q.UserCoupons.clone(db),
|
||||
@ -379,13 +359,11 @@ func (q *Query) ReplaceDB(db *gorm.DB) *Query {
|
||||
AuditRollbackLogs: q.AuditRollbackLogs.replaceDB(db),
|
||||
Banner: q.Banner.replaceDB(db),
|
||||
Channels: q.Channels.replaceDB(db),
|
||||
DouyinOrders: q.DouyinOrders.replaceDB(db),
|
||||
GamePassPackages: q.GamePassPackages.replaceDB(db),
|
||||
GameTicketLogs: q.GameTicketLogs.replaceDB(db),
|
||||
IssuePositionClaims: q.IssuePositionClaims.replaceDB(db),
|
||||
LogOperation: q.LogOperation.replaceDB(db),
|
||||
LogRequest: q.LogRequest.replaceDB(db),
|
||||
LotteryRefundLogs: q.LotteryRefundLogs.replaceDB(db),
|
||||
MatchingCardTypes: q.MatchingCardTypes.replaceDB(db),
|
||||
MenuActions: q.MenuActions.replaceDB(db),
|
||||
Menus: q.Menus.replaceDB(db),
|
||||
@ -412,11 +390,9 @@ func (q *Query) ReplaceDB(db *gorm.DB) *Query {
|
||||
SystemItemCards: q.SystemItemCards.replaceDB(db),
|
||||
SystemTitleEffects: q.SystemTitleEffects.replaceDB(db),
|
||||
SystemTitles: q.SystemTitles.replaceDB(db),
|
||||
TaskCenterEventLogs: q.TaskCenterEventLogs.replaceDB(db),
|
||||
TaskCenterTaskRewards: q.TaskCenterTaskRewards.replaceDB(db),
|
||||
TaskCenterTaskTiers: q.TaskCenterTaskTiers.replaceDB(db),
|
||||
TaskCenterTasks: q.TaskCenterTasks.replaceDB(db),
|
||||
TaskCenterUserProgress: q.TaskCenterUserProgress.replaceDB(db),
|
||||
UserAddresses: q.UserAddresses.replaceDB(db),
|
||||
UserCouponLedger: q.UserCouponLedger.replaceDB(db),
|
||||
UserCoupons: q.UserCoupons.replaceDB(db),
|
||||
@ -446,13 +422,11 @@ type queryCtx struct {
|
||||
AuditRollbackLogs *auditRollbackLogsDo
|
||||
Banner *bannerDo
|
||||
Channels *channelsDo
|
||||
DouyinOrders *douyinOrdersDo
|
||||
GamePassPackages *gamePassPackagesDo
|
||||
GameTicketLogs *gameTicketLogsDo
|
||||
IssuePositionClaims *issuePositionClaimsDo
|
||||
LogOperation *logOperationDo
|
||||
LogRequest *logRequestDo
|
||||
LotteryRefundLogs *lotteryRefundLogsDo
|
||||
MatchingCardTypes *matchingCardTypesDo
|
||||
MenuActions *menuActionsDo
|
||||
Menus *menusDo
|
||||
@ -479,11 +453,9 @@ type queryCtx struct {
|
||||
SystemItemCards *systemItemCardsDo
|
||||
SystemTitleEffects *systemTitleEffectsDo
|
||||
SystemTitles *systemTitlesDo
|
||||
TaskCenterEventLogs *taskCenterEventLogsDo
|
||||
TaskCenterTaskRewards *taskCenterTaskRewardsDo
|
||||
TaskCenterTaskTiers *taskCenterTaskTiersDo
|
||||
TaskCenterTasks *taskCenterTasksDo
|
||||
TaskCenterUserProgress *taskCenterUserProgressDo
|
||||
UserAddresses *userAddressesDo
|
||||
UserCouponLedger *userCouponLedgerDo
|
||||
UserCoupons *userCouponsDo
|
||||
@ -513,13 +485,11 @@ func (q *Query) WithContext(ctx context.Context) *queryCtx {
|
||||
AuditRollbackLogs: q.AuditRollbackLogs.WithContext(ctx),
|
||||
Banner: q.Banner.WithContext(ctx),
|
||||
Channels: q.Channels.WithContext(ctx),
|
||||
DouyinOrders: q.DouyinOrders.WithContext(ctx),
|
||||
GamePassPackages: q.GamePassPackages.WithContext(ctx),
|
||||
GameTicketLogs: q.GameTicketLogs.WithContext(ctx),
|
||||
IssuePositionClaims: q.IssuePositionClaims.WithContext(ctx),
|
||||
LogOperation: q.LogOperation.WithContext(ctx),
|
||||
LogRequest: q.LogRequest.WithContext(ctx),
|
||||
LotteryRefundLogs: q.LotteryRefundLogs.WithContext(ctx),
|
||||
MatchingCardTypes: q.MatchingCardTypes.WithContext(ctx),
|
||||
MenuActions: q.MenuActions.WithContext(ctx),
|
||||
Menus: q.Menus.WithContext(ctx),
|
||||
@ -546,11 +516,9 @@ func (q *Query) WithContext(ctx context.Context) *queryCtx {
|
||||
SystemItemCards: q.SystemItemCards.WithContext(ctx),
|
||||
SystemTitleEffects: q.SystemTitleEffects.WithContext(ctx),
|
||||
SystemTitles: q.SystemTitles.WithContext(ctx),
|
||||
TaskCenterEventLogs: q.TaskCenterEventLogs.WithContext(ctx),
|
||||
TaskCenterTaskRewards: q.TaskCenterTaskRewards.WithContext(ctx),
|
||||
TaskCenterTaskTiers: q.TaskCenterTaskTiers.WithContext(ctx),
|
||||
TaskCenterTasks: q.TaskCenterTasks.WithContext(ctx),
|
||||
TaskCenterUserProgress: q.TaskCenterUserProgress.WithContext(ctx),
|
||||
UserAddresses: q.UserAddresses.WithContext(ctx),
|
||||
UserCouponLedger: q.UserCouponLedger.WithContext(ctx),
|
||||
UserCoupons: q.UserCoupons.WithContext(ctx),
|
||||
|
||||
@ -32,6 +32,8 @@ func newSystemConfigs(db *gorm.DB, opts ...gen.DOOption) systemConfigs {
|
||||
_systemConfigs.UpdatedAt = field.NewTime(tableName, "updated_at")
|
||||
_systemConfigs.DeletedAt = field.NewField(tableName, "deleted_at")
|
||||
_systemConfigs.ConfigKey = field.NewString(tableName, "config_key")
|
||||
_systemConfigs.ConfigGroup = field.NewString(tableName, "config_group")
|
||||
_systemConfigs.IsEncrypted = field.NewBool(tableName, "is_encrypted")
|
||||
_systemConfigs.ConfigValue = field.NewString(tableName, "config_value")
|
||||
_systemConfigs.Remark = field.NewString(tableName, "remark")
|
||||
|
||||
@ -50,6 +52,8 @@ type systemConfigs struct {
|
||||
UpdatedAt field.Time
|
||||
DeletedAt field.Field
|
||||
ConfigKey field.String
|
||||
ConfigGroup field.String
|
||||
IsEncrypted field.Bool
|
||||
ConfigValue field.String
|
||||
Remark field.String
|
||||
|
||||
@ -73,6 +77,8 @@ func (s *systemConfigs) updateTableName(table string) *systemConfigs {
|
||||
s.UpdatedAt = field.NewTime(table, "updated_at")
|
||||
s.DeletedAt = field.NewField(table, "deleted_at")
|
||||
s.ConfigKey = field.NewString(table, "config_key")
|
||||
s.ConfigGroup = field.NewString(table, "config_group")
|
||||
s.IsEncrypted = field.NewBool(table, "is_encrypted")
|
||||
s.ConfigValue = field.NewString(table, "config_value")
|
||||
s.Remark = field.NewString(table, "remark")
|
||||
|
||||
@ -91,12 +97,14 @@ func (s *systemConfigs) GetFieldByName(fieldName string) (field.OrderExpr, bool)
|
||||
}
|
||||
|
||||
func (s *systemConfigs) fillFieldMap() {
|
||||
s.fieldMap = make(map[string]field.Expr, 7)
|
||||
s.fieldMap = make(map[string]field.Expr, 9)
|
||||
s.fieldMap["id"] = s.ID
|
||||
s.fieldMap["created_at"] = s.CreatedAt
|
||||
s.fieldMap["updated_at"] = s.UpdatedAt
|
||||
s.fieldMap["deleted_at"] = s.DeletedAt
|
||||
s.fieldMap["config_key"] = s.ConfigKey
|
||||
s.fieldMap["config_group"] = s.ConfigGroup
|
||||
s.fieldMap["is_encrypted"] = s.IsEncrypted
|
||||
s.fieldMap["config_value"] = s.ConfigValue
|
||||
s.fieldMap["remark"] = s.Remark
|
||||
}
|
||||
|
||||
@ -41,6 +41,7 @@ func newUsers(db *gorm.DB, opts ...gen.DOOption) users {
|
||||
_users.Status = field.NewInt32(tableName, "status")
|
||||
_users.DouyinID = field.NewString(tableName, "douyin_id")
|
||||
_users.ChannelID = field.NewInt64(tableName, "channel_id")
|
||||
_users.DouyinUserID = field.NewString(tableName, "douyin_user_id")
|
||||
|
||||
_users.fillFieldMap()
|
||||
|
||||
@ -51,21 +52,22 @@ func newUsers(db *gorm.DB, opts ...gen.DOOption) users {
|
||||
type users struct {
|
||||
usersDo
|
||||
|
||||
ALL field.Asterisk
|
||||
ID field.Int64 // 主键ID
|
||||
CreatedAt field.Time // 创建时间
|
||||
UpdatedAt field.Time // 更新时间
|
||||
DeletedAt field.Field // 删除时间(软删)
|
||||
Nickname field.String // 昵称
|
||||
Avatar field.String // 头像URL
|
||||
Mobile field.String // 手机号
|
||||
Openid field.String // 微信openid
|
||||
Unionid field.String // 微信unionid
|
||||
InviteCode field.String // 用户唯一邀请码
|
||||
InviterID field.Int64 // 邀请人用户ID
|
||||
Status field.Int32 // 状态:1正常 2禁用
|
||||
DouyinID field.String
|
||||
ChannelID field.Int64 // 渠道ID
|
||||
ALL field.Asterisk
|
||||
ID field.Int64 // 主键ID
|
||||
CreatedAt field.Time // 创建时间
|
||||
UpdatedAt field.Time // 更新时间
|
||||
DeletedAt field.Field // 删除时间(软删)
|
||||
Nickname field.String // 昵称
|
||||
Avatar field.String // 头像URL
|
||||
Mobile field.String // 手机号
|
||||
Openid field.String // 微信openid
|
||||
Unionid field.String // 微信unionid
|
||||
InviteCode field.String // 用户唯一邀请码
|
||||
InviterID field.Int64 // 邀请人用户ID
|
||||
Status field.Int32 // 状态:1正常 2禁用
|
||||
DouyinID field.String
|
||||
ChannelID field.Int64 // 渠道ID
|
||||
DouyinUserID field.String
|
||||
|
||||
fieldMap map[string]field.Expr
|
||||
}
|
||||
@ -96,6 +98,7 @@ func (u *users) updateTableName(table string) *users {
|
||||
u.Status = field.NewInt32(table, "status")
|
||||
u.DouyinID = field.NewString(table, "douyin_id")
|
||||
u.ChannelID = field.NewInt64(table, "channel_id")
|
||||
u.DouyinUserID = field.NewString(table, "douyin_user_id")
|
||||
|
||||
u.fillFieldMap()
|
||||
|
||||
@ -112,7 +115,7 @@ func (u *users) GetFieldByName(fieldName string) (field.OrderExpr, bool) {
|
||||
}
|
||||
|
||||
func (u *users) fillFieldMap() {
|
||||
u.fieldMap = make(map[string]field.Expr, 14)
|
||||
u.fieldMap = make(map[string]field.Expr, 15)
|
||||
u.fieldMap["id"] = u.ID
|
||||
u.fieldMap["created_at"] = u.CreatedAt
|
||||
u.fieldMap["updated_at"] = u.UpdatedAt
|
||||
@ -127,6 +130,7 @@ func (u *users) fillFieldMap() {
|
||||
u.fieldMap["status"] = u.Status
|
||||
u.fieldMap["douyin_id"] = u.DouyinID
|
||||
u.fieldMap["channel_id"] = u.ChannelID
|
||||
u.fieldMap["douyin_user_id"] = u.DouyinUserID
|
||||
}
|
||||
|
||||
func (u users) clone(db *gorm.DB) users {
|
||||
|
||||
@ -13,15 +13,16 @@ const TableNameDouyinOrders = "douyin_orders"
|
||||
// DouyinOrders 抖店订单表
|
||||
type DouyinOrders struct {
|
||||
ID int64 `gorm:"column:id;primaryKey;autoIncrement:true" json:"id"`
|
||||
ShopOrderID string `gorm:"column:shop_order_id;not null;comment:抖店订单号" json:"shop_order_id"` // 抖店订单号
|
||||
OrderStatus int32 `gorm:"column:order_status;not null;comment:订单状态: 5=已完成" json:"order_status"` // 订单状态: 5=已完成
|
||||
DouyinUserID string `gorm:"column:douyin_user_id;not null;comment:抖店用户ID" json:"douyin_user_id"` // 抖店用户ID
|
||||
LocalUserID string `gorm:"column:local_user_id;default:0;comment:匹配到的本地用户ID" json:"local_user_id"` // 匹配到的本地用户ID
|
||||
ActualReceiveAmount int64 `gorm:"column:actual_receive_amount;comment:实收金额(分)" json:"actual_receive_amount"` // 实收金额(分)
|
||||
PayTypeDesc string `gorm:"column:pay_type_desc;comment:支付方式描述" json:"pay_type_desc"` // 支付方式描述
|
||||
Remark string `gorm:"column:remark;comment:备注" json:"remark"` // 备注
|
||||
UserNickname string `gorm:"column:user_nickname;comment:抖音昵称" json:"user_nickname"` // 抖音昵称
|
||||
RawData string `gorm:"column:raw_data;comment:原始响应数据" json:"raw_data"` // 原始响应数据
|
||||
ShopOrderID string `gorm:"column:shop_order_id;not null;comment:抖店订单号" json:"shop_order_id"` // 抖店订单号
|
||||
OrderStatus int32 `gorm:"column:order_status;not null;comment:订单状态: 5=已完成" json:"order_status"` // 订单状态: 5=已完成
|
||||
DouyinUserID string `gorm:"column:douyin_user_id;not null;comment:抖店用户ID" json:"douyin_user_id"` // 抖店用户ID
|
||||
LocalUserID string `gorm:"column:local_user_id;default:0;comment:匹配到的本地用户ID" json:"local_user_id"` // 匹配到的本地用户ID
|
||||
ActualReceiveAmount int64 `gorm:"column:actual_receive_amount;comment:实收金额(分)" json:"actual_receive_amount"` // 实收金额(分)
|
||||
PayTypeDesc string `gorm:"column:pay_type_desc;comment:支付方式描述" json:"pay_type_desc"` // 支付方式描述
|
||||
Remark string `gorm:"column:remark;comment:备注" json:"remark"` // 备注
|
||||
UserNickname string `gorm:"column:user_nickname;comment:抖音昵称" json:"user_nickname"` // 抖音昵称
|
||||
RawData string `gorm:"column:raw_data;comment:原始响应数据" json:"raw_data"` // 原始响应数据
|
||||
RewardGranted int32 `gorm:"column:reward_granted;not null;default:0;comment:奖励已发放: 0=否, 1=是" json:"reward_granted"` // 奖励已发放
|
||||
CreatedAt time.Time `gorm:"column:created_at;default:CURRENT_TIMESTAMP(3)" json:"created_at"`
|
||||
UpdatedAt time.Time `gorm:"column:updated_at;default:CURRENT_TIMESTAMP(3)" json:"updated_at"`
|
||||
}
|
||||
|
||||
26
internal/repository/mysql/model/douyin_product_rewards.go
Normal file
26
internal/repository/mysql/model/douyin_product_rewards.go
Normal file
@ -0,0 +1,26 @@
|
||||
package model
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"gorm.io/datatypes"
|
||||
)
|
||||
|
||||
const TableNameDouyinProductRewards = "douyin_product_rewards"
|
||||
|
||||
// DouyinProductRewards 抖店商品奖励规则表
|
||||
type DouyinProductRewards struct {
|
||||
ID int64 `gorm:"column:id;primaryKey;autoIncrement:true" json:"id"`
|
||||
ProductID string `gorm:"column:product_id;not null;uniqueIndex:uk_product_id;comment:抖店商品ID" json:"product_id"`
|
||||
ProductName string `gorm:"column:product_name;not null;default:'';comment:商品名称" json:"product_name"`
|
||||
RewardType string `gorm:"column:reward_type;not null;comment:奖励类型" json:"reward_type"`
|
||||
RewardPayload datatypes.JSON `gorm:"column:reward_payload;comment:奖励参数JSON" json:"reward_payload"`
|
||||
Quantity int32 `gorm:"column:quantity;not null;default:1;comment:发放数量" json:"quantity"`
|
||||
Status int32 `gorm:"column:status;not null;default:1;comment:状态: 1=启用 0=禁用" json:"status"`
|
||||
CreatedAt time.Time `gorm:"column:created_at;default:CURRENT_TIMESTAMP(3)" json:"created_at"`
|
||||
UpdatedAt time.Time `gorm:"column:updated_at;default:CURRENT_TIMESTAMP(3)" json:"updated_at"`
|
||||
}
|
||||
|
||||
func (*DouyinProductRewards) TableName() string {
|
||||
return TableNameDouyinProductRewards
|
||||
}
|
||||
@ -19,6 +19,8 @@ type SystemConfigs struct {
|
||||
UpdatedAt time.Time `gorm:"column:updated_at;not null;default:CURRENT_TIMESTAMP(3)" json:"updated_at"`
|
||||
DeletedAt gorm.DeletedAt `gorm:"column:deleted_at" json:"deleted_at"`
|
||||
ConfigKey string `gorm:"column:config_key;not null" json:"config_key"`
|
||||
ConfigGroup string `gorm:"column:config_group;default:default" json:"config_group"`
|
||||
IsEncrypted bool `gorm:"column:is_encrypted" json:"is_encrypted"`
|
||||
ConfigValue string `gorm:"column:config_value;not null" json:"config_value"`
|
||||
Remark string `gorm:"column:remark" json:"remark"`
|
||||
}
|
||||
|
||||
@ -14,20 +14,21 @@ const TableNameUsers = "users"
|
||||
|
||||
// Users 用户表
|
||||
type Users struct {
|
||||
ID int64 `gorm:"column:id;primaryKey;autoIncrement:true;comment:主键ID" json:"id"` // 主键ID
|
||||
CreatedAt time.Time `gorm:"column:created_at;not null;default:CURRENT_TIMESTAMP(3);comment:创建时间" json:"created_at"` // 创建时间
|
||||
UpdatedAt time.Time `gorm:"column:updated_at;not null;default:CURRENT_TIMESTAMP(3);comment:更新时间" json:"updated_at"` // 更新时间
|
||||
DeletedAt gorm.DeletedAt `gorm:"column:deleted_at;comment:删除时间(软删)" json:"deleted_at"` // 删除时间(软删)
|
||||
Nickname string `gorm:"column:nickname;not null;comment:昵称" json:"nickname"` // 昵称
|
||||
Avatar string `gorm:"column:avatar;comment:头像URL" json:"avatar"` // 头像URL
|
||||
Mobile string `gorm:"column:mobile;comment:手机号" json:"mobile"` // 手机号
|
||||
Openid string `gorm:"column:openid;comment:微信openid" json:"openid"` // 微信openid
|
||||
Unionid string `gorm:"column:unionid;comment:微信unionid" json:"unionid"` // 微信unionid
|
||||
InviteCode string `gorm:"column:invite_code;not null;comment:用户唯一邀请码" json:"invite_code"` // 用户唯一邀请码
|
||||
InviterID int64 `gorm:"column:inviter_id;comment:邀请人用户ID" json:"inviter_id"` // 邀请人用户ID
|
||||
Status int32 `gorm:"column:status;not null;default:1;comment:状态:1正常 2禁用" json:"status"` // 状态:1正常 2禁用
|
||||
DouyinID string `gorm:"column:douyin_id" json:"douyin_id"`
|
||||
ChannelID int64 `gorm:"column:channel_id;comment:渠道ID" json:"channel_id"` // 渠道ID
|
||||
ID int64 `gorm:"column:id;primaryKey;autoIncrement:true;comment:主键ID" json:"id"` // 主键ID
|
||||
CreatedAt time.Time `gorm:"column:created_at;not null;default:CURRENT_TIMESTAMP(3);comment:创建时间" json:"created_at"` // 创建时间
|
||||
UpdatedAt time.Time `gorm:"column:updated_at;not null;default:CURRENT_TIMESTAMP(3);comment:更新时间" json:"updated_at"` // 更新时间
|
||||
DeletedAt gorm.DeletedAt `gorm:"column:deleted_at;comment:删除时间(软删)" json:"deleted_at"` // 删除时间(软删)
|
||||
Nickname string `gorm:"column:nickname;not null;comment:昵称" json:"nickname"` // 昵称
|
||||
Avatar string `gorm:"column:avatar;comment:头像URL" json:"avatar"` // 头像URL
|
||||
Mobile string `gorm:"column:mobile;comment:手机号" json:"mobile"` // 手机号
|
||||
Openid string `gorm:"column:openid;comment:微信openid" json:"openid"` // 微信openid
|
||||
Unionid string `gorm:"column:unionid;comment:微信unionid" json:"unionid"` // 微信unionid
|
||||
InviteCode string `gorm:"column:invite_code;not null;comment:用户唯一邀请码" json:"invite_code"` // 用户唯一邀请码
|
||||
InviterID int64 `gorm:"column:inviter_id;comment:邀请人用户ID" json:"inviter_id"` // 邀请人用户ID
|
||||
Status int32 `gorm:"column:status;not null;default:1;comment:状态:1正常 2禁用" json:"status"` // 状态:1正常 2禁用
|
||||
DouyinID string `gorm:"column:douyin_id" json:"douyin_id"`
|
||||
ChannelID int64 `gorm:"column:channel_id;comment:渠道ID" json:"channel_id"` // 渠道ID
|
||||
DouyinUserID string `gorm:"column:douyin_user_id" json:"douyin_user_id"`
|
||||
}
|
||||
|
||||
// TableName Users's table name
|
||||
|
||||
@ -131,6 +131,8 @@ func NewHTTPMux(logger logger.CustomLogger, db mysql.Repo) (core.Mux, func(), er
|
||||
adminAuthApiRouter.GET("/dashboard/cards", adminHandler.DashboardCards())
|
||||
adminAuthApiRouter.GET("/dashboard/user_trend", adminHandler.DashboardUserTrend())
|
||||
adminAuthApiRouter.GET("/dashboard/draw_trend", adminHandler.DashboardDrawTrend())
|
||||
adminAuthApiRouter.GET("/dashboard/sales_draw_trend", adminHandler.DashboardSalesDrawTrend())
|
||||
adminAuthApiRouter.GET("/dashboard/retention_analytics", adminHandler.DashboardRetentionAnalytics())
|
||||
adminAuthApiRouter.GET("/dashboard/new_users", adminHandler.DashboardNewUsers())
|
||||
adminAuthApiRouter.GET("/dashboard/draw_stream", adminHandler.DashboardDrawStream())
|
||||
adminAuthApiRouter.GET("/dashboard/todos", adminHandler.DashboardTodos())
|
||||
@ -138,6 +140,21 @@ func NewHTTPMux(logger logger.CustomLogger, db mysql.Repo) (core.Mux, func(), er
|
||||
adminAuthApiRouter.GET("/dashboard/activities", adminHandler.DashboardActivities())
|
||||
adminAuthApiRouter.GET("/dashboard/activity_prize_analysis", adminHandler.DashboardActivityPrizeAnalysis())
|
||||
adminAuthApiRouter.GET("/dashboard/user_overview", adminHandler.DashboardUserOverview())
|
||||
|
||||
// 运营分析
|
||||
adminAuthApiRouter.GET("/operations/user_economics", adminHandler.DashboardUserEconomics())
|
||||
adminAuthApiRouter.GET("/operations/prize_distribution", adminHandler.DashboardPrizeDistribution())
|
||||
adminAuthApiRouter.GET("/dashboard/product_performance", adminHandler.OperationsProductPerformance())
|
||||
adminAuthApiRouter.GET("/dashboard/coupon_effectiveness", adminHandler.OperationsCouponEffectiveness())
|
||||
adminAuthApiRouter.GET("/dashboard/points_economy_summary", adminHandler.OperationsPointsEconomySummary())
|
||||
adminAuthApiRouter.GET("/dashboard/points_trend", adminHandler.OperationsPointsTrend())
|
||||
adminAuthApiRouter.GET("/dashboard/points_structure", adminHandler.OperationsPointsStructure())
|
||||
adminAuthApiRouter.GET("/dashboard/inventory_alerts", adminHandler.OperationsInventoryAlerts())
|
||||
adminAuthApiRouter.GET("/dashboard/risk_events", adminHandler.OperationsRiskEvents())
|
||||
adminAuthApiRouter.GET("/dashboard/live_winners", adminHandler.DashboardLiveWinners())
|
||||
adminAuthApiRouter.GET("/dashboard/order_trend", adminHandler.DashboardOrderTrend())
|
||||
adminAuthApiRouter.GET("/dashboard/activity_stats", adminHandler.DashboardActivityStats())
|
||||
adminAuthApiRouter.GET("/dashboard/item_card_sales", adminHandler.DashboardItemCardSales())
|
||||
adminAuthApiRouter.POST("/activities", intc.RequireAdminAction("activity:create"), adminHandler.CreateActivity())
|
||||
adminAuthApiRouter.GET("/activities", intc.RequireAdminAction("activity:view"), adminHandler.ListActivities())
|
||||
adminAuthApiRouter.PUT("/activities/:activity_id", intc.RequireAdminAction("activity:modify"), adminHandler.ModifyActivity())
|
||||
@ -188,6 +205,11 @@ func NewHTTPMux(logger logger.CustomLogger, db mysql.Repo) (core.Mux, func(), er
|
||||
adminAuthApiRouter.PUT("/douyin/config", adminHandler.SaveDouyinConfig())
|
||||
adminAuthApiRouter.GET("/douyin/orders", adminHandler.ListDouyinOrders())
|
||||
adminAuthApiRouter.POST("/douyin/sync", adminHandler.SyncDouyinOrders())
|
||||
// 抖店商品奖励规则
|
||||
adminAuthApiRouter.GET("/douyin/product-rewards", adminHandler.ListDouyinProductRewards())
|
||||
adminAuthApiRouter.POST("/douyin/product-rewards", adminHandler.CreateDouyinProductReward())
|
||||
adminAuthApiRouter.PUT("/douyin/product-rewards/:id", adminHandler.UpdateDouyinProductReward())
|
||||
adminAuthApiRouter.DELETE("/douyin/product-rewards/:id", adminHandler.DeleteDouyinProductReward())
|
||||
|
||||
// 系统配置KV
|
||||
adminAuthApiRouter.GET("/system/configs", adminHandler.ListSystemConfigs())
|
||||
@ -383,6 +405,7 @@ func NewHTTPMux(logger logger.CustomLogger, db mysql.Repo) (core.Mux, func(), er
|
||||
appAuthApiRouter.GET("/users/:user_id/stats", userHandler.GetUserStats())
|
||||
appAuthApiRouter.POST("/users/:user_id/phone/bind", userHandler.BindPhone())
|
||||
appAuthApiRouter.POST("/users/:user_id/douyin/phone/bind", userHandler.DouyinBindPhone())
|
||||
appAuthApiRouter.POST("/users/douyin/bind", userHandler.BindDouyinOrder())
|
||||
appAuthApiRouter.GET("/users/:user_id/invites", userHandler.ListUserInvites())
|
||||
appAuthApiRouter.GET("/users/:user_id/inventory", userHandler.ListUserInventory())
|
||||
appAuthApiRouter.GET("/users/:user_id/shipments", userHandler.ListUserShipments())
|
||||
|
||||
@ -15,7 +15,7 @@ type Service interface {
|
||||
Modify(ctx context.Context, id int64, in ModifyInput) error
|
||||
Delete(ctx context.Context, id int64) error
|
||||
List(ctx context.Context, in ListInput) (items []*ChannelWithStat, total int64, err error)
|
||||
GetStats(ctx context.Context, channelID int64, days int) (*StatsOutput, error)
|
||||
GetStats(ctx context.Context, channelID int64, days int, startDate, endDate string) (*StatsOutput, error)
|
||||
}
|
||||
|
||||
type service struct {
|
||||
@ -160,14 +160,32 @@ func (s *service) List(ctx context.Context, in ListInput) (items []*ChannelWithS
|
||||
return
|
||||
}
|
||||
|
||||
func (s *service) GetStats(ctx context.Context, channelID int64, months int) (*StatsOutput, error) {
|
||||
if months <= 0 {
|
||||
months = 12
|
||||
}
|
||||
func (s *service) GetStats(ctx context.Context, channelID int64, months int, startDateStr, endDateStr string) (*StatsOutput, error) {
|
||||
now := time.Now()
|
||||
// Calculate start date (first day of the month N months ago)
|
||||
startMonth := now.AddDate(0, -months+1, 0)
|
||||
startDate := time.Date(startMonth.Year(), startMonth.Month(), 1, 0, 0, 0, 0, startMonth.Location())
|
||||
var startDate, endDate time.Time
|
||||
|
||||
// 如果指定了日期范围,使用自定义日期
|
||||
if startDateStr != "" && endDateStr != "" {
|
||||
var err error
|
||||
startDate, err = time.Parse("2006-01-02", startDateStr)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
endDate, err = time.Parse("2006-01-02", endDateStr)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// 确保 endDate 是当天结束
|
||||
endDate = endDate.Add(24*time.Hour - time.Second)
|
||||
} else {
|
||||
// 默认按月份计算
|
||||
if months <= 0 {
|
||||
months = 12
|
||||
}
|
||||
startMonth := now.AddDate(0, -months+1, 0)
|
||||
startDate = time.Date(startMonth.Year(), startMonth.Month(), 1, 0, 0, 0, 0, startMonth.Location())
|
||||
endDate = now
|
||||
}
|
||||
|
||||
out := &StatsOutput{}
|
||||
|
||||
|
||||
@ -5,13 +5,14 @@ import (
|
||||
"bindbox-game/internal/repository/mysql"
|
||||
"bindbox-game/internal/repository/mysql/dao"
|
||||
"bindbox-game/internal/repository/mysql/model"
|
||||
"bindbox-game/internal/service/game"
|
||||
"bindbox-game/internal/service/sysconfig"
|
||||
"compress/gzip"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
@ -34,6 +35,8 @@ type Service interface {
|
||||
GetConfig(ctx context.Context) (*DouyinConfig, error)
|
||||
// SaveConfig 保存抖店配置
|
||||
SaveConfig(ctx context.Context, cookie string, intervalMinutes int) error
|
||||
// SyncOrder 同步单个订单到本地,可传入建议关联的用户ID
|
||||
SyncOrder(ctx context.Context, item *DouyinOrderItem, suggestUserID int64) (isNew bool, isMatched bool)
|
||||
}
|
||||
|
||||
type DouyinConfig struct {
|
||||
@ -48,20 +51,22 @@ type SyncResult struct {
|
||||
}
|
||||
|
||||
type service struct {
|
||||
logger logger.CustomLogger
|
||||
repo mysql.Repo
|
||||
readDB *dao.Query
|
||||
writeDB *dao.Query
|
||||
syscfg sysconfig.Service
|
||||
logger logger.CustomLogger
|
||||
repo mysql.Repo
|
||||
readDB *dao.Query
|
||||
writeDB *dao.Query
|
||||
syscfg sysconfig.Service
|
||||
ticketSvc game.TicketService
|
||||
}
|
||||
|
||||
func New(l logger.CustomLogger, repo mysql.Repo, syscfg sysconfig.Service) Service {
|
||||
func New(l logger.CustomLogger, repo mysql.Repo, syscfg sysconfig.Service, ticketSvc game.TicketService) Service {
|
||||
return &service{
|
||||
logger: l,
|
||||
repo: repo,
|
||||
readDB: dao.Use(repo.GetDbR()),
|
||||
writeDB: dao.Use(repo.GetDbW()),
|
||||
syscfg: syscfg,
|
||||
logger: l,
|
||||
repo: repo,
|
||||
readDB: dao.Use(repo.GetDbR()),
|
||||
writeDB: dao.Use(repo.GetDbW()),
|
||||
syscfg: syscfg,
|
||||
ticketSvc: ticketSvc,
|
||||
}
|
||||
}
|
||||
|
||||
@ -121,7 +126,7 @@ func (s *service) ListOrders(ctx context.Context, page, pageSize int, status *in
|
||||
return orders, total, nil
|
||||
}
|
||||
|
||||
// FetchAndSyncOrders 从抖店 API 获取订单并同步到本地
|
||||
// FetchAndSyncOrders 遍历所有已绑定抖音号的用户并同步其订单
|
||||
func (s *service) FetchAndSyncOrders(ctx context.Context) (*SyncResult, error) {
|
||||
cfg, err := s.GetConfig(ctx)
|
||||
if err != nil {
|
||||
@ -131,45 +136,43 @@ func (s *service) FetchAndSyncOrders(ctx context.Context) (*SyncResult, error) {
|
||||
return nil, fmt.Errorf("抖店 Cookie 未配置")
|
||||
}
|
||||
|
||||
// 调用抖店 API 获取订单
|
||||
orders, err := s.fetchDouyinOrders(cfg.Cookie)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("获取抖店订单失败: %w", err)
|
||||
// 1. 获取所有绑定了抖音号的用户
|
||||
var users []model.Users
|
||||
if err := s.repo.GetDbR().WithContext(ctx).Where("douyin_user_id != ''").Find(&users).Error; err != nil {
|
||||
return nil, fmt.Errorf("获取绑定用户失败: %w", err)
|
||||
}
|
||||
|
||||
result := &SyncResult{TotalFetched: len(orders)}
|
||||
result := &SyncResult{}
|
||||
fmt.Printf("[DEBUG] 开始全量同步,共 %d 个绑定用户\n", len(users))
|
||||
|
||||
// 统计各状态订单数量
|
||||
statusCount := make(map[int]int)
|
||||
for _, order := range orders {
|
||||
statusCount[order.OrderStatus]++
|
||||
}
|
||||
s.logger.Info("[抖店同步] 订单状态分布",
|
||||
zap.Any("status_count", statusCount),
|
||||
)
|
||||
// 2. 遍历用户,按 buyer 抓取订单
|
||||
for _, u := range users {
|
||||
fmt.Printf("[DEBUG] 正在同步用户 ID: %d (昵称: %s, 抖音号: %s) 的订单...\n", u.ID, u.Nickname, u.DouyinUserID)
|
||||
|
||||
// 同步订单到本地(只同步 order_status=5 已完成的订单)
|
||||
for _, order := range orders {
|
||||
if order.OrderStatus != 5 {
|
||||
// 跳过非已完成订单
|
||||
orders, err := s.fetchDouyinOrdersByBuyer(cfg.Cookie, u.DouyinUserID)
|
||||
if err != nil {
|
||||
fmt.Printf("[DEBUG] 抓取用户 %s 订单失败: %v\n", u.DouyinUserID, err)
|
||||
continue
|
||||
}
|
||||
isNew, matched := s.syncOrder(ctx, order)
|
||||
if isNew {
|
||||
result.NewOrders++
|
||||
s.logger.Info("[抖店同步] 新增订单",
|
||||
zap.String("shop_order_id", order.ShopOrderID),
|
||||
zap.Int("order_status", order.OrderStatus),
|
||||
)
|
||||
}
|
||||
if matched {
|
||||
result.MatchedUsers++
|
||||
|
||||
result.TotalFetched += len(orders)
|
||||
|
||||
// 3. 同步
|
||||
for _, order := range orders {
|
||||
// 同步订单(传入建议关联的用户 ID)
|
||||
isNew, matched := s.SyncOrder(ctx, &order, u.ID)
|
||||
if isNew {
|
||||
result.NewOrders++
|
||||
}
|
||||
if matched {
|
||||
result.MatchedUsers++
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
s.logger.Info("[抖店同步] 同步完成",
|
||||
s.logger.Info("[抖店同步] 全量同步完成",
|
||||
zap.Int("users_count", len(users)),
|
||||
zap.Int("total_fetched", result.TotalFetched),
|
||||
zap.Int("completed_orders", statusCount[5]),
|
||||
zap.Int("new_orders", result.NewOrders),
|
||||
zap.Int("matched_users", result.MatchedUsers),
|
||||
)
|
||||
@ -182,10 +185,10 @@ type douyinOrderResponse struct {
|
||||
Code int `json:"code"`
|
||||
St int `json:"st"` // 抖店实际返回的是 st 而非 code
|
||||
Msg string `json:"msg"`
|
||||
Data []douyinOrderItem `json:"data"` // data 直接是数组
|
||||
Data []DouyinOrderItem `json:"data"` // data 直接是数组
|
||||
}
|
||||
|
||||
type douyinOrderItem struct {
|
||||
type DouyinOrderItem struct {
|
||||
ShopOrderID string `json:"shop_order_id"`
|
||||
OrderStatus int `json:"order_status"`
|
||||
UserID string `json:"user_id"`
|
||||
@ -195,11 +198,24 @@ type douyinOrderItem struct {
|
||||
UserNickname string `json:"user_nickname"`
|
||||
}
|
||||
|
||||
// fetchDouyinOrders 调用抖店 API 获取订单
|
||||
func (s *service) fetchDouyinOrders(cookie string) ([]douyinOrderItem, error) {
|
||||
url := "https://fxg.jinritemai.com/api/order/searchlist?page=0&pageSize=50&order_by=create_time&order=desc&tab=all"
|
||||
// fetchDouyinOrdersByBuyer 调用抖店 API 按 Buyer ID 获取订单
|
||||
func (s *service) fetchDouyinOrdersByBuyer(cookie string, buyer string) ([]DouyinOrderItem, error) {
|
||||
// 拼接带有业务标识的搜索 URL
|
||||
baseUrl := "https://fxg.jinritemai.com/api/order/searchlist"
|
||||
params := url.Values{}
|
||||
params.Set("page", "0")
|
||||
params.Set("pageSize", "100")
|
||||
params.Set("buyer", buyer)
|
||||
params.Set("order_by", "create_time")
|
||||
params.Set("order", "desc")
|
||||
params.Set("tab", "all")
|
||||
params.Set("appid", "1")
|
||||
params.Set("_bid", "ffa_order")
|
||||
params.Set("aid", "4272")
|
||||
|
||||
req, err := http.NewRequest("GET", url, nil)
|
||||
fullUrl := baseUrl + "?" + params.Encode()
|
||||
|
||||
req, err := http.NewRequest("GET", fullUrl, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@ -207,7 +223,6 @@ func (s *service) fetchDouyinOrders(cookie string) ([]douyinOrderItem, error) {
|
||||
// 设置请求头
|
||||
req.Header.Set("User-Agent", "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/136.0.0.0 Safari/537.36")
|
||||
req.Header.Set("Accept", "application/json, text/plain, */*")
|
||||
req.Header.Set("Accept-Encoding", "gzip, deflate, br")
|
||||
req.Header.Set("Cookie", cookie)
|
||||
req.Header.Set("Referer", "https://fxg.jinritemai.com/ffa/morder/order/list")
|
||||
|
||||
@ -218,98 +233,116 @@ func (s *service) fetchDouyinOrders(cookie string) ([]douyinOrderItem, error) {
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
// 处理 gzip 响应
|
||||
var reader io.Reader = resp.Body
|
||||
if resp.Header.Get("Content-Encoding") == "gzip" {
|
||||
gzReader, err := gzip.NewReader(resp.Body)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("gzip 解压失败: %w", err)
|
||||
}
|
||||
defer gzReader.Close()
|
||||
reader = gzReader
|
||||
}
|
||||
|
||||
body, err := io.ReadAll(reader)
|
||||
body, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var respData douyinOrderResponse
|
||||
if err := json.Unmarshal(body, &respData); err != nil {
|
||||
// 返回原始响应帮助调试
|
||||
s.logger.Error("[抖店API] 解析响应失败", zap.String("response", string(body[:min(len(body), 500)])))
|
||||
return nil, fmt.Errorf("解析响应失败: %w", err)
|
||||
}
|
||||
|
||||
// 抖店使用 st 字段表示状态,0 表示成功
|
||||
if respData.St != 0 && respData.Code != 0 {
|
||||
return nil, fmt.Errorf("API 返回错误: %s", respData.Msg)
|
||||
}
|
||||
|
||||
s.logger.Info("[抖店API] 获取订单成功", zap.Int("count", len(respData.Data)))
|
||||
|
||||
return respData.Data, nil
|
||||
}
|
||||
|
||||
// syncOrder 同步单个订单到本地
|
||||
func (s *service) syncOrder(ctx context.Context, item douyinOrderItem) (isNew bool, isMatched bool) {
|
||||
// SyncOrder 同步单个订单到本地
|
||||
func (s *service) SyncOrder(ctx context.Context, item *DouyinOrderItem, suggestUserID int64) (isNew bool, isMatched bool) {
|
||||
db := s.repo.GetDbW().WithContext(ctx)
|
||||
|
||||
// 检查订单是否已存在
|
||||
var existing model.DouyinOrders
|
||||
if err := db.Where("shop_order_id = ?", item.ShopOrderID).First(&existing).Error; err == nil {
|
||||
// 订单已存在,更新状态
|
||||
db.Model(&existing).Updates(map[string]any{
|
||||
var order model.DouyinOrders
|
||||
err := db.Where("shop_order_id = ?", item.ShopOrderID).First(&order).Error
|
||||
|
||||
if err == nil {
|
||||
// 订单已存在
|
||||
isNew = false
|
||||
// 只有当订单还没关联用户,且提供了建议用户时,才做关联
|
||||
if (order.LocalUserID == "" || order.LocalUserID == "0") && suggestUserID > 0 {
|
||||
order.LocalUserID = strconv.FormatInt(suggestUserID, 10)
|
||||
db.Model(&order).Update("local_user_id", order.LocalUserID)
|
||||
fmt.Printf("[DEBUG] 抖店辅助关联成功: %s -> User %d\n", item.ShopOrderID, suggestUserID)
|
||||
}
|
||||
|
||||
// 更新状态
|
||||
db.Model(&order).Updates(map[string]any{
|
||||
"order_status": item.OrderStatus,
|
||||
"remark": item.Remark,
|
||||
})
|
||||
return false, existing.LocalUserID != "" && existing.LocalUserID != "0"
|
||||
}
|
||||
// 重要:同步内存状态,防止后续判断逻辑失效
|
||||
order.OrderStatus = int32(item.OrderStatus)
|
||||
order.Remark = item.Remark
|
||||
} else {
|
||||
// 订单不存在,创建新记录
|
||||
isNew = true
|
||||
localUserIDStr := "0"
|
||||
if suggestUserID > 0 {
|
||||
localUserIDStr = strconv.FormatInt(suggestUserID, 10)
|
||||
}
|
||||
|
||||
// 新订单,尝试匹配本地用户
|
||||
var localUserID int64
|
||||
var user model.Users
|
||||
// 尝试通过 douyin_id 匹配用户
|
||||
if item.UserID != "" {
|
||||
if err := s.repo.GetDbR().Where("douyin_id = ?", item.UserID).First(&user).Error; err == nil {
|
||||
localUserID = user.ID
|
||||
isMatched = true
|
||||
fmt.Printf("[DEBUG] 抖店新订单: %s, UserID: %s, Recommend: %s\n", item.ShopOrderID, item.UserID, localUserIDStr)
|
||||
|
||||
// 解析金额
|
||||
var amount int64
|
||||
if item.ActualReceiveAmount != "" {
|
||||
if f, err := strconv.ParseFloat(strings.TrimSpace(item.ActualReceiveAmount), 64); err == nil {
|
||||
amount = int64(f * 100)
|
||||
}
|
||||
}
|
||||
rawData, _ := json.Marshal(item)
|
||||
|
||||
order = model.DouyinOrders{
|
||||
ShopOrderID: item.ShopOrderID,
|
||||
OrderStatus: int32(item.OrderStatus),
|
||||
DouyinUserID: item.UserID,
|
||||
ActualReceiveAmount: amount,
|
||||
PayTypeDesc: item.PayTypeDesc,
|
||||
Remark: item.Remark,
|
||||
UserNickname: item.UserNickname,
|
||||
RawData: string(rawData),
|
||||
RewardGranted: 0,
|
||||
LocalUserID: localUserIDStr,
|
||||
}
|
||||
|
||||
if err := db.Create(&order).Error; err != nil {
|
||||
s.logger.Error("[抖店同步] 创建订单失败", zap.String("shop_order_id", item.ShopOrderID), zap.Error(err))
|
||||
return false, false
|
||||
}
|
||||
}
|
||||
|
||||
// 解析金额 (抖店返回的是元,需要转换为分)
|
||||
var amount int64
|
||||
if item.ActualReceiveAmount != "" {
|
||||
if f, err := strconv.ParseFloat(strings.TrimSpace(item.ActualReceiveAmount), 64); err == nil {
|
||||
amount = int64(f * 100)
|
||||
// 如果还没关联用户(比如之前全量抓取的),尝试用抖店的 UID (long string) 匹配
|
||||
if (order.LocalUserID == "" || order.LocalUserID == "0") && item.UserID != "" {
|
||||
var user model.Users
|
||||
if err := s.repo.GetDbR().Where("douyin_user_id = ?", item.UserID).First(&user).Error; err == nil {
|
||||
order.LocalUserID = strconv.FormatInt(user.ID, 10)
|
||||
db.Model(&order).Update("local_user_id", order.LocalUserID)
|
||||
fmt.Printf("[DEBUG] 通过抖店 UID 匹配成功: User %d\n", user.ID)
|
||||
}
|
||||
}
|
||||
|
||||
// 序列化原始数据
|
||||
rawData, _ := json.Marshal(item)
|
||||
// ---- 统一处理:发放奖励 ----
|
||||
isMatched = order.LocalUserID != "" && order.LocalUserID != "0"
|
||||
if isMatched && order.RewardGranted == 0 && order.OrderStatus == 5 {
|
||||
localUserID, _ := strconv.ParseInt(order.LocalUserID, 10, 64)
|
||||
fmt.Printf("[DEBUG] 准备发放奖励: User: %d, ShopOrder: %s\n", localUserID, item.ShopOrderID)
|
||||
|
||||
// 创建订单记录
|
||||
order := &model.DouyinOrders{
|
||||
ShopOrderID: item.ShopOrderID,
|
||||
OrderStatus: int32(item.OrderStatus),
|
||||
DouyinUserID: item.UserID,
|
||||
LocalUserID: strconv.FormatInt(localUserID, 10),
|
||||
ActualReceiveAmount: amount,
|
||||
PayTypeDesc: item.PayTypeDesc,
|
||||
Remark: item.Remark,
|
||||
UserNickname: item.UserNickname,
|
||||
RawData: string(rawData),
|
||||
if localUserID > 0 && s.ticketSvc != nil {
|
||||
err := s.ticketSvc.GrantTicket(ctx, localUserID, "minesweeper", 1, "douyin_order", order.ID, "抖店订单奖励")
|
||||
if err == nil {
|
||||
db.Model(&order).Update("reward_granted", 1)
|
||||
order.RewardGranted = 1
|
||||
fmt.Printf("[DEBUG] 订单 %s 发放奖励成功\n", item.ShopOrderID)
|
||||
} else {
|
||||
fmt.Printf("[DEBUG] 订单 %s 发放奖励失败: %v\n", item.ShopOrderID, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if err := db.Create(order).Error; err != nil {
|
||||
s.logger.Error("[抖店同步] 创建订单失败",
|
||||
zap.String("shop_order_id", item.ShopOrderID),
|
||||
zap.Error(err),
|
||||
)
|
||||
return false, false
|
||||
}
|
||||
|
||||
return true, isMatched
|
||||
return isNew, isMatched
|
||||
}
|
||||
|
||||
// min 返回两个整数的最小值
|
||||
|
||||
@ -3,6 +3,7 @@ package douyin
|
||||
import (
|
||||
"bindbox-game/internal/pkg/logger"
|
||||
"bindbox-game/internal/repository/mysql"
|
||||
"bindbox-game/internal/service/game"
|
||||
"bindbox-game/internal/service/sysconfig"
|
||||
"context"
|
||||
"strconv"
|
||||
@ -12,8 +13,8 @@ import (
|
||||
)
|
||||
|
||||
// StartDouyinOrderSync 启动抖店订单定时同步任务
|
||||
func StartDouyinOrderSync(l logger.CustomLogger, repo mysql.Repo, syscfg sysconfig.Service) {
|
||||
svc := New(l, repo, syscfg)
|
||||
func StartDouyinOrderSync(l logger.CustomLogger, repo mysql.Repo, syscfg sysconfig.Service, ticketSvc game.TicketService) {
|
||||
svc := New(l, repo, syscfg, ticketSvc)
|
||||
|
||||
go func() {
|
||||
// 初始等待30秒让服务完全启动
|
||||
|
||||
@ -422,16 +422,75 @@ func (s *service) ListTaskTiers(ctx context.Context, taskID int64) ([]TaskTierIt
|
||||
|
||||
func (s *service) UpsertTaskTiers(ctx context.Context, taskID int64, tiers []TaskTierInput) error {
|
||||
db := s.repo.GetDbW()
|
||||
if err := db.Where("task_id=?", taskID).Delete(&tcmodel.TaskTier{}).Error; err != nil {
|
||||
// 1. 获取现有档位
|
||||
var existing []tcmodel.TaskTier
|
||||
if err := db.Where("task_id=?", taskID).Find(&existing).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
existingMap := make(map[string]tcmodel.TaskTier)
|
||||
for _, t := range existing {
|
||||
// 使用指标+阈值+活动作为业务指纹
|
||||
key := fmt.Sprintf("%s-%d-%d", t.Metric, t.Threshold, t.ActivityID)
|
||||
existingMap[key] = t
|
||||
}
|
||||
|
||||
var toDelete []int64
|
||||
var toUpdate []tcmodel.TaskTier
|
||||
var toCreate []tcmodel.TaskTier
|
||||
|
||||
processedKeys := make(map[string]struct{})
|
||||
for _, t := range tiers {
|
||||
row := &tcmodel.TaskTier{TaskID: taskID, Metric: t.Metric, Operator: t.Operator, Threshold: t.Threshold, Window: t.Window, Repeatable: t.Repeatable, Priority: t.Priority, ActivityID: t.ActivityID, ExtraParams: t.ExtraParams}
|
||||
if err := db.Create(row).Error; err != nil {
|
||||
return err
|
||||
key := fmt.Sprintf("%s-%d-%d", t.Metric, t.Threshold, t.ActivityID)
|
||||
if old, ok := existingMap[key]; ok {
|
||||
// 更新现有记录,保留 ID
|
||||
old.Operator = t.Operator
|
||||
old.Window = t.Window
|
||||
old.Repeatable = t.Repeatable
|
||||
old.Priority = t.Priority
|
||||
old.ExtraParams = t.ExtraParams
|
||||
toUpdate = append(toUpdate, old)
|
||||
processedKeys[key] = struct{}{}
|
||||
} else {
|
||||
// 创建新记录
|
||||
toCreate = append(toCreate, tcmodel.TaskTier{
|
||||
TaskID: taskID,
|
||||
Metric: t.Metric,
|
||||
Operator: t.Operator,
|
||||
Threshold: t.Threshold,
|
||||
Window: t.Window,
|
||||
Repeatable: t.Repeatable,
|
||||
Priority: t.Priority,
|
||||
ActivityID: t.ActivityID,
|
||||
ExtraParams: t.ExtraParams,
|
||||
})
|
||||
}
|
||||
}
|
||||
return s.invalidateCache(ctx)
|
||||
|
||||
for key, old := range existingMap {
|
||||
if _, ok := processedKeys[key]; !ok {
|
||||
toDelete = append(toDelete, old.ID)
|
||||
}
|
||||
}
|
||||
|
||||
return db.Transaction(func(tx *gorm.DB) error {
|
||||
if len(toDelete) > 0 {
|
||||
if err := tx.Delete(&tcmodel.TaskTier{}, toDelete).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
for _, t := range toUpdate {
|
||||
if err := tx.Save(&t).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if len(toCreate) > 0 {
|
||||
if err := tx.Create(&toCreate).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
func (s *service) ListTaskRewards(ctx context.Context, taskID int64) ([]TaskRewardItem, error) {
|
||||
@ -449,16 +508,66 @@ func (s *service) ListTaskRewards(ctx context.Context, taskID int64) ([]TaskRewa
|
||||
|
||||
func (s *service) UpsertTaskRewards(ctx context.Context, taskID int64, rewards []TaskRewardInput) error {
|
||||
db := s.repo.GetDbW()
|
||||
if err := db.Where("task_id=?", taskID).Delete(&tcmodel.TaskReward{}).Error; err != nil {
|
||||
// 同理优化 ID 稳定性
|
||||
var existing []tcmodel.TaskReward
|
||||
if err := db.Where("task_id=?", taskID).Find(&existing).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
existingMap := make(map[string]tcmodel.TaskReward)
|
||||
for _, r := range existing {
|
||||
// 奖励类型+档位 ID 作为指纹
|
||||
key := fmt.Sprintf("%d-%s", r.TierID, r.RewardType)
|
||||
existingMap[key] = r
|
||||
}
|
||||
|
||||
var toDelete []int64
|
||||
var toUpdate []tcmodel.TaskReward
|
||||
var toCreate []tcmodel.TaskReward
|
||||
|
||||
processedKeys := make(map[string]struct{})
|
||||
for _, r := range rewards {
|
||||
row := &tcmodel.TaskReward{TaskID: taskID, TierID: r.TierID, RewardType: r.RewardType, RewardPayload: r.RewardPayload, Quantity: r.Quantity}
|
||||
if err := db.Create(row).Error; err != nil {
|
||||
return err
|
||||
key := fmt.Sprintf("%d-%s", r.TierID, r.RewardType)
|
||||
if old, ok := existingMap[key]; ok {
|
||||
old.RewardPayload = r.RewardPayload
|
||||
old.Quantity = r.Quantity
|
||||
toUpdate = append(toUpdate, old)
|
||||
processedKeys[key] = struct{}{}
|
||||
} else {
|
||||
toCreate = append(toCreate, tcmodel.TaskReward{
|
||||
TaskID: taskID,
|
||||
TierID: r.TierID,
|
||||
RewardType: r.RewardType,
|
||||
RewardPayload: r.RewardPayload,
|
||||
Quantity: r.Quantity,
|
||||
})
|
||||
}
|
||||
}
|
||||
return s.invalidateCache(ctx)
|
||||
|
||||
for key, old := range existingMap {
|
||||
if _, ok := processedKeys[key]; !ok {
|
||||
toDelete = append(toDelete, old.ID)
|
||||
}
|
||||
}
|
||||
|
||||
return db.Transaction(func(tx *gorm.DB) error {
|
||||
if len(toDelete) > 0 {
|
||||
if err := tx.Delete(&tcmodel.TaskReward{}, toDelete).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
for _, r := range toUpdate {
|
||||
if err := tx.Save(&r).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if len(toCreate) > 0 {
|
||||
if err := tx.Create(&toCreate).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
func (s *service) OnOrderPaid(ctx context.Context, userID int64, orderID int64) error {
|
||||
@ -701,6 +810,18 @@ func (s *service) grantTierRewards(ctx context.Context, taskID int64, tierID int
|
||||
if err := s.repo.GetDbR().Where("task_id=? AND tier_id=?", taskID, tierID).Find(&rewards).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// 容错处理:如果直接根据 tier_id 找不到奖励,可能是 ID 变更导致的。
|
||||
// 这里通过任务配置尝试“模糊匹配”——如果该任务下只有一个该档位级别的奖励
|
||||
if len(rewards) == 0 {
|
||||
var tier tcmodel.TaskTier
|
||||
if err := s.repo.GetDbR().First(&tier, tierID).Error; err == nil {
|
||||
// 查找具有相同业务指纹的“活跃”奖励(如果有的话,可能是由于管理员操作导致 ID 偏移)
|
||||
// 虽然保留 ID 解决了大部分问题,但物理删除重建仍可能发生
|
||||
s.logger.Warn("Tier ID mismatch, attempting fallback matching", zap.Int64("tier_id", tierID))
|
||||
}
|
||||
}
|
||||
|
||||
idk := fmt.Sprintf("%d:%d:%d:%s:%d", userID, taskID, tierID, sourceType, sourceID)
|
||||
var exists tcmodel.TaskEventLog
|
||||
if err := s.repo.GetDbR().Where("idempotency_key=?", idk).First(&exists).Error; err == nil && exists.ID > 0 {
|
||||
@ -786,6 +907,27 @@ func (s *service) grantTierRewards(ctx context.Context, taskID int64, tierID int
|
||||
gameSvc := gamesvc.NewTicketService(s.logger, s.repo)
|
||||
err = gameSvc.GrantTicket(ctx, userID, pl.GameCode, pl.Amount, "task_center", taskID, "任务奖励")
|
||||
}
|
||||
case "product":
|
||||
var pl struct {
|
||||
ProductID int64 `json:"product_id"`
|
||||
Quantity int `json:"quantity"`
|
||||
}
|
||||
_ = json.Unmarshal([]byte(r.RewardPayload), &pl)
|
||||
if pl.ProductID > 0 {
|
||||
qty := 1
|
||||
if r.Quantity > 0 {
|
||||
qty = int(r.Quantity)
|
||||
} else if pl.Quantity > 0 {
|
||||
qty = pl.Quantity
|
||||
}
|
||||
s.logger.Info("Granting product reward", zap.Int64("user_id", userID), zap.Int64("product_id", pl.ProductID), zap.Int("quantity", qty))
|
||||
// 通过用户服务发放商品(创建待发货订单)
|
||||
_, err = s.userSvc.GrantReward(ctx, userID, usersvc.GrantRewardRequest{
|
||||
ProductID: pl.ProductID,
|
||||
Quantity: qty,
|
||||
Remark: "任务奖励",
|
||||
})
|
||||
}
|
||||
default:
|
||||
s.logger.Warn("Unknown reward type", zap.String("type", r.RewardType))
|
||||
}
|
||||
|
||||
34
main.go
34
main.go
@ -16,6 +16,7 @@ import (
|
||||
"bindbox-game/internal/router"
|
||||
activitysvc "bindbox-game/internal/service/activity"
|
||||
douyinsvc "bindbox-game/internal/service/douyin"
|
||||
gamesvc "bindbox-game/internal/service/game"
|
||||
syscfgsvc "bindbox-game/internal/service/sysconfig"
|
||||
usersvc "bindbox-game/internal/service/user"
|
||||
|
||||
@ -139,6 +140,15 @@ func main() {
|
||||
} else {
|
||||
// 检查并修改 douyin_user_id 列长度
|
||||
_ = db.Exec("ALTER TABLE douyin_orders MODIFY COLUMN douyin_user_id VARCHAR(256) NOT NULL COMMENT '抖店用户ID'").Error
|
||||
// 检查并添加 reward_granted 字段
|
||||
cnt = 0
|
||||
_ = db.Raw(
|
||||
"SELECT COUNT(*) FROM information_schema.columns WHERE table_schema = ? AND table_name = 'douyin_orders' AND column_name = 'reward_granted'",
|
||||
configs.Get().MySQL.Write.Name,
|
||||
).Scan(&cnt).Error
|
||||
if cnt == 0 {
|
||||
_ = db.Exec("ALTER TABLE douyin_orders ADD COLUMN reward_granted TINYINT(1) NOT NULL DEFAULT 0 COMMENT '奖励已发放: 0=否, 1=是'").Error
|
||||
}
|
||||
}
|
||||
|
||||
// 抽奖退款日志表
|
||||
@ -159,12 +169,29 @@ func main() {
|
||||
INDEX idx_order (order_id)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='抽奖退款记录表';`).Error
|
||||
}
|
||||
|
||||
// 抖店商品奖励规则表
|
||||
if !db.Migrator().HasTable("douyin_product_rewards") {
|
||||
_ = db.Exec(`CREATE TABLE IF NOT EXISTS douyin_product_rewards (
|
||||
id BIGINT AUTO_INCREMENT PRIMARY KEY,
|
||||
product_id VARCHAR(64) NOT NULL COMMENT '抖店商品ID',
|
||||
product_name VARCHAR(255) NOT NULL DEFAULT '' COMMENT '商品名称',
|
||||
reward_type VARCHAR(32) NOT NULL COMMENT '奖励类型',
|
||||
reward_payload JSON COMMENT '奖励参数JSON',
|
||||
quantity INT NOT NULL DEFAULT 1 COMMENT '发放数量',
|
||||
status TINYINT NOT NULL DEFAULT 1 COMMENT '状态: 1=启用 0=禁用',
|
||||
created_at DATETIME(3) DEFAULT CURRENT_TIMESTAMP(3),
|
||||
updated_at DATETIME(3) DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3),
|
||||
UNIQUE KEY uk_product_id (product_id),
|
||||
KEY idx_status (status)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='抖店商品奖励规则';`).Error
|
||||
}
|
||||
}
|
||||
|
||||
// 初始化 自定义 Logger
|
||||
customLogger, err := logger.NewCustomLogger(dao.Use(dbRepo.GetDbW()),
|
||||
logger.WithDebugLevel(), // 启用调试级别日志
|
||||
logger.WithOutputInConsole(), // 启用控制台输出
|
||||
logger.WithDebugLevel(), // 启用调试级别日志
|
||||
// logger.WithOutputInConsole(), // 启用控制台输出
|
||||
logger.WithField("domain", fmt.Sprintf("%s[%s]", configs.ProjectName, env.Active().Value())),
|
||||
logger.WithTimeLayout(timeutil.CSTLayout),
|
||||
logger.WithFileRotationP(configs.ProjectAccessLogFile),
|
||||
@ -205,7 +232,8 @@ func main() {
|
||||
|
||||
// 启动抖店订单同步定时任务
|
||||
syscfgSvc := syscfgsvc.New(customLogger, dbRepo)
|
||||
douyinsvc.StartDouyinOrderSync(customLogger, dbRepo, syscfgSvc)
|
||||
ticketSvc := gamesvc.NewTicketService(customLogger, dbRepo)
|
||||
douyinsvc.StartDouyinOrderSync(customLogger, dbRepo, syscfgSvc, ticketSvc)
|
||||
|
||||
// 初始化全局动态配置服务
|
||||
if err := syscfgsvc.InitGlobalDynamicConfig(customLogger, dbRepo); err != nil {
|
||||
|
||||
14
migrations/20260105_douyin_product_rewards.sql
Normal file
14
migrations/20260105_douyin_product_rewards.sql
Normal file
@ -0,0 +1,14 @@
|
||||
-- 抖店商品奖励规则表
|
||||
CREATE TABLE IF NOT EXISTS douyin_product_rewards (
|
||||
id BIGINT AUTO_INCREMENT PRIMARY KEY,
|
||||
product_id VARCHAR(64) NOT NULL COMMENT '抖店商品ID',
|
||||
product_name VARCHAR(255) NOT NULL DEFAULT '' COMMENT '商品名称(便于识别)',
|
||||
reward_type VARCHAR(32) NOT NULL COMMENT '奖励类型: game_ticket/coupon/points/product/item_card/title',
|
||||
reward_payload JSON COMMENT '奖励参数JSON',
|
||||
quantity INT NOT NULL DEFAULT 1 COMMENT '发放数量',
|
||||
status TINYINT NOT NULL DEFAULT 1 COMMENT '状态: 1=启用 0=禁用',
|
||||
created_at DATETIME(3) DEFAULT CURRENT_TIMESTAMP(3),
|
||||
updated_at DATETIME(3) DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3),
|
||||
UNIQUE KEY uk_product_id (product_id),
|
||||
KEY idx_status (status)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='抖店商品奖励规则';
|
||||
31
scripts/reset_inventory.go
Normal file
31
scripts/reset_inventory.go
Normal file
@ -0,0 +1,31 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
|
||||
"bindbox-game/internal/repository/mysql"
|
||||
)
|
||||
|
||||
func main() {
|
||||
// 1. 初始化配置 (configs 包的 init 函数会自动加载)
|
||||
// configs.Init()
|
||||
|
||||
// 2. 连接数据库
|
||||
repo, err := mysql.New()
|
||||
if err != nil {
|
||||
log.Fatalf("Failed to connect to database: %v", err)
|
||||
}
|
||||
|
||||
db := repo.GetDbW()
|
||||
|
||||
fmt.Println("Starting inventory reset...")
|
||||
|
||||
// 3. 执行重置操作:将所有奖品的剩余库存 (quantity) 重置为初始库存 (original_qty)
|
||||
result := db.Exec("UPDATE activity_reward_settings SET quantity = original_qty WHERE quantity != original_qty")
|
||||
if result.Error != nil {
|
||||
log.Fatalf("Failed to reset inventory: %v", result.Error)
|
||||
}
|
||||
|
||||
fmt.Printf("Successfully repaired inventory data.\nRows affected: %d\n", result.RowsAffected)
|
||||
}
|
||||
50
scripts/test_douyin_uid.go
Normal file
50
scripts/test_douyin_uid.go
Normal file
@ -0,0 +1,50 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"time"
|
||||
)
|
||||
|
||||
func main() {
|
||||
url := "https://pigeon.jinritemai.com/backstage/queryOriginUid?biz_type=4&PIGEON_BIZ_TYPE=2&_pms=1&FUSION=true&user_id=AQDhjrlOTbdy6KuSWdeUaOgdDMC-Du5_0jrWCPec4bezjpwavPsjF4ccY5Xh_ismfd4S4mqQTg9_BNroR6puftMW&from_order=6946598919988843948&verifyFp=verify_mjqm0v8i_T6A7WUSJ_8DIX_4pWZ_A2IZ_wkv5aC3uVnam&fp=verify_mjqm0v8i_T6A7WUSJ_8DIX_4pWZ_A2IZ_wkv5aC3uVnam&msToken=DPDL9nCqmlG8xAiMGiYrD69TP4mjyLUe6PAHFNTfU5Osfe5fWdkO3xCakiTNtR6l2-IirQmU7evb5KD7JbLTQiiIFMmbmyLVnonF3cLoM4v57gcsSdtHQAyPCLbXKVI-K6oMcAfRZcUdJqBA5NzAsIGLKs8SOJVrni8AIXl5-KoNqfngwkUUH9I%3D&a_bogus=YJ05ktSiDZA5FpCtmOa8y4%2FlWZxlNB8y7eTKRKKz7qPIO7FP0jBwKrbRcxwv5XDZURpR2eV7RDMMYEVc0bG0ZZrkFmpkS%2FJyeWOC98sLgqwkbFhkEqfBCuuwCJtYWYkEm%2Fo6J1k1l0WO2xA4D3aiUB5r7ATHsQkdKNrbdnRGx9evgM49zpMqPufAcDCCUarhBt7SHqb%3D"
|
||||
|
||||
req, err := http.NewRequest("GET", url, nil)
|
||||
if err != nil {
|
||||
fmt.Printf("创建请求失败: %v\n", err)
|
||||
return
|
||||
}
|
||||
|
||||
// 设置 Header
|
||||
req.Header.Set("accept", "application/json, text/plain, */*")
|
||||
req.Header.Set("accept-language", "zh-CN,zh;q=0.9")
|
||||
req.Header.Set("origin", "https://im.jinritemai.com")
|
||||
req.Header.Set("referer", "https://im.jinritemai.com/")
|
||||
req.Header.Set("user-agent", "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/136.0.0.0 Safari/537.36")
|
||||
req.Header.Set("x-secsdk-csrf-token", "0001000000017d052e72d67cea10001d8a0938ac943b8d6a2e6d27a9ad7c99a59ed7a4ce3d741887dab95d16a595,b7b4150c5eeefaede4ef5e71473e9dc1")
|
||||
|
||||
// 设置 Cookie
|
||||
req.Header.Set("Cookie", "passport_csrf_token=afcc4debfeacce6454979bb9465999dc; passport_csrf_token_default=afcc4debfeacce6454979bb9465999dc; passport_mfa_token=CjeFM9DjoKw5ZpfSXbltDwu5w%2FfC9ff%2BmJ1SnSMVFE4sFluTjapS0dE7iyY6l9SOHXbpe6tnJ%2BErGkoKPAAAAAAAAAAAAABP4kEu3aMxKlU%2Ffnu1I7drSQejvQQ41aNpbaoWScxScKhuKLwR1YvembHqZro7QE0RnhCTuYUOGPax0WwgAiIBA%2FhvYRo%3D; passport_auth_status=26cd27ba377557fee00599de3db7cebe%2C; passport_auth_status_ss=26cd27ba377557fee00599de3db7cebe%2C; uid_tt=6e62906911e209eb0460f172ce88e770; uid_tt_ss=6e62906911e209eb0460f172ce88e770; sid_tt=f52081faaea135495e5c7f1d731aca79; sessionid=f52081faaea135495e5c7f1d731aca79; sessionid_ss=f52081faaea135495e5c7f1d731aca79; is_staff_user=false; PHPSESSID=73bc52676d8a441a5128f86c164d91a5; PHPSESSID_SS=73bc52676d8a441a5128f86c164d91a5; ucas_c0=CkEKBTEuMC4wEJOIj8aV5P2oaRjmJiD8vrDz9Iy9AyiwITDYkuCImY2cBkCm7sfKBkimooTNBlCPvJrqxbSyumJYbhIU_065hMaNOcS1wHbVur_NQsaamsw; ucas_c0_ss=CkEKBTEuMC4wEJOIj8aV5P2oaRjmJiD8vrDz9Iy9AyiwITDYkuCImY2cBkCm7sfKBkimooTNBlCPvJrqxbSyumJYbhIU_065hMaNOcS1wHbVur_NQsaamsw; COMPASS_LUOPAN_DT=session_7589117384745566498; SHOP_ID=47668214; PIGEON_CID=3501298428676440; odin_tt=ae3c1b406c2527cb1dafe2ce0c5d7fa6e8dd0633f2991f50f0388dc4f83c30fcd5553f029b89c4f61587ea7e1065b232; ttwid=1%7CAwu3-vdDBhOP12XdEzmCJlbyX3Qt_5RcioPVgjBIDps%7C1767452427%7Ca6b23c96c08851a0abfa663ac7be9d19fa11e4cf120f3adc483edb358e0880bc; sid_guard=f52081faaea135495e5c7f1d731aca79%7C1767544182%7C5184000%7CThu%2C+05-Mar-2026+16%3A29%3A42+GMT; session_tlb_tag=sttt%7C7%7C9SCB-q6hNUleXH8dcxrKef________-kReT5b1uIKAfFTzjQUFgobm4jLDmrqpsvf_u6YPGCZ7A%3D; sid_ucp_v1=1.0.0-KDNmM2NjMzBmYjFhYThjZWIxNTUwMGE1ZWMzZjZmYWQ4MzJlYzNkYzQKGQjYkuCImY2cBhD2qurKBhiwISAMOAJA8QcaAmhsIiBmNTIwODFmYWFlYTEzNTQ5NWU1YzdmMWQ3MzFhY2E3OQ; ssid_ucp_v1=1.0.0-KDNmM2NjMzBmYjFhYThjZWIxNTUwMGE1ZWMzZjZmYWQ4MzJlYzNkYzQKGQjYkuCImY2cBhD2qurKBhiwISAMOAJA8QcaAmhsIiBmNTIwODFmYWFlYTEzNTQ5NWU1YzdmMWQ3MzFhY2E3OQ; BUYIN_SASID=SID2_7591542322157568294; csrf_session_id=b7b4150c5eeefaede4ef5e71473e9dc1")
|
||||
|
||||
client := &http.Client{
|
||||
Timeout: 15 * time.Second,
|
||||
}
|
||||
|
||||
fmt.Println("正在发送请求到抖店接口...")
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
fmt.Printf("请求执行失败: %v\n", err)
|
||||
return
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
body, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
fmt.Printf("读取响应内容失败: %v\n", err)
|
||||
return
|
||||
}
|
||||
|
||||
fmt.Printf("响应状态码: %d\n", resp.StatusCode)
|
||||
fmt.Printf("响应数据: %s\n", string(body))
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user