邹方成 a7a0f639e1 feat: 新增取消发货功能并优化任务中心
fix: 修复微信通知字段截断导致的编码错误
feat: 添加有效邀请相关字段和任务中心常量
refactor: 重构一番赏奖品格位逻辑
perf: 优化道具卡列表聚合显示
docs: 更新项目说明文档和API文档
test: 添加字符串截断工具测试
2025-12-23 22:26:07 +08:00

616 lines
19 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

package user
import (
"context"
"strings"
"bindbox-game/internal/repository/mysql/model"
)
// DrawReceiptInfo 抽奖验证凭据信息
type DrawReceiptInfo struct {
DrawLogID int64 `json:"draw_log_id"`
RewardID int64 `json:"reward_id,omitempty"`
DrawIndex int `json:"draw_index"`
AlgoVersion string `json:"algo_version"`
RoundID int64 `json:"round_id"`
DrawID int64 `json:"draw_id"`
ClientID int64 `json:"client_id"`
Timestamp int64 `json:"timestamp"`
ServerSeedHash string `json:"server_seed_hash"`
ServerSubSeed string `json:"server_sub_seed"`
ClientSeed string `json:"client_seed"`
Nonce int64 `json:"nonce"`
ItemsRoot string `json:"items_root"`
WeightsTotal int64 `json:"weights_total"`
SelectedIndex int32 `json:"selected_index"`
RandProof string `json:"rand_proof"`
Signature string `json:"signature,omitempty"`
ItemsSnapshot string `json:"items_snapshot,omitempty"`
}
// OrderWithItems 包含订单项的订单信息
type OrderWithItems struct {
*model.Orders
Items []*model.OrderItems `json:"items"`
ActivityName string `json:"activity_name"`
ActivityID int64 `json:"activity_id,omitempty"`
PlayType string `json:"play_type,omitempty"`
CategoryID int64 `json:"category_id,omitempty"`
CategoryName string `json:"category_name,omitempty"`
IssueNumber string `json:"issue_number"`
IsDraw bool `json:"is_draw"`
IsWinner bool `json:"is_winner"`
RewardLevel int32 `json:"reward_level"`
DrawReceipts []*DrawReceiptInfo `json:"draw_receipts"`
CouponInfo *CouponSimpleInfo `json:"coupon_info,omitempty"`
ItemCardInfo *ItemCardSimpleInfo `json:"item_card_info,omitempty"`
}
type CouponSimpleInfo struct {
UserCouponID int64 `json:"user_coupon_id"`
Name string `json:"name"`
Type int32 `json:"type"` // 1:满减 2:折扣
Value int64 `json:"value"`
}
type ItemCardSimpleInfo struct {
UserCardID int64 `json:"user_card_id"`
Name string `json:"name"`
EffectType int32 `json:"effect_type"`
}
func (s *service) ListOrders(ctx context.Context, userID int64, page, pageSize int) (items []*model.Orders, total int64, err error) {
// 查询用户的所有订单,包括商城直购(1)、抽奖票据(2)和系统发放(3)
q := s.readDB.Orders.WithContext(ctx).ReadDB().Where(s.readDB.Orders.UserID.Eq(userID))
total, err = q.Count()
if err != nil {
return nil, 0, err
}
if page <= 0 {
page = 1
}
if pageSize <= 0 {
pageSize = 20
}
if pageSize > 100 {
pageSize = 100
}
items, err = q.Order(s.readDB.Orders.ID.Desc()).Offset((page - 1) * pageSize).Limit(pageSize).Find()
if err != nil {
return nil, 0, err
}
return items, total, nil
}
// GetOrderWithItems 查询单个订单详情
func (s *service) GetOrderWithItems(ctx context.Context, userID int64, orderID int64) (*OrderWithItems, error) {
order, err := s.readDB.Orders.WithContext(ctx).ReadDB().Where(s.readDB.Orders.ID.Eq(orderID), s.readDB.Orders.UserID.Eq(userID)).First()
if err != nil {
return nil, err
}
if order == nil {
return nil, nil
}
res := &OrderWithItems{
Orders: order,
}
items, err := s.readDB.OrderItems.WithContext(ctx).ReadDB().Where(s.readDB.OrderItems.OrderID.Eq(order.ID)).Find()
if err != nil {
return nil, err
}
if len(items) > 0 {
ids := make(map[int64]struct{})
for _, it := range items {
if strings.TrimSpace(it.Title) == "" || strings.TrimSpace(it.Title) == "系统发放奖励" || it.ProductImages == "" || it.ProductImages == "[]" {
ids[it.ProductID] = struct{}{}
}
}
if len(ids) > 0 {
pidList := make([]int64, 0, len(ids))
for id := range ids {
pidList = append(pidList, id)
}
pros, err2 := s.readDB.Products.WithContext(ctx).ReadDB().Where(s.readDB.Products.ID.In(pidList...)).Find()
if err2 != nil {
return nil, err2
}
pm := make(map[int64]*model.Products, len(pros))
for _, p := range pros {
pm[p.ID] = p
}
for _, it := range items {
p := pm[it.ProductID]
if p == nil {
continue
}
if strings.TrimSpace(it.Title) == "" || strings.TrimSpace(it.Title) == "系统发放奖励" {
it.Title = p.Name
}
if it.ProductImages == "" || it.ProductImages == "[]" {
it.ProductImages = p.ImagesJSON
}
}
}
res.Items = items
}
// 补充优惠券和道具卡信息
if order.CouponID > 0 {
if uc, _ := s.readDB.UserCoupons.WithContext(ctx).ReadDB().Where(s.readDB.UserCoupons.ID.Eq(order.CouponID)).First(); uc != nil {
if sc, _ := s.readDB.SystemCoupons.WithContext(ctx).ReadDB().Where(s.readDB.SystemCoupons.ID.Eq(uc.CouponID)).First(); sc != nil {
val := sc.DiscountValue
// 尝试查询实际抵扣金额
if oc, _ := s.readDB.OrderCoupons.WithContext(ctx).ReadDB().Where(
s.readDB.OrderCoupons.OrderID.Eq(order.ID),
s.readDB.OrderCoupons.UserCouponID.Eq(order.CouponID),
).First(); oc != nil {
val = oc.AppliedAmount
}
res.CouponInfo = &CouponSimpleInfo{
UserCouponID: uc.ID,
Name: sc.Name,
Type: sc.DiscountType,
Value: val,
}
}
}
}
if order.ItemCardID > 0 {
if uc, _ := s.readDB.UserItemCards.WithContext(ctx).ReadDB().Where(s.readDB.UserItemCards.ID.Eq(order.ItemCardID)).First(); uc != nil {
if sc, _ := s.readDB.SystemItemCards.WithContext(ctx).ReadDB().Where(s.readDB.SystemItemCards.ID.Eq(uc.CardID)).First(); sc != nil {
res.ItemCardInfo = &ItemCardSimpleInfo{
UserCardID: uc.ID,
Name: sc.Name,
EffectType: sc.EffectType,
}
}
}
}
// 补充开奖信息
logs, _ := s.readDB.ActivityDrawLogs.WithContext(ctx).ReadDB().Where(s.readDB.ActivityDrawLogs.OrderID.Eq(order.ID)).Find()
if len(logs) > 0 {
res.IsDraw = true
// 取第一条记录的信息
log := logs[0]
res.IsWinner = log.IsWinner == 1
res.RewardLevel = log.Level
issue, _ := s.readDB.ActivityIssues.WithContext(ctx).ReadDB().Where(s.readDB.ActivityIssues.ID.Eq(log.IssueID)).First()
if issue != nil {
res.IssueNumber = issue.IssueNumber
act, _ := s.readDB.Activities.WithContext(ctx).ReadDB().Where(s.readDB.Activities.ID.Eq(issue.ActivityID)).First()
if act != nil {
res.ActivityName = act.Name
res.ActivityID = act.ID
res.PlayType = act.PlayType
res.CategoryID = act.ActivityCategoryID
if act.ActivityCategoryID > 0 {
cat, _ := s.readDB.ActivityCategories.WithContext(ctx).ReadDB().Where(s.readDB.ActivityCategories.ID.Eq(act.ActivityCategoryID)).First()
if cat != nil {
res.CategoryName = cat.Name
}
}
}
}
// 查询抽奖凭据
drawLogIDs := make([]int64, len(logs))
drawLogIDToIndex := make(map[int64]int)
for i, lg := range logs {
drawLogIDs[i] = lg.ID
drawLogIDToIndex[lg.ID] = i + 1
}
receipts, _ := s.readDB.ActivityDrawReceipts.WithContext(ctx).ReadDB().Where(s.readDB.ActivityDrawReceipts.DrawLogID.In(drawLogIDs...)).Find()
if len(receipts) > 0 {
res.DrawReceipts = make([]*DrawReceiptInfo, 0, len(receipts))
for _, r := range receipts {
drawIndex := drawLogIDToIndex[r.DrawLogID]
// 找到对应的 reward_id
var rewardID int64
for _, lg := range logs {
if lg.ID == r.DrawLogID {
rewardID = lg.RewardID
break
}
}
res.DrawReceipts = append(res.DrawReceipts, &DrawReceiptInfo{
DrawLogID: r.DrawLogID,
RewardID: rewardID,
DrawIndex: drawIndex,
AlgoVersion: r.AlgoVersion,
RoundID: r.RoundID,
DrawID: r.DrawID,
ClientID: r.ClientID,
Timestamp: r.Timestamp,
ServerSeedHash: r.ServerSeedHash,
ServerSubSeed: "",
ClientSeed: r.ClientSeed,
Nonce: r.Nonce,
ItemsRoot: r.ItemsRoot,
WeightsTotal: r.WeightsTotal,
SelectedIndex: r.SelectedIndex,
RandProof: r.RandProof,
Signature: r.Signature,
ItemsSnapshot: r.ItemsSnapshot,
})
}
}
}
// Special handling for Ichiban Kuji: Do not show Item Card
if res.PlayType == "ichiban" {
res.ItemCardInfo = nil
}
return res, nil
}
// ListOrdersWithItems 查询用户的订单列表,包含订单项详情
func (s *service) ListOrdersWithItems(ctx context.Context, userID int64, status int32, isConsumed *int32, page, pageSize int) (items []*OrderWithItems, total int64, err error) {
// 查询用户的所有订单,包括商城直购(1)、抽奖票据(2)和系统发放(3)
q := s.readDB.Orders.WithContext(ctx).ReadDB().Where(s.readDB.Orders.UserID.Eq(userID))
if status > 0 {
q = q.Where(s.readDB.Orders.Status.Eq(status))
}
if isConsumed != nil {
q = q.Where(s.readDB.Orders.IsConsumed.Eq(*isConsumed))
}
total, err = q.Count()
if err != nil {
return nil, 0, err
}
if page <= 0 {
page = 1
}
if pageSize <= 0 {
pageSize = 20
}
if pageSize > 100 {
pageSize = 100
}
// 查询订单列表
orders, err := q.Order(s.readDB.Orders.ID.Desc()).Offset((page - 1) * pageSize).Limit(pageSize).Find()
if err != nil {
return nil, 0, err
}
// 构建订单ID列表
orderIDs := make([]int64, len(orders))
for i, order := range orders {
orderIDs[i] = order.ID
}
var allItems []*model.OrderItems
if len(orderIDs) > 0 {
allItems, err = s.readDB.OrderItems.WithContext(ctx).ReadDB().Where(s.readDB.OrderItems.OrderID.In(orderIDs...)).Find()
if err != nil {
return nil, 0, err
}
ids := make(map[int64]struct{})
for _, it := range allItems {
if strings.TrimSpace(it.Title) == "" || strings.TrimSpace(it.Title) == "系统发放奖励" || it.ProductImages == "" || it.ProductImages == "[]" {
ids[it.ProductID] = struct{}{}
}
}
if len(ids) > 0 {
pidList := make([]int64, 0, len(ids))
for id := range ids {
pidList = append(pidList, id)
}
pros, err2 := s.readDB.Products.WithContext(ctx).ReadDB().Where(s.readDB.Products.ID.In(pidList...)).Find()
if err2 != nil {
return nil, 0, err2
}
pm := make(map[int64]*model.Products, len(pros))
for _, p := range pros {
pm[p.ID] = p
}
for _, it := range allItems {
p := pm[it.ProductID]
if p == nil {
continue
}
if strings.TrimSpace(it.Title) == "" || strings.TrimSpace(it.Title) == "系统发放奖励" {
it.Title = p.Name
}
if it.ProductImages == "" || it.ProductImages == "[]" {
it.ProductImages = p.ImagesJSON
}
}
}
}
// 构建订单ID到订单项的映射
itemsMap := make(map[int64][]*model.OrderItems)
for _, item := range allItems {
itemsMap[item.OrderID] = append(itemsMap[item.OrderID], item)
}
// 批量查询活动开奖信息
drawLogsListMap := make(map[int64][]*model.ActivityDrawLogs) // orderID -> logs
activityMap := make(map[int64]*model.Activities)
issueMap := make(map[int64]*model.ActivityIssues)
var allDrawLogIDs []int64
if len(orderIDs) > 0 {
logs, _ := s.readDB.ActivityDrawLogs.WithContext(ctx).ReadDB().Where(s.readDB.ActivityDrawLogs.OrderID.In(orderIDs...)).Find()
var issueIDs []int64
for _, log := range logs {
drawLogsListMap[log.OrderID] = append(drawLogsListMap[log.OrderID], log)
issueIDs = append(issueIDs, log.IssueID)
allDrawLogIDs = append(allDrawLogIDs, log.ID)
}
if len(issueIDs) > 0 {
issues, _ := s.readDB.ActivityIssues.WithContext(ctx).ReadDB().Where(s.readDB.ActivityIssues.ID.In(issueIDs...)).Find()
var activityIDs []int64
for _, issue := range issues {
issueMap[issue.ID] = issue
activityIDs = append(activityIDs, issue.ActivityID)
}
if len(activityIDs) > 0 {
activities, _ := s.readDB.Activities.WithContext(ctx).ReadDB().Where(s.readDB.Activities.ID.In(activityIDs...)).Find()
for _, act := range activities {
activityMap[act.ID] = act
}
}
}
// 为没有开奖记录的抽奖订单补充查询活动信息
var extraActIDs []int64
for _, order := range orders {
if order.SourceType == 2 {
if _, hasLogs := drawLogsListMap[order.ID]; !hasLogs {
actID := parseActivityIDFromRemark(order.Remark)
if actID > 0 {
if _, exists := activityMap[actID]; !exists {
extraActIDs = append(extraActIDs, actID)
}
}
}
}
}
if len(extraActIDs) > 0 {
extraActs, _ := s.readDB.Activities.WithContext(ctx).ReadDB().Where(s.readDB.Activities.ID.In(extraActIDs...)).Find()
for _, act := range extraActs {
activityMap[act.ID] = act
}
}
}
// 批量查询活动分类信息
categoryMap := make(map[int64]*model.ActivityCategories)
var categoryIDs []int64
for _, act := range activityMap {
if act.ActivityCategoryID > 0 {
categoryIDs = append(categoryIDs, act.ActivityCategoryID)
}
}
if len(categoryIDs) > 0 {
categories, _ := s.readDB.ActivityCategories.WithContext(ctx).ReadDB().Where(s.readDB.ActivityCategories.ID.In(categoryIDs...)).Find()
for _, cat := range categories {
categoryMap[cat.ID] = cat
}
}
// 批量查询抽奖凭据
receiptsMap := make(map[int64]*model.ActivityDrawReceipts) // drawLogID -> receipt
if len(allDrawLogIDs) > 0 {
receipts, _ := s.readDB.ActivityDrawReceipts.WithContext(ctx).ReadDB().Where(s.readDB.ActivityDrawReceipts.DrawLogID.In(allDrawLogIDs...)).Find()
for _, r := range receipts {
receiptsMap[r.DrawLogID] = r
}
}
// 批量查询优惠券和道具卡信息
userCouponIDs := make([]int64, 0)
userItemCardIDs := make([]int64, 0)
for _, order := range orders {
if order.CouponID > 0 {
userCouponIDs = append(userCouponIDs, order.CouponID)
}
if order.ItemCardID > 0 {
userItemCardIDs = append(userItemCardIDs, order.ItemCardID)
}
}
couponMap := make(map[int64]*CouponSimpleInfo)
// orderID -> userCouponID -> appliedAmount
appliedAmountMap := make(map[int64]map[int64]int64)
if len(userCouponIDs) > 0 {
// 查询优惠券基本信息
userCoupons, _ := s.readDB.UserCoupons.WithContext(ctx).ReadDB().Where(s.readDB.UserCoupons.ID.In(userCouponIDs...)).Find()
var sysCouponIDs []int64
ucMap := make(map[int64]*model.UserCoupons)
for _, uc := range userCoupons {
sysCouponIDs = append(sysCouponIDs, uc.CouponID)
ucMap[uc.ID] = uc
}
if len(sysCouponIDs) > 0 {
sysCoupons, _ := s.readDB.SystemCoupons.WithContext(ctx).ReadDB().Where(s.readDB.SystemCoupons.ID.In(sysCouponIDs...)).Find()
scMap := make(map[int64]*model.SystemCoupons)
for _, sc := range sysCoupons {
scMap[sc.ID] = sc
}
for id, uc := range ucMap {
if sc, ok := scMap[uc.CouponID]; ok {
couponMap[id] = &CouponSimpleInfo{
UserCouponID: uc.ID,
Name: sc.Name,
Type: sc.DiscountType,
Value: sc.DiscountValue, // 默认使用面值,后面会尝试用实际抵扣金额覆盖
}
}
}
}
// 查询订单实际使用的优惠券金额
if len(orderIDs) > 0 {
ocs, _ := s.readDB.OrderCoupons.WithContext(ctx).ReadDB().Where(
s.readDB.OrderCoupons.OrderID.In(orderIDs...),
s.readDB.OrderCoupons.UserCouponID.In(userCouponIDs...),
).Find()
for _, oc := range ocs {
if _, ok := appliedAmountMap[oc.OrderID]; !ok {
appliedAmountMap[oc.OrderID] = make(map[int64]int64)
}
appliedAmountMap[oc.OrderID][oc.UserCouponID] = oc.AppliedAmount
}
}
}
itemCardMap := make(map[int64]*ItemCardSimpleInfo)
if len(userItemCardIDs) > 0 {
userCards, _ := s.readDB.UserItemCards.WithContext(ctx).ReadDB().Where(s.readDB.UserItemCards.ID.In(userItemCardIDs...)).Find()
var sysCardIDs []int64
ucMap := make(map[int64]*model.UserItemCards)
for _, uc := range userCards {
sysCardIDs = append(sysCardIDs, uc.CardID)
ucMap[uc.ID] = uc
}
if len(sysCardIDs) > 0 {
sysCards, _ := s.readDB.SystemItemCards.WithContext(ctx).ReadDB().Where(s.readDB.SystemItemCards.ID.In(sysCardIDs...)).Find()
scMap := make(map[int64]*model.SystemItemCards)
for _, sc := range sysCards {
scMap[sc.ID] = sc
}
for id, uc := range ucMap {
if sc, ok := scMap[uc.CardID]; ok {
itemCardMap[id] = &ItemCardSimpleInfo{
UserCardID: uc.ID,
Name: sc.Name,
EffectType: sc.EffectType,
}
}
}
}
}
// 构建返回结果
items = make([]*OrderWithItems, len(orders))
for i, order := range orders {
// 复制一份 CouponInfo因为同一个优惠券可能被多个订单使用但金额不同
var cInfo *CouponSimpleInfo
if baseInfo, ok := couponMap[order.CouponID]; ok {
cInfo = &CouponSimpleInfo{
UserCouponID: baseInfo.UserCouponID,
Name: baseInfo.Name,
Type: baseInfo.Type,
Value: baseInfo.Value,
}
// 尝试使用实际抵扣金额
if amMap, ok := appliedAmountMap[order.ID]; ok {
if amount, ok2 := amMap[order.CouponID]; ok2 {
cInfo.Value = amount
}
}
}
items[i] = &OrderWithItems{
Orders: order,
Items: itemsMap[order.ID],
CouponInfo: cInfo,
ItemCardInfo: itemCardMap[order.ItemCardID],
}
if logs, ok := drawLogsListMap[order.ID]; ok && len(logs) > 0 {
items[i].IsDraw = true
// 取第一条记录的基本信息
log := logs[0]
items[i].IsWinner = log.IsWinner == 1
items[i].RewardLevel = log.Level
if issue, ok := issueMap[log.IssueID]; ok {
items[i].IssueNumber = issue.IssueNumber
if act, ok := activityMap[issue.ActivityID]; ok {
items[i].ActivityName = act.Name
items[i].ActivityID = act.ID
items[i].PlayType = act.PlayType
items[i].CategoryID = act.ActivityCategoryID
if cat, ok := categoryMap[act.ActivityCategoryID]; ok {
items[i].CategoryName = cat.Name
}
}
}
// 填充抽奖凭据
for idx, lg := range logs {
if r, ok := receiptsMap[lg.ID]; ok {
items[i].DrawReceipts = append(items[i].DrawReceipts, &DrawReceiptInfo{
DrawLogID: r.DrawLogID,
RewardID: lg.RewardID,
DrawIndex: idx + 1,
AlgoVersion: r.AlgoVersion,
RoundID: r.RoundID,
DrawID: r.DrawID,
ClientID: r.ClientID,
Timestamp: r.Timestamp,
ServerSeedHash: r.ServerSeedHash,
ServerSubSeed: "",
ClientSeed: r.ClientSeed,
Nonce: r.Nonce,
ItemsRoot: r.ItemsRoot,
WeightsTotal: r.WeightsTotal,
SelectedIndex: r.SelectedIndex,
RandProof: r.RandProof,
Signature: r.Signature,
ItemsSnapshot: r.ItemsSnapshot,
})
}
}
} else if order.SourceType == 2 {
// 抽奖订单但没有开奖记录(如对对碰支付后还没玩),从备注解析活动信息
actID := parseActivityIDFromRemark(order.Remark)
if actID > 0 {
if act, ok := activityMap[actID]; ok {
items[i].ActivityName = act.Name
items[i].ActivityID = act.ID
items[i].PlayType = act.PlayType
items[i].CategoryID = act.ActivityCategoryID
if cat, ok := categoryMap[act.ActivityCategoryID]; ok {
items[i].CategoryName = cat.Name
}
}
}
}
// Special handling for Ichiban Kuji: Do not show Item Card
if items[i].PlayType == "ichiban" {
items[i].ItemCardInfo = nil
}
}
return items, total, nil
}
// parseActivityIDFromRemark 从订单备注解析活动ID
func parseActivityIDFromRemark(remark string) int64 {
if remark == "" {
return 0
}
parts := strings.Split(remark, "|")
for _, p := range parts {
if strings.HasPrefix(p, "lottery:activity:") {
idStr := p[len("lottery:activity:"):]
var n int64
for i := 0; i < len(idStr); i++ {
c := idStr[i]
if c < '0' || c > '9' {
break
}
n = n*10 + int64(c-'0')
}
return n
}
}
return 0
}