WIP: fix(shipping): 使用资产价值快照价格确保发货与分解价格一致 #1

Draft
34310384 wants to merge 0 commits from zuncle into main
Collaborator

修复改价后发货价格与分解价格不一致的问题:

  • 发货时优先使用 user_inventory.value_cents 快照价格
  • 后台发货列表使用 shipping_records.price 存储的快照价格
  • 确保盈亏统计时价格数据准确一致
修复改价后发货价格与分解价格不一致的问题: - 发货时优先使用 user_inventory.value_cents 快照价格 - 后台发货列表使用 shipping_records.price 存储的快照价格 - 确保盈亏统计时价格数据准确一致
34310384 added 1 commit 2026-03-05 18:15:44 +08:00
修复改价后发货价格与分解价格不一致的问题:
- 发货时优先使用 user_inventory.value_cents 快照价格
- 后台发货列表使用 shipping_records.price 存储的快照价格
- 确保盈亏统计时价格数据准确一致
34310384 added 1 commit 2026-03-08 16:04:30 +08:00
问题描述:
用户退单后,翻牌游戏资格会重新出现(被重置),但用户已经抽过奖了。
这导致用户可以通过退款获得额外的翻牌机会。

根本原因:
退款处理逻辑 reclaimLivestreamAssets 只回收了 user_inventory 中的实物奖品,
但没有回收 user_game_tickets 中的翻牌游戏资格。

解决方案:
在 reclaimLivestreamAssets 函数后添加 reclaimFlipCardTicket 函数,
用于检测并回收翻牌游戏资格:

1. 通过 shop_order_id 查询抖店订单获取商品ID
2. 查询 douyin_product_rewards 表检查商品是否配置了翻牌游戏奖励
   - 检查 reward_type = 'game_ticket'
   - 检查 payload.game_code = 'flip_card'
3. 如果配置了翻牌奖励,回收用户的翻牌次数
   - 扣减 user_game_tickets.available
   - 扣减 user_game_tickets.total_earned
4. 在 game_ticket_logs 表中记录回收日志

影响范围:
- 仅影响配置了翻牌游戏奖励的商品订单退款
- 退款时会同步回收翻牌游戏资格
- 已使用过的翻牌次数不会被回收(只回收 available > 0 的记录)

测试建议:
1. 购买配置了翻牌奖励的商品
2. 进行翻牌游戏
3. 申请退款
4. 验证翻牌资格是否被正确回收
34310384 added 1 commit 2026-03-11 14:18:05 +08:00
- SubmitAddressShare 事务内 SELECT FOR UPDATE 锁定资产行,防止并发重复提交
- 检查 UPDATE RowsAffected,静默失败时回滚事务
- 防重检查从 readDB 移入事务内写库,消除主从延迟竞态
- RedeemInventoryToPoints/RedeemInventoriesToPoints 添加转赠来源校验,
  禁止通过转赠获得的资产兑换积分
34310384 added 3 commits 2026-03-15 13:23:22 +08:00
1. SELECT FOR UPDATE 锁定资产行,防止并发转赠竞态条件
2. 检查 RowsAffected 防止 GORM 静默失败导致空壳发货记录
3. 兑换积分时校验转赠来源,禁止转赠资产兑换积分
4. 转赠来源校验改用写库查询,避免主从延迟绕过
5. 转赠来源查询错误不再静默忽略,失败时返回错误

基于 zuncle 分支修复,额外修正了两个安全隐患:
- RedeemInventoryToPoints/RedeemInventoriesToPoints 中
  转赠记录查询从 readDB 改为 writeDB
- Count()/Find() 返回的 error 不再丢弃
经数据核实,转赠后兑换积分属于合法行为(资产转赠后归接收方所有)。
并发漏洞虽然产生了重复转赠/发货记录,但实际经济损失为 0 元:
- 18 个重复发货资产中,没有任何一个真正被两方都发了货
- 没有任何资产被重复兑换积分

保留前两个并发修复(SELECT FOR UPDATE + RowsAffected 检查),
回退第三个业务限制(禁止转赠资产兑换积分)。
接收者未登录时提交地址会错误保存到赠送者名下,现改为:
- API层:登录态从可选改为必选,未登录返回401
- Service层:始终用提交者ID作为地址归属人
34310384 added 1 commit 2026-03-17 18:11:35 +08:00
- 在cancel_shipping.go中添加48小时限制检查
- 在shipping_groups.go中返回created_at字段供前端判断
- 超过48小时的订单撤销时提示'需要撤销发货请联系客服'
34310384 added 1 commit 2026-03-17 19:23:34 +08:00
- 新增优惠券总价值统计(关联system_coupons表)
- 新增次卡总价值统计(关联activities表price_draw)
- 使用Raw SQL执行复杂JOIN查询
34310384 added 1 commit 2026-03-17 21:20:11 +08:00
订单取消退券逻辑依赖 used_order_id 匹配,但金额券在下单时
不设置 used_order_id(仅在支付确认后设置),导致未支付订单
取消时 WHERE 条件匹配不到行,退券静默失败。

