Files
go_win_template/internal/api/server.go
Geliebte e266d3098a feat: 抖音直播间弹幕监控 - Go+Wails 实现
后端:
- SQLite 数据层 (rooms/users/chat_logs/settings/error_logs)
- 抖音 WebSocket 抓取器 (proto 解析, 心跳, watchdog)
- __ac_signature 算法 (纯 Go 移植)
- a_bogus/sign.js 签名 (goja JS 引擎)
- Echo HTTP API 服务器 (端口 5000)
- SSE 事件广播
- 自动保活 + 授权守护
- PAYS 云授权对接

前端:
- 顶栏: 品牌 + 日志/调试/客户名单/授权/文档/房间控制/主题切换
- 添加直播间 (链接解析 + 关键词)
-  Dashboard (卡片/列表视图, 公开/匿名房筛选, 分页)
- 详情页 (多 tab 消息流, 右侧用户面板, 颜色标记, 精准客户, 已看标记)
- 精准客户名单 (跨房间汇总)
- 订阅授权页面 (登录/注册/购买)
- 使用帮助 + 系统调试
- 白昼/黑夜主题切换 (CSS 变量 + iconfont)
- 底部版权栏
2026-09-18 04:25:57 +08:00

804 lines
23 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package api
import (
"encoding/csv"
"fmt"
"io"
"log"
"net/http"
"regexp"
"strconv"
"strings"
"sync"
"time"
"github.com/labstack/echo/v4"
"github.com/labstack/echo/v4/middleware"
"github.com/qingwuyingxing/douyin-live-fetcher/internal/broadcaster"
"github.com/qingwuyingxing/douyin-live-fetcher/internal/db"
"github.com/qingwuyingxing/douyin-live-fetcher/internal/fetcher"
"github.com/qingwuyingxing/douyin-live-fetcher/internal/license"
)
var (
fetchers = make(map[string]*fetcher.Fetcher)
fetcherMu sync.RWMutex
bc *broadcaster.Broadcaster
startedAt = time.Now()
)
func StartServer(b *broadcaster.Broadcaster, assets http.FileSystem, port int) error {
bc = b
e := echo.New()
e.HideBanner = true
e.Use(middleware.Recover())
e.Use(middleware.CORSWithConfig(middleware.CORSConfig{
AllowOrigins: []string{"*"},
AllowMethods: []string{"GET", "POST", "OPTIONS"},
AllowHeaders: []string{"Content-Type"},
}))
g := e.Group("/api")
g.POST("/start", handleStart)
g.POST("/stop", handleStop)
g.POST("/del", handleDel)
g.GET("/rooms", handleRooms)
g.GET("/rooms_db", handleRoomsDB)
g.POST("/rooms", handleAddRoom)
g.GET("/messages", handleMessages)
g.GET("/users", handleUsers)
g.GET("/users_db", handleUsersDB)
g.GET("/marks", handleMarksGet)
g.POST("/mark", handleMarkSet)
g.POST("/marks_clear", handleMarksClear)
g.POST("/msg_seen", handleMsgSeen)
g.POST("/msg_seen_bulk", handleMsgSeenBulk)
g.POST("/msg_seen_clear", handleMsgSeenClear)
g.POST("/lead", handleLead)
g.GET("/leads", handleLeads)
g.GET("/leads_export", handleLeadsExport)
g.GET("/export", handleExport)
g.GET("/keywords", handleKeywordsGet)
g.POST("/keywords", handleKeywordsSet)
g.GET("/resolve", handleResolve)
g.GET("/status", handleStatus)
g.GET("/stream", handleStream)
g.GET("/errors", handleErrors)
g.POST("/error", handleError)
g.GET("/license", handleLicenseGet)
g.POST("/license", handleLicensePost)
g.POST("/license/logout", handleLicenseLogout)
g.GET("/settings", handleSettingsGet)
g.POST("/settings", handleSettingsPost)
// 静态文件服务(直接读文件系统)
e.GET("/", func(c echo.Context) error {
return c.File("frontend/dist/index.html")
})
e.Static("/assets", "frontend/dist/assets")
go _autoWatch()
go _licenseWatch()
addr := fmt.Sprintf(":%d", port)
log.Printf("[api] starting server on %s", addr)
return e.Start(addr)
}
func serveFile(c echo.Context, fs http.FileSystem, name string) error {
f, err := fs.Open(name)
if err != nil {
log.Printf("[serveFile] open %q failed: %v", name, err)
return c.String(404, "not found")
}
defer f.Close()
data, err := io.ReadAll(f)
if err != nil {
log.Printf("[serveFile] read %q failed: %v", name, err)
return c.String(500, "read failed")
}
c.Response().Header().Set("Content-Type", http.DetectContentType(data))
c.Response().Header().Set("Cache-Control", "public, max-age=3600")
c.Response().WriteHeader(200)
_, err = c.Response().Write(data)
if err != nil {
log.Printf("[serveFile] write %q failed: %v", name, err)
}
return nil
}
// ─── 房间 ────────────────────────────────────────────────────
func handleStart(c echo.Context) error {
if !license.LicenseAllowed(false) {
return c.JSON(403, map[string]string{"error": "授权不可用"})
}
var body struct {
LiveID string `json:"live_id"`
Keywords string `json:"keywords"`
}
if err := c.Bind(&body); err != nil {
return c.JSON(400, map[string]string{"error": err.Error()})
}
lid := resolveID(body.LiveID)
if lid == "" {
return c.JSON(400, map[string]string{"error": "无效直播间ID"})
}
kws := body.Keywords
if kws == "" {
kws = db.GetSetting("kw_" + lid)
}
db.SaveRoom(lid, kws)
startFetcher(lid, kws, "manual")
return c.JSON(200, map[string]string{"status": "ok", "live_id": lid})
}
func handleStop(c echo.Context) error {
var body struct {
LiveID string `json:"live_id"`
}
c.Bind(&body)
lid := strings.TrimSpace(body.LiveID)
fetcherMu.Lock()
if lid == "all" {
for id := range fetchers {
stopFetcher(id)
}
fetcherMu.Unlock()
return c.JSON(200, map[string]string{"status": "ok", "message": "Stopped all"})
}
if lid != "" {
stopFetcher(lid)
}
fetcherMu.Unlock()
return c.JSON(200, map[string]string{"status": "ok"})
}
func handleDel(c echo.Context) error {
var body struct {
LiveID string `json:"live_id"`
}
c.Bind(&body)
lid := resolveID(body.LiveID)
fetcherMu.Lock()
stopFetcher(lid)
fetcherMu.Unlock()
db.DelRoom(lid)
return c.JSON(200, map[string]string{"status": "ok"})
}
func handleRooms(c echo.Context) error {
fetcherMu.RLock()
rooms := make([]map[string]interface{}, 0)
for id, f := range fetchers {
rooms = append(rooms, map[string]interface{}{
"live_id": id, "connected": f.Ok(), "silent": f.IsSilent(),
"last_pkt_age": time.Since(time.Unix(int64(f.LastPkt()), 0)).Seconds(),
"chatter_count": len(f.Users()), "keywords": f.Keywords(),
})
}
fetcherMu.RUnlock()
return c.JSON(200, map[string]interface{}{"rooms": rooms})
}
func handleRoomsDB(c echo.Context) error {
return c.JSON(200, map[string]interface{}{"rooms": db.GetRooms()})
}
func handleAddRoom(c echo.Context) error {
var body struct {
LiveID string `json:"live_id"`
}
c.Bind(&body)
lid := resolveID(body.LiveID)
if lid == "" {
return c.JSON(400, map[string]string{"error": "无效的直播间ID"})
}
db.SaveRoom(lid, "")
if license.LicenseAllowed(false) {
startFetcher(lid, db.GetSetting("kw_"+lid), "add-room")
}
return c.JSON(200, map[string]interface{}{"status": "ok", "live_id": lid})
}
// ─── 消息 ────────────────────────────────────────────────────
func handleMessages(c echo.Context) error {
lid := resolveID(c.QueryParam("live_id"))
if lid == "" {
return c.JSON(400, map[string]string{"error": "missing live_id"})
}
after := parseInt64(c.QueryParam("after_id"), 0)
limit := min(parseInt(c.QueryParam("limit"), 200), 500)
msgs, last := db.GetMessages(lid, after, limit)
return c.JSON(200, map[string]interface{}{"live_id": lid, "after_id": last, "messages": msgs})
}
// ─── 用户 ────────────────────────────────────────────────────
func handleUsers(c echo.Context) error {
lid := resolveID(c.QueryParam("live_id"))
if lid == "" {
return c.JSON(400, map[string]string{"error": "missing live_id"})
}
fetcherMu.RLock()
f := fetchers[lid]
fetcherMu.RUnlock()
if f != nil {
return c.JSON(200, map[string]interface{}{"live_id": lid, "count": len(f.Users()), "users": f.Users()})
}
users := db.GetUsers(lid)
return c.JSON(200, map[string]interface{}{"live_id": lid, "count": len(users), "users": userMapList(users)})
}
func handleUsersDB(c echo.Context) error {
lid := resolveID(c.QueryParam("live_id"))
if lid == "" {
return c.JSON(400, map[string]string{"error": "missing live_id"})
}
users := db.GetUsers(lid)
return c.JSON(200, map[string]interface{}{"live_id": lid, "count": len(users), "users": userMapList(users)})
}
// ─── 标记 / 已看 / 客户 ─────────────────────────────────────
func handleMarksGet(c echo.Context) error {
lid := resolveID(c.QueryParam("live_id"))
if lid == "" {
return c.JSON(400, map[string]string{"error": "missing live_id"})
}
return c.JSON(200, map[string]interface{}{"live_id": lid, "marks": db.GetMarks(lid)})
}
func handleMarkSet(c echo.Context) error {
var body struct {
LiveID string `json:"live_id"`
UserID string `json:"user_id"`
Mark string `json:"mark"`
}
c.Bind(&body)
lid := resolveID(body.LiveID)
if lid == "" || body.UserID == "" {
return c.JSON(400, map[string]string{"error": "missing live_id/user_id"})
}
valid := map[string]bool{"blue": true, "red": true, "yellow": true}
mark := body.Mark
if mark != "" && !valid[mark] {
mark = ""
}
db.SetMark(lid, body.UserID, mark)
fetcherMu.RLock()
if f := fetchers[lid]; f != nil {
f.SetMark(body.UserID, mark)
}
fetcherMu.RUnlock()
return c.JSON(200, map[string]interface{}{"status": "ok", "user_id": body.UserID, "mark": mark})
}
func handleMarksClear(c echo.Context) error {
var body struct {
LiveID string `json:"live_id"`
}
c.Bind(&body)
lid := resolveID(body.LiveID)
if lid == "" {
return c.JSON(400, map[string]string{"error": "missing live_id"})
}
db.ClearMarks(lid)
fetcherMu.RLock()
if f := fetchers[lid]; f != nil {
f.ClearMarks()
}
fetcherMu.RUnlock()
return c.JSON(200, map[string]interface{}{"status": "ok"})
}
func handleMsgSeen(c echo.Context) error {
var body struct {
LiveID string `json:"live_id"`
MsgID int64 `json:"msg_id"`
Seen bool `json:"seen"`
}
c.Bind(&body)
lid := resolveID(body.LiveID)
if lid == "" || body.MsgID == 0 {
return c.JSON(400, map[string]string{"error": "missing live_id/msg_id"})
}
n := db.SetMsgSeen(lid, body.MsgID, body.Seen)
return c.JSON(200, map[string]interface{}{"status": "ok", "updated": n})
}
func handleMsgSeenBulk(c echo.Context) error {
var body struct {
LiveID string `json:"live_id"`
IDs []int64 `json:"ids"`
Seen bool `json:"seen"`
}
c.Bind(&body)
lid := resolveID(body.LiveID)
if lid == "" || len(body.IDs) == 0 {
return c.JSON(400, map[string]string{"error": "missing live_id/ids"})
}
n := db.SetMsgSeenMany(lid, body.IDs, body.Seen)
return c.JSON(200, map[string]interface{}{"status": "ok", "updated": n})
}
func handleMsgSeenClear(c echo.Context) error {
var body struct {
LiveID string `json:"live_id"`
}
c.Bind(&body)
lid := resolveID(body.LiveID)
if lid == "" {
return c.JSON(400, map[string]string{"error": "missing live_id"})
}
n := db.ClearMsgSeen(lid)
return c.JSON(200, map[string]interface{}{"status": "ok", "cleared": n})
}
func handleLead(c echo.Context) error {
var body struct {
LiveID string `json:"live_id"`
UserID string `json:"user_id"`
Lead bool `json:"lead"`
}
c.Bind(&body)
lid := resolveID(body.LiveID)
if lid == "" || body.UserID == "" {
return c.JSON(400, map[string]string{"error": "missing live_id/user_id"})
}
db.SetLead(lid, body.UserID, body.Lead)
fetcherMu.RLock()
if f := fetchers[lid]; f != nil {
f.SetLead(body.UserID, body.Lead)
}
fetcherMu.RUnlock()
return c.JSON(200, map[string]interface{}{"status": "ok", "lead": body.Lead})
}
func handleLeads(c echo.Context) error {
lid := resolveID(c.QueryParam("live_id"))
leads := db.GetLeads(lid)
return c.JSON(200, map[string]interface{}{"live_id": lid, "count": len(leads), "leads": userMapList(leads)})
}
func handleLeadsExport(c echo.Context) error {
lid := resolveID(c.QueryParam("live_id"))
leads := db.GetLeads(lid)
buf := new(strings.Builder)
w := csv.NewWriter(buf)
w.Write([]string{"live_id", "user_id", "nickname", "profile_url", "mark", "last_msg", "msg_count",
"gift_count", "enter_count", "like_count", "follow_count", "lead_time", "last_seen"})
for _, u := range leads {
profileURL := ""
if u.SecUID != "" {
profileURL = "https://www.douyin.com/user/" + u.SecUID
}
w.Write([]string{
u.LiveID, u.UserID, u.UserName, profileURL, u.Mark, u.LastMsg,
strconv.Itoa(u.MsgCount), strconv.Itoa(u.GiftCount), strconv.Itoa(u.EnterCount),
strconv.Itoa(u.LikeCount), strconv.Itoa(u.FollowCount),
formatTS(u.LeadTs), formatTS(u.LastSeen),
})
}
w.Flush()
resp := c.Response()
resp.Header().Set("Content-Type", "text/csv; charset=utf-8")
resp.Header().Set("Content-Disposition", "attachment; filename=leads.csv")
resp.Write([]byte("\ufeff" + buf.String()))
return nil
}
func handleExport(c echo.Context) error {
lid := resolveID(c.QueryParam("live_id"))
fmtType := c.QueryParam("fmt")
if lid == "" {
return c.JSON(400, map[string]string{"error": "missing live_id"})
}
fetcherMu.RLock()
var users []map[string]interface{}
if f := fetchers[lid]; f != nil {
users = f.Users()
}
fetcherMu.RUnlock()
if len(users) == 0 {
users = userMapList(db.GetUsers(lid))
}
if fmtType == "json" {
return c.JSON(200, users)
}
buf := new(strings.Builder)
w := csv.NewWriter(buf)
w.Write([]string{"user_id", "nickname", "msg_count", "gift_count", "enter_count", "like_count",
"follow_count", "last_msg", "hit_keyword", "mark", "lead", "last_action", "sec_uid", "profile_url"})
for _, u := range users {
suid := strVal(u, "sec_uid")
profileURL := ""
if suid != "" {
profileURL = "https://www.douyin.com/user/" + suid
}
w.Write([]string{
strVal(u, "user_id"), strVal(u, "user_name"),
strVal(u, "msg_count"), strVal(u, "gift_count"), strVal(u, "enter_count"),
strVal(u, "like_count"), strVal(u, "follow_count"),
strVal(u, "last_msg"), strVal(u, "hit_kw"), strVal(u, "mark"),
func() string { if boolVal(u, "lead") { return "精准客户" }; return "" }(),
strVal(u, "last_action"), suid, profileURL,
})
}
w.Flush()
resp := c.Response()
resp.Header().Set("Content-Type", "text/csv; charset=utf-8")
resp.Header().Set("Content-Disposition", "attachment; filename=users_"+lid+".csv")
resp.Write([]byte("\ufeff" + buf.String()))
return nil
}
// ─── 关键词 / 解析 ───────────────────────────────────────────
func handleKeywordsGet(c echo.Context) error {
lid := resolveID(c.QueryParam("live_id"))
if lid == "" {
return c.JSON(400, map[string]string{"error": "missing live_id"})
}
return c.JSON(200, map[string]interface{}{"live_id": lid, "keywords": db.GetSetting("kw_" + lid)})
}
func handleKeywordsSet(c echo.Context) error {
var body struct {
LiveID string `json:"live_id"`
Keywords string `json:"keywords"`
}
c.Bind(&body)
lid := resolveID(body.LiveID)
if lid == "" {
return c.JSON(400, map[string]string{"error": "missing live_id"})
}
db.SetSetting("kw_"+lid, body.Keywords)
fetcherMu.RLock()
if f := fetchers[lid]; f != nil {
f.SetKeywords(body.Keywords)
}
fetcherMu.RUnlock()
return c.JSON(200, map[string]string{"status": "ok"})
}
func handleResolve(c echo.Context) error {
lid := resolveID(c.QueryParam("input"))
if lid == "" {
return c.JSON(400, map[string]string{"error": "无法识别为抖音直播间,请输入直播间ID或完整分享链接"})
}
return c.JSON(200, map[string]string{"live_id": lid})
}
// ─── 状态 / SSE ─────────────────────────────────────────────
func handleStatus(c echo.Context) error {
fetcherMu.RLock()
rooms := make([]map[string]interface{}, 0)
for id, f := range fetchers {
rooms = append(rooms, map[string]interface{}{
"live_id": id, "connected": f.Ok(),
"last_pkt_age": time.Since(time.Unix(int64(f.LastPkt()), 0)).Seconds(),
"silent": f.IsSilent(), "chatter_count": len(f.Users()), "keywords": f.Keywords(),
})
}
fetcherMu.RUnlock()
ts := license.AppLicenseState(false)
note, _ := ts["note"].(string)
if note == "" {
note = "登录订阅账号以解锁全部功能"
}
return c.JSON(200, map[string]interface{}{
"mode": func() string { if db.GetSetting("mode") == "debug" { return "debug" }; return "release" }(),
"version": "1.1.0",
"uptime": time.Since(startedAt).Seconds(),
"log_enabled": db.GetSetting("log_enabled") == "1",
"rooms": rooms,
"license": ts,
"note": note,
})
}
func handleStream(c echo.Context) error {
ch := bc.Register()
c.Response().Header().Set("Content-Type", "text/event-stream")
c.Response().Header().Set("Cache-Control", "no-cache")
c.Response().Header().Set("Connection", "keep-alive")
flusher, _ := c.Response().Writer.(http.Flusher)
done := c.Request().Context().Done()
for {
select {
case <-done:
bc.Unregister(ch)
return nil
case data := <-ch:
fmt.Fprintf(c.Response().Writer, "data: %s\n\n", string(data))
if flusher != nil {
flusher.Flush()
}
}
}
}
// ─── 错误日志 ────────────────────────────────────────────────
func handleErrors(c echo.Context) error {
limit := parseInt(c.QueryParam("limit"), 100)
return c.JSON(200, map[string]interface{}{"errors": db.GetErrors(limit)})
}
func handleError(c echo.Context) error {
var body struct {
Message string `json:"message"`
Stack string `json:"stack"`
RoomID string `json:"room_id"`
}
c.Bind(&body)
msg := body.Message
if len(msg) > 2000 {
msg = msg[:2000]
}
stack := body.Stack
if len(stack) > 8000 {
stack = stack[:8000]
}
db.LogError("frontend", body.RoomID, msg, stack)
return c.JSON(200, map[string]string{"status": "ok"})
}
// ─── 授权 ────────────────────────────────────────────────────
func handleLicenseGet(c echo.Context) error {
return c.JSON(200, map[string]interface{}{
"license": license.AppLicenseState(true),
"config": license.LicenseConfig(),
})
}
func handleLicensePost(c echo.Context) error {
var body struct {
ServerUrl string `json:"server_url"`
CustomerCode string `json:"customer_code"`
ProductCode string `json:"product_code"`
AccountToken string `json:"account_token"`
Login string `json:"login"`
Password string `json:"password"`
}
c.Bind(&body)
if body.Login != "" && body.Password != "" {
data, err := license.LoginAccount(body.ServerUrl, body.Login, body.Password)
if err != nil {
return c.JSON(400, map[string]interface{}{"status": "error", "message": err.Error(), "license": license.AppLicenseState(false)})
}
return c.JSON(200, map[string]interface{}{"status": "ok", "account": data, "license": license.CheckCloudLicense(true), "config": license.LicenseConfig()})
}
license.SaveLicenseConfig(body.ServerUrl, body.CustomerCode, body.ProductCode, body.AccountToken)
return c.JSON(200, map[string]interface{}{"status": "ok", "license": license.CheckCloudLicense(true), "config": license.LicenseConfig()})
}
func handleLicenseLogout(c echo.Context) error {
license.ClearLicenseLogin()
return c.JSON(200, map[string]interface{}{"status": "ok", "license": license.AppLicenseState(true), "config": license.LicenseConfig()})
}
// ─── 设置 ────────────────────────────────────────────────────
func handleSettingsGet(c echo.Context) error {
return c.JSON(200, map[string]interface{}{
"log_enabled": db.GetSetting("log_enabled") == "1",
"mode": db.GetSetting("mode"),
})
}
func handleSettingsPost(c echo.Context) error {
var body struct {
LogEnabled *bool `json:"log_enabled"`
Mode string `json:"mode"`
}
c.Bind(&body)
if body.LogEnabled != nil {
db.SetSetting("log_enabled", boolStr(*body.LogEnabled))
}
if body.Mode == "debug" || body.Mode == "release" {
db.SetSetting("mode", body.Mode)
}
return c.JSON(200, map[string]string{"status": "ok"})
}
// ─── 内部 helper ─────────────────────────────────────────────
func startFetcher(lid, keywords, reason string) {
fetcherMu.Lock()
defer fetcherMu.Unlock()
if f, ok := fetchers[lid]; ok {
if f.ThreadAlive() {
log.Printf("[api] room %s already running", lid)
return
}
f.Stop()
delete(fetchers, lid)
}
if keywords == "" {
keywords = db.GetSetting("kw_" + lid)
}
log.Printf("[api] start room=%s kw=[%s] reason=%s", lid, keywords, reason)
bc.Broadcast(map[string]interface{}{"type": "status", "room_id": lid, "content": "connecting", "timestamp": float64(time.Now().Unix())})
f := fetcher.NewFetcher(lid, bc, keywords)
fetchers[lid] = f
go f.Start()
}
func stopFetcher(lid string) {
fetcherMu.Lock()
f := fetchers[lid]
delete(fetchers, lid)
fetcherMu.Unlock()
if f != nil {
f.Stop()
}
}
func _autoWatch() {
for {
time.Sleep(15 * time.Second)
if !license.LicenseAllowed(false) {
continue
}
rooms := db.GetRooms()
fetcherMu.Lock()
for _, r := range rooms {
lid := r.LiveID
f := fetchers[lid]
if f == nil {
startFetcher(lid, db.GetSetting("kw_"+lid), "auto-start")
continue
}
alive := f.ThreadAlive()
ok := f.Ok()
age := time.Since(time.Unix(int64(f.LastPkt()), 0)).Seconds()
silent := f.IsSilent()
if alive && ok && !silent && age <= 90 {
continue
}
log.Printf("[auto-watch] room %s reconnect: age=%.1fs", lid, age)
stopFetcher(lid)
startFetcher(lid, db.GetSetting("kw_"+lid), "auto-reconnect")
}
for lid := range fetchers {
found := false
for _, r := range rooms {
if r.LiveID == lid {
found = true
break
}
}
if !found {
stopFetcher(lid)
}
}
fetcherMu.Unlock()
}
}
func _licenseWatch() {
for {
time.Sleep(30 * time.Second)
if !license.LicenseAllowed(false) && len(fetchers) > 0 {
fetcherMu.Lock()
for id := range fetchers {
stopFetcher(id)
}
fetcherMu.Unlock()
log.Printf("[license] 授权不可用,全部直播间已停止")
}
}
}
func resolveID(raw string) string {
raw = strings.TrimSpace(raw)
if raw == "" {
return ""
}
if allDigits(raw) {
return raw
}
re1 := regexp.MustCompile(`live\.douyin\.com/(\d+)`)
if m := re1.FindStringSubmatch(raw); len(m) > 1 {
return m[1]
}
re2 := regexp.MustCompile(`webcast\.amemv\.com/\w+/\w+/reflow/(\d+)`)
if m := re2.FindStringSubmatch(raw); len(m) > 1 {
return m[1]
}
re3 := regexp.MustCompile(`https?://\S+`)
for _, u := range re3.FindAllString(raw, -1) {
if m := re1.FindStringSubmatch(u); len(m) > 1 {
return m[1]
}
if m := re2.FindStringSubmatch(u); len(m) > 1 {
return m[1]
}
}
if allDigits(raw) {
return raw
}
return ""
}
func allDigits(s string) bool {
if s == "" {
return false
}
for _, c := range s {
if c < '0' || c > '9' {
return false
}
}
return true
}
func userMapList(users []db.User) []map[string]interface{} {
out := make([]map[string]interface{}, len(users))
for i, u := range users {
out[i] = map[string]interface{}{
"id": u.ID, "live_id": u.LiveID, "user_id": u.UserID,
"user_name": u.UserName, "first_seen": u.FirstSeen, "last_seen": u.LastSeen,
"msg_count": u.MsgCount, "gift_count": u.GiftCount, "enter_count": u.EnterCount,
"like_count": u.LikeCount, "follow_count": u.FollowCount, "last_msg": u.LastMsg,
"last_action": u.LastAction, "hit_kw": u.HitKw, "sec_uid": u.SecUID,
"avatar": u.Avatar, "mark": u.Mark, "lead": u.Lead, "lead_ts": u.LeadTs,
}
}
return out
}
func strVal(m map[string]interface{}, k string) string {
v, ok := m[k]
if !ok {
return ""
}
s, _ := v.(string)
return s
}
func boolVal(m map[string]interface{}, k string) bool {
v, ok := m[k]
if !ok {
return false
}
b, _ := v.(bool)
return b
}
func parseInt(s string, def int) int {
n, err := strconv.Atoi(s)
if err != nil {
return def
}
return n
}
func parseInt64(s string, def int64) int64 {
n, err := strconv.ParseInt(s, 10, 64)
if err != nil {
return def
}
return n
}
func boolStr(b bool) string {
if b {
return "1"
}
return "0"
}
func formatTS(ts float64) string {
if ts == 0 {
return ""
}
return time.Unix(int64(ts), 0).Format("2006-01-02 15:04")
}