Initial commit

This commit is contained in:
Donny
2019-04-22 20:46:32 +08:00
commit 49ab8aadd1
25441 changed files with 4055000 additions and 0 deletions

View File

@@ -0,0 +1,22 @@
package(default_visibility = ["//visibility:public"])
filegroup(
name = "package-srcs",
srcs = glob(["**"]),
tags = ["automanaged"],
visibility = ["//visibility:private"],
)
filegroup(
name = "all-srcs",
srcs = [
":package-srcs",
"//app/admin/main/answer/cmd:all-srcs",
"//app/admin/main/answer/conf:all-srcs",
"//app/admin/main/answer/dao:all-srcs",
"//app/admin/main/answer/http:all-srcs",
"//app/admin/main/answer/model:all-srcs",
"//app/admin/main/answer/service:all-srcs",
],
tags = ["automanaged"],
)

View File

@@ -0,0 +1,43 @@
### answer-admin
##### v2.1.0
1. 简繁翻译
##### v2.0.2
1. 用户行为日志
##### v2.0.1
1. 答题v3 fix chan full
##### v2.0.0
1. 答题v3
##### v1.3.0
1. 改为 metadata RemoteIP
##### v1.2.0
1. fix answer list
##### v1.1.6
1. fixed identify
##### v1.1.5
1. bm http server
##### v1.1.4
1. move path
##### v1.1.3
1. delete rpc
##### v1.1.2
1. remove stastd
##### v1.1.1
1. 优化identify
##### v1.1.0
1. 基础库HTTPClient更新
##### v1.0.0
1. 初始化项目,更新依赖

View File

@@ -0,0 +1,13 @@
# Owner
liangkai
zhaogangtao
# Author
guhao
zhangshengchao
zhaogangtao
# Reviewer
liangkai
guhao
zhangshengchao

View File

@@ -0,0 +1,18 @@
# See the OWNERS docs at https://go.k8s.io/owners
approvers:
- guhao
- liangkai
- zhangshengchao
- zhaogangtao
labels:
- admin
- admin/main/answer
- main
options:
no_parent_owners: true
reviewers:
- guhao
- liangkai
- zhangshengchao
- zhaogangtao

View File

@@ -0,0 +1,11 @@
#### answer admin init
##### 项目简介
> 1.提供答题后台管理接口
##### 编译环境
> 请只用golang v1.7.x以上版本编译执行。
##### 依赖包
> 1.公共包go-common

View File

@@ -0,0 +1,44 @@
package(default_visibility = ["//visibility:public"])
load(
"@io_bazel_rules_go//go:def.bzl",
"go_binary",
"go_library",
)
go_binary(
name = "cmd",
embed = [":go_default_library"],
tags = ["automanaged"],
)
go_library(
name = "go_default_library",
srcs = ["main.go"],
data = ["answer-admin-test.toml"],
importpath = "go-common/app/admin/main/answer/cmd",
tags = ["automanaged"],
visibility = ["//visibility:public"],
deps = [
"//app/admin/main/answer/conf:go_default_library",
"//app/admin/main/answer/http:go_default_library",
"//app/admin/main/answer/service:go_default_library",
"//library/log:go_default_library",
"//library/net/trace:go_default_library",
"//library/text/translate/chinese:go_default_library",
],
)
filegroup(
name = "package-srcs",
srcs = glob(["**"]),
tags = ["automanaged"],
visibility = ["//visibility:private"],
)
filegroup(
name = "all-srcs",
srcs = [":package-srcs"],
tags = ["automanaged"],
visibility = ["//visibility:public"],
)

View File

@@ -0,0 +1,77 @@
[log]
dir = "/data/log/answer-admin/"
stdout = true
[bm]
addr = "0.0.0.0:7581"
maxListen = 10
timeout = "1s"
[mysql]
dsn = "root:123456@tcp(127.0.0.1:3306)/bilibili_answer?timeout=5s&readTimeout=5s&writeTimeout=5s&parseTime=true&loc=Local&charset=utf8,utf8mb4"
active = 5
idle = 5
idleTimeout = "4h"
[answer]
fontFilePath = "/data/yahei.ttf"
debug=true
[redis]
name = "answer"
proto = "tcp"
addr = "172.18.33.61:6879"
idle = 10
active = 10
dialTimeout = "1s"
readTimeout = "1s"
writeTimeout = "1s"
idleTimeout = "10s"
expire = "720h"
ansCountExpire = "12h"
ansAddFlagCountExpire = "1m"
[auth]
managerHost = "http://uat-manager.bilibili.co"
dashboardHost = "http://uat-dashboard-mng.bilibili.co"
dashboardCaller = "manager-go"
[auth.DsHTTPClient]
key = "manager-go"
secret = "949bbb2dd3178252638c2407578bc7ad"
dial = "1s"
timeout = "1s"
keepAlive = "60s"
[auth.DsHTTPClient.breaker]
window = "3s"
sleep = "100ms"
bucket = 10
ratio = 0.5
request = 100
[auth.MaHTTPClient]
key = "f6433799dbd88751"
secret = "36f8ddb1806207fe07013ab6a77a3935"
dial = "1s"
timeout = "1s"
keepAlive = "60s"
[auth.MaHTTPClient.breaker]
window = "3s"
sleep = "100ms"
bucket = 10
ratio = 0.5
request = 100
[auth.session]
sessionIDLength = 32
cookieLifeTime = 1800
cookieName = "mng-go"
domain = ".bilibili.co"
[auth.session.Memcache]
name = "go-business/auth"
proto = "tcp"
addr = "172.16.33.54:11211"
active = 5
idle = 5
dialTimeout = "1s"
readTimeout = "1s"
writeTimeout = "1s"
idleTimeout = "80s"

View File

@@ -0,0 +1,47 @@
package main
import (
"flag"
"os"
"os/signal"
"syscall"
"go-common/app/admin/main/answer/conf"
"go-common/app/admin/main/answer/http"
"go-common/app/admin/main/answer/service"
"go-common/library/log"
"go-common/library/net/trace"
"go-common/library/text/translate/chinese"
)
func main() {
flag.Parse()
// init conf,log,trace,stat,perf.
if err := conf.Init(); err != nil {
panic(err)
}
log.Init(conf.Conf.Log)
defer log.Close()
trace.Init(conf.Conf.Tracer)
defer trace.Close()
chinese.Init()
svr := service.New(conf.Conf)
http.Init(conf.Conf, svr)
// signal handler
log.Info("answer-admin start")
c := make(chan os.Signal, 1)
signal.Notify(c, syscall.SIGHUP, syscall.SIGQUIT, syscall.SIGTERM, syscall.SIGINT)
for {
s := <-c
log.Info("answer-admin get a signal %s", s.String())
switch s {
case syscall.SIGQUIT, syscall.SIGTERM, syscall.SIGINT:
log.Info("answer-admin exit")
return
case syscall.SIGHUP:
// TODO reload
default:
return
}
}
}

View File

@@ -0,0 +1,39 @@
package(default_visibility = ["//visibility:public"])
load(
"@io_bazel_rules_go//go:def.bzl",
"go_library",
)
go_library(
name = "go_default_library",
srcs = ["conf.go"],
importpath = "go-common/app/admin/main/answer/conf",
tags = ["automanaged"],
visibility = ["//visibility:public"],
deps = [
"//library/cache/redis:go_default_library",
"//library/conf:go_default_library",
"//library/database/orm:go_default_library",
"//library/log:go_default_library",
"//library/net/http/blademaster:go_default_library",
"//library/net/http/blademaster/middleware/permit:go_default_library",
"//library/net/trace:go_default_library",
"//library/time:go_default_library",
"//vendor/github.com/BurntSushi/toml:go_default_library",
],
)
filegroup(
name = "package-srcs",
srcs = glob(["**"]),
tags = ["automanaged"],
visibility = ["//visibility:private"],
)
filegroup(
name = "all-srcs",
srcs = [":package-srcs"],
tags = ["automanaged"],
visibility = ["//visibility:public"],
)

View File

@@ -0,0 +1,103 @@
package conf
import (
"errors"
"flag"
"go-common/library/cache/redis"
"go-common/library/conf"
"go-common/library/database/orm"
"go-common/library/log"
bm "go-common/library/net/http/blademaster"
"go-common/library/net/http/blademaster/middleware/permit"
"go-common/library/net/trace"
"go-common/library/time"
"github.com/BurntSushi/toml"
)
// Conf global variable.
var (
Conf = &Config{}
confPath string
client *conf.Client
)
// Config struct of conf.
type Config struct {
// log
Log *log.Config
// db
Mysql *orm.Config
// redis
Redis *Redis
// tracer
Tracer *trace.Config
// Answer
Answer *Answer
// bm
BM *bm.ServerConfig
Auth *permit.Config
}
// Redis .
type Redis struct {
*redis.Config
Expire time.Duration
AnsCountExpire time.Duration
AnsAddFlagCountExpire time.Duration
}
// Answer conf.
type Answer struct {
Debug bool
FontFilePath string
}
func configCenter() (err error) {
if client, err = conf.New(); err != nil {
panic(err)
}
if err = load(); err != nil {
return
}
go func() {
for range client.Event() {
log.Info("config reload")
if load() != nil {
log.Error("config reload error (%v)", err)
}
}
}()
return
}
func load() (err error) {
var (
s string
ok bool
tmpConf *Config
)
if s, ok = client.Toml2(); !ok {
return errors.New("load config center error")
}
if _, err = toml.Decode(s, &tmpConf); err != nil {
return errors.New("could not decode config")
}
*Conf = *tmpConf
return
}
func init() {
flag.StringVar(&confPath, "conf", "", "default config path")
}
// Init create config instance.
func Init() (err error) {
if confPath == "" {
err = configCenter()
} else {
_, err = toml.DecodeFile(confPath, &Conf)
}
return
}

