diff --git a/api/v1/radio/enter.go b/api/v1/radio/enter.go index da05de7..9f09646 100644 --- a/api/v1/radio/enter.go +++ b/api/v1/radio/enter.go @@ -11,6 +11,7 @@ type ApiGroup struct { PayApi VipApi AnalyticsApi + UserApi } var ApiGroupApp = new(ApiGroup) @@ -24,4 +25,5 @@ var ( payService = service.GroupApp.RadioServiceGroup.PayService vipService = service.GroupApp.RadioServiceGroup.VipService analyticsService = service.GroupApp.RadioServiceGroup.AnalyticsService + userService = service.GroupApp.RadioServiceGroup.UserService ) diff --git a/api/v1/radio/user.go b/api/v1/radio/user.go new file mode 100644 index 0000000..64f86d6 --- /dev/null +++ b/api/v1/radio/user.go @@ -0,0 +1,48 @@ +package radio + +import ( + "strconv" + "sundynix-go/global" + common "sundynix-go/model/commom/request" + "sundynix-go/model/commom/response" + + "github.com/gin-gonic/gin" + "go.uber.org/zap" +) + +type UserApi struct{} + +// GetRadioUserList 获取电台用户列表 +// @Tags 用户管理 +// @Summary 获取电台用户列表(含订阅/收听统计) +// @Accept application/json +// @Produce application/json +// @Param current query int false "页码" +// @Param pageSize query int false "每页大小" +// @Param keyword query string false "搜索关键字" +// @Param isVip query int false "VIP筛选: 0全部 1VIP 2非VIP" +// @Success 200 {object} response.Response +// @Router /radio/user/list [get] +func (a *UserApi) GetRadioUserList(c *gin.Context) { + var info common.PageInfo + info.Keyword = c.Query("keyword") + current, _ := strconv.Atoi(c.DefaultQuery("current", "1")) + pageSize, _ := strconv.Atoi(c.DefaultQuery("pageSize", "10")) + info.Current = current + info.PageSize = pageSize + + isVip, _ := strconv.Atoi(c.DefaultQuery("isVip", "0")) + + list, total, err := userService.GetRadioUserList(info, isVip, info.Keyword) + if err != nil { + global.Logger.Error("获取用户列表失败!", zap.Error(err)) + response.FailWithMsg(err.Error(), c) + return + } + response.OkWithData(response.PageResult{ + List: list, + Total: total, + Page: info.Current, + PageSize: info.PageSize, + }, c) +} diff --git a/config-dev.yaml b/config-dev.yaml index 38dcfc7..7962716 100644 --- a/config-dev.yaml +++ b/config-dev.yaml @@ -22,12 +22,11 @@ mini-program: app-id: wx52dfc635739a9c19 app-secret: 84c6ddab1f24d0222da57bedb681c81f -# 腾讯文字转语音 -tencent-tts: - app-id: 1312892187 - secret-id: AKIDKaeU7XjhSzIOGuKWUEk26wY1MUP6asyr - secret-key: lU0JOFrGSSGqDMLKBoIbnmX6TcXIqKbe - +# 统一音频TTS服务(火山引擎) +tts: + app-id: "9604175735" + resource-id: "seed-tts-2.0" # 火山引擎TTS服务资源ID (原cluster) + access-key: "IMSrxDQgXWOwaJnuF-5G7uppRQutwBny" # 接口调用的Token # 微信支付 wechat-pay: diff --git a/initialize/router.go b/initialize/router.go index 3b9a105..0313346 100644 --- a/initialize/router.go +++ b/initialize/router.go @@ -56,6 +56,7 @@ func Routers() { radioRouter.InitPayRouter(NeedAuthGroup, PublicGroup) //支付和回调 radioRouter.InitInteractionRouter(NeedAuthGroup) //用户互动相关 radioRouter.InitAnalyticsRouter(NeedAuthGroup) //数据分析相关 + radioRouter.InitUserRouter(NeedAuthGroup) //用户管理相关 } address := fmt.Sprintf(":%d", global.Config.System.Addr) diff --git a/model/radio/response/user.go b/model/radio/response/user.go new file mode 100644 index 0000000..ef82a2f --- /dev/null +++ b/model/radio/response/user.go @@ -0,0 +1,24 @@ +package response + +import "time" + +// RadioUserItem 电台用户列表项 +type RadioUserItem struct { + Id string `json:"id"` + Name string `json:"name"` + NickName string `json:"nickName"` + Account string `json:"account"` + Phone string `json:"phone"` + AvatarUrl string `json:"avatarUrl"` + Gender int `json:"gender"` // 0:未知 1:男 2:女 + IsVip int `json:"isVip"` // 0:否 1:是 + VipExpireAt *time.Time `json:"vipExpireAt"` // VIP过期时间 + LastLoginAt *time.Time `json:"lastLoginAt"` // 最后登录时间 + LastLoginIp string `json:"lastLoginIp"` // 最后登录IP + CreatedAt time.Time `json:"createdAt"` // 注册时间 + SubscribeCount int64 `json:"subscribeCount"` // 订阅频道数 + ListenCount int64 `json:"listenCount"` // 收听次数 + FavoriteCount int64 `json:"favoriteCount"` // 收藏数 + TotalSpent int64 `json:"totalSpent"` // 累计消费(分) + OrderCount int64 `json:"orderCount"` // 订单数 +} diff --git a/model/system/request/sys_user.go b/model/system/request/sys_user.go index efb5e85..cb6486b 100644 --- a/model/system/request/sys_user.go +++ b/model/system/request/sys_user.go @@ -13,6 +13,7 @@ type GetUserList struct { common.PageInfo Account string `json:"account" form:"account"` Phone string `json:"phone" form:"phone"` + IsVip *int `json:"isVip" form:"isVip"` } type ChangePwd struct { diff --git a/model/system/sys_user.go b/model/system/sys_user.go index 8b9c900..5ff8e00 100644 --- a/model/system/sys_user.go +++ b/model/system/sys_user.go @@ -14,10 +14,10 @@ type User struct { global.BaseModel TenantId string `gorm:"size:20;" json:"tenantId" form:"tenantId"` ClientId string `gorm:"size:20;" json:"clientId"` - Name string `gorm:"size:20" json:"name" form:"name"` + Name string `gorm:"size:100" json:"name" form:"name"` Account string `gorm:"size:11;" json:"account" form:"account"` Password string `gorm:"size:100;" json:"-" form:"password"` - NickName string `gorm:"size:20;column:nick_name" json:"nickName" form:"nickName"` + NickName string `gorm:"size:100;column:nick_name" json:"nickName" form:"nickName"` Phone string `gorm:"size:20;column:phone" json:"phone" form:"phone"` SessionKey string `gorm:"size:80;column:session_key" json:"sessionKey" form:"sessionKey"` UnionId string `gorm:"size:80;column:union_id" json:"unionId"` diff --git a/router/radio/enter.go b/router/radio/enter.go index e267cfd..25b7351 100644 --- a/router/radio/enter.go +++ b/router/radio/enter.go @@ -11,6 +11,7 @@ type RadioRouterGroup struct { PayRouter VipRouter AnalyticsRouter + UserRouter } var GroupApp = new(RadioRouterGroup) @@ -24,4 +25,5 @@ var ( payApi = v1.ApiGroupApp.RadioApiGroup.PayApi vipApi = v1.ApiGroupApp.RadioApiGroup.VipApi analyticsApi = v1.ApiGroupApp.RadioApiGroup.AnalyticsApi + userApi = v1.ApiGroupApp.RadioApiGroup.UserApi ) diff --git a/router/radio/user_router.go b/router/radio/user_router.go new file mode 100644 index 0000000..4f0ad7a --- /dev/null +++ b/router/radio/user_router.go @@ -0,0 +1,14 @@ +package radio + +import ( + "github.com/gin-gonic/gin" +) + +type UserRouter struct{} + +func (r *UserRouter) InitUserRouter(Router *gin.RouterGroup) { + userRouter := Router.Group("/radio/user") + { + userRouter.GET("list", userApi.GetRadioUserList) + } +} diff --git a/service/radio/enter.go b/service/radio/enter.go index f7d5689..9930223 100644 --- a/service/radio/enter.go +++ b/service/radio/enter.go @@ -11,6 +11,7 @@ type ServiceGroup struct { VipService TTSService AnalyticsService + UserService } var GroupApp = new(ServiceGroup) diff --git a/service/radio/user_service.go b/service/radio/user_service.go new file mode 100644 index 0000000..253bb84 --- /dev/null +++ b/service/radio/user_service.go @@ -0,0 +1,143 @@ +package radio + +import ( + "sundynix-go/global" + common "sundynix-go/model/commom/request" + "sundynix-go/model/radio/response" + "sundynix-go/model/system" +) + +type UserService struct{} + +// GetRadioUserList 获取电台用户列表(带订阅/收听统计) +func (s *UserService) GetRadioUserList(info common.PageInfo, isVip int, keyword string) ([]response.RadioUserItem, int64, error) { + var total int64 + var users []system.User + + db := global.DB.Model(&system.User{}) + + // 关键字搜索 + if keyword != "" { + db = db.Where("nick_name LIKE ? OR phone LIKE ? OR account LIKE ?", + "%"+keyword+"%", "%"+keyword+"%", "%"+keyword+"%") + } + + // VIP 筛选: 1=VIP, 2=非VIP, 0或其他=全部 + if isVip == 1 { + db = db.Where("is_vip = 1") + } else if isVip == 2 { + db = db.Where("is_vip = 0") + } + + // 统计总数 + if err := db.Count(&total).Error; err != nil { + return nil, 0, err + } + + // 分页查询用户列表 + if err := db.Preload("Avatar"). + Scopes(info.Paginate()). + Order("created_at DESC"). + Find(&users).Error; err != nil { + return nil, 0, err + } + + // 批量获取用户ID + userIds := make([]string, len(users)) + for i, u := range users { + userIds[i] = u.Id + } + + if len(userIds) == 0 { + return []response.RadioUserItem{}, total, nil + } + + // 批量查询订阅数 + type UserCount struct { + UserId string + Count int64 + } + var subCounts []UserCount + global.DB.Table("sundynix_radio_subscription"). + Select("user_id, COUNT(*) as count"). + Where("user_id IN ? AND status = 1 AND deleted_at IS NULL", userIds). + Group("user_id"). + Scan(&subCounts) + subMap := make(map[string]int64) + for _, sc := range subCounts { + subMap[sc.UserId] = sc.Count + } + + // 批量查询收听次数 + var listenCounts []UserCount + global.DB.Table("sundynix_radio_listen_log"). + Select("user_id, COUNT(*) as count"). + Where("user_id IN ? AND deleted_at IS NULL", userIds). + Group("user_id"). + Scan(&listenCounts) + listenMap := make(map[string]int64) + for _, lc := range listenCounts { + listenMap[lc.UserId] = lc.Count + } + + // 批量查询收藏数 + var favCounts []UserCount + global.DB.Table("sundynix_radio_favorite"). + Select("user_id, COUNT(*) as count"). + Where("user_id IN ? AND deleted_at IS NULL", userIds). + Group("user_id"). + Scan(&favCounts) + favMap := make(map[string]int64) + for _, fc := range favCounts { + favMap[fc.UserId] = fc.Count + } + + // 批量查询订单总额(分) + type UserAmount struct { + UserId string + TotalAmount int64 + OrderCount int64 + } + var orderStats []UserAmount + global.DB.Table("sundynix_order"). + Select("user_id, SUM(amount) as total_amount, COUNT(*) as order_count"). + Where("user_id IN ? AND status = 1 AND deleted_at IS NULL", userIds). + Group("user_id"). + Scan(&orderStats) + amountMap := make(map[string]int64) + orderCountMap := make(map[string]int64) + for _, os := range orderStats { + amountMap[os.UserId] = os.TotalAmount + orderCountMap[os.UserId] = os.OrderCount + } + + // 组装结果 + result := make([]response.RadioUserItem, len(users)) + for i, u := range users { + avatarUrl := "" + if u.Avatar != nil { + avatarUrl = u.Avatar.Url + } + result[i] = response.RadioUserItem{ + Id: u.Id, + Name: u.Name, + NickName: u.NickName, + Account: u.Account, + Phone: u.Phone, + AvatarUrl: avatarUrl, + Gender: u.Gender, + IsVip: u.IsVip, + VipExpireAt: u.VipExpireAt, + LastLoginAt: u.LastLoginAt, + LastLoginIp: u.LastLoginIp, + CreatedAt: u.CreatedAt, + SubscribeCount: subMap[u.Id], + ListenCount: listenMap[u.Id], + FavoriteCount: favMap[u.Id], + TotalSpent: amountMap[u.Id], + OrderCount: orderCountMap[u.Id], + } + } + + return result, total, nil +} diff --git a/service/system/sys_user.go b/service/system/sys_user.go index e8b0162..8edf7b3 100644 --- a/service/system/sys_user.go +++ b/service/system/sys_user.go @@ -20,6 +20,7 @@ import ( location "sundynix-go/utils/location" "sundynix-go/utils/uniqueid" "sundynix-go/utils/wechat" + "time" "go.uber.org/zap" "gorm.io/gorm" @@ -72,6 +73,9 @@ func (userService *UserService) GetUserList(info systemReq.GetUserList) (list in db := global.DB.Model(&system.User{}) var userList []system.User + if info.IsVip != nil { + db = db.Where("is_vip = ?", *info.IsVip) + } if info.Account != "" { db = db.Where("account LIKE ?", "%"+info.Account+"%") } @@ -82,7 +86,7 @@ func (userService *UserService) GetUserList(info systemReq.GetUserList) (list in if err != nil { return } - err = db.Limit(limit).Offset(offset).Find(&userList).Error + err = db.Limit(limit).Offset(offset).Order("created_at desc").Find(&userList).Error return userList, total, err } @@ -144,6 +148,7 @@ func (userService *UserService) MiniLogin(code, ip string) (result *system.User, // 7. 根据openid查询用户 存在--> 更新session_key 返回数据 var user system.User + now := time.Now() err = global.DB.Where("open_id = ?", wxResp.Openid).Preload("Avatar").First(&user).Error if errors.Is(err, gorm.ErrRecordNotFound) { // 8. 使用 Transaction 闭包管理事务 @@ -154,6 +159,7 @@ func (userService *UserService) MiniLogin(code, ip string) (result *system.User, OpenId: wxResp.Openid, SessionKey: wxResp.SessionKey, LastLoginIp: ip, + LastLoginAt: &now, } if err := tx.Create(&newUser).Error; err != nil { return err @@ -174,6 +180,7 @@ func (userService *UserService) MiniLogin(code, ip string) (result *system.User, updateData := map[string]interface{}{ "session_key": wxResp.SessionKey, "last_login_ip": ip, + "last_login_at": &now, } if err = global.DB.Model(&user).Updates(updateData).Error; err != nil { global.Logger.Error("更新session_key失败", zap.Error(err))