bindbox-game/internal/api/admin/livestream_admin.go

672 lines
22 KiB
Go

package admin
import (
"net/http"
"strconv"
"time"
"bindbox-game/internal/code"
"bindbox-game/internal/pkg/core"
"bindbox-game/internal/pkg/validation"
"bindbox-game/internal/repository/mysql/model"
"bindbox-game/internal/service/livestream"
)
// ========== 直播间活动管理 ==========
type createLivestreamActivityRequest struct {
Name string `json:"name" binding:"required"`
StreamerName string `json:"streamer_name"`
StreamerContact string `json:"streamer_contact"`
DouyinProductID string `json:"douyin_product_id"`
TicketPrice int64 `json:"ticket_price"`
StartTime string `json:"start_time"`
EndTime string `json:"end_time"`
}
type livestreamActivityResponse struct {
ID int64 `json:"id"`
Name string `json:"name"`
StreamerName string `json:"streamer_name"`
StreamerContact string `json:"streamer_contact"`
AccessCode string `json:"access_code"`
DouyinProductID string `json:"douyin_product_id"`
TicketPrice int64 `json:"ticket_price"`
Status int32 `json:"status"`
StartTime string `json:"start_time,omitempty"`
EndTime string `json:"end_time,omitempty"`
CreatedAt string `json:"created_at"`
}
// CreateLivestreamActivity 创建直播间活动
// @Summary 创建直播间活动
// @Description 创建新的直播间活动,自动生成唯一访问码
// @Tags 管理端.直播间
// @Accept json
// @Produce json
// @Param RequestBody body createLivestreamActivityRequest true "请求参数"
// @Success 200 {object} livestreamActivityResponse
// @Failure 400 {object} code.Failure
// @Router /api/admin/livestream/activities [post]
// @Security LoginVerifyToken
func (h *handler) CreateLivestreamActivity() core.HandlerFunc {
return func(ctx core.Context) {
req := new(createLivestreamActivityRequest)
if err := ctx.ShouldBindJSON(req); err != nil {
ctx.AbortWithError(core.Error(http.StatusBadRequest, code.ParamBindError, validation.Error(err)))
return
}
input := livestream.CreateActivityInput{
Name: req.Name,
StreamerName: req.StreamerName,
StreamerContact: req.StreamerContact,
DouyinProductID: req.DouyinProductID,
TicketPrice: req.TicketPrice,
}
if req.StartTime != "" {
if t, err := time.ParseInLocation("2006-01-02 15:04:05", req.StartTime, time.Local); err == nil {
input.StartTime = &t
}
}
if req.EndTime != "" {
if t, err := time.ParseInLocation("2006-01-02 15:04:05", req.EndTime, time.Local); err == nil {
input.EndTime = &t
}
}
activity, err := h.livestream.CreateActivity(ctx.RequestContext(), input)
if err != nil {
ctx.AbortWithError(core.Error(http.StatusInternalServerError, code.ServerError, err.Error()))
return
}
ctx.Payload(&livestreamActivityResponse{
ID: activity.ID,
Name: activity.Name,
StreamerName: activity.StreamerName,
StreamerContact: activity.StreamerContact,
AccessCode: activity.AccessCode,
DouyinProductID: activity.DouyinProductID,
TicketPrice: activity.TicketPrice,
Status: activity.Status,
CreatedAt: activity.CreatedAt.Format("2006-01-02 15:04:05"),
})
}
}
type updateLivestreamActivityRequest struct {
Name string `json:"name"`
StreamerName string `json:"streamer_name"`
StreamerContact string `json:"streamer_contact"`
DouyinProductID string `json:"douyin_product_id"`
TicketPrice *int64 `json:"ticket_price"`
Status *int32 `json:"status"`
StartTime string `json:"start_time"`
EndTime string `json:"end_time"`
}
// UpdateLivestreamActivity 更新直播间活动
// @Summary 更新直播间活动
// @Description 更新直播间活动信息
// @Tags 管理端.直播间
// @Accept json
// @Produce json
// @Param id path integer true "活动ID"
// @Param RequestBody body updateLivestreamActivityRequest true "请求参数"
// @Success 200 {object} simpleMessageResponse
// @Failure 400 {object} code.Failure
// @Router /api/admin/livestream/activities/{id} [put]
// @Security LoginVerifyToken
func (h *handler) UpdateLivestreamActivity() core.HandlerFunc {
return func(ctx core.Context) {
id, err := strconv.ParseInt(ctx.Param("id"), 10, 64)
if err != nil || id <= 0 {
ctx.AbortWithError(core.Error(http.StatusBadRequest, code.ParamBindError, "无效的活动ID"))
return
}
req := new(updateLivestreamActivityRequest)
if err := ctx.ShouldBindJSON(req); err != nil {
ctx.AbortWithError(core.Error(http.StatusBadRequest, code.ParamBindError, validation.Error(err)))
return
}
input := livestream.UpdateActivityInput{
Name: req.Name,
StreamerName: req.StreamerName,
StreamerContact: req.StreamerContact,
DouyinProductID: req.DouyinProductID,
TicketPrice: req.TicketPrice,
Status: req.Status,
}
if req.StartTime != "" {
if t, err := time.ParseInLocation("2006-01-02 15:04:05", req.StartTime, time.Local); err == nil {
input.StartTime = &t
}
}
if req.EndTime != "" {
if t, err := time.ParseInLocation("2006-01-02 15:04:05", req.EndTime, time.Local); err == nil {
input.EndTime = &t
}
}
if err := h.livestream.UpdateActivity(ctx.RequestContext(), id, input); err != nil {
ctx.AbortWithError(core.Error(http.StatusInternalServerError, code.ServerError, err.Error()))
return
}
ctx.Payload(&simpleMessageResponse{Message: "操作成功"})
}
}
type listLivestreamActivitiesRequest struct {
Page int `form:"page"`
PageSize int `form:"page_size"`
Status *int32 `form:"status"`
}
type listLivestreamActivitiesResponse struct {
List []livestreamActivityResponse `json:"list"`
Total int64 `json:"total"`
Page int `json:"page"`
PageSize int `json:"page_size"`
}
// ListLivestreamActivities 直播间活动列表
// @Summary 直播间活动列表
// @Description 获取直播间活动列表
// @Tags 管理端.直播间
// @Accept json
// @Produce json
// @Param page query int false "页码" default(1)
// @Param page_size query int false "每页数量" default(20)
// @Param status query int false "状态过滤"
// @Success 200 {object} listLivestreamActivitiesResponse
// @Failure 400 {object} code.Failure
// @Router /api/admin/livestream/activities [get]
// @Security LoginVerifyToken
func (h *handler) ListLivestreamActivities() core.HandlerFunc {
return func(ctx core.Context) {
req := new(listLivestreamActivitiesRequest)
if err := ctx.ShouldBindForm(req); err != nil {
ctx.AbortWithError(core.Error(http.StatusBadRequest, code.ParamBindError, validation.Error(err)))
return
}
if req.Page <= 0 {
req.Page = 1
}
if req.PageSize <= 0 {
req.PageSize = 20
}
list, total, err := h.livestream.ListActivities(ctx.RequestContext(), req.Page, req.PageSize, req.Status)
if err != nil {
ctx.AbortWithError(core.Error(http.StatusInternalServerError, code.ServerError, err.Error()))
return
}
res := &listLivestreamActivitiesResponse{
List: make([]livestreamActivityResponse, len(list)),
Total: total,
Page: req.Page,
PageSize: req.PageSize,
}
for i, a := range list {
item := livestreamActivityResponse{
ID: a.ID,
Name: a.Name,
StreamerName: a.StreamerName,
StreamerContact: a.StreamerContact,
AccessCode: a.AccessCode,
DouyinProductID: a.DouyinProductID,
TicketPrice: a.TicketPrice,
Status: a.Status,
CreatedAt: a.CreatedAt.Format("2006-01-02 15:04:05"),
}
if !a.StartTime.IsZero() {
item.StartTime = a.StartTime.Format("2006-01-02 15:04:05")
}
if !a.EndTime.IsZero() {
item.EndTime = a.EndTime.Format("2006-01-02 15:04:05")
}
res.List[i] = item
}
ctx.Payload(res)
}
}
// GetLivestreamActivity 获取直播间活动详情
// @Summary 获取直播间活动详情
// @Description 根据ID获取直播间活动详情
// @Tags 管理端.直播间
// @Accept json
// @Produce json
// @Param id path integer true "活动ID"
// @Success 200 {object} livestreamActivityResponse
// @Failure 400 {object} code.Failure
// @Router /api/admin/livestream/activities/{id} [get]
// @Security LoginVerifyToken
func (h *handler) GetLivestreamActivity() core.HandlerFunc {
return func(ctx core.Context) {
id, err := strconv.ParseInt(ctx.Param("id"), 10, 64)
if err != nil || id <= 0 {
ctx.AbortWithError(core.Error(http.StatusBadRequest, code.ParamBindError, "无效的活动ID"))
return
}
activity, err := h.livestream.GetActivity(ctx.RequestContext(), id)
if err != nil {
ctx.AbortWithError(core.Error(http.StatusNotFound, code.ServerError, "活动不存在"))
return
}
res := &livestreamActivityResponse{
ID: activity.ID,
Name: activity.Name,
StreamerName: activity.StreamerName,
StreamerContact: activity.StreamerContact,
AccessCode: activity.AccessCode,
DouyinProductID: activity.DouyinProductID,
TicketPrice: activity.TicketPrice,
Status: activity.Status,
CreatedAt: activity.CreatedAt.Format("2006-01-02 15:04:05"),
}
if !activity.StartTime.IsZero() {
res.StartTime = activity.StartTime.Format("2006-01-02 15:04:05")
}
if !activity.EndTime.IsZero() {
res.EndTime = activity.EndTime.Format("2006-01-02 15:04:05")
}
ctx.Payload(res)
}
}
// DeleteLivestreamActivity 删除直播间活动
// @Summary 删除直播间活动
// @Description 删除指定直播间活动
// @Tags 管理端.直播间
// @Accept json
// @Produce json
// @Param id path integer true "活动ID"
// @Success 200 {object} simpleMessageResponse
// @Failure 400 {object} code.Failure
// @Router /api/admin/livestream/activities/{id} [delete]
// @Security LoginVerifyToken
func (h *handler) DeleteLivestreamActivity() core.HandlerFunc {
return func(ctx core.Context) {
id, err := strconv.ParseInt(ctx.Param("id"), 10, 64)
if err != nil || id <= 0 {
ctx.AbortWithError(core.Error(http.StatusBadRequest, code.ParamBindError, "无效的活动ID"))
return
}
if err := h.livestream.DeleteActivity(ctx.RequestContext(), id); err != nil {
ctx.AbortWithError(core.Error(http.StatusInternalServerError, code.ServerError, err.Error()))
return
}
ctx.Payload(&simpleMessageResponse{Message: "删除成功"})
}
}
// ========== 直播间奖品管理 ==========
type createLivestreamPrizeRequest struct {
Name string `json:"name"`
Image string `json:"image"`
Level int32 `json:"level"`
Weight int32 `json:"weight"`
Quantity int32 `json:"quantity"`
ProductID int64 `json:"product_id"`
CostPrice int64 `json:"cost_price"`
}
type livestreamPrizeResponse struct {
ID int64 `json:"id"`
Name string `json:"name"`
Image string `json:"image"`
Level int32 `json:"level"`
Weight int32 `json:"weight"`
Quantity int32 `json:"quantity"`
Remaining int64 `json:"remaining"`
ProductID int64 `json:"product_id"`
CostPrice int64 `json:"cost_price"`
Sort int32 `json:"sort"`
}
// CreateLivestreamPrizes 批量创建奖品
// @Summary 批量创建直播间奖品
// @Description 为指定活动批量创建奖品
// @Tags 管理端.直播间
// @Accept json
// @Produce json
// @Param activity_id path integer true "活动ID"
// @Param RequestBody body []createLivestreamPrizeRequest true "奖品列表"
// @Success 200 {object} simpleMessageResponse
// @Failure 400 {object} code.Failure
// @Router /api/admin/livestream/activities/{activity_id}/prizes [post]
// @Security LoginVerifyToken
func (h *handler) CreateLivestreamPrizes() core.HandlerFunc {
return func(ctx core.Context) {
activityID, err := strconv.ParseInt(ctx.Param("id"), 10, 64)
if err != nil || activityID <= 0 {
ctx.AbortWithError(core.Error(http.StatusBadRequest, code.ParamBindError, "无效的活动ID"))
return
}
var req []createLivestreamPrizeRequest
if err := ctx.ShouldBindJSON(&req); err != nil {
ctx.AbortWithError(core.Error(http.StatusBadRequest, code.ParamBindError, validation.Error(err)))
return
}
var inputs []livestream.CreatePrizeInput
for _, p := range req {
inputs = append(inputs, livestream.CreatePrizeInput{
Name: p.Name,
Image: p.Image,
Weight: p.Weight,
Quantity: p.Quantity,
Level: p.Level,
ProductID: p.ProductID,
CostPrice: p.CostPrice,
})
}
if err := h.livestream.CreatePrizes(ctx.RequestContext(), activityID, inputs); err != nil {
ctx.AbortWithError(core.Error(http.StatusInternalServerError, code.ServerError, err.Error()))
return
}
ctx.Payload(&simpleMessageResponse{Message: "创建成功"})
}
}
// ListLivestreamPrizes 获取活动奖品列表
// @Summary 获取直播间活动奖品列表
// @Description 获取指定活动的所有奖品
// @Tags 管理端.直播间
// @Accept json
// @Produce json
// @Param activity_id path integer true "活动ID"
// @Success 200 {object} []livestreamPrizeResponse
// @Failure 400 {object} code.Failure
// @Router /api/admin/livestream/activities/{activity_id}/prizes [get]
// @Security LoginVerifyToken
func (h *handler) ListLivestreamPrizes() core.HandlerFunc {
return func(ctx core.Context) {
activityID, err := strconv.ParseInt(ctx.Param("id"), 10, 64)
if err != nil || activityID <= 0 {
ctx.AbortWithError(core.Error(http.StatusBadRequest, code.ParamBindError, "无效的活动ID"))
return
}
prizes, err := h.livestream.ListPrizes(ctx.RequestContext(), activityID)
if err != nil {
ctx.AbortWithError(core.Error(http.StatusInternalServerError, code.ServerError, err.Error()))
return
}
res := make([]livestreamPrizeResponse, len(prizes))
for i, p := range prizes {
res[i] = livestreamPrizeResponse{
ID: p.ID,
Name: p.Name,
Image: p.Image,
Weight: p.Weight,
Quantity: p.Quantity,
Remaining: int64(p.Remaining),
Level: p.Level,
ProductID: p.ProductID,
CostPrice: p.CostPrice,
Sort: p.Sort,
}
}
ctx.Payload(res)
}
}
// DeleteLivestreamPrize 删除奖品
// @Summary 删除直播间奖品
// @Description 删除指定奖品
// @Tags 管理端.直播间
// @Accept json
// @Produce json
// @Param prize_id path integer true "奖品ID"
// @Success 200 {object} simpleMessageResponse
// @Failure 400 {object} code.Failure
// @Router /api/admin/livestream/prizes/{prize_id} [delete]
// @Security LoginVerifyToken
func (h *handler) DeleteLivestreamPrize() core.HandlerFunc {
return func(ctx core.Context) {
prizeID, err := strconv.ParseInt(ctx.Param("id"), 10, 64)
if err != nil || prizeID <= 0 {
ctx.AbortWithError(core.Error(http.StatusBadRequest, code.ParamBindError, "无效的奖品ID"))
return
}
if err := h.livestream.DeletePrize(ctx.RequestContext(), prizeID); err != nil {
ctx.AbortWithError(core.Error(http.StatusInternalServerError, code.ServerError, err.Error()))
return
}
ctx.Payload(&simpleMessageResponse{Message: "删除成功"})
}
}
// ========== 直播间中奖记录 ==========
type livestreamDrawLogResponse struct {
ID int64 `json:"id"`
ActivityID int64 `json:"activity_id"`
PrizeID int64 `json:"prize_id"`
PrizeName string `json:"prize_name"`
Level int32 `json:"level"`
DouyinOrderID int64 `json:"douyin_order_id"` // 关联ID
ShopOrderID string `json:"shop_order_id"` // 店铺订单号
LocalUserID int64 `json:"local_user_id"`
DouyinUserID string `json:"douyin_user_id"`
UserNickname string `json:"user_nickname"` // 用户昵称
SeedHash string `json:"seed_hash"`
CreatedAt string `json:"created_at"`
}
type listLivestreamDrawLogsResponse struct {
List []livestreamDrawLogResponse `json:"list"`
Total int64 `json:"total"`
Page int `json:"page"`
PageSize int `json:"page_size"`
}
type listLivestreamDrawLogsRequest struct {
Page int `form:"page"`
PageSize int `form:"page_size"`
StartTime string `form:"start_time"`
EndTime string `form:"end_time"`
Keyword string `form:"keyword"`
}
// ListLivestreamDrawLogs 获取中奖记录
// @Summary 获取直播间中奖记录
// @Description 获取指定活动的中奖记录,支持时间范围和关键词筛选
// @Tags 管理端.直播间
// @Accept json
// @Produce json
// @Param activity_id path integer true "活动ID"
// @Param page query int false "页码" default(1)
// @Param page_size query int false "每页数量" default(20)
// @Param start_time query string false "开始时间 (YYYY-MM-DD)"
// @Param end_time query string false "结束时间 (YYYY-MM-DD)"
// @Param keyword query string false "搜索关键词 (昵称/订单号/奖品名称)"
// @Success 200 {object} listLivestreamDrawLogsResponse
// @Failure 400 {object} code.Failure
// @Router /api/admin/livestream/activities/{activity_id}/draw_logs [get]
// @Security LoginVerifyToken
func (h *handler) ListLivestreamDrawLogs() core.HandlerFunc {
return func(ctx core.Context) {
activityID, err := strconv.ParseInt(ctx.Param("id"), 10, 64)
if err != nil || activityID <= 0 {
ctx.AbortWithError(core.Error(http.StatusBadRequest, code.ParamBindError, "无效的活动ID"))
return
}
req := new(listLivestreamDrawLogsRequest)
_ = ctx.ShouldBindForm(req)
page := req.Page
pageSize := req.PageSize
if page <= 0 {
page = 1
}
if pageSize <= 0 {
pageSize = 20
}
// 解析时间范围
var startTime, endTime *time.Time
if req.StartTime != "" {
if t, err := time.ParseInLocation("2006-01-02", req.StartTime, time.Local); err == nil {
startTime = &t
}
}
if req.EndTime != "" {
if t, err := time.ParseInLocation("2006-01-02", req.EndTime, time.Local); err == nil {
// 结束时间设为当天结束
end := t.Add(24*time.Hour - time.Nanosecond)
endTime = &end
}
}
// 使用底层 GORM 直接查询以支持 keyword
db := h.repo.GetDbR().Model(&model.LivestreamDrawLogs{}).Where("activity_id = ?", activityID)
if startTime != nil {
db = db.Where("created_at >= ?", startTime)
}
if endTime != nil {
db = db.Where("created_at <= ?", endTime)
}
if req.Keyword != "" {
keyword := "%" + req.Keyword + "%"
db = db.Where("(user_nickname LIKE ? OR shop_order_id LIKE ? OR prize_name LIKE ?)", keyword, keyword, keyword)
}
var total int64
db.Count(&total)
var logs []model.LivestreamDrawLogs
if err := db.Order("id DESC").Offset((page - 1) * pageSize).Limit(pageSize).Find(&logs).Error; err != nil {
ctx.AbortWithError(core.Error(http.StatusInternalServerError, code.ServerError, err.Error()))
return
}
res := &listLivestreamDrawLogsResponse{
List: make([]livestreamDrawLogResponse, len(logs)),
Total: total,
Page: page,
PageSize: pageSize,
}
for i, log := range logs {
res.List[i] = livestreamDrawLogResponse{
ID: log.ID,
ActivityID: log.ActivityID,
PrizeID: log.PrizeID,
PrizeName: log.PrizeName,
Level: log.Level,
DouyinOrderID: log.DouyinOrderID,
ShopOrderID: log.ShopOrderID,
LocalUserID: log.LocalUserID,
DouyinUserID: log.DouyinUserID,
UserNickname: log.UserNickname,
SeedHash: log.SeedHash,
CreatedAt: log.CreatedAt.Format("2006-01-02 15:04:05"),
}
}
ctx.Payload(res)
}
}
// ========== 直播间承诺管理 ==========
type livestreamCommitmentSummaryResponse struct {
SeedVersion int32 `json:"seed_version"`
Algo string `json:"algo"`
HasSeed bool `json:"has_seed"`
LenSeed int `json:"len_seed_master"`
LenHash int `json:"len_seed_hash"`
}
// GenerateLivestreamCommitment 生成直播间活动承诺
// @Summary 生成直播间活动承诺
// @Description 为直播间活动生成可验证的承诺种子
// @Tags 管理端.直播间
// @Accept json
// @Produce json
// @Param id path integer true "活动ID"
// @Success 200 {object} map[string]int32
// @Failure 400 {object} code.Failure
// @Router /api/admin/livestream/activities/{id}/commitment/generate [post]
// @Security LoginVerifyToken
func (h *handler) GenerateLivestreamCommitment() core.HandlerFunc {
return func(ctx core.Context) {
id, err := strconv.ParseInt(ctx.Param("id"), 10, 64)
if err != nil || id <= 0 {
ctx.AbortWithError(core.Error(http.StatusBadRequest, code.ParamBindError, "无效的活动ID"))
return
}
version, err := h.livestream.GenerateCommitment(ctx.RequestContext(), id)
if err != nil {
ctx.AbortWithError(core.Error(http.StatusInternalServerError, code.ServerError, err.Error()))
return
}
ctx.Payload(map[string]int32{"seed_version": version})
}
}
// GetLivestreamCommitmentSummary 获取直播间活动承诺摘要
// @Summary 获取直播间活动承诺摘要
// @Description 获取直播间活动的承诺状态信息
// @Tags 管理端.直播间
// @Accept json
// @Produce json
// @Param id path integer true "活动ID"
// @Success 200 {object} livestreamCommitmentSummaryResponse
// @Failure 400 {object} code.Failure
// @Router /api/admin/livestream/activities/{id}/commitment/summary [get]
// @Security LoginVerifyToken
func (h *handler) GetLivestreamCommitmentSummary() core.HandlerFunc {
return func(ctx core.Context) {
id, err := strconv.ParseInt(ctx.Param("id"), 10, 64)
if err != nil || id <= 0 {
ctx.AbortWithError(core.Error(http.StatusBadRequest, code.ParamBindError, "无效的活动ID"))
return
}
summary, err := h.livestream.GetCommitmentSummary(ctx.RequestContext(), id)
if err != nil {
ctx.AbortWithError(core.Error(http.StatusInternalServerError, code.ServerError, err.Error()))
return
}
ctx.Payload(&livestreamCommitmentSummaryResponse{
SeedVersion: summary.SeedVersion,
Algo: summary.Algo,
HasSeed: summary.HasSeed,
LenSeed: summary.LenSeed,
LenHash: summary.LenHash,
})
}
}