View File

@@ -0,0 +1,66 @@
load(
"@io_bazel_rules_go//go:def.bzl",
"go_library",
"go_test",
)
package(default_visibility = ["//visibility:public"])
filegroup(
name = "package-srcs",
srcs = glob(["**"]),
tags = ["automanaged"],
visibility = ["//visibility:private"],
)
filegroup(
name = "all-srcs",
srcs = [":package-srcs"],
tags = ["automanaged"],
)
go_library(
name = "go_default_library",
srcs = [
"dao.go",
"draw.go",
"es.go",
"mysql.go",
"redis.go",
],
importpath = "go-common/app/admin/main/answer/dao",
tags = ["automanaged"],
visibility = ["//visibility:public"],
deps = [
"//app/admin/main/answer/conf:go_default_library",
"//app/admin/main/answer/model:go_default_library",
"//library/cache/redis:go_default_library",
"//library/database/elastic:go_default_library",
"//library/database/orm:go_default_library",
"//library/log:go_default_library",
"//library/time:go_default_library",
"//vendor/github.com/golang/freetype:go_default_library",
"//vendor/github.com/jinzhu/gorm:go_default_library",
"//vendor/golang.org/x/image/font:go_default_library",
"//vendor/golang.org/x/image/math/fixed:go_default_library",
],
)
go_test(
name = "go_default_test",
srcs = [
"dao_test.go",
"draw_test.go",
"es_test.go",
"mysql_test.go",
"redis_test.go",
],
embed = [":go_default_library"],
rundir = ".",
tags = ["automanaged"],
deps = [
"//app/admin/main/answer/conf:go_default_library",
"//app/admin/main/answer/model:go_default_library",
"//vendor/github.com/smartystreets/goconvey/convey:go_default_library",
],
)

View File

@@ -0,0 +1,52 @@
package dao
import (
"time"
"go-common/app/admin/main/answer/conf"
"go-common/library/cache/redis"
"go-common/library/database/elastic"
"go-common/library/database/orm"
"github.com/jinzhu/gorm"
)
// Dao struct info of Dao.
type Dao struct {
c *conf.Config
db *gorm.DB
es *elastic.Elastic
redis *redis.Pool
redisExpire int32
}
// TextImgConf text img conf.
type TextImgConf struct {
Fontsize int
Length int
Ansfontsize int
Spacing float64
Ansspacing float64
}
// New new a Dao and return.
func New(c *conf.Config) (d *Dao) {
d = &Dao{
c: c,
db: orm.NewMySQL(c.Mysql),
es: elastic.NewElastic(nil),
redis: redis.NewPool(c.Redis.Config),
redisExpire: int32(time.Duration(c.Redis.Expire) / time.Second),
}
return
}
// Close close connections of mc, redis, db.
func (d *Dao) Close() {
if d.db != nil {
d.db.Close()
}
if d.redis != nil {
d.redis.Close()
}
}

View File

@@ -0,0 +1,52 @@
package dao
import (
"flag"
"os"
"testing"
"go-common/app/admin/main/answer/conf"
. "github.com/smartystreets/goconvey/convey"
)
var (
d *Dao
)
func TestMain(m *testing.M) {
if os.Getenv("DEPLOY_ENV") != "" {
flag.Set("app_id", "main.account-law.answer-admin")
flag.Set("conf_appid", "main.account-law.answer-admin")
flag.Set("conf_token", "bec0ecd7a2799a424602f9a0daea070d")
flag.Set("tree_id", "4752")
flag.Set("conf_version", "docker-1")
flag.Set("deploy_env", "uat")
flag.Set("conf_env", "10")
flag.Set("conf_host", "config.bilibili.co")
flag.Set("conf_path", "/tmp")
flag.Set("region", "sh")
flag.Set("zone", "sh001")
} else {
flag.Set("conf", "../cmd/answer-admin-test.toml")
}
flag.Parse()
if err := conf.Init(); err != nil {
panic(err)
}
d = New(conf.Conf)
os.Exit(m.Run())
}
func TestNew(t *testing.T) {
Convey("Close", t, func() {
New(conf.Conf)
})
}
func TestClose(t *testing.T) {
Convey("Close", t, func() {
d.Close()
d = New(conf.Conf)
})
}

View File

@@ -0,0 +1,120 @@
package dao
import (
"image"
"image/draw"
"io/ioutil"
"math"
"unicode/utf8"
"go-common/library/log"
"github.com/golang/freetype"
"golang.org/x/image/font"
"golang.org/x/image/math/fixed"
)
// QueConf question img conf.
func (d *Dao) QueConf(mobile bool) (c *TextImgConf) {
if mobile {
// Mobile
c = &TextImgConf{
Fontsize: 16, // mobile font size in points
Length: 11, // mobile question length
Ansfontsize: 12, // mobile ans font size in points
}
} else {
// PC
c = &TextImgConf{
Fontsize: 12, //font size in points
Length: 36, //question length
Ansfontsize: 10, //ans font size in points
}
}
c.Spacing = 2 // line spacing (e.g. 2 means double spaced)
c.Ansspacing = 2 // line ansspacing (e.g. 2 means double spaced)
return
}
// DrawQue draw question title.
func (d *Dao) DrawQue(c *freetype.Context, s string, conf *TextImgConf, pt *fixed.Point26_6) {
c.SetFontSize(float64(conf.Fontsize))
srune := []rune(s)
var end = conf.Length
for len(srune) > 0 {
if conf.Length > len(srune) {
end = len(srune)
}
d.text(c, string(srune[:end]), pt, conf.Fontsize, conf.Spacing)
srune = srune[end:]
}
}
// DrawAns draw ans
func (d *Dao) DrawAns(c *freetype.Context, conf *TextImgConf, anss [4]string, pt *fixed.Point26_6) {
c.SetFontSize(float64(conf.Ansfontsize))
arr := [4]string{"A.", "B.", "C.", "D."}
for i, a := range anss {
d.text(c, arr[i]+a, pt, conf.Ansfontsize, conf.Ansspacing)
}
}
//Height get img height
func (d *Dao) Height(c *TextImgConf, que string, anslen int) (h int) {
len := utf8.RuneCountInString(que)
line := math.Ceil(float64(len) / float64(c.Length))
h = int(math.Ceil(c.Spacing*line*float64(c.Fontsize))) + int(math.Ceil(c.Ansspacing*float64(anslen)*float64(c.Ansfontsize)))
return
}
// text Draw text.
func (d *Dao) text(c *freetype.Context, s string, pt *fixed.Point26_6, size int, spacing float64) (err error) {
_, err = c.DrawString(s, *pt)
if err != nil {
return
}
pt.Y += fixed.Int26_6(int(float64(size)*spacing) << 6)
return
}
var (
dpi = float64(72) // screen resolution in Dots Per Inch
hinting = "none" // none | full
)
// Board init draw board.
func (d *Dao) Board(h int) (r *image.Gray) {
bg := image.White
r = image.NewGray(image.Rect(0, 0, 600, h))
draw.Draw(r, r.Bounds(), bg, image.ZP, draw.Src)
return
}
// Context freetype init context.
func (d *Dao) Context(r *image.Gray, fileStr string) (c *freetype.Context) {
fg := image.Black
// Read the font data.
fontBytes, err := ioutil.ReadFile(fileStr)
if err != nil {
log.Error("ioutil.ReadFile(),err:%+v", err)
return
}
f, err := freetype.ParseFont(fontBytes)
if err != nil {
log.Error("freetype.ParseFont(),err:%+v", err)
return
}
c = freetype.NewContext()
c.SetDPI(dpi)
c.SetFont(f)
c.SetClip(r.Bounds())
c.SetDst(r)
c.SetSrc(fg)
switch hinting {
default:
c.SetHinting(font.HintingNone)
case "full":
c.SetHinting(font.HintingFull)
}
return
}

View File