修复:去掉 used_order_id 条件,按券 ID 直接退还,增加幂等
校验和错误处理,兜底从流水回推预扣金额。
34310384 added 1 commit 2026-03-18 20:16:16 +08:00
- 新增 AdminCancelShipping handler,支持批量撤销待发货记录(status=1→5)
- 事务内同步恢复 user_inventory.status=1 并清空 shipping_no
- 在 remark 记录操作人 adminID,保证审计可追溯
- 注册路由 POST /api/admin/shipping/orders/cancel
34310384 added 1 commit 2026-03-18 21:12:50 +08:00
34310384 added 1 commit 2026-03-18 21:58:30 +08:00
- store.go: 积分商城优惠券列表加 valid_end > now 过滤
- coupons_list.go: 修复 NULL valid_end 被错误排除,无截止日期券正确显示为有效
- activity_order_service.go: 过期/不可用券下单返回明确错误,不再静默跳过
- points_redeem_coupon_app.go: 积分兑换前校验模板 valid_end
- coupon_add.go: 发券前校验模板 valid_end,过期拒绝发放
34310384 added 1 commit 2026-03-19 16:29:28 +08:00
34310384 added 1 commit 2026-03-20 17:45:23 +08:00
- source_type=4: 区分购买次卡/次卡抽奖/一番赏
- source_type=5: 区分运费订单/直播间抽奖
34310384 added 1 commit 2026-03-20 20:32:46 +08:00
问题:
  commit b9a40df 修复了 CancelOrder()(用户/管理员主动取消)的券退回逻辑,
  去掉了 `AND status = 4` 条件,但遗漏了 order_timeout.go 中超时取消的
  同一逻辑,导致次数卡包(game_pass_package)订单超时取消时金额券余额丢失。

根因:
  game_pass_package 下单时,金额券(type=1)通过 applyCouponToGamePassOrder()
  直接扣减 balance_amount 并保持 status=1(有余额)或 status=2(用完),
  不会设置 status=4(预扣中)。而 cancelExpiredOrder() 的 UPDATE 语句带有
  `WHERE id = ? AND status = 4` 条件,导致匹配不到行,退券静默失败。

  生产已确认影响:用户9110的券1690(订单28229)和券1532(订单26743)
  因此bug各丢失10元余额。

修复:
  - 去掉 `AND status = 4` 条件,改为 `WHERE id = ?`,兼容所有券状态
  - 新增幂等校验:先查 timeout_refund 流水是否已存在,防止重复退还
  - 新增兜底逻辑:order_coupons 无记录时,从 user_coupon_ledger 流水
    回推预扣金额,与 CancelOrder() 的修复方案完全对齐
34310384 added 1 commit 2026-03-20 20:39:55 +08:00
问题:
  盈亏分析(GetUserProfitLossTrend)和用户画像(GetUserProfile)中的
  "商品产出"查询条件为 `ui.status = 1`,只统计了待发货/库存中的商品,
  已发货/已兑换(status=3)的商品被完全排除。

  示例:用户9110实际累计获得874件商品(价值¥16,279.40),但因为大部分
  已发货(status=3),盈亏分析只显示商品产出¥12.50,全资产产出严重偏低。

  而 dashboard_user_spending.go 中的同类查询正确使用了
  `status IN (1, 3)`,说明此处是遗漏。

修复:
  - users_profit_loss.go: 当前资产快照查询改为 `status IN (1, 3)`
  - users_profile.go: 库存统计查询改为 `status IN (1, 3)`
  - 与 dashboard_user_spending.go 的计算口径对齐
34310384 added 1 commit 2026-03-20 21:02:27 +08:00
问题:
  盈亏分析(GetUserProfitLossTrend)和用户画像(GetUserProfile)中的
  "商品产出"与玩家消费排行榜(DashboardPlayerSpendingLeaderboard)
  计算口径不一致,导致同一用户的商品产出差异巨大。

  排行榜显示用户9110商品产出¥16,913.40,盈亏分析显示¥0.00。

