package achievementconfig import ( "strings" "time" "hyapp-admin-server/internal/appctx" "hyapp-admin-server/internal/integration/activityclient" "hyapp-admin-server/internal/middleware" "hyapp-admin-server/internal/modules/shared" "hyapp-admin-server/internal/response" activityv1 "hyapp.local/api/proto/activity/v1" "github.com/gin-gonic/gin" ) type Handler struct { activity activityclient.Client audit shared.OperationLogger } func New(activity activityclient.Client, audit shared.OperationLogger) *Handler { return &Handler{activity: activity, audit: audit} } type achievementRequest struct { CollectionID string `json:"collection_id"` CollectionType string `json:"collection_type"` AchievementType string `json:"achievement_type"` Title string `json:"title"` Description string `json:"description"` Status string `json:"status"` PrimaryBadgeResourceID int64 `json:"primary_badge_resource_id"` RewardResourceGroupID int64 `json:"reward_resource_group_id"` AutoPinPolicy string `json:"auto_pin_policy"` HonorVisibility bool `json:"honor_visibility"` EffectiveFromMS int64 `json:"effective_from_ms"` EffectiveToMS int64 `json:"effective_to_ms"` SortOrder int32 `json:"sort_order"` DisplayConfigJSON string `json:"display_config_json"` Conditions []conditionRequest `json:"conditions"` } type conditionRequest struct { MetricType string `json:"metric_type"` TargetValue int64 `json:"target_value"` TargetUnit string `json:"target_unit"` DimensionFilterJSON string `json:"dimension_filter_json"` } type achievementDTO struct { AchievementID string `json:"achievement_id"` CollectionID string `json:"collection_id"` CollectionType string `json:"collection_type"` AchievementType string `json:"achievement_type"` Title string `json:"title"` Description string `json:"description"` Status string `json:"status"` Version int64 `json:"version"` PrimaryBadgeResourceID int64 `json:"primary_badge_resource_id"` RewardResourceGroupID int64 `json:"reward_resource_group_id"` AutoPinPolicy string `json:"auto_pin_policy"` HonorVisibility bool `json:"honor_visibility"` EffectiveFromMS int64 `json:"effective_from_ms"` EffectiveToMS int64 `json:"effective_to_ms"` SortOrder int32 `json:"sort_order"` DisplayConfigJSON string `json:"display_config_json"` CreatedByAdminID int64 `json:"created_by_admin_id"` UpdatedByAdminID int64 `json:"updated_by_admin_id"` CreatedAtMS int64 `json:"created_at_ms"` UpdatedAtMS int64 `json:"updated_at_ms"` Conditions []conditionDTO `json:"conditions"` } type conditionDTO struct { ConditionID string `json:"condition_id"` AchievementID string `json:"achievement_id"` Version int64 `json:"version"` MetricType string `json:"metric_type"` TargetValue int64 `json:"target_value"` TargetUnit string `json:"target_unit"` DimensionFilterJSON string `json:"dimension_filter_json"` } // ListAchievementDefinitions exposes the achievement rule catalog for operations; no user progress is mixed into this read. func (h *Handler) ListAchievementDefinitions(c *gin.Context) { options := shared.ListOptions(c) resp, err := h.activity.ListAchievementDefinitions(c.Request.Context(), &activityv1.ListAchievementsRequest{ Meta: h.meta(c), CollectionType: strings.TrimSpace(firstQuery(c, "collection_type", "collectionType")), CollectionId: strings.TrimSpace(firstQuery(c, "collection_id", "collectionId")), Page: int32(options.Page), PageSize: int32(options.PageSize), }) if err != nil { response.ServerError(c, "获取成就配置失败") return } items := make([]achievementDTO, 0, len(resp.GetAchievements())) for _, item := range resp.GetAchievements() { items = append(items, achievementFromProto(item.GetDefinition())) } response.OK(c, response.Page{Items: items, Page: options.Page, PageSize: options.PageSize, Total: resp.GetTotal()}) } // CreateAchievementDefinition writes one current-version achievement rule; reward badges are granted through the selected resource group. func (h *Handler) CreateAchievementDefinition(c *gin.Context) { var req achievementRequest if err := c.ShouldBindJSON(&req); err != nil { response.BadRequest(c, "成就参数不正确") return } resp, err := h.activity.UpsertAchievementDefinition(c.Request.Context(), req.toProto(c, "", h.meta(c))) if err != nil { response.BadRequest(c, err.Error()) return } item := achievementFromProto(resp.GetAchievement()) h.auditLog(c, "create-achievement", item.AchievementID, item.Title) response.Created(c, item) } // UpdateAchievementDefinition rewrites the definition and condition set; activity-service owns versioning and unlock semantics. func (h *Handler) UpdateAchievementDefinition(c *gin.Context) { achievementID := strings.TrimSpace(c.Param("achievement_id")) if achievementID == "" { response.BadRequest(c, "ID 参数不正确") return } var req achievementRequest if err := c.ShouldBindJSON(&req); err != nil { response.BadRequest(c, "成就参数不正确") return } resp, err := h.activity.UpsertAchievementDefinition(c.Request.Context(), req.toProto(c, achievementID, h.meta(c))) if err != nil { response.BadRequest(c, err.Error()) return } item := achievementFromProto(resp.GetAchievement()) h.auditLog(c, "update-achievement", item.AchievementID, item.Title) response.OK(c, item) } // DeleteAchievementDefinition 采用归档删除,避免后台删除动作破坏用户已获得徽章和历史成就事实。 func (h *Handler) DeleteAchievementDefinition(c *gin.Context) { achievementID := strings.TrimSpace(c.Param("achievement_id")) if achievementID == "" { response.BadRequest(c, "ID 参数不正确") return } resp, err := h.activity.DeleteAchievementDefinition(c.Request.Context(), &activityv1.DeleteAchievementDefinitionRequest{ Meta: h.meta(c), AchievementId: achievementID, OperatorAdminId: int64(middleware.CurrentUserID(c)), }) if err != nil { response.BadRequest(c, err.Error()) return } item := achievementFromProto(resp.GetAchievement()) h.auditLog(c, "delete-achievement", item.AchievementID, item.Title) response.OK(c, item) } func (r achievementRequest) toProto(c *gin.Context, achievementID string, meta *activityv1.RequestMeta) *activityv1.UpsertAchievementDefinitionRequest { conditions := make([]*activityv1.AchievementCondition, 0, len(r.Conditions)) for _, condition := range r.Conditions { conditions = append(conditions, &activityv1.AchievementCondition{ MetricType: strings.TrimSpace(condition.MetricType), TargetValue: condition.TargetValue, TargetUnit: strings.TrimSpace(condition.TargetUnit), DimensionFilterJson: strings.TrimSpace(condition.DimensionFilterJSON), }) } return &activityv1.UpsertAchievementDefinitionRequest{ Meta: meta, AchievementId: strings.TrimSpace(achievementID), CollectionId: strings.TrimSpace(r.CollectionID), CollectionType: strings.TrimSpace(r.CollectionType), AchievementType: strings.TrimSpace(r.AchievementType), Title: strings.TrimSpace(r.Title), Description: strings.TrimSpace(r.Description), Status: strings.TrimSpace(r.Status), PrimaryBadgeResourceId: r.PrimaryBadgeResourceID, RewardResourceGroupId: r.RewardResourceGroupID, AutoPinPolicy: strings.TrimSpace(r.AutoPinPolicy), HonorVisibility: r.HonorVisibility, EffectiveFromMs: r.EffectiveFromMS, EffectiveToMs: r.EffectiveToMS, SortOrder: r.SortOrder, DisplayConfigJson: strings.TrimSpace(r.DisplayConfigJSON), OperatorAdminId: int64(middleware.CurrentUserID(c)), Conditions: conditions, } } func (h *Handler) meta(c *gin.Context) *activityv1.RequestMeta { return &activityv1.RequestMeta{ RequestId: middleware.CurrentRequestID(c), Caller: "admin-server", AppCode: appctx.FromContext(c.Request.Context()), SentAtMs: time.Now().UnixMilli(), } } func (h *Handler) auditLog(c *gin.Context, action string, achievementID string, detail string) { if h.audit == nil { return } shared.OperationLogWithResourceID(c, h.audit, action, "achievement_definitions", achievementID, "success", detail) } func achievementFromProto(item *activityv1.AchievementDefinition) achievementDTO { if item == nil { return achievementDTO{} } conditions := make([]conditionDTO, 0, len(item.GetConditions())) for _, condition := range item.GetConditions() { conditions = append(conditions, conditionFromProto(condition)) } return achievementDTO{ AchievementID: item.GetAchievementId(), CollectionID: item.GetCollectionId(), CollectionType: item.GetCollectionType(), AchievementType: item.GetAchievementType(), Title: item.GetTitle(), Description: item.GetDescription(), Status: item.GetStatus(), Version: item.GetVersion(), PrimaryBadgeResourceID: item.GetPrimaryBadgeResourceId(), RewardResourceGroupID: item.GetRewardResourceGroupId(), AutoPinPolicy: item.GetAutoPinPolicy(), HonorVisibility: item.GetHonorVisibility(), EffectiveFromMS: item.GetEffectiveFromMs(), EffectiveToMS: item.GetEffectiveToMs(), SortOrder: item.GetSortOrder(), DisplayConfigJSON: item.GetDisplayConfigJson(), CreatedByAdminID: item.GetCreatedByAdminId(), UpdatedByAdminID: item.GetUpdatedByAdminId(), CreatedAtMS: item.GetCreatedAtMs(), UpdatedAtMS: item.GetUpdatedAtMs(), Conditions: conditions, } } func conditionFromProto(item *activityv1.AchievementCondition) conditionDTO { if item == nil { return conditionDTO{} } return conditionDTO{ ConditionID: item.GetConditionId(), AchievementID: item.GetAchievementId(), Version: item.GetVersion(), MetricType: item.GetMetricType(), TargetValue: item.GetTargetValue(), TargetUnit: item.GetTargetUnit(), DimensionFilterJSON: item.GetDimensionFilterJson(), } } func firstQuery(c *gin.Context, keys ...string) string { for _, key := range keys { if value := c.Query(key); value != "" { return value } } return "" }