@@ -0,0 +1,52 @@
package dao
import (
"testing"
. "github.com/smartystreets/goconvey/convey"
)
func TestQueConf(t *testing.T) {
Convey("QueConf true", t, func() {
d.QueConf(true)
})
Convey("QueConf false", t, func() {
d.QueConf(false)
})
}
func TestHeight(t *testing.T) {
Convey("Height true", t, func() {
textImgConf := d.QueConf(true)
d.Height(textImgConf, "这里错了", 1)
})
Convey("Height false", t, func() {
textImgConf := d.QueConf(false)
d.Height(textImgConf, "这里对了", 1)
})
}
func TestDaoBoard(t *testing.T) {
Convey("Board", t, func() {
res := d.Board(4)
So(res, ShouldNotBeNil)
})
}
func TestContext(t *testing.T) {
Convey("Context", t, func() {
board := d.Board(4)
d.Context(board, "/data/conf/yahei.ttf")
})
}
// func TestDrawQue(t *testing.T) {
// Convey("DrawQue", t, func() {
// quec := d.QueConf(true)
// imgh := d.Height(quec, "这里对了", 1)
// board := d.Board(imgh)
// imgc := d.Context(board, "/data/conf/yahei.ttf")
// pt := freetype.Pt(0, int(quec.Fontsize))
// d.DrawQue(imgc, "这里对了", quec, &pt)
// d.DrawAns(imgc, quec, [4]string{"A", "B", "C", "D"}, &pt)
// })
// }

View File

@@ -0,0 +1,151 @@
package dao
import (
"context"
"encoding/json"
"fmt"
"sort"
"time"
"go-common/app/admin/main/answer/model"
"go-common/library/log"
xtime "go-common/library/time"
)
// Page search page info
type Page struct {
Num int64 `json:"num"`
Size int64 `json:"size"`
Total int64 `json:"total"`
}
// SearchSubjectLog get subject logs
type SearchSubjectLog struct {
Page *Page
Result []*struct {
MID int64 `json:"mid"`
Business string `json:"business"`
Action string `json:"action"`
ExtraData string `json:"extra_data"`
Ctime string `json:"ctime"`
}
}
// const .
const (
answerLogID = 15
answerUpdate = "answer_update"
basePass = "basePass"
extraStartTime = "extraStartTime"
extraCheck = "extraCheck"
proQues = "proQues"
proCheck = "proCheck"
captcha = "captchaPass"
level = "level"
)
// HistoryES search archives by es.
func (d *Dao) HistoryES(c context.Context, arg *model.ArgHistory) (reply []*model.AnswerHistoryDB, err error) {
var index []string
for year := time.Now().Year(); year >= 2018; year-- { // 2018.12 开始接入日志
index = append(index, fmt.Sprintf("log_user_action_%d_%d", answerLogID, year))
}
r := d.es.NewRequest("log_user_action").Index(index...)
r.Fields("mid", "action", "ctime", "extra_data")
r.WhereEq("mid", arg.Mid)
r.Pn(arg.Pn).Ps(arg.Ps)
res := &SearchSubjectLog{}
if err = r.Scan(c, &res); err != nil || res == nil {
log.Error("HistoryES:Scan params(%s) error(%v)", r.Params(), err)
return
}
log.Error("HistoryES:%+v", res)
mmh := make(map[int64]map[string]*model.AnswerHistory)
for _, v := range res.Result {
mh := make(map[string]*model.AnswerHistory)
if err = json.Unmarshal([]byte(v.ExtraData), &mh); err != nil {
log.Error("json.Unmarshal(%s) error(%v)", v.ExtraData, err)
return
}
for k, v := range mh {
if mmh[v.Hid] == nil {
mmh[v.Hid] = make(map[string]*model.AnswerHistory)
}
mmh[v.Hid][k] = v
}
}
data := make([]*model.AnswerHistory, 0)
for _, v := range mmh {
if v == nil {
continue
}
h := new(model.AnswerHistory)
if eh, ok := v[basePass]; ok && eh != nil {
h = eh
}
if eh, ok := v[extraStartTime]; ok && eh != nil {
h.StepExtraStartTime = eh.StepExtraStartTime
h.Mtime = eh.Mtime
}
if eh, ok := v[extraCheck]; ok && eh != nil {
h.StepExtraCompleteTime = eh.StepExtraCompleteTime
h.Score = eh.Score
h.StepExtraScore = eh.StepExtraScore
h.Mtime = eh.Mtime
}
if eh, ok := v[proQues]; ok && eh != nil {
h.StepTwoStartTime = eh.StepTwoStartTime
h.Mtime = eh.Mtime
}
if eh, ok := v[proCheck]; ok && eh != nil {
h.CompleteResult = eh.CompleteResult
h.CompleteTime = eh.CompleteTime
h.Score = eh.Score
h.IsFirstPass = eh.IsFirstPass
h.RankID = eh.RankID
h.Mtime = eh.Mtime
}
if eh, ok := v[captcha]; ok && eh != nil {
h.IsPassCaptcha = eh.IsPassCaptcha
h.Mtime = eh.Mtime
}
if eh, ok := v[level]; ok && eh != nil {
h.IsFirstPass = eh.IsFirstPass
h.PassedLevel = eh.PassedLevel
h.Mtime = eh.Mtime
}
data = append(data, h)
}
sort.Sort(model.Histories(data))
return convent(data), nil
}
func convent(src []*model.AnswerHistory) []*model.AnswerHistoryDB {
res := make([]*model.AnswerHistoryDB, 0, len(src))
for _, s := range src {
r := &model.AnswerHistoryDB{
ID: s.ID,
Hid: s.Hid,
Mid: s.Mid,
StartTime: xtime.Time(s.StartTime.Unix()),
StepOneErrTimes: s.StepOneErrTimes,
StepOneCompleteTime: s.StepOneCompleteTime,
StepExtraStartTime: xtime.Time(s.StepExtraStartTime.Unix()),
StepExtraCompleteTime: s.StepExtraCompleteTime,
StepExtraScore: s.StepExtraScore,
StepTwoStartTime: xtime.Time(s.StepTwoStartTime.Unix()),
CompleteTime: xtime.Time(s.CompleteTime.Unix()),
CompleteResult: s.CompleteResult,
Score: s.Score,
IsFirstPass: s.IsFirstPass,
IsPassCaptcha: s.IsPassCaptcha,
PassedLevel: s.PassedLevel,
RankID: s.RankID,
Ctime: xtime.Time(s.Ctime.Unix()),
Mtime: xtime.Time(s.Mtime.Unix()),
}
res = append(res, r)
}
return res
}

View File

@@ -0,0 +1,16 @@
package dao
import (
"context"
"testing"
"go-common/app/admin/main/answer/model"
. "github.com/smartystreets/goconvey/convey"
)
func TestHistoryES(t *testing.T) {
Convey("HistoryES", t, func() {
d.HistoryES(context.Background(), &model.ArgHistory{Mid: 14771787, Pn: 1, Ps: 1})
})
}

View File

