123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409 |
- package dao
- import (
- "context"
- "encoding/json"
- "fmt"
- "go-common/app/service/bbq/topic/api"
- "go-common/app/service/bbq/topic/internal/model"
- "go-common/library/cache/redis"
- "go-common/library/ecode"
- "go-common/library/log"
- "go-common/library/sync/errgroup.v2"
- "go-common/library/xstr"
- "strings"
- )
- const (
- _selectTopic = "select `id`, `name`, `desc`, `state` from topic where id in (%s)"
- _insertUpdateTopic = "insert into topic (`name`,`score`,`state`,`video_num`) values %s on duplicate key update `video_num`=`video_num`+1"
- _selectTopicID = "select id, name from topic where name in (%s)"
- _selectDiscoveryTopic = "select id from topic where state=0 %s order by score desc, id desc limit %d, %d"
- _selectUnavailabelTopic = "select id from topic where state=1 limit %d,%d"
- _updateTopicField = "update topic set `%s` = ? where `id` = ?"
- )
- const (
- _topicKey = "topic:%d"
- )
- // RawTopicInfo 从mysql获取topic info
- func (d *Dao) RawTopicInfo(ctx context.Context, topicIDs []int64) (res map[int64]*api.TopicInfo, err error) {
- res = make(map[int64]*api.TopicInfo)
- if len(topicIDs) == 0 {
- return
- }
- querySQL := fmt.Sprintf(_selectTopic, xstr.JoinInts(topicIDs))
- rows, err := d.db.Query(ctx, querySQL)
- if err != nil {
- log.Errorw(ctx, "log", "get topic error", "err", err, "sql", querySQL)
- return
- }
- defer rows.Close()
- for rows.Next() {
- topicInfo := new(api.TopicInfo)
- if err = rows.Scan(&topicInfo.TopicId, &topicInfo.Name, &topicInfo.Desc, &topicInfo.State); err != nil {
- log.Errorw(ctx, "log", "get topic from mysql fail", "sql", querySQL)
- return
- }
- topicInfo.CoverUrl = "http://i0.hdslb.com/bfs/bbq/video-image/userface/155886860_1547729941"
- res[topicInfo.TopicId] = topicInfo
- }
- log.V(1).Infow(ctx, "log", "get topic", "req", topicIDs, "rsp_size", len(res))
- return
- }
- // CacheTopicInfo 从缓存获取topic info
- func (d *Dao) CacheTopicInfo(ctx context.Context, topicIDs []int64) (res map[int64]*api.TopicInfo, err error) {
- res = make(map[int64]*api.TopicInfo)
- keys := make([]string, 0, len(topicIDs))
- keyMidMap := make(map[int64]bool, len(topicIDs))
- for _, topicID := range topicIDs {
- key := fmt.Sprintf(_topicKey, topicID)
- if _, exist := keyMidMap[topicID]; !exist {
- // duplicate mid
- keyMidMap[topicID] = true
- keys = append(keys, key)
- }
- }
- conn := d.redis.Get(ctx)
- defer conn.Close()
- for _, key := range keys {
- conn.Send("GET", key)
- }
- conn.Flush()
- var data []byte
- for i := 0; i < len(keys); i++ {
- if data, err = redis.Bytes(conn.Receive()); err != nil {
- if err == redis.ErrNil {
- err = nil
- } else {
- log.Errorv(ctx, log.KV("event", "redis_get"), log.KV("key", keys[i]))
- }
- continue
- }
- topicInfo := new(api.TopicInfo)
- json.Unmarshal(data, topicInfo)
- res[topicInfo.TopicId] = topicInfo
- }
- log.Infov(ctx, log.KV("event", "redis_get"), log.KV("row_num", len(res)))
- return
- }
- // AddCacheTopicInfo 添加topic info缓存
- func (d *Dao) AddCacheTopicInfo(ctx context.Context, topicInfos map[int64]*api.TopicInfo) (err error) {
- keyValueMap := make(map[string][]byte, len(topicInfos))
- for topicID, topicInfo := range topicInfos {
- key := fmt.Sprintf(_topicKey, topicID)
- if _, exist := keyValueMap[key]; !exist {
- data, _ := json.Marshal(topicInfo)
- keyValueMap[key] = data
- }
- }
- conn := d.redis.Get(ctx)
- defer conn.Close()
- for key, value := range keyValueMap {
- conn.Send("SET", key, value, "EX", d.topicExpire)
- }
- conn.Flush()
- for i := 0; i < len(keyValueMap); i++ {
- conn.Receive()
- }
- log.Infov(ctx, log.KV("event", "redis_set"), log.KV("row_num", len(topicInfos)))
- return
- }
- // DelCacheTopicInfo 删除topic info缓存
- func (d *Dao) DelCacheTopicInfo(ctx context.Context, topicID int64) {
- var key = fmt.Sprintf(_topicKey, topicID)
- conn := d.redis.Get(ctx)
- defer conn.Close()
- conn.Do("DEL", key)
- }
- // InsertTopics 插入话题
- func (d *Dao) InsertTopics(ctx context.Context, topics map[string]*api.TopicInfo) (newTopics map[string]*api.TopicInfo, err error) {
- //func (d *Dao) InsertTopics(ctx context.Context, topics map[string]int64) (err error) {
- newTopics = make(map[string]*api.TopicInfo)
- // 0. check
- if len(topics) == 0 {
- return
- }
- if len(topics) > model.MaxBatchLen {
- err = ecode.TopicNumTooManyErr
- return
- }
- // 长度校验
- for _, item := range topics {
- if strings.Count(item.Name, "")-1 > model.MaxTopicNameLen {
- err = ecode.TopicNameLenErr
- log.Errorw(ctx, "log", "topic name len too long", "name", item.Name)
- return
- }
- }
- // 1. 插入更新
- group := errgroup.WithCancel(ctx)
- group.GOMAXPROCS(5)
- var groupInsertTopic = func(topicInfo *api.TopicInfo) {
- group.Go(func(ctx context.Context) (err error) {
- topicID, err := d.insertTopic(ctx, topicInfo)
- if err != nil {
- log.Warnw(ctx, "log", "get topic videos fail", "topic_name", topicInfo.Name)
- return
- }
- if topicID == 0 {
- log.Errorw(ctx, "log", "get error topic_id", "name", topicInfo.Name)
- err = ecode.TopicInsertErr
- return
- }
- topicInfo.TopicId = topicID
- return
- })
- }
- for _, topic := range topics {
- groupInsertTopic(topic)
- }
- err = group.Wait()
- if err != nil {
- log.Warnw(ctx, "log", "do group insert topic fail")
- return
- }
- // 由于insert的时候会返回ID,所以直接赋值返回
- newTopics = topics
- return
- }
- // insertTopic 插入话题
- func (d *Dao) insertTopic(ctx context.Context, topicInfo *api.TopicInfo) (topicID int64, err error) {
- //func (d *Dao) InsertTopics(ctx context.Context, topics map[string]int64) (err error) {
- // 0. check
- // 长度校验
- if strings.Count(topicInfo.Name, "")-1 > model.MaxTopicNameLen {
- err = ecode.TopicNameLenErr
- log.Errorw(ctx, "log", "topic name len too long", "name", topicInfo.Name)
- return
- }
- var str string
- // 1. 插入更新
- str += fmt.Sprintf("('%s',%f,%d,1)", topicInfo.Name, topicInfo.Score, topicInfo.State)
- insertSQL := fmt.Sprintf(_insertUpdateTopic, str)
- log.V(1).Infow(ctx, "sql", insertSQL)
- res, err := d.db.Exec(ctx, insertSQL)
- if err != nil {
- log.Errorw(ctx, "log", "insert topic fail", "topic_name", topicInfo.Name)
- return
- }
- topicID, err = res.LastInsertId()
- if err != nil {
- log.Errorw(ctx, "log", "insert topic fail", "topic_name", topicInfo.Name)
- return
- }
- return
- }
- // UpdateTopic 更新话题,当前有简介和状态
- // 这个函数把操作权其实已经交给上层了,设计上不是个好设计,但是在于避免重复代码
- func (d *Dao) UpdateTopic(ctx context.Context, topicID int64, field string, value interface{}) (err error) {
- if field != "desc" && field != "state" {
- return ecode.ReqParamErr
- }
- querySQL := fmt.Sprintf(_updateTopicField, field)
- _, err = d.db.Exec(ctx, querySQL, value, topicID)
- if err != nil {
- log.Errorw(ctx, "log", "update topic field fail", "field", field, "value", value, "topic_id", topicID)
- return
- }
- d.DelCacheTopicInfo(ctx, topicID)
- return
- }
- // TopicID 通过话题name获取话题ID
- // 话题ID结果存在topics中
- func (d *Dao) TopicID(ctx context.Context, names []string) (topics map[string]int64, err error) {
- topics = make(map[string]int64)
- if len(names) == 0 {
- return
- }
- if len(names) > model.MaxBatchLen {
- err = ecode.TopicNumTooManyErr
- return
- }
- querySQL := fmt.Sprintf(_selectTopicID, "\""+strings.Join(names, "\",\"")+"\"")
- log.V(1).Infow(ctx, "log", "select topic id", "sql", querySQL)
- rows, err := d.db.Query(ctx, querySQL)
- if err != nil {
- log.Errorw(ctx, "log", "get topic id error", "err", err, "sql", querySQL)
- return
- }
- defer rows.Close()
- var topicID int64
- var name string
- for rows.Next() {
- if err = rows.Scan(&topicID, &name); err != nil {
- log.Errorw(ctx, "log", "scan topic id error", "err", err, "sql", querySQL)
- return
- }
- topics[name] = topicID
- }
- log.V(1).Infow(ctx, "log", "get topic id", "req", names, "rsp", topics)
- return
- }
- // ListUnAvailableTopics .
- func (d *Dao) ListUnAvailableTopics(ctx context.Context, page int32, size int32) (list []int64, hasMore bool, err error) {
- hasMore = true
- // 0. check
- if page < 1 {
- err = ecode.TopicReqParamErr
- return
- }
- if page > model.MaxDiscoveryTopicPage {
- hasMore = false
- return
- }
- // 2. get list
- offset := (page - 1) * size
- querySQL := fmt.Sprintf(_selectUnavailabelTopic, offset, size)
- rows, err := d.db.Query(ctx, querySQL)
- if err != nil {
- log.Errorw(ctx, "log", "get topic video error", "err", err, "sql", querySQL)
- return
- }
- defer rows.Close()
- for rows.Next() {
- var topicID int64
- if err = rows.Scan(&topicID); err != nil {
- log.Errorw(ctx, "log", "get topic from mysql fail", "sql", querySQL)
- return
- }
- list = append(list, topicID)
- }
- // 3. 判断has_more
- if len(list) < int(size) {
- hasMore = false
- }
- return
- }
- // ListRankTopics 获取推荐的话题列表
- // TODO: 把置顶逻辑移上去
- func (d *Dao) ListRankTopics(ctx context.Context, page int32, size int32) (list []int64, hasMore bool, err error) {
- hasMore = true
- // 0. check
- if page < 1 {
- err = ecode.TopicReqParamErr
- return
- }
- if page > model.MaxDiscoveryTopicPage {
- hasMore = false
- return
- }
- // 1. 获取置顶数据s
- additionalConditionSQL := ""
- stickList, err := d.GetStickTopic(ctx)
- if err != nil {
- log.Warnw(ctx, "log", "get stick topic fail")
- } else if len(stickList) > 0 {
- additionalConditionSQL = fmt.Sprintf("and id not in (%s)", xstr.JoinInts(stickList))
- }
- // 2. 若page=1,则获取推荐
- if page == 1 {
- list = stickList
- }
- // 3. 根据page获取话题列表
- offset := (page - 1) * size
- querySQL := fmt.Sprintf(_selectDiscoveryTopic, additionalConditionSQL, offset, size)
- log.Infow(ctx, "sql", querySQL, "page", page, "size", size)
- rows, err := d.db.Query(ctx, querySQL)
- if err != nil {
- log.Errorw(ctx, "log", "get topic video error", "err", err, "sql", querySQL)
- return
- }
- defer rows.Close()
- for rows.Next() {
- var topicID int64
- if err = rows.Scan(&topicID); err != nil {
- log.Errorw(ctx, "log", "get topic from mysql fail", "sql", querySQL)
- return
- }
- list = append(list, topicID)
- }
- // 4. 判断has_more
- if len(list) < int(size) {
- hasMore = false
- }
- return
- }
- // GetStickTopic 获取置顶视频
- // TODO: 这个方式是临时之计,当qps增大时,会导致热点的产生
- func (d *Dao) GetStickTopic(ctx context.Context) (list []int64, err error) {
- return d.getRedisList(ctx, model.RedisStickTopicKey)
- }
- func (d *Dao) setStickTopic(ctx context.Context, list []int64) (err error) {
- return d.setRedisList(ctx, model.RedisStickTopicKey, list)
- }
- // StickTopic .
- func (d *Dao) StickTopic(ctx context.Context, opTopicID, op int64) (err error) {
- // 0. check
- info, err := d.TopicInfo(ctx, []int64{opTopicID})
- if err != nil {
- log.Warnw(ctx, "log", "get topic info fail", "topic_id", opTopicID)
- return
- }
- topicInfo, exists := info[opTopicID]
- if !exists {
- log.Errorw(ctx, "log", "stick topic fail due to error topic_id", "topic_id", opTopicID)
- err = ecode.TopicIDNotFound
- return
- }
- if topicInfo.State != api.TopicStateAvailable {
- log.Errorw(ctx, "log", "topic state unavailable to do sticking", "state", topicInfo.State, "topic_id", opTopicID)
- err = ecode.TopicStateErr
- return
- }
- // 1. 获取stick topic
- stickList, err := d.GetStickTopic(ctx)
- if err != nil {
- log.Warnw(ctx, "log", "get stick topic fail")
- return
- }
- // 2. 操作stick topic
- var newStickList []int64
- if op != 0 {
- newStickList = append(newStickList, opTopicID)
- }
- for _, stickTopicID := range stickList {
- if stickTopicID != opTopicID {
- newStickList = append(newStickList, stickTopicID)
- }
- }
- if len(newStickList) > model.MaxStickTopicNum {
- newStickList = newStickList[:model.MaxStickTopicNum]
- }
- // 3. 更新stick topic
- err = d.setStickTopic(ctx, newStickList)
- if err != nil {
- log.Warnw(ctx, "update stick topic fail")
- return
- }
- return
- }
|