diff --git a/.claude/plan/gmv-payment-breakdown.md b/.claude/plan/gmv-payment-breakdown.md new file mode 100644 index 0000000..e45dd75 --- /dev/null +++ b/.claude/plan/gmv-payment-breakdown.md @@ -0,0 +1,135 @@ +## 实施计划:GMV 支付方式拆分展示 + +### 需求分析 + +当前渠道统计只展示一个"累计实付金额"(GMV 总数),用户无法看到这笔钱的构成。需要拆分为: +- **现金支付** (`actual_amount`) — 用户通过微信支付的真金白银 +- **优惠券抵扣** (`discount_amount`) — 优惠券抵扣部分 +- **积分抵扣** (`points_amount`) — 积分抵扣部分(当前数据为0,但字段已预留) + +验证:`total_amount = actual_amount + discount_amount + points_amount` 在所有订单上完全成立(0条不等式)。 + +### 数据现状(dev 环境) + +| 支付方式 | 订单数 | 金额(元) | 占比 | +|---------|--------|---------|------| +| GMV 总额 | 3,595 | 124,526.80 | 100% | +| 现金 | 3,595 | 90,067.85 | 72.3% | +| 优惠券 | 1,896 | 34,458.95 | 27.7% | +| 积分 | 0 | 0.00 | 0% | + +### 技术方案 + +orders 表已有完整的拆分字段,**无需新建表或字段**,只需在查询和展示层增加维度。 + +### 实施步骤 + +#### Step 1: 后端 — 扩展数据结构 + +文件:`internal/service/channel/channel.go` + +1.1 `StatsOverview` 结构体新增字段: +```go +CashCents int64 `json:"cash_cents"` // 现金支付(分) +CouponCents int64 `json:"coupon_cents"` // 优惠券抵扣(分) +PointsCents int64 `json:"points_cents"` // 积分抵扣(分) +``` + +1.2 `StatsDailyItem` 结构体新增字段: +```go +CashCents int64 `json:"cash_cents"` +CouponCents int64 `json:"coupon_cents"` +PointsCents int64 `json:"points_cents"` +``` + +#### Step 2: 后端 — 修改 GMV 查询方法 + +文件:`internal/service/channel/channel.go` + +2.1 `calcGMVByTotalAmount` 改为同时返回 actual_amount / discount_amount / points_amount 的分组统计: + +```go +type GMVBreakdown struct { + Total int64 + Cash int64 + Coupon int64 + Points int64 +} + +func calcGMVByTotalAmount(...) (GMVBreakdown, map[string]GMVBreakdown) +``` + +查询 SELECT 增加:`orders.actual_amount, orders.discount_amount, orders.points_amount` + +2.2 `GetStats` 方法中将拆分数据写入 Overview 和 Daily: +```go +out.Overview.CashCents = breakdown.Cash +out.Overview.CouponCents = breakdown.Coupon +out.Overview.PointsCents = breakdown.Points +``` + +#### Step 3: 后端 — List 接口也返回拆分数据(可选) + +文件:`internal/service/channel/channel.go` + +`ChannelWithStat` 结构体和 `List` 方法中的 GMV 查询也增加拆分统计,用于渠道列表页 tooltip 展示。 + +#### Step 4: 前端 — API 类型更新 + +文件:`web/admin/src/api/channels.ts` + +```ts +interface StatsOverview { + // ... existing fields + cash_cents?: number + coupon_cents?: number + points_cents?: number +} + +interface StatsDailyItem { + // ... existing fields + cash_cents?: number + coupon_cents?: number + points_cents?: number +} +``` + +#### Step 5: 前端 — Stats 概览卡片展示 + +文件:`web/admin/src/views/operations/channels/index.vue` + +在"累计实付金额"卡片下方或旁边展示支付构成: +- 显示 3 个子指标:现金 / 优惠券 / 积分 +- 各自显示金额和占比百分比 +- 积分为 0 时可隐藏或灰显 + +#### Step 6: 前端 — 每日趋势图支持 + +文件:`web/admin/src/views/operations/channels/index.vue` + +在 revenue tab 的折线图中,可以选择查看: +- GMV 总额(默认) +- 现金 / 优惠券 / 积分 分层堆叠 + +#### Step 7: 测试 + +文件:`internal/service/channel/channel_stats_test.go` + +更新测试用例,验证 GMV 拆分字段的正确性。 + +### 关键文件 + +| 文件 | 操作 | 说明 | +|------|------|------| +| `internal/service/channel/channel.go` | 修改 | 数据结构 + 查询逻辑 | +| `internal/service/channel/channel_stats_test.go` | 修改 | 测试覆盖拆分字段 | +| `web/admin/src/api/channels.ts` | 修改 | API 类型定义 | +| `web/admin/src/views/operations/channels/index.vue` | 修改 | 概览卡片 + 图表展示 | + +### 风险与缓解 + +| 风险 | 缓解措施 | +|------|----------| +| 旧版前端未适配新字段 | 新字段均为可选,不影响旧版展示 | +| 积分字段当前全为0 | 字段预留,后续开启积分抵扣时自动生效 | +| 查询性能 | 无额外 JOIN,只增加 3 个 SUM 列,影响可忽略 | diff --git a/cmd/channel_stats_compare/profit_loss_query.go b/cmd/channel_stats_compare/profit_loss_query.go new file mode 100644 index 0000000..550a282 --- /dev/null +++ b/cmd/channel_stats_compare/profit_loss_query.go @@ -0,0 +1,147 @@ +//go:build ignore + +package main + +import ( + "fmt" + + "gorm.io/driver/mysql" + "gorm.io/gorm" +) + +func main() { + dsn := "root:bindbox2025kdy@tcp(150.158.78.154:3306)/dev_game?charset=utf8mb4&parseTime=True&loc=Local" + db, err := gorm.Open(mysql.Open(dsn), &gorm.Config{}) + if err != nil { + fmt.Println("连接失败:", err) + return + } + + filter := "users.deleted_at IS NULL AND orders.status = 2 AND orders.total_amount > 0 AND orders.actual_amount > 0 AND orders.source_type IN (2,3,4) AND (orders.ext_order_id = '' OR orders.ext_order_id IS NULL)" + + // 1. 各字段使用分布 + type FieldStats struct { + Label string + Count int64 + Sum int64 + } + + fmt.Println("========== GMV 支付方式拆分数据探查 ==========") + fmt.Println() + + // actual_amount (现金) + var cash FieldStats + db.Table("orders").Joins("JOIN users ON users.id = orders.user_id"). + Select("'现金(actual_amount)' as label, COUNT(*) as count, COALESCE(SUM(actual_amount),0) as sum"). + Where(filter).Scan(&cash) + + // discount_amount (优惠券) + var coupon FieldStats + db.Table("orders").Joins("JOIN users ON users.id = orders.user_id"). + Select("'优惠券(discount_amount)' as label, COUNT(*) as count, COALESCE(SUM(discount_amount),0) as sum"). + Where(filter + " AND discount_amount > 0").Scan(&coupon) + + // points_amount (积分) + var points FieldStats + db.Table("orders").Joins("JOIN users ON users.id = orders.user_id"). + Select("'积分(points_amount)' as label, COUNT(*) as count, COALESCE(SUM(points_amount),0) as sum"). + Where(filter + " AND points_amount > 0").Scan(&points) + + // 道具卡 (item_card_id > 0) + var itemCard FieldStats + db.Table("orders").Joins("JOIN users ON users.id = orders.user_id"). + Select("'道具卡(item_card_id)' as label, COUNT(*) as count, 0 as sum"). + Where(filter + " AND item_card_id > 0").Scan(&itemCard) + + // 总 GMV + var totalGMV FieldStats + db.Table("orders").Joins("JOIN users ON users.id = orders.user_id"). + Select("'总GMV(total_amount)' as label, COUNT(*) as count, COALESCE(SUM(total_amount),0) as sum"). + Where(filter).Scan(&totalGMV) + + fmt.Printf("%-25s %8s %14s\n", "字段", "订单数", "金额(元)") + fmt.Println("--------------------------------------------------") + for _, f := range []FieldStats{totalGMV, cash, coupon, points, itemCard} { + fmt.Printf("%-25s %8d %14.2f\n", f.Label, f.Count, float64(f.Sum)/100) + } + + // 2. 验证: total = actual + discount + points ? + fmt.Println() + fmt.Println("========== 验证: total_amount = actual + discount + points ? ==========") + type MismatchRow struct { + Count int64 + } + var mismatch MismatchRow + db.Table("orders").Joins("JOIN users ON users.id = orders.user_id"). + Select("COUNT(*) as count"). + Where(filter + " AND total_amount != (actual_amount + discount_amount + points_amount)"). + Scan(&mismatch) + fmt.Printf(" 不等式成立的订单数: %d\n", mismatch.Count) + + if mismatch.Count > 0 { + type DetailRow struct { + ID int64 + TotalAmount int64 + ActualAmount int64 + DiscountAmount int64 + PointsAmount int64 + Diff int64 + } + var details []DetailRow + db.Table("orders").Joins("JOIN users ON users.id = orders.user_id"). + Select("orders.id, orders.total_amount, orders.actual_amount, orders.discount_amount, orders.points_amount, (orders.total_amount - orders.actual_amount - orders.discount_amount - orders.points_amount) as diff"). + Where(filter + " AND total_amount != (actual_amount + discount_amount + points_amount)"). + Limit(5).Scan(&details) + fmt.Println(" 抽样:") + for _, d := range details { + fmt.Printf(" #%d total=%d actual=%d discount=%d points=%d diff=%d\n", + d.ID, d.TotalAmount, d.ActualAmount, d.DiscountAmount, d.PointsAmount, d.Diff) + } + } + + // 3. 次卡购买订单的支付方式拆分 + fmt.Println() + fmt.Println("========== source_type=4 购买次卡的支付方式 ==========") + var gpCash, gpCoupon, gpPoints FieldStats + gpFilter := "users.deleted_at IS NULL AND orders.status = 2 AND orders.total_amount > 0 AND orders.actual_amount > 0 AND orders.source_type = 4 AND orders.order_no LIKE 'GP%' AND (orders.ext_order_id = '' OR orders.ext_order_id IS NULL)" + + db.Table("orders").Joins("JOIN users ON users.id = orders.user_id"). + Select("'现金' as label, COUNT(*) as count, COALESCE(SUM(actual_amount),0) as sum"). + Where(gpFilter).Scan(&gpCash) + db.Table("orders").Joins("JOIN users ON users.id = orders.user_id"). + Select("'优惠券' as label, COUNT(*) as count, COALESCE(SUM(discount_amount),0) as sum"). + Where(gpFilter + " AND discount_amount > 0").Scan(&gpCoupon) + db.Table("orders").Joins("JOIN users ON users.id = orders.user_id"). + Select("'积分' as label, COUNT(*) as count, COALESCE(SUM(points_amount),0) as sum"). + Where(gpFilter + " AND points_amount > 0").Scan(&gpPoints) + + fmt.Printf(" 现金: %d单 %.2f元\n", gpCash.Count, float64(gpCash.Sum)/100) + fmt.Printf(" 优惠券: %d单 %.2f元\n", gpCoupon.Count, float64(gpCoupon.Sum)/100) + fmt.Printf(" 积分: %d单 %.2f元\n", gpPoints.Count, float64(gpPoints.Sum)/100) + + // 4. 按 source_type 拆分 GMV 构成 + fmt.Println() + fmt.Println("========== 按游戏类型 × 支付方式 ==========") + srcNames := map[int]string{2: "小程序抽奖", 3: "对对碰", 4: "一番赏/次卡"} + for _, st := range []int{2, 3, 4} { + stFilter := fmt.Sprintf("users.deleted_at IS NULL AND orders.status = 2 AND orders.total_amount > 0 AND orders.actual_amount > 0 AND orders.source_type = %d AND (orders.ext_order_id = '' OR orders.ext_order_id IS NULL)", st) + var total, actual, discount, pts FieldStats + db.Table("orders").Joins("JOIN users ON users.id = orders.user_id"). + Select("'total' as label, COUNT(*) as count, COALESCE(SUM(total_amount),0) as sum"). + Where(stFilter).Scan(&total) + db.Table("orders").Joins("JOIN users ON users.id = orders.user_id"). + Select("'actual' as label, COUNT(*) as count, COALESCE(SUM(actual_amount),0) as sum"). + Where(stFilter).Scan(&actual) + db.Table("orders").Joins("JOIN users ON users.id = orders.user_id"). + Select("'discount' as label, COUNT(*) as count, COALESCE(SUM(discount_amount),0) as sum"). + Where(stFilter).Scan(&discount) + db.Table("orders").Joins("JOIN users ON users.id = orders.user_id"). + Select("'points' as label, COUNT(*) as count, COALESCE(SUM(points_amount),0) as sum"). + Where(stFilter).Scan(&pts) + + fmt.Printf(" %s(type=%d): %d单 GMV=%.2f 现金=%.2f 优惠券=%.2f 积分=%.2f\n", + srcNames[st], st, total.Count, + float64(total.Sum)/100, float64(actual.Sum)/100, + float64(discount.Sum)/100, float64(pts.Sum)/100) + } +} diff --git a/cmd/douyin_sync_debug/main.go b/cmd/douyin_sync_debug/main.go index ce1b698..e62290b 100644 --- a/cmd/douyin_sync_debug/main.go +++ b/cmd/douyin_sync_debug/main.go @@ -64,7 +64,7 @@ func main() { env.Active() // 初始化 env flag(依赖已有的全局 -env/ACTIVE_ENV 配置) configs.Init() - cookie := "passport_csrf_token=40ba4a1be914a9f167320ed28b8c93d7; passport_csrf_token_default=40ba4a1be914a9f167320ed28b8c93d7; is_staff_user=false; s_v_web_id=verify_mkf83bbo_zfQ3q1Gp_5irf_4OOI_9y4N_C253269yUIJy; SHOP_ID=156231010; PIGEON_CID=4339134776748827; __security_mc_1_s_sdk_crypt_sdk=db47f387-4d0b-bf21; bd_ticket_guard_client_web_domain=2; bd_ticket_guard_client_data=eyJiZC10aWNrZXQtZ3VhcmQtdmVyc2lvbiI6MiwiYmQtdGlja2V0LWd1YXJkLWl0ZXJhdGlvbi12ZXJzaW9uIjoxLCJiZC10aWNrZXQtZ3VhcmQtcmVlLXB1YmxpYy1rZXkiOiJCTHVTREdkVFRHWUdNMVY3ZDZKS2M4V2FwWGJ1K3JVYmVqRThONTZoeTI4SUJXdmVxZjBLMS9GczE0dWx5RTVRd2d4cjdnaDd6SXdMZjlsWDkwOFZQQWs9IiwiYmQtdGlja2V0LWd1YXJkLXdlYi12ZXJzaW9uIjoyfQ%3D%3D; bd_ticket_guard_web_domain=3; gfkadpd=4272,23756; ecom_gray_shop_id=156231010; zsgw_business_data=%7B%22uuid%22%3A%226756720f-c380-4bda-ab81-3dd27ca08a2d%22%2C%22platform%22%3A%22pc%22%2C%22source%22%3A%22seo.baidu.069%22%7D; source=seo.baidu.069; Hm_lvt_b6520b076191ab4b36812da4c90f7a5e=1771350555,1772107597,1772794481,1773223394; HMACCOUNT=9C6B7571794A6624; csrf_session_id=8173f094b830570b2b64e98900924731; passport_mfa_token=CjcMUe8O6Zz52W9O1T3zlEkIxpWSHBCB4dHw9XBdiDU%2BIPU1pzwEXLpVjGth2W2nXGHC8OM6ffSmGkoKPAAAAAAAAAAAAABQK6uUDAbmPNiLgEkCaMWLdiWMpTEiK%2Fm1NGLpqOUmR4vBZtoNbJWrAhzjfim%2BBtfMlxCj6IsOGPax0WwgAiIBA8pTDDU%3D; Hm_lpvt_b6520b076191ab4b36812da4c90f7a5e=1773224382; ttwid=1%7CNnXcElGkMBE8UTpDOFYR5OfCUYkFjQaLyn1EagPBZgM%7C1773224307%7C18bc27eb78d0a5da332f8c3ec951f81229670377d82025fcb5e600e3766e367b; tt_scid=uSkT0B7AzW.AKqYpEsRrpTqtws.7fqp2P4-gBF1FyffuNMOl1AKuRvuymbUWzXRvcc00; odin_tt=6edadb78040b4604bed517fc3edef437495387c8a3bf60fa177788ff81dd88daaed661705eb0729801e665c086b098b263c3090fef72c26e872d2f3172f6e364; passport_auth_status=581a8676e64d918c69ee3930f4dacf8b%2C4bb14205ac4179b872cba76a97208a7e; passport_auth_status_ss=581a8676e64d918c69ee3930f4dacf8b%2C4bb14205ac4179b872cba76a97208a7e; bd_ticket_guard_server_data=eyJ0aWNrZXQiOiJoYXNoLk1SWGtrczRwYTZpWG91ODhuZENOT05idm9iSjI2SHlXOXRYN2JKNTdZMWM9IiwidHNfc2lnbiI6InRzLjIuMDg1MDhmMjljNWI2MjkzMjQ4ZTAwNGY0YjdiNjMwODI4ODk1YjFkZWQ1ZTRlYmFiZTc3NmYzZTUxYWJjZjZhNGM0ZmJlODdkMjMxOWNmMDUzMTg2MjRjZWRhMTQ5MTFjYTQwNmRlZGJlYmVkZGIyZTMwZmNlOGQ0ZmEwMjU3NWQiLCJjbGllbnRfY2VydCI6InB1Yi5CTHVTREdkVFRHWUdNMVY3ZDZKS2M4V2FwWGJ1K3JVYmVqRThONTZoeTI4SUJXdmVxZjBLMS9GczE0dWx5RTVRd2d4cjdnaDd6SXdMZjlsWDkwOFZQQWs9IiwibG9nX2lkIjoiMjAyNjAzMTExODE4NDBGQUVGNkZGMDBCMkUwQTJEQTU2QSIsImNyZWF0ZV90aW1lIjoxNzczMjI0MzIwfQ%3D%3D; uid_tt=e8ca5ad2e6032b72a0fd8c0843ff5e9b; uid_tt_ss=e8ca5ad2e6032b72a0fd8c0843ff5e9b; sid_tt=c1a29f1f0f71ea4ed9fbcde60bc2b390; sessionid=c1a29f1f0f71ea4ed9fbcde60bc2b390; sessionid_ss=c1a29f1f0f71ea4ed9fbcde60bc2b390; PHPSESSID=05ca4c3439dacd9ac5f1d86a78516abb; PHPSESSID_SS=05ca4c3439dacd9ac5f1d86a78516abb; ucas_c0=CkEKBTEuMC4wELaIgqLTr9DYaRjmJiD61rDnqc2DBCiwITCb1oDYuM3aB0CCg8XNBkiCt4HQBlC_vL6Ekt3t1GdYbhIUI1wJXqAsE71YWUNwS6OvJ9dOEVE; ucas_c0_ss=CkEKBTEuMC4wELaIgqLTr9DYaRjmJiD61rDnqc2DBCiwITCb1oDYuM3aB0CCg8XNBkiCt4HQBlC_vL6Ekt3t1GdYbhIUI1wJXqAsE71YWUNwS6OvJ9dOEVE; sid_guard=c1a29f1f0f71ea4ed9fbcde60bc2b390%7C1773224328%7C5184000%7CSun%2C+10-May-2026+10%3A18%3A48+GMT; sid_ucp_v1=1.0.0-KDA3MGQyMjJkNmQ1NDUxOGQ1MWRhYTFjMzBkZTZkMDBlMTNlYWJhYWUKGwib1oDYuM3aBxCIg8XNBhiwISAMOAZA9AdIBBoCaGwiIGMxYTI5ZjFmMGY3MWVhNGVkOWZiY2RlNjBiYzJiMzkw; ssid_ucp_v1=1.0.0-KDA3MGQyMjJkNmQ1NDUxOGQ1MWRhYTFjMzBkZTZkMDBlMTNlYWJhYWUKGwib1oDYuM3aBxCIg8XNBhiwISAMOAZA9AdIBBoCaGwiIGMxYTI5ZjFmMGY3MWVhNGVkOWZiY2RlNjBiYzJiMzkw; session_tlb_tag=sttt%7C17%7CwaKfHw9x6k7Z-83mC8KzkP________-tSxexYwusSRjOrIMuB3YiA6EaLnfr1fbbR8LfwAsAu74%3D; BUYIN_SASID=SID2_7615938059562205474; COMPASS_LUOPAN_DT=session_7615939876688511241" + cookie := "passport_csrf_token=0b67ab6212a41bd1903f03d4f9a887f9; passport_csrf_token_default=0b67ab6212a41bd1903f03d4f9a887f9; is_staff_user=false; s_v_web_id=verify_mkf83bbo_zfQ3q1Gp_5irf_4OOI_9y4N_C253269yUIJy; SHOP_ID=156231010; PIGEON_CID=4339134776748827; __security_mc_1_s_sdk_crypt_sdk=db47f387-4d0b-bf21; bd_ticket_guard_client_web_domain=2; bd_ticket_guard_client_data=eyJiZC10aWNrZXQtZ3VhcmQtdmVyc2lvbiI6MiwiYmQtdGlja2V0LWd1YXJkLWl0ZXJhdGlvbi12ZXJzaW9uIjoxLCJiZC10aWNrZXQtZ3VhcmQtcmVlLXB1YmxpYy1rZXkiOiJCTHVTREdkVFRHWUdNMVY3ZDZKS2M4V2FwWGJ1K3JVYmVqRThONTZoeTI4SUJXdmVxZjBLMS9GczE0dWx5RTVRd2d4cjdnaDd6SXdMZjlsWDkwOFZQQWs9IiwiYmQtdGlja2V0LWd1YXJkLXdlYi12ZXJzaW9uIjoyfQ%3D%3D; bd_ticket_guard_web_domain=3; gfkadpd=4272,23756; ecom_gray_shop_id=156231010; zsgw_business_data=%7B%22uuid%22%3A%226756720f-c380-4bda-ab81-3dd27ca08a2d%22%2C%22platform%22%3A%22pc%22%2C%22source%22%3A%22seo.baidu.069%22%7D; source=seo.baidu.069; Hm_lvt_b6520b076191ab4b36812da4c90f7a5e=1771350555,1772107597,1772794481,1773223394; HMACCOUNT=9C6B7571794A6624; csrf_session_id=8173f094b830570b2b64e98900924731; passport_mfa_token=CjcMUe8O6Zz52W9O1T3zlEkIxpWSHBCB4dHw9XBdiDU%2BIPU1pzwEXLpVjGth2W2nXGHC8OM6ffSmGkoKPAAAAAAAAAAAAABQK6uUDAbmPNiLgEkCaMWLdiWMpTEiK%2Fm1NGLpqOUmR4vBZtoNbJWrAhzjfim%2BBtfMlxCj6IsOGPax0WwgAiIBA8pTDDU%3D; Hm_lpvt_b6520b076191ab4b36812da4c90f7a5e=1773224382; ttwid=1%7CNnXcElGkMBE8UTpDOFYR5OfCUYkFjQaLyn1EagPBZgM%7C1773224307%7C18bc27eb78d0a5da332f8c3ec951f81229670377d82025fcb5e600e3766e367b; tt_scid=uSkT0B7AzW.AKqYpEsRrpTqtws.7fqp2P4-gBF1FyffuNMOl1AKuRvuymbUWzXRvcc00; odin_tt=6edadb78040b4604bed517fc3edef437495387c8a3bf60fa177788ff81dd88daaed661705eb0729801e665c086b098b263c3090fef72c26e872d2f3172f6e364; passport_auth_status=581a8676e64d918c69ee3930f4dacf8b%2C4bb14205ac4179b872cba76a97208a7e; passport_auth_status_ss=581a8676e64d918c69ee3930f4dacf8b%2C4bb14205ac4179b872cba76a97208a7e; bd_ticket_guard_server_data=eyJ0aWNrZXQiOiJoYXNoLk1SWGtrczRwYTZpWG91ODhuZENOT05idm9iSjI2SHlXOXRYN2JKNTdZMWM9IiwidHNfc2lnbiI6InRzLjIuMDg1MDhmMjljNWI2MjkzMjQ4ZTAwNGY0YjdiNjMwODI4ODk1YjFkZWQ1ZTRlYmFiZTc3NmYzZTUxYWJjZjZhNGM0ZmJlODdkMjMxOWNmMDUzMTg2MjRjZWRhMTQ5MTFjYTQwNmRlZGJlYmVkZGIyZTMwZmNlOGQ0ZmEwMjU3NWQiLCJjbGllbnRfY2VydCI6InB1Yi5CTHVTREdkVFRHWUdNMVY3ZDZKS2M4V2FwWGJ1K3JVYmVqRThONTZoeTI4SUJXdmVxZjBLMS9GczE0dWx5RTVRd2d4cjdnaDd6SXdMZjlsWDkwOFZQQWs9IiwibG9nX2lkIjoiMjAyNjAzMTExODE4NDBGQUVGNkZGMDBCMkUwQTJEQTU2QSIsImNyZWF0ZV90aW1lIjoxNzczMjI0MzIwfQ%3D%3D; uid_tt=e8ca5ad2e6032b72a0fd8c0843ff5e9b; uid_tt_ss=e8ca5ad2e6032b72a0fd8c0843ff5e9b; sid_tt=c1a29f1f0f71ea4ed9fbcde60bc2b390; sessionid=c1a29f1f0f71ea4ed9fbcde60bc2b390; sessionid_ss=c1a29f1f0f71ea4ed9fbcde60bc2b390; PHPSESSID=05ca4c3439dacd9ac5f1d86a78516abb; PHPSESSID_SS=05ca4c3439dacd9ac5f1d86a78516abb; ucas_c0=CkEKBTEuMC4wELaIgqLTr9DYaRjmJiD61rDnqc2DBCiwITCb1oDYuM3aB0CCg8XNBkiCt4HQBlC_vL6Ekt3t1GdYbhIUI1wJXqAsE71YWUNwS6OvJ9dOEVE; ucas_c0_ss=CkEKBTEuMC4wELaIgqLTr9DYaRjmJiD61rDnqc2DBCiwITCb1oDYuM3aB0CCg8XNBkiCt4HQBlC_vL6Ekt3t1GdYbhIUI1wJXqAsE71YWUNwS6OvJ9dOEVE; sid_guard=c1a29f1f0f71ea4ed9fbcde60bc2b390%7C1773224328%7C5184000%7CSun%2C+10-May-2026+10%3A18%3A48+GMT; sid_ucp_v1=1.0.0-KDA3MGQyMjJkNmQ1NDUxOGQ1MWRhYTFjMzBkZTZkMDBlMTNlYWJhYWUKGwib1oDYuM3aBxCIg8XNBhiwISAMOAZA9AdIBBoCaGwiIGMxYTI5ZjFmMGY3MWVhNGVkOWZiY2RlNjBiYzJiMzkw; ssid_ucp_v1=1.0.0-KDA3MGQyMjJkNmQ1NDUxOGQ1MWRhYTFjMzBkZTZkMDBlMTNlYWJhYWUKGwib1oDYuM3aBxCIg8XNBhiwISAMOAZA9AdIBBoCaGwiIGMxYTI5ZjFmMGY3MWVhNGVkOWZiY2RlNjBiYzJiMzkw; session_tlb_tag=sttt%7C17%7CwaKfHw9x6k7Z-83mC8KzkP________-tSxexYwusSRjOrIMuB3YiA6EaLnfr1fbbR8LfwAsAu74%3D; BUYIN_SASID=SID2_7615938059562205474; COMPASS_LUOPAN_DT=session_7615939876688511241" if cookie == "" { fmt.Println("请通过环境变量 DOUYIN_COOKIE 提供抖店 Cookie") os.Exit(1) diff --git a/internal/api/user/address_share_submit_public.go b/internal/api/user/address_share_submit_public.go index f3004ce..0b1858c 100755 --- a/internal/api/user/address_share_submit_public.go +++ b/internal/api/user/address_share_submit_public.go @@ -45,16 +45,20 @@ func (h *handler) SubmitAddressShare() core.HandlerFunc { return } - // 尝试获取登录用户信息 (可选) + // 登录态验证 - 必须登录才能提交(确保地址归属正确) var submitUserID *int64 authHeader := ctx.GetHeader("Authorization") - if authHeader != "" { - // 如果有 Authorization 尝试解析 - if claims, err := jwtoken.New(configs.Get().JWT.PatientSecret).Parse(authHeader); err == nil { - uid := int64(claims.SessionUserInfo.Id) - submitUserID = &uid - } + if authHeader == "" { + ctx.AbortWithError(core.Error(http.StatusUnauthorized, 10027, "请先登录后再提交收货地址")) + return } + claims, claimsErr := jwtoken.New(configs.Get().JWT.PatientSecret).Parse(authHeader) + if claimsErr != nil { + ctx.AbortWithError(core.Error(http.StatusUnauthorized, 10027, "登录已过期,请重新登录")) + return + } + uid := int64(claims.SessionUserInfo.Id) + submitUserID = &uid ip := ctx.Request().RemoteAddr // 统一使用 ctx.RequestContext() 包含 context 内容 diff --git a/internal/service/douyin/order_sync.go b/internal/service/douyin/order_sync.go index 1a19ff9..0dcd890 100755 --- a/internal/service/douyin/order_sync.go +++ b/internal/service/douyin/order_sync.go @@ -533,7 +533,7 @@ func (s *service) fetchDouyinOrdersByBuyer(cookie string, buyer string, proxy st params.Set("appid", "1") params.Set("_bid", "ffa_order") params.Set("aid", "4272") - params.Set("__token", "a75db7d1f0eb7c205962656dMPidWeczHqu1PZ0Tes1wGdbAcooEJkdvUmpkpo27xfzAYfVbY1iwZHHpzmpDuk7Sa4/9nyfohqJZPBvhI3m8eGGxoi-OUAsPQ/d3EaLX60Uo2gQfqrKNloO9CAeNQ/Y8TrBcd-RhSxKIQCEqNp1uun6pzhR5") + params.Set("__token", "0b67ab6212a41bd1903f03d4f9a887f9") return s.fetchDouyinOrders(cookie, params, proxy) } @@ -1004,7 +1004,7 @@ func (s *service) SyncAllOrders(ctx context.Context, duration time.Duration, use // 临时:强制使用用户提供的最新 Cookie if len(cfg.Cookie) < 100 { - cfg.Cookie = "passport_csrf_token=afcc4debfeacce6454979bb9465999dc; passport_csrf_token_default=afcc4debfeacce6454979bb9465999dc; is_staff_user=false; zsgw_business_data=%7B%22uuid%22%3A%22fa769974-ba17-4daf-94cb-3162ba299c40%22%2C%22platform%22%3A%22pc%22%2C%22source%22%3A%22seo.fxg.jinritemai.com%22%7D; s_v_web_id=verify_mjqlw6yx_mNQjOEnB_oXBo_4Etb_AVQ9_7tQGH9WORNRy; SHOP_ID=47668214; PIGEON_CID=3501298428676440; x-web-secsdk-uid=663d5a20-e75c-4789-bc98-839744bf70bc; Hm_lvt_b6520b076191ab4b36812da4c90f7a5e=1766891015,1766979339,1767628404,1768381245; HMACCOUNT=95F3EBE1C47ED196; ttcid=7962a054674f4dd7bf895af73ae3f34142; passport_mfa_token=CjfZetGovLzEQb6MwoEpMQnvCSomMC9o0P776kEFy77vhrRCAdFvvrnTSpTXY2aib8hCdU5w3tQvGkoKPAAAAAAAAAAAAABP88E%2FGYNOqYg7lJ6fcoAzlVHbNi0bqTR%2Fru8noACGHR%2BtNjtq%2FnW9rBK32mcHCC5TzRDW8YYOGPax0WwgAiIBA3WMQyg%3D; source=seo.fxg.jinritemai.com; gfkadpd=4272,23756; csrf_session_id=b7b4150c5eeefaede4ef5e71473e9dc1; Hm_lpvt_b6520b076191ab4b36812da4c90f7a5e=1768381314; ttwid=1%7CAwu3-vdDBhOP12XdEzmCJlbyX3Qt_5RcioPVgjBIDps%7C1768381315%7Ca763fd05ed6fa274ed997007385cc0090896c597cfac0b812c962faf34f04897; tt_scid=f4YqIWnO3OdWrfVz0YVnJmYahx-qu9o9j.VZC2op7nwrQRodgrSh1ka0Ow3g5nyKd42a; odin_tt=bcf942ae72bd6b4b8f357955b71cc21199b6aec5e9acee4ce64f80704f08ea1cbaaa6e70f444f6a09712806aa424f4d0cce236e77b0bfa2991aa8a23dab27e1e; passport_auth_status=b3b3a865e0bd3857e6a28ea5a6854830%2C228cf6630632c26472c096506639ed6e; passport_auth_status_ss=b3b3a865e0bd3857e6a28ea5a6854830%2C228cf6630632c26472c096506639ed6e; uid_tt=4dfa662033e2e4eefe629ad8815f076f; uid_tt_ss=4dfa662033e2e4eefe629ad8815f076f; sid_tt=4cc6aa2f1a6e338ec72d663a0b611d3c; sessionid=4cc6aa2f1a6e338ec72d663a0b611d3c; sessionid_ss=4cc6aa2f1a6e338ec72d663a0b611d3c; PHPSESSID=a1b2fd062c1346e5c6f94bac3073cd7d; PHPSESSID_SS=a1b2fd062c1346e5c6f94bac3073cd7d; ucas_c0=CkEKBTEuMC4wEJOIgezc9NazaRjmJiD61rDnqc2DBCiwITCb1oDYuM3aB0Cpt53LBkip69nNBlC_vL6Ekt3t1GdYbhIU2LuS6yHmC8_SKu9Jok5ToGxfQIg; ucas_c0_ss=CkEKBTEuMC4wEJOIgezc9NazaRjmJiD61rDnqc2DBCiwITCb1oDYuM3aB0Cpt53LBkip69nNBlC_vL6Ekt3t1GdYbhIU2LuS6yHmC8_SKu9Jok5ToGxfQIg; ecom_gray_shop_id=156231010; sid_guard=4cc6aa2f1a6e338ec72d663a0b611d3c%7C1768381360%7C5184000%7CSun%2C+15-Mar-2026+09%3A02%3A40+GMT; session_tlb_tag=sttt%7C4%7CTMaqLxpuM47HLWY6C2EdPP________-x3_oZvMYjz8-Uw3dAm6JiPFDhS1ih9XTV79AgAO_5cvo%3D; sid_ucp_v1=1.0.0-KGRmNzNkZjM2YjUwZDk2M2M0MjQ5MGE2NzNkNGZkZjNhZWFhYmJkMmIKGQib1oDYuM3aBxCwt53LBhiwISAMOAZA9AcaAmxmIiA0Y2M2YWEyZjFhNmUzMzhlYzcyZDY2M2EwYjYxMWQzYw; ssid_ucp_v1=1.0.0-KGRmNzNkZjM2YjUwZDk2M2M0MjQ5MGE2NzNkNGZkZjNhZWFhYmJkMmIKGQib1oDYuM3aBxCwt53LBhiwISAMOAZA9AcaAmxmIiA0Y2M2YWEyZjFhNmUzMzhlYzcyZDY2M2EwYjYxMWQzYw; COMPASS_LUOPAN_DT=session_7595137429020049706; BUYIN_SASID=SID2_7595138116287152420" + cfg.Cookie = "s_v_web_id=verify_mm0pjkt7_rRCYDU7B_F5Yl_4UYj_8yQ0_ue0vAcKwYt3z; passport_csrf_token=fe2b51efeb70763190b402f49ad9f0e9; passport_csrf_token_default=fe2b51efeb70763190b402f49ad9f0e9; is_staff_user=false; Hm_lvt_b6520b076191ab4b36812da4c90f7a5e=1772792724,1773139856,1773407744,1773419459; HMACCOUNT=74DD13C46DE836FC; Hm_lpvt_b6520b076191ab4b36812da4c90f7a5e=1773419460; ttwid=1%7C71OUHp7yB34JMc3dVW9XMZxKJfcmzgfSzG407fx6Gqo%7C1773419462%7C940ba119bf375a540dbfb48b29ee8a53cc5ba26577ce549403b05104b21f131a; tt_scid=.nFHMbvBFFlz8.mmqZBwKQjwz0N1JvQ1I1w2MyHaGZmu3BVy3yPKL2ncw-E.ivZb5a00; odin_tt=0713b49299b28eef2e88f31274e12df7b1c89868218304a69fd95ec6182b253f256dc7582fcb2126d41015cc56ba2dd42664995c96204c5ae6006feaee932d5f; passport_auth_status=7fa995a5f448285c6c61bcd602ad9187%2Ce971f44a8160083bd1abde91277b1f99; passport_auth_status_ss=7fa995a5f448285c6c61bcd602ad9187%2Ce971f44a8160083bd1abde91277b1f99; uid_tt=adbd52561542b0cf10860338efe190fb; uid_tt_ss=adbd52561542b0cf10860338efe190fb; sid_tt=ab489ae34b28f997213fff3280b71961; sessionid=ab489ae34b28f997213fff3280b71961; sessionid_ss=ab489ae34b28f997213fff3280b71961; PHPSESSID=9aa8f439691b30a250dfc0235abc9e3e; PHPSESSID_SS=9aa8f439691b30a250dfc0235abc9e3e; ucas_c0=CkEKBTEuMC4wEKSIgpyE-47aaRjmJiD61rDnqc2DBCiwITCb1oDYuM3aB0Dp99DNBkjpq43QBlC_vL6Ekt3t1GdYbhIU-PnTyhrNrbd5k-QhCp6F7QMD3L0; ucas_c0_ss=CkEKBTEuMC4wEKSIgpyE-47aaRjmJiD61rDnqc2DBCiwITCb1oDYuM3aB0Dp99DNBkjpq43QBlC_vL6Ekt3t1GdYbhIU-PnTyhrNrbd5k-QhCp6F7QMD3L0; zsgw_business_data=%7B%22uuid%22%3A%2267d1b0e4-dca5-484d-997a-b70cb555e396%22%2C%22platform%22%3A%22pc%22%2C%22source%22%3A%22seo.fxg.jinritemai.com%22%7D; source=seo.fxg.jinritemai.com; gfkadpd=4272,23756; ecom_gray_shop_id=156231010; csrf_session_id=1579f92b6914e7cbfbc81471e18918fc; BUYIN_SASID=SID2_7616774858777182473; sid_guard=ab489ae34b28f997213fff3280b71961%7C1773419504%7C5184000%7CTue%2C+12-May-2026+16%3A31%3A44+GMT; session_tlb_tag=sttt%7C14%7Cq0ia40so-ZchP_8ygLcZYf_________D7K6IkHAPtWpxy0TIIHAoYAfUVdc7T8ZGVSu1iVCn8ZE%3D; sid_ucp_v1=1.0.0-KDMyOGFkNjY4OTZjM2NjNTM3ODNkMzFkMmVjMDIwOGRhMWQ2YmRmYjAKGQib1oDYuM3aBxDw99DNBhiwISAMOAZA9AcaAmxxIiBhYjQ4OWFlMzRiMjhmOTk3MjEzZmZmMzI4MGI3MTk2MQ; ssid_ucp_v1=1.0.0-KDMyOGFkNjY4OTZjM2NjNTM3ODNkMzFkMmVjMDIwOGRhMWQ2YmRmYjAKGQib1oDYuM3aBxDw99DNBhiwISAMOAZA9AcaAmxxIiBhYjQ4OWFlMzRiMjhmOTk3MjEzZmZmMzI4MGI3MTk2MQ; COMPASS_LUOPAN_DT=session_7616776334971060490" } startTime := time.Now().Add(-duration) diff --git a/internal/service/user/address_share.go b/internal/service/user/address_share.go index 4405f2e..fdb20d2 100755 --- a/internal/service/user/address_share.go +++ b/internal/service/user/address_share.go @@ -113,12 +113,12 @@ func (s *service) SubmitAddressShare(ctx context.Context, shareToken string, nam s.logger.Info("SubmitAddressShare: Processing", zap.Int64("invID", claims.InventoryID), zap.Int64("owner", claims.OwnerUserID)) // 1. 确定资产最终归属地 (实名转赠逻辑) - targetUserID := claims.OwnerUserID - isTransfer := false - if submittedByUserID != nil && *submittedByUserID > 0 && *submittedByUserID != claims.OwnerUserID { - targetUserID = *submittedByUserID - isTransfer = true + // 必须登录才能提交,submittedByUserID 由 API 层保证非空 + if submittedByUserID == nil || *submittedByUserID <= 0 { + return 0, fmt.Errorf("login_required") } + targetUserID := *submittedByUserID + isTransfer := targetUserID != claims.OwnerUserID var addrID int64 err = s.repo.GetDbW().Transaction(func(tx *gorm.DB) error { diff --git a/web/.DS_Store b/web/.DS_Store index 164f4c7..cc0c9cd 100755 Binary files a/web/.DS_Store and b/web/.DS_Store differ