@@ -0,0 +1,203 @@
package dao
import (
"context"
"database/sql"
"fmt"
"go-common/app/admin/main/answer/model"
"go-common/library/log"
)
const (
_questionTable = "ans_v3_question"
_typeTable = "ans_v3_question_type"
_queHistory = "answer_history_%d"
)
// QueByID by id.
func (d *Dao) QueByID(c context.Context, id int64) (res *model.Question, err error) {
que := &model.QuestionDB{}
if err := d.db.Table(_questionTable).Where("id=?", id).First(que).Error; err != nil {
return nil, err
}
res = &model.Question{QuestionDB: que, Ans: []string{que.Ans1, que.Ans2, que.Ans3, que.Ans4}}
return
}
// ByIDs by id.
func (d *Dao) ByIDs(c context.Context, IDs []int64) (res []*model.QuestionDB, err error) {
if err := d.db.Table(_questionTable).Where("id in (?)", IDs).Find(&res).Error; err != nil {
return nil, err
}
return
}
// IDsByState by id.
func (d *Dao) IDsByState(c context.Context) (res []int64, err error) {
var (
rows *sql.Rows
)
if rows, err = d.db.Table(_questionTable).Select("id").Where("state = ?", 1).Rows(); err != nil {
return nil, err
}
for rows.Next() {
var qid int64
if err = rows.Scan(&qid); err != nil {
return
}
res = append(res, qid)
}
return
}
// QuestionAdd add register question.
func (d *Dao) QuestionAdd(c context.Context, q *model.QuestionDB) (aff int64, err error) {
db := d.db.Save(q)
if err = db.Error; err != nil {
return
}
aff = db.RowsAffected
return
}
// QuestionEdit edit register question.
func (d *Dao) QuestionEdit(c context.Context, arg *model.QuestionDB) (aff int64, err error) {
que := map[string]interface{}{
"question": arg.Question,
"ans1": arg.Ans1,
"ans2": arg.Ans2,
"ans3": arg.Ans3,
"ans4": arg.Ans4,
"operator": arg.Operator,
}
db := d.db.Table(_questionTable).Omit("ctime, operator").Where("id = ?", arg.ID).Updates(que)
if err = db.Error; err != nil {
log.Error("%+v", err)
return
}
aff = db.RowsAffected
return
}
// QuestionList .
func (d *Dao) QuestionList(c context.Context, arg *model.ArgQue) (res []*model.QuestionDB, err error) {
db := d.db.Table(_questionTable)
if arg.TypeID != 0 {
db = db.Where("type_id=?", arg.TypeID)
}
if arg.State != -1 {
db = db.Where("state=?", arg.State)
}
if len(arg.Question) != 0 {
db = db.Where("question LIKE '%%" + arg.Question + "%%'")
}
db = db.Offset((arg.Pn - 1) * arg.Ps).Limit(arg.Ps).Order("id desc")
if err = db.Find(&res).Error; err != nil {
return nil, err
}
return
}
// QuestionCount question page total count.
func (d *Dao) QuestionCount(c context.Context, arg *model.ArgQue) (res int64, err error) {
db := d.db.Table(_questionTable)
if arg.TypeID != 0 {
db = db.Where("type_id=?", arg.TypeID)
}
if arg.State != -1 {
db = db.Where("state=?", arg.State)
}
if len(arg.Question) != 0 {
db = db.Where("question LIKE '%%" + arg.Question + "%%'")
}
if err = db.Count(&res).Error; err != nil {
return 0, err
}
return
}
// UpdateStatus update question state.
func (d *Dao) UpdateStatus(c context.Context, state int8, qid int64, operator string) (aff int64, err error) {
val := map[string]interface{}{
"state": state,
"operator": operator,
}
db := d.db.Table(_questionTable).Where("id=?", qid).Updates(val)
if err = db.Error; err != nil {
return
}
aff = db.RowsAffected
return
}
// Types get all types.
func (d *Dao) Types(c context.Context) (res []*model.TypeInfo, err error) {
db := d.db.Table(_typeTable)
if err = db.Where("parentid != 0").Find(&res).Error; err != nil {
return nil, err
}
return
}
// TypeSave add register question type.
func (d *Dao) TypeSave(c context.Context, t *model.TypeInfo) (aff int64, err error) {
db := d.db.Save(t)
if err = db.Error; err != nil {
return
}
aff = db.RowsAffected
return
}
// BaseQS .
func (d *Dao) BaseQS(c context.Context) (res []*model.QuestionDB, err error) {
db := d.db.Table("ans_register_question")
db = db.Where("type_id=6 AND state=1")
if err = db.Omit("id").Find(&res).Error; err != nil {
return nil, err
}
return
}
// AllQS .
func (d *Dao) AllQS(c context.Context) (res []*model.QuestionDB, err error) {
db := d.db.Table("ans_v3_question")
if err = db.Find(&res).Error; err != nil {
return nil, err
}
return
}
// InsBaseQs .
func (d *Dao) InsBaseQs(c context.Context, qs *model.QuestionDB) (lastID int64, err error) {
db := d.db.Table(_questionTable)
qs.TypeID = 36
qs.State = 1
db = db.Create(qs)
if err = db.Error; err != nil {
return
}
lastID = qs.ID
return
}
// QueHistory .
func (d *Dao) QueHistory(c context.Context, arg *model.ArgHistory) (res []*model.AnswerHistoryDB, err error) {
db := d.db.Table(fmt.Sprintf(_queHistory, arg.Mid%10))
db = db.Where("mid = ?", arg.Mid).Offset((arg.Pn - 1) * arg.Ps).Limit(arg.Ps).Order("id desc")
if err = db.Find(&res).Error; err != nil {
return nil, err
}
return
}
// HistoryCount .
func (d *Dao) HistoryCount(c context.Context, arg *model.ArgHistory) (res int64, err error) {
db := d.db.Table(fmt.Sprintf(_queHistory, arg.Mid%10))
if err = db.Where("mid = ?", arg.Mid).Count(&res).Error; err != nil {
return 0, err
}
return
}

View File

@@ -0,0 +1,168 @@
package dao
import (
"context"
"fmt"
"testing"
"time"
"go-common/app/admin/main/answer/model"
. "github.com/smartystreets/goconvey/convey"
)
func TestDaoIDsByState(t *testing.T) {
Convey("IDsByState", t, func() {
_, err := d.IDsByState(context.TODO())
So(err, ShouldBeNil)
})
}
func TestDaoQuestionAll(t *testing.T) {
var id int64 = 1
q := &model.QuestionDB{ID: id, State: 1}
Convey("QuestionAdd", t, func() {
aff, err := d.QuestionAdd(context.TODO(), q)
So(err, ShouldBeNil)
So(aff, ShouldNotBeNil)
})
Convey("QueByID", t, func() {
que, err := d.QueByID(context.TODO(), id)
So(err, ShouldBeNil)
So(que, ShouldNotBeNil)
})
Convey("QuestionEdit", t, func() {
q.Question = "testone"
aff, err := d.QuestionEdit(context.TODO(), q)
So(err, ShouldBeNil)
So(aff, ShouldNotBeNil)
})
Convey("ByIDs", t, func() {
res, err := d.ByIDs(context.TODO(), []int64{id})
So(err, ShouldBeNil)
So(res, ShouldNotBeNil)
})
}
func TestDaoQuestionPage(t *testing.T) {
arg := &model.ArgQue{State: 1}
Convey("QuestionList", t, func() {
res, err := d.QuestionList(context.TODO(), arg)
So(err, ShouldBeNil)
So(res, ShouldNotBeNil)
})
Convey("QuestionCount", t, func() {
res, err := d.QuestionCount(context.TODO(), arg)
So(err, ShouldBeNil)
So(res, ShouldNotBeNil)
})
}
func TestDaoUpdateStatus(t *testing.T) {
Convey("UpdateStatus", t, func() {
aff, err := d.UpdateStatus(context.TODO(), 0, 0, "")
So(err, ShouldBeNil)
So(aff, ShouldNotBeNil)
})
}
func TestDaoTypes(t *testing.T) {
Convey("Types", t, func() {
res, err := d.Types(context.TODO())
So(err, ShouldBeNil)
So(res, ShouldNotBeNil)
})
}
func TestDaoTypeAdd(t *testing.T) {
Convey("Types", t, func() {
var allType = []*model.TypeInfo{
{ID: 1, Parentid: 0, Name: "游戏"},
{ID: 2, Parentid: 0, Name: "影视"},
{ID: 3, Parentid: 0, Name: "科技"},
{ID: 4, Parentid: 0, Name: "动画"},
{ID: 5, Parentid: 0, Name: "艺术"},
{ID: 6, Parentid: 0, Name: "流行前线"},
{ID: 7, Parentid: 0, Name: "鬼畜"},
{ID: 8, Parentid: 1, Name: "动作射击", LabelName: "游戏"},
{ID: 9, Parentid: 1, Name: "冒险格斗", LabelName: "游戏"},
{ID: 12, Parentid: 1, Name: "策略模拟 ", LabelName: "游戏"},
{ID: 13, Parentid: 1, Name: "角色扮演 ", LabelName: "游戏"},
{ID: 14, Parentid: 1, Name: "音乐体育 ", LabelName: "游戏"},
{ID: 15, Parentid: 2, Name: "纪录片 ", LabelName: "影视"},
{ID: 16, Parentid: 2, Name: "电影 ", LabelName: "影视"},
{ID: 17, Parentid: 2, Name: "电视剧 ", LabelName: "影视"},
{ID: 18, Parentid: 3, Name: "军事 ", LabelName: "科技"},
{ID: 19, Parentid: 3, Name: "地理 ", LabelName: "科技"},
{ID: 20, Parentid: 3, Name: "历史 ", LabelName: "科技"},
{ID: 21, Parentid: 3, Name: "文学 ", LabelName: "科技"},
{ID: 22, Parentid: 3, Name: "数学 ", LabelName: "科技"},
{ID: 23, Parentid: 3, Name: "物理 ", LabelName: "科技"},
{ID: 24, Parentid: 3, Name: "化学 ", LabelName: "科技"},
{ID: 25, Parentid: 3, Name: "生物 ", LabelName: "科技"},
{ID: 26, Parentid: 3, Name: "数码科技 ", LabelName: "科技"},
{ID: 27, Parentid: 4, Name: "动画声优 ", LabelName: "动画"},
{ID: 28, Parentid: 4, Name: "动漫内容 ", LabelName: "动画"},
{ID: 29, Parentid: 5, Name: "ACG音乐 ", LabelName: "艺术"},
{ID: 30, Parentid: 5, Name: "三次元音乐 ", LabelName: "艺术"},
{ID: 31, Parentid: 5, Name: "绘画 ", LabelName: "艺术"},
{ID: 32, Parentid: 6, Name: "娱乐 ", LabelName: "流行前线"},
{ID: 33, Parentid: 6, Name: "时尚 ", LabelName: "流行前线"},
{ID: 34, Parentid: 6, Name: "运动 ", LabelName: "流行前线"},
{ID: 35, Parentid: 7, Name: "鬼畜 ", LabelName: "鬼畜"},
{ID: 36, Parentid: 0, Name: "基础题", LabelName: "基础题"},
}
for _, v := range allType {
res, err := d.TypeSave(context.TODO(), v)
So(err, ShouldBeNil)
So(res, ShouldNotBeNil)
}
})
}
func TestDaoBaseQS(t *testing.T) {
Convey("TestBaseQS", t, func() {
res, err := d.BaseQS(context.Background())
for k, re := range res {
fmt.Printf("k:%d re:%+v \n", k, re)
}
So(err, ShouldBeNil)
So(res, ShouldNotBeNil)
})
}
func TestDaoInsBaseQs(t *testing.T) {
var qs = &model.QuestionDB{
Mid: 1,
IP: "127.0.0.1",
Question: "qs",
Ans1: "Ans1",
Ans2: "Ans2",
Ans3: "Ans3",
Ans4: "Ans4",
Tips: "tips",
AvID: 1,
MediaType: 1,
Source: 1,
Ctime: time.Now(),
Mtime: time.Now(),
Operator: "operator",
}
Convey("TestInsBaseQs", t, func() {
af, err := d.InsBaseQs(context.Background(), qs)
fmt.Printf("%+v \n", af)
So(err, ShouldBeNil)
})
}
func TestAllQS(t *testing.T) {
Convey("TestAllQS", t, func() {
res, err := d.AllQS(context.Background())
for k, re := range res {
fmt.Printf("k:%d re:%+v \n", k, re)
}
So(err, ShouldBeNil)
So(res, ShouldNotBeNil)
})
}

