gengx
This commit is contained in:
@@ -0,0 +1,399 @@
|
||||
package app
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"math"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
type profileView struct {
|
||||
ID int64 `json:"id"`
|
||||
PublicID string `json:"publicId"`
|
||||
Nickname string `json:"nickname"`
|
||||
Avatar string `json:"avatar"`
|
||||
Cover string `json:"cover"`
|
||||
Gender int `json:"gender"`
|
||||
Age int `json:"age"`
|
||||
Height int `json:"height"`
|
||||
City string `json:"city"`
|
||||
Bio string `json:"bio"`
|
||||
VIP bool `json:"vip"`
|
||||
VIPLevel int `json:"vipLevel"`
|
||||
Online bool `json:"online"`
|
||||
Distance float64 `json:"distance"`
|
||||
DistanceText string `json:"distanceText"`
|
||||
FollowingCount int `json:"followingCount"`
|
||||
FollowerCount int `json:"followerCount"`
|
||||
PostCount int `json:"postCount"`
|
||||
LikeCount int `json:"likeCount"`
|
||||
Following bool `json:"following"`
|
||||
Liked bool `json:"liked"`
|
||||
Tags []string `json:"tags"`
|
||||
}
|
||||
|
||||
func (a *App) loadProfile(r *http.Request, id, viewerID int64) (profileView, error) {
|
||||
var item profileView
|
||||
var birthday sql.NullTime
|
||||
var active sql.NullTime
|
||||
var vip int
|
||||
err := a.db.QueryRowContext(r.Context(), `SELECT u.id,u.public_id,p.nickname,p.avatar_url,p.cover_url,p.gender,p.birthday,COALESCE(p.height_cm,0),p.city_name,p.bio,p.is_vip,p.vip_level,p.last_active_at,
|
||||
(SELECT COUNT(*) FROM user_follows WHERE user_id=u.id),(SELECT COUNT(*) FROM user_follows WHERE target_user_id=u.id),(SELECT COUNT(*) FROM posts WHERE user_id=u.id AND status=1),(SELECT COUNT(*) FROM post_likes pl JOIN posts po ON po.id=pl.post_id WHERE po.user_id=u.id),
|
||||
EXISTS(SELECT 1 FROM user_follows WHERE user_id=? AND target_user_id=u.id),EXISTS(SELECT 1 FROM user_likes WHERE user_id=? AND target_user_id=u.id)
|
||||
FROM users u JOIN user_profiles p ON p.user_id=u.id WHERE u.id=? AND u.status=1`, viewerID, viewerID, id).Scan(
|
||||
&item.ID, &item.PublicID, &item.Nickname, &item.Avatar, &item.Cover, &item.Gender, &birthday, &item.Height, &item.City, &item.Bio, &vip, &item.VIPLevel, &active, &item.FollowingCount, &item.FollowerCount, &item.PostCount, &item.LikeCount, &item.Following, &item.Liked)
|
||||
if err != nil {
|
||||
return item, err
|
||||
}
|
||||
item.VIP = vip == 1
|
||||
if birthday.Valid {
|
||||
item.Age = age(birthday.Time)
|
||||
}
|
||||
item.Online = active.Valid && time.Since(active.Time) < 15*time.Minute
|
||||
item.Tags = []string{"摄影爱好者", "旅行达人", "天秤座"}
|
||||
return item, nil
|
||||
}
|
||||
|
||||
func (a *App) userProfile(w http.ResponseWriter, r *http.Request) {
|
||||
id, err := pathID(r)
|
||||
if err != nil {
|
||||
fail(w, 400, 20001, "invalid id")
|
||||
return
|
||||
}
|
||||
who := current(r)
|
||||
if id != who.ID {
|
||||
_, _ = a.db.ExecContext(r.Context(), `INSERT INTO profile_visits (viewer_user_id,target_user_id,source) VALUES (?,?,'profile')`, who.ID, id)
|
||||
}
|
||||
item, err := a.loadProfile(r, id, who.ID)
|
||||
if err != nil {
|
||||
fail(w, 404, 30001, "用户不存在")
|
||||
return
|
||||
}
|
||||
reply(w, item)
|
||||
}
|
||||
|
||||
func (a *App) discover(w http.ResponseWriter, r *http.Request) { a.discoverList(w, r, false) }
|
||||
func (a *App) nearby(w http.ResponseWriter, r *http.Request) { a.discoverList(w, r, true) }
|
||||
|
||||
func (a *App) discoverList(w http.ResponseWriter, r *http.Request, byDistance bool) {
|
||||
who := current(r)
|
||||
gender, _ := strconv.Atoi(r.URL.Query().Get("gender"))
|
||||
scope := r.URL.Query().Get("scope")
|
||||
sortMode := r.URL.Query().Get("sort")
|
||||
query := `SELECT u.id,u.public_id,p.nickname,p.avatar_url,p.cover_url,p.gender,p.birthday,COALESCE(p.height_cm,0),p.city_name,p.bio,p.is_vip,p.vip_level,p.last_active_at,l.latitude,l.longitude,
|
||||
EXISTS(SELECT 1 FROM user_follows f WHERE f.user_id=? AND f.target_user_id=u.id),EXISTS(SELECT 1 FROM user_likes x WHERE x.user_id=? AND x.target_user_id=u.id)
|
||||
FROM users u JOIN user_profiles p ON p.user_id=u.id LEFT JOIN user_location_states l ON l.user_id=u.id
|
||||
WHERE u.status=1 AND u.id<>? 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=?))`
|
||||
args := []any{who.ID, who.ID, who.ID, who.ID, who.ID}
|
||||
if scope == "following" {
|
||||
query += ` AND EXISTS(SELECT 1 FROM user_follows mine WHERE mine.user_id=? AND mine.target_user_id=u.id)`
|
||||
args = append(args, who.ID)
|
||||
}
|
||||
if gender > 0 {
|
||||
query += ` AND p.gender=?`
|
||||
args = append(args, gender)
|
||||
}
|
||||
if sortMode == "latest" {
|
||||
query += ` ORDER BY p.last_active_at DESC,u.id DESC LIMIT 50`
|
||||
} else {
|
||||
query += ` ORDER BY p.is_vip DESC,p.last_active_at DESC LIMIT 50`
|
||||
}
|
||||
rows, err := a.db.QueryContext(r.Context(), query, args...)
|
||||
if err != nil {
|
||||
fail(w, 500, 50001, err.Error())
|
||||
return
|
||||
}
|
||||
defer rows.Close()
|
||||
items := []profileView{}
|
||||
var myLat, myLng sql.NullFloat64
|
||||
_ = a.db.QueryRowContext(r.Context(), `SELECT latitude,longitude FROM user_location_states WHERE user_id=?`, who.ID).Scan(&myLat, &myLng)
|
||||
for rows.Next() {
|
||||
var item profileView
|
||||
var birthday sql.NullTime
|
||||
var active sql.NullTime
|
||||
var lat, lng sql.NullFloat64
|
||||
var vip int
|
||||
if err := rows.Scan(&item.ID, &item.PublicID, &item.Nickname, &item.Avatar, &item.Cover, &item.Gender, &birthday, &item.Height, &item.City, &item.Bio, &vip, &item.VIPLevel, &active, &lat, &lng, &item.Following, &item.Liked); err != nil {
|
||||
continue
|
||||
}
|
||||
item.VIP = vip == 1
|
||||
if birthday.Valid {
|
||||
item.Age = age(birthday.Time)
|
||||
}
|
||||
item.Online = active.Valid && time.Since(active.Time) < 15*time.Minute
|
||||
if myLat.Valid && myLng.Valid && lat.Valid && lng.Valid {
|
||||
item.Distance = haversine(myLat.Float64, myLng.Float64, lat.Float64, lng.Float64)
|
||||
item.DistanceText = distanceText(item.Distance)
|
||||
}
|
||||
item.Tags = []string{"摄影", "旅行"}
|
||||
items = append(items, item)
|
||||
}
|
||||
if byDistance {
|
||||
sortProfilesByDistance(items)
|
||||
}
|
||||
reply(w, map[string]any{"items": items, "total": len(items)})
|
||||
}
|
||||
|
||||
func (a *App) updateLocation(w http.ResponseWriter, r *http.Request) {
|
||||
var req struct {
|
||||
Latitude float64 `json:"latitude"`
|
||||
Longitude float64 `json:"longitude"`
|
||||
CityCode string `json:"cityCode"`
|
||||
}
|
||||
if err := decode(r, &req); err != nil || req.Latitude == 0 || req.Longitude == 0 {
|
||||
fail(w, 400, 20001, "无效的位置")
|
||||
return
|
||||
}
|
||||
if req.CityCode == "" {
|
||||
req.CityCode = "310100"
|
||||
}
|
||||
_, err := a.db.ExecContext(r.Context(), `INSERT INTO user_location_states (user_id,city_code,location_cell,latitude,longitude,source) VALUES (?,?, 'wx4g',?,?,'gps') ON DUPLICATE KEY UPDATE city_code=VALUES(city_code),latitude=VALUES(latitude),longitude=VALUES(longitude),last_location_at=NOW(3),source='gps'`, current(r).ID, req.CityCode, req.Latitude, req.Longitude)
|
||||
if err != nil {
|
||||
fail(w, 500, 50001, "位置更新失败")
|
||||
return
|
||||
}
|
||||
reply(w, map[string]bool{"success": true})
|
||||
}
|
||||
|
||||
func (a *App) follow(w http.ResponseWriter, r *http.Request) {
|
||||
a.relationship(w, r, "user_follows", true)
|
||||
}
|
||||
func (a *App) unfollow(w http.ResponseWriter, r *http.Request) {
|
||||
a.relationship(w, r, "user_follows", false)
|
||||
}
|
||||
func (a *App) likeUser(w http.ResponseWriter, r *http.Request) {
|
||||
a.relationship(w, r, "user_likes", true)
|
||||
}
|
||||
func (a *App) unlikeUser(w http.ResponseWriter, r *http.Request) {
|
||||
a.relationship(w, r, "user_likes", false)
|
||||
}
|
||||
|
||||
func (a *App) relationship(w http.ResponseWriter, r *http.Request, table string, create bool) {
|
||||
target, err := pathID(r)
|
||||
if err != nil || target == current(r).ID {
|
||||
fail(w, 400, 20001, "无效用户")
|
||||
return
|
||||
}
|
||||
if create {
|
||||
if table == "user_likes" {
|
||||
_, err = a.db.ExecContext(r.Context(), `INSERT IGNORE INTO user_likes (user_id,target_user_id,source) VALUES (?,?,'profile')`, current(r).ID, target)
|
||||
} else {
|
||||
_, err = a.db.ExecContext(r.Context(), `INSERT IGNORE INTO user_follows (user_id,target_user_id) VALUES (?,?)`, current(r).ID, target)
|
||||
}
|
||||
} else {
|
||||
_, err = a.db.ExecContext(r.Context(), `DELETE FROM `+table+` WHERE user_id=? AND target_user_id=?`, current(r).ID, target)
|
||||
}
|
||||
if err != nil {
|
||||
fail(w, 500, 50001, "操作失败")
|
||||
return
|
||||
}
|
||||
reply(w, map[string]bool{"success": true})
|
||||
}
|
||||
|
||||
type postView struct {
|
||||
ID int64 `json:"id"`
|
||||
User profileView `json:"user"`
|
||||
Content string `json:"content"`
|
||||
Location string `json:"location"`
|
||||
LikeCount int `json:"likeCount"`
|
||||
CommentCount int `json:"commentCount"`
|
||||
Liked bool `json:"liked"`
|
||||
Media []string `json:"media"`
|
||||
CreatedAt time.Time `json:"createdAt"`
|
||||
}
|
||||
|
||||
func (a *App) feed(w http.ResponseWriter, r *http.Request) {
|
||||
who := current(r)
|
||||
query := `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.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)))`
|
||||
args := []any{who.ID, who.ID, who.ID}
|
||||
if r.URL.Query().Get("scope") == "following" {
|
||||
query += ` AND EXISTS(SELECT 1 FROM user_follows f WHERE f.user_id=? AND f.target_user_id=p.user_id)`
|
||||
args = append(args, who.ID)
|
||||
}
|
||||
query += ` ORDER BY p.created_at DESC LIMIT 50`
|
||||
rows, err := a.db.QueryContext(r.Context(), query, args...)
|
||||
if err != nil {
|
||||
fail(w, 500, 50001, err.Error())
|
||||
return
|
||||
}
|
||||
defer rows.Close()
|
||||
items := []postView{}
|
||||
for rows.Next() {
|
||||
var item postView
|
||||
var uid int64
|
||||
var vip int
|
||||
if rows.Scan(&item.ID, &uid, &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) != nil {
|
||||
continue
|
||||
}
|
||||
item.User.ID = uid
|
||||
item.User.VIP = vip == 1
|
||||
mediaRows, _ := a.db.QueryContext(r.Context(), `SELECT media_url FROM post_media WHERE post_id=? ORDER BY sort_order`, item.ID)
|
||||
item.Media = []string{}
|
||||
if mediaRows != nil {
|
||||
for mediaRows.Next() {
|
||||
var url string
|
||||
_ = mediaRows.Scan(&url)
|
||||
item.Media = append(item.Media, url)
|
||||
}
|
||||
_ = mediaRows.Close()
|
||||
}
|
||||
items = append(items, item)
|
||||
}
|
||||
reply(w, map[string]any{"items": items, "total": len(items)})
|
||||
}
|
||||
|
||||
func (a *App) createPost(w http.ResponseWriter, r *http.Request) {
|
||||
if a.isSanctionActive(r.Context(), current(r).ID, "CONTENT_LIMIT") {
|
||||
fail(w, http.StatusForbidden, 10006, "账号处于内容发布限制期")
|
||||
return
|
||||
}
|
||||
var req struct {
|
||||
Content string `json:"content"`
|
||||
Media []string `json:"media"`
|
||||
Location string `json:"location"`
|
||||
Visibility int `json:"visibility"`
|
||||
}
|
||||
if decode(r, &req) != nil || strings.TrimSpace(req.Content) == "" {
|
||||
fail(w, 400, 20001, "请输入动态内容")
|
||||
return
|
||||
}
|
||||
if req.Visibility != 2 {
|
||||
req.Visibility = 1
|
||||
}
|
||||
tx, _ := a.db.BeginTx(r.Context(), nil)
|
||||
result, err := tx.ExecContext(r.Context(), `INSERT INTO posts (user_id,content,visibility,city_code,location_text) VALUES (?,?,?,'310100',?)`, current(r).ID, req.Content, req.Visibility, req.Location)
|
||||
if err != nil {
|
||||
_ = tx.Rollback()
|
||||
fail(w, 500, 50001, "发布失败")
|
||||
return
|
||||
}
|
||||
id, _ := result.LastInsertId()
|
||||
for i, url := range req.Media {
|
||||
_, _ = tx.ExecContext(r.Context(), `INSERT INTO post_media(post_id,media_url,sort_order)VALUES(?,?,?)`, id, url, i)
|
||||
}
|
||||
_ = tx.Commit()
|
||||
reply(w, map[string]any{"id": id})
|
||||
}
|
||||
|
||||
func (a *App) likePost(w http.ResponseWriter, r *http.Request) { a.postLike(w, r, true) }
|
||||
func (a *App) unlikePost(w http.ResponseWriter, r *http.Request) { a.postLike(w, r, false) }
|
||||
func (a *App) postLike(w http.ResponseWriter, r *http.Request, create bool) {
|
||||
id, err := pathID(r)
|
||||
if err != nil {
|
||||
fail(w, 400, 20001, "invalid id")
|
||||
return
|
||||
}
|
||||
tx, _ := a.db.BeginTx(r.Context(), nil)
|
||||
if create {
|
||||
result, _ := tx.ExecContext(r.Context(), `INSERT IGNORE INTO post_likes(post_id,user_id)VALUES(?,?)`, id, current(r).ID)
|
||||
affected, _ := result.RowsAffected()
|
||||
if affected > 0 {
|
||||
_, _ = tx.ExecContext(r.Context(), `UPDATE posts SET like_count=like_count+1 WHERE id=?`, id)
|
||||
}
|
||||
} else {
|
||||
result, _ := tx.ExecContext(r.Context(), `DELETE FROM post_likes WHERE post_id=? AND user_id=?`, id, current(r).ID)
|
||||
affected, _ := result.RowsAffected()
|
||||
if affected > 0 {
|
||||
_, _ = tx.ExecContext(r.Context(), `UPDATE posts SET like_count=GREATEST(like_count-1,0) WHERE id=?`, id)
|
||||
}
|
||||
}
|
||||
_ = tx.Commit()
|
||||
reply(w, map[string]bool{"success": true})
|
||||
}
|
||||
|
||||
func (a *App) comments(w http.ResponseWriter, r *http.Request) {
|
||||
id, _ := pathID(r)
|
||||
rows, err := a.db.QueryContext(r.Context(), `SELECT c.id,c.content,c.created_at,p.user_id,p.nickname,p.avatar_url FROM post_comments c JOIN user_profiles p ON p.user_id=c.user_id WHERE c.post_id=? AND c.status=1 ORDER BY c.created_at`, id)
|
||||
if err != nil {
|
||||
fail(w, 500, 50001, "查询失败")
|
||||
return
|
||||
}
|
||||
defer rows.Close()
|
||||
items := []map[string]any{}
|
||||
for rows.Next() {
|
||||
var cid, uid int64
|
||||
var content, nick, avatar string
|
||||
var created time.Time
|
||||
_ = rows.Scan(&cid, &content, &created, &uid, &nick, &avatar)
|
||||
items = append(items, map[string]any{"id": cid, "content": content, "createdAt": created, "user": map[string]any{"id": uid, "nickname": nick, "avatar": avatar}})
|
||||
}
|
||||
reply(w, map[string]any{"items": items})
|
||||
}
|
||||
|
||||
func (a *App) createComment(w http.ResponseWriter, r *http.Request) {
|
||||
id, _ := pathID(r)
|
||||
var req struct {
|
||||
Content string `json:"content"`
|
||||
}
|
||||
if decode(r, &req) != nil || req.Content == "" {
|
||||
fail(w, 400, 20001, "评论不能为空")
|
||||
return
|
||||
}
|
||||
tx, _ := a.db.BeginTx(r.Context(), nil)
|
||||
result, err := tx.ExecContext(r.Context(), `INSERT INTO post_comments(post_id,user_id,content)VALUES(?,?,?)`, id, current(r).ID, req.Content)
|
||||
if err != nil {
|
||||
_ = tx.Rollback()
|
||||
fail(w, 500, 50001, "评论失败")
|
||||
return
|
||||
}
|
||||
_, _ = tx.ExecContext(r.Context(), `UPDATE posts SET comment_count=comment_count+1 WHERE id=?`, id)
|
||||
_ = tx.Commit()
|
||||
cid, _ := result.LastInsertId()
|
||||
reply(w, map[string]any{"id": cid})
|
||||
}
|
||||
|
||||
func (a *App) createReport(w http.ResponseWriter, r *http.Request) {
|
||||
var req struct {
|
||||
TargetType string `json:"targetType"`
|
||||
TargetID int64 `json:"targetId"`
|
||||
Reason string `json:"reason"`
|
||||
Description string `json:"description"`
|
||||
}
|
||||
if decode(r, &req) != nil || req.TargetID == 0 {
|
||||
fail(w, 400, 20001, "举报信息不完整")
|
||||
return
|
||||
}
|
||||
result, err := a.db.ExecContext(r.Context(), `INSERT INTO reports(reporter_user_id,target_type,target_id,reason_code,description)VALUES(?,?,?,?,?)`, current(r).ID, req.TargetType, req.TargetID, req.Reason, req.Description)
|
||||
if err != nil {
|
||||
fail(w, 500, 50001, "提交失败")
|
||||
return
|
||||
}
|
||||
id, _ := result.LastInsertId()
|
||||
reply(w, map[string]any{"id": id, "status": "PENDING"})
|
||||
}
|
||||
|
||||
func age(birthday time.Time) int {
|
||||
now := time.Now()
|
||||
years := now.Year() - birthday.Year()
|
||||
if now.YearDay() < birthday.YearDay() {
|
||||
years--
|
||||
}
|
||||
return years
|
||||
}
|
||||
func haversine(lat1, lng1, lat2, lng2 float64) float64 {
|
||||
const earth = 6371
|
||||
dlat := (lat2 - lat1) * math.Pi / 180
|
||||
dlng := (lng2 - lng1) * math.Pi / 180
|
||||
a := math.Sin(dlat/2)*math.Sin(dlat/2) + math.Cos(lat1*math.Pi/180)*math.Cos(lat2*math.Pi/180)*math.Sin(dlng/2)*math.Sin(dlng/2)
|
||||
return earth * 2 * math.Atan2(math.Sqrt(a), math.Sqrt(1-a))
|
||||
}
|
||||
func distanceText(value float64) string {
|
||||
if value < 1 {
|
||||
return fmt.Sprintf("%.2fkm", value)
|
||||
}
|
||||
return fmt.Sprintf("%.1fkm", value)
|
||||
}
|
||||
func sortProfilesByDistance(items []profileView) {
|
||||
for i := 0; i < len(items); i++ {
|
||||
for j := i + 1; j < len(items); j++ {
|
||||
if items[j].Distance < items[i].Distance {
|
||||
items[i], items[j] = items[j], items[i]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
func jsonBytes(value any) []byte { data, _ := json.Marshal(value); return data }
|
||||
Reference in New Issue
Block a user