差异点:
  1. status条件: 盈亏用 status=1(仅待发货),排行榜用 status IN (1,3)
  2. 价格回退链: 盈亏缺少 price_snapshot_cents 回退层级
  3. 道具卡倍率: 盈亏未计算 reward_multiplier_x1000
  4. void排除: 盈亏未排除 remark 含 void 的作废项

修复:
  - users_profit_loss.go: 商品产出查询完全对齐排行榜公式
  - users_profile.go: 库存价值查询同步对齐
  - 公式: COALESCE(value_cents, snapshot_cents, price, 0)
          * GREATEST(COALESCE(multiplier, 1000), 1000) / 1000
  - 条件: status IN (1,3) AND remark NOT LIKE '%void%'
34310384 added 1 commit 2026-03-20 21:19:22 +08:00
1. CAST修复:
   MySQL的 / 运算符返回DECIMAL类型,GORM无法将DECIMAL扫描进int64,
   导致商品产出静默返回0。添加 CAST(... AS SIGNED) 与排行榜对齐。

2. 视角统一:
   盈亏分析原为用户视角(B-A),排行榜为平台视角(A-B),同一用户
   一个显示+¥12,130(用户赚了),一个显示-¥12,127(平台亏了),
   造成管理员困惑。

   修改:
   - 净盈亏: Value-Cost → Cost-Value (A-B,平台盈利为正)
   - 盈亏比: Value/Cost → Cost/Value (A/B,>1表示平台盈利)
   - 趋势图每个数据点同步调整
34310384 added 1 commit 2026-03-23 22:28:44 +08:00
- products 表新增 cost_price 字段(成本价/分)
- activity_reward_settings 新增 drop_quantity(单次产出数量,默认1)
  和 cost_snapshot_cents(成本价快照)
- 奖品创建/修改时自动快照成本价,drop_quantity 限制 1-100
- 抽奖发放逻辑按 drop_quantity 循环创建多个库存项
- 抽奖结果接口按 drop_quantity 返回多条 item,前端自动合并显示
- 抽奖记录接口返回 drop_quantity 字段
- 商品管理 API 全链路支持 cost_price
34310384 added 1 commit 2026-03-24 17:11:34 +08:00
将 /store/items 和 /product_categories 从 appAuthApiRouter
移至 appPublicApiRouter,配合小程序端解决微信审核问题。
34310384 added 1 commit 2026-03-25 22:02:21 +08:00
rewards 接口的 rewardItem 新增 price_snapshot_cents 字段,
将数据库中的商品价格快照(分)暴露给前端用于展示参考价。
34310384 added 1 commit 2026-03-26 00:04:27 +08:00
1. Revenue 口径统一为 actual_amount(真实现金到账)
   - 优惠券(discount_amount)和积分(points_amount)是平台免费发放的营销补贴,
     不算收入,改为展示字段
   - 涉及: profit_metrics.go, dashboard_spending.go, users_profit_loss.go,
     dashboard_user_spending.go, activity_rankings_admin.go

2. Cost 口径统一为奖品库存价值
   - 删除 finance service 中的积分成本扫描(Step 3)和优惠券成本扫描(Step 4)
   - 之前优惠券同时算在收入和成本两侧,导致利润被人为压低
   - 涉及: query_user.go, query_activity.go

3. 统一 value_cents fallback chain
   - finance service 改为与排行榜一致的三级回退:
     COALESCE(NULLIF(value_cents,0), price_snapshot_cents, products.price, 0)
   - 涉及: query_user.go, query_activity.go

4. 活动盈亏收入统一到 finance service
   - 删除 dashboard_activity.go 自有的 revenue SQL(含比例分摊逻辑)
   - 收入和成本统一由 finance.Service.QueryActivityProfitLoss() 提供
   - 修复日志明细 profit:道具卡倍率改用 ComputePrizeCostWithMultiplier

5. finance service 新增展示字段
   - ProfitLossDetail 增加 CouponDiscount, PointsDiscount, GamePassValue
   - 不参与 Revenue/Cost/Profit 计算,仅供前端展示营销补贴明细

6. 修复对对碰次卡订单 discount_amount 数据污染
   - matching_game_app.go 次卡下单时 DiscountAmount 错误设为活动全价
   - 改为 0(次卡支付不涉及优惠券)
   - 附带历史数据修复 migration SQL

7. 排除已分解奖品的成本重复计算
   - 用户可以把奖品分解成积分再兑换新商品,导致同一份价值被计算两次
   - 所有库存查询增加排除条件: status=3 且 remark 含 redeemed_points 或 batch_redeemed
   - 涉及 6 个文件的库存成本/资产查询

8. 排行榜详情抽屉限定活动范围
   - prize 查询增加 activity_id > 0 过滤,排除积分兑换/转入/合成等非活动产出
   - 使排行榜与其详情抽屉口径一致