View File

@@ -0,0 +1,32 @@
package dao
import (
"context"
"go-common/library/log"
)
const (
_baseQsIdsKey = "v3_tc_bqs"
)
// SetQidCache set question id into question set
func (d *Dao) SetQidCache(c context.Context, id int64) (err error) {
var (
key = _baseQsIdsKey
conn = d.redis.Get(c)
)
defer conn.Close()
if err = conn.Send("SET", key, id); err != nil {
log.Error("conn.Send(SET, %s, %d) error(%v)", key, id, err)
return
}
if err = conn.Flush(); err != nil {
log.Error("conn.Flush error(%v)", err)
return
}
if _, err = conn.Receive(); err != nil {
log.Error("conn.Receive() error(%v)", err)
}
return
}

View File

@@ -0,0 +1,15 @@
package dao
import (
"context"
"testing"
"github.com/smartystreets/goconvey/convey"
)
func TestDaoSetQidCache(t *testing.T) {
convey.Convey("SetQidCache", t, func() {
err := d.SetQidCache(context.TODO(), 0)
convey.So(err, convey.ShouldBeNil)
})
}

View File

@@ -0,0 +1,40 @@
package(default_visibility = ["//visibility:public"])
load(
"@io_bazel_rules_go//go:def.bzl",
"go_library",
)
go_library(
name = "go_default_library",
srcs = [
"http.go",
"question.go",
],
importpath = "go-common/app/admin/main/answer/http",
tags = ["automanaged"],
visibility = ["//visibility:public"],
deps = [
"//app/admin/main/answer/conf:go_default_library",
"//app/admin/main/answer/model:go_default_library",
"//app/admin/main/answer/service:go_default_library",
"//library/ecode:go_default_library",
"//library/log:go_default_library",
"//library/net/http/blademaster:go_default_library",
"//library/net/http/blademaster/middleware/permit:go_default_library",
],
)
filegroup(
name = "package-srcs",
srcs = glob(["**"]),
tags = ["automanaged"],
visibility = ["//visibility:private"],
)
filegroup(
name = "all-srcs",
srcs = [":package-srcs"],
tags = ["automanaged"],
visibility = ["//visibility:public"],
)

View File

@@ -0,0 +1,50 @@
package http
import (
"go-common/app/admin/main/answer/conf"
"go-common/app/admin/main/answer/service"
"go-common/library/log"
bm "go-common/library/net/http/blademaster"
"go-common/library/net/http/blademaster/middleware/permit"
)
var (
answerSvc *service.Service
permitX *permit.Permit
)
// Init init http sever instance.
func Init(c *conf.Config, s *service.Service) {
answerSvc = s
permitX = permit.New(c.Auth)
// init inner router
engine := bm.DefaultServer(c.BM)
innerRouter(engine)
// init inner server
if err := engine.Start(); err != nil {
log.Error("xhttp.Serve inner error(%v)", err)
panic(err)
}
}
// innerRouter init inner router.
func innerRouter(e *bm.Engine) {
// health check
e.Ping(ping)
//internal api
que := e.Group("/x/admin/answer/v3")
{
que.GET("/types", types)
que.GET("/questions", permitX.Permit("ANSWER_LIST"), quesList)
que.POST("/upload", permitX.Permit("ANSWER_UPLOAD"), uploadQsts)
que.POST("/question/edit", permitX.Permit("ANSWER_EDIT"), queEdit)
que.POST("/question/disable", permitX.Permit("ANSWER_DISABLE"), queDisable)
que.GET("/question/history", queHistory)
que.GET("/history", history)
que.GET("/load/img", loadImg)
}
}
// ping check server ok.
func ping(c *bm.Context) {}

View File

@@ -0,0 +1,117 @@
package http
import (
"context"
"go-common/app/admin/main/answer/model"
"go-common/library/ecode"
bm "go-common/library/net/http/blademaster"
"mime/multipart"
)
func queDisable(c *bm.Context) {
var (
err error
)
arg := new(struct {
Qids []int64 `form:"id,split"`
Operator string `form:"operator"`
})
if err = c.Bind(arg); err != nil {
return
}
if len(arg.Qids) > model.MaxCount {
c.JSON(nil, ecode.RequestErr)
return
}
username, ok := c.Get("username")
if !ok {
c.JSON(nil, ecode.AccessDenied)
return
}
arg.Operator = username.(string)
if err = answerSvc.BatchUpdateState(c, arg.Qids, model.StageDisable, arg.Operator); err != nil {
c.JSON(nil, err)
return
}
// if arg.State == 1 {
// answerSvc.CreateBFSImg(c, arg.Qids)
// }
c.JSON(nil, nil)
}
func quesList(c *bm.Context) {
var (
err error
arg = new(model.ArgQue)
)
if err = c.Bind(arg); err != nil {
return
}
c.JSON(answerSvc.QuestionList(c, arg))
}
func types(c *bm.Context) {
c.JSON(answerSvc.Types(c))
}
func uploadQsts(c *bm.Context) {
var (
f multipart.File
h *multipart.FileHeader
err error
)
f, h, err = c.Request.FormFile("file")
if err != nil {
c.JSON(nil, ecode.RequestErr)
return
}
username, ok := c.Get("username")
if !ok {
c.JSON(nil, ecode.AccessDenied)
return
}
c.JSON(answerSvc.UploadQsts(c, f, h, username.(string)))
}
func queEdit(c *bm.Context) {
var (
err error
arg = new(model.QuestionDB)
)
if err = c.Bind(arg); err != nil {
return
}
username, ok := c.Get("username")
if !ok {
c.JSON(nil, ecode.AccessDenied)
return
}
arg.Operator = username.(string)
c.JSON(answerSvc.QuestionEdit(c, arg))
}
func loadImg(c *bm.Context) {
c.JSON(nil, answerSvc.LoadImg(context.Background()))
}
func queHistory(c *bm.Context) {
var (
err error
arg = new(model.ArgHistory)
)
if err = c.Bind(arg); err != nil {
return
}
c.JSON(answerSvc.QueHistory(c, arg))
}
func history(c *bm.Context) {
var (
err error
arg = new(model.ArgHistory)
)
if err = c.Bind(arg); err != nil {
return
}
c.JSON(answerSvc.History(c, arg))
}

View File

@@ -0,0 +1,32 @@
package(default_visibility = ["//visibility:public"])
load(
"@io_bazel_rules_go//go:def.bzl",
"go_library",
)
go_library(
name = "go_default_library",
srcs = [
"anwer.go",
"question.go",
],
importpath = "go-common/app/admin/main/answer/model",
tags = ["automanaged"],
visibility = ["//visibility:public"],
deps = ["//library/time:go_default_library"],
)
filegroup(
name = "package-srcs",
srcs = glob(["**"]),
tags = ["automanaged"],
visibility = ["//visibility:private"],
)
filegroup(
name = "all-srcs",
srcs = [":package-srcs"],
tags = ["automanaged"],
visibility = ["//visibility:public"],
)

View File

