funifier-mcp 0.2.26 → 0.2.27
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.cursor/rules/funifier.mdc +38 -41
- package/.github/copilot-instructions.md +38 -41
- package/AGENTS.md +56 -49
- package/README.md +40 -22
- package/datasource-funifier-docs/.coverage.json +326 -0
- package/datasource-funifier-docs/.validation.json +593 -0
- package/datasource-funifier-docs/knowledge/guides/aggregates.md +182 -70
- package/datasource-funifier-docs/knowledge/guides/database-access.md +174 -88
- package/datasource-funifier-docs/knowledge/guides/java-entities.md +294 -204
- package/datasource-funifier-docs/knowledge/guides/java-libraries.md +202 -226
- package/datasource-funifier-docs/knowledge/guides/java-managers.md +343 -265
- package/datasource-funifier-docs/knowledge/guides/trigger-examples.md +180 -236
- package/datasource-funifier-docs/knowledge/guides/triggers-guide.md +273 -191
- package/datasource-funifier-docs/knowledge/index.md +4 -1
- package/datasource-funifier-docs/knowledge/modules/achievement.md +1126 -28
- package/datasource-funifier-docs/knowledge/modules/action-log.md +469 -62
- package/datasource-funifier-docs/knowledge/modules/action.md +522 -70
- package/datasource-funifier-docs/knowledge/modules/auth.md +718 -69
- package/datasource-funifier-docs/knowledge/modules/avatar.md +483 -18
- package/datasource-funifier-docs/knowledge/modules/backup.md +603 -25
- package/datasource-funifier-docs/knowledge/modules/challenge.md +1048 -220
- package/datasource-funifier-docs/knowledge/modules/compact.md +469 -26
- package/datasource-funifier-docs/knowledge/modules/competition.md +811 -109
- package/datasource-funifier-docs/knowledge/modules/crossword.md +504 -28
- package/datasource-funifier-docs/knowledge/modules/csv-data.md +645 -20
- package/datasource-funifier-docs/knowledge/modules/custom-object.md +701 -36
- package/datasource-funifier-docs/knowledge/modules/database.md +730 -164
- package/datasource-funifier-docs/knowledge/modules/folder.md +935 -280
- package/datasource-funifier-docs/knowledge/modules/kpi-formulas.md +410 -15
- package/datasource-funifier-docs/knowledge/modules/lastmile.md +568 -29
- package/datasource-funifier-docs/knowledge/modules/leaderboard.md +595 -126
- package/datasource-funifier-docs/knowledge/modules/level.md +536 -54
- package/datasource-funifier-docs/knowledge/modules/lottery.md +809 -76
- package/datasource-funifier-docs/knowledge/modules/marketplace.md +688 -17
- package/datasource-funifier-docs/knowledge/modules/mystery.md +662 -52
- package/datasource-funifier-docs/knowledge/modules/notification.md +564 -26
- package/datasource-funifier-docs/knowledge/modules/patterns.md +519 -814
- package/datasource-funifier-docs/knowledge/modules/player.md +773 -73
- package/datasource-funifier-docs/knowledge/modules/point.md +380 -83
- package/datasource-funifier-docs/knowledge/modules/public.md +508 -178
- package/datasource-funifier-docs/knowledge/modules/question.md +619 -99
- package/datasource-funifier-docs/knowledge/modules/quiz.md +565 -120
- package/datasource-funifier-docs/knowledge/modules/scheduler.md +1092 -39
- package/datasource-funifier-docs/knowledge/modules/security.md +674 -112
- package/datasource-funifier-docs/knowledge/modules/staging.md +742 -19
- package/datasource-funifier-docs/knowledge/modules/story.md +565 -29
- package/datasource-funifier-docs/knowledge/modules/studio-page.md +470 -144
- package/datasource-funifier-docs/knowledge/modules/swap.md +552 -84
- package/datasource-funifier-docs/knowledge/modules/team.md +563 -45
- package/datasource-funifier-docs/knowledge/modules/trigger.md +876 -134
- package/datasource-funifier-docs/knowledge/modules/upload.md +468 -95
- package/datasource-funifier-docs/knowledge/modules/virtual-good.md +510 -63
- package/datasource-funifier-docs/knowledge/modules/webhook.md +375 -28
- package/datasource-funifier-docs/knowledge/modules/websocket.md +459 -26
- package/datasource-funifier-docs/knowledge/modules/widget.md +613 -27
- package/dist/cli/init.d.ts.map +1 -1
- package/dist/cli/init.js +42 -1
- package/dist/cli/init.js.map +1 -1
- package/dist/cli/init.test.js +74 -3
- package/dist/cli/init.test.js.map +1 -1
- package/dist/cli/persona.d.ts +3 -0
- package/dist/cli/persona.d.ts.map +1 -0
- package/dist/cli/persona.js +25 -0
- package/dist/cli/persona.js.map +1 -0
- package/dist/mcp/bundle.js +119 -93
- package/dist/mcp/index.js +2 -2
- package/dist/mcp/index.js.map +1 -1
- package/dist/mcp/resources/documentation.d.ts +1 -1
- package/dist/mcp/resources/documentation.d.ts.map +1 -1
- package/dist/mcp/resources/documentation.js +39 -3
- package/dist/mcp/resources/documentation.js.map +1 -1
- package/dist/mcp/tools/connect.d.ts.map +1 -1
- package/dist/mcp/tools/connect.js +18 -8
- package/dist/mcp/tools/connect.js.map +1 -1
- package/dist/mcp/tools/database.d.ts.map +1 -1
- package/dist/mcp/tools/database.js +59 -47
- package/dist/mcp/tools/database.js.map +1 -1
- package/dist/mcp/tools/database.test.js +2 -2
- package/dist/mcp/tools/database.test.js.map +1 -1
- package/dist/mcp/tools/delete.d.ts.map +1 -1
- package/dist/mcp/tools/delete.js +13 -3
- package/dist/mcp/tools/delete.js.map +1 -1
- package/dist/mcp/tools/execute.d.ts.map +1 -1
- package/dist/mcp/tools/execute.js +20 -9
- package/dist/mcp/tools/execute.js.map +1 -1
- package/dist/mcp/tools/folder.d.ts.map +1 -1
- package/dist/mcp/tools/folder.js +22 -12
- package/dist/mcp/tools/folder.js.map +1 -1
- package/dist/mcp/tools/get.d.ts.map +1 -1
- package/dist/mcp/tools/get.js +16 -6
- package/dist/mcp/tools/get.js.map +1 -1
- package/dist/mcp/tools/index.d.ts +1 -1
- package/dist/mcp/tools/index.d.ts.map +1 -1
- package/dist/mcp/tools/index.js +3 -1
- package/dist/mcp/tools/index.js.map +1 -1
- package/dist/mcp/tools/list.d.ts.map +1 -1
- package/dist/mcp/tools/list.js +38 -14
- package/dist/mcp/tools/list.js.map +1 -1
- package/dist/mcp/tools/logs.d.ts.map +1 -1
- package/dist/mcp/tools/logs.js +15 -5
- package/dist/mcp/tools/logs.js.map +1 -1
- package/dist/mcp/tools/save.d.ts.map +1 -1
- package/dist/mcp/tools/save.js +14 -4
- package/dist/mcp/tools/save.js.map +1 -1
- package/dist/mcp/tools/save.test.js +3 -3
- package/dist/mcp/tools/save.test.js.map +1 -1
- package/dist/mcp/tools/search-docs.d.ts +3 -0
- package/dist/mcp/tools/search-docs.d.ts.map +1 -0
- package/dist/mcp/tools/search-docs.js +102 -0
- package/dist/mcp/tools/search-docs.js.map +1 -0
- package/package.json +6 -2
- package/skills/acquire-funifier-knowledge/SKILL.md +132 -0
- package/skills/acquire-funifier-knowledge/assets/templates/CONCERNS.md +25 -0
- package/skills/acquire-funifier-knowledge/assets/templates/CUSTOM_ENDPOINTS.md +24 -0
- package/skills/acquire-funifier-knowledge/assets/templates/CUSTOM_PAGES.md +24 -0
- package/skills/acquire-funifier-knowledge/assets/templates/GAME_MECHANICS.md +35 -0
- package/skills/acquire-funifier-knowledge/assets/templates/INTEGRATIONS.md +35 -0
- package/skills/acquire-funifier-knowledge/assets/templates/LEADERBOARDS.md +24 -0
- package/skills/acquire-funifier-knowledge/assets/templates/OVERVIEW.md +47 -0
- package/skills/acquire-funifier-knowledge/assets/templates/PLAYER_MODEL.md +31 -0
- package/skills/acquire-funifier-knowledge/assets/templates/SCHEDULERS.md +25 -0
- package/skills/acquire-funifier-knowledge/assets/templates/TECHNIQUES_AND_PATTERNS.md +26 -0
- package/skills/acquire-funifier-knowledge/assets/templates/TRIGGERS.md +27 -0
- package/skills/acquire-funifier-knowledge/references/funifier-inventory-checklist.md +81 -0
- package/skills/acquire-funifier-knowledge/references/game-techniques-taxonomy.md +62 -0
- package/skills/acquire-funifier-knowledge/references/mcp-call-patterns.md +118 -0
- package/skills/funifier/SKILL.md +88 -0
- package/skills/funifier/references/configure-security.md +96 -0
- package/skills/{funifier-create-action/SKILL.md → funifier/references/create-action.md} +0 -33
- package/skills/funifier/references/create-aggregate.md +144 -0
- package/skills/funifier/references/create-challenge.md +116 -0
- package/skills/funifier/references/create-competition.md +98 -0
- package/skills/funifier/references/create-crossword.md +574 -0
- package/skills/funifier/references/create-custom-object.md +91 -0
- package/skills/funifier/references/create-custom-page.md +135 -0
- package/skills/funifier/references/create-folder.md +104 -0
- package/skills/funifier/references/create-lastmile.md +643 -0
- package/skills/{funifier-create-leaderboard/SKILL.md → funifier/references/create-leaderboard.md} +0 -33
- package/skills/funifier/references/create-level.md +94 -0
- package/skills/funifier/references/create-lottery.md +913 -0
- package/skills/funifier/references/create-mystery.md +769 -0
- package/skills/funifier/references/create-notification.md +75 -0
- package/skills/{funifier-create-point/SKILL.md → funifier/references/create-point.md} +0 -33
- package/skills/funifier/references/create-quiz.md +98 -0
- package/skills/funifier/references/create-scheduler.md +141 -0
- package/skills/funifier/references/create-story.md +636 -0
- package/skills/funifier/references/create-swap.md +95 -0
- package/skills/{funifier-create-trigger/SKILL.md → funifier/references/create-trigger.md} +0 -33
- package/skills/funifier/references/create-virtual-good.md +96 -0
- package/skills/funifier/references/create-webhook.md +72 -0
- package/skills/funifier/references/create-websocket.md +71 -0
- package/skills/funifier/references/create-widget.md +76 -0
- package/skills/funifier/references/debug.md +87 -0
- package/skills/funifier/references/help.md +81 -0
- package/skills/funifier/references/implement-frontend.md +106 -0
- package/skills/funifier/references/import-csv.md +75 -0
- package/skills/funifier/references/manage-player.md +82 -0
- package/skills/funifier/references/manage-team.md +76 -0
- package/skills/funifier/references/upload-file.md +91 -0
- package/skills/funifier-create-aggregate/SKILL.md +0 -127
- package/skills/funifier-create-challenge/SKILL.md +0 -88
- package/skills/funifier-create-custom-page/SKILL.md +0 -127
- package/skills/funifier-create-level/SKILL.md +0 -87
- package/skills/funifier-create-quiz/SKILL.md +0 -87
- package/skills/funifier-create-scheduler/SKILL.md +0 -127
- package/skills/funifier-create-virtual-good/SKILL.md +0 -87
- package/skills/funifier-debug/SKILL.md +0 -92
- package/skills/funifier-help/SKILL.md +0 -86
- package/skills/funifier-implement-frontend/SKILL.md +0 -90
- package/skills/funifier-index/SKILL.md +0 -58
|
@@ -1,200 +1,669 @@
|
|
|
1
|
-
#
|
|
1
|
+
# `leaderboard`
|
|
2
2
|
|
|
3
3
|
**Acesso Studio:** `/studio/leaderboard`
|
|
4
4
|
**API Endpoint:** `/v3/leaderboard`
|
|
5
|
+
**Coleção MongoDB:** `leaderboard`
|
|
6
|
+
**Coleções auxiliares:** `leader` (resultado calculado), `leader_header` (metadados do cálculo), `expire` (controle de TTL do cache) — **compartilhadas com o módulo de crowning**.
|
|
5
7
|
|
|
6
|
-
|
|
8
|
+
> Documentação de engenharia reversa. Baseada no código de `funifier-service` (commit `830037e`).
|
|
9
|
+
> Classes principais: `LeaderBoard_V2`, `Operation`, `Period`, `Filter` (pacote `engine.crowning`); `Leader` (`engine.leader`); `LeaderBoardManager` (`engine.leader`); `CrowningManager` (`engine.crowning`); `LeaderboardRest` (`rest.v3.rest`).
|
|
7
10
|
|
|
8
|
-
|
|
11
|
+
---
|
|
9
12
|
|
|
10
|
-
##
|
|
13
|
+
## 1. Visão Geral
|
|
11
14
|
|
|
12
|
-
Leaderboard
|
|
15
|
+
Leaderboard é um ranking que ordena **jogadores** ou **equipes** por um valor calculado a partir de dados de gamificação: contagem de ações, soma/média de atributos de ações, soma de conquistas (pontos, challenges, etc.) ou um KPI pré-preparado. O ranking é sempre relativo a um **período** (janela relativa ou intervalo fixo) e compara a posição atual com a do período anterior (`previous_position`, `move`).
|
|
13
16
|
|
|
14
|
-
|
|
17
|
+
A entidade de configuração é `LeaderBoard_V2`, persistida na coleção `leaderboard`. O **resultado** do ranking não é calculado em cada requisição; ele é materializado na coleção `leader`, com metadados em `leader_header` e validade controlada por um registro na coleção `expire`. Quando o cache expira, o ranking é recalculado.
|
|
15
18
|
|
|
16
|
-
###
|
|
19
|
+
### Particularidade arquitetural central: dois motores de cálculo distintos
|
|
17
20
|
|
|
18
|
-
|
|
19
|
-
|---|---|
|
|
20
|
-
| `0` | Player (jogadores individuais) |
|
|
21
|
-
| `1` | Team (equipes) |
|
|
21
|
+
O módulo possui **duas implementações independentes** de cálculo de líderes, acionadas por endpoints diferentes, com comportamentos divergentes. Entender essa divisão é o ponto mais importante deste módulo:
|
|
22
22
|
|
|
23
|
-
|
|
23
|
+
| Motor | Endpoint | Classe / método | Triggers | Tipos de operação suportados | Cache (TTL default) |
|
|
24
|
+
| --- | --- | --- | --- | --- | --- |
|
|
25
|
+
| **A — V3 "leaders"** | `GET /v3/leaderboard/{id}/leaders` | `CrowningManager.calculateLeaderBoardV3` | dispara `before_render` e `after_render` | player: 1,2,3,10 (**type 4 quebra**); team: 1,2,3,4,10 | `+1h` |
|
|
26
|
+
| **B — "aggregate"** | `POST /v3/leaderboard/{id}/leader/aggregate` | `LeaderBoardManager.findLiveLeadersAggregate` / `findCacheLeadersAggregate` | nenhum | player: 1,2,3,10 (**type 4 quebra**); team: 1,2,3,10 **mas filtro de equipe está hardcoded/quebrado** | `+3h` |
|
|
24
27
|
|
|
25
|
-
|
|
26
|
-
|---|---|---|
|
|
27
|
-
| `1` | Count Actions | Conta quantas vezes a action foi executada |
|
|
28
|
-
| `2` | Sum of attributes | Soma um atributo numérico de uma action |
|
|
29
|
-
| `3` | Sum achievements | Soma pontos/conquistas acumulados (**mais comum**) |
|
|
30
|
-
| `4` | Average of attributes | Média de um atributo numérico de uma action |
|
|
31
|
-
| `10` | Prepared KPI | Usa um KPI pré-configurado |
|
|
28
|
+
Os dois motores **não compartilham cache** (usam chaves de referência diferentes na coleção `expire`) e produzem resultados com formatos de campo ligeiramente diferentes. Detalhes nas seções 2, 5 e 7.
|
|
32
29
|
|
|
33
|
-
###
|
|
30
|
+
### Relação com outros módulos
|
|
34
31
|
|
|
35
|
-
|
|
36
|
-
|
|
32
|
+
- **`action`/`action_log`** — operações `Count Actions`, `Sum/Avg attribute` agregam sobre `action_log`.
|
|
33
|
+
- **`achievement`** — operação `Sum achievements` agrega sobre a coleção `achievement` (pontos, challenges, levels, etc.).
|
|
34
|
+
- **`team`/`team_player`** — rankings de equipe usam `team_player` (membros) e `team` (nome/imagem).
|
|
35
|
+
- **`player`** — nomes e imagens dos jogadores; jogadores não registrados são removidos do resultado (motor A).
|
|
36
|
+
- **`challenge_rule_prepared`** — operação `Prepared KPI` (type 10) executa um pipeline de agregação armazenado.
|
|
37
|
+
- **`trigger`** — eventos `before_render`/`after_render` (somente motor A).
|
|
38
|
+
- **`package`** — `PackageManager` exporta/importa leaderboards (`findAll`, `findById`, `addLeaderboard`, `delete`).
|
|
39
|
+
- **`crowning`** — feature separada (coroamento de 1 posição) que compartilha o pacote, os helpers de cálculo e as coleções `leader`/`leader_header`/`expire`. **Não confundir** com leaderboard.
|
|
40
|
+
|
|
41
|
+
---
|
|
42
|
+
|
|
43
|
+
## 2. Arquitetura e Fluxos
|
|
44
|
+
|
|
45
|
+
### 2.1 Pipeline de persistência (criar / atualizar / excluir)
|
|
46
|
+
|
|
47
|
+
**Criar/atualizar** — `POST /v3/leaderboard` → `LeaderBoardManager.addLeaderboard` (`LeaderBoardManager.java:94`):
|
|
48
|
+
|
|
49
|
+
1. Se `_id` ausente/vazio → gera `Guid.shortTimeMillis()`.
|
|
50
|
+
2. `c.save(board)` na coleção `leaderboard` — **upsert/full-replace**, não patch.
|
|
51
|
+
3. **Purga o cache do board** (side effect): remove de `leader` (`{boardId:#}`), de `expire` (`{extra.item:#}`) e de `leader_header` (`{board:#}`).
|
|
52
|
+
4. Resposta: o próprio objeto com campos nulos removidos (`JsonUtil.toJsonRemoveNullFields`), status `201`.
|
|
53
|
+
|
|
54
|
+
**Excluir** — `DELETE /v3/leaderboard/{id}` → `LeaderBoardManager.delete` (`LeaderBoardManager.java:108`): remove o board e executa a mesma purga das 3 coleções auxiliares. Status `204`.
|
|
55
|
+
|
|
56
|
+
> Não há validação de `operation`, `period` ou `principalType` no insert. Configurações inválidas são aceitas silenciosamente e só falham no momento do cálculo (ver seções 5 e 7).
|
|
57
|
+
|
|
58
|
+
### 2.2 Motor A — `GET /v3/leaderboard/{id}/leaders`
|
|
59
|
+
|
|
60
|
+
`LeaderboardRest.findLeaders` (`LeaderboardRest.java:241`) → `CrowningManager.calculateLeaderBoardV3` (`CrowningManager.java:109`) + `findHeaderV3`.
|
|
61
|
+
|
|
62
|
+
Sequência:
|
|
63
|
+
|
|
64
|
+
1. **Monta período de consulta.** Se `time_amount`+`time_scale` informados → `Period` variável; senão se `start_date`+`end_date` → `Period` fixo; senão usa o `period` default do board. Esse período sobrescreve `board.period` e entra na **chave de cache** (`getSearchId`).
|
|
65
|
+
2. `reference = getSearchId(board, sub)` = `"leaderboard_" + board.id + "_" + sub + "_" + board.period.toString()`.
|
|
66
|
+
3. Busca `expire` por `{extra.reference:#}`.
|
|
67
|
+
4. **Se cache expirou ou não existe:**
|
|
68
|
+
a. dispara trigger `before_render` na coleção `leaderboard`;
|
|
69
|
+
b. `calculateLeaderBoardLeadersIfNecessaryV3` (recalcula — ver 2.3);
|
|
70
|
+
c. dispara trigger `after_render`.
|
|
71
|
+
5. Consulta a coleção `leader` por `{extra.cache: expire.id}`, aplica filtro de view, ordena por `position` e limita.
|
|
72
|
+
6. Monta resposta com `leaderboard`, dados do header (`creation_datetime`, `expiration_datetime`, `start_datetime`, `end_datetime`, `operation_unit`) e `leaders`.
|
|
73
|
+
|
|
74
|
+
#### Fluxo de cálculo — Motor A (`/leaders`)
|
|
75
|
+
|
|
76
|
+
```mermaid
|
|
77
|
+
flowchart TD
|
|
78
|
+
A["GET /{id}/leaders"] --> B["monta Period da query<br/>(time_scale/start_date) ou usa board.period"]
|
|
79
|
+
B --> C["reference = getSearchId(board, sub)"]
|
|
80
|
+
C --> D{"expire existe<br/>e não expirou?"}
|
|
81
|
+
D -- sim --> J["consulta coleção leader<br/>{extra.cache: expire.id}"]
|
|
82
|
+
D -- não --> E["trigger before_render"]
|
|
83
|
+
E --> F["calculateLeaderBoardLeadersIfNecessaryV3"]
|
|
84
|
+
F --> G["trigger after_render"]
|
|
85
|
+
G --> J
|
|
86
|
+
J --> K["aplica view (FRIENDS/MIDDLE)"]
|
|
87
|
+
K --> L["sort position + limit"]
|
|
88
|
+
L --> M["resposta: leaderboard + header + leaders"]
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
### 2.3 Recálculo do Motor A — `calculateLeaderBoardLeadersIfNecessaryV3`
|
|
92
|
+
|
|
93
|
+
Método `synchronized` (`CrowningManager.java:196`):
|
|
94
|
+
|
|
95
|
+
1. Cria registro `expire` se não existir (`extra.reference`, `extra.type="leaderboard"`, `extra.item=board.id`).
|
|
96
|
+
2. Se expirado: remove `leader` antigos (`{extra.cache: expire.id}`).
|
|
97
|
+
3. **Calcula faixas de tempo.** Trata **apenas** `TYPE_VARIABLE` e `TYPE_FIXED`. ⚠️ **`period.expression` é ignorado aqui** (diferente de `Period.getCurrentRange`, usado pelo Motor B).
|
|
98
|
+
- `currentRange` = janela atual; `previewRange` = janela anterior (mesmo tamanho, imediatamente antes).
|
|
99
|
+
4. **Player** (`principalType=0`):
|
|
100
|
+
- `findPlayerIds(board.principals)` resolve principals (cada um pode ser jogador ou equipe) em ids de usuário; se vazio → todos.
|
|
101
|
+
- `excludePrincipals` = todos os `teamId` (remove equipes do ranking de jogadores).
|
|
102
|
+
- `calculatePlayerLeadersHelper(operation, currentRange, ...)` e idem para `previewRange`.
|
|
103
|
+
- Para cada líder: resolve `Player`, define `position` (1-based), `name`, `image` (`image.medium.url`), `boardId`, `extra.cache`. **Jogadores não encontrados na coleção `player` são descartados.**
|
|
104
|
+
- Calcula `previous_position`/`previous_total` cruzando com a janela anterior e define `move` (ver pseudocódigo).
|
|
105
|
+
5. **Team** (`principalType=1`):
|
|
106
|
+
- `linkedTeams` é passado **vazio** (`new ArrayList<>()`, `CrowningManager.java:292` — `findAllTeamsByLeaderboard` comentado), então `calculateTeamLeadersHelper` usa **todas** as equipes.
|
|
107
|
+
- `board.principals` é **ignorado** para equipes.
|
|
108
|
+
6. Salva cada `Leader` na coleção `leader`.
|
|
109
|
+
7. Define `expire.expireAt` = `board.timeout` (keyword) ou **`+1h`** default.
|
|
110
|
+
8. Monta `LeaderHeader` (datas + `unit`):
|
|
111
|
+
- `Sum achievements` → `unit = findOptionByTypeAndItem(achievement_type, item)`.
|
|
112
|
+
- `Count actions`/`Sum attribute` → `unit = {type:"action", item, title, image}` (+ `attribute` no caso de sum attribute).
|
|
113
|
+
- **Sem `unit` para `Avg attribute` (4) e `Prepared KPI` (10).**
|
|
114
|
+
|
|
115
|
+
Pseudocódigo do cálculo de `move` (Motor A):
|
|
116
|
+
|
|
117
|
+
```
|
|
118
|
+
se position == previous_position: move = "stop"
|
|
119
|
+
senão se position < previous_position
|
|
120
|
+
OU previous_position == 0: move = "up" // novo entrante sobe
|
|
121
|
+
senão se position > previous_position: move = "down"
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
### 2.4 Motor B — `POST /v3/leaderboard/{id}/leader/aggregate`
|
|
125
|
+
|
|
126
|
+
`LeaderboardRest.findAllLeadersAggregate` (`LeaderboardRest.java:352`). Aceita um corpo JSON com **estágios de agregação extras** (array) que são anexados ao pipeline do ranking.
|
|
127
|
+
|
|
128
|
+
```mermaid
|
|
129
|
+
flowchart TD
|
|
130
|
+
A["POST /{id}/leader/aggregate?live="] --> B{"live=true?"}
|
|
131
|
+
B -- "true" --> C["findLiveLeadersAggregate<br/>(pipeline Mongo em tempo real)"]
|
|
132
|
+
B -- "false (default)" --> D["findCacheLeadersAggregate"]
|
|
133
|
+
D --> E{"cache válido?"}
|
|
134
|
+
E -- não --> F["recalcula via findLiveLeadersAggregate<br/>insere em leader em lotes de 5000"]
|
|
135
|
+
E -- sim --> G["$match {extra.cache: expire.id}"]
|
|
136
|
+
F --> G
|
|
137
|
+
C --> H["applyViewLeadersAggregate (FRIENDS/MIDDLE)"]
|
|
138
|
+
G --> H
|
|
139
|
+
H --> I["anexa estágios de agregação do corpo"]
|
|
140
|
+
I --> J["PaginationUtil (Range header) → resposta paginada"]
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
- **`live=true`** → `findLiveLeadersAggregate` (`LeaderBoardManager.java:302`): monta o pipeline Mongo completo em tempo real (não persiste).
|
|
144
|
+
- **`live=false` / ausente** → `findCacheLeadersAggregate` (`LeaderBoardManager.java:146`):
|
|
145
|
+
- `reference = "leaderboard_" + id` (+ `"_" + period` se `period` informado). ⚠️ **Chave diferente da do Motor A.**
|
|
146
|
+
- Se expirado: cria/atualiza `expire`, recalcula via `findLiveLeadersAggregate`, remove `leader` antigos (`{extra.cache: expire.id}`), insere os novos **em lotes de 5.000** na coleção `leader`. TTL = `board.timeout` ou **`+3h`** default.
|
|
147
|
+
- Retorna `$match {extra.cache: expire.id}` sobre `leader`.
|
|
148
|
+
|
|
149
|
+
Estrutura interna do pipeline `findLiveLeadersAggregate` (por operação): um `$facet` com `p1` (janela atual) e `p2` (janela anterior), `$group` por jogador/equipe, ordenação com desempate por `time`, `$unwind` com `includeArrayIndex` para gerar `position`, `$cmp` para calcular `move`, `$lookup` em `player`/`team` para nome/imagem (`image.small.url`), e marcação `extra.cache` + `_id` composto `"<id>_<cache>"`.
|
|
150
|
+
|
|
151
|
+
Cálculo de `move` no Motor B: `move = $cmp(prev_pos, position)` com `prev_pos` default `9999999` quando nulo → `1`→`"up"`, `0`→`"stop"`, `-1`→`"down"`.
|
|
152
|
+
|
|
153
|
+
### 2.5 Interação entre componentes (Motor A)
|
|
154
|
+
|
|
155
|
+
```mermaid
|
|
156
|
+
sequenceDiagram
|
|
157
|
+
participant C as Cliente
|
|
158
|
+
participant R as LeaderboardRest
|
|
159
|
+
participant CM as CrowningManager
|
|
160
|
+
participant T as TriggerManager
|
|
161
|
+
participant DB as MongoDB (leader/expire/leader_header)
|
|
162
|
+
|
|
163
|
+
C->>R: GET /{id}/leaders
|
|
164
|
+
R->>CM: calculateLeaderBoardV3(board, sub, ...)
|
|
165
|
+
CM->>DB: findOne expire {extra.reference}
|
|
166
|
+
alt cache expirado
|
|
167
|
+
CM->>T: execute(board, "before_render")
|
|
168
|
+
CM->>CM: calculateLeaderBoardLeadersIfNecessaryV3
|
|
169
|
+
CM->>DB: remove + save leaders, save expire, save header
|
|
170
|
+
CM->>T: execute(board, "after_render")
|
|
171
|
+
end
|
|
172
|
+
CM->>DB: aggregate leader {extra.cache}
|
|
173
|
+
CM-->>R: List<Leader>
|
|
174
|
+
R->>CM: findHeaderV3(board, sub)
|
|
175
|
+
R-->>C: { leaderboard, header, leaders }
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
---
|
|
179
|
+
|
|
180
|
+
## 3. Estrutura dos Objetos
|
|
181
|
+
|
|
182
|
+
### 3.1 `LeaderBoard_V2` — documento raiz (coleção `leaderboard`)
|
|
183
|
+
|
|
184
|
+
| Campo | Tipo | Padrão | Obrigatório | Descrição |
|
|
185
|
+
| --- | --- | --- | --- | --- |
|
|
186
|
+
| `_id` | String | auto (`Guid.shortTimeMillis()` se ausente) | — | Identificador. POST com `_id` existente faz **full replace**. |
|
|
187
|
+
| `title` | String | — | recomendado | Título exibido. |
|
|
188
|
+
| `description` | String | — | não | Descrição. **Persiste, mas não tem getter/setter** (acesso por campo via Jackson). |
|
|
189
|
+
| `principalType` | int | `0` | sim (na prática) | `0` = Player, `1` = Team. |
|
|
190
|
+
| `operation` | `Operation` | — | sim (para cálculo) | Define como calcular o valor. Ver 3.2. |
|
|
191
|
+
| `period` | `Period` | — | sim (para cálculo) | Janela de tempo do ranking. Ver 3.3. |
|
|
192
|
+
| `principals` | List<String> | `null` | não | Restringe o escopo a jogadores/equipes específicos. **Usado só em rankings de player; ignorado em team.** |
|
|
193
|
+
| `techniques` | List<String> | `null` | não | Códigos de técnica (GT). Backfill automático com `GT03` por rotina de manutenção (ver 3.8). |
|
|
194
|
+
| `timeout` | String | `null` | não | Expressão keyword de TTL do cache (ex.: `30m`, `3h`, `1d`). Sem ele: `+1h` (motor A) ou `+3h` (motor B). |
|
|
195
|
+
| `extra` | Map<String,Object> | `{}` | não | Mapa de extensão **livre**. Persistido, mas **nenhuma chave é consumida** pelo motor de cálculo. |
|
|
196
|
+
|
|
197
|
+
Campos do schema **não consumidos em runtime**: `extra` (apenas armazenado).
|
|
198
|
+
Não há campos computados persistidos no documento raiz.
|
|
199
|
+
|
|
200
|
+
### 3.2 `Operation` — fórmula de cálculo
|
|
201
|
+
|
|
202
|
+
| Campo | Tipo | Obrigatório | Descrição |
|
|
203
|
+
| --- | --- | --- | --- |
|
|
204
|
+
| `type` | int | sim | Tipo de operação (enum abaixo). |
|
|
205
|
+
| `achievement_type` | int | quando `type=3` | Tipo de conquista (enum abaixo). |
|
|
206
|
+
| `item` | String | depende | `type=3`: `_id` do ponto/conquista; `type=1/2/4`: `_id` da action. |
|
|
207
|
+
| `attribute` | String | quando `type=2/4` | Nome do atributo numérico da action a somar/mediar. **É `attribute`, NÃO `kpi`.** |
|
|
208
|
+
| `filters` | List<`Filter`> | não | Filtros sobre `attributes.*` da action (ver 3.4). |
|
|
209
|
+
| `sort` | int | não | `-1` = maior primeiro (desc), `1` = menor primeiro (asc). Qualquer valor ≠ 1 vira `-1`. |
|
|
210
|
+
| `sub` | boolean | não | Habilita **sub-rankings** por atributo de action. Ver correção em 3.7. |
|
|
211
|
+
| `subAttribute` | String | quando `sub=true` | Atributo da action que define o sub-ranking (ex.: `restaurante`). |
|
|
212
|
+
| `prepared` | String | quando `type=10` | `_id` de um `challenge_rule_prepared` (pipeline de agregação). |
|
|
213
|
+
|
|
214
|
+
**`operation.type` (enum, `Operation.java`):**
|
|
215
|
+
|
|
216
|
+
| Valor | Constante | Significado | Coleção agregada |
|
|
217
|
+
| --- | --- | --- | --- |
|
|
218
|
+
| `1` | TYPE_COUNT_ACTIONS | Conta execuções da action | `action_log` |
|
|
219
|
+
| `2` | TYPE_SUM_ATTRIBUTE | Soma `attributes.<attribute>` da action | `action_log` |
|
|
220
|
+
| `3` | TYPE_SUM_ACHIEVEMENTS | Soma o `total` de conquistas | `achievement` |
|
|
221
|
+
| `4` | TYPE_AVG_ATTRIBUTE | Média `attributes.<attribute>` | `action_log` |
|
|
222
|
+
| `10` | TYPE_PREPARED_KPI | Executa pipeline `prepared` | `prepared.entity` |
|
|
223
|
+
|
|
224
|
+
> ⚠️ `type=4` (média) **não é uniformemente suportado** — ver seção 7.
|
|
225
|
+
|
|
226
|
+
**`operation.achievement_type` (usado direto como `type` no match de `achievement`; aceita todo o enum de `Achievement`):**
|
|
227
|
+
|
|
228
|
+
| Valor | Significado |
|
|
229
|
+
| --- | --- |
|
|
230
|
+
| `-1` | ACHIEVEMENT_TYPE_NONE — não filtra por tipo |
|
|
37
231
|
| `0` | Point |
|
|
38
232
|
| `1` | Challenge |
|
|
39
233
|
| `2` | Catalog Item |
|
|
40
234
|
| `3` | Level |
|
|
235
|
+
| `4` | Crown |
|
|
41
236
|
| `5` | Lottery |
|
|
237
|
+
| `6` | Mystery Box |
|
|
238
|
+
| `7` | Character Star Stat |
|
|
239
|
+
| `8` | Bonus |
|
|
42
240
|
| `9` | Competition |
|
|
241
|
+
| `50` | Lottery Ticket |
|
|
242
|
+
| `99` | Custom |
|
|
243
|
+
|
|
244
|
+
Legado: a constante `subType` está comentada no código (`Operation.java:38`) — funcionalidade não existe.
|
|
245
|
+
|
|
246
|
+
### 3.3 `Period` — janela de tempo
|
|
43
247
|
|
|
44
|
-
|
|
248
|
+
| Campo | Tipo | Descrição |
|
|
249
|
+
| --- | --- | --- |
|
|
250
|
+
| `type` | int | `0` = Variable (relativa), `1` = Fixed (intervalo). |
|
|
251
|
+
| `timeAmount` | int | Quantidade de unidades (variable). |
|
|
252
|
+
| `timeScale` | int | Escala (variable): `4`=Hour, `5`=Day, `6`=Week, `7`=Month, `8`=Year (`TimeScale`). |
|
|
253
|
+
| `startDate` | Date | Início (fixed). |
|
|
254
|
+
| `endDate` | Date | Fim (fixed). |
|
|
255
|
+
| `expression` | String | Expressão keyword (v3) ex.: `-0M-;+3d` (start;end). |
|
|
45
256
|
|
|
46
|
-
|
|
47
|
-
|
|
257
|
+
Resolução de faixa (`Period.getCurrentRange`): **`expression` tem prioridade** (se `length > 5`), depois `Variable`, depois `Fixed`.
|
|
258
|
+
⚠️ **`expression` só é honrada pelo Motor B e por `getCurrentRange`. O Motor A (`/leaders`) ignora `expression`** e trata apenas `Variable`/`Fixed` (`CrowningManager.java:225`).
|
|
48
259
|
|
|
49
|
-
###
|
|
260
|
+
### 3.4 `Filter` — filtro de atributo de action
|
|
50
261
|
|
|
51
|
-
|
|
|
52
|
-
|
|
53
|
-
|
|
|
54
|
-
| `
|
|
262
|
+
| Campo | Tipo | Descrição |
|
|
263
|
+
| --- | --- | --- |
|
|
264
|
+
| `param` | String | Nome do atributo. Aplicado como `attributes.<param>`. |
|
|
265
|
+
| `operator` | int | Operador (tabela abaixo). |
|
|
266
|
+
| `value` | String | Valor (numérico se parseável, senão string). |
|
|
55
267
|
|
|
56
|
-
|
|
268
|
+
`operator` — **somente estes são tratados** por `buildJsonFilter` (`CrowningManager.java:1279`):
|
|
57
269
|
|
|
58
|
-
| Valor |
|
|
59
|
-
|
|
60
|
-
| `
|
|
61
|
-
| `
|
|
270
|
+
| Valor | Operador | Mongo |
|
|
271
|
+
| --- | --- | --- |
|
|
272
|
+
| `1` | Equal | igualdade direta |
|
|
273
|
+
| `3` | Regex (Contains) | `$regex` |
|
|
274
|
+
| `4` | Greater Than | `$gt` |
|
|
275
|
+
| `5` | Greater Than or Equal | `$gte` |
|
|
276
|
+
| `6` | Less Than | `$lt` |
|
|
277
|
+
| `7` | Less Than or Equal | `$lte` |
|
|
62
278
|
|
|
63
|
-
|
|
279
|
+
> ⚠️ Operadores existentes em `Operator` mas **não tratados** (`2`=Like, `8`=Not Equal, `9`=Exist, `10`=Not Exist, `20`=Max Distance, `40`=Mod) geram uma cláusula JSON malformada (`{attributes.<param>: }`) que **quebra o parse do pipeline**. Ver seção 7.
|
|
280
|
+
> Filtros **não são aplicados** quando `operation.type=3` (Sum achievements) — o branch de achievements não anexa `buildJsonFilter`.
|
|
64
281
|
|
|
65
|
-
|
|
66
|
-
|---|---|---|
|
|
67
|
-
| `0` | Variable | Janela relativa: últimos N períodos (ex: última semana) |
|
|
68
|
-
| `1` | Fixed | Intervalo fixo com data de início e fim |
|
|
282
|
+
### 3.5 `Leader` — registro calculado (coleção `leader`, somente leitura)
|
|
69
283
|
|
|
70
|
-
|
|
284
|
+
Saída do cálculo; **não é criado manualmente**.
|
|
71
285
|
|
|
72
|
-
|
|
|
73
|
-
|
|
74
|
-
| `
|
|
75
|
-
| `
|
|
76
|
-
| `
|
|
77
|
-
| `
|
|
78
|
-
| `
|
|
286
|
+
| Campo | Tipo | Descrição |
|
|
287
|
+
| --- | --- | --- |
|
|
288
|
+
| `_id` | String | Motor A: GUID. Motor B: composto `"<principalId>_<cache>"`. |
|
|
289
|
+
| `boardId` | String | `_id` do leaderboard. |
|
|
290
|
+
| `player` | String | Id do jogador ou da equipe. |
|
|
291
|
+
| `name` | String | Nome do jogador/equipe. |
|
|
292
|
+
| `image` | String | URL da imagem. Varia por branch: player ~ `image.medium` (Motor A) / `image.small` (Motor B); equipes e paths `Prepared KPI` usam `image.original`. |
|
|
293
|
+
| `position` | int | Posição atual (1-based). |
|
|
294
|
+
| `total` | **long** | Valor agregado. ⚠️ Tipo inteiro — ver nota. |
|
|
295
|
+
| `previous_position` | int | Posição na janela anterior (`0` se ausente). |
|
|
296
|
+
| `previous_total` | long | Total na janela anterior. |
|
|
297
|
+
| `move` | String | `up` / `down` / `stop`. |
|
|
298
|
+
| `sub` | String | Valor do sub-ranking (quando aplicável). |
|
|
299
|
+
| `period` | `Period` | Período do cálculo (geralmente não populado). |
|
|
300
|
+
| `extra.cache` | String | Id do `expire` que originou o registro (chave de cache). |
|
|
79
301
|
|
|
80
|
-
`
|
|
302
|
+
> **Nota sobre `total` (long):** `$sum`/`$avg` sobre atributos decimais retornam `double`, mas `Leader.total` é `long`. O comportamento exato na desserialização (truncamento) **não foi verificado** — valide para casos com valores fracionários, especialmente `Avg attribute`.
|
|
81
303
|
|
|
82
|
-
###
|
|
304
|
+
### 3.6 `LeaderHeader` — metadados do cálculo (coleção `leader_header`)
|
|
83
305
|
|
|
84
|
-
|
|
|
85
|
-
|
|
86
|
-
| `
|
|
87
|
-
| `
|
|
88
|
-
| `
|
|
89
|
-
| `
|
|
90
|
-
| `
|
|
91
|
-
| `
|
|
306
|
+
| Campo | Tipo | Descrição |
|
|
307
|
+
| --- | --- | --- |
|
|
308
|
+
| `_id` | String | = `expire.id`. |
|
|
309
|
+
| `board` | String | Id do leaderboard/crown. |
|
|
310
|
+
| `creation_datetime` | String | Quando foi calculado (ISO zoned). |
|
|
311
|
+
| `expiration_datetime` | String | Quando expira/recalcula. |
|
|
312
|
+
| `start_datetime` | String | Início da faixa de registros usada. |
|
|
313
|
+
| `end_datetime` | String | Fim da faixa. |
|
|
314
|
+
| `unit` | Map | Unidade do valor (ponto/action). Ausente para `type=4` e `type=10`. |
|
|
92
315
|
|
|
93
|
-
|
|
316
|
+
### 3.7 Correção: `sub`/`subAttribute` ≠ saldo líquido
|
|
94
317
|
|
|
95
|
-
|
|
318
|
+
⚠️ Documentações anteriores descreviam `operation.sub=true` como "saldo líquido — desconta débitos (GT75)". **Isto está incorreto.** No código, `sub`+`subAttribute` habilitam **sub-rankings filtrados por um atributo da action** (ex.: ranking por restaurante). O filtro é aplicado em `getSubValueFilter` / nos helpers, combinando `subAttribute` (config) com o query param `sub` (valor). As classes `Lost`/`LostDebit`/`LostManager` **não têm nenhuma relação** com leaderboard.
|
|
319
|
+
|
|
320
|
+
### 3.8 Técnica de jogo (`techniques`)
|
|
321
|
+
|
|
322
|
+
- Código relevante: **`GT03`** (LEADERBOARD_CODE).
|
|
323
|
+
- **Não é obrigatório no insert** e `addLeaderboard` não o define nem valida.
|
|
324
|
+
- É preenchido automaticamente por `GameTechniqueManager.autoConfigureMissingTechniqueFields()` (rotina de manutenção): boards sem `techniques` recebem `["GT03"]`. Serve para categorização/mapa de técnicas no Studio.
|
|
325
|
+
|
|
326
|
+
---
|
|
327
|
+
|
|
328
|
+
## 4. Endpoints
|
|
329
|
+
|
|
330
|
+
Todos exigem `Authorization: Bearer <token>` (via `AuthBean`).
|
|
331
|
+
|
|
332
|
+
### `POST /v3/leaderboard`
|
|
333
|
+
|
|
334
|
+
| Aspecto | Detalhe |
|
|
335
|
+
| --- | --- |
|
|
336
|
+
| Finalidade | Cria ou substitui um leaderboard |
|
|
337
|
+
| Full replace ou patch | **Full replace** (`c.save`) — envie o objeto completo |
|
|
338
|
+
| Comportamento real | Gera `_id` se ausente; **purga** `leader`/`expire`/`leader_header` do board; resposta sem campos nulos; status `201` |
|
|
96
339
|
|
|
97
340
|
```json
|
|
98
341
|
{
|
|
99
|
-
"title": "
|
|
100
|
-
"description": "Jogadores com mais XP na semana",
|
|
342
|
+
"title": "General",
|
|
101
343
|
"principalType": 0,
|
|
102
|
-
"operation": {
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
"item": "xp",
|
|
106
|
-
"sort": -1,
|
|
107
|
-
"sub": false,
|
|
108
|
-
"filters": []
|
|
109
|
-
},
|
|
110
|
-
"period": { "type": 0, "timeAmount": 1, "timeScale": 6 },
|
|
111
|
-
"techniques": ["GT03"]
|
|
344
|
+
"operation": { "type": 3, "achievement_type": 0, "item": "xp", "sort": -1, "sub": false, "filters": [] },
|
|
345
|
+
"period": { "type": 0, "timeAmount": 1, "timeScale": 7 },
|
|
346
|
+
"principals": []
|
|
112
347
|
}
|
|
113
348
|
```
|
|
114
349
|
|
|
115
|
-
###
|
|
350
|
+
### `DELETE /v3/leaderboard/{id}`
|
|
351
|
+
|
|
352
|
+
Remove o board e purga as 3 coleções auxiliares. Status `204`.
|
|
353
|
+
|
|
354
|
+
### `GET /v3/leaderboard`
|
|
355
|
+
|
|
356
|
+
Lista/filtra leaderboards (pipeline de agregação).
|
|
357
|
+
|
|
358
|
+
| Param | Tipo | Descrição |
|
|
359
|
+
| --- | --- | --- |
|
|
360
|
+
| `id` | String | Filtra por `_id` |
|
|
361
|
+
| `title` | String | Filtra por título (`$regex`, case-insensitive) |
|
|
362
|
+
| `type` | String | `"team"` → `principalType=1`; qualquer outro → `0` |
|
|
363
|
+
| `fields` | String | Projeção: campos separados por vírgula (`$project`) |
|
|
364
|
+
| `orderby` | String | Campo de ordenação (injetado direto no `$sort` — ver seção 8) |
|
|
365
|
+
| `reverse` | boolean | `true` → ordem descendente |
|
|
366
|
+
| `max_results` | int | Limite (default `100`) |
|
|
367
|
+
|
|
368
|
+
### `GET /v3/leaderboard/{id}`
|
|
369
|
+
|
|
370
|
+
Retorna a configuração do board (sem campos nulos).
|
|
371
|
+
|
|
372
|
+
### `GET /v3/leaderboard/{id}/leaders` — **Motor A**
|
|
373
|
+
|
|
374
|
+
| Aspecto | Detalhe |
|
|
375
|
+
| --- | --- |
|
|
376
|
+
| Finalidade | Retorna o ranking calculado (com cache, triggers `before/after_render`) |
|
|
377
|
+
| Comportamento real | Recalcula se cache expirou; TTL default `+1h`; ignora `period.expression` |
|
|
378
|
+
|
|
379
|
+
| Query param | Tipo | Descrição |
|
|
380
|
+
| --- | --- | --- |
|
|
381
|
+
| `player` | String | Jogador de referência (`"me"` resolve do token); obrigatório para `view=1/2` |
|
|
382
|
+
| `team` | String | Restringe a membros de uma equipe |
|
|
383
|
+
| `sub` | String | Valor do sub-ranking |
|
|
384
|
+
| `time_amount` + `time_scale` | int | Período variável que sobrescreve o default |
|
|
385
|
+
| `start_date` + `end_date` | String (RFC 3339 ou keyword) | Período fixo |
|
|
386
|
+
| `max_results` | int | Limite (default `100`) |
|
|
387
|
+
| `view` | int | `0`=Default, `1`=Middle (vizinhos), `2`=Friends |
|
|
116
388
|
|
|
117
389
|
```json
|
|
118
390
|
{
|
|
119
|
-
"
|
|
120
|
-
"
|
|
121
|
-
"
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
"
|
|
127
|
-
|
|
128
|
-
},
|
|
129
|
-
"period": { "type": 0, "timeAmount": 1, "timeScale": 7 },
|
|
130
|
-
"techniques": ["GT03"]
|
|
391
|
+
"leaderboard": { "_id": "...", "title": "...", "operation": {...} },
|
|
392
|
+
"creation_datetime": "2026-05-20T10:00:00-03:00",
|
|
393
|
+
"expiration_datetime": "2026-05-20T11:00:00-03:00",
|
|
394
|
+
"start_datetime": "2026-05-13T00:00:00-03:00",
|
|
395
|
+
"end_datetime": "2026-05-20T23:59:59-03:00",
|
|
396
|
+
"operation_unit": { "type": "point", "item": "xp" },
|
|
397
|
+
"leaders": [
|
|
398
|
+
{ "player": "ana@x.com", "name": "Ana", "position": 1, "total": 100, "previous_position": 2, "previous_total": 80, "move": "up" }
|
|
399
|
+
]
|
|
131
400
|
}
|
|
132
401
|
```
|
|
133
402
|
|
|
134
|
-
###
|
|
403
|
+
### `POST /v3/leaderboard/{id}/leader/aggregate` — **Motor B**
|
|
404
|
+
|
|
405
|
+
| Aspecto | Detalhe |
|
|
406
|
+
| --- | --- |
|
|
407
|
+
| Finalidade | Retorna o ranking permitindo **estágios de agregação extras** no corpo |
|
|
408
|
+
| Comportamento real | `live=true` calcula em tempo real; default usa/gera cache (TTL `+3h`); paginação via header `Range` |
|
|
409
|
+
|
|
410
|
+
| Query param | Tipo | Descrição |
|
|
411
|
+
| --- | --- | --- |
|
|
412
|
+
| `live` | boolean | `true` = tempo real; default = cache |
|
|
413
|
+
| `strict` | boolean | `true` = serialização via Gson |
|
|
414
|
+
| `player` | String | Referência para view (`"me"` resolve do token) |
|
|
415
|
+
| `view` | int | `0`/`1`/`2` (default/middle/friends) |
|
|
416
|
+
| `limit` | int | Limite (default `100`) |
|
|
417
|
+
| `period` | String | Expressão keyword de período |
|
|
418
|
+
| `Range` (header) | String | Paginação, ex.: `items=0-99` |
|
|
419
|
+
|
|
420
|
+
Corpo: array JSON de estágios de agregação Mongo anexados ao final do pipeline. Ex.: `[{"$match": {"total": {"$gte": 10}}}]`.
|
|
421
|
+
|
|
422
|
+
### `GET /v3/leaderboard/reset`
|
|
423
|
+
|
|
424
|
+
`CrowningManager.reset` → **dropa as coleções `expire`, `leader_header` e `leader` inteiras**. Afeta **todos** os leaderboards e crownings do tenant. Ver seção 5.
|
|
425
|
+
|
|
426
|
+
> Legado: existe um endpoint antigo `GET /2.0.0/leaderboard/reset` (`rest.engine.LeaderboardRest`) — apenas reset, API v2 obsoleta.
|
|
427
|
+
|
|
428
|
+
---
|
|
429
|
+
|
|
430
|
+
## 5. Regras de Negócio
|
|
431
|
+
|
|
432
|
+
Regras presentes no código e ausentes do schema:
|
|
433
|
+
|
|
434
|
+
- **Validação deferida.** `addLeaderboard` não valida `type`/`item`/`period`/`principalType`. Configurações inválidas são aceitas no insert e só falham (ou retornam vazio) no cálculo.
|
|
435
|
+
- **TTL de cache diverge por motor:** Motor A default `+1h`, Motor B default `+3h`. Com `board.timeout` definido, ambos usam a expressão (com `+` forçado). Operadores que consomem os dois endpoints veem janelas de atualização diferentes.
|
|
436
|
+
- **Caches separados por motor.** As chaves de `expire.extra.reference` são diferentes (`leaderboard_<id>_<sub>_<period>` no A; `leaderboard_<id>[_<period>]` no B). Recalcular por um endpoint não atualiza o cache do outro.
|
|
437
|
+
- **Somente jogadores registrados aparecem** (Motor A): líderes sem `Player` correspondente são descartados.
|
|
438
|
+
- **Equipes são excluídas de rankings de player** (`$nin` sobre todos os `teamId`).
|
|
439
|
+
- **`principals` é ignorado em rankings de equipe** (`linkedTeams` vazio → todas as equipes).
|
|
440
|
+
- **Filtros não se aplicam a `Sum achievements`** (type 3).
|
|
441
|
+
- **Sub-ranking** exige `operation.sub=true` **e** `operation.subAttribute` **e** o query param `sub`; o tipo do valor é convertido conforme o tipo do atributo da action (number/boolean/string).
|
|
442
|
+
- **Ordenação com desempate:** empates em `total` são desempatados pelo `time` mais recente (`max:1`).
|
|
443
|
+
- **Isolamento multi-tenant:** toda operação resolve o cliente por `apiKey` (`FrontController.getInstance(apiKey)`), apontando para o banco do tenant.
|
|
444
|
+
- **Consistência eventual:** o ranking exibido é o do último cálculo; mudanças em ações/conquistas só refletem após expiração do cache ou `reset`.
|
|
445
|
+
|
|
446
|
+
---
|
|
447
|
+
|
|
448
|
+
## 6. Comportamentos Automáticos
|
|
449
|
+
|
|
450
|
+
| Comportamento | Trigger | Impacto | Persistência |
|
|
451
|
+
| --- | --- | --- | --- |
|
|
452
|
+
| Geração de `_id` | POST sem `_id` | `Guid.shortTimeMillis()` | Sim |
|
|
453
|
+
| Purga de cache | POST/DELETE do board | Remove `leader`/`expire`/`leader_header` do board | Sim (remoção) |
|
|
454
|
+
| Backfill de técnica | `autoConfigureMissingTechniqueFields()` (manutenção) | Adiciona `GT03` a boards sem `techniques` | Sim |
|
|
455
|
+
| Trigger `before_render` | Cache expirado, **Motor A** | Executa triggers da coleção `leaderboard` antes do cálculo | Conforme trigger |
|
|
456
|
+
| Trigger `after_render` | Após recálculo, **Motor A** | Executa triggers depois do cálculo | Conforme trigger |
|
|
457
|
+
| Recálculo + cache | Cache expirado, qualquer motor | Materializa `leader`, grava `expire`/`leader_header` | Sim |
|
|
458
|
+
| Cálculo de `move` | Cada cálculo | Compara posição atual vs anterior (`up`/`down`/`stop`) | Sim (no `leader`) |
|
|
459
|
+
| Inserção em lote | Recálculo Motor B | Insere `leader` em lotes de 5.000 | Sim |
|
|
460
|
+
|
|
461
|
+
### Fluxo de behaviors automáticos (recálculo)
|
|
462
|
+
|
|
463
|
+
```mermaid
|
|
464
|
+
flowchart LR
|
|
465
|
+
A["cache expirado"] --> B{"motor?"}
|
|
466
|
+
B -- "A (/leaders)" --> C["before_render"]
|
|
467
|
+
C --> D["calcula líderes"]
|
|
468
|
+
D --> E["salva leader/expire/header"]
|
|
469
|
+
E --> F["after_render"]
|
|
470
|
+
B -- "B (aggregate)" --> G["calcula pipeline"]
|
|
471
|
+
G --> H["insere leader em lotes de 5000"]
|
|
472
|
+
```
|
|
473
|
+
|
|
474
|
+
> **Código legado/aparente bug:** em `findCacheLeadersAggregate` (`LeaderBoardManager.java:230-233`) há um bloco que **sempre** reatribui `a = aggregate({$match:{extra.cache: expire.id}})` logo após o bloco condicional. É redundante (mesmo resultado), mas chama atenção numa primeira leitura.
|
|
475
|
+
|
|
476
|
+
---
|
|
477
|
+
|
|
478
|
+
## 7. Suportado vs NÃO Suportado
|
|
479
|
+
|
|
480
|
+
### ✅ Suportado
|
|
481
|
+
|
|
482
|
+
- Rankings de **player** e **team** (motor A).
|
|
483
|
+
- Operações: Count actions (1), Sum attribute (2), Sum achievements (3), Prepared KPI (10).
|
|
484
|
+
- `Avg attribute` (4) **apenas para team via `/leaders`**.
|
|
485
|
+
- Períodos variáveis e fixos; `expression` (apenas Motor B).
|
|
486
|
+
- Filtros de atributo com operadores `1,3,4,5,6,7`.
|
|
487
|
+
- Sub-rankings por atributo de action.
|
|
488
|
+
- Views: default, middle (vizinhos), friends.
|
|
489
|
+
- Estágios de agregação customizados no corpo do `/leader/aggregate`.
|
|
490
|
+
- Cache com TTL configurável (`timeout`).
|
|
491
|
+
- Comparação com período anterior (`previous_*`, `move`).
|
|
492
|
+
|
|
493
|
+
### ❌ NÃO Suportado / quebrado
|
|
494
|
+
|
|
495
|
+
- **Rankings de equipe via `/leader/aggregate` (Motor B):** o `$match` de equipe está **hardcoded** como `{"team":{"$in":["t1","t2"]}}` em todos os branches de team (`LeaderBoardManager.java:398, 414, 490, 506, 581, 596`). Resultado: só retorna equipes literalmente chamadas `t1`/`t2` — na prática, **vazio**. Use o Motor A (`/leaders`) para equipes.
|
|
496
|
+
- **`Avg attribute` (4) para player:** `calculatePlayerLeadersHelper` (Motor A) e `findLiveLeadersAggregate` (Motor B) **não têm branch para type 4**. No Motor A, a execução chega a `a.and(...)` com `a==null` (`CrowningManager.java:921`) → **NullPointerException**. (O helper singular `calculatePlayerLeaderHelper`, que suporta avg, é usado só pelo `BonusManager`, não por leaderboard.)
|
|
497
|
+
- **`Prepared KPI` (10) sem `prepared` válido:** `a` permanece nulo → mesmo NPE/erro.
|
|
498
|
+
- **`period.expression` no Motor A (`/leaders`):** ignorada — só `Variable`/`Fixed` são tratados.
|
|
499
|
+
- **Operadores de filtro `2,8,9,10,20,40`:** não tratados em `buildJsonFilter`; produzem JSON malformado e quebram o pipeline.
|
|
500
|
+
- **Filtros em `Sum achievements` (3):** silenciosamente não aplicados.
|
|
501
|
+
- **Validação no insert:** inexistente — configs inválidas passam por POST/GET sem erro e só falham no cálculo.
|
|
502
|
+
- **`board.extra`:** persistido, mas nenhuma chave é lida pelo motor.
|
|
503
|
+
- **`description`:** persiste, mas sem getter/setter (não exposto programaticamente além do campo).
|
|
504
|
+
- **Totais decimais:** `Leader.total` é `long` — soma/média de atributos fracionários pode truncar (não verificado).
|
|
505
|
+
- **Endpoint legado `2.0.0/leaderboard`:** somente `/reset`.
|
|
506
|
+
|
|
507
|
+
---
|
|
508
|
+
|
|
509
|
+
## 8. Segurança e Permissões
|
|
510
|
+
|
|
511
|
+
- **Autenticação:** Bearer token via `AuthBean` em todos os endpoints. `player="me"` resolve o jogador do token.
|
|
512
|
+
- **Isolamento multi-tenant:** `FrontController.getInstance(apiKey)` direciona para o banco do tenant; leaderboards, leaders e cache são por tenant.
|
|
513
|
+
- **`GET /reset` é destrutivo e global:** dropa `expire`, `leader_header` e `leader` inteiras — afeta todos os rankings e crownings do tenant. Não há filtro por board.
|
|
514
|
+
- **Superfícies de injeção (concatenação de string em queries Mongo):**
|
|
515
|
+
|
|
516
|
+
| Origem | Local | Risco |
|
|
517
|
+
| --- | --- | --- |
|
|
518
|
+
| `orderby` (query param) | `{$sort:{<orderby>:...}}` (`LeaderBoardManager.java:88`) | Campo injetado cru no `$sort` |
|
|
519
|
+
| `operation.attribute` | `$sum/$avg:'$attributes.<attribute>'` (vários) | Caminho de campo injetado cru |
|
|
520
|
+
| `operation.subAttribute` + `sub` | `attributes.<subAttribute>` (`getSubValueFilter`, helpers) | Nome e valor injetados crus |
|
|
521
|
+
| `Filter.param` / `Filter.value` | `buildJsonFilter` (`CrowningManager.java:1279`) | Pode injetar operadores Mongo; valor não-numérico só é "quotado", sem sanitização |
|
|
522
|
+
| corpo de `/leader/aggregate` | estágios anexados ao pipeline | Permite agregação arbitrária sobre `leader` (escopo do tenant) |
|
|
523
|
+
| `prepared` (type 10) | pipeline armazenado em `challenge_rule_prepared` | Executa agregação arbitrária definida na config |
|
|
524
|
+
|
|
525
|
+
> Esses campos vêm de configuração administrativa (Studio) ou de tokens autenticados do tenant; ainda assim, são concatenados sem sanitização — tratar como superfície de injeção dentro do escopo do tenant.
|
|
526
|
+
|
|
527
|
+
---
|
|
528
|
+
|
|
529
|
+
## 9. Observabilidade e Troubleshooting
|
|
530
|
+
|
|
531
|
+
### Diagnóstico
|
|
532
|
+
|
|
533
|
+
1. Confirme a configuração: `GET /v3/leaderboard/{id}`.
|
|
534
|
+
2. Force recálculo do cache: `GET /v3/leaderboard/reset` (⚠️ global — derruba o cache de tudo).
|
|
535
|
+
3. Compare os dois motores: se `/leaders` retorna dados e `/leader/aggregate` não (em board de equipe), é o bug do `t1/t2`.
|
|
536
|
+
|
|
537
|
+
### Inspeção direta no MongoDB
|
|
538
|
+
|
|
539
|
+
```js
|
|
540
|
+
// configuração
|
|
541
|
+
db.leaderboard.findOne({_id: "<id>"})
|
|
542
|
+
|
|
543
|
+
// registros de cache do ranking (resultado materializado)
|
|
544
|
+
db.leader.find({boardId: "<id>"}).sort({position: 1})
|
|
545
|
+
|
|
546
|
+
// metadados/validade do cálculo
|
|
547
|
+
db.expire.find({"extra.item": "<id>"})
|
|
548
|
+
db.leader_header.find({board: "<id>"})
|
|
549
|
+
```
|
|
550
|
+
|
|
551
|
+
### Erros comuns e causas
|
|
552
|
+
|
|
553
|
+
| Sintoma | Causa provável |
|
|
554
|
+
| --- | --- |
|
|
555
|
+
| Ranking vazio em board de **equipe** via `/leader/aggregate` | Bug do filtro hardcoded `t1/t2` — use `/leaders` |
|
|
556
|
+
| `500`/NPE ao consultar `/leaders` | `operation.type=4` em board de **player**, ou `type=10` sem `prepared` válido |
|
|
557
|
+
| Pipeline quebra / erro de parse | Filtro com operador não suportado (`2,8,9,10,...`) |
|
|
558
|
+
| Período "errado" só em `/leaders` | `period.expression` ignorada nesse motor — use `Variable`/`Fixed` |
|
|
559
|
+
| Scores zerados | `operation.item` incorreto (`_id` do ponto/action, não o nome) ou `achievement_type` divergente |
|
|
560
|
+
| Resultados desatualizados | Cache ainda válido (TTL `+1h`/`+3h`); aguarde ou `reset` |
|
|
561
|
+
| Jogador não aparece | Sem `Player` registrado, ou é uma equipe num board de player, ou fora de `principals` |
|
|
562
|
+
|
|
563
|
+
### Comandos de verificação
|
|
564
|
+
|
|
565
|
+
```
|
|
566
|
+
GET /v3/leaderboard/<id>
|
|
567
|
+
GET /v3/leaderboard/<id>/leaders?max_results=10
|
|
568
|
+
POST /v3/leaderboard/<id>/leader/aggregate?live=true (body: [])
|
|
569
|
+
GET /v3/leaderboard/reset
|
|
570
|
+
```
|
|
571
|
+
|
|
572
|
+
---
|
|
573
|
+
|
|
574
|
+
## 10. Exemplos Práticos
|
|
575
|
+
|
|
576
|
+
### Exemplo mínimo — ranking de pontos por semana
|
|
135
577
|
|
|
136
578
|
```json
|
|
137
579
|
{
|
|
138
|
-
"title": "
|
|
580
|
+
"title": "Top XP - Semana",
|
|
139
581
|
"principalType": 0,
|
|
140
|
-
"operation": {
|
|
141
|
-
|
|
142
|
-
"item": "sell",
|
|
143
|
-
"sort": -1,
|
|
144
|
-
"filters": []
|
|
145
|
-
},
|
|
146
|
-
"period": { "type": 0, "timeAmount": 1, "timeScale": 5 },
|
|
147
|
-
"techniques": ["GT03"]
|
|
582
|
+
"operation": { "type": 3, "achievement_type": 0, "item": "xp", "sort": -1 },
|
|
583
|
+
"period": { "type": 0, "timeAmount": 1, "timeScale": 6 }
|
|
148
584
|
}
|
|
149
585
|
```
|
|
150
586
|
|
|
151
|
-
###
|
|
587
|
+
### Exemplo avançado — sub-ranking por atributo, com filtro e timeout
|
|
588
|
+
|
|
589
|
+
Ranking de receita (`Sum attribute`) por loja, somente vendas acima de 100, cache de 30 min:
|
|
152
590
|
|
|
153
591
|
```json
|
|
154
592
|
{
|
|
155
|
-
"title": "
|
|
593
|
+
"title": "Receita por Loja",
|
|
156
594
|
"principalType": 0,
|
|
595
|
+
"timeout": "30m",
|
|
157
596
|
"operation": {
|
|
158
597
|
"type": 2,
|
|
159
|
-
"item": "
|
|
160
|
-
"
|
|
598
|
+
"item": "venda",
|
|
599
|
+
"attribute": "valor",
|
|
600
|
+
"sub": true,
|
|
601
|
+
"subAttribute": "loja",
|
|
161
602
|
"sort": -1,
|
|
162
|
-
"filters": [
|
|
603
|
+
"filters": [
|
|
604
|
+
{ "param": "valor", "operator": 5, "value": "100" }
|
|
605
|
+
]
|
|
163
606
|
},
|
|
164
|
-
"period": { "type": 0, "timeAmount": 1, "timeScale":
|
|
165
|
-
"techniques": ["GT03"]
|
|
607
|
+
"period": { "type": 0, "timeAmount": 1, "timeScale": 7 }
|
|
166
608
|
}
|
|
167
609
|
```
|
|
168
610
|
|
|
169
|
-
|
|
611
|
+
Consulta de um sub-ranking específico:
|
|
170
612
|
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
### Criar / Atualizar
|
|
175
|
-
**Método:** POST — `/v3/leaderboard`
|
|
613
|
+
```
|
|
614
|
+
GET /v3/leaderboard/<id>/leaders?sub=Loja-Centro&max_results=20
|
|
615
|
+
```
|
|
176
616
|
|
|
177
|
-
###
|
|
178
|
-
**Método:** DELETE — `/v3/leaderboard/:id`
|
|
617
|
+
### Exemplo de equipe — usar o motor correto
|
|
179
618
|
|
|
180
|
-
|
|
181
|
-
|
|
619
|
+
```json
|
|
620
|
+
{
|
|
621
|
+
"title": "Top Equipes - Mês",
|
|
622
|
+
"principalType": 1,
|
|
623
|
+
"operation": { "type": 3, "achievement_type": 0, "item": "xp", "sort": -1 },
|
|
624
|
+
"period": { "type": 0, "timeAmount": 1, "timeScale": 7 }
|
|
625
|
+
}
|
|
626
|
+
```
|
|
182
627
|
|
|
183
|
-
|
|
184
|
-
**Método:** GET — `/v3/leaderboard/reset`
|
|
628
|
+
Consulta (✅ use `/leaders`; ❌ não use `/leader/aggregate` para equipes):
|
|
185
629
|
|
|
186
|
-
|
|
630
|
+
```
|
|
631
|
+
GET /v3/leaderboard/<id>/leaders
|
|
632
|
+
```
|
|
187
633
|
|
|
188
|
-
-
|
|
189
|
-
- **Scores errados**: confirme `operation.type` e `achievement_type` — usar `type: 3` com `achievement_type: 0` é o caminho para pontos
|
|
190
|
-
- **Período incorreto**: `timeScale: 6` = semana, `timeScale: 7` = mês — fácil de confundir
|
|
191
|
-
- **Cache desatualizado**: use `GET /v3/leaderboard/reset` para forçar recálculo
|
|
634
|
+
### Anti-pattern — o que NÃO fazer
|
|
192
635
|
|
|
193
|
-
|
|
636
|
+
```json
|
|
637
|
+
{
|
|
638
|
+
"title": "Média de Vendas (player)",
|
|
639
|
+
"principalType": 0,
|
|
640
|
+
"operation": { "type": 4, "item": "venda", "attribute": "valor", "sort": -1 },
|
|
641
|
+
"period": { "type": 0, "timeAmount": 1, "timeScale": 6 }
|
|
642
|
+
}
|
|
643
|
+
```
|
|
194
644
|
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
-
|
|
198
|
-
-
|
|
199
|
-
-
|
|
200
|
-
-
|
|
645
|
+
❌ `type=4` (Avg) em board de **player** causa **NullPointerException** ao consultar `/leaders` (não há branch para média no helper de jogadores). Alternativas: usar `Sum attribute` (2), ou um `Prepared KPI` (10) que calcule a média, ou aplicar a média via estágio extra no `/leader/aggregate`.
|
|
646
|
+
|
|
647
|
+
Outros anti-patterns:
|
|
648
|
+
- Usar `"kpi"` em vez de `"attribute"` para somar atributo — o campo correto é **`attribute`** (`kpi` é ignorado).
|
|
649
|
+
- Esperar que `/leader/aggregate` traga equipes — está quebrado (`t1/t2`).
|
|
650
|
+
- Confiar em `operation.sub` para "saldo líquido" — `sub` é sub-ranking por atributo, não desconto de débitos.
|
|
651
|
+
- Usar operador de filtro `2`/`8` — quebra o pipeline.
|
|
652
|
+
|
|
653
|
+
---
|
|
654
|
+
|
|
655
|
+
## Checklist de Configuração
|
|
656
|
+
|
|
657
|
+
- [ ] `principalType` definido (`0` player, `1` team).
|
|
658
|
+
- [ ] `operation.type` válido para o motor que você vai consultar (evite `type=4` em player).
|
|
659
|
+
- [ ] `operation.item` = `_id` do ponto/action (não o nome amigável).
|
|
660
|
+
- [ ] `operation.type=3` → `achievement_type` correto (ex.: `0` para pontos).
|
|
661
|
+
- [ ] `operation.type=2/4` → use **`attribute`** (não `kpi`).
|
|
662
|
+
- [ ] `operation.type=10` → `prepared` aponta para um `challenge_rule_prepared` existente.
|
|
663
|
+
- [ ] `period.type` + (`timeAmount`/`timeScale`) ou (`startDate`/`endDate`) preenchidos. `expression` só funciona no `/leader/aggregate`.
|
|
664
|
+
- [ ] Filtros usam apenas operadores `1,3,4,5,6,7`.
|
|
665
|
+
- [ ] Sub-ranking: `sub=true` **e** `subAttribute` definidos; passe o valor em `?sub=`.
|
|
666
|
+
- [ ] Para equipes, consulte por **`/leaders`** (não `/leader/aggregate`).
|
|
667
|
+
- [ ] Ciente de que POST faz **full replace** e **purga o cache** do board.
|
|
668
|
+
- [ ] Ciente de que `GET /reset` é **global e destrutivo**.
|
|
669
|
+
- [ ] Armadilha: omitir `timeout` deixa TTL em `+1h` (`/leaders`) ou `+3h` (`/leader/aggregate`).
|