0904
This commit is contained in:
@@ -0,0 +1,480 @@
|
||||
// Copyright 2022 E99p1ant. All rights reserved.
|
||||
// Use of this source code is governed by a MIT-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package user
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/url"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/sirupsen/logrus"
|
||||
|
||||
"github.com/tamakyi/TamaBox/internal/context"
|
||||
"github.com/tamakyi/TamaBox/internal/db"
|
||||
"github.com/tamakyi/TamaBox/internal/dbutil"
|
||||
)
|
||||
|
||||
type conversationDetail struct {
|
||||
Conversation *db.Conversation
|
||||
OwnerDomain string
|
||||
OwnerName string
|
||||
FirstMessage string
|
||||
IsBlocked bool
|
||||
}
|
||||
|
||||
type receivedItem struct {
|
||||
Type string // "question_public", "question_private", "chat"
|
||||
CreatedAt time.Time
|
||||
Content string
|
||||
Link string
|
||||
IsAnswered bool
|
||||
IsRead bool // 仅对普通提问有效:有效已读 = is_read=true 或已回答
|
||||
IsPrivate bool // 仅对普通提问有效
|
||||
IsBlocked bool // 仅对私聊有效
|
||||
QuestionID uint // 仅对普通提问有效,用于置顶操作
|
||||
IsPinned bool // 仅对普通提问有效
|
||||
PinLink string // 仅对普通提问有效:置顶/取消置顶操作链接
|
||||
}
|
||||
|
||||
// QuestionList 用户后台提问列表(支持筛选与全文搜索)
|
||||
func QuestionList(ctx context.Context) {
|
||||
// 获取筛选参数
|
||||
sortParam := ctx.Query("sort")
|
||||
if sortParam != "latest" && sortParam != "oldest" {
|
||||
sortParam = "latest"
|
||||
}
|
||||
statusParam := ctx.Query("status")
|
||||
if statusParam != "all" && statusParam != "answered" && statusParam != "unanswered" {
|
||||
statusParam = "all"
|
||||
}
|
||||
typeParam := ctx.Query("type")
|
||||
// 私聊已独立成"私聊"标签页(见 ReceivedChats),不再参与提问列表的类型筛选;
|
||||
// 旧的 type=chat 链接统一归一为"全部",避免空列表。
|
||||
if typeParam != "all" && typeParam != "public" && typeParam != "private_question" {
|
||||
typeParam = "all"
|
||||
}
|
||||
readParam := ctx.Query("read")
|
||||
if readParam != "all" && readParam != "read" && readParam != "unread" {
|
||||
readParam = "all"
|
||||
}
|
||||
keyword := strings.TrimSpace(ctx.Query("q"))
|
||||
|
||||
// 映射排序方向
|
||||
sortOrder := "DESC"
|
||||
if sortParam == "oldest" {
|
||||
sortOrder = "ASC"
|
||||
}
|
||||
|
||||
// 映射回答状态
|
||||
answeredStatus := 0 // 全部
|
||||
if statusParam == "answered" {
|
||||
answeredStatus = 1
|
||||
} else if statusParam == "unanswered" {
|
||||
answeredStatus = 2
|
||||
}
|
||||
|
||||
// 映射阅读状态
|
||||
readStatus := 0 // 全部
|
||||
if readParam == "read" {
|
||||
readStatus = 1
|
||||
} else if readParam == "unread" {
|
||||
readStatus = 2
|
||||
}
|
||||
|
||||
// 管理后台进入问题详情的链接:携带 from=admin 及当前筛选参数,
|
||||
// 使详情页底部「返回」按钮跳回带筛选条件的后台列表(保留 sort/status/type/read/q)。
|
||||
adminLinkQuery := url.Values{}
|
||||
adminLinkQuery.Set("from", "admin")
|
||||
if sortParam != "latest" {
|
||||
adminLinkQuery.Set("sort", sortParam)
|
||||
}
|
||||
if statusParam != "all" {
|
||||
adminLinkQuery.Set("status", statusParam)
|
||||
}
|
||||
if typeParam != "all" {
|
||||
adminLinkQuery.Set("type", typeParam)
|
||||
}
|
||||
if readParam != "all" {
|
||||
adminLinkQuery.Set("read", readParam)
|
||||
}
|
||||
if keyword != "" {
|
||||
adminLinkQuery.Set("q", keyword)
|
||||
}
|
||||
adminLinkQueryStr := adminLinkQuery.Encode()
|
||||
|
||||
var receivedItems []receivedItem
|
||||
|
||||
// 1. 获取普通提问
|
||||
var questions []*db.Question
|
||||
if keyword != "" {
|
||||
results, err := db.Questions.SearchByUserID(ctx.Request().Context(), ctx.User.ID, keyword)
|
||||
if err != nil {
|
||||
logrus.WithContext(ctx.Request().Context()).WithError(err).Error("Failed to search questions")
|
||||
} else {
|
||||
questions = results
|
||||
}
|
||||
} else if typeParam == "all" || typeParam == "public" || typeParam == "private_question" {
|
||||
privateStatus := 0 // 全部
|
||||
if typeParam == "public" {
|
||||
privateStatus = 1 // 仅公开
|
||||
} else if typeParam == "private_question" {
|
||||
privateStatus = 2 // 仅私密
|
||||
}
|
||||
|
||||
// 游标分页:每页固定大小,游标为上一页最后一条提问 ID(默认按创建时间倒序,
|
||||
// 与 db.GetQuestionsByUserID 的 id < cursor 游标一致)。避免全量返回。
|
||||
questionsPageSize := 50
|
||||
qCursor := parseCursorUint(ctx.Query("q_cursor"))
|
||||
qOpts := db.GetQuestionsByUserIDOptions{
|
||||
AnsweredStatus: answeredStatus,
|
||||
PrivateStatus: privateStatus,
|
||||
// PrivateStatus=0("全部")时必须显式声明含私密:store 层的旧调用兼容分支
|
||||
// 会把 "ShowPrivate=false 且未指定 PrivateStatus" 当作仅公开过滤,
|
||||
// 导致私密提问在"全部"类型下不可见(与统计看板/内存过滤逻辑不一致)。
|
||||
ShowPrivate: true,
|
||||
ReadStatus: readStatus,
|
||||
SortOrder: sortOrder,
|
||||
Cursor: &dbutil.Cursor{PageSize: questionsPageSize},
|
||||
}
|
||||
if qCursor > 0 {
|
||||
qOpts.Cursor.Value = qCursor
|
||||
}
|
||||
|
||||
var qs []*db.Question
|
||||
var qErr error
|
||||
qs, qErr = db.Questions.GetByUserID(ctx.Request().Context(), ctx.User.ID, qOpts)
|
||||
if qErr != nil {
|
||||
logrus.WithContext(ctx.Request().Context()).WithError(qErr).Error("Failed to get questions by user ID")
|
||||
} else {
|
||||
questions = qs
|
||||
}
|
||||
// 提问"加载更多"(仅非搜索路径支持游标分页):满页且仍有更多时暴露下一页游标
|
||||
if qErr == nil && len(qs) >= questionsPageSize {
|
||||
lastQID := qs[len(qs)-1].ID
|
||||
ctx.Data["HasMoreQuestions"] = true
|
||||
ctx.Data["QuestionLoadMoreURL"] = buildQListPageURL(ctx.Request().URL.Query(), "q_cursor", strconv.FormatUint(uint64(lastQID), 10), "")
|
||||
} else {
|
||||
ctx.Data["HasMoreQuestions"] = false
|
||||
}
|
||||
}
|
||||
|
||||
// 搜索场景下关键词已在 SQL 中匹配,但类型/状态筛选需在此统一应用,
|
||||
// 使"搜索 + 筛选"可组合(完善筛选逻辑)。非搜索场景下此处与 SQL 过滤结果一致。
|
||||
for _, q := range questions {
|
||||
if answeredStatus == 1 && q.Answer == "" {
|
||||
continue
|
||||
}
|
||||
if answeredStatus == 2 && q.Answer != "" {
|
||||
continue
|
||||
}
|
||||
if typeParam == "public" && q.IsPrivate {
|
||||
continue
|
||||
}
|
||||
if typeParam == "private_question" && !q.IsPrivate {
|
||||
continue
|
||||
}
|
||||
// 有效已读 = 手动已读(is_read) 或 已回答(answer != "")
|
||||
isRead := q.IsRead || q.Answer != ""
|
||||
if readStatus == 1 && !isRead {
|
||||
continue
|
||||
}
|
||||
if readStatus == 2 && isRead {
|
||||
continue
|
||||
}
|
||||
itemType := "question_public"
|
||||
if q.IsPrivate {
|
||||
itemType = "question_private"
|
||||
}
|
||||
receivedItems = append(receivedItems, receivedItem{
|
||||
Type: itemType,
|
||||
CreatedAt: q.CreatedAt,
|
||||
Content: q.Content,
|
||||
Link: "/_/" + ctx.User.Domain + "/" + fmt.Sprint(q.ID) + "?" + adminLinkQueryStr,
|
||||
IsAnswered: q.Answer != "",
|
||||
IsRead: isRead,
|
||||
IsPrivate: q.IsPrivate,
|
||||
QuestionID: q.ID,
|
||||
IsPinned: q.IsPinned,
|
||||
PinLink: "/_/" + ctx.User.Domain + "/" + fmt.Sprint(q.ID) + "/pin",
|
||||
})
|
||||
}
|
||||
|
||||
// 2. 私聊会话:独立"私聊"标签页展示(ReceivedChats),不再混入提问列表。
|
||||
// 会话没有回答状态/阅读状态概念,不受提问的 sort/status/type/read/q 筛选影响。
|
||||
// 私聊列表分页(游标=offset),避免一次拉取全部会话造成内存/DB 压力。
|
||||
chatPageSize := 50
|
||||
chatOffset := parseCursorUint(ctx.Query("chat_cursor"))
|
||||
|
||||
var receivedChats []receivedItem
|
||||
convs, err := db.Conversations.ListByOwner(ctx.Request().Context(), ctx.User.ID, chatPageSize, int(chatOffset))
|
||||
if err != nil {
|
||||
logrus.WithContext(ctx.Request().Context()).WithError(err).Error("Failed to get owned conversations")
|
||||
} else if len(convs) > 0 {
|
||||
// 批量获取首条消息
|
||||
convIDs := make([]uint, len(convs))
|
||||
for i, c := range convs {
|
||||
convIDs[i] = c.ID
|
||||
}
|
||||
firstMsgMap, err := db.Messages.GetFirstMessagesByConversationIDs(ctx.Request().Context(), convIDs)
|
||||
if err != nil {
|
||||
logrus.WithContext(ctx.Request().Context()).WithError(err).Error("Failed to batch get first messages")
|
||||
firstMsgMap = make(map[uint]string)
|
||||
}
|
||||
|
||||
// 批量判断"我拉黑了哪些 asker",避免循环 IsUserBlocked 造成 N+1。
|
||||
askerIDs := make([]uint, 0, len(convs))
|
||||
for _, c := range convs {
|
||||
askerIDs = append(askerIDs, c.AskerID)
|
||||
}
|
||||
blockedMap, err := db.Users.BlockedByUser(ctx.Request().Context(), ctx.User.ID, askerIDs)
|
||||
if err != nil {
|
||||
logrus.WithContext(ctx.Request().Context()).WithError(err).Error("Failed to batch check blocked askers")
|
||||
blockedMap = make(map[uint]bool)
|
||||
}
|
||||
|
||||
for _, conv := range convs {
|
||||
receivedChats = append(receivedChats, receivedItem{
|
||||
Type: "chat",
|
||||
CreatedAt: conv.CreatedAt,
|
||||
Content: firstMsgMap[conv.ID],
|
||||
Link: "/_/" + ctx.User.Domain + "/chat/" + conv.Token,
|
||||
IsBlocked: blockedMap[conv.AskerID],
|
||||
})
|
||||
}
|
||||
}
|
||||
ctx.Data["ReceivedChats"] = receivedChats
|
||||
|
||||
// 私聊总数与"加载更多"判定(基于真实总数,而非当前页长度)
|
||||
chatTotalReceived, cntErr := db.Conversations.CountByOwner(ctx.Request().Context(), ctx.User.ID)
|
||||
if cntErr != nil {
|
||||
logrus.WithContext(ctx.Request().Context()).WithError(cntErr).Error("Failed to count owned conversations")
|
||||
chatTotalReceived = int64(len(convs))
|
||||
}
|
||||
hasMoreChats := chatTotalReceived > 0 && (chatOffset+uint(len(convs)) < uint(chatTotalReceived))
|
||||
if hasMoreChats {
|
||||
nextOffset := chatOffset + uint(len(convs))
|
||||
ctx.Data["ChatLoadMoreURL"] = buildQListPageURL(ctx.Request().URL.Query(), "chat_cursor", strconv.FormatUint(uint64(nextOffset), 10), "chats")
|
||||
}
|
||||
ctx.Data["HasMoreChats"] = hasMoreChats
|
||||
|
||||
// 排序(搜索时保留搜索结果的倒序,不混合排序)
|
||||
if keyword == "" {
|
||||
sort.Slice(receivedItems, func(i, j int) bool {
|
||||
if sortParam == "latest" {
|
||||
return receivedItems[i].CreatedAt.After(receivedItems[j].CreatedAt)
|
||||
}
|
||||
return receivedItems[i].CreatedAt.Before(receivedItems[j].CreatedAt)
|
||||
})
|
||||
}
|
||||
|
||||
// ---------- 提问箱统计看板 ----------
|
||||
// 独立统计(不受筛选影响),供管理页顶部展示。全部用 COUNT 查询,避免全量拉取问题记录。
|
||||
totalCount, _ := db.Questions.Count(ctx.Request().Context(), ctx.User.ID, db.GetQuestionsCountOptions{ShowPrivate: true})
|
||||
answeredCount, _ := db.Questions.Count(ctx.Request().Context(), ctx.User.ID, db.GetQuestionsCountOptions{FilterAnswered: true, ShowPrivate: true})
|
||||
unansweredCount := totalCount - answeredCount
|
||||
|
||||
isPrivateTrue := true
|
||||
privateCount, _ := db.Questions.Count(ctx.Request().Context(), ctx.User.ID, db.GetQuestionsCountOptions{ShowPrivate: true, IsPrivate: &isPrivateTrue})
|
||||
isPinnedTrue := true
|
||||
pinnedCount, _ := db.Questions.Count(ctx.Request().Context(), ctx.User.ID, db.GetQuestionsCountOptions{ShowPrivate: true, IsPinned: &isPinnedTrue})
|
||||
|
||||
// 阅读状态统计(不受其他筛选影响):已读 = is_read=true 或已回答;未读 = 其余。
|
||||
readCount, _ := db.Questions.Count(ctx.Request().Context(), ctx.User.ID, db.GetQuestionsCountOptions{ShowPrivate: true, ReadStatus: 1})
|
||||
unreadCount, _ := db.Questions.Count(ctx.Request().Context(), ctx.User.ID, db.GetQuestionsCountOptions{ShowPrivate: true, ReadStatus: 2})
|
||||
|
||||
ctx.Data["Stats"] = map[string]int64{
|
||||
"total": totalCount,
|
||||
"answered": answeredCount,
|
||||
"unanswered": unansweredCount,
|
||||
"read": readCount,
|
||||
"unread": unreadCount,
|
||||
"private": privateCount,
|
||||
"pinned": pinnedCount,
|
||||
}
|
||||
// 我创建的投票(提问箱「投票」面板用)—— 与投票管理列表共用富信息条目,
|
||||
// 含参与数、有效状态(draft/published/not_started/ended/archived)、剩余时间等。
|
||||
// 分页:每页固定大小,游标为上一页最后一条投票 ID(按创建时间倒序),避免全量返回。
|
||||
votesPageSize := 50
|
||||
voteCursor := parseCursorUint(ctx.Query("vote_cursor"))
|
||||
voteListOpts := db.ListVotesOptions{Cursor: &dbutil.Cursor{PageSize: votesPageSize}}
|
||||
if voteCursor > 0 {
|
||||
voteListOpts.Cursor.Value = voteCursor
|
||||
}
|
||||
myVotesRaw, err := db.Votes.ListByUserID(ctx.Request().Context(), ctx.User.ID, voteListOpts)
|
||||
if err != nil {
|
||||
logrus.WithContext(ctx.Request().Context()).WithError(err).Error("Failed to get my votes")
|
||||
myVotesRaw = []*db.Vote{}
|
||||
}
|
||||
myVotes := buildVoteItems(ctx, myVotesRaw)
|
||||
ctx.Data["MyVotes"] = myVotes
|
||||
|
||||
// 投票统计看板(不受筛选影响):总数(真实全量计数)+ 本页有效进行中数
|
||||
voteTotal, _ := db.Votes.CountByUserID(ctx.Request().Context(), ctx.User.ID)
|
||||
votePublished := int64(0)
|
||||
for _, it := range myVotes {
|
||||
if it.EffectiveStatus == db.VoteStatusPublished {
|
||||
votePublished++
|
||||
}
|
||||
}
|
||||
ctx.Data["VoteStats"] = map[string]int64{
|
||||
"total": voteTotal,
|
||||
"published": votePublished,
|
||||
}
|
||||
// 投票"加载更多":本页满页且仍有更多时暴露下一页游标(保留筛选参数并跳回投票面板)
|
||||
if len(myVotesRaw) >= votesPageSize {
|
||||
lastID := myVotesRaw[len(myVotesRaw)-1].ID
|
||||
ctx.Data["HasMoreVotes"] = true
|
||||
ctx.Data["VoteLoadMoreURL"] = buildQListPageURL(ctx.Request().URL.Query(), "vote_cursor", strconv.FormatUint(uint64(lastID), 10), "votes")
|
||||
} else {
|
||||
ctx.Data["HasMoreVotes"] = false
|
||||
}
|
||||
|
||||
ctx.Data["Keyword"] = keyword
|
||||
|
||||
ctx.Data["ReceivedItems"] = receivedItems
|
||||
ctx.Data["SelectedSort"] = sortParam
|
||||
ctx.Data["SelectedStatus"] = statusParam
|
||||
ctx.Data["SelectedRead"] = readParam
|
||||
ctx.Data["SelectedType"] = typeParam
|
||||
|
||||
// 我发起的私聊 — 同样使用批量查询与分页(复用 chat_cursor 游标)
|
||||
askerConvs, err := db.Conversations.ListByAsker(ctx.Request().Context(), ctx.User.ID, chatPageSize, int(chatOffset))
|
||||
if err != nil {
|
||||
logrus.WithContext(ctx.Request().Context()).WithError(err).Error("Failed to get asked conversations")
|
||||
askerConvs = []*db.Conversation{}
|
||||
}
|
||||
var askerDetails []conversationDetail
|
||||
if len(askerConvs) > 0 {
|
||||
askerConvIDs := make([]uint, len(askerConvs))
|
||||
for i, c := range askerConvs {
|
||||
askerConvIDs[i] = c.ID
|
||||
}
|
||||
askerFirstMsgMap, err := db.Messages.GetFirstMessagesByConversationIDs(ctx.Request().Context(), askerConvIDs)
|
||||
if err != nil {
|
||||
logrus.WithContext(ctx.Request().Context()).WithError(err).Error("Failed to batch get first messages for asker")
|
||||
askerFirstMsgMap = make(map[uint]string)
|
||||
}
|
||||
|
||||
// 批量查询所有 owner(去重),避免逐个 GetByID 造成 N+1;拉黑状态用批量关联表查询。
|
||||
ownerIDSet := make(map[uint]struct{})
|
||||
ownerIDs := make([]uint, 0, len(askerConvs))
|
||||
for _, conv := range askerConvs {
|
||||
if _, ok := ownerIDSet[conv.OwnerID]; !ok {
|
||||
ownerIDSet[conv.OwnerID] = struct{}{}
|
||||
ownerIDs = append(ownerIDs, conv.OwnerID)
|
||||
}
|
||||
}
|
||||
ownerMap := make(map[uint]*db.User)
|
||||
if owners, err := db.Users.GetByIDs(ctx.Request().Context(), ownerIDs); err == nil {
|
||||
for _, u := range owners {
|
||||
ownerMap[u.ID] = u
|
||||
}
|
||||
} else {
|
||||
logrus.WithContext(ctx.Request().Context()).WithError(err).Error("Failed to batch get owners")
|
||||
}
|
||||
|
||||
// 批量判断"哪些 owner 拉黑了我",避免循环查询。
|
||||
blockedByMap, err := db.Users.BlockedByAnyOf(ctx.Request().Context(), ownerIDs, ctx.User.ID)
|
||||
if err != nil {
|
||||
logrus.WithContext(ctx.Request().Context()).WithError(err).Error("Failed to batch check blocked by owners")
|
||||
blockedByMap = make(map[uint]bool)
|
||||
}
|
||||
|
||||
for _, conv := range askerConvs {
|
||||
firstMsg := askerFirstMsgMap[conv.ID]
|
||||
ownerDomain := "unknown"
|
||||
ownerName := "未知用户"
|
||||
if owner, ok := ownerMap[conv.OwnerID]; ok {
|
||||
ownerDomain = owner.Domain
|
||||
ownerName = owner.Name
|
||||
}
|
||||
askerDetails = append(askerDetails, conversationDetail{
|
||||
Conversation: conv,
|
||||
OwnerDomain: ownerDomain,
|
||||
OwnerName: ownerName,
|
||||
FirstMessage: firstMsg,
|
||||
IsBlocked: blockedByMap[conv.OwnerID],
|
||||
})
|
||||
}
|
||||
}
|
||||
ctx.Data["ConversationDetails"] = askerDetails
|
||||
|
||||
// 我发起的私聊总数与"加载更多"判定
|
||||
askerTotal, cntErr := db.Conversations.CountByAsker(ctx.Request().Context(), ctx.User.ID)
|
||||
if cntErr != nil {
|
||||
logrus.WithContext(ctx.Request().Context()).WithError(cntErr).Error("Failed to count asked conversations")
|
||||
askerTotal = int64(len(askerConvs))
|
||||
}
|
||||
hasMoreAsked := askerTotal > 0 && (chatOffset+uint(len(askerConvs)) < uint(askerTotal))
|
||||
if hasMoreAsked {
|
||||
nextOffset := chatOffset + uint(len(askerConvs))
|
||||
ctx.Data["AskedChatsLoadMoreURL"] = buildQListPageURL(ctx.Request().URL.Query(), "chat_cursor", strconv.FormatUint(uint64(nextOffset), 10), "chats")
|
||||
}
|
||||
ctx.Data["HasMoreAskedChats"] = hasMoreAsked
|
||||
|
||||
// ---------- 私聊统计看板 ----------
|
||||
// 与提问/投票看板一致,展示在「私聊」面板的顶部。全部基于已加载的会话切片计算,
|
||||
// 不触发额外数据库查询:总数 = 我收到的(作为箱主) + 我发起的(作为提问者);
|
||||
// 已关闭 = 两端会话中 is_closed=true 的计数。
|
||||
chatClosed := int64(0)
|
||||
for _, c := range convs {
|
||||
if c.IsClosed {
|
||||
chatClosed++
|
||||
}
|
||||
}
|
||||
for _, c := range askerConvs {
|
||||
if c.IsClosed {
|
||||
chatClosed++
|
||||
}
|
||||
}
|
||||
ctx.Data["ChatStats"] = map[string]int64{
|
||||
"total": chatTotalReceived + askerTotal,
|
||||
"received": chatTotalReceived,
|
||||
"initiated": askerTotal,
|
||||
"closed": chatClosed,
|
||||
}
|
||||
|
||||
ctx.Success("user/question-list")
|
||||
}
|
||||
|
||||
// parseCursorUint 将游标字符串安全解析为 uint;空或非法返回 0(表示从头分页)。
|
||||
func parseCursorUint(s string) uint {
|
||||
s = strings.TrimSpace(s)
|
||||
if s == "" {
|
||||
return 0
|
||||
}
|
||||
id, err := strconv.ParseUint(s, 10, 64)
|
||||
if err != nil || id == 0 {
|
||||
return 0
|
||||
}
|
||||
return uint(id)
|
||||
}
|
||||
|
||||
// buildQListPageURL 在保留当前筛选/搜索参数的基础上构造"加载更多"链接,
|
||||
// 并追加锚点(anchor 指定跳转到的面板;空串表示默认提问面板)。
|
||||
// 用于投票/提问列表的游标分页,避免丢失已选的 sort/status/type/q 等筛选条件。
|
||||
func buildQListPageURL(current url.Values, key, val, anchor string) string {
|
||||
q := url.Values{}
|
||||
for k, vs := range current {
|
||||
q[k] = vs
|
||||
}
|
||||
if val != "" {
|
||||
q.Set(key, val)
|
||||
} else {
|
||||
q.Del(key)
|
||||
}
|
||||
qs := q.Encode()
|
||||
path := "/user/questions"
|
||||
if qs != "" {
|
||||
path += "?" + qs
|
||||
}
|
||||
if anchor != "" {
|
||||
path += "#" + anchor
|
||||
}
|
||||
return path
|
||||
}
|
||||
Reference in New Issue
Block a user