@@ -0,0 +1,47 @@
package model
import "go-common/library/time"
// TypeInfo type info
type TypeInfo struct {
ID int64 `json:"id"`
Parentid int64 `json:"-" gorm:"column:parentid"`
Name string `json:"name" gorm:"column:typename"`
LabelName string `json:"label_name" gorm:"column:lablename"`
Subs []*SubType `json:"subs"`
}
// TableName for gorm.
func (t *TypeInfo) TableName() string {
return "ans_v3_question_type"
}
// SubType sub type info
type SubType struct {
ID int64 `json:"id"`
Name string `json:"name"`
LabelName string `json:"-"`
}
// AnswerHistoryDB info.
type AnswerHistoryDB struct {
ID int64 `json:"id"`
Hid int64 `json:"hid"`
Mid int64 `json:"mid"`
StartTime time.Time `json:"start_time"`
StepOneErrTimes int8 `json:"step_one_err_times"`
StepOneCompleteTime int64 `json:"step_one_complete_time"`
StepExtraStartTime time.Time `json:"step_extra_start_time"`
StepExtraCompleteTime int64 `json:"step_extra_complete_time"`
StepExtraScore int64 `json:"step_extra_score"`
StepTwoStartTime time.Time `json:"step_two_start_time"`
CompleteTime time.Time `json:"complete_time"`
CompleteResult string `json:"complete_result"`
Score int8 `json:"score"`
IsFirstPass int8 `json:"is_first_pass"`
IsPassCaptcha int8 `json:"is_pass_captcha"`
PassedLevel int8 `json:"passed_level"`
RankID int `json:"rank_id"`
Ctime time.Time `json:"ctime"`
Mtime time.Time `json:"mtime"`
}

View File

@@ -0,0 +1,150 @@
package model
import "time"
// check info.
const (
WaitCheck = int8(0)
PassCheck = int8(1)
NoPassCheck = int8(2)
)
// count.
const (
ArgsCount = 6
MaxCount = 1000
FileMaxSize = 2 * (1024 * 1024) // FileMaxSize max 2M
)
// Medal info
const (
PassNum50 = 50
PassNum100 = 100
PassNum200 = 200
Nid53 = 53
Nid54 = 54
Nid55 = 55
StageDisable int8 = 2
)
// size
const (
MaxQuestion = 120
MinQuestion = 6
MaxAns = 100
MinAns = 2
MaxTips = 100
MinTips = 2
MaxLoadQueSize = 100000
)
// media type
const (
TextMediaType = int8(1)
ImageMediaType = int8(2)
)
//QuestionPage admin page
type QuestionPage struct {
Total int64 `json:"total"`
Items []*QuestionDB `json:"items"`
}
//HistoryPage .
type HistoryPage struct {
Total int64 `json:"total"`
Items []*AnswerHistoryDB `json:"items"`
}
// QuestionDB question info.
type QuestionDB struct {
ID int64 `gorm:"column:id" json:"id" form:"id" validate:"required"`
Mid int64 `gorm:"column:mid" json:"mid"`
IP string `gorm:"column:ip" json:"ip"`
TypeID int8 `gorm:"column:type_id" json:"type_id"`
Question string `gorm:"column:question" json:"question" form:"question" validate:"required"`
Ans1 string `gorm:"column:ans1" json:"ans1" form:"ans1" validate:"required"`
Ans2 string `gorm:"column:ans2" json:"ans2" form:"ans2" validate:"required"`
Ans3 string `gorm:"column:ans3" json:"ans3" form:"ans3" validate:"required"`
Ans4 string `gorm:"column:ans4" json:"ans4" form:"ans4" validate:"required"`
State int8 `gorm:"column:state" json:"state"`
Tips string `gorm:"column:tips" json:"tips"`
AvID int32 `gorm:"column:avid" json:"avid"`
MediaType int8 `gorm:"column:media_type" json:"media_type"`
Source int8 `gorm:"column:source" json:"source"`
Ctime time.Time `gorm:"column:ctime" json:"ctime"`
Mtime time.Time `gorm:"column:mtime" json:"mtime"`
Operator string `gorm:"column:operator" json:"operator"`
}
// TableName for gorm.
func (b QuestionDB) TableName() string {
return "ans_v3_question"
}
// Question question info.
type Question struct {
*QuestionDB
Ans []string
}
// ArgQue admin question query param.
type ArgQue struct {
Question string `form:"question"`
TypeID int8 `form:"type_id"`
State int8 `form:"state" default:"-1"`
Ps int `form:"ps" default:"20"`
Pn int `form:"pn" default:"1"`
}
// ArgHistory .
type ArgHistory struct {
Mid int64 `form:"mid" validate:"required"`
Ps int `form:"ps" default:"20"`
Pn int `form:"pn" default:"1"`
}
// Sizer .
type Sizer interface {
Size() int64
}
// AnswerHistory info.
type AnswerHistory struct {
ID int64 `json:"id"`
Hid int64 `json:"hid"`
Mid int64 `json:"mid"`
StartTime time.Time `json:"start_time"`
StepOneErrTimes int8 `json:"step_one_err_times"`
StepOneCompleteTime int64 `json:"step_one_complete_time"`
StepExtraStartTime time.Time `json:"step_extra_start_time"`
StepExtraCompleteTime int64 `json:"step_extra_complete_time"`
StepExtraScore int64 `json:"step_extra_score"`
StepTwoStartTime time.Time `json:"step_two_start_time"`
CompleteTime time.Time `json:"complete_time"`
CompleteResult string `json:"complete_result"`
Score int8 `json:"score"`
IsFirstPass int8 `json:"is_first_pass"`
IsPassCaptcha int8 `json:"is_pass_captcha"`
PassedLevel int8 `json:"passed_level"`
RankID int `json:"rank_id"`
Ctime time.Time `json:"ctime"`
Mtime time.Time `json:"mtime"`
}
//List .
type List struct {
Total int `json:"total"`
Items []*AnswerHistory `json:"items"`
}
// Histories history sorted.
type Histories []*AnswerHistory
func (h Histories) Len() int { return len(h) }
func (h Histories) Less(i, j int) bool {
return h[i].Ctime.Unix() > h[j].Ctime.Unix()
}
func (h Histories) Swap(i, j int) { h[i], h[j] = h[j], h[i] }

View File

@@ -0,0 +1,61 @@
package(default_visibility = ["//visibility:public"])
load(
"@io_bazel_rules_go//go:def.bzl",
"go_test",
"go_library",
)
go_test(
name = "go_default_test",
srcs = [
"img_test.go",
"question_test.go",
"service_test.go",
],
embed = [":go_default_library"],
rundir = ".",
tags = ["automanaged"],
deps = [
"//app/admin/main/answer/conf:go_default_library",
"//app/admin/main/answer/model:go_default_library",
"//library/xstr:go_default_library",
"//vendor/github.com/smartystreets/goconvey/convey:go_default_library",
],
)
go_library(
name = "go_default_library",
srcs = [
"img.go",
"question.go",
"service.go",
],
importpath = "go-common/app/admin/main/answer/service",
tags = ["automanaged"],
visibility = ["//visibility:public"],
deps = [
"//app/admin/main/answer/conf:go_default_library",
"//app/admin/main/answer/dao:go_default_library",
"//app/admin/main/answer/model:go_default_library",
"//library/cache:go_default_library",
"//library/ecode:go_default_library",
"//library/log:go_default_library",
"//library/text/translate/chinese:go_default_library",
"//vendor/github.com/golang/freetype:go_default_library",
],
)
filegroup(
name = "package-srcs",
srcs = glob(["**"]),
tags = ["automanaged"],
visibility = ["//visibility:private"],
)
filegroup(
name = "all-srcs",
srcs = [":package-srcs"],
tags = ["automanaged"],
visibility = ["//visibility:public"],
)

View File

