303 lines
8.4 KiB
Go
303 lines
8.4 KiB
Go
|
package dao
|
|||
|
|
|||
|
import (
|
|||
|
"context"
|
|||
|
"database/sql"
|
|||
|
"encoding/json"
|
|||
|
"fmt"
|
|||
|
"go-common/app/interface/bbq/bullet/api"
|
|||
|
"go-common/app/interface/bbq/bullet/internal/model"
|
|||
|
"go-common/library/log"
|
|||
|
"go-common/library/net/rpc/warden"
|
|||
|
|
|||
|
"go-common/app/interface/bbq/bullet/internal/conf"
|
|||
|
user "go-common/app/service/bbq/user/api"
|
|||
|
video "go-common/app/service/bbq/video/api/grpc/v1"
|
|||
|
filter "go-common/app/service/main/filter/api/grpc/v1"
|
|||
|
"go-common/library/cache/redis"
|
|||
|
xsql "go-common/library/database/sql"
|
|||
|
)
|
|||
|
|
|||
|
// Dao dao
|
|||
|
type Dao struct {
|
|||
|
c *conf.Config
|
|||
|
redis *redis.Pool
|
|||
|
db *xsql.DB
|
|||
|
filterClient filter.FilterClient
|
|||
|
userClient user.UserClient
|
|||
|
videoClient video.VideoClient
|
|||
|
}
|
|||
|
|
|||
|
// New init mysql db
|
|||
|
func New(c *conf.Config) (dao *Dao) {
|
|||
|
dao = &Dao{
|
|||
|
c: c,
|
|||
|
redis: redis.NewPool(c.Redis),
|
|||
|
db: xsql.NewMySQL(c.MySQL),
|
|||
|
filterClient: newFilterClient(c.GRPCClient["filter"]),
|
|||
|
userClient: newUserClient(c.GRPCClient["user"]),
|
|||
|
videoClient: newVideoClient(c.GRPCClient["video"]),
|
|||
|
}
|
|||
|
return
|
|||
|
}
|
|||
|
|
|||
|
// newVideoClient .
|
|||
|
func newVideoClient(cfg *conf.GRPCConf) video.VideoClient {
|
|||
|
cc, err := warden.NewClient(cfg.WardenConf).Dial(context.Background(), cfg.Addr)
|
|||
|
if err != nil {
|
|||
|
panic(err)
|
|||
|
}
|
|||
|
return video.NewVideoClient(cc)
|
|||
|
}
|
|||
|
|
|||
|
// newUserClient .
|
|||
|
func newUserClient(cfg *conf.GRPCConf) user.UserClient {
|
|||
|
cc, err := warden.NewClient(cfg.WardenConf).Dial(context.Background(), cfg.Addr)
|
|||
|
if err != nil {
|
|||
|
panic(err)
|
|||
|
}
|
|||
|
return user.NewUserClient(cc)
|
|||
|
}
|
|||
|
|
|||
|
// newUserClient .
|
|||
|
func newFilterClient(cfg *conf.GRPCConf) filter.FilterClient {
|
|||
|
cc, err := warden.NewClient(cfg.WardenConf).Dial(context.Background(), cfg.Addr)
|
|||
|
if err != nil {
|
|||
|
panic(err)
|
|||
|
}
|
|||
|
return filter.NewFilterClient(cc)
|
|||
|
}
|
|||
|
|
|||
|
// Close close the resource.
|
|||
|
func (d *Dao) Close() {
|
|||
|
d.redis.Close()
|
|||
|
d.db.Close()
|
|||
|
}
|
|||
|
|
|||
|
// Ping dao ping
|
|||
|
func (d *Dao) Ping(ctx context.Context) error {
|
|||
|
// TODO: add mc,redis... if you use
|
|||
|
return d.db.Ping(ctx)
|
|||
|
}
|
|||
|
|
|||
|
// ContentPost .
|
|||
|
func (d *Dao) ContentPost(ctx context.Context, req *api.Bullet) (dmid int64, err error) {
|
|||
|
result, err := d.db.Exec(ctx,
|
|||
|
"insert into bullet_content (oid, mid, offset_ms, offset, content) values (?, ?, ?, ?, ?)",
|
|||
|
req.Oid, req.Mid, req.OffsetMs, req.OffsetMs/1000, req.Content)
|
|||
|
if err != nil {
|
|||
|
log.Errorv(ctx, log.KV("log", "insert bullet fail: req=%s"+req.String()))
|
|||
|
return
|
|||
|
}
|
|||
|
dmid, err = result.LastInsertId()
|
|||
|
return
|
|||
|
}
|
|||
|
|
|||
|
// ContentGet .
|
|||
|
func (d *Dao) ContentGet(ctx context.Context, req *api.ListBulletReq) (res []*api.Bullet, err error) {
|
|||
|
res = []*api.Bullet{}
|
|||
|
mid := req.Mid
|
|||
|
|
|||
|
querySQL := fmt.Sprintf("select id, mid, offset, content from bullet_content where "+
|
|||
|
"oid=%d and state=0 and offset>=%d and offset<%d order by offset, id desc",
|
|||
|
req.Oid, req.StartMs/1000, req.EndMs/1000)
|
|||
|
rows, err := d.db.Query(ctx, querySQL)
|
|||
|
if err != nil {
|
|||
|
return
|
|||
|
}
|
|||
|
defer rows.Close()
|
|||
|
log.V(1).Infow(ctx, "sql", querySQL)
|
|||
|
// 获取时间范围内的全量视频
|
|||
|
var allBullet []*api.Bullet
|
|||
|
midBullets := make(map[int32]*[]*api.Bullet)
|
|||
|
for rows.Next() {
|
|||
|
bullet := new(api.Bullet)
|
|||
|
if err = rows.Scan(&bullet.Id, &bullet.Mid, &bullet.Offset, &bullet.Content); err != nil {
|
|||
|
log.Errorv(ctx, log.KV("log", "scan mysql fail: sql="+querySQL))
|
|||
|
return
|
|||
|
}
|
|||
|
bullet.OffsetMs = bullet.Offset * 1000
|
|||
|
allBullet = append(allBullet, bullet)
|
|||
|
// 先把访问者发过的弹幕按照秒级别进行汇总
|
|||
|
if mid == bullet.Mid {
|
|||
|
v, exists := midBullets[bullet.Offset]
|
|||
|
if !exists {
|
|||
|
v = new([]*api.Bullet)
|
|||
|
midBullets[bullet.Offset] = v
|
|||
|
}
|
|||
|
*v = append(*v, bullet)
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
// 根据全量数据,选择满足条件的弹幕
|
|||
|
currSecond := int32(-1)
|
|||
|
currSecondCount := 0
|
|||
|
for _, bullet := range allBullet {
|
|||
|
if currSecond != bullet.Offset {
|
|||
|
currSecond = bullet.Offset
|
|||
|
currSecondCount = 0
|
|||
|
if midBulletArray, exists := midBullets[currSecond]; exists {
|
|||
|
log.V(10).Infow(ctx, "log", "current second user have published danmu", "offset", currSecond, "len", len(*midBulletArray))
|
|||
|
for _, midBullet := range *midBulletArray {
|
|||
|
currSecondCount++
|
|||
|
res = append(res, midBullet)
|
|||
|
if currSecondCount >= model.SecondMaxNum {
|
|||
|
break
|
|||
|
}
|
|||
|
}
|
|||
|
}
|
|||
|
}
|
|||
|
if currSecondCount >= model.SecondMaxNum {
|
|||
|
continue
|
|||
|
}
|
|||
|
if bullet.Mid != mid {
|
|||
|
currSecondCount++
|
|||
|
bullet.OffsetMs = bullet.Offset * 1000
|
|||
|
res = append(res, bullet)
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
if len(res) > 0 {
|
|||
|
var cursor CursorValue
|
|||
|
cursor.Offset = res[len(res)-1].Offset
|
|||
|
b, _ := json.Marshal(cursor)
|
|||
|
res[len(res)-1].CursorValue = string(b)
|
|||
|
}
|
|||
|
|
|||
|
return
|
|||
|
}
|
|||
|
|
|||
|
// ContentList 用于返回弹幕列表
|
|||
|
/*
|
|||
|
*/
|
|||
|
func (d *Dao) ContentList(ctx context.Context, req *api.ListBulletReq) (res *api.ListBulletReply, err error) {
|
|||
|
res = new(api.ListBulletReply)
|
|||
|
|
|||
|
// 0. 前期准备
|
|||
|
// 获取当前oid的最大offset弹幕的offset
|
|||
|
oidLastOffset, err := d.lastOffset(ctx, req.Oid)
|
|||
|
if err != nil {
|
|||
|
log.Warnv(ctx, log.KV("log", "get has more info fail"))
|
|||
|
return
|
|||
|
}
|
|||
|
// 解析cursor
|
|||
|
cursor, err := parseCursorValue(ctx, req.CursorNext)
|
|||
|
if err != nil {
|
|||
|
log.Warnv(ctx, log.KV("log", "parse cursor value fail"))
|
|||
|
return
|
|||
|
}
|
|||
|
// 当两者相等,则说明已经到列表的最后了
|
|||
|
if oidLastOffset <= cursor.Offset {
|
|||
|
res.HasMore = false
|
|||
|
log.Warnw(ctx, "log", "offset already end", "oid_last_offset", oidLastOffset, "cursor_offset", cursor.Offset)
|
|||
|
return
|
|||
|
}
|
|||
|
|
|||
|
// 1. 按照条数取SecondMaxNum条,返回数据的offset范围start和end
|
|||
|
// 这步是为了保证该次返回至少有条数
|
|||
|
startS := cursor.Offset + 1
|
|||
|
startS, endS, err := d.getNumBulletTs(ctx, req.Oid, startS, model.SecondMaxNum)
|
|||
|
if err != nil {
|
|||
|
log.Warnv(ctx, log.KV("log", "get num start bullet fail"))
|
|||
|
return
|
|||
|
}
|
|||
|
log.V(1).Infow(ctx, "log", "get num bullet ts", "start_s", startS, "end_s", endS)
|
|||
|
endS += 1
|
|||
|
|
|||
|
// 2. 根据选择的时间范围获取弹幕
|
|||
|
newReq := &api.ListBulletReq{StartMs: startS * 1000, EndMs: endS * 1000, Oid: req.Oid, Mid: req.Mid}
|
|||
|
bullets, err := d.ContentGet(ctx, newReq)
|
|||
|
if err != nil {
|
|||
|
log.Warnv(ctx, log.KV("log", "content get fail: req="+newReq.String()))
|
|||
|
return
|
|||
|
}
|
|||
|
res.List = bullets
|
|||
|
|
|||
|
// 3. has_more设置,如果offset和最后时间offset相等,那么肯定没有更多弹幕了
|
|||
|
if len(bullets) > 0 && oidLastOffset > bullets[len(bullets)-1].Offset {
|
|||
|
res.HasMore = true
|
|||
|
} else {
|
|||
|
res.HasMore = false
|
|||
|
}
|
|||
|
|
|||
|
return
|
|||
|
}
|
|||
|
|
|||
|
func (d *Dao) getNumBulletTs(ctx context.Context, oid int64, startOffset, size int32) (startS, endS int32, err error) {
|
|||
|
querySQL := fmt.Sprintf(
|
|||
|
"select offset from bullet_content where oid=%d and state=0 and offset>=%d order by offset limit %d",
|
|||
|
oid, startOffset, size)
|
|||
|
rows, err := d.db.Query(ctx, querySQL)
|
|||
|
if err != nil {
|
|||
|
log.Errorv(ctx, log.KV("log", fmt.Sprintf("get num bullet from db fail: sql=%s", querySQL)))
|
|||
|
return
|
|||
|
}
|
|||
|
log.V(1).Infow(ctx, "sql", querySQL)
|
|||
|
var offset int32
|
|||
|
var index int32
|
|||
|
for rows.Next() {
|
|||
|
if err = rows.Scan(&offset); err != nil {
|
|||
|
log.Errorv(ctx, log.KV("log", "scan mysql fail: sql="+querySQL))
|
|||
|
return
|
|||
|
}
|
|||
|
if index == 0 {
|
|||
|
startS = offset
|
|||
|
}
|
|||
|
endS = offset
|
|||
|
index++
|
|||
|
}
|
|||
|
return
|
|||
|
}
|
|||
|
|
|||
|
func (d *Dao) lastOffset(ctx context.Context, oid int64) (lastOffset int32, err error) {
|
|||
|
querySQL := fmt.Sprintf("select offset from bullet_content where oid=%d and state=0 order by offset desc limit 1", oid)
|
|||
|
row := d.db.QueryRow(ctx, querySQL)
|
|||
|
if err = row.Scan(&lastOffset); err != nil {
|
|||
|
if err == sql.ErrNoRows {
|
|||
|
err = nil
|
|||
|
lastOffset = -1
|
|||
|
} else {
|
|||
|
log.Errorw(ctx, "log", "get has more from db fail", "sql", querySQL, "err", err)
|
|||
|
return
|
|||
|
}
|
|||
|
}
|
|||
|
return
|
|||
|
}
|
|||
|
|
|||
|
// CursorValue .
|
|||
|
type CursorValue struct {
|
|||
|
Offset int32 `json:"offset"`
|
|||
|
// level本来是想要用于避免一次选择太少的弹幕,但后面修改策略进行二次查找之后就没这个必要了
|
|||
|
//Level int32 `json:"level"`
|
|||
|
//duration int32 `json:"duration"`
|
|||
|
}
|
|||
|
|
|||
|
func parseCursorValue(ctx context.Context, cursorValue string) (cursor CursorValue, err error) {
|
|||
|
if len(cursorValue) == 0 {
|
|||
|
cursor.Offset = -1
|
|||
|
//cursor.Level = 1
|
|||
|
return
|
|||
|
}
|
|||
|
if err = json.Unmarshal([]byte(cursorValue), &cursor); err != nil {
|
|||
|
log.Errorw(ctx, "log", "unmarshal fail: str="+cursorValue, "err", err)
|
|||
|
return
|
|||
|
}
|
|||
|
return
|
|||
|
}
|
|||
|
|
|||
|
//
|
|||
|
//// 这里做了个优化,当对于弹幕数较少的视频,level等级定的高点,在弹幕列表页中就可以选取更长范围的弹幕
|
|||
|
//func getCursorLevel(duration int32, num int32) (level int32) {
|
|||
|
// numPerSecond := num / duration
|
|||
|
// if numPerSecond < 1 {
|
|||
|
// level = 10
|
|||
|
// } else if numPerSecond < 2 {
|
|||
|
// level = 5
|
|||
|
// } else if numPerSecond < 5 {
|
|||
|
// level = 2
|
|||
|
// } else {
|
|||
|
// level = 1
|
|||
|
// }
|
|||
|
// return
|
|||
|
//}
|