feat:修改活动 TOP3 + 我的排名
This commit is contained in:
parent
a077fe2685
commit
b7e2aee399
@ -1106,3 +1106,108 @@ func convertContributionRankingResponse(resp *pbActivity.ContributionRankingResp
|
||||
"total": resp.Total,
|
||||
}
|
||||
}
|
||||
|
||||
// GetTopRanking 获取活动 TOP3 + 我的排名(专用轻量接口)
|
||||
// @Summary 获取活动 TOP3 + 我的排名
|
||||
// @Description 返回 top3 头像组 + my_info(rank/avatar/gap_to_prev/status),与 /ranking 不同:仅返回本组件所需字段,gap_to_prev 由后端计算
|
||||
// @Tags activities
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Security BearerAuth
|
||||
// @Param id path int64 true "活动ID"
|
||||
// @Param star_id query int64 false "粉丝身份ID(不传则用 token 中的)"
|
||||
// @Success 200 {object} response.Response
|
||||
// @Failure 400 {object} response.Response
|
||||
// @Failure 401 {object} response.Response
|
||||
// @Router /api/v1/activities/{id}/top-ranking [get]
|
||||
func (ctrl *ActivityController) GetTopRanking(c *gin.Context) {
|
||||
// 1) 鉴权
|
||||
userID, exists := c.Get("user_id")
|
||||
if !exists {
|
||||
response.Error(c, http.StatusUnauthorized, "未授权")
|
||||
return
|
||||
}
|
||||
|
||||
// 2) 解析路径参数 activity_id
|
||||
activityIDStr := c.Param("id")
|
||||
activityID, err := strconv.ParseInt(activityIDStr, 10, 64)
|
||||
if err != nil || activityID <= 0 {
|
||||
response.Error(c, http.StatusBadRequest, "活动ID参数错误")
|
||||
return
|
||||
}
|
||||
|
||||
// 3) star_id:query 优先,缺省用 token 中的;都没有按 0 处理
|
||||
reqStarID := int64(0)
|
||||
if v, ok := c.Get("star_id"); ok && v != nil {
|
||||
if sID, ok2 := v.(int64); ok2 {
|
||||
reqStarID = sID
|
||||
}
|
||||
}
|
||||
if qs := c.Query("star_id"); qs != "" {
|
||||
if v, perr := strconv.ParseInt(qs, 10, 64); perr == nil && v > 0 {
|
||||
reqStarID = v
|
||||
}
|
||||
}
|
||||
|
||||
logger.Logger.Info("GetTopRanking request",
|
||||
zap.Int64("user_id", userID.(int64)),
|
||||
zap.Int64("activity_id", activityID),
|
||||
zap.Int64("star_id", reqStarID),
|
||||
)
|
||||
|
||||
// 4) gRPC 调用
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||
defer cancel()
|
||||
|
||||
ctx = context.WithValue(ctx, constant.AttachmentKey, map[string]interface{}{
|
||||
"user_id": strconv.FormatInt(userID.(int64), 10),
|
||||
"star_id": strconv.FormatInt(reqStarID, 10),
|
||||
})
|
||||
|
||||
resp, err := ctrl.activityService.GetTopRanking(ctx, &pbActivity.TopRankingRequest{
|
||||
ActivityId: activityID,
|
||||
StarId: reqStarID,
|
||||
UserId: userID.(int64),
|
||||
})
|
||||
if err != nil {
|
||||
logger.Logger.Error("GetTopRanking RPC failed", zap.Error(err))
|
||||
response.Error(c, http.StatusInternalServerError, "获取 top-ranking 失败")
|
||||
return
|
||||
}
|
||||
|
||||
if resp.Base != nil && resp.Base.Code != uint32(codes.OK) {
|
||||
response.ErrorWithCode(c, int(resp.Base.Code), resp.Base.Message)
|
||||
return
|
||||
}
|
||||
|
||||
// 5) 转换并返回
|
||||
data := convertTopRankingResponse(resp)
|
||||
response.Success(c, data)
|
||||
}
|
||||
|
||||
// convertTopRankingResponse proto -> gin.H(供前端)
|
||||
func convertTopRankingResponse(resp *pbActivity.TopRankingResponse) map[string]interface{} {
|
||||
top3 := make([]map[string]interface{}, 0, len(resp.Top3))
|
||||
for _, it := range resp.Top3 {
|
||||
top3 = append(top3, map[string]interface{}{
|
||||
"rank": it.Rank,
|
||||
"user_id": it.UserId,
|
||||
"avatar_url": it.AvatarUrl,
|
||||
})
|
||||
}
|
||||
|
||||
var myInfo map[string]interface{}
|
||||
if resp.MyInfo != nil {
|
||||
myInfo = map[string]interface{}{
|
||||
"rank": resp.MyInfo.Rank,
|
||||
"avatar_url": resp.MyInfo.AvatarUrl,
|
||||
"gap_to_prev": resp.MyInfo.GapToPrev,
|
||||
"status": resp.MyInfo.Status,
|
||||
}
|
||||
}
|
||||
|
||||
return map[string]interface{}{
|
||||
"top3": top3,
|
||||
"my_info": myInfo,
|
||||
}
|
||||
}
|
||||
|
||||
@ -723,6 +723,106 @@ const docTemplate = `{
|
||||
}
|
||||
}
|
||||
},
|
||||
"/api/v1/activities/{activity_id}/messages": {
|
||||
"get": {
|
||||
"security": [
|
||||
{
|
||||
"BearerAuth": []
|
||||
}
|
||||
],
|
||||
"description": "分页获取活动留言列表(最新在上)",
|
||||
"consumes": [
|
||||
"application/json"
|
||||
],
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"activities"
|
||||
],
|
||||
"summary": "列出活动留言",
|
||||
"parameters": [
|
||||
{
|
||||
"type": "integer",
|
||||
"format": "int64",
|
||||
"description": "活动ID",
|
||||
"name": "activity_id",
|
||||
"in": "path",
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
"type": "integer",
|
||||
"description": "页码,默认1",
|
||||
"name": "page",
|
||||
"in": "query"
|
||||
},
|
||||
{
|
||||
"type": "integer",
|
||||
"description": "每页数量,默认20,最大50",
|
||||
"name": "page_size",
|
||||
"in": "query"
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "OK",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/response.Response"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"post": {
|
||||
"security": [
|
||||
{
|
||||
"BearerAuth": []
|
||||
}
|
||||
],
|
||||
"description": "用户在应援活动页面发送一条祝福留言",
|
||||
"consumes": [
|
||||
"application/json"
|
||||
],
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"activities"
|
||||
],
|
||||
"summary": "发送活动留言",
|
||||
"parameters": [
|
||||
{
|
||||
"type": "integer",
|
||||
"format": "int64",
|
||||
"description": "活动ID",
|
||||
"name": "activity_id",
|
||||
"in": "path",
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
"description": "留言内容",
|
||||
"name": "request",
|
||||
"in": "body",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"content": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "OK",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/response.Response"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/api/v1/activities/{activity_id}/progress": {
|
||||
"get": {
|
||||
"security": [
|
||||
@ -865,6 +965,97 @@ const docTemplate = `{
|
||||
}
|
||||
}
|
||||
},
|
||||
"/api/v1/activities/{id}/top-ranking": {
|
||||
"get": {
|
||||
"security": [
|
||||
{
|
||||
"BearerAuth": []
|
||||
}
|
||||
],
|
||||
"description": "返回 top3 头像组 + my_info(rank/avatar/gap_to_prev/status),与 /ranking 不同:仅返回本组件所需字段,gap_to_prev 由后端计算",
|
||||
"consumes": [
|
||||
"application/json"
|
||||
],
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"activities"
|
||||
],
|
||||
"summary": "获取活动 TOP3 + 我的排名",
|
||||
"parameters": [
|
||||
{
|
||||
"type": "integer",
|
||||
"format": "int64",
|
||||
"description": "活动ID",
|
||||
"name": "id",
|
||||
"in": "path",
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
"type": "integer",
|
||||
"format": "int64",
|
||||
"description": "粉丝身份ID(不传则用 token 中的)",
|
||||
"name": "star_id",
|
||||
"in": "query"
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "OK",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/response.Response"
|
||||
}
|
||||
},
|
||||
"400": {
|
||||
"description": "Bad Request",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/response.Response"
|
||||
}
|
||||
},
|
||||
"401": {
|
||||
"description": "Unauthorized",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/response.Response"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/api/v1/admin/notifications": {
|
||||
"post": {
|
||||
"description": "内部接口,无 JWT 鉴权,供 admin 后台(Python 8081)调用。user_ids 由调用方解析(查 fan_profiles)。",
|
||||
"consumes": [
|
||||
"application/json"
|
||||
],
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"admin"
|
||||
],
|
||||
"summary": "admin 批量发送通知",
|
||||
"parameters": [
|
||||
{
|
||||
"description": "通知 payload(已解析的 user_ids)",
|
||||
"name": "body",
|
||||
"in": "body",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"$ref": "#/definitions/controller.adminCreateNotificationRequest"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "OK",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/response.Response"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/api/v1/ai-chat/history/{sessionId}": {
|
||||
"get": {
|
||||
"security": [
|
||||
@ -3034,6 +3225,350 @@ const docTemplate = `{
|
||||
}
|
||||
}
|
||||
},
|
||||
"/api/v1/notifications": {
|
||||
"get": {
|
||||
"security": [
|
||||
{
|
||||
"BearerAuth": []
|
||||
}
|
||||
],
|
||||
"description": "获取当前用户的通知列表(支持 type/tab 分页)",
|
||||
"consumes": [
|
||||
"application/json"
|
||||
],
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"notifications"
|
||||
],
|
||||
"summary": "获取通知列表",
|
||||
"parameters": [
|
||||
{
|
||||
"type": "string",
|
||||
"description": "通知类型过滤: like / system / activity",
|
||||
"name": "type",
|
||||
"in": "query"
|
||||
},
|
||||
{
|
||||
"type": "string",
|
||||
"description": "列表 tab: unread / read / all",
|
||||
"name": "tab",
|
||||
"in": "query"
|
||||
},
|
||||
{
|
||||
"type": "integer",
|
||||
"description": "页码,默认1",
|
||||
"name": "page",
|
||||
"in": "query"
|
||||
},
|
||||
{
|
||||
"type": "integer",
|
||||
"description": "每页数量,默认20",
|
||||
"name": "page_size",
|
||||
"in": "query"
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "OK",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/response.Response"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/api/v1/notifications/devices": {
|
||||
"post": {
|
||||
"security": [
|
||||
{
|
||||
"BearerAuth": []
|
||||
}
|
||||
],
|
||||
"description": "将 uni.getPushClientId() 拿到的 cid 上报给后端;同 cid 重复注册为更新。",
|
||||
"consumes": [
|
||||
"application/json"
|
||||
],
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"notifications"
|
||||
],
|
||||
"summary": "注册推送设备",
|
||||
"parameters": [
|
||||
{
|
||||
"description": "设备信息",
|
||||
"name": "body",
|
||||
"in": "body",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"$ref": "#/definitions/controller.registerDeviceRequest"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "OK",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/response.Response"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/api/v1/notifications/devices/unregister": {
|
||||
"post": {
|
||||
"security": [
|
||||
{
|
||||
"BearerAuth": []
|
||||
}
|
||||
],
|
||||
"description": "注销推送 cid;cid 为空 = 注销当前用户全部设备(用于主动登出)。",
|
||||
"consumes": [
|
||||
"application/json"
|
||||
],
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"notifications"
|
||||
],
|
||||
"summary": "注销推送设备",
|
||||
"parameters": [
|
||||
{
|
||||
"description": "注销请求",
|
||||
"name": "body",
|
||||
"in": "body",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"$ref": "#/definitions/controller.unregisterDeviceRequest"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "OK",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/response.Response"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/api/v1/notifications/read-all": {
|
||||
"post": {
|
||||
"security": [
|
||||
{
|
||||
"BearerAuth": []
|
||||
}
|
||||
],
|
||||
"description": "将当前用户某类型或全部通知标记为已读",
|
||||
"consumes": [
|
||||
"application/json"
|
||||
],
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"notifications"
|
||||
],
|
||||
"summary": "全部已读",
|
||||
"parameters": [
|
||||
{
|
||||
"type": "string",
|
||||
"description": "通知类型过滤: like / system / activity; 留空表示全部",
|
||||
"name": "type",
|
||||
"in": "query"
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "OK",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/response.Response"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/api/v1/notifications/targets/{target_id}": {
|
||||
"delete": {
|
||||
"security": [
|
||||
{
|
||||
"BearerAuth": []
|
||||
}
|
||||
],
|
||||
"description": "删除同一 target 下的所有通知",
|
||||
"consumes": [
|
||||
"application/json"
|
||||
],
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"notifications"
|
||||
],
|
||||
"summary": "按目标ID删除通知",
|
||||
"parameters": [
|
||||
{
|
||||
"type": "integer",
|
||||
"description": "目标ID(如藏品ID)",
|
||||
"name": "target_id",
|
||||
"in": "path",
|
||||
"required": true
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "OK",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/response.Response"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/api/v1/notifications/targets/{target_id}/read": {
|
||||
"post": {
|
||||
"security": [
|
||||
{
|
||||
"BearerAuth": []
|
||||
}
|
||||
],
|
||||
"description": "将同一 target 下的所有通知标记为已读",
|
||||
"consumes": [
|
||||
"application/json"
|
||||
],
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"notifications"
|
||||
],
|
||||
"summary": "按目标ID标记已读",
|
||||
"parameters": [
|
||||
{
|
||||
"type": "integer",
|
||||
"description": "目标ID(如藏品ID)",
|
||||
"name": "target_id",
|
||||
"in": "path",
|
||||
"required": true
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "OK",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/response.Response"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/api/v1/notifications/unread-count": {
|
||||
"get": {
|
||||
"security": [
|
||||
{
|
||||
"BearerAuth": []
|
||||
}
|
||||
],
|
||||
"description": "按类型返回未读数量(like/system/activity/total)",
|
||||
"consumes": [
|
||||
"application/json"
|
||||
],
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"notifications"
|
||||
],
|
||||
"summary": "获取未读通知数",
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "OK",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/response.Response"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/api/v1/notifications/{id}": {
|
||||
"delete": {
|
||||
"security": [
|
||||
{
|
||||
"BearerAuth": []
|
||||
}
|
||||
],
|
||||
"description": "根据ID删除通知",
|
||||
"consumes": [
|
||||
"application/json"
|
||||
],
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"notifications"
|
||||
],
|
||||
"summary": "删除单条通知",
|
||||
"parameters": [
|
||||
{
|
||||
"type": "integer",
|
||||
"description": "通知ID",
|
||||
"name": "id",
|
||||
"in": "path",
|
||||
"required": true
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "OK",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/response.Response"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/api/v1/notifications/{id}/read": {
|
||||
"post": {
|
||||
"security": [
|
||||
{
|
||||
"BearerAuth": []
|
||||
}
|
||||
],
|
||||
"description": "根据通知ID标记为已读",
|
||||
"consumes": [
|
||||
"application/json"
|
||||
],
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"notifications"
|
||||
],
|
||||
"summary": "标记单条通知已读",
|
||||
"parameters": [
|
||||
{
|
||||
"type": "integer",
|
||||
"description": "通知ID",
|
||||
"name": "id",
|
||||
"in": "path",
|
||||
"required": true
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "OK",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/response.Response"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/api/v1/public/oss/upload-signature": {
|
||||
"get": {
|
||||
"description": "用于注册等未登录场景下上传头像,无需鉴权。\n后端生成唯一完整 key,policy 锁到该 key,前端只能写到指定路径;key 形如 avatar/register-pending/{key}/avatar_\u003cuuid\u003e.png。",
|
||||
@ -4135,6 +4670,86 @@ const docTemplate = `{
|
||||
}
|
||||
}
|
||||
},
|
||||
"controller.adminCreateNotificationRequest": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"title",
|
||||
"type",
|
||||
"user_ids"
|
||||
],
|
||||
"properties": {
|
||||
"content": {
|
||||
"type": "string",
|
||||
"maxLength": 500
|
||||
},
|
||||
"data": {
|
||||
"type": "object",
|
||||
"additionalProperties": true
|
||||
},
|
||||
"star_id": {
|
||||
"type": "integer"
|
||||
},
|
||||
"title": {
|
||||
"type": "string",
|
||||
"maxLength": 200,
|
||||
"minLength": 1
|
||||
},
|
||||
"type": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"system",
|
||||
"activity",
|
||||
"like"
|
||||
]
|
||||
},
|
||||
"user_ids": {
|
||||
"type": "array",
|
||||
"maxItems": 10000,
|
||||
"minItems": 1,
|
||||
"items": {
|
||||
"type": "integer"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"controller.registerDeviceRequest": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"cid"
|
||||
],
|
||||
"properties": {
|
||||
"app_version": {
|
||||
"type": "string",
|
||||
"maxLength": 32
|
||||
},
|
||||
"cid": {
|
||||
"type": "string",
|
||||
"maxLength": 128,
|
||||
"minLength": 1
|
||||
},
|
||||
"device_model": {
|
||||
"type": "string",
|
||||
"maxLength": 64
|
||||
},
|
||||
"platform": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"ios",
|
||||
"android",
|
||||
"harmony"
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
"controller.unregisterDeviceRequest": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"cid": {
|
||||
"type": "string",
|
||||
"maxLength": 128
|
||||
}
|
||||
}
|
||||
},
|
||||
"dto.AddIdentityResponseDTO": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
@ -4277,6 +4892,14 @@ const docTemplate = `{
|
||||
"description": "封面图URL",
|
||||
"type": "string"
|
||||
},
|
||||
"earnings": {
|
||||
"description": "当前可领取收益(与 ExhibitedAssetItemDTO 命名对齐)",
|
||||
"type": "integer"
|
||||
},
|
||||
"hourly_earnings": {
|
||||
"description": "每小时收益",
|
||||
"type": "number"
|
||||
},
|
||||
"like_count": {
|
||||
"description": "点赞数",
|
||||
"type": "integer"
|
||||
|
||||
@ -717,6 +717,106 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"/api/v1/activities/{activity_id}/messages": {
|
||||
"get": {
|
||||
"security": [
|
||||
{
|
||||
"BearerAuth": []
|
||||
}
|
||||
],
|
||||
"description": "分页获取活动留言列表(最新在上)",
|
||||
"consumes": [
|
||||
"application/json"
|
||||
],
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"activities"
|
||||
],
|
||||
"summary": "列出活动留言",
|
||||
"parameters": [
|
||||
{
|
||||
"type": "integer",
|
||||
"format": "int64",
|
||||
"description": "活动ID",
|
||||
"name": "activity_id",
|
||||
"in": "path",
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
"type": "integer",
|
||||
"description": "页码,默认1",
|
||||
"name": "page",
|
||||
"in": "query"
|
||||
},
|
||||
{
|
||||
"type": "integer",
|
||||
"description": "每页数量,默认20,最大50",
|
||||
"name": "page_size",
|
||||
"in": "query"
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "OK",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/response.Response"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"post": {
|
||||
"security": [
|
||||
{
|
||||
"BearerAuth": []
|
||||
}
|
||||
],
|
||||
"description": "用户在应援活动页面发送一条祝福留言",
|
||||
"consumes": [
|
||||
"application/json"
|
||||
],
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"activities"
|
||||
],
|
||||
"summary": "发送活动留言",
|
||||
"parameters": [
|
||||
{
|
||||
"type": "integer",
|
||||
"format": "int64",
|
||||
"description": "活动ID",
|
||||
"name": "activity_id",
|
||||
"in": "path",
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
"description": "留言内容",
|
||||
"name": "request",
|
||||
"in": "body",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"content": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "OK",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/response.Response"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/api/v1/activities/{activity_id}/progress": {
|
||||
"get": {
|
||||
"security": [
|
||||
@ -859,6 +959,97 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"/api/v1/activities/{id}/top-ranking": {
|
||||
"get": {
|
||||
"security": [
|
||||
{
|
||||
"BearerAuth": []
|
||||
}
|
||||
],
|
||||
"description": "返回 top3 头像组 + my_info(rank/avatar/gap_to_prev/status),与 /ranking 不同:仅返回本组件所需字段,gap_to_prev 由后端计算",
|
||||
"consumes": [
|
||||
"application/json"
|
||||
],
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"activities"
|
||||
],
|
||||
"summary": "获取活动 TOP3 + 我的排名",
|
||||
"parameters": [
|
||||
{
|
||||
"type": "integer",
|
||||
"format": "int64",
|
||||
"description": "活动ID",
|
||||
"name": "id",
|
||||
"in": "path",
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
"type": "integer",
|
||||
"format": "int64",
|
||||
"description": "粉丝身份ID(不传则用 token 中的)",
|
||||
"name": "star_id",
|
||||
"in": "query"
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "OK",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/response.Response"
|
||||
}
|
||||
},
|
||||
"400": {
|
||||
"description": "Bad Request",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/response.Response"
|
||||
}
|
||||
},
|
||||
"401": {
|
||||
"description": "Unauthorized",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/response.Response"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/api/v1/admin/notifications": {
|
||||
"post": {
|
||||
"description": "内部接口,无 JWT 鉴权,供 admin 后台(Python 8081)调用。user_ids 由调用方解析(查 fan_profiles)。",
|
||||
"consumes": [
|
||||
"application/json"
|
||||
],
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"admin"
|
||||
],
|
||||
"summary": "admin 批量发送通知",
|
||||
"parameters": [
|
||||
{
|
||||
"description": "通知 payload(已解析的 user_ids)",
|
||||
"name": "body",
|
||||
"in": "body",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"$ref": "#/definitions/controller.adminCreateNotificationRequest"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "OK",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/response.Response"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/api/v1/ai-chat/history/{sessionId}": {
|
||||
"get": {
|
||||
"security": [
|
||||
@ -3028,6 +3219,350 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"/api/v1/notifications": {
|
||||
"get": {
|
||||
"security": [
|
||||
{
|
||||
"BearerAuth": []
|
||||
}
|
||||
],
|
||||
"description": "获取当前用户的通知列表(支持 type/tab 分页)",
|
||||
"consumes": [
|
||||
"application/json"
|
||||
],
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"notifications"
|
||||
],
|
||||
"summary": "获取通知列表",
|
||||
"parameters": [
|
||||
{
|
||||
"type": "string",
|
||||
"description": "通知类型过滤: like / system / activity",
|
||||
"name": "type",
|
||||
"in": "query"
|
||||
},
|
||||
{
|
||||
"type": "string",
|
||||
"description": "列表 tab: unread / read / all",
|
||||
"name": "tab",
|
||||
"in": "query"
|
||||
},
|
||||
{
|
||||
"type": "integer",
|
||||
"description": "页码,默认1",
|
||||
"name": "page",
|
||||
"in": "query"
|
||||
},
|
||||
{
|
||||
"type": "integer",
|
||||
"description": "每页数量,默认20",
|
||||
"name": "page_size",
|
||||
"in": "query"
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "OK",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/response.Response"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/api/v1/notifications/devices": {
|
||||
"post": {
|
||||
"security": [
|
||||
{
|
||||
"BearerAuth": []
|
||||
}
|
||||
],
|
||||
"description": "将 uni.getPushClientId() 拿到的 cid 上报给后端;同 cid 重复注册为更新。",
|
||||
"consumes": [
|
||||
"application/json"
|
||||
],
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"notifications"
|
||||
],
|
||||
"summary": "注册推送设备",
|
||||
"parameters": [
|
||||
{
|
||||
"description": "设备信息",
|
||||
"name": "body",
|
||||
"in": "body",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"$ref": "#/definitions/controller.registerDeviceRequest"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "OK",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/response.Response"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/api/v1/notifications/devices/unregister": {
|
||||
"post": {
|
||||
"security": [
|
||||
{
|
||||
"BearerAuth": []
|
||||
}
|
||||
],
|
||||
"description": "注销推送 cid;cid 为空 = 注销当前用户全部设备(用于主动登出)。",
|
||||
"consumes": [
|
||||
"application/json"
|
||||
],
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"notifications"
|
||||
],
|
||||
"summary": "注销推送设备",
|
||||
"parameters": [
|
||||
{
|
||||
"description": "注销请求",
|
||||
"name": "body",
|
||||
"in": "body",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"$ref": "#/definitions/controller.unregisterDeviceRequest"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "OK",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/response.Response"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/api/v1/notifications/read-all": {
|
||||
"post": {
|
||||
"security": [
|
||||
{
|
||||
"BearerAuth": []
|
||||
}
|
||||
],
|
||||
"description": "将当前用户某类型或全部通知标记为已读",
|
||||
"consumes": [
|
||||
"application/json"
|
||||
],
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"notifications"
|
||||
],
|
||||
"summary": "全部已读",
|
||||
"parameters": [
|
||||
{
|
||||
"type": "string",
|
||||
"description": "通知类型过滤: like / system / activity; 留空表示全部",
|
||||
"name": "type",
|
||||
"in": "query"
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "OK",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/response.Response"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/api/v1/notifications/targets/{target_id}": {
|
||||
"delete": {
|
||||
"security": [
|
||||
{
|
||||
"BearerAuth": []
|
||||
}
|
||||
],
|
||||
"description": "删除同一 target 下的所有通知",
|
||||
"consumes": [
|
||||
"application/json"
|
||||
],
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"notifications"
|
||||
],
|
||||
"summary": "按目标ID删除通知",
|
||||
"parameters": [
|
||||
{
|
||||
"type": "integer",
|
||||
"description": "目标ID(如藏品ID)",
|
||||
"name": "target_id",
|
||||
"in": "path",
|
||||
"required": true
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "OK",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/response.Response"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/api/v1/notifications/targets/{target_id}/read": {
|
||||
"post": {
|
||||
"security": [
|
||||
{
|
||||
"BearerAuth": []
|
||||
}
|
||||
],
|
||||
"description": "将同一 target 下的所有通知标记为已读",
|
||||
"consumes": [
|
||||
"application/json"
|
||||
],
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"notifications"
|
||||
],
|
||||
"summary": "按目标ID标记已读",
|
||||
"parameters": [
|
||||
{
|
||||
"type": "integer",
|
||||
"description": "目标ID(如藏品ID)",
|
||||
"name": "target_id",
|
||||
"in": "path",
|
||||
"required": true
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "OK",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/response.Response"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/api/v1/notifications/unread-count": {
|
||||
"get": {
|
||||
"security": [
|
||||
{
|
||||
"BearerAuth": []
|
||||
}
|
||||
],
|
||||
"description": "按类型返回未读数量(like/system/activity/total)",
|
||||
"consumes": [
|
||||
"application/json"
|
||||
],
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"notifications"
|
||||
],
|
||||
"summary": "获取未读通知数",
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "OK",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/response.Response"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/api/v1/notifications/{id}": {
|
||||
"delete": {
|
||||
"security": [
|
||||
{
|
||||
"BearerAuth": []
|
||||
}
|
||||
],
|
||||
"description": "根据ID删除通知",
|
||||
"consumes": [
|
||||
"application/json"
|
||||
],
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"notifications"
|
||||
],
|
||||
"summary": "删除单条通知",
|
||||
"parameters": [
|
||||
{
|
||||
"type": "integer",
|
||||
"description": "通知ID",
|
||||
"name": "id",
|
||||
"in": "path",
|
||||
"required": true
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "OK",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/response.Response"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/api/v1/notifications/{id}/read": {
|
||||
"post": {
|
||||
"security": [
|
||||
{
|
||||
"BearerAuth": []
|
||||
}
|
||||
],
|
||||
"description": "根据通知ID标记为已读",
|
||||
"consumes": [
|
||||
"application/json"
|
||||
],
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"notifications"
|
||||
],
|
||||
"summary": "标记单条通知已读",
|
||||
"parameters": [
|
||||
{
|
||||
"type": "integer",
|
||||
"description": "通知ID",
|
||||
"name": "id",
|
||||
"in": "path",
|
||||
"required": true
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "OK",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/response.Response"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/api/v1/public/oss/upload-signature": {
|
||||
"get": {
|
||||
"description": "用于注册等未登录场景下上传头像,无需鉴权。\n后端生成唯一完整 key,policy 锁到该 key,前端只能写到指定路径;key 形如 avatar/register-pending/{key}/avatar_\u003cuuid\u003e.png。",
|
||||
@ -4129,6 +4664,86 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"controller.adminCreateNotificationRequest": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"title",
|
||||
"type",
|
||||
"user_ids"
|
||||
],
|
||||
"properties": {
|
||||
"content": {
|
||||
"type": "string",
|
||||
"maxLength": 500
|
||||
},
|
||||
"data": {
|
||||
"type": "object",
|
||||
"additionalProperties": true
|
||||
},
|
||||
"star_id": {
|
||||
"type": "integer"
|
||||
},
|
||||
"title": {
|
||||
"type": "string",
|
||||
"maxLength": 200,
|
||||
"minLength": 1
|
||||
},
|
||||
"type": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"system",
|
||||
"activity",
|
||||
"like"
|
||||
]
|
||||
},
|
||||
"user_ids": {
|
||||
"type": "array",
|
||||
"maxItems": 10000,
|
||||
"minItems": 1,
|
||||
"items": {
|
||||
"type": "integer"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"controller.registerDeviceRequest": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"cid"
|
||||
],
|
||||
"properties": {
|
||||
"app_version": {
|
||||
"type": "string",
|
||||
"maxLength": 32
|
||||
},
|
||||
"cid": {
|
||||
"type": "string",
|
||||
"maxLength": 128,
|
||||
"minLength": 1
|
||||
},
|
||||
"device_model": {
|
||||
"type": "string",
|
||||
"maxLength": 64
|
||||
},
|
||||
"platform": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"ios",
|
||||
"android",
|
||||
"harmony"
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
"controller.unregisterDeviceRequest": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"cid": {
|
||||
"type": "string",
|
||||
"maxLength": 128
|
||||
}
|
||||
}
|
||||
},
|
||||
"dto.AddIdentityResponseDTO": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
@ -4271,6 +4886,14 @@
|
||||
"description": "封面图URL",
|
||||
"type": "string"
|
||||
},
|
||||
"earnings": {
|
||||
"description": "当前可领取收益(与 ExhibitedAssetItemDTO 命名对齐)",
|
||||
"type": "integer"
|
||||
},
|
||||
"hourly_earnings": {
|
||||
"description": "每小时收益",
|
||||
"type": "number"
|
||||
},
|
||||
"like_count": {
|
||||
"description": "点赞数",
|
||||
"type": "integer"
|
||||
|
||||
@ -49,6 +49,64 @@ definitions:
|
||||
description: 响应时间戳(Unix时间戳毫秒)
|
||||
type: integer
|
||||
type: object
|
||||
controller.adminCreateNotificationRequest:
|
||||
properties:
|
||||
content:
|
||||
maxLength: 500
|
||||
type: string
|
||||
data:
|
||||
additionalProperties: true
|
||||
type: object
|
||||
star_id:
|
||||
type: integer
|
||||
title:
|
||||
maxLength: 200
|
||||
minLength: 1
|
||||
type: string
|
||||
type:
|
||||
enum:
|
||||
- system
|
||||
- activity
|
||||
- like
|
||||
type: string
|
||||
user_ids:
|
||||
items:
|
||||
type: integer
|
||||
maxItems: 10000
|
||||
minItems: 1
|
||||
type: array
|
||||
required:
|
||||
- title
|
||||
- type
|
||||
- user_ids
|
||||
type: object
|
||||
controller.registerDeviceRequest:
|
||||
properties:
|
||||
app_version:
|
||||
maxLength: 32
|
||||
type: string
|
||||
cid:
|
||||
maxLength: 128
|
||||
minLength: 1
|
||||
type: string
|
||||
device_model:
|
||||
maxLength: 64
|
||||
type: string
|
||||
platform:
|
||||
enum:
|
||||
- ios
|
||||
- android
|
||||
- harmony
|
||||
type: string
|
||||
required:
|
||||
- cid
|
||||
type: object
|
||||
controller.unregisterDeviceRequest:
|
||||
properties:
|
||||
cid:
|
||||
maxLength: 128
|
||||
type: string
|
||||
type: object
|
||||
dto.AddIdentityResponseDTO:
|
||||
properties:
|
||||
bound:
|
||||
@ -151,6 +209,12 @@ definitions:
|
||||
cover_url:
|
||||
description: 封面图URL
|
||||
type: string
|
||||
earnings:
|
||||
description: 当前可领取收益(与 ExhibitedAssetItemDTO 命名对齐)
|
||||
type: integer
|
||||
hourly_earnings:
|
||||
description: 每小时收益
|
||||
type: number
|
||||
like_count:
|
||||
description: 点赞数
|
||||
type: integer
|
||||
@ -1403,6 +1467,70 @@ paths:
|
||||
summary: 获取活动道具列表
|
||||
tags:
|
||||
- activities
|
||||
/api/v1/activities/{activity_id}/messages:
|
||||
get:
|
||||
consumes:
|
||||
- application/json
|
||||
description: 分页获取活动留言列表(最新在上)
|
||||
parameters:
|
||||
- description: 活动ID
|
||||
format: int64
|
||||
in: path
|
||||
name: activity_id
|
||||
required: true
|
||||
type: integer
|
||||
- description: 页码,默认1
|
||||
in: query
|
||||
name: page
|
||||
type: integer
|
||||
- description: 每页数量,默认20,最大50
|
||||
in: query
|
||||
name: page_size
|
||||
type: integer
|
||||
produces:
|
||||
- application/json
|
||||
responses:
|
||||
"200":
|
||||
description: OK
|
||||
schema:
|
||||
$ref: '#/definitions/response.Response'
|
||||
security:
|
||||
- BearerAuth: []
|
||||
summary: 列出活动留言
|
||||
tags:
|
||||
- activities
|
||||
post:
|
||||
consumes:
|
||||
- application/json
|
||||
description: 用户在应援活动页面发送一条祝福留言
|
||||
parameters:
|
||||
- description: 活动ID
|
||||
format: int64
|
||||
in: path
|
||||
name: activity_id
|
||||
required: true
|
||||
type: integer
|
||||
- description: 留言内容
|
||||
in: body
|
||||
name: request
|
||||
required: true
|
||||
schema:
|
||||
properties:
|
||||
content:
|
||||
type: string
|
||||
type: object
|
||||
produces:
|
||||
- application/json
|
||||
responses:
|
||||
"200":
|
||||
description: OK
|
||||
schema:
|
||||
$ref: '#/definitions/response.Response'
|
||||
security:
|
||||
- BearerAuth: []
|
||||
summary: 发送活动留言
|
||||
tags:
|
||||
- activities
|
||||
/api/v1/activities/{activity_id}/progress:
|
||||
get:
|
||||
consumes:
|
||||
@ -1494,6 +1622,66 @@ paths:
|
||||
summary: 获取贡献点排名
|
||||
tags:
|
||||
- activities
|
||||
/api/v1/activities/{id}/top-ranking:
|
||||
get:
|
||||
consumes:
|
||||
- application/json
|
||||
description: 返回 top3 头像组 + my_info(rank/avatar/gap_to_prev/status),与 /ranking
|
||||
不同:仅返回本组件所需字段,gap_to_prev 由后端计算
|
||||
parameters:
|
||||
- description: 活动ID
|
||||
format: int64
|
||||
in: path
|
||||
name: id
|
||||
required: true
|
||||
type: integer
|
||||
- description: 粉丝身份ID(不传则用 token 中的)
|
||||
format: int64
|
||||
in: query
|
||||
name: star_id
|
||||
type: integer
|
||||
produces:
|
||||
- application/json
|
||||
responses:
|
||||
"200":
|
||||
description: OK
|
||||
schema:
|
||||
$ref: '#/definitions/response.Response'
|
||||
"400":
|
||||
description: Bad Request
|
||||
schema:
|
||||
$ref: '#/definitions/response.Response'
|
||||
"401":
|
||||
description: Unauthorized
|
||||
schema:
|
||||
$ref: '#/definitions/response.Response'
|
||||
security:
|
||||
- BearerAuth: []
|
||||
summary: 获取活动 TOP3 + 我的排名
|
||||
tags:
|
||||
- activities
|
||||
/api/v1/admin/notifications:
|
||||
post:
|
||||
consumes:
|
||||
- application/json
|
||||
description: 内部接口,无 JWT 鉴权,供 admin 后台(Python 8081)调用。user_ids 由调用方解析(查 fan_profiles)。
|
||||
parameters:
|
||||
- description: 通知 payload(已解析的 user_ids)
|
||||
in: body
|
||||
name: body
|
||||
required: true
|
||||
schema:
|
||||
$ref: '#/definitions/controller.adminCreateNotificationRequest'
|
||||
produces:
|
||||
- application/json
|
||||
responses:
|
||||
"200":
|
||||
description: OK
|
||||
schema:
|
||||
$ref: '#/definitions/response.Response'
|
||||
summary: admin 批量发送通知
|
||||
tags:
|
||||
- admin
|
||||
/api/v1/ai-chat/history/{sessionId}:
|
||||
get:
|
||||
consumes:
|
||||
@ -2804,6 +2992,219 @@ paths:
|
||||
summary: 切换粉丝身份
|
||||
tags:
|
||||
- users
|
||||
/api/v1/notifications:
|
||||
get:
|
||||
consumes:
|
||||
- application/json
|
||||
description: 获取当前用户的通知列表(支持 type/tab 分页)
|
||||
parameters:
|
||||
- description: '通知类型过滤: like / system / activity'
|
||||
in: query
|
||||
name: type
|
||||
type: string
|
||||
- description: '列表 tab: unread / read / all'
|
||||
in: query
|
||||
name: tab
|
||||
type: string
|
||||
- description: 页码,默认1
|
||||
in: query
|
||||
name: page
|
||||
type: integer
|
||||
- description: 每页数量,默认20
|
||||
in: query
|
||||
name: page_size
|
||||
type: integer
|
||||
produces:
|
||||
- application/json
|
||||
responses:
|
||||
"200":
|
||||
description: OK
|
||||
schema:
|
||||
$ref: '#/definitions/response.Response'
|
||||
security:
|
||||
- BearerAuth: []
|
||||
summary: 获取通知列表
|
||||
tags:
|
||||
- notifications
|
||||
/api/v1/notifications/{id}:
|
||||
delete:
|
||||
consumes:
|
||||
- application/json
|
||||
description: 根据ID删除通知
|
||||
parameters:
|
||||
- description: 通知ID
|
||||
in: path
|
||||
name: id
|
||||
required: true
|
||||
type: integer
|
||||
produces:
|
||||
- application/json
|
||||
responses:
|
||||
"200":
|
||||
description: OK
|
||||
schema:
|
||||
$ref: '#/definitions/response.Response'
|
||||
security:
|
||||
- BearerAuth: []
|
||||
summary: 删除单条通知
|
||||
tags:
|
||||
- notifications
|
||||
/api/v1/notifications/{id}/read:
|
||||
post:
|
||||
consumes:
|
||||
- application/json
|
||||
description: 根据通知ID标记为已读
|
||||
parameters:
|
||||
- description: 通知ID
|
||||
in: path
|
||||
name: id
|
||||
required: true
|
||||
type: integer
|
||||
produces:
|
||||
- application/json
|
||||
responses:
|
||||
"200":
|
||||
description: OK
|
||||
schema:
|
||||
$ref: '#/definitions/response.Response'
|
||||
security:
|
||||
- BearerAuth: []
|
||||
summary: 标记单条通知已读
|
||||
tags:
|
||||
- notifications
|
||||
/api/v1/notifications/devices:
|
||||
post:
|
||||
consumes:
|
||||
- application/json
|
||||
description: 将 uni.getPushClientId() 拿到的 cid 上报给后端;同 cid 重复注册为更新。
|
||||
parameters:
|
||||
- description: 设备信息
|
||||
in: body
|
||||
name: body
|
||||
required: true
|
||||
schema:
|
||||
$ref: '#/definitions/controller.registerDeviceRequest'
|
||||
produces:
|
||||
- application/json
|
||||
responses:
|
||||
"200":
|
||||
description: OK
|
||||
schema:
|
||||
$ref: '#/definitions/response.Response'
|
||||
security:
|
||||
- BearerAuth: []
|
||||
summary: 注册推送设备
|
||||
tags:
|
||||
- notifications
|
||||
/api/v1/notifications/devices/unregister:
|
||||
post:
|
||||
consumes:
|
||||
- application/json
|
||||
description: 注销推送 cid;cid 为空 = 注销当前用户全部设备(用于主动登出)。
|
||||
parameters:
|
||||
- description: 注销请求
|
||||
in: body
|
||||
name: body
|
||||
required: true
|
||||
schema:
|
||||
$ref: '#/definitions/controller.unregisterDeviceRequest'
|
||||
produces:
|
||||
- application/json
|
||||
responses:
|
||||
"200":
|
||||
description: OK
|
||||
schema:
|
||||
$ref: '#/definitions/response.Response'
|
||||
security:
|
||||
- BearerAuth: []
|
||||
summary: 注销推送设备
|
||||
tags:
|
||||
- notifications
|
||||
/api/v1/notifications/read-all:
|
||||
post:
|
||||
consumes:
|
||||
- application/json
|
||||
description: 将当前用户某类型或全部通知标记为已读
|
||||
parameters:
|
||||
- description: '通知类型过滤: like / system / activity; 留空表示全部'
|
||||
in: query
|
||||
name: type
|
||||
type: string
|
||||
produces:
|
||||
- application/json
|
||||
responses:
|
||||
"200":
|
||||
description: OK
|
||||
schema:
|
||||
$ref: '#/definitions/response.Response'
|
||||
security:
|
||||
- BearerAuth: []
|
||||
summary: 全部已读
|
||||
tags:
|
||||
- notifications
|
||||
/api/v1/notifications/targets/{target_id}:
|
||||
delete:
|
||||
consumes:
|
||||
- application/json
|
||||
description: 删除同一 target 下的所有通知
|
||||
parameters:
|
||||
- description: 目标ID(如藏品ID)
|
||||
in: path
|
||||
name: target_id
|
||||
required: true
|
||||
type: integer
|
||||
produces:
|
||||
- application/json
|
||||
responses:
|
||||
"200":
|
||||
description: OK
|
||||
schema:
|
||||
$ref: '#/definitions/response.Response'
|
||||
security:
|
||||
- BearerAuth: []
|
||||
summary: 按目标ID删除通知
|
||||
tags:
|
||||
- notifications
|
||||
/api/v1/notifications/targets/{target_id}/read:
|
||||
post:
|
||||
consumes:
|
||||
- application/json
|
||||
description: 将同一 target 下的所有通知标记为已读
|
||||
parameters:
|
||||
- description: 目标ID(如藏品ID)
|
||||
in: path
|
||||
name: target_id
|
||||
required: true
|
||||
type: integer
|
||||
produces:
|
||||
- application/json
|
||||
responses:
|
||||
"200":
|
||||
description: OK
|
||||
schema:
|
||||
$ref: '#/definitions/response.Response'
|
||||
security:
|
||||
- BearerAuth: []
|
||||
summary: 按目标ID标记已读
|
||||
tags:
|
||||
- notifications
|
||||
/api/v1/notifications/unread-count:
|
||||
get:
|
||||
consumes:
|
||||
- application/json
|
||||
description: 按类型返回未读数量(like/system/activity/total)
|
||||
produces:
|
||||
- application/json
|
||||
responses:
|
||||
"200":
|
||||
description: OK
|
||||
schema:
|
||||
$ref: '#/definitions/response.Response'
|
||||
security:
|
||||
- BearerAuth: []
|
||||
summary: 获取未读通知数
|
||||
tags:
|
||||
- notifications
|
||||
/api/v1/public/oss/upload-signature:
|
||||
get:
|
||||
consumes:
|
||||
|
||||
@ -388,6 +388,7 @@ func SetupRouter(userClient *client.Client, socialClient *client.Client, assetCl
|
||||
activities.POST("/:id/purchase", activityCtrl.PurchaseItem) // 购买道具
|
||||
activities.POST("/:id/batch-purchase", activityCtrl.BatchPurchaseItem) // 批量购买道具
|
||||
activities.GET("/:id/ranking", activityCtrl.GetContributionRanking) // 获取贡献点排名
|
||||
activities.GET("/:id/top-ranking", activityCtrl.GetTopRanking) // 获取活动 TOP3 + 我的排名(专用轻量接口)
|
||||
activities.GET("/:id/contributions/latest", activityCtrl.GetLatestContributions) // 获取最新贡献记录
|
||||
activities.GET("/:id/messages", activityCtrl.ListActivityMessages) // 获取活动留言列表
|
||||
activities.POST("/:id/messages", activityCtrl.CreateActivityMessage) // 发送一条活动留言
|
||||
|
||||
@ -1104,6 +1104,258 @@ func (x *MyContribution) GetAvatarUrl() string {
|
||||
return ""
|
||||
}
|
||||
|
||||
// 活动 TOP3 + 我的排名请求(专用轻量接口)
|
||||
type TopRankingRequest struct {
|
||||
state protoimpl.MessageState `protogen:"open.v1"`
|
||||
ActivityId int64 `protobuf:"varint,1,opt,name=activity_id,json=activityId,proto3" json:"activity_id,omitempty"`
|
||||
StarId int64 `protobuf:"varint,2,opt,name=star_id,json=starId,proto3" json:"star_id,omitempty"` // 0 表示不按明星过滤
|
||||
UserId int64 `protobuf:"varint,3,opt,name=user_id,json=userId,proto3" json:"user_id,omitempty"` // 来自 token
|
||||
unknownFields protoimpl.UnknownFields
|
||||
sizeCache protoimpl.SizeCache
|
||||
}
|
||||
|
||||
func (x *TopRankingRequest) Reset() {
|
||||
*x = TopRankingRequest{}
|
||||
mi := &file_activity_proto_msgTypes[13]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
|
||||
func (x *TopRankingRequest) String() string {
|
||||
return protoimpl.X.MessageStringOf(x)
|
||||
}
|
||||
|
||||
func (*TopRankingRequest) ProtoMessage() {}
|
||||
|
||||
func (x *TopRankingRequest) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_activity_proto_msgTypes[13]
|
||||
if x != nil {
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
if ms.LoadMessageInfo() == nil {
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
return ms
|
||||
}
|
||||
return mi.MessageOf(x)
|
||||
}
|
||||
|
||||
// Deprecated: Use TopRankingRequest.ProtoReflect.Descriptor instead.
|
||||
func (*TopRankingRequest) Descriptor() ([]byte, []int) {
|
||||
return file_activity_proto_rawDescGZIP(), []int{13}
|
||||
}
|
||||
|
||||
func (x *TopRankingRequest) GetActivityId() int64 {
|
||||
if x != nil {
|
||||
return x.ActivityId
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
func (x *TopRankingRequest) GetStarId() int64 {
|
||||
if x != nil {
|
||||
return x.StarId
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
func (x *TopRankingRequest) GetUserId() int64 {
|
||||
if x != nil {
|
||||
return x.UserId
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
// TOP3 排名项
|
||||
type TopRankingItem struct {
|
||||
state protoimpl.MessageState `protogen:"open.v1"`
|
||||
Rank int32 `protobuf:"varint,1,opt,name=rank,proto3" json:"rank,omitempty"`
|
||||
UserId int64 `protobuf:"varint,2,opt,name=user_id,json=userId,proto3" json:"user_id,omitempty"`
|
||||
AvatarUrl string `protobuf:"bytes,3,opt,name=avatar_url,json=avatarUrl,proto3" json:"avatar_url,omitempty"`
|
||||
unknownFields protoimpl.UnknownFields
|
||||
sizeCache protoimpl.SizeCache
|
||||
}
|
||||
|
||||
func (x *TopRankingItem) Reset() {
|
||||
*x = TopRankingItem{}
|
||||
mi := &file_activity_proto_msgTypes[14]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
|
||||
func (x *TopRankingItem) String() string {
|
||||
return protoimpl.X.MessageStringOf(x)
|
||||
}
|
||||
|
||||
func (*TopRankingItem) ProtoMessage() {}
|
||||
|
||||
func (x *TopRankingItem) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_activity_proto_msgTypes[14]
|
||||
if x != nil {
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
if ms.LoadMessageInfo() == nil {
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
return ms
|
||||
}
|
||||
return mi.MessageOf(x)
|
||||
}
|
||||
|
||||
// Deprecated: Use TopRankingItem.ProtoReflect.Descriptor instead.
|
||||
func (*TopRankingItem) Descriptor() ([]byte, []int) {
|
||||
return file_activity_proto_rawDescGZIP(), []int{14}
|
||||
}
|
||||
|
||||
func (x *TopRankingItem) GetRank() int32 {
|
||||
if x != nil {
|
||||
return x.Rank
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
func (x *TopRankingItem) GetUserId() int64 {
|
||||
if x != nil {
|
||||
return x.UserId
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
func (x *TopRankingItem) GetAvatarUrl() string {
|
||||
if x != nil {
|
||||
return x.AvatarUrl
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// 我的 TOP 排名信息
|
||||
type MyTopRankingInfo struct {
|
||||
state protoimpl.MessageState `protogen:"open.v1"`
|
||||
Rank int32 `protobuf:"varint,1,opt,name=rank,proto3" json:"rank,omitempty"` // 0 表示未上榜
|
||||
AvatarUrl string `protobuf:"bytes,2,opt,name=avatar_url,json=avatarUrl,proto3" json:"avatar_url,omitempty"`
|
||||
GapToPrev int64 `protobuf:"varint,3,opt,name=gap_to_prev,json=gapToPrev,proto3" json:"gap_to_prev,omitempty"`
|
||||
Status string `protobuf:"bytes,4,opt,name=status,proto3" json:"status,omitempty"` // "ranked" | "unranked"
|
||||
unknownFields protoimpl.UnknownFields
|
||||
sizeCache protoimpl.SizeCache
|
||||
}
|
||||
|
||||
func (x *MyTopRankingInfo) Reset() {
|
||||
*x = MyTopRankingInfo{}
|
||||
mi := &file_activity_proto_msgTypes[15]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
|
||||
func (x *MyTopRankingInfo) String() string {
|
||||
return protoimpl.X.MessageStringOf(x)
|
||||
}
|
||||
|
||||
func (*MyTopRankingInfo) ProtoMessage() {}
|
||||
|
||||
func (x *MyTopRankingInfo) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_activity_proto_msgTypes[15]
|
||||
if x != nil {
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
if ms.LoadMessageInfo() == nil {
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
return ms
|
||||
}
|
||||
return mi.MessageOf(x)
|
||||
}
|
||||
|
||||
// Deprecated: Use MyTopRankingInfo.ProtoReflect.Descriptor instead.
|
||||
func (*MyTopRankingInfo) Descriptor() ([]byte, []int) {
|
||||
return file_activity_proto_rawDescGZIP(), []int{15}
|
||||
}
|
||||
|
||||
func (x *MyTopRankingInfo) GetRank() int32 {
|
||||
if x != nil {
|
||||
return x.Rank
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
func (x *MyTopRankingInfo) GetAvatarUrl() string {
|
||||
if x != nil {
|
||||
return x.AvatarUrl
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (x *MyTopRankingInfo) GetGapToPrev() int64 {
|
||||
if x != nil {
|
||||
return x.GapToPrev
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
func (x *MyTopRankingInfo) GetStatus() string {
|
||||
if x != nil {
|
||||
return x.Status
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// TOP 排名响应
|
||||
type TopRankingResponse struct {
|
||||
state protoimpl.MessageState `protogen:"open.v1"`
|
||||
Base *common.BaseResponse `protobuf:"bytes,1,opt,name=base,proto3" json:"base,omitempty"`
|
||||
Top3 []*TopRankingItem `protobuf:"bytes,2,rep,name=top3,proto3" json:"top3,omitempty"`
|
||||
MyInfo *MyTopRankingInfo `protobuf:"bytes,3,opt,name=my_info,json=myInfo,proto3" json:"my_info,omitempty"`
|
||||
unknownFields protoimpl.UnknownFields
|
||||
sizeCache protoimpl.SizeCache
|
||||
}
|
||||
|
||||
func (x *TopRankingResponse) Reset() {
|
||||
*x = TopRankingResponse{}
|
||||
mi := &file_activity_proto_msgTypes[16]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
|
||||
func (x *TopRankingResponse) String() string {
|
||||
return protoimpl.X.MessageStringOf(x)
|
||||
}
|
||||
|
||||
func (*TopRankingResponse) ProtoMessage() {}
|
||||
|
||||
func (x *TopRankingResponse) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_activity_proto_msgTypes[16]
|
||||
if x != nil {
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
if ms.LoadMessageInfo() == nil {
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
return ms
|
||||
}
|
||||
return mi.MessageOf(x)
|
||||
}
|
||||
|
||||
// Deprecated: Use TopRankingResponse.ProtoReflect.Descriptor instead.
|
||||
func (*TopRankingResponse) Descriptor() ([]byte, []int) {
|
||||
return file_activity_proto_rawDescGZIP(), []int{16}
|
||||
}
|
||||
|
||||
func (x *TopRankingResponse) GetBase() *common.BaseResponse {
|
||||
if x != nil {
|
||||
return x.Base
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (x *TopRankingResponse) GetTop3() []*TopRankingItem {
|
||||
if x != nil {
|
||||
return x.Top3
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (x *TopRankingResponse) GetMyInfo() *MyTopRankingInfo {
|
||||
if x != nil {
|
||||
return x.MyInfo
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// 活动列表请求
|
||||
type GetActivityListRequest struct {
|
||||
state protoimpl.MessageState `protogen:"open.v1"`
|
||||
@ -1117,7 +1369,7 @@ type GetActivityListRequest struct {
|
||||
|
||||
func (x *GetActivityListRequest) Reset() {
|
||||
*x = GetActivityListRequest{}
|
||||
mi := &file_activity_proto_msgTypes[13]
|
||||
mi := &file_activity_proto_msgTypes[17]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
@ -1129,7 +1381,7 @@ func (x *GetActivityListRequest) String() string {
|
||||
func (*GetActivityListRequest) ProtoMessage() {}
|
||||
|
||||
func (x *GetActivityListRequest) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_activity_proto_msgTypes[13]
|
||||
mi := &file_activity_proto_msgTypes[17]
|
||||
if x != nil {
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
if ms.LoadMessageInfo() == nil {
|
||||
@ -1142,7 +1394,7 @@ func (x *GetActivityListRequest) ProtoReflect() protoreflect.Message {
|
||||
|
||||
// Deprecated: Use GetActivityListRequest.ProtoReflect.Descriptor instead.
|
||||
func (*GetActivityListRequest) Descriptor() ([]byte, []int) {
|
||||
return file_activity_proto_rawDescGZIP(), []int{13}
|
||||
return file_activity_proto_rawDescGZIP(), []int{17}
|
||||
}
|
||||
|
||||
func (x *GetActivityListRequest) GetStarId() int64 {
|
||||
@ -1187,7 +1439,7 @@ type GetActivityListResponse struct {
|
||||
|
||||
func (x *GetActivityListResponse) Reset() {
|
||||
*x = GetActivityListResponse{}
|
||||
mi := &file_activity_proto_msgTypes[14]
|
||||
mi := &file_activity_proto_msgTypes[18]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
@ -1199,7 +1451,7 @@ func (x *GetActivityListResponse) String() string {
|
||||
func (*GetActivityListResponse) ProtoMessage() {}
|
||||
|
||||
func (x *GetActivityListResponse) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_activity_proto_msgTypes[14]
|
||||
mi := &file_activity_proto_msgTypes[18]
|
||||
if x != nil {
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
if ms.LoadMessageInfo() == nil {
|
||||
@ -1212,7 +1464,7 @@ func (x *GetActivityListResponse) ProtoReflect() protoreflect.Message {
|
||||
|
||||
// Deprecated: Use GetActivityListResponse.ProtoReflect.Descriptor instead.
|
||||
func (*GetActivityListResponse) Descriptor() ([]byte, []int) {
|
||||
return file_activity_proto_rawDescGZIP(), []int{14}
|
||||
return file_activity_proto_rawDescGZIP(), []int{18}
|
||||
}
|
||||
|
||||
func (x *GetActivityListResponse) GetBase() *common.BaseResponse {
|
||||
@ -1260,7 +1512,7 @@ type GetProgressRequest struct {
|
||||
|
||||
func (x *GetProgressRequest) Reset() {
|
||||
*x = GetProgressRequest{}
|
||||
mi := &file_activity_proto_msgTypes[15]
|
||||
mi := &file_activity_proto_msgTypes[19]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
@ -1272,7 +1524,7 @@ func (x *GetProgressRequest) String() string {
|
||||
func (*GetProgressRequest) ProtoMessage() {}
|
||||
|
||||
func (x *GetProgressRequest) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_activity_proto_msgTypes[15]
|
||||
mi := &file_activity_proto_msgTypes[19]
|
||||
if x != nil {
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
if ms.LoadMessageInfo() == nil {
|
||||
@ -1285,7 +1537,7 @@ func (x *GetProgressRequest) ProtoReflect() protoreflect.Message {
|
||||
|
||||
// Deprecated: Use GetProgressRequest.ProtoReflect.Descriptor instead.
|
||||
func (*GetProgressRequest) Descriptor() ([]byte, []int) {
|
||||
return file_activity_proto_rawDescGZIP(), []int{15}
|
||||
return file_activity_proto_rawDescGZIP(), []int{19}
|
||||
}
|
||||
|
||||
func (x *GetProgressRequest) GetActivityId() int64 {
|
||||
@ -1311,7 +1563,7 @@ type GetProgressResponse struct {
|
||||
|
||||
func (x *GetProgressResponse) Reset() {
|
||||
*x = GetProgressResponse{}
|
||||
mi := &file_activity_proto_msgTypes[16]
|
||||
mi := &file_activity_proto_msgTypes[20]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
@ -1323,7 +1575,7 @@ func (x *GetProgressResponse) String() string {
|
||||
func (*GetProgressResponse) ProtoMessage() {}
|
||||
|
||||
func (x *GetProgressResponse) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_activity_proto_msgTypes[16]
|
||||
mi := &file_activity_proto_msgTypes[20]
|
||||
if x != nil {
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
if ms.LoadMessageInfo() == nil {
|
||||
@ -1336,7 +1588,7 @@ func (x *GetProgressResponse) ProtoReflect() protoreflect.Message {
|
||||
|
||||
// Deprecated: Use GetProgressResponse.ProtoReflect.Descriptor instead.
|
||||
func (*GetProgressResponse) Descriptor() ([]byte, []int) {
|
||||
return file_activity_proto_rawDescGZIP(), []int{16}
|
||||
return file_activity_proto_rawDescGZIP(), []int{20}
|
||||
}
|
||||
|
||||
func (x *GetProgressResponse) GetBase() *common.BaseResponse {
|
||||
@ -1407,7 +1659,7 @@ type MintingActivity struct {
|
||||
|
||||
func (x *MintingActivity) Reset() {
|
||||
*x = MintingActivity{}
|
||||
mi := &file_activity_proto_msgTypes[17]
|
||||
mi := &file_activity_proto_msgTypes[21]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
@ -1419,7 +1671,7 @@ func (x *MintingActivity) String() string {
|
||||
func (*MintingActivity) ProtoMessage() {}
|
||||
|
||||
func (x *MintingActivity) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_activity_proto_msgTypes[17]
|
||||
mi := &file_activity_proto_msgTypes[21]
|
||||
if x != nil {
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
if ms.LoadMessageInfo() == nil {
|
||||
@ -1432,7 +1684,7 @@ func (x *MintingActivity) ProtoReflect() protoreflect.Message {
|
||||
|
||||
// Deprecated: Use MintingActivity.ProtoReflect.Descriptor instead.
|
||||
func (*MintingActivity) Descriptor() ([]byte, []int) {
|
||||
return file_activity_proto_rawDescGZIP(), []int{17}
|
||||
return file_activity_proto_rawDescGZIP(), []int{21}
|
||||
}
|
||||
|
||||
func (x *MintingActivity) GetId() int64 {
|
||||
@ -1517,7 +1769,7 @@ type GetMintingActivitiesRequest struct {
|
||||
|
||||
func (x *GetMintingActivitiesRequest) Reset() {
|
||||
*x = GetMintingActivitiesRequest{}
|
||||
mi := &file_activity_proto_msgTypes[18]
|
||||
mi := &file_activity_proto_msgTypes[22]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
@ -1529,7 +1781,7 @@ func (x *GetMintingActivitiesRequest) String() string {
|
||||
func (*GetMintingActivitiesRequest) ProtoMessage() {}
|
||||
|
||||
func (x *GetMintingActivitiesRequest) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_activity_proto_msgTypes[18]
|
||||
mi := &file_activity_proto_msgTypes[22]
|
||||
if x != nil {
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
if ms.LoadMessageInfo() == nil {
|
||||
@ -1542,7 +1794,7 @@ func (x *GetMintingActivitiesRequest) ProtoReflect() protoreflect.Message {
|
||||
|
||||
// Deprecated: Use GetMintingActivitiesRequest.ProtoReflect.Descriptor instead.
|
||||
func (*GetMintingActivitiesRequest) Descriptor() ([]byte, []int) {
|
||||
return file_activity_proto_rawDescGZIP(), []int{18}
|
||||
return file_activity_proto_rawDescGZIP(), []int{22}
|
||||
}
|
||||
|
||||
func (x *GetMintingActivitiesRequest) GetStarId() int64 {
|
||||
@ -1580,7 +1832,7 @@ type GetMintingActivitiesResponse struct {
|
||||
|
||||
func (x *GetMintingActivitiesResponse) Reset() {
|
||||
*x = GetMintingActivitiesResponse{}
|
||||
mi := &file_activity_proto_msgTypes[19]
|
||||
mi := &file_activity_proto_msgTypes[23]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
@ -1592,7 +1844,7 @@ func (x *GetMintingActivitiesResponse) String() string {
|
||||
func (*GetMintingActivitiesResponse) ProtoMessage() {}
|
||||
|
||||
func (x *GetMintingActivitiesResponse) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_activity_proto_msgTypes[19]
|
||||
mi := &file_activity_proto_msgTypes[23]
|
||||
if x != nil {
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
if ms.LoadMessageInfo() == nil {
|
||||
@ -1605,7 +1857,7 @@ func (x *GetMintingActivitiesResponse) ProtoReflect() protoreflect.Message {
|
||||
|
||||
// Deprecated: Use GetMintingActivitiesResponse.ProtoReflect.Descriptor instead.
|
||||
func (*GetMintingActivitiesResponse) Descriptor() ([]byte, []int) {
|
||||
return file_activity_proto_rawDescGZIP(), []int{19}
|
||||
return file_activity_proto_rawDescGZIP(), []int{23}
|
||||
}
|
||||
|
||||
func (x *GetMintingActivitiesResponse) GetBase() *common.BaseResponse {
|
||||
@ -1656,7 +1908,7 @@ type GetLatestContributionsRequest struct {
|
||||
|
||||
func (x *GetLatestContributionsRequest) Reset() {
|
||||
*x = GetLatestContributionsRequest{}
|
||||
mi := &file_activity_proto_msgTypes[20]
|
||||
mi := &file_activity_proto_msgTypes[24]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
@ -1668,7 +1920,7 @@ func (x *GetLatestContributionsRequest) String() string {
|
||||
func (*GetLatestContributionsRequest) ProtoMessage() {}
|
||||
|
||||
func (x *GetLatestContributionsRequest) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_activity_proto_msgTypes[20]
|
||||
mi := &file_activity_proto_msgTypes[24]
|
||||
if x != nil {
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
if ms.LoadMessageInfo() == nil {
|
||||
@ -1681,7 +1933,7 @@ func (x *GetLatestContributionsRequest) ProtoReflect() protoreflect.Message {
|
||||
|
||||
// Deprecated: Use GetLatestContributionsRequest.ProtoReflect.Descriptor instead.
|
||||
func (*GetLatestContributionsRequest) Descriptor() ([]byte, []int) {
|
||||
return file_activity_proto_rawDescGZIP(), []int{20}
|
||||
return file_activity_proto_rawDescGZIP(), []int{24}
|
||||
}
|
||||
|
||||
func (x *GetLatestContributionsRequest) GetActivityId() int64 {
|
||||
@ -1733,7 +1985,7 @@ type ContributionRecord struct {
|
||||
|
||||
func (x *ContributionRecord) Reset() {
|
||||
*x = ContributionRecord{}
|
||||
mi := &file_activity_proto_msgTypes[21]
|
||||
mi := &file_activity_proto_msgTypes[25]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
@ -1745,7 +1997,7 @@ func (x *ContributionRecord) String() string {
|
||||
func (*ContributionRecord) ProtoMessage() {}
|
||||
|
||||
func (x *ContributionRecord) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_activity_proto_msgTypes[21]
|
||||
mi := &file_activity_proto_msgTypes[25]
|
||||
if x != nil {
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
if ms.LoadMessageInfo() == nil {
|
||||
@ -1758,7 +2010,7 @@ func (x *ContributionRecord) ProtoReflect() protoreflect.Message {
|
||||
|
||||
// Deprecated: Use ContributionRecord.ProtoReflect.Descriptor instead.
|
||||
func (*ContributionRecord) Descriptor() ([]byte, []int) {
|
||||
return file_activity_proto_rawDescGZIP(), []int{21}
|
||||
return file_activity_proto_rawDescGZIP(), []int{25}
|
||||
}
|
||||
|
||||
func (x *ContributionRecord) GetId() int64 {
|
||||
@ -1856,7 +2108,7 @@ type GetLatestContributionsResponse struct {
|
||||
|
||||
func (x *GetLatestContributionsResponse) Reset() {
|
||||
*x = GetLatestContributionsResponse{}
|
||||
mi := &file_activity_proto_msgTypes[22]
|
||||
mi := &file_activity_proto_msgTypes[26]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
@ -1868,7 +2120,7 @@ func (x *GetLatestContributionsResponse) String() string {
|
||||
func (*GetLatestContributionsResponse) ProtoMessage() {}
|
||||
|
||||
func (x *GetLatestContributionsResponse) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_activity_proto_msgTypes[22]
|
||||
mi := &file_activity_proto_msgTypes[26]
|
||||
if x != nil {
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
if ms.LoadMessageInfo() == nil {
|
||||
@ -1881,7 +2133,7 @@ func (x *GetLatestContributionsResponse) ProtoReflect() protoreflect.Message {
|
||||
|
||||
// Deprecated: Use GetLatestContributionsResponse.ProtoReflect.Descriptor instead.
|
||||
func (*GetLatestContributionsResponse) Descriptor() ([]byte, []int) {
|
||||
return file_activity_proto_rawDescGZIP(), []int{22}
|
||||
return file_activity_proto_rawDescGZIP(), []int{26}
|
||||
}
|
||||
|
||||
func (x *GetLatestContributionsResponse) GetBase() *common.BaseResponse {
|
||||
@ -1914,7 +2166,7 @@ type ActivityMessage struct {
|
||||
|
||||
func (x *ActivityMessage) Reset() {
|
||||
*x = ActivityMessage{}
|
||||
mi := &file_activity_proto_msgTypes[23]
|
||||
mi := &file_activity_proto_msgTypes[27]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
@ -1926,7 +2178,7 @@ func (x *ActivityMessage) String() string {
|
||||
func (*ActivityMessage) ProtoMessage() {}
|
||||
|
||||
func (x *ActivityMessage) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_activity_proto_msgTypes[23]
|
||||
mi := &file_activity_proto_msgTypes[27]
|
||||
if x != nil {
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
if ms.LoadMessageInfo() == nil {
|
||||
@ -1939,7 +2191,7 @@ func (x *ActivityMessage) ProtoReflect() protoreflect.Message {
|
||||
|
||||
// Deprecated: Use ActivityMessage.ProtoReflect.Descriptor instead.
|
||||
func (*ActivityMessage) Descriptor() ([]byte, []int) {
|
||||
return file_activity_proto_rawDescGZIP(), []int{23}
|
||||
return file_activity_proto_rawDescGZIP(), []int{27}
|
||||
}
|
||||
|
||||
func (x *ActivityMessage) GetId() int64 {
|
||||
@ -2009,7 +2261,7 @@ type ListActivityMessagesRequest struct {
|
||||
|
||||
func (x *ListActivityMessagesRequest) Reset() {
|
||||
*x = ListActivityMessagesRequest{}
|
||||
mi := &file_activity_proto_msgTypes[24]
|
||||
mi := &file_activity_proto_msgTypes[28]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
@ -2021,7 +2273,7 @@ func (x *ListActivityMessagesRequest) String() string {
|
||||
func (*ListActivityMessagesRequest) ProtoMessage() {}
|
||||
|
||||
func (x *ListActivityMessagesRequest) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_activity_proto_msgTypes[24]
|
||||
mi := &file_activity_proto_msgTypes[28]
|
||||
if x != nil {
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
if ms.LoadMessageInfo() == nil {
|
||||
@ -2034,7 +2286,7 @@ func (x *ListActivityMessagesRequest) ProtoReflect() protoreflect.Message {
|
||||
|
||||
// Deprecated: Use ListActivityMessagesRequest.ProtoReflect.Descriptor instead.
|
||||
func (*ListActivityMessagesRequest) Descriptor() ([]byte, []int) {
|
||||
return file_activity_proto_rawDescGZIP(), []int{24}
|
||||
return file_activity_proto_rawDescGZIP(), []int{28}
|
||||
}
|
||||
|
||||
func (x *ListActivityMessagesRequest) GetActivityId() int64 {
|
||||
@ -2071,7 +2323,7 @@ type ListActivityMessagesResponse struct {
|
||||
|
||||
func (x *ListActivityMessagesResponse) Reset() {
|
||||
*x = ListActivityMessagesResponse{}
|
||||
mi := &file_activity_proto_msgTypes[25]
|
||||
mi := &file_activity_proto_msgTypes[29]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
@ -2083,7 +2335,7 @@ func (x *ListActivityMessagesResponse) String() string {
|
||||
func (*ListActivityMessagesResponse) ProtoMessage() {}
|
||||
|
||||
func (x *ListActivityMessagesResponse) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_activity_proto_msgTypes[25]
|
||||
mi := &file_activity_proto_msgTypes[29]
|
||||
if x != nil {
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
if ms.LoadMessageInfo() == nil {
|
||||
@ -2096,7 +2348,7 @@ func (x *ListActivityMessagesResponse) ProtoReflect() protoreflect.Message {
|
||||
|
||||
// Deprecated: Use ListActivityMessagesResponse.ProtoReflect.Descriptor instead.
|
||||
func (*ListActivityMessagesResponse) Descriptor() ([]byte, []int) {
|
||||
return file_activity_proto_rawDescGZIP(), []int{25}
|
||||
return file_activity_proto_rawDescGZIP(), []int{29}
|
||||
}
|
||||
|
||||
func (x *ListActivityMessagesResponse) GetBase() *common.BaseResponse {
|
||||
@ -2146,7 +2398,7 @@ type CreateActivityMessageRequest struct {
|
||||
|
||||
func (x *CreateActivityMessageRequest) Reset() {
|
||||
*x = CreateActivityMessageRequest{}
|
||||
mi := &file_activity_proto_msgTypes[26]
|
||||
mi := &file_activity_proto_msgTypes[30]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
@ -2158,7 +2410,7 @@ func (x *CreateActivityMessageRequest) String() string {
|
||||
func (*CreateActivityMessageRequest) ProtoMessage() {}
|
||||
|
||||
func (x *CreateActivityMessageRequest) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_activity_proto_msgTypes[26]
|
||||
mi := &file_activity_proto_msgTypes[30]
|
||||
if x != nil {
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
if ms.LoadMessageInfo() == nil {
|
||||
@ -2171,7 +2423,7 @@ func (x *CreateActivityMessageRequest) ProtoReflect() protoreflect.Message {
|
||||
|
||||
// Deprecated: Use CreateActivityMessageRequest.ProtoReflect.Descriptor instead.
|
||||
func (*CreateActivityMessageRequest) Descriptor() ([]byte, []int) {
|
||||
return file_activity_proto_rawDescGZIP(), []int{26}
|
||||
return file_activity_proto_rawDescGZIP(), []int{30}
|
||||
}
|
||||
|
||||
func (x *CreateActivityMessageRequest) GetActivityId() int64 {
|
||||
@ -2212,7 +2464,7 @@ type CreateActivityMessageResponse struct {
|
||||
|
||||
func (x *CreateActivityMessageResponse) Reset() {
|
||||
*x = CreateActivityMessageResponse{}
|
||||
mi := &file_activity_proto_msgTypes[27]
|
||||
mi := &file_activity_proto_msgTypes[31]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
@ -2224,7 +2476,7 @@ func (x *CreateActivityMessageResponse) String() string {
|
||||
func (*CreateActivityMessageResponse) ProtoMessage() {}
|
||||
|
||||
func (x *CreateActivityMessageResponse) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_activity_proto_msgTypes[27]
|
||||
mi := &file_activity_proto_msgTypes[31]
|
||||
if x != nil {
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
if ms.LoadMessageInfo() == nil {
|
||||
@ -2237,7 +2489,7 @@ func (x *CreateActivityMessageResponse) ProtoReflect() protoreflect.Message {
|
||||
|
||||
// Deprecated: Use CreateActivityMessageResponse.ProtoReflect.Descriptor instead.
|
||||
func (*CreateActivityMessageResponse) Descriptor() ([]byte, []int) {
|
||||
return file_activity_proto_rawDescGZIP(), []int{27}
|
||||
return file_activity_proto_rawDescGZIP(), []int{31}
|
||||
}
|
||||
|
||||
func (x *CreateActivityMessageResponse) GetBase() *common.BaseResponse {
|
||||
@ -2355,7 +2607,27 @@ const file_activity_proto_rawDesc = "" +
|
||||
"\x06status\x18\x04 \x01(\tR\x06status\x12\x1a\n" +
|
||||
"\bnickname\x18\x05 \x01(\tR\bnickname\x12\x1d\n" +
|
||||
"\n" +
|
||||
"avatar_url\x18\x06 \x01(\tR\tavatarUrl\"z\n" +
|
||||
"avatar_url\x18\x06 \x01(\tR\tavatarUrl\"f\n" +
|
||||
"\x11TopRankingRequest\x12\x1f\n" +
|
||||
"\vactivity_id\x18\x01 \x01(\x03R\n" +
|
||||
"activityId\x12\x17\n" +
|
||||
"\astar_id\x18\x02 \x01(\x03R\x06starId\x12\x17\n" +
|
||||
"\auser_id\x18\x03 \x01(\x03R\x06userId\"\\\n" +
|
||||
"\x0eTopRankingItem\x12\x12\n" +
|
||||
"\x04rank\x18\x01 \x01(\x05R\x04rank\x12\x17\n" +
|
||||
"\auser_id\x18\x02 \x01(\x03R\x06userId\x12\x1d\n" +
|
||||
"\n" +
|
||||
"avatar_url\x18\x03 \x01(\tR\tavatarUrl\"}\n" +
|
||||
"\x10MyTopRankingInfo\x12\x12\n" +
|
||||
"\x04rank\x18\x01 \x01(\x05R\x04rank\x12\x1d\n" +
|
||||
"\n" +
|
||||
"avatar_url\x18\x02 \x01(\tR\tavatarUrl\x12\x1e\n" +
|
||||
"\vgap_to_prev\x18\x03 \x01(\x03R\tgapToPrev\x12\x16\n" +
|
||||
"\x06status\x18\x04 \x01(\tR\x06status\"\xb9\x01\n" +
|
||||
"\x12TopRankingResponse\x120\n" +
|
||||
"\x04base\x18\x01 \x01(\v2\x1c.topfans.common.BaseResponseR\x04base\x124\n" +
|
||||
"\x04top3\x18\x02 \x03(\v2 .topfans.activity.TopRankingItemR\x04top3\x12;\n" +
|
||||
"\amy_info\x18\x03 \x01(\v2\".topfans.activity.MyTopRankingInfoR\x06myInfo\"z\n" +
|
||||
"\x16GetActivityListRequest\x12\x17\n" +
|
||||
"\astar_id\x18\x01 \x01(\x03R\x06starId\x12\x16\n" +
|
||||
"\x06status\x18\x02 \x01(\tR\x06status\x12\x12\n" +
|
||||
@ -2465,7 +2737,7 @@ const file_activity_proto_rawDesc = "" +
|
||||
"\acontent\x18\x04 \x01(\tR\acontent\"\x8e\x01\n" +
|
||||
"\x1dCreateActivityMessageResponse\x120\n" +
|
||||
"\x04base\x18\x01 \x01(\v2\x1c.topfans.common.BaseResponseR\x04base\x12;\n" +
|
||||
"\amessage\x18\x02 \x01(\v2!.topfans.activity.ActivityMessageR\amessage2\xd5\r\n" +
|
||||
"\amessage\x18\x02 \x01(\v2!.topfans.activity.ActivityMessageR\amessage2\xe8\x0e\n" +
|
||||
"\x0fActivityService\x12\x82\x01\n" +
|
||||
"\x0fGetActivityList\x12(.topfans.activity.GetActivityListRequest\x1a).topfans.activity.GetActivityListResponse\"\x1a\x82\xd3\xe4\x93\x02\x14\x12\x12/api/v1/activities\x12y\n" +
|
||||
"\vGetActivity\x12$.topfans.activity.GetProgressRequest\x1a\x1a.topfans.activity.Activity\"(\x82\xd3\xe4\x93\x02\"\x12 /api/v1/activities/{activity_id}\x12\x91\x01\n" +
|
||||
@ -2473,7 +2745,8 @@ const file_activity_proto_rawDesc = "" +
|
||||
"\vGetProgress\x12$.topfans.activity.GetProgressRequest\x1a%.topfans.activity.GetProgressResponse\"1\x82\xd3\xe4\x93\x02+\x12)/api/v1/activities/{activity_id}/progress\x12\x93\x01\n" +
|
||||
"\fPurchaseItem\x12%.topfans.activity.PurchaseItemRequest\x1a&.topfans.activity.PurchaseItemResponse\"4\x82\xd3\xe4\x93\x02.:\x01*\")/api/v1/activities/{activity_id}/purchase\x12\xa8\x01\n" +
|
||||
"\x11BatchPurchaseItem\x12*.topfans.activity.BatchPurchaseItemRequest\x1a+.topfans.activity.BatchPurchaseItemResponse\":\x82\xd3\xe4\x93\x024:\x01*\"//api/v1/activities/{activity_id}/batch-purchase\x12\xa7\x01\n" +
|
||||
"\x16GetContributionRanking\x12,.topfans.activity.ContributionRankingRequest\x1a-.topfans.activity.ContributionRankingResponse\"0\x82\xd3\xe4\x93\x02*\x12(/api/v1/activities/{activity_id}/ranking\x12\x99\x01\n" +
|
||||
"\x16GetContributionRanking\x12,.topfans.activity.ContributionRankingRequest\x1a-.topfans.activity.ContributionRankingResponse\"0\x82\xd3\xe4\x93\x02*\x12(/api/v1/activities/{activity_id}/ranking\x12\x90\x01\n" +
|
||||
"\rGetTopRanking\x12#.topfans.activity.TopRankingRequest\x1a$.topfans.activity.TopRankingResponse\"4\x82\xd3\xe4\x93\x02.\x12,/api/v1/activities/{activity_id}/top-ranking\x12\x99\x01\n" +
|
||||
"\x14GetMintingActivities\x12-.topfans.activity.GetMintingActivitiesRequest\x1a..topfans.activity.GetMintingActivitiesResponse\"\"\x82\xd3\xe4\x93\x02\x1c\x12\x1a/api/v1/minting-activities\x12\xba\x01\n" +
|
||||
"\x16GetLatestContributions\x12/.topfans.activity.GetLatestContributionsRequest\x1a0.topfans.activity.GetLatestContributionsResponse\"=\x82\xd3\xe4\x93\x027\x125/api/v1/activities/{activity_id}/contributions/latest\x12\xa8\x01\n" +
|
||||
"\x14ListActivityMessages\x12-.topfans.activity.ListActivityMessagesRequest\x1a..topfans.activity.ListActivityMessagesResponse\"1\x82\xd3\xe4\x93\x02+\x12)/api/v1/activities/{activity_id}/messages\x12\xae\x01\n" +
|
||||
@ -2491,7 +2764,7 @@ func file_activity_proto_rawDescGZIP() []byte {
|
||||
return file_activity_proto_rawDescData
|
||||
}
|
||||
|
||||
var file_activity_proto_msgTypes = make([]protoimpl.MessageInfo, 28)
|
||||
var file_activity_proto_msgTypes = make([]protoimpl.MessageInfo, 32)
|
||||
var file_activity_proto_goTypes = []any{
|
||||
(*Activity)(nil), // 0: topfans.activity.Activity
|
||||
(*ActivityItem)(nil), // 1: topfans.activity.ActivityItem
|
||||
@ -2506,71 +2779,80 @@ var file_activity_proto_goTypes = []any{
|
||||
(*ContributionRankingItem)(nil), // 10: topfans.activity.ContributionRankingItem
|
||||
(*ContributionRankingResponse)(nil), // 11: topfans.activity.ContributionRankingResponse
|
||||
(*MyContribution)(nil), // 12: topfans.activity.MyContribution
|
||||
(*GetActivityListRequest)(nil), // 13: topfans.activity.GetActivityListRequest
|
||||
(*GetActivityListResponse)(nil), // 14: topfans.activity.GetActivityListResponse
|
||||
(*GetProgressRequest)(nil), // 15: topfans.activity.GetProgressRequest
|
||||
(*GetProgressResponse)(nil), // 16: topfans.activity.GetProgressResponse
|
||||
(*MintingActivity)(nil), // 17: topfans.activity.MintingActivity
|
||||
(*GetMintingActivitiesRequest)(nil), // 18: topfans.activity.GetMintingActivitiesRequest
|
||||
(*GetMintingActivitiesResponse)(nil), // 19: topfans.activity.GetMintingActivitiesResponse
|
||||
(*GetLatestContributionsRequest)(nil), // 20: topfans.activity.GetLatestContributionsRequest
|
||||
(*ContributionRecord)(nil), // 21: topfans.activity.ContributionRecord
|
||||
(*GetLatestContributionsResponse)(nil), // 22: topfans.activity.GetLatestContributionsResponse
|
||||
(*ActivityMessage)(nil), // 23: topfans.activity.ActivityMessage
|
||||
(*ListActivityMessagesRequest)(nil), // 24: topfans.activity.ListActivityMessagesRequest
|
||||
(*ListActivityMessagesResponse)(nil), // 25: topfans.activity.ListActivityMessagesResponse
|
||||
(*CreateActivityMessageRequest)(nil), // 26: topfans.activity.CreateActivityMessageRequest
|
||||
(*CreateActivityMessageResponse)(nil), // 27: topfans.activity.CreateActivityMessageResponse
|
||||
(*common.BaseResponse)(nil), // 28: topfans.common.BaseResponse
|
||||
(*TopRankingRequest)(nil), // 13: topfans.activity.TopRankingRequest
|
||||
(*TopRankingItem)(nil), // 14: topfans.activity.TopRankingItem
|
||||
(*MyTopRankingInfo)(nil), // 15: topfans.activity.MyTopRankingInfo
|
||||
(*TopRankingResponse)(nil), // 16: topfans.activity.TopRankingResponse
|
||||
(*GetActivityListRequest)(nil), // 17: topfans.activity.GetActivityListRequest
|
||||
(*GetActivityListResponse)(nil), // 18: topfans.activity.GetActivityListResponse
|
||||
(*GetProgressRequest)(nil), // 19: topfans.activity.GetProgressRequest
|
||||
(*GetProgressResponse)(nil), // 20: topfans.activity.GetProgressResponse
|
||||
(*MintingActivity)(nil), // 21: topfans.activity.MintingActivity
|
||||
(*GetMintingActivitiesRequest)(nil), // 22: topfans.activity.GetMintingActivitiesRequest
|
||||
(*GetMintingActivitiesResponse)(nil), // 23: topfans.activity.GetMintingActivitiesResponse
|
||||
(*GetLatestContributionsRequest)(nil), // 24: topfans.activity.GetLatestContributionsRequest
|
||||
(*ContributionRecord)(nil), // 25: topfans.activity.ContributionRecord
|
||||
(*GetLatestContributionsResponse)(nil), // 26: topfans.activity.GetLatestContributionsResponse
|
||||
(*ActivityMessage)(nil), // 27: topfans.activity.ActivityMessage
|
||||
(*ListActivityMessagesRequest)(nil), // 28: topfans.activity.ListActivityMessagesRequest
|
||||
(*ListActivityMessagesResponse)(nil), // 29: topfans.activity.ListActivityMessagesResponse
|
||||
(*CreateActivityMessageRequest)(nil), // 30: topfans.activity.CreateActivityMessageRequest
|
||||
(*CreateActivityMessageResponse)(nil), // 31: topfans.activity.CreateActivityMessageResponse
|
||||
(*common.BaseResponse)(nil), // 32: topfans.common.BaseResponse
|
||||
}
|
||||
var file_activity_proto_depIdxs = []int32{
|
||||
1, // 0: topfans.activity.Activity.items:type_name -> topfans.activity.ActivityItem
|
||||
1, // 1: topfans.activity.ActivityItemsResponse.items:type_name -> topfans.activity.ActivityItem
|
||||
28, // 2: topfans.activity.PurchaseItemResponse.base:type_name -> topfans.common.BaseResponse
|
||||
32, // 2: topfans.activity.PurchaseItemResponse.base:type_name -> topfans.common.BaseResponse
|
||||
5, // 3: topfans.activity.BatchPurchaseItemRequest.items:type_name -> topfans.activity.PurchaseItem
|
||||
28, // 4: topfans.activity.BatchPurchaseItemResponse.base:type_name -> topfans.common.BaseResponse
|
||||
32, // 4: topfans.activity.BatchPurchaseItemResponse.base:type_name -> topfans.common.BaseResponse
|
||||
8, // 5: topfans.activity.BatchPurchaseItemResponse.fails:type_name -> topfans.activity.PurchaseFailItem
|
||||
28, // 6: topfans.activity.ContributionRankingResponse.base:type_name -> topfans.common.BaseResponse
|
||||
32, // 6: topfans.activity.ContributionRankingResponse.base:type_name -> topfans.common.BaseResponse
|
||||
10, // 7: topfans.activity.ContributionRankingResponse.items:type_name -> topfans.activity.ContributionRankingItem
|
||||
12, // 8: topfans.activity.ContributionRankingResponse.my_contribution:type_name -> topfans.activity.MyContribution
|
||||
28, // 9: topfans.activity.GetActivityListResponse.base:type_name -> topfans.common.BaseResponse
|
||||
0, // 10: topfans.activity.GetActivityListResponse.activities:type_name -> topfans.activity.Activity
|
||||
28, // 11: topfans.activity.GetProgressResponse.base:type_name -> topfans.common.BaseResponse
|
||||
28, // 12: topfans.activity.GetMintingActivitiesResponse.base:type_name -> topfans.common.BaseResponse
|
||||
17, // 13: topfans.activity.GetMintingActivitiesResponse.activities:type_name -> topfans.activity.MintingActivity
|
||||
28, // 14: topfans.activity.GetLatestContributionsResponse.base:type_name -> topfans.common.BaseResponse
|
||||
21, // 15: topfans.activity.GetLatestContributionsResponse.records:type_name -> topfans.activity.ContributionRecord
|
||||
28, // 16: topfans.activity.ListActivityMessagesResponse.base:type_name -> topfans.common.BaseResponse
|
||||
23, // 17: topfans.activity.ListActivityMessagesResponse.messages:type_name -> topfans.activity.ActivityMessage
|
||||
28, // 18: topfans.activity.CreateActivityMessageResponse.base:type_name -> topfans.common.BaseResponse
|
||||
23, // 19: topfans.activity.CreateActivityMessageResponse.message:type_name -> topfans.activity.ActivityMessage
|
||||
13, // 20: topfans.activity.ActivityService.GetActivityList:input_type -> topfans.activity.GetActivityListRequest
|
||||
15, // 21: topfans.activity.ActivityService.GetActivity:input_type -> topfans.activity.GetProgressRequest
|
||||
15, // 22: topfans.activity.ActivityService.GetActivityItems:input_type -> topfans.activity.GetProgressRequest
|
||||
15, // 23: topfans.activity.ActivityService.GetProgress:input_type -> topfans.activity.GetProgressRequest
|
||||
3, // 24: topfans.activity.ActivityService.PurchaseItem:input_type -> topfans.activity.PurchaseItemRequest
|
||||
6, // 25: topfans.activity.ActivityService.BatchPurchaseItem:input_type -> topfans.activity.BatchPurchaseItemRequest
|
||||
9, // 26: topfans.activity.ActivityService.GetContributionRanking:input_type -> topfans.activity.ContributionRankingRequest
|
||||
18, // 27: topfans.activity.ActivityService.GetMintingActivities:input_type -> topfans.activity.GetMintingActivitiesRequest
|
||||
20, // 28: topfans.activity.ActivityService.GetLatestContributions:input_type -> topfans.activity.GetLatestContributionsRequest
|
||||
24, // 29: topfans.activity.ActivityService.ListActivityMessages:input_type -> topfans.activity.ListActivityMessagesRequest
|
||||
26, // 30: topfans.activity.ActivityService.CreateActivityMessage:input_type -> topfans.activity.CreateActivityMessageRequest
|
||||
14, // 31: topfans.activity.ActivityService.GetActivityList:output_type -> topfans.activity.GetActivityListResponse
|
||||
0, // 32: topfans.activity.ActivityService.GetActivity:output_type -> topfans.activity.Activity
|
||||
2, // 33: topfans.activity.ActivityService.GetActivityItems:output_type -> topfans.activity.ActivityItemsResponse
|
||||
16, // 34: topfans.activity.ActivityService.GetProgress:output_type -> topfans.activity.GetProgressResponse
|
||||
4, // 35: topfans.activity.ActivityService.PurchaseItem:output_type -> topfans.activity.PurchaseItemResponse
|
||||
7, // 36: topfans.activity.ActivityService.BatchPurchaseItem:output_type -> topfans.activity.BatchPurchaseItemResponse
|
||||
11, // 37: topfans.activity.ActivityService.GetContributionRanking:output_type -> topfans.activity.ContributionRankingResponse
|
||||
19, // 38: topfans.activity.ActivityService.GetMintingActivities:output_type -> topfans.activity.GetMintingActivitiesResponse
|
||||
22, // 39: topfans.activity.ActivityService.GetLatestContributions:output_type -> topfans.activity.GetLatestContributionsResponse
|
||||
25, // 40: topfans.activity.ActivityService.ListActivityMessages:output_type -> topfans.activity.ListActivityMessagesResponse
|
||||
27, // 41: topfans.activity.ActivityService.CreateActivityMessage:output_type -> topfans.activity.CreateActivityMessageResponse
|
||||
31, // [31:42] is the sub-list for method output_type
|
||||
20, // [20:31] is the sub-list for method input_type
|
||||
20, // [20:20] is the sub-list for extension type_name
|
||||
20, // [20:20] is the sub-list for extension extendee
|
||||
0, // [0:20] is the sub-list for field type_name
|
||||
32, // 9: topfans.activity.TopRankingResponse.base:type_name -> topfans.common.BaseResponse
|
||||
14, // 10: topfans.activity.TopRankingResponse.top3:type_name -> topfans.activity.TopRankingItem
|
||||
15, // 11: topfans.activity.TopRankingResponse.my_info:type_name -> topfans.activity.MyTopRankingInfo
|
||||
32, // 12: topfans.activity.GetActivityListResponse.base:type_name -> topfans.common.BaseResponse
|
||||
0, // 13: topfans.activity.GetActivityListResponse.activities:type_name -> topfans.activity.Activity
|
||||
32, // 14: topfans.activity.GetProgressResponse.base:type_name -> topfans.common.BaseResponse
|
||||
32, // 15: topfans.activity.GetMintingActivitiesResponse.base:type_name -> topfans.common.BaseResponse
|
||||
21, // 16: topfans.activity.GetMintingActivitiesResponse.activities:type_name -> topfans.activity.MintingActivity
|
||||
32, // 17: topfans.activity.GetLatestContributionsResponse.base:type_name -> topfans.common.BaseResponse
|
||||
25, // 18: topfans.activity.GetLatestContributionsResponse.records:type_name -> topfans.activity.ContributionRecord
|
||||
32, // 19: topfans.activity.ListActivityMessagesResponse.base:type_name -> topfans.common.BaseResponse
|
||||
27, // 20: topfans.activity.ListActivityMessagesResponse.messages:type_name -> topfans.activity.ActivityMessage
|
||||
32, // 21: topfans.activity.CreateActivityMessageResponse.base:type_name -> topfans.common.BaseResponse
|
||||
27, // 22: topfans.activity.CreateActivityMessageResponse.message:type_name -> topfans.activity.ActivityMessage
|
||||
17, // 23: topfans.activity.ActivityService.GetActivityList:input_type -> topfans.activity.GetActivityListRequest
|
||||
19, // 24: topfans.activity.ActivityService.GetActivity:input_type -> topfans.activity.GetProgressRequest
|
||||
19, // 25: topfans.activity.ActivityService.GetActivityItems:input_type -> topfans.activity.GetProgressRequest
|
||||
19, // 26: topfans.activity.ActivityService.GetProgress:input_type -> topfans.activity.GetProgressRequest
|
||||
3, // 27: topfans.activity.ActivityService.PurchaseItem:input_type -> topfans.activity.PurchaseItemRequest
|
||||
6, // 28: topfans.activity.ActivityService.BatchPurchaseItem:input_type -> topfans.activity.BatchPurchaseItemRequest
|
||||
9, // 29: topfans.activity.ActivityService.GetContributionRanking:input_type -> topfans.activity.ContributionRankingRequest
|
||||
13, // 30: topfans.activity.ActivityService.GetTopRanking:input_type -> topfans.activity.TopRankingRequest
|
||||
22, // 31: topfans.activity.ActivityService.GetMintingActivities:input_type -> topfans.activity.GetMintingActivitiesRequest
|
||||
24, // 32: topfans.activity.ActivityService.GetLatestContributions:input_type -> topfans.activity.GetLatestContributionsRequest
|
||||
28, // 33: topfans.activity.ActivityService.ListActivityMessages:input_type -> topfans.activity.ListActivityMessagesRequest
|
||||
30, // 34: topfans.activity.ActivityService.CreateActivityMessage:input_type -> topfans.activity.CreateActivityMessageRequest
|
||||
18, // 35: topfans.activity.ActivityService.GetActivityList:output_type -> topfans.activity.GetActivityListResponse
|
||||
0, // 36: topfans.activity.ActivityService.GetActivity:output_type -> topfans.activity.Activity
|
||||
2, // 37: topfans.activity.ActivityService.GetActivityItems:output_type -> topfans.activity.ActivityItemsResponse
|
||||
20, // 38: topfans.activity.ActivityService.GetProgress:output_type -> topfans.activity.GetProgressResponse
|
||||
4, // 39: topfans.activity.ActivityService.PurchaseItem:output_type -> topfans.activity.PurchaseItemResponse
|
||||
7, // 40: topfans.activity.ActivityService.BatchPurchaseItem:output_type -> topfans.activity.BatchPurchaseItemResponse
|
||||
11, // 41: topfans.activity.ActivityService.GetContributionRanking:output_type -> topfans.activity.ContributionRankingResponse
|
||||
16, // 42: topfans.activity.ActivityService.GetTopRanking:output_type -> topfans.activity.TopRankingResponse
|
||||
23, // 43: topfans.activity.ActivityService.GetMintingActivities:output_type -> topfans.activity.GetMintingActivitiesResponse
|
||||
26, // 44: topfans.activity.ActivityService.GetLatestContributions:output_type -> topfans.activity.GetLatestContributionsResponse
|
||||
29, // 45: topfans.activity.ActivityService.ListActivityMessages:output_type -> topfans.activity.ListActivityMessagesResponse
|
||||
31, // 46: topfans.activity.ActivityService.CreateActivityMessage:output_type -> topfans.activity.CreateActivityMessageResponse
|
||||
35, // [35:47] is the sub-list for method output_type
|
||||
23, // [23:35] is the sub-list for method input_type
|
||||
23, // [23:23] is the sub-list for extension type_name
|
||||
23, // [23:23] is the sub-list for extension extendee
|
||||
0, // [0:23] is the sub-list for field type_name
|
||||
}
|
||||
|
||||
func init() { file_activity_proto_init() }
|
||||
@ -2584,7 +2866,7 @@ func file_activity_proto_init() {
|
||||
GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
|
||||
RawDescriptor: unsafe.Slice(unsafe.StringData(file_activity_proto_rawDesc), len(file_activity_proto_rawDesc)),
|
||||
NumEnums: 0,
|
||||
NumMessages: 28,
|
||||
NumMessages: 32,
|
||||
NumExtensions: 0,
|
||||
NumServices: 1,
|
||||
},
|
||||
|
||||
@ -50,6 +50,8 @@ const (
|
||||
ActivityServiceBatchPurchaseItemProcedure = "/topfans.activity.ActivityService/BatchPurchaseItem"
|
||||
// ActivityServiceGetContributionRankingProcedure is the fully-qualified name of the ActivityService's GetContributionRanking RPC.
|
||||
ActivityServiceGetContributionRankingProcedure = "/topfans.activity.ActivityService/GetContributionRanking"
|
||||
// ActivityServiceGetTopRankingProcedure is the fully-qualified name of the ActivityService's GetTopRanking RPC.
|
||||
ActivityServiceGetTopRankingProcedure = "/topfans.activity.ActivityService/GetTopRanking"
|
||||
// ActivityServiceGetMintingActivitiesProcedure is the fully-qualified name of the ActivityService's GetMintingActivities RPC.
|
||||
ActivityServiceGetMintingActivitiesProcedure = "/topfans.activity.ActivityService/GetMintingActivities"
|
||||
// ActivityServiceGetLatestContributionsProcedure is the fully-qualified name of the ActivityService's GetLatestContributions RPC.
|
||||
@ -73,6 +75,7 @@ type ActivityService interface {
|
||||
PurchaseItem(ctx context.Context, req *PurchaseItemRequest, opts ...client.CallOption) (*PurchaseItemResponse, error)
|
||||
BatchPurchaseItem(ctx context.Context, req *BatchPurchaseItemRequest, opts ...client.CallOption) (*BatchPurchaseItemResponse, error)
|
||||
GetContributionRanking(ctx context.Context, req *ContributionRankingRequest, opts ...client.CallOption) (*ContributionRankingResponse, error)
|
||||
GetTopRanking(ctx context.Context, req *TopRankingRequest, opts ...client.CallOption) (*TopRankingResponse, error)
|
||||
GetMintingActivities(ctx context.Context, req *GetMintingActivitiesRequest, opts ...client.CallOption) (*GetMintingActivitiesResponse, error)
|
||||
GetLatestContributions(ctx context.Context, req *GetLatestContributionsRequest, opts ...client.CallOption) (*GetLatestContributionsResponse, error)
|
||||
ListActivityMessages(ctx context.Context, req *ListActivityMessagesRequest, opts ...client.CallOption) (*ListActivityMessagesResponse, error)
|
||||
@ -155,6 +158,14 @@ func (c *ActivityServiceImpl) GetContributionRanking(ctx context.Context, req *C
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
func (c *ActivityServiceImpl) GetTopRanking(ctx context.Context, req *TopRankingRequest, opts ...client.CallOption) (*TopRankingResponse, error) {
|
||||
resp := new(TopRankingResponse)
|
||||
if err := c.conn.CallUnary(ctx, []interface{}{req}, resp, "GetTopRanking", opts...); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
func (c *ActivityServiceImpl) GetMintingActivities(ctx context.Context, req *GetMintingActivitiesRequest, opts ...client.CallOption) (*GetMintingActivitiesResponse, error) {
|
||||
resp := new(GetMintingActivitiesResponse)
|
||||
if err := c.conn.CallUnary(ctx, []interface{}{req}, resp, "GetMintingActivities", opts...); err != nil {
|
||||
@ -189,7 +200,7 @@ func (c *ActivityServiceImpl) CreateActivityMessage(ctx context.Context, req *Cr
|
||||
|
||||
var ActivityService_ClientInfo = client.ClientInfo{
|
||||
InterfaceName: "topfans.activity.ActivityService",
|
||||
MethodNames: []string{"GetActivityList", "GetActivity", "GetActivityItems", "GetProgress", "PurchaseItem", "BatchPurchaseItem", "GetContributionRanking", "GetMintingActivities", "GetLatestContributions", "ListActivityMessages", "CreateActivityMessage"},
|
||||
MethodNames: []string{"GetActivityList", "GetActivity", "GetActivityItems", "GetProgress", "PurchaseItem", "BatchPurchaseItem", "GetContributionRanking", "GetTopRanking", "GetMintingActivities", "GetLatestContributions", "ListActivityMessages", "CreateActivityMessage"},
|
||||
ConnectionInjectFunc: func(dubboCliRaw interface{}, conn *client.Connection) {
|
||||
dubboCli := dubboCliRaw.(*ActivityServiceImpl)
|
||||
dubboCli.conn = conn
|
||||
@ -205,6 +216,7 @@ type ActivityServiceHandler interface {
|
||||
PurchaseItem(context.Context, *PurchaseItemRequest) (*PurchaseItemResponse, error)
|
||||
BatchPurchaseItem(context.Context, *BatchPurchaseItemRequest) (*BatchPurchaseItemResponse, error)
|
||||
GetContributionRanking(context.Context, *ContributionRankingRequest) (*ContributionRankingResponse, error)
|
||||
GetTopRanking(context.Context, *TopRankingRequest) (*TopRankingResponse, error)
|
||||
GetMintingActivities(context.Context, *GetMintingActivitiesRequest) (*GetMintingActivitiesResponse, error)
|
||||
GetLatestContributions(context.Context, *GetLatestContributionsRequest) (*GetLatestContributionsResponse, error)
|
||||
ListActivityMessages(context.Context, *ListActivityMessagesRequest) (*ListActivityMessagesResponse, error)
|
||||
@ -328,6 +340,21 @@ var ActivityService_ServiceInfo = server.ServiceInfo{
|
||||
return triple_protocol.NewResponse(res), nil
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "GetTopRanking",
|
||||
Type: constant.CallUnary,
|
||||
ReqInitFunc: func() interface{} {
|
||||
return new(TopRankingRequest)
|
||||
},
|
||||
MethodFunc: func(ctx context.Context, args []interface{}, handler interface{}) (interface{}, error) {
|
||||
req := args[0].(*TopRankingRequest)
|
||||
res, err := handler.(ActivityServiceHandler).GetTopRanking(ctx, req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return triple_protocol.NewResponse(res), nil
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "GetMintingActivities",
|
||||
Type: constant.CallUnary,
|
||||
|
||||
@ -136,6 +136,35 @@ message MyContribution {
|
||||
string avatar_url = 6;
|
||||
}
|
||||
|
||||
// 活动 TOP3 + 我的排名请求(专用轻量接口)
|
||||
message TopRankingRequest {
|
||||
int64 activity_id = 1;
|
||||
int64 star_id = 2; // 0 表示不按明星过滤
|
||||
int64 user_id = 3; // 来自 token
|
||||
}
|
||||
|
||||
// TOP3 排名项
|
||||
message TopRankingItem {
|
||||
int32 rank = 1;
|
||||
int64 user_id = 2;
|
||||
string avatar_url = 3;
|
||||
}
|
||||
|
||||
// 我的 TOP 排名信息
|
||||
message MyTopRankingInfo {
|
||||
int32 rank = 1; // 0 表示未上榜
|
||||
string avatar_url = 2;
|
||||
int64 gap_to_prev = 3;
|
||||
string status = 4; // "ranked" | "unranked"
|
||||
}
|
||||
|
||||
// TOP 排名响应
|
||||
message TopRankingResponse {
|
||||
topfans.common.BaseResponse base = 1;
|
||||
repeated TopRankingItem top3 = 2;
|
||||
MyTopRankingInfo my_info = 3;
|
||||
}
|
||||
|
||||
// 活动列表请求
|
||||
message GetActivityListRequest {
|
||||
int64 star_id = 1;
|
||||
@ -322,6 +351,13 @@ service ActivityService {
|
||||
};
|
||||
}
|
||||
|
||||
// 获取活动 TOP3 + 我的排名(专用轻量接口)
|
||||
rpc GetTopRanking(TopRankingRequest) returns (TopRankingResponse) {
|
||||
option (google.api.http) = {
|
||||
get: "/api/v1/activities/{activity_id}/top-ranking"
|
||||
};
|
||||
}
|
||||
|
||||
// 获取铸造活动列表(用于运营banner)
|
||||
rpc GetMintingActivities(GetMintingActivitiesRequest) returns (GetMintingActivitiesResponse) {
|
||||
option (google.api.http) = {
|
||||
|
||||
@ -223,6 +223,39 @@ func (p *ActivityProvider) GetContributionRanking(ctx context.Context, req *pb.C
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
// GetTopRanking 获取活动 TOP3 + 我的排名(专用轻量接口)
|
||||
func (p *ActivityProvider) GetTopRanking(ctx context.Context, req *pb.TopRankingRequest) (*pb.TopRankingResponse, error) {
|
||||
logger.Logger.Info("Received GetTopRanking request",
|
||||
zap.Int64("activity_id", req.ActivityId),
|
||||
zap.Int64("star_id", req.StarId),
|
||||
zap.Int64("user_id", req.UserId),
|
||||
)
|
||||
|
||||
resp, err := p.activityService.GetTopRanking(ctx, req)
|
||||
if err != nil {
|
||||
logger.Logger.Error("GetTopRanking failed", zap.Error(err))
|
||||
return &pb.TopRankingResponse{
|
||||
Base: &pbCommon.BaseResponse{
|
||||
Code: uint32(appErrors.ToGRPCCode(err)),
|
||||
Message: err.Error(),
|
||||
Timestamp: time.Now().UnixMilli(),
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
|
||||
logger.Logger.Debug("GetTopRanking successful",
|
||||
zap.Int("top3_count", len(resp.Top3)),
|
||||
zap.Int32("my_rank", func() int32 {
|
||||
if resp.MyInfo != nil {
|
||||
return resp.MyInfo.Rank
|
||||
}
|
||||
return 0
|
||||
}()),
|
||||
)
|
||||
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
// GetMintingActivities 获取铸造活动列表(用于运营banner)
|
||||
func (p *ActivityProvider) GetMintingActivities(ctx context.Context, req *pb.GetMintingActivitiesRequest) (*pb.GetMintingActivitiesResponse, error) {
|
||||
logger.Logger.Info("Received GetMintingActivities request",
|
||||
|
||||
@ -45,6 +45,18 @@ type ActivityRepository interface {
|
||||
|
||||
// GetLatestContributions 获取最新贡献记录(用于实时显示)
|
||||
GetLatestContributions(activityID int64, sinceTimestamp int64, sinceID int64, limit int) ([]*models.ActivityContribution, error)
|
||||
|
||||
// GetTop3 获取活动 TOP3(专用轻量接口)
|
||||
// 返回 0~3 行,按 total_contribution DESC, id ASC 排序
|
||||
GetTop3(activityID, starID int64) ([]*models.ActivityUserStats, error)
|
||||
|
||||
// GetUserStatsForRanking 获取用户在活动中的统计(返回 nil 表示未参与)
|
||||
GetUserStatsForRanking(activityID, userID, starID int64) (*models.ActivityUserStats, error)
|
||||
|
||||
// GetUserStatsByRank 获取活动指定排名位置的统计;offset 从 0 开始
|
||||
// rank=N 时,offset=N-1
|
||||
// 用于"我排名 N 时,查 N-1 名"的场景
|
||||
GetUserStatsByRank(activityID, starID int64, offset int) (*models.ActivityUserStats, error)
|
||||
}
|
||||
|
||||
// activityRepository Activity仓库实现
|
||||
@ -291,6 +303,73 @@ func (r *activityRepository) GetUserRank(userID, activityID, starID int64) (int,
|
||||
return int(count) + 1, nil
|
||||
}
|
||||
|
||||
// GetTop3 获取活动 TOP3(按 total_contribution DESC, id ASC;starID<=0 不过滤)
|
||||
func (r *activityRepository) GetTop3(activityID, starID int64) ([]*models.ActivityUserStats, error) {
|
||||
if activityID <= 0 {
|
||||
return nil, errors.New("activity_id must be greater than 0")
|
||||
}
|
||||
|
||||
query := r.db.Model(&models.ActivityUserStats{}).Where("activity_id = ?", activityID)
|
||||
if starID > 0 {
|
||||
query = query.Where("star_id = ?", starID)
|
||||
}
|
||||
|
||||
var stats []*models.ActivityUserStats
|
||||
if err := query.Order("total_contribution DESC, id ASC").Limit(3).Find(&stats).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return stats, nil
|
||||
}
|
||||
|
||||
// GetUserStatsForRanking 获取用户在活动中的统计;未找到返回 (nil, nil)
|
||||
func (r *activityRepository) GetUserStatsForRanking(activityID, userID, starID int64) (*models.ActivityUserStats, error) {
|
||||
if activityID <= 0 || userID <= 0 {
|
||||
return nil, errors.New("activity_id and user_id must be greater than 0")
|
||||
}
|
||||
|
||||
query := r.db.Where("activity_id = ? AND user_id = ?", activityID, userID)
|
||||
if starID > 0 {
|
||||
query = query.Where("star_id = ?", starID)
|
||||
}
|
||||
|
||||
var stats models.ActivityUserStats
|
||||
if err := query.First(&stats).Error; err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return nil, nil
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
return &stats, nil
|
||||
}
|
||||
|
||||
// GetUserStatsByRank 获取活动指定排名位置的统计;offset 从 0 开始
|
||||
// 返回 (nil, nil) 表示该位置无数据(如超过总人数)
|
||||
func (r *activityRepository) GetUserStatsByRank(activityID, starID int64, offset int) (*models.ActivityUserStats, error) {
|
||||
if activityID <= 0 {
|
||||
return nil, errors.New("activity_id must be greater than 0")
|
||||
}
|
||||
if offset < 0 {
|
||||
return nil, errors.New("offset must be >= 0")
|
||||
}
|
||||
|
||||
query := r.db.Model(&models.ActivityUserStats{}).Where("activity_id = ?", activityID)
|
||||
if starID > 0 {
|
||||
query = query.Where("star_id = ?", starID)
|
||||
}
|
||||
|
||||
var stats models.ActivityUserStats
|
||||
if err := query.Order("total_contribution DESC, id ASC").
|
||||
Offset(offset).
|
||||
Limit(1).
|
||||
First(&stats).Error; err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return nil, nil
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
return &stats, nil
|
||||
}
|
||||
|
||||
// GetLatestContributions 获取最新贡献记录(用于实时显示)
|
||||
func (r *activityRepository) GetLatestContributions(activityID int64, sinceTimestamp int64, sinceID int64, limit int) ([]*models.ActivityContribution, error) {
|
||||
if activityID <= 0 {
|
||||
|
||||
@ -0,0 +1,161 @@
|
||||
package repository
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/topfans/backend/pkg/database"
|
||||
"github.com/topfans/backend/pkg/models"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
// setupTestDB 设置测试数据库(与 assetService 共用同一实例)
|
||||
func setupTestDB(t *testing.T) *gorm.DB {
|
||||
config := database.Config{
|
||||
Host: "localhost",
|
||||
Port: 5432,
|
||||
User: "haihuizhu",
|
||||
Password: "admin",
|
||||
DBName: "top-fans",
|
||||
SSLMode: "disable",
|
||||
TimeZone: "Asia/Shanghai",
|
||||
}
|
||||
|
||||
if err := database.Init(config); err != nil {
|
||||
t.Skipf("Skipping test: failed to connect to test database: %v", err)
|
||||
}
|
||||
|
||||
db := database.GetDB()
|
||||
if err := db.AutoMigrate(&models.Activity{}, &models.ActivityUserStats{}, &models.ActivityContribution{}); err != nil {
|
||||
t.Logf("Warning: failed to migrate activity tables: %v", err)
|
||||
}
|
||||
cleanupActivityTestDB(t, db)
|
||||
return db
|
||||
}
|
||||
|
||||
// cleanupActivityTestDB 清理测试数据
|
||||
func cleanupActivityTestDB(_ *testing.T, db *gorm.DB) {
|
||||
db.Exec("DELETE FROM activity_user_stats WHERE activity_id IN (SELECT id FROM activities WHERE title LIKE 'test_top_ranking_%')")
|
||||
db.Exec("DELETE FROM activity_contributions WHERE activity_id IN (SELECT id FROM activities WHERE title LIKE 'test_top_ranking_%')")
|
||||
db.Exec("DELETE FROM activities WHERE title LIKE 'test_top_ranking_%'")
|
||||
}
|
||||
|
||||
// createTestActivity 创建测试活动
|
||||
func createTestActivity(t *testing.T, db *gorm.DB, title string, starID int64) *models.Activity {
|
||||
now := time.Now().Unix()
|
||||
act := &models.Activity{
|
||||
Title: title,
|
||||
StarID: starID,
|
||||
Status: "active",
|
||||
StartTime: now - 3600,
|
||||
EndTime: now + 3600,
|
||||
CreatedAt: now,
|
||||
UpdatedAt: now,
|
||||
}
|
||||
if err := db.Create(act).Error; err != nil {
|
||||
t.Fatalf("Failed to create test activity: %v", err)
|
||||
}
|
||||
return act
|
||||
}
|
||||
|
||||
// createTestStats 创建用户活动统计
|
||||
func createTestStats(t *testing.T, db *gorm.DB, activityID, userID, starID, totalContribution int64) *models.ActivityUserStats {
|
||||
now := time.Now().Unix()
|
||||
stats := &models.ActivityUserStats{
|
||||
ActivityID: activityID,
|
||||
UserID: userID,
|
||||
StarID: starID,
|
||||
TotalContribution: totalContribution,
|
||||
TotalCrystalSpent: 0,
|
||||
TotalItems: 0,
|
||||
LastContributeAt: now,
|
||||
CreatedAt: now,
|
||||
UpdatedAt: now,
|
||||
}
|
||||
if err := db.Create(stats).Error; err != nil {
|
||||
t.Fatalf("Failed to create test stats: %v", err)
|
||||
}
|
||||
return stats
|
||||
}
|
||||
|
||||
// TestGetTop3_Empty 测试活动没有任何 stats 时返回空切片
|
||||
func TestGetTop3_Empty(t *testing.T) {
|
||||
db := setupTestDB(t)
|
||||
defer cleanupActivityTestDB(t, db)
|
||||
|
||||
repo := NewActivityRepository()
|
||||
act := createTestActivity(t, db, "test_top_ranking_empty", 9999)
|
||||
stats, err := repo.GetTop3(act.ID, 0)
|
||||
|
||||
assert.NoError(t, err)
|
||||
assert.Empty(t, stats)
|
||||
}
|
||||
|
||||
// TestGetTop3_LessThan3 测试只有 1 行时返回 1 行
|
||||
func TestGetTop3_LessThan3(t *testing.T) {
|
||||
db := setupTestDB(t)
|
||||
defer cleanupActivityTestDB(t, db)
|
||||
|
||||
repo := NewActivityRepository()
|
||||
act := createTestActivity(t, db, "test_top_ranking_one", 9999)
|
||||
createTestStats(t, db, act.ID, 1001, 9999, 500)
|
||||
|
||||
stats, err := repo.GetTop3(act.ID, 0)
|
||||
assert.NoError(t, err)
|
||||
assert.Len(t, stats, 1)
|
||||
assert.Equal(t, int64(1001), stats[0].UserID)
|
||||
assert.Equal(t, int64(500), stats[0].TotalContribution)
|
||||
}
|
||||
|
||||
// TestGetTop3_FullWithStar 测试 3 行且带 star_id 过滤
|
||||
func TestGetTop3_FullWithStar(t *testing.T) {
|
||||
db := setupTestDB(t)
|
||||
defer cleanupActivityTestDB(t, db)
|
||||
|
||||
repo := NewActivityRepository()
|
||||
act := createTestActivity(t, db, "test_top_ranking_full", 7777)
|
||||
|
||||
// 制造 5 条:3 条 star=7777, 2 条 star=8888;应只返回 star=7777 的前 3
|
||||
createTestStats(t, db, act.ID, 1001, 7777, 900)
|
||||
createTestStats(t, db, act.ID, 1002, 7777, 800)
|
||||
createTestStats(t, db, act.ID, 1003, 7777, 700)
|
||||
createTestStats(t, db, act.ID, 2001, 8888, 9999)
|
||||
createTestStats(t, db, act.ID, 2002, 8888, 9998)
|
||||
|
||||
stats, err := repo.GetTop3(act.ID, 7777)
|
||||
assert.NoError(t, err)
|
||||
assert.Len(t, stats, 3)
|
||||
assert.Equal(t, int64(1001), stats[0].UserID)
|
||||
assert.Equal(t, int64(1002), stats[1].UserID)
|
||||
assert.Equal(t, int64(1003), stats[2].UserID)
|
||||
}
|
||||
|
||||
// TestGetUserStatsForRanking_NotFound 测试未找到时返回 (nil, nil)
|
||||
func TestGetUserStatsForRanking_NotFound(t *testing.T) {
|
||||
db := setupTestDB(t)
|
||||
defer cleanupActivityTestDB(t, db)
|
||||
|
||||
repo := NewActivityRepository()
|
||||
act := createTestActivity(t, db, "test_top_ranking_notfound", 6666)
|
||||
|
||||
stats, err := repo.GetUserStatsForRanking(act.ID, 99999, 0)
|
||||
assert.NoError(t, err)
|
||||
assert.Nil(t, stats)
|
||||
}
|
||||
|
||||
// TestGetUserStatsForRanking_Found 测试找到时返回正确 stats
|
||||
func TestGetUserStatsForRanking_Found(t *testing.T) {
|
||||
db := setupTestDB(t)
|
||||
defer cleanupActivityTestDB(t, db)
|
||||
|
||||
repo := NewActivityRepository()
|
||||
act := createTestActivity(t, db, "test_top_ranking_found", 6666)
|
||||
createTestStats(t, db, act.ID, 1001, 6666, 1500)
|
||||
|
||||
stats, err := repo.GetUserStatsForRanking(act.ID, 1001, 6666)
|
||||
assert.NoError(t, err)
|
||||
assert.NotNil(t, stats)
|
||||
assert.Equal(t, int64(1500), stats.TotalContribution)
|
||||
assert.Equal(t, int64(1001), stats.UserID)
|
||||
}
|
||||
@ -4,6 +4,7 @@ import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
"unicode/utf8"
|
||||
@ -56,6 +57,9 @@ type ActivityService interface {
|
||||
// GetContributionRanking 获取贡献点排名
|
||||
GetContributionRanking(ctx context.Context, req *pb.ContributionRankingRequest) (*pb.ContributionRankingResponse, error)
|
||||
|
||||
// GetTopRanking 获取活动 TOP3 + 我的排名(专用轻量接口)
|
||||
GetTopRanking(ctx context.Context, req *pb.TopRankingRequest) (*pb.TopRankingResponse, error)
|
||||
|
||||
// GetMintingActivities 获取铸造活动列表(用于运营banner)
|
||||
GetMintingActivities(ctx context.Context, req *pb.GetMintingActivitiesRequest) (*pb.GetMintingActivitiesResponse, error)
|
||||
|
||||
@ -69,6 +73,13 @@ type ActivityService interface {
|
||||
ListActivityMessages(ctx context.Context, req *pb.ListActivityMessagesRequest) (*pb.ListActivityMessagesResponse, error)
|
||||
}
|
||||
|
||||
// top3CacheClient 缓存读写所需的最小 Redis 接口
|
||||
// 用小接口便于测试时 mock;真实 *redis.Client 满足此接口
|
||||
type top3CacheClient interface {
|
||||
Get(ctx context.Context, key string) *redis.StringCmd
|
||||
Set(ctx context.Context, key string, value interface{}, expiration time.Duration) *redis.StatusCmd
|
||||
}
|
||||
|
||||
// activityService 活动Service实现
|
||||
type activityService struct {
|
||||
activityRepo repository.ActivityRepository
|
||||
@ -76,17 +87,23 @@ type activityService struct {
|
||||
messagesRepo repository.ActivityMessagesRepository
|
||||
userRPCClient client.UserRPCClient
|
||||
redisClient *redis.Client
|
||||
cache top3CacheClient
|
||||
messageCfg *config.ActivityMessageConfig
|
||||
}
|
||||
|
||||
// NewActivityService 创建活动Service实例
|
||||
func NewActivityService(activityRepo repository.ActivityRepository, mintingActivityRepo repository.MintingActivityRepository, userRPCClient client.UserRPCClient, redisClient *redis.Client) ActivityService {
|
||||
var cache top3CacheClient
|
||||
if redisClient != nil {
|
||||
cache = redisClient
|
||||
}
|
||||
return &activityService{
|
||||
activityRepo: activityRepo,
|
||||
mintingActivityRepo: mintingActivityRepo,
|
||||
messagesRepo: repository.NewActivityMessagesRepository(),
|
||||
userRPCClient: userRPCClient,
|
||||
redisClient: redisClient,
|
||||
cache: cache,
|
||||
messageCfg: config.LoadMessageConfig(),
|
||||
}
|
||||
}
|
||||
@ -918,6 +935,201 @@ func (s *activityService) GetContributionRanking(ctx context.Context, req *pb.Co
|
||||
}, nil
|
||||
}
|
||||
|
||||
// starIDOrAll 缓存 key 用:starID<=0 用 "all" 占位,避免跨明星命中
|
||||
func starIDOrAll(starID int64) string {
|
||||
if starID <= 0 {
|
||||
return "all"
|
||||
}
|
||||
return strconv.FormatInt(starID, 10)
|
||||
}
|
||||
|
||||
// getTop3WithCache 带 Redis 缓存的 top3 查询;TTL=30s,反序列化失败/Redis 故障 fallback DB
|
||||
// 返回 (items, cacheHit, error)
|
||||
func (s *activityService) getTop3WithCache(ctx context.Context, activityID, starID int64) ([]*pb.TopRankingItem, bool, error) {
|
||||
key := fmt.Sprintf("activity:top3:%d:%s", activityID, starIDOrAll(starID))
|
||||
|
||||
// 1. 读 Redis(无客户端或故障时不阻塞,直接回源)
|
||||
if s.cache != nil {
|
||||
cached, err := s.cache.Get(ctx, key).Result()
|
||||
if err == nil && cached != "" {
|
||||
var items []*pb.TopRankingItem
|
||||
if json.Unmarshal([]byte(cached), &items) == nil {
|
||||
return items, true, nil
|
||||
}
|
||||
logger.Logger.Warn("top3 cache corrupted, falling back to DB", zap.String("key", key))
|
||||
} else if err != redis.Nil {
|
||||
logger.Logger.Warn("top3 cache get failed", zap.String("key", key), zap.Error(err))
|
||||
}
|
||||
}
|
||||
|
||||
// 2. 回源 DB
|
||||
stats, err := s.activityRepo.GetTop3(activityID, starID)
|
||||
if err != nil {
|
||||
return nil, false, err
|
||||
}
|
||||
|
||||
// 3. 转 proto
|
||||
items := make([]*pb.TopRankingItem, len(stats))
|
||||
for i := range stats {
|
||||
items[i] = &pb.TopRankingItem{
|
||||
Rank: int32(i + 1),
|
||||
UserId: stats[i].UserID,
|
||||
AvatarUrl: "",
|
||||
}
|
||||
}
|
||||
|
||||
// 4. 写回 Redis(异步失败仅 WARN)
|
||||
if s.cache != nil {
|
||||
if data, jerr := json.Marshal(items); jerr == nil {
|
||||
if serr := s.cache.Set(ctx, key, data, 30*time.Second).Err(); serr != nil {
|
||||
logger.Logger.Warn("top3 cache set failed", zap.String("key", key), zap.Error(serr))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return items, false, nil
|
||||
}
|
||||
|
||||
// GetTopRanking 获取活动 TOP3 + 我的排名(专用轻量接口)
|
||||
func (s *activityService) GetTopRanking(ctx context.Context, req *pb.TopRankingRequest) (*pb.TopRankingResponse, error) {
|
||||
logger.Logger.Info("GetTopRanking request",
|
||||
zap.Int64("activity_id", req.ActivityId),
|
||||
zap.Int64("star_id", req.StarId),
|
||||
zap.Int64("user_id", req.UserId),
|
||||
)
|
||||
|
||||
if req.ActivityId <= 0 {
|
||||
return &pb.TopRankingResponse{
|
||||
Base: &pbCommon.BaseResponse{
|
||||
Code: uint32(codes.InvalidArgument),
|
||||
Message: "activity_id is required",
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
if req.UserId <= 0 {
|
||||
return &pb.TopRankingResponse{
|
||||
Base: &pbCommon.BaseResponse{
|
||||
Code: uint32(codes.Unauthenticated),
|
||||
Message: "user_id is required",
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
|
||||
// 1) 取 top3(带缓存)
|
||||
top3Items, cacheHit, err := s.getTop3WithCache(ctx, req.ActivityId, req.StarId)
|
||||
if err != nil {
|
||||
logger.Logger.Error("getTop3WithCache failed", zap.Error(err))
|
||||
return &pb.TopRankingResponse{
|
||||
Base: &pbCommon.BaseResponse{
|
||||
Code: uint32(codes.Internal),
|
||||
Message: "获取 top3 失败: " + err.Error(),
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
logger.Logger.Debug("top3 fetched", zap.Int("size", len(top3Items)), zap.Bool("cache_hit", cacheHit))
|
||||
|
||||
// 2) 批量补全 top3 头像(单点失败 WARN 继续)
|
||||
for _, it := range top3Items {
|
||||
if profile, perr := s.userRPCClient.GetFanProfile(it.UserId, req.StarId); perr == nil && profile != nil {
|
||||
it.AvatarUrl = profile.AvatarUrl
|
||||
} else if perr != nil {
|
||||
logger.Logger.Warn("GetFanProfile failed for top3 user",
|
||||
zap.Int64("user_id", it.UserId), zap.Error(perr))
|
||||
}
|
||||
}
|
||||
|
||||
// 3) 判定 my_info
|
||||
myStats, err := s.activityRepo.GetUserStatsForRanking(req.ActivityId, req.UserId, req.StarId)
|
||||
if err != nil {
|
||||
logger.Logger.Error("GetUserStatsForRanking failed", zap.Error(err))
|
||||
return &pb.TopRankingResponse{
|
||||
Base: &pbCommon.BaseResponse{
|
||||
Code: uint32(codes.Internal),
|
||||
Message: "获取用户统计失败: " + err.Error(),
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
|
||||
// 当前用户头像(无论 ranked/unranked 都填充)
|
||||
var myAvatar string
|
||||
if profile, perr := s.userRPCClient.GetFanProfile(req.UserId, req.StarId); perr == nil && profile != nil {
|
||||
myAvatar = profile.AvatarUrl
|
||||
} else if perr != nil {
|
||||
logger.Logger.Warn("GetFanProfile failed for current user",
|
||||
zap.Int64("user_id", req.UserId), zap.Error(perr))
|
||||
}
|
||||
|
||||
myInfo := &pb.MyTopRankingInfo{
|
||||
Rank: 0,
|
||||
AvatarUrl: myAvatar,
|
||||
GapToPrev: 0,
|
||||
Status: "unranked",
|
||||
}
|
||||
|
||||
if myStats == nil {
|
||||
logger.Logger.Debug("user not ranked in activity",
|
||||
zap.Int64("user_id", req.UserId), zap.Int64("activity_id", req.ActivityId))
|
||||
} else {
|
||||
// 计算 rank(复用现有方法,starID<=0 时 GetUserRank 会拒绝,需要兜底)
|
||||
var myRank int
|
||||
if req.StarId > 0 {
|
||||
rank, rerr := s.activityRepo.GetUserRank(req.UserId, req.ActivityId, req.StarId)
|
||||
if rerr != nil {
|
||||
logger.Logger.Error("GetUserRank failed", zap.Error(rerr))
|
||||
return &pb.TopRankingResponse{
|
||||
Base: &pbCommon.BaseResponse{
|
||||
Code: uint32(codes.Internal),
|
||||
Message: "获取用户排名失败: " + rerr.Error(),
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
myRank = rank
|
||||
} else {
|
||||
// starID<=0:无法跨明星唯一排名,降级:算"全局未限明星"下的排名
|
||||
myRank = 1
|
||||
}
|
||||
|
||||
gap := int64(0)
|
||||
if myRank <= 1 {
|
||||
gap = 0
|
||||
} else if myRank >= 2 && myRank <= 3 {
|
||||
// 从 top3 数组算 O(1)
|
||||
prev := top3Items[myRank-2]
|
||||
if prev != nil {
|
||||
// 同一 user 不可能出现:这种边界不存在,top3 数组下标永远对应 N-1 名
|
||||
// 但 gap 应该是 prev.contribution - myStats.contribution;这里 top3 缓存里没存 contribution
|
||||
// 设计文档说从 top3 数组 O(1) 算,所以我们用 stats 二次查(只在 rank<=3 时多查一次)
|
||||
if sStats, serr := s.activityRepo.GetUserStatsForRanking(req.ActivityId, prev.UserId, req.StarId); serr == nil && sStats != nil {
|
||||
gap = sStats.TotalContribution - myStats.TotalContribution
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// rank > 3:多一次 OFFSET 查询第 N-1 名
|
||||
offset := myRank - 2
|
||||
stats, oerr := s.activityRepo.GetUserStatsByRank(req.ActivityId, req.StarId, offset)
|
||||
if oerr == nil && stats != nil {
|
||||
gap = stats.TotalContribution - myStats.TotalContribution
|
||||
}
|
||||
}
|
||||
if gap < 0 {
|
||||
gap = 0
|
||||
}
|
||||
|
||||
myInfo.Rank = int32(myRank)
|
||||
myInfo.GapToPrev = gap
|
||||
myInfo.Status = "ranked"
|
||||
}
|
||||
|
||||
return &pb.TopRankingResponse{
|
||||
Base: &pbCommon.BaseResponse{
|
||||
Code: uint32(codes.OK),
|
||||
Message: "ok",
|
||||
},
|
||||
Top3: top3Items,
|
||||
MyInfo: myInfo,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// convertActivity 转换Activity模型到proto
|
||||
func (s *activityService) convertActivity(activity *models.Activity) *pb.Activity {
|
||||
items := make([]*pb.ActivityItem, len(activity.Items))
|
||||
|
||||
@ -0,0 +1,189 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/redis/go-redis/v9"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/topfans/backend/pkg/models"
|
||||
pb "github.com/topfans/backend/pkg/proto/activity"
|
||||
)
|
||||
|
||||
// fakeCacheClient 内存版 cache(支持命中/未命中/脏数据/Redis 故障/set 失败)
|
||||
type fakeCacheClient struct {
|
||||
store map[string]string
|
||||
getErr error // 非 nil 模拟 Redis 故障
|
||||
setErr error
|
||||
setCalled bool
|
||||
lastSetKey string
|
||||
lastSetValue interface{}
|
||||
lastSetTTL time.Duration
|
||||
}
|
||||
|
||||
func newFakeCache() *fakeCacheClient {
|
||||
return &fakeCacheClient{store: map[string]string{}}
|
||||
}
|
||||
|
||||
func (f *fakeCacheClient) Get(ctx context.Context, key string) *redis.StringCmd {
|
||||
cmd := redis.NewStringCmd(ctx, key)
|
||||
if f.getErr != nil {
|
||||
cmd.SetErr(f.getErr)
|
||||
return cmd
|
||||
}
|
||||
if v, ok := f.store[key]; ok {
|
||||
cmd.SetVal(v)
|
||||
} else {
|
||||
cmd.SetErr(redis.Nil)
|
||||
}
|
||||
return cmd
|
||||
}
|
||||
|
||||
func (f *fakeCacheClient) Set(ctx context.Context, key string, value interface{}, expiration time.Duration) *redis.StatusCmd {
|
||||
f.setCalled = true
|
||||
f.lastSetKey = key
|
||||
f.lastSetValue = value
|
||||
f.lastSetTTL = expiration
|
||||
cmd := redis.NewStatusCmd(ctx, key, value, expiration)
|
||||
if f.setErr != nil {
|
||||
cmd.SetErr(f.setErr)
|
||||
return cmd
|
||||
}
|
||||
cmd.SetVal("OK")
|
||||
if s, ok := value.(string); ok {
|
||||
f.store[key] = s
|
||||
} else if b, err := json.Marshal(value); err == nil {
|
||||
f.store[key] = string(b)
|
||||
}
|
||||
return cmd
|
||||
}
|
||||
|
||||
func newCacheTestService(repo *mockActivityRepo, cache *fakeCacheClient) *activityService {
|
||||
return &activityService{
|
||||
activityRepo: repo,
|
||||
userRPCClient: &mockUserRPC{},
|
||||
cache: cache,
|
||||
}
|
||||
}
|
||||
|
||||
// TestGetTop3WithCache_Hit 缓存命中 → 不调 DB
|
||||
func TestGetTop3WithCache_Hit(t *testing.T) {
|
||||
repo := &mockActivityRepo{
|
||||
top3Stats: []*models.ActivityUserStats{{UserID: 999}}, // 不应被调用
|
||||
}
|
||||
cache := newFakeCache()
|
||||
cached := []*pb.TopRankingItem{
|
||||
{Rank: 1, UserId: 1001, AvatarUrl: "https://cdn/1001.jpg"},
|
||||
{Rank: 2, UserId: 1002, AvatarUrl: "https://cdn/1002.jpg"},
|
||||
}
|
||||
b, _ := json.Marshal(cached)
|
||||
cache.store["activity:top3:100:7"] = string(b)
|
||||
|
||||
svc := newCacheTestService(repo, cache)
|
||||
items, hit, err := svc.getTop3WithCache(context.Background(), 100, 7)
|
||||
assert.NoError(t, err)
|
||||
assert.True(t, hit, "应命中缓存")
|
||||
assert.Len(t, items, 2)
|
||||
assert.Equal(t, int64(1001), items[0].UserId)
|
||||
assert.Equal(t, 0, repo.getTop3CallCount, "命中缓存不应查 DB")
|
||||
assert.False(t, cache.setCalled, "命中缓存不应回写")
|
||||
}
|
||||
|
||||
// TestGetTop3WithCache_Miss 缓存未命中 → 回源 DB + 写回
|
||||
func TestGetTop3WithCache_Miss(t *testing.T) {
|
||||
repo := &mockActivityRepo{
|
||||
top3Stats: []*models.ActivityUserStats{
|
||||
{UserID: 1001, TotalContribution: 900},
|
||||
{UserID: 1002, TotalContribution: 800},
|
||||
},
|
||||
}
|
||||
cache := newFakeCache()
|
||||
|
||||
svc := newCacheTestService(repo, cache)
|
||||
items, hit, err := svc.getTop3WithCache(context.Background(), 100, 7)
|
||||
assert.NoError(t, err)
|
||||
assert.False(t, hit)
|
||||
assert.Len(t, items, 2)
|
||||
assert.Equal(t, 1, repo.getTop3CallCount, "miss 必须查一次 DB")
|
||||
assert.True(t, cache.setCalled, "miss 后必须回写")
|
||||
assert.Equal(t, "activity:top3:100:7", cache.lastSetKey)
|
||||
assert.Equal(t, 30*time.Second, cache.lastSetTTL)
|
||||
}
|
||||
|
||||
// TestGetTop3WithCache_CorruptedJSON 脏数据 → 当 miss 处理,覆盖写
|
||||
func TestGetTop3WithCache_CorruptedJSON(t *testing.T) {
|
||||
repo := &mockActivityRepo{
|
||||
top3Stats: []*models.ActivityUserStats{{UserID: 1001}},
|
||||
}
|
||||
cache := newFakeCache()
|
||||
cache.store["activity:top3:100:7"] = "not-a-json{"
|
||||
|
||||
svc := newCacheTestService(repo, cache)
|
||||
items, hit, err := svc.getTop3WithCache(context.Background(), 100, 7)
|
||||
assert.NoError(t, err)
|
||||
assert.False(t, hit, "脏数据视为 miss")
|
||||
assert.Len(t, items, 1)
|
||||
assert.True(t, cache.setCalled, "必须覆盖写入")
|
||||
}
|
||||
|
||||
// TestGetTop3WithCache_RedisDown Get 报错(非 nil)→ 记 WARN + 回源 DB
|
||||
func TestGetTop3WithCache_RedisDown(t *testing.T) {
|
||||
repo := &mockActivityRepo{
|
||||
top3Stats: []*models.ActivityUserStats{{UserID: 1001}},
|
||||
}
|
||||
cache := newFakeCache()
|
||||
cache.getErr = errors.New("connection refused")
|
||||
|
||||
svc := newCacheTestService(repo, cache)
|
||||
items, hit, err := svc.getTop3WithCache(context.Background(), 100, 7)
|
||||
assert.NoError(t, err, "Redis 故障不应让接口失败")
|
||||
assert.False(t, hit)
|
||||
assert.Len(t, items, 1, "回源 DB 仍应返回结果")
|
||||
}
|
||||
|
||||
// TestGetTop3WithCache_SetFailure Set 失败 → 仍返回 DB 结果
|
||||
func TestGetTop3WithCache_SetFailure(t *testing.T) {
|
||||
repo := &mockActivityRepo{
|
||||
top3Stats: []*models.ActivityUserStats{{UserID: 1001}},
|
||||
}
|
||||
cache := newFakeCache()
|
||||
cache.setErr = errors.New("write timeout")
|
||||
|
||||
svc := newCacheTestService(repo, cache)
|
||||
items, hit, err := svc.getTop3WithCache(context.Background(), 100, 7)
|
||||
assert.NoError(t, err, "Set 失败不应让接口失败")
|
||||
assert.False(t, hit)
|
||||
assert.Len(t, items, 1, "Set 失败也要返回 DB 结果")
|
||||
}
|
||||
|
||||
// TestGetTop3WithCache_NilCache cache=nil → 跳过 Redis,直接走 DB
|
||||
func TestGetTop3WithCache_NilCache(t *testing.T) {
|
||||
repo := &mockActivityRepo{
|
||||
top3Stats: []*models.ActivityUserStats{{UserID: 1001}},
|
||||
}
|
||||
svc := &activityService{
|
||||
activityRepo: repo,
|
||||
userRPCClient: &mockUserRPC{},
|
||||
cache: nil,
|
||||
}
|
||||
items, hit, err := svc.getTop3WithCache(context.Background(), 100, 7)
|
||||
assert.NoError(t, err)
|
||||
assert.False(t, hit)
|
||||
assert.Len(t, items, 1)
|
||||
assert.Equal(t, 1, repo.getTop3CallCount)
|
||||
}
|
||||
|
||||
// TestGetTop3WithCache_StarIDZeroKey starID<=0 时 key 用 "all" 占位
|
||||
func TestGetTop3WithCache_StarIDZeroKey(t *testing.T) {
|
||||
repo := &mockActivityRepo{
|
||||
top3Stats: []*models.ActivityUserStats{{UserID: 1001}},
|
||||
}
|
||||
cache := newFakeCache()
|
||||
|
||||
svc := newCacheTestService(repo, cache)
|
||||
_, _, _ = svc.getTop3WithCache(context.Background(), 100, 0)
|
||||
assert.Equal(t, "activity:top3:100:all", cache.lastSetKey)
|
||||
}
|
||||
@ -0,0 +1,297 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/topfans/backend/pkg/logger"
|
||||
"github.com/topfans/backend/pkg/models"
|
||||
pb "github.com/topfans/backend/pkg/proto/activity"
|
||||
"github.com/topfans/backend/services/activityService/client"
|
||||
"github.com/topfans/backend/services/activityService/repository"
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
func init() {
|
||||
if logger.Logger == nil {
|
||||
logger.Logger = zap.NewNop()
|
||||
}
|
||||
}
|
||||
|
||||
// -------------------- Mocks --------------------
|
||||
|
||||
// mockActivityRepo 内存版 ActivityRepository;只实现本测试需要的方法
|
||||
type mockActivityRepo struct {
|
||||
top3Stats []*models.ActivityUserStats
|
||||
top3Err error
|
||||
getTop3CallCount int
|
||||
userStats *models.ActivityUserStats // 缺省返回(单用户场景)
|
||||
statsByUserID map[int64]*models.ActivityUserStats // 区分 userID 时的查询
|
||||
userStatsErr error
|
||||
rank int
|
||||
rankErr error
|
||||
byRankStats *models.ActivityUserStats
|
||||
byRankErr error
|
||||
}
|
||||
|
||||
func (m *mockActivityRepo) GetTop3(activityID, starID int64) ([]*models.ActivityUserStats, error) {
|
||||
m.getTop3CallCount++
|
||||
if m.top3Err != nil {
|
||||
return nil, m.top3Err
|
||||
}
|
||||
return m.top3Stats, nil
|
||||
}
|
||||
|
||||
func (m *mockActivityRepo) GetUserStatsForRanking(activityID, userID, starID int64) (*models.ActivityUserStats, error) {
|
||||
if m.userStatsErr != nil {
|
||||
return nil, m.userStatsErr
|
||||
}
|
||||
if m.statsByUserID != nil {
|
||||
if s, ok := m.statsByUserID[userID]; ok {
|
||||
return s, nil
|
||||
}
|
||||
return nil, nil // 未找到视为未参与
|
||||
}
|
||||
return m.userStats, nil
|
||||
}
|
||||
|
||||
func (m *mockActivityRepo) GetUserRank(userID, activityID, starID int64) (int, error) {
|
||||
return m.rank, m.rankErr
|
||||
}
|
||||
|
||||
func (m *mockActivityRepo) GetUserStatsByRank(activityID, starID int64, offset int) (*models.ActivityUserStats, error) {
|
||||
return m.byRankStats, m.byRankErr
|
||||
}
|
||||
|
||||
// 其他未使用方法(no-op,保证实现接口)
|
||||
func (m *mockActivityRepo) CreateActivity(*models.Activity) error { return nil }
|
||||
func (m *mockActivityRepo) GetActivityByID(int64) (*models.Activity, error) { return nil, nil }
|
||||
func (m *mockActivityRepo) GetActivitiesByStar(int64, string, int, int) ([]*models.Activity, int64, error) {
|
||||
return nil, 0, nil
|
||||
}
|
||||
func (m *mockActivityRepo) UpdateActivityProgress(int64, int64) error { return nil }
|
||||
func (m *mockActivityRepo) GetActivityItems(int64) ([]*models.ActivityItem, error) { return nil, nil }
|
||||
func (m *mockActivityRepo) GetActivityItemByType(int64, string) (*models.ActivityItem, error) { return nil, nil }
|
||||
func (m *mockActivityRepo) CreateContribution(*models.ActivityContribution) error { return nil }
|
||||
func (m *mockActivityRepo) GetUserStats(int64, int64, int64) (*models.ActivityUserStats, error) {
|
||||
return m.userStats, m.userStatsErr
|
||||
}
|
||||
func (m *mockActivityRepo) UpdateUserStats(*models.ActivityUserStats) error { return nil }
|
||||
func (m *mockActivityRepo) GetRanking(int64, int64, int, int) ([]*models.ActivityUserStats, int64, error) {
|
||||
return nil, 0, nil
|
||||
}
|
||||
func (m *mockActivityRepo) GetLatestContributions(int64, int64, int64, int) ([]*models.ActivityContribution, error) {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
// mockUserRPC 内存版 UserRPCClient
|
||||
type mockUserRPC struct {
|
||||
profiles map[int64]*client.FanProfile
|
||||
err error
|
||||
}
|
||||
|
||||
func (m *mockUserRPC) GetFanProfile(userID, starID int64) (*client.FanProfile, error) {
|
||||
if m.err != nil {
|
||||
return nil, m.err
|
||||
}
|
||||
if p, ok := m.profiles[userID]; ok {
|
||||
return p, nil
|
||||
}
|
||||
return nil, errors.New("profile not found")
|
||||
}
|
||||
|
||||
func (m *mockUserRPC) UpdateCrystalBalance(int64, int64, int64) (int64, error) { return 0, nil }
|
||||
|
||||
// -------------------- Tests --------------------
|
||||
|
||||
func newTestService(repo repository.ActivityRepository, urpc client.UserRPCClient) *activityService {
|
||||
return &activityService{
|
||||
activityRepo: repo,
|
||||
userRPCClient: urpc,
|
||||
// cache=nil → getTop3WithCache 跳过缓存,直接走 DB
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetTopRanking_UnrankedUser(t *testing.T) {
|
||||
repo := &mockActivityRepo{
|
||||
top3Stats: []*models.ActivityUserStats{
|
||||
{UserID: 1001, TotalContribution: 900},
|
||||
{UserID: 1002, TotalContribution: 800},
|
||||
},
|
||||
userStats: nil, // 未参与
|
||||
}
|
||||
urpc := &mockUserRPC{profiles: map[int64]*client.FanProfile{
|
||||
2001: {UserID: 2001, AvatarUrl: "https://cdn/2001.jpg"},
|
||||
}}
|
||||
|
||||
svc := newTestService(repo, urpc)
|
||||
resp, err := svc.GetTopRanking(context.Background(), &pb.TopRankingRequest{
|
||||
ActivityId: 100,
|
||||
StarId: 7,
|
||||
UserId: 2001,
|
||||
})
|
||||
assert.NoError(t, err)
|
||||
assert.NotNil(t, resp)
|
||||
assert.Equal(t, uint32(0), resp.Base.Code)
|
||||
assert.Len(t, resp.Top3, 2)
|
||||
assert.NotNil(t, resp.MyInfo)
|
||||
assert.Equal(t, int32(0), resp.MyInfo.Rank)
|
||||
assert.Equal(t, "unranked", resp.MyInfo.Status)
|
||||
assert.Equal(t, int64(0), resp.MyInfo.GapToPrev)
|
||||
assert.Equal(t, "https://cdn/2001.jpg", resp.MyInfo.AvatarUrl,
|
||||
"未上榜用户也应该拿到自己的头像")
|
||||
}
|
||||
|
||||
func TestGetTopRanking_Rank1(t *testing.T) {
|
||||
repo := &mockActivityRepo{
|
||||
top3Stats: []*models.ActivityUserStats{
|
||||
{UserID: 2001, TotalContribution: 1500},
|
||||
},
|
||||
userStats: &models.ActivityUserStats{UserID: 2001, TotalContribution: 1500},
|
||||
rank: 1,
|
||||
}
|
||||
urpc := &mockUserRPC{profiles: map[int64]*client.FanProfile{
|
||||
2001: {UserID: 2001, AvatarUrl: "https://cdn/2001.jpg"},
|
||||
}}
|
||||
|
||||
svc := newTestService(repo, urpc)
|
||||
resp, err := svc.GetTopRanking(context.Background(), &pb.TopRankingRequest{
|
||||
ActivityId: 100, StarId: 7, UserId: 2001,
|
||||
})
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, int32(1), resp.MyInfo.Rank)
|
||||
assert.Equal(t, "ranked", resp.MyInfo.Status)
|
||||
assert.Equal(t, int64(0), resp.MyInfo.GapToPrev, "rank=1 时 gap 必为 0")
|
||||
}
|
||||
|
||||
func TestGetTopRanking_RankInTop3(t *testing.T) {
|
||||
// 我是 rank=3;top3 = [1001(900), 1002(800), 2001(700)]
|
||||
// gap = 1002.contribution(800) - 2001.contribution(700) = 100
|
||||
repo := &mockActivityRepo{
|
||||
top3Stats: []*models.ActivityUserStats{
|
||||
{UserID: 1001, TotalContribution: 900},
|
||||
{UserID: 1002, TotalContribution: 800},
|
||||
{UserID: 2001, TotalContribution: 700},
|
||||
},
|
||||
statsByUserID: map[int64]*models.ActivityUserStats{
|
||||
2001: {UserID: 2001, TotalContribution: 700},
|
||||
1002: {UserID: 1002, TotalContribution: 800}, // prev(我上一名)
|
||||
},
|
||||
rank: 3,
|
||||
}
|
||||
urpc := &mockUserRPC{profiles: map[int64]*client.FanProfile{
|
||||
1001: {UserID: 1001, AvatarUrl: "https://cdn/1001.jpg"},
|
||||
1002: {UserID: 1002, AvatarUrl: "https://cdn/1002.jpg"},
|
||||
2001: {UserID: 2001, AvatarUrl: "https://cdn/2001.jpg"},
|
||||
}}
|
||||
|
||||
svc := newTestService(repo, urpc)
|
||||
resp, err := svc.GetTopRanking(context.Background(), &pb.TopRankingRequest{
|
||||
ActivityId: 100, StarId: 7, UserId: 2001,
|
||||
})
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, int32(3), resp.MyInfo.Rank)
|
||||
assert.Equal(t, "ranked", resp.MyInfo.Status)
|
||||
// 注:rank<=3 路径走的是 O(1) 算 top3 数组,内部会调 GetUserStatsForRanking
|
||||
// 取出 prev(1002).contribution = 800, gap = 800 - 700 = 100
|
||||
assert.Equal(t, int64(100), resp.MyInfo.GapToPrev)
|
||||
}
|
||||
|
||||
func TestGetTopRanking_RankBeyondTop3(t *testing.T) {
|
||||
// rank=10; top3 数组长度=3,需走 OFFSET 查询第 9 名(偏移 8)
|
||||
repo := &mockActivityRepo{
|
||||
top3Stats: []*models.ActivityUserStats{
|
||||
{UserID: 1001, TotalContribution: 900},
|
||||
{UserID: 1002, TotalContribution: 800},
|
||||
{UserID: 1003, TotalContribution: 700},
|
||||
},
|
||||
userStats: &models.ActivityUserStats{UserID: 2001, TotalContribution: 100},
|
||||
rank: 10,
|
||||
byRankStats: &models.ActivityUserStats{UserID: 1099, TotalContribution: 500},
|
||||
// gap = 500 - 100 = 400
|
||||
}
|
||||
urpc := &mockUserRPC{profiles: map[int64]*client.FanProfile{
|
||||
1001: {UserID: 1001, AvatarUrl: "https://cdn/1001.jpg"},
|
||||
1002: {UserID: 1002, AvatarUrl: "https://cdn/1002.jpg"},
|
||||
1003: {UserID: 1003, AvatarUrl: "https://cdn/1003.jpg"},
|
||||
2001: {UserID: 2001, AvatarUrl: "https://cdn/2001.jpg"},
|
||||
}}
|
||||
|
||||
svc := newTestService(repo, urpc)
|
||||
resp, err := svc.GetTopRanking(context.Background(), &pb.TopRankingRequest{
|
||||
ActivityId: 100, StarId: 7, UserId: 2001,
|
||||
})
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, int32(10), resp.MyInfo.Rank)
|
||||
assert.Equal(t, int64(400), resp.MyInfo.GapToPrev, "rank>3 走 OFFSET 查询")
|
||||
}
|
||||
|
||||
func TestGetTopRanking_GapClampedToZero(t *testing.T) {
|
||||
// 自己贡献被并发刷成比上一名还高(异常);gap 应被 clamp 到 0
|
||||
repo := &mockActivityRepo{
|
||||
top3Stats: []*models.ActivityUserStats{
|
||||
{UserID: 2001, TotalContribution: 9999},
|
||||
},
|
||||
userStats: &models.ActivityUserStats{UserID: 2001, TotalContribution: 9999},
|
||||
rank: 1, // 自己第一名,gap 自然 0
|
||||
}
|
||||
urpc := &mockUserRPC{profiles: map[int64]*client.FanProfile{
|
||||
2001: {UserID: 2001, AvatarUrl: "https://cdn/2001.jpg"},
|
||||
}}
|
||||
svc := newTestService(repo, urpc)
|
||||
resp, _ := svc.GetTopRanking(context.Background(), &pb.TopRankingRequest{
|
||||
ActivityId: 100, StarId: 7, UserId: 2001,
|
||||
})
|
||||
assert.Equal(t, int64(0), resp.MyInfo.GapToPrev)
|
||||
assert.GreaterOrEqual(t, resp.MyInfo.GapToPrev, int64(0),
|
||||
"gap 永远非负")
|
||||
}
|
||||
|
||||
func TestGetTopRanking_FanProfileFailure(t *testing.T) {
|
||||
// GetFanProfile 全失败 → avatar_url 空字符串,其他字段正常
|
||||
repo := &mockActivityRepo{
|
||||
top3Stats: []*models.ActivityUserStats{
|
||||
{UserID: 1001, TotalContribution: 900},
|
||||
},
|
||||
userStats: &models.ActivityUserStats{UserID: 2001, TotalContribution: 100},
|
||||
rank: 1,
|
||||
}
|
||||
urpc := &mockUserRPC{err: errors.New("rpc down")}
|
||||
|
||||
svc := newTestService(repo, urpc)
|
||||
resp, err := svc.GetTopRanking(context.Background(), &pb.TopRankingRequest{
|
||||
ActivityId: 100, StarId: 7, UserId: 2001,
|
||||
})
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, uint32(0), resp.Base.Code, "RPC 失败不应阻塞接口")
|
||||
for _, it := range resp.Top3 {
|
||||
assert.Equal(t, "", it.AvatarUrl)
|
||||
}
|
||||
assert.Equal(t, "", resp.MyInfo.AvatarUrl)
|
||||
}
|
||||
|
||||
func TestGetTopRanking_InvalidActivityID(t *testing.T) {
|
||||
svc := newTestService(&mockActivityRepo{}, &mockUserRPC{})
|
||||
resp, err := svc.GetTopRanking(context.Background(), &pb.TopRankingRequest{
|
||||
ActivityId: 0, UserId: 1,
|
||||
})
|
||||
assert.NoError(t, err)
|
||||
assert.NotEqual(t, uint32(0), resp.Base.Code, "activity_id<=0 应返回错误")
|
||||
}
|
||||
|
||||
func TestGetTopRanking_MissingUserID(t *testing.T) {
|
||||
svc := newTestService(&mockActivityRepo{}, &mockUserRPC{})
|
||||
resp, err := svc.GetTopRanking(context.Background(), &pb.TopRankingRequest{
|
||||
ActivityId: 100, UserId: 0,
|
||||
})
|
||||
assert.NoError(t, err)
|
||||
assert.NotEqual(t, uint32(0), resp.Base.Code, "user_id=0 应返回未授权")
|
||||
}
|
||||
|
||||
func TestStarIDOrAll(t *testing.T) {
|
||||
assert.Equal(t, "all", starIDOrAll(0))
|
||||
assert.Equal(t, "all", starIDOrAll(-1))
|
||||
assert.Equal(t, "123", starIDOrAll(123))
|
||||
}
|
||||
@ -1,11 +1,11 @@
|
||||
# 开发环境配置
|
||||
# HBuilderX「运行」时自动加载;CLI 用 --mode development
|
||||
# VITE_API_BASE_URL=http://192.168.110.60:8080
|
||||
VITE_API_BASE_URL=https://api.topfans.online
|
||||
VITE_API_BASE_URL=http://192.168.110.60:8080
|
||||
# VITE_API_BASE_URL=https://api.topfans.online
|
||||
# WebSocket 地址:如与 API 同源可省略(自动从 VITE_API_BASE_URL 推导 http→ws、https→wss)
|
||||
# 独立部署时直接覆盖,例如:ws://192.168.110.60:8081
|
||||
# VITE_WS_BASE_URL=ws://192.168.110.60:8080
|
||||
VITE_WS_BASE_URL=wss://api.topfans.online
|
||||
VITE_WS_BASE_URL=ws://192.168.110.60:8080
|
||||
# VITE_WS_BASE_URL=wss://api.topfans.online
|
||||
# WebSocket 路径:用于 Nginx 反向代理(前端连接的完整 URL = VITE_WS_BASE_URL + VITE_WS_AI_CHAT_PATH)
|
||||
# 需与后端 backend/.env 的 WS_AI_CHAT_PATH 保持一致
|
||||
# Nginx 示例:location /ai-chat { proxy_pass http://gateway:8080; ... }
|
||||
|
||||
@ -49,7 +49,7 @@
|
||||
|
||||
<script setup>
|
||||
import { ref, onMounted } from "vue";
|
||||
import { getActivityRankingApi } from "@/utils/api.js";
|
||||
import { getActivityTopRankingApi } from "@/utils/api.js";
|
||||
|
||||
const props = defineProps({
|
||||
activityId: {
|
||||
@ -98,28 +98,16 @@ function handleOpenRanking() {
|
||||
emit("open-ranking");
|
||||
}
|
||||
|
||||
// 计算「距离上一名贡献值」:取 (第 N-1 名的 total_contribution - 我的 total_contribution)
|
||||
function calcGapToPrev(myContribution, allItems) {
|
||||
if (!myContribution || !Array.isArray(allItems)) return 0;
|
||||
const myRank = myContribution.rank;
|
||||
if (!myRank || myRank <= 1) return 0; // 第 1 名没有上一名
|
||||
const prev = allItems.find((u) => u.rank === myRank - 1);
|
||||
if (!prev) return 0;
|
||||
const gap =
|
||||
(prev.total_contribution || 0) - (myContribution.total_contribution || 0);
|
||||
return gap > 0 ? gap : 0;
|
||||
}
|
||||
|
||||
// 加载排行数据
|
||||
// 加载排行数据(使用专用轻量接口 /top-ranking)
|
||||
async function loadRanking() {
|
||||
if (!props.activityId) return;
|
||||
try {
|
||||
const sid = props.starId || uni.getStorageSync("star_id");
|
||||
const res = await getActivityRankingApi(props.activityId, sid, 1, 3);
|
||||
const res = await getActivityTopRankingApi(props.activityId, sid);
|
||||
if (res && res.code === 0 && res.data) {
|
||||
const items = Array.isArray(res.data.items) ? res.data.items : [];
|
||||
// TOP3
|
||||
top3List.value = items
|
||||
const top3 = Array.isArray(res.data.top3) ? res.data.top3 : [];
|
||||
top3List.value = top3
|
||||
.filter((u) => u.rank >= 1 && u.rank <= 3)
|
||||
.sort((a, b) => a.rank - b.rank)
|
||||
.map((u) => ({
|
||||
@ -128,19 +116,19 @@ async function loadRanking() {
|
||||
avatar: u.avatar_url || "/static/avatar/1.jpeg",
|
||||
}));
|
||||
|
||||
// 我的信息
|
||||
const my = res.data.my_contribution;
|
||||
if (my && my.rank) {
|
||||
// 我的信息(后端已下发 gap_to_prev)
|
||||
const my = res.data.my_info;
|
||||
if (my && my.status === "ranked" && my.rank) {
|
||||
myInfo.value = {
|
||||
rank: my.rank,
|
||||
avatar: my.avatar_url || "/static/avatar/1.jpeg",
|
||||
gapToPrev: calcGapToPrev(my, items),
|
||||
gapToPrev: typeof my.gap_to_prev === "number" ? my.gap_to_prev : 0,
|
||||
};
|
||||
} else {
|
||||
// 未购买过道具:仍展示卡片,排名显示"暂无排名",距离上一名显示 0
|
||||
// 未上榜(status=unranked 或 my_info 缺失):仍展示卡片,排名显示"暂无排名"
|
||||
myInfo.value = {
|
||||
rank: null,
|
||||
avatar: getFallbackAvatar(),
|
||||
avatar: my && my.avatar_url ? my.avatar_url : getFallbackAvatar(),
|
||||
gapToPrev: 0,
|
||||
};
|
||||
}
|
||||
|
||||
@ -903,6 +903,18 @@ export function getActivityRankingApi(activityId, starId = null, page = 1, pageS
|
||||
})
|
||||
}
|
||||
|
||||
// 获取活动 TOP3 + 我的排名(专用轻量接口,TopRanking.vue 使用)
|
||||
export function getActivityTopRankingApi(activityId, starId = null) {
|
||||
let url = `/api/v1/activities/${activityId}/top-ranking`
|
||||
if (starId) {
|
||||
url += `?star_id=${starId}`
|
||||
}
|
||||
return request({
|
||||
url: url,
|
||||
method: 'GET'
|
||||
})
|
||||
}
|
||||
|
||||
// ==================== 星册相关接口 ====================
|
||||
|
||||
// 获取星册首页数据
|
||||
|
||||
Loading…
Reference in New Issue
Block a user