@@ -0,0 +1,124 @@
package service
import (
"bufio"
"bytes"
"context"
"crypto/hmac"
"crypto/md5"
"crypto/sha1"
"encoding/base64"
"encoding/hex"
"fmt"
"image/jpeg"
"net/http"
"strconv"
"time"
"go-common/library/log"
"go-common/library/text/translate/chinese"
"github.com/golang/freetype"
)
var (
allOrders [][]int
x = []int{0, 1, 2, 3}
bname = "member"
accessKey = "3d34b1ea1dbbb0ca"
secretKey = "d4caa344f3b115e302033b05dd0aa4"
_template = "%s\n%s\n%s\n%d\n"
fileFormat = "v3_%s_A-%s_B-%s_C-%s_D-%s_%s"
bfsURL = "http://bfs.bilibili.co/bfs/%s/%s"
platform = map[string]bool{"H5": true, "PC": false}
language = []string{"zh-CN", "zh-TW"}
)
// generate 全排列组合算法 4*3*2*1 种情况
func (s *Service) generate(c context.Context, a []int, begin int, end int) {
if begin == end {
allOrders = append(allOrders, []int{a[0], a[1], a[2], a[3]})
return
}
for i := begin; i <= end; i++ {
a[begin], a[i] = a[i], a[begin]
s.generate(c, a, begin+1, end)
a[begin], a[i] = a[i], a[begin]
}
return
}
// GenerateImage .
func (s *Service) GenerateImage(c context.Context) {
// 把有效id放redis
ids, _ := s.dao.IDsByState(c)
s.CreateBFSImg(c, ids)
}
// CreateBFSImg .
func (s *Service) CreateBFSImg(c context.Context, ids []int64) {
for _, qid := range ids {
log.Error("qid_%d", qid)
que, err := s.dao.QueByID(c, qid)
log.Error("que:%+v", que)
if err != nil {
log.Error("get question(%d), err(%v)", qid, err)
}
for _, langv := range language {
if langv == "zh-TW" {
que.Question = chinese.Convert(c, que.Question)
que.Ans[0] = chinese.Convert(c, que.Ans[0])
que.Ans[1] = chinese.Convert(c, que.Ans[1])
que.Ans[2] = chinese.Convert(c, que.Ans[2])
que.Ans[3] = chinese.Convert(c, que.Ans[3])
}
for plat, platv := range platform {
for _, order := range allOrders {
log.Error("allOrders(len:%d) %s_%s_%s_%s_%s_%s, %+v", len(allOrders), strconv.FormatInt(qid, 10), que.Ans[order[0]], que.Ans[order[1]], que.Ans[order[2]], que.Ans[order[3]], plat, que)
quec := s.dao.QueConf(platv)
imgh := s.dao.Height(quec, que.Question, len(que.Ans))
r := s.dao.Board(imgh)
imgc := s.dao.Context(r, s.c.Answer.FontFilePath)
pt := freetype.Pt(0, int(quec.Fontsize))
s.dao.DrawQue(imgc, que.Question, quec, &pt)
as := [4]string{que.Ans[order[0]], que.Ans[order[1]], que.Ans[order[2]], que.Ans[order[3]]}
s.dao.DrawAns(imgc, quec, as, &pt)
buf := new(bytes.Buffer)
jpeg.Encode(buf, r, nil)
bufReader := bufio.NewReader(buf)
ts := time.Now().Unix()
tm := time.Unix(ts, 0)
m := md5.New()
m.Write([]byte(fmt.Sprintf(fileFormat, strconv.FormatInt(qid, 10), que.Ans[order[0]], que.Ans[order[1]], que.Ans[order[2]], que.Ans[order[3]], plat)))
fname := hex.EncodeToString(m.Sum(nil)) + ".jpg"
if s.c.Answer.Debug {
fname = fmt.Sprintf("debug_%s", fname)
}
//imgName = append(imgName, fname)
client := &http.Client{}
content := fmt.Sprintf(_template, "PUT", bname, fname, ts)
mac := hmac.New(sha1.New, []byte(secretKey))
mac.Write([]byte(content))
sign := base64.StdEncoding.EncodeToString(mac.Sum(nil))
req, _ := http.NewRequest(http.MethodPut, fmt.Sprintf(bfsURL, bname, fname), bufReader)
req.Host = "bfs.bilibili.co"
req.Header.Add("Date", tm.Format("2006-01-02 03:04:05"))
req.Header.Add("Authorization", accessKey+":"+sign+":"+strconv.FormatInt(ts, 10))
req.Header.Set("Content-Type", "image/jpeg")
resp, _ := client.Do(req)
//defer resp.Body.Close()
if err != nil {
log.Error("qid %s bfs upload failed error(%s)", strconv.FormatInt(qid, 10), err)
//return
}
log.Info("upload img(%d), %s, %+v", qid, fname, resp)
time.Sleep(time.Millisecond * 50)
}
}
}
}
}

View File

@@ -0,0 +1,31 @@
package service
import (
"context"
"testing"
"go-common/app/admin/main/answer/model"
"go-common/library/xstr"
. "github.com/smartystreets/goconvey/convey"
)
func TestServiceImg(t *testing.T) {
Convey("TestServiceImg", t, func() {
ids, err := xstr.SplitInts("35")
s.CreateBFSImg(context.TODO(), ids)
So(err, ShouldBeNil)
})
}
// go test -test.v -test.run TestAddImg
func TestAddImg(t *testing.T) {
q := &model.QuestionDB{ID: 1, State: 1, Question: "test2333"}
Convey("TestAddImgQuestionAdd", t, func() {
err := s.QuestionAdd(context.TODO(), q)
So(err, ShouldBeNil)
})
Convey("TestAddImg", t, func() {
s.GenerateImage(context.TODO())
})
}

View File

