更新若干功能
This commit is contained in:
147
profile.go
147
profile.go
@@ -1,16 +1,15 @@
|
||||
package main
|
||||
|
||||
// profile.go 用户公开资料:昵称/头衔/邮箱/简介/技术栈标签 + 头像缩略图。
|
||||
// 本地存 meta(离线可编辑),登录同步时按 LWW 与远端 user_profiles 表推拉,
|
||||
// 供同服务器的团队成员互相查看(表结构见 init.sql)。
|
||||
// 本地存 meta(离线可编辑),登录同步时按 LWW 与远端 /profile 推拉。
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"net/http"
|
||||
"os"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
@@ -23,7 +22,6 @@ type UserProfile struct {
|
||||
UpdatedAt string `json:"updatedAt"`
|
||||
}
|
||||
|
||||
// parseTechTags 解析技术栈标签 JSON 数组,坏数据回退空。
|
||||
func parseTechTags(s string) []string {
|
||||
out := []string{}
|
||||
if e := json.Unmarshal([]byte(s), &out); e != nil {
|
||||
@@ -135,39 +133,134 @@ func encodeThumb(raw []byte) string {
|
||||
}
|
||||
|
||||
// syncUserProfile 在同步循环中推拉公开资料(LWW),并保持头像缩略图最新。
|
||||
// 表缺失(服务器未升级)时静默跳过。
|
||||
func (a *App) syncUserProfile(ctx context.Context, db *sql.DB, userID int64) {
|
||||
func (a *App) syncUserProfile() {
|
||||
local := a.myProfileLocal()
|
||||
var remote UserProfile
|
||||
var remoteTags string
|
||||
e := db.QueryRowContext(ctx, `SELECT nickname,title,email,bio,tech_tags,updated_at FROM user_profiles WHERE user_id=?`, userID).
|
||||
Scan(&remote.Nickname, &remote.Title, &remote.Email, &remote.Bio, &remoteTags, &remote.UpdatedAt)
|
||||
hasRemote := e == nil
|
||||
if e != nil && e != sql.ErrNoRows {
|
||||
var remote struct {
|
||||
Nickname string `json:"nickname"`
|
||||
Title string `json:"title"`
|
||||
Email string `json:"email"`
|
||||
Bio string `json:"bio"`
|
||||
TechTags []string `json:"techTags"`
|
||||
AvatarThumb string `json:"avatarThumb"`
|
||||
UpdatedAt string `json:"updatedAt"`
|
||||
}
|
||||
if e := a.apiDecode(http.MethodGet, "/api/v1/profile", nil, &remote, true); e != nil {
|
||||
return
|
||||
}
|
||||
hasRemote := remote.UpdatedAt != ""
|
||||
switch {
|
||||
case hasRemote && remote.UpdatedAt > local.UpdatedAt:
|
||||
remote.TechTags = parseTechTags(remoteTags)
|
||||
raw, _ := json.Marshal(remote)
|
||||
p := UserProfile{
|
||||
Nickname: remote.Nickname,
|
||||
Title: remote.Title,
|
||||
Email: remote.Email,
|
||||
Bio: remote.Bio,
|
||||
TechTags: remote.TechTags,
|
||||
UpdatedAt: remote.UpdatedAt,
|
||||
}
|
||||
if p.TechTags == nil {
|
||||
p.TechTags = []string{}
|
||||
}
|
||||
raw, _ := json.Marshal(p)
|
||||
_ = a.store.SetMeta("user_profile", string(raw))
|
||||
case local.UpdatedAt != "" && (!hasRemote || local.UpdatedAt > remote.UpdatedAt):
|
||||
tags, _ := json.Marshal(local.TechTags)
|
||||
_, _ = db.ExecContext(ctx, `INSERT INTO user_profiles(user_id,nickname,title,email,bio,tech_tags,avatar_thumb,updated_at)
|
||||
VALUES(?,?,?,?,?,?,?,?)
|
||||
ON DUPLICATE KEY UPDATE nickname=VALUES(nickname),title=VALUES(title),email=VALUES(email),bio=VALUES(bio),
|
||||
tech_tags=VALUES(tech_tags),avatar_thumb=VALUES(avatar_thumb),updated_at=VALUES(updated_at)`,
|
||||
userID, local.Nickname, local.Title, local.Email, local.Bio, string(tags), a.avatarThumb(), local.UpdatedAt)
|
||||
return // 全量推送已带最新头像
|
||||
body := map[string]any{
|
||||
"nickname": local.Nickname,
|
||||
"title": local.Title,
|
||||
"email": local.Email,
|
||||
"bio": local.Bio,
|
||||
"techTags": local.TechTags,
|
||||
"avatarThumb": a.avatarThumb(),
|
||||
"updatedAt": local.UpdatedAt,
|
||||
}
|
||||
_ = a.apiDecode(http.MethodPut, "/api/v1/profile", body, nil, true)
|
||||
return
|
||||
}
|
||||
// 头像单独变更:只刷新缩略图列,不动 updated_at(文本字段 LWW 不受影响)。
|
||||
// 头像单独变更:只刷新缩略图(服务端 Put 会带上 avatarThumb)。
|
||||
avAt := a.store.Meta("avatar_updated_at")
|
||||
if avAt != "" && avAt != a.store.Meta("profile_avatar_pushed_at") {
|
||||
if _, e := db.ExecContext(ctx, `INSERT INTO user_profiles(user_id,nickname,title,email,bio,tech_tags,avatar_thumb,updated_at)
|
||||
VALUES(?,?,?,?,?,?,?,?)
|
||||
ON DUPLICATE KEY UPDATE avatar_thumb=VALUES(avatar_thumb)`,
|
||||
userID, local.Nickname, local.Title, local.Email, local.Bio, "[]", a.avatarThumb(), local.UpdatedAt); e == nil {
|
||||
body := map[string]any{
|
||||
"nickname": local.Nickname,
|
||||
"title": local.Title,
|
||||
"email": local.Email,
|
||||
"bio": local.Bio,
|
||||
"techTags": local.TechTags,
|
||||
"avatarThumb": a.avatarThumb(),
|
||||
"updatedAt": local.UpdatedAt,
|
||||
}
|
||||
if local.UpdatedAt == "" {
|
||||
body["updatedAt"] = nowRFC()
|
||||
}
|
||||
if a.apiDecode(http.MethodPut, "/api/v1/profile", body, nil, true) == nil {
|
||||
_ = a.store.SetMeta("profile_avatar_pushed_at", avAt)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// AvatarHistoryItem 线上头像历史一条。
|
||||
type AvatarHistoryItem struct {
|
||||
ID int64 `json:"id"`
|
||||
Mode string `json:"mode"`
|
||||
Value string `json:"value"`
|
||||
CreatedAt string `json:"createdAt"`
|
||||
}
|
||||
|
||||
func (a *App) avatarHistoryLoggedIn() bool {
|
||||
return a.syncUserID() > 0 && strings.TrimSpace(a.store.Meta("sync_access_token")) != ""
|
||||
}
|
||||
|
||||
// ListAvatarHistory 拉取当前账号的线上头像历史(需已登录)。
|
||||
func (a *App) ListAvatarHistory() ([]AvatarHistoryItem, error) {
|
||||
if e := a.ready(); e != nil {
|
||||
return nil, e
|
||||
}
|
||||
if !a.avatarHistoryLoggedIn() {
|
||||
return nil, errors.New("SYNC_NOT_LOGGED_IN")
|
||||
}
|
||||
var resp struct {
|
||||
Items []AvatarHistoryItem `json:"items"`
|
||||
}
|
||||
if e := a.apiDecode(http.MethodGet, "/api/v1/profile/avatars", nil, &resp, true); e != nil {
|
||||
return nil, e
|
||||
}
|
||||
if resp.Items == nil {
|
||||
resp.Items = []AvatarHistoryItem{}
|
||||
}
|
||||
return resp.Items, nil
|
||||
}
|
||||
|
||||
// PushAvatarHistory 把一条头像写入线上历史(path 模式会被服务端忽略)。
|
||||
func (a *App) PushAvatarHistory(mode, value string) ([]AvatarHistoryItem, error) {
|
||||
if e := a.ready(); e != nil {
|
||||
return nil, e
|
||||
}
|
||||
if !a.avatarHistoryLoggedIn() {
|
||||
return nil, errors.New("SYNC_NOT_LOGGED_IN")
|
||||
}
|
||||
value = strings.TrimSpace(value)
|
||||
if value == "" {
|
||||
return nil, errors.New("AVATAR_VALUE_REQUIRED")
|
||||
}
|
||||
var resp struct {
|
||||
Items []AvatarHistoryItem `json:"items"`
|
||||
}
|
||||
body := map[string]string{"mode": mode, "value": value}
|
||||
if e := a.apiDecode(http.MethodPost, "/api/v1/profile/avatars", body, &resp, true); e != nil {
|
||||
return nil, e
|
||||
}
|
||||
if resp.Items == nil {
|
||||
resp.Items = []AvatarHistoryItem{}
|
||||
}
|
||||
return resp.Items, nil
|
||||
}
|
||||
|
||||
// DeleteAvatarHistory 删除线上一条头像历史。
|
||||
func (a *App) DeleteAvatarHistory(id int64) error {
|
||||
if e := a.ready(); e != nil {
|
||||
return e
|
||||
}
|
||||
if !a.avatarHistoryLoggedIn() {
|
||||
return errors.New("SYNC_NOT_LOGGED_IN")
|
||||
}
|
||||
return a.apiDecode(http.MethodDelete, "/api/v1/profile/avatars/"+strconv.FormatInt(id, 10), nil, nil, true)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user