Files
kefu/im/backend/internal/app/extensions.go
T
2026-09-03 08:38:17 +08:00

250 lines
11 KiB
Go

package app
import (
"database/sql"
"net/http"
"strings"
)
func (a *App) searchUsers(w http.ResponseWriter, r *http.Request) {
keyword := strings.TrimSpace(r.URL.Query().Get("q"))
if keyword == "" {
reply(w, map[string]any{"items": []profileView{}, "total": 0})
return
}
pattern := "%" + keyword + "%"
page, pageSize, offset := pageOptions(r)
var total int
if err := a.db.QueryRowContext(r.Context(), `SELECT COUNT(*) FROM users u JOIN user_profiles p ON p.user_id=u.id JOIN user_privacy_settings privacy ON privacy.user_id=u.id WHERE u.status=1 AND u.id<>? AND privacy.allow_search=1 AND (p.nickname LIKE ? OR u.public_id LIKE ?) AND NOT EXISTS(SELECT 1 FROM user_blocks b WHERE (b.user_id=? AND b.blocked_user_id=u.id) OR (b.user_id=u.id AND b.blocked_user_id=?))`, current(r).ID, pattern, pattern, current(r).ID, current(r).ID).Scan(&total); err != nil {
fail(w, 500, 50001, "搜索失败")
return
}
rows, err := a.db.QueryContext(r.Context(), `SELECT u.id FROM users u JOIN user_profiles p ON p.user_id=u.id JOIN user_privacy_settings privacy ON privacy.user_id=u.id WHERE u.status=1 AND u.id<>? AND privacy.allow_search=1 AND (p.nickname LIKE ? OR u.public_id LIKE ?) AND NOT EXISTS(SELECT 1 FROM user_blocks b WHERE (b.user_id=? AND b.blocked_user_id=u.id) OR (b.user_id=u.id AND b.blocked_user_id=?)) ORDER BY p.is_vip DESC,p.last_active_at DESC LIMIT ? OFFSET ?`, current(r).ID, pattern, pattern, current(r).ID, current(r).ID, pageSize, offset)
if err != nil {
fail(w, 500, 50001, "搜索失败")
return
}
defer rows.Close()
items := []profileView{}
for rows.Next() {
var id int64
_ = rows.Scan(&id)
if item, loadErr := a.loadProfile(r, id, current(r).ID); loadErr == nil {
item.AvatarThumbnail = avatarThumbnailURL(item.Avatar)
items = append(items, item)
}
}
reply(w, map[string]any{"items": items, "total": total, "keyword": keyword, "page": page, "pageSize": pageSize, "hasMore": offset+len(items) < total})
}
func (a *App) followingList(w http.ResponseWriter, r *http.Request) {
a.relationshipList(w, r, "following")
}
func (a *App) followerList(w http.ResponseWriter, r *http.Request) {
a.relationshipList(w, r, "followers")
}
func (a *App) visitorList(w http.ResponseWriter, r *http.Request) {
if !a.resolveMembershipEntitlements(r.Context(), current(r).ID).CanViewVisitors {
fail(w, http.StatusForbidden, 10006, "开通会员后可查看访客记录")
return
}
a.relationshipList(w, r, "visitors")
}
func (a *App) relationshipList(w http.ResponseWriter, r *http.Request, listType string) {
var rows *sql.Rows
var err error
page, pageSize, offset := pageOptions(r)
var total int
switch listType {
case "following":
_ = a.db.QueryRowContext(r.Context(), `SELECT COUNT(*) FROM user_follows WHERE user_id=?`, current(r).ID).Scan(&total)
rows, err = a.db.QueryContext(r.Context(), `SELECT target_user_id FROM user_follows WHERE user_id=? ORDER BY created_at DESC LIMIT ? OFFSET ?`, current(r).ID, pageSize, offset)
case "followers":
_ = a.db.QueryRowContext(r.Context(), `SELECT COUNT(*) FROM user_follows WHERE target_user_id=?`, current(r).ID).Scan(&total)
rows, err = a.db.QueryContext(r.Context(), `SELECT user_id FROM user_follows WHERE target_user_id=? ORDER BY created_at DESC LIMIT ? OFFSET ?`, current(r).ID, pageSize, offset)
default:
_ = a.db.QueryRowContext(r.Context(), `SELECT COUNT(DISTINCT viewer_user_id) FROM profile_visits WHERE target_user_id=?`, current(r).ID).Scan(&total)
rows, err = a.db.QueryContext(r.Context(), `SELECT viewer_user_id FROM profile_visits WHERE target_user_id=? GROUP BY viewer_user_id ORDER BY MAX(visited_at) DESC LIMIT ? OFFSET ?`, current(r).ID, pageSize, offset)
}
if err != nil {
fail(w, 500, 50001, "查询失败")
return
}
defer rows.Close()
items := []profileView{}
for rows.Next() {
var id int64
_ = rows.Scan(&id)
if item, loadErr := a.loadProfile(r, id, current(r).ID); loadErr == nil {
item.AvatarThumbnail = avatarThumbnailURL(item.Avatar)
items = append(items, item)
}
}
reply(w, map[string]any{"items": items, "total": total, "type": listType, "page": page, "pageSize": pageSize, "hasMore": offset+len(items) < total})
}
type privacyView struct {
NearbyVisible bool `json:"nearbyVisible"`
DistanceVisible bool `json:"distanceVisible"`
OnlineVisible bool `json:"onlineVisible"`
LastActiveVisible bool `json:"lastActiveVisible"`
AllowStrangerMessage bool `json:"allowStrangerMessage"`
AllowProfileVisitRecord bool `json:"allowProfileVisitRecord"`
InvisibleVisit bool `json:"invisibleVisit"`
AllowSearch bool `json:"allowSearch"`
}
func (a *App) getPrivacy(w http.ResponseWriter, r *http.Request) {
var values [8]int
err := a.db.QueryRowContext(r.Context(), `SELECT nearby_visible,distance_visible,online_visible,last_active_visible,allow_stranger_message,allow_profile_visit_record,invisible_visit,allow_search FROM user_privacy_settings WHERE user_id=?`, current(r).ID).Scan(&values[0], &values[1], &values[2], &values[3], &values[4], &values[5], &values[6], &values[7])
if err != nil {
fail(w, 500, 50001, "查询失败")
return
}
reply(w, privacyView{NearbyVisible: values[0] == 1, DistanceVisible: values[1] == 1, OnlineVisible: values[2] == 1, LastActiveVisible: values[3] == 1, AllowStrangerMessage: values[4] == 1, AllowProfileVisitRecord: values[5] == 1, InvisibleVisit: values[6] == 1, AllowSearch: values[7] == 1})
}
func (a *App) updatePrivacy(w http.ResponseWriter, r *http.Request) {
var req privacyView
if decode(r, &req) != nil {
fail(w, 400, 20001, "隐私设置格式错误")
return
}
if req.InvisibleVisit && !a.resolveMembershipEntitlements(r.Context(), current(r).ID).CanInvisibleVisit {
fail(w, http.StatusForbidden, 10006, "当前会员等级不支持隐身访问")
return
}
_, err := a.db.ExecContext(r.Context(), `UPDATE user_privacy_settings SET nearby_visible=?,distance_visible=?,online_visible=?,last_active_visible=?,allow_stranger_message=?,allow_profile_visit_record=?,invisible_visit=?,allow_search=? WHERE user_id=?`, req.NearbyVisible, req.DistanceVisible, req.OnlineVisible, req.LastActiveVisible, req.AllowStrangerMessage, req.AllowProfileVisitRecord, req.InvisibleVisit, req.AllowSearch, current(r).ID)
if err != nil {
fail(w, 500, 50001, "保存失败")
return
}
reply(w, req)
}
func (a *App) blockUser(w http.ResponseWriter, r *http.Request) {
target, err := pathID(r)
if err != nil || target == current(r).ID {
fail(w, 400, 20001, "无效用户")
return
}
var targetExists int
if err = a.db.QueryRowContext(r.Context(), `SELECT EXISTS(SELECT 1 FROM users WHERE id=? AND status=1 AND deleted_at IS NULL)`, target).Scan(&targetExists); err != nil || targetExists != 1 {
fail(w, http.StatusNotFound, 30001, "用户不存在")
return
}
tx, err := a.db.BeginTx(r.Context(), nil)
if err != nil {
fail(w, 500, 50001, "拉黑失败")
return
}
defer tx.Rollback()
_, err = tx.ExecContext(r.Context(), `INSERT IGNORE INTO user_blocks(user_id,blocked_user_id,reason)VALUES(?,?,'user_action')`, current(r).ID, target)
if err == nil {
_, err = tx.ExecContext(r.Context(), `DELETE FROM user_follows WHERE (user_id=? AND target_user_id=?) OR (user_id=? AND target_user_id=?)`, current(r).ID, target, target, current(r).ID)
}
if err == nil {
_, err = tx.ExecContext(r.Context(), `DELETE FROM user_likes WHERE (user_id=? AND target_user_id=?) OR (user_id=? AND target_user_id=?)`, current(r).ID, target, target, current(r).ID)
}
if err == nil {
err = tx.Commit()
}
if err != nil {
fail(w, 500, 50001, "拉黑失败")
return
}
reply(w, map[string]bool{"success": true})
}
func (a *App) unblockUser(w http.ResponseWriter, r *http.Request) {
target, err := pathID(r)
if err != nil {
fail(w, 400, 20001, "无效用户")
return
}
_, err = a.db.ExecContext(r.Context(), `DELETE FROM user_blocks WHERE user_id=? AND blocked_user_id=?`, current(r).ID, target)
if err != nil {
fail(w, 500, 50001, "解除拉黑失败")
return
}
reply(w, map[string]bool{"success": true})
}
func (a *App) loadPost(r *http.Request, id int64) (postView, error) {
var item postView
var userID int64
var vip int
err := a.db.QueryRowContext(r.Context(), `SELECT p.id,p.user_id,p.content,p.location_text,p.like_count,p.comment_count,p.created_at,u.public_id,pr.nickname,pr.avatar_url,pr.gender,pr.is_vip,EXISTS(SELECT 1 FROM post_likes l WHERE l.post_id=p.id AND l.user_id=?) FROM posts p JOIN users u ON u.id=p.user_id JOIN user_profiles pr ON pr.user_id=p.user_id WHERE p.id=? AND p.status=1 AND (p.visibility=1 OR p.user_id=? OR (p.visibility=2 AND EXISTS(SELECT 1 FROM user_follows audience WHERE audience.user_id=? AND audience.target_user_id=p.user_id)))`, current(r).ID, id, current(r).ID, current(r).ID).Scan(&item.ID, &userID, &item.Content, &item.Location, &item.LikeCount, &item.CommentCount, &item.CreatedAt, &item.User.PublicID, &item.User.Nickname, &item.User.Avatar, &item.User.Gender, &vip, &item.Liked)
if err != nil {
return item, err
}
item.User.ID = userID
item.User.VIP = vip == 1
item.User.AvatarThumbnail = avatarThumbnailURL(item.User.Avatar)
item.CanEdit = userID == current(r).ID
item.CanDelete = userID == current(r).ID
item.Media = []string{}
item.MediaThumbnails = []string{}
rows, _ := a.db.QueryContext(r.Context(), `SELECT media_url FROM post_media WHERE post_id=? ORDER BY sort_order`, id)
if rows != nil {
defer rows.Close()
for rows.Next() {
var media string
_ = rows.Scan(&media)
item.Media = append(item.Media, media)
item.MediaThumbnails = append(item.MediaThumbnails, mediaThumbnailURL(media))
}
}
return item, nil
}
func (a *App) postDetail(w http.ResponseWriter, r *http.Request) {
id, err := pathID(r)
if err != nil {
fail(w, 400, 20001, "动态编号无效")
return
}
item, err := a.loadPost(r, id)
if err != nil {
fail(w, 404, 30001, "动态不存在")
return
}
reply(w, item)
}
func (a *App) userPosts(w http.ResponseWriter, r *http.Request) {
userID, err := pathID(r)
if err != nil {
fail(w, 400, 20001, "用户编号无效")
return
}
page, pageSize, offset := pageOptions(r)
var total int
if err := a.db.QueryRowContext(r.Context(), `SELECT COUNT(*) FROM posts p WHERE p.user_id=? AND p.status=1 AND (p.visibility=1 OR p.user_id=? OR (p.visibility=2 AND EXISTS(SELECT 1 FROM user_follows audience WHERE audience.user_id=? AND audience.target_user_id=p.user_id)))`, userID, current(r).ID, current(r).ID).Scan(&total); err != nil {
fail(w, 500, 50001, "查询失败")
return
}
rows, err := a.db.QueryContext(r.Context(), `SELECT p.id FROM posts p WHERE p.user_id=? AND p.status=1 AND (p.visibility=1 OR p.user_id=? OR (p.visibility=2 AND EXISTS(SELECT 1 FROM user_follows audience WHERE audience.user_id=? AND audience.target_user_id=p.user_id))) ORDER BY p.created_at DESC LIMIT ? OFFSET ?`, userID, current(r).ID, current(r).ID, pageSize, offset)
if err != nil {
fail(w, 500, 50001, "查询失败")
return
}
defer rows.Close()
ids := []int64{}
for rows.Next() {
var id int64
_ = rows.Scan(&id)
ids = append(ids, id)
}
items := []postView{}
for _, id := range ids {
if item, loadErr := a.loadPost(r, id); loadErr == nil {
items = append(items, item)
}
}
reply(w, map[string]any{"items": items, "total": total, "userId": userID, "page": page, "pageSize": pageSize, "hasMore": offset+len(items) < total})
}