弹幕:广播带半显用户名(前两字+***),前端展示前缀

Made-with: Cursor
This commit is contained in:
whm
2026-03-26 15:04:32 +08:00
parent 2e675bda51
commit 07ae6c02ef
3 changed files with 32 additions and 6 deletions

View File

@@ -61,6 +61,19 @@ func SiteDanmakuTokenValid(tokenStr string) bool {
return ok return ok
} }
// MaskSiteUsernameForDanmaku 弹幕展示半匿名1 字为「a***」2 字及以上为前两字 + ***(如 aa***、ab***
func MaskSiteUsernameForDanmaku(username string) string {
username = strings.TrimSpace(username)
if username == "" {
return "***"
}
runes := []rune(username)
if len(runes) == 1 {
return string(runes[0]) + "***"
}
return string(runes[:2]) + "***"
}
type siteRegisterInput struct { type siteRegisterInput struct {
Username string `json:"username" binding:"required"` Username string `json:"username" binding:"required"`
Password string `json:"password" binding:"required"` Password string `json:"password" binding:"required"`

View File

@@ -28,9 +28,14 @@ func writeDanmakuJSON(ws *websocket.Conn, v any) error {
return ws.WriteMessage(websocket.TextMessage, b) return ws.WriteMessage(websocket.TextMessage, b)
} }
// handleDanmakuWS 弹幕:收 JSON {"text":"..."};未带有效 token 仅可收广播不可发。广播 {"type":"dm","text","ts"},不落库 // handleDanmakuWS 弹幕:收 JSON {"text":"..."};未带有效 token 仅可收广播不可发。广播 {"type":"dm","text","from","ts"},不落库
func handleDanmakuWS(c *gin.Context) { func handleDanmakuWS(c *gin.Context) {
canSend := handlers.SiteDanmakuTokenValid(c.Query("token")) claims, tokenOK := handlers.ParseSiteClaims(c.Query("token"))
canSend := tokenOK
fromDisplay := "***"
if tokenOK && claims != nil {
fromDisplay = handlers.MaskSiteUsernameForDanmaku(claims.Username)
}
ws, err := upgrader.Upgrade(c.Writer, c.Request, nil) ws, err := upgrader.Upgrade(c.Writer, c.Request, nil)
if err != nil { if err != nil {
@@ -72,6 +77,7 @@ func handleDanmakuWS(c *gin.Context) {
out, err := json.Marshal(map[string]interface{}{ out, err := json.Marshal(map[string]interface{}{
"type": "dm", "type": "dm",
"text": text, "text": text,
"from": fromDisplay,
"ts": time.Now().UnixMilli(), "ts": time.Now().UnixMilli(),
}) })
if err != nil { if err != nil {

View File

@@ -32,7 +32,7 @@
class="live-dm-line" class="live-dm-line"
:style="{ top: d.top + '%' }" :style="{ top: d.top + '%' }"
> >
{{ d.text }} <span class="live-dm-from">{{ d.from }}</span>{{ d.text }}
</div> </div>
</div> </div>
<div class="live-video-toolbar" role="toolbar" aria-label="播放控制"> <div class="live-video-toolbar" role="toolbar" aria-label="播放控制">
@@ -193,10 +193,12 @@ function toggleVideoFullscreen() {
el.requestFullscreen?.() || el.webkitRequestFullscreen?.() el.requestFullscreen?.() || el.webkitRequestFullscreen?.()
} }
function pushDmLine(text) { function pushDmLine(text, fromRaw) {
const from =
typeof fromRaw === 'string' && fromRaw.trim() !== '' ? fromRaw.trim() : '***'
const id = ++dmIdSeq const id = ++dmIdSeq
const top = 8 + Math.floor(Math.random() * 55) const top = 8 + Math.floor(Math.random() * 55)
dmItems.value = [...dmItems.value, { id, text, top }] dmItems.value = [...dmItems.value, { id, text, top, from }]
setTimeout(() => { setTimeout(() => {
dmItems.value = dmItems.value.filter((x) => x.id !== id) dmItems.value = dmItems.value.filter((x) => x.id !== id)
}, 12000) }, 12000)
@@ -272,7 +274,7 @@ function connectDanmaku() {
return return
} }
if (j.type === 'dm' && typeof j.text === 'string' && j.text) { if (j.type === 'dm' && typeof j.text === 'string' && j.text) {
pushDmLine(j.text) pushDmLine(j.text, j.from)
return return
} }
if (j.type === 'error' && j.code === 'login_required') { if (j.type === 'error' && j.code === 'login_required') {
@@ -459,6 +461,11 @@ onUnmounted(() => {
text-shadow: 0 0 6px #000, 0 0 2px #000; text-shadow: 0 0 6px #000, 0 0 2px #000;
animation: live-dm-marquee 12s linear forwards; animation: live-dm-marquee 12s linear forwards;
} }
.live-dm-from {
color: #7fdbff;
font-weight: 700;
margin-right: 2px;
}
@keyframes live-dm-marquee { @keyframes live-dm-marquee {
from { from {
transform: translateX(105%); transform: translateX(105%);