修改文件(12个):
- internal/service/finance/profit_metrics.go
- internal/service/finance/query_user.go
- internal/service/finance/query_activity.go
- internal/service/finance/types.go
- internal/api/admin/dashboard_activity.go
- internal/api/admin/dashboard_spending.go
- internal/api/admin/dashboard_user_spending.go
- internal/api/admin/users_profit_loss.go
- internal/api/admin/users_profile.go
- internal/api/admin/activity_rankings_admin.go
- internal/api/activity/matching_game_app.go
- migrations/20260325_fix_matching_gamepass_discount.sql
34310384 added 1 commit 2026-03-26 14:37:35 +08:00
问题背景:
- 平台审核失败,原因:小程序页面未完整浏览即要求授权登录,属于登录规范不合规
- GET /api/app/products/:id 位于认证路由组,未登录用户访问返回 401,
  触发前端全局拦截器弹出"账号登录已过期"弹窗

修改内容:
1. router.go: 将 GET /products/:id 从 appAuthApiRouter(认证组)
   移至 appPublicApiRouter(公开组),允许未登录用户浏览商品详情
2. product.go: 针对公开端点增加安全加固
   - 增加商品 ID 参数校验(ParseInt 错误和 id<=0)
   - 将兜底错误信息从 validation.Error(err) 改为通用提示
     "商品信息获取失败",防止原始 DB 错误泄露给未认证请求

影响范围:
- 仅影响 GET /api/app/products/:id 端点
- GET /api/app/products(商品列表)仍保留在认证组
- 该 handler 不依赖 JWT session 上下文,移至公开组后功能无变化
- 返回数据仅包含商品元信息(名称、图片、价格、库存),不含用户敏感数据
34310384 added 1 commit 2026-03-31 21:08:45 +08:00
- 运费预下单接口:< 5 件允许创建运费订单,>= 5 件拒绝
- 批量发货接口:< 5 件时强制校验已支付运费订单
- 单件发货接口:同样强制校验已支付运费订单
- 运费订单 remark 存入 inventory_ids JSON 用于关联
34310384 added 1 commit 2026-04-03 18:03:45 +08:00
34310384 added 1 commit 2026-04-04 00:06:32 +08:00
修复 sales_draw_trend 自定义日期按 UTC 解析导致的统计偏差,并统一使用半开区间处理日维度边界,补充上海时区与单日范围回归测试。
34310384 added 2 commits 2026-04-10 01:21:05 +08:00
Replace channel cost aggregation with draw-source based cost calculation
that follows activity profit-loss logic and keeps cost attribution on the
original ordering user's channel. Update channel stats tests to cover the
new cost path and related schema fields.
让渠道分析弹窗的概览卡片和趋势图共用同一时间筛选范围,
并将默认行为调整为历史全量展示。前端改为在未选择日期时
不再回退最近 12 天,后端新增统一时间窗口解析逻辑,使
overview 与 daily 在全量、指定区间和 days 模式下保持一致。
34310384 added 1 commit 2026-04-12 21:39:11 +08:00
将直播间统计从基于 user_inventory 当前持有状态和 remark 反推成本,改为基于 livestream_draw_logs 中奖事实直接关联 products.cost_price 计算成本。统一 /livestream/activities/:id/stats 与 /livestream/activities/:id/draw_logs 两个接口的营收、退款、成本和净利润口径,避免因转赠、remark 覆盖或订单行缺失导致统计失真,并补充针对转赠、退款、零订单和 product 回退场景的回归测试。
34310384 added 1 commit 2026-04-13 19:29:51 +08:00
将玩家消费排行榜和用户消费详情中的产出成本口径从 user_inventory 快照值改为订单抽奖链路成本。
排行榜按 user_id + 活动分类聚合,详情按 user_id + activity_id 聚合,成本统一基于已支付订单对应的 draw log、奖励配置、商品成本价、掉落数量和翻倍道具卡补量规则计算,使列表、详情与活动盈亏页的业务口径保持一致。
34310384 added 1 commit 2026-04-14 23:16:44 +08:00
活动奖品分析接口统一使用 products.cost_price 计算 prizeValue、cost 和 totalCost,避免误用售价导致成本统计偏差。
This pull request is marked as a work in progress.
This branch is out-of-date with the base branch

Checkout

From your project repository, check out a new branch and test the changes.
git fetch -u origin zuncle:zuncle
git checkout zuncle
Sign in to join this conversation.
No Reviewers
No Label
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: zfc/bindbox-game#1
No description provided.