@@ -0,0 +1,282 @@
package service
import (
"context"
"encoding/csv"
"io"
"mime/multipart"
"strconv"
"strings"
"time"
"go-common/app/admin/main/answer/model"
"go-common/library/ecode"
"go-common/library/log"
)
// QuestionList .
func (s *Service) QuestionList(c context.Context, arg *model.ArgQue) (res *model.QuestionPage, err error) {
res = &model.QuestionPage{}
if res.Total, err = s.dao.QuestionCount(c, arg); err != nil {
return
}
res.Items = []*model.QuestionDB{}
if res.Total > 0 {
if res.Items, err = s.dao.QuestionList(c, arg); err != nil {
return
}
}
return
}
// UpdateStatus update question state
func (s *Service) UpdateStatus(c context.Context, qid int64, state int8, operator string) (err error) {
var (
r int64
q *model.Question
)
if q, err = s.dao.QueByID(c, qid); err != nil {
log.Error("dao QueByID(%d) error(%v)", qid, err)
return
}
if q == nil || q.State == state {
return
}
if r, err = s.dao.UpdateStatus(c, state, qid, operator); err != nil || r != 1 {
return
}
return
}
// BatchUpdateState bacth update question state.
func (s *Service) BatchUpdateState(c context.Context, qids []int64, state int8, operator string) (err error) {
for _, id := range qids {
s.UpdateStatus(c, id, state, operator)
}
return
}
// Types question type
func (s *Service) Types(c context.Context) (res []*model.TypeInfo, err error) {
return s.dao.Types(c)
}
// ReadCsv read csv file
func (s *Service) ReadCsv(f multipart.File, h *multipart.FileHeader) (rs [][]string, err error) {
r := csv.NewReader(f)
for {
record, err := r.Read()
if err == io.EOF {
break
}
if err != nil {
log.Error("upload question ReadCsv error(%v)", err)
break
}
if len(record) == model.ArgsCount {
rs = append(rs, record)
}
}
return
}
// UploadQsts upload questions
func (s *Service) UploadQsts(c context.Context, f multipart.File, h *multipart.FileHeader, operator string) (msg string, err error) {
defer f.Close()
if h != nil && !strings.HasSuffix(h.Filename, ".csv") {
msg = "not csv file."
return
}
sz, ok := f.(model.Sizer)
if !ok {
msg = "get file size faild."
return
}
size := sz.Size()
if size > model.FileMaxSize {
msg = "file size more than 2M."
return
}
rs, err := s.ReadCsv(f, h)
log.Info("file %s, len(%d)", h.Filename, len(rs))
if len(rs) == 0 || len(rs) > model.MaxCount {
msg = "file size count is 0 or more than " + strconv.FormatInt(model.MaxCount, 10)
return
}
for _, r := range rs {
typeID, err := strconv.ParseInt(r[0], 10, 8)
if err != nil {
log.Error("strconv.ParseInt(%+v) err(%v)", r[0], err)
}
if err == nil {
q := &model.QuestionDB{
TypeID: int8(typeID),
Question: r[1],
Ans1: r[2],
Ans2: r[3],
Ans3: r[4],
Ans4: r[5],
Operator: operator,
}
if err = s.QuestionAdd(c, q); err != nil {
log.Error("s.QuestionAdd(%+v) error(%v)", q, err)
}
}
}
return
}
// QuestionAdd add register question
func (s *Service) QuestionAdd(c context.Context, q *model.QuestionDB) (err error) {
if len(q.Question) < model.MinQuestion || len(q.Question) > model.MaxQuestion {
err = ecode.QuestionStrNotAllow
return
}
if len(q.Ans1) < model.MinAns || len(q.Ans1) > model.MaxAns ||
len(q.Ans2) < model.MinAns || len(q.Ans2) > model.MaxAns ||
len(q.Ans3) < model.MinAns || len(q.Ans3) > model.MaxAns ||
len(q.Ans4) < model.MinAns || len(q.Ans4) > model.MaxAns {
err = ecode.QuestionAnsNotAllow
return
}
if q.Tips != "" && (len(q.Tips) < model.MinTips || len(q.Tips) > model.MaxTips) {
err = ecode.QuestionTipsNotAllow
return
}
if q.TypeID <= 0 {
err = ecode.QuestionTypeNotAllow
return
}
// only sourport text question
q.MediaType = model.TextMediaType
q.State = model.PassCheck
q.Ctime = time.Now()
if _, err = s.dao.QuestionAdd(c, q); err != nil {
return
}
qid := q.ID
s.eventChan.Save(func() {
s.CreateBFSImg(context.Background(), []int64{qid})
})
return
}
func (s *Service) loadtypes(c context.Context) (t map[int64]*model.TypeInfo, err error) {
var tys []*model.TypeInfo
tys, err = s.dao.Types(c)
if err != nil {
log.Error("s.dao.Types error(%v)", err)
return
}
t = make(map[int64]*model.TypeInfo)
for _, v := range tys {
if v.Parentid == 0 && t[v.ID] == nil {
t[v.ID] = &model.TypeInfo{ID: v.ID, Name: v.Name, Subs: []*model.SubType{}}
} else if t[v.Parentid] != nil {
t[v.Parentid].Subs = append(t[v.Parentid].Subs, &model.SubType{ID: v.ID, Name: v.Name, LabelName: v.LabelName})
}
}
return
}
// QuestionEdit .
func (s *Service) QuestionEdit(c context.Context, arg *model.QuestionDB) (aff int64, err error) {
if aff, err = s.dao.QuestionEdit(c, arg); err != nil {
return
}
s.eventChan.Save(func() {
s.CreateBFSImg(context.Background(), []int64{arg.ID})
})
return
}
// LoadTypes .
func (s *Service) LoadTypes(c context.Context) (err error) {
var allType = []*model.TypeInfo{
{ID: 1, Parentid: 0, Name: "游戏"},
{ID: 2, Parentid: 0, Name: "影视"},
{ID: 3, Parentid: 0, Name: "科技"},
{ID: 4, Parentid: 0, Name: "动画"},
{ID: 5, Parentid: 0, Name: "艺术"},
{ID: 6, Parentid: 0, Name: "流行前线"},
{ID: 7, Parentid: 0, Name: "鬼畜"},
{ID: 8, Parentid: 1, Name: "动作射击", LabelName: "游戏"},
{ID: 9, Parentid: 1, Name: "冒险格斗", LabelName: "游戏"},
{ID: 12, Parentid: 1, Name: "策略模拟 ", LabelName: "游戏"},
{ID: 13, Parentid: 1, Name: "角色扮演 ", LabelName: "游戏"},
{ID: 14, Parentid: 1, Name: "音乐体育 ", LabelName: "游戏"},
{ID: 15, Parentid: 2, Name: "纪录片 ", LabelName: "影视"},
{ID: 16, Parentid: 2, Name: "电影 ", LabelName: "影视"},
{ID: 17, Parentid: 2, Name: "电视剧 ", LabelName: "影视"},
{ID: 18, Parentid: 3, Name: "军事 ", LabelName: "科技"},
{ID: 19, Parentid: 3, Name: "地理 ", LabelName: "科技"},
{ID: 20, Parentid: 3, Name: "历史 ", LabelName: "科技"},
{ID: 21, Parentid: 3, Name: "文学 ", LabelName: "科技"},
{ID: 22, Parentid: 3, Name: "数学 ", LabelName: "科技"},
{ID: 23, Parentid: 3, Name: "物理 ", LabelName: "科技"},
{ID: 24, Parentid: 3, Name: "化学 ", LabelName: "科技"},
{ID: 25, Parentid: 3, Name: "生物 ", LabelName: "科技"},
{ID: 26, Parentid: 3, Name: "数码科技 ", LabelName: "科技"},
{ID: 27, Parentid: 4, Name: "动画声优 ", LabelName: "动画"},
{ID: 28, Parentid: 4, Name: "动漫内容 ", LabelName: "动画"},
{ID: 29, Parentid: 5, Name: "ACG音乐 ", LabelName: "艺术"},
{ID: 30, Parentid: 5, Name: "三次元音乐 ", LabelName: "艺术"},
{ID: 31, Parentid: 5, Name: "绘画 ", LabelName: "艺术"},
{ID: 32, Parentid: 6, Name: "娱乐 ", LabelName: "流行前线"},
{ID: 33, Parentid: 6, Name: "时尚 ", LabelName: "流行前线"},
{ID: 34, Parentid: 6, Name: "运动 ", LabelName: "流行前线"},
{ID: 35, Parentid: 7, Name: "鬼畜 ", LabelName: "鬼畜"},
{ID: 36, Parentid: 0, Name: "基础题", LabelName: "基础题"},
}
for _, v := range allType {
if _, err := s.dao.TypeSave(context.Background(), v); err != nil {
log.Error("s.dao.TypeSave(%+v) err(%v)", v, err)
}
}
return
}
// LoadImg .
func (s *Service) LoadImg(c context.Context) (err error) {
qss, err := s.dao.AllQS(c)
if err != nil {
log.Error("s.dao.AllQS() err(%v)", err)
}
for _, qs := range qss {
lastID := qs.ID
if err = s.eventChan.Save(func() {
s.CreateBFSImg(context.Background(), []int64{lastID})
}); err != nil {
log.Error("s.CreateBFSImg(%d) err(%v)", lastID, err)
}
}
return
}
// QueHistory .
func (s *Service) QueHistory(c context.Context, arg *model.ArgHistory) (res *model.HistoryPage, err error) {
res = &model.HistoryPage{}
if res.Total, err = s.dao.HistoryCount(c, arg); err != nil {
return
}
res.Items = []*model.AnswerHistoryDB{}
if res.Total > 0 {
if res.Items, err = s.dao.QueHistory(c, arg); err != nil {
return
}
}
return
}
// History .
func (s *Service) History(c context.Context, arg *model.ArgHistory) (res *model.HistoryPage, err error) {
if arg.Pn <= 0 || arg.Ps <= 0 {
arg.Pn, arg.Ps = 1, 1000
}
res = &model.HistoryPage{}
if res.Items, err = s.dao.HistoryES(c, arg); err != nil {
return
}
res.Total = int64(len(res.Items))
return
}

View File

@@ -0,0 +1,51 @@
package service
import (
"context"
"testing"
"go-common/app/admin/main/answer/model"
"github.com/smartystreets/goconvey/convey"
)
func TestServiceBatchUpdateState(t *testing.T) {
convey.Convey("BatchUpdateState", t, func() {
err := s.BatchUpdateState(context.TODO(), []int64{1, 2, 3}, 0, "")
convey.So(err, convey.ShouldBeNil)
})
}
func TestServiceTypes(t *testing.T) {
convey.Convey("Types", t, func() {
res, err := s.Types(context.TODO())
convey.So(err, convey.ShouldBeNil)
convey.So(res, convey.ShouldNotBeNil)
})
}
func TestServiceQuestionAdd(t *testing.T) {
q := &model.QuestionDB{ID: 1, Question: "test2333"}
convey.Convey("QuestionAdd", t, func() {
err := s.QuestionAdd(context.TODO(), q)
convey.So(err, convey.ShouldBeNil)
})
convey.Convey("UpdateStatus", t, func() {
err := s.UpdateStatus(context.TODO(), 1, 0, "")
convey.So(err, convey.ShouldBeNil)
})
arg := &model.ArgQue{State: 1}
convey.Convey("QuestionList", t, func() {
res, err := s.QuestionList(context.TODO(), arg)
convey.So(err, convey.ShouldBeNil)
convey.So(res, convey.ShouldNotBeNil)
})
}
func TestServiceloadtypes(t *testing.T) {
convey.Convey("loadtypes", t, func() {
t, err := s.loadtypes(context.TODO())
convey.So(err, convey.ShouldBeNil)
convey.So(t, convey.ShouldNotBeNil)
})
}

View File

@@ -0,0 +1,32 @@
package service
import (
"context"
"go-common/app/admin/main/answer/conf"
"go-common/app/admin/main/answer/dao"
"go-common/library/cache"
)
// Service struct of service.
type Service struct {
c *conf.Config
dao *dao.Dao
eventChan *cache.Cache
}
// New create service instance and return.
func New(c *conf.Config) (s *Service) {
s = &Service{
c: c,
dao: dao.New(c),
eventChan: cache.New(1, 10240),
}
s.generate(context.Background(), x, 0, len(x)-1)
return
}
// Close dao.
func (s *Service) Close() {
s.dao.Close()
}

View File

@@ -0,0 +1,44 @@
package service
import (
"flag"
"go-common/app/admin/main/answer/conf"
"os"
"testing"
"github.com/smartystreets/goconvey/convey"
)
var (
s *Service
)
func TestMain(m *testing.M) {
if os.Getenv("DEPLOY_ENV") != "" {
flag.Set("app_id", "main.account-law.answer-admin")
flag.Set("conf_appid", "main.account-law.answer-admin")
flag.Set("conf_token", "bec0ecd7a2799a424602f9a0daea070d")
flag.Set("tree_id", "4752")
flag.Set("conf_version", "docker-1")
flag.Set("deploy_env", "uat")
flag.Set("conf_env", "10")
flag.Set("conf_host", "config.bilibili.co")
flag.Set("conf_path", "/tmp")
flag.Set("region", "sh")
flag.Set("zone", "sh001")
} else {
flag.Set("conf", "../cmd/answer-admin-test.toml")
}
flag.Parse()
if err := conf.Init(); err != nil {
panic(err)
}
s = New(conf.Conf)
os.Exit(m.Run())
}
func TestServiceClose(t *testing.T) {
convey.Convey("Close", t, func() {
s